搞工控现场的朋友对 CAN 总线都不陌生,它就像设备之间那条"看不见的传送带",谁发谁收、优先级怎么排、出错怎么兜底,全在这一对差分线上完成。这次我拿 GD32H759 这颗 Cortex-M7 内核的高性能 MCU,配上 RT-Thread 这套国产实时操作系统,把一个典型的 CAN 通信节点从零搭起来,顺手把踩过的坑和实测数据整理出来。整篇是"GD32H759 + RT-Thread 工控实战"系列的第一篇,只聊 CAN 总线这一件事,后面还会有 Modbus、以太网、文件系统那些。适合已经会点 C 语言、摸过单片机、但没在工控场景里认真做过 CAN 的朋友,也适合正在选型、纠结用中断还是 DMA 收报文的人。我不会讲太多教科书定义,重点是"为什么这么配、参数怎么算、现场会出什么问题",能直接抄作业的地方我都把代码和配置贴出来了。
1. 为什么这个工控节点我选了 GD32H759 加 RT-Thread 这套组合
1.1 选型时我到底在权衡什么
工控设备跟消费电子最大的区别,是"它得连轴转好几年,还不能挂"。所以选 MCU 我第一看外设够不够硬、第二看主频有没有余量、第三看生态能不能省事。GD32H759 这颗片子用的是 Cortex-M7 内核,主频拉到 600MHz,带双精度浮点单元和指令数据缓存,片上 SRAM 给得也大方,跑 RTOS 加上协议栈完全不虚。CAN 这块它给到多路独立的 CAN-FD 控制器,兼容经典 CAN,收发 FIFO 深度也够,做多节点组网时不用外扩控制器,这一点对工控板卡很关键——PCB 面积和 BOM 成本都能压下来。
RT-Thread 这边我选它的原因更实际:它的设备驱动框架把 CAN 抽象成了标准设备,rt_device_find、rt_device_open、rt_device_write一套下来,业务代码动都不用动就能换底层。现场调试的时候你经常要临时改过滤规则、改波特率,有这层框架顶着,改起来就是改几个参数的事,不用把整个底层驱动翻一遍。这套组合放到工控场景里,等于"硬件性能有冗余、软件维护成本低",长期看是划算的。
1.2 GD32H759 的 CAN 资源摊开来看
不说虚的,直接把跟 CAN 相关的资源列出来。GD32H759 集成了多路 CAN-FD 控制器,每一路都有自己的发送 FIFO、接收 FIFO 和一组可配置的过滤器。CAN-FD 相比经典 CAN 最大的改动是数据段可以变速、单帧最多 64 字节数据,这在工控里很有用——比如一次要下发一批参数,经典 CAN 得拆成七八帧,CAN-FD 一帧就搞定,总线负载和延迟都下来了。
不过要注意,CAN-FD 是向下兼容经典 CAN 的,但你不能指望一条总线上既有 CAN-FD 节点又有纯经典 CAN 节点还能跑满性能。仲裁段大家用同一个速率,数据段如果 FD 节点提速了,经典 CAN 节点会直接报格式错误。所以现场规划总线的时候,要么整条线统一用经典 CAN,要么统一上 CAN-FD,别混着来。我一般做新项目就直接按 CAN-FD 规划,速率留够余量,后面加节点不慌。
过滤器这块值得单独说。GD32 的 CAN 过滤器支持掩码模式和列表模式,可以按 ID 精确匹配也可以按位屏蔽匹配。工控现场往往一条总线上挂十几二十个节点,如果不过滤全收,CPU 光处理中断就够呛。所以过滤器配置是必须认真做的一步,后面第 2 章我会细讲怎么算、怎么配。
1.3 RT-Thread 的 CAN 设备框架为什么省事
RT-Thread 的 CAN 框架本质上是一层"翻译官"。你的业务逻辑只管调用标准接口发数据、收数据,具体是哪个寄存器、哪路 FIFO、怎么触发中断,全交给底层驱动。这带来的好处是代码可移植性极强——今天用 GD32H759,明天换个别的国产 MCU,只要把底层驱动对接好,上层业务一行不用改。
框架里收发模型是这么设计的:发送走rt_device_write,底层把报文塞进硬件发送 FIFO 就返回,真正的发送完成可以配回调;接收一般走信号量或者消息队列,中断里把报文取出来挂到消息队列上,再由专门的接收线程去消费。这种"中断干活少、线程干活多"的设计是 RTOS 编程的经典套路,目的就是把中断处理时间压到最短,避免高优先级中断阻塞整个系统。
我在实际用下来发现,RT-Thread 的 CAN 框架对错误处理的支持需要自己补一些东西。比如总线关闭(Bus-Off)之后怎么自动恢复,框架本身给的不多,得自己在驱动里加恢复逻辑。这块是工控现场最容易翻车的地方,第 5 章会重点讲。
2. CAN 总线在工控现场的核心参数到底怎么定
2.1 位时序和波特率我是这么算的
CAN 是异步总线,没有时钟线,靠位时序来采样。一个位被切成若干时间份额(Time Quantum,简称 tq),通常分成四段:同步段(SYNC_SEG)、传播段(PROP_SEG)、相位缓冲段 1(BS1)、相位缓冲段 2(BS2)。采样点就落在 BS1 和 BS2 的交界处,采样点的位置直接决定通信稳不稳。
计算公式很简单:
- 波特率 = CAN 时钟频率 / (BRP × (1 + BS1 + BS2))
- 采样点位置 = (1 + BS1) / (1 + BS1 + BS2)
拿 GD32H759 举例,假设 CAN 外设时钟配到 50MHz,我目标是 500kbps。50MHz / 500kHz = 100,也就是一个位要 100 个 tq。BRP 取 5,那么每个位就是 20 个 tq,再拆:SYNC_SEG=1,BS1=15,BS2=4,加起来刚好 20。采样点 =(1+15)/20 = 80%,落在推荐范围内。
| 目标波特率 | CAN 时钟 | BRP | BS1 | BS2 | 采样点 |
|---|---|---|---|---|---|
| 1Mbps | 50MHz | 5 | 7 | 2 | 80% |
| 500kbps | 50MHz | 5 | 15 | 4 | 80% |
| 250kbps | 50MHz | 5 | 31 | 8 | 80% |
| 125kbps | 50MHz | 10 | 31 | 8 | 80% |
采样点一般建议放在 75%~87.5% 之间。80% 是个万能值,绝大多数场景都能跑稳。如果现场线缆特别长、干扰大,可以把采样点往后挪一点,比如 85%,给传播延迟留更多余量。但别挪太靠后,否则相位缓冲段 2 太短,重同步能力会变差。
注意:总线上所有节点的波特率必须完全一致,采样点也建议尽量统一。如果一端 80% 一端 87.5%,短距离没事,长距离就会零星出现错误帧,非常难查。
2.2 过滤器配置:别让无关报文冲垮你的 CPU
工控现场最不缺的就是报文。一条总线上可能有 PLC、伺服、传感器、HMI 同时发数据,如果你的节点不过滤全收,CPU 会被大量无关中断拖垮。所以过滤器的核心思路是"只收我关心的,其余硬件直接丢"。
GD32 的 CAN 过滤器有两种模式:掩码模式(Mask)和列表模式(List)。掩码模式是"ID 里的某些位必须匹配,其余位不管",适合收一组连续 ID;列表模式是"必须精确等于列表里的某个 ID",适合收几个离散的固定 ID。
举个实际例子。假设我这条总线上,伺服状态帧的 ID 是 0x180~0x187,我只需要收这一段。配置思路是把 ID 的高 7 位(0x180 到 0x187 只差最低 3 位)作为匹配域,低 3 位作为屏蔽域:
/* 掩码模式:只关心 ID 的高位,低 3 位不比较 */ can_filter_parameter_struct filter; filter.filter_number = 0; filter.filter_mode = CAN_FILTERMODE_MASK; filter.filter_bits = CAN_FILTERBITS_32BIT; filter.filter_list_high = 0x180 << 5; /* 标准帧 ID 左移 */ filter.filter_list_low = 0x0000; filter.filter_mask_high = 0x7F8 << 5; /* 高 7 位匹配,低 3 位屏蔽 */ filter.filter_mask_low = 0x0000; filter.filter_fifo_number = CAN_FIFO0; filter.filter_enable = ENABLE; can_filter_init(&filter);这里0x7F8是二进制11111111000,意思是 ID 的高 7 位必须匹配,最低 3 位随便。这样 0x180 到 0x187 的帧都会被硬件收进 FIFO0,其余 ID 全部丢弃。CPU 一次中断都没被吵到,效率高很多。
2.3 负载率算清楚再上总线
工控项目里我见过太多人拍脑袋挂节点,结果总线负载率飙到 80% 以上,通信偶尔丢帧还找不到原因。负载率一定要提前算。
经典 CAN 标准帧一帧大约 111 位(不含位填充),加上位填充,最坏情况要考虑放大到 130 位左右。扩展帧约 131 位,最坏约 150 位。计算方法:
- 单帧位时间 = 帧位数 / 波特率
- 每帧占用率 = 单帧位时间 × 帧频率
- 总负载率 = Σ 所有帧的占用率
举个例子:500kbps 总线上,有 10 个节点,每个每秒发 100 帧标准帧。每帧按 130 位算,单帧时间 = 130 / 500000 = 260µs。每节点每秒 100 帧,占用 26ms,10 个节点就是 260ms。负载率 = 260 / 1000 = 26%,还算健康。
如果负载率超过 50%,延迟就开始明显;超过 70%,低优先级帧延迟可能到几十毫秒;超过 80%,基本就是在临界线跳舞,丢帧是必然的。
| 负载率 | 状态 | 建议 |
|---|---|---|
| < 30% | 健康 | 可长期运行 |
| 30%~50% | 正常 | 注意低优先级帧延迟 |
| 50%~70% | 偏高 | 需优化帧频率或拆分总线 |
| > 70% | 危险 | 必须重新设计 |
我在实际项目中一般把负载率控制在 40% 以内,留足余量应对突发。工控设备偶尔会有事件触发一堆报文,如果平时就 60% 了,突发时直接堵死。
3. 从裸机到 RT-Thread:CAN 驱动对接全流程
3.1 时钟树和引脚复用先理清
GD32H759 的 CAN 外设挂在 APB 总线上,用之前得先把时钟打开,还要把对应的 GPIO 复用到 CAN 功能上。CAN 收发是差分信号,RX 和 TX 两个引脚,通常配成复用推挽输出(TX)和浮空输入或上拉输入(RX)。
/* 打开 CAN0 和 GPIO 时钟 */ rcu_periph_clock_enable(RCU_CAN0); rcu_periph_clock_enable(RCU_GPIOB); /* PB8 -> CAN0_RX, PB9 -> CAN0_TX (具体按你板子调整) */ gpio_af_set(GPIOB, GPIO_AF_2, GPIO_PIN_8 | GPIO_PIN_9); gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_8 | GPIO_PIN_9); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_60MHZ, GPIO_PIN_8 | GPIO_PIN_9);这里有个坑我踩过:GPIO 的复用编号(AF 号)一定要查对应型号的手册,不同型号同一个引脚可能复用号不一样。另外 RX 引脚的上拉建议开着,总线空闲时能保证是隐性电平,减少误触发。
3.2 CAN 外设初始化和过滤器落地代码
时钟和引脚弄好,接下来初始化 CAN 控制器本体。关键是位时序参数、工作模式、自动重传这些配置。
can_parameter_struct can_init_struct; can_deinit(CAN0); can_struct_para_init(&can_init_struct); can_init_struct.time_triggered = DISABLE; can_init_struct.auto_bus_off_recovery = ENABLE; /* 自动恢复 */ can_init_struct.auto_wake_up = DISABLE; can_init_struct.no_auto_retrans = DISABLE; /* 允许自动重传 */ can_init_struct.rec_fifo_overwrite = DISABLE; /* FIFO 满不覆盖 */ can_init_struct.trans_fifo_order = DISABLE; can_init_struct.working_mode = CAN_NORMAL_MODE; can_init_struct.resync_jump_width = CAN_BT_SJW_1TQ; can_init_struct.time_segment_1 = CAN_BT_BS1_15TQ; can_init_struct.time_segment_2 = CAN_BT_BS2_4TQ; can_init_struct.prescaler = 5; can_init(CAN0, &can_init_struct);注意auto_bus_off_recovery这个参数,开了之后硬件在检测到总线关闭时会自动尝试恢复,省了我们不少事。但硬件自动恢复有个问题:它不告诉你"什么时候恢复的、恢复失败没有",如果现场总线持续故障,节点会反复进出 Bus-Off,你根本不知道。所以我在关键项目里会关掉硬件自动恢复,改由软件自己管,这样每次恢复都能记录日志,第 5 章细说。
3.3 挂到 RT-Thread CAN 设备框架上
裸机的 CAN 初始化写完了,还得让它跟 RT-Thread 的设备框架接上。RT-Thread 里 CAN 设备通过rt_can_device结构体注册,核心是提供几个回调:配置、发送、接收。
static rt_err_t drv_can_configure(rt_can_device_t *can, void *cfg) { struct can_configure *conf = (struct can_configure *)cfg; /* 根据 conf->baud_rate 重新配置位时序 */ can_baudrate_set(CAN0, conf->baud_rate); return RT_EOK; } static rt_size_t drv_can_sendmsg(rt_can_device_t *can, const void *buf, rt_uint32_t boxno) { const struct rt_can_msg *msg = (const struct rt_can_msg *)buf; /* 把 msg 塞进硬件发送邮箱 */ can_transmit(CAN0, msg->id, (can_ide_enum)(msg->ide), (can_rtre_enum)(msg->rtr), msg->len, msg->data); return 1; } static int drv_can_recvmsg(rt_can_device_t *can, void *buf, rt_uint32_t boxno) { /* 从硬件 FIFO 取出报文填到 buf */ /* 通常在这里做中断里的搬运 */ return 1; }注册的时候把这些回调填进rt_can_ops,然后调用rt_hw_can_register。之后上层就能用rt_device_find("can0")找到设备,用rt_device_write发送。
3.4 收发的线程模型怎么设计
这块是我最想强调的。很多人从裸机转 RTOS,习惯在中断里把活全干完,结果系统一忙就出问题。RT-Thread 下的正确姿势是:中断里只做"搬运",即把硬件 FIFO 的报文读到一个缓冲区,然后释放信号量或往消息队列里扔一条消息,真正的解析和处理放到专门的接收线程里。
发送端则相反,发送一般不需要专门的线程,业务线程拿到数据直接rt_device_write就行。但如果发送频率很高,建议加一个发送队列,由一个发送线程统一出队发送,避免多个线程同时抢 CAN 发送邮箱。
/* 接收线程示例 */ static void can_rx_thread_entry(void *param) { struct rt_can_msg rxmsg; while (1) { /* 阻塞等信号量,超时用 RT_WAITING_FOREVER */ rt_sem_take(&can_rx_sem, RT_WAITING_FOREVER); while (drv_can_recvmsg(&can0_dev, &rxmsg, 0) > 0) { can_frame_dispatch(&rxmsg); /* 业务分发 */ } } }这套模型的好处是把中断处理时间压到最短,即使业务处理慢,也不会丢帧——只要消息队列够大、接收线程优先级够高。我在实际项目中给接收线程的优先级一般设得比较高,但低于系统关键任务,保证它能及时消费。
4. 中断接收还是 DMA 接收:实测数据说话
4.1 中断接收的写法与实测
这是问得最多的一个问题。先说结论:在 500kbps 及以下、报文量中等(每节点每秒几百帧以内)的工控场景,中断接收完全够用,没必要为了 DMA 而 DMA。DMA 适合的是高波特率、大报文量的场景,比如 CAN-FD 下 5Mbps 数据段,那个量级中断真扛不住。
中断接收的写法很直接:在 CAN 接收中断服务函数里读 FIFO,读一条释放一次信号量。关键是中断优先级和中断服务时间要控制好。
void CAN0_RX0_IRQHandler(void) { rt_interrupt_enter(); if (can_interrupt_flag_get(CAN0, CAN_INT_FLAG_RF0N)) { can_interrupt_flag_clear(CAN0, CAN_INT_FLAG_RF0N); rt_sem_release(&can_rx_sem); /* 只通知,不搬运 */ } rt_interrupt_leave(); }我实测下来,500kbps 下满负载(负载率约 70%),中断方式接收丢帧率为 0,CPU 占用大概 8%~12%,取决于报文大小和线程调度。这个开销对 600MHz 的 M7 来说压力很小。真正的问题不在中断本身,而在于你中断里干了多少事。如果中断里直接解析报文、做业务运算,那就很容易丢帧了。所以核心原则永远是"中断搬运、线程处理"。
提示:中断接收的 FIFO 深度有限,如果接收线程消费不及时,FIFO 会满。配置
rec_fifo_overwrite决定满了之后是覆盖旧数据还是丢弃新数据。工控里我更倾向丢新保旧,因为旧数据往往是状态基线,新数据刷新一下还有。
4.2 DMA 接收的写法与实测
再来看 DMA。DMA 接收的思路是让 DMA 把 CAN 接收 FIFO 的数据直接搬到内存缓冲区,CPU 只在缓冲区满或半满时被通知一次,然后批量处理。这样中断次数大幅降低,适合高频场景。
不过 CAN 的 DMA 比串口、SPI 那些麻烦,因为 CAN 是"消息"型外设,不是连续字节流,每条报文长度还不一样(标准帧最长 8 字节数据,FD 帧最长 64 字节)。所以 DMA 搬的时候得按固定步长搬,或者配合 CAN 的 FIFO 管理寄存器一起用。GD32H759 这边,CAN 接收可以触发 DMA 请求,把报文按邮箱单位搬到一块 SRAM 缓冲区。
/* DMA 配置思路(伪代码,具体寄存器按手册) */ dma_deinit(DMA0, DMA_CH3); dma_single_data_parameter_struct dma_init; dma_init.periph_addr = (uint32_t)&CAN_RFIFO0(CAN0); /* 接收 FIFO */ dma_init.memory0_addr = (uint32_t)can_dma_buffer; dma_init.direction = DMA_PERIPH_TO_MEMORY; dma_init.number = CAN_DMA_BUF_FRAMES; dma_init.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_init.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_init.periph_width = DMA_PERIPH_WIDTH_32BIT; dma_init.memory_width = DMA_MEMORY_WIDTH_32BIT; dma_init.priority = DMA_PRIORITY_HIGH; dma_single_data_mode_init(DMA0, DMA_CH3, &dma_init); dma_circulation_enable(DMA0, DMA_CH3); dma_channel_enable(DMA0, DMA_CH3);实测下来,在 1Mbps 负载率 60% 的场景里,DMA 接收的 CPU 占用能从中断方式的 20% 左右降到 6%~8%,效果确实明显。但代价是代码复杂度上去了,缓冲区管理、半满中断处理、报文边界对齐这些都要额外处理,出错几率也高。
4.3 两种方式的取舍
我整理了一张对比表,实际选型的时候直接看这个:
| 维度 | 中断接收 | DMA 接收 |
|---|---|---|
| 适用波特率 | ≤ 1Mbps | ≥ 500kbps,尤其 CAN-FD |
| 适用报文密度 | 中低(< 每秒 500 帧) | 高(每秒 1000 帧以上) |
| CPU 占用 | 8%~20% | 6%~10% |
| 代码复杂度 | 低 | 高 |
| 调试难度 | 低 | 高 |
| 内存开销 | 小 | 需要一块 DMA 缓冲区 |
| 推荐场景 | 常规工控节点 | 网关、数据采集密集节点 |
我的建议是:新项目没特殊需求就先用中断,跑起来测一下 CPU 和丢帧,如果都达标就别上 DMA。只有当报文量真的把 CPU 顶到 30% 以上,或者你要做 CAN 网关需要转发大量报文时,再上 DMA。为了"看起来高级"就上 DMA,后期调试的苦头只有自己吃。
5. 错误帧、总线关闭和恢复:现场最容易翻车的地方
5.1 错误计数器和错误帧到底怎么回事
CAN 有个很强的设计:每个节点都维护两个错误计数器,发送错误计数器(TEC)和接收错误计数器(REC)。每发错一次 TEC 加 8,每收错一次 REC 加 1,成功一次相应减 1。TEC 超过 255 节点进入 Bus-Off,也就是彻底断开总线,不再发送。
错误帧本身是一段特殊格式:由 6 个连续的显性位组成,故意破坏位填充规则,让总线上所有节点都能检测到"有人出错了"。错误帧分主动错误帧和被动错误帧,主动错误的节点会发 6 个显性位强提醒大家,被动错误的节点只能发 6 个隐性位。错误计数超过 127 就变成被动错误状态。
工控现场错误帧频繁,通常就几个原因:终端电阻没接或阻值不对(应该是 120Ω 两端各一个)、波特率不一致、线缆屏蔽没做好、共模干扰大。我见过一个现场,两个节点距离 80 米,用了普通双绞线还没加隔离,干扰一来错误帧一堆,最后加了共模电感和隔离收发器才稳定。
注意:终端电阻必须加在总线的物理两端,中间节点不要加。很多人以为每个节点都加一个更稳,其实错了,中间节点加电阻会降低总线阻抗,反而更容易出错。
5.2 Bus-Off 恢复的软件实现
硬件自动恢复省事但不可控,我更喜欢软件自己管。思路是:进 Bus-Off 中断后,设置一个标志,由一个监控线程在延迟一段时间后尝试恢复。恢复前先检查总线状态,连续若干次恢复失败就上报故障。
/* Bus-Off 中断里只置标志 */ void can_busoff_isr(void) { rt_event_send(&can_event, CAN_EVT_BUSOFF); } /* 监控线程处理恢复 */ static void can_monitor_thread(void *param) { rt_uint32_t evt; while (1) { if (rt_event_recv(&can_event, CAN_EVT_BUSOFF, RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &evt) == RT_EOK) { rt_thread_mdelay(100); /* 等总线安静下来 */ if (can_recover(CAN0) != RT_EOK) { LOG_E("CAN bus-off recovery failed"); /* 上报故障、点亮故障灯 */ } } } }恢复的关键是延迟时间。总线持续短路或严重干扰时,立即恢复会立刻再次 Bus-Off,反复进出反而不利于排查。100ms 是个经验值,够总线安静,又不至于影响业务太久。
5.3 常见问题速查表
现场排查我总结了这张表,基本覆盖了 90% 的问题:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全收不到报文 | 终端电阻缺失、接线反了 | 万用表测终端电阻,正常约 60Ω |
| 零星错误帧 | 波特率不一致、屏蔽没接 | 示波器看波形,核对两端位时序 |
| 通信时好时坏 | 干扰大、地线没共地 | 检查屏蔽层单点接地、加隔离 |
| 一段时间后失联 | Bus-Off 未恢复 | 检查 TEC/REC、加恢复逻辑 |
| 高频时丢帧 | FIFO 溢出、线程消费慢 | 查看 FIFO 溢出标志、提升线程优先级 |
| 发送总失败 | 无应答、自动重传耗尽 | 查总线上是否有其他节点应答 |
| CAN-FD 帧报格式错误 | 总线上有经典 CAN 节点 | 统一总线协议版本 |
排查顺序我一般是"先物理、后参数、最后软件"。物理层(电阻、线缆、接地)出问题的概率远远高于软件,先花五分钟量一下电阻,比盯半天代码管用。
5.4 几个反直觉的实操细节
最后分享几个我踩坑踩出来的细节。第一,CAN 的发送邮箱只有三个,如果你短时间连续发多条,第四帧开始就要等待,rt_device_write会阻塞或者失败,所以高频发送一定要配发送队列而不是循环直发。第二,接收 FIFO 溢出标志你最好定期读一下并清零,这个标志能帮你量化丢帧情况,比事后猜要靠谱。第三,调试初期建议把 CAN 设为回环模式(Loopback),一个节点自己收发自己,验证驱动写对没有,避免一上来就是硬件问题干扰判断。第四,屏蔽双绞线的屏蔽层要单点接地,两端都接地容易形成地环流,反而引入干扰——这个和很多人的直觉相反,但我在几个现场验证过,单点接地确实更稳。
我个人在实际操作中的体会是,CAN 这东西"硬件占七分、软件占三分"。驱动写得再漂亮,物理层没弄好一样白搭。所以每次新板子回来,我第一件事就是量终端电阻、看波形,确认物理层没问题再动软件。这套习惯帮我省了无数个加班的夜晚。下一篇我会接着聊 GD32H759 上 RT-Thread 的定时器和消息队列怎么做多任务调度,到时候再细说。