引言
随着SOC技术的不断发展,越来越多的微控制器集成了I2C、SPI、USART、USB等通信接口,这些微控制器往往性价比很高,又方便应用。其中I2C、SPI等接口一般用在微控制器和外围芯片之间通信,而USART、USB等接口往往用于设备和PC之间通信。由于USB具有热插拔、即插即用、速度快等特点,普及率很高,现在很多台式机、笔记本去掉了串口接口,增加了USB接口。从可靠性、易应用性来讲,通过USB接口来升级固件程序是很好的选择。
固件是直接操作硬件的嵌入式代码,它对设备能否正常、可靠的工作,起决定性作用。虽然各公司对其作过很多测试,但有些较为隐蔽的软件缺陷可能是一款产品发布后,客户使用过程中发现的。假如该产品不支持固件升级,那么处理这类问题会十分麻烦。相反,若是有固件升级功能,那么产商在修复这个问题后,可以将新固件和升级方法一并发给客户,客户更新固件就能妥善的解决问题。
本文描述的固件更新实际上是在应用中编程(InApplication Programming, IAP)的一种,它是在微控制器的FLASH上实现IAP功能,这个IAP通过USB通道对主控的FLASH进行再编程。
1 DFU升级概述
DFU是Device Firmware Upgrade的简称,它是通过USB接口的DFU类来实现固件升级。
从图1中可以看出,支持IAP多了一个IAP程序和一个向量表,正是由于IAP和用户程序存放在两个相互独立的区域,才能用IAP程序来再编程用户代码。这两个FLASH空间示意图的起始地址都是FLASH基地址(0x 0800 0000),左图由于增加了IAP程序,它占据低地址(0x 08000000) 开始内部FLASH空间,所以用户应用程序是从一个特定的地址(0x 0800 4000)开始存放的。而右图只有用户程序,其起始地址就是FLASH基地址。另外,在写带有IAP功能的用户程序时必须作相应的调整,主要是中断向量表。
图1 支持/不支持IAP功能固件的FLASH存储空间示意图
IAP程序主要完成两部分功能:① 有用户程序时,跳转到用户程序。② 没有用户程序时,进入DFU状态,通过上位机下载固件。当然我们还可以扩展其功能,比如对硬件的检测等。
2 硬件设计简介
硬件电路大体上分成微控制器部分和外围部分,MCU选用ST公司的STM32F103C8T6。微控制器部分主要由时钟电路(8 MHz是正常工作时使用的,32.768 kHz的是8 MHz故障时使用的),启动模式选择电路和LED控制电路组成。其中,PA6与PC13连接是用于校准HIS的。外围部分主要由JTAG接口、USB接口电路、蜂鸣器控制电路组成。微控制器部分电路、外围部分电路如图2、图3所示。
图2 微控制器部分电路
图3 外围部分电路
3 软件设计
图4是软件的主程序流程,这里涉及到的东西比较多,其中灯和蜂鸣器是提示信号,TIM2用于超时检测,看门狗则用于程序跑飞时对芯片复位、用户程序检测、USB设备初始化等等。其中启动时钟比较重要,但它在主程序之前,是由汇编代码调用的,后面有详细阐述。HIS校准和状态检测序列比较复杂,也有单独的阐述。
图4 主程序流程图
3.1 启动时钟图3外围部分电路
STM32F103C8的启动代码在Keil平台下是startup_stm32f10x_md.s和system_stm32f10x.c两个文件,其中startup_stm32f10x_md.s文件是用汇编写的,主要完成中断向量表、堆栈配置初始化等,它还调用了C语言写的SystemInit函数。新版KEIL把一部分启动代码(SystemInit)用C语言写完后,放在system_stm32f10x.c中。需要改的一般只是system_stm32f10x.c文件,有的工程中会将这两个文件及库文件设成只读,防止重要文件意外改动,出现严重后果。这种只读文件在Keil工程对应的文件中会有个类似锁的标志,若是想修改这种文件,得先在操作系统下把该文件的只读属性去掉,然后对其修改,建议修改完后仍然给它加上只读属性。
STM32F103C8T6的最大时钟是72 MHz,这也是系统的默认时钟。HSI被用作PLL时钟的输入时钟,系统时钟能得到的最大频率是64 MHz。由于USB预分频器是直接挂在PLLCLK上的,而且USB预分频只能是1或者是1.5,而USBCLK必须是48 MHz。由上述关系,得到如下等式:
PLLCLK/1 或者 PLLCLK/1.5 = USBCLK = 48MHz
HIS(8 MHz)/2×PLLMLL(2~16)= PLLCLK
HSE(8 MHz)/2× PLLMLL(2~16)= PLLCLK 或者
HSE(8 MHz)/1× PLLMLL(2~16)= PLLCLK
满足以上等式的值有多组,这里笔者选取了一组比较合适的值,使得使用HIS和HSE时USB预分频值都一样:
PLLCLK = 48 MHz。
USB预分频值=1。
HIS时:PLLMLL=12(8 MHz/2×12=48 MHz)。
HSE时:PLLMLL=6(8 MHz/1×6=48 MHz)。
由于HIS和HSE生成的PLLCLK一样,所以USB预分频值也相同;而当它们生成的PLLCLK不一样时,USB预分频值也不同。假如不小心设成一样,会导致其中一种时钟下USB设备不能识别,无法正常工作。另外,FLASH的访问延迟周期和主频也有关系,要是设置不当会导致读写FLASH失败。
关键代码略——编者注。
3.2 校准内部时钟
这里有两种算法:最小偏差和固定偏差,分别对应HSI_CalibrateMinError()和HSI_CalibrateFixedError()函数。最小偏差会尝试32种的HSITRIM可能值,返回这些值对应的最小偏差的频率(KHz)。固定偏差则从将HSITRIM值设置成16开始,依次尝试两边的值,发现实际绝对值偏差值小于参数的值(Hz)时,返回成功。从上述描述可以看出,两个算法各有优缺点,在本次设计中把IAP程序空间放大了,更好地发挥了这两个函数的优点。HSI_CalibrateFixedError()函数在通常情况下使用,它失败时再调用HSI_CalibrateMinError()函数。
代码略——编者注。
3.3 进入DFU状态序列
通常情况下迫使IAP程序进入DFU升级流程的是设备上电时某个按钮已按下。这部分描述的是进入DFU状态的序列,它可以作为一个不用按钮来触发的例子。
正常情况下卡槽就是用来插卡,然后对卡操作的,所以这个序列的设计就必须避开正常使用卡槽的情况,还得考虑用户程序正在操作CPU卡时,完成看门狗等复位后,才可以正常进入用户程序。所以要求设备上电前,卡槽里必须插入一张正常的CPU卡,这是IAP程序进入DFU状态序列的第一步,如果不满足上述条件则直接跳转到用户程序。每一个状态检测成功后,蜂鸣器鸣两声,然后进入下一状态。由于单片机运行速度较快,还没等用户装备好下一状态,它就检测到当前状态不是它想要的,然后用红灯闪烁提示。这时候按流程准备即可,等所有的状态条件都满足后,就进入了DFU状态,流程图如图5所示。
图5 进入DFU状态流程图
关键代码:
boolstate_test(u8 state_no) //状态号:1-4
{
…
if (SC_Detect() == 0) {//假如卡槽里已插卡
_CardSetPower(PCSC1,POWERUP);//卡槽1上电
PCSC_STATE[PCSC1] = SC_POWER_OFF;//设置卡槽1为下电状态
value = SC_Reset((u8 *)&Send_Buffer[4], (u16 *)&len); //对CPU复位
}
else {
if (state_no != 4) //假如当前状态为4,返回假
{ return FALSE; }
}
switch (state_no & 0x7) {
case 1:
case 3:
if ((value == 0x9000) && (Send_Buffer[4] == 0x3B)) {//假如复位结果是0x9000,并且卡片返回的第一个字节是0x3B
ret_value = TRUE;//设置返回值为真
operation_success();//蜂鸣器鸣两声
}
else {
operation_fail();//红灯闪烁
}
break;
case 2:
if (value != 0x9000) {//假如复位失败
ret_value = TRUE;//设置返回值为真
operation_success();//蜂鸣器鸣两声
}
else {
if (PCSC_STATE[PCSC1]!= SC_POWER_OFF) {//卡槽1当前是非下电状态
if (TIM2_TIMEOUT != TRUE) {//假如当前没超时
TIM_Cmd(TIM2, ENABLE);//使能超时定时器TIM2
}
}
operation_fail();//红灯闪烁
}
break;
case 4:
if (value != 0x9000) {//假如复位失败
DelayMs(220);//演示220ms
if (SC_Detect() == 1) {//假如卡槽1没卡
ret_value = TRUE;//设置返回值为真
operation_success();//蜂鸣器鸣两声
}
}
else {
operation_fail();//红灯闪烁
}
break;
…
}
3.4 用户程序设计
由于增加了IAP功能,用户程序需要做一些改动,否则无法正常运行。
在Keil平台下改动部分如下:
①修改Target选项中IROM1的开始地址和大小。设芯片内部FLASH的基地址为BASE_FLASH,FLASH的大小为Size_FLASH。IROM1的默认设置值适合没有IAP程序,开始地址对应BASE_FLASH,大小则对应为Size_FLASH。由于增加了IAP程序,它存放于BASE_FLASH开始的一片空间,用户程序存放的基地址不再是BASE_FLASH。设IAP程序所需空间为Size_iap_code,预留空间为Size_reserve,IAP所占空间为Size_iap,那么Size_iap=Size_iap_code+Size_reserve。另外由于FLASH操作的限制,Size_iap可能要做调整,使其程序代码FLASH页的整数倍。用户程序的开始地址是FLASH的基地址+Size_iap,大小是Size_FLASH- Size_iap。虽然一般情况下只要设对开始地址,大小保持不变都能正常运行,但是这么做会有隐患。
②修改system_stm32f10x.c文件中的“#define VECT_TAB_OFFSET0x0”,将其大小改为上面得到的Size_iap。这个宏定义是在该文件的SystemInit函数中使用的,代码如下:
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
//将向量中断表映射到合适的内部FLASH空间
在用户程序中加入清固件功能,那么就可以通过上位机方便地操作,使设备进入DFU状态,然后升级固件。需要注意的是,在清固件后最好紧接着执行复位命令,而不要在中间执行过多无关的操作。当然可以将擦除和复位放在一个函数中,不过需要解决回应上位机的擦除固件命令问题,比较合适的方法是通过USB中断传输发回应信息。
关键代码如下:
void CMD_Restore(void){
FLASH_Unlock();//解锁内部FLASH控制器,以便后面擦除
FLASH_ErasePage(0x8004000);//擦除0x8004000开始的一页空间
……
}
void CMD_Reset(void){
USB_Cable_Config(DISABLE);//断开USB使能线
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x00);
//将向量中断表映射到内部FLASH的基地址
NVIC_SystemReset();//系统复位
}
4 固件升级及测试
工具软件包下载地址为:
http://www.st.com/web/catalog/tools/FM147/CL1794/SC961/SS1533/PF257916#.
通常情况下IDE开发环境生成hex、bin或是s19格式的文件,但是该文件仅仅是可执行的原始数据,它们还缺少执行更新的必要信息,比如厂商ID、产品ID、版本号等。有了这些信息,升级才更安全。DFU文件就是用工具软件在hex、bin、s19格式的文件上加入升级所需要的必要信息后生成的文件。
经测试,在HES异常时,IAP也能顺利地跳转到用户程序(假如用户程序没有处理HES异常的情况,那么也无法正常工作)、顺利地进入DFU模式及更新固件;通过上位机擦除固件命令,再进入DFU模式升级固件也正常。
结语
本文介绍的可靠固件升级技术,虽然会多占用4个I/O引脚(通过增加一个I/O控制另外2个I/O是否相连,可以减少到3个引脚)以及芯片内部的FLASH存储空间,但是它增强了稳定性、可靠性。在HES异常时,仍然能够正常地升级、跳转到用户程序等,而这是传统的固件升级无法做到的。本文介绍了两种升级方法互为补充:通常情况下可以通过上位机去擦固件的方式升级固件;当用户程序工作异常无法完成清固件时,采用DFU检测序列来升级固件。