解决硬件复位后按键响应变慢的问题 —— 嵌入式容错设计实战

1. 问题现象

最近在调试一个基于 FreeRTOS 和 Modbus 的嵌入式项目时,发现一个奇怪的现象:长按开发板上的复位键,松开后立即按按键,按键的响应变得非常迟钝。原本按下按键 LED 会立刻翻转,现在却要等好几秒才有反应。更诡异的是,如果等待一段时间(比如 10 秒)再按,按键又恢复正常了。

2. 复现步骤

  1. 系统正常运行,按键响应正常。

  2. 用手指长按复位键(硬件复位)约 2 秒后松开。

  3. 立即按下任意按键,观察 LED 或串口输出,发现响应极慢(延迟数秒)。

  4. 等待约 10 秒后再次按键,响应恢复正常。

3. 原因分析

3.1 硬件复位的影响

按下复位键时,主控 MCU 会立即重启,但挂在同一串口总线上的传感器模块(例如 Modbus 从机)可能没有复位。从机可能还处在之前通信的某个中间状态,比如正在等待主机响应超时,或者串口接收缓冲区卡在了错误状态。

3.2 主控重启后的通信行为

主控重启后,程序会重新初始化外设并开始运行。在任务中,通常会周期性地通过 Modbus 读取传感器数据。如果从机处于异常状态,Modbus 请求就会失败,而 libmodbus 默认的超时时间很长(原始值可能是 1 秒或更长),并且会重试。这导致主控卡在 Modbus 通信上,占用大量 CPU 时间,按键扫描任务的执行频率被严重拉低,从而感觉按键响应变慢。

3.3 UART 错误状态

更严重的情况是,复位过程中串口可能产生了帧错误、溢出错误等,导致 UART 外设进入错误中断,如果没有合理的恢复机制,后续的 DMA 接收将无法正常工作,所有串口通信都会失败。

3.4 从机的超时等待

从机在收到一个不完整的请求后,会启动超时计时,等待下一个字符或帧结束。如果主控复位后立即发送新请求,从机可能还在超时等待中,导致新请求被忽略。

4. 解决方案

针对以上原因,我们增加了三处容错代码,彻底解决了按键响应慢的问题。

4.1 添加 UART 错误恢复代码

当 UART 发生错误时,HAL 库会调用 HAL_UART_ErrorCallback 回调函数。我们在该回调中重新初始化 UART 并重新启动 DMA 接收,让串口从错误中恢复。

cs 复制代码
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    PUART_Data data = NULL;

    if (huart == &huart1) data = &g_uart1_data;
    if (huart == &huart2) data = &g_uart2_data;
    if (huart == &huart4) data = &g_uart4_data;

    if (data)
    {
        // 重新初始化 UART
        HAL_UART_DeInit(data->huart);
        HAL_UART_Init(data->huart);

        // 重新启动 DMA + IDLE 接收
        HAL_UARTEx_ReceiveToIdle_DMA(data->huart, data->rx_buf, RX_BUF_LEN);
    }
}

这样,一旦串口出现错误,硬件会自动恢复,不会一直卡在错误状态。

4.2 调整 libmodbus 的超时时间

libmodbus 内部有两个重要的超时参数:响应超时字节超时。默认值可能偏大(比如 1 秒),我们将其改小,避免长时间等待。

修改 Middlewares\Third_Party\libmodbus\modbus-private.h

cs 复制代码
#define _RESPONSE_TIMEOUT 10000   // 响应超时 10ms(原可能为 1000000 微秒)
#define _BYTE_TIMEOUT     10000   // 字节超时 10ms

注意:这里的单位是微秒(µs),所以 10000 微秒 = 10 毫秒。缩短超时时间后,一次失败的 Modbus 通信最多等待 20 毫秒,大大减小了阻塞时间。

4.3 主控发出请求前增加延时

主控获得 Modbus 锁之后,先延时 20 毫秒再发送请求,给从机足够的时间退出超时等待状态,进入新一轮的帧接收。

修改前的代码片段(简化):

cs 复制代码
xSemaphoreTake(g_ch1_Lock, portMAX_DELAY);
modbus_set_slave(ctx, 2);
rc = modbus_read_input_registers(ctx, 0, 2, vals);
xSemaphoreGive(g_ch1_Lock);

修改后:

cs 复制代码
xSemaphoreTake(g_ch1_Lock, portMAX_DELAY);
vTaskDelay(20);                      // 延时 20ms,让从机退出超时
modbus_set_slave(ctx, 2);
rc = modbus_read_input_registers(ctx, 0, 2, vals);
xSemaphoreGive(g_ch1_Lock);

在需要写从机的代码块中也做了同样的处理:

cs 复制代码
if (xSemaphoreTake(g_xBinarySemaphoreENV, 500) == pdTRUE)
{
    xSemaphoreTake(g_ch1_Lock, portMAX_DELAY);
    vTaskDelay(20);
    modbus_set_slave(ctx, 2);
    rc = modbus_write_bits(ctx, 0, 5, &g_mb_mapping->tab_bits[6]);
    xSemaphoreGive(g_ch1_Lock);
}

这样,即使从机还在超时等待中,20ms 后它也会退出并准备好接收新请求。

5. 效果验证

加入以上三点容错代码后,反复测试长按复位后立即按键,按键响应立刻恢复正常,再也没有延迟现象。通过串口日志观察,Modbus 通信在复位后第一次请求可能会失败(因为从机可能还在复位中),但很快就能恢复,并且每次失败仅消耗几十毫秒,不会影响按键扫描。

6. 总结

这个问题的本质是系统复位后外设状态不一致导致的通信故障,进而影响了实时性。嵌入式系统中,硬件复位并不等于所有外设都同步复位,因此我们需要在软件层面做好容错设计:

  • 错误恢复机制:UART、I2C 等外设发生错误时,要有自动恢复的代码。

  • 合理的超时设置:通信协议的超时不能太长,否则会拖累整个系统。

  • 给从机留缓冲时间:主从机之间要有一定的时序宽容度,避免"一复位就猛攻"。

通过这个案例,我深刻体会到:容错代码不是可有可无的补丁,而是保证系统健壮性的核心。希望这篇笔记能帮助自己日后复习,也能给遇到类似问题的朋友一点启发。


参考资料:百问网《嵌入式实战教程》7.8.2 节 增加容错代码

相关推荐
charlie1145141917 小时前
嵌入式C++工程实践第16篇:第四次重构 —— LED模板,从通用GPIO到专用抽象
c语言·开发语言·c++·驱动开发·嵌入式硬件·重构
深圳市九鼎创展科技9 小时前
MT8883 vs RK3588 开发板全面对比:选型与场景落地指南
大数据·linux·人工智能·嵌入式硬件·ubuntu
三品吉他手会点灯11 小时前
STM32 VSCode 开发-C/C++的环境配置中,找不到C/C++: Edit Configurations选项
c语言·c++·vscode·stm32·单片机·嵌入式硬件·编辑器
yu859395814 小时前
STM32 智能红外循迹小车(含码盘测速 + 避障)
stm32·单片机·嵌入式硬件
三品吉他手会点灯14 小时前
STM32 DAP 烧录报错-最终解决方法的原理和操作逻辑
stm32·单片机·嵌入式硬件
fengfuyao98514 小时前
TFT 彩屏 GUI 开发
stm32·嵌入式硬件
长安第一美人15 小时前
算能 BM1688 低延迟推流:Qt+WebSocket 直出 H5/HDMI
开发语言·网络·嵌入式硬件·websocket·交互
yongui4783415 小时前
STM32 三相电机FOC驱动方案(三电阻单电阻双模式)
stm32·单片机·嵌入式硬件
WeeJot嵌入式16 小时前
【串口】初始串口-轮询模式
stm32·单片机·嵌入式
yong999016 小时前
基于 51 单片机配合霍尔传感器实现计数 + 转速测量
单片机·嵌入式硬件