项目目录结构:标准划分原则
说到项目目录结构,我见过太多新手一上来就把所有文件堆在同一个文件夹里。嗯,刚开始写单片机程序时我也这么干过——一个 main.c 搞定所有,头文件、源文件、文档全混在一起。结果项目一复杂,找文件比找钥匙还费劲。
今天咱们聊聊标准目录怎么划分。说白了,就是给代码安个家,每个文件都有它该待的地方。
为什么需要标准目录结构?
你想想看,一个嵌入式项目少说几十个文件,多则上千。没有统一规划,光是找某个驱动文件就能耗掉半小时。我在项目中遇到过最夸张的情况:同事把 BSP 驱动和上层应用代码全塞在 src 里,结果编译时链接错误满天飞。
标准目录结构解决三个核心问题:
- 可读性——新人接手项目,扫一眼目录就知道代码怎么组织的
- 可维护性——修改某个模块,不会误伤其他模块
- 可复用性——库文件、公共头文件能轻松移植到其他项目
核心目录划分原则
我个人习惯把项目目录分成七个核心区域。每个区域各司其职,互不干扰。
| 目录名 | 存放内容 | 核心原则 |
|---|---|---|
| src | 源文件 (.c) | 按模块划分子目录 |
| include | 头文件 (.h) | 公共接口放这里,内部头文件放 src 子目录 |
| lib | 第三方库、静态/动态库 | 只读,不修改 |
| test | 单元测试、集成测试代码 | 与 src 结构镜像 |
| doc | 设计文档、API 文档、数据手册 | 按版本归档 |
| build | 编译产物 (.o, .elf, .bin) | 不提交到版本控制 |
| tools | 编译脚本、烧录工具、辅助脚本 | 平台无关 |
核心原则:每个目录只做一件事。src 只放源码,include 只放公开头文件,build 只放编译产物。千万别把 .o 文件和 .c 文件混在一起,那会让 git 仓库变得臃肿不堪。
src 目录:源码的核心
src 是项目的灵魂。我一般按功能模块划分子目录:
src/
├── app/ # 应用层逻辑
│ ├── main.c
│ └── task_manager.c
├── bsp/ # 板级支持包
│ ├── uart.c
│ ├── gpio.c
│ └── spi.c
├── driver/ # 外设驱动
│ ├── sensor.c
│ └── display.c
├── os/ # 操作系统相关
│ └── freertos_hook.c
└── utils/ # 工具函数
├── ringbuf.c
└── crc.c
这里有个小技巧:每个子目录内部可以再放一个 internal 文件夹,存放模块内部使用的头文件。外部模块只需要引用 include 目录下的公开接口。
我的经验:src 目录不要超过三层嵌套。超过三层,编译脚本会变得复杂,而且容易引发循环依赖。我曾经在一个五层嵌套的项目里调试了整整两天,最后发现是头文件路径写错了。
include 目录:接口的契约
include 目录放的是公开头文件。说白了,就是模块对外承诺的接口。内部实现细节不要暴露出去。
include/
├── app.h
├── bsp.h
├── driver.h
└── utils.h
为什么要单独拎出来?因为头文件是模块之间的契约。你改了头文件,所有引用它的模块都得重新编译。所以头文件要尽量稳定,只放必要的声明。
注意:不要在 include 里放 #include "..\src\bsp\gpio.h" 这种相对路径。所有头文件引用应该基于 include 目录的根路径。否则换个编译器就编译不过。
lib 目录:拿来主义
lib 目录放第三方库。我习惯分两类:
- 源码库——直接放源码,编译时一起编译
- 预编译库——放 .a 或 .lib 文件,只链接不编译
预编译库要附带对应的头文件,放在 lib 的子目录里。比如:
lib/
├── freertos/ # FreeRTOS 源码
│ ├── include/
│ └── src/
├── lwip/ # lwIP 协议栈
│ ├── include/
│ └── src/
└── closed_source/ # 供应商提供的预编译库
├── libdriver.a
└── driver_api.h
这里有个坑:预编译库的编译器版本必须和项目一致。我曾经因为 GCC 版本不同,链接时符号找不到,折腾了一下午才发现是 ABI 不兼容。
test 目录:质量的门神
test 目录的结构最好和 src 镜像。这样找测试文件时不用动脑子。
test/
├── app/
│ └── test_task_manager.c
├── bsp/
│ ├── test_uart.c
│ └── test_gpio.c
└── utils/
└── test_ringbuf.c
我建议每个 .c 文件都配一个对应的测试文件。别嫌麻烦——有一次我改了一个 ringbuf 的边界条件,自认为没问题,结果测试用例直接报错,救了我一命。
doc 与 build:辅助但不次要
doc 目录放设计文档、API 手册、芯片数据手册。我习惯按版本建子目录,比如 doc/v1.0/、doc/v2.0/。这样回溯问题时能快速找到对应版本的文档。
build 目录是编译产物。.o 文件、.elf 文件、.bin 文件全放这里。这个目录不要提交到 git,在 .gitignore 里加上 build/。否则每次编译都会产生大量变更,代码审查时根本看不清楚。
一张图看懂目录结构
下面这张图展示了标准目录结构的核心逻辑。每个目录之间通过明确的接口(头文件)进行交互,编译产物独立存放。
避坑指南
最后分享几个我踩过的坑:
- 不要把 build 目录提交到 git——.o 文件是二进制文件,每次编译都会变,代码审查时根本看不出差异。加
build/到 .gitignore。 - 头文件不要用相对路径——
#include "../src/bsp/gpio.h"这种写法,换个目录结构就废了。用-I编译选项指定 include 路径。 - test 目录要和 src 结构一致——别把测试文件乱放。找测试文件的时间应该花在写测试上,而不是找文件上。
- lib 目录里的库要注明版本——我习惯在 lib 子目录里放一个 README,写明库的版本、来源、修改记录。否则半年后你根本记不清这个库是哪个版本的。
一个小建议:刚开始做项目时,花 10 分钟搭好目录结构。别想着「先写代码,后面再整理」。后面永远不会整理——代码只会越写越乱。目录结构就像房子的地基,地基歪了,房子再漂亮也住不安稳。