STM32 IAP技术 bootloader设计
介绍
(一般来说还需要一个交付给客户使用的上位机程序,本篇只讲bootloader)
Flash空间分配
bootloader设计
通信协议
通信协议就是boot和上位机约定的一个规则:收到什么信号时接收数据包,什么时候接收完成,什么时候跳转应用,什么时候恢复出厂设置等等,这个可以按照canopen协议来,也可以自己规定
我这边规定的如下:大致思路就是先握手,握手完成就开始接收程序,接收完成之后写入flash
if(CAN2_Receive_Flag)
{
CAN2_Receive_Flag = 0;
// 握手
if(CAN_RxHeader.StdId == 0x0F0)
{
update_begin_flag = 1;
CAN2_Transmit(0x0F0, Shake_Hands); // 握手完成
}
// 开始接收程序
if(CAN_RxHeader.StdId == 0x0F1)
{
}
// 接收完成,开始写入
if(CAN_RxHeader.StdId == 0x0F2)
{
update_finish_flag = 1;
}
}
程序跳转
包括两个步骤:不更新时从boot跳转到APP,以及更新完后跳转到APP,原理都是一模一样的,直接放代码:
if(!update_begin_flag)
{
if(wait_ms >= WAITTIME) // 等待超时
{
if(((*(uint32_t*)(APP_ADDRESS+4))&0xFF000000)==0x08000000) // 检查地址
{
Jump_to_APP(APP_ADDRESS); // 跳转
}
}
}
其中Jump_to_APP代码:
void Jump_to_APP(uint32_t app_address)
{
uint8_t i = 0;
if(((*(uint32_t*)app_address)&0x2FFE0000)==0x20000000) // 检查地址
{
// 地址+4是复位中断服务程序地址
jump_to_app = (iapfun)*(uint32_t*)(app_address+4);
// 关闭全局中断
__set_PRIMASK(1);
// 关闭滴答定时器
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
// 设置所有时钟到默认状态
HAL_RCC_DeInit();
// 关闭所有中断
for (i = 0; i < 8; i++)
{
NVIC->ICER[i]=0xFFFFFFFF;
NVIC->ICPR[i]=0xFFFFFFFF;
}
// 使能全局中断
__set_PRIMASK(0);
// 这条在RTOS中比较重要,先照搬过来,不深究
__set_CONTROL(0);
__set_MSP(*(uint32_t*)app_address); // APP堆栈地址
jump_to_app(); // 跳转
// 跳转失败才会继续往下运行
while(1)
{
;
}
}
}
总之上面这段代码里面最关键的就是__set_MSP(*(uint32_t*)app_address);和jump_to_app = (iapfun)*(uint32_t*)(app_address+4);前者设置堆栈地址,即SP指针,后者设置程序入口。其余的都是一些安全措施,防止跳转过程收到乱七八糟的影响。
其中iapfun是需要定义的
// 用于跳转到特定地址的函数入口
typedef void (*iapfun)(void);
要实现完整的程序跳转,还要APP也配合在main函数起始位置加一条语句,并在target中进行设置:
SCB->VTOR = FLASH_BASE | 0x00020000;
这段代码将 Cortex-M 核心的中断向量表基址重定位到 Flash 存储的地址0x08020000,没有这条语句的话就算boot成功跳转了也没法运行的
程序接收
实现方式就是定义一个很大的uint8_t类型的数组,然后通过CAN总线的报文一个一个字节接收过来
if(CAN_RxHeader.StdId == 0x0F1)
{
if(APPLength < APP_LEN_MAX)
{
for(uint8_t i = 0; i < CAN_RxHeader.DLC; i++)
{
APP_RX_BUF[APPLength] = CAN2_Rx_data[i];
APPLength++;
}
}
else // 数组容量不足
{
;
}
}
这部分代码很简单,主要是上位机和boot程序要配合好,还要注意在 250 kbit/s 的波特率下,CAN总线每秒可以发送约 2212 条报文(8字节),所以数据发送周期最好不要设置太大。我设置10ms发一条报文,这样一个50k字节的程序差不多64秒发送完(实际项目程序也就30k左右的样子),还算可以接受。
程序写入
这个过程就是把接收的数组写到flash的对应位置中,之前介绍过flash读写的相关内容了,可以看一下STM32 使用HAL库实现flash读写_stm32halflash程序-CSDN博客
这里直接贴一下代码
if(((*(uint32_t*)(APP_ADDRESS+4))&0xFF000000)==0x08000000)
{
Write_APP(APP_ADDRESS, APP_RX_BUF, APPLength);
update_finish_flag = 1;
}
void Write_APP(uint32_t WriteAddr, uint8_t* pBuffer, uint16_t Num)
{
static FLASH_EraseInitTypeDef EraseInitStruct;
static uint32_t SECTORError;
// Unlock the ROM
HAL_FLASH_Unlock();
// Erase the ROM
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;
EraseInitStruct.Sector = FLASH_SECTOR_5;
EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3;
EraseInitStruct.NbSectors = 1;
if (HAL_FLASHEx_Erase(&EraseInitStruct, &SECTORError) != HAL_OK)
{
Error_Handler();
}
// Write
for(uint16_t i=0; i<Num; i++)
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, WriteAddr, *pBuffer) != HAL_OK)
{
Error_Handler();
}
WriteAddr += 1;
pBuffer ++;
}
// Lock the ROM
HAL_FLASH_Lock();
}
注意一下EraseInitStruct.Sector需要根据自己写入程序的地址来选,不要擦除错了。还要注意HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, WriteAddr, *pBuffer)中使用的是FLASH_TYPEPROGRAM_BYTE,按字节写入。
总结
这样一个简单的bootloader就算设计完了,当然要实现完整的IAP功能的话还需要上位机程序的配合。如果仅仅是需要单纯的IAP,也可以使用一些可以直接发送bin文件的CAN总线调试程序。可以参考一下STM32的IAP技术,基于CAN总线的STM32F103 BootLoader设计_哔哩哔哩_bilibili这位大佬的教程,介绍的非常详细
由于工作需要我还要在上位机中设计一些特定的功能,所以我还需要自己来写(╥╯^╰╥)