上个月一个同事Z跳槽去了德赛西威,Z之前是完全不懂诊断的MCU工程师,去德赛后做诊断开发,让我感觉到,汽车嵌入式行业,CAN和诊断工程师还是比较稀缺的。之前我和Z共同负责一个项目,我负责CAN网络和诊断部分,经过4个多月的奋战,我一个人把汽车诊断UDS的系统搭建出来,自认为,完成度很高,代码质量也极好。他跳槽去德赛做诊断开发,我想多少有点受益于我开发的诊断代码,另外我也悉心指导他,讲解相关的知识,他确实也学到不少,即便是现在,他有问题也会打电话向我求助。
前两年的工作和学习,我了解到汽车CAN网络和诊断还是比较难以学习的,网上资料参差不齐,我花了很大功夫才把这部分掌握,所以考虑写几篇相关的文章,以帮助后来者。
网络层的国际标准是ISO 15756-2,该标准详细规定了协议的具体细节。CAN总线是一帧8个字节,该协议可以使CAN总线高效的传输大约8个字节(up to 4095 bytes)的命令和数据。基于该标准文档,我开发出了一个独立性良好的协议栈,工作在上层诊断协议之下和下层CAN驱动之上,下面详解开发协议栈时需要实现的部分(基于 ISO 15765-2:2004(E))
4.2小节是描述网络层协议提供给上层的服务
有四个,其中第1个是发送消息的服务,我实现为一个外部函数,提供给上层调用,第2,3,4是上层获取协议栈发送和接收状态的服务,我按照回调函数的方式实现,于是变成了上层提供给网络层的接口。如果转成C++代码,可以用虚函数来实现。
1) N_USData.request
是网络层提供给上层的发送消息的服务,5.2.1小节对其有详细的描述,我只实现了两个参数,msg_buf和msg_dlc,发送时根据消息长度判断是单帧发送还是多帧发送,
extern void network_send_udsmsg (uint8_t msg_buf[],uint16_t msg_dlc)
{
if (msg_dlc==0|| msg_dlc> UDS_FF_DL_MAX)return;
if (msg_dlc<= UDS_SF_DL_MAX)
{
send_singleframe (msg_buf, msg_dlc);
}
else
{
nwl_st = NWL_XMIT;
send_multipleframe (msg_buf, msg_dlc);
}
}
2)N_USData_FF.indication
该服务用来通知上层,网络层收到了首帧,5.2.3小节对其有详细的描述,我实现了一个参数msg_dlc,该函数通过回调实现,具体细节在上层代码中,按下不表。
函数原型声明如下
typedef void (*ffindication_func) (uint16_t msg_dlc);
网络层接收到首帧后调用该服务。
3)N_USData.indication
该服务把接收到的完整消息传递给上层,5.2.4小节对其有详细的描述,我实现了3个参数,msg_buf,msg_dlc和n_result,该函数通过回调实现,具体细节在上层代码中,按下不表。
函数原型声明如下
typedef void (*indication_func) (uint8_t msg_buf[], uint16_t msg_dlc, n_result_t n_result);
该函数调用较多:
1.接收到单帧,with N_OK
2.接收连续帧,如果sn错误,with N_WRONG_SN
3.接收连续帧,如果长度正确,with N_OK
4.网络层主循环中,如果CR定时器超时,with N_TIMEOUT_Cr
5.接收到首帧和单帧,如果网络层状态异常,with N_UNEXP_PDU
4)N_USData.confirm
该服务用来通知上层,消息发送已经完成,并返回成功与否,5.2.2小节对其有详细的描述。我实现了1个参数n_result,该函数通过回调实现。具体细节在上层代码中,按下不表。
函数原型声明如下
typedef void(*confirm_func)(n_result_t n_result);
该函数调用如下:
1.接受到流控帧,如果流状态>= FS_RESERVED, with N_INVALID_FS
2.接收到流控帧,如果流状态== FS_OVERFLOW, with N_BUFFER_OVFLW
3.网络层主循环中,如果BS定时器超时,with N_TIMEOUT_Bs
协议参数控制服务有两个,我没有实现,具体用处我还不明白,但是不影响实现协议栈功能。
第6节描述网络层协议内容
当消息长度小于等于6(扩展地址和混合地址)或者7(普通地址)个字节时,是通过一个N_PDU(数据单元)发送完成,叫做SF(单帧)。
当消息长度较大时,是通过多个N_PDUs(数据单元)发送完成,这种数据单元叫做FF(首帧,第一个N_PDU)和CF(连续帧,后续的N_PDUs)。
FF(首帧)包括前面5个(扩展地址和混合地址)或者6个(普通地址)字节的内容,1个或者多个CF(连续帧),每个CF包括后续的6个(扩展地址和混合地址)或者7个(普通地址)字节的内容,当然也可以少于6个或者7个字节。消息长度信息在FF(首帧)中发送,所有的CF(连续帧)在发送端被编号,以帮助接收者按顺序重组
消息。(最后一句话没什么卵用)
接收者通过Flow control(流控帧)的机制,告知发送者自己有多大的接收能力。(其实就是每两个FC之间允许连续发送多少个CF,每两个CF之间的时间不能过快)
Flow control 包含三个字段:
Flow status(FS),流状态,用来控制发送方接下来的行为,总共有三个定义,分别是FC.CTS(继续发送),FC.WAIT(继续等待),FC_OVFLW(缓存溢出,此时应该终止发送)。
Block Size (BS),每次收到流控帧之后,发送者最大可发送的连续帧的个数。
SeparationTimeMin (STmin),两个连续帧之间的最小间隔。
综上所述,网络层共有4中数据单元类型:SF N_PDU,FF N_PDU, CF N_PDU, FC N_PDU。详细说明在6.4节,不再赘述。
Tale 2 是N_PDU format (数据单元格式),每个N_PDU由三个域组成。
在使用普通地址时,地址域仅由CAN ID组成,CAN消息数据的第一个字节(或前两字节)为N_PCI Bytes。N_PCI(Protocol control information)标识了一条消息的类型和附加信息。
Table 3描述各种类型的N_PDU 的N_PCI bytes的定义。
N_PCI byte的第一个字节的高4位为N_PCItype,标识该N_PDU(数据单元)的类型。
0,SF(单帧)
1,FF(首帧)
2,CF(连续帧)
3,FC(流控帧)
4-F,保留定义
我在程序中接收到一条诊断报文后,通过一条宏定义获取N_PCItype
#define NT_GET_PCI_TYPE(n_pci) (n_pci>>4)
pci_type = NT_GET_PCI_TYPE (frame_buf[0]);
然后根据pci_type进行不同的处理。
(1)单帧的情况下,N_PCI byte第一个字节的低4位为SF_DL(消息长度),范围在1-6(扩展地址和混合地址)或者1-7(普通地址)之间,如果SF_DL错误,网络层应该忽略这条N_PDU
(2)首帧的情况下,N_PCI bytes 第一个字节的低4位和第二个字节共同组成FF_DL(消息长度),范围在8-FFF(扩展地址和混合地址)或者7-FFF(普通地址)之间,如果FF_DL大于接收者的接收缓存,网络层应该丢弃这条消息,并且发送FC with FlowStatus = Overflow
(3)连续帧情况下,N_PCI byte第一个字节的低4位为SN(SequenceNumber),
在每开始发送一段数据的时候SN必须从零开始,FF(首帧)没有SN字段,但应该被认为是SN = 0,
FF之后的第一个CF的SN应该为1,
每发送一个新的CF,SN都应该增加1,
CF的值不应该受FC的影响,
当SN的值达到15的时候,下次发送的CF,SN应被重置为0,
如果SN出错,网络层应该丢弃已接收到的消息,并且调用N_USData.indication服务,with N_WRONG_SN
(4)流控帧情况下,
N_PCI bytes第一个字节的低4位为FS(Flow status),FS有4个定义,
0, CTS ,代表发送者可以正常发送
1, WT ,代表发送者应该再等待下一个FC,并且重启N_BS timer
2, OVFLW,代表接收方缓存溢出,发送方收到此FS后,应该终止发送,调用N_USData.confirm 服务,with N_BUFFER_OVFLW
3-F, Reserved
如果发送者收到的FS出错,网络层应该停止消息发送,并且调用 N_USData.confirm 服务,with N_INVALID_FS
N_PCI bytes的第2个字节为BS(BlockSize),BS代表发送方在收到下一个FC之前,应发送的CF的数量,只有最后一块数据,其CF的数量可以少于BS,BS的值分两个情况,
0, 代表没有BS限制,发送方不必等待FC,把所有的FC一次发送。
1-FF,代表发送方发送BS数量的CF后,需等待FC,
N_PCI bytes的第3个字节为STmin,发送方收到FC后,应该把STmin保存下来,该值表明两个CF之间的最小间隔,STmin的值定义如下图
如果发送方收到一个FC,其STmin的值是Reserved,则发送方应默认STmin为7F(127ms)
STmin参数体现在程序中就是一个定时器,发送完一帧CF后,应该立即启动STmin timer
timer超时之后才能发送下一个CF,我的实现方式如下,nt_timer_run(TIMER_STmin) < 0 代表STmin timer超时。
if (nt_timer_run (TIMER_STmin) < 0)
{
g_xcf_sn++;
if (g_xcf_sn > 0x0f)
g_xcf_sn = 0;
OSMutexPend(UdsMutex,0,&err);
send_len = send_consecutiveframe (&remain_buf[remain_pos], remain_len, g_xcf_sn);
remain_pos += send_len;
remain_len -= send_len;
if (remain_len > 0)
{
if (g_rfc_bs > 0)
{
g_xcf_bc++;
if (g_xcf_bc < g_rfc_bs)
{
nt_timer_start (TIMER_STmin);
}
else
{
/**
* start N_Bs and wait for a fc.
*/
g_wait_fc = TRUE;
nt_timer_start (TIMER_N_BS);
}
}
else
{
nt_timer_start (TIMER_STmin);
}
}
else
{
clear_network ();
}
OSMutexPost(UdsMutex);
}
extern void
network_recv_frame (uint8_t func_addr, uint8_t frame_buf[], uint8_t frame_dlc)
{
uint8_t err;
uint8_t pci_type; /* protocol control information type */
/**
* The reception of a CAN frame with a DLC value
* smaller than expected shall be ignored by the
* network layer without any further action
*/
if(frame_dlc != UDS_VALID_FRAME_LEN) return;
if (func_addr == 0)
g_tatype = N_TATYPE_PHYSICAL;
else
g_tatype = N_TATYPE_FUNCTIONAL;
OSMutexPend(UdsMutex,0,&err);
pci_type = NT_GET_PCI_TYPE (frame_buf[0]);
switch(pci_type)
{
case PCI_SF:
if (nwl_st == NWL_RECV || nwl_st == NWL_IDLE)
{
clear_network ();
if (nwl_st == NWL_RECV)
N_USData.indication (recv_buf, recv_len, N_UNEXP_PDU);
recv_singleframe (frame_buf, frame_dlc);
}
break;
case PCI_FF:
if (nwl_st == NWL_RECV || nwl_st == NWL_IDLE)
{
clear_network ();
if (nwl_st == NWL_RECV)
N_USData.indication (recv_buf, recv_len, N_UNEXP_PDU);
if (recv_firstframe (frame_buf, frame_dlc) > 0)
nwl_st = NWL_RECV;
else
nwl_st = NWL_IDLE;
}
break;
case PCI_CF:
if (nwl_st == NWL_RECV && g_wait_cf == TRUE)
{
if (recv_consecutiveframe (frame_buf, frame_dlc) <= 0)
{
clear_network ();
nwl_st = NWL_IDLE;
}
}
break;
case PCI_FC:
if (nwl_st == NWL_XMIT && g_wait_fc == TRUE)
if (recv_flowcontrolframe (frame_buf, frame_dlc) < 0)
{
clear_network ();
nwl_st = NWL_IDLE;
}
break;
default:
break;
}
OSMutexPost(UdsMutex);
}