DEV Community

Hedy
Hedy

Posted on

How to connect LCD to STM32 with STM32CubeMX?

You don’t “connect it in CubeMX” so much as:

  1. Wire the LCD to the STM32 pins
  2. Tell CubeMX which pins/peripherals you’re using
  3. Let CubeMX generate init code
  4. Add a small LCD driver (C code) using HAL

I’ll show you the most common case: an HD44780-style 16×2 character LCD, first with direct GPIO (4-bit mode) and then briefly with I²C backpack.

1. Hardware: connect LCD → STM32 (4-bit GPIO)

Typical HD44780 16×2 LCD pins:

  1. VSS – GND
  2. VDD – +5 V (many also work at 3.3 V; if not, use level shifting for signals)
  3. VO – contrast (middle pin of a 10–20 kΩ pot between VDD and GND)
  4. RS – Register Select
  5. RW – Read/Write
  6. E – Enable
  7. D0 – not used in 4-bit
  8. D1 – not used
  9. D2 – not used
  10. D3 – not used
  11. D4 – Data bit 4
  12. D5 – Data bit 5
  13. D6 – Data bit 6
  14. D7 – Data bit 7
  15. LED+ – backlight + (via resistor)
  16. LED- – backlight –

Example wiring to STM32 (you can choose any free GPIOs):

  • RS → PB0
  • RW → GND (write-only)
  • E → PB1
  • D4 → PB5
  • D5 → PB6
  • D6 → PB7
  • D7 → PB8

Plus:

  • LCD VSS → GND, VDD → 5 V or 3.3 V (check your LCD),
  • VO via pot for contrast,
  • Backlight pins via resistor and GND/VCC as needed.

2. Configure STM32 in STM32CubeMX

  1. Create a new project

Select your MCU / Nucleo board (e.g. STM32F103C8Tx, NUCLEO-F401RE, etc.).

  1. Enable GPIO for the LCD pins
  • In the Pinout & Configuration tab, click on PB0, PB1, PB5, PB6, PB7, PB8 and set them as GPIO_Output.
  • No pull-up/down is usually fine (or default).
  1. Clock config

In Clock Configuration, set system clock (e.g. 72 MHz or whatever your board supports).

  1. Project settings
  • Project → Settings:

    • Toolchain: e.g. STM32CubeIDE or “Makefile”.
  • Generate code (“Generate Code” button).

CubeMX will create:

  • main.c,
  • gpio.c/gpio.h,
  • main.h with macros for your pins (LCD_RS_Pin, etc., if you renamed them or you can rename later).

3. Add a simple LCD driver (HAL + GPIO)

Open main.c and add a few static functions under the includes.

3.1. Make sure you have pin macros

In main.h, after CubeMX generated code you’ll typically have something like:

#define LCD_RS_Pin GPIO_PIN_0
#define LCD_RS_GPIO_Port GPIOB
#define LCD_E_Pin  GPIO_PIN_1
#define LCD_E_GPIO_Port  GPIOB
#define LCD_D4_Pin GPIO_PIN_5
#define LCD_D4_GPIO_Port GPIOB
#define LCD_D5_Pin GPIO_PIN_6
#define LCD_D5_GPIO_Port GPIOB
#define LCD_D6_Pin GPIO_PIN_7
#define LCD_D6_GPIO_Port GPIOB
#define LCD_D7_Pin GPIO_PIN_8
#define LCD_D7_GPIO_Port GPIOB
Enter fullscreen mode Exit fullscreen mode

If not, add them and match your wiring.

3.2. LCD helper functions

In main.c (above int main(void)), add:

#include "main.h"

/* Small delay helper (in microseconds-ish). For simplicity, use HAL_Delay(ms)
   for long delays and this busy-wait for short ones. */
static void lcd_delay_us(volatile uint32_t us)
{
    // crude delay; for real projects use a timer-based microsecond delay
    while (us--) {
        __NOP();
    }
}

static void lcd_pulse_enable(void)
{
    HAL_GPIO_WritePin(LCD_E_GPIO_Port, LCD_E_Pin, GPIO_PIN_SET);
    lcd_delay_us(50); // > 450 ns
    HAL_GPIO_WritePin(LCD_E_GPIO_Port, LCD_E_Pin, GPIO_PIN_RESET);
    lcd_delay_us(50);
}

static void lcd_write_nibble(uint8_t nibble)
{
    HAL_GPIO_WritePin(LCD_D4_GPIO_Port, LCD_D4_Pin, (nibble & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LCD_D5_GPIO_Port, LCD_D5_Pin, (nibble & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LCD_D6_GPIO_Port, LCD_D6_Pin, (nibble & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LCD_D7_GPIO_Port, LCD_D7_Pin, (nibble & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET);
    lcd_pulse_enable();
}

static void lcd_send(uint8_t value, uint8_t rs)
{
    // RS: 0 = command, 1 = data
    HAL_GPIO_WritePin(LCD_RS_GPIO_Port, LCD_RS_Pin, rs ? GPIO_PIN_SET : GPIO_PIN_RESET);

    // high nibble
    lcd_write_nibble(value >> 4);
    // low nibble
    lcd_write_nibble(value & 0x0F);

    HAL_Delay(2); // most commands need < 1.6 ms
}

static void lcd_command(uint8_t cmd)
{
    lcd_send(cmd, 0);
}

static void lcd_data(uint8_t data)
{
    lcd_send(data, 1);
}

static void lcd_clear(void)
{
    lcd_command(0x01); // clear display
    HAL_Delay(2);
}

static void lcd_home(void)
{
    lcd_command(0x02);
    HAL_Delay(2);
}

static void lcd_init(void)
{
    HAL_Delay(50); // wait > 40 ms after power-up

    // 4-bit init sequence (per HD44780 datasheet)
    HAL_GPIO_WritePin(LCD_RS_GPIO_Port, LCD_RS_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LCD_E_GPIO_Port,  LCD_E_Pin,  GPIO_PIN_RESET);

    // send 0x3 three times to force 8-bit mode
    lcd_write_nibble(0x03);
    HAL_Delay(5);
    lcd_write_nibble(0x03);
    HAL_Delay(5);
    lcd_write_nibble(0x03);
    HAL_Delay(5);

    // switch to 4-bit
    lcd_write_nibble(0x02);
    HAL_Delay(1);

    // function set: 4-bit, 2 lines, 5x8 font
    lcd_command(0x28);

    // display off
    lcd_command(0x08);

    // clear
    lcd_clear();

    // entry mode set: increment, no shift
    lcd_command(0x06);

    // display on, cursor off, blink off
    lcd_command(0x0C);
}

static void lcd_set_cursor(uint8_t col, uint8_t row)
{
    // 16x2 typical: line 0: 0x00, line 1: 0x40
    static const uint8_t row_offsets[] = {0x00, 0x40, 0x14, 0x54};
    if (row > 1) row = 1; // limit to 2 lines
    lcd_command(0x80 | (col + row_offsets[row]));
}

static void lcd_print(const char *s)
{
    while (*s) {
        lcd_data((uint8_t)*s++);
    }
}
Enter fullscreen mode Exit fullscreen mode

3.3. Use the LCD in main()

Still in main.c, inside main():

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();   // generated by CubeMX

    lcd_init();       // our function

    lcd_set_cursor(0, 0);
    lcd_print("Hello STM32");
    lcd_set_cursor(0, 1);
    lcd_print("LCD 16x2 Demo");

    while (1)
    {
        // Your main loop
    }
}
Enter fullscreen mode Exit fullscreen mode

Build & flash. If wiring + supply are OK, you should see your text.

4. Using an I²C LCD (PCF8574 backpack)

If your 16×2 LCD has a little backpack with 4 pins (VCC, GND, SDA, SCL):

Hardware

  • LCD backpack VCC → 5 V (or 3.3 V if compatible)
  • GND → GND
  • SDA → STM32 I2C SDA (e.g. PB7)
  • SCL → STM32 I2C SCL (e.g. PB6)
  • Pull-up resistors: usually already on the module; if not, add 4.7 kΩ from SDA/SCL to VCC.

CubeMX

  1. Enable I2C1 (or another I2C) in I2C mode.
  2. Select PB6 (SCL), PB7 (SDA) or your board’s I2C pins.
  3. Set speed (e.g. 100 kHz).
  4. Generate code — you’ll get MX_I2C1_Init() and a hi2c1 handle.

Code idea

The PCF8574 exposes 8 bits that map to RS/E/D4–D7/backlight. You then:

  • Use HAL_I2C_Master_Transmit(&hi2c1, address<<1, &data, 1, 10) to send each nibble.
  • The rest of the logic (init commands, set cursor, print) is similar; only the lcd_write_nibble() function becomes “write byte via I2C to PCF8574”.

If you want, I can write a ready-to-paste I²C LCD driver for STM32 HAL using CubeMX’s I2C_HandleTypeDef and show exactly how to call it.

5. Common gotchas

No characters / only blocks:
– Contrast (VO) too high/low → adjust pot.
– Init timing wrong → double-check lcd_init() sequence and delays.

Garbled text:
– Wrong D4–D7 wiring or pins not matching what you defined in main.h.

Nothing at all:
– VCC/GND swapped, backlight off, or LCD not actually powered.
– Wrong power voltage for LCD (some need 5 V).

Top comments (0)