前言
很多计算机从业者对CAN总线这种通信协议一知半解甚至一头雾水,可能是由于其主要应用于汽车工业领域,让人觉得几分神秘。其实,作为计算机从业者,面对各种网络协议,其复杂程度可以吊打CAN总线协议。所以本文作为系列课程的第一课,就是要揭开CAN总线的神秘面纱,让读者在动手写代码并调试的过程中体会CAN总线协议的工作原理。
愿景是让您车上实操,看看汽车到底在传输什么CAN总线消息。对了,首先要有辆车。当然,这是玩笑。在没有车的情况下,可以模拟CAN总线消息的传输,同样可以理解CAN总线的运行机制。
接下来,我们从开发板、CAN模块,软件开发环境等各方面分别介绍,目标是第一个CAN总线消息收发程序能够运行起来!
使用Arduino DUE开发板
Arduino DUE是一个基于Atmel SAM3X8E ARM Cortex-M3 CPU的微控制器开发板。同时,它也是第一款基于32位的ARM核心微控制器。有54个数字输入输出针脚(其中12个可以被用作PWM输出),12个模拟信号输入,4个UART(硬件串口)。
详细配置如下表所示,
微处理器 | AT91SAM3X8E |
---|---|
运行电压 | 3.3V |
输入电压 (推荐) | 7-12V |
输入电压 (极限) | 6-16V |
数字I/O针脚 | 54 (其中12 个PWM) |
模拟信号输入 | 12 |
模拟信号输出 | 2 (DAC) |
全部I/O线输出直流电流 | 130 mA |
3.3V针脚输出直流电流 | 800 mA |
5V针脚输出直流电流 | 800 mA |
闪存 | 所有可用于用户程序共512 KB |
SRAM | 96 KB (两个bank: 64KB 和 32KB) |
时钟速度 | 84 MHz |
注意,不同于其他Arduino家族开发板运行电压是5V,Arduino DUE运行电压是3.3V。超过这个电压可能会损坏开发板。
Arduino DUE开发板实物图
Arduino DUE针脚定义
下图是Arduino DUE针脚定义。注意左下角CANRX0、CANTX0,这引出了本文的重点,也就是说Arduino DUE原生支持CAN总线(关于CAN总线,后面会介绍)控制器模块。但是貌似只支持一个(RX和TX分别是CAN模块的收发信号线)CAN控制器模块?至于为什么需要多于一个CAN控制器模块,后面会揭晓。
Arduino DUE对CAN控制器模块的支持
打开顶部Atmel SAM3X8E ARM Cortex-M3 CPU链接,找到如下Block Diagram,注意CAN0、CAN1。
这说明,在微控制器级别,该MCU支持两个CAN控制器模块,而且暴露出CANRX0和CANTX0、CANRX1和CANTX1共4个针脚。当然,MCU支持不等于开发板支持,因为MCU针脚不一定被开发板暴露。
很庆幸,经过查阅相关资料,确认MCU里4个CAN针脚全部被Arduino DUE暴露,只是Arduino DUE官方文档没有明说CANRX1和CANTX1。实际CAN0和CAN1如下图所示,
好了,现在从硬件角度来说,Arduino DUE内置了两个CAN控制器模块。注意,这里说的时控制器模块,但就像计算机理有USB控制器模块,不等于有USb外设。要想实现诸如键盘、鼠标等USB外设的通信,还需要有USB设备。类似地,如果想实现CAN通信还需要有个CAN总线收发器模块。不过,为了不至于制造困惑,接下来我们来讨论什么是CAN总线,然后再回来讨论CAN总线收发器模块。
CAN总线简介
CAN是Controller Area Network的简称,中文翻译为控制器局域网总线。CAN总线是一种用于实时串行通信协议总线,它可以使用两根交缠在一起的双绞线来传输信号。CAN协议用于汽车中各种不同元件之间通信,以取代昂贵笨重的配电线束。该协议的健壮性使其用途延伸到其他自动化和工业领域。
简单来说,
假设(注意用词)有两根线,均长约40米(和大刀无关,为什么40米,后面课程会说),一根命名为CAN High,一个命名为CAN Low,对应隐形电平和显性电平(这个后面课程会详细介绍)。
这两根线为了抵消彼此因为高频电流(电平同时变化)产生的电磁干扰,即共模消除干扰,采用双绞线的形式两者绞缠(类似于网线里面俩俩绞缠)在一起。
Arduino DUE的CAN0总线控制器连接到CAN0总线收发器,收发器有两根线,一根是CAN High,一根是CAN Low。这两根线的CAN High和CAN Low分别连接到40米双绞线的CAN High和CAN Low。
CAN Hig电压介于2.5V ~ 3.5V;CAN Low电压介于1.5V ~ 2.5V,从而High和Low形成差分信号。注意,两根线不可错接。
同样的,Arduino DUE的CAN1总线控制器连接到CAN1总线收发器,收发器有两根线,一根是CAN High,一根是CAN Low。这两根线的CAN High和CAN Low分别连接到40米双绞线的CAN High和CAN Low。注意,不可错接。
这样,CAN0和CAN1都连接到40米的双绞线,两者通过这种方式建立了连接。
这就像居民楼里入户的220V交流电电缆,每户都是2根线,零线和火线,各种用电设备都接在这连根线上。理论上,所有变电站下方的220V用电设备,其两根火线或两根零线其电位一致。
把居民入户220V交流电缆和CAN总线相比不同之处在于,CAN总线任意两个节点直接都可以发送消息。
另外,有些人可能疑惑,为什么一直在纠结是不是两个CAN模块呢?难道一个模块不行吗?CAN总线协议规定,一个CAN帧必须得到至少一个除了本节点的其他节点的确认(Acknowledgement),否则视为出错。所以,如果有两个或者两个以上的CAN控制器即收发器模块便于测试,尤其继集成在个开发板上。这也是为什么我们入门的第一课选择了Arduino DUE。
关于CAN总线线介绍到这里,详细介绍请关注后续课程。
CAN收发器和控制器
CAN收发器和控制器分别对应CAN总线的物理层和数据链路层。
CAN收发器
CAN收发器是将差分信号转换成TTL (Transistor Transistor Logic)电平信号,或者将TTL电平信号转成差分信号。
- CAH High - CAN Low = 2.5V - 2.5V = 0.0V,显性电平。
- CAH High - CAN Low = 3.5V - 1.5V = 2.0V,隐形电平。
CAN控制器
CAN控制器实现CAN总线的协议栈以及数据链路层,
- 用于生成CAN数据帧并以二进制的方式发送。并在此过程中进行位填充、添加CRC校验、应答检测等。
-
用于将接收到的二进制进行解析,并在此过程中进行收发比对、去位填充、执行CRC校验等操作。
连接CAN收发器到Arduino DUE开发板
准备好了知识,现在可以开始上手了。首先硬件准备。
设备准备
按照下表来准备设备。
设备 | 型号 | 数量 | 是否必选 |
---|---|---|---|
开发板 | Arduino DUE | 1 | 是 |
CAN收发器 | 任何集成SN65HVD230芯片收发器。比如微雪、丢石头等品牌。 | 2 | 是 |
面包板 | 任何5排以上的面包板 | 1 | 否 |
跳线 | 任何公-公(使用面包板)、或者公-母(不使用面包板)针跳线。 | 若干 | 是 |
设备连线
按照下表来连接设备。
开发板 | CAN收发器0 | CAN收发器1 |
---|---|---|
CAN High | CAN High | |
CAN Low | CAN Low | |
3.3V | 3.3V | 3.3V |
GND | GND | GND |
CANRX | RX | |
CANTX | TX | |
DAC0 | RX | |
53 | TX |
参考图片如下,
检查无误后,插上MicroUSB,建议使用Native USB Port(离电源插口远的插口)上电。
留一个疑问?双绞线在哪?
使用Arduino IDE开发
请自行安装Arduino IDE。安装完成后,运行Arduino IDE,打开BOARDS MANAGER,搜关键字“Due”,然后点“INSTALL”安装。
安装完开发板,在安装Library。打开LIBRARY MANAGER,搜索“can_due”,滚动到最下方,点“INSTALL”安装,
打开”File“,然后选择‘Save“,可以命名为“CANTransceiver”,此时,项目如下所示,
接下来我们通过代码在两个收发器之间传送消息。
#include <due_can.h>
// Leave the macro defined in the case you use the Native USB Port.
// Comment out if you use then Programming Port.
#define Serial SerialUSB
#define MAXIMUM_CAN_FRAME_DATA_LENGTH 8
#define MAXIMUM_FRAMES_NUMBER 5000
void setup()
{
Serial.begin(115200);
while (!Serial);
if (Can0.begin(CAN_BPS_1000K) && Can1.begin(CAN_BPS_1000K))
Serial.println("CAN0 and CAN1 initialized.");
else
Serial.println("Failed to initialize the CAN devices.");
echo();
}
void loop()
{
// put your main code here, to run repeatedly:
}
static void echo(void)
{
CAN_FRAME frame1, frame2, incoming;
uint32_t id = random(0x00000001, 0x00FFFDFF);
Serial.println(id, HEX);
frame1.id = id;
frame1.length = MAXIMUM_CAN_FRAME_DATA_LENGTH;
frame1.data.low = 0x0001;
frame1.data.high = 0x1000;
frame1.extended = 1;
uint16_t addition = 0x0200;
frame2.id = id + addition;
frame2.length = MAXIMUM_CAN_FRAME_DATA_LENGTH;
frame2.data.low = 0x00000002;
frame2.data.high = 0x20000000;
frame2.extended = 1;
Can0.watchFor(id);
Can1.watchFor(id + addition);
uint32_t sentFrames, receivedFrames;
uint16_t counter = 0;
Can0.sendFrame(frame2);
sentFrames ++;
while (1)
{
if (Can0.available() > 0)
{
Can0.read(incoming);
Can0.sendFrame(frame2);
delayMicroseconds(100);
receivedFrames ++;
sentFrames ++;
counter++;
}
if (Can1.available() > 0)
{
Can1.read(incoming);
Can1.sendFrame(frame1);
delayMicroseconds(100);
receivedFrames ++;
sentFrames ++;
counter++;
}
if (counter > MAXIMUM_FRAMES_NUMBER)
{
counter = 0;
Serial.print("Sent: ");
Serial.print(sentFrames);
Serial.print(", Received: ");
Serial.print(receivedFrames);
Serial.println(".");
}
}
}
代码简单解释如下,
- 首先在void setup()方法里初始化Can0和Can1。
- 在echo()方法里,首先定义了三个数据帧变量,frame1、frame2以及传入的incoming帧。查看CAN_FRAME,发现这是一个结构体,容易转换成字节流。
- 接着对frame1和frame2进行赋值,分别是id、数据字段、长度以及是否是扩展帧,这些都是必选字段。
- watchFor是通过id过滤,器用过滤会忽然那些非符合条件的帧,毕竟像汽车这种适合CAN总线使用的场景,可能有多达数百个CAN设备,而且一个设备一秒钟发送很多帧都有可能。
所以,一个过滤功能非常有必要。
上传烧录程序
上传烧录,运行程序,打开Serial Monitor可以看到数据帧在两个CAN收发器之间传送。
小结
本文作为CAN总线第一课,弱化了概念,简化了程序代码,目的就是为让您上手。