通过函数指针在 RAM 中运行外部程序(非 XIP 方案)
背景
单片机程序通常运行在内部 Flash 上,而内部 Flash 一般不允许外部直接访问。当需要让单片机执行一段"外部程序"(例如可更新的算法模块、第三方代码)时,常见的思路有两条:
- XIP(Execute In Place):直接跳转到外部 Flash 上取指执行。优点是无需搬运,缺点是要求 MCU 硬件支持 XIP 总线。
- 搬运到 RAM 执行:将外部程序从外部 Flash 读入 RAM 的指定区域,再跳转执行。适用于不支持 XIP 的芯片。
本文讨论第二种方案,基于 ARM Cortex-M + Keil MDK 环境,用分散加载(Scatter File)规划 RAM 布局,用函数指针数组向外部程序提供 API。
注意:本文代码为简化演示,未处理栈初始化、中断屏蔽、边界校验等生产级问题,不能直接用于量产固件。
整体思路
- 用 Keil 分散加载文件在 RAM 中划出三块区域:API 接口区、外部代码执行区、常规 RW/ZI 区。
- 主固件通过函数指针数组暴露可调用的 API,外部程序按约定地址查找并调用。
- 运行时将外部 Flash 中已映射到地址空间的程序二进制拷贝到执行区,跳转到入口执行。
第一步:配置分散加载文件
1.1 关闭 Target 对话框的内存布局
打开 Keil,进入 Options for Target → Linker,取消勾选 Use Memory Layout from Target Dialog。

取消后,Target 对话框中的内存分配表将不再生效,链接器完全由我们自己的 .sct 文件控制。

1.2 编写 .sct 文件
在 Linker 选项卡中点击 Edit 编辑(或新建).sct 文件:

LR_IROM1 0x10000000 0x0003E000 { ; load region size_region
ER_IROM1 0x10000000 0x0003E000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
EXPORT_TB 0x200017a0 0x00000200 { ; 此段存放导出函数, 512字节, 一共可以放128个函数地址
*.o (.export_tb)
}
EXTERN_EXEC 0x200019a0 0x00001000 { ; 外部模块加载到这里执行
.ANY (.extern_exec)
}
RW_IRAM1 0x200029a0 0x00005660 { ; RW data
.ANY (+RW +ZI)
}
}各区域说明:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| ER_IROM1 | 0x10000000 | 0x3E000 | 主固件代码(内部 Flash) |
| EXPORT_TB | 0x200017A0 | 0x200(512 B) | 存放导出给外部程序的 API 函数指针 |
| EXTERN_EXEC | 0x200019A0 | 0x1000(4 KB) | 外部程序加载与执行区 |
| RW_IRAM1 | 0x200029A0 | 0x5660 | 主固件 RW + ZI 段 |
以上地址和大小是针对特定 MCU 的 RAM 布局,移植到其他芯片时必须重新规划,确保各区域不重叠且总大小不超过可用 RAM。
第二步:向外部程序提供 API 接口
typedef void (*export_api_t)(void);
uint16_t GetLibraryVersion(void)
{
return 0x0080; //0.0.1.0
}
const export_api_t export_apis[] __attribute__((__used__)) __attribute__((__section__(".extern_exec"))) =
{
(export_api_t)GetLibraryVersion, //获取版本号
};要点说明:
export_api_t是一个void(void)函数指针类型。实际项目中应根据需要扩展参数和返回值,例如typedef uint32_t (*export_api_t)(uint32_t arg);。__attribute__((__used__))告诉编译器该数组不会被优化掉。__attribute__((__section__(".extern_exec")))将数组放入.extern_exec段。
原文不一致提醒:sct 文件中 EXPORT_TB 区域(0x200017A0,512 B)匹配的是
.export_tb段,而代码中export_apis标注的 section 是.extern_exec,会被链接到 EXTERN_EXEC 区域(0x200019A0)。这意味着 API 表与外部代码缓冲区位于同一区域,存在重叠覆盖风险。若需将 API 表独立存放,应将 section 改为.export_tb。
按代码中的 section 标注,export_apis 实际位于 EXTERN_EXEC 区域(0x200019A0 起),外部程序需按实际链接地址查找并调用。
第三步:加载并执行外部程序
typedef void (*pFunction)(void);
pFunction JumpToApplication;
/* Applet代码执行区 */
uint8_t extern_exec_ram[4096] __attribute__((__used__)) __attribute__((__section__(".extern_exec"))) ;extern_exec_ram[4096]对应 sct 中 EXTERN_EXEC 区域(4 KB),用于存放外部程序二进制。JumpToApplication是跳转用的函数指针。
拷贝与跳转
uint32_t *p_ram = (uint32_t *)extern_exec_ram;
uint32_t *p = (uint32_t*) (0x01700000);
for(int i = 0; i < 1024; i++)
{
*p_ram = *p;
p++;
p_ram++;
}
JumpToApplication = (pFunction)(extern_exec_ram + 1);
JumpToApplication();执行流程:
- 将地址
0x01700000起始的 1024 个 32-bit 字(共 4 KB)拷贝到extern_exec_ram。 extern_exec_ram + 1的数值是基地址加 1。在 Cortex-M 上,分支目标地址的最低位用来表示 Thumb 状态;CPU 取指时会使用对齐后的地址。因此这里更像是在为 RAM 基地址设置 Thumb 位,不是“从第 2 个字节开始执行”。- 通过函数指针跳入 RAM 代码。更清晰的表达是先转为
uintptr_t,再用entry | 1u设置 Thumb 位;对象指针与函数指针的转换仍属工具链相关行为,必须按 ARMCC/armclang 文档验证。
0x01700000 被直接解引用,这要求该外部存储区已映射到 MCU 地址空间。若外部 Flash 只能通过 SPI/QSPI 命令访问,就应调用驱动读取,不能直接使用指针拷贝。
风险与边界条件
- 模块形式要先定义:这段代码更接近“按指定 RAM 地址链接的函数模块”,而不是带初始 SP/复位向量的完整固件。模块会沿用主程序的栈、向量表和运行时环境,双方必须统一 ABI、调用约定和可用 RAM 范围。
- Thumb 入口依赖工具链:最低位置 1 是 Cortex-M 函数地址的常见处理,但应使用
uintptr_t明确表达,并检查链接生成的入口符号,不要凭猜测固定偏移。 - 段容量可能冲突:
EXTERN_EXEC只有 4 KB,而extern_exec_ram本身已经是 4 KB。export_apis又被放入同一.extern_exec段时,链接器应报区域溢出,或者布局与预期不一致。API 表应改放.export_tb,并以 map 文件为准检查地址。 - 长度和完整性未校验:示例始终拷贝 4 KB,没有模块头、实际长度、CRC/签名或版本检查。量产方案应先验证再跳转。
- 栈与中断共享:函数模块使用主程序当前栈,需要预留足够栈空间并保存被调用者要求的寄存器。中断是否屏蔽取决于模块协议;若保留中断,ISR 依赖的全局状态不得被模块破坏。
- 平台依赖:地址、分散加载语法、可执行 RAM 权限和指针转换都与 MCU/工具链有关,必须结合芯片手册、map 文件和反汇编确认。
验证建议
- 在 RAM 中放置一段简单的
while(1)或 LED 翻转程序作为外部程序,确认跳转后主固件不再执行。 - 用调试器查看跳转后 PC 是否落在
EXTERN_EXEC区域,并确认 LR 和栈指针仍满足主程序调用约定。 - 在外部程序中故意触发一个中断,观察是否导致 HardFault,以此验证中断处理策略。
- 对 API 调用做单元测试:外部程序调用
GetLibraryVersion()并返回预期值。
小结
本文演示了在不支持 XIP 的 Cortex-M 平台上,通过分散加载规划 RAM、函数指针暴露 API、内存拷贝后跳转执行外部程序的基本流程。该方案的核心约束是外部程序必须能放入预留的 RAM 区域,且执行期间与主固件共享同一套中断和栈资源,因此对资源隔离的要求较高。后续还需要实现外部程序的下载/更新流程(写入外部 Flash),以及更完善的 API 调用约定和错误处理机制。
<!-- csdn-article-id: 124885524 -->本文最初于 2022/5/20 发布在 CSDN。
相关文章
评论
正在读取评论…