//! 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 ); }