大家好,我是杂烩君。
在嵌入式项目中,经常遇到这样的场景:现场设备返回错误码-5,对着日志一脸茫然——究竟是哪个模块出错?是硬件故障还是参数非法?翻代码查半天定义,耽误定位时间。更糟的情况是,不同模块用了相同错误码表示不同含义,跨模块调用时问题更难排查。
本文总结嵌入式错误码设计的实用方法:什么场景选什么方案、如何避免常见陷阱、错误码模块如何设计。
一、错误码方案选择
错误码方案得看项目实际情况:
1. 项目规模与模块数量
小型项目(单MCU裸机、单驱动模块)模块少、异常简单,没必要搞复杂设计。中大型项目(多模块协同、嵌入式Linux、跨团队开发)如果还用简单方案,后面维护就是灾难。
2. 运行平台特性
裸机MCU没有系统标准错误码,错误定义完全自己说了算,关键是把硬件异常和逻辑异常都覆盖到。嵌入式Linux或RTOS环境有系统自带的errno,自定义错误码就得考虑兼容性,别和系统码冲突。
3. 异常类型复杂度
简单场景(参数校验、空指针、内存不足)这种纯软件逻辑错误,基础设计就够用。复杂场景涉及SPI/CAN总线、Flash擦写、DMA传输这些硬件外设异常,就需要细化错误信息,不然现场定位问题根本摸不着头脑。
4. 系统集成需求
如果错误码只在设备内部用,规则可以灵活点,团队商量着来就行。但要是跨设备传递、上报服务器、不同版本要兼容,那就必须标准化设计,不能随意改码值和含义。
二、三种方案
方案一:极简整型错误码
这种方案最简单粗暴,适合裸机小驱动、单功能模块这类异常类型少于10种的场景。早期写单片机驱动时经常这么用,见效快。
设计时遵循一个约定:成功固定为0,负数表示致命错误(硬件故障、参数非法),正数表示警告(非致命,可重试)。这样if(ret < 0)就能快速判断是否出错。
下面是模块化的代码实现:
/**
* @file error_simple.h
*/
#ifndef ERROR_SIMPLE_H
#define ERROR_SIMPLE_H
#include<stdint.h>
/* 全局通用错误码 */
#define ERR_OK 0 /* 成功 */
#define ERR_PARAM -1 /* 参数非法 */
#define ERR_TIMEOUT -2 /* 超时 */
#define ERR_HW_FAIL -3 /* 硬件故障 */
#define WARN_BUSY 1 /* 设备忙(非致命) */
/* 获取错误描述字符串 */
constchar* err_get_string(int err_code);
#endif/* ERROR_SIMPLE_H */
/**
* @file error_simple.c
* @brief 错误码解析实现
*/
#include"error_simple.h"
constchar* err_get_string(int err_code)
{
switch (err_code) {
case ERR_OK: return"Success";
case ERR_PARAM: return"Invalid parameter";
case ERR_TIMEOUT: return"Operation timeout";
case ERR_HW_FAIL: return"Hardware failure";
case WARN_BUSY: return"Device busy";
default: return"Unknown error";
}
}
方案二:枚举型错误码
中大型裸机或RTOS项目中,模块多了之后用整型错误码就力不从心了。枚举型方案能很好地解决这个问题,异常类型在10~50种之间时特别合适。
这种方案有几个关键点:使用枚举类型,编译器能做类型检查,防止低级错误;每个模块独立定义枚举,统一前缀(比如GPIO_ERR_、SPI_ERR_);提前规划码段(GPIO占100~199,SPI占200~299),彻底避免冲突。
看看具体怎么写:
/**
* @file error_common.h
* @brief 通用错误码基础定义(所有模块共享)
*/
#ifndef ERROR_COMMON_H
#define ERROR_COMMON_H
#include<stdint.h>
/* 全局通用错误基类 */
typedefenum {
ERR_OK = 0, /* 全局成功标志 */
ERR_PARAM = 1, /* 参数错误 */
ERR_MEMORY = 2, /* 内存不足 */
ERR_TIMEOUT = 3, /* 超时 */
ERR_UNKNOWN = 0xFF /* 未知错误 */
} err_base_t;
/* 错误码转字符串回调函数类型 */
typedefconstchar* (*err_to_string_fn)(int err_code);
/* 注册错误码解析器 */
voiderr_register_parser(uint8_t module_id, err_to_string_fn parser);
/* 统一错误码解析入口 */
constchar* err_parse(uint8_t module_id, int err_code);
#endif/* ERROR_COMMON_H */
/**
* @file error_common.c
* @brief 通用错误码解析实现
*/
#include"error_common.h"
#include<stddef.h>
#define MAX_MODULES 16
staticstruct {
uint8_t module_id;
err_to_string_fn parser;
} parser_table[MAX_MODULES];
staticint parser_count = 0;
voiderr_register_parser(uint8_t module_id, err_to_string_fn parser)
{
if (parser_count >= MAX_MODULES || parser == NULL) {
return;
}
parser_table[parser_count].module_id = module_id;
parser_table[parser_count].parser = parser;
parser_count++;
}
constchar* err_parse(uint8_t module_id, int err_code)
{
for (int i = 0; i < parser_count; i++) {
if (parser_table[i].module_id == module_id) {
return parser_table[i].parser(err_code);
}
}
return"Module parser not found";
}
/**
* @file error_gpio.h
* @brief GPIO模块错误码定义
*/
#ifndef ERROR_GPIO_H
#define ERROR_GPIO_H
#include"error_common.h"
/* GPIO模块错误码段:100~199 */
typedefenum {
GPIO_ERR_OK = ERR_OK,
GPIO_ERR_PIN = 100,
GPIO_ERR_MODE = 101,
GPIO_ERR_HW = 102,
GPIO_ERR_BUSY = 103
} gpio_err_t;
constchar* gpio_err_to_string(int err_code);
gpio_err_tgpio_init(uint8_t pin, uint8_t mode);
#endif/* ERROR_GPIO_H */
/**
* @file error_gpio.c
* @brief GPIO模块错误处理实现
*/
#include"error_gpio.h"
#include<stddef.h>
#define GPIO_MAX_PIN 31
constchar* gpio_err_to_string(int err_code)
{
switch ((gpio_err_t)err_code) {
case GPIO_ERR_OK: return"GPIO success";
case GPIO_ERR_PIN: return"GPIO pin number invalid";
case GPIO_ERR_MODE: return"GPIO mode invalid";
case GPIO_ERR_HW: return"GPIO hardware init failed";
case GPIO_ERR_BUSY: return"GPIO pin is busy";
default: return"GPIO unknown error";
}
}
使用示例:
#include"error_common.h"
#include"error_gpio.h"
#include<stdio.h>
#define MODULE_ID_GPIO 1
intmain(void)
{
/* 初始化时注册各模块的错误码解析器 */
err_register_parser(MODULE_ID_GPIO, gpio_err_to_string);
if (ret != GPIO_ERR_OK) {
printf("Error: %sn", err_parse(MODULE_ID_GPIO, ret));
}
return0;
}
方案三:结构化错误码
遇到多MCU协同、Linux驱动加应用层、模块很多、或者需要把错误码上报云端的场景,前面两种方案就不够用了。结构化错误码通过32位整型拆分字段,用位运算解析,能精确表达错误的来源和细节。
字段划分是这样的:用32位整型,高8位存模块ID(区分GPIO、SPI、CAN等),中8位存主错误类型(参数错误、硬件错误、总线错误等),低16位存子错误细节(比如SPI总线忙、CRC校验失败等具体原因)。
|-----8bit-----|-----8bit-----|--------16bit--------|
| 模块ID | 主错误码 | 子错误码 |
| (MODULE_ID) | (MAIN_ERR) | (SUB_ERR) |
模块化代码实现:
/**
* @file error_struct.h
* @brief 结构化错误码定义(适用于大型系统)
*/
#ifndef ERROR_STRUCT_H
#define ERROR_STRUCT_H
#include<stdint.h>
/* 错误码类型定义 */
typedefuint32_terr_code_t;
/* 字段位掩码定义 */
#define ERR_MODULE_MASK 0xFF000000U /* 高8位:模块ID */
#define ERR_MAIN_MASK 0x00FF0000U /* 中8位:主错误码 */
#define ERR_SUB_MASK 0x0000FFFFU /* 低16位:子错误码 */
/* 位移偏移量 */
#define ERR_MODULE_SHIFT 24
#define ERR_MAIN_SHIFT 16
#define ERR_SUB_SHIFT 0
/* 模块ID枚举 */
typedefenum {
MODULE_SYSTEM = 0x00, /* 系统模块 */
MODULE_GPIO = 0x01, /* GPIO模块 */
MODULE_SPI = 0x02, /* SPI模块 */
MODULE_CAN = 0x03, /* CAN模块 */
MODULE_UART = 0x04, /* UART模块 */
MODULE_APP = 0x10 /* 应用层 */
} module_id_t;
/* 主错误类型枚举 */
typedefenum {
MAIN_ERR_OK = 0x00, /* 成功 */
MAIN_ERR_PARAM = 0x01, /* 参数错误 */
MAIN_ERR_HW = 0x02, /* 硬件错误 */
MAIN_ERR_BUS = 0x03, /* 总线错误 */
MAIN_ERR_TIMEOUT = 0x04, /* 超时 */
MAIN_ERR_MEM = 0x05 /* 内存错误 */
} main_err_t;
/* 构造结构化错误码 */
#define ERR_MAKE(module, main, sub)
((err_code_t)(((module) << ERR_MODULE_SHIFT) |
((main) << ERR_MAIN_SHIFT) |
((sub) << ERR_SUB_SHIFT)))
/* 从错误码提取模块ID */
#define ERR_GET_MODULE(err_code)
(((err_code) & ERR_MODULE_MASK) >> ERR_MODULE_SHIFT)
/* 从错误码提取主错误码 */
#define ERR_GET_MAIN(err_code)
(((err_code) & ERR_MAIN_MASK) >> ERR_MAIN_SHIFT)
/* 从错误码提取子错误码 */
#define ERR_GET_SUB(err_code)
((err_code) & ERR_SUB_MASK)
/* 判断是否成功 */
#define ERR_IS_OK(err_code)
(ERR_GET_MAIN(err_code) == MAIN_ERR_OK)
/* 子错误码解析函数类型 */
typedefconstchar* (*err_sub_parser_fn)(uint16_t sub_code);
/* 注册模块的子错误码解析器 */
voiderr_register_sub_parser(uint8_t module_id, err_sub_parser_fn parser);
/* 解析错误码到字符串 */
interr_parse_to_string(err_code_t err_code, char *buf, size_t len);
/* 获取模块名称 */
constchar* err_get_module_name(uint8_t module_id);
/* 获取主错误描述 */
constchar* err_get_main_desc(uint8_t main_err);
#endif/* ERROR_STRUCT_H */
/**
* @file error_struct.c
* @brief 结构化错误码解析实现
*/
#include"error_struct.h"
#include<stdio.h>
#include<string.h>
#define MAX_MODULES 16
staticstruct {
uint8_t module_id;
err_sub_parser_fn parser;
} sub_parser_table[MAX_MODULES];
staticint sub_parser_count = 0;
constchar* err_get_module_name(uint8_t module_id)
{
switch (module_id) {
case MODULE_SYSTEM: return"SYSTEM";
case MODULE_GPIO: return"GPIO";
case MODULE_SPI: return"SPI";
case MODULE_CAN: return"CAN";
case MODULE_UART: return"UART";
case MODULE_APP: return"APP";
default: return"UNKNOWN";
}
}
constchar* err_get_main_desc(uint8_t main_err)
{
switch (main_err) {
case MAIN_ERR_OK: return"Success";
case MAIN_ERR_PARAM: return"Invalid parameter";
case MAIN_ERR_HW: return"Hardware failure";
case MAIN_ERR_BUS: return"Bus error";
case MAIN_ERR_TIMEOUT: return"Timeout";
case MAIN_ERR_MEM: return"Memory error";
default: return"Unknown error";
}
}
voiderr_register_sub_parser(uint8_t module_id, err_sub_parser_fn parser)
{
if (sub_parser_count >= MAX_MODULES || parser == NULL) {
return;
}
sub_parser_table[sub_parser_count].module_id = module_id;
sub_parser_table[sub_parser_count].parser = parser;
sub_parser_count++;
}
staticconstchar* find_sub_parser(uint8_t module_id, uint16_t sub_code)
{
for (int i = 0; i < sub_parser_count; i++) {
if (sub_parser_table[i].module_id == module_id) {
return sub_parser_table[i].parser(sub_code);
}
}
returnNULL;
}
interr_parse_to_string(err_code_t err_code, char *buf, size_t len)
{
if (buf == NULL || len == 0) {
return0;
}
uint8_tmodule = ERR_GET_MODULE(err_code);
uint8_t main = ERR_GET_MAIN(err_code);
uint16_t sub = ERR_GET_SUB(err_code);
constchar* sub_desc = find_sub_parser(module, sub);
if (sub_desc != NULL) {
returnsnprintf(buf, len, "[%s] %s - %s",
err_get_module_name(module),
err_get_main_desc(main),
sub_desc);
} else {
returnsnprintf(buf, len, "[%s] %s (sub:%d)",
err_get_module_name(module),
err_get_main_desc(main),
sub);
}
}
使用示例:
/**
* @file spi_driver.c
* @brief SPI模块错误码实现
*/
#include"error_struct.h"
#include<stdio.h>
#include<stddef.h>
/* SPI子错误码定义 */
#define SPI_SUB_ERR_NONE 0
#define SPI_SUB_ERR_BUS_BUSY 1
#define SPI_SUB_ERR_CRC_FAIL 2
#define SPI_SUB_ERR_NO_DEVICE 3
/* SPI子错误码解析函数 */
staticconstchar* spi_sub_err_to_string(uint16_t sub_code)
{
switch (sub_code) {
case SPI_SUB_ERR_NONE: return"None";
case SPI_SUB_ERR_BUS_BUSY: return"Bus busy";
case SPI_SUB_ERR_CRC_FAIL: return"CRC check failed";
case SPI_SUB_ERR_NO_DEVICE: return"No device";
default: return"Unknown sub error";
}
}
/* SPI驱动函数 */
err_code_tspi_transfer(uint8_t *data, uint32_t len)
{
if (data == NULL || len == 0) {
return ERR_MAKE(MODULE_SPI, MAIN_ERR_PARAM, SPI_SUB_ERR_NONE);
}
return ERR_MAKE(MODULE_SPI, MAIN_ERR_OK, SPI_SUB_ERR_NONE);
}
/* 应用层使用 */
staticvoidprint_error(err_code_t err)
{
char err_str[128];
err_parse_to_string(err, err_str, sizeof(err_str));
printf(" Parsed: %sn", err_str);
}
intmain(void)
{
err_code_t ret;
uint8_t data[10] = {0};
err_register_sub_parser(MODULE_GPIO, gpio_sub_err_to_string);
err_register_sub_parser(MODULE_SPI, spi_sub_err_to_string);
printf("gpio_init(10, 1):n");
ret = gpio_init(10, 1);
print_error(ret);
printf("n");
printf("spi_transfer(valid):n");
ret = spi_transfer(data, 10);
print_error(ret);
printf("n");
return0;
}
三、总结
错误码设计的关键是匹配项目实际需求,不是越复杂越好:
- 小型项目用极简或枚举方案,开发效率高,维护简单大型项目用结构化方案,定位精准
注意事项:
-
- 错误码值不可修改。一旦定义并发布了,错误码的数值和含义永远不改,只能新增。尽量提供解析函数。不写解析函数,调试时看到数字要翻代码查定义,效率太低。避免跨模块码值冲突。整型或枚举方案,要提前规划码段,比如GPIO占100~199,SPI占200~299,严格遵守。错误码与处理逻辑解耦。错误码只定义"是什么错"(比如SPI_BUS_BUSY),不定义"怎么处理"。
246