一、在系统编程ISP
芯片系统区 ROM(0x1FFFF000) 一般会有一个厂家固化好的升级程序,允许用户对芯片进行烧录和升级。但是有几个缺点:
- 进入这个系统区程序需要在上电前控制BOOT引脚,即需要设备预留额外硬件接口;
- 通信协议固定、通信接口固定,使用上不自由;
所以,单片机程序一般还需要自行准备一个 bootloader 来应对各种升级需求。
二、升级方案
- 不支持对 bootloader 自身的升级。除了不安全外(bootloader自身升级失败就会彻底无解),这个功能需要把 bootloader 定位到 RAM 中跑,增加了复杂度和对编程工具的依赖,当代码(向其他编程工具)移植时很不友好;
- 不支持app地址无关或者可重定位,即不支持app既能在A地址上跑也能在B地址上跑,否则一样造成复杂度高、依赖性强、移植性差。(因为有一个升级方案就是若A地址程序在跑就升级到B地址,若B地址程序在跑就升级到A地址);
- 如果有需要断电保存的参数可以放在某个分区的末尾扇区,但至少要占据一个扇区,否则app对该区域擦写时可能会因为擦掉自己导致死机。
在以上几个特征的基础上,分别有以下方案:
1、方案1
- 文件下载功能放在 bootloader 里;
- 除去 bootloader,剩下的flash空间都给app使用。
优点:
- app可用空间足够大。
缺点:
- 上电后,bootloader 可能需要等待几秒钟尝试与主机连接;
- 需要升级时,主机需要将ARM单片机单独重启一次;
- 升级时会破坏正常的app区域,若失败后,app无法运行,直到升级成功为止。
2、方案2
- 文件下载功能放在 app 里;
- 除去 bootloader,剩下的flash空间一半给app使用,一半用来存储下载文件。
优点:
- bootloader 足够简单足够小,上电后 bootloader 无需等待主机连接,可以立即启动app;
- 运行期间可以随时下载升级文件,而不影响正常功能;
- 升级成败与否,都不影响app的正常运行;
缺点:
- app的可用空间缩小一半。
flash 分区布局:
bootloader 区 | app 运行区 | app 参数区 | 升级文件存储区 |
---|---|---|---|
4K | (128K - 4K) / 2 - 1K | 1K | (128K - 4K) / 2 |
如此,app大小就是 61K = (128K - 4K) / 2 - 1K。
方案2的 bootloader 足够简单,以下代码就以该方案为例。
三、程序设计(方案2)
1、分区检查
bootloader 运行后,是否需要复制升级文件到运行区,决定于运行区和文件存储区的内容合法性,合法性校验典型的就是CRC校验。
分区定义
#define FLASH_PART_BOOT 0 // bootloader 分区
#define FLASH_PART_RUN 1 // 运行分区
#define FLASH_PART_PARAM 2 // 参数分区
#define FLASH_PART_UPGRADE 3 // 升级文件分区
#define FLASH_TOTAL_SIZE (128*1024) // flash总大小 128K
#define FLASH_BOOT_SIZE (4*1024) // bootloader 大小 4K
#define FLASH_PARAM_SIZE (1*1024) // 需要断电保存的参数大小 1K
// app程序大小 61K
#define FLASH_APP_SIZE ((FLASH_TOTAL_SIZE - FLASH_BOOT_SIZE)/2 - FLASH_PARAM_SIZE)
分区数据合法性检查
uint16_t CRC16_CCIT_Calc(const void *data, uint32_t len);
uint32_t Boot_PartAddrGet(int partId);
// 校验一个分区是否合法,成功返回0
int Boot_PartCheck(int partId)
{
return (CRC16_CCIT_Calc((void*)Boot_PartAddrGet(partId), FLASH_APP_SIZE) == 0) ? 0:-1;
}
升级文件末尾的CRC值,可以由PC工具在制作bin文件时完成,也可以由发送端程序得到bin文件后自动添加。
一块数据连同它的CRC一起,如果再次对其进行CRC计算,那么结果应该是0。这里的CRC是16位的,所以就有一个大小端问题,即2字节的CRC值应该以怎样的字节序放入数据块的尾部。例如下面这个CRC算法:
uint16_t CRC16_CCIT_Calc(const void *data, uint32_t len)
{
if (data == 0 || len == 0) {
return 0xffff;
}
uint16_t crc = 0xffff;
uint16_t poly = 0x1021;
uint8_t byte = 0;
uint8_t *buf = (uint8_t*)data;
while (len-- > 0) {
byte = *(buf++);
crc ^= (byte << 8);
for (int i=0; i<8; i++) {
if (crc & 0x8000)
crc = (crc << 1) ^ poly;
else
crc = crc << 1;
}
}
return crc;
}
上述程序中,出现 crc & 0x8000,可以看出算法是先算高字节,所以CRC值应该以大端方式参与校验,即高字节在低地址处,低字节在高地址处。
2、升级检查
升级条件判定
// 比较两个分区里指定长度的内容是否相等,相等返回0
int Boot_PartCompare(int partIdx, int partIdy, uint32_t len);
// 检查是否需要升级,成功返回0
int Boot_UpgradeCheck(void)
{
if (Boot_PartCheck(FLASH_PART_UPGRADE) == 0) {
// 运行分区不合法,或者运行分区合法但与文件存储区不一样,就表示需要升级
if (Boot_PartCheck(FLASH_PART_RUN) != 0 ||
Boot_PartCompare(FLASH_PART_RUN, FLASH_PART_UPGRADE, FLASH_APP_SIZE) != 0) {
return 0;
}
}
return -1;
}
3、升级app
升级函数
int Boot_FlashWrite(uint32_t addr, const void *data, uint32_t len);
// 升级,成功返回0
int Boot_Upgrade(void)
{
Boot_FlashWrite( Boot_PartAddrGet(FLASH_PART_RUN),
(void*)Boot_PartAddrGet(FLASH_PART_UPGRADE),
FLASH_APP_SIZE);
if (Boot_PartCompare(FLASH_PART_RUN, FLASH_PART_UPGRADE, FLASH_APP_SIZE) != 0) {
return -1;
}
return 0;
}
4、运行app
#define U32_READ(x) (*((uint32_t*)(x)))
int Board_DeInit(void);
void Boot_AppRun(void)
{
uint32_t sp = U32_READ(Boot_PartAddrGet(FLASH_PART_RUN));
uint32_t rstHandler;
// 检查sp指向的RAM地址是否合法
if ((sp & 0x20000000) != 0x20000000) {
return;
}
// 解除某些外设的初始化。例如bootloader打开了串口中断,如果忘记关闭,app里就可能意外触发串口中断导致死机
Board_DeInit();
__set_PRIMASK(1); // 禁止中断
__set_MSP(sp)); // 设置MSP
__set_CONTROL(0); // 设置特权模式,且使用MSP而不是PSP
// 跳转到app的复位向量处
rstHandler = U32_READ(Boot_PartAddrGet(FLASH_PART_RUN) + 4);
( (void (*)(void))rstHandler )();
}
5、主程序结构
int Board_Init(void);
int Boot_AppLost(void);
int main(void)
{
Board_Init();
if (Boot_UpgradeCheck() == 0) {
Boot_Upgrade();
}
Boot_AppRun();
Boot_AppLost();
for (;;);
return 0;
}