diff --git a/src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs b/src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs index 3d52ef1..fbb9162 100644 --- a/src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs +++ b/src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs @@ -14,6 +14,10 @@ public sealed class IoBank0Peripheral : IMemoryMappedDevice { private const int GPIO_COUNT = 30; + /// Raised on each pad input transition (pin, new level), after the edge is latched. + /// The machine routes these to peripherals that consume pin inputs (e.g. the PWM B pins). + public Action? OnInputChanged; + // Register layout offsets private const uint GPIO_CTRL_LAST = 0x0EC; // last byte of GPIO pair area private const uint INTR_BASE = 0x0F0; // INTR0-3 raw interrupt (write 1 to clear edge) @@ -195,6 +199,9 @@ public void UpdatePinInput(int pin, bool value) if (value) _intrEdge[pin] |= IRQ_EDGE_HIGH; else _intrEdge[pin] |= IRQ_EDGE_LOW; + // Fan out the edge to peripherals with pin inputs (PWM B-pin gating/edge-count DIVMODEs). + OnInputChanged?.Invoke(pin, value); + // Only run the (whole-bank) interrupt scan when this pin actually has an interrupt enabled or // forced. A bit-banged input with no GPIO IRQ — e.g. the CYW43 gSPI DATA line toggling per bit — // would otherwise pay a full bank scan on every edge; skipping it is the single biggest saving diff --git a/src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs b/src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs index f03c8d1..5c6d20d 100644 --- a/src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs +++ b/src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs @@ -5,9 +5,10 @@ namespace RP2040.Peripherals.Pwm; /// /// RP2040 PWM peripheral (base 0x40050000). -/// 8 slices (A/B channels each). Supports: -/// - Free-running mode (default): counter wraps at TOP -/// - Level-sensitive: counter resets when input goes low +/// 8 slices (A/B channels each). Supports the four CSR.DIVMODE clocking modes: +/// free-running (clk_sys/div), gated on the B-pin level, and rising/falling B-pin +/// edge counting (what CircuitPython's countio uses). The B-pin input arrives via +/// from the GPIO input path when the pin is muxed to PWM. /// Provides ITickable to advance the counter. /// public sealed class PwmPeripheral : IMemoryMappedDevice, ITickable @@ -40,6 +41,7 @@ public sealed class PwmPeripheral : IMemoryMappedDevice, ITickable private long[] _fracAccum = new long[SLICE_COUNT]; private bool[] _phaseDir = new bool[SLICE_COUNT]; // true=counting up (phase-correct) + private readonly bool[] _bInput = new bool[SLICE_COUNT]; // B-pin level (gated / edge DIVMODEs) private uint _enable; // slice enable bitfield (mirrors CSR.EN per slice) private uint _intr; @@ -76,56 +78,77 @@ public void Tick(long deltaCycles) { if ((_csr[s] & CSR_EN) == 0) continue; // slice not enabled - // DIV = integer (bits 11:4) + fraction (bits 3:0) in 8.4 format - var divInt = (int)((_div[s] >> 4) & 0xFF); - var divFrac = (int)(_div[s] & 0xF); - if (divInt == 0) divInt = 1; + var divMode = (_csr[s] & CSR_DIVMODE) >> 4; + if (divMode >= 2) continue; // edge modes: SetBInput advances the slice + if (divMode == 1 && !_bInput[s]) continue; // gated: only counts while B is high - // Fixed-point divisor in 1/16 units - var divisor = divInt * 16 + divFrac; + Advance(s, deltaCycles * 16); + } + } - _fracAccum[s] += deltaCycles * 16; - var steps = _fracAccum[s] / divisor; - _fracAccum[s] %= divisor; + /// Drive the slice's B-pin input (from the GPIO mux when FUNCSEL=PWM). In the + /// edge-count DIVMODEs each matching transition advances the counter by one divider event. + public void SetBInput(int slice, bool level) + { + if (_bInput[slice] == level) return; + _bInput[slice] = level; + if ((_csr[slice] & CSR_EN) == 0) return; + var divMode = (_csr[slice] & CSR_DIVMODE) >> 4; + if ((divMode == 2 && level) || (divMode == 3 && !level)) + Advance(slice, 16); + } - var phCorrect = (_csr[s] & CSR_PH_CORRECT) != 0; + private void Advance(int s, long events16) + { + // DIV = integer (bits 11:4) + fraction (bits 3:0) in 8.4 format + var divInt = (int)((_div[s] >> 4) & 0xFF); + var divFrac = (int)(_div[s] & 0xF); + if (divInt == 0) divInt = 1; - for (var i = 0L; i < steps; i++) + // Fixed-point divisor in 1/16 units + var divisor = divInt * 16 + divFrac; + + _fracAccum[s] += events16; + var steps = _fracAccum[s] / divisor; + _fracAccum[s] %= divisor; + + var phCorrect = (_csr[s] & CSR_PH_CORRECT) != 0; + + for (var i = 0L; i < steps; i++) + { + if (phCorrect) { - if (phCorrect) + // Phase-correct: count up to TOP then back down to 0 + if (_phaseDir[s]) { - // Phase-correct: count up to TOP then back down to 0 - if (_phaseDir[s]) + _ctr[s]++; + if (_ctr[s] >= _top[s]) { - _ctr[s]++; - if (_ctr[s] >= _top[s]) - { - _ctr[s] = _top[s]; - _phaseDir[s] = false; - } - } - else - { - if (_ctr[s] == 0) - { - _phaseDir[s] = true; - _intr |= 1u << s; - if ((_inte & (1u << s)) != 0) - _cpu.SetInterrupt(4, true); // PWM_IRQ_WRAP is single shared IRQ - } - else _ctr[s]--; + _ctr[s] = _top[s]; + _phaseDir[s] = false; } } else { - _ctr[s]++; - if (_ctr[s] > _top[s]) + if (_ctr[s] == 0) { - _ctr[s] = 0; + _phaseDir[s] = true; _intr |= 1u << s; if ((_inte & (1u << s)) != 0) _cpu.SetInterrupt(4, true); // PWM_IRQ_WRAP is single shared IRQ } + else _ctr[s]--; + } + } + else + { + _ctr[s]++; + if (_ctr[s] > _top[s]) + { + _ctr[s] = 0; + _intr |= 1u << s; + if ((_inte & (1u << s)) != 0) + _cpu.SetInterrupt(4, true); // PWM_IRQ_WRAP is single shared IRQ } } } diff --git a/src/RP2040Sharp/Peripherals/RP2040Machine.cs b/src/RP2040Sharp/Peripherals/RP2040Machine.cs index fa7790e..89c3426 100644 --- a/src/RP2040Sharp/Peripherals/RP2040Machine.cs +++ b/src/RP2040Sharp/Peripherals/RP2040Machine.cs @@ -210,6 +210,14 @@ public RP2040Machine(uint flashSize = 2 * 1024 * 1024, Pwm = new PwmPeripheral(Cpu); apb.Register(0x40050000, Pwm); + // B-pin inputs for the PWM gated/edge-count DIVMODEs: odd GPIOs muxed to PWM (FUNCSEL 4) + // feed slice (pin >> 1) & 7 — what CircuitPython's countio counts. + IoBank0.OnInputChanged = (pin, level) => + { + if ((pin & 1) != 0 && IoBank0.GetFuncSel(pin) == 4) + Pwm.SetBInput((pin >> 1) & 7, level); + }; + // Timer @ 0x40054000 (slot 21) Timer = new TimerPeripheral(Cpu, CLK_HZ); apb.Register(0x40054000, Timer); diff --git a/tests/RP2040Sharp.Tests/Pwm/PwmTests.cs b/tests/RP2040Sharp.Tests/Pwm/PwmTests.cs index 25590cc..4a5c06c 100644 --- a/tests/RP2040Sharp.Tests/Pwm/PwmTests.cs +++ b/tests/RP2040Sharp.Tests/Pwm/PwmTests.cs @@ -196,4 +196,91 @@ public void PH_ADV_not_stored_in_CSR() (csr & CSR_PH_ADV).Should().Be(0u, "PH_ADV is a strobe and must not be stored in CSR"); } } + + public class DivMode + { + private const uint DIVMODE_GATED = 1u << 4; + private const uint DIVMODE_RISE = 2u << 4; + private const uint DIVMODE_FALL = 3u << 4; + + [Fact] + public void EdgeRise_counts_rising_B_edges_only() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CSR(0), CSR_EN | DIVMODE_RISE); + + for (var i = 0; i < 5; i++) + { + f.Pwm.SetBInput(0, true); + f.Pwm.Tick(10_000); + f.Pwm.SetBInput(0, false); + f.Pwm.Tick(10_000); + } + + f.Pwm.ReadWord(CTR(0)).Should().Be(5u, "each rising edge is one count; clock cycles must not advance the slice"); + } + + [Fact] + public void EdgeFall_counts_falling_B_edges_only() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CSR(0), CSR_EN | DIVMODE_FALL); + + for (var i = 0; i < 3; i++) + { + f.Pwm.SetBInput(0, true); + f.Pwm.SetBInput(0, false); + } + f.Pwm.SetBInput(0, true); + + f.Pwm.ReadWord(CTR(0)).Should().Be(3u); + } + + [Fact] + public void Gated_counts_cycles_only_while_B_high() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CSR(0), CSR_EN | DIVMODE_GATED); + + f.Pwm.Tick(100); + f.Pwm.ReadWord(CTR(0)).Should().Be(0u, "B low: the slice clock is gated off"); + + f.Pwm.SetBInput(0, true); + f.Pwm.Tick(100); + f.Pwm.ReadWord(CTR(0)).Should().Be(100u, "B high: counts at clk/div"); + + f.Pwm.SetBInput(0, false); + f.Pwm.Tick(100); + f.Pwm.ReadWord(CTR(0)).Should().Be(100u); + } + + [Fact] + public void EdgeRise_applies_fractional_divider() + { + using var f = new Fixture(); + f.Pwm.WriteWord(DIV(0), 4u << 4); + f.Pwm.WriteWord(CSR(0), CSR_EN | DIVMODE_RISE); + + for (var i = 0; i < 8; i++) + { + f.Pwm.SetBInput(0, true); + f.Pwm.SetBInput(0, false); + } + + f.Pwm.ReadWord(CTR(0)).Should().Be(2u, "8 edges / div 4 = 2 counts"); + } + + [Fact] + public void FreeRun_ignores_B_pin() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CSR(0), CSR_EN); + + f.Pwm.SetBInput(0, true); + f.Pwm.SetBInput(0, false); + f.Pwm.Tick(50); + + f.Pwm.ReadWord(CTR(0)).Should().Be(50u, "edges must not add counts in free-running mode"); + } + } }