【超详细】STM32F407ZGT6 + HAL库 手把手移植RT-Thread
前言
在嵌入式开发中,裸机开发仅能实现简单的前后台循环逻辑,面对多设备协同、多任务并行、定时处理等复杂场景,存在任务阻塞、实时性差、代码耦合度高、维护难度大等诸多问题。而RT-Thread作为国内开源、轻量化、高实时性的嵌入式RTOS,兼具小巧精简、稳定可靠、生态完善的优势,是单片机裸机开发进阶实时操作系统开发的最优选择之一。
本文采用STM32CubeMX原生HAL库裸机工程 + 纯手动移植RT-Thread的方式,从零完成内核适配、时钟节拍配置、串口控制调试、多任务测试,摒弃一键生成框架,拆解每一步移植核心逻辑,规避各类适配冲突问题。教程适配新手、步骤完整、落地性强,看完即可独立完成RT-Thread HAL库版本移植,彻底掌握Cortex-M内核RTOS底层适配核心思路。
开发环境
主控芯片:STM32F407ZGT6
编译工具:Keil MDK5
配置工具:STM32CubeMX 6.17.0
操作系统:RT-Thread 3.1.5(轻量稳定,适配F4内核)
驱动库:STM32F4xx HAL库 最新版
目录
前言
一、下载 RT-Thread 源码
二、RT-Thread 源码概述
2.1 RT-Thread 目录结构
2.2 RT-Thread 文件介绍
2.2.1 bsp
2.2.2 libcpu
2.2.3 src
2.2.4 RT-Thread 核心文件
三、CubeMX生成HAL裸机工程
3.1 芯片选型
3.2 配置系统时钟
3.3 设置SYS
3.3 配置GPIO
3.4 配置UART
3.5 生成最终工程
四、移植RT-Thread
4.1 移植时涉及的主要文件
4.1.1 CPU 部分
4.1.2 BSP 部分
4.2 移植操作
4.3 RT-Thread 源码改写
五、上机测试
六、源码分享
一、下载 RT-Thread 源码
获取地址:https://www.rt-thread.org/download.html
这里我们选择rt-thread-3.1.5.zip。
选择 3.1.5 版本的主要原因是:
经典稳定:经过长期工业验证,内核稳定,Bug 极少。
体积极小:压缩包仅122MB(相比 5.x 的近 500MB),去除了冗余的板级支持包,下载解压快。
适配成熟:对 STM32F1/F4 等经典 MCU 的底层支持极其完善,开箱即用。
工具链友好:高度兼容老版本 MDK/IAR,无需强迫迁入新版 RT-Thread Studio 开发环境。
(注:如果是新品开发,日后需要联网、LVGL等复杂组件,建议尽量升级到 5.x 版本;如果只是学习或老项目延续,3.1.5 足够。)
二、RT-Thread 源码概述
2.1 RT-Thread 目录结构
2.2 RT-Thread 文件介绍
2.2.1 bsp
bsp目录下是各单板的板级支持包(Board support package),即针对某个特定单板兼容适配的工程。比如ST官方公板:stm32\stm32f407-st-discovery。
移植时,需要针对特定单板适配串口端口、GPIO等。
2.2.2 libcpu
为了在不同CPU架构芯片上运行,RT-Thread提供 libcpu 抽象层对内核提供统一接口,包括全局中断开关、线程栈初始化等。
移植时,需要针对特地CPU架构实现全局中断开关、线程上下文切换等。
2.2.3 src
src文件夹的内容是RT-Thread 的核心文件。
2.2.4 RT-Thread 核心文件
以理解、使用RT-Thread 为目标的话,最核心的文件只有5个:
- src\ipc.c
- src\scheduler.c
- src\thread.c
- src\timer.c
- include\rtservice.h
这些文件的作用列表如下:
| 重要的文件 | 作用 |
|---|---|
| src\thread.c | 线程核心文件,实现线程创建、启动、挂起、删除等全套线程管理功能 |
| src\timer.c | 软件定时器相关实现,依托系统节拍完成定时唤醒、周期调度 |
| src\scheduler.c | 任务调度器核心:维护线程就绪链表、实现优先级抢占调度,提供临界区进出保护函数 |
| src\ipc.c | 线程间通信组件实现,包含消息队列、信号量、互斥量、事件集等同步互斥对象 |
| include\rtservice.h | 封装通用双向链表操作接口,为线程、定时器、IPC 对象提供底层链表支撑 |
三、CubeMX生成HAL裸机工程
3.1 芯片选型
打开STM32CubeMX,选择芯片STM32F407ZGT6,新建工程。
3.2 配置系统时钟
在RCC中,配置外部高速晶振(HSE)。
时钟树:倍频至168MHz(F407最大主频)。
3.3 设置SYS
开启SYS时钟、调试接口(SWD模式)。
3.3 配置GPIO
开发板上,PF9、PF10为LED1、LED2,低电平点亮。
3.4 配置UART
配置USART1(PA9/PA10)为异步串口,这里不用作FinSH控制台,只作任务输出信息用。另外这里不用中断和DMA。
3.5 生成最终工程
选择MDK-ARM,以及配置工程,如下图:
四、移植RT-Thread
4.1 移植时涉及的主要文件
移植RT-Thread,包含CPU 部分移植和BSP 部分移植。
4.1.1 CPU 部分
CPU 部分移植就是移植libcpu 目录,主要涉及cpuport.c 和context_xx.S。
比如:libcpu/arm/cortex-m4/cpuport.c 和 context_rvds.S,这表示 Cortex-M4 架构在 RVDS 或Keil 工具上的移植文件。
- cpuport.c:主要用来初始化线程的栈。
- context_rvds.S:主要线程切换、全局中断开关。
如果RT-Thread 还没支持你使用的CPU,你才需要实现这两个文件。 一般来说,对于大部分的CPU, RT-Thread都已经支持,不需要我们移植。
4.1.2 BSP 部分
BSP 就是Board Support Packet,板级支持包,就是开发板的相关文件。
BSP 部分主要涉及main.c、board.c 和rtconfig.h
比如:bsp/stm32/stm32f407-atk-explorer 目录下,包含 applications/main.c 、 board/board.c 和 rtconfig.h
- main.c:工程的主函数入口 。
- board.c:该单板的系统时钟相关配置 。
- rtconfig.h:内核配置文件,配置裁剪系统功能。
4.2 移植操作
(1)将解压得到的 rt-thread-3.1.5 源码文件夹,复制到裸机工程目录下,并在该目录中新建一个名为“RT-Thread”的文件夹。
(2)在新建的 RT-Thread 文件夹中,分别创建 config、board、libcpu 三个子文件夹。接着,在 libcpu 文件夹内创建 arm 文件夹,并在 arm 文件夹中继续创建 cortex-m4 文件夹。
(3)将\F407_RTT_HAL\rt-thread-3.1.5\rt-thread-3.1.5\bsp\stm32\stm32f407-atk-explorer目录下的rtconfig.h文件,复制到新建的RT_Thread\config文件夹中。
(4)将\F407_RTT_HAL\rt-thread-3.1.5\rt-thread-3.1.5\bsp\stm32\stm32f407-atk-explorer\board目录下的board.c和board.h文件,复制到新建的RT_Thread\board文件夹中。
(5)将\F407_RTT_HAL\rt-thread-3.1.5\rt-thread-3.1.5\libcpu\arm\cortex-m4目录下的context_rvds.S和cpuport.c文件,复制到\F407_RTT_HAL\RT-Thread\libcpu\arm\cortex-m4目录中。
(6)将\F407_RTT_HAL\rt-thread-3.1.5\rt-thread-3.1.5目录下的include、src和components文件夹,完整复制到\F407_RTT_HAL\RT_Thread目录下。
(7)删除\F407_RTT_HAL\RT-Thread\components目录下的finsh文件夹。
(8)在 Keil5 中新建四个组别:RT_Thread/config、RT_Thread/board、RT_Thread/libcpu和RT_Thread/src。
(9)将对应的文件分别添加到这四个新建的组中。
(10)在 keil5 中添加对应路径
4.3 RT-Thread 源码改写
(1)在rtconfig.h文件中,注释掉RT_USING_FINSH宏定义。由于本工程不需要命令行交互调试功能,因此需要在组件配置中关闭 FinSH。
(2)为避免函数重定义冲突,需在stm32f4xx_it.c和stm32f1xx_it.h文件中注释掉PendSV_Handler、SysTick_Handler和HardFault_Handler这三个中断服务函数。
(3)根据目标板卡需求,重新编写board.c文件。
/* * Copyright (c) 2006-2021, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2018-11-06 SummerGift first version */ #include "board.h" #include <rthw.h> #include <rtthread.h> #include "main.h" #include "usart.h" #include "gpio.h" #include "stdio.h" /* 定义内存堆的大小与空间 */ #if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP) /* * Please modify RT_HEAP_SIZE if you enable RT_USING_HEAP * the RT_HEAP_SIZE max value = (sram size - ZI size), 1024 means 1024 bytes */ #define RT_HEAP_SIZE (15*1024) static rt_uint8_t rt_heap[RT_HEAP_SIZE]; RT_WEAK void *rt_heap_begin_get(void) { return rt_heap; } RT_WEAK void *rt_heap_end_get(void) { return rt_heap + RT_HEAP_SIZE; } #endif void rt_os_tick_callback(void) { rt_interrupt_enter(); rt_tick_increase(); rt_interrupt_leave(); } /** * This function will initial your board. */ void rt_hw_board_init(void) { /* 初始化 SysTick */ extern void SystemClock_Config(void); SystemClock_Config(); //下面放硬件初始化 MX_GPIO_Init(); MX_USART1_UART_Init(); #ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); #endif #if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP) rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get()); #endif } #ifdef RT_USING_CONSOLE /** * @brief 串口控制台初始化(此处未使用,所以保留空函数即可) */ static int uart_init(void) { //#error "TODO 2: Enable the hardware uart and config baudrate." return 0; } INIT_BOARD_EXPORT(uart_init); /** * @brief 重定向 RT-Thread 默认打印输出(未使用 rt_kprintf,留空不实现) */ void rt_hw_console_output(const char *str) { //#error "TODO 3: Output the string 'str' through the uart." } #endif void SysTick_Handler(void) { /* 进入中断 */ rt_interrupt_enter(); /* 更新时基 */ rt_tick_increase(); /* 离开中断 */ rt_interrupt_leave(); }(4)重新写board.h。
/* * Copyright (c) 2006-2021, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2018-11-5 SummerGift first version */ #ifndef BOARD_H #define BOARD_H #include "stm32f4xx_hal.h" void rt_hw_board_init(void); void SysTick_Handler(void); #endif(5)重定向printf函数,在usart.c文件的末尾添加以下代码。
/* USER CODE BEGIN 1 */ int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (const uint8_t *)&ch, 1, 50); return ch; } /* USER CODE END 1 */用
50而不用0xFFFF,核心原因就一个:防止硬件异常时线程被永久卡死。
0xFFFF(HAL_MAX_DELAY) 是无限等待,一旦串口状态位没响应,函数永不返回,线程甚至整个系统都会挂掉。
50ms是有上限的阻塞,足够正常发送几百个字节(115200 下非常充裕)。万一出错也会超时返回,系统还能继续跑。
(6)编写main.c文件。在该文件中,我们静态创建了一个线程(线程1,用于串口打印“thread1 run”),并动态创建了两个线程:线程2(串口打印“thread2 run”)和线程3(控制 LED1 和 LED2 交替闪烁)。
/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2026 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "usart.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "board.h" #include "rtthread.h" #include "stdio.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ static struct rt_thread thread1; //定义线程1句柄 static struct rt_thread *thread2; //定义线程2句柄指针 static struct rt_thread *thread3; //定义线程3句柄指针 static rt_uint8_t thread1_stack[1024];//设置线程1的内存栈 /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* 线程1的入口函数 */ static void thread1_entry(void *parameter) { while(1) { printf("Thread1 run\r\n"); rt_thread_delay(1000); } } /* 线程2的入口函数 */ static void thread2_entry(void *parameter) { while(1) { printf("Thread2 run\r\n"); rt_thread_delay(2000); } } /* 线程3的入口函数 */ static void thread3_entry(void *parameter) { while(1) { HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); rt_thread_delay(1000); HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET); rt_thread_delay(1000); } } /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ //HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ //SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ //MX_GPIO_Init(); //MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ /* 初始化静态线程1,名称是Thread1,入口是thread1_entry */ rt_thread_init(&thread1, //线程句柄 "thread1", //线程名字 thread1_entry, //入口函数 RT_NULL, //入口函数参数 &thread1_stack[0], //线程栈起始地址 sizeof(thread1_stack), //栈大小 5, //线程优先级 15); //线程时间片大小 /* 启动线程1 */ rt_thread_startup(&thread1); /* 创建动态线程2,名称是thread2,入口是thread2_entry*/ thread2 = rt_thread_create("thread2", //线程名字 thread2_entry, //入口函数 RT_NULL, //入口函数参数 512, //栈大小 5, //线程优先级 15); //线程时间片大小 /* 判断创建结果,再启动线程2 */ if (thread2 != RT_NULL) { rt_thread_startup(thread2); } /* 创建动态线程3,名称是thread3,入口是thread3_entry*/ thread3 = rt_thread_create("thread3", //线程名字 thread3_entry, //入口函数 RT_NULL, //入口函数参数 512, //栈大小 5, //线程优先级 15); //线程时间片大小 /* 判断创建结果,再启动线程2 */ if (thread3 != RT_NULL) { rt_thread_startup(thread3); } /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { rt_thread_delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Configure the main internal regulator output voltage */ __HAL_RCC_PWR_CLK_ENABLE(); __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM = 25; RCC_OscInitStruct.PLL.PLLN = 336; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; RCC_OscInitStruct.PLL.PLLQ = 4; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) { Error_Handler(); } } /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ /** * @brief This function is executed in case of error occurrence. * @retval None */ void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { } /* USER CODE END Error_Handler_Debug */ } #ifdef USE_FULL_ASSERT /** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */ void assert_failed(uint8_t *file, uint32_t line) { /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ } #endif /* USE_FULL_ASSERT */(7)在 Keil5 中配置工程,勾选“Use MicroLIB”选项,并完成 Debug 相关配置。
问题:在测试过程中,发现三个线程均未运行,串口无数据输出,LED 灯也不亮。通过调试发现,程序卡在
_sys_open函数处。经排查,原因是代码中使用了printf这类标准 C 库 I/O 函数,而工程未勾选Use MicroLIB选项。标准库在启动时会尝试通过半主机(semihosting)打开stdout,在没有调试器连接的情况下,该操作会无限等待,导致程序无法进入main函数,所有线程因此无法运行。解决:有两种解决方案:一是在 Keil5 工程配置中勾选Use MicroLIB;二是自行实现
_sys_open、_write等函数,以绕过半主机机制。
五、上机测试
下载程序到STM32F407ZGT6开发板,上电后LED周期闪烁,串口会交替输出,说明多任务线程正常调度。
六、源码分享
通过网盘分享的文件:F407_RTT_HAL.zip
链接: https://pan.baidu.com/s/1uei1uzYubFSna8AGxITOyA?pwd=taoq 提取码: taoq
--来自百度网盘超级会员v3的分享。
参考与致谢
RT-Thread 官方文档
基于STM32的嵌入式操作系统RT-Thread移植教学(HAL库版本)_rt-thread stm32移植-CSDN博客
【RT-Thread】基于STM32+HAL库的RT-Thread移植(纯手动)_rtthread-CSDN博客
以上资料提供了清晰的 HAL 库手动移植框架,本文在此基础上进行了实际验证与细节补充,特此致谢。
若发现文中疏漏,欢迎指正交流。创作不易,还望多多支持。