To display a menu on a 72x40 OLED, you need to write a firmware that maps menu items to pixel coordinates, handle user input for navigation, and refresh the screen efficiently. This resolution is tiny—just 72 columns by 40 rows of pixels—so you can’t show much text or graphics at once. The key is to use a scrolling or paging system, where each “page” shows a few menu options, and you cycle through them with buttons or an encoder. For example, with a 0.42 inch 72x40 oled display, you typically use an I2C interface (address 0x3C or 0x3D) and a driver like SSD1306 or SH1106. The display’s RAM is 72x40 bits, so you’re working with 360 bytes total. You can fit about 4 to 5 lines of 6x8 pixel font characters, or 2 to 3 lines of 8x16 pixel font. For a menu, you’d allocate a buffer in microcontroller RAM (e.g., 360 bytes for a full frame), draw menu text using a bitmap font, and update only changed regions to save CPU cycles. The refresh rate can hit 30-60 Hz depending on the I2C clock speed (typically 400 kHz for fast mode).
Let’s break down the hardware side first. The 72x40 OLED panel is usually monochrome, with a pixel pitch around 0.15 mm, giving a visible area of about 10.8 mm by 6 mm. It uses a CMOS driver IC that includes a charge pump for the OLED voltage (typically 7-15 V). The I2C interface requires just two wires (SDA and SCL) plus power (3.3 V or 5 V) and ground. Current consumption is around 10-20 mA with all pixels on, but for a menu with mostly black background, it drops to 5-10 mA. The display’s contrast is controlled by a register (0x81) with values from 0 to 255, and you can set the segment remap (0xA0 vs 0xA1) to flip the horizontal orientation. The multiplex ratio is fixed at 40 rows, so the COM pins drive each row sequentially. In practice, you’ll initialize the display with commands like: 0xAE (display off), 0xD5 (set display clock divide ratio/oscillator frequency), 0x80 (default), 0xA8 (set multiplex ratio), 0x27 (40 rows), 0xD3 (set display offset), 0x00, 0x40 (set start line), 0x8D (charge pump setting), 0x14 (enable), 0x20 (set memory addressing mode), 0x00 (horizontal), 0xA1 (segment remap), 0xC8 (COM output scan direction), 0xDA (set COM pins hardware configuration), 0x12, 0x81 (set contrast), 0xCF (value), 0xD9 (set pre-charge period), 0xF1, 0xDB (set VCOMH deselect level), 0x40, 0xA4 (display on resume), 0xA6 (normal display), 0xAF (display on).
Now, for the menu software architecture. You need a state machine that tracks the current menu level, selected item, and scroll position. For a 72x40 OLED, you can’t show more than 4-5 lines of small text, so a typical menu might have 3 items per page, with a scroll indicator (like an arrow) at the bottom. The font choice is critical: a 5x7 pixel font (like the one from Adafruit GFX) uses 5 bytes per character, plus 1 byte for spacing. For a 6x8 font, each character is 6 pixels wide, so you can fit 12 characters per line (72/6 = 12). With 40 rows, you get 5 lines of 8-pixel height, but you need 1-2 pixels of spacing between lines, so 4 lines is more realistic. That means you can show 4 menu items at a time, each with a 12-character label. For example, “1. Start” and “2. Settings” would fit, but “3. Advanced Options” would be truncated. You can use a scrolling text feature for long labels, but that complicates the display update. A simpler approach is to use abbreviations or icons. For icons, you can store 8x8 pixel bitmaps in flash memory—each icon takes 8 bytes, and you can fit 9 icons per row (72/8 = 9). But for a menu, you’d typically use a combination of text and a small cursor symbol (like a triangle or arrow) that points to the selected item.
User input handling is the next piece. Most 72x40 OLED modules come with a breakout board that has four pins (VCC, GND, SDA, SCL), but no buttons. You’ll need to add external buttons—typically a rotary encoder with a push button, or three tactile switches (up, down, select). The encoder gives you two quadrature signals (A and B) plus a switch. You can debounce them with a 10 ms delay in software, or use hardware debouncing with RC filters (10 kΩ resistor and 0.1 µF capacitor). For a simple menu, you’d map the encoder rotation to cursor movement, and the push button to select. Each step of the encoder increments or decrements a menu index, which wraps around the total items. The display update should happen only when the index changes, to avoid flickering. You can use a dirty flag: set it when input is detected, then in the main loop, check the flag and redraw the menu. The redraw can be optimized by only updating the changed line—for example, if the cursor moves from line 0 to line 1, you only need to clear the old cursor line and draw the new one. This reduces I2C traffic and improves responsiveness.
Let’s talk about the buffer management. The SSD1306 driver supports three memory addressing modes: horizontal, vertical, and page. For a 72x40 display, the page mode is most common: the display is divided into 5 pages of 8 rows each (40/8 = 5). Each page is 72 columns wide. When you send data, you set the page start address (0xB0 to 0xB4 for pages 0-4) and the column start and end addresses (0x21 and 0x22 commands). You can send a full frame by writing 72 bytes per page, 5 pages, total 360 bytes. But if you’re only updating a small portion, you can set the column and page bounds to just the area that changed. For example, if the cursor moves from (0,0) to (0,10), you only need to update a 72x8 pixel area (one page). That’s 72 bytes, which takes about 1.8 ms at 400 kHz I2C (72 bytes * 9 bits per byte / 400 kHz = 1.62 ms, plus overhead). For a full frame update, it’s 9 ms, which is fine for a 30 Hz refresh rate (33 ms per frame). But if you’re doing animations or scrolling, you might need to optimize further by using the display’s hardware scrolling feature (0x2A command) or by using a double buffer in RAM.
Double buffering is a common technique: you allocate a 360-byte buffer in the microcontroller’s RAM, draw all changes to that buffer, then send the entire buffer to the display. This avoids partial updates and ensures a clean image. For a microcontroller like an Arduino Uno (2 KB RAM), 360 bytes is a significant chunk (17.6% of total RAM), but it’s doable. For a more powerful chip like an ESP32 (520 KB RAM), it’s trivial. The buffer can be a 2D array: uint8_t buffer[5][72] for pages and columns. You write to this buffer using functions like drawPixel(), drawChar(), and drawBitmap(). Then you call a function that sends the buffer to the display via I2C. The drawChar() function for a 6x8 font would look up the glyph in a font table (each character is 6 bytes, one per column), and write the byte to the appropriate row in the buffer. For example, the character ‘A’ might be stored as {0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00} for a 6x8 font. You’d OR this into the buffer at the correct position.
Menu data structures matter. You can define a menu item as a struct with a label (string), a pointer to a submenu or a function, and a type (e.g., action, submenu, toggle). For a simple menu, you might have an array of these structs. For example:
struct MenuItem {
const char* label;
void (*action)(void);
uint8_t type;
};
Then you define a root menu array: MenuItem rootMenu[] = {{"Start", startFunc, 0}, {"Settings", NULL, 1}, {"About", aboutFunc, 0}, {"", NULL, 0}}; (the last one is a sentinel). The menu renderer iterates through this array, draws the label for each item, and highlights the selected one. For submenus, you push a new menu array onto a stack, and the renderer switches to that. The stack depth is typically 3-4 levels, but with a 72x40 OLED, you’ll want to keep it shallow because screen real estate is limited.
Performance considerations: The I2C bus speed is a bottleneck. At 400 kHz, you can send about 50,000 bytes per second. For a full frame update of 360 bytes, that’s 138 updates per second theoretically, but in practice, the microcontroller’s overhead (function calls, buffer manipulation) reduces it to 30-60 Hz. If you’re using a slower I2C speed (100 kHz), the max frame rate drops to 34 Hz. To improve, you can use the display’s “page write” mode to send multiple pages in one transaction, or use DMA on microcontrollers that support it (like STM32). Another trick is to reduce the number of colors: since it’s monochrome, you can use a 1-bit per pixel buffer, but that’s already the case. You can also use the display’s “contrast” control to simulate grayscale, but that’s not standard for menus.
Power consumption is another angle. The OLED display draws about 10-20 mA, but if you’re battery-powered, you can put it to sleep (0xAE command) between menu interactions. The sleep current is under 1 µA. You can wake it up on a button press interrupt. For a menu system, you’d set a timeout: after 10 seconds of no input, send the display off command. When the user presses a button, the interrupt wakes the MCU, reinitializes the display (which takes about 50 ms), and shows the last menu state. You can store the menu state in non-volatile memory (EEPROM) to persist across power cycles, but that’s optional.
Let’s look at a real-world example: a 72x40 OLED used in a handheld device like a multimeter or a thermostat. The menu might show “Temp”, “Humidity”, “Settings”, “Back”. Each item is 6-8 characters long. The selected item is indicated by a right-pointing arrow (>) at the start of the line. The arrow is drawn as a 5x7 bitmap: {0x08, 0x0C, 0x0E, 0x0C, 0x08} for a right arrow. When the user presses the up/down button, the arrow moves to the next item. The display updates only the two lines that changed: the old line (clear the arrow) and the new line (draw the arrow). This minimizes I2C traffic and gives a snappy feel. For a rotary encoder, you can add acceleration: if the encoder spins fast, you skip multiple menu items per tick. This is done by measuring the time between encoder interrupts and using a lookup table for the step size.
Error handling is important. The I2C bus can have errors (e.g., NACK from the display). You should implement a retry mechanism: if the display doesn’t acknowledge, try again after 1 ms, up to 3 times. If it fails, set a flag and show an error message on the menu (e.g., “Display Error”). Also, the display’s RAM is volatile, so if the power glitches, you might see garbage. You can store a checksum of the buffer in the last byte, and on each update, verify it. If it’s wrong, reinitialize the display and redraw the menu.
For the font rendering, you can use a proportional font (like 5x7) or a fixed-width font (like 6x8). Fixed-width is easier for menu alignment because each character takes the same horizontal space. With a 6x8 font, 12 characters fit per line. For a 5x7 font, you can fit 14 characters per line (72/5 = 14.4, but you need spacing, so 13-14). But 5x7 fonts are harder to read on a small display. A good compromise is to use a 6x8 font for menu labels and a 8x16 font for titles or values. The 8x16 font takes 16 bytes per character, so you can only show 9 characters per line (72/8 = 9), and 2 lines (40/16 = 2.5, but with spacing, 2 lines). This is useful for a status screen that shows a large number, like “25.4°C”.
I2C address conflicts can occur if you have multiple devices on the same bus. The 72x40 OLED typically uses address 0x3C (write) or 0x3D (read). You can change it by soldering a resistor on the module, but most modules are fixed. If you have another I2C device at 0x3C, you’ll need to use an I2C multiplexer (like TCA9548A) or change the display’s address by modifying the driver IC’s configuration (if supported). The SSD1306 supports a second address (0x3D) by changing the SA0 pin level, but on a 72x40 module, this pin is often not broken out.
For the menu layout, you can use a tree structure. For example:
Main Menu
├── Start
├── Settings
│ ├── WiFi
│ ├── Display
│ └── Audio
└── About
On the 72x40 OLED, you’d show the current level. For the main menu, you show “Start”, “Settings”, “About”. When the user selects “Settings”, you push the settings submenu and show “WiFi”, “Display”, “Audio”, “Back”. The “Back” item returns to the previous menu. This is a standard pattern. You can also add a breadcrumb at the top: “Main > Settings” in a small font (6x8), but that takes up one line, leaving only 3 lines for menu items. Alternatively, you can use a status bar at the bottom with the current level name.
Animations can be done with the display’s hardware scrolling. The SSD1306 supports horizontal scrolling (0x26 command) and vertical scrolling (0x29). You can use this to scroll a long menu item horizontally if it’s too long to fit. For example, if a menu item is “Advanced Network Configuration”, you can scroll it left to right. But this is tricky because the scrolling affects the entire display area, not just one line. You’d need to use a separate buffer for the scrolling line and composite it with the rest of the menu. This is advanced and rarely needed for a 72x40 OLED because the screen is small and users expect simple interactions.
Testing and debugging: Use a logic analyzer to capture I2C traffic. You can see the commands and data being sent. Common issues include wrong initialization sequence, incorrect contrast settings (too dim or too bright), and buffer overflow. The 72x40 OLED has a maximum column address of 71 (0x00 to 0x47) and page address of 4 (0xB0 to 0xB4). If you write beyond these, the data wraps around or is ignored. Also, the display’s RAM is organized in pages, so if you’re in horizontal addressing mode, writing to column 72 will wrap to the next page. This can cause weird artifacts. Stick to page addressing mode for simplicity.
For a production-ready menu, you’ll also need to handle internationalization (i18n). The 6x8 font can support ASCII characters only. For non-ASCII, you need a custom font with Unicode support, which takes more flash memory. For a 72x40 OLED, you’re better off sticking to English or using icons. Icons can be stored as 8x8 bitmaps, and you can have a set of 16-32 icons for common actions (e.g., gear for settings, house for home, arrow for back). Each icon takes 8 bytes, so 32 icons take 256 bytes of flash, which is negligible.
Finally, the choice of microcontroller matters. For an Arduino Uno (ATmega328P), you have 32 KB flash and 2 KB RAM. The font tables (e.g., 96 characters of 6x8 font) take 576 bytes. The menu data structures take maybe 100 bytes. The buffer takes 360 bytes. That leaves about 964 bytes for code and stack. It’s tight but workable. For an ESP32, you have 4 MB flash and 520 KB RAM, so you can use a full GUI library like LVGL (Light and Versatile Graphics Library) that supports OLEDs. LVGL has a built-in menu widget that handles scrolling, animations, and touch