我觉得这应该算是纯干货了,搞了一个月了,方法也调整了几遍
还是决定记录一下,毕竟这个东西我也搞了挺久的,遇到一些棘手的,或者是因为我很粗心遇到的问题也和大家分享一下,。
板卡核心是 STM32F103RCT6,256kflash
资源链接:https://pan.baidu.com/s/1p_29aBWS6K-A9HfkYgEnug 密码:l1f4
前言
开始看这个远程升级的时候,我是一头雾水的,原子哥的教程里面也有说到过iap,有了一个大概的了解,我想不明白的是,通过串口下载的方式把hex文件下载到flash,和把bin文件下载到flash难道不是拉一根线,下载一个文件嘛,(初入职场的我并没有考虑到安全这个问题)如果直接将hex文件给到客户那边,风险是非常大的,在原子的论坛里面也看见有人说没有加密被抄板了,这也是为什么要加密的原因,这里加密用的是AES加密,AES加密有AES128,AES192,AES256。下面会有传送门,各种帖子的传送门分享给大家。(下面我会把每一部分都讲一下,有一些是直接移植过来的也不详细的讲了)
IAP
在学习IAP之前建议大家先看看正点原子的教程,知道大概是个怎样子的过程,以及作用(STM32串口IAP实验(战舰STM32开发板实验)-OpenEdv-开源电子网)
IAP是什么
IAP即应用编程,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是在产品发布以后可以方便的通过预留通信口对产品中的固件程序进行升级更新
IAP的实现方法
通常在用户需要实现IAP功能时,即用户程序运行中作自身更新操作的时候,需要在设计固件程序的时编写两个项目代码,这两部分代码都同时烧录在User Flash中,这两部分分别叫做bootloader引导,application应用,当芯片上电后,首先是第一个项目代码开始运行,它有如下操作:
- 检查是否需要对第二部分代码进行更新(在bootloader的程序中要进行判断)
- 如果不需要更新则跳转到第4步
- 执行更新操作
- 跳转到application应用程序中执行
第一部分代码必须通过其他手段进行烧录,我使用的是串口。
第二部分的代码可以使用第一部分的IAP功能烧录,也可以和第一部分一起烧录(这里可能说的不是很清楚)
我们平时对板卡进行程序下载的时候,有多种方式,USB、串口、JTAG、SWD等,用flymcu进行串口方式烧录的时候,下载的文件是.hex的文件,st-link是直接下载(我也不知道是怎么个原理,网上看到有帖子说st-link也能下载hex文件),所以上面的我想说的是可以把两部分代码(bootloader和application)分别编译生成两个hex文件,再将两部分hex文件合并成一个hex文件,再进行下载(肯定有人问两个hex合并成一个hex还能用嘛,所以就要了解hex文件格式,以及合并原理了,传送门向下看)
Ymodem
我们在进行IAP下载的时候,下载的是bin文件,但是在正点原子的IAP实验中,直接通过串口传输文件,没有进行校验,和错误重发,对于IAP认知而言,是挺好的,但是在实际项目中并不会这样使用(以至于我的同事问我要不要自定义一个协议),但是在别人的博客中发现了ST官方对这一块有例程,使用的是Ymodem协议,并且还有循环冗余校验,ST官方的源码在下面传送门,大多都可以移植过来直接用,但是还是建议把Ymodem协议看明白。(我已经看了一个月了,在这个基础上还自己写了一遍,我觉得已经很明白了)
(看这里:如果要用十六进制看bin文件的内容的话,我发现一个好方法,太舒服了,在linux下输入命令
xxd -ps [1.bin] >1.txt
这样1.bin就会用十六进制的方式输出存储到1.txt中
使用Ymodem协议我们只要写接收端的代码,接收端代码ST也已经处理好了,发送使用的是SecureCRT(见传送门)
我这里使用的是普通串口,RS485、蓝牙等方式其实都可以。
在使用SecureCRT进行Ymodem传输的过程中遇到有以下几个问题:
- Ymodem发送的时候,卡在Transferring LED.bin...一直不能成功发送,后来发现是我串口接收的程序写的有问题(其实还有可能是线松了)
- 用485的时候,由于是半双工,所以接收的时候要使能一下
- 当我Ymodem成功发送之后,程序还是不能正常跳转,有可能有两个原因(只是我遇到的两个原因)1.app程序的地址有问题,也就是没有修改flash初始地址,这里产生的原因是,我直接使用以前的例程进行编译修改地址,但是后来查看map中发现,初始地址并没有改变,还是0x80000000,所以一定要新建 2.修改地址只能一次,我修改一次之后,觉得给bootloader的空间太大了,然后改了第二次,顺带着bootloader程序中的也改了,但是当我看map的时候发现application程序的起始地址还是我第一次设置的,两个部分设置的地址不一样,能成功运行就奇怪了
- 跳转之前要关中断,这是我从别人问题那里看见的,因为我bootloader程序中没有用到中断
(bug总是会有很多,慢慢debug,总会成功的。。。。。这是真的(因为我初始写这篇博文的时候是8月现在9月了,我终于改完了))
Ymodem协议的接收代码分析:发送是由SecureCRT(只写了一点点注释,结合Ymodem协议的格式,还是很容易看懂的)
我也在接收端加入了AES解密,密钥是123456789abcdeff
/**
* @brief Receive a file using the ymodem protocol
* @param buf: Address of the first byte
* @retval The size of the file
*/
int32_t Ymodem_Receive (uint8_t *buf,uint32_t addr)
{
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
int32_t i, j, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
uint8_t *BufferIn;
char key[17]={'1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','f','\0'};
/* Initialize FlashDestination variable */
FlashDestination = addr;
for (session_done = 0, errors = 0, session_begin = 0; ;)
{
for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
{
switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))
{
case 0: //结束传输
errors = 0;
switch (packet_length)
{
/* Abort by sender */
case - 1:
Send_Byte(ACK);
return 0;
/* End of transmission */
case 0:
Send_Byte(ACK);
file_done = 1;
break;
/* Normal packet */
default:
if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff)) //检验数据包是否正确
{
Send_Byte(NAK); //接收者接收数据失败,因此需要重新发送
}
else
{
if (packets_received == 0) //第0个数据包,起始帧
{
/* Filename packet */
if (packet_data[PACKET_HEADER] != 0) //文件名存在
{
/* Filename packet has valid data */
for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
{
file_name[i++] = *file_ptr++; //文件名字
}
file_name[i++] = '\0';
for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
{
file_size[i++] = *file_ptr++; //文件大小
}
file_size[i++] = '\0';
Str2Int(file_size, &size); //字符串转整数存储在size中
/* Test the size of the image to be sent */
/* Image size is greater than Flash size */
if (size > (FLASH_SIZE - 1)) //数据大小超过flash大小
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -1;
}
/* Erase the needed pages where the user application will be loaded */
/* Define the number of page to be erased */
//擦除将要加载用户程序的页面
//定义要擦除的页数
NbrOfPage = FLASH_PagesMask(size); //计算页数
/* Erase the FLASH pages */
for (EraseCounter = 0; (EraseCounter < NbrOfPage) && (FLASHStatus == FLASH_COMPLETE); EraseCounter++)
{
FLASHStatus = FLASH_ErasePage(FlashDestination + (PageSize * EraseCounter));
}
Send_Byte(ACK);
Send_Byte(CRC16);
}
/* Filename packet is empty, end session */
else //文件名为空
{
Send_Byte(ACK);
file_done = 1;
session_done = 1;
break;
}
}
/* Data packet */
else
{
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
/*---------------------------aes解密-----------------------------------*/
BufferIn=buf;
deAes(BufferIn,packet_length,key);
/*---------------------------------------------------------------------*/
RamSource = (uint32_t)buf;
for (j = 0;(j < packet_length) && (FlashDestination < addr + size);j += 4) //写入
{
/* Program the data received into STM32F10x Flash */
FLASH_ProgramWord(FlashDestination, *(uint32_t*)RamSource); //写入1个字 4个字节
if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -2;
}
FlashDestination += 4;
RamSource += 4;
}
Send_Byte(ACK);
}
packets_received ++;
session_begin = 1;
}
}
break;
case 1: //正常中止
Send_Byte(CA);
Send_Byte(CA);
return -3;
default: //异常中止
if (session_begin > 0)
{
errors ++;
}
if (errors > MAX_ERRORS)
{
Send_Byte(CA);
Send_Byte(CA);
return 0;
}
Send_Byte(CRC16);
break;
}
if (file_done != 0)
{
break;
}
}
if (session_done != 0) //文件发送完成
{
break;
}
}
return (int32_t)size;
}
AES加密解密
这个东西我也搞了三天,用的是AES128,要安全性高一点可以用AES256,关于加密解密的过程网上都有现成的,只有对文件加密解密的时候是我自己写的,还没有集成成软件,现在还在试用阶段(实际上是等我学一点C#再说吧)。现在就停留在linux命令行下,将要升级的文件加密,然后解密是在传送的时候,要存储之前解密存储。
AES过程如下:(解密反过来就好了,我还发现一份特别棒的资料,放在传送门了)
相关的过程可以看这一篇博文
对于文件的加密和解密C语言代码如下:
void aesFile(char *key)
{
char fileName[64];
char buf_16[16];
char buf[64];
FILE *fp;
FILE *cipherfp;
int cnt=0;
int rc;
printf("请输入要加密的文件名,该文件必须和本程序在同一目录下\n");
if(scanf("%s", fileName) == 1)
{
fp=fopen(fileName, "r+");
if(fp == NULL)
{
printf("文件不存在或者打开失败\n");
return;
}
printf("请输入加密后的文件名:\n");
scanf("%s", buf);
cipherfp=fopen(buf,"a+");
if(cipherfp==NULL)
{
printf("密文文件创建失败\n");
fclose(fp);
return;
}
while(!feof(fp)) //判断有没有到文件末尾
{
if((rc=fread(buf_16,sizeof(char),sizeof(buf_16),fp))==16) //每次读16个
{
aes(buf_16,16,key);
fwrite(buf_16,1,16,cipherfp);
}
}
if(rc) //不满16个就在末尾填充0,最后一位表示填充0的个数值
{
memset(buf_16+rc,'\0',15-rc);
buf_16[15]=16-rc;
printf("明文 buf_16[15] = %d\n",buf_16[15]);
aes(buf_16,16,key);
fwrite(buf_16,1,16,cipherfp);
}
}
fclose(cipherfp);
fclose(fp);
printf("加密成功\n\n");
}
void deAesFile(char *key)
{
char fileName[64];
char buf[64];
char buf_16[16];
FILE *fp;
FILE *cleartext;
int len;
int times=0;
int count;
printf("请输入要解密的文件名,该文件必须和本程序在同一个目录\n");
if(scanf("%s", fileName) == 1)
{
fp=fopen(fileName,"r+");
if(fp == NULL)
{
printf("文件不存在或文件打开失败\n");
return;
}
printf("请输入解密后的文件名:\n");
scanf("%s", buf);
cleartext=fopen(buf,"a+");
if(cleartext==NULL)
{
printf("明文文件创建失败\n");
fclose(fp);
return;
}
//取文件长度
fseek(fp,0,SEEK_END); //将文件指针置尾
len=ftell(fp); //取指针当前位置到第一个位置的字节数
rewind(fp); //将文件指针重新指向文件头
while(1)
{
//密文的字节数一定是16的倍数
fread(buf_16,sizeof(char),16,fp);
deAes(buf_16,16,key);
times += 16;
if(times < len)
{
fwrite(buf_16,sizeof(char),16,cleartext);
}
else
{
break;
}
}
/*判断末尾是否被填充*/
if(buf_16[15]<16)
{
for(count = 16-buf_16[15];count<15;count++)
{
if(buf_16[count]!='\0')
{
break;
}
}
if(count == 15) //有填充
{
fwrite(buf_16,sizeof(char),16-buf_16[15],cleartext);
}
else //无填充
{
fwrite(buf_16,sizeof(char),16,cleartext);
}
}
}
fclose(cleartext);
fclose(fp);
printf("\n解密成功!!!\n\n");
}
(没时间写了今天就先写这么多,有问题的也可以私聊或者留言哦(9月23))
传送门
分享我的项目必需品:IAP+YMODEM+CRC16+AES256+PC端软件+hex合并分享我的项目必需品:IAP+YMODEM+CRC16+AES256+PC端软件+hex合并-OpenEdv-开源电子网
hex文件格式详解(对于要自己开发hex合并软件的同学还是很有帮助的):hex文件格式详解
分割线:我以为我对IAP了如指掌,上面那些东西是在上一家公司做的,那时候刚毕业,学习到了很多,现在快工作两年了,跳槽的公司也要做这个,基本一天直接移植搞定,但是遇见了以前从来没有遇见过的问题,如下:
1、传输过来的bin文件存入flash的过程中出现了存入的数据与原始数据对应不上的问题,但是在出现这个问题之前,iap一直测试都是成功的,出现这个问题是我修改了几行看上去平平无奇的代码,但是把修改去掉之后还是不行,我好恨我没有备份
解决办法:后面想来想去还是觉得是内部flash有问题,后面清空了flash里面的各种标志位好了
void FLASH_If_Init(void)
{
/* Unlock the Program memory */
FLASH_Unlock();
/* Clear all FLASH flags */
FLASH_ClearFlag(FLASH_FLAG_EOP|FLASH_FLAG_WRPERR | FLASH_FLAG_PGERR | FLASH_FLAG_BSY);
}
2. 这次跳转的APP程序中带了UCOS系统,跳转之后出现了串口发送乱码的情况,发现过了一会软定时出现问题,串口发送只能发送最后一个字节,如果不带ucos系统,串口发送正常。串口接收是正常的,串口接收的数据能够正常的运行。在论坛找到的解决办法,先码住(结果是不可用的,我的问题不是下面解决方法能够解决的,但是下面的也写的很好,也是要注意的点)
如果是从APP跳转到BL,跳转之前关闭所有中断,记得开了哪些中断就关闭哪些中断,不要遗漏了,另外ucos使用了systick的中断,这个也要记得关闭,然后
如果是从BL跳转到APP,同样需要逐个关闭BL中使用的中断,同时还要在APP中做向量偏移
当然,如果逐个关闭太麻烦,有一个关闭总中断的函数
__set_FAULTMASK(1);
这个函数会将总中断关闭掉,然后跳转成功后再开启总中断
__set_FAULTMASK(0);
当然这样也是有弊端的,那就是虽然能够成功跳转,但是中断的开关会相互干扰,比如我在BL中开启了TIM2的中断,那么跳转到APP后,当打开总中断后TIM2同样是开启状态的,但是如果APP中,没有使用TIM2的中断,即没有TIM2对应的处理函数,那么此时就是出现HardFault_Handler
所以我通常的处理方法是这样的
1、从BL跳转到APP时,由于BL中开启的中断比较少,可能也就USART,顶多再加一个TIM,逐个关闭即可,当然为了保险我也会将总中断关闭掉,然后跳转到APP,在APP中将总中断打开
2、从APP跳转到BL时,由于APP中开启的中断太多了,逐个关闭太麻烦,而且如果后面又另外开启了其他中断,还要做调整,所以就通过直接通过软件复位来变相实现,因为BL一般都会放到0x08000000处,复位后stm32也会直接从这个地方启动,所以变相实现了跳转。
解决办法:发现是app开始地址没有对齐,后面app起始地址改成0x08010000之后一切良好。以前用的是这个地址,后面手欠改成别的了。。。。。