diff --git a/misc/stm32f1_test_project/timer/event_queue.cpp b/misc/stm32f1_test_project/timer/event_queue.cpp new file mode 100644 index 0000000000..b7ffc8a5ff --- /dev/null +++ b/misc/stm32f1_test_project/timer/event_queue.cpp @@ -0,0 +1,279 @@ +/** + * @file event_queue.cpp + * This is a data structure which keeps track of all pending events + * Implemented as a linked list, which is fine since the number of + * pending events is pretty low + * todo: MAYBE migrate to a better data structure, but that's low priority + * + * this data structure is NOT thread safe + * + * @date Apr 17, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#include "pch.h" +#include "os_access.h" +#include "event_queue.h" +#include "efitime.h" +#include "os_util.h" + +#if EFI_UNIT_TEST +extern int timeNowUs; +extern bool verboseMode; +#endif /* EFI_UNIT_TEST */ + + +/** + * @return true if inserted into the head of the list + */ +bool EventQueue::insertTask(scheduling_s *scheduling, efitime_t timeX, action_s action) { + ScopePerf perf(PE::EventQueueInsertTask); + +#if EFI_UNIT_TEST + assertListIsSorted(); +#endif /* EFI_UNIT_TEST */ + efiAssert(CUSTOM_ERR_ASSERT, action.getCallback() != NULL, "NULL callback", false); + +// please note that simulator does not use this code at all - simulator uses signal_executor_sleep + + if (scheduling->action) { +#if EFI_UNIT_TEST + if (verboseMode) { + printf("Already scheduled was %d\r\n", (int)scheduling->momentX); + printf("Already scheduled now %d\r\n", (int)timeX); + } +#endif /* EFI_UNIT_TEST */ + return false; + } + + scheduling->momentX = timeX; + scheduling->action = action; + + if (head == NULL || timeX < head->momentX) { + // here we insert into head of the linked list + LL_PREPEND2(head, scheduling, nextScheduling_s); +#if EFI_UNIT_TEST + assertListIsSorted(); +#endif /* EFI_UNIT_TEST */ + return true; + } else { + // here we know we are not in the head of the list, let's find the position - linear search + scheduling_s *insertPosition = head; + while (insertPosition->nextScheduling_s != NULL && insertPosition->nextScheduling_s->momentX < timeX) { + insertPosition = insertPosition->nextScheduling_s; + } + + scheduling->nextScheduling_s = insertPosition->nextScheduling_s; + insertPosition->nextScheduling_s = scheduling; +#if EFI_UNIT_TEST + assertListIsSorted(); +#endif /* EFI_UNIT_TEST */ + return false; + } +} + +void EventQueue::remove(scheduling_s* scheduling) { +#if EFI_UNIT_TEST + assertListIsSorted(); +#endif /* EFI_UNIT_TEST */ + + // Special case: event isn't scheduled, so don't cancel it + if (!scheduling->action) { + return; + } + + // Special case: empty list, nothing to do + if (!head) { + return; + } + + // Special case: is the item to remove at the head? + if (scheduling == head) { + head = head->nextScheduling_s; + scheduling->nextScheduling_s = nullptr; + scheduling->action = {}; + } else { + auto prev = head; // keep track of the element before the one to remove, so we can link around it + auto current = prev->nextScheduling_s; + + // Find our element + while (current && current != scheduling) { + prev = current; + current = current->nextScheduling_s; + } + + // Walked off the end, this is an error since this *should* have been scheduled + if (!current) { + firmwareError(OBD_PCM_Processor_Fault, "EventQueue::remove didn't find element"); + return; + } + + efiAssertVoid(OBD_PCM_Processor_Fault, current == scheduling, "current not equal to scheduling"); + + // Link around the removed item + prev->nextScheduling_s = current->nextScheduling_s; + + // Clean the item to remove + current->nextScheduling_s = nullptr; + current->action = {}; + } + +#if EFI_UNIT_TEST + assertListIsSorted(); +#endif /* EFI_UNIT_TEST */ +} + +/** + * On this layer it does not matter which units are used - us, ms ot nt. + * + * This method is always invoked under a lock + * @return Get the timestamp of the soonest pending action, skipping all the actions in the past + */ +expected EventQueue::getNextEventTime(efitime_t nowX) const { + if (head != NULL) { + if (head->momentX <= nowX) { + /** + * We are here if action timestamp is in the past. We should rarely be here since this 'getNextEventTime()' is + * always invoked by 'scheduleTimerCallback' which is always invoked right after 'executeAllPendingActions' - but still, + * for events which are really close to each other we would end up here. + * + * looks like we end up here after 'writeconfig' (which freezes the firmware) - we are late + * for the next scheduled event + */ + return nowX + lateDelay; + } else { + return head->momentX; + } + } + + return unexpected; +} + +/** + * See also maxPrecisionCallbackDuration for total hw callback time + */ +uint32_t maxEventCallbackDuration = 0; + +/** + * Invoke all pending actions prior to specified timestamp + * @return number of executed actions + */ +int EventQueue::executeAll(efitime_t now) { + ScopePerf perf(PE::EventQueueExecuteAll); + + int executionCounter = 0; + +#if EFI_UNIT_TEST + assertListIsSorted(); +#endif + + bool didExecute; + do { + didExecute = executeOne(now); + executionCounter += didExecute ? 1 : 0; + } while (didExecute); + + return executionCounter; +} + +bool EventQueue::executeOne(efitime_t now) { + // Read the head every time - a previously executed event could + // have inserted something new at the head + scheduling_s* current = head; + + // Queue is empty - bail + if (!current) { + return false; + } + + // If the next event is far in the future, we'll reschedule + // and execute it next time. + // We do this when the next event is close enough that the overhead of + // resetting the timer and scheduling an new interrupt is greater than just + // waiting for the time to arrive. On current CPUs, this is reasonable to set + // around 10 microseconds. + if (current->momentX > now + lateDelay) { + return false; + } + + // near future - spin wait for the event to happen and avoid the + // overhead of rescheduling the timer. + // yes, that's a busy wait but that's what we need here + while (current->momentX > getTimeNowNt()) { + UNIT_TEST_BUSY_WAIT_CALLBACK(); + } + + // step the head forward, unlink this element, clear scheduled flag + head = current->nextScheduling_s; + current->nextScheduling_s = nullptr; + + // Grab the action but clear it in the event so we can reschedule from the action's execution + auto action = current->action; + current->action = {}; + +#if EFI_UNIT_TEST + printf("QUEUE: execute current=%d param=%d\r\n", (uintptr_t)current, (uintptr_t)action.getArgument()); +#endif + + // Execute the current element + { + ScopePerf perf2(PE::EventQueueExecuteCallback); + action.execute(); + } + +#if EFI_UNIT_TEST + // (tests only) Ensure we didn't break anything + assertListIsSorted(); +#endif + + return true; +} + +int EventQueue::size(void) const { + scheduling_s *tmp; + int result; + LL_COUNT2(head, tmp, result, nextScheduling_s); + return result; +} + +void EventQueue::assertListIsSorted() const { + scheduling_s *current = head; + while (current != NULL && current->nextScheduling_s != NULL) { + efiAssertVoid(CUSTOM_ERR_6623, current->momentX <= current->nextScheduling_s->momentX, "list order"); + current = current->nextScheduling_s; + } +} + +scheduling_s * EventQueue::getHead() { + return head; +} + +// todo: reduce code duplication with another 'getElementAtIndexForUnitText' +scheduling_s *EventQueue::getElementAtIndexForUnitText(int index) { + scheduling_s * current; + + LL_FOREACH2(head, current, nextScheduling_s) + { + if (index == 0) + return current; + index--; + } + + return NULL; +} + +void EventQueue::clear(void) { + // Flush the queue, resetting all scheduling_s as though we'd executed them + while(head) { + auto x = head; + // link next element to head + head = x->nextScheduling_s; + + // Reset this element + x->momentX = 0; + x->nextScheduling_s = nullptr; + x->action = {}; + } + + head = nullptr; +} diff --git a/misc/stm32f1_test_project/timer/event_queue.h b/misc/stm32f1_test_project/timer/event_queue.h new file mode 100644 index 0000000000..ca7bb0dcd2 --- /dev/null +++ b/misc/stm32f1_test_project/timer/event_queue.h @@ -0,0 +1,71 @@ +/** + * @file event_queue.h + * + * @date Apr 17, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#include "scheduler.h" +#include "utlist.h" +#include "expected.h" + +#pragma once + +#define QUEUE_LENGTH_LIMIT 1000 + +// templates do not accept field names so we use a macro here +#define assertNotInListMethodBody(T, head, element, field) \ + /* this code is just to validate state, no functional load*/ \ + T * current; \ + int counter = 0; \ + LL_FOREACH2(head, current, field) { \ + if (++counter > QUEUE_LENGTH_LIMIT) { \ + firmwareError(CUSTOM_ERR_LOOPED_QUEUE, "Looped queue?"); \ + return false; \ + } \ + if (current == element) { \ + /** \ + * for example, this might happen in case of sudden RPM change if event \ + * was not scheduled by angle but was scheduled by time. In case of scheduling \ + * by time with slow RPM the whole next fast revolution might be within the wait period \ + */ \ + warning(CUSTOM_RE_ADDING_INTO_EXECUTION_QUEUE, "re-adding element into event_queue"); \ + return true; \ + } \ + } \ + return false; + + +/** + * Execution sorted linked list + */ +class EventQueue { +public: + // See comment in EventQueue::executeAll for info about lateDelay - it sets the + // time gap between events for which we will wait instead of rescheduling the next + // event in a group of events near one another. + EventQueue(efitime_t lateDelay = 0) : lateDelay(lateDelay) {} + + /** + * O(size) - linear search in sorted linked list + */ + bool insertTask(scheduling_s *scheduling, efitime_t timeX, action_s action); + void remove(scheduling_s* scheduling); + + int executeAll(efitime_t now); + bool executeOne(efitime_t now); + + expected getNextEventTime(efitime_t nowUs) const; + void clear(void); + int size(void) const; + scheduling_s *getElementAtIndexForUnitText(int index); + scheduling_s * getHead(); + void assertListIsSorted() const; +private: + /** + * this list is sorted + */ + scheduling_s *head = nullptr; + const efitime_t lateDelay; +}; + diff --git a/misc/stm32f1_test_project/timer/microsecond_timer.cpp b/misc/stm32f1_test_project/timer/microsecond_timer.cpp new file mode 100644 index 0000000000..157b72dde3 --- /dev/null +++ b/misc/stm32f1_test_project/timer/microsecond_timer.cpp @@ -0,0 +1,184 @@ +/** + * @file microsecond_timer.cpp + * + * Here we have a 1MHz timer dedicated to event scheduling. We are using one of the 32-bit timers here, + * so this timer can schedule events up to 4B/100M ~ 4000 seconds ~ 1 hour from current time. + * + * GPT5 timer clock: 84000000Hz + * If only it was a better multiplier of 2 (84000000 = 328125 * 256) + * + * @date Apr 14, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#include "pch.h" +#include "microsecond_timer.h" +#include "port_microsecond_timer.h" + +#if EFI_PROD_CODE + +#include "periodic_task.h" + +// Just in case we have a mechanism to validate that hardware timer is clocked right and all the +// conversions between wall clock and hardware frequencies are done right +// delay in milliseconds +#define TEST_CALLBACK_DELAY 10 +// if hardware timer is 20% off we throw a critical error and call it a day +// maybe this threshold should be 5%? 10%? +#define TIMER_PRECISION_THRESHOLD 0.2 + +/** + * Maximum duration of complete timer callback, all pending events together + * See also 'maxEventCallbackDuration' for maximum duration of one event + */ +uint32_t maxPrecisionCallbackDuration = 0; + +static efitick_t lastSetTimerTimeNt; +static bool isTimerPending = false; + +static int timerCallbackCounter = 0; +static int timerRestartCounter = 0; + +static const char * msg; + +static int timerFreezeCounter = 0; +static int setHwTimerCounter = 0; +static bool hwStarted = false; + +/** + * sets the alarm to the specified number of microseconds from now. + * This function should be invoked under kernel lock which would disable interrupts. + */ +void setHardwareSchedulerTimer(efitick_t nowNt, efitick_t setTimeNt) { + efiAssertVoid(OBD_PCM_Processor_Fault, hwStarted, "HW.started"); + + // How many ticks in the future is this event? + auto timeDeltaNt = setTimeNt - nowNt; + + setHwTimerCounter++; + + /** + * #259 BUG error: not positive deltaTimeNt + * Once in a while we night get an interrupt where we do not expect it + */ + if (timeDeltaNt <= 0) { + timerFreezeCounter++; + warning(CUSTOM_OBD_LOCAL_FREEZE, "local freeze cnt=%d", timerFreezeCounter); + } + + // We need the timer to fire after we return - 1 doesn't work as it may actually schedule in the past + if (timeDeltaNt < US2NT(2)) { + timeDeltaNt = US2NT(2); + } + + if (timeDeltaNt >= TOO_FAR_INTO_FUTURE_NT) { + // we are trying to set callback for too far into the future. This does not look right at all + firmwareError(CUSTOM_ERR_TIMER_OVERFLOW, "setHardwareSchedulerTimer() too far: %d", timeDeltaNt); + return; + } + + // Skip scheduling if there's a firmware error active + if (hasFirmwareError()) { + return; + } + + // Do the actual hardware-specific timer set operation + portSetHardwareSchedulerTimer(nowNt, setTimeNt); + + lastSetTimerTimeNt = getTimeNowNt(); + isTimerPending = true; + timerRestartCounter++; +} + +void globalTimerCallback(); + +void portMicrosecondTimerCallback() { + timerCallbackCounter++; + isTimerPending = false; + + uint32_t before = getTimeNowLowerNt(); + globalTimerCallback(); + uint32_t precisionCallbackDuration = getTimeNowLowerNt() - before; + if (precisionCallbackDuration > maxPrecisionCallbackDuration) { + maxPrecisionCallbackDuration = precisionCallbackDuration; + } +} + +class MicrosecondTimerWatchdogController : public PeriodicTimerController { + void PeriodicTask() override { + efitick_t nowNt = getTimeNowNt(); + if (nowNt >= lastSetTimerTimeNt + 2 * CORE_CLOCK) { + firmwareError(CUSTOM_ERR_SCHEDULING_ERROR, "watchdog: no events since %d", lastSetTimerTimeNt); + return; + } + + msg = isTimerPending ? "No_cb too long" : "Timer not awhile"; + // 2 seconds of inactivity would not look right + efiAssertVoid(CUSTOM_TIMER_WATCHDOG, nowNt < lastSetTimerTimeNt + 2 * CORE_CLOCK, msg); + } + + int getPeriodMs() override { + return 500; + } +}; + +static MicrosecondTimerWatchdogController watchdogControllerInstance; + +static scheduling_s watchDogBuddy; + +static void watchDogBuddyCallback(void*) { + /** + * the purpose of this periodic activity is to make watchdogControllerInstance + * watchdog happy by ensuring that we have scheduler activity even in case of very broken configuration + * without any PWM or input pins + */ + engine->executor.scheduleForLater(&watchDogBuddy, MS2US(1000), watchDogBuddyCallback); +} + +static volatile bool testSchedulingHappened = false; +static efitimems_t testSchedulingStart; + +static void timerValidationCallback(void*) { + testSchedulingHappened = true; + efitimems_t actualTimeSinceScheduling = (currentTimeMillis() - testSchedulingStart); + + if (absI(actualTimeSinceScheduling - TEST_CALLBACK_DELAY) > TEST_CALLBACK_DELAY * TIMER_PRECISION_THRESHOLD) { + firmwareError(CUSTOM_ERR_TIMER_TEST_CALLBACK_WRONG_TIME, "hwTimer broken precision: %ld ms", actualTimeSinceScheduling); + } +} + +/** + * This method would validate that hardware timer callbacks happen with some reasonable precision + * helps to make sure our GPT hardware settings are somewhat right + */ +static void validateHardwareTimer() { + if (hasFirmwareError()) { + return; + } + testSchedulingStart = currentTimeMillis(); + + // to save RAM let's use 'watchDogBuddy' here once before we enable watchdog + engine->executor.scheduleForLater(&watchDogBuddy, MS2US(TEST_CALLBACK_DELAY), timerValidationCallback); + + chThdSleepMilliseconds(TEST_CALLBACK_DELAY + 2); + if (!testSchedulingHappened) { + firmwareError(CUSTOM_ERR_TIMER_TEST_CALLBACK_NOT_HAPPENED, "hwTimer not alive"); + } +} + +void initMicrosecondTimer() { + portInitMicrosecondTimer(); + + hwStarted = true; + + lastSetTimerTimeNt = getTimeNowNt(); + + validateHardwareTimer(); + + watchDogBuddyCallback(NULL); +#if EFI_EMULATE_POSITION_SENSORS + watchdogControllerInstance.Start(); +#endif /* EFI_EMULATE_POSITION_SENSORS */ +} + +#endif /* EFI_PROD_CODE */ diff --git a/misc/stm32f1_test_project/timer/microsecond_timer.h b/misc/stm32f1_test_project/timer/microsecond_timer.h new file mode 100644 index 0000000000..a763f0e1b6 --- /dev/null +++ b/misc/stm32f1_test_project/timer/microsecond_timer.h @@ -0,0 +1,14 @@ +/** + * @file microsecond_timer.h + * + * @date Apr 14, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#pragma once + +void initMicrosecondTimer(); +void setHardwareSchedulerTimer(efitick_t nowNt, efitick_t setTimeNt); + +#define TOO_FAR_INTO_FUTURE_US (10 * US_PER_SECOND) +#define TOO_FAR_INTO_FUTURE_NT US2NT(TOO_FAR_INTO_FUTURE_US) diff --git a/misc/stm32f1_test_project/timer/microsecond_timer_gpt.cpp b/misc/stm32f1_test_project/timer/microsecond_timer_gpt.cpp new file mode 100644 index 0000000000..89e2e7803b --- /dev/null +++ b/misc/stm32f1_test_project/timer/microsecond_timer_gpt.cpp @@ -0,0 +1,45 @@ +#include "pch.h" +#include "port_microsecond_timer.h" + +#if EFI_PROD_CODE && HAL_USE_GPT + +void portSetHardwareSchedulerTimer(efitick_t nowNt, efitick_t setTimeNt) { + int32_t deltaTimeUs = NT2US((int32_t)setTimeNt - (int32_t)nowNt); + + // If already set, reset the timer + if (GPTDEVICE.state == GPT_ONESHOT) { + gptStopTimerI(&GPTDEVICE); + } + + if (GPTDEVICE.state != GPT_READY) { + firmwareError(CUSTOM_HW_TIMER, "HW timer state %d", GPTDEVICE.state); + return; + } + + // Start the timer + gptStartOneShotI(&GPTDEVICE, deltaTimeUs); +} + +static void hwTimerCallback(GPTDriver*) { + portMicrosecondTimerCallback(); +} + +/* + * The specific 1MHz frequency is important here since 'setHardwareUsTimer' method takes microsecond parameter + * For any arbitrary frequency to work we would need an additional layer of conversion. + */ +static constexpr GPTConfig gpt5cfg = { 1000000, /* 1 MHz timer clock.*/ + hwTimerCallback, /* Timer callback.*/ +0, 0 }; + +void portInitMicrosecondTimer() { + gptStart(&GPTDEVICE, &gpt5cfg); + efiAssertVoid(CUSTOM_ERR_TIMER_STATE, GPTDEVICE.state == GPT_READY, "hw state"); +} + +#endif // EFI_PROD_CODE + +// This implementation just uses the generic port counter - this usually returns a count of CPU cycles since start +uint32_t getTimeNowLowerNt() { + return port_rt_get_counter_value(); +} diff --git a/misc/stm32f1_test_project/timer/port_microsecond_timer.h b/misc/stm32f1_test_project/timer/port_microsecond_timer.h new file mode 100644 index 0000000000..b688200ceb --- /dev/null +++ b/misc/stm32f1_test_project/timer/port_microsecond_timer.h @@ -0,0 +1,13 @@ +/** + * This file defines the API for the microsecond timer that a port needs to implement + * + * Do not call these functions directly, they should only be called by microsecond_timer.cpp + */ + +#pragma once + +void portInitMicrosecondTimer(); +void portSetHardwareSchedulerTimer(efitick_t nowNt, efitick_t setTimeNt); + +// The port should call this callback when the timer expires +void portMicrosecondTimerCallback(); diff --git a/misc/stm32f1_test_project/timer/pwm_generator_logic.cpp b/misc/stm32f1_test_project/timer/pwm_generator_logic.cpp new file mode 100644 index 0000000000..bb891b6703 --- /dev/null +++ b/misc/stm32f1_test_project/timer/pwm_generator_logic.cpp @@ -0,0 +1,382 @@ +/** + * @file pwm_generator_logic.cpp + * + * This PWM implementation keep track of when it would be the next time to toggle the signal. + * It constantly sets timer to that next toggle time, then sets the timer again from the callback, and so on. + * + * @date Mar 2, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#include "pch.h" +#include "os_access.h" + +#if EFI_PROD_CODE +#include "mpu_util.h" +#endif // EFI_PROD_CODE + +// 1% duty cycle +#define ZERO_PWM_THRESHOLD 0.01 + +SimplePwm::SimplePwm() +{ + seq.waveCount = 1; + seq.phaseCount = 2; +} + +SimplePwm::SimplePwm(const char *name) : SimplePwm() { + this->name = name; +} + +PwmConfig::PwmConfig() { + memset((void*)&scheduling, 0, sizeof(scheduling)); + memset((void*)&safe, 0, sizeof(safe)); + dbgNestingLevel = 0; + periodNt = NAN; + mode = PM_NORMAL; + memset(&outputPins, 0, sizeof(outputPins)); + pwmCycleCallback = nullptr; + stateChangeCallback = nullptr; + executor = nullptr; + name = "[noname]"; + arg = this; +} + +/** + * This method allows you to change duty cycle on the fly + * @param dutyCycle value between 0 and 1 + * See also setFrequency + */ +void SimplePwm::setSimplePwmDutyCycle(float dutyCycle) { + if (isStopRequested) { + // we are here in order to not change pin once PWM stop was requested + return; + } + if (cisnan(dutyCycle)) { + warning(CUSTOM_DUTY_INVALID, "%s spwd:dutyCycle %.2f", name, dutyCycle); + return; + } else if (dutyCycle < 0) { + warning(CUSTOM_DUTY_TOO_LOW, "%s dutyCycle too low %.2f", name, dutyCycle); + dutyCycle = 0; + } else if (dutyCycle > 1) { + warning(CUSTOM_PWM_DUTY_TOO_HIGH, "%s duty too high %.2f", name, dutyCycle); + dutyCycle = 1; + } + +#if EFI_PROD_CODE + if (hardPwm) { + hardPwm->setDuty(dutyCycle); + return; + } +#endif + + // Handle zero and full duty cycle. This will cause the PWM output to behave like a plain digital output. + if (dutyCycle == 0.0f && stateChangeCallback) { + // Manually fire falling edge + stateChangeCallback(0, arg); + } else if (dutyCycle == 1.0f && stateChangeCallback) { + // Manually fire rising edge + stateChangeCallback(1, arg); + } + + if (dutyCycle < ZERO_PWM_THRESHOLD) { + mode = PM_ZERO; + } else if (dutyCycle > FULL_PWM_THRESHOLD) { + mode = PM_FULL; + } else { + mode = PM_NORMAL; + seq.setSwitchTime(0, dutyCycle); + } +} + +/** + * returns absolute timestamp of state change + */ +static efitick_t getNextSwitchTimeNt(PwmConfig *state) { + efiAssert(CUSTOM_ERR_ASSERT, state->safe.phaseIndex < PWM_PHASE_MAX_COUNT, "phaseIndex range", 0); + int iteration = state->safe.iteration; + // we handle PM_ZERO and PM_FULL separately + float switchTime = state->mode == PM_NORMAL ? state->multiChannelStateSequence->getSwitchTime(state->safe.phaseIndex) : 1; + float periodNt = state->safe.periodNt; +#if DEBUG_PWM + efiPrintf("iteration=%d switchTime=%.2f period=%.2f", iteration, switchTime, period); +#endif /* DEBUG_PWM */ + + /** + * Once 'iteration' gets relatively high, we might lose calculation precision here. + * This is addressed by iterationLimit below, using any many cycles as possible without overflowing timeToSwitchNt + */ + uint32_t timeToSwitchNt = (uint32_t)((iteration + switchTime) * periodNt); + +#if DEBUG_PWM + efiPrintf("start=%d timeToSwitch=%d", state->safe.start, timeToSwitch); +#endif /* DEBUG_PWM */ + return state->safe.startNt + timeToSwitchNt; +} + +void PwmConfig::setFrequency(float frequency) { + if (cisnan(frequency)) { + // explicit code just to be sure + periodNt = NAN; + return; + } + /** + * see #handleCycleStart() + * 'periodNt' is below 10 seconds here so we use 32 bit type for performance reasons + */ + periodNt = USF2NT(frequency2periodUs(frequency)); +} + +void PwmConfig::stop() { + isStopRequested = true; +} + +void PwmConfig::handleCycleStart() { + if (safe.phaseIndex != 0) { + // https://github.com/rusefi/rusefi/issues/1030 + firmwareError(CUSTOM_PWM_CYCLE_START, "handleCycleStart %d", safe.phaseIndex); + return; + } + + if (pwmCycleCallback != NULL) { + pwmCycleCallback(this); + } + // Compute the maximum number of iterations without overflowing a uint32_t worth of timestamp + uint32_t iterationLimit = (0xFFFFFFFF / periodNt) - 2; + + efiAssertVoid(CUSTOM_ERR_6580, periodNt != 0, "period not initialized"); + efiAssertVoid(CUSTOM_ERR_6580, iterationLimit > 0, "iterationLimit invalid"); + if (forceCycleStart || safe.periodNt != periodNt || safe.iteration == iterationLimit) { + /** + * period length has changed - we need to reset internal state + */ + safe.startNt = getTimeNowNt(); + safe.iteration = 0; + safe.periodNt = periodNt; + + forceCycleStart = false; +#if DEBUG_PWM + efiPrintf("state reset start=%d iteration=%d", state->safe.start, state->safe.iteration); +#endif + } +} + +/** + * @return Next time for signal toggle + */ +efitick_t PwmConfig::togglePwmState() { + if (isStopRequested) { + return 0; + } + +#if DEBUG_PWM + efiPrintf("togglePwmState phaseIndex=%d iteration=%d", safe.phaseIndex, safe.iteration); + efiPrintf("period=%.2f safe.period=%.2f", period, safe.periodNt); +#endif + + if (cisnan(periodNt)) { + /** + * NaN period means PWM is paused, we also set the pin low + */ + stateChangeCallback(0, arg); + return getTimeNowNt() + MS2NT(NAN_FREQUENCY_SLEEP_PERIOD_MS); + } + if (mode != PM_NORMAL) { + // in case of ZERO or FULL we are always at starting index + safe.phaseIndex = 0; + } + + if (safe.phaseIndex == 0) { + handleCycleStart(); + } + + /** + * Here is where the 'business logic' - the actual pin state change is happening + */ + int cbStateIndex; + if (mode == PM_NORMAL) { + // callback state index is offset by one. todo: why? can we simplify this? + cbStateIndex = safe.phaseIndex == 0 ? multiChannelStateSequence->phaseCount - 1 : safe.phaseIndex - 1; + } else if (mode == PM_ZERO) { + cbStateIndex = 0; + } else { + cbStateIndex = 1; + } + + { + ScopePerf perf(PE::PwmConfigStateChangeCallback); + stateChangeCallback(cbStateIndex, arg); + } + + efitick_t nextSwitchTimeNt = getNextSwitchTimeNt(this); +#if DEBUG_PWM + efiPrintf("%s: nextSwitchTime %d", state->name, nextSwitchTime); +#endif /* DEBUG_PWM */ + + // If we're very far behind schedule, restart the cycle fresh to avoid scheduling a huge pile of events all at once + // This can happen during config write or debugging where CPU is halted for multiple seconds + bool isVeryBehindSchedule = nextSwitchTimeNt < getTimeNowNt() - MS2NT(10); + + safe.phaseIndex++; + if (isVeryBehindSchedule || safe.phaseIndex == multiChannelStateSequence->phaseCount || mode != PM_NORMAL) { + safe.phaseIndex = 0; // restart + safe.iteration++; + + if (isVeryBehindSchedule) { + forceCycleStart = true; + } + } +#if EFI_UNIT_TEST + printf("PWM: nextSwitchTimeNt=%d phaseIndex=%d iteration=%d\r\n", nextSwitchTimeNt, + safe.phaseIndex, + safe.iteration); +#endif /* EFI_UNIT_TEST */ + return nextSwitchTimeNt; +} + +/** + * Main PWM loop: toggle pin & schedule next invocation + * + * First invocation happens on application thread + */ +static void timerCallback(PwmConfig *state) { + ScopePerf perf(PE::PwmGeneratorCallback); + + state->dbgNestingLevel++; + efiAssertVoid(CUSTOM_ERR_6581, state->dbgNestingLevel < 25, "PWM nesting issue"); + + efitick_t switchTimeNt = state->togglePwmState(); + if (switchTimeNt == 0) { + // we are here when PWM gets stopped + return; + } + if (state->executor == nullptr) { + firmwareError(CUSTOM_NULL_EXECUTOR, "exec on %s", state->name); + return; + } + + state->executor->scheduleByTimestampNt(state->name, &state->scheduling, switchTimeNt, { timerCallback, state }); + state->dbgNestingLevel--; +} + +/** + * Incoming parameters are potentially just values on current stack, so we have to copy + * into our own permanent storage, right? + */ +void copyPwmParameters(PwmConfig *state, MultiChannelStateSequence const * seq) { + state->multiChannelStateSequence = seq; + if (state->mode == PM_NORMAL) { + state->multiChannelStateSequence->checkSwitchTimes(1); + } +} + +/** + * this method also starts the timer cycle + * See also startSimplePwm + */ +void PwmConfig::weComplexInit(const char *msg, ExecutorInterface *executor, + MultiChannelStateSequence const * seq, + pwm_cycle_callback *pwmCycleCallback, pwm_gen_callback *stateChangeCallback) { + UNUSED(msg); + this->executor = executor; + isStopRequested = false; + + efiAssertVoid(CUSTOM_ERR_6582, periodNt != 0, "period is not initialized"); + if (seq->phaseCount == 0) { + firmwareError(CUSTOM_ERR_PWM_1, "signal length cannot be zero"); + return; + } + if (seq->phaseCount > PWM_PHASE_MAX_COUNT) { + firmwareError(CUSTOM_ERR_PWM_2, "too many phases in PWM"); + return; + } + efiAssertVoid(CUSTOM_ERR_6583, seq->waveCount > 0, "waveCount should be positive"); + + this->pwmCycleCallback = pwmCycleCallback; + this->stateChangeCallback = stateChangeCallback; + + copyPwmParameters(this, seq); + + safe.phaseIndex = 0; + safe.periodNt = -1; + safe.iteration = -1; + + // let's start the indefinite callback loop of PWM generation + timerCallback(this); +} + +void startSimplePwm(SimplePwm *state, const char *msg, ExecutorInterface *executor, + OutputPin *output, float frequency, float dutyCycle) { + efiAssertVoid(CUSTOM_ERR_PWM_STATE_ASSERT, state != NULL, "state"); + efiAssertVoid(CUSTOM_ERR_PWM_DUTY_ASSERT, dutyCycle >= 0 && dutyCycle <= 1, "dutyCycle"); + if (frequency < 1) { + warning(CUSTOM_OBD_LOW_FREQUENCY, "low frequency %.2f %s", frequency, msg); + return; + } + + state->seq.setSwitchTime(0, dutyCycle); + state->seq.setSwitchTime(1, 1); + state->seq.setChannelState(0, 0, TV_FALL); + state->seq.setChannelState(0, 1, TV_RISE); + + state->outputPins[0] = output; + + state->setFrequency(frequency); + state->setSimplePwmDutyCycle(dutyCycle); + state->weComplexInit(msg, executor, &state->seq, NULL, (pwm_gen_callback*)applyPinState); +} + +void startSimplePwmExt(SimplePwm *state, const char *msg, + ExecutorInterface *executor, + brain_pin_e brainPin, OutputPin *output, float frequency, + float dutyCycle) { + + output->initPin(msg, brainPin); + + startSimplePwm(state, msg, executor, output, frequency, dutyCycle); +} + +/** + * @param dutyCycle value between 0 and 1 + */ +void startSimplePwmHard(SimplePwm *state, const char *msg, + ExecutorInterface *executor, + brain_pin_e brainPin, OutputPin *output, float frequency, + float dutyCycle) { +#if EFI_PROD_CODE && HAL_USE_PWM + auto hardPwm = hardware_pwm::tryInitPin(msg, brainPin, frequency, dutyCycle); + + if (hardPwm) { + state->hardPwm = hardPwm; + } else { +#endif + startSimplePwmExt(state, msg, executor, brainPin, output, frequency, dutyCycle); +#if EFI_PROD_CODE && HAL_USE_PWM + } +#endif +} + +/** + * This method controls the actual hardware pins + * + * This method takes ~350 ticks. + */ +void applyPinState(int stateIndex, PwmConfig *state) /* pwm_gen_callback */ { +#if EFI_PROD_CODE + if (!engine->isPwmEnabled) { + for (int channelIndex = 0; channelIndex < state->multiChannelStateSequence->waveCount; channelIndex++) { + OutputPin *output = state->outputPins[channelIndex]; + output->setValue(0); + } + return; + } +#endif // EFI_PROD_CODE + + efiAssertVoid(CUSTOM_ERR_6663, stateIndex < PWM_PHASE_MAX_COUNT, "invalid stateIndex"); + efiAssertVoid(CUSTOM_ERR_6664, state->multiChannelStateSequence->waveCount <= PWM_PHASE_MAX_WAVE_PER_PWM, "invalid waveCount"); + for (int channelIndex = 0; channelIndex < state->multiChannelStateSequence->waveCount; channelIndex++) { + OutputPin *output = state->outputPins[channelIndex]; + int value = state->multiChannelStateSequence->getChannelState(channelIndex, stateIndex); + output->setValue(value); + } +} diff --git a/misc/stm32f1_test_project/timer/pwm_generator_logic.h b/misc/stm32f1_test_project/timer/pwm_generator_logic.h new file mode 100644 index 0000000000..cc8cbb8ed0 --- /dev/null +++ b/misc/stm32f1_test_project/timer/pwm_generator_logic.h @@ -0,0 +1,160 @@ +/** + * @file pwm_generator_logic.h + * + * @date Mar 2, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#pragma once + +#include "state_sequence.h" +#include "global.h" +#include "scheduler.h" +#include "efi_gpio.h" + +#define PERCENT_TO_DUTY(x) (x) * 0.01 + +#define NAN_FREQUENCY_SLEEP_PERIOD_MS 100 + +// 99% duty cycle +#define FULL_PWM_THRESHOLD 0.99 + +typedef struct { + /** + * a copy so that all phases are executed on the same period, even if another thread + * would be adjusting PWM parameters + */ + float periodNt; + /** + * Iteration counter + */ + int iteration; + /** + * Start time of current iteration + */ + efitick_t startNt; + int phaseIndex; +} pwm_config_safe_state_s; + +class PwmConfig; + +typedef void (pwm_cycle_callback)(PwmConfig *state); +typedef void (pwm_gen_callback)(int stateIndex, void *arg); + +typedef enum { + PM_ZERO, + PM_NORMAL, + PM_FULL +} pwm_mode_e; + +/** + * @brief Multi-channel software PWM output configuration + */ +class PwmConfig { +public: + PwmConfig(); + void *arg = nullptr; + + void weComplexInit(const char *msg, + ExecutorInterface *executor, + MultiChannelStateSequence const * seq, + pwm_cycle_callback *pwmCycleCallback, + pwm_gen_callback *callback); + + ExecutorInterface *executor; + + /** + * We need to handle zero duty cycle and 100% duty cycle in a special way + */ + pwm_mode_e mode; + bool isStopRequested = false; + + /** + * @param use NAN frequency to pause PWM + */ + void setFrequency(float frequency); + + void handleCycleStart(); + const char *name; + + // todo: 'outputPins' should be extracted away from here since technically one can want PWM scheduler without actual pin output + OutputPin *outputPins[PWM_PHASE_MAX_WAVE_PER_PWM]; + MultiChannelStateSequence const * multiChannelStateSequence = nullptr; + efitick_t togglePwmState(); + void stop(); + + int dbgNestingLevel; + + scheduling_s scheduling; + + pwm_config_safe_state_s safe; + + /** + * this callback is invoked before each wave generation cycle + */ + pwm_cycle_callback *pwmCycleCallback; + + /** + * this main callback is invoked when it's time to switch level on any of the output channels + */ + pwm_gen_callback *stateChangeCallback = nullptr; +private: + /** + * float value of PWM period + * PWM generation is not happening while this value is NAN + */ + float periodNt; + + // Set if we are very far behind schedule and need to reset back to the beginning of a cycle to find our way + bool forceCycleStart = true; +}; + +struct hardware_pwm; + +struct IPwm { + virtual void setSimplePwmDutyCycle(float dutyCycle) = 0; +}; + +class SimplePwm : public PwmConfig, public IPwm { +public: + SimplePwm(); + explicit SimplePwm(const char *name); + void setSimplePwmDutyCycle(float dutyCycle) override; + MultiChannelStateSequenceWithData<2> seq; + hardware_pwm* hardPwm = nullptr; +}; + +/** + * default implementation of pwm_gen_callback which simply toggles the pins + * + */ +void applyPinState(int stateIndex, PwmConfig* state) /* pwm_gen_callback */; + +/** + * Start a one-channel software PWM driver. + * + * This method should be called after scheduling layer is started by initSignalExecutor() + */ +void startSimplePwm(SimplePwm *state, const char *msg, + ExecutorInterface *executor, + OutputPin *output, + float frequency, float dutyCycle); + +/** + * initialize GPIO pin and start a one-channel software PWM driver. + * + * This method should be called after scheduling layer is started by initSignalExecutor() + */ +void startSimplePwmExt(SimplePwm *state, + const char *msg, + ExecutorInterface *executor, + brain_pin_e brainPin, OutputPin *output, + float frequency, float dutyCycle); + +void startSimplePwmHard(SimplePwm *state, const char *msg, + ExecutorInterface *executor, + brain_pin_e brainPin, OutputPin *output, float frequency, + float dutyCycle); + +void copyPwmParameters(PwmConfig *state, MultiChannelStateSequence const * seq); + diff --git a/misc/stm32f1_test_project/timer/scheduler.cpp b/misc/stm32f1_test_project/timer/scheduler.cpp new file mode 100644 index 0000000000..68fd997871 --- /dev/null +++ b/misc/stm32f1_test_project/timer/scheduler.cpp @@ -0,0 +1,21 @@ +/** + * @file scheduler.h + * + * @date October 1, 2020 + */ +#include "pch.h" + +#include "scheduler.h" + +void action_s::execute() { + efiAssertVoid(CUSTOM_ERR_ASSERT, callback != NULL, "callback==null1"); + callback(param); +} + +schfunc_t action_s::getCallback() const { + return callback; +} + +void * action_s::getArgument() const { + return param; +} diff --git a/misc/stm32f1_test_project/timer/scheduler.h b/misc/stm32f1_test_project/timer/scheduler.h new file mode 100644 index 0000000000..352504056a --- /dev/null +++ b/misc/stm32f1_test_project/timer/scheduler.h @@ -0,0 +1,69 @@ +/** + * @file scheduler.h + * + * @date May 18, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ +#pragma once + +typedef void (*schfunc_t)(void *); + +class action_s { +public: + // Default constructor constructs null action (ie, implicit bool conversion returns false) + action_s() = default; + + // Allow implicit conversion from schfunc_t to action_s + action_s(schfunc_t callback) : action_s(callback, nullptr) { } + action_s(schfunc_t callback, void *param) : callback(callback), param(param) { } + + // Allow any function that takes a single pointer parameter, so long as param is also of the same pointer type. + // This constructor means you shouldn't ever have to cast to schfunc_t on your own. + template + action_s(void (*callback)(TArg*), TArg* param) : callback((schfunc_t)callback), param(param) { } + + void execute(); + schfunc_t getCallback() const; + void * getArgument() const; + + operator bool() const { + return callback != nullptr; + } + +private: + schfunc_t callback = nullptr; + void *param = nullptr; +}; + +/** + * This structure holds information about an event scheduled in the future: when to execute what callback with what parameters + */ +#pragma pack(push, 4) +struct scheduling_s { +#if EFI_SIGNAL_EXECUTOR_SLEEP + virtual_timer_t timer; +#endif /* EFI_SIGNAL_EXECUTOR_SLEEP */ + + /** + * timestamp represented as 64-bit value of ticks since MCU start + */ + volatile efitime_t momentX = 0; + + /** + * Scheduler implementation uses a sorted linked list of these scheduling records. + */ + scheduling_s *nextScheduling_s = nullptr; + + action_s action; +}; +#pragma pack(pop) + +struct ExecutorInterface { + /** + * see also scheduleByAngle + */ + virtual void scheduleByTimestamp(const char *msg, scheduling_s *scheduling, efitimeus_t timeUs, action_s action) = 0; + virtual void scheduleByTimestampNt(const char *msg, scheduling_s *scheduling, efitime_t timeUs, action_s action) = 0; + virtual void scheduleForLater(scheduling_s *scheduling, int delayUs, action_s action) = 0; + virtual void cancel(scheduling_s* scheduling) = 0; +}; diff --git a/misc/stm32f1_test_project/timer/single_timer_executor.cpp b/misc/stm32f1_test_project/timer/single_timer_executor.cpp new file mode 100644 index 0000000000..9b253aea4c --- /dev/null +++ b/misc/stm32f1_test_project/timer/single_timer_executor.cpp @@ -0,0 +1,191 @@ +/** + * @file SingleTimerExecutor.cpp + * + * This class combines the powers of a 1MHz hardware timer from microsecond_timer.cpp + * and pending events queue event_queue.cpp + * + * As of version 2.6.x, ChibiOS tick-based kernel is not capable of scheduling events + * with the level of precision we need, and realistically it should not. + * + * Update: actually newer ChibiOS has tickless mode and what we have here is pretty much the same thing :) + * open question if rusEfi should simply migrate to ChibiOS tickless scheduling (which would increase coupling with ChibiOS) + * + * See https://rusefi.com/forum/viewtopic.php?f=5&t=373&start=360#p30895 + * for some performance data: with 'debug' firmware we spend about 5% of CPU in TIM5 handler which seem to be executed + * about 1500 times a second + * + * http://sourceforge.net/p/rusefi/tickets/24/ + * + * @date: Apr 18, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#include "pch.h" + +#include "os_access.h" +#include "single_timer_executor.h" +#include "efitime.h" + +#if EFI_SIGNAL_EXECUTOR_ONE_TIMER + +#include "microsecond_timer.h" +#include "os_util.h" + +uint32_t hwSetTimerDuration; + +void globalTimerCallback() { + efiAssertVoid(CUSTOM_ERR_6624, getCurrentRemainingStack() > EXPECTED_REMAINING_STACK, "lowstck#2y"); + + ___engine.executor.onTimerCallback(); +} + +SingleTimerExecutor::SingleTimerExecutor() + // 8us is roughly the cost of the interrupt + overhead of a single timer event + : queue(US2NT(8)) +{ +} + +void SingleTimerExecutor::scheduleForLater(scheduling_s *scheduling, int delayUs, action_s action) { + scheduleByTimestamp("scheduleForLater", scheduling, getTimeNowUs() + delayUs, action); +} + +/** + * @brief Schedule an event at specific delay after now + * + * Invokes event callback after the specified amount of time. + * callback would be executed either on ISR thread or current thread if we would need to execute right away + * + * @param [in, out] scheduling Data structure to keep this event in the collection. + * @param [in] delayUs the number of microseconds before the output signal immediate output if delay is zero. + * @param [in] dwell the number of ticks of output duration. + */ +void SingleTimerExecutor::scheduleByTimestamp(const char *msg, scheduling_s *scheduling, efitimeus_t timeUs, action_s action) { + scheduleByTimestampNt(msg, scheduling, US2NT(timeUs), action); +} + +void SingleTimerExecutor::scheduleByTimestampNt(const char *msg, scheduling_s* scheduling, efitime_t nt, action_s action) { + ScopePerf perf(PE::SingleTimerExecutorScheduleByTimestamp); + +#if EFI_ENABLE_ASSERTS + int32_t deltaTimeNt = (int32_t)nt - getTimeNowLowerNt(); + + if (deltaTimeNt >= TOO_FAR_INTO_FUTURE_NT) { + // we are trying to set callback for too far into the future. This does not look right at all + firmwareError(CUSTOM_ERR_TASK_TIMER_OVERFLOW, "scheduleByTimestampNt() too far: %d %s", deltaTimeNt, msg); + return; + } +#endif + + scheduleCounter++; + + // Lock for queue insertion - we may already be locked, but that's ok + chibios_rt::CriticalSectionLocker csl; + + bool needToResetTimer = queue.insertTask(scheduling, nt, action); + if (!reentrantFlag) { + executeAllPendingActions(); + if (needToResetTimer) { + scheduleTimerCallback(); + } + } +} + +void SingleTimerExecutor::cancel(scheduling_s* scheduling) { + // Lock for queue removal - we may already be locked, but that's ok + chibios_rt::CriticalSectionLocker csl; + + queue.remove(scheduling); +} + +void SingleTimerExecutor::onTimerCallback() { + timerCallbackCounter++; + + chibios_rt::CriticalSectionLocker csl; + + executeAllPendingActions(); + scheduleTimerCallback(); +} + +/* + * this private method is executed under lock + */ +void SingleTimerExecutor::executeAllPendingActions() { + ScopePerf perf(PE::SingleTimerExecutorDoExecute); + + executeAllPendingActionsInvocationCounter++; + /** + * Let's execute actions we should execute at this point. + * reentrantFlag takes care of the use case where the actions we are executing are scheduling + * further invocations + */ + reentrantFlag = true; + + /** + * in real life it could be that while we executing listeners time passes and it's already time to execute + * next listeners. + * TODO: add a counter & figure out a limit of iterations? + */ + + // starts at -1 because do..while will run a minimum of once + executeCounter = -1; + + bool didExecute; + do { + efitick_t nowNt = getTimeNowNt(); + didExecute = queue.executeOne(nowNt); + + // if we're stuck in a loop executing lots of events, panic! + if (executeCounter++ == 500) { + firmwareError(CUSTOM_ERR_LOCK_ISSUE, "Maximum scheduling run length exceeded - CPU load too high"); + } + + } while (didExecute); + + maxExecuteCounter = maxI(maxExecuteCounter, executeCounter); + + if (!isLocked()) { + firmwareError(CUSTOM_ERR_LOCK_ISSUE, "Someone has stolen my lock"); + return; + } + reentrantFlag = false; +} + +/** + * This method is always invoked under a lock + */ +void SingleTimerExecutor::scheduleTimerCallback() { + ScopePerf perf(PE::SingleTimerExecutorScheduleTimerCallback); + + /** + * Let's grab fresh time value + */ + efitick_t nowNt = getTimeNowNt(); + expected nextEventTimeNt = queue.getNextEventTime(nowNt); + + if (!nextEventTimeNt) { + return; // no pending events in the queue + } + + efiAssertVoid(CUSTOM_ERR_6625, nextEventTimeNt.Value > nowNt, "setTimer constraint"); + + setHardwareSchedulerTimer(nowNt, nextEventTimeNt.Value); +} + +void initSingleTimerExecutorHardware(void) { + initMicrosecondTimer(); +} + +void executorStatistics() { + if (engineConfiguration->debugMode == DBG_EXECUTOR) { +#if EFI_TUNER_STUDIO + engine->outputChannels.debugIntField1 = ___engine.executor.timerCallbackCounter; + engine->outputChannels.debugIntField2 = ___engine.executor.executeAllPendingActionsInvocationCounter; + engine->outputChannels.debugIntField3 = ___engine.executor.scheduleCounter; + engine->outputChannels.debugIntField4 = ___engine.executor.executeCounter; + engine->outputChannels.debugIntField5 = ___engine.executor.maxExecuteCounter; +#endif /* EFI_TUNER_STUDIO */ + } +} + +#endif /* EFI_SIGNAL_EXECUTOR_ONE_TIMER */ + diff --git a/misc/stm32f1_test_project/timer/single_timer_executor.h b/misc/stm32f1_test_project/timer/single_timer_executor.h new file mode 100644 index 0000000000..8465d5d769 --- /dev/null +++ b/misc/stm32f1_test_project/timer/single_timer_executor.h @@ -0,0 +1,36 @@ +/** + * @file single_timer_executor.h + * + * @date: Apr 18, 2014 + * @author Andrey Belomutskiy, (c) 2012-2020 + */ + +#pragma once + +#include "scheduler.h" +#include "event_queue.h" + +class SingleTimerExecutor final : public ExecutorInterface { +public: + SingleTimerExecutor(); + void scheduleByTimestamp(const char *msg, scheduling_s *scheduling, efitimeus_t timeUs, action_s action) override; + void scheduleByTimestampNt(const char *msg, scheduling_s *scheduling, efitime_t timeNt, action_s action) override; + void scheduleForLater(scheduling_s *scheduling, int delayUs, action_s action) override; + void cancel(scheduling_s* scheduling) override; + + void onTimerCallback(); + int timerCallbackCounter = 0; + int scheduleCounter = 0; + int maxExecuteCounter = 0; + int executeCounter; + int executeAllPendingActionsInvocationCounter = 0; +private: + EventQueue queue; + bool reentrantFlag = false; + void executeAllPendingActions(); + void scheduleTimerCallback(); +}; + +void initSingleTimerExecutorHardware(void); +void executorStatistics(); +