diff --git a/src/Emulator/Peripherals/Peripherals/Timers/STM32_Timer.cs b/src/Emulator/Peripherals/Peripherals/Timers/STM32_Timer.cs index b669728d3..eb4d9e9cc 100644 --- a/src/Emulator/Peripherals/Peripherals/Timers/STM32_Timer.cs +++ b/src/Emulator/Peripherals/Peripherals/Timers/STM32_Timer.cs @@ -53,41 +53,20 @@ public STM32_Timer(IMachine machine, ulong frequency, uint initialLimit) : base( enableRequested = false; } - Limit = autoReloadValue; - - for(var i = 0; i < NumberOfCCChannels; ++i) + if(repetitionsLeft == 0) { - UpdateCaptureCompareTimer(i); - if(!ccTimers[i].Enabled) - { - continue; - } - - switch(outputCompareModes[i].Value) - { - case OutputCompareMode.PwmMode1: - Connections[i].Set(); - break; - case OutputCompareMode.PwmMode2: - Connections[i].Unset(); - break; - } - } + GenerateUpdateEvent(); - if(updateInterruptEnable.Value && repetitionsLeft == 0) - { // 2 of central-aligned modes should raise IRQ only on overflow/underflow, hence it happens 2 times less often var centerAlignedUnbalancedMode = (centerAlignedMode.Value == CenterAlignedMode.CenterAligned1) || (centerAlignedMode.Value == CenterAlignedMode.CenterAligned2); - this.Log(LogLevel.Noisy, "IRQ pending"); - updateInterruptFlag = true; - repetitionsLeft = 1u + (uint)repetitionCounter.Value * (centerAlignedUnbalancedMode ? 2u : 1u); - UpdateInterrupts(); + repetitionsLeft = (uint)repetitionCounter.Value * (centerAlignedUnbalancedMode ? 2u : 1u); } - - if(repetitionsLeft > 0) + else { repetitionsLeft--; } + + UpdateTRGO(); }; for(var i = 0; i < NumberOfCCChannels; ++i) @@ -99,15 +78,17 @@ public STM32_Timer(IMachine machine, ulong frequency, uint initialLimit) : base( switch(outputCompareModes[j].Value) { case OutputCompareMode.SetActiveOnMatch: - Connections[j].Blink(); // high pulse + Connections[j].Set(); break; case OutputCompareMode.SetInactiveOnMatch: Connections[j].Unset(); - Connections[j].Set(); // low pulse break; case OutputCompareMode.ToggleOnMatch: Connections[j].Toggle(); break; + case OutputCompareMode.Frozen: + // Special case: even if frozen, some master modes trigger TRGO on match + break; case OutputCompareMode.PwmMode1: Connections[j].Unset(); break; @@ -122,6 +103,12 @@ public STM32_Timer(IMachine machine, ulong frequency, uint initialLimit) : base( this.Log(LogLevel.Noisy, "cctimer{0}: Compare IRQ pending", j + 1); UpdateInterrupts(); } + + if(j == 0) + { + PulseTRGOIfModeIs(MasterMode.ComparePulse); + } + UpdateTRGO(); }; } @@ -148,7 +135,7 @@ public STM32_Timer(IMachine machine, ulong frequency, uint initialLimit) : base( .WithReservedBits(1, 1) .WithTaggedFlag("CCUS", 2) .WithTaggedFlag("CCDS", 3) - .WithTag("MMS", 4, 2) + .WithEnumField(4, 3, out masterMode, name: "MMS") .WithTaggedFlag("TI1S", 7) .WithTaggedFlag("OIS1", 8) .WithTaggedFlag("OIS1N", 9) @@ -158,17 +145,22 @@ public STM32_Timer(IMachine machine, ulong frequency, uint initialLimit) : base( .WithTaggedFlag("OIS3N", 13) .WithTaggedFlag("OIS4", 14) .WithReservedBits(15, 17) + .WithWriteCallback((_, __) => UpdateTRGO()) }, {(long)Registers.SlaveModeControl, new DoubleWordRegister(this) - .WithTag("SMS", 0, 3) + .WithValueField(0, 3, out slaveModeLSB, name: "SMS[2:0]") .WithTaggedFlag("OCCS", 3) - .WithTag("TS", 4, 2) + .WithValueField(4, 3, out triggerSourceLSB, name: "TS[2:0]") .WithTaggedFlag("MSM", 7) - .WithTag("ETF", 8, 3) + .WithTag("ETF", 8, 4) .WithTag("ETPS", 12, 2) .WithTaggedFlag("ECE", 14) .WithTaggedFlag("ETP", 15) - .WithReservedBits(16, 16) + .WithFlag(16, out slaveModeMSB, name: "SMS[3]") + .WithReservedBits(17, 3) + .WithValueField(20, 2, out triggerSourceMSB, name: "TS[4:3]") + .WithReservedBits(22, 10) + .WithWriteCallback((_, __) => UpdateSlaveSubscription()) }, {(long)Registers.DmaOrInterruptEnable, new DoubleWordRegister(this) .WithFlag(0, out updateInterruptEnable, name: "Update interrupt enable (UIE)") @@ -228,29 +220,8 @@ public STM32_Timer(IMachine machine, ulong frequency, uint initialLimit) : base( { return; } - if(Direction == Direction.Ascending) - { - Value = 0; - } - else if(Direction == Direction.Descending) - { - Value = autoReloadValue; - } - - repetitionsLeft = (uint)repetitionCounter.Value; - - if(!updateRequestSource.Value && updateInterruptEnable.Value) - { - this.Log(LogLevel.Noisy, "IRQ pending"); - updateInterruptFlag = true; - } - for(var i = 0; i < NumberOfCCChannels; ++i) - { - if(ccTimers[i].Enabled) - { - ccTimers[i].Value = Value; - } - } + ReinitializeCounter(); + GenerateUpdateEvent(!updateRequestSource.Value); }, name: "Update generation (UG)") .WithTag("Capture/compare 1 generation (CC1G)", 1, 1) .WithTag("Capture/compare 2 generation (CC2G)", 2, 1) @@ -404,6 +375,7 @@ public STM32_Timer(IMachine machine, ulong frequency, uint initialLimit) : base( } registers = new DoubleWordRegisterCollection(this, registersMap); + itrReceiver.Parent = this; Reset(); } @@ -458,6 +430,7 @@ public override void Reset() Connections[i].Unset(); } UpdateInterrupts(); + UpdateSlaveSubscription(); } [DefaultInterrupt] @@ -469,22 +442,82 @@ public override void Reset() public GPIO TriggerInterrupt { get; } = new GPIO(); + public GPIO TRGO { get; } = new GPIO(); + public GPIO CommutationInterrupt { get; } = new GPIO(); - + public GPIO CaptureCompareInterrupt { get; } = new GPIO(); + public STM32_TimerTriggerMatrix TriggerMatrix + { + get => triggerMatrix; + set + { + triggerMatrix = value; + UpdateSlaveSubscription(); + } + } + public IReadOnlyDictionary Connections => connections; public long Size => 0x400; + private bool IsChannelNeededForTRGO(int i) + { + if(masterMode.Value >= MasterMode.CompareOC1 && masterMode.Value <= MasterMode.CompareOC4) + { + return (int)masterMode.Value - (int)MasterMode.CompareOC1 == i; + } + if(masterMode.Value == MasterMode.ComparePulse) + { + return i == 0; + } + return false; + } + private void UpdateCaptureCompareTimer(int i) { - ccTimers[i].Enabled = Enabled && IsInterruptOrOutputEnabled(i) && Value < ccTimers[i].Limit; + var channelNeeded = IsInterruptOrOutputEnabled(i) || IsChannelNeededForTRGO(i); + ccTimers[i].Enabled = Enabled && channelNeeded && Value < ccTimers[i].Limit; if(ccTimers[i].Enabled) { ccTimers[i].Value = Value; } ccTimers[i].Direction = Direction; + + if(Enabled && outputCompareModes[i] != null) + { + switch(outputCompareModes[i].Value) + { + case OutputCompareMode.PwmMode1: + if(Value < ccTimers[i].Limit) + { + Connections[i].Set(); + } + else + { + Connections[i].Unset(); + } + break; + case OutputCompareMode.PwmMode2: + if(Value < ccTimers[i].Limit) + { + Connections[i].Unset(); + } + else + { + Connections[i].Set(); + } + break; + case OutputCompareMode.ForceActive: + Connections[i].Set(); + break; + case OutputCompareMode.ForceInactive: + Connections[i].Unset(); + break; + } + } + UpdateTRGO(); } private void UpdateCaptureCompareTimers() @@ -540,6 +573,7 @@ private void WriteOutputCompareMode(int i, OutputCompareMode value) Connections[i].Set(); break; } + UpdateCaptureCompareTimer(i); } private void ClaimCaptureCompareInterrupt(int i, bool value) @@ -567,12 +601,174 @@ private void UpdateInterrupts() TriggerInterrupt.Set(false); CommutationInterrupt.Set(false); CaptureCompareInterrupt.Set(ccIrq); + UpdateTRGO(); + } + + private void UpdateSlaveSubscription() + { + if(triggerMatrix == null) + { + return; + } + + if(currentSubscribedItr != null) + { + currentSubscribedItr.Disconnect(new GPIOEndpoint(itrReceiver, 0)); + currentSubscribedItr = null; + } + + if(CurrentSlaveMode != SlaveMode.Disabled) + { + currentSubscribedItr = triggerMatrix.GetITR(CurrentTriggerSource); + if(currentSubscribedItr != null) + { + this.Log(LogLevel.Noisy, "Subscribing to matrix line {0}", CurrentTriggerSource); + currentSubscribedItr.Connect(itrReceiver, 0); + } + else + { + this.Log(LogLevel.Warning, "No matrix mapping for ITR source {0}", CurrentTriggerSource); + } + } + } + + private void OnITR(bool value) + { + this.Log(LogLevel.Noisy, "ITR signal received: {0}", value); + if(!value) + { + // We only support rising edge triggers for now + return; + } + + switch(CurrentSlaveMode) + { + case SlaveMode.Reset: + this.Log(LogLevel.Noisy, "Slave reset received"); + ReinitializeCounter(); + GenerateUpdateEvent(); + break; + case SlaveMode.Trigger: + if(!Enabled) + { + this.Log(LogLevel.Info, "Slave trigger received: starting timer. TIM_CNT={0}", Value); + enableRequested = true; + Enabled = autoReloadValue > 0; + } + break; + case SlaveMode.CombinedResetTrigger: + ReinitializeCounter(); + GenerateUpdateEvent(); + if(!Enabled) + { + enableRequested = true; + Enabled = autoReloadValue > 0; + } + break; + default: + this.Log(LogLevel.Warning, "Slave Mode {0} is not supported", CurrentSlaveMode); + break; + } + } + + private void GenerateUpdateEvent(bool setInterruptFlag = true) + { + if(setInterruptFlag && updateInterruptEnable.Value) + { + this.Log(LogLevel.Noisy, "IRQ pending"); + updateInterruptFlag = true; + } + + PulseTRGOIfModeIs(MasterMode.Update); + UpdateInterrupts(); + + // Register update logic (shadow registers) + Limit = autoReloadValue; + for(var i = 0; i < NumberOfCCChannels; ++i) + { + UpdateCaptureCompareTimer(i); + if(!ccTimers[i].Enabled) + { + continue; + } + + switch(outputCompareModes[i].Value) + { + case OutputCompareMode.PwmMode1: + Connections[i].Set(); + break; + case OutputCompareMode.PwmMode2: + Connections[i].Unset(); + break; + } + } + } + + private void ReinitializeCounter() + { + if(Direction == Direction.Ascending) + { + Value = 0; + } + else if(Direction == Direction.Descending) + { + Value = autoReloadValue; + } + + var centerAlignedUnbalancedMode = (centerAlignedMode.Value == CenterAlignedMode.CenterAligned1) || (centerAlignedMode.Value == CenterAlignedMode.CenterAligned2); + repetitionsLeft = (uint)repetitionCounter.Value * (centerAlignedUnbalancedMode ? 2u : 1u); + + for(var i = 0; i < NumberOfCCChannels; ++i) + { + if(ccTimers[i].Enabled) + { + ccTimers[i].Value = Value; + } + } + PulseTRGOIfModeIs(MasterMode.Reset); + } + + private void PulseTRGOIfModeIs(MasterMode mode) + { + if(masterMode.Value == mode) + { + TRGO.Blink(); + } + } + + private void UpdateTRGO() + { + switch(masterMode.Value) + { + case MasterMode.Enable: + TRGO.Set(Enabled); + break; + case MasterMode.CompareOC1: + case MasterMode.CompareOC2: + case MasterMode.CompareOC3: + case MasterMode.CompareOC4: + var channel = (int)masterMode.Value - (int)MasterMode.CompareOC1; + var channelState = ((GPIO)Connections[channel]).IsSet; + TRGO.Set(channelState); + break; + case MasterMode.Reset: + case MasterMode.Update: + case MasterMode.ComparePulse: + // Pulse-based modes are handled at the event site. + break; + default: + this.Log(LogLevel.Warning, "Master Mode {0} is not supported", masterMode.Value); + break; + } } private uint autoReloadValue; private uint repetitionsLeft; private bool updateInterruptFlag; private bool enableRequested; + private IGPIO currentSubscribedItr; + private STM32_TimerTriggerMatrix triggerMatrix; + private readonly ITRReceiver itrReceiver = new ITRReceiver(); private readonly bool[] ccInterruptFlag = new bool[NumberOfCCChannels]; private readonly bool[] ccInterruptEnable = new bool[NumberOfCCChannels]; private readonly bool[] ccOutputEnable = new bool[NumberOfCCChannels]; @@ -583,7 +779,12 @@ private void UpdateInterrupts() private readonly IFlagRegisterField updateRequestSource; private readonly IFlagRegisterField updateInterruptEnable; private readonly IFlagRegisterField autoReloadPreloadEnable; + private readonly IEnumRegisterField masterMode; private readonly IEnumRegisterField centerAlignedMode; + private readonly IValueRegisterField slaveModeLSB; + private readonly IFlagRegisterField slaveModeMSB; + private readonly IValueRegisterField triggerSourceLSB; + private readonly IValueRegisterField triggerSourceMSB; private readonly IValueRegisterField repetitionCounter; private readonly DoubleWordRegisterCollection registers; private readonly IEnumRegisterField[] outputCompareModes = new IEnumRegisterField[NumberOfCCChannels]; @@ -592,6 +793,9 @@ private void UpdateInterrupts() private readonly IBusController sysbus; private readonly Dictionary connections; + private SlaveMode CurrentSlaveMode => (SlaveMode)(slaveModeLSB.Value | (slaveModeMSB.Value ? 1u << 3 : 0u)); + private int CurrentTriggerSource => (int)(triggerSourceLSB.Value | (triggerSourceMSB.Value << 3)); + private const int NumberOfCCChannels = 4; // Does not resemble an actual pin, just serves the purpose @@ -606,6 +810,31 @@ private enum CenterAlignedMode CenterAligned3 = 3, // Up and down alternatively, compare interrupt flag set on both up/down counting } + private enum MasterMode + { + Reset = 0, + Enable = 1, + Update = 2, + ComparePulse = 3, + CompareOC1 = 4, + CompareOC2 = 5, + CompareOC3 = 6, + CompareOC4 = 7, + } + + private enum SlaveMode + { + Disabled = 0, + Encoder1 = 1, + Encoder2 = 2, + Encoder3 = 3, + Reset = 4, + Gated = 5, + Trigger = 6, + ExternalClock1 = 7, + CombinedResetTrigger = 8, + } + private enum OutputCompareMode { Frozen = 0, // Comparison between CNT and CCR has no effect on the outputs @@ -655,5 +884,16 @@ private enum Registers : long DmaAddressForFullTransfer = 0x4C, Option = 0x50 } + private class ITRReceiver : IGPIOReceiver + { + public void OnGPIO(int number, bool value) + { + Parent?.OnITR(value); + } + + public void Reset() { } + + public STM32_Timer Parent { get; set; } + } } -} \ No newline at end of file +} diff --git a/src/Emulator/Peripherals/Peripherals/Timers/STM32_TimerTriggerMatrix.cs b/src/Emulator/Peripherals/Peripherals/Timers/STM32_TimerTriggerMatrix.cs new file mode 100644 index 000000000..395830a3c --- /dev/null +++ b/src/Emulator/Peripherals/Peripherals/Timers/STM32_TimerTriggerMatrix.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) 2010-2026 Antmicro +// +// This file is licensed under the MIT License. +// Full license text is available in 'licenses/MIT.txt'. +// +using Antmicro.Renode.Core; +using Antmicro.Renode.Peripherals.Bus; +using Antmicro.Renode.Logging; + +namespace Antmicro.Renode.Peripherals.Timers +{ + public class STM32_TimerTriggerMatrix : IGPIOReceiver, IPeripheral + { + public STM32_TimerTriggerMatrix(IMachine machine) + { + this.machine = machine; + itrLines = new GPIO[32]; + for(var i = 0; i < itrLines.Length; i++) + { + itrLines[i] = new GPIO(); + } + } + + public void OnGPIO(int number, bool value) + { + if(number < 0 || number >= itrLines.Length) + { + this.Log(LogLevel.Error, "Invalid TRGO input number: {0}", number); + return; + } + + itrLines[number].Set(value); + } + + public IGPIO GetITR(int index) + { + if(index < 0 || index >= itrLines.Length) + { + this.Log(LogLevel.Error, "Invalid ITR index requested: {0}", index); + return null; + } + return itrLines[index]; + } + + public void Reset() + { + foreach(var line in itrLines) + { + line.Unset(); + } + } + + private readonly GPIO[] itrLines; + private readonly IMachine machine; + } +} diff --git a/src/Emulator/Peripherals/Test/PeripheralsTests/PeripheralsTests.csproj b/src/Emulator/Peripherals/Test/PeripheralsTests/PeripheralsTests.csproj index 5948fb9e8..77197e52d 100644 --- a/src/Emulator/Peripherals/Test/PeripheralsTests/PeripheralsTests.csproj +++ b/src/Emulator/Peripherals/Test/PeripheralsTests/PeripheralsTests.csproj @@ -58,6 +58,7 @@ + diff --git a/src/Emulator/Peripherals/Test/PeripheralsTests/STM32_TimerTests.cs b/src/Emulator/Peripherals/Test/PeripheralsTests/STM32_TimerTests.cs new file mode 100644 index 000000000..211cc2288 --- /dev/null +++ b/src/Emulator/Peripherals/Test/PeripheralsTests/STM32_TimerTests.cs @@ -0,0 +1,373 @@ +// +// Copyright (c) 2010-2026 Antmicro +// +// This file is licensed under the MIT License. +// Full license text is available in 'licenses/MIT.txt'. +// +using System; +using Antmicro.Renode.Core; +using Antmicro.Renode.Peripherals.Timers; +using Antmicro.Renode.Time; +using Antmicro.Renode.Peripherals.Bus; +using NUnit.Framework; + +namespace Antmicro.Renode.UnitTests +{ + [TestFixture] + public class STM32_TimerTests + { + [SetUp] + public void Setup() + { + machine = new Machine(); + timer = new STM32_Timer(machine, Frequency, 0xFFFF); // 16-bit timer. + sysbus = machine.GetSystemBus(timer); + } + + [Test] + public void ShouldResetCounter() + { + WriteRegister(Registers.Counter, 0x1234); + Assert.AreEqual(0x1234, ReadRegister(Registers.Counter)); + + timer.Reset(); + Assert.AreEqual(0, ReadRegister(Registers.Counter)); + } + + [Test] + public void ShouldCountUp() + { + WriteRegister(Registers.AutoReload, 1000); + WriteRegister(Registers.Prescaler, 0); + WriteRegister(Registers.Control1, 1); + + Assert.AreEqual(0, ReadRegister(Registers.Counter)); + AdvanceByTicks(500); + Assert.AreEqual(500, ReadRegister(Registers.Counter)); + AdvanceByTicks(500); + // On overflow, it should wrap (or stop if OPM, but default is periodic) + Assert.AreEqual(0, ReadRegister(Registers.Counter)); + } + + [Test] + public void ShouldRespectPrescaler() + { + WriteRegister(Registers.AutoReload, 1000); + WriteRegister(Registers.Prescaler, 9); + WriteRegister(Registers.Control1, 1); + + AdvanceByTicks(100); + Assert.AreEqual(10, ReadRegister(Registers.Counter)); + } + + [Test] + public void ShouldRespectRepetitionCounter() + { + WriteRegister(Registers.AutoReload, 100); + WriteRegister(Registers.RepetitionCounter, 2); // Update every 3 overflows + + // Generate update event to load RCR into repetitionsLeft + WriteRegister(Registers.EventGeneration, 1); + + WriteRegister(Registers.Control1, 1); + + var receiver = new DummyReceiver(); + timer.UpdateInterrupt.Connect(receiver, 0); + WriteRegister(Registers.DmaOrInterruptEnable, 1); + + // The UG event already set the interrupt flag if UIE was high. + // In our case UIE was set AFTER UG, so it's clean (or we should clear it). + WriteRegister(Registers.Status, 0); + receiver.Reset(); + + // 1st overflow + AdvanceByTicks(99); // Total 99 + Assert.IsFalse(receiver.Received, "Should not update before 1st overflow"); + AdvanceByTicks(2); // Total 101 + Assert.IsFalse(receiver.Received, "Should not update after 1st overflow"); + + // 2nd overflow + AdvanceByTicks(98); // Total 199 + Assert.IsFalse(receiver.Received, "Should not update before 2nd overflow"); + AdvanceByTicks(2); // Total 201 + Assert.IsFalse(receiver.Received, "Should not update after 2nd overflow"); + + // 3rd overflow + AdvanceByTicks(98); // Total 299 + Assert.IsFalse(receiver.Received, "Should not update before 3rd overflow"); + AdvanceByTicks(2); // Total 301 + Assert.IsTrue(receiver.Received, "Should update after 3rd overflow"); + } + + [Test] + public void ShouldTriggerTRGOOnEnable() + { + WriteRegister(Registers.Control2, 1 << 4); // MMS: Enable + Assert.IsFalse(timer.TRGO.IsSet); + + WriteRegister(Registers.Control1, 1); + Assert.IsTrue(timer.TRGO.IsSet); + + WriteRegister(Registers.Control1, 0); + Assert.IsFalse(timer.TRGO.IsSet); + } + + [Test] + public void ShouldTriggerTRGOOnUpdatePulse() + { + var receiver = new DummyReceiver(); + timer.TRGO.Connect(receiver, 0); + + WriteRegister(Registers.Control2, 2 << 4); // MMS: Update + WriteRegister(Registers.AutoReload, 100); + WriteRegister(Registers.Control1, 1); + + AdvanceByTicks(99); + Assert.IsFalse(receiver.Received, "Should not pulse TRGO before overflow"); + AdvanceByTicks(2); + Assert.IsTrue(receiver.Received, "Should pulse TRGO on overflow"); + } + + [Test] + public void ShouldTriggerTRGOOnCompareMatch() + { + var receiver = new DummyReceiver(); + timer.TRGO.Connect(receiver, 0); + + WriteRegister(Registers.Control2, 4 << 4); // MMS: Compare OC1REF + WriteRegister(Registers.AutoReload, 1000); + WriteRegister(Registers.CaptureOrCompare1, 500); + WriteRegister(Registers.CaptureOrCompareMode1, 6 << 4); // OC1M: PWM Mode 1 + WriteRegister(Registers.CaptureOrCompareEnable, 1); + WriteRegister(Registers.Control1, 1); + + // In PWM Mode 1, OC1REF is high when CNT < CCR1 + Assert.IsTrue(timer.TRGO.IsSet, "TRGO should be high when CNT < CCR1"); + Assert.AreEqual(1, receiver.PulseCount, "TRGO should have pulsed/set high exactly once"); + + AdvanceByTicks(499); + Assert.IsTrue(timer.TRGO.IsSet, "TRGO should still be high at CNT=499"); + Assert.AreEqual(1, receiver.PulseCount, "TRGO should not have pulsed again during counting"); + + AdvanceByTicks(2); // At 501 ticks + Assert.IsFalse(timer.TRGO.IsSet, "TRGO should be low when CNT > CCR1"); + Assert.AreEqual(1, receiver.PulseCount, "TRGO should have changed level to low but NOT pulsed high again"); + } + + [Test] + public void ShouldTriggerTRGOOnComparePulse() + { + var receiver = new DummyReceiver(); + timer.TRGO.Connect(receiver, 0); + + WriteRegister(Registers.Control2, 3 << 4); // MMS: Compare Pulse + WriteRegister(Registers.AutoReload, 1000); + WriteRegister(Registers.CaptureOrCompare1, 500); + WriteRegister(Registers.Control1, 1); + + AdvanceByTicks(498); + Assert.IsFalse(receiver.Received, "TRGO should not pulse before CC1 match"); + + AdvanceByTicks(3); + Assert.IsTrue(receiver.Received, "TRGO should have pulsed on CC1 match"); + } + + [Test] + public void ShouldStartOnSlaveTrigger() + { + var matrix = new STM32_TimerTriggerMatrix(machine); + timer.TriggerMatrix = matrix; + + // Ensure ITR0 is Low before configuring slave mode + matrix.OnGPIO(0, false); + + // Slave: Trigger, TS: ITR2 + WriteRegister(Registers.SlaveModeControl, 6 | (2 << 4)); + WriteRegister(Registers.AutoReload, 1000); + + Assert.IsFalse(timer.Enabled); + + // Negative check: ITR0 should do nothing + matrix.OnGPIO(0, true); + Assert.IsFalse(timer.Enabled, "Should not start on ITR0 when TS=ITR2"); + + // Real trigger: ITR2 rising edge + matrix.OnGPIO(2, true); + Assert.IsTrue(timer.Enabled, "Timer should start on rising edge of ITR2"); + + AdvanceByTicks(500); + Assert.AreEqual(500, ReadRegister(Registers.Counter)); + } + + [Test] + public void ShouldResetOnSlaveReset() + { + var matrix = new STM32_TimerTriggerMatrix(machine); + timer.TriggerMatrix = matrix; + + matrix.OnGPIO(0, false); + + // Slave: Reset, TS: ITR1 + WriteRegister(Registers.SlaveModeControl, 4 | (1 << 4)); + WriteRegister(Registers.AutoReload, 1000); + WriteRegister(Registers.Control1, 1); + + AdvanceByTicks(500); + Assert.AreEqual(500, ReadRegister(Registers.Counter)); + + // Negative check: ITR0 + matrix.OnGPIO(0, true); + Assert.AreEqual(500, ReadRegister(Registers.Counter), "Should not reset on ITR0 when TS=ITR1"); + + // Real trigger: ITR1 rising edge + matrix.OnGPIO(1, true); + Assert.AreEqual(0, ReadRegister(Registers.Counter), "Timer should reset on rising edge of ITR1"); + } + + [Test] + public void ShouldHandleCombinedResetTrigger() + { + var matrix = new STM32_TimerTriggerMatrix(machine); + timer.TriggerMatrix = matrix; + + matrix.OnGPIO(0, false); + + // Slave: Combined Reset + Trigger, TS: ITR3 + WriteRegister(Registers.SlaveModeControl, (1 << 16) | (3 << 4)); + WriteRegister(Registers.AutoReload, 1000); + + Assert.IsFalse(timer.Enabled); + WriteRegister(Registers.Counter, 500); + + // Negative check: ITR0 + matrix.OnGPIO(0, true); + Assert.IsFalse(timer.Enabled, "Should not start on ITR0 when TS=ITR3"); + + // Real trigger: ITR3 rising edge + matrix.OnGPIO(3, true); + Assert.IsTrue(timer.Enabled, "Timer should start on rising edge of ITR3"); + Assert.AreEqual(0, ReadRegister(Registers.Counter), "Timer should reset on rising edge of ITR3"); + } + + [Test] + public void ShouldHandlePwmMode2() + { + var receiver = new DummyReceiver(); + timer.TRGO.Connect(receiver, 0); + + WriteRegister(Registers.Control2, 4 << 4); // MMS: Compare OC1REF + WriteRegister(Registers.AutoReload, 1000); + WriteRegister(Registers.CaptureOrCompare1, 500); + WriteRegister(Registers.CaptureOrCompareMode1, 7 << 4); // OC1M: PWM Mode 2 + WriteRegister(Registers.CaptureOrCompareEnable, 1); + WriteRegister(Registers.Control1, 1); + + // In PWM Mode 2, OC1REF is low when CNT < CCR1 + Assert.IsFalse(timer.TRGO.IsSet, "TRGO should be low when CNT < CCR1"); + + AdvanceByTicks(499); + Assert.IsFalse(timer.TRGO.IsSet, "TRGO should still be low at CNT=499"); + + AdvanceByTicks(2); // At 501 ticks + Assert.IsTrue(timer.TRGO.IsSet, "TRGO should be high when CNT > CCR1"); + Assert.AreEqual(1, receiver.PulseCount, "TRGO should have transitioned high exactly once"); + } + + [Test] + public void ShouldHandleForceModes() + { + WriteRegister(Registers.Control2, 4 << 4); // MMS: Compare OC1REF + WriteRegister(Registers.CaptureOrCompareEnable, 1); + + WriteRegister(Registers.CaptureOrCompareMode1, 5 << 4); // OC1M: Force Active + Assert.IsTrue(timer.TRGO.IsSet, "TRGO should be high (Force Active)"); + + WriteRegister(Registers.Control1, 1); + AdvanceByTicks(1000); + Assert.IsTrue(timer.TRGO.IsSet, "TRGO should remain high after advancement"); + + WriteRegister(Registers.CaptureOrCompareMode1, 4 << 4); // OC1M: Force Inactive + Assert.IsFalse(timer.TRGO.IsSet, "TRGO should be low (Force Inactive)"); + + AdvanceByTicks(1000); + Assert.IsFalse(timer.TRGO.IsSet, "TRGO should remain low after advancement"); + } + + [Test] + public void ShouldPreserveLatchModesBetweenOverflows() + { + WriteRegister(Registers.Control2, 4 << 4); // MMS: Compare OC1REF + WriteRegister(Registers.AutoReload, 100); + WriteRegister(Registers.CaptureOrCompare1, 50); + WriteRegister(Registers.CaptureOrCompareMode1, 1 << 4); // OC1M: Set Active On Match + WriteRegister(Registers.CaptureOrCompareEnable, 1); + WriteRegister(Registers.Control1, 1); + + Assert.IsFalse(timer.TRGO.IsSet, "Should start inactive"); + + AdvanceByTicks(51); // Cross match (50) + Assert.IsTrue(timer.TRGO.IsSet, "Should be active after match"); + + AdvanceByTicks(100); // Cross overflow (at 100) + Assert.IsTrue(timer.TRGO.IsSet, "Should STAY active after overflow (Latch behavior)"); + + // Contrast with PWM Mode 1 + WriteRegister(Registers.CaptureOrCompareMode1, 6 << 4); // Switch to PWM Mode 1 + Assert.IsFalse(timer.TRGO.IsSet, "PWM Mode 1 should be low after overflow if CNT >= CCR"); + } + + private void WriteRegister(Registers reg, uint value) + { + timer.WriteDoubleWord((long)reg, value); + } + + private uint ReadRegister(Registers reg) + { + return timer.ReadDoubleWord((long)reg); + } + + private void AdvanceByTicks(ulong ticks) + { + var interval = TimeInterval.FromMicroseconds(ticks * 1000000 / Frequency); + ((BaseClockSource)machine.ClockSource).Advance(interval); + } + + private class DummyReceiver : IGPIOReceiver + { + public int PulseCount { get; private set; } + public bool Received => PulseCount > 0; + public void OnGPIO(int number, bool value) + { + if(value) PulseCount++; + } + public void Reset() { PulseCount = 0; } + } + + private STM32_Timer timer; + private IMachine machine; + private IBusController sysbus; + private const ulong Frequency = 1000000; // 1MHz + + private enum Registers : long + { + Control1 = 0x0, + Control2 = 0x04, + SlaveModeControl = 0x08, + DmaOrInterruptEnable = 0x0C, + Status = 0x10, + EventGeneration = 0x14, + CaptureOrCompareMode1 = 0x18, + CaptureOrCompareMode2 = 0x1C, + CaptureOrCompareEnable = 0x20, + Counter = 0x24, + Prescaler = 0x28, + AutoReload = 0x2C, + RepetitionCounter = 0x30, + CaptureOrCompare1 = 0x34, + CaptureOrCompare2 = 0x38, + CaptureOrCompare3 = 0x3C, + CaptureOrCompare4 = 0x40, + BreakAndDeadTime = 0x44, + } + } +} diff --git a/src/Infrastructure.csproj b/src/Infrastructure.csproj index f87a09663..caccacdcd 100644 --- a/src/Infrastructure.csproj +++ b/src/Infrastructure.csproj @@ -693,6 +693,7 @@ +