CXXRTL bindings for Zig.
ili9341spi uses this in combination with Niar to build a CXXRTL simulation with Amaranth and Zig.
Note
This guide assumes you're driving the build from outside, and use Zig's build
system just to build the Zig parts and link the final object. This gives you a
lot of flexibility, but if you don't need it, you can simplify by bringing the
CXXRTL object file building into your build.zig
too. Refer to zxxrtl's
build.zig
for guidance.
Add zxxrtl to your build.zig.zon
:
zig fetch --save https://github.com/kivikakk/zxxrtl/archive/<commit>.tar.gz
Now let's add the import to your build.zig
. We'll take the CXXRTL object file
list as an option in the build()
function:
const cxxrtl_o_paths = b.option([][]const u8, "cxxrtl_o_path", "path to .o file to link against")
orelse &[_][]const u8{"../build/cxxrtl/ili9341spi.o"};
We supply a default value for the object file paths --- it should match your development environment. This is to ensure ZLS still works.
Then add the dependency, and import the module into your executable:
const zxxrtl_mod = b.dependency("zxxrtl", .{
.target = target,
.optimize = optimize,
}).module("zxxrtl");
exe.root_module.addImport("zxxrtl", zxxrtl_mod);
The last step is to link against the CXXRTL object files:
for (cxxrtl_o_paths) |cxxrtl_o_path| {
exe.addObjectFile(b.path(cxxrtl_o_path));
}
If you want to be able to specify the Yosys data dir from the zig build
line,
you can specify it when adding the zxxrtl dependency. Here we add an option,
with a default fallback to actually calling yosys-config --datdir
for ZLS or
the lazy:
const yosys_data_dir = b.option([]const u8, "yosys_data_dir", "yosys data dir (per yosys-config --datdir)")
orelse @import("zxxrtl").guessYosysDataDir(b);
Now adapt the b.dependency()
call:
const zxxrtl_mod = b.dependency("zxxrtl", .{
.target = target,
.optimize = optimize,
.yosys_data_dir = yosys_data_dir,
}).module("zxxrtl");
const Cxxrtl = @import("zxxrtl");
// Initialise the design.
const cxxrtl = Cxxrtl.init();
// Optionally start recording VCD. Assume `vcd_out` is `?[]const u8` representing an
// optional output filename.
var vcd: ?Cxxrtl.Vcd = null;
if (vcd_out != null) vcd = Cxxrtl.Vcd.init(cxxrtl);
defer {
if (vcd) |*vcdh| vcdh.deinit();
cxxrtl.deinit();
}
// Get handles to the clock and reset lines.
const clk = cxxrtl.get(bool, "clk");
const rst = cxxrtl.get(bool, "rst"); // These are of type `Cxxrtl.Object(bool)`.
// Reset for a tick.
rst.next(true);
clk.next(false);
cxxrtl.step();
if (vcd) |*vcdh| vcdh.sample();
clk.next(true);
cxxrtl.step();
if (vcd) |*vcdh| vcdh.sample();
rst.next(false);
// Play out 10 cycles.
for (0..10) |_| {
clk.next(false);
cxxrtl.step();
if (vcd) |*vcdh| vcdh.sample();
clk.next(true);
cxxrtl.step();
if (vcd) |*vcdh| vcdh.sample();
}
if (vcd) |*vcdh| {
// Assume `alloc` exists.
const buffer = try vcdh.read(alloc);
defer alloc.free(buffer);
var file = try std.fs.cwd().createFile(vcd_out.?, .{});
defer file.close();
try file.writeAll(buffer);
}
Cxxrtl.Object(T)
is the basic interface to CXXRTL objects. It exposes two
methods: curr(Self) T
, and next(Self, T) void
, which get the current value,
and set the next value respectively.
There's also a helper, Cxxrtl.Sample(T)
, which is used for change detection in
driver loops: you call its tick(Self)
on every trigger edge, and then can
query its prev
and curr
values, and if it's stable(Self)
. If T == bool
,
you can also ask whether it's falling(Self)
, rising(Self)
,
stable_low(Self)
or stable_high(Self)
.
The following example is adapted from an SPI peripheral blackbox. Each byte of
payload from the design is specified as data or command depending on the dc
line during the last bit. The module returns events to the caller on each tick.
const std = @import("std");
const Cxxrtl = @import("zxxrtl");
const SpiConnector = @This();
clk: Cxxrtl.Sample(bool),
res: Cxxrtl.Sample(bool),
dc: Cxxrtl.Sample(bool),
copi: Cxxrtl.Sample(bool),
sr: u8 = 0,
index: u8 = 0,
const Tick = union(enum) {
Nop,
Command: u8,
Data: u8,
};
pub fn init(cxxrtl: Cxxrtl) SpiConnector {
const clk = Cxxrtl.Sample(bool).init(cxxrtl, "spi_clk", false);
const res = Cxxrtl.Sample(bool).init(cxxrtl, "spi_res", false);
const dc = Cxxrtl.Sample(bool).init(cxxrtl, "spi_dc", false);
const copi = Cxxrtl.Sample(bool).init(cxxrtl, "spi_copi", false);
return .{
.clk = clk,
.res = res,
.dc = dc,
.copi = copi,
};
}
pub fn tick(self: *SpiConnector) Tick {
const clk = self.clk.tick();
const res = self.res.tick();
const dc = self.dc.tick();
const copi = self.copi.tick();
var result: Tick = .Nop;
if (res.curr) {
self.sr = 0;
self.index = 0;
}
if (clk.rising()) {
self.sr = (self.sr << 1) | @as(u8, (if (copi.curr) 1 else 0));
if (self.index < 7)
self.index += 1
else if (dc.curr) {
result = .{ .Command = self.sr };
self.index = 0;
} else {
result = .{ .Data = self.sr };
self.index = 0;
}
}
return result;
}
This is a very simple use case. For a relatively overcomplicated one, see
sh1107's I2CConnector
.