cherry-pick timer from ECU and use it for sequencing logic (#289)

* enable timestamp api

* add timer class

* use unsigned integer for timestamps

* heater uses timers

* timer mocking and test adjustment
This commit is contained in:
Matthew Kennedy 2023-11-15 17:42:37 -08:00 committed by GitHub
parent d1e993dd71
commit 17acd065d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 203 additions and 40 deletions

View File

@ -173,7 +173,7 @@ typedef int pid_t;
* @note The default is @p TRUE. * @note The default is @p TRUE.
*/ */
#if !defined(CH_CFG_USE_TIMESTAMP) #if !defined(CH_CFG_USE_TIMESTAMP)
#define CH_CFG_USE_TIMESTAMP FALSE #define CH_CFG_USE_TIMESTAMP TRUE
#endif #endif
/** /**

View File

@ -173,7 +173,7 @@ typedef int pid_t;
* @note The default is @p TRUE. * @note The default is @p TRUE.
*/ */
#if !defined(CH_CFG_USE_TIMESTAMP) #if !defined(CH_CFG_USE_TIMESTAMP)
#define CH_CFG_USE_TIMESTAMP FALSE #define CH_CFG_USE_TIMESTAMP TRUE
#endif #endif
/** /**

View File

@ -173,7 +173,7 @@ typedef int pid_t;
* @note The default is @p TRUE. * @note The default is @p TRUE.
*/ */
#if !defined(CH_CFG_USE_TIMESTAMP) #if !defined(CH_CFG_USE_TIMESTAMP)
#define CH_CFG_USE_TIMESTAMP FALSE #define CH_CFG_USE_TIMESTAMP TRUE
#endif #endif
/** /**

View File

@ -173,7 +173,7 @@ typedef int pid_t;
* @note The default is @p TRUE. * @note The default is @p TRUE.
*/ */
#if !defined(CH_CFG_USE_TIMESTAMP) #if !defined(CH_CFG_USE_TIMESTAMP)
#define CH_CFG_USE_TIMESTAMP FALSE #define CH_CFG_USE_TIMESTAMP TRUE
#endif #endif
/** /**

View File

@ -173,7 +173,7 @@ typedef int pid_t;
* @note The default is @p TRUE. * @note The default is @p TRUE.
*/ */
#if !defined(CH_CFG_USE_TIMESTAMP) #if !defined(CH_CFG_USE_TIMESTAMP)
#define CH_CFG_USE_TIMESTAMP FALSE #define CH_CFG_USE_TIMESTAMP TRUE
#endif #endif
/** /**

View File

@ -5,8 +5,10 @@
using namespace wbo; using namespace wbo;
HeaterControllerBase::HeaterControllerBase(int ch) HeaterControllerBase::HeaterControllerBase(int ch, int preheatTimeSec, int warmupTimeSec)
: ch(ch) : ch(ch)
, m_preheatTimeSec(preheatTimeSec)
, m_warmupTimeSec(warmupTimeSec)
{ {
} }
@ -14,6 +16,9 @@ void HeaterControllerBase::Configure(float targetTempC, float targetEsr)
{ {
m_targetTempC = targetTempC; m_targetTempC = targetTempC;
m_targetEsr = targetEsr; m_targetEsr = targetEsr;
m_preheatTimer.reset();
m_warmupTimer.reset();
} }
bool HeaterControllerBase::IsRunningClosedLoop() const bool HeaterControllerBase::IsRunningClosedLoop() const
@ -55,7 +60,7 @@ HeaterState HeaterControllerBase::GetNextState(HeaterState currentState, HeaterA
if (!heaterAllowed) if (!heaterAllowed)
{ {
// ECU hasn't allowed preheat yet, reset timer, and force preheat state // ECU hasn't allowed preheat yet, reset timer, and force preheat state
timeCounter = preheatTimeCounter; m_preheatTimer.reset();
return HeaterState::Preheat; return HeaterState::Preheat;
} }
@ -66,17 +71,15 @@ HeaterState HeaterControllerBase::GetNextState(HeaterState currentState, HeaterA
switch (currentState) switch (currentState)
{ {
case HeaterState::Preheat: case HeaterState::Preheat:
timeCounter--;
// If preheat timeout, or sensor is already hot (engine running?) // If preheat timeout, or sensor is already hot (engine running?)
if (timeCounter <= 0 || sensorTemp > closedLoopTemp) if (m_preheatTimer.hasElapsedSec(m_preheatTimeSec) || sensorTemp > closedLoopTemp)
{ {
// If enough time has elapsed, start the ramp // If enough time has elapsed, start the ramp
// Start the ramp at 4 volts // Start the ramp at 4 volts
rampVoltage = 4; rampVoltage = 4;
// Next phase times out at 15 seconds // Reset the timer for the warmup phase
timeCounter = HEATER_WARMUP_TIMEOUT / HEATER_CONTROL_PERIOD; m_warmupTimer.reset();
return HeaterState::WarmupRamp; return HeaterState::WarmupRamp;
} }
@ -84,13 +87,11 @@ HeaterState HeaterControllerBase::GetNextState(HeaterState currentState, HeaterA
// Stay in preheat - wait for time to elapse // Stay in preheat - wait for time to elapse
break; break;
case HeaterState::WarmupRamp: case HeaterState::WarmupRamp:
timeCounter--;
if (sensorTemp > closedLoopTemp) if (sensorTemp > closedLoopTemp)
{ {
return HeaterState::ClosedLoop; return HeaterState::ClosedLoop;
} }
else if (timeCounter == 0) else if (m_warmupTimer.hasElapsedSec(m_warmupTimeSec))
{ {
SetFault(ch, Fault::SensorDidntHeat); SetFault(ch, Fault::SensorDidntHeat);
return HeaterState::Stopped; return HeaterState::Stopped;

View File

@ -6,6 +6,7 @@
#include "can.h" #include "can.h"
#include "pid.h" #include "pid.h"
#include "timer.h"
enum class HeaterState enum class HeaterState
{ {
@ -29,7 +30,7 @@ struct IHeaterController
class HeaterControllerBase : public IHeaterController class HeaterControllerBase : public IHeaterController
{ {
public: public:
HeaterControllerBase(int ch); HeaterControllerBase(int ch, int preheatTimeSec, int warmupTimeSec);
void Configure(float targetTempC, float targetEsr); void Configure(float targetTempC, float targetEsr);
void Update(const ISampler& sampler, HeaterAllow heaterAllowState) override; void Update(const ISampler& sampler, HeaterAllow heaterAllowState) override;
@ -42,11 +43,6 @@ public:
HeaterState GetNextState(HeaterState currentState, HeaterAllow haeterAllowState, float batteryVoltage, float sensorTemp); HeaterState GetNextState(HeaterState currentState, HeaterAllow haeterAllowState, float batteryVoltage, float sensorTemp);
float GetVoltageForState(HeaterState state, float sensorEsr); float GetVoltageForState(HeaterState state, float sensorEsr);
int GetTimeCounter() const
{
return timeCounter;
}
private: private:
Pid heaterPid = Pid heaterPid =
{ {
@ -57,7 +53,6 @@ private:
HEATER_CONTROL_PERIOD HEATER_CONTROL_PERIOD
}; };
int timeCounter = preheatTimeCounter;
int batteryStabTime = batteryStabTimeCounter; int batteryStabTime = batteryStabTimeCounter;
float rampVoltage = 0; float rampVoltage = 0;
float heaterVoltage = 0; float heaterVoltage = 0;
@ -69,11 +64,14 @@ private:
float m_targetEsr = 0; float m_targetEsr = 0;
float m_targetTempC = 0; float m_targetTempC = 0;
// TODO: private:
public:
const uint8_t ch; const uint8_t ch;
static const int preheatTimeCounter = HEATER_PREHEAT_TIME / HEATER_CONTROL_PERIOD; const int m_preheatTimeSec;
const int m_warmupTimeSec;
Timer m_preheatTimer;
Timer m_warmupTimer;
static const int batteryStabTimeCounter = HEATER_BATTERY_STAB_TIME / HEATER_CONTROL_PERIOD; static const int batteryStabTimeCounter = HEATER_BATTERY_STAB_TIME / HEATER_CONTROL_PERIOD;
}; };

View File

@ -28,7 +28,7 @@ static const PWMConfig heaterPwmConfig = {
class HeaterController : public HeaterControllerBase { class HeaterController : public HeaterControllerBase {
public: public:
HeaterController(int ch, int pwm_ch) HeaterController(int ch, int pwm_ch)
: HeaterControllerBase(ch) : HeaterControllerBase(ch, HEATER_PREHEAT_TIME, HEATER_WARMUP_TIMEOUT)
, pwm_ch(pwm_ch) , pwm_ch(pwm_ch)
{ {
} }

110
firmware/util/timer.cpp Normal file
View File

@ -0,0 +1,110 @@
#include <cstdint>
#include "timer.h"
#define US_PER_SECOND_F 1000000.0
Timer::Timer() {
init();
}
#ifdef MOCK_TIMER
// in mock land, ticks == microseconds
#define TIME_US2I(us) (us)
#define TIME_I2US(ticks) (ticks)
/*static*/ int64_t Timer::mockTimeStamp = 0;
int64_t Timer::getTimestamp() const {
return Timer::mockTimeStamp;
}
/*static*/ void Timer::setMockTime(int64_t stamp) {
Timer::mockTimeStamp = stamp;
}
#else
#include "ch.hpp"
int64_t Timer::getTimestamp() const {
// Ensure that our timestamp type is compatible with the one ChibiOS returns
static_assert(sizeof(int64_t) == sizeof(systimestamp_t));
return chVTGetTimeStamp();
}
#endif // MOCK_TIMER
void Timer::reset() {
reset(getTimestamp());
}
void Timer::reset(int64_t stamp) {
m_lastReset = stamp;
}
void Timer::init() {
// Use not-quite-minimum value to avoid overflow
m_lastReset = INT64_MIN / 8;
}
bool Timer::hasElapsedSec(float seconds) const {
return hasElapsedMs(seconds * 1000);
}
bool Timer::hasElapsedMs(float milliseconds) const {
return hasElapsedUs(milliseconds * 1000);
}
bool Timer::hasElapsedUs(float microseconds) const {
auto delta = getTimestamp() - m_lastReset;
// If larger than 32 bits, timer has certainly expired
if (delta >= UINT32_MAX) {
return true;
}
auto delta32 = (uint32_t)delta;
return delta32 > TIME_US2I(microseconds);
}
float Timer::getElapsedSeconds() const {
return getElapsedSeconds(getTimestamp());
}
float Timer::getElapsedSeconds(int64_t stamp) const {
return 1 / US_PER_SECOND_F * getElapsedUs(stamp);
}
float Timer::getElapsedUs() const {
return getElapsedUs(getTimestamp());
}
float Timer::getElapsedUs(int64_t stamp) const {
auto deltaNt = stamp - m_lastReset;
// Yes, things can happen slightly in the future if we get a lucky interrupt between
// the timestamp and this subtraction, that updates m_lastReset to what's now "the future",
// resulting in a negative delta.
if (deltaNt < 0) {
return 0;
}
if (deltaNt > UINT32_MAX - 1) {
deltaNt = UINT32_MAX - 1;
}
auto delta32 = (uint32_t)deltaNt;
return TIME_I2US(delta32);
}
float Timer::getElapsedSecondsAndReset() {
auto stamp = getTimestamp();
float result = getElapsedSeconds(stamp);
reset(stamp);
return result;
}

41
firmware/util/timer.h Normal file
View File

@ -0,0 +1,41 @@
#pragma once
/**
* Helper class with "has X amount of time elapsed since most recent reset" methods
* Brand new instances have most recent reset time far in the past, i.e. "hasElapsed" is true for any reasonable range
*/
class Timer final {
public:
Timer();
// returns timer to the most original-as-constructed state
void init();
void reset();
bool hasElapsedSec(float seconds) const;
bool hasElapsedMs(float ms) const;
bool hasElapsedUs(float us) const;
// Return the elapsed time since the last reset.
// If the elapsed time is longer than 2^32 timer tick counts,
// then a time period representing 2^32 counts will be returned.
float getElapsedSeconds() const;
float getElapsedUs() const;
// Perform an atomic update and returning the delta between
// now and the last reset
float getElapsedSecondsAndReset();
static void setMockTime(int64_t stamp);
private:
int64_t getTimestamp() const;
void reset(int64_t stamp);
float getElapsedSeconds(int64_t stamp) const;
float getElapsedUs(int64_t stamp) const;
int64_t m_lastReset;
static int64_t mockTimeStamp;
};

View File

@ -2,3 +2,4 @@ WIDEBANDSRC = \
$(FIRMWARE_DIR)/pid.cpp \ $(FIRMWARE_DIR)/pid.cpp \
$(FIRMWARE_DIR)/sampling.cpp \ $(FIRMWARE_DIR)/sampling.cpp \
$(FIRMWARE_DIR)/heater_control.cpp \ $(FIRMWARE_DIR)/heater_control.cpp \
$(FIRMWARE_DIR)/util/timer.cpp \

View File

@ -40,8 +40,8 @@
// ******************************* // *******************************
#define HEATER_CONTROL_PERIOD 50 #define HEATER_CONTROL_PERIOD 50
#define HEATER_PREHEAT_TIME 5000 #define HEATER_PREHEAT_TIME 5
#define HEATER_WARMUP_TIMEOUT 60000 #define HEATER_WARMUP_TIMEOUT 60
#define HEATER_BATTERY_STAB_TIME 500 #define HEATER_BATTERY_STAB_TIME 500
// minimal battery voltage to start heating without CAN command // minimal battery voltage to start heating without CAN command

View File

@ -39,6 +39,7 @@ INCDIR += \
$(RUSEFI_LIB_INC) \ $(RUSEFI_LIB_INC) \
$(FIRMWARE_DIR) \ $(FIRMWARE_DIR) \
$(FIRMWARE_DIR)/boards \ $(FIRMWARE_DIR)/boards \
$(FIRMWARE_DIR)/util \
# User may want to pass in a forced value for SANITIZE # User may want to pass in a forced value for SANITIZE
ifeq ($(SANITIZE),) ifeq ($(SANITIZE),)
@ -77,6 +78,8 @@ ifeq ($(USE_CPPOPT),)
USE_CPPOPT = -std=c++17 -fno-rtti -fno-use-cxa-atexit USE_CPPOPT = -std=c++17 -fno-rtti -fno-use-cxa-atexit
endif endif
USE_CPPOPT += -DMOCK_TIMER
# Enable address sanitizer for C++ files, but not on Windows since x86_64-w64-mingw32-g++ doesn't support it. # Enable address sanitizer for C++ files, but not on Windows since x86_64-w64-mingw32-g++ doesn't support it.
# only c++ because lua does some things asan doesn't like, but don't actually cause overruns. # only c++ because lua does some things asan doesn't like, but don't actually cause overruns.
ifeq ($(SANITIZE),yes) ifeq ($(SANITIZE),yes)

View File

@ -5,7 +5,7 @@
struct MockHeater : public HeaterControllerBase struct MockHeater : public HeaterControllerBase
{ {
MockHeater() : HeaterControllerBase(0) { } MockHeater() : HeaterControllerBase(0, 5, 10) { }
MOCK_METHOD(void, SetDuty, (float), (const, override)); MOCK_METHOD(void, SetDuty, (float), (const, override));
}; };
@ -55,20 +55,26 @@ TEST(HeaterStateOutput, Cases)
TEST(HeaterStateMachine, PreheatToWarmupTimeout) TEST(HeaterStateMachine, PreheatToWarmupTimeout)
{ {
MockHeater dut; MockHeater dut;
Timer::setMockTime(0);
dut.Configure(780, 300); dut.Configure(780, 300);
for (size_t i = 0; i < HeaterControllerBase::preheatTimeCounter - 1; i++) // For a while it should stay in preheat
{ Timer::setMockTime(1e6);
EXPECT_EQ(HeaterState::Preheat, dut.GetNextState(HeaterState::Preheat, HeaterAllow::Allowed, 12, 500)); EXPECT_EQ(HeaterState::Preheat, dut.GetNextState(HeaterState::Preheat, HeaterAllow::Allowed, 12, 500));
} Timer::setMockTime(2e6);
EXPECT_EQ(HeaterState::Preheat, dut.GetNextState(HeaterState::Preheat, HeaterAllow::Allowed, 12, 500));
Timer::setMockTime(4.9e6);
EXPECT_EQ(HeaterState::Preheat, dut.GetNextState(HeaterState::Preheat, HeaterAllow::Allowed, 12, 500));
// Timer expired, transition to warmup ramp // Timer expired, transition to warmup ramp
Timer::setMockTime(5.1e6);
EXPECT_EQ(HeaterState::WarmupRamp, dut.GetNextState(HeaterState::Preheat, HeaterAllow::Allowed, 12, 500)); EXPECT_EQ(HeaterState::WarmupRamp, dut.GetNextState(HeaterState::Preheat, HeaterAllow::Allowed, 12, 500));
} }
TEST(HeaterStateMachine, PreheatToWarmupAlreadyWarm) TEST(HeaterStateMachine, PreheatToWarmupAlreadyWarm)
{ {
MockHeater dut; MockHeater dut;
Timer::setMockTime(0);
dut.Configure(780, 300); dut.Configure(780, 300);
// Preheat for a little while // Preheat for a little while
@ -84,6 +90,7 @@ TEST(HeaterStateMachine, PreheatToWarmupAlreadyWarm)
TEST(HeaterStateMachine, WarmupToClosedLoop) TEST(HeaterStateMachine, WarmupToClosedLoop)
{ {
MockHeater dut; MockHeater dut;
Timer::setMockTime(0);
dut.Configure(780, 300); dut.Configure(780, 300);
// Warm up for a little while // Warm up for a little while
@ -99,17 +106,19 @@ TEST(HeaterStateMachine, WarmupToClosedLoop)
TEST(HeaterStateMachine, WarmupTimeout) TEST(HeaterStateMachine, WarmupTimeout)
{ {
MockHeater dut; MockHeater dut;
Timer::setMockTime(0);
dut.Configure(780, 300); dut.Configure(780, 300);
size_t timeoutPeriod = dut.GetTimeCounter(); // For a while it should stay in warmup
Timer::setMockTime(1e6);
// Warm up for a little while EXPECT_EQ(HeaterState::WarmupRamp, dut.GetNextState(HeaterState::WarmupRamp, HeaterAllow::Allowed, 12, 500));
for (size_t i = 0; i < timeoutPeriod - 1; i++) Timer::setMockTime(2e6);
{ EXPECT_EQ(HeaterState::WarmupRamp, dut.GetNextState(HeaterState::WarmupRamp, HeaterAllow::Allowed, 12, 500));
EXPECT_EQ(HeaterState::WarmupRamp, dut.GetNextState(HeaterState::WarmupRamp, HeaterAllow::Allowed, 12, 500)) << "i = " << i; Timer::setMockTime(9.9e6);
} EXPECT_EQ(HeaterState::WarmupRamp, dut.GetNextState(HeaterState::WarmupRamp, HeaterAllow::Allowed, 12, 500));
// Warmup times out, sensor transitions to stopped // Warmup times out, sensor transitions to stopped
Timer::setMockTime(10.1e6);
EXPECT_EQ(HeaterState::Stopped, dut.GetNextState(HeaterState::WarmupRamp, HeaterAllow::Allowed, 12, 500)); EXPECT_EQ(HeaterState::Stopped, dut.GetNextState(HeaterState::WarmupRamp, HeaterAllow::Allowed, 12, 500));
} }