你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

基于STM32F1的CAN通信之之串口IAP

[复制链接]
攻城狮Melo 发布时间:2023-10-23 22:37
一、串口IAP简介
1.1 什么是IAP

IAP,英文全称In Application Programming,在应用中编程。很好理解,就是在程序运行过程中我们进行程序的烧写,或者叫升级。

1.2 STM32下载程序
我们都知道,STM32可以利用串口下载程序,这是因为ST公司在产线上就在产品中内嵌了自举程序。所谓的自举程序,实际就是支持我们通过串口下载程序的代码。自举程序被存放在系统存储区,因此如果我们需要通过串口下载程序,需要将Boot0接高电平,Boot1接低电平,让程序从系统存储器开始运行,运行自举程序。下载完成后我们再将Boot0接地,让程序从主闪存存储器开始运行。自举程序是我们用户无法修改的。


二、串口IAP有什么作用
上面我们介绍了什么是IAP,那么这个IAP到底有什么作用呢?

首先介绍两个词——Bootloader和Application。Bootloader实际就是一段引导程序,单片机上电后先执行Bootloader程序,然后再执行用户编写的应用程序Application。介绍完这两个词,我们来介绍一下IAP有什么作用。比如我们生产了A,B两款产品。A产品是某个精密器件的一部分,B产品是一款物联网产品。我们的产品销售范围很广,远销海外。

某天A产品的某个客户反映了一个Bug,我们编写好了程序,需要进行程序更新,或者叫升级。利用IAP,我们可以在程序运行时,通过预留的通信接口直接烧写程序。而不需要再把整个设备拆开,像我们调试时那样下载程序。甚至我们可以直接给客户邮寄一个小设备,客户直接进行傻瓜式升级。

又过了一段时间,B产品的程序出现了一个Bug,风险等级比较低,但是依旧需要全体升级程序。我们总不能挨个产品派人去升级,成本极大。这时候又轮到我们的IAP出场了。它可以在所有设备在线运行的情况下,直接通过网络下发升级程序,实现在线升级,节约了大量的人力成本。

通过上面这两个例子,大家应该能够基本了解IAP的用处,使用IAP让我们不需要再使用调试器进行下载,甚至实现设备的在线升级。


三、启动流程
在介绍如何实现IAP之前,我们先来简单了解以下STM32的启动流程。

3.1 正常启动流程
这里的正常启动流程指的是,没有添加IAP的流程。


微信图片_20231023223616.png

正常启动流程


程序启动时首先开辟栈空间,配置栈顶指针。然后配置堆空间。配置完成后,建立中断向量表,在中断向量表中找到复位中断,开始执行复位中断服务函数,然后跳转到main函数中,执行用户代码。当用户代码中有中断请求时,会回到中断向量表,根据中断源执行相应的中断服务函数。

3.2 加入IAP后的启动流程
下面是加入IAP之后的启动流程。

微信图片_20231023223619.png

加入IAP启动流程



可以看到,与上面不同的是,加入IAP后,执行完复位中断服务函数后直接进入IAP的main函数。在执行完IAP之后,跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main 函数。

由上面的两个启动过程我们可以看出
• 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始。
• 必须将新程序的中断向量表相应的移动,移动的偏移量为 x。


四、必备知识
44.1 修改程序运行起始地址

点击魔术棒,选择“Target”,修改运行起始地址和代码大小。

微信图片_20231023223624.png

修改App运行起始地址



4.2 设置中断向量表偏移
VTOR 寄存器存放的是中断向量表的起始地址。如果要设置中断向量表偏移,只需要在main函数最开始添加如下语句即可
  1. SCB->VTOR = FLASH BASE | 偏移量:
复制代码

4.3 生成.bin文件
点击魔术棒,选择“User”,按照如下配置,输入下面的内容
  1. fromelf --bin -o "$L@L.bin" "#L"
复制代码

微信图片_20231023223706.png

生成.bin文件配置



点击编译,不报错就可以,去设置的输出文件夹中就可以找到对应的.bin文件。

微信图片_20231023223710.png

编译提示



五、串口IAP实现
本次的目标是实现一个串口IAP,也就是编写Bootloader,在程序运行过程中实现程序的下载。Bootloader程序应该可以通过串口接收上位机发来的.bin文件(App程序),检查后将.bin文件写入到Flash特定位置,然后跳转到App程序运行。

5.1 串口中断服务函数
本次的串口中断服务函数与之前不同,这里单独贴出来。需要定义一个接收数组,接收数组的起始地址限制为0X20001000。接收数组最多可以接收55K字节,可以根据需要调整。但是需要注意的是,数组的大小需要比App程序要大,而且不能超过芯片的SRAM空间大小。
  1. /*
  2. *==============================================================================
  3. *函数名称:USART1_IRQHandler
  4. *函数功能:USART1中断服务函数
  5. *输入参数:无
  6. *返回值:无
  7. *备  注:无
  8. *==============================================================================
  9. */
  10. u32 gReceCount = 0;   // 接收计数变量
  11. // 接收数组
  12. // 限制起始地址为0X20001000
  13. // 保证偏移量为 0X200的倍数
  14. // 是为了给App留SRAM空间
  15. u8 gReceFifo[USART_RECE_MAX_LEN]__attribute__ ((at(0X20001000)));

  16. void USART1_IRQHandler(void)  
  17. {
  18.     if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)   //接收到一个字节  
  19.     {
  20.         if (gReceCount < USART_RECE_MAX_LEN)
  21.         {
  22.             gReceFifo[gReceCount++] = USART1->DR;
  23.         }
  24.         else
  25.         {
  26.             printf ("APP code out of memory!\r\n");
  27.         }
  28.     }
  29. }
复制代码

5.2 Flash写入程序
关于Flash程序,这里就不在赘述,只是贴一下带检查的写入程序。其他具体内容可以到博主STM32速成笔记专栏查看。
  1. /*
  2. *==============================================================================
  3. *函数名称:Med_Flash_Write
  4. *函数功能:从指定地址开始写入指定长度的数据
  5. *输入参数:WriteAddr:写入起始地址;pBuffer:数据指针;
  6.                         NumToRead:写入(半字)数
  7. *返回值:无
  8. *备  注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
  9. *==============================================================================
  10. */

  11. // 根据中文参考手册,大容量产品的每一页是2K字节
  12. #if STM32_FLASH_SIZE < 256
  13.     #define STM32_SECTOR_SIZE   1024   // 字节
  14. #else
  15.     #define STM32_SECTOR_SIZE   2048
  16. #endif

  17. // 一个扇区的内存
  18. u16 STM32_FLASH_BUF[STM32_SECTOR_SIZE / 2];

  19. void Med_Flash_Write (u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
  20. {
  21.     u32 secpos;   // 扇区地址
  22.     u16 secoff;   // 扇区内偏移地址(16位字计算)
  23.     u16 secremain;   // 扇区内剩余地址(16位计算)   
  24.      u16 i;   
  25.     u32 offaddr;   // 去掉0X08000000后的地址
  26.    
  27.     // 判断写入地址是否在合法范围内
  28.     if (WriteAddr < STM32_FLASH_BASE || (WriteAddr >= (STM32_FLASH_BASE + 1024 * STM32_FLASH_SIZE)))
  29.     {
  30.         return;   // 非法地址
  31.     }
  32.    
  33.     FLASH_Unlock();   // 解锁
  34.     offaddr = WriteAddr - STM32_FLASH_BASE;   // 实际偏移地址
  35.     secpos = offaddr / STM32_SECTOR_SIZE;   // 扇区地址
  36.     secoff = (offaddr % STM32_SECTOR_SIZE) / 2;   // 在扇区内的偏移(2个字节为基本单位)
  37.     secremain = STM32_SECTOR_SIZE / 2 - secoff;   // 扇区剩余空间大小
  38.    
  39.     if (NumToWrite <= secremain)
  40.     {
  41.         secremain = NumToWrite;   // 不大于该扇区范围
  42.     }
  43.    
  44.     while (1)
  45.     {
  46.         // 读出整个扇区的内容
  47.         Med_Flash_Read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
  48.         
  49.         // 校验数据
  50.         for (i = 0;i < secremain;i ++)
  51.         {
  52.             // 需要擦除
  53.             if (STM32_FLASH_BUF[secoff + i] != 0XFFFF)
  54.             {
  55.                 break;
  56.             }   
  57.         }
  58.         // 需要擦除
  59.         if (i < secremain)
  60.         {
  61.             FLASH_ErasePage(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE);   // 擦除这个扇区
  62.             
  63.             // 复制
  64.             for (i = 0;i < secremain;i ++)
  65.             {
  66.                 STM32_FLASH_BUF[i + secoff] = pBuffer[i];   
  67.             }
  68.             
  69.             // 写入整个扇区
  70.             Med_Flash_Write_NoCheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
  71.         }
  72.         else
  73.         {
  74.             // 写已经擦除了的,直接写入扇区剩余区间
  75.             Med_Flash_Write_NoCheck(WriteAddr,pBuffer,secremain);
  76.         }
  77.         
  78.         if (NumToWrite == secremain)
  79.         {
  80.             break;   // 写入结束了
  81.         }
  82.         // 写入未结束
  83.         else
  84.         {
  85.             secpos ++;   // 扇区地址增1
  86.             secoff = 0;   // 偏移位置为0   
  87.             pBuffer += secremain;   // 指针偏移
  88.             WriteAddr += secremain;   // 写地址偏移   
  89.             NumToWrite -= secremain;   // 字节(16位)数递减
  90.             if (NumToWrite > (STM32_SECTOR_SIZE / 2))
  91.             {
  92.                 secremain = STM32_SECTOR_SIZE / 2;   // 下一个扇区还是写不完
  93.             }
  94.             else
  95.             {
  96.                 secremain = NumToWrite;   // 下一个扇区可以写完了
  97.             }
  98.         }  
  99.     }
  100.     FLASH_Lock();   // 上锁
  101. }
复制代码

5.3 IAP程序
IAP程序包含两部分,一部分是将接收到的.bin文件写入Flash,另一部分是跳转到App执行。
  1. /*
  2. *==============================================================================
  3. *函数名称:iap_write_appbin
  4. *函数功能:写入.bin文件
  5. *输入参数:appxaddr:App程序起始地址;appbuf:缓存App程序的数组;
  6.                         appsize:App程序大小
  7. *返回值:无
  8. *备  注:无
  9. *==============================================================================
  10. */
  11. // 对Flash操作的最小单位是16位
  12. u16 iapbuf[1024];   // 要写入Flash的内容

  13. void iap_write_appbin (u32 appxaddr,u8 *appbuf,u32 appsize)
  14. {
  15.     u16 t;   // 临时循环变量
  16.     u16 i = 0;   // 自增索引
  17.     u16 temp;   // 临时计算变量
  18.    
  19.     u32 fwaddr = appxaddr;   // 当前写入的地址
  20.     u8 *dfu = appbuf;   // 指向App代码数组首地址的指针
  21.    
  22.     // for循环,2K为单位进行写入
  23.     for(t = 0;t < appsize;t += 2)
  24.     {  
  25.         temp = (u16)dfu[1] << 8;
  26.         temp += (u16)dfu[0];   
  27.         dfu += 2;   // 偏移2个字节
  28.         iapbuf[i++] = temp;     
  29.         if(i == 1024)
  30.         {
  31.             i = 0;
  32.             Med_Flash_Write (fwaddr,iapbuf,1024);
  33.             fwaddr += 2048;   // 偏移2048  16=2*8所以要乘以2
  34.         }
  35.     }
  36.     if(i)
  37.     {
  38.         Med_Flash_Write (fwaddr,iapbuf,i);   // 将最后的一些内容字节写进去
  39.     }
  40. }
  41. /*
  42. *==============================================================================
  43. *函数名称:iap_load_app
  44. *函数功能:跳转到App
  45. *输入参数:appxaddr:App程序起始地址
  46. *返回值:无
  47. *备  注:无
  48. *==============================================================================
  49. */
  50. iapfun jump2app;

  51. void iap_load_app (u32 appxaddr)
  52. {
  53.     if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)   // 检查栈顶地址是否合法
  54.     {
  55.         jump2app=(iapfun)*(vu32*)(appxaddr+4);   // 用户代码区第二个字为程序开始地址(复位地址)  
  56.         MSR_MSP(*(vu32*)appxaddr);   // 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
  57.         jump2app();   // 跳转到APP
  58.     }
  59. }
复制代码

5.4 main函数
main函数设计如下
  1. int main(void)
  2. {
  3.     u32 curRecCunt = 0;   // 当前接收数量
  4.    
  5.     // 设置NVIC中断分组2:2位抢占优先级,2位响应优先级
  6.     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  7.     delay_init();   // 延时初始化
  8.     uart_init(115200);   // 串口初始化
  9.    
  10.     printf ("Enter the Bootloader Code!\r\n");
  11.    
  12.     while(1)
  13.   {
  14.         // 如果接收到内容且传输完成
  15.         if (curRecCunt == gReceCount && gReceCount != 0)
  16.         {
  17.             printf ("App Code reception complete!\r\n");
  18.             printf ("The length of the received App code is %d!\r\n",gReceCount);
  19.             
  20.             // 开始准备写入Flash
  21.             if(((*(vu32*)(0X20001000+4)) & 0xFF000000) == 0x08000000)   // 判断是否为0X08XXXXXX.
  22.             {
  23.                 iap_write_appbin(FLASH_APP1_ADDR,gReceFifo,gReceCount);   // 更新FLASH代码  
  24.                 gReceCount = 0;   // 清零接收计数变量
  25.                 printf("Update complete!\r\n");
  26.             }
  27.             else
  28.             {
  29.                 printf("Update error!\r\n");
  30.             }
  31.             
  32.             // 准备跳转App执行
  33.             if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)   //判断是否为0X08XXXXXX.
  34.             {
  35.                 printf("Enter App!\r\n");
  36.                 iap_load_app(FLASH_APP1_ADDR);   //执行FLASH APP代码
  37.             }else
  38.             {
  39.                 printf("Enter error!\r\n");
  40.             }
  41.         }
  42.         else   // 未传输完成
  43.         {
  44.             curRecCunt = gReceCount;   // 更新当前接收数量
  45.             // 延时一定要有,给串口中断留出时间
  46.             // 如果没有会导致接收不完全
  47.             delay_ms(10);
  48.         }
  49.     }
  50. }
复制代码

六、注意事项
需要注意的是,上面的程序中App程序是从0x8010000开始,需要配置好程序起始地址和中断向量表偏移。


转载自:二土电子
如有侵权请联系删除

收藏 评论0 发布时间:2023-10-23 22:37

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版