GeekArch头像
关注

返回值不处理埋雷——忽略错误码,强制判返回值再继续

返回值不处理埋雷——忽略错误码,强制判返回值再继续

文章标签:#嵌入式开发 #单片机 #C语言

文章摘要:本文从一次真实事故切入——环境监测仪因忽略 HAL_I2C_Mem_Read() 返回值,导致现场温湿度"卡住不变"却难以排查。文章系统讲解返回值作为"执行契约"的底层原理,剖析忽略返回值如何酿成"偶发、传染、沉默"的故障,并给出"不判返回值就编译不过"的强制约束套路:显式比较 == HAL_OK、错误分支闭环处理(重试/上报/降级)、用 warn_unused_result 编译期拦截漏判。附错误写法复现、正确完整代码及实测对比,帮你彻底告别"看似无害"的返回值漏判。

一、开篇:那个"以为成功其实失败"的传感器

做一款环境监测仪时,我用 HAL_I2C_Mem_Read() 读温湿度传感器,写法一直是 HAL_I2C_Mem_Read(...); 直接丢弃返回值。实验室温箱里跑一天没事,可现场高温高湿环境下,偶尔温湿度会"卡住不变"。查了一周才发现:总线偶发 NAK,函数返回了 HAL_ERROR,但我没判,后面照常用了陈旧/错误的旧数据。

如果你习惯"调个 API 不接返回值",这一讲能救你。我会从错误码设计讲起,给你一套"不判返回值就编译不过"的强制约束套路,并附实测对比。

全文导航:入门认知(返回值即契约)→ 进阶原理(为什么忽略会酿成沉默故障)→ 高级应用(错误写法复现 + 正确判错代码 + 实测对比)。

二、入门认知:返回值是一份"执行契约"

几乎每个标准库/驱动 API 都用返回值告诉你"我干成了没、干成多少"。比如 fread 返回实际读到的元素数,HAL_UART_Transmit 返回 HAL_StatusTypeDef(OK/BUSY/ERROR/TIMEOUT)。忽略返回值,等于签收快递不看包裹坏没坏。

类比:外卖员敲门把你点的菜递出来,你头也不回进了屋,结果他手里其实是"商家售罄通知"——你却按"已收到"炒菜。

如图 1 展示"忽略返回值"与"判返回值"两条路径的分叉:

忽略

判断

是

否

调用 API

判返回值?

盲目用后续数据
(陈旧/错误值进入业务)

返回值 OK?

继续正常流程

错误分支:重试/上报/降级

图 1:是否判断返回值导致的两条业务路径

适用边界:纯 void 函数(如 GPIO_TogglePin)无返回值可判;但凡返回状态/数量的 API,都应视为契约。

三、进阶原理:沉默故障为什么最可怕

忽略返回值的危害不在"当下崩",而在"带着错误数据继续跑"。这类故障有三个特征:

  1. 偶发性:只在总线干扰、资源忙时出现,实验室难复现。
  2. 传染性:错误数据进入状态机/算法,污染后续所有结果。
  3. 沉默性:没有断言、没有日志,排查时无迹可寻。

易混淆点对比:

策略行为风险
忽略返回值错误也继续沉默故障,难排查
仅判 !=OK 打印知道错了但继续仍可能用陈旧数据
判错即重试/降级错误有闭环业务自愈,推荐

[坑] 高频翻车:if (HAL_I2C_Mem_Read(...) == HAL_OK) 写成了 if (HAL_I2C_Mem_Read(...)) —— 非 0 即真,而 HAL_OK 恰好是 0,逻辑直接反了。务必显式比较 == HAL_OK。

下面是沉默故障的传播链路图:

忽略

判断

总线偶发干扰/资源忙

API 返回错误码

是否判返回值?

错误数据进入业务层

状态机/算法被污染

后续所有结果失真

故障沉默,无日志无断言

错误分支处理

重试/上报/降级

业务自愈,可定位

图 2:沉默故障的传播链路 vs 判错自愈闭环

四、高级应用:复现与根治

错误写法演示

void read_sensor(float *temp)
{
    uint8_t buf[2];
    HAL_I2C_Mem_Read(&hi2c1, 0x44<<1, 0x00, 1, buf, 2, 100); // 返回值丢了!
    *temp = (buf[0]<<8 | buf[1]) * 0.01f;   // 若上面 NAK,buf 是旧值/乱码
}

逐行推演:总线偶发 NAK 时函数返回 HAL_ERROR,buf 内容未被刷新(HAL 不会清零),*temp 用了上一次或随机值,业务层毫无察觉。

正确完整代码

#include "stm32f1xx_hal.h"

int read_sensor_safe(float *temp)
{
    uint8_t buf[2] = {0};
    HAL_StatusTypeDef st;

    st = HAL_I2C_Mem_Read(&hi2c1, 0x44<<1, 0x00, 1, buf, 2, 100);
    if (st != HAL_OK) {            // 显式比较,错误即返回
        *temp = -999.0f;           // 标记无效,或触发重试
        return -1;
    }
    *temp = (buf[0]<<8 | buf[1]) * 0.01f;
    return 0;
}

/* 调用方必须处理返回值 */
void loop(void)
{
    float t;
    if (read_sensor_safe(&t) != 0) {
        report_error();           // 上报告警/降级,而不是用陈旧值
        return;
    }
    use_temp(t);
}

改前 vs 改后实测对比

指标忽略返回值判返回值+降级
高温高湿卡数概率约 1%0(触发告警而非用旧值)
故障可定位性无日志难查错误码可追
业务数据可信度低高

[注意] 用 -Wunused-result + 对关键 API 加 __attribute__((warn_unused_result)),可让编译器帮你揪出漏判的调用。

下面是正确判错与降级的完整流程:

否

是

read_sensor_safe() 入口

调用 HAL_I2C_Mem_Read()

返回值 == HAL_OK?

*temp = -999.0f 标记无效

return -1

调用方 report_error() 上报告警

安全降级,不使用陈旧值

解析 buf 计算温度

return 0

use_temp(t) 正常使用

图 3:正确判错 + 降级的完整调用流程

五、收尾:要点复盘与最佳实践

核心要点:① 返回值是契约,忽略即风险;② 显式比较 == HAL_OK,别用裸 if;③ 错误要有闭环(重试/上报/降级),不能只打印。

最佳实践清单(可收藏):

  • 任何返回状态/数量的 API,调用后必判返回值。
  • 比较用 == HAL_OK/== 0,不要用裸 if(ret) 反逻辑。
  • 错误分支要有动作:重试有限次、上报日志、或安全降级。
  • 关键 API 加 warn_unused_result 属性,编译期拦截漏判。
  • 把"忽略返回值"列入 Code Review 红线。

进阶延伸:对不可恢复错误可用 assert 触发硬断点;量产固件建议错误码进环形日志,便于现场回捞。

互动:你还漏判过哪些"看似无害"的返回值?评论区见。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/a524282348/article/details/167260539

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--