Gaugette

Building gauges and gadgets with Arduinos, Raspberry Pis and Switec stepper motors.

Controlling an Adafruit SSD1306 SPI OLED With a Beaglebone Black

Permalink

What Are We Doing Here?

In an earlier post I described how to use the py-gaugette library to drive an Adafruit 128x32 monochrome OLED display from a Raspberry Pi, and a followup article added high-quality fonts.

I’ve now updated the library to run on the BeagleBone Black and to support Adafruit’s larger 128x64 display.

On the BBB py-gaugette uses Adafruit’s BBIO library for SPI and GPIO access.

Preparing the BeagleBone Black

I started with a fresh Angstrom boot image (the 2013-09-04 eMMC flasher image). After booting from the SD and flashing the eMMC (which takes about 30 minutes), I installed the Adafruit BBIO library following the instructions in their tutorial, which boil down to this:

/usr/bin/ntpdate -b -s -u pool.ntp.org
opkg update && opkg install python-pip python-setuptools python-smbus
pip install Adafruit_BBIO

Make sure you have flashed your eMMC and rebooted into the eMMC image before you run the above steps. If you are still running the eMMC flasher image when you run opkg to install the library, things get weird. Yep, I did that, not proud.

Once you have these packages installed, you might think to look for /dev/spidevX.Y to verify that the SPI drivers are installed. To my surprise they don’t show up until you actually run code that loads the SPI library. The Linux 3.8 kernel uses new and crafty device overlay trees to manage devices. The Adafruit library will automatically load the overlay that creates those devices as necessary, so only if you look at /dev after running the sample code will you see the spidev device files.

Confused? Let me illustrate.

After a reboot there are no spidev devices:

root@beaglebone:~# ls -l /dev/spidev*
ls: cannot access /dev/spidev*: No such file or directory

Run an application that instantiates Adafruit_BBIO.SPI:

python -c "from Adafruit_BBIO.SPI import SPI; SPI(0,0)"

Look again, there they are, it’s magic:

root@beaglebone:~# ls -l /dev/spidev*
crw------- 1 root root 153, 1 Jan 28 02:25 /dev/spidev1.0
crw------- 1 root root 153, 0 Jan 28 02:25 /dev/spidev1.1

Wire It Up

The BBB has two SPI interfaces. I’m using SPI0. If you want to use SPI1 you will need to follow these instructions to disable HDMI first.

BeagleBone BlackSignalColourAdafruit OLED
P9_1GNDblackGnd
P9_3VccredVin
P9_13Data/CmdpurpleDC
P9_15ResetwhiteRst
P9_17Slave SelgreenCS
P9_18MOSIblueData
P9_22ClockyellowClk

Pins P9_17, P9_18 and P9_22 are fixed by the SPI0 interface. Pins P9_13 and P9_15 are arbitrarily chosen GPIO pins, feel free to use any available pins and pass the appropriate pin name to the constructor.

Porting the Code

Porting gaugette.SSD1306 was straight forward. You can see what was required in this commit.

The only catch getting this running on the BBB was a small bug in SPI_writebytes in the Adafruit_BBIO.SPI module. To refresh the entire 128x64 display we need to transfer 1024 bytes of data. Conveniently Adafruit’s writebytes routine has a maximum transfer size of 1024 bytes. Unfortunately the C code uses an 8-bit counter to store the length, which effectively reduces the maximum transfer size to 255 bytes. For now I’ve worked around that by chunking transfers into 255-byte blocks, and I have filed a bugfix with Adafruit so hopefully we can remove that hack soon.

Update: Adafruit accepted the fix but I will wait until they update the python package currently at 0.0.19 before I remove the workaround.

I can see that maintaining separate branches of py-gaugette for the RPi and BBB is going to be burden, so I’ve now refactored the library to isolate all of the SPI and GPIO dependencies into abstraction classes that will automatically detect the current platform and delegate to the appropriate library. The new classes are gaugette.spi.SPI and gaugette.gpio.GPIO. This change makes all of the gaugette device classes run cross-platform except rgbled, which requires PWM support. I’ll write an abstraction layer for PWM later.

Testing the Display

There are a couple of sample applications in py-gaugette that will let you see your display in action. Note the first line shown below works around an issue with git over https on the BBB, and is only required if you are checking out directly on your BBB.

Install py-gaugette:

git config --global http.sslVerify false # workaround for https issue
git clone https://github.com/guyc/py-gaugette.git
ln -s ../gaugette py-gaugette/samples # link library for samples

This test shows the current date and time using a low-resolution font.

python py-gaugette/samples/ssd1306_test.py

This test cycles through some of the high-resolution fonts.

python py-gaugette/samples/font_test.py

The python code for creating an instance for a 128x64 display now looks like this:

# This example is for the 128x64 pixel display on the BeagleBone Black
RESET_PIN = "P9_15"
DC_PIN = "P9_13"
led = gaugette.ssd1306.SSD1306(reset_pin=RESET_PIN, dc_pin=DC_PIN, rows=64)

Notice that pins are identified as strings like "P9_15" on the BBB, which is great for clarity.

The video shows the output of the font test script. This particular sample code was written for a 32-row display where the bottom half of the display memory is off-screen, and so it looks a bit odd on a 64-bit display, but it does illustrate the high-resolution fonts.

python py-gaugette/samples/font_test.py

Improved Test Code for Rotary Encoders

Permalink

While working through a few queries about the rotary encoder library, it became evident that it would help to have a better test application to diagnose wiring issues.

I’ve committed a better version of rotary_test.py that writes out a comprehensive table of state information, updated whenever anything changes.


The output looks like this:

A B STATE SEQ DELTA SWITCH
1 1 3 2 1 0
0 1 2 3 1 0
0 0 0 0 1 0
1 0 1 1 1 0
1 1 3 2 1 0
0 1 2 3 1 0

The values in the table are defined as follows:

ColumnMeaning
ARaw value of input A
BRaw value of input B
STATEQuadrature state 0,1,3,2
SEQOrdinal sequence 0,1,2,3
DELTANet change in position
SWITCHPush-button switch state

One small note on the implementation: although there are library calls to directly fetch each of the values in the table, the test program only calls the library to retrieve STATE and SWITCH, and then derives the other values from STATE. I did this to make sure each column is generated from the same inputs. If instead I made separate library calls to fetch each column value, it is quite likely that the inputs would change while generating a row of this table, producing inconsistent and confusing results.

Updating  py-gaugette to  wiringpi2

Permalink

Phillip Howard and Gordon Henderson recently announced the availability of WiringPi2 along with a new python wrapper WiringPi2-Python.

The python library name has changed from wiringpi to wiringpi2, so any application code referencing the module needs to be updated. I’ve forked py-gaugette to create a wiringpi2 branch. I plan to roll this into the master branch in the not-too-distant future. When I do, wiringpi2 will be a requirement for using py-gaugette.

Here’s how I installed the wiringpi2 libraries on my boxes:

Install WiringPi2

At the time of writing, it does not work to install with sudo pip install wiringpi2; the library builds and installs fine, but will fail on the next step because the installed version is incompatible with current version of WiringPi2-Python. So instead, build from the github source:

git clone git://git.drogon.net/wiringPi
cd wiringPi
./build
cd ..

Install WiringPi2-Python

git clone https://github.com/Gadgetoid/WiringPi2-Python.git
cd WiringPi2-Python/
sudo python setup.py install
cd ..

Checkout py-gaugette for WiringPi2

git clone git://github.com/guyc/py-gaugette.git
cd py-gaugette
git checkout wiringpi2
sudo python setup.py install # skip this step if you do not want the library installed
cd ..

OpenXC - Hack Your Car

Permalink

OpenXC is a very cool initiative involving Ford Motor Company and Bug Labs aimed at creating an open development ecosystem for interfacing with in-car electronics. Looks like a pretty great project.

OpenXC Switec X25-based “Retro Gauge”

One of the OpenXC projects featured on their website is a retro gauge based around Switec X25 type micro-steppers.

They have published Eagle schematics for a gorgeous little combination analog/digital instrument gauge. Their in-gauge PCB cleverly incorporates an Arduino Pro Mini. They have also published STL files for a 3D-printable housing.
This all looks very slick to me.

Their firmware uses our very own Switec X25 library.

You can find detailed documentation and resources at their github repository.

Raspberry Pi Time Clock

Permalink

Gaugette Time Clock

This build combines a Raspberry Pi with a rotary-encoder, an RGB LED and an OLED character display to create a time clock that logs my time on tasks directly to a Google Docs spreadsheet.

Motivation

Whenever I have to record time against projects, I find it really hard to diligently keep my time records up to date. Maybe with a purpose-built time clock I will keep better records? Hey, it could happen!

Overview

The off-the-shelf components:

I used a common cathode LED from Sparkfun. You could use Adafruit’s common anode equivalent with minor code changes.

The theory of operation is pretty simple:

  • at start-up, it pulls a list of jobs from a Google Docs spreadsheet,
  • rotating the knob scrolls through the list of jobs,
  • clicking the knob logs a start time or end time in the spreadsheet.

The Case

The top of the case is made from a single block of wood.

The bottom of the block has been hollowed out to house the RPi board. The RPi sits in a plastic carriage that screws to the bottom of the block. I have not provided access to the HDMI, audio or video ports on the RPi since I’m not going to use them.

The carriage for the Raspberry Pi was designed in OpenSCAD and printed on a Makerbot Replicator. The RPi board doesn’t have mounting holes, so the carriage has edge-clips to grasp the board. The posts at the corners were originally intended to screw the carriage to the block, but I found that to be impractical, so I added the two tabs at the bottom and routed out matching recesses in the block. I left the posts there because they make the carriage look a little more interesting. Or because I was too lazy to remove them, I’ve heard it both ways.

The OpenScad sources and STL files for the base are available from Thingiverse.

The Circuit

I’m using two of the ground pins originally documented as Do Not Connect (DNC). The extra grounds are really convenient, and Eben has publicly committed to keeping these available in future board revisions.

It’s worth pointing out that I deliberately selected GPIO pin 5 for the push button because of the Raspberry Pi Safe Mode feature. If you have a recent firmware release, you can boot your RPi in safe mode by holding the knob down (shorting pin 5 to ground) when you power up. In truth my intention here is not so much to make safe mode available (I’ve never needed it) but to make sure that pin 5 is not unintentionally shorted to ground at boot time, as it could be if you used it for one of the quadrature encoder inputs. Yep, that happened. After I upgraded to the version of firmware that supports safe mode my box stopped booting. Lesson learned; be careful with pin 5, or disable safe mode by adding avoid_safe_mode=1 to /boot/config.txt.

For most of the GPIO connections I cut pre-terminated Female Female jumper wires in half and soldered the cut end to the component. I already had header pins on the SSD1306 OLED module, so I used 3” premade cables from Little Bird Electronics.
It is crucial to keep wiring short and well insulated so that it will all pack in neatly and without shorts when the case is closed up.

RGB LED Indicator

For this device I wanted a large, diffuse and interesting state indicator built around an RGB LED. I use the WiringPi soft PWM library to drive the LED, and the py-gaugette RgbLed class makes it easy to do animated colour transition loops. My first prototype was made from the back of a GU10 light bulb. The bulb glass is thick and diffuses the light nicely, and I thought the terminals would make cool capacitive switches.

I liked that a lot, but ultimately settled on a long oven light which had a more compact footprint. It gives elegant curving internal reflections which look quite nice.

When I cut the metal tip of the bulb off with a Dremel I expected to be able to remove the burned-out filament, but discovered this bulb (like most others I would guess) is made with an inner glass plug that encases the filament wires and seals the bulb. It isn’t easily removed, but has a hollow neck just big enough to receive an LED. So the filament stays. Maybe I should have used a new bulb!

I trimmed the red, green and blue leads on the LED quite short and soldered a 270Ω current-limiting resistor to each one. This keeps the resistors tucked up away from the board. I then added connector wires to each lead, pushed the LED up into the bulb neck and pumped some hot glue in to keep it all in place.

I’ve seen advice suggesting I should have selected different values for the resistors to to get optimal white balance. I didn’t bother. Colour balance is fine. I aint bovvered.

Rotary Encoder

I documented the encoder library in a previous post. I’m using the py-gaugette RotaryEncoder.Worker class to poll the encoder GPIOs in the background which keeps the application code very simple.

128x32 SSD1306 OLED Display

I’ve written about using these great little 128x32 OLEDs from Adafruit before. Mounting it in the block was a challenge. I used a router on the inside of the case to thin the material down to just a few mm over an area big enough to place the PCB. I then cut a rectangular hole for the display using a Dremel, file and scalpel, taking great pains not to crack the thin wood. I couldn’t see a practical way to use the mounting holes so I hot-glued the board in place. I positioned it with the display powered up showing a test pattern so I could line up the active part of the display with the hole.

The bezel was printed on a Makerbot Replicator and painted with Plaid brand copper Liquid Leaf. I’ve been looking for a metallic paint that wouldn’t dissolve the ABS. Liquid Leaf is xylene-based, which should be safe for ABS, although maybe not so safe for humans.

The effect of metallic paint on the printed surface is interesting; it highlights the individual filaments in the print. I like it. It would be possible to reduce the relief by brushing the surface with acetone before painting, but I think the sharp relief is good for what I’m doing here.

Cooling

It occurred to me fairly late in this build that I hadn’t provided for any air flow to help cool the processor. That got me to wondering, then to worrying… am I cooking my Pi? And is there no end to the bad pie puns?

Fortunately a recent firmware update provided a tool that allows us to measure the CPU temperature in code, so I did a little experiment. I recorded the temperature using vcgencmd.

while :; do /opt/vc/bin/vcgencmd measure_temp; sleep 3; done

I ran this loop from a cold boot for 25 minutes, first with the top off, then again (after letting the system cool down) with the case closed up tight. For the record the ambient temperature in my office was around 26°C.

The results show that the closed box adds about 4°C to the CPU temperature at idle. I tried removing the bulb from the centre of the cover to allow hot air to be convected away, but that made no measurable difference in temperature.

These temperatures were taken at idle. My application code runs around 20% CPU utilization. With application running and the lid off the temperature settles in around 46°C, and with the lid on at around 51°C.

Based on these results I’m happy to ignore air flow for now. 51°C isn’t worryingly high, and it looks like it would take a lot of work to improve air flow enough to make an appreciable difference.

Software

The code for all of the I/O devices used here is available in the py-gaugette library. I will release the application source soon; there are a few loose ends I want to tidy up first.

All Together Now

In the video below, the LED colours are as follows:

  • purple is idle - time not being logged
  • slowly pulsing blue is active - time is being logged against a task, pulses slowly
  • flashing is busy - updating the spreadsheet via Google Docs

The resulting spreadsheet looks like this:

Analog Gauge Stepper Breakout Board Available on Tindie

Permalink

Check out this great new Tindie project launched by The Renaissance Engineer Adam Fabio for a new breakout board designed for Switec motors and clones.

The breakout board incorporates flyback diodes to protect your circuit from inductive kickback, and the also serves as a convenient base to mount the motors. The kit includes the board, diodes, 6-pin header, a Switec X27.168 and 3d-printed needle.

If you want to get in on this initial offering act quickly - it closes in 12 days. The project has already exceeded the funding target. Nice work Adam!


Rotary Encoder Library for the Raspberry Pi

Permalink

Here’s a quick overview of the rotary encoder I/O class in the py-gaugette library.

The encoder I’m using is a 2-bit quadrature-encoded rotary encoder, available from Adafruit.
The datasheet is here.

The documentation for this encoder says that it gives 24 pulses per 360° rotation, which I interpreted to mean 24 resolvable positions, but after trying it I see that it has 24 detent positions, and between each detent is a full 4-step quadrature cycle, so there are actually 96 resolvable steps per rotation. This unit also includes a momentary switch which is closed when the button is pushed down. Takes a solid push to close the switch.

The diagram above shows how I’ve connected the rotary encoder to the Raspberry Pi. I’m using pin 9, one of the “Do Not Connect” (DNC) pins to get a second ground pin for convenience. Eben made a public commitment to keep the extra power and ground pins unchanged in future releases, so I think it is safe to publish circuits using them. Right? Here’s a pinout highlighting the functions of the DNC pins, along with the wiringpi pin numbers.

Decoder Logic

The implementation configures pins A and B as inputs, turns on the internal pull-up resistors for each, so they will read high when the contacts are open, low when closed. The inputs generate the following sequence of values as we advance through the quadrature sequence:

SEQBAA ^ B
0000
1011
2110
3101

One non-obvious detail here: the library uses the bitwise xor value A ^ B to efficiently transform the input bits into an ordinal sequence number. There is no reason behind the xor other than it gives us the bit sequence we want.

seq = (a_state ^ b_state) | b_state << 1

Because we are pulling our inputs high and shorting to ground when the contacts close, our inputs for A and B are actually inverted against those shown in Quadrature Output Table figure above. It turns out you can ignore this inversion in the decode logic; inverting the signals moves the quadrature half a cycle forward, but makes no difference at all to the decode logic.

Once the library has computed the sequence value, it determines the direction of movement by comparing the current sequence position against the previous sequence position, like this:

delta = (current_sequence - previous_sequence) % 4

which will yield values in the range 0 through 3, interpreted as follows:

deltameaning
0no change
11 step clockwise
22 steps clockwise or counter-clockwise
31 step counter clockwise

If we get a value of 2, we know we have missed a transition (oops!) but we don’t know if the encoder has turned clockwise or counter-clockwise. In this case the library assumes the direction is the same as the previous rotation. Of course if the encoder has actually moved 3 steps between reads, it will decode as 1 step in the wrong direction. You can only fix this by polling faster or using interrupts.

Polling Vs Interrupts

The current implementation of the RotaryEncoder class uses polling to monitor the inputs rather than using GPIO interrupts. It looks like the plumbing is in place within wiringpi to use interrupts on GPIOs, but I’ll leave that for another day. Instead I have included an optional worker thread class that can be used to monitor the inputs, leaving the main thread free to go about its business.

The push switch is handled by a separate Switch class that doesn’t do anything clever like debounce - it just reads and returns the current switch state.

Here’s how to read the rotary encoder and switch without starting a worker thread.

import gaugette.rotary_encoder
import gaugette.switch
A_PIN = 7
B_PIN = 9
SW_PIN = 8
encoder = gaugette.rotary_encoder.RotaryEncoder(A_PIN, B_PIN)
switch = gaugette.switch.Switch(SW_PIN)
last_state = None
while True:
delta = encoder.get_delta()
if delta!=0:
print "rotate %d" % delta
sw_state = switch.get_state()
if sw_state != last_state:
print "switch %d" % sw_state
last_state = sw_state

Spin the knob and you should see something like this:

$ sudo python rotary_worker_test.py
switch 0
rotate 1
rotate 1
rotate -1
...

Using the class as shown above, you must call encoder.get_delta() frequently enough to catch all transitions. A single missed step is handled okay, but 2 missed steps will be misinterpreted as a single step in the wrong direction, so if you turn the knob too quickly you might see some jitter.

To ensure the inputs are polled quickly enough, even if your application’s main thread is busy doing heavy lifting, you can use the worker thread class class to monitor the switch positions. Using the worker class is trivial; instantiate RotaryEncoder.Worker instead of RotaryEncoder with the same parameters, and call the start() method to begin polling - only lines 8 and 9 below have changed.

import gaugette.rotary_encoder
import gaugette.switch
A_PIN = 7
B_PIN = 9
SW_PIN = 8
encoder = gaugette.rotary_encoder.RotaryEncoder.Worker(A_PIN, B_PIN)
encoder.start()
switch = gaugette.switch.Switch(SW_PIN)
last_state = None
while 1:
delta = encoder.get_delta()
if delta!=0:
print "rotate %d" % delta
sw_state = switch.get_state()
if sw_state != last_state:
print "switch %d" % sw_state
last_state = sw_state

Here’s a video showing the rotary encoder at work. In this case I use the main thread to service the OLED scrolling, with the worker thread keeping an eye on the rotary encoder. There is another worker thread that manages the RGB led transitions.

Better Fonts for the SSD1306

Permalink

The first release of the SSD1306 support library py-gaugette used the 5x7 pixel fonts from the Adafruit GFX library. That’s a fine and compact font, but wouldn’t it be nice to have some pretty high-res fonts to take advantage of the memory and resolution we have to work with?

Generating Font Bitmaps

I started with The Dot Factory by Eran Duchan. Its a handy C# (Windows) tool for generating C and C++ bitmap files quickly, and source code is available. I modified it to generate Python code, and to add a kerning table to store the minimum number of pixels the cursor must move between any two characters to avoid collison.

The kerning isn’t completely right yet - I noticed that the underscore character can slip beheath other characters. I’ll need to look at that some more in due time - and I’d also like to replace the C# app for a command-line tool to generate the rasterized image files.

Each font is stored in a module like the following. The size suffix (16 in this case) indicates the character height in pixels. The descriptor table specifies the width of each character and the character’s offset in the bitmap.

# Module gaugette.fonts.arial_16
# generated from Arial 12pt
name = "Arial 16"
start_char = '#'
end_char = '~'
char_height = 16
space_width = 8
gap_width = 2
bitmaps = (
# @0 '!' (1 pixels wide)
0x00, #
0x80, # O
0x80, # O
0x80, # O
0x80, # O
0x80, # O
0x80, # O
0x80, # O
0x80, # O
0x80, # O
0x80, # O
0x00, #
0x80, # O
0x00, #
0x00, #
0x00, #
# @16 '"' (4 pixels wide)
0x00, #
0x90, # O O
0x90, # O O
0x90, # O O
0x90, # O O
0x00, #
0x00, #
0x00, #
0x00, #
0x00, #
0x00, #
0x00, #
0x00, #
0x00, #
0x00, #
0x00, #
# @32 '#' (9 pixels wide)
0x00, 0x00, #
0x11, 0x00, # O O
0x11, 0x00, # O O
0x11, 0x00, # O O
0x22, 0x00, # O O
0xFF, 0x80, # OOOOOOOOO
0x22, 0x00, # O O
0x22, 0x00, # O O
0x22, 0x00, # O O
0xFF, 0x80, # OOOOOOOOO
0x44, 0x00, # O O
0x44, 0x00, # O O
0x44, 0x00, # O O
0x00, 0x00, #
0x00, 0x00, #
0x00, 0x00, #
...
)
# (width, byte offset)
descriptors = (
(1,0),# !
(4,16),# "
(9,32),# #
...
)
# kerning[c1][c2] yeilds minimum number of pixels to advance
# after drawing #c1 before drawing #c2 so that the characters
# do not collide.
kerning = (
(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,),
...
)

Note the font is a module, not a class, because it allows a very concise syntax:

from gaugette.fonts import arial_16
textSize = led.draw_text3(0,0,"Hello World",arial_16)

Supporting Horizontal Scrolling

In the process of testing these fonts, I realized I would like to be able to scroll horizontally, and the SSD1306 doesn’t have hardware support for that. Vertical scrolling is accomplished using the SET_START_LINE command, but the horizontal scrolling commands do not support scrolling through an image that is wider than the display. We need to do it in software.

It turns out that blitting memory from the Pi to the SSD1306 over SPI is pretty fast; fast enough to get a reasonable horizontal scroll effect by blitting complete frames from the Pi’s memory to the SSD1306. There’s just one thing - the default memory mode of the SSD1306 is row-major, and for horizontal scrolling we really want to send a vertical slice of the memory buffer over SPI. To avoid buffer manipulation I switched the Pi-side memory buffer to use column-major order, and use MEMORY_MODE_VERT on the SSD1306 when blitting.

To illustrate: the memory buffer is stored as a python list of bytes. Consider a virtual buffer that is 256 columns wide and 64 rows high. Using column-major layout we can address the 128 columns starting at column 100 using the python list addressing buffer[100*64/8:128*64/8].

buffer = [0] * 256 * 64 / 8 # buffer for 256 columns, 64 rows, 8 pixels stored per byte
start = 100 * 64 / 8 # byte offset to 100th column
length = 128 * 64 / 8 # byte count of 128 columns x 64 rows
led.command(led.SET_MEMORY_MODE, led.MEMORY_MODE_VERT) # use vertical addressing mode
led.data(buffer[start:start+length]) # send a vertical slice of the virtual buffer

Note that using column-major layout we cannot easily blit a horizontal slice of the virtual memory buffer into display ram, so we can’t use the same method for vertical scrolling. Stick with SET_START_LINE for vertical scrolling. The combination of these methods gives us fast horizontal and vertical scrolling.

An updated library with sample code is available on github.

Controlling an Adafruit SSD1306 SPI OLED With a Raspberry Pi

Permalink

Adafruit’s lovely little 128x32 monochrome SPI OLED module uses a SSD1306 driver chip (datasheet), and Adafruit have published excellent tutorials and libraries for driving this from an Arduino.

When asked in their forum about Raspberry Pi support, Adafruit have said that there is a huge backlog of libraries to port to the RasPi and (they) don’t have any ETA on the SSD1306.

I’m working on a project that was originally intended for an Arduino, but I’ve decided to switch to the Raspberry Pi, so I need to get this display working with the Pi. To that end, I’ve partially ported Adafruit’s SSD1306 library to Python for the Raspberry Pi. The port is partial in that:

  1. it only supports the 128x32 SPI module (unlike the original that supports the I²C and 128x64 modules) and
  2. it only supports pixel and text drawing functions (no geometric drawing functions).

Signal Levels

The SSD1306 operates at 3.3V, and the Adafruit module has built-in level-shifters for 5V operation. However I want to drive it at 3.3V from the Pi, and I wasn’t confident from the documentation that it would operate at 3.3V without modification. However the back-side silkscreen says very clearly 3.3 - 5V and I can confirm it works very happily with both Vin and signalling at 3.3V.

SPI Signals

In SPI nomenclature MISO is master in, slave out, MOSI is master out, slave in. The SSD1306 module is write-only using SPI, and so there is no MISO connection available, and MOSI is labelled DATA on the module. Of course SPI always reads while it writes, but it is fine to leave MISO disconnected. It will be pulled low, so if you ever looked at the data you would see all zeros.

The D/C (Data/Command) signal on the module is not part of the SPI specification, and it took a little experimenting to understand exactly what it is used for. The data sheet says “When it is pulled HIGH (i.e. connect to VDD), the data at D[7:0] is treated as data. When it is pulled LOW, the data at D[7:0] will be transferred to the command register.”

Initially I supposed data to include the argument bytes that follow the opcode when sending multi-byte commands. For example the “Set Contrast Control” command consists of a one-byte opcode (0x81) followed by a one-byte contrast value, so I was sending the first byte with D/C high, and pulling it low for the argument byte. Wrongo! That’s not what they mean by data; keep the D/C line high for all bytes in a command, and pull it low when blitting image data into the image memory buffer. Simple as that.

Platform

I’m running Python 2.7.3 on a Rasberry Pi Model B (v1) with the following software:

Wire Up

Here’s how I’ve wired it up. You can freely change the GPIOs for D/C and Reset.

Test Code

Note that pin numbers passed in the constructor are the wiring pin numbers, not the connector pin numbers! For example I have Reset wired to connector pin 8, which is BCP gpio 14, but wiringPi pin 15. It’s confusing, but just refer to the wiringPi GPIO table.

The python library for the SSD1306 has been rolled into the py-gaugette library available on github.

The test code below vertically scrolls vertically between two display buffers, one showing the current time, one showing the current date.

This sample code is included in the py-gaugette library.

import gaugette.ssd1306
import time
import sys
RESET_PIN = 15
DC_PIN = 16
led = gaugette.ssd1306.SSD1306(reset_pin=RESET_PIN, dc_pin=DC_PIN )
led.begin()
led.clear_display()
offset = 0 # buffer row currently displayed at the top of the display
while True:
# Write the time and date onto the display on every other cycle
if offset == 0:
text = time.strftime("%A")
led.draw_text2(0,0,text,2)
text = time.strftime("%e %b %Y")
led.draw_text2(0,16,text,2)
text = time.strftime("%X")
led.draw_text2(8,32+4,text,3)
led.display()
time.sleep(0.2)
else:
time.sleep(0.5)
# vertically scroll to switch between buffers
for i in range(0,32):
offset = (offset + 1) % 64
led.command(led.SET_START_LINE | offset)
time.sleep(0.01)

About Fonts

This test code uses the 5x7 bitmap font from the Adafruit GFX library scaled to x2 and x3. It works, but Steve Jobs would not approve! It isn’t taking advantage of the very high resolution of these lovely little displays. Larger fonts with kerning would be a great addition.