十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F072移植RT-Thread Nano与FinSH:轻量级RTOS实践指南

STM32F072移植RT-Thread Nano与FinSH:轻量级RTOS实践指南 1. 项目缘起为什么选择RT-Thread Nano与FinSH最近在做一个基于STM32F072RBT的小型数据采集设备项目需求不算复杂但需要一定的实时性和一个方便调试的交互接口。一开始考虑过裸机轮询但随着外设ADC、I2C、UART增多状态机写得越来越复杂维护起来很头疼。FreeRTOS当然是个好选择但感觉对于我这个资源有限的Cortex-M0内核芯片STM32F072只有128KB Flash和16KB RAM来说它的内存占用和启动速度还是让我有点犹豫。这时RT-Thread Nano进入了我的视线。它是一个极简版的RT-Thread实时操作系统专门为资源受限的MCU设计内核非常小巧最小配置下RAM能控制在1KB以内Flash占用也很少而且提供了任务调度、信号量、邮箱等核心机制完全能满足我的需求。更重要的是RT-Thread生态里有FinSH组件这是一个可以通过串口进行交互的命令行外壳shell能动态查看线程状态、修改变量值、执行函数对于现场调试和参数配置来说简直是“神器”。于是我决定在STM32F072RBT上尝试移植RT-Thread Nano并把FinSH组件也加进去把整个过程记录下来给有类似需求的朋友一个参考。2. 环境准备与工程骨架搭建移植的第一步是准备好“战场”。我的开发环境是Keil MDK-ARM V5芯片包已经安装好了STM32F0系列。RT-Thread Nano的源码可以从RT-Thread官网的GitHub仓库下载我选择的是3.1.5这个长期支持版本比较稳定。2.1 获取RT-Thread Nano源码包RT-Thread Nano的源码包通常是一个独立的压缩文件里面包含了内核核心文件、移植相关的板级支持包BSP模板以及一些示例。解压后我们主要关注以下几个目录和文件rt-thread/这是核心里面的src目录包含了内核的所有源代码如clock.c时钟管理、thread.c线程管理、ipc.c进程间通信如信号量、互斥锁等。bsp/这里存放了针对不同芯片和开发板的移植模板。对于STM32我们通常参考stm32目录下的模板。components/这里存放了可选组件其中就有我们需要的finsh/。libcpu/这里是与CPU架构相关的移植代码比如上下文切换的汇编实现。对于Cortex-M0我们需要arm/cortex-m0下的文件。我的做法是不在原工程里直接引用这些分散的文件而是将必要的文件复制到我自己的项目目录中这样工程结构更清晰也便于版本管理。我在项目根目录下新建了一个rt-thread文件夹然后把src核心文件、libcpu/arm/cortex-m0下的文件、以及components/finsh下的文件都复制了过来。bsp下的文件我们只参考不直接复制因为我们需要针对STM32F072做定制。2.2 创建基础Keil工程并集成内核首先我用STM32CubeMX生成了一个基于STM32F072RBT的基础工程配置好系统时钟使用内部HSI 8MHz通过PLL倍频到48MHz、SysTick定时器作为RT-Thread的时钟节拍来源、以及一个用于FinSH的串口USART1波特率115200。生成代码时我选择只生成必要的HAL库代码避免工程过于臃肿。在Keil中新建工程后我开始组织文件结构。我在项目管理器里创建了几个组GroupApplication: 存放我的应用代码如main.c。RT-Thread/kernel: 存放从rt-thread/src复制来的所有.c文件。RT-Thread/port: 存放移植层文件。这里需要重点关注几个文件context_iar.s或context_gcc.S这是上下文切换的汇编代码。由于我用KeilARMCC编译器我需要一个对应ARM Compiler 5armcc或6armclang的版本。RT-Thread Nano包中可能没有直接提供但我们可以从其他BSP示例中找一个context_rvds.s来修改或者根据Cortex-M0的汇编指令自己写一个。核心是实现rt_hw_context_switch_to和rt_hw_context_switch这两个函数用于保存和恢复线程的寄存器现场包括PSP、R4-R11等。cpuport.c: 这个文件实现了一些CPU架构相关的函数最重要的是rt_hw_stack_init它用于初始化一个新线程的栈空间并把栈顶指针指向线程入口函数和参数。对于Cortex-M0栈帧的结构初始PSR、PC、LR等寄存器的压栈顺序必须符合ARM架构过程调用标准AAPCS。board.c: 这是板级初始化文件我们需要在这里实现系统时钟初始化rt_hw_board_init和时钟节拍配置SysTick_Handler中断服务函数中调用rt_tick_increase()。把上述文件添加到对应的Keil组里然后开始配置头文件路径。必须包含的路径有rt-thread/include内核头文件、rt-thread我们自己复制文件的根目录方便找到port下的头文件、以及libcpu/arm/cortex-m0。接下来就是最关键的步骤修改配置文件。3. 内核裁剪与关键配置详解RT-Thread Nano的精髓在于可裁剪性。所有的配置都在一个名为rtconfig.h的头文件中完成。我们可以手动创建这个文件也可以从示例中复制一个过来修改。这个文件里的宏定义决定了内核包含哪些功能。3.1 基础内核功能使能首先必须开启内核支持#define RT_USING_NANO // 明确使用Nano版本 #define RT_USING_OVERFLOW_CHECK // 开启栈溢出检查调试必备 #define RT_THREAD_PRIORITY_MAX 8 // 设置最大优先级为8对于小应用足够了 #define RT_TICK_PER_SECOND 100 // 系统时钟节拍频率100Hz即10ms一个tick #define RT_ALIGN_SIZE 4 // 内存对齐字节数ARM Cortex-M通常是4 #define RT_NAME_MAX 8 // 线程名称最大长度 #define RT_USING_HEAP // 启用动态内存堆FinSH和部分API需要这里RT_TICK_PER_SECOND设置为100意味着系统每10ms会产生一次时钟中断。这个值需要权衡太低了调度不灵敏太高了会增加中断开销。对于我的数据采集设备10ms的粒度完全足够。3.2 组件与设备配置接下来我们需要开启FinSH组件和它依赖的串口设备#define RT_USING_FINSH // 启用FinSH组件 #define FINSH_USING_MSH // 启用模块化shell这是主流用法 #define FINSH_THREAD_STACK_SIZE 512 // FinSH线程的栈大小根据命令复杂度调整 #define FINSH_THREAD_PRIORITY 20 // FinSH线程优先级设得比应用线程低一些避免阻塞应用 #define FINSH_USING_HISTORY // 允许使用上下箭头查看历史命令 #define FINSH_USING_SYMTAB // 使用符号表这是FinSH能找到我们自定义命令的关键 #define FINSH_CMD_SIZE 80 // 命令行缓冲区大小 #define RT_USING_DEVICE // 启用设备框架 #define RT_USING_SERIAL // 启用串口设备驱动 #define RT_SERIAL_USING_DMA // 如果使用DMA收发串口数据可以开启我这里先不用注意FINSH_THREAD_PRIORITY我设置为20数字越大优先级越低RT-Thread默认优先级数越小优先级越高。我的数据采集线程优先级设为10这样FinSH就不会抢占采集任务保证实时性。3.3 内存堆配置动态内存堆是FinSH和内核对象动态创建的基础。RT-Thread Nano支持多种内存管理算法对于STM32F072我选择使用小内存管理算法SLAB的变种或者简单的连续内存管理。我们需要在board.c的rt_hw_board_init()函数中初始化堆空间// 在board.c中定义一个数组作为堆空间 #define HEAP_BEGIN (Image$$RW_IRAM1$$ZI$$Limit) // 使用链接脚本定义的未初始化数据段结束地址作为堆起始 #define HEAP_END (0x20004000) // STM32F072RB的RAM结束地址是0x2000400016KB void rt_hw_board_init() { // ... 初始化HAL库、系统时钟等 ... SystemCoreClockUpdate(); // 初始化系统时钟节拍(SysTick) SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND); // 初始化动态内存堆 rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END); // 初始化板级串口设备后文详述 rt_hw_usart_init(); // 打印RT-Thread版本信息可选依赖串口 rt_kprintf(\nRT-Thread Nano v%d.%d.%d\n, RT_VERSION, RT_SUBVERSION, RT_REVISION); }这里Image$$RW_IRAM1$$ZI$$Limit是一个由链接器生成的符号代表已初始化数据和未初始化数据BSS段结束后的地址也就是空闲RAM的起始地址。这样能确保堆空间不会和全局变量冲突。我们需要在Keil的链接器配置里确保这个符号可用通常默认就是可用的。4. 串口驱动与FinSH的“搭桥”工作内核跑起来了堆也有了现在需要让FinSH能通过串口和我们对话。这需要两个步骤实现一个串口设备驱动并将这个设备注册为FinSH的控制台。4.1 实现RT-Thread设备驱动框架下的串口驱动RT-Thread的设备驱动框架定义了一套标准的操作接口struct rt_device_ops。我们需要为USART1实现这些接口。我选择在board.c旁边新建一个drv_usart.c文件。首先定义一个继承自rt_device的结构体包含我们需要的硬件句柄HAL的UART_HandleTypeDef和收发缓冲区#include rtdevice.h #include stm32f0xx_hal.h struct stm32_uart { rt_uint32_t uart_periph; // 外设基地址如USART1 UART_HandleTypeDef huart; // HAL句柄 rt_uint16_t irqno; // 中断号 };然后实现最关键的几个操作函数rt_err_t uart_configure(struct rt_device *dev, struct rt_device_configuration *cfg): 配置串口波特率、数据位等。这里可以直接调用HAL_UART_Init。rt_size_t uart_write(struct rt_device *dev, rt_off_t pos, const void *buffer, rt_size_t size): 发送数据。我们可以使用阻塞式的HAL_UART_Transmit或者为了效率使用中断或DMA。为了简单起见初次调试我用了阻塞发送。rt_size_t uart_read(struct rt_device *dev, rt_off_t pos, void *buffer, rt_size_t size): 读取数据。同样可以使用HAL_UART_Receive或中断方式。这里有个关键点FinSH是通过轮询poll方式读取控制台输入的所以我们的read函数不能一直阻塞等待。一个常见的做法是在串口接收中断服务函数USART1_IRQHandler中将收到的字符存入一个环形缓冲区ring buffer然后在uart_read函数中从这个环形缓冲区里取数据。如果缓冲区为空则返回0表示当前无数据。这样FinSH线程就能非阻塞地检查是否有输入。最后实现一个初始化函数rt_hw_usart_init()在这个函数里初始化HAL的UART句柄配置引脚、波特率等。调用rt_device_register函数将我们实现的这个stm32_uart设备注册到RT-Thread的设备框架中设备类型为RT_Device_Class_Char字符设备并给它起个名字比如uart1。开启串口接收中断。4.2 将串口设备绑定为FinSH控制台设备注册好后我们需要告诉FinSH使用哪个设备作为输入输出。在main.c或board.c的初始化后期在rt_hw_board_init()之后调用以下函数#include finsh.h void rt_console_set_device(const char* device_name);例如rt_console_set_device(uart1);。这样rt_kprintf函数内核打印函数和FinSH的输入输出就都会重定向到uart1这个设备上了。此时编译下载程序如果串口线连接正确上电后应该能看到通过rt_kprintf打印的RT-Thread版本信息。4.3 创建FinSH线程并导出符号表仅仅设置控制台设备还不够FinSH本身需要一个独立的线程来运行。通常我们会在main.c中创建初始线程然后在这个初始线程里调用finsh_system_init()来启动FinSH线程// 定义初始线程的栈和线程句柄 ALIGN(RT_ALIGN_SIZE) static rt_uint8_t main_stack[512]; static struct rt_thread main_thread; // 初始线程入口函数 void main_thread_entry(void* parameter) { // 用户应用初始化... // 例如初始化ADC、启动数据采集线程等 // 初始化并启动FinSH finsh_system_init(); // 此时FinSH线程已经创建并启动在后台等待命令 while (1) { // 主线程可以执行其他任务或者挂起 rt_thread_mdelay(1000); } } int main(void) { // 硬件初始化HAL_Init等已在启动文件完成或需手动调用 // RT-Thread内核初始化 rt_thread_init(main_thread, main, main_thread_entry, RT_NULL, main_stack[0], sizeof(main_stack), 10, // 优先级 20); // 时间片 rt_thread_startup(main_thread); // 启动初始线程 // 启动RT-Thread调度器永不返回 rt_system_scheduler_start(); return 0; }finsh_system_init()函数内部会创建一个名为“tshell”的线程其栈大小和优先级就是我们之前在rtconfig.h里配置的FINSH_THREAD_STACK_SIZE和FINSH_THREAD_PRIORITY。要让FinSH能识别并执行我们自定义的函数比如一个读取ADC值的函数我们需要导出符号表。有两种方式自动导出推荐在要导出的函数定义前加上RT-Thread提供的宏。例如我有一个函数int read_adc_value(void)想通过FinSH调用只需在函数定义处添加#include finsh.h FINSH_FUNCTION_EXPORT(read_adc_value, read adc value);这样FinSH启动时就会自动将这个函数名read_adc_value和帮助信息read adc value加入到命令表中。在串口终端输入read_adc_value回车就能执行这个函数并看到返回值。手动导出在finsh组件初始化前调用finsh_syscall_append函数手动添加。这种方式更灵活但代码稍显繁琐。5. 编译、调试与排坑实录配置和代码都写好了接下来就是激动人心的编译和调试环节。这个过程几乎不可能一帆风顺我遇到了几个典型问题这里分享出来希望能帮你节省时间。5.1 链接错误未定义的汇编符号第一次编译链接器报错Error: L6218E: Undefined symbol rt_hw_context_switch_to。这是因为我们缺少了上下文切换的汇编文件或者文件没添加到工程或者文件里的函数名与内核引用的不对应。解决方案确保context_rvds.s或你使用的汇编文件已添加到Keil的RT-Thread/port组中。打开这个文件检查里面定义的函数名是否与rtdef.h中声明的一致。通常需要以下函数rt_hw_context_switch_to用于启动第一个线程。rt_hw_context_switch用于在线程间切换。rt_hw_context_switch_interrupt用于在中断服务程序中进行线程切换可选但建议实现以支持rt_interrupt_enter/leave。 对于Cortex-M0这些函数需要用汇编实现PSP进程栈指针的保存与恢复以及 PendSV 异常触发机制。5.2 FinSH无响应或打印乱码程序下载后串口有输出版本信息但按回车没反应或者输入字符后显示乱码。可能原因及排查串口配置问题首先用示波器或逻辑分析仪检查USART1的TX引脚看是否有数据发出。如果有但PC端乱码检查波特率、数据位、停止位、校验位是否与终端软件如Putty、SecureCRT设置一致。特别注意RT-Thread的rt_kprintf和FinSH默认使用\n作为换行而有些终端软件需要\r\n。可以在rtconfig.h中定义RT_USING_TERMINAL并实现相关钩子函数或者直接修改finsh组件中关于行结束符的处理。控制台设备未正确设置确认rt_console_set_device(uart1)中的uart1与你注册设备时使用的名字完全一致大小写敏感。可以在调用此函数后加一句rt_kprintf(console set to uart1\n)来验证。FinSH线程未启动或栈溢出在rtconfig.h中打开RT_USING_OVERFLOW_CHECK和线程栈调试信息。然后在线程初始化后在main线程里调用list_thread命令如果FinSH还没起来可以先用rt_kprintf打印线程列表。查看tshell线程的状态是否为running以及栈使用率是否接近100%。如果栈溢出增大FINSH_THREAD_STACK_SIZE。串口接收中断未正确触发FinSH需要接收字符。确保你的drv_usart.c中正确配置并开启了串口接收中断HAL_UART_Receive_IT并且中断服务函数USART1_IRQHandler被正确实现将接收到的数据存入环形缓冲区。可以在中断服务函数里加一个翻转LED的代码测试是否有字符输入中断。5.3 自定义命令无法识别输入read_adc_valueFinSH返回msh: command not found.。排查步骤检查宏定义确认rtconfig.h中FINSH_USING_SYMTAB已定义。检查导出宏确认自定义函数前使用了FINSH_FUNCTION_EXPORT宏并且函数名和帮助字符串没有语法错误。一个常见的坑这个宏会在编译时在特定的段section放置一个数据结构。你需要检查Keil的链接器配置确保包含了这个段。通常RT-Thread的移植模板会提供一个分散加载文件.sct或链接脚本其中定义了FSymTab和VSymTab段的存放位置。如果没有你可能需要手动在链接器选项中添加--keepFSymTab和--keepVSymTab对于ARMCC或者确保这些段不会被优化掉。查看符号表在FinSH中有一个内置命令list_symbol()可以列出所有已导出的命令。输入这个命令看看你的read_adc_value是否在列表中。如果不在说明导出失败。5.4 系统运行不稳定或HardFault加入FinSH后系统偶尔跑飞触发HardFault。可能原因栈空间不足这是最常见的原因。FinSH线程栈、你的应用线程栈、以及中断嵌套都可能消耗栈空间。使用list_thread命令查看各线程栈使用情况适当增加栈大小。特别是中断服务函数中不要使用大的局部变量。堆空间不足FinSH内部可能会动态分配内存如历史命令记录。如果堆空间太小rt_malloc失败可能导致异常。检查HEAP_END - HEAP_BEGIN的大小确保有足够的空闲RAM对于STM32F072留出2-4KB给堆是比较安全的。中断优先级冲突SysTick中断和PendSV中断的优先级需要正确设置。对于Cortex-MSysTick和PendSV通常设置为最低优先级以确保它们不会打断高优先级的中断如你的串口接收中断。在board.c的rt_hw_board_init()中调用HAL_NVIC_SetPriority(SysTick_IRQn, 0xF, 0)来设置SysTick优先级具体值根据NVIC优先级分组而定。PendSV的优先级在上下文切换汇编代码中设置。设备驱动操作非线程安全如果你的串口write或read函数被多个线程调用比如rt_kprintf可能在中断中调用而没有加锁保护可能会造成数据竞争。考虑在设备驱动结构体中增加一个互斥锁rt_mutex_t在操作前后进行加锁解锁。6. 优化与实践心得系统稳定运行后还可以做一些优化让开发体验更好。6.1 优化FinSH输入体验默认的FinSH输入可能不支持退格键Backspace删除、方向键移动光标。这些功能依赖于终端类型VT100的支持。你可以在终端软件中设置为VT100或ANSI模式。同时确保你的串口驱动在收到特殊字符如0x7F代表退格0x1B代表ESC时能正确处理。RT-Thread的FinSH组件通常已经处理了这些但需要终端模拟器配合。6.2 使用MSH模块化Shell命令除了导出C函数RT-Thread的MSH还支持用更自然的方式定义命令类似于Linux shell中的命令风格。你可以使用MSH_CMD_EXPORT宏来定义命令和处理函数。这种方式定义的命令可以带参数解析起来更方便。例如static void my_cmd(int argc, char** argv) { if (argc 2) { rt_kprintf(Usage: my_cmd value\n); return; } int val atoi(argv[1]); rt_kprintf(You input: %d\n, val); } MSH_CMD_EXPORT(my_cmd, this is my command);在FinSH中输入my_cmd 123就会调用这个函数并输出You input: 123。6.3 将FinSH作为强大的调试工具FinSH不仅仅是一个命令执行器结合RT-Thread的内置命令它变成了一个强大的实时调试工具list_thread查看所有线程的状态、优先级、栈大小和使用率、错误代码。这是分析系统负载和排查线程阻塞的利器。list_timer查看系统定时器列表。list_sem/list_mutex/list_mailbox/list_mempool查看各种内核对象的状态对于分析同步问题非常有帮助。free查看当前堆内存的使用情况。你甚至可以自定义命令来读取或修改某个全局变量或者触发一个特定的测试流程极大地方便了现场调试和参数整定。6.4 关于资源占用的实测数据最后分享一下在STM32F072RBT上最终的资源占用情况通过Keil的Map文件查看Flash占用内核 FinSH组件不含浮点数打印等高级功能总计约12KB。RAM占用内核数据区约1KB加上为FinSH线程栈512字节、堆空间2KB以及全局变量总RAM使用约6KB剩余10KB左右供应用使用。这个占用比预想的还要小完全在STM32F072的承受范围内。整个移植过程虽然遇到一些坑但一旦跑通带来的开发效率提升是巨大的。特别是对于需要现场调试和参数调整的设备不用再反复烧录程序通过串口命令行就能完成大部分工作这种感觉非常棒。
返回列表