STM32F407 + RT-Thread 实战:从工程结构到多线程 LED 闪烁
一、工程简介
最近看了一个基于 `STM32F407` 的 `RT-Thread` 工程,整体结构比较标准,功能上也比较适合作为入门练手项目。
这个工程的核心功能并不复杂,主要是通过 `RT-Thread` 创建多个线程,分别控制不同的 LED 引脚按不同节奏闪烁。虽然只是一个“多线程点灯”实验,但它已经把 RTOS 中几个关键概念串起
来了,比如:
- 线程创建
- 线程调度
- GPIO 控制
- 板级初始化
- 自动初始化机制
如果你刚开始接触 `RT-Thread`,这种工程非常适合拿来分析和练手。
---
二、工程目录结构分析
从目录上看,这个工程主要包含以下几个部分:
- `applications`
- `drivers`
- `cubemx`
- `rt-thread`
- `.config / rtconfig.h`
1. applications
这是应用层代码目录,也是我们最需要关注的部分。
当前工程中主要有两个文件:
- `main.c`
- `led_blink.c`
它们分别完成不同线程的创建与启动。
2. drivers
这个目录主要是板级支持包相关代码,比较关键的是 `board.c`,负责:
- 堆内存初始化
- 底层硬件初始化
- 控制台设备设置
- 板级组件初始化
也就是说,RT-Thread 跑起来之前,底层环境准备工作主要在这里完成。
3. cubemx
这个目录里是 `STM32CubeMX` 生成的底层代码,包括:
- GPIO 初始化
- 时钟配置
- 中断处理文件
- HAL 驱动源码
- 启动文件
说明这个项目是通过 `CubeMX + RT-Thread BSP` 结合起来搭建的。
4. rt-thread
这里是 RT-Thread 内核和组件源码,包括:
- 线程调度
- 内存管理
- 设备驱动框架
- FinSH/MSH 命令行组件
- 各类内核组件
通常做应用开发时不会频繁修改这里的源码,但理解其结构对后续学习很有帮助。
---
三、这个工程实现了什么功能
这个工程本质上是一个“多线程控制 3 个 LED 闪烁”的实验。
对应引脚如下:
- `PA6`:LED1
- `PA4`:LED2
- `PA3`:LEDSYS
三个线程的运行节奏不同:
- `LED1` 每 `100ms` 闪烁一次
- `LED2` 每 `500ms` 闪烁一次
- `LEDSYS` 每 `1000ms` 翻转一次
从实验现象上看,就是三个 LED 同时以不同频率闪烁,这正好体现了 RTOS 多线程并发运行的基本效果。
---
四、main.c 中的动态线程创建
在 `main.c` 中,定义了两个线程入口函数,分别控制 LED1 和 LED2。
1. LED1 与 LED2 线程入口函数
#include <rtthread.h> #define DBG_TAG "main" #define DBG_LVL DBG_LOG #include <rtdbg.h> #include "main.h" #include <board.h> #include <rtdbg.h> #define LED1_PIN GET_PIN(A,6) #define LED2_PIN GET_PIN(A,4) #define LEDSYS_PIN GET_PIN(A, 3) #define THREAD_PRIORITY 25 #define THREAD_TIMESLICE 5 void led1_thread_entry(void *parameter) { while(1) { rt_pin_write(LED1_PIN, PIN_LOW); rt_thread_mdelay(100); rt_pin_write(LED1_PIN, PIN_HIGH); rt_thread_mdelay(100); } } void led2_thread_entry(void *parameter) { while(1) { rt_pin_write(LED2_PIN, PIN_LOW); rt_thread_mdelay(500); rt_pin_write(LED2_PIN, PIN_HIGH); rt_thread_mdelay(500); } }这里比较容易看懂:
- led1_thread_entry() 控制 PA6
- led2_thread_entry() 控制 PA4
- 两个线程都在 while(1) 中循环运行
- 通过 rt_thread_mdelay() 实现周期性闪烁
2. main 函数中创建线程
int main(void) { int count = 1; rt_pin_mode(LED1_PIN, PIN_MODE_OUTPUT); rt_pin_mode(LED2_PIN, PIN_MODE_OUTPUT); rt_thread_t thread_led1 = rt_thread_create("LED1", led1_thread_entry, RT_NULL, 1024, THREAD_PRIORITY, THREAD_TIMESLICE); if (thread_led1 != RT_NULL) { rt_thread_startup(thread_led1); } rt_thread_t thread_led2 = rt_thread_create("LED2", led2_thread_entry, RT_NULL, 1024, THREAD_PRIORITY, THREAD_TIMESLICE); if (thread_led2 != RT_NULL) { rt_thread_startup(thread_led2); } while (count++) { rt_thread_mdelay(1000); } return RT_EOK; }这部分展示了 RT-Thread 最常见的动态线程创建方式:
- rt_thread_create():创建线程
- rt_thread_startup():启动线程
线程创建时传入的参数分别包括:
- 线程名称
- 线程入口函数
- 入口参数
- 线程栈大小
- 线程优先级
- 时间片
这种方式的优点是使用方便,适合快速创建任务;缺点是依赖动态内存分配。
———
五、led_blink.c 中的静态线程创建
除了动态线程,这个工程还演示了静态线程初始化方式,代码在 led_blink.c 中。
#include <rtthread.h> #include <board.h> #include <rtdbg.h> #define LEDSYS_PIN GET_PIN(A, 3) #define THREAD_PRIORITY 25 #define THREAD_TIMESLICE 5 ALIGN(RT_ALIGN_SIZE) static char ledsys_stack[256]; static struct rt_thread ledsys; void ledsys_thread_entry(void *parameter) { while(1) { rt_pin_write(LEDSYS_PIN, !rt_pin_read(LEDSYS_PIN)); rt_thread_mdelay(1000); } } int ledsys_thread_init(void) { rt_pin_mode(LEDSYS_PIN, PIN_MODE_OUTPUT); rt_thread_init(&ledsys, "LEDSYS", ledsys_thread_entry, RT_NULL, ledsys_stack, sizeof(ledsys_stack), THREAD_PRIORITY - 1, THREAD_TIMESLICE); rt_thread_startup(&ledsys); return RT_EOK ; } INIT_APP_EXPORT(ledsys_thread_init);这个文件的重点有两个。
1. 静态线程对象和栈空间
ALIGN(RT_ALIGN_SIZE) static char ledsys_stack[256]; static struct rt_thread ledsys;这表示:
- 线程栈由用户手动分配
- 线程控制块也是静态定义的
- 不依赖动态内存
这种方式在资源可控、稳定性要求高的场景更常见。
2. 自动初始化机制
INIT_APP_EXPORT(ledsys_thread_init);
这一句非常关键。
它的作用是把 ledsys_thread_init() 注册到应用初始化阶段,在系统启动时自动调用。也就是说,这个线程不需要在 main() 里手动创建,系统启动后会自动完成初始化和启动。
六、board.c 中的板级初始化
在 drivers/board.c 中,可以看到 RT-Thread 板级初始化入口:
RT_WEAK void rt_hw_board_init() { extern void hw_board_init(char *clock_src, int32_t clock_src_freq, int32_t clock_target_freq); #if defined(RT_USING_HEAP) rt_system_heap_init((void *) HEAP_BEGIN, (void *) HEAP_END); #endif hw_board_init(BSP_CLOCK_SOURCE, BSP_CLOCK_SOURCE_FREQ_MHZ, BSP_CLOCK_SYSTEM_FREQ_MHZ); #if defined(RT_USING_DEVICE) && defined(RT_USING_CONSOLE) rt_console_set_device(RT_CONSOLE_DEVICE_NAME); #endif #ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); #endif }这段代码说明系统上电后主要做了以下几件事:
- 初始化堆内存
- 初始化板级底层硬件
- 设置控制台设备
- 执行组件初始化
这部分代码虽然不直接控制 LED,但它是整个 RT-Thread 工程正常运行的基础。
———
七、工程配置项分析
从 rtconfig.h 可以看到,该工程启用了一些比较常用的 RT-Thread 功能。
例如:
#define RT_USING_COMPONENTS_INIT #define RT_USING_USER_MAIN #define RT_USING_MSH #define RT_USING_FINSH #define RT_USING_SERIAL #define RT_USING_PIN #define RT_USING_SEMAPHORE #define RT_USING_MUTEX #define RT_USING_EVENT #define RT_USING_MAILBOX #define RT_USING_MESSAGEQUEUE这说明当前工程已经具备:
- 用户 main() 入口
- MSH/FinSH 命令行
- 串口驱动支持
- GPIO 引脚操作
- 常见线程间通信机制
不过需要注意的是,虽然配置中打开了信号量、互斥锁、事件、邮箱、消息队列这些功能,但当前应用层代码还没有实际使用到,现阶段主要还是一个线程调度和 GPIO 输出实验。
———
八、这个工程适合学什么
虽然这个工程功能简单,但它非常适合作为 RT-Thread 入门项目,尤其适合学习以下内容:
- RT-Thread 工程结构
- 动态线程创建方式
- 静态线程创建方式
- 线程优先级与时间片
- GPIO 控制 LED
- 自动初始化宏 INIT_APP_EXPORT
- 板级初始化流程
对于初学者来说,这种工程的价值不在于功能复杂,而在于它足够清晰,能帮助我们快速建立对 RTOS 工程组织方式的理解。
———
九、总结
这个 STM32F407 + RT-Thread 工程,本质上就是一个“多线程点灯”实验工程。
它主要完成了三件事:
- 在 main.c 中动态创建两个线程
- 在 led_blink.c 中静态创建一个自动启动线程
- 在 board.c 中完成底层板级初始化
虽然目前业务功能比较简单,但它已经具备了一个标准 RT-Thread 工程的基本框架。后续如果继续扩展,可以很自然地加入:
- 按键输入任务
- 串口通信任务
- 消息队列通信
- 软件定时器
- 传感器采集线程
对于刚开始学习 RT-Thread 的同学来说,这样一个工程非常适合作为起点。
———
十、结尾
如果你刚开始接触 RT-Thread,建议先把这种“多线程点灯”工程彻底跑通,再一步一步往里面加功能。先理解线程,再理解线程间通信,最后再逐步扩展成完整项目,这样学习路径会更清晰。
如果这篇文章对你有帮助,欢迎交流。
