STM32串口IAP


让STM32的应用程序能够通过串口在线升级,这就是STM32的串口IAP。要实现串口升级,简单来说,就是给STM32编写一个bootloader引导程序,就想计算机的BIOS一样,在这段代码中接收串口的数据,然后将数据固化到STM32内部指定的flash地址空间中,接着再跑到这段代码执行。

接触过Linux uboot的应该会注意到,除了功能的实现外,bootloader的界面设计也非常重要。通过串口在计算机的串口软件中实现一个简洁的界面,列出bootloader的各项功能,并支持用户选择。实现这样一个人际交互的界面也是一个优秀的bootloader必不可少的一部分。
本文就要讲讲如何设计这个有功能有界面的bootloader程序!还是基于我自己的规范工程。
1、工程的修改
1)串口升级当然需要用到USART与FLASH了,我的原工程已经添加了串口的库文件stm32f10x_usart.c与stm32f10x_flash.c,所以就不需要在添加这两个文件了。
2)拷贝《STM32多路软定时器》一文中编写SoftTimer.c与SoftTimer.h文件分别保存在工程文件的BSP文件下的src与inc文件夹中,并将SoftTimer.c文件添加到BSP工程组中。
3)既然用到了定时器,所以将库文件stm32f10x_tim.c文件添加到 STM32F10x_StdPeriod_Driver工程组中。
4) 打开stm32f10x_conf.h文件,将原先屏蔽的:“#include stm32f10x_tim.h”语句的屏蔽去掉。
5) 新建IAP.c和IAP.h 两个文件分别保存到BSP文件夹下的src与inc两个文件中。并将IAP.c文件添加到BSP工程组中。
工程的架构如下图所示:
STM32串口IAP - ziye334 - ziye334的博客
 
2、IAP.c与IAP.h文件的编写
考虑到开发板资源,我采用串口1作为升级的通道,所以原先在规范工程中作为调试接口的串口1的相关代码需要从BSP.c与BSP.h两个文件中完全删除掉。
按照之前的习惯,在IAP.c第一需要编写的函数应为:IAP_Init(),在这个函数中,配置串口相关的代码,如配置串口引脚,串口时钟,串口属性,串口中断等,具体的代码如下:
 
  

/*************************************************************
Function : IAP_Init
Description: IAP初始化函数,初始化串口1
Input : none
return : none
*************************************************************/
void IAP_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB2PeriphClockCmd(COM1_RCC, ENABLE);//使能 USART1 时钟
RCC_APB2PeriphClockCmd(COM1_GPIO_RCC, ENABLE);//使能串口2引脚时钟

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//配置 USART1 的Tx 引脚类型为推挽式的
GPIO_InitStructure.GPIO_Pin = COM1_TX_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(COM1_GPIO_PORT, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//配置 USART1 的Rx 为输入悬空
GPIO_InitStructure.GPIO_Pin = COM1_RX_PIN;
GPIO_Init(COM1_GPIO_PORT, &GPIO_InitStructure);

USART_InitStructure.USART_BaudRate = 115200;//设置波特率为115200
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//设置数据位为8位
USART_InitStructure.USART_StopBits = USART_StopBits_1;//设置停止位为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_ITConfig(COM1, USART_IT_RXNE, ENABLE);//接收中断使能
USART_Init(COM1,&USART_InitStructure);//串口2相关寄存器的配置
USART_Cmd(COM1,ENABLE); //使能串口2

NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//通道设置为串口2中断
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;//中断占优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//打开中断
NVIC_Init(&NVIC_InitStructure);
}

串口初始化完毕后,当然顺带得将发送与接收的函数编写完,有与上面配置了中断接收,所以在IAP.c文件中,只需要编写串口发送函数就可以了:
 
  

/*************************************************************
Function : IAP_SerialSendByte
Description: 串口发送字节
Input : c-要发送的字节
return : none
*************************************************************/
static void IAP_SerialSendByte(u8 c)
{
USART_SendData(COM1, c);
while (USART_GetFlagStatus(COM1, USART_FLAG_TXE) == RESET) {}
}
/*************************************************************
Function : IAP_SerialSendStr
Description: 串口发送字符串
Input : none
return : none
*************************************************************/
void IAP_SerialSendStr(u8 *s)
{
while(*s != '\0')
{
IAP_SerialSendByte(*s);
s++;
}
}

至于串口接收数据的相关实现需要借助一个环形的缓冲区。所谓的环形缓冲本质就是定义一个有限的数组UsartBuffer[MAXBUFFER],然后通过两个变量:UsartWptr和UsartRptr分别标注当前环形缓冲区的读与写位置,当接收到一个数据,写位置变量 UsartWptr就会向后偏移一个位置;当读走一个数据, UsartRptr也会向后偏移一个位置;当 UsartWptr等于 UsartRptr时,说明缓冲区中没有新的数据,否者存在未读数据。环形缓冲能够大大节省处理器的资源,它具体的原理也不在多说,下面看看它的实现:
 
  

#define MAXBUFFER 512 //缓冲大小
u8 UsartBuffer[MAXBUFFER]; //数据缓冲区
u16 UsartWptr = 0;
u16 UsartRptr = 0;

/*************************************************************
Function : IAP_BufferWrite
Description: 写缓冲区
Input : none
return : none
*************************************************************/
void IAP_BufferWrite(void)
{
if(UsartWptr == (UsartRptr - 1))//缓冲区存满了
{
return;//返回
}

UsartBuffer[UsartWptr] = USART_ReceiveData(COM1);//存取串口数据
UsartWptr++;//缓冲写位置值自增
UsartWptr = UsartWptr%MAXBUFFER;//保证写位置值不溢出
}

/*************************************************************
Function : IAP_BufferRead
Description: 读缓冲区
Input : none
return : none
*************************************************************/
static u8 IAP_BufferRead(u8 *data)
{
if(UsartRptr == UsartWptr)//无数据可读
{
return 0;
}
*data = UsartBuffer[UsartRptr];//读取缓冲区数据
UsartRptr++;//读位置值自增
UsartRptr = UsartRptr % MAXBUFFER;//保证读位置值不溢出
return 1;
}

接下去先讲讲bootloader的界面,然后在在根据界面中的功能选项再将具体的实现方法。bootloader的界面需要设计得简洁明了,下面就是我自己设计的界面:
 
  

/*************************************************************
Function : IAP_ShowMenu
Description: 显示菜单界面
Input : none
return : none
*************************************************************/
void IAP_ShowMenu(void)
{
IAP_SerialSendStr("\r\n+================(C) COPYRIGHT 2014 Ziye334 ================+");
IAP_SerialSendStr("\r\n| In-Application Programing Application (Version 1.0) |");
IAP_SerialSendStr("\r\n+----command----+-----------------function------------------+");
IAP_SerialSendStr("\r\n| 1: FWUPDATA | Update the firmware to flash |");
IAP_SerialSendStr("\r\n| 2: FWERASE | Erase the current firmware |");
IAP_SerialSendStr("\r\n| 3: BOOT | Excute the current firmware |");
IAP_SerialSendStr("\r\n| 4:REBOOT | Reboot |");
IAP_SerialSendStr("\r\n| ?: HELP | Display this help |");
IAP_SerialSendStr("\r\n+===========================================================+");
IAP_SerialSendStr("\r\n\r\n");
IAP_SerialSendStr("STM32-IAP>>");
}

上面的界面是不是设计得很漂亮。界面用各种符号实现了一个类似表格的界面,界面的第一行强调了版权所属以及设计时间,第二行在讲明bootloader的总体功能及版本,之后的几行就是具体罗列bootloader能实现的具体功能及说明,最后还有设计“STM32-IAP>>"字样,提示输入选择。
先实现界面中罗列的第一个功能:FWUPDATA即固件升级。要实现固件升级自然就是将数据固化到芯片flash中去的过程。 flash空间的程序烧写代码如下:
 
 

extern u8 rcvTimeout; //接收超时标志

/*************************************************************
Function : IAP_UpdataProgram
Description: 更新程序
Input : none
return : none
*************************************************************/
static void IAP_UpdataProgram(void)
{
u8 blockNum = 0; //块,一块4页,对于STM32F10X_HD来说,1页2Kbytes
u8 n = 0;
u8 data = 0;
u8 datalow = 0;
u8 datahigh = 0;
u32 UserMemoryMask = 0;

rcvTimeout = 0; //清除接收超时标志位
blockNum = (IAP_ADDR - FLASH_BASE_ADDR) >> 12; //计算flash块
UserMemoryMask = ((u32)(~((1 << blockNum) - 1)));//计算掩码
//查看块所在区域是否写保护
if((FLASH_GetWriteProtectionOptionByte() & UserMemoryMask) != UserMemoryMask)
{
FLASH_EraseOptionBytes (); //关闭写保护
}
while(1)
{
switch(n)
{
case 0:
if(IAP_BufferRead(&data)) //接收地字节数据
{
datalow = data;
n = 1;
}
else
{
break;
}
case 1:
if(IAP_BufferRead(&data)) //接收高字节数据
{
datahigh = data;
n = 0;
IAP_FlashProgramdata(((u16)(datalow)) | ((u16)(datahigh << 8)));
}
if(rcvTimeout)//接收超时,错误或者接收结束
{
datahigh = 0xff;
n = 0;
IAP_FlashProgramdata(((u16)(datalow)) | ((u16)(datahigh << 8)));
}
default:
break;
}
if(rcvTimeout)//接收超时
{
break;
}
}
}

要想烧写程序到的flash空间中去,首先需要检测这段空间是否写保护了,如果被写保护了,则关闭写保护。接着在while(1)中每次循环就读读取2个字节拼凑成半字,然后调用 IAP_FlashProgramdata()函数将半字烧写进flash中。这里有人会问,为什么不凑齐4个字节即一个字的时候,再烧写进flash中?这样做自然也可以,但是代码实现相对复杂。下面在给出 IAP_FlashProgramdata()函数代码:
 
  

/*************************************************************
Function : IAP_FlashProgramdata
Description: 烧写数据
Input : data-要烧写的数据
return : none
*************************************************************/
static void IAP_FlashProgramdata(u16 data)
{
static u32 flashwptr = IAP_ADDR;

FLASH_Unlock();//flash上锁
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);//清除flash相关标志位
FLASH_ProgramHalfWord(flashwptr, data); //烧写半字数据
if(flashwptr == IAP_ADDR)//开始烧写
{
IAP_SerialSendStr("\r\nUpdating firmware to 0x8005000 ");
}
IAP_SerialSendStr(".");//显示烧写进程
flashwptr = flashwptr + 2;//移动烧写地址
FLASH_Lock();//flash解锁
}

在每次烧写flash一个字或者半字时需要将Flash解锁,如果没有解锁,就不会烧写成功的,在烧写完后在将flash锁上,这么做是出于安全考虑。代码中IAP_ADDR就是升级的目标flash地址,在IAP.h中定义,这里我定义的地址是0x8005000,在最开始烧写时,会在串口界面上显示:"Updating firmware to 0x8005000"信息。然后每烧写完成后打印'.',这样的话就可以在串口上看到烧写的进度了。
再来实现界面中的第二个功能:FWERASE擦除flash空间代码。这里的擦除指的是擦除IAP_ADDR地址后的flash空间。需要注意的是flash的擦除并不像烧写一样可以一个字或半字地进行操作,对于擦除来说,它的最小单位是page页,它只能实现页擦除,每一页的也的大小更具处理器的类型而略有不同,在IAP.h中会给出差异的。flash擦除的代码如下:
 
  

/*************************************************************
Function : IAP_FlashEease
Description: 擦除Flash
Input : none
return : none
*************************************************************/
static void IAP_FlashEease(void)
{
u16 eraseCounter = 0;
u16 nbrOfPage = 0;

nbrOfPage = (FLASH_BASE_ADDR + FLASH_SIZE - IAP_ADDR)/PAGE_SIZE;//计算page数

FLASH_Unlock(); //解除flash擦写锁定
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);//清除flash相关标志位
for(eraseCounter = 0; eraseCounter < nbrOfPage; eraseCounter++)//开始擦除
{
IAP_SerialSendStr(".");//显示进度
FLASH_ErasePage(IAP_ADDR + (eraseCounter * PAGE_SIZE));//擦除
}
FLASH_Lock();//flash擦写锁定
}

再来实现界面中的第三个功能:BOOT启动。BOOT启动即程序运行指定空间代码。这个功能的实现需要一些技巧,还是先来看它的相关代码:
 
  

typedef void (*pFunction)(void);
pFunction Jump_To_Application;
u32 JumpAddress; //跳转地址

/*************************************************************
Function : IAP_JumpToApplication
Description: 跳转到升级程序处
Input : none
return : none
*************************************************************/
void IAP_JumpToApplication(void)
{
if(((*(__IO u32 *)IAP_ADDR) & 0x2FFE0000) == 0x20000000)//有升级代码,IAP_ADDR地址处理应指向主堆栈区,即0x20000000
{
JumpAddress = *(__IO u32 *)(IAP_ADDR + 4);//获取复位地址
Jump_To_Application = (pFunction)JumpAddress;//函数指针指向复位地址
__set_MSP(*(__IO u32*)IAP_ADDR);//设置主堆栈指针MSP指向升级机制IAP_ADDR
Jump_To_Application();//跳转到升级代码处
}
}

代码中可以看到,在最开始处,自定义了一个无参数无返回值的函数指针,然后定义了指向这种结构的函数指针 Jump_To_Application,还定义了一个变量 JumpAddress。 JumpAddress 获取升级代码复位处地址( JumpAddress = *(__IO u32 *)(IAP_ADDR + 4);)然后函数指针 Jump_To_Application指向这个代码,接下去设置主堆栈地址( __set_MSP(*(__IO u32*)IAP_ADDR); )其中 __set_MSP函数在core_cm3.c中定义。函数指针指向升级空间复位地址处以及主堆栈地址指向升级空间主堆栈地址处,这两个条件满足后,就可以执行升级程序了( Jump_To_Application(); )。这里不能忽略的是if里的条件,条件肯呢过理解起来有困难,这里稍微解释下:判断 IAP_ADDR地址处即升级代码的指向的主堆栈地址是不是0x20000000(RAM空间的起始地址),如果是说明有升级代码,如果不是则说明升级处没有升级代码。
再来实现界面中的第四个功能:REBOOT重启。这里的REBOOT的意思可能跟你理解有出入,我将重新显示界面功能作为REBOOT功能。这里没有给出指定函数来实现,后面会讲到这个功能的实现。
最后再实现界面的最后一个功能:HELP帮助功能。代码如下:
 
  

/*************************************************************
Function : ShwHelpInfo
Description: 显示帮助信息
Input : none
return : none
*************************************************************/
static void ShwHelpInfo(void)
{
IAP_SerialSendStr("\r\nEnter '1' to updtate you apllication code!");
IAP_SerialSendStr("\r\nRnter '2' to erase the current application code!");
IAP_SerialSendStr("\r\nEnter '3' to go to excute the current application code!");
IAP_SerialSendStr("\r\nEnter '4' to restart the system!");
IAP_SerialSendStr("\r\nEnter '?' to show the help infomation!\r\n");
}

这样的界面的功能相关的函数都编写好了,下面需要在编写一个用户选择功能的函数,这个函数中根据用户的选择调用对应的功能函数实现。也就是说这个函数实现了与用户的交互。在将这个函数之前,需要编写一个函数IAP_GetKey()用来获取用户的输入的值,代码如下:
 
  

/*************************************************************
Function : IAP_GetKey
Description: 获取键入值
Input : none
return : 返回键值
*************************************************************/
static u8 IAP_GetKey(void)
{
u8 data;
while(!IAP_BufferRead(&data)){}//从缓冲区中获取键值
return data;
}

当调用这个函数时,没有收到串口数据,函数会一直阻塞。
接下去就要讲讲怎么实现这段交互代码了,代码如下:
 
  

/*************************************************************
Function : IAP_WiatForChoose
Description: 功能选择
Input : none
return : none
*************************************************************/
void IAP_WiatForChoose(void)
{
u8 c = 0;

while (1)
{
c = IAP_GetKey();//获取键值
IAP_SerialSendByte(c);//串口返回键值
switch(c)
{
case '1': //FWUPDATA固件升级
if((IAP_GetKey() == '\r'))//检测回车键
{
IAP_SerialSendStr("\r\nErasing...");
IAP_FlashEease();//擦除Flash
IAP_SerialSendStr("\r\nErase done!\r\n");
IAP_SerialSendStr("Please send firmware file!\r\n");
IAP_UpdataProgram();//烧写升级代码
IAP_SerialSendStr("\r\nFirmware update done!\r\n");
IAP_SerialSendStr("Booting...\r\n");
Delay_ms(500);
NVIC_SystemReset();//重启
}
break;
case '2'://FWERASE固件擦除
if((IAP_GetKey() == '\r'))//检测回车键
{
IAP_SerialSendStr("\r\nErasing...");
IAP_FlashEease();//擦除Flash
IAP_SerialSendStr("\r\nErase done!\r\n");
return;//退出循环
}
break;
case '3'://BOOT执行升级程序
if((IAP_GetKey() == '\r'))
{
IAP_SerialSendStr("\r\nBotting...\r\n");
if(((*(__IO u32 *)IAP_ADDR) & 0x2FFE0000) != 0x20000000)
{
IAP_SerialSendStr("No user program! Please download a firmware!\r\n");
}
Delay_ms(500);
NVIC_SystemReset();
return;//退出循环
}
break;
case '4'://REBOOT系统重启
if((IAP_GetKey() == '\r'))//检测回车键
{
IAP_SerialSendStr("\r\nRebooting...\r\n");
return;//退出循环
}
break;
case '?'://HELP帮助
if((IAP_GetKey() == '\r'))
{
ShwHelpInfo();//显示帮助信息
return;//退出循环
}
break;
default:
IAP_SerialSendStr("\r\nInvalid Number! The number should be either 1、2、3、4or5\r\n");
return;//退出循环
}
}
}

这是一个无线循环函数,在显示完界面后,就会等待用户选择输入( c = IAP_GetKey(); ),接着将用户输入的值回显在界面上( IAP_SerialSendByte(c); ),然后根据输入的值执行对应功能代码(switch(c)..case语句),如果输入选项不是制定的功能则会提示信息。用户的输入需要按下键盘的回车才会有效,所以在case语句中,需要再等待用户出入回车( if((IAP_GetKey() == '\r')) ),当用户按下回车,才会正真执行对应的代码。当用户选择'1'时,开始固件升级,要想烧写程序,先要擦除flash空间才能烧写,所以这个功能只要调用IAP_FlashEease()和IAP_UpdataProgram()这两个函数并加些进度提示就可以了,最后再软件重启下就可以了;当用户选择'2'即擦除flash空间,则直需调用 IAP_FlashEease()函数擦写flash空间,然后return跳出循环退出函数。当用户选择'3',BOOT,先判断下升级处代码是否有效(if(((*(__IO u32 *)IAP_ADDR) & 0x2FFE0000) != 0x20000000) ),如果无效,则显示提示信息,然后软件重启下;当用户选择‘4’REBOOT,则显示相关信息,然后退出函数;当用户选择'?'HELP时,则调用ShwHelpInfo()显示帮助信息。
这样的话IAP.c文件代码就全部写完了,下面在各处IAP.h的代码:
 
  

#ifndef __IAP_H__
#define __IAP_H__
#include "stm32f10x.h"

#define FLASH_BASE_ADDR 0x8000000 //Flash基地址
#define IAP_ADDR 0x8005000 //升级代码地址

#if defined (STM32F10X_MD) || defined (STM32F10X_MD_VL)
#define PAGE_SIZE (0x400) // 1 Kbyte
#define FLASH_SIZE (0x20000) // 128 KBytes
#elif defined STM32F10X_CL
#define PAGE_SIZE (0x800) // 2 Kbytes
#define FLASH_SIZE (0x40000) // 256 KBytes
#elif defined STM32F10X_HD || defined (STM32F10X_HD_VL)
#define PAGE_SIZE (0x800) // 2 Kbytes
#define FLASH_SIZE (0x80000) // 512 KBytes
#elif defined STM32F10X_XL
#define PAGE_SIZE (0x800) // 2 Kbytes
#define FLASH_SIZE (0x100000) // 1 MByte
#else
#error "Please select first the STM32 device to be used (in stm32f10x.h)"
#endif

void IAP_Init(void);
void IAP_SerialSendStr(u8 *s);
void IAP_ShowMenu(void);
void IAP_WiatForChoose(void);
void IAP_BufferWrite(void);
void IAP_JumpToApplication(void);

#endif

在这个.h文件中可以看到我们的升级处地址IAP_ADDR为0x8005000,下面还有定义了不同型号的STM32对应的页大小以及flash空间,我使用的是STM32F103ZET6处理器,属于 STM32F10X_HD系列处理器,所以他的有0x80000即512K的flash空间,它的页大小为2K。

3、stm32f10x_it.c文件的修改
由于使用了我之前写的软定时器,所以需要编写它的中断服务函数:
 
  

/*************************************************************
Function : TIM2_IRQHandler
Description: 定时器2中断服务程序
Input : none
return : none
*************************************************************/
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
SoftTimer_TimerExecute();
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}

还要编写串口的中断服务程序:
 
  

/*************************************************************
Function : USART1_IRQHandler
Description: 串口1中断服务程序
Input : none
return : none
*************************************************************/
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)//检测接收中断标志位
{
SoftTimer_TimerStart(0, 100, RcvTimeoutSet, (void*)0, TIMER_ONESHOT);//开启软定时器1
IAP_BufferWrite();//存放接收到的数据
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}

在串口的中断函数中,启动了一个软定时器,定时时间为100ms,即用于定时相邻两个字节的间隔是否超时。调用 IAP_BufferWrite()将接收到的数据保存在环形缓冲区中。                                     
既然开启了一个软定时器,还需要编写它的超时回调函数,在这个函数中,将超时标志位置1,代码如下:
 
   

u8 rcvTimeout = 0; //接收超时标志

/*************************************************************
Function : RcvTimeoutSet
Description: 串口接收数据超时
Input : parameter-参数
return : none
*************************************************************/
void RcvTimeoutSet(void *parameter)
{
rcvTimeout = 1; //设置接收超时标志
}


4、main函数的编写
串口升级的流程一般是:根据一个按键是否按下,在决定是升级程序还是执行升级后的代码,所以需要先初始化一个按键以及检测函数。我的开发板PA8d对应的就是一个按键,它的代码如下:
 
   

/*************************************************************
Function : KeyInit
Description: 初始化按键
Input : none
return : none
*************************************************************/
void KeyInit (void)
{
GPIO_InitTypeDef GPIO_InitStructure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA , &GPIO_InitStructure);
}

/*************************************************************
Function : GetKey
Description: 获取按键状态
Input : none
return : none
*************************************************************/
u8 GetKey (void)
{
return (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8));
}

然后就也可以编写main函数了,代码如下:
 
   

/*************************************************************
Function : main
Description: main入口
Input : none
return : none
*************************************************************/
int main(void)
{
BSP_Init();//板子初始化
KeyInit();//初始化按键
if(!GetKey ())//按键按下,进入升级界面
{
set: IAP_Init();//初始化串口
SoftTimer_Init();//初始化软定时器
shw: IAP_ShowMenu();//显示功能菜单
IAP_WiatForChoose(); //等待选择界面
goto shw;//重新显示界面
}
else
{
IAP_JumpToApplication();//跳转到升级出代码执行
goto set;//没有升级程序或者升级程序错误才会执行到这句,然后天转到升级界面
}
}

当系统上电过后,没有检测到按键按下,则跳到升级代码去执行,如果存在升级代码,程序是不会执行到 IAP_JumpToApplication()后面的goto set;语句的,但如果不存在升级代码,则会利用goto跳到串口升级的代码中去让用户去下载升级代码。
当系统上电后,检测到按键按下,就去指向串口升级代码。代码执行到 IAP_WiatForChoose()函数时会更具用户选择会否跳出这个函数,如果跳出了这个函数,就会执行goto语句跳到shw代码处,即重新显示一下界面。
这里可能有人提出:你怎么用goto语句,不是说最好不要用goto语句吗?其实,之所以限制goto的使用是怕代码的错乱。但是如果你在一个函数中使用goto语句,而且goto的目标也在这个函数中,这么使用反而减少了代码量增加了理解,正如上面的代码,goto与goto的目的地都在main函数中,明眼人一看就知道程序的流程。


5、测试
当系统上电的时候,因为没有升级程序,所以自动会进入串口升级程序中,最先会显示下面的界面:
STM32串口IAP - ziye334 - ziye334的博客
 
所以下面来下载升级程序,输入1,按下回车,先会擦除flash空间就会提示你输入升级文件:
STM32串口IAP - ziye334 - ziye334的博客
  在Transfer->Send Binary,选择要升级的.bn文件,然后串口就开始烧写收到的数据,并打印进度,程序烧写完之后就会自动运行升级后的代码,如下:
STM32串口IAP - ziye334 - ziye334的博客
  开发板重新上电,并按下按键,又会显示菜单,然后输入2,回车,就开始擦除flash,擦除完成后,就会重新显示界面如下:
STM32串口IAP - ziye334 - ziye334的博客
  接下去用户输入3,回车,就会执行升级后的代码,如下:
STM32串口IAP - ziye334 - ziye334的博客
  系统重新上电,同时按下按键,又会进入到这个菜单界面,输入4,回车,就会重新显示菜单界面,如下:
STM32串口IAP - ziye334 - ziye334的博客
再输入?,回车,就会显示帮助信息,然后又会重新显示菜单界面,如下:
STM32串口IAP - ziye334 - ziye334的博客

猜你喜欢

转载自blog.csdn.net/qingzhuyuxian/article/details/80769057