{"id":4711,"date":"2026-09-04T08:39:48","date_gmt":"2026-09-04T08:39:48","guid":{"rendered":"https:\/\/blog.embeddedexpert.io\/?p=4711"},"modified":"2026-09-04T08:40:36","modified_gmt":"2026-09-04T08:40:36","slug":"no-flicker-no-mess-building-a-3-button-oled-menu-with-u8g2","status":"publish","type":"post","link":"https:\/\/blog.embeddedexpert.io\/?p=4711","title":{"rendered":"No Flicker, No Mess: Building a 3-Button OLED Menu with U8g2"},"content":{"rendered":"<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-1024x683.png\" alt=\"\" class=\"wp-image-4712\" srcset=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-1024x683.png 1024w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-300x200.png 300w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-768x512.png 768w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-1150x767.png 1150w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-750x500.png 750w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-400x267.png 400w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM-250x167.png 250w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-3-2026-at-05_16_35-PM.png 1536w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure><\/div>\n\n\n<p>A responsive menu system is the backbone of any interactive embedded device, but navigating it without screen tearing requires careful state management. In this guide, you will learn how to build a sleek, zero-flicker OLED menu using U8g2 that is driven by simple Up, Down, and OK push-buttons.<\/p>\n\n\n\n<p>In this guide, we shall cover the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Introduction.<\/li>\n\n\n\n<li>STM32CubeMX configuration.<\/li>\n\n\n\n<li>Firmware development.<\/li>\n\n\n\n<li>Results.<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Introduction:<\/h2>\n\n\n\n<p>In the modern era of embedded systems, it is easy to assume that every project requires a capacitive-touch RGB TFT screen to provide a good user experience. We are surrounded by sleek smartphones and tablets that set a high bar for industrial design, and that expectation often bleeds into our microcontroller projects. However, reaching for a color touchscreen the moment you need to adjust a few settings often leads to unnecessary complications: inflated Bill of Materials (BOM) costs, complex GUI frameworks, steep learning curves, and fragile hardware that fails in harsh environments.<\/p>\n\n\n\n<p>The truth is, for the vast majority of embedded devices\u2014whether it\u2019s a 3D printer controller, a custom audio synthesizer, a benchtop power supply, or an industrial sensor node\u2014a classic monochrome OLED paired with a few physical push buttons remains the absolute gold standard for reliability, cost-effectiveness, and performance.<\/p>\n\n\n\n<p>But building a robust menu system from scratch is rarely as straightforward as it seems. Any engineer who has attempted it has likely run into the same notorious pitfalls: spaghetti-code state machines that become impossible to maintain, mechanical button bouncing that causes the cursor to jump three items at a time, and\u2014most frustrating of all\u2014dreadful screen flicker that makes the device feel cheap and poorly engineered. Sending data byte-by-byte directly to the display controller, or hammering it with refresh commands on every single loop iteration, is a guaranteed recipe for a tearing, unresponsive interface.<\/p>\n\n\n\n<p>In this guide, we are going to build a sleek, professional-grade menu system from the ground up using a standard 128&#215;64 OLED, the <strong>U8g2<\/strong> graphics library, and three simple push buttons: Up, Down, and OK.<\/p>\n\n\n\n<p>We will move past the basic &#8220;Hello World&#8221; text and dive deep into embedded UI architecture. You will learn how to structure your firmware using an event-driven state machine that cleanly separates hardware input from screen rendering. We will implement a &#8220;Redraw Flag&#8221; system, ensuring the screen <em>only<\/em> updates when an actual state change occurs, completely eliminating flicker and freeing up valuable CPU cycles. We will also tackle the practical challenges of navigating nested submenus, creating scrollable lists for menus that exceed the physical height of the screen, and handling mechanical button debouncing without relying on blocking delays.<\/p>\n\n\n\n<p>By the end of this guide, you will have a reusable, rock-solid menu framework that looks premium, responds instantly, and can be easily dropped into any future microcontroller project. Let\u2019s dive in and build an interface that just works.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. STM32CubeMX Configuration:<\/h2>\n\n\n\n<p>We shall continue from this guide <a href=\"https:\/\/blog.embeddedexpert.io\/?p=4679\" data-type=\"link\" data-id=\"https:\/\/blog.embeddedexpert.io\/?p=4679\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<p>Open the project .ioc file in STM32CubeMX.<\/p>\n\n\n\n<p>Use any three GPIO as input and give them the following name:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Up<\/li>\n\n\n\n<li>Down<\/li>\n\n\n\n<li>Ok.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"662\" src=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-1024x662.jpg\" alt=\"\" class=\"wp-image-4713\" srcset=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-1024x662.jpg 1024w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-300x194.jpg 300w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-768x497.jpg 768w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-1536x993.jpg 1536w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-2048x1324.jpg 2048w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-1150x744.jpg 1150w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-750x485.jpg 750w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-400x259.jpg 400w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-13-07-250x162.jpg 250w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Next, from GPIO in System Core, enable the internal pullup for the select GPIO as follows:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"662\" src=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-1024x662.jpg\" alt=\"\" class=\"wp-image-4714\" srcset=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-1024x662.jpg 1024w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-300x194.jpg 300w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-768x497.jpg 768w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-1536x993.jpg 1536w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-2048x1324.jpg 2048w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-1150x744.jpg 1150w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-750x485.jpg 750w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-400x259.jpg 400w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/2026-09-04_11-15-27-250x162.jpg 250w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Finally click on Generate Code.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Thats all for the STM32CubeMX configuration.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Firmware Development:<\/h2>\n\n\n\n<p>Before heading into the development, the flow of the code as follows:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"657\" height=\"1024\" src=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image-657x1024.png\" alt=\"\" class=\"wp-image-4715\" srcset=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image-657x1024.png 657w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image-192x300.png 192w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image-768x1197.png 768w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image-750x1169.png 750w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image-400x624.png 400w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image-250x390.png 250w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2026\/09\/exported_image.png 982w\" sizes=\"(max-width: 657px) 100vw, 657px\" \/><\/figure><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Open the project in STM32CubeIDE:<\/p>\n\n\n\n<p>In user code begin code 0, declare the following enumeration:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">typedef enum {\n    MENU_ACTION_NONE,\n    MENU_ACTION_OPEN_SUBMENU,\n    MENU_ACTION_BACK,\n    MENU_ACTION_TOGGLE,\n    MENU_ACTION_ACTION_1,\n    MENU_ACTION_ACTION_2\n} MenuAction_t;<\/pre><\/div>\n\n\n\n<p>This enum defines the specific behaviors a menu item can trigger when its corresponding button is pressed. It acts as a clean, readable command identifier, allowing the menu&#8217;s logic to easily route user inputs to actions like opening submenus, toggling settings, or executing custom code.<\/p>\n\n\n\n<p>Next, declare the following structures:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">typedef struct {\n    const char* name;\n    MenuAction_t action;\n    uint8_t* value_ptr;\n} MenuItem_t;<\/pre><\/div>\n\n\n\n<p>This struct acts as the blueprint for an individual menu row, storing its display text, the action it triggers when selected, and a pointer to its state variable. The <code>value_ptr<\/code> is especially useful for toggle switches, allowing the code to dynamically read and flip boolean variables (like an LED state) without hardcoding them into the logic.<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">typedef struct {\n    const char* title;\n    const MenuItem_t* items;\n    uint8_t item_count;\n} MenuScreen_t;<\/pre><\/div>\n\n\n\n<p>This struct defines a complete menu page by grouping its header title with an array of menu items and a count of how many items it contains. It allows you to easily build a multi-level menu system by defining separate screens (like a main menu and a settings page) that the code can navigate between.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Next, declare the following constants:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">const MenuItem_t sub2_items[] = {\n    {&quot;Option A&quot;, MENU_ACTION_NONE, NULL},\n    {&quot;Option B&quot;, MENU_ACTION_NONE, NULL},\n    {&quot;Option C&quot;, MENU_ACTION_NONE, NULL},\n    {&quot;Back&quot;,    MENU_ACTION_BACK,  NULL}\n};<\/pre><\/div>\n\n\n\n<p>This array defines the individual rows for a submenu, specifying their display text and associated actions. The &#8220;Back&#8221; item is explicitly mapped to trigger a return to the previous screen, while the other options are currently configured as inactive placeholders.<\/p>\n\n\n\n<p>Next, declare the two static variables:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">static uint8_t setting_led_state = 1;\nstatic uint8_t setting_buzz_state = 0;<\/pre><\/div>\n\n\n\n<p>These static variables store the live boolean states for the LED and buzzer settings, keeping them persistent in memory. By linking them to a menu item&#8217;s <code>value_ptr<\/code>, the UI can directly toggle and display their status without needing custom read\/write functions for each setting.<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">const MenuItem_t sub1_items[] = {\n    {&quot;LED Enable&quot;,  MENU_ACTION_TOGGLE, &amp;setting_led_state},\n    {&quot;Buzzer Enbl&quot;, MENU_ACTION_TOGGLE, &amp;setting_buzz_state},\n    {&quot;Sub 2&quot;,       MENU_ACTION_OPEN_SUBMENU, NULL},\n    {&quot;Back&quot;,        MENU_ACTION_BACK,  NULL}\n};\n\nconst MenuItem_t main_items[] = {\n    {&quot;Start System&quot;, MENU_ACTION_ACTION_1, NULL},\n    {&quot;View Data&quot;,    MENU_ACTION_ACTION_2, NULL},\n    {&quot;Settings&quot;,     MENU_ACTION_OPEN_SUBMENU, NULL},\n    {&quot;Reboot&quot;,       MENU_ACTION_NONE, NULL}\n};\n\nconst MenuScreen_t screens[] = {\n    {&quot;MAIN MENU&quot;, main_items, 4},\n    {&quot;SETTINGS&quot;,  sub1_items, 4},\n    {&quot;SUB MENU 2&quot;,sub2_items, 4}\n};<\/pre><\/div>\n\n\n\n<p>These arrays populate the screens with their specific items, mapping actions like toggling LEDs or opening submenus directly to the state variables and navigation logic. The <code>screens<\/code> array then compiles these individual pages into a single, easily indexable master list that the navigation engine uses to track the active UI layout.<\/p>\n\n\n\n<p>Next, declare the following variables:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">#define MAX_SCREENS_DEPTH 3\nuint8_t screen_stack[MAX_SCREENS_DEPTH] = {0, 0, 0};\nuint8_t screen_stack_idx = 0;\nuint8_t selected_item = 0;<\/pre><\/div>\n\n\n\n<p>These variables track the navigation hierarchy and user cursor, using <code>screen_stack<\/code> as a LIFO array to remember the path through nested menus. The <code>screen_stack_idx<\/code> keeps track of the current depth, while <code>selected_item<\/code> tracks the highlighted row on the active screen.<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">#define MENU_FONT      u8g2_font_6x10_tr\n#define MENU_FONT_REG  u8g2_font_6x10_tr\n#define ITEM_HEIGHT    12\n#define MAX_VISIBLE    4<\/pre><\/div>\n\n\n\n<p>These macros configure the UI&#8217;s visual layout by defining the specific U8g2 fonts used for text and the vertical pixel height of each row. The <code>MAX_VISIBLE<\/code> constant dictates exactly how many items fit on the screen at once before the scrolling logic activates.<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">volatile uint8_t ui_needs_update = 1;<\/pre><\/div>\n\n\n\n<p>This flag variable acts as the heart of the flicker-free rendering engine, ensuring the OLED is only redrawn when a state change actually occurs. It initializes to <code>1<\/code> so the main menu is drawn immediately on startup, and is later toggled by button inputs to trigger a screen refresh only when needed.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Next, the function to display the menu:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">void render_menu_ui(void) {\n    uint8_t current_screen_idx = screen_stack[screen_stack_idx];\n    const MenuScreen_t* current_screen = &amp;screens[current_screen_idx];\n\n    u8g2_ClearBuffer(&amp;myDisplay);\n\n    \/\/ 1. Draw Title Bar\n    u8g2_SetFont(&amp;myDisplay, MENU_FONT);\n    u8g2_DrawBox(&amp;myDisplay, 0, 0, 128, 12);\n    u8g2_SetDrawColor(&amp;myDisplay, 0);\n    u8g2_DrawStr(&amp;myDisplay, 2, 10, current_screen-&gt;title);\n    u8g2_SetDrawColor(&amp;myDisplay, 1);\n\n    \/\/ 2. Draw Scroll Indicator\n    if (current_screen-&gt;item_count &gt; MAX_VISIBLE) {\n        uint8_t scroll_track_h = MAX_VISIBLE * ITEM_HEIGHT;\n        uint8_t scroll_thumb_h = (scroll_track_h * MAX_VISIBLE) \/ current_screen-&gt;item_count;\n        uint8_t scroll_thumb_y = 13 + ((selected_item * scroll_track_h) \/ current_screen-&gt;item_count);\n        u8g2_DrawVLine(&amp;myDisplay, 126, 13, scroll_track_h);\n        u8g2_DrawBox(&amp;myDisplay, 125, scroll_thumb_y, 3, scroll_thumb_h);\n    }\n\n    \/\/ 3. Draw Menu Items\n    uint8_t start_idx = 0;\n    if (current_screen-&gt;item_count &gt; MAX_VISIBLE &amp;&amp; selected_item &gt;= MAX_VISIBLE - 1) {\n        start_idx = selected_item - (MAX_VISIBLE - 2);\n        if (start_idx + MAX_VISIBLE &gt; current_screen-&gt;item_count) {\n            start_idx = current_screen-&gt;item_count - MAX_VISIBLE;\n        }\n    }\n\n    u8g2_SetFont(&amp;myDisplay, MENU_FONT_REG);\n    for (uint8_t i = 0; i &lt; MAX_VISIBLE &amp;&amp; (start_idx + i) &lt; current_screen-&gt;item_count; i++) {\n        uint8_t item_idx = start_idx + i;\n        uint8_t y_pos = 14 + (i * ITEM_HEIGHT);\n\n        if (item_idx == selected_item) {\n            u8g2_DrawBox(&amp;myDisplay, 0, y_pos - 1, 124, ITEM_HEIGHT);\n            u8g2_SetDrawColor(&amp;myDisplay, 0);\n        } else {\n            u8g2_SetDrawColor(&amp;myDisplay, 1);\n        }\n\n        u8g2_DrawStr(&amp;myDisplay, 2, y_pos + 7, current_screen-&gt;items[item_idx].name);\n\n        if (current_screen-&gt;items[item_idx].action == MENU_ACTION_TOGGLE) {\n            if (current_screen-&gt;items[item_idx].value_ptr != NULL) {\n                uint8_t val = *(current_screen-&gt;items[item_idx].value_ptr);\n                const char* state_str = val ? &quot;[ON]&quot; : &quot;[OFF]&quot;;\n                u8g2_DrawStr(&amp;myDisplay, 95, y_pos + 7, state_str);\n            }\n        }\n        u8g2_SetDrawColor(&amp;myDisplay, 1);\n    }\n    u8g2_SendBuffer(&amp;myDisplay);\n}<\/pre><\/div>\n\n\n\n<p>The <code>render_menu_ui<\/code> function is the graphical engine of the menu, translating the abstract state variables into actual pixels on the OLED. Here is a text-based breakdown of how it operates in four distinct stages:<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">1. Screen Lookup and Buffer Clearing<\/h4>\n\n\n\n<p>The function begins by looking up the current active screen using the <code>screen_stack<\/code> and <code>screen_stack_idx<\/code> variables. Once it has a pointer to the current <code>MenuScreen_t<\/code> struct, it clears the U8g2 RAM buffer to ensure a blank canvas.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">2. Drawing the Inverse Title Bar<\/h4>\n\n\n\n<p>To create a modern, high-contrast header, it draws a solid white box across the top of the screen. It then sets the draw color to <code>0<\/code> (inverse\/black) and prints the screen&#8217;s title text inside that box, creating a white-on-black header. Finally, it resets the draw color back to <code>1<\/code> (white) for the rest of the drawing operations.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">3. Dynamic Scrollbar Logic<\/h4>\n\n\n\n<p>If the current screen has more items than can physically fit on the OLED (<code>item_count &gt; MAX_VISIBLE<\/code>), the scrolling engine activates.<br>It calculates the total height of the visible track and the height of the &#8220;thumb&#8221; (the movable scroll indicator) proportional to the total number of items. The vertical position of the thumb is calculated based on the <code>selected_item<\/code> index, giving the user an immediate visual cue of where they are in the list.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">4. Windowed Item Rendering and Toggles<\/h4>\n\n\n\n<p>Because only <code>MAX_VISIBLE<\/code> items can fit on screen, the code calculates a <code>start_idx<\/code> to create a sliding &#8220;window&#8221; of items. If the user navigates past the fourth item, the window shifts down so the highlighted item stays on screen.<br>It then loops through the visible items:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Highlighting:<\/strong> If the current item in the loop matches <code>selected_item<\/code>, it draws a solid white box and switches the draw color to <code>0<\/code> so the text appears inverted (black on white).<\/li>\n\n\n\n<li><strong>Toggle States:<\/strong> If the item&#8217;s action is <code>MENU_ACTION_TOGGLE<\/code>, it dereferences the <code>value_ptr<\/code> to check the live boolean state of that variable. It then prints <code>[ON]<\/code> or <code>[OFF]<\/code> at the right edge of that row.<\/li>\n<\/ul>\n\n\n\n<p>Once all lines are drawn into the RAM buffer, <code>u8g2_SendBuffer<\/code> pushes the entire completed frame to the OLED in one shot, preventing any screen tearing or flickering.<\/p>\n\n\n\n<p>Next, input handling logic:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">void menu_handle_up(void) {\n    uint8_t current_screen_idx = screen_stack[screen_stack_idx];\n    uint8_t item_count = screens[current_screen_idx].item_count;\n    if (selected_item &gt; 0) selected_item--;\n    else selected_item = item_count - 1; \/\/ Wrap around\n    ui_needs_update = 1;\n}\n\nvoid menu_handle_down(void) {\n    uint8_t current_screen_idx = screen_stack[screen_stack_idx];\n    uint8_t item_count = screens[current_screen_idx].item_count;\n    if (selected_item &lt; item_count - 1) selected_item++;\n    else selected_item = 0; \/\/ Wrap around\n    ui_needs_update = 1;\n}\n\nvoid menu_handle_ok(void) {\n    uint8_t current_screen_idx = screen_stack[screen_stack_idx];\n    const MenuItem_t* selected_menu_item = &amp;screens[current_screen_idx].items[selected_item];\n\n    switch (selected_menu_item-&gt;action) {\n        case MENU_ACTION_OPEN_SUBMENU:\n            if (screen_stack_idx &lt; MAX_SCREENS_DEPTH - 1) {\n                screen_stack_idx++;\n                if (current_screen_idx == 0 &amp;&amp; selected_item == 2) screen_stack[screen_stack_idx] = 1;\n                else if (current_screen_idx == 1 &amp;&amp; selected_item == 2) screen_stack[screen_stack_idx] = 2;\n                selected_item = 0;\n            }\n            break;\n        case MENU_ACTION_BACK:\n            if (screen_stack_idx &gt; 0) {\n                screen_stack_idx--;\n                selected_item = 0;\n            }\n            break;\n        case MENU_ACTION_TOGGLE:\n            if (selected_menu_item-&gt;value_ptr != NULL) {\n                *(selected_menu_item-&gt;value_ptr) = !(*(selected_menu_item-&gt;value_ptr));\n            }\n            break;\n        case MENU_ACTION_ACTION_1:\n            \/\/ Do something (Start System)\n            break;\n        case MENU_ACTION_ACTION_2:\n            \/\/ Do something (View Data)\n            break;\n        default:\n            break;\n    }\n    ui_needs_update = 1;\n}\n<\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\">1. <code>menu_handle_up<\/code> and <code>menu_handle_down<\/code> (Cursor Navigation)<\/h4>\n\n\n\n<p>These two functions are strictly responsible for moving the user&#8217;s cursor.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Screen Lookup:<\/strong> They first determine which screen is currently active by reading the <code>screen_stack<\/code> array. This tells the code how many total items are in the current list.<\/li>\n\n\n\n<li><strong>Boundary Wrapping:<\/strong> When moving up, if the user is already at the top (<code>selected_item == 0<\/code>), the code wraps the cursor to the very bottom of the list. When moving down, if they are at the bottom, it wraps back to the top (<code>0<\/code>). This allows rapid cycling through the menu without dead ends.<\/li>\n\n\n\n<li><strong>Triggering Redraw:<\/strong> After adjusting the <code>selected_item<\/code> index, the function sets <code>ui_needs_update = 1<\/code>, telling the main loop to redraw the screen with the new highlight position.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">2. <code>menu_handle_ok<\/code> (The Action Dispatcher)<\/h4>\n\n\n\n<p>When the OK button is pressed, the code doesn&#8217;t just blindly execute code; it looks up the <code>action<\/code> enum assigned to the currently highlighted <code>MenuItem_t<\/code> and uses a <code>switch<\/code> statement to route the behavior.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>MENU_ACTION_OPEN_SUBMENU<\/code> (Navigating Deeper):<\/strong><br>To enter a submenu, the code pushes a new screen onto the navigation stack. It increments <code>screen_stack_idx<\/code> and hardcodes the mapping of which screen to go to next (e.g., if you are on Screen 0 and press OK on item 2, it loads Screen 1). It also resets <code>selected_item<\/code> to <code>0<\/code> so the cursor starts at the top of the new page.<\/li>\n\n\n\n<li><strong><code>MENU_ACTION_BACK<\/code> (Navigating Backward):<\/strong><br>To go back, the code pops the stack by decrementing <code>screen_stack_idx<\/code>. It also resets <code>selected_item<\/code> to <code>0<\/code>, though a more advanced implementation could remember the previous cursor position.<\/li>\n\n\n\n<li><strong><code>MENU_ACTION_TOGGLE<\/code> (Dynamic State Flipping):<\/strong><br>This is where the <code>value_ptr<\/code> in the <code>MenuItem_t<\/code> struct shines. Instead of writing custom <code>if\/else<\/code> logic for every single toggle in your app, the code checks if the pointer is valid, dereferences it, and flips the boolean value (<code>!<\/code>). Because the rendering engine also reads this pointer, the screen instantly updates to show <code>[ON]<\/code> or <code>[OFF]<\/code> without any extra code.<\/li>\n\n\n\n<li><strong>Custom Actions (<code>ACTION_1<\/code>, <code>ACTION_2<\/code>):<\/strong><br>These cases are placeholders for executing actual firmware tasks, like starting a motor, reading a sensor, or sending data over UART.<\/li>\n\n\n\n<li><strong>Triggering Redraw:<\/strong><br>Regardless of which action was executed, the function concludes by setting <code>ui_needs_update = 1<\/code>. This ensures that if a toggle changed, or a new screen was loaded, the OLED instantly reflects the new state.<\/li>\n<\/ul>\n\n\n\n<p>Next, in user  code begin 2 in main function, render the UI:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">render_menu_ui();<\/pre><\/div>\n\n\n\n<p>Next, in while 1 loop:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-csrc&quot;,&quot;theme&quot;:&quot;dracula&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C&quot;,&quot;language&quot;:&quot;C&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;c&quot;}\">static uint32_t last_btn_tick = 0;\nuint32_t now = HAL_GetTick();\n\nif (now - last_btn_tick &gt; 200) {\n\n  if (HAL_GPIO_ReadPin(Up_GPIO_Port, Up_Pin) == GPIO_PIN_RESET) {\n    menu_handle_up();\n    last_btn_tick = now;\n  }\n  else if (HAL_GPIO_ReadPin(Down_GPIO_Port, Down_Pin) == GPIO_PIN_RESET) {\n    menu_handle_down();\n    last_btn_tick = now;\n  }\n  else if (HAL_GPIO_ReadPin(Ok_GPIO_Port, Ok_Pin) == GPIO_PIN_RESET) {\n    menu_handle_ok();\n    last_btn_tick = now;\n  }\n}\n\n\/\/ 2. State Machine for UI Redraw\n\/\/ Only draw if a button was pressed\nif (ui_needs_update) {\n  render_menu_ui();\n  ui_needs_update = 0; \/\/ Clear the flag\n}<\/pre><\/div>\n\n\n\n<p>This snippet from the main loop handles non-blocking button polling and flicker-free rendering. It uses a 200-millisecond timestamp check to debounce the physical buttons, ensuring a single press doesn&#8217;t trigger multiple menu jumps. After processing inputs, it checks the <code>ui_needs_update<\/code> flag, calling the heavy <code>render_menu_ui<\/code> function only when a state change actually occurred, and immediately clears the flag to keep the CPU idle and the screen flicker-free.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Thats all for the firmware.<\/p>\n\n\n\n<p>Save the project and run it on your MCU as follows:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"34\" src=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-1024x34.jpg\" alt=\"\" class=\"wp-image-3598\" srcset=\"https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-1024x34.jpg 1024w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-300x10.jpg 300w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-768x25.jpg 768w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-1536x51.jpg 1536w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-2048x68.jpg 2048w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-1150x38.jpg 1150w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-750x25.jpg 750w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-400x13.jpg 400w, https:\/\/blog.embeddedexpert.io\/wp-content\/uploads\/2025\/07\/2025-07-03_17-48-35-250x8.jpg 250w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>You may download the project from&nbsp;<a href=\"https:\/\/github.com\/hussamaldean\/Embedded-Expert-Post-Projects\/tree\/main\/Projects\/U8G2_UI\">here<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Results:<\/h2>\n\n\n\n<p><\/p>\n\n\n\n<p>You should get this.<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"No Flicker, No Mess: Building a 3-Button OLED Menu with U8g2\" width=\"1170\" height=\"658\" src=\"https:\/\/www.youtube.com\/embed\/7WL5EV72TVc?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n<\/div><\/figure>\n\n\n\n<p>Happy coding \ud83d\ude09<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A responsive menu system is the backbone of any interactive embedded device, but navigating it without screen tearing requires careful state management. In this guide, you will learn how to build a sleek, zero-flicker OLED menu using U8g2 that is driven by simple Up, Down, and OK push-buttons. In this guide, we shall cover the [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,19,12],"tags":[],"class_list":["post-4711","post","type-post","status-publish","format-standard","hentry","category-embedded-systems","category-lcd","category-stm32"],"_links":{"self":[{"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=\/wp\/v2\/posts\/4711"}],"collection":[{"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4711"}],"version-history":[{"count":2,"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=\/wp\/v2\/posts\/4711\/revisions"}],"predecessor-version":[{"id":4718,"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=\/wp\/v2\/posts\/4711\/revisions\/4718"}],"wp:attachment":[{"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4711"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4711"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.embeddedexpert.io\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4711"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}