拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ESP32 TWAI/CAN 通信项目完整实战:从硬件选型到现场联调排错

做外包项目这些年我最大的体会是真正拉开差距的不是代码写得有多花哨而是把通信方案从需求阶段到现场联调完整走通的能力。上个月刚交付完一个 ESP32 TWAI (CAN) 通信的硬件与软件设计项目客户最初的需求只有一句话把几个传感器数据通过 CAN 总线发到 PLC再接收 PLC 的控制命令。一句话的需求背后是一整套链路问题选什么收发器、终端电阻怎么处理、ESP32 内部的 TWAI 控制器怎么配置位时序、驱动 API 怎么初始化、现场联调时波形不对怎么定位。这篇文章就把这个项目的完整过程拆开讲从硬件电路设计到 ESP-IDF 驱动实现再到现场调试踩坑适合正在做 ESP32 CAN 通信、或者接了类似外包项目不知道从哪里下手的嵌入式工程师参考。1. 外包项目从需求沟通到通信方案选型为什么是 TWAI 而不是 RS4851.1 客户现场的真实约束项目背景是一家自动化设备厂商需要给产线的几个检测工位加装数据采集节点。每个工位有一个 ESP32 为核心的控制器需要把温湿度、振动、开关量这些数据汇总到主 PLC同时接收 PLC 下发的启停命令。客户明确要求走 CAN 总线因为 PLC 侧已经有成熟的 CAN 通信模块产线上也挂了其他品牌的仪表节点。接外包项目第一件事绝对不是画板子而是把需求边界问清楚。我列了一份清单和客户逐项确认总线速率客户 PLC 侧配置是 500 kbps所有节点必须统一。帧格式现场其他设备用的是扩展帧29 位 ID不是标准帧。节点数量当前 6 个后续会扩到 12 个左右。总线长度预估在 20 米以内线束走线槽会经过变频器区域。供电方式每个节点独立 24V 供电板载 DC-DC 转 3.3V。这些看似基础的信息直接决定了后面所有的硬件选型和软件设计。比如扩展帧这一点如果不提前问清楚等代码写了一半才发现 ID 解析对不上返工成本就很高。1.2 CAN 方案对比 RS485 的实际优势客户提出用 CAN 的时候我也评估过 RS485 方案。RS485 在半双工主从轮询模式下实现起来确实简单但对于多个节点主动上报 PLC 随时下发命令这种场景CAN 的载波监听多路访问和逐位仲裁机制有天然优势。具体到应用层感受就是CAN 节点不需要被主机点名才能发言。传感器工位检测到异常可以立刻往总线上发消息ID 小的帧会优先赢得仲裁这个优先级是由硬件仲裁决定的不需要软件调度。对于有实时性要求的工业设备这个特性非常关键。RS485 当然也能做多主机通信但要么用轮询要么做令牌机制总线上充满了无效的查询帧有效带宽利用率低。CAN 在 500 kbps 下虽然是半双工但最小的数据帧传输时间也就几十微秒对于传感器周期上报和命令下发完全够用。1.3 ESP32 在项目中的定位项目选型阶段还对比过 STM32 和 ESP32。最终选择 ESP32 不是因为 CAN 能力比 STM32 强多少而是因为这个项目除了 CAN 通信还需要 WiFi 配置参数、蓝牙调试接口和本地 LCD 显示。ESP32 一颗芯片全搞定不需要额外挂 WiFi 模组。关于 TWAI 这个名称很多新手会陌生。TWAI 就是 Two-Wire Automotive Interface是乐鑫对 CAN 控制器的内部叫法协议层面兼容 ISO 11898-1 标准。在 ESP32 数据手册里你找不到CAN这个表述都叫 TWAI。ESP32 芯片内部已经集成了 TWAI 控制器但控制器输出的是 TX/RX 逻辑电平不是差分信号所以外部必须再接一颗 CAN 收发器芯片。2. 硬件电路收发器芯片、终端电阻、保护电路那些容易做错的决定2.1 收发器选型3.3V 逻辑芯片是首选ESP32 是 3.3V IOCAN 收发器的选择直接影响电路复杂度。市面上常见的收发器芯片主要分两类我整理了一个对比表芯片型号供电电压逻辑电平ESP32 直连兼容性备注TJA10505VRXD 输出接近 5V不兼容需电平转换或分压经典型号但已显得过时MCP25515VRXD 输出接近 5V不兼容需处理电平量大但 ESP32 不建议直接接SN65HVD2303.3V3.3V兼容老牌 3.3V 收发器好用TJA1051T/33.3V3.3V 逻辑兼容电平设计更省心ISO10425V/3.3V 可选隔离输出兼容需配隔离电源成本高但抗干扰最好这个项目我选了 SN65HVD230。理由很简单3.3V 供电、3.3V 逻辑电平和 ESP32 的 GPIO 直接对接不需要在中间加电平转换电路。虽然这芯片上市有年头了但稳定性和供货都成熟外包项目最怕的就是原理图没问题结果芯片交期三个月。ROS 边还有一颗内置收发器且支持 CAN的模组不是此项目的用途这里不展开。2.2 终端电阻不是每个节点各放一个CAN 总线终端电阻是外包项目里最容易出问题的地方。按 ISO 11898 规范120Ω 终端电阻应该放在总线物理距离的两端而不是每个节点都放一个。如果每个节点板上都贴了 120Ω 电阻节点一多并联电阻值就会远低于 60Ω总线信号幅度会被吃掉波形畸变通信距离和稳定性直线下降。我在这块板子上的处理方式是每个节点预留两组 1206 封装电阻位一个 0Ω 跳线电阻位。默认生产时不贴终端电阻只有确认该节点处于总线物理末端时产线工人才会焊上 120Ω 电阻。这样既保证了灵活性又避免非末端节点因为误贴电阻影响整条总线。如果要求更严格的抗干扰性能我会用分体式终端用两个 60Ω 电阻串联中点通过 4.7nF 电容接地。两个 60Ω 串联对差分信号来说等效阻抗还是 120Ω但中点电容给共模噪声提供了一条低阻泄放路径能明显降低总线辐射和共模干扰。这个项目因为客户预算和交期原因没有用分体式方案但后续类似项目我会优先推荐。2.3 保护电路TVS 和共模电感不能省工业现场不是实验室CANH 和 CANL 两条线要沿着线槽和变频器动力线一起走几十米感应出来的浪涌电压轻轻松松超过收发器极限。SN65HVD230 这类收发器虽然内部有基本的 ESD 保护但针对 IEC 61000-4-2 级别的静电放电和感应浪涌还是不够。我在这块板子上加了三级防护从连接器往里依次是PESD1CAN一颗专门为 CAN 总线设计的双向 TVS 二极管放在 CANH/CANL 之间钳位差模过压。共模电感具体型号是 ACT45B-510-2P两个绕组分别串在 CANH 和 CANL 上。共模电感对差分信号几乎无影响但对共模噪声呈现出高阻抗能显著抑制变频器产生的共模干扰。收发器芯片的 Rs 引脚SN65HVD230 的斜率控制引脚。这个引脚悬空或接高电平是待机模式接低电平是高速模式。我们直接用 10kΩ 电阻到地让它工作在高速模式。2.4 PCB 布局上的两个细节PCB 布局对 CAN 通信的影响经常被低估。这块板子第一版打样回来后CAN 通信在桌面测试正常但装进设备外壳后就出现偶发错误帧。后来定位到一个重要原因收发器和排针连接器之间的走线太长且没有做阻抗控制。修改后的布局约束有两点一是 CAN 收发器尽量靠近连接器放置收发器到连接器的走线控制在 10mm 以内二是 CANH/CANL 两条走线要等长成对走线宽不低于 10mil间距尽量拉开避免两条线之间形成不必要的耦合电容。电源去耦电容 100nF 紧贴收发器电源引脚放置这点很多参考设计都会画但真正摆件时会因为空间紧张把电容放远了效果差很多。3. TWAI 总线机制与位时序不理解透这些代码写了也会翻车3.1 TWAI 控制器与 CAN 协议的对应关系ESP32 内部的 TWAI 控制器实现的是 CAN 协议的数据链路层和物理层中的编码部分它负责帧的封装、CRC 校验、位填充、仲裁和错误处理。所以我们写代码的时候不需要自己算 CRC、不需要手动做位填充驱动层封装好了这些底层细节。但理解帧结构仍然必要因为 debug 的时候需要读波形、看 CAN 分析仪上的报文解析最终都要回到帧结构上。一个标准数据帧从左到右依次是SOF 显性起始位、11 位 ID、RTR 位、IDE 位、DLC 数据长度码、最多 8 字节数据、15 位 CRC、CRC 定界符、ACK 槽、ACK 定界符、EOF 帧结束和 IFS 帧间隔。这里尤其要说 ACK 槽。发送节点在 ACK 槽阶段输出的是隐性电平总线上任意一个节点只要正确收到了这个帧就会在这个位置拉一个显性电平来应答。如果总线上没有其他节点或者接收节点校验失败发送节点看到 ACK 槽还是隐性电平就会报 ACK 错误。这个特性在调试时非常有用。3.2 位时序的组成和采样点CAN 总线上每个 bit 的时长在 500 kbps 下就是固定 2μs但一个 bit 内部被分成了若干时间量子 TQ。这些 TQ 分成四段同步段、传播段、相位缓冲段 1、相位缓冲段 2。同步段固定 1 个 TQ用来让总线上的节点对齐边沿。采样点位于相位缓冲段 1 和相位缓冲段 2 的交界处。理论上位时间长度的 50% 到 90% 之间都可以采样但工程上推荐把采样点放在 75% 到 85% 附近。原因很简单采样点太靠前总线传播延迟和信号上升下降时间带来的边沿偏移会让采样误判采样点太靠后留给相位缓冲段 2 的余量就不够时钟偏差稍大就会采到下一个 bit 里。3.3 500 kbps 位时序的推导过程这块板子的 ESP32 APB 时钟是 80 MHz。500 kbps 意味着每个 bit 所占用的周期数是 80 MHz / 500 kHz 160 个时钟周期。TWAI 控制器通过预分频器 BRP 把 APB 时钟再分频得到 TQ 时钟分频后一个 TQ 对应若干个 APB 周期。我实际配置采用了 brp 8这样 TQ 频率就是 10 MHz即每个 TQ 100ns。每个 bit 需要 2μs / 100ns 20 个 TQ。合理的分配是同步段 1 TQ传播段和相位缓冲段 1 合计 14 TQ相位缓冲段 2 是 5 TQ。采样点位置就是 (1 14) / 20 75%正好落在推荐区间。对应到 ESP-IDF 驱动代码里timing_config 结构体的目标是做同样的参数设置。不过实际操作时我在代码注释里保留了推导过程方便后续维护的人理解这些数字怎么来的而不是面对一个 magic number 不敢动。3.4 仲裁机制理解ID 越小为什么优先级越高CAN 总线仲裁机制是另一个容易踩坑的知识点。多个节点同时发送时总线上的电平是线与关系显性电平覆盖隐性电平。每个节点在发送 ID 的每一位时都会回读总线状态如果自己发的是隐性位但检测到总线上是显性电平就立即停止发送转为接收状态。这意味着 ID 数值小的帧在最高位第一次出现差异时就会赢得仲裁。高优先级消息永远不需要等待总线空闲才能抢到发送权这也是 CAN 被评为确定性强的原因。实际项目中应该把急停、报警这类需要低延迟的消息分配较小的 ID把周期上报的采集数据分配较大的 ID。这个原则写在交付文档里客户后续自己加节点时也会遵循。4. ESP-IDF 驱动层实现从初始化到收发一体的完整代码路径4.1 引入 TWAI 驱动头文件与引脚规划这个项目基于 ESP-IDF v5.x 开发。虽然 Arduino 框架也有 TWAI 库但我最终选择 ESP-IDF原因有三个驱动 API 更贴近 TWAI 控制器本身、错误状态反馈更细、长期运行的稳定性更好。外包项目交付之后客户要持续维护IDF 工程对后续裁剪也更友好。引脚规划上ESP32 的 TWAI 控制器允许把 TX 和 RX 映射到任意 GPIO。很多参考设计默认用 GPIO 21 做 TX、GPIO 22 做 RX但这个项目里这两个引脚被 LVGL 屏幕占用了所以我改成了 GPIO 4 和 GPIO 5。这里的一个注意事项是RX 输入引脚不要和板上其他高速翻转信号离得太近避免串扰导致误触发。4.2 驱动初始化的完整流程初始化的流程分三步先配置驱动三个结构体然后调用 twai_driver_install 安装驱动最后 twai_start 启动控制器。#include driver/twai.h #define TWAI_TX_PIN GPIO_NUM_4 #define TWAI_RX_PIN GPIO_NUM_5 void twai_init(void) { twai_general_config_t g_config { .mode TWAI_MODE_NORMAL, .tx_io TWAI_TX_PIN, .rx_io TWAI_RX_PIN, .clkout_io TWAI_IO_UNUSED, .bus_off_io TWAI_IO_UNUSED, .tx_queue_len 10, .rx_queue_len 20, .alerts_enabled TWAI_ALERT_ERR_PASS | TWAI_ALERT_BUS_OFF | TWAI_ALERT_RX_DATA | TWAI_ALERT_ERR_ACT | TWAI_ALERT_ERR_PASS, .intr_flags ESP_INTR_FLAG_LEVEL1, }; twai_timing_config_t t_config { .brp 8, .tseg_1 14, .tseg_2 5, .sjw 1, .triple_sampling false, }; twai_filter_config_t f_config TWAI_FILTER_CONFIG_ACCEPT_ALL(); twai_driver_install(g_config, t_config, f_config); twai_start(); }需要说明的是上面 t_config 的 brp、tseg_1、tseg_2 是直接结构体赋值的方式。如果不想手动算ESP-IDF 也提供了现成的宏比如 TWAI_TIMING_CONFIG_500KBITS()。但手动算一次能帮助理解位时序而且某些定制场景下宏不一定匹配实际晶振手动配置更可把控。4.3 发送函数从业务数据到 CAN 报文发送的核心是把业务数据装进 twai_message_t 结构体。这个结构体里的 identifier 字段是 IDdata_length_code 是数据长度data 是数据载荷。因为客户现场用的是扩展帧必须显式设置 TWAI_MSG_FLAG_EXTD 标志位。typedef struct { uint16_t temperature_x10; uint8_t vibration_level; uint8_t switch_state; uint8_t reserved[4]; } sensor_payload_t; void send_sensor_report(const sensor_payload_t *payload) { twai_message_t msg {0}; msg.identifier 0x18FF50E5; msg.flags TWAI_MSG_FLAG_EXTD; msg.data_length_code 8; memcpy(msg.data, payload, sizeof(sensor_payload_t)); if (twai_transmit(msg, pdMS_TO_TICKS(50)) ! ESP_OK) { ESP_LOGE(TWAI, transmit failed, err %d, twai_get_last_error()); } }发送时有一个特别容易被忽视的地方twai_transmit 的阻塞时间。如果总线上有持续的错误状态发送队列满了之后 transmit 会一直阻塞直到超时。我把超时时间设置成略大于一个最坏情况下的发送周期而不是填 portMAX_DELAY。这样即使总线异常业务任务也不会被彻底卡死还能通过日志把失败信息上报。4.4 接收任务与过滤规则接收端我单独建了一个 FreeRTOS 任务阻塞在 twai_receive 上。这里比较关键的一点是消息过滤配置。客户 PLC 下发命令的 ID 只有两个0x18FF2233 是启停控制0x18FF2244 是参数设置。如果我使用 ACCEPT_ALL 会收到总线上所有节点的帧对软件来说是浪费资源也增加了解析负担。正确的做法是配置验收滤波器让控制器只把关心的帧放进 RX 队列twai_filter_config_t f_config { .acceptance_code (0x18FF2000 3), // 只校验高16位匹配 0x18FF2xxx 范围 .acceptance_mask 0x0007FFFF, .single_filter true, };关于过滤器的位运算我强烈建议拿到 ESP32 技术参考手册对照着算一遍。不同 IDF 版本对 code 和 mask 的移位处理可能不同网上很多帖子给出的公式已经过时。最可靠的验证方式是配置完之后用上位机发几种不同 ID 的帧观察中断触发情况以实际测试结果为准。void twai_rx_task(void *arg) { twai_message_t msg; while (1) { if (twai_receive(msg, portMAX_DELAY) ESP_OK) { if (msg.identifier 0x18FF2233) { handle_start_stop_cmd(msg.data[0]); } else if (msg.identifier 0x18FF2244) { handle_parameter_cmd(msg.data); } } } }4.5 错误状态监控与总线恢复CAN 控制器内部有两个错误计数器发送错误计数器 TEC 和接收错误计数器 REC。错误计数超过一定阈值节点会进入错误被动状态再严重会进入 Bus-Off。这个状态不能等到通信完全断了才被业务代码感知我注册了 alerts 来监控。void twai_alert_task(void *arg) { uint32_t alerts; while (1) { if (twai_read_alerts(alerts, pdMS_TO_TICKS(1000)) ESP_OK) { if (alerts TWAI_ALERT_ERR_ACT) { ESP_LOGW(TWAI, back to active); } if (alerts TWAI_ALERT_ERR_PASS) { ESP_LOGW(TWAI, enter error passive); } if (alerts TWAI_ALERT_BUS_OFF) { ESP_LOGE(TWAI, bus off detected, recovering); twai_initiate_recovery(); } } } }Bus-Off 恢复这个点现场调试阶段大概率会用上。总线短路、强干扰或者波特率配置错误都可能导致节点进入 Bus-Off此时节点不会自动恢复发送必须调用 twai_initiate_recovery 或者重新 stop/start。最好的做法是像上面这样用一个独立任务监控 alert自动完成恢复流程同时把状态推到日志里方便远程排查。5. 联调排错示波器波形、ACK 错误和总线关断的真实排查过程5.1 第一版测试板通电一连串 ACK 错误硬件和驱动都写完最期待的联调环节来了。我拿来两块 ESP32 开发板各插一个 SN65HVD230 收发器模块用两根杜邦线把 CANH 和 CANL 对接以为马上就能看到数据互传结果控制台刷出来一屏 ACK 错误。当时第一反应是收发器模块坏了或者杜邦线接触不良。用万用表量了 CANH 和 CANL 之间的电阻发现是无穷大。问题出在我图省事直接把两块开发板的收发器模块对接但两个模块之间没有接终端电阻。CAN 总线上的 ACK 机制依赖收发器的差分输出驱动能力没有终端电阻时显性电平的差分幅度可能达不到接收节点的判定阈值于是接收节点没有正确应答发送节点就报 ACK 错误。解决办法是在收发器模块两端分别并联了 120Ω 电阻。这里并联上电阻后 CANH/CANL 之间静态阻值变成了 60Ω说明终端配置已正确。这个经历也验证了前面硬件设计里在 PCB 上做可选电阻的决策是必要的。5.2 用示波器看 CAN 波形三件必查的事接入终端电阻后两板通信正常了但后续第三块板加入总线后又开始出问题。这次我直接用示波器挂在 CANH 和 CANL 之间看差分波形排查分三步。第一步看幅值。正常显性位的差分电压应该在 1.5V 到 3V 之间如果看到的幅值偏低大概率是终端电阻并联过多或者总线负载过重。第二步看位时间。500 kbps 下每个 bit 时长 2μs示波器调到 1μs/格就能清晰看到帧起始 SOF 之后一长串方波如果每个 bit 宽度不对那就是位时序配置和实际晶振频率不匹配。第三步看边沿。上升沿和下降沿如果有明显振铃说明终端电阻匹配不好或者线缆过长。排查时发现第三块板导致的异常是典型的边沿振铃。原因是第三块板子没有终端电阻但连接线又比较长形成了反射。把板子上的可选终端电阻焊上后振铃明显消失。5.3 我能发不能收的过滤器坑联调中还遇到一个看似诡异的问题A 板发消息B 板收不到但 A 板自己的 CAN 分析仪能收到。一开始怀疑是 B 板硬件问题换一块板还是不行。后来检查 B 板的过滤配置发现验收掩码算错了把关心的 ID 也给过滤掉了。这个排查过程走了不少弯路但教训很深刻当软件里没有手动配置过滤时一定要用 ACCEPT_ALL 先验证物理链路确认链路通了再逐步收缩过滤范围。物理层通不通和过滤配得对不对是两个层面的问题混在一起排查会浪费大量时间。我当时先把 B 板过滤临时改成 ACCEPT_ALL马上就能收到 A 板消息证明链路没问题再回头对着手册重新算过滤掩码。5.4 工业现场偶发错误帧共地问题浮出水面实验室测试一切正常到了客户现场装上去问题开始变得玄学通信几分钟正常然后突然出现一批错误帧过一会儿又自己恢复。这种偶发性问题最难查幸好我在代码里接了报警日志从时间戳和错误计数变化找到了规律错误帧总是出现在现场某台大功率电机启动的瞬间。示波器测量 CANH 和 CANL 对地的共模电压发现电机启动瞬间共模电压尖峰接近 30V。虽然差分信号看起来正常但过高的共模电压超过了收发器输入共模范围导致接收端误判。根本原因不是 CAN 本身抗干扰差而是分站设备和 PLC 之间没有可靠共地地电位差在电机启动瞬间被拉大。解决这个问题的长期方案是用隔离收发器类似 ISO1042配合板载隔离电源让 CAN 总线侧与 ESP32 的电源地彻底分开。短期措施是检查现场接线把各节点的 24V 电源负极可靠接在一起减小地电位差。最终客户选择了暂不改硬件由现场施工统一整改地线同时我也在交付文档里写明了后续升级隔离方案的注意事项。5.5 周立功 USBCAN 在 Windows 11 下的驱动兼容问题程序调试阶段客户现场主要用了周立功 USBCAN 分析仪抓总线报文。对方工程师反馈在 Windows 11 笔记本上分析仪软件识别不到设备反复提示驱动异常。这个问题和 ESP32 本身无关但非常消耗联调时间。周立功的早期驱动版本确实存在 Win11 兼容性不佳的情况我的建议是去官网下载最新版本的驱动和上位机软件如果装了旧版本驱动需要先彻底卸载再装新版否则设备管理器里可能会残留一个带感叹号的设备。如果换了新版驱动仍然不行试试在设备管理器里手动更新驱动、指向安装包里的驱动目录通常能解决。6. 复盘如果再做一次我会改掉的三个决定6.1 从第一天就加入自环回自测代码这个项目一开始没有做自测代码硬件回来后第一件事就是接外部设备联调结果链路不通时很难判断是 ESP32 这边初始化失败还是外部设备的问题。如果一开始就在固件里留一个 TWAI 自环回模式也就是驱动配置里把 mode 设成 TWAI_MODE_SELF_TEST收发器外部无需接任何设备就能验证控制器本身工作是否正常。后续我会在所有交付项目里默认加这个自测固件。出厂前烧录自测 firmware硬件测试通过后再烧正式 firmware。这个习惯能省下大量现场排障时间。6.2 预留 CAN FD 的扩展空间项目开始时客户明确只要经典 CAN 2.0 帧所以收发器选型没有考虑 CAN FD。复盘时候我觉得在这种面向未来有升级可能性的项目里物理层芯片可以直接选支持 CAN FD 的型号比如 MCP2562FD 或者 TJA1044。经典 CAN 和 CAN FD 的物理层是兼容的只是波特率可能不同。选支持 FD 的收发器成本增加很少但后续客户想把总线速率提上去、传输更大的诊断数据时就不需要改硬件重新过认证了。当然要真正支持 CAN FD 帧ESP32 芯片本身也得支持所以在和客户谈需求时还是要讲清楚这一点避免对方以为换颗收发器就能升级。6.3 需求阶段就把 DBC 文件或者至少 ID 清单落到纸面项目中期出现过一次 ID 分配冲突客户后来新增了一个第三方节点占用了我们已经在用的 ID导致两套数据在总线上错乱。原因是需求阶段客户只口头说ID 你们自己规划没有形成统一的 ID 分配表。后来的解决方式是我起草了一份 ID 分配表规定了每个功能域的 ID 范围、帧周期、DLC 长度、字节序让客户确认后作为项目交付物之一。这看起来是文档工作但对嵌入式通信项目来说它就是协议本身。没有这份东西每个人按自己的习惯发数据联调就是互相猜谜。这个项目交付后客户又问过我可不可以把两个节点通信距离拉长到 100 米我说 500 kbps 下这个距离比较勉强建议要么降速到 125 kbps要么加中继。最后客户选择了降速改动成本很小位时序配置改一下就行。这也是当初把位时序推导过程写清楚的好处改起来心理有底。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门