Skip to content
Engineering Notes · Yeinz

How to display a menu on a 2.08 inch 256x64 OLED display?

a By admin

How to Display a Menu on a 2.08 inch 256x64 OLED Display

To display a menu on a 2.08 inch 256x64 oled display, you need to treat it as a pixel-addressable, monochrome matrix with a resolution of 256 columns and 64 rows. This specific size, often driven by an SSD1306 or SH1106 controller via SPI, is ideal for compact UIs because each pixel is individually controlled. The first step is wiring: connect the display’s CS (chip select), DC (data/command), RES (reset), SDA (data), and SCL (clock) pins to your microcontroller’s SPI bus. For an Arduino Uno, that means pin 10 for CS, pin 9 for DC, pin 8 for RES, pin 11 for MOSI (SDA), and pin 13 for SCK (SCL). Power it with 3.3V or 5V depending on the module, but never exceed 5V on logic pins. The display draws about 20mA typical, so a standard USB port handles it fine.

Once wired, you need a library. The Adafruit SSD1306 library works for 256x64 displays, but you must set the correct dimensions in the constructor: Adafruit_SSD1306 display(256, 64, &SPI, DC, CS, RES);. Initialize it in setup() with display.begin(SSD1306_SWITCHCAPVCC, 0x3C) for I2C or display.begin(SSD1306_SWITCHCAPVCC) for SPI. The SPI version is faster—up to 10 MHz clock speed—which matters when redrawing a menu. The frame buffer is 256 * 64 / 8 = 2048 bytes. That’s small enough to store in RAM on most microcontrollers, but on an ATmega328P (2KB SRAM), you’re tight. Use a microcontroller with at least 4KB SRAM, like an ESP32 or STM32, to avoid buffer overflow.

For a menu, you need to design a layout that fits the 256x64 pixel grid. Each character in a 5x7 font takes 6 pixels wide (including spacing). With 256 pixels, you can fit 42 characters per line. The display is 64 pixels tall, so you can show 8 lines of 8-pixel tall text (like 8x8 font) or 5 lines of 13-pixel tall text (like 13x16 font). A practical menu uses a 12x16 font for headers and 8x8 for items. For example, a main menu with 4 items: each item occupies 16 pixels tall (including padding), so you fit 4 items on screen. The remaining 64 – (4*16) = 0 pixels, so no scrolling needed. But if you have 6 items, you need scrolling. Implement a viewport: track a menuOffset variable that shifts which items are visible. Redraw only the changed portion using display.setCursor() and display.fillRect() to clear old text.

User input is crucial. Use a rotary encoder with a button: connect the encoder’s A and B pins to interrupts (e.g., pins 2 and 3 on Arduino) and the button to a digital pin. Each encoder tick increments or decrements a menuIndex variable. Wrap it modulo the number of items. The button press selects the current item. In code, read the encoder in an interrupt service routine (ISR) to avoid missing steps. Debounce the button with a 50ms delay. For a tactile switch, use a pull-up resistor (10kΩ) to 5V. The encoder’s mechanical detents give 20 steps per revolution, so one click moves one menu item. That’s precise enough for 4-10 items.

Rendering the menu involves drawing a cursor or highlight. Use display.fillRect() to draw a filled rectangle behind the selected item. For example, if item 2 is selected, calculate its y-position: int y = 2 * 16 + menuOffset (assuming 16-pixel item height). Then display.fillRect(0, y, 256, 16, WHITE) to highlight it. Invert the text color by drawing text in BLACK on the WHITE background. This creates a high-contrast selection. The rest of the items are drawn with display.setTextColor(WHITE) on a black background. For a submenu, clear the screen with display.clearDisplay() and draw a new set of items. Memory-wise, each fillRect call modifies the frame buffer, so you need to call display.display() to push the buffer to the display. Over SPI, this takes about 2ms at 8 MHz, so you can update at 500 Hz—far faster than human interaction.

Data density matters. A 256x64 monochrome display has 16,384 pixels. Each pixel is either on or off, so you can store a full screen in 2KB. For a menu with icons, precompute bitmaps. For example, a 16x16 icon takes 32 bytes. Store them in PROGMEM on AVR or in flash on ESP32. Use display.drawBitmap() to place them. A menu with 4 icons and text uses about 200 bytes of flash per page. If you have 10 pages, that’s 2KB—acceptable. But avoid storing full-screen bitmaps for each menu; instead, draw text and shapes procedurally to save memory.

Performance optimization: only redraw changed parts. Use display.setCursor() and display.print() for text, but clear the old text area first with display.fillRect(). If you have a static background (like a header), draw it once in setup() and never redraw it. Use display.dim() to reduce brightness if needed—the display draws 20mA at full brightness, but dimming to 50% cuts current to 12mA. For battery-powered devices, this matters. The display’s contrast register (0x81) can be set to values 0-255. Default is 0x7F (127). Lower it to 0x40 for longer battery life.

Real-world example: a menu for a sensor readout. Item 1: "Temperature: 23.4°C", Item 2: "Humidity: 55%", Item 3: "Pressure: 1013 hPa", Item 4: "Settings". Use a 8x8 font for values and 12x16 for headers. The display’s 256 pixels wide let you show two columns: left column for labels (16 chars each) and right column for values (16 chars each). That’s 32 chars per line, leaving 10 pixels for padding. The 64-pixel height fits 4 rows of 16 pixels each. So you can show 4 items simultaneously. For scrolling, use a scrollOffset that increments when the encoder turns past the last visible item. The display’s hardware scrolling (via command 0x27) shifts the entire frame buffer vertically, but it’s easier to implement software scrolling by redrawing the buffer.

For the 2.08 inch 256x64 oled display specifically, the physical dimensions are 2.08 inches diagonally, which is about 52.8mm. The active area is 48.0mm x 12.0mm, with a pixel pitch of 0.188mm. This gives a sharp 132 DPI—enough for readable text at 30cm distance. The SPI interface runs at 3.3V logic, but 5V-tolerant pins are common. The display’s driver, typically SSD1306, supports 128x64 natively, but the 256x64 version uses two 128x64 drivers side by side, addressed via a segment remap. The initialization sequence must include command 0xA0 (segment remap) and 0xA8 (multiplex ratio) set to 63. Without this, the display will show garbled output. The datasheet specifies that the display can be refreshed at 100 Hz, but typical use is 60 Hz to avoid flicker.

To handle multiple menu levels, use a state machine. Define an enum: enum MenuState { MAIN_MENU, SUBMENU_TEMP, SUBMENU_HUM, SUBMENU_SETTINGS };. Each state has its own drawing function. The loop() function checks the encoder and button, updates the state, and calls the appropriate draw function. Avoid using delay(); use millis() for non-blocking timing. For example, a button press triggers a 200ms debounce period. During that time, ignore further inputs. This prevents accidental double-clicks. The encoder ISR should be fast: just increment or decrement a volatile int. The main loop reads it and resets it to 0.

Testing: connect the display to an oscilloscope on the SDA line. At 8 MHz SPI, each byte takes 1.25µs. A full frame of 2048 bytes takes 2.56ms, plus command overhead. So you can update the display at 390 Hz theoretically. In practice, the Arduino library adds overhead, so 30-60 Hz is realistic. For a menu, 30 Hz is smooth enough. If you see flicker, reduce the update rate by only calling display.display() when the menu changes. Use a flag: bool menuChanged = true; set by the encoder ISR. In loop(), if menuChanged, redraw and set it to false.

Power consumption: the display itself uses 20mA at 3.3V. The microcontroller adds 10-50mA depending on the chip. An ESP32 in deep sleep can drop to 10µA, but the display must be turned off via the display.ssd1306_command(SSD1306_DISPLAYOFF) command. This saves power. For a battery-powered menu, use a P-channel MOSFET to cut power to the display entirely. The display’s startup time is 100ms, so it’s acceptable for periodic wake-ups.

One common pitfall: the display’s buffer is 2048 bytes, but the SSD1306 library uses a 1024-byte buffer for 128x64. For 256x64, you need to modify the library or use a custom one. The Adafruit library supports it if you define SSD1306_128_64 as 0 and set WIDTH and HEIGHT manually. Alternatively, use the 2.08 inch 256x64 oled display with a pre-configured library from the manufacturer. The library handles the dual-driver addressing. Without it, you’ll see only half the screen.

For a menu with graphics, draw shapes using display.drawLine(), display.drawRect(), and display.drawCircle(). These use Bresenham’s algorithm, which is fast on 8-bit microcontrollers. A 256-pixel line takes 256 iterations, each with a few integer operations. That’s about 50µs at 16 MHz. For a menu separator line, draw a horizontal line at y=16: display.drawLine(0, 16, 255, 16, WHITE). This creates a visual break between header and items.

Data storage: if you have a list of menu strings, store them in flash using const char menuItems[][16] PROGMEM = {"Temp", "Humidity", "Pressure", "Settings"};. Each string is 16 bytes max. For 10 items, that’s 160 bytes. Use strcpy_P() to copy to RAM before printing. This saves SRAM for the frame buffer. The total SRAM usage is 2048 bytes for buffer + 16 bytes for temp string + 4 bytes for variables = 2068 bytes. On an ATmega328P with 2KB SRAM, you have 44 bytes left—tight but workable. For more headroom, use an ESP32 with 520KB SRAM.

User experience: the menu should be responsive. The encoder’s mechanical detents give tactile feedback, but you can add audio feedback with a piezo buzzer on pin 6. Generate a 1kHz tone for 10ms on each encoder tick. This confirms the user’s action. The button press produces a 2kHz tone for 20ms. The buzzer draws 5mA, so use a transistor driver if needed. The display’s refresh rate is fast enough to show animations, like a sliding highlight. To animate, increment the highlight’s y-position by 1 pixel per frame over 16 frames. This takes 16 * 2ms = 32ms, which is imperceptible. But it adds polish.

Edge cases: if the menu has more items than fit, show a scrollbar. Draw a vertical rectangle on the right side: width 4 pixels, height 64 pixels. The scrollbar’s position and length indicate the viewport. For example, 10 items total, 4 visible. The scrollbar height = 64 * (4 / 10) = 25.6 pixels, round to 26. Its y-position = 64 * (menuOffset / 10) = 64 * 0 = 0 for first page. Update it on each scroll. This uses 4 * 64 = 256 pixels, or 32 bytes of buffer. It’s negligible.

To debug, use the serial monitor to print the menuIndex and menuOffset. The display’s SPI bus can be shared with other devices if you use separate CS pins. But the display’s CS must be low during communication. The DC pin distinguishes data (high) from commands (low). The RES pin is active low; hold it high during normal operation. A 10µF capacitor between VCC and GND on the display module filters noise. Without it, the display may flicker during SPI bursts.

For a production device, consider the display’s temperature range: -40°C to +85°C. The OLED’s lifetime is 50,000 hours to half brightness. The glass substrate is 1.6mm thick, so handle it carefully. The connector is a 2.54mm pitch, 6-pin header. Use a ribbon cable with a locking connector to avoid disconnection. The display’s viewing angle is 160°, so it’s readable from the side. The contrast ratio is 2000:1, making it legible in direct sunlight if you set the brightness high.

In summary, displaying a menu on this display involves wiring, library setup, layout design, input handling, and performance tuning. The 256x64 resolution gives you 16,384 pixels to work with, which is enough for 4-8 menu items with icons. The SPI interface is fast enough for 30 Hz updates. The key is to manage the frame buffer in SRAM and use efficient drawing routines. With a rotary encoder and a state machine, you can build a responsive menu system. The display’s low power consumption (20mA) makes it suitable for portable devices. The physical dimensions (2.08 inches) fit in a handheld enclosure. The pixel density (132 DPI) ensures sharp text. All these factors make it a solid choice for embedded menus.

Next step

See how Yeinz cuts your deploy time in half — without ripping out your stack.

Book a 30-minute demo