嵌入式 C 语言编程规范个人学习笔记,参考华为《C 语言编程规范》
嵌入式 C 语言编程规范文档
目录
0 规范制订说明
1 头文件
2 函数
3 标识符命名与定义
4 变量
5 宏、常量
6 质量保证
7 程序效率
8 注释
9 排版与格式
10 表达式
11 代码编辑、编译
12 可测性
13 安全性
14 单元测试
15 可移植性
16 业界编程规范
0 规范制订说明
0.1 前言
本规范旨在指导电子行业开发人员编写 “简洁、可维护、可靠、可测试、高效、可移植” 的 C 语言代码,适用于所有使用 C 语言编码的软件。规范修订基于对公司内部典型编码问题的分析总结,并参考业界最新成果,在 1999 年版基础上进行了梳理、优化与刷新。
规范分为完整版(含规则解释、正反示例及参考材料)和精简版(仅规则条文)。自发布之日起,新编写及修改的代码必须遵守本规范。对于嵌入式开发而言,由于系统生命周期长(通常 5-10 年)、硬件资源受限(如 8 位 MCU 的 RAM 多为 KB 级)、维护成本高(占生命周期成本的 40%-90%),规范的代码能显著降低调试难度与后期维护成本,尤其在多模块协作与硬件交互场景中作用突出。
0.2 代码总体原则
(1)清晰第一
代码首要目标是 “让人读懂”,其次才是 “机器执行”。嵌入式代码常涉及寄存器操作、中断处理等底层逻辑,清晰性不足会导致调试时难以定位问题(如传感器数据异常、电机控制失灵)。
- 实践要求:
- 代码应 “自解释”,通过合理命名(如
uart_tx_buf
而非buf1
)和简洁逻辑减少对注释的依赖。 - 优先保证可读性,再优化性能。例如,某温度采集函数为追求效率使用复杂位运算,后期因逻辑模糊难以排查精度问题;重构为分步计算后,虽耗时增加 5%,但稳定性显著提升。
- 代码应 “自解释”,通过合理命名(如
- 理论依据:维护期修改代码的成本,小型系统是开发期的 5 倍,大型系统(100 万行代码以上)可达 100 倍。清晰的代码能减少 “补错” 人力消耗,提升维护效率。
(2)简洁为美
代码越长,出错概率越高。嵌入式系统内存与存储资源有限,简洁的代码能减少资源占用,降低出错风险。
- 实践要求:
- 及时清除废弃代码(未被调用的函数、全局变量),重复逻辑提炼为函数(如多个模块共用的 CRC 校验逻辑)。
- 函数仅完成单一功能,避免 “大而全”。例如,将 “ADC 读取 + 数据转换 + 上报” 拆分为
adc_read()
、data_convert()
、report_to_host()
三个独立函数。
- 案例:某电机控制函数因包含初始化、调速、保护等功能长达 300 行,调试时 “过流保护失效” 问题定位耗时 2 天;拆分后,问题定位时间缩短至 2 小时。
(3)风格统一
团队共享统一编码风格,可降低协作成本。嵌入式项目常涉及硬件驱动、应用逻辑等多模块开发,风格不一致会增加跨模块理解难度。
- 实践要求:
- 选择 Unix 风格(小写 + 下划线,如
spi_init()
)或 Windows 风格(大小写混合,如SpiInit()
),嵌入式领域更倾向 Unix 风格(与硬件手册命名习惯一致)。 - 修改旧代码时,保持原有风格(如 legacy 代码使用匈牙利命名法,新增代码暂时沿用,避免混合混乱)。
- 选择 Unix 风格(小写 + 下划线,如
0.3 规范实施与解释
- 本规范由质量体系负责维护,实施过程中出现的问题可通过内部论坛讨论解决。
- 特殊场景(如 BSP 驱动开发)需违反规则时,必须经团队正式评审决策,个人不得擅自违反。
- 对嵌入式开发的特殊说明:因硬件差异(如 ARM 与 RISC-V 架构),若需针对特定芯片修改规则(如使用汇编优化时序),需团队共同确认,避免后续移植困难。
0.4 术语定义
- 原则:编程时必须坚持的指导思想(如 “清晰第一”)。
- 规则:强制遵守的约定(如 “头文件禁止循环依赖”)。
- 建议:需优先考虑的约定(如 “函数参数不超过 5 个”)。
1 头文件
背景
头文件设计体现系统架构,不合理的头文件布局会导致编译时间过长、模块耦合度高。例如,某产品实验表明,即使注释掉所有函数实现,编译时间仅减少 10%,因头文件依赖链复杂(A 包含 B,B 包含 C,最终几乎所有文件都包含了项目所有头文件)。
原则 1.1 头文件中适合放置接口的声明,不适合放置实现
头文件是模块对外接口,应放置对外函数声明、宏定义、类型定义等,内部逻辑(如驱动缓冲区)应放在.c
文件中。
- 禁止内容:
- 内部函数声明(仅模块内使用的函数)。
- 内部宏、枚举、结构定义(仅模块内使用)。
- 变量定义(会导致多文件包含时重复定义)。
- 嵌入式反例:某
oled.h
包含oled_draw_pixel()
的实现代码,导致多个.c
文件包含时出现函数重定义错误,且修改绘制逻辑需重新编译所有依赖模块。 - 正确做法:
oled.h
声明oled_draw_pixel()
,oled.c
实现具体逻辑。
原则 1.2 头文件应当职责单一
头文件过于复杂会导致依赖链混乱,增加编译时间。例如,某平台定义WORD
类型的头文件包含 20 个无关头文件,导致 10000 个源文件中stdio.h
被不必要展开 9900 次,编译时间增加 300%。
- 实践要求:避免 “万能头文件”,按功能拆分。例如,
board.h
不应同时包含 GPIO、UART、SPI 的定义,应拆分为gpio.h
、uart.h
、spi.h
。
原则 1.3 头文件应向稳定的方向包含
依赖关系应遵循 “不稳定模块依赖稳定模块”,避免稳定模块依赖不稳定模块。例如:
- 正确:传感器驱动(不稳定)依赖硬件抽象层 HAL(稳定)。
- 错误:HAL 依赖传感器驱动,导致 HAL 无法单独编译测试。
规则 1.1 每一个.c
文件应有一个同名.h
文件,用于声明需要对外公开的接口
- 说明:
adc_driver.c
对应adc.h
,adc.h
声明adc_init()
、adc_read()
等接口,方便其他模块调用。 - 例外:
main.c
(无对外接口)、纯定义文件(如cmd_ids.h
仅定义命令 ID,无对应.c
)。 - 嵌入式实践:驱动文件
uart_driver.c
对应uart.h
,声明uart_init()
、uart_send()
,应用层通过包含uart.h
调用驱动功能。
规则 1.2 禁止头文件循环依赖
- 危害:
a.h
包含b.h
,b.h
包含a.h
,修改任一文件需重新编译所有关联模块,在大型嵌入式项目(如车载系统)中会显著增加编译时间。 - 解决方法:提取公共接口到独立头文件。例如,
a.h
和b.h
共有的类型定义移至common.h
,两者均包含common.h
而非互相包含。
规则 1.3 .c
/.h
文件禁止包含用不到的头文件
- 嵌入式痛点:MCU 交叉编译资源有限,多余头文件会增加编译时间和固件体积。例如,某 LED 驱动无需
ethernet.h
,若误包含会导致不必要的依赖。
规则 1.4 头文件应当自包含
头文件应可独立编译,无需用户额外包含其他文件。例如,i2c.h
中使用uint8_t
,需自行包含<stdint.h>
,而非要求用户在包含i2c.h
前手动包含。
规则 1.5 总是编写内部#include
保护符(#define
保护)
- 目的:防止头文件被多次包含,命名格式为
PROJECT_PATH_FILENAME_H
(如BOARD_INCLUDE_UART_H
)。 - 示例:
c
#ifndef BOARD_INCLUDE_UART_H #define BOARD_INCLUDE_UART_H // 头文件内容 #endif
- 例外:版权声明和整体注释可放在保护符外。
规则 1.6 禁止在头文件中定义变量
- 危害:头文件中定义
int g_uart_flag;
,多个.c
包含时会导致变量重定义。 - 正确做法:
.c
中定义(int g_uart_flag;
),.h
中声明(extern int g_uart_flag;
)。
规则 1.7 只能通过包含头文件的方式使用其他.c
提供的接口,禁止在.c
中通过extern
的方式使用外部函数接口、变量
- 说明:使用其他模块的
foo()
函数时,应包含其头文件(如#include "bar.h"
),而非在当前.c
中写extern int foo();
,避免声明与定义不一致(如参数修改后未同步)。
规则 1.8 禁止在extern "C"
中包含头文件
- 危害:可能改变被包含头文件的链接规范(如 C++ 函数被误标记为 C 链接),导致编译错误。
- 正确做法:先包含头文件,再声明
extern "C"
:c
#include "xxx.h" extern "C" {// 函数声明 }
建议 1.1 一个模块通常包含多个.c
文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个.h
,文件名为目录名
- 说明:模块对外提供一个统一接口头文件,掩盖内部实现细节。例如,Google Test 框架对外仅提供
gtest.h
,用户无需关心内部文件结构。 - 嵌入式实践:
sensor/
目录包含temp_sensor.c
、humidity_sensor.c
,对外提供sensor.h
,声明sensor_init()
、sensor_read_all()
等接口。
建议 1.2 如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的.h
,文件名为子模块名
- 说明:降低接口使用者的编写难度。例如,
communication/
模块包含uart/
、spi/
子模块,分别提供uart.h
、spi.h
。
建议 1.3 头文件不要使用非习惯用法的扩展名,如.inc
- 原因:
.inc
不被 IDE(如 Keil、IAR)识别为头文件,导致 “跳转到定义” 等功能失效,增加调试难度。
建议 1.4 同一产品统一包含头文件排列方式
- 常见方式:
- 功能块排序(如先标准库、再平台头文件、最后应用头文件)。
- 文件名升序(避免重复包含)。
- 稳定度排序(不稳定头文件放前面,如产品头文件放平台头文件前,便于快速定位错误)。
2 函数
背景
函数设计的核心是 “编写整洁函数,同时有效组织代码”。整洁函数要求逻辑简单、意图明确,代码组织需兼顾逻辑层(模块接口)与物理层(目录结构、命名)。
原则 2.1 一个函数仅完成一件功能
- 说明:函数实现多个功能会增加开发、使用、维护难度。例如,标准库
realloc
因同时承担内存分配、释放、迁移等功能,易导致内存泄漏(如buffer = realloc(buffer, new_size)
若失败,原内存地址丢失)。 - 嵌入式实践:将 “UART 接收 + 解析 + 发送响应” 拆分为
uart_recv()
、uart_parse()
、uart_send_resp()
,各自专注单一功能。
原则 2.2 重复代码应该尽可能提炼成函数
- 说明:重复代码会增加维护成本。当一段代码重复两次时,应考虑消除重复;超过三次时,必须提炼为函数。
- 案例:某项目中 CRC 校验代码重复出现 5 次,某次算法更新后漏改 2 处,导致设备通信异常;提炼为
crc16_check()
后,未再出现类似问题。
规则 2.1 避免函数过长,新增函数不超过 50 行(非空非注释行)
- 说明:过长函数往往功能不单一、逻辑复杂。函数有效代码行数(NBNC)应在 [1,50] 区间。
- 例外:算法实现函数(如 FFT)因逻辑聚合可超过 50 行。
- 嵌入式必要性:嵌入式调试依赖单步跟踪,过长函数会增加定位问题的难度。
规则 2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过 4 层
- 说明:嵌套过深会增加阅读难度,需记住过多上下文。优秀代码嵌套深度参考值为 [1,4]。
- 反例:
c
void serial(void) {if (!Received) {TmoCount = 0;switch (Buff) {case AISGFLG:if ((TiBu.Count > 3) && (TiBuff.Buf[0] == 0xff || TiBuff.Buf[0] == CurPa.ADR)) {Flg7E = false;Received = true;} else {TiBuff.Count = 0;Flg7D = false;Flg7E = true;}break;default:break;}} }
- 优化方案:拆分内层逻辑为独立函数,减少嵌套。
规则 2.3 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护
- 可重入性:函数被多个任务并发调用时结果一致,嵌入式中断服务程序(ISR)和多任务函数需保证可重入。
- 实现要点:
- 不使用全局变量 / 静态变量,或通过信号量 / 关中断保护。
- 示例(带互斥的可重入函数):
c
uint32_t get_sensor_data(void) {uint32_t data;sem_wait(&sensor_sem); // 申请信号量data = sensor_reg; // 访问共享寄存器sem_post(&sensor_sem); // 释放信号量return data; }
规则 2.4 对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组 / 模块内应统一规定。缺省由调用者负责
- 说明:避免重复检查或遗漏。例如,
i2c_write()
规定由调用者检查地址合法性,函数内部无需重复检查。
规则 2.5 对函数的错误返回码要全面处理
- 嵌入式风险:忽略错误码可能导致硬件操作失败(如 SPI 通信超时未处理,导致传感器数据丢失)。
- 反例:
c
FILE *fp = fopen("config.bin", "r"); fread(buf, 1, 32, fp); // fp为NULL时崩溃
- 正例:
c
FILE *fp = fopen("config.bin", "r"); if (fp == NULL) {log_error("Open config failed");return -1; }
规则 2.6 设计高扇入,合理扇出(小于 7)的函数
- 扇入:调用该函数的上级函数数量(越高越好,说明复用性强)。
- 扇出:函数直接调用的其他函数数量(建议 3~5,过高表示函数复杂)。
- 嵌入式应用:底层驱动函数(如
gpio_set()
)应高扇入,调度函数(如task_scheduler()
)扇出可稍高。
规则 2.7 废弃代码(没有被调用的函数和变量)要及时清除
- 危害:废弃代码占用空间,可能影响功能与性能,增加测试、维护难度。
建议 2.1 函数不变参数使用const
- 示例:
void uart_send(const uint8_t *data, uint16_t len)
,data
为const
表明函数不修改输入,增强安全性与可读性。
建议 2.2 函数应避免使用全局变量、静态局部变量和 I/O 操作,不可避免的地方应集中使用
- 原因:静态局部变量会使函数行为不可预测(如
static uint8_t cnt
在中断中累加,结果依赖历史状态)。 - 例外:函数返回指针时,需用静态变量(如
char* get_version()
返回静态缓冲区地址)。
建议 2.3 检查函数所有非参数输入的有效性,如数据文件、公共变量等
- 示例:
c
// 错误:未检查非参数输入 hr = root_node->get_first_child(&log_item); // list.xml为空,log_item为NULL hr = log_item->get_next_sibling(&media_next_node); // 空指针访问导致宕机 // 正确:检查输入有效性 hr = root_node->get_first_child(&log_item); if (log_item == NULL) return retValue; hr = log_item->get_next_sibling(&media_next_node);
建议 2.4 函数的参数个数不超过 5 个
- 处理方法:参数过多时封装为结构体。例如,
void config_timer(TimerConfig *cfg)
,TimerConfig
包含频率、模式等参数。
建议 2.5 除打印类函数外,不要使用可变长参函数
- 原因:可变长参函数处理复杂,易引入错误,维护难度大。
建议 2.6 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static
关键字
- 说明:
static
确保函数仅在当前文件可见,避免与其他文件标识符冲突。建议定义STATIC
宏,调试时为static
,发布时为空,便于热补丁操作:c
#ifdef DEBUG #define STATIC static #else #define STATIC #endif
3 标识符命名与定义
3.1 通用命名规则
原则 3.1 标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解
- 示例:
- 好:
error_number
、number_of_completed_connection
- 差:
n
、nerr
、n_comp_conns
(缩写模糊)
- 好:
原则 3.2 除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音
- 通用缩写:
buf
(buffer)、cfg
(configuration)、dev
(device)、err
(error)等。 - 禁用:
dta
(data)、shuju
(数据)。
规则 3.1 产品 / 项目组内部应保持统一的命名风格
- 说明:选择 Unix 风格或 Windows 风格,保持一致。例外:即使产品之前使用匈牙利命名法,新代码也不应使用。
建议 3.1 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等
- 示例:
add/remove
、create/destroy
、lock/unlock
、send/receive
。
建议 3.2 尽量避免名字中出现数字编号,除非逻辑上的确需要编号
- 反例:
EXAMPLE_0_TEST_
、EXAMPLE_1_TEST_
,应改为EXAMPLE_UNIT_TEST
、EXAMPLE_ASSERT_TEST
。
建议 3.3 标识符前不应添加模块、项目、产品、部门的名称作为前缀
- 危害:文件名过长,与模块绑定不利于维护移植(如
a_module_foo.c
迁移到b_module
需改名)。
建议 3.4 平台 / 驱动等适配代码的标识符命名风格保持和平台 / 驱动一致
- 说明:外购芯片及配套驱动的适配代码,保持原有风格,便于后续升级。
建议 3.5 重构 / 修改部分代码时,应保持和原有代码的命名风格一致
- 说明:维持代码整体一致性,降低阅读难度。
3.2 文件命名规则
建议 3.6 文件命名统一采用小写字符
- 原因:不同系统对文件名大小写处理不同(如 Windows 不区分,Linux 区分),全小写可避免冲突。
3.3 变量命名规则
规则 3.2 全局变量应增加 “g_” 前缀
- 示例:
g_system_tick
(全局系统滴答计数器),突出全局性,提醒谨慎修改。
规则 3.3 静态变量应增加 “s_” 前缀
- 示例:
static uint8_t s_uart_rx_buf[64]
,表明仅当前文件可见。
规则 3.4 禁止使用单字节命名变量,但允许定义i
、j
、k
作为局部循环变量
- 反例:
int a;
(含义不明),正例:int sensor_id;
建议 3.7 不建议使用匈牙利命名法
- 原因:变量名应体现含义而非类型,修改类型时需批量改变量名,降低可读性。
建议 3.8 使用名词或者形容词 + 名词方式命名变量
- 示例:
temperature
、max_temperature
。
3.4 函数命名规则
建议 3.9 函数命名应以函数要执行的动作命名,一般采用动词或者动词 + 名词的结构
- 示例:
adc_read()
、motor_start()
、get_current_directory()
。
建议 3.10 函数指针除了前缀,其他按照函数的命名规则命名
- 示例:
typedef int (*DataProcessFunc)(uint8_t *data, uint16_t len);
3.5 宏的命名规则
规则 3.5 对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线的方式命名(枚举同样建议使用此方式定义)
- 示例:
#define PI_ROUNDED 3.14
、enum { UART_BAUD_9600, UART_BAUD_115200 }
。
规则 3.6 除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线开头和结尾
- 原因:下划线开头的标识符可能与系统库冲突(C99 规定系统宏通常以 “_” 开头)。
4 变量
原则 4.1 一个变量只有一个功能,不能把一个变量用作多种用途
- 反例:
c
WORD DelRelTimeQue(void) {WORD Locate;Locate = 3;Locate = DeleteFromQue(Locate); // Locate既表示位置,又表示返回值return Locate; }
- 正例:
c
WORD DelRelTimeQue(void) {WORD Ret;WORD Locate = 3;Ret = DeleteFromQue(Locate);return Ret; }
原则 4.2 结构功能单一;不要设计面面俱到的数据结构
- 说明:结构应抽象同一对象,元素需相关。
- 反例:
c
typedef struct STUDENT_STRU {unsigned char name[32]; // 学生姓名unsigned char age; // 学生年龄unsigned char teacher_name[32]; // 教师姓名(与学生关联性弱) } STUDENT;
- 正例:
c
typedef struct TEACHER_STRU {unsigned char name[32];unsigned int id; } TEACHER; typedef struct STUDENT_STRU {unsigned char name[32];unsigned char age;unsigned int teacher_id; // 关联教师ID } STUDENT;
原则 4.3 不用或者少用全局变量
- 说明:全局变量易导致模块耦合,建议通过函数接口访问。单个文件内部可使用
static
全局变量(类似类的私有成员)。 - 实践:定义
STATIC
宏,调试时为static
,发布时为空,便于热补丁:c
#ifdef DEBUG #define STATIC static #else #define STATIC #endif STATIC int s_module_data; // 模块私有数据
规则 4.1 防止局部变量与全局变量同名
- 危害:虽不报错,但易混淆,导致逻辑错误。
规则 4.2 通讯过程中使用的结构,必须注意字节序
- 说明:不同 MCU 字节序不同(大端 / 小端),通讯结构体成员需转换字节序。发送前转为网络序,接收后转为主机序。
- 示例:
c
uint32_t value = 0x12345678; uint32_t net_value = htonl(value); // 转为大端(网络序)
规则 4.3 严禁使用未经初始化的变量作为右值
- 风险:嵌入式栈内存初始值随机,未初始化变量可能导致硬件操作异常(如用未初始化的
len
作为 SPI 发送长度)。
建议 4.1 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象
- 说明:降低全局变量耦合度,避免多人修改导致的冲突。
建议 4.2 使用面向接口编程思想,通过 API 访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥
- 示例:风扇管理模块提供
SetFanWorkMode()
、GetFanSpeed()
等接口,而非直接暴露g_fan_mode
。
建议 4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好
- 示例:
c
// 不佳:初始化与使用分离 int speedup_factor; if (condition) speedup_factor = 2; else speedup_factor = -1; // 较好:初始化与使用紧密 int speedup_factor = condition ? 2 : -1;
建议 4.4 明确全局变量的初始化顺序,避免跨模块的初始化依赖
- 危害:系统启动阶段,若全局变量 A 依赖 B 的初始化,但 B 未初始化,会导致 A 值异常。
建议 4.5 尽量减少没有必要的数据类型默认转换与强制转换
- 风险:类型转换可能导致值的含义变化(如
char ch = -1; unsigned short exam = ch;
结果为0xFF
)。
5 宏、常量
规则 5.1 用宏定义表达式时,要使用完备的括号
- 反例:
#define MUL(a,b) a*b
,调用MUL(2+3,4)
会展开为2+3*4=14
(预期 20)。 - 正例:
#define MUL(a,b) ((a)*(b))
规则 5.2 将宏所定义的多条表达式放在大括号中,建议使用do-while(0)
包裹
- 反例:
c
#define FOO(x) printf("arg: %d\n", x); do_something(x); // 调用时若不加括号,仅第一条语句执行 if (cond) FOO(x);
- 正例:
c
#define FOO(x) do { \printf("arg: %d\n", x); \do_something(x); \ } while(0)
规则 5.3 使用宏时,不允许参数发生变化
- 反例:
#define SQUARE(a) ((a)*(a))
,调用SQUARE(i++)
会导致i
自增两次。 - 正例:
val = SQUARE(i); i++;
规则 5.4 不允许直接使用魔鬼数字
- 危害:代码难以理解,修改时需多处改动。
- 解决方法:定义宏或
const
变量,命名自解释。例如:c
// 错误:魔鬼数字 uart_init(115200); // 正确:宏定义 #define UART_BAUD_RATE 115200 uart_init(UART_BAUD_RATE);
建议 5.1 除非必要,应尽可能使用函数代替宏
- 宏的缺点:缺乏类型检查、易产生副作用、难以调试。
- 示例:
c
// 宏定义(有副作用) #define MAX(a,b) ((a) > (b) ? (a) : (b)) // 函数(安全) int max(int a, int b) { return a > b ? a : b; }
建议 5.2 常量建议使用const
定义代替宏
- 优势:
const
有类型检查,可加入符号表,便于调试。例如:c
const double ASPECT_RATIO = 1.653; // 优于#define ASPECT_RATIO 1.653
建议 5.3 宏定义中尽量不使用return
、goto
、continue
、break
等改变程序流程的语句
- 危害:可能导致资源泄漏。例如:
c
#define CHECK_AND_RETURN(cond, ret) { if (cond == NULL) return ret; } // 调用时若提前返回,内存未释放 pMem = malloc(64); CHECK_AND_RETURN(pMem, -1); // 若pMem为NULL,无内存泄漏 pMem2 = malloc(64); CHECK_AND_RETURN(pMem2, -1); // 若pMem2为NULL,pMem未释放,导致泄漏
6 质量保证
原则 6.1 代码质量保证优先原则
质量属性优先级:
- 正确性(实现设计功能)
- 简洁性(易于理解实现)
- 可维护性(便于修改)
- 可靠性(按设计运行的概率)
- 可测试性(发现并定位故障的能力)
- 性能高效(少占资源)
- 可移植性(跨环境运行能力)
- 个人表达方式(最低优先级)
原则 6.2 要时刻注意易混淆的操作符
- 易混淆操作符:
- 赋值
=
与相等==
- 逻辑与
&&
与位与&
- 逻辑或
||
与位或|
- 自增
++
、自减--
的前缀 / 后缀差异
- 赋值
- 示例:
if (a = b)
(赋值)与if (a == b)
(相等判断)的区别。
原则 6.3 必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等
- 嵌入式内存分配:
- 局部变量:栈(Stack)
- 静态变量 / 全局变量:数据段(Data/BSS)
- 动态分配:堆(Heap)
原则 6.4 不仅关注接口,同样要关注实现
- 说明:实现细节可能影响接口功能(如物理内存限制、网络状况),需兼顾接口与实现。
规则 6.1 禁止内存操作越界
- 危害:数组、指针操作越界可能改写硬件寄存器,导致系统崩溃。
- 预防措施:
- 使用
strncpy
、snprintf
等安全函数,避免strcpy
、sprintf
。 - 字符串预留
\0
空间(如char buf[16]
最多存 15 个字符)。 - 检查数组下标、指针加减范围。
- 使用
- 案例:某设备因
char TempShold[10]
存储 10 位整数(需 11 字节),导致缓冲区溢出,系统宕机。
规则 6.2 禁止内存泄漏
- 危害:嵌入式系统无内存回收机制,泄漏会导致堆耗尽。
- 预防措施:
- 异常出口释放所有资源(内存、文件句柄等)。
- 使用
goto
统一处理释放逻辑:c
int init_devices() {void *p1 = malloc(64);if (!p1) goto err;void *p2 = malloc(64);if (!p2) goto err_p1;return 0; err_p1:free(p1); err:return -1; }
规则 6.3 禁止引用已经释放的内存空间
- 预防措施:
- 内存释放后置
NULL
:free(buf); buf = NULL;
- 使用前检查指针非空:
if (buf != NULL) { ... }
- 内存释放后置
规则 6.4 编程时,要防止差 1 错误
- 示例:
for (i=0; i <= 10; i++)
(预期循环 10 次,实际 11 次)。
规则 6.5 所有的if....else if
结构应该由else
子句结束;switch
语句必须有default
分支
- 嵌入式必要性:未处理的分支可能导致硬件状态异常(如
switch
未处理的命令码导致电机停转)。
建议 6.1 函数中分配的内存,在函数退出之前要释放
- 说明:在申请处注释释放位置,避免遗漏。
建议 6.2 if
语句尽量加上else
分支,对没有else
分支的语句要小心对待
- 说明:明确处理所有情况,避免逻辑漏洞。
建议 6.3 不要滥用goto
语句
- 合理使用场景:多重循环退出、统一释放资源。例如:
c
int foo() {char *p1 = malloc(64);if (!p1) goto exit;// ... 其他操作 exit:free(p1);return 0; }
建议 6.4 时刻注意表达式是否会上溢、下溢
- 示例:
unsigned char size = 0; while (size >= 0) { size--; }
(死循环,size
下溢为0xFF
)。
7 程序效率
原则 7.1 在保证软件系统的正确性、简洁、可维护性、可靠性及可测性的前提下,提高代码效率
- 说明:不牺牲可读性换取效率,仅当性能成为瓶颈时优化。例如,某函数因异常处理在前导致正常流程稍慢,但代码清晰;仅当证明该函数是瓶颈时,才调整分支顺序。
原则 7.2 通过对数据结构、程序算法的优化来提高效率
- 示例:用哈希表代替链表查找,将 O (n) 复杂度降至 O (1)。
建议 7.1 将不变条件的计算移到循环体外
- 示例:
c
// 低效:每次循环调用函数 for (i=0; i < func_calc_max(); i++) { ... } // 高效:一次计算 int max = func_calc_max(); for (i=0; i < max; i++) { ... }
建议 7.2 对于多维大数组,避免来回跳跃式访问数组成员
- 原理:嵌入式 CPU 缓存小,连续访问可提升缓存命中率。
- 示例:
c
// 低效:按列访问(跳跃式) for (i=0; i < SIZE_B; i++)for (j=0; j < SIZE_A; j++)sum += x[j][i]; // 高效:按行访问(连续) for (i=0; i < SIZE_B; i++)for (j=0; j < SIZE_A; j++)sum += x[i][j];
建议 7.3 创建资源库,以减少分配对象的开销
- 说明:使用内存池、线程池,避免频繁分配释放。例如,预分配 10 个缓冲区,按需分配与回收,减少
malloc
/free
调用。
建议 7.4 将多次被调用的 “小函数” 改为inline
函数或者宏实现
- 说明:减少函数调用开销,但需权衡代码体积与效率。
inline
函数便于调试,宏可跨文件使用。
8 注释
原则 8.1 优秀的代码可以自我解释,不通过注释即可轻易读懂
- 说明:注释无法弥补糟糕的代码,需大量注释的代码往往需要重构。
- 示例:
c
// 差:需注释解释 int p(int m) {int k = sqrt(m);for (i=2; i <=k; i++)if (m%i ==0) break;return i>k ? 1 :0; } // 好:自解释 int IsPrimeNumber(int num) {int sqrt_of_num = sqrt(num);for (i=2; i <= sqrt_of_num; i++)if (num%i ==0) return 0;return 1; }
原则 8.2 注释的内容要清楚、明了,含义准确,防止注释二义性
- 反例:注释与代码矛盾,或描述模糊。
- 正例:明确说明设计意图,如 “上报故障 ID 与恢复 ID 由告警级别确定,清除级别用 ClearId,否则用 AlarmId”。
原则 8.3 在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描述代码
- 冗余注释:
i++; // increment i
- 有效注释:解释算法原理、硬件特性等,如 “芯片存在写错误,需回读校验,最多重试 3 次”。
规则 8.1 修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性。不再有用的注释要删除
- 说明:避免注释误导,无用代码从版本库找回,不留在注释中。
规则 8.2 文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明
- 示例:
c
/************************************************* Copyright © Huawei Technologies Co., Ltd. 1998-2023. All rights reserved. File name: uart.h Author: Zhang San (ID: 12345) Version: V1.0 Date: 2023-01-01 Description: UART驱动接口,支持波特率9600/115200,提供初始化、发送、接收功能。 Others: 依赖gpio.h配置引脚。 History: 2023-01-01: 初始版本(Zhang San) *************************************************/
规则 8.3 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等
- 示例:
c
/*** @brief 读取ADC值* @param channel 通道号(0~7)* @return 转换后的值(0~4095),失败返回-1* @note 可重入,需关中断保护共享寄存器*/ int adc_read(uint8_t channel);
规则 8.4 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明
- 示例:
c
/*** 全局GT翻译错误码* 取值范围:0-成功,1-GT表错误,2-GT值错误* 仅SCCPTranslate()可修改,其他模块通过GetGTTransErrorCode()访问*/ BYTE g_GTTranErrorCode;
规则 8.5 注释应放在其代码上方相邻位置或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同
- 示例:
c
// 激活的统计任务数 #define MAX_ACT_TASK_NUMBER 1000 enum {N_UNITDATA_IND, // 通知用户有数据到达N_NOTICE_IND // 通知网络传输失败 };
规则 8.6 对于switch
语句下的case
语句,如果因为特殊情况需要处理完一个case
后进入下一个case
处理,必须在该case
语句处理完、下一个case
语句前加上明确的注释
- 示例:
c
case CMD_FWD:ProcessFwd();/* 继续处理CMD_A */ case CMD_A:ProcessA();break;
规则 8.7 避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写
规则 8.8 同一产品或项目组统一注释风格
建议 8.1 避免在一行代码或表达式的中间插入注释
建议 8.2 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达
建议 8.3 文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式(如 doxygen)
- 示例:
c
/*** @file uart.h* @brief UART驱动接口*/
9 排版与格式
规则 9.1 程序块采用缩进风格编写,每级缩进为 4 个空格
- 说明:禁用
Tab
,避免不同编辑器显示差异。
规则 9.2 相对独立的程序块之间、变量说明之后必须加空行
- 示例:
c
if (!valid_ni(ni)) {// 处理逻辑 }repssn_ind = ssn_data[index].repssn_index;
规则 9.3 一条语句不能过长,如不能拆分需要分行写。一行字符数建议不超过 132(兼顾宽屏与 VTY)
- 换行建议:
- 换行时增加一级缩进。
- 低优先级操作符处拆分,操作符放新行首。
规则 9.4 多个短语句(包括赋值语句)不允许写在同一行内,即一行只写一条语句
- 反例:
int a=5; int b=10;
- 正例:
c
int a=5; int b=10;
规则 9.5 if
、for
、do
、while
、case
、switch
、default
等语句独占一行
- 建议:执行语句加
{}
,即使单行也如此,避免逻辑错误。
规则 9.6 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->
),后不应加空格
- 示例:
c
a = b + c; if (d > e) { ... } p->id = pid;
建议 9.1 注释符(包括/*
、//
、*/
)与注释内容之间要用一个空格进行分隔
建议 9.2 源程序中关系较为紧密的代码应尽可能相邻
10 表达式
规则 10.1 表达式的值在标准所允许的任何运算次序下都应该是相同的
- 风险:子表达式运算次序未定义,可能导致结果不一致。
- 反例:
c
x = b[i] + i++; // i++次序不确定 func(i++, i); // 参数计算次序不确定
- 正例:
c
x = b[i] + i; i++; i++; func(i, i);
建议 10.1 函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利
- 反例:
printf("%d, %d", fun1(), fun2());
(函数调用次序不确定) - 正例:
c
int a = fun1(); int b = fun2(); printf("%d, %d", a, b);
建议 10.2 赋值语句不要写在if
等语句中,或者作为函数的参数使用
- 反例:
if ((x = y) != 0) { ... }
(易混淆=
与==
) - 正例:
x = y; if (x != 0) { ... }
建议 10.3 用括号明确表达式的操作顺序,避免过分依赖默认优先级
- 示例:
x = (a * 3) + c;
(明确乘法优先)
建议 10.4 赋值操作符不能使用在产生布尔值的表达式上
- 反例:
if (x = y) { ... }
- 正例:
x = y; if (x != 0) { ... }
11 代码编辑、编译
规则 11.1 使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警
- 说明:编译器告警常提示潜在问题,如类型不匹配、未使用变量等。
规则 11.2 在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略
- 说明:局部禁用告警需注释原因,避免团队成员重复处理。
规则 11.3 本地构建工具(如 PC-Lint)的配置应该和持续集成的一致
- 说明:避免本地构建通过但持续集成失败。
规则 11.4 使用版本控制(配置管理)系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功
- 说明:降低集成难度,避免代码冲突。
建议 11.1 要小心地使用编辑器提供的块拷贝功能编程
- 说明:拷贝代码易导致重复或逻辑错误,需仔细检查。
12 可测性
原则 12.1 模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难
- 单元测试依赖:
- 接口定义清晰、稳定。
- 功能有明确验收条件。
- 关键状态和数据可查询、修改。
规则 12.1 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明
- 说明:通过编译开关区分测试与发行版本,统一调测接口。
规则 12.2 在同一项目组或产品组内,调测打印的日志要有统一的规定
- 内容要求:包含模块名、行号、函数名、错误码等,便于定位问题。
规则 12.3 使用断言记录内部假设
- 说明:断言检查内部逻辑,如
assert(ptr != NULL);
,调试阶段生效,发布阶段禁用。
规则 12.4 不能用断言来检查运行时错误
- 说明:断言用于内部逻辑校验,外部输入错误需用防错程序处理(如参数检查)。
建议 12.1 为单元测试和系统故障注入测试准备好方法和通道
- 说明:便于模拟异常场景(如网络中断、传感器故障),验证系统容错能力。
13 安全性
原则 13.1 对用户输入进行检查
- 说明:用户输入可能恶意或有误,需验证合法性,避免 DOS、注入攻击等。
13.1 字符串操作安全
规则 13.1 确保所有字符串是以 NULL 结束
- 示例:
c
char a[16]; strncpy(a, "0123456789abcdef", sizeof(a)-1); a[sizeof(a)-1] = '\0';
规则 13.2 不要将边界不明确的字符串写到固定长度的数组中
- 反例:
strcpy(buff, getenv("EDITOR"));
(环境变量长度不确定) - 正例:
c
char *editor = getenv("EDITOR"); char *buff = malloc(strlen(editor) + 1); strcpy(buff, editor);
13.2 整数安全
规则 13.3 避免整数溢出
- 示例:
c
unsigned int length; if (length < FSM_HDRLEN) return ERROR; length -= FSM_HDRLEN; // 避免下溢
规则 13.4 避免符号错误
- 反例:
if (len < BUF_SIZE) memcpy(buf, data, len);
(len
为负时检查失效) - 正例:
if (len > 0 && len < BUF_SIZE) memcpy(buf, data, len);
规则 13.5 避免截断错误
- 反例:
unsigned short total = strlen(a) + strlen(b) + 1;
(可能截断) - 正例:
size_t total = strlen(a) + strlen(b) + 1;
(检查是否溢出)
13.3 格式化输出安全
规则 13.6 确保格式字符和参数匹配
- 反例:
printf("Error: %s", error_code);
(类型不匹配) - 正例:
printf("Error: %d", error_code);
规则 13.7 避免将用户输入作为格式化字符串的一部分或者全部
- 反例:
printf(input);
(用户输入可能含%s
等格式符) - 正例:
printf("%s", input);
13.4 文件 IO 安全
规则 13.8 避免使用strlen()
计算二进制数据的长度
- 说明:
strlen()
遇\0
停止,二进制数据可能含\0
,需用fread
返回值获取长度。
规则 13.9 使用int
类型变量来接受字符 I/O 函数的返回值
- 说明:
fgetc()
等返回int
(含EOF
),char
无法区分0xFF
与EOF
。
13.5 其它
规则 13.10 防止命令注入
- 反例:
system(sprintf("cmd %s", input));
(input
可能含恶意命令) - 正例:使用
execve
代替system
,参数独立传递。
14 单元测试
规则 14.1 在编写代码的同时,或者编写代码前,编写单元测试用例验证软件设计 / 编码的正确
- 说明:测试驱动开发(TDD)可提前发现设计缺陷。
建议 14.1 单元测试关注单元的行为而不是实现,避免针对函数的测试
- 说明:测试模块功能而非内部细节,便于重构。
15 可移植性
规则 15.1 不能定义、重定义或取消定义标准库 / 平台中保留的标识符、宏和函数
- 说明:避免与标准库冲突(如重定义
printf
)。
建议 15.1 不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性
- 反例:直接操作寄存器(如
_AX = 0x4000;
) - 正例:使用标准库函数或硬件抽象层接口。
建议 15.2 除非为了满足特殊需求,避免使用嵌入式汇编
- 说明:汇编依赖架构,影响可移植性,必要时用条件编译:
c
#ifdef __ARM_ARCH__// ARM汇编 #elif __RISCV// RISC-V汇编 #endif
16 业界编程规范
google C++ 编程指南
- 目标:增强代码一致性,限制 C++ 复杂特性以简化代码。
- 特点:解释规则优缺点,举例丰富,可读性强,适合作为 C++ 学习资料。
汽车业 C 语言使用规范(MISRA)
- 目标:针对 C 语言缺陷,规范安全使用,适合安全关键领域(汽车、医疗)。
- 特点:聚焦语言风险点(如未初始化变量、类型转换),无风格类规则。
结语
本规范从嵌入式开发实际需求出发,覆盖代码设计、实现、测试全流程,核心是在资源受限环境下保证代码的清晰性、可靠性与可维护性。开发人员需结合硬件特性灵活应用,通过持续实践形成规范编程习惯,为嵌入式系统的稳定性与可扩展性奠定基础。