基于HAL库的STM32F407IGT6的IAP远程程序升级方法
文章目录
- 前言
- 一、IAP基本原理
- 二、CubeMx配置
- 三、代码功能实现
- 总结
前言
在嵌入式产品层出不穷的今天,功能迭代升级需求不可或缺。已经封装好的产品很难拆下来烧录程序,IAP是In Application Programming的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。常见的IAP远程升级方法有:串口和HTTP,本文主要分享HTTP方法。
一、IAP基本原理
应用编程IAP(In-Application Programming)是应用在Flash程序存储器的一种编程模式。它可以在应用程序正常运行的情况下,通过调用特定的IAP程序对另外一段程序Flash空间进行读/写操作,甚至可以控制对某段、某页甚至某个字节的读/写操作,这为数据存储和固件的现场升级带来了更大的灵活性。通常在用户需要实现IAP功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信管道(如USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在User Flash中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:
1. 检查是否需要对第二部分代码进行更新;
2. 如果不需要更新则转到4;
3. 执行更新操作;
4. 跳转到第二部分代码执行。
第一部分代码必须通过其它手段,如JTAG或ISP烧入;第二部分代码可以使用第一部分代码IAP功能烧入,也可以和第一部分代码共同烧入,需要程序更新时再通过第一部分IAP代码更新。我们将第一个项目代码称为Bootloader程序,第二个项目代码称为APP程序,他们存放在STM32F407 FLASH的不同地址范围,一般从最低地址区开始存放Bootloader,其次就是APP程序,这样我们就是要实现2个程序:Bootloader和APP。我们先来看看STM32F4正常的程序运行流程(为了方便说明IAP过程,我们先仅考虑代码全部存放在内部FLASH的情况),如下图所示。
图1 STM32F4程序运行流程
STM32F407 的内部闪存(FLASH)地址起始于 0X0800 0000, 一般情况下,程序文件就从此地址开始写入。此外 STM32F407 是基于 Cortex-M4 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动, 而这张“中断向量表”的起始地址是 0x08000004,当中断来临, STM32F407的内部硬件机制亦会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。在上图中, STM32F407在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生了中断),此时 STM32F407强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
当加入 IAP 程序之后,程序运行流程如下图所示:
图2 STM32F4加入IAP后的程序运行流程
在上图所示流程中, STM32F407 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,此部分同正常的程序运行流程图一样;在执行完 IAP 以后(即将新的 APP 代码写入STM32F407 的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数, 如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32F407 的 FLASH,在不同位置上,共有两个中断向量表。在 main 函数执行过程中,如果 CPU 得到一个中断请求, PC 指针仍然会强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
1. 新程序必须在IAP程序之后的某个偏移量为X的地址开始;
2. 必须将新程序的中断向量表相应的移动,移动的偏移量为X;
二、CubeMx配置
1. 在主界面选择File-->New Project或者直接点击ACCEE TO MCU SELECTOR。
2. 出现芯片型号选择,搜索自己芯片的型号,双击型号,或者点击Start Project进入配置在搜索栏的下面,提供的各种查找方式,可以选择芯片内核、型号等等,可以帮助你查找芯片。本实验选取的芯片型号为:STM32F407IGTx。
3. 配置RCC,使用外部时钟源。
4. 配置调试引脚。
5. 将LED对应的3个引脚(PI5,PI6,PI7)设置为GPIO_Output,参数设置参考下图,请配置完进行核对。
6. 配置SPI引脚模式,PA7--SPI1_MOSI,PB3--SPI1_SCK,PB4--SPI1_MISO,PA15--GPIO_Output(单独设置)。
7. 时钟源设置,选择外部高速时钟源,配置为最大主频。
8. 工程文件的设置, 这里就是工程的各种配置 因为涉及到文件读写,堆栈调大一些。
9. 点击Code Generator,进行进一步配置。
10. 点击GENERATE CODE创建工程。
11. 创建成功,用Keil打开工程即可。
三、代码功能实现
//主函数
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
spi1.initialize();
w5500.initialize();
LED_GREEN_ON;
if(ARM_KEY_STATE == KEY_DOWN)
{ //按键松开状态直接跳向新的应用程序
goto start;
}
if(ARM_KEY_STATE == KEY_UP)
{ //按键按下状态直接跳向远程升级入口
goto IAP;
}
start:
while(1)
{
do_trans(); //程序跳转,运行新程序
}
IAP:
while(1)
{
do_http(); //http处理,进入程序远程升级功能
}
}
/*****************自定义封装函数**************************/ //2024/1/16
void do_trans(void)
{
//判断用户是否已经下载程序,因为正常情况下此地址是栈地址。
//测试用户app地址是不是在APPLICATION_ADDRESS位置。检测栈顶的地址,来检验app是否下载成功
if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000){
//判断栈定地址值是否在0x2000 0000 - 0x 2000 2000之间
//跳转至用户程序,APPLICATION_ADDRESS + 4对应的是app中断向量表的第二项,复位地址
JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
//把地址强转为函数指针
Jump_To_Application = (pFunction) JumpAddress;
//初始化用户程序的堆栈指针,设置主函数栈指针
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
//跳转至应用程序,实际失去app复位地址去执行复位操作
Jump_To_Application();
}else{
led_trade();
}
}
//HTTP处理函数
void do_http(void)
{
// unsigned char ch=SOCK_HTTP; //定义HTTP通信的Scoket端口变量
unsigned short int len;
memset(rx_buf,0x00,MAX_URI_SIZE);
/* http service start */
switch(getSn_SR(0)) //获取Socket SOCK_TCPS状态
{
case SOCK_INIT: //Socket初始化完成
listen(0); //Web服务器监听
break;
// case SOCK_LISTEN: //Socket监听状态
// break;
case SOCK_ESTABLISHED: //Socket连接建立
if(getSn_IR(0) & Sn_IR_CON)
{
setSn_IR(0, Sn_IR_CON); //清除中断标志
}
if ((len = getSn_RX_RSR(0)) > 0)
{
if(len > 1460)len = 1460;
len = recv(0, (unsigned char *)rx_buf, len); //接收客户端的请求并存储
receive_length = len;
*(((unsigned char*)rx_buf)+len) = 0;
proc_http(0, (unsigned char*)rx_buf); //解析HTTP请求,并发送rx_buf
disconnect(0); //断开TCP
}
break;
case SOCK_CLOSE_WAIT: //Socket 连接等待
if ((len = getSn_RX_RSR(0)) > 0)
{
len = recv(0, (unsigned char*)rx_buf, len);
*(((unsigned char*)rx_buf)+len) = 0;
proc_http(0, (unsigned char*)rx_buf); //解析HTTP请求,并发送rx_buf
}
disconnect(0); //断开TCP连接
break;
case SOCK_CLOSED: //Socket关闭
socket(0, Sn_MR_TCP, 80, Sn_MR_ND); //初始化Socket端口
break;
default:
break;
}// end of switch
}
void proc_http(SOCKET s, uint8 * buf)
{
char* name; //get method request file name
char req_name[32]={0x00,}; //post method request file name
unsigned long int file_len = 0;
unsigned short int send_len = 0;
unsigned char* http_response;
st_http_request *http_request;
unsigned long int content_len=0;
unsigned long int rx_len=0;
static long int temp_value;
long int cnt;
char sub[10];
char *p;
long int wr_len = 0;
unsigned long int tmp_len=0;
unsigned char data;
memset(tx_buf,0x00,MAX_URI_SIZE);
http_response = (unsigned char*)rx_buf;
http_request = (st_http_request*)tx_buf;
parse_http_request(http_request, buf); // After analyze request, convert into http_request
//method Analyze
switch (http_request->METHOD)
{
case METHOD_ERR :
memcpy(http_response, ERROR_REQUEST_PAGE, sizeof(ERROR_REQUEST_PAGE));
send(s, (unsigned char *)http_response, strlen((char const*)http_response));
break;
case METHOD_HEAD:
case METHOD_GET:
//get file name from uri
name = http_request->URI;
if(strcmp(name,"/index.htm")==0 || strcmp(name,"/")==0 || (strcmp(name,"/index.html")==0))
{
file_len = strlen(ALLOCATION_HTML);
make_http_response_head((uint8*)http_response, PTYPE_HTML,file_len);
send(s,http_response,strlen((char const*)http_response));
send_len=0;
while(file_len)
{
if(file_len>1024)
{
if(getSn_SR(s)!=SOCK_ESTABLISHED)
{
return;
}
send(s, (uint8 *)ALLOCATION_HTML+send_len, 1024);
send_len+=1024;
file_len-=1024;
}
else
{
send(s, (uint8 *)ALLOCATION_HTML+send_len, file_len);
send_len+=file_len;
file_len-=file_len;
}
}
}
break;
/*POST method*/
case METHOD_POST: //向服务器提交数据,数据放在容器(HTML HEADER)内且不可见
//get file name from uri
mid(http_request->URI, "/", " ", req_name);
if(strcmp(req_name,"upload.cgi") == 0){ //比较文件名是否一致
if(receive_length != 1460 - 5){
temp_value = getSn_RX_RSR(s);
if(temp_value > (1460 - 5 - receive_length)){
temp_value = recv(s, (unsigned char*)rx_buf, 1460 - 5 - receive_length);
}
for(cnt = receive_length;cnt < (1460 - 5);cnt++){
*(http_request->URI + cnt) = rx_buf[cnt - receive_length];
}
receive_length = 1460 - 5;
}
mid(http_request->URI,"boundary=", "\r\n", (char*)boundary);
//get Content-Length
mid((char*)http_request->URI,"Content-Length: ","\r\n",sub);
content_len = ATOI32(sub,10);
p = strstr((char*)http_request->URI,boundary);
p += strlen(boundary);
p = strstr(p,boundary);
rx_len = p - http_request->URI;
rx_len = receive_length - rx_len + 2; //回复的文件大小
p = strstr((char*)http_request->URI,"octet-stream\r\n\r\n");
p += strlen("octet-stream\r\n\r\n");
wr_len = p - http_request->URI;
wr_len = receive_length - wr_len; //请求的文件大小
LED_BLUE_ON;
LED_GREEN_OFF;
HAL_FLASH_Unlock();
FLASH->ACR&=~(1<<10);
/*擦除FLASH*/
for(cnt = FLASH_SECTOR_2;cnt < FLASH_SECTOR_11;cnt ++){
FLASH_Erase_Sector(cnt,FLASH_VOLTAGE_RANGE_3);
}
for(cnt = 0;cnt < wr_len;cnt++){
data = *(p + cnt);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, APPLICATION_ADDRESS + cnt, data);
}
while(rx_len != content_len)
{
red_led_flash();
tmp_len=getSn_RX_RSR(s);
if(tmp_len > 0)
{
if(tmp_len > 1460) tmp_len = 1460;
memset(rx_buf,0x00,MAX_URI_SIZE);
tmp_len=recv(s, (unsigned char*)rx_buf, tmp_len);
for(cnt = 0;cnt < tmp_len;cnt++){
data = *(rx_buf + cnt);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE,APPLICATION_ADDRESS + wr_len + cnt,data);
}
wr_len += tmp_len;
}
rx_len += tmp_len;
if(rx_len == content_len)
{
LED_RED_OFF;
make_upload_response(tx_buf);
sprintf((char *)http_response,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length:%d\r\n\r\n%s",strlen(tx_buf),tx_buf);
send(s, (unsigned char *)http_response, strlen((char *)http_response));
HAL_FLASH_Lock();
LED_BLUE_OFF;
LED_RED_OFF;
LED_GREEN_ON;
//while(1);
//HAL_Delay(3000); //延时3秒
do_trans(); //跳转到应用程序 2024/1/16 add
}
}
}
break;
//get file name from uri
default :
break;
}
}
总结
程序流程如下:
硬件连接方法:
- 把仿真器与单片机哦的SWD调试口相连(直接相连或者通过转换器相连);
- 将跳线帽插在USB UART;
- 连接USB UART至电脑,位系统供电;
- 把单片机的网口通过网线与计算机的网口相连;
- 设置本机IP(192.168.0.2);
- 打开Keil MDK 开发环境,并打开APP实现工程(自己所需的功能);
- 烧写程序到单片机上;
- 也可以进入Debug模式,单步运行或设置断点验证程序逻辑;
- 打开浏览器输入单片机的IP地址(192.168.0.10)。
- 注意:.bin文件生成方法:如下图,键入此内容即可在工程中生成.bin文件。
代码工程链接: https://download.csdn.net/download/hyp12347/88750630