tiistai 10. helmikuuta 2015

Initializing STM32F4 LCD-TFT (LTDC) controller

(Update: there is also slightly updated version of this available now)

Last time I referred quickly to STM32F4 Discovery board and its display. Now for everyone's pleasure here is the source for getting that display to work, in quick'n'dirty way. Note that I won't be handing you fully functional copy-pasteable code here, some things like debug output (through USART) and GPIO setup code are left out. Former you don't most likely need, and latter should be relatively trivial.

Since the board I'm planning on building will not have external RAM, I chose to go for minimal memory footprint and set up the display for 320x240 L8 (indexed RGB, 8 bits per pixel; 76800 bytes total) mode. This leaves enough memory for other goodies and even another display layer if becomes necessary. It has some other annoying limitations though, but more of them in later posts...

So, the code. First includes and display module definitions, along with frame buffer data.

[ EDIT: When I started using different display module I found out that the timing information here (both the defines here, and the initialization code below) is somewhat incorrect in general case, although it kinda sorta works for this specific display. I will try to post corrected (and more easily readable) data/code for included display when I find time to return to it; currently I'm a bit busy with the new one... ]

#include "stm32f4_discovery.h"
#include "stm32f4xx_conf.h"

 // Display module timing configuration. These are different for 
 // each display module; see module's datasheet for details.
#define DISP_WIDTH 240
#define DISP_HEIGHT 320
#define DISP_HSYNC_W  10
#define DISP_VSYNC_H  2
#define DISP_ACCUM_HORIZ_BACKPORCH  29
#define DISP_ACCUM_VERT_BACKPORCH  3
#define DISP_ACCUM_ACTIVE_W 269
#define DISP_ACCUM_ACTIVE_H 323
#define DISP_TOTALW 279
#define DISP_TOTALH 327


Highest level display initization routines. You should only need to call dispInit() in your main to get some data shown.


#define LTDC_CFG_PINCOUNT 22
const int ltdc_lcd_pins[] =
    // port AF pin. For some reason LCD has pins in both AF9 (alternate function 9) 
    // and AF14 slots.
  { PORTA | 3 | (14<<8), PORTA | 4 | (14<<8), PORTA | 6  | (14<<8), 
    PORTA | 11  | (14<<8), PORTA | 12  | (14<<8),
    PORTB | 0 | (9<<8),  PORTB | 1 | (9<<8),
    PORTB | 8 | (14<<8), PORTB | 9 | (14<<8), PORTB | 10 | (14<<8), 
    PORTB | 11 | (14<<8),PORTC | 6 | (14<<8), PORTC | 7 | (14<<8), 
    PORTC | 10 | (14<<8),PORTD | 3 | (14<<8), PORTD | 6 | (14<<8), 
    PORTF | 10 | (14<<8), PORTG | 6 | (14<<8), PORTG | 7 | (14<<8), 
    PORTG | 10 | (9<<8), PORTG | 11 | (14<<8), PORTG | 12 | (9<<8) };
   // some pins for easier probing;
   // PC2  = CS
   // PD13 = CMD/DATA
   // PF9  = SDA
   // PF7  = CLK
   // PA4  = vsync (active low)
   // PC6  = hsync (active low)
   // PF10 = DE (active high)
   // PG7  = dotclk
const int ltdc_spi_pins[] = // SPI pin listing; they're used in IO mode
  { PORTF | 7, PORTF | 9, PORTC | 2, PORTD | 13 };


 
void dispInit()
{
   // Enable clocks for GPIOs (pins) and devices. Last is DMA2D (Chrom-Art)
  unsigned int i;
  RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOCEN | 
     RCC_AHB1ENR_GPIODEN | RCC_AHB1ENR_GPIOFEN | RCC_AHB1ENR_GPIOGEN | (1<<23) ); 

  // Initialize pins. SPI pins are set up in IO mode, others in alternate function mode.
  for (i = 0; i < LTDC_CFG_PINCOUNT; ++i)
    { unsigned int af = (ltdc_lcd_pins[i] >> 8) & 15;
       // Set pin (3) in (PORTA) to (AF14) through GPIO controller
      ioPinSetAF(ltdc_lcd_pins[i] & (~0xff00), 0, af);
       // SPI has only four pins to set up; all push-pull output. 
      if (i < 4)
        ioPinSetOutput(ltdc_spi_pins[i], 0); 
    }

  // SPI CS high and clock low
  ioSetPin(PORTC | 2);
  ioClearPin(PORTF | 7);

  delay(10);

  dispSPIInit();

  dispLTDCInit();

}

The nasty part. Even single bit error in SPI init sequence may result nothing shown, and examples on how they should be set up are often completely missing. Like previously said I had to measure how example binary does it to get all working...

 // Display's SPI init data table.
 // Bit8 is command/data indication (0=command). 
 // Value 0x800 indicates delay (used after some commands). 
 // 0xfff is end of table.
 // Mostly these are written so first value on line is command, rest data.
static const unsigned short ltdc_init_spi_data[] = {
     0x0CA, 0x1C3, 0x108, 0x150,
     0x0CF, 0x100, 0x1C1, 0x130,
     0x0ED, 0x164, 0x103, 0x112, 0x181,
     0x0E8, 0x185, 0x100, 0x178,
     0x0CB, 0x139, 0x12C, 0x100, 0x134, 0x102,
     0x0F7, 0x120,
     0x0EA, 0x100, 0x100,
     0x0B1, 0x100, 0x11B,
     0x0B6, 0x10A, 0x1A2,
     0x0C0, 0x110,
     0x0C1, 0x110,
     0x0C5, 0x145, 0x115,
     0x0C7, 0x190,
     0x036, 0x1C8,
     0x0F2, 0x100,
     0x0B0, 0x1C2,
     0x0B6, 0x10A, 0x1A7, 0x127, 0x104,
     0x02A, 0x100, 0x100, 0x100, 0x1EF,
     0x02B, 0x100, 0x100, 0x101, 0x13F,
     0x0F6, 0x101, 0x100, 0x106,
     0x02C, 0x800,
     0x026, 0x101, 0x0E0, 0x10F, 0x129, 0x124, 0x10C, 0x10E, 0x109, 0x14E, 0x178, 
         0x13C, 0x109, 0x113, 0x105, 0x117, 0x111, 0x100,
     0x0E1, 0x100, 0x116, 0x11B, 0x104, 0x111, 0x107, 0x131, 0x133, 0x142, 0x105, 
         0x10C, 0x10A, 0x128, 0x12F, 0x10F,
     0x011, 0x800,
    0x029,
    0x02C,
     0xFFF /* END */ };



/* -----------------------------------------------------------
 * Send single SPI command to display.
 */
void dispSPIWrite(unsigned int dc, unsigned int data)
{
  ioClearPin(PORTC | 2);
  delay(2);

  ioPin(PORTD | 13, dc); // 0=cmd, 1=data
  delay(2);

  unsigned int i;
  for (i = 0; i< 8; ++i)
    {
      ioPin(PORTF | 9, (data & 0x80) );
      delay(2);
      ioSetPin(PORTF | 7);
      delay(2);
      ioClearPin(PORTF | 7);
      data <<= 1;
    }

  delay(2);
  ioSetPin(PORTC | 2);
  delay(2);
}


/* ---------------------------------------------------------
 * Send display init commands via SPI interface. GPIO pins must be 
 * enabled before this. Goes through the init table above and send data 
 * to controller. I'm using simple bit-banging interface as this is 
 * short one-time operation; no need to use full SPI peripheral for it.
 */
void dispSPIInit()
{
  unsigned int i = 0;
  unsigned int w = ltdc_init_spi_data[0];
  while (w != 0xfff)
    {
      if (w == 0x800)
        { delay(500); // delay command; around 15us should be enough 
        }
      else
        { dispSPIWrite((w & 0x100), (w & 0xff));
        }

      w = ltdc_init_spi_data[++i];
    }


}

And finally the setup of LTDC. Note that the source package I had did not have LTDC definitions in headers so I had to add them myself. If you need them too (and don't want to type them up yourself, all the information in after all in datasheets) let me know.

/* -----------------------------------------------------------------
 * Init internal LTDC controller.
 */
void dispLTDCInit()
{
  RCC->APB2ENR |= (1 << 26); // Enable LTDC clock.

  /*   Clock setup: LCD pixel clock = HSE / M * N / R / DIVR (see your 
   *   display module specs to see what it should be) Most discovery board 
   *   templates has HSE 8MHz and M at 8. This gives PLL input clock of 1 MHz.
   *
   *   Then we use values:
   *      N = 192,  R = 4,  DIVR = 8 so output freq is 6 MHz
   *
   * PLL has also Q divider, for SAI (audio), but we do not use it so it
   * it just set to something here.
   */

#define PLLSAI_N     192  /* Multiplier for input clock (which is HSE/M) */
#define PLLSAI_R     4    /* Divider 1; /4 */
#define PLLSAI_DIVR  2    /* Divider 2; 0=/2, 1=/4, 2=/8, 3=/16 */

  RCC->PLLSAICFGR = (PLLSAI_R << 28) | (4 << 24) | (PLLSAI_N << 6);
  RCC->DCKCFGR = (RCC->DCKCFGR & (~0x30000)) | (PLLSAI_DIVR << 16);    

  RCC->CR |= (1<<28); // enable PLL SAI and wait until it is ready
  unsigned int n = 0;
  while (!(RCC->CR & (1<<29)))
    { if (++n > 2000000)
        { serialWrite((unsigned char*)"pllsai fail\r"); // PLL clock did not start; failure somewhere.
          return;
        }
    }

  LTDC->GCR = 0; // disable LCD-TFT controller for re-initialisation

   // Set up LCD timing variables. At first these values may seem like black magic
   // but trust me, if you spend some time with display datasheet they become clear eventually.
   // Or just trust me on these :)
  LTDC->SSCR = ((DISP_HSYNC_W-1) << 16) | ((DISP_VSYNC_H-1) << 0);
  LTDC->BPCR = ((DISP_ACCUM_HORIZ_BACKPORCH) << 16) | ((DISP_ACCUM_VERT_BACKPORCH));
  LTDC->AWCR = (DISP_ACCUM_ACTIVE_W << 16) | (DISP_ACCUM_ACTIVE_H);
  LTDC->TWCR = (DISP_TOTALW << 16) | (DISP_TOTALH);

   // background color (23:0 : R8G8B8). Purple-ish for testing.
  LTDC->BCCR = 0xff00ff;

   // Enable layer 1. We have 320x240 (76800) bytes in internal memory in L8 (with color LUT) mode
   // Discovery board has also external memory that could be used for bigger
   // display sizes, color depths or multiple display pages.

   // From the display controller's point of view the image is 240x320 pixels, portrait mode.
   // Entire area configured for layer 1.
  LTDC->L1CR = 0; // disable for reprogramming.
  LTDC->L1WHPCR = ((DISP_ACCUM_HORIZ_BACKPORCH + DISP_WIDTH) << 16) | (DISP_ACCUM_HORIZ_BACKPORCH+1);
  LTDC->L1WVPCR = ((DISP_ACCUM_VERT_BACKPORCH + DISP_HEIGHT) << 16) | (DISP_ACCUM_VERT_BACKPORCH+1);
  LTDC->L1PFCR = 5; // Pixel format: 5=L8. 2= RGB565
  LTDC->L1CFBAR = (unsigned int)dispFrameBuff;
  LTDC->L1CFBLR = (DISP_WIDTH << 16) | (DISP_WIDTH+3); // hi:line pitch in bytes; lo:(width_in_bytes+3).
  LTDC->L1CFBLNR = DISP_HEIGHT;
  LTDC->L1CKCR = 0x000; // Black data is transparent, allowing background to be shown.

    // Set color look-up tables. Here we fill only first 128 entries with black-to-full color values.
  unsigned int i;
  for (i = 0; i < 16; ++i) 
    LTDC->L1CLUTWR = ((i+0)  << 24) + (i*0x101010); //   0-15: 16 greyscales
  for (i = 0; i < 16; ++i) 
    LTDC->L1CLUTWR = ((i+16) << 24) + (i*0x100000); //  16-31: 16 reds
  for (i = 0; i < 16; ++i) 
    LTDC->L1CLUTWR = ((i+32) << 24) + (i*0x001000); //  32-47: 16 greens
  for (i = 0; i < 16; ++i) 
    LTDC->L1CLUTWR = ((i+48) << 24) + (i*0x000010); //  48-63: 16 blues
  for (i = 0; i < 16; ++i) 
    LTDC->L1CLUTWR = ((i+64) << 24) + (i*0x101000); //  64-79: 16 yellows
  for (i = 0; i < 16; ++i) 
    LTDC->L1CLUTWR = ((i+80) << 24) + (i*0x100010); //  80-95: 16 purples
  for (i = 0; i < 16; ++i) 
    LTDC->L1CLUTWR = ((i+96) << 24) + (i*0x001010); // 96-112: 16 cyans

  LTDC->L1CR = 0x13; // Enable layer1 (0x01), CLUT (0x10) and color key (0x02)

   // Fill display buffer with some data.
  for (i = 0; i < sizeof(dispFrameBuff); ++i)
    { dispFrameBuff[i] = 7;
    }

   // White borders
  for (i = 0; i < DISP_WIDTH; ++i)
    {
      dispFrameBuff[i] = 15;
      dispFrameBuff[i+DISP_WIDTH] = 15;
      dispFrameBuff[i+(DISP_HEIGHT-1)*DISP_WIDTH] = 15;
      dispFrameBuff[i+(DISP_HEIGHT-2)*DISP_WIDTH] = 15;
    }
  for (i = 0; i < DISP_HEIGHT; ++i)
    {
      dispFrameBuff[i*DISP_WIDTH] = 15;
      dispFrameBuff[i*DISP_WIDTH+1] = 15;
      dispFrameBuff[i*DISP_WIDTH+DISP_WIDTH-1] = 15;
      dispFrameBuff[i*DISP_WIDTH+DISP_WIDTH-2] = 15;
    }

   // Color square at upper left corner
  for (i = 0; i < 16; ++i)
    { unsigned int j;
      for (j = 0; j < 16; ++j)
        { dispFrameBuff[DISP_WIDTH*2+2+ i*DISP_WIDTH*2+j*2] = i+j*16;
          dispFrameBuff[DISP_WIDTH*2+2+ i*DISP_WIDTH*2+1+j*2] = i+j*16;
          dispFrameBuff[DISP_WIDTH*3+2+ i*DISP_WIDTH*2+j*2] = i+j*16;
          dispFrameBuff[DISP_WIDTH*3+2+ i*DISP_WIDTH*2+1+j*2] = i+j*16;
        }
    }


   // Reload shadow registers to active in SRCR
  LTDC->SRCR = 1; // 1=reload immeiately; 2=reload on vertical blank

   // Enable controller.
  LTDC->GCR = 1;


}

There you go. You should now have some data on screen. Next similar quick introduction to the Chrom-Art controller so you don't have to use slow manual writing for everything.

1 kommentti:

  1. Hi, I'm using HAL drivers to configure LTDC but I can't manage to get it to work.
    With SPI everything is fine, graphics are displayed and initialization is OK. But with LTDC only garbage is shown (random horizontal colored lines). Disabling LTDC dowsn't make any difference...can you help?

    VastaaPoista