I'm using the following (relevant) combination:

  • STM32F103C8T6 devboard (bluepill)
  • 1602 LCD with I2C (HDD44780)
  • STM32CubeIDE HAL

Also I have a keypad and some other components attached, but these are irrelevant for the problem.

I use a stripped down library, removing some unnecessary delays (I think, since in debugging circumstances it works).

What I see is when I debug the application, all is ok. Occasionally the LCD will not go through initialization, but when I remove the power to the LCD and reconnect, and restart, debugging is fine.

However, when I do not debug, thus running the application 'normally', the I2C display shows either rubbish or shows the first line lighted and the second line empty. The application itself seems to work, except for the display.

The only way to solve this is to press the reset button on the STM32F103C8T6. Removing the power from the STM32 (and thus LCD) and reconnect power does not help.

How can I fix this? It seems I have to programmatically reset/init the I2C somehow?

Relevant code


Global code, creating the LCD display, MyInit that is called from main.cpp (Init).


// Refresh every 99 ms, not 100 ms (than fractions are changing when displayed items have a period of
// % 100 ms == 0
LcdDisplay       _lcdDisplay   (&hi2c1, 0x27, &UpdateLcd, 99, 1);

void MyInit()



The constructor saves some variables for the instance, MyInit initializes the screen. In this function is high likely a problem.

LcdDisplay::LcdDisplay(I2C_HandleTypeDef* hI2c, uint8_t i2cChannel,
      UPDATE_LCD_FUNCTION_PTR callbackFunction, uint16_t refreshTime, uint8_t sysTickSubscriberIndex)
: _hI2c(hI2c),
  _i2cChannel(i2cChannel << 1),
   SysTickSubscribers::SetSubscriber(sysTickSubscriberIndex, this);
   SysTickSubscribers::SetInterval(sysTickSubscriberIndex, refreshTime);

void LcdDisplay::Init()
    // 4-bit mode, 2 lines, 5x7 format

    // display & cursor home (keep this!)

    // display on, right shift, underline off, blink off

    // clear display (optional here)

void LcdDisplay::SendCommand(uint8_t cmd)
    SendInternal(cmd, 0);

HAL_StatusTypeDef LcdDisplay::SendInternal(uint8_t data, uint8_t flags)
    HAL_StatusTypeDef res;
    for(;;) {
        res = HAL_I2C_IsDeviceReady(_hI2c, _i2cChannel, 1, HAL_MAX_DELAY);
        if(res == HAL_OK)

    uint8_t up = data & 0xF0;
    uint8_t lo = (data << 4) & 0xF0;
    uint8_t data_arr[4];

    data_arr[0] = up|flags|BACKLIGHT|PIN_EN;
    data_arr[1] = up|flags|BACKLIGHT;
    data_arr[2] = lo|flags|BACKLIGHT|PIN_EN;
    data_arr[3] = lo|flags|BACKLIGHT;

    //res = HAL_I2C_Master_Transmit_IT(_hI2c, _i2cChannel, data_arr, sizeof(data_arr));

    res = HAL_I2C_Master_Transmit(_hI2c, _i2cChannel, data_arr, sizeof(data_arr), HAL_MAX_DELAY);

    return res;

As you can see I removed the HAL_Delay (it seemed unnecessary). I tried to use I2C using interrupts, but didn't got it working (so far).

  • 1
    \$\begingroup\$ Please provide the code so it can be analyzed. \$\endgroup\$
    – Justme
    Commented Feb 27, 2020 at 11:53
  • 1
    \$\begingroup\$ Good start! However the LCD is used via the I2C IO expander, so please provide init code for it too, and open up the SendCommand how it uses the I2C to transmit the commands. I think I have spotted already one issue in the LCD init but I want to be sure. Oh and also the constructors if they do any I2C init, IO expander init, or LCD init. \$\endgroup\$
    – Justme
    Commented Feb 27, 2020 at 13:02
  • 1
    \$\begingroup\$ OK, thank you - can you say what IO expander it is? PCA 8-bit something? Because it is unusual if it does not need an init to make IO pin directions right first (there are ones with quasi-bidi-IO, I know, but just want to make sure) \$\endgroup\$
    – Justme
    Commented Feb 27, 2020 at 13:19
  • 1
    \$\begingroup\$ I had some issues with 4-bit mode. If it was already in 4-bit mode, it was unhappy if I put it in 4-bit mode again. I never completely resolved it, I have a hack workaround for now, I cycle the LCD power with a transistor controlled by the MCU. On power-up it defaults to 8-bit mode. \$\endgroup\$
    – Mattman944
    Commented Feb 27, 2020 at 16:51
  • 1
    \$\begingroup\$ @MichelKeijzers - my issue doesn't match your symptoms exactly, but there are similarities. Just something else to keep in mind. \$\endgroup\$
    – Mattman944
    Commented Feb 27, 2020 at 18:10

3 Answers 3


The delay before initialization will not help in the long run.

The state of IO expander controlling the LCD bus and the LCD controller are reset to known state only when there is a power-up, as they have a power-up reset.

After power-up reset, the LCD bus (IO expander) will be set to all pins high, and the LCD controller is in 8-bit mode, waiting for 8-bit data cycles. The current initialization is not perfect and fails to go reliably into 4-bit mode. The first falling E pulse of first nibble is understood as 8-bit command, and after it the LCD goes to 4-bit mode, but the sending of second nibble throws the bus out of sync, as the LCD is expecting first nibble of 4-bit command now. So in fact, after power-up, the sequence will always cause the display nybbles to be out of sync, by a nybble. That is why resetting MCU helps, as the sequence might get back to sync accidentally.

But when MCU code has been running, and if only MCU is reset, there is no guarantee at which state the LCD bus (IO expander output) or the LCD controller is, so that sequence should be hand-crafted to walk out of any state to a known state. As there is no reset pins or ability to power-cycle the IO expander and LCD via IO pin, it must be performed via I2C bus.

The LCD Enable pin might already be up, while RS and RW need to be set up before rising edge of Enable. Therefore the IO expander (LCD bus) needs one I2C write to get it to known state, preferably by setting Enable low if it was high, and backlight state set to whatever necessary. If it was high, this can execute an unwanted LCD command or send data to it, but not to worry, it is handled next.

The LCD is most likely in 4-bit mode from previous running sequence of MCU, but it is unknown whether it is expecting the first nibble or second nibble of 4-bit command, or still in 8-bit mode after power-up reset. Therefore, the proper sequence to syncronize the 4-bit bus mode is to send single nybbles to LCD at first, commanding it to go back to 8-bit mode. It needs three nybbles of commands to go to 8-bit mode, but there must be enough time between the nybbles, to allow execution of the slowest commands available after the first and second nybble. When the LCD is properly set back to 8-bit mode, it can be commanded to go back to 4-bit mode. This needs basically three nybbles - first an 8-bit command to go to 4-bit mode, and then it will be in 4-bit mode, but without proper font and lines configuration. From this point on, the command bytes can be sent as two nybbles as usual, so first thing it needs is the command byte again as two nybbles, so it stays in 4-bit mode but this time it gets proper font and lines config.

  • \$\begingroup\$ That's a lot of text thanks, I really have to go slowly through it when I have more time (in the weekend). However, I think I need (afaik see fastly now), to use my sequence, but between every step in the init put a delay (which I also saw in some cases). \$\endgroup\$ Commented Feb 27, 2020 at 22:36
  • 1
    \$\begingroup\$ The sequence I described should be similar to what HD44780 datasheet, Wikipedia, and Arduino driver all say. Plus, I just noticed, you don't have a delay after Clear or Home commands, they take more than 1ms to complete, so the next commands are most likely just skipped. Depends on what controller your LCD actually has, it may only be HD44780 compatible, not actually HD44780. \$\endgroup\$
    – Justme
    Commented Feb 28, 2020 at 0:27
  • \$\begingroup\$ It could be a compatible, I don't know … and good to know about the delay's … I now put my update speed to 10 ms so that's why I don't see a problem probably for the clear/homing. \$\endgroup\$ Commented Feb 28, 2020 at 11:43
  • \$\begingroup\$ Thanks for the improvement/modification in my GitHub project. Although I don't like delay's, I don't mind in the Init (as it's not called within an interrupt). For the clear/home, I probably will leave it like now... This evening I can check your code. yesterday late I added only a 10 ms delay before the Init, and it solved the boot problem (which does not mean every possible problem will solve). \$\endgroup\$ Commented Feb 28, 2020 at 11:58

I had the same problem as you, I was using TCA9555 IO expander for driving LCD ST7565 & button...

I set low RST,A0,RW,E before init and it got solved.


I put this answer as an 'assumption'.

I see in many libraries using I2C and LCD, a delay of 40-100 ms before initialization of I2C/LCD starts.

Because when I'm debugging, the IDE always stops before that line before pressing F8 to continue, but this waiting for the key results in a delay is more than 100 ms alone.

  • 1
    \$\begingroup\$ That would explain why it does not work out of powerup. Just put a delay that is before LCD init and try if it solves the power-up issue. But it won't solve the issue where LCD does not work after resetting the MCU during debugging. \$\endgroup\$
    – Justme
    Commented Feb 27, 2020 at 13:06
  • \$\begingroup\$ Indeed … I can try it only late tonight (not at home now). I don't care much about the second problem (that's just a bit annoying), but I do mind if later I would put it in an enclosure and I cannot reach the reset button of the STM anymore. \$\endgroup\$ Commented Feb 27, 2020 at 13:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.