7 February 2025
Now for some odd reason the display is not working at all today. Ummm, well, no, I’m wrong. It was working just fine. It was just displaying a screen full of zeros, as was right and proper for it to be doing. I was messing around with the screen initialization values, poking various bit patterns in to see where they showed up. Yesterday, the dots would show up in a random-seeming column. As I had not specifically programmed the column address, that was fine and to be expected. But today, oddly, the column pointer was randomly set to one of the ‘invisible’ columns: 0, 1, 130, 131. The SH1106 supports a 132 x 64 display, but this module has a 128 x 64 OLED attached. The designers decided to put it in the middle of the columns, starting with column 2. Again, fine and something that I was already aware of. But disconcertng when you think things are ‘going great’ and suddenly nothing works anymore.
One good thing about this diversion was that I had the opportunity to measure the screen update time to be ~24 ms, which gives an effective frame rate just over 40 Hz. So that’s not going to be the bottleneck that I thought it might be. I’m really not motivatated at this point to try to up the SCL frequency in hopes of a maximized data rate.
Because of the way the SH1106 wraps around from the end of a page to the beginning of the same page, it truly doesn’t matter where you start writing values, as long as you write 132 of them. If it’s all zeros, you can’t see any difference. If it’s a proper image, then it does matter.
The reason I was tinkering with the initialization values is that I had been experimenting yesterday with it and not being happy with the outcome. I eventually added a separate ‘clear screen’ loop that wrote zeros to all the memory and that did the trick. So instead of initializing the data in the frame buffer declaration as ‘{ 0 }’, which I thought would populate all of the elements with zeros, I just specifiy ‘{ }’, and the compiler treats it as ‘uninitialized’ and writes zeros in there for me.
Having a frame buffer for the display is nice. I no longer have to think about accessing the display’s memory buffer in pages and stacks of pixels. This allows me the freedom to think about designing glyphs in their appropriate sizes, not what is mathematically convenient.
I’d like to be able to use a Cartesian coordinate system to refer to the individual pixels on the display, in furtherance of my graphical ambitions. In one respect, half of the work has already been done for me, as the abscissa, also known as the x coordinate or column, maps directly to the index of an array I set up to represent the frame buffer. The ordinate, or y coordinate or row, has to be broken down into two components: the memory page index and a bitmask.
The frame buffer is built as an array of pages, with each page containing a three byte header and another array of 132 bytes. The three byte header contains the secret language of the SH1106 and allows me to just blast the entire 135 byte payload to the module and have it magically go to the right place within the OLED’s memory map.
Each page is defined by this typedef’d structure:
typedef struct { // data structure for holding display data with OLED header
uint8_t control_byte_1;
uint8_t page_address_command;
uint8_t control_byte_2;
uint8_t page_data[SH1106_WIDTH];
} SH1106_PAGE_t;
My frame buffer is just an array of these pages:
SH1106_PAGE_t SH1106_frame_buffer[SH1106_PAGES];
where I have previously #define’d various dimensions as:
// specific SH1106 module parameters are defined here
#define SH1106_WIDTH 132
#define SH1106_HEIGHT 64
#define SH1106_PAGES 8
Assuming we stay in Quadrant I of the Cartesean plane, arguably the best quadrant, with the origin (0,0) in the lower left corner, the x coordinate maps directly to the index of the page_data[] array. That part was easy.
The y coordinate is only a bit more complex. Given the range of 0-63 of possible y values, we can represent that with a 6 bit integer. The upper 3 bits determine the page number, which is the index into the frame buffer array, and the lower 3 bits identify a single bit within what I refer to as a ‘stripe’ in the SH1106 memory. It’s a short, vertical space, one bit wide and 8 bits tall. The lowest bit is the top-most spot within the stripe.
Now if we acted like we didn’t care, we could just take the three upper bits of the y coordinate and call that the page number. That would have the consequence of giving us a plane mirrored about the x axis, as page 0 is at the top and page 7 is at the bottom. We just need to subtract the upper 3 bits from 7 to get the right-side-up, happy Quadrant I orientation that I happen to prefer. So a little more complex, but not much.
So having now spelt this out in people jibber-jabber, it’s time to encode this into a series of mathematical transformations and some hopefully readable source code.
My first function will be the point() function. Technically, a point has no dimension, only a location. Our ‘points’ actually have a size of ‘one’ in both dimensions, but they do have a location that can be specified as offsets from the origin of our Cartesean coordinate system.
The parameters of the point() function should include the x and y coordinates as well as a ‘color’ value. Being a display of modest ambition, this OLED supports the binary options of ‘on’ or ‘off’. We can reporesent that as a one or a zero in the code.
I have taken the liberty of formalizing the available color palette:
typedef enum { // all the colors
COLOR_OFF = 0,
COLOR_ON = 1
} COLOR_t;
Now I am making an exective-level decision to have the graphics functions pretend that the display is only 128 x 64 pixels in extent. Perhaps this will save me some time in the future and keep me from looking for ‘invisible’ pixels that are there but hiding just off stage.
I will have to try to remember to update the display after these functions, as they only manipulate the contents of the frame buffer but do not actually communicate with the OLED.
So here is the point() function as it currently stands:
void point(uint8_t x, uint8_t y, COLOR_t color) { // plot a single point of color at (x,y)
uint8_t page = (SH1106_PAGES - 1) - (y >> 3); // top three bits represent page number, reversed to be in Quadrant I
uint8_t bit_mask = 1 << (y & 0x07); // bit mask of pixel location within display memory stripe
x += 2; // move into visible portion of OLED screen
if(color == COLOR_OFF) { // we'll reset a bit in the memory array
SH1106_frame_buffer[page].page_data[x] &= ~bit_mask; // clear bit
} else { // we'll set a bit in the memory array
SH1106_frame_buffer[page].page_data[x] |= bit_mask; // set bit
}
}
I realized later that I could just invert the top three bits of the y coordinate instead of subtracting them from ‘one less than the number of pages’. Either way seems equally obtuse.
And it works! Why am I always so surprised when anything does as expected?
Now to see how performant this little manifestation of my algorithm can be. I’ll write a loop that sets and then clears all the pixels, one by one. If it’s visibly slow, I’ll have to think about spending some time optimizing the process. If not, I’m not going to worry about it.
It’s pretty fast. It causes a brief flash on the screen, and then it goes blank again, all pretty quickly. There is a visible ‘tearing’ artifact across the bottom of the screen in this process.
Looking at the oscilloscope, I measure ~18 ms to write ones to the screen, and ~16.4 ms to write zeros. That’s a surprising difference. Given there are 8,192 indiviual pixels to be written, the setting function, including the loop overhead, is taking ~2.2us and the clearing function is taking 2us per pixel.
So it takes less time to set or clear all the pixels in the frame buffer than it does to send them to the display via I2C. Good to know.
Here is where, historically, I go nuts writing a bunch of optimized graphics primitives, such as vertical, horizontal and ‘other’ lines, filled and unfilled rectangles and circles, etc.
But for now I want to pretend to focus on actually finishing this project and resist the urge to write yet another library of functions that may or may not ever get used.
So now we will proceed to fonts or glyphs, as you prefer. The first one is always the most interesting. I’ve already got one that I like and will start there, but it was designed to be small and permit a larger amount of text on the screen at one time. One of the overall goals of this project is to make it at least somewhat visible and legible at a distance, so larger formats will be needed.
This brings me back to the need for a better font design tool. I’ve spent way too much time typing in ones and zeros and squinting at the screen while transcribing hexadecimal numbers. I have serached for a more appropriate tool that is already in existence but have yet to find anything that works within the constraints of this project. I feel yet another tangent coming.
Well, before embarking on the world’s greatest font design tool tangent, I’ll have to be happy with a tiny side quest. I noticed that to accomodate the discrepancy between the SH1106 memory and the physical OLED screen width, I had hard-coded a “+2” to the x coordinate in the point() function. The solution was to add a couple of new fields to the page structure to align with the ‘invisible’ columns on the left and the right side of the screen.
That part was easy. Modifying the dimension of the page_data member to not use what looks like (and totally is) another magic number, I had used a uint16_t as the requisite padding on each side, which is exactly two bytes long, then used the friendly-looking (not really) equation:
SH1106_WIDTH - (2 * sizeof(uint16_t))
as the number of elements. So it should still send out all 132 bytes of the frame buffer, but we don’t have to offset the x coordinate every single time. That saves about 20ns per pixel!
Now that’s fixed, I went back and checked the ‘single pixel at the origin’ test and noticed that sometimes the pixel seemed to travel along the bottom edge of the screen. That’s because nowhere was I setting the column address to 0, or to anything else, either. It was going to be whatever it happened to end up being. After a power on reset, the module is supposed to reset the column address to zero, and I’m sure it does. But I have updated the initialization sequence to specifically set the column address to zero. This is done in two steps, as there is a single byte command to set the lower four bits of the column address and another to set the four upper bits. Here is the new sequence:
uint8_t SH1106_init_sequence[] = {
SH1106_NO_CONTINUATION | SH1106_COMMAND, // control byte
SH1106_COMMON_REVERSE, // swap column scanning order
SH1106_SEGMENT_REVERSE, // swap row scanning order
SH1106_COLUMN_LOWER, // column address lower 4 bits = 0
SH1106_COLUMN_UPPER, // column address upper 4 bits = 0
SH1106_DISPLAY_ON, // command to turn on display
};
Now my little pixel is just where it belongs… or is it? Honestly, it’s pretty hard to see. One way to test this is to draw a single line rectangle around the edge or the screen and make sure all edges are visible.
Which reminds me that I am not checking the input arguments to the point() function. I’ll just do a quick test and silently return on out-of-bound values.
So I added a couple of quick argument checks to the point() function that just return on out-of-bounds values. Another option would be to simply mask off the invalid bits and look like we’ve “wrapped around” after passing the edge of the screen.
So the rectangle test shows that there is still room for improvement in my equations. It’s hard to describe, exactly, but it looks like each page starts writing a little to the right of the previous page, so that the ‘vertical’ lines are distinctly leaning.
One thing is for sure, and that’s that my bit mask formula is exactly backwards. In retrospect, I see it now. The larger the y value, the lower the bit position within the strip should be, not the other way. I replaced:
1 << (y & 0x07)
with:
0x80 >> (y & 0x07)
and the horizontal lines seem to be right on the edge of the screen now.
But each page is still scooched over one pixel to the right after the last page. This could be caused by sending out one too many bytes per page in the update function. As the function uses the reported size of the page structure as the byte count, it occurred to me that the compiler was padding the struct somehow. Adding the modifier ‘__attribute__((packed))’ to the struct declaration fixed the problem. This is not the first time that structure packing issues have created off-by-a-little-bit errors for me, especially in communication protocols.
Now my rectangle looks properly rectangular. Going back, I also check that the origin pixel is very decidedly in the lowest leftest spot. With just the right amount of background light, I can barely see the edge of the OLED grid.
Now I can import my existing, hand-crafted OLED font from another, similar project. The font is contained in a C source code file named ‘font_5x8.c’ from the previously-mentioned C8-SH1106 project for the 203.
Copying the bits out of the font definition array and writing them to the frame buffer works like a charm.
I put that code in a little loop to go through and print all the available characters, and it goes by a bit too quickly to be able to see what it happening. I added a short delay to the loop and it’s quite satisfying to see it working so well. Here is the code:
for(uint8_t glyph = 0x20; glyph < 0x80; glyph++) { // all the characters in the font file
for(x = 0; x < 5; x++) { // columns
for(y = 0; y < 8; y++) { // rows
if(font_5x8[glyph][x] & 0x80 >> y) {
point(x, y, COLOR_ON); // draw the pixel
} else {
point(x, y, COLOR_OFF); // erase the pixel
}
}
}
SH1106_update(); // let's see what happened
Delay_Ms(250); // short delay
}
The Delay_Ms() function is provided by the boilerplate example project generated by the MRS2 software when asked to create a new project.