summaryrefslogtreecommitdiff
path: root/examples/rf.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 /examples/rf.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 'examples/rf.zig')
-rw-r--r--examples/rf.zig613
1 files changed, 613 insertions, 0 deletions
diff --git a/examples/rf.zig b/examples/rf.zig
new file mode 100644
index 0000000..93babbd
--- /dev/null
+++ b/examples/rf.zig
@@ -0,0 +1,613 @@
+//! The RF experiment from `04-report` section 5, re-emitted from this toolchain.
+//!
+//! zig build -Dapp=examples/rf.zig flash
+//! cd ../02-esp32p4-m3-radio && tools/rfprobe.py chop --tag zig-tone-chop --cmd-on T --cmd-off o
+//! cd ../02-esp32p4-m3-radio && tools/rfprobe.py iq --tag zig-tone-burst --cmd-on t --keep-iq
+//! cd ../02-esp32p4-m3-radio && tools/decode_tone.py captures/zig-tone-burst.iq
+//!
+//! The original firmware was ESP-IDF: FreeRTOS tasks, `ledc_timer_config`, `esp_timer_get_time`,
+//! `printf`. This is the same physical experiment driven by this project's own HAL, so the SDR and
+//! the unmodified analysis tools become an external oracle on the HAL: if `hal.ledc`'s divider
+//! arithmetic, `hal.gpio`'s matrix routing or `hal.systimer`'s timebase are wrong, the captured
+//! carrier lands on a different frequency, the pulse widths drift, or the decoded word is not
+//! 0x4200.
+//!
+//! What is reproducible here and what is not, stated up front:
+//!
+//! * **Reproducible: the 25 MHz keyed carrier.** It is the P4's own pin, driven by LEDC. That is
+//! the whole of section 5, including the only real SDR spectrum in the report.
+//! * **Not reproducible: Wi-Fi, BLE, 802.15.4.** The P4 has no radio. Those went out over an
+//! ESP32-C6 across SDIO under `esp_hosted` + `esp_wifi_remote` - a 19,000-line host stack plus a
+//! prebuilt coprocessor binary, none of which exists here and none of which is low-level
+//! hardware. Section 4 of the report is out of this toolchain's scope by construction, and
+//! claiming otherwise would be the dishonest part.
+//!
+//! The frequency is the sharp end. LEDC at 1-bit duty resolution off the 80 MHz PLL-derived clock
+//! cannot synthesise a round 25 MHz: the Q10.8 divider closest to it is 410 (= 1.6015625), giving
+//! 80e6 * 256 / (410 * 2) = 24,975,609 Hz. The report measured 24.977455 MHz by phase slope, +73.9
+//! ppm from that. So the number this firmware should produce is 24.9756 MHz, not 25.0 MHz, and it is
+//! predicted by the divider arithmetic rather than by the requested frequency.
+
+const std = @import("std");
+const soc = @import("soc");
+const hal = @import("hal");
+const regs = @import("regs");
+const mmio = @import("mmio");
+
+pub const panic = std.debug.FullPanic(struct {
+ fn call(msg: []const u8, _: ?usize) noreturn {
+ soc.rom.print("MARK RF_PANIC %s\r\n", .{msg.ptr});
+ while (true) {}
+ }
+}.call);
+
+/// GPIO20, JP1 pin 17: the LED pin when blinking, the carrier pin when keyed. Same pad the report
+/// used, which matters because the antenna coupling is whatever the header wire happens to be.
+const tone_pin: u8 = 20;
+const tone_channel: u32 = 0;
+const tone_timer: u32 = 0;
+
+/// The word, MSB first, and the frame that carries it: 500 ms preamble, 200 ms gap, then 16 cells of
+/// 100 ms carrier-if-set followed by 100 ms silence. 3.9 s total. Identical to the C firmware's
+/// `tone_burst`, because `decode_tone.py` uses PREAMBLE_MS and GAP_MS as known constants.
+const key_word: u16 = 0x4200;
+const preamble_ms: u32 = 500;
+const gap_ms: u32 = 200;
+const cell_ms: u32 = 100;
+
+var carrier_running = false;
+
+/// The console. UART0 is where the CH340 is wired and where the ROM's printf goes, so the transmit
+/// side is already working; this is only ever used to *read* commands.
+///
+/// Reading is the one thing that is safe to do to this peripheral here. Its FIFO register at offset
+/// 0 pops on read - which is exactly what a console reader wants, and is the same property that
+/// makes a register-block snapshot of a UART unsound. Nothing in this file reconfigures UART0: a
+/// reset or a baud change on the console would cut the wire this experiment reports over.
+const console = hal.uart.Uart.init(0);
+
+/// ESP-IDF's own LEDC LL, compiled into this image by `-Doracle`. Present so the two
+/// implementations can be compared with *one* instrument on *one* board in *one* boot: the register
+/// differential already proves they write the same words, so the only question left is behavioural,
+/// and a claim about behaviour needs both sides measured the same way.
+extern fn oracle_ledc_configure_timer(timer: c_uint, src_hz: c_uint, freq_hz: c_int, resolution: c_uint) void;
+extern fn oracle_ledc_configure_channel(channel: c_uint, timer: c_uint, duty: c_uint, hpoint: c_uint, idle_level: c_uint, output_enabled: c_int) void;
+extern fn oracle_ledc_set_pin(pin: c_uint, channel: c_uint) void;
+
+/// Count rising edges on the carrier pad, bounded. The instrument for the A/B below.
+fn countEdges(reads: u32) u32 {
+ var seen: u32 = 0;
+ var prev: u1 = hal.gpio.getLevel(tone_pin);
+ var g: u32 = 0;
+ while (g < reads) : (g += 1) {
+ const v = hal.gpio.getLevel(tone_pin);
+ if (v == 1 and prev == 0) seen += 1;
+ prev = v;
+ }
+ return seen;
+}
+
+fn micros() u64 {
+ return hal.systimer.micros(.unit0) orelse 0;
+}
+
+fn mark(comptime event: [*:0]const u8, comptime detail: [*:0]const u8) void {
+ soc.rom.print("MARK %s %s t=%uus\r\n", .{ event, detail, @as(u32, @truncate(micros())) });
+}
+
+fn delayMs(ms: u32) void {
+ hal.systimer.delayMicros(ms * 1000);
+}
+
+// ------------------------------------------------------------------------------- the emitter
+
+/// Bring LEDC up on the 80 MHz source and stage the carrier, without starting it.
+fn toneInit() void {
+ hal.clkrst.init(.ledc);
+ hal.ledc.init(.pll_div);
+
+ // 1-bit duty resolution: the counter has two states, so a duty of 1 is a 50 % square wave and
+ // the output frequency is the timer frequency. Asking for 25 MHz gets divider 410 and therefore
+ // 24.9756 MHz - the arithmetic is IDF's, reproduced exactly, including its rounding.
+ hal.ledc.configureTimer(tone_timer, .{
+ .resolution = 1,
+ .freq_hz = 25_000_000,
+ .src_hz = hal.ledc.pll_div_hz,
+ }) catch {
+ mark("TONE_FAIL", "divider out of range for 25MHz at 1-bit resolution");
+ return;
+ };
+
+ hal.ledc.configureChannel(tone_channel, .{
+ .timer = tone_timer,
+ .duty = 1, // half of 2^1: a square wave
+ .hpoint = 0,
+ .idle_level = 0,
+ });
+ hal.ledc.attachPin(tone_channel, tone_pin);
+ hal.ledc.stop(tone_channel, 0);
+ carrier_running = false;
+}
+
+fn toneOn() void {
+ hal.ledc.start(tone_channel);
+ carrier_running = true;
+}
+
+fn toneOff() void {
+ hal.ledc.stop(tone_channel, 0);
+ carrier_running = false;
+}
+
+/// The keyed frame. Timed with the systimer rather than a task delay, so the cell widths depend on
+/// a 16 MHz counter instead of on a scheduler tick - the report measured 100.062 ms and 100.000 ms
+/// against 100 ms commanded, and that is the number to beat.
+fn toneBurst() void {
+ mark("TONE_START", "gpio20 24975609Hz pattern=0x4200");
+
+ toneOn();
+ delayMs(preamble_ms);
+ toneOff();
+ delayMs(gap_ms);
+
+ var bit: i32 = 15;
+ while (bit >= 0) : (bit -= 1) {
+ if (key_word & (@as(u16, 1) << @intCast(bit)) != 0) toneOn();
+ delayMs(cell_ms);
+ toneOff();
+ delayMs(cell_ms);
+ }
+
+ mark("TONE_END", "gpio20 24975609Hz pattern=0x4200");
+ // Hand the pad back as a readable output, the way the C firmware did, so the blink witness still
+ // works afterwards.
+ hal.gpio.configureOutput(tone_pin, .{ .readback = true });
+}
+
+/// Ten samples at 125 ms, the C firmware's proof that the pad is really toggling rather than sitting
+/// at a level. A 1 Hz blink sampled at 125 ms must show runs of four.
+fn blinkWitness() void {
+ soc.rom.print("MARK BLINK_WITNESS gpio20 levels:", .{});
+ var i: u32 = 0;
+ while (i < 10) : (i += 1) {
+ soc.rom.print(" %u", .{@as(u32, hal.gpio.getLevel(tone_pin))});
+ delayMs(125);
+ }
+ soc.rom.print(" t=%uus\r\n", .{@as(u32, @truncate(micros()))});
+}
+
+/// On-chip corroboration that the pad is really switching, before believing anything an SDR says.
+///
+/// The report did this with the ADC (GPIO20 is also ADC1 channel 4) and read the min and max of a
+/// 64-sample burst rather than the mean, because the ADC cannot track 25 MHz and its sampling phase
+/// is uncorrelated with the pad. This HAL has no ADC, so it uses the pad's own input register
+/// instead: at ~90 MHz the core can issue a load every few cycles, so a run of reads across a
+/// 25 MHz square wave must catch both levels. Catching only one level means the pin is sitting
+/// still, which is the failure an SDR null cannot distinguish from bad coupling.
+fn padSample() void {
+ var ones: u32 = 0;
+ var zeros: u32 = 0;
+ var edges: u32 = 0;
+ var prev: u1 = hal.gpio.getLevel(tone_pin);
+ var i: u32 = 0;
+ // The sampling phase has to be *uncorrelated* with the signal, and a tight read loop is not.
+ //
+ // A first version read the pad 4096 times back to back and reported "constant high" for every
+ // carrier at or above 20 MHz - including 40 MHz, which is exactly APB/2, and 20 MHz, exactly
+ // APB/4. Both the read cadence and the LEDC output descend from the same clock, so a loop with a
+ // fixed period samples one phase of the waveform forever and reports a level that is not there.
+ // The ESP-IDF firmware avoided this by accident of its instrument: it used the ADC, whose
+ // conversion time is unrelated to the pad, and the report is explicit that its min/max - never
+ // its mean - is what carries the information.
+ //
+ // The software equivalent is to walk the phase deliberately: a delay that grows by one cycle
+ // every iteration cannot stay locked to any fixed period.
+ var jitter: u32 = 0;
+ while (i < 4096) : (i += 1) {
+ jitter = (jitter + 1) & 63;
+ var d: u32 = 0;
+ while (d < jitter) : (d += 1) asm volatile ("nop");
+ const v = hal.gpio.getLevel(tone_pin);
+ if (v == 1) ones += 1 else zeros += 1;
+ if (v != prev) edges += 1;
+ prev = v;
+ }
+ soc.rom.print("MARK PAD_SAMPLE carrier=%u ones=%u zeros=%u edges=%u of 4096 t=%uus\r\n", .{
+ @as(u32, @intFromBool(carrier_running)), ones, zeros, edges, @as(u32, @truncate(micros())),
+ });
+}
+
+/// The registers that decide whether this pin oscillates, printed rather than inferred.
+fn dumpRegs() void {
+ const ledc_base: u32 = @intCast(regs.LEDC_CH0_CONF0_REG);
+ soc.rom.print("MARK REGDUMP ch0_conf0=0x%08x ch0_hpoint=0x%08x ch0_duty=0x%08x ch0_conf1=0x%08x\r\n", .{
+ mmio.Reg.atAddress(ledc_base).raw(),
+ mmio.Reg.atAddress(@intCast(regs.LEDC_CH0_HPOINT_REG)).raw(),
+ mmio.Reg.atAddress(@intCast(regs.LEDC_CH0_DUTY_REG)).raw(),
+ mmio.Reg.atAddress(@intCast(regs.LEDC_CH0_CONF1_REG)).raw(),
+ });
+ soc.rom.print("MARK REGDUMP timer0_conf=0x%08x timer0_value=0x%08x ledc_conf=0x%08x\r\n", .{
+ mmio.Reg.atAddress(@intCast(regs.LEDC_TIMER0_CONF_REG)).raw(),
+ mmio.Reg.atAddress(@intCast(regs.LEDC_TIMER0_VALUE_REG)).raw(),
+ mmio.Reg.atAddress(@intCast(regs.LEDC_CONF_REG)).raw(),
+ });
+ soc.rom.print("MARK REGDUMP pad20=0x%08x matrix_out20=0x%08x gpio_out=0x%08x gpio_enable=0x%08x\r\n", .{
+ mmio.Reg.atAddress(@as(u32, @intCast(regs.PERIPHS_IO_MUX_U_PAD_GPIO0)) + 4 * 20).raw(),
+ mmio.Reg.atAddress(@as(u32, @intCast(regs.GPIO_FUNC0_OUT_SEL_CFG_REG)) + 4 * 20).raw(),
+ mmio.Reg.atAddress(@intCast(regs.GPIO_OUT_REG)).raw(),
+ mmio.Reg.atAddress(@intCast(regs.GPIO_ENABLE_REG)).raw(),
+ });
+ // The active shadow, and the counter sampled twice: `duty` is what was staged, `duty_r` is what
+ // the hardware is using, and a counter that does not move between two reads has no clock.
+ const v1 = mmio.Reg.atAddress(@intCast(regs.LEDC_TIMER0_VALUE_REG)).raw();
+ const v2 = mmio.Reg.atAddress(@intCast(regs.LEDC_TIMER0_VALUE_REG)).raw();
+ soc.rom.print("MARK REGDUMP duty_r=0x%08x cnt1=0x%08x cnt2=0x%08x moved=%u\r\n", .{
+ mmio.Reg.atAddress(@intCast(regs.LEDC_CH0_DUTY_R_REG)).raw(),
+ v1, v2, @as(u32, @intFromBool(v1 != v2)),
+ });
+ soc.rom.print("MARK REGDUMP ledc_sig_idx=%u clkrst_ctrl22=0x%08x t=%uus\r\n", .{
+ @as(u32, @intCast(regs.LEDC_LS_SIG_OUT_PAD_OUT0_IDX)),
+ mmio.Reg.atAddress(@intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL22_REG)).raw(),
+ @as(u32, @truncate(micros())),
+ });
+}
+
+fn status() void {
+ const div = hal.ledc.getClockDivider(tone_timer);
+ soc.rom.print(
+ "MARK STATUS carrier=%u divider=%u(Q10.8) freq=%uHz src=80000000Hz pin=%u cpu_mhz_x1000=%u t=%uus\r\n",
+ .{
+ @as(u32, @intFromBool(carrier_running)),
+ div,
+ hal.ledc.frequencyOf(hal.ledc.pll_div_hz, div, 1),
+ @as(u32, tone_pin),
+ cpuKhz(),
+ @as(u32, @truncate(micros())),
+ },
+ );
+}
+
+/// The CPU clock, measured against the systimer's fixed 16 MHz rather than assumed. Printed in the
+/// status line because every timing number in this experiment depends on the systimer, and this is
+/// the cheapest continuous check that its timebase is what it claims.
+fn cpuKhz() u32 {
+ const t0 = hal.systimer.read(.unit0) orelse return 0;
+ const c0 = soc.cycles();
+ soc.rom.ets_delay_us(20_000);
+ const t1 = hal.systimer.read(.unit0) orelse return 0;
+ const c1 = soc.cycles();
+ const ticks = t1 - t0;
+ if (ticks == 0) return 0;
+ return @intCast(((c1 - c0) * (hal.systimer.hz / 1000)) / ticks);
+}
+
+export fn zig_main() noreturn {
+ // Without this the board resets about ten seconds in, which for a 3.9 s frame captured inside a
+ // 10 s SDR dwell is the difference between a capture and a reboot.
+ _ = hal.rwdt.disable();
+
+ hal.systimer.init();
+ hal.gpio.configureOutput(tone_pin, .{ .readback = true });
+ toneInit();
+
+ soc.rom.print("\r\nMARK RF_READY zig toolchain, no esp-idf, no freertos\r\n", .{});
+ status();
+ soc.rom.print(
+ \\commands: T carrier on o carrier off t keyed burst (0x4200)
+ \\ ? status g blink witness p pad sampler i idle
+ \\
+ , .{});
+
+ // The command loop doubles as the 1 Hz blink when nothing is being transmitted, so the pad is
+ // never left floating and `g` has something to witness.
+ var last_toggle = micros();
+ var level: u1 = 0;
+ while (true) {
+ if (console.rxCount() > 0) {
+ var buf: [1]u8 = undefined;
+ if (console.read(&buf) == 1) {
+ switch (buf[0]) {
+ 'T' => {
+ toneOn();
+ mark("TONE_CONTINUOUS_ON", "gpio20 24975609Hz square");
+ },
+ 'o' => {
+ toneOff();
+ mark("TONE_OFF", "gpio20 released to blink");
+ hal.gpio.configureOutput(tone_pin, .{ .readback = true });
+ },
+ 't' => toneBurst(),
+ '?' => status(),
+ 'g' => blinkWitness(),
+ 'p' => padSample(),
+ 'd' => dumpRegs(),
+ // Experiment: hand the pad's output enable back to GPIO_ENABLE (oen_sel = 1)
+ // instead of to the routed peripheral. LEDC's signal carries no output-enable
+ // line, so with oen_sel = 0 there may be nothing asserting OE at all.
+ // Bisection: a slow LEDC output that the pad sampler can obviously see. If this
+ // toggles, LEDC and the routing work and the 25 MHz case is about the divider or
+ // a frequency ceiling; if it does not, the output path itself is broken.
+ 's' => {
+ hal.ledc.configureTimer(tone_timer, .{
+ .resolution = 8,
+ .freq_hz = 1000,
+ .src_hz = hal.ledc.pll_div_hz,
+ }) catch {
+ soc.rom.print("MARK SLOW_FAIL divider out of range\r\n", .{});
+ continue;
+ };
+ hal.ledc.configureChannel(tone_channel, .{
+ .timer = tone_timer,
+ .duty = 128, // half of 2^8
+ .hpoint = 0,
+ .idle_level = 0,
+ });
+ hal.ledc.attachPin(tone_channel, tone_pin);
+ hal.ledc.start(tone_channel);
+ carrier_running = true;
+ const div = hal.ledc.getClockDivider(tone_timer);
+ soc.rom.print("MARK SLOW_ON 1kHz 8-bit divider=%u freq=%uHz\r\n", .{
+ div, hal.ledc.frequencyOf(hal.ledc.pll_div_hz, div, 8),
+ });
+ },
+ // Where does it stop? Sweep resolution/frequency pairs and count edges on the
+ // pad. This turns "25 MHz does not work" into a measured ceiling.
+ 'S' => {
+ const cases = [_]struct { res: u5, hz: u32 }{
+ .{ .res = 8, .hz = 1_000 },
+ .{ .res = 8, .hz = 100_000 },
+ .{ .res = 4, .hz = 1_000_000 },
+ .{ .res = 2, .hz = 5_000_000 },
+ .{ .res = 2, .hz = 12_500_000 },
+ .{ .res = 1, .hz = 1_000_000 },
+ .{ .res = 1, .hz = 10_000_000 },
+ .{ .res = 1, .hz = 20_000_000 },
+ .{ .res = 1, .hz = 25_000_000 },
+ .{ .res = 1, .hz = 40_000_000 },
+ };
+ inline for (cases) |c| {
+ const want_div = hal.ledc.divisor(hal.ledc.pll_div_hz, c.hz, c.res);
+ if (!hal.ledc.divisorValid(want_div)) {
+ soc.rom.print("MARK SWEEP res=%u want=%uHz div=%u REJECTED\r\n", .{
+ @as(u32, c.res), c.hz, want_div,
+ });
+ } else {
+ hal.ledc.setClockDivider(tone_timer, want_div);
+ hal.ledc.setDutyResolution(tone_timer, c.res);
+ hal.ledc.commitTimer(tone_timer);
+ hal.ledc.resumeTimer(tone_timer);
+ hal.ledc.resetTimer(tone_timer);
+ hal.ledc.configureChannel(tone_channel, .{
+ .timer = tone_timer,
+ .duty = @as(u32, 1) << (c.res - 1),
+ .hpoint = 0,
+ .idle_level = 0,
+ });
+ hal.ledc.attachPin(tone_channel, tone_pin);
+ hal.ledc.start(tone_channel);
+ var ones: u32 = 0;
+ var edges: u32 = 0;
+ var prev: u1 = hal.gpio.getLevel(tone_pin);
+ var k: u32 = 0;
+ while (k < 2048) : (k += 1) {
+ const v = hal.gpio.getLevel(tone_pin);
+ if (v == 1) ones += 1;
+ if (v != prev) edges += 1;
+ prev = v;
+ }
+ soc.rom.print("MARK SWEEP res=%u want=%uHz div=%u got=%uHz ones=%u edges=%u\r\n", .{
+ @as(u32, c.res), c.hz, want_div,
+ hal.ledc.frequencyOf(hal.ledc.pll_div_hz, want_div, c.res),
+ ones, edges,
+ });
+ }
+ }
+ hal.ledc.stop(tone_channel, 0);
+ carrier_running = false;
+ },
+ // The LEDC registers are bit-identical to ESP-IDF's for this configuration
+ // (proven by the differential harness, 96 words), so if the pad still does not
+ // move the difference is in a clock mux outside the block. There are two:
+ // HP_SYS_CLKRST.peri_clk_ctrl22.reg_ledc_clk_src_sel, and LEDC_CONF.APB_CLK_SEL
+ // inside the block whose documented meaning is 0=APB_CLK, 1=RC_FAST, 2=XTAL,
+ // 3=invalid. Try every combination and sample the pad.
+ // Measure the LEDC output period against the systimer's fixed 16 MHz, at a
+ // frequency slow enough to time edges reliably. That yields the *actual* source
+ // clock, which is the number every divider here assumes and none has verified:
+ // this image runs at whatever the bootloader left (90 MHz CPU), not at the
+ // 360 MHz the ESP-IDF firmware configures, so its APB need not be 80 MHz.
+ // The A/B that settles it: ESP-IDF's LL and this HAL, same image, same pad, same
+ // edge counter, at the resolutions that matter.
+ 'A' => {
+ const cases = [_]struct { hz: u32, res: u5 }{
+ .{ .hz = 5_000_000, .res = 2 },
+ .{ .hz = 10_000_000, .res = 1 },
+ .{ .hz = 25_000_000, .res = 1 },
+ };
+ inline for (cases) |c| {
+ // ESP-IDF's side.
+ oracle_ledc_configure_timer(tone_timer, hal.ledc.pll_div_hz, @intCast(c.hz), c.res);
+ oracle_ledc_configure_channel(tone_channel, tone_timer, @as(u32, 1) << (c.res - 1), 0, 0, 1);
+ oracle_ledc_set_pin(tone_pin, tone_channel);
+ const idf_edges = countEdges(300_000);
+ // Ours.
+ hal.ledc.configureTimer(tone_timer, .{
+ .resolution = c.res, .freq_hz = c.hz, .src_hz = hal.ledc.pll_div_hz,
+ }) catch {};
+ hal.ledc.configureChannel(tone_channel, .{
+ .timer = tone_timer, .duty = @as(u32, 1) << (c.res - 1),
+ .hpoint = 0, .idle_level = 0,
+ });
+ hal.ledc.attachPin(tone_channel, tone_pin);
+ hal.ledc.start(tone_channel);
+ const our_edges = countEdges(300_000);
+ soc.rom.print("MARK AB res=%u hz=%u idf_edges=%u our_edges=%u (300k reads each)\r\n", .{
+ @as(u32, c.res), c.hz, idf_edges, our_edges,
+ });
+ }
+ hal.ledc.stop(tone_channel, 0);
+ carrier_running = false;
+ },
+ 'F' => {
+ // Same measurement across rates, with a generous guard: the question is only
+ // whether ANY edge appears, so a read loop that cannot keep up with the rate
+ // still answers it.
+ const rates = [_]struct { hz: u32, res: u5 }{
+ .{ .hz = 10_000, .res = 8 }, .{ .hz = 1_000_000, .res = 4 },
+ .{ .hz = 5_000_000, .res = 2 }, .{ .hz = 10_000_000, .res = 1 },
+ .{ .hz = 15_000_000, .res = 1 }, .{ .hz = 20_000_000, .res = 1 },
+ .{ .hz = 25_000_000, .res = 1 },
+ };
+ inline for (rates) |r| {
+ const d = hal.ledc.divisor(hal.ledc.pll_div_hz, r.hz, r.res);
+ hal.ledc.setClockDivider(tone_timer, d);
+ hal.ledc.setDutyResolution(tone_timer, r.res);
+ hal.ledc.commitTimer(tone_timer);
+ hal.ledc.resumeTimer(tone_timer);
+ hal.ledc.resetTimer(tone_timer);
+ hal.ledc.configureChannel(tone_channel, .{
+ .timer = tone_timer,
+ .duty = @as(u32, 1) << (r.res - 1),
+ .hpoint = 0,
+ .idle_level = 0,
+ });
+ hal.ledc.attachPin(tone_channel, tone_pin);
+ hal.ledc.start(tone_channel);
+ var seen: u32 = 0;
+ var prev: u1 = hal.gpio.getLevel(tone_pin);
+ var g: u32 = 0;
+ while (seen < 200 and g < 2_000_000) : (g += 1) {
+ const v = hal.gpio.getLevel(tone_pin);
+ if (v == 1 and prev == 0) seen += 1;
+ prev = v;
+ }
+ soc.rom.print("MARK EDGES want=%uHz res=%u div=%u rising_edges=%u in %u reads\r\n", .{
+ r.hz, @as(u32, r.res), d, seen, g,
+ });
+ }
+ hal.ledc.stop(tone_channel, 0);
+ carrier_running = false;
+ },
+ 'f' => {
+ const want: u32 = 10_000;
+ const res: u5 = 8;
+ const div = hal.ledc.divisor(hal.ledc.pll_div_hz, want, res);
+ hal.ledc.setClockDivider(tone_timer, div);
+ hal.ledc.setDutyResolution(tone_timer, res);
+ hal.ledc.commitTimer(tone_timer);
+ hal.ledc.resumeTimer(tone_timer);
+ hal.ledc.resetTimer(tone_timer);
+ hal.ledc.configureChannel(tone_channel, .{
+ .timer = tone_timer, .duty = 128, .hpoint = 0, .idle_level = 0,
+ });
+ hal.ledc.attachPin(tone_channel, tone_pin);
+ hal.ledc.start(tone_channel);
+ // Time 100 rising edges.
+ var seen: u32 = 0;
+ var prev: u1 = hal.gpio.getLevel(tone_pin);
+ const t_start = hal.systimer.read(.unit0) orelse 0;
+ var guard: u32 = 0;
+ while (seen < 100 and guard < 20_000_000) : (guard += 1) {
+ const v = hal.gpio.getLevel(tone_pin);
+ if (v == 1 and prev == 0) seen += 1;
+ prev = v;
+ }
+ const t_end = hal.systimer.read(.unit0) orelse 0;
+ const us = (t_end - t_start) / (hal.systimer.hz / 1_000_000);
+ // measured_hz = edges / seconds; source = measured * div * 2^res / 256
+ const measured_hz: u64 = if (us != 0) (@as(u64, seen) * 1_000_000) / us else 0;
+ const src_est: u64 = (measured_hz * div * (@as(u64, 1) << res)) / 256;
+ soc.rom.print("MARK FREQ want=%uHz div=%u edges=%u in %uus -> measured=%uHz implied_src=%uHz\r\n", .{
+ want, div, seen, @as(u32, @truncate(us)),
+ @as(u32, @truncate(measured_hz)), @as(u32, @truncate(src_est)),
+ });
+ },
+ 'M' => {
+ const ctrl22 = mmio.Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL22_REG);
+ const src_sel = mmio.Field.of(regs.HP_SYS_CLKRST_REG_LEDC_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_LEDC_CLK_SRC_SEL_V);
+ const fclk_en = mmio.Field.of(regs.HP_SYS_CLKRST_REG_LEDC_CLK_EN_S, regs.HP_SYS_CLKRST_REG_LEDC_CLK_EN_V);
+ const conf = mmio.Reg.at(regs.LEDC_CONF_REG);
+ const apb_sel = mmio.Field.of(regs.LEDC_APB_CLK_SEL_S, regs.LEDC_APB_CLK_SEL_V);
+ var ssel: u32 = 0;
+ while (ssel < 3) : (ssel += 1) {
+ var asel: u32 = 0;
+ while (asel < 3) : (asel += 1) {
+ ctrl22.modify(.{ src_sel.is(ssel), fclk_en.is(1) });
+ conf.modify(.{apb_sel.is(asel)});
+ hal.ledc.configureTimer(tone_timer, .{
+ .resolution = 1,
+ .freq_hz = 25_000_000,
+ .src_hz = hal.ledc.pll_div_hz,
+ }) catch {};
+ hal.ledc.configureChannel(tone_channel, .{
+ .timer = tone_timer,
+ .duty = 1,
+ .hpoint = 0,
+ .idle_level = 0,
+ });
+ hal.ledc.attachPin(tone_channel, tone_pin);
+ hal.ledc.start(tone_channel);
+ var ones: u32 = 0;
+ var edges: u32 = 0;
+ var prev: u1 = hal.gpio.getLevel(tone_pin);
+ var k: u32 = 0;
+ var jit: u32 = 0;
+ while (k < 1024) : (k += 1) {
+ jit = (jit + 1) & 31;
+ var d: u32 = 0;
+ while (d < jit) : (d += 1) asm volatile ("nop");
+ const v = hal.gpio.getLevel(tone_pin);
+ if (v == 1) ones += 1;
+ if (v != prev) edges += 1;
+ prev = v;
+ }
+ soc.rom.print("MARK MUX src_sel=%u apb_sel=%u ones=%u edges=%u of 1024\r\n", .{
+ ssel, asel, ones, edges,
+ });
+ }
+ }
+ },
+ 'E' => {
+ const sel = mmio.Reg.atAddress(@as(u32, @intCast(regs.GPIO_FUNC0_OUT_SEL_CFG_REG)) + 4 * @as(u32, tone_pin));
+ sel.modify(.{mmio.Field.of(regs.GPIO_FUNC0_OEN_SEL_S, regs.GPIO_FUNC0_OEN_SEL_V).is(1)});
+ hal.gpio.outputEnable(tone_pin);
+ soc.rom.print("MARK OEN_SEL set to 1 (GPIO_ENABLE drives OE), matrix_out20=0x%08x\r\n", .{sel.raw()});
+ },
+ 'i' => {
+ toneOff();
+ hal.gpio.configureOutput(tone_pin, .{ .readback = true });
+ mark("IDLE", "-");
+ },
+ else => {},
+ }
+ }
+ }
+
+ if (!carrier_running) {
+ const now = micros();
+ if (now - last_toggle >= 500_000) {
+ last_toggle = now;
+ level = ~level;
+ hal.gpio.setLevel(tone_pin, level);
+ }
+ }
+ }
+}
+
+export fn _start() linksection(".text.entry") callconv(.naked) noreturn {
+ asm volatile (
+ \\ li t0, 1 << 13
+ \\ csrs mstatus, t0
+ \\ la sp, __stack_top
+ \\ mv fp, sp
+ \\ la t0, __bss_start
+ \\ la t1, __bss_end
+ \\ bgeu t0, t1, 2f
+ \\1:
+ \\ sw zero, 0(t0)
+ \\ addi t0, t0, 4
+ \\ bltu t0, t1, 1b
+ \\2:
+ \\ j zig_main
+ );
+}