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 的 2 号引脚。

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

我们将使用这些示例的稍微简化版本。
下面给出了 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){};
// init can bus : baudrate = 500k
while (CAN_OK != CAN.begin(CAN_500KBPS)) {
Serial.println("CAN init fail, retry...");
delay(100);
}
Serial.println("CAN init ok!");
}
unsigned char stmp[8] = {0, 0, 0, 0, 0, 0, 0, 0};
void loop() {
// send data: id = 0x00, standrad frame, data len = 8, stmp: data buf
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); // send data per 100ms
Serial.println("CAN BUS sendMsgBuf ok!");
}大部分代码是不言自明的。我们将简要介绍一下实现。
我们使用芯片选择引脚(在本例中为引脚 10)初始化 CAN。
mcp2515_can CAN(SPI_CS_PIN); // Set CS pin
我们在 Setup 中将波特率设置为 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) {
; // wait for serial port to connect. Needed for native USB port only
}
// start interrupt
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), MCP2515_ISR, FALLING);
// init can bus : baudrate = 500k
while (CAN_OK != CAN.begin(CAN_500KBPS)) {
Serial.println("CAN init fail, retry...");
delay(100);
}
Serial.println("CAN init ok!");
}
void MCP2515_ISR() {
flagRecv = 1;
}
void loop() {
if (flagRecv) {
// check if get data
flagRecv = 0; // clear flag
Serial.println("into loop");
// iterate over all pending messages
// If either the bus is saturated or the MCU is busy,
// both RX buffers may be in use and reading a single
// message does not clear the IRQ conditon.
while (CAN_MSGAVAIL == CAN.checkReceive()) {
// read data, len: data length, buf: data buf
Serial.println("checkReceive");
CAN.readMsgBuf(&len, buf);
// print the data
for (int i = 0; i < len; i++) {
Serial.print(buf[i]); Serial.print("\t");
}
Serial.println();
}
}
}如您所见,初始变量声明和设置与 SEND 类似。唯一的区别是将中断附加到数字引脚 2。请记住,每当接收到消息时,模块的 INT 引脚都会变为低电平。因此,将下降沿中断附加到连接到模块 INT 引脚的 2 号引脚。
// start interrupt attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), MCP2515_ISR, FALLING);
在 MCP2515_ISR 函数中,我们只需将**flagRecv**设置为 1,并在循环内检查它。
当**flagRecv**值为 1 时,将检查 CAN 缓冲区中是否有可用数据(使用**CAN.checkReceive()**)并读取并打印到串行监视器上。读取数据的函数是:
CAN.readMsgBuf(&len, buf);
第一个参数指示可用数据的长度,第二个参数是存储传入数据的缓冲区。
希望您喜欢这篇文章。建议您浏览此库附带的其他示例。
数据结构
网络
关系数据库管理系统
操作系统
Java
iOS
HTML
CSS
Android
Python
C 语言编程
C++
C#
MongoDB
MySQL
Javascript
PHP