第0x01讲 使用Arduino DUE开发板进行CAN总线通信

前言

很多计算机从业者对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开发板实物图
图1 Arduino DUE开发板实物图1
图1 Arduino DUE开发板实物图2
Arduino DUE针脚定义

下图是Arduino DUE针脚定义。注意左下角CANRX0、CANTX0,这引出了本文的重点,也就是说Arduino DUE原生支持CAN总线(关于CAN总线,后面会介绍)控制器模块。但是貌似只支持一个(RX和TX分别是CAN模块的收发信号线)CAN控制器模块?至于为什么需要多于一个CAN控制器模块,后面会揭晓。

图3 Arduino DUE针脚定义

Arduino DUE对CAN控制器模块的支持

打开顶部Atmel SAM3X8E ARM Cortex-M3 CPU链接,找到如下Block Diagram,注意CAN0、CAN1。


图4 Atmel SAM3X8E ARM Cortex-M3 CPU支持两个CAN控制器

这说明,在微控制器级别,该MCU支持两个CAN控制器模块,而且暴露出CANRX0和CANTX0、CANRX1和CANTX1共4个针脚。当然,MCU支持不等于开发板支持,因为MCU针脚不一定被开发板暴露。

很庆幸,经过查阅相关资料,确认MCU里4个CAN针脚全部被Arduino DUE暴露,只是Arduino DUE官方文档没有明说CANRX1和CANTX1。实际CAN0和CAN1如下图所示,


图5 Arduino DUE另外一个隐藏的CAN控制器

好了,现在从硬件角度来说,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米的双绞线,两者通过这种方式建立了连接。

图6 CAN总线CAN High和CAN Low电压

这就像居民楼里入户的220V交流电电缆,每户都是2根线,零线和火线,各种用电设备都接在这连根线上。理论上,所有变电站下方的220V用电设备,其两根火线或两根零线其电位一致。

把居民入户220V交流电缆和CAN总线相比不同之处在于,CAN总线任意两个节点直接都可以发送消息。

另外,有些人可能疑惑,为什么一直在纠结是不是两个CAN模块呢?难道一个模块不行吗?CAN总线协议规定,一个CAN帧必须得到至少一个除了本节点的其他节点的确认(Acknowledgement),否则视为出错。所以,如果有两个或者两个以上的CAN控制器即收发器模块便于测试,尤其继集成在个开发板上。这也是为什么我们入门的第一课选择了Arduino DUE。

关于CAN总线线介绍到这里,详细介绍请关注后续课程。

CAN收发器和控制器

CAN收发器和控制器分别对应CAN总线的物理层和数据链路层。


图7 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,隐形电平。
图8 CAN收发器显性电平和隐形电平

图9 某款CAN收发器
CAN控制器

CAN控制器实现CAN总线的协议栈以及数据链路层,

  • 用于生成CAN数据帧并以二进制的方式发送。并在此过程中进行位填充、添加CRC校验、应答检测等。
  • 用于将接收到的二进制进行解析,并在此过程中进行收发比对、去位填充、执行CRC校验等操作。


    图10 CAN控制器工作原理

连接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

参考图片如下,

图11 Arduino DUE与CAN收发器连线1

图12 Arduino DUE与CAN收发器连线2

图13 Arduino DUE与CAN收发器连线3

检查无误后,插上MicroUSB,建议使用Native USB Port(离电源插口远的插口)上电。


图14 Arduino DUE插上MicroUSB到Native USB Port

留一个疑问?双绞线在哪?

使用Arduino IDE开发

请自行安装Arduino IDE。安装完成后,运行Arduino IDE,打开BOARDS MANAGER,搜关键字“Due”,然后点“INSTALL”安装。


图15 Arduino IDE安装DUE开发板支持

安装完开发板,在安装Library。打开LIBRARY MANAGER,搜索“can_due”,滚动到最下方,点“INSTALL”安装,


图16 Arduino IDE安装can_due驱动

打开”File“,然后选择‘Save“,可以命名为“CANTransceiver”,此时,项目如下所示,


图17 创建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收发器之间传送。

图18 程序运行结果

小结

本文作为CAN总线第一课,弱化了概念,简化了程序代码,目的就是为让您上手。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容