1. Executive Overview
We built a Wordle-style game on the Renesas HMI-Board to show that small embedded systems can now have smooth, touch-friendly screens like consumer devices. This project is a stress test for the RA6M3 microcontroller, the RT-Thread real-time operating system, and the LVGL graphics library, checking if they can handle fast game logic and a responsive GUI at the same time. In this report, we explain how we managed tight memory, created better randomness using the OS, and fixed tricky UI styling issues, so other engineers can reuse these ideas on similar HMI projects with limited hardware resources.2. The Hardware Foundation: Renesas HMI-Board
Before we started coding, we needed to understand the Renesas HMI-Board. This board mixes a small microcontroller with features usually found in more powerful processors, giving us fast graphics display and touch screen capabilities without needing a lot of power.
2.1 The Core: Renesas RA6M3 Microcontroller
At the center of our system is the Renesas RA6M3 chip, which uses an Arm Cortex-M4F core running at 120 MHz. In embedded systems, this is plenty of speed to handle real-time work like reading input and updating the screen. The “F” means it has a Floating Point Unit, which helps with heavy math for animations and scrolling so the main game logic can run smoothly.
2.2 Memory Architecture and Constraints
Memory is the biggest challenge in our project. The RA6M3 has 2MB of storage for code and data, and 640KB of working memory.
One full screen needs about 255KB of memory. If we used double buffering (two screens at once to avoid flicker), that's 510KB—leaving only 130KB for everything else like the operating system, game data, and touch input. So we had to be very careful about what we put on screen and how we built the UI to avoid running out of memory.
The Visual Interface
The board has a 4.3-inch screen with 480x272 pixels, using 16-bit color to save memory. The RA6M3 has two special pieces built in for graphics:
Graphics LCD Controller (GLCDC): This automatically sends the picture from memory to the screen without using the main processor.
2D Accelerator: This handles drawing tasks like filling shapes or copying images, which frees up the main processor to focus on game logic and reading touch input instead of pushing pixels.
2.4Connectivity and Peripherals
The board has extra features built in for future projects. It includes an RW007 Wi-Fi module that can download data from the internet, audio support for sound, and Arduino expansion ports so you can add more features later. These features are designed for smart home devices like thermostats or voice-activated controls.
For our Wordle game, we used the touch screen (connected by I2C) to read finger taps and the debug port to load our code onto the board.
3. The Software Ecosystem: RT-Thread and LVGL
We used the RT-Thread real-time operating system as the base for our Wordle game. It is open-source, built for small embedded devices, and has many ready-made components, which makes it easier to organize our code into separate threads instead of one big loop.
3.1 Why RT-Thread?
In a system with no operating system, the whole program runs in one endless loop, so if it is busy drawing an animation it can miss a screen tap.
With RT-Thread, we split the work into separate threads: one for the UI, one for touch input, and one for background tasks.
RT-Thread quickly switches the CPU between these threads, so the input thread can interrupt lower-priority work and make sure touches are handled right away, which keeps the game feeling responsive.
3.2 The LVGL Graphics Library
We use LVGL (Light and Versatile Graphics Library) to draw everything on the screen. It is written in C, uses little memory, and still supports smooth edges, transparency, and scrolling, which makes it a good fit for small embedded devices. In this project we use LVGL 8, which adds a CSS-like styling system so we can customize the look of the Wordle game in detail, though this also made some things like button borders harder to fine-tune.
4. Project Architecture and Build Setup
We set up the project so it’s easy to change and grow in the future. RT-Thread uses the SCons build system (written in Python), which helps automatically manage project files and configuration, so we spend less time on build setup and more time on code.
4.1 Directory Structure
We organized our code into separate folders to keep it clean and easy to work with:
rt-thread/: The operating system kernel itself, which we don't change
- rt-thread/: The operating system kernel itself, which we don't change
packages/: Outside libraries like LVGL, downloaded automatically
- packages/: Outside libraries like LVGL, downloaded automatically
drivers/: Low-level code that controls hardware pins and connections
- drivers/: Low-level code that controls hardware pins and connections
applications/: Our custom code including:
main.c: Starts the program and creates threads
- main.c: Starts the program and creates threads
wordle_game.c: The game rules and logic
- wordle_game.c: The game rules and logic
wordle_ui.c: The screen display built with LVGL
- wordle_ui.c: The screen display built with LVGL
wordle_dict.h: The list of valid words
- wordle_dict.h: The list of valid words
- applications/: Our custom code including:main.c: Starts the program and creates threadswordle_game.c: The game rules and logicwordle_ui.c: The screen display built with LVGLwordle_dict.h: The list of valid words
This structure makes it easy to find code and add new features later.
4.2 Configuration via rtconfig.h
We control the build through a configuration file called rtconfig.h. The RT-Thread Studio IDE has a tool that makes it easy to turn settings on and off:
RT_USING_LIBC: Turned on so we can use standard C functions like rand(), strcpy(), and memset()
- RT_USING_LIBC: Turned on so we can use standard C functions like rand(), strcpy(), and memset()
PKG_USING_LVGL: Turned on to compile the graphics library
- PKG_USING_LVGL: Turned on to compile the graphics library
LV_COLOR_DEPTH: Set to 16-bit color to save memory
- LV_COLOR_DEPTH: Set to 16-bit color to save memory
BSP_USING_LCD: Turned on to activate the screen driver
- BSP_USING_LCD: Turned on to activate the screen driver
BSP_USING_TOUCH: Turned on to activate the touch input driver
- BSP_USING_TOUCH: Turned on to activate the touch input driver
These settings tell the build system which parts to include and how to set them up for our board.
4.3 The Build Process
When we click "Build" in the IDE, here's what happens:
SCons looks for Python script files called SConscript that tell it which C files to compile
- SCons looks for Python script files called SConscript that tell it which C files to compile
The GCC compiler (for Arm) translates our C code into machine code
- The GCC compiler (for Arm) translates our C code into machine code
The linker combines our game code, the RT-Thread kernel, and the LVGL graphics library into one file that fits in the 2MB Flash memory
- The linker combines our game code, the RT-Thread kernel, and the LVGL graphics library into one file that fits in the 2MB Flash memory
The final result is a binary file (.elf or.bin) ready to load onto the board.
5. Game Logic: Rules and State Management
We kept the game rules separate from the screen code. This means the game logic (wordle_game.c) doesn't know anything about graphics—it just handles the word checking, guesses, and win/loss conditions. Because of this separation, we could even run the game in a text-only terminal on a regular computer, which makes the code cleaner and easier to test.
5.1 Finite State Machine (FSM)
The game has five states that control what happens:
STATE_INIT: Starting up, loading the word list
- STATE_INIT: Starting up, loading the word list
STATE_PLAYING: User is typing letters
- STATE_PLAYING: User is typing letters
STATE_ANIMATING: Tiles are flipping after pressing Enter (no new input allowed)
- STATE_ANIMATING: Tiles are flipping after pressing Enter (no new input allowed)
STATE_WIN: User guessed the word correctly
- STATE_WIN: User guessed the word correctly
STATE_LOSS: User ran out of tries
- STATE_LOSS: User ran out of tries
We use an enum (a list of named values) to track which state the game is in, plus a structure that stores the secret word, past guesses, and current position. This way, the game always knows what mode it's in and what actions are allowed.
The most complex part of Wordle is the feedback logic—determining if a letter is Green, Yellow, or Grey. We implemented a robust algorithm to handle duplicate letters, a common source of bugs in naive implementations.
The Algorithm Strategy:
We process the guess in two distinct passes.
Pass 1 (Green / Exact Match):We iterate through the 5 letters of the guess. If guess[i] == secret[i], we mark the result as RESULT_CORRECT (Green). Critically, we also mark that specific position in the secret word as "consumed" using a temporary boolean array. This prevents a letter that has already been matched exactly from triggering a "Yellow" match elsewhere.
- Pass 1 (Green / Exact Match):We iterate through the 5 letters of the guess. If guess[i] == secret[i], we mark the result as RESULT_CORRECT (Green). Critically, we also mark that specific position in the secret word as "consumed" using a temporary boolean array. This prevents a letter that has already been matched exactly from triggering a "Yellow" match elsewhere.
Pass 2 (Yellow / Partial Match):We iterate through the remaining letters (those not marked Green). For each letter guess[i], we search the secret word to see if that character exists at any other index j.The condition for a Yellow match is strict:
- Pass 2 (Yellow / Partial Match):We iterate through the remaining letters (those not marked Green). For each letter guess[i], we search the secret word to see if that character exists at any other index j.The condition for a Yellow match is strict:
secret[j] must equal guess[i].
- secret[j] must equal guess[i].
Position j in the secret word must NOT have been "consumed" by a Green match in Pass 1.
- Position j in the secret word must NOT have been "consumed" by a Green match in Pass 1.
Position j must NOT have been "consumed" by a previous Yellow match in this pass.
- Position j must NOT have been "consumed" by a previous Yellow match in this pass.
If all conditions are met, we mark guess[i] as RESULT_PRESENT (Yellow) and mark secret[j] as consumed.If the letter is not found or all instances are consumed, we mark it as RESULT_ABSENT (Grey).
This logic ensures that if the secret word is "ABBEY" and the user guesses "BABES":
The first 'B' (index 0) is Yellow (matches index 1 or 2 of secret).
- The first 'B' (index 0) is Yellow (matches index 1 or 2 of secret).
The 'A' (index 1) is Yellow (matches index 0 of secret).
- The 'A' (index 1) is Yellow (matches index 0 of secret).
The 'B' (index 2) is Green (matches index 2 of secret).
- The 'B' (index 2) is Green (matches index 2 of secret).
The 'E' (index 3) is Green (matches index 3 of secret).
- The 'E' (index 3) is Green (matches index 3 of secret).
The 'S' (index 4) is Grey.
- The 'S' (index 4) is Grey.
One of the significant challenges in embedded game development is randomness. Computers are deterministic; they follow instructions. Without an external source of unpredictability (entropy), a function like rand() will produce the exact same sequence of numbers every time the device is powered on. This would mean the "Secret Word" would be the same every day—a disastrous user experience.
Most desktop systems use the current time (e.g., milliseconds since 1970) as a seed. However, the HMI-Board does not have a battery-backed Real Time Clock (RTC) initialized by default. Every time we turn it on, the system time starts at 0.
6.1 Leveraging rt_tick_getWe solved this by exploiting the unpredictability of human behavior. We utilize the RT-Thread kernel's system tick counter.
The function rt_tick_get() returns the number of system ticks (usually milliseconds) that have elapsed since the board was booted.5 This variable is maintained by the OS kernel and incremented by a hardware timer interrupt.
Our implementation introduces a "Start Screen." When the board powers up, it sits on this screen waiting for the user to press "Play."
The time it takes for a user to tap that button is variable. It might be 1543 milliseconds, or 5092 milliseconds. This variance is our entropy.
When the "Play" button is pressed, we execute:
We analyzed the RT-Thread source code for clock.c to ensure this was safe. The function rt_tick_get uses rt_atomic_load to read the tick variable.
This atomic load is crucial. Since rt_tick is a 32-bit (or potentially 64-bit) variable updated by an interrupt, reading it directly without atomicity could result in a "torn read" if an interrupt occurred exactly while we were reading the variable. The atomic load guarantees we get a valid integer.
By using this human-derived seed, we ensure that rand() % DICTIONARY_SIZE produces a different index each session, making the game feel "truly random" to the player.
7. Graphics Implementation: LVGL OptimizationImplementing the UI on the HMI-Board required a deep understanding of LVGL's internal mechanics. The RA6M3's 2D accelerator helps, but we still must be efficient.
7.1 The Keyboard: Button MatrixFor the on-screen keyboard, we had two choices:
Create 28 individual "Button" widgets (lv_btn), each with its own "Label" widget (lv_label).
- Create 28 individual "Button" widgets (lv_btn), each with its own "Label" widget (lv_label).
Use a single "Button Matrix" widget (lv_btnmatrix).
- Use a single "Button Matrix" widget (lv_btnmatrix).
We chose the Button Matrix. In LVGL, a standard button widget consumes significant RAM (roughly 100-150 bytes plus the label overhead).7 Creating 30 of them would fragment the heap and waste memory.
The Button Matrix is a "virtual" widget. It takes a string array (the "Map") and draws the buttons on the fly.
This approach uses only a fraction of the memory, as the button coordinates are calculated dynamically during the render loop rather than stored permanently.
7.2 The Grid: Container LayoutFor the 5x6 grid of letter tiles, we used a Flex layout (lv_obj_set_layout(cont, LV_LAYOUT_FLEX)). We created a container for each row, and added 5 label objects to each row.
To optimize rendering, we explicitly set the style of these tiles to be simple. We avoided using heavy effects like blurs or gradients, which are computationally expensive. The RA6M3's 2D accelerator is excellent at filling solid colors, so we stuck to a flat design language (Solid Green, Solid Yellow, Solid Grey) which aligns perfectly with the hardware's strengths.
8. The User Interface: Addressing the Border ArtifactDuring our development, we encountered a persistent visual glitch that serves as an excellent case study in LVGL styling.
8.1 The ProblemWhen we rendered the Button Matrix keyboard, we wanted a "flat" look—clean keys floating on the background. However, we noticed a persistent outline or border around each key, and sometimes a strange artifact where the background color would "bleed" or double-draw when a key was pressed.
This is a known complexity in LVGL v8's default theme.4 A Button Matrix is composed of two "Parts":
LV_PART_MAIN: The background rectangle of the entire keyboard area.
- LV_PART_MAIN: The background rectangle of the entire keyboard area.
LV_PART_ITEMS: The individual virtual buttons (keys).
- LV_PART_ITEMS: The individual virtual buttons (keys).
A common mistake—which we initially made—is to try to remove the border from the main object:
This removes the box around the outside of the keyboard. It does not affect the keys inside. The keys have their own style defined by the default theme, which often includes a 1px border and a shadow to give a 3D effect.
8.2 The Solution: Targeted StylingTo fix this, we had to create a specific style that targets LV_PART_ITEMS. We also had to ensure this style applied to all states, especially the PRESSED state.
We implemented the fix as follows:
By explicitly targeting LV_PART_ITEMS, we told the renderer: "When you draw the individual 'Q' or 'W' buttons, do not draw a border." This resulted in the clean, flat aesthetic we required.
9. Input Processing and Threading ModelThe responsiveness of the game depends on how we handle the capacitive touch inputs.
9.1 The Driver StackHardware Layer: The capacitive touch panel detects a change in capacitance at coordinates (X, Y). The controller chip (often a FocalTech or Goodix IC) triggers an interrupt line connected to the RA6M3.
- Hardware Layer: The capacitive touch panel detects a change in capacitance at coordinates (X, Y). The controller chip (often a FocalTech or Goodix IC) triggers an interrupt line connected to the RA6M3.
Driver Layer (drivers/): The RT-Thread driver handles the interrupt. It reads the X/Y coordinates over the I2C bus.
- Driver Layer (drivers/): The RT-Thread driver handles the interrupt. It reads the X/Y coordinates over the I2C bus.
Input Device Layer: LVGL has a concept of "Input Devices." We registered a pointer device (lv_indev_drv_t) that points to our touch driver's read function.
- Input Device Layer: LVGL has a concept of "Input Devices." We registered a pointer device (lv_indev_drv_t) that points to our touch driver's read function.
In main.c, we created a dedicated thread for the GUI:
The lv_task_handler() function is where the magic happens. It calculates new animations, redraws dirty areas of the screen, and polls the input device.
The rt_thread_mdelay(5) call is vital. It puts the GUI thread to "sleep" for 5 milliseconds. This yields the CPU back to the RT-Thread scheduler. During this sleep time, the scheduler can run the Idle thread or other background system tasks. If we removed this delay, the GUI thread would consume 100% of the CPU cycles, potentially starving the touch interrupt handler or causing the processor to overheat unnecessarily.
10. Performance AnalysisWe closely monitored the performance of our implementation.
Frame Rate: Thanks to the RA6M3's 2D accelerator, we achieved a steady frame rate (approx. 30-60 FPS) during tile flip animations. The accelerator handles the buffer clearing and solid color fills effectively.
- Frame Rate: Thanks to the RA6M3's 2D accelerator, we achieved a steady frame rate (approx. 30-60 FPS) during tile flip animations. The accelerator handles the buffer clearing and solid color fills effectively.
Memory Usage: By using the Button Matrix and RGB565 color depth, we kept our RAM usage well within the 640KB limit. We did not need to resort to external SDRAM, which simplifies the hardware design.
- Memory Usage: By using the Button Matrix and RGB565 color depth, we kept our RAM usage well within the 640KB limit. We did not need to resort to external SDRAM, which simplifies the hardware design.
Touch Latency: The combination of RT-Thread's preemptive scheduling and the high priority of the touch interrupt resulted in imperceptible latency. The game feels snappy and responsive.
- Touch Latency: The combination of RT-Thread's preemptive scheduling and the high priority of the touch interrupt resulted in imperceptible latency. The game feels snappy and responsive.
Our development of Wordle for the Renesas HMI-Board has successfully demonstrated the viability of this platform for sophisticated HMI applications. We have proven that:
Complexity is Manageable: By separating game logic from UI code, we created a robust and testable system.
- Complexity is Manageable: By separating game logic from UI code, we created a robust and testable system.
Resource Constraints Foster Creativity: The memory limits of the RA6M3 forced us to use efficient widgets like the Button Matrix, resulting in a leaner, faster application.
- Resource Constraints Foster Creativity: The memory limits of the RA6M3 forced us to use efficient widgets like the Button Matrix, resulting in a leaner, faster application.
The Ecosystem Works: The combination of RT-Thread's kernel utilities (like rt_tick_get) and LVGL's styling engine provides a comprehensive toolkit for solving real-world embedded problems.
- The Ecosystem Works: The combination of RT-Thread's kernel utilities (like rt_tick_get) and LVGL's styling engine provides a comprehensive toolkit for solving real-world embedded problems.
Looking forward, this project lays the groundwork for more advanced features. With the board's Wi-Fi capabilities, we could easily add a "Daily Challenge" mode that downloads a word from a server. The audio capabilities could be used to add sound effects for winning or losing. The foundation we have built—a modular, multi-threaded, hardware-accelerated application—is ready to support these expansions.
We believe this report provides a complete blueprint for engineers looking to leverage the Renesas HMI-Board for their own interactive products.
12. Technical AppendixTable 1: Hardware Specifications Summary
Table 2: Game Logic State Table
Table 3: Logic Evaluation Truth Table (Example: Target "SPEED")
1. HMI-Board OTA Upgrade Reference. Hardware Platform | by RT-Thread IoT OS | Medium, accessed November 28, 2025, https://rt-thread.medium.com/hmi-board-ota-upgrade-reference-f7d9bb9034a2
2. $25 Renesas "HMI Board" features RA6M3 microcontroller for RT-Thread & LVGL development - CNX Software, accessed November 28, 2025, https://www.cnx-software.com/2023/05/17/25-renesas-hmi-board-features-ra6m3-microcontroller-for-rt-thread-lvgl-development/
3. RT-Thread, Renesas Electronics RA6M3 HMI board - board certification review, accessed November 28, 2025, https://blog.lvgl.io/2023-06-14/ra6m3-hmi-board-review
4. btnmatrix buttons don't respond to border_color when in `LV_STATE_CHECKED` state · Issue #1687 · lvgl/lvgl - GitHub, accessed November 28, 2025, https://github.com/lvgl/lvgl/issues/1687
5. RT-Thread RTOS: Clock and Timer Management, accessed November 28, 2025, https://www.rt-thread.org/download/rttdoc_1_0_0/group___clock.html
6. Button matrix (lv_btnmatrix) — LVGL documentation, accessed November 28, 2025, https://docs.lvgl.io/8.3/widgets/core/btnmatrix.html
7. Button styling to fully remove border - How-to - LVGL Forum, accessed November 28, 2025, https://forum.lvgl.io/t/button-styling-to-fully-remove-border/1365Works cited
8. HMI-Board OTA Upgrade Reference. Hardware Platform | by RT-Thread IoT OS | Medium, accessed November 28, 2025, https://rt-thread.medium.com/hmi-board-ota-upgrade-reference-f7d9bb9034a2
9. $25 Renesas "HMI Board" features RA6M3 microcontroller for RT-Thread & LVGL development - CNX Software, accessed November 28, 2025, https://www.cnx-software.com/2023/05/17/25-renesas-hmi-board-features-ra6m3-microcontroller-for-rt-thread-lvgl-development/
10. RT-Thread, Renesas Electronics RA6M3 HMI board - board certification review, accessed November 28, 2025, https://blog.lvgl.io/2023-06-14/ra6m3-hmi-board-review
11. btnmatrix buttons don't respond to border_color when in `LV_STATE_CHECKED` state · Issue #1687 · lvgl/lvgl - GitHub, accessed November 28, 2025, https://github.com/lvgl/lvgl/issues/1687
12. RT-Thread RTOS: Clock and Timer Management, accessed November 28, 2025, https://www.rt-thread.org/download/rttdoc_1_0_0/group___clock.html
13. Button matrix (lv_btnmatrix) — LVGL documentation, accessed November 28, 2025, https://docs.lvgl.io/8.3/widgets/core/btnmatrix.html













Comments