2
\$\begingroup\$

I am building a system using a Waveshare RP2040-Zero ( like a Raspberry Pi Pico ) with an HD44780-2004 LCD over I2C. I'm facing a strange issue where the text on one line of the display occasionally gets overwritten by text from another line after the main loop runs for a while.

My sample code has two main parts:

  • A Timer interrupt that updates the time and date on line 1 every second.
  • A main loop that updates the thermostat status on line 2 every 5 seconds.

The Problem:

After approximately 10 loops, the text intended for line 3 ("Status: ...") is sometimes printed on line 2. Later this happens again, every 6-12 loops after the first occurrence with even half words but not clearing existing text (it looks like that the added spaces are removed ). Line 1 (the time) continues to update correctly.

Here is my simplified test code that reproduces the issue:

# micro python # lcd row test import machine import time from machine import I2C, Pin, Timer from time import sleep from pico_i2c_lcd import I2cLcd LCD_HD44780_2004_ADDR = 0x27 i2c = I2C(0, sda=Pin(0), scl=Pin(1), freq=400000) devices = i2c.scan() if LCD_HD44780_2004_ADDR in devices: lcd = I2cLcd(i2c, LCD_HD44780_2004_ADDR, 4, 20) else: lcd = None def update_tijd_datum(timer): """Show time and date on line 1 of the LCD - independent of main loop""" if lcd is None: return current_time = time.localtime() tijd_str = "{:02d}-{:02d}-{} {:02d}:{:02d}:{:02d}".format( current_time[2], current_time[1], current_time[0], current_time[3], current_time[4], current_time[5] ) lcd.move_to(0, 0) tijd_str = tijd_str + ' ' * (20 - len(tijd_str)) lcd.putstr(tijd_str) # INITIALIZE TIMER INTERRUPT tijd_timer = Timer() tijd_timer.init(period=1000, mode=Timer.PERIODIC, callback=update_tijd_datum) def toon_thermostaat_info(): if lcd is None: return lcd.move_to(0, 1) lcd.putstr("Temp: 21.5C" + ' ' * (20 - len("Temp: 21.5C"))) lcd.move_to(0, 2) lcd.putstr("Status: Verwarming" + ' ' * (20 - len("Status: Verwarming"))) lcd.move_to(0, 3) lcd.putstr("Set: 22.0C" + ' ' * (20 - len("Set: 22.0C"))) toon_thermostaat_info() counter = 0 try: while True: sleep(5) counter += 1 if lcd: lcd.move_to(0, 2) # Intend to write to line 3 import random status = random.choice(["Status: Verwarming", "Status: Koeling", "Status: Uit"]) print(f"{counter}, {status}") status = status + ' ' * (20 - len(status)) lcd.putstr(status) # This text sometimes appears on line 2! except KeyboardInterrupt: print("Programma gestopt") tijd_timer.deinit() 

What I've Investigated:

I am padding the strings with spaces to clear the entire line, so it shouldn't be leftover characters. The move_to(0, 2) command is correctly placed before writing the status. The problem is intermittent, which makes me suspect a race condition or a shared resource conflict.

As a side note, I did try it with a different same type/model brand new display but same result. The timer interrupt (updating line 1) and the main loop (updating line 2) are both accessing the same lcd object. I suspect they might be interfering with each other, even though they are writing to different lines.

My Questions:

What is the root cause of this "line jumping" behavior? Is my suspicion about a race condition between the interrupt and the main loop correct?

How can I make this solid? What is the best practice for handling concurrent access to an I2C LCD from both a timer interrupt and the main loop in MicroPython?

Any insights or suggestions would be greatly appreciated

\$\endgroup\$
3
  • \$\begingroup\$ "I suspect they might be interfering with each other, even though they are writing to different lines.' Different lines makes no difference. They are both using the same hardware resources to access the screen, so this needs to be properly protected for sharing. \$\endgroup\$ Commented Oct 16 at 14:16
  • \$\begingroup\$ You and the others are right. At first I was thinking that micro python are thread safe by default ( yup, new to micro python ). \$\endgroup\$ Commented Oct 16 at 17:07
  • 1
    \$\begingroup\$ Have the timer interrupt set a flag. The main loop tests the flag to keep track of time and to call the required function(s). Only one task should manage the LCD. As such, you really don’t need to have any interrupts - if you can test the timer hardware interrupt flag in the main loop then interrupts are not needed. \$\endgroup\$ Commented Oct 17 at 11:57

2 Answers 2

4
\$\begingroup\$

Yes, this is a race condition. The problem is that the state of the LCD controller is being changed by both the main loop and interrupt - the cursor position is moved by both parts of code for instance.

Sharing hardware can be difficult - basically you need to design some interface between the interrupt handler and the main loop.

One way could be that there is a memory buffer where you can specify the X,Y cursor location for a string to print and the text of the string. And then there is a flag which you set to tell the interrupt routine to start writing the text in the buffer at the X,Y cursor location after it has written the time to the LCD. The interrupt routine clears the flag after it has finished. So your main loop checks the flag is clear, sets up the command and then sets the flag. And then it waits for the flag to clear .

So the main loop is not allowed to directly touch the LCD controller. This is common in real time systems - you write to a "driver" which manages the hardware.

\$\endgroup\$
1
  • \$\begingroup\$ you and 'justme' did get me on track what the issue is. (See my reaction also at Justme's answer). So my temp bandage is almost the way to go. Both thank you. \$\endgroup\$ Commented Oct 16 at 17:04
4
\$\begingroup\$

I think you already asked your own question.

When using libraries or high level code from multiple places, you must be sure which API is re-entrant and which is not.

It is great that in this case only the printing of letters is interleaved, instead of totally corrupting the I2C bus transactions to the GPIO expander that drives the HD44780 parallel bus.

If you don't know what code is re-entrant and what isn't, don't call from multiple threads of execution, use a mutex. Or read documentation or source code to be sure it is re-entrant.

Without further knowledge how to do it properly in Micropython APIs:

Update text into some kind of data structure, like array of two strings, top and bottom. Perhaps with a flag to indicate the string has been updated so it can be written to screen. Don't update the string until it has been written as indicated by the flag.

Update whole screen from one place, like the interrupt context, checking the flags and clearing it.

\$\endgroup\$
1
  • \$\begingroup\$ Of course. This clear thing up. Thank you. To have it work I do use an array with the four lines to avoid this and print it directly when something changed, a flag that every second ( time timer )gets updated, but originally want to avoid all of that. \$\endgroup\$ Commented Oct 16 at 17:02

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.