ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RT-Thread内核移植实战:在iCore3双核心板(STM32F407)上构建多线程应用

RT-Thread内核移植实战:在iCore3双核心板(STM32F407)上构建多线程应用 1. 项目背景与核心价值最近在折腾一块挺有意思的板子——银杏科技的iCore3双核心板。这板子有意思的地方在于它把一颗STM32F407和一颗Altera的Cyclone IV FPGA给“捏”在了一起形成了一个ARMFPGA的异构计算平台。硬件底子很扎实但官方给的例程大多是裸机或者简单的RTOS应用总感觉没把这套硬件的潜力完全发挥出来。正好手头有个项目需要更复杂的任务调度和实时性保障裸机那套前后台轮询的架构已经有点力不从心了于是决定把RT-Thread这个国产的硬实时操作系统给移植上去。为什么是RT-Thread首先它开源免费社区活跃文档和资源在国内算是相当丰富的遇到问题好解决。其次它的内核非常精巧实时性有保障并且提供了丰富的组件比如文件系统、网络协议栈、设备框架等这对于未来想在iCore3上跑更复杂的应用比如通过以太网或者某种高速接口与FPGA交互数据来说是个很好的基础。最后也是想通过这个移植过程彻底摸清从零开始在一个新硬件平台上搭建RTOS的完整链路这比单纯调用API要有意思得多。这次移植的核心目标就是让RT-Thread的内核在iCore3的ARM核心STM32F4上先跑起来。而内核跑起来最直观的标志就是能创建并调度多个线程。所以这篇内容就聚焦在“内核移植”和“线程创建”这两个最基础、也最关键的点上。我会把从搭建环境、修改底层驱动、配置内核到最终创建出两个交替运行的线程的整个过程包括中间踩过的坑和验证方法都详细记录下来。如果你手头也有类似的ARM Cortex-M4内核芯片或者对RTOS的底层移植感兴趣那这篇内容应该能给你提供一个清晰的参考路径。2. 环境搭建与工程骨架创建移植的第一步是准备好“施工场地”。我们需要一个基础的、能在iCore3上点灯的裸机工程作为起点然后在这个基础上引入RT-Thread的内核源码。2.1 获取基础工程与RT-Thread源码银杏科技通常会提供iCore3的裸机例程包里面包含了基本的驱动库可能是标准库也可能是HAL库、链接脚本、启动文件等。我使用的是基于STM32CubeMX生成的HAL库工程因为它对STM32F4系列的外设抽象做得比较好后续维护起来方便。这个基础工程必须确保能正常编译、下载并且能让一个LED闪烁这证明了你的工具链编译器、下载器和硬件连接是没问题的。接下来是RT-Thread源码。我直接从GitHub的RT-Thread官方仓库拉取了最新稳定版比如4.1.x版本。这里有个关键点我们只需要内核部分。RT-Thread的源码结构很清晰主要关注以下几个目录rt-thread/src/: 内核核心源码包括线程调度、时钟管理、IPC等。rt-thread/include/: 内核头文件。rt-thread/libcpu/arm/: ARM架构相关的底层代码特别是上下文切换的汇编实现如context_gcc.S或context_iar.S取决于你的编译器。rt-thread/components/finsh/: 可选组件一个强大的命令行shell用于调试非常方便建议一并移植。我的做法是在本地新建一个项目目录例如icore3_rtt然后将RT-Thread的src,include,libcpu/arm选择cortex-m4目录和components/finsh复制到项目目录下的rt-thread文件夹中。同时把iCore3的裸机工程文件Src,Inc,Drivers等也拷贝进来。这样一个融合的工程骨架就初步形成了。2.2 配置编译环境与头文件路径我使用的开发环境是Keil MDKARMCC 或 AC6编译器。在Keil中新建一个工程或者打开你拷贝过来的基础工程然后开始添加源文件和头文件路径。添加RT-Thread内核源文件组在Keil的Project窗口新建几个Groups例如“RT-Thread Kernel”, “RT-Thread CPU Port”, “RT-Thread Finsh”。然后将rt-thread/src下的所有.c文件添加到Kernel组将rt-thread/libcpu/arm/cortex-m4下的.c和对应的汇编文件如context_armcc.S添加到CPU Port组将finsh组件下的.c文件添加到Finsh组。添加头文件路径这是关键一步编译器必须能找到RT-Thread的头文件。在Options for Target - C/C - Include Paths中添加以下路径.\rt-thread\include.\rt-thread\libcpu\arm\cortex-m4.\rt-thread\components\finsh同时也要包含你原有工程的头文件路径如.\Inc,.\Drivers\STM32F4xx_HAL_Driver\Inc等。预处理器宏定义同样在C/C选项卡在Define框中添加必要的宏。对于RT-Thread最核心的是RT_USING_NANO如果你只想要纳米内核或者直接使用标准版。对于STM32F4通常需要定义芯片型号如STM32F407xx。此外还需要告诉RT-Thread你使用的编译器例如RT_USING_ARMCC。注意直接编译肯定会报一大堆错这很正常。因为还没配置RT-Thread的板级支持包BSP和rtconfig.h配置文件。我们下一步就是解决这些错误。3. 板级支持包BSP适配与内核裁剪RT-Thread通过一个名为rtconfig.h的配置文件来裁剪内核功能、设置系统参数。同时它需要一个板级支持包来提供芯片特定的初始化代码特别是系统时钟和滴答定时器SysTick的配置。3.1 创建与配置rtconfig.h最简单的方法是从RT-Thread源码的bsp目录下找一个STM32F4系列的BSP比如bsp/stm32/stm32f407-atk-explorer里面的rtconfig.h拷贝过来然后基于我们的iCore3进行修改。如果没有也可以从一个最简模板开始。以下是一些最核心的配置项我放在项目根目录下的rtconfig.h文件中#ifndef _RT_CONFIG_H_ #define _RT_CONFIG_H_ /* 指定芯片和编译器 */ #define STM32F407xx #define RT_USING_ARMCC /* 内核基础配置 */ #define RT_NAME_MAX 8 // 线程名最大长度 #define RT_ALIGN_SIZE 4 // 对齐字节数ARM通常为4 #define RT_THREAD_PRIORITY_MAX 32 // 最大优先级数32足够 #define RT_TICK_PER_SECOND 1000 // 系统时钟频率1000Hz即1ms一个tick #define RT_USING_OVERFLOW_CHECK // 启用栈溢出检查调试利器 #define RT_USING_HOOK // 启用钩子函数 #define RT_USING_IDLE_HOOK // 空闲线程钩子可用于测量CPU使用率 #define RT_IDLE_HOOK_LIST_SIZE 4 /* 线程间通信组件 */ #define RT_USING_SEMAPHORE // 信号量 #define RT_USING_MUTEX // 互斥锁 // #define RT_USING_EVENT // 事件集按需开启 // #define RT_USING_MAILBOX // 邮箱按需开启 // #define RT_USING_MESSAGEQUEUE // 消息队列按需开启 /* 内存管理 */ #define RT_USING_MEMPOOL // 内存池 #define RT_USING_MEMHEAP // 内存堆 #define RT_USING_HEAP // 使用动态堆内存 #define RT_USING_SMALL_MEM // 使用小内存管理算法适合资源受限的MCU /* 控制台与Finsh Shell */ #define RT_USING_CONSOLE #define RT_CONSOLE_DEVICE_NAME uart1 // 指定控制台使用的串口设备名后续驱动对接 #define RT_USING_FINSH // 启用Finsh组件 #define FINSH_THREAD_NAME tshell // Finsh线程名 #define FINSH_USING_HISTORY // 命令历史 #define FINSH_USING_SYMTAB // 符号表 #define FINSH_USING_DESCRIPTION // 命令描述 /* 设备驱动框架 */ #define RT_USING_DEVICE // #define RT_USING_SERIAL // 串口设备驱动需要控制台时必须开启 // #define RT_USING_PIN // GPIO设备驱动后续点灯有用 #endif /* _RT_CONFIG_H_ */这个配置文件定义了内核的基本行为系统时钟1ms一次支持动态内存分配启用了控制台和Finsh用于调试并选择了一些基础的IPC机制。RT_CONSOLE_DEVICE_NAME这里先预设为uart1我们后面需要实现串口1的驱动并注册到RT-Thread的设备框架中。3.2 实现板级初始化board.c这是BSP的核心文件需要我们自己创建。主要完成三件事系统时钟初始化RT-Thread内核需要知道系统时钟频率SystemCoreClock。我们可以直接调用在main.c之前已经初始化好的HAL库时钟配置函数通常是SystemClock_Config()确保SystemCoreClock变量被正确赋值例如168MHz。SysTick定时器配置RT-Thread的心跳。我们需要实现SysTick_Handler中断服务程序并在其中调用RT-Thread的rt_tick_increase()函数。同时需要实现一个弱定义的rt_hw_board_init()函数它会在RT-Thread启动时最早被调用在这里完成引脚、时钟、以及硬件定时器的初始化。这里有个关键细节RT-Thread的时钟节拍Tick源不一定非得是SysTick。对于STM32我们可以用SysTick也可以用其他通用定时器。但SysTick最简单因为它就是为操作系统设计的。在rt_hw_board_init()中我们需要配置SysTick的中断频率为RT_TICK_PER_SECOND即1000Hz。// board.c 中的关键函数 void rt_hw_board_init() { /* 初始化HAL库时钟已经在启动阶段配置好 */ HAL_Init(); /* 配置SysTick使其每1ms产生一次中断 */ SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND); /* 初始化硬件串口1作为控制台 */ rt_hw_usart_init(); // 这个函数需要自己实现配置USART1的GPIO和参数 /* 初始化系统堆内存 */ rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END); // HEAP_BEGIN/END在链接脚本中定义 /* 初始化板载LED引脚可选用于调试 */ rt_hw_led_init(); /* 打印RT-Thread版本信息 */ rt_console_set_device(RT_CONSOLE_DEVICE_NAME); // 关联控制台设备 rt_kprintf(\n\nHello, iCore3 with RT-Thread!\n); rt_kprintf(SystemCoreClock: %d Hz\n, SystemCoreClock); /* 调用RT-Thread组件初始化 */#ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); #endif }/* SysTick中断服务函数 */ void SysTick_Handler(void) { /* 进入中断 */ rt_interrupt_enter(); /* 增加系统时钟 */ rt_tick_increase(); /* 离开中断 */ rt_interrupt_leave(); } 实现rt_hw_console_getchar()和rt_hw_console_putchar()这是为了支持Finsh的输入输出。我们需要用串口的收发函数来实现这两个函数。例如rt_hw_console_putchar可以直接调用HAL_UART_Transmit而rt_hw_console_getchar可以调用HAL_UART_Receive或者基于中断和环形缓冲区实现一个非阻塞的版本这样更好。做完这一步编译错误会少很多但依然会有链接错误因为堆内存的地址HEAP_BEGIN,HEAP_END和中断向量表还没处理。4. 链接脚本与启动文件修改这是让RT-Thread在芯片上正确运行的关键底层适配。4.1 修改链接脚本.sct或.ld文件我们需要告诉链接器两件事第一为RT-Thread的动态内存堆heap划分一块区域第二正确处理RT-Thread用到的一些新段section。以Keil的分散加载文件.sct为例我们需要在RW数据段后面预留出一块空间作为堆。首先你要知道你芯片的RAM大小和起始地址。STM32F407有192KB的RAM地址从0x20000000开始。在.sct文件中找到定义RW数据通常是RW_IRAM1的地方。我们需要修改它让RW数据不占用全部RAM而是留出一部分。一种常见的做法是LR_IROM1 0x08000000 0x00100000 { ; 加载区域Flash ER_IROM1 0x08000000 0x00100000 { ; 执行区域代码 *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00020000 { ; 执行区域数据但限制大小 .ANY (RW ZI) } ; 新增一个执行区域专门作为堆 RW_IRAM2 0x2001C000 0x00004000 { ; 从0x2001C000开始分配16KB作为堆 * (HEAP) ; 将所有HEAP段放在这里 } }上面这个例子中RW_IRAM1只分配了128KB0x20000给静态和全局变量而从0x2001C000开始的16KB0x4000区域被命名为RW_IRAM2并指定存放HEAP段。然后在board.c中我们就可以这样定义#define HEAP_BEGIN (void*)Image$$RW_IRAM2$$Base // Keil特定的符号表示RW_IRAM2的起始 #define HEAP_END (void*)Image$$RW_IRAM2$$Limit // Keil特定的符号表示RW_IRAM2的结束Image$$RW_IRAM2$$Base和Image$$RW_IRAM2$$Limit是由链接器自动生成的符号对应我们定义的RW_IRAM2区域的首尾地址。这样rt_system_heap_init就知道从哪里开始管理堆内存了。注意堆的大小需要根据你的实际应用来调整。如果后续要使用网络、文件系统等大内存组件需要预留更多。4.2 修改启动文件startup_stm32f407xx.s启动文件主要处理中断向量表。RT-Thread接管了SysTick和PendSV用于上下文切换中断。因此我们需要确保启动文件中的中断向量表指向RT-Thread提供的处理函数而不是默认的弱定义空函数。在汇编启动文件中找到SysTick_Handler和PendSV_Handler这两行。通常它们被声明为Weak并跳转到一个默认的无限循环(B .)。我们需要将它们指向RT-Thread的实现。对于ARMCC编译器修改如下; 将原有的弱定义注释掉或删除 ; PUBWEAK SysTick_Handler ; PUBWEAK PendSV_Handler ; ... ; SECTION .text:REORDER:NOROOT(2) ; SysTick_Handler ; B . ; PendSV_Handler ; B . ; 改为直接导入RT-Thread库中的强符号 IMPORT SysTick_Handler ; 从C代码中导入 IMPORT PendSV_Handler然后在board.c中我们确实需要提供SysTick_Handler前面已经实现而PendSV_Handler则由RT-Thread在libcpu/arm/cortex-m4下的汇编文件中实现如context_armcc.S我们只需要确保链接时能找到它。修改完这两处再次编译应该就能得到一个没有错误、可以生成.axf或.hex文件的工程了。5. 第一个线程的创建与系统启动工程编译通过只是第一步接下来要让内核真正跑起来并创建我们的第一个线程。5.1 理解RT-Thread的启动流程RT-Thread的启动顺序是芯片复位后执行启动文件中的汇编代码初始化栈指针(SP)跳转到Reset_Handler。Reset_Handler调用SystemInit初始化时钟然后复制数据段清零BSS段最后跳转到main函数。在main函数中通常由我们实现调用rt_hw_board_init()进行板级初始化就是我们上面实现的。调用rt_show_version()显示版本信息可选。创建并启动第一个线程通常是main线程。调用rt_system_scheduler_start()启动调度器系统开始运行。5.2 创建初始线程在main.c中我们不再写裸机的while(1)循环而是这样写#include rtthread.h #include “board.h” /* 定义线程栈 */ ALIGN(RT_ALIGN_SIZE) // 栈对齐 static rt_uint8_t rt_main_thread_stack[512]; // 512字节的栈空间 /* 定义线程控制块 */ static struct rt_thread main_thread; /* 第一个线程的入口函数 */ static void main_thread_entry(void *parameter) { rt_kprintf(“Main thread started.\n”); while (1) { rt_kprintf(“Main thread is running…\n”); rt_thread_mdelay(1000); // 睡眠1000毫秒让出CPU } } int main(void) { rt_err_t result RT_EOK; /* 板级初始化时钟、串口、堆内存等 */ rt_hw_board_init(); /* 显示RT-Thread版本 */ rt_show_version(); /* 初始化main线程 */ result rt_thread_init(main_thread, // 线程控制块 “main”, // 线程名 main_thread_entry, // 入口函数 RT_NULL, // 参数 rt_main_thread_stack[0], // 栈起始地址 sizeof(rt_main_thread_stack), // 栈大小 RT_THREAD_PRIORITY_MAX / 2, // 优先级数字越小优先级越高 20); // 时间片 if (result RT_EOK) { rt_thread_startup(main_thread); // 启动线程将其加入就绪队列 } else { rt_kprintf(“Failed to init main thread!\n”); return -1; } /* 启动调度器永不返回 */ rt_system_scheduler_start(); /* 不会执行到这里 */ while (1); }这里详细解释一下rt_thread_init的参数main_thread: 线程控制块TCB的地址用于内核管理这个线程的所有信息。“main”: 线程的名字方便调试时查看。main_thread_entry: 线程函数线程被调度执行时就从这里开始运行。RT_NULL: 传递给线程函数的参数这里没有。rt_main_thread_stack[0]: 线程栈的起始地址。线程有自己独立的栈空间用于保存局部变量、函数调用现场等。sizeof(…): 栈的大小。512字节对于这个简单的打印任务足够了但复杂的任务需要更大的栈否则会溢出。我们之前开启了RT_USING_OVERFLOW_CHECK如果栈溢出内核会给出提示。RT_THREAD_PRIORITY_MAX / 2: 优先级。RT-Thread中优先级数值越小优先级越高。这里设为中等级别。20: 时间片。当多个相同优先级的线程就绪时调度器会根据时间片进行轮转调度。单位是系统时钟节拍Tick。rt_thread_startup函数会将线程状态设置为就绪RT_THREAD_READY并把它插入到对应优先级的就绪队列中。最后rt_system_scheduler_start()会启动调度器。调度器会从就绪队列中找出最高优先级且就绪的线程并进行上下文切换执行它的代码。一旦调度器启动main函数就不会再返回了CPU的控制权完全交给了RT-Thread内核。将程序编译下载到iCore3通过串口助手波特率与rt_hw_usart_init中配置的一致如115200应该能看到RT-Thread的版本信息以及“Main thread started.”和周期性的“Main thread is running…”打印。这说明内核已经成功运行并且你的第一个线程正在被调度6. 多线程创建与同步运行验证单个线程还看不出调度器的威力。现在我们来创建第二个线程让它们交替运行直观感受RT-Thread的并发执行。6.1 创建第二个线程例如LED闪烁线程我们在main.c中再添加一个线程/* 定义LED线程栈和控制块 */ ALIGN(RT_ALIGN_SIZE) static rt_uint8_t rt_led_thread_stack[256]; static struct rt_thread led_thread; /* LED线程入口函数 */ static void led_thread_entry(void *parameter) { rt_kprintf(“LED thread started.\n”); /* 假设LED连接在PG13引脚 */ rt_pin_mode(PIN_LED, PIN_MODE_OUTPUT); while (1) { rt_pin_write(PIN_LED, PIN_HIGH); rt_thread_mdelay(200); // 亮200ms rt_pin_write(PIN_LED, PIN_LOW); rt_thread_mdelay(300); // 灭300ms rt_kprintf(“LED toggled.\n”); } }注意这里使用了rt_pin_mode和rt_pin_write函数。这需要我们在rtconfig.h中开启RT_USING_PIN并且实现iCore3的GPIO设备驱动并将其注册到RT-Thread的PIN设备框架下。这是一个相对独立的部分简单来说你需要实现一个rt_hw_pin_init()函数在其中调用rt_device_pin_register来注册你的GPIO操作函数集。为了简化你可以先不使用PIN设备直接用HAL库的HAL_GPIO_WritePin函数来控制LED但这会破坏RT-Thread的设备框架一致性。建议花点时间把PIN驱动做了这是理解RT-Thread设备模型的好机会。然后在main函数中在初始化main线程之后启动调度器之前初始化并启动这个LED线程/* 初始化LED线程 */ result rt_thread_init(led_thread, “led”, led_thread_entry, RT_NULL, rt_led_thread_stack[0], sizeof(rt_led_thread_stack), RT_THREAD_PRIORITY_MAX / 2, // 与main线程同优先级 10); // 时间片设为10与main线程不同 if (result RT_EOK) { rt_thread_startup(led_thread); }这里我把LED线程的优先级设得和main线程一样都是RT_THREAD_PRIORITY_MAX / 2但时间片一个20一个10。这样当两个线程都就绪时调度器会采用时间片轮转调度。main线程运行20个tick后会被强制挂起LED线程获得10个tick的运行时间如此循环。6.2 观察与调试使用Finsh Shell如果一切顺利下载程序后你应该能看到两个线程的启动信息交替打印并且LED灯以500ms的周期闪烁亮200ms灭300ms。但更强大的调试工具是Finsh Shell。因为我们已经在rtconfig.h中启用了RT_USING_FINSH并且在board.c的rt_hw_board_init()最后调用了rt_components_board_init()该函数会初始化所有已开启的组件包括Finsh所以Finsh线程应该已经自动创建了。通过串口终端如PuTTY、MobaXterm等需要设置为交互模式按回车你应该能看到Finsh的命令提示符msh 。输入ps或者list_thread命令可以查看当前系统中所有线程的状态、优先级、栈使用情况等。msh list_thread thread pri status sp stack size max used left tick error -------- --- ------- ---------- ---------- ------ ---------- --- tshell 20 running 0x000000cc 0x00000400 27% 0x0000000a 000 main 15 suspend 0x000000a0 0x00000200 45% 0x0000000f 000 led 15 suspend 0x00000088 0x00000100 38% 0x0000000a 000 tidle0 31 ready 0x00000058 0x00000100 22% 0x0000000a 000从这个列表可以清晰地看到tshell: Finsh线程优先级20正在运行因为你在和它交互。main和led: 我们创建的两个线程优先级都是15状态是suspend挂起因为它们正在执行rt_thread_mdelay进行延时让出了CPU。tidle0: 系统空闲线程优先级最低31当没有其他线程运行时它就运行。stack size和max used列非常有用可以监控栈的使用情况防止溢出。你还可以使用free命令查看堆内存的使用情况。这些工具对于在资源受限的嵌入式系统中调试和优化是极其宝贵的。7. 移植过程中的关键陷阱与解决方案第一次移植不可能一帆风顺以下是几个我踩过的坑及其解决办法坑一链接错误——找不到heap段或HEAP区域未定义。现象编译成功链接时报错提示Image$$RW_IRAM2$$Base等符号未定义或者rt_system_heap_init的参数无效。根因链接脚本.sct或.ld文件修改不正确没有正确定义出堆内存区域或者定义的区域名与C代码中引用的符号不匹配。解决仔细检查链接脚本。在Keil中确保在Options for Target - Linker下勾选了Use Memory Layout from Target Dialog然后去Target选项卡查看IRAM1的尺寸是否设置正确比如0x20000000, 0x00030000 表示192KB。更可靠的方法是直接编辑.sct文件像第4.1节那样明确定义一个RW_IRAM2区域给HEAP。然后确认board.c中的HEAP_BEGIN和HEAP_END定义与链接脚本中的区域名严格对应。坑二系统启动后卡死无任何输出。现象程序能下载但串口没有任何打印LED也不闪。排查步骤检查时钟首先确认SystemCoreClock全局变量的值是否正确应该是168M。在rt_hw_board_init最开始用rt_kprintf打印一下但前提是串口先初始化好。一个笨办法是先注释掉所有RT-Thread相关的初始化只做最基本的GPIO点灯确保硬件和基础工程是好的。检查SysTick配置SysTick_Config的参数是SystemCoreClock / RT_TICK_PER_SECOND。如果RT_TICK_PER_SECOND是1000SystemCoreClock是168M那么参数就是168000。这个数值必须在SysTick重载值寄存器的有效范围内24位。168000是没问题的。但如果计算错误导致中断频率过高或过低系统可能无法正常调度。检查栈大小初始线程main线程或空闲线程的栈给得太小可能导致一开始就栈溢出。尝试将初始线程栈扩大到1024或2048字节试试。检查中断优先级RT-Thread要求PendSV和SysTick的中断优先级为最低即优先级数值最大。在STM32的NVIC中优先级数值越小优先级越高。我们需要在rt_hw_board_init中调用HAL_NVIC_SetPriority将SysTick_IRQn和PendSV_IRQn的优先级设置为最低例如15。这是非常关键且容易遗漏的一步HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0); HAL_NVIC_SetPriority(PendSV_IRQn, 15, 0);单步调试如果以上都不行只能祭出杀手锏——JTAG/SWD单步调试。在rt_system_scheduler_start()处设断点看程序能否执行到这里。如果不能问题出在之前的初始化如果能再单步进去看调度器是否成功切换到了第一个线程。坑三Finsh无法输入或输出乱码。现象能看到RT-Thread启动打印但按回车没反应或者输入字符显示乱码。根因串口驱动对接有问题。rt_hw_console_getchar/putchar实现不正确或者串口初始化波特率、停止位等与终端软件设置不匹配。解决确保rtconfig.h中的RT_CONSOLE_DEVICE_NAME与你注册的串口设备名一致例如“uart1”。在rt_hw_board_init中先初始化串口硬件再调用rt_console_set_device。实现rt_hw_console_getchar时建议使用中断环形缓冲区的方式实现非阻塞读取。如果简单使用HAL_UART_Receive进行阻塞读取可能会因为等不到数据而卡死整个系统。一个简单的非阻塞实现是在串口接收中断服务程序中将数据存入环形缓冲区rt_hw_console_getchar则从该缓冲区读取。检查波特率。这是最容易被忽略的确保代码里的波特率如115200和串口终端软件的设置完全一致。坑四线程创建失败返回错误码。现象rt_thread_init返回非RT_EOK。排查rt_thread_init可能返回的错误码有-RT_ERROR一般错误、-RT_ENOMEM内存不足等。最常见的是栈地址未对齐。确保线程栈数组使用了ALIGN(RT_ALIGN_SIZE)进行对齐。RT_ALIGN_SIZE在rtconfig.h中定义对于Cortex-M4通常是4或8。移植RT-Thread到一块新的板子就像搭积木底层驱动时钟、串口、GPIO是地基内核配置和链接脚本是骨架线程和应用是血肉。整个过程需要耐心和细致的调试。一旦跑通看到多个线程在你的iCore3上井然有序地并发执行那种成就感是非常棒的。这为后续集成更复杂的组件如文件系统、网络协议栈乃至与FPGA端进行高速数据交互打下了坚实的基础。
返回列表