Files
STM32F4-Base/docs/代码规范/嵌入式C语言代码规范_V1.0.md
T
wandering1 6505cef9df feat(门禁): coding-standard 加 CODE-RULE-20 禁 C 库堆分配(销 §4.8 空白 ②)
规则:在**掩码后**的文本上匹配 malloc/calloc/realloc/strdup/strndup/
free/aligned_alloc 调用。三条判据都是踩过坑之后定的——
- 大小写敏感:pvPortMalloc / vPortFree(唯一允许的配对)不会误报;
- 词边界 \b:dhara_nand_is_free(、buf_alloc( 这类同形后缀不误报;
- 用掩码:注释与字符串字面量里写 malloc 不报(规范示例就踩过这种)。
覆盖名比 AGENTS 列的五枚多带上 strndup / aligned_alloc——同族同类,
漏一个就等于留后门。

出处不在规范 V1.0,在平台约束:MicroLIB 的 C 堆由 startup_stm32f407xx.s
的 Heap_Size EQU 0x200 只有 512 B,KB 级分配必返 NULL,而模拟器(宿主
CRT 堆)完全正常、**照不出来**——配置引擎 2026-09-18 那次堆分配纠偏与
lftpd 的 malloc 隐患都是**先上线才发现**的那类,靠评审等于没管。

落地前按 门禁说明/coding-standard.md §8 的纪律先全库普查:钩子范围内
C/H 共 151 个文件,命中**只有** simulator/sim/cfg_file_store_sim.c 两行,
即 cfg_store_alloc/cfg_store_free = **平台分配后端的宿主侧实现**(设计依据
写在 docs/子系统设计/配置文件设计说明.md 的"MCU 端 = pvPortMalloc、
模拟器端 = 宿主 CRT")。所以零基线、直接硬拦截;豁免用脚本里的
HEAP_ALLOC_ALLOWLIST(**文件路径 + 函数名**双条件),刻意不做目录级豁免:
下次谁要碰 C 堆就得往这张表加一行,评审看得见。

自证:临时样例文件里 malloc/calloc/free 各报一处,pvPortMalloc/vPortFree/
buf_alloc(、注释、字符串五种陷阱形态全部不误报;
pre-commit run coding-standard --all-files → **Passed**(= 全库零存量)。

顺带修三处口径不一致:
- 规范 V1.0 §6.1 的示例自己写着 malloc/free(通用 C 模板遗留)→ 已在示例
  下加"本仓例外,别照抄"批注,事实源指向 AGENTS.md 平台约束;
- docs/门禁说明/mem-budget.md 里"这条纪律只靠 AGENTS 铁律与评审"已过期 →
  更正为由 CODE-RULE-20 机器拦截;
- AGENTS.md 平台约束条加"已机器拦截 + 唯一豁免"一句;scripts/README.md
  补登 check_test_coverage.py(上一个提交漏登记)。

另:本次一并修好上一处编辑事故——报告 §4.8 曾被我的脚本调用截成半句
(只落第一行、丢掉 malloc 那半句),已重写为"两处空白均已闭合"。

自查:pre-commit run --files(9 个改动文件)适用项全 Passed;
check_doc_links.py --all = 35 处,与主干同数(本分支贡献 0);
check_ids.py --strict 通过(裸编号 P0-1 改平实说法,避免记成新账);
9 个文件 BOM/CR/U+FFFD/行尾与 HEAD 逐项一致。本轮零 .c/.h 改动,未跑
@build、未占 build/bench 锁;分支已 rebase 到 v2-on-ui,覆盖率脚本全量
回归仍 16/16、line=2552/2893 不变。
2026-10-23 23:44:46 +08:00

525 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 1. 文档概述
### 1.1 目的
本文档旨在统一嵌入式C语言代码的编写风格,提高代码的可读性、可维护性、可移植性和可靠性,降低团队协作成本,减少代码缺陷。
### 1.2 适用范围
本规范适用于所有基于C语言的嵌入式软件开发项目,包括但不限于51单片机、STM32、ARM、DSP等平台。
### 1.3 修订记录
| 版本号 | 修订日期 | 修订内容 | 修订人 |
| ---- | ---------- | ------ | --- |
| V1.0 | 2026-05-11 | 创建初始版本 | 王建锋 |
| V1.0.1 | 2026-09-01 | 修复示例与规则矛盾(括号风格、示例类型、变量初始化、硬编码、注释补全);规则定案:保护宏改`XXX_H`、常量在右、头文件常量用`#define`、存储类前缀优先、允许`void`返回值 | 王建锋 |
## 2. 文件结构规范
### 2.1 头文件(.h)结构
```c
#ifndef MODULE_NAME_H
#define MODULE_NAME_H
/*
* 模块名称:模块英文名称
* 模块功能:简要描述模块的主要功能
* 适用平台:列出支持的硬件平台
* 作者:作者姓名
* 创建日期:YYYY-MM-DD
* 修改记录:
* YYYY-MM-DD 修改人 修改内容说明
*/
#ifdef __cplusplus
extern "C" {
#endif
/* 头文件包含区 - 仅包含本模块必需的头文件 */
#include <stdint.h>
/* 宏定义区 */
#define MODULE_NAME_CONSTANT 100
/* 类型定义区 */
typedef enum {
MODULE_NAME_STATUS_OK = 0,
MODULE_NAME_STATUS_ERROR
} module_name_status_t;
/* 函数声明区 */
void module_name_init(void);
#ifdef __cplusplus
}
#endif
#endif /* MODULE_NAME_H */
```
### 2.2 源文件(.c)结构
```c
/*
* 模块名称:模块英文名称
* 模块功能:简要描述模块的主要功能
* 适用平台:列出支持的硬件平台
* 作者:作者姓名
* 创建日期:YYYY-MM-DD
* 修改记录:
* YYYY-MM-DD 修改人 修改内容说明
*/
/* 头文件包含区 - 先包含系统头文件,再包含自定义头文件 */
#include "module_name.h"
/* 私有宏定义区 */
#define MODULE_NAME_PRIVATE_CONSTANT 200
/* 私有类型定义区 */
typedef struct {
uint8_t data;
} module_name_private_t;
/* 全局变量定义区 - 尽量避免使用全局变量 */
uint8_t g_module_name_global_var = 0; /* 全局状态变量(示例) */
/* 静态变量定义区 */
static module_name_private_t s_module_name_private_var; /* 私有状态变量(示例) */
/* 私有函数声明区 */
static void module_name_private_function(void);
/* 函数定义区 - 先写公共函数,再写私有函数 */
void module_name_init(void) {
/* 函数实现 */
}
static void module_name_private_function(void) {
/* 函数实现 */
}
```
### 2.3 头文件包含规则
1. 头文件必须包含头文件保护宏,格式为`MODULE_NAME_H`(不得使用`__`双下划线前缀,其为C标准保留标识符)
2. 头文件中只包含本模块接口必需的其他头文件
3. 源文件中先包含系统头文件,再包含自定义头文件
4. 禁止在头文件中定义变量和函数体
5. 禁止使用相对路径包含头文件
## 3. 命名规范
### 3.1 通用命名原则
1. 所有名称必须使用英文,禁止使用拼音和中文
2. 名称必须准确反映其实际含义,做到"见名知意"
3. 名称长度适中,避免过长或过短
4. 禁止使用单个字母作为变量名(循环变量i、j、k除外)
5. 禁止使用关键字和保留字作为名称
### 3.2 变量命名
1. 采用**小写字母+下划线**命名法
2. 全局变量以`g_`前缀开头
3. 静态变量以`s_`前缀开头
4. 指针变量以`p_`前缀开头
5. 数组变量以`a_`前缀开头
6. 布尔变量以`is_`、`has_`、`can_`等前缀开头
7. 前缀冲突时,存储类前缀(`g_`/`s_`)优先于类型前缀(`p_`/`a_`),如静态数组`s_uart_rx_buffer`
**示例:**
```c
uint8_t g_system_status = 0; /* 全局系统状态变量 */
static uint16_t s_timer_count = 0; /* 静态定时器计数变量 */
uint8_t *p_data_buffer = NULL; /* 数据缓冲区指针 */
uint16_t a_adc_value[10] = { 0 }; /* ADC采样值数组 */
bool is_button_pressed = false; /* 按钮是否按下标志 */
```
### 3.3 函数命名
1. 采用**小写字母+下划线**命名法
2. 公共函数以**模块名**作为前缀
3. 私有函数以**模块名+private**作为前缀
4. 函数名应为"动词+名词"结构,明确表示函数功能
**示例:**
```c
/* 公共函数 */
void uart_init(uint32_t baud_rate);
uint8_t uart_send_byte(uint8_t data);
/* 私有函数 */
static void uart_private_handle_interrupt(void);
```
### 3.4 宏和常量命名
1. 采用**大写字母+下划线**命名法
2. 以**模块名**作为前缀
3. 头文件对外常量使用`#define`(见2.3.4);源文件内私有常量可用`const`,命名遵循3.2(小写+下划线)
**示例:**
```c
/* 头文件对外常量:使用 #define */
#define UART_BAUD_RATE_9600 9600
#define UART_BUFFER_SIZE 128
/* 源文件内私有常量:可用 const,命名遵循 3.2(小写) */
const uint8_t uart_default_data_bits = 8;
```
### 3.5 类型定义命名
1. 采用**小写字母+下划线**命名法
2. 以`_t`作为后缀
3. 枚举类型成员以**模块名**作为前缀
**示例:**
```c
typedef enum {
UART_STATUS_OK = 0,
UART_STATUS_ERROR,
UART_STATUS_TIMEOUT
} uart_status_t;
typedef struct {
uint8_t data_bits;
uint8_t stop_bits;
uint32_t baud_rate;
} uart_config_t;
```
## 4. 格式与排版规范
### 4.1 缩进
1. 使用**4个空格**进行缩进,禁止使用Tab键
2. 所有包含关系的内容必须缩进
3. 同一级别的代码保持相同的缩进级别
### 4.2 空格使用
1. 所有赋值语句、比较语句、算术运算符前后必须加空格
2. 函数参数列表中,逗号后面必须加空格
3. 关键字后面必须加空格
4. 括号内部两侧不加空格
5. 一元运算符前后不加空格
**正确示例:**
```c
int32_t a = 10;
int32_t b = 0;
int32_t i = 0;
uint8_t c[10] = { 0 };
if (a > 5) {
b = a + 3;
}
for (i = 0; i < 10; i++) {
c[i] = 0;
}
```
**错误示例:**
```c
int32_t a=10;
int32_t b=0;
int32_t i=0;
uint8_t c[10]={0};
if(a>5){
b=a+3;
}
for(i=0;i<10;i++){
c[i]=0;
}
```
### 4.3 换行与空行
1. 每行代码长度不超过80个字符
2. 函数之间必须空一行
3. 逻辑上相关的代码块之间可以空一行
4. 函数内变量声明区和代码执行区之间必须空一行
5. 长表达式应在运算符处换行,新行与运算符对齐
**示例:**
```c
int32_t calculate_sum(int32_t a, int32_t b, int32_t c, int32_t d) {
int32_t sum = 0;
sum = a + b
+ c
+ d;
return sum;
}
```
### 4.4 大括号使用
1. **所有包含关系必须加大括号**,即使只有一条语句或为空
2. 左大括号`{`与前面的语句在同一行,前面加一个空格
3. 右大括号`}`单独占一行,与对应的左大括号缩进级别相同
4. 空函数体的大括号也必须单独占一行
**正确示例:**
```c
if (condition) {
do_something();
}
while (1) {
}
```
**错误示例:**
```c
if (condition)
do_something();
while (1) ;
```
## 5. 注释规范
### 5.1 通用注释原则
1. 注释必须清晰、准确、简洁,与代码保持一致
2. 解释性注释使用`/* */`,调试性注释使用`//`
3. 注释应解释"为什么这么做",而不是"做了什么"
4. 代码修改时,必须同步修改相关注释
5. 禁止注释掉的代码提交到版本库
### 5.2 文件头注释
每个文件开头必须包含文件头注释,格式见2.1和2.2节。
### 5.3 函数注释
所有函数(包括私有函数)必须包含完整的函数注释,格式如下:
```c
/*
* 函数功能:详细描述函数的功能
* 入口参数:param1 - 参数1说明 类型 取值范围
* param2 - 参数2说明 类型 取值范围
* 出口参数:param3 - 参数3说明 类型 取值范围
* 返回值:返回值说明 类型 取值范围
* 限定条件:函数使用的前提条件和限制
* 函数说明:1. 函数的详细说明
* 2. 注意事项
* 3. 其他需要说明的内容
*/
```
**示例:**
```c
#define DELAY_INNER_LOOP 123 /* 延时内层循环次数,按12MHz时钟整定 */
/*
* 函数功能:毫秒级软件延时函数
* 入口参数:ms - 需要延时的毫秒数 uint32_t 0 - 65535
* 出口参数:无
* 返回值:无(void)
* 限定条件:0 <= ms <= 65535
* 函数说明:1. 采用空指令循环方式实现延时,会阻塞CPU运行
* 2. 延时精度依赖系统时钟,默认适配12MHz时钟
* 3. 系统时钟改变时,需重新调整内层循环次数(DELAY_INNER_LOOP)
* 4. 当ms为0时,函数立即返回
*/
void delay_ms(uint32_t ms) {
uint32_t i;
uint32_t j;
for (i = 0; i < ms; i++) {
for (j = 0; j < DELAY_INNER_LOOP; j++) {
}
}
}
```
### 5.4 变量注释
1. 全局变量和静态变量必须添加注释
2. 重要的局部变量应添加注释
3. 注释可以写在变量定义的同一行或上一行
**示例:**
```c
/* 系统运行时间,单位:毫秒 */
uint32_t g_system_time = 0;
static uint8_t s_uart_rx_buffer[UART_BUFFER_SIZE]; /* UART接收缓冲区 */
```
### 5.5 代码行注释
1. **关键逻辑代码每一行都要添加注释**
2. 复杂的算法和逻辑必须添加详细注释
3. 注释应单独占一行(变量定义的行内注释除外,见5.4.3),与被注释代码缩进级别相同
**示例:**
```c
#define CRC16_INITIAL 0xFFFF /* CRC-16初值 */
#define CRC16_POLY_REFLECTED 0xA001 /* CRC-16/MCC反射多项式 */
#define BITS_PER_BYTE 8 /* 每字节的位数 */
/* 计算CRC校验值 */
uint16_t crc_calculate(uint8_t *data, uint16_t length) {
uint16_t crc = CRC16_INITIAL;
uint16_t i;
uint16_t j;
/* 遍历所有数据字节 */
for (i = 0; i < length; i++) {
/* 将当前字节与CRC寄存器低8位异或 */
crc ^= data[i];
/* 对每个位进行处理 */
for (j = 0; j < BITS_PER_BYTE; j++) {
/* 检查最低位是否为1 */
if (crc & 0x0001) {
/* 最低位为1,右移并与多项式异或 */
crc = (crc >> 1) ^ CRC16_POLY_REFLECTED;
} else {
/* 最低位为0,直接右移 */
crc = crc >> 1;
}
}
}
/* 返回计算得到的CRC值 */
return crc;
}
```
## 6. 编程实践规范
### 6.1 变量声明与初始化
1. 变量应在使用前声明,并尽可能在靠近使用的地方声明
2. 所有变量必须初始化,禁止使用未初始化的变量
3. 尽量使用局部变量,避免使用全局变量
4. 指针变量必须初始化为`NULL`
5. 使用标准数据类型(`uint8_t`、`int32_t`等),避免使用`char`、`int`等不确定长度的类型
**示例:**
```c
#define DATA_BUF_SIZE 100 /* 数据缓冲区大小(字节) */
void function(void) {
uint8_t status = 0;
uint16_t count = 0;
uint8_t *p_data = NULL;
p_data = (uint8_t *)malloc(DATA_BUF_SIZE);
if (p_data == NULL) {
return;
}
/* 使用p_data */
free(p_data);
p_data = NULL;
}
```
> ⚠ **本仓例外(2026-10-23 补,别照抄这段示例)**:示例里的 `malloc`/`free` 是**通用 C 写法**,本仓
> **禁止**——工具链是 MicroLIB,其 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 只有 **512 B**,
> KB 级分配必然返回 NULL,而模拟器(宿主 CRT 堆)完全正常、**照不出来**(只有真机才炸)。这段示例只教
> 三件事:**声明即初始化、检查 NULL、释放后置 NULL**。实际分配一律 `pvPortMalloc`/`vPortFree`,或把
> 分配**收进平台后端**(MCU 端与宿主端各一份实现,例:`simulator/sim/cfg_file_store_sim.c` 的
> `cfg_store_alloc`)。机器拦截 = 提交门禁 `coding-standard` 的 **CODE-RULE-20**,细则见
> `docs/门禁说明/coding-standard.md`。
### 6.2 函数设计原则
1. 函数应遵循"单一职责原则",一个函数只做一件事
2. 函数长度不宜过长,一般不超过50行
3. 函数参数不宜过多,一般不超过5个
4. 有失败路径的函数必须有明确的返回值,用于表示执行状态;无失败路径的纯输出函数可返回`void`
5. 避免使用函数参数作为返回值
6. 私有函数必须声明为`static`
### 6.3 控制结构
1. `if`语句中,常量应写在比较运算符的右边
2. `switch`语句必须包含`default`分支
3. 避免使用`goto`语句,除非用于错误处理
4. 循环嵌套不宜超过3层
**示例:**
```c
if (status == 0) {
do_something();
}
switch (command) {
case COMMAND_START:
start_process();
break;
case COMMAND_STOP:
stop_process();
break;
default:
handle_unknown_command();
break;
}
```
### 6.4 错误处理
1. 所有可能失败的函数都必须检查返回值
2. 对输入参数进行合法性检查
3. 对指针进行非空检查
4. 数组访问时检查下标是否越界
5. 使用断言`assert`检查开发阶段的错误
**示例:**
```c
uint8_t uart_send_data(uint8_t *data, uint16_t length) {
/* 检查输入参数合法性 */
if (data == NULL) {
return UART_STATUS_ERROR;
}
if (length == 0 || length > UART_BUFFER_SIZE) {
return UART_STATUS_ERROR;
}
/* 发送数据 */
return UART_STATUS_OK;
}
```
## 7. 可移植性与安全规范
1. 避免使用编译器特有的扩展功能
2. 避免使用硬编码的数值,使用宏定义代替
3. 注意字节序问题,多字节数据传输时进行字节序转换
4. 注意数据类型的长度和符号问题
5. 避免使用不安全的函数,如`strcpy`、`sprintf`等,使用`strncpy`、`snprintf`代替
6. 禁止使用可变参数函数
7. 禁止使用递归函数
## 8. 版本控制规范
1. 每次提交必须填写清晰、准确的提交信息
2. 提交信息格式:`[模块名] 修改内容说明`
3. 每次提交只包含一个逻辑修改
4. 提交前必须进行代码编译和测试
5. 禁止提交编译错误的代码
6. 禁止提交调试信息和注释掉的代码
## 9. 附录
### 9.1 完整示例代码
```c
#ifndef LED_H
#define LED_H
/*
* 模块名称:LED控制模块
* 模块功能:提供LED初始化、点亮、熄灭和翻转功能
* 适用平台:STM32F103系列单片机
* 作者:张三
* 创建日期:2026-05-11
* 修改记录:
* 2026-05-11 张三 创建初始版本
*/
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/* LED编号定义 */
#define LED_NUM_1 0
#define LED_NUM_2 1
#define LED_NUM_MAX 2
/* LED状态定义 */
#define LED_OFF 0
#define LED_ON 1
/*
* 函数功能:LED初始化函数
* 入口参数:led_num - LED编号 uint8_t 0 - LED_NUM_MAX-1
* 返回值:0 - 成功,其他 - 失败
* 限定条件:无
* 函数说明:初始化LED对应的GPIO引脚为推挽输出模式
*/
uint8_t led_init(uint8_t led_num);
/*
* 函数功能:点亮LED
* 入口参数:led_num - LED编号 uint8_t 0 - LED_NUM_MAX-1
* 返回值:0 - 成功,其他 - 失败
* 限定条件:led_init()函数已成功调用
* 函数说明:将LED对应的GPIO引脚置为低电平
*/
uint8_t led_on(uint8_t led_num);
/*
* 函数功能:熄灭LED
* 入口参数:led_num - LED编号 uint8_t 0 - LED_NUM_MAX-1
* 返回值:0 - 成功,其他 - 失败
* 限定条件:led_init()函数已成功调用
* 函数说明:将LED对应的GPIO引脚置为高电平
*/
uint8_t led_off(uint8_t led_num);
/*
* 函数功能:翻转LED状态
* 入口参数:led_num - LED编号 uint8_t 0 - LED_NUM_MAX-1
* 返回值:0 - 成功,其他 - 失败
* 限定条件:led_init()函数已成功调用
* 函数说明:将LED对应的GPIO引脚电平取反
*/
uint8_t led_toggle(uint8_t led_num);
#ifdef __cplusplus
}
#endif
#endif /* LED_H */
```