第三章 工程目录结构设计:模块化思想、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 的实战技巧,包括怎么处理交叉编译、怎么管理依赖库。嗯,那些可都是实打实的干货。