feat: integrate cabin CAN and verified dynamic seat modes
This commit is contained in:
@@ -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
Binary file not shown.
Binary file not shown.
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user