GCC Code Coverage Report


Directory: ./
File: firmware/controllers/system/timer/pwm_generator_logic.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 82.7% 134 0 162
Functions: 82.4% 14 0 17
Branches: 66.7% 66 0 99
Decisions: 73.2% 41 - 56

Line Branch Decision Exec Source
1 /**
2 * @file pwm_generator_logic.cpp
3 *
4 * This PWM implementation keep track of when it would be the next time to toggle the signal.
5 * It constantly sets timer to that next toggle time, then sets the timer again from the callback, and so on.
6 *
7 * @date Mar 2, 2014
8 * @author Andrey Belomutskiy, (c) 2012-2020
9 */
10
11 #include "pch.h"
12
13
14 #if EFI_PROD_CODE
15 #include "mpu_util.h"
16 #include "gpio_ext.h"
17 #endif // EFI_PROD_CODE
18
19 // 1% duty cycle
20 #define ZERO_PWM_THRESHOLD 0.01
21 // 99% duty cycle
22 #define FULL_PWM_THRESHOLD 0.99
23
24 23 SimplePwm::SimplePwm()
25 {
26 23 seq.waveCount = 1;
27 23 seq.phaseCount = 2;
28 23 }
29
30 9 SimplePwm::SimplePwm(const char *name) : SimplePwm() {
31 9 m_name = name;
32 9 }
33
34 26 PwmConfig::PwmConfig() {
35 26 memset((void*)&scheduling, 0, sizeof(scheduling));
36 26 memset((void*)&safe, 0, sizeof(safe));
37 26 dbgNestingLevel = 0;
38 26 periodNt = NAN;
39 26 mode = PM_NORMAL;
40 26 memset(&outputPins, 0, sizeof(outputPins));
41 26 m_name = "[noname]";
42 26 }
43
44 /**
45 * This method allows you to change duty cycle on the fly
46 * @param dutyCycle value between 0 and 1
47 * See also setFrequency
48 */
49 2187 void SimplePwm::setSimplePwmDutyCycle(float dutyCycle) {
50
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2187 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2187 times.
2187 if (isStopRequested) {
51 // we are here in order to not change pin once PWM stop was requested
52 return;
53 }
54
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2187 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2187 times.
2187 if (std::isnan(dutyCycle)) {
55 warning(ObdCode::CUSTOM_DUTY_INVALID, "%s spwd:dutyCycle %.2f", m_name, dutyCycle);
56 return;
57
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2187 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2187 times.
2187 } else if (dutyCycle < 0) {
58 warning(ObdCode::CUSTOM_DUTY_TOO_LOW, "%s dutyCycle too low %.2f", m_name, dutyCycle);
59 dutyCycle = 0;
60
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2187 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2187 times.
2187 } else if (dutyCycle > 1) {
61 warning(ObdCode::CUSTOM_PWM_DUTY_TOO_HIGH, "%s duty too high %.2f", m_name, dutyCycle);
62 dutyCycle = 1;
63 }
64
65 #if EFI_PROD_CODE
66 if (hardPwm) {
67 hardPwm->setDuty(dutyCycle);
68 return;
69 }
70 #endif
71
72 // Handle near-zero and near-full duty cycle. This will cause the PWM output to behave like a plain digital output.
73
2/2
✓ Branch 0 taken 1756 times.
✓ Branch 1 taken 431 times.
2/2
✓ Decision 'true' taken 1756 times.
✓ Decision 'false' taken 431 times.
2187 if (dutyCycle < ZERO_PWM_THRESHOLD) {
74 1756 mode = PM_ZERO;
75
76
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1755 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 1755 times.
1756 if (m_stateChangeCallback) {
77 // Manually fire falling edge
78 1 m_stateChangeCallback(0, this);
79 }
80
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 430 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 430 times.
431 } else if (dutyCycle > FULL_PWM_THRESHOLD) {
81 1 mode = PM_FULL;
82
83
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1 time.
1 if (m_stateChangeCallback) {
84 // Manually fire rising edge
85 m_stateChangeCallback(1, this);
86 }
87 } else {
88 430 mode = PM_NORMAL;
89 430 seq.setSwitchTime(0, dutyCycle);
90 }
91 }
92
93 /**
94 * returns absolute timestamp of state change
95 */
96 15 static efitick_t getNextSwitchTimeNt(PwmConfig *state) {
97
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, state->safe.phaseIndex < PWM_PHASE_MAX_COUNT, "phaseIndex range", 0);
98 15 int iteration = state->safe.iteration;
99 // we handle PM_ZERO and PM_FULL separately
100
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 10 times.
15 float switchTime = state->mode == PM_NORMAL ? state->multiChannelStateSequence->getSwitchTime(state->safe.phaseIndex) : 1;
101 15 float periodNt = state->safe.periodNt;
102 #if DEBUG_PWM
103 efiPrintf("iteration=%d switchTime=%.2f period=%.2f", iteration, switchTime, period);
104 #endif /* DEBUG_PWM */
105
106 /**
107 * Once 'iteration' gets relatively high, we might lose calculation precision here.
108 * This is addressed by iterationLimit below, using any many cycles as possible without overflowing timeToSwitchNt
109 * Shall we reuse 'sumTickAndFloat' here?
110 */
111 15 uint32_t timeToSwitchNt = (uint32_t)((iteration + switchTime) * periodNt);
112
113 #if DEBUG_PWM
114 efiPrintf("start=%d timeToSwitch=%d", state->safe.start, timeToSwitch);
115 #endif /* DEBUG_PWM */
116 15 return state->safe.startNt + timeToSwitchNt;
117 }
118
119 8 void PwmConfig::setFrequency(float frequency) {
120
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 6 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 6 times.
8 if (std::isnan(frequency)) {
121 // explicit code just to be sure
122 2 periodNt = NAN;
123 2 return;
124 }
125 /**
126 * see #handleCycleStart()
127 * 'periodNt' is below 10 seconds here so we use 32 bit type for performance reasons
128 */
129 6 periodNt = USF2NT(frequency2periodUs(frequency));
130 }
131
132 void PwmConfig::stop() {
133 isStopRequested = true;
134 }
135
136 13 void PwmConfig::handleCycleStart() {
137
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 13 times.
13 if (safe.phaseIndex != 0) {
138 // https://github.com/rusefi/rusefi/issues/1030
139 firmwareError(ObdCode::CUSTOM_PWM_CYCLE_START, "handleCycleStart %d", safe.phaseIndex);
140 return;
141 }
142
143
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 13 times.
13 if (m_pwmCycleCallback) {
144 m_pwmCycleCallback(this);
145 }
146
147 // Compute the maximum number of iterations without overflowing a uint32_t worth of timestamp
148 13 uint32_t iterationLimitInt32 = (0xFFFFFFFF / periodNt) - 2;
149
150 // Maximum number of iterations that don't lose precision due to 32b float (~7 decimal significant figures)
151 // We want at least 0.01% timing precision (aka 1/10000 cycle, 0.072 degree for trigger stimulator), which
152 // means we can't do any more than 2^23 / 10000 cycles = 838 iterations before a reset
153 13 uint32_t iterationLimitFloat = 838;
154
155 13 uint32_t iterationLimit = minI(iterationLimitInt32, iterationLimitFloat);
156
157
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 efiAssertVoid(ObdCode::CUSTOM_ERR_6580, periodNt != 0, "period not initialized");
158
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 efiAssertVoid(ObdCode::CUSTOM_ERR_6580, iterationLimit > 0, "iterationLimit invalid");
159
4/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 10 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 10 times.
13 if (forceCycleStart || safe.periodNt != periodNt || safe.iteration == iterationLimit) {
160 /**
161 * period length has changed - we need to reset internal state
162 */
163 3 safe.startNt = getTimeNowNt();
164 3 safe.iteration = 0;
165 3 safe.periodNt = periodNt;
166
167 3 forceCycleStart = false;
168 #if DEBUG_PWM
169 efiPrintf("state reset start=%d iteration=%d", state->safe.start, state->safe.iteration);
170 #endif
171 }
172 }
173
174 /**
175 * @return Next time for signal toggle
176 */
177 18 efitick_t PwmConfig::togglePwmState() {
178
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 18 times.
18 if (isStopRequested) {
179 return 0;
180 }
181
182 #if DEBUG_PWM
183 efiPrintf("togglePwmState phaseIndex=%d iteration=%d", safe.phaseIndex, safe.iteration);
184 efiPrintf("period=%.2f safe.period=%.2f", period, safe.periodNt);
185 #endif
186
187
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 15 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 15 times.
18 if (std::isnan(periodNt)) {
188 // NaN period means PWM is paused, we also set the pin low
189
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 3 times.
✗ Decision 'false' not taken.
3 if (m_stateChangeCallback) {
190 3 m_stateChangeCallback(0, this);
191 }
192
193 3 return getTimeNowNt() + MS2NT(NAN_FREQUENCY_SLEEP_PERIOD_MS);
194 }
195
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
2/2
✓ Decision 'true' taken 10 times.
✓ Decision 'false' taken 5 times.
15 if (mode != PM_NORMAL) {
196 // in case of ZERO or FULL we are always at starting index
197 10 safe.phaseIndex = 0;
198 }
199
200
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 2 times.
2/2
✓ Decision 'true' taken 13 times.
✓ Decision 'false' taken 2 times.
15 if (safe.phaseIndex == 0) {
201 13 handleCycleStart();
202 }
203
204 /**
205 * Here is where the 'business logic' - the actual pin state change is happening
206 */
207 int cbStateIndex;
208
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 10 times.
2/2
✓ Decision 'true' taken 5 times.
✓ Decision 'false' taken 10 times.
15 if (mode == PM_NORMAL) {
209 // callback state index is offset by one. todo: why? can we simplify this?
210
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 cbStateIndex = safe.phaseIndex == 0 ? multiChannelStateSequence->phaseCount - 1 : safe.phaseIndex - 1;
211
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
2/2
✓ Decision 'true' taken 6 times.
✓ Decision 'false' taken 4 times.
10 } else if (mode == PM_ZERO) {
212 6 cbStateIndex = 0;
213 } else {
214 4 cbStateIndex = 1;
215 }
216
217 {
218 15 ScopePerf perf(PE::PwmConfigStateChangeCallback);
219
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 15 times.
✗ Decision 'false' not taken.
15 if (m_stateChangeCallback) {
220
1/1
✓ Branch 1 taken 15 times.
15 m_stateChangeCallback(cbStateIndex, this);
221 }
222 }
223
224 15 efitick_t nextSwitchTimeNt = getNextSwitchTimeNt(this);
225 #if DEBUG_PWM
226 efiPrintf("%s: nextSwitchTime %d", state->m_name, nextSwitchTime);
227 #endif /* DEBUG_PWM */
228
229 // If we're very far behind schedule, restart the cycle fresh to avoid scheduling a huge pile of events all at once
230 // This can happen during config write or debugging where CPU is halted for multiple seconds
231 15 bool isVeryBehindSchedule = nextSwitchTimeNt < getTimeNowNt() - MS2NT(10);
232
233 15 safe.phaseIndex++;
234
5/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 10 times.
✓ Branch 5 taken 3 times.
2/2
✓ Decision 'true' taken 12 times.
✓ Decision 'false' taken 3 times.
15 if (isVeryBehindSchedule || safe.phaseIndex == multiChannelStateSequence->phaseCount || mode != PM_NORMAL) {
235 12 safe.phaseIndex = 0; // restart
236 12 safe.iteration++;
237
238
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 12 times.
12 if (isVeryBehindSchedule) {
239 forceCycleStart = true;
240 }
241 }
242 #if EFI_UNIT_TEST
243 15 printf("PWM: nextSwitchTimeNt=%d phaseIndex=%d iteration=%d\r\n", nextSwitchTimeNt,
244 safe.phaseIndex,
245 safe.iteration);
246 #endif /* EFI_UNIT_TEST */
247 15 return nextSwitchTimeNt;
248 }
249
250 /**
251 * Main PWM loop: toggle pin & schedule next invocation
252 *
253 * First invocation happens on application thread
254 */
255 18 static void timerCallback(PwmConfig *state) {
256 18 ScopePerf perf(PE::PwmGeneratorCallback);
257
258 18 state->dbgNestingLevel++;
259
1/3
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
✗ Branch 3 not taken.
18 efiAssertVoid(ObdCode::CUSTOM_ERR_6581, state->dbgNestingLevel < 25, "PWM nesting issue");
260
261
1/1
✓ Branch 1 taken 18 times.
18 efitick_t switchTimeNt = state->togglePwmState();
262
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 18 times.
18 if (switchTimeNt == 0) {
263 // we are here when PWM gets stopped
264 return;
265 }
266
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 18 times.
18 if (state->m_executor == nullptr) {
267 firmwareError(ObdCode::CUSTOM_NULL_EXECUTOR, "exec on %s", state->m_name);
268 return;
269 }
270
271
1/1
✓ Branch 3 taken 18 times.
18 state->m_executor->schedule(state->m_name, &state->scheduling, switchTimeNt, action_s::make<timerCallback>( state ));
272 18 state->dbgNestingLevel--;
273 }
274
275 /**
276 * Incoming parameters are potentially just values on current stack, so we have to copy
277 * into our own permanent storage, right?
278 */
279 4 void copyPwmParameters(PwmConfig *state, MultiChannelStateSequence const * seq) {
280 4 state->multiChannelStateSequence = seq;
281
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 1 time.
4 if (state->mode == PM_NORMAL) {
282 3 state->multiChannelStateSequence->checkSwitchTimes(1);
283 }
284 4 }
285
286 /**
287 * this method also starts the timer cycle
288 * See also startSimplePwm
289 */
290 4 void PwmConfig::weComplexInit(Scheduler *executor,
291 MultiChannelStateSequence const * seq,
292 pwm_cycle_callback *pwmCycleCallback, pwm_gen_callback *stateChangeCallback) {
293 4 m_executor = executor;
294 4 isStopRequested = false;
295
296 // NaN is 'not initialized' but zero is not expected
297
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 criticalAssertVoid(periodNt != 0, "period is not initialized");
298
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 criticalAssertVoid(seq->phaseCount != 0, "signal length cannot be zero");
299
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 criticalAssertVoid(seq->phaseCount <= PWM_PHASE_MAX_COUNT, "too many phases in PWM");
300
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 criticalAssertVoid(seq->waveCount > 0, "waveCount should be positive");
301
302 4 m_pwmCycleCallback = pwmCycleCallback;
303 4 m_stateChangeCallback = stateChangeCallback;
304
305 4 copyPwmParameters(this, seq);
306
307 4 safe.phaseIndex = 0;
308 4 safe.periodNt = -1;
309 4 safe.iteration = -1;
310
311 // let's start the indefinite callback loop of PWM generation
312 4 timerCallback(this);
313 }
314
315 4 void startSimplePwm(SimplePwm *state, const char *msg,
316 Scheduler *executor,
317 OutputPin *output, float frequency, float dutyCycle, pwm_gen_callback *callback) {
318
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 efiAssertVoid(ObdCode::CUSTOM_ERR_PWM_STATE_ASSERT, state != NULL, "state");
319
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
4 efiAssertVoid(ObdCode::CUSTOM_ERR_PWM_DUTY_ASSERT, dutyCycle >= 0 && dutyCycle <= PWM_MAX_DUTY, "dutyCycle");
320
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 4 times.
4 if (frequency < 1) {
321 warning(ObdCode::CUSTOM_OBD_LOW_FREQUENCY, "low frequency %.2f %s", frequency, msg);
322 return;
323 }
324
325 #if EFI_PROD_CODE
326 #if (BOARD_EXT_GPIOCHIPS > 0)
327 if (!callback) {
328 /* No specific scheduler, we can try enabling HW PWM */
329 if (brain_pin_is_ext(output->brainPin)) {
330 /* this pin is driven by external gpio chip, let's see if it can PWM */
331 state->hardPwm = gpiochip_tryInitPwm(msg, output->brainPin, frequency, dutyCycle);
332 }
333 /* TODO: sohuld we try to init MCU PWM on on-chip brainPin?
334 * Or this should be done only on startSimplePwmHard() call? */
335 }
336
337 /* We have succesufully started HW PWM on this output, no need to continue with SW */
338 if (state->hardPwm) {
339 return;
340 }
341 #endif
342 #endif
343
344 /* Set default executor for SW PWM */
345
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 4 times.
✗ Decision 'false' not taken.
4 if (!callback) {
346 4 callback = applyPinState;
347 }
348
349 4 state->seq.setSwitchTime(0, dutyCycle);
350 4 state->seq.setSwitchTime(1, PWM_MAX_DUTY);
351 4 state->seq.setChannelState(0, 0, TriggerValue::FALL);
352 4 state->seq.setChannelState(0, PWM_MAX_DUTY, TriggerValue::RISE);
353
354 4 state->outputPins[0] = output;
355
356 4 state->setFrequency(frequency);
357 4 state->setSimplePwmDutyCycle(dutyCycle);
358 4 state->weComplexInit(executor, &state->seq, nullptr, callback);
359 }
360
361 void startSimplePwmExt(SimplePwm *state, const char *msg,
362 Scheduler *executor,
363 brain_pin_e brainPin, OutputPin *output, float frequency,
364 float dutyCycle, pwm_gen_callback *callback) {
365
366 output->initPin(msg, brainPin);
367
368 startSimplePwm(state, msg, executor, output, frequency, dutyCycle, callback);
369 }
370
371 /**
372 * @param dutyCycle value between 0 and 1
373 */
374 void startSimplePwmHard(SimplePwm *state, const char *msg,
375 Scheduler *executor,
376 brain_pin_e brainPin, OutputPin *output, float frequency,
377 float dutyCycle) {
378 #if EFI_PROD_CODE && HAL_USE_PWM
379 auto hardPwm = hardware_pwm::tryInitPin(msg, brainPin, frequency, dutyCycle);
380
381 if (hardPwm) {
382 state->hardPwm = hardPwm;
383 } else {
384 #endif
385 startSimplePwmExt(state, msg, executor, brainPin, output, frequency, dutyCycle);
386 #if EFI_PROD_CODE && HAL_USE_PWM
387 }
388 #endif
389 }
390
391 /**
392 * default implementation of pwm_gen_callback which simply toggles the pins
393 *
394 */
395 19 void PwmConfig::applyPwmValue(OutputPin *output, int stateIndex, /* weird argument order to facilitate default parameter value */int channelIndex) {
396 19 TriggerValue value = multiChannelStateSequence->getChannelState(channelIndex, stateIndex);
397 19 output->setValue(value == TriggerValue::RISE);
398 19 }
399
400 /**
401 * This method controls the actual hardware pins
402 */
403 19 void applyPinState(int stateIndex, PwmConfig *state) /* pwm_gen_callback */ {
404 #if EFI_PROD_CODE
405 if (!engine->isPwmEnabled) {
406 for (int channelIndex = 0; channelIndex < state->multiChannelStateSequence->waveCount; channelIndex++) {
407 OutputPin *output = state->outputPins[channelIndex];
408 output->setValue(0);
409 }
410 return;
411 }
412 #endif // EFI_PROD_CODE
413
414
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 efiAssertVoid(ObdCode::CUSTOM_ERR_6663, stateIndex < PWM_PHASE_MAX_COUNT, "invalid stateIndex");
415
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 efiAssertVoid(ObdCode::CUSTOM_ERR_6664, state->multiChannelStateSequence->waveCount <= PWM_PHASE_MAX_WAVE_PER_PWM, "invalid waveCount");
416
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 19 times.
2/2
✓ Decision 'true' taken 19 times.
✓ Decision 'false' taken 19 times.
38 for (int channelIndex = 0; channelIndex < state->multiChannelStateSequence->waveCount; channelIndex++) {
417 19 OutputPin *output = state->outputPins[channelIndex];
418 19 state->applyPwmValue(output, stateIndex, channelIndex);
419 }
420 }
421