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