扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

简易嵌入式错误码模块设计!

3小时前
246
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。

嵌入式项目中,经常遇到这样的场景:现场设备返回错误码-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;
}

三、总结

错误码设计的关键是匹配项目实际需求,不是越复杂越好:

    小型项目用极简或枚举方案,开发效率高,维护简单大型项目用结构化方案,定位精准

注意事项:

    1. 错误码值不可修改。一旦定义并发布了,错误码的数值和含义永远不改,只能新增。尽量提供解析函数。不写解析函数,调试时看到数字要翻代码查定义,效率太低。避免跨模块码值冲突。整型或枚举方案,要提前规划码段,比如GPIO占100~199,SPI占200~299,严格遵守。错误码与处理逻辑解耦。错误码只定义"是什么错"(比如SPI_BUS_BUSY),不定义"怎么处理"。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!