Driving controllerless Hitachi SP14Q002 QVGA (320x240)

13 Feb 2012

I had some SP14Q002 lcds for long time now, but it was not possible to make them work. The reason was that they need a controller like SED1335 for the interface with a microcontroller.

Because the lcd doesn't have any controller you constantly need to refresh it every 13.2ms or 75Hz in order to avoid flickering.

So I start trying to drive the lcd with mbed. My first try was using the mbed i/o library but the refresh rate was to slow. Then, I tried accessing the gpio registers direct and I improved the refresh rate. But still it was not possible to have the 75Hz refresh rate that the lcd needs for not flickering. Also with the above approach the cpu load is high!

I start looking for a DMA approach in order to reduce the mbed load and also to increase the refresh rate. Finally I find out the following post "DMA and fast GPIO" from Burt Hashizume http://mbed.org/forum/mbed/topic/1138/

With this approach you can get 60ns clock that was super for my application.

So here is my implementation:

The interface for this lcd is the following:

/media/uploads/tmav123/sp14q002_interface.jpg

The timing requirements are:

/media/uploads/tmav123/sp14q002_timing.jpg

/media/uploads/tmav123/sp14q002_timing1.jpg

So I setup a ticker every 55us for sending 1 line via DMA at a time in order to have a full lcd refresh every 13.2ms or 75Hz refresh rate.

I have a line update in less than 20us via DMA and a lot of cpu power to do other tasks.

/media/uploads/tmav123/sp14q002_tek.jpg

Here is a small demo that I made:

And here is the code:

#include "mbed.h"
#include "font.h"
#include "Font16x24.h"
#include <stdio.h>
#include <stdarg.h>

///////////////////////////////////////////////////////////////////////////////
#define DMA_CHANNEL_ENABLE      1
#define DMA_TRANSFER_TYPE_M2M   (0UL << 11)   
#define DMA_CHANNEL_TCIE        (1UL << 31)
#define DMA_CHANNEL_SRC_INC     (1UL << 26)
#define DMA_MASK_IE             (1UL << 14)
#define DMA_MASK_ITC            (1UL << 15)

DigitalOut myled1(LED1);
DigitalOut myled2(LED2);
DigitalOut myled3(LED3);
DigitalOut myled4(LED4);

DigitalOut frame(p26);
DigitalOut ndispoff(p25);
// p9  - P0.0 - CP
// p10 - P0.1 - LOAD
// p30 - P0.4 - D0
// p29 - P0.5 - D1
// p8  - P0.6 - D2
// p7  - P0.7 - D3

Ticker ticker;      // irq to prepare pattern for sending one line via DMA

char buffer[500];   // buffer for the line timing pattern
////////////////////////////////////////////////////////////////////////////////
#define LCD_WIDTH  320
#define LCD_HEIGTH 240

char Mem[LCD_WIDTH * LCD_HEIGTH / 8];   // lcd shadow memory
///////////////////////////////////////////////////////////////////////////////
// clear the lcd
void LCDClear(void)
{
  int i;
  for (i = 0; i < LCD_WIDTH * LCD_HEIGTH / 8; i++)
    Mem[i] = 0;
}
////////////////////////////////////////////////////////////////////////////////
// set one pixel (x:0-319, y:0-239)
void SetPixel(int x, int y)
{
 	int i;

	i = y * LCD_WIDTH / 8 + (x / 8);
 	Mem[i] |= 1 << (8 - (x % 8));
}
////////////////////////////////////////////////////////////////////////////////
// clear one pixel (x:0-319, y:0-239)
void ClearPixel(int x, int y)
{
 	int i;

	i = y * LCD_WIDTH / 8 + (x / 8);
 	Mem[i] &= ~(1 << (8 - (x % 8)));
}
////////////////////////////////////////////////////////////////////////////////
// display one character 8x8 font on lcd (x:0-39, y:0-29)
void LCD_char(int x, int y, char n)
{
 	int j, i;

	i = y * LCD_WIDTH + x;
 	for (j = 0; j < 8; j++)
	{
  	Mem[i] = FONT8x8F[n-32][j];
    i += LCD_WIDTH / 8;
  }
}
////////////////////////////////////////////////////////////////////////////////
// display a string (8x8 font) on the lcd (x:0-39, y:0-29)
void LCD_string(int x, int y, char * str)
{
 	while(*str)
	{
  	LCD_char(x++, y, *str);
  	str++;
 	}
}
////////////////////////////////////////////////////////////////////////////////
char str5[80];
// display a string (8x8 font) via printf on the lcd (x:0-39, y:0-29)
void lcd_printf(int x, int y, char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);
  vsprintf(str5, fmt, args);
	LCD_string(x, y, str5);
  va_end(args);
}
////////////////////////////////////////////////////////////////////////////////
// display one character 16x24 font on lcd (x:0-19, y:0-9)
void LCD_char16x24(int x, int y, char n)
{
 	unsigned int j, i, tmp;

	i = y * LCD_WIDTH * 3 + 2 * x;
 	for (j = 0; j < 24; j++)
	{
    tmp = Font_16x24[(n - 32) * 24 + j];
  	Mem[i] = (char)((tmp & 0xFF00) >> 8);
  	Mem[i + 1] = (char)(tmp & 0x00FF);
    i += LCD_WIDTH / 8;
  }
}
////////////////////////////////////////////////////////////////////////////////
// display a string (16x24 font) on the lcd (x:0-19, y:0-9)
void LCD_string16x24(int x, int y, char * str)
{
 	while(*str)
	{
  	LCD_char16x24(x++, y, *str);
  	str++;
 	}
}
////////////////////////////////////////////////////////////////////////////////
// display a string (16x24 font) via printf on the lcd (x:0-19, y:0-9)
void lcd_printf16x24(int x, int y, char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);
  vsprintf(str5, fmt, args);
	LCD_string16x24(x, y, str5);
  va_end(args);
}
///////////////////////////////////////////////////////////////////////////////
// DMA start setup
// copy all buffer pattern (4*80+1 bytes) to the Port0 (byte)

void DMAStart(void)
{
    myled2 = 0;
    myled3 = 0;
    myled4 = 0;
    // Prep Channel0 to send the bytes of the buffer to the P0. 
    LPC_GPDMACH0->DMACCSrcAddr  = (uint32_t)buffer;
    LPC_GPDMACH0->DMACCDestAddr = (uint32_t)&LPC_GPIO0->FIOPIN;
    LPC_GPDMACH0->DMACCLLI      = 0;
    LPC_GPDMACH0->DMACCControl  = DMA_CHANNEL_TCIE | DMA_CHANNEL_SRC_INC | (4 * 80 + 1);

    myled2 = 1;
    
    // Fire GPDMA Channel0
    LPC_GPDMACH0->DMACCConfig = DMA_CHANNEL_ENABLE | DMA_TRANSFER_TYPE_M2M | DMA_MASK_IE | DMA_MASK_ITC;
}
///////////////////////////////////////////////////////////////////////////////
// prepare the line timing pattern to send via DMA
void PreparePattern(char *buf)
{
    char tmp;
    int i;

    for (i = 0; i < 8 * 39; i += 8) 
    {
        tmp = *buf & 0xF0;
        buffer[i]   = tmp + 1;    // cp set
        buffer[i+1] = tmp + 1;    // wait for data setup
        buffer[i+2] = tmp;        // cp low shift data
        buffer[i+3] = tmp;        // cp low shift data (just a delay)

        tmp = *buf & 0x0F;
        tmp = tmp << 4;           // shift data to P0.4-P0.7
        buffer[i+4] = tmp + 1;    // cp set
        buffer[i+5] = tmp + 1;    // wait for data setup
        buffer[i+6] = tmp;        // cp low shift data
        buffer[i+7] = tmp;        // cp low shift data (just a delay)

        buf++;
    }
        tmp = *buf & 0xF0;
        buffer[i]   = tmp + 1;    // cp set
        buffer[i+1] = tmp + 1;    // wait for data setup
        buffer[i+2] = tmp;        // cp low shift data
        buffer[i+3] = tmp;        // cp low shift data  (just a delay)

        tmp = *buf & 0x0F;
        tmp = tmp << 4;           // shift data to P0.4-P0.7
        buffer[i+4] = tmp + 3;    // cp set, load set
        buffer[i+5] = tmp + 3;    // wait for data setup
        buffer[i+6] = tmp + 2;    // cp low shift data, load set
        buffer[i+7] = tmp + 2;    // cp low shift data, load set  (just a delay)
        buffer[i+8] = tmp;        // load line frame
}
///////////////////////////////////////////////////////////////////////////////
long ticker_cntr;
// irq every 55us for preparing the pattern and firing the DMA to start the transfer
void ticker_ISR(void)
{
  ticker_cntr++;    // line counter
  if (ticker_cntr >= LCD_HEIGTH)
  {
    ticker_cntr = 0;
    frame = 1;    // first line start
  }
  myled1 = 1;
  PreparePattern(&Mem[ticker_cntr * LCD_WIDTH / 8]);
  DMAStart();
  myled1 = 0;
}
///////////////////////////////////////////////////////////////////////////////
// DMA_IRQHandler on the end of the dma transfer or on error
extern "C" void DMA_IRQHandler(void) __irq 
{
    frame = 0;    // first line end
    myled2 = 1;
    if (LPC_GPDMA->DMACIntStat & 1) 
    {
        if (LPC_GPDMA->DMACIntTCStat & 1) 
        {
            myled3 = 1;    
            LPC_GPDMA->DMACIntTCClear = 1;
        }
        if (LPC_GPDMA->DMACIntErrStat & 1) 
        {
            myled4 = 1;
            LPC_GPDMA->DMACIntErrClr = 1;
        }
    }
}
///////////////////////////////////////////////////////////////////////////////
// get a normalized sine value
int getNormalizedSine(int x, int halfY, int Amplitude, int maxX) 
{
  float piDouble = 2 * 3.14;
  float factor = piDouble / maxX;
  int ret;
  ret = sin(x * factor) * Amplitude + halfY;
  return (ret);
 }
///////////////////////////////////////////////////////////////////////////////
int main() 
{
    int cntr1, cntr2, i, j, ampl;

    myled1 = 0;
    myled2 = 0;
    myled3 = 0;
    myled4 = 0;
    
    ndispoff = 1;
    frame = 1;    // first line start
    cntr1 = 0;
    cntr2 = 0;
    ampl = 0;
    LCDClear();
    ticker.attach(ticker_ISR, 0.000055);      // shift 1 line every 55us ~ 75Hz refresh rate for the complete lcd 320x240
    // set outputs for P0.0, P0.1, P0.4, P0.5, P0.6, P0.7 (used from DMA transfer)
    LPC_GPIO0->FIODIR   |=  (1UL <<  0) + (1UL <<  1) + (1UL <<  4) + (1UL <<  5) + (1UL <<  6) + (1UL <<  7);  
    // set mask only for the outputs
    LPC_GPIO0->FIOMASK = ~((1UL <<  0) +  (1UL <<  1) + (1UL <<  4) + (1UL <<  5) + (1UL <<  6) + (1UL <<  7)); 
  
    // Power up the GPDMA. 
    LPC_SC->PCONP |= (1UL << 29);
    LPC_GPDMA->DMACConfig = 1;
    LPC_GPDMA->DMACIntTCClear = 0x1;
    NVIC_EnableIRQ(DMA_IRQn);
       
    DMAStart();    
    while(1) 
    {   
//        LCDClear();
        cntr1++;

        if (cntr1 % 50 == 0)
          cntr2++;
///////// fill all lcd with the same character
        for (j = 0; j < 10; j++)
        {
          for (i = 0; i < 20; i++)
            LCD_char16x24(i, j, (cntr2 & 0x1f) + 'A');
        }
///////// display the cntr1
        lcd_printf(5, 1, "  %d  ", cntr1);
        lcd_printf16x24(5, 1, "  %d  ", cntr1);
///////// display a sine wave
        if (cntr1 % LCD_HEIGTH  < LCD_HEIGTH / 2)
          ampl++;
        else
          ampl--;
        for (i = 0; i < LCD_WIDTH; i++)
        {
          j = getNormalizedSine(i, LCD_HEIGTH / 2, ampl, LCD_WIDTH);
          SetPixel(i, j - 3);
          SetPixel(i, j - 2);
          SetPixel(i, j - 1);
          SetPixel(i, j);
          SetPixel(i, j + 1);
          SetPixel(i, j + 2);
          SetPixel(i, j + 3);
        }
        wait(0.01); 
    }
}
///////////////////////////////////////////////////////////////////////////////

13 Feb 2012

Thank you. I began to do this for another display. I recoded it to use a memory buffer instead of directly writing the data over SPI. But I just wanted to use MODDMA for the dma stuff. Didn't analyze your code completely but 1 question: Can you be sure your memory is updated completely before it gets prepared in the timer isr?

14 Feb 2012

What you are mentioning can be done with double lcd shadow buffers. This is not implemented! I have only one lcd shadow buffer that I update in the loop and in parallel the ticker_isr is firing dma to refresh line by line the lcd. For this reason in the demo you can see some letters appearing under the running counter!

But I think double buffering is easy to implement. It is only a matter of available ram.

07 Mar 2014

Hi,

Nice job.