Language:

HD44780-compartible LCD display and STM32.

Categories: Components, STM32
Tags: No Tags
Comments: No Comments
Published on: 27/06/2012

HD44780-compartible 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 20×4 LCD module. Most of the modules are designed to be used with 5V power supply, but 3.3V modules exist as well. They are not as popular as 5V modules and are usually twice as more 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 it is cheaper than 3.3V modules. For LCD control RS, R/W and E pins are used. Data is transported by 8-bit bus, but it is possible to switch to 4-bit bus. In this case each byte is sent by two steps. It means that we need from 7 to 11 pins to control the module. 8-bit data bus is easier to use, have higher speed and requires less of a microcontroller time than 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 use the highest bit of data bus as “ready” bit. Despite that the first method is less time effective, it is preferable to use in some cases. For instance, when 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 8-bit data bus in my projects, which requires 10 pins of a microcontroller and a cheap voltage transition chip.

Modules can be supplied with a different charsets, so if you are going to use any other languages than English in your project it is better to make sure that you are buying the proper module. Also, several user characters can be programmed in the module.

Pin outs can be different in different modules, but the most common is 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 (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 50-100 Ohm current limiting resistor. The contrast can be setup with a 10-20k trim pot connected 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 STM32L152R6 microcontroller that is used as 140K floppy-drive emulator. The schematic is:

Lets initialise the microcontroller with its usual commands. Also, lets setup delay functions in ms and us. I used TIM6 timer for delays but a simple loop with measured time will do the job as well.

?Download init.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include "stm32l1xx.h"
#include "stm32l1xx_conf.h"
#include "stm32l1xx_gpio.h"
#include "stm32l1xx_exti.h"
#include "stm32l1xx_syscfg.h"
#include "misc.h"
 
#include
#include
#include
#include 
 
void delay_us(uint16_t usv)                 // Delay in us
{
  TIM6->PSC=32-1;
  TIM6->ARR=usv;
  TIM6->DIER = TIM_DIER_UIE;
  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 initialisation
  RCC->CR|=RCC_CR_HSION;
  while (!(RCC->CR & RCC_CR_HSIRDY));             // Wait until it's stable
  RCC->CFGR|=RCC_CFGR_PLLDIV_0 | RCC_CFGR_PLLMUL_0;   // Switch to HSI as SYSCLK
  RCC->CR|=RCC_CR_PLLON;                              // Turn PLL on
  while (!(RCC->CR & RCC_CR_PLLRDY));             // Wait PLL to stabilise
 
  //Setting up flash for high speed
  FLASH->ACR=FLASH_ACR_ACC64;
  FLASH->ACR|=FLASH_ACR_LATENCY;
  FLASH->ACR|=FLASH_ACR_PRFTEN;
 
  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
  RCC->AHBENR|=RCC_AHBENR_GPIOCEN;
  GPIOC->MODER = (uint32_t) 0x5555000;
  GPIOC->OTYPER &= (uint16_t) 0xFF;
  GPIOC->OSPEEDR = (uint16_t) 0xAAAA000;
  GPIOC->ODR = (uint32_t) 0;
 
  RCC->AHBENR|=RCC_AHBENR_GPIOBEN;
  GPIOB->MODER = (uint32_t) 0x155000;
  GPIOB->OTYPER&=~(GPIO_OTYPER_OT_8 | GPIO_OTYPER_OT_9 | GPIO_OTYPER_OT_10 | GPIO_OTYPER_OT_6 | GPIO_OTYPER_OT_7);
  GPIOB->OSPEEDR|=(GPIO_OSPEEDER_OSPEEDR8_1 | GPIO_OSPEEDER_OSPEEDR9_1 | GPIO_OSPEEDER_OSPEEDR10_1);
  GPIOB->ODR = (uint16_t) 0;
 
  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;                 // TIM6 timer clock
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
}

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

?Download lcd_nybble.c
1
2
3
4
5
6
7
void lcd_nybble()
{
 char i;
 GPIOB->BSRRL=GPIO_BSRR_BS_10;
 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 data bus and pins RS and R/W set to zero. After that we need to “blink” with E signal. Each command requires some time to finilise in which the module is not accessible. So, time delays need to be used. As I mentioned before, a “ready” signal can be used instead. The delays need to be adjusted when different modules are used. Some command require more time than others. My command function “lcd_command” is:

1
2
3
4
5
6
7
8
9
10
11
12
void lcd_command(uint16_t lcmnd)
{
  uint16_t temp_port;
 
  delay_us(40);
  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
  lcd_nybble();
  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:

?Download lcd_init.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
  delay_us(160);
  GPIOC->BSRRL = (uint16_t) 0xE00;
  lcd_nybble();
  delay_us(160);
  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 a datasheet for details.

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

?Download lcd_write.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**********************************************************/
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)
  {
    delay_us(40);
    temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
    GPIOC->ODR = temp_port | (ldata[k] << 6);  //put data on output Port
    lcd_nybble();
    k++;
  }
}
 
/**********************************************************/
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
  delay_us(40);
  temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
  GPIOC->ODR = temp_port | (ldata << 6);      //put data on output Port
  lcd_nybble();
}

Finally, we need a command to locate a cursor to any position of the screen. Please pay attention 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”:

?Download lcd_goto.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void lcd_goto(uint8_t x, uint8_t y)
{
  uint8_t addr;
 
  switch (y)
  {
  case 0:
    addr = 0x80;
    break;
  case 1:
    addr = 0xC0;
    break;
  case 2:
    addr = 0x94;
    break;
  default:
    addr = 0xD4;
    break;
  }
  addr = addr + x;
  lcd_command((uint16_t) addr);
}

I hope that the function is quite clear.

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

?Download main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void)
{
  stm_init();
  lcd_init();                                // LCD initialisation
  lcd_command((uint16_t) 0x01);              // Clear screen
  delay_us(1700);
  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 new symbol “Г” with 0x00 code and shows an intro from my “AGAT-7’ project. Please pay attention that we need to use “lcd_char” function instead of “lcd_write” to display a character with 0x00 code. Here is the outcome:

A lot more of useful information can be found in a datasheet to a module.

I hope that this post will help you to start using these modules in your projects. See ya!

Leave a Reply

Welcome , today is Wednesday, 29/03/2017