I bought the Adafruit Monochrome 1.12" 128x128 OLED Graphic Display from Pimoroni. Time to investigate.

I wrote a few notes here, but it’s a bit cryptic. I still have the code, but I’d like to document it a bit better. Of course, there is the datasheet (which is linked from the product page), so I’m not going to repeat everything it says. But having a few more code snippets could help.

First Attempts

I found some code that does something. First of all:

#define I2C_PORT i2c0
#define I2C_SDA 20
#define I2C_SCL 21

#define SH1107_ADDRESS 0x3d

The default I2C address is written on the board itself. I’m using a 4 Pin JST-SH Cable, where blue=SDA and yellow=SCL.

To initialise, we need to look at a few commands. I’m using this:

static uint8_t INIT_SEQUENCE[] = {
        /* display off */
        0x80, 0xae,
        /* vertical addressing mode */
        0x80, 0x21,
        /* display offset = 0x60 */
        0x80, 0xd3,
        0x80, 0x60,
        /* scan COM[n-1] to COM0 */
        0x80, 0xcf,
        /* segment re-map disabled */
        0x80, 0xa0,
};

According to the datasheet:

A command word consists of a control byte, which defines Co and C/D (note1), plus a data byte (see Fig.9).

The MSB is the continuation bit. When it’s 1, it means that the data byte is followed by another control byte. When it’s 0, only data bytes follow. The second most significant bit says whether the data byte is a command (0) or RAM data (1).

So 0x80 says that the next byte is a command, and there’s more.

Vertical Addressing Mode

There are two memory addressing modes. In page addressing mode, each page is 8 rows, and each byte written (or read) is a column of 8 bits. That means that writing 0x1f 0x23 (as data bytes) would result in the following two columns:

1 1
1 1
1 0
1 0
1 0
0 1
0 0
0 0

In vertical addressing mode, the second byte goes to the next page (I won’t add a diagram - it would have 16 rows and a single column). While this feels even more awkward than page addressing mode, the chip allows us to control whether the pages to from top to bottom or from left to right (or the opposite of those two). If the pages go from left to right, then suddenly this is a “natural” byte order. The bytes stack from left to right, which could be easier to use.

To be honest, it doesn’t really matter. It would only matter if we supported partial blits and were to guess what kind of rectangles would be more common, etc. But generally software can deal with any addressing mode.

I think that this works together with the “Set Common Output Scan Direction” command, but maybe I should get rid of this and just work with the default addressing mode.

Moving On

Without further ado, let’s look at turning the display on and off:

static uint8_t DISPLAY_ON[] = {
        /* display on */
        0x00, 0xaf
};
static uint8_t DISPLAY_OFF[] = {
        /* display off */
        0x00, 0xae
};

0x00 means “no more commands, next byte is a command” (as explained above).

So now the question is: how do we update the display? The answer is the following sequence of commands:

        *p++ = 0x80;
        *p++ = i & 0xf;  /* lower column address */
        *p++ = 0x80;
        *p++ = 0x10 | (i >> 4);   /* higher column address */
        *p++ = 0x80;
        *p++ = 0xb0;     /* page address */
        *p++ = 0x40;

This snippet shows the idea - set the column address, set the page address, and then 0x40 to say “no more commands, next sequence of bytes is RAM data”. So the main question is about addresses. This really depends on the addressing mode (clearly). Let’s use the default, which is page addressing mode. In this mode we:

  1. Set the column address to 0
  2. Set the page address to 0 .. 15
  3. Write 128 bytes (a page)

This means that if we write 128 0xff and 128 0x00, alternating, we should see a zebra crossing. Let’s give it a go.

INTERFACE Library

Before I start coding, what I really want is a library for this board, to avoid copying and pasting the same code between projects. The library should be and INTERFACE library, and would contain all the shared code. The build system would take what it needs.

I have now spent three days (evenings only, of course), trying to make this work. So far I have the following. For the library:

cmake_minimum_required(VERSION 3.28)
project(cymric C)

set(CMAKE_C_STANDARD 11)

add_library(cymric INTERFACE)
target_include_directories(cymric INTERFACE ${CMAKE_CURRENT_LIST_DIR})
target_sources(cymric INTERFACE ${CMAKE_CURRENT_LIST_DIR}/library.c)

And in the main CMakeLists.txt:

add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../cymric ${CMAKE_CURRENT_LIST_DIR}/../cymric/build)

And then I can just add cymric as a library.

By the way, cymric is a cat breed.

We’ll see how adding some Pico dependencies to the library changes things, but I’m happy that this works.

Display On

To start off with:

    sh1107_init();
    sh1107_display_on();
    sleep_ms(2000);
    sh1107_display_off();
    sh1107_shutdown();

That shows a random image for 2 seconds, and the display then turns off. Fabulous.

image

A simple attempt to figure out the orientation of the screen and how the code can update it:

void sh1107_display_demo(void)
{
    uint8_t preamble[] = {
        0x80, 0x00,  // column low bits
        0x80, 0x10,  // column high bits
        0x80, 0xb3,  // page address
    };
    uint8_t buffer[1 + 128];
    uint8_t *p = buffer;
    *p++ = 0x40;  // data
    for (int column = 0; column < 128; column++) {
        *p++ = 0x81 | (column & 0x3f) << 1;
    }
    i2c_write_blocking(I2C_PORT, SH1107_ADDRESS, preamble, sizeof preamble, true);
    i2c_write_blocking(I2C_PORT, SH1107_ADDRESS, buffer, sizeof buffer, false);
}

Page address is 0x03, and I’m using a pattern where the LSB and MSB are 1 and between them the column number modulo 64 is displayed. The result:

image

So this is actually as expected (surprise!) - page address is from the top, and columns are left to right.

With this, I should be able to display a nice pattern.

image

It’s been a while since I last coded this, and this time I just copied the algorithm directly from Wikipedia.

I think that it’s quite enough for now. I have some code to initialise the display, turn it on/off and set/clear a pixel. Drawing text can be a bit more efficient than one pixel at a time (using a 6- or 7-row fixed width font would allow me to just copy one letter at a time). But that’s all boring detail.