基于纳芯微实时控制MCU NS800RT5039的IAP固件升级指南(2)

博主:旭日财富者旭日财富者 2026-02-06 3471

来源:纳芯微电子

在上期《无需返厂!基于实时控制MCU NS800RT5039 的 IAP 固件升级指南(理论篇)》中,我们已经详细拆解了 IAP 技术的核心原理、关键概念、方案设计逻辑以及实战中常见问题的避坑要点。本篇将聚焦实操落地,以 MDK 开发环境为依托,展示如何基于 NS800RT5039, 从 0 开发一个简单的 IAP 项目。

硬件准备和环境搭建

NSSinePad-NS800RT5039_V2.0开发板×1

f973038e-026b-11f1-90a1-92fbcf53809c.jpg

图1 开发板

USB串口模块:可使用 NSSinePad 板载USB串口

IAP上位机准备:UartAssist串口调试助手或自行开发

图2 上位机界面

工程环境准备

安装MDK

解压《NS800RT5XXX-SDK-v1.4.0》开发包

导入安装 SDK 目录下的 "utilitiespackMDKNOVOSENSE.NS800RT5XXX.xxx.zip" pack 包

基于 SDK 模版工程新建两个新的工程 App 和 Bootloader,配置好源文件路径和头文件路径,还有sct文件路径

fa2380c4-026b-11f1-90a1-92fbcf53809c.png

图3 新建MDK工程

方案设计总览

通讯协议设计:使用 UART 接口,无特定数据包结构,传输固件原始数据。当 MCU 准备好接收数据时,上位机以分包的形式传输升级固件,包大小为 512 字节,包延时为 125 ms ,MCU每收到一包便将数据烧录进 Flash

上位机:我们可以用串口调试助手作为上位机,使用自带的二进制流传输功能进行固件的发送

Bootloader 启动方式:将 Bootloader 固件烧写在默认的 Flash 启动地址,MCU 复位默认启动 Bootloader

App 启动方式:在 Bootloader 中判断 App 的有效性,App 有效则软件跳转,App 无效或者 BootKey 按下则进入 Bootloader 升级程序

分区地址规划:

App 工程和 Bootloader 工程编译输出的固件结构大致如下所示:

fad9c99c-026b-11f1-90a1-92fbcf53809c.jpg

图4 bin固件结构

将它们的位置划分在 Flash 的不同区域:

fb31fbf8-026b-11f1-90a1-92fbcf53809c.jpg

图5 App 和 Bootloader 固件区域划分

考虑 Ram 资源的使用:

Bootloader 是通过复位 MCU 的方式启动,每次启动都会再次进行堆栈的初始化操作

App 通过软件跳转的方式启动,每次启动也会再次进行堆栈初始化操作

当从 Bootloader 跳转 App 时,因为方案中 App 与 Bootloader 没有共享的 Ram 数据,Bootloader 所使用的 Ram 数据对于 App 是不重要的,App 可以在启动时覆盖掉 Ram 空间,同理 Bootloader 也是一样的情况,因此 Bootloader 和 App 在对 Ram 利用率的角度上考虑,可以共用同一块 Ram 空间,它们可以忽略 Ram 的资源竞争问题。

程序启动时 Flash 空间和 Ram 空间的使用情况大致如下所示:

fb8d4170-026b-11f1-90a1-92fbcf53809c.jpg

图6Flash 和 Ram 资源使用示意图

IAP 升级程序通信流程

在 Bootloader 中通过 UART 接收来自上位机的 App 固件数据,核心逻辑如下图所示:

fbe49614-026b-11f1-90a1-92fbcf53809c.jpg

图7 UART数据接收程序流程图

程序实现如下:

程序清单1 UART 升级主程序

struct
{
 uint8_trxDataBuf[IAP_UART_PACKETSIZE];
 intrxcnt;
} iap;


intuart_download(void)
{
 intres =0;
 uint32_tflash_address = IAP_APP_ADDR;
 printf("Prepare for downloading
");


 /* 接收状态初始化 */
 IAP_readyUart();


 /* 等待第一个数据 */
 while(UART_getRxFifoStatus(IAP_UART) == UART_FIFO_RX0) { }
 uint32_ttick =SystemTimer_getTime();


 while(SystemTimer_getTime() - tick < 2000)
    {
        if (0 != IAP_receiveUartData())
        {
            return -1;
        }


        if (iap.rxcnt == IAP_UART_PACKETSIZE)
        {
            res = IAP_writeFlash(flash_address, iap.rxDataBuf, iap.rxcnt);
            if (res) return res;
            flash_address += IAP_UART_PACKETSIZE;
            iap.rxcnt = 0;
            tick = SystemTimer_getTime();
        }
    }
    /* UART数据流中止,传输结束 */
    if (iap.rxcnt >0)
  {
    res =IAP_writeFlash(flash_address, iap.rxDataBuf, iap.rxcnt);
   if(res)returnres;
  }
 return0;
}

其中从 UART RXFIFO 读取全部数据保存到数据缓冲区中,使用下方UART数据接收函数实现。

程序清单2 UART 接收数据

staticintIAP_receiveUartData(void)
{
 intrxcnt = (int)UART_getRxFifoStatus(IAP_UART);
 if(iap.rxcnt + rxcnt >512)
   return-1;
   
 if(UART_getStatus(IAP_UART) & (UART_STAT_OR | UART_STAT_NF | UART_STAT_FE | UART_STAT_PF))
   return-1;


 while(rxcnt--)
  {
    iap.rxDataBuf[iap.rxcnt]= UART_readChar(IAP_UART);
    iap.rxcnt++;
  }


 return0;
}

使用 sct 文件

配置 Bootloader 与 App 的分区

Bootloader 的 sct 文件

程序清单3 Bootloader 的 sct 配置

;*************************************************************
;*** Scatter-Loading Description File generated by uVision ***
;*************************************************************


LR_IROM1 0x08000000 0x00004000 {  ; load region size_region
 ER_IROM1 0x08000000 0x00004000 { ; load address = execution address
  *.o (RESET, +First)
  *(InRoot$$Sections)
  .ANY (+RO)
  .ANY (+XO)
 }


 RW_ITCM 0x00000000 0x00010000 { ; 64K ITCM -> code in sram
 *.o (.itcm)
 }


 RW_VT_DTCM 0x20000000 0x00000400 { ; 1K VT_DTCM -> Vector Table
  * (.vt_dtcm)
 }


 RW_DTCM 0x20000400 0x00010000 { ; 63K DTCM -> fast sram
  .ANY (.dtcm, +RW +ZI)
  startup_*.o(STACK)
  startup_*.o(HEAP)
 }


 RW_SRAM1 0x20100000 0x00020000 { ; sram with parity
  * (.sram1)
 }
 RW_SRAM2 0x20120000 0x00020000 { ; sram with ECC
  * (.sram2)
 }
 RW_SRAMB 0x400C7000 UNINIT 0x00001000 { ; sram with backup
  * (.bss.backupsram)
 }
}

App 的 sct 文件

程序清单4 App 的 sct 配置

LR_IROM10x080040000x0007C000 {  ; load region size_region
ER_IROM10x080040000x0007C000 { ; load address = execution address
  *.o(RESET, +First)
  *(InRoot$$Sections)
  .ANY(+RO)
  .ANY(+XO)
 }


RW_ITCM0x000000000x00010000 { ; 64KITCM-> codeinsram
  *.o(.itcm)
 }


RW_VT_DTCM0x200000000x00000400 { ; 1KVT_DTCM->VectorTable
  * (.vt_dtcm)
 }


RW_DTCM0x200004000x00010000 { ; 63KDTCM-> fast sram
  .ANY(.dtcm, +RW+ZI)
  startup_*.o(STACK)
  startup_*.o(HEAP)
 }


RW_SRAM10x201000000x00020000 { ; sramwithparity
  * (.sram1)
 }
RW_SRAM20x201200000x00020000 { ; sramwithECC
  * (.sram2)
 }
RW_SRAMB0x400C7000UNINIT0x00001000 { ; sramwithbackup
  * (.bss.backupsram)
 }
}

向量表重定向

在 NS800RT5039 的 SDK 中,向量表重定向的操作在 SystemInit 函数中执行。

Bootloader 的 SystemInit 函数

如下方代码所示,line 9 处初始化VTOR寄存器地址。由于 Bootloader 固件处于 Flash 默认启动地址,故使用默认文件即可:

程序清单5 Bootloader 的 SystemInit 配置

voidSystemInit (void) {
 /************************************** SCB register start **************************************/
  SCB_EnableICache();
  SCB_EnableDCache();
  SCB->CACR|= SCB_CACR_FORCEWT_Msk;
 /* Enable CP10/CP11 Full Access */
  SCB->CPACR |= ((3U << 10U*2U) | (3U << 11U*2U));
    /* Set the base address of interrupt vector table */
    SCB->VTOR =0x08000000;


 /*************************************** SCB register end ***************************************/
 
 /************************************** system clock start **************************************/
  RCC->UNLOCK.WORDVAL =0x55AA6699;
 /* Enable MIRC2 & Check MIRC2 is ready */
  RCC->CR.BIT.MIRC2EN =1;
 while(!RCC->CR.BIT.MIRC2RDY) {;}
 /* Set MIRC2 as system clock */
  RCC->CFGR.BIT.SWSEL =0;
 /* Lock rcc register */
  RCC->UNLOCK.WORDVAL =0x55AA6698;
 /*************************************** system clock end ***************************************/
}

App 的 SystemInit 函数

App 固件的起始地址不是 MCU 默认启动地址,需要修改向量表重定向的地址,如下方 line 9处:

程序清单6 App 的 SystemInit 配置

voidSystemInit (void){
 /************************************** SCB register start **************************************/
  SCB_EnableICache();
  SCB_EnableDCache();
  SCB->CACR|= SCB_CACR_FORCEWT_Msk;
 /* Enable CP10/CP11 Full Access */
  SCB->CPACR |= ((3U << 10U*2U) | (3U << 11U*2U));
    /* Set the base address of interrupt vector table */
    SCB->VTOR =0x08004000;


 /*************************************** SCB register end ***************************************/


 /************************************** system clock start **************************************/
  RCC->UNLOCK.WORDVAL =0x55AA6699;
 /* Enable MIRC2 & Check MIRC2 is ready */
  RCC->CR.BIT.MIRC2EN =1;
 while(!RCC->CR.BIT.MIRC2RDY) {;}
 /* Set MIRC2 as system clock */
  RCC->CFGR.BIT.SWSEL =0;
 /* Lock rcc register */
  RCC->UNLOCK.WORDVAL =0x55AA6698;
 /*************************************** system clock end ***************************************/
}

从 Bootloader 跳转至 App

跳转是 IAP 技术的关键步骤,需要依据启动标准流程进行操作,下方给出通过C语言实现的跳转方法:

设置栈顶指针

设置主栈操作使用 cmsis_armclang_m.h 的函数,如程序清单6所示。

程序清单7 __set_MSP() 函数

__STATIC_FORCEINLINEvoid__set_MSP(uint32_t topOfMainStack)
{
 __ASMvolatile("MSR msp, %0": :"r"(topOfMainStack) : );
}

传入的 topOfMainStack 参数为 App 区的中断向量表的第一个元素的值,即主栈的指针值。

基于 __set_MSP() 封装的通用跳转函数

程序清单8 使用向量表地址进行跳转

staticvoidjump_to(uint32_tVectorTableAddress)
{
 /* 关闭全局中断 */
 Interrupt_disableGlobal();


 /* 使用静态变量保存函数指针 */
 typedefvoid(* __IO jump_ft)(void);
 staticjump_ft jump;
  jump = (jump_ft)(*(uint32_t*)(VectorTableAddress +4));


 /* 数据同步屏障 */
  __DSB();


 /* 手动加载栈顶地址 */
  __set_MSP(*(__Iuint32_t*)VectorTableAddress);


 /* 数据同步屏障 */
  __DSB();


 /* 跳转 VectorTable->Reset_Handle */
 jump();
}

封装 IAP 层的跳转函数

程序清单9 跳转到 App

staticvoidIAP_jumpApp(void)
{
 printf("Jump to App.
");
 /* 反初始化 */
 IAP_deinit();


 /* 跳转 */
 jump_to(IAP_APP_ADDR);
}

Flash 的擦除与写入

升级过程中需将接收到的 APP 固件写入 EFlash,需实现擦除、写入两个核心接口:

Flash 的擦除

擦除操作代码如程序清单10所示,该函数通过调用 SDK API 擦除Flash,从 App 起始地址开始页擦,连续擦除指定数量的页面。

程序清单10 Flash 擦除操作

staticintIAP_eraseFlash_AppRegion(void)
{
 uint8_tret;


 SCB_DisableDCache();
 SCB_DisableICache();


 for(inti =0; i < IAP_APP_PAGENUMS; i++)
    {
        ret = FLASH_erasePage(IAP_APP_ADDR + FLASH_PAGESIZE * i);
        if (ret == 0)
            break;
    }


    SCB_EnableDCache();
    SCB_EnableICache();
    return ret - 1;
}

Flash 的写入

对 NS800RT5039 的 Flash 执行写入操作时,其地址需要满足以 8 bytes 边界对齐,并且写入数量需要满足 8 的倍数,SDK 仅提供基础接口,写入地址和写入数量需要满足上述要求,为了方便 IAP 层操作 Flash 需要封装拓展功能,使其能够支持写入任意长度的数据。

程序清单11 Flash 写入操作

staticintIAP_writeFlash(uint32_taddr,constuint8_t*pBuf,uint32_tcnt)
{
 SCB_DisableDCache();
 SCB_DisableICache();


  __IOuint8_tflashBuf[8];
 uint8_t*pBufTmp = (uint8_t*)pBuf;
 uint32_twrite_len = cnt & ~(0x7U);
 intret =0;
 if(write_len >0)
  {
   if(1==FLASH_writeBytes(addr, pBufTmp, write_len))
    {
      pBufTmp += write_len;
      addr += write_len;
    }
   else
      ret =-3;
  }


 if(ret ==0)
  {
   uint32_talign = cnt &0x7U;
   if(align >0)
    {
     for(uint32_ti =0; i < align; i++)
                flashBuf[i] = pBufTmp[i];


            for (uint32_t i = align; i < 8U; i++)
                flashBuf[i] = 0xFFU;


            write_len = 8;
            if (1 == FLASH_writeBytes(addr, (uint8_t*)flashBuf, write_len))
                pBufTmp += write_len;
            else
                ret = -3;
        }
    }


    SCB_EnableDCache();
    SCB_EnableICache();
     return ret;
}

App 程序中的中断向量初始化

在 App 工程中,我们通过 STIM1 的中断触发翻转 LED1 和 LED2,以此来演示 App 的程序和中断的正常运行。

下面是通过调用 SDK API 初始化中断服务的代码:

程序清单12 App 中断初始化

intmain(void)
{
 /*  ...  */
 Interrupt_initModule();
 Interrupt_initVectorTable();
 Interrupt_register(STIM1_IRQn,STIM1_IRQHandler);
 Interrupt_enable(STIM1_IRQn);
 /*  ...  */
}

如上图所示,在当前版本的 SDK 的 Interrupt_initVectorTable 函数中,该函数将向量表复制到 DTCM 区域,并重定向,以此来提高中断响应速度:

程序清单13 SDK API 中断向量表初始化

voidInterrupt_initVectorTable(void)
{
 uint32_tn;
 uint32_t*vector_table_flash = (uint32_t*)VECTOR_TABLE_FLASH_ADDRESS;


 for(n =0; n < NVIC_USER_IRQ_OFFSET; n++) {
        vectorTableDTCM[n] = vector_table_flash[n];
    }


    SCB->VTOR = (uint32_t)vectorTableDTCM;
  __ISB();


 for(n = NVIC_USER_IRQ_OFFSET; n < VECTOR_SIZE; n++)
    {
        vectorTableDTCM[n] = (uint32_t)&Interrupt_defaultHandler;
    }
}

VECTOR_TABLE_FLASH_ADDRESS 是定义在 interrupt.h 的常量宏:

需要将 interrupt.c 和 interrupt.h 创建副本并将下方宏改为 App 向量表地址 0x08004000,在工程中使用副本的 interrupt.c 和 interrupt.h 替代驱动库(不建议直接改驱动库)。

#define VECTOR_TABLE_FLASH_ADDRESS  0x08004000UL  // Starting address of the interrupt vector table in FLASH

配置 MDK 输出 bin 文件

按下图操作,配置 MDK,输入指令 "fromelf --bin --output=$L@L.bin !L" :

fc3c3860-026b-11f1-90a1-92fbcf53809c.png

图8 MDK 配置生成 bin 文件

编译后自动输出 bin 文件到 output 文件夹。

IAP 升级完整流程

编译好 Bootloader 工程和 App 工程

点击 MDK 上方选项卡 Flash -> Erase,擦除全部 Flash,确保 Flash 无数据

下载 Bootloader 固件

连接 PC 与板载串口,打开串口调试助手,并选择文件传输模式,浏览 App.bin 文件准备传输

fca2023a-026b-11f1-90a1-92fbcf53809c.png

图9 串口调试助手使用方法

复位开发板,MCU 发出串口信息,看到升级程序处于准备状态

fcfd557c-026b-11f1-90a1-92fbcf53809c.png

图10 MCU 串口打印“准备中”

按下发送按钮,串口助手开始传输

fd563b74-026b-11f1-90a1-92fbcf53809c.png

图11串口调试助手发送固件

传输结束,自动跳转至 App 中,可以看到中断翻转LED正常,串口打印信息

fdad71be-026b-11f1-90a1-92fbcf53809c.png

图12MCU 串口打印“运行App”

fe068fce-026b-11f1-90a1-92fbcf53809c.gif

图13 App 运行效果

结论与建议

本文基于 NS800RT5039 的 IAP 方案,通过 “分区 + 通信 + 跳转 + 存储” 四大模块,实现了设备现场固件升级,无需返厂依赖烧录工具。方案代码可直接复用,避坑要点覆盖多数实战问题,适用于工业控制消费电子等场景,能大幅降低升级成本、提升设备维护效率。

如需获取《NS800RT系列IAP实现原理及参考样例》应用笔记,请联系sales@novosns.com。更多产品信息、技术资料敬请访问www.novosns.com。

纳芯微电子(简称纳芯微,科创板股票代码:688052;香港联交所股票代码:02676.HK)是高性能高可靠性模拟及混合信号芯片公司。自2013年成立以来,公司聚焦传感器、信号链、电源管理三大方向,为汽车、工业、信息通讯及消费电子等领域提供丰富的半导体产品及解决方案。

纳芯微以『“感知”“驱动”未来,共建绿色、智能、互联互通的“芯”世界』为使命,致力于为数字世界和现实世界的连接提供芯片级解决方案。