Dit artikel beschrijft de technische uitdagingen bij het ontwikkelen van een custom user interface voor een scooter-display met de programmeertaal Rust en het framework Buoyant.
De belangrijkste technische punten zijn:
- Type Explosion: Het gebruik van een declaratieve UI-structuur in Rust leidt tot zeer complexe generieke types, wat resulteert in een aanzienlijke toename van de binary size (code bloat).
- Geheugenbeheer: Vanwege de beperkte RAM (32KB) kan er geen volledige framebuffer worden gebruikt. Om flikkeringen te voorkomen, is een Quadtree-based Dirty Region Tracking systeem geïmplementeerd dat alleen de gewijzigde delen van het scherm ververst via de SPI/I2C bus.
- Hardware-conflicten: Er ontstonden problemen met de CAN-bus communicatie door conflicten tussen de bootloader en de applicatie-clockconfiguratie. Dit werd opgelost door een handmatige 'tear down' sequentie van de CRM-registers te implementeren om de hardware terug te brengen naar een schone staat.
Here is a detailed breakdown of the technical concepts discussed in the text:
1. The UI Framework: "Buoyant"
The author is using a library called Buoyant, which appears to be a declarative UI framework similar to SwiftUI or Jetpack Compose, but designed for embedded systems.
- Declarative Structure: The code uses
VStack (Vertical Stack) and HStack (Horizontal Stack) to define layout, rather than absolute X/Y coordinates.
- State Management (Lenses): The use of
Lens suggests a way to "zoom in" on a specific part of the global state (e.g., the PIN digits) and provide a mutable reference to it for a specific component without passing the entire state object around.
- The "Type Explosion" Problem: The author highlights a common issue in Rust when nesting many generics. Because every modifier (padding, color, etc.) wraps the previous component in a new type, the resulting type for the rest of the screen becomes a "monster" (as seen in the 150-line type definition). This leads to binary bloat because the compiler generates specific code for every single unique combination of these types.
2. Solving the "Flicker" (The Quadtree Solution)
Most modern displays use a framebuffer (a slice of RAM that holds every pixel), which is then pushed to the screen. However, this MCU only has 32KB of RAM, which is far too small for a full-color framebuffer.
The Problem: Without a framebuffer, the MCU must send drawing commands directly to the display. If you redraw the whole screen every frame, the display flickers because the "erase" and "redraw" steps are visible.
The Solution: A Quadtree-based Dirty Region Tracking system.
- Dirty Tree: Tracks areas of the screen that have changed and must be redrawn.
- Overdrawn Tree: Tracks areas that were just drawn over by a larger component (meaning anything beneath them doesn't need to be redrawn).
- Why a Quadtree? A single bounding box for all changes is inefficient if two small things change on opposite corners of the screen (it would force the entire middle to redraw). A quadtree allows the system to isolate multiple small "dirty" rectangles, minimizing the data sent over the SPI/I2C bus.
3. The Hardware "Dance" (Clock & CAN Bus)
The most "low-level" part of the post describes a nightmare scenario in embedded programming: Bootloader Interference.
- The Conflict: The bootloader (the first piece of code that runs) initialized the system clocks. When the main application took over, it tried to set the clocks again.
- The Bug: In many MCUs, you cannot change the clock divisor or source while the clock is actively running. Because the bootloader left the clocks "on," the application's clock configuration failed silently.
- The Symptom: The CAN bus (Controller Area Network, used for communication between the scooter's motor controller and display) failed. Since CAN bus timing is extremely sensitive to the clock frequency, the incorrect clock caused "framing errors."
- The Fix: The author had to write a "tear down" sequence. Before configuring the clocks, they manually reset the
CRM (Clock Reset and Management) registers and disabled peripherals to return the hardware to a "clean slate," allowing the clock configuration to actually take effect.
Summary of Trade-offs
| Feature | Benefit | Cost/Trade-off |
| Declarative UI | Rapid development, easy layout | Massive binary size (code bloat) |
| Quadtree Redraw | No flickering, low RAM usage | Complexity in the UI engine |
| Rust Language | Memory safety, strong typing | Steep learning curve for generic types |
| Custom Firmware | Total control over hardware | Must manually handle bootloader clock conflicts |
Here is a detailed breakdown of the technical concepts discussed in the text:
1. The UI Framework: "Buoyant"
The author is using a library called Buoyant, which appears to be a declarative UI framework similar to SwiftUI or Jetpack Compose, but designed for embedded systems.
- Declarative Structure: The code uses
VStack (Vertical Stack) and HStack (Horizontal Stack) to define layout, rather than absolute X/Y coordinates.
- State Management (Lenses): The use of
Lens suggests a way to "zoom in" on a specific part of the global state (e.g., the PIN digits) and provide a mutable reference to it for a specific component without passing the entire state object around.
- The "Type Explosion" Problem: The author highlights a common issue in Rust when nesting many generics. Because every modifier (padding, color, etc.) wraps the previous component in a new type, the resulting type for the rest of the screen becomes a "monster" (as seen in the 150-line type definition). This leads to binary bloat because the compiler generates specific code for every single unique combination of these types.
2. Solving the "Flicker" (The Quadtree Solution)
Most modern displays use a framebuffer (a slice of RAM that holds every pixel), which is then pushed to the screen. However, this MCU only has 32KB of RAM, which is far too small for a full-color framebuffer.
The Problem: Without a framebuffer, the MCU must send drawing commands directly to the display. If you redraw the whole screen every frame, the display flickers because the "erase" and "redraw" steps are visible.
The Solution: A Quadtree-based Dirty Region Tracking system.
- Dirty Tree: Tracks areas of the screen that have changed and must be redrawn.
- Overdrawn Tree: Tracks areas that were just drawn over by a larger component (meaning anything beneath them doesn't need to be redrawn).
- Why a Quadtree? A single bounding box for all changes is inefficient if two small things change on opposite corners of the screen (it would force the entire middle to redraw). A quadtree allows the system to isolate multiple small "dirty" rectangles, minimizing the data sent over the SPI/I2C bus.
3. The Hardware "Dance" (Clock & CAN Bus)
The most "low-level" part of the post describes a nightmare scenario in embedded programming: Bootloader Interference.
- The Conflict: The bootloader (the first piece of code that runs) initialized the system clocks. When the main application took over, it tried to set the clocks again.
- The Bug: In many MCUs, you cannot change the clock divisor or source while the clock is actively running. Because the bootloader left the clocks "on," the application's clock configuration failed silently.
- The Symptom: The CAN bus (Controller Area Network, used for communication between the scooter's motor controller and display) failed. Since CAN bus timing is extremely sensitive to the clock frequency, the incorrect clock caused "framing errors."
- The Fix: The author had to write a "tear down" sequence. Before configuring the clocks, they manually reset the
CRM (Clock Reset and Management) registers and disabled peripherals to return the hardware to a "clean slate," allowing the clock configuration to actually take effect.
Summary of Trade-offs
| Feature | Benefit | Cost/Trade-off |
| Declarative UI | Rapid development, easy layout | Massive binary size (code bloat) |
| Quadtree Redraw | No flickering, low RAM usage | Complexity in the UI engine |
| Rust Language | Memory safety, strong typing | Steep learning curve for generic types |
| Custom Firmware | Total control over hardware | Must manually handle bootloader clock conflicts |