SSD1306 Refresh / skipping interrupts?

All ESP32 boards running MicroPython.
Target audience: MicroPython users with an ESP32 board.
Post Reply
User avatar
spookyrufus
Posts: 12
Joined: Wed Jul 15, 2020 8:09 am
Location: Estonia
Contact:

SSD1306 Refresh / skipping interrupts?

Post by spookyrufus » Wed Jul 15, 2020 9:01 am

Hello, new user here. Been using micropython on ESP32 for about 1 year, and I love it!!!
I have now hit an issue, which I can't find my way around -and after banging my head on it for the best part of a week- I decided to turn here.

I am trying to set up a basic device which uses an I2C SSD1306 for output, and a rotary encoder w/ push button for input.
While I have no issues using these two peripherals separately, and I manage to get very decent refresh rates on the display alone, as well as very precise readings on the rotary encoder (using the micropython-stm-lib by SpotlightKid), I run into jitter / skipped readings issues whenever I try using them together.

My stripped-down code looks like this:

Code: Select all

old_v = rotary.value  
while True:
	...
	- draw something on the display -
	...
	
	if rotary.value != old_v:
		print(rotary.value)
		old_v = rotary.value
	
	display.show()
	display.fill(0)
If I remove the display.show() instruction, everything runs smoothly (except of course the display won't refresh), and I can see correct and responsive readings from the rotary encoder.
If I try to refresh the display, the encoder readings start behaving erratically, and I get very jittery values.

The test application I've made to highlight this problem is a simple paddle-ball game where a pixel bounces around the edges of the SSD1306 display, and a paddle on the bottom moves according to the rotary encoder's readings (which is absolutely NOT the intended final result :D) . I wasn't able so far to achieve this.
I tried capturing a video to better show what I'm experiencing, but I'm unsure whether this helps:

Image
See video on Gyfcat

I understand this has to do with the reading frequency of the changing edges on the encoder's pins, which likely becomes too low when the display is refreshed, but I don't understand why would that be, since the library is interrupt-driven, and I assumed that such interrupts could easily take over the display refresh routines?

I have tried in several ways getting around this issue, but as I'm clearly not experienced at low-level programming, I have miserably failed so far. What I have tried:
  • Tried using different encoder libraries
  • Tried writing my own poll-driven encoder library, and polling it for changes at every iteration of the loop. Same problem when using it together with the display.
  • Tried putting the display refresh instructions in a separate thread (both using a hardware timer, and the _threading module)
I would appreciate if someone could point me in the right direction, for tackling this problem the proper way, or at least an explanation for the behavior I'm seeing, which is far from the expected one.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: SSD1306 Refresh / skipping interrupts?

Post by pythoncoder » Wed Jul 15, 2020 11:07 am

This could be a tricky one. The driver sends the entire buffer to the display in one I2C burst. It is possible that pin interrupts are disabled while I2C transfers are in progress. There are two thoughts that come to mind if this is the case. One is to raise a firmware issue: perhaps pin interrupts should be allowed during a transfer.

The other approach would be to modify the display driver so that the data is sent in smaller amounts. This would impact refresh time so I'd guess it would be unlikely to be adopted officially, but you could do it in your own code.

I'm guessing here, but I can't think of another reason for this effect.
Peter Hinch
Index to my micropython libraries.

User avatar
spookyrufus
Posts: 12
Joined: Wed Jul 15, 2020 8:09 am
Location: Estonia
Contact:

Re: SSD1306 Refresh / skipping interrupts?

Post by spookyrufus » Wed Jul 15, 2020 12:49 pm

Thanks Peter for the reply! I admire your dedication in these forums, and your deep understanding of virtually every topic I searched about.
pythoncoder wrote:
Wed Jul 15, 2020 11:07 am
This could be a tricky one. The driver sends the entire buffer to the display in one I2C burst. It is possible that pin interrupts are disabled while I2C transfers are in progress. There are two thoughts that come to mind if this is the case
This is what I have been fearing all the time. I tried looking into the low level code, without much understanding, but it seems to be the case, reading on other microcontrollers and languages forums, that I2C relies on interrupts to work properly.

What can I do then to achieve a more or less fast display access, without sacrificing completely interrupts on my setup?
It is totally an option for me, to use a different type of display, as long as its basic features (like text and drawing for example, for which I'm using your excellent Writer class) are supported by some existing library.
I have found many available ones online, and the one which gave best results was using ST7789 display + a Fast C implementation of its driver, which though required recompiling Micropython, which wasn't very pleasant nor portable in my opinion.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: SSD1306 Refresh / skipping interrupts?

Post by pythoncoder » Wed Jul 15, 2020 4:50 pm

Displays and drivers are of two types. In some cases the frame buffer is on the microcontroller, in others it's on the display. Both have advantages and drawbacks. If the frame buffer is on the microcontroller, then copying it to the display is slow. I2C is much slower than SPI. On the plus side, a MicroPython display driver class which subclasses the built-in framebuf class, can be quite simple because it inherits the framebuf's fast graphics primitives.

If the frame buffer is located on the display, the display hardware takes responsibility for drawing. To draw a line you send a command to the display and it does it. For a filled rectangle you send a command and some args which is quicker than sending a whole stream of pixels. An example is the official LCD160CR display driver and hardware. Displays with large numbers of pixels usually have their own frame buffer for performance reasons.

I would think the second type of display would be better suited to your application.
Peter Hinch
Index to my micropython libraries.

User avatar
spookyrufus
Posts: 12
Joined: Wed Jul 15, 2020 8:09 am
Location: Estonia
Contact:

Re: SSD1306 Refresh / skipping interrupts?

Post by spookyrufus » Thu Jul 16, 2020 11:12 am

Thank you Peter. Everything clear, and very helpful.

kml27
Posts: 3
Joined: Sat Jul 25, 2020 7:32 am

Re: SSD1306 Refresh / skipping interrupts?

Post by kml27 » Sat Jul 25, 2020 8:39 am

I'm trying to add a board definition for ESP32 that supports the SSD1306 display, for this dev board https://github.com/Heltec-Aaron-Lee/WiF ... iagram.PDF and code here https://github.com/HelTecAutomation/Hel ... r/src/oled

What build process/driver code are you using to include the SSD1306 in the ESP32 build (or are you copying over the library/module from the drivers directory after the fact)?

I know the pin outs on this dev board may differ from your breadboard pin outs since, from what I've read, there's pins for multiple I2C busses on the ESP32.

Is this similar to your breadboard wiring? https://randomnerdtutorials.com/esp32-s ... duino-ide/

I'd like to build out the ESP32 and ESP8266 board definitions (even if they're for breadboards) and the build toolchain with Docker more extensively if you're interested in working on either of those at all either. (Espressif seems to be indicating in their github that the RTOS SDK for 8266 is being ported to ESP-IDF).

I'll be testing the _thread library tomorrow for my project in which I also plan to use blocking I/O (though multiprocessing might really be needed in ESP32 to handle blocking I/O as we might hope). I'd expect the two I2C and dual xtensa cores *might* be able to handle this depending on the other aspects of the architecture design.

You might already be able to take resolve your issues using double buffering for your video framebuffer and copying to the display. Although there may be some benefit to working out how to copy to the display using one proc core while reading from the rotary i2c and rendering the upcoming framebuffer frame with the other (though the copy might take a different amount of time than the rotary read and second proc framebuffer render, if it's less than you're good, despite the 1-2 frame(s) of latency given the ESP32 clock rates vs. human visual system 'acceptable' refresh rates).

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

No changes to the build system required

Post by pythoncoder » Sat Jul 25, 2020 3:10 pm

The Heltec driver is written in C, whereas the official MicroPython driver is written in Python. Consequently the official driver merely needs to be copied to the target's filesystem.
Peter Hinch
Index to my micropython libraries.

Post Reply