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外设接口架构图 DSP Core C66x / C55x / C28x 内部总线桥 高速外设 EMIF (DDR3/LPDDR2) SRIO (5 Gbps/lane) PCIe (Gen2/Gen3) 中速外设 McBSP (音频I2S) UART (115200~3Mbps) SPI / I2C 低速/控制外设 GPIO (中断触发) ePWM / eCAP / eQEP 看门狗定时器 EDMA3 控制器 64通道,3级传输 中断控制器 (INTC) 可屏蔽/不可屏蔽中断 注意:外设时钟需单独使能 PSC模块配置不可遗漏

这张图展示了一个典型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,缓存一致性要手动维护。