ESP32 实战教程:用 TWAI 监听 CAN 总线并解析数据帧

本文由 AI 协助整理。ESP32 的 TWAI 是经典 CAN 控制器能力,不等于 CAN-FD;不同 ESP32 芯片和 Arduino-ESP32/ESP-IDF 版本支持情况不同。汽车和工业总线可能涉及安全关键系统,本文只做低风险监听和实验,不建议未经验证发送控制帧。

ESP32 TWAI 与 CAN 总线架构

TWAI、CAN 和收发器的关系

CAN 总线使用差分线 CANH、CANL。ESP32 内部的 TWAI 控制器负责帧格式、仲裁、CRC 和错误处理,但 GPIO 输出的是逻辑信号,不能直接接到 CANH/CANL。中间必须有 CAN 收发器,例如 3.3V 兼容的 SN65HVD230 一类芯片。

可以把它们分成三层:

  • ESP32 TWAI 控制器:负责 CAN 帧逻辑。
  • CAN 收发器:把 TX/RX 逻辑转换成差分总线。
  • CAN 网络:多个节点共享 CANH、CANL,线路两端配置终端。

缺少收发器,程序即使编译成功,也只是对着空气发送逻辑电平。收发器供电和逻辑电平还要与开发板匹配,不能因为两个模块都写着“CAN”就默认接口一样。

硬件准备与接线

  • 经典 ESP32 开发板。
  • 3.3V CAN 收发器模块。
  • 另一个 CAN 节点或 CAN 分析仪,便于验证。
  • 双绞线、稳定供电和必要的 120Ω 终端电阻。

示例使用 GPIO21 作为 RX、GPIO22 作为 TX:

ESP32 收发器
GPIO22 TXD
GPIO21 RXD
3V3 VCC
GND GND
- CANH / CANL

GPIO 可以通过矩阵调整,但要避开启动、Flash、摄像头和其他外设占用。CANH 与 CANL 要保持对应,不能把它当成 UART 的 TX/RX 交叉线。

总线终端和只听模式

高速 CAN 总线一般在物理两端各有 120Ω 终端,断电测量两线之间常见约 60Ω。桌面上两块短线模块有时已经内置终端;如果每块板都打开终端,等效阻值会过低,通信可能变差。

第一次接入现有总线,建议使用 TWAI_MODE_LISTEN_ONLY。只听模式不参与 ACK,也不会主动影响总线,适合查看帧 ID 和数据。只有在明确知道协议和安全后,才使用正常模式发送。

Arduino 中调用 TWAI 驱动

当前 Arduino-ESP32 示例使用底层头文件:

#include <Arduino.h>
#include "driver/twai.h"

constexpr gpio_num_t CAN_TX = GPIO_NUM_22;
constexpr gpio_num_t CAN_RX = GPIO_NUM_21;
bool driverStarted = false;

不同 Arduino-ESP32 核心版本可能对应不同 ESP-IDF TWAI API。若 driver/twai.h 或旧配置结构无法编译,应先查看当前核心随附的 TWAI 示例与 ESP-IDF 版本,不要盲目把网上另一版本的函数签名混进来。

初始化为 500 kbit/s、只听、接受全部帧:

void setupTwai() {
  twai_general_config_t general =
      TWAI_GENERAL_CONFIG_DEFAULT(CAN_TX, CAN_RX,
                                  TWAI_MODE_LISTEN_ONLY);
  twai_timing_config_t timing = TWAI_TIMING_CONFIG_500KBITS();
  twai_filter_config_t filter = TWAI_FILTER_CONFIG_ACCEPT_ALL();

  if (twai_driver_install(&general, &timing, &filter) != ESP_OK) {
    Serial.println("TWAI driver install failed");
    return;
  }

  if (twai_start() != ESP_OK) {
    Serial.println("TWAI driver start failed");
    return;
  }

  uint32_t alerts = TWAI_ALERT_RX_DATA |
                    TWAI_ALERT_BUS_ERROR |
                    TWAI_ALERT_RX_QUEUE_FULL |
                    TWAI_ALERT_ERR_PASS;
  if (twai_reconfigure_alerts(alerts, nullptr) != ESP_OK) {
    Serial.println("TWAI alert setup failed");
    return;
  }

  driverStarted = true;
  Serial.println("TWAI listen-only started");
}

如果总线不是 500 kbit/s,应选择匹配的 timing 宏,例如 250 kbit/s。波特率不匹配时,最常见的结果不是“数据稍微不准”,而是大量总线错误。

打印接收到的帧

void printFrame(const twai_message_t& message) {
  Serial.printf("ID=0x%03lx format=%s RTR=%d DLC=%d data=",
                static_cast<unsigned long>(message.identifier),
                message.extd ? "extended" : "standard",
                message.rtr,
                message.data_length_code);

  if (!message.rtr) {
    for (int i = 0; i < message.data_length_code; ++i) {
      Serial.printf("%02X ", message.data[i]);
    }
  }
  Serial.println();
}

void loop() {
  if (!driverStarted) {
    delay(1000);
    return;
  }

  twai_message_t message{};
  if (twai_receive(&message, pdMS_TO_TICKS(1000)) == ESP_OK) {
    printFrame(message);
  }
}

标准帧 ID 与扩展帧 ID 的位数不同,远端请求帧 RTR 也没有普通数据区。打印时保留格式、RTR、DLC 和原始字节,后续解析才不会因为只保存一个十六进制 ID 而失去上下文。

使用过滤器减少无关数据

总线监听时接受全部帧适合第一轮观察。确认目标 ID 后,可以把过滤器改成只接收某个标准 ID,降低 RX 队列压力。过滤器的位掩码写法和版本相关,学习阶段先用 ACCEPT_ALL,再参考当前 SDK 的过滤器示例修改,避免凭记忆写出一个“过滤了所有东西”的掩码。

应用层还可以在 printFrame() 后做白名单:

bool isInteresting(uint32_t id, bool extended) {
  return !extended && (id == 0x180 || id == 0x280 || id == 0x380);
}

只解析已确认的 ID。看到一个陌生帧时,不要直接把字节 0、1 当温度,CAN 只负责运送数据,含义由上层协议决定。

从原始字节推导数值

假设协议文档明确说明:ID 0x180 的字节 0、1 是大端无符号转速,单位为 1 rpm;字节 2 是温度,原始值减去 40:

void decode180(const twai_message_t& message) {
  if (message.identifier != 0x180 ||
      message.data_length_code < 3 || message.extd || message.rtr) {
    return;
  }

  uint16_t rpm = (static_cast<uint16_t>(message.data[0]) << 8) |
                 message.data[1];
  int temperature = static_cast<int>(message.data[2]) - 40;

  Serial.printf("rpm=%u temp=%d C\n", rpm, temperature);
}

没有协议文档时只能记录原始数据、时间和帧频率,不能把猜测写成结论。汽车 CAN 的同一个字节在不同车型和不同报文中可能代表完全不同的含义。

处理 TWAI 错误与队列溢出

void handleAlerts() {
  uint32_t alerts = 0;
  if (twai_read_alerts(&alerts, 0) != ESP_OK) return;

  twai_status_info_t status{};
  twai_get_status_info(&status);

  if (alerts & TWAI_ALERT_BUS_ERROR) {
    Serial.printf("bus errors=%lu\n",
                  static_cast<unsigned long>(status.bus_error_count));
  }

  if (alerts & TWAI_ALERT_RX_QUEUE_FULL) {
    Serial.printf("rx missed=%lu overrun=%lu\n",
                  static_cast<unsigned long>(status.rx_missed_count),
                  static_cast<unsigned long>(status.rx_overrun_count));
  }

  if (alerts & TWAI_ALERT_ERR_PASS) {
    Serial.println("controller became error passive");
  }
}

如果接收队列满,说明程序处理不过来或总线流量太高。不要只把队列调大;同时减少串口打印、增加过滤器、把原始帧放入 FreeRTOS 队列,或降低业务处理频率。

发送帧时的安全边界

发送需要正常模式,并且总线上必须有其他节点对帧 ACK。单独一块板在正常模式发送,可能不断重试并进入错误状态。更重要的是,汽车或工业总线上的控制帧可能改变真实设备行为。

本教程只做监听。若学习发送,应先用两块实验板、独立供电、低风险 ID 和逻辑分析仪验证,禁止把试验帧直接发到正在运行的车辆控制网络。

分阶段验证

  1. 先确认收发器为 3.3V 逻辑兼容。
  2. 两端终端电阻只保留物理总线两端。
  3. 使用只听模式和已知波特率。
  4. 先接受全部帧,保存原始日志。
  5. 按 ID、格式、DLC 建立白名单。
  6. 用文档和已知测试值验证字节解析。
  7. 模拟高流量,观察 RX 队列和错误统计。

常见问题

现象 优先检查
完全没有帧 收发器供电、CANH/CANL、波特率和总线是否真的有活动
大量 bus error 终端电阻、线序、波特率、收发器电平
只有发送没有接收 RX/TX 引脚、收发器方向和正常/只听模式
RX queue full 串口打印太多、没有过滤、业务处理太慢
编译找不到 TWAI API Arduino-ESP32 核心与示例版本不匹配
数值看起来乱 ID、字节序、缩放系数和协议文档

总结

TWAI/CAN 项目最容易被忽略的是物理层。ESP32 需要外部收发器,CAN 总线需要正确终端和波特率,监听程序还要保留帧格式、DLC、错误状态等上下文。

先以只听模式观察原始帧,再建立 ID 白名单和协议解析,最后才考虑发送。把安全边界放在代码之前,CAN 总线才会是学习工具,而不是给真实设备制造惊喜的遥控器。

参考资料

打赏

如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏码

打赏支持:王庆岩

posted on 2026-09-28 23:43  潇洒石头  阅读(27)  评论(0)    收藏  举报

导航