4、DSP存储器架构:哈佛架构与冯诺依曼架构、片内RAM/ROM、Flash与Bootloader、存储器映射表
好,咱们今天聊点硬核的——DSP的存储器架构。说实话,我刚入行那会儿,觉得存储器不就是存数据的地方嘛,有啥好研究的?直到第一次调DSP的Bootloader,程序死活跑不起来,查了三天才发现是存储器映射配错了。从那以后,我每次做新项目,第一件事就是先把存储器映射表打印出来贴在工位上。
4.1 哈佛架构 vs 冯诺依曼架构
这两个概念,说白了就是CPU怎么跟存储器“说话”的两种方式。
冯诺依曼架构:程序指令和数据共用一条总线、同一个存储空间。优点是设计简单,缺点是——你想想看,CPU取指令和读写数据得排队,谁先谁后?这就是所谓的“冯诺依曼瓶颈”。
哈佛架构:程序总线和数据总线物理上分开,各有各的存储空间。CPU可以一边取指令,一边读写数据,并行操作。DSP做数字信号处理,最怕的就是数据搬来搬去耽误时间,所以绝大多数DSP都采用哈佛架构或其改进型。
我在项目中遇到过最典型的例子:用某款C2000系列DSP做电机控制,PWM中断服务程序里既要查正弦表,又要算PID。如果是冯诺依曼架构,取指令和查表就得抢总线,中断响应时间直接翻倍。但哈佛架构下,指令从程序总线走,数据从数据总线走,互不干扰。
| 对比项 | 冯诺依曼架构 | 哈佛架构 |
|---|---|---|
| 总线数量 | 1条(指令+数据共用) | 至少2条(程序+数据分离) |
| 并行能力 | 串行,取指与数据访问不能同时 | 并行,取指与数据访问可同时 |
| 存储空间 | 统一编址 | 独立编址 |
| 典型DSP | 早期DSP或部分MCU | TMS320系列、ADSP系列 |
| 瓶颈 | 总线冲突,性能受限 | 程序/数据空间大小需分别规划 |
个人习惯:做DSP移植时,我第一件事就是确认目标芯片是纯哈佛还是改进型哈佛。改进型哈佛允许在数据总线上取指令(比如从RAM执行代码),这对Bootloader设计至关重要。
4.2 片内RAM与ROM
DSP芯片内部集成的存储资源,一般分这么几类:
- SARAM(单访问RAM):一个时钟周期只能被访问一次。适合存普通变量。
- DARAM(双访问RAM):一个时钟周期内可以被访问两次。我一般用它存中断向量表、堆栈这种高频访问的数据。
- ROM(只读存储器):出厂固化的程序或数据表,掉电不丢失。比如正弦表、滤波器系数。
- Flash:可擦写非易失存储器,用户程序就放这儿。
嗯,这里要注意:片内RAM的速度通常比片外快一个数量级。我见过有人把关键算法数据放在片外SDRAM里,结果实时性死活达不到。后来把数据搬到片内DARAM,延迟直接降了60%。
我曾经踩过的坑:某项目用F28335,片内RAM只有68KB。我把所有全局变量、堆栈、中断向量全塞进去,结果链接时报溢出。后来才发现,编译器默认把.const段也放RAM里了。解决方案是把常量表放到Flash里,用#pragma指定段位置。
4.3 Flash与Bootloader
Flash是DSP的“硬盘”,程序就固化在里面。但DSP上电时,Flash的读取速度慢,而且有些DSP不能直接从Flash执行代码。这时候就需要Bootloader了。
Bootloader说白了就是一段“引导程序”,它负责:
- 芯片上电后,从Flash或外部存储器加载用户程序。
- 把程序搬运到RAM中(如果需要)。
- 跳转到用户程序入口。
我习惯把Bootloader分成两类:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 片上Bootloader(ROM固化) | 出厂自带,不可修改 | 标准启动流程,串口/SPI/I2C加载 |
| 用户自定义Bootloader | 可编程,支持远程升级 | 产品现场固件更新,OTA升级 |
关键点:DSP上电后,首先执行的是固化在ROM里的Bootloader(也叫“一级引导”)。它会检查GPIO电平或特定标志位,决定从哪个外设加载程序。比如C2000系列,上电时检测SCITXDA引脚电平,低电平则进入SCI引导模式。
我曾经做过一个远程升级方案:用户Bootloader放在Flash的扇区0,应用程序放在扇区1~N。上电后Bootloader先检查是否有升级请求(通过CAN报文),有则接收新固件写入Flash,无则直接跳转应用程序。这里有个坑——跳转前必须关闭所有中断、复位外设,否则程序跑飞了都不知道。
// 典型的Bootloader跳转代码(C2000系列)
void jump_to_app(uint32_t app_addr) {
// 1. 关闭全局中断
__asm(" SETC INTM");
// 2. 禁用外设中断
PieCtrlRegs.PIECTRL.bit.ENPIE = 0;
// 3. 复位所有外设(可选)
// 4. 初始化堆栈指针
uint32_t sp = *(uint32_t*)app_addr;
// 5. 获取程序入口地址
uint32_t pc = *(uint32_t*)(app_addr + 4);
// 6. 跳转
__asm(" MOV SP, %0"::"r"(sp));
__asm(" MOV PC, %0"::"r"(pc));
}
避坑指南:跳转前一定要把看门狗关了。我曾经忘了关WDT,跳转后看门狗超时复位,又回到Bootloader,形成死循环。查了两天才发现。
4.4 存储器映射表
这是DSP开发中最基础也最容易出错的地方。每个DSP芯片出厂时,它的存储空间是怎么划分的——哪段是RAM、哪段是Flash、哪段是外设寄存器——都写在芯片手册的“Memory Map”里。
我习惯把存储器映射表理解成一张“地址地图”:
- 0x000000~0x003FFF:片内DARAM(比如存放中断向量表)
- 0x004000~0x00FFFF:片内SARAM
- 0x100000~0x1FFFFF:片内Flash
- 0x600000~0x6FFFFF:外设寄存器区
- 0x800000~0x8FFFFF:外部存储器接口(EMIF)
嗯,这里要注意:不同系列的DSP,映射表差异很大。比如C2000和C6000的地址分配完全不同。移植时如果直接套用原来的链接脚本(.cmd文件),大概率会出问题。
我曾经犯过的错:把C28346的工程直接改芯片型号到F28379D,没改.cmd文件。结果程序下载后,Flash擦写函数写到了保留地址区,芯片直接锁死。后来只能用JTAG解锁,折腾了一整天。
下面这张图是我自己总结的DSP存储器架构关系,每次做新项目前都会看一眼:
最后说一句:存储器映射表不是看一遍就完事的。每次换芯片型号、改Flash分区、加外部存储器,都得重新核对。我个人的习惯是把映射表打印出来,用荧光笔标出当前工程用到的区域,贴在显示器边上。调试时遇到地址访问异常,一眼就能看出来是不是越界了。