1)实验平台:定时原子开拓者FPGA开发版。

(2)从摘自《开拓者 Nios II开发指南》的官方微信号公众号中获取更多信息:定时原子。

3)完整实验源手册视频下载地址:

第20章CAN通信实验

现场总线技术是自动控制领域的后起之秀,成本低,容易利用现有的数字化和互联网。

网络技术的新成果具有改造系统等特点,顺应了当今时代数字化、模块化、网络化的发展。

朝向。CAN总线是现场总线系列中最有希望的现场总线之一,在汽车产业、工艺产业、机械工

在产业、机器人、建筑物自动化等领域发挥着重要作用。在本章中,您将学习使用CAN总线。本章介绍了

下一章:

20.1简介

20.2实验任务

20.3硬件设计

20.4软件设计

20.5下载验证

简介

20世纪80年代以来,汽车ECU越来越多,包括ABS、电控门、电子燃油喷射装置等。比如说,

如果仍然采用一般的点对点布线方式,即电线一端连接到开关,另一端与电气设备相通。

汽车的电线数量急剧增加,线束的冗余和维修成本增加了。此外,现在的蒸汽

汽车业界因对安全性、舒适性、便利性、低污染、低成本的要求,进行了多种电子控制

系统开发出来了。因为这些系统之间通信使用的数据类型和可靠性要求不同。

对汽车的线束分布及信息通信提出了更高的要求。CAN总线技术应运而生。

CAN是控制器区域网络(控制器LAN)的缩写,是ISO国际标准化的基于消息的

广播模式下的串行通信协议实时共享信息,传统布线方式下线束较多,布线可行。

困难、费用高的问题。德国汽车电子产品开发和生产著名的BOSCH开发并多次修改。

1991年9月,形成了由2.0A(11位标准帧格式)和2.0B(29位扩展)组成的技术规范版本2.0

框架)两部分。此后,CAN通过ISO11898和ISO11519进行了标准化,其中ISO11898是关于通信速度的。

这是125Kbps到1Mbps的高速通信标准,而ISO11519用于125Kbps以下的低速通信

标准。在欧洲,CAN已经是汽车网络的标准协议。

图20.1.1 CAN汽车网络拓扑结构

目前,CAN的高性能和可靠性已经得到承认,并广泛用于工业自动化、船舶、医疗设施。

准备、工业设备等方面。现场总线是当今自动化领域技术发展的热点之一,被称为自动化领域

计算机局域网。它的出现通过分布式控制系统,实现了节点之间实时、可靠的数据通信。

强大的技术支持。

CAN协议具有以下特征:

1)多主控制。总线空闲时,所有单元都可以发送消息(多主控制),3个以上的单个

元同时开始发送消息时,将根据标识符(称为标识符或以下ID)确定优先级。ID不是表示法

发送的大象地址表示访问总线的消息的优先级。两个或多个单元同时开始发送消息

对每个消息ID的每个位执行一次仲裁比较。仲裁胜利(被判定为优先级最高的单位)

如果继续发送信息,仲裁失败的单位将立即停止发送,进行接收工作。

2)系统的灵活性。连接到总线的单元没有类似于“地址”的信息。因此,向总线添加单个

从元连接到总线的其他设备的硬件和软件以及应用层不需要更改。

3)实时性能、远距离传输、快速通信速度(最高1Mbps(距离小于40米)、最高

最长10公里(5Kbps以下),抗电磁干扰能力强,成本节约优势。

4)提供错误检测、错误通知和错误恢复功能。检测所有单元中的错误(故障检测功能)

是的,检测到错误的单元立即向所有其他单元(错误通知功能)发送消息

如果单元检测到错误,则强制终止当前传输。结束强制关闭的设备将继续重新发送

此消息将一直发送到成功发送为止(故障恢复功能)。

5)关闭故障的功能。CAN可以看出,错误的类型是总线上的临时数据。

错误(如外部噪声

等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上

发生持续数据错误时,可将引起此故障的单元从总线上隔离出去。

6)连接节点多。CAN总线是可同时连接多个单元的总线。可连接的单元总数理论上是没

有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速

度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。

正是因为CAN协议的这些特点,使得CAN特别适合工业过程监控设备的互连,因此,越来

越受到工业界的重视,并已公认为最有前途的现场总线之一。

下面我们从CAN的基本概念入手,对CAN有一个基本的了解。

一、 总线拓扑图

CAN网络的拓扑一般为线型。线束最常用的是双绞线,其中一根线称为CAN_H,另一根线

称为CAN_L,两根线上传输的是对称的差分电平信号。下图为CAN总线网络示意图:

图 20.1.2 CAN总线网络示意图

CAN总线网络挂在CAN_H和CAN_L,连接在CAN总线上的设备叫做节点设备(CAN Node),

各个节点通过这两条线实现信号的串行差分传输,为了避免信号的反射和干扰,还需要在

CAN_H和CAN_L之间接上120Ω的终端电阻,来做阻抗匹配,以减少回波反射。节点设备主要包

括MCU、控制器和收发器。MCU常集成有CAN控制器,CAN控制器负责处理协议相关功能,以减

轻MCU的负担。CAN收发器将数据传到总线或者从总线接收数据给控制器。

图 20.1.3 120Ω的终端电阻

注:为什么使用120Ω的终端电阻?任何一根线缆的特征阻抗都可以通过实验的方式得

出。线缆的一端接方波发生器,另一端接一个可调电阻,并通过示波器观察电阻上的波形。

调整电阻阻值的大小,直到电阻上的信号是一个良好的无振铃的方波,此时的电阻值可以认

为与线缆的特征阻抗一致。大部分汽车线缆都是单线的,如果采用两根汽车使用的典型线

缆,将它们扭制成双绞线,就可根据上述方法得到特征阻抗大约为120Ω,这也是CAN标准推

荐的终端电阻阻值。

CAN总线分高速CAN和低速CAN,收发器也分为高速CAN收发器(1Mbps)和低速CAN收发器

(125Kbps)。低速CAN也叫Fault Tolerance CAN,指的是即使总线上一根线失效,总线依然

可以通信。我们开拓者使用的TJA1050为高速CAN收发器。

图 20.1.4 CAN收发器

在发送数据时,CAN控制器把要发送的二进制编码通过TXD引脚发送到CAN收发器,然后由

收发器把这个普通的逻辑电平信号转化成差分信号,通过差分线CAN_H和CAN_L输出到CAN总线

网络。接收数据过程则相反。CAN收发器具体的管脚定义如下:

图 20.1.5 管脚定义

二、 CAN 信号表示

CAN总线采用差分信号传输,通常情况下只需要两根信号线就可以进行正常的通信。在差

分信号中,逻辑0和逻辑1是用两根差分信号线的电压差来表示。当处于逻辑1,CAN_H和CAN_L

的电压差小于0.5V,称为隐性电平(Recessive);当处于逻辑0,CAN_H和CAN_L的电压差大

于0.9V,称为显性电平(Dominant)。

图 20.1.6 高速CAN总线差分信号

典型地,CAN总线为隐性(逻辑1)时,CAN_H和CAN_L的电平都为2.5V(电位差为0V);

CAN总线为显性(逻辑0)时,CAN_H和CAN_L电平分别为3.5V和1.5V(电位差为2.0V)。在总

线上显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平。而隐形电

平则具有包容的意味,只有所有的单元都输出隐性电平,总线上才为隐性电平。

三、 CAN 协议的帧格式

CAN网络可以配置为使用两种不同的帧格式:标准帧格式(在CAN 2.0A和CAN2.0 B中描

述)和扩展帧格式(仅由CAN2.0 B描述))。两种格式之间的唯一区别是“CAN标准帧”支持

标识符的11位长度,“CAN扩展帧”支持标识符的29位长度,由11位标识符组成(“标准标识

符”)和18位扩展名(“扩展标识符”)。CAN标准帧格式和CAN扩展帧格式之间的区别是通

过使用IDE位来实现的,该位在11位帧的情况下作为显性发送,并且在29位帧的情况下作为隐

性发送。支持扩展帧格式消息的CAN控制器也能够以CAN标准帧格式发送和接收消息。所有帧

都以帧起始(SOF)位开始,该位表示帧传输的开始。

CAN有四种帧类型:

◆ 数据帧:发送单元向接收单元传送数据的帧

◆ 远程帧:总线单元发出远程帧,请求发送具有同一识别符的数据帧

◆ 错误帧:由检测到错误的任何节点发送的帧

◆ 过载帧:在数据或远程帧之间注入延迟的帧

由于篇幅所限,我们这里仅对数据帧进行详细介绍,

数据帧是唯一实际传输数据的帧,结构上由7个段组成,其中根据仲裁段ID码长度的不

同,分为标准帧(CAN2.0A)和扩展帧(CAN2.0B):

➢ 标准帧格式:具有 11 个标识符位

➢ 扩展帧格式:具有 29 个标识符位

标准数据帧的构成如下图所示:

图 20.1.7 CAN的标准数据帧

数据帧一般由7个段构成,即:

(1)帧起始:表示数据帧开始的段。

(2)仲裁段:表示该帧优先级的段。

(3)控制段:表示数据的字节数及保留位的段。

(4)数据段:数据的内容,一帧可发送0~8个字节的数据。

(5)CRC段:检查帧的传输错误的段。

(6)ACK段:表示确认正常接收的段。

(7)帧结束:表示数据帧结束的段。

对于标准数据帧,格式如下(长度为bit,排列顺序为发送顺序):

图 20.1.8 标准数据帧格式

对于扩展数据帧,格式如下

图 20.1.9 扩展数据帧格式

标准帧与扩展帧的区别段如下图所示:

图 20.1.10 标准帧与扩展帧的区别段

下面我们具体进行介绍。

帧起始和帧结束:帧起始和帧结束用于界定一个数据帧,无论是标准数据帧还是扩展数

据帧都包含这两个段。

图 20.1.11 数据帧的帧起始和帧结束

仲裁段:仲裁段的内容主要为本数据帧的ID信息。在CAN协议中,ID决定着数据帧发送的

优先级,也决定着其它设备是否会接收这个数据帧。数据帧的标准格式和扩展格式的主要区

别就在于ID信息的长度:标准格式的ID为11位;扩展格式为29位,具体区别如下图所示:

图 20.1.12 数据帧仲裁段构成

标准格式的ID有11个位。其中RTR位用于标识是否是远程帧(0,数据帧;1,远程帧),

IDE位为标识符选择位(0,使用标准标识符;1,使用扩展标识符),SRR位为代替远程请求

位,为隐性位,它代替了标准帧中的RTR位。

控制段:由6个位构成,表示数据段的字节数。标准帧和扩展帧的控制段稍有不同,如下

图所示:

图 20.1.13 数据帧控制段构成

上图中,r0和r1为保留位,必须全部以显性电平发送,但是接收端可以接收显性、隐性及任意组合的电平。DLC段为数据长度表示段,高位在前,DLC段有效值为0~8,但是接收方接

收到9~15的时候并不认为是错误,另外DLC服从BCD8421编码。

数据段:该段可包含0~8个字节的数据。从最高位(MSB)开始输出,标准帧和扩展帧在

这个段的定义都是一样的。如下图所示:

图 20.1.14 数据帧数据段构成

CRC段:该段用CRC校验检查帧传输错误,CRC段由15位的CRC校验值和1位CRC界定符构

成,如下图所示:

图 20.1.15 数据帧CRC段构成

此段CRC的值计算范围包括:帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算CRC值并进行比较,不一致时会通报错误。

ACK段:在该段发送节点发送两个隐性电平位,所有接收到匹配CRC序列(CRC

SEQUENCE)的节点即正确接收到有效的报文的节点会在ACK槽(ACK Slot)期间用一显性电平位

写入发送器的“隐性”位来作出应答。标准帧和扩展帧在这个段的格式也是相同的。如下图

所示:

图 20.1.16 数据帧CRC段构成

ACK槽也称为应答间隙。

ACK界定符:ACK界定符是ACK段的第二位,并且是一个必须为“隐性”的位。因此,应答

间隙(ACK SLOT)被两个“隐性”的位所包围,也就是CRC界定符和ACK界定符。

至此,数据帧的7个段就介绍完了,其他帧的介绍,请大家参考光盘的《CAN入门书》相

关章节。接下来,我们再来看看CAN的仲裁功能是如何实现的。

在总线空闲态,总线上任何节点都可以发送报文,最先开始发送消息的单元获得发送

权。如果有两个或两个以上的节点开始传送报文,那么就会存在总线访问冲突的可能。CAN使

用了标识符的逐位仲裁方法解决了这个问题。

当多个单元同时开始发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显

性电平最多的单元可继续发送。实现过程,如下图所示:

图 20.1.17 总线仲裁图

在仲裁期间,每一个发送器都对发送的电平与被监控的总线电平进行比较。如果电平相

同,则这个单元可以继续发送。如果发送的是一"隐性"电平而监视到的是一"显性"电平,那

么这个节点失去了仲裁,必须退出发送状态。

上图中,节点A、B、C同时开始向总线发送数据,开始部分他们的数据格式是一样的,故

无法区分优先级,直到ID的第5位,节点B输出隐性电平,而节点A、C输出显性电平,此时节

点B仲裁失利,立刻转入接收状态工作(只听模式),不再与节点A、C竞争,而节点A、C则继

续仲裁,在ID的第3位节点A输出显性电平,节点C输出隐性电平,此时节点C仲裁失利,立刻转入

接收状态工作,不再与节点A竞争,节点A顺利获得总线使用权,继续发送自己的数据。这就实现

了仲裁,让连续发送显性电平多的单元获得总线使用权。具体的数字化表示如下表:

图 20.1.18 仲裁的数字化表示

从上表我们可以看到,帧ID越小的节点优先级越高;CAN总线采用"线与"的规则进行总

线冲裁,即1&0=0,所以0为显性。同理,由于标准帧的IDE位为显性电平,扩展帧的IDE位为

隐性电平,对于前11位ID相同的标准帧和扩展帧,标准帧的优先级比扩展帧高。

图 20.1.19 前11位相同ID的标准帧比扩展帧优先级高

通过以上介绍,我们对CAN总线有了个大概了解,详细介绍参考光盘的:《CAN入门

书.pdf》。

四、 CAN 总线与 RS485 的比较

CAN总线与RS485都是采用差分信号进行传输,这两者有什么区别呢?

1) 速度与距离:CAN 与 RS485 以 1Mbps 的高速率传输的距离都不超过 100M,可谓高速

上的距离差不多。但是低速 CAN 以 5Kbps 时,距离可达 10KM。而增强型 RS485 收发

器在最低的速率时亦能传输超过 10KM(都无中继),两者在长距离的传输上也难分

伯仲;

图 20.1.20 楼宇自动化

2) 总线利用率:RS485 是单主从结构,就是一个总线上只能有一台主机,通讯都由它发

起的。它没有下命令,下面的节点不能发送,而且要发完即答,受到答复后,主机才

向下一个节点询问,这样是为了防止多个节点向总线发送数据,而造成数据错乱。而

CAN 是多主从结构,每个节点都有 CAN 控制器,多个节点发送时,以发送的 ID 号自

动进行仲裁,这样就可以实现总线数据不错乱,而且一个节点发送完,另一个节点探

测到总线空闲,而马上发送,这样省去了主机的询问,提高了总线利用率,增强了快

速性。所以在汽车等实性要求高的系统,都是用 CAN 总线,或者其他类似的总线;

3) 错误检测机制:RS485 只规定了物理层,而没有数据链路层,所以它对错误是无法识

别的,除非一些短路等物理错误。这样容易造成一个节点破坏了,拼命向总线发数据

(一直发 1),这样造成整个总线瘫痪。所以 RS485 一旦坏一个节点,这个总线网络

都瘫痪。而 CAN 总线有 CAN 控制器,可以对总线任何错误进行检测,自动转换错误

状态,适时关闭总线,进而保护总线。如果检测到其他节点错误或者自身错误,都会

向总线发送错误帧,来提示其他节点。这样 CAN 总线一旦有一个节点程序跑飞了,它

的控制器自动闭锁,保护总线。所以在安全性要求高的网路,CAN 是较好的选择;

4) 器件价格:随着 CAN 总线迅猛发展,目前 CAN 隔离收发器单价大有与 RS485 价格持

平的趋势,RS485 收发器逐渐失去价格优势;

5) 开发难度:CAN 具有完善的通信协议,底层机制由 CAN 控制器芯片及其接口芯片来实现,研发工程师只需要了解面向客户的应用层,从而大大降低了系统的开发难度,缩

短了开发周期。而 RS485 协议仅仅只有电气协议,客户开发需要自己开发链路层和应

用层,开发难度较大。

总结对比:

图 20.1.21 CAN与RS485的比较

实验任务

本章我们的实验任务是:使用两块开拓者开发板实现CAN接口通信功能,并把接收到的数

据显示在数码管上。

硬件设计

在前面的简介部分我们知道一个CAN节点除了需要CAN收发器外还需要CAN控制器,由于

Qsys没有CAN控制器IP核,而CAN控制器设计比较复杂,所以这里我们使用OpenCore网站上的

一个CAN Protocol Controller,网址为;该CAN控制

器具有如下特点:

✓ 非破坏性逐位仲裁(CSMA/CA)

✓ 基于消息的寻址/过滤

✓ 广播通讯

✓ 支持 1 Mbit/Sec 的操作

✓ 提供了 WISHBONE 总线接口和 8051 接口

✓ 兼容 SJA1000 接口。

下面我们用该CAN控制器来实现我们的CAN环回实验。

对于该CAN控制器,最大的方便之处是其兼容SJA1000接口,所以我们可以像使用SJA1000

那样来使用该CAN控制器。那SJA1000是什么呢?

SJA1000是一种独立CAN控制器,用于移动目标和一般工业环境中的区域网络控制

(CAN)。它是PHILIPS半导体PCA82C200 CAN控制器BasicCAN的替代产品,而且增加了一种新

的作模式PeliCAN,这种模式支持具有很多新特性的CAN 2.0B协议。对于SJA1000 CAN控制器

的具体说明可以参考我们提供的硬件资料的《CAN控制器SJA1000中文资料.pdf》文档。这里

就不多做介绍了。

另外对于该CAN控制器内部的代码由于后面我们直接封装成Qsys IP核使用,而且其提供

了SJA1000的接口,不需要了解其内部的代码逻辑,所以这里我们就不做介绍了。

为了方便使用,我们首先将该CAN控制器封装成Qsys IP核。由于该CAN控制器提供了

WISHBONE总线接口和8051接口,这里我们选用WISHBONE总线接口,使其与Avalon MM总线桥

接。下面我们介绍下如何封装成Qsys IP核。

首先我们在par目录下新建一个my_ip文件夹,接着在my_ip文件夹下新建一个can文件

夹,该文件夹用来放我们定制的CAN IP核文件。如同《自定义IP核—数码管》实验一样,我

们在can文件新建HAL、HDL、inc文件夹,如下图所示:

图 20.3.1 文件夹结构

现在我们将下载下来的(下载需注册OpenCore账号,可以直接用软件资料下的

can_la压缩包文件)CAN控制器压缩文件的trunk/rtl/verilog目录下的CAN控制

器Verilog HDL文件解压到刚才新建的HDL文件夹下,为了使用WISHBONE接口和使用Altera

RAM,我们对其中的can_de文件做如下修改:

图 20.3.2 修改宏定义

即将该两行取消注释,然后我们在该文件夹(HDL)下新建一个can_con文件,

内容如下:

1 module can_controller(

2 //module clock

3 input csi_clk ,

4 input rsi_reset,

5

6 //Avalon MM interface

7 input [7:0] avs_address,

8 input avs_chipselect,

9 input avs_write,

10 input avs_read,

11 input [7:0] avs_writedata,

12 output [7:0] avs_readdata,

13 output avs_waitrequest_n,

14

15 // CAN interface

16 input can_clk, //CAN控制器的操作时钟

17 input can_reset, //CAN控制器复位信号

18 input can_rx, //CAN控制器接收数据引脚

19 output can_tx, //CAN控制器发送数据引脚

20 output can_irq_n, //CAN控制器中断信号

21 output can_clkout //CAN控制器分频时钟输出信号

22

23 );

24

25 //*****************************************************

26 //** main code

27 //*****************************************************

28

29 can_top u1_can_top(

30 //Wishbone interface

31 .wb_clk_i (csi_clk ),

32 .wb_rst_i (rsi_reset | can_reset),

33 .wb_dat_i (avs_writedata[7:0] ),

34 .wb_dat_o (avs_readdata[7:0] ),

35 .wb_cyc_i (avs_write | avs_read ),

36 .wb_stb_i (avs_chipselect ),

37 .wb_we_i (avs_write & ~avs_read),

38 .wb_adr_i (avs_address[7:0] ),

39 .wb_ack_o (avs_waitrequest_n ),

40 //CAN interface

41 .clk_i (can_clk ),

42 .rx_i (can_rx ),

43 .tx_o (can_tx ),

44 .bus_off_on (),

45 .irq_on (can_irq_n ),

46 .clkout_o (can_clkout )

47 );

48

49 endmodule

50

该文件的功能是实现WISHBONE总线接口与Avalon总线接口的桥接。桥接方式可参考下

表:

图 20.3.3 Wishbone => Avalon

图 20.3.4 Avalon => Wishbone

另外对于CAN接口,我们将其封装成SJA1000的接口,clk_i是CAN控制器输入时钟引脚;

rx_i是CAN收发器传给CAN控制器的数据引脚;tx_o是CAN控制器传给CAN收发器的数据引脚;

irq_on中断输出引脚,用于向微控制器传递中断,低电平有效;clkout_o是SJA1000产生时钟

输出信号;bus_off_on没有对应的SJA1000引脚,所以这里就不使用该引脚。

接下来就是将can_con作为顶层文件将整个CAN控制器封装成Qsys IP核,这一步可参照《自定义IP核—数码管》实验。需要注意的是生成的can_con默认是

在par目录下,我们将其移动到can目录下时,需要更改PATH路径,如下图所示:

图 20.3.5 更改路径

当然了如果保持默认的存放位置的话是不用修改的,不过这样以后使用就比较麻烦。

封装成Qsys IP核之后,我们就可以在Qsys中调用了,如同调用PIO IP核一样简单。不

过,现在有一个问题,就是如何配置该CAN控制器IP核呢?

由于该CAN控制器兼容SJA1000 CAN控制器,所以我们可以按照配置SJA1000的方式来配

置。

为了方便以后的使用,下面我们就给该IP核添加寄存器头文件和底层驱动文件。我们将

SJA1000的寄存器列在文件里,该文件放在can/inc目录下。由于内容太长,

我们摘取部分如下图所示:

图 20.3.6 SJA1000寄存器文件

该文件中我们将SJA1000的两种CAN模式——BasicCAN模式和PeliCAN模式的寄存器都列出

来了。默认使用PeliCAN模式,该模式完全支持BasicCAN模式的功能,不过做的更好。另外对

于上图中的“命令寄存器及其位定义(写)”的“写”是表明该寄存器只能写或者读出来的

值无意义,后面的可参照该方式。

为了读者更好的使用SJA1000,我们重点介绍Peli模式下总线定时器和时钟分频寄存器。

总线定时寄存器分为总线定时寄存器0和总线定时寄存器1。

总线定时寄存器0定义了波特率预设值BRP和同步跳转宽度SJW的值。复位模式有效时这个

寄存器是可以被访问(读/写)的。如果选择的是PeliCAN模式,此寄存器在工作模式中是只

读的。在BasicCAN模式中总是FFH。位说明如下:

图 20.3.7 位说明

其中波特率预设值(BRP)决定了CAN系统时钟tSCL,以输入时钟can_clk(tCLK)为基准,

而且决定了相应的位时序。CAN系统时钟tSCL由如下公式计算:

tSCL = 2×tCLK×(32×BRP.5+16×BRP.4+8×BRP.3+4×BRP.2+2×BRP.1+BRP.0+1)

同步跳转宽度(SJW)定义了每一位周期可以被重新同步缩短或延长的时钟周期的最大数

目,是为了补偿在不同总线控制器的时钟振荡器之间的相位偏移,任何总线控制器必须在当

前传送的相关信号边沿重新同步,与CAN系统时钟tSCL的关系如下:

tSJW = tSCL×(2×SJW.1+SJW.0+1)

总线定时寄存器1定义了每个位周期的长度、采样点的位置和在每个采样点的采样数目。

在复位模式中,这个寄存器可以被读/写访问。在PeliCAN模式的工作模式中,这个寄存器是

只读的,在BasicCAN模式中总是FFH。位说明如下:

图 20.3.8 位说明

SAM位决定了是三倍采样(总线采样三次)还是单倍采样(总线采样一次),建议在高速

总线上使用单倍采样,此时SAM值为0。

时间段1(TSEG1)和时间段2(TSEG2)决定了每一位的时钟数目和采样点的位置,这里

tSYNCSEG = 1×tSCL

tTSEG1 = tSC×(8×TSEG1.3+4×TSEG1.2+2×TSEG1.1+TSEG1.0+1)

tTSEG2 = tSCL×(4×TSEG2.2+2×TSEG2.1+TSEG2.1+1)

图 20.3.9 总线定时器的设置与位时钟关系

时钟分频寄存器(CDR)为微控制器控制CLKOUT的频率以及屏蔽CLKOUT引脚输出,而且决

定了SJA1000是运行于BasicCAN模式还是PeliCAN模式。软件复位(复位请求/复位模式)时,

此寄存器不受影响。位说明如下:

图 20.3.10 位说明

保留位CDR.4总是0。应用软件总是向此位写0以与将来可能使用此位的特性兼容。对于该

寄存器我们一般使用的是BIT.7和BIT.3,即选择PeliCAN模式和关闭时钟输出。

另外对于SJA1000还有两类重要的寄存器:验收代码寄存器(ACR)和验收屏蔽寄存器AMR,

使用起来不难,只需要记住验收代码寄存器设置了CAN控制器接收怎样的ID和数据信息,而验

收屏蔽寄存器决定了验收代码寄存器的相应位起不起作用,当验收屏蔽寄存器的某一位为1

时,验收代码寄存器的相应位不起作用。

为了方便使用CAN控制器,我们实现了一些基本的底层驱动功能,驱动头文件

can_con放在can/HAL/inc目录下,内容如下:

1 #ifndef __CAN_CONTROLLER_H__

2 #define __CAN_CONTROLLER_H__

3

4 #include ";

5 #include <;

6 #include <io.h>

7 #include "al;

8 #include ""

9

10 #ifdef __cplusplus

11 extern "C"

12 {

13 #endif /* __cplusplus */

14

15 //复位设置结构体

16 typedef struct{

17 uint8_t btr0; //总线定时器BTR0

18 uint8_t btr1; //总线定时器BTR1

19 uint8_t cdr; //时钟分频寄存器CDR

20 uint8_t acr[4]; //验收代码寄存器ACR

21 uint8_t amr[4]; //验收屏蔽寄存器AMR

22 }_rest_val;

23

24 /* CAN_CONTROLLER function */

25 void can_brt0(uint8_t val);

26 void can_brt1(uint8_t val);

27 void can_ocr(uint8_t val);

28 void can_acp(const uint8_t *acceptance);

29 void can_rest(void);

30 void can_txf(uint8_t tx_data[], uint8_t tx_info);

31 void can_rxf();

32 void tx_id_f(uint8_t ff, const uint8_t *id);

33 void rx_id_f(uint8_t *id);

34 void rx_data_f(uint8_t *data);

35

36 /* Macros used by alt_sys_init */

37 #define CAN_CONTROLLER_INSTANCE(name, dev) alt_u32 canbaseaddr = name##_BASE

38 #define CAN_CONTROLLER_INIT(name, dev)

39 #ifdef __cplusplus

40 }

41 #endif /* __cplusplus */

42

43 #endif /* __SEGLED_CONTROLLER_H__ */

在代码第16行,我们定义了一个结构体,里面的值是我们使用CAN控制器之前需要设置的

值。我们还声明了一些函数,这些函数实现的功能可以查看can_con文件,主要是

为了方便使用CAN控制器。can_con文件代码如下:

1 #include <;

2 #include "can_con"

3

4 #define SIZE 13

5

6 extern alt_u32 canbaseaddr;

7

8 uint8_t tx_buffer[SIZE];

9 uint8_t rx_buffer[SIZE];

10

11 //默认的复位值

12 _rest_val rest_val={

13 0x00, //sjw=1tscl, tscl=2tclk

14 0x14, //位长时间为8tscl

15 CANMODE_BIT|CLKOFF_BIT,

16 {0x00,0x00,0x00,0x00},

17 {0xff,0xff,0xff,0xff}

18 };

19

20 /*****************************************************************

21 函数功能:写CAN ID

22 入口参数:ff:帧格式:1扩展帧,0标准帧:id:CAN ID

23 返回参数:

24 说明 :默认在Peli_CAN模式下

25 ******************************************************************/

26 void tx_id_f(uint8_t ff, const uint8_t *id)

27 {

28 if(ff){

29 tx_buffer[1] = id[1];

30 tx_buffer[2] = id[2];

31 tx_buffer[3] = id[3];

32 tx_buffer[4] = id[4];

33 }

34 else{

35 tx_buffer[1] = id[1];

36 tx_buffer[2] = id[2];

37 }

38 }

39

40 /*****************************************************************

41 函数功能:读CAN ID

42 入口参数:id:接收到的CAN ID,id[0]为帧信息

43 返回参数:

44 说明 :默认在Peli_CAN模式下

45 ******************************************************************/

46 void rx_id_f(uint8_t *id)

47 {

48 uint8_t i;

49 if(rx_buffer[0] & 0x80)

50 for(i=0; i < 5; i++)

51 id[i] = rx_buffer[i];

52 else

53 for(i=0; i < 3; i++)

54 id[i] = rx_buffer[i];

55 }

56

57 /*****************************************************************

58 函数功能:读取接收到的数据

59 入口参数:data:接收数据的数组

60 返回参数:

61 说明 :默认在Peli_CAN模式下

62 ******************************************************************/

63 void rx_data_f(uint8_t *data)

64 {

65 uint8_t i;

66 if(rx_buffer[0] & 0x80)

67 for(i=0; i < (rx_buffer[0] & 0x0f); i++)

68 data[i] = rx_buffer[i+5];

69 else

70 for(i=0; i < (rx_buffer[0] & 0x0f); i++)

71 data[i] = rx_buffer[i+3];

72 }

73

74 /*****************************************************************

75 函数功能:设置总线定时器0

76 入口参数:val:总线定时器0的值

77 返回参数:

78 说明 :默认在Peli_CAN模式下

79 ******************************************************************/

80 void can_brt0(uint8_t val)

81 {

82 re = val;

83

84 }

85

86 /*****************************************************************

87 函数功能:设置总线定时器1

88 入口参数:val:总线定时器1的值

89 返回参数:

90 说明 :默认在Peli_CAN模式下

91 ******************************************************************/

92 void can_brt1(uint8_t val)

93 {

94 re = val;

95 }

96

97 /*****************************************************************

98 函数功能:设置输出控制寄存器

99 入口参数:val:输出控制寄存器的值

100 返回参数:

101 说明 :默认在Peli_CAN模式下

102 ******************************************************************/

103 void can_ocr(uint8_t val)

104 {

105 re = val;

106 }

107

108 /*****************************************************************

109 函数功能:设置验收滤波器

110 入口参数:acceptance:验收滤波器设置

111 返回参数:

112 说明 :默认在Peli_CAN模式下

113 ******************************************************************/

114 void can_acp(const uint8_t *acceptance)

115 {

116 re[0] = acceptance[0]; //验收代码寄存器设置

117 re[1] = acceptance[1];

118 re[2] = acceptance[2];

119 re[3] = acceptance[3];

120 re[0] = acceptance[4]; //验收屏蔽寄存器设置

121 re[1] = acceptance[5];

122 re[2] = acceptance[6];

123 re[3] = acceptance[7];

124 }

125

126 /*****************************************************************

127 函数功能:复位SJA1000

128 入口参数:

129 返回参数:

130 说明 :默认在Peli_CAN模式下

131 ******************************************************************/

132 void can_rest(void)

133 {

134 //确定是否在复位模式,不在则进入复位模式

135 while(!(IORD_8DIRECT(canbaseaddr, SJA_MOD) & RM_BIT))

136 IOWR_8DIRECT(canbaseaddr, SJA_MOD, RM_BIT);

137

138 IOWR_8DIRECT(canbaseaddr, SJA_BTR0, re);

139 IOWR_8DIRECT(canbaseaddr, SJA_BTR1, re); //位长时间为8tscl

140 //设为Peli_CAN模式&禁能时钟输出

141 IOWR_8DIRECT(canbaseaddr, SJA_CDR, re);

142

143 IOWR_8DIRECT(canbaseaddr, SJA_ACR0, re[0]); //验收代码寄存器设置

144 IOWR_8DIRECT(canbaseaddr, SJA_ACR1, re[1]);

145 IOWR_8DIRECT(canbaseaddr, SJA_ACR2, re[2]);

146 IOWR_8DIRECT(canbaseaddr, SJA_ACR3, re[3]);

147 IOWR_8DIRECT(canbaseaddr, SJA_AMR0, re[0]); //验收屏蔽寄存器设置

148 IOWR_8DIRECT(canbaseaddr, SJA_AMR1, re[1]);

149 IOWR_8DIRECT(canbaseaddr, SJA_AMR2, re[2]);

150 IOWR_8DIRECT(canbaseaddr, SJA_AMR3, re[3]);

151

152 IOWR_8DIRECT(canbaseaddr, SJA_IER, RIE_BIT); //使能接收中断

153 IOWR_8DIRECT(canbaseaddr, SJA_CMR, RRB_BIT); //释放接收缓冲器

154

155 IOWR_8DIRECT(canbaseaddr, SJA_MOD, AFM_BIT); //设置为单个验收滤波器

156 while(!(IORD_8DIRECT(canbaseaddr, SJA_MOD) == AFM_BIT))

157 IOWR_8DIRECT(canbaseaddr, SJA_MOD, AFM_BIT);

158 printf("INIT_DONE!n");

159 }

160

161 /*****************************************************************

162 函数功能:CAN控制器发送函数

163 入口参数:tx_data[]:CAN控制器发送的数据

164 tx_info:发送帧信息,bit7:0 标准帧,1扩展帧,

165 bit6:0:数据帧,1遥控帧

166 bit3-0:数据长度DLC

167 返回参数:

168 说明 :发送帧信息

169 ******************************************************************/

170 void can_txf(uint8_t tx_data[], uint8_t tx_info)

171 {

172 uint8_t i;

173

174 if(tx_info & 0x80){ //判断是否为扩展帧

175 tx_buffer[0] = tx_info;

176 for(i=0; i < (tx_info & 0x0f); i++)

177 tx_buffer[5+i] = tx_data[i];

178 }

179 else{

180 tx_buffer[0] = tx_info;

181 for(i=0; i < (tx_info & 0x0f); i++)

182 tx_buffer[3+i] = tx_data[i];

183 }

184 while(IORD_8DIRECT(canbaseaddr, SJA_SR) & RS_BIT); //等待

185 while(!(IORD_8DIRECT(canbaseaddr, SJA_SR) & (TCS_BIT|TBS_BIT))); //等待

186 IOWR_8DIRECT(canbaseaddr, SJA_TBR0 , tx_buffer[ 0]);

187 IOWR_8DIRECT(canbaseaddr, SJA_TBR1 , tx_buffer[ 1]);

188 IOWR_8DIRECT(canbaseaddr, SJA_TBR2 , tx_buffer[ 2]);

189 IOWR_8DIRECT(canbaseaddr, SJA_TBR3 , tx_buffer[ 3]);

190 IOWR_8DIRECT(canbaseaddr, SJA_TBR4 , tx_buffer[ 4]);

191 IOWR_8DIRECT(canbaseaddr, SJA_TBR5 , tx_buffer[ 5]);

192 IOWR_8DIRECT(canbaseaddr, SJA_TBR6 , tx_buffer[ 6]);

193 IOWR_8DIRECT(canbaseaddr, SJA_TBR7 , tx_buffer[ 7]);

194 IOWR_8DIRECT(canbaseaddr, SJA_TBR8 , tx_buffer[ 8]);

195 IOWR_8DIRECT(canbaseaddr, SJA_TBR9 , tx_buffer[ 9]);

196 IOWR_8DIRECT(canbaseaddr, SJA_TBR10, tx_buffer[10]);

197 IOWR_8DIRECT(canbaseaddr, SJA_TBR11, tx_buffer[11]);

198 IOWR_8DIRECT(canbaseaddr, SJA_TBR12, tx_buffer[12]);

199

200 IOWR_8DIRECT(canbaseaddr, SJA_CMR, TR_BIT); //置位发送请求位

201 printf("TX DONE!n");

202 }

203

204 /*****************************************************************

205 函数功能:接收节点发送的帧信息

206 入口参数:

207 返回参数:

208 说明 :在中断服务程序中调用

209 ******************************************************************/

210 void can_rxf()

211 {

212 rx_buffer[ 0] = IORD_8DIRECT(canbaseaddr, SJA_RBR0 );

213 rx_buffer[ 1] = IORD_8DIRECT(canbaseaddr, SJA_RBR1 );

214 rx_buffer[ 2] = IORD_8DIRECT(canbaseaddr, SJA_RBR2 );

215 rx_buffer[ 3] = IORD_8DIRECT(canbaseaddr, SJA_RBR3 );

216 rx_buffer[ 4] = IORD_8DIRECT(canbaseaddr, SJA_RBR4 );

217 rx_buffer[ 5] = IORD_8DIRECT(canbaseaddr, SJA_RBR5 );

218 rx_buffer[ 6] = IORD_8DIRECT(canbaseaddr, SJA_RBR6 );

219 rx_buffer[ 7] = IORD_8DIRECT(canbaseaddr, SJA_RBR7 );

220 rx_buffer[ 8] = IORD_8DIRECT(canbaseaddr, SJA_RBR8 );

221 rx_buffer[ 9] = IORD_8DIRECT(canbaseaddr, SJA_RBR9 );

222 rx_buffer[10] = IORD_8DIRECT(canbaseaddr, SJA_RBR10);

223 rx_buffer[11] = IORD_8DIRECT(canbaseaddr, SJA_RBR11);

224 rx_buffer[12] = IORD_8DIRECT(canbaseaddr, SJA_RBR12);

225

226 IOWR_8DIRECT(canbaseaddr, SJA_CMR, RRB_BIT); //释放接收缓冲器

227

228 printf("RX DONE!n");

229 }

需要注意的是这些函数都是默认在PeliCAN模式下工作的,而且与SJA1000不同的是我们默认的就是PeliCAN模式。我们在代码第12行设置了复位时的默认值,如果需要重新设置只需

调用相应的函数即可。另外这些函数包括了初始化SJA1000,发送CAN信息,接收CAN信息等基

本功能,如果想实现更复杂的功能,譬如错误处理,可以在该文件中添加错误处理函数即

可。

编写完了寄存器头文件和驱动底层头文件,为了让Nios SBT for Eclipse自动获取IP核

的HAL,我们需要编写can_con文件,由于该代码比较简单,并且该代码中的

内容与我们的自定义数据管IP核实验中的文件中的代码基本一致,所以我们就不再对

can_con进一步进行讲解了。现在为了方便以后我们在其它工程中调用CAN控

制器IP核,我们将my_ip文件夹拷贝到<Quartus安装目录>/ip文件夹下,至此封装IP核结束。

现在我们实现Qsys的CAN环回实验。

搭建的Qsys硬件框架如下:

图 20.3.11 硬件框架

这里我们除了基本的sdram和epcs外,增加了我们数码管、按键控制发送的PIO和刚才封

装的CAN控制器。CAN控制器的时钟can_clk_i我们引出来,接到顶层的PLL的输出的16MHz上,

can_clk_o是CAN控制器的输出时钟,一般不需要。

顶层代码如下:

1 module qsys_can_loopback(

2 //module clock

3 input sys_clk , //系统时钟,50Mhz

4 input sys_rst_n , //系统复位,低电平有效

5

6 //SDRAM interface

7 output sdram_clk , //SDRAM 芯片时钟

8 output sdram_cke , //SDRAM 时钟有效

9 output sdram_cs_n , //SDRAM 片选

10 output sdram_ras_n, //SDRAM 行有效

11 output sdram_cas_n, //SDRAM 列有效

12 output sdram_we_n , //SDRAM 写有效

13 output [ 1:0] sdram_ba , //SDRAM Bank地址

14 output [12:0] sdram_addr , //SDRAM 行/列地址

15 inout [15:0] sdram_data , //SDRAM 数据

16 output [ 1:0] sdram_dqm , //SDRAM 数据掩码

17

18 //EPCS FLASH interface

19 output epcs_dclk , // EPCS 时钟信号

20 output epcs_sce , // EPCS 片选信号

21 output epcs_sdo , // EPCS 数据输出信号

22 input epcs_data0 , // EPCS 数据输入信号

23

24 //can interface

25 input can_rx , // CAN接收引脚

26 output can_tx , // CAN发送引脚

27

28 //segled interface

29 output [ 5:0] sel , // 数码管位选端

30 output [ 7:0] seg_led , // 数码管段选端

31

32 //key interface

33 input key2 // 使能CAN发送信号

34 );

35

36 //wire define

37 wire clk_100m; //SDRAM 控制器时钟

38 wire can_clk ; //CAN驱动器的时钟

39 wire rst_n ; //系统复位信号

40 wire locked ; //PLL输出稳定标志

41

42 //*****************************************************

43 //** main code

44 //*****************************************************

45

46 //待PLL输出稳定之后,停止系统复位

47 assign rst_n = sys_rst_n & locked;

48

49 //例化PLL

50 pll_clk u_pll_clk(

51 .areset (~sys_rst_n),

52 .inclk0 (sys_clk ),

53 .c0 (clk_100m ),

54 .c1 (sdram_clk ),

55 .c2 (can_clk ), // CAN控制器驱动时钟16MHz

56 .locked (locked )

57 );

58

59 //例化Nios2系统模块

60 nios2os u_nios2os (

61 .clk_clk (clk_100m ), // 时钟100M

62 .reset_reset_n (rst_n ), // 复位信号

63 .sdram_addr (sdram_addr ), // SDRAM 行/列地址

64 .sdram_ba (sdram_ba ), // SDRAM Bank地址

65 .sdram_cas_n (sdram_cas_n), // SDRAM 列有效

66 .sdram_cke (sdram_cke ), // SDRAM 时钟有效

67 .sdram_cs_n (sdram_cs_n ), // SDRAM 片选

68 .sdram_dq (sdram_data ), // SDRAM 数据

69 .sdram_dqm (sdram_dqm ), // SDRAM 数据掩码

70 .sdram_ras_n (sdram_ras_n), // SDRAM 行有效

71 .sdram_we_n (sdram_we_n ), // SDRAM 写有效

72 .epcs_dclk (epcs_dclk ), // EPCS 时钟信号

73 .epcs_sce (epcs_sce ), // EPCS 片选信号

74 .epcs_sdo (epcs_sdo ), // EPCS 数据输出信号

75 .epcs_data0 (epcs_data0 ), // EPCS 数据输入信号

76 .can_clk_i_clk (can_clk ), // can_clk_in

77 .can_rt_rx (can_rx ), // can_rt.rx

78 .can_rt_tx (can_tx ), // .tx

79 .can_clk_out_clk ( ), // can_clk_out

80 .can_tx_en_export (key2 ), // 使能CAN发送信号

81 .segled_sel (sel ), // 数码管位选端

82 .segled_seg_led (seg_led ) // 数码管段选端

83 );

84

85 endmodule

顶层代码主要是连接例化模块的各端口,实现信号的交互。

接下来进行引脚分配,常用的引脚我们就不贴图了,我们贴下CAN控制器使用到的引脚和

按键使能CAN发送的引脚,如下表所示:

图 20.3.12 CAN环回实验部分管脚分配

软件设计

创建好软件工程后,我们将更改为q。接下来我们来

看一下本实验的软件工程代码,如下:

1 #include ";

2 #include <uni;

3 #include <;

4 #include <;

5 #include "sy;

6 #include "sy;

7 #include "al; //PIO寄存器文件

8 #include "can_con" //CAN控制器驱动文件

9 #include "segled_con" //数码管驱动文件

10

11 #define CanBaseAddr CAN_CONTROLLER_BASE

12 //帧信息

13 #define FF 0 //帧格式: 0:SFF 1:EFF

14 #define RTR 0 //0数据帧 1远程帧

15 #define DLC 3 //数据长度(0~8)

16 #define FRM_INFO (FF<<7) + (RTR<<6) + DLC //帧信息

17

18 const uint8_t id[2]={0x00,0x20}; //发送的ID

19

20 uint8_t acceptance[8]={0x00,0x00,0x00,0x00, //验收代码

21 0xff,0xff,0xff,0xff}; //验收屏蔽

22 uint8_t rx_data[8]; //接收的数据

23 uint8_t rx_flag = 0; //接收有效标志

24

25 void can_isr(void* context, alt_u32 id);

26 void delay_ms(uint32_t n);

27

28 int main(void)

29 {

30 int rc;

31 uint8_t tx_data[8],i;

32 uint8_t key;

33

34 printf("Hello from Nios II!n");

35 //需发送数据

36 tx_data[0] = 18;

37 tx_data[1] = 12;

38 tx_data[2] = 25;

39 IOWR_AVALON_SEGLED_EN(SEGLED_CONTROLLER_BASE,1);

40 alt_ic_isr_register(

41 CAN_CONTROLLER_IRQ_INTERRUPT_CONTROLLER_ID,

42 CAN_CONTROLLER_IRQ,

43 can_isr,

44 0,

45 0); //注册CAN控制器中断服务

46 can_brt0(0x00); //设置总线定时器0 sjw=1tscl, tscl=2tclk

47 can_brt1(0x14); //设置总线定时器1 位长时间为8tscl

48 can_acp(acceptance); //设置验收滤波器

49 can_rest(); //复位CAN控制器

50 while(1){

51 key = IORD_ALTERA_AVALON_PIO_DATA(CAN_TX_EN_BASE);

52 if(!key){

53 tx_id_f(FF,id); //发送ID

54 can_txf(tx_data,FRM_INFO); //发送数据

55 delay_ms(500);

56 }

57 else if(rx_flag){

58 rx_data_f(rx_data);

59 for(i=0;i<DLC;i++){

60 IOWR_AVALON_SEGLED_DATA(SEGLED_CONTROLLER_BASE,rx_data[i]);

61 delay_ms(1000);

62 }

63 rx_flag = 0;

64 }

65 }

66 return 0;

67 }

68

69 /*****************************************************************

70 函数功能:延时函数

71 入口参数:n:延时的时间

72 返回参数:

73 说明 :单位ms

74 ******************************************************************/

75 void delay_ms(uint32_t n)

76 {

77 usleep(n*1000);

78 }

79

80 /*****************************************************************

81 函数功能:中断服务函数

82 入口参数:

83 返回参数:

84 说明 :CAN 控制器的中断服务函数

85 ******************************************************************/

86 void can_isr(void* context, alt_u32 id)

87 {

88 uint8_t status;

89 status = IORD_8DIRECT(CanBaseAddr, SJA_IR);

90 printf("status:%xn",status);

91 if(status & RI_BIT){ //判断是否是接收中断

92 can_rxf(); //接收数据

93 rx_flag = 1;

94 }

95 }

代码第13行起,我们宏定义了帧格式、数据长度等信息。另外第20行的验收屏蔽位设为全1,这样就可以接收所有的ID信息。代码第36行起的3行我们定义了需发送的数据18、12、

15,发送3字节的数据与代码第15行的DLC对应。代码第46和47行的总线定时寄存器配置后,

在16MHz的CAN控制器输入时钟下,CAN控制器输出的波特率为1Mbps。代码第49行起CAN控制器

复位之后处于接收状态,当接收到有效数据后触发接收中断,将接收到的数据显示在数码管

上,如果是按下按键,就发送数据。

下载验证

讲完了软件工程,接下来我们就将该实验下载至我们的开拓者开发板进行验证。

首先我们将两个开拓者开发板上的CAN接口用两根杜邦线连接起来,如图 20.5.1所示。

连接时注意接口位置一一对应,不要接反了。另外开拓者开发板上的CAN接口与RS485接口十

分相像,使用时请注意区分。还有一点需要注意的是,两块开发板的P5口都需要使用杜邦线

或者跳帽进行连接选择CAN口,否则无法进行CAN通信。然后分别将两个开发板上的下载器一

端连电脑,另一端与开发板上的JTAG下载端口连接,最后连接电源线并打开电源开关。然后

我们将程序固化到开发板上。

图 20.5.1 连接及结果图

固化完成后我们依次按下任意一个开发板上KEY2按键,可以观察到另外一个开发板上依次显示收到的数据18、12和25,说明程序下载验证成功。

相关推荐