IAP(In Application Programming)即在应用编程, IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写。利用TCP实现IAP即可以实现对应用代码的远程更新,可以对后期应用程序的维护和更新节省很大的人力物力。
在进行应用程序更新时,应用程序是不能正常运行的,所以更新时我们需要运行另一段代码。因此我们需要在STM32中写入两段代码:Bootloader 和 APP。Bootloader 主要进行引导进入应用程序和进行应用程序更新,而APP是我们需要执行的应用程序。
Bootloader 是进行引导应用程序的,所以我们必须保证Bootloader 的稳定性,且不能随意更新,要让Bootloader 具有“不死之身”。 系统开机一般首先执行 Bootloader,也许这里你会有疑问,我们这里的Bootloader不过是用来更新APP的,开机直接运行到APP,需要更新时再跳转到Bootloader执行不行吗?我们前面说过Bootloader 具有不死之身,而我们的APP的不过是肉体凡胎。由于反复的更新等操作,都可能随意引入未知的BUG,这些未知的BUG可能随时导致我们的程序崩溃。如果我们的APP崩溃了,而我们的系统开机就执行APP,那么就再也没有办法进入Bootloader了,那么我们的板子也没有办法进行更新了,这时候就只能进行“返厂”重新烧写了。我们每次开机执行Bootloader,在跳转到APP,这样即便APP崩溃,Bootloader还是可以正常运行,我们就可对APP进行更新。本文中我们是用TCP来进行APP的更新,所以Bootloader 中我们必定要包含TCP通信功能。Bootloader所实现的内容如下:(1)开机执行Bootloader;(2)启动后等待建立TCP连接;(3)接收TCP发送的APP,收到“start”后,将APP写入flash并执行;(4)20s未收到数据,则执行APP,避免系统卡在Bootloader。
APP是由Bootloader拉起,在运行过程中我们也需要进行程序更新,所以在APP中我们也需要能够远程再次跳转到Bootloader。所以APP除本身的功能外,还应该具备以下功能:(1)建立TCP连接;(2)收到“boot”后跳转Bootloader执行。
本次实验采用的是正点原子ALIENTEK 战舰 STM32F103 开发板,且代码借鉴了正点原子的例程。本次实验代码仅用于学习交流,请勿用作其他商业用途。本文中仅贴上部分关键代码,源码请下载源码工程。
Bootloader的主函数位于TCP IAP\TCP Bootloader\USER\main.c,这里面我们主要是建立了TCP服务端,然后LED0闪烁,如果20s没有收到数据就跳转到flash的APP执行。
int main(void)
{
u32 t;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
TIM3_Int_Init(1000,719);//定时器3频率为100hz
FSMC_SRAM_Init(); //初始化外部SRAM
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMEX); //初始化外部内存池
lwip_comm_init();//lwip初始化
tcp_server_test(); //TCP Server模式
while(1)
{
lwip_periodic_handle(); //LWIP内核需要定时处理的函数
lwip_pkt_handle();
delay_ms(2);
t++;
wait_time++;
if(t==200)
{
t=0;
LED0=!LED0;
}
if(wait_time==10000)//20s没有接收到数据
{
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}
}
}
TCP数据的处理位于TCP IAP\TCP Bootloader\LWIP\lwip_app\tcp_server_demo\tcp_server_demo.c. tcp_server_recvbuf用来临时存放我们的接收缓存,tcp_server_appdata用来存放我们接受的应用文件,因为比较大,所以存放在外部SRAM的地址中。我们在malloc中已经占有了部分地址,所以这里从0X68000000+MEM2_MAX_SIZE+MEM2_ALLOC_TABLE_SIZE*2开始。
//TCP Server接收数据缓冲区
u8 tcp_server_recvbuf[TCP_SERVER_RX_BUFSIZE];
u8 tcp_server_appdata[TCP_SERVER_RX_APPSIZE] __attribute__ ((at(0X68000000+MEM2_MAX_SIZE+MEM2_ALLOC_TABLE_SIZE*2)));
Bootloader中的接受处理逻辑:1.每次收到数据我们都将wait_time=0,清除等待时间,将每次接受到的数据都存在了tcp_server_recvbuf,因为tcp接受的数据会分片,然后我们将每次的数据组装放入u8 tcp_server_appdata。2.如果收到"clean",我们就将tcp_server_appdata清空。3.如果收到"start",我们先判读是否有效,然后将其写到falsh,再跳转运行,这时开始执行app。
//lwIP tcp_recv()函数的回调函数
err_t tcp_server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
err_t ret_err;
u32 data_len = 0;
struct pbuf *q;
struct tcp_server_struct *es;
LWIP_ASSERT("arg != NULL",arg != NULL);
es=(struct tcp_server_struct *)arg;
if(p==NULL) //从客户端接收到空数据
{
es->state=ES_TCPSERVER_CLOSING;//需要关闭TCP 连接了
es->p=p;
ret_err=ERR_OK;
}else if(err!=ERR_OK) //从客户端接收到一个非空数据,但是由于某种原因err!=ERR_OK
{
if(p)pbuf_free(p); //释放接收pbuf
ret_err=err;
}else if(es->state==ES_TCPSERVER_ACCEPTED) //处于连接状态
{
if(p!=NULL) //当处于连接状态并且接收到的数据不为空时将其打印出来
{
memset(tcp_server_recvbuf,0,TCP_SERVER_RX_BUFSIZE); //数据接收缓冲区清零
wait_time=0;//清除等待时间
for(q=p;q!=NULL;q=q->next) //遍历完整个pbuf链表
{
//判断要拷贝到TCP_SERVER_RX_BUFSIZE中的数据是否大于TCP_SERVER_RX_BUFSIZE的剩余空间,如果大于
//的话就只拷贝TCP_SERVER_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据
if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len)) memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len));//拷贝数据
else memcpy(tcp_server_recvbuf+data_len,q->payload,q->len);
data_len += q->len;
if(data_len > TCP_SERVER_RX_BUFSIZE)
{
data_len=TCP_SERVER_RX_BUFSIZE;
break; //超出TCP客户端接收数组,跳出
}
}
tcp_server_flag|=1<<6; //标记接收到数据了
lwipdev.remoteip[0]=tpcb->remote_ip.addr&0xff; //IADDR4
lwipdev.remoteip[1]=(tpcb->remote_ip.addr>>8)&0xff; //IADDR3
lwipdev.remoteip[2]=(tpcb->remote_ip.addr>>16)&0xff;//IADDR2
lwipdev.remoteip[3]=(tpcb->remote_ip.addr>>24)&0xff;//IADDR1
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
pbuf_free(p); //释放内存
ret_err=ERR_OK;
if(memcmp(tcp_server_recvbuf,"clean",5)==0)
{
printf("清空数据!\r\n");
memset(tcp_server_appdata,0,TCP_SERVER_RX_APPSIZE);
appdata_len=0;
}
else if(memcmp(tcp_server_recvbuf,"start",5)==0)
{
printf("开始更新固件...\r\n");
if(((*(vu32*)((0X68000000+MEM2_MAX_SIZE+MEM2_ALLOC_TABLE_SIZE*2)+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_write_appbin(FLASH_APP1_ADDR,tcp_server_appdata,appdata_len);//更新FLASH代码
printf("固件更新完成!\r\n");
printf("开始执行FLASH用户代码!!\r\n");
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
//lwip_comm_mem_free();
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}else
{
printf("非FLASH应用程序,无法执行!\r\n");
}
}else
{
printf("非FLASH应用程序!\r\n");
}
}
else
{
memcpy(tcp_server_appdata+appdata_len,tcp_server_recvbuf,data_len);
appdata_len=appdata_len+data_len;
printf("正在接收数据 %d byte...\r\n",appdata_len);
}
}
}else//服务器关闭了
{
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
es->p=NULL;
pbuf_free(p); //释放内存
ret_err=ERR_OK;
}
return ret_err;
}
APP的主函数位于\USER\main.c,主要是建立了TCP服务端,以及闪灯。
int main(void)
{
u32 t;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
TIM3_Int_Init(1000,719);//定时器3频率为100hz
FSMC_SRAM_Init(); //初始化外部SRAM
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMEX); //初始化外部内存池
lwip_comm_init();//lwip初始化
tcp_server_test(); //TCP Server模式
while(1)
{
lwip_periodic_handle(); //LWIP内核需要定时处理的函数
lwip_pkt_handle();
delay_ms(2);
t++;
if(t==200)
{
t=0;
LED1=!LED1;
}
}
}
APP的接收处理函数比较简单,只要一收到"boot",则跳转到Bootloader代码段执行。
//lwIP tcp_recv()函数的回调函数
err_t tcp_server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
err_t ret_err;
u32 data_len = 0;
struct pbuf *q;
struct tcp_server_struct *es;
LWIP_ASSERT("arg != NULL",arg != NULL);
es=(struct tcp_server_struct *)arg;
if(p==NULL) //从客户端接收到空数据
{
es->state=ES_TCPSERVER_CLOSING;//需要关闭TCP 连接了
es->p=p;
ret_err=ERR_OK;
}else if(err!=ERR_OK) //从客户端接收到一个非空数据,但是由于某种原因err!=ERR_OK
{
if(p)pbuf_free(p); //释放接收pbuf
ret_err=err;
}else if(es->state==ES_TCPSERVER_ACCEPTED) //处于连接状态
{
if(p!=NULL) //当处于连接状态并且接收到的数据不为空时将其打印出来
{
memset(tcp_server_recvbuf,0,TCP_SERVER_RX_BUFSIZE); //数据接收缓冲区清零
for(q=p;q!=NULL;q=q->next) //遍历完整个pbuf链表
{
//判断要拷贝到TCP_SERVER_RX_BUFSIZE中的数据是否大于TCP_SERVER_RX_BUFSIZE的剩余空间,如果大于
//的话就只拷贝TCP_SERVER_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据
if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len)) memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len));//拷贝数据
else memcpy(tcp_server_recvbuf+data_len,q->payload,q->len);
data_len += q->len;
if(data_len > TCP_SERVER_RX_BUFSIZE)
{
data_len=TCP_SERVER_RX_BUFSIZE;
break; //超出TCP客户端接收数组,跳出
}
}
tcp_server_flag|=1<<6; //标记接收到数据了
lwipdev.remoteip[0]=tpcb->remote_ip.addr&0xff; //IADDR4
lwipdev.remoteip[1]=(tpcb->remote_ip.addr>>8)&0xff; //IADDR3
lwipdev.remoteip[2]=(tpcb->remote_ip.addr>>16)&0xff;//IADDR2
lwipdev.remoteip[3]=(tpcb->remote_ip.addr>>24)&0xff;//IADDR1
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
pbuf_free(p); //释放内存
ret_err=ERR_OK;
if(memcmp(tcp_server_recvbuf,"boot",4)==0)
{
printf("进入Bootloader...\r\n");
iap_load_app(0x8000000);
}
}
}else//服务器关闭了
{
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
es->p=NULL;
pbuf_free(p); //释放内存
ret_err=ERR_OK;
}
return ret_err;
}
网络配置LWIP\lwip_app\lwip_comm\lwip_comm.c中的lwip_comm_default_ip_set中
//lwip 默认IP设置
//lwipx:lwip控制结构体指针
void lwip_comm_default_ip_set(__lwip_dev *lwipx)
{
//默认远端IP为:192.168.1.100
lwipx->remoteip[0]=192;
lwipx->remoteip[1]=168;
lwipx->remoteip[2]=1;
lwipx->remoteip[3]=104;
//MAC地址设置(高三字节固定为:2.0.0,低三字节用STM32唯一ID)
lwipx->mac[0]=dm9000cfg.mac_addr[0];
lwipx->mac[1]=dm9000cfg.mac_addr[1];
lwipx->mac[2]=dm9000cfg.mac_addr[2];
lwipx->mac[3]=dm9000cfg.mac_addr[3];
lwipx->mac[4]=dm9000cfg.mac_addr[4];
lwipx->mac[5]=dm9000cfg.mac_addr[5];
//默认本地IP为:192.168.1.30
lwipx->ip[0]=192;
lwipx->ip[1]=168;
lwipx->ip[2]=1;
lwipx->ip[3]=88;
//默认子网掩码:255.255.255.0
lwipx->netmask[0]=255;
lwipx->netmask[1]=255;
lwipx->netmask[2]=255;
lwipx->netmask[3]=0;
//默认网关:192.168.1.1
lwipx->gateway[0]=192;
lwipx->gateway[1]=168;
lwipx->gateway[2]=1;
lwipx->gateway[3]=1;
lwipx->dhcpstatus=0;//没有DHCP
}
因为Bootloader和APP运行于不同的地址段中,所以我们需要设置程序的起始地址,点击 Options for Target>Target 选项卡,下图是Bootloader,默认的条件下,图中 IROM1 的起始地址(Start)一般为 0X08000000,大小(Size)为 0X80000,即从 0X08000000 开始的 512K 空间为我们的程序存储(因为我们的 STM32F103ZET6 的 FLASH大小是 512K)。
APP需要进行地址偏移,设置起始地址(Start)为 0X08010000,即偏移量为 0X10000(64K字节),因而,留给 APP 用的 FLASH 空间(Size)只有 0X80000-0X10000=0X70000(448K 字节)大小了。
MDK 默认生成的文件是.hex 文件,我们用作 IAP文件是.bin 文件,这样可以方便进行 IAP 升级。这里我们通过 MDK 自带的格式转换工具 fromelf.exe,来实现.axf 文件到.bin 文件的转换。该工具在 MDK 的安装目录\ARM\BIN40 文件夹里面。在 MDK 点击 Options for Target>User 选项卡,在 After Build/Rebuild 栏,
勾选 Run #1,并写入: D:\Keil\MDK525\ARM\ARMCC\bin\fromelf.exe --bin -o …\OBJ\APP1.bin …\OBJ\APP1.axf。这样便后就能在\OBJ目录生成bin文件。
最后我们给如何实现远程更新呢?我们的实验比较粗糙,我们直接使用网络调试助手实现:
1.在开发板中下载Bootloader程序,接入网络,使电脑和开发板在同一局域网并且能相互ping通。
2.在电脑上打开网络调试助手,选择TC Pclient、板子的ip和端口号,建立连接。
3.在网络调试助手启用文件数据源,选择APP的bin文件,发送完毕后再发送"start"即可执行。
4.如果已经在APP,可在建立连接后发送“boot”,进入Bootloader。
本文仅供学习交流,请勿用于商业用途。