1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
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);
}
|