MC2023 Chapter 4: I2C and an OLED

Page created: 2023-04-24 , updated: 2023-05-07

Welcome to the fourth "chapter" of my Year of the Microcontroller!


Today I read through Chapters 10 and 11 of Programming the Raspberry Pi Pico in C. These chapters cover the SPI bus. I’m going to come back to these if I find myself using any SPI devices. Most of the parts I’ve been interested in using seem to use I2C instead.

So I’m on to Chapter 12 now, which covers the I2C bus. I did I2C communication in Micropython for my multitimer a year and a half ago and I had to debug it at the bus level, so I feel pretty good about the basics.

I have some tiny I2C OLED screens coming tomorrow. I’ve always wanted to use these because they look so beautiful. And I can’t believe cheap these are now!.


Excellent day!

My goal is to do some I2C communication with nothing but the SDK.

But first, I wanted to confirm beyond a doubt that my screen uses the Solomon Systech SSD1306 chip to drive the tiny 128x64 pixel OLED display. So I looked for an existing library to test it.

I immediately found Harbys’s SSD1306 OLED Library for RP2040 ( Then it was just a matter of hooking up the ground, power, I2C Data, and I2C Clock lines. Followed the README example and had drawn a line on the screen right away!

(The only "problem" I had was trying to #include the .cpp library from my .c file, which was amusing. The solution: rename my main application from .c to .cpp.)

The Harbys library also has font rendering and four fonts to choose from, so I was able to lay out some text.

shot of the oled with keyboard for scale

This screen is just 21.74mm wide (less than an inch, for us primitive Americans). And the 5x8 font I’m using in the yellow area is itty-bitty.

close-up of the oled with test text in different font sizes

It should be noted that the yellow and blue are areas on the screen. I didn’t choose to write the first two lines in yellow, the text just happens to fall in the yellow area. This is a monochrome display.

Next, I’m going to try to talk to it myself without the library. Between my book and the datasheet (, I should be able to figure it out.

2023-04-25 - 2023-04-29

Read the datasheet and made notes for a couple days.

The I2C info starts on page 19 of the datasheet so I printed out a chunk starting on that page and am making notes right on the paper. It’s gonna be a bit slow going, but I’m learning (or re-learning, or sometimes merely confirming) a lot.

After that, I started my attempts to setup up the screen and display a pixel.

The hard part is working blind until something shows up. In addition to the datasheet, I also peeked at Harbys’s library and also at Natesh Narain’s SSD1306 OLED Display Driver using I2C ( article.

Many of the device settings seem almost hopelessly arcane. But you gotta just keep writing notes and documenting and eventually it starts to make sense.

When I first managed to see anything using just my raw I2C communication, I felt like Tom Hanks in Cast Away saying, "I have made fire!"

oled screen displaying random pixels

Until you specifically set the pixel data, the screen just shows whatever random bits happen to be set in RAM. It’s a lot like seeing the static in an old television set. I like to think it’s even from the same cosmic ray source, though it’s probably not. Also, they don’t move because once it’s on, the RAM doesn’t change unless you change it.

2023-04-29 - 2023-05-03

I can draw boxes!

yellow and blue boxes drawn to the display

(Note: These are supposed to be squares. What I didn’t realize until later is that I didn’t have my setup quite right yet. The gaps between the pixel rows are lines that haven’t been drawn yet!)

After I learned how to specifically place pixels on the screen, I thought it would be cool to get input from the serial UART connection.

So I made a dirt-simple while(true) loop to gather characters from the serial connection and draw them as raw pixels to the screen.

Then I could open a connection with minicom to /dev/serial0 and type directly on the screen in this pixelated output. (I also echoed the characters back so I could see what I was typing in minicom.)

What’s tricky about this display controller is that you draw an entire "page column" with one byte of data. Each bit in the byte is one pixel in the 8-pixel column.

rows of pixels in a corner of the screen

Every column in this image is one ASCII character. The least significant bit of every character is at the top. If you’re really patient, you could decode whatever nonsense I was typing when I tried it out in the image above.

Even though it’s silly, it was neat to finally have something interactive!


The next thing that occurred to me is that if I could stream ASCII characters to the display, why couldn’t I display an entire image by sending it over the serial connection the same way?

I knew of the PBM 1-bit image format (

And with the help of this wiki (, I learned how to make one in GIMP.

(Trust GIMP to make arcane stuff easy and easy stuff arcane, ha ha.)

Here is the image I created to display:

black and white pixel picture of cat eyes and a box that says meow

It’s from a photo of my cat, Mr. Jackson. The box that says "Meow" is the exact height of the yellow portion of my OLED display (16px). Don’t worry, the image is way less creepy with the light and dark pixels inverted.

And this is one of several test images I used to debug my image conversion and display:

nested black and white rectangles

I started writing a program in Zig to convert the PBM to packed bytes in the SSD1306’s page/column order.

2023-05-05 - 2023-05-07

I switched from the binary PBM format to ASCII (thank you, GIMP, for having this option) because with the ASCII format, I could easily see the file as I figured out how to parse it. Also, with ASCII, there was no need to unpack the bits because they’re '1' and '0' characters.

Then I figured out how to convert this image data to the format the screen controller would understand. That took an evening and most of a weekend.

A good portion of that time was spent with paper, pen, calculator, and a comfortable couch.

paper notes about ssd1306 ram positions

One of the more helpful things I did was create a little math.zig program (in the repo linked at the bottom of this page) that prints out a list of "interesting" image positions that cross page and column boundaries. I knew it was some combination of integer division and modulus division. Seeing my experiments with these update all of the values at once "spreadsheet-style" was extremely helpful.

As I mentioned above, each pixel of the display is held in display ram that is broken into 128 byte columns per "page". There are 8 pages, giving 64 pixels of height.

To convert image data in the usual left-to-right and top-to-bottom scan order, each bit has to be packed into the correct byte at the correct page.

In order to assist with this, I also created a terminal viewer program, also in Zig. It uses ASCII '@' and '.' characters to display the light and dark pixels.

Other than a tricky bit shift operation, I found the viewer to be much simpler to write because it can loop over page/row/column and extract a bit from the byte at that location.

(By the way, if you ever have to shift an integer by a variable amount in Zig and get an error that looks something like this: note: unsigned 6-bit int cannot represent all possible unsigned 64-bit values, then it will be helpful to know that the shift amount (not the value being shifted) needs to be cast to a restricted size so Zig knows you won’t be causing a shift overflow. In my case, I needed const shiftby = @intCast(u3, bit); to ensure I wouldn’t shift a u8 by more than 7 places (7 is the largest number that will fit in a u3 integer!). No doubt it is immediately apparent to you, but I had do sleep on it before I could figure out what the Zig compiler was trying to tell me.)

Here are the two PBM images above as piped through my converer and then directly into my terminal viewer:

ascii art rectangles

The rectangles are pretty boring, but the ASCII art cat eyes look pretty cool:

ascii art cat

To save the images to disk, I just redirected them to files. Truly, UNIX is glorious sometimes.

$ zig run convert.zig <foo.pbm >foo.bin               # save
$ zig run viewer.zig <foo.bin                         # view saved
$ zig run convert.zig <foo.pbm | zig run viewer.zig   # just view

(Actually, in the screenshots above, I was redirecting to files as an intermediary step just to save time since I wanted the files anyway.)

Then it turned out to be equally challenging to figure out how to send a file via the USB/UART serial connection.

I thought that would be the easy part!

six pictures of test images

Here you can see the distorted cat eyes image and some different test images (the FOSS drawing program Krita has pixel art brushes and can also export directly to binary PBM.) The square with the diagonal lines was especially weird and revealing because that test image was supposed to have only ONE diagonal in the square from corner to corner.

I tried minicom and screen before settling on configuring the device with stty and just redirecting the raw image data directly to it!

$ stty -F /dev/serial0 9600
$ cat foo.bin > /dev/serial0

I spent hours reading the stty man page and various documents online. That stuff is straight-up Lovecraftian Old Ones speech. I’m pretty sure I summoned a demon at some point because I had to reboot before anything would even transmit over the device again.

You’ll also note that the only thing I’m setting above is the baud rate 9600. That’s because I found it was far easier to configure the connection from the Pico SDK than try to figure out how to get the Linux device just right.

So the "default" of 8 data bits, 1 stop bit, and no parity is what I ended up with.

During this time is also when I found out why there were blank lines between the rows in all of the images above (and those duplicated diagonal lines!). It was a "COM" pin setting in the SSD1306 controller than can be configured to match how the hardware is setup (you also have to flip the image vertically on my device). All of the working settings are in the oled3.c source file in the repo linked at the bottom of this page.

At last, I had a perfect test image:

perfect straight rectangles on oled screen

(By the way, that line between the yellow and blue areas is not a bug. It’s a gap between the color portions in the physical display itself.)

And here’s the cat eyes I’d been trying to display for four days:

cat eyes on oled screen with each row of 8 pixels off by one pixel

But for reasons I do not yet understand, the cat image has each page shifted off by 1 column!

You would think the image is one column too wide or something. But it’s not. My converter and viewer programs will both complain and exit if the image isn’t the exact correct dimensions. And it’s the exact same file size as the test image. Everything checks out when I view it with the xxd hex viewer as well.

But clearly it’s something to do with this specific image because when I send it multiple times, the columns keep shifting 8 pixels per send as you can see here:

cli screen shot sending image multiple times and the badly shifted cat image

I can send the rectangle test image as many times as I want and it stays perfectly straight. But this image shifts the pointer one pixel per page and I have no idea why.

I might eventually figure it out. But for now, I have an italic cat image displayer, which is a lot more than I had four days ago.

For now, I’ll use the Harbys library if I want to display text on this display. I think it will be very useful to have an on-board output method for debugging.

I’m extremely pleased to have made this work.

All of the source files for everything above are in this repo:

That rounds out this "chapter". The next one will follow the Harry Fairhead book into the realm of PIO (the Pico’s programmable I/O).