拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F103嵌入式FFT实战:CMSIS-DSP配置与ADC-DMA同步

简介本资源是一份面向嵌入式开发初学者与STM32进阶实践者的FFT算法实操教程聚焦在STM32F103微控制器上高效实现数字信号处理任务。内容覆盖DSP库获取、工程集成、采样数据模拟含350Hz/8400Hz/18725Hz三频正弦合成、256点基4 FFT调用及幅值计算全流程并附关键代码片段与性能实测数据72MHz主频下256点FFT仅需约0.3ms特别适合音频分析、频谱监测等实时信号处理场景。资源为单文件PDF文档大小411KB结构清晰含库配置说明、数据格式规范高16位实部/低16位虚部、函数原型解析及结果解析方法。目前已有4262人学习下载可直接用于项目移植与算法验证省去官方库适配与格式调试的常见痛点。1. STM32F103 上跑通 FFT 不是“移植 MATLAB 代码”那么简单很多刚接触嵌入式信号处理的工程师拿到“STM32F103 进行 FFT 算法教程.pdf”时第一反应是找现成例程、复制arm_math.h函数、接个 ADC 就能出频谱——结果烧录后串口只打印乱码示波器看波形正常但 FFT 幅值跳变剧烈甚至arm_cfft_f32()返回 NaN。根本原因在于STM32F103 是 Cortex-M3 内核主频 72MHz、SRAM 仅 20KB而 FFT 的计算量、内存布局、数据对齐、定点/浮点选择、ADC 采样同步、结果缩放等环节任何一个没对齐硬件约束算法就从“理论可行”变成“实测失效”。本教程不讲通用 FFT 原理只聚焦 STM32F103 最小系统下可稳定运行、可调试、可复现的完整链路从 ADC 采样率与 FFT 点数的硬约束关系开始到 CMSIS-DSP 库的裁剪配置再到 DMA定时器触发的零抖动采集最后用 UART 输出符合 Python matplotlib 直接绘图的 CSV 格式频谱数据。适合已能点亮 LED、配置串口、使用 Keil 或 STM32CubeIDE 的中级开发者也包含 HAL 库与标准外设库混用时的寄存器级避坑细节。2. 为什么必须用 CMSIS-DSP 而不是手写 FFT选型依据与内存边界验证2.1 CMSIS-DSP 是唯一兼顾精度、速度与资源的可行路径STM32F103 的 72MHz 主频在浮点运算上实际等效于约 12–15 MIPS因 FPU 缺失所有 float 运算由软件模拟。若手动实现基-2 Cooley-Tukey FFTN1024 点需约 5×N×log₂N ≈ 51,200 次浮点乘加纯 C 实现耗时超 80ms远超实时分析需求。CMSIS-DSP 的arm_cfft_f32()经过 ARM 官方汇编优化同样 N1024 时实测耗时仅 9.2msKeil MDK -O3 Thumb-2且提供三种精度模式arm_cfft_sR_f32_len1024标准、arm_cfft_radix4_f32Radix-4更快但要求 N 为 4ᵏ、arm_cfft_f32通用接口。关键区别在于标准版使用位反转重排Radix-4 版无需重排但输入数组必须按特定顺序填充——这点常被教程忽略导致 FFT 结果相位全错。提示STM32F103 无硬件 FPU所有float运算走软浮点。若改用arm_cfft_q15()Q15 定点速度提升 3.2 倍实测 2.8ms但动态范围压缩至 ±1.0ADC 12bit 数据需做归一化缩放且频谱幅值需额外乘以 2¹⁵ 才还原真实幅度。新手建议从f32入手确认流程后再切q15。2.2 SRAM 分配必须精确到字节FFT 缓冲区与 DMA 内存对齐CMSIS-DSP 的 FFT 函数要求输入/输出缓冲区地址 32-bit 对齐即地址 % 4 0否则arm_cfft_f32()可能触发 HardFault。STM32F103 的 SRAM 起始地址为0x20000000共 20KB0x20000000–0x20004FFF。常见错误是直接声明float fft_in[1024]编译器可能将其分配在未对齐地址。正确做法是显式对齐// 在 .c 文件全局区声明非栈内 __attribute__((aligned(4))) float fft_in[1024]; // 强制 4 字节对齐 __attribute__((aligned(4))) float fft_out[1024]; __attribute__((aligned(4))) float fft_mag[512]; // 幅值谱N/21 点验证对齐是否生效编译后查看 map 文件搜索fft_in地址末两位应为00、04、08或0C。若地址为0x20000102则未对齐需加__align(4)或改用malloc但嵌入式慎用动态内存。2.3 关键参数表FFT 点数、采样率、频率分辨率的三角约束参数计算公式STM32F103 约束实例推荐FFT 点数 N必须为 2ᵏk≥4最大安全值 N1024占 8KB RAMN5124KB采样率 FsFs 定时器触发 ADC 频率ADC 最高 1MHz12bit 模式但需留出转换时间Fs 10kHz频率分辨率 ΔfΔf Fs / NΔf 越小分辨相邻频率能力越强Δf 10kHz/512 ≈ 19.5Hz最高分析频率 f_maxf_max Fs / 2奈奎斯特实际受限于 ADC 带宽与抗混叠滤波f_max 5kHz注意若 Fs10kHz 且 N512则单次 FFT 耗时 N/Fs 51.2ms意味着每秒最多执行 19.5 帧频谱。若需更高刷新率必须降低 N如 N256 → 25.6ms/帧或提高 Fs但 Fs10kHz 时 ADC 精度下降需实测 SNR。3. 用 CubeMX 配置 DMA定时器触发 ADC实现零抖动连续采样3.1 CubeMX 中的关键配置项HAL 库RCC 设置HSE8MHzPLL 9×→72MHzAHB72MHzAPB272MHzADC 在 APB2ADC1ModeIndependentResolution12BitsData AlignmentRightScan Conversion ModeEnabled多通道时启用Continuous Conversion ModeDisabled用定时器触发非连续External TriggerTIM2 TRGO必须选此项Trigger EdgeRising EdgeTIM2作为 ADC 触发源Clock SourceInternal ClockPrescaler71 → 1MHz 计数频率72MHz/72Counter Period99 → 10kHz 触发频率1MHz/100Master ModeTrigger Output (TRGO) → Update Event确保每次计数溢出触发 ADCDMA1 Channel1ADC1RequestADC1DirectionPeripheral to MemoryCircular ModeEnabled关键避免 DMA 传输完成中断频繁打断Data WidthHalf Word16-bit因 ADC 12bit 右对齐实际取低 12bitMemory IncrementEnabledPriorityHigh防止被其他 DMA 抢占3.2 初始化后必须修改的 HAL 层寄存器绕过 HAL 库 BugHAL 库默认将 ADC 配置为软件触发即使 CubeMX 设为外部触发。需在MX_ADC1_Init()后手动使能外部触发// 在 HAL_ADC_Start_DMA() 调用前插入 ADC-CR2 | ADC_CR2_EXTEN_0; // 外部触发使能上升沿 ADC-CR2 ~ADC_CR2_EXTSEL; // 清除触发源选择位 ADC-CR2 | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_0; // 选择 TIM2 TRGOEXTSEL011b3.3 DMA 传输完成回调中启动 FFT 计算CubeMX 生成的HAL_ADC_ConvCpltCallback()在 Circular Mode 下每 N 个采样触发一次。此处不能直接调用arm_cfft_f32()需先将半字16bitADC 数据转为 float 并归一化void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { static uint16_t adc_raw[512]; static float fft_in[512]; // 1. 从 DMA 缓冲区拷贝假设 dma_buffer 已定义 memcpy(adc_raw, dma_buffer, sizeof(adc_raw)); // 2. 归一化ADC 12bit 满量程 0–4095 → float [-1.0, 1.0] for(uint16_t i 0; i 512; i) { fft_in[i] ((float)adc_raw[i] - 2048.0f) / 2048.0f; // 中心化并缩放 } // 3. 执行 FFT注意cmsis_dsp_init() 已在 main() 中调用 arm_cfft_f32(arm_cfft_sR_f32_len512, fft_in, 0, 1); // 正向变换0不逆序 // 4. 计算幅值谱前 N/21 点 arm_cmplx_mag_f32(fft_in, fft_mag, 256); // 输出 256 点幅值含 DC 和 Nyquist }逻辑说明arm_cfft_f32()输入fft_in是复数数组实部在偶数索引虚部在奇数索引但arm_cfft_sR_f32_len512接受实数输入内部自动补零虚部arm_cmplx_mag_f32()将复数数组转为实数幅值长度为 N/21512 点 FFT 输出 256 点幅值谱对应 0Hz 到 Fs/2。4. UART 输出可直绘频谱数据CSV 格式与波特率容错设计4.1 避免 UART 阻塞双缓冲 优先级调度FFT 计算耗时约 4.1msN512而 UART 以 115200bps 发送 256 个 float每个 12 字符逗号≈3072 字节需 266ms远超 FFT 周期。若在回调中直接printf会导致 DMA 采样被延迟频谱失真。解决方案用双缓冲队列 低优先级任务发送。// 定义环形缓冲区大小需 ≥ 单次频谱字节数 × 2 #define SPECTRUM_BUF_SIZE 8192 static uint8_t uart_tx_buf[SPECTRUM_BUF_SIZE]; static uint16_t tx_head 0, tx_tail 0; // FFT 回调中写入缓冲区原子操作 void append_spectrum_to_uart(float* mag, uint16_t len) { char line[32]; for(uint16_t i 0; i len; i) { int written snprintf(line, sizeof(line), %.4f,, mag[i]); if(tx_head written SPECTRUM_BUF_SIZE) { memcpy(uart_tx_buf[tx_head], line, written); tx_head written; } } // 补换行符 uart_tx_buf[tx_head] \n; } // 在 main loop 中非阻塞发送或用 HAL_UART_Transmit_IT if(tx_head ! tx_tail) { uint16_t send_len MIN(tx_head - tx_tail, 64); // 每次发 64 字节 HAL_UART_Transmit(huart1, uart_tx_buf[tx_tail], send_len, 10); tx_tail send_len; }4.2 Python 端直绘脚本验证数据有效性接收端用 Python 读取 UART CSV实时绘图import serial import numpy as np import matplotlib.pyplot as plt ser serial.Serial(COM7, 115200, timeout1) Fs 10000 # 与 STM32 配置一致 N 512 freqs np.linspace(0, Fs/2, N//21) # 256 点频率轴 plt.ion() fig, ax plt.subplots() line, ax.plot(freqs, np.zeros(N//21)) ax.set_xlim(0, Fs/2) ax.set_ylim(0, 1.0) while True: try: line_data ser.readline().decode(ascii).strip() if not line_data: continue mag_vals [float(x) for x in line_data.split(,) if x] if len(mag_vals) 256: line.set_ydata(mag_vals) fig.canvas.draw() fig.canvas.flush_events() except (ValueError, UnicodeDecodeError): continue参数说明snprintf(%.4f,)控制浮点精度避免 UART 传输过载MIN(tx_head - tx_tail, 64)限制单次发送长度防止 HAL_UART_Transmit 超时Python 脚本中len(mag_vals) 256是关键校验若失败说明 STM32 端数据截断或格式错误。5. 排查 FFT 结果异常的 4 个硬核检查点5.1 检查 ADC 输入信号是否满足奈奎斯特采样定理用示波器探头直接测量 ADC 输入引脚观察信号最高频率分量。若信号含 8kHz 成分而 Fs10kHz则发生混叠FFT 中会出现虚假的 2kHz 峰值10kHz−8kHz。解决方法在 ADC 前加无源 RC 低通滤波器fcFs/2.5≈4kHz电阻 1kΩ 电容 10nF。5.2 验证 FFT 缓冲区是否被意外覆盖在fft_in数组前后各声明 4 字节保护区并在 FFT 前后检查其值uint32_t guard_before 0xDEADBEEF; __attribute__((aligned(4))) float fft_in[512]; uint32_t guard_after 0xDEADBEEF; // FFT 前后插入检查 if(guard_before ! 0xDEADBEEF || guard_after ! 0xDEADBEEF) { // 触发 LED 报警或 UART 打印 BUFFER CORRUPT }5.3 测量实际 FFT 执行时间排除定时器干扰用 GPIO 引脚配合示波器测量HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 开始前拉高 arm_cfft_f32(arm_cfft_sR_f32_len512, fft_in, 0, 1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 结束后拉低实测高电平宽度应为 4.1±0.3msN512。若超过 5ms检查是否启用了__weak函数重定向或调试器断点影响。5.4 幅值谱归一化系数验证表CMSIS-DSP 的arm_cfft_f32()输出幅值需乘以下系数才对应真实物理幅度FFT 类型归一化系数说明arm_cfft_f32通用1.0 / N需手动除以 Narm_cfft_sR_f32_len5121.0 / √N默认已做缩放无需再除arm_cfft_q151.0 / (N × 2¹⁵)Q15 需额外缩放实测方法输入纯正弦波如 1kHz, 1VppFFT 后找到 1kHz 对应 bin 的幅值应接近 0.5因sin(x)幅值 0.5 对应峰值 1.0。若测得 256.0则说明漏了/√N√512≈22.6256/22.6≈11.3仍不对——此时检查是否用了arm_cfft_f32但未除 N。注意arm_cmplx_mag_f32()输出的是复数模长即sqrt(real² imag²)已为正数。若发现 DC bin索引 0幅值异常高检查 ADC 是否有直流偏移((float)adc_raw[i] - 2048.0f)中的 2048 是否准确——实测可用0.95 * 2048动态校准。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门