Skip to content

Commit 4ab66a2

Browse files
author
Martin Crossley
committed
finalise example
1 parent e54125d commit 4ab66a2

File tree

8 files changed

+136
-63
lines changed

8 files changed

+136
-63
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ App|Description
368368
[spi_dma](spi/spi_dma) | Use DMA to transfer data both to and from the SPI simultaneously. The SPI is configured for loopback.
369369
[spi_flash](spi/spi_flash) | Erase, program and read a serial flash device attached to one of the SPI controllers.
370370
[spi_master_slave](spi/spi_master_slave) | Demonstrate SPI communication as master and slave.
371+
[ssd1309_stdout_spi](spi/ssd1309_stdout_spi/) | Display text from stdout and simple graphics on an SSD1309-based OLED panel via SPI.
371372
[max7219_8x7seg_spi](spi/max7219_8x7seg_spi) | Attaching a Max7219 driving an 8 digit 7 segment display via SPI.
372373
[max7219_32x8_spi](spi/max7219_32x8_spi) | Attaching a Max7219 driving an 32x8 LED display via SPI.
373374

spi/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ if (TARGET hardware_spi)
44
add_subdirectory_exclude_platforms(spi_dma)
55
add_subdirectory_exclude_platforms(spi_master_slave)
66
add_subdirectory_exclude_platforms(spi_flash)
7-
add_subdirectory_exclude_platforms(ssd1309_spi_dma)
7+
add_subdirectory_exclude_platforms(ssd1309_stdout_spi)
88
add_subdirectory_exclude_platforms(max7219_32x8_spi)
99
add_subdirectory_exclude_platforms(max7219_8x7seg_spi)
1010
else()
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
add_executable(ssd1309_spi_dma
2-
ssd1309_spi_dma.c
1+
add_executable(ssd1309_stdout_spi
2+
ssd1309_stdout_spi.c
33
)
44

55
# pull in common dependencies and additional hardware support
6-
target_link_libraries(ssd1309_spi_dma
6+
target_link_libraries(ssd1309_stdout_spi
77
pico_stdlib
88
hardware_spi
99
hardware_dma
1010
)
1111

1212
# create map/bin/hex file etc.
13-
pico_add_extra_outputs(ssd1309_spi_dma)
13+
pico_add_extra_outputs(ssd1309_stdout_spi)
1414

1515
# add url via pico_set_program_url
16-
example_auto_set_url(ssd1309_spi_dma)
16+
example_auto_set_url(ssd1309_stdout_spi)

spi/ssd1309_stdout_spi/README.adoc

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
= Simple graphics and stdout text on an OLED display via SPI
2+
3+
Ths example displays text and graphics on one of the widely-available small OLED panels based on the *SSD1309* controller. It should also work with compatible devices such as the *SSD1306* (not tested).
4+
5+
These modules typically support either I2C or SPI. For this example you will need one configured for SPI.
6+
7+
The code renders content onto a frame buffer that is transferred to the SPI port in the background, by a DMA channel under the control of a frame timer. A simple driver is used to copy _stdout_ to the frame buffer and some basic graphics functions are provided.
8+
9+
The interface uses one of the Pico's onboard SPI peripherals and two extra GPIO pins as shown below. Note that in SPI mode the SSD1309 is a receive-only device.
10+
11+
For details of the commands supported by the SSD1309 controller and its addressing modes see the manufacturer's datasheet: https://www.hpinfotech.ro/SSD1309.pdf.
12+
13+
14+
== Wiring information
15+
16+
Wiring up the device requires seven jumpers as follows:
17+
18+
* Display CS (chip select) -> Pico GP17 (SPI0 CSn), pin 22
19+
* Display DC (data/command) -> Pico GP20, pin 26
20+
* Display RES (reset) -> Pico GP21, pin 27
21+
* Display SDA (MOSI) -> Pico GP19 (SPI0 TX), pin 25
22+
* Display SCLK (SPI clock) -> Pico GP18 (SPI0 SCK), pin 24
23+
* Display VDD (3.3v) -> Pico 3V3_OUT, pin 36
24+
* Display VSS (0v, gnd) -> Pico GND, pin 23
25+
26+
The example uses SPI device 0 and powers the display from the Pico 3.3v output. If you power the display from an external supply then ensure that the Pico's logic pins are not exposed to any voltage higher than 3.3v.
27+
28+
[NOTE]
29+
======
30+
The pins on your board may be labelled slightly differently to the list above. Check the documentation for your display.
31+
32+
You can change the code to use different pins if you like, but the ones for CSn, TX and SCK must match your SPI device: see the GPIO Function Select Table in the datasheet.
33+
======
34+
35+
36+
[[wiring_diagram.png]]
37+
[pdfwidth=75%]
38+
.Wiring Diagram for SSD1309 with SPI interface
39+
image::wiring_diagram.png[]
40+
41+
== List of Files
42+
43+
CMakeLists.txt:: A file to configure the CMake build system for the example.
44+
ssd1309_stdout_spi.c:: The example code.
45+
font.h:: A basic 8x8 bit font table used by the example.
46+
47+
48+
== Bill of Materials
49+
50+
.A list of materials required for the example
51+
[[SSD1309-bom-table]]
52+
[cols=3]
53+
|===
54+
| *Item* | *Quantity* | Details
55+
| Breadboard | 1 | generic part
56+
| Raspberry Pi Pico or Pico 2 | 1 | https://www.raspberrypi.com/products/raspberry-pi-pico/
57+
| SSD1309-based OLED display panel with SPI interface| 1 | generic part
58+
| M/F Jumper wires (assorted colours) | 7 | generic part
59+
|===
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
* Copyright (c) 2026 mjcross
33
*
44
* SPDX-License-Identifier: BSD-3-Clause
5-
*/
5+
**/
66

77
// Vertical bitmaps for commonly-used characters.
88

9-
// Each bitmap is 8 pixels wide and 8 pixels high, with the least
10-
// significant bit at the top. The patterns are based on bitmaps
11-
// originally credited to IBM.
9+
// Each bitmap is 8 pixels wide and 8 pixels high, with the least significant bit
10+
// at the top. The bitmaps were translated from one of the original IBM BIOS fonts
11+
// released over 45yrs ago (see https://int10h.org/oldschool-pc-fonts/)
1212

1313
#include <stdint.h>
1414

spi/ssd1309_spi_dma/ssd1309_spi_dma.c renamed to spi/ssd1309_stdout_spi/ssd1309_stdout_spi.c

Lines changed: 66 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
11
/**
2-
* Copyright (c) 2025 mjcross
2+
* Copyright (c) 2026 mjcross
33
*
44
* SPDX-License-Identifier: BSD-3-Clause
55
**/
66

7-
// An example showing how to drive a ssd1309-based OLED panel from a frame buffer using
8-
// DMA transfers over SPI. It should also work on a ssd1306 device (not tested).
7+
// Copy stdout and/or simple graphics to an OLED display panel based on the ssd1309
8+
// (or compatible) controller, using a frame buffer transferred by DMA over an SPI interface.
99
//
10-
// To understand the commands and addressing modes for the display refer to
11-
// the manufacturer's datasheet at https://www.hpinfotech.ro/SSD1309.pdf
10+
// For details of the display controller and its commands and addressing modes refer to the
11+
// manufacturer's datasheet at https://www.hpinfotech.ro/SSD1309.pdf
1212

1313
#include <stdio.h>
14+
#include <string.h>
15+
#include <stdlib.h>
1416
#include "pico/stdio/driver.h"
1517
#include "pico/stdlib.h"
1618
#include "hardware/dma.h"
1719
#include "hardware/spi.h"
18-
#include "string.h"
19-
2020
#include "font.h"
2121

22-
// dimensions of our OLED display panel
22+
// dimensions of the display panel
2323
#define NUM_X_PIXELS 128
2424
#define NUM_Y_PIXELS 64
2525
#define PIXELS_PER_BYTE 8
2626
#define TABSTOPS 4
2727

2828
// how often we want to refresh the display from the frame buffer
29-
#define FRAME_PERIOD_MS 20
29+
#define FRAME_PERIOD_MS 20 // 50 Hz
3030

31-
// ssd1309 accepts a maximum SPI clock rate of 10 Mbit/sec
31+
// clock rate for the SPI interface
32+
// the ssd1309 is specified up to 10 Mbit/sec
3233
#define DISPLAY_SPI_BITRATE 10 * 1000 * 1000
3334

34-
// define the pins for the SPI interface
35+
// pins to use for the SPI interface
3536
// we will use the spi0 peripheral and the following GPIO pins (see the
3637
// GPIO function select table in the Pico datasheet).
3738
#define SPI_DEVICE spi0
@@ -41,26 +42,26 @@
4142
#define PIN_DC 20 // data/command mode (low for command)
4243
#define PIN_R 21 // reset (active low)
4344

44-
// modes for the ssd1309 data/command pin (see ssd1309 datasheet)
45+
// modes for the ssd1309 data/command pin (see the datasheet)
4546
#define DC_COMMAND_MODE 0
4647
#define DC_DATA_MODE 1
4748

4849
// global variables
49-
uint8_t frame_buffer[NUM_X_PIXELS * NUM_Y_PIXELS / PIXELS_PER_BYTE];
50+
uint8_t frame_buffer[ NUM_X_PIXELS * NUM_Y_PIXELS / PIXELS_PER_BYTE ];
5051
int dma_ch_transfer_fb;
5152
volatile bool display_needs_refresh = false;
52-
volatile uint fb_out_index = 0;
53+
volatile uint fb_cursor_index = 0;
5354

5455

55-
// configure a DMA channel to transmit the frame buffer over SPI
56+
// configure a dma channel to send the frame buffer over SPI
5657
void dma_init() {
5758
dma_ch_transfer_fb = dma_claim_unused_channel(true);
5859
dma_channel_config_t c = dma_channel_get_default_config(dma_ch_transfer_fb);
5960
channel_config_set_transfer_data_size(&c, DMA_SIZE_8);
6061
channel_config_set_dreq(&c, spi_get_dreq(SPI_DEVICE, true));
6162
dma_channel_configure(
6263
dma_ch_transfer_fb,
63-
&c,
64+
&c, // the channel_config above
6465
&spi_get_hw(SPI_DEVICE)->dr, // write address (doesn't increment)
6566
frame_buffer, // initial read address
6667
dma_encode_transfer_count(count_of(frame_buffer)),
@@ -70,7 +71,7 @@ void dma_init() {
7071

7172
// initialise the SPI interface
7273
void interface_init() {
73-
// configure the SPI controller for 8-bit transfers using Motorola SPI mode 0
74+
// configure the SPI controller for 8-bit transfers and Motorola SPI mode 0
7475
spi_init(SPI_DEVICE, DISPLAY_SPI_BITRATE);
7576
spi_set_format(SPI_DEVICE, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
7677

@@ -98,108 +99,121 @@ void display_reset() {
9899
spi_write_blocking(SPI_DEVICE, cmd_list, sizeof(cmd_list));
99100
gpio_put(PIN_DC, DC_DATA_MODE);
100101

101-
// clear the frame buffer and set it to refresh
102+
// clear the frame buffer and flag it to be transferred
102103
memset(frame_buffer, 0x00, sizeof(frame_buffer));
103-
fb_out_index = 0;
104+
fb_cursor_index = 0;
104105
display_needs_refresh = true;
105106
}
106107

107-
// this will be called every frame period
108-
bool frame_refresh_callback(__unused struct repeating_timer *t) {
109-
if (display_needs_refresh) {
110-
// reset read address and start transfer
111-
dma_channel_set_read_addr(dma_ch_transfer_fb, frame_buffer, true);
112-
display_needs_refresh = false;
113-
}
114-
return true; // repeat timer
115-
}
116108

117-
// simple stdout driver for the display
109+
// a simple stdout driver for the display
118110
void fb_out_chars(const char *buf, int len) {
111+
uint font_index;
119112
while (len) {
120113
char code = *buf;
121-
uint font_index;
122114
buf += 1;
123115
len -= 1;
124-
// scroll display if needed (using DMA would almost certainly be OTT)
125-
while (fb_out_index >= sizeof(frame_buffer)) {
116+
while (fb_cursor_index >= sizeof(frame_buffer)) {
117+
// scroll frame buffer (on RP2350 you could use a decementing dma transfer but it's probably overkill)
126118
memmove(frame_buffer, frame_buffer + NUM_X_PIXELS, sizeof(frame_buffer) - NUM_X_PIXELS);
127-
fb_out_index -= NUM_X_PIXELS;
119+
fb_cursor_index -= NUM_X_PIXELS;
128120
memset(&frame_buffer[sizeof(frame_buffer) - NUM_X_PIXELS - 1], 0x00, NUM_X_PIXELS);
129121
}
130122
// handle control codes
131123
if (code == '\n') {
132-
fb_out_index = (fb_out_index / NUM_X_PIXELS + 1) * NUM_X_PIXELS;
124+
fb_cursor_index = (fb_cursor_index / NUM_X_PIXELS + 1) * NUM_X_PIXELS;
133125
} else if (code == '\t') {
134-
fb_out_index = (fb_out_index / (TABSTOPS * FONT_BYTES_PER_CODE) + 1) * TABSTOPS * FONT_BYTES_PER_CODE;
126+
fb_cursor_index = (fb_cursor_index / (TABSTOPS * FONT_BYTES_PER_CODE) + 1) * TABSTOPS * FONT_BYTES_PER_CODE;
135127
} else {
136128
// handle alphanumeric codes
137129
if (code < FONT_CODE_FIRST || code > FONT_CODE_LAST ) {
138130
font_index = FONT_INDEX_UNDEF;
139131
} else {
140132
font_index = FONT_BYTES_PER_CODE * (FONT_INDEX_START + code - FONT_CODE_FIRST);
141133
}
142-
// copy bitmap from font table
143-
memcpy(&frame_buffer[fb_out_index], &font[font_index], FONT_BYTES_PER_CODE);
144-
fb_out_index += FONT_BYTES_PER_CODE;
134+
// copy bitmap to frame buffer and advance cursor
135+
memcpy(&frame_buffer[fb_cursor_index], &font[font_index], FONT_BYTES_PER_CODE);
136+
fb_cursor_index += FONT_BYTES_PER_CODE;
145137
}
146138
}
147139
display_needs_refresh = true;
148140
}
149141

150142

151-
// convenience functions to set/clear pixels in the frame buffer
152-
void set_pixel_xy(uint x, uint y) {
143+
// some simple graphics funtions (for the display memory layout see the datasheet)
144+
void set_pixel(uint x, uint y) {
153145
if (x < NUM_X_PIXELS && y < NUM_Y_PIXELS) {
154146
frame_buffer[x + (y / 8) * NUM_X_PIXELS] |= (1 << (y % 8));
155147
display_needs_refresh = true;
156148
}
157149
}
158150

159-
void clear_pixel_xy(uint x, uint y) {
151+
void clear_pixel(uint x, uint y) {
160152
if (x < NUM_X_PIXELS && y < NUM_Y_PIXELS) {
161153
frame_buffer[x + (y / 8) * NUM_X_PIXELS] &= ~(1 << (y % 8));
162154
display_needs_refresh = true;
163155
}
164156
}
165157

166-
// set text output position in rows and columns
167-
// rows go from 0 (top) to NUM_Y_PIXELS / 8 - 1
168-
// columns go from 0 (left) to NUM_X_PIXELS / 8 - 1
158+
void draw_line(int x0, int y0, int x1, int y1) {
159+
int dx = abs (x1 - x0), sx = x0 < x1 ? 1 : -1;
160+
int dy = -abs (y1 - y0), sy = y0 < y1 ? 1 : -1;
161+
int err = dx + dy, e2;
162+
while(true) {
163+
set_pixel (x0, y0);
164+
if (x0 == x1 && y0 == y1) break;
165+
e2 = 2 * err;
166+
if (e2 >= dy) { err += dy; x0 += sx; }
167+
if (e2 <= dx) { err += dx; y0 += sy; }
168+
}
169+
display_needs_refresh = true;
170+
}
171+
172+
173+
// set the text output position
174+
// rows go from 0 at the top to NUM_Y_PIXELS/8 - 1 and columns go from 0 on the left to NUM_X_PIXELS/8 - 1
169175
void set_cursor_pos(uint text_row, uint text_col) {
170176
if (text_row < NUM_Y_PIXELS / PIXELS_PER_BYTE && text_col < NUM_X_PIXELS / FONT_BYTES_PER_CODE) {
171-
fb_out_index = text_row * NUM_X_PIXELS + text_col * FONT_BYTES_PER_CODE;
177+
fb_cursor_index = text_row * NUM_X_PIXELS + text_col * FONT_BYTES_PER_CODE;
178+
}
179+
}
180+
181+
// the callback function for our frame-rate timer
182+
bool frame_refresh_callback(__unused struct repeating_timer *t) {
183+
if (display_needs_refresh) {
184+
dma_channel_set_read_addr(dma_ch_transfer_fb, frame_buffer, true); // reset and trigger the dma channel
185+
display_needs_refresh = false;
172186
}
187+
return true; // reschedule the timer
173188
}
174189

175190

176191
int main(){
177192
stdio_init_all();
178193

179-
// initialise the interface and display
194+
// initialise everything
180195
dma_init();
181196
interface_init();
182197
display_reset();
183198

184-
// refresh the display from the frame buffer (if needed) once per frame using DMA
199+
// start refreshing the display
185200
struct repeating_timer timer;
186201
add_repeating_timer_ms(FRAME_PERIOD_MS, frame_refresh_callback, NULL, &timer);
187202

188-
// use a simple driver to copy stdout to the frame buffer
203+
// install our simple driver to copy stdout to the frame buffer
189204
stdio_driver_t fb_stdio_driver = { fb_out_chars };
190205
stdio_set_driver_enabled(&fb_stdio_driver, true);
191206

192-
// show some text on the screen
207+
// display some text
193208
set_cursor_pos(0, 2);
194209
printf("Hello, World\n");
195210

196211
// show a moving 'snake'
197212
int head_x = NUM_Y_PIXELS - 1, head_y = NUM_Y_PIXELS - 1, head_dx = 1, head_dy = 1;
198213
int tail_x = FONT_BYTES_PER_CODE + 1, tail_y = FONT_BYTES_PER_CODE + 1, tail_dx = 1, tail_dy = 1;
199214
while(true) {
200-
set_pixel_xy(head_x, head_y);
201-
clear_pixel_xy(tail_x, tail_y);
202-
// update head position
215+
set_pixel(head_x, head_y);
216+
clear_pixel(tail_x, tail_y);
203217
if (head_x + head_dx < 0 || head_x + head_dx >= NUM_X_PIXELS) {
204218
head_dx = -head_dx;
205219
}
@@ -208,7 +222,6 @@ int main(){
208222
head_dy = -head_dy;
209223
}
210224
head_y += head_dy;
211-
// update tail position
212225
if (tail_x + tail_dx < 0 || tail_x + tail_dx >= NUM_X_PIXELS) {
213226
tail_dx = -tail_dx;
214227
}
34.9 KB
Binary file not shown.
131 KB
Loading

0 commit comments

Comments
 (0)