From f5f8068fac59b4f16046c2022c2fc7c7e447ef4c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 25 Aug 2026 12:40:53 -0300 Subject: 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. --- examples/rf.zig | 613 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 613 insertions(+) create mode 100644 examples/rf.zig (limited to 'examples/rf.zig') 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 + ); +} -- cgit v1.3