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 runzvm i master --zlsto install bothzigandzls.
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 former creates a snapshot in
zig-pkg/, whereas the latter will track updates after a git pull and is
probably more practical for development at the moment.
-
Zig fetch: create a new zig project and add the Muzig package dependency:
mkdir muapp cd muapp zig init zig fetch --save git+https://codeberg.org/jcw/muzig.git -
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 initIn this case,
build.zig.zonalso needs to be told where to find Muzig:.dependencies = .{ .muzig = .{ .path = "../muzig" }, },
The zig-generated files include a lot of comments ... feel free remove them.
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¶
The source code for these examples can be found in the examples/src/ folder in
the Muzig repository.
The next two examples are NOT the way to do things in Muzig, but an illustration of low-level hardware register access.
LED's blink¶
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 {
// set up PB1 in push-pull output mode
RCC.AHB1ENR.GPIOBEN = 1;
GPIOB.MODER.MODER1 = 1;
while (true) {
GPIOB.ODR.ODR1 ^= 1; // toggle PB1
for (0..10_000_000) |_| asm volatile ("");
}
}
comptime {
_ = mu; // needed to bring in the startup code
}
It only uses direct register access. With minor adjustments, this code will work with any GPIO pin and any STM32 µC.
Or let's talk¶
Another minimal example illustrates how to communicate over a UART by addressing all the hardware registers directly:
pub const Chip = @import("chip");
const RCC = Chip.RCC.regs;
const GPIOC = Chip.GPIOC.regs;
const UART = Chip.USART6.regs;
const mu = @import("muzig");
pub fn main() !void {
const hz = 16_000_000;
const baud = 115_200;
// set the PC6 & PC7 UART pins to alt mode 8
RCC.AHB1ENR.GPIOCEN = 1;
GPIOC.MODER.MODER6 = 2;
GPIOC.AFR[0].AFR6 = 8;
GPIOC.MODER.MODER7 = 2;
GPIOC.AFR[0].AFR7 = 8;
// set the baud rate and enable the UART
RCC.APB2ENR.USART6EN = 1;
UART.BRR.BRR = @intCast(hz / baud);
UART.CR1 = .{ .TE = 1, .RE = 1, .UE = 1 };
// poll for incoming data and send it back out
while (true) {
while (UART.ISR.RXNE == 0) {}
UART.TDR.DR = UART.RDR.DR;
}
}
comptime {
_ = mu; // needed to bring in the startup code
}
This code uses USART6 on PC6 and PC7, sets it to 115200 baud with default "8N1" framing, and enters a loop to echo all incoming bytes back out.
Hello from Muzig¶
The previous examples used low-level register access. This example combines both and simplifies the code a bit:
const std = @import("std");
pub const Chip = @import("chip");
const mu = @import("muzig");
const Pin = mu.gpio.Pin;
const Uart = mu.uart;
const SYSTICK = mu.cortex.SYSTICK;
const led: Pin = .B1;
pub fn main() !void {
led.mode("P"); // push-pull output
SYSTICK.setRate(250, mu.stm32.hz); // 4 toggles/sec: 2 Hz blink rate
var console = Uart.Def(.{
.dev = Chip.USART6,
.baud = 115_200,
.tx = .C6,
.rx = .C7,
}).init();
for ("\nHello? ... ") |c|
console.putc(c);
while (true)
if (console.getc()) |c|
console.putc(c);
}
pub fn sysTick_Handler() callconv(.c) void {
led.toggle();
}
comptime {
_ = mu; // needed to bring in the startup code
}
Pinis a type which simplifies the use of names and setup of GPIO pinsUart.Defis a way to defined a UART type and.init()then configures itSYSTICKprovides access to the ARM Cortex SysTick hardware registers- there's no need to specify "ALT" modes, Muzig extracts this from the Chip file
- the bus clock divider for USART6 is also extracted from the Chip file
- the startup code sees the
systick_Handlerand ties it into the IRQ vector
This is now a startup greeting, an echo loop, and a blinking LED driven by SysTick interrupts triggering in the background.
Formatted output¶
With just a minor addition, the UART can be tied into Zig's formatted I/O functions in the standard library:
pub const Chip = @import("chip");
const mu = @import("muzig");
const Uart = mu.uart;
const SYSTICK = mu.cortex.SYSTICK;
pub fn main() !void {
const ms = 250; // 4 Hz
SYSTICK.setRate(ms, mu.stm32.hz);
var console = mu.uart.Def(.{ .dev = Chip.USART6, .tx = .C6 }).init();
const stdout = &console.writer; // set up a std.Io.Writer
var ticks: u32 = 0;
while (true) {
try stdout.print("{} ms\n", .{ticks});
asm volatile ("wfi"); // go to sleep
ticks += ms;
}
}
comptime {
_ = mu; // needed to bring in the startup code
}
- all the
std.Iomagic is based onconst stdout = &console.writer; - the SysTick interrupt still fires, but now it merely wakes up from "WFI"
- on each wakeup, the elapsed time is updated and printed on the console
- the blinking LED code has been omitted for brevcity
Run fast and shrink¶
This code switches to the STM32F7's maximum clock speed, and then reports that speed plus some application statistics:
pub const std = @import("std");
pub const Chip = @import("chip");
const mu = @import("muzig");
const Uart = mu.uart;
var stdout: *std.Io.Writer = undefined;
pub fn main() !void {
mu.stm32.fastClock(25, 216); // switch to 216 MHz, using 25 MHz XTAL + PLL
var console = Uart.Def(.{ .dev = Chip.USART6, .tx = .C6 }).init();
stdout = &console.writer;
prAppInfo();
}
fn pr(comptime fmt: []const u8, args: anytype) void {
stdout.print(fmt ++ "\n", args) catch {}; // ignore errors
}
fn prAppInfo() void {
pr("\n{s} {s} @ {} MHz (reset: {}) - {}b text, {}b data, {}b bss", .{
@tagName(Chip.name),
@tagName(Chip.core),
mu.stm32.hz / 1_000_000,
mu.rcc.ResetCause.getAndClear(),
@intFromPtr(&_etext) - @intFromPtr(&_stext),
@intFromPtr(&_edata) - @intFromPtr(&_sdata),
@intFromPtr(&_ebss) - @intFromPtr(&_sbss),
});
}
// these locations are defined in the "linker.ld" file
extern const _stext: u32;
extern const _etext: u32;
extern const _sdata: u32;
extern const _edata: u32;
extern const _sbss: u32;
extern const _ebss: u32;
comptime {
_ = mu; // needed to bring in the startup code
}
Sample output for a "safe" build, which includes numerous safety checks:
STM32F723IE cm7 @ 216 MHz (reset: .rst) - 6360b text, 0b data, 4b bss
Sample output for a "small" build, with all Zig runtime checks dropped:
STM32F723IE cm7 @ 216 MHz (reset: .rst) - 1632b text, 0b data, 4b bss
Note that the startup cause is also mentioned (a hard reset in this case).
Don't panic¶
Safe builds include checks which will trigger a Zig "panic" when they fail. These can be sent to the console port:
const std = @import("std");
pub const Chip = @import("chip");
const mu = @import("muzig");
const Uart = mu.uart;
var stdout: *std.Io.Writer = undefined;
var oops: u8 = 255;
pub fn main() !void {
var console = Uart.Def(.{ .dev = Chip.USART6, .tx = .C6 }).init();
stdout = &console.writer; // set up a std.Io.Writer
oops += 1;
}
fn pr(comptime fmt: []const u8, args: anytype) void {
stdout.print(fmt ++ "\n", args) catch {}; // ignore errors
}
pub const panic = std.debug.FullPanic(myPanic);
fn myPanic(msg: []const u8, fta: ?usize) noreturn {
pr("\n### {s} @ {X:08} ###", .{ msg, fta orelse 0 });
while (true) {}
}
comptime {
_ = mu; // needed to bring in the startup code
}
Sample output:
### integer overflow @ 080002E9 ###
The address is where the panic happened. It can be looked up in the executable.
Tasks and events¶
This example illustrates multi-tasking. It blinks 8 LEDs at different rates,
using multiple tasks with their own delay timers. These tasks are Blinker
objects, with a process() method which will be called for each incoming event.
A first event with "tag" zero is always sent once initialisation is complete.
This event can be used by tasks to set up their initial state:
const std = @import("std");
pub const Chip = @import("chip");
const mu = @import("muzig");
const Pin = mu.gpio.Pin;
const Task = mu.task;
const Event = Task.Event;
const Blinker = struct {
led: Pin,
ms: u16,
task: Task = undefined,
fn init(self: *Blinker) void {
self.task.init(process);
}
fn process(tp: *Task, in: Event, out: Event) Event {
var self: *Blinker = @fieldParentPtr("task", tp);
switch (in.tag) {
0 => self.led.mode("P"),
1 => self.led.toggle(),
else => unreachable,
}
// request a delayed reply with tag 1 from the ticker
mu.ticker.delay(self.ms, 1);
return out;
}
};
pub fn main() !void {
const sel1: Pin = .I10;
const sel2: Pin = .H15;
sel1.mode("P"); // set up the F723 Discovery to enable the pins on
sel2.mode("P"); // the PMOD connector where the LEDs are attached
var blinkers = [_]Blinker{
.{ .led = .I0, .ms = 30 },
.{ .led = .I3, .ms = 50 },
.{ .led = .I2, .ms = 70 },
.{ .led = .I1, .ms = 110 },
.{ .led = .B11, .ms = 130 },
.{ .led = .F11, .ms = 170 },
.{ .led = .G12, .ms = 190 },
.{ .led = .H2, .ms = 230 },
};
mu.ticker.init(10); // 10 ms SysTick
for (&blinkers) |*b|
b.init();
while (true)
asm volatile ("wfi");
}
pub const sysTick_Handler = mu.ticker.sysTick_Handler;
pub const svCall_Handler = Task.svc_ISR;
pub const pendSv_Handler = Task.pend_ISR;
comptime {
_ = mu; // needed to bring in the startup code
}
The "ticker" is a task which sends back events after a specified delay. It uses a priority queue to keep everything sorted, based on SysTick timer interrupts.
The "handler" definitions at the end are needed to route the various interrupt to their proper place. The "SVC" and "PendSV" system interrupts perform some stacking trickery to manage task suspends and resumes, based on their priority.
The while loop at the end of main() is typical for a multi-tasking setup. It
puts the µC to sleep when all tasks are idle and have returned to their caller.
The role of main() is only to set up everything and then let the tasks do the
work.
Background DMA¶
In the previous examples, the UART uses busy polling to send its data out, blocking until everything has been sent. This can be avoided with DMA and a separate output buffer:
pub const Chip = @import("chip");
const mu = @import("muzig");
const Uart = mu.uart;
const SYSTICK = mu.cortex.SYSTICK;
var ticks: u32 = 0;
pub fn main() !void {
SYSTICK.setRate(1, mu.stm32.hz); // 1000 Hz
var console = mu.uart.Def(.{ .dev = Chip.USART6, .tx = .C6 }).init();
const stdout = &console.writer;
// 50*52 bytes @ 115200 baud takes ≈225 ms
var text: [50*52]u8 = @splat(' ');
for (0..50) |i| {
const off = 52*i;
text[off+i] = '\\';
text[off+50] = '|';
text[off+51] = '\n';
}
// without this buffer, the output will stop for 200 ms each time
var buf: [3000]u8 = undefined;
_ = console.enableTxDma(&buf);
while (true) {
try stdout.writeAll(&text);
// needs to be volatile, otherwise the next loop is optimised away
const p: *volatile u32 = &ticks;
const t = p.*;
while (p.* -% t < 200) {} // spend 200 ms doing ... nothing
}
}
pub fn sysTick_Handler() callconv(.c) void {
ticks += 1;
}
comptime {
_ = mu; // needed to bring in the startup code
}
This writes a text buffer which takes some time to complete. Since the extra
buffer is large enough to contain the entire text, writeAll will return right
away, with DMA performing the transfer and leaving the CPU free to do other
things (just keeping the CPU busy, in this case). There are no UART or DMA
interrupts involved in this mode: the UART uses the ARM Cortex's WFE + SEVONPEND
mechanism to determine when the DMA request has been completed.
The result is continuous output, while the CPU is free for other work, or will go to sleep if there is nothing to do.
Buffered input¶
The uart.getc() input function used in a previous example polls the UART's
hardware register, but it'll lose input data if the UART is not polled often
enough. The following example uses DMA with a buffer to collect incoming data in
the background:
pub const Chip = @import("chip");
const mu = @import("muzig");
const Uart = mu.uart;
pub fn main() !void {
var console = mu.uart.Def(.{ .dev = Chip.USART6, .tx = .C6, .rx = .C7 }).init();
const stdout = &console.writer;
const stdin = &console.reader;
// without this buffer, even short multi-char input may get lost
var buf: [20]u8 = undefined;
_ = console.enableRxDma(&buf);
while (true) {
const c = try stdin.takeByte(); // this blocks until there is data
try stdout.print("[{X:02}] '{c}'\n", .{c, c});
}
}
comptime {
_ = mu; // needed to bring in the startup code
}
There are no interrupts, events, or tasks involved. This merely sets up a DMA
stream to store coming data in a circular buffer. Polling is still used to wait
for and consume incoming data, but the chance of data loss is now much lower.
Note that getc() can no longer be used: the data must now be pulled from
the buffer using one of Zig's std.Io.Reader methods.
For an STM32F723, DMA channel setup is automatic. For others, more configuration details can be added in the UART setup.
Pin change events¶
Every STM32 µC supports pin change IRQs via its built-in EXTI hardware.
These can be turned into events to activate tasks:
const std = @import("std");
pub const Chip = @import("chip");
const mu = @import("muzig");
const Pin = mu.gpio.Pin;
pub const svCall_Handler: mu.Isr = mu.task.svc_ISR;
pub const pendSv_Handler: mu.Isr = mu.task.pend_ISR;
const Task = mu.task;
const Event = Task.Event;
const exti = mu.exti;
var stdout: *std.Io.Writer = undefined;
const btn: Pin = .A0;
const Button = struct {
task: Task = undefined,
fn init(self: *Button) void {
self.task.init(process);
}
fn process(_: *Task, in: Event, out: Event) Event {
switch (in.tag) {
0 => exti.attach(btn, .both, 1), // none, rise, fall, or both
1 => pr("button: {}", .{ btn.get() }),
else => unreachable,
}
return out;
}
};
pub fn main() !void {
var console = mu.uart.Def(.{ .dev = Chip.USART6, .tx = .C6 }).init();
stdout = &console.writer;
btn.mode("F");
var button: Button = undefined;
exti.init();
button.init();
pr("Press button ...", .{});
while (true)
asm volatile ("wfi");
}
pub fn pr(comptime fmt: []const u8, args: anytype) void {
stdout.print(fmt ++ "\n", args) catch {}; // ignore errors
}
// important: the IRQ handler needs to be tied to the one defined in exti.zig
pub const EXTI0_Handler = mu.exti.exti_Handler;
comptime {
_ = mu; // needed to bring in the startup code
}
This uses a Button task, since events can only be used with tasks. On startup
(in.tag == 0), the task enables pin change events on the rising & falling
edges of pin PA0, which is tied to a button on the F723 Discovery board used in
this example.
From now on, whenever that button is pressed or released, process() will be
called with event tag "1", as specified during startup. To cancel further
events: re-attach the same pin using .none as request mode.
Wakeup from sleep¶
The real-time clock has a periodic wakeup timer function, which can be used to resume from various low-power modes:
const std = @import("std");
pub const Chip = @import("chip");
const mu = @import("muzig");
const Pin = mu.gpio.Pin;
pub const EXTI0_Handler = mu.exti.exti_Handler;
pub const RTC_WKUP_Handler = mu.clock.wakeup_Handler;
pub const svCall_Handler: mu.Isr = mu.task.svc_ISR;
pub const pendSv_Handler: mu.Isr = mu.task.pend_ISR;
const Task = mu.task;
const Event = Task.Event;
const exti = mu.exti;
const clock = mu.clock;
var stdout: *std.Io.Writer = undefined;
const btn: Pin = .A0;
var task: Task = undefined;
fn process(_: *Task, in: Event, out: Event) Event {
switch (in.tag) {
0 => {
exti.attach(btn, .both, 1); // none, rise, fall, or both
btn.mode("F");
pr("Press button ...", .{});
},
1 => {
pr("Ping = {}", .{btn.get()});
clock.wakeup(1, 2); // send event tag "2" after 1 s
mu.cortex.SCB.SCR.SLEEPDEEP = 1; // enable stop mode
},
2 => {
pr("Pong after {} s", .{in.val});
clock.wakeup(in.val + 1, 2); // increase wakeup period by 1 s
if (in.val == 2) // now also enable standby, i.e. reset on wakeup
Chip.PWR.regs.CR1.PDDS = 1; // in.val 3 will never be reached
},
else => unreachable,
}
for (0..10_000) |_| asm volatile (""); // drain UART before sleeping
return out;
}
pub fn main() !void {
var console = mu.uart.Def(.{ .dev = Chip.USART6, .tx = .C6 }).init();
stdout = &console.writer;
clock.init(.lse);
exti.init();
task.init(process);
while (true)
asm volatile ("wfi");
}
pub fn pr(comptime fmt: []const u8, args: anytype) void {
stdout.print(fmt ++ "\n", args) catch {}; // ignore errors
}
comptime {
_ = mu; // needed to bring in the startup code
}
Sample output:
Press button ...
Ping = 1
Ping = 0
Pong after 1 s
Pong after 2 s
Press button ...
- this example uses EXTI interrupts to respond to button presses
- both button presses and releases are reported, hence the two Ping messages
- when pressed, the RTC wakeup timer is started and STOP mode is enabled
- when the first wakeup triggers, a new one is started, but now in STANDBY mode
- the second wakeup then resumes, but from STANDBY that's always a hard reset
- if the button is pressed before "Pong after 2 s", it restarts the wakeup timer
- the EXTI clock is disabled in STOP mode, so the button no longer responds
- since there's a full reset after STANDBY mode, the whole cycle starts over
The code above also illustrates how to set up a task in main itself, there's no
need for a separate Button struct.
Event-based input¶
The "Buffered input" example helps avoid data loss, but as a blocking mechanism it's not always practical. A much better approach is to have events come in when there is data. The example below implements a UART <=> UART pass-through app, which connects the on-board ESP8266 to the serial console port, with data flowing in both directions:
const std = @import("std");
pub const Chip = @import("chip");
const mu = @import("muzig");
const Task = mu.task;
const Event = Task.Event;
var tty: mu.uart.Def(
.{ .dev = Chip.USART6, .baud = 115_200, .tx = .C6, .rx = .C7 },
) = undefined;
var esp: mu.uart.Def(
.{ .dev = Chip.UART5, .baud = 1_200, .tx = .C12, .rx = .D2 },
) = undefined;
var task: Task = undefined;
fn process(_: *Task, in: Event, out: Event) Event {
switch (in.tag) {
0 => {
tty.write("\nHello ...\n");
tty.onRxAvail(1); // event tag "1" when data is available on tty
esp.onRxAvail(2); // event tag "2" when data is available on esp
},
1 => esp.write(tty.read(in.val)),
2 => tty.write(esp.read(in.val)),
else => {},
}
return out;
}
pub fn main() !void {
tty = .init();
var ttyBuf: [50]u8 = undefined;
_ = tty.enableRxDma(&ttyBuf);
esp = .init();
var espBuf: [50]u8 = undefined;
_ = esp.enableRxDma(&espBuf);
const reset: mu.gpio.Pin = .G14;
reset.mode("P"); // reset the ESP8266
for (0..10_000) |_| asm volatile ("");
reset.set(1);
task.init(process);
while (true)
asm volatile ("wfi");
}
pub const USART6_Handler: mu.Isr = tty.irqHandler();
pub const DMA2_Stream1_Handler: mu.Isr = tty.dmaRxHandler();
pub const UART5_Handler: mu.Isr = esp.irqHandler();
pub const DMA1_Stream0_Handler: mu.Isr = esp.dmaRxHandler();
pub const svCall_Handler: mu.Isr = mu.task.svc_ISR;
pub const pendSv_Handler: mu.Isr = mu.task.pend_ISR;
comptime {
_ = mu; // needed to bring in the startup code
}
Sample output, with the startup greeting and the output of an ATI5 command:
Zimodem ESP8266 Firmware v4.0.2
sdk=2.2.2-dev(38a443e) chipid=1335429 cpu@80
tot=1024k heap=387k fsize=139k
INITIALIZED
READY.
ATI5
ATB1200E1Q0V1X1F4R1&D&MS0=1S2=43S3=13S4=10S5=8S40=127S41=0S43=0S44=0S45=0S46=0S47=2S48=0S49=5S50=0S51=4S52=0S53=14S54=0S55=12S56=0S57=13S60=0S62=1S63=0&P0&O0
OK
- this is now a task which is activated when there are new bytes on either side
- the baud rates are very different, but the buffering helps deal with that
- input events arrive in small chunks, every 25 bytes (half the
espbuffer)- more precisely: every "full" and "half full" DMA IRQ and on UART "idle"
write()andread()are wrappers forstd.Io'swriteAll()andtake()- the six handlers at the end are required to connect to the ARM's IRQ vector
Documentation¶
T.B.D.