Arduino 上的 CAN 总线
UART(串行)、I2C 和 SPI 等通信协议非常流行,因为使用这些协议可以将多种外设连接到 Arduino。CAN(控制器局域网)是另一种类似的协议,虽然它通常不太流行,但在汽车领域有多种应用。
虽然本文不详细介绍 CAN 总线,但您可以在此处找到相关信息。不过,您需要了解以下几点 −
CAN 是一种基于消息的协议(即消息和内容比发送方更重要)。一个设备发送的消息会被所有设备接收,包括发送设备本身。
如果多个设备同时发送消息,优先级最高的设备将继续发送,其他设备则退出。请注意,由于 CAN 是基于消息的协议,因此 ID 分配给消息而不是设备。
它使用两条线路 CAN_H 和 CAN_L 进行数据传输。这两条线路之间的差分电压决定信号。高于阈值的正差表示 1,而负差表示 0。
网络中的设备称为节点。CAN 非常灵活,因为可以向网络添加新节点,也可以删除节点。网络中的所有节点仅共享两条线路。
数据传输以帧为单位。每个数据帧包含 11 个(基本帧格式)或 29 个(扩展帧格式)标识符位以及 0 到 8 个数据字节。
目前,Arduino Uno 不像支持 UART、SPI 和 I2C 那样直接支持 CAN。因此,我们将使用外部模块带有 TJA1050 收发器的 MCP2515,该模块通过 SPI 与 Arduino 连接,并使用 CAN 传输消息。

电路图
单个节点(发送器)的电路图如下所示。

如上所示,模块的 Vcc 线连接到 Arduino 的 5V,GND 连接到 GND,CS 连接到引脚 10,SO 连接到引脚 12 (MISO),SI 连接到引脚 11 (MOSI),SCK 连接到引脚 13 (SCK)。
在接收端,连接类似,只是模块的 INT 引脚连接到Arduino。

发射器和接收器通过 CAN_H 和 CAN_L 线连接在一起(CAN_H 到 CAN_H,CAN_L 到 CAN_L)。
所需库
我们将使用 Seeed Studio 中的这个库。Arduino 的库管理器中找不到此库。下载第三方库的步骤如下 − https://www.tutorialspoint.com/using-a-third-party-library-in-arduino
代码演练
安装库后,您可以在文件 -> 示例 -> 中找到 send 和 receive_interrupt 示例。 CAN_BUS 屏蔽。。

我们将使用这些示例的稍微简化版本。
SEND 的代码如下 −
#include <SPI.h>
#include "mcp2515_can.h"
onst int SPI_CS_PIN = 10;
mcp2515_can CAN(SPI_CS_PIN); // Set CS pin
void setup() {
Serial.begin(115200);
while(!Serial){};
// 初始化 CAN 总线:波特率 = 500k
while (CAN_OK != CAN.begin(CAN_500KBPS)) {
Serial.println("CAN 初始化失败,重试...");
delay(100);
}
Serial.println("CAN 初始化成功!");
}
unsigned char stmp[8] = {0, 0, 0, 0, 0, 0, 0, 0};
void loop() {
// 发送数据:id = 0x00,标准帧,数据长度 = 8,stmp:数据缓冲区
stmp[7] = stmp[7] + 1;
if (stmp[7] == 100) {
stmp[7] = 0;
stmp[6] = stmp[6] + 1;
if (stmp[6] == 100) {
stmp[6] = 0;
stmp[5] = stmp[5] + 1;
}
}
CAN.sendMsgBuf(0x00, 0, 8, stmp);
delay(100); // 每 100 毫秒发送一次数据
Serial.println("CAN BUS sendMsgBuf ok!");
}
大部分代码都一目了然。我们将简要介绍其实现。
我们使用片选引脚(在本例中为引脚 10)初始化 CAN。
mcp2515_can CAN(SPI_CS_PIN); // Set CS pin
我们在设置中将波特率设置为 500 kbps,并检查 CAN 是否正常启动。
CAN.begin(CAN_500KBPS))
我们在每个帧中向接收端发送 8 个字节。这些字节一开始都是 0。在循环中,我们不断增加最后一个字节,直到它达到 100,然后将倒数第二个字节加 1,再次不断增加最后一个字节,直到它达到 100,依此类推。如果倒数第二个字节达到 100,我们就增加倒数第三个字节。这样我们就有多次迭代可以进行。
重要的函数是 −
CAN.sendMsgBuf(0x00, 0, 8, stmp);
第一个参数是消息的 ID (0x00),第二个参数表示使用的是基本格式还是扩展格式(0 表示基本格式,1 表示扩展格式),第三个参数是数据的长度 (8),第四个参数是数据缓冲区。
这将持续向接收方发送 8 字节数据帧。
RECEIVE 的代码如下所示 −
#include <SPI.h>
#include "mcp2515_can.h"
const int SPI_CS_PIN = 10;
const int CAN_INT_PIN = 2;
mcp2515_can CAN(SPI_CS_PIN); // Set CS pin
unsigned char flagRecv = 0;
unsigned char len = 0;
unsigned char buf[8];
char str[20];
void setup() {
Serial.begin(115200);
while (!Serial) {
; // 等待串口连接。仅适用于原生 USB 端口
}
// 启动中断
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), MCP2515_ISR, FALLING);
// 初始化 CAN 总线:波特率 = 500k
while (CAN_OK != CAN.begin(CAN_500KBPS)) {
Serial.println("CAN 初始化失败,重试...");
delay(100);
}
Serial.println("CAN init ok!");
}
void MCP2515_ISR() {
flagRecv = 1;
}
void loop() {
if (flagRecv) {
// 检查是否获取数据
flagRecv = 0; // 清除标志
Serial.println("进入循环");
// 遍历所有待处理的消息
// 如果总线饱和或 MCU 繁忙,
// 两个 RX 缓冲区可能都在使用中,并且正在读取单个
// 消息不会清除 IRQ 条件。
while (CAN_MSGAVAIL == CAN.checkReceive()) {
// 读取数据,len:数据长度,buf:数据缓冲区
Serial.println("checkReceive");
CAN.readMsgBuf(&len, buf);
// 打印数据
for (int i = 0; i < len; i++) {
Serial.print(buf[i]); Serial.print("\t");
}
Serial.println();
}
}
}
如您所见,初始变量声明和设置与 SEND 类似。唯一的区别是中断连接到数字引脚 2。请记住,每当收到消息时,模块的 INT 引脚都会变为低电平。因此,下降沿中断连接到引脚 2,而引脚 2 连接到模块的 INT 引脚。
// 启动中断 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), MCP2515_ISR, FALLING);
在 MCP2515_ISR 函数中,我们只需将 flagRecv 设置为 1,并在循环内进行检查。
当 flagRecv 值为 1 时,将检查 CAN 缓冲区中的可用数据(使用 CAN.checkReceive()),并将其读取并打印在串行监视器上。读取数据的函数是 −
CAN.readMsgBuf(&len, buf);
第一个参数表示可用数据的长度,第二个参数是用于存储传入数据的缓冲区。
希望您喜欢这篇文章。建议您尝试一下此库附带的其他示例。

