Tasmota 中的 MCP2515 CAN 总线库:从 SPI 驱动原理到收发实战
【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota
导读
本指南以 arduino-mcp2515 库 README 为主线,系统讲解基于 MCP2515 CAN 控制器 + MCP2551 收发器的 CAN 总线扩展方案:包括库安装、初始化流程、帧格式、发送/接收、掩码过滤等全部核心 API,并结合 Tasmota 仓库内xsns_87_mcp2515.ino与xsns_87_can_sniffer.ino的真实集成代码,揭示底层 SPI 寄存器操作原理。读完本文,你将能够独立完成一块 CAN 节点的初始化、收发、过滤配置,并理解其在 Tasmota 固件中的落地方式。
CAN 总线与 MCP2515 硬件背景
CAN-BUS(Controller Area Network)是一种常见的工业总线,具备传输距离远、通信速率适中、可靠性高的特点,广泛用于现代机床、工业现场以及车载诊断总线。本库针对的 CAN-BUS Shield 由两部分组成:
- MCP2515:Microchip 的独立 CAN 协议控制器,通过SPI 接口与主控(Arduino/ESP8266/ESP32)通信;
- MCP2551:CAN 收发器,负责把控制器逻辑电平转换为 CAN 差分信号接入总线。
配合 OBD-II 转接线与 OBD-II 库,即可构建车载诊断设备或数据记录器。该库核心特性(见 README):
- 支持 CAN V2.0B,最高速率1 Mb/s;
- SPI 接口最高10 MHz(源码中
SPI_CLOCK = 10000000,见 mcp2515.h); - 支持标准帧(11 位 ID)与扩展帧(29 位 ID)的数据帧与远程帧;
- 双接收缓冲器,支持优先级消息存储。
库安装
- 下载库的 ZIP 压缩包;
- 在 Arduino IDE 中通过
Sketch -> Include Library... -> Add .ZIP Library...添加; - 重启 Arduino IDE,即可在示例中看到
mcp2515库的示例程序。
该库在 Tasmota 仓库中的位置为 lib/lib_div/arduino-mcp2515-1.0.1,由头文件mcp2515.h、实现mcp2515.cpp、CAN 帧定义can.h及两个示例 CAN_write.ino、CAN_read.ino 组成。
1. 初始化
创建与 MCP2515 的连接时,需要提供 SPI CS 片选引脚号(默认 10)、波特率和运行模式。
工作模式
mcp2515.setNormalMode(); // 正常模式:真正收发总线消息 mcp2515.setLoopbackMode(); // 回环模式:自测,消息不会上总线 mcp2515.setListenOnlyMode(); // 仅监听:只收不发,适合嗅探此外mcp2515.h还暴露了setConfigMode()(配置模式,设置位速率/过滤前自动进入)与setSleepMode()(休眠模式)。
支持的波特率
enum CAN_SPEED { CAN_5KBPS, CAN_10KBPS, CAN_20KBPS, CAN_31K25BPS, CAN_33KBPS, CAN_40KBPS, CAN_50KBPS, CAN_80KBPS, CAN_83K3BPS, CAN_95KBPS, CAN_100KBPS, CAN_125KBPS, CAN_200KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS };初始化示例
MCP2515 mcp2515(10); mcp2515.reset(); mcp2515.setBitrate(CAN_125KBPS); mcp2515.setLoopbackMode();指定晶振频率
部分模块板载晶振不是 16 MHz,可以在设置位速率时显式指定振荡器频率:
mcp2515.setBitrate(CAN_125KBPS, MCP_8MHZ);可选时钟频率:
enum CAN_CLOCK { MCP_20MHZ, MCP_16MHZ, MCP_8MHZ };默认值为MCP_16MHZ(见 mcp2515.cpp)。
注意:在 CAN 高速传输下如需经 UART 转发数据,务必同步提高 UART 波特率,避免串口成为瓶颈。
初始化原理(源码视角)
构造函数内部即调用SPI.begin()并将 CS 引脚置为输出高电平(mcp2515.cpp)。reset()通过 SPI 发送INSTRUCTION_RESET(0xC0)复位芯片,随后清零三个发送缓冲器与两个接收缓冲器,并配置CANINTE中断使能(RX0IF/RX1IF/ERRIF/MERRF),同时把两个接收缓冲器设为标准+扩展帧模式并启用 RXB0 的 BUKT 滚动接收(mcp2515.cpp)。
setBitrate()内部会先调用setConfigMode()进入配置模式,再根据(时钟 × 波特率)查表写入CNF1/CNF2/CNF3三个位时序配置寄存器——这张查表共覆盖 8/16/20 MHz 三种时钟下的各档波特率(见 mcp2515.h)。所有setXxxMode()最终都调用setMode():写入CANCTRL_REQOP后轮询CANSTAT寄存器确认模式切换成功,10 ms 超时未达成返回ERROR_FAIL(mcp2515.cpp)。
2. 帧数据格式
库采用 Linux 内核 SocketCAN 风格的帧结构,定义于 can.h:
struct can_frame { canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ __u8 can_dlc; /* 帧负载长度(0 .. CAN_MAX_DLEN) */ __u8 data[CAN_MAX_DLEN] __attribute__((aligned(8))); };其中can_id是 32 位无符号整数,高三位携带帧属性标志(can.h):
| 标志宏 | 位 | 含义 |
|---|---|---|
CAN_EFF_FLAG | bit 31 | 1 = 扩展帧格式(29 位 ID),0 = 标准帧(11 位 ID) |
CAN_RTR_FLAG | bit 30 | 1 = 远程传输请求帧(RTR) |
CAN_ERR_FLAG | bit 29 | 1 = 错误消息帧 |
CAN_SFF_MASK(0x000007FF):标准帧格式 11 位 ID 掩码;CAN_EFF_MASK(0x1FFFFFFF):扩展帧格式 29 位 ID 掩码。
can_dlc为数据长度码,最大 8 字节(CAN_MAX_DLEN),与 ISO 11898-1 定义一致。
3. 发送数据
MCP2515::ERROR sendMessage(const MCP2515::TXBn txbn, const struct can_frame *frame); MCP2515::ERROR sendMessage(const struct can_frame *frame);第一个版本指定发送缓冲器(TXB0/TXB1/TXB2三个,见 mcp2515.h),第二个版本由库自动选择空闲缓冲器。
发送标准帧
struct can_frame frame; frame.can_id = 0x000; frame.can_dlc = 4; frame.data[0] = 0xFF; frame.data[1] = 0xFF; frame.data[2] = 0xFF; frame.data[3] = 0xFF; /* 发送到总线,告知其他设备这是来自 0x00 的标准帧 */ mcp2515.sendMessage(&frame);发送扩展帧
struct can_frame frame; frame.can_id = 0x12345678 | CAN_EFF_MASK; // 置 EFF 标志,声明 29 位 ID frame.can_dlc = 2; frame.data[0] = 0xFF; frame.data[1] = 0xFF; /* 使用第二个发送缓冲器发送,告知其他设备这是来自 0x12345678 的扩展帧 */ mcp2515.sendMessage(MCP2515::TXB1, &frame);发送实现原理
sendMessage(frame)无缓冲器版本首先校验can_dlc不超过 8,随后依次查询三个 TX 缓冲器的TXB_TXREQ位,找到空闲缓冲器调用带缓冲器参数的版本;若全部繁忙返回ERROR_FAILTX(mcp2515.cpp)。实际写入时:
- 根据
CAN_EFF_FLAG/CAN_RTR_FLAG拆解 ID,由prepareId()填充 SIDH/SIDL/EID8/EID0 寄存器(mcp2515.cpp); - RTR 帧在 DLC 字节上置
RTR_MASK(0x40); - 通过
setRegisters()一次性写入 ID+DLC+数据共 5+dlc 字节,最后置TXB_TXREQ请求发送(mcp2515.cpp)。
完整可运行的发送示例见 CAN_write.ino:其初始化序列为mcp2515.reset(); mcp2515.setBitrate(CAN_125KBPS); mcp2515.setNormalMode();,然后在loop()中每 100 ms 发送两帧(ID 0x0F6 与 0x036)。
4. 接收数据
MCP2515::ERROR readMessage(const MCP2515::RXBn rxbn, struct can_frame *frame); MCP2515::ERROR readMessage(struct can_frame *frame);第一个版本从指定接收缓冲器读取(RXB0/RXB1),第二个版本自动选择。若已配置掩码和过滤器,则只能收到符合过滤条件的帧。接收有两种方式:轮询与中断。
方式一:轮询读取
struct can_frame frame; void loop() { if (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { // frame 包含接收到的消息 } }readMessage(frame)通过READ_STATUS(0xA0)指令读取状态,按 RX0IF/RX1IF 优先级依次读取缓冲器;若无消息返回ERROR_NOMSG(mcp2515.cpp)。
方式二:中断读取
bool interrupt = false; struct can_frame frame; void irqHandler() { interrupt = true; } void setup() { ... attachInterrupt(0, irqHandler, FALLING); // MCP2515 INT 引脚(低电平有效) } void loop() { if (interrupt) { interrupt = false; uint8_t irq = mcp2515.getInterrupts(); if (irq & MCP2515::CANINTF_RX0IF) { if (mcp2515.readMessage(MCP2515::RXB0, &frame) == MCP2515::ERROR_OK) { // frame 包含来自 RXB0 的消息 } } if (irq & MCP2515::CANINTF_RX1IF) { if (mcp2515.readMessage(MCP2515::RXB1, &frame) == MCP2515::ERROR_OK) { // frame 包含来自 RXB1 的消息 } } } }getInterrupts()直接读取CANINTF中断标志寄存器,其位定义(mcp2515.h):
| 标志 | 值 | 含义 |
|---|---|---|
CANINTF_RX0IF | 0x01 | 接收缓冲器 0 满 |
CANINTF_RX1IF | 0x02 | 接收缓冲器 1 满 |
CANINTF_TX0IF/TX1IF/TX2IF | 0x04/0x08/0x10 | 发送完成中断 |
CANINTF_ERRIF | 0x20 | 错误中断 |
CANINTF_WAKIF | 0x40 | 唤醒中断 |
CANINTF_MERRF | 0x80 | 报文错误中断 |
读取完成后readMessage()会清除对应CANINTF_RXnIF位(mcp2515.cpp),避免中断反复触发。
5. 设置接收掩码与过滤器
控制器芯片上有2 个接收掩码寄存器(MASK0/MASK1)和5 个过滤器寄存器(RXF0~RXF5),保证只接收目标设备的数据,在大规模多节点网络中尤为实用。库提供两个函数:
MCP2515::ERROR setFilterMask(const MASK mask, const bool ext, const uint32_t ulData) MCP2515::ERROR setFilter(const RXF num, const bool ext, const uint32_t ulData)参数说明:
- MASK mask:两个掩码之一,
MCP2515::MASK0或MCP2515::MASK1; - RXF num:六个验收过滤器之一,
MCP2515::RXF0至MCP2515::RXF5(RXF0/RXF1 归属 MASK0,RXF2~RXF5 归属 MASK1); - ext:帧类型,
false表示标准帧的掩码/过滤器,true表示扩展帧的; - ulData:掩码或过滤器的内容(11 位或 29 位 ID 值)。
注意:setFilterMask()与setFilter()内部都会先自动进入配置模式(setConfigMode()),写入 RXM0SIDH/RXM1SIDH 或 RXF0SIDH~RXF5SIDH 寄存器(mcp2515.cpp),因此必须在reset()之后、进入正常模式之前调用,否则会打断正常收发。
典型的过滤思路:掩码决定 ID 中哪些位必须参与匹配(1 = 必须匹配,0 = 忽略),过滤器给出这些位上期望的值。例如要只接收 ID 低 8 位为 0x55 的标准帧,可设置setFilterMask(MASK0, false, 0x055)配合setFilter(RXF0, false, 0x055)。
6. 在 Tasmota 固件中的集成实践
该库在 Tasmota 仓库中被两个传感器驱动直接引用:
- xsns_87_mcp2515.ino:完整 MCP2515 CAN 总线支持(默认
CAN_500KBPS、MCP_8MHZ),默认启用 BMS 客户端,可解析 SMA / Freedom Won 电池管理系统数据(SOC、SOH、电压、电流、温度、容量等字段); - xsns_87_can_sniffer.ino:CAN 嗅探器模式(默认
CAN_100KBPS),提供Can、CanSend控制台命令,可通过 JSON 直接向总线发送报文:
void CmndCanSend(void) { JsonParser parser(XdrvMailbox.data); ... uint16_t id = root.getUInt(PSTR("ID"), 0); uint16_t len = root.getUInt(PSTR("LEN"), 0); ... mcp2515->sendMessage(&canMsg); }即控制台输入形如CanSend {"ID":1234,"LEN":2,"DATA":[0xFF,0xFF]}即可通过 MCP2515 发出 CAN 帧。
两个驱动互斥:启用USE_MCP2515时会自动禁用USE_CANSNIFFER(反之亦然),且均需同时启用USE_SPI。典型接线(Tasmota/ESP8266):
| MCP2515 | ESP8266 | Tasmota |
|---|---|---|
| INT | 未使用 | - |
| SCK | GPIO14 | SPI CLK |
| SI | GPIO13 | SPI MOSI |
| SO | GPIO12 | SPI MISO |
| CS | GPIO0~5, 15, 16 | MCP2515 |
| Gnd | Gnd | Gnd |
| VCC | Vin/5V | Vin/5V |
可配置编译宏(在两个驱动中均有默认值):MCP2515_BITRATE、MCP2515_CLOCK、MCP2515_MAX_FRAMES、CAN_KEEP_ALIVE_SECS(默认 300 秒)、MCP2515_TIMEOUT(默认 10)。
7. 示例与参考资料
- 基于 CanHacker(lawicel)协议实现的 USB-CAN 设备示例:
can-usb项目(README 中有外部链接); - Seeed Studio CAN-BUS Shield 官方 wiki 提供了更详细的硬件说明;
- 帧结构与 Linux SocketCAN 规范一致,可参考内核网络文档
can.txt。
许可与致谢
该库由 Seeed Studio 的 loovee 编写,后由 Dmitry(autowp)更新维护,采用MIT 许可证(详见 LICENSE.md)。Tasmota 中基于该库实现的xsns_87系列驱动则以 GPL-3.0 授权发布。
结语
从初始化、帧封装到双缓冲接收、掩码过滤,arduino-mcp2515 库将 MCP2515 繁琐的 SPI 寄存器操作封装为直观的 C++ API,而其与 SocketCAN 一致的帧结构也降低了 Linux 与嵌入式之间的移植成本。结合 Tasmota 的xsns_87驱动,一块普通的 ESP8266/ESP32 + MCP2515 模块即可摇身变成 CAN 嗅探器、诊断节点或 BMS 数据网关,是低成本接入工业 CAN 总线网络的实用方案。
【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考