3. 算法移植前的代码规范与工程结构
各位工程师,大家好。今天我们来聊聊算法移植前最重要的一步——把代码写得规规矩矩,把工程搭得清清楚楚。说实话,我见过太多移植失败的案例,最后查下来,问题往往不是算法本身,而是代码乱成一锅粥,连自己都看不懂了。
这一章,我会从三个角度展开:C语言编码规范(MISRA-C)、模块化分层设计,以及版本控制(Git)入门。这三样东西,说白了就是让你写的代码能活下来、能跑起来、能改得动。
3.1 为什么非要讲MISRA-C?
你可能觉得,写个点钞机算法,又不是造火箭,搞那么严格干嘛?嗯,我以前也这么想。直到有一次,我在一个项目中因为一个隐式类型转换,导致钞票识别率在特定光照下直接掉到60%。查了三天,最后发现是一个int和unsigned int混用,编译器悄悄做了截断。
MISRA-C,全称是“汽车工业软件可靠性协会C语言规范”。虽然它起源于汽车行业,但金融设备对安全性和可靠性的要求,一点都不比汽车低。你想想看,点钞机如果数错钱,那可不是闹着玩的。
核心原则:MISRA-C不是限制你的创造力,而是帮你避开C语言里那些容易踩坑的“暗礁”。
3.1.1 我常用的几条MISRA-C规则
规则很多,我不可能全背下来。但以下几条,是我在嵌入式项目里几乎每行代码都会用到的:
| 规则编号 | 规则内容 | 我的理解 |
|---|---|---|
| Rule 10.1 | 不允许隐式类型转换 | 所有类型转换必须显式写出,别指望编译器帮你猜 |
| Rule 14.3 | 控制表达式必须是布尔类型 | if(x) 这种写法要改成 if(x != 0),更清晰 |
| Rule 15.5 | 每个switch必须有default分支 | 哪怕你觉得所有情况都覆盖了,也要写default做兜底 |
| Rule 17.4 | 数组索引必须在有效范围内 | 别相信“肯定不会越界”,加个断言保平安 |
我的小技巧:在项目开始前,我会用工具(比如PC-Lint或Coverity)把MISRA-C检查集成到编译流程里。每次编译都自动跑一遍,省心省力。
3.1.2 一个反面教材
我曾经接手过一个遗留代码,里面有一段这样的写法:
int count = 0;
unsigned int total = 100;
if (count - total > 0) {
// 处理逻辑
}
看出来问题了吗?count - total的结果是unsigned int,永远大于等于0。这个if条件永远不会成立。这就是典型的隐式类型转换陷阱。按照MISRA-C,你应该写成:
int count = 0;
unsigned int total = 100;
if ((int)(count - total) > 0) {
// 处理逻辑
}
或者更干脆,把变量类型统一。
3.2 模块化分层设计:HAL、Driver、Algorithm、App
好了,代码规范讲完了。接下来聊聊工程结构。我个人习惯把点钞机的软件分成四层。为什么是四层?因为我在一个项目里试过三层,发现算法层和硬件层耦合太紧,换个传感器就得重写一半代码。后来改成四层,清爽多了。
3.2.1 四层架构总览
| 层级 | 全称 | 职责 | 典型文件 |
|---|---|---|---|
| App层 | Application Layer | 业务逻辑、用户交互、状态机 | app_main.c, app_display.c |
| Algorithm层 | Algorithm Layer | 货币识别算法、特征提取、分类器 | alg_currency.c, alg_feature.c |
| Driver层 | Driver Layer | 外设驱动、传感器读写、电机控制 | drv_motor.c, drv_sensor.c |
| HAL层 | Hardware Abstraction Layer | MCU寄存器操作、中断、时钟配置 | hal_gpio.c, hal_timer.c |
你想想看,每一层只跟相邻层打交道。App层调用Algorithm层,Algorithm层调用Driver层,Driver层调用HAL层。反过来不行。这样设计的好处是,哪天你想换一个MCU,只需要重写HAL层和部分Driver层,算法和业务逻辑基本不用动。
注意:千万不要在Algorithm层里直接操作寄存器。我见过有人把GPIO读写直接写在算法里,结果换了个芯片,算法全废了。这种耦合,说白了就是给自己挖坑。
3.2.2 一个实际的分层例子
假设我们要实现“检测钞票厚度”这个功能。在四层架构里,调用流程是这样的:
// App层:发起检测
void app_check_currency(void) {
float thickness = alg_get_thickness();
if (thickness > THICKNESS_THRESHOLD) {
// 认为是两张重叠
}
}
// Algorithm层:处理传感器数据
float alg_get_thickness(void) {
uint16_t raw = drv_sensor_read(THICKNESS_SENSOR);
return alg_calibrate(raw); // 校准算法
}
// Driver层:读取传感器
uint16_t drv_sensor_read(uint8_t sensor_id) {
// 调用HAL层的I2C接口
return hal_i2c_read(sensor_id, REG_THICKNESS);
}
// HAL层:操作硬件寄存器
uint16_t hal_i2c_read(uint8_t dev_addr, uint8_t reg) {
// 直接操作I2C外设寄存器
I2C1->DR = dev_addr;
// ...
}
看到了吗?每一层只做自己的事。App层不知道传感器是怎么读的,Algorithm层不知道I2C协议是什么。这就是模块化的精髓。
3.3 版本控制(Git)入门
最后,聊聊版本控制。说实话,我刚开始做嵌入式的时候,觉得Git是给写网页的人用的。我们搞硬件的,代码量又不大,用U盘拷一下不就行了?直到有一次,我改了一个文件,结果把整个工程搞崩了,想回退却找不到之前的版本。从那以后,我再也不敢不用Git了。
3.3.1 嵌入式项目Git的基本配置
对于点钞机这种项目,我建议的.gitignore文件至少包含以下内容:
# 编译产物
*.o
*.hex
*.bin
*.elf
# IDE生成文件
*.uvguix.*
*.uvoptx
.Debug/
.Release/
# 临时文件
*.bak
*.swp
为什么要忽略这些?因为编译产物和IDE配置文件,每个人生成的都不一样。你提交到仓库里,只会造成无意义的冲突。
3.3.2 我常用的Git工作流
对于小团队(3-5人),我推荐用功能分支工作流。说白了就是:
- main分支:永远保持可发布状态。每次提交都要经过代码审查。
- develop分支:日常开发的主线。所有功能分支从这里拉出去,最后合并回来。
- feature/xxx分支:每个新功能一个分支。比如
feature/currency_euro。 - fix/xxx分支:修bug用的。比如
fix/sensor_timeout。
我的习惯:每次提交前,我都会用git diff仔细看一遍改动。不是为了检查语法,而是确认自己有没有不小心改错东西。这个习惯救过我很多次。
3.3.3 一个常见的坑
我曾经犯过一个错误:在main分支上直接修改代码,然后编译测试。结果发现改错了,想回退,但中间又提交了几次其他修改,搞得一团糟。正确的做法是:
# 从develop拉一个新分支
git checkout develop
git pull
git checkout -b feature/fix_threshold
# 在这个分支上修改、测试
# ... 修改代码 ...
git add .
git commit -m "fix: 调整厚度阈值"
# 测试通过后,合并回develop
git checkout develop
git merge feature/fix_threshold
这样,即使改错了,也不会影响主分支。删掉这个功能分支就行了,干净利落。
3.4 本章小结
好了,这一章的内容就到这里。我们来回顾一下:
- MISRA-C规范:不是束缚,是保护。重点记住隐式类型转换、布尔表达式、switch-default、数组边界这四条。
- 四层架构:HAL、Driver、Algorithm、App。每层只跟相邻层通信,别越级。
- Git入门:功能分支工作流,main永远保持可发布。.gitignore要配好。
下一章,我们会正式开始讲算法移植的具体流程。到时候,你会看到这些规范是怎么在实际代码中落地的。嗯,做好准备,干货要来了。