ARTICLE DETAIL

资讯详情

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

STM32F429移植RT-Thread Nano:从内核移植到FinSH集成的完整实践

STM32F429移植RT-Thread Nano:从内核移植到FinSH集成的完整实践 1. 项目概述为什么要在F429上折腾RT-Thread最近在做一个需要复杂外设管理和网络功能的新项目手头正好有几块STM32F429IIT6的开发板。这芯片资源挺足的Cortex-M4内核带FPU180MHz主频2MB Flash256KB RAM还有SDRAM控制器和LCD-TFT接口拿来跑个带图形界面或者网络协议栈的应用正合适。裸机开发的话状态机、中断管理这些写起来会越来越复杂后期维护也头疼。所以把RT-Thread这个国产的实时操作系统RTOS移植上去就成了一个很自然的选择。RT-Thread这几年在嵌入式圈子里热度挺高它不光是一个内核更像一个组件丰富的物联网操作系统平台。文件系统、网络协议栈LwIP、GUI框架比如LVGL的官方支持、甚至软件包中心类似手机的应用商店这些功能都能通过它的软件包机制很方便地集成进来。对于F429这种级别的MCU跑RT-Thread能充分发挥其硬件潜力让开发效率提升不少。这次移植的目标就是在MDK-ARMKeil MDK开发环境下让RT-Thread Nano版本内核FinSH控制台在STM32F429IIT6上跑起来为后续添加更多组件打好基础。2. 移植前的核心准备理清思路与备齐工具移植操作系统听起来高大上但其实核心工作就是让操作系统的代码能在你的目标芯片上正确编译、链接并且能接管芯片的启动流程和系统时钟。对于RT-Thread Nano到STM32F4的移植我们可以把它分解成几个清晰的步骤。首先你得把必要的“原材料”准备好。2.1 开发环境与源码获取开发环境我选用的是Keil MDK 5.36ARM Compiler 6默认但为了兼容性后续可能切回AC5。选择MDK主要是因为STM32的HAL库和标准外设库对它支持都很好而且国内用的人多资料全。RT-Thread Nano的源码可以从官网 rt-thread.org 下载或者直接从GitHub仓库获取。我更推荐后者因为版本更新也方便查看提交历史。你需要获取的主要是rt-thread仓库里的bsp板级支持包和components组件目录。不过对于Nano移植我们其实只需要内核部分。一个更简单的方法是使用RT-Thread Studio它的IDE创建一个基于STM32F4的Nano工程然后把内核相关文件提取出来。但为了理解整个过程我们这次手动从源码构建。关键的文件包括rt-thread/src/目录下的所有.c文件这是内核的核心实现。rt-thread/include/目录内核的头文件。rt-thread/libcpu/arm/cortex-m4/目录这是与Cortex-M4架构相关的底层代码特别是上下文切换的汇编部分context_gcc.S或context_iar.S我们需要针对MDK的ARMCC编译器准备一份context_rvds.S。rt-thread/components/finsh/目录FinSH组件的源码这是RT-Thread的交互式命令行组件调试神器强烈建议带上。2.2 理解STM32F429的启动流程在裸机程序中启动文件比如startup_stm32f429xx.s会初始化堆栈指针然后跳转到SystemInit函数初始化时钟最后进入main函数。RT-Thread需要接管这个过程。RT-Thread的启动入口通常是$Sub$$main函数这是一个ARM编译器的特殊语法表示在真正的main函数之前执行。在这个函数里它会先调用rtthread_startup()。这个函数依次完成关闭全局中断防止在初始化过程中被中断打断。板级初始化(rt_hw_board_init)这里是我们移植的重中之重。需要初始化系统时钟HAL库的SystemClock_Config、初始化滴答定时器SysTick为RT-Thread提供系统时钟节拍、初始化串口用于FinSH输出、以及初始化内存堆。打印RT-Thread版本Logo表明系统启动。初始化系统定时器创建系统定时器线程。初始化调度器(rt_system_scheduler_init)。初始化应用线程比如创建一个LED闪烁的示例线程。初始化定时器线程。初始化空闲线程优先级最低的线程当系统无事可做时运行。启动调度器(rt_system_scheduler_start)从这里开始RT-Thread正式接管CPU的控制权多任务调度开始运行。注意这个函数正常情况下不会返回。永远不会执行到的代码在rt_system_scheduler_start之后理论上还会调用rt_application_init等但实际上调度器启动后程序流就由调度器控制了。所以我们的移植工作核心就是正确实现rt_hw_board_init这个函数并准备好正确的上下文切换汇编代码。3. 手把手移植从零搭建MDK工程理论清楚了我们开始动手。假设你已经有一个能正常编译、运行的STM32F429裸机工程至少串口和SysTick能工作。我们将在这个工程的基础上集成RT-Thread Nano。3.1 工程文件结构与添加首先在MDK工程中创建清晰的文件夹结构。我在项目根目录下新建了一个rtthread文件夹里面包含以下子目录rtthread/src/存放从RT-Thread源码中拷贝来的内核.c文件。rtthread/include/存放内核头文件。rtthread/libcpu/arm/cortex-m4/存放架构相关文件。rtthread/bsp/存放我们板级相关的移植文件主要是board.c和rtconfig.h。然后在MDK的Project窗口中创建对应的Groups分组并将文件添加进去。例如创建RT-Thread/kernel分组添加src目录下的clock.c,device.c,idle.c,ipc.c包含信号量、互斥锁、事件、邮箱等,mem.c,mempool.c,object.c,scheduler.c,thread.c,timer.c等核心文件。创建RT-Thread/arch分组添加libcpu目录下的文件。创建RT-Thread/bsp分组添加我们自己写的board.c。注意components/finsh下的文件也需要添加包括finsh_port.c需要我们自己实现或修改和shell.c,msh.c等。为了简化第一步我们可以先不添加FinSH等内核跑通后再集成。3.2 核心配置文件 rtconfig.h 的定制rtconfig.h是RT-Thread的“总开关”所有功能的宏定义都在这里。我们可以从任意一个STM32F4的BSP示例中拷贝一份过来修改。关键配置如下#ifndef RT_CONFIG_H__ #define RT_CONFIG_H__ /* 自动生成不要修改 */ #define RT_USING_NEWLIB /* 内核基础配置 */ #define RT_NAME_MAX 8 // 线程名最大长度 #define RT_ALIGN_SIZE 4 // 对齐字节数 #define RT_THREAD_PRIORITY_MAX 32 // 最大优先级默认32足够 #define RT_TICK_PER_SECOND 1000 // 系统时钟节拍1000表示1ms一个tick #define RT_USING_OVERFLOW_CHECK // 开启栈溢出检查 #define RT_USING_HOOK // 开启钩子函数 #define RT_USING_IDLE_HOOK // 空闲任务钩子 #define IDLE_THREAD_STACK_SIZE 256 // 空闲任务栈大小 /* 内核调试配置 */ #define RT_DEBUG #define RT_DEBUG_COLOR #define RT_DEBUG_INIT_CONFIG #define RT_DEBUG_INIT 1 // 打印组件初始化信息 #define RT_DEBUG_THREAD_CONFIG #define RT_DEBUG_THREAD 1 // 打印线程切换信息初期调试很有用 /* 组件配置 */ #define RT_USING_COMPONENTS_INIT // 使用组件自动初始化 #define RT_USING_USER_MAIN // 启用用户main入口 #define RT_MAIN_THREAD_STACK_SIZE 2048 // 主线程栈大小即main线程 #define RT_MAIN_THREAD_PRIORITY 10 // 主线程优先级 /* FinSH配置如果启用 */ // #define RT_USING_FINSH // #define FINSH_THREAD_NAME tshell // #define FINSH_USING_HISTORY // #define FINSH_USING_SYMTAB // #define FINSH_USING_DESCRIPTION // #define FINSH_THREAD_PRIORITY 20 // #define FINSH_THREAD_STACK_SIZE 4096 // #define FINSH_USING_MSH // #define FINSH_USING_MSH_DEFAULT // #define FINSH_ARG_MAX 10 /* 设备驱动框架 */ #define RT_USING_DEVICE // #define RT_USING_DEVICE_IPC // #define RT_USING_SERIAL // 串口设备用FinSH时必须开启 /* 内存管理 */ #define RT_USING_HEAP /* 使用动态内存堆对于STM32F429我们使用其内部的SRAM */ #define RT_USING_MEMHEAP /* 可以启用多内存堆如果后续用到了外部SDRAM会很有用 */ // #define RT_USING_MEMHEAP_AS_HEAP // #define RT_USING_MEMHEAP_AUTO_BINDING #define RT_USING_SMALL_MEM // 使用小内存管理算法 #endif /* RT_CONFIG_H__ */这个配置文件定义了系统的基本行为。比如RT_TICK_PER_SECOND设置为1000意味着系统每秒有1000个时钟节拍每个节拍1毫秒。这决定了rt_thread_delay(1000)会让线程延时1秒。调试相关的宏在初期建议打开方便通过串口观察系统启动和线程切换日志。3.3 板级支持文件 board.c 的实现board.c是移植的核心它包含了rt_hw_board_init()函数。我们需要在这里完成硬件的早期初始化。#include rtthread.h #include rthw.h // 包含关中断等硬件相关函数 #include board.h // 你的板级头文件通常包含HAL库引用和引脚定义 #include drv_usart.h // 串口驱动头文件如果需要 extern void SystemClock_Config(void); // 你的时钟配置函数 /** * 这是RT-Thread的硬件板级初始化函数 */ void rt_hw_board_init(void) { /* 1. 关闭全局中断 */ rt_hw_interrupt_disable(); /* 2. 初始化系统时钟HAL库版本*/ SystemClock_Config(); // 确保你的180MHz时钟配置正确 /* 3. 初始化SysTick为RT-Thread提供系统时钟节拍 */ // SysTick_Config函数由CMSIS提供计算方法是SystemCoreClock / RT_TICK_PER_SECOND // 对于180MHz和1000Hz重装载值应为 180000000 / 1000 - 1 179999 // 但通常我们直接调用库函数它内部会计算。 SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND); /* 4. 初始化硬件串口用于调试输出*/ // 假设你使用USART1并已经实现了drv_usart.c中的初始化函数 rt_hw_usart_init(); // 这个函数需要你自己实现配置好GPIO和USART /* 5. 初始化控制台设备将控制台绑定到串口1*/ // 如果启用了RT_USING_CONSOLE需要调用rt_console_set_device #ifdef RT_USING_CONSOLE rt_console_set_device(RT_CONSOLE_DEVICE_NAME); // 例如 uart1 #endif /* 6. 打印板载信息 */ rt_kprintf(\r\n--- RT-Thread Startup on STM32F429IIT6 ---\r\n); rt_kprintf(System Clock: %d Hz\r\n, SystemCoreClock); rt_kprintf(Tick Frequency: %d Hz\r\n, RT_TICK_PER_SECOND); /* 7. 初始化内存堆 */ // rt_system_heap_init 用于初始化内存堆的起始和结束地址 // 对于STM32F429我们使用内部SRAM。需要知道SRAM的起始地址和大小。 // 起始地址0x20000000 // 大小256KB (0x40000)。但注意栈从顶部向下生长堆从底部向上生长。 // 我们需要留出部分空间给系统栈和主线程栈。这里简单划分 // 堆起始0x20000000 栈预留大小例如0x1000 // 堆结束0x20040000 - 1 extern int __heap_base; // 这些符号通常由链接脚本定义更规范的做法是使用链接脚本变量 extern int __heap_limit; rt_system_heap_init((void*)__heap_base, (void*)__heap_limit); /* 8. 初始化板载外设如LED GPIO*/ // 可以在这里初始化LED、按键等GPIO方便测试 // BSP_LED_Init(); /* 9. 初始化组件如果启用了RT_USING_COMPONENTS_INIT*/ #ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); // 初始化板级组件 rt_components_init(); // 初始化所有已启用的组件如FinSH #endif }这里有几个关键点SysTick初始化SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND)是标准做法。确保你的SystemCoreClock全局变量在SystemClock_Config()后被正确设置为180000000。内存堆初始化这是最容易出问题的地方。上面的例子使用了链接脚本中定义的符号这是最规范的方式。你需要修改MDK的链接脚本.sct文件明确定义堆Heap和栈Stack的区域。对于STM32F429典型的内部RAM分配如下LR_IROM1 0x08000000 0x00200000 { ; 加载区域Flash ER_IROM1 0x08000000 0x00200000 { ; 执行区域Flash *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00040000 { ; 执行区域RAM .ANY (RW ZI) } }我们需要在这个RW_IRAM1区域中划分出堆和栈。一种常见的做法是在链接脚本中定义两个符号RW_IRAM1 0x20000000 0x00040000 { .ANY (RW ZI) *(.heap) ; 可选显式定义堆段 }然后在board.c中声明extern unsigned char Image$$RW_IRAM1$$ZI$$Limit[]; // ZI段的结束地址 #define HEAP_BEGIN ((void*)Image$$RW_IRAM1$$ZI$$Limit) #define HEAP_END ((void*)(0x20000000 0x40000)) rt_system_heap_init(HEAP_BEGIN, HEAP_END);这样堆区就从未初始化的全局变量ZI段结束的地方开始到RAM的末尾结束。栈则由启动文件设置通常位于RAM顶部。串口初始化rt_hw_usart_init()需要你自己实现基于HAL库或标准库。更重要的是你需要实现rt_hw_console_output函数让rt_kprintf能通过串口输出字符。这个函数通常放在drv_usart.c里。3.4 上下文切换汇编文件 context_rvds.SRT-Thread的线程切换依赖于底层汇编代码。对于Cortex-M4和MDKARMCC/ARMClang我们需要一份名为context_rvds.S的文件。可以从RT-Thread源码的libcpu/arm/cortex-m4目录下找到context_gcc.S给GCC用的作为参考但语法完全不同。你需要根据ARM汇编器ARMASM的语法重写。核心是PendSV_Handler异常处理函数它负责实际的上下文保存与恢复。这里涉及保存R4-R11寄存器到当前线程的栈然后从下一个线程的栈中恢复这些寄存器。由于代码较长且高度依赖汇编语法一个可行的方案是从RT-Thread官方为其他Cortex-M芯片提供的MDK BSP中比如STM32F103拷贝一份context_rvds.S它通常是通用的只需要确认CPU架构是cortex-m4即可。将其放入libcpu/arm/cortex-m4/目录并添加到MDK工程的RT-Thread/arch分组中。3.5 修改启动文件与链接脚本启动文件startup_stm32f429xx.s需要做一处关键修改将PendSV_Handler和SysTick_Handler的弱定义WEAK注释掉或者确保它们不会被链接。因为RT-Thread在context_rvds.S和clock.c中已经提供了这两个中断服务例程ISR的强实现。如果启动文件中的弱定义被链接就会导致RT-Thread的版本不被调用。更安全的做法是在启动文件中找到这两行.weak PendSV_Handler .weak SysTick_Handler以及它们对应的跳转表条目但通常不需要删除。只要RT-Thread提供了强符号链接器就会优先使用强符号。为了保险你可以将RT-Thread相关目标文件的链接顺序放在启动文件之前。链接脚本.sct文件如前所述需要确保堆栈空间划分合理。对于256KB RAM可以给栈预留8KB0x2000堆区从ZI段结束开始。在MDK的Options for Target - Linker选项卡中可以取消勾选Use Memory Layout from Target Dialog然后编辑生成的.sct文件。4. 编译、调试与第一个线程的创建完成以上文件添加和配置后就可以尝试编译了。第一次编译肯定会遇到大量错误主要是头文件路径问题和函数未定义。4.1 解决编译错误头文件路径在MDK的Options for Target - C/C - Include Paths中添加RT-Thread头文件路径./rtthread/include以及你的板级支持路径./rtthread/bsp。未定义符号最常见的错误是rt_hw_console_output,rt_hw_usart_init等函数未定义。你需要在drv_usart.c中实现它们。rt_hw_console_output这个函数通常以一个简单的轮询方式发送字符串。void rt_hw_console_output(const char *str) { rt_size_t i 0, size 0; char a \r; size rt_strlen(str); for (i 0; i size; i) { if (*(str i) \n) { while(__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET); HAL_UART_Transmit(huart1, (uint8_t *)a, 1, 1000); } while(__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET); HAL_UART_Transmit(huart1, (uint8_t *)(str i), 1, 1000); } }rt_hw_usart_init初始化串口硬件配置引脚和中断如果使用中断接收。链接错误如果出现$Super$$main或$Sub$$main相关的链接错误说明RT-Thread的components.c文件没有正确参与编译。确保它被添加到工程中并且RT_USING_USER_MAIN宏已定义。这个文件提供了$Sub$$main的实现。4.2 创建第一个用户线程并测试当编译通过后我们修改main.c不再写裸机的while循环而是创建RT-Thread的线程。#include main.h #include rtthread.h #include rtdevice.h /* 定义线程栈 */ ALIGN(RT_ALIGN_SIZE) // 栈对齐 static rt_uint8_t led_thread_stack[512]; // LED线程栈 static rt_uint8_t main_thread_stack[2048]; // 主线程栈 /* 定义线程控制块 */ static struct rt_thread led_thread; static struct rt_thread main_thread; /* LED线程入口函数 */ static void led_thread_entry(void *parameter) { rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT); // 假设已定义LED_PIN while (1) { rt_pin_write(LED_PIN, PIN_HIGH); rt_thread_delay(500); // 延时500个tick即500ms rt_pin_write(LED_PIN, PIN_LOW); rt_thread_delay(500); } } /* 主线程入口函数传统的main函数*/ int main(void) { rt_err_t result; /* 初始化LED线程 */ result rt_thread_init(led_thread, led, led_thread_entry, RT_NULL, led_thread_stack[0], sizeof(led_thread_stack), 10, // 优先级数值越小优先级越高 20); // 时间片 if (result RT_EOK) { rt_thread_startup(led_thread); // 启动线程 } /* 主线程本身也是一个线程可以在这里做其他初始化 */ while (1) { rt_kprintf(Main thread is running...\r\n); rt_thread_delay(1000); // 每秒打印一次 } // return 0; // 实际上不会执行到这里 } /* 将main线程也纳入调度可选更规范的做法*/ int rt_application_init(void) { rt_thread_t tid; tid rt_thread_create(main, main_thread_entry, // 可以是一个新函数 RT_NULL, 2048, 8, 20); if (tid ! RT_NULL) rt_thread_startup(tid); return 0; }注意上面的main函数实际上是在$Sub$$main之后被调用的。在rtthread_startup()中会调用rt_application_init。一种更清晰的做法是在main函数里只做最必要的硬件初始化如果board.c没做完的然后直接返回。将所有的线程创建放在rt_application_init函数中。这样代码结构更符合RT-Thread的规范。4.3 调试与常见问题排查下载程序后打开串口助手如115200, 8N1你应该能看到RT-Thread的启动Logo和版本信息以及“Main thread is running...”的打印。LED灯应该以1Hz的频率闪烁。如果没现象按以下步骤排查没有输出检查串口引脚配置和波特率。检查rt_hw_console_output函数是否被正确调用。可以在rt_hw_board_init里的rt_kprintf前后加一个简单的引脚翻转用示波器或逻辑分析仪看确认程序执行到了这里。检查RT_DEBUG相关宏是否打开初期建议全部打开。程序卡死或进入HardFault栈溢出这是最常见的原因。检查每个线程的栈大小是否足够。特别是rt_application_init或main线程的栈。可以适当调大比如从1024调到2048。堆空间不足在rt_hw_board_init中打印出HEAP_BEGIN和HEAP_END的地址确认堆空间有效且足够大。中断优先级冲突RT-Thread将PendSV和SysTick的中断优先级设置为最低0xFF以确保上下文切换不会打断关键中断。检查你的其他中断特别是USB、以太网优先级是否设置正确避免在中断服务程序中调用可能导致调度的RT-Thread函数如rt_mutex_take 但rt_sem_release通常是安全的。时钟配置错误确保SystemCoreClock全局变量的值与实际系统时钟一致。SysTick是基于这个值计算的如果不对rt_thread_delay的延时就不准。线程不切换确认SysTick中断能正常触发。可以在SysTick_Handler在clock.c中里加一个引脚翻转来测试。确认PendSV_Handler在context_rvds.S中被正确实现和链接。MDK调试时可以单步跟踪进入中断看是否能执行到PendSV。5. 进阶集成FinSH组件与内存管理优化内核跑起来只是第一步。接下来我们把FinSH组件加进来这样就能通过串口命令行动态查看线程状态、内存使用情况甚至动态创建/删除线程调试起来会方便得多。5.1 集成FinSH命令行组件首先在rtconfig.h中打开FinSH相关的宏定义取消之前那部分的注释。然后将components/finsh目录下的源文件shell.c,msh.c,cmd.c等添加到MDK工程中。关键是要实现一个finsh_port.c文件通常放在BSP目录下。finsh_port.c主要做两件事实现字符输入函数char rt_hw_console_getchar(void)。这个函数应该以阻塞或非阻塞方式从串口读取一个字符。对于简单的轮询方式可以检查串口接收标志位。char rt_hw_console_getchar(void) { int ch -1; if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_RXNE) ! RESET) { ch huart1.Instance-DR 0xff; } return ch; }更推荐使用中断方式。你需要开启串口接收中断在中断服务程序中将字符放入一个环形缓冲区ringbuffer然后rt_hw_console_getchar从这个缓冲区读取。RT-Thread的drv_usart.c通常已经实现了这套机制。初始化FinSH在rt_hw_board_init函数的末尾调用finsh_system_init()。如果使用了组件自动初始化并且finsh_port.c中正确声明了INIT_APP_EXPORT或INIT_DEVICE_EXPORT这一步可以自动完成。编译下载后重启开发板在串口终端按回车应该能看到msh 提示符。输入help可以查看命令列表ps可以查看当前所有线程的状态名称、优先级、栈使用率等free可以查看内存堆的使用情况。5.2 内存堆管理与优化STM32F429有256KB的RAM对于很多应用来说足够了。但如果你后续要加载文件系统、网络协议栈或者GUI内存管理就很重要。RT-Thread的小内存管理算法RT_USING_SMALL_MEM在资源紧张时效率很高但它不支持内存碎片的整理。对于F429一个更优的方案是使用多内存堆。比如将内部SRAM的前192KB作为主堆RT_USING_MEMHEAP用于分配小对象和线程栈将剩余的64KB或者连接的外部SDRAM比如32MB作为另一个内存堆专门用于分配大块内存如图形缓冲区、网络数据包。这需要在board.c中初始化多个堆/* 定义内部SRAM堆 */ static struct rt_memheap internal_sram_heap; /* 定义外部SDRAM堆 */ static struct rt_memheap external_sdram_heap; void rt_hw_board_init(void) { // ... 其他初始化 /* 初始化内部SRAM堆 */ rt_memheap_init(internal_sram_heap, internal, (void*)0x20000000, 0x30000); // 192KB /* 初始化外部SDRAM堆假设已初始化好SDRAM控制器*/ rt_memheap_init(external_sdram_heap, external, (void*)0xD0000000, // SDRAM起始地址 0x2000000); // 32MB /* 设置内部堆为系统默认堆 */ rt_system_heap_init((void*)0x20000000, (void*)(0x200000000x30000)); }然后在应用程序中你可以通过rt_memheap_alloc从指定的堆分配内存或者使用rt_malloc和rt_free它们会从默认堆分配。通过memheap命令可以在FinSH中查看各个堆的使用情况。5.3 系统时钟节拍与低功耗考量RT_TICK_PER_SECOND设置为1000意味着每秒有1000次SysTick中断。这对于需要高精度延时的应用是必要的但也会带来一定的功耗和CPU开销。对于一些电池供电的、对实时性要求不极端高的应用可以考虑降低这个值比如设置为10010ms一个tick。这能显著减少中断次数降低功耗。但要注意所有基于rt_thread_delay的延时精度都会变成10ms。RT-Thread也支持Tickless模式RT_USING_TICKLESS_LOW_POWER当系统空闲时可以动态停止SysTick进入深度睡眠并在下一个定时事件到来时唤醒。这需要底层驱动支持精确的定时唤醒如RTC或低功耗定时器。对于STM32F4可以利用其LP定时器实现。启用Tickless能极大降低空闲时的功耗是低功耗应用的关键。6. 从Nano到标准版的扩展思考这次我们移植的是Nano版本它只包含了内核和FinSH。RT-Thread的强大之处在于其丰富的软件包和组件。在Nano稳定运行的基础上你可以像搭积木一样添加更多功能设备驱动框架启用RT_USING_DEVICE和RT_USING_SERIAL后串口操作将从直接调用HAL库变为使用rt_device_find,rt_device_open,rt_device_write等统一接口这为后续动态加载设备驱动打下了基础。文件系统添加DFS设备虚拟文件系统组件然后可以挂载ELM FatFs到SD卡或SPI Flash或者使用专为嵌入式设计的LittleFS后者对掉电保护更友好。网络协议栈集成LwIP软件包。STM32F429自带以太网MAC控制器配合PHY芯片如LAN8720可以实现有线网络连接。你需要编写以太网驱动通常实现eth_device接口并处理好网络中断和内存管理使用PBUF内存池。图形界面集成LVGL软件包。STM32F429有LCD控制器和足够的RAM尤其是如果有外部SDRAM可以流畅运行LVGL。你需要实现lv_port_disp显示和lv_port_indev输入设备如触摸屏的移植层。软件包中心使用Env工具或RT-Thread Studio可以通过菜单配置轻松添加和管理这些软件包scons或MDK工程会自动更新。移植过程中最深的体会是理解内存布局和中断管理是稳定性的基石。一开始我因为栈空间分配太小线程一切换就HardFault排查了很久。后来养成了习惯在rtconfig.h中把RT_USING_OVERFLOW_CHECK打开并在FinSH里经常用ps命令查看线程栈的使用率防患于未然。另一个坑是中断优先级特别是用了DMA和高速外设时一定要理清哪些中断不能被上下文切换打断合理设置PendSV和SysTick为最低优先级。最后RT-Thread的社区和文档是国内嵌入式RTOS里相当活跃和丰富的。遇到问题在论坛搜索或提问通常都能找到答案。把F429这个性能不错的平台和RT-Thread这个生态丰富的系统结合起来能玩的东西就很多了从简单的多任务调度到复杂的物联网网关都有了扎实的基础。
返回列表