Tagging structs

I interact with a device using “registers” at specific “addresses”.

I respresent each address as an enum (RegisterMap) and each register as a packed struct.

pub const RegisterMap = enum(u16) {
    DL_information = 0x0000,
    station_address = 0x0010,
    DL_control = 0x0100,
    DL_control_enable_alias_address = 0x0103,
    DL_status = 0x0110,
    AL_control = 0x0120,
    AL_status = 0x0130,
    PDI_control = 0x0140,
    // many more ...
}

pub const DLStatusRegister = packed struct {
    pdi_operational: bool,
    watchdog_ok: bool,
    exteded_link_detection: bool,
    reserved: u1 = 0,
    port0_link_status: bool,
    port1_link_status: bool,
    port2_link_status: bool,
    port3_link_status: bool,
    port0_loop_active: bool,
    port0_rx_signal_det: bool,
    port1_loop_active: bool,
    port1_rx_signal_det: bool,
    port2_loop_active: bool,
    port2_rx_signal_det: bool,
    port3_loop_active: bool,
    port3_rx_signal_det: bool,
};

Then I use a generic function (that only accepts packed structs) to write to the register, here is an example:

pub fn assignStationAddress(port: *Port, station_address: u16, ring_position: u16, recv_timeout_us: u32) Port.SendDatagramWkcError!void {
    const autoinc_address = Subdevice.autoincAddressFromRingPos(ring_position);
    try port.apwrPackWkc(
        esc.ConfiguredStationAddressRegister{
            .configured_station_address = station_address,
        },
        telegram.PositionAddress{
            .autoinc_address = autoinc_address,
            .offset = @intFromEnum(esc.RegisterMap.station_address),
        },
        recv_timeout_us,
        1,
    );
}

Here are some issues I have:

  1. There is nothing stopping me from writing to the wrong address accidentally. DL_status = 0x0110 is not tied to DLStatusRegister.
  2. I don’t love the idea of adding a magic decl to the packed structs, like pub const _address = 0x0110. This would likely work but I am wondering if there is a better, more type safe, and more explicit way to accomplish what I want.

The magic decl option:

pub const DLStatusRegister = packed struct {
    pub const _address: u16 = 0x0110;

    pdi_operational: bool,
    watchdog_ok: bool,
    exteded_link_detection: bool,
    reserved: u1 = 0,
    port0_link_status: bool,
    port1_link_status: bool,
    //...
}

I think you can use union(enum(u16)) to describe that. Something along the lines of

const Register = union(enum(u16)) {
    DL_status: StatusRegister = 0x0110,
    // ...,
};

If there’s only one address per type, just do a switch on the type to get the address:

// You can edit this code!
// Click into the editor and start typing.
const std = @import("std");
const builtin = @import("builtin");

pub fn main() void {
    doThingWithType(A{});
    doThingWithType(B{});
}

pub const A = struct {};
pub const B = struct {};

fn doThingWithType(v: anytype) void {
    const T = @TypeOf(v);
    std.debug.print("{s} has addr {d}\n", .{ @typeName(T), addr(T) });
}

fn addr(T: type) u16 {
    return switch (T) {
        A => 1,
        B => 2,
        else => unreachable,
    };
}

(Assuming these “addresses” aren’t actually memory addresses. If they are, statically initialized pointers should probably be involved, but I haven’t done any embedded Zig so I haven’t tried anything like that.)

2 Likes