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人),我推荐用功能分支工作流。说白了就是:

  1. main分支:永远保持可发布状态。每次提交都要经过代码审查。
  2. develop分支:日常开发的主线。所有功能分支从这里拉出去,最后合并回来。
  3. feature/xxx分支:每个新功能一个分支。比如feature/currency_euro
  4. 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要配好。

下一章,我们会正式开始讲算法移植的具体流程。到时候,你会看到这些规范是怎么在实际代码中落地的。嗯,做好准备,干货要来了。