The Pure Functional Approach to Hard Real-Time Systems
ReactHome is a high-integrity embedded framework built on Haskell and Ivory eDSL. It enables architects to design firmware as mathematical formulas, ensuring safety, determinism, and zero-copy performance for mission-critical applications like industrial automation, power grid control, and real-time audio processing.
In this framework, bugs are unrepresentable. By leveraging Haskell's type system and Ivory's safety guarantees, we eliminate 99% of common embedded pitfalls at compile time:
- Memory Safety: No null pointers, no buffer overflows, and no dynamic allocation
- Concurrency Safety: Race conditions are prevented by atomically blocks (IRQ masking) and strictly typed synchronization
- Logic Integrity: State machines (Mealy/Moore) are verified by types. If it compiles, transitions are valid
- Zero-Risk Refactoring: Refactor Haskell abstractions freely. If core logic remains unchanged, generated C code will be bit-for-bit identical
The entire device is described as a Formula. You don't "initialize" peripherals; you declare hardware topology. The system verifies that your pinouts, clock frequencies, and MCU capabilities match perfectly before generating any C code.
rsHub4 :: Formula GD32F4xx
rsHub4 = Formula {
name = "rs_hub4",
model = deviceTypeRsHub4,
version = (6, 2),
shouldInit = true,
mcu = gd32f450vgt6,
quartzFrequency = 25_000_000,
systemFrequency = 200_000_000,
implementation = hub
(U.rbus eth_0)
(F.rbus $ rs485 uart_5 out_pb_14 :> rs485 uart_3 out_pc_12 :> Nil)
(dimmers $ pwm_0 :> pwm_1 :> pwm_2 :> Nil)
(dinputs $ in_pa_8 :> in_pb_4 :> in_pb_10 :> in_pa_15 :> Nil)
(ds18b20 ow_0 od_pb_3)
(indicator npx_pwm_0 50)
(aled npx_pwm_1 etc)
}Our Context is a monoid. Different modules (UART, RBUS, Hub) can independently request initializations or tasks. The framework uses nub to collapse duplicates, ensuring that the final main and loop functions are clean, minimal, and idempotent.
data Context = Context {
getModule :: ModuleDef,
getInits :: [Def ('[] :-> ())],
getTasks :: [Task],
getSyncs :: [Def ('[] :-> ())],
getBody :: forall s. [(String, Ivory (ProcEffects s ()) ())]
}- Hard Real-Time: One tick equals one transition
- Phase Multiplexing: Use phase to stagger heavy tasks or build multi-step sensor polling (Reset β Phase 10ms β Measure β Phase 20ms β Read) without blocking the CPU
- Multi-Body Functions: Multiple independent modules can "stitch" their logic into a single interrupt handler or event hook without knowing about each other
data Task = Task {
period :: Maybe Period,
getTask :: Def ('[] :-> ())
}
delayPhase interval phase id run = task (Just $ Period interval phase) id runWe use inversion of control for data transmission. Instead of copying data into buffers, we pass a generator function.
class LazyTransport x where
lazyTransmit :: x -> Uint8 -> ((Uint8 -> forall eff. Ivory eff ()) -> forall eff. Ivory eff ()) -> Ivory (ProcEffects s t) ()- RTP Ready: Headers, IDs, and CRC16 are calculated and injected on-the-fly as the hardware requests the next byte
- ElasticQueue: Jitter buffers with built-in hysteresis for smooth audio streaming
- Shake + MD5: Shake is a Haskell library used for build automation. Every build is isolated in a directory named after the MD5 hash of its compiler flags and definitions. No "stale" object files, ever.
- GCC -O3 Ready: Ivory generates "dead-simple" C code that C-optimizers love. It's safe to use -O3 because undefined behavior is caught in Haskell.
- Architecture Independent: Porting to RISC-V, Cortex-M, or a new vendor (GD32, STM32, ESP32) only requires swapping the low-level HAL. The business logic remains untouched.
- Cabal Integration: Use
cabal runto generate C code, compile it, and link everything automatically.
Master-slave communication with automatic device discovery and confirmation:
data Preamble = Preamble {
discovery :: Uint8, -- 0xaa/0x55
ping :: Uint8, -- 0xcc/0x33
confirm :: Uint8, -- 0xaf/0x5f
message :: Uint8 -- 0xa0/0x50
}- RBUS Mode: Master-slave with addressing and confirmation
- RS485 Mode: Simple serial communication
- DMX512 Mode: Lighting control protocol with break generation
Multi-port communication hub with dimmers, digital inputs, and temperature sensors:
rsHub4 :: Formula GD32F4xx
rsHub4 = Formula { ... } -- See full example aboveSimple digital input module with temperature sensing:
di4 :: Formula GD32F3x0
di4 = Formula {
name = "di4",
model = deviceTypeDi4,
version = (4, 9),
mcu = gd32f330k8u6,
systemFrequency = 84_000_000,
implementation = di
(rbus $ rs485 uart_1 out_pa_4)
(dinputs $ in_pa_12 :> in_pa_11 :> in_pa_10 :> in_pa_9 :> Nil)
(ds18b20 ow_1 od_pa_8)
}- Zero-Copy: All data transmission uses generator functions, eliminating buffer copies
- Deterministic: Fixed-time task scheduling with phase multiplexing
- Memory Efficient: No dynamic allocation, all memory allocated at compile time
- Real-Time: Hard real-time guarantees with IRQ-based synchronization
Create a formula file in formula/Formula/YourDevice.hs:
{-# LANGUAGE NumericUnderscores #-}
module Formula.YourDevice where
import Core.Formula
import Device.YourMCU
import Feature.YourFeatures
import Implementation.YourImplementation
yourDevice :: Formula YourMCU
yourDevice = Formula {
name = "your_device",
model = deviceTypeYourDevice,
version = (1, 0),
mcu = yourMcu,
systemFrequency = 168_000_000,
implementation = yourImplementation
}# Generate C code, compile it and link
cabal run
# The generated firmware is in firmware/ directory
# Each device has its own folder with .c and .h files
ls firmware/# Flash using J-Link scripts (examples in script/ directory)
# For GD32F330 devices:
./script/flash_loc_330.sh
# For GD32F450 devices:
./script/flash_loc_450.sh
# Or use J-Link directly:
JLinkExe -Device GD32F330K8 -If SWD -Speed 1000 jlink/gd32f330/FlashMCU.jlink
# Debug with logic analyzer (not GDB!)
# Verify timing and protocol integrity at the pinsWe don't use GDB. We use oscilloscopes and logic analyzers.
Because memory and state logic are guaranteed by the type system, we only need to verify physical timing and protocol integrity at the pins.
- No null pointers (Ivory enforces non-null references)
- No buffer overflows (fixed-size arrays with bounds checking)
- No dynamic allocation (all memory allocated at compile time)
- Race condition prevention through IRQ masking
- Strictly typed synchronization primitives
- Atomic operations enforced by type system
- State machine transitions verified at compile time
- Message format validated by types
- CRC and checksum calculations guaranteed correct
Instead of imperative initialization, devices are declared as mathematical formulas:
rsHub4 :: Formula GD32F4xx
rsHub4 = Formula {
name = "rs_hub4",
model = deviceTypeRsHub4,
version = (6, 2),
mcu = gd32f450vgt6,
quartzFrequency = 25_000_000,
systemFrequency = 200_000_000,
implementation = hub
(U.rbus eth_0)
(F.rbus $ rs485 uart_5 out_pb_14 :> rs485 uart_3 out_pc_12 :> Nil)
(dimmers $ pwm_0 :> pwm_1 :> pwm_2 :> Nil)
(dinputs $ in_pa_8 :> in_pb_4 :> in_pb_10 :> in_pa_15 :> Nil)
(ds18b20 ow_0 od_pb_3)
(indicator npx_pwm_0 50)
(aled npx_pwm_1 etc)
}Benefits:
- Hardware topology verification at compile-time
- Pin assignment conflicts detected early
- Clock frequency validation
- Zero configuration drift
The framework uses monoidal composition to automatically resolve dependencies:
data Context = Context {
getModule :: ModuleDef,
getInits :: [Def ('[] :-> ())],
getTasks :: [Task],
getSyncs :: [Def ('[] :-> ())],
getBody :: forall s. [(String, Ivory (ProcEffects s ()) ())]
}
instance Semigroup Context where
(Context m1 i1 t1 s1 b1) <> (Context m2 i2 t2 s2 b2) =
Context (m1 <> m2) (nub $ i1 <> i2) (nub $ t1 <> t2) (nub $ s1 <> s2) (b1 <> b2)Key Features:
- Idempotent Initialization: Duplicate init calls automatically collapsed via
nub - Decentralized Design: Modules independently request resources
- Automatic Assembly: Framework builds clean, minimal main/loop functions
- Conflict Resolution: No manual dependency management required
Multiple modules can stitch code into single interrupt handlers:
-- In Build/Firmware.hs
bodyNames = nub $ fst <$> bodies
multiBodyFunctions = mkMultiBodyFunction <$> bodyNames
mkMultiBodyFunction :: String -> Def ('[] :-> ())
mkMultiBodyFunction name' = proc name' $ body $ mapM_ snd $ filter (\(id', _) -> id' == name') bodiesUsage Example:
-- Module A
addBody "uart_irq" $ do
-- Handle UART data for RBUS
-- Module B
addBody "uart_irq" $ do
-- Handle UART data for logging
-- Framework automatically generates:
uart_irq :: Def ('[] :-> ())
uart_irq = proc "uart_irq" $ body $ do
-- Module A code
-- Module B codeBenefits:
- True encapsulation - modules don't know about each other
- HAL-level abstraction isolates business logic from interrupt handling
- Guaranteed execution order based on context assembly
- No manual ISR management
Framework provides type-safe state machine construction:
-- Mealy Machine (output depends on state AND input)
runState :: (IvoryStore a, IvoryEq a) =>
(t -> Ref s (Stored a)) -> [(a, t -> p -> Ivory eff ())] -> t -> p -> Ivory eff ()
-- Moore Machine (output depends only on state)
runState' :: (IvoryStore a, IvoryEq a) =>
(t -> Ref s (Stored a)) -> [(a, t -> Ivory eff ())] -> t -> Ivory eff ()
-- Input dispatcher
runInput :: (IvoryEq p) =>
[(p, t -> p -> Ivory eff ())] -> t -> p -> Ivory eff ()RBUS Protocol Example:
receive :: Slave n -> Uint8 -> Ivory (ProcEffects s ()) ()
receive = runState state [
readyToReceive |-> receivePreamble,
receivingMessage |-> receiveMessage,
receivingConfirm |-> receiveConfirm,
receivingDiscovery |-> receiveDiscovery,
receivingPing |-> receivePing,
skippingAll |-> skipAll,
skippingMsg |-> skipMsg
]Guarantees:
- Invalid states are unrepresentable
- Deterministic transitions (one tick = one transition)
- Type-safe state and input handling
- Protocol correctness enforced by structure
Revolutionary approach to data transmission using generator functions:
class LazyTransport x where
lazyTransmit :: x -> Uint8 ->
((Uint8 -> forall eff. Ivory eff ()) -> forall eff. Ivory eff ()) ->
Ivory (ProcEffects s t) ()
-- RGB LED palette with on-the-fly color extraction
sendPalette LEDs{..} = do
let n = 3 * ln' + 3
lazyTransmit transport n \transmit -> do
transmit actionRGB
transmit . castDefault $ i' + 1
transmit 1
arrayMap \cx -> do
value <- deref (colors ! ix' ! cx)
let r' = castDefault $ (value `iShiftR` 16) .& 0xff
let g' = castDefault $ (value `iShiftR` 8) .& 0xff
let b' = castDefault $ value .& 0xff
transmit r' >> transmit g' >> transmit b'Revolutionary Benefits:
- Zero Memory Allocation: No intermediate buffers
- On-the-Fly Processing: CRC calculated during transmission
- Real-Time Responsiveness: Bytes generated as hardware requests them
- Perfect for RTP: Audio streaming with minimal jitter
Single function works for both direct transmission and queuing:
run :: (KnownNat l, SafeCast Uint8 v, IvoryStore v) =>
Slave 255 ->
(Slave 255 -> (Uint8 -> forall eff. Ivory eff ()) -> Ivory (ProcEffects s t) ()) ->
Buffer l v ->
Uint16 ->
Ivory (ProcEffects s t) Uint8
run protocol transmit buff offset = do
size <- local $ ival 0
let go :: Uint8 -> Ivory eff ()
go v = do
i <- deref size
let ix = toIx $ offset + safeCast i
store (buff ! ix) $ safeCast v
store size $ i + 1
transmit protocol go
deref sizeUsage Scenarios:
-- Direct transmission
run protocol (transmitMessage' size' transmit) buff offset
-- Queue buffering
run protocol (toQueue' size' transmit) buff offsetBenefits:
- Single protocol implementation for multiple transports
- Automatic size calculation
- Type-safe buffer management
- Compile-time bounds checking
Tasks can be staggered to avoid CPU contention:
data Task = Task {
period :: Maybe Period,
getTask :: Def ('[] :-> ())
}
data Period = Period {
interval :: Uint32,
phase :: Uint32
}
delayPhase :: Uint32 -> Uint32 -> String -> Ivory (ProcEffects s ()) () -> TaskExample: Multi-Step Sensor Polling
-- Phase 0: Reset sensor
addTask $ delayPhase 1000 0 "sensor_reset" resetSensor
-- Phase 10ms: Start measurement
addTask $ delayPhase 1000 10 "sensor_measure" startMeasurement
-- Phase 20ms: Read result
addTask $ delayPhase 1000 20 "sensor_read" readResultBenefits:
- Deterministic timing without blocking delays
- CPU load distribution across tick
- Complex sequencing made simple
- Hard real-time guarantees
- No NULL Pointers: Ivory enforces non-null references
- No Buffer Overflows: Fixed-size arrays with bounds checking
- No Dynamic Allocation: All memory allocated at compile time
- Stack Overflow Prevention: Ivory guarantees bounded stack usage
- IRQ Masking: All shared state modifications protected by
atomicallyblocks - Type-Safe Synchronization: Semaphore and queue operations verified by types
- Race Condition Prevention: Shared state access patterns enforced by types
- State Machine Verification: Invalid transitions are type errors
- CRC Enforcement: Checksum calculation integrated into transmission
- Message Structure: Packet format guaranteed by function signatures
- Protocol Compliance: Invalid states unrepresentable
- Generator Control: Automatic transfer switches with phase monitoring
- Load Balancing: Real-time phase imbalance detection and correction
- Safety Interlocks: Type-enforced prevention of hazardous states
- Emergency Response: Deterministic fail-safe behavior
- Boiler Management: Multi-stage heating with temperature feedback
- Ventilation Control: CO2 monitoring with adaptive fan control
- Energy Optimization: Learning algorithms with hard real-time constraints
- Safety Systems: Overheat protection and emergency shutdown
- RTP Streaming: ElasticQueue with hysteresis for jitter buffering
- Zero-Copy Processing: Sample processing without memory allocation
- Low Latency: Sub-millisecond audio processing chains
- Multi-Channel: Synchronized multi-speaker systems
"This framework transforms embedded development from writing instructions to designing mathematical systems. If it compiles, it works."
- Industrial Automation: PLCs, motor controllers, sensor networks
- Power Grid Control: Protective relays, SCADA endpoints, smart meters
- Real-Time Audio: Audio processing, RTP streaming, effects pedals
- Lighting Control: DMX512 controllers, LED drivers, architectural lighting
- IoT Gateways: Protocol converters, edge computing, data aggregation
reacthome-firmware/
βββ formula/ # Device formulas (blueprints)
β βββ Formula/ # Individual device definitions
βββ src/ # Core framework
β βββ Build/ # Build system and compilation
β βββ Core/ # Framework abstractions
β βββ Data/ # Data structures and utilities
β βββ Device/ # Hardware abstraction layer
β βββ Endpoint/ # Hardware endpoint implementations
β βββ Feature/ # High-level feature implementations
β βββ Implementation/ # Device implementation logic
β βββ Interface/ # Hardware interface definitions
β βββ Ivory/ # Ivory eDSL support
β βββ Protocol/ # Communication protocol implementations
β βββ Support/ # Support libraries and utilities
β βββ Transport/ # Transport layer implementations
β βββ Util/ # General utilities
βββ firmware/ # Generated C code and headers
βββ script/ # J-Link flash scripts
βββ jlink/ # J-Link configuration files
βββ lab/ # C experiments to learn hardware
βββ support/ # Third-party C dependencies
- Prerequisites: GHC, ARM GCC toolchain, J-Link
- Clone:
git clone <repository> - Build:
cabal build - Generate Firmware:
cabal runto generate optimized C code and compile firmware - Flash: Use J-Link scripts in
script/directory - Debug: Use logic analyzer to verify timing and protocol integrity
[License information here]
Built with β€οΈ for mission-critical embedded systems