Skip to content

Muzig

Muzig is a library which supports applications for STM32 microcontrollers in Zig (hence the name: "µ" + "zig"). It has a built-in code generator to provide all hardware register definitions for any of the STM32 µC chip families and cores. Hardware registers can be accessed using named structs and bitfields, thanks to an amazing JSON file collection from the Embassy-RS project.

Muzig has optional real-time support, based on a "stacking event-driven task model". All tasks share a single stack, processing incoming events in a nested fashion. Each task has a priority - only higher tasks can pre-empt lower ones. Tasks can send request events to higher tasks, and reply with events to lower tasks. The library uses a single lock-free event queue: interrupt requests are never blocked (except on M0/M0+, which lacks LDREX/STREX support).

Drivers are set up as tasks with optional interrupt handlers. Each handler can then generate events to alert their task as needed. Since events are only consumed when a task is inactive, event processing is atomic by design.

The basic model is that a task's process() is called whenever there is an event, and that a task will only be suspended when a higher task is activated, either by an event sent from this one, or via some interrupt. Apart from these two cases, all calls to process() "run to completion" without having to guard against their own interrupts, for example. If a task does blocking I/O or some amount of CPU processing, it won't return and lower tasks can't proceed.

This design was chosen so that it works with a single stack and so that - with proper task layering - no atomic guards are ever needed at the task / application level. There is no memory allocation in Muzig, the maximum number of tasks and pending events are set at compile time.

The Muzig library aims to be extremely "lean and mean" and can be used on very low-end microcontrollers.

Muzig is work-in-progress and based on a previous design in C++, called "JeeH". See the git repository for further details.

Getting started

Zig

  • The Zig compiler, build system, and standard library are installed as one package with no further dependencies. There are pre-compiled builds for all major platforms, see the https://ziglang.org homepage.

    Muzig currently requires a "master" release. i.e. 0.17.<something>, because it relies on some features which are newer than the 0.16.0 "tagged" release. See https://ziglang.org/learn/getting-started/.

  • There is also a Zig Language Server (zls) which helps code editors do a much better job of pointing out syntax errors and locating code definitions and references in the app and in the standard library. See https://zigtools.org/zls/install/.

  • One way to manage both of the above is to install the Zig Version Manager (zvm) and let it do all the installation and upgrading. See https://www.zvm.app/guides/install-zvm/ and then run zvm i master --zls to install both zig and zls.

Muzig

There are two ways to get a copy of the Muzig source code: 1) as a package, using zig fetch, or 2) as a git clone. The latter is probably more practical for development at the moment.

  1. Zig fetch: create a new zig project and add the Muzig package dependency:

    mkdir muApp
    cd muApp
    zig init
    zig fetch --save https://codeberg.org/jcw/muzig
    
  2. Git clone: make a repository clone and create a new zig project next to it:

    git clone https://codeberg.org/jcw/muzig.git
    mkdir muApp
    cd muApp
    zig init
    

    In this case, build.zig.zon also needs to be told where to find Muzig:

    .dependencies = .{
        .muzig = .{ .path = "../muzig" },
    },
    

Next, add the following snippet to build.zig to automatically build and run the "chipGen" tool, to generate a chip.zig hardware register definition file and a linker.ld µC-specific linker map file:

const muzig = b.dependency("muzig", .{});

const chipGen = b.addExecutable(.{
    .name = "chipGen",
    .root_module = b.createModule(.{
        .root_source_file = muzig.path("tools/chipGen.zig"),
        .target = b.graph.host,
    }),
});

const chipGenTool = b.addRunArtifact(chipGen);
chipGenTool.addArg("STM32F723IE");
const chipMod = chipGenTool.addOutputFileArg("chip.zig");
const linkerLd = chipGenTool.addOutputFileArg("linker.ld");

exe.root_module.addAnonymousImport(
    "chip",
    .{ .root_source_file = chipMod },
);

exe.setLinkerScript(linkerLd);

This assumes that there is an "exe" build step and ties it all together.

Embassy

Note that the chipGen tool takes the exact version of STM32 microcontroller as first argument ("STM32F723IE" in the example above). It determines what gets generated in chip.zig and the memory layout defined in linker.ld.

This relies on the Embassy-RS project's JSON datafiles from embassy-rs/stm32-data-generated on GitHub.

The JSON datafiles need to be installed manually for now ...

A convenient place to put these files is next to the Muzig project area:

cd ..
git clone https://github.com/embassy-rs/stm32-data-generated.git
cd ../muApp
ln -s ../stm32-data-generated/data .

In other words: Muzig expects a data/ folder or symlink in the project area, which chipGen then uses to locate the proper JSON files.

Blackmagic

There are many ways to upload firmware to a microcontroller. One is "Blackmagic Debug" - see https://black-magic.org. It has the benefit of automatically detecting the type of debug probe, i.e. either an ST-Link, a JLink, or a Black Magic Probe.

Firware uploads often need a "bin" file, which can be created as build step:

const bin = exe.addObjCopy(.{ .format = .binary });
const install_bin = b.addInstallBinFile( bin.getOutput(), "muApp.bin");
install_bin.step.dependOn(&install_exe.step);

The upload can then be (yet another) build step:

const flash_run = b.addSystemCommand(&.{ "blackmagic", "-w" });
flash_run.addFileArg("zig-out/bin/muApp.bin");
flash_run.step.dependOn(&install_bin.step);

const run_step = b.step(ex.name, ex.desc);
run_step.dependOn(&flash_run.step);

If all is well, a build + upload should now be a matter of simply typing zig build. With an extra --watch option, this process will even automatically repeat whenever a source file is changed.

Examples

This is a minimal src/main.zig example to blink an LED in the most basic possible way:

pub const Chip = @import("chip");
const RCC = Chip.RCC.regs;
const GPIOB = Chip.GPIOB.regs;

const mu = @import("muzig");

pub fn main() !void {
    RCC.AHB1ENR.GPIOBEN = 1;
    GPIOB.MODER.MODER1 = 1; // PB1 push-pull output mode

    while (true) {
        GPIOB.ODR.ODR1 ^= 1; // toggle PB1
        for (0..10_000_000) |_| asm volatile ("");
    }
}

comptime {
    _ = mu; // needed to bring in the startup code
}

This only uses direct register access. With minor adjustments, this code can work with any GPIO pin and any STM32 µC.

Documentation

T.B.D.