ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 ADC+DMA实时数据采集配置与FFT预处理实战

STM32 ADC+DMA实时数据采集配置与FFT预处理实战 1. 项目缘起从“能采样”到“会分析”的跨越做嵌入式开发的朋友尤其是玩STM32的对ADC采样肯定不陌生。配置个定时器触发开个DMA循环搬运把模拟信号变成一长串数字这套流程大家闭着眼睛都能写出来。但很多时候我们采回来的数据就像一堆未经雕琢的矿石堆在那里价值有限。我们真正需要的往往是矿石里蕴含的“黄金”——信号的频率成分、幅值变化、噪声水平。这时候FFT快速傅里叶变换就登场了。我最近在做一个电机振动监测的小项目核心需求就是实时分析电机运行时振动传感器的信号频谱判断是否存在异常频率。这要求MCU不仅要高速、连续地采集ADC数据还要能实时地对这些数据进行FFT运算。如果让CPU在ADC中断里一个个搬数据再吭哧吭哧算FFT那系统基本就干不了别的了实时性也无从谈起。所以一个高效的“ADCDMAFFT”处理流水线就成了刚需。STM32 CubeMX这个工具大大简化了外设的初始化配置但如何把ADC、DMA和后续的FFT处理无缝衔接起来形成一条自动化的数据流这里面有不少细节值得琢磨。比如DMA该用哪种模式采来的数据怎么存才能方便FFT库处理FFT运算的结果又该如何解读和应用这个系列我就打算结合自己的踩坑经历把这条流水线从硬件配置到软件实现的完整链路拆解清楚。今天是第一部分我们先搞定最基础也最重要的一环用CubeMX配置ADC和DMA搭建一个稳定、高效的实时数据采集框架。2. CubeMX工程创建与时钟树配置万事开头难但用CubeMX开头就简单多了。首先根据你手头的开发板型号选择正确的MCU。我用的是一块STM32F407VET6资源足够应对大多数中等复杂度的信号处理任务。创建好工程后第一个重头戏就是配置时钟树。ADC的采样率、DMA的传输速度乃至后续FFT运算的速度都直接依赖于系统时钟。对于需要做FFT分析的场景ADC的采样率必须满足奈奎斯特采样定理即至少是目标信号最高频率的两倍。比如你想分析1kHz的信号采样率至少得2kHz。但实际中为了获得更好的频谱效果我们通常会让采样率是信号最高频率的5-10倍。假设我的振动信号主要成分在500Hz以内我打算设置ADC采样率为5kHz。那么就需要根据这个目标来倒推时钟配置。STM32F4的ADC时钟ADCCLK最大为36MHz它由APB2时钟分频而来。在Clock Configuration标签页里我先把HSE外部高速晶振设为8MHzPLL倍频到168MHz作为系统时钟SYSCLK。然后将APB2的预分频器设为2得到APB2时钟为84MHz。最后将ADC的预分频器设为4这样ADCCLK就是21MHz84MHz / 4这个频率在安全范围内且为后续配置采样时间留出了余地。注意不同系列的STM32其最大ADCCLK可能不同务必查阅对应型号的参考手册。过高的ADCCLK可能导致ADC工作不稳定甚至损坏。时钟树配置看似枯燥但它决定了整个系统性能的基石。一个合理的时钟配置能让后续所有外设都运行在最佳状态避免因时钟瓶颈导致采样丢点、DMA传输卡顿等问题。3. ADC参数化配置详解时钟配好了接下来就是主角ADC的配置。在CubeMX的Pinout Configuration标签页下找到ADC模块。我打算使用ADC1的通道0对应PA0引脚进行采样。3.1 基础参数设置点开ADC1的配置首先看到的是“Parameter Settings”选项卡。Resolution分辨率这里我选择“12-bit”。对于振动信号分析12位分辨率4096个量化等级通常已经足够能在动态范围和精度之间取得良好平衡。如果信号非常微弱可以考虑外部放大电路而不是盲目提高ADC位数因为STM32内部更高分辨率如16位的ADC模式往往是通过过采样实现的会消耗更多时间和CPU资源。Scan Conversion Mode扫描转换模式因为我们只用一个通道所以选择“Disabled”。如果使能它会按顺序扫描多个开启的通道。Continuous Conversion Mode连续转换模式这里非常关键要选择“Enabled”。这意味着ADC一旦启动就会不停地、一个接一个地进行转换为DMA的连续传输提供源源不断的数据。Discontinuous Conversion Mode不连续转换模式保持“Disabled”与连续模式互斥。DMA Continuous RequestsDMA连续请求必须使能Enabled。这个选项允许DMA在ADC每次转换完成后自动发起新的传输请求是实现“ADC连续转换 - DMA连续搬运”这个自动化流程的核心开关。如果不禁用DMA在传输完预设的数据量后就会停止需要手动重新启动无法实现真正的后台连续流传输。End Of Conversion Selection转换结束选择选择“EOC flag at the end of single channel conversion”。这样每个数据转换完成都会产生标志方便DMA或中断响应。3.2 采样时间与规则通道配置切换到“Analog”选项卡或直接在左侧引脚图上点击PA0将其功能设置为“ADC1_IN0”。 然后在ADC1配置的“Analog”子选项卡下找到“Regular Conversion Mode”。点击“Add”添加一个规则组通道。在“Channel”中选择“Channel 0”。Sampling Time采样时间这是另一个极易出错的点。采样时间 转换周期数 / ADCCLK。转换周期数越多ADC内部的采样保持电容充电越充分结果越准确但转换速度越慢。我的ADCCLK是21MHz目标采样率是5kHz。那么一次完整转换所需的最长时间不能超过 1/5000 200us。ADC的转换时间 采样时间 12.5个周期12位分辨率下的固定转换时间。假设我设置采样时间为“84 Cycles”那么总周期数 84 12.5 96.5 cycles。总时间 96.5 / 21MHz ≈ 4.6us。这远小于200us完全满足5kHz采样率的要求并且留有充足的余量。对于信号源阻抗较高或需要更高精度的场合可以适当增加采样周期数。3.3 触发源选择在“Parameter Settings”的底部找到“External Trigger Conversion Source”。对于连续转换模式触发源其实可以简单选择“Regular Conversion launched by software”。这样我们只需要在软件中调用一次HAL_ADC_Start_DMA()转换就会持续进行下去。如果你需要非常精确的采样间隔例如严格每200us采样一次那么应该使用定时器触发。这里为了简化流程我们先使用软件触发。在后续优化部分可以再探讨如何引入定时器TIM来获得更精准的采样时钟。4. DMA配置构建自动数据搬运工ADC配置好了它会产生连续的数据流。我们需要一个“搬运工”及时把这些数据从ADC数据寄存器DR搬到我们指定的内存数组中这就是DMA的工作。在CubeMX左侧的“System Core”组里找到DMA并点击“Add”。DMA Request选择“ADC1”。Stream对于F4系列DMA有Stream概念可以任选一个可用的比如Stream0。Direction方向选择“Peripheral To Memory”。外设ADC是源头内存是目的地。Priority优先级设为“High”。确保ADC数据能及时被搬走防止数据覆盖丢失。Mode模式Normal普通模式传输完预设的数据量后停止。需要手动重启。Circular循环模式这是我们需要的模式必须选择它。在这种模式下DMA在传输完预设数据量后会自动回到起始地址重新开始传输形成一个首尾相接的“环”。这完美契合了ADC连续不断产出数据、我们需要循环缓冲区来接收的需求。Increment Address地址自增Peripheral外设地址保持“Disable”。ADC的数据寄存器地址是固定的我们总是从同一个地方读数。Memory内存地址必须“Enable”。这样每传输一个数据DMA的目的地址指针就会自动指向数组的下一个位置。Data Width数据宽度Peripheral外设数据宽度选择“Word”32位。虽然ADC是12位但其数据寄存器是32位的对于F4ADC_DR是32位寄存器低16位有效。Memory内存数据宽度也选择“Word”。保持对齐避免不必要的麻烦。FIFO Mode可以先保持默认。配置完成后回到ADC1的配置界面在“DMA Settings”选项卡中你会看到刚刚添加的DMA请求。点击“Add”将其关联到ADC1。至此CubeMX的图形化配置部分就完成了。你可以点击GENERATE CODE生成初始化代码。CubeMX会帮我们生成ADC1和DMA的初始化函数MX_ADC1_Init()和MX_DMA_Init()并在main()中调用它们。5. 软件驱动编写启动采集与数据缓冲区管理生成了代码只是搭好了舞台演员还没上场。我们需要在用户代码区编写驱动逻辑。5.1 定义数据缓冲区首先在main.c的/* USER CODE BEGIN PV */私有变量区域定义两个核心数组#define ADC_BUFFER_SIZE 1024 // 定义缓冲区大小建议为2的N次幂方便FFT volatile uint32_t adc_dma_buffer[ADC_BUFFER_SIZE]; // DMA搬运的目的地volatile关键字至关重要。它告诉编译器这个变量可能被硬件DMA在程序流程之外修改禁止编译器对其做激进的优化比如缓存到寄存器确保我们每次读取的都是内存中的最新值。缓冲区大小ADC_BUFFER_SIZE需要仔细考量。它决定了每次DMA传输回调触发时你能拿到多少数据。大小要兼顾实时性和FFT运算量。1024点是一个很常用的值因为很多FFT库如ARM的CMSIS-DSP对1024点有高度优化。5.2 启动ADC与DMA在/* USER CODE BEGIN 2 */区域系统初始化完成后启动ADC的DMA传输/* USER CODE BEGIN 2 */ // 启动ADC并让其通过DMA将数据搬运到指定的缓冲区 // 参数ADC句柄目标内存地址数据长度以字为单位 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); // 启动失败进入错误处理 } /* USER CODE END 2 */调用HAL_ADC_Start_DMA后ADC会立即开始连续转换并且每转换完一个数据DMA就自动将其从ADC-DR寄存器搬运到adc_dma_buffer数组中。当搬满了ADC_BUFFER_SIZE个数据即完成一次“半传输”或“全传输”取决于回调后因为DMA配置的是循环模式它会自动回到数组开头覆盖旧数据继续搬运。整个过程完全由硬件自动完成无需CPU干预。5.3 处理采集完成的数据数据在后台被DMA不停地搬运但我们怎么知道已经采集够一帧数据比如1024点可以进行FFT了呢这里通常有两种策略策略一使用DMA半传输/传输完成中断CubeMX生成的HAL库驱动默认使能了DMA传输完成中断。我们可以在stm32f4xx_it.c中找到DMA Stream的中断服务函数但更规范的做法是使用HAL库的回调机制。在main.c中我们需要重写DMA传输完成回调函数。首先在/* USER CODE BEGIN 4 */区域声明一个函数void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 当DMA搬运完ADC_BUFFER_SIZE个数据后会进入此回调 // 此时adc_dma_buffer中包含了最新、完整的一帧1024个数据 // 可以在这里设置一个标志位通知主循环或任务进行FFT处理 fft_ready_flag 1; } }然后在主循环中检查这个标志位/* USER CODE BEGIN WHILE */ while (1) { if (fft_ready_flag) { fft_ready_flag 0; // 执行FFT运算处理 adc_dma_buffer 中的数据 do_fft_analysis((uint16_t*)adc_dma_buffer, ADC_BUFFER_SIZE); } // ... 其他任务 /* USER CODE END WHILE */这种方法的优点是时机精确每次回调都对应着完整的一帧新数据。缺点是中断上下文不宜进行复杂的FFT运算必须尽快设置标志位退出将耗时操作放到主循环。策略二双缓冲Ping-Pong Buffer机制这是更高级、也更推荐用于实时处理的策略。我们定义两个一样大的缓冲区Buffer_A和Buffer_B。初始化时让DMA搬运到Buffer_A。当DMA传输完成中断或半传输中断触发时意味着Buffer_A已经填满。在回调函数中我们不处理数据而是立刻将DMA的目的地切换到Buffer_B同时设置一个标志告诉主程序“Buffer_A已满可处理”。主程序看到标志后对Buffer_A进行FFT运算。在此期间DMA正在向Buffer_B填充数据。当Buffer_B填满时再次触发中断将DMA目的地切回Buffer_A并通知主程序处理Buffer_B。如此往复就像打乒乓球一样。这样做的好处是数据采集DMA和数据处理CPU在时间上完全并行几乎没有空闲等待极大地提高了系统效率和实时性。实现双缓冲需要对HAL库的DMA控制有更深的理解可能需要直接操作DMA寄存器的M0AR内存0地址寄存器和M1AR内存1地址寄存器并配合使用半传输中断HT和传输完成中断TC。这是后续文章可以深入的话题。5.4 数据格式转换与校准从adc_dma_buffer读出的数据是原始的ADC数字量对于12位ADC范围0-4095。在进行FFT之前通常需要将其转换为电压值或物理量。float adc_value_to_voltage(uint32_t adc_raw, float vref) { // vref 是ADC的参考电压通常是3.3V return ((float)adc_raw / 4095.0f) * vref; }另外别忘了ADC可能有零点误差和增益误差。对于精度要求高的场合需要进行校准。STM32的ADC模块通常提供自校准功能可以在初始化ADC后调用HAL_ADCEx_Calibration_Start()来执行。6. 调试与验证确保数据流畅通配置和代码都写完了怎么验证ADCDMA是否正常工作呢6.1 使用调试器查看内存最直接的方法是在IDE如Keil、IAR或STM32CubeIDE的调试模式下添加adc_dma_buffer到Watch窗口并设置为周期性更新。然后给ADC输入一个已知的直流电压比如通过开发板上的电位器观察数组中的值是否稳定在预期的数字量附近。改变输入电压数组值应随之变化。6.2 利用串口打印数据将采集到的数据通过串口发送到PC用串口助手或Python/Matlab绘制波形。这是一个非常有效的方法。// 在DMA传输完成回调中将数据通过串口发送注意中断中不宜进行耗时操作此处仅为示例 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 只发送前100个点看看效果 for(int i0; i100; i) { printf(%d\n, (uint16_t)adc_dma_buffer[i]); // 假设printf已重定向到串口 } } }在PC端用Python的matplotlib可以快速绘图import serial import matplotlib.pyplot as plt ser serial.Serial(COM3, 115200) # 根据实际情况修改串口号和波特率 data [] for _ in range(100): line ser.readline().decode().strip() if line: data.append(int(line)) plt.plot(data) plt.show()如果输入一个正弦波信号你应该能看到打印出的数据也是正弦变化。6.3 检查常见的配置问题数据全是0或固定值检查ADC引脚配置是否正确模拟输入电压是否加上ADC和DMA的时钟是否使能。数据不变检查是否启动了连续转换模式Continuous Conversion Mode和DMA连续请求DMA Continuous Requests。数据错乱或溢出检查DMA缓冲区大小是否足够DMA是否配置为循环模式Circular内存地址自增是否开启。确保在DMA搬运的同时CPU没有同时去修改这个缓冲区除非使用双缓冲等同步机制。采样率不对复核ADC时钟ADCCLK和采样周期Sampling Time的计算。采样率 ADCCLK / (采样周期 12.5)。可以使用定时器产生一个精确的脉冲用逻辑分析仪或示波器测量ADC转换完成EOC引脚或DMA请求的间隔来验证。7. 为FFT做准备数据预处理要点现在我们已经有了一个稳定流动的ADC原始数据流。但在喂给FFT算法之前通常还需要一些预处理。7.1 数据类型的转换FFT库如CMSIS-DSP通常要求输入数据是浮点数float32_t或定点数q15_t,q31_t。我们需要将uint32_t的原始ADC值转换为合适的类型。为了保留精度通常先转换为浮点数。float32_t fft_input[ADC_BUFFER_SIZE]; for(int i0; iADC_BUFFER_SIZE; i) { // 转换为电压并去除直流分量减去均值这对频谱分析很重要 fft_input[i] adc_value_to_voltage(adc_dma_buffer[i], 3.3f) - dc_offset; }去除直流分量减去信号的均值是一个关键步骤。如果不做会在频谱的0Hz处直流分量产生一个巨大的峰值可能掩盖附近低频信号的细节。7.2 加窗Windowing由于我们截取的是无限长信号中的一段1024点这相当于用一个矩形窗去截取信号。在频域上这会导致频谱泄露Leakage即一个频率的能量会“泄露”到旁边的频率点上使频谱变得模糊。为了减少泄露需要对时域数据加窗。常用的窗函数有汉宁窗Hann、汉明窗Hamming等。// 应用汉宁窗 for(int i0; iADC_BUFFER_SIZE; i) { float32_t window 0.5f * (1.0f - arm_cos_f32(2 * PI * i / (ADC_BUFFER_SIZE - 1))); // 汉宁窗公式 fft_input[i] * window; }ARM的CMSIS-DSP库直接提供了加窗函数如arm_hanning_f32使用起来更方便。加窗会稍微改变信号的幅值在计算最终幅值时需要进行补偿窗函数的相干增益补偿。7.3 对齐与填充大多数FFT算法要求数据长度N是2的整数次幂如2565121024。我们之前定义的ADC_BUFFER_SIZE为1024正好满足。如果采集的数据点不是2的幂需要将其补零Zero Padding到最近的2的幂。补零不会增加频率分辨率但可以让FFT算法更高效地运行并且使频谱图看起来更平滑。至此一个基于STM32 CubeMX的ADCDMA实时数据采集框架就搭建并验证完毕了。我们配置了时钟和ADC参数建立了DMA循环传输通道编写了启动和数据处理逻辑并探讨了为后续FFT所做的预处理。这个框架就像一条自动化流水线源源不断地将模拟世界的信号转换成整齐排列在内存中的数字阵列为下一步的频率域分析——FFT做好了全部准备。在下一篇文章中我们将深入探讨如何在这个框架上集成CMSIS-DSP库进行高效的FFT运算并解读频谱结果的实际意义。
返回列表