关于工程的目录结构

看到你的截图,感觉你的工程结构瞬间专业了很多!
关于你提到的单复数命名问题,这确实是程序员“强迫症”经常纠结的地方。在业界并没有绝对的死规定,但有一套通用的惯例。
1. 文件夹命名:单数还是复数?
在编程习惯中,文件夹命名通常遵循以下逻辑:
单数(如 Task, Driver, Module):强调的是分类名称或逻辑层级。比如
Task文件夹的意思是“这里是存放任务层代码的地方”。复数(如 Utils, Tools):强调的是内容的集合。因为
Util(工具)通常是由很多个互不相关的独立函数(如数学转换、字符串处理)组成的,所以习惯上用复数Utils。
建议建议:
为了视觉上的统一,你可以考虑全部改为复数,这也是目前开源项目(如很多 GitHub 上的库)更主流的做法:
Drivers(因为里面有很多个驱动)Tasks(因为里面有很多个任务)ModulesUtils
2. 各个文件夹该放什么?
为了保持工程的整洁,建议按照以下原则“对号入座”:
| 文件夹 | 存放内容示例 | 例子 |
|---|---|---|
| Driver | 与特定硬件芯片相关的封装代码。直接操作 HAL 库或寄存器,驱动板载硬件。 | oled.c, mpu6050.c, motor_pwm.c |
| Module | 具备独立功能的业务模块。通常不直接操作硬件,而是调用 Driver。 | pid_controller.c, kalman_filter.c, imu_sensor_fusion.c |
| Task | FreeRTOS 的任务函数。负责逻辑调度,把各个 Module 串联起来。 | task_flight_control.c, task_telemetry.c |
| Utils | 通用的工具函数。与当前项目逻辑无关,在任何工程都能用的。 | MingLog.c, math_utils.c, ring_buffer.c |
3. 如何让你的 VS Code (CMake) 识别这些新文件夹?
由于你手动增加了这么多深度目录,你需要修改根目录下的 CMakeLists.txt。否则,你编译时会报错说找不到头文件,或者函数未定义。
你可以尝试在 CMakeLists.txt 中加入这段逻辑,它会自动遍历 App 文件夹:
CMake
1 | |
4. 小细节提醒
头文件包含:在
main.c里如果你想用MingLog,你只需要写#include "MingLog.h",前提是你在 CMake 里已经把App/Utils加入了include_directories。MingLog 的位置:放在
Utils是非常准确的,因为它是一个通用的辅助工具。
你现在是不是在尝试把 MingLog 跑起来?需要我帮你看看如何针对新的目录结构修改 MingLog.h 的包含路径吗?
这个工程架构本来是完全裸机的,就是有APP目录,那个目录下全是我自己的代码,然后app_main.c、.h里面有init()和loop()函数放入main.c来替代原来的cubemx生成的main.c(因为cubemx生成的main.c内容很多,很容易看错或者改错),这种情况下,裸机开发很方便。但是我现在需要用到freertos。如果我使用cubemx直接引入freerots,可以非常方便地通过图形化界面配置,新建任务等等等等,但是cubemx新建的任务函数全都在它后来生成的freertos.c里面,这样的话我又要回到它生成的文件里了。。。所以有没有什么好办法呢
这是一个非常经典的需求!你是希望 保持项目结构的整洁(App 目录独立),同时又不放弃 CubeMX 图形化配置 FreeRTOS 的便利性。
目前你的架构已经是“半解耦”了,要完美接入 FreeRTOS 且不被 freertos.c 绑架,我推荐你采用 “任务注册制” 的架构。
核心思路:弱化 freertos.c,只把它当成“启动器”
不要在 CubeMX 界面里创建具体的任务(Task),只在 CubeMX 里开启 FreeRTOS 核心组件。
第一步:CubeMX 的配置技巧
在 CubeMX 的 FreeRTOS 选项中,只保留默认生成的
defaultTask(或者干脆一个都不建,只配置 Heap 和 Kernel 参数)。在 Code Generation 设置中,确保勾选了 “Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”,这样生成的代码更散,不会全挤在
main.c。
第二步:在 App 目录下建立任务管理
在你的 App/Tasks 目录下创建一个 app_tasks.c 和 app_tasks.h,专门用来写你的业务任务。
App/Tasks/app_tasks.c 示例:
C
1 | |
第三步:在 app_main.c 中接管启动流
你的 app_main.c 现在的 init() 和 loop() 模式需要微调。在 RTOS 下,loop() 几乎是用不到的,因为内核接管了。
App/app_main.c:
C
1 | |
第四步:修改 main.c 的入口
在 CubeMX 生成的 main.c 中,你只需要在调度器启动前,插入你的 App_Init()。
C
1 | |
这种架构的优势
图形化与代码分离:如果你以后想在 CubeMX 里增加一个 PWM 或修改时钟,点击 Generate Code,它只会修改
MX_xxx_Init(),绝对不会动你App目录下的任何一行逻辑代码。纯净的
freertos.c:你可以完全不理会freertos.c。所有的任务堆栈大小、优先级都在你自己的app_tasks.c里通过代码控制,比在 GUI 里点点点更直观,也方便做版本控制(Git 里的 diff 会非常清晰)。兼容性:即使哪天你不想用 FreeRTOS 了,想换成 RT-Thread 或者裸机,你只需要修改
App/Tasks里的实现,Core文件夹基本不用动。
进阶建议:利用好你的 MingLog
在 RTOS 环境下,建议在 MingLog 中加入 中断安全机制(判断当前是否在中断中)和 互斥锁,防止多个任务同时打印导致串口数据乱码。
你想让我帮你写一个针对 FreeRTOS 环境下带互斥锁保护的 MingLog 打印函数模板吗?
这种做法好吗?相比直接在freertos.c和main.c里写代码?
另外,cubeMX里不能delete默认的defaulttask
这种做法在专业嵌入式开发中被视为最佳实践(Best Practice)。相比于直接在 main.c 或 freertos.c 里塞代码,它的优势是压倒性的。
以下是详细的对比分析,以及如何处理那个“删不掉”的 defaultTask。
1. 深度对比:App 分层架构 vs. CubeMX 混合架构
| 维度 | 直接写在生成的 freertos.c 中 | 采用你的 App 分层架构 |
|---|---|---|
| 代码所有权 | 属于 ST (被 USER CODE 注释包裹) |
完全属于你 |
| 移植性 | 极差,换个芯片(如 GD32)要改几千行 | 极好,只需改底层的 App_Init 适配 |
| 版本控制 | Git 记录里全是 CubeMX 生成的废话 | Git 记录清晰,只看得到你的逻辑变更 |
| 团队协作 | 多人改同一个 main.c 必发冲突 |
同事 A 搞 Tasks,同事 B 搞 Drivers,互不干扰 |
| 逻辑解耦 | 业务逻辑和底层初始化高度耦合 | 完全解耦,符合 SOLID 原则 |
总结: 直接写在 main.c 适合写“一次性”的简单 Demo;做真正的工程项目,必须把业务逻辑从生成的框架中提取出来。
2. 如何处理“删不掉”的 defaultTask?
你说得对,CubeMX 强制要求至少有一个任务。这是因为 CMSIS-RTOS 规范建议至少有一个任务来保证内核不会空转。你可以用两种策略优雅地处理它:
策略 A:将其作为“系统监视器”(推荐)
既然它删不掉,就把它当成项目的 Heartbeat(心跳) 或 Monitor(监控) 任务。
在 CubeMX 里把它的名字改成
MonitorTask。在
freertos.c的StartDefaultTask函数里,只放一行代码:C
1
2
3
4
5
6void StartDefaultTask(void *argument) {
for(;;) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 闪个灯,证明系统没挂
osDelay(1000);
}
}
策略 B:将其作为“初始化启动器”
利用它来启动你的 App。
在
defaultTask里调用你的App_Init(),然后自毁。C
1
2
3
4void StartDefaultTask(void *argument) {
App_Init(); // 在这里创建你所有的 App 任务
vTaskDelete(NULL); // 任务自毁,腾出堆栈空间
}
3. 推荐的项目通信流图
采用这种架构后,你的系统运行流程应该是这样的:
硬件层 (
Core): 负责把时钟、SPI、UART 这些“管脚”拨通。调度层 (
freertos.c): 仅仅作为 OS 的入口点,不写任何业务逻辑。应用层 (
App): 你的App_Init被调用后,开始创建真正的业务任务(比如SX1280_Task)。
4. 一个小陷阱:FreeRTOS 堆栈(Stack)
当你开始在 App 目录下用 xTaskCreate 手动创建任务时,要注意:
内存分配:如果你在 CubeMX 里选的是
configSUPPORT_DYNAMIC_ALLOCATION,那么xTaskCreate会从 FreeRTOS 的堆(通常是ucHeap)里申请内存。总堆大小:记得在 CubeMX 的 FreeRTOS -> Config parameters 里把
TOTAL_HEAP_SIZE改大一点(比如 F407 内存很大,可以给到 32KB 以上),否则你的自定义任务会创建失败。
你想让我帮你写一个 App_Init 的标准模板,把任务创建和错误检查(防止任务创建失败导致死机)都包含进去吗?
最后确定的FreeRTOS工程结构:
不要app_task.c .h,直接用我的App/app_main.c .h来当总的调度集中管理,然后task目录下全都是具体的任务,如LEDtask.c,UARTtask.c等等。为了方便,直接把defaulttask改成我的所有的启动函数,而且cubeMX支持把生成的defaultTask设为虚函数(尽管不能直接删除),所以这样的话在App_main.c是不是就不需要App_init()了,直接改为defaultTask的实现。
这是一个非常聪明的想法!你已经触及到了架构精简的核心。
结论:完全可以,而且这在工程实践中非常流行。
你这是在利用 CubeMX 的“弱符号(Weak Reference)”机制。CubeMX 在 freertos.c 中生成的任务入口通常标记为 __weak,这意味着你可以在自己的 app_main.c 中重新定义它。
以下是实现这一方案的专业姿势:
1. CubeMX 端的配置
在 FreeRTOS 的 Tasks 标签页,双击 defaultTask。
将 Code Generation Option 从
Default改为As external或者直接在生成后忽略它。最关键的一点:在 Task Parameters 里的 Entry Function,你可以把它改成一个更有意义的名字,比如
App_EntryPoint。
2. 在 app_main.c 中接管
现在,你的 app_main.c 成了整个项目的“生命起点”。它不再需要被动地由 main.c 调用 App_Init,它自己就是第一个运行的任务。
C
1 | |
3. 这种做法的优缺点分析
优点:
启动时序最安全:在
App_Init()里创建任务时,RTOS 调度器还没跑,某些涉及 OS API 的操作可能会出问题。而现在,你在StartDefaultTask里操作时,OS 已经跑起来了,所有vTask...接口都可以安全调用。减少对
main.c的侵入:你几乎不需要去改动 CubeMX 生成的main.c,所有的业务逻辑和任务编排都缩在App/目录下。
缺点/注意点:
堆栈大小:CubeMX 默认给
defaultTask分配的堆栈通常较小(如 128 字)。如果你在StartDefaultTask里要做很复杂的初始化,记得在 CubeMX 里调大它的 Stack Size。优先级:
defaultTask默认优先级是osPriorityNormal。如果是启动任务,这没问题。
4. 架构示意图
5. 建议的操作步骤
修改 CubeMX:把
defaultTask的 Entry Function 改为App_EntryPoint。生成代码。
在
app_main.c实现:写出void App_EntryPoint(void *argument) { ... }。清理
main.c:删掉原本在裸机时代添加的App_Init()和App_Loop()。
既然决定用这种“自启动任务”模式,你现在的任务(比如 LEDTask)的堆栈大小,你打算根据什么原则来分配呢?(这是初学者最容易导致 HardFault 死机的地方)