前言
在嵌入式开发中,裸机开发仅能实现简单的前后台循环逻辑,面对多设备协同、多任务并行、定时处理等复杂场景,存在任务阻塞、实时性差、代码耦合度高、维护难度大等诸多问题。而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 源码
获取地址: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最大主频)。
注意:HSE(外部高速晶振)的频率需要根据实际开发板上的晶振规格进行配置。

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 (151024)
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 0 */
#include <stdio.h>
/* USER CODE END 0 */
/* 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 = 12;
RCC_OscInitStruct.PLL.PLLN = 168;
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/1Mj4Uxjb7Pf5DU3K5cbc6gQ?pwd=taoq 提取码: taoq
--来自百度网盘超级会员v3的分享
参考与致谢
RT-Thread 官方文档
基于STM32的嵌入式操作系统RT-Thread移植教学(HAL库版本)_rt-thread stm32移植-CSDN博客
【RT-Thread】基于STM32+HAL库的RT-Thread移植(纯手动)_rtthread-CSDN博客
以上资料提供了清晰的 HAL 库手动移植框架,本文在此基础上进行了实际验证与细节补充,特此致谢。
后续将更新使用RT-Thread Studio从零开始创建标准示例工程。
若发现文中疏漏,欢迎指正交流。创作不易,还望多多支持。

3万+

被折叠的 条评论
为什么被折叠?



