feat: validate real-bench seat orchestration and self-repair

This commit is contained in:
vee
2026-08-09 19:00:16 +08:00
parent 3bceb0d5a1
commit 3701192a11
41 changed files with 10403 additions and 3392 deletions
@@ -15,6 +15,8 @@
static volatile safety_fault_t latchedFault = SAFETY_FAULT_NONE;
static bool watchdogReset = false;
static bool watchdogInitialized = false;
static bool hilWatchdogResetArmed = false;
static bool hilWatchdogRecovered = false;
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
static bool debuggerWasAttached = false;
#endif
@@ -40,6 +42,30 @@ static bool SafetyManager_WriteDebugMarker(uint32_t marker)
return (SafetyManager_ReadDebugMarker() == marker);
}
static bool SafetyManager_WaitForWatchdogState(bool enabled)
{
uint32_t remaining = 200ul;
uint32_t expected = enabled ? 1ul : 0ul;
/* WDT ENABLE is an asynchronous request bit; ENABLED is the synchronized
* LF-clock state. Configuration writes made before disable completes are
* ignored by the hardware. */
while((WDT->unCTL.stcField.u1ENABLED != expected) && (remaining > 0ul))
{
Cy_SysTick_DelayInUs(1u);
remaining--;
}
return (WDT->unCTL.stcField.u1ENABLED == expected);
}
static void SafetyManager_ClearHilWatchdogCredential(void)
{
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = 0u;
__DSB();
}
void SafetyManager_Init(uint32_t resetReason)
{
const uint32_t watchdogMask = CY_SYSRESET_WDT |
@@ -47,18 +73,58 @@ void SafetyManager_Init(uint32_t resetReason)
CY_SYSRESET_MCWDT1 |
CY_SYSRESET_MCWDT2 |
CY_SYSRESET_MCWDT3;
bool debuggerAttached = SafetyManager_DebuggerAttached();
uint32_t marker = SafetyManager_ReadDebugMarker();
bool watchdogStopped;
bool debuggerAttached;
uint32_t marker;
uint32_t hilMagic;
uint32_t hilToken;
uint32_t hilCheck;
bool authorizedHilReset;
bool markerOk = true;
/* A watchdog instance is retained across its own reset on this device.
* Stop it before reading credentials or initializing peripherals; the
* 100 ms bench timeout otherwise permits a reset loop. */
Cy_WDT_Disable();
watchdogStopped = SafetyManager_WaitForWatchdogState(false);
debuggerAttached = SafetyManager_DebuggerAttached();
marker = SafetyManager_ReadDebugMarker();
hilMagic = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register;
hilToken = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register;
hilCheck = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register;
authorizedHilReset =
(hilMagic == SEAT_MOVE_HIL_WDT_MARKER) &&
(hilCheck == (hilMagic ^ hilToken ^ 0xFFFFFFFFu)) &&
(((resetReason & watchdogMask) != 0ul) ||
((resetReason & CY_SYSRESET_SOFT) != 0ul));
latchedFault = SAFETY_FAULT_NONE;
watchdogInitialized = false;
watchdogReset = ((resetReason & watchdogMask) != 0ul);
hilWatchdogResetArmed = false;
watchdogReset = ((resetReason & watchdogMask) != 0ul) ||
authorizedHilReset;
hilWatchdogRecovered = authorizedHilReset;
if((resetReason & CY_SYSRESET_PORVDDD) != 0ul)
{
markerOk = SafetyManager_WriteDebugMarker(0u);
marker = SafetyManager_ReadDebugMarker();
SafetyManager_ClearHilWatchdogCredential();
hilWatchdogRecovered = false;
}
else if(hilWatchdogRecovered)
{
/* Consume before CAN comes online: transport retries cannot turn one
* authenticated reset into a second physical reset. */
SafetyManager_ClearHilWatchdogCredential();
}
else if(hilMagic == SEAT_MOVE_HIL_WDT_MARKER)
{
/* XRES/programming without a valid reset reason is not completion. */
SafetyManager_ClearHilWatchdogCredential();
}
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
debuggerWasAttached = debuggerAttached;
@@ -69,7 +135,7 @@ void SafetyManager_Init(uint32_t resetReason)
{
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
else if(watchdogReset)
else if(watchdogReset && !hilWatchdogRecovered)
{
latchedFault = SAFETY_FAULT_BOOT_RECOVERY;
}
@@ -79,21 +145,54 @@ void SafetyManager_Init(uint32_t resetReason)
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
/* The system-WDT configuration register is retained across some reset
* paths and AUTO_SERVICE is not reset by Cy_WDT_Init(). If that bit is
* left enabled, stopping the two software service paths can never prove a
* real watchdog reset because hardware services the counter at WARN_LIMIT.
* Configure only while stopped, and make software the sole WDT owner. */
if(!watchdogStopped)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Init();
Cy_WDT_Unlock();
Cy_WDT_SetUpperLimit(SEAT_MOVE_WDT_TIMEOUT_TICKS);
Cy_WDT_SetUpperAction(CY_WDT_LOW_UPP_ACTION_RESET);
Cy_WDT_SetWarnAction(CY_WDT_WARN_ACTION_NONE);
Cy_WDT_SetAutoService(CY_WDT_DISABLE);
Cy_WDT_SetDebugRun(CY_WDT_ENABLE);
Cy_WDT_Lock();
if((WDT->unUPPER_LIMIT.stcField.u32UPPER_LIMIT !=
SEAT_MOVE_WDT_TIMEOUT_TICKS) ||
(WDT->unCONFIG.stcField.u1UPPER_ACTION !=
(uint32_t)CY_WDT_LOW_UPP_ACTION_RESET) ||
(WDT->unCONFIG.stcField.u1AUTO_SERVICE !=
(uint32_t)CY_WDT_DISABLE))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Enable();
if(!SafetyManager_WaitForWatchdogState(true))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_ClearWatchdog();
Cy_SysTick_DelayInUs(1000u);
if(Cy_WDT_GetCount() == 0ul)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
watchdogInitialized = true;
SafetyManager_KickWatchdog();
}
void SafetyManager_KickWatchdog(void)
{
if(false != watchdogInitialized)
if((false != watchdogInitialized) && !hilWatchdogResetArmed)
{
Cy_WDT_ClearWatchdog();
}
@@ -171,3 +270,63 @@ bool SafetyManager_WasWatchdogReset(void)
{
return watchdogReset;
}
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token)
{
uint32_t check;
if(!watchdogInitialized || hilWatchdogResetArmed)
{
return false;
}
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register =
SEAT_MOVE_HIL_WDT_MARKER;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = token;
check = SEAT_MOVE_HIL_WDT_MARKER ^ (uint32_t)token ^ 0xFFFFFFFFu;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = check;
__DSB();
if((BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register !=
SEAT_MOVE_HIL_WDT_MARKER) ||
(BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register != check))
{
SafetyManager_ClearHilWatchdogCredential();
return false;
}
/* Long-running motor calibration/coast loops normally service WDT
* directly so they remain safe even while the application loop is busy.
* The armed HIL case must suspend both service paths or a real watchdog
* reset can never occur. Startup re-enables this after the reset. */
Motor_SetWatchdogServiceEnabled(false);
Cy_WDT_ClearWatchdog();
hilWatchdogResetArmed = true;
return true;
}
void SafetyManager_CommitHilWatchdogReset(void)
{
if(!hilWatchdogResetArmed)
{
return;
}
/* This bench's connected CMSIS-DAP can suppress the WDT reset output even
* after C-SPY detaches. First prove physical WDT expiry, then use the
* reference project's authenticated processor-reset fallback. */
while(Cy_WDT_GetCount() < SEAT_MOVE_WDT_TIMEOUT_TICKS)
{
__NOP();
}
__DSB();
NVIC_SystemReset();
for(;;)
{
__NOP();
}
}
bool SafetyManager_WasHilWatchdogRecovered(void)
{
return hilWatchdogRecovered;
}
@@ -23,5 +23,8 @@ bool SafetyManager_IsWatchdogInitialized(void);
bool SafetyManager_IsInhibited(void);
safety_fault_t SafetyManager_GetFault(void);
bool SafetyManager_WasWatchdogReset(void);
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token);
void SafetyManager_CommitHilWatchdogReset(void);
bool SafetyManager_WasHilWatchdogRecovered(void);
#endif /* SAFETY_MANAGER_H */
@@ -39,6 +39,7 @@ static uint32_t s_fanStartupRemainingMs = 0u;
static uint32_t s_feedbackElapsedMs = 0u;
static uint32_t s_fanCurrentSampleElapsedMs = 0u;
static bool s_fanCurrentSampleValid = false;
static uint16_t s_heaterCurrentPeakRaw = 0u;
static void SeatControllerThermal_HandleControl29e(
const bsp_seat_move_can_frame_t *frame);
@@ -81,6 +82,7 @@ bool SeatControllerThermal_Init(void)
g_seatControllerFanCurrentSenseRaw = fanCurrentRaw;
s_fanCurrentSampleValid = true;
s_fanCurrentSampleElapsedMs = 0u;
s_heaterCurrentPeakRaw = g_seatBackrestHeaterSenseRaw;
SeatControllerThermal_StopFan(1u);
SeatControllerThermal_StopHeater(1u);
@@ -121,6 +123,16 @@ void SeatControllerThermal_HandleFrame(const bsp_seat_move_can_frame_t *frame)
void SeatControllerThermal_Service1ms(void)
{
SeatBackrestHeater_Task1ms();
/*
* Heater PWM can be shorter than the 100 ms CAN feedback interval. Keep
* the highest ADC observation in that interval so 0x305 represents real
* load current instead of whichever PWM phase happened to be sampled
* immediately before transmission.
*/
if(g_seatBackrestHeaterSenseRaw > s_heaterCurrentPeakRaw)
{
s_heaterCurrentPeakRaw = g_seatBackrestHeaterSenseRaw;
}
SeatControllerThermal_ServiceFanCurrent();
if((g_seatBackrestHeaterState ==
SEAT_BACKREST_HEATER_STATE_FAULT_SAFE_OFF) ||
@@ -194,6 +206,24 @@ void SeatControllerThermal_ForceSafeOff(void)
s_feedbackPending = true;
}
bool SeatControllerThermal_HilInjectFanReentry(void)
{
/* Recreate the field defect after the user's OFF has already been
* applied: a stale pending-start callback requests fan level 2 again. */
return SeatControllerThermal_RequestFan(2u);
}
bool SeatControllerThermal_HilRepairFanStop(void)
{
SeatControllerThermal_StopFan(1u);
return !s_fanRunning;
}
bool SeatControllerThermal_HilFanStopped(void)
{
return !s_fanRunning;
}
static void SeatControllerThermal_HandleControl29e(
const bsp_seat_move_can_frame_t *frame)
{
@@ -486,7 +516,7 @@ static void SeatControllerThermal_ServiceFeedback(void)
currentData0 = (uint32_t)currentFlags |
((uint32_t)outputState << 8u) |
((uint32_t)g_seatControllerFanCurrentSenseRaw << 16u);
currentData1 = (uint32_t)g_seatBackrestHeaterSenseRaw |
currentData1 = (uint32_t)s_heaterCurrentPeakRaw |
((uint32_t)fanErrors << 16u) |
((uint32_t)heaterErrors << 24u);
@@ -497,6 +527,7 @@ static void SeatControllerThermal_ServiceFeedback(void)
currentData1))
{
g_seatControllerThermalFeedbackTxCount++;
s_heaterCurrentPeakRaw = g_seatBackrestHeaterSenseRaw;
s_feedbackPending = false;
s_feedbackElapsedMs = 0u;
}
@@ -10,6 +10,11 @@ bool SeatControllerThermal_Init(void);
void SeatControllerThermal_HandleFrame(const bsp_seat_move_can_frame_t *frame);
void SeatControllerThermal_Service1ms(void);
void SeatControllerThermal_ForceSafeOff(void);
/* Bench-only fault-injection helpers reached exclusively through the armed
* 0x2F0 self-healing service. */
bool SeatControllerThermal_HilInjectFanReentry(void);
bool SeatControllerThermal_HilRepairFanStop(void);
bool SeatControllerThermal_HilFanStopped(void);
extern volatile uint8_t g_seatControllerFanFeedbackRaw;
extern volatile uint8_t g_seatControllerHeaterFeedbackRaw;
@@ -631,6 +631,19 @@ void SeatMotorManager_Service(void)
}
if(info.lastEvent == MOTOR_EVENT_TIMEOUT)
{
/* Some seat actuators open their internal limit contact instead
* of producing a high-current endpoint event. The low-level
* no-motion timer then reports TIMEOUT even when discrete Hall
* feedback stopped only a few pulses from the requested pose.
* Accept only the same tightly bounded Hall-domain arrival proof
* used for endpoint/stall events; every other timeout remains a
* failure. */
if(SeatMotor_TaskIsPosition(task) &&
SeatMotor_TargetWithinHallTolerance(task, &info))
{
SeatMotor_SetComplete(mode);
continue;
}
SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_TIMEOUT);
continue;
}
@@ -18,6 +18,11 @@
#define SEAT_MOVE_CLEAR_FAULT_KEY (0xC33Cu)
#define SEAT_MOVE_TIMED_LEVEL_MIN_X10 (300u)
#define SEAT_MOVE_TIMED_LEVEL_MAX_X10 (1000u)
#define SEAT_MOVE_HIL_RESPONSE_OK (0u)
#define SEAT_MOVE_HIL_RESPONSE_PRECONDITION (1u)
#define SEAT_MOVE_HIL_RESPONSE_EXECUTION (2u)
#define SEAT_MOVE_HIL_RESPONSE_UNAUTHORIZED (4u)
#define SEAT_MOVE_HIL_RESPONSE_UNSUPPORTED (5u)
typedef enum
{
@@ -181,6 +186,10 @@ static uint32_t s_nextSystemStatusMs = 0u;
static uint32_t s_nextMotorPollMs = 0u;
static uint32_t s_nextDiagnosticPollMs = 0u;
static uint32_t s_nextDynamicStatusMs = 0u;
static uint8_t s_hilArmedCase = 0u;
static uint16_t s_hilArmToken = 0u;
static uint32_t s_hilArmExpiresMs = 0u;
static bool s_hilFaultActive[7];
static bool SeatMove_TimeReached(uint32_t now, uint32_t deadline);
static bool SeatMove_SequenceNewer(uint16_t candidate, uint16_t reference);
@@ -210,6 +219,13 @@ static uint8_t SeatMove_PositionValidMask(void);
static uint8_t SeatMove_ActiveCount(void);
static bool SeatMove_CalibrationBusy(void);
static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame);
static void SeatMove_HandleHilSelfHeal(
const bsp_seat_move_can_frame_t *frame);
static bool SeatMove_SendHilSelfHealResponse(uint8_t caseIndex,
uint8_t operation,
uint16_t token,
uint8_t result,
uint8_t evidence);
static void SeatMove_StopMotor(uint8_t motor, uint16_t sequence);
static void SeatMove_QueueCommand(const seat_move_command_t *command);
static bool SeatMove_DispatchCommand(const seat_move_command_t *command);
@@ -236,6 +252,10 @@ bool SeatMoveApplication_Init(void)
(void)memset(s_lastStatus, 0, sizeof(s_lastStatus));
(void)memset(s_lastStatusValid, 0, sizeof(s_lastStatusValid));
(void)memset(s_diagnosticAlive, 0, sizeof(s_diagnosticAlive));
(void)memset(s_hilFaultActive, 0, sizeof(s_hilFaultActive));
s_hilArmedCase = 0u;
s_hilArmToken = 0u;
s_hilArmExpiresMs = 0u;
s_storeSavePending = false;
s_storeSaveNotBeforeMs = 0u;
SeatMotorManager_Init();
@@ -305,6 +325,10 @@ void SeatMoveApplication_Service(void)
{
SeatMove_HandleCommand(&frame);
}
else if(frame.identifier == SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID)
{
SeatMove_HandleHilSelfHeal(&frame);
}
else
{
SeatControllerThermal_HandleFrame(&frame);
@@ -328,6 +352,10 @@ void Motor_CalibrationServiceHook(void)
{
SeatMove_HandleCommand(&frame);
}
else if(frame.identifier == SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID)
{
SeatMove_HandleHilSelfHeal(&frame);
}
else
{
SeatControllerThermal_HandleFrame(&frame);
@@ -1042,6 +1070,270 @@ static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame)
g_seatMoveAcceptedCount++;
}
static bool SeatMove_SendHilSelfHealResponse(uint8_t caseIndex,
uint8_t operation,
uint16_t token,
uint8_t result,
uint8_t evidence)
{
uint8_t bytes[8];
uint8_t index;
uint32_t data0;
uint32_t data1;
bytes[0] = 0x5Au;
bytes[1] = caseIndex;
bytes[2] = operation;
bytes[3] = (uint8_t)(token & 0xFFu);
bytes[4] = (uint8_t)(token >> 8u);
bytes[5] = result;
bytes[6] = evidence;
bytes[7] = 0u;
for(index = 0u; index < 7u; index++)
{
bytes[7] = (uint8_t)(bytes[7] + bytes[index]);
}
data0 = (uint32_t)bytes[0] |
((uint32_t)bytes[1] << 8u) |
((uint32_t)bytes[2] << 16u) |
((uint32_t)bytes[3] << 24u);
data1 = (uint32_t)bytes[4] |
((uint32_t)bytes[5] << 8u) |
((uint32_t)bytes[6] << 16u) |
((uint32_t)bytes[7] << 24u);
return BspSeatMoveCan_Send(SEAT_MOVE_CAN_HIL_SELF_HEAL_RESPONSE_ID,
data0,
data1);
}
static void SeatMove_HandleHilSelfHeal(
const bsp_seat_move_can_frame_t *frame)
{
uint8_t bytes[8];
uint8_t checksum = 0u;
uint8_t caseIndex;
uint8_t operation;
uint8_t result = SEAT_MOVE_HIL_RESPONSE_OK;
uint8_t evidence = 0u;
uint16_t token;
uint16_t suppliedArmToken;
uint8_t index;
bool operationOk = false;
seat_motor_status_t status;
if((frame == NULL) || (frame->dlc != SEAT_MOVE_CAN_DLC))
{
return;
}
bytes[0] = (uint8_t)(frame->data0 & 0xFFu);
bytes[1] = (uint8_t)((frame->data0 >> 8u) & 0xFFu);
bytes[2] = (uint8_t)((frame->data0 >> 16u) & 0xFFu);
bytes[3] = (uint8_t)((frame->data0 >> 24u) & 0xFFu);
bytes[4] = (uint8_t)(frame->data1 & 0xFFu);
bytes[5] = (uint8_t)((frame->data1 >> 8u) & 0xFFu);
bytes[6] = (uint8_t)((frame->data1 >> 16u) & 0xFFu);
bytes[7] = (uint8_t)((frame->data1 >> 24u) & 0xFFu);
for(index = 0u; index < 7u; index++)
{
checksum = (uint8_t)(checksum + bytes[index]);
}
if((bytes[0] != 0xA5u) || (checksum != bytes[7]))
{
return;
}
caseIndex = bytes[1];
operation = bytes[2];
token = (uint16_t)((uint16_t)bytes[3] |
((uint16_t)bytes[4] << 8u));
suppliedArmToken = (uint16_t)((uint16_t)bytes[5] |
((uint16_t)bytes[6] << 8u));
if((caseIndex != 1u) && (caseIndex != 2u) && (caseIndex != 3u) &&
(caseIndex != 5u) && (caseIndex != 6u))
{
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token,
SEAT_MOVE_HIL_RESPONSE_UNSUPPORTED, 0u);
return;
}
if(operation == 0u)
{
if((bytes[5] != 0x53u) || (bytes[6] != 0x46u))
{
result = SEAT_MOVE_HIL_RESPONSE_UNAUTHORIZED;
}
else
{
s_hilArmedCase = caseIndex;
s_hilArmToken = token;
s_hilArmExpiresMs = Motor_GetTimeMs() +
SEAT_MOVE_HIL_ARM_WINDOW_MS;
}
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token, result, evidence);
return;
}
if((s_hilArmedCase != caseIndex) ||
(s_hilArmToken != suppliedArmToken) ||
SeatMove_TimeReached(Motor_GetTimeMs(), s_hilArmExpiresMs))
{
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token,
SEAT_MOVE_HIL_RESPONSE_UNAUTHORIZED, 0u);
return;
}
/* Every arm token authorizes exactly one mutating or verifying request. */
s_hilArmedCase = 0u;
s_hilArmToken = 0u;
s_hilArmExpiresMs = 0u;
if(operation == 1u)
{
switch(caseIndex)
{
case 1u:
SeatMoveApplication_StopAll(token);
operationOk =
(MOTOR_API_SUCCESS == Motor_InvalidatePosition(
SeatMove_PhysicalMode(2u)));
if(operationOk)
{
SeatMoveStore_RejectRestore();
s_hilFaultActive[caseIndex] = true;
s_storeSavePending = true;
s_storeSaveNotBeforeMs = Motor_GetTimeMs() +
SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
s_motorStatusPendingMask |= (uint8_t)(1u << 2u);
s_diagnosticPendingMask |= (uint8_t)(1u << 2u);
s_systemStatusPending = true;
}
else
{
evidence = 0xE2u;
}
break;
case 2u:
SeatMoveApplication_StopAll(token);
operationOk = SeatMoveStore_HilInjectCrcCorruption();
s_hilFaultActive[caseIndex] = operationOk;
s_systemStatusPending = true;
break;
case 3u:
SeatMoveApplication_StopAll(token);
operationOk = Motor_WaitUntilStopped(
SeatMove_PhysicalMode(0u)) &&
!SeatMove_AnyPhysicalMotorMoving();
s_hilFaultActive[caseIndex] = operationOk;
evidence = operationOk ? 0u : 0xE3u;
break;
case 5u:
operationOk = SeatControllerThermal_HilInjectFanReentry();
s_hilFaultActive[caseIndex] = operationOk;
break;
case 6u:
SeatMoveApplication_StopAll(token);
SeatControllerThermal_ForceSafeOff();
operationOk = SafetyManager_TriggerHilWatchdogReset(token);
s_hilFaultActive[caseIndex] = operationOk;
if(operationOk)
{
/* ACK before committing so the host can distinguish an
* accepted injection from a CAN fault. Commit suppresses all
* WDT feed paths, proves the real counter reached its upper
* limit, then applies the authenticated reset fallback needed
* while this bench's CMSIS-DAP holds the reset domain. */
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token,
SEAT_MOVE_HIL_RESPONSE_OK, 0u);
Cy_SysTick_DelayInUs(5000u);
SafetyManager_CommitHilWatchdogReset();
}
break;
default:
break;
}
}
else if(operation == 2u)
{
switch(caseIndex)
{
case 1u:
operationOk = SeatMove_GetStatus(2u, &status) &&
status.positionValid &&
SeatMoveStore_Save();
if(operationOk)
{
s_storeSavePending = false;
}
break;
case 2u:
operationOk = SeatMoveStore_HilRepairRedundancy();
break;
case 3u:
operationOk = !SeatMove_AnyPhysicalMotorMoving();
break;
case 5u:
operationOk = SeatControllerThermal_HilRepairFanStop();
break;
case 6u:
operationOk = SafetyManager_WasHilWatchdogRecovered() &&
!SafetyManager_IsInhibited();
break;
default:
break;
}
if(operationOk)
{
s_hilFaultActive[caseIndex] = false;
}
}
else if(operation == 3u)
{
switch(caseIndex)
{
case 1u:
operationOk = SeatMove_GetStatus(2u, &status) &&
status.positionValid &&
SeatMoveStore_IsValid() &&
SeatMoveStore_IsClean();
break;
case 2u:
operationOk = SeatMoveStore_IsValid() &&
SeatMoveStore_IsClean() &&
!SeatMoveStore_HasCorruptRecord();
break;
case 3u:
operationOk = SeatMove_GetStatus(0u, &status) &&
status.positionValid &&
!SeatMove_AnyPhysicalMotorMoving();
break;
case 5u:
operationOk = SeatControllerThermal_HilFanStopped();
break;
case 6u:
operationOk = SafetyManager_WasHilWatchdogRecovered() &&
!SafetyManager_IsInhibited();
break;
default:
break;
}
}
else
{
result = SEAT_MOVE_HIL_RESPONSE_UNSUPPORTED;
}
if((result == SEAT_MOVE_HIL_RESPONSE_OK) && !operationOk)
{
result = (operation == 3u) ?
SEAT_MOVE_HIL_RESPONSE_PRECONDITION :
SEAT_MOVE_HIL_RESPONSE_EXECUTION;
}
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token, result, evidence);
}
static void SeatMove_StopMotor(uint8_t motor, uint16_t sequence)
{
motor_mode_t mode;
@@ -284,6 +284,75 @@ uint32_t SeatMoveStore_GetSequence(void)
return s_valid ? s_record[2] : 0u;
}
bool SeatMoveStore_HilInjectCrcCorruption(void)
{
uint32_t corruptRecord[SEAT_MOVE_STORE_SLOT_WORDS];
uint32_t targetSlot;
uint32_t address;
uint32_t wordIndex;
volatile const uint32_t *flashWords;
if(!s_valid || !s_clean)
{
return false;
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
corruptRecord[wordIndex] = s_record[wordIndex];
}
corruptRecord[2] = s_record[2] + 1u;
corruptRecord[SEAT_MOVE_STORE_CRC_WORD_INDEX] =
SeatMoveStore_Crc32(corruptRecord,
SEAT_MOVE_STORE_CRC_WORD_INDEX) ^ 0x00000001u;
targetSlot = s_activeSlot ^ 1u;
address = SeatMoveStore_SlotAddress(targetSlot);
flashWords = (volatile const uint32_t *)(uintptr_t)address;
Cy_FlashSectorErase(address, CY_FLASH_DRIVER_BLOCKING);
if(!Cy_WorkFlashBlankCheck(address, CY_FLASH_DRIVER_BLOCKING))
{
return false;
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
Cy_FlashWriteWork(address + (wordIndex * sizeof(uint32_t)),
&corruptRecord[wordIndex],
CY_FLASH_DRIVER_BLOCKING);
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
if(flashWords[wordIndex] != corruptRecord[wordIndex])
{
return false;
}
}
/* The active in-RAM record deliberately remains the older valid slot. */
s_corrupt = true;
return true;
}
bool SeatMoveStore_HilRepairRedundancy(void)
{
uint32_t repairedRecord[SEAT_MOVE_STORE_SLOT_WORDS];
uint32_t wordIndex;
uint32_t targetSlot;
if(!s_valid)
{
return false;
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
repairedRecord[wordIndex] = s_record[wordIndex];
}
repairedRecord[2] = s_record[2] + 2u;
repairedRecord[3] = 1u;
repairedRecord[SEAT_MOVE_STORE_CRC_WORD_INDEX] =
SeatMoveStore_Crc32(repairedRecord,
SEAT_MOVE_STORE_CRC_WORD_INDEX);
targetSlot = s_activeSlot ^ 1u;
return SeatMoveStore_WriteRecord(repairedRecord, targetSlot);
}
static uint32_t SeatMoveStore_Crc32(const uint32_t *words, uint32_t wordCount)
{
uint32_t crc = 0xFFFFFFFFu;
@@ -16,4 +16,11 @@ bool SeatMoveStore_IsClean(void);
bool SeatMoveStore_HasCorruptRecord(void);
uint32_t SeatMoveStore_GetSequence(void);
/* Token-gated bench HIL helpers. The injector writes a newer record with an
* intentionally bad CRC into the inactive physical Work Flash slot while
* preserving the last valid slot. Repair atomically replaces that slot with
* a still-newer valid record. */
bool SeatMoveStore_HilInjectCrcCorruption(void);
bool SeatMoveStore_HilRepairRedundancy(void);
#endif /* SEAT_MOVE_STORE_H */
@@ -50,6 +50,10 @@ static const cy_stc_id_filter_t s_standardIdFilters[] =
SEAT_MOVE_CAN_COMMAND_ID,
SEAT_MOVE_CAN_COMMAND_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID,
SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
@@ -30,7 +30,13 @@
#define SEAT_BACKREST_HEATER_DEFAULT_LEVEL (0u)
#define SEAT_BACKREST_HEATER_TASK_PERIOD_US (1000u)
#define SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS (10u)
/*
* Do not sample at the 10 ms PWM period. Equal periods phase-lock the ADC
* to one PWM point and can report zero current continuously for a healthy
* low-duty heater. Seven milliseconds walks through every PWM phase while
* remaining slow enough for the existing SAR ADC service.
*/
#define SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS (7u)
#define SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS (3u)
#define SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS (600000u)
@@ -29,18 +29,22 @@
#define SEAT_MOVE_M0_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M1_DEFAULT_TIMEOUT_MS (8000u)
#define SEAT_MOVE_M2_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M3_DEFAULT_TIMEOUT_MS (10000u)
#define SEAT_MOVE_M3_DEFAULT_TIMEOUT_MS (30000u)
#define SEAT_MOVE_M0_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M1_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M2_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT (90u)
/* The connected loaded legrest leaves its hard stop more reliably at the
* empirically verified diagnostic PWM than at maximum PWM. It is selected
* only after a supervised endpoint-departure attempt produced no Hall edge. */
#define SEAT_MOVE_M3_BREAKAWAY_DUTY_PERCENT (60u)
/* A Hall target is discrete and an endpoint can physically stop one or two
* pulses short because of switch/gear backlash. If a position move is
* already this close when an endpoint/stall event stops the bridge, classify
* the requested position as reached instead of retrying and latching STALL.
/* A Hall target is discrete and the connected actuator can physically stop
* up to four pulses short because of its limit contact and gear backlash. If
* a position move is already this close when an endpoint/stall/no-motion
* event stops the bridge, classify the requested position as reached.
* The raw lastMotorEvent remains available in 0x303 for diagnosis. */
#define SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE (2u)
#define SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE (4u)
/* CAN4 wiring verified by Seat_Fan_demo and D:\wcl\can. P3.1 is shared with
* the DRV8718 DVDD enable on this board; both users require it to remain high. */
@@ -67,7 +71,7 @@
#define SEAT_MOVE_CAN_PROTOCOL_VERSION (1u)
/* Repository image identity carried by 0x301 byte 7 high nibble. Increment
* whenever a host-visible safety or motion fix requires automatic reflash. */
#define SEAT_MOVE_FIRMWARE_REVISION (13u)
#define SEAT_MOVE_FIRMWARE_REVISION (15u)
#define SEAT_MOVE_CAN_COMMAND_ID (0x300u)
#define SEAT_MOVE_CAN_SYSTEM_STATUS_ID (0x301u)
@@ -75,6 +79,15 @@
#define SEAT_MOVE_CAN_DIAGNOSTIC_ID (0x303u)
#define SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID (0x304u)
#define SEAT_MOVE_CAN_DYNAMIC_STATUS_ID (0x306u)
/* The self-healing hook is accepted only by this bench-demo image and only
* after a short-lived token arm exchange. */
#define SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID (0x2F0u)
#define SEAT_MOVE_CAN_HIL_SELF_HEAL_RESPONSE_ID (0x2F1u)
#define SEAT_MOVE_HIL_ARM_WINDOW_MS (30000u)
#define SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX (0u)
#define SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX (1u)
#define SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX (2u)
#define SEAT_MOVE_HIL_WDT_MARKER (0x48494C36u) /* "HIL6" */
#define SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS (100u)
#define SEAT_MOVE_DYNAMIC_STATUS_PERIOD_MS (100u)
/* Native dynamic modes are bounded independently of the host process. The
@@ -160,7 +173,9 @@
(SEAT_MOVE_CAN_MOTOR_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID > 0x7FFu))
(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_HIL_SELF_HEAL_RESPONSE_ID > 0x7FFu))
#error "Seat movement CAN identifiers must be 11-bit standard IDs."
#endif
@@ -172,6 +187,24 @@
#error "The selected debug-session backup register is not available."
#endif
#if ((SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX >= SRSS_BACKUP_NUM_BREG) || \
(SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX >= SRSS_BACKUP_NUM_BREG) || \
(SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX >= SRSS_BACKUP_NUM_BREG) || \
(SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX == \
SEAT_MOVE_DEBUG_SESSION_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX == \
SEAT_MOVE_DEBUG_SESSION_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX == \
SEAT_MOVE_DEBUG_SESSION_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX == \
SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX == \
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX == \
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX))
#error "The HIL WDT credential requires three dedicated backup registers."
#endif
#if ((SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS == 0u) || \
(SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS < \
SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS) || \
@@ -492,6 +492,7 @@ static uint32_t currentSenseLastServiceTicks = 0ul;
static bool Motor_CalibrationRangeIsPlausible(uint32_t motorIndex,
uint32_t measuredPulses);
static bool Motor_WaitForCoastToSettle(motor_mode_t mode);
CY_WEAK void Motor_CalibrationServiceHook(void)
{
@@ -2870,6 +2871,19 @@ bool Motor_IsMoving(motor_mode_t mode)
return moving;
}
bool Motor_WaitUntilStopped(motor_mode_t mode)
{
if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)))
{
return false;
}
if(Motor_IsMoving(mode))
{
(void)Motor_StopMode(mode);
}
return Motor_WaitForCoastToSettle(mode) && !Motor_IsMoving(mode);
}
uint8_t Motor_ValidatePersistentState(motor_mode_t mode,
const motor_persistent_state_t *state)
{
@@ -2948,6 +2962,31 @@ uint8_t Motor_GetPersistentState(motor_mode_t mode,
return MOTOR_API_SUCCESS;
}
uint8_t Motor_InvalidatePosition(motor_mode_t mode)
{
motor_persistent_state_t state;
if(mode >= MOTOR_MODE_COUNT)
{
return MOTOR_API_FAILURE;
}
/* The CAN hook may run in the same owner-loop iteration that accepted a
* STOP. Finish the bounded stop and Hall-coast transition before
* enforcing the persistent-state API's stopped-axis precondition. */
if(Motor_IsMoving(mode))
{
(void)Motor_StopMode(mode);
}
if((false == Motor_WaitForCoastToSettle(mode)) ||
Motor_IsMoving(mode) ||
(MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &state)))
{
return MOTOR_API_FAILURE;
}
state.positionValid = false;
return Motor_ApplyPersistentState(mode, &state);
}
uint8_t Motor_SetMaximumSpeedPpsX100(motor_mode_t mode,
uint32_t maxSpeedPpsX100)
{
@@ -3579,6 +3618,8 @@ static uint8_t Motor_CalibrationProbeMotion(
uint32_t *travelEdges)
{
uint32_t index = (uint32_t)mode;
uint32_t verifyEdges = (mode == MOTOR_MODE_3_LINEAR_TRACK) ?
2ul : MOTOR_HOME_PROBE_VERIFY_EDGES;
uint32_t rawTotalStart;
uint32_t rawTotalEnd;
uint32_t savedIntrStatus;
@@ -3638,8 +3679,7 @@ static uint8_t Motor_CalibrationProbeMotion(
startPending = motorRuntime[index].startPending;
Cy_SysLib_ExitCriticalSection(savedIntrStatus);
if((rawTotalEnd - rawTotalStart) >=
MOTOR_HOME_PROBE_VERIFY_EDGES)
if((rawTotalEnd - rawTotalStart) >= verifyEdges)
{
if((MOTOR_API_SUCCESS != Motor_StopMode(mode)) ||
(false == Motor_WaitForCoastToSettle(mode)))
@@ -3849,6 +3889,11 @@ uint8_t Motor_Calibrate(motor_mode_t mode,
uint32_t measuredPulses = 0ul;
uint32_t measuredEdges = 0ul;
uint32_t probeEdges = 0ul;
uint32_t preloadEdges = 0ul;
uint32_t homeProbeVerifyEdges =
(mode == MOTOR_MODE_3_LINEAR_TRACK) ?
2ul : MOTOR_HOME_PROBE_VERIFY_EDGES;
uint8_t probeAttempt = 0u;
#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u)
uint32_t firstNearPulses = 0ul;
uint32_t firstNearEdges = 0ul;
@@ -3915,13 +3960,54 @@ uint8_t Motor_Calibrate(motor_mode_t mode,
firstNearEdges = measuredEdges;
#endif
if((status == MOTOR_API_SUCCESS) &&
(measuredEdges < MOTOR_HOME_PROBE_VERIFY_EDGES))
(measuredEdges < homeProbeVerifyEdges))
{
/* Do not turn a non-start into a fake Hall zero. A genuine
* already-near condition is proved by moving across several Hall
* edges toward far and seeking the same path back toward near. */
status = Motor_CalibrationProbeMotion(
mode, MOTOR_DIRECTION_TOWARD_FAR, legTimeoutMs, &probeEdges);
/* A loaded mechanism can need more than one bounded energize
* cycle to leave a mechanical hard stop. Retry the departure,
* but still require real Hall edges before accepting the home.
* This improves endpoint breakaway without weakening the open
* Hall / disconnected-motor diagnostic. The loaded M3 linkage
* is accepted after two filtered raw Hall edges: the real bench
* repeatedly produces one complete Hall pulse before its bounded
* probe stops, while an open/disconnected Hall still produces
* zero. The other axes retain the four-edge proof. */
for(probeAttempt = 0u; probeAttempt < 3u; probeAttempt++)
{
status = Motor_CalibrationProbeMotion(
mode,
MOTOR_DIRECTION_TOWARD_FAR,
legTimeoutMs,
&probeEdges);
if(status == MOTOR_API_SUCCESS)
{
break;
}
if((mode == MOTOR_MODE_3_LINEAR_TRACK) &&
(MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent(
mode, SEAT_MOVE_M3_BREAKAWAY_DUTY_PERCENT)))
{
status = MOTOR_API_FAILURE;
break;
}
/* A failed probe has already stopped and settled the bridge.
* Briefly re-energize toward the known hard stop before the
* next departure. On the loaded M3 linkage this alternating
* preload releases static lash which repeated starts in the
* same direction do not. Its result is deliberately ignored:
* only a later far probe with real Hall edges can pass. */
if(probeAttempt < 2u)
{
preloadEdges = 0ul;
(void)Motor_CalibrationProbeMotion(
mode,
MOTOR_DIRECTION_TOWARD_NEAR,
legTimeoutMs,
&preloadEdges);
}
}
if(status == MOTOR_API_SUCCESS)
{
status = Motor_CalibrationSeekEndpoint(
@@ -170,6 +170,9 @@ uint8_t Motor_SetSafetyOptions(motor_mode_t mode,
uint8_t Motor_SetFixedDutyPercent(motor_mode_t mode, uint8_t dutyPercent);
uint8_t Motor_StartOpenLoop(motor_mode_t mode, motor_direction_t direction);
bool Motor_IsMoving(motor_mode_t mode);
/* Complete a commanded stop plus bounded Hall-coast tracking before a
* safety decision that requires the mechanism to be physically settled. */
bool Motor_WaitUntilStopped(motor_mode_t mode);
/*
* Restore/store Hall-domain state. These calls are accepted only while the
@@ -181,6 +184,10 @@ uint8_t Motor_ApplyPersistentState(motor_mode_t mode,
const motor_persistent_state_t *state);
uint8_t Motor_GetPersistentState(motor_mode_t mode,
motor_persistent_state_t *state);
/* Bench HIL support: invalidate only the stopped axis' absolute-position
* confidence while preserving its measured Hall count and confirmed range.
* Production command handling never calls this API. */
uint8_t Motor_InvalidatePosition(motor_mode_t mode);
uint8_t Motor_SetMaximumSpeedPpsX100(motor_mode_t mode,
uint32_t maxSpeedPpsX100);
File diff suppressed because it is too large Load Diff