Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public sealed class IoBank0Peripheral : IMemoryMappedDevice
{
private const int GPIO_COUNT = 30;

/// <summary>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).</summary>
public Action<int, bool>? 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)
Expand Down Expand Up @@ -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
Expand Down
97 changes: 60 additions & 37 deletions src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ namespace RP2040.Peripherals.Pwm;

/// <summary>
/// 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
/// <see cref="SetBInput"/> from the GPIO input path when the pin is muxed to PWM.
/// Provides ITickable to advance the counter.
/// </summary>
public sealed class PwmPeripheral : IMemoryMappedDevice, ITickable
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
/// <summary>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.</summary>
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
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/RP2040Sharp/Peripherals/RP2040Machine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
87 changes: 87 additions & 0 deletions tests/RP2040Sharp.Tests/Pwm/PwmTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
}
Loading