How to Use a 0.96 Inch OLED with a PSoC

To use a 0.96 inch OLED with a PSoC, you wire the display’s I2C pins (SDA and SCL) to the PSoC’s I2C hardware block, configure the PSoC Creator project with a 400 kHz I2C master component, and send commands and data through a simple buffer. The 0.96 inch 128x64 i2c oled display uses the SSD1306 driver, which expects a 7-bit address of 0x3C (or 0x3D if the SA0 pin is pulled high). On the PSoC 5LP or PSoC 6, you allocate two pins for I2C—typically P12[0] for SCL and P12[1] for SDA on the CY8CKIT-059 kit—and set them as open-drain with internal pull-ups disabled because the display module already includes 4.7 kΩ resistors on its breakout board. The display’s logic voltage is 3.3 V, so you must never connect it to 5 V pins directly; the PSoC’s GPIO banks operate at 3.3 V by default, which matches perfectly. Power consumption sits at around 20 mA during full-on operation, but you can drop that to under 10 µA by sending a sleep command (0xAE) when the display is idle.

You start by initializing the SSD1306 in the PSoC firmware. The initialization sequence is a set of 25 bytes sent over I2C, starting with 0xAE (display off), then 0xD5 (set display clock divide ratio), 0x80 (default ratio), 0xA8 (set multiplex ratio), 0x3F (64 rows), 0xD3 (display offset), 0x00, 0x40 (display start line), 0x8D (charge pump), 0x14 (enable charge pump), 0x20 (memory addressing mode), 0x00 (horizontal mode), 0xA1 (segment remap, column 127 mapped to SEG0), 0xC8 (COM output scan direction, remapped mode), 0xDA (COM pins hardware configuration), 0x12 (alternative pin configuration), 0x81 (contrast), 0xCF (default contrast value), 0xD9 (pre-charge period), 0xF1, 0xDB (VCOMH deselect level), 0x40, 0xA4 (display on resume), 0xA6 (normal display, not inverted), 0x2E (deactivate scroll), and finally 0xAF (display on). Each byte is sent as a command by writing to the control byte 0x00 (Co=0, D/C#=0) followed by the command byte. The entire sequence takes about 10 ms at 400 kHz I2C clock, which is well within the PSoC’s timing capabilities.

Once initialized, you write pixel data to the display’s internal 1024-byte GDDRAM (128 columns × 64 rows / 8 bits per page = 128 × 8 = 1024 bytes). The SSD1306 organizes memory into 8 pages, each page covering 8 rows of pixels. Page 0 covers rows 0–7, page 1 covers rows 8–15, and so on up to page 7 covering rows 56–63. To write a full frame, you set the column start and end addresses (0x21 command, then 0x00 for start column, 0x7F for end column), set the page start and end addresses (0x22 command, then 0x00 for start page, 0x07 for end page), and then send 1024 bytes of data with the control byte 0x40 (Co=0, D/C#=1). At 400 kHz I2C, each byte transfer takes about 27 µs (including start, address, ACK, data, and stop), so the full frame update takes roughly 1024 × 27 µs = 27.6 ms, giving a theoretical refresh rate of about 36 Hz. In practice, you can achieve 30 Hz stable updates on a PSoC 5LP running at 80 MHz, because the CPU spends only about 3 ms of that time copying data from a buffer to the I2C TX FIFO.

For pixel manipulation, you need to map your (x, y) coordinates to the correct byte in the buffer. The formula is: byte_index = (y / 8) * 128 + x, and the bit position within that byte is y % 8. So if you want to set pixel (50, 20) to white, you compute page = 20 / 8 = 2 (page 2), byte offset = 2 * 128 + 50 = 306, and bit = 20 % 8 = 4. You then set bit 4 of buffer[306] to 1. To clear the pixel, set that bit to 0. You can precompute these offsets for performance, especially if you’re drawing text or shapes. The PSoC’s Cortex-M3 core can handle this math in under 1 µs per pixel, so drawing a 100-pixel line takes about 100 µs, plus the 27.6 ms to send the buffer to the display. That’s a total of 27.7 ms per frame, which is acceptable for static or slow-updating UIs.

I2C timing is critical. The SSD1306 datasheet specifies a maximum SCL frequency of 400 kHz in fast mode, and the PSoC’s I2C component can be configured to standard mode (100 kHz) or fast mode (400 kHz). I recommend using 400 kHz because it cuts the frame update time by 75% compared to 100 kHz. On the PSoC 5LP, the I2C component uses a dedicated hardware block that handles start/stop conditions, clock stretching, and arbitration automatically. You set the clock divider to achieve 400 kHz from the 24 MHz bus clock: divider = 24 MHz / (2 * 400 kHz) = 30, so you set the I2C_CFG register’s low and high period to 15 each. On the PSoC 6, the I2C block runs from the peripheral clock (typically 8 MHz), so you set the divider to 8 MHz / (2 * 400 kHz) = 10, with low and high periods of 5 each. Always verify the SCL waveform with an oscilloscope to ensure the duty cycle is close to 50% and the rise time is under 300 ns. If the rise time is too slow, add external pull-up resistors of 2.2 kΩ to 3.3 V, but the display module’s built-in 4.7 kΩ resistors usually work fine for short wires (under 10 cm).

Power management is another practical concern. The SSD1306 draws 12 mA to 20 mA when the display is on, depending on the contrast setting. The default contrast register (0x81) value is 0x7F, but you can reduce it to 0x10 to lower current to about 8 mA while still maintaining readable text. The charge pump (0x8D, 0x14) is necessary for the internal DC-DC converter that generates the 7–8 V needed for the OLED pixels. If you disable the charge pump by sending 0x8D, 0x10, the display goes completely dark and draws only 1 µA, which is useful for battery-powered PSoC designs. You can also use the display’s sleep mode by sending 0xAE; the display retains the GDDRAM contents but turns off the charge pump and driver, dropping current to 2 µA. On the PSoC side, you can put the I2C block into stop mode when not in use, and wake it up with a GPIO interrupt if you need to update the display. This combination yields a total system sleep current under 10 µA, which is competitive for wearable or IoT applications.

For text rendering, you need a font bitmap. A common choice is the 5x7 font, where each character occupies 5 columns and 7 rows, with one column of spacing between characters. You store the font as a 96-byte array (for ASCII 32–127, each character 5 bytes wide, but only 5 columns per character, so 96 × 5 = 480 bytes). To draw a character at (x, y), you iterate over the 5 columns, and for each column, you read the font byte, then for each of the 7 rows, check if the bit is set, and set the corresponding pixel in the buffer. At 30 Hz refresh, you can update a 20-character line in about 2 ms of CPU time, plus the 27.6 ms buffer transfer, so you can do real-time text updates without noticeable lag. If you need larger fonts, use a 8x16 font, which doubles the resolution but quadruples the buffer size (16 rows per character, so you need to handle two pages per character). The PSoC’s flash memory can store multiple fonts; a 5x7 font occupies 480 bytes, while an 8x16 font occupies 2048 bytes, both easily fitting in the 256 KB flash of a PSoC 5LP.

Drawing shapes like lines, circles, and rectangles requires a small graphics library. You can implement Bresenham’s line algorithm in about 20 lines of C code. For a line from (x0, y0) to (x1, y1), the algorithm uses integer arithmetic and avoids floating-point operations. The PSoC’s 32-bit CPU can compute each pixel in about 0.5 µs, so a 100-pixel line takes 50 µs. For circles, use the midpoint circle algorithm, which similarly uses integer math. A circle of radius 20 pixels requires about 80 pixel computations, taking 40 µs. These algorithms are deterministic and fit within the 27.6 ms frame budget easily. You can also draw filled rectangles by iterating over rows and columns, setting pixels in a nested loop. A 50x50 rectangle has 2500 pixels, which takes about 1.25 ms of CPU time, still leaving plenty of headroom for other tasks.

I2C bus errors can occur if the display is not powered or if the SDA/SCL lines are shorted. The PSoC’s I2C component provides status flags for arbitration lost, NACK, and bus error. You should check the I2C_MasterStatus register after each transaction. If you get a NACK (0x01), the display is not responding—check the address (0x3C, left-shifted to 0x78 for write) and the power supply. If you get arbitration lost (0x02), another master is driving the bus; ensure no other I2C devices are conflicting. The PSoC’s I2C block can be reset by calling I2C_Stop() and I2C_Start() in sequence, which clears the FIFO and resets the state machine. In my experience, a robust initialization routine includes a 100 ms delay after power-up, then three retries with 50 ms intervals if the display fails to ACK. This handles most power-on glitches.

Temperature and reliability are factors. The OLED display operates from -40°C to +85°C, but the I2C bus speed may degrade at high temperatures due to increased leakage current. At 85°C, the rise time of the SCL line can increase by 30% if the pull-up resistors are too weak. Using 2.2 kΩ resistors instead of 4.7 kΩ ensures the rise time stays under 300 ns even at high temperatures. The PSoC’s I2C pins have built-in Schmitt triggers that handle noise up to 0.5 V, so the bus is robust in noisy environments. If you’re running the display in a high-EMI environment, add a 100 nF capacitor between VCC and GND on the display module, and keep the I2C traces as short as possible (under 5 cm).

For advanced usage, you can implement partial updates. The SSD1306 supports page addressing mode, where you can write to a specific page without rewriting the entire buffer. This is useful for updating a single line of text without redrawing the whole screen. For example, to update only page 2 (rows 16–23), you send 0x21 (set column address), 0x00, 0x7F, then 0x22 (set page address), 0x02, 0x02, then send 128 bytes of data for that page. This reduces the transfer time from 27.6 ms to 128 × 27 µs = 3.5 ms, allowing you to update a single line at 285 Hz. The PSoC can handle this by maintaining a full 1024-byte buffer in RAM and only sending the modified pages. This technique is critical for applications like scrolling text or real-time graphs where you need high update rates.

Interfacing the display with the PSoC’s DMA controller can offload the CPU entirely. The PSoC 5LP has a DMA engine that can transfer data from a buffer to the I2C TX register without CPU intervention. You configure the DMA to trigger on the I2C TX FIFO empty signal, and set the source address to the buffer, destination to the I2C_DR register, and length to 1024 bytes. The DMA completes the transfer in about 20 ms, freeing the CPU for other tasks like sensor reading or communication. This is especially useful for animation loops where you need to update the display at 30 Hz while simultaneously processing data. The DMA setup requires about 10 lines of code in PSoC Creator, using the DMA component wizard.

Finally, debugging the display is straightforward with a logic analyzer. Capture the I2C traffic on SDA and SCL. The expected sequence is: start condition, address 0x78 (0x3C left-shifted), ACK from display, control byte 0x00 (command), ACK, command byte, ACK, stop condition. For data writes, the control byte is 0x40. If you see no ACK, the display is not powered or the address is wrong. If you see clock stretching (SCL held low by the display), the display is busy processing a command; the PSoC handles this automatically, but it can increase transfer time. The SSD1306 datasheet specifies a maximum clock stretch of 50 µs, but in practice it’s under 10 µs. If you see repeated start conditions instead of stop-start, that’s a combined transaction, which is valid but not necessary. The PSoC’s I2C component can be configured to generate repeated starts by setting the I2C_M_CSR2_STOP bit to 0, but for simplicity, I always use separate stop-start sequences.