STM32 LCD Interface for Multiple Display Proteus Simulation

Things used in this project

Software apps and online services:

1- STMicroelectronics STM32CubeMX

2- STMicroelectronics STM32CubeIDE

3- Proteus 8

Interfacing Multiple Displays with STM32 Microcontroller in 4-Bit Mode: A Guide to LCD Integration and Operation:

In this project, we will connect multiple displays to the STM32 microcontroller using the 4-bit mode, which requires six GPIO pins. The STM32 operates at 3.3V logic levels, while the displays operate at 5V. However, this is not a problem because the displays can still recognize the 3.3V output from the STM32. The only 5V line that is required is the VDD line for the displays, which can be powered using the 5V pin on the STM32 board. We will simulate this setup using Proteus software. Following this article, we will take the 16×2 LCD as an example to explain how displays work.

Liquid crystal displays (LCDs) are a commonly used to display data in devices such as calculators, microwave ovens, and many other electronic devices, the 16×2 LCD with a 16-pin header interface. Assuming it has the standard Hitachi LCD driver HD44780 controller. We’ll see how it works internally and how to interface it with STM32.

LCD Module Pinout:

This is the pinout for a typical LCD 16×2 display unit.

Pins’ Functions Description:

STM32CubeMX Configuration:

  • Open CubeMX & Create New Project Choose The Target MCU STM32F103C6 & Double-Click Its Name
  • Go To The Clock Configuration & Set The System Clock To 8MHz
  • Configure The GPIO Pins [PB3..PB9] as Output Pins
  • Generate The Initialization Code & Open The Project In CubeIDE
  • Create header and source file for liquid crystal

STM32CubeIDE Configuration :

  • Write The Application Layer Code
  • LiquidCrystal.h & LiquidCrystal.c
  •  main.c
#ifndef LIQUIDCRYSTAL_H_
#define LIQUIDCRYSTAL_H_

// commands
#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_ENTRYMODESET 0x04
#define LCD_DISPLAYCONTROL 0x08
#define LCD_CURSORSHIFT 0x10
#define LCD_FUNCTIONSET 0x20
#define LCD_SETCGRAMADDR 0x40
#define LCD_SETDDRAMADDR 0x80

// flags for display entry mode
#define LCD_ENTRYRIGHT 0x00
#define LCD_ENTRYLEFT 0x02
#define LCD_ENTRYSHIFTINCREMENT 0x01
#define LCD_ENTRYSHIFTDECREMENT 0x00

// flags for display on/off control
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00

// flags for display/cursor shift
#define LCD_DISPLAYMOVE 0x08
#define LCD_CURSORMOVE 0x00
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00

// flags for function set
#define LCD_8BITMODE 0x10
#define LCD_4BITMODE 0x00
#define LCD_2LINE 0x08
#define LCD_1LINE 0x00
#define LCD_5x10DOTS 0x04
#define LCD_5x8DOTS 0x00


// low-level functions
void send(uint8_t, GPIO_PinState);
void write4bits(uint8_t);
void write8bits(uint8_t);
void pulseEnable(void);

// initializers
void LiquidCrystal(GPIO_TypeDef *gpioport, uint16_t rs, uint16_t rw, uint16_t enable,
  uint16_t d0, uint16_t d1, uint16_t d2, uint16_t d3);

void init(uint8_t fourbitmode, GPIO_TypeDef *gpioport, uint16_t rs, uint16_t rw, uint16_t enable,
    uint16_t d0, uint16_t d1, uint16_t d2, uint16_t d3,
    uint16_t d4, uint16_t d5, uint16_t d6, uint16_t d7);

void begin(uint8_t cols, uint8_t rows);
void enableClock(void);

// high-level functions
void clear(void);
void home(void);

void noDisplay(void);
void display(void);
void noBlink(void);
void blink(void);
void noCursor(void);
void cursor(void);
void scrollDisplayLeft(void);
void scrollDisplayRight(void);
void leftToRight(void);
void rightToLeft(void);
void autoscroll(void);
void noAutoscroll(void);
size_t print(const char []);

void setRowOffsets(int row1, int row2, int row3, int row4);
void createChar(uint8_t, uint8_t[]);
void setCursor(uint8_t, uint8_t);
size_t write(uint8_t);
void command(uint8_t);


#endif /* LIQUIDCRYSTAL_H_ */

#include "stm32f1xx_hal.h" // change this line accordingly
#include "LiquidCrystal.h"
#include <stdio.h>
#include <string.h>

// change this to 0 if you want to use 8-bit mode
uint8_t _fourbit_mode = 1;

// change this to LCD_5x10DOTS for some 1 line displays that can select a 10 pixel high font
uint8_t dotsize = LCD_5x8DOTS;

// pin definitions and other LCD variables
uint16_t _rs_pin; // LOW: command.  HIGH: character.
uint16_t _rw_pin; // LOW: write to LCD.  HIGH: read from LCD.
uint16_t _enable_pin; // activated by a HIGH pulse.
uint16_t _data_pins[8];
GPIO_TypeDef *_port;

uint8_t _displayfunction;
uint8_t _displaycontrol;
uint8_t _displaymode;

uint8_t _initialized;

uint8_t _numlines;
uint8_t _row_offsets[4];

void LiquidCrystal(GPIO_TypeDef *gpioport, uint16_t rs, uint16_t rw, uint16_t enable,
           uint16_t d0, uint16_t d1, uint16_t d2, uint16_t d3)
{
  if(_fourbit_mode)
    init(1, gpioport, rs, rw, enable, d0, d1, d2, d3, 0, 0, 0, 0);
  else
    init(0, gpioport, rs, rw, enable, d0, d1, d2, d3, 0, 0, 0, 0);
}

void init(uint8_t fourbitmode, GPIO_TypeDef *gpioport, uint16_t rs, uint16_t rw, uint16_t enable,
       uint16_t d0, uint16_t d1, uint16_t d2, uint16_t d3,
       uint16_t d4, uint16_t d5, uint16_t d6, uint16_t d7)
{
  _rs_pin = rs;
  _rw_pin = rw;
  _enable_pin = enable;
  _port = gpioport;

  _data_pins[0] = d0;
  _data_pins[1] = d1;
  _data_pins[2] = d2;
  _data_pins[3] = d3;
  _data_pins[4] = d4;
  _data_pins[5] = d5;
  _data_pins[6] = d6;
  _data_pins[7] = d7;

  if (fourbitmode)
    _displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;
  else
    _displayfunction = LCD_8BITMODE | LCD_1LINE | LCD_5x8DOTS;

 // begin(16, 2);
}

void begin(uint8_t cols, uint8_t lines) {
  if (lines > 1) {
    _displayfunction |= LCD_2LINE;
  }
  _numlines = lines;

  setRowOffsets(0x00, 0x40, 0x00 + cols, 0x40 + cols);

  // for some 1 line displays you can select a 10 pixel high font
  if ((dotsize != LCD_5x8DOTS) && (lines == 1)) {
    _displayfunction |= LCD_5x10DOTS;
  }

  //Initializing GPIO Pins
  enableClock();

  GPIO_InitTypeDef gpio_init;
  gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
  gpio_init.Mode = GPIO_MODE_OUTPUT_PP;

 if(_fourbit_mode)
    gpio_init.Pin = _rs_pin | _rw_pin | _enable_pin | _data_pins[0] | _data_pins[1] | _data_pins[2] | _data_pins[3];
  else
    gpio_init.Pin = _rs_pin | _rw_pin | _enable_pin | _data_pins[0] | _data_pins[1] | _data_pins[2] | _data_pins[3] |
                    _data_pins[4] | _data_pins[5] | _data_pins[6] | _data_pins[7];
// SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION!
// according to datasheet, we need at least 40ms after power rises above 2.7V
// so we'll wait 50 just to make sure
  HAL_Delay(50);

  // Now we pull both RS and R/W low to begin commands
  HAL_GPIO_WritePin(_port, _rs_pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(_port, _enable_pin, GPIO_PIN_RESET);

  if (_rw_pin != 255) {
    HAL_GPIO_WritePin(_port, _rw_pin, GPIO_PIN_RESET);
  }

  //put the LCD into 4 bit or 8 bit mode
  if (! (_displayfunction & LCD_8BITMODE)) {
    // this is according to the hitachi HD44780 datasheet
    // figure 24, pg 46

    // we start in 8bit mode, try to set 4 bit mode
    write4bits(0x03);
    HAL_Delay(5); // wait min 4.1ms

    // second try
    write4bits(0x03);
    HAL_Delay(5); // wait min 4.1ms

    // third go!
    write4bits(0x03);
    HAL_Delay(1);

    // finally, set to 4-bit interface
    write4bits(0x02);
  } else {
    // this is according to the hitachi HD44780 datasheet
    // page 45 figure 23

    // Send function set command sequence
    command(LCD_FUNCTIONSET | _displayfunction);
    HAL_Delay(5);  // wait more than 4.1ms

    // second try
    command(LCD_FUNCTIONSET | _displayfunction);
    HAL_Delay(1);

    // third go
    command(LCD_FUNCTIONSET | _displayfunction);
  }

  // finally, set # lines, font size, etc.
  command(LCD_FUNCTIONSET | _displayfunction);

  // turn the display on with no cursor or blinking default
  _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF;
  display();

  // clear it off
  clear();

  // Initialize to default text direction (for romance languages)
  _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT;
  // set the entry mode
  command(LCD_ENTRYMODESET | _displaymode);

}

// enables GPIO RCC Clock
void enableClock(void)
{
  if(_port == GPIOA)
    __HAL_RCC_GPIOA_CLK_ENABLE();
  else if(_port == GPIOB)
    __HAL_RCC_GPIOB_CLK_ENABLE();
  else if(_port == GPIOB)
    __HAL_RCC_GPIOB_CLK_ENABLE();
  else if(_port == GPIOC)
    __HAL_RCC_GPIOC_CLK_ENABLE();
  else if(_port == GPIOD)
    __HAL_RCC_GPIOD_CLK_ENABLE();

  // if you have a port that is not listed add it below the other else ifs
}

void setRowOffsets(int row0, int row1, int row2, int row3)
{
  _row_offsets[0] = row0;
  _row_offsets[1] = row1;
  _row_offsets[2] = row2;
  _row_offsets[3] = row3;
}

/********** high level commands, for the user! */
void clear(void)
{
  command(LCD_CLEARDISPLAY);  // clear display, set cursor position to zero
  HAL_Delay(2);  // this command takes a long time!
}

void home(void)
{
  command(LCD_RETURNHOME);  // set cursor position to zero
  HAL_Delay(2);  // this command takes a long time!
}

void setCursor(uint8_t col, uint8_t row)
{
  const size_t max_lines = sizeof(_row_offsets) / sizeof(*_row_offsets);
  if ( row >= max_lines ) {
    row = max_lines - 1;    // we count rows starting w/0
  }
  if ( row >= _numlines ) {
    row = _numlines - 1;    // we count rows starting w/0
  }

  command(LCD_SETDDRAMADDR | (col + _row_offsets[row]));
}

// Turn the display on/off (quickly)
void noDisplay(void) {
  _displaycontrol &= ~LCD_DISPLAYON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void display(void) {
  _displaycontrol |= LCD_DISPLAYON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}

// Turns the underline cursor on/off
void noCursor(void) {
  _displaycontrol &= ~LCD_CURSORON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void cursor(void) {
  _displaycontrol |= LCD_CURSORON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}

// Turn on and off the blinking cursor
void noBlink(void) {
  _displaycontrol &= ~LCD_BLINKON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void blink(void) {
  _displaycontrol |= LCD_BLINKON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}

// These commands scroll the display without changing the RAM
void scrollDisplayLeft(void) {
  command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT);
}
void scrollDisplayRight(void) {
  command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT);
}

// This is for text that flows Left to Right
void leftToRight(void) {
  _displaymode |= LCD_ENTRYLEFT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// This is for text that flows Right to Left
void rightToLeft(void) {
  _displaymode &= ~LCD_ENTRYLEFT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// This will 'right justify' text from the cursor
void autoscroll(void) {
  _displaymode |= LCD_ENTRYSHIFTINCREMENT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// This will 'left justify' text from the cursor
void noAutoscroll(void) {
  _displaymode &= ~LCD_ENTRYSHIFTINCREMENT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// This will print character string to the LCD
size_t print(const char str[]) {
  if (str == NULL) return 0;

  const uint8_t *buffer = (const uint8_t *)str;
  size_t size = strlen(str);
  size_t n = 0;

  while (size--) {
    if (write(*buffer++)) n++;
    else break;
  }
  return n;
}

// Allows us to fill the first 8 CGRAM locations
// with custom characters
void createChar(uint8_t location, uint8_t charmap[]) {
  location &= 0x7; // we only have 8 locations 0-7
  command(LCD_SETCGRAMADDR | (location << 3));
  for (int i=0; i<8; i++) {
    write(charmap[i]);
  }
}

/*********** mid level commands, for sending data/cmds */

inline void command(uint8_t value) {
  send(value, GPIO_PIN_RESET);
}

inline size_t write(uint8_t value) {
  send(value, GPIO_PIN_SET);
  return 1; // assume sucess
}

/************ low level data pushing commands **********/

// write either command or data, with automatic 4/8-bit selection
void send(uint8_t value, GPIO_PinState mode) {
  HAL_GPIO_WritePin(_port, _rs_pin, mode);

  // if there is a RW pin indicated, set it low to Write
  if (_rw_pin != 255) {
    HAL_GPIO_WritePin(_port, _rw_pin, GPIO_PIN_RESET);
  }

  if (_displayfunction & LCD_8BITMODE) {
    write8bits(value);
  } else {
    write4bits(value>>4);
    write4bits(value);
  }
}

void pulseEnable(void) {
  HAL_GPIO_WritePin(_port, _enable_pin, GPIO_PIN_RESET);
  HAL_Delay(1);
  HAL_GPIO_WritePin(_port, _enable_pin, GPIO_PIN_SET);
  HAL_Delay(1);    // enable pulse must be >450ns
  HAL_GPIO_WritePin(_port, _enable_pin, GPIO_PIN_RESET);
  HAL_Delay(1);   // commands need > 37us to settle
}

void write4bits(uint8_t value) {
  for (int i = 0; i < 4; i++) {
    HAL_GPIO_WritePin(_port, _data_pins[i], ((value >> i) & 0x01)?GPIO_PIN_SET:GPIO_PIN_RESET);
  }

  pulseEnable();
}

void write8bits(uint8_t value) {
  for (int i = 0; i < 8; i++) {
    HAL_GPIO_WritePin(_port, _data_pins[i], ((value >> i) & 0x01)?GPIO_PIN_SET:GPIO_PIN_RESET);
  }

  pulseEnable();
}

#include "main.h"
#include "LiquidCrystal.h"

int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
LiquidCrystal(GPIOB, GPIO_PIN_3,GPIO_PIN_4,GPIO_PIN_5,GPIO_PIN_6 ,GPIO_PIN_7,GPIO_PIN_8,GPIO_PIN_9);
begin(20,4);      //LCD 20*4
 
while (1)
  {
    setCursor(0, 0);
    print("HELLO WORLD");
    HAL_Delay(1000);
    clear();
  }
}

Proteus Configuration :

  • Open Proteus & Create New Project and click next

  • Click on Pick Device
  • Search for STM32F103C6 & [LCD 16*2… LCD 20*4]
  • Click on Terminal Mode then choose (DEFAULT & POWER &GROUND)
  • finally make the circuit below and start the simulation

That’s all!

If you have any questions or suggestions don’t hesitate to leave a comment below

Related posts

STM32-Driven LED Bar: Integration with 74HC595 Shift Register

How to Interface STM32 Microcontrollers with ADC128S102 via SPI

HC-SR04 Ultrasonic Sensor: Integrating with STM32 Microcontrollers Using TIMER