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

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

相关推荐
我叫洋洋3 分钟前
[Proteus 和 stm32f103c8t6]的使用控制OLED篇]
c语言·stm32·单片机·嵌入式硬件·蓝桥杯·proteus
yuan199972 小时前
STM32F103C8T6 串口通信程序实例
stm32·单片机·嵌入式硬件
IT方大同4 小时前
(实时操作系统)线程管理
c语言·开发语言·嵌入式硬件
意法半导体STM324 小时前
【官方原创】STM32H7双核芯片通过 STlink连接失败问题分析 LAT1654
开发语言·前端·javascript·stm32·单片机·嵌入式硬件
夜星辰20234 小时前
MobaXterm会话窗口详解
嵌入式硬件·ssh·调试串口
BT-BOX5 小时前
第7章《Stm32CubeMX+Proteus仿真入门》--独立按键扫描
stm32·嵌入式硬件·proteus
广药门徒5 小时前
PADS 等长处理方法
嵌入式硬件
zd8451015006 小时前
ESP8266 MQTT连接onenet
stm32·单片机
3壹7 小时前
STM32按键检测与上拉电阻详解
c语言·stm32·嵌入式硬件
昵称只能一个月修改一次。。。7 小时前
【无标题】
单片机·嵌入式硬件