HD44780-compartible LCD display and STM32.

HD44780-compatible LCD digital-alphabetical displays are very convenient to use in hobby projects. They exist in different sizes and are quite cheap. The most popular are 2 line modules with 16 characters per line (16×2). Also, 8х2, 16х1, 20х2, 20х4 and 40х2 modules exist.

For my “AGAT-7” retro computer project, I use a 20×4 LCD module. Most modules are designed to use a 5V power supply, but 3.3V modules also exist. They are not as popular as 5V modules and are usually twice as expensive, so I prefer to use 5V modules with transition IC if a 3.3V microcontroller is used (i.e. STM32). This IC costs less than $1 and is cheaper than 3.3V modules. LCD control RS, R/W and E pins are used. Data is transported by an 8-bit bus, but switching to the 4-bit bus is possible. In this case, each byte is sent in two steps. It means we need 7 to 11 pins to control the module. The 8-bit data bus is easier to use, has higher speed and requires less microcontroller time than the 4-bit bus.

There are also two methods of controlling the intervals between commands – timer and reading the “ready” bit. In the second method, the module uses the highest bit of the data bus as the “ready” bit. Although the first method is less time effective, it is sometimes preferable. For instance, when a 3.3V microcontroller is used, the first method allows avoiding expensive two-way “5V <-> 3.3V” level transition IC. Also, we can spare one pin with this method because R/W signal control is not required. Usually, I prefer to use the timer method with an 8-bit data bus in my projects, which requires ten microcontroller pins and a cheap voltage transition chip.

Modules can be supplied with different charsets, so if you use languages other than English in your project, it is better to ensure you buy the proper module. Also, several user characters can be programmed in the module.

Pinouts can be different in different modules, but the most common is the following:

    1. GND
    2. Vcc (+5V)
    3. Contrast setup (Vo)
    4. RS
    5. R/W
    6. Enable (E)
    7. Bit 0 (Low bit for 8-bit interface)
    8. Bit 1
    9. Bit 2
    10. Bit 3
    11. Bit 4 (low bit for 4-bit interface)
    12. Bit 5
    13. Bit 6
    14. Bit 7 (the high bit)
    15. Background light power supply for modules with it (anode)
    16. Background light power supply for modules with it (cathode)

The background light is recommended to connect through a 50-100 Ohm current limiting resistor. The contrast can be set up with a 10-20k trim pot bound as a voltage divider (A and B to 5V, W – to pin Vo).

Here is an example of HD44780 LCD module usage from one of my projects (“AGAT-7”). It is connected to an STM32L152R6 microcontroller used as a 140K floppy-drive emulator. The schematic is:

Let’s initialise the microcontroller with its usual commands. Also, let’s set up delay functions in ms and us. I used a TIM6 timer for delays, but a simple loop with measured time will also do the job.

#include "stm32l1xx.h"
#include "stm32l1xx_conf.h"
#include "stm32l1xx_gpio.h"
#include "stm32l1xx_exti.h"
#include "stm32l1xx_syscfg.h"
#include "misc.h"

void delay_us(uint16_t usv)                 // Delay in us
  TIM6->SR &= ~TIM_SR_UIF;
  TIM6->CR1 &= ~TIM_CR1_ARPE;
  TIM6->SR = 0x00;
  TIM6->CR1 |= TIM_CR1_CEN;

  while (TIM6->SR == 0)

void delay_ms(uint16_t msv)            // Delay in ms
 uint16_t i;
 for(i=0; i<msv; i++)="" delay_us(1000);="" }="" void="" stm_init()="" a="" microcontroller="" initialisation="" {=""  ="" int="" i;="" system="" clock="" rcc-="">CR|=RCC_CR_HSION;
  while (!(RCC->CR & RCC_CR_HSIRDY));             // Wait until it's stable
  RCC->CR|=RCC_CR_PLLON;                              // Turn PLL on
  while (!(RCC->CR & RCC_CR_PLLRDY));             // Wait PLL to stabilise

  //Setting up flash for high speed

  RCC->CFGR|=RCC_CFGR_SW_1 | RCC_CFGR_SW_0;           // Set PLL as SYSCLK
  RCC->CR&=~RCC_CR_MSION;                         // Turn off MSI

  //Enabling clock for GPIOB & GPIOC
  GPIOC->MODER = (uint32_t) 0x5555000;
  GPIOC->OTYPER &= (uint16_t) 0xFF;
  GPIOC->OSPEEDR = (uint16_t) 0xAAAA000;
  GPIOC->ODR = (uint32_t) 0;

  GPIOB->MODER = (uint32_t) 0x155000;
  GPIOB->ODR = (uint16_t) 0;

  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;                 // TIM6 timer clock
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

We need to “blink” with the E signal often in communication with the module, so having a separate function, “lcd_nybble“, for this task is worth it. In your project, the timing intervals may require an adjustment because of different modules.

void lcd_nybble()
 char i;
 for(i=0; i<5; i++);
 GPIOB->BSRRH=GPIO_BSRR_BS_10;         	//Clock enable: falling edge

To send a command to the module, it has to be set on the data bus and pins RS and R/W set to zero. After that, we need to “blink” with the E signal. Each command requires some time to finalise, in which the module is not accessible. So, time delays need to be used. As mentioned, a “ready” signal can be used instead. The delays need to be adjusted when different modules are used. Some commands require more time than others. My command function “lcd_command” is:

void lcd_command(uint16_t lcmnd)
  uint16_t temp_port;

  temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
  GPIOC->ODR = temp_port | (lcmnd << 6);   //put data on output Port
  GPIOB->BSRRH=GPIO_BSRR_BS_8;             //RS=LOW : send data
  GPIOB->BSRRH=GPIO_BSRR_BS_9;             //R/W=LOW : Write
  if (lcmnd < 4) delay_us(1700);           // It takes longer to complete some commands

LCD module needs to be initialised. I use the following function for this purpose. I hope that it explains itself:

void lcd_init();
  GPIOC->BSRRH = (uint16_t) 0x3FC0;                    // Reset all ports
  GPIOB->BSRRH = (uint16_t) 0x7C0;                     // Reset all ports
  delay_us(40000);                                     // Wait >15 msec after power is applied
  GPIOC->BSRRL = GPIO_BSRR_BS_10 | GPIO_BSRR_BS_11;    // put 0x30 on the output port
  delay_us(5000);                                      // must wait 5ms, busy flag not available
  lcd_nybble();                                        // command 0x30 = Wake up
  delay_us(160);                                       // must wait 160us, busy flag not available
  lcd_nybble();                                        // command 0x30 = Wake up #2
  delay_us(160);                                       // must wait 160us, busy flag not available
  lcd_nybble();                                        // command 0x30 = Wake up #3
  GPIOC->BSRRL = (uint16_t) 0xE00;
  lcd_command((uint16_t) 0x10);
  lcd_command((uint16_t) 0x0C);
  lcd_command((uint16_t) 0x06);

After this, the module is ready to work. You can change the module behaviour with the last lines of the function. For instance, a cursor can be made visible. Please read the datasheet for details.

Also, we need functions for string and character output. My versions of the commands “lcd_write” and “lcd_char” are:

void lcd_write(char ldata[])
  uint16_t temp_port;
  char k=0;

  GPIOB->BSRRL=GPIO_BSRR_BS_8;                 //RS=HIGH : send data
  GPIOB->BSRRH=GPIO_BSRR_BS_9;                 //R/W=LOW : Write

  while(ldata[k] != 0)
    temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
    GPIOC->ODR = temp_port | (ldata[k] << 6);  //put data on output Port

void lcd_char(char ldata)
  uint16_t temp_port;

  GPIOB->BSRRL=GPIO_BSRR_BS_8;                //RS=HIGH : send data
  GPIOB->BSRRH=GPIO_BSRR_BS_9;                //R/W=LOW : Write
  temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
  GPIOC->ODR = temp_port | (ldata << 6);      //put data on output Port

Finally, we need a command to locate a cursor to any position on the screen. Please remember that addresses are not linear – the third row goes after the first, and the second follows the third. It has had to be counted in the function “lcd_goto“:

void lcd_goto(uint8_t x, uint8_t y)
  uint8_t addr;

  switch (y)
  case 0:
    addr = 0x80;
  case 1:
    addr = 0xC0;
  case 2:
    addr = 0x94;
    addr = 0xD4;
  addr = addr + x;
  lcd_command((uint16_t) addr);

I hope that the function is quite clear.

Now we have all the necessary tools to try the LCD module. Let’s write a simple program that programs a user character and displays some strings on the screen:

int main(void)
  lcd_init();                                // LCD initialisation
  lcd_command((uint16_t) 0x01);              // Clear screen
  lcd_command(0x40);                         // Start recording a cyrillyc symbol "Г" to the memory to zero place
  lcd_char(0x1f); lcd_char(0x10); lcd_char(0x10); lcd_char(0x10);
  lcd_char(0x10); lcd_char(0x10); lcd_char(0x10); lcd_char(0x00);   // The character recorded
  lcd_command(0x01);                         // Clear screen
  lcd_goto(0,0); lcd_write("********************");
  lcd_goto(0,1); lcd_write("*      A"); lcd_char(0x00); lcd_write("AT-7      *");
  lcd_goto(0,2); lcd_write("*electronicsfun.net*");
  lcd_goto(0,3); lcd_write("********************");
  while (1)

The program initialises the module, creates a new symbol “Г” with 0x00 code and shows an intro from my “AGAT-7′ project. Please pay attention that we need to use the “lcd_char” function instead of “lcd_write” to display a character with 0x00 code. Here is the outcome:

More helpful information can be found in a datasheet to a module.

I hope this post will help you use these modules in your projects. See you!

Leave a Reply