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

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 节 增加容错代码

相关推荐
czhaii4 小时前
定时器三时段输出自动控制器
单片机·嵌入式硬件
Neil今天也要学习6 小时前
永磁同步电机控制算法--基于数据驱动的超局部无模型预测电流控制MFPC及改进
单片机·嵌入式硬件·算法
在岸上走的鱼7 小时前
ISE 14.7安装到80%(82%或者83%)卡主
嵌入式硬件·硬件工程
Python小老六8 小时前
STM32(ARM32)烧录方式详解:串口、JTAG、SWD
stm32·单片机·嵌入式硬件
CHENG-JustDoIt8 小时前
嵌入式开发 | ARM Cortex-M 系列中M3、M4、M23 和 M33四款处理器的深度对比分析
arm开发·单片机·嵌入式硬件·arm
国科安芯9 小时前
多相交错并联系统的时钟同步精度与输入纹波抵消效应研究
网络·单片机·嵌入式硬件·fpga开发·性能优化
清风6666669 小时前
基于51单片机的双档交流电压表设计与实现
单片机·嵌入式硬件·毕业设计·51单片机·课程设计·期末大作业
17(无规则自律)1 天前
LubanCat 2烧录一个新镜像后开发环境搭建
linux·嵌入式硬件·考研·软件工程
张槊哲1 天前
IIC图解
单片机·嵌入式硬件