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: 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:
爲了使用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編程詳解