« Posts list

Announcing the SH1106 OLED display driver

Similar to the SSD1306 covered previously, the SH1106 OLED display is a small, self contained module perfect for hacking into a project or prototype. It's available on AliExpress and eBay for peanuts, but is lacking a Rust driver. Until now with the sh1106 crate!

The SH1106 crate is compatible with the embedded-hal traits, so should be usable on anything from an ARM micro to a Raspberry Pi. Please note that it currently doesn't use the internal memory of the IC as the display buffer, so could be memory-optimised quite a lot. PRs welcome!

The driver currently only supports I2C as I haven't seen many SPI SH1106 modules in the wild, so let's take a look at the code to hook up the display over I2C. I'll draw the Rust logo as seen in the header image in the following example.

I've left some of the boilerplate out of this code, but you can find the complete example here.

use embedded_graphics::image::Image1BPP;
use embedded_graphics::prelude::*;
use sh1106::prelude::*;
use sh1106::Builder;

let dp = stm32::Peripherals::take().unwrap();
let mut flash = dp.FLASH.constrain();
let mut rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze(&mut flash.acr);
let mut afio = dp.AFIO.constrain(&mut rcc.apb2);
let mut gpiob = dp.GPIOB.split(&mut rcc.apb2);
let scl = gpiob.pb8.into_alternate_open_drain(&mut gpiob.crh);
let sda = gpiob.pb9.into_alternate_open_drain(&mut gpiob.crh);

let i2c = BlockingI2c::i2c1(
    dp.I2C1,
    (scl, sda),
    &mut afio.mapr,
    Mode::Fast {
        frequency: 400_000,
        duty_cycle: DutyCycle::Ratio2to1,
    },
    clocks,
    &mut rcc.apb1,
    1000,
    10,
    1000,
    1000,
);

let mut disp: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();

disp.init().unwrap();
disp.flush().unwrap();

let im = Image1BPP::new(include_bytes!("./rust.raw"), 64, 64).translate(Coord::new(32, 0));

disp.draw(im.into_iter());

disp.flush().unwrap();

Complete, runnable example

Simple as that! The image is included with include_bytes!() and is naturally monochrome to suit the display. It should be compressed to 1 bit per pixel, which can be done with the following Imagemagick command:

convert rust.png -depth 1 gray:rust.raw

Now let's break down the example above. First, we need a display in GraphicsMode, so we can draw primitives and images.

let mut disp: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();

This uses the Builder pattern to connect to the display over I2C. Take a look at the Builder docs for more options like setting display size and rotation. Lastly, we call .into() to convert the display from RawMode into GraphicsMode. If you want to just draw individual pixels, skip this step.

Next, we need to initialise the display with init() and flush() (to clear it).

disp.init().unwrap();
disp.flush().unwrap();

And now, draw the image:

let im = Image1BPP::new(include_bytes!("./rust.raw"), 64, 64).translate(Coord::new(32, 0));

disp.draw(im.into_iter());

disp.flush().unwrap();

The image is drawn to the display buffer by using im.into_iter(). embedded-graphics tries hard to use as little memory as possible internally, and iterators really help with that! Currently, the SH1106 crate uses a framebuffer, but the IC contains its own memory that can be read and written. This means that, in the future, a microcontroller could consume almost no memory when driving this display. Awesome. Once the image is in the buffer, a call to disp.flush() sends the buffer to the screen.

Please help test this crate out! There are a few examples that should Just Work on an STM32 Blue Pill, but it would be great to test this crate on other devices as well. The SH1106 contains a readable framebuffer which the crate currently doesn't use. This wastes a lot of memory on the microcontroller, so using the builtin framebuffer is a priority for future work.