4. 开发环境搭建

好,咱们今天聊点硬核的——DSP的存储器架构。说实话,我刚入行那会儿,觉得存储器不就是存数据的地方嘛,有啥好研究的?直到第一次调DSP的Bootloader,程序死活跑不起来,查了三天才发现是存储器映射配错了。从那以后,我每次做新项目,第一件事就是先把存储器映射表打印出来贴在工位上。

4.1 哈佛架构 vs 冯诺依曼架构

这两个概念,说白了就是CPU怎么跟存储器“说话”的两种方式。

冯诺依曼架构:程序指令和数据共用一条总线、同一个存储空间。优点是设计简单,缺点是——你想想看,CPU取指令和读写数据得排队,谁先谁后?这就是所谓的“冯诺依曼瓶颈”。

哈佛架构:程序总线和数据总线物理上分开,各有各的存储空间。CPU可以一边取指令,一边读写数据,并行操作。DSP做数字信号处理,最怕的就是数据搬来搬去耽误时间,所以绝大多数DSP都采用哈佛架构或其改进型。

我在项目中遇到过最典型的例子:用某款C2000系列DSP做电机控制,PWM中断服务程序里既要查正弦表,又要算PID。如果是冯诺依曼架构,取指令和查表就得抢总线,中断响应时间直接翻倍。但哈佛架构下,指令从程序总线走,数据从数据总线走,互不干扰。

对比项冯诺依曼架构哈佛架构
总线数量1条(指令+数据共用)至少2条(程序+数据分离)
并行能力串行,取指与数据访问不能同时并行,取指与数据访问可同时
存储空间统一编址独立编址
典型DSP早期DSP或部分MCUTMS320系列、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说白了就是一段“引导程序”,它负责:

  1. 芯片上电后,从Flash或外部存储器加载用户程序。
  2. 把程序搬运到RAM中(如果需要)。
  3. 跳转到用户程序入口。

我习惯把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存储器架构关系,每次做新项目前都会看一眼:

DSP存储器架构总览 冯诺依曼架构 单总线,指令与数据共用 串行操作,存在瓶颈 哈佛架构 双总线,程序与数据分离 并行操作,DSP主流选择 片内存储资源 DARAM(双访问) 中断向量、堆栈 SARAM(单访问) 全局变量、缓冲 Flash 用户程序固化 ROM Bootloader、查表 Bootloader启动流程 上电复位 ROM Bootloader 加载用户程序 跳转至APP 关键:存储器映射表决定每个段放在哪个地址,.cmd文件必须与芯片手册一致

最后说一句:存储器映射表不是看一遍就完事的。每次换芯片型号、改Flash分区、加外部存储器,都得重新核对。我个人的习惯是把映射表打印出来,用荧光笔标出当前工程用到的区域,贴在显示器边上。调试时遇到地址访问异常,一眼就能看出来是不是越界了。