【超详细】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 bsp2.2.2 libcpu2.2.3 src2.2.4 RT-Thread 核心文件三、CubeMX生成HAL裸机工程3.1 芯片选型3.2 配置系统时钟3.3 设置SYS3.3 配置GPIO3.4 配置UART3.5 生成最终工程四、移植RT-Thread4.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 bspbsp目录下是各单板的板级支持包(Board support package)即针对某个特定单板兼容适配的工程。比如ST官方公板stm32\stm32f407-st-discovery。移植时需要针对特定单板适配串口端口、GPIO等。2.2.2 libcpu为了在不同CPU架构芯片上运行RT-Thread提供 libcpu 抽象层对内核提供统一接口包括全局中断开关、线程栈初始化等。移植时需要针对特地CPU架构实现全局中断开关、线程上下文切换等。2.2.3 srcsrc文件夹的内容是RT-Thread 的核心文件。2.2.4 RT-Thread 核心文件以理解、使用RT-Thread 为目标的话最核心的文件只有5个src\ipc.csrc\scheduler.csrc\thread.csrc\timer.cinclude\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。时钟树倍频至168MHzF407最大主频。3.3 设置SYS开启SYS时钟、调试接口SWD模式。3.3 配置GPIO开发板上PF9、PF10为LED1、LED2低电平点亮。3.4 配置UART配置USART1PA9/PA10为异步串口这里不用作FinSH控制台只作任务输出信息用。另外这里不用中断和DMA。3.5 生成最终工程选择MDK-ARM以及配置工程如下图四、移植RT-Thread4.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.hmain.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); #endif5重定向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?pwdtaoq 提取码: taoq--来自百度网盘超级会员v3的分享。参考与致谢RT-Thread 官方文档基于STM32的嵌入式操作系统RT-Thread移植教学HAL库版本_rt-thread stm32移植-CSDN博客【RT-Thread】基于STM32HAL库的RT-Thread移植纯手动_rtthread-CSDN博客以上资料提供了清晰的 HAL 库手动移植框架本文在此基础上进行了实际验证与细节补充特此致谢。若发现文中疏漏欢迎指正交流。创作不易还望多多支持。