2. DSP基础回顾
拿到一块新的DSP芯片,别急着写代码。我个人的习惯是,先花半天时间把芯片手册翻烂。你想想看,连目标平台长什么样都不清楚,移植工作就是闭着眼睛过河。这一章,咱们就聊聊怎么把目标平台看透。
2.1 CPU vs DSP:核心架构差异
很多人觉得CPU和DSP都是处理器,能跑代码就行。其实差别大了去了。CPU是“通才”,什么都能干,但干得不一定快。DSP是“专才”,专门为数字信号处理优化过。
我举个例子。你在CPU上做个FIR滤波器,可能需要几十个指令周期。在DSP上,可能一个周期就搞定了。为什么?因为DSP的硬件乘法器是专门为乘加运算设计的。
| 对比维度 | 通用CPU | DSP |
|---|---|---|
| 指令集设计 | 复杂指令集(CISC)或精简指令集(RISC),强调通用性 | 专用指令集,单周期乘加(MAC)、循环寻址、位反转 |
| 数据通路 | 通用寄存器组,多级流水线 | 多组独立数据总线(哈佛架构),可并行取指、取数、写回 |
| 乘法器 | 通常1-2个,延迟较高 | 多个硬件乘法器,单周期完成 |
| 寻址模式 | 基址+偏移、变址等 | 循环寻址、位反转寻址(FFT专用) |
| 中断响应 | 复杂,需保存大量上下文 | 快速中断,硬件自动保存关键寄存器 |
| 典型应用 | 操作系统、数据库、办公软件 | 音频编解码、雷达信号处理、电机控制 |
核心结论:DSP的强项在于“数据流密集、计算模式固定”的任务。如果你的算法里全是乘加、卷积、FFT,DSP是首选。如果算法逻辑复杂、分支多、需要跑操作系统,CPU更合适。
我记得有一次,团队里有人非要在DSP上跑一个复杂的决策树算法。结果呢?代码写出来,效率还不如同频率的ARM Cortex-M。说白了,工具要用对地方。
2.2 外设接口分析
外设接口是DSP和外界打交道的通道。移植的时候,接口驱动往往是第一个要重写的模块。为什么?因为不同芯片的外设寄存器布局、中断机制、DMA通道都不一样。
我一般会先画一张外设接口图,把每个接口的用途、带宽、中断号、DMA通道号标清楚。这样后面写驱动的时候,心里有数。
这张图展示了一个典型DSP的外设层次。高速外设(如DDR控制器、SRIO)直接挂在高速总线上,中速外设(如McBSP、UART)通过桥接访问,低速控制外设(如GPIO、ePWM)则挂在低速外设总线上。
我的经验:移植时,先搞定UART和GPIO。UART用来打印调试信息,GPIO可以用来点灯、测时序。这两个接口通了,后面的工作才有抓手。
2.3 内存与存储架构评估
内存架构是DSP移植中最容易踩坑的地方。CPU的内存模型相对简单,通常就是一级缓存、二级缓存、主存。DSP的内存架构要复杂得多,而且不同系列差异很大。
我见过有人把一个大数组定义在DDR里,结果每次访问都要等几十个周期,性能直接崩了。后来改成放到片内L2 SRAM里,速度提升了5倍。
2.3.1 典型DSP内存层次
| 存储层级 | 典型容量 | 访问延迟 | 用途 |
|---|---|---|---|
| L1P (程序缓存) | 32KB | 1 cycle | 存放最常执行的代码段 |
| L1D (数据缓存) | 32KB | 1 cycle | 存放最常访问的数据 |
| L2 SRAM | 256KB~1MB | 8~15 cycles | 关键数据缓冲区、堆栈 |
| DDR3/DDR4 | 128MB~2GB | 50~100 cycles | 大块数据、非实时任务 |
| ROM/Flash | 1MB~16MB | 极慢(需先加载) | 固件存储、启动代码 |
避坑指南:我曾经在一个项目中,把音频缓冲区定义在DDR里,结果播放时总有“咔咔”的爆音。查了两天才发现,是DDR的刷新周期导致访问延迟抖动。后来把缓冲区挪到L2 SRAM里,问题立刻解决。记住:实时性要求高的数据,一定要放在片内SRAM。
2.3.2 内存映射与地址空间
每个DSP芯片都有固定的内存映射。移植时,必须搞清楚每个段的起始地址和大小。我一般会在工程里建一个memory.ld或.cmd文件,把内存布局写清楚。
/* TMS320C6678 内存映射示例 (链接器命令文件片段) */
MEMORY
{
L2SRAM: o = 0x00800000, l = 0x00100000 /* 1MB L2 SRAM */
DDR3: o = 0x80000000, l = 0x20000000 /* 512MB DDR3 */
MSMCSRAM: o = 0x0C000000, l = 0x00400000 /* 4MB 多核共享内存 */
SPI_FLASH: o = 0x30000000, l = 0x01000000 /* 16MB SPI Flash */
}
SECTIONS
{
.text: > L2SRAM /* 代码段放L2 */
.data: > L2SRAM /* 数据段放L2 */
.bss: > L2SRAM /* BSS段放L2 */
.far: > DDR3 /* 大数组放DDR */
.cinit: > SPI_FLASH /* 初始化表放Flash */
}
这段代码看起来简单,但实际项目中,内存分配往往要反复调整。比如,代码段太大放不进L2怎么办?那就得把不常用的函数放到DDR里,只把热点函数留在L2。
2.3.3 缓存一致性问题
DSP的缓存和CPU的缓存不太一样。很多DSP的L1缓存是“可配置”的——你可以把它全部配成SRAM,也可以部分配成缓存。这给了你很大的灵活性,但也带来了麻烦。
我记得有一次,DMA直接把数据写到了DDR里,但CPU读的时候却从L1D缓存里取,结果读到的全是旧数据。这就是缓存一致性问题。
解决方案:
- 方法一:把L1D全部配成SRAM,不用缓存。简单粗暴,但性能会下降。
- 方法二:在DMA传输前后,手动做缓存无效化(Cache Invalidate)和回写(Cache Writeback)。
- 方法三:使用DSP提供的“缓存一致性”硬件机制(如TI的C66x CorePac中的XMC模块)。
/* 缓存维护示例 (C66x DSP) */
void dma_transfer_complete(void)
{
/* 在读取DMA写入的数据前,先无效化L1D缓存 */
CACHE_invL1d(buffer_addr, buffer_size, CACHE_WAIT);
/* 现在可以安全地读取buffer中的数据了 */
process_data(buffer);
}
void cpu_write_to_dma_buffer(void)
{
/* CPU写入数据后,回写L1D缓存到内存 */
CACHE_wbL1d(buffer_addr, buffer_size, CACHE_WAIT);
/* 然后触发DMA传输 */
DMA_start_transfer(buffer_addr, dst_addr, buffer_size);
}
小技巧:调试缓存问题时,可以先把L1D全部配成SRAM,等逻辑调通了,再逐步打开缓存优化性能。这样能避免缓存问题干扰算法调试。
2.4 本章小结
目标平台分析是移植工作的地基。地基没打好,后面全是坑。我个人的经验是,花30%的时间做平台分析,能省下70%的调试时间。
记住三个要点:
- 架构对比:搞清楚DSP和CPU的差异,别用CPU的思维写DSP代码。
- 外设接口:先通UART和GPIO,再搞其他外设。
- 内存架构:实时数据放片内SRAM,大块数据放DDR,缓存一致性要手动维护。