第二十一章:安全与异常处理:看门狗应用、任务超时检测、安全状态恢复
做运动控制这些年,我踩过最大的坑是什么?
不是算法精度不够,也不是电机选型失误。而是——系统跑着跑着,突然死机了。
你想想看,一台五轴雕刻机正在加工模具,刀尖离工件还有0.1mm,主控芯片挂了。电机停在那,刀还插在工件里。等看门狗复位完,机械位置早就丢了。这种事故,轻则报废工件,重则撞坏主轴。
所以这一章,咱们专门聊聊运动控制里的安全底线。说白了,就是怎么让系统在出问题时,能体面地「死」,而不是暴毙。
21.1 看门狗:最后的防线
看门狗这玩意儿,我刚开始做嵌入式时觉得它可有可无。直到有一次,我在一个四轴平台上调试插补算法,一个死循环没处理好,电机直接冲过限位开关,「咔」一声撞上硬限位。从那以后,我再也不敢省看门狗了。
在RTOS环境下,看门狗的使用有个讲究。你不能简单地在主循环里喂狗,因为RTOS的任务调度是抢占式的。一个任务卡住了,其他任务可能还在跑,这时候喂狗反而掩盖了问题。
我个人习惯的做法是:在空闲任务里喂狗。为什么?因为空闲任务只有在所有其他任务都正常运行时才会被执行。如果某个高优先级任务死锁了,空闲任务就得不到CPU时间,看门狗自然超时复位。
核心原则:看门狗不是用来「防止」系统死机的,而是用来「检测」系统死机并自动恢复的。喂狗的位置决定了你能检测到什么级别的故障。
来看一个实际代码片段:
// 空闲任务中的看门狗喂狗实现
void vApplicationIdleHook(void)
{
// 只有在所有任务都正常运行时,才会进入这里
// 如果某个任务卡死,空闲任务得不到执行,看门狗就会超时
HAL_IWDG_Refresh(&hiwdg);
// 顺便检查一下各个关键任务的运行状态
static uint32_t last_check = 0;
if (HAL_GetTick() - last_check > 1000)
{
// 每秒检查一次任务的心跳
check_task_heartbeat();
last_check = HAL_GetTick();
}
}
注意:不要在中断服务函数里喂狗!我见过有人把喂狗放在SysTick中断里,结果主程序死循环了,看门狗照样被定时刷新,系统永远无法复位。这种「假活」状态比死机更危险。
21.2 任务超时检测:别让一个任务拖垮整个系统
运动控制里,每个任务都有严格的时序要求。插补任务必须在1ms内完成一次计算,否则下一个周期的位置指令就发不出去。通信任务如果超时,可能导致上位机误判设备状态。
我常用的方法是任务心跳检测。每个关键任务在每次循环中更新一个时间戳,然后由一个监控任务定期检查这些时间戳是否超时。
具体实现是这样的:
// 任务心跳结构体
typedef struct {
TaskHandle_t task_handle; // 任务句柄
uint32_t last_heartbeat; // 上次心跳时间
uint32_t timeout_ms; // 超时阈值
const char* task_name; // 任务名称
uint8_t fault_count; // 连续故障次数
} TaskHeartbeat_t;
// 关键任务列表
TaskHeartbeat_t g_task_heartbeats[] = {
{NULL, 0, 2, "Interpolation", 0}, // 插补任务,2ms超时
{NULL, 0, 5, "ServoUpdate", 0}, // 伺服更新,5ms超时
{NULL, 0, 20, "Communication", 0}, // 通信任务,20ms超时
{NULL, 0, 100, "HMI_Update", 0}, // 人机界面,100ms超时
};
// 每个任务在循环中调用此函数
void Task_HeartbeatUpdate(const char* task_name)
{
for (int i = 0; i < sizeof(g_task_heartbeats)/sizeof(g_task_heartbeats[0]); i++)
{
if (strcmp(g_task_heartbeats[i].task_name, task_name) == 0)
{
g_task_heartbeats[i].last_heartbeat = HAL_GetTick();
g_task_heartbeats[i].fault_count = 0;
break;
}
}
}
// 监控任务:每秒检查一次所有任务的心跳
void vTaskMonitor(void *pvParameters)
{
while(1)
{
uint32_t now = HAL_GetTick();
for (int i = 0; i < sizeof(g_task_heartbeats)/sizeof(g_task_heartbeats[0]); i++)
{
uint32_t elapsed = now - g_task_heartbeats[i].last_heartbeat;
if (elapsed > g_task_heartbeats[i].timeout_ms)
{
g_task_heartbeats[i].fault_count++;
// 连续超时3次,判定为严重故障
if (g_task_heartbeats[i].fault_count >= 3)
{
// 触发安全状态恢复
Safety_EnterSafeState(g_task_heartbeats[i].task_name);
}
}
}
vTaskDelay(pdMS_TO_TICKS(100)); // 每100ms检查一次
}
}
经验之谈:超时阈值不要设得太紧。我曾经把插补任务的超时设为1ms,结果偶尔的调度抖动就触发了误报警。后来改成2ms,配合连续3次超时才判故障,既保证了灵敏度,又避免了误报。
21.3 安全状态恢复:怎么「优雅地死」
检测到故障之后,下一步就是怎么恢复。这里有个原则:宁可停机,不可乱动。
运动控制的安全状态恢复,我一般分三个等级:
| 等级 | 触发条件 | 恢复动作 |
|---|---|---|
| 一级(警告) | 单次超时、通信偶发错误 | 记录日志,继续运行,降低速度 |
| 二级(严重) | 连续超时、位置偏差超限 | 急停所有轴,保持当前位置,等待人工干预 |
| 三级(致命) | 看门狗复位、硬件故障 | 切断动力电源,保存故障现场,自动重启 |
二级恢复是我最常用的。具体做法是:
void Safety_EnterSafeState(const char* fault_task)
{
// 1. 立即停止所有运动指令
Motion_StopAllAxes();
// 2. 记录故障现场
FaultRecord_t record;
record.timestamp = HAL_GetTick();
record.fault_task = fault_task;
record.axis_positions[0] = Motion_GetActualPosition(0);
record.axis_positions[1] = Motion_GetActualPosition(1);
// ... 记录所有轴的位置
FaultLog_Save(&record);
// 3. 通知上位机
Communication_SendFault(&record);
// 4. 保持伺服使能,但力矩归零
// 这样电机可以自由转动,但不会突然抱死
for (int i = 0; i < AXIS_COUNT; i++)
{
Servo_SetTorque(i, 0);
Servo_KeepEnabled(i); // 保持使能,维持位置环
}
// 5. 点亮故障指示灯
GPIO_WritePin(FAULT_LED_PIN, GPIO_PIN_SET);
// 6. 挂起所有非关键任务
vTaskSuspend(g_task_heartbeats[2].task_handle); // 通信任务
vTaskSuspend(g_task_heartbeats[3].task_handle); // HMI任务
// 7. 进入安全循环,等待复位
while(1)
{
// 只保留监控任务和心跳检测
vTaskDelay(pdMS_TO_TICKS(100));
// 检查是否收到复位指令
if (Safety_CheckResetCommand())
{
Safety_SystemReset();
}
}
}
重要提醒:安全状态下,千万不要直接切断伺服使能。我见过有人急停时直接关使能,结果垂直轴直接掉下来,砸坏了工作台。正确的做法是:先让力矩归零,再保持位置环,最后才考虑是否断电。
21.4 看门狗与任务监控的协同设计
这两个机制不是孤立的。我习惯把它们设计成一个分层防御体系:
这个分层设计的好处是:每一层只管自己的事。应用层只管任务是否超时,系统层只管看门狗是否溢出,硬件层只管芯片是否复位。上层解决不了的问题,自然由下层兜底。
举个例子:插补任务因为一个bug陷入死循环,应用层的心跳检测首先发现超时,尝试重启任务。如果重启失败,系统层的空闲任务得不到执行,看门狗超时触发硬件复位。整个过程不需要人为干预,系统自己就能恢复。
一个小技巧:我在每个任务的循环开头都会加一个「喂狗检查点」。如果任务检测到看门狗即将超时,会主动放弃当前计算,先喂狗再继续。这样既保证了实时性,又不会因为长时间计算导致看门狗误触发。
21.5 实际项目中的避坑指南
做了这么多年运动控制,我总结了几条血泪教训:
- 看门狗超时时间要留余量。我曾经把看门狗设为500ms,结果有一次系统启动时初始化外设花了600ms,还没进主循环就被复位了。后来我改成2秒,等系统稳定后再缩短到1秒。
- 故障恢复后要检查机械位置。看门狗复位后,电机的位置可能已经丢失。我习惯在启动时先让所有轴回零,确认位置无误后再进入工作模式。
- 不要依赖单一的安全机制。光有看门狗不够,光有心跳检测也不够。两者配合使用,才能覆盖大多数故障场景。
- 故障日志要保存到非易失存储器。看门狗复位后,RAM里的数据全丢了。如果不把故障信息存到Flash或EEPROM,你永远不知道系统为什么死机。
嗯,安全这块就聊这么多。说白了,运动控制系统的安全设计,就是一场「与不确定性共舞」的游戏。你永远不知道下一秒哪个任务会卡死,哪个中断会丢失。但只要你把防御体系搭好了,系统就能在绝大多数情况下自己站起来,继续干活。
记住一句话:好的安全设计,是让用户感觉不到安全设计的存在。