前言
IAP(In-Application Programming) 指MCU可以在系统中获取新代码并对自己重新编程,即可用程序来改变程序。在应用编程(IAP)是用户的应用代码对片内Flash存储器进行擦除/编程的方法。这种方式的典型应用就是用一小段代码来实现程序的下载,实际上单片机的ISP功能就是通过IAP技术来实现的,即片子在出厂前就已经有一段小的boot程序在里面,片子上电后,开始运行这段程序,当检测到上位机有下载要求时,便和上位机通信,然后下载数据到数据存储区,从而实现固件升级。
本代码仅仅对文件头四个字节是否为0x5A5A5E5E和尾四个字节是否为0xA5A5E5E5进行判断,除此之外没有其他正确性验证,需要按需添加。
本代码基于官方lwip与flash示例代码修改而来。
一、工程环境
选yes的话软件会自动帮你配置该开发板的所有外设以及系统,选no则不会,在意内存消耗,并且没有其他功能需求就选no。我有其他需求这里选择了yes。
因为我选择了yes大部分东西已经自动配置好了,这里告诉选择no的人需要修改的地方,可以自己参考,具体参数这里就不给出了,重新生成一个yes的配置就可以参考。
时钟树默认即可
配置完成后,生成代码并且用keil打开。
二、添加boot代码
1.入口
//启动标志
unsigned int x = 0x5A5A5E5E;
void Start_BootLoader(void)
{
/*
测试代码
功能:在app1没有能力修改标志位时,将标志位清除,避免标志位锁死
后续将该处删除并且在app1中修改标志位后软重启即可实现需要功能
*/
if(0 == n)
{
n++;
WriteFlash(0X080BFFFC, &n, 1);
}
/*
功能选择代码
功能:通过读取标志位的值,进入不同功能
1.启动app1
2.将app2中的升级程序写入到app1
......
*/
switch (Read_Start_Mode())
{
case Startup_Normol://正常启动Startup_Normol
{
//printf("\nNormal start......\n");
IAP_ExecuteApp(Application_1_Addr);
break;
}
case Startup_Update://升级Startup_Update
{
//printf("> Start update......\r\n");
MoveCode(Application_2_Addr, Application_1_Addr, Application_Size);
WriteFlash(0X080BFFFC, &x, 1);//修改标志位,写入跳转标志
//printf("> Update down......\r\n");
break;
}
default:
break;
}
/*
以太网接收升级程序代码
功能:连接以太网
*/
while(Read_Start_Mode() != Startup_Normol && Read_Start_Mode() !=Startup_Update)
{
MX_LWIP_Process();
}
}
2.跳转
typedef void (*pFunction)(void);
void IAP_ExecuteApp (uint32_t App_Addr)
{
pFunction JumpToApp;
if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶
{
//__set_FAULTMASK(1);
__set_PRIMASK(1);
//设置所有时钟到默认状态
HAL_RCC_DeInit();
//关闭滴答定时器,复位到默认值
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
//关闭所有中断,清除所有中断挂起标志
for (int i = 0; i < 8; i++)
{
NVIC->ICER[i]=0xFFFFFFFF;
NVIC->ICPR[i]=0xFFFFFFFF;
}
//使能全局中断
__set_PRIMASK(0);
__set_CONTROL(0);
JumpToApp = (pFunction) * ( __IO uint32_t *)(App_Addr + 4); //第二个字开始
//MSP( * ( __IO uint32_t * ) App_Addr ); //初始化堆栈
__set_MSP(*(__IO uint32_t*) App_Addr);
JumpToApp();
while(1);
}
}
3.获得升级数据
void MoveCode(unsigned int src_addr, unsigned int des_addr, unsigned int byte_size)
{
//擦掉app1
//printf("> Start erase des flash......\r\n");
Erase_page(des_addr, 0x0807FFFF);
//printf("> Erase des flash down......\r\n");
//接收数组
unsigned int temp[256];
/*
读app2写app1代码
功能:从app2的第五个字节开始,写入到app1中
......
*/
for(int i = 0; i < byte_size/1024; i++)
{
ReadFlash((src_addr + i*1024 +4), temp, 256);//写入app2数据的头四个字节为0x5A5A5E5E需要跳过
WriteFlash((des_addr + i*1024), temp, 256);
}
//printf("> Copy down......\r\n");
Erase_page(Application_2_Addr, 0x080BFFFF);
//校验数据
}
4.扇区擦除
/*static*/ int Erase_page(uint32_t Start_Addr, uint32_t End_Addr)
{
uint32_t FirstSector = 0, NbOfSectors = 0;
HAL_FLASH_Unlock();
FirstSector = GetSector(Start_Addr);//起始扇区
NbOfSectors = GetSector(End_Addr) - FirstSector + 1;//擦除数
//擦FLASH
FLASH_EraseInitTypeDef FlashSet;
FlashSet.TypeErase = FLASH_TYPEERASE_SECTORS;//扇区擦除FLASH_TYPEERASE_MASSERASE块擦除
FlashSet.VoltageRange = FLASH_VOLTAGE_RANGE_3;
FlashSet.Sector = FirstSector;
FlashSet.NbSectors = NbOfSectors;
/*设置PageError,调用擦除函数*/
uint32_t PageError = 0;
HAL_FLASHEx_Erase(&FlashSet, &PageError);
HAL_FLASH_Lock();
return 1;
}
5.标志位读取
unsigned int Read_Start_Mode(void)
{
unsigned int mode = 0;
ReadFlash(0X080BFFFC, &mode, 1);
return mode;
}
6.数据写入
/*static*/ void WriteFlash(uint32_t addr, uint32_t * buff, uint16_t word_size)
{
//解锁flash
HAL_FLASH_Unlock();
// 写入flash
for(int i = 0;i < word_size ; i++)
{
if(HAL_OK == HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,addr + 4 * i ,buff[i]))
{
// HAL_Delay(1);
}
}
//flash上锁
HAL_FLASH_Lock();
}
7.获取地址所在扇区
static uint32_t GetSector(uint32_t Address)
{
uint32_t sector = 0;
if((Address < ADDR_FLASH_SECTOR_1) && (Address >= ADDR_FLASH_SECTOR_0))
{
sector = FLASH_SECTOR_0;
}
else if((Address < ADDR_FLASH_SECTOR_2) && (Address >= ADDR_FLASH_SECTOR_1))
{
sector = FLASH_SECTOR_1;
}
else if((Address < ADDR_FLASH_SECTOR_3) && (Address >= ADDR_FLASH_SECTOR_2))
{
sector = FLASH_SECTOR_2;
}
else if((Address < ADDR_FLASH_SECTOR_4) && (Address >= ADDR_FLASH_SECTOR_3))
{
sector = FLASH_SECTOR_3;
}
else if((Address < ADDR_FLASH_SECTOR_5) && (Address >= ADDR_FLASH_SECTOR_4))
{
sector = FLASH_SECTOR_4;
}
else if((Address < ADDR_FLASH_SECTOR_6) && (Address >= ADDR_FLASH_SECTOR_5))
{
sector = FLASH_SECTOR_5;
}
else if((Address < ADDR_FLASH_SECTOR_7) && (Address >= ADDR_FLASH_SECTOR_6))
{
sector = FLASH_SECTOR_6;
}
else /* (Address < FLASH_END_ADDR) && (Address >= ADDR_FLASH_SECTOR_7) */
{
sector = FLASH_SECTOR_7;
}
return sector;
}
8.读取数据
static void ReadFlash(uint32_t addr, uint32_t * buff, uint16_t word_size)
{
for(int i =0; i < word_size; i++)
{
buff[i] = *(volatile uint32_t*)(addr + 4 * i);
}
return;
}
三、添加tcp代码
1.tcp初始化
void tcp_echoserver_init(void)
{
//套接字
tcp_echoserver_pcb = tcp_new();
if(tcp_echoserver_pcb != NULL)//创建成功
{
err_t err;
err = tcp_bind(tcp_echoserver_pcb, IP_ADDR_ANY, Port_Number);//绑定ip以及端口
if (err == ERR_OK)
{
tcp_echoserver_pcb = tcp_listen(tcp_echoserver_pcb);//开始监听
// printf("开始监听 \r\n");
tcp_accept(tcp_echoserver_pcb, tcp_echoserver_accept);//等待连接
// printf("挂载客户端连接回调函数 \r\n");
}
else
{
memp_free(MEMP_TCP_PCB, tcp_echoserver_pcb);
}
}
}
记得在主函数中调用。
2.处理tcp连接信息
static err_t tcp_echoserver_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
err_t ret_err;
struct tcp_echoserver_struct *es;
LWIP_UNUSED_ARG(arg);
LWIP_UNUSED_ARG(err);
tcp_setprio(newpcb, TCP_PRIO_MIN);//新建立的TCP连接设置最低优先级
// printf("收到客户端连接请求,设置刚连接的客户端为最低优先级 \r\n");
// printf ("客户端 IP address: %s\r\n", iptxt);
es = (struct tcp_echoserver_struct *)mem_malloc(sizeof(struct tcp_echoserver_struct));
if (es != NULL)
{
es->state = ES_ACCEPTED;
es->pcb = newpcb;//将TCP PCB与新数据结构关联
es->retries = 0;//初始化重试计数器
es->p = NULL;
// printf("为新连接的客户端挂载需要的回调函数及 调用参数 \r\n");
tcp_arg(newpcb, es);//将新分配的 es 结构作为参数传递给 newpcb
tcp_recv(newpcb, tcp_echoserver_recv);//初始化LWIP tcp_recv NewPCB的回调函数
tcp_err(newpcb, tcp_echoserver_error);//初始化NewPCB的LWIP tcp_err的回调函数
tcp_poll(newpcb, tcp_echoserver_poll, 0);//初始化LWIP tcp_poll NewPCB的回调函数
tcp_sent(newpcb, tcp_echoserver_sent);//初始化LWIP tcp_sent 的回调函数
ret_err = ERR_OK;
}
else
{
tcp_echoserver_connection_close(newpcb, es);//关闭连接
// printf("tcp_echoserver_struct 内存申请失败 关闭连接 \r\n");
ret_err = ERR_MEM;
}
return ret_err;
}
3.处理收到的数据
static err_t tcp_echoserver_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
struct tcp_echoserver_struct *es;
struct pbuf *ptr = NULL;
uint16_t t = 0, s = 0;
err_t ret_err;
//unsigned int State = 0;
LWIP_ASSERT("arg != NULL",arg != NULL);
// printf("收到客户端数据\r\n");
es = (struct tcp_echoserver_struct *)arg;
if(p == NULL)//从客户端接收到一个空帧,关闭连接
{
// printf("收到断开连接请求 \r\n");
es->state = ES_CLOSING;//断开远程客户端
if(es->p == NULL)
{
tcp_echoserver_connection_close(tpcb,es);
}
else//数据未发送完成
{
// printf("发送的数据还未发送完成 \r\n");
// printf("装载发送完成回调函数 \r\n");
tcp_echoserver_send(tpcb, es);
}
ret_err = ERR_OK;
}
else if(err != ERR_OK)//从客户端收到非空帧,但由于某种原因err != ERR_OK
{
if(p != NULL)//释放p
{
es->p = NULL;
pbuf_free(p);
}
ret_err = err;
}
else if(es->state == ES_ACCEPTED)//保存并发送数据
{
rxdatalen = p->len;//长度
if(rxdatalen > RX_DATA_SIZE)
{
// printf("receive data too long\n");
while(1);
}
/*
接收缓存代码
功能:将phy中的数据缓存写入数组
*/
ptr = p;
while(ptr != NULL)
{
memset(rxdata,0,RX_DATA_SIZE);
for(; s < ptr->len; t++, s++)
{
rxdata[t] = *((uint8_t*)ptr->payload + s);
}
ptr = ptr->next;
s = 0;
}
/*
接收数据处理代码
功能:在识别到标准数据头后开始写入
NumBering可作校验和
*/
if(rxdata[0] == 0x5A && rxdata[1] == 0x5A && rxdata[2] == 0x5E && rxdata[3] == 0x5E)
{
int i = 0;
for(;i < rxdatalen; i++ )//将收到的数据包进行处理转化为纯数据
{
if( i % 4 == 0)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i];
}
else if( i % 4 == 1)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i] << 8;
}
else if( i % 4 == 2)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i] << 16;
}
else if( i % 4 == 3)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i] << 24;
}
//my_send_data(tcp_echoserver_pcb, &rxdata[i], 4);
}
tcp_write(tpcb, RelayData, 1, TCP_WRITE_FLAG_COPY);//回复客户端,观察写入数据
Erase_page(Application_2_Addr, 0x080BFFFF);//擦除app2
WriteFlash((Application_2_Addr), RelayData, rxdatalen/4);//写入app2
NumBering += rxdatalen;//累加字节数
memset(RelayData,0,TR_DATA_SIZE*4);//清零
}
else if(NumBering > 0)
{
int i = 0;
for(;i < rxdatalen; i++ )//将收到的数据包进行处理转化为纯数据
{
if( i % 4 == 0)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i];
}
else if( i % 4 == 1)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i] << 8;
}
else if( i % 4 == 2)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i] << 16;
}
else if( i % 4 == 3)
{
RelayData[i/4] = RelayData[i/4] | rxdata[i] << 24;
}
//my_send_data(tcp_echoserver_pcb, &rxdata[i], 4);
}
tcp_write(tpcb, RelayData, 1, TCP_WRITE_FLAG_COPY);//回复客户端,观察写入数据
if(0xE5E5A5A5 == RelayData[(i-1)/4])//在是标准尾时,将标志位写入升级
{
WriteFlash((Application_2_Addr+NumBering), RelayData, i/4-1);
WriteFlash(0X080BFFFC, &Upgrade_Flags, 1);
}
else
{
WriteFlash((Application_2_Addr+NumBering), RelayData, i/4);
}
NumBering += rxdatalen;//累加字节数
memset(RelayData,0,TR_DATA_SIZE*4);//清零
}
tcp_recved(tpcb, p->tot_len);
pbuf_free(p);
ret_err = ERR_OK;
}
else
{
/* 未知的 es->state,垃圾数据 */
tcp_recved(tpcb, p->tot_len);
//es->p = NULL;
pbuf_free(p);
ret_err = ERR_OK;
}
return ret_err;
}
4.处理连接错误
static void tcp_echoserver_error(void *arg, err_t err)
{
struct tcp_echoserver_struct *es;
LWIP_UNUSED_ARG(err);
//printf("错误 : %d \r\n",err);
es = (struct tcp_echoserver_struct *)arg;
if (es != NULL)
{
/* free es structure */
mem_free(es);
}
}
5.轮询函数,处理TCP连接事件
static err_t tcp_echoserver_poll(void *arg, struct tcp_pcb *tpcb)
{
err_t ret_err;
struct tcp_echoserver_struct *es;
es = (struct tcp_echoserver_struct *)arg;
//HAL_Delay(250);
if (es != NULL)
{
if (es->p != NULL)
{
//tcp_sent(tpcb, tcp_echoserver_sent);
tcp_echoserver_send(tpcb, es);
}
else
{
if(es->state == ES_CLOSING)
{
tcp_echoserver_connection_close(tpcb, es);// 发送完毕,关闭连接
}
}
ret_err = ERR_OK;
}
else
{
tcp_abort(tpcb);
ret_err = ERR_ABRT;
}
return ret_err;
}
6.数据被成功发送后被调用。用于管理TCP回显服务器的发送逻辑
static err_t tcp_echoserver_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
struct tcp_echoserver_struct *es;
LWIP_UNUSED_ARG(len);
es = (struct tcp_echoserver_struct *)arg;
es->retries = 0;
if(es->p != NULL)
{
/* still got pbufs to send */
tcp_echoserver_send(tpcb, es);
}
return ERR_OK;
}
7.发送从客户端接收到的数据
static void tcp_echoserver_send(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es)
{
struct pbuf *ptr;
err_t wr_err = ERR_OK;
//printf("发送数据的总长度 : %ld \r\n",es->p->tot_len);
//printf("发送数据: \r\n");
while ((wr_err == ERR_OK) && (es->p != NULL) && (es->p->len <= tcp_sndbuf(tpcb)))
{
/* get pointer on pbuf from es structure */
ptr = es->p; //得到当前需要发送的数据缓存 pbuf
/* enqueue data for transmission */
wr_err = tcp_write(tpcb, ptr->payload, ptr->len, TCP_WRITE_FLAG_COPY); //发送
if (wr_err == ERR_OK)
{
u16_t plen;
u8_t freed;
plen = ptr->len; //得到当前节点的数据长度
/* continue with next pbuf in c hain (if any) */
es->p = ptr->next; //得到链表的下一个节点
if(es->p != NULL) //如果节点不为空
{
/* increment reference count for es->p */
pbuf_ref(es->p); //成员 ref 的值加1 当收到数据时,pbuf成员ref的值为1 ,在pbuf_free()函数中是对成员ref减一 如果结果为0则释放此节点内存并进入下一个节点 否则只是把成员ref的值减一
}
/* chop first pbuf from chain */
do
{
/* try hard to free pbuf */
freed = pbuf_free(ptr); //每执行一个 pbuf中的成员ref的值减一 当它这个节点没有释放时,返回一直为0
}
while(freed == 0); //执行直到释放这个节点内存为止 pbuf_free()函数的返回值表示释放的内存的节点数
/* we can read more data now */
tcp_recved(tpcb, plen); //增加可以接收数据的大小
}
else if(wr_err == ERR_MEM)
{
/* we are low on memory, try later / harder, defer to poll */
es->p = ptr; //重发
}
else
{
/* other problem ?? */;
}
}
}
8. 关闭TCP连接,并释放与该连接相关的资源。
static void tcp_echoserver_connection_close(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es)
{
/* remove all callbacks */
tcp_arg(tpcb, NULL);
tcp_sent(tpcb, NULL);
tcp_recv(tpcb, NULL);
tcp_err(tpcb, NULL);
tcp_poll(tpcb, NULL, 0);
/* delete es structure */
if (es != NULL)
{
mem_free(es);
}
/* close tcp connection */
tcp_close(tpcb);
//printf("已关闭连接 \r\n");
}
四、问题
1. flash写入
在进行flash写入之前一定要对flash进行擦除操作,因为flash的编程原理都是只能将1写为0,而不能将0写为1,所以在使用flash之前,必须将区域擦除,而擦除的过程就是把所有位都写为1的过程。
但写入标准位是需要重复进行的,所以需要在写入标志位之前合适的位置对app2进行擦除,或者是设计一个合适的标志位的值来进行功能选择,例如:0111 1111作为升级标志位,0011 1111作为启动标志位来避免擦除操作。
2.注意stm32是小端模式
在我给出的代码 3.处理收到的数据 中接收数组通过字节的形式接收,但在写入flash时选择使用一字的方式进行写入,如果将第一个字节写入到int数组的最高位,会导致最后写入flash时数据高低位反向。
3. app代码中断向量表的修改
在使用boot后,需要对中断向量表进行偏移,其偏移值就是你boot的大小,具体原因可以自行搜索。
修改中断向量表有两种方法,第一种可以通过在main函数的开始通过在SCB->VTOR赋值的方式对中断向量表修改,或者在system_stm32f7xx.c文件中修改VECT_TAB_OFFSET宏来的方式对中断向量表进行修改,但个人在实际使用时,使用第一种方式后有部分工程在写入后无法运行,而第二种方式可行。
4.从以太网接收数据缺失
该问题的原因有很多,这里只说一种情况。
当上位机一次发包的字节大于536,只有前536字节被接收其他字节的数据丢失。这种情况的原因可能是TCP_MSS宏定义过小,在opt.h中搜索TCP_MSS,然后将其值修改成自己需要的值可以解决。
5.boot、app1、app2分区注意
本工程模板中的flash擦除预设只有扇区擦除或者块擦除,没注意到是否有页擦除,所以在分区时不能存在两个区处于同一个扇区的情况,否则在擦除其中一个分区时会对其他分区造成影响。