尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GD32F103移植UCOSIII实战:从环境搭建到多任务应用优化

GD32F103移植UCOSIII实战:从环境搭建到多任务应用优化 简介本资源是面向嵌入式开发初学者与进阶工程师的GD32F103微控制器uC/OS-III实时操作系统移植实践套件聚焦解决ARM Cortex-M3平台下RTOS底层移植难、外设协同调度不熟、中断与任务机制理解不深等典型问题适用于工业控制、物联网终端等需多任务实时响应的开发场景。压缩包共353个文件涵盖64个C源文件核心驱动与任务逻辑、64个汇编文件含cpu_a.asm、os_cpu_a.asm等关键移植层代码、60个头文件配置与接口定义、57个编译中间文件crf及多个工程构建文件uvprojx、axf、hex、sct等完整呈现Keil MDK环境下从裸机到RTOS的全流程工程结构包体大小为7.13MB。已有1221人学习下载。读者可直接复用已验证的Systick时基配置、中断服务封装、任务堆栈初始化模板及GPIO/UART等外设的UCOSIII兼容驱动框架快速掌握抢占式调度、信号量同步、任务间通信等关键机制并通过附带的SingleChannel.axf等可执行镜像进行实机调试验证。1. 项目概述当国产MCU遇上经典RTOS最近在做一个工控小项目主控选型时又用回了GD32F103这颗经典的国产增强型M3内核MCU。项目功能不复杂但涉及到多路传感器的数据采集、状态机控制以及一个简单的串口通信协议栈裸机状态机写起来已经有点力不从心代码耦合度高后期维护是个噩梦。于是很自然地就想到了上实时操作系统RTOS来解耦任务、规范开发。在众多RTOS中我选择了UCOSIII一个老牌、稳定、资料相对丰富的实时内核。把GD32F103和UCOSIII搭配起来并不是什么新奇的做法但其中从环境搭建、任务划分到系统调优的完整实践过程却有很多值得细说的门道。这篇文章我就把自己从零开始在GD32F103上成功移植并应用UCOSIII的全过程、踩过的坑以及一些优化心得记录下来给有类似需求的工程师朋友一个可以直接参考的“操作手册”。简单来说这个组合能帮你解决什么问题如果你正在使用GD32F103这类资源相对有限通常Flash 64KB-256KB RAM 20KB左右的Cortex-M3/M4内核单片机项目复杂度又超出了简单前后台的范畴比如需要同时处理按键扫描、显示刷新、通信解析、算法运算等多个有实时性要求的功能那么引入UCOSIII这样的RTOS可以让你的软件架构瞬间变得清晰。它将CPU时间划分为多个时间片每个任务你可以理解为一个独立的、无限循环的函数在属于自己的时间片内运行由内核负责调度和切换。这样一来复杂的逻辑被分解为一个个独立的任务降低了耦合度提高了代码的可维护性和可扩展性。GD32F103作为国产MCU的佼佼者其高性价比和与STM32F103的高度兼容性使得它成为很多成本敏感型项目的首选。而UCOSIII作为一款源码开放、可裁剪的RTOS其内核稳定可靠特别适合在资源受限的嵌入式环境中使用。2. 开发环境搭建与工程初始化2.1 工具链与源码准备工欲善其事必先利其器。第一步是准备好所有必要的“原材料”。我的开发环境基于Windows 10主要工具如下集成开发环境IDE我选择了Keil MDK-ARMVersion 5。这是最主流的ARM开发环境之一对UCOSIII的支持也最成熟有现成的移植包和大量参考例程。当然使用IAR或者GCCMakefile也是完全可行的但本文以Keil为例进行说明。GD32F103固件库从兆易创新GigaDevice的官方网站下载对应型号的固件库Firmware Library。这个库包含了芯片所有外设的驱动函数、启动文件以及一些示例工程是我们开发的基础。UCOSIII源码从Micrium现已被Silicon Labs收购的官网或可靠的资源站获取UCOSIII的官方源码。关键是要获取到针对Cortex-M3内核的移植层代码。通常源码包中会包含以下几个核心文件夹uC-CPU/与CPU架构相关的接口如临界段管理、中断开关等。uC-LIB/Micrium提供的一些通用库函数可选。uCOS-III/UCOSIII内核的核心源码包括调度、任务、信号量、消息队列等。uC-CONFIG/配置文件用于裁剪和定制内核功能。uC-BSP/板级支持包这里需要我们自己根据GD32的板子来编写。注意务必使用正版或官方允许试用的软件与源码尊重知识产权。UCOSIII用于商业项目需要授权。2.2 在Keil中创建基础工程拿到所有材料后我们开始“搭积木”。首先为GD32F103创建一个最简单的裸机工程确保LED能闪烁串口能打印这是后续一切工作的基础。新建工程打开Keil选择对应的GD32F103型号例如GD32F103C8T6。在弹出框中选择运行环境时暂时只添加Device-Startup启动文件和Device-GD32F10x_StdPeriphDriver标准外设驱动即可。添加必要文件将GD32固件库中的核心文件复制到你的工程目录下例如Drivers/文件夹放外设驱动Projects/下放你的应用代码。在Keil的工程管理窗口中建立对应的分组Groups如User,Drivers/CMSIS,Drivers/Firmware等并将对应的.c和.h文件添加进去。配置时钟与基本外设在main.c中编写系统时钟初始化函数通常使用固件库提供的rcu_config()配置好系统主频比如108MHz。然后初始化一个GPIO驱动LED一个USART用于打印调试信息。编写一个简单的main函数让LED以1Hz频率闪烁并通过串口输出“Hello GD32”。编译下载确认硬件正常工作。这个裸机工程是我们的“试验田”接下来就要把UCOSIII这颗“种子”种进去。2.3 将UCOSIII源码引入工程这是移植的关键一步结构清晰至关重要。我在工程目录下新建了一个Middlewares/UCOSIII文件夹用于存放所有UCOSIII相关的代码。复制源码将下载的UCOSIII源码包中必要的文件夹复制到Middlewares/UCOSIII下。我通常复制以下内容uCOS-III/Ports/ARM-Cortex-M3/Generic/RealView/这个路径下的os_cpu.h,os_cpu_c.c和os_cpu_a.asm是移植层的核心它们包含了与ARM Cortex-M3架构相关的汇编代码如任务切换和C接口。uCOS-III/Source/内核所有核心源文件。uCOS-III/Config/模板配置文件。uC-CPU/ARM-Cortex-M3/RealView/CPU相关接口。uC-CPU/Source/CPU通用源文件。uC-LIB/可选如果需要使用其库函数则添加。在Keil中建立分组在工程管理器中新建分组例如UCOSIII/Core,UCOSIII/Port,UCOSIII/Config,UCOSIII/CPU。然后将对应的源文件添加到相应分组。特别注意os_cpu_a.asm是汇编文件Keil需要将其识别为汇编语言文件。添加头文件路径在Keil的Options for Target - C/C - Include Paths中添加UCOSIII所有.h文件所在的目录路径。这步必不可少否则编译时会找不到头文件。至此工程的物理结构就搭建好了。但直接编译肯定会报错因为我们需要针对GD32进行具体的配置和修改。3. UCOSIII内核移植详解移植的核心工作就是让UCOSIII内核能够在GD32F103这颗具体的CPU上跑起来主要涉及中断、时钟和任务栈的处理。3.1 修改移植层文件os_cpu_a.asm 与 os_cpu_c.cos_cpu_a.asm中的OS_CPU_PendSVHandler是任务切换的“发动机”它由PendSV异常触发。对于Cortex-M3这个函数通常已经由官方提供我们一般不需要修改。但需要确认一点在GD32的启动文件startup_gd32f10x_hd.s等中PendSV的中断服务程序Handler名称是否与os_cpu_a.asm中导出的符号一致。在Keil环境下通常都是PendSV_Handler所以是匹配的。如果不匹配需要修改启动文件或移植文件使两者统一。os_cpu_c.c中有几个函数需要我们根据GD32的特性来实现OS_CPU_SysTickInit()这是系统时钟节拍SysTick初始化函数。UCOSIII需要一个周期性的时钟中断来驱动任务调度和延时。我们需要在这里配置GD32的SysTick定时器。void OS_CPU_SysTickInit (CPU_INT32U cnts) { CPU_INT32U prio; // 设置重装载值cnts由OS_CPU_SysTickClkFreq()计算得到 SysTick-LOAD cnts - 1u; // 设置SysTick中断优先级。在Cortex-M中优先级数值越小优先级越高。 // UCOSIII建议SysTick和PendSV的优先级设置为最低以避免任务切换干扰其他中断。 prio (1uL __NVIC_PRIO_BITS) - 1u; // 计算最低优先级 NVIC_SetPriority(SysTick_IRQn, prio); // 设置当前值寄存器 SysTick-VAL 0u; // 使能SysTick使用处理器时钟HCLK并开启中断 SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; }OS_CPU_SysTickClkFreq()这个函数返回系统时钟节拍的频率Hz。它用于计算OS_CPU_SysTickInit()中的cnts参数。你需要根据GD32实际的系统核心频率SystemCoreClock来返回。CPU_INT32U OS_CPU_SysTickClkFreq (void) { return (SystemCoreClock); // 假设你的SystemCoreClock已经正确设置为108000000等 }3.2 配置系统时钟节拍与中断在main()函数中在调用OSInit()初始化内核之后OSStart()启动内核之前必须调用OS_CPU_SysTickInit()来启动系统节拍定时器。cnts参数决定了SysTick中断的频率也就是系统的时间片粒度。它通过以下公式计算cnts OS_CPU_SysTickClkFreq() / OSCfg_TickRate_Hz其中OSCfg_TickRate_Hz在os_cfg_app.h中定义默认是1000Hz即1ms一个节拍。对于GD32F103跑在108MHzcnts 108000000 / 1000 108000。这个值不能超过SysTick 24位重装载寄存器的最大值2^24 -1。实操心得节拍频率并非越高越好。1000Hz1ms是通用选择。更高的频率如5000Hz可以提供更精细的时间分辨率但也会增加中断开销消耗更多CPU资源。对于大多数应用1ms完全足够。我曾在一个对实时性要求极高的电机控制项目中尝试过500Hz发现任务响应延迟变大改回1000Hz后问题解决。3.3 编写应用配置文件os_cfg_app.h这个文件是UCOSIII的“功能开关面板”决定了内核的哪些功能被编译进去直接影响最终代码的大小和性能。对于GD32F103这种资源有限的芯片裁剪至关重要。基础必选项OS_CFG_SCHED_ROUND_ROBIN_EN时间片轮转调度通常启用。OS_CFG_ARG_CHK_EN参数检查在调试阶段启用发布时可以关闭以节省代码空间和性能。任务相关OS_CFG_TASK_EN任务功能必须启用。OS_CFG_TASK_PROFILE_EN任务剖析用于调试任务执行时间非常有用但会占用额外RAM可根据需要开启。内核对象根据你计划使用的功能启用对应的选项如OS_CFG_SEM_EN信号量、OS_CFG_MUTEX_EN互斥信号量、OS_CFG_Q_EN消息队列等。不用的务必关闭。内存管理OS_CFG_MEM_EN内存管理如果你打算使用UCOSIII自带的内存池可以开启。但我更倾向于在资源紧张时使用静态分配直接关闭此功能。钩子函数OS_CFG_APP_HOOKS_EN应用钩子允许你在任务创建、删除等关键时刻插入自己的代码用于调试或监控建议开发时开启。我的一个典型裁剪配置如下针对64KB Flash20KB RAM的GD32F103C8T6#define OS_CFG_SCHED_ROUND_ROBIN_EN 1u // 启用轮转调度 #define OS_CFG_ARG_CHK_EN 0u // 发布时关闭参数检查 #define OS_CFG_TASK_PROFILE_EN 1u // 开发时开启任务剖析 #define OS_CFG_SEM_EN 1u // 启用信号量 #define OS_CFG_MUTEX_EN 1u // 启用互斥量 #define OS_CFG_Q_EN 1u // 启用消息队列 #define OS_CFG_TMR_EN 0u // 关闭软件定时器用硬件定时器替代 #define OS_CFG_MEM_EN 0u // 关闭内存管理使用静态分配通过这样精细的裁剪可以将UCOSIII内核的大小控制在10KB Flash和2KB RAM左右为应用留下充足空间。4. 多任务应用设计与实现内核跑起来后就是如何设计应用了。这是体现RTOS优势的地方。4.1 任务栈分配与优先级规划在UCOSIII中每个任务都需要独立的任务控制块OS_TCB和任务栈Stack。栈空间是在任务创建时静态分配的数组。栈大小的估算是一个经验活也是容易出问题的地方。栈大小估算栈用于存放局部变量、函数调用时的返回地址和寄存器现场。一个简单的经验是对于不调用复杂函数、局部变量不多的任务如LED闪烁512字节可能就够了。对于调用层次深、有较大局部数组的任务如数据处理任务可能需要1KB甚至更多。最稳妥的方法是在调试时通过UCOSIII提供的任务剖析功能查看栈使用的高水位线Stack Usage High Water Mark。我通常先给一个偏大的值比如2KB运行一段时间后查看实际使用量再调整到一个安全余量比如高水位线20%的值。#define TASK_LED_STK_SIZE 128 // 简单任务栈可以小 CPU_STK TaskLedStk[TASK_LED_STK_SIZE]; #define TASK_COMM_STK_SIZE 512 // 通信任务可能处理缓冲区 CPU_STK TaskCommStk[TASK_COMM_STK_SIZE];优先级规划UCOSIII中数字越小优先级越高。优先级0通常保留给空闲任务优先级1给统计任务如果开启。我的规划原则是高优先级小数字赋予对实时性要求极高的任务如紧急故障处理、关键信号采集。但这类任务要短小精悍执行时间短。中优先级赋予主要业务逻辑任务如控制算法计算、协议解析。低优先级大数字赋予非实时或后台任务如数据记录、状态显示更新。特别注意要避免“优先级反转”。比如一个低优先级任务持有一个互斥锁Mutex一个高优先级任务试图获取这个锁时就会被阻塞直到低优先级任务释放。如果此时有一个中优先级任务就绪它甚至会抢占低优先级任务导致高优先级任务被无限期阻塞。解决方法是使用“优先级继承”功能的互斥量UCOSIII的互斥量支持此功能。4.2 创建第一个任务LED闪烁让我们从最简单的任务开始。在main()函数中初始化硬件和UCOSIII内核后创建任务。int main(void) { OS_ERR err; // 1. 硬件初始化 System_Init(); // 系统时钟、GPIO、串口等 // 2. 初始化UCOSIII内核 OSInit(err); // 3. 创建起始任务通常具有较高优先级用于创建其他所有应用任务 OSTaskCreate((OS_TCB *)AppTaskStartTCB, (CPU_CHAR *)App Task Start, (OS_TASK_PTR )AppTaskStart, (void *)0, (OS_PRIO )2, // 较高优先级 (CPU_STK *)AppTaskStartStk[0], (CPU_STK_SIZE )APP_TASK_START_STK_SIZE / 10, (CPU_STK_SIZE )APP_TASK_START_STK_SIZE, (OS_MSG_QTY )0, (OS_TICK )0, (void *)0, (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), (OS_ERR *)err); // 4. 启动多任务调度 OSStart(err); while(1); // 正常情况下不会执行到这里 }AppTaskStart函数里我们创建真正的应用任务比如LED任务void AppTaskStart (void *p_arg) { OS_ERR err; (void)p_arg; // 创建LED闪烁任务 OSTaskCreate(TaskLedTCB, Task Led, TaskLed, (void*)0, 10, TaskLedStk[0], TASK_LED_STK_SIZE/10, TASK_LED_STK_SIZE, 0, 0, 0, OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR, err); // ... 创建其他任务 while (1) { OSTimeDlyHMSM(0, 0, 1, 0, OS_OPT_TIME_HMSM_STRICT, err); // 起始任务自身可以延时或执行其他管理功能 } }LED任务函数TaskLed就是一个典型的无限循环使用OSTimeDly()进行延时实现闪烁。void TaskLed (void *p_arg) { OS_ERR err; (void)p_arg; while (1) { gpio_bit_write(LED_PORT, LED_PIN, SET); // 点亮LED OSTimeDly(500, OS_OPT_TIME_DLY, err); // 延时500个系统节拍500ms gpio_bit_write(LED_PORT, LED_PIN, RESET); // 熄灭LED OSTimeDly(500, OS_OPT_TIME_DLY, err); // 延时500ms } }编译下载后你应该能看到LED按照预定的节奏闪烁而此时CPU并非在空等而是在执行其他就绪的任务或空闲任务。这就是多任务并发的雏形。4.3 任务间通信以串口数据收发为例单一任务意义不大任务间协作才是关键。假设我们有两个任务TaskCommRx串口接收和TaskDataProcess数据处理。接收任务负责从串口中断中收集数据处理任务负责解析。它们之间需要一个消息队列Queue来传递数据。定义消息队列#define COMM_QUEUE_SIZE 20 // 队列能存储20条消息 OS_Q CommQueue; // 消息队列控制块 CPU_CHAR CommQueueSto[COMM_QUEUE_SIZE * sizeof(USART_RX_PACKET)]; // 队列存储区USART_RX_PACKET是你自定义的数据包结构体。创建消息队列在起始任务或初始化函数中创建。OSQCreate(CommQueue, UART Comm Queue, COMM_QUEUE_SIZE, CommQueueSto[0], sizeof(CommQueueSto), err);接收任务生产者在串口中断服务程序ISR中收到一帧完整数据后不要进行复杂处理仅将数据包放入队列。注意在ISR中调用OS提供的函数必须使用OSIntEnter()和OSIntExit()包裹并且使用OS_OPT_POST_FIFO等带_ISR后缀的选项。void USART0_IRQHandler(void) { OS_ERR err; OSIntEnter(); // 进入中断 // ... 串口中断处理组装数据包 p_packet // 将数据包投递到队列在中断中 OSQPost((OS_Q *)CommQueue, (void *)p_packet, (OS_MSG_SIZE )sizeof(USART_RX_PACKET), (OS_OPT )OS_OPT_POST_FIFO | OS_OPT_POST_ALL, (OS_ERR *)err); OSIntExit(); // 退出中断可能触发任务调度 }处理任务消费者这是一个独立的任务持续等待队列中的消息。void TaskDataProcess (void *p_arg) { OS_ERR err; USART_RX_PACKET *p_rx_pkt; while (1) { // 等待消息无限期等待 p_rx_pkt (USART_RX_PACKET *)OSQPend(CommQueue, 0, OS_OPT_PEND_BLOCKING, msg_size, ts, err); if (err OS_ERR_NONE) { // 成功收到消息进行数据处理 process_packet(p_rx_pkt); // 处理完后注意释放内存如果数据包是动态分配的 } } }通过消息队列我们完美解耦了数据接收中断上下文和数据处理任务上下文。接收中断只负责快速投递处理任务可以安心进行复杂的解析运算互不干扰。即使处理任务暂时繁忙数据也会在队列中缓存不会丢失。5. 系统调试、优化与问题排查系统跑起来后真正的挑战才开始。如何让它跑得稳、跑得好5.1 利用UCOSIII内置调试与统计功能UCOSIII提供了强大的运行时诊断工具这是裸机编程难以比拟的优势。任务状态查看通过调用OSTaskStkChk()可以检查每个任务的栈使用情况。我通常在系统稳定运行一段时间后在调试串口或通过SEGGER SystemView等工具输出所有任务的栈高水位线据此精确调整栈大小避免浪费或溢出。CPU使用率统计在os_cfg_app.h中启用OS_CFG_STAT_TASK_EN和OS_CFG_STAT_TASK_STK_CHK_EN内核会自动创建一个统计任务OS_StatTask它定期计算CPU总使用率和各任务的使用率。通过OSStatTaskCPUUsage这个全局变量可以获取0-10000对应0%-100%。我发现一个任务如果长期CPU使用率超过80%就需要考虑是否将其拆分或优化算法。系统运行时信息OSRunning系统是否运行、OSIntNestingCtr中断嵌套计数等全局变量在调试死机、卡死问题时非常有用。5.2 常见问题与排查实录在GD32F103上跑UCOSIII我遇到过几个典型问题系统启动后直接进入HardFault可能原因1栈空间不足或溢出。这是最常见的原因。特别是中断栈在启动文件中定义和任务的栈。检查启动文件中的Stack_Size是否足够我一般设为1024或2048。使用任务栈检查功能确认应用任务栈是否够用。可能原因2移植层汇编代码与编译器不兼容。确保os_cpu_a.asm使用的是正确的汇编语法Keil ARM Assembler。有时需要检查OS_CPU_PendSVHandler的PRESERVE8和THUMB指令。可能原因3系统时钟节拍SysTick配置错误。OS_CPU_SysTickClkFreq()返回的频率必须准确cnts计算值不能溢出。可以在OS_CPU_SysTickInit函数内设置断点检查传入的cnts值是否合理。排查方法在HardFault_Handler中断函数中读取SCB-CFSR配置故障状态寄存器、SCB-HFSR硬故障状态寄存器以及SCB-MMFAR/SCB-BFAR内存管理/总线故障地址寄存器可以精确定位故障原因。网上有现成的HardFault诊断函数可以借鉴。任务调度不工作只有高优先级任务在运行可能原因没有正确启用时间片轮转调度。在os_cfg_app.h中确保OS_CFG_SCHED_ROUND_ROBIN_EN设置为1。并且在创建任务时或者之后调用OSSchedRoundRobinCfg()函数来配置时间片长度。即使优先级相同如果没有时间片轮转一个任务不主动放弃CPU如调用OSTimeDly其他同优先级任务也无法运行。解决方法在系统初始化后调用OSSchedRoundRobinCfg((OS_BOOLEAN)DEF_ENABLED, (OS_TICK )1, // 每个任务默认时间片长度节拍数 (OS_ERR *)err);中断响应变慢或丢失可能原因在中断服务程序ISR中调用了可能引起任务调度的UCOSIII函数如OSFlagPost,OSQPost等但没有使用OSIntEnter()和OSIntExit()包裹。或者在ISR中执行了过于耗时的操作。黄金法则ISR要快进快出。只做最必要的操作如读取数据、清除标志然后将事件通过信号量、消息队列等方式发送给任务去处理。务必使用OSIntEnter()和OSIntExit()。系统运行一段时间后死机可能原因1堆栈溢出。这是最隐蔽的问题。除了检查任务栈还要注意是否在中断或任务中定义了非常大的局部数组例如uint8_t buffer[1024]这很容易导致栈溢出。可能原因2资源竞争导致死锁。两个任务互相等待对方持有的互斥锁。设计时要避免嵌套申请多个锁或者规定统一的锁申请顺序。可能原因3内存泄漏。如果使用了动态内存malloc或UCOSIII内存池申请后没有释放。排查方法使用调试器观察死机时的PC指针和LR寄存器看卡在哪个函数。结合任务状态和CPU使用率统计分析死机前的系统状态。5.3 性能优化与资源管理心得对于GD32F103这类资源受限的MCU优化是永恒的主题。关掉不必要的调试功能发布版本中将OS_CFG_ARG_CHK_EN,OS_CFG_DBG_EN等调试相关的配置项关闭能显著减少代码尺寸和提高运行速度。使用静态分配尽量避免在运行时动态分配内存malloc/free。所有任务栈、内核对象信号量、队列等都使用全局数组静态创建。这虽然增加了RAM的静态占用但避免了内存碎片和分配失败的风险系统行为更确定。优化任务优先级和调度策略仔细分析每个任务的实时性要求。对于周期性的任务使用OSTimeDly()或OSTimeDlyHMSM()进行延时让出CPU。对于事件驱动的任务使用信号量、事件标志组或消息队列进行等待。避免让任务在就绪态空转while(1)循环中不加延时或等待。中断优先级配置除了SysTick和PendSV其他硬件中断的优先级需要合理配置。UCOSIII管理的中断即会调用OSIntEnter/Exit的中断优先级必须高于SysTick和PendSV否则会破坏内核调度。在GD32中通过NVIC_SetPriority()设置注意数值越小优先级越高。经过这些步骤一个基于GD32F103和UCOSIII的稳定、高效的多任务应用框架就搭建起来了。从点灯到多任务通信再到系统调试优化每一步都需要结合硬件特性和软件原理仔细考量。这个组合为GD32F103赋予了处理复杂应用的能力而清晰的架构也让后续的功能扩展和维护变得轻松许多。在实际项目中我还会根据需要使用软件定时器、事件标志组等更多内核服务但核心的移植和应用模式万变不离其宗。本文还有配套的精品资源点击获取
返回列表