I have looked at many examples, blogs and websites to try to get an output from an SSD1306 OLED connected to the Pynq Z2 Arduino pins or to a Grove Arduino shield.
If you are interested to understand my testing and experimental logic, read on. If you just want a screen to display something, jump to the "SOLUTION" section below, you can always come back.
I don't have a Grove PMOD adaptor and I understand that they are no longer being sold so I am unlikely to get one. The Grove Arduino Shield that I have is a Seeed Arduino Sensor Kit Base shield. I took all of the sensors off the board during testing to avoid any unexpected interactions.
The Seeed Arduino Sensor Kit Shield is not (internally) wired the same as the Pynq Grove shield, you need to look carefully at the pins that are used if you are going to try to complete any of the Pynq Jupyter examples using the Seeed board. I'm not actually promoting the Seeed board but according to Digilent: "The PYNQ Grove Add-on Boards are retired and no longer for sale in our store." which does restrict your choices. The wiring for the Seeed and Pynq boards IS the same for I2C.
I have had two main problems with the posted Jupyter I2C OLED examples that I have found:
1) Many threw up errors that I found hard to understand. I have seen lots of questions on websites asking about the same errors; the answers seem to be fairly flippant and unhelpful. Maybe if you have 20 years experience of this board it may mean something to you (in which case you should not be on this page as it will only frustrate you that someone can ask foolish questions because, unlike yourself, they weren't born knowing how to do this)
2) Some / several examples seemed to do absolutely nothing. The code runs, after a while you get a nice reassuring number next to the Jupyter code box but the display is blank. After verifying that your OLED works AT 3.3V on an Arduino or Raspberry Pi and you check, double check the SDA and SCL connections and them swap them over a couple of times, rebooting the Pynq board just for good measure (Only swap the SDA and SCL if you are using bare pins into the Arduino header. If you are using the Grove connectors, just check the writing at each end)
There was a 3 as well, sometimes the example throws an error saying that the device is not connected or does not exist. This is a confirmation message from some of the drivers whereby I2C address 0x3C (or actually any other address) is tested for a response. The Pynq doesn't work like that; if you run "!i2cdetect -y -r 1" in a Jupyter box you will not see any attached I2C devices (other than 0x3b, which is there when there is noting plugged in to the I2C pins). In the OLED case, 0x3C is tested and does not respond, the driver ends at that point and nothing more is sent.
So I tried a lot, including adding scopes to SDA/SCL and seeing waveforms, adding logic analysers and reading clocks and data (sometimes only clocks, check the SDA definition if it happens. You probably don't need the definition, just [] if anything)
Finally I stumbled on one last Jupyter example. I was trying to send individual I2C commands because I had read that the OLED needs some initialisation and while it seemed that it was being done, I wanted to check. Suddenly the OLED displayed some text that I had not sent! I tried a few times and had the same text back even though I still hadn't sent it.
Checking back, the text was sent by one of my earlier attempts. When I reran that attempt the display flickered off and back on. I changed the text and the display changed. Everything seemed to be working.
After a short break and a reboot, I tried again but the example that was working was now not working. It took me a few minutes to backtrack. Running the example to send individual I2C commands made it and many of the other examples work. That example was obviously initialising the OLED when the other examples did not. I think that I tried this example before but it gave me a blank screen, it must have been that there was nothing in the OLED memory to display until I ran one of the other examples.
Jolly frustrating few days but very satisfying at the end. Now I just need to remember what it was that I wanted to display!
The SOLUTION
Here is the Jupyter file for you to do with as you wish. It is a mash up of other files. I only take (minor) credit for putting them together and for some re-ordering. I am going to reuse the first part for other I2C devices if needed as it can read and write individual I2C commands.
Connecting PYNQ Grove OLED to Pynq-Z2 board Either use I2C connection on Arduino Sensor Kit Base shield or direct to Arduino connector pins. Needs Gnd, 3.3V, SDA and SCL (dedicated or I2C connector on shield) Each time the OLED is powered up it needs to be initialised before it can be used, Grove_OLED driver does NOT manage this alone, some direct i2C commands are required first.
import time
from pynq.overlays.base import BaseOverlay
from pynq.lib import MicroblazeLibrary
base = BaseOverlay('base.bit')
arduino_lib = MicroblazeLibrary(base.ARDUINO, ['i2c'])
i2c_bus = arduino_lib.i2c_open_device(0)
OLED_ADDRESS = 0x3C
def oled_command(cmd):
#Sends a control directive command using the correct write syntax.
# Syntax: i2c_bus.write(slave_address, data_array, number_of_bytes)
i2c_bus.write(OLED_ADDRESS, [0x00, cmd], 2)
def oled_data(data_bytes):
#Sends pixel/character payload streams directly into GDDRAM display memory.
## 0x40 is the Control Byte flag indicating subsequent bytes are raw pixel data
for byte in data_bytes:
i2c_bus.write(OLED_ADDRESS, [0x40, byte], 2)
# Standard SSD1306 Initialization Routine sequence
init_sequence = [
0xAE, # Display OFF
0xD5, 0x80, # Set Display Clock Divide Ratio
0xA8, 0x3F, # Set Multiplex Ratio (64 lines for 128x64 display)
0xD3, 0x00, # Set Display Offset to 0
0x40, # Set Display Start Line to 0
0x8D, 0x14, # Enable Internal Charge Pump (Crucial for module power)
0x20, 0x00, # Set Memory Addressing Mode to Horizontal
0xA1, # Set Segment Re-map (Flip Horizontally)
0xC8, # Set COM Output Scan Direction (Flip Vertically)
0xDA, 0x12, # Set COM Pins Hardware Configuration
0x81, 0xCF, # Set Contrast Control Value
0xD9, 0xF1, # Set Pre-charge Period
0xDB, 0x40, # Set VCOMH Deselect Level
0xA4, # Output follows RAM content
0xA6, # Set Normal (Non-inverted) Display Mode
0xAF # Display ON
]
for cmd in init_sequence:
oled_command(cmd)
time.sleep(0.01)
print("OLED Screen initialized successfully!")
#from pynq.overlays.base import BaseOverlay
from pynq.lib.arduino.arduino_grove_oled import Grove_OLED
print("OLED Screen imports completed successfully!")
# Load the active FPGA layout for Grove_OLED (cannot reuse "base" from earlier)
ol = BaseOverlay('base.bit')
# Initialize the SSD1306 (Grove_OLED) peripheral with fixed I2C pins SDA SCL
oled = Grove_OLED(ol.ARDUINO, [])
print("OLED Screen connected successfully, ready to use!")
Some tests:
oled.clear()
oled.set_normal_mode()
oled.set_position(0,0)
oled.write("Hello PYNQ-Z2")
oled.set_position(2,0)
oled.write("SSD1306")
oled.set_position(4,0)
oled.write("Me! **")
# wraps and overwrites first line on some displays!!
oled.clear()
oled.set_normal_mode()
oled.write("Hello PYNQ-Z2 ") # this line might be missing!
oled.write("SSD1306 ")
oled.write(" Me! ")
oled.clear()
oled.set_normal_mode()
oled.set_position(0,0)
oled.write("Hello PYNQ-Z2")
oled.set_position(2,0)
oled.write("SSD1306")
oled.set_position(4,0)
oled.write("Me! **")
# using the set position command
oled.clear()
oled.set_normal_mode()
#oled.set_position(0,0)
oled.write("Hello PYNQ-Z2")
oled.set_position(2,0)
oled.write("SSD1306")
oled.set_position(4,0)
oled.write(" Me")
oled.set_inverse_mode()
oled.clear()
oled.set_horizontal_mode()
oled.set_position(0,0)
oled.write("Hello PYNQ-Z2")
oled.set_normal_mode()