0x0a IR Rust Driver
This repository contains a Bare-Metal Rust driver for an IR (Infrared) Receiver decoding the NEC Protocol on the RP2350 (and RP2040) microcontrollers, implemented strictly using a GPIO pin and a hardware timer to measure pulse widths.
It includes:
- A demo (
src/main.rs) that waits for IR signals and decodes/prints the received command via UART. - A reusable library module (
src/ir.rs) providing a hardware-agnosticir_libcontaining the timing thresholds, bit accumulation logic, frame validation, and string formatting routines. - Board initialization logic (
src/board.rs).
🚀 Getting Started from Scratch
If you're starting with a fresh machine, follow these exact steps to install the toolchain, build the code, and flash it to your microcontroller.
1. Install Rust
First, install rustup (the Rust toolchain installer) if you haven't already:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Note: Restart your terminal or run source $HOME/.cargo/env after this finishes.
Ensure your Rust compiler is up to date:
rustup update
2. Install the Target Architecture
This project is configured for the RP2350 (ARM Cortex-M33). We need to install the cross-compilation target for it:
rustup target add thumbv8m.main-none-eabihf
(If you were targeting the RP2040, you would use thumbv6m-none-eabi instead).
3. Install Build Tools
You will need a few extra tools to help link and format the firmware for the RP-series chips.
Install flip-link (adds zero-cost stack overflow protection):
cargo install flip-link
Install picotool (used by cargo run to flash the chip):
- macOS:
brew install picotool - Linux/Windows: Follow the official Raspberry Pi documentation to install
picotoolor build it from source.
4. Building the Code
To compile the code for the microcontroller, simply run:
cargo build
To build a highly optimized release version (smaller and faster):
cargo build --release
5. Flashing to the Microcontroller
This project is pre-configured in .cargo/config.toml to use picotool as the custom runner.
To flash the code:
- Hold down the BOOTSEL button on your RP2350 board.
- Plug it into your computer via USB (or press the RUN/RESET button while holding BOOTSEL).
- Run the following command:
cargo run --release
cargo will compile the code and automatically use picotool to upload the .elf file directly to your board and start executing it!
6. Testing on the Host
Because the NEC protocol logic (timing constants, bit shifting, checksum validation, and string formatting) is separated into a reusable math library without touching hardware registers, you can run the unit tests natively on your computer!
However, because this project sets a default bare-metal target (thumbv8m.main-none-eabihf) in .cargo/config.toml, running a plain cargo test will fail because the standard library doesn't exist on the microcontroller. You must explicitly tell Cargo to compile the tests for your host computer's processor architecture:
Mac (Apple Silicon):
cargo test --lib --target aarch64-apple-darwin
Linux (Intel/AMD 64-bit):
cargo test --lib --target x86_64-unknown-linux-gnu
Windows (64-bit):
cargo test --lib --target x86_64-pc-windows-msvc
🧠 Code Walkthrough
This section explains exactly how the code works, where the entry point is, and traces the flow of execution as if you were stepping through it line-by-line.
1. The Entry Point (src/main.rs)
Unlike a standard computer program, bare-metal microcontrollers do not have an operating system to call main(). Instead, we use the #[entry] macro from the HAL (Hardware Abstraction Layer) to define the very first function that runs after the chip boots up.
main() -> !: This is the absolute start of our code. It takes ownership of all the hardware peripherals (hal::pac::Peripherals::take().unwrap()) and immediately passes them intoboard::run(...). The-> !means this function never returns (because embedded devices run in an infinite loop).
2. Board Initialization (src/board.rs)
Once execution enters board.rs, we initialize the system clocks, pins, UART (for logging), SysTick (for delay), and a hardware timer (for microsecond precision measurement of IR pulses).
run(...): The master setup function. Calls the helper initialization functions below, prints an initialization banner over UART, and enters the infinite polling loop waiting for IR signals.poll_receiver(...): Called repeatedly in the main loop. It invokes the decoding sequence (ir_getkey) and, if a valid command is returned, formats it and writes it to UART.ir_getkey(...): The high-level function for receiving a full NEC frame. It waits for the 9ms leader mark and 4.5ms leader space, then attempts to read all 32 data bits, and finally validates the frame using the library.wait_leader(...): Executes the strict NEC protocol synchronization: waits for the line to go idle, then validates the lengths of the incoming leader mark (active low) and leader space (idle high).read_nec_bit(...): Waits for the standard 562.5µs bit mark, measures the duration of the following space, and calls the hardware-agnosticaccumulate_nec_bit()function in their_libto pack a 1 (long space) or 0 (short space) into the byte array.wait_for_level(...): A robust spin-loop that takes a timestamp, checks the GPIO state, and calculates elapsed time, returning the total duration if the state changes before the timeout.
3. The Reusable IR Library (src/ir.rs)
Because distinguishing between logical 0s and 1s requires precise duration thresholds, and manipulating bits across 4 bytes is tedious, this module handles all pure-logic data validation.
accumulate_nec_bit(...): Shifts the current bit index and inserts a1if the measured pulse duration exceeds the 1.2ms threshold (a standard NEC "1" space is ~1.69ms).validate_nec_frame(...): The NEC protocol transmits the 8-bit Address, the inverse Address, the 8-bit Command, and the inverse Command. This function verifies thatbyte0 + byte1 == 0xFFandbyte2 + byte3 == 0xFFto ensure data integrity, then returns the Command byte.format_command(...): Efficiently constructs the final ASCII string containing the hex and decimal representation of the command (e.g.,NEC command: 0x45 (69)) without allocating memory, allowing theboard.rscode to pipe the raw bytes directly to UART.