OS3 - RISC-V Event Kernel
Bare-metal event-driven kernel for CH32V003 (RV32EC, 48MHz, 16KB flash, 2KB RAM)
Features
- Table-driven FSM engine with explicit state transitions
- Event queue with O(1) dispatch
- Tickless software timers (SysTick)
- UART console (115200 8N1)
- Debounced EXTI edge detection
- Heartbeat LED
Build
./run.sh
minichlink -w build/kernel.bin flash -b
Requires: riscv32-unknown-elf-gcc toolchain
Directory Structure
core/ - Event loop, FSM engine, timer, console services
drivers/ - CH32V003 hardware (UART, EXTI, GPIO, clock)
subfsm/ - Table-driven finite state machines
handlers/ - Stateless event handlers
link/ - Linker scripts
Hardware Pins
| Pin |
Function |
| PD3 |
Button input (EXTI, falling edge, pull-up) |
| PD5 |
UART TX (115200 8N1) |
| PD6 |
UART RX |
| PC1 |
Heartbeat LED (1Hz toggle) |
Console Commands
stats - Display kernel, timer, console, and edge statistics
Quick Start
Creating a Handler (stateless)
Handlers react to events with immediate action. No state transitions.
# handlers/my_handler.S
.include "core/events.inc"
.section .bss
.align 4
my_counter: .word 0
# Auto-registration
.section .kernel_init, "a"
.align 2
.word my_handler_init
.section .text
.align 2
my_handler_init:
addi sp, sp, -4
sw ra, 0(sp)
# Register for EV_KIND_TIMER, id=5
li a0, EV_KIND_TIMER
li a1, 5
la a2, my_handler
call register_activity
# Arm timer (1 second = 48000000 ticks)
li a0, 5 # timer id
li a1, 48000000 # ticks
call timer_arm_delta
lw ra, 0(sp)
addi sp, sp, 4
ret
my_handler:
# Called on each timer event
# a0=header, a1=data0, a2=data1
la t0, my_counter
lw t1, 0(t0)
addi t1, t1, 1
sw t1, 0(t0)
# Rearm timer
addi sp, sp, -4
sw ra, 0(sp)
li a0, 5
li a1, 48000000
call timer_arm_delta
lw ra, 0(sp)
addi sp, sp, 4
ret
Creating an FSM (with state transitions)
FSMs have explicit states and table-driven transitions.
# subfsm/my_fsm.S
.include "core/events.inc"
# FSM layout offsets
.equ FSM_MAGIC, 0
.equ FSM_STATE, 4
.equ FSM_EVENT_BITS, 5
.equ FSM_NUM_STATES, 6
.equ FSM_NUM_EVENTS, 7
.equ FSM_TABLE, 8
.equ FSM_ACTIONS, 12
.equ FSM_DATA, 16
# States
.equ STATE_OFF, 0
.equ STATE_ON, 1
# Events (must be power of 2 count)
.equ EV_TOGGLE, 0
.equ EV_RESET, 1
.section .bss
.align 4
my_fsm:
.space 16 # FSM header
.word 0 # +16: custom data
# Transition table: [next_state, action_id]
.section .rodata
.align 2
my_fsm_table:
# STATE_OFF
.byte STATE_ON, 1 # EV_TOGGLE -> ON, action_turn_on
.byte STATE_OFF, 0 # EV_RESET -> OFF, action_nop
# STATE_ON
.byte STATE_OFF, 2 # EV_TOGGLE -> OFF, action_turn_off
.byte STATE_OFF, 2 # EV_RESET -> OFF, action_turn_off
my_fsm_actions:
.word action_nop
.word action_turn_on
.word action_turn_off
# Auto-registration
.section .kernel_init, "a"
.align 2
.word my_fsm_init
.section .text
.align 2
my_fsm_init:
addi sp, sp, -4
sw ra, 0(sp)
# Initialize FSM
la a0, my_fsm
la a1, my_fsm_table
la a2, my_fsm_actions
li a3, 1 # event_bits (2 events = 2^1)
li a4, 2 # num_states
li a5, STATE_OFF # initial state
call fsm_init
# Register dispatcher for your event source
li a0, EV_KIND_SOFT
li a1, 10 # your event id
la a2, my_fsm_dispatch
call register_activity
lw ra, 0(sp)
addi sp, sp, 4
ret
my_fsm_dispatch:
# Map kernel event to FSM event and call fsm_step
addi sp, sp, -8
sw ra, 0(sp)
sw a1, 4(sp) # save data0
# Extract flags or data to determine FSM event
andi t0, a0, 0xFFFF # flags from header
li a1, EV_TOGGLE # default event
la a0, my_fsm
lw a2, 4(sp) # data0
li a3, 0 # data1
call fsm_step
lw ra, 0(sp)
addi sp, sp, 8
ret
action_nop:
ret
action_turn_on:
# a0 = fsm instance, a1 = data0, a2 = data1
# Turn on LED, etc.
ret
action_turn_off:
# Turn off LED, etc.
ret
Key Rules
- 1 event → 1 step — No loops in handlers, bounded execution
- Actions don't write state — Only
fsm_step modifies FSM state
- Auto-registration — Place init pointer in
.kernel_init section
- Timer IDs 0-15 — Shared resource, pick unique IDs
- Event IDs 0-31 — Per event kind, pick unique IDs
Adding to Build
Edit run.sh:
# Add your handler
riscv32-unknown-elf-as $AS_FLAGS -o $BUILD_DIR/my_handler.o handlers/my_handler.S
# Add to linker
riscv32-unknown-elf-ld ... $BUILD_DIR/my_handler.o ...
Memory Map (RAM: 0x20000000 - 0x20000800)
Event System
| Address |
Symbol |
Size |
Description |
| 0x20000000 |
trap_snapshot_mcause |
4 |
Exception cause code |
| 0x20000004 |
trap_snapshot_mepc |
4 |
Faulting PC address |
| 0x20000008 |
trap_snapshot_mtval |
4 |
Trap value (bad addr/insn) |
| 0x2000000C |
isr_table |
256 |
IRQ handler dispatch table (64 × 4 bytes) |
| 0x2000010C |
event_queue |
384 |
Event ring buffer (32 × 12 bytes) |
| 0x2000028C |
event_head |
4 |
Write index (modified by ISRs) |
| 0x20000290 |
event_tail |
4 |
Read index (modified by event_loop) |
| 0x20000294 |
event_drop |
4 |
Dropped events counter |
Kernel Statistics
| Address |
Symbol |
Size |
Description |
| 0x20000298 |
kernel_stats |
16 |
Kernel statistics block |
| +0 |
kernel_stat_dispatched |
4 |
Events successfully dispatched |
| +4 |
kernel_stat_unhandled |
4 |
Events with no registered handler |
| +8 |
kernel_stat_dropped |
4 |
Events dropped (bounds violation) |
| +12 |
kernel_stat_max_depth |
4 |
Maximum queue depth observed |
Timer Service
| Address |
Symbol |
Size |
Description |
| 0x200002A8 |
timer_deadlines |
64 |
Timer deadline array (16 × 4 bytes) |
| 0x200002E8 |
timer_stats |
16 |
Timer statistics block |
| +0 |
timer_stat_armed |
4 |
Timers armed |
| +4 |
timer_stat_canceled |
4 |
Timers canceled |
| +8 |
timer_stat_expired |
4 |
Timers expired |
| +12 |
timer_stat_isr_calls |
4 |
SysTick ISR invocations |
Console Service
| Address |
Symbol |
Size |
Description |
| 0x200002F8 |
console_stats |
12 |
Console statistics block |
| +0 |
console_stat_rx_count |
4 |
RX events processed |
| +4 |
console_stat_tx_count |
4 |
TX bytes sent |
| +8 |
console_stat_tx_busy_drops |
4 |
TX attempts dropped (busy) |
TX Streaming Job
| Address |
Symbol |
Size |
Description |
| 0x20000304 |
tx_job |
12 |
TX streaming state |
| +0 |
tx_job_ptr |
4 |
Current buffer pointer |
| +4 |
tx_job_remaining |
4 |
Bytes remaining |
| +8 |
tx_job_state |
4 |
0=IDLE, 1=STREAMING |
FSM: Edge Detection
| Address |
Symbol |
Size |
Description |
| 0x20000310 |
edge_fsm |
28 |
Edge FSM instance |
| +0 |
magic |
4 |
"FSM0" signature (0x46534D30) |
| +4 |
state |
1 |
Current state (0=IDLE, 1=DEBOUNCE) |
| +5 |
event_bits |
1 |
log2(num_events) = 1 |
| +6 |
num_states |
1 |
Number of states = 2 |
| +7 |
num_events |
1 |
Number of events = 2 |
| +8 |
table |
4 |
Pointer to transition table |
| +12 |
actions |
4 |
Pointer to action table |
| +16 |
edge_count |
4 |
Validated edges (after debounce) |
| +20 |
raw_count |
4 |
Raw EXTI events (all) |
| +24 |
last_timestamp |
4 |
Timestamp of last edge |
Handler: Heartbeat
| Address |
Symbol |
Size |
Description |
| 0x2000032C |
heartbeat_state |
8 |
Heartbeat context |
| +0 |
magic |
4 |
"HB00" signature (0x48423030) |
| +4 |
counter |
4 |
Heartbeat tick count |
Console Handler
| Address |
Symbol |
Size |
Description |
| 0x20000334 |
uart_console_state |
44 |
Console FSM state |
| +0 |
magic |
4 |
"UC00" signature (0x55433030) |
| +4 |
state |
4 |
FSM state (0=INIT, 1=IDLE) |
| +8 |
rx_pos |
4 |
Current position in rx_buffer |
| +12 |
rx_buffer |
32 |
Line input buffer |
Monitor
| Address |
Symbol |
Size |
Description |
| 0x20000360 |
monitor |
12 |
Debug monitor structure |
| +0 |
magic |
4 |
"MON0" signature (0x4D4F4E30) |
| +4 |
event_counter |
4 |
Total events processed |
| +8 |
debug_mie |
4 |
MIE register snapshot |
Event Format (12 bytes)
Word 0: header = kind:8 | id:8 | flags:16
Word 1: data0 = event-specific data
Word 2: data1 = event-specific data
Event Kinds
| Kind |
Value |
Description |
| EV_KIND_NONE |
0 |
Invalid/empty |
| EV_KIND_IRQ |
1 |
Hardware interrupt |
| EV_KIND_TIMER |
2 |
Timer expiration |
| EV_KIND_CONSOLE |
3 |
Console RX/TX |
| EV_KIND_SOFT |
4 |
Software events |
Event IDs
EV_KIND_IRQ:
| ID |
Symbol |
Description |
| 0 |
EV_IRQ_SYSTICK |
SysTick timer |
| 1 |
EV_IRQ_EXTI7_0 |
EXTI lines 7-0 |
| 2 |
EV_IRQ_USART1 |
USART1 interrupt |
EV_KIND_TIMER:
| ID |
Description |
| 0 |
Heartbeat timer |
| 1 |
Edge debounce timer |
EV_KIND_CONSOLE:
| ID |
Symbol |
Description |
| 0 |
EV_CONSOLE_RX |
Character received (data0=byte) |
| 1 |
EV_CONSOLE_TX |
TX complete (reserved) |
EV_KIND_SOFT:
| ID |
Symbol |
Description |
| 1 |
EV_SOFT_TX_KICK |
TX streaming kick |
FSM Transition Tables
Edge FSM
States: IDLE=0, DEBOUNCE=1
Events: EV_EXTI=0, EV_TIMEOUT=1
| State |
Event |
Next State |
Action |
| IDLE |
EV_EXTI |
DEBOUNCE |
arm_debounce (50ms timer) |
| IDLE |
EV_TIMEOUT |
IDLE |
nop (stale) |
| DEBOUNCE |
EV_EXTI |
DEBOUNCE |
nop (suppress bounce) |
| DEBOUNCE |
EV_TIMEOUT |
IDLE |
edge_valid (increment counter) |
IRQ Mapping
| IRQ |
Vector |
Handler |
Event |
| 12 |
SysTick |
timer_isr |
EV_KIND_TIMER |
| 18 |
USART1 |
uart_hw_rx_isr |
EV_CONSOLE_RX |
| 20 |
EXTI7_0 |
exti_isr |
EV_IRQ_EXTI7_0 |
GDB Debugging
riscv32-unknown-elf-gdb build/kernel.elf
(gdb) target remote :3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue
Useful Commands
# View kernel stats
x/4w &kernel_stats
# View timer stats
x/4w &timer_stats
# View console stats
x/3w &console_stats
# View edge FSM state
x/7w &edge_fsm
# View event queue head/tail
x/3w &event_head
License
MIT