终于有人把CAN通信说明白了
大家好!我是付工。
CAN通信起源于汽车行业,近几年发展比较迅速,慢慢在其他行业也有所应用。
CAN 的高性能和可靠性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。CAN总线协议是建立在OSI七层参考模型的基础上的,但是它的模型结构只有三层,即物理层、数据链路层和应用层。
CAN基础
说起CAN通信,可能很多人都比较陌生,但实际上我们却一直在和它打交道。随着家用汽车的普及,我们开车过程中的每次刹车、每次踩油门,甚至每次的开车门、开车窗,其实都是CAN通信的应用。

CAN 是Controller Area Network 的缩写,是ISO国际标准化的串行通信协议。1986年,为适应“减少线束的数量”、“通过多个LAN,进行大量数据的高速通信”的需要,德国电气商博世公司开发了面向汽车的CAN 通信协议。此后,CAN 通过ISO11898 及ISO11519 进行了标准化,现在在欧洲已是汽车网络的标准协议。
CAN总线是一种串行数据通信协议,其通信接口中集成了CAN协议中的物理层和数据链路层功能,可完成对通信数据的成帧处理,包括位填充、数据块编码、CRC校验及优先级判别等工作。CAN总线有以下特点:
多主工作,网络上任意一个节点均可以在任意时刻主动向网络上的其他节点发送信息,而不区分主从,通信方式灵活。
网络上的节点信息会分成多种不同的优先级,可以满足不同的实时性要求。
采用非破坏性位仲裁机制,当两个节点同时向网络上传送信息时,优先级低的节点主动停止数据发送,而优先级高的节点则不受影响继续传输。
可以点对点、一点对多点及全局广播来进行数据传输。
直接通信距离最远可达10KM。
通信速率最高可达1MB/S。
节点数实际可达110个。
采用短帧结构,每一帧的有效字节数位8个。
每帧信息都有CRC校验及其他检错机制,数据出错率较低。
通信介质可采用双绞线、同轴电缆及光纤,一般采用廉价的双绞线即可。
节点在错误严重的情况下,具有自动关闭总线的功能,切换与总线之间的联系,以使总线上的其他操作不受影响。
CAN与RS485
RS-485总线也是现在工业现场应用非常广泛的总线协议之一,为了便于大家更好的理解CAN通信,我们可以结合RS-485通信来做下对比:
通信距离:RS-485总线最大传输距离是1.2KM,CAN总线最大传输距离是10KM。
传输速率:RS-485总线传输速率为300-10M bps,CAN总线传输速率为5K-1M bps。
网络结构:RS-485总线网络构成为主从式集散控制系统,CAN总线可以实现点对点、一对多及全局广播的形式来发送接收数据。
网络容量:RS-485总线网络的一条通信最大可接255个节点,CAN总线节点可达110个或更多。
通信方式:RS-485总线网络很难实现实时通信,CAN采用非破坏性技术,并结合位仲裁机制,可以大大节省总线冲突裁决时间。
通信可靠性:RS-485总线网络的容错与检错能力较差,CAN总线在错误严重的情况下,具有自动关闭总线的功能。
CAN硬件
学习CAN通信,需要有CAN卡的支持。
Kvaser是瑞士的一家专门提供CAN和LIN总线分析仪及数据记录仪的公司,在CAN产品开发领域已经有近30年的经验,本案例选择的CAN通信硬件型号是Kvaser Leaf Light v2,产品如下图:

为了配套该产品,还准备了一个威柏电子(Westpac)提供的模拟控制器RL78 CAN ECU来进行功能测试,如下图所示,该模拟控制器可以通过上位机控制,实现速度调节及转向灯控制。

硬件准备就绪,这个接线也比较简单,由于是已经集成好的DB9插头,所以ECU与CAN卡之间直接公头母头对接就可以了,CAN卡通过USB接入电脑,安装好驱动后,就可以通过电脑的设备管理器中看到CAN设备了,如下图所示:

CAN开发
CAN开发的本质是二次开发。
CAN通信开发需要调用厂家提供好的一些SDK或者Lib库,而且厂家一般都是提供一些Demo,Demo中包含多种不同的语言,我们找到dotnet或者C#或者cs的字眼,然后打开相关的案例进行研究。Kvaser并没有提供类似的Demo,只是提供了一个Kvaser CANLib SDK的软件,安装之后打开包含以下内容:

接着打开dotnet >> win32 >> fw40,找到下面这些dll,我们的开发主要就是基于这个文件夹里面的dll来实现的,dotnet下面有win32和x64两个文件夹,这里根据不同的项目平台版本,选择相应的dll,这里主要使用canlibCLSNET.dll和kvadblibCLSNET.dll,前者用于通信,后者用于DBC解析。

Kvaser针对CANLib库的使用,提供了一个HTML帮助手册,如下图所示,如果对于某个函数或者参数不理解,可以通过这个帮助手册来查找:

虽然有了dll以及帮助手册,但是对于应该调用哪些方法,调用方法之间的顺序,很多时候我们仍然是一头雾水,下面的这个开发指南应该是雪中送炭。

代码编写
万事具备之后,这时候我们就可以进行程序开发了,先创建一个Windows窗体应用程序,项目名称为xbd.kvaserCANECU,界面初步设计如下图所示:

1、UI界面设计完成后,首先将canlibCLSNET.dll复制到项目中并添加引用
2、在窗体初始化中调用canInitializeLibrary初始化并初始化波特率。
public FrmMain()
{
InitializeComponent();
//初始化CanLib
Canlib.canInitializeLibrary();
this.cmb_BaudRate.Items.Add("500000");
this.cmb_BaudRate.Items.Add("250000");
this.cmb_BaudRate.SelectedIndex = 1;
btn_Refresh_Click(null, null);
}
3、刷新按钮事件下,获取所有能获取到的CAN通信信息。
private Canlib.canStatus canstatus;
private void btn_Refresh_Click(object sender, EventArgs e)
{
int count = 0;
canstatus = Canlib.canGetNumberOfChannels(out count);
if (canstatus != Canlib.canStatus.canOK)
{
HandleError("canGetNumberOfChannels", canstatus);
return;
}
this.cmb_Channel.Items.Clear();
for (int i = 0; i < count; i++)
{
string result = string.Empty;
object obj;
//获取通道
canstatus = Canlib.canGetChannelData(i, Canlib.canCHANNELDATA_CHANNEL_NAME, out obj);
HandleError("canCHANNELDATA_CHANNEL_NAME", canstatus);
result += obj.ToString();
//获取序列号
canstatus = Canlib.canGetChannelData(i, Canlib.canCHANNELDATA_CARD_SERIAL_NO, out obj);
HandleError("canCHANNELDATA_CARD_SERIAL_NO", canstatus);
result += " " + obj.ToString();
this.cmb_Channel.Items.Add(result);
}
if (count > 0)
this.cmb_Channel.SelectedIndex = 0;
}
4、打开按钮事件中,实现打开CAN卡及关闭CAN卡的功能,打开成功后,进行DBC文件解析。
private void btn_Open_Click(object sender, EventArgs e)
{
if (this.btn_Open.Text == "打开")
{
//打开
handle = Canlib.canOpenChannel(this.cmb_Channel.SelectedIndex, Canlib.canOPEN_OVERRIDE_EXCLUSIVE + Canlib.canOPEN_ACCEPT_VI
//设置波特率
canstatus = Canlib.canSetBitrate(handle, Convert.ToInt32(this.cmb_BaudRate.Text));
HandleError("canSetBitrate", canstatus);
//BUS ON
canstatus = Canlib.canBusOn(handle);
if (canstatus != Canlib.canStatus.canOK)
{
HandleError("canBusOn", canstatus);
return;
}
else
{
if (dbcReader.ImportDbc("EDU.dbc"))
{
this.gb_Control.Enabled = true;
this.btn_Open.Text = "关闭";
this.tssl_Info.Text = "系统初始化成功";
this.writeTimer = new System.Windows.Forms.Timer();
this.writeTimer.Interval = 100;
this.writeTimer.Tick += WriteTimer_Tick;
this.writeTimer.Enabled = true;
}
else
{
this.tssl_Info.Text = "系统初始化错误";
}
}
}
else
{
Canlib.canClose(handle);
this.gb_Control.Enabled = false;
this.btn_Open.Text = "打开";
this.writeTimer.Enabled = false;
}
}
5、至于速度和方向等控制,我们可以编写一个通用写入方法。
private bool CommonWrite(string VarName, double value)
{
if (dbcReader.dicSigNameID.ContainsKey(VarName))
{
//通过名称找到ID
int id = dbcReader.dicSigNameID[VarName];
if (dbcReader.dicMessage.ContainsKey(id))
{
//通过ID找到Message
var msg = dbcReader.dicMessage[id];
//通过MESSAGESTRUCT的信号字典遍历找到信号
if (msg.dicsignal.ContainsKey(VarName))
{
var sig = msg.dicsignal[VarName];
switch (sig.name)
{
case "Speed":
//通过信号做转换
status = Kvadblib.StoreSignalValuePhys(sig.sh, SpeedBuffer, SpeedBuffer.Length, value);
if (status == Kvadblib.Status.OK)
{
//执行写入
return Canlib.canWrite(handle, id, SpeedBuffer, msg.dlc, (int)canMSGFlag.canMsg_STD) == Canlib.canStatus.canOK;
}
break;
case "RightLight":
case "LeftLight":
//通过信号做转换
status = Kvadblib.StoreSignalValuePhys(sig.sh, LightBuffer, LightBuffer.Length, value);
if (status == Kvadblib.Status.OK)
{
//执行写入
return Canlib.canWrite(handle, id, LightBuffer, msg.dlc, (int)canMSGFlag.canMsg_STD) == Canlib.canStatus.canOK;
}
break;
default:
break;
}
}
}
}
return false;
}
6、调用通用写入方法,实现速度和方向的控制,代码如下:
private void WriteTimer_Tick(object sender, EventArgs e)
{
CommonWrite("Speed", this.trackSpeed.Value);
if (this.rdo_Left.Checked)
{
CommonWrite("RightLight", 0);
CommonWrite("LeftLight", 1);
}
else if (this.rdo_Right.Checked)
{
CommonWrite("LeftLight", 0);
CommonWrite("RightLight", 1);
}
else if (this.rdo_Both.Checked)
{
CommonWrite("LeftLight", 1);
CommonWrite("RightLight", 1);
}
else
{
CommonWrite("LeftLight", 0);
CommonWrite("RightLight", 0);
}
}