欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ZYNQ学习之路7.CAN总线学习

程序员文章站 2024-02-23 22:38:46
...

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。

ZYNQ学习之路7.CAN总线学习
图1-1: CAN总线T型网络结构

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

ZYNQ学习之路7.CAN总线学习
图1-2: CAN总线星型网络结构

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

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

ZYNQ学习之路7.CAN总线学习
图1-3: 使用集线器的CAN网络

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

1.2 CAN信号

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

ZYNQ学习之路7.CAN总线学习
图1-4: CAN电平标准

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所示:

ZYNQ学习之路7.CAN总线学习
图1-5: CAN控制器原理

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

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

ZYNQ学习之路7.CAN总线学习
图1-6: CAN收发器转换电平示意图

二. CAN总线协议

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

2.1 总线仲裁

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

ZYNQ学习之路7.CAN总线学习
图2-1: CAN总线仲裁

2.2 帧结构

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

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

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

ZYNQ学习之路7.CAN总线学习
图2-2: 数据帧帧结构

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

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

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

ZYNQ学习之路7.CAN总线学习
图2-3: CAN远程帧

远程帧分为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
ZYNQ学习之路7.CAN总线学习
图3-1: Linux内核添加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:

ZYNQ学习之路7.CAN总线学习
图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 <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的数据帧。

ZYNQ学习之路7.CAN总线学习
图3-3: CAN发送与接收测试

四 总结

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

 

参考资料

[1]. CAN总线要点

[2]. CAN总线(一)

[3].Linux CAN编程详解