summaryrefslogtreecommitdiff
path: root/src/soc.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/soc.zig')
-rw-r--r--src/soc.zig132
1 files changed, 132 insertions, 0 deletions
diff --git a/src/soc.zig b/src/soc.zig
new file mode 100644
index 0000000..f022b50
--- /dev/null
+++ b/src/soc.zig
@@ -0,0 +1,132 @@
+//! ESP32-P4 peripherals, modelled at comptime.
+//!
+//! There is no HAL here and no generated 20k-line register header: a `Reg` is a typed pointer to
+//! an MMIO word, and a peripheral is a struct of them. Everything is `inline`, so `gpio.setHigh(20)`
+//! compiles to the single `sw` instruction it should be, and a wrong bit index is a compile error
+//! rather than a silent write.
+//!
+//! Addresses are from ESP-IDF v6.0.2 `components/soc/esp32p4/register/hw_ver1/soc/` - the pre-v3
+//! header set, which is the one that matches this silicon (rev v1.3) - except the GPIO matrix
+//! signal index, which lives in `components/soc/esp32p4/include/soc/gpio_sig_map.h`.
+
+const std = @import("std");
+
+/// A 32-bit memory-mapped register.
+pub fn Reg(comptime addr: usize) type {
+ return struct {
+ pub const address = addr;
+ const ptr: *volatile u32 = @ptrFromInt(addr);
+
+ pub inline fn read() u32 {
+ return ptr.*;
+ }
+ pub inline fn write(value: u32) void {
+ ptr.* = value;
+ }
+ pub inline fn set(mask: u32) void {
+ ptr.* = ptr.* | mask;
+ }
+ pub inline fn clear(mask: u32) void {
+ ptr.* = ptr.* & ~mask;
+ }
+ /// Read-modify-write a bitfield: `modify(.{ .shift = 12, .width = 3 }, 5)`.
+ pub inline fn modify(comptime field: Field, value: u32) void {
+ const mask: u32 = ((@as(u32, 1) << field.width) - 1) << field.shift;
+ ptr.* = (ptr.* & ~mask) | ((value << field.shift) & mask);
+ }
+ };
+}
+
+pub const Field = struct { shift: u5, width: u5 };
+
+/// A register with a named layout: pass a packed struct whose bit width is 32 and the accessors
+/// become typed, so a pad is configured by naming fields instead of shifting bits. Read-modify-
+/// write stays explicit - `var v = reg.read(); v.mcu_sel = 1; reg.write(v);` - because that is one
+/// load and one store, and hiding it behind a partial-update type buys nothing here.
+pub fn Typed(comptime T: type, comptime addr: usize) type {
+ comptime std.debug.assert(@bitSizeOf(T) == 32);
+ return struct {
+ pub const address = addr;
+ const ptr: *volatile T = @ptrFromInt(addr);
+
+ pub inline fn read() T {
+ return ptr.*;
+ }
+ pub inline fn write(value: T) void {
+ ptr.* = value;
+ }
+ /// Apply `f` to the current value and write the result back.
+ pub inline fn modify(comptime f: fn (T) T) void {
+ ptr.* = f(ptr.*);
+ }
+ };
+}
+
+/// An array of identical registers. The index type is narrowed to the array's real range, so an
+/// out-of-range access is a compile error in every optimize mode - an `assert` would have been
+/// compiled out under ReleaseSmall, which is this project's default.
+pub fn RegArray(comptime base: usize, comptime stride: usize, comptime count: usize) type {
+ return struct {
+ pub const Index = std.math.IntFittingRange(0, count - 1);
+
+ /// comptime, because `IntFittingRange` rounds up to a whole width: for a 57-entry array the
+ /// index type is u6, which would happily accept 57..63. Every caller here passes a comptime
+ /// pin anyway, so this costs nothing and makes the bound real in all optimize modes.
+ pub inline fn at(comptime index: Index) *volatile u32 {
+ comptime std.debug.assert(index < count);
+ return @ptrFromInt(base + @as(usize, index) * stride);
+ }
+ };
+}
+
+const hp_periph1 = 0x500C0000;
+
+/// GPIO and the IO MUX now live in the HAL, which builds them out of ESP-IDF's own register macros
+/// (`hal/gpio.zig`) instead of the hand-transcribed addresses that used to be here. The
+/// transcription is exactly the kind of thing that goes quietly wrong: this file's matrix constant
+/// said 256 with a comment warning that the S3's is 128, and the first hand-written replacement in
+/// the HAL used 128 anyway. It now comes from `SIG_GPIO_OUT_IDX` in IDF's `gpio_sig_map.h`.
+pub const gpio = @import("hal").gpio;
+
+/// Mask ROM routines. These are the only "library" a bare image links against: the addresses come
+/// from `components/esp_rom/esp32p4/ld/esp32p4.rom.ld` and the linker script re-declares them.
+pub const rom = struct {
+ pub extern fn ets_printf(fmt: [*:0]const u8, ...) c_int;
+ pub extern fn ets_delay_us(us: u32) void;
+
+ pub inline fn print(comptime fmt: [*:0]const u8, args: anytype) void {
+ _ = @call(.auto, ets_printf, .{fmt} ++ args);
+ }
+};
+
+/// Busy-wait for a number of CPU cycles, using the cycle counter rather than the mask ROM. Useful
+/// when an image must not depend on ROM entry points at all, and for delays shorter than the ROM's
+/// microsecond granularity.
+pub inline fn delayCycles(n: u64) void {
+ const start = cycles();
+ while (cycles() - start < n) {}
+}
+
+/// Cycle counter: CSR 0xC00/0xC80, i.e. `cycle`/`cycleh` - the unprivileged shadows of mcycle, and
+/// what ESP-IDF itself reads on this part (`rv_utils.h`: `RV_READ_CSR(cycle)`, because
+/// SOC_CPU_HAS_CSR_PC is not defined for the P4).
+///
+/// Read high-low-high: two separate CSR reads can straddle a wrap of the low word, which would
+/// otherwise report a value 2^32 too large roughly every 47 seconds at 90 MHz.
+pub inline fn cycles() u64 {
+ while (true) {
+ var hi0: u32 = undefined;
+ var lo: u32 = undefined;
+ var hi1: u32 = undefined;
+ asm volatile ("csrr %[r], 0xC80"
+ : [r] "=r" (hi0),
+ );
+ asm volatile ("csrr %[r], 0xC00"
+ : [r] "=r" (lo),
+ );
+ asm volatile ("csrr %[r], 0xC80"
+ : [r] "=r" (hi1),
+ );
+ if (hi0 == hi1) return (@as(u64, hi0) << 32) | lo;
+ }
+}