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);
第一个参数指示可用数据的长度,第二个参数是存储传入数据的缓冲区。
希望您喜欢这篇文章。建议您浏览此库附带的其他示例。