Ebay HY-MiniSTM32V LCD initialization and Adafruit GFX Library port to STM32F103

A while ago I’ve bought a HY-MiniSTM32V board from Ebay. There are two boards – a main board with the STM32F103VCT6 microcontroller and a 240×320 pixel LCD board with resistive touch screen. The LCD itself is connected to the FSMC (Flexible Static Memory Controller) and can be mapped as a memory device.

The FSMC is an embedded external memory controller that allows the STM32F10xxx microcontroller to interface with a wide range of memories, including SRAM, NOR Flash, NAND Flash and LCD modules. The suitable connection to LCD is as a NOR Flash / SRAM device.

From AN2790 – TFT LCD interfacing with the high-density STM32F10xxx FSMC:

To control a NOR Flash/SRAM memory, the FSMC provides the following features:

  • Select the bank to be used to map the NOR Flash/SRAM memory: there are four independent banks that can be used to interface with NOR Flash/SRAM/PSRAM memories, and each bank is selected using a separate Chip Select pin.
  • Enable or disable the address/data multiplexing feature.
  • Select the memory type to be used: NOR Flash/SRAM/PSRAM.
  • Define the external memory databus width: 8/16 bits.
  • Enable or disable the burst access mode for NOR Flash synchronous memories.
  • Configure the use of the wait signal: enable/disable, polarity setting and timing configuration.
  • Enable or disable the extended mode: this mode is used to access the memory with different timing configurations for read and write operations.

As the NOR Flash/PSRAM controller supports both asynchronous and synchronous memories, the user should select only the useful parameters depending on the memory characteristics.

The FSMC also provides the possibility of programming several parameters to interface correctly with the external memory. Depending on the memory type, some parameters are not used.

The STM32F10xxx FSMC has four different banks of 64 Mbytes to support NOR Flash memories/PSRAMs and similar external memories.

The external memories share the addresses, data and control signals with the controller

Each external device is accessed by means of a unique Chip Select signal, but the FSMC can gain access to only one external device at a time. Each bank is configured by means of dedicated registers including the different features and the timing parameters.

The FSMC signals used for LCD interfacing are described below:

  • FSMC [D0:D15]: FSMC databus: 16-bit width
  • FSMC NEx: FSMC Chip Select
  • FSMC NOE: FSMC Output Enable
  • FSMC NWE: FSMC Write Enable
  • FSMC Ax: one address line used to select between LCD Registers and LCD Display RAM where x can be 0 to 25

The LCD address depends on the used FSMC NOR Flash/PSRAM bank (NEx) and the selected address (Ax) to drive the LCD RS pin

In our case, we will use Bank 1 and A16 for RS, therefore the address should be:

  • In case of RS=0, the address is the address of FSMC_BANK1, which is 0x6000 0000
  • in case of RS=1, A16 will be also 1, therefore the address will become 0x6000 0000 + 0x10000, which is 0x6001 0000

However according to the reference manual section 36.4.1 NOR/PSRAM address mapping:

In case of a 16-bit external memory width, the FSMC will internally use HADDR[25:1] to generate the address for external memory FSMC_A[24:0]. Whatever the external memory width (16-bit or 8-bit), FSMC_A[0] should be connected to external memory address A[0].

Therefore in case RS=1, A16 will be A17m and the address will become 0x6000 0000 + 0x10000, which is 0x6002 0000.

This means that once the FSMC is initialized, if we’re writing a byte to the address 0x6000 0000, we’re writing it directly to the LCD, with the RS signal set to 0. If we’re writing the same byte to the address 0x6001 0000, we’re writing it directly to the LCD, with the RS signal set to 1.

In order to be able to do this, we’ll have to initialize the LCD. For this, first we have to configure all the involved GPIO-s to use the FSMC alternate function:

/**
 * Initialize the GPIOs where the LCD is connected.
 * Connections are as follows:
 * D00 - PD.14       D01 - PD.15
 * D02 - PD.00       D03 - PD.01
 * D04 - PE.07       D05 - PE.08
 * D06 - PE.09       D07 - PE.10
 * D08 - PE.11       D09 - PE.12
 * D10 - PE.13       D11 - PE.14
 * D12 - PE.15       D13 - PD.08
 * D14 - PD.09       D15 - PD.10
 * CS  - PD.07 (NE1) RS  - PD.11 (FSMC-A16)
 * RD  - PD.04 (NOE) WR  - PD.05 (NWE)
 * RST - PE.01
 *
 * Touch panel (not used for now):
 * TP_IRQ - PD06
 * TP_SCK - PB13
 * TP_SI  - PB15
 * TP_SO  - PB14
 * TP_CS  - PB12
 *
 */
void lcd_gpio_config(void) {

  GPIO_InitTypeDef GPIO_InitStructure;

  // Enable FSMC, GPIOD, GPIOE and AFIO clocks
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE);

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD |
      RCC_APB2Periph_GPIOE |
      RCC_APB2Periph_GPIOF |
      RCC_APB2Periph_GPIOG |
      RCC_APB2Periph_GPIOC |
      RCC_APB2Periph_GPIOB |
      RCC_APB2Periph_GPIOA |
      RCC_APB2Periph_AFIO, ENABLE);

  // PE.07(D4), PE.08(D5), PE.09(D6), PE.10(D7), PE.11(D8), PE.12(D9),
  // PE.13(D10), PE.14(D11), PE.15(D12)
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 |
      GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;

  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(GPIOE, &GPIO_InitStructure);

  // PD.00(D2), PD.01(D3), PD.04(RD), PD.5(WR), PD.7(CS), PD.8(D13), PD.9(D14),
  // PD.10(D15), PD.11(RS) PD.14(D0) PD.15(D1)
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7 |
      GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_14 | GPIO_Pin_15;

  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(GPIOD, &GPIO_InitStructure);

  // PE.01
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_1;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_Init(GPIOE, &GPIO_InitStructure);

  // issue RESET
  GPIO_ResetBits(GPIOE, GPIO_Pin_1);
  delay(100);
  GPIO_SetBits(GPIOE, GPIO_Pin_1 );
  delay(100);
}

Once this is complete, we need to configure the FSMC:

/**
 * Initialize the FSMC and map the LCD as memory mapped device, mapped to
 *
 * FSMC_BANK1 address of which is 0x6000 0000
 * A16 set is 0x20000 ( 0x10000 << 1, since the bus is 16bit, we have to shift
 * it to left - Reference manual 20.4.1 NOR/PSRAM address mapping:
 * 	In case of a 16-bit external memory width, the FSMC will internally use HADDR[25:1]
 * 	to generate the address for external memory FSMC_A[24:0].
 * 	Whatever the external memory width (16-bit or 8-bit), FSMC_A[0] should be connected
 * 	to external memory address A[0] )
 *
 *
 * When RS=0 -> A16=0 - command is sent to LCD (at address 0x6000 0000)
 * When RS=1 -> A16=1 - data is sent to LCD (at address 0x6000 0000+0x20000)
 *
 * Therefore:
 *     LCD CMD is mapped at 0x60000000
 *     LCD RAM is mapped at 0x60020000
 */
void lcd_FSMC_config(void) {

  FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure;
  FSMC_NORSRAMTimingInitTypeDef FSMC_NORSRAMTimingInitStructure;

  FSMC_NORSRAMTimingInitStructure.FSMC_AddressSetupTime = 5;
  FSMC_NORSRAMTimingInitStructure.FSMC_AddressHoldTime = 0;
  FSMC_NORSRAMTimingInitStructure.FSMC_DataSetupTime = 5;
  FSMC_NORSRAMTimingInitStructure.FSMC_BusTurnAroundDuration = 0x00;
  FSMC_NORSRAMTimingInitStructure.FSMC_CLKDivision = 0x00;
  FSMC_NORSRAMTimingInitStructure.FSMC_DataLatency = 0x00;
  FSMC_NORSRAMTimingInitStructure.FSMC_AccessMode = FSMC_AccessMode_A;

  FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM1;
  FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable;
  FSMC_NORSRAMInitStructure.FSMC_MemoryType = FSMC_MemoryType_SRAM;
  FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;
  FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable;
  FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;
  FSMC_NORSRAMInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable;
  FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;
  FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable;
  FSMC_NORSRAMInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable;
  FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait = FSMC_AsynchronousWait_Disable;
  FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable;
  FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
  FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &FSMC_NORSRAMTimingInitStructure;

  FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure);

  FSMC_NORSRAMTimingInitStructure.FSMC_AddressSetupTime = 5;
  FSMC_NORSRAMTimingInitStructure.FSMC_AddressHoldTime = 0;
  FSMC_NORSRAMTimingInitStructure.FSMC_DataSetupTime = 5;
  FSMC_NORSRAMTimingInitStructure.FSMC_BusTurnAroundDuration = 0x00;
  FSMC_NORSRAMTimingInitStructure.FSMC_CLKDivision = 0x00;
  FSMC_NORSRAMTimingInitStructure.FSMC_DataLatency = 0x00;
  FSMC_NORSRAMTimingInitStructure.FSMC_AccessMode = FSMC_AccessMode_A;
  FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &FSMC_NORSRAMTimingInitStructure;

  FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure);

  // Enable FSMC Bank1_SRAM Bank
  FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);
}

At this point the LCD is memory mapped to the addresses  0x6000 0000 and  0x6002 0000. However, in order to be used, it’s controller still needs an initialization sequence. The LCD controller on this board is SSD1289, and it’s initialization sequence is more or less something like the following:

void lcd_controller_init() {

  write_reg(DISPLAY_CTL, 0x0021);
  write_reg(OSCEN, 0x0001);
  write_reg(DISPLAY_CTL, 0x0023);
  write_reg(SLEEP_MODE, 0x0000); delay(30);
  write_reg(DISPLAY_CTL, 0x0033);
  write_reg(ENTRY_MODE, 0x6830);
  write_reg(LCD_DRV_AC_CTL, 0x0600);
  write_reg(OPTIMIZE_ACCESS_SPEED3, 0x6CEB);
  write_reg(PWR_CTL1, 0xA8A4);
  write_reg(PWR_CTL2, 0x0000);
  write_reg(PWR_CTL3, 0x080C);
  write_reg(PWR_CTL4, 0x2B00);
  write_reg(PWR_CTL5, 0x00B0);
  write_reg(DRV_OUT_CTL, 0x2b3F); //RGB
  write_reg(COMPARE_REG1, 0x0000);
  write_reg(COMPARE_REG2, 0x0000);
  write_reg(HORIZONTAL_PORCH, 0xEF1C);
  write_reg(VERTICAL_PORCH, 0x0103);
  write_reg(FRAME_CYCLE_CTL, 0x0000);
  write_reg(GATE_SCAN_START_POS, 0x0000);
  write_reg(VERTICAL_SCROLL_CTL1, 0x0000);
  write_reg(VERTICAL_SCROLL_CTL2, 0x0000);
  write_reg(FIRST_WINDOW_START, 0x0000);
  write_reg(FIRST_WINDOW_END, 0x013F);
  write_reg(SECOND_WINDOW_START, 0x0000);
  write_reg(SECOND_WINDOW_END ,0x0000);
  write_reg(HORIZONTAL_RAM_ADDR_POS, 0xEF00);
  write_reg(VERTICAL_RAM_ADDR_START_POS, 0x0000);
  write_reg(VERTICAL_RAM_ADDR_END_POS, 0x013F);
  write_reg(GAMA_CTL1, 0x0707);
  write_reg(GAMA_CTL2, 0x0204);
  write_reg(GAMA_CTL3, 0x0204);
  write_reg(GAMA_CTL4, 0x0502);
  write_reg(GAMA_CTL5, 0x0507);
  write_reg(GAMA_CTL6, 0x0204);
  write_reg(GAMA_CTL7, 0x0204);
  write_reg(GAMA_CTL8, 0x0502);
  write_reg(GAMA_CTL9, 0x0302);
  write_reg(OPTIMIZE_ACCESS_SPEED2, 0x12BE);
  write_reg(GAMA_CTL10, 0x0302);
  write_reg(RAM_WRITE_DATA_MASK1, 0x0000);
  write_reg(RAM_WRITE_DATA_MASK2, 0x0000);
  write_reg(FRAME_FREQUENCY, 0x8000);
  write_reg(SET_GDDRAM_X_ADDR_COUNTER, 0x0000);
  write_reg(SET_GDDRAM_Y_ADDR_COUNTER, 0x0000);

  lcd_clear(0);
}

where the various defines for registers and the functions are these:

// FSMC BANK1 SRAM
#define LCD_REG      (((volatile unsigned short *) 0x60000000))

// FSMC BANK1 SRAM with A16 set, 16bit bus: A16 -> A17)
#define LCD_RAM      (((volatile unsigned short *) 0x60020000))

// SSD1289 registers
#define OSCEN						0x00
#define DRV_OUT_CTL					0x01
#define LCD_DRV_AC_CTL				0x02
#define PWR_CTL1					0x03
#define COMPARE_REG1				0x05
#define COMPARE_REG2				0x06
#define DISPLAY_CTL					0x07
#define FRAME_CYCLE_CTL				0x0b
#define PWR_CTL2					0x0c
#define PWR_CTL3					0x0d
#define PWR_CTL4					0x0e
#define GATE_SCAN_START_POS			0x0f
#define SLEEP_MODE					0x10
#define ENTRY_MODE					0x11
#define OPTIMIZE_ACCESS_SPEED3		0x12
#define GENERIC_INTERFACE_CTL		0x15
#define HORIZONTAL_PORCH			0x16
#define VERTICAL_PORCH				0x17
#define PWR_CTL5					0x1e
#define RAM_DATA_WRITE				0x22
#define RAM_WRITE_DATA_MASK1		0x23
#define RAM_WRITE_DATA_MASK2		0x24
#define FRAME_FREQUENCY				0x25
#define VCOM_OTP					0x28
#define OPTIMIZE_ACCESS_SPEED2		0x28 /* ? */
#define GAMA_CTL1					0x30
#define GAMA_CTL2					0x31
#define GAMA_CTL3					0x32
#define GAMA_CTL4					0x33
#define GAMA_CTL5					0x34
#define GAMA_CTL6					0x35
#define GAMA_CTL7					0x36
#define GAMA_CTL8					0x37
#define GAMA_CTL9					0x3a
#define GAMA_CTL10					0x3b
#define VERTICAL_SCROLL_CTL1		0x41
#define VERTICAL_SCROLL_CTL2		0x42
#define HORIZONTAL_RAM_ADDR_POS		0x44
#define VERTICAL_RAM_ADDR_START_POS	0x45
#define VERTICAL_RAM_ADDR_END_POS	0x46
#define FIRST_WINDOW_START			0x48
#define FIRST_WINDOW_END			0x49
#define SECOND_WINDOW_START			0x4a
#define SECOND_WINDOW_END			0x4b
#define SET_GDDRAM_X_ADDR_COUNTER	0x4e
#define SET_GDDRAM_Y_ADDR_COUNTER	0x4f

// contains the current tick count
volatile uint32_t systick_counter;

/**
 * Called once each systick
 */
void SysTick_Handler() {
  systick_counter++;
}

uint32_t getSysTick_Counter() {

  return systick_counter;
}
/*
 * parameter says how many ticks to wait. With a 1khz interrupt
 * one tick equals 1ms.
 */
void delay(int ticks) {
  uint32_t ticks_start = systick_counter;

  while(systick_counter < (ticks_start + ticks)) {
    asm volatile("nop");
  }

  return;
}

/**
 * Write a CMD to the LCD controller
 */
void write_cmd(unsigned short cmd) {

  *LCD_REG = cmd;
}

/**
 * write data to the LCD controller
 */
void write_data(unsigned short data_code ) {

  *LCD_RAM = data_code;
}

/**
 * read data from the LCD controller
 */
unsigned short read_data(void) {

  return *LCD_RAM;
}

/**
 * Write a value to a given register of LCD controller
 */
void write_reg(unsigned char reg_addr,unsigned short reg_val) {

  write_cmd(reg_addr);
  write_data(reg_val);
}

/**
 * Read a specified LCD controller register value
 */
inline unsigned short read_reg(unsigned char reg_addr) {

  write_cmd(reg_addr);
  return read_data();
}

At this point the LCD is initialized and can be used for drawing. In order to draw on it, I’ve downloaded and ported the Adafruit GFX Library, which went remarcably smooth and quick, after gathering all the required files. In order to port it, a new class was required to be implemented, to extend the Adafruit_GFX class and override the initialization, pixel drawing and write to LCD:

#include <Adafruit_GFX.h>

/**
 * Prepare sequential write to LCD GRAM
 */
void lcd_start_ram_data_write(void) {

    write_cmd(RAM_DATA_WRITE);
}

class EbayLCD_GFX: public Adafruit_GFX {

public:
  EbayLCD_GFX();
  virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
  virtual void startWrite(void);
};

EbayLCD_GFX::EbayLCD_GFX()
  : Adafruit_GFX(240, 320) {
}

void EbayLCD_GFX::drawPixel(int16_t x, int16_t y, uint16_t color) {

  lcd_set_pixel(x, y, color);
}

void EbayLCD_GFX::startWrite(void) {

  lcd_start_ram_data_write();
}

After this, the LCD can be used by instantiating the EbayLCD_GFX class and calling the drawing operations through it:

void test_lcd() {

  EbayLCD_GFX gfx;

  while(true) {
    for(auto i=0;i<3000;i++) {
      auto x1 = random() % 240;
      auto y1 = random() % 320;

      auto x2 = random() % 240;
      auto y2 = random() % 320;

      auto r = random() % 0xff;
      auto g = random() % 0xff;
      auto b = random() % 0xff;


      gfx.drawLine(x1, y1, x2, y2, RGB(r,g,b));
    }

    for(auto i=0;i<3000;i++) {
      auto x1 = random() % 240;
      auto y1 = random() % 320;

      auto r = random() % 0xff;
      auto g = random() % 0xff;
      auto b = random() % 0xff;

      gfx.setCursor(x1, y1);
      gfx.setTextColor(RGB(r,g,b));
      gfx.print("Hello World");
    }

    for(auto i=0;i<120000;i++) {
      auto x1 = random() % 240;
      auto y1 = random() % 320;

      auto r = random() % 0xff;
      auto g = random() % 0xff;
      auto b = random() % 0xff;

      gfx.drawPixel(x1, y1, RGB(r,g,b));
    }

    for(auto i=0;i<1500;i++) {
      auto x1 = random() % 240;
      auto y1 = random() % 320;

      auto x2 = random() % 240;
      auto y2 = random() % 320;

      auto x3 = random() % 240;
      auto y3 = random() % 320;

      auto r = random() % 0xff;
      auto g = random() % 0xff;
      auto b = random() % 0xff;

      gfx.drawTriangle(x1, y1, x2, y2, x3, y3, RGB(r,g,b));
    }

    for(auto i=0;i<1500;i++) {
      auto x1 = random() % 240;
      auto y1 = random() % 320;

      auto rad = random() % 240;

      auto r = random() % 0xff;
      auto g = random() % 0xff;
      auto b = random() % 0xff;

      gfx.drawCircle(x1, y1, rad, RGB(r,g,b));
    }
  }
}

int main(void) {

  // 1000 interrupts per second = 1khz
  SysTick_Config(SystemCoreClock/1000);

  lcd_gpio_config();
  lcd_FSMC_config();
  lcd_controller_init();

  test_lcd();
}

Here is a short video with the code in action:

Leave a Comment