Electronic – Boot problem with STM32 HAL, I2C HDD44780 and 1602 LCD

hali2clcdstm32

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

MyMain.cpp

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()
{
   //_lcdDisplay.I2C_Scan();
   _lcdDisplay.Init();

   …

LcdDisplay.cpp

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),
  _callbackFunction(callbackFunction)
{
   SysTickSubscribers::SetSubscriber(sysTickSubscriberIndex, this);
   SysTickSubscribers::SetInterval(sysTickSubscriberIndex, refreshTime);
}


void LcdDisplay::Init()
{
    // 4-bit mode, 2 lines, 5x7 format
    SendCommand(0b00110000);

    // display & cursor home (keep this!)
    SendCommand(0b00000010);

    // display on, right shift, underline off, blink off
    SendCommand(0b00001100);

    // clear display (optional here)
    SendCommand(0b00000001);
}

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)
            break;
    }

    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);

    //HAL_Delay(LCD_DELAY_MS);
    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).

Best Answer

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.

Related Topic