Bare-metal RTOS written in C and ARM assembly.
Started as a learning project — now focused on deterministic scheduling, memory protection, and debuggability.

STM32H753ZI (FDCAN) ↔ FRDM-K64F (FlexCAN) — real two-node bus, SN65HVD230 transceivers, verified ACK on both sides
arm compiler version:
arm-none-eabi-gcc -v
..
..
Thread model: single
Supported LTO compression algorithms: zlib zstd
gcc version 15.3.1 20260627 (Arm GNU Toolchain 15.3.Rel1 (Build arm-15.149)) Kernel
- Preemptive scheduler, PendSV context switch, PSP per-task isolation
- FPU context switching (lazy stacking via EXC_RETURN; FPU variant differs per board — see below)
Sync
- Mutex (LDREX/STREX) with priority inheritance — elevates the lock owner's priority to match a higher-priority blocked waiter, preventing priority inversion; restores original priority on unlock
- Semaphore with priority-based wait queues
- Queue — fixed-size circular buffer, priority-ordered wait list on both the send side and the receive side (a task blocked on a full/empty queue is served in priority order, not FIFO order)
- Event flags (event groups) — bitmask-based
EVENT_WAIT_ANY/EVENT_WAIT_ALLwaiting with optional auto-clear on wake - Timeout support across the board —
mutex_lock_timeout,queue_send_timeout,queue_receive_timeout,event_wait_timeoutall bound their wait on an absolute deadline (viasystick_get_ms()), so a task that wakes and loses a race re-arms only the remaining budget instead of the full timeout again - ISR-Safe primitives —
queue_send_from_isr/queue_receive_from_isr/event_set_from_isr/event_clear_from_isr, verified from a real interrupt context (not simulated), including a deliberate flood test where 977/1000 ISR-context sends correctly returned an immediate error instead of ever hanging the interrupt - Critical section (cpsid/cpsie)
- Software Timer — fixed-size static pool (no dynamic allocation) of one-shot and auto-reload timers
- Tickless Idle — Already not checking every 1ms tick. Verified on both boards with zero measured drift: a task requesting
sched_delay_ms(500)woke up at exactly 500ms elapsed. - Deadline/Response Time Monitor — per-task execution-time tracking (min/avg/max, overrun count vs. budget), independent of scheduler internals — wraps any periodic task's work with begin()/end() timing calls. The kind of timing evidence a DO-178C-style certification process would expect — currently measures response time (wall-clock, including preemption), not WCET. (note: STM32H753ZI's SysTick is clocked from AHB/8, not the core clock directly )
- Task Notification the fastest signal path in the kernel tested on m4 and m7
- Stack High-Water Mark — M4 and M7 showed consistent ~56-57 bytes per call level, and the deep task measured identically at 900 bytes on both STM32 and K64F.
- Jitter Monitor — measures how consistently a periodic task wakes up relative to its expected schedule, independent of per-cycle execution time (that's what Deadline Monitor tracks). tested with 2 task one regular one irregular regular one was everything 2ms anoyher was changing as expected.
Servo/ESC
actuators/servo.c— generic hobby servo/ESC abstraction over each board's PWM driver. Maps a 0-180 degree angle (or raw microsecond pulse, for ESC throttle) to the standard 1000-2000us hobby servo/ESC convention (1000=0deg/min-throttle, 1500=90deg/center, 2000=180deg/max-throttle)- Continuous 0↔180 sweep helper (
servo_sweep_step()), independent per channel — verified on both boards with a logic analyzer showing a clean, correctly-timed 20ms/50Hz signal
Boot
- Hand-written startup with I-Cache/D-Cache invalidate + enable sequence (PM0253)
- Linker script (DTCM, AXI, SRAM1-4, BKPSRAM)
- RCC init — HSI 64MHz and HSE/PLL1 480MHz
Kernel
- Preemptive scheduler (Cortex-M7 port)
- PendSV context switch
- PSP per-task isolation
- FPU context switching (fpv5-d16, double-precision, lazy stacking via EXC_RETURN)
- MPU-based stack overflow guard (hardware-enforced, MemManage on violation — verified end-to-end: a real stack overflow inside a running task trips the guard and lands in the fault handler with the correct MMFAR)
Fault handling & safety
- Unified fault handler for HardFault, MemManage, BusFault, and UsageFault — all four vectors route through one handler that dumps PC/LR/xPSR/R0-R3/R12/SP, decodes every CFSR bit (DIVBYZERO, DACCVIOL, IACCVIOL, STKERR, IBUSERR, etc.), and reports MMFAR/BFAR when valid
- Crash record persisted to Backup SRAM (survives a normal reset, not just visible over UART at the moment of the crash) and automatically re-reported on the next boot, then cleared — both the write and the clear are D-cache–flushed explicitly, since a plain cache invalidate-without-clean on the next boot would otherwise silently discard an unflushed write (or an unflushed clear, causing the same crash to be re-reported forever)
- Independent Watchdog (IWDG) — verified to actually reset the board when a task stops kicking it, with the reset cause (
IWDGRSTFinRCC_RSR) correctly distinguished from a normal reset on the next boot FAULT_HANDLER_DEBUG_HALTbuild flag: defaults to 0 (production — logs then hangs safely for inspection; pair with IWDG for auto-recovery), set to 1 only on an explicit debug build to halt instead, for live GDB inspection
Sync
- Mutex (LDREX/STREX) with priority inheritance — elevates the lock owner's priority to match a higher-priority blocked waiter, preventing priority inversion; restores original priority on unlock
- Semaphore with priority-based wait queues
- Queue — priority-ordered wait on send and receive, with a bounded (
_timeout) variant - Event flags — ANY/ALL/auto_clear, with a bounded (
_timeout) variant - Critical section (cpsid/cpsie)
- Software Timer + Tickless Idle — same shared kernel implementation, verified independently on this board
Drivers
- SysTick (1ms tick, AHB/8) — also the shared kernel's monotonic time source (
systick_get_ms()) used by every_timeoutprimitive above - RCC (HSI 64MHz and HSE/PLL1 480MHz)
- UART (USART3, 115200, Virtual COM via ST-Link)
- I2C (I2C1 PB8/PB9 AF4)
- FDCAN1 (PB8=RX (AF9), PB9=TX (AF9) — PA11/PA12 they're tied to USB OTG FS) — verified on a real two-node bus: internal loopback passes, and with an SN65HVD230 transceiver on each board, STM32 and K64F exchange frames continuously with zero ACK failures on either side, plus an ARINC825-inspired application-layer message format on top (categorized CAN ID ranges by criticality, a standard node/sequence/data-type header, and a periodic per-node heartbeat so a lost link is detected as a health event rather than inferred from a missing specific message type)
- IWDG (Independent Watchdog, LSI-clocked, software-configurable timeout)
- PWM (TIM2_CH1, PA0 / Arduino D32, 50Hz/1-2ms hobby servo & ESC convention) — verified with a logic analyzer: clean 20.0ms period, pulse width tracking the requested 1000-2000us value exactly.
- ADC1 (PA3 / Arduino A0, ADC1_INP15, single-conversion polling mode, 16-bit resolution) — verified end-to-end
- Ethernet MAC+DMA (RMII, register-level, no HAL) — PHY link detection via MDIO (LAN8742, PHY ID confirmed), TX and RX both fully verified end-to-end on real hardware: TX payload confirmed byte-for-byte in Wireshark, RX confirmed with 50/50 zero-loss unicast test frames sent from a PC. Went through seven distinct root causes during bring-up (DTCM being DMA-unreachable, tail-pointer semantics, D-Cache coherency + MPU memory-type selection, linker alignment, unset RX buffer size, unset MTL store-and-forward, unset MAC address filter) — full writeup at https://auctra.app
Sensors
- MPU6050 (I2C, accel/gyro, feeds the shared Kalman filter)
- BMP280 (I2C, address 0x76 — misidentified as a BMP180 for most of a debugging session before a bus scan caught it) — pressure/temperature/zero-referenced relative altitude
Tests
tests/stm/test_mutex_priority_inheritance.c— validates elevate/restore behavior under LOW/HIGH/MED priority contentiontests/stm/test_queue_priority_order.c— validates priority-ordered wake on both the send side and the receive side of a queuetests/stm/test_event_flags.c— validates ANY/ALL wake, auto_clear, and both timeout-expiry/timeout-success pathstests/stm/test_timeout.c— validatesmutex_lock_timeout/queue_send_timeout/queue_receive_timeoutagainst both an expiring wait and a wait satisfied just before the deadlinetests/stm/imu_kalman_fdcan.c— imu kalman fdcan testtests/stm/test_fdcan1.c— internal loopback: send/receive round-trip, byte-for-bytetests/stm/test_fdcan_real_bus.c— real-bus test (loopback disabled): periodic TX + continuous RX polling, paired with the matching K64F test belowtests/stm/test_fault_handler.c— deliberately triggers UsageFault/BusFault/MemManage/HardFault and verifies UART dump + Backup SRAM persistence across a real resettests/stm/test_mpu_stack_guard.c— runs a real scheduled task to destruction to verify the MPU stack-overflow guard fires with the correct faulting addresstests/stm/test_iwdg.c— arms IWDG, proves kicking prevents reset, then deliberately starves it to confirm the board actually resets and the cause is correctly reportedtests/stm/pwm_test.c— sweeps TIM2_CH1 pulse width 1000-2000us continuously, printing each step over UARTtests/stm/servo_sweep_test.c— continuous 0-180 degree servo sweep via the actuators/servo.c abstractiontests/stm/adc_test.c— continuously reads PA3/ADC1_INP15 and prints the value :)tests/stm/test_software_timer.c— validates one-shot and auto-reload software timers: exact fire counts,timer_stop()correctly halting an active periodic timer, and pool-exhaustion behaviortests/test_tickless_idle.c— verifies a task delayed viasched_delay_ms(500)wakes up at exactly 500ms elapsed with zero measured drift over dozens of cyclestests/stm/test_deadline_monitor.c— simulated periodic task with a 10ms max time, 30 cycles, 4 deliberately-heavy cycles to verify overrun detectiontests/stm/test_task_notify.c— validates give-then-wait, timeout, wait-then-give, and ISR-context notification scenariostests/stm/test_stack_monitor.c— stack monitor test for stmtests/stm/test_eth_link_monitor.c— PHY link up/down + speed/duplex, real cable plug/unplugtests/stm/test_eth_tx_only.c— continuous TX, confirmed byte-for-byte in Wireshark on a connected PCtests/stm/test_eth_rx_only.c— real unicast RX, 50/50 test frames received with zero losstests/stm/test_adc_op.c— adc and conected opamp test file use volatge divider for this test x8 using
Still improving — development notes at https://auctra.app
Boot
- Hand-written startup
- Linker script
- MCG clock init (120MHz)
Kernel
- Preemptive scheduler
- PendSV context switch
- PSP per-task isolation
- FPU context switching (fpv4-sp-d16, single-precision, lazy stacking via EXC_RETURN)
- Stack overflow guard: software canary only (K64F MPU registers use a non-standard memory layout vs. the STM32H753ZI port; hardware guard planned for a future update)
- Tick source is selectable at build time: native ARM SysTick by default (Cortex-M4 shares the same
0xE000E010SysTick block as the STM32H753ZI port's Cortex-M7 — identical technique), orTICK_SOURCE=pitto drive the scheduler tick from the PIT peripheral instead. Either way, board-agnostic kernel code always sees the samesystick_init()/systick_get_ms().
Sync
- Mutex (LDREX/STREX) with priority inheritance — elevates the lock owner's priority to match a higher-priority blocked waiter, preventing priority inversion; restores original priority on unlock
- Semaphore with priority-based wait queues
- Queue — priority-ordered wait on send and receive, with a bounded (
_timeout) variant - Event flags — ANY/ALL/auto_clear, with a bounded (
_timeout) variant - Critical section (cpsid/cpsie)
- Software Timer + Tickless Idle — same shared kernel implementation, verified independently on this board
Drivers
- SysTick (default tick source — see Kernel above) or PIT (32-bit, opt-in via
TICK_SOURCE=pit) - UART
- MCG
- FlexCAN0 (PTB18=TX (ALT2), PTB19=RX (ALT2)) — verified on a real two-node bus: internal loopback passes (5 sequential frames, no drops), and with an SN65HVD230 transceiver, K64F and STM32 exchange frames continuously with zero ACK failures on either side.
- PWM (FTM0 + FTM3, 4 channels: PTC1=FTM0_CH0/ALT4, PTC5=FTM0_CH2/ALT7, PTC8=FTM3_CH4/ALT3, PTC9=FTM3_CH5/ALT3 — 50Hz/1-2ms hobby servo & ESC convention) — verified on all 4 channels with a logic analyzer.
- ADC0 (PTB2 / Arduino A0, ADC0_SE12, 12-bit) — full-range sweep verified with a potentiometer
Tests
tests/k64f/test_queue_priority_order.c— K64F port of the queue priority-order testtests/k64f/test_event_flags.c— K64F port of the event-flags test (all 5 scenarios)tests/k64f/test_flexcan_loopback.c— internal loopback: single frame + 5 sequential frames, plus a non-blockingrx_pending()checktests/k64f/test_flexcan_real_bus.c— real-bus test (loopback disabled), paired with the STM32 test abovetests/k64f/pwm_test.c— sweeps FTM0_CH0 (PTC1, Motor 1) pulse width 1000-2000us, printing each step over UARTtests/k64f/servo_sweep_test.c— continuous 0↔180 degree sweep on all 4 channels simultaneously (PTC1/PTC5/PTC8/PTC9), each channel tracking independent sweep statetests/k64f/test_software_timer.c— K64F port of the software timer test (same three scenarios as the STM32 version)tests/k64f/test_tickless_idle.c— K64F port of the tickless idle drift testtests/k64f/test_deadline_monitor.c— simulated periodic task with a 10ms max time, 30 cycles, 4 deliberately-heavy cycles to verify overrun detectiontests/k64f/test_task_notify.c— K64F port of the task notification test, same four scenariostests/k64f/test_stack_monitor.c— stack monitor test for k64f
Experimental dual implementation for comparing low-level Zig vs C. Boots via Limine with a Multiboot2 header. Active development paused on x86 PORT — focus is on ARM.
Implemented
- GDT setup
- Serial monitor
- ISO via xorriso
- Zig + C comparison
Boot output
Zig -> TamgaOS
KERNEL OK
C -> TamgaOS __C__
GDT OK __C__
Kernel OK __C__
make clean
make BOARD=k64f
make flash BOARD=k64fUse the PIT-based tick source instead of native SysTick:
make clean BOARD=k64f
make BOARD=k64f TICK_SOURCE=pit
make flash BOARD=k64fmake clean
make BOARD=stm32h753zi
make flash BOARD=stm32h753ziDebug build (halts on fault instead of resetting, for GDB inspection):
make clean BOARD=stm32h753zi
make BOARD=stm32h753zi DEBUG=1
make flash BOARD=stm32h753zizig build -Doptimize=ReleaseFast
.\mkiso.ps1
qemu-system-i386 -cdrom .\TamgaOS.iso -boot d -serial stdio.\mkiso_c.ps1
qemu-system-i386 -cdrom .\TamgaOS_C.iso -boot d -serial stdioFull writeup of the seven-bug DMA/cache/MAC-filtering behind the Ethernet driver above (DTCM being DMA-unreachable, tail-pointer semantics, D-Cache coherency + MPU memory-type selection, linker alignment, unset RX buffer size, unset MTL store-and-forward, unset MAC address filter): https://auctra.app (part 10 and prt 11)
-
tamga-koruk a secure bootloader for the STM32H753ZI port, built as an independent project rather than folded into this repo. Verifies firmware integrity/authenticity (HMAC-SHA256, hardware-accelerated via STM32H753ZI's HASH peripheral) before jumping to the application image, with planned rollback protection and A/B partition support. Very early — currently just the basic toolchain (RCC, SysTick, UART) confirmed working on real hardware (ı put simple blink code on main...)
-
tamga-yanki — an acoustic modem transmitter built on TamgaOS's RTOS (real scheduler tasks + queue), sending digital data as an audible tone via OOK modulation.
Not a production OS. Development log: https://auctra.app



