目录
[1. flash分散加载介绍及优势](#1. flash分散加载介绍及优势)
[2. flash地址分区](#2. flash地址分区)
[3. .ld脚本配置](#3. .ld脚本配置)
[4. 实现 LPUART 驱动共享代码](#4. 实现 LPUART 驱动共享代码)
[5. 使用共享代码](#5. 使用共享代码)
1. flash分散加载介绍及优势
1.1 flash分散加载介绍
Flash分散加载(Scatter Loading)是一种通过修改链接脚本(.ld脚本/GCC环境、.sct脚本/Keil环境),将单片机程序的不同模块(如BootLoader、应用程序、共用驱动、核心算法)分配到flash的不同地址分区,实现代码模块化管理、复用的加载方式。
传统的集中式加载,会将所有代码打包到一个连续的flash区域,导致BootLoader和APP中重复的驱动代码(如串口、无线通信)被多次存储,浪费宝贵的flash空间;而分散加载通过"分区管理+代码复用",让多个程序(Boot、APP1、APP2)共享同一部分代码,从根源上解决空间不足问题。
1.2 flash分散加载的核心优势
*** 省flash空间:** 将Boot和APP共用的代码(如LPUART驱动、无线通信代码)独立到共享分区,避免重复存储,尤其适合64KB及以下小容量flash芯片,可节省10%-30%的空间,为OTA升级、新增功能预留足够空间。
*** 提升调试效率:** 对于体积较大、逻辑固定的代码(如复杂算法、通用驱动),可将其固定到共享分区,调试应用程序时,无需每次都下载完整代码,仅下载应用层代码即可,大幅缩短下载时间。
*保护核心源码: 多人协作开发时,可将核心算法、加密逻辑等关键代码放到共享分区,仅提供函数调用接口,不暴露源码,避免核心技术泄露。
*支持OTA升级: OTA升级时,BootLoader和APP需共用通信驱动(如LPUART、SPI),分散加载可实现驱动代码复用,避免Boot和APP分别存储驱动导致的空间不足,确保升级流程稳定。
2. flash地址分区
以HC32系列芯片,64KB Flash为例。
| 分区 | Flash起始地址 | 大小(KB) | 功能 |
|---|---|---|---|
| BootLoader | 0x00000000 | 6 | OTA 升级引导程序 |
| 共享固件 | 0x00001800 | 6 | 存放Boot和APP共用的代码程序 |
| 参数存储区 | 0x00003000 | 2 | 存放重要的配置参数 |
| APP1 | 0x00003800 | 25 | 主应用程序1 |
| APP2 | 0x00009C00 | 25 | 主应用程序2 |
3. .ld脚本配置
VsCode + GCC编译器环境下,链接脚本(.ld文件)是实现flash分散加载的核心,通过修改.ld脚本,划分flash分区、指定各模块的存储地址。本文以LPUART串口程序共享为例,详细讲解Boot程序.ld脚本的修改方法(APP程序.ld脚本修改逻辑类似)。
3.1 修改Boot程序的链接脚本(.ld)
首先查看原始.ld脚本的默认配置,再根据前面的分区规划,修改分区定义和代码段绑定,确保共享代码能正确定位到共享固件区。
bash
# 原始 .ld 配置
MEMORY
{
FLASH (rx): ORIGIN = 0x00000000, LENGTH = 64K
RAM (rwx): ORIGIN = 0x20000000, LENGTH = 8K
}
bash
# 修改后的 .ld 配置
MEMORY
{
FLASH (rx): ORIGIN = 0x00000000, LENGTH = 6K
FLASH_SHARED (rx) : ORIGIN = 0x00001800, LENGTH = 6K
RAM (rwx): ORIGIN = 0x20000000, LENGTH = 8K
}
# /* SECTIONS 增加 */
SECTIONS
{
.fini_array :
{
/* finit data */
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP(*(SORT(.fini_array.*)))
KEEP(*(.fini_array))
PROVIDE_HIDDEN (__fini_array_end = .);
} >FLASH
# /* 前面其余部分不变 */
# /*增加 shared_code */
.shared_code :
{
. = ALIGN(4);
KEEP(*(.shared_ptr_table)) /* 函数指针表(固定在共享区起始地址) */
KEEP(*(.shared_func)) /*要共享的所有函数存放位置 */
. = ALIGN(4);
} > FLASH_SHARED /* 绑定到共享FLASH分区 */
# /* 后面其余部分不变 */
__etext = LOADADDR(.data);
.data :
{
} >RAM AT> FLASH
}
# 关键配置说明
# MEMORY分区修改:新增FLASH_SHARED分区,指定起始地址0x00001800、大小6K,与前面的分区规划完全对应;FLASH分区缩小为6K,仅用于存放BootLoader代码。
# .shared_code段:专门用于存放共享代码和函数指针表,绑定到FLASH_SHARED分区,确保共享代码能被固定到指定地址。
# ALIGN(4):ARM架构要求代码地址4字节对齐,否则会导致CPU取指异常,必须添加该配置。
# KEEP关键字:GCC编译器在优化时,可能会删除未被当前工程直接调用的代码(如共享函数),KEEP关键字可强制保留指定段的代码,确保共享函数不被优化删除。
4. 实现 LPUART 驱动共享代码
共享代码的核心是"将需要复用的函数(如LPUART驱动)固定到共享区,并通过函数指针表提供调用接口",Boot和APP通过访问函数指针表的固定地址,即可调用共享区的驱动函数,无需重复编写驱动代码。本节实现HC32系列芯片的LPUART驱动共享,包含头文件和源文件的示例实现。
4.1 LpuartDrv.h 头文件实现
c
#ifndef __LPUARTDRV_H
#define __LPUARTDRV_H
#include <stdint.h>
// 函数指针类型定义
typedef void (*Ptr_LpUartInit)(void);
typedef uint32_t (*Ptr_LpUartRead)(uint8_t* buf, uint32_t bufLen);
typedef void (*Ptr_LpUartWrite)(uint8_t* data, uint32_t dataLen);
// 函数指针表结构体定义
typedef struct {
Ptr_LpUartInit init;
Ptr_LpUartRead read;
Ptr_LpUartWrite write;
} LPUART_API_TABLE_T;
// 共享区固定起始地址
#define LPUART_API_ADDR 0x00001800
#define g_LpuartDrv ((const LPUART_API_TABLE_T *)LPUART_API_ADDR)
4.2 LPUART.c 文件实现
c
#include "LpuartDrv.h"
#include "ddl.h"
#include "gpio.h"
#include "lpuart.h"
#include "string.h"
// / 所有函数,强制放入 .shared_func 区域
// GPIO初始化函数
__attribute__((section(".shared_func")))
static void lpUartInitGpio(void)
{
}
// 串口初始化函数
__attribute__((section(".shared_func")))
static void lpUartInit(void)
{
}
// 初始化函数
__attribute__((section(".shared_func")))
void LpUartDrvInit(void)
{
lpUartInitGpio();
lpUartInit();
}
// 读串口数据
__attribute__((section(".shared_func")))
uint32_t LpUartDrvRead(uint8_t* buf, uint32_t bufLen)
{
}
// 发送串口数据
__attribute__((section(".shared_func")))
void LpUartDrvWrite(uint8_t* data, uint32_t dataLen)
{
}
// 串口中断处理函数
__attribute__((section(".shared_func")))
void LPUART0_IRQHandler(void)
{
}
// 函数指针表 → 固定放在共享区起始地址 LPUART_API_ADDR
__attribute__((section(".shared_ptr_table")))
const LPUART_API_TABLE_T s_Lpuart_Api = {
.init = LpUartDrvInit,
.read = LpUartDrvRead,
.write = LpUartDrvWrite,
};
5. 使用共享代码
共享代码编译完成后,Boot和APP工程无需再编写LPUART驱动代码,仅需包含头文件LpuartDrv.h,通过函数指针表即可调用共享区的驱动函数。同时,需配置中断向量表,确保共享区的中断函数能正常响应。
Boot/APP工程无需添加LPUART.c文件,仅需包含头文件,通过g_LpuartDrv指针访问共享驱动接口,实现串口通信。
5.1 APP工程调用示例
C
#include "LpuartDrv.h"
#include "string.h"
int main (void)
{
// 直接调用 LPUART_API_ADDR 地址共享的函数代码接口
g_LpuartDrv->init();
char *send = "App Project Run";
g_LpuartDrv->write(send, strlen(send));
}
5.2 更改 LPUART0_IRQHandler 向量为绝对物理地址
共享区的串口中断函数 LPUART0_IRQHandler 是全局唯一的,Boot 和 APP 的汇编向量表中,串口中断向量必须全部直接指向共享区的这个函数物理地址 ,且两个程序都要配置中断向量重定向 (VTOR) .
bash
# 1. 编译共享代码工程后,打开 .map 文件 ,搜索 LPUART0_IRQHandler 。
# 如下 .map文件 LPUART0_IRQHandler 示例:
.shared_code 0x00001800 0x354
0x00001800 . = ALIGN (0x4)
*(.shared_ptr_table)
.shared_ptr_table
0x00001800 0x18 ./build/Debug/.obj/LpUart/LpuartDrv.o
0x00001800 g_LpuartDrv
*(.shared_func)
.shared_func 0x00001818 0x33c ./build/Debug/.obj/LpUart/LpuartDrv.o
0x00001918 LpUartDrvInit
0x00001a3c LpUartDrvRead
0x00001ad8 LpUartDrvWrite
0x00001b04 LPUART0_IRQHandler
0x00001b54 . = ALIGN (0x4)
0x00005ab4 __etext = LOADADDR (.data)
# 2. 记录 LPUART0_IRQHandler 的绝对物理地址 0x00001b04 ; 这个地址是固定的,Boot/APP 向量表都用它 .
# 3. 更改 Boot/APP 工程里的 .S文件, 将 LPUART0_IRQHandler 替换为绝对物理地址 0x00001b04
# .S文件的 中断向量表 如下:
/* Interrupts */
.long PORTA_IRQHandler
.long PORTB_IRQHandler
.long PORTC_IRQHandler
.long PORTD_IRQHandler
.long DMAC_IRQHandler
.long TIM3_IRQHandler
.long UART0_IRQHandler
.long UART1_IRQHandler
# .long LPUART0_IRQHandler 更改此行
.long 0x00001b04 # 替换为共享区中断物理地址
.long LPUART1_IRQHandler
.long SPI0_IRQHandler
.long SPI1_IRQHandler
.long I2C0_IRQHandler
# 原写法 .long LPUART0_IRQHandler:链接器会找当前工程的函数地址
# 新写法 .long 0x00001B04:直接告诉 MCU 硬件物理地址,跨工程、跨分区通用
5.3 Boot/App进行 中断向量表重定向
bash
# 1. Boot 程序 VTOR 配置 为 Boot 分区的起始地址
SCB->VTOR = FLASH_BOOT_ADDR_START;
# 2. App 程序 VTOR 配置 为 App 分区的起始地址
SCB->VTOR = FLASH_APP_ADDR_START;