feat: integrate cabin CAN and verified dynamic seat modes

This commit is contained in:
vee
2026-08-04 17:51:38 +08:00
parent 155fbb59ba
commit dc7c91800e
99 changed files with 51897 additions and 3618 deletions
@@ -0,0 +1,679 @@
#include "seat_dynamic_mode.h"
#include <stddef.h>
#include <string.h>
#include "motor_control.h"
#include "seat_dynamic_mode_config.h"
#include "seat_motor_manager.h"
#include "seat_move_config.h"
#define SEAT_DYNAMIC_TARGET_TOLERANCE_PULSES (3u)
#define SEAT_DYNAMIC_ENTRY_TIMEOUT_MS (30000u)
#define SEAT_DYNAMIC_ENTRY_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_CRADLE_PHASE_TIMEOUT_MS (30000u)
#define SEAT_DYNAMIC_CRADLE_RENDEZVOUS_PULSES (12u)
#define SEAT_DYNAMIC_CRADLE_FINAL_DUTY_PERCENT (50u)
typedef enum
{
SEAT_DYNAMIC_CONTROL_TIMED_ARRIVAL = 0u,
SEAT_DYNAMIC_CONTROL_HOLD,
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
} seat_dynamic_control_t;
typedef struct
{
uint8_t phase;
uint32_t durationMs;
uint32_t timeoutMs;
uint16_t targetHall[SEAT_MOVE_MOTOR_COUNT];
seat_dynamic_control_t control;
} seat_dynamic_phase_definition_t;
typedef struct
{
uint8_t modeId;
const uint16_t *entryTargetHall;
const seat_dynamic_phase_definition_t *phases;
uint8_t phaseCount;
const uint8_t *cruiseDutyPercent;
uint16_t rendezvousPulses;
uint8_t finalDutyPercent;
} seat_dynamic_mode_definition_t;
typedef struct
{
uint8_t modeId;
uint8_t state;
uint8_t phase;
uint8_t phaseIndex;
uint8_t faultMask;
uint16_t cycleIndex;
uint16_t internalSequence;
uint32_t modeStartedMs;
uint32_t phaseStartedMs;
uint32_t phaseDurationMs;
uint8_t rendezvousStage;
uint16_t targetHall[SEAT_MOVE_MOTOR_COUNT];
uint16_t stagingTargetHall[SEAT_MOVE_MOTOR_COUNT];
uint16_t finalTargetHall[SEAT_MOVE_MOTOR_COUNT];
} seat_dynamic_context_t;
static const motor_mode_t s_dynamicLogicalToPhysical[SEAT_MOVE_MOTOR_COUNT] =
{
(motor_mode_t)SEAT_MOVE_LOGICAL_M0_PHYSICAL_MODE,
(motor_mode_t)SEAT_MOVE_LOGICAL_M1_PHYSICAL_MODE,
(motor_mode_t)SEAT_MOVE_LOGICAL_M2_PHYSICAL_MODE,
(motor_mode_t)SEAT_MOVE_LOGICAL_M3_PHYSICAL_MODE
};
/* A cradle must feel like one coordinated body movement. All four axes move
* toward their staging poses in the same scheduler pass; no axis is held back
* for a late catch-up. M0/M1/M3 use the proven breakaway duty while M2 runs
* at loaded duty. After every axis reaches a point exactly 12 Hall pulses
* before the requested pose, they execute an equal-distance 50% final
* approach. This keeps both the visible start and final arrival coordinated
* while making completion independent of an arbitrary 30-second timer. */
static const uint8_t s_cradleCruiseDuty[SEAT_MOVE_MOTOR_COUNT] =
{
50u, 50u, 90u, 50u
};
static const uint8_t s_horseCruiseDuty[SEAT_MOVE_MOTOR_COUNT] =
{
SEAT_DYNAMIC_HORSE_M0_DUTY_PERCENT,
SEAT_DYNAMIC_HORSE_M1_DUTY_PERCENT,
SEAT_DYNAMIC_HORSE_M2_DUTY_PERCENT,
SEAT_DYNAMIC_HORSE_M3_DUTY_PERCENT
};
/* Targets are the N7 Hall coordinates supplied for the connected seat bench.
* They are intentionally stored as pulses rather than rounded percentages. */
static const uint16_t s_rockingHorseEntry[SEAT_MOVE_MOTOR_COUNT] =
{
SEAT_DYNAMIC_HORSE_ENTRY_M0_HALL,
SEAT_DYNAMIC_HORSE_ENTRY_M1_HALL,
SEAT_DYNAMIC_HORSE_ENTRY_M2_HALL,
SEAT_DYNAMIC_HORSE_ENTRY_M3_HALL
};
static const seat_dynamic_phase_definition_t s_rockingHorsePhases[] =
{
{
SEAT_DYNAMIC_PHASE_HORSE_FORWARD,
0u,
SEAT_DYNAMIC_HORSE_PHASE_TIMEOUT_MS,
{
SEAT_DYNAMIC_HORSE_FORWARD_M0_HALL,
SEAT_DYNAMIC_HORSE_FORWARD_M1_HALL,
SEAT_DYNAMIC_HORSE_FORWARD_M2_HALL,
SEAT_DYNAMIC_HORSE_FORWARD_M3_HALL
},
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
},
{
SEAT_DYNAMIC_PHASE_HORSE_BACKWARD,
0u,
SEAT_DYNAMIC_HORSE_PHASE_TIMEOUT_MS,
{
SEAT_DYNAMIC_HORSE_BACKWARD_M0_HALL,
SEAT_DYNAMIC_HORSE_BACKWARD_M1_HALL,
SEAT_DYNAMIC_HORSE_BACKWARD_M2_HALL,
SEAT_DYNAMIC_HORSE_BACKWARD_M3_HALL
},
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
}
};
static const uint16_t s_cradleEntry[SEAT_MOVE_MOTOR_COUNT] =
{
391u, 140u, 405u, 404u
};
static const seat_dynamic_phase_definition_t s_cradlePhases[] =
{
{
SEAT_DYNAMIC_PHASE_CRADLE_FORWARD,
0u,
SEAT_DYNAMIC_CRADLE_PHASE_TIMEOUT_MS,
{ 246u, 281u, 701u, 494u },
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
},
{
SEAT_DYNAMIC_PHASE_CRADLE_RETURN,
0u,
SEAT_DYNAMIC_CRADLE_PHASE_TIMEOUT_MS,
{ 391u, 140u, 405u, 404u },
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
}
};
static const seat_dynamic_mode_definition_t s_dynamicModes[] =
{
{
SEAT_DYNAMIC_MODE_ROCKING_HORSE,
s_rockingHorseEntry,
s_rockingHorsePhases,
(uint8_t)(sizeof(s_rockingHorsePhases) /
sizeof(s_rockingHorsePhases[0])),
s_horseCruiseDuty,
SEAT_DYNAMIC_HORSE_RENDEZVOUS_PULSES,
SEAT_DYNAMIC_HORSE_FINAL_DUTY_PERCENT
},
{
SEAT_DYNAMIC_MODE_CRADLE,
s_cradleEntry,
s_cradlePhases,
(uint8_t)(sizeof(s_cradlePhases) / sizeof(s_cradlePhases[0])),
s_cradleCruiseDuty,
SEAT_DYNAMIC_CRADLE_RENDEZVOUS_PULSES,
SEAT_DYNAMIC_CRADLE_FINAL_DUTY_PERCENT
}
};
static seat_dynamic_context_t s_dynamic;
static bool SeatDynamic_TimeReached(uint32_t now, uint32_t deadline);
static uint32_t SeatDynamic_ElapsedMs(uint32_t now, uint32_t started);
static const seat_dynamic_mode_definition_t *SeatDynamic_Definition(
uint8_t modeId);
static uint8_t SeatDynamic_ActiveMask(void);
static bool SeatDynamic_StatusFailed(const seat_motor_status_t *status);
static bool SeatDynamic_TargetsReached(bool *failed);
static bool SeatDynamic_TargetsWithinTolerance(void);
static bool SeatDynamic_StartEntry(
const seat_dynamic_mode_definition_t *definition,
uint32_t now);
static bool SeatDynamic_StartPhase(
const seat_dynamic_mode_definition_t *definition,
uint8_t phaseIndex,
uint32_t now);
static bool SeatDynamic_StartRendezvousFinal(
const seat_dynamic_mode_definition_t *definition);
static void SeatDynamic_Fail(void);
static void SeatDynamic_Advance(
const seat_dynamic_mode_definition_t *definition,
uint32_t now);
void SeatDynamicMode_Init(void)
{
(void)memset(&s_dynamic, 0, sizeof(s_dynamic));
s_dynamic.state = SEAT_DYNAMIC_STATE_IDLE;
}
bool SeatDynamicMode_Start(uint8_t modeId, uint16_t commandSequence)
{
const seat_dynamic_mode_definition_t *definition =
SeatDynamic_Definition(modeId);
uint8_t motor;
if((definition == NULL) || SeatDynamicMode_IsActive() ||
SeatMotorManager_IsAnyActive())
{
return false;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status) ||
!status.positionValid ||
(definition->entryTargetHall[motor] > status.maxHallCount))
{
return false;
}
}
(void)memset(&s_dynamic, 0, sizeof(s_dynamic));
s_dynamic.modeId = modeId;
s_dynamic.state = SEAT_DYNAMIC_STATE_ENTRY;
s_dynamic.phase = SEAT_DYNAMIC_PHASE_ENTRY;
s_dynamic.internalSequence = commandSequence;
s_dynamic.modeStartedMs = Motor_GetTimeMs();
if(!SeatDynamic_StartEntry(definition, s_dynamic.modeStartedMs))
{
SeatDynamic_Fail();
return false;
}
return true;
}
void SeatDynamicMode_Service(void)
{
const seat_dynamic_mode_definition_t *definition;
uint32_t now;
uint32_t elapsed;
bool failed = false;
bool reached;
const seat_dynamic_phase_definition_t *phase;
if(!SeatDynamicMode_IsActive())
{
return;
}
definition = SeatDynamic_Definition(s_dynamic.modeId);
if(definition == NULL)
{
SeatDynamic_Fail();
return;
}
now = Motor_GetTimeMs();
if(SeatDynamic_TimeReached(
now, s_dynamic.modeStartedMs + SEAT_MOVE_DYNAMIC_MAX_RUNTIME_MS))
{
/* The ten-minute safety cap is owned by the ECU, not by a frontend
* lease or a host-side timer. Reuse the normal stop path so every
* bridge is de-energized and 0x306 returns to all-zero IDLE. */
SeatDynamicMode_Stop((uint16_t)(s_dynamic.internalSequence + 1u));
return;
}
reached = SeatDynamic_TargetsReached(&failed);
if(failed)
{
SeatDynamic_Fail();
return;
}
if(s_dynamic.state == SEAT_DYNAMIC_STATE_ENTRY)
{
if(reached)
{
if(!SeatDynamic_StartPhase(definition, 0u, now))
{
SeatDynamic_Fail();
}
return;
}
if(SeatDynamic_TimeReached(
now, s_dynamic.phaseStartedMs + SEAT_DYNAMIC_ENTRY_TIMEOUT_MS))
{
SeatDynamic_Fail();
}
return;
}
phase = &definition->phases[s_dynamic.phaseIndex];
elapsed = SeatDynamic_ElapsedMs(now, s_dynamic.phaseStartedMs);
if(phase->control == SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS)
{
if(reached)
{
if(s_dynamic.rendezvousStage == 0u)
{
if(!SeatDynamic_StartRendezvousFinal(definition))
{
SeatDynamic_Fail();
}
}
else
{
SeatDynamic_Advance(definition, now);
}
return;
}
if(elapsed > phase->timeoutMs)
{
SeatDynamic_Fail();
}
return;
}
if(elapsed < s_dynamic.phaseDurationMs)
{
return;
}
if(reached)
{
SeatDynamic_Advance(definition, now);
return;
}
if(SeatDynamic_TargetsWithinTolerance())
{
/* At the exact phase boundary an adaptive task may still be active
* while every Hall position is already inside the accepted discrete
* target band. Stop all bridges before entering the hold or opposite
* direction phase; waiting for mathematical equality creates a false
* timeout and can extend the requested phase. */
SeatMotorManager_StopAll(s_dynamic.internalSequence);
SeatDynamic_Advance(definition, now);
return;
}
if(elapsed > (s_dynamic.phaseDurationMs +
SEAT_MOVE_DYNAMIC_ARRIVAL_GRACE_MS))
{
SeatDynamic_Fail();
}
}
void SeatDynamicMode_Stop(uint16_t commandSequence)
{
if(SeatDynamicMode_IsActive())
{
Motor_RequestAbortAllFromIsr(commandSequence);
Motor_ServiceAll();
SeatMotorManager_StopAll(commandSequence);
}
(void)memset(&s_dynamic, 0, sizeof(s_dynamic));
s_dynamic.state = SEAT_DYNAMIC_STATE_IDLE;
}
bool SeatDynamicMode_IsActive(void)
{
return (s_dynamic.state == SEAT_DYNAMIC_STATE_ENTRY) ||
(s_dynamic.state == SEAT_DYNAMIC_STATE_RUNNING);
}
bool SeatDynamicMode_GetStatus(seat_dynamic_mode_status_t *status)
{
uint32_t elapsed;
if(status == NULL)
{
return false;
}
elapsed = SeatDynamicMode_IsActive() ?
SeatDynamic_ElapsedMs(Motor_GetTimeMs(),
s_dynamic.phaseStartedMs) : 0u;
status->modeId = s_dynamic.modeId;
status->state = s_dynamic.state;
status->phase = s_dynamic.phase;
status->activeMask = (uint8_t)
(SeatDynamic_ActiveMask() | (uint8_t)(s_dynamic.faultMask << 4u));
status->cycleIndex = s_dynamic.cycleIndex;
status->phaseElapsedMs = (elapsed > UINT16_MAX) ?
UINT16_MAX : (uint16_t)elapsed;
return true;
}
static bool SeatDynamic_TimeReached(uint32_t now, uint32_t deadline)
{
return ((int32_t)(now - deadline) >= 0);
}
static uint32_t SeatDynamic_ElapsedMs(uint32_t now, uint32_t started)
{
return now - started;
}
static const seat_dynamic_mode_definition_t *SeatDynamic_Definition(
uint8_t modeId)
{
uint8_t index;
for(index = 0u;
index < (uint8_t)(sizeof(s_dynamicModes) / sizeof(s_dynamicModes[0]));
index++)
{
if(s_dynamicModes[index].modeId == modeId)
{
return &s_dynamicModes[index];
}
}
return NULL;
}
static uint8_t SeatDynamic_ActiveMask(void)
{
uint8_t motor;
uint8_t mask = 0u;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
if(SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status) && status.active)
{
mask |= (uint8_t)(1u << motor);
}
}
return mask;
}
static bool SeatDynamic_StatusFailed(const seat_motor_status_t *status)
{
return (status->taskState == SEAT_MOTOR_TASK_FAILED) ||
(status->result == SEAT_MOTOR_RESULT_STALL) ||
(status->result == SEAT_MOTOR_RESULT_TIMEOUT) ||
(status->result == SEAT_MOTOR_RESULT_DRIVER_FAULT) ||
(status->result == SEAT_MOTOR_RESULT_INVALID) ||
(status->result == SEAT_MOTOR_RESULT_CALIBRATION_FAILED);
}
static bool SeatDynamic_TargetsReached(bool *failed)
{
uint8_t motor;
bool reached = true;
if(failed == NULL)
{
return false;
}
*failed = false;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
uint32_t target = s_dynamic.targetHall[motor];
uint32_t delta;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status))
{
s_dynamic.faultMask |= (uint8_t)(1u << motor);
*failed = true;
return false;
}
if(SeatDynamic_StatusFailed(&status))
{
s_dynamic.faultMask |= (uint8_t)(1u << motor);
*failed = true;
return false;
}
delta = (status.hallCount >= target) ?
(status.hallCount - target) : (target - status.hallCount);
if(status.active ||
(delta > SEAT_DYNAMIC_TARGET_TOLERANCE_PULSES))
{
reached = false;
}
}
return reached;
}
static bool SeatDynamic_TargetsWithinTolerance(void)
{
uint8_t motor;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
uint32_t target = s_dynamic.targetHall[motor];
uint32_t delta;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status) ||
SeatDynamic_StatusFailed(&status))
{
return false;
}
delta = (status.hallCount >= target) ?
(status.hallCount - target) : (target - status.hallCount);
if(delta > SEAT_DYNAMIC_TARGET_TOLERANCE_PULSES)
{
return false;
}
}
return true;
}
static bool SeatDynamic_StartEntry(
const seat_dynamic_mode_definition_t *definition,
uint32_t now)
{
uint8_t motor;
s_dynamic.internalSequence++;
s_dynamic.phaseStartedMs = now;
s_dynamic.phaseDurationMs = SEAT_DYNAMIC_ENTRY_TIMEOUT_MS;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
s_dynamic.targetHall[motor] = definition->entryTargetHall[motor];
if(!SeatMotor_MoveAbsoluteHallDuty(
s_dynamicLogicalToPhysical[motor],
definition->entryTargetHall[motor],
SEAT_DYNAMIC_ENTRY_DUTY_PERCENT,
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
static bool SeatDynamic_StartPhase(
const seat_dynamic_mode_definition_t *definition,
uint8_t phaseIndex,
uint32_t now)
{
const seat_dynamic_phase_definition_t *phase;
uint8_t motor;
if((definition == NULL) || (phaseIndex >= definition->phaseCount))
{
return false;
}
phase = &definition->phases[phaseIndex];
s_dynamic.state = SEAT_DYNAMIC_STATE_RUNNING;
s_dynamic.phaseIndex = phaseIndex;
s_dynamic.phase = phase->phase;
s_dynamic.phaseStartedMs = now;
s_dynamic.phaseDurationMs = phase->durationMs;
s_dynamic.rendezvousStage = 0u;
s_dynamic.internalSequence++;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
s_dynamic.targetHall[motor] = phase->targetHall[motor];
s_dynamic.finalTargetHall[motor] = phase->targetHall[motor];
}
if(phase->control == SEAT_DYNAMIC_CONTROL_HOLD)
{
return true;
}
if(phase->control == SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS)
{
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
uint16_t target = phase->targetHall[motor];
uint16_t current;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status))
{
return false;
}
current = (uint16_t)status.hallCount;
if(target > current + definition->rendezvousPulses)
{
s_dynamic.stagingTargetHall[motor] = (uint16_t)
(target - definition->rendezvousPulses);
}
else if(current > target +
definition->rendezvousPulses)
{
s_dynamic.stagingTargetHall[motor] = (uint16_t)
(target + definition->rendezvousPulses);
}
else
{
s_dynamic.stagingTargetHall[motor] = target;
}
s_dynamic.targetHall[motor] =
s_dynamic.stagingTargetHall[motor];
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
if(!SeatMotor_MoveAbsoluteHallDuty(
s_dynamicLogicalToPhysical[motor],
s_dynamic.targetHall[motor],
definition->cruiseDutyPercent[motor],
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
if(!SeatMotor_MoveAbsoluteHallInTimeWithGrace(
s_dynamicLogicalToPhysical[motor],
phase->targetHall[motor],
phase->durationMs,
SEAT_MOVE_DYNAMIC_ARRIVAL_GRACE_MS,
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
static bool SeatDynamic_StartRendezvousFinal(
const seat_dynamic_mode_definition_t *definition)
{
uint8_t motor;
if(definition == NULL)
{
return false;
}
s_dynamic.internalSequence++;
s_dynamic.rendezvousStage = 2u;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
s_dynamic.targetHall[motor] = s_dynamic.finalTargetHall[motor];
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
if(!SeatMotor_MoveAbsoluteHallDuty(
s_dynamicLogicalToPhysical[motor],
s_dynamic.finalTargetHall[motor],
definition->finalDutyPercent,
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
static void SeatDynamic_Fail(void)
{
uint16_t sequence = (uint16_t)(s_dynamic.internalSequence + 1u);
Motor_RequestAbortAllFromIsr(sequence);
Motor_ServiceAll();
SeatMotorManager_StopAll(sequence);
s_dynamic.internalSequence = sequence;
s_dynamic.state = SEAT_DYNAMIC_STATE_FAULT;
s_dynamic.phaseStartedMs = Motor_GetTimeMs();
}
static void SeatDynamic_Advance(
const seat_dynamic_mode_definition_t *definition,
uint32_t now)
{
uint8_t nextPhase = (uint8_t)(s_dynamic.phaseIndex + 1u);
if(nextPhase >= definition->phaseCount)
{
nextPhase = 0u;
s_dynamic.cycleIndex++;
}
if(!SeatDynamic_StartPhase(definition, nextPhase, now))
{
SeatDynamic_Fail();
}
}
@@ -0,0 +1,49 @@
#ifndef SEAT_DYNAMIC_MODE_H
#define SEAT_DYNAMIC_MODE_H
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
SEAT_DYNAMIC_MODE_NONE = 0u,
SEAT_DYNAMIC_MODE_ROCKING_HORSE = 1u,
SEAT_DYNAMIC_MODE_CRADLE = 2u
} seat_dynamic_mode_id_t;
typedef enum
{
SEAT_DYNAMIC_STATE_IDLE = 0u,
SEAT_DYNAMIC_STATE_ENTRY = 1u,
SEAT_DYNAMIC_STATE_RUNNING = 2u,
SEAT_DYNAMIC_STATE_FAULT = 3u
} seat_dynamic_state_t;
typedef enum
{
SEAT_DYNAMIC_PHASE_NONE = 0u,
SEAT_DYNAMIC_PHASE_ENTRY = 1u,
SEAT_DYNAMIC_PHASE_HORSE_FORWARD = 2u,
SEAT_DYNAMIC_PHASE_HORSE_BACKWARD = 3u,
SEAT_DYNAMIC_PHASE_CRADLE_FORWARD = 5u,
SEAT_DYNAMIC_PHASE_CRADLE_RETURN = 6u
} seat_dynamic_phase_t;
typedef struct
{
uint8_t modeId;
uint8_t state;
uint8_t phase;
uint8_t activeMask;
uint16_t cycleIndex;
uint16_t phaseElapsedMs;
} seat_dynamic_mode_status_t;
void SeatDynamicMode_Init(void);
bool SeatDynamicMode_Start(uint8_t modeId, uint16_t commandSequence);
void SeatDynamicMode_Service(void);
void SeatDynamicMode_Stop(uint16_t commandSequence);
bool SeatDynamicMode_IsActive(void);
bool SeatDynamicMode_GetStatus(seat_dynamic_mode_status_t *status);
#endif /* SEAT_DYNAMIC_MODE_H */
@@ -10,6 +10,13 @@
#define SEAT_MOTOR_SPEED_CAL_MEASURE_MS (1000ul)
#define SEAT_MOTOR_SPEED_CAL_DUTY_PERCENT (95u)
#define SEAT_MOTOR_DEFAULT_CAL_LEG_TIMEOUT_MS (70000ul)
#define SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS (500ul)
#define SEAT_MOTOR_TIMED_M0_NEAR_EXTENSION_MS (900ul)
#define SEAT_MOTOR_TIMED_M0_FAR_EXTENSION_MS (1000ul)
#define SEAT_MOTOR_TIMED_M1_NEAR_EXTENSION_MS (1200ul)
#define SEAT_MOTOR_TIMED_M1_FAR_EXTENSION_MS (700ul)
#define SEAT_MOTOR_TIMED_M3_NEAR_EXTENSION_MS (1900ul)
#define SEAT_MOTOR_TIMED_M3_FAR_EXTENSION_MS (1600ul)
typedef char seat_motor_manager_requires_four_motors[
(MOTOR_MODE_COUNT == 4) ? 1 : -1];
@@ -22,6 +29,8 @@ typedef struct
uint32_t commandSequence;
uint32_t targetValueX100;
uint32_t durationMs;
uint32_t speedPlanDurationMs;
uint32_t deadlineGraceMs;
uint32_t deadlineMs;
uint32_t retryAtMs;
uint32_t speedValuePerSecondX100;
@@ -95,6 +104,36 @@ static uint32_t SeatMotor_AbsDelta(uint32_t lhs, uint32_t rhs)
return (lhs >= rhs) ? (lhs - rhs) : (rhs - lhs);
}
static uint32_t SeatMotor_TimedPlanExtensionMs(
motor_mode_t mode,
motor_direction_t direction)
{
/* The four loaded mechanisms do not have identical low-speed breakaway
* and coast behavior. Bench traces show repeatable early arrival against
* M2 when every axis follows the same ideal time-position line. Extend
* only the speed plan (never the phase deadline) by the measured lead so
* all four axes converge at the requested phase boundary. */
if(mode == MOTOR_MODE_0_LINEAR)
{
return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ?
SEAT_MOTOR_TIMED_M0_NEAR_EXTENSION_MS :
SEAT_MOTOR_TIMED_M0_FAR_EXTENSION_MS;
}
if(mode == MOTOR_MODE_1_ANGLE)
{
return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ?
SEAT_MOTOR_TIMED_M1_NEAR_EXTENSION_MS :
SEAT_MOTOR_TIMED_M1_FAR_EXTENSION_MS;
}
if(mode == MOTOR_MODE_3_LINEAR_TRACK)
{
return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ?
SEAT_MOTOR_TIMED_M3_NEAR_EXTENSION_MS :
SEAT_MOTOR_TIMED_M3_FAR_EXTENSION_MS;
}
return 0ul;
}
static bool SeatMotor_TargetWithinHallTolerance(
const seat_motor_task_t *task,
const motor_position_info_t *info)
@@ -192,9 +231,10 @@ static bool SeatMotor_PrepareNewTask(motor_mode_t mode,
static bool SeatMotor_StartPositionTask(motor_mode_t mode,
uint32_t targetValueX100,
uint8_t dutyPercent,
uint32_t speedValuePerSecondX100,
uint32_t durationLimitMs,
uint32_t commandSequence,
uint32_t speedValuePerSecondX100,
uint32_t durationLimitMs,
uint32_t speedPlanDurationMs,
uint32_t commandSequence,
bool dynamicSpeed,
bool preserveRetry,
seat_motor_task_state_t runningState)
@@ -203,7 +243,7 @@ static bool SeatMotor_StartPositionTask(motor_mode_t mode,
seat_motor_task_t *task;
motor_position_info_t info;
uint32_t travelValueX100;
uint32_t planDurationMs;
uint32_t planDurationMs = 0ul;
uint8_t retryCount;
if((false == SeatMotor_ModeValid(mode)) ||
@@ -257,7 +297,8 @@ static bool SeatMotor_StartPositionTask(motor_mode_t mode,
if(false != dynamicSpeed)
{
planDurationMs = (durationLimitMs != 0ul) ? durationLimitMs :
planDurationMs = (speedPlanDurationMs != 0ul) ?
speedPlanDurationMs : (durationLimitMs != 0ul) ? durationLimitMs :
(uint32_t)((((uint64_t)travelValueX100 * 1000ull) +
speedValuePerSecondX100 - 1ull) /
speedValuePerSecondX100);
@@ -282,6 +323,7 @@ static bool SeatMotor_StartPositionTask(motor_mode_t mode,
SEAT_MOTOR_TASK_WAIT_START : runningState;
task->targetValueX100 = targetValueX100;
task->durationMs = durationLimitMs;
task->speedPlanDurationMs = planDurationMs;
task->deadlineMs = 0ul;
task->speedValuePerSecondX100 = speedValuePerSecondX100;
task->dutyPercent = dutyPercent;
@@ -490,7 +532,8 @@ void SeatMotorManager_Service(void)
}
else if(0ul != task->durationMs)
{
task->deadlineMs = now + task->durationMs;
task->deadlineMs = now + task->durationMs +
task->deadlineGraceMs;
}
else
{
@@ -517,6 +560,7 @@ void SeatMotorManager_Service(void)
task->dutyPercent,
task->speedValuePerSecondX100,
task->durationMs,
task->speedPlanDurationMs,
task->commandSequence,
task->dynamicSpeed,
true,
@@ -643,6 +687,7 @@ bool SeatMotor_MoveAbsoluteDuty(motor_mode_t mode,
dutyPercent,
0ul,
0ul,
0ul,
commandSequence,
false,
false,
@@ -696,6 +741,7 @@ bool SeatMotor_MoveAbsoluteNormalized(motor_mode_t mode,
dutyPercent,
0ul,
timeoutMs,
0ul,
commandSequence,
false,
false,
@@ -795,6 +841,7 @@ bool SeatMotor_MoveAbsoluteSpeed(motor_mode_t mode,
0u,
speedValuePerSecondX100,
0ul,
0ul,
commandSequence,
true,
false,
@@ -879,10 +926,27 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t commandSequence)
{
return SeatMotor_MoveAbsoluteInTimeWithGrace(mode,
targetValueX100,
durationMs,
0ul,
commandSequence);
}
bool SeatMotor_MoveAbsoluteInTimeWithGrace(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence)
{
motor_position_info_t info;
uint32_t travel;
uint32_t speed;
uint32_t speedPlanDurationMs;
uint32_t speedPlanExtensionMs;
motor_direction_t direction;
bool started;
if((0ul == durationMs) ||
(MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
@@ -898,16 +962,94 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
MOTOR_SPEED_MIN_DUTY_PERCENT,
commandSequence);
}
speed = (uint32_t)((((uint64_t)travel) * 1000ull) / durationMs);
return SeatMotor_StartPositionTask(mode,
targetValueX100,
0u,
speed,
durationMs,
commandSequence,
true,
false,
SEAT_MOTOR_TASK_POSITION);
direction = (targetValueX100 >= info.valueX100) ?
MOTOR_DIRECTION_TOWARD_FAR :
MOTOR_DIRECTION_TOWARD_NEAR;
speedPlanDurationMs = (durationMs > SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS) ?
(durationMs - SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS) : durationMs;
speedPlanExtensionMs = SeatMotor_TimedPlanExtensionMs(mode, direction);
if(speedPlanExtensionMs > (UINT32_MAX - speedPlanDurationMs))
{
return false;
}
speedPlanDurationMs += speedPlanExtensionMs;
speed = (uint32_t)
((((uint64_t)travel) * 1000ull) / speedPlanDurationMs);
started = SeatMotor_StartPositionTask(mode,
targetValueX100,
0u,
speed,
durationMs,
speedPlanDurationMs,
commandSequence,
true,
false,
SEAT_MOTOR_TASK_POSITION);
if(started)
{
tasks[(uint32_t)mode].deadlineGraceMs = deadlineGraceMs;
}
return started;
}
bool SeatMotor_MoveAbsoluteHallDuty(motor_mode_t mode,
uint32_t targetHallCount,
uint8_t dutyPercent,
uint32_t commandSequence)
{
motor_position_info_t info;
uint32_t targetValueX100;
if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
(false == info.positionValid) || (0ul == info.maxHallCount) ||
(targetHallCount > info.maxHallCount))
{
return false;
}
targetValueX100 = (uint32_t)
(((uint64_t)targetHallCount * info.maxValueX100 +
(info.maxHallCount / 2ul)) / info.maxHallCount);
return SeatMotor_MoveAbsoluteDuty(mode,
targetValueX100,
dutyPercent,
commandSequence);
}
bool SeatMotor_MoveAbsoluteHallInTime(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t commandSequence)
{
return SeatMotor_MoveAbsoluteHallInTimeWithGrace(mode,
targetHallCount,
durationMs,
0ul,
commandSequence);
}
bool SeatMotor_MoveAbsoluteHallInTimeWithGrace(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence)
{
motor_position_info_t info;
uint32_t targetValueX100;
if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
(false == info.positionValid) || (0ul == info.maxHallCount) ||
(targetHallCount > info.maxHallCount))
{
return false;
}
targetValueX100 = (uint32_t)
(((uint64_t)targetHallCount * info.maxValueX100 +
(info.maxHallCount / 2ul)) / info.maxHallCount);
return SeatMotor_MoveAbsoluteInTimeWithGrace(mode,
targetValueX100,
durationMs,
deadlineGraceMs,
commandSequence);
}
bool SeatMotor_CalibrateRange(motor_mode_t mode,
@@ -110,6 +110,30 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t commandSequence);
/* Keep the same speed plan while allowing a bounded scheduler/settling grace
* before the manager reports TIMEOUT. */
bool SeatMotor_MoveAbsoluteInTimeWithGrace(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Hall-coordinate variants used by firmware-owned synchronized trajectories.
* The target is checked against the live calibrated MaxHall before conversion
* to the motor's physical value. */
bool SeatMotor_MoveAbsoluteHallDuty(motor_mode_t mode,
uint32_t targetHallCount,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTime(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTimeWithGrace(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Range calibration is interruptible by Motor_RequestAbortAllFromIsr(). */
bool SeatMotor_CalibrateRange(motor_mode_t mode,
@@ -7,6 +7,7 @@
#include "motor_control.h"
#include "safety_manager.h"
#include "seat_controller_thermal.h"
#include "seat_dynamic_mode.h"
#include "seat_motor_manager.h"
#include "seat_move_config.h"
#include "seat_move_store.h"
@@ -67,7 +68,9 @@ typedef enum
SEAT_MOVE_OPCODE_CALIBRATE_RANGE = 0x0Au,
SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE = 0x0Bu,
SEAT_MOVE_OPCODE_QUERY_STATUS = 0x0Cu,
SEAT_MOVE_OPCODE_CLEAR_FAULT = 0x0Du
SEAT_MOVE_OPCODE_CLEAR_FAULT = 0x0Du,
SEAT_MOVE_OPCODE_START_DYNAMIC_MODE = 0x0Eu,
SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE = 0x0Fu
} seat_move_opcode_t;
typedef enum
@@ -170,12 +173,14 @@ static uint8_t s_motorPeriodicId = 0u;
static uint8_t s_diagnosticPeriodicId = 0u;
static bool s_motorPeriodicPending = false;
static bool s_diagnosticPeriodicPending = false;
static bool s_dynamicStatusPending = true;
static uint8_t s_systemAlive = 0u;
static uint8_t s_diagnosticAlive[SEAT_MOVE_MOTOR_COUNT];
static uint32_t s_nextStartMs = 0u;
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 bool SeatMove_TimeReached(uint32_t now, uint32_t deadline);
static bool SeatMove_SequenceNewer(uint16_t candidate, uint16_t reference);
@@ -220,6 +225,7 @@ static bool SeatMove_SendSystemStatus(void);
static bool SeatMove_SendMotorStatus(uint8_t motor);
static bool SeatMove_SendDiagnostic(uint8_t motor);
static bool SeatMove_SendCurrentDiagnostic(uint8_t motor);
static bool SeatMove_SendDynamicStatus(void);
static void SeatMove_ServiceFeedback(void);
bool SeatMoveApplication_Init(void)
@@ -233,6 +239,7 @@ bool SeatMoveApplication_Init(void)
s_storeSavePending = false;
s_storeSaveNotBeforeMs = 0u;
SeatMotorManager_Init();
SeatDynamicMode_Init();
SeatMoveStore_Init();
if(SeatMoveStore_IsValid() && SeatMoveStore_IsClean() &&
@@ -254,9 +261,11 @@ bool SeatMoveApplication_Init(void)
s_nextSystemStatusMs = now;
s_nextMotorPollMs = now;
s_nextDiagnosticPollMs = now;
s_nextDynamicStatusMs = now;
s_systemStatusPending = true;
s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_dynamicStatusPending = true;
return true;
}
@@ -273,6 +282,15 @@ void SeatMoveApplication_Service(void)
{
uint8_t motor;
if(SeatDynamicMode_IsActive())
{
SeatDynamicMode_Stop(g_seatMoveLastSequence);
s_storeSavePending = true;
s_storeSaveNotBeforeMs =
Motor_GetTimeMs() + SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
s_dynamicStatusPending = true;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
s_pending[motor].valid = false;
@@ -294,6 +312,7 @@ void SeatMoveApplication_Service(void)
}
SeatMove_ServiceDispatcher();
SeatMotorManager_Service();
SeatDynamicMode_Service();
SeatMove_ServiceStateChanges();
SeatMove_ServicePersistence();
SeatMove_ServiceFeedback();
@@ -326,7 +345,9 @@ void Motor_CalibrationServiceHook(void)
void SeatMoveApplication_StopAll(uint16_t sequence)
{
uint8_t logicalMotor;
bool dynamicWasActive = SeatDynamicMode_IsActive();
SeatDynamicMode_Stop(sequence);
Motor_RequestAbortAllFromIsr(sequence);
Motor_ServiceAll();
for(logicalMotor = 0u;
@@ -342,6 +363,13 @@ void SeatMoveApplication_StopAll(uint16_t sequence)
s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_systemStatusPending = true;
s_dynamicStatusPending = true;
if(dynamicWasActive)
{
s_storeSavePending = true;
s_storeSaveNotBeforeMs =
Motor_GetTimeMs() + SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
}
}
static bool SeatMove_TimeReached(uint32_t now, uint32_t deadline)
@@ -479,6 +507,8 @@ static bool SeatMove_ParseCommand(const bsp_seat_move_can_frame_t *frame,
case SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE:
case SEAT_MOVE_OPCODE_QUERY_STATUS:
case SEAT_MOVE_OPCODE_CLEAR_FAULT:
case SEAT_MOVE_OPCODE_START_DYNAMIC_MODE:
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
break;
default:
*reject = SEAT_MOVE_REJECT_BAD_OPCODE;
@@ -501,7 +531,9 @@ static bool SeatMove_ParametersValid(const seat_move_command_t *command,
*reject = SEAT_MOVE_REJECT_BAD_MOTOR;
return false;
}
if((command->opcode == SEAT_MOVE_OPCODE_STOP_ALL) &&
if(((command->opcode == SEAT_MOVE_OPCODE_STOP_ALL) ||
(command->opcode == SEAT_MOVE_OPCODE_START_DYNAMIC_MODE) ||
(command->opcode == SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE)) &&
(command->motor != 0u))
{
*reject = SEAT_MOVE_REJECT_BAD_MOTOR;
@@ -590,6 +622,22 @@ static bool SeatMove_ParametersValid(const seat_move_command_t *command,
return false;
}
break;
case SEAT_MOVE_OPCODE_START_DYNAMIC_MODE:
if(((command->paramA != SEAT_DYNAMIC_MODE_ROCKING_HORSE) &&
(command->paramA != SEAT_DYNAMIC_MODE_CRADLE)) ||
(command->paramB != 0u))
{
*reject = SEAT_MOVE_REJECT_PARAM_RANGE;
return false;
}
break;
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
if((command->paramA != 0u) || (command->paramB != 0u))
{
*reject = SEAT_MOVE_REJECT_PARAM_RANGE;
return false;
}
break;
default:
*reject = SEAT_MOVE_REJECT_BAD_OPCODE;
return false;
@@ -801,9 +849,27 @@ static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame)
g_seatMoveAcceptedCount++;
return;
}
if(command.opcode == SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE)
{
SeatMoveApplication_StopAll(command.sequence);
SeatMove_SetResponse(SEAT_MOVE_ACK_ACCEPTED,
SEAT_MOVE_REJECT_NONE,
&command);
g_seatMoveAcceptedCount++;
return;
}
if(command.opcode == SEAT_MOVE_OPCODE_STOP_MOTOR)
{
SeatMove_StopMotor(command.motor, command.sequence);
if(SeatDynamicMode_IsActive())
{
/* A one-axis stop would invalidate a synchronized trajectory.
* Stop the complete dynamic mode instead. */
SeatMoveApplication_StopAll(command.sequence);
}
else
{
SeatMove_StopMotor(command.motor, command.sequence);
}
SeatMove_SetResponse(SEAT_MOVE_ACK_ACCEPTED,
SEAT_MOVE_REJECT_NONE,
&command);
@@ -855,6 +921,64 @@ static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame)
&command);
return;
}
if(command.opcode == SEAT_MOVE_OPCODE_START_DYNAMIC_MODE)
{
if(SeatDynamicMode_IsActive() || SeatMotorManager_IsAnyActive() ||
SeatMove_AnyPending())
{
g_seatMoveConflictCount++;
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_BUSY_SAME_MOTOR,
&command);
return;
}
if(SeatMove_PositionValidMask() != SEAT_MOVE_ACTIVE_MOTOR_MASK)
{
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SeatMoveStore_HasCorruptRecord() ?
SEAT_MOVE_REJECT_CAL_DATA_INVALID :
SEAT_MOVE_REJECT_POSITION_INVALID,
&command);
return;
}
SafetyManager_KickWatchdog();
if(!SeatMoveStore_MarkMovementStarted())
{
SafetyManager_LatchFault(SAFETY_FAULT_CONFIG_STORE);
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_INTERNAL_ERROR,
&command);
return;
}
s_storeSavePending = true;
s_storeSaveNotBeforeMs =
Motor_GetTimeMs() + SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
if(!SeatDynamicMode_Start((uint8_t)command.paramA,
command.sequence))
{
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_INTERNAL_ERROR,
&command);
s_dynamicStatusPending = true;
return;
}
s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_dynamicStatusPending = true;
SeatMove_SetResponse(SEAT_MOVE_ACK_ACCEPTED,
SEAT_MOVE_REJECT_NONE,
&command);
g_seatMoveAcceptedCount++;
return;
}
if(SeatDynamicMode_IsActive())
{
g_seatMoveConflictCount++;
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_BUSY_SAME_MOTOR,
&command);
return;
}
if(SeatMove_IsPositionCommand(command.opcode))
{
if(!SeatMove_GetStatus(command.motor, &status))
@@ -1069,6 +1193,14 @@ static void SeatMove_ServicePersistence(void)
return;
}
/* A continuously cycling dynamic mode must not commit Work Flash at each
* hold or phase boundary. The dirty record remains authoritative until
* STOP_ALL/STOP_DYNAMIC_MODE and one final settled save. */
if(SeatDynamicMode_IsActive())
{
return;
}
now = Motor_GetTimeMs();
if(SeatMove_AnyPending() || SeatMotorManager_IsAnyActive() ||
SeatMove_AnyPhysicalMotorMoving())
@@ -1166,8 +1298,11 @@ static uint8_t SeatMove_MapTaskResult(const seat_motor_status_t *status)
static uint8_t SeatMove_SystemState(void)
{
seat_dynamic_mode_status_t dynamicStatus;
uint8_t activeMask = SeatMove_ActiveMask();
uint8_t faultedMask = SeatMove_FaultedMask();
bool dynamicFault = SeatDynamicMode_GetStatus(&dynamicStatus) &&
(dynamicStatus.state == SEAT_DYNAMIC_STATE_FAULT);
if(SafetyManager_IsInhibited())
{
@@ -1178,12 +1313,12 @@ static uint8_t SeatMove_SystemState(void)
{
return SEAT_MOVE_SYSTEM_CALIBRATION;
}
if((faultedMask != 0u) ||
if((faultedMask != 0u) || dynamicFault ||
(SafetyManager_GetFault() != SAFETY_FAULT_NONE))
{
return SEAT_MOVE_SYSTEM_FAULT;
}
if(activeMask != 0u)
if((activeMask != 0u) || SeatDynamicMode_IsActive())
{
return SEAT_MOVE_SYSTEM_ACTIVE;
}
@@ -1192,11 +1327,15 @@ static uint8_t SeatMove_SystemState(void)
static bool SeatMove_SendSystemStatus(void)
{
seat_dynamic_mode_status_t dynamicStatus;
uint8_t activeMask = SeatMove_ActiveMask();
uint8_t faultedMask = SeatMove_FaultedMask();
uint8_t positionValidMask = SeatMove_PositionValidMask();
bool calibrationBusy = SeatMove_CalibrationBusy();
bool faultSummary = (faultedMask != 0u) ||
bool dynamicStatusValid = SeatDynamicMode_GetStatus(&dynamicStatus);
bool dynamicFault = dynamicStatusValid &&
(dynamicStatus.state == SEAT_DYNAMIC_STATE_FAULT);
bool faultSummary = (faultedMask != 0u) || dynamicFault ||
(SafetyManager_GetFault() != SAFETY_FAULT_NONE) ||
SeatMoveStore_HasCorruptRecord();
uint8_t byte0 = (uint8_t)((g_seatMoveAck & 0x0Fu) |
@@ -1205,6 +1344,7 @@ static bool SeatMove_SendSystemStatus(void)
uint8_t byte4 = g_seatMoveLastOpcode;
uint8_t byte5 = (uint8_t)((activeMask & 0x0Fu) |
(SafetyManager_IsInhibited() ? 0x10u : 0u) |
(SeatDynamicMode_IsActive() ? 0x20u : 0u) |
(calibrationBusy ? 0x40u : 0u) |
(faultSummary ? 0x80u : 0u));
uint8_t byte6 = (uint8_t)((faultedMask & 0x0Fu) |
@@ -1304,7 +1444,9 @@ static bool SeatMove_SendDiagnostic(uint8_t motor)
UINT16_MAX : (uint16_t)status.hallCount;
maxHallCount = (status.maxHallCount > UINT16_MAX) ?
UINT16_MAX : (uint16_t)status.maxHallCount;
speedRaw = (status.hallSpeedPpsX100 + 100u) / 200u;
/* 0x303 B6 is specified as 0.5 pulse/s per LSB. hallSpeed is x100,
* therefore divide by 50 (with half-up rounding), not by 200. */
speedRaw = (status.hallSpeedPpsX100 + 25u) / 50u;
if(speedRaw > UINT8_MAX)
{
speedRaw = UINT8_MAX;
@@ -1363,6 +1505,27 @@ static bool SeatMove_SendCurrentDiagnostic(uint8_t motor)
data1);
}
static bool SeatMove_SendDynamicStatus(void)
{
seat_dynamic_mode_status_t status;
uint32_t data0;
uint32_t data1;
if(!SeatDynamicMode_GetStatus(&status))
{
return false;
}
data0 = (uint32_t)status.modeId |
((uint32_t)status.state << 8u) |
((uint32_t)status.phase << 16u) |
((uint32_t)status.activeMask << 24u);
data1 = (uint32_t)status.cycleIndex |
((uint32_t)status.phaseElapsedMs << 16u);
return BspSeatMoveCan_Send(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID,
data0,
data1);
}
static void SeatMove_ServiceFeedback(void)
{
uint32_t now = Motor_GetTimeMs();
@@ -1373,6 +1536,11 @@ static void SeatMove_ServiceFeedback(void)
s_systemStatusPending = true;
s_nextSystemStatusMs = now + SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS;
}
if(SeatMove_TimeReached(now, s_nextDynamicStatusMs))
{
s_dynamicStatusPending = true;
s_nextDynamicStatusMs = now + SEAT_MOVE_DYNAMIC_STATUS_PERIOD_MS;
}
if(!s_motorPeriodicPending &&
SeatMove_TimeReached(now, s_nextMotorPollMs))
{
@@ -1408,6 +1576,19 @@ static void SeatMove_ServiceFeedback(void)
}
return;
}
if(s_dynamicStatusPending)
{
if(SeatMove_SendDynamicStatus())
{
s_dynamicStatusPending = false;
g_seatMoveFeedbackTxCount++;
}
else
{
g_seatMoveFeedbackTxFailureCount++;
}
return;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
uint8_t bit = (uint8_t)(1u << motor);
@@ -0,0 +1,55 @@
#ifndef SEAT_DYNAMIC_MODE_CONFIG_H
#define SEAT_DYNAMIC_MODE_CONFIG_H
/* Rocking-horse tuning for the connected N7 passenger seat.
*
* M0/M1/M2/M3 are longitudinal, cushion tilt, backrest and legrest. Keep all
* Hall targets inside the calibrated ranges before building for another seat.
* The centre pose and two symmetric endpoints are deliberately separated from
* the mechanical endpoints. M1 uses a 120-Hall peak-to-peak stroke (about
* 18 mm of the calibrated vertical component) so the rider can feel a real
* rise/fall instead of only a backrest pitch. M0 uses a 100-Hall stroke at
* 90% duty to overcome the opposing horizontal component created by that
* diagonal M1 stroke. This file is the ECU-side customization
* point; config/modes/vehicles/denza_n7/rocking_horse.yaml mirrors these values
* for the long-term mode library and frontend.
*/
#define SEAT_DYNAMIC_HORSE_ENTRY_M0_HALL (320u)
#define SEAT_DYNAMIC_HORSE_ENTRY_M1_HALL (190u)
#define SEAT_DYNAMIC_HORSE_ENTRY_M2_HALL (500u)
#define SEAT_DYNAMIC_HORSE_ENTRY_M3_HALL (430u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M0_HALL (270u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M1_HALL (250u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M2_HALL (460u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M3_HALL (400u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M0_HALL (370u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M1_HALL (130u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M2_HALL (540u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M3_HALL (460u)
#define SEAT_DYNAMIC_HORSE_M0_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_HORSE_M1_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_HORSE_M2_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_HORSE_M3_DUTY_PERCENT (50u)
#define SEAT_DYNAMIC_HORSE_FINAL_DUTY_PERCENT (50u)
#define SEAT_DYNAMIC_HORSE_RENDEZVOUS_PULSES (8u)
#define SEAT_DYNAMIC_HORSE_PHASE_TIMEOUT_MS (15000u)
#if ((SEAT_DYNAMIC_HORSE_ENTRY_M0_HALL > 480u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M0_HALL > 480u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M0_HALL > 480u) || \
(SEAT_DYNAMIC_HORSE_ENTRY_M1_HALL > 374u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M1_HALL > 374u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M1_HALL > 374u) || \
(SEAT_DYNAMIC_HORSE_ENTRY_M2_HALL > 917u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M2_HALL > 917u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M2_HALL > 917u) || \
(SEAT_DYNAMIC_HORSE_ENTRY_M3_HALL > 647u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M3_HALL > 647u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M3_HALL > 647u))
#error "Rocking-horse Hall targets exceed the nominal N7 calibrated ranges."
#endif
#endif /* SEAT_DYNAMIC_MODE_CONFIG_H */
@@ -67,14 +67,25 @@
#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 (6u)
#define SEAT_MOVE_FIRMWARE_REVISION (13u)
#define SEAT_MOVE_CAN_COMMAND_ID (0x300u)
#define SEAT_MOVE_CAN_SYSTEM_STATUS_ID (0x301u)
#define SEAT_MOVE_CAN_MOTOR_STATUS_ID (0x302u)
#define SEAT_MOVE_CAN_DIAGNOSTIC_ID (0x303u)
#define SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID (0x304u)
#define SEAT_MOVE_CAN_DYNAMIC_STATUS_ID (0x306u)
#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
* ECU keeps cycling after the start ACK, but always stops after ten minutes
* if no STOP_DYNAMIC/STOP_ALL command arrives. */
#define SEAT_MOVE_DYNAMIC_MAX_RUNTIME_MS (600000u)
/* The phase duration is the requested synchronized-arrival target. A loaded
* bench axis may be physically slower (notably M2 for the 472-pulse breathing
* stroke), so normal Hall progress may continue through this bounded grace
* window. Stall/no-motion and driver-fault protections remain immediate. */
#define SEAT_MOVE_DYNAMIC_ARRIVAL_GRACE_MS (30000u)
#define SEAT_MOVE_MOTOR_STATUS_POLL_MS (25u)
/* One motor is emitted per poll; 25 ms gives each axis a 100 ms 0x303/0x304
* update period and captures the 200 ms high-current confirmation window. */
@@ -148,7 +159,8 @@
(SEAT_MOVE_CAN_SYSTEM_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_MOTOR_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu))
(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID > 0x7FFu))
#error "Seat movement CAN identifiers must be 11-bit standard IDs."
#endif
@@ -1,6 +1,6 @@
# Seat_Controller_demo
`Seat_Controller_demo` 是 `CYT4BFBCJE` / `CY_MCU_rev_d` / Cortex-M0+ Flash 的单板统一座椅控制器工程,集成座椅通风、靠背加热和四电机移动。SDL 公共源码未修改,应用配置和功能代码均位于本工程目录。
`Seat_Controller_demo` 是 `CYT4BFBCJE` / `CY_MCU_rev_d` / Cortex-M0+ Flash 的单板统一座椅控制器工程,集成座椅通风、靠背加热、四电机移动和两个原生连续循环动态模式。动态模式收到停止/全停命令或启动累计10分钟时终止。SDL 公共源码未修改,应用配置和功能代码均位于本工程目录。
## 单板资源
@@ -22,8 +22,9 @@
- TX `0x301/0x302/0x303`:移动 ACK、状态和 Hall 诊断。
- TX `0x304`:四轴电流诊断;B0 MotorID,B1 bits0..4依次为采样有效/零点有效/高电流/低Hall/标定中,B2..3 ADC raw,B4..5停止态零点raw,B6..7该轴堵转电流差值阈值raw。
- TX `0x305`:热管理电流诊断;B0采样有效标志,B1输出状态,B2..3通风电流raw,B4..5靠背加热电流raw,B6/B7为两路ADC累计错误计数(饱和到255)。
- TX `0x306`:100 ms动态模式状态;ModeID、状态、阶段、四轴ActiveMask、循环序号和阶段已运行毫秒数。
CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为 1;支持 `MOVE_ABSOLUTE`、`MOVE_RELATIVE`、`MOVE_TIMED`、`STOP_MOTOR`、`STOP_ALL`、`CALIBRATE_RANGE`、`HOME_KNOWN_RANGE`、`QUERY_STATUS` 和 `CLEAR_FAULT`。四轴定义及完整字节布局见 `Docs/outputs/seat_move_four_motor_20260724/座椅四电机移动功能CAN矩阵表.xlsx`。
CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为 1;支持 `MOVE_ABSOLUTE`、`MOVE_RELATIVE`、`MOVE_TIMED`、`STOP_MOTOR`、`STOP_ALL`、`CALIBRATE_RANGE`、`HOME_KNOWN_RANGE`、`QUERY_STATUS`、`CLEAR_FAULT`、`START_DYNAMIC_MODE(0x0E)` 和 `STOP_DYNAMIC_MODE(0x0F)`。ModeID 1为木马模式,ModeID 2为动态摇篮;两者均按目标到位连续循环。四轴定义及完整字节布局见 `Docs/outputs/seat_move_four_motor_20260724/座椅四电机移动功能CAN矩阵表.xlsx`。
2026-07-30 四个独立量程入口已验证 CAN MotorID 与板级物理 Mode 同号映射:M0/physical0 水平前后、M1/physical1 坐盆上下、M2/physical2 靠背前后、M3/physical3 腿托上下。首次测试仍须目视确认没有错误执行机构动作。
@@ -46,6 +47,7 @@ CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为
- 四轴分别具有启动宽限(M0/M1/M2=1 s,M3=2 s)。普通移动仅在“500 ms Hall双边沿窗口不超过2个边沿”且“相对停止态零点的轴电流超过独立阈值并持续200 ms”同时成立时报告 `STALL`;低Hall但电流低或采样过期时不误报堵转,连续5 s无运动后关断并报告 `TIMEOUT`。
- 真实台架诊断必须同时保存命令期间的 `0x304` 电流峰值和 `0x303` Hall 变化:高电流且无 Hall 表示机械阻塞或 Hall 公共参考异常,低电流且双向无 Hall 更符合驱动桥、连接器、电机线束或 Hall 地线开路。2026-08-01 的 M0/M1/M2 异常最终定位为 Hall 地线断开;2026-08-02 恢复接线后完整 33 条自动 HIL 全部通过。不得靠提高 Duty、阈值或超时掩盖同类线路问题。
- 四轴普通绝对/相对移动默认使用90% PWM占空比;堵转电流差值阈值在重新带载标定前统一使用500 raw保守默认值,优先避免正常运动误报。PWM软件上限保持95%。
- 动态模式按Hall目标和阶段时长做低速位置进度闭环;四轴同步启动,在相位边界以±3 Hall判定到位后统一停止再切换。任何轴不能在阶段宽限内到位时全停并通过`0x306`报告FAULT。
- 位置任务因端点/堵转事件停止时,若最终Hall已进入目标±2脉冲范围则按到位完成;底层LastEvent仍由`0x303`保留用于诊断。超过该范围的STALL/TIMEOUT仍按失败处理。
- `HOME_KNOWN_RANGE`/全量标定的寻端语义与普通移动分离。端点要求500 ms内Hall双边沿为0,避免慢速移动仅有下降沿时被误判为端点;已知量程归零若第一次寻近端少于4个真实Hall边沿,会先向远端验证至少4个双边沿再重新寻近端,并校验回程与离端边沿数(允许1边沿误差),禁止把电机未启动直接清零。无法证明往返运动时归零失败并使PositionValid失效。位置和量程仍按Hall上升沿计数。
- `STOP_ALL` 不受旧 Sequence 限制;CAN RX ISR 先立即终止四路 PWM,因此也能打断阻塞式全行程标定,主流程随后完成 ACK 和 Sequence 重同步。
@@ -70,5 +72,18 @@ CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为
- `Tests/manual_hil/05_seat_function_bridge.py`:在ZCANPRO内运行的CAN桥接。
- `Tests/manual_hil/run_seat_function_gui.bat`:Windows选择界面,从仓库根目录 `座椅功能模式配置表.xlsx` 连续动态读取当前11种模式/12个场景,显示物理目标、重算Hall、CAN结果并保存人工观察;小憩模式为单场景,并包含新增“小腿解压悬浮模式”。
- `Tests/manual_hil/02_power_memory.py` 默认只做正常掉电恢复;`03_protection_faults.py` 默认只做无需注入设备的协议保护。
- `Tests/manual_hil/07_dynamic_modes.py`:revision 13原生动态模式HIL。验证带水平耦合补偿的木马模式和动态摇篮的完整阶段顺序、精确Hall目标、四轴全程联动、自动跨周期和停止;ECU另有600 s硬截止。
2026-08-04已验证公共动态运行时的自动续环、循环中停止,以及单次连续运行600.024 s后的ECU安全截止;任何轴堵转、无运动或驱动故障仍会保持全停,固件不绕过底层保护。
同日revision 9在M1执行已知量程回近端后通过重新设计的摇篮实测:Step 1→2启动/到位离散0.077/0.338 s,Step 2→3为0.079/0.495 s,全部目标在±3 Hall内,CycleIndex自动进入1,下一轮四轴启动离散0.082 s,停止后`0x306`全零。
同日revision 10删除M1延迟追赶:正向/返回水平与靠背约走25%时,坐盆已走44.0%/39.0%;四轴启动离散0.070/0.050 s、到位离散0.342/0.339 s,下一轮启动离散0.051 s。四轴从阶段开始全程联动,停止后`0x306`全零。
同日revision 11用木马模式替换旧ModeID 1轨迹。木马在前俯`[290,220,460,400]`和后仰`[350,160,540,460]`间连续往复;稳态前俯/后仰启动离散0.080/0.079 s,到位离散0.234/0.232 s,水平到25%时坐盆已达41.7%/36.7%,完整周期约9.0 s。目标、自动续环和停止全零均PASS,配置见`Config/seat_dynamic_mode_config.h`。
同日revision 12将坐盆目标扩大到`250↔130` Hall并把M1巡航占空比提升为90%,坐盆峰峰垂直分量约18 mm。稳态前俯/后仰启动离散0.054/0.081 s,到位离散0.389/0.182 s,水平到25%时坐盆已达46.6%/35.8%,完整周期约9.2 s;无FAULT、目标、自动续环和停止全零均PASS。
同日revision 13将M0水平目标扩大到`270↔370` Hall并把巡航Duty提升为90%,用100 Hall峰峰行程补偿M1斜向运动。实测水平峰峰行程约43.1 mm、估算净水平摆幅约26.7 mm;稳态前俯/后仰启动离散0.077/0.052 s,到位离散0.337/0.182 s,四轴巡航持续推进、无FAULT、目标、自动续环和停止全零均PASS。
本轮不执行 `01_calibration_position.py` 或 `04_waveform_full_function.py`,两者均已设置归档保护。具体顺序和日志回传要求见 `Tests/manual_hil/README_CN.md`。
@@ -1,13 +1,13 @@
# 座椅控制器 CAN/HIL 测试规范
版本:2.3(2026-07-30)
版本:3.0(2026-08-03)
对象:`Seat_Controller_demo`、`量程测试脚本`
MCU:CYT4BFBCJE / CY_MCU_rev_d / Cortex-M0+ Flash
总线:Classic CAN,11-bit 标准帧,500 kbit/s,DLC=8,Intel/Little-endian
## 1. 目的与范围
本规范用于验证统一控制器的座椅通风、靠背加热、四轴移动、自动标定、反馈周期和安全停机。测试分为静态构建、无机械动作 CAN 检查、单轴低风险动作、多轴并发、全行程标定和故障注入。未经安全确认不得直接跳到多轴或标定阶段。
本规范用于验证统一控制器的座椅通风、靠背加热、四轴移动、原生动态模式、自动标定、反馈周期和安全停机。测试分为静态构建、无机械动作 CAN 检查、单轴低风险动作、多轴并发、动态时序、全行程标定和故障注入。未经安全确认不得直接跳到多轴、动态模式或标定阶段。
配套文件:
@@ -66,20 +66,32 @@ Byte0=Opcode;Byte1=`0x10 | MotorID | (Direction<<2)`;Byte2..3=Sequence;Byt
| `0B` | 已知量程回最近端 | `0x4B52` | 最近端寻找10~600 s |
| `0C` | 查询状态 | 0 | 0 |
| `0D` | 清故障 | `0xC33C` | 0 |
| `0E` | 启动原生动态模式 | ModeID:1木马模式、2动态摇篮 | 0 |
| `0F` | 停止原生动态模式 | 0 | 0 |
### 3.3 移动反馈
- `0x301`:Byte0 低半字节 ACK、高半字节 SystemState;Byte1 RejectReason;Byte2..3 Sequence;Byte4 Opcode;Byte5 ActiveMask/抑制/标定/故障;Byte6 FaultedMask/PositionValidMask;Byte7 低半字节 Alive、高半字节 FirmwareRevision(当前为 2)。
- `0x301`:Byte0 低半字节 ACK、高半字节 SystemState;Byte1 RejectReason;Byte2..3 Sequence;Byte4 Opcode;Byte5 ActiveMask/抑制/动态/标定/故障;Byte6 FaultedMask/PositionValidMask;Byte7 低半字节 Alive、高半字节 FirmwareRevision(当前为 13)。
- `0x302`:Byte0 MotorID;Byte1 TaskState/Result;Byte2 方向、Active、PositionValid、端点、堵转和驱动故障;Byte3..4 Sequence;Byte5..6 归一化位置;Byte7 驱动等级。
- `0x303`:Byte0 MotorID;Byte1 LastEvent/FaultFlags;Byte2..3 HallCount;Byte4..5 MaxHallCount;Byte6 Hall速度(0.5 pulse/s/LSB);Byte7 Retry/Alive。
- `0x306`:原生动态模式状态,周期100 ms;Byte0 ModeID,Byte1 State(0 IDLE、1 ENTRY、2 RUNNING、3 FAULT),Byte2 Phase(1 ENTRY、2 HORSE_FORWARD、3 HORSE_BACKWARD、5 CRADLE_FORWARD、6 CRADLE_RETURN;4保留),Byte3低半字节ActiveMask、高半字节FaultMask,Byte4..5 CycleIndex,Byte6..7 PhaseElapsedMs。
ACK:0 NONE、1 ACCEPTED、2 REJECTED、3 DUPLICATE、4 STOP_ALL_ACCEPTED。RejectReason 与 TaskState/Result 以移动矩阵“枚举定义”页为准。`0x301` Byte5 bit5 当前固定为0,预留给未来通信监控。
ACK:0 NONE、1 ACCEPTED、2 REJECTED、3 DUPLICATE、4 STOP_ALL_ACCEPTED。RejectReason 与 TaskState/Result 以移动矩阵“枚举定义”页为准。`0x301` Byte5 bit5 表示原生动态模式正在执行。
RejectReason:0 NONE、1 BAD_DLC、2 BAD_VERSION、3 BAD_OPCODE、4 BAD_MOTOR、5 BAD_DIRECTION、6 PARAM_RANGE、7 POSITION_INVALID、8 SEQUENCE_OLD、9 BUSY_SAME_MOTOR、10 INTERLOCK、11 FAULT_ACTIVE、12 SERVICE_MODE_REQUIRED、13 CAL_DATA_INVALID、14 INTERNAL_ERROR。
TaskState:0 IDLE、1 ACCEPTED、2 POSITION_RUN、3 TIMED_RUN、4 CALIBRATION、5 STOPPING、6 COMPLETE、7 FAULT。Result:0 NONE、1 RUNNING、2 OK、3 STOPPED_BY_CMD、4 ENDPOINT_REACHED、5 STALL、6 TIMEOUT、7 DRIVER_FAULT、8 HALL_FAULT、9 INVALID_CMD、10 INTERLOCKED、11 CALIBRATION_FAILED。
### 3.4 本地位置记忆(SMV8)
### 3.4 原生动态模式时序
- ModeID 1“木马模式”revision 13:入口 Hall `[320,190,500,430]`;向前俯摆到`[270,250,460,400]`,再向后仰摆到`[370,130,540,460]`,随后在两个端点间连续往复。M1峰峰行程120 Hall;M0峰峰行程100 Hall,以更大的水平速度和行程补偿M1斜向运动;每阶段先到目标前8 Hall会合,再以50% Duty完成等距末段,单段15 s仅作为无到位安全上限。
- ModeID 2“动态摇篮”revision 13:Step 1入口 Hall `[391,140,405,404]`;Step 1→Step 2四轴从阶段开始全程联动并以位置会合方式到`[246,281,701,494]`;Step 2→Step 3同步返回`[391,140,405,404]`。阶段完成条件是四轴全部目标到位,不再声明或等待固定30 s,也不允许任一轴延迟到末段追赶。
- 两种模式的每个运动阶段均在同一调度批次启动四轴;木马巡航占空比M0/M1/M2/M3为90/90/90/50%,动态摇篮为50/50/90/50%,到会合点后再同步执行等距末段。起动与到位离散均要求不大于1.0 s。
- Step 3与下一轮Step 1的坐标完全相同,因此该边界物理位移为0;ECU原子递增CycleIndex并立即同步启动下一轮Step 1→Step 2,不伪造一次零位移电机启动。
- 四轴命令同时启动,动态目标容差为±3 Hall。底层5 s无运动、堵转、驱动故障和最终到位超时仍会立即或在其既定保护时间内全停,并令`0x306.State=FAULT`。
- `STOP_DYNAMIC(0x0F)`和`STOP_ALL(0x09)`都必须终止循环;未收到停止指令时,ECU从启动命令起累计600 s后自动执行同等安全停止。正常或到时停止后`0x306`回到全零IDLE。
### 3.5 本地位置记忆(SMV8)
- 两个128-byte Work Flash 小扇区交替保存,记录含 Magic/Version、单调 Sequence、Clean标志、四轴绝对 Hall、MaxHall、最大速度、累计 Hall 上升沿、物理绝对量、PositionValid 和 CRC32。
- 每个动作命令真正启动前先写入较新 dirty 记录;写完才允许 PWM 启动。若此时掉电,新记录阻止旧 clean 位置被恢复。
@@ -109,7 +121,8 @@ TaskState:0 IDLE、1 ACCEPTED、2 POSITION_RUN、3 TIMED_RUN、4 CALIBRATION
5. 每轴 300~500 ms 正反向点动并目视签字确认映射。
6. 单轴定时、同轴换向、单轴停止和四轴并发互不干涉。
7. 已知量程回最近端、每次移动结束保存、正常掉电恢复、运动中掉电失效及恢复、绝对/相对位置精度。
8. 堵转、驱动故障、CAN bus-off、C-SPY 结束和看门狗路径。
8. `07_dynamic_modes.py`:完整验证两个原生动态模式的命令ACK、0x306阶段顺序、阶段时长、四轴启动/到位离散度、自动跨周期和停止。
9. 堵转、驱动故障、CAN bus-off、C-SPY 结束和看门狗路径。
每个阶段开始前先发送全停,结束后再次发送全停。任何实物动作与预期不符时停止后续用例。
@@ -152,7 +165,20 @@ TaskState:0 IDLE、1 ACCEPTED、2 POSITION_RUN、3 TIMED_RUN、4 CALIBRATION
- 重复帧只返回 DUPLICATE,不产生第二次动作;旧 Sequence 拒绝且输出不变;旧 Sequence 的 STOP_ALL 仍必须执行。
- 未标定时绝对/相对位置命令拒绝,CurrentPosition=`0xFFFF`;定时命令仍可用。
### 6.4 标定与位置
### 6.4 动态模式
- 动态摇篮revision 13不使用固定30 s/60 s完成条件;Step 1→2和Step 2→3只在四轴全部进入±3 Hall目标带后切换,单段保护上限30 s。四轴启动离散度和最终到位离散度均不大于1.0 s;水平与靠背均达到25%行程时,坐盆归一化进度必须不小于15%,用于阻止末段追赶回归。
- 木马模式不设置停驻阶段;前俯与后仰只在四轴全部进入±3 Hall目标带后反向,单段保护上限15 s。入口只用于建立中间姿态,验收必须额外观察至少一个后仰→前俯→后仰的完整稳态循环。
- 台架轴在标称时间尚未到位时,只有Hall正常进展且在30 s窗口内到达精确目标才可继续循环;不得虚报到位或省略目标。无Hall进展、堵转、驱动故障或超过窗口必须进入FAULT并全停。
- 2026-08-04已验证公共动态运行时单次连续运行600.024 s后自动回到全零IDLE且无FAULT;循环中`STOP_DYNAMIC(0x0F)`也可立即安全停止。该公共安全逻辑由两个当前模式共用。
- 当前台架动态摇篮在向前收拢时由M1坐盆轴触发STALL;`0x306`故障位为`0x2`。独立单轴90%固定占空比复测同样在约45.2%(Hall约169)失速,无法到达75%(281 pulse)目标,证明阻断点不在动态调度。修复机构卡滞、负载、供电/驱动或Hall问题前,不得屏蔽堵转保护、降低需求目标或伪造循环PASS。
- 2026-08-04 revision 9重新设计并先执行M1已知量程回近端:Hall 169连续回到0,Near/PositionValid有效且MaxHall保持374。随后Step 1→2实测17.329 s,四轴启动/到位离散0.077/0.338 s;Step 2→3实测15.155 s,离散0.079/0.495 s;目标分别精确到`[246,281,701,494]`和`[391,140,405,405]`,CycleIndex进入1且无FAULT。下一轮四轴重新启动离散0.082 s,停止后`0x306`全零,PASS。
- 2026-08-04 revision 10移除M1延迟追赶后,Step 1→2在水平进度25.5%、靠背35.3%时坐盆已达44.0%;Step 2→3在水平25.5%、靠背28.7%时坐盆已达39.0%。启动离散0.070/0.050 s,到位离散0.342/0.339 s,目标精确、下一轮启动离散0.051 s、无FAULT、停止全零,PASS。
- 2026-08-04 revision 11木马模式实测稳态前俯/后仰启动离散0.080/0.079 s,到位离散0.234/0.232 s;水平到25%时坐盆已达41.7%/36.7%,目标误差不超过1 Hall,稳态完整周期约9.0 s。自动进入下一轮且停止后`0x306`全零,PASS。
- 2026-08-04 revision 12木马模式把坐盆峰峰行程由60增至120 Hall,实测稳态前俯/后仰启动离散0.054/0.081 s,到位离散0.389/0.182 s;水平到25%时坐盆已达46.6%/35.8%,目标误差不超过2 Hall,稳态完整周期约9.2 s。自动进入下一轮、无FAULT且停止后`0x306`全零,PASS。
- 2026-08-04 revision 13木马模式把M0峰峰行程由60增至100 Hall、巡航Duty由50%增至90%。按实测MaxHall=499换算水平峰峰行程约43.1 mm,扣除M1斜向行程估算的16.4 mm反向水平分量后净水平摆幅约26.7 mm。稳态前俯/后仰启动离散0.077/0.052 s,到位离散0.337/0.182 s;巡航检查点四轴均持续推进,目标误差不超过1 Hall,自动续环、无FAULT且停止全零,PASS。
### 6.5 标定与位置
- 每轴按负端→正端完成,标定中 `0x301 SystemState=CALIBRATION`、Byte5 bit6=1,`0x302 TaskState=CALIBRATION`,周期反馈不中断。
- MaxHallCount 软件允许范围(实测值的70%~130%):M0 336~624,M1 261~486,M2 641~1192,M3 452~841,且大于等于8。台架目标值分别为480、374、917、647;超出允许范围标定失败。
@@ -74,6 +74,10 @@
#define MOTOR_SPEED_CONTROL_START_DELAY_MS (500ul)
#define MOTOR_SPEED_CONTROL_CHECK_MS (100ul)
#define MOTOR_SPEED_CONTROL_WINDOW_MS (500ul)
#define MOTOR_SPEED_PROGRESS_CONTROL_START_MS (100ul)
#define MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT (5u)
#define MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT (0u)
#define MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES (1ul)
#define MOTOR_SPEED_CONTROL_HISTORY_SAMPLES \
(MOTOR_SPEED_CONTROL_WINDOW_MS / MOTOR_SPEED_CONTROL_CHECK_MS)
#define MOTOR_SPEED_TOLERANCE_PERCENT (10ul)
@@ -210,6 +214,11 @@
#error "MOTOR_SPEED_CONTROL_WINDOW_MS must be a multiple of the speed check period."
#endif
#if ((MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT == 0u) || \
(MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT >= MOTOR_SPEED_MIN_DUTY_PERCENT))
#error "Adaptive speed duty must be below the fixed-duty command minimum."
#endif
#if ((MOTOR_PWM_DUTY_PERCENT < MOTOR_SPEED_MIN_DUTY_PERCENT) || \
(MOTOR_PWM_DUTY_PERCENT > MOTOR_SPEED_MAX_DUTY_PERCENT))
#error "The default motor duty must stay inside the speed-control duty limits."
@@ -711,9 +720,9 @@ static bool Motor_CalculateSpeedPlan(uint32_t motorIndex,
profile->speedStepPpsX100 - 1ul) /
profile->speedStepPpsX100;
initialDuty = dutySteps * MOTOR_SPEED_DUTY_STEP_PERCENT;
if(initialDuty < MOTOR_SPEED_MIN_DUTY_PERCENT)
if(initialDuty < MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT)
{
initialDuty = MOTOR_SPEED_MIN_DUTY_PERCENT;
initialDuty = MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT;
}
else if(initialDuty > MOTOR_SPEED_MAX_DUTY_PERCENT)
{
@@ -1187,6 +1196,21 @@ static void Motor_SafetyTaskFromIsr(void)
continue;
}
/* Long synchronized trajectories use pulse-density control whenever
* their requested speed is below the loaded mechanism's proven 50%
* breakaway duty. A zero-duty pause is intentional de-energization,
* not a no-motion fault. The absolute task/phase deadline still
* bounds the pause, and the activity window is reset when drive
* resumes. */
if((false != state->speedControlEnabled) &&
(state->speedCurrentDutyPercent ==
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT))
{
state->lowHallTracking = false;
state->stallCurrentTracking = false;
continue;
}
if(false != state->calibrating)
{
state->lowHallTracking = false;
@@ -1804,6 +1828,14 @@ static void Motor_CommitPreparedHardwareStartUnsafe(uint32_t motorIndex)
(false == state->calibrating))
{
dutyPercent = state->speedInitialDutyPercent;
/* Dynamic trajectories may need a low steady-state PWM to stretch a
* short Hall travel over tens of seconds. Give every loaded axis the
* proven 50% breakaway pulse first; after the 500 ms measurement delay
* the closed loop may ramp below the public fixed-duty command floor. */
if(dutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT)
{
dutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
}
}
state->speedCurrentDutyPercent = dutyPercent;
Motor_ResetSpeedMeasurementUnsafe(state);
@@ -2302,6 +2334,9 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
uint32_t elapsedMs;
uint32_t actualPpsX100;
uint32_t targetPpsX100;
uint32_t planElapsedMs;
uint32_t plannedProgressPulses;
uint32_t actualProgressPulses;
uint32_t savedIntrStatus;
uint8_t historyIndex;
uint8_t newDutyPercent;
@@ -2347,6 +2382,39 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
state->speedLastSampleTicks = nowTicks;
elapsedTicks = (uint32_t)(nowTicks - baseTicks);
planElapsedMs = MOTOR_TICKS_TO_MS(
(uint32_t)(nowTicks - state->speedControlStartTicks));
if(planElapsedMs > state->speedDurationMs)
{
planElapsedMs = state->speedDurationMs;
}
plannedProgressPulses = (uint32_t)
(((uint64_t)state->speedPlannedHallPulses * planElapsedMs +
(state->speedDurationMs / 2ul)) / state->speedDurationMs);
actualProgressPulses = rawEdges - state->speedBaselineRawEdges;
/* Loaded axes need a short 50% breakaway pulse, but waiting for the full
* 500 ms speed window lets a short-travel axis build an unrecoverable
* lead. Position progress is meaningful from the first 100 ms sample,
* so de-energize an already-ahead axis immediately. Driving it at an
* arbitrary sub-breakaway duty can create current without Hall progress
* and was the cause of false cradle STALLs. */
if(((uint32_t)(nowTicks - state->speedControlStartTicks) >=
MOTOR_MS_TO_TICKS(MOTOR_SPEED_PROGRESS_CONTROL_START_MS)) &&
(actualProgressPulses >
(plannedProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES)))
{
if(state->speedCurrentDutyPercent !=
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT)
{
state->speedCurrentDutyPercent =
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT;
Motor_ApplyDutyPercent(motorIndex,
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT);
}
Cy_SysLib_ExitCriticalSection(savedIntrStatus);
return;
}
if(((uint32_t)(nowTicks - state->speedControlStartTicks) <
MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_START_DELAY_MS)) ||
(elapsedTicks < MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_WINDOW_MS)))
@@ -2369,11 +2437,43 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
newDutyPercent = state->speedCurrentDutyPercent;
state->speedActualPpsX100 = actualPpsX100;
if((((uint64_t)actualPpsX100) * 100ull) <
(((uint64_t)targetPpsX100) *
(100ul - MOTOR_SPEED_TOLERANCE_PERCENT)))
/* Speed-only regulation cannot synchronize very short and long axes: the
* loaded motor breakaway pulse puts a short travel several seconds ahead
* even when its eventual speed is correct. Track the time-position line
* as well. An axis ahead of the line drops immediately to the adaptive
* floor; an axis behind it ramps up. Within one Hall pulse, the ordinary
* measured-speed loop keeps each energized burst bounded. */
if(actualProgressPulses >
(plannedProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES))
{
if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT)
newDutyPercent = MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT;
}
else if((actualProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES) <
plannedProgressPulses)
{
if(newDutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
}
else if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT)
{
newDutyPercent = (uint8_t)
(newDutyPercent + MOTOR_SPEED_DUTY_STEP_PERCENT);
if(newDutyPercent > MOTOR_SPEED_MAX_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_MAX_DUTY_PERCENT;
}
}
}
else if((((uint64_t)actualPpsX100) * 100ull) <
(((uint64_t)targetPpsX100) *
(100ul - MOTOR_SPEED_TOLERANCE_PERCENT)))
{
if(newDutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
}
else if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT)
{
newDutyPercent = (uint8_t)
(newDutyPercent + MOTOR_SPEED_DUTY_STEP_PERCENT);
@@ -2400,6 +2500,10 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
}
}
else if(newDutyPercent != MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT;
}
}
else
{
@@ -2408,6 +2512,15 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
if(newDutyPercent != state->speedCurrentDutyPercent)
{
if((state->speedCurrentDutyPercent ==
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT) &&
(newDutyPercent >= MOTOR_SPEED_MIN_DUTY_PERCENT))
{
/* Resume gets a fresh 500 ms activity window, matching a normal
* hardware start. This prevents the first loaded-current sample
* from being compared with the intentionally quiet pause. */
Motor_ResetActivityWindowUnsafe(state);
}
state->speedCurrentDutyPercent = newDutyPercent;
Motor_ApplyDutyPercent(motorIndex, newDutyPercent);
}
@@ -2275,6 +2275,12 @@
<file>
<name>$PROJ_DIR$\..\App\seat_move_application.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\App\seat_dynamic_mode.c</name>
</file>
<file>
<name>$PROJ_DIR$\..\App\seat_dynamic_mode.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\App\seat_move_store.c</name>
</file>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,577 @@
"""Revision-13 native dynamic-mode CAN/Hall verification for ZCANPRO.
Run this file from the ZCANPRO extension-script window after programming the
repository HEX. It validates one complete rocking-horse cycle and one
complete cradle cycle, then stops safely. STOP_ALL and thermal-off are always
attempted from ``finally``.
"""
import importlib
import os
import sys
import time
# ZCANPRO keeps one interpreter for scripts from multiple worktrees. Loading
# by a unique module name prevents an older desktop copy of ``hil_common``
# from winning through ``sys.modules`` or the retained import search path.
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
_COMMON_PATH = os.path.join(_BASE_DIR, "hil_common.py")
_COMMON_NAME = "cognitive_agent_dynamic_hil_common_v13"
_COMMON_SPEC = importlib.util.spec_from_file_location(_COMMON_NAME, _COMMON_PATH)
if _COMMON_SPEC is None or _COMMON_SPEC.loader is None:
raise RuntimeError("Cannot load repository hil_common.py")
_hil_common = importlib.util.module_from_spec(_COMMON_SPEC)
sys.modules[_COMMON_NAME] = _hil_common
_COMMON_SPEC.loader.exec_module(_hil_common)
HilSession = _hil_common.HilSession
command_payload = _hil_common.command_payload
parse_302 = _hil_common.parse_302
parse_303 = _hil_common.parse_303
parse_306 = _hil_common.parse_306
SELECTED_MODES = ("ROCKING_HORSE", "DYNAMIC_CRADLE")
EXPECTED_REVISION = 13
EXPECTED_MAX_HALL = (480, 374, 917, 647)
HALL_TOLERANCE = 3
PHASE_DURATION_TOLERANCE_S = 0.55
ARRIVAL_SPREAD_TOLERANCE_S = 1.00
ARRIVAL_EARLY_TOLERANCE_S = 1.00
LINKED_REFERENCE_PROGRESS = 0.25
LINKED_MIN_CUSHION_PROGRESS = 0.15
MODE_DEFINITIONS = {
"ROCKING_HORSE": {
"mode_id": 1,
"sequence": 0x0700,
"first_phase": 2,
"cycle_duration_ms": None,
"entry_target": (320, 190, 500, 430),
"phase_order": (2, 3),
"phases": {
2: {
"name": "rock_forward",
"completion": "all_targets_reached",
"timeout_ms": 15000,
"target": (270, 250, 460, 400),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
3: {
"name": "rock_backward",
"completion": "all_targets_reached",
"timeout_ms": 15000,
"target": (370, 130, 540, 460),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
},
},
"DYNAMIC_CRADLE": {
"mode_id": 2,
"sequence": 0x0720,
"first_phase": 5,
"cycle_duration_ms": None,
"entry_target": (391, 140, 405, 404),
"phase_order": (5, 6),
"phases": {
5: {
"name": "forward",
"completion": "all_targets_reached",
"timeout_ms": 30000,
"target": (246, 281, 701, 494),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
6: {
"name": "return",
"completion": "all_targets_reached",
"timeout_ms": 30000,
"target": (391, 140, 405, 404),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
},
},
}
session = None
def z_notify(event_type, obj):
del obj
if event_type == "stop" and session is not None:
session.request_stop()
def hall_snapshot(s, capture_s=0.35):
if capture_s > 0:
s.capture_state(capture_s, clear=False)
values = []
for motor in range(4):
diagnostic = s.latest.get((0x303, motor))
values.append(None if diagnostic is None else diagnostic["hall"])
return values
def hall_match(actual, expected):
return all(
value is not None and abs(value - target) <= HALL_TOLERANCE
for value, target in zip(actual, expected)
)
def wait_idle_status(s, timeout_s=3.0):
deadline = time.monotonic() + timeout_s
latest = None
while time.monotonic() < deadline and not s.stop_requested:
for can_id, data, frame in s.receive():
del frame
if can_id != 0x306:
continue
latest = parse_306(data)
if latest is not None and latest["mode_id"] == 0 and \
latest["state"] == 0:
return latest
time.sleep(0.005)
return latest
def stop_dynamic(s, sequence, label):
response = s.send_command(
command_payload(0x0F, 0, 0, sequence),
expected_ack=1,
expected_reject=0,
timeout_s=2.0,
note=label + " STOP_DYNAMIC",
)
idle = wait_idle_status(s)
passed = response is not None and response["ack"] == 1 and \
response["reject"] == 0 and idle is not None and \
idle["mode_id"] == 0 and idle["state"] == 0
s.record(label + "-STOP", passed,
{"ack": response, "dynamic_status": idle},
"Opcode 0x0F accepted and 0x306 returns IDLE")
return passed
def new_segment(status, observed_at, start_hall):
phase_start = observed_at - status["phase_elapsed_ms"] / 1000.0
return {
"cycle_index": status["cycle_index"],
"phase": status["phase"],
"phase_start": phase_start,
"first_observed": observed_at,
"last_status": dict(status),
"first_active": {},
"arrival": {},
"arrival_source": {},
"terminal": {},
"target_hall_at_end": None,
"start_hall": list(start_hall),
"latest_hall": list(start_hall),
"cruise_checkpoint": None,
}
def observe_cycle(s, label, definition):
expected_order = definition["phase_order"]
first_phase = definition["first_phase"]
segments = []
current = None
started = False
deadline = time.monotonic() + 115.0
failure_status = None
while time.monotonic() < deadline and not s.stop_requested:
frames = s.receive()
if not frames:
time.sleep(0.005)
continue
now = time.monotonic()
for can_id, data, frame in frames:
del frame
if can_id == 0x306:
status = parse_306(data)
if status is None or status["mode_id"] != definition["mode_id"]:
continue
if status["state"] == 3:
failure_status = status
break
phase = status["phase"]
if phase not in definition["phases"]:
continue
key = (status["cycle_index"], phase)
current_key = None if current is None else (
current["cycle_index"], current["phase"])
if key != current_key:
if current is not None:
current["target_hall_at_end"] = hall_snapshot(s, 0)
segments.append(current)
current = new_segment(status, now, hall_snapshot(s, 0))
if phase == first_phase:
if not started:
started = True
elif status["cycle_index"] >= 1:
# Capture the returned Step-1 pose now, before the
# next Step-1 -> Step-2 movement changes Hall.
current["target_hall_at_end"] = hall_snapshot(s, 0)
elif current is not None:
current["last_status"] = dict(status)
elif can_id == 0x302 and current is not None:
motor = parse_302(data)
if motor is None or motor["motor"] >= 4:
continue
motor_id = motor["motor"]
if motor["active"]:
current["first_active"].setdefault(motor_id, now)
if current["cycle_index"] >= 1 and \
current["phase"] == first_phase and all(
axis in current["first_active"]
for axis in definition["phases"][first_phase]["moving_axes"]
):
# Step 3 and Step 1 have the same Hall pose, so their
# boundary has no physical displacement. The next
# observable requirement is that all four axes launch
# the following Step-1 -> Step-2 leg synchronously.
segments.append(current)
current = None
break
elif motor_id in current["first_active"]:
current["terminal"][motor_id] = {
"time": now,
"status": dict(motor),
}
if definition["phases"][current["phase"]].get(
"completion") != "all_targets_reached":
current["arrival"].setdefault(motor_id, now)
current["arrival_source"].setdefault(
motor_id, "0x302 COMPLETE")
elif can_id == 0x303:
diagnostic = parse_303(data)
if diagnostic is not None and diagnostic["fault_flags"]:
failure_status = diagnostic
break
if diagnostic is not None and current is not None:
motor_id = diagnostic["motor"]
current["latest_hall"][motor_id] = diagnostic["hall"]
target = definition["phases"][current["phase"]]["target"][motor_id]
if motor_id in current["first_active"] and \
abs(diagnostic["hall"] - target) <= HALL_TOLERANCE:
# A phase may roll into the next command before the
# staggered 0x302 slot publishes COMPLETE for the last
# axis. The independent 0x303 Hall target is equivalent
# physical arrival evidence and avoids a telemetry race.
current["arrival"].setdefault(motor_id, now)
current["arrival_source"].setdefault(
motor_id, "0x303 Hall target")
phase_definition = definition["phases"][current["phase"]]
if phase_definition.get("continuous_four_axis_cruise") and \
current["cruise_checkpoint"] is None:
progress = []
for axis in range(4):
start = current["start_hall"][axis]
end = phase_definition["target"][axis]
hall = current["latest_hall"][axis]
if start is None or hall is None:
progress.append(0.0)
else:
travel = abs(end - start)
progress.append(
1.0 if travel == 0 else
min(1.0, abs(hall - start) / float(travel))
)
if progress[0] >= LINKED_REFERENCE_PROGRESS and \
progress[2] >= LINKED_REFERENCE_PROGRESS:
current["cruise_checkpoint"] = {
"elapsed_s": now - current["phase_start"],
"normalized_progress": progress,
"hall": list(current["latest_hall"]),
}
if failure_status is not None:
break
if current is None and started and segments and \
segments[-1]["cycle_index"] >= 1 and \
segments[-1]["phase"] == first_phase:
break
complete = (
failure_status is None
and len(segments) == len(expected_order) + 1
and tuple(item["phase"] for item in segments[:-1]) == expected_order
and segments[-1]["phase"] == first_phase
and segments[-1]["cycle_index"] >= 1
)
return complete, segments, failure_status
def validate_segments(s, label, definition, segments):
phase_results = []
for index, segment in enumerate(segments[:-1]):
phase_definition = definition["phases"][segment["phase"]]
next_segment = segments[index + 1]
observed_duration = next_segment["phase_start"] - segment["phase_start"]
target_completion = phase_definition.get("completion") == \
"all_targets_reached"
if target_completion:
expected_duration = None
arrival_grace = 0.0
timeout_s = phase_definition["timeout_ms"] / 1000.0
duration_ok = 0.0 < observed_duration <= (
timeout_s + PHASE_DURATION_TOLERANCE_S)
else:
expected_duration = phase_definition["duration_ms"] / 1000.0
arrival_grace = phase_definition["arrival_grace_ms"] / 1000.0
timeout_s = expected_duration + arrival_grace
duration_ok = (
observed_duration >= expected_duration -
PHASE_DURATION_TOLERANCE_S
and observed_duration <= expected_duration + arrival_grace +
PHASE_DURATION_TOLERANCE_S
)
moving_axes = phase_definition["moving_axes"]
terminals = segment["terminal"]
arrivals = segment["arrival"]
active_starts = segment["first_active"]
if moving_axes:
all_active_seen = all(axis in active_starts for axis in moving_axes)
all_arrival_seen = all(axis in arrivals for axis in moving_axes)
start_times = [active_starts[axis] for axis in moving_axes
if axis in active_starts]
terminal_times = [arrivals[axis] for axis in moving_axes
if axis in arrivals]
start_spread = max(start_times) - min(start_times) \
if len(start_times) == len(moving_axes) else None
arrival_spread = max(terminal_times) - min(terminal_times) \
if all_arrival_seen else None
latest_arrival = max(terminal_times) - segment["phase_start"] \
if all_arrival_seen else None
terminal_status_ok = all_arrival_seen if target_completion else all(
value["status"]["task"] == 6
and value["status"]["result"] == 2
for value in terminals.values()
) and all_arrival_seen
used_arrival_grace = False if target_completion else (
observed_duration > expected_duration +
PHASE_DURATION_TOLERANCE_S
)
sync_ok = (
all_active_seen
and all_arrival_seen
and start_spread <= ARRIVAL_SPREAD_TOLERANCE_S
# When the loaded bench cannot meet the nominal arrival time,
# faster axes stop at target while the healthy slow axis uses
# its bounded grace. Simultaneous start remains mandatory;
# arrival spread is only an exact-timing assertion when the
# phase did not need that grace.
and arrival_spread <= ARRIVAL_SPREAD_TOLERANCE_S
and (target_completion or latest_arrival >=
expected_duration - ARRIVAL_EARLY_TOLERANCE_S)
and latest_arrival <= timeout_s + PHASE_DURATION_TOLERANCE_S
and terminal_status_ok
)
else:
start_spread = 0.0
arrival_spread = 0.0
latest_arrival = None
used_arrival_grace = False
sync_ok = not active_starts and segment["last_status"]["active_mask"] == 0
target_ok = hall_match(
segment["target_hall_at_end"], phase_definition["target"]
)
checkpoint = segment["cruise_checkpoint"]
if phase_definition.get("continuous_four_axis_cruise"):
cruise_ok = checkpoint is not None and \
checkpoint["normalized_progress"][1] >= \
LINKED_MIN_CUSHION_PROGRESS
else:
cruise_ok = True
passed = duration_ok and sync_ok and target_ok and cruise_ok
details = {
"phase": phase_definition["name"],
"cycle_index": segment["cycle_index"],
"expected_duration_s": expected_duration,
"arrival_grace_s": arrival_grace,
"completion": phase_definition.get("completion", "elapsed_time"),
"timeout_s": timeout_s,
"observed_duration_s": round(observed_duration, 3),
"used_arrival_grace": used_arrival_grace,
"moving_axes": list(moving_axes),
"active_start_spread_s": None if start_spread is None else round(start_spread, 3),
"arrival_spread_s": None if arrival_spread is None else round(arrival_spread, 3),
"latest_arrival_from_phase_start_s": (
None if latest_arrival is None else round(latest_arrival, 3)
),
"terminal_status": {
str(axis): terminals[axis]["status"]
for axis in terminals
},
"arrival_source": {
str(axis): segment["arrival_source"][axis]
for axis in arrivals
},
"actual_hall_at_phase_end": segment["target_hall_at_end"],
"expected_hall_at_phase_end": list(phase_definition["target"]),
"duration_ok": duration_ok,
"synchronization_ok": sync_ok,
"target_ok": target_ok,
"continuous_four_axis_cruise_ok": cruise_ok,
"cruise_checkpoint": checkpoint,
}
s.record("%s-%s" % (label, phase_definition["name"].upper()),
passed, details,
"continuous cruise, synchronized start/arrival, and Hall target within tolerance")
phase_results.append(passed)
cycle_duration = segments[-1]["phase_start"] - segments[0]["phase_start"]
if definition["cycle_duration_ms"] is None:
cycle_expected = None
cycle_grace = 0.0
cycle_limit = sum(
definition["phases"][phase]["timeout_ms"]
for phase in definition["phase_order"]
) / 1000.0
cycle_ok = 0.0 < cycle_duration <= cycle_limit + \
PHASE_DURATION_TOLERANCE_S
else:
cycle_expected = definition["cycle_duration_ms"] / 1000.0
cycle_grace = sum(
definition["phases"][phase]["arrival_grace_ms"]
for phase in definition["phase_order"]
) / 1000.0
cycle_tolerance = PHASE_DURATION_TOLERANCE_S * \
len(definition["phase_order"])
cycle_limit = cycle_expected + cycle_grace
cycle_ok = (
cycle_duration >= cycle_expected - cycle_tolerance
and cycle_duration <= cycle_limit + cycle_tolerance
)
repeat_hall = segments[-1]["target_hall_at_end"]
repeat_starts = segments[-1]["first_active"]
repeat_axes = definition["phases"][first_phase]["moving_axes"]
repeat_start_values = [repeat_starts[axis] for axis in repeat_axes
if axis in repeat_starts]
repeat_start_spread = max(repeat_start_values) - min(repeat_start_values) \
if len(repeat_start_values) == len(repeat_axes) else None
repeat_ok = hall_match(repeat_hall, definition["entry_target"]) and \
repeat_start_spread is not None and \
repeat_start_spread <= ARRIVAL_SPREAD_TOLERANCE_S
s.record(label + "-CYCLE-REPEAT", cycle_ok and repeat_ok,
{"expected_cycle_s": cycle_expected,
"maximum_cycle_s": cycle_limit,
"observed_cycle_s": round(cycle_duration, 3),
"cycle_index": segments[-1]["cycle_index"],
"actual_repeat_hall": repeat_hall,
"expected_repeat_hall": list(definition["entry_target"]),
"next_cycle_active_start_spread_s": (
None if repeat_start_spread is None else
round(repeat_start_spread, 3)
)},
"one complete cycle reaches Step 1 and synchronously starts the next Step 2 leg")
return all(phase_results) and cycle_ok and repeat_ok
def run_mode(s, label, definition):
start = s.send_command(
command_payload(0x0E, 0, 0, definition["sequence"],
definition["mode_id"], 0),
expected_ack=1,
expected_reject=0,
timeout_s=2.0,
note=label + " START_DYNAMIC_MODE",
)
accepted = start is not None and start["ack"] == 1 and \
start["reject"] == 0 and start["opcode"] == 0x0E
s.record(label + "-START", accepted, start,
"Opcode 0x0E accepted for native mode_id=%d" % definition["mode_id"])
if not accepted:
return False
complete, segments, failure = observe_cycle(s, label, definition)
s.record(label + "-PHASE-SEQUENCE", complete,
{"observed": [
{"cycle_index": item["cycle_index"], "phase": item["phase"]}
for item in segments
], "failure": failure},
"0x306 reports one ordered cycle and automatic repeat")
validated = complete and validate_segments(s, label, definition, segments)
stopped = stop_dynamic(s, definition["sequence"] + 1, label)
return validated and stopped
def z_main():
global session
session = None
results = {}
try:
selected = tuple(str(value).strip().upper() for value in SELECTED_MODES)
unknown = [value for value in selected if value not in MODE_DEFINITIONS]
if unknown:
raise RuntimeError("Unknown SELECTED_MODES=" + repr(unknown))
session = HilSession("07_dynamic_modes")
s = session
s.write_operator_template([
"Secure the empty seat bench and keep hardware E-stop reachable.",
"ROCKING_HORSE pitches forward/back smoothly with all axes engaged: PASS/FAIL ___",
"DYNAMIC_CRADLE moves forward/returns smoothly without steps: PASS/FAIL ___",
"No unintended thermal output, noise, collision, or cable strain: PASS/FAIL ___",
])
s.thermal_off()
s.stop_all(0)
initial = s.capture_state(2.0)
system = initial["system"]
ready = (
system is not None
and system["firmware_revision"] == EXPECTED_REVISION
and system["position_valid_mask"] == 0x0F
and system["active_mask"] == 0
and system["faulted_mask"] == 0
and all(
item is not None
and abs(item["max_hall"] - EXPECTED_MAX_HALL[index])
<= max(4, round(EXPECTED_MAX_HALL[index] * 0.05))
for index, item in enumerate(initial["diagnostics"])
)
)
s.record("DYNAMIC-PRECONDITION", ready, initial,
"revision 13, four valid axes, no active/fault, confirmed ranges")
if not ready:
for label in selected:
s.record(label + "-MODE", False, "not executed",
"dynamic precondition failed", status="NOT_RUN")
return
for label in selected:
if s.stop_requested:
break
results[label] = run_mode(s, label, MODE_DEFINITIONS[label])
# A native-mode failure is isolated by STOP_DYNAMIC inside
# run_mode(). Continue with the other definition so one
# physically infeasible trajectory cannot hide its HIL result.
time.sleep(0.5)
except Exception as error:
if session is not None:
session.record("SCRIPT", False, repr(error), "unhandled exception")
finally:
if session is not None:
try:
session.stop_all(0)
session.thermal_off()
except Exception as error:
session.log("FINAL_SAFE_OFF_ERROR " + repr(error))
final_state = None
try:
final_state = session.capture_state(1.0)
except Exception as error:
session.log("FINAL_CAPTURE_ERROR " + repr(error))
session.finish({
"selected_modes": list(SELECTED_MODES),
"mode_results": results,
"expected_firmware_revision": EXPECTED_REVISION,
"hall_tolerance_pulses": HALL_TOLERANCE,
"phase_duration_tolerance_s": PHASE_DURATION_TOLERANCE_S,
"arrival_spread_tolerance_s": ARRIVAL_SPREAD_TOLERANCE_S,
"final_state": final_state,
})
@@ -41,6 +41,26 @@ Bus-Off、加热ADC错误和DRV8718故障只有在具备对应故障注入条件
脚本启动时会强制重新加载 `hil_common.py`。ZCANPRO使用常驻Python解释器,若不重载,修改公共辅助模块后再次运行可能继续使用旧类定义;日志中的 `HIL_COMMON_VERSION` 可用于确认实际加载版本。
### Revision 13 动态模式
烧录 revision 13 后,在 ZCANPRO 扩展脚本窗口运行 `07_dynamic_modes.py`。脚本先验证
`0x301` 的固件版本、四轴 PositionValid/MaxHall 与故障状态,再依次运行木马模式和动态摇篮。
验收证据包括 `0x306` 阶段顺序/自动续环、`0x302` 四轴同步启停与终态、`0x303` 精确 Hall
目标;任一失败或脚本停止都会发送 `STOP_ALL` 并关闭热管理。两种模式均须由停止指令终止,
未收到停止时由ECU在启动累计10分钟后自动停止。
木马入口为`[320,190,500,430]`,随后在前俯`[270,250,460,400]`和后仰
`[370,130,540,460]`之间连续往复,不设置停驻。坐盆峰峰行程120 Hall、折算垂直分量约
18 mm;水平峰峰行程100 Hall且巡航占空比90%,用于抵消坐盆斜向运动的反向水平分量。每个半周期四轴同批启动,先到目标前
8 Hall会合,再以50% Duty完成等距末段。配置入口为`Config/seat_dynamic_mode_config.h`,N7
长期库`config/modes/vehicles/denza_n7/rocking_horse.yaml`必须镜像同一参数。
2026-08-04 revision 13实物复测:稳态前俯/后仰启动离散0.077/0.052 s,到位离散
0.337/0.182 s;四轴在巡航检查点均持续推进,目标误差不超过1 Hall。按实测M0 MaxHall=499
换算水平峰峰行程约43.1 mm,扣除坐盆估算反向水平分量后净摆幅约26.7 mm;自动进入下一轮,
`STOP_DYNAMIC(0x0F)`后`0x306`全零且无FAULT,PASS。证据为
`artifacts/hil/rocking_horse_rev13_horizontal_compensation_20260804.json`。
- 通风1/2/3档、关闭;加热1/2/3档、关闭;通风2档+加热2档。
- 每轴从当前位置偏移约12%并回到原位置,验证M0/M1/M2/M3的执行器、方向、CAN ACK、终态位置和Hall反馈。
- 若SMV8位置有效则直接使用且不发生归零动作;若某轴无效则先仅将该轴回最近端,验证Hall≈0与MaxHall保持不变后再测试。
@@ -9,8 +9,8 @@ import time
import zcanpro
SEAT_RX_IDS = (0x301, 0x302, 0x303, 0x304, 0x305, 0x4FA)
HIL_COMMON_VERSION = "2026-07-30-hall-current-stall-v4"
SEAT_RX_IDS = (0x301, 0x302, 0x303, 0x304, 0x305, 0x306, 0x4FA)
HIL_COMMON_VERSION = "2026-08-04-dynamic-modes-v8"
CONFIRMED_MAX_HALL = (480, 374, 917, 647)
HOME_KNOWN_RANGE_KEY = 0x4B52
HOME_LEG_TIMEOUT_S = 120
@@ -112,6 +112,20 @@ def parse_305(data):
}
def parse_306(data):
if data is None or len(data) < 8:
return None
return {
"mode_id": data[0],
"state": data[1],
"phase": data[2],
"active_mask": data[3] & 0x0F,
"fault_mask": (data[3] >> 4) & 0x0F,
"cycle_index": data[4] | (data[5] << 8),
"phase_elapsed_ms": data[6] | (data[7] << 8),
}
def command_payload(opcode, motor, direction, sequence, param_a=0,
param_b=0, version=1):
control = ((version & 0x03) << 4) | ((direction & 0x03) << 2) | \
@@ -228,6 +242,8 @@ class HilSession(object):
value["hall_activity_low"]
elif can_id == 0x305:
self.latest[0x305] = parse_305(data)
elif can_id == 0x306:
self.latest[0x306] = parse_306(data)
elif can_id == 0x4FA:
self.latest[0x4FA] = data
output.append((can_id, data, frame))
@@ -311,7 +327,7 @@ class HilSession(object):
def capture_state(self, duration_s=2.0, clear=True):
if clear:
for key in list(self.latest):
if key in (0x301, 0x305) or isinstance(key, tuple):
if key in (0x301, 0x305, 0x306) or isinstance(key, tuple):
del self.latest[key]
deadline = time.monotonic() + duration_s
while time.monotonic() < deadline and not self.stop_requested:
@@ -325,6 +341,7 @@ class HilSession(object):
"currents": [self.latest.get((0x304, motor))
for motor in range(4)],
"thermal_current": self.latest.get(0x305),
"dynamic_mode": self.latest.get(0x306),
}
def reset_current_peaks(self, motor=None):