4. MISRA规则解读基础
编码风格这东西,说实话,很多团队一开始都不太当回事。我早年带过一个项目,代码写得飞起,结果到了集成测试阶段,几个人对着同一个文件改,光对齐缩进就花了两天。嗯,从那以后我就明白了——风格不是审美问题,是工程问题。
MISRA-C 对编码风格没有强制规定,但它要求代码必须「可审查」。什么叫可审查?就是别人拿到你的代码,不用猜,不用问,一眼就能看懂逻辑。下面我把我这些年积累的经验拆开来讲。
4.1 命名约定:让变量自己说话
我见过最离谱的命名是什么?int a1, a2, a3; 三个全局变量,分别控制三个电机。你想想看,三个月后你自己回来看,还记得 a1 是哪个电机吗?
我个人习惯用这套规则,在多个项目里验证过,效果不错:
| 元素 | 命名规则 | 示例 | 说明 |
|---|---|---|---|
| 全局变量 | g_ + 模块名 + 含义 | g_motor_speed |
一眼看出作用域和归属 |
| 静态全局 | s_ + 模块名 + 含义 | s_adc_buffer |
文件内可见,外部不可访问 |
| 局部变量 | 小驼峰或下划线 | motorSpeed 或 motor_speed |
团队统一即可 |
| 宏定义 | 全大写 + 下划线 | BUFFER_SIZE_MAX |
与变量区分,防止误用 |
| 函数名 | 模块名_动作_对象 | motor_set_speed() |
动宾结构,语义清晰 |
| 类型定义 | 模块名 + _t | motor_config_t |
符合 MISRA 类型规范 |
i、j、k 做嵌套循环的计数器,三层循环下来,代码审查时谁也分不清哪个 i 对应哪层。后来我们强制规定:嵌套超过两层时,计数器必须带含义,比如 ch_idx、byte_idx。
命名还有一个容易被忽略的点——不要用否定逻辑。比如 flag_not_ready,你写 if (!flag_not_ready) 的时候,脑子要转两个弯。改成 flag_ready,直接 if (flag_ready),多清爽。
4.2 缩进与排版:视觉上的「呼吸感」
缩进这件事,说白了就是让代码结构在视觉上「立起来」。我见过有人用 Tab,有人用空格,还有人混着用——在某个编辑器里看着对齐,换一个就全乱了。
我的建议很简单:统一用 4 个空格。为什么不是 2 个?2 个缩进在深层嵌套时层次感不够。为什么不是 8 个?8 个缩进会让代码很快跑到屏幕右边去。4 个是折中,也是工业界的主流选择。
来看一个对比:
/* 不推荐的排版:缩进混乱,括号位置随意 */
void motor_init(void)
{
int speed = 0;
if (speed > 0)
{
motor_run();
}
else
{
motor_stop();
}
}
/* 推荐的排版:缩进统一,括号对齐 */
void motor_init(void)
{
int speed = 0;
if (speed > 0)
{
motor_run();
}
else
{
motor_stop();
}
}
你想想看,第二种是不是一眼就能看出 if-else 的结构?这就是「呼吸感」——代码块之间有明确的视觉边界,审查时不容易漏掉逻辑分支。
4.3 注释规范:写给人看的,不是写给编译器看的
注释这件事,我踩过不少坑。早期我总觉得注释越多越好,结果代码改了,注释没改,反而误导人。后来我总结了一条原则:注释应该解释「为什么」,而不是「是什么」。
来看一个反面教材:
/* 将 speed 加 1 */
speed = speed + 1;
这种注释就是废话。代码本身已经说清楚了「是什么」。真正需要注释的是这种:
/*
* 这里加 1 是因为电机在启动瞬间需要克服静摩擦力,
* 实测发现不加这个补偿,启动成功率只有 73%。
* 参考测试报告 v2.3 第 4 节。
*/
speed = speed + 1;
看到了吗?注释告诉后来人:为什么这么做。这才是注释的价值。
MISRA-C 对注释还有一条硬性要求:禁止使用 // 风格的注释(C90 标准不支持)。虽然 C99 之后支持了,但很多嵌入式编译器仍然只支持 C90。所以统一用 /* */ 是最安全的。
我个人的注释规范如下:
- 文件头注释:版权、作者、创建日期、文件功能简述
- 函数头注释:功能描述、参数说明、返回值、注意事项
- 关键逻辑注释:解释「为什么」这么做,引用测试数据或标准条款
- TODO 注释:统一格式
/* TODO: 原因 - 责任人 - 日期 */
核心原则:好的代码本身应该是自解释的。如果一段代码需要大段注释才能看懂,说明代码本身写得不够好。先优化代码,再补充注释。
4.4 文件组织结构:让项目像书架一样整齐
一个嵌入式项目,少则几十个文件,多则几百个。如果没有清晰的文件组织结构,找文件就像在垃圾堆里翻东西。
我习惯用这种结构:
project_root/
├── app/ # 应用层代码
│ ├── motor/
│ │ ├── motor.c
│ │ └── motor.h
│ └── sensor/
│ ├── sensor.c
│ └── sensor.h
├── bsp/ # 板级支持包
│ ├── bsp_uart.c
│ ├── bsp_uart.h
│ ├── bsp_gpio.c
│ └── bsp_gpio.h
├── lib/ # 通用库
│ ├── ring_buffer.c
│ ├── ring_buffer.h
│ ├── crc.c
│ └── crc.h
├── config/ # 配置文件
│ ├── project_config.h
│ └── pin_mapping.h
├── test/ # 测试代码
│ ├── test_motor.c
│ └── test_sensor.c
└── doc/ # 文档
└── architecture.md
这个结构有几个好处:
- 按功能模块分目录:每个模块一个文件夹,内聚性强
- 头文件与源文件同目录:查找方便,不用来回跳路径
- 配置单独拎出来:换平台时只改 config 目录,不动业务代码
- 测试代码独立:不污染生产代码,也方便自动化测试
这里有一个容易犯的错——头文件的包含路径。我见过有人把所有的 .h 文件扔到一个 inc/ 目录下,结果不同模块的同名头文件冲突了。后来我们规定:包含头文件时用相对路径,比如 #include "motor/motor.h",这样模块归属一目了然。
extern 声明在头文件里,定义放在对应的 .c 文件中。
4.5 知识结构总览
下面这张图把本章的核心知识点串起来了,你可以对照着检查自己的项目:
这张图把四个核心维度串在了一起。你可以用它来对照检查自己的项目——看看命名有没有统一前缀,缩进是不是混用了 Tab 和空格,注释是不是在解释「为什么」,文件目录是不是按功能分的。
编码风格这件事,前期花点时间定好规矩,后面能省下大把的沟通成本和调试时间。别等到代码写了几万行再回头改,那时候改起来就真的想哭了。