一、前言
- 今天分享一下串口bootloader,利用串口bootloader程序可以更新芯片的APP程序,对于一些不便于拆卸的产品,通过对外引出的通信端口来升级程序的维护方式非常好用。
- 串口bootloader网上可以找到很多,例如RT-Thread的通用bootloader,还可以通过http来更新APP程序,不过暂时只支持F1和F4系列的,或者我们可以自己写一个简单的串口bootloader,当然这也需要编写配套的上位机软件才能进行烧写,这样就有点麻烦,不过好在STM32为我们提供了串口bootloader的例程,使用起来相当方便,只需要使用支持Ymodern协议的PC软件,就可以对STM32进行固件更新,而且Ymodern协议是分包写入flash中,对于更新较大的APP的程序,使用小量的RAM空间就能完成,那么本次我们就来移植一下,工程以STM32F303RCT6为例。
- 移植需要芯片的一个UART,以及连接PC串口端的转换工具,在此之前,必须调通UART能和PC串口端通信,例如写一个简单的验证程序,PC端发送任意数,芯片UART就将接收到的任意数回应给PC,因为本人在移植的时候,就因为串口端的转换工具异常(TTL转RS232),浪费了不少时间,甚至一度怀疑官方IAP程序有问题,最后还是用示波器发现转换工具在PC发送时,无端回应一些错误数据给PC,在此提醒大家做好移植前的硬件准备,工欲善其事必先利其器!(本文使用的模板工程已上传至:https://gitee.com/INT_TANG/some-project/tree/master/STM32/Bootloader)
二、移植准备
1. 如果我们安装了STM32CubeMX,并生成过对应芯片的工程,那么可以在其安装目录下,找到对应芯片型号的Cube包,也可以从官网上下载Cube包,串口bootloader例程一般位于文件夹Cube包下的…\Projects\STM32XXX_EVAL\Applications\IAP中。
1.不一定每个文件夹都有IAP例程,STM32提供多种开发板,我们选择相似的芯片型号里面去查找即可。
2. 位于IAP\IAP_Main\Src,存放的就是串口bootloader的实现。
1.跟串口相关,主要是格式转换,我们也可以自己实现printf函数达到格式输出要求。
2.跟芯片的Flash相关,包含Flash的擦除,写入,读取等相关。
3.实现串口bootloader的菜单项,通过PC终端操作,选择写入/读取固件,跳转APP程序。
4.Ymodern协议的主要实现。
3. Ymodern协议
关于Ymodern协议解析可以参考链接:https://blog.csdn.net/INT_TANG/article/details/117334848
或者可以阅读官方的Ymodern,文件已上传到我的仓库:https://gitee.com/INT_TANG/doc/tree/master/XYmodern
三、代码分析
1. 查看例程main.c文件
- 70行通过外部按钮触发是否进入bootloader模式。
- 73行将flash初始化。
- 75行是串口的初始化。
- 77行是bootloader的实现。
2. Main_Menu ()
函数
- 162行清除串口的收发器。
- 165行接收一个命令字节。
- 171行串口下载更新flash。
- 180行获取APP固件的复位函数地址。
- 184行设置堆栈指针,在向量表分配中,第一个分配的是堆栈指针,第二个是复位函数入口地址,这与cortex-M内核的架构有关。
- 185行跳转到APP固件执行。
可以看出该函数实现一个菜单项,输入不同的命令执行相应操作,本次我们重点分析SerialDownoad()
函数。
3. SerialDownoad()
函数
可以看出,其主要调用的函数是Ymodem_Receive()
。
4. Ymodem_Receive()
函数
COM_StatusTypeDef Ymodem_Receive ( uint32_t *p_size )
{
uint32_t i, packet_length, session_done = 0, file_done, errors = 0, session_begin = 0;
uint32_t flashdestination, ramsource, filesize;
uint8_t *file_ptr;
uint8_t file_size[FILE_SIZE_LENGTH], tmp, packets_received;
COM_StatusTypeDef result = COM_OK;
/* APP的起始地址,即从该地址开始写入APP固件,APPLICATION_ADDRESS在flash_if.h文件中定义 */
flashdestination = APPLICATION_ADDRESS;
/* 等待文件完成或串口的状态 */
while ((session_done == 0) && (result == COM_OK))
{
packets_received = 0;
file_done = 0;
/* 等待文件完成或串口的状态 */
while ((file_done == 0) && (result == COM_OK))
{
/* ReceivePacket主要接收串口的数据,并验证是否符合Ymodern协议的数据包 */
switch (ReceivePacket(aPacketData, &packet_length, DOWNLOAD_TIMEOUT))
{
case HAL_OK:
errors = 0;
switch (packet_length)
{
case 2:
/* PC传输中断 */
Serial_PutByte(ACK);
result = COM_ABORT;
break;
case 0:
/* 传输结束 */
Serial_PutByte(ACK);
file_done = 1;
break;
default:
/* 判断是否接收正确的数据包数,不成功则回应NAK,此时PC会重发数据包 */
if (aPacketData[PACKET_NUMBER_INDEX] != packets_received)
{
Serial_PutByte(NAK);
}
else
{
/* 第一包数据包和传输完成后传送的结束包,都是以0数据包传送 */
if (packets_received == 0)
{
/* 第一包数据传输的是文件名和文件大小 */
if (aPacketData[PACKET_DATA_INDEX] != 0)
{
/* File name extraction */
i = 0;
file_ptr = aPacketData + PACKET_DATA_INDEX;
while ( (*file_ptr != 0) && (i < FILE_NAME_LENGTH))
{
aFileName[i++] = *file_ptr++;
}
/* File size extraction */
aFileName[i++] = '\0';
i = 0;
file_ptr ++;
while ( (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH))
{
file_size[i++] = *file_ptr++;
}
file_size[i++] = '\0';
Str2Int(file_size, &filesize);
/* Test the size of the image to be sent */
/* Image size is greater than Flash size */
/* 验证文件大小是否超过允许的APP固件大小,USER_FLASH_SIZE 在flash_if.h文件中定义 */
if (*p_size > (USER_FLASH_SIZE + 1))
{
/* End session */
tmp = CA;
HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
result = COM_LIMIT;
}
/* 写入flash前需擦除该区域 */
FLASH_If_Erase(APPLICATION_ADDRESS);
*p_size = filesize;
Serial_PutByte(ACK);
Serial_PutByte(CRC16);
}
/* 文件名为空,说明是结束包 */
else
{
Serial_PutByte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
else /* Data packet */
{
ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];
/* 写入flash */
if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
{
flashdestination += packet_length;
Serial_PutByte(ACK);
}
else /* An error occurred while writing to Flash memory */
{
/* End session */
Serial_PutByte(CA);
Serial_PutByte(CA);
result = COM_DATA;
}
}
packets_received ++;
session_begin = 1;
}
break;
}
break;
case HAL_BUSY: /* Abort actually */
Serial_PutByte(CA);
Serial_PutByte(CA);
result = COM_ABORT;
break;
default:
if (session_begin > 0)
{
errors ++;
}
if (errors > MAX_ERRORS)
{
/* Abort communication */
Serial_PutByte(CA);
Serial_PutByte(CA);
}
else
{
/* 若函数ReceivePacket超时没有收到数据,则发送‘C’应答,通知PC启动传输 */
Serial_PutByte(CRC16); /* Ask for a packet */
}
break;
}
}
}
return result;
}
5. ReceivePacket()
函数
static HAL_StatusTypeDef ReceivePacket(uint8_t *p_data, uint32_t *p_length, uint32_t timeout)
{
uint32_t crc;
uint32_t packet_size = 0;
HAL_StatusTypeDef status;
uint8_t char1;
*p_length = 0;
status = HAL_UART_Receive(&UartHandle, &char1, 1, timeout);
/* 判断接收的数据头是哪一种包类型 */
if (status == HAL_OK)
{
switch (char1)
{
case SOH:
packet_size = PACKET_SIZE;
break;
case STX:
packet_size = PACKET_1K_SIZE;
break;
case EOT:
break;
case CA:
if ((HAL_UART_Receive(&UartHandle, &char1, 1, timeout) == HAL_OK) && (char1 == CA))
{
/* PC主动停止传输 */
packet_size = 2;
}
else
{
status = HAL_ERROR;
}
break;
case ABORT1:
case ABORT2:
status = HAL_BUSY;
break;
default:
status = HAL_ERROR;
break;
}
*p_data = char1;
/* 如果是正常的数据包 */
if (packet_size >= PACKET_SIZE )
{
status = HAL_UART_Receive(&UartHandle, &p_data[PACKET_NUMBER_INDEX], packet_size + PACKET_OVERHEAD_SIZE, timeout);
/* Simple packet sanity check */
if (status == HAL_OK )
{
if (p_data[PACKET_NUMBER_INDEX] != ((p_data[PACKET_CNUMBER_INDEX]) ^ NEGATIVE_BYTE))
{
packet_size = 0;
status = HAL_ERROR;
}
else
{
/* Check packet CRC */
crc = p_data[ packet_size + PACKET_DATA_INDEX ] << 8;
crc += p_data[ packet_size + PACKET_DATA_INDEX + 1 ];
if (Cal_CRC16(&p_data[PACKET_DATA_INDEX], packet_size) != crc )
{
packet_size = 0;
status = HAL_ERROR;
}
}
}
else
{
packet_size = 0;
}
}
}
*p_length = packet_size;
return status;
}
四、移植
1. 准备一个可用的串口工程,创建BOOT文件,并从官方提供的IAP例程中复制出以下文件。
其中的SerilaCOM.c和SerilaCOM.h是自行创建的,主要是串口的初始化,STM32例程将串口初始化放到了main文件中,不便于移植到其他工程中,我们将其独立出来。
2. flash_if.h文件修改
在上图中发现,官方例程只检查了8-39区是否存在写保护,如果我们的APP程序很大,超过这些区域,则可能因为被写保护而无法写入,而在这个检查写保护没检查出来是不行的,我们根据使用的型号容量进行修改。
以上设置是有必要的,这划分了flash的boot区和APP分区,分区信息如下:
3. 添加一些延时
某些PC终端软件,在特殊情况下,例如中断传输,传输完成等,会有一定的反应时间,如果不添加延时,则可能看不到这些信息的有效输出。
4. 修改Ymodem_Receive()
函数
上图中,根据Ymodern协议,第一包先发送文件名,文件大小,但本人发现在此之前,*p_size是没有被任何数赋值到,认为此处代码有错,应该是将上一行代码中的filesize比较,得出文件大小是否超出flash大小,从而是否执行更新,应修改为
if (filesize > (USER_FLASH_SIZE + 1))
5. bootloader工程设置
此处设置的是链接地址,boot是首先运行的程序,所以需要链接到0x8000000,大小需要和设置的APP地址对应,同样,RAM我们也划分开来,分配专有的RAM区,从而使得boot区和APP区使用的资源隔离开。
6. APP程序的工程设置
设置APP工程的链接地址为0x8004000,这个地址不是固定的,是由bootloader工程里面的
APPLICATION_ADDRESS
决定的,这是bootloader跳转到的地址。同样,RAM分区我们也需要隔离开,同bootloader区分开。
设置向量表的偏移,这里已经告诉需要设置满足条件偏移量0x200(不同芯片偏移可能不一样),也就是APP的起始链接地址需要满足的条件,同时也是bootloader工程里面的
APPLICATION_ADDRESS
需要满足的条件。为了不改动官方的源码,我们选择在外部设置。
偏移到APP程序的链接地址。如果使用RTOS,需要在之前设置,比如RTThread:
至此,就移植完成bootloader,可以看出步骤非常简单。
五、下载
使用SecureCRT软件。
注意,如果你的APP固件大于30K以上,需要设置如下:
原因在于Ymodern使用一个字节作为数据包计数,当使用128数据包模式时(128*255/1024约等于31K),该字节会溢出并重新计数,但STM32的源码存在一定漏洞。
对于RAM容量紧张的芯片,可能没有分配到那么大的串口接收缓冲区,那么使用128数据包模式就很有必要了,这样串口接收缓冲区就只需要少量RAM,继续修改一下
Ymodem_Receive
函数。
COM_StatusTypeDef Ymodem_Receive ( uint32_t *p_size )
{
/* 省略部分代码 */
uint32_t flashdestination, ramsource, filesize=0;//将记录文件大小的变量filesize清零
uint32_t alreadyfilesize=0; //新建变量alreadyfilesize用于记录已接收到的数据
/* 省略部分代码 */
/* 判断文件是否已经接收完成 */
if ((packets_received == 0)&&(alreadyfilesize >= filesize))
{
/* File name packet */
if (aPacketData[PACKET_DATA_INDEX] != 0)
{
/* 省略部分代码 */
}
/* File header packet is empty, end session */
else
{
/* 省略部分代码 */
}
}
else /* Data packet */
{
ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];
/* Write received data in Flash */
if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
{
flashdestination += packet_length;
/* 记录已接收数据大小 */
alreadyfilesize += packet_length;
Serial_PutByte(ACK);
}
else /* An error occurred while writing to Flash memory */
{
/* End session */
Serial_PutByte(CA);
Serial_PutByte(CA);
result = COM_DATA;
}
}
/* 省略部分代码 */
}
六、通用性
对比了H7系列的IAP例程,它们的基本架构相同,且提供的函数接口相同,也就是说,当移植好了上面的工程模板(F303),如果迁移到H7/F1/F4等等系列,只需复制对应型号的flash_if.c和flash_if.h文件(主要是flash的容量和分配区域不同),同时修改flash_if.h支持的APP固件大小,起始扇区,保护读取扇区,以及修改SerilaCOM.c即可以做到通用,其他文件不需要更改,非常方便应用到不同系列的bootloader。