A high-performance, ultra-low-power, bare-metal wireless control system implemented on the cost-efficient CH32V003F4P6 (RISC-V) microcontroller. The project features a software-defined ASK/OOK physical layer protocol, custom frame decoding state machines, and deep standby power management (<10Β΅A idle).
It uses a one-way RF link at 433.92 MHz with the standard FS1000A transmitter and XY-MK-5V receiver modules to remotely control four independent momentary switches with dedicated LED status outputs.
The system is split into two independent nodes: the Transmitter Unit and the Receiver Unit.
ββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β TRANSMITTER UNIT β β RECEIVER UNIT β
β [CH32V003F4P6 MCU] β β [CH32V003F4P6 MCU] β
ββββββββββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββββββββ€
β PD0-PD3 βββΆ Input (Pull-Up) β β PD0-PD3 βββΆ Output (Push-Pull) β
β [4x Push Buttons] β β [4x Status LEDs] β
β β β β
β PC1 βββΆ RF DATA OUT βββββ [433 MHz] βββββΆβ PC1 βββΆ RF DATA IN β
β [FS1000A Modulator] β OOK Link β [XY-MK-5V Receiver]β
β β β β
β Power βββΆ Standby WFE Mode β β Power βββΆ Continuous Polling β
β (<10Β΅A Idle Current)β β (SysTick & TIM2) β
ββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
- Bare-Metal Firmware: Written entirely in C using the WCH
noneos-sdkframework, without any RTOS overhead, keeping flash and RAM footprint minimal. - Ultra-Low Power Standby (<10Β΅A): The transmitter MCU resides in deep standby mode (
WFEmode) when idle. External GPIO interrupts on PD0-PD3 wake the processor instantly. - Software-Defined OOK Physical Layer: Eliminates hardware serial overhead (no UART/SPI) by bit-banging precise pulse widths on the GPIO pins using microsecond-level hardware timer resolution.
- Resilient State-Machine Receiver: The receiver uses hardware Timer 2 (TIM2) in input capture style to decode the incoming raw bitstream, filter RF noise, validate addresses, verify XOR checksums, and timeout automatically.
Since cheap 433MHz ASK modules (like XY-MK-5V) feature Automatic Gain Control (AGC) that amplifies background RF noise when idle, standard serial framing (UART) is highly prone to corruption. This project implements a custom software-defined protocol designed specifically for noisy ASK/OOK mediums.
Instead of standard NRZ (Non-Return-to-Zero) encoding, bits are represented by unequal HIGH-to-LOW pulse ratios:
- Logic 0: 500 Β΅s HIGH pulse followed by a 500 Β΅s LOW pulse (total 1.0 ms).
- Logic 1: 1000 Β΅s HIGH pulse followed by a 500 Β΅s LOW pulse (total 1.5 ms).
Logic 0: βββββββ
β β
βββββββ΄ββββββ
ββ500ββΊββ500ββΊ (Β΅s)
Logic 1: ββββββββββββ
β β
ββββββββββββ΄ββββββ
βββ1000βββββΊββ500ββΊ (Β΅s)
This variable-length symbol mapping ensures the receiver's AGC remains locked and makes the protocol self-clocking.
Data is sent in structured packets of 4 bytes (32 bits):
| Byte | Field Name | Value | Purpose |
|---|---|---|---|
| 0 | Training Byte | 0x55 |
Sent before the packet to settle the receiver's Automatic Gain Control (AGC). |
| 1 | Preamble / Sync | 0xAA |
Used by the receiver to detect start of frame (SOF). |
| 2 | Device Address | 0x42 |
8-bit addressing to reject interference from nearby transmitters. |
| 3 | Button State | 0x0X |
4-bit momentary bitmap (Bits 0-3 mapping to buttons 1-4). |
| 4 | XOR Checksum | 0xXX |
Calculated as Preamble ^ Address ^ Button State to verify integrity. |
This custom PWM-based protocol is designed to address constraints specific to bare-metal microcontrollers (like the CH32V003). Here is a comparison of this project's approach versus the widely used Arduino RadioHead (RH_ASK) / VirtualWire library:
| Feature / Dimension | Custom PWM Protocol (This Project) | RadioHead (RH_ASK) |
|---|---|---|
| Simplicity | High: Simple edge-timing classification, direct bit-shifting, and XOR checksum. No lookup tables or complex algorithms. | Low: Requires 4-to-6 bit symbol conversion lookup tables, sub-bit clock sampling (PLL), and 16-bit CRC. |
| Memory Footprint | Ultra-Low: Fits in under 2KB of flash and 300 bytes of RAM, leaving headroom on resource-constrained chips. | Moderate: Requires library dependencies, buffers, and tables, which increase binary size. |
| Noise Immunity | Moderate: Relies on a training byte and timing windows to filter out noise, but susceptible to strong interference. | Excellent: Uses a 36-bit preamble to settle AGC and 4-to-6 bit encoding to maintain a constant signal balance (preventing AGC drift). |
| Error Detection | Basic: An 8-bit XOR checksum has a 1-in-256 false-positive rate. | Strong: A 16-bit CRC (CRC-CCITT) offers a 1-in-65,536 false-positive rate. |
| Clock Tolerance | Low: Sensitive to clock frequency errors (requires calibrated delays via SystemCoreClockUpdate()). |
High: Incorporates software clock recovery (PLL) to track and adjust for oscillator drifts. |
Key Takeaway:
- Use this Custom PWM Protocol for ultra-cheap, highly resource-constrained microcontrollers (like the CH32V003 with 2KB RAM / 16KB Flash) where execution efficiency, low code size, and low power sleep modes are the primary constraints.
- Use RadioHead for standard microcontrollers (e.g., ATmega328P/Arduino Uno) in environments with heavy RF congestion where maximum noise rejection and high-security packet validation are paramount.
To maximize battery life on the transmitter, the system employs several advanced power-saving techniques:
- Standby Mode via WFE: When no buttons are pressed, the transmitter configures GPIO Port D Pins 0-3 as external event sources (
EXTI_Mode_Event) on the falling edge (active low buttons). It then invokesPWR_EnterSTANDBYMode(PWR_STANDBYEntry_WFE). - Zero-Power ASK Idle State: The FS1000A transmitter is connected directly to PC1. Since ASK (Amplitude Shift Keying) transmits power only when the input line is HIGH, holding PC1
RESET(LOW) disables the transmitter's internal oscillator, reducing transmitter power consumption to 0 mA without requiring a dedicated power-gating MOSFET. - Unused GPIO Stabilization: All unused GPIO pins are configured with internal pull-ups (
GPIO_Mode_IPU) to prevent floating gate leakages that drain power. - Instant Wake & Debug Protection (PD1/SWIO Conflict): On the CH32V003,
PD1is the dedicated Single-Wire Interface (SWIO) pin used for programming and debugging. However, in thismainbranch,PD1is also mapped as a GPIO pin (Button 2 on the transmitter, LED 2 on the receiver). Since the transmitter immediately enters deep Standby sleep (WFE) when idle, the internal clocks and SWIO block are shut down, locking out the debugger. To permit in-circuit debugger connections, a standard 2-second startup safety delay is implemented at boot. The MCU detects the Low-Power Reset flag (RCC_FLAG_LPWRRST); if it is a fresh power-on or programmer reset (flag is reset), the MCU waits for 2 seconds to allow the WCH-LinkE debugger to connect. If it is a standby wakeup (flag is set), the MCU clears the flag and immediately runs, bypassing the safety delay to achieve low button latency (<15ms).
The receiver runs a robust non-blocking decoding loop:
- Edge Detection: Reads PC1 and measures timing between edges using
TIM2(1 Β΅s tick rate). - Pulsed Bit Demodulation:
- A falling edge triggers high-pulse duration verification.
- A rising edge triggers low-pulse verification (must be 300 Β΅s β 700 Β΅s).
- If low-pulse timing is valid, the high pulse is classified into
0or1based on timing thresholds.
- Preamble Synchronization: If in
STATE_SEARCH_PREAMBLE, incoming bits are shifted into a register. When the lower 8 bits match0xAA, the state transitions toSTATE_RECEIVE_DATA. - Validation: After 32 bits are received, the system validates the address and the XOR checksum. If valid, LED states on
PD0-PD3are immediately updated. - Bit Timeout: If the gap between bits exceeds 2.5 ms, the state machine resets. This prevents partial transmissions or noise spikes from causing stuck frames.
- Safety Momentary Timeout: If no valid packets are received for 100 ms, a timeout function automatically clears all LED outputs. This prevents the outputs from being stuck "ON" if the transmitter button release packet is lost in transit due to RF collision.
To ensure system-wide fault tolerance and defense against hardware lockups (such as unexpected EMI interference, power line glitches, or clock failures), both nodes run the hardware Independent Watchdog (IWDG):
- Timeout Period: Both nodes configure the IWDG with a prescaler of 32 and reload value of 1250. Ticking from the chip's internal Low-Speed Oscillator (LSI ~40kHz), this establishes a ~1.0-second recovery timeout window.
- Transmitter Node: The watchdog is initialized immediately after the 2-second debugger safety delay. It is fed at the start of each button scanning iteration. Before transitioning into deep Standby sleep (WFE), a final watchdog feed is performed. In deep Standby, the watchdog is automatically frozen by the chip's hardware configuration, preventing loop reset cycles.
- Receiver Node: The watchdog is initialized during boot after standard timer/GPIO setups and is fed continuously at the start of the
while (1)event decoding loop. Any unexpected lockup in bit processing triggers a hardware reset within 1 second.
Important
PD1 / SWIO Debug Pin Sharing Conflict: Because PD1 is shared between Button 2/LED 2 and the debugger interface, there is a risk of programmer lockout. For an alternative hardware wiring scheme that completely resolves this conflict by mapping the buttons and LEDs to PC4-PC7 and leaving PD1 dedicated exclusively to SWIO, switch to the simplified-pins branch (git checkout simplified-pins).
| MCU Pin | Function | Direction | Configuration | Hardware Connection |
|---|---|---|---|---|
| PD0 | Button 1 | Input | Internal Pull-Up | Tactile Switch (Active Low) |
| PD1 | Button 2 | Input | Internal Pull-Up | Tactile Switch (Active Low) |
| PD2 | Button 3 | Input | Internal Pull-Up | Tactile Switch (Active Low) |
| PD3 | Button 4 | Input | Internal Pull-Up | Tactile Switch (Active Low) |
| PC1 | RF DATA | Output | Push-Pull | FS1000A DATA IN |
| MCU Pin | Function | Direction | Configuration | Hardware Connection |
|---|---|---|---|---|
| PD0 | LED 1 | Output | Push-Pull | Red LED (via Current Limiter) |
| PD1 | LED 2 | Output | Push-Pull | Green LED (via Current Limiter) |
| PD2 | LED 3 | Output | Push-Pull | Blue LED (via Current Limiter) |
| PD3 | LED 4 | Output | Push-Pull | Yellow LED (via Current Limiter) |
| PC1 | RF DATA | Input | Floating | XY-MK-5V DATA OUT |
.
βββ platformio.ini # PlatformIO Multi-Environment Configuration
βββ src/
β βββ common/
β β βββ protocol.h # Shared RF parameters, sync word, and bit thresholds
β βββ transmitter/
β β βββ main.c # Transmitter entry point, low power manager, & modulator
β βββ receiver/
β βββ main.c # Receiver entry point, SysTick timer, & TIM2 demodulator
βββ docs/
βββ PRD.md # Product Requirements Document
βββ ARCHITECTURE.md # Detailed System Architecture
βββ HARDWARE_GUIDE.md # Hardware details, BOM, antenna & wiring guide
This project uses PlatformIO with the open-source community platform for CH32V MCUs.
- Install PlatformIO IDE (VS Code extension) or the PlatformIO CLI.
- A WCH-LinkE programmer (connected to the SWIO line of the CH32V003).
To build both environments:
pio runTo flash the transmitter node:
pio run -e transmitter --target uploadTo flash the receiver node:
pio run -e receiver --target uploadThis repository implements several production-grade firmware design methodologies:
- Custom Protocol Implementation: Implementing robust physical and link layers from scratch to solve real-world hardware limits (ASK noise).
- Deep System-Level Integration: Configuring interrupts, timers, low-power registers, and clock trees at the register level using vendor headers.
- Low-Power Optimization: Achieving sub-microamp consumption of peripherals (like RF transceivers) through clever driver and pin-state management.
- Defensive Firmware Design: Incorporating recovery delays, timeout watchdogs, packet validation, and automatic output shutdowns to prevent lockups.