π Getting Started with MENU_LIB¶
This guide explains how to integrate MENU_LIB into a complete embedded application using:
- LCD_HD44780 β the display driver responsible for rendering text
- PUSHBUTTON_SWITCH_LIB β the input layer providing debounced pushbutton handling
In this example, we will build a small hierarchical menu displayed on a 20Γ4 character LCD and controlled by four pushbuttons (UP, DOWN, ENTER, ESC).
Step by step, you will connect the display, keypad, and menu logic into a single functional embedded UI layer.
A ready-to-build reference example is available in the examples/ directory.
For detailed hardware configuration, wiring, and library-specific setup, refer to the documentation of each library.
βοΈβ οΈ Assumptions β οΈβοΈ
This tutorial assumes that both LCD_HD44780 and PUSHBUTTON_SWITCH_LIB are already correctly
configured, initialized, and verified to work independently on your hardware.
The purpose of this guide is to show how to integrate MENU_LIB on top of these libraries to build a complete menu system.If you are not yet familiar with LCD_HD44780 or PUSHBUTTON_SWITCH_LIB, start by running those libraries first:
LCD_HD44780 Documentation
PUSHBUTTON_SWITCH_LIB DocumentationFor information about the hardware used in the examples, see Examples Overview.
π§± Project Structure Overview¶
The integration uses three main layers:
graph TD
A[PUSHBUTTON_SWITCH_LIB] -->|Input Events| B[MENU_LIB Core]
B -->|Render Commands| C[LCD_HD44780 Display Driver]
C -->|Text Output| D[LCD 20x4 Display]
The guide below is based on this example project structure:
getting_started_example_1/
βββ hw/
β βββ STM32G070RB/... # No IDE bare metal project HW configuration
β βββ STM32G070RB_HAL_LL/ # CubeMX project with HW configuration
β βββ Core/
β β βββ Inc/...
β β βββ Src/
β β β βββ main.c
β β β βββ lcd_hd44780_GPIO_interface.c
β β β βββ pushbutton_GPIO_interface.c
β β β βββ stm32g0xx_it.c # β updated in Step 2
β β β βββ ...
β β βββ Startup/..
β βββ Drivers/..
β βββ CMakeLists.txt
βββ lib/
β βββ MENU_LIB/
β β βββ CMakeLists.txt
β βββ LCD_HD44780/ # Configured and integrated library
β β βββ CMakeLists.txt
β β βββ lcd_hd44780.c
β β βββ lcd_hd44780.h
β β βββ lcd_hd44780_config.h
β β βββ lcd_hd44780_GPIO_interface.h
β βββ PUSHBUTTON_SWITCH_LIB/ # Configured and integrated library
β βββ CMakeLists.txt
β βββ pushbutton.c
β βββ pushbutton.h
β βββ pushbutton_GPIO_interface.h
βββ src/
βββ ui/
β βββ keypad.c # β created in Step 2
β βββ keypad.h # β created in Step 2
β βββ menu_handlers.c # β created in Step 3
β βββ menu_handlers.h # β created in Step 3
β βββ menu_screen_driver_interface.c # β created in Step 1
β βββ menu.c # β created in Step 3
β βββ menu.h # β created in Step 3
β βββ ui.c # β created in Step 4
β βββ ui.h # β created in Step 4
βββ main_app.c
You may keep the libraries anywhere in your project tree, but make sure they are included in the build system, either via:
CMakeLists.txt- a
Makefile, or - your IDE configuration (e.g., STM32CubeIDE)
Whenever you create new source files (for example, menu_screen_driver_interface.c), remember to add them to the build configuration so they are compiled and linked into the project.
ποΈ Step 1 β Integrate the Display Driver¶
After adding and configuring LCD_HD44780 (including its GPIO interface), create the file:
src/ui/menu_screen_driver_interface.c
In this file, include the LCD driver headers and connect them to the
menu_screen_driver_interface_struct. Then implement the accessor
get_menu_display_driver_interface() required by MENU_LIB.
#include "lcd_hd44780.h"
#include "lcd_hd44780_config.h"
#include "menu_screen_driver_interface.h"
// --- Callback declarations ---
static uint8_t get_screen_lines(void);
static uint8_t get_screen_columns(void);
// --- LCD driver interface definition ---
const struct menu_screen_driver_interface_struct display_driver = {
.screen_init = lcd_init,
.clr_scr = lcd_buf_cls,
.cursor_position = (void (*)(uint8_t, uint8_t))lcd_buf_locate,
.print_string = lcd_buf_str,
.print_char = lcd_buf_char,
.get_number_of_screen_lines = get_screen_lines,
.get_number_of_chars_per_line = get_screen_columns
};
// --- Callback definitions ---
static uint8_t get_screen_lines(void)
{
return LCD_Y;
}
static uint8_t get_screen_columns(void)
{
return LCD_X;
}
// --- Accessor function for MENU_LIB ---
const struct menu_screen_driver_interface_struct *get_menu_display_driver_interface(void)
{
return &display_driver;
}
π§ Explanation¶
LCD_XandLCD_Ycome fromlcd_hd44780_config.hbased on the selected LCD type.get_menu_display_driver_interface()provides MENU_LIB with a stable access point to the display driver.- This keeps the hardware abstraction clean and allows you to change the LCD driver later without modifying any menu logic.
Thatβs it for the display driver interface. Save the file and continue β the remaining UI modules will be initialized in later steps.
ποΈ Step 2 β Create the Keypad Layer (UI)¶
Create a thin keypad layer that maps PUSHBUTTON_SWITCH_LIB events to MENU_LIB navigation using the defined buttons.
This layer also allows you to customize keypad behavior for menu items that require their own input handling.
Add the following files:
src/ui/keypad.h and src/ui/keypad.c
keypad.h¶
#ifndef UI_KEYPAD_H_
#define UI_KEYPAD_H_
#ifdef __cplusplus
extern "C" {
#endif
typedef void(*key_action_cb_t)(void);
void keypad_init(void); // create buttons, register default (app) callbacks
void keypad_process(void); // poll buttons; call periodically in main loop
void update_keypad_debounce_timers(void); // update all keys debounce timers -> mandatory to update every 1 ms
// Bind custom handlers to keypad
void keypad_bind_conrtol_handlers( key_action_cb_t on_up, key_action_cb_t on_down, key_action_cb_t on_enter, key_action_cb_t on_esc);
// Keypad additional functionality for later usage
void enable_keypad_up_down_repetition(void);
void disable_keypad_up_down_repetition(void);
#ifdef __cplusplus
}
#endif
#endif /* UI_KEYPAD_H_ */
keypad.c¶
#include "keypad.h"
#include "pushbutton.h"
#include <stdio.h>
// Four navigation buttons
static PUSHBUTTON_TypDef btn_up;
static PUSHBUTTON_TypDef btn_down;
static PUSHBUTTON_TypDef btn_enter;
static PUSHBUTTON_TypDef btn_esc;
void keypad_init(void)
{
// All buttons: trigger on push, no repetition
init_pushbutton(&btn_up, REPETITION_OFF, TRIGGER_ON_PUSH, PB_up_driver_interface_get);
init_pushbutton(&btn_down, REPETITION_OFF, TRIGGER_ON_PUSH, PB_down_driver_interface_get);
init_pushbutton(&btn_enter, REPETITION_OFF, TRIGGER_ON_PUSH, PB_enter_driver_interface_get);
init_pushbutton(&btn_esc, REPETITION_OFF, TRIGGER_ON_PUSH, PB_esc_driver_interface_get);
}
void keypad_process(void)
{
// Poll debounced state machines; call from main loop
check_pushbutton(&btn_up);
check_pushbutton(&btn_down);
check_pushbutton(&btn_enter);
check_pushbutton(&btn_esc);
}
void update_keypad_debounce_timers(void)
{
dec_pushbutton_deb_rep_timer(&btn_up);
dec_pushbutton_deb_rep_timer(&btn_down);
dec_pushbutton_deb_rep_timer(&btn_enter);
dec_pushbutton_deb_rep_timer(&btn_esc);
}
void keypad_bind_conrtol_handlers( key_action_cb on_up, key_action_cb on_down, key_action_cb on_enter, key_action_cb on_esc)
{
register_button_push_callback(&btn_up, on_up);
register_button_push_callback(&btn_down, on_down);
register_button_push_callback(&btn_enter, on_enter);
register_button_push_callback(&btn_esc, on_esc);
}
void enable_keypad_up_down_repetition(void)
{
enable_pusbutton_repetition(&btn_up);
enable_pusbutton_repetition(&btn_down);
}
void disable_keypad_up_down_repetition(void)
{
disable_pusbutton_repetition(&btn_up);
disable_pusbutton_repetition(&btn_down);
}
Trigger update_keypad_debounce_timers() every 1 ms (MANDATORY)¶
For example, use the SysTick timer (assuming it is enabled):
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
update_keypad_debounce_timers();
}
- This assumes that
pushbutton_GPIO_interface.cprovidesPB_get_driver_interface(). - Ensure that
keypad.cis added to your build system (CMake/Makefile/IDE). update_keypad_debounce_timers()updates all keypad debounce timers and must be called every 1 ms according to the PUSHBUTTON_SWITCH_LIB documentation.
π§© Step 3 β Define the Menu (via Generator)¶
Create the menu data (menu.h / menu.c) using the Menu Generator Tool:
- π§ Live app: https://niwciu.github.io/LCD_MENU_GENERATOR_APP/
- π How-to: menu_generator.md
Proposed Example Menu (for 20Γ4 LCD)¶
graph TD
A[Main Menu]
A --> B[Info]
A --> C[Settings]
A --> D[Actions]
A --> E[About]
C --> C1[LCD Brightness]
C --> C2[Sound]
D --> D1[Start]
D --> D2[Stop]
%% --- Klasy / style ---
classDef highlight fill:#f3f4f6,stroke:#888,stroke-width:1px,color:#000;
class B,C1 highlight;
π Legend¶
The soft gray background highlights menu items for which this example will additionally demonstrate
the implementation of handling logic outside the menu structure itself, following the flow described in the documentation:
- Info
- LCD Brightness
Notes
Info/Aboutcan be leaf items with callbacks.Settings/Actionsare submenus; leaves (e.g., LCD Brightness, Sound, Start, Stop) may trigger callbacks.
Generator Output β Example Files¶
Place the generated files under src/ui/.
src/ui/menu.h¶
/**
* @file menu.h
* @author LCD menu code generator
* @brief Generated menu declarations
* @date 2025-11-02
*/
#ifndef _MENU_H_
#define _MENU_H_
#ifdef __cplusplus
extern "C" {
#endif
#include "menu_lib_type.h"
#define MAX_MENU_DEPTH 2
extern menu_t menu_1;
extern menu_t menu_2;
extern menu_t menu_2_1;
extern menu_t menu_2_2;
extern menu_t menu_3;
extern menu_t menu_3_1;
extern menu_t menu_3_2;
extern menu_t menu_4;
#ifdef __cplusplus
}
#endif
#endif /* _MENU_H_ */
src/ui/menu.c¶
/**
* @file menu.c
* @author LCD menu code generator
* @brief Generated menu definitions
* @date 2025-11-02
*/
#include "menu.h"
#include <stddef.h>
static void info_cb(void);
static void lcd_brightness_cb(void);
static void sound_cb(void);
static void start_cb(void);
static void stop_cb(void);
static void about_cb(void);
menu_t menu_1 = { "Info", &menu_2, NULL, NULL, NULL, info_cb };
menu_t menu_2 = { "Settings", &menu_3, &menu_1, &menu_2_1, NULL, NULL };
menu_t menu_2_1 = { "LCD Brightness",&menu_2_2, NULL, NULL, &menu_2, lcd_brightness_cb };
menu_t menu_2_2 = { "Sound", NULL, &menu_2_1, NULL, &menu_2, sound_cb };
menu_t menu_3 = { "Action", &menu_4, &menu_2, &menu_3_1, NULL, NULL };
menu_t menu_3_1 = { "Start", &menu_3_2, NULL, NULL, &menu_3, start_cb };
menu_t menu_3_2 = { "Stop", NULL, &menu_3_1, NULL, &menu_3, stop_cb };
menu_t menu_4 = { "About", NULL, &menu_3, NULL, NULL, about_cb };
static void info_cb(void) { /* TODO: implement */ }
static void lcd_brightness_cb(void) { /* TODO: implement */ }
static void sound_cb(void) { /* TODO: implement */ }
static void start_cb(void) { /* TODO: implement */ }
static void stop_cb(void) { /* TODO: implement */ }
static void about_cb(void) { /* TODO: implement */ }
Notes * Make sure to add
menu.h&menu.cto your build system (CMake/Makefile/IDE).
π‘ Tip β Keep Your Menu Maintainable (Recommended Pattern)¶
Although you could implement the entire menu logic directly inside the generated
menu.c and menu.h, this is not recommended when using the generator β
especially if your menu may evolve over time.
A more flexible and maintainable approach is to separate menu logic into a dedicated layer:
src/ui/
β
βββ menu.c β generated by the Menu Generator
βββ menu.h β generated by the Menu Generator
β
βββ menu_handlers.c β your implementation
βββ menu_handlers.h β your declarations
Why this approach?¶
β When the menu changes, regenerating menu.c / menu.h will not overwrite any logic.
β The generated files will only contain lightweight wrappers calling your real code.
β Version control diffs remain clean β only small wrapper adjustments after regeneration.
β Works great for:
- large or deeply nested menus
- multiple menu structures
- rapidly evolving UI logic
How to implement it¶
- Add your handler layer:
Create:
src/ui/menu_handlers.c
src/ui/menu_handlers.h
menu.c:
#include "menu_handlers.h"
menu_handler that will be implemented in next step.
Bellow you can see menu.c file after implemenitg described changes:
#include "menu.h"
#include "menu_handlers.h"
#include <stddef.h>
static void info_cb(void);
static void lcd_brightness_cb(void);
static void sound_cb(void);
static void start_cb(void);
static void stop_cb(void);
static void about_cb(void);
menu_t menu_1 = {"Info", &menu_2, NULL, NULL, NULL, info_cb};
menu_t menu_2 = {"Settings", &menu_3, &menu_1, &menu_2_1, NULL, NULL};
menu_t menu_2_1 = {"LCD Brightness", &menu_2_2, NULL, NULL, &menu_2, lcd_brightness_cb};
menu_t menu_2_2 = {"Sound", NULL, &menu_2_1, NULL, &menu_2, sound_cb};
menu_t menu_3 = {"Action", &menu_4, &menu_2, &menu_3_1, NULL, NULL};
menu_t menu_3_1 = {"Start", &menu_3_2, NULL, NULL, &menu_3, start_cb};
menu_t menu_3_2 = {"Stop", NULL, &menu_3_1, NULL, &menu_3, stop_cb};
menu_t menu_4 = {"About", NULL, &menu_3, NULL, NULL, about_cb};
static void info_cb(void)
{
info_enter_cb();
}
static void lcd_brightness_cb(void)
{
lcd_brightness_enter_cb();
}
static void sound_cb(void)
{
sound_enter_cb();
}
static void start_cb(void)
{
start_enter_cb();
}
static void stop_cb(void)
{
stop_enter_cb();
}
static void about_cb(void)
{
about_enter_cb();
}
menu_handlers.h:
#ifndef _MENU_HANDLERS_H_
#define _MENU_HANDLERS_H_
#ifdef __cplusplus
extern "C" {
#endif
/** menu enter callbacks */
void info_enter_cb(void);
void lcd_brightness_enter_cb(void);
void sound_enter_cb(void);
void start_enter_cb(void);
void stop_enter_cb(void);
void about_enter_cb(void);
#ifdef __cplusplus
}
#endif
#endif /* _MENU_HANDLERS_H_ */
- Define callback functions in `menu_handlers.c', e.g.:
#include "menu_handlers.h"
#include "lcd_hd44780.h"
#include "keypad.h"
#include "menu_lib.h"
#include <stddef.h>
static uint8_t brightness = 5; // demo state 0..20
/** menu info static section handlers */
static void info_exit_cb(void);
/** menu brightness static section handlers */
static void br_up(void);
static void br_down(void);
static void br_apply(void);
static void br_exit(void);
static void update_lcd_br_view(void);
/*************************** MENU HANDLERS API FUNCTON DEFINITION SECTION *******************************/
/** menu enter calback section */
void info_enter_cb(void)
{
keypad_bind_conrtol_handlers(NULL, NULL, NULL, NULL);
lcd_buf_cls();
lcd_buf_locate(LINE_1, C5);
lcd_buf_str("Device info:");
lcd_buf_locate(LINE_2, C7);
lcd_buf_str("FW 1.0.0");
lcd_buf_locate(LINE_3, C5);
lcd_buf_str("Build: demo");
lcd_buf_locate(LINE_4, C6);
lcd_buf_str("<ESC> back");
keypad_bind_conrtol_handlers(NULL, NULL, NULL, info_exit_cb);
}
/** main app UI functionality API */
void lcd_brightness_enter_cb(void)
{
keypad_bind_conrtol_handlers(br_up, br_down, br_apply, br_exit);
enable_keypad_up_down_repetition();
lcd_buf_cls();
lcd_buf_locate(LINE_1, C4);
lcd_buf_str("LCD Brightness");
lcd_buf_locate(LINE_3, C1);
update_lcd_br_view();
}
/*************************** MENU HANDLERS INTERNALL FUNCTON DEFINITION SECTION *******************************/
/** menu info static section handlers */
static void info_exit_cb(void)
{
lcd_buf_cls();
keypad_bind_menu_controls();
update_screen_view();
}
/** menu settings section handlers */
static void br_up(void)
{
if (brightness < 20)
{
brightness++;
}
lcd_brightness_enter_cb();
}
static void br_down(void)
{
if (brightness > 0)
{
brightness--;
}
lcd_brightness_enter_cb();
}
static void br_apply(void)
{
/* TODO: apply to real backlight driver */
// after applying new setings go back to menu
br_exit();
}
static void br_exit(void)
{
lcd_buf_cls();
disable_keypad_up_down_repetition();
keypad_bind_menu_controls();
update_screen_view(); // refresh where user left off
}
static void update_lcd_br_view(void)
{
// Simple bar visualization (20 chars wide)
char line[21];
for (uint8_t i = 0; i < 20; ++i)
line[i] = (i < brightness) ? '*' : '-';
line[20] = '\0';
lcd_buf_str(line);
}
π§ Production Strategy (Optional but Powerful)¶
In final production builds you can take this pattern one step further:
- Replace wrapper calls with direct function pointers in the generated structure
- Commit this as the βrelease-optimizedβ version
- If later you need to modify the menu again:
- revert to the wrapper version
- update / regenerate
- and optionally remove wrappers again before the next release
This strategy keeps your codebase clean while allowing rapid iteration during development.
This approach significantly improves maintainability, reduces merge conflicts, and
keeps generator-driven workflow smooth β especially for frequently changing or large menus.
π§· Step 4 β Integrate All Components Into the UI Layer¶
With the menu structure generated and input handling prepared, the next step is to
create a dedicated UI layer that wires all components together:
- MENU_LIB
- display driver
- keypad abstraction
- menu handlers
- app-level UI behavior
This allows your main application to stay clean and free of internal UI dependencies.
The UI layer exposes only two functions:
ui_init()β initializes MENU_LIB, LCD, keypad, backlight, and main app screenui_update()β processes keypad and refreshes the LCD (if buffering is enabled)
π Files to Create¶
src/ui/ui.h
src/ui/ui.c
π src/ui/ui.h¶
#ifndef UI_H_
#define UI_H_
#ifdef __cplusplus
extern "C" {
#endif
void ui_init(void);
void ui_update(void);
#ifdef __cplusplus
}
#endif
#endif /* UI_H_ */
π§© src/ui/ui.c¶
#include "keypad.h"
#include "lcd_hd44780.h"
#include "menu_lib.h"
#include "menu_handlers.h"
void ui_init(void)
{
// Initialize MENU_LIB (fetches display interface and validates it and initialize the lcd)
menu_init();
// turn on lcd backlight
lcd_enable_backlight();
// Initialize keypad/UI (ENTER enters the menu) and bind main app keys functionality
keypad_init();
keypad_bind_main_app_controls();
// Print main app label
set_UI_main_app_scr();
}
void ui_update(void)
{
// Process debounced buttons; drives MENU_LIB when in menu mode
keypad_process();
// Keep the display refreshed when buffering is enabled
lcd_update();
}
π§· Step 5 β Create missing UI functionality¶
Looking at code above there is a need for create keypad_bind_main_app_controls() and set_UI_main_app_scr() functions. We can place them in menu_handlers.
Lets update menu_handlers.h:
#ifndef _MENU_HANDLERS_H_
#define _MENU_HANDLERS_H_
#ifdef __cplusplus
extern "C" {
#endif
/** UI main app handlers */
void set_UI_main_app_scr(void);
void keypad_bind_main_app_controls(void);
/** menu enter callbacks */
void info_enter_cb(void);
void lcd_brightness_enter_cb(void);
void sound_enter_cb(void);
void start_enter_cb(void);
void stop_enter_cb(void);
void about_enter_cb(void);
#ifdef __cplusplus
}
#endif
#endif /* _MENU_HANDLERS_H_ */
And lets add function required function services to menu_handler.c:
#include "menu_handlers.h"
#include "lcd_hd44780.h"
#include "keypad.h"
#include "menu_lib.h"
#include <stddef.h>
static uint8_t brightness = 5; // demo state 0..20
/** menu_handlers internal function declarations */
static void on_menu_enter(void);
static void on_menu_exit(void);
static void keypad_bind_menu_controls(void);
/** menu info static section handlers */
static void info_exit_cb(void);
/** menu brightness static section handlers */
static void br_up(void);
static void br_down(void);
static void br_apply(void);
static void br_exit(void);
static void update_lcd_br_view(void);
/** UI comon basic menu handlers */
void set_UI_main_app_scr(void)
{
lcd_buf_locate(LINE_2, C6);
lcd_buf_str("Example 1");
lcd_buf_locate(LINE_3, C3);
lcd_buf_str("Main App. Screen");
}
void keypad_bind_main_app_controls(void)
{
// Main app mode: only ENTER enters the menu
keypad_bind_conrtol_handlers(NULL, NULL, on_menu_enter, NULL);
}
/** main app UI functionality API */
void lcd_brightness_enter_cb(void)
{
keypad_bind_conrtol_handlers(br_up, br_down, br_apply, br_exit);
enable_keypad_up_down_repetition();
lcd_buf_cls();
lcd_buf_locate(LINE_1, C4);
lcd_buf_str("LCD Brightness");
lcd_buf_locate(LINE_3, C1);
update_lcd_br_view();
}
/** menu enter calback section */
void info_enter_cb(void)
{
keypad_bind_conrtol_handlers(NULL, NULL, NULL, NULL);
lcd_buf_cls();
lcd_buf_locate(LINE_1, C5);
lcd_buf_str("Device info:");
lcd_buf_locate(LINE_2, C7);
lcd_buf_str("FW 1.0.0");
lcd_buf_locate(LINE_3, C5);
lcd_buf_str("Build: demo");
lcd_buf_locate(LINE_4, C6);
lcd_buf_str("<ESC> back");
keypad_bind_conrtol_handlers(NULL, NULL, NULL, info_exit_cb);
}
/** general menu_handlers internall functions */
static void on_menu_enter(void)
{
// Start menu view at top-level; default header (NULL)
// Root pointer is the first top-level item (menu_1) from the generator
menu_view_init(&menu_1, on_menu_exit, NULL);
keypad_bind_menu_controls();
}
static void on_menu_exit(void)
{
lcd_buf_cls();
keypad_bind_main_app_controls();
set_UI_main_app_scr();
}
static void keypad_bind_menu_controls(void)
{
keypad_bind_conrtol_handlers(menu_prev, menu_next, menu_enter, menu_esc);
}
/** menu info static section handlers */
static void info_exit_cb(void)
{
lcd_buf_cls();
keypad_bind_menu_controls();
update_screen_view();
}
/** menu settings section handlers */
static void br_up(void)
{
if (brightness < 20)
{
brightness++;
}
lcd_brightness_enter_cb();
}
static void br_down(void)
{
if (brightness > 0)
{
brightness--;
}
lcd_brightness_enter_cb();
}
static void br_apply(void)
{
/* TODO: apply to real backlight driver */
// after applying new setings go back to menu
br_exit();
}
static void br_exit(void)
{
lcd_buf_cls();
disable_keypad_up_down_repetition();
keypad_bind_menu_controls();
update_screen_view(); // refresh where user left off
}
static void update_lcd_br_view(void)
{
// Simple bar visualization (20 chars wide)
char line[21];
for (uint8_t i = 0; i < 20; ++i)
line[i] = (i < brightness) ? '*' : '-';
line[20] = '\0';
lcd_buf_str(line);
}
π Notes¶
- The UI layer integrates all UI components but does not contain business logic.
- MENU_LIB automatically updates the display in response to navigation calls.
- LCD initialization is done via the display interface already provided to MENU_LIB.
keypad_bind_main_app_controls()allows defining button behavior outside menu mode.- This architecture is scalable and clean, ideal for production and future menu expansion.
Why update_screen_view() for exit?
It refreshes the menu at the previous position. Re-initializing with menu_view_init() would reset position/history.
Tip: For more advanced screens, re-draw after each key press and/or use LCD buffering (
lcd_buf_*,lcd_update()).
Step 7 βΆοΈ Updating Your Main Application¶
With the UI layer in place, the main application becomes minimal and clean.
It contains no UI logic, no keypad logic, and no menu logic β only initialization and looping.
Example Main Application¶
#include "ui.h"
int main(void)
{
ui_init();
while (1)
{
ui_update();
}
}
π§± Step 8 β Build & Run¶
Below are minimal build instructions and first-run expectations.
β Ensure files are in the build¶
For the example project, all sources are listed in the main CMakeLists.txt.
Ensure LCD_HD44780 and PUSHBUTTON_SWITCH_LIB (and their GPIO interfaces) are compiled with your app. If they are separate libraries, link them via
target_link_libraries(app ...).
π οΈ Build commands¶
Open a terminal in the exampleβs main folder (where the main CMakeLists.txt is stored):
=== Makefiles
cmake -S . -B out -G "Unix Makefiles"
cd out
make all
cmake -S . -B out -G "Ninja"
cd out
ninja -V
β« Flash & run¶
Flash the binary using your usual tool (ST-Link, OpenOCD, IDE). If you have STM32_Programmer_CLI, you can use predefined targets:
make flash # or: ninja flash
On reset you should see:
- Screen with label βExample 1 Main App. Screenβ.
- Press ENTER β menu opens with header and first page.
- UP/DOWN navigate siblings, ENTER enters a submenu / triggers a callback, ESC goes back.
- Pressing ESC at top-level menu exits to the default screen (βExample 1 Main App. Screenβ).
π§― Troubleshooting & Tips¶
Blank screen
- Check LCD contrast & power.
- Verify
lcd_hd44780_GPIO_interface.cpin mapping. - Ensure
menu_screen_driver_interface.cis compiled andmenu_init()is called. - Confirm correct LCD type in
lcd_hd44780_config.h(e.g.,#define LCD_TYPE 2004). - If buffering is enabled, call
lcd_update()periodically (~ every 40 ms is a reasonable starting point, e.g., from a timer/RTOS task). For a quick sanity check, call:
lcd_init();
lcd_buf_str("Hello");
during init to verify basic output.
Menu does not react to buttons
- Ensure
keypad_init()is called andkeypad_process()runs in the main loop. - Confirm
PB_get_driver_interface()is implemented and passed toinit_pushbutton(...). - Check
TRIGGER_ON_PUSHand that callbacks are registered (menu mode vs app mode).
Wrong text wrapping/truncation
- Keep item labels β€
LCD_X - 1characters. - Prefer short names; use submenus for longer descriptions.
Error codes
MENU_ERR_MENU_TOO_DEEPβ reduce nesting or increaseMAX_MENU_DEPTHin generatedmenu.h. The generator sets this automatically; manual edits may desync it.MENU_ERR_NO_DISPLAY_INTERFACE/MENU_ERR_INCOMPLETE_INTERFACEβ ensure no NULLs in the display driver struct.
Callbacks behavior
- Leaf item callback runs on ENTER when the item has no child.
- Use the wrapper pattern (Step 6) to swap keypad roles during feature handling and restore them on exit with
update_screen_view().
β‘οΈ Where to go next¶
- Examples β richer setups and hardware configs:
examples/and Examples Overview. - Menu Generator β iterate quickly on structure: docs/menu_generator.md.
- API Reference (MD) β quick lookup of public types & calls: docs/api_reference.md.
- Architecture β deeper understanding of layers: docs/architecture.md.