自定义通讯协议(软件层以USART为例)

一、使用场景及平台

1.使用场景

本文章旨在讲述设备与设备之间的通信,适合在两个设备在运行时主机需要控从机或是获取从机运行数据的场景,接下来会以USART为例(软件层),通信方式也可以采用IIC、SPI的通信方式进行数据传输。

2.使用平台

本文章使用的平台为国民技术N32G031系列为平台进行讲解和演示,同时若看到有错误或有问题的地方还请多多指教,一起学习一起进步。

二、指令格式和CRC校验

1.指令格式

数据在发送时需要遵循通信双方一致的指令格式,不然就牛头不对马嘴数据永远传送不成功,本协议采用的格式为:帧头+帧长+指令码+参数+校验码。其中帧头占用两个字节、帧长度占用两个字节、指令码占用一个字节、参数也就是数据占用不定长(根据自己需要进行长短设定)、校验码占用两个字节。数据格式为大端模式;帧长度包括(通俗讲就是从指令码到校验码的长度,包括指令码和校验码):指令码+参数+校验码;校验码:指令码+参数。这些帧头、帧长、指令等都是可以进行修改的,只要是通信双方使用的协议一样即可,若过一样就可以想象成IIC接口接上SPI接口来传输数据。举例:

帧头1 帧头2      帧长度   指令码              参数                     校验码

0xCC, 0x34, 0x00, 0x07, 0x700x01, 0x10, 0x02, 0x00, 0x80, 0x03

帧头

Header

数据

校验码

CRC

帧长

指令码

参数

0xCC34

Length

Cmd_ID

Param

CRC

2Byte

2Byte

1Byte

不定长

2Byte

2.CRC校验

CRC校验也称为和校验,是最常用的通信校验方式之一,本文采取的校验方式也是CRC校验,在使用中会从指令码开始,两位两位相加(最后若是只剩一个字节就直接相加该字节数据)

/**
 * @brief  通信协议数据校验
 * @param *packet - 接收到的数据包.
 * @param sum - CRC校验结果.
 */
unsigned short calcCheckSum(uint8_t *packet) 
{
    int index = 4;		//注意,此处为4,前面有两个帧头和帧长
    int len = 0;
    unsigned short sum = 0;
    len = ((packet[2] << 8) | packet[3]) - 2;
    while(len > 1)
    {
        sum += ((uint8_t)packet[index] << 8) | (uint8_t)packet[index+1];
        sum = sum & 0xFFFF;
        len -= 2;
        index += 2;
    }
    if (len > 0)
        sum = sum ^ (uint8_t)((uint8_t)packet[index]);
    return sum;
}

 三、串口和DMA配置

1.串口配置

串口配置基本上都是大同小异,根据不同芯片平台来写对应的串口配置即可,需要注意的是要将串口中断配置为空闲中断、串口的DMA发送接收都要使能,其他都还是正安装自己使用芯片平台正常配置。

void N32G031_USART_Init(void)
{
    GPIO_InitType GPIO_InitStructure;
	USART_InitType USART_InitStructure;
	NVIC_InitType NVIC_InitStructure;

    RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
	RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_USART1, ENABLE);
	
	GPIO_InitStruct(&GPIO_InitStructure);   
	GPIO_InitStructure.Pin            = GPIO_PIN_6;    //TX
	GPIO_InitStructure.GPIO_Mode      = GPIO_MODE_AF_PP;
	GPIO_InitStructure.GPIO_Alternate = GPIO_AF4_USART1;
	GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);
    GPIO_InitStructure.Pin            = GPIO_PIN_7;		//RX
    GPIO_InitStructure.GPIO_Alternate = GPIO_AF4_USART1;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);
	
	USART_StructInit(&USART_InitStructure);
	USART_InitStructure.BaudRate = 115200; //配置比特率为115200
	USART_InitStructure.WordLength = USART_WL_8B; 
	USART_InitStructure.StopBits = USART_STPB_1; 
	USART_InitStructure.Parity = USART_PE_NO; 
	USART_InitStructure.HardwareFlowControl = USART_HFCTRL_NONE; 
	USART_InitStructure.Mode = USART_MODE_RX | USART_MODE_TX;
	USART_Init(USART1, &USART_InitStructure);
	USART_EnableDMA(USART1,USART_DMAREQ_RX |  USART_DMAREQ_TX,ENABLE);
	USART_ConfigInt(USART1, USART_INT_IDLEF, ENABLE);
	
	NVIC_InitStructure.NVIC_IRQChannel         = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	USART_Enable(USART1, ENABLE); //使能USART1
}

2.DMA配置

DMA的具体配置需要根据自己芯片平台来进行,比如相对应的USART对应的通道等。本文采用先将需要发送的数据加载进本地缓存也就是TxBuffer_fill[TxBuffer_Size_fill]数组,等待需要发送时,再将本地缓存数据读取到TxBuffer[TxBuffer_Size]中去,同样,接收之后也是先将数据读取到本地,在使用单独的数据时再进行对应的读取。

#define TxBuffer_Size 100
#define RxBuffer_Size 100
#define TxBuffer_Size_fill 100
#define RxBuffer_Size_fill 100

uint8_t TxBuffer[TxBuffer_Size]={0};	 //数据发送Buff
uint8_t TxBuffer_fill[TxBuffer_Size_fill]={0};//数据本地缓存区Buff
uint8_t RxBuffer[RxBuffer_Size]={0};     //数据接收Buff
uint8_t RxBuffer_fill[RxBuffer_Size_fill]={0};//数据本地缓存区Buff

void N32G031K8_USART_DMA_Init(void)
{

	DMA_InitType DMA_InitStructure;

	RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_DMA, ENABLE);
	
	DMA_DeInit(DMA_CH4);
	DMA_InitStructure.PeriphAddr     = (USART1_BASE + 0x04);
	DMA_InitStructure.MemAddr        = (uint32_t)TxBuffer;		//TX
	DMA_InitStructure.Direction      = DMA_DIR_PERIPH_DST;
	DMA_InitStructure.BufSize        = TxBuffer_Size;
	DMA_InitStructure.PeriphInc      = DMA_PERIPH_INC_DISABLE;
	DMA_InitStructure.DMA_MemoryInc  = DMA_MEM_INC_ENABLE;
	DMA_InitStructure.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
	DMA_InitStructure.MemDataSize    = DMA_MemoryDataSize_Byte;
	DMA_InitStructure.CircularMode   = DMA_MODE_NORMAL;
	DMA_InitStructure.Priority       = DMA_PRIORITY_VERY_HIGH;
	DMA_InitStructure.Mem2Mem        = DMA_M2M_DISABLE;
	DMA_Init(DMA_CH4, &DMA_InitStructure);
	DMA_RequestRemap(DMA_REMAP_USART1_TX, DMA, DMA_CH4, ENABLE);//发送完成之后中断
	DMA_SetCurrDataCounter(DMA_CH4, TxBuffer_Size);
	DMA_EnableChannel(DMA_CH4, ENABLE);
	
	DMA_DeInit(DMA_CH5);
	DMA_InitStructure.PeriphAddr = (USART1_BASE + 0x04);
	DMA_InitStructure.MemAddr    = (uint32_t)RxBuffer;			//RX
	DMA_InitStructure.Direction  = DMA_DIR_PERIPH_SRC;
	DMA_InitStructure.BufSize    = RxBuffer_Size;
	DMA_Init(DMA_CH5, &DMA_InitStructure);
	DMA_RequestRemap(DMA_REMAP_USART1_RX, DMA, DMA_CH5, ENABLE);//接收完成之后中断
	DMA_SetCurrDataCounter(DMA_CH5, RxBuffer_Size);
	DMA_EnableChannel(DMA_CH5, ENABLE);
}	

四、接收处理

当设备接收到数据之后,DMA会进行数据读取,当一整串数据读取结束之后就会产生串口中断,并在串口中断里将数据读取到解析函数。并且需要注意的是在开始解析接收到的数据时,需要暂时关闭DMA接收通道,防止数据被覆盖,并在设置好下次需要接收的字节之后再次开启DMA使能

/**
 * @brief  串口 1 中断服务函数.
 * @param None
 * @return None
 * @note None
 */
void USART1_IRQHandler(void)
{		
	u8 RX_len=0;
	if (USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET)
	{
		USART1->STS;
		USART1->DAT;
		DMA_EnableChannel(DMA_CH5,DISABLE); //关闭DMA串口接收传输通道5
		RX_len = RxBuffer_Size - DMA_GetCurrDataCounter(DMA_CH5);//本次接收数据长度总-(总-用)=用
		for (uint8_t i = 0; i < RX_len; i++) {
			Receive(RxBuffer[i]); //解析串口发送来的数据
		}
		DMA_ClrIntPendingBit(DMA_INT_TXC5,DMA); //清除DMA中断标志位
		DMA_SetCurrDataCounter(DMA_CH5,RxBuffer_Size);  //设置下次需要接收的字节长度
		DMA_EnableChannel(DMA_CH5,ENABLE);
		
  }
}

接下来是Receive()函数,当串口中断之后,通过一个for循环将DMA接收到RxBuffer数组的数据,调用Receive函数来解析接收到的数据。在接收数据时采用的状态机的方式进行解析,通过固定的帧头之后,再接收需要接收的数据长度,当接收到发来数据长度的长度之后,就会进行整包数据校验,若出现缺帧或者是校验和发送时的校验不正确就会将这个数据包丢弃,不再进行数据分析。

/**
 * @brief 接收状态机
 * @param bytedata - 接收数据状态机
 * @param None
 */
void Receive(uint8_t bytedata)
{
	static uint8_t step=0;//状态变量初始化为0 在函数中必须为静态变量
	static uint8_t cnt=0,Buf[300],len,cmd,*data_ptr;
	static uint16_t crc16;
	//进行数据解析 状态机
	switch(step)
	{
	    case 0://接收帧头1状态
	        if(bytedata== 0xCC)
	        {
	          step++;
	          cnt = 0;
	          Buf[cnt++] = bytedata;
	        }break;
	    case 1://接收帧头2状态
	        if(bytedata== 0x34)
	        {
	          step++;
	          Buf[cnt++] = bytedata;
	        }
	        else if(bytedata== 0xCC) //第二帧若是第一帧数据再接收一次,否则重新接收
			  step = 1;
	        else
			  step = 0;
	        break;
	    case 2://接收数据高八位字节状态
	        step++;
	        Buf[cnt++] = bytedata;
	        break;
		case 3://接收数据低八位字节状态
			step++;
			Buf[cnt++] = bytedata;
			len = bytedata;
			break;
	    case 4://接收命令字节状态
	        step++;
	        Buf[cnt++] = bytedata;
	        cmd = bytedata;
	        data_ptr = &Buf[cnt];//记录数据指针首地址
	        if(len == 0)step++;//数据字节长度为0则跳过数据接收状态
	        break;
	    case 5://接收len字节数据状态
	        Buf[cnt++] = bytedata;
	        if(data_ptr + (len-3) == &Buf[cnt])//利用指针地址偏移判断是否接收完len位数据
			{
			  step++;
			}
	        break;
	    case 6://接收crc16校验高8位字节
	        step++;
	        crc16 = bytedata;
	        break;
	    case 7://接收crc16校验低8位字节
	        crc16 <<= 8;
	        crc16 += bytedata;
	        if(crc16 == calcCheckSum(Buf))//校验正确进入下一状态
	        {
			  Data_Analysis(cmd,data_ptr,len);//数据解析
			  step = 0;
	        }
	        else if(bytedata == 0xcc)
	          step = 1;
	        else{
	          step = 0;
			  printf("Error Data, correct Data is:%04x\r\n",crc16);}
	        break;
	    default:step=0;break;//多余状态,正常情况下不可能出现
	}
}

当整包数据都正确没有问题之后,就对接收到的整包数据进行解析,这时可以根据不同的指令码进行区分,若只是传输一种形式的数据,那么可以自己修改协议将指令码字节删除。这里我做的是在接收到数据之后,将数据全部读入到接收本地缓存当中,当要使用数据时再读取本地缓存来进行使用

/**
 * @brief  接收数据解包
 * @param cmd - 解析数据包的指令 *datas - 解析数据包的数据. len - 解析数据包的长度
 * @param None
 */
void Data_Analysis(uint8_t cmd,const uint8_t *datas,uint8_t len)
{
	switch(cmd)
	{
		case 0x01:
			break;
		case 0x02:
			break;
		case 0x70://接收到的指令码
			for(uint8_t i=0;i<=8;i++){
			RxBuffer_fill[i]=datas[i];
			break;
		case 0x71:
			break;
	}
	
}

五、发送处理

在发送时采用的也是DMA进行发送,本文章的发送是将需要发送的数据先读入到本地发送缓存当中,然后在定时发送时调用数据发送函数即可,也可自己设计当状态改变时再发送数据也行。在程序编写过程中,发现国民技术N32G031K8C6这款芯片是不能在TIM1、TIM3定时中断里发送,由于TIM8用于PWM输出就没测试,在用上述定时器中断来使用DMA发送串口数据时,会出现死机的问题,会一直卡在发送完成标志位那里(发送完成之后标志位不能置一),只能使用LPTIM6中断发送才能发送成功,并且还会偶尔发送失败,同平台的需要稍微注意一下,如果是软件配置问题,还请指出呢。USART_Send_Updata()函数可根据自己工程需要来修改配置,目的是定时更新需要发送的数据,可有可无。

/**
  * @brief 更新串口发送数据值
  * @param None
  * @retval None
  */
void USART_Send_Updata(void)
{
		TxBuffer_fill[0]=0x01;		//设备在线
		TxBuffer_fill[1]=Get_data();				
		TxBuffer_fill[2]=Get_data();				
		TxBuffer_fill[3]=Get_data();						
		TxBuffer_fill[4]=Get_data();			
		TxBuffer_fill[5]=Get_data();					
		TxBuffer_fill[6]=Get_data();	
		TxBuffer_fill[7]=Get_data();								
		TxBuffer_fill[8]=Get_data();							
		TxBuffer_fill[9]=Get_data();				
		TxBuffer_fill[10]=Get_data();		
}

在数据发送之前是要将数据进行打包发送,要将帧头、帧长度、指令码和CRC校验一起写入数据包,这样才能是一个完成的数据包,Send_Cmd_Data()函数可以直接调用发送数据,它最后调用的函数是USART_DMA_Send(),USART_DMA_Send()函数是将打包好的数据使用DMA进行发送

/**
 * @brief  通信协议数据打包
 * @param *datas - 获取需要打包的数据. len - 需要打包数据的长度
 * @param None
 */
void Send_Cmd_Data(uint8_t cmd,const uint8_t *datas,uint16_t len)
{
    uint8_t buf[300],i,cnt=0;
    uint16_t crc16;
    buf[cnt++] = 0xCC; 
    buf[cnt++] = 0x34; 
    buf[cnt++] = len>>8; //高八位
		buf[cnt++] = len&0xFF;//低八位
    buf[cnt++] = cmd; 
    for(i=0;i<len-3;i++)  
    {
        buf[cnt++] = datas[i]; //将数据加载进协议包里
    }
    crc16 = calcCheckSum(buf); //+1意为在数据校验时将指令加入
    buf[cnt++] = crc16>>8;
    buf[cnt++] = crc16&0xFF;
    USART_DMA_Send(buf,cnt);//调用数据帧发送函数将打包好的数据帧发送出去
}

/**
 * @brief  串口 1 DMA 发送.
 * @param *data - 要发送的数据首地址.
 * @param len - 要发送的数据长度.
 */
void USART_DMA_Send(uint8_t *data, uint16_t len)
{
    if (len > TxBuffer_Size) {
        SEGGER_RTT_printf(0, "DMA Channel 4 transfer data over maximum length!!!\r\n");
        return;
    }
    DMA_EnableChannel(DMA_CH4, DISABLE);
    memcpy(TxBuffer, data, len);
    DMA_SetCurrDataCounter(DMA_CH4, len);
    DMA_EnableChannel(DMA_CH4, ENABLE);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXDE) != RESET)
		{
			printf("DMA Send Data NO Completion\r\n");
		}	
}

  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自定义通信协议可以根据实际需求进行设计,一般包括以下几个方面: 1. 帧头和帧尾:定义起始和结束位置,一般使用特定的字符或字符组合,例如0xAA或0xFFFF。 2. 数据长度:定义数据区域的长度,可以根据实际需求进行设定。 3. 命令字:定义具体的命令,例如读取数据、设置参数等。 4. 校验和:根据数据区域的内容计算出来的校验和,用于检测数据的完整性和正确性。 下面是一个简单的自定义通信协议示例,用于实现单片机与PC机之间的串口通信: ```c #include <reg52.h> #define FREQ_OSC 11059200ul #define FRAME_HEAD 0xAA #define FRAME_TAIL 0xFF #define CMD_READ 0x01 #define CMD_WRITE 0x02 typedef struct { unsigned char head; // 帧头 unsigned char len; // 数据区长度 unsigned char cmd; // 命令字 unsigned char data[10]; // 数据区 unsigned char sum; // 校验和 unsigned char tail; // 帧尾 } FRAME_T; sbit led = P1^0; void delay_ms(unsigned int ms) { unsigned int i,j; for(i=0;i<ms;i++) for(j=0;j<114;j++); } void init_serial() { TMOD = 0x20; // 设置定时器1为模式2 TH1 = 0xFD; // 波特率为9600,计算得到TH1的值 SCON = 0x50; // 串口工作在模式1 TR1 = 1; // 启动定时器1 } void putchar_serial(char ch) { SBUF = ch; while(!TI); TI = 0; } char getchar_serial() { char ch; while(!RI); ch = SBUF; RI = 0; return ch; } unsigned char calc_sum(FRAME_T *frame) { unsigned char i, sum = 0; for(i=0;i<frame->len;i++) sum += frame->data[i]; return sum; } void send_frame(FRAME_T *frame) { unsigned char i; frame->sum = calc_sum(frame); putchar_serial(frame->head); putchar_serial(frame->len); putchar_serial(frame->cmd); for(i=0;i<frame->len;i++) putchar_serial(frame->data[i]); putchar_serial(frame->sum); putchar_serial(frame->tail); } void main() { FRAME_T frame; unsigned char ch; unsigned int i; init_serial(); while(1) { if(RI) { ch = getchar_serial(); if(ch == FRAME_HEAD) { frame.head = ch; frame.len = getchar_serial(); frame.cmd = getchar_serial(); for(i=0;i<frame.len;i++) frame.data[i] = getchar_serial(); frame.sum = getchar_serial(); frame.tail = getchar_serial(); if(frame.tail == FRAME_TAIL && frame.sum == calc_sum(&frame)) { if(frame.cmd == CMD_READ) { // 读取数据 } else if(frame.cmd == CMD_WRITE) { // 写入数据 if(frame.data[0] == 0x01) led = 1; else if(frame.data[0] == 0x00) led = 0; } } } } delay_ms(100); } } ``` 上面的示例代码中,定义了一个简单的自定义通信协议,包括帧头、数据长度、命令字、数据区和校验和等字段。主函数中通过不停的循环来检测是否有串口数据到来,如果有则读取数据并进行处理。当接收到帧头时,依次读取数据并进行校验,如果校验通过则根据命令字进行相应的处理。在本例中,当接收到命令字为0x02时,根据数据区中的内容来控制LED灯的开关状态。同时,为了避免串口数据读写时出现干扰,还需要添加适当的延时函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值