导言

数字信号处理(DSP)是现代嵌入式系统的骨干,能够实时音频,视频,遥测,通信操作. 为DSP任务编写高效的C码直接撞击系统吞吐量,功耗,以及潜伏性. 与通用代码不同,DSP算法必须在严格的时间限制内执行,同时最大限度地使用有限的内存和处理资源. 本指南扩展了核心原理,为DSP应用提供了可操作的写作生产级C码的技术,从固定点算术到硬件特定优化.

理解C中的DSP基本原理

DSP 涉及数学操作,如过滤、转换、演化和对抽样信号的光谱分析。在C 中,程序员控制数据表达和流的方方面面,这对确定执行至关重要。DSP 代码经常运行在硬件紧密结合的微控制器或数字信号处理器上 — — 例如专用MAC(多积分)单元或SIMD矢量引擎。 深知目标架构的内存结构、指令集和外围能力对于写入高效的C 代码至关重要。

DSP 代码的关键特性 :

  • 重排算法:[] 以乘积操作为主的循环(如FIR滤波器).
  • 真实时间限制:[]每个样本必须在样本期内处理.
  • 数据流:[]连续输入/输出流需要高效的缓冲和最小的复制.
  • Memory带宽绑定: 许多DSP算法受到数据可移动速度的限制,而不是算术操作的限制.

关于基础参考,见 辅助设备的DSP基本原理.

固定点算术:不浮点高空精度

许多DSP处理器缺乏硬件浮点单元(FPU)或FPU较慢. 固定点算法使用带有暗射点的整数操作,提供定数性能和较低的功耗. 最常见的表示法是Q Notation:Q[m]n ],其中m 位是整数部分,n位分数部分. 例如,一个Q15格式(1个符号位,15个分位位)在16位DSP中是无比值的.

C部分固定点业务的实施

固定点加法是直截了当的(简单加整数),但乘法需要调整弧度点. 对于Q15乘法,两个Q15数字的产物需要32位中间结果,然后右移15位才能返回Q15. 示例:

typedef int16_t q15_t;
q15_t q15_mul(q15_t a, q15_t b) {
 int32_t temp = (int32_t)a * (int32_t)b;
 return (q15_t)(temp >> 15);
}

当积累发生(例如,在过滤器中),守护位防止溢出. 使用32位甚至64位的累积器和饱和结果. 固定点库如ARM CMSIS-DSP[]提供优化的固定点功能,包括过滤,变换,矩阵操作.

何时使用固定点对浮点

具有FPU(如Cortex-M4/M7)的现代处理器可以像固定点一样快速执行浮点操作. 使用浮点时:

  • 算法动态范围(如适应性滤波器)很高.
  • 代码的可维护性是一个优先事项(小缩放分析).
  • FPU硬件存在,管道可以重叠加成和乘法.

在没有FPU的高容量设备上,固定点仍然是成本敏感应用的标准.

优化 DSP 的内存访问

DSP 算法通常会按顺序处理大量数据。缓存失算和总线档会杀死性能。遵循这些原则:

  • 线上数据访问: 以毗连顺序(C中的row-major)进行转录数组,避免扭曲的访问模式,除非算法要求(例如FFT比特-逆变).
  • 数据对齐:确保数组与缓存行边界对齐. 使用编译器属性如或特殊内存部分.
  • 缓冲: 使用双缓冲来与CPU处理重叠 DMA 传输,虽然CPU在一个缓冲上工作,但下一个样块正在被加载.
  • 限制关键词:在指针上使用C99的通知编译器指针不别名,使矢量化和更好的指令调度成为可能.

例如,在输入和输出缓冲器分开时,应当将简单的FIR过滤功能写成“限制性”:

void fir_lowpass(const int16_t * restrict x, int16_t * restrict y,
 const int16_t * restrict coeffs, int len, int order) {
 for (int i = 0; i < len; i++) {
 int32_t acc = 0;
 for (int j = 0; j < order; j++) {
 acc += (int32_t)x[i + j] * coeffs[j];
 }
 y[i] = (int16_t)(acc >> 15);
 }
}

高效的算法选择和执行

算术复杂度直接翻译为执行时间和权力。 总是选择任务最有效的算法 :

  • Fast Fourier Transform (FFT): 使用 Cooley-Tukey radex-2或分光二长功率。避免天真DFT,即O(N2),预计算曲折因子并存储在ROM中。
  • FIR滤波器:对杀伤/插值使用多相分解;对线相滤波器利用对称性,将乘数减半.
  • IIR滤波器:使用直接形式II移植,以达到更好的数值稳定性;使用级联双曲段(二阶级),降低对系数定量的敏感度.
  • 演化: 对于长序,使用基于FFT的重叠加法或重叠保法,而不是直接演化.

参考FFTW库,以参考现代FFFT技术(虽然不在C中,但其原理被广泛复制到嵌入式DSP库中).

利用硬件特性: SIMD 和 DSP 指令

几乎所有现代微控制器都包括SIMD(单指令多数据)或DSP增强指令. 例如:

  • ARM Cortex-M4/M7:SIMD(SADD,SMUAD等),饱和算术,分数操作(QADD,QSUB). 使用CMSIS-DSP内在功能.
  • TI C6000 DSP:8个乘数单元,双MAC,和软件的管道衬线. The TI DSP优化指南[提供了详细的技术.
  • RISC-V带有P型扩展:未来核心将具有类似DSP的指令.

要使用 C 中的这些特性, 请写出编译器可以自动校验的代码( 例如, 无依赖性的简单循环) 或使用编译器内在函数 。 示例使用 ARM CMSIS- DSP 来进行 FIR 过滤 :

#include "arm_math.h"
arm_fir_instance_f32 S;
float32_t firState[128];
arm_fir_init_f32(&S, numTaps, coeffs, firState, blockSize);
arm_fir_f32(&S, input, output, blockSize);

此类库在组装中进行手调,以达到最大性能。在从通用 C 功能切换到库功能之前和之后,总是要配置 。

循环优化技术

由于DSP算法是循环重的,因此循环水平的优化可以产生大股利:

  • 循环解卷: 手动或与编译器prapas( ⁇ pragma unroll N')一起减少循环管理,增加指令级并行性.
  • 软件管道线条:[] 调整循环结构,使多个迭代同时飞行。有些编译器会自动这样做;使用`-O3'和架构专用旗帜。
  • 调制分支:]用算术(例如使用三进制的min/max)取代条件,或者用检索表来进行非线性函数.
  • 使用本地变量:通过在循环内声明变量或使用`登记 ' 提示,将经常访问的数据存储在登记册中。
  • 最小化除法:[]用恒数取代除法,用对等的乘法取代;用移法为二权.

预算常数和查询表

DSP 函数, 如三角值、 系数和双曲线系数, 都应该预先计算出并存储在 ROM 中, 作为恒定数组。 对于非实时启动, 您可以计算一次并重用。 例如: 对于一个 1024 点 FFT, 预计算每个阶段的正弦/ 二次曲线值。 这将取消运行时的评价并降低功率 。

查询表(LUT) 也帮助了DSP(如语音处理)中使用的方根,exprofile,和日志等函数. 使用表格条目之间的线性插值来交换内存与准确性.

剖析和图解

没有测量, 无法完成优化。 使用这些技术来识别瓶颈 :

  • 环-精确剖面:] 使用机载循环计数器(如Cortex-M上的DWT CYCCNT)来测量函数持续时间.
  • 统计剖面分析:[样本程序计数器(PC),以查看哪些函数消耗CPU时间.
  • 记忆剖析: 使用工具来监视缓存漏失(如果有的话)和总线交易.
  • 编译器反馈: 使编译器优化报告(GCC中的`-fopt-info-vec-opimized')能够查看循环是否向量化.

线性: 度量、 变化、 度量。 最大的收益往往来自改善内存访问模式,而不是调整算术。

实际摘要:将所有这一切结合在一起

C中写入高效的DSP代码需要整体的方法:

  • 选择正确的数据表示(固定点对浮点).
  • 设计数据结构,用于顺序访问和对齐.
  • 选择低复杂度(FFT,多相)的算法.
  • 可用时使用供应商 DSP 库 。
  • 解卷转转减支.
  • 预算ROM中的常数.
  • 配置文件时会毫不留情地让编译器帮助 。

通过应用这些原则,开发者可以实现信号处理的吞吐量与手调装可同时保留C的可移植性和可维护性。 结果就是可靠、实时的DSP系统,满足现代嵌入式产品的需求 — — 从助听器到5G基站。