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");
+ }
+ }
}