Posted on Leave a comment

Notes on RISC-V Assembly Language Programming – Part 13

6 February 2025

Today’s first objective is to capture and measure the SCL signal and see how close it gets to the requested 400 KHz that I specified in the I2C initialization function.

After attaching an extension cable in order to tap into the SCL line going to the OLED module, I measure a SCL signal trying so hard to wiggle at 423 KHz, which is almost 5% over what I specified. Again, it’s not a critical value, as I have successfully run these OLED displays at 1 MHz in the recent past.

Debugging the program, I can look at the I2C registers directly and see what has been set up for me. The CTLR2 has a field named FREQ, and it has been set to 48. This is in line with what the RM indicates should be done. The CLKCFGR has a field called CCR, for the clock division factor field, and it is set to 0x04.

The actual timing calculations are shrouded in mystery, at least from the standpoint of trying to understand what the RM says. My experimentation suggests that the FREQ field has zero effect on the SCL frequency, and that the CCR field alone sets the pace. It’s also dependent on whether or not you are using ‘fast mode’ or not, as well as the selected clock duty cycle.

Also worthy of note is that the waveform has a very slow rise time and quite abrupt fall time, as would be expected from an open-drain output with no pull-up resistor to help. I have a second OLED module set up with 1KΩ pull-up resistors installed, which is considered quite stiff in the I2C community. This module’s SCL line shows much sharper rise times. So I think that in the lab it’s OK to “get away with” no pull-up resistors for testing purposes, but any final product design should certainly incorporate them. Surface mount resistors are not expensive.

The first improvement I would like to make on the existing system is to use interrupts to pace the transmission and reception of data over the bus, instead of spin loops that may or may not be monitored for a time-out condition. There are two interrupts available for each I2C periperhal on these chips, one for ‘events’ and the other for ‘errors’. I’ll need to define a state machine that is set up before any communications and serviced by the two interrupt handlers.

I will also need to come up with a suitable API to be able to hand off various payloads to the display. While the OLED controller chip allows for both reading and writing, I am not immediately seeing a strong case for ever reading anything back. So I’m thinking that the majority of transfers will be writing some combination of commands and data to the display.

The first case is the initialization phase. Ideally, the screen memory needs to be either cleared or preset to a boot splash screen, followed by commands to adjust any operating parameters. The controller chip’s built-in power on reset sequence does almost everything we need as far as setting up its internal timing. We only need to flip the ‘on’ switch to see dots. But as I alluded to yesterday, the screen on this module is mounted upside down and backwards. While there is no single “rotate 180°” command available, there are two other commands that will do effectively the same thing. One reverses the column scanning order and the other reverses the row scanning order. So we’ll need to send those two commnds before we turn on the display. There’s also a setting called ‘contrast’ that might more accurately be called ‘brightness’ that defaults to spang in the middle of the range.

Unlike the other popular OLED controller, the SSD1306, the SH1106 does not automatically roll over to the next ‘page’ of memory once it gets to the end of the row. This means that the ‘screen fill’ task must be broken up into eight page fills. Each of these must be preceded with a page address command. So the initialization ‘payload’ begins to take shape:

Address page 7, fill with 132 bytes of some pattern
Address page 6, fill with 132 bytes of some pattern
Address page 5, fill with 132 bytes of some pattern
Address page 4, fill with 132 bytes of some pattern
Address page 3, fill with 132 bytes of some pattern
Address page 2, fill with 132 bytes of some pattern
Address page 1, fill with 132 bytes of some pattern
Address page 0, fill with 132 bytes of some pattern
Reverse column scan
Reverse row scan
Optionally set contrast level
Display on

I fill the pages in ‘reverse’ order so that it ends up addressing page 0, which seems the logical place to start in the next stage. It will save at most one page address command, so this trick might get axed to favor clarity over cleverness.

The SH1106, much like the SSD1306, is a simple matrix display controller and does not offer any sort of built-in text capabilities. We have to supply our own fonts, which translates into “We get to supply our own fonts”.

I had originally used a 8×8 font that was very easy to read, but ultimately went with a 6×8 font that was, to me, much nicer looking. I then spent a lot of time writing what I considered ‘optimized’ routines to place characters on the screen in what seemed a sensible manner. Mostly this had to do with working within the constraints of the memory organization of the controller chip’s display RAM. This resulted in feeling very much blocked in to using either 8×8 or 16×16 fonts.

What I’m thinking about doing now is very different. Instead of writing each character directly to the screen’s memory, I’m going to introduce an intermediate frame buffer within the CH32X035 memory space. It’s only 1,056 bytes if we map every display location, but only 1,024 bytes if we only map the visible 128 columns that are supported by the physical OLED screen of this module. Each byte contians, as you know, 8 bits, and each bit corresponds to a single screen pixel. There are no shades of gray; it’s either on or off.

So my ‘print’ and ‘plot’ functions will actually only write to an internal SRAM-based frame buffer, and when ‘the time is right’, the whole memory will be transferred to the OLED display. This could be aided by DMA and interrupts to help off-load some of this burden from the CPU.

So that’s my plan for completely over-engineering this project and multiplying the amount of effort required to get to the finish line.

A couple of little experiments to try before diving into the big stuff. I noticed in the SDK that the GPIO initialization for the I2C port used two separate calls to the GPIO_Init() function, one for each of the two I2C signals. The library can actually set up as many pins on a single port as you need. You just indicate the pins needing initiailization with a bitmap passed in as the GPIO_Pin structure member. So I was able to combine the two calls into one:

// configure SCL/SDA pins

GPIO_InitTypeDef GPIO_InitStructure = {0};

// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
// GPIO_Init( GPIOA, &GPIO_InitStructure );

// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
// GPIO_Init( GPIOA, &GPIO_InitStructure );

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; // PA10/SCL, PA11/SDA
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

I also tried bumping up the SCL frequency to 1 MHz, via the I2C_ClockSpeed structure member passed to the I2C_Init() function. No dice. I don’t know why yet, but I might find out in the future. Right now it’s chugging along at over 400 KHz, and that should be fine for now. In theory, I should be able to push almost 38 frames per second to the display at this speed.

And now on to the Great Embiggening of the I2C API. First, I want to enable the available interrupts and get a feel for how and when they are triggered and then work around that.

The SDK provides a function to enable or disable the various combinations of available interrupts. There appears to be an additional type of event interrupt for when either the TXE or RXNE status bits are set, indicating space is now available for more of whatever was going on at the time.

Right now I just want to look at the event interrupts, then I will look into the error interrupts and once I get the DMA configured, I’ll have another look at the buffer interrupts.

Note that the I2C_ITConfig() function only enables the interrupts at the device level. It does not enable any interrupts at the system level.

To do that, we use the SDK function NVIC_EnableIRQ(). The argument to pass is the interrupt number, and it took a bit of sleuthing on my part to track it down. There is an enumerated type IRQn_Type in ch32x035.h that contains the values of all the interrupt numbers. The one we want right now is I2C1_EV_IRQn, which has a value of 30. I was able to find the value in the RM, but I much prefer to have a defined value referenced and not a “magic number”.

There is also a SDK function called NVIC_Init() that will let you either enable or disable an interrupt as well as set the Preemption priority and subpriority.

Note that the system-level global interrupts are enabled in the supplied startup_ch32x035.S file.

The SDK also defines labels for all the interrupts. The I2C interrupts are:

I2C1_EV_IRQHandler
I2C1_ER_IRQHandler

So at this point, I need to define a function for this interrupt handler. It also needs to specify that it is an interrupt handler, so it gets the proper signature and whatever else the compiler wants.

The first thing the interrupt handler needs to do is figure out why it was invoked. Going in order things we actually did, the first thing to look for would be the start bit SB in STAR1, bit 0 being set, indicating that a START condition was set.

I have seen the I2C event interrupt being triggered as expected. I added code to examine the status registers and respond accordingly. There are really only three condition of note.

1.  I2C_EVENT_MASTER_MODE_SELECT
2.  I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED
3.  I2C_EVENT_MASTER_BYTE_TRANSMITTED

The first happens after a START condition is set to indicate that the device has entered MASTER mode.

The second happens after the device address and direction bit have been successfully transmitted.

The third happens after each byte has been transmitted.

Additionally, and for no obvious reason, one more interrupt occurs after the STOP condition is set, even though the status registers all read zero. I choose to ignore this.

So I replaced the entire SH1106_init() function with a call to the new i2c_write() function, passing the SH1106 device address and both a pointer to and a length of an initialization 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_DISPLAY_ON, // command to turn on display
};

So now the display should be neither umop episdn upside down nor backwards, as well as on. And it works!

Now I need to dive a little deeper into the SH1106 data sheet and try to understand the ways to send data and commands to the controller chip. I’m still a little fuzzy on how the ‘continuation bit’ works as far as sending larger packages of data and commands to module is supposed to work.

The next communique I would like to send to the module is a ‘page fill’ command. This is composed of a ‘page address’ command, from 0-7, followed by 132 of your favorite numbers.

I added a ‘state’ variable to the I2C API, as it exists now, so that it doesn’t clobber itself. This is possible, as starting the process is quick and the function returns immediately, but the transfer takes a small but non-zero amount of time to complete.

I had a bright idea to break up the page fill routine into sending a ‘preamble’ with the page address command preformatted, then send the data as a separate function call. This doesn’t work, because each call to i2c_write() is a self-contained thing, with its own START and STOP conditions. This does not seem to sit well with the SH1106.

I reformatted the frame buffer to actually have some space between the data rows to fit in the OLED commands, and this seems to be working fine. Right now I’m just zeroing out the memory and it clears the screen. Ultimately, I would like to have a ‘splash’ screen that shows up for a second when the device is first powered on.

So the first of my goals (using interrupts) has been realized. I’m debating the value of pursuing the DMA option at this point. I think I will spend some time trying to get some reasonable looking dots onto the screen, such as text and maybe some geometric graphics.

Leave a Reply