第三章 工程目录结构设计:模块化思想、src/include/test/build目录划分、命名规范与版本管理(Git)

好,咱们进入第三章。这一章我打算聊聊工程目录结构。

你可能觉得,目录结构嘛,不就是建几个文件夹?说实话,我早年也这么想。直到有一次,一个项目做到一半,代码堆得跟垃圾场似的——头文件乱放,测试代码和源码混在一起,连我自己都找不到想要的东西。嗯,那次之后,我彻底改了。

一个清晰的目录结构,说白了就是给项目画一张地图。你想想看,一个新同事加入,打开工程一眼就能看懂该往哪放代码、去哪找测试、编译脚本在哪——这能省多少沟通成本?

3.1 模块化思想:别把所有代码塞进一个文件夹

嵌入式视觉项目,说白了就是一堆功能模块拼起来的。摄像头驱动、图像处理算法、通信协议、UI显示……每个模块都应该有自己的小天地。

我个人习惯把模块化拆成三层:

  • 硬件抽象层(HAL):封装底层硬件操作,比如I2C读写、GPIO控制。换芯片时只改这一层。
  • 中间件层:图像处理库、算法模块、通信协议栈。这部分尽量与硬件解耦。
  • 应用层:业务逻辑、UI界面、任务调度。说白了就是产品最终表现的样子。

核心原则:每个模块只做一件事,且做好一件事。模块之间通过接口通信,不要互相引用内部变量。

我在项目中遇到过最典型的反面教材:一个工程师把摄像头初始化、图像缩放、人脸检测、串口打印全写在一个.c文件里,足足两千行。后来要单独测试人脸检测算法,得把整个文件搬过去——你说这多痛苦?

3.2 目录划分:src/include/test/build 四兄弟

一个成熟的嵌入式视觉工程,我建议至少分这四个目录。咱们一个一个说。

3.2.1 src/ —— 源码的根据地

所有.c文件都放这里。按模块再建子目录:

src/
├── hal/          # 硬件抽象层
│   ├── camera.c
│   ├── display.c
│   └── sensor.c
├── algorithm/    # 算法模块
│   ├── face_detect.c
│   ├── color_convert.c
│   └── filter.c
├── comm/         # 通信模块
│   ├── uart.c
│   ├── spi.c
│   └── protocol.c
├── ui/           # 用户界面
│   ├── menu.c
│   └── widget.c
└── main.c        # 主入口,尽量精简

注意,main.c 我习惯只放初始化流程和主循环。业务逻辑别往里塞,否则它很快就会变成一个“垃圾堆”。

3.2.2 include/ —— 头文件的集中营

头文件单独放,目录结构与src保持一致。这样做的好处是:别人看接口时,不用在源码里翻来翻去。

include/
├── hal/
│   ├── camera.h
│   ├── display.h
│   └── sensor.h
├── algorithm/
│   ├── face_detect.h
│   ├── color_convert.h
│   └── filter.h
├── comm/
│   ├── uart.h
│   ├── spi.h
│   └── protocol.h
└── ui/
    ├── menu.h
    └── widget.h

一个小技巧:每个头文件都加上 #ifndef 保护宏。我曾经因为忘记加这个,导致编译时重复定义,排查了整整两个小时。嗯,从那以后我养成了条件反射。

3.2.3 test/ —— 测试代码的专属空间

很多嵌入式工程师不写单元测试,觉得浪费时间。我个人觉得,至少核心算法模块要有测试。目录结构同样与src对齐:

test/
├── hal/
│   └── test_camera.c
├── algorithm/
│   ├── test_face_detect.c
│   └── test_color_convert.c
└── comm/
    └── test_uart.c

为什么要单独放?因为测试代码通常需要额外的mock和stub,混在src里会让正式代码变得臃肿。而且,发布时直接把test目录删掉就行,省心。

3.2.4 build/ —— 编译产物的垃圾桶

所有编译生成的 .o 文件、可执行文件、map文件、hex文件,全扔这里。我习惯在build里再建两个子目录:

  • debug/:调试版本,带符号表,方便GDB调试。
  • release/:发布版本,优化全开,体积最小。

注意:build目录不要提交到Git仓库!在.gitignore里加上 build/。否则每次编译都会产生大量变更,你的Git log会变得惨不忍睹。

3.3 命名规范:让代码自己会说话

命名这件事,看似简单,其实坑很多。我总结了几条铁律:

类型 规范 示例 说明
文件名 小写+下划线 face_detect.c 不要用大写或空格
函数名 模块名_动作_对象 camera_init() 一看就知道属于哪个模块
全局变量 g_模块_名称 g_camera_fps 加g_前缀,一眼识别
局部变量 小写+下划线 frame_count 简短清晰即可
宏定义 全大写+下划线 #define MAX_FRAME_SIZE 1024 与变量区分开
枚举/结构体 模块名_类型名_t camera_status_t 加_t后缀,表示类型

为什么要这么麻烦?你想想看,一个项目做半年后,你打开一个文件,看到 g_camera_fps 就知道这是个全局变量、属于摄像头模块、存的是帧率。根本不需要翻注释。这就是“自文档化”。

避坑指南:我曾经接手过一个项目,变量名全是 a、b、c、tmp1、tmp2…… 我花了三天才理清楚每个变量是干嘛的。从那以后,我坚持命名必须“望文生义”。

3.4 版本管理(Git):给项目上保险

Git 这东西,用好了是神器,用不好是灾难。我见过太多人只会 git add .git commit -m "update"。说实话,这样跟没用版本管理差不多。

3.4.1 .gitignore 要提前配好

项目一开始就要创建 .gitignore 文件。至少包含这些:

# 编译产物
build/
*.o
*.elf
*.hex
*.map

# IDE 配置
.idea/
.vscode/
*.swp

# 临时文件
*.log
*.tmp
*.bak

# 系统文件
.DS_Store
Thumbs.db

我见过有人把 .o 文件提交到仓库,结果每次编译都产生几百个文件变更,Pull Request 根本没法看。嗯,这种坑踩过一次就够了。

3.4.2 分支策略:别在 master 上直接改

我个人习惯用这种分支模型:

  • master:只放稳定版本,每次发布打一个tag。
  • develop:日常开发的主分支,所有功能分支从这里拉。
  • feature/xxx:开发新功能时用,完成后合并回develop。
  • fix/xxx:修bug专用,修完合并回develop和master。

举个例子:你要加一个“人脸识别”功能,就建一个 feature/face_recognition 分支。开发完、测试通过,再合并到 develop。这样 master 永远是干净的。

3.4.3 Commit 信息怎么写

别写“update”、“fix bug”这种废话。我建议用这种格式:

[模块] 动作: 简短描述

详细说明(可选)

比如:

[camera] fix: 修复OV2640初始化时I2C超时问题

原因是上电后传感器需要至少10ms稳定时间,
在初始化前增加延时即可解决。

为什么要这么写?因为半年后你回看git log,一眼就知道每个提交改了啥、为什么改。你想想看,如果全是“update”,你不得一个一个翻代码?

一个小习惯:每次提交前,先 git diff 看看改了哪些内容。我经常发现一些不该提交的调试代码,比如 printf("here\n");。嗯,这个习惯帮我避免了好几次尴尬。

3.5 一个完整的目录结构示例

好了,说了这么多,咱们看一个实际项目的目录长什么样:

project_name/
├── src/
│   ├── hal/
│   │   ├── camera.c
│   │   └── display.c
│   ├── algorithm/
│   │   ├── face_detect.c
│   │   └── color_convert.c
│   ├── comm/
│   │   └── uart.c
│   ├── ui/
│   │   └── menu.c
│   └── main.c
├── include/
│   ├── hal/
│   │   ├── camera.h
│   │   └── display.h
│   ├── algorithm/
│   │   ├── face_detect.h
│   │   └── color_convert.h
│   ├── comm/
│   │   └── uart.h
│   └── ui/
│       └── menu.h
├── test/
│   ├── hal/
│   │   └── test_camera.c
│   └── algorithm/
│       └── test_face_detect.c
├── build/
│   ├── debug/
│   └── release/
├── docs/              # 文档目录
│   ├── architecture.md
│   └── api_reference.md
├── scripts/           # 辅助脚本
│   ├── flash.sh
│   └── monitor.py
├── .gitignore
├── Makefile           # 或 CMakeLists.txt
└── README.md

这个结构我用了好几年,团队协作时几乎不需要额外解释。新成员看一眼就知道该往哪放代码、去哪找测试、编译脚本在哪。

总结一下:目录结构不是花架子,它是项目健康度的晴雨表。结构清晰的项目,维护成本低、协作效率高、bug也少。反之,结构混乱的项目,迟早会变成一团乱麻。

下一章咱们聊聊编译系统的搭建。到时候我会分享一些 Makefile 和 CMake 的实战技巧,包括怎么处理交叉编译、怎么管理依赖库。嗯,那些可都是实打实的干货。