本文主要讲述BootLoader程序升级原理及一些代码的解析,力图用通俗易懂的语言描述清楚BootLoader升级的主要关键点。
首次接触这一块时,有一个概念叫IAP(在应用编程),通俗一点讲便是通过一段已有的程序(我们称之为BootLoader程序)去升级另外的一段程序(用户程序)。升级的方式多种多样,可以通过串口、USB、SPI等等多种接口去升级。实际上,我们是把我们需要升级的芯片里面分为两个区域,暂且称之为A
区域和B
区域。
A
区域主要存放BootLoader程序,B
区域主要存放用户程序,也就是我们希望升级或修改的程序。
一般情况下,为了升级流程的方便,我们会把A
区域布置在芯片flash(有人喜欢称ROM,就是存放代码的区域)的起始位置,也就是0x0开始的位置,至于A
区域在哪里结束,这需要看你的BootLoader程序有多大了,它能占用多少的代码量了。比如你的BootLoader程序编译完后有2.5KB左右的大小,那么你可以计算一下:
2.5K = 2.5 X 1024 = 2560B = A00(H)
也就是说,你的这段代码如果从零地址开始存放的话,他会在0xA00的位置结束,0xA00之后的区域你便可以用来存放需要升级的用户代码了。但有时我们并不会紧接着0xA01的位置开始放置用户代码,而是会留出一定的空间,比如从0xB00处开始存放代码,这主要是因为BootLoader程序在flash中存放时不一定会紧挨着存放,有时代码段之间会有空闲区域,这一点可以参考之前的文章 IAR编译器如何节省代码占用的flash空间?然后,我们通过在BootLoader程序中设置相关参数(应用程序起始位置等),使应用程序升级时按照我们设置的位置存放在B
区域,从而完成升级。这一点我会在后面的代码详解中介绍。
我们可以通过下面的图解来理解bootloader程序升级时的区域占用情况。
进行BootLoader程序编写之前,我们需要了解并熟悉以下几个关键点:
1. 文件传输协议。因为升级时需要上位机软件配合下位机的BootLoader程序进行应用程序代码的传输,因此文件传输协议至关重要,笔者推荐使用Xmodem 1K协议。 这个协议的好处便是上位机可以自动打包数据,每一包数据含有1k字节的代码,传输效率很高,传输时间很短。
2. 芯片空间map。 做BootLoader升级相关的项目,肯定离不开对芯片空间的了解,需要对自己所用芯片的RAM、ROM以及向量表(如果有的话)等占用情况有比较深入的了解。
3. 跳转函数。这个是程序从BootLoader程序跳转到应用程序运行的关键。笔者在做项目时曾经在这一块浪费了不少的时间。文末笔者会提供实用的跳转函数。
其实前面说的再多,脱离了代码都是纸上谈兵。下面通过一个实际的BootLoader升级例子,结合笔者自己编写的BootLoader代码,对这一过程进行解析。
代码主要包括
main.c
BootLoader.c
xmodem-1k.c
跳转函数
其中,
main.c
主要是升级执行初始化及升级完成后初始化升级环境及跳转代码实现的部分。
BootLoader.c
部分主要是升级流程的代码控制。
xmodem-1k.c
主要是文件传输协议的代码实现。
至于其他部分的代码,比如串口相关以及时钟相关的代码,每种芯片的编程方式都不尽相同,因此笔者不详细介绍这部分,该部分代码大家可以从需要升级的应用程序中直接移植即可。
int main (void)
{
SystemCoreClockUpdate(); // 时钟初始化
WatchDog_Initial(); // 看门狗初始化
vBootLoader(&vScene_Init,&vScene_Renew); // BootLoader主程序
}
在vBootloader()
这个函数中用到了两个函数指针,分别指向初始化函数vScene_Init()
和环境重置函数vScene_Renew()
。初始化函数很好理解,在运行程序之前,先对芯片时钟、管脚等初始化,或者有些参数需要初始化,这个根据自己的代码情况进行选择。
那么环境重置函数什么意思呢?这主要是为了和需要升级的应用程序的运行想配合,因为我们的bootloader程序的相关配置有时候并不一定会和应用程序的配置完全一致,如果运行完BootLoader之后,没有把BootLoader程序的相关配置关闭掉或者恢复到默认值,运行到应用程序之后,还可能会执行BootLoader程序的配置,这样会出现问题。举个栗子,在BootLoader程序中用中断喂狗,跳转到应用程序之前,没有关闭喂狗中断,如果在应用程序中没有配置相关喂狗中断的程序,那么应用程序仍然会按照bootloader的配置执行中断喂狗,这样会导致应用程序中的喂狗失效,因为中断喂狗是很准时的,往往起不到喂狗的效果,有时会影响程序的复位操作。因此,环境重置函数说白了,就是把bootloader用到的配置关掉。笔者建议把用到的所有的东西全部关闭(包括但不限于串口、时钟、看门狗、IO等),因为在应用程序中会根据自己的应用程序配置相关的代码。
/**********************************************************
* BootLoader流程控制函数
** 参 数: pfunSenceInitCallBack 初始化芯片指针函数
** pfunSenceRenewCallBack 重新初始化代码环境指针函数
** 返回值: 无
**********************************************************/
void vBootLoader(void(* pfunSenceInitCallBack)(void), void (* pfunSenceRenewCallBack)(void))
{
uint8_t ucMessage = 0;
unsigned int sp;
unsigned int pc;
uint16_t bootflag_read;
sp = APP_START_Flash;
pc = sp + 4;
pfunSenceInitCallBack(); //初始化函数指针,具体函数怎么写这里不再赘述
while(1)
{
wdt_feed();
do{
ucMessage = u8UpdateMode(); // 此函数为升级主函数
if (UPDATE_OK == ucMessage) /* 升级成功 */
{
memcpy(erase_pg_buf, bootflag_OK, sizeof(bootflag_OK));
update_bootflag();
break;
}
else if (UPDATE_NO == ucMessage) /* 没有升级 */
{
break;
}
else /* 升级错误 */
{
memcpy(erase_pg_buf, bootflag_ERROR, sizeof(bootflag_ERROR));
update_bootflag();
break;
}
} while (1);
bootflag_read = *( volatile uint16_t *)(BOOT_FLAG_ADDR); /* 读取存放在bootflag地址的值 */
if (u8UserCodeEffect() == USERCODE_OK) // 代码判断
{
if (bootflag_read == 0xAA55)
{
pfunSenceRenewCallBack(); // 环境重置函数
vControlSwitch(sp,pc); // 跳转函数
}
}
}
}
上面是bootloader程序擦写完flash之后判断是否升级成功以及执行跳转函数的代码。流程主要是,升级主函数u8UpdateMode()
(下面是详细代码)中进行数据接收校验以及flash擦写工作,如果擦写成功,该函数返回0(UPDATE-OK)
,擦写失败,该函数返回1(UPDATE-ERROR)
,没有擦写操作,该函数返回2(UPDATE-NO)
。在这个函数中根据相关返回标志进行处理。处理完去读取flash中存放BootLoader标志的地方的数据,如果使我们希望的数据,我们就执行跳转函数,让程序从BootLoader跳转到应用程序中,如果标志不正确,说明升级过程出了问题,我们就不跳转,一直运行在BootLoader程序中。当然,在跳转之前需要执行我们之前提到的环境重置函数pfunSenceRenewCallBack()
。
// 升级主函数
static uint8_t u8UpdateMode(void)
{
uint8_t ret;
if (iap_prepare_sector(APP_START_SECTOR, APP_END_SECTOR) == CMD_SUCCESS) // 准备扇区
{
if (iap_erase_sector(APP_START_SECTOR, APP_END_SECTOR) == CMD_SUCCESS) // 擦除扇区
{
ret = u8Xmodem1kClient(ProgramFlash, (uint16_t)BOOT_DELAYTIME_C, (uint16_t)BOOT_WAITTIME_UPDATE);// 编程指针+X-Modem协议识别
if (0 == ret)
{
return UPDATE_OK;// 返回标志
}
if (2 == ret)
{
return UPDATE_NO;
}
}
}
return UPDATE_ERROR;
}
主函数代码不难理解,进来之后先准备相应的扇区,然后擦除(FLash内部值置全FF),然后启动X-Modem协议接收数据,数据接收完成启动写flash函数ProgramFlash()
进行代码烧写,这一部分在下一节X-Modem-1K.c
中讲。不同的片子的烧写流程不同,这个得看芯片手册,有的需要准备扇区,有的不需要,但是大多数流程都保留了先擦除后烧写的内容。
注意:擦除和烧写之前需要看技术手册搞明白芯片是支持区块擦除还是支持页擦除。
关于X-Modem协议是什么,大家可以自行去百度,这里也不再赘述。
先看代码,比较长:
/**********************************************************
** Xmodem1k协议传输程序
** 参 数: pfunPktHandle,: Xmodem1k协议传输所需函数结构体指针
** u16ShortDly: 轮询发送C字符的时间间隔
** u8LongDly: 等待传输开始超时时限
** 返回值: 传输结果: 0--成功,1--升级失败(错误或取消升级),2--没有升级
**********************************************************/
uint8_t u8Xmodem1kClient(pFunPKTHAND pfunPktHandle, uint16_t u16ShortDly, uint16_t u8LongDly)
{
uint32_t u32ByteCnt = 0; /* 位计数器 计数一包的第几个字节数据 */
uint8_t u8TimeoutCnt = 0; /* 超时次数 */
uint8_t u8DataerrCnt = 0; /* 数据错误次数 */
uint8_t u8PktIndex = 1; /* 包序号期望值 */
uint8_t u8STATE = STAT_IDLE_C; /* 状态变量 */
uint8_t u8Data; /* 存放接收数据及发送命令 */
volatile uint16_t u16PktLen; /* 包中有效数据的长度 */
uint8_t u8Message;
sysTimerClr(1);
while (1)
{
wdt_feed();
switch (u8STATE)
{
case STAT_IDLE_C: /* 轮询发C状态 */
if (sysTimerGet(1) >= u8LongDly )
{
u8STATE = STAT_TIMEOUT_C; /* 等待开始超时,跳到结束状态 */
}
else
{
u8Data = POLL;
do {
u8Message = UART_SendByte(u8Data);
} while (u8Message == UART_NO_SPACE);
sysTimerClr(0);
u8STATE = STAT_IDLE_DATA; /* 跳到轮询读数状态 */
}
break;
case STAT_IDLE_DATA: /* 轮询读数状态 */
if (UART_RecvByte(&u8Data) == UART_SUCCESS)
{
u8STATE = STAT_CONNECT; /* 接收到数据,跳到数据链接状态 */
sysTimerClr(0);
}
else
{
if (sysTimerGet(0) >= (u16ShortDly * SECOND_PER_TICK))
{
u8STATE = STAT_IDLE_C; /* 轮询读数超时,跳回轮询发C */
}
}
break;
case STAT_CONNECT:
if ((u8Data == SOH) || (u8Data == STX)) /* 数据连接状态 SOH--CRC128字节协议 STX--1k协议 */
{
u16PktLen = (u8Data == SOH)? SHORTPKT_LEN : LONGPKT_LEN;
((uint8_t *)ptHead)[u32ByteCnt] = u8Data;
u32ByteCnt++;
u8STATE = STAT_RECEIVE; /* 连接成功,跳到数据接收状态 */
sysTimerClr(2);
}
else
{
u8STATE = STAT_IDLE_C; /* 起始控制字符错,跳回轮询发C */
}
break;
case STAT_RECEIVE: /* 数据接收状态 */
if (UART_RecvByte(&u8Data) == UART_SUCCESS)
{
if (u32ByteCnt < PKT_HEAD_LEN)
{
((uint8_t *)ptHead)[u32ByteCnt] = u8Data; /* 控制字符、序号、序号补码 */
if (ptHead->u8Ctrl == EOT)
{
u8STATE = STAT_ACK;
break;
}
}
else
{
((uint8_t *)puData)[u32ByteCnt - 3] = u8Data; /* 数据段部分(数据、CRC值) */
}
u32ByteCnt++;
if (u32ByteCnt >= u16PktLen + PKT_HEAD_LEN + 2)
{
u8STATE = STAT_HANDLE; /* 包接收结束,跳到数据处理状态 */
}
u8TimeoutCnt = 0;
sysTimerClr(0);
}
else
{ /* 未收到数据,判断超时 */
/* 包间隔最大为1s,字符间隔最大为20ms, 根据包内部和包之间的不同选择不同的超时间隔 */
if (sysTimerGet(0) >= ((u32ByteCnt == 0) ? PKT_TIMEOUT_MS : CHAR_TIMEOUT_MS))
{
sysTimerClr(0);
u8TimeoutCnt++;
u8STATE = STAT_NAK;
}
}
break;
case STAT_HANDLE: /* 数据处理状态 */
{
uint16_t u16CRCTemp;
if (ptHead->u8Ctrl != ((u16PktLen == SHORTPKT_LEN) ? SOH : STX)) /* 检查控制字符是否一致 */
{
u8DataerrCnt++;
u8STATE = STAT_NAK;
break;
}
if (ptHead->u8Index + ptHead->u8Patch != 0xFF) /* 检查序号、序号补码是否完整 */
{
u8DataerrCnt++;
u8STATE = STAT_NAK;
break;
}
if ((ptHead->u8Index) == (u8PktIndex - 1)) /* 检查序号是否为上一包序号, 数据重发的时候,检测是否为上一包序号 */
{
u8STATE = STAT_ACK;
break;
}
if (ptHead->u8Index != u8PktIndex) /* 检查序号是否为期望的包序号 */
{
u8DataerrCnt++;
u8STATE = STAT_NAK;
break;
}
u16CRCTemp = ((uint16_t)(*((uint8_t *)puData + u16PktLen)) << 8) | (*((uint8_t *)puData + u16PktLen + 1));
if (u16CRCVerify((uint8_t *)puData, u16PktLen, 0) != u16CRCTemp)
{
u8DataerrCnt++;
u8STATE = STAT_NAK; /* CRC检查 */
break;
}
if (!pfunPktHandle((uint8_t *)puData, u16PktLen)) // 烧写flash的函数指针,具体怎么烧写查看芯片手册
{
u8PktIndex++;
u8STATE = STAT_ACK; /* 数据处理 */
break;
}
u8DataerrCnt++;
u8STATE = STAT_NAK;
break;
}
case STAT_ACK: /* 正常响应状态(ACK) */
u8Data = ACK;
do {
u8Message = UART_SendByte(u8Data);
} while (u8Message == UART_NO_SPACE);
if (ptHead->u8Ctrl == EOT) /* 结束控制符时进入ACK状态情况 */
{
u8STATE = STAT_END; /* 发送方发送EOT结束传输 */
break;
}
u8DataerrCnt = 0;
u32ByteCnt = 0;
u8STATE = STAT_RECEIVE; /* 正常响应发送ACK后跳到数据接收*/
break;
case STAT_NAK: /* 非正常响应状态(NAK) */
if ((u8DataerrCnt >= 5) || (u8TimeoutCnt >= 5)) /* 发送错误次数或接收超时次数超过5次*/
{
u8STATE = STAT_CAN;
break;
}
u8Data = NAK;
do {
u8Message = UART_SendByte(u8Data);
} while (u8Message == UART_NO_SPACE);
u32ByteCnt = 0;
u8STATE = STAT_RECEIVE;
break;
case STAT_CAN: /* 强制结束状态(CAN) */
u8Data = CAN;
do {
u8Message = UART_SendByte(u8Data);
} while (u8Message == UART_NO_SPACE);
return 1;
case STAT_END: /* 传输结束状态(CAN) */
return 0;
case STAT_TIMEOUT_C:
return 2;
default:
break;
}
}
}
代码虽长,但其实比较好理解。
用了一个while(1)
循环,先默认向上位机发C
确认需要升级,上位机发送来升级数据后根据xmodem协议判断是否正确,然后执行不同的case。直到全部升级完成或者升级失败或者没有升级,也就是之前所说的3中状态,也对应了这个程序中的三个出口(分别为return 0/1/2
)。
看到这,基本上整个升级流程就完成了,包括:
1. 初始化
2. 进入升级主函数
3. 协议判断(烧写flash等)
4. 3种出口,对应三种处理方式
5. 查询BootLoader标志
6. 是否跳转
最后一步很关键的是跳转函数。
跳转函数的基本思想是将芯片的pc指针指向应用程序烧写的起始地址APP_START_Flash
,然后sp
调到APP_START_Flash + 4
的位置也就是复位向量所在的地方,然后开始执行。
下面是跳转函数的代码,该方法对于M0+核的芯片均适用,前提是sp
和PC
要正确。
static void vControlSwitch(unsigned int sp,unsigned int pc)
{
asm("ldr r0, [r0]");
asm("mov sp, r0");
asm("ldr r0, [r1]");
asm("bx r0");
}
如果你是从头看到这的话,我相信你应该会对升级的流程有了大致的理解,以上的代码均可直接运行。相信你能写出更好的更精湛更有效的升级代码来。
限于笔者的能力及精力的限制,以上有可能会有疏漏,如发现有不妥之处,欢迎留言交流。