首先要了解LIN的协议,LIN是采用问询的方式进行通信的,一主多从,从机根据帧ID进行回应。
问询的帧头也是规定的。帧头包括同步间隔段、同步段以及PID(Protected Identifier,受保护ID)段,应答包括数据段和校验和段,其中值“0”为显性电平(Dominant),值“1”为隐性电平(Recessive)
这些都跟后续写代码有着密切联系,所以要先了解(文章最后附上源码,需要自己整理一下)。
同步间隔段:同步间隔段由同步间隔和同步间隔段间隔符构成,同步间隔是至少持续13位的显性电平,同步间隔段间隔符是至少持续1位的隐性电平。
这个代码实现就比较简单,HAL库有封装好的程序,直接调用就行。
//同步间隔段
void Lin_SendBreak(UART_HandleTypeDef *huart)
{
if(HAL_LIN_SendBreak(huart)!=HAL_OK)
{
printf("Lin_SendBreak Failed\r\n");
}
}
同步段:数据传输都是先发送LSB,最后发送MSB,这里确保所有从机节点都和主机节点波特率相同发送和接收数据,同步段固定一个字节,值固定为0x55。
代码实现就比较简单了,直接发送应该0X55就行。
unsigned char SyncSegmentData[1] = {0x55};
//同步段
void Lin_SendSyncSegment(UART_HandleTypeDef *huart)
{
if(HAL_UART_Transmit(huart,SyncSegmentData,1, 1000)!=HAL_OK)
{
printf("Lin1_SendSyncSegment Failed\r\n");
}
}
受保护ID段(PID):受保护ID由6位帧ID和两位奇偶校验组成,帧ID的范围在0x00~0x3F之间,共64个,奇偶校验位的校验公式如下:
校验公式如下,其中“⊕”代表“异或”运算, “¬”代表“取非”运算。
P0 = ID0⊕ID1⊕ID2⊕ID4
P1 = ¬(ID1⊕ID3⊕ID4⊕ID5)
代码实现需要对其进行运算。
//PID(protect ID),这里的前六位为ID,后两位为校验位
//函数功能为:输入ID,返回PID。
uint8_t Lin_CheckPID(uint8_t id)
{
uint8_t returnpid ;
uint8_t P0 ;
uint8_t P1 ;
P0 = (((id)^(id>>1)^(id>>2)^(id>>4))&0x01)<<6 ;
P1 = ((~((id>>1)^(id>>3)^(id>>4)^(id>>5)))&0x01)<<7 ;
returnpid = id|P0|P1 ;
return returnpid ;
}
数据段:节点发送的数据位于数据段,最高8个字节,先发送编号最低的DATA1,依次增加发送。
代码就是调用HAL库封装好的串口发送函数。
//LIN数据发送
void Lin_SentData(UART_HandleTypeDef *huart,uint8_t data[],uint8_t length)
{
HAL_UART_Transmit(huart,data,length,1000);
}
校验和段:就是对帧中所传输的内容进行校验。
这里由两种校验方式分为标准型校验和(Classic Checksum)及增强型校验和(Enhanced Checksum) 。
经典型校验算法涵盖的范围只有LIN的数据段,即一帧LIN信号的数据段包含几个字节的数据就计算几个字节,计算的时候是累加计算,因为校验字节只有一个字节,所以checksum的数值一定是小于255(0xFF),用一个公式来表示如下:
byte 0 + ... + byte n + checksum = 0xFF 的整数倍
即 checksum = 0xFF - 数据之和的低八位。
//输入ID+数据,返回校验和段(标准型)
uint8_t Lin_CheckSum_S(uint8_t id , uint8_t data[],uint8_t length)
{
uint8_t t ;
uint16_t sum ;
sum = data[0];
for(t=1;t<length;t++)
{
sum += data[t];
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
}
sum = ~sum;
return (uint8_t)sum ;
}
增强型校验相比于经典型校验最大的区别就是计算的范围变化了,增强型校验需要计算的范围是除了数据段还要加上ID的数值,变化一下计算公式如下:
ID + byte 0 + ... + byte n + checksum = 0xFF 的整数倍
除此之外,在计算上和经典型校验完全一样。
//输入ID+数据,返回校验和段(增强型)
uint8_t Lin_CheckSum_E(uint8_t id , uint8_t data[],uint8_t length)
{
uint8_t t ;
uint16_t sum ;
sum = data[0];
if(id == 0x3c)
{
for(t=1;t<length;t++)
{
sum += data[t];
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
}
sum = ~sum;
return (uint8_t)sum;
}
for(t=1;t<length;t++)
{
sum += data[t];
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
}
sum+=Lin_CheckPID(id);
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
sum = ~sum;
return (uint8_t)sum ;
}
至此,整个发送过程结束。
因为有帧头的原因,所以接收需要在接收中断做一些处理。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
uint8_t ReceiveData;
if (Run_Mode == LIN_MODE)
{
if (huart == &huart3)
{
HAL_UART_Receive_IT(&huart3, LIN1RxData, 1);
// printf("%02x ",LIN1RxData[0]);
ReceiveData = LIN1RxData[0];
//帧头判断
if (DataProcess1 == 0) //同步段间隔段
{
if (ReceiveData != 0x00)
return;
if (ReceiveData == 0x00)
{
DataProcess1 = 1;
return;
}
}
if (DataProcess1 == 1)//同步段
{
if (ReceiveData != 0x55)
return;
if (ReceiveData == 0x55)
{
DataProcess1 = 2;
return;
}
}
//帧ID处理
if (DataProcess1 == 2)
{
ReceivePID1 = ReceiveData;
ReceiveID1 = ReceivePID1 & 0x3f;
LIN1Buffer.id = ReceiveID1;
lin_time_flag1 = 1;
DataProcess1 = 3;
DtRProcess1 = 0;
return;
}
//数据段接收
if (DataProcess1 == 3)
{
LIN1Buffer.linDMA_rxBuf[DtRProcess1++] = ReceiveData;
}
}
}
程序按照过程式接收,相应的数据正确才往下一步走。
if (lin_time_flag1)
{
lin_time_flag1 = 0;
DtRProcess1 = 0;
DataProcess1 = 0;
}
if (lin1_s_sendFlag == 1)
{
if(LIN1Buffer.id == 0x01)
{
Lin1_SentData(LIN1TxData, 8);
Lin1_SendCheckSum(0x01, LIN1TxData, 8);
}
lin1_s_sendFlag = 0;
}
从机主函数进行标志位清零和数据发送。
最后附上LIN.C和LIN.H;
LIN.C
/*
* lin.c
*
* Created on: Apr 19, 2023
* Author: Administrator
*/
#include "lin.h"
#include "usart.h"
#include "stdio.h"
unsigned char SyncSegmentData[1] = {0x55};
unsigned char CheckPIDData[1] = {0x00};
unsigned char CheckSumData[1] = {0x00};
unsigned char LIN1useFlag = 0,LIN2useFlag = 0;
//同步间隔段
void Lin_SendBreak(UART_HandleTypeDef *huart)
{
if(HAL_LIN_SendBreak(huart)!=HAL_OK)
{
printf("Lin_SendBreak Failed\r\n");
}
}
//同步段
void Lin_SendSyncSegment(UART_HandleTypeDef *huart)
{
if(HAL_UART_Transmit(huart,SyncSegmentData,1, 1000)!=HAL_OK)
{
printf("Lin1_SendSyncSegment Failed\r\n");
}
}
//PID(protect ID),这里的前六位为ID,后两位为校验位
//函数功能为:输入ID,返回PID。
uint8_t Lin_CheckPID(uint8_t id)
{
uint8_t returnpid ;
uint8_t P0 ;
uint8_t P1 ;
P0 = (((id)^(id>>1)^(id>>2)^(id>>4))&0x01)<<6 ;
P1 = ((~((id>>1)^(id>>3)^(id>>4)^(id>>5)))&0x01)<<7 ;
returnpid = id|P0|P1 ;
return returnpid ;
}
//主机帧头
void Lin_SendHead(UART_HandleTypeDef *huart,uint8_t id)
{
Lin_SendBreak(huart);
Lin_SendSyncSegment(huart);
CheckPIDData[0] = Lin_CheckPID(id);
if(HAL_UART_Transmit(huart,CheckPIDData,1, 1000)!=HAL_OK)
{
printf("Lin1_SendHead Failed\r\n");
}
}
//输入ID+数据,返回校验和段(标准型)
uint8_t Lin_CheckSum_S(uint8_t id , uint8_t data[],uint8_t length)
{
uint8_t t ;
uint16_t sum ;
sum = data[0];
for(t=1;t<length;t++)
{
sum += data[t];
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
}
sum = ~sum;
return (uint8_t)sum ;
}
//输入ID+数据,返回校验和段(增强型)
uint8_t Lin_CheckSum_E(uint8_t id , uint8_t data[],uint8_t length)
{
uint8_t t ;
uint16_t sum ;
sum = data[0];
if(id == 0x3c)
{
for(t=1;t<length;t++)
{
sum += data[t];
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
}
sum = ~sum;
return (uint8_t)sum;
}
for(t=1;t<length;t++)
{
sum += data[t];
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
}
sum+=Lin_CheckPID(id);
if(sum&0xff00)
{
sum&=0x00ff;
sum+=1;
}
sum = ~sum;
return (uint8_t)sum ;
}
//LIN数据发送
void Lin_SentData(UART_HandleTypeDef *huart,uint8_t data[],uint8_t length)
{
HAL_UART_Transmit(huart,data,length,1000);
}
//LIN数据校验和
void Lin_SendCheckSum(UART_HandleTypeDef *huart,uint8_t id ,uint8_t data[],uint8_t length)
{
CheckSumData[0] = Lin_CheckSum_S(id,data,length);
HAL_UART_Transmit(huart,CheckSumData,1, 1000);
}
void linAskCmd(UART_HandleTypeDef *huart,uint8_t id)
{
Lin_SendHead(huart,id);
}
LIN.H
/*
* lin.h
*
* Created on: Apr 19, 2023
* Author: Administrator
*/
#ifndef INC_LIN_H_
#define INC_LIN_H_
#include "stm32g4xx_hal.h"
#include "main.h"
#define LIN_DATA_LEN 1200//每一帧最多传的字节数,包括CRC,必须为2的整数倍
extern unsigned char SyncSegmentData[1];
extern unsigned char CheckPIDData[1];
extern unsigned char CheckSumData[1];
typedef struct
{
uint8_t receive_flag:1;//空闲接收完成标志
uint8_t id;//接收id
uint8_t rx_len;//接收长度
uint8_t linDMA_rxBuf[LIN_DATA_LEN];//DMA接收缓存
}LINRXBuffer;
void Lin_SendHead(UART_HandleTypeDef *huart,uint8_t id);
void Lin_SentData(UART_HandleTypeDef *huart,uint8_t data[],uint8_t length);
void Lin_SendCheckSum(UART_HandleTypeDef *huart,uint8_t id ,uint8_t data[],uint8_t length);
uint8_t Lin_CheckSum_S(uint8_t id , uint8_t data[],uint8_t length);
uint8_t Lin_CheckSum_E(uint8_t id , uint8_t data[],uint8_t length);
void linAskCmd(UART_HandleTypeDef *huart,uint8_t id);
#endif /* INC_LIN_H_ */
最后再附上几点建议(自己踩的坑):
- LIN的波特率最高20k,波特率不要选到115200了,不然全是错误帧,发不出去,推荐9600.
- 开了看门狗的一定要在中断喂狗,不然很容易被其他中断打断,导致不停重启。
- 主函数不要加延迟函数,最好利用定时器定时。不然很容易出现主机问了三四帧,从机才回了一帧,因为从机是通过标志位回复主机的,如果有延迟函数的话,主函数就不会一直去判断标志位,就会导致少回了。