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
|
//! LEDC: the LED PWM controller. Four timers, eight channels, and the first **shadow-register**
//! peripheral in this HAL.
//!
//! Three things make LEDC different from everything else here, and all three are load-bearing.
//!
//! **1. Configuration is staged, then committed.** `LEDC_PARA_UP_CHn` (channel) and
//! `LEDC_TIMERn_PARA_UP` (timer) are write-to-trigger bits: writing 1 copies the staged fields into
//! the shadow registers the counter and comparators actually use, and the hardware clears the bit
//! again by itself (`ledc_reg.h:42-47`, `:951-958`). Values written without a commit are visible in
//! the register file and have no effect on the output. So every mutator here stages, and every
//! commit is its own store - `commitChannel` / `commitTimer` - exactly as ESP-IDF's
//! `ledc_ll_ls_channel_update` (ledc_ll.h:435-438) and `ledc_ll_ls_timer_update` (ledc_ll.h:286-290)
//! do it.
//!
//! The commit store is a read-modify-write, and that is deliberate rather than sloppy: the commit
//! bit shares its word with the staged fields it commits. `LEDC_PARA_UP_CH0` is bit 4 of
//! `LEDC_CH0_CONF0_REG`, whose other fields are `TIMER_SEL`, `SIG_OUT_EN`, `IDLE_LV` and `OVF_*`, so
//! a bare `writeRaw(1 << 4)` would erase the very configuration it was meant to commit. Compare
//! `systimer.zig`'s `op.write(.{update.is(1)})`, which is a single whole-word store because
//! `SYSTIMER_UNIT0_OP_REG` contains nothing else. The read-modify-write is safe here for the reason
//! `mmio.zig` gives: `PARA_UP` is `WT`, it reads back 0, so the read half of the read-modify-write
//! can never re-trigger an earlier commit. That is the difference between a self-clearing bit and a
//! write-1-to-clear bit, and it is why LEDC does not need the interrupt-status treatment.
//!
//! **2. The divider is fixed point, Q10.8.** `LEDC_CLK_DIV_TIMERn` is an 18-bit field at [22:5]
//! (`ledc_reg.h:921-928`) holding a divider with 8 fractional bits (`LEDC_LL_FRACTIONAL_BITS`,
//! ledc_ll.h:30): bits [17:8] are the integer part, bits [7:0] the fraction, so the value 0x4E2
//! means 1250/256 = 4.8828. The output frequency is
//!
//! f_pwm = f_src * 256 / (div * 2^duty_res)
//!
//! and `divisor()` below is ESP-IDF's arithmetic for the inverse, transcribed operation for
//! operation from `esp_driver_ledc/src/ledc.c:468-497` - including the two places where it is
//! surprising. See its comment.
//!
//! **3. On the P4 the clock mux left the peripheral.** `LEDC_CONF_REG.LEDC_APB_CLK_SEL` still exists
//! in the register map and still documents an encoding (0: APB, 1: RC_FAST, 2: XTAL), and ESP-IDF's
//! P4 LL never touches it: the real mux is `HP_SYS_CLKRST.PERI_CLK_CTRL22.REG_LEDC_CLK_SRC_SEL`,
//! with a *different* encoding (0: XTAL, 1: RC_FAST, 2: PLL_DIV) - ledc_ll.h:223-242. Writing the
//! in-block register would silently do nothing, and reading it back to check would silently agree.
//! `ClockSource` below is the HP_SYS_CLKRST encoding.
//!
//! Gamma fade *ramps* are out of scope, but one gamma register is not optional: the P4 moved
//! `DUTY_NUM`/`DUTY_CYCLE`/`DUTY_SCALE`/`DUTY_INC` out of `LEDC_CHn_CONF1_REG` - which on this die
//! holds only `DUTY_START` - and into gamma RAM. A constant duty is therefore a degenerate one-step
//! fade, and `setDuty` writes that single entry, which is what ESP-IDF's `ledc_duty_config` does for
//! every plain duty change (ledc.c:263-280).
const std = @import("std");
const regs = @import("regs");
const mmio = @import("mmio");
const clkrst = @import("clkrst.zig");
const gpio = @import("gpio.zig");
const Reg = mmio.Reg;
const Field = mmio.Field;
/// Eight channels, four timers (`soc_caps.h:385-386`).
pub const channel_count = 8;
pub const timer_count = 4;
/// The counter is 20 bits, so the duty resolution is at most 20 (`soc_caps.h:387`). The register
/// field is five bits wide and will happily accept 21-31; the hardware will not.
pub const max_duty_resolution = 20;
/// Fractional bits in `LEDC_CLK_DIV_TIMERn` - `LEDC_LL_FRACTIONAL_BITS`, ledc_ll.h:30.
pub const fractional_bits = 8;
/// The divider must be at least 1.0 and must fit the field: ESP-IDF's `LEDC_IS_DIV_INVALID`
/// (ledc.c:114) rejects anything `<= LEDC_LL_FRACTIONAL_MAX` or `> LEDC_TIMER_DIV_NUM_MAX`.
pub const divisor_min: u32 = 1 << fractional_bits;
pub const divisor_max: u32 = 0x3ffff;
pub const Error = error{
/// The requested frequency cannot be reached from this source at this resolution: the divider
/// would be below 1.0 (frequency too high) or wider than 18 bits (frequency too low).
DividerOutOfRange,
DutyResolutionOutOfRange,
};
// ------------------------------------------------------------------------------------- registers
// Five registers per channel, stride 0x14; two per timer, stride 0x08. Both strides come from a
// second instance's macro rather than being assumed - see mmio.RegArray.
const ch_conf0 = mmio.RegArray(regs.LEDC_CH0_CONF0_REG, regs.LEDC_CH1_CONF0_REG, channel_count);
const ch_hpoint = mmio.RegArray(regs.LEDC_CH0_HPOINT_REG, regs.LEDC_CH1_HPOINT_REG, channel_count);
const ch_duty = mmio.RegArray(regs.LEDC_CH0_DUTY_REG, regs.LEDC_CH1_DUTY_REG, channel_count);
const ch_conf1 = mmio.RegArray(regs.LEDC_CH0_CONF1_REG, regs.LEDC_CH1_CONF1_REG, channel_count);
const ch_duty_r = mmio.RegArray(regs.LEDC_CH0_DUTY_R_REG, regs.LEDC_CH1_DUTY_R_REG, channel_count);
const ch_gamma_conf = mmio.RegArray(regs.LEDC_CH0_GAMMA_CONF_REG, regs.LEDC_CH1_GAMMA_CONF_REG, channel_count);
// Gamma RAM: 16 entries per channel, so the per-channel stride is 0x40 and entry 0 is the base.
const ch_gamma_range0 = mmio.RegArray(regs.LEDC_CH0_GAMMA_RANGE0_REG, regs.LEDC_CH1_GAMMA_RANGE0_REG, channel_count);
const tim_conf = mmio.RegArray(regs.LEDC_TIMER0_CONF_REG, regs.LEDC_TIMER1_CONF_REG, timer_count);
const tim_value = mmio.RegArray(regs.LEDC_TIMER0_VALUE_REG, regs.LEDC_TIMER1_VALUE_REG, timer_count);
// Field geometry is taken from instance 0 and reused for every instance, which is only sound if the
// instances agree; the comptime block below checks the ends of both ranges against instance 0. That
// is not paranoia about the silicon, it is paranoia about the macro names: `LEDC_CLK_DIV_TIMER0` and
// `LEDC_TIMER0_DUTY_RES` put the instance number in different places, and picking up
// `LEDC_TIMER1_DUTY_RES_S` while meaning timer 0's shift is a one-character mistake.
const timer_sel = Field.of(regs.LEDC_TIMER_SEL_CH0_S, regs.LEDC_TIMER_SEL_CH0_V);
const sig_out_en = Field.of(regs.LEDC_SIG_OUT_EN_CH0_S, regs.LEDC_SIG_OUT_EN_CH0_V);
const idle_lv = Field.of(regs.LEDC_IDLE_LV_CH0_S, regs.LEDC_IDLE_LV_CH0_V);
const ch_para_up = Field.of(regs.LEDC_PARA_UP_CH0_S, regs.LEDC_PARA_UP_CH0_V);
const hpoint = Field.of(regs.LEDC_HPOINT_CH0_S, regs.LEDC_HPOINT_CH0_V);
const duty = Field.of(regs.LEDC_DUTY_CH0_S, regs.LEDC_DUTY_CH0_V);
const duty_r = Field.of(regs.LEDC_DUTY_CH0_R_S, regs.LEDC_DUTY_CH0_R_V);
const duty_start = Field.of(regs.LEDC_DUTY_START_CH0_S, regs.LEDC_DUTY_START_CH0_V);
const gamma_entry_num = Field.of(regs.LEDC_CH0_GAMMA_ENTRY_NUM_S, regs.LEDC_CH0_GAMMA_ENTRY_NUM_V);
const gamma_duty_inc = Field.of(regs.LEDC_CH0_GAMMA_RANGE0_DUTY_INC_S, regs.LEDC_CH0_GAMMA_RANGE0_DUTY_INC_V);
const gamma_duty_cycle = Field.of(regs.LEDC_CH0_GAMMA_RANGE0_DUTY_CYCLE_S, regs.LEDC_CH0_GAMMA_RANGE0_DUTY_CYCLE_V);
const gamma_scale = Field.of(regs.LEDC_CH0_GAMMA_RANGE0_SCALE_S, regs.LEDC_CH0_GAMMA_RANGE0_SCALE_V);
const gamma_duty_num = Field.of(regs.LEDC_CH0_GAMMA_RANGE0_DUTY_NUM_S, regs.LEDC_CH0_GAMMA_RANGE0_DUTY_NUM_V);
const duty_res = Field.of(regs.LEDC_TIMER0_DUTY_RES_S, regs.LEDC_TIMER0_DUTY_RES_V);
const clk_div = Field.of(regs.LEDC_CLK_DIV_TIMER0_S, regs.LEDC_CLK_DIV_TIMER0_V);
const tim_pause = Field.of(regs.LEDC_TIMER0_PAUSE_S, regs.LEDC_TIMER0_PAUSE_V);
const tim_rst = Field.of(regs.LEDC_TIMER0_RST_S, regs.LEDC_TIMER0_RST_V);
const tim_para_up = Field.of(regs.LEDC_TIMER0_PARA_UP_S, regs.LEDC_TIMER0_PARA_UP_V);
comptime {
const same = struct {
fn check(comptime what: []const u8, comptime a: Field, comptime b: Field) void {
if (a.shift != b.shift or a.width != b.width) @compileError(
"the per-instance " ++ what ++ " macros disagree on bit position or width; " ++
"this file must index the field per instance instead of reusing instance 0's",
);
}
}.check;
// Channels: 1 and 7, the two ends of the range beyond instance 0.
same("LEDC_TIMER_SEL_CHn", timer_sel, Field.of(regs.LEDC_TIMER_SEL_CH1_S, regs.LEDC_TIMER_SEL_CH1_V));
same("LEDC_TIMER_SEL_CHn", timer_sel, Field.of(regs.LEDC_TIMER_SEL_CH7_S, regs.LEDC_TIMER_SEL_CH7_V));
same("LEDC_SIG_OUT_EN_CHn", sig_out_en, Field.of(regs.LEDC_SIG_OUT_EN_CH7_S, regs.LEDC_SIG_OUT_EN_CH7_V));
same("LEDC_IDLE_LV_CHn", idle_lv, Field.of(regs.LEDC_IDLE_LV_CH7_S, regs.LEDC_IDLE_LV_CH7_V));
same("LEDC_PARA_UP_CHn", ch_para_up, Field.of(regs.LEDC_PARA_UP_CH7_S, regs.LEDC_PARA_UP_CH7_V));
same("LEDC_HPOINT_CHn", hpoint, Field.of(regs.LEDC_HPOINT_CH7_S, regs.LEDC_HPOINT_CH7_V));
same("LEDC_DUTY_CHn", duty, Field.of(regs.LEDC_DUTY_CH7_S, regs.LEDC_DUTY_CH7_V));
same("LEDC_DUTY_START_CHn", duty_start, Field.of(regs.LEDC_DUTY_START_CH7_S, regs.LEDC_DUTY_START_CH7_V));
same("LEDC_CHn_GAMMA_ENTRY_NUM", gamma_entry_num, Field.of(regs.LEDC_CH7_GAMMA_ENTRY_NUM_S, regs.LEDC_CH7_GAMMA_ENTRY_NUM_V));
same("LEDC_CHn_GAMMA_RANGE0_SCALE", gamma_scale, Field.of(regs.LEDC_CH7_GAMMA_RANGE0_SCALE_S, regs.LEDC_CH7_GAMMA_RANGE0_SCALE_V));
// Timers: 1 and 3.
same("LEDC_TIMERn_DUTY_RES", duty_res, Field.of(regs.LEDC_TIMER1_DUTY_RES_S, regs.LEDC_TIMER1_DUTY_RES_V));
same("LEDC_TIMERn_DUTY_RES", duty_res, Field.of(regs.LEDC_TIMER3_DUTY_RES_S, regs.LEDC_TIMER3_DUTY_RES_V));
same("LEDC_CLK_DIV_TIMERn", clk_div, Field.of(regs.LEDC_CLK_DIV_TIMER3_S, regs.LEDC_CLK_DIV_TIMER3_V));
same("LEDC_TIMERn_PAUSE", tim_pause, Field.of(regs.LEDC_TIMER3_PAUSE_S, regs.LEDC_TIMER3_PAUSE_V));
same("LEDC_TIMERn_RST", tim_rst, Field.of(regs.LEDC_TIMER3_RST_S, regs.LEDC_TIMER3_RST_V));
same("LEDC_TIMERn_PARA_UP", tim_para_up, Field.of(regs.LEDC_TIMER3_PARA_UP_S, regs.LEDC_TIMER3_PARA_UP_V));
// `LEDC_TIMER_DIV_NUM_MAX` (ledc.c:110) is a literal in the driver; it should be the field's
// own mask, and if a future die widens the field this is where the two part company.
if (divisor_max != clk_div.max()) @compileError(
"divisor_max no longer matches LEDC_CLK_DIV_TIMERn's width",
);
// The eight output signals must be consecutive for `signalIndex` to be arithmetic.
if (regs.LEDC_LS_SIG_OUT_PAD_OUT7_IDX - regs.LEDC_LS_SIG_OUT_PAD_OUT0_IDX != channel_count - 1)
@compileError("the LEDC output signal indices are not consecutive; signalIndex must be a table");
}
// ------------------------------------------------------------------------------ clocks and reset
/// LEDC's function clock, in HP_SYS_CLKRST rather than in the peripheral (ledc_ll.h:179, :241).
/// Shared with RMT's fields, hence the interrupt-masked read-modify-write.
const peri_clk_ctrl22 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL22_REG);
const clk_src_sel = Field.of(regs.HP_SYS_CLKRST_REG_LEDC_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_LEDC_CLK_SRC_SEL_V);
const func_clk_en = Field.of(regs.HP_SYS_CLKRST_REG_LEDC_CLK_EN_S, regs.HP_SYS_CLKRST_REG_LEDC_CLK_EN_V);
/// The four timers' shared source. Encoding from `ledc_ll_set_slow_clk_sel` (ledc_ll.h:223-242) -
/// *not* the encoding `LEDC_CONF_REG.APB_CLK_SEL` documents, which is a different register on a
/// different block and is dead on this die.
pub const ClockSource = enum(u2) {
/// 40 MHz on this board (`clk_tree_defs.h:145`).
xtal = 0,
/// The internal RC oscillator: approximately 17.5 MHz (`clk_tree_defs.h:58`) and not trimmed.
/// ESP-IDF calibrates it against XTAL before using it for a divider; there is no calibration
/// here, so a frequency computed from `rc_fast_hz_approx` is approximate too.
rc_fast = 1,
/// PLL_F80M, 80 MHz (`clk_tree_defs.h:168`). Called `LEDC_SLOW_CLK_PLL_DIV` by ESP-IDF.
pll_div = 2,
/// The source frequency to feed `divisor`, or null for RC_FAST, whose real rate has to be
/// measured rather than assumed.
pub fn hz(self: ClockSource) ?u32 {
return switch (self) {
.xtal => xtal_hz,
.pll_div => pll_div_hz,
.rc_fast => null,
};
}
};
pub const xtal_hz: u32 = 40_000_000;
pub const pll_div_hz: u32 = 80_000_000;
pub const rc_fast_hz_approx: u32 = 17_500_000;
/// Select the timers' source clock. A read-modify-write of a register that also holds RMT's clock
/// fields, so it runs with interrupts masked, like everything else that touches HP_SYS_CLKRST.
pub fn setClockSource(src: ClockSource) void {
const guard = clkrst.maskInterrupts();
defer guard.release();
peri_clk_ctrl22.modify(.{clk_src_sel.is(@intFromEnum(src))});
}
pub fn getClockSource() ClockSource {
return @enumFromInt(peri_clk_ctrl22.get(clk_src_sel));
}
/// LEDC's core ("function") clock gate. Distinct from the APB gate in `clkrst`: the APB clock makes
/// the registers addressable, this one makes the counters run - and ESP-IDF notes that some LEDC
/// registers and the gamma RAM need it just to be read or written (ledc.c:433-436).
pub fn setFunctionClockEnabled(on: bool) void {
const guard = clkrst.maskInterrupts();
defer guard.release();
peri_clk_ctrl22.modify(.{func_clk_en.is(@intFromBool(on))});
}
/// Bring the peripheral up, in the only order that works: bus clock, reset, function clock, source.
///
/// The bus clock first because LEDC is one of the blocks whose APB gate is *off* at power-on
/// (`hp_sys_clkrst_reg.h:835`, REG_LEDC_APB_CLK_EN default 0), so every register read before this
/// returns the last value the bus latched. The function clock before any configuration because the
/// gamma RAM needs it. ESP-IDF deasserts the reset rather than pulsing it (ledc.c:430-431), because
/// its driver may be attaching to a running LEDC; this pulses, which is the stronger guarantee for a
/// fresh boot and is measurably safe on this board - pulsing REG_RST_EN_LEDC for 1 ms left the
/// console untouched and returned LEDC_CH0_CONF0 to 0.
pub fn init(src: ClockSource) void {
clkrst.setClockEnabled(.ledc, true);
clkrst.resetPeripheral(.ledc);
setFunctionClockEnabled(true);
setClockSource(src);
}
// -------------------------------------------------------------------------------- divider maths
/// ESP-IDF's `ledc_calculate_divisor`, transcribed from `esp_driver_ledc/src/ledc.c:468-497`:
///
/// return (((uint64_t) src_clk_freq << LEDC_LL_FRACTIONAL_BITS) + freq_hz * precision / 2)
/// / (freq_hz * precision);
///
/// Result is Q10.8 - see the file comment - and `divisorValid` says whether it fits the field.
///
/// Two properties of that C expression are not obvious and are reproduced deliberately, because a
/// HAL that computed a *better* divider than IDF's would disagree with it on real inputs and there
/// would be no way to tell which of the two was wrong:
///
/// 1. `freq_hz * precision` is `int * uint32_t`, so it is computed in **32 bits and wraps**, and
/// the wrap is not always harmlessly out of range. Ask for 4097 Hz at 20-bit resolution from the
/// 40 MHz XTAL: the true product is 2^32 + 2^20, the C code divides by 2^20 instead, and the
/// answer is 9766 - a *valid* divider, which programs 1.0 Hz. IDF accepts it, because the value
/// passes its own range check. `%*` here is that wrap, on purpose: reproducing it is what makes
/// the on-die comparison meaningful, and the numbers above are how a caller can recognise it.
/// 2. The quotient is `uint64_t` but the return type is `uint32_t`, so it is **truncated**. From a
/// 40 MHz source at 1 Hz and 1-bit resolution the quotient is 5.12e9 and IDF returns 825032704.
/// `@truncate` is that truncation.
///
/// The one place this cannot follow IDF is `freq_hz * precision == 0`, reachable at exactly 4096 Hz
/// with 20-bit resolution (2^32, wrapping to zero), where the C code divides by zero. Returning 0 is
/// a deliberate substitution: it is not a valid divider, so `divisorValid` rejects it and the caller
/// gets an error instead of undefined behaviour.
pub fn divisor(src_hz: u32, freq_hz: u32, resolution: u5) u32 {
const precision: u32 = @as(u32, 1) << resolution;
const den: u32 = freq_hz *% precision;
if (den == 0) return 0;
const num: u64 = (@as(u64, src_hz) << fractional_bits) + den / 2;
return @truncate(num / den);
}
/// `LEDC_IS_DIV_INVALID`, inverted (ledc.c:114). A divider below 1.0 means the requested frequency
/// is faster than the source can produce at that resolution.
pub fn divisorValid(div: u32) bool {
return div >= divisor_min and div <= divisor_max;
}
/// The frequency a given divider and resolution actually produce: `f_src * 256 / (div * 2^res)`,
/// rounded, and 0 for a divider of 0.
///
/// This is `ledc_get_freq`'s arithmetic (ledc.c:1175) with one deliberate difference: the
/// denominator is computed in 64 bits, so it does not wrap. Nothing compares this against IDF - it
/// is a convenience for callers checking what they got - and a wrapped denominator here would be a
/// bug rather than a compatibility requirement.
pub fn frequencyOf(src_hz: u32, div: u32, resolution: u5) u32 {
if (div == 0) return 0;
const den: u64 = @as(u64, div) * (@as(u64, 1) << resolution);
const num: u64 = (@as(u64, src_hz) << fractional_bits) + den / 2;
return @truncate(num / den);
}
// --------------------------------------------------------------------------------------- timers
/// Stage the divider. `ledc_ll_set_clock_divider`, ledc_ll.h:345-348.
pub fn setClockDivider(timer: u32, div: u32) void {
std.debug.assert(timer < timer_count);
tim_conf.at(timer).modify(.{clk_div.is(div)});
}
pub fn getClockDivider(timer: u32) u32 {
std.debug.assert(timer < timer_count);
return tim_conf.at(timer).get(clk_div);
}
/// Stage the duty resolution, in bits. `ledc_ll_set_duty_resolution`, ledc_ll.h:391-394.
pub fn setDutyResolution(timer: u32, bits: u5) void {
std.debug.assert(timer < timer_count);
std.debug.assert(bits <= max_duty_resolution);
tim_conf.at(timer).modify(.{duty_res.is(bits)});
}
pub fn getDutyResolution(timer: u32) u5 {
std.debug.assert(timer < timer_count);
return @intCast(tim_conf.at(timer).get(duty_res));
}
/// Commit the staged divider and resolution. One store, and the bit clears itself.
///
/// ESP-IDF does not wait for it: "we don't wait for the bit gets cleared since it can take quite
/// long depends on the pwm frequency" (ledc_ll.h:289). Neither does this - a poll here would block
/// for a whole PWM period, and there is nothing useful to do with the answer.
pub fn commitTimer(timer: u32) void {
std.debug.assert(timer < timer_count);
tim_conf.at(timer).modify(.{tim_para_up.is(1)});
}
/// Reset the timer's counter: assert, deassert (`ledc_ll_timer_rst`, ledc_ll.h:301-305).
///
/// Note the reset value of `LEDC_TIMERn_RST` is **1** (ledc_reg.h:936-943), which is one of the
/// 46.7% of fields whose reset value is not zero, and the reason `configureTimer` finishes by
/// clearing it: a freshly reset LEDC block holds all four counters at zero and they stay there until
/// something writes that bit back down.
pub fn resetTimer(timer: u32) void {
std.debug.assert(timer < timer_count);
const r = tim_conf.at(timer);
r.modify(.{tim_rst.is(1)});
r.modify(.{tim_rst.is(0)});
}
/// Freeze the counter where it is (`ledc_ll_timer_pause`, ledc_ll.h:316-319).
pub fn pauseTimer(timer: u32) void {
std.debug.assert(timer < timer_count);
tim_conf.at(timer).modify(.{tim_pause.is(1)});
}
pub fn resumeTimer(timer: u32) void {
std.debug.assert(timer < timer_count);
tim_conf.at(timer).modify(.{tim_pause.is(0)});
}
/// The counter's current value, 20 bits. Reading it is a plain load - no latch handshake, unlike
/// systimer.
pub fn timerCount(timer: u32) u32 {
std.debug.assert(timer < timer_count);
return tim_value.at(timer).raw();
}
/// Everything a timer needs to produce `freq_hz` at `resolution` bits, in ESP-IDF's order:
/// divider, resolution, commit, then out of pause and out of reset (`ledc_set_timer_params`,
/// ledc.c:244-261, followed by ledc.c:816-818).
///
/// Returns `DividerOutOfRange` rather than programming a divider the hardware cannot hold. The
/// caller passes the source frequency because this HAL has no clock tree: `ClockSource.hz()` gives
/// it for XTAL and PLL_DIV, and RC_FAST has to be measured.
pub fn configureTimer(timer: u32, opts: struct {
src_hz: u32,
freq_hz: u32,
resolution: u5,
}) Error!void {
std.debug.assert(timer < timer_count);
if (opts.resolution == 0 or opts.resolution > max_duty_resolution) return Error.DutyResolutionOutOfRange;
const div = divisor(opts.src_hz, opts.freq_hz, opts.resolution);
if (!divisorValid(div)) return Error.DividerOutOfRange;
setClockDivider(timer, div);
setDutyResolution(timer, opts.resolution);
commitTimer(timer);
resumeTimer(timer);
resetTimer(timer);
}
// ------------------------------------------------------------------------------------- channels
/// Which timer drives this channel. Staged; needs `commitChannel`.
/// `ledc_ll_bind_channel_timer`, ledc_ll.h:697-700.
pub fn bindTimer(channel: u32, timer: u32) void {
std.debug.assert(channel < channel_count and timer < timer_count);
ch_conf0.at(channel).modify(.{timer_sel.is(timer)});
}
pub fn boundTimer(channel: u32) u32 {
std.debug.assert(channel < channel_count);
return ch_conf0.at(channel).get(timer_sel);
}
/// Where in the period the output goes high, in counter ticks. Staged.
/// `ledc_ll_set_hpoint`, ledc_ll.h:450-453.
pub fn setHpoint(channel: u32, value: u32) void {
std.debug.assert(channel < channel_count and value <= hpoint.max());
ch_hpoint.at(channel).modify(.{hpoint.is(value)});
}
/// Stage a duty value, in counter ticks out of `2^resolution`.
///
/// Two things happen here that the name does not suggest, and both are ESP-IDF's
/// (`ledc_ll_set_duty_int_part` ledc_ll.h:480-483, `ledc_duty_config` ledc.c:263-280):
///
/// * The register holds duty in **Q21.4** - four fractional bits, used by fades - so the integer
/// duty is shifted left by 4. `getDuty` shifts back.
/// * The P4 has no plain-duty path. `DUTY_NUM`/`DUTY_CYCLE`/`DUTY_SCALE`/`DUTY_INC` moved out of
/// `CHn_CONF1` into gamma RAM, so a constant duty is a one-step fade of scale 0: entry 0 gets
/// (increase, one cycle, scale 0, one step) and the range count is set to 1. Without that entry
/// the staged duty is committed and the output does not move.
///
/// Staged; needs `commitChannel` (or `start`, which commits).
pub fn setDuty(channel: u32, value: u32) void {
std.debug.assert(channel < channel_count);
std.debug.assert(value <= duty.max() >> 4);
ch_duty.at(channel).modify(.{duty.is(value << 4)});
stageNoFade(channel);
}
/// The duty the hardware is currently using, from the read-only shadow (`ledc_ll_get_duty`,
/// ledc_ll.h:495-498). This is the one register that shows whether a commit actually happened - and
/// it only updates when the timer next overflows, so it is not a synchronous read-back.
pub fn currentDuty(channel: u32) u32 {
std.debug.assert(channel < channel_count);
return ch_duty_r.at(channel).get(duty_r) >> 4;
}
/// Gamma RAM entry 0 as "no fade": one step, one cycle, scale 0, increasing. Exactly the parameters
/// `ledc_set_duty` passes down (ledc.c:1109-1117) for a constant duty.
fn stageNoFade(channel: u32) void {
// The whole word is being established, and every field in it is being named, so this is one of
// the few places `write` is right rather than `modify`.
ch_gamma_range0.at(channel).write(.{
gamma_duty_inc.is(1),
gamma_duty_cycle.is(1),
gamma_scale.is(0),
gamma_duty_num.is(1),
});
ch_gamma_conf.at(channel).modify(.{gamma_entry_num.is(1)});
}
/// The output driver. Staged; needs `commitChannel`.
/// `ledc_ll_set_sig_out_en`, ledc_ll.h:592-596.
pub fn setOutputEnabled(channel: u32, on: bool) void {
std.debug.assert(channel < channel_count);
ch_conf0.at(channel).modify(.{sig_out_en.is(@intFromBool(on))});
}
/// The level the pad holds while the channel is disabled - and only while it is disabled
/// (`ledc_reg.h:34-37`: "Valid only when LEDC_SIG_OUT_EN_CHn is 0"). Staged.
/// `ledc_ll_set_idle_level`, ledc_ll.h:622-626.
pub fn setIdleLevel(channel: u32, level: u1) void {
std.debug.assert(channel < channel_count);
ch_conf0.at(channel).modify(.{idle_lv.is(level)});
}
/// Hand the staged duty to the fade engine. `ledc_ll_set_duty_start`, ledc_ll.h:607-610.
///
/// `DUTY_START` lives in `CHn_CONF1`, alone, and is annotated `R/W/SC` - the hardware clears it when
/// the (here one-step) fade finishes. A read-modify-write is still the right store: the bit is the
/// only field in the word, but bits 30:0 are reserved and writing them back as read is what IDF's
/// bitfield assignment does.
pub fn startFade(channel: u32) void {
std.debug.assert(channel < channel_count);
ch_conf1.at(channel).modify(.{duty_start.is(1)});
}
/// Commit the channel's staged fields: `TIMER_SEL`, `SIG_OUT_EN`, `IDLE_LV`, `HPOINT`,
/// `DUTY_START`, `OVF_CNT_EN` and the duty (`ledc_reg.h:42-47`).
///
/// One deliberate store, never folded into the store that staged the values, matching
/// `ledc_ll_ls_channel_update` (ledc_ll.h:435-438). It is a read-modify-write because the commit bit
/// shares its word with the staged fields - see the file comment - and that is safe only because the
/// bit reads back as 0.
pub fn commitChannel(channel: u32) void {
std.debug.assert(channel < channel_count);
ch_conf0.at(channel).modify(.{ch_para_up.is(1)});
}
/// Start driving: output on, duty handed over, committed. `_ledc_update_duty`, ledc.c:1021-1026.
pub fn start(channel: u32) void {
setOutputEnabled(channel, true);
startFade(channel);
commitChannel(channel);
}
/// Stop driving and hold the pad at `idle_level`. `ledc_stop`, ledc.c:1039-1050.
///
/// The order is IDF's and it matters: the idle level is staged *before* the output is disabled, so
/// the two reach the hardware in the same commit and the pad never spends a period at the old idle
/// level.
pub fn stop(channel: u32, idle_level: u1) void {
setIdleLevel(channel, idle_level);
setOutputEnabled(channel, false);
commitChannel(channel);
}
/// A whole channel in one commit: timer, duty, hpoint, idle level, output enable.
///
/// This is the one operation here that is not a transcription of an ESP-IDF function - IDF's
/// `ledc_channel_config` also allocates a driver object, reserves the pin and installs a fade
/// service - but it is the same register sequence: stage everything, then commit once. One commit
/// rather than five is the point: the channel changes all at once, at a period boundary, instead of
/// drifting through four intermediate configurations.
pub fn configureChannel(channel: u32, opts: struct {
timer: u32,
duty: u32,
hpoint: u32 = 0,
idle_level: u1 = 0,
output_enabled: bool = true,
}) void {
bindTimer(channel, opts.timer);
setHpoint(channel, opts.hpoint);
setDuty(channel, opts.duty);
setIdleLevel(channel, opts.idle_level);
setOutputEnabled(channel, opts.output_enabled);
startFade(channel);
commitChannel(channel);
}
// ------------------------------------------------------------------------------------ pin output
/// The GPIO matrix signal index for a channel's output. `ledc_periph_signal[0].sig_out0_idx` is
/// `LEDC_LS_SIG_OUT_PAD_OUT0_IDX` (esp_hal_ledc/esp32p4/ledc_periph.c:14-18) and the driver adds the
/// channel number to it (ledc.c:831); the eight indices are consecutive from 126, asserted above.
pub fn signalIndex(channel: u32) u32 {
std.debug.assert(channel < channel_count);
return @as(u32, @intCast(regs.LEDC_LS_SIG_OUT_PAD_OUT0_IDX)) + channel;
}
/// Route a channel's output to a pad through the GPIO matrix. No LEDC register is involved: the
/// peripheral has no pad of its own, and this is the whole of `ledc_set_pin`'s hardware effect
/// (ledc.c:823-836, whose `gpio_matrix_output` is func_sel + matrix source + output-enable control,
/// gpio_hal.c:60-69).
pub fn attachPin(channel: u32, pin: u8) void {
gpio.matrixOut(pin, signalIndex(channel));
}
// ----------------------------------------------------------------------------------------- tests
test "the divider is Q10.8: integer part in [17:8], fraction in [7:0]" {
// 40 MHz XTAL, 1 kHz, 13-bit resolution. 40e6*256/(1000*8192) = 1250 = 0x4E2, i.e. 4 + 226/256
// = 4.8828. Checked against ESP-IDF's own expression compiled on the host over a 1,680-point
// sweep of (source, frequency, resolution).
try std.testing.expectEqual(@as(u32, 1250), divisor(40_000_000, 1_000, 13));
try std.testing.expectEqual(@as(u32, 1250 >> 8), 4);
try std.testing.expectEqual(@as(u32, 1250 & 0xff), 226);
// And back again, to within the rounding the format allows.
try std.testing.expectEqual(@as(u32, 1_000), frequencyOf(40_000_000, 1250, 13));
}
test "divider values for the frequencies the differential harness uses" {
try std.testing.expectEqual(@as(u32, 2000), divisor(40_000_000, 5_000, 10));
try std.testing.expectEqual(@as(u32, 500), divisor(40_000_000, 20_000, 10));
try std.testing.expectEqual(@as(u32, 2083), divisor(40_000_000, 300, 14));
// 80 MHz PLL_F80M, same request: exactly twice the divider.
try std.testing.expectEqual(@as(u32, 4000), divisor(80_000_000, 5_000, 10));
}
test "the arithmetic reproduces IDF's overflow and truncation rather than fixing them" {
// 32-bit wrap of freq*precision: the true product at 1 MHz / 13 bits is 8_192_000_000, and the
// C expression divides by 3_897_032_704 instead, giving 3 where the unwrapped arithmetic would
// give 1. Neither is a usable divider - both are below 1.0, so `divisorValid` rejects them the
// way `LEDC_IS_DIV_INVALID` does - but the *value* has to be IDF's, or a caller comparing the
// two implementations sees a difference that is really just two different roundings.
try std.testing.expectEqual(@as(u32, 1_000_000 *% (@as(u32, 1) << 13)), 3_897_032_704);
try std.testing.expectEqual(@as(u32, 3), divisor(40_000_000, 1_000_000, 13));
try std.testing.expect(!divisorValid(divisor(40_000_000, 1_000_000, 13)));
// u64 quotient truncated to u32, exactly as the C return type does.
try std.testing.expectEqual(@as(u32, 825_032_704), divisor(40_000_000, 1, 1));
// The one input where IDF divides by zero: 4096 * 2^20 == 2^32.
try std.testing.expectEqual(@as(u32, 0), divisor(40_000_000, 4096, 20));
try std.testing.expect(!divisorValid(divisor(40_000_000, 4096, 20)));
// The wrap that is *not* self-limiting: 4097 Hz at 20 bits gives a divider IDF's own range check
// accepts, and it programs 1.0 Hz. Reproduced rather than corrected, because the point of the
// differential test is to be wrong in the same way IDF is or not at all.
try std.testing.expectEqual(@as(u32, 9766), divisor(40_000_000, 4097, 20));
try std.testing.expect(divisorValid(9766));
try std.testing.expectEqual(@as(u32, 1), frequencyOf(40_000_000, 9766, 20));
}
test "validity is the field's range, not the whole u32" {
try std.testing.expect(!divisorValid(0xff)); // below 1.0
try std.testing.expect(divisorValid(0x100)); // exactly 1.0
try std.testing.expect(divisorValid(0x3ffff));
try std.testing.expect(!divisorValid(0x40000));
// 40 MHz cannot make 5 kHz at 13 bits: that needs a divider of 0.98.
try std.testing.expect(!divisorValid(divisor(40_000_000, 5_000, 13)));
}
|