Embedded systems development has long been the domain of hardened C programmers and assembly specialists—engineers who think in register maps and interrupt vectors rather than abstractions and guarantees. The STM32 family of microcontrollers exemplifies this tradition, offering remarkable capabilities but demanding intimate knowledge of hardware details and careful navigation of their inherent constraints. This bare metal approach has been both a necessity and a burden: necessary due to resource limitations, burdensome due to the cognitive overhead it imposes.
Beyond the Bare Metal Paradigm
The industry has accepted this trade-off as inevitable. Higher-level languages bring safety and productivity but at the cost of performance and determinism—qualities embedded systems cannot sacrifice. This assumption has remained largely unchallenged for decades, creating a vast divide between embedded development and mainstream software engineering practices. The result has been siloed expertise, duplicated effort, and a fundamental disconnect between the programming models used for resource-constrained systems and those used elsewhere.
The Fidelity Framework represents a direct challenge to this long-standing assumption. By creating a true compilation path from F# to native code through MLIR and LLVM, it eliminates the traditional dichotomy between high-level expressiveness and low-level control. Rather than forcing developers to choose between these qualities, Fidelity enables them to work at the level of abstraction most appropriate for the task at hand while still generating code that meets the exacting requirements of embedded targets like the STM32 family.
LLVM’s Evolution: The Foundation for a New Approach
LLVM has quietly revolutionized compilation technology over the past two decades. What began as a research project at the University of Illinois has transformed how we think about code generation, optimization, and cross-platform targeting. For embedded systems, LLVM’s modular design and sophisticated targeting capabilities create a foundation upon which new approaches can be built.
The introduction of MLIR (Multi-Level IR) represents a quantum leap in LLVM’s capabilities. Where traditional compilers translate directly from source language to LLVM IR, MLIR provides a flexible framework for progressive lowering through domain-specific dialects. This innovation is particularly valuable for embedded systems, where the gap between high-level abstractions and hardware realities is most pronounced.
With MLIR, we can represent hardware-specific concepts like memory-mapped registers, interrupt controllers, and DMA transfers as operations in specialized dialects. These operations can then be progressively lowered through a series of transformations until they reach LLVM IR and ultimately native code. This approach provides unprecedented flexibility while maintaining the performance characteristics essential for embedded targets.
The STM32 family, with its ARM Cortex-M cores, is particularly well-suited to this approach. LLVM’s ARM backend has matured significantly, offering excellent code generation for the Thumb2 instruction set used by these processors. The ability to target specific microarchitectural features—like the Cortex-M33’s DSP extensions or TrustZone security features—means that generated code can take full advantage of the hardware’s capabilities.
F#’s Unique Suitability for Embedded Development
While LLVM provides the compilation technology, F# brings a type system and programming model uniquely suited to embedded development. This might seem counterintuitive—functional languages are often associated with high abstraction and runtime overhead, qualities at odds with embedded constraints. Yet F#’s particular design choices make it remarkably well-aligned with the needs of embedded systems.
Three features stand out as especially valuable: units of measure, discriminated unions, and immutability by default. Units of measure enable dimensional analysis at compile time, catching errors that would otherwise manifest at runtime or remain latent in the system. For embedded systems working with physical quantities—temperature, pressure, voltage—this provides safety without cost.
// Type-safe hardware register access with units of measure
[<Measure>] type address
[<Measure>] type register
// STM32 GPIO port definition with type-safe register access
type GPIOPort(baseAddress: int<address>) =
// Type-safe register offsets prevent confusion
let modeRegOffset = 0x00<register>
let outputRegOffset = 0x14<register>
let inputRegOffset = 0x10<register>
// Configure pin as output (type system prevents register confusion)
member this.SetPinAsOutput(pinNumber: int) =
// Access MODE register with compile-time safety
let modeReg = MemoryMap.readRegister<uint32> (baseAddress + modeRegOffset)
let newMode = modeReg ||| (0x01u <<< (pinNumber * 2))
MemoryMap.writeRegister (baseAddress + modeRegOffset) newMode
// Write to pin (type system ensures we use OUTPUT register)
member this.WritePin(pinNumber: int, state: bool) =
let value = if state then (1u <<< pinNumber) else (1u <<< (pinNumber + 16))
// Atomically set/reset pin using BSRR register
MemoryMap.writeRegister (baseAddress + 0x18<register>) value
Discriminated unions provide a type-safe way to model state machines, a ubiquitous pattern in embedded systems. Where C code might use enums and switch statements, F# can express the same concept with stronger guarantees. The compiler ensures that all cases are handled, preventing the dangerous “forgotten state” bugs that plague embedded development.
Immutability by default creates a natural distinction between configuration and runtime behavior—a separation critical in embedded systems, where many parameters are set once during initialization. This aligns with the hardware reality of registers that should typically be configured at startup and left unchanged during normal operation.
These features combine to create a language that can express embedded concepts with remarkable clarity while maintaining the guarantees needed for reliable operation. When compiled through MLIR and LLVM to native code, the result is a system that offers both high-level expressiveness and bare metal performance.
Memory Management for Resource-Constrained Systems
Perhaps the most critical aspect of embedded development is memory management. With constraints measured in kilobytes rather than gigabytes, embedded systems demand careful attention to allocation strategies. Traditional approaches have relied on static allocation or simple bump allocators, avoiding the complexity and nondeterminism of full-featured memory managers.
Fidelity approaches this challenge through a graduated memory management system that adapts to the constraints of the target platform. For resource-constrained systems like the STM32, it can employ fully static allocation with zero-copy operations:
// Memory management with explicit control for resource-constrained systems
// Static buffer with guaranteed alignment
let sensorBuffer = AlignedBuffer<float>.CreateStatic<N16>(alignment = 8<bytes>)
// Function to process sensor data with zero heap allocation
let processSensorData (readings: AlignedBuffer<float>) : Statistics =
// All operations happen in-place or with stack allocation
// No garbage collection or dynamic memory management
let mean = computeMeanInPlace readings
let stdDev = computeStdDevInPlace readings
// Return small struct on stack
{ Mean = mean; StdDev = stdDev }
Unlike Rust’s approach, which relies on complex lifetime analysis and explicit borrowing, Fidelity achieves similar safety guarantees through its type system. F#’s units of measure and type inference create a system where memory safety can be enforced without the cognitive overhead of tracking lifetimes. This leads to code that is both safer and more approachable than traditional embedded development.
For slightly less constrained systems, Fidelity can employ more sophisticated strategies like per-task memory pools or region-based memory management. The key insight is that memory management strategy should be determined by system constraints rather than language limitations. This flexibility allows the same core application logic to execute efficiently across targets ranging from tiny microcontrollers to powerful servers.
LicenseToMLIR: The Missing Bridge
The path from F# to native code requires a bridge—a component that can translate from F#’s high-level abstractions to MLIR’s intermediate representation. This is the role of LicenseToMLIR, inspired by the earlier LicenseToCIL project but adapted for the MLIR ecosystem.
LicenseToMLIR provides type-safe code generation through its core MLIROp<'in, 'out>
type, which represents operations that transform the compilation state. This approach ensures that operations compose correctly, with the compiler verifying that the output type of one operation matches the input type of the next.
For embedded developers, LicenseToMLIR offers a domain-specific language for hardware interaction that maintains safety while generating efficient code. Register access, bit manipulation, and peripheral configuration can all be expressed in a type-safe manner that prevents common errors while compiling to the same machine code a skilled C programmer would write by hand.
// Type-safe MLIR generation for STM32 GPIO configuration
let configureGPIOPin (port: GPIOPort) (pin: int) (mode: GPIOMode) =
mlir {
// Compute register address with type safety
yield MLIRPrimitives.constant (getPortAddress port)
yield MLIRPrimitives.constant (getModeRegisterOffset())
yield MLIRPrimitives.add<nativeint>
// Load current register value
yield MLIRPrimitives.loadVolatile
// Clear mode bits for the specified pin
yield MLIRPrimitives.constant (~~~(3u <<< (pin * 2)))
yield MLIRPrimitives.bitwiseAnd
// Set new mode bits
yield MLIRPrimitives.constant ((uint32 mode) <<< (pin * 2))
yield MLIRPrimitives.bitwiseOr
// Store updated value
yield MLIRPrimitives.storeVolatile
}
This approach combines the clarity of high-level programming with the control of assembly language. The mlir
computation expression provides an ergonomic syntax for building complex operations, while the type system ensures that those operations are composed correctly. When compiled, these expressions generate the same efficient machine code that an experienced embedded developer would write by hand.
Native Hardware Abstractions Without Compromise
Traditional hardware abstraction layers like CMSIS or vendor-provided HALs attempt to simplify embedded development by wrapping hardware details in C functions and macros. While these efforts have value, they suffer from fundamental limitations. Macros provide no type safety, and C functions introduce runtime overhead through unnecessary copies and function calls.
Fidelity takes a different approach through its native hardware abstractions. These aren’t wrappers around lower-level code but direct compilations from high-level expressions to native instructions. The key insight is that hardware abstractions should be compile-time transformations rather than runtime indirections.
For the STM32 family, this means that peripheral interactions can be expressed through high-level, typed APIs that compile directly to the appropriate register accesses:
// High-level peripheral access that compiles to direct register operations
// Configure system clock
Clock.configureSystem {
Source = ClockSource.HSI
PLLMultiplier = 8
APB1Divider = 2
APB2Divider = 1
}
// Configure LED pin (PA5 on many STM32 boards)
let led = GPIO.configurePin GPIOA Pin5 {
Mode = Output
Speed = Low
PullUpDown = NoPull
}
// Create timer for precise timing
let timer = Timer.configure Timer2 {
Prescaler = 8000u
Period = 1000u
ClockDivision = Div1
CounterMode = Up
}
// Blink LED using timer for precise timing
while true do
led |> GPIO.toggle
timer |> Timer.delay 500u
This code appears high-level and abstract, yet it compiles to the same efficient machine code as hand-written C or assembly. There are no function call overheads, no unnecessary stack operations, and no runtime type checking. The abstractions exist purely at compile time, disappearing completely in the final binary.
The Farscape tool enhances this approach by automatically generating these abstractions from vendor header files. Rather than manually wrapping C APIs, developers can use Farscape to automatically create type-safe F# bindings for any C library, including vendor HALs and device drivers. These generated bindings maintain the performance characteristics of the original C code while adding the safety guarantees of F#’s type system.
User Experience: The Human Element of Embedded Development
Technical capabilities alone don’t define a framework’s value—the developer experience matters profoundly. Embedded development has traditionally demanded specialized knowledge, creating barriers to entry for many software engineers. The ideal system would maintain the performance characteristics of traditional approaches while removing unnecessary complexity.
Fidelity approaches this challenge through progressive disclosure of complexity. For common tasks, developers can work with high-level, task-oriented APIs that handle hardware details automatically. For specialized needs, direct access to hardware resources remains available but wrapped in type-safe abstractions.
This approach aligns with human cognition by allowing developers to focus on their actual goals rather than implementation details. A developer who wants to read a sensor, process some data, and control an actuator can express these intents directly, without manual register configuration or bit manipulation.
// Progressive disclosure of complexity for different developer needs
// High-level task-oriented API for standard development
let monitorTemperature = coldStream {
// Connect to temperature sensor with automatic initialization
use! sensor = I2C.connectSensor<TemperatureSensor> I2CBus1 Address0x48
// Monitor in a loop with automatic error handling
while! not CancelToken.isCancelled do
let! temperature = sensor.ReadTemperature()
// Respond to temperature changes
if temperature > 30.0<celsius> then
led |> GPIO.turnOn
else
led |> GPIO.turnOff
// Wait with precise timing
do! timer |> Timer.delayMilliseconds 100
}
// Direct hardware access when needed
// (Still type-safe but with explicit control)
let configureAdvancedTimer timer =
// Direct register access with type safety
let timerBase = getTimerBaseAddress timer
// Configure capture/compare mode register
MemoryMap.writeRegister (timerBase + 0x18<register>) 0x00010001u
// Set prescaler for precise timing
MemoryMap.writeRegister (timerBase + 0x28<register>) 7199u
This duality—high-level by default, low-level when needed—creates a system that can grow with developers as they gain expertise. Beginners can start with task-oriented APIs, gradually learning about the underlying hardware as they need more specific control. Experts can leverage their knowledge while still benefiting from the safety guarantees and productivity enhancements of the higher-level abstractions.
The Path Forward: Embedded Systems Without Compromises
Embedded development has long been forced to choose between conflicting values: performance versus productivity, control versus safety, specialization versus accessibility. The Fidelity Framework represents a direct challenge to these false dichotomies. By leveraging advances in compilation technology, type systems, and programming language design, it creates a system where these values coexist and reinforce one another.
For the STM32 family and similar microcontrollers, this approach opens new possibilities. Systems that once required specialized expertise can now be built using the same programming models and tools used in mainstream software development. The knowledge and skills developed in other domains can be applied directly to embedded systems, breaking down the artificial barriers that have isolated embedded development.
This democratization doesn’t come at the cost of performance or control. The generated code maintains the efficiency and determinism required for embedded systems, while the type system provides safety guarantees beyond what traditional approaches can offer. The result is a development experience that feels like high-level programming but produces code suitable for the most constrained environments.
The garden path created by Fidelity to travel from the safety of F# to operating “on the metal” represents more than just a technical evolution—it’s a recognition that embedded systems are too important to remain accessible only to specialists. As computing continues to spread into every aspect of our physical world, the ability to program these systems safely and productively becomes increasingly crucial. The Fidelity Framework, with its combination of F#’s expressiveness, MLIR’s flexibility, and LLVM’s performance, offers a path toward embedded development that balances creativity and control.