1 // 2 // Copyright (c) 2010-2023 Antmicro 3 // Copyright (c) 2011-2015 Realtime Embedded 4 // 5 // This file is licensed under the MIT License. 6 // Full license text is available in 'licenses/MIT.txt'. 7 // 8 using System; 9 using Antmicro.Renode.Core; 10 using Antmicro.Renode.Peripherals; 11 using System.Threading; 12 using Antmicro.Renode.Peripherals.Miscellaneous; 13 using Antmicro.Renode.Utilities; 14 using Antmicro.Renode.Time; 15 using System.Runtime.CompilerServices; 16 17 namespace Antmicro.Renode.Testing 18 { 19 public static class LEDTesterExtenions 20 { CreateLEDTester(this Emulation emulation, string name, ILed led, float defaultTimeout = 0)21 public static void CreateLEDTester(this Emulation emulation, string name, ILed led, float defaultTimeout = 0) 22 { 23 emulation.ExternalsManager.AddExternal(new LEDTester(led, defaultTimeout), name); 24 } 25 } 26 27 public class LEDTester : IExternal 28 { LEDTester(ILed led, float defaultTimeout = 0)29 public LEDTester(ILed led, float defaultTimeout = 0) 30 { 31 ValidateArgument(defaultTimeout, nameof(defaultTimeout), allowZero: true); 32 this.led = led; 33 this.machine = led.GetMachine(); 34 this.defaultTimeout = defaultTimeout; 35 } 36 AssertState(bool state, float? timeout = null, bool pauseEmulation = false)37 public LEDTester AssertState(bool state, float? timeout = null, bool pauseEmulation = false) 38 { 39 timeout = timeout ?? defaultTimeout; 40 ValidateArgument(timeout.Value, nameof(timeout), allowZero: true); 41 42 var emulation = EmulationManager.Instance.CurrentEmulation; 43 var timeoutEvent = GetTimeoutEvent((ulong)(timeout * 1000)); 44 AutoResetEvent emulationPausedEvent = null; 45 46 var ev = new ManualResetEvent(false); 47 var method = (Action<ILed, bool>) 48 ((led, currState) => 49 { 50 if(pauseEmulation && currState == state) 51 { 52 machine.PauseAndRequestEmulationPause(precise: true); 53 } 54 ev.Set(); 55 }); 56 57 try 58 { 59 led.StateChanged += method; 60 // Don't start the emulation if the assert would succeed instantly 61 // or regardless of the LED state if the timeout is 0 62 if(led.State != state && timeout != 0) 63 { 64 emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation); 65 } 66 67 do 68 { 69 if(led.State == state) 70 { 71 emulationPausedEvent?.WaitOne(); 72 return this; 73 } 74 75 WaitHandle.WaitAny(new [] { timeoutEvent.WaitHandle, ev }); 76 } 77 while(!timeoutEvent.IsTriggered); 78 } 79 finally 80 { 81 led.StateChanged -= method; 82 } 83 84 throw new InvalidOperationException("LED assertion not met."); 85 } 86 AssertAndHoldState(bool initialState, float timeoutAssert, float timeoutHold, bool pauseEmulation = false)87 public LEDTester AssertAndHoldState(bool initialState, float timeoutAssert, float timeoutHold, bool pauseEmulation = false) 88 { 89 ValidateArgument(timeoutAssert, nameof(timeoutAssert), allowZero: true); 90 ValidateArgument(timeoutHold, nameof(timeoutHold)); 91 var emulation = EmulationManager.Instance.CurrentEmulation; 92 AutoResetEvent emulationPausedEvent = null; 93 var locker = new Object(); 94 int numberOfStateChanges = 0; 95 bool isHolding = false; 96 97 var ev = new AutoResetEvent(false); 98 TimeoutEvent timeoutEvent; 99 var method = (Action<ILed, bool>) 100 ((led, currState) => 101 { 102 lock(locker) 103 { 104 ++numberOfStateChanges; 105 if(!isHolding && numberOfStateChanges == 1) 106 { 107 // Create a new event for holding at the precise moment that the LED state changed 108 timeoutEvent = GetTimeoutEvent((ulong)(timeoutHold * 1000), MakePauseRequest(emulation, pauseEmulation)); 109 } 110 ev?.Set(); 111 } 112 }); 113 114 try 115 { 116 // this needs to be treated as atomic block so the state doesn't change during initialization 117 lock(locker) 118 { 119 led.StateChanged += method; 120 isHolding = initialState == led.State; 121 var timeout = isHolding ? timeoutHold : timeoutAssert; 122 // If we're already holding, make the first timeout event pause the emulation 123 timeoutEvent = GetTimeoutEvent((ulong)(timeout * 1000), 124 MakePauseRequest(emulation, pauseEmulation && isHolding)); 125 emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation); 126 } 127 128 do 129 { 130 var eventSrc = WaitHandle.WaitAny( new [] { timeoutEvent.WaitHandle, ev } ); 131 132 if(isHolding) 133 { 134 if(numberOfStateChanges > 0) 135 { 136 throw new InvalidOperationException("LED changed state."); 137 } 138 } 139 else 140 { 141 lock(locker) 142 { 143 if(numberOfStateChanges == 1) 144 { 145 isHolding = true; 146 --numberOfStateChanges; 147 } 148 else if(eventSrc == 0) 149 { 150 throw new InvalidOperationException("Initial LED assertion not met."); 151 } 152 else 153 { 154 throw new InvalidOperationException("LED changed state."); 155 } 156 } 157 } 158 } 159 while((!timeoutEvent.IsTriggered && isHolding) || !isHolding); 160 } 161 finally 162 { 163 lock(locker) 164 { 165 led.StateChanged -= method; 166 ev.Dispose(); 167 ev = null; 168 } 169 } 170 171 emulationPausedEvent?.WaitOne(); 172 173 return this; 174 } 175 AssertDutyCycle(float testDuration, double expectedDutyCycle, double tolerance = 0.05, bool pauseEmulation = false)176 public LEDTester AssertDutyCycle(float testDuration, double expectedDutyCycle, double tolerance = 0.05, bool pauseEmulation = false) 177 { 178 ValidateArgument(testDuration, nameof(testDuration)); 179 ValidateArgument(expectedDutyCycle, nameof(expectedDutyCycle), min: 0, max: 1); 180 ValidateArgument(tolerance, nameof(tolerance), min: 0, max: 1); 181 var emulation = EmulationManager.Instance.CurrentEmulation; 182 AutoResetEvent emulationPausedEvent = null; 183 ulong lowTicks = 0; 184 ulong highTicks = 0; 185 186 var method = MakeStateChangeHandler((currState, dt) => 187 { 188 if(currState) 189 { 190 // we switch to high, so up to this point it was low 191 lowTicks += dt.Ticks; 192 } 193 else 194 { 195 highTicks += dt.Ticks; 196 } 197 }); 198 199 try 200 { 201 led.StateChanged += method; 202 var timeoutEvent = GetTimeoutEvent((ulong)(testDuration * 1000), MakePauseRequest(emulation, pauseEmulation)); 203 emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation); 204 205 timeoutEvent.WaitHandle.WaitOne(); 206 207 var highPercentage = (double)highTicks / (highTicks + lowTicks) * 100; 208 if(highPercentage < expectedDutyCycle - (tolerance * 100) || expectedDutyCycle > expectedDutyCycle + (tolerance * 100)) 209 { 210 throw new InvalidOperationException($"Fill assertion not met: expected {expectedDutyCycle} with tolerance {tolerance * 100}%, but got {highPercentage}"); 211 } 212 } 213 finally 214 { 215 led.StateChanged -= method; 216 } 217 218 emulationPausedEvent?.WaitOne(); 219 220 return this; 221 } 222 AssertIsBlinking(float testDuration, double onDuration, double offDuration, double tolerance = 0.05, bool pauseEmulation = false)223 public LEDTester AssertIsBlinking(float testDuration, double onDuration, double offDuration, double tolerance = 0.05, bool pauseEmulation = false) 224 { 225 ValidateArgument(testDuration, nameof(testDuration)); 226 ValidateArgument(onDuration, nameof(onDuration)); 227 ValidateArgument(offDuration, nameof(offDuration)); 228 var emulation = EmulationManager.Instance.CurrentEmulation; 229 AutoResetEvent emulationPausedEvent = null; 230 var stateChanged = false; 231 var patternMismatchEvent = new ManualResetEvent(false); 232 var method = MakeStateChangeHandler((currState, dt) => 233 { 234 stateChanged = true; 235 // currState is after a switch, so when it's high we need to check the off duration 236 var expectedDuration = currState ? offDuration : onDuration; 237 if(!IsInRange(dt.TotalSeconds, expectedDuration, tolerance)) 238 { 239 patternMismatchEvent.Set(); 240 } 241 }); 242 243 try 244 { 245 led.StateChanged += method; 246 var timeoutEvent = GetTimeoutEvent((ulong)(testDuration * 1000), MakePauseRequest(emulation, pauseEmulation)); 247 emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation); 248 var eventIdx = WaitHandle.WaitAny( new [] { timeoutEvent.WaitHandle, patternMismatchEvent } ); 249 250 if(!stateChanged) 251 { 252 throw new InvalidOperationException("Expected blinking pattern not detected (LED state never changed)"); 253 } 254 if(eventIdx == 1) 255 { 256 throw new InvalidOperationException("Expected blinking pattern not detected (State duration was out of specified range)"); 257 } 258 } 259 finally 260 { 261 led.StateChanged -= method; 262 } 263 264 emulationPausedEvent?.WaitOne(); 265 266 return this; 267 } 268 269 [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] IsInRange(double actualValue, double expectedValue, double tolerance)270 private bool IsInRange(double actualValue, double expectedValue, double tolerance) 271 { 272 return (actualValue >= expectedValue * (1 - tolerance)) && (actualValue <= (expectedValue * (1 + tolerance))); 273 } 274 275 [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] GetTimeoutEvent(ulong timeout, Action callback = null)276 private TimeoutEvent GetTimeoutEvent(ulong timeout, Action callback = null) 277 { 278 return machine.LocalTimeSource.EnqueueTimeoutEvent(timeout, callback); 279 } 280 MakeStateChangeHandler(Action<bool, TimeInterval> stateChanged)281 private Action<ILed, bool> MakeStateChangeHandler(Action<bool, TimeInterval> stateChanged) 282 { 283 TimeStamp? previousEventTimestamp = null; 284 return (Action<ILed, bool>)((led, currState) => 285 { 286 if(!TimeDomainsManager.Instance.TryGetVirtualTimeStamp(out var vts)) 287 { 288 throw new InvalidOperationException("Couldn't obtain virtual time"); 289 } 290 291 // first we need to "sync" with the first state change; 292 // it doesn't matter if that's low/high or high/low transition 293 if(previousEventTimestamp == null) 294 { 295 previousEventTimestamp = vts; 296 return; 297 } 298 299 TimeInterval dt; 300 // TODO: Below `if` block is a way to avoid TimeInterval underflow and the resulting exception 301 // that occurs when the OnGPIO comes from a different thread(core) than expected. 302 // This should be fixed by making sure we always get the Timestamp from the correct thread. 303 // Until then the results will be radomly less accurate. 304 if(vts.TimeElapsed > previousEventTimestamp.Value.TimeElapsed) 305 { 306 dt = vts.TimeElapsed - previousEventTimestamp.Value.TimeElapsed; 307 } 308 else 309 { 310 dt = TimeInterval.Empty; 311 } 312 313 stateChanged.Invoke(currState, dt); 314 315 previousEventTimestamp = vts; 316 }); 317 } 318 MakePauseRequest(Emulation emulation, bool pause)319 private Action MakePauseRequest(Emulation emulation, bool pause) 320 { 321 return pause ? (Action)(() => 322 { 323 emulation.PauseAll(); 324 }) : null; 325 } 326 StartEmulationAndGetPausedEvent(Emulation emulation, bool pause)327 private AutoResetEvent StartEmulationAndGetPausedEvent(Emulation emulation, bool pause) 328 { 329 var emulationPausedEvent = pause ? emulation.GetStartedStateChangedEvent(false) : null; 330 if(!emulation.IsStarted) 331 { 332 emulation.StartAll(); 333 } 334 return emulationPausedEvent; 335 } 336 337 // If no min or max is provided, this function checks whether the argument is not negative 338 // (if allowZero) or positive (otherwise) ValidateArgument(double value, string name, bool allowZero = false, double? min = null, double? max = null)339 private static void ValidateArgument(double value, string name, bool allowZero = false, 340 double? min = null, double? max = null) 341 { 342 if(min != null || max != null) 343 { 344 if(value < min || value > max) 345 { 346 throw new ArgumentException($"Value must be in range [{min}; {max}], but was {value}", name); 347 } 348 } 349 else if(value < 0 || !allowZero && value == 0) 350 { 351 var explanation = allowZero ? "not be negative" : "be positive"; 352 throw new ArgumentException($"Value must {explanation}, but was {value}", name); 353 } 354 } 355 356 private readonly ILed led; 357 private readonly IMachine machine; 358 private readonly float defaultTimeout; 359 } 360 } 361 362