at-os3 is a small event-driven AT modem firmware for CH32V003 boards driving
an SX1278-compatible LoRa radio.
The firmware exposes a UART AT command interface and keeps the radio control path deterministic:
interrupt -> event queue -> event loop -> FSM / handler
The current target is a CH32V003 wired to an SX1278 or Ebyte E32-style LoRa module. It is used as a raw LoRa modem by a Linux host.
at-os3 is built on the OS3 kernel model used in this repository: a small
deterministic event kernel, not a general-purpose RTOS.
OS3 is organized around a few rules:
- all progress starts from explicit events;
- interrupts only acknowledge hardware, capture minimal data, and enqueue one event;
- protocol behavior runs later from the event loop;
- stateful behavior lives in table-driven FSMs;
- hardware access stays in drivers;
- services and handlers must not create hidden background progression.
For the LoRa modem path, the intended execution shape is:
UART byte / radio IRQ
-> event_enqueue
-> event_loop
-> AT parser / LoRa FSM
-> bounded action
This is why the code is split into:
| Directory | Role |
|---|---|
core/ |
event queue, event loop, FSM engine, timer, console service |
drivers/ch32v003/ |
CH32V003 peripherals, UART, SPI, EXTI, SX1278 register access |
subfsm/ |
table-driven domain FSMs, including the AT parser and LoRa FSM |
handlers/ |
stateless event reactions |
The project constitution is in CONSTITUTION.md.
- UART AT command interface at 115200 8N1
- Raw LoRa RX/TX
- SX1278 register-level control
- Frequency, SF, bandwidth, coding rate, preamble, sync word, IQ inversion, CRC, LDRO, and implicit payload length configuration
- RX packet reports with RSSI, SNR, and frequency error estimate
- Event-driven interrupt handling
- Table-driven LoRa FSM
See doc/AT_COMMANDS.md for the command reference.
Current firmware target:
- MCU: official CH32V003 development board
- Radio: SX1278-compatible LoRa front end
- Tested module family: Ebyte E32-style SX1278 modules
- Host link: CH32V003 USART1 connected to a USB-UART bridge or host UART
- Default UART: 115200 baud, 8 data bits, no parity, 1 stop bit
Example CH32V003 development board:
https://fr.aliexpress.com/item/1005005269690018.html
Example Ebyte LoRa module:
https://fr.aliexpress.com/item/1005004447877680.html
Equivalent CH32V003 boards can be used if the pins required by doc/PINOUT.md are available.
Important pins:
- Radio SPI/control:
PC0..PC7 - Host UART:
PD5TX,PD6RX - LEDs:
PD4heartbeat,PD2radio activity
See doc/PINOUT.md for the complete pinout.
The build script expects a RISC-V embedded toolchain in PATH:
riscv32-unknown-elf-asriscv32-unknown-elf-ldriscv32-unknown-elf-objcopyriscv32-unknown-elf-sizeriscv32-unknown-elf-nm
The firmware is assembled as RV32EC with the zicsr extension and the
ilp32e ABI.
From the repository root:
./run.shBuild output is written to:
build/ch32v003/kernel.elf
build/ch32v003/kernel.bin
The script also prints section sizes and .kernel_init entries.
Flash the generated binary with your CH32V003 programming tool. For a local
minichlink setup, the command shape is:
minichlink -w build/ch32v003/kernel.bin flashminichlink is part of the ch32fun project:
https://github.com/cnlohr/ch32fun
Use the exact command required by your installed programmer and target board.
After flashing, connect the host UART at 115200 8N1 and send:
AT
Expected response:
+OK
Read the firmware version:
AT+VER?
Expected response:
+VER=at-os3-0.1.0
+OK
The repository includes tools/test_radio.py, a small
host-side serial test script for RX/TX checks. It requires Python 3 and
pyserial:
python3 -m pip install pyserialProbe the modem and SX1278 SPI link:
python3 tools/test_radio.py /dev/ttyACM0 --probeListen on a raw LoRa profile:
python3 tools/test_radio.py /dev/ttyACM0 \
--freq 436995000 --sf 8 --bw 62.5 --cr 7 --sw 0x12 \
--rx-seconds 120Transmit text on the same profile:
python3 tools/test_radio.py /dev/ttyACM0 \
--freq 436995000 --sf 8 --bw 62.5 --cr 7 --sw 0x12 \
--send-text pingTransmit a hexadecimal payload:
python3 tools/test_radio.py /dev/ttyACM0 \
--freq 436995000 --sf 8 --bw 62.5 --cr 7 --sw 0x12 \
--send 70696E67Example raw LoRa RX profile:
AT+MODE=1
AT+BAND=436995000
AT+PARAMETER=8,6,3,8
AT+PKT=1,0,0
AT+SYNCWORD=18
AT+IQI=0
AT+MODE=0
Received packets are emitted as:
+RCV=<addr>,<len>,<hex_payload>,<rssi_dbm>,<snr_db>,<freq_err_hz>
PHY CRC failures are emitted as:
+ERR=1
core/ hardware-agnostic event/FSM/kernel services
drivers/ch32v003/ CH32V003 hardware drivers and SX1278 driver
subfsm/ table-driven domain FSMs
handlers/ stateless event handlers
link/ linker scripts
doc/ AT command and hardware documentation
run.sh build script
Copyright (C) 2026 Dominique CARREL (netmonk) netmonk@netmonk.org.
Firmware source code, build scripts, and hardware documentation are licensed under GPL-3.0-or-later.
The OS3 constitution text in CONSTITUTION.md is licensed separately under CC BY-ND 4.0, because the constitution is the project's canonical design contract and must remain attributable and non-mutated.
See LICENSE and LICENSE-CONSTITUTION.md.
- The firmware is a raw LoRa modem, not a network stack.
AT+ADDRESSandAT+NETWORKIDare stored for host-side compatibility, but raw LoRa RX/TX payloads are not framed with a network header.- Ebyte module RF performance outside its specified operating band depends on the module RF front end, not only on the SX1278 synthesizer range.