首页 / 资讯中心 / 文章详情

DSP寻址方式与汇编指令实战:循环寻址和MAC实现高性能FIR滤波器

DSP寻址方式与汇编指令实战:循环寻址和MAC实现高性能FIR滤波器 ★ FEATURED ARTICLE
简介这是一份面向 DSP数字信号处理开发与学习者的汇编基础文档围绕 TMS320C2000 系列芯片系统梳理直接寻址、寄存器间接寻址、寄存器相对寻址、基址相对寻址、堆栈寻址等常见寻址方式并按算术、逻辑、移位、跳转等类别讲解汇编指令助记符及其操作数约定。内容还涉及 CPU 工作模式C28x/C27x/C2xLP与兼容模式配置、条件判断标志位、乘积移位方式等细节适合正在入门 DSP 汇编编程或需要复习指令系统的读者作为知识点笔记参考。资料为单个 docx 文件压缩包大小约 96KB便于下载后直接按目录浏览学习。截至目前已有 439 人学习内容结构清晰重点公式与表格式说明集中可帮助读者更快掌握 DSP 寻址与汇编指令的核心要点。1. DSP的寻址方式和汇编指令寄存器级别的东西为什么让写C的算法工程师也翻车前阵子帮某开发者调一块音频板的噪声问题。C代码在通用MCU上跑得好好的换到DSP上就爆音查到最后罪魁祸首是环形缓冲区的指针回绕——DSP的循环寻址要求起始地址按长度对齐而C编译器并没有帮你处理这个细节。这事让我重新意识到DSP的寻址方式和汇编指令不是“会C就能无视”的黑匣子它直接决定算法能不能跑满真实时钟频率。这篇笔记就沿着这个标题拆开讲先认识寻址方式再分类读指令然后用一个能跑的FIR示例把两者串起来最后给几条高频踩坑记录。适合正要上手DSP的嵌入式工程师以及做音频、电力电子算法的同学。2. 寻址方式先立住地址从哪来决定了一条指令能干什么DSP汇编里每条指令要操作数据第一步就是回答“数据在哪”。这个回答过程就是寻址。和通用MCU相比DSP的寻址方式跟单周期访存、双操作数读、环形回绕这些特性绑得很紧所以不能只看成语法。寻址方式选对了代码短一截选错了后面所有优化都白费。2.1 立即寻址与直接寻址常数和全局变量两种最常见的取数方式立即寻址最简单操作数直接写在指令里。常见写法是LDI #256, BG ; 把常数256加载到背景寄存器BG这条指令不访问内存指令字里直接携带常数适合初始化系数表长度、缓冲区大小这类固定值。参数说明常数范围受指令字宽限制一般是16位超过范围时要么拆成低16位和高16位分两次加载要么先放进数据区再用直接寻址读。如果你在代码里看到LDI #0xFFFF, BG这种写法说明它已经触到指令字能表达的上限了。直接寻址则是另一种思路指令只写地址的低位高位由数据页寄存器DP补全。比如LDI 0x1000, AR0 ; DP0x0080时实际访问地址为0x8100这样指令字短访问全局变量很省空间。实现细节链接器会把不同模块的数据分到不同数据页页基址在程序进入模块时装载到DP。手写汇编最容易在这踩坑后面第5章会专门说。读者在移植代码时如果发现某个全局变量读出来是0xFF或者随机值第一反应就应该是查看当前DP值。2.2 间接寻址指针本身就是操作数后增量修饰符是数组遍历的标配间接寻址把地址放在辅助寄存器ARn里指令通过ARn去访问内存。最常用的修饰符是后增量、后减量、变址偏移例如MAC *AR2, *AR3, A ; A (*AR2) * (*AR3)然后AR2、AR3都自增这里的“”不是加法指令而是寻址修饰符告诉硬件在本次访存结束后把ARn加1。为什么爱用后增量因为数组遍历时指针本来就要移动把它塞进访存指令里就省掉了单独的地址更新指令。注意ARn的自增量可以配置成1、-1或者某个变址寄存器的值具体看CPU的寻址寄存器组。我在写循环时习惯先把修饰符写清楚再填指令避免回头被ARn的跳动方向搞晕。2.3 循环寻址是DSP的命根子环形缓冲区靠硬件回绕而不是比较跳转通用MCU写环形队列无非是“指针到达末尾就重置”要么比较要么求余都得额外花指令。DSP直接把回绕做成硬件功能你只需要配三个寄存器起始地址、长度、当前指针之后每次间接访存指针越过末端时自动跳回首部。先用C理解回绕逻辑// 模拟DSP循环寻址的回绕size要求是2的幂 unsigned int wrap_pointer(unsigned int ptr, unsigned int base, unsigned int size) { return base ((ptr - base) (size - 1)); }逻辑说明ptr走到basesize时(ptr-base)size与(size-1)求与后变成0于是回到base。这个操作在DSP里是用单周期位与完成的不产生分支所以延迟线遍历不会引起流水线停顿。对应到汇编初始化是这样一组指令LDI #DELAY_BASE, BG ; BG循环起始地址 LDI #DELAY_LEN, BK ; BK循环长度必须是2的幂 LDI PTR, AR0 ; AR0当前读指针之后在循环里对AR0使用循环寻址修饰符AR0越界后自动回绕。参数设置要记住两条硬约束长度必须为2的幂起始地址必须按长度对齐。比如长度取5120x200起始地址就必须是0x200的整数倍。这个约束很多第一次接触的人会忽略结果回绕到错误地址滤波输出周期性出现尖峰。我一般会在初始化代码里写断言// 调试期断言上线前可以去掉 if ((base (len - 1)) ! 0 || (len (len - 1)) ! 0) { // 打印错误并停在这里 }提示在部分DSP内核里循环寻址的起始地址寄存器和长度寄存器必须在初始化时一起写入单独改其中一个可能导致硬件回绕行为异常。下面是一张寻址方式对比表方便你对照选型寻址方式操作数来源典型用途最容易踩的坑立即寻址指令内的常数初始化系数、长度常数范围超16位直接寻址指令低位DP高位全局变量忘记切换DP间接寻址ARn寄存器存地址数组、指针后增量方向配错变址寻址基址ARn偏移查表、矩阵偏移不检查边界循环寻址基址长度指针延迟线、FFT蝶形长度非2的幂3. 把DSP汇编指令按功能分成四类算术、逻辑、数据移动、程序控制各有各的脾气DSP的指令集看起来总是很长但真正高频用到的就那十几条。我习惯按功能分成四类来读手册算术、逻辑、数据移动、程序控制。这样遇到新内核时先找每一类的代表指令读起来比从头翻快得多。3.1 算术指令MAC、MPY是算法的心脏一条指令顶C里三行MAC乘累加是DSP最核心的算术指令。C语言里“y x[i] * h[i]”在DSP上直接对应MAC *AR2, *AR3, A ; 单周期A (*AR2) * (*AR3)并自增两个指针为什么能单周期因为内核里乘法器和累加器的旁路路径是专用硬件两个操作数可以同时从内存读到。这在FIR、IIR、相关运算里是绝对主力一条指令替掉了C语言的乘法、加法和两次数组索引。MAC的参数有三个需要留意。第一有符号还是无符号通常在指令前缀或者控制寄存器里指定默认按有符号做第二结果是否饱和由饱和模式位决定实时音频一般开饱和避免溢出产生爆音第三累加器宽度通常比操作数宽不少比如16位乘16位、累加器40位目的就是让中间结果不溢出。其余算术指令里MPY只管乘ADD/SUB只管加SHIFT管移位。写循环里的小计算时能用MPYADD组合的往往不如直接用MAC因为后者少一次指令取指。3.2 数据移动指令双操作数并行移动让延迟线更新不额外花周期DSP里搬数据不是简单memcpy。典型场景是音频延迟线新样本写入后整条延迟线要顺移一位。C语言写是循环赋值编译成指令少说要N条。DSP的做法是用双操作数移动指令PMOVE *AR2, *AR3 ; 从AR2地址取一个操作数写入AR3地址两个指针同时自增注意有些内核还提供并行双字加载一条指令搬两个32位操作数到寄存器对LDDW *AR4, A ; 从AR4指向的地址加载64位数据到累加器对逻辑说明这类指令的价值在于“移动数据的同时移动指针”而且访存是并行的不额外占用算术单元。延迟线顺移如果整个用PMOVE做通常能把更新开销从N次循环压到N/2次甚至更少。实际工程中我很少真的用指令一条条搬延迟线而是让延迟线和循环寻址配合指针在环形缓冲里走新样本覆盖最旧样本压根不用搬数据。数据移动指令更多用在帧拷贝、系数重载这类场景。这一点在第4章的例子里会看得更清楚。搬数据时要小心存储体冲突两个操作数尽量安排在不同物理存储体否则单周期会变成双周期。3.3 程序控制指令延迟槽和条件执行分支指令没那么好懂程序控制指令是新手最容易看懵的部分因为DSP普遍带延迟槽。所谓延迟槽就是分支指令后面跟的若干条指令无论分支是否跳转它们都会在跳转发生前先执行完。常见写法BR LOOP NOP ; 延迟槽无论是否跳转这条都会执行逻辑说明硬件为了不浪费已取到流水线里的指令把分支判断延后让紧跟在后面的指令照常执行。如果你在延迟槽里放一条本应“只在跳转后执行”的指令程序逻辑就错了。条件执行也是DSP的特色。普通MCU用“比较跳转”DSP很多指令可以直接挂条件IF LT, MPY *AR2, *AR3, A ; 若上次结果为负则执行这条乘累加这样避免了短跳转的流水线开销。程序控制指令里DO/REPEAT类硬件循环指令也值得注意它会把循环计数放进专用寄存器循环体执行时不产生跳转惩罚。我在FIR例子里就是用硬件循环把循环体压到几条指令的。4. 把寻址和指令串起来一个可用循环寻址MAC实现的FIR滤波器最小示例这一章是全文的“可抄作业”部分。以64抽头FIR为例输入数据来自AD转换输出送去DA转换处理器是典型的累加器型DSP内核。这个例子的核心思想是延迟线用循环寻址管理系数表用后增量指针遍历MAC把乘加缩到单周期。4.1 最小可运行汇编初始化、循环体、收尾三段先给出完整代码分三段写便于照抄。初始化段; 64抽头FIR延迟线长度L1282的幂 LDI #DELAY_BASE, BG ; BG环形缓冲起始地址 LDI #128, BK ; BK环形缓冲长度 LDI #COEF_BASE, AR1 ; AR1系数表指针 LDI #63, R2 ; R2循环计数64-1 ZERO A ; 累加器清零逻辑说明延迟线放在DELAY_BASE开始、长度128的区间。为什么长度取128而不是正好64因为循环寻址长度必须是2的幂64也是但给读指针留出绕行余量后新样本才有地方写。实际操作中每次采样中断会先覆盖最旧样本再启动乘加循环。采样中断处理段; 假设AR0在初始化例程里已指向延迟线当前最旧样本位置 LDI NEW_SAMPLE, R3 STORE R3, *AR0 ; 新样本写入当前指针位置覆盖最旧样本 ZERO A ; 累加器清零 LDI #COEF_BASE, AR1 ; 系数表指针复位到h[0] LDI #63, R2 ; 循环计数 FILT_LOOP: MAC *AR0-, *AR1, A ; A 当前x[n-k] * h[k]AR0后减量、AR1后增量 BNZ FILT_LOOP, R2 ; R2自减非零跳回 ; 循环结束后AR0停在最旧样本的前一个位置下次写入正好落在那里收尾段STORE A, Y_OUT ; 把累加结果写到输出寄存器或DAC的数据寄存器逻辑说明AR0从最新样本开始依次取x[n]、x[n-1]、……、x[n-63]与h[0]、h[1]、……、h[63]相乘累加。64次MAC之后AR0通过循环寻址回绕回到与起始位置相差64的位置。下一次中断新样本又要覆盖最旧单元而指针恰好停在那里。这就是延迟线不需要搬移的原因。参数说明循环寻址的长度是128但MAC只转64次这没问题——环形缓冲比实际抽头数多留出的空间只是为了让指针回绕时不越界。BK寄存器配的是缓冲长度R2配的是抽头数两者不要混用。4.2 与C代码逐行对照看每条指令的价值C代码对应汇编周期delay[ptr]x;STORE R3, *AR01y0; for(k0;k64;k) yh[k]*delay[(ptr-k)127];ZERO A; LDI #COEF_BASE, AR1; LDI #63, R2; LOOP: MAC *AR0-, *AR1, A; BNZ264*2左右outy;STORE A, Y_OUT1周期估算逻辑如果内核MAC单周期、BNZ带延迟槽整个滤波器大约128周期加少量初始化开销。这个数字通常比C编译器生成的版本快一倍以上。为什么编译器没那么聪明因为编译器不总知道延迟线地址符合循环寻址对齐约束也不敢随意把数组访问改成环形指针。注意以上汇编是累加器型DSP的通用助记符写法。移植到具体内核时指令名可能写作MAC、LDI、BNZ的近义词但寻址修饰符和延迟槽概念是共通的。4.3 汇编循环里AR0和AR1的移动方向为什么要相反再强调一点AR0后减量AR1后增量这是故意安排的。延迟线按时间顺序排列时最新样本在当前位置取历史样本要往前减地址系数表从h[0]开始要往后加地址。如果两个指针移动方向一致FIR输出的相位就反了听感上是反的卷积。调试时遇到输出波形明显不对先查这两个指针方向不要急着改系数。5. 避坑寻址方式和汇编指令的高频翻车现场每条都是真实板子上的教训5.1 循环寻址长度没按2的幂设置滤波输出周期性尖峰现象FIR抽头数300输出波形每隔300个采样点出现一次明显尖峰其余时间正常。原因循环寻址的回绕用位与实现长度必须是2的幂。300不是2的幂硬件回绕到错误地址取到的延迟线数据错位。C仿真时数组长度300没问题因为C里是数组下标加取余取余不受2的幂约束。解决把环形缓冲长度补到512抽头数仍按300算循环次数用300。多出来的空间闲置不用。注意BK寄存器配成512MAC循环就要转300次而不是512次。改完再对比输出波形尖峰应该消失。5.2 直接寻址的数据页DP没切换跨模块访问全局变量读到0xFF现象程序从模块A跳转到模块B后B里访问全局变量config_flag读回来总是0xFF。原因直接寻址的地址高位由DP提供模块A的DP指向A的数据段B的数据段在另一个页进入B时没有重装DP。解决在手写汇编的函数入口加一条LDI B_DATAPAGE, DP。编译器编译C时通常自动生成手写汇编时容易漏。如果你在混合编程检查汇编函数入口是否有DP装载指令没有就补上。5.3 MAC的两个操作数落在同一个存储体单周期变成双周期现象优化后MAC指令周期数没有达到手册标称值循环整体时间比预期多了一倍。原因两个并行操作数都从同一物理存储体读取访存宽度不够硬件自动拆成两个周期。DSP手册里通常写着“同一bank双读冲突”的限制但没人会逐字去读。解决把系数表放到和延迟线不同的存储体或者把系数预先加载到寄存器组改用“寄存器×内存”的MAC变体。判断方法很简单单步执行MAC看CPU周期计数器的增量是1还是2。5.4 延迟槽理解错条件分支前后指令执行顺序乱套现象条件跳转后的变量赋值时灵时不灵单步调试正常全速运行错乱。原因全速运行时流水线生效分支指令后面延迟槽的指令总会执行单步时延迟槽效果不明显掩盖了问题。解决给分支指令后面人为排NOP先把逻辑跑对再考虑填有用指令。如果要把某条指令放进延迟槽必须确认它“无论跳与不跳都该执行”。例如循环计数自减可以放进延迟槽但“只在跳转后才更新指针”的指令不能放。5.5 饱和标志位未复位一次削波让后面几十帧全部爆音现象某次输入信号过大触发饱和从那以后输出一直钳在满幅即使输入已经恢复正常。原因累加器或乘法器的饱和状态锁存在状态寄存器下一帧开始累加时饱和标志仍然有效负数继续被钳制符号扩展也跟着出错。解决在每帧处理开头执行清除饱和标志的指令或者把状态寄存器中的饱和模式位重新写一遍。这个清除动作本身会花几个周期放在帧头没问题别放进循环体里面。音频处理尤其要注意这一点实时流一旦饱和后续每一帧都可能被污染。6. 进阶把FIR循环体压到3条指令顺便验证优化有没有生效如果前面的最小示例已经跑通下一个问题就是怎么榨干它以64抽头FIR为例基线版本循环体是“MAC BNZ”两条指令每轮处理1个抽头平均每个抽头2个周期上下。但在有足够指令级并行度的内核上可以一次展开两轮MAC让循环体变成3条指令LDI #32, R2 ; 循环32次每次处理2个抽头 LOOP2: MAC *AR0-, *AR1, A ; 处理第k个抽头 MAC *AR0-, *AR1, A ; 处理第k1个抽头 BNZ LOOP2, R2 ; R2自减非零跳回为什么是3条而不是4条因为展开后循环计数减半BNZ仍然只有一条。两条MAC即使没有超标量发射也能靠流水线重叠减少停顿。这个技巧的前提有两个一是AR0的循环寻址在连续两次后减量时回绕逻辑必须正确这需要在初始化时确认BG/BK寄存器二是系数表必须连续排列AR1的后增量不会跨页。验证优化是否真的生效我一般用三个办法。第一看周期数在循环体外用定时器或周期计数器包一圈记录滤波器函数耗时优化前后对比64抽头从大约128周期掉到70周期以内说明展开有效。第二查内存窗口在仿真器里单步跑完整个循环检查AR0地址序列重点看它是否按“最新样本→最旧样本→回绕到最新”的顺序走完64个点中间没有重复也没有跳变。第三对拍数据用一个已知的冲激信号喂给C版本和汇编版本比较输出序列理想情况下两个序列完全一致差几个LSB通常是饱和模式或舍入模式不同不是寻址问题。这套改法值得投入的时间大概半天换来的是循环周期减半以及对你手头这颗DSP内核流水线认识的加深。我第一次做这类优化时直接把BK设成抽头数64结果AR0在回绕时跳到了错误位置输出全是噪声。排查下来才发现循环寻址长度必须按2的幂对齐而我的延迟线缓存恰恰没有对齐。后来我养成了一个习惯任何寻址方式改动之后先看仿真器里的内存和寄存器窗口再上板。这个习惯帮我省掉的调板次数远比多花的那几分钟仿真时间值钱。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站