Keil MDK移植RT-Thread Nano:从裸机到RTOS的实战指南

发布时间:2026/8/19 16:07:36
Keil MDK移植RT-Thread Nano:从裸机到RTOS的实战指南 1. 项目缘起为什么要在Keil MDK上折腾RT-Thread Nano如果你手头正好有个STM32的项目之前一直用着裸机前后台或者FreeRTOS最近可能听说了RT-Thread这个国产的物联网操作系统想试试水但又觉得它的标准版RT-Thread功能太多、体积太大怕自己的小Flash吃不消。那么RT-Thread Nano以下简称Nano可能就是你的菜。它是一个极简版的实时内核包含了实时操作系统RTOS最核心的任务调度、同步通信、定时器等功能但去掉了设备框架、FinSH组件、网络协议栈等“重型”模块内核体积可以裁剪到3KB ROM、1KB RAM左右对资源极其敏感的应用场景非常友好。而Keil MDKMicrocontroller Development Kit作为ARM Cortex-M内核单片机开发领域的“瑞士军刀”几乎是每个嵌入式工程师的标配。它的集成开发环境IDE、强大的调试器和丰富的中间件支持让开发效率大大提升。所以在Keil MDK这个最熟悉的战场上把RT-Thread Nano这个轻量级内核移植进去就成了一个非常自然且高频的需求。这不仅能让你在熟悉的开发环境中快速体验RTOS带来的结构化编程优势比如解决裸机编程中复杂的状态机管理和优先级调度问题也为后续项目升级到功能更全的RT-Thread标准版铺平了道路。网上关于“Keil MDK移植RT-Thread”的讨论一直很热相关的搜索词也五花八门从基础的freertos移植、lwip移植到具体的stm32f407移植freertos、lvgl移植stm32都反映了开发者们在不同RTOS和组件间迁移、集成的普遍需求。这也侧面说明了掌握一种RTOS的移植方法其思路和踩坑经验是具有通用性的。今天我就以最经典的STM32F103C8T6也就是我们常说的“蓝桥杯”板子或最小系统板为例带你走一遍在Keil MDK V5环境下从零开始移植RT-Thread Nano 3.1.5的全过程。我会重点讲清楚为什么要这么做而不仅仅是怎么做并分享几个我亲自踩过、文档里不一定写的“坑”。2. 移植前的战略准备理清思路与获取弹药在动手写任何代码之前我们必须先想明白移植的本质是什么。所谓“移植”就是让一个操作系统内核能在目标硬件平台上跑起来。这通常需要解决两个层面的问题CPU架构适配和板级支持包BSP适配。幸运的是对于ARM Cortex-M系列内核RT-Thread团队已经做好了绝大部分的架构适配工作比如任务切换的汇编代码、中断处理等。我们的工作重心就落在了**板级支持包BSP**上具体来说就是实现系统时钟、串口控制台、GPIO等硬件资源的驱动对接。所以我们的核心思路是以RT-Thread Nano官方提供的“样板工程”为骨架将其核心文件融入我们自己的Keil MDK工程中并修改关键配置使其适配我们的具体硬件。这里切忌自己从头造轮子。第一步获取官方资源包。RT-Thread官方提供了极其方便的Nano移植包。访问RT-Thread官网的下载中心找到RT-Thread Nano 3.1.5的发布页面下载rt-thread-nano-v3.1.5.zip。解压后你会发现里面有几个关键目录rt-thread-3.1.5这是Nano内核的源代码包含内核、组件、libcpuCPU移植层等。bsp这里存放了针对不同芯片厂商的板级支持包示例。对我们来说bsp\stm32\libraries下的HAL库或标准外设库文件以及bsp\stm32\drivers下的驱动框架很有参考价值。但注意我们不一定全盘照搬。documentation官方文档遇到问题时可以查阅。第二步准备你的Keil MDK工程。假设你已经有一个能正常编译、下载、运行的STM32裸机工程比如一个点灯程序。这个工程是你的“基地”它确保了你的芯片型号、时钟配置、下载器设置等都是正确的。记下你的工程路径我们将在其基础上进行“手术”。第三步明确需要引入的核心文件。从官方资源包中我们主要需要以下文件以STM32F1系列使用标准外设库为例内核源码rt-thread-3.1.5\src目录下的所有.c文件。这是RT-Thread Nano的心脏。CPU移植层rt-thread-3.1.5\libcpu\arm\cortex-m3对应Cortex-M3内核目录下的context_*.S任务切换汇编和cpuport.cCPU相关接口如开关中断。配置文件rt-thread-3.1.5\include\rtconfig.h这是最核心的配置文件所有功能的裁剪、系统时钟频率的设置都在这里。官方示例里可能有一个rtconfig_bsp.h我们可以将其复制并重命名为rtconfig.h然后在此基础上修改。板级支持包BSP示例文件参考bsp\stm32\libraries和bsp\stm32\drivers但更重要的是bsp\stm32\applications下的board.c和drv_uart.c。board.c包含了系统时钟初始化SystemClock_Config、板级初始化rt_hw_board_init等关键函数。drv_uart.c则提供了串口驱动框架用于后续的rt_kprintf输出。注意这里的一个常见误区是直接复制整个BSP目录。对于Nano移植我们只需要最精简的、能让内核跑起来的几个文件过多的驱动文件反而会增加初期的复杂度。建议采用“按需添加”的策略。3. 工程“嫁接”手术文件添加与路径配置现在我们开始在Keil MDK工程中进行实际操作。假设你的裸机工程名为MyProject位于D:\Projects\MyProject。3.1 在工程目录中创建源码结构在你的工程根目录下新建一个rt-thread文件夹然后仿照官方资源包的结构创建子目录MyProject/ ├── rt-thread/ │ ├── src/ (存放内核源码 .c 文件) │ ├── include/ (存放内核头文件 .h 文件) │ ├── libcpu/arm/cortex-m3/ (存放CPU移植文件) │ └── bsp/ (存放我们板级相关的文件) │ ├── board.c │ ├── drv_uart.c │ └── rtconfig.h (最终的配置文件)然后从官方资源包中将对应的文件复制到相应目录。注意include目录下的头文件也需要一并复制过来。3.2 在Keil MDK中添加文件分组打开你的Keil工程在Project侧边栏你的Target下新建几个文件组Group以便更好地管理文件RT-Thread Kernel: 添加rt-thread/src目录下的所有.c文件。RT-Thread CPU: 添加rt-thread/libcpu/arm/cortex-m3目录下的context_*.S和cpuport.c。RT-Thread BSP: 添加rt-thread/bsp目录下的board.c和drv_uart.c。3.3 配置头文件包含路径这是关键一步如果路径不对编译时会疯狂报错error #5: cannot open source input file。在Keil的Options for Target-C/C-Include Paths中添加以下路径根据你的实际目录调整.\rt-thread\include .\rt-thread\libcpu\arm\cortex-m3 .\rt-thread\bsp .\rt-thread\src添加后编译器在查找#include rtthread.h等头文件时就知道去这些目录里找了。3.4 处理汇编文件对于context_*.S这类汇编文件Keil MDK默认可能无法识别或编译选项不对。右键点击该文件选择Options for File确保Properties中的File Type被正确设置为Assembly Language file。有时候文件扩展名是大写的.SKeil可能认不出改成小写的.s或者手动设置文件类型即可。4. 核心配置的“心脏手术”深度定制rtconfig.hrtconfig.h是移植成败的灵魂。它通过一系列#define RT_USING_XXX的宏定义来决定内核包含哪些功能。我们直接基于一个最简配置开始修改。4.1 基础配置// rtconfig.h #ifndef RT_CONFIG_H__ #define RT_CONFIG_H__ /* 生成ROM二进制文件时的自动初始化机制默认开启即可 */ #define RT_USING_USER_MAIN #define RT_MAIN_THREAD_STACK_SIZE 256 // 主线程栈大小根据需求调整 /* 内核调试功能初期调试建议打开稳定后可关闭以节省资源 */ #define RT_DEBUG #define RT_DEBUG_INIT 1 /* 注意RT_DEBUG_ASSERT宏如果开启断言失败时会调用rt_assert_handler需要自己实现或关闭 */ #define RT_DEBUG_ASSERT 0 /* 初期建议关闭避免未定义的处理函数导致死循环 */ /* 系统时钟节拍频率即每秒的tick数决定任务调度的粒度 */ /* 1000表示1ms一个tick常用值。值越大调度越灵敏但系统开销也越大 */ #define RT_TICK_PER_SECOND 1000 /* 字节对齐方式与硬件架构相关Cortex-M通常是8字节对齐 */ #define RT_ALIGN_SIZE 8 /* 线程优先级范围默认32级足够优先级数越小优先级越高 */ #define RT_THREAD_PRIORITY_MAX 32 /* 线程名最大长度 */ #define RT_NAME_MAX 8 /* 使用组件初始化机制 */ #define RT_USING_COMPONENTS_INIT /* 使用用户main函数即我们自己的main */ #define RT_USING_USER_MAIN为什么是1000Hz这是一个经验值。1ms的时钟中断间隔对于大多数需要毫秒级响应的嵌入式应用如按键消抖、LED闪烁、简单的通信超时来说足够精细。如果设置成100Hz10ms你可能在实现一个需要5ms精确定时的功能时就会感到吃力。但也不是越高越好1000Hz意味着每秒1000次时钟中断会带来一定的CPU开销。对于低功耗应用可能需要权衡后降低该值。4.2 内核对象与内存管理/* 最大对象数量线程、信号量、互斥锁、事件、邮箱、消息队列等 */ #define RT_USING_HEAP // 必须开启使用动态堆内存 #define RT_USING_SMALL_MEM // 使用小内存管理算法适合资源受限的MCU //#define RT_USING_SLAB // SLAB算法更高效但更复杂Nano默认用SMALL_MEM即可 #define RT_USING_CONSOLE // 启用控制台用于rt_kprintf输出 #define RT_CONSOLEBUF_SIZE 128 // 控制台缓冲区大小关于动态堆内存即使你打算主要使用静态内存RT_USING_HEAP也建议开启。因为内核内部的一些对象如定时器的创建默认会用到动态内存。如果你确定不用任何动态创建功能可以深入研究并关闭但初期为了简化建议打开。4.3 线程间通信与同步机制根据你的项目需求选择性开启。初期为了测试可以只开信号量和互斥锁。#define RT_USING_SEMAPHORE // 信号量 #define RT_USING_MUTEX // 互斥锁 // #define RT_USING_EVENT // 事件集 // #define RT_USING_MAILBOX // 邮箱 // #define RT_USING_MESSAGEQUEUE // 消息队列选型建议信号量用于任务同步如等待一个事件发生互斥锁用于保护共享资源如一个全局变量。事件集适用于一个任务等待多个事件中的任意一个或全部发生。邮箱和消息队列用于传递数据。建议先从信号量和互斥锁开始理解。4.4 设备驱动与FinSHNano默认不包含完整的设备驱动框架和设备模型。我们通常直接操作寄存器或使用原有的库如标准外设库、HAL库来驱动外设。因此以下选项通常关闭// #define RT_USING_DEVICE // 标准版RT-Thread的设备框架Nano移植通常关闭 // #define RT_USING_DEVICE_IPC // #define RT_USING_FINSH // FinSH组件强大的命令行交互工具但会占用较多ROM/RAMNano移植通常关闭为什么不直接用RT-Thread的设备框架对于简单的Nano移植引入完整的设备框架会增加复杂性和体积。我们的目标是让内核先跑起来外设驱动沿用裸机时代的写法或基于官方提供的drv_uart.c范例进行简化实现是更快捷的路径。5. 板级初始化与“第一推动力”board.c的实现board.c是连接硬件和操作系统的桥梁。它的核心函数是rt_hw_board_init()系统启动后会首先调用它。5.1 系统时钟初始化这个函数直接调用你裸机工程里已经写好的SystemClock_Config()函数。确保你的系统时钟如STM32F103的72MHz配置正确因为RT-Thread的系统时钟节拍SysTick以及后续的软件定时器都依赖于此。// board.c #include rtthread.h #include board.h // 这个头文件可能需要你创建或者直接包含芯片相关的头文件如stm32f10x.h extern void SystemClock_Config(void); // 声明你的时钟配置函数它可能在main.c或system_stm32f10x.c中 void rt_hw_board_init() { /* 1. 初始化系统时钟 */ SystemClock_Config(); /* 2. 配置SysTick作为RT-Thread的时钟节拍源 */ /* 注意SysTick_Config()通常在RT-Thread的启动代码中已经调用。 但我们需要确保SystemCoreClock这个全局变量是正确的。 它定义在system_stm32f10x.c中由SystemClock_Config()函数更新。*/ // SystemCoreClock已经由SystemClock_Config()更新此处无需重复操作 /* 3. 初始化硬件外设如串口用于调试打印 */ // 假设我们有一个uart_init()函数 uart_init(); /* 4. 初始化控制台将串口绑定到控制台 */ /* 这需要实现一个名为rt_hw_console_output的函数用于输出字符 */ rt_console_set_device(RT_CONSOLE_DEVICE_NAME); // RT_CONSOLE_DEVICE_NAME 需在rtconfig.h或此处定义如uart1 /* 5. 打印RT-Thread版本信息 */ rt_show_version(); /* 6. 初始化系统堆内存 */ /* rt_system_heap_init()需要指定堆内存的起始和结束地址。 通常我们使用一个静态数组作为堆空间或者指定MCU内部RAM的某一段空闲区域。*/ extern void rt_system_heap_init(void *begin_addr, void *end_addr); // 例如在STM32F103C8T6上RAM地址0x20000000开始共20KB。 // 我们需要留出已使用的空间全局变量、栈等。一个简单的做法是使用一个末尾的数组。 // 更规范的做法是修改链接脚本这里先用一个静态数组示例。 // static rt_uint8_t rt_heap[1024 * 4]; // 4KB的堆 // rt_system_heap_init(rt_heap, rt_heap sizeof(rt_heap)); // 更好的方法见下文“内存管理”章节。 /* 7. 初始化调度器 */ rt_system_scheduler_init(); /* 8. 初始化系统定时器线程 */ rt_system_timer_thread_init(); /* 9. 初始化应用组件如果开启了RT_USING_COMPONENTS_INIT */ #ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); rt_components_init(); #endif /* 10. 创建主线程如果开启了RT_USING_USER_MAIN */ #ifdef RT_USING_USER_MAIN rt_application_init(); // 这个函数内部会调用你的main()函数 #endif }关键点剖析SysTick配置通常在RT-Thread的启动文件如startup_stm32f103xe.s中的Reset_Handler或移植层的cpuport.c里会调用SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND)来配置滴答定时器。你需要确认这一点。如果没找到可能需要在board.c的rt_hw_board_init里显式调用一次。堆内存初始化这是最容易出问题的地方。上面的示例使用了静态数组简单但不灵活。更推荐的做法是修改链接脚本.sct或.ld文件预留出一段RAM空间专供堆使用。例如在Keil的sct文件中你可以定义两个RAM区域一个用于常规数据RW_IRAM1另一个用于堆RW_IRAM2。然后在board.c中使用rt_system_heap_init((void*)Image$$RW_IRAM2$$Base, (void*)Image$$RW_IRAM2$$Limit)来初始化。这需要对链接脚本有一定了解但一劳永逸。控制台输出rt_hw_console_output函数需要你自己实现。它通常是一个简单的串口发送函数。例如void rt_hw_console_output(const char *str) { rt_size_t i 0; while (str[i] ! \0) { if (str[i] \n) // 处理换行可能需要在前面加回车符\r { uart_putc(\r); } uart_putc(str[i]); i; } }其中uart_putc是你基于标准外设库或HAL库实现的串口发送一个字节的函数。6. 主线程的华丽转身从main到thread在裸机程序中main()函数是程序的唯一入口里面通常是一个while(1)超级循环。在RT-Thread中main()函数变成了一个线程通常叫主线程。操作系统启动后会自动创建并启动这个线程。6.1 修改你的main.c// main.c #include rtthread.h #include board.h /* 定义线程栈 */ ALIGN(RT_ALIGN_SIZE) // 栈需要按照系统要求的对齐方式对齐 static rt_uint8_t main_stack[512]; // 主线程栈空间 /* 定义线程控制块 */ static struct rt_thread main_thread; /* 主线程入口函数 */ static void main_thread_entry(void *parameter) { /* 这里放你原来在main函数while(1)里的代码 */ while (1) { /* 你的应用代码例如点灯 */ LED_ON; rt_thread_mdelay(500); // 使用RT-Thread的延时函数让出CPU LED_OFF; rt_thread_mdelay(500); /* 你可以创建其他线程、信号量等 */ rt_kprintf(Hello RT-Thread!\n); // 使用RT-Thread的打印函数 } } /* 标准的main函数 */ int main(void) { rt_err_t result; /* 初始化主线程 */ /* 参数依次为线程控制块指针、线程名、入口函数、参数、栈起始地址、栈大小、优先级、时间片 */ result rt_thread_init(main_thread, main, main_thread_entry, RT_NULL, main_stack[0], sizeof(main_stack), 10, // 优先级数值越小优先级越高 5); // 时间片单位是tick表示该线程一次能运行的最长时间 if (result RT_EOK) { rt_thread_startup(main_thread); // 启动线程 } else { // 初始化失败处理 while(1); } /* main函数本身不应该再进入循环线程调度由内核接管 */ /* 实际上执行完rt_thread_startup调度器可能就开始运行了 */ return 0; }为什么要把原来的main改成线程这是RTOS编程思想的体现。原来的超级循环独占CPU所有任务都是“平等”地在一个循环里轮询。而RTOS中每个任务都是一个独立的线程拥有自己的栈和优先级由内核进行调度。这样高优先级的任务如处理紧急中断、关键通信可以及时得到执行程序结构更清晰响应更实时。rt_thread_mdelay(500)这个延时函数是协作式的它会让当前线程挂起500个tickCPU立即去执行其他就绪的线程极大地提高了CPU利用率。6.2 启动流程梳理芯片上电执行启动文件中的Reset_Handler。Reset_Handler调用SystemInit初始化时钟等然后跳转到__mainC库函数进行数据初始化最后调用rtthread_startup()RT-Thread的启动函数。rtthread_startup()会依次调用rt_hw_board_init()我们刚才实现的板级初始化。rt_show_version()打印版本。rt_system_timer_init()初始化系统定时器。rt_system_scheduler_init()初始化调度器。rt_application_init()这里会调用我们写的main()函数rt_system_timer_thread_init()创建定时器线程。rt_thread_idle_init()创建空闲线程。rt_system_scheduler_start()启动调度器从此CPU控制权交给RT-Thread内核。调度器启动后会从就绪队列中找出最高优先级的线程来运行。此时我们的main_thread已经由rt_application_init()创建并启动处于就绪状态因此很快就会被调度执行。7. 编译、下载与“第一盏灯”的调试完成以上步骤后点击Keil的编译按钮。你大概率会遇到一堆错误。别慌这是移植的必经之路。7.1 常见编译错误与解决error #5: cannot open source input file rtthread.h头文件路径没添加对。请严格按照第3.3节检查Include Paths。undefined symbol SystemCoreClock在board.c中可能引用了SystemCoreClock但这个变量定义在system_stm32f10x.c中。确保你的工程包含了这个文件并且在board.c里包含了正确的头文件如#include stm32f10x.h。undefined symbol __heap_base, __heap_limit如果你使用了链接脚本指定堆内存并且rt_system_heap_init使用了这些符号但链接脚本里没有定义。需要检查并修改链接脚本.sct文件。关于.S文件的汇编语法错误Keil的ARM汇编器和GCC的语法有细微差别。RT-Thread官方提供的context_*.S文件通常是GCC格式的。你需要找到针对Keil MDK修改过的版本或者自己进行适配。一个常见的修改是将GCC汇编中的.thumb、.syntax unified等指令改为Keil支持的THUMB、PRESERVE8等。这是移植过程中的一个关键难点。建议直接从RT-Thread官方GitHub仓库的bsp\stm32\libraries\cortex-m3目录下寻找Keil版本的启动文件和移植文件或者参考其他已成功的Keil工程。rt_hw_console_output未定义你需要在board.c或其他文件中实现这个函数并确保其被正确声明通常在board.h中声明为void rt_hw_console_output(const char *str);。7.2 链接脚本Scatter File的调整对于STM32F103C8T6RAM只有20KB合理分配内存至关重要。我们需要在链接脚本Project.sct中明确划分堆空间。; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00010000 { ; 加载区域起始地址0x08000000大小64KB ER_IROM1 0x08000000 0x00010000 { ; 执行区域代码和只读数据放在这里 *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { ; 执行区域RW数据全局变量、静态变量等 .ANY (RW ZI) } ; 新增一个执行区域专门作为堆空间 RW_IRAM2 0x20005000 0x00001000 { ; 起始地址0x2000000020KB大小4KB .ANY (HEAP) ; 将所有名为HEAP的段放在这里 } }然后在board.c中extern int Image$$RW_IRAM2$$Base; extern int Image$$RW_IRAM2$$Limit; rt_system_heap_init((void*)Image$$RW_IRAM2$$Base, (void*)Image$$RW_IRAM2$$Limit);这样我们就从RAM末尾划出了4KB专供RT-Thread的堆内存使用避免了与全局变量和栈的空间冲突。7.3 下载与调试编译通过后下载程序到开发板。连接串口调试助手如Putty、SecureCRT波特率设置为你uart_init()中配置的波特率如115200。如果一切顺利你应该能看到串口输出RT-Thread的版本信息以及你主线程中rt_kprintf打印的“Hello RT-Thread!”。同时LED也应该开始闪烁。如果没有输出检查硬件连接TX、RX、GND线是否接对。检查串口初始化确保uart_init()函数正确配置了GPIO和USART并且使能了时钟。检查rt_hw_console_output单步调试看这个函数是否被调用以及内部的uart_putc是否能正确发送数据。检查系统时钟如果SysTick配置的频率不对比如RT_TICK_PER_SECOND是1000但实际SysTick中断是100Hz可能会导致系统运行异常包括调度和延时都不准。使用调试器在rt_hw_board_init()开始处打一个断点看程序是否能执行到这里。如果不能检查启动文件、向量表是否正常。如果能再一步步往下跟踪。8. 进阶与排坑从“能跑”到“跑得好”当第一个程序跑起来后我们还需要考虑一些进阶问题和常见陷阱。8.1 中断管理与PendSV在Cortex-M架构上RT-Thread使用PendSV异常来进行任务上下文切换。你需要确保在启动文件中PendSV_Handler的优先级被设置为最低通常如此。在你的中断服务程序ISR中如果调用了RT-Thread提供的、可能引起任务调度的API如释放信号量rt_sem_release、发送消息rt_mb_send等必须使用中断版本的API即带_isr后缀的函数如rt_sem_release_isr。这是因为在中断上下文中不能直接进行任务调度。中断嵌套的处理。RT-Thread的移植层已经处理了大部分情况但如果你有特殊需求需要了解rt_interrupt_enter()和rt_interrupt_leave()这两个函数的作用。8.2 系统嘀嗒SysTick与硬件定时器RT-Thread默认使用SysTick作为系统时钟源。但有些项目可能因为功耗或其他原因希望使用其他硬件定时器如TIM2作为时钟源。这需要修改cpuport.c中的相关函数如rt_tick_increase()的调用者。这属于深度定制除非必要不建议初学者改动。8.3 内存管理与堆溢出即使我们划分了堆空间如果应用程序动态创建了太多线程、信号量等对象或者线程栈设置得过大仍然可能导致堆溢出。RT-Thread提供了内存钩子函数rt_malloc_hook和rt_free_hook可以用于在调试阶段检测内存分配和释放帮助定位内存问题。在rtconfig.h中打开RT_USING_MEMTRACE和RT_USING_MEMHEAP_AS_HEAP等选项可以启用更详细的内存调试功能。8.4 线程栈大小的设定线程栈大小设置是一门艺术。设小了会导致栈溢出程序跑飞现象难以捉摸比如某个函数调用后莫名其妙死机。设大了浪费宝贵的RAM。一个粗略的估算方法是观察线程中局部变量的大小、函数调用深度。对于简单的LED闪烁线程256字节可能就够了对于处理复杂协议栈如LWIP的线程可能需要1KB甚至更多。一个实用的调试技巧是在rtconfig.h中打开RT_USING_OVERFLOW_CHECK它会在线程切换时检查栈溢出一旦发现就会调用rt_assert_handler帮助你快速定位问题线程。8.5 与原有裸机代码的共存你可能有很多现成的裸机驱动代码如ADC采集、SPI读写Flash。在RT-Thread中这些代码通常可以不加修改地放到一个线程中运行。但需要注意共享资源的保护。例如如果ADC驱动中有一个全局的adc_value变量同时被读取线程和显示线程访问就需要用互斥锁rt_mutex_t来保护。这是RTOS编程与裸机编程最大的思维转变之一从“顺序执行全局变量”转变为“并发执行同步机制”。移植RT-Thread Nano到Keil MDK更像是一次对嵌入式系统理解的重塑。它迫使你去思考中断、调度、内存这些底层机制。过程虽然会遇到各种报错和异常但每一次问题的解决都是对系统理解的一次加深。当你的LED灯按照预定的节奏闪烁串口稳定地输出着调试信息那种对系统掌控感带来的愉悦是裸机编程难以比拟的。这只是一个开始接下来你可以尝试创建多个线程用信号量让它们协同工作或者集成一个轻量级的文件系统如LittleFS或网络协议栈如LWIP一步步构建出更复杂、更强大的嵌入式应用。