Project 1: The “Architectural” Blink (RISC-V Assembly)
Build an LED blink that touches GPIO by direct memory-mapped register writes using inline RISC-V assembly on the XIAO ESP32-C3/C6.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C with inline RISC-V assembly |
| Alternative Programming Languages | Rust (inline asm), Zig |
| Coolness Level | Very High |
| Business Potential | Low (learning project) |
| Prerequisites | C basics, bitwise ops, ESP-IDF build/flash, reading a datasheet |
| Key Topics | MMIO, volatile semantics, RISC-V load/store, GPIO register model |
1. Learning Objectives
By completing this project, you will:
- Explain how a RISC-V store instruction becomes a voltage change on a GPIO pin.
- Translate a GPIO register map into correct bitmasks and MMIO addresses.
- Write safe inline assembly with proper compiler constraints and memory clobbers.
- Prove correctness with serial logs that show exact register addresses and values.
- Identify and avoid common hazards: read-modify-write bugs, pin mux conflicts, and compiler reordering.
2. All Theory Needed (Per-Concept Breakdown)
2.1 Concept 1: Memory-Mapped I/O (MMIO) and Volatile Register Access
Fundamentals
Memory-mapped I/O means hardware peripherals are controlled by reading and writing ordinary memory addresses. Instead of calling a function to turn on a pin, you write a value to a specific address, and that write is intercepted by the peripheral. The CPU does not know it is “special” memory; it simply issues a store over the system bus. In embedded systems, this is the dominant control method. The compiler must be told that these addresses are not normal RAM, otherwise it may remove or reorder accesses. That is why registers are declared volatile. Volatile tells the compiler that every read and write has side effects and must remain in the exact order you wrote in C. On the ESP32-C3/C6, the GPIO peripheral has write-one-to-set and write-one-to-clear registers that allow you to set or clear individual bits without a read-modify-write cycle. Understanding that register model is essential to avoid flicker, race conditions, and unintended toggles.
Deep Dive into the Concept
MMIO is a design pattern that comes from early computer architecture and persists because it is simple and fast. A peripheral is mapped into the physical address space. When the CPU performs a load or store to that address range, the system bus routes the transaction to the peripheral instead of DRAM. On a microcontroller, the bus fabric is often a simple AHB/APB or a vendor-specific crossbar. The GPIO peripheral sits on that bus with a base address, and each register is at an offset. For example, GPIO_OUT_W1TS (write-one-to-set) might be at GPIO_BASE + 0x08, and GPIO_OUT_W1TC (write-one-to-clear) at GPIO_BASE + 0x0C. Writing a 1 to bit N sets or clears the output latch for GPIO N. The “write-one” pattern avoids read-modify-write hazards. If you used a read-modify-write on GPIO_OUT, you would read the entire register, change one bit, and write it back. If an interrupt toggled another pin between your read and write, you would accidentally overwrite its state. W1TS/W1TC turns the operation into an idempotent bit-set or bit-clear that does not disturb other pins.
Volatile semantics are subtle. Volatile only prevents the compiler from removing or reordering accesses to that object. It does not make operations atomic, and it does not insert memory barriers for multi-core systems. For a single-core ESP32-C3, volatile is usually enough to keep your writes in order, but you still must consider the CPU pipeline, the bus write buffer, and peripheral synchronization. Some MCUs require a read-back to ensure a write has completed; on the ESP32 family, many peripherals are clocked from the APB, and writes are posted. If you need to guarantee that a write has taken effect before proceeding (for example, before entering sleep), you may need to read the same register or insert a small delay. In this project, the delay between toggles provides enough time, but it is important to understand that delay is not a formal barrier.
Another common MMIO hazard is alignment and access width. If the register is 32 bits wide, you must use a 32-bit store. A byte store might be ignored or may only update part of the register. Some peripherals have side effects on read: reading a status register can clear an interrupt flag. This is why you must understand the register description, not just its address. GPIO registers are usually safe to read, but interrupt and event registers often are not. On ESP32, the GPIO output registers are simple latches, but the IO_MUX and pin function registers have fields that control pull-up/down, drive strength, and input enable. If you write to output without enabling output in the direction register, nothing happens. That is still MMIO, but it is MMIO applied to the wrong register. The consequence is a “silent” failure where your code runs but the hardware does nothing.
Finally, MMIO ties software to a specific silicon version. If you move from ESP32-C3 to ESP32-C6, the base addresses and bit positions may shift. The SDK hides that, but this project intentionally bypasses the SDK. That means your code must be accurate to the TRM (Technical Reference Manual) for your chip. You should always define addresses as constants with clear comments and never hardcode them in multiple places. Treat MMIO like a contract: correct address, correct width, correct order. When you follow that contract, a single store instruction can change the physical state of the board.
How this fits on projects
This project is a pure MMIO exercise. You will use MMIO to set GPIO direction, then use W1TS/W1TC registers to toggle the LED. You will also re-use the MMIO mental model in the USB bridge project and the oscilloscope project for peripheral setup.
Definitions & Key Terms
- MMIO: Memory-mapped I/O, using memory addresses to control hardware.
- Volatile: A C qualifier that prevents compiler optimizations that remove or reorder accesses.
- Register map: The list of peripheral registers and their offsets from a base address.
- W1TS/W1TC: Write-one-to-set / write-one-to-clear register semantics.
- Read-modify-write (RMW): A read followed by a write that can overwrite concurrent changes.
- Posting: Writes that are buffered and complete later.
- Peripheral base address: The starting address of a peripheral’s register block.
Mental Model Diagram (ASCII)
CPU core
|
| store instruction
v
System bus -------> GPIO peripheral
|
v
Output latch
|
v
LED
How It Works (Step-by-Step)
- Look up the GPIO base address and the offsets of output and direction registers in the TRM.
- Compute a bitmask for the LED pin (1 « GPIO_NUM).
- Write the bitmask to the GPIO direction register to enable output.
- Write the bitmask to W1TS to set the pin high.
- Delay for a visible interval.
- Write the bitmask to W1TC to set the pin low.
- Repeat forever.
Minimal Concrete Example
#define GPIO_BASE 0x60004000u
#define GPIO_OUT_W1TS (GPIO_BASE + 0x08)
#define GPIO_OUT_W1TC (GPIO_BASE + 0x0C)
#define GPIO_ENABLE_W1TS (GPIO_BASE + 0x20)
static inline void mmio_write(uint32_t addr, uint32_t val) {
*(volatile uint32_t *)addr = val;
}
void app_main(void) {
const uint32_t led_mask = (1u << 20); // Example LED pin
mmio_write(GPIO_ENABLE_W1TS, led_mask);
while (1) {
mmio_write(GPIO_OUT_W1TS, led_mask);
vTaskDelay(pdMS_TO_TICKS(500));
mmio_write(GPIO_OUT_W1TC, led_mask);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
Common Misconceptions
- “Volatile makes the write atomic.” It only prevents compiler reordering.
- “Any byte write will work.” Many registers require 32-bit writes.
- “The base address is the same across chips.” It is not; check the TRM.
Check-Your-Understanding Questions
- Why are W1TS/W1TC registers safer than a read-modify-write on GPIO_OUT?
- What does the
volatilequalifier guarantee and what does it not guarantee? - Why might a GPIO write succeed in code but not change the pin voltage?
- How could a posted write affect timing-sensitive code?
Check-Your-Understanding Answers
- Because they only affect the bits you set to 1 and do not overwrite other pins.
- It guarantees the compiler will not remove or reorder the access, but it does not make it atomic or insert barriers.
- The pin may not be configured as output, or the IO mux may route a different function.
- The CPU might continue before the peripheral sees the write, so timing assumptions could break.
Real-World Applications
- Writing to SPI, I2C, or UART registers in custom drivers.
- Handling memory-mapped registers in PCI devices on a PC.
- Bare-metal bring-up of new silicon where no SDK exists.
Where You’ll Apply It
- See Section 3.2 Functional Requirements and Section 4.4 Data Structures in this project.
- Also used in: P09 USB-to-UART Bridge, P10 Web-Based Oscilloscope.
References
- “Computer Systems: A Programmer’s Perspective” (memory chapter)
- “Making Embedded Systems” (hardware access chapters)
- ESP32-C3 Technical Reference Manual (GPIO chapter)
Key Insights
A GPIO toggle is just a correctly ordered memory write to a special address.
Summary
MMIO gives you the most direct control of hardware. When you combine correct register addresses with volatile semantics and W1TS/W1TC usage, you can change a pin with a single store instruction and avoid race conditions.
Homework/Exercises to Practice the Concept
- Locate the GPIO output register map in the ESP32-C3 TRM and list the W1TS/W1TC addresses.
- Explain why a read-modify-write could break if an interrupt toggles another pin.
Solutions to the Homework/Exercises
- The TRM lists GPIO base and offsets; W1TS/W1TC are at fixed offsets from the base.
- The read captures a stale value; the write overwrites changes that occurred between read and write.
2.2 Concept 2: RISC-V Load/Store, Inline Assembly, and Address Construction
Fundamentals
RISC-V is a load/store architecture: only load and store instructions access memory, while arithmetic happens in registers. That means every interaction with a peripheral register is a load or store, and you must build the target address in a register first. RISC-V 32-bit immediate fields are limited in size, so you cannot encode a full 32-bit address in a single instruction. Instead, you use lui to load the upper bits and addi to add the lower bits. Inline assembly in C lets you place those instructions in your program, but you must also tell the compiler which registers you use and which memory you touch. Otherwise, the compiler can reorder or optimize around your assembly in ways that break hardware access. The skill here is not just writing assembly, but writing assembly that cooperates with the compiler.
Deep Dive into the Concept
A typical RISC-V core has 32 integer registers (x0-x31). x0 is hardwired to zero; the rest are general purpose. The calling convention (ABI) assigns roles: a0-a7 for arguments, t0-t6 for temporaries, s0-s11 for saved registers. Inline assembly in C is compiled by GCC or Clang and must follow those ABI rules. If you clobber a saved register without telling the compiler, you corrupt state. If you clobber memory but do not declare a memory clobber, the compiler may move loads/stores across your asm block, which can break MMIO ordering. The safest approach is to either keep inline assembly very small and use input/output constraints, or to write a separate assembly function with a defined ABI.
Address construction is a key RISC-V detail. A 32-bit address is split into upper 20 bits and lower 12 bits. The lui instruction loads a 20-bit immediate into the upper bits of a register, shifting left by 12. The addi instruction adds a 12-bit signed immediate. That means you must account for sign extension in the lower bits. Toolchains often provide la pseudo-instructions that handle this, but in inline assembly you frequently see lui t0, %hi(addr) followed by addi t0, t0, %lo(addr). If you get the immediate wrong by even 4 bytes, you write to the wrong register and the MCU may crash or silently fail. It is common to use li for small constants, but for addresses you must use the hi/lo split or let the assembler do it.
Another important factor is alignment. A 32-bit store should be 4-byte aligned; otherwise, the CPU may generate an exception. MMIO regions often require word accesses for safety. The ESP32-C3 is tolerant, but it is still good practice to align addresses. In the context of GPIO, you should only use sw (store word) to the GPIO registers and avoid sb (store byte) unless the register explicitly supports byte access. When you compile with optimization, the compiler may choose to inline or reorder code. Without proper volatile or asm volatile, your store instruction might be removed. asm volatile tells the compiler that the assembly has side effects. But you still need a memory clobber to prevent memory operations from moving across it.
Timing and pipelining are also relevant. A store instruction does not necessarily complete before the next instruction; the CPU may continue while the bus transaction finishes. If you need to guarantee ordering between two MMIO writes, you can insert a fence instruction. Most microcontroller firmware does not need explicit fences because there is a single core and the peripheral bus is simple, but if you toggle pins in a tight loop you may observe differences depending on compiler optimization and bus latency. Inline assembly gives you precise control, but also makes you responsible for all those details. That is why the blink project is so educational: it is a microcosm of bare-metal programming. You write a few lines of assembly, but you are engaging with ABI, instruction encoding, and compiler behavior.
How this fits on projects
The blink uses inline assembly to load the GPIO register address and perform a store. The same skills apply when writing fast interrupt handlers, bit-banging protocols, or reading a register without compiler interference.
Definitions & Key Terms
- Load/store ISA: An architecture where memory is accessed only by load and store instructions.
- ABI: Application Binary Interface, defines register usage and calling convention.
- LUI/ADDI: Instructions used to build 32-bit addresses from immediates.
- Inline assembly: Assembly embedded in C with compiler constraints.
- Memory clobber: Tells the compiler that the asm touches memory.
- Fence: Instruction that enforces ordering of memory operations.
Mental Model Diagram (ASCII)
[ C code ]
|
v
[ compiler ] -> [ inline asm block ] -> [ RISC-V instructions ]
| | |
LUI ADDI SW
How It Works (Step-by-Step)
- The compiler parses the inline assembly and reserves registers for operands.
luiloads the upper 20 bits of the MMIO address.addiadds the lower 12 bits to form the full address.swstores the bitmask to the MMIO address.- The bus routes the store to the GPIO peripheral.
- The peripheral updates its output latch and the LED changes state.
Minimal Concrete Example
static inline void gpio_set_mask(uint32_t mask) {
asm volatile (
"lui t0, %%hi(0x60004008)\n"
"addi t0, t0, %%lo(0x60004008)\n"
"sw %0, 0(t0)\n"
:
: "r" (mask)
: "t0", "memory"
);
}
Common Misconceptions
- “Inline asm is always faster.” It can be slower if it blocks compiler optimizations.
- “A 32-bit address fits in one instruction.” It does not in RV32.
- “asm volatile alone prevents reordering.” Without a memory clobber, other loads/stores can move around.
Check-Your-Understanding Questions
- Why do you need
lui+addito construct an MMIO address? - What happens if you forget the
memoryclobber in inline assembly? - Which registers can you safely clobber in inline assembly and why?
- When would you need a
fenceinstruction?
Check-Your-Understanding Answers
- Because RV32 immediates are too small to encode a full 32-bit address.
- The compiler might move memory operations across the asm block, breaking ordering.
- Temporary registers (t0-t6) can be clobbered if declared, saved registers must be preserved.
- When ordering between MMIO operations matters across cores or bus buffering.
Real-World Applications
- Boot ROM bring-up code and minimal device drivers.
- Context switch code in RTOS ports.
- Fast GPIO bit-banging for protocols like WS2812.
Where You’ll Apply It
- See Section 5.1 Development Environment Setup for toolchain details and Section 5.4 Concepts You Must Understand First.
- Also used in: P02 Deep Sleep Champion and P08 Wi-Fi Sniffer.
References
- “Computer Organization and Design RISC-V” (ISA chapters)
- “The RISC-V Reader” (instruction encoding and ABI)
- GCC inline assembly documentation
Key Insights
Inline assembly works only when you respect the ABI and tell the compiler exactly what you touched.
Summary
RISC-V load/store rules, immediate limitations, and compiler constraints define how you must write inline assembly. When done correctly, your assembly becomes a predictable, precise hardware control tool.
Homework/Exercises to Practice the Concept
- Write a tiny function that loads an address with
lui/addiand reads a 32-bit value. - Modify the example to toggle two different GPIO bits without corrupting registers.
Solutions to the Homework/Exercises
- Use
lwinstead ofswwith the same address construction and return the value. - Use two bitmasks and two
swinstructions, or OR the masks before the store.
3. Project Specification
3.1 What You Will Build
A firmware project that toggles the on-board LED using only direct MMIO writes (no SDK GPIO helpers) and logs the exact register addresses and values used. The firmware must compile with ESP-IDF for the XIAO ESP32-C3/C6 and run reliably with deterministic timing.
3.2 Functional Requirements
- Register Mapping: Use correct GPIO base address and offsets from the chip TRM.
- Direction Setup: Set the LED pin as output using the appropriate enable register.
- Write-One Semantics: Use W1TS and W1TC registers to set and clear the LED pin.
- Inline Assembly: Perform at least one GPIO write via inline RISC-V assembly.
- Logging: Print register addresses and values to the serial console.
- Deterministic Timing: Use a fixed delay (e.g., 500 ms) for a stable blink rate.
3.3 Non-Functional Requirements
- Performance: Blink interval stays within +/- 5 percent of the configured delay.
- Reliability: Runs for 10 minutes without crash or watchdog reset.
- Usability: Clear instructions for pin selection and board variant.
3.4 Example Usage / Output
I (0) ASM_BLINK: GPIO_ENABLE_W1TS = 0x60004020
I (0) ASM_BLINK: GPIO_OUT_W1TS = 0x60004008
I (0) ASM_BLINK: GPIO_OUT_W1TC = 0x6000400C
I (10) ASM_BLINK: LED bit = 0x00100000
[LED ON]
[LED OFF]
3.5 Data Formats / Schemas / Protocols
- Serial Log Line:
LEVEL (timestamp) TAG: message - Register Print:
NAME = 0xXXXXXXXX(uppercase hex, fixed width)
3.6 Edge Cases
- Incorrect LED GPIO number for the board variant.
- Using a pin that is input-only.
- Forgetting to set output enable before toggling.
- Misaligned register address (results in exception).
3.7 Real World Outcome
A working board with a blinking LED and a serial log that proves the exact addresses and bitmasks used.
3.7.1 How to Run (Copy/Paste)
cd /Users/douglas/Sites/learning_journey_c/project_based_ideas/HARDWARE_EMBEDDED/LEARN_SEEED_XIAO_ECOSYSTEM_DEEP_DIVE/firmware/p01_mmio_blink
idf.py set-target esp32c3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor
3.7.2 Golden Path Demo (Deterministic)
- Fix delay to 500 ms.
- Disable log timestamps or keep them constant by printing your own count.
Expected serial log (example):
COUNT=0 GPIO_OUT_W1TS=0x60004008 MASK=0x00100000 LED=ON
COUNT=1 GPIO_OUT_W1TC=0x6000400C MASK=0x00100000 LED=OFF
COUNT=2 GPIO_OUT_W1TS=0x60004008 MASK=0x00100000 LED=ON
3.7.3 Failure Demo (Bad GPIO)
If you set an invalid GPIO number (e.g., a reserved or input-only pin), the firmware should log an error and stop toggling:
E (0) ASM_BLINK: GPIO 19 is input-only on this board
E (0) ASM_BLINK: Aborting blink loop
3.7.4 If CLI
This project does not include a standalone CLI tool. Exit codes are not applicable.
3.7.5 If Web App
Not applicable.
3.7.6 If API
No API is exposed. Error JSON shape is not applicable.
3.7.7 If GUI / Desktop / Mobile
Not applicable.
3.7.8 If TUI
Not applicable.
4. Solution Architecture
4.1 High-Level Design
[main loop]
|
v
[MMIO init] -> [GPIO enable] -> [toggle using W1TS/W1TC] -> [delay]
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Register map constants | Define base and offsets | Use TRM, one source of truth |
| MMIO write helper | Safely write volatile addresses | Inline asm + memory clobber |
| Blink loop | Toggle LED with fixed timing | W1TS/W1TC, deterministic delay |
| Logger | Print addresses and masks | Fixed format, easy to verify |
4.3 Data Structures (No Full Code)
struct gpio_regs {
volatile uint32_t out;
volatile uint32_t out_w1ts;
volatile uint32_t out_w1tc;
// ... other registers
};
4.4 Algorithm Overview
Key Algorithm: MMIO Blink
- Compute
led_mask = 1 << LED_GPIO. - Enable output for the LED pin.
- Loop: set bit, delay, clear bit, delay.
Complexity Analysis:
- Time: O(1) per toggle
- Space: O(1)
5. Implementation Guide
5.1 Development Environment Setup
# ESP-IDF install (example)
python3 -m venv .venv
source .venv/bin/activate
pip install -r $IDF_PATH/requirements.txt
idf.py set-target esp32c3
5.2 Project Structure
p01_mmio_blink/
+-- CMakeLists.txt
+-- main/
| +-- app_main.c
| +-- mmio_gpio.h
+-- README.md
5.3 The Core Question You’re Answering
“How does a single store instruction turn into a physical voltage change on a pin?”
5.4 Concepts You Must Understand First
Stop and research these before coding:
- MMIO and volatile semantics
- RISC-V address construction (lui/addi)
- GPIO direction and output register model
5.5 Questions to Guide Your Design
- Which GPIO pin is connected to the LED on your exact XIAO variant?
- Which register enables output for that pin?
- What address and mask will you print in logs to prove correctness?
5.6 Thinking Exercise
Draw the bus path for a store instruction: CPU -> bus -> GPIO register -> output latch -> pad -> LED.
5.7 The Interview Questions They’ll Ask
- Why does
volatilematter for MMIO? - What is the difference between W1TS/W1TC and GPIO_OUT?
- How does RISC-V load/store affect peripheral access?
5.8 Hints in Layers
Hint 1: Verify the LED pin with a normal SDK GPIO example first.
Hint 2: Use W1TS/W1TC to avoid read-modify-write hazards.
Hint 3: Add a memory clobber in inline assembly.
Hint 4: Log the exact address and mask to confirm correctness.
5.9 Books That Will Help
| Topic | Book | Chapter | |——|——|———| | MMIO and memory | Computer Systems: A Programmer’s Perspective | Memory chapter | | RISC-V ISA | Computer Organization and Design RISC-V | ISA chapters | | Embedded debugging | Making Embedded Systems | Debugging chapters |
5.10 Implementation Phases
Phase 1: Register Discovery (2-4 hours)
Goals:
- Identify correct GPIO base and LED pin.
- Build a constant table.
Tasks:
- Read the TRM and note the GPIO register map.
- Confirm the LED pin from the board schematic.
Checkpoint: You can print the base address and LED mask with confidence.
Phase 2: MMIO Toggle (1 day)
Goals:
- Toggle LED using W1TS/W1TC.
- Log register addresses and values.
Tasks:
- Write the MMIO helper.
- Implement the blink loop with fixed delay.
Checkpoint: LED toggles and serial logs match expected format.
Phase 3: Inline Assembly + Robustness (1-2 days)
Goals:
- Move a write into inline assembly.
- Add guard checks for invalid pins.
Tasks:
- Replace one MMIO write with inline asm.
- Add a validation function for LED pin.
Checkpoint: Inline asm path works and logs are deterministic.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | GPIO control method | GPIO_OUT RMW vs W1TS/W1TC | W1TS/W1TC | Safer with interrupts | | Inline asm location | Inlined vs separate .S file | Inline for first pass | Faster feedback | | Delay source | busy loop vs vTaskDelay | vTaskDelay | Stable timing with RTOS tick |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | Validate masks and addresses | Host-side constants checks | | Integration Tests | Run on hardware | LED blink and serial log | | Edge Case Tests | Invalid pin and misaligned address | Error logs, no crash |
6.2 Critical Test Cases
- LED Pin Valid: Correct pin number toggles LED.
- Invalid Pin: Firmware logs error and stops toggling.
- W1TS/W1TC Path: No read-modify-write is used.
6.3 Test Data
LED_GPIO=20
LED_MASK=0x00100000
GPIO_OUT_W1TS=0x60004008
GPIO_OUT_W1TC=0x6000400C
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution | |——–|———|———-| | Wrong GPIO pin | LED never toggles | Check schematic and pin map | | Missing output enable | LED stuck low | Set direction register | | Missing volatile or clobber | Compiler removes write | Use volatile and memory clobber |
7.2 Debugging Strategies
- Serial log verification: Print addresses and masks and compare to TRM.
- SDK sanity check: Toggle LED using SDK GPIO to verify hardware.
7.3 Performance Traps
- Busy loops can starve the RTOS and trigger watchdog resets. Use RTOS delay where possible.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a second GPIO pin and alternate the blink.
- Print the current cycle count between toggles.
8.2 Intermediate Extensions
- Move the MMIO access into a separate assembly file and call it from C.
- Implement a timer interrupt that toggles the LED without busy waiting.
8.3 Advanced Extensions
- Write a minimal GPIO driver that supports multiple pins and atomic operations.
- Port the same code to ESP32-C6 and document register differences.
9. Real-World Connections
9.1 Industry Applications
- Bare-metal bring-up of new chips before SDKs exist.
- Low-level driver work in safety-critical systems.
9.2 Related Open Source Projects
- esp-idf - Official ESP32 SDK with GPIO driver.
- zephyr - Open RTOS with GPIO device drivers.
9.3 Interview Relevance
- Memory-mapped I/O and volatile semantics are common in embedded interviews.
- Understanding register maps and bitmasks is a core firmware skill.
10. Resources
10.1 Essential Reading
- “Computer Organization and Design RISC-V” - ISA and load/store model
- “Making Embedded Systems” - Hardware access and debugging
10.2 Video Resources
- “RISC-V from the Ground Up” (lecture series)
- “Embedded Systems - Memory Mapped I/O” (conference talk)
10.3 Tools & Documentation
- ESP32-C3 Technical Reference Manual
- ESP-IDF GPIO driver documentation
10.4 Related Projects in This Series
- P02 Deep Sleep Champion - power measurements still rely on accurate GPIO control
- P09 USB-to-UART Bridge - register-level peripheral control
11. Self-Assessment Checklist
11.1 Understanding
- I can explain how an MMIO write reaches a peripheral.
- I can build a 32-bit address with
luiandaddi. - I can describe why W1TS/W1TC avoids RMW hazards.
11.2 Implementation
- The LED toggles using only MMIO writes.
- Serial logs show the correct addresses and masks.
- The firmware runs for 10 minutes without failure.
11.3 Growth
- I documented a root cause for at least one bug.
- I can explain this project in an interview.
12. Submission / Completion Criteria
Minimum Viable Completion:
- LED toggles via MMIO (no SDK GPIO calls).
- Serial log prints register addresses and masks.
- Inline assembly is used for at least one MMIO write.
Full Completion:
- All minimum criteria plus:
- Deterministic log output with fixed delay.
- Error handling for invalid pin selection.
Excellence (Going Above & Beyond):
- Port to a second XIAO variant and document differences.
- Provide a short write-up explaining MMIO hazards and fixes.