Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/kivikakk/zxxrtl

Idiomatic CXXRTL bindings for Zig
https://github.com/kivikakk/zxxrtl

cxxrtl yosys zig

Last synced: 4 days ago
JSON representation

Idiomatic CXXRTL bindings for Zig

Awesome Lists containing this project

README

        

# zxxrtl

[CXXRTL] bindings for Zig.

[CXXRTL]: https://yosyshq.readthedocs.io/projects/yosys/en/latest/cmd/write_cxxrtl.html

## Example

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

[ili9341spi]: https://github.com/kivikakk/ili9341spi
[Niar]: https://github.com/kivikakk/niar
[Amaranth]: https://amaranth-lang.org

## 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.

[zxxrtl's `build.zig`]: https://github.com/kivikakk/zxxrtl/blob/main/build.zig

Add zxxrtl to your `build.zig.zon`:

```console
zig fetch --save https://github.com/kivikakk/zxxrtl/archive/.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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
const zxxrtl_mod = b.dependency("zxxrtl", .{
.target = target,
.optimize = optimize,
.yosys_data_dir = yosys_data_dir,
}).module("zxxrtl");
```

## Usage

```zig
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.

```zig
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`].

[sh1107]: https://github.com/kivikakk/sh1107
[`I2CConnector`]: https://github.com/kivikakk/sh1107/blob/266adfb0bac55f462393e2ee12610cda321de39a/vsh/src/I2CConnector.zig#L125