vfreg translates a CMSIS-SVD
XML description into a single, self-contained Zig source file containing
memory-mapped peripheral, register and bit-field definitions. It is written in
Zig and provides both a command-line tool and a library API.
Requires Zig 0.17.0 or newer. Zig fetches the zig-xml dependency
automatically.
zig build
zig build testThe executable is installed to zig-out/bin/reg (reg.exe on Windows). The
run step forwards its arguments to the executable:
zig build run -- .svd/STM32F103.svd STM32F103.zigreg <input.svd> [output.zig]
If output.zig is omitted, the generated source is written to stdout. Errors
are reported on stderr and terminate with exit status 1.
pub const GPIOx_CTRL_MODE = enum(u2) {
Input = 0x0,
Output = 0x1,
Alternate = 0x2,
_,
};
pub const GPIOx_CTRL = packed struct(u32) {
/// Enable
ENABLE: u1 = 0x1,
/// Mode selection
MODE: GPIOx_CTRL_MODE = .Alternate,
z_reserve_0: u5 = 0,
/// Kept field
KEEP: u8 = 0x0,
z_reserve_1: u16 = 0,
};
pub const GPIOx = extern struct {
/// Control register
/// Access: Read-Write
CTRL: GPIOx_CTRL,
};
pub const GPIOA: *volatile GPIOx = @ptrFromInt(0x40010800);Register layouts are packed structs with explicit backing integer types;
unused bits become z_reserve_N fields. SVD descriptions and register access
rights are emitted as documentation comments, and resetValue, when present,
becomes the default value of each field. Fields with numeric enumeratedValues use an
enum(uN) type; an enum is non-exhaustive when its declared values do not cover
every bit pattern. Peripherals in the same group with identical layouts share a
type using an x placeholder (GPIOx), while every peripheral instance is
emitted as a *volatile pointer constant.
Add vfreg to build.zig.zon as usual. Inside build, with the usual
target and optimize values, generate the register module and import it:
const vfreg = b.dependency("vfreg", .{
.target = b.graph.host,
});
const generator = b.addRunArtifact(vfreg.artifact("reg"));
generator.addFileArg(b.path("input.svd"));
const generated = generator.addOutputFileArg2("registers.zig", .{});
const registers = b.createModule(.{
.root_source_file = generated,
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "registers", .module = registers },
},
}),
});The generator executable runs on the host, while the generated module is compiled for the requested target.
The package also exposes a vfreg module (via dependency.module("vfreg"))
with the parser and generator:
const vfreg = @import("vfreg");
// allocator: std.mem.Allocator
var parser: vfreg.parser.Parser = .init(allocator, svd_bytes);
defer parser.deinit();
const device = try parser.parse();
const zig_source = try vfreg.generator.generate(allocator, device);svd_bytes must remain valid until parse returns. Model strings and the
generated source are allocated with allocator; Device has no deinit, so
using an arena is the simplest way to manage them.
- Alternate register descriptions that overlap an already-described address are omitted, since one Zig struct cannot contain two overlapping fields.
- Clusters are flattened into the peripheral layout, and register/field arrays are emitted as individual members.
- Common
derivedFromforms are resolved (peripherals, clusters, registers, fields and enumerated values); derived names that contain array placeholders (%s) are not resolved. - Address offsets are treated as byte offsets.
- CPU, interrupt and address-block metadata is parsed but not emitted; the output focuses on register access.