☰
AT32F413实现正弦波频率测量:ADC+TMR+DMA+FFT方案详解
2026/11/4 5:21:35 网站建设 项目流程

简介:该资源面向嵌入式开发工程师与信号处理学习者,提供基于AT32F413单片机的正弦波频率测量完整工程,解决ADC采样、定时器触发、DMA搬运与FFT频谱分析协同工作的实现问题,适合具备一定外设编程基础、希望掌握实时频率测量方案的中级开发者参考。压缩包共560个文件,约6.33MB,以C源码与头文件为主体,涵盖ADC、TMR、DMA驱动及DSP库中的FFT、矩阵运算、FIR滤波等算法实现,另含Keil工程文件、链接脚本、编译中间产物与说明文档,目录结构贴近实际量产项目。已有429人学习下载。工程将模拟采样、定时触发、内存直传与频域分析串联成完整链路,读者可据此理解采样率与频率分辨率的关系、DSP库调用方式及外设配置细节,并可直接在硬件上复现调试,是学习单片机信号处理的实用范例。

1. 用一颗 AT32F413 把正弦波频率测准:这套 ADC+TMR+DMA+FFT 方案到底能干什么

手上有一块 AT32F413 的开发板,信号发生器输出一路正弦波,想实时读出它的频率——这件事听起来简单,真做起来却容易翻车。用比较器过零检测,噪声一大就误触发;用输入捕获测周期,波形稍微畸变读数就飘;想上 FFT,又担心单片机算力不够、采样和计算对不上号。这套基于 AT32F413 的 ADC + TMR + DMA + DSP 库 FFT 方案,解决的正是这个场景:定时器精确控制采样率,ADC 连续采样,DMA 搬运不占 CPU,最后用官方 DSP 库做 FFT 找峰值频点,把正弦波频率算出来。适合正在做信号检测、音频分析、传感器前端,或者单纯想把「采样—传输—变换」这条链路跑通的嵌入式开发者。下面按「能干什么 → 怎么跑 → 怎么改」拆开讲。

2. 技术选型与整体链路:为什么是 ADC+TMR+DMA+FFT 这套组合

2.1 四个外设各管一段,缺一不可

这套方案的核心思路是把「采样」和「计算」彻底解耦。TMR(定时器)负责产生精确的采样触发信号,ADC 被触发后采一个点,DMA 把这个点悄悄搬到内存缓冲区,CPU 全程不参与搬运,等攒够一帧数据再统一做 FFT。四个角色分工明确:

模块职责关键参数
TMR产生采样触发,决定采样率 fs定时周期 = 1/fs
ADC把模拟正弦波转成数字量分辨率、采样时间、触发源
DMA把 ADC 数据搬到内存,不占 CPU传输宽度、循环模式、缓冲区长度
DSP 库 FFT把时域序列变换到频域找峰值点数 N、窗函数、幅值换算

为什么不用 CPU 轮询 ADC?因为采样率一旦上到几十 kHz,轮询会吃掉大量算力,还会因为中断抖动导致采样间隔不均匀,FFT 出来的频谱全是泄漏和杂散。TMR 触发 + DMA 搬运是嵌入式里做等间隔采样的标准做法,采样时刻由硬件保证,抖动在纳秒级,这是后面频率能测准的前提。

2.2 采样率、点数、频率分辨率三者的关系

这是整套方案里最容易被忽略、又最影响结果的一组参数。FFT 的频率分辨率 Δf = fs / N,也就是说你想分辨多细的频率间隔,由采样率和点数共同决定。比如 fs = 10 kHz、N = 1024,Δf ≈ 9.77 Hz,两个相差 5 Hz 的信号在这个配置下根本分不开。

反过来,根据奈奎斯特采样定理,fs 至少要大于信号最高频率的 2 倍,工程上一般取 4~10 倍留余量。测 1 kHz 正弦波,fs 取 10 kHz 比较稳妥,N 取 1024 或 2048,兼顾分辨率和 RAM 占用。AT32F413 的 RAM 有限,N 太大缓冲区吃不消,2048 点浮点 FFT 大约要 8 KB 复数缓冲,得提前算好内存账。

2.3 用官方 DSP 库而不是自己撸 FFT

自己写基 2 FFT 不是不行,但定点/浮点缩放、位反转、旋转因子表这些细节很容易出错,调试成本高。AT32 官方提供的 DSP 库里有现成的cr4_fft_1024_stm32这类函数(不同版本命名略有差异,常见做法是查库里的 fft 头文件确认函数名),直接调用即可。用库的好处是经过验证、速度快,坏处是要搞清楚它的输入格式——很多库要求输入是 Q15 定点或者实部虚部交错存放,喂错格式结果就是一团乱码。这一点后面代码里会专门讲。

3. 从零跑通:工程搭建、外设配置与采样链路

3.1 工程骨架与依赖准备

拿到源码后先看目录,典型结构大致是这样:

project/ ├── libraries/ │ ├── cmsis/ # 内核头文件 │ ├── at32f413_std/ # 标准外设库 │ └── dsp/ # 官方 DSP 库(FFT 相关) ├── project/ │ ├── inc/ # 用户头文件 │ └── src/ │ ├── main.c │ ├── adc_config.c │ ├── tmr_config.c │ └── fft_process.c └── md5/ 或 keil 工程文件

先确认三件事:DSP 库有没有加进工程、链接脚本里 RAM 够不够、系统时钟配到了多少。AT32F413 常见主频 200 MHz 左右(具体看型号和配置),ADC 时钟不能超过手册规定上限,通常要分频。我一般先把时钟树在system_at32f413.c或时钟配置函数里核对一遍,再动外设,否则后面采样率算出来全是错的,排查半天发现是时钟源没配对——这种坑踩一次就够。

3.2 定时器触发 ADC 的配置

定时器在这里不是用来计数的,而是当「节拍器」。以 TMR3 为例,配置成向上计数、更新事件触发 ADC:

void tmr3_config(uint32_t sample_rate) { tmr_base_init(TMR3, (system_core_clock / sample_rate) - 1, 0); tmr_cnt_dir_set(TMR3, TMR_COUNT_UP); /* 更新事件作为 ADC 外部触发源 */ tmr_trigger_output_config(TMR3, TMR_TR_OUTPUT_SELECT_UPDATE); tmr_counter_enable(TMR3, TRUE); }

sample_rate就是目标采样率 fs,分频值用系统时钟除以 fs 再减一得到。这里有个细节:tmr_trigger_output_config让更新事件输出到触发线,ADC 那边要配置成外部触发模式并选中对应的触发源(比如 TMR3_TRGO)。两边对不上,ADC 就永远等不到触发,现象是 DMA 缓冲区一直是 0,很多人第一次调会以为是 DMA 坏了,其实是触发源没连上。

3.3 ADC + DMA 循环采样

ADC 配置成连续转换、外部触发、DMA 循环模式,缓冲区长度等于 FFT 点数:

#define FFT_N 1024 uint16_t adc_buf[FFT_N]; void adc_dma_config(void) { dma_init_type dma_init_struct; dma_reset(DMA1_CHANNEL1); dma_init_struct.direction = DMA_DIR_PERIPHERAL_TO_MEMORY; dma_init_struct.memory_base_addr = (uint32_t)adc_buf; dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_HALFWORD; dma_init_struct.peripheral_base_addr = (uint32_t)&ADC1->odt; dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_HALFWORD; dma_init_struct.buffer_size = FFT_N; dma_init_struct.loop_mode_enable = TRUE; /* 循环模式,采满自动回卷 */ dma_init(DMA1_CHANNEL1, &dma_init_struct); dma_channel_enable(DMA1_CHANNEL1, TRUE); adc_base_config(ADC1, ADC_MODE_INDEPENDENT, ...); adc_external_trigger_config(ADC1, ADC_EXT_TRIGGER_RISING_EDGE, TRUE); adc_dma_mode_enable(ADC1, TRUE); adc_enable(ADC1, TRUE); }

loop_mode_enable = TRUE是关键,采满 1024 点后 DMA 自动回到缓冲区头部继续写,配合一个「半满/全满」标志就能做乒乓处理。memory_data_width用半字是因为 ADC 数据寄存器是 12 位,用uint16_t存刚好。注意 ADC 的采样时间要设够,信号源内阻大时采样时间太短会导致采样值偏低,频谱上表现为幅值不准,但频率一般还能测对。

3.4 主循环里做 FFT 与峰值搜索

采满一帧后,把adc_buf里的原始数据转成 FFT 库要求的格式,调用 FFT,再在幅值谱里找最大峰:

void fft_measure_freq(void) { uint16_t i; float max_mag = 0.0f; uint16_t max_idx = 0; /* 1. 去直流 + 转 Q15 定点(库要求) */ for (i = 0; i < FFT_N; i++) { int32_t v = (int32_t)adc_buf[i] - 2048; /* 12 位 ADC 中点 */ fft_in[i] = (int16_t)(v << 4); /* 放大到 Q15 量级 */ } /* 2. 调用 DSP 库 FFT(函数名以实际库为准) */ cr4_fft_1024_stm32(fft_out, fft_in, FFT_N); /* 3. 计算幅值,找最大峰(只看前半谱) */ for (i = 1; i < FFT_N / 2; i++) { float re = (float)(int16_t)(fft_out[i] & 0xFFFF); float im = (float)(int16_t)(fft_out[i] >> 16); float mag = re * re + im * im; if (mag > max_mag) { max_mag = mag; max_idx = i; } } /* 4. 频点换算成频率 */ float freq = (float)max_idx * SAMPLE_RATE / FFT_N; printf("freq = %.2f Hz\r\n", freq); }

逐段说明:第一步减 2048 是去掉 ADC 的直流偏置,否则 FFT 后 0 Hz 处会有一个巨大的直流峰,把真正的信号峰盖住;左移 4 位是很多定点 FFT 库对输入幅度的要求,太小会丢精度,太大会溢出,具体移位量要看库文档。第二步调用库函数,注意输入输出缓冲区别混用。第三步只扫前半谱,因为实信号 FFT 后频谱对称,后半段是镜像。第四步用max_idx * fs / N换算频率,这个公式就是频率分辨率的直接应用。

提示:如果测出来的频率总是差一个固定值,先检查SAMPLE_RATE宏和定时器实际分频是否一致,这是最高频的翻车点。

4. 避坑与排查:这套链路最容易出问题的五个地方

4.1 现象:DMA 缓冲区全是 0 或固定值

原因通常是 ADC 触发源没配对,或者 DMA 通道选错。AT32F413 的 ADC1 一般映射到固定 DMA 通道,选错通道 DMA 请求永远不来。解决:对照参考手册的 DMA 请求映射表确认通道,再用调试器看 ADC 的odt寄存器有没有在变,变说明 ADC 在采,不变就是触发没到。

4.2 现象:频谱上信号峰位置对,但旁边一堆杂散

采样间隔不均匀或者没加窗。TMR 触发本身是均匀的,但如果主循环里做了耗时操作导致 DMA 缓冲区被覆盖一半才开始读,数据就撕裂了。解决:用 DMA 传输完成中断置标志,主循环只在标志置位后处理,处理期间让 DMA 写另一块缓冲(双缓冲)。另外对非整周期截断的信号,加汉宁窗能明显压低泄漏。

4.3 现象:频率读数跳动大,每次差几十 Hz

点数太少或分辨率不够。Δf = fs/N,N=1024、fs=10k 时分辨率约 9.77 Hz,峰值索引跳一格就是 10 Hz 的跳动。解决:增大 N 到 2048/4096,或者在峰值附近做抛物线插值,用相邻三个点的幅值估算真实峰值位置,能把精度提高一个量级。

4.4 现象:FFT 结果全是乱值或溢出

输入格式喂错。定点 FFT 库通常要求 Q15 格式、实部虚部按特定顺序排列,直接把 ADC 原始值塞进去必然出错。解决:先拿一个已知频率的仿真正弦序列(在代码里用sin()生成)喂进 FFT 验证链路,确认库调用正确后再接真实 ADC 数据,这样能把「库用错」和「采样错」两类问题分开。

4.5 现象:低频段测不准,高频反而正常

ADC 采样时间不足或信号源带直流偏置。低频信号周期长,如果采样窗口没覆盖完整周期,相位不连续。解决:确保采样时长至少覆盖信号的好几个周期,即 N/fs 远大于 1/f_signal;同时确认前端有没有隔直电容。

5. 二次开发:把这套链路改成能落地的测量工具

跑通 demo 只是起点,真正要用起来,得在三个方向做改造。第一是实时性:把 FFT 计算放进 DMA 传输完成中断里做双缓冲乒乓,主循环只负责显示,这样采样不中断,频率刷新率能到几十 Hz。第二是精度:在峰值搜索后加抛物线插值,代码不长但效果明显:

/* 抛物线插值:用峰值及左右邻点估算真实频点 */ float y0 = mag[max_idx - 1]; float y1 = mag[max_idx]; float y2 = mag[max_idx + 1]; float delta = 0.5f * (y0 - y2) / (y0 - 2.0f * y1 + y2); float freq = ((float)max_idx + delta) * SAMPLE_RATE / FFT_N;

delta是峰值相对整数索引的偏移量,范围在 -0.5 到 0.5 之间,加上它之后频率分辨率不再受限于 fs/N,实测能压到 1 Hz 以内。第三是鲁棒性:加一个幅值门限,最大峰低于门限就判定为无信号,避免噪声被当成正弦波报出一个随机频率。另外把采样率、点数做成宏或者运行时可配,换测量场景不用改代码结构。

验证方法我一般这么走:先用信号发生器给 1 kHz、5 kHz、10 kHz 几个点,记录读数误差;再把幅值从满量程降到 10%,看频率是否还稳;最后叠加一点白噪声,观察门限逻辑是否误判。三步都过,这套链路才算能交付。

血泪经验是:每次改采样率或点数,一定先把SAMPLE_RATE宏、定时器分频、DMA 缓冲区长度、FFT 函数名这四处一起改,漏一处就是玄学读数。从那以后我每次动这套参数都强制走一遍「仿真正弦自测 → 实测已知频率 → 降幅值复测」的流程,再没在客户现场翻过车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询