An embedded OS in Zig
Zuric is an embedded OS for stm32 hardware written in pure Zig.
Why would one consider using this over a C/C++ or Rust embedded SDK?
Unlike C++, Zig's errors do not require memory allocation making them usable on embedded devices. Unlike C, errors are a part of the language, meaning no more generic "error" status return values that give no indication of what the actual error was.
self.controlLoop() catch |err| switch (err) {
error.ControllerNotReady => {
self.emergencyStop();
self.display_mode = .Reset;
defer self.display_mode = .Position;
while (self.estop_btn.read()) {
// Wait for operator ...
}
},
// other errors..
};
In the example code shown at the below if an I2C connection error to the LCD display occurs it (eg I2CArbitrationLost
) is automatically propagated up and printed in the debug console by the panic handler... without any extra code you can see exactly what went wrong.
Zig's release safe mode significantly speeds up development time by catching programming errors like integer overflows, casting errors, or indexing errors.
Optional types prevent a whole class of errors.
Slices and first class function pointers make code that's easier to read, write, and debug.
Zig's defer statement makes it easy to properly clean up resources.
// Attach to uart read events
try uart.attach(Controller.onUartData, self, .rx);
defer uart.detach(.rx);
Many of Zig's standard library structures work out of the box. There's no need to roll your own fifo's or string formatting.
// and read from outside an ISR
pub const ReadBuffer = struct {
const Fifo = std.fifo.LinearFifo(u8, .{.Static=4096*2});
unprotected_fifo: Fifo,
pub fn init() ReadBuffer{
return ReadBuffer{
.unprotected_fifo = Fifo.init(),
};
}
// Read from the fifo by ensuring that interrupts are disabled
pub fn readBuffered(self: *ReadBuffer) []const u8 {
mcu.arm.core.critical_section.enter();
defer mcu.arm.core.critical_section.exit();
return self.unprotected_fifo.readableSlice(0);
}
// ...
A simple stack based event loop is included in the OS. Async or "generator" functions can be used to simplify multi-tasking.
_ = async self.processTask();
_ = async self.optimizerTask();
_ = async self.uiTask();
Hardware independent code can be unit tested on the spot leading to better code quality.
test "parse-code-simple" {
var r = try Code(f32).parse("G0");
try testing.expectEqual(r.name, 'G');
try testing.expect(r.value == 0);
try testing.expectEqual(r.index, 2);
try testing.expectEqualSlices(u8, r.data, "");
}
Zig's comptime features make it trivial to support multiple devices with a single codebase and tells you exactly what needs implemented to add support for a new device.
pub inline fn enableCrc(self: *SPI) void {
if (self.config.crc) |_| {
switch (mcu.family) {
.stm32h7 => {
reg.setMask(u32, &self.instance.CFG1, hw.SPI_CFG1_CRCEN);
},
.stm32g4 => {
reg.setMask(u32, &self.instance.CR1, hw.SPI_CR1_CRCEN);
},
else => @compileError("Not implemented"),
}
}
}
Zig takes a simple approach to generics that lets you do the same thing as "C++ templates" without the complexity.
The stepper motion controller library can achive over 400khz stepping rates with 3 motors in release fast mode.
The stm32g0 example on the left fits in 11k
bytes when compiled in release-small mode.
Program received signal SIGINT, Interrupt.
0x08005f54 in .mcu.stm32.time.sleep (delay=100) at zuric/lib/mcu/stm32/time.zig:21
21 if (system.time(units) >= expires) break;
(gdb) load
`zuric/targets/stm32g0/firmware.elf' has changed; re-reading symbols.
Loading section .text, size 0x299b lma 0x8000000
Loading section .ARM.exidx, size 0x250 lma 0x800299c
Loading section .data, size 0x8 lma 0x8002bec
Start address 0x0800025c, load size 11251
What does it look like? Here's short example printing the system time and the user button on a NUCLEO-G071RB dev board to an I2C 2 x 16 LCD.
// -------------------------------------------------------------------------- //
// Copyright (c) 2022, CodeLV. //
// Distributed under the terms of the Zuric License. //
// The full license is in the file LICENSE, distributed with this software. //
// -------------------------------------------------------------------------- //
const std = @import("std");
const mcu = @import("mcu");
const gfx = @import("gfx");
const gpio = mcu.gpio;
const time = mcu.time;
const system = mcu.system;
const UART = mcu.uart.UART;
const I2C = mcu.i2c.I2C;
const LCD = gfx.drivers.hd44780u.CharacterDisplay(2, 16);
var serial: UART = undefined;
pub fn main() !void {
// Setup debug serial port
serial = UART.init("USART2", .{.tx = .PA2, .rx = .PA3});
try serial.configure(.{.baudrate = 115200});
mcu.debug.stream = serial.writer();
mcu.debug.info("Started", .{});
var led1 = gpio.Pin.initOutput(.PA5);
var btn1 = gpio.Pin.initInput(.PC13);
var lcd = LCD.init(
I2C.init("I2C1", .{.sda = .PB9, .scl = .PB8 }),
0x27 << 1
);
try lcd.configure(.{});
try lcd.displayOn();
// Start an echo loop
while (true) {
const v = btn1.read();
led1.write(!v);
try lcd.printLine(0, "T: {d}ms", .{system.time(.ms)});
try lcd.printLine(1, "Btn: {s}", .{v});
try lcd.update();
time.sleep(100, .ms);
}
}
Zuric does not start from scratch but leverages Zig's c-translation to reuse device headers provided from the vendor. This means the registers and API's will be familiar to those with experiance in a given platform.
A function from the arm core NVIC API.
// Enables a device specific interrupt in the NVIC interrupt controller.
pub inline fn enableIrq(irqn: IRQn_Type) void {
if (@enumToInt(irqn) < 0) return;
const index = getIrqnIndex(irqn);
const value = @as(u32, 1) << @intCast(u5, @enumToInt(irqn) & 0x1F);
reg.write(u32, &NVIC.ISER[index], value);
}
The current hardware support grid is shown below. Not all device drivers support non-blocking io (ie interrupts).
Board | Clock | GPIO | UART | Timer | SPI | I2C | DAC | ADC | DMA | USB |
---|---|---|---|---|---|---|---|---|---|---|
Nucleo h743zi2 | X | X | X | X | X | X | X | |||
Nucleo l552ze-q | X | X | X | X | ||||||
Nucleo g474re | X | X | X | X | X | X | ||||
Nucleo g071rb | X | X | X | X | X | X |
Various optional device drivers are available:
The project is not yet being released to the public. It will likely be dual licensed with copyleft and commercial. If you are interested in getting access to a pre-release version please contact me. At a minimum you must have access to a scope and have a long standing github account to even be considered.
Zuric is currently internally used for several motion control research projects (a CNC lathe and engraver).
Yes, the code has provisions for supporting other vendors than stm32.
Zuric is an acronym for "Zig on your IC".