ZYNQ学习之路7.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-2所示:
如果图中节点采用等长接线连接,可以不使用CAN集线器设备,调节每个节点的终端电阻即可实现组网。终端电阻R=N*60Ω,N是分支节点的个数。注意网络中心不能加任何电阻。
在实际的应用中,我们几乎无法做到等长,在T型网络中也很难做到支线较短的情况,这个时候我们就需要使用CAN集线器来进行分支,如图1-3所示。
集线器的使用可以使布线灵活,可根据需要进行任意分支,减少了约束条件。
1.2 CAN信号
CAN报文传送的位流信号采用非归零码(NZR)编码,也就是一个完整的电平要么是显性要么是隐性,在“隐性”状态下,CAN_H和CAN_L都是平均电压电平,Vdiff近似为零,在“显性”状态下,以大于最小阈值的差分电压表示。CAN电平标准有两个,IOS11898和IOS11519,两者的差别在于电平特性的不同,如图1-4所示:
CAN总线的通信距离与波特率成反比,一般的工程中比较常用的500kbps,CAN总线中任意两个节点的最大传输距离与速率如下表所示:
波特率/kbps |
1000 |
500 |
250 |
125 |
100 |
50 |
20 |
10 |
5 |
最大距离/m |
40 |
130 |
270 |
530 |
620 |
1300 |
3300 |
6700 |
10000 |
1.3 CAN控制器与收发器
CAN控制器和CAN收发器是实现CAN网络物理层和数据链路层所必备的组件,其中CAN控制器是将欲发送的信息(报文)转换成符合CAN规范的CAN帧,通过CAN收发器在CAN总线上交换信息。
CAN控制器分为两类:独立的控制器芯片和集成在微控制器中的外设。ZYNQ7000中集成了CAN控制器。
CAN控制器原理框图如图1-5所示:
CAN核心模块用于将串行接收的数据转换为并行数据,发送则相反。验收滤波器根据用户的设置过滤掉不需要接收的报文。
CAN收发器是CAN控制器与物理总线之间的接口,用于将CAN控制器的逻辑电平转换为CAN总线的差分电平,将二进制码流转换为差分信号发送,将差分信号转换为二进制码流接收。ZTurn board上使用的CAN收发器是TJA1050,电平转换示意图如图1-6所示:
二. CAN总线协议
CAN总线是一种广播类型的总线,在总线上连接的所有节点都可以监听总线上传输的数据。CAN总线的控制器提供了过滤功能,接收信息时只保留与自己相关的信息。
2.1 总线仲裁
只要总线处于空闲状态,总线上的任何节点都可以发送报文,如果两个或两个以上的节点开始发送报文,那么就会存在总线冲突的可能。CAN使用了标识符的逐位仲裁方法,在发送数据的同时监控总线电平,如果电平相同,则这个单元可以继续发送。如果不同则失去仲裁退出发送状态,如果出现不匹配的位不是在总裁期间则产生错误事件。
2.2 帧结构
CAN总线传输的基本单位是CAN帧,CAN的通信帧分为5中类型,分别是数据帧、远程帧、错误帧、过载帧和帧间隔。
数据帧是节点之间用来收发数据,是使用最多的帧类型;远程帧用来接收节点向发送节点接收数据;错误帧是某个节点发送帧错误来向其他节点通知的帧;过载帧是接收节点用来向发送节点告知自身接收能力的帧;帧间隔是用来将数据帧、远程帧与前面帧隔离的帧。
数据帧根据仲裁域格式的不同,分为标准帧(CAN2.0 A)和扩展帧(CAN2.0 B),如图2-2所示:
其中SRR为"替代远程请求位",IDE为"扩展标识符位",RTR为"远程传输请求位",CRC为"循环冗余校验",ACK为应答。
从图2-2可以看出,基本帧的格式可以分为仲裁段,数据段,CRC段和ACK段。
远程帧与数据帧非常相似,只是远程帧没有数据域,一个远程帧如图2-3所示:
远程帧分为6个段,也分为标准帧和扩展帧,且RTR位为1(隐性电平),远程帧与数据帧的差别如下表所示:
比较项 |
数据帧 |
远程帧 |
ID |
发送节点的ID |
被请求发送节点的ID |
SRR |
0(显性电平) |
1(隐性电平) |
RTR |
0(显性电平) |
1(隐性电平) |
DLC |
发送数据长度 |
请求的数据长度 |
是否有数据段 |
是 |
否 |
CRC校验范围 |
帧起始+仲裁段+控制段+数据段 |
帧起始+仲裁段+控制段 |
三. ZYNQ使用CAN
3.1 构建硬件系统
使用ZturnBoard的模板工程,在此基础上添加CAN0外设,引脚为MIO14, MIO15.时钟频率默认即可,编译综合之后生成fsbl文件,制作SD卡启动镜像。
配置内核,将CAN的驱动编译进内核:
<*>Networking support --->
<*>CAN bus subsystem support --->
CAN device Drivers --->
<*>Xilinx CAN
修改设备树文件,添加CAN0节点。ZturnBoard开发板提供了设备树zynq-zturn.dts文件,该文件引用了zynq-7000.dts文件,该文件包含了PS外设所有的设备树描述节点,CAN0的描述信息如下:
can0: aaa@qq.com {
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:
为了使用CAN,需要下载CAN的工具包,将canutils_install目录复制到开发板,libskt_install文件夹中的libsocketcan.so.2.2.0复制到开发板的lib目录下,并建立软链接:ln -s libsocketcan.so.2.2.0 libsocketcan.so.2;
在发行版Linux中可以使用以下一些命令:
- 设置can0的波特率,这里设置为100kbps:ip link set can0 type can bitrate 100000
- 设置完成后可以通过以下命令查询can0设备的参数:ip -details link show can0
- 当设置完成后,可以使用以下命令使能can0设备:ifconfig can0 up
- 使用以下命令关闭can0设备:ifconfig can0 down
- 在设备工作中,可以使用下面的命令来查询工作状态:ip -d -s link show can0
- 设置can0为回环模式,自发自收: ip link set can0 up type can loopback on
在ramdisk文件系统中,复制canutils_install到系统目录中,进入canutils_install目录,使用sbin目录下的工具:
- 设置can0的波特率:./canconfig can0 bitrate 100000
- 启动can0:./canconfig can0 start
- 关闭can0: ./canconfig can0 stop
- 设置回环模式: ./canconfig can0 ctrlmode loopback on
- 发送can数据: ./cansend can0 -i 0x14
- 接收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 <iostream>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
using namespace std;
int main()
{
cout<<"test for can socket send!"<<endl;
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"<<endl;
nbytes = write(s, &frame[0], sizeof(frame[0]));//send frame[0]
if(nbytes != sizeof(frame[0]))
{
cout<<"Send error frame[0]"<<endl;
}
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]"<<endl;
}
sleep(1);//wait 1s
}
close(s);
cout<<"send can frame over!!!"<<endl;
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 <iostream>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
using namespace std;
int main()
{
cout<<"test for can socket!"<<endl;
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的数据帧。
四 总结
本文详细介绍了CAN总线的原理以及在Linux系统中的使用,在实验过程中需要注意动态链接库的使用以及CAN的设置,确保数据链接正常,然后再调试软件部分,实验并不难,仅在于学习如何使用CAN网络。
参考资料
[1]. CAN总线要点
[2]. CAN总线(一)
[3].Linux CAN编程详解
下一篇: pyenv命令管理多个Python版本