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);

第一个参数指示可用数据的长度,第二个参数是存储传入数据的缓冲区。

希望您喜欢这篇文章。建议您浏览此库附带的其他示例。

更新于:2021 年 7 月 24 日

11K+ 次查看

开启您的职业生涯

通过完成课程获得认证

开始学习
广告