ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

GD32H759+RT-Thread CAN总线通信实战:从驱动配置到负载率估算

GD32H759+RT-Thread CAN总线通信实战:从驱动配置到负载率估算 先说结论这篇文章不是一个简单的“点灯式”外设演示而是把我最近在一个工业控制器项目里用GD32H759搭配RT-Thread做CAN总线通信的完整过程整理了出来。GD32H759这颗料在国产MCU里属于性能靠前的一档Cortex-M7内核主频高、外设全用在需要一定算力和丰富接口的工控主板上很合适RT-Thread则解决了多任务调度和设备管理的问题让CAN收发从“裸机状态机”变成“设备框架下的标准操作”。这第1篇我会把CAN总线从硬件选型、软件框架、报文收发到错误帧处理和负载率计算全部过一遍适合正在用GD32系列做工业通信、或者想把RT-Thread的CAN设备框架用起来的工程师参考。如果你已经开始搭GD32H759的RT-Thread工程大概率会卡在这几个点上CAN驱动怎么从Studio里配置出来、波特率到底怎么算才准、接收用中断还是DMA、总线异常时驱动会怎么表现、多节点通讯的带宽够不够用。这篇文章就把这些一个个拆开讲尽量做到拿到就能直接用。1. 为什么工控现场我会选GD32H759搭配RT-Thread做CAN节点1.1 CAN总线在工控场景里的实际定位CAN总线在工业现场有很强的存在感尤其在设备间距离几十米、节点数十几个、电磁环境不友好的场景里。RS485虽然便宜但半双工通信的仲裁机制弱多主并发时协议层很容易打架以太网带宽高可物理层和协议栈的成本、实时性保障在中小型设备上并不划算。CAN总线在数据链路层就把冲突仲裁、错误检测做掉了节点只要按照报文ID的优先级去竞争总线硬件自动保证高优先级报文先发出去这让它在工程机械、光伏逆变器、储能BMS、充电桩、纺织机械这些行业里长期占据主导地位。我这次项目的需求也比较典型一台设备作为主控下面挂显示面板、IO扩展模块、变频器和几个传感器节点每个节点按不同的周期上报数据主控还要下行控制指令。通讯距离大概20米左右波特率定在500kbps。这个场景对MCU的要求是CAN控制器至少两路、中断延迟可控、主频不能太低因为主控还要同时处理触摸屏数据、运动控制算法和远程升级。1.2 这套组合的定位和本系列规划选定GD32H759的理由很简单Cortex-M7带来的算力余量很足跑RT-Thread加几个业务线程完全不紧张CAN控制器支持经典CAN和CAN-FD后续项目升级到CAN-FD不需要换平台RAM和Flash也够大给协议栈、日志、升级备份都留了空间。RT-Thread这边我主要看重它的设备框架尤其CAN设备驱动框架应用层不需要关心具体的寄存器操作直接通过统一API开关设备、配置参数、收发报文方便项目在不同MCU之间迁移。这个系列我打算按外设逐个拆开写CAN总线是第一篇后边还会涉及基于RT-Thread的定时器与PWM控制、ADC多通道采集、Modbus RTU主从机、Bootloader与固件升级等内容。每一篇都保持同一个原则不堆理论直接给工程里验证过的做法和踩坑记录。2. CAN外设与硬件设计评估板引脚分配、终端电阻和收发器的那些坑2.1 GD32H759的CAN控制器底子GD32H759的CAN外设提供经典CAN和CAN-FD两种工作模式这一点在选型时很重要。经典CAN的帧格式大家都熟悉最大8字节数据段CAN-FD把数据段扩展到64字节并且数据段可以使用比仲裁段更高的波特率。我这个项目目前用的还是经典CAN但代码里已经把外设配置成兼容CAN-FD的模式防止后续现场设备升级后主控不支持。从驱动实现的角度GD32H759的CAN控制器带有硬件FIFO接收缓冲区有多组报文过滤寄存器。接收FIFO的意义在于总线上一旦有报文进来硬件会自动把报文存入FIFO并置位相应标志CPU只需要在合适的时间去读。这意味着波特率比较高、报文比较密集的时候只要FIFO没满即使软件响应有一点延迟报文也不会丢。这一点在下一章讲中断和DMA选择的时候还会再提。2.2 引脚规划与收发器选型引脚规划上我手里这块评估板的CAN0和CAN1分别引出了一组引脚默认配置如下表。实际做自己的板子时务必对照GD32H759数据手册的AFIO复用表确认引脚的复用功能编号同一引脚可能有多组复用配置错了一个字节都收不到。外设发送引脚接收引脚默认复用功能CAN0PB9PB8AF9CAN1PD1PD0AF9CAN0备用PA12PA11AF9CAN1备用PB13PB12AF9收发器我用的TJA1050兼容型号国产的SIT1050也试过基本Pin to Pin。选型要注意工作电压TJA1050是5V供电而GD32H759的IO口通常是3.3VCAN控制器引脚和收发器之间需要确认是否兼容。GD32H759的CAN引脚耐压设计可以直连5V收发器的情况比较常见但保险起见我看了评估板原理图它做了电平转换处理。自己做板子的时候建议加上电平匹配电路或者直接选用3.3V/5V兼容的TJA1044省去转换的麻烦。2.3 原理图上的三个关键细节第一个是终端电阻。CAN总线两端节点必须各接一个120欧姆电阻很多小节点板子为了省电不加终端电阻导致总线波形反射严重、误码率升高。如果节点确定是总线最末端必须加上不确定就预留焊盘现场用万用表量一下总线电阻再决定。第二个是共模电感和保护器件。CAN总线长距离走线容易引入共模干扰我在CAN_H和CAN_L上串了共模电感并在连接器入口处加了TVS管。这个在实验室不明显到了现场电磁环境复杂的地方能少跑很多趟。第三个是总线供电问题。如果CAN收发器需要5V电源要注意这个5V是否来自隔离DCDC以及地与CAN_GND之间是否隔离。工业现场不同设备之间地电位不一致会导致较大的地环路电流轻则波形变形重则烧收发器。有条件的话建议CAN通信做隔离至少电源隔离和信号隔离二选一项目长期稳定性会好很多。3. RT-Thread Studio工程搭建与CAN设备框架接入3.1 创建工程和打开CAN驱动的操作路径我用的开发环境是RT-Thread Studio版本在5.0以上对GD32H7系列的支持已经比较完整。新建工程时选择GD32H759型号RT-Thread版本选最新稳定版即可。工程生成后CAN驱动默认不一定打开在RT-Thread Settings图形界面里把CAN设备驱动勾上然后在board配置文件里使能对应的CAN外设。这里有一个常见坑Studio生成的工程里drv_can.c默认注册的是can0设备如果你要用CAN1必须去board配置里把串口号映射关系改掉同时在drv_can.c或者board.c里补上CAN1的初始化资源。很多初学者直接rt_device_find(can1)返回空指针就是因为外设没有注册进系统。3.2 CAN设备框架的API关系与初始化流程RT-Thread的CAN设备框架把底层驱动和应用层隔离得很干净。上层用标准的设备API来操作CAN核心接口就几个找设备、打开设备、配置设备、发送、接收、注册接收回调。#include rtthread.h #include rtdevice.h #define CAN_DEV_NAME can0 static rt_device_t can_dev; static struct rt_can_msg rx_msg; static struct rt_semaphore rx_sem; /* 接收回调只是发信号量不做数据拷贝 */ static rt_err_t can_rx_ind(rt_device_t dev, rt_size_t size) { rt_sem_release(rx_sem); return RT_EOK; } static void can_rx_thread_entry(void *parameter) { while (1) { rt_sem_take(rx_sem, RT_WAITING_FOREVER); while (rt_device_read(can_dev, 0, rx_msg, sizeof(rx_msg)) 0) { /* 这里处理接收到的报文 */ if (rx_msg.ide RT_CAN_STD) { rt_kprintf(std id0x%x len%d , rx_msg.id, rx_msg.len); for (int i 0; i rx_msg.len; i) { rt_kprintf(%02x , rx_msg.data[i]); } rt_kprintf(\n); } } } } static void can_init(void) { struct rt_can_config config; can_dev rt_device_find(CAN_DEV_NAME); if (can_dev RT_NULL) { rt_kprintf(find %s failed\n, CAN_DEV_NAME); return; } rt_sem_init(rx_sem, rx_sem, 0, RT_IPC_FLAG_PRIO); rt_device_open(can_dev, RT_DEVICE_OFLAG_RDWR); config.baud_rate 500; /* 500kbps */ config.mode RT_CAN_MODE_NORMAL; config.filter RT_CAN_FILTER_MODE_MASK; rt_device_control(can_dev, RT_DEVICE_CTRL_CONFIG, config); rt_device_set_rx_indicate(can_dev, can_rx_ind); rt_thread_t tid rt_thread_create(can_rx, can_rx_thread_entry, RT_NULL, 1024, 10, 10); if (tid ! RT_NULL) { rt_thread_startup(tid); } }这段代码是CAN接收最基石的框架。注意接收回调里不能调用任何阻塞操作因为中断上下文里不能拿信号量等待只能释放信号量。实际的数据读取和业务处理放到线程里做这是RT-Thread接收处理的正确姿势。3.3 双CAN实例与过滤器匹配策略GD32H759有两路CAN我这边CAN0用于设备内部组网CAN1用于和上层PLC对接。两路使用相同的设备框架但过滤器策略不同。CAN0的节点之间报文ID定义了优先级我把控制指令ID设为0x100把状态上报ID设为0x200传感器数据ID设为0x300。由于总线上的报文类型固定CAN0过滤器全部使用掩码模式只放行这三类ID其它ID一律丢弃这样可以有效减轻CPU的无效中断压力。CAN1面向外部PLCPLC发送的报文的ID无法完全预知所以CAN1一开始使用标识符列表模式把常用指令的ID加入过滤表。如果后续需要透传所有报文就把过滤器改成掩码全0的方式即不过滤所有ID但此时要格外注意中断频率高导致的CPU占用问题。4. 报文收发实战波特率计算、发送超时与接收回调线程4.1 波特率配置背后的时间量子计算CAN波特率不是直接写一个频率值就完事控制器内部会按照时间量子来划分一个位时间。一个位时间由同步段、传播段、相位缓冲段1、相位缓冲段2四部分组成采样点就在相位缓冲段1和2之间。RT-Thread的CAN驱动对经典CAN协议做了封装直接配置baud_rate就能跑但没有告诉你这个波特率在GD32H759上到底靠不靠谱。以500kbps为例GD32H759的CAN外设时钟来自APB1如果APB1时钟是60MHz那么波特率分频器需要配置为120则时间量子为1MHz。一个位时间要拆成20个时间量子才能得到500kbps500k 1MHz / 20。采样点通常在75%附近也就是同步段1个量子、传播段相位缓冲段1合计14个量子、相位缓冲段2为5个量子。如果采样点设置不合理总线较长或节点时钟有偏差时就会出现偶发错帧。经验值500kbps以下用75%到80%采样点1Mbps建议用75%。RT-Thread标准驱动里有些平台没有开放采样点配置这时候就需要手动改驱动底层把位时间寄存器配置到位。4.2 从发送到接收回调的完整代码链路发送报文的代码比接收简单但还是有几个细节要处理好否则调试时会被坑到。static void can_send_msg(uint32_t id, uint8_t *data, uint8_t len) { struct rt_can_msg msg; rt_err_t result; msg.id id; msg.ide RT_CAN_STD; msg.rtr RT_CAN_DTR; /* 数据帧 */ msg.len len; rt_memcpy(msg.data, data, len); result rt_device_write(can_dev, 0, msg, sizeof(msg)); if (result ! sizeof(msg)) { rt_kprintf(can send failed, result0x%x\n, result); } }rt_device_write的返回值最容易被误解。对于CAN设备返回值等于写入消息结构体字节数才算成功而不是返回成功发送的帧数。如果驱动底层发送邮箱全满这个函数会返回错误或阻塞取决于打开设备时的标志和驱动实现。我这里发送没有使用中断或DMA就是最简单的直接写发送邮箱模式。原因是本项目的发送频率不高主控每10ms才发一次控制指令CPU写CAN发送寄存器的耗时可以忽略不计。如果发送报文很多建议使用发送中断或TX FIFO机制否则低优先级报文会被高优先级报文反复插队出现发送饿死。接收链路前面代码已经给了硬件收到报文后触发接收中断驱动在中断中把报文读入FIFO然后调用rt_device_set_rx_indicate注册的接收回调回调释放信号量接收线程从信号量中唤醒再调用rt_device_read把报文读出来。4.3 接收线程优先级的分配考量这里专门说一下接收线程优先级。RT-Thread下优先级数值越小优先级越高。我的工程里接收线程优先级设为10主控制线程设为8Modbus线程设为15。接收回调用信号量唤醒不占优先级参与调度真正干活的是接收线程。需要避免的情况是把接收线程优先级调得太高导致控制线程被饿死或者调得太低总线突发流量时接收线程来不及取报文FIFO溢出丢帧。报文密集的应用场景可以考虑用两个接收线程分别处理CAN0和CAN1并且把接收线程的栈大小至少放到1024字节以上因为CAN报文处理常会调用rt_kprintf这类函数它们会消耗较多栈空间。我一开始栈只给了512字节跑到第2天系统随机崩溃排查了很久才发现是栈溢出。5. 中断接收和DMA接收怎么选基于GD32H759的实际取舍5.1 三种接收路线的机制差异CAN报文接收有轮询、中断、DMA三种典型路线。轮询就是主循环里不断查询接收FIFO状态代码最简单但报文稍一密集就丢帧线程调度一抖动数据就断工控场景基本排除。中断接收是主流方案CAN控制器收到完整报文后把数据存入FIFO并拉起接收中断。CPU进入中断服务函数把FIFO读回内存。中断方案的优点是实时性好报文到达后在几个微秒内就能得到响应缺点是报文量大时CPU频繁进中断整体开销并不小。DMA接收是把CAN控制器FIFO中的数据通过DMA搬运到内存缓冲区搬运完成后DMA中断再通知CPU处理。核心好处是CPU几乎不参与搬运报文量再大也只是在搬完一帧或一批后处理一次。但这里有一个前提需要CAN控制器本身支持DMA请求否则外部DMA根本无法感知FIFO状态。我给GD32H759做驱动适配时确认过它的CAN外设接收路径并不是为DMA设计的那种流式接口而是按报文槽位组织的邮箱/FIFO结构DMA可以做的场景有限而且驱动层适配成本高、容易出边界问题。5.2 CAN接收的FIFO与中断关键点GD32H759的CAN控制器在接收方向上有多级FIFO缓冲。这意味着即使CPU中断响应有几十微秒的延迟只要FIFO没有被连续的高流量占满新报文也不会丢。实际运行中500kbps波特率下假设总线上每1ms有一帧密集报文FIFO能缓存好几帧处理线程只需在中断产生后快速读取即可。中断处理的注意事项也很明确中断服务函数里尽量只做读取FIFO和释放信号量两件事不要把协议解析、数据拷贝、日志打印放进中断。RT-Thread的中断延迟本身已经有保障但应用层如果违反规则再好的设备驱动都会被拖垮。5.3 我的决策结论和实际效果我在这个项目里的结论是GD32H759的CAN接收使用中断方式发送使用直接写邮箱方式不使用DMA。理由有三条第一现场CAN报文流量不算极端。500kbps下一帧标准帧按130位左右算总线上每秒最多能跑大概3800帧哪怕是全速接收每个报文触发一次中断CPU占用也远没到危险线。第二DMA在CAN接收上的收益在GD32H759这种按报文槽组织的控制器上不明显反而要处理FIFO边界、半满中断、DMA与硬件队列的同步等问题出了问题极难排查。对于追求长期稳定性的工控产品简单可靠比极致性能重要。第三接收中断的方案和RT-Thread设备框架配合得最成熟驱动底层已经验证过无数遍我要做的就是保证接收线程足够快。目前实际压测总线上1ms周期注入大量报文CAN0没有丢帧CPU占用率大概在12%左右完全可接受。6. 错误帧、bus-off与总线故障排查链路6.1 错误帧的五种来源与错误状态机CAN协议定义了五种错误类型位错误、位填充错误、CRC错误、格式错误和应答错误。位错误发生在节点发送位时时刻监视总线发现总线上的电平和自己发送的不一致会立刻报错位填充错误是接收方对连续5个相同电平后的填充位校验失败CRC错误是接收方计算的CRC序列和发送方不一致通常由总线干扰或波特率偏差引起格式错误是固定位的电平不对比如CRC定界符必须是隐性电平应答错误是发送方在ACK槽时段没有检测到其他节点的显性应答这种情况往往是总线上只有发送方一个节点或者接收节点故障。错误状态机的变化值得关注。每个节点维护发送错误计数TEC和接收错误计数REC。错误计数小于128时是错误主动状态节点检测到错误后会发送主动错误标志错误计数在128到255之间时进入错误被动状态节点只能发送被动错误标志发送错误计数TEC超过255时节点进入bus-off状态与总线完全隔离不再参与通信。6.2 现场排查的总线诊断步骤总线异常现场排查我一般按照从物理层到协议层的顺序来。第一步用万用表量CAN_H和CAN_L之间的直流电阻正常应该60欧姆左右因为两个120欧姆终端电阻并联如果量到120欧姆说明总线只有一端有终端电阻如果量到几十欧以下可能有短路或者节点收发器损坏。第二步用示波器看CAN_H和CAN_L的差分波形。正常波形应该幅值2V左右显性电平约2V隐性电平约0V边沿清晰。波形变形通常指向终端电阻配置错误、总线分支过长或收发器供电异常。尤其是CAN_H和CAN_L两条线的物理长度和走线方式分支线不建议超过1米。第三步在报文抓取层面确认错误帧类型。用逻辑分析仪或者CAN卡接到总线上观察错误帧出现的规律。如果错误帧集中在某个节点上线时出现多半是这个节点的波特率和采样点配置不对如果错误帧随机出现且CRC错误为主基本可以判定是电磁干扰或者地环路问题。6.3 软件层面复位与恢复策略标准做法是总线故障后由应用层决定恢复策略。我的rt_can驱动适配里额外做了一层监控周期检查CAN控制器的错误状态寄存器当检测到进入bus-off时先尝试软件复位CAN外设再重新打开设备。注意复位后不能立刻恢复通信总线上的其他节点可能还在错误状态需要给它们留出错误计数的衰减时间。恢复策略我用了三层层级策略说明第一层软件复位CAN外设检测到bus-off后等待50ms重新初始化第二层监测错误帧计数如果持续大量错误帧切换为只接收不发送模式第三层应用层降级持续故障时上报主控进入故障安全状态这个策略跑下来效果不错。现场曾出现过一次接线端子松动导致的间歇性接触不良软件在几百毫秒内就自动恢复通信没有造成整机停机。如果只做软件复位不做降级保护总线反复抖动时节点会在恢复和掉线之间来回折腾比彻底断线更难排查。7. 总线负载率估算与多节点通讯规划7.1 负载率的手算公式总线负载率是指总线上实际传输的位数与总线波特率的比例直接反映总线拥挤程度。计算公式很直观负载率 每秒总线上传输的位数 / 波特率。关键是搞清楚一帧CAN报文到底占多少位。标准帧11位ID、8字节数据在示波器上观察到的位长度大约130位左右这个值包含了起始位、仲裁场、控制场、数据场、CRC场、应答场、帧结束和间休场还把位填充造成的额外位也估算进去了。扩展帧长度更大一般为150位左右。7.2 实例计算与工程经验值以下按本项目CAN0的实际数据流量做个手算。CAN0波特率500kbps总线上的报文分为三类主控每10ms发送一帧控制指令标准帧8字节从站A每10ms上报状态标准帧8字节从站B每20ms上报传感器数据标准帧8字节。每秒发送的报文总数 主控100帧 从站A 100帧 从站B 50帧 250帧。按每帧130位计算总线每秒传输位数 250 x 130 32500位。总线负载率 32500 / 500000 6.5%。这个负载率在工控里是非常健康的。行业经验值常规系统总线负载率建议控制在30%以下超过40%时偶发错误可能会引发连锁反应超过60%基本到极限不建议再往上加节点或缩短周期。预留余量很重要因为今后还会增加调试报文、升级报文这些平时不跑、关键时刻必须能挤进去。7.3 高负载场景的报文调度建议前期设计多节点通讯时报文周期和ID规划要一起考虑。高优先级报文不仅要ID小发送周期还要尽量避开和其他报文同时竞争总线。我见过一个现场问题三台设备每秒各自定时100ms发送恰好重叠时总线瞬时冲突严重虽然CAN仲裁能解决但低优先级报文的延迟会明显变大。比较好的做法是错开不同节点的发送时间片比如主控控制指令在整10ms的起始时刻发送从站A在3ms时刻上报从站B在8ms时刻上报。逐帧错开后总线瞬时负载被平均延迟抖动会大幅缩小。如果确实遇到高负载特别是多个节点都按很短的周期密集发送优先考虑两个方向一是升级到CAN-FD64字节数据段会显著减少帧头开销GD32H759本身支持这一点驱动层预留了兼容二是做报文合并把多个8字节数据合并到一帧CAN-FD里减少帧数比提高波特率效果更直接。最后再分享一个我项目里的实际做法CAN报文的数据区定义严格遵守“高字节在前”的固定字节序并且在协议里给每个报文加了1字节累加和校验。虽然CAN本身有CRC但应用层加一道校验能防止错误帧里的数据被业务逻辑误用出现问题时排查范围会小很多。这套组合从硬件定型到现在跑了三个多月总线一直很干净后面几篇我会接着写定时器与PWM、Modbus协议实现和Bootloader升级感兴趣的话可以持续关注。
返回列表