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