vesperW · 3 天前

量产级别代码,单片机通过WiFI进行OTA固件升级

在物联网设备快速迭代和远程维护需求日益增长的今天,OTA(Over-the-Air)远程升级技术已成为智能硬件开发中不可或缺的一部分。

OTA 升级,指通过无线通信方式(如 Wi-Fi、蓝牙、NB-IoT 等)将新固件发送到设备,实现远程软件更新。它有以下几个显著优势:

  • 无需现场维护:节省人工成本,提高维护效率
  • 产品生命周期内灵活迭代:快速修复 BUG、添加功能
  • 提高用户体验:实时响应用户反馈

本文将详细介绍如何基于涂鸦 IoT 平台,在 STM32F103C8T6 这款经典单片机上实现 OTA 升级,助力你的产品实现远程升级,运维起来更加。

当然,不止于涂鸦平台,原理都一样,可以在任何支持的平台上实现,包括 MCU,其它型号的 MCU 也都大同小异。

核心思想非常简单,我们把整个FLASH分成4部分,bootloader,APP1,APP2,FLAG

image.png

STM32F103C8T6 一共 64K,FLASH 一共 64 页,每页 1K,bootloader 分 8K,FLAG 分 2K,APP1 与 APP2 各 27K,也就是我们的应用程序,编译出来不能超过 27K 的大小。

bootloader 是一个独立的固件,在启动后负责检查 FLAG 区域的升级标志位以确定是否有新的固件需要升级,如果有就跳到升级部分,将 APP2 的部分 copy 到 APP1,copy 完之后再清空 FLAG 区域的升级标志,然后重启,就会运行新的 APP1 也就是升级后的程序了。

这里如果升级到一半断电了,下次重启还是会重新 copy 一次 APP2 到 APP1,因为升级标志位还没被清空,所以这里不用担心升级的时候断电导致无法重启

bootloader 在启动的时候,如果 FLAG 区域的升级标志位没有置位,那就直接启动 APP1 就可以了。

大概原理就是这样,下面我们结合代码看下:

  if(ReadFlashTest(UPFLAG) == 0x55555555)  //0x55555555
  {
     update_firmware(APP1ADDR,APP2ADDR);
     EarseFlash_1K(UPFLAG);
     WriteFlash(UPFLAG,UPFlagBuffer,4);
     printf("APP复制中\r\n");
     NVIC_SystemReset();
  }
  else 
  {
     Jump2APP(APP1ADDR);
  }

从 APP2 复制到 APP1,先读一页,然后擦除,再写:

void update_firmware(uint32_t SourceAddress,uint32_t TargetAddress)
{
  //先读  后擦除  后写  
  uint16_t i;
  volatile uint32_t nK ;
  nK = (TargetAddress - SourceAddress)/1024;
  for(i = 0;i < nK;i++)
  {
    memset(cBuffer,0xff,sizeof(cBuffer));
    ReadFlash(TargetAddress+i*0x0400,cBuffer,1024); 
    EarseFlash_1K(SourceAddress+i*0x0400);
    WriteFlash(SourceAddress+i*0x0400,cBuffer,1024); 
  }
}

Jump2APP 函数通过验证应用程序地址、设置堆栈指针以及跳转到应用程序的入口点,促进从引导加载程序到应用程序代码的转换。

void Jump2APP(uint32_t app_address)
{
  volatile uint32_t JumpAPPaddr;
if (((*(uint32_t*)app_address) &0x2FFE0000 ) == 0x20000000)
  {
    JumpAPPaddr = *(volatile uint32_t*)(app_address + 4);
    jump2app = (APP_FUNC) JumpAPPaddr;
    printf("APP跳转地址:%x\r\n",app_address);
    __set_MSP(*(__IO uint32_t*) app_address);
    jump2app();
  }
else{
    printf("输入地址不合法\r\n");
  }
}

volatile uint32_t JumpAPPaddr; 声明一个变量,用于存储应用程序的入口点地址(重置处理程序的地址)。volatile 关键字确保编译器不会优化对此变量的访问,因为它将从内存中读取。

if (((_(uint32_t_)app_address) & 0x2FFE0000 ) == 0x20000000) 在 STM32 向量表中,第一个字(位于 app_address 处)是初始堆栈指针(MSP,即主堆栈指针)。检查掩码值是否指示堆栈指针位于 SRAM 中。如果为真,则该地址被视为有效,因为合法应用程序的堆栈指针应该位于 SRAM 中。

JumpAPPaddr = (volatile uint32_t)(app_address + 4); app_address + 4 指向向量表中的第二个字,该字保存重置处理程序(应用程序的入口点)的地址。

jump2app = (APP_FUNC) JumpAPPaddr; 将程序地址转换为函数指针。

__set_MSP(_(__IO uint32_t_) app_address); __set_MSP 是一个 CMSIS 函数,它使用从 app_address (应用程序的初始堆栈指针)读取的值更新 MSP 寄存器。

jump2app(); 调用函数指针 jump2app,实际上将控制权转移到应用程序的入口点。这将启动应用程序代码的执行。

bootloader 部分的设置,IROM1 的起始地址为 0x8000000,大小为 0x2000。

图片

选择 Erase Sectors,Reset and Run,下载。

图片

bootloader 部分就介绍到这里。

下面介绍 APP 部分。

用户的代码逻辑都在 APP 部分,APP 部分需要额外设置的比较简单,我们只需要在代码启动后重新设置一下应用程序的向量表地址

    __set_FAULTMASK(1);
    SCB->VTOR = FLASH_BASE | APP1;  
    __set_FAULTMASK(0);

在引导加载程序跳转到应用程序后执行(例如,通过 Jump2APP)。它确保应用程序将 Cortex-M CPU 配置为其自己的向量表,设置前关闭中断,确保向量表重定位不会被中断。

这里中断向量表的起始地址就是 0x08002000 了。

这里需要注意的是,应用程序的下载地址需要设置为0x8002000,大小为0x6C00

图片

在 Keil 中,需要配置下编译选项 fromelf.exe --bin -o "$L@L.bin" "#L",以生成 bin 文件。

图片

bin 文件不同于 hex 文件,hex 文件中包含烧录地址,bin 文件中不含,所以我们可以指定 bin 文件的烧录地址。这样,在烧录的时候,就可以直接烧录 APP 到 0x08002000 了。

也就是开始需要先烧录 bootloader 文件,再烧录 app 文件,后续量产的时候,看可以把 bootloader 文件与 app 文件合并成一个文件进行烧录。

关于如何合并,我们之前发过一篇文章可以参考。

MCU OTA 方案中,BootLoader 与 APP 如何合并?(点击阅读)

APP1 运行的时候,如果检测到服务器有新的版本号,就开始执行升级指令,就会将新版本的 bin 文件,分包向 APP1 发送,APP1 接收到之后,就原封不动的将其存储到 APP2 区域,最后一包接收完之后,就将 FLAG 区域的升级标志位设置为相应的标志数据,然后重启,就回到上面的升级流程了。

图片

#ifdef SUPPORT_MCU_FIRM_UPDATE
        case UPDATE_START_CMD:                                //升级开始
            //获取升级包大小全局变量
            firm_flag = PACKAGE_SIZE;
            if(firm_flag == 0) {
                firm_size = 256;
            }elseif(firm_flag == 1) {
                firm_size = 512;
            }elseif(firm_flag == 2) { 
                firm_size = 1024;
            }

            firm_length = wifi_data_process_buf[offset + DATA_START];
            firm_length <<= 8;
            firm_length |= wifi_data_process_buf[offset + DATA_START + 1];
            firm_length <<= 8;
            firm_length |= wifi_data_process_buf[offset + DATA_START + 2];
            firm_length <<= 8;
            firm_length |= wifi_data_process_buf[offset + DATA_START + 3];
            
            upgrade_package_choose(PACKAGE_SIZE);
            firm_update_flag = UPDATE_START_CMD;
        break;
    
        case UPDATE_TRANS_CMD:                                //升级传输
            if(firm_update_flag == UPDATE_START_CMD)
            {
                //停止一切数据上报
                stop_update_flag = ENABLE;
      
                total_len = (wifi_data_process_buf[offset + LENGTH_HIGH] << 8) | wifi_data_process_buf[offset + LENGTH_LOW];
      
                dp_len = wifi_data_process_buf[offset + DATA_START];
                dp_len <<= 8;
                dp_len |= wifi_data_process_buf[offset + DATA_START + 1];
                dp_len <<= 8;
                dp_len |= wifi_data_process_buf[offset + DATA_START + 2];
                dp_len <<= 8;
                dp_len |= wifi_data_process_buf[offset + DATA_START + 3];
      
                firmware_addr = (unsigned char *)wifi_data_process_buf;
                firmware_addr += (offset + DATA_START + 4);
      
                if((total_len == 4) && (dp_len == firm_length))
                {
                    //最后一包
                    ret = mcu_firm_update_handle(firmware_addr,dp_len,0);
                    firm_update_flag = 0;
                }
                elseif((total_len - 4) <= firm_size)
                {
                    ret = mcu_firm_update_handle(firmware_addr,dp_len,total_len - 4);
                }
                else
                {
                    firm_update_flag = 0;
                    ret = ERROR;
                }
      
                if(ret == SUCCESS)
                {
                    wifi_uart_write_frame(UPDATE_TRANS_CMD, MCU_SEND_VER, 0);
                }
                //恢复一切数据上报
                stop_update_flag = DISABLE;    
            }
        break;
#endif     

基本原理就是这样,关于 STM32 一些启动过程原理与涂鸦的 SDK 逻辑,本文不做详细的介绍,大家自行看一下就都懂了。

以上 OTA 代码逻辑,在实际产品中,已经量产并持续在使用,有需要的小伙伴可以参考,欢迎大家在评论区与老宇哥进行交流。

代码源文件全部工程:

通过网盘分享的文件:tuya-OTA-lightDemo1.0.0.rar

链接: https://pan.baidu.com/s/1z0Ea... 提取码: 8k17

END

来源:strongerHuang

推荐阅读

欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。

推荐阅读
关注数
2923
内容数
370
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息