项目目录结构:标准划分原则

说到项目目录结构,我见过太多新手一上来就把所有文件堆在同一个文件夹里。嗯,刚开始写单片机程序时我也这么干过——一个 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/。否则每次编译都会产生大量变更,代码审查时根本看不清楚。

一张图看懂目录结构

下面这张图展示了标准目录结构的核心逻辑。每个目录之间通过明确的接口(头文件)进行交互,编译产物独立存放。

标准项目目录结构 project_root/ src/ 源码文件 include/ 公开头文件 lib/ 第三方库 test/ 测试代码 doc/ 文档资料 build/ 编译产物 目录间的关系 #include 链接 测试 文档 src 通过 include 引用头文件 lib 通过链接器与 src 结合 test 镜像 src 结构进行测试 doc 记录 src 的设计与接口

避坑指南

最后分享几个我踩过的坑:

  • 不要把 build 目录提交到 git——.o 文件是二进制文件,每次编译都会变,代码审查时根本看不出差异。加 build/ 到 .gitignore。
  • 头文件不要用相对路径——#include "../src/bsp/gpio.h" 这种写法,换个目录结构就废了。用 -I 编译选项指定 include 路径。
  • test 目录要和 src 结构一致——别把测试文件乱放。找测试文件的时间应该花在写测试上,而不是找文件上。
  • lib 目录里的库要注明版本——我习惯在 lib 子目录里放一个 README,写明库的版本、来源、修改记录。否则半年后你根本记不清这个库是哪个版本的。

一个小建议:刚开始做项目时,花 10 分钟搭好目录结构。别想着「先写代码,后面再整理」。后面永远不会整理——代码只会越写越乱。目录结构就像房子的地基,地基歪了,房子再漂亮也住不安稳。

公众号:蓝海资料掘金营,微信deep3321