ZYNQ学习之路——CAN总线学习

本文转载自:亦梦云烟的博客

CAN总线是控制器局域网(Controller Area Network)的简称,是国际上应用最广泛的现场总线之一,CAN总线协议已成为汽车控制系统和嵌入式工业局域网的标准总线。CAN总线有很多优秀的特点,比如:传输速度最高达1Mbps,通信距离最远到10Km,无损位仲裁机制,多主结构,理论上挂载到总线上的设备没有数量限制。

因此掌握CAN总线协议是很重要的,本文简要介绍CAN总线协议,以Linux驱动CAN网络为重点介绍。

一. CAN总线的物理特性
1.1 CAN总线的网络结构
CAN总线有CAN_H和CAN_L两根线组成,线上传输差分信号,为了避免信号的反射和不连续,需要在总线的两个端点接120欧姆电阻,不可不接或单接,因为双绞线的特性阻抗为120欧姆,在终端模拟无限远的传输线。CAN网络一般采用"T"型连接,如下图1-1所示,在波特率为1Mbps的情况下,分支长度最好不要超过0.3m。

图1-1: CAN总线T型网络结构

当然也可采用星型拓扑结构,如图1-2所示:

图1-2: CAN总线星型网络结构

如果图中节点采用等长接线连接,可以不使用CAN集线器设备,调节每个节点的终端电阻即可实现组网。终端电阻R=N*60Ω,N是分支节点的个数。注意网络中心不能加任何电阻。

在实际的应用中,我们几乎无法做到等长,在T型网络中也很难做到支线较短的情况,这个时候我们就需要使用CAN集线器来进行分支,如图1-3所示。

图1-3: 使用集线器的CAN网络

集线器的使用可以使布线灵活,可根据需要进行任意分支,减少了约束条件。

1.2 CAN信号
CAN报文传送的位流信号采用非归零码(NZR)编码,也就是一个完整的电平要么是显性要么是隐性,在“隐性”状态下,CAN_H和CAN_L都是平均电压电平,Vdiff近似为零,在“显性”状态下,以大于最小阈值的差分电压表示。CAN电平标准有两个,IOS11898和IOS11519,两者的差别在于电平特性的不同,如图1-4所示:

图1-4: CAN电平标准

CAN总线的通信距离与波特率成反比,一般的工程中比较常用的500kbps,CAN总线中任意两个节点的最大传输距离与速率如下表所示:

1.3 CAN控制器与收发器
CAN控制器和CAN收发器是实现CAN网络物理层和数据链路层所必备的组件,其中CAN控制器是将欲发送的信息(报文)转换成符合CAN规范的CAN帧,通过CAN收发器在CAN总线上交换信息。

CAN控制器分为两类:独立的控制器芯片和集成在微控制器中的外设。ZYNQ7000中集成了CAN控制器。

CAN控制器原理框图如图1-5所示:

图1-5: CAN控制器原理

CAN核心模块用于将串行接收的数据转换为并行数据,发送则相反。验收滤波器根据用户的设置过滤掉不需要接收的报文。

CAN收发器是CAN控制器与物理总线之间的接口,用于将CAN控制器的逻辑电平转换为CAN总线的差分电平,将二进制码流转换为差分信号发送,将差分信号转换为二进制码流接收。ZTurn board上使用的CAN收发器是TJA1050,电平转换示意图如图1-6所示:

图1-6: CAN收发器转换电平示意图

二. CAN总线协议
CAN总线是一种广播类型的总线,在总线上连接的所有节点都可以监听总线上传输的数据。CAN总线的控制器提供了过滤功能,接收信息时只保留与自己相关的信息。

2.1 总线仲裁
只要总线处于空闲状态,总线上的任何节点都可以发送报文,如果两个或两个以上的节点开始发送报文,那么就会存在总线冲突的可能。CAN使用了标识符的逐位仲裁方法,在发送数据的同时监控总线电平,如果电平相同,则这个单元可以继续发送。如果不同则失去仲裁退出发送状态,如果出现不匹配的位不是在总裁期间则产生错误事件。

图2-1: CAN总线仲裁

2.2 帧结构
CAN总线传输的基本单位是CAN帧,CAN的通信帧分为5中类型,分别是数据帧、远程帧、错误帧、过载帧和帧间隔。

数据帧是节点之间用来收发数据,是使用最多的帧类型;远程帧用来接收节点向发送节点接收数据;错误帧是某个节点发送帧错误来向其他节点通知的帧;过载帧是接收节点用来向发送节点告知自身接收能力的帧;帧间隔是用来将数据帧、远程帧与前面帧隔离的帧。

数据帧根据仲裁域格式的不同,分为标准帧(CAN2.0 A)和扩展帧(CAN2.0 B),如图2-2所示:

图2-2: 数据帧帧结构

其中SRR为"替代远程请求位",IDE为"扩展标识符位",RTR为"远程传输请求位",CRC为"循环冗余校验",ACK为应答。

从图2-2可以看出,基本帧的格式可以分为仲裁段,数据段,CRC段和ACK段。

远程帧与数据帧非常相似,只是远程帧没有数据域,一个远程帧如图2-3所示:

图2-3: CAN远程帧

远程帧分为6个段,也分为标准帧和扩展帧,且RTR位为1(隐性电平),远程帧与数据帧的差别如下表所示:

三. ZYNQ使用CAN
3.1 构建硬件系统
使用ZturnBoard的模板工程,在此基础上添加CAN0外设,引脚为MIO14, MIO15.时钟频率默认即可,编译综合之后生成fsbl文件,制作SD卡启动镜像。

配置内核,将CAN的驱动编译进内核:

<*>Networking support --->
    <*>CAN bus subsystem support --->
        CAN device Drivers --->
        <*>Xilinx CAN

图3-1: Linux内核添加CAN驱动

修改设备树文件,添加CAN0节点。ZturnBoard开发板提供了设备树zynq-zturn.dts文件,该文件引用了zynq-7000.dts文件,该文件包含了PS外设所有的设备树描述节点,CAN0的描述信息如下:

can0: can@e0008000 {
    compatible = "xlnx,zynq-can-1.0";
    status = "disabled";
    clocks = <&clkc 19>, <&clkc 36>;
    clock-names = "can_clk", "pclk";
    reg = <0xe0008000 0x1000>;
    interrupts = <0 28 4>;
    interrupt-parent = <&intc>;
    tx-fifo-depth = <0x40>;
    rx-fifo-depth = <0x40>;
};

所以在zynt-zturn.dts文件中添加以下描述即可:

&can0 {
	status = "okay";
};

准备好以上的文件之后,启动Linux系统。

3.2 Linux系统中使用CAN网络
在Linux系统中,CAN总线接口设备作为网络设备被系统进行统一管理,本节介绍控制台下CAN总线的使用。

Linux系统启动之后,终端输入ifconfig -a后能看到网络设备中增加了can0:

图3-2: can0网络设备信息

为了使用CAN,需要下载CAN的工具包,将canutils_install目录复制到开发板,libskt_install文件夹中的libsocketcan.so.2.2.0复制到开发板的lib目录下,并建立软链接:ln -s libsocketcan.so.2.2.0 libsocketcan.so.2;

在发行版Linux中可以使用以下一些命令:

1. 设置can0的波特率,这里设置为100kbps:ip link set can0 type can bitrate 100000
2. 设置完成后可以通过以下命令查询can0设备的参数:ip -details link show can0
3. 当设置完成后,可以使用以下命令使能can0设备:ifconfig can0 up
4. 使用以下命令关闭can0设备:ifconfig can0 down
5. 在设备工作中,可以使用下面的命令来查询工作状态:ip -d -s link show can0
6. 设置can0为回环模式,自发自收: ip link set can0 up type can loopback on

在ramdisk文件系统中,复制canutils_install到系统目录中,进入canutils_install目录,使用sbin目录下的工具:

1. 设置can0的波特率:./canconfig can0 bitrate 100000
2. 启动can0:./canconfig can0 start
3. 关闭can0: ./canconfig can0 stop
4. 设置回环模式: ./canconfig can0 ctrlmode loopback on
5. 发送can数据: ./cansend can0 -i 0x14
6. 接收can数据: ./candump can0

3.3 CAN网络应用程序开发
Linux系统将CAN设备作为网络设备进行管理,提供了SocketCAN接口,使得CAN总线通信可以像以太网一样,应用程序开发接口更加通用,也更灵活。

(1)初始化

SocketCAN中大部分的数据结构和函数定义在linux/can.h中,CAN总线套接字的创建采用标准的网络套接字来完成。

int s, nbytes;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame[2] = {{0}};
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);//create CAN socket
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);	//can0 device
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr*)&addr, sizeof(addr)); //bind socket to can0

(2)数据发送

CAN总线每次接收数据都是以can_frame为单位,该结构体定义如下:

struct canfd_frame {
    canid_t can_id;  /* 32 bit CAN_ID + EFF/RTR/ERR flags */
    __u8    len;     /* frame payload length in byte */
    __u8    flags;   /* additional flags for CAN FD */
    __u8    __res0;  /* reserved / padding */
    __u8    __res1;  /* reserved / padding */
    __u8    data[CANFD_MAX_DLEN] __attribute__((aligned(8)));
};

can_id为帧的标识符,如果发送的是标准帧,就使用can_id的低11位;如果为扩展帧,就是用0~28位。can_id的低29,30,31位是帧的标识位,用来定义帧的类型,如下所示:

/* special address description flags for the CAN_ID */
#define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */
#define CAN_RTR_FLAG 0x40000000U /* remote transmission request */
#define CAN_ERR_FLAG 0x20000000U /* error message frame */

数据发送使用write函数实现,例如:发送数据帧标识符为0x123,包含单个字节0xAB的数据,发送方法如下:

struct can_frame frame;
frame.can_id = 0x123;
frame.can_dlc = 1;
frame.data[0] = 0xAB;
int nbytes = write(s, &frame, sizeof(frame));
if(nbytes != sizeof(frame))
printf("Error\n");

如果发送的是远程帧,则frame.can_id = CAN_RTR_FLAG | 0x123

(3)数据接收

数据接收使用read函数来完成,实现如下:

struct can_frame frame;
int nbytes = read(s, &frame, sizeof(frame))

(4)错误处理

当接收到数据帧,可以通过判断can_id中的CAN_ERR_FLAG位来判断接收的帧是否为错误帧,如果为错误帧,可以通过can_id中的其它位来判断具体的错误原因。

(5)过滤设置

通过设置过滤规则,可以过滤掉不需要接收的数据。过滤规则使用can_filter结构体来实现,定义如下:

struct can_filter {
    canid_t can_id;
    canid_t can_mask;
};

接收到的数据帧的can_id & can_mask == can_filter .can_id & can_filter .can_mask则接收。

(6)回环功能

在默认情况下,本地回环功能是开启的,可以使用下面的方法关闭/开启:

int loopback = 0;//0:关闭,1:开启
setsockopt(s_s,SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));

在本地回环功能开启的情况下,所有的发送的帧都会被回环到与CAN总线接口对应的套接字上。默认情况下,发送CAN报文不想接收自己发送的报文,因此发送套接字上的回环功能是关闭的,打开这一功能可以使用如下方法:

int ro = 1;//0:关闭,1:开启
setsockopt(s_s, SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS, &ro, sizeof(ro));

3.4 Linux系统中CAN接口应用程序示例:
首先使用两块ZturnBoard开发板,使用连根导线连接CAN的H和L两个端点,复制libsocketcan.so.2.2.0到开发板并建立软链接,设置两个开发板的can0波特率一致,启动can0。

can发送程序:
#include "unistd.h"
#include "net/if.h"
#include "sys/ioctl.h"
#include "linux/can/raw.h"
#include "linux/can.h"
#include "sys/socket.h"
#include
#include
#include
#include

using namespace std;

int main()
{
cout<<"test for can socket send!"<

int s, nbytes;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame[2];
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);//create CAN socket
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr); //can0 device
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr*)&addr, sizeof(addr));//bind socket to can0

//disable filter
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
//two frame
frame[0].can_id = 0x11;
frame[0].can_dlc = 1;
frame[0].data[0] = 'A';
frame[1].can_id = 0x22;
frame[1].can_dlc = 1;
frame[1].data[0] = 'B';
for(int i = 0; i<10; i++)
{
cout<<"send can frame"< nbytes = write(s, &frame[0], sizeof(frame[0]));//send frame[0]
if(nbytes != sizeof(frame[0]))
{
cout<<"Send error frame[0]"< }
sleep(1);//wait 1s
nbytes = write(s, &frame[1], sizeof(frame[1]));//send frame[0]
if(nbytes != sizeof(frame[1]))
{
cout<<"Send error frame[1]"< }
sleep(1);//wait 1s
}
close(s);
cout<<"send can frame over!!!"<

return 0;
}

can接收程序:
#include "unistd.h"
#include "net/if.h"
#include "sys/ioctl.h"
#include "linux/can/raw.h"
#include "linux/can.h"
#include "sys/socket.h"
#include
#include
#include
#include

using namespace std;

int main()
{
cout<<"test for can socket!"<

int s, nbytes;
struct sockaddr_can addr;
struct ifreq ifr;
//receive frame which id==0x11
struct can_filter rfilter;
struct can_frame frame;
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));

rfilter.can_id = 0x11;
rfilter.can_mask = CAN_SFF_MASK;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
while(1)
{
nbytes = read(s, &frame, sizeof(frame));
if(nbytes > 0)
{
printf("ID=0x%0x DLC=%d data[0]=0x%x\n", frame.can_id,
frame.can_dlc,frame.data[0]);
}
}

return 0;
}

分别再两个开饭中运行两个程序,在接收端可以看到只接收了地址ID=0x11的数据帧。

图3-3: CAN发送与接收测试

四 总结
本文详细介绍了CAN总线的原理以及在Linux系统中的使用,在实验过程中需要注意动态链接库的使用以及CAN的设置,确保数据链接正常,然后再调试软件部分,实验并不难,仅在于学习如何使用CAN网络。

参考资料

[1]. CAN总线要点

[2]. CAN总线(一)

[3].Linux CAN编程详解

最新文章

最新文章