☰
裸机编程的中间路线:定时器模拟任务架构详解
2026/10/10 13:08:49 网站建设 项目流程

很多做嵌入式开发的朋友,都会在裸机编程里遇到同一个坎:想让多个任务“同时”工作,但代码写到最后总是变成一团乱麻。按键扫描不能卡住 LED 闪烁,串口打印又不能影响电机控制,一个地方用了延时,整个系统的节奏全被打乱。这个时候,有人会告诉你“上 RTOS 吧”,但对于很多资源受限、项目周期紧、或者场景本来就不复杂的项目来说,引入 RTOS 往往是大炮打蚊子,反而增加了调试和排错的复杂度。

那有没有一条中间路线?当然有,这就是本文要讲的:定时器模拟任务。简单说,就是用定时器中断作为“心脏”,把原本在超级大循环里干等延时的任务,拆成一个个小的状态机,在固定的时间片里轮转执行。它算不上真正的多任务,但能用最小的成本,让裸机程序具备多任务并存的效果。这是一个非常经典的嵌入式软件设计架构思路,也是许多老工程师在裸机项目里的首选方案。

这篇文章会把概念、原理、代码、坑点一次性讲透。你可以把这套思路直接用在 STM32、51、ESP32 等主流单片机上。读完你会明白:什么时候该用大循环,什么时候该用模拟任务,以及怎么用代码把它落到真实项目里。

1. 先回答一个关键问题:为什么需要模拟任务架构

先看一个很典型的场景。假设你正在做一个智能小车项目,主控是 STM32F103。需求看起来并不复杂:LED 每隔 200ms 闪烁一次,按键按下后切换电机转速,OLED 屏幕每 100ms 刷新一次数据,串口每 500ms 打印一次运行状态。

用最直觉的写法,是下面这样的:

while (1) { LED_Blink(); // 里面用了 delay(200) Key_Scan(); // 里面有延时消抖 OLED_Refresh(); // 刷新屏幕,耗时可能 10ms UART_SendStatus(); // 打包发送,耗时不确定 }

问题在哪里呢?只要Key_Scan()里有个 10ms 的延时,LED 的 200ms 闪烁周期马上就会被拉长,变成 210ms 多。OLED 刷新如果耗时太长,串口发送就会滞后。更严重的是,如果你在一段代码里用了delay(1000),整个系统在这一秒里基本等于“死机”。所有任务都在一个循环里排队,谁耗时多,谁就拖慢所有人。这就是最典型的超级大循环(Super Loop)架构。

超级大循环的痛点,总结起来就是三个:

痛点具体表现后果
阻塞耗时任意一个任务的延时或等待都会卡住整个循环系统实时性极差
耦合严重所有任务代码都挤在 main 函数的 while 里代码阅读、维护困难
扩展性差新增一个功能就得往循环里再塞一段循环越来越长,越来越乱

那为什么不用 RTOS 呢?因为很多项目根本不需要。任务只有三五个,内存只有几十 KB,产品生命周期里也不会出现大型的业务逻辑。为了这么点需求引入 FreeRTOS,意味着你要学信号量、队列、任务优先级,还可能要处理优先级反转、临界区保护这些复杂问题。开发成本和 bug 出现的概率,反而更高。

所以,定时器模拟任务架构就是那个恰到好处的中间方案。它不引入实时操作系统,却能让每个任务拥有相对独立的“执行节奏”。它在架构层解决的核心问题,不是“并发”,而是**“定时”和“分片”**。

2. 定时器的本质:嵌入式系统的“心跳”

要理解模拟任务,必须先理解定时器的角色。定时器在嵌入式系统里的地位,相当于人的心跳。它负责给整个系统提供一个稳定的时间基准,所有任务都靠这个基准来安排自己什么时候干活。

2.1 硬件定时器与软件定时器的区别

先做一个概念区分,很多初学者会把这两者搞混。

硬件定时器是芯片内部的一个外设,本质是一个计数器。它依赖时钟源,自己数数,数到设定值之后触发中断或者翻转引脚。它不消耗 CPU 资源,是“硬件层面”的时间计量工具。STM32 里的 TIM2、TIM3,51 单片机里的 Timer0、Timer1,都是硬件定时器。

软件定时器则完全不同。它是基于硬件定时器中断,在软件层做出来的“虚拟定时器”。典型做法是:硬件定时器每隔 1ms 产生一次中断,中断服务函数里对几个全局变量做++操作。每个变量代表一个定时器,谁计数到目标值就置位一个标志位,主循环检查到这个标志位后执行对应的任务。

模拟任务架构真正依托的,是软件定时器。硬件定时器只是它的底座。

2.2 定时器中断的执行机制

当硬件定时器计数到设定值,CPU 会暂停当前执行的代码,跳转到中断服务函数(ISR)里执行中断逻辑。执行完再返回暂停的位置。

这带来一个非常重要的特性:**中断可以打断主循环。**哪怕主循环里正在跑一个很耗时的算法,定时器中断依然能够准时触发。这就是模拟任务架构能够脱离超级大循环的根本原因。

不过在中断里执行代码,有两条铁律必须遵守:

  • 中断服务函数必须尽量短,能不做的事绝不在中断里做。
  • 中断里不能调用带延时、阻塞、或者非重入的函数,比如HAL_Delay()、printf()这类函数,绝对不能出现在中断里。

很多新手第一次写模拟任务架构,就是栽在这里:他直接在定时器中断里调用HAL_Delay(5),结果中断没退出,主循环进不去,看起来比超级大循环还卡。正确做法是:中断里只做标记,主循环里做任务。

3. 模拟任务架构的核心设计思路

现在进入正题。定时器模拟任务,本质上是一种协作式时间片轮转调度。它把 CPU 时间分成一小片一小片,每一片给一个任务用。任务执行完必须主动让出 CPU,主循环才能继续调度下一个任务。

3.1 时间片复用思想

时间片复用,听起来高级,其实非常好理解。我们用一个生活场景来类比。

假设你是一家奶茶店的老板,手上只有一个员工。这个员工一小时内要做三件事:煮珍珠、打奶盖、招呼客人。如果他在煮珍珠时,一定要全程盯着锅看 20 分钟,那他这 20 分钟里什么也干不了。但如果他改用“先烧水,再去招呼客人,5 分钟后再回来放珍珠”的方式,就能把时间复用起来,三件事都能推进。

嵌入式里的定时器模拟任务就是同一个思路。LED 闪烁任务不需要 CPU 一直盯着,它只需要在到达 200ms 时间点的时候,翻转一次引脚电平。所以 CPU 完全可以在这 200ms 里去做按键扫描、串口发送这些事情。这就是“分时复用”。

3.2 从“轮询”到“事件驱动”的转变

超级大循环是典型的轮询架构:每个循环都去问“按键按下了吗?时间到了吗?数据来了吗?”。不管有没有事件发生,每个任务都被完整执行一遍。

模拟任务架构则迈向了事件驱动的雏形。它的运行逻辑变成:

  • 定时器中断产生时间事件。
  • 主循环检查各个时间标志。
  • 只有标志被置位的任务才执行。

这个转变非常关键。它意味着,一个任务没有到执行时间时,主循环根本不会进入它的函数体,CPU 不会为它浪费任何指令周期。这也是模拟任务架构比大循环更省资源的原因。

从行业趋势来看,很多嵌入式工程师都在从“超级大循环”走向“事件驱动”,这确实是嵌入式架构升级的一道分水岭。但这里要说清楚:模拟任务只是事件驱动的一种简化实现,它不涉及操作系统的任务切换,也不涉及优先级抢占,它依靠的是“每个任务主动检查自己的执行条件”。

3.3 模拟任务的三要素

一个完整的模拟任务,由三个要素组成:

第一,时间标志。每个任务都有一个关联的运行周期。比如 LED 任务是 200ms,按键扫描是 10ms。在定时器中断里,用计数器对周期做累加,到达周期后置位标志位。这是任务的“闹钟”。

第二,状态机。每个任务内部必须拆成几个状态,一次只处理一个状态。这是任务的“执行逻辑”。

第三,非阻塞执行。执行任务时不能有阻塞等待。如果任务执行到一半需要等待外部条件,应该记录当前状态,先退出函数,等下一次调度时再继续。这是任务的“纪律”。

可以把这三要素理解成:闹钟(时间标志)+ 剧本(状态机)+ 演员守则(非阻塞)。三者齐全,一个任务才能真正融入模拟任务架构。

4. 环境准备与前置条件

纸上谈兵没有意义,下面用一个可运行的工程来演示。为了让示例尽量通用,这里以 STM32F103 系列芯片为硬件平台,开发环境选择 STM32CubeIDE 或者 Keil MDK 都可以,核心代码并不依赖某个特定 IDE。

建议前置条件如下:

  • 一块 STM32F103 最小系统板(C8T6 就够用)。
  • 一个 LED 连接到 PC13(很多开发板板载 LED 就是这个引脚)。
  • 一个按键连接到 PA0。
  • STM32CubeMX 生成基础工程,开启 SysTick 或 TIM2 作为时基。

需要说明的是,本示例不依赖 HAL 库的HAL_Delay(),只使用 HAL 库的定时器中断回调机制。如果你想用手工配置寄存器的方式,原理完全一致,只是代码写法不同。本文重点演示的是通用的架构思路。

如果使用 STM32CubeMX,需要配置的内容如下:

  • 时钟源:内部 8MHz 或外部晶振均可,毕竟我们只用定时器计时,对主频不敏感。
  • 定时器:使能 TIM2,预分频和自动重载值根据你的时钟频率计算,目标是让更新中断频率为 1kHz,也就是 1ms 进一次中断。
  • 中断:使能 TIM2 的全局中断。
  • GPIO:PC13 设为输出,PA0 设为输入,可配下拉电阻。

5. 完整代码实现:一个三任务并行的模拟调度器

下面直接给出一套完整代码。这个示例包含三个模拟任务:LED 闪烁(周期 200ms)、按键扫描(周期 10ms)、串口状态打印(周期 500ms)。

5.1 任务定义与全局变量

先定义任务控制结构。这里不需要像 RTOS 那么复杂,每个任务只需要三个字段:运行周期、计数器、时间标志。

// 文件路径:user/task.h #ifndef TASK_H #define TASK_H #include "main.h" #define TASK_NUM 3 typedef struct { uint16_t period; // 任务运行周期,单位 ms uint16_t counter; // 倒计时计数器 uint8_t flag; // 时间到标志,置 1 表示该执行任务了 } SoftTimer_TypeDef; extern SoftTimer_TypeDef softTimer[TASK_NUM]; #define TASK_LED 0 #define TASK_KEY 1 #define TASK_UART 2 void SoftTimer_Init(void); void SoftTimer_Proc(void); void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim); #endif
// 文件路径:user/task.c #include "task.h" #include "gpio.h" #include "usart.h" SoftTimer_TypeDef softTimer[TASK_NUM]; // 初始化三个软件定时器 void SoftTimer_Init(void) { softTimer[TASK_LED].period = 200; softTimer[TASK_LED].counter = 0; softTimer[TASK_LED].flag = 0; softTimer[TASK_KEY].period = 10; softTimer[TASK_KEY].counter = 0; softTimer[TASK_KEY].flag = 0; softTimer[TASK_UART].period = 500; softTimer[TASK_UART].counter = 0; softTimer[TASK_UART].flag = 0; }

这里用一个数组来管理多个任务,后续要新增任务,只需要扩展数组长度,再加上任务各自的函数实现即可。这种设计的好处是,调度逻辑是通用的,不需要为每个新增任务重复写一套。

5.2 定时器中断服务函数

定时器中断服务函数是整个架构的“心跳源”。它的作用很简单:每隔 1ms 对每个任务的计数器做一次减一操作,减到 0 就重新装载周期值,并置位时间标志。

// 放在 stm32f1xx_it.c 或由 HAL 库回调到本文件 void SysTick_Handler(void) { HAL_IncTick(); } // 这里由 HAL 库统一回调,TIM2 更新事件会进入本函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { uint8_t i; for (i = 0; i < TASK_NUM; i++) { if (softTimer[i].counter > 0) { softTimer[i].counter--; } if (softTimer[i].counter == 0) { softTimer[i].flag = 1; softTimer[i].counter = softTimer[i].period; } } } }

这段代码有什么讲究呢?注意,中断里只对计数器和标志位做操作,不执行任何业务逻辑。LED 翻转、按键读取、串口发送,这些全部被放到主循环里的SoftTimer_Proc()中。延迟 1ms 进一次中断,中断里是几行简单的比较和赋值语句,整个中断耗时可以控制在几微秒以内,完全不会影响主流程。

5.3 主循环调度器

// 文件路径:user/task.c (续) void SoftTimer_Proc(void) { if (softTimer[TASK_LED].flag == 1) { softTimer[TASK_LED].flag = 0; HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); } if (softTimer[TASK_KEY].flag == 1) { softTimer[TASK_KEY].flag = 0; Key_ScanTask(); } if (softTimer[TASK_UART].flag == 1) { softTimer[TASK_UART].flag = 0; UART_ReportTask(); } }

这个调度函数非常朴素:检查标志位,执行任务,清标志位。但它已经具备了一个微型调度器的雏形。主循环只要不断调用SoftTimer_Proc(),就能保证每个任务按自己的节奏运行。

5.4 任务函数:按键扫描状态机

按键扫描是模拟任务里最典型的一个例子。如果不做状态机,按键消抖就要用delay(10),而这个延时一旦进入任务函数,其他任务也会跟着卡住。所以这里使用一个简单的两状态状态机。

// 文件路径:user/task.c (续) static void Key_ScanTask(void) { static uint8_t keyState = 0; switch (keyState) { case 0: // 空闲状态 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) { keyState = 1; // 发现按键可能按下,进入确认状态 } break; case 1: // 消抖确认状态 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) { // 确实按下了,执行一次业务处理 keyState = 0; // 这里可以设置电机转速等全局变量 } else { keyState = 0; // 抖动,取消本次按键 } break; default: keyState = 0; break; } }

为什么需要状态机?因为这个函数每次只被调用一次,一次只执行一个状态。按下、消抖、确认,这个流程被拆到了多次调用中完成。在第 1 次调用时发现按键按下,不做延时,直接跳到确认状态;第 10ms 后第 2 次调用,再来看按键是否仍然按下。这样就把原本需要“延时 10ms”的阻塞逻辑,转换成了“分次检查”的非阻塞逻辑。

5.5 任务函数:串口状态上报

串口打印任务如果直接用printf,在某些单片机配置下会阻塞运行,尤其当串口外设忙时,CPU 会一直在等待。更稳妥的做法是准备一个环形缓冲区,把要发送的数据放进去,由串口中断或者 DMA 真正去传输。

// 文件路径:user/task.c (续) static void UART_ReportTask(void) { static uint8_t tick = 0; char buf[32]; tick++; sprintf(buf, "tick = %d\r\n", tick); // 这里使用非阻塞方式发送,具体函数视你的串口驱动而定 UART_SendString_DMA((uint8_t *)buf, strlen(buf)); }

关键点是:sprintf在任务函数里执行没问题,但UART_SendString_DMA必须是非阻塞的。也就是说,它把缓冲区地址和长度交给 DMA 控制器后,立即返回,实际发送在后台进行。如果怕地址失效,可以把缓冲区定义为静态数组。

5.6 主函数

// 文件路径:Core/Src/main.c (关键部分) int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); MX_USART1_UART_Init(); // 初始化软件定时器 SoftTimer_Init(); // 启动 TIM2,开始产生 1ms 中断 HAL_TIM_Base_Start_IT(&htim2); while (1) { // 主循环只做一件事:让调度器去检查任务 SoftTimer_Proc(); } }

整个 main 函数非常干净,这是模拟任务架构对比超级大循环最有说服力的地方。主循环不需要知道具体任务怎么实现,它只需要不断地调用SoftTimer_Proc(),让调度器去决定“现在执行哪个任务”。

6. 运行结果与效果验证

把上面的代码烧录到开发板,你会看到以下现象:

  • 板载 LED 以约 200ms 周期翻转,也就是 5Hz 左右在闪烁。这个闪烁频率稳定,不受按键按下次数影响。
  • 按下 PA0 按键,任务会执行一次按键业务逻辑,但不会导致 LED 闪烁中断或变慢。
  • 串口每隔 500ms 会输出一条tick = x的消息,格式稳定,与按键动作无关。

如果一切正常,说明三个任务已经实现了“逻辑并行”。为了进一步确认调度效果,你还可以故意在UART_ReportTask()里加一段HAL_Delay(50),然后观察 LED。你会发现 LED 的闪烁开始出现不稳定的抖动。这恰好说明了一个问题:模拟任务架构虽然能把大循环里的“干等延时”拆掉,但如果任务函数内部再次引入阻塞操作,整个调度照样会被卡住。所以架构只管“定时调度”,真正要控制的是每个任务内部的执行纪律。

验证时建议再加一个辅助手段:在SoftTimer_Proc()的循环里加一个翻转引脚 GPOID 的操作,用示波器测量这个引脚的波形。如果调度顺畅,你会看到主循环的执行频率是稳定的;如果出现卡顿,波形会立刻显示出毛刺。

如果发现某个定时器不工作,按下面的顺序排查:

  1. 检查 TIM2 是否真正启动了HAL_TIM_Base_Start_IT(&htim2)。漏掉这一句,所有软件定时器都不会跳动。
  2. 检查HAL_TIM_PeriodElapsedCallback是否被正确触发,可以在回调里临时翻转一个调试引脚确认。
  3. 检查主循环是否调用了SoftTimer_Proc()。如果主循环里自己写了一个HAL_Delay(1000)全局阻塞,定时器中断可能照常触发,但主循环里SoftTimer_Proc()没有机会执行,任务标志位全被置位,却无人消费。

7. 常见问题与排查思路

模拟任务架构本身不复杂,但在实际工程里,新手遇到的问题往往集中在几个固定的地方。这里整理成一个表格,方便你按图索骥。

问题现象可能原因排查方式解决方案
任务完全不动定时器更新中断没有开启检查HAL_TIM_Base_Start_IT是否调用调用HAL_TIM_Base_Start_IT(&htim)启动中断
任务执行时间明显不准任务函数内使用了阻塞延时查看任务代码是否含HAL_Delay把阻塞延时改为状态机或删掉
两个任务同时抢占一个变量任务里访问全局变量时未考虑中断检查中断与主循环是否读写同一变量中断只置标志位,变量在主循环里读写
进中断后系统卡死中断服务函数中调用了不可重入函数检查回调代码是否干净保证 ISR 里只做标志位操作
按键扫描偶尔失灵消抖逻辑未正确处理抖动打印按键状态,观察状态切换节奏增加状态机,至少两次确认
串口数据乱码缓冲区地址失效或发送未完成检查 DMA 回调,确认发送完成标志使用静态全局缓冲区,用环形队列保证数据完整性

这里要特别强调第一类问题。很多人在 CubeMX 里配置好了 TIM2,以为它就会自动工作。实际上,TIM2 初始化后默认是关闭状态的,必须调用HAL_TIM_Base_Start_IT(&htim2),更新中断才会真正使能。这一步是新手最容易漏掉的。

8. 最佳实践与工程建议

定时器模拟任务是一个很灵活的架构,但正因为它灵活,如果设计时没有规则,代码很快就会变得难以控制。下面是我在实际项目中总结的几条建议,每一条都踩过坑,值得你认真对待。

8.1 中断里只做一件事:更新计数器和标志位

这条规则再怎么强调也不过分。定时器中断是这个架构的心脏,它的执行时间直接决定系统的最小响应延迟。如果你在中断里做了浮点数计算、调用了长时间的函数,系统响应就会变慢,甚至出现中断嵌套错乱。

建议的严格标准是:中断服务函数里的代码,控制在几十行以内,只做整型变量的自增、比较、赋值。如果要做复杂操作,应该把操作转化成一个“待执行请求”,也就是置位一个更高层次的标志,由主循环消费。

8.2 每个任务尽量做到“一次调用,马上返回”

模拟任务不是多线程,它没有真正的抢断能力。一个任务执行多久,取决于它自己什么时候退出函数。所以,任务函数内部的设计原则是:

  • 不做任何while循环等待。
  • 不调用会阻塞的外部库函数。
  • 一个任务如果需要处理很多数据,把它拆成多个状态,一次处理一部分。

比如串口解析任务,如果缓冲区里有 100 帧数据,不要在一个调用里全部解析完,每一帧的处理都可能导致其他任务被延迟。正确做法是每次只处理一帧,剩余的在下一轮调度时继续。

8.3 任务周期之间保持合理的整数倍数关系

如果 Task A 的周期是 20ms,Task B 的周期是 30ms,两个任务的时间点会偶尔重叠,导致某一瞬间响应变慢。建议把任务周期设置为一个基础时基的整数倍,比如 10ms、20ms、50ms、100ms、500ms。这样,调度节奏稳定、可预测。

8.4 共享变量的访问注意原子性

模拟任务架构里,任务之间的通信通常通过全局变量完成。如果某个全局变量被中断以外的任务修改,同时又被另一个任务读取,而这个变量的类型是 8 位或 16 位的,通常没问题。但如果变量是 32 位的,或者结构体、数组,建议在访问时关闭中断保护。

__disable_irq(); // 访问共享结构体 __enable_irq();

代码很短,但能避免很多难以复现的偶发 bug。

8.5 做好任务执行时间预算

虽然模拟任务已经把“延时”拆掉了,但每个任务还是有执行时间的。在实际项目里,建议用示波器测量一下每个任务的执行时间,估算出最坏情况下整个SoftTimer_Proc()的耗时。这决定了系统能否满足最差情况下的实时性要求。

比如 LED 任务耗时 1us,按键任务耗时 5us,串口任务耗时 100us(主要是 sprintf),那么一轮调度在最坏情况下可能在 200us 左右。这个量级对于绝大多数裸机场景完全够用。

8.6 从超级大循环迁移时的步骤建议

如果你是在改造一个已经跑得通的超级大循环项目,不要一下子全重写。建议按下面的步骤推进:

  1. 列出现有 while 循环里的所有任务,估算每个任务的周期。
  2. 对涉及延时的任务,先用“标志位等待”替代延时,把延时拆掉。
  3. 建立软件定时器框架,把拆好的任务挂到对应的周期槽里。
  4. 每挂一个任务,就实测一下原有功能是否正常,再挂下一个。
  5. 全部挂完后再统一整理全局变量,精简共享数据。

这套迁移路径风险很小,每一步都能验证,非常适合正在维护老项目的工程师。

9. 继续向事件驱动架构迈进的路线

定时器模拟任务只是解决了一个基础问题:如何让多个任务按各自周期运行。但真正的嵌入式系统,还常常需要处理外部事件,比如按键来了、串口收到数据了、传感器数值超限了。模拟任务架构对这类“异步事件”处理得还比较原始。

下一步,你可以在现有架构上引入一个“事件标志组”的概念。每个模块把产生的事件写到一个全局变量中,主循环在调度时检查并处理。这其实就是事件驱动架构的雏形。很多轻量级框架,比如很多人用的 ProtoThreads、或者基于函数指针的简单状态机,都是在这个阶段再加一层。

从更长远的角度看,如果你发现任务数量超过 10 个、任务之间有频繁的数据交换和复杂依赖、某些任务要求微秒级的实时响应,那模拟任务架构就不太够用了。这才是真正应该切换到 FreeRTOS 或 RT-Thread 的时刻。

判断的标准很简单:当代码里开始出现大量“标志位互相等待”“状态机嵌套状态机”时,就该考虑上 RTOS 了。在那之前,模拟任务架构能帮你用最小的复杂度解决 80% 的问题。

10. 总结

定时器模拟任务架构,是嵌入式裸机开发里性价比非常高的一种软件设计方式。它不需要引入额外系统,却能通过“定时器中断 + 软件定时器 + 状态机”的组合,把超级大循环里的痛点逐个拆解掉。核心思想可以概括为:用定时器做时间基准,用标志位做任务调度,用状态机做非阻塞任务逻辑。

如果你现在还被疯狂的延时和嵌套循环折磨,强烈建议在下一个项目里尝试这个架构。先把基础的 1ms 心跳跑通,再看手头任务各自需要的周期,最后把业务逻辑逐步移植成状态机。你会明显感受到,代码变得清爽了,调试也轻松了很多。后续如果再遇到“同时处理按键、LED、屏幕、串口”这类需求,就不会再被主循环堵得头疼了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询