Skip to content

Latest commit

 

History

History
224 lines (170 loc) · 5.9 KB

README.md

File metadata and controls

224 lines (170 loc) · 5.9 KB

zxxrtl

CXXRTL bindings for Zig.

Example

ili9341spi uses this in combination with Niar to build a CXXRTL simulation with Amaranth and Zig.

Setup

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));
}

Specify Yosys' data dir

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");

Usage

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.