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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
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
);
}
|