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

图1 开发板
USB串口模块:可使用 NSSinePad 板载USB串口
IAP上位机准备:UartAssist串口调试助手或自行开发
图2 上位机界面
工程环境准备
安装MDK
解压《NS800RT5XXX-SDK-v1.4.0》开发包
导入安装 SDK 目录下的 "utilitiespackMDKNOVOSENSE.NS800RT5XXX.xxx.zip" pack 包
基于 SDK 模版工程新建两个新的工程 App 和 Bootloader,配置好源文件路径和头文件路径,还有sct文件路径

图3 新建MDK工程
方案设计总览
通讯协议设计:使用 UART 接口,无特定数据包结构,传输固件原始数据。当 MCU 准备好接收数据时,上位机以分包的形式传输升级固件,包大小为 512 字节,包延时为 125 ms ,MCU每收到一包便将数据烧录进 Flash
上位机:我们可以用串口调试助手作为上位机,使用自带的二进制流传输功能进行固件的发送
Bootloader 启动方式:将 Bootloader 固件烧写在默认的 Flash 启动地址,MCU 复位默认启动 Bootloader
App 启动方式:在 Bootloader 中判断 App 的有效性,App 有效则软件跳转,App 无效或者 BootKey 按下则进入 Bootloader 升级程序
分区地址规划:
App 工程和 Bootloader 工程编译输出的固件结构大致如下所示:

图4 bin固件结构
将它们的位置划分在 Flash 的不同区域:

图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 空间的使用情况大致如下所示:

图6Flash 和 Ram 资源使用示意图
IAP 升级程序通信流程
在 Bootloader 中通过 UART 接收来自上位机的 App 固件数据,核心逻辑如下图所示:

图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" :

图8 MDK 配置生成 bin 文件
编译后自动输出 bin 文件到 output 文件夹。
IAP 升级完整流程
编译好 Bootloader 工程和 App 工程
点击 MDK 上方选项卡 Flash -> Erase,擦除全部 Flash,确保 Flash 无数据
下载 Bootloader 固件
连接 PC 与板载串口,打开串口调试助手,并选择文件传输模式,浏览 App.bin 文件准备传输

图9 串口调试助手使用方法
复位开发板,MCU 发出串口信息,看到升级程序处于准备状态

图10 MCU 串口打印“准备中”
按下发送按钮,串口助手开始传输

图11串口调试助手发送固件
传输结束,自动跳转至 App 中,可以看到中断翻转LED正常,串口打印信息

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

图13 App 运行效果
结论与建议
本文基于 NS800RT5039 的 IAP 方案,通过 “分区 + 通信 + 跳转 + 存储” 四大模块,实现了设备现场固件升级,无需返厂依赖烧录工具。方案代码可直接复用,避坑要点覆盖多数实战问题,适用于工业控制、消费电子等场景,能大幅降低升级成本、提升设备维护效率。
如需获取《NS800RT系列IAP实现原理及参考样例》应用笔记,请联系sales@novosns.com。更多产品信息、技术资料敬请访问www.novosns.com。
纳芯微电子(简称纳芯微,科创板股票代码:688052;香港联交所股票代码:02676.HK)是高性能高可靠性模拟及混合信号芯片公司。自2013年成立以来,公司聚焦传感器、信号链、电源管理三大方向,为汽车、工业、信息通讯及消费电子等领域提供丰富的半导体产品及解决方案。
纳芯微以『“感知”“驱动”未来,共建绿色、智能、互联互通的“芯”世界』为使命,致力于为数字世界和现实世界的连接提供芯片级解决方案。
