ARTICLE DETAIL

资讯详情

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

FreeRTOS与Zephyr线程优先级对比及迁移避坑指南

FreeRTOS与Zephyr线程优先级对比及迁移避坑指南 做嵌入式这几年只要一聊RTOS十有八九先问“跑几个任务、栈开多大”。其实还有一个更底层的问题——线程优先级。Zephyr和FreeRTOS在这件事上的思路完全不同从FreeRTOS迁到Zephyr时优先级方向几乎是反着来的很多人第一个坑就踩在这里。这篇文章就专门拆开“线程优先级”这个点把两边的模型、源码表现、实际迁移方法、优先级翻转、时间片、SMP场景下的差异一次讲清楚。适合正用STM32CubeMX搭FreeRTOS项目的朋友也适合准备转向Zephyr做多协议/多核项目的开发者哪怕你只是面试前想搞明白任务优先级和中断优先级到底差在哪这篇也能直接当复习资料用。1. 优先级模型的根本差异一个看数字大小一个看正负1.1 FreeRTOS的“数字越大越优先”FreeRTOS的线程优先级规则非常简单直接取值范围从0到configMAX_PRIORITIES - 1数值越大优先级越高。0是最低优先级通常被空闲任务Idle Task占用用户任务一般至少从1开始往上排。比如你在CubeMX里配置一个任务优先级为3、另一个为1那么优先级3的任务只要进入就绪态就能马上抢占优先级1的任务。这个模型在代码层面很好理解TCB里存一个uxPriority字段调度器每次挑选任务时找“uxPriority最大且处于就绪态”的任务。因为数值越大越优先所以它天然适合用位图快速查找最高优先级这也是FreeRTOS能够做到O(1)调度的核心原因之一。实际项目里我见过不少人把优先级理解成“分层越细越好”一个项目里从1到5排了十来个等级。其实FreeRTOS的优先级数量不是越多越好。优先级越多调度器维护就绪链表和位图的开销越大而且高优先级任务过多抢占低优先级任务更容易被“饿死”。官方文档也建议优先级数量够用就行不必追求十几二十个。1.2 Zephyr的“数字越小越优先”与协作/抢占两级体系到了Zephyr这边规则直接反过来数字越小优先级越高。而且它不是单纯“反一下”这么简单因为Zephyr把线程优先级分成两段——负数是协作式Cooperative优先级非负数是抢占式Preemptive优先级。具体来说协作式线程的可配置优先级范围是-CONFIG_NUM_COOP_PRIORITIES到-1抢占式线程的可配置优先级范围是0到CONFIG_NUM_PREEMPT_PRIORITIES - 1。比如你配置CONFIG_NUM_COOP_PRIORITIES2、CONFIG_NUM_PREEMPT_PRIORITIES6那么合法优先级就是-2、-1、0、1、2、3、4、5。其中-2最高5最低。为什么负数反而是最高优先级因为Zephyr调度器统一按“数值最小者先执行”来选线程。负数天然小于所有非负数所以协作式线程整体上排在所有抢占式线程前面。但有个重要前提协作式线程一旦开始运行即使更高优先级的抢占式线程就绪了也不能打断它必须等协作式线程主动调用k_yield()、k_sleep()或者等待某个内核对象时调度器才有机会切换到其他线程。这个设计其实是把“非抢占调度”和“抢占调度”融合进了同一个优先级体系而不是像FreeRTOS那样所有任务默认都是抢占式的。你在FreeRTOS里几乎不会碰到“任务不让出CPU就没法切走”的情况但在Zephyr里如果协作式线程写了个死循环且没有任何阻塞调用整个系统都能被它卡死。1.3 一张表看懂映射关系两边的核心差异可以先压成一张表后面再逐一展开对比项FreeRTOSZephyr优先级方向数字越大越优先数字越小越优先默认最低优先级0空闲任务占用CONFIG_NUM_PREEMPT_PRIORITIES - 1数值最大是否有系统级空闲任务有优先级0有优先级低于所有用户可配优先级协作式线程无单独概念普通任务可被抢占负优先级线程运行后不可被抢占需主动让出抢占式线程所有普通任务非负优先级线程优先级范围0 ~configMAX_PRIORITIES - 1-CONFIG_NUM_COOP_PRIORITIES~CONFIG_NUM_PREEMPT_PRIORITIES - 1动态修改APIvTaskPrioritySetk_thread_priority_set迁移时最粗暴但有效的方法就是做一个“相对等级映射表”。比如FreeRTOS任务优先级1到4对应Zephyr的3、2、1、0保持次序不变即可。后面第三节我会用一个实际STM32工程举例。2. 从源码层面看优先级“藏”在哪里2.1 FreeRTOSTCB优先级与就绪位图FreeRTOS的每个任务核心数据结构叫tskTaskControlBlock简称TCB里面存着当前优先级uxPriority和初始优先级uxBasePriority。uxBasePriority的主要用途是配合互斥量的优先级继承机制在解锁后恢复原来的优先级。这东西在日常应用层开发中看不到但排查优先级翻转问题时会非常有用。就绪状态的任务会按优先级被挂到一组链表数组里也就是pxReadyTasksLists[configMAX_PRIORITIES]同一优先级的多个就绪任务串在同一个链表中。调度器找一个最高优先级任务时会先通过位图找到“当前最高非空优先级”再从这个优先级对应的链表中取出第一个任务。这就是FreeRTOS所谓O(1)调度的时间来源。当你调用vTaskPrioritySet()修改一个任务的优先级时内核会把该任务从原来优先级的就绪链表摘下来改成新优先级后再重新插入对应的链表。如果这个任务本来就在运行而且新优先级比当前运行任务更高内核会立刻请求一次上下文切换。这也是FreeRTOS动态优先级能立即生效的原因。源码层面的另一个关键是portGET_HIGHEST_PRIORITY这类宏。在Cortex-M平台上它通常借助一个32位位图变量用CLZ指令或者查表法快速找到最高优先级。不同MCU移植层可能会微调但思路都一样。如果你只是用CubeMX生成模板一般不关心这些但真正遇到“调度延迟异常”这类问题就得回到这里看是哪个宏拖慢了查找速度。2.2 Zephyrk_thread.prio与统一就绪队列Zephyr的线程内核对象是struct k_thread优先级字段就叫prio一个普通的signed int。创建线程时可以指定优先级比如用K_THREAD_DEFINE宏定义线程时第三个参数就是优先级也可以用k_thread_create()动态创建传入优先级参数。和FreeRTOS“每个优先级一个链表”不同Zephyr内核里维护的是一个统一的就绪队列_ready_q所有就绪线程都塞到这个队列里按优先级排序并用位图辅助快速找到“数值最小且非空的优先级”。从调度算法上说它同样是近似O(1)的但数据结构上比FreeRTOS更统一。由于优先级是int类型源码里可以直接比较比如内核在决定“当前线程还能不能继续跑”时会拿当前线程优先级和就绪队列头部线程优先级做比较。如果就绪队列头部的线程优先级数值更小说明出现了更高优先级的抢占式线程调度器就会触发切换。Zephyr还允许你通过k_thread_priority_set()在运行期修改线程优先级。这个API和FreeRTOS的vTaskPrioritySet()行为不完全一样如果目标线程是抢占式线程且新优先级足够高调度器可能立即重新调度如果目标线程是协作式线程那么即使改了优先级也要等到它主动让出CPU后才可能切换过去。说白了协作式优先级更像“下一次调度时的排序权重”而不是“马上就能抢占的开关”。2.3 动态优先级修改的调度差异很多从FreeRTOS迁到Zephyr的人会把vTaskPrioritySet(task, 3)直接翻译成k_thread_priority_set(thread, 3)觉得结果一样。实际上方向不一样在FreeRTOS里你把任务优先级改成3是“升位”在Zephyr里改成3反而会“降位”。这不是API用法问题而是优先级模型本身的差异。更隐蔽的是协作式线程的优先级修改。在FreeRTOS中不存在“协作式任务不可被抢占”的概念所有任务只要处于就绪态且优先级更高就能切走当前任务。Zephyr里则不同你让一个协作式线程从-2改成-1它依然不会立刻被打断它可能正在一个循环里做密集型计算改完优先级后系统看起来毫无反应直到它主动让出CPU。所以我的建议是凡是涉及动态调整优先级的代码一定要在写注释时标明“当前代码运行在线程上下文还是中断上下文”“目标线程是协作式还是抢占式”。这两个条件决定了下一次调度到底何时发生踩过一次协作式线程的坑以后你会对这句话印象特别深。3. 实际项目中的优先级分配与迁移以STM32LVGL为例3.1 FreeRTOS项目里我惯用的优先级分档先拿一个典型的STM32F407项目举例跑LVGL做界面显示外接温湿度传感器用Wi-Fi模块上报数据再加几个按键输入。这种项目用CubeMX配置FreeRTOS非常顺手任务不会太多我一般这么排优先级任务功能FreeRTOS优先级网络通信任务Wi-Fi数据收发、协议解析4业务逻辑任务传感器数据融合、状态机3LVGL刷新任务调用lv_task_handler或lv_timer_handler2按键/事件派发读取GPIO、投递事件到队列2喂狗与统计任务喂独立看门狗、打印任务栈水位1空闲任务系统自动创建0这里我特意把网络通信排到最高因为Wi-Fi模块如果处理不及时缓冲区会被塞满导致丢包LVGL刷新排到中等保证界面不卡但又不至于抢走网络资源喂狗任务排最低因为正常运行时所有任务都应该活着如果优先级高的任务卡死了喂狗任务也得不到执行看门狗才能正确复位系统。这个方案在FreeRTOS下稳定性很好。但如果直接平移去Zephyr不做方向反转LVGL任务排2、网络任务排4那Zephyr会把4当成最低优先级结果就是网络任务饿死Wi-Fi疯狂丢包。你查半天都查不出逻辑问题直到打印线程优先级才发现数值反过来用了。3.2 迁移到Zephyr时的映射表做法在Zephyr上重建这套任务我会先在prj.conf里定义好优先级空间CONFIG_NUM_PREEMPT_PRIORITIES6 CONFIG_NUM_COOP_PRIORITIES2这样Zephyr的抢占式优先级就是0到5协作式优先级是-2和-1。接下来按相对次序做映射原FreeRTOS优先级角色映射到Zephyr4网络通信0抢占最高3业务逻辑1抢占2LVGL刷新2抢占2按键/事件2抢占同LVGL同优先级1喂狗/统计3抢占最低无关键传感器采集-1协作可选用代码写就是K_THREAD_DEFINE(net_thread, NET_STACK_SIZE, net_task, NULL, NULL, NULL, 0, 0, 0); K_THREAD_DEFINE(bus_logic_thread, LOGIC_STACK_SIZE, logic_task, NULL, NULL, NULL, 1, 0, 0); K_THREAD_DEFINE(lvgl_thread, LVGL_STACK_SIZE, lvgl_task, NULL, NULL, NULL, 2, 0, 0); K_THREAD_DEFINE(wdg_thread, WDG_STACK_SIZE, wdg_task, NULL, NULL, NULL, 3, 0, 0);注意映射时不能只看数值要看“相对顺序”。FreeRTOS的4变成Zephyr的0FreeRTOS的1变成Zephyr的3次序完全反过来但任务之间的抢占关系保持原样。如果你有任务需要相当严格的时序控制比如传感器启动序列、电机抱闸控制可以考虑放到协作式优先级-1或-2。但要保证这个任务绝对不能死循环必须周期性地k_sleep()或k_yield()。3.3 最容易踩的“方向反转”坑我见过最典型的低级错误是把FreeRTOS任务优先级原封不动写进Zephyr比如网络任务写4、LVGL任务写2。结果网络任务变成了最低优先级只要LVGL在跑网络就得不到调度。这种问题比逻辑bug更难发现因为它不会直接报错只是系统“变慢”。排查手段其实很简单用串口把每个线程的优先级和状态打出来。FreeRTOS可以用vTaskList()打印Task ListZephyr可以在shell里用kernel threads命令查看线程优先级、状态、栈使用率。两边都有现成工具不要靠猜。另外一个次生坑是“同优先级任务的时间片”。FreeRTOS默认开启时间片同优先级任务之间会自动轮转Zephyr虽然也支持时间片但需要CONFIG_TIMESLICING开启而且默认可能不开启。如果Zephyr里LVGL任务和按键任务都排优先级2但没有开时间片那么两个任务之间如果都不互相yield谁先拿到CPU谁就独占按键任务可能长时间得不到执行。所以迁移后的测试一定要覆盖“同优先级任务是否都能被调度到”这一条。4. 优先级翻转、中断优先级与互斥等待4.1 优先级继承两边都有但别搞混优先级翻转是RTOS开发员必知的问题低优先级任务持有互斥量高优先级任务在等这个互斥量此时中优先级任务一直抢占CPU低优先级任务没法释放锁高优先级任务就被无限拖延。解决手段是优先级继承也就是临时把低优先级任务的优先级提高到等待它的最高优先级任务的级别。FreeRTOS的普通二值信号量不做优先级继承但互斥量Mutex做了。注意这里说的互斥量是指xSemaphoreCreateMutex()创建的那个而不是用二值信号量“手动实现”的互斥。很多新手用二值信号量做临界区保护遇到高优先级任务被饿死就是没用真正的互斥量。Zephyr的k_mutex同样实现了优先级继承。如果你需要多线程共享外设缓冲区、LVGL显示缓冲区这类资源直接用k_mutex而不是k_sem。这里有一个容易混淆的点信号量在某些场景下也能达到互斥效果但它没有优先级继承机制在高并发系统中就是一颗定时炸弹。我自己做项目时凡是保护共享资源的锁一律用k_mutex信号量只用来做事件通知和资源计数。4.2 中断优先级与线程优先级是两套系统“FreeRTOS的任务优先级与中断优先级区别”几乎是面试必问。任务优先级决定的是“就绪任务谁先被调度”而中断优先级是硬件中断控制器Cortex-M的NVIC决定的。任务优先级再高中断来了它也得让路只不过中断服务程序一般很短只做最少量工作把重的活儿通过队列或信号量交给任务去处理。FreeRTOS里中断服务程序里调用API一般要用带FromISR后缀的版本比如xQueueSendFromISR。而且如果中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY这个中断里不能调FreeRTOS API。这个约束很多人会忽略一旦踩到直接跑飞。Zephyr的体系里中断优先级和线程优先级也是两个完全独立的维度。ISR运行在中断上下文里没有任务优先级一说ISR里可以调用部分Zephyr API比如k_sem_give、k_msgq_put但最终唤醒哪个线程还是看线程优先级。你可以用生活常识来记任务优先级是“排队号”中断优先级是“VIP插队卡”两套牌子不是一个体系谁也不能替代谁。4.3 队列、消息队列与看门狗中的优先级陷阱消息队列和看门狗也是优先级问题的高发区。FreeRTOS的队列xQueueSend、Zephyr的k_msgq_put都属于典型的内核对象当多个任务同时等同一个队列时唤醒顺序一般按优先级从高到低排。这本身不算错但会带来一个隐藏问题高优先级任务如果疯狂生产消息低优先级消费者可能一直抢不到队列项。我遇到过的一个实际案例一个传感器任务以200Hz频率往消息队列里塞数据UI任务从队列里取数据刷新界面由于传感器任务优先级高队列几乎总是满的UI任务每次只能拿到最新几条触控响应变得特别“飘”。最后把UI任务优先级提上去再把传感器采集任务和数据处理任务之间做了“只保留最新值”的覆盖策略问题才解决。看门狗这边喂狗任务本身也是一种任务它的优先级选择很讲究。如果把喂狗任务设得比业务任务还高那就算业务任务卡死喂狗任务照样运行看门狗永远不复位整个监控形同虚设。反过来喂狗任务优先级太低系统稍微繁忙一点就喂不上狗出现误复位。比较稳妥的做法是把喂狗任务优先级放到项目里的“最低业务优先级档位”让它能正常运行但一旦关键高优先级任务卡死它也很快得不到调度从而触发复位。这个思路在FreeRTOS和Zephyr下都一样。5. 时间片、空闲线程与调度器行为差异5.1 同优先级轮转时间片的开关不同FreeRTOS在默认配置下多个同优先级就绪任务会按时间片轮转调度。这个行为由configUSE_TIME_SLICING控制如果把它配置成0同优先级任务之间就不会自动轮转得靠任务自己调用taskYIELD()或者被更高优先级任务打断否则先运行的任务可能一直占着CPU。Zephyr的时间片控制更分散一些。全局上有个CONFIG_TIMESLICING开关还有CONFIG_TIMESLICE_SIZE之类配置项。而且Zephyr的时间片只对“抢占式且非负优先级”的线程生效协作式线程没有时间片概念必须主动让出。这一点其实是Zephyr把“协作式”贯彻到底的结果既然你选择了不可被抢占那么时间片也不应该再强制切割你。所以迁移时要注意FreeRTOS默认同优先级任务可以自动轮转Zephyr如果没开时间片或者时间片配置不合适同优先级任务在必要时必须自行k_yield()。在一些Zephyr老版本里还有CONFIG_TIMESLICE_SIZE单位从tick改成毫秒之类的变化升级内核时要去查版本迁移文档。5.2 协作式线程的“让权”义务Zephyr的协作式线程是很多人从FreeRTOS迁移后最容易翻车的地方。协作式线程一旦进入运行态调度器不会因为其他更高优先级线程就绪而把它切走。也就是说如果你在一个协作式线程里写了类似这样的代码while (1) { /* 大量计算 */ }那么这个线程会独占CPU其他所有线程哪怕优先级是抢占式0也全部得不到调度直到系统看门狗复位。正确做法是协作式线程的每个主要循环节点都要主动让出CPU常见手段包括调用k_yield()主动放弃当前时间片调用k_sleep()进入睡眠指定时间调用k_sem_take()、k_mutex_lock()等阻塞API等待内核对象调用k_msgq_get()等队列接收API等待数据到达。协作式线程最适合的场景是某段代码希望原子地执行多个操作又不希望被其他线程打断。你把它放到负优先级保证调度器优先选择它它运行期间又不会被打断相当于实现了“用户态的临界区”。但它绝不能一直“不退位”。5.3 谁在处理空闲线程FreeRTOS的空闲任务优先级是0在普通任务未全部就绪时占用CPU。你可以用vApplicationIdleHook()往空闲任务里挂一些低优先级的工作比如系统进入低功耗模式的检查、CPU使用率统计。有一点要注意空闲任务里绝对不能调用任何阻塞API否则系统调度器可能失去“兜底”线程。Zephyr也有空闲线程但它的优先级被定义得比所有用户可配置优先级都低而且一般不建议用户往里面塞业务逻辑。Zephyr的思路是让空闲线程自动执行CPU低功耗指令配合电源管理子系统来做节能。用户想统计CPU占用率可以用Zephyr的内置监测或定时器。你不需要也不应该干预空闲线程内容因为内核调度器选择空闲线程意味着“此刻确实没有任何用户线程可以运行”。6. 多核与SMP优先级是不是还够用6.1 FreeRTOS SMP的优先级处理如果你在TC387这类多核MCU上跑FreeRTOS用的多半是带SMP扩展的FreeRTOS版本。多核SMP下全局就绪队列是所有核心共享的多个核同时修改就绪队列或优先级位图就必须要引入调度器锁。官方SMP版本已经做了这部分工作但使用时的注意事项不少。典型问题是两个核心可能同时运行两个同优先级的就绪任务这在单核下是不可能的但在SMP下是常态。所以“全局最高优先级任务一定在跑”这个经验在多核下会变复杂它可能正在核0上跑而核1跑着一个稍低优先级的任务。要考虑真正的实时性必须配合CPU亲和力把关键任务绑定到指定核心。FreeRTOS SMP的优先级方向没有变依然是数字越大越优先。但涉及动态优先级修改时vTaskPrioritySet()在多核环境下可能触发多个核心的调度器操作如果中断屏蔽和锁没有处理好会出现就绪链表错乱。遇到这类问题优先排查是不是在中断服务程序里改了任务优先级。6.2 Zephyr原生SMP的优先级与CPU亲和Zephyr对SMP的支持是内核原生的配置CONFIG_SMPy后可以搭配k_thread_cpu_pin()或k_thread_cpu_mask()这类API控制线程跑在哪个核上。线程优先级本身仍是全局的数字越小越优先的规则不会因为多核而改变。Zephyr的多核调度器会保证每个空闲核尽量从全局就绪队列中取最高优先级线程运行。这里一个容易忽略的点是Zephyr的协作式线程在多核下依然不可被抢占但它占用的只是“当前核”另一个核还是可以运行其他任务的。所以在多核Zephyr系统里协作式线程导致“整个系统卡死”的可能性比单核小但也会占住一个核不放造成CPU资源分配不均。如果你的项目要从FreeRTOS单核迁到Zephyr多核建议先把优先级映射做好再考虑CPU亲和力。很多人在单核上调得好好的优先级表到多核下乱了是因为没分清“线程优先级”和“核心绑定”是两种资源控制手段。优先级管的是抢CPU先后亲和力管的是允许用哪个CPU两者叠加才是完整的调度画像。7. 优先级问题排查与常见坑7.1 高优先级任务饿死其他任务怎么定位高优先级任务饿死低优先级任务最典型的症状是低优先级任务“完全不动”但系统没有死机看门狗也不复位。因为高优先级任务一直在正常执行、正常喂狗只是低优先级任务没机会跑。FreeRTOS下可以用vTaskList()或uxTaskGetSystemState()把每个任务的状态、优先级、栈水位打出来重点看某几个任务是否长期停留在“Ready”但从未“Running”。Zephyr下最方便的是接上shell敲kernel threads命令观察每个线程的“prio”和“state”。如果某个低优先级线程一直是thread pending或者ready但CPU时间几乎为零基本可以判定是饿死。定位到之后优先检查两件事一是它等的事件是否真的在发生二是它的优先级和上游事件产生任务的优先级关系。很多时候不是优先级本身错了而是消息产生者优先级太高直接把消费者堵死了。7.2 协作式线程占死CPU的现场Zephyr里协作式线程占死CPU的现场很容易识别系统看起来“死了”但如果你接了调试器敲暂停会发现程序停在一个你完全没预料到的循环里更新PC指针一看就是某个协作式线程的while(1)。FreeRTOS不会有这种问题因为所有任务都是抢占式的只要tick中断还在任务最终会被切走。我的排查经验是遇到Zephyr系统卡死先不要在业务流程里找bug先看线程状态。如果卡住的线程是负优先级十有八九是协作式线程没让出CPU。快速验证方法是在那个循环里临时加一个k_sleep(K_MSEC(1))如果系统“复活”说明就是它占死了CPU。后续再根据实际逻辑设计合理的让出点。7.3 栈溢出、看门狗与优先级的关系栈溢出和优先级看着不相关实际关联很深。高优先级任务频繁抢占会让低优先级任务在被抢占时反复保存现场栈的使用水位比“连续运行”时要高。如果低优先级任务栈本来就给得紧高压调度下更容易溢出。FreeRTOS可以开启configCHECK_FOR_STACK_OVERFLOW在栈溢出钩子里抓现场也可以用uxTaskGetStackHighWaterMark()看任务栈剩余量。Zephyr在Cortex-M上可以开CONFIG_MPU_STACK_GUARD线程一旦越界访问会触发MPU异常比裸奔更好排查。看门狗的问题也经常和优先级绑定在一起。我见过一个案子系统偶尔复位看门狗任务优先级设得比通信任务低通信任务在某个异常分支里进入了长循环喂狗任务一直得不到执行看门狗复位。看起来是“通信任务卡死导致复位”本质是优先级和看门狗设计配合的问题。后来把喂狗任务优先级提了一档同时在通信任务超时分支里主动taskYIELD()复位问题就消失了。这是典型的“用优先级策略喂狗”案例。7.4 面试与学习向优先级相关高频题如果你是在准备面试下面几个问题值得提前吃透FreeRTOS任务优先级和中断优先级的区别是什么FreeRTOS中优先级数值越大越优先Zephyr中相反为什么Zephyr要这样设计什么是优先级反转互斥量的优先级继承机制怎么解决这个问题Zephyr的协作式线程为什么不能被抢占中断能打断它吗前两个问题考的是基础第三个考的是并发意识第四个考的是对调度模型的理解。把这些理解了很多RTOS面试题其实都是同一套底层逻辑。你要是已经把前面的章节读完这几个问题应该都能用自己的话答出来。最后再分享一个我自己的习惯所有RTOS项目的优先级定义一定要集中收敛到一个头文件里别散落在各个任务的实现中。跨平台迁移时只要改这个映射头文件其他代码几乎不用动。这也是我从FreeRTOS迁到Zephyr后最受益的一招越大的项目越明显。每次写新任务第一件事不是写逻辑而是去优先级那个表里找位置想清楚它应该压谁、让谁、和谁平级。优先级这东西设计时多想一分钟调试时少熬十小时。
返回列表