能否用arm-none-eabi-gcc以软件方式实现类MMU的SPI RAM内存访问?
软件模拟外部内存访问的arm-none-eabi-gcc实现方案
针对你的Cortex-M0+(SAML21)场景,无需修改libaom/libavif源码,可通过虚拟地址映射+HardFault异常拦截的方式,将特定地址范围的内存访问自动转为SPI RAM的读写函数调用,以下是具体实现思路和技巧:
核心方案:虚拟地址+HardFault拦截
1. 定义虚拟外部RAM地址范围
选择一块与内部SRAM(0x20000000~0x2000FFFF)不重叠的地址空间作为外部SPI RAM的虚拟映射区,例如:
#define EXTERNAL_RAM_BASE 0x20010000 #define EXTERNAL_RAM_SIZE 0x100000 // 1MB,根据实际SPI RAM大小调整
2. 配置MPU触发HardFault
SAML21的Cortex-M0+内置MPU,可通过配置将虚拟地址范围设为不可访问,这样当代码访问该区域时会直接触发HardFault异常。示例配置代码:
void mpu_config_external_ram(void) { // 禁用MPU SCB->MPU_CTRL &= ~SCB_MPU_CTRL_ENABLE_Msk; // 配置外部RAM虚拟地址区域为不可访问 MPU->RBAR = EXTERNAL_RAM_BASE | MPU_RBAR_VALID_Msk; MPU->RASR = MPU_RASR_SIZE_Msk(MPU_REGION_SIZE_1MB) | MPU_RASR_AP_Msk(MPU_AP_NONE) | MPU_RASR_ENABLE_Msk; // 启用MPU并允许硬 fault 触发 SCB->MPU_CTRL |= SCB_MPU_CTRL_ENABLE_Msk | SCB_MPU_CTRL_HFNMIENA_Msk; }
3. 编写HardFault异常处理函数
在HardFault handler中解析栈帧,识别内存访问指令(LDR/STR等),提取目标地址、操作类型(读/写)和数据,调用SPI RAM读写函数后,修改栈帧中的PC指针跳过当前指令,继续执行。
示例框架(需根据Thumb-1指令集完善指令解析逻辑):
void HardFault_Handler(void) { // 获取栈帧指针(异常发生时自动保存的上下文) uint32_t *stack_frame = (uint32_t *)__get_MSP(); uint32_t pc = stack_frame[6]; // 栈帧中PC的位置 uint16_t instr = *(uint16_t *)pc; // 获取当前执行的Thumb-1指令 // 判断是否为访问虚拟外部RAM的指令 uint32_t target_addr = 0; uint8_t is_read = 0; uint32_t data = 0; uint8_t rd = 0; // 解析LDR/STR指令(示例仅处理简单的立即数偏移寻址,需扩展其他寻址方式) if ((instr & 0xF000) == 0x6000) { // STR Rd, [Rn, #imm] is_read = 0; rd = (instr >> 8) & 0x0F; uint8_t rn = (instr >> 0) & 0x0F; uint32_t imm = (instr >> 4) & 0x0F; target_addr = stack_frame[rn] + imm; data = stack_frame[rd]; } else if ((instr & 0xF000) == 0x7000) { // LDR Rd, [Rn, #imm] is_read = 1; rd = (instr >> 8) & 0x0F; uint8_t rn = (instr >> 0) & 0x0F; uint32_t imm = (instr >> 4) & 0x0F; target_addr = stack_frame[rn] + imm; } // 检查目标地址是否在虚拟外部RAM范围内 if (target_addr >= EXTERNAL_RAM_BASE && target_addr < (EXTERNAL_RAM_BASE + EXTERNAL_RAM_SIZE)) { uint32_t offset = target_addr - EXTERNAL_RAM_BASE; if (is_read) { // 调用SPI RAM读函数 data = spi_ram_read(offset); stack_frame[rd] = data; // 将读取的数据放回栈帧的对应寄存器 } else { // 调用SPI RAM写函数 spi_ram_write(offset, data); } // 修改PC指针,跳过当前16位指令 stack_frame[6] += 2; // 退出异常,继续执行 return; } // 非外部RAM访问的HardFault,进入死循环(可添加调试逻辑) while (1); }
辅助技巧:自动分配虚拟地址
为了让libaom/libavif的内存分配自动落在虚拟外部RAM区域,可通过链接器--wrap选项包装标准内存分配函数:
- 在编译选项中添加链接器参数:
-Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=free
- 实现包装函数:
void *__wrap_malloc(size_t size) { // 简单顺序分配(需根据实际需求实现外部RAM的内存管理逻辑) static uint32_t current_offset = 0; if (current_offset + size > EXTERNAL_RAM_SIZE) return NULL; void *addr = (void *)(EXTERNAL_RAM_BASE + current_offset); current_offset += size; return addr; } void __wrap_free(void *ptr) { // 实现外部RAM的内存释放逻辑(若无需动态释放可简化) if ((uint32_t)ptr >= EXTERNAL_RAM_BASE && (uint32_t)ptr < (EXTERNAL_RAM_BASE + EXTERNAL_RAM_SIZE)) { // 标记对应偏移为可用(需配合内存管理链表实现) } } void *__wrap_calloc(size_t nmemb, size_t size) { void *ptr = __wrap_malloc(nmemb * size); if (ptr) { // 清零外部RAM对应区域 for (size_t i = 0; i < nmemb * size; i++) { spi_ram_write((uint32_t)ptr - EXTERNAL_RAM_BASE + i, 0); } } return ptr; }
注意事项
- 指令解析需覆盖libaom/libavif中用到的所有内存访问指令类型(如寄存器偏移、多寄存器加载/存储等),否则会触发未处理的HardFault。
- HardFault handler需尽量精简,避免在handler中执行可能再次触发内存访问的操作(如printf),防止嵌套异常。
- 该方案速度极慢,每次内存访问都需经历异常进入、指令解析、SPI传输、异常退出,仅适合对性能要求极低的场景。
- 确保SPI RAM读写函数
spi_ram_read/spi_ram_write是可重入且能在HardFault环境下安全执行(如关闭全局中断)。
内容的提问来源于stack exchange,提问作者unknownperson
相关产品推荐
相关产品推荐

