单片机不同的程序下载方式
ICP
ICP是指在电路中编程。使用厂家配套的软件或仿真器进行程序烧录,目前主流的有JTAG接口和SWD接口,常用的烧录工具为J-Link、ST-Link等。在程序开发阶段,通常在连接下载器的情况下直接使用编程软件进行程序下载调试。在MDK软件中可以选择不同的下载器。一般用于调试阶段,实际产品中的电路板一般会密封在外壳中,不便于用下载器烧录程序。
ISP
在系统中编程。以STM32为例,其内置了一段Bootloader程序,可以通过更改BOOT引脚电平来运行这段程序,再通过ISP编程工具(flymcu通过串口烧录程序)将程序下载进去。下载完毕之后,再更改BOOT至正常状态,使得MCU运行所下载的程序。这种方法需要更改boot模式,实际工程中也不方便。
IAP
在应用中编程。IAP可以使用微控制器支持的任一种通信接口(如I/O端口、USB、CAN、UART、I2C、SPI等)下载程序或数据到FLASH中。IAP允许用户在程序运行时重新烧写FLASH中的内容。但需要注意,IAP要求至少有一部分程序(Bootloader)已经使用ICP或ISP烧到FLASH中。一般情况下,产品的外壳都会留有通信接口,若能通过这种通信方式对程序进行升级,则可以省去拆装的麻烦。在此基础上,若引入远距离或无线数据传输方案,更可以实现远程编程或无线编程。
通常实现 IAP 功能时,需要在设计固件程序时编写两个项目代码,第一个项目程序(bootloader)不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码(APP)才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是bootloader开始运行,它作如下操作:
①检查是否需要对APP代码进行更新
②如果不需要更新则转到④
③执行更新操作
④跳转到APP执行
bootloader必须通过其它手段,如 JTAG 或 ISP 烧入;APP代码可以使用bootloader的 IAP 功能烧入,也可以和boot loader一起烧入,以后需要程序更新时再通过boot loader更新。 他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader(0x08000000),紧跟其后的就是 APP 程序。
判断是否需要更新APP的方法:
1.通过拨码开关、跳线帽等方式设定单片机某一引脚电平状态,程序通过读取引脚电平判断是否需要升级。此种方式需要接触板卡进行操作,当板卡被封闭在外壳中或安装于不便于操作位置时很难实现。
2.软件内设定一标志位(变量),通过判断标志位状态判断是否需要升级。该标志位状态掉电不能改变,故需要存储在外部EEPROM或单片机内部FLASH中。若存储在外部EEPROM,则需要增加额外的电路;若存储在单片机内部FLASH,由于FLASH每次写入都需要擦除一整页,会造成资源浪费。
3.单片机每次上电首先进入BootLoader程序,在BootLoader中等待一定时间,若上位机软件在该时间段内发起通讯,则停留在Bootloader程序中等待固件升级;若该时间段内无通讯,则跳转到正常的APP程序。该方式每次上电都要等待一定时间,需要考虑是否可以接受。
STM32程序运行流程
正常程序运行流程
STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求,此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
加入IAP后程序运行流程
STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到bootloader的 main 函数,如图标号①所示,此部分同上图 一样;在执行完boot loader以后(即将新的 APP 代码写入 STM32的 FLASH,新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
①新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
②必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
不同型号STM32的FLASH大小
BootLoader程序和APP 程序存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 BootLoader,紧跟其后的就是 APP 程序。在进行FLASH空间划分之前,首先需要了解一下不同型号STM32单片机的FLASH大小。下图中的闪存容量就是FLASH的大小。
对于不同容量的STM32F1系列产品,其FLASH页大小是不同的,具体的容量划分规则如下: 小容量产品:FLASH容量在16K至32K字节之间。
中容量产品:FLASH容量在64K至128K字节之间。
大容量产品:FLASH容量在256K至512K字 节之间。
对于小容量和中容量的产品,其页大小为1K,对于大容量产品,其页大小为2K。
用户程序写在flash的主存储器中,对于小容量产品来说,FLASH每页1K,最多32页,也就是32K。 对于中容量产品来说,FLASH每页1K,最多128页,也就是128K。对于大容量产品来说,FLASH每页2K,最多256页,也就是512K,大容量产品的flash模块组织如图:
keil中配置bootloader和APP
配置boot loader
在进行FLASH空间划分时,必须知道编写的程序占用FLASH空间大小。用MDK软件进行工程编译之后会生成一个.map文件,在该文件末尾可找到程序需要占用的FLASH空间。在实际设计过程中,主要是确定boot loader的占用空间,在FLASH起始位置给Bootloader留出足够的空间,以便确定APP的起始地址,也就是中断向量表的偏移量。在实际分配过程中,可以给Bootloader多一些空间,以便后续对Bootloader的功能拓展。
在MDK软件配置项中,可以对程序的起始位置以及大小进行设置。对于BootLoader程序来说,只需要设置其Size,该值可根据刚才map文件中的值进行预估。0x08000000就是FLASH的起始地址,这里设置bootloader的大小为0x4000,也就是16k。
配置APP
BootLoader程序按照正常的程序编写即可。而APP程序由于其下载位置与默认程序下载位置不同,故需要做一些特殊的配置。首先是APP 程序起始地址设置。起始位置即去除BootLoader程序之后剩余空间的首地址。一般设定为某一页的首地址,因为FLASH写入之前必须进行页擦除,在下图中按照Bootloader的size可得APP首地址为0x08004000。Size <= FLASH原始大小 - 偏移量(0x4000),例子中是STM32F103C8T6,FLASH大小为64K。上面分给Bootloader的大小为0x4000,也就是16K,那么size最大就是48k,也就是0xc000。如下图所示。
接着设置中断向量表的偏移量,在主函数起始位置(也就是main函数的第一句话)添加:
SCB->VTOR = FLASH_BASE | 0x4000; //0x4000即BootLoader大小(偏移量)
bin文件生成
IAP过程中传输的数据文件一般为后缀名为bin的文件,该文件内容与正常烧录进FLASH中的数据内容一致,便于程序升级。但是MDK软件并不能直接生成bin文件,需要进行一些配置。这里我们通过 MDK 自带的格式转换工具 fromelf.exe,来实现.axf 文件到.bin 文件的转换。该工具在 MDK 的安装目录\ARM\BIN40 文件夹里面。我们通过在 MDK 点击 Options for Target→User 选项卡,在 After Build/Rebuild 栏,勾选 Run #1,并写入:D:\MDK5.21A\ARM\ARMCC\bin\fromelf.exe --bin -o ..\OBJ\TEST.bin ..\OBJ\TEST.axf,如下图所示。加粗部分根据实际MDK安装路径进行修改,TEST.bin和TEST.axf中的TEST根据工程名称修改。
除了上述方法生成bin文件还可以在网上下载hex2bin等软件来生成bin文件。
代码解析
以正点原子的串口IAP实验为例,该实验通过串口接收APP的bin文件,通过按键控制固件更新和APP代码执行。
bootloader代码
串口接收部分
#define USART_REC_LEN 41*1024 //定义最大接收字节数 41K
u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1
}
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
if(USART_RX_CNT<USART_REC_LEN)
{
USART_RX_BUF[USART_RX_CNT]=Res;//将读取到的数据存到缓冲区
USART_RX_CNT++;
}
}
main函数
#define FLASH_APP1_ADDR 0x08004000 //第一个应用程序起始地址(存放在FLASH)
int main(void)
{
u8 t;
u8 key;
u16 oldcount=0; //老的串口接收数据值
u16 applenth=0; //接收到的app代码长度
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
delay_init(); //延时函数初始化
uart_init(115200); //串口初始化为256000
LED_Init(); //初始化与LED连接的硬件接口
KEY_Init(); //按键初始化
while(1)
{
if(USART_RX_CNT)//如果串口接收到的数据不为空
{
if(oldcount==USART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.
{
applenth=USART_RX_CNT;//得到数据长度
oldcount=0;
USART_RX_CNT=0;
printf("用户程序接收完成!\r\n");
printf("代码长度:%dBytes\r\n",applenth);
}else oldcount=USART_RX_CNT;
}
key=KEY_Scan(0);//按键扫描
if(key==WKUP_PRES) //WK_UP按键按下
{
if(applenth)//如果接收到的数据不为空
{
printf("开始更新固件...\r\n");
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);//更新FLASH代码
printf("固件更新完成!\r\n");
}else
{
printf("非FLASH应用程序!\r\n");
}
}else
{
printf("没有可以更新的固件!\r\n");
}
}
if(key==KEY1_PRES)//KEY1按下
{
printf("开始执行FLASH用户代码!!\r\n");
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}else
{
printf("非FLASH应用程序,无法执行!\r\n");
}
}
}
}
检查地址合法性解析
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)
//0X20001000是串口接收数据缓冲区的起始地址,接收的数据是APP,0X20001000处存放的是栈顶地址,0X20001000 + 4处存放的是中断向量表起始地址,也就是复位中断服务函数的地址,对其进行强制类型转换,然后解引用,得到复位中断服务函数的地址,判断地址是不是在flash范围内
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)
//上面将串口缓冲区接收到的数据存到了flash中,FLASH_APP1_ADDR处存放的东西和上面0X20001000存放的东西是一样的
跳转到APP代码解析
typedef void (*iapfun)(void); //重命名一个函数指针类型.
iapfun jump2app; //创建一个函数指针jump2app
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}
检查栈顶地址解析
STM32的RAM起始地址为0x20000000,上述代码中appxaddr处存放的是栈顶地址,栈顶地址在RAM中,appxaddr+4处存放的是复位中断向量,在FLASH中。
appxaddr是存放栈顶地址的指针,所以对appxaddr解引用就得到了栈顶地址。由于栈顶地址应该在RAM中,
1.对于128kRAM的芯片,堆栈地址的空间是0x20000000 - 0x2001FFFF,地址与上0x2FFE0000,就是得到bit17~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十七位不需要判断。
2.对于48KRAM的芯片,堆栈地址空间是0x20000000 - 0x2000BFFF,地址应该与上0x2FFFC000,就是得到bit14~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十四位不需要判断。
3.对于512KRAM的芯片,堆栈地址空间是0x20000000 - 0x2007FFFF,地址应该与上0x2FF80000,就是得到bit19~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十九位不需要判断。
ST32最早的时候内部ram的大小基本在128Kb以内,RAM小于128K的芯片使用0x2FFE0000也基本没问题,就一直在沿用。
APP跳转解析
jump2app=(iapfun)*(vu32*)(appxaddr+4);
(vu32*)(appxaddr+4)//将复位中断向量地址值强制转化为32位的指针变量
*(vu32*)(appxaddr+4)//取出复位中断向量地址保存的数据=复位中断的中断服务程序入口地址
(iapfun)*(vu32*)(appxaddr+4)//将中断服务程序入口地址再转化成定义的函数指针类型
jump2app=(iapfun)*(vu32*)(appxaddr+4);//中断服务程序入口地址(iapfun类型的函数指针)赋值jump2app(定义的iapfun类型的函数指针其名为jump2app)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //调用函数且jump2app函数的起始地址在上面赋值,可实现PC指针的跳转;