一、Ymodem协议简介
1、 帧格式
帧头 | 包号 | 包号反码 | 信息块 | 校验码 |
---|---|---|---|---|
SOH /STX | 1 | 1 | 1024/128 | CRC(2) |
0x01/0x02(1byte) | 0~0xFF(1byte) | 1byte | 1024byte/128byte | 2byte |
信息帧包含三种形式:起始帧, 数据帧,结束帧。
起始帧
帧头 | 包号 | 包号反码 | 文件名称 | 文件大小 | 填充区域 | 校验高位 | 校验低位 |
---|---|---|---|---|---|---|---|
SOH | 0X00 | 0XFF | 文件名(例:CH_APP_A.bin) | size(例:7184) | NULL(0X00) | CRC_H | CRC_L |
数据帧
帧头 | 包号 | 包号反码 | 数据区 | 校验高位 | 校验低位 |
---|---|---|---|---|---|
SOH/STX | 0X00 | 0XFF | data(128/1024) | CRC_H | CRC_L |
对于数据帧,中间过程一般是128字节的信息,或者1024,但需要对最后一帧数据做一些处理,例如小于128字节的有效数据或者小于1024,大于128字节的有效数据时的情况处理
data长度< 128 byte | 128<data<1024 byte |
---|---|
133byte | 1029 |
不够128字节的补0x1A | 不够1024字节的补0X1A |
结束帧
帧头 | 包号 | 包号反码 | 数据区 | 校验高位 | 校验低位 |
---|---|---|---|---|---|
SOH | 0X00 | 0XFF | 0x00 | 0x00 | 0x00 |
1.1、帧头
根据这一帧数据的第一个字节,判断下发的这一帧数据的信息块是128还是1024字节
SOH | STX |
---|---|
0x01 | 0x02 |
128byte | 1024byte |
1.2、包序号
对于数据包序号而言,只包含1个字节,因此计算范围是0~255;对于数据包大于255的,序号归零重复计算.
1.3、帧长度
SOH | STX |
---|---|
0x01 | 0x02 |
133byte | 1029byte |
1.4、校验位
Ymodem采用的是CRC16校验算法,校验值为2字节,传输时CRC高八位在前,低)八位在后;CRC计算数据为
信息块数据,不包含帧头、包号、包号反码。
二、Ymodem 命令
cmd | 命令码 | 注释 |
---|---|---|
SOH | 0x01 | 133byte 帧长度 |
STX | 0x02 | 1029byte 帧长度 |
EOT | 0x04 | 结束命令 |
ACK | 0x06 | 应答命令 |
NAK | 0x15 | 重传当前数据包请求命令 |
CAN | 0x18 | 取消传输命令,连续发送5个该命令 |
C | 0x43 | 握手命令 |
握手信号由接收方发起,在发送方开始传输文件前,接收方需发送YMODEM C(字符C,Ascii码为0x43)命
令,发送方收到后,开始传输起始帧。
三、通信过程
3.1、通信简介
首先,由接收机(如下位机MCU),发送0x43,表示已经准备好了接收来自上位机的数据。
此时可以通过超级终端或者有Ymodem协议的上位机界面查看到字符‘C’,然后,上位机选中要发送的BIN文件,点击发送,此时可以在上位机上看到发送的进度条。如果发送完成显示100%.
3.2、具体分析
3.2.1、操作流程
到这一步基本就上位机的配置完成。为了回显我们在上位机上输出的字符,点击文件,然后属性,点击ASCII设置回显。如下:
3.2.2、时序分析
通过逻辑分析仪,将其连接在RX和TX上,可以观察到上位机和下位机之间交互的数据信息
我使用的是Pluse View,具体设置如下所示:
由于这个时序较长,只能截取部分看
第一帧数据(起始帧):
由图可看其结构
帧头 | 包号 | 包号反码 | 文件名称 | 文件大小 | 填充区域 | 校验高位 | 校验低位 |
---|---|---|---|---|---|---|---|
SOH | 0X00 | 0XFF | CH_APP_A.bin | 7104 | NULL(0X00) | CRC_H | CRC_L |
数据帧:
我们可以在VSCODE中查看到ASCII的值和逻辑分析仪中的值是一致的
在最后一帧数据
由于帧头是02开头,所以当数据发送完不足1024的,补0x1A,如下所示:
接收完成后,上位机发送发送结束命令0x04
下位机回应ACK命令0x06
然后上位机下发结束帧数据
可见,最后一帧数据校验是0。到此,数据传输完成。
四、代码分析
主要是接收部分代码:我也是抄的安富莱电子的代码:点击此处进入安富莱电子
在文章最后我会附上这IAP的安装包。
/*
*********************************************************************************************************
* 函 数 名: Receive_Packet
* 功能说明: 按照ymodem协议接收数据
* 形 参: buf 数据首地址
* 返 回 值: 文件大小
*********************************************************************************************************
*/
uint32_t TotalSize = 0;
int32_t Ymodem_Receive (uint8_t *buf, uint32_t appadr)
{
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
int32_t i, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
uint32_t flashdestination, ramsource;
uint8_t ucState;
uint32_t SectorCount = 0;
uint32_t SectorRemain = 0;
/* 初始化flash编程首地址 */
flashdestination = appadr;
/* 接收数据并进行flash编程 */
for (session_done = 0, errors = 0, session_begin = 0; ;)
{
for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
{
switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))
{
/* 返回0表示接收成功 */
case 0:
errors = 0;
switch (packet_length)
{
/* 发送端终止传输 */
case - 1:
Send_Byte(ACK);
return 0;
/* 传输结束 */
case 0:
Send_Byte(ACK);
file_done = 1;
break;
/* 接收数据 */
default:
if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
{
Send_Byte(NAK);
}
else
{
if (packets_received == 0)
{
/* 文件名数据包 */
if (packet_data[PACKET_HEADER] != 0)
{
/* 读取文件名 */
for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
{
FileName[i++] = *file_ptr++;
}
/* 文件名末尾加结束符 */
FileName[i++] = '\0';
/* 读取文件大小 */
for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
{
file_size[i++] = *file_ptr++;
}
file_size[i++] = '\0';
/* 将文件大小的字符串转换成整型数据 */
Str2Int(file_size, &size);
/* 检测文件大小是否比flash空间大 */
if (size > (1024*1024*2 + 1))
{
/* 终止传输 */
Send_Byte(CA);
Send_Byte(CA);
return -1;
}
/* 擦除用户区flash */
SectorCount = size/(128*1024);
SectorRemain = size%(128*1024);
for(i = 0; i < SectorCount; i++)
{
bsp_EraseCpuFlash((uint32_t)(flashdestination + i*128*1024));
}
if(SectorRemain)
{
bsp_EraseCpuFlash((uint32_t)(flashdestination + i*128*1024));
}
Send_Byte(ACK);
Send_Byte(CRC16);
}
/* 文件名数据包处理完,终止此部分,开始接收数据 */
else
{
Send_Byte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
/* 数据包 */
else
{
/* 读取数据 */
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
ramsource = (uint32_t)buf;
/* 扇区编程 */
ucState = bsp_WriteCpuFlash((uint32_t)(flashdestination + TotalSize), (uint8_t *)ramsource, packet_length);
TotalSize += packet_length;
/* 如果返回非0,表示编程失败 */
if(ucState != 0)
{
/* 终止传输 */
Send_Byte(CA);
Send_Byte(CA);
return -2;
}
Send_Byte(ACK);
}
/* 接收数据包递增 */
packets_received ++;
session_begin = 1;
}
}
break;
/* 用户终止传输 */
case 1:
Send_Byte(CA);
Send_Byte(CA);
return -3;
/* 其它 */
default:
if (session_begin > 0)
{
errors ++;
}
if (errors > MAX_ERRORS)
{
Send_Byte(CA);
Send_Byte(CA);
return 0;
}
Send_Byte(CRC16);
break;
}
if (file_done != 0)
{
break;
}
}
if (session_done != 0)
{
break;
}
}
return (int32_t)size;
}
对于不同的MCU,需要修改的地方很少:
/* 擦除用户区flash */
SectorCount = size/(128*1024);
SectorRemain = size%(128*1024);
for(i = 0; i < SectorCount; i++)
{
bsp_EraseCpuFlash((uint32_t)(flashdestination + i*128*1024));
}
if(SectorRemain)
{
bsp_EraseCpuFlash((uint32_t)(flashdestination + i*128*1024));
}
Send_Byte(ACK);
Send_Byte(CRC16);
}
/* 文件名数据包处理完,终止此部分,开始接收数据 */
else
{
Send_Byte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
/* 数据包 */
else
{
/* 读取数据 */
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
ramsource = (uint32_t)buf;
/* 扇区编程 */
ucState = bsp_WriteCpuFlash((uint32_t)(flashdestination + TotalSize), (uint8_t *)ramsource, packet_length);
TotalSize += packet_length;
就只有数据在FLASH中的擦除和写入换成对应MCU的就行。
我使用的是CH32V303RCT6,一页大小是256byte,同时也可以进行快速页编辑,对于1024的YModem_1k协议,刚好是*4,所以,先计算下发文件在FLASH中占据的页数,如size/256 = NbrOfPage,可以多加一页擦除,防止出现错误; 再通过快速页擦除,
最后写入即可。
FLASH_Status BSP_Flash_ErasePage(uint32_t addr, uint32_t cnt)
{
FLASH_Status status = FLASH_COMPLETE;
uint32_t value = 0xE339E339;
HAL_Flash_FastUnLock();
for (int i = 0; i < cnt; ++i)
{
FLASH_ErasePage_Fast(addr);
addr += 256;
}
HAL_Flash_FastLock();
for (int j = 0; j < cnt; ++ j)
{
for (int i = 0; i < 64; ++i)
{
if (value != *(uint32_t *)addr)
{
status = FLASH_ERROR_WRP;
printf("flash erase fail addr = %#x, valeu = %#x\r\n", (uint32_t *)addr, *(uint32_t *)addr);
}
addr += 4;
}
}
return status;
}
在CH32V303中,当下载完成后,关闭使用的外设,然后使用软件复位,具体参考官方例程:
void IAP_To_APP(void)
{
Delay_Ms(50);
GPIO_DeInit(GPIOA);
GPIO_DeInit(GPIOB);
USART_DeInit(USART3);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, DISABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, DISABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, DISABLE);
Delay_Ms(10);
NVIC_EnableIRQ(Software_IRQn);
NVIC_SetPendingIRQ(Software_IRQn);
}
修改中断地址
void SW_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SW_Handler(void)
{
__asm("li a6, 0x5000");
__asm("jr a6");
while(1);
}
将整个0x5000改为自己APP的地址即可,这里可以设置一个IF判断,来选择跳入那个APP,比如想跳入A区,则if(addr == app_a_addr),同理B区页一样。
五、实验现象
下载程序到APP_A分区
下载程序到APP_B分区
AB分区跳转
使用软件:超级终端
SecureRTC
参考代码:stm iap
这两个软件操作没啥区别。都可以实现。同时这个也可以用来验证自己写的Ymodem上位机或下位机程序,十分方便。