How to display a waveform on a 0.95 inch 96x64 OLED?
To display a waveform on a 0.95 inch 96x64 OLED, you need to drive the display with a microcontroller that samples an analog signal, maps the voltage values to pixel coordinates, and refreshes the screen at a rate that avoids flicker. The 0.95 inch 96x64 color oled display uses a 6-pin SPI interface, typically with the SSD1331 controller, which supports 16-bit color (65,536 colors) and a pixel clock up to 4 MHz. For a real-time waveform, you’ll sample an input signal using an ADC (like the one in an Arduino Uno or ESP32), store the values in a buffer, then draw lines between consecutive points. The display’s resolution—96 columns by 64 rows—limits the waveform to 64 vertical steps (0–63) and 96 horizontal samples per frame. A practical refresh rate is 30–60 Hz, achieved by writing the entire frame buffer via SPI at 4 MHz, which takes about 2.5 ms per frame (96 * 64 * 2 bytes = 12,288 bytes, at 4 MHz SPI clock, that’s roughly 24.6 µs per byte, so 12,288 * 24.6 µs ≈ 302 ms if you send one byte at a time, but with DMA or block writes, you can push it down to 10–15 ms). To get a smooth waveform, you’ll need to double-buffer: write to a back buffer while the display shows the front buffer, then swap them. The SSD1331 has a built-in 96x64x16-bit RAM, so you can directly write pixel data to the GDDRAM via the SPI command 0x2A (set column address) and 0x2B (set row address), followed by 0x2C (write memory). For a 0.95 inch OLED, the pixel pitch is about 0.24 mm, so the waveform will be crisp if you anti-alias the lines—but at 96x64, you’re better off using Bresenham’s line algorithm to draw thick lines (2–3 pixels wide) to make the trace visible. The OLED’s contrast ratio is typically 10,000:1, and the brightness can be set via the contrast register (0x81) from 0 to 255, but for waveform display, you’ll want a high contrast (e.g., 0x7F) to avoid ghosting. Power consumption is around 20–30 mA at full brightness, which is fine for battery-powered scopes. The display’s viewing angle is >160 degrees, so the waveform is readable from any angle.
Now, let’s break down the hardware and software requirements in detail. You need a microcontroller with at least 12 KB of RAM for the frame buffer (96 * 64 * 2 = 12,288 bytes), plus extra for the waveform buffer. The ESP32 is a good choice because it has 520 KB SRAM, a 12-bit ADC (0–4095), and two SPI interfaces. The Arduino Uno, with only 2 KB SRAM, can’t store a full frame buffer, so you’d have to write pixel data directly to the display (which is slow and causes tearing). For the ESP32, use the SPI library with a clock divider of 2 (80 MHz / 2 = 40 MHz), but the SSD1331 max is 4 MHz, so set the SPI clock to 4 MHz. The wiring is straightforward: connect the OLED’s CS (chip select) to GPIO 5, DC (data/command) to GPIO 4, RES (reset) to GPIO 18, SDA (MOSI) to GPIO 23, SCL (SCK) to GPIO 18, and VCC to 3.3V. The OLED’s logic level is 3.3V, but the ESP32 is also 3.3V, so no level shifting is needed. For the analog input, use the ESP32’s ADC1 channel 0 (GPIO 36) with an attenuation of 11 dB to measure 0–3.3V. The ADC has a resolution of 12 bits, but you’ll map it to 64 vertical pixels: value = (adc_reading >> 6) & 0x3F (since 4096 / 64 = 64). This gives you a 6-bit resolution, which is enough for a 64-row display. The waveform buffer should be a circular array of 96 8-bit values (0–63), updated at 100 Hz (10 ms per sample). To draw the waveform, clear the display by writing a background color (e.g., black, 0x0000) to the entire GDDRAM, then draw lines between consecutive points. For a scrolling waveform, shift the buffer left by one sample each time, and draw the new point at the rightmost column. Use the Adafruit GFX library or the TFT_eSPI library (which supports SSD1331) to handle the drawing, but for performance, write your own SPI commands. Here’s a pseudo-code snippet for the drawing loop:
```
void drawWaveform(uint8_t *buffer, uint8_t len) {
for (int x = 0; x < len - 1; x++) {
int y1 = 63 - buffer[x]; // invert y because OLED y=0 is top
int y2 = 63 - buffer[x+1];
drawLine(x, y1, x+1, y2, 0xFFFF); // white color
}
}
```
The `drawLine` function uses Bresenham’s algorithm, which you can implement in about 20 lines of C code. The display’s SPI write sequence for a single pixel is: set column address (0x2A, start col, end col), set row address (0x2B, start row, end row), then write memory (0x2C) with two bytes for the color (R5, G6, B5). For a line, you can set the column and row addresses to the bounding box of the line, then write all pixels in one burst. This reduces SPI overhead. For example, if the line spans columns 10 to 20 and rows 30 to 40, set the column address to 10 and 20, row address to 30 and 40, then write (21 * 11) * 2 bytes = 462 bytes. This is much faster than writing each pixel individually. The SSD1331’s maximum write speed is 4 MHz, so a burst of 462 bytes takes about 924 µs (462 * 2 bytes * 8 bits / 4 MHz = 1.85 ms). For a full waveform of 96 points, you’ll have 95 lines, each with a bounding box of 2 columns and up to 64 rows. The total SPI time for a full frame is roughly 95 * 1.85 ms = 176 ms, which is too slow for 60 Hz. To fix this, you can optimize by drawing the entire waveform as a single polygon or using the display’s hardware acceleration—the SSD1331 has a “draw line” command (0x21) that draws a line from (x0, y0) to (x1, y1) with a single command. This command takes 6 bytes: 0x21, x0, y0, x1, y1, color. So for 95 lines, you send 95 * 6 = 570 bytes, which at 4 MHz takes 570 * 8 / 4 MHz = 1.14 ms. That’s fast enough for 60 Hz. The command 0x21 draws a line of 1-pixel width, but you can make it thicker by drawing multiple lines offset by 1 pixel. For a 2-pixel-wide line, draw two lines: (x0, y0) to (x1, y1) and (x0, y0+1) to (x1, y1+1). This doubles the data to 1,140 bytes, still under 2.3 ms. So the total frame time is 2.3 ms for drawing plus 2.5 ms for clearing the display (if you clear it by writing a rectangle of black pixels). To clear the display, set the column and row addresses to the full screen (0x2A, 0, 95; 0x2B, 0, 63) and write 12,288 bytes of black (0x0000). This takes 12,288 * 2 * 8 / 4 MHz = 49.2 ms, which is too slow. Instead, you can avoid clearing the entire display by only overwriting the previous waveform’s pixels. This requires storing the previous frame’s pixel positions and drawing black lines over them before drawing the new waveform. Alternatively, use a double-buffer in the microcontroller’s RAM: write the new waveform to a back buffer, then send the entire back buffer to the display via a single SPI burst. With an ESP32, you can pre-allocate a 12,288-byte buffer and use SPI’s `writePixels` function to send it in one go. The SPI transfer time for 12,288 bytes at 4 MHz is 24.6 ms, which gives a refresh rate of 40 Hz. If you increase the SPI clock to 8 MHz (the SSD1331 can handle up to 4 MHz, but some modules overclock to 8 MHz), the transfer time drops to 12.3 ms, giving 81 Hz. However, overclocking may cause data corruption, so stick to 4 MHz and accept 40 Hz. For a waveform display, 40 Hz is fine because the human eye can’t perceive flicker above 30 Hz.
Now, let’s talk about the signal conditioning. The analog input to the ESP32’s ADC should be buffered with an op-amp (e.g., LM358) to avoid loading the signal source. The ADC has an input impedance of about 100 kΩ, so a voltage divider or direct connection works for low-impedance sources. For high-impedance signals (e.g., from a microphone), add a unity-gain buffer. The ADC’s sampling rate can be set to 10 kHz using the `adc1_config_width` and `adc1_config_channel_atten` functions, then read in a loop with `adc1_get_raw`. To achieve 100 Hz waveform update, you sample at 100 Hz (10 ms per sample) and store 96 samples. This means the waveform shows a 960 ms time window. If you want a faster sweep, reduce the number of samples or increase the sampling rate. For example, at 1 kHz sampling, you can fill 96 samples in 96 ms, showing a 96 ms window. The ESP32’s ADC can sample at up to 2 MHz, but the waveform buffer update rate is limited by the display refresh. To trigger the sampling, use a timer interrupt (e.g., using the ESP32’s timer group) to read the ADC at precise intervals. The timer should be set to the desired sampling period (e.g., 10 ms for 100 Hz). The ISR (interrupt service routine) should be short: read the ADC, map to 0–63, and store in the circular buffer. The main loop then draws the waveform. For a triggered display (like an oscilloscope), you can add a trigger condition: if the signal crosses a threshold, start sampling. This requires a separate hardware comparator or a software threshold in the ADC reading. The SSD1331’s response time is about 80 µs per pixel, so the display is fast enough for real-time updates.
Let’s look at the data sheet specifications for the 0.95 inch 96x64 color oled display. The display module has a resolution of 96x64 pixels, with a pixel size of 0.24 mm x 0.24 mm. The active area is 23.04 mm x 15.36 mm, which gives a diagonal of 0.95 inches. The display supports 262K colors (16-bit RGB565), with a brightness of 100 cd/m² (typical). The contrast ratio is 10,000:1, and the viewing angle is >160 degrees. The interface is 4-wire SPI (CS, DC, RES, SDA, SCL) with a maximum clock frequency of 4 MHz. The operating voltage is 2.8V to 3.3V, and the logic input voltage is 1.8V to 3.3V. The driver IC is the SSD1331, which includes a 96x64x16-bit SRAM for the frame buffer. The display consumes 20 mA at full brightness and 0.1 mA in sleep mode. The module includes a built-in DC-DC converter for the OLED panel, so no external boost converter is needed. The pinout is: 1: GND, 2: VCC (3.3V), 3: SCL (clock), 4: SDA (MOSI), 5: RES (reset), 6: DC (data/command), 7: CS (chip select). Some modules have an additional pin for BS1 (bus select) but it’s usually tied to GND for SPI. The display’s SPI command set includes over 20 commands for setting contrast, brightness, sleep mode, and drawing primitives. The key commands for waveform display are: 0xAE (display off), 0xAF (display on), 0x81 (set contrast), 0xA0 (set remap), 0xA1 (set display start line), 0xA2 (set display offset), 0xA4 (set display mode normal), 0xA6 (set display mode inverse), 0x21 (draw line), 0x22 (draw rectangle), 0x23 (copy area), 0x24 (draw filled rectangle), 0x25 (set draw mode), 0x26 (set draw color), 0x27 (set draw background), 0x2A (set column address), 0x2B (set row address), 0x2C (write memory), 0x2E (read memory). The draw line command (0x21) takes 6 bytes: command, x0, y0, x1, y1, color. The color is 16-bit, sent as two bytes: high byte (R5 G3) and low byte (G3 B5). For example, white is 0xFFFF (high: 0xFF, low: 0xFF). The draw rectangle command (0x22) takes 7 bytes: command, x0, y0, x1, y1, color, fill. The fill byte is 0 for outline, 1 for filled. For clearing the waveform area, use a filled rectangle with black color (0x0000). The copy area command (0x23) is useful for scrolling the waveform: copy a region from one part of the GDDRAM to another. This can be used to shift the waveform left by one column without redrawing all pixels. The command takes 6 bytes: command, source_x, source_y, dest_x, dest_y, width, height. For a 96-pixel-wide waveform, you can copy columns 1 to 95 to columns 0 to 94, then draw the new sample at column 95. This reduces SPI traffic significantly. For example, to scroll left by one column, send: 0x23, 1, 0, 0, 0, 95, 64 (copy 95 columns from x=1 to x=0). This copies 95 * 64 = 6,080 pixels, but the SPI transfer is only 6 bytes for the command, plus the copy operation is done internally by the SSD1331, so it’s very fast. Then you only need to draw the new column (64 pixels) at x=95. The new column can be drawn using the draw line command from (95, y_prev) to (95, y_new) with a vertical line. This approach reduces the SPI data per frame to about 6 bytes (copy) + 6 bytes (line) = 12 bytes, plus the initial setup. This gives a frame time of 12 * 8 / 4 MHz = 24 µs, which is negligible. The display’s internal copy operation takes about 100 µs, so the total frame time is under 200 µs, allowing refresh rates of 5 kHz. However, the ADC sampling rate and the waveform buffer update will limit the practical refresh rate to 1 kHz or less.
For a practical implementation, you’ll need to initialize the SSD1331 with the following sequence: reset the display by pulling RES low for 10 ms, then high. Send the commands: 0xAE (display off), 0xA0 (set remap) with 0x72 (enable COM remap, column remap, and nibble remap), 0xA1 (set display start line) with 0x00, 0xA2 (set display offset) with 0x00, 0x81 (set contrast) with 0x7F, 0x8B (set pre-charge speed) with 0x62, 0x8C (set pre-charge voltage) with 0x7F, 0x87 (set master current) with 0x06, 0x81 (set contrast) with 0x7F, 0xB1 (set phase length) with 0x74, 0xB3 (set clock divider) with 0xF0, 0x2A (set column address) with 0x00 and 0x5F (95), 0x2B (set row address) with 0x00 and 0x3F (63), 0x2C (write memory) to clear the display, then 0xAF (display on). This initialization takes about 50 ms. After that, you can start the waveform loop. The display’s contrast register (0x81) controls the brightness of the OLED panel. For a waveform display, you want a high contrast to make the trace bright against a dark background. Set the contrast to 0x7F (127) for normal brightness, or 0xFF for maximum. The display’s current consumption scales with contrast, so at 0xFF, it may draw 30 mA. The pre-charge voltage (0x8C) should be set to 0x7F for typical operation. The clock divider (0xB3) sets the internal oscillator frequency; a value of 0xF0 gives a frame rate of about 100 Hz, which is fine.
Now, let’