1MHz software timer into F103 project #3840
This commit is contained in:
parent
5371a510b1
commit
33e0ece536
|
@ -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<efitime_t> 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;
|
||||
}
|
|
@ -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<efitime_t> 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;
|
||||
};
|
||||
|
|
@ -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 */
|
|
@ -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)
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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 <typename TArg>
|
||||
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;
|
||||
};
|
|
@ -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<efitick_t> 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 */
|
||||
|
|
@ -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();
|
||||
|
Loading…
Reference in New Issue