Skip to content

How to make a menu on a 0.95 inch color OLED?

By admin·

How to Make a Menu on a 0.95 Inch Color OLED

To build a menu on a 0.95 inch color OLED, you start by selecting the right hardware and understanding its constraints. The 0.95 inch 96x64 color oled display is a small, full-color screen with a resolution of 96x64 pixels, typically driven by an SSD1331 controller over SPI. This means you have 6,144 pixels to work with, each capable of displaying 65,000 colors via 16-bit RGB565 encoding. The display’s physical size is about 25.7mm x 13.5mm, so text and icons must be tiny—usually 5x7 or 8x8 pixel fonts. You’ll need a microcontroller like an ESP32, STM32, or Arduino board with at least 4KB of RAM for frame buffering, as the SSD1331 lacks internal memory for full-screen storage. SPI speed should be set to 8-16 MHz to avoid flicker during menu transitions.

First, wire the OLED correctly. The SSD1331 uses 8 pins: VCC (3.3V), GND, SCLK (clock), MOSI (data), CS (chip select), DC (data/command), RST (reset), and optionally MISO (unused). Connect VCC to 3.3V—never 5V, as it can damage the driver. Use a logic level shifter if your microcontroller runs at 5V. For example, on an ESP32, assign GPIO 18 for SCLK, GPIO 23 for MOSI, GPIO 5 for CS, GPIO 17 for DC, and GPIO 16 for RST. Pull CS high when idle to avoid bus conflicts. The SPI mode is mode 0 (CPOL=0, CPHA=0), meaning data is sampled on the rising edge of the clock. Initialize the display with a sequence of commands: set the display off (0xAE), set column and row address ranges (0x15 and 0x75 for 96x64), set contrast (0x81 with value 0x80 for 50% brightness), set pre-charge speed (0x8A with 0x78), and finally turn display on (0xAF). Each command takes about 1-2 microseconds to send at 8 MHz SPI.

Now, plan your menu structure. With only 96 columns and 64 rows, you can fit about 3-4 lines of text if using a 8x8 pixel font, or 5-6 lines with a 5x7 font. Each line needs 1-2 pixels of spacing for readability. A typical menu might have a title bar at the top (8 pixels high), a list of 4 items (each 12 pixels tall including spacing), and a footer (8 pixels). That leaves 64 - 8 - 48 - 8 = 0 pixels, so you need to scroll or use pages. For a 5-item menu, use a 10-pixel font with 2-pixel spacing, giving 5 items in 60 pixels, plus 4 pixels for margins. Data density: each character in a 5x7 font requires 5x7=35 bits, but with 16-bit color, you need 5x7x16=560 bits per character. A 96-pixel wide line holds 96/5=19 characters max, but with spacing, 15-16 is practical. So a 4-line menu with 16 characters each consumes 4x16x560=35,840 bits, or about 4.5 KB of frame buffer. The SSD1331’s RAM is 96x64x16=98,304 bits (12 KB), so you have headroom for icons.

Implement the menu logic in C/C++ using a library like Adafruit_SSD1331 or u8g2. The u8g2 library supports 5x7 and 8x8 fonts, and handles SPI with DMA on ESP32. Start by defining a menu structure: an array of strings (max 20 characters each) and a function pointer for each item. For example: const char* menuItems[] = {"Settings", "Brightness", "WiFi", "About", "Exit"};. Use a variable currentIndex (0-4) to track selection. Draw the menu in a loop: clear the buffer with u8g2_ClearBuffer(), set font with u8g2_SetFont(u8g2_font_5x7_tr), then iterate through items. For each item, calculate its y-position: y = 8 + (i * 12) for 12-pixel row height. If i == currentIndex, draw a filled rectangle behind the text using u8g2_DrawBox(0, y-1, 96, 10) with a color like RGB(0, 0, 255) for blue highlight. Then draw the text with u8g2_DrawStr(2, y+8, menuItems[i]) in white (RGB(255,255,255)). Send the buffer with u8g2_SendBuffer(). This takes about 10-20 milliseconds per frame at 8 MHz SPI, so you can update at 50-100 Hz.

Handle user input with two buttons: one for up/down (using a rotary encoder or two tactile switches) and one for select. On an ESP32, read GPIO pins with debouncing via a 50ms timer interrupt. For example, pin 25 for up, pin 26 for down, pin 27 for select. When a button is pressed, increment or decrement currentIndex with bounds checking (0 to 4). On select, call the function pointer: menuItems[currentIndex].action(). For a submenu like “Brightness,” create a new menu array with items like “Increase,” “Decrease,” “Back.” Use a stack to track menu depth—max 3 levels due to memory limits. Each level consumes about 100 bytes for the array and state, so 3 levels use 300 bytes, leaving 3.7 KB for other data on an ESP32 with 4KB RAM.

Add icons to improve readability. Since the display is 96x64, use 16x16 pixel icons for each menu item. For example, a gear icon for “Settings” and a WiFi icon for “WiFi.” Store icons as arrays of 16-bit RGB565 values. A 16x16 icon requires 16x16x2=512 bytes. For 5 icons, that’s 2.5 KB, which fits in flash memory. Draw icons to the left of text: u8g2_DrawXBM(2, y, 16, 16, settingsIcon). Adjust text position to x=20 to avoid overlap. If using u8g2, note that it supports XBM format only for monochrome—for color icons, use the Adafruit library’s drawBitmap() function. Precompute icons with a tool like LCD Image Converter, exporting as 16-bit RGB565 arrays. Test contrast: a bright icon on a dark background works best, e.g., icon color RGB(255, 0, 0) on black background.

Optimize performance by using double buffering. The SSD1331 has a built-in 12KB frame buffer, but you can also use a software buffer in RAM. On ESP32, allocate a 12KB buffer with malloc(96*64*2). Write all drawing operations to this buffer, then send it via SPI in one burst using SPI.writeBytes(buffer, 12288). This avoids flicker from partial updates. The SPI transaction takes about 1.2 milliseconds at 16 MHz (12288 bytes / 2 bytes per transfer = 6144 transfers, each 8 bits at 16 MHz = 0.5 microseconds, total ~3ms with overhead). To reduce memory, use a 1-bit buffer for monochrome text and overlay color—but that complicates code. Stick with 16-bit color for simplicity.

Handle scrolling for menus with more than 5 items. If you have 10 items, show only 5 at a time. Use a scrollOffset variable (0 to 5) to shift the visible window. When currentIndex goes below the scroll offset, decrement offset; when above offset+4, increment offset. Redraw the menu with the new offset. For example, with items 0-9 and offset=0, show items 0-4. When user moves to item 5, set offset=1, showing items 1-5. This requires recalculating y-positions: y = 8 + ((i - offset) * 12). Ensure the highlight box stays within the visible area. Test with a 10-item menu: each frame update takes about 15ms, so scrolling feels smooth at 60 fps.

Add animations for transitions. When switching between menus, fade out the current screen by reducing each pixel’s brightness by 10% per frame over 10 frames. Use a loop: for each pixel, read its value, multiply by 0.9, and write back. This takes 10x12KB=120KB of SPI transfers, but at 16 MHz, it’s about 36ms—acceptable. Alternatively, slide the menu left or right by shifting the buffer. For a left slide, copy columns 1-95 to 0-94, then fill the rightmost column with black. Repeat 96 times for a 96-pixel slide. This is CPU-intensive but looks professional. Use a timer interrupt to update at 30 fps during transitions.

Power consumption is critical for battery-powered devices. The SSD1331 draws about 20-30 mA at full brightness with all pixels white. Reduce to 10 mA by setting contrast to 0x40 (25% brightness) and using a dark background (black pixels consume less current). In your menu, set the background to black (0x0000) and text to white (0xFFFF). The OLED’s current is proportional to the number of lit pixels—a 50% filled menu (text and icons) uses about 15 mA. Use a sleep mode: after 10 seconds of inactivity, send the display off command (0xAE) and put the microcontroller into deep sleep, waking on a button press via an interrupt. This drops current to under 0.1 mA.

Debug with a logic analyzer. Connect a Saleae or similar to the SPI lines (SCLK, MOSI, CS) and capture transactions. Look for correct command sequences: after reset, the first byte should be 0xAE (display off), then 0x15 (set column range) with data 0x00 and 0x5F (96 columns), then 0x75 (set row range) with 0x00 and 0x3F (64 rows). Verify that the CS line goes low for each command. If the display shows garbage, check that the SPI mode is set correctly (mode 0) and that the reset pin is held high for at least 10ms after power-up. Common issues: wrong pin mapping, missing pull-up resistors on CS (10kΩ to 3.3V), or incorrect voltage levels (3.3V logic only).

Use a real-time operating system (RTOS) like FreeRTOS on ESP32 to manage menu tasks. Create two tasks: one for SPI communication (priority 2, stack size 2048 bytes) and one for user input (priority 1, stack size 1024 bytes). Use a queue to pass button events from the input task to the menu logic. The SPI task waits on a semaphore before sending buffer updates, ensuring no conflicts. This prevents flicker from overlapping SPI transactions. With RTOS, the menu update rate stays consistent at 50 Hz even with background tasks like WiFi scanning.

Test with a simple example: a 3-item menu with “LED On,” “LED Off,” and “Status.” Use an LED on GPIO 2. When “LED On” is selected, set GPIO 2 high. For “Status,” read the GPIO state and display “ON” or “OFF” on the OLED. This validates your menu logic and hardware interaction. Measure the response time: from button press to LED change, it should be under 50ms. If slower, optimize the SPI write by using DMA (direct memory access), which offloads the CPU. On ESP32, enable DMA with SPI.beginTransaction(SPISettings(16000000, MSBFIRST, SPI_MODE0)); and use SPI.transfer(buffer, NULL, 12288); for non-blocking transfers.

For advanced features, add a progress bar or slider for brightness control. Use a 96-pixel wide bar, 8 pixels tall. Draw a filled rectangle for the current level: u8g2_DrawBox(0, 56, (brightness * 96) / 255, 8) with color RGB(0, 255, 0). Update brightness by reading an analog potentiometer on ADC pin. This adds 10 lines of code and improves user experience. The 0.95 inch OLED’s color capability allows you to use red for warnings, green for success, and blue for info—each consumes different current, so monitor power.

Store menu configurations in non-volatile memory (NVS) on ESP32. Use the Preferences library to save the last selected menu item and brightness level. On boot, read these values and restore the menu state. This adds about 50 bytes of NVS usage. For example: preferences.begin("menu", false); preferences.putInt("lastIndex", currentIndex);. This ensures the menu shows the same screen after power cycle, which is critical for devices like smart watches or IoT controllers.

Finally, consider the viewing angle and readability. The 0.95 inch OLED has a 160-degree viewing angle, but direct sunlight can wash out colors. Use a polarizing film or increase contrast to 0xFF (maximum) for outdoor use. The pixel pitch is 0.21mm, so text smaller than 5x7 is unreadable. Stick to 8x8 fonts for critical labels. With these techniques, you can build a responsive, multi-level menu system that fits in under 12KB of RAM and runs on a $3 microcontroller. The key is balancing pixel density with processing speed—every millisecond counts when you have only 6,144 pixels to work with.