前言
在介绍UDS服务之前,首先需要介绍一下网络层(传输层TP层),这是UDS与控制器之间的中间层,否则控制器收到的就是普通的CAN报文,无法实现对应的诊断服务,为什么需要TP层呢?因为有些数据不是一帧报文就能发送完的,需要发送很多帧,网络层需要做的就是数据的解包、打包和传输。
一、网络层(TP层)
1、网络层(TP层)作用
在汽车诊断协议中,UDS(Unified Diagnostic Services,统一诊断服务)基于ISO 14229标准,其协议栈遵循OSI模型。其中,网络层(Network Layer),也称为传输协议层(Transport Protocol Layer,TP层),主要负责将应用层(如UDS服务)的长消息拆分成适合底层总线(如CAN、CAN FD、FlexRay等)传输的帧,并在接收端重新组装。网络层的核心标准是 ISO 15765-2(适用于CAN总线)。
当应用层消息长度超过底层总线单帧容量(如CAN帧的8字节)时,网络层将消息拆分为多个帧发送,接收端重新组合为完整消息,例如:UDS的`0x36服务传输数据时,数据可能长达数KB,需分多帧传输;它还可以用于流控制,管理发送端与接收端的数据传输速率,避免接收缓冲区溢出。这里只列举了简单的功能,有关网络层的详细内容,还请阅读ISO 15765-2协议。
2、网络层帧类型
在UDS的网络协议控制中一共有4种类型的帧,分别为:单帧,首帧,续帧,流控帧。它们通过通信帧的第一字节的高4位来区分。
3、网络层长消息传输流程
以34服务为例,34服务需要传输块大小和块长度,一般情况下会超过7个字节,因此需要多帧传输。
a. 发送首帧(FF)
发送端发送首帧(FF),包含消息总长度。例如:10 0b 34 00 44 08 00 50,其中第一个字节10的高4位为1,表示这是首帧;数据长度是第一个字节的低4位和第三个字节组成,在这里值是0x00b,11个字节。
b. 接收端回复流控帧(FC)
接收端回复流控帧(FC),指定传输参数,包括:
流控状态(FS):0x0(继续发送)、0x1(等待)、0x2(错误);
块大小(BS):发送端每次连续发送的帧数(如BS=0表示无限制);
帧间隔时间(STmin):连续帧之间的最小时间间隔(单位ms)。例如30 00 00 CC CC CC CC CC,表示接收端允许发送端继续发送,块大小不限制,帧间隔时间不限制。
c. 发送端发送连续帧(CF)
发送端按流控帧指定的参数发送连续帧(CF),每帧包含顺序编号和数据块。顺序号:从0x01开始递增,循环范围(0x00-0x0F)。例如:21 00 00 00 37 60 AA AA,其中21表示多帧的第一帧,后面如果还有内容,第一个字节的低4位会继续递增,21,22,23,24,~2F。
d. 循环直至传输完成,重复步骤b和c,直至所有数据发送完毕。
注:上面图中的各个时间参数可以阅读ISO15765-2,后面我们在升级如果由于时间参数的问题导致bug,再回头来仔细分析。
帧类型格式示例
a. 单帧(SF),以CAN为例
02 10 01 00 00 00 00 00,02的高4位为0,表示单帧,10表示会话服务,子服务是01,意思就是请求默认会话。
b.多帧(FF+FC)
10 0b 34 00 44 08 00 50即表示首帧,首帧的前两个字节,byte0的高4位是1。
byte0的低4位和byte2组合是数据长度,这里我们可以看到最大的长度是0xfff(4095),这也是为什么基于CAN的UDS最大允许传输长度是4095个字节;
34表示请求传输服务:
30 00 00 CC CC CC CC CC表示接收端发送来的流控帧。
30表示该帧是流控帧,第1个字节30的低4位为0,表示FS为0,继续发送后续帧;
第2个字节00为BS = 0,块大小值,控制连续帧的连续传输的个数。例如:当BS设置成8时,表示接收流控帧之后,当发送完8个连续帧就需要再次接受流控帧。当值为0时,表示后续连续帧的发送不受流控帧的控制。
第3个字节00表示Stmin连续帧之间最小时间间隔不限制,如果要实现间隔是10ms,则第3个字节应为0A。
c. 连续帧(CF)
21 00 00 00 37 60 AA AA表示该帧是连续帧,连续帧与首帧组合起来表示34服务要传输的内容为:这一个block要传输的块的内存地址是0x08005000,该块的长度是0x00003760(14176)。
二、代码实现
1.CAN接口
网络层与CAN接口通过全局变量UDS_Data来实现,HAL_CAN_RxFifo0MsgPendingCallback函数是HAL库自带的CAN接收中断函数,每次收到CAN数据都会进入中断。并且我们测试过,通过中断接收不会导致丢帧问题。
代码如下:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan1)
{
CAN_RxHeaderTypeDef rxHeader;
uint8_t Rec_Data[8];
uint8_t i;
// 从FIFO0读取数据
if (HAL_CAN_GetRxMessage(hcan1, CAN_RX_FIFO0, &rxHeader, Rec_Data) == HAL_OK)
{
// 处理接收到的数据
if (rxHeader.StdId == PhyCANID_Rx)
{
// 解析rxData数组中的数据
for(i = 0;i < 8;i++)
{
UDS_Data[i] = Rec_Data[i]; /*将接收到的数据赋给全局变量UDS_Data*/
}
}
}
}
2.首帧(FF)数据处理
这里每次处理完数据都会把UDS_Data数据clear掉,因为我将处理函数放在了while(1)循环内,会不断监控这个全局变量,如果不清空,如果后面没有数据会导致一直卡在一个分支。
代码如下:
typedef struct /*定义buffer存储结构体,包括数据,长度,有无错误等*/
{
uint8_t Data[UDS_MAX_DATA_LEN];
uint16_t Length;
uint8_t N_PCI;
uint8_t N_Result;
bool RecvComplete;
bool MFTxEnFlg;
uint8_t MFTxLength;
uint8_t *MFTxDataBuffPtr;
uint8_t NTX_PCI;
}N_UDSData;
下面展示一些 内联代码片
。
N_PCI = (UDS_Data[0] >> 4);/*byte0的高4位,bit4~bit7*/
DataPtr->RecvComplete = FALSE;/*接收完成标志位置false*/
case PCI_FF: /*10 0b 34 00 44 08 00 50*/
DataLen = (uint16_t)(((UDS_Data[0] & 0x0F) << 8) | UDS_Data[1]);/*多帧的长度*/
if (DataLen > UDS_MAX_DATA_LEN)/*大于4095,报错*/
{
DataPtr->N_Result = N_ERROR;
}
else if(DataLen <= 7)/*小于等于7,报错,因为如果小于等于7个字节,没必要通过多帧传输*/
{
DataPtr->N_Result = N_ERROR;
}
else
{
/*长度正确,将UDS_Data拷贝至一个N_UDSData类型的Data buffer,这个buffer长度是4095,所有的UDS处理数据都通过这个buffer*/
DataPtr->Length = DataLen;
Rx_Index = 0; /*清空缓冲区*/
DataPtr->Data[Rx_Index++] = UDS_Data[2];
DataPtr->Data[Rx_Index++] = UDS_Data[3];
DataPtr->Data[Rx_Index++] = UDS_Data[4];
DataPtr->Data[Rx_Index++] = UDS_Data[5];
DataPtr->Data[Rx_Index++] = UDS_Data[6];
DataPtr->Data[Rx_Index++] = UDS_Data[7];
DataPtr->N_PCI = PCI_SF;
DataPtr->N_Result = N_OK;
DataPtr->RecvComplete = TRUE;
FF_RecFlag = 1;/*接收到了首帧*/
DataLen = DataLen - 6;/*长度减去6,因为首帧数据只有后面6个字节是数据*/
DiagTimer_Set(&ReciveTimerCr, N_Cr);/*开启Cr定时器*/
UDS_FC_TxData();/*自动回复流控帧*/
for(i = 0;i < 8;i++)
{
UDS_Data[i] = 0x00;/*clear 临时buffer*/
}
}
break;
2.连续帧(CF)数据处理
每次收到连续帧,都要把N_Cr定时器打开,然后这个定时器会不断地被更新覆盖,因为ReciveTimerCr1是一个全局变量。
case PCI_CF: /*接收连续帧,21 00 00 00 37 60 AA AA*/
DiagTimer_Set(&ReciveTimerCr1, N_Cr);/*开启Cr定时器*/
if(FF_RecFlag == 1)
{
if(DataLen > 7)
{
/*数据长度大于一帧的长度*/
DataPtr->Data[Rx_Index++] = UDS_Data[1];
DataPtr->Data[Rx_Index++] = UDS_Data[2];
DataPtr->Data[Rx_Index++] = UDS_Data[3];
DataPtr->Data[Rx_Index++] = UDS_Data[4];
DataPtr->Data[Rx_Index++] = UDS_Data[5];
DataPtr->Data[Rx_Index++] = UDS_Data[6];
DataPtr->Data[Rx_Index++] = UDS_Data[7];
DataLen = DataLen - 7;/* 成功接收了7个长度数据 */
}
else
{
/* 剩余长度最后一帧 */
for(i = 0;i < DataLen;i++)
{
DataPtr->Data[Rx_Index++] = UDS_Data[i+1];
}
DataLen = 0;
DataPtr->RecvComplete = TRUE;
FF_RecFlag = 0; //本次发完,可以继续接受首帧了
}
for(i = 0;i < 8;i++)
{
UDS_Data[i] = 0x00;
}
DataPtr->N_PCI = PCI_CF;
DataPtr->N_Result = N_OK;
}
else
{
DataPtr->N_Result = N_ERROR;
}
break;
default:
break;
三、测试
测试首帧时,先把自动回复流控帧注释掉,单片机收到首帧不回复连续帧。上位机使用的是TSmaster,它可以自己创建Bootloader的升级上位机,后面会通过一篇内容进行介绍。
自动回复流控帧打开,单片机收到首帧回复连续帧,TSmaster收到流控帧会自动发送下一个连续帧。
四、总结
这一篇内容,主要讲解了UDS的网络层,以及代码如何实现首帧和连续帧,并且对功能进行了简单的测试。下一篇内容将先实现34服务,本着先实现再完美的原则,通过搭积木的方式实现UDS Bootloader的开发。
感兴趣的小伙伴可以加我微信公众号:行至汽车电子(XZJZLN),会及时推送文章