summaryrefslogtreecommitdiff
path: root/src/mmio.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 12:40:53 -0300
committerGabriel Schneider <[email protected]>2026-08-25 12:46:51 -0300
commitf5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch)
tree2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/mmio.zig
downloadesp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz
esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker. src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers; src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip; src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/mmio.zig')
-rw-r--r--src/mmio.zig261
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);
+}