diff options
Diffstat (limited to 'src/mmio.zig')
| -rw-r--r-- | src/mmio.zig | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/src/mmio.zig b/src/mmio.zig new file mode 100644 index 0000000..4e57985 --- /dev/null +++ b/src/mmio.zig @@ -0,0 +1,261 @@ +//! The typed layer over ESP-IDF's register macros. +//! +//! `@import("regs")` is `zig translate-c` over every `*_reg.h` header of the ESP32-P4: about 86,000 +//! flat constants, three per field - `X_REG` (address), `X_S` (shift), `X_V` (unshifted value mask). +//! Those are the right numbers but the wrong shape; writing `p.* = (p.* & ~(v << s)) | (x << s)` by +//! hand at every call site is how register bugs are made. +//! +//! This module turns those triples into checked accessors, at comptime, with no generated code: +//! +//! const conf0 = mmio.Reg.at(regs.LEDC_CH0_CONF0_REG); +//! const timer_sel = mmio.Field.of(regs.LEDC_TIMER_SEL_CH0_S, regs.LEDC_TIMER_SEL_CH0_V); +//! +//! conf0.set(timer_sel, 2); // read-modify-write just that field +//! conf0.modify(.{ timer_sel.is(2), en.is(1) }); // several fields, one store, rest preserved +//! conf0.write(.{ timer_sel.is(2), en.is(1) }); // several fields, one store, rest ZEROED +//! const t = conf0.get(timer_sel); +//! +//! **`modify` is the default; `write` is the exception.** The difference is what happens to the bits +//! you did not name, and on this chip that is usually not "nothing to worry about": 4,126 of the +//! 20,007 documented fields (20.6%) have a non-zero reset value, and within the headers a low-level +//! driver actually touches, 628 of 1,345 registers (46.7%) contain at least one. A `write` that +//! names two fields silently zeroes those, so it is correct only where the whole word is being +//! established deliberately. +//! +//! The converse hazard is narrower than it looks. Read-modify-write is only unsafe on +//! write-1-to-clear and read-to-clear bits - 139 of 5,365 registers, 28 of them in scope here - +//! because a plain self-clearing (`WT`) or write-only bit reads back as 0, so the read-modify-write +//! rewrites 0 and triggers nothing. The registers that genuinely need care are the interrupt-status +//! ones, and they are recognisable: `INT`, `ST`, `RAW` in the name. +//! +//! For the write-1-to-set/write-1-to-clear *alias* registers (`GPIO_OUT_W1TS_REG` and friends) +//! neither applies - the right operation is `writeRaw(mask)`, and the hardware does the rest. +//! +//! Everything here is `inline` and comptime-folded: a `set` of a constant field with a constant +//! value compiles to the same three instructions as the hand-written version, and a composed +//! `write` of constants compiles to a single `li`/`sw` pair. + +const std = @import("std"); + +/// A C macro value from translate-c (`c_int`, `c_uint`, comptime_int) as a u32 address, checked. +/// +/// translate-c types most of these as `c_int`, i.e. signed. Any register address that overflowed +/// into negative would silently become a wild pointer, so the cast is a comptime assertion instead. +pub inline fn addr(comptime macro: anytype) u32 { + comptime { + const v = @as(i64, macro); + if (v < 0 or v > 0xffff_ffff) @compileError(std.fmt.comptimePrint( + "register address {d} is not a 32-bit address - translate-c signedness or the wrong macro", + .{v}, + )); + return @intCast(v); + } +} + +/// One field of a register: where it sits and how wide it is. +/// +/// Built from the `_S` and `_V` macro pair. `_V` is the *unshifted* mask, so it must be +/// `2^width - 1`; anything else means the macro is not a field mask and the caller has picked up +/// the wrong constant (`_M`, the pre-shifted mask, is the usual mistake). +pub const Field = struct { + shift: u5, + width: u6, + + pub inline fn of(comptime shift_macro: anytype, comptime mask_macro: anytype) Field { + comptime { + const s = @as(i64, shift_macro); + const m = @as(i64, mask_macro); + if (s < 0 or s > 31) @compileError(std.fmt.comptimePrint("field shift {d} out of range", .{s})); + if (m <= 0) @compileError(std.fmt.comptimePrint("field mask {d} is not positive", .{m})); + const um: u64 = @intCast(m); + if (um & (um + 1) != 0) @compileError(std.fmt.comptimePrint( + "field mask 0x{x} is not 2^n-1 - this looks like a pre-shifted _M macro, not a _V mask", + .{um}, + )); + const width = 64 - @clz(um); + if (s + width > 32) @compileError(std.fmt.comptimePrint( + "field at bit {d} is {d} bits wide, which runs past bit 31", + .{ s, width }, + )); + return .{ .shift = @intCast(s), .width = @intCast(width) }; + } + } + + /// A single-bit field, for the `(BIT(n))` style macros that carry no separate `_S`/`_V` pair. + pub inline fn bit(comptime n: anytype) Field { + comptime { + const b = @as(i64, n); + if (b < 0 or b > 31) @compileError(std.fmt.comptimePrint("bit {d} out of range", .{b})); + return .{ .shift = @intCast(b), .width = 1 }; + } + } + + /// Mask in place, i.e. what `_M` would have been. + pub inline fn mask(self: Field) u32 { + return self.unshiftedMask() << self.shift; + } + + pub inline fn unshiftedMask(self: Field) u32 { + return if (self.width >= 32) 0xffff_ffff else (@as(u32, 1) << @intCast(self.width)) - 1; + } + + pub inline fn max(self: Field) u32 { + return self.unshiftedMask(); + } + + /// Pair this field with a value, for a composed `Reg.write`. + pub inline fn is(self: Field, value: u32) Value { + return .{ .field = self, .value = value }; + } +}; + +/// A field/value pair, the argument type of `Reg.write`. +pub const Value = struct { + field: Field, + value: u32, +}; + +/// A 32-bit MMIO register. +pub const Reg = struct { + address: usize, + + pub inline fn at(comptime macro: anytype) Reg { + return .{ .address = addr(macro) }; + } + + /// For registers the HAL reaches by computed address (per-channel strides). + pub inline fn atAddress(a: usize) Reg { + return .{ .address = a }; + } + + pub inline fn ptr(self: Reg) *volatile u32 { + return @ptrFromInt(self.address); + } + + pub inline fn raw(self: Reg) u32 { + return self.ptr().*; + } + + pub inline fn writeRaw(self: Reg, v: u32) void { + self.ptr().* = v; + } + + /// Read one field, shifted down. + pub inline fn get(self: Reg, f: Field) u32 { + return (self.raw() >> f.shift) & f.unshiftedMask(); + } + + /// Read-modify-write one field, preserving every other bit. The right default. Unsafe only on + /// write-1-to-clear / read-to-clear bits, i.e. interrupt-status registers. + pub inline fn set(self: Reg, f: Field, value: u32) void { + const p = self.ptr(); + p.* = (p.* & ~f.mask()) | ((value & f.unshiftedMask()) << f.shift); + } + + /// Compose one store from a tuple of `field.is(value)` pairs, **zeroing every bit not named**. + /// Use only when establishing a whole word deliberately; `modify` is what a driver usually + /// wants, because nearly half the registers here have a field whose reset value is not zero. + /// + /// Naming two fields that share a bit is asserted against: it means one of the two constants is + /// wrong, and the hardware would silently get whichever won. + pub inline fn write(self: Reg, values: anytype) void { + var acc: u32 = 0; + var seen: u32 = 0; + inline for (values) |v| { + const m = v.field.mask(); + // A debug assert rather than a compile error: the pairs carry runtime values, so the + // geometry is not always comptime-known at this point. It fires in host tests and in + // Debug builds, and costs nothing in ReleaseSmall. + std.debug.assert(seen & m == 0); + seen |= m; + acc |= (v.value & v.field.unshiftedMask()) << v.field.shift; + } + self.writeRaw(acc); + } + + /// Read-modify-write several fields in one store, leaving every other bit as it was. + pub inline fn modify(self: Reg, values: anytype) void { + var keep: u32 = 0xffff_ffff; + var acc: u32 = 0; + inline for (values) |v| { + const m = v.field.mask(); + std.debug.assert(keep & m != 0); + keep &= ~m; + acc |= (v.value & v.field.unshiftedMask()) << v.field.shift; + } + const p = self.ptr(); + p.* = (p.* & keep) | acc; + } + + /// Spin until a field reads the wanted value. Returns false on timeout rather than hanging: + /// a peripheral that never answers is a bug to report, not a board to power-cycle. + pub inline fn waitFor(self: Reg, f: Field, want: u32, spins: u32) bool { + var n: u32 = 0; + while (n < spins) : (n += 1) { + if (self.get(f) == want) return true; + } + return false; + } +}; + +/// An array of identical registers, for the per-channel blocks (LEDC channels, timer groups, UARTs) +/// whose macros come one-per-instance. The stride is checked against a second instance's macro, so +/// a wrong stride is a compile error rather than a wild write into the next channel. +pub fn RegArray(comptime first: anytype, comptime second: anytype, comptime count: u32) type { + return struct { + pub const base = addr(first); + pub const stride = addr(second) - addr(first); + pub const len = count; + + comptime { + if (addr(second) <= addr(first)) @compileError("RegArray: second instance is not above the first"); + } + + pub inline fn at(i: u32) Reg { + std.debug.assert(i < count); + return Reg.atAddress(base + stride * i); + } + }; +} + +test "field geometry is derived from the macro pair" { + const f = Field.of(5, 0x3ff); // LEDC_OVF_NUM_CH0: bitpos [14:5] + try std.testing.expectEqual(@as(u5, 5), f.shift); + try std.testing.expectEqual(@as(u6, 10), f.width); + try std.testing.expectEqual(@as(u32, 0x3ff << 5), f.mask()); + try std.testing.expectEqual(@as(u32, 1023), f.max()); +} + +test "single bit fields" { + const f = Field.bit(2); + try std.testing.expectEqual(@as(u32, 4), f.mask()); + try std.testing.expectEqual(@as(u6, 1), f.width); +} + +test "composed write builds one word" { + // The bit pattern a real LEDC channel enable would produce: timer_sel=2, sig_out_en=1. + const timer_sel = Field.of(0, 0x3); + const sig_out_en = Field.bit(2); + var cell: u32 = 0xffff_ffff; + const r = Reg.atAddress(@intFromPtr(&cell)); + r.write(.{ timer_sel.is(2), sig_out_en.is(1) }); + try std.testing.expectEqual(@as(u32, 0b110), cell); +} + +test "modify preserves unnamed bits, write does not" { + const lo = Field.of(0, 0xf); + var cell: u32 = 0xdead_beef; + const r = Reg.atAddress(@intFromPtr(&cell)); + r.modify(.{lo.is(0x5)}); + try std.testing.expectEqual(@as(u32, 0xdead_bee5), cell); + r.write(.{lo.is(0x5)}); + try std.testing.expectEqual(@as(u32, 0x5), cell); +} + +test "values wider than the field are truncated, not smeared into neighbours" { + const f = Field.of(4, 0xf); + var cell: u32 = 0; + const r = Reg.atAddress(@intFromPtr(&cell)); + r.set(f, 0xff); + try std.testing.expectEqual(@as(u32, 0xf0), cell); +} |
