嵌入式执行器架构:统一动作表实现与两阶段执行深度解析
一张声明式表格描述每个状态对应的全部执行器输出。MotorTask 周期遍历,一次性下发。新增状态只需加一行表。
一、问题:散落 switch 的代价
重构前的执行器层有 7+ 张独立的查找表,分布在 packer_actuator.c 的不同位置:
flowchart TB
subgraph TABLE[统一动作表]
T1[状态 → 执行器输出]
T2[声明式二维表]
end
subgraph TASK[MotorTask]
M1[周期遍历动作表]
M2[批量下发]
end
subgraph ACT[执行器]
A1[步进电机]
A2[加热器]
A3[阀门]
end
TABLE --> TASK --> ACT
style TABLE fill:transparent,stroke:#8dc7ff,color:#eaf4ff
style TASK fill:transparent,stroke:#8dc7ff,color:#eaf4ff- 一张表把状态映射到 LED 闪烁周期
- 一张表把状态映射到直流电机动作
- 一张表把状态映射到步进电机单轴动作
- 一张表把状态映射到步进电机双轴动作
- 一张表把状态映射到加热使能
- 一张表把状态映射到风机请求
- 一张表把状态映射到功率总使能
每次状态切换,MotorTask 要依次遍历 7 张表。新增一个状态就要改 7 个地方。遗漏任何一张表都会导致该状态下的执行器输出与预期不符。
更糟糕的是,这些表散落在 packer_actuator_task_once() 函数中,以 if-else-if 链条或 switch-case 的形式存在,阅读和维护都十分困难。
二、统一动作描述表
重构后的核心是一张 s_state_actions[] 表,每个状态一个条目,字段涵盖所有执行器输出:
typedef struct {
packer_state_t state;
pact_stepper_mode_t stepper_mode;
bsp_dc_motor_id_t dc_motor;
bsp_dc_motor_dir_t dc_motor_dir;
uint8_t hopper_conveyor : 1;
uint8_t power_on : 1;
uint8_t heater_toggle : 1;
} packer_state_action_t;
查找函数 pact_find() 极为简单——一次线性遍历即可定位:
static const packer_state_action_t *pact_find(packer_state_t state) {
uint8_t i;
for (i = 0U; i < (uint8_t)(sizeof(s_state_actions) / sizeof(s_state_actions[0])); i++) {
if (s_state_actions[i].state == state) return &s_state_actions[i];
}
return NULL;
}
三、动作表内容
完整的状态动作映射如下(步进模式字段决定后续的步进解析路径):
static const packer_state_action_t s_state_actions[] = {
{POWER_ON, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 0, 0, 0},
{SELF_CHECK, PACT_STEPPER_NONE, PUSHER_ST2, FORWARD, 0, 0, 0},
{IDLE, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 0, 0, 0},
{BAG_CALIB_CHECK, PACT_STEPPER_NONE, PUSHER_ST2, FORWARD, 0, 0, 1},
{BAG_CALIB_CLEAR, PACT_STEPPER_BAG_RUNTIME, PUSHER_ST2, FORWARD, 0, 0, 1},
{BAG_CALIB_SEEK, PACT_STEPPER_BAG_RUNTIME, PUSHER_ST2, FORWARD, 0, 0, 1},
{BAG_CALIB_BACKOFF,PACT_STEPPER_BAG_RUNTIME, PUSHER_ST2, FORWARD, 0, 0, 1},
{BAG_OUT, PACT_STEPPER_BAG_RUNTIME, DC_ID_COUNT, DIR_STOP, 0, 0, 1},
{PREHEAT, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 0, 1, 1},
{SEAL_FORWARD, PACT_STEPPER_NONE, PUSHER_ST2, FORWARD, 0, 0, 1},
{CONVEYOR_RUN, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 1, 0, 0},
{SEAL_BACKWARD, PACT_STEPPER_NONE, PUSHER_ST2, FORWARD, 0, 0, 0},
{PRESS_RETRACT, PACT_STEPPER_DUAL, DC_ID_COUNT, DIR_STOP, 0, 0, 0},
{PRESS_CLOSE, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 0, 1, 0},
{SEAL_HOLD, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 0, 1, 0},
{TEAR_OFF, PACT_STEPPER_SINGLE, DC_ID_COUNT, DIR_STOP, 0, 1, 0},
{PRESS_OPEN, PACT_STEPPER_DUAL, DC_ID_COUNT, DIR_STOP, 0, 1, 0},
{RESET, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 0, 0, 0},
{ERROR, PACT_STEPPER_NONE, DC_ID_COUNT, DIR_STOP, 0, 0, 0},
};
步进模式字段(PACT_STEPPER_NONE/SINGLE/DUAL/BAG_RUNTIME)充当二级路由,将步进动作解析委托给对应的运行时函数,构成"一次定位 + 二次分发"的两级查找结构。
四、状态机 + 动作表 = 正交架构
状态机(app_packer_sm)负责"什么时候该切状态",执行器动作表(s_state_actions[])负责"当前状态硬件该干什么"。两个表是正交的。一个决定"什么时候该切状态",一个决定"当前状态硬件该干什么"。
五、统一动作表实现:s_state_actions[]
重构的核心是将 7+ 张独立查找表合并为单一 s_state_actions[] 统一动作描述表。每张旧表都曾独立维护、独立遍历,新增一个状态就要改 7 处。合并后,状态 → 执行器映射只在一个地方定义。
实际的结构体 packer_state_action_t 字段如下:
typedef struct {
packer_state_t state; /**< 打包机流程状态 */
pact_stepper_mode_t stepper_mode; /**< NONE/SINGLE/DUAL/BAG_RUNTIME */
bsp_dc_motor_id_t dc_motor; /**< BSP_DC_MOTOR_ID_COUNT 表示无 */
bsp_dc_motor_dir_t dc_motor_dir; /**< STOP 表示不启动 */
uint8_t hopper_conveyor : 1; /**< 投料传送带继电器 */
uint8_t power_on : 1; /**< 功率总使能 */
uint8_t heater_toggle : 1; /**< 兼容保留位,实际加热已下沉到 HeaterTask */
} packer_state_action_t;
各字段解读:
- stepper_mode:步进电机动作模式。NONE 表示无步进动作;SINGLE 查
s_stepper_flow_actions[]编译期表;DUAL 查s_dual_stepper_flow_actions[];BAG_RUNTIME 由运行时袋长/速度参数在状态进入时换算脉冲数和频率。 - dc_motor + dc_motor_dir:直流电机(推杆挡板)的启动请求。DIR_STOP 表示不启动。实际启动时还会经过
packer_power_limit功率限制准入检查。 - hopper_conveyor:投料传送带继电器的控制位。仅在 CONVEYOR_RUN 状态置 1。
- power_on:功率总使能(MOSFET 大功率输出)。PREHEAT、PRESS_CLOSE、SEAL_HOLD、TEAR_OFF、PRESS_OPEN 等需要加热或压杆保持的状态置 1。
- heater_toggle:兼容保留位。实际加热控制已在 HeaterTask 中独立实现,此位保留用于日志和未来扩展。
原来的 7 张独立表是:
- 状态 → LED 闪烁周期
- 状态 → 直流电机动作
- 状态 → 步进单轴动作(s_stepper_flow_actions)
- 状态 → 步进双轴动作(s_dual_stepper_flow_actions)
- 状态 → 加热使能
- 状态 → 风机请求
- 状态 → 功率总使能
合并后,单次 pact_find() 线性查找替代了原先 7+ 次独立表遍历。步进模式的二次分发通过 stepper_mode 字段路由到对应的运行时解析函数(pad_get_stepper_action、pad_get_dual_stepper_action、pad_get_bag_runtime_action),形成"一次定位 + 按模式二次分发"的两级查找结构。
六、两阶段执行:On-Enter vs Per-Cycle
执行器逻辑显式拆分为两个阶段,分别由两个函数承担:
6.1 pact_apply_state_action() — 状态进入时一次下发
每次状态切换时调用。执行以下操作:
- 解析步进电机动作(SINGLE/DUAL/BAG_RUNTIME),计算目标脉冲和频率
- 通过增量启停机制启动/停止/重启步进轴
- 下发直流电机动作(推杆挡板)
- 控制投料传送带继电器
- 设置功率总使能
- 更新风机请求状态
典型的调用链——在 packer_actuator_task_once() 中检测到状态变化后执行:
if ((state != s_last_applied_state) || (s_state_entry_tick == 0U) ||
(press_profile != s_last_press_profile)) {
s_state_entry_tick = now;
state_elapsed_ms = 0U;
pact_apply_state_action(state);
s_last_applied_state = state;
s_last_press_profile = press_profile;
}
6.2 pact_apply_per_cycle_actions() — 每周期维护
每个 MotorTask 周期(通常 50ms)无条件调用。处理以下定时子阶段:
- SELF_CHECK 挡板与推杆驱动:读取自检子阶段的驱动命令(
sm_self_check_get_flap_drive、sm_self_check_get_dc1_drive),发生跳变时下发直流电机动作。 - PREHEAT 出袋预备时序:BAG 流程进入 PREHEAT 后,按
dc_motor2_bag_prepare_close_time_ms划分 CLOSE/PRE_OPEN 两个子阶段,先合挡板再预开。 - SEAL_FORWARD 封口收尾时序:BAG 流程进入 SEAL_FORWARD 后,按
dc_motor2_return_settle_time_ms划分 CLOSE/OPEN 两个子阶段,先保持合拢再开挡板。 - RESET 推杆 5 相位节拍:DELAY → FORWARD → HOLD → BACKWARD → IDLE,每个相位时长由运行时配置参数控制。
6.3 为什么需要两个阶段
状态切换(On-Enter)需要一次性的硬件配置:步进电机启动/停止、功率总使能开关、风机请求变更。这些操作只应在状态进入时执行一次,不应在每个周期重复。
稳态运行(Per-Cycle)需要周期性的维护:自检阶段的挡板驱动随子阶段变化、出袋预备中挡板需按时间切换合/开、复位推杆需按节拍前进/后退/停止。这些操作的触发条件是时间流逝或子阶段变化,而非状态切换本身。
两阶段分离的好处:
- On-Enter 阶段可安全执行 stop/start 轴操作,无需担心被周期重复执行
- Per-Cycle 阶段可以使用跳变检测(比较当前值与上次值),只在变化时执行操作
- 故障恢复时只需重置 Per-Cycle 的阶段状态变量,不影响 On-Enter 的动作表
七、增量启动/停止(避免机械噪声)
早期版本在每次状态切换时执行全局 stop_all + start_all,即使轴上一次已经在运行且方向和速度没变。这导致不必要的机械噪声和磨损。
重构后引入 s_last_* 缓存和位图比较逻辑:
static uint8_t s_last_plan_valid = 0U;
static uint8_t s_last_has_stepper = 0U;
static uint8_t s_last_has_dual_stepper = 0U;
static uint8_t s_last_axis_mask = 0U;
static uint32_t s_last_stepper_resolved_hz = 0U;
static uint32_t s_last_dual_resolved_hz = 0U;
static app_stepper_flow_action_t s_last_stepper_action;
static app_dual_stepper_flow_action_t s_last_dual_action;
每次 pact_apply_state_action() 执行时,先计算当前动作画像(轴、方向、频率、目标脉冲),然后与缓存的上一帧画像逐字段比较:
if ((s_last_plan_valid != 0U) &&
(has_stepper != 0U) &&
(s_last_has_stepper != 0U) &&
(s_last_stepper_action.axis == stepper_action.axis) &&
(s_last_stepper_action.direction == stepper_action.direction) &&
(s_last_stepper_resolved_hz == stepper_resolved_hz) &&
(s_last_stepper_action.target_pulses == stepper_action.target_pulses) &&
(s_last_stepper_action.finite_stop_mode == stepper_action.finite_stop_mode)) {
single_action_same = 1U;
}
基于比较结果分三种情况处理:
- 新增轴(add_mask):当前需要启动但上一帧未运行的轴 → 直接启动
- 变更轴(restart_mask):当前与上一帧都运行但参数变化 → 先停止再启动
- 移除轴(remove_mask):上一帧在运行但当前不需要的轴 → 停止
位图计算逻辑:
uint8_t old_mask = s_running_axis_mask;
uint8_t add_mask = (uint8_t)(axis_mask & (uint8_t)(~old_mask));
uint8_t remove_mask = (uint8_t)(old_mask & (uint8_t)(~axis_mask));
// 仅停止需要变更的轴
if (restart_mask != 0U) {
pact_stepper_stop_axes_by_mask(restart_mask);
}
// 仅启动新增和变更后的轴
start_mask = (uint8_t)(add_mask | restart_mask);
if (start_mask != 0U) {
pact_stepper_start_actions_by_mask(&stepper_action, has_stepper,
&dual_stepper_action, has_dual_stepper,
start_mask);
}
// 仅停止不再需要的轴
if (remove_mask != 0U) {
pact_stepper_stop_axes_by_mask(remove_mask);
}
四个轴分别对应位图中的 bit 0~3。全局 s_running_axis_mask 持续追踪当前运行轴集合,增量操作仅作用于变化的部分。
效果:SEAL_FORWARD → SEAL_BACKWARD 切换时轴 1(出袋轴)已在 BAG_OUT 完成后停止,不会再次被 stop;PRESS_CLOSE → PRESS_OPEN 切换时轴 2(压杆轴)已在 PRESS_CLOSE 中停止,轴 3/4(扒口轴)持续运行方向不变时不中断。
八、4 轴步进子系统
系统管理 4 个步进轴,分别对应不同的物理执行器:
| 逻辑轴 | BSP 映射 | 默认频率 (Hz) | 运行时配置键 | 用途 |
|---|---|---|---|---|
| 轴 1 | BSP_STEPPER_AXIS_1 | 105039 | speed_bag_out_hz | 出袋、校准、扯断 |
| 轴 2 | BSP_STEPPER_AXIS_2 | 40000 | speed_seal_move_hz | 压杆下压/回位 |
| 轴 3 | BSP_STEPPER_AXIS_3 | 30000 | speed_seal_return_hz | 扒口正转 |
| 轴 4 | BSP_STEPPER_AXIS_4 | 30000 | speed_seal_return_hz | 扒口正转 |
配置表定义:
static const app_stepper_motor_cfg_t s_stepper_motors[] = {
{APP_AXIS_1, BSP_STEPPER_AXIS_1, 105039U, 0U, 0U, 0U},
{APP_AXIS_2, BSP_STEPPER_AXIS_2, 40000U, 0U, 0U, 0U},
{APP_AXIS_3, BSP_STEPPER_AXIS_3, 30000U, 0U, 0U, 0U},
{APP_AXIS_4, BSP_STEPPER_AXIS_4, 30000U, 0U, 0U, 0U},
};
TIM4 共享 ARR 约束:四轴共用 TIM4 的四个通道,共享同一个 ARR 寄存器,因此同一时刻四轴只能运行在同一脉冲频率。当需要不同轴以不同速度运行时(如 PRESS_OPEN 中的轴 2 回位和轴 3/4 扒口),通过脉冲数比例自动换算频率,使行程短的轴先到位,行程长的轴以 IN1 传感器收尾。
多轴同步启动通过 bsp_stepper_group_command_t 实现:
typedef struct {
bsp_stepper_command_t commands[APP_AXIS_COUNT];
uint8_t command_count;
} bsp_stepper_group_command_t;
典型场景——pact_stepper_start_actions_by_mask() 中组合同步启动:
- PRESS_RETRACT:仅轴 2 单轴启动(压杆下压),脉冲数由运行时配置的压杆行程长度换算。
- PRESS_OPEN:轴 3/4 双轴同步启动(扒口打开)+ 轴 2 连续模式回 IN1。三轴共用频率,轴 2 频率根据行程比例自动缩放。
九、运行时动作解析器
6 个解析器函数负责在状态进入时根据运行时配置动态生成步进动作参数:
9.1 bag_action — 出袋系列动作
static uint8_t pact_fill_runtime_bag_action(packer_state_t state,
app_stepper_flow_action_t *out);
处理 BAG_CALIB_CLEAR/SEEK/BACKOFF、BAG_OUT 四个状态。从 packer_bag_runtime_config 读取袋长和速度参数,换算目标脉冲数和频率。校准清空(CALIB_CLEAR)和校准寻找(CALIB_SEEK)为连续模式(target_pulses=0),校准回退(CALIB_BACKOFF)和出袋(BAG_OUT)为有限脉冲模式。
9.2 press_action — 压杆下压
static uint8_t pact_fill_runtime_press_action(packer_state_t state,
app_stepper_flow_action_t *out);
仅处理 PRESS_RETRACT 状态。从 packer_press_runtime_config 读取 press_retract_len_mm_x1000,独立换算下压脉冲数。方向由 pact_get_press_axis2_retract_dir() 根据编译期宏决定。
9.3 tear_off_action — 扯断
static uint8_t pact_fill_runtime_tear_off_action(packer_state_t state,
app_stepper_flow_action_t *out);
处理 TEAR_OFF 状态。从 rcfg 读取 tear_off_len_mm_x1000 和 speed_tear_off_hz,独立频率参数使扯断速度可与出袋速度不同。
9.4 press_dual_action — 扒口双轴(含自动频率缩放)
static uint8_t pact_fill_runtime_press_dual_action(packer_state_t state,
app_dual_stepper_flow_action_t *out);
处理所有涉及轴 3/4 双轴同步的场景,包括自检扒口、PRESS_CLOSE(扒口闭合)、PRESS_OPEN 双轴回位(DUAL_RETURN)。对于 OPEN_DUAL_RETURN 场景,根据轴 2 回位脉冲数与轴 3/4 脉冲数的比例自动缩放频率,确保"双轴先到位、轴 2 以 IN1 收尾"的节拍不变。
9.5 axis2_return_action — 轴 2 回 IN1(连续模式)
static uint8_t pact_try_fill_press_axis2_return_action(
packer_state_t state, sm_press_profile_t profile,
app_stepper_flow_action_t *out);
处理轴 2 回 IN1 传感器的高优先动作。以连续模式(target_pulses=0)运行,IN1 物理传感器命中为主完成条件,脉冲保护上限仅用于超时检测。自检回位和正常回位可使用不同的速度参数。
9.6 press_open_wrapper — PRESS_OPEN 入口包装器
static uint8_t pact_fill_press_open_axis2_action(packer_state_t state,
app_stepper_flow_action_t *out);
专门处理 PRESS_OPEN 状态下轴 2 回 IN1 的入口。通过 sm_press_get_profile() 获取当前压杆画像,判断是否需要启动轴 2,然后委托给 pact_try_fill_press_axis2_return_action() 完成具体填充。
十、协议点动支持
协议层提供两种独立于状态机的点动机制,用于调试和手动操作:
10.1 步进点动
uint8_t packer_actuator_start_protocol_stepper_jog(uint8_t motor_id,
uint8_t forward,
uint16_t target_pulses);
协议层指定 1~4 号步进电机、正/反向、目标脉冲数。点动使用该轴的默认频率(轴 1 从运行时袋长配置取值,轴 2~4 从 motor 表默认值取值)。启动前自动 stop_all 所有步进和直流电机,关断功率输出。
10.2 直流电机点动
uint8_t packer_actuator_start_protocol_dc_motor_jog(uint8_t motor_id,
uint8_t forward,
uint16_t duration_ms);
协议层指定 1~2 号直流电机、正/反向、持续时长(毫秒)。通过 packer_power_limit 准入后下发到 BSP。
10.3 互锁与生命周期
两种点动互相 busy-locked:
if ((s_protocol_stepper_jog_active != 0U) ||
(s_protocol_dc_motor_jog_active != 0U)) {
return 2U; // 忙
}
启动时记录 deadline tick,在 packer_actuator_task_once() 的每个 MotorTask 周期中检查是否超时,超时后自动停止并清除 busy 标志。一旦触发点动,协议层不再通过状态机控制执行器,直到点动完成。
十一、LED 状态映射
每种状态或状态组对应一个固定的 LED 闪烁周期:
| 状态组 | LED 闪烁周期常量 | 典型值 |
|---|---|---|
| POWER_ON、SELF_CHECK | APP_CFG_LED_BOOT_BLINK_MS | 800 ms |
| IDLE | APP_CFG_LED_IDLE_BLINK_MS | 500 ms |
| BAG_CALIB_*、BAG_OUT | APP_CFG_LED_CALIB_BLINK_MS | 200 ms |
| SEAL_HOLD | APP_CFG_LED_HOLD_BLINK_MS | 100 ms |
| ERROR | APP_CFG_LED_ERROR_BLINK_MS | 80 ms |
| 其他运行状态 | APP_CFG_LED_RUN_BLINK_MS | 150 ms |
static uint32_t pact_get_led_blink_ms(packer_state_t state)
{
switch (state) {
case PACKER_STATE_POWER_ON:
case PACKER_STATE_SELF_CHECK:
return APP_CFG_LED_BOOT_BLINK_MS;
case PACKER_STATE_IDLE:
return APP_CFG_LED_IDLE_BLINK_MS;
case PACKER_STATE_BAG_CALIB_CHECK:
case PACKER_STATE_BAG_CALIB_CLEAR:
case PACKER_STATE_BAG_CALIB_SEEK:
case PACKER_STATE_BAG_CALIB_BACKOFF:
case PACKER_STATE_BAG_OUT:
return APP_CFG_LED_CALIB_BLINK_MS;
case PACKER_STATE_SEAL_HOLD:
return APP_CFG_LED_HOLD_BLINK_MS;
case PACKER_STATE_ERROR:
return APP_CFG_LED_ERROR_BLINK_MS;
default:
return APP_CFG_LED_RUN_BLINK_MS;
}
}
LED 切换时输出日志:
if ((state != s_last_led_state) || (blink_period != s_last_led_period_ms)) {
BSP_LOG_INF("LED_MAP state=%s period=%lums", psh_state_name(state),
(unsigned long)blink_period);
s_last_led_state = state;
s_last_led_period_ms = blink_period;
}
十二、已知 Bug 修复
12.1 Per-Cycle 阶段状态残留
在早期版本中,pact_apply_per_cycle_actions() 内部的 6 个阶段跟踪变量声明为 static 局部变量,作用域仅限于函数内部。当故障发生时,psh_enter_error() 直接切换状态到 ERROR,MotorTask 可能尚未执行到重置这些变量的 else 分支,导致阶段状态残留。
根因:故障恢复后新出袋进入 PREHEAT 时,s_bag_prep_phase 仍然保持 CLOSE 值,而新周期的 elapsed_ms=0 也计算出 CLOSE,跳变检测 phase != s_bag_prep_phase 判定为假,风机请求被跳过。
修复:将 6 个 static 局部变量提升为文件作用域:
typedef enum {
PACT_BAG_PREP_PHASE_IDLE = 0,
PACT_BAG_PREP_PHASE_CLOSE,
PACT_BAG_PREP_PHASE_PRE_OPEN
} pact_bag_prep_phase_t;
static pact_bag_prep_phase_t s_bag_prep_phase = PACT_BAG_PREP_PHASE_IDLE;
static pact_bag_finish_phase_t s_bag_finish_phase = PACT_BAG_FINISH_PHASE_IDLE;
static pact_reset_phase_t s_reset_last_phase = PACT_RESET_PHASE_IDLE;
static uint8_t s_reset_pusher_active = 0U;
static sm_self_check_flap_drive_t s_self_check_flap_drive =
SM_SELF_CHECK_FLAP_DRIVE_STOP;
static sm_self_check_dc1_drive_t s_self_check_dc1_drive =
SM_SELF_CHECK_DC1_DRIVE_STOP;
新增显式重置函数:
void packer_actuator_reset_per_cycle_phases(void)
{
s_bag_prep_phase = PACT_BAG_PREP_PHASE_IDLE;
s_bag_finish_phase = PACT_BAG_FINISH_PHASE_IDLE;
s_reset_last_phase = PACT_RESET_PHASE_IDLE;
s_reset_pusher_active = 0U;
s_self_check_flap_drive = SM_SELF_CHECK_FLAP_DRIVE_STOP;
s_self_check_dc1_drive = SM_SELF_CHECK_DC1_DRIVE_STOP;
}
在 psh_enter_error() 中同步调用:
void psh_enter_error(uint8_t alarm_code) {
// ... 停止所有执行器 ...
packer_actuator_stop_all_release_all();
packer_actuator_reset_per_cycle_phases(); // 确保阶段状态归零
// ...
}
12.2 静态指针跨周期覆盖
早期版本中,pact_apply_state_action() 使用静态指针指向当前步进动作结构体。当多个状态快速切换(特别是在 stop→start 之间被其他调用中断)时,静态指针可能被覆盖,导致 PRESS_OPEN 阶段的轴 3/4 双轴动作丢失。
修复:动作结构体从静态指针改为栈上分配:
// 栈上分配动作结构体,消除静态指针覆盖风险
app_stepper_flow_action_t stepper_action;
app_dual_stepper_flow_action_t dual_stepper_action;
每次调用 pact_apply_state_action() 都使用新的栈内存,彻底消除了跨调用覆盖的可能性。
Comments NOTHING