关于工程的目录结构

Pasted image 20260116105826
看到你的截图,感觉你的工程结构瞬间专业了很多!

关于你提到的单复数命名问题,这确实是程序员“强迫症”经常纠结的地方。在业界并没有绝对的死规定,但有一套通用的惯例。

1. 文件夹命名:单数还是复数?

在编程习惯中,文件夹命名通常遵循以下逻辑:

  • 单数(如 Task, Driver, Module):强调的是分类名称逻辑层级。比如 Task 文件夹的意思是“这里是存放任务层代码的地方”。

  • 复数(如 Utils, Tools):强调的是内容的集合。因为 Util(工具)通常是由很多个互不相关的独立函数(如数学转换、字符串处理)组成的,所以习惯上用复数 Utils

建议建议:

为了视觉上的统一,你可以考虑全部改为复数,这也是目前开源项目(如很多 GitHub 上的库)更主流的做法:

  • Drivers (因为里面有很多个驱动)

  • Tasks (因为里面有很多个任务)

  • Modules

  • Utils


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 自动搜索 App 目录下所有的 .c 源文件
file(GLOB_RECURSE APP_SOURCES
"App/*.c"
)

# 2. 将 App 及其子目录添加到头文件搜索路径
include_directories(
App/Driver
App/Module
App/Task
App/Utils
# 如果以后还有新文件夹,记得在这里加一行
)

# 3. 在可执行文件目标中加入这些源文件
# 找到 add_executable 这一行,确保包含 ${APP_SOURCES}
add_executable(${PROJECT_NAME} ${SOURCES} ${APP_SOURCES})

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 的配置技巧

  1. 在 CubeMX 的 FreeRTOS 选项中,只保留默认生成的 defaultTask(或者干脆一个都不建,只配置 Heap 和 Kernel 参数)。

  2. Code Generation 设置中,确保勾选了 “Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”,这样生成的代码更散,不会全挤在 main.c

第二步:在 App 目录下建立任务管理

在你的 App/Tasks 目录下创建一个 app_tasks.capp_tasks.h,专门用来写你的业务任务。

App/Tasks/app_tasks.c 示例:

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "FreeRTOS.h"
#include "task.h"
#include "app_main.h"

// 定义任务句柄
TaskHandle_t MyTaskHandle = NULL;

// 具体的任务函数实现
void MyTask_Entry(void *argument) {
// 这里可以调用你 Modules 里的驱动
for(;;) {
// 执行业务逻辑
vTaskDelay(pdMS_TO_TICKS(100));
}
}

// 暴露一个统一的初始化接口
void App_Tasks_Init(void) {
xTaskCreate(MyTask_Entry, "MyTask", 256, NULL, osPriorityNormal, &MyTaskHandle);
}

第三步:在 app_main.c 中接管启动流

你的 app_main.c 现在的 init()loop() 模式需要微调。在 RTOS 下,loop() 几乎是用不到的,因为内核接管了。

App/app_main.c

C

1
2
3
4
5
6
7
8
9
10
11
12
#include "app_main.h"
#include "app_tasks.h"

void App_Init(void) {
// 1. 初始化你的底层驱动(如 SX1280)
SX1280_Init();

// 2. 创建所有业务任务
App_Tasks_Init();

// 此时并不启动调度器,调度器由 main.c 启动
}

第四步:修改 main.c 的入口

在 CubeMX 生成的 main.c 中,你只需要在调度器启动前,插入你的 App_Init()

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* main.c */
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI_Init();
// ... 其他 MX 初始化

/* 调用你的 App 初始化 */
App_Init();

/* 启动 FreeRTOS 调度器 */
osKernelStart();

while (1) {
// 永远不会运行到这里
}
}

这种架构的优势

  1. 图形化与代码分离:如果你以后想在 CubeMX 里增加一个 PWM 或修改时钟,点击 Generate Code,它只会修改 MX_xxx_Init(),绝对不会动你 App 目录下的任何一行逻辑代码。

  2. 纯净的 freertos.c:你可以完全不理会 freertos.c。所有的任务堆栈大小、优先级都在你自己的 app_tasks.c 里通过代码控制,比在 GUI 里点点点更直观,也方便做版本控制(Git 里的 diff 会非常清晰)。

  3. 兼容性:即使哪天你不想用 FreeRTOS 了,想换成 RT-Thread 或者裸机,你只需要修改 App/Tasks 里的实现,Core 文件夹基本不用动。

进阶建议:利用好你的 MingLog

在 RTOS 环境下,建议在 MingLog 中加入 中断安全机制(判断当前是否在中断中)和 互斥锁,防止多个任务同时打印导致串口数据乱码。

你想让我帮你写一个针对 FreeRTOS 环境下带互斥锁保护的 MingLog 打印函数模板吗?

这种做法好吗?相比直接在freertos.c和main.c里写代码?

另外,cubeMX里不能delete默认的defaulttask

这种做法在专业嵌入式开发中被视为最佳实践(Best Practice)。相比于直接在 main.cfreertos.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.cStartDefaultTask 函数里,只放一行代码:

    C

    1
    2
    3
    4
    5
    6
    void StartDefaultTask(void *argument) {
    for(;;) {
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 闪个灯,证明系统没挂
    osDelay(1000);
    }
    }

策略 B:将其作为“初始化启动器”

利用它来启动你的 App

  • defaultTask 里调用你的 App_Init(),然后自毁

    C

    1
    2
    3
    4
    void StartDefaultTask(void *argument) {
    App_Init(); // 在这里创建你所有的 App 任务
    vTaskDelete(NULL); // 任务自毁,腾出堆栈空间
    }

3. 推荐的项目通信流图

采用这种架构后,你的系统运行流程应该是这样的:

  1. 硬件层 (Core): 负责把时钟、SPI、UART 这些“管脚”拨通。

  2. 调度层 (freertos.c): 仅仅作为 OS 的入口点,不写任何业务逻辑。

  3. 应用层 (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 端的配置

  1. 在 FreeRTOS 的 Tasks 标签页,双击 defaultTask

  2. Code Generation OptionDefault 改为 As external 或者直接在生成后忽略它。

  3. 最关键的一点:在 Task Parameters 里的 Entry Function,你可以把它改成一个更有意义的名字,比如 App_EntryPoint

2. 在 app_main.c 中接管

现在,你的 app_main.c 成了整个项目的“生命起点”。它不再需要被动地由 main.c 调用 App_Init,它自己就是第一个运行的任务。

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "FreeRTOS.h"
#include "task.h"

// 引入各任务的启动函数
extern void Start_LEDTask(void);
extern void Start_UARTTask(void);
extern void Start_RadioTask(void);

/**
* @brief 接管 CubeMX 生成的默认任务入口
* @note 这个函数现在运行在 RTOS 调度器启动后的第一个任务上下文中
*/
void StartDefaultTask(void *argument) // 这里的名字要和 CubeMX 里定义的一致
{
// 1. 初始化那些依赖 OS 的模块(比如信号量、队列、互斥锁)
// MingLog_OS_Init();

// 2. 启动其他业务任务(招聘员工)
Start_LEDTask();
Start_UARTTask();
Start_RadioTask();

// 3. 此时 DefaultTask 的使命完成了,它有两条路:

// 方案 A:原地转化为“监视任务”(监控系统运行状态)
for(;;)
{
// 比如:每秒喂一次看门狗,或者闪个“系统正常”的绿灯
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
vTaskDelay(pdMS_TO_TICKS(1000));
}

// 方案 B:如果不想让它占内存,直接自毁
// vTaskDelete(NULL);
}

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. 建议的操作步骤

  1. 修改 CubeMX:把 defaultTask 的 Entry Function 改为 App_EntryPoint

  2. 生成代码

  3. app_main.c 实现:写出 void App_EntryPoint(void *argument) { ... }

  4. 清理 main.c:删掉原本在裸机时代添加的 App_Init()App_Loop()

既然决定用这种“自启动任务”模式,你现在的任务(比如 LEDTask)的堆栈大小,你打算根据什么原则来分配呢?(这是初学者最容易导致 HardFault 死机的地方)


关于工程的目录结构
https://mingzaitown.github.io/2026/01/18/STM32/关于工程的目录结构/
作者
MingZai
发布于
2026年1月19日
许可协议