介绍
(一般来说还需要一个交付给客户使用的上位机程序,本篇只讲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 ROMHAL_FLASH_Unlock();// Erase the ROMEraseInitStruct.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();}// Writefor(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 ROMHAL_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这位大佬的教程,介绍的非常详细
由于工作需要我还要在上位机中设计一些特定的功能,所以我还需要自己来写(╥╯^╰╥)