基于CAN总线的汽车诊断协议UDS (网络层 ISO 15765)

    上个月一个同事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 Network layer overview

    4.2 Services provided by network layer to higher layers

    4.2小节是描述网络层协议提供给上层的服务

    (a) Communication services  (通信服务)

    有四个,其中第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

    b) Protocol parameter setting services (协议参数控制服务)

协议参数控制服务有两个,我没有实现,具体用处我还不明白,但是不影响实现协议栈功能


    6 Network layer protocol

    第6节描述网络层协议内容

    6.1-6.4小节简要说明

    当消息长度小于等于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)标识了一条消息的类型和附加信息。

6.5 Protocol control information specification

Table 3描述各种类型的N_PDU 的N_PCI bytes的定义。

基于CAN总线的汽车诊断协议UDS (网络层 ISO 15765)_第1张图片

    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的值定义如下图

基于CAN总线的汽车诊断协议UDS (网络层 ISO 15765)_第2张图片

如果发送方收到一个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);
    }

6.6 Maximum number of FC.Wait frame transmissions (N_WFTmax)

6.6节,最大FC.Wait次数,是本地(local)的参数,不包含在FC中,
        指明接收方最大能连续发送多少个FC.Wait,
        这个上限参数应该在系统规划的时候由用户定义,
        该参数只在接收消息的时候使用,
        该参数如果为0,则接收方应该禁用FC.Wait,即不发送FS = WT的流控帧。
我实现的时候,默认了该参数为0,实际是根本没定义该参数,也不使用FC = WT的流控帧,

6.7 Network layer timing

6.7.1 Timing parameters

    Table 16 定义了网络层的时间参数值,以及各个时间参数的开始和结束点,这些体现通信性能的值,通信双方都应该满足,每个程序都可以定义具体的值,但是要在Table 16的范围内。(实际上,车厂会给一个文档,叫做诊断规范,会规定这些参数的值)
    通常,将超时值定义为高于性能要求的值,以确保系统能在特殊情况下工作。指定的超时值应被视为任何给定实现的下限。真正的超时值应不晚于指定的超时值 + 50%。(这是一堆废话,按照车厂的诊断规范确定超时值)
基于CAN总线的汽车诊断协议UDS (网络层 ISO 15765)_第3张图片


6.7.2 Network layer timeouts

Table 17 定义了网络层定时器超时产生原因和超时后的处理行为
基于CAN总线的汽车诊断协议UDS (网络层 ISO 15765)_第4张图片


    (1)N_As和N_Ar
    N_As和N_Ar可以认为是同一个timer,是发送者本地的定时器,从网络层发出request(网络层调用CAN消息发送函数)开始,到网络层收到confirm(CAN消息发送成功或失败)结束,如果超时就丢弃消息,并调用N_USData.confirm 服务,with N_TIMEOUT_A。
    N_Ar超时的Action描述应该有误,我认为应该跟N_As一样,然而我并没有实现这两个timer,这两个timer是为了规避本地CAN消息阻塞而引入的。如果系统中不会出现阻塞或者出现阻塞也不会影响到后续消息发送,则不需要实现。
    (2)N_Bs
    N_Bs是发送者用来监控对端的定时器,如Table 16 中的描述,“Time until reception of the next FlowControl N_PDU. ” ,N_Bs是指到接收到下一个FC的最大时间,具体实现方法如下:
    发送者发送完FF或者BS(发送者每次连续发送的CF个数)个CF后,启动定时器TIMER_N_BS,如果定时器超时,则应该丢弃目前正在发送的消息,并且调用N_USData.confirm 服务,with N_TIMEOUT_Bs。如果发送者收到了FC,则应该检查TIMER_N_BS定时器是否超时,如果没有超时,则关闭该定时器,继续处理FC N_PDU,然后如果该FC携带的FS = WT,则应该重新启动TIMER_N_BS,继续等待下一个FC;如果发现TIMER_N_BS已经超时,则应该丢弃该FC。
    (3)N_Br
    N_Br是接收者本地的时间参数,如Table 16中描述“Time until transmission of the next FlowControl N_PDU ”,B_Br是指到发送下一个FC的时间。后面的Start说明,我认为有问题,Start中L_Data.indication (FF) 是指接收者收到FF(首帧),是没问题的,这一条是为了保证接收者接收到FF后要尽快发送FC;Start中的L_Data.confirm (FC) 是指接收者成功发送FC,这就没道理了,首先下一个FC的发送时间不应该参考上一个FC,即便是参考上一个FC,时间参数也不该跟接收到FF之后发送FC的时间参数相同。(有点绕口)。实际代码中,我没有显式的实现这一个参数,但是却满足该参数的要求,因为我收到FF之后没有做特殊延时,立即回复FC,另一个情况是收到BS个CF之后,也是立即回复FC。
    (4)N_Cs
    N_Cs是发送者本地的时间参数,如Table 16中描述“Time until transmission of the next ConsecutiveFrame N_PDU L”,N_Cs是指从收到FC或者发送完一个CF后,到发送下一个CF的时间,同样,实际代码中,我没有显式的实现这一参数,但是却满足该参数的要求,我收到FC之后立即发送一个CF,而发送完成一个CF之后,我等待STmin时间后立即发送下一个CF,并未做其他特殊延时。
    (5)N_Cr
    N_Cr是接收者用来监控对端的定时器,如Table 16中描述“Time until reception of the next ConsecutiveFrame N_PDU ”,N_Cr是指到接收到下一个CF的最大时间,Table 16中描述的Start是发送完FC或者接收到CF,然而我在实现时稍微变通了一下,Start变成了接收到FF或者CF后(因为收到FF后会立即发送FC,收到BS个CF后也会立即发送FC,所以不如直接在每次收到FF或者CF后启动TIMER_N_CR),具体实现方法如下:
    接收者收到一个FF或者CF之后,启动定时器TIMER_N_CR,如果定时器超时,则应该丢弃目前正在接收的消息,并且调用N_USData.confirm 服务with N_TIMEOUT_Cr。如果接收者收到一个CF,则应该检查TIMER_N_CR是否超时,如果没有超时,则应该关闭定时器,继续处理CF N_PDU;如果发现TIMER_N_CR已经超时,则应该丢弃该CF。
    (6)STmin
    STmin没有在Table 16和Table 17中描述,但它却是协议栈需要实现的一个定时器,不同于前面几个定时参数限制最大时间,这个定时器是限制最小时间间隔的,当发送完一个CF后,发送者需要延时STmin才能发送下一帧CF,具体实现方式如下:
    发送者发送完一个CF之后,如果连续发送的CF数量没有达到BS个,则立即启动定时器TIMER_STmin,在网络层主循环中运行TIMER_STmin,当定时器超时后,立即发送下一个CF。前面介绍流控帧时也有说明。

6.7.3 Unexpected arrival of N_PDU

    6.7.3小节介绍意外的数据单元
    如果通信的一方收到了不符合正常顺序的数据单元,就叫做unexpected N_PDU。unexpected N_PDU 可能是SF,FF,CF,FC或者不被本文档识别的类型。网络层的设计决定其支持全双工或者半双工通信,unexpected N_PDU的判断跟全双工和半双工有关,两者不同。
半双工是指:同一时间,两个节点之间只允许一个方向进行通信,(A向B发送SF,FF,CF,B向A回复FC,这叫做一个方向)
全双工是指:同一时间,两个节点之间能允许两个方向进行通信,

In addition to the network layer design decision, the possibility has to be considered that a reception or transmission from/to a node with the same address information (N_AI), as contained in the received unexpected N_PDU, is in progress.
英语太渣,这句话理解不了。

普遍情况下,除了SF和包含物理地址的FF,收到的其他unexpected N_PDU都应该被忽略;包含功能寻址的FF也应该被忽略。忽略的意思是指网络层不用告知上层它收到了这个N_PDU。
Table 18 描述了网络层在收到unexpected N_PDU时的行为,跟网络层当前的内部状态(NWL status)和全双工/半双工有关。并且默认收到的unexpected N_PDU的地址信息跟正常接收和发送的地址信息一致。
基于CAN总线的汽车诊断协议UDS (网络层 ISO 15765)_第5张图片

我开发的系统是属于半双工的,按照半双工来解读。
Idle是指空闲状态,起始默认状态,此时如果收到SF或者FF,都当作是新的接收时序的开始,收到其他的类型的N_PDU都应该忽略;
在接收状态下(Segmented Receive in progress),如果收到了SF或者FF,都当作新的接收时序的开始,并且要调用N_USData.indication服务通知上层,with N_UNEXP_PDU,
在接收状态下,如果正在等待CF的情况下收到了CF,则按正常CF处理,如果当前没有等待CF,则应该忽略这条CF。
在接收状态下,如果收到了FC,半双工系统应直接忽略。
在接收状态下,如果收到不识别的N_PDU,应直接忽略。
可以看出来,表格并未说明在接收状态下是否允许发送,我觉得发送应该有更高优先级,所以我在开发网络层的时候,任何时候都能发送,并且如果是多帧传输,立即把NWL status置为发送状态。所以关于发送,我的详细实现方式如下:
在任何状态下,如果上层请求发送数据,则立即进行发送,并丢弃当前正在发送的数据(但是不丢弃接收),如果请求发送是多帧传输,则把发送状态置为发送。
在发送状态下,(Segmented Transmit in progress),收到SF,FF,CF都应该忽略,
在发送状态下,收到FC,如果当前正在等待FC,则正常处理该FC,否则忽略,
在发送状态下,如果收到不识别的N_PDU,应直接忽略。

这一部分功能我在网络层接收数据的入口进行处理,变量nwl_st指示当前网络层状态。

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);
}


6.7.4 Wait frame error handling 

    6.7.4小节介绍,如果接收者连续发送等待流控帧(FC N_PDU WT)到最大次数,并且仍然不能正常接收,这时候接收者应该丢弃已经收到的消息,并且调用
 N_USData.indication 服务,with  N_WFT_OVRN ,告知上层。
    这一功能我并没有实现,因为我开发的网络层不使用(FC N_PDU WT),任何情况下都能正常接收。

6.8 Interleaving of messages

    6.8小节的内容是,网络层应该有并行传输不同地址的诊断消息的能力,
    这一功能我也没有实现,现状是我们的系统诊断地址都是固定的,由车厂分配,可能这一功能对于网关是有必要的,这里就不再赘述。

7 Data link layer usage

    第7节讲的是链路层的设计,以及扩展地址情况下,数据单元的格式,
    我们普遍使用的都是普通地址,这一部分也不再详细介绍了。

你可能感兴趣的:(ISO15765)