通俗易懂Xmodem协议

在工作中写Bootloader时,需要串口传输代码数据,为了保证传输数据时不丢帧,需要用到通信协议,我选择的是Xmodem协议。

文章目录

      • 1. 定义
      • 2. 控制字符
      • 3. 帧数据格式
      • 4. 数据包说明
      • 5. 启动传输
      • 6. 传输过程
      • 7. 代码
        • 7.1 Xmodem接收数据代码
        • 7.2 CRC16位校验代码

1. 定义

Xmodem协议是串口通信中广泛使用到的异步文件传输协议。以128字节块的形式传输数据,并且每个块都使用一个校验过程来进行错误检测。在校验过程中如果接收方关于一个块的检验和与它在发送方的检验相同时,接收方就向发送方发送一个确认字节ACK。如果有错则发送一个字节NAK要求重发。以保证传输过程中的正确性,但是由于需要对每个块都要进行检验,显得效率比较低。

2. 控制字符

控制字符 说明
SOH 0x01 Xmodem数据头
STX 0x02 1K-Xmodem数据头
EOT 0x04 发送结束
ACK 0x06 认可响应
NAK 0x15 不认可响应
CAN 0x18 终止传送
CTRLZ 0x1A 填充数据包

3. 帧数据格式

Byte0 Byte1 Byte2 Byte3~Byte130 Byte131~Byte132
数据头SOH 序列号 序列号补码 128位数据 CRC16位校验

Xmodem协议的传输数据单位为信息包,包含一个标题开始字符SOH或者STX,一个单字节包序号,一个单字节包包序号的补码128个字节数据和一个双字节的CRC16校验

4. 数据包说明

对于标准Xmodem协议来说,如果传送的文件不是128的整数倍,那么最后一个数据包的有效内容肯定小于帧长,不足的部分需要用CTRL-Z(0x1A)来填充。

这里可能有人会问,如果我传送的是bootloader工程生成的.bin文件,mcu收到后遇到0x1A字符会怎么处理?其实如果传送的是文本文件,那么接收方对于接收的内容是很容易识别的,因为CTRL-Z不是前128个ascii码,不是通用可见字符,如果是二进制文件,mcu其实也不会把它当作代码来执行。哪怕是excel文件等,由于其内部会有些结构表示各个字段长度等,所以不会读取多余的填充字符。否则Xmodem太弱了。

5. 启动传输

Xmodem协议的传输由接收方启动,接收方向发送方发送C或者NAK(这里的NAK是用来启动传输的。下面我们用到的NAK是用来对数据产生重传机制)。其中接收方发送NAK信号表示接收方打算用累加和校验;发送字符C则表示接收方打算使用CRC校验。

LPUART_DRV_SendData(INST_LPUART1, &C, 1); //发送Xmodem帧传输开始命令

6. 传输过程

通俗易懂Xmodem协议_第1张图片
当接收方发送的第一个C或者NAK到达发送方,发送方认为可以发送第一个数据包了,传输启动。发送方接着接着应该将数据以每次128字节的数据加上包头,包号,包号补码,末尾加上校验和,打包成帧格式传送。发送方发了第一个包后就等待接收方的确认字节ACK,收到接收方传来的ACK确认,就认为数据包被接收方正确接收,并且接收方要求发送方继续发送下一个包;如果发送方收到接收方传来的NAK(这里的表示重发),则表示接收方请求重发刚才的数据包;如果发送方收到接收方传来的CAN字节,则表示接收方请求无条件停止传输。

如果发送方正常传输完全部数据,需要结束传输,正常结束需要发送方发送EOT通知接收方。接收方回以ACK进行确认。如果接收方发送CAN给发送方也可以强制停止传输,发送方受到CAN后不需要发送EOT确认,此时传输已经结束。

7. 代码

这里是接收Xmodem协议数据的代码,开发环境是S32DS,功能是用Term上位机发送Xmodem协议数据,串口接收到再用CAN发送给升级端。最后附上CRC16位检验代码。希望对你们有帮助,有疑问的可以在下面留言。

7.1 Xmodem接收数据代码

LPUART_DRV_SendData(INST_LPUART1, &C, 1); //发送Xmodem帧传输开始命令
LPUART_DRV_ReceiveDataPolling(INST_LPUART1, frame_buf, 133); /* 接收第一帧数据 */
while(LPUART_DRV_GetReceiveStatus(INST_LPUART1, &bytesRemaining) != STATUS_SUCCESS);

/*
 * @brief           : 发送一帧Xmodem数据,命令等待,接收到118板的命令再发送下一帧,这个函数在main函数中死循环执行
 * @param XDdata    : Xmodem数据数组
 * @param Command   : 接收端发送的命令
 * @param timeout   : 超时等待
 * @return          : None
 */
void Xmodem_CANSend(uint8_t *XDdata, uint8_t *Command, uint32_t timeout)
{
	static uint8_t ACK = 0x06;
	static uint8_t NAK = 0x15;
	static uint16_t crc = 0;
	static uint32_t bytesRemaining = 0;
	static uint32_t timeout_value = 700000;

	crc = crc16(&(XDdata[3]), 128);
	/* 检验成功,上一帧数据正确,开始发送数据 */
	if(crc == (XDdata[CRCBIT1] << 8 | XDdata[CRCBIT2]))
	{
        /* 向发送端发送一个响应,继续接收数据 */
		LPUART_DRV_SendData(INST_LPUART1, &ACK, 1);  
        /* 再接收一帧Xmodem数据 */
		LPUART_DRV_ReceiveData(INST_LPUART1, XDdata, 133); 

		timeout = timeout_value;  //超时退出
		while((timeout--) &&(LPUART_DRV_GetReceiveStatus(INST_LPUART1, &bytesRemaining) != STATUS_SUCCESS));

		if(XDdata[HEADBIT] == EOT) //数据发送完毕
		{
			LPUART_DRV_SendData(INST_LPUART1, &ACK, 1);
		}
		/* 将这一帧128字节数据发送出去 */
		CANSendData(&(XDdata[3]), 128);
	}
	else
	{
		LPUART_DRV_SendData(INST_LPUART1, &NAK, 1);  // 数据有误,再发送一次
		LPUART_DRV_ReceiveData(INST_LPUART1, XDdata, 133);
		timeout = timeout_value;  /* 加入了超时退出 */
		while((timeout--) && (LPUART_DRV_GetReceiveStatus(INST_LPUART1, &bytesRemaining) != STATUS_SUCCESS)); 

		CANSendData(&(XDdata[3]), 128);
	}
}

7.2 CRC16位校验代码

/*
 * @brief           : CRC16校验函数
 * @param addr      : 要进行校验的数据数组
 * @param num       : 校验多少位
 * @return          : None
 */
uint16_t crc16(const uint8_t *addr, uint32_t num)
{
	uint16_t crc = 0;
    for( ; num > 0; num--)
    {
    	crc = crc ^ (*addr++ << 8);
    	for(int i=0; i < 8; i++)
    	{
    		if(crc & 0x8000)
    			crc = (crc << 1) ^ 0x1021;
    		else
    			crc <<= 1;
    	}
    	crc &= 0xFFFF;
    }
    return crc;
}

你可能感兴趣的:(专业)