bcrist/microbe
Embedded Zig framework for ARM microcontrollers
Microbe is a framework for developing embedded firmware in Zig, primarily targeting small 32-bit architectures like ARM Cortex-M0. It is similar in many ways to microzig and in fact some parts were forked directly from that project. But there are a few things about microzig that I didn't like:
comptime parameter. The conflation of chip pins names and board pin names is annoying.I don't bring these up to criticize the microzig project, but rather to highlight the areas where Microbe takes a different path. If you haven't tried microzig yet but you're looking to do embedded programming with Zig, start there first. If you have tried microzig but share some of the feelings I listed above, then this project may be useful to you. And if you're a microzig contributor and think something you see here should be ported, let me know and I'll see if I can help.
Add this package to your build.zig.zon file and import it in your build.zig script as microbe. Then just call microbe.addExecutable(...) instead of std.Build.addExecutable(...), providing the chip and section information for your desired target.
You can find example applications and build.zig scripts here:
There are a few API conventions that should be followed in order for chip-specific code to interact well with application code and the common code in this repo, as well as to make porting between architectures as easy as possible.
Most of the symbols that chip implementations are expected to expose can be found in src/chip_interface.zig.
Chip implementations should provide an enum chip.interrupts.Interrupt which lists all the "external" (i.e. NVIC-controlled) interrupts supported by the chip. The integer values associated with this enum indicate their offset in the NVIC registers. Additionally chip.interrupts.Exception should be an enum which includes all the interrupts, but also may contain synchronous exceptions & fault conditions. The integer values associated with this enum indicate the exception number. An interrupts exception number is generally different from its interrupt number.
Chip implementations should look for an interrupts struct in the root source file and automatically populate the vector table with the addresses of the handlers provided within it. The handler names must match the names from chip.interrupts.Exception exactly.
Chip implementations should provide a struct chip.clocks.Config which allows configuration of all the major clock domains on the chip. The exact format will depend on the details of the architecture, but for every major clock domain in the chip, the clock config should have a field:
xxx_frequency_hz: comptime_int
Where xxx is the name of the clock domain. A frequency of 0 Hz should be considered to mean "clock disabled". If a clock domain is sourced from another clock, it should additionally have a field:
xxx_source: E
Where E is an enum type, or optional-wrapped enum type, giving the options that can be used as a source.
On reset, chip implementations should initialize the chip's clocks based on a clocks constant (of type chip.clocks.Config) declared in the root source file. If no such declaration exists, the chip's default clock configuration should be used.
Chip implementations should also provide chip.clocks.apply_config(...) to allow dynamic clock changes. Peripherals that are sensitive to clock frequencies (UARTs, PWMs etc.) will generally assume the clocks they use do not change, so care must be taken when using this.
Chip implementations may also provide chip.clocks.get_config() to provide a version of the configuration with additional details and defaults filled in. This should be comptime callable. If it does not return a chip.clocks.Config struct, it should return a chip.clocks.Parsed_Config struct.
The clock config struct may also contain fields for configuring low-power modes or other power-related features.
Chip implementations may provide one or more UART implementations that allow std.io streams to be used. If there is only one implementation, chip.uart.UART should be a function that takes a comptime configuration struct and returns an implementation struct. If multiple implementations are provided via separate constructor functions.
The recommended names and types for some common configuration options are:
baud_rate_hz: comptime_intdata_bits: enum.seven or .eight, sometimes maybe other valuesparity: enum.even, .odd, or .nonestop_bits: enum.one or .two, sometimes .one_and_half or .halfwhich: ?enumrx: ?Pad_IDtx: ?Pad_IDcts: ?Pad_IDrts: ?Pad_IDtx_buffer_size: comptime_intrx_buffer_size: comptime_inttx_dma_channel: ?enumrx_dma_channel: ?enumAll UART implementations should expose at least these declarations:
const Data_Type // usually u8
fn init() Self
fn start(*Self) void
fn stop(*Self) void
Implementations that have reception capability should provide:
fn is_rx_idle(*Self) bool // optional; some hardware may not be capable of reporting this
fn get_rx_available_count(*Self) usize
fn can_read(*Self) bool
fn peek(*Self, []Data_Type) Read_Error![]const Data_Type
fn peek_one(*Self) Read_Error!?Data_Type
const Read_Error
const Reader // usually std.io.Reader(..., Read_Error, ...)
fn reader(*Self) Reader
const Read_Error_Nonblocking
const Reader_Nonblocking
fn reader_nonblocking(*Self) Reader_Nonblocking
Read_Error usually consists of some subset of:
error.Overrunerror.Parity_Errorerror.Framing_Errorerror.Break_Interrupterror.Noise_ErrorImplementations that have transmission capability should provide:
fn is_tx_idle(*Self) bool // optional; some hardware may not be capable of reporting this
fn get_tx_available_count(*Self) usize
fn can_write(*Self) bool
const Write_Error
const Writer // usually std.io.Writer(..., Write_Error, ...)
fn writer(*Self) Writer
const Write_Error_Nonblocking
const Writer_Nonblocking
fn writer_nonblocking(*Self) Writer_Nonblocking
The Read_Error_Nonblocking and Write_Error_Nonblocking should generally match include everything from the blocking variants, as well as error.Would_Block, which is returned when the buffer is empty/full and no more data can be read or written. Ideally, when reading or writing multiple words, either the entire operation succeeds, or it has no effect if Would_Block is returned, except for functions that give feedback on how much work they accomplished (e.g. Writer.write). This precludes the use of std.io.Reader/Writer for the non-blocking variants.
Some implementations may require additional functions, e.g. to handle interrupts.