Project 6: RGB + OLED (Feedback and Status UI)
Add per-layer RGB lighting and an OLED status display to a QMK keyboard while keeping scan latency stable.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Intermediate |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (QMK) |
| Alternative Programming Languages | None |
| Coolness Level | Level 4: Wizard |
| Business Potential | Level 3: Premium Add-on |
| Prerequisites | Project 5, basic I2C/SPI familiarity |
| Key Topics | QMK feature flags, OLED rendering, RGB performance |
1. Learning Objectives
By completing this project, you will:
- Enable and configure RGB Matrix and OLED features in QMK.
- Implement per-layer RGB feedback with minimal latency impact.
- Render a compact OLED UI (layer, caps lock, WPM).
- Measure scan rate before and after features.
- Keep firmware size within MCU flash limits.
2. All Theory Needed (Per-Concept Breakdown)
2.1 Scan Loop Performance and Peripheral Timing
Fundamentals
In QMK, the matrix scan loop runs continuously, reading the state of every key. Peripheral features like RGB animations and OLED rendering can steal CPU time from this loop if they are executed too frequently. When the scan loop slows down, latency increases and key detection can become unreliable. Therefore, every visual feature should be scheduled or throttled to avoid dominating the CPU. The core principle is to decouple UI updates from the scan cycle and update them at a lower rate (e.g., 5-10 Hz for OLED, 20-50 Hz for RGB effects).
Deep Dive into the Concept
The matrix scan loop in QMK is the heartbeat of the firmware. It is typically configured to run at a high frequency (hundreds or thousands of Hz) to ensure low latency. Each scan cycle includes reading rows and columns, applying debounce, processing key events, and updating USB HID reports. When you add peripherals, you are effectively inserting additional work into this loop. For example, OLED rendering involves sending bytes over I2C or SPI, which can take hundreds of microseconds per frame depending on bus speed and display size. RGB matrix effects can involve iterating over each LED and calculating colors, which is O(N) per frame and can be expensive for large LED counts.
The main strategy for keeping performance acceptable is throttling. QMK provides hooks like oled_task_user and rgb_matrix_indicators_user that are called during the scan loop. You can guard these with time checks so they only run occasionally. A common pattern is to store a timestamp and update the OLED only if 100 ms have passed. Similarly, you can run RGB animations at a lower frame rate or use static colors based on layer state. This reduces CPU usage and avoids scan latency spikes.
Latency issues are often subtle. A keyboard might feel fine at idle but degrade when you type rapidly because key processing and rendering compete for CPU time. This is why measurement is important. QMK provides a debug_matrix option and matrix_scan_user hooks where you can toggle a GPIO and measure scan frequency with a logic analyzer. If the scan rate drops significantly when RGB animations are enabled, you need to simplify the effect or reduce frame rate.
Power consumption is another aspect of performance. RGB LEDs draw significant current, especially at full brightness. If your firmware updates colors rapidly, the instantaneous current draw can cause voltage droop, which can interfere with the MCU or USB power stability. By limiting brightness and update rate, you improve both stability and power consumption.
Finally, scheduling decisions influence user experience. An OLED that updates too slowly feels laggy, while an OLED that updates too quickly drains CPU. A good target is 5-10 updates per second for text-based UI. For RGB, 20-30 updates per second is often enough for smooth transitions. The key is that these updates should be decoupled from scan timing, making the scan loop deterministic and stable.
Additional performance tuning: consider the bus speed and protocol for your display. I2C at 100 kHz can be a bottleneck; if your MCU and OLED support 400 kHz, use it. For SPI displays, you can push higher throughput, but watch for extra CPU overhead. QMK also allows disabling unused OLED pages and fonts to reduce render time. Measure the scan loop before and after each change so you can correlate specific features with latency impact. If you see sporadic lag, log a simple scan counter and compare its rate with expected values.
Extra performance note: RGB Matrix and RGB Light are different subsystems. RGB Light is simpler and often cheaper in CPU usage, which can be a better fit for small MCUs. If you only need static underglow, RGB Light might be enough. For OLED, consider caching rendered strings and only updating when values change (layer, caps state, WPM). This minimizes I2C traffic and keeps scan timing consistent.
Additional optimization: the oled_task_user hook can return false to skip default rendering, which saves time if you fully control the display. If you only need occasional updates, gate the entire OLED render with a timer and avoid formatting strings in the scan loop. For RGB, consider RGB_MATRIX_TIMEOUT to turn off lighting after inactivity and reduce both CPU and power use.
How this fits on projects
This concept is essential in Project 6 and is relevant to Project 8 (split keyboard latency) and Project 9 (battery-powered firmware).
Definitions & key terms
- Scan loop: The continuous cycle that reads the key matrix.
- Frame rate: How often a visual update occurs.
- Throttling: Limiting update frequency to reduce CPU usage.
Mental model diagram (ASCII)
Scan loop (1 kHz)
|scan|scan|scan|scan|scan|
|OLED update every 100 ms|
|RGB update every 40 ms|
How it works (step-by-step, with invariants and failure modes)
- Run matrix scan on a fixed loop.
- Check timestamps for OLED/RGB updates.
- If update window elapsed, render once.
- Return quickly to scan loop.
Invariant: scan loop must not block for long operations. Failure modes include unthrottled OLED rendering and expensive RGB animations.
Minimal concrete example
bool oled_task_user(void) {
if (timer_elapsed32(last_oled) < 100) return false;
last_oled = timer_read32();
render_status();
return false;
}
Common misconceptions
- “More frames = better”: Excessive updates reduce responsiveness.
- “OLED is cheap”: Rendering can be expensive over I2C.
- “RGB timing doesn’t matter”: It can dominate CPU time.
Check-your-understanding questions
- Why should OLED updates be throttled?
- How do RGB animations affect scan latency?
- What tools can measure scan frequency?
Check-your-understanding answers
- OLED rendering is slow; frequent updates block the scan loop.
- Each frame requires per-LED computation, which adds time to the scan loop.
- QMK debug logs, GPIO toggles with a logic analyzer, or built-in timers.
Real-world applications
- Performance tuning for embedded UI systems.
- Battery-sensitive firmware design.
Where you’ll apply it
- In this project: §3.3 Non-Functional Requirements and §5.10 Phase 2.
- Also used in: Project 8, Project 9.
References
- QMK docs: OLED and RGB Matrix features.
- “Making Embedded Systems” Ch. 4-6.
Key insights
UI features must be scheduled, not allowed to run at scan rate.
Summary
The scan loop is sacred. Throttle OLED and RGB updates so the keyboard remains responsive under load.
Homework/Exercises to practice the concept
- Measure scan rate with RGB disabled and enabled.
- Adjust OLED update intervals and observe CPU usage.
Solutions to the homework/exercises
- The scan rate should drop when RGB is enabled; tune to recover.
- Larger intervals reduce CPU use and improve scan stability.
2.2 Firmware Feature Flags, Flash Limits, and Power Budget
Fundamentals
Microcontrollers used in keyboards often have limited flash and RAM. Enabling features like RGB Matrix, OLED, and WPM tracking increases firmware size. QMK uses rules.mk feature flags to include or exclude code at compile time. If you enable too many features, the firmware can exceed flash size and fail to build. Power budget is also constrained: RGB LEDs can draw hundreds of milliamps, which can exceed USB limits if not managed. Therefore, you must balance feature set and power constraints carefully.
Deep Dive into the Concept
QMK’s build system is feature-flag driven. Each feature adds code and data. On AVR chips like ATmega32U4, flash is limited to 32 KB, and RAM is only 2.5 KB. Enabling RGB Matrix, OLED, and audio together can easily exceed these limits. The build output provides a size summary; if your firmware is too large, you must disable features or enable optimizations like LTO_ENABLE = yes. Understanding how to read size output is essential: the .text section is flash usage, .bss is RAM usage. A firmware that barely fits can still be unstable if RAM is exhausted.
Power budget is another hard limit. USB 2.0 devices are typically limited to 500 mA (or 100 mA before enumeration). Many RGB LEDs can exceed this if set to full brightness. QMK allows you to set RGB limits (RGB_MATRIX_MAXIMUM_BRIGHTNESS) and to disable animations that spike current. OLEDs also consume power, though less than RGB. If you power the board from a weak USB port or a laptop, excessive current draw can cause disconnects or brownouts. A well-designed firmware throttles brightness and limits update frequency to stay within safe current draw.
Feature flags in rules.mk and config.h let you manage both size and performance. For example, you can use RGB_MATRIX_ENABLE = yes with a limited set of effects, or you can use RGBLIGHT_ENABLE = yes which is a simpler, lighter feature for underglow. Similarly, enabling WPM tracking (WPM_ENABLE) adds a small amount of code and state. The best practice is to start with only the features you need, measure size and performance, and then add extras incrementally.
Understanding feature flags is also essential for maintainability. In a production environment (Project 11), you may want multiple firmware variants with different feature sets. A clean rules.mk structure allows you to build these variants without changing the core code. This project gives you the habit of reading the size report and thinking in terms of feature trade-offs.
Additional size and power tactics: a common optimization is disabling unused action features in QMK (NO_ACTION_MACRO, NO_ACTION_FUNCTION). This reduces code size and RAM usage. For power, consider USB suspend behavior; when the host suspends, your firmware should dim or disable RGB to stay within allowed current. Use RGB_MATRIX_MAXIMUM_BRIGHTNESS and define a conservative default so that users can opt-in to higher brightness at their own risk. If you support multiple boards, create build profiles with different feature sets for small MCUs.
Extra power and size note: many MCUs support USB suspend events. Implement a suspend handler to turn off RGB and dim the OLED when the host suspends, which is required to stay within USB suspend current limits. Also, review the .map file or size report after each feature change to ensure your flash usage does not creep up over time. Establish a size budget and fail the build if you exceed it.
Additional power constraint: USB devices are limited to 100 mA before enumeration and 500 mA after enumeration on USB 2.0. If your RGB LEDs draw significant current at startup, the device can brown out during enumeration. Set a conservative startup brightness and raise it only after USB is configured. Document these limits so users understand why brightness is capped by default.
How this fits on projects
This concept is critical in Project 6 and carries over to Project 11 where you must ship multiple firmware variants.
Definitions & key terms
- Feature flag: Build-time switch to include/exclude code.
- Flash size: Program memory for firmware.
- Power budget: Maximum current draw allowed by USB or battery.
Mental model diagram (ASCII)
Features -> Code size -> Flash limit
Features -> LED count -> Current draw
How it works (step-by-step, with invariants and failure modes)
- Enable features in
rules.mk. - Compile and inspect size report.
- Adjust feature set or enable LTO if too large.
- Set brightness limits to stay within current budget.
Invariant: firmware size must fit flash; current must stay within USB limits. Failure modes include build failures and USB disconnects.
Minimal concrete example
RGB_MATRIX_ENABLE = yes
OLED_ENABLE = yes
LTO_ENABLE = yes
Common misconceptions
- “If it compiles, it will run”: RAM overflow can still crash.
- “USB ports always supply enough current”: Some ports are limited or strict.
- “All RGB features cost the same”: Different effects have different CPU and memory costs.
Check-your-understanding questions
- What does
LTO_ENABLEdo? - Why might firmware that fits in flash still be unstable?
- How can you limit RGB current draw?
Check-your-understanding answers
- It enables link-time optimization to reduce code size.
- RAM could be exhausted, causing stack overflows or crashes.
- Reduce brightness or limit effects and LED count.
Real-world applications
- Shipping firmware variants for different hardware SKUs.
- Battery-powered keyboards with strict power budgets.
Where you’ll apply it
- In this project: §3.3 Non-Functional Requirements and §5.11 Key Decisions.
- Also used in: Project 11.
References
- QMK docs: firmware size and feature flags.
- “Making Embedded Systems” Ch. 2-4.
Key insights
Feature flags are not just toggles; they are the control knobs for size, power, and stability.
Summary
Firmware features have real costs. By managing feature flags and brightness limits, you keep the keyboard stable and within hardware constraints.
Homework/Exercises to practice the concept
- Compile firmware with RGB and OLED enabled and record the size.
- Disable one feature and compare size and scan rate.
Solutions to the homework/exercises
- You should see a significant increase in
.textand possibly.bss. - Disabling a feature reduces size and improves scan stability.
3. Project Specification
3.1 What You Will Build
A QMK keyboard configuration that:
- Enables RGB Matrix with per-layer colors.
- Enables OLED display with a status screen.
- Maintains stable scan rate and fits in flash.
3.2 Functional Requirements
- RGB per layer: Base, Nav, Sym each mapped to a color.
- OLED status: Display layer, caps lock, and WPM.
- Performance: OLED updates at <=10 Hz.
- Firmware size: Fits in MCU flash.
3.3 Non-Functional Requirements
- Performance: Scan rate drop < 10% after enabling features.
- Reliability: No USB disconnects due to current spikes.
- Usability: UI is readable and minimal.
3.4 Example Usage / Output
OLED (128x32):
Layer: NAV
WPM: 82
CAPS: off
3.5 Data Formats / Schemas / Protocols
- QMK feature flags and OLED rendering functions.
3.6 Edge Cases
- OLED updates too frequently (lag).
- RGB brightness too high (USB brownout).
- Firmware exceeds flash size.
3.7 Real World Outcome
RGB reflects layer state and OLED shows status without lag.
3.7.1 How to Run (Copy/Paste)
qmk compile -kb kyria/rev1 -km oled_rgb
qmk flash -kb kyria/rev1 -km oled_rgb
3.7.2 Golden Path Demo (Deterministic)
- Base layer shows blue, NAV shows green, SYM shows red.
- OLED updates every 100 ms.
3.7.3 If CLI: exact terminal transcript
$ qmk compile -kb kyria/rev1 -km oled_rgb
Creating hex file: .build/kyria_rev1_oled_rgb.hex
exit_code=0
$ qmk flash -kb kyria/rev1 -km oled_rgb
Flashing complete
exit_code=0
$ qmk compile -kb kyria/rev1 -km badname
error: keymap not found
exit_code=2
4. Solution Architecture
4.1 High-Level Design
Layer State -> RGB Indicator
Layer State -> OLED Renderer (throttled)
Matrix Scan -> Key Events -> WPM
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| RGB Matrix | Per-layer color mapping | Static colors vs animations |
| OLED Renderer | Display status text | 5-10 Hz updates |
| Performance | Keep scan rate stable | Throttle updates |
4.3 Data Structures (No Full Code)
static uint32_t last_oled = 0;
4.4 Algorithm Overview
Key Algorithm: Throttled OLED Update
- Check elapsed time.
- If >=100 ms, render status.
- Return quickly to scan loop.
Complexity Analysis:
- Time: O(1) per scan + O(n) per OLED frame
- Space: O(1)
5. Implementation Guide
5.1 Development Environment Setup
qmk setup
qmk doctor
5.2 Project Structure
keyboards/<board>/keymaps/oled_rgb/
├── keymap.c
└── rules.mk
5.3 The Core Question You’re Answering
“How do I add user feedback without harming scan performance?”
5.4 Concepts You Must Understand First
- Scan loop timing and throttling.
- Feature flags and flash limits.
- RGB and OLED driver configuration.
5.5 Questions to Guide Your Design
- What minimal information should the OLED show?
- Which layers need distinct color feedback?
- What brightness limit keeps power safe?
5.6 Thinking Exercise
Choose three UI elements for the OLED and justify why they are the most useful.
5.7 The Interview Questions They’ll Ask
- How do RGB animations affect scan latency?
- How do you manage firmware size when enabling OLED?
- What is the difference between RGB Matrix and RGB Light?
5.8 Hints in Layers
Hint 1: Start with static colors Animations can come later.
Hint 2: Throttle OLED updates Update at 5-10 Hz.
Hint 3: Use LTO
If flash size is tight, enable LTO_ENABLE.
Hint 4: Measure scan rate Compare before/after enabling features.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Embedded timing | “Making Embedded Systems” | Ch. 4-6 |
| Build flags | “The GNU Make Book” | Ch. 3-4 |
| Debugging | “The Art of Debugging with GDB” | Ch. 1-2 |
5.10 Implementation Phases
Phase 1: Feature Enablement (2-3 days)
Goals: enable RGB and OLED
Tasks:
- Set
RGB_MATRIX_ENABLE = yesandOLED_ENABLE = yes. - Compile and verify size.
Checkpoint: Firmware compiles successfully.
Phase 2: UI Implementation (3-4 days)
Goals: per-layer RGB and OLED status
Tasks:
- Implement layer-based color mapping.
- Render layer/WPM/caps on OLED.
Checkpoint: Visual feedback matches layer state.
Phase 3: Performance Tuning (2-3 days)
Goals: stable scan rate
Tasks:
- Throttle OLED and RGB updates.
- Measure scan rate before/after.
Checkpoint: Scan rate drop < 10%.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| RGB mode | static vs animated | static | low CPU cost |
| OLED update rate | 5 Hz vs 20 Hz | 10 Hz | balance readability/time |
| Brightness limit | 50 vs 100 | 50 | avoid USB brownout |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Visual Tests | Verify RGB/OLED output | Layer toggles |
| Performance Tests | Measure scan rate | debug logs |
| Size Tests | Check flash usage | build size output |
6.2 Critical Test Cases
- Switch layers and verify RGB color change.
- OLED updates at <=10 Hz with no flicker.
- Firmware fits in flash with LTO if needed.
6.3 Test Data
Layer colors: BASE=blue, NAV=green, SYM=red
OLED fields: layer, WPM, CAPS
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| OLED updates too fast | Typing lag | Throttle updates |
| RGB too bright | USB disconnects | Lower brightness limit |
| Firmware too large | Build fails | Disable features or enable LTO |
7.2 Debugging Strategies
- Compare scan rates before and after enabling features.
- Log layer state to ensure RGB/OLED match.
7.3 Performance Traps
Using large fonts or bitmaps on OLED can consume RAM and CPU. Keep it text-only.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a caps lock indicator LED.
- Add a simple boot animation.
8.2 Intermediate Extensions
- Add reactive RGB effects on keypress.
- Display a layer name instead of number.
8.3 Advanced Extensions
- Add per-key RGB highlighting for modifiers.
- Add power-saving mode that dims RGB when idle.
9. Real-World Connections
9.1 Industry Applications
- Premium custom keyboards with visual feedback.
- Embedded devices with small status screens.
9.2 Related Open Source Projects
- QMK OLED examples.
- RGB matrix effect libraries.
9.3 Interview Relevance
- Demonstrates performance trade-offs in embedded UI design.
10. Resources
10.1 Essential Reading
- QMK docs: RGB Matrix and OLED features.
- “Making Embedded Systems” Ch. 4-6.
10.2 Video Resources
- OLED rendering tutorials for QMK.
10.3 Tools & Documentation
- Logic analyzer for scan timing.
10.4 Related Projects in This Series
11. Self-Assessment Checklist
11.1 Understanding
- I can explain how OLED updates affect scan rate.
- I can describe how feature flags change firmware size.
11.2 Implementation
- RGB reflects layer state.
- OLED displays correct status.
11.3 Growth
- I can measure and tune scan latency.
12. Submission / Completion Criteria
Minimum Viable Completion:
- RGB per-layer colors enabled.
- OLED status screen shows layer and caps.
Full Completion:
- Scan rate stable (<10% drop).
- Firmware fits in flash.
Excellence (Going Above & Beyond):
- Reactive RGB effects with power limits.
- Idle dimming and documented performance metrics.