STM32 ADC采集与DAC输出实战:从硬件设计到信号质量优化

发布时间:2026/8/31 15:43:02
STM32 ADC采集与DAC输出实战:从硬件设计到信号质量优化 简介本资源是一套面向嵌入式初学者与STM32进阶开发者的ADC采集与DAC输出实战工程聚焦模拟信号链路的完整闭环实现解决信号采样、处理与重建中的同步控制、速率匹配及CPU负载优化等典型问题。压缩包共208个文件含40个.h头文件定义外设寄存器与接口、39个.c源文件涵盖ADC初始化、DMA配置、DAC触发输出及主循环调度逻辑、27个.o目标文件与26个.d依赖文件辅以uvproj工程文件、hex可烧录镜像及map/lnp等调试支持文件整体大小为4.39MB。已有104人学习下载。资源基于STM32F10x系列代码结构清晰包含stm32f10x_adc.__i、stm32f10x_dac.__i等关键预编译头完整呈现无DMA模式下ADC读取与DAC写入的时序协同、中断服务函数设计及采样失真边界分析如700Hz有效采集上限与4kHz以上打点不全现象便于读者深入理解模数/数模转换底层机制并快速复现验证。 STM32的ADC采集和DAC输出这对组合在嵌入式里就像人的眼睛和手——一个负责感知外面的模拟世界一个负责把计算结果重新变回模拟信号。做仪器仪表、信号发生器、电机控制、音频处理的朋友应该都有体会单独用ADC或者单独用DAC都不难难的是把这一进一出放在同一块板子上还能保证信号质量不崩。这篇文章我打算完整记录一次基于STM32的ADC采集和DAC输出实现过程从芯片选型、硬件电路设计、CubeMX配置、代码编写到联调时遇到的各种信号质量问题、软件滤波方案、以及几个很有意思的进阶玩法全部串起来讲。我踩过不少坑这篇文章里的每一段基本都有对应的真实调试经历希望能帮你在自己的项目里少走弯路。1. 硬件选型和模拟链路设计ADC/DAC的“底子”决定上限很多人做STM32的ADC和DAC项目时掉进一个误区上来就写代码觉得CubeMX配置一下、调几个HAL库函数就完事了。但我做了几个项目之后的体会是代码只能决定功能能不能跑起来信号质量好不好八九成取决于硬件设计。ADC/DAC电路不像数字电路那样只要电平对就行它对参考电压、电源纹波、输入输出阻抗都特别敏感。1.1 选型前先看清楚不是所有STM32都带DAC先说一个特别容易踩的坑。很多新手用STM32F103C8T6就是最常见的那种蓝板子做实验配置的时候发现怎么都找不到DAC外设——因为F103系列里只有大容量产品比如RCT6、ZET6和互联型产品才有DACC8T6这种中等容量芯片根本没有DAC模块。我第一次做需要同时采集和输出的项目时也在这上面耽误了半天最后换成了F103RCT6才解决问题。如果是新项目选型我建议先花几分钟把芯片的ADC/DAC资源摸清楚。我自己常用的几款芯片对比如下型号ADC资源DAC资源内部参考电压适合场景STM32F103RCT612位ADC × 312位DAC × 2无需外部VREF入门、资料最多、性价比高STM32F407ZET612位ADC × 312位DAC × 2无VREF引脚主频高、定时器多、适合波形发生器STM32G071系列12位ADC 硬件过采样12位DAC × 2内置VREFINT低功耗、可以省掉外部基准芯片选型的关键不仅是看有没有DAC还要看ADC的输入通道够不够、定时器资源是否能用来触发采样、以及有没有内部基准电压。G071这样带内部基准的芯片做便携设备就很方便但如果你对精度要求高无论用哪款芯片最终还是得外接基准电压源。1.2 输入侧设计ADC不只是一个“读电压”的引脚STM32的ADC内部结构是“采样电容逐次逼近比较器”。采样的时候内部采样电容会瞬间连接到输入引脚从一个等效阻抗较高的信号源抽取电荷。如果信号源的内阻太高采样电容充不满读出来的值就会偏低而且随着采样时间不同漂移。这就是为什么规格书里会给出最大输入源阻抗的推荐值一般F1系列建议不超过10kΩ。处理方式有三种信号源内阻本身很低比如运放输出可以直接连到ADC引脚。信号源内阻中等用RC低通滤波R取值控制在几百欧到几kΩC选1nF到100nF但要留出采样时间让电容完成充电。信号源内阻很高比如一个几十kΩ的分压器输出最稳妥的做法是加一级电压跟随器。用一个轨到轨运放比如LMV321、OPA340接成跟随器把高阻信号转成低阻信号再送给ADC。RC截止频率的计算公式是f_c 1 / (2πRC)。比如R1kΩ、C100nF时截止频率约为1.6kHz适合采样几百Hz以内的缓变信号。如果你想采集更快的信号就要缩小R或C的值但缩小以后要重新评估源阻抗匹配问题。1.3 输出侧设计DAC的驱动能力和缓冲器问题DAC输出端同样有讲究。STM32的DAC本质上是一个电阻网络它内部集成了输出缓冲器但缓冲器不是万能的。开启Buffer之后输出阻抗会降低能带动一定的负载但会引入额外的失调电压和少量的非线性关闭Buffer则输出阻抗较高带负载能力差但精度会稍好一些。我的做法是如果DAC后面接的是高输入阻抗的下一级电路比如运放同相输入端就直接开Buffer简单省事如果DAC要直接驱动几十欧以下的负载那就必须在外面再接一级运放做功率放大不能指望片内缓冲器扛太重的负载。另外DAC输出的毛刺电压glitch也是一个常见问题。快速改变DAC输出值时内部开关切换会产生短暂的尖峰。在输出端并联一个几十到几百pF的电容能明显改善。但电容也不能太大否则会影响输出信号的上升沿这个平衡在第三章讲方波波形时会重点展开。1.4 供电方案和模拟地处理信号质量的隐形杀手这一节可能是整个项目里最“便宜”但最有效的优化。ADC/DAC性能再好如果给它供电的是一个纹波很大的开关电源输出和测量结果都会带上周期性噪声。我常用的供电方案是两级处理数字电源进来之后先经过一个LC滤波比如10μH电感10μF电容再给模拟部分供电。如果是3.3V供电还可以单独用一个低噪声LDO如AMS1117分出来的模拟3.3V给VREF供电。F1系列虽然没有真正独立的AVDD引脚但VREF引脚可以独立供电我就把VREF单独接到一个高精度基准源比如REF3030输出3.0V/30ppm℃上ADC/DAC满量程精度会提升一个档次。地线处理记住一句话模拟地和数字地没有物理分离再贵的基准芯片也白搭。我习惯在PCB上把模拟电路区域和数字电路区域分开铺铜然后在一个单点通常是电源地接入点附近做单点连接或者用0Ω电阻/磁珠连接。这样数字电路产生的地弹噪声就不会直接串进ADC的采样回路里。2. ADC采集从单通道阻塞读值到多通道DMA循环采样ADC这部分的代码逻辑其实不复杂HAL库把底层寄存器的操作都封装好了。但很多人配置完之后发现数据有问题——要么多通道采集的数据顺序和通道号对不上要么采样速率远低于预期要么DMA中断一直进不去。这些问题我在实践中都遇到并逐一解决掉下面按我的使用习惯从头过一遍。2.1 CubeMX里的关键配置项逐项说人话在CubeMX里配置ADC1按照你的项目需求选择Enable Regular Conversion Mode常规转换模式。几个关键参数的含义Resolution分辨率F103和F4系列都是12位G0系列也支持12位。如果没有特殊需求就保持12位位数越低采样速度越快但精度下降。Scan Conversion Mode扫描模式只用单通道就关掉要采集多个通道就必须打开。打开后ADC会按你配置的通道顺序依次转换转换结果按顺序存放在数据寄存器里。Continuous Conversion Mode连续转换模式开启后ADC转换完一轮就马上开始下一轮适合配合DMA做连续采样。如果只用软件启动单次读取这个模式可以关。Discontinuous Conversion Mode不连续模式一般用不到它是把多个通道分成若干小组轮询转换省电流但影响速率。DMA Continuous RequestsDMA连续请求如果开了连续转换模式再配合这个选项ADC每转换完一个通道就会自动触发DMA搬运不需要CPU参与。采样时间Sampling Time是最需要根据实际信号源来设置的参数。采样时间越长采样电容充电越充分精度越高但单位时间内能转换的次数就越少。我一般先按最保守的配置如239.5周期跑通之后再去逐渐减小采样时间直到精度仍然满足要求为止。2.2 单通道读取的最简实现先看一个最简单的单通道阻塞式读取适合低速、无实时的场景做做传感器慢速采集完全够用#include adc.h #include main.h extern ADC_HandleTypeDef hadc1; uint32_t read_adc_value(void) { uint32_t adc_value 0; HAL_ADC_Start(hadc1); // 等待转换完成超时设为100ms if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); return adc_value; }读出来的ADC原始值怎么换算成实际电压呢公式是实际电压 ADC原始值 / 4095 × 参考电压如果参考电压是3.3VADC读到2048对应的电压就是 2048 / 4095 × 3.3 ≈ 1.65V。这里面有个很容易忽略的问题参考电压到底精确到多少假如你用的3.3V实际是3.28V算出来的电压值就会整体偏高。所以关键应用里最好用万用表实测一次VREF电压然后把实测值代入公式。2.3 多通道DMA循环采样做实时采集的正确姿势一旦要连续采集多路模拟量阻塞式和中断式就都不合适了。中断式虽然不阻塞CPU但每次ADC转换完成都要进一次中断频繁进出会拖慢主循环而且中断处理逻辑里读数据、存数组稍不留神就会丢数据。DMA方式由硬件把ADC的数据寄存器搬到内存数组里CPU只在整轮数据准备好的时候去取结果这才是嵌入式采集系统的常规做法。CubeMX里的配置步骤是把Scan Conversion Mode打开然后在ADC的配置界面里把要用的通道比如IN0、IN1、IN2、IN3都添加进去。DMA设置里添加ADC1Mode选择Circular循环模式Data Width选择Half Word半字因为12位的数据类型是uint16_t。DMA的目标内存是一个数组顺序很关键数组的第0个元素永远对应第一个配置的通道第1个元素对应第二个通道以此类推。如果你在CubeMX里先加的IN2后加的IN0那么数组[0]就是IN2的数据这点经常有人搞混。生成代码后启动方式如下uint16_t adc_buf[4]; // 对应4个通道 void start_adc_dma(void) { // 参数2是数组首地址参数3是要搬运的数据个数 // 这个个数等于通道数 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 4); } // 当一轮转换完成数据已全部搬运到数组时HAL库会回调这个函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { for (int i 0; i 4; i) { // 在这里处理adc_buf[i]的数据 g_adc_raw[i] adc_buf[i]; } g_adc_ready 1; } }注意HAL_ADC_Start_DMA的第二个参数类型是uint32_t*但实际传递的是uint16_t数组指针编译时可能需要强转一下。数组在DMA循环模式下会持续被硬件更新所以不要在主循环之外的地方读取这个数组防止读到一半数据被改写。我一般会先拷贝到另一个变量里再处理虽然多花一点内存和时间但能避免很多莫名其妙的问题。如果数据量更大比如要一次性采集128个点做FFTDMA的Half Transfer中断半传输中断就很有用了。在采集前半段时主循环可以处理上一次的后半段数据相当于把处理时间藏在了采集时间里。这也是很多音频采集项目用“DMA半传输传输完成双回调”做乒乓缓冲的套路。2.4 采样率到底能到多少自己动手推一遍很多人拿示波器去测ADC采样率发现和理论值差很多根本原因是没有搞清楚时钟树和转换周期的关系。我用F103举例完整推导一遍。F103的ADC时钟来源于APB2总线时钟默认72MHz但ADC外设的最大时钟是14MHz所以必须分频。CubeMX里ADC Clock Prescaler可以选2/4/6/8分频选6分频就是12MHz选8分频就是9MHz选4分频就是18MHz——如果你不小心选了4分频ADC时钟超频了转换结果精度会显著下降。这一步超频导致数据跳动是新手经常忽视的问题。ADC完成一次12位转换需要固定的12.5个ADC时钟周期再加上你配置的采样时间。比如配置采样时间为1.5周期那么总周期数 12.5 1.5 14个周期。ADC时钟12MHz、采样时间1.5周期时最大采样率 12MHz / 14 ≈ 857kSPS。这个速率是针对单通道连续转换的。如果是多通道速率要除以通道数4个通道扫描时每个通道的实际采样率约214kSPS。但注意这是ADC模块本身的理论极限实际系统中DMA带宽、主循环处理速度、以及等待中断的时间都会有影响。所以估算时要留出余量不能在理论上限附近设计系统。2.5 上电校准不做校准精度就输在起跑线上F1系列和F4系列的ADC内部都有校准功能但它不是自动的必须由软件触发。如果不做校准ADC的零点偏移误差可能导致读值偏大或偏小几十个LSB。这在高精度采集场景是不可接受的。CubeMX不会自动生成校准代码需要手动在初始化之后加一行HAL_ADCEx_Calibration_Start(hadc1);这个函数会执行一次内部校准流程完成后校准因子会保存在ADC的寄存器里之后转换结果会自动修正。G0系列还有偏移校准和线性校准用法类似。每次上电后执行一次校准就够了不需要在运行中反复校准。唯一的例外是如果你在运行中忽然改变了ADC时钟或参考电压建议重新校准一次。还有一个常见问题是有人把校准函数放在HAL_ADC_Init之前调用结果校验失败。正确顺序是先调用MX_ADC1_Init()完成初始化再调用校准最后启动转换。3. DAC输出方波过冲、正弦波生成和波形质量改善DAC部分的实战中我印象最深的不是代码写不出来而是波形在示波器上露馅——明明寄存器里写的是完美的数字波形输出到引脚上就变了样。3.1 DAC输出原理和计算公式STM32的DAC内部结构是R-2R梯形电阻网络加输出缓冲器。DAC输入数字量经过电阻网络转换成电流再通过内部的输出放大器转成电压。用户能控制的寄存器是DOR数据输出寄存器计算公式是输出电压 DOR / 4095 × VREF举个例子VREF3.3V想输出1.0VDOR 1.0 / 3.3 × 4095 ≈ 1241转换成十六进制就是0x4D9。使用HAL库写DAC值的函数是HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 1241);第三参数DAC_ALIGN_12B_R表示右对齐12位数据。如果你需要更高分辨率但DAC本身位数不足可以用PWMDAC的实现方式但STM32G0系列内部有硬件过采样也能起到类似提升有效位数的效果。3.2 输出直流电压误差校准不可省DAC输出一个固定直流电压用万用表测出来往往不是理论值。比如我想输出1.0V实测可能只有0.98V再换个电压点测误差比例还不太一样。这体现的是DAC的增益误差和偏移误差叠加结果。解决办法是两点校准。我通常先输出满量程的10%约0.33V和90%约2.97V用高精度万用表记录实测值然后建立线性关系实际电压 实测10%点 (DOR - DOR_10%) / (DOR_90% - DOR_10%) × (实测90%点 - 实测10%点)把这个线性关系保存成校准参数在每次设置DAC输出之前反算实际应该填写的DOR值。校准之后DAC输出精度一般能从±30mV提升到±5mV以内前提是参考电压稳定。3.3 方波输出上升沿过冲和振荡的解决过程这是我在做信号发生器时遇到的问题。DAC配置好之后写一个简单的方波输出程序void output_square_wave(void) { while (1) { HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0); delay_us(50); HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 4095); delay_us(50); } }用示波器看波形发现方波上升沿并不理想电压先冲到目标值以上约20%然后又跌回来反复振荡几次才稳定。下降沿也有类似的下冲。这一看就是典型的过冲振铃现象。这个问题有三层原因从主到次分别是首先输出回路的寄生电感和负载电容组成了一个LC谐振回路。DAC引脚到示波器探头之间会有几十nH的寄生电感加上示波器探头自身约10pF的输入电容谐振频率可能在几十MHz。方波的上升沿包含丰富的高频分量正好激励起这个谐振。其次DAC内部的输出缓冲器有一个有限的带宽快速跳变时输出级会出现过冲。最后如果示波器探头的地线夹用的是那种十几厘米长的鳄鱼夹线探头回路引入了额外的电感会让振铃更严重。解决方案我试过几种效果最好的是在DAC输出端串联一个小电阻100Ω左右再并联一个10nF电容到地。这个RC组合的截止频率约160kHz它能明显压制高频振铃。代价是方波的上升沿变缓了但只要能满足系统对边沿的要求换来的稳定波形是值得的。如果对边沿速度有要求可以改用两级结构DAC输出先进一个单位增益运放在运放输出端再做RC整形。运放的压摆率决定了边沿能有多快同时它的低输出阻抗会削弱寄生谐振。3.4 查表法生成正弦波DACDMA的经典组合正弦波比直流和方波更进一步。实现思路是先做一个正弦波查找表存256个点的数据然后周期性把表里的数据依次写入DAC。用DMA循环搬运可以获得非常稳定的波形CPU完全不用参与。256点正弦表的生成可以用Python提前算好import math points 256 with open(sin_table.h, w) as f: f.write(const uint16_t sin_table[256] {\n) for i in range(points): # 12位DAC取0~4095范围中点偏置2047.5 val int((math.sin(2 * math.pi * i / points) 1) * 2047.5) if i % 8 7: f.write(f{val},\n) else: f.write(f{val}, ) f.write(};\n)DAC配合定时器触发每个定时器周期搬一个数据到DAC数据寄存器。DMA配置为Circular模式内存地址指向正弦表外设地址指向DAC_DHR12R1寄存器。启动代码大致是HAL_TIM_Base_Start(htim6); HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)sin_table, 256, DAC_ALIGN_12B_R);这里用DMA搬运的同时HAL_DAC_Start_DMA内部已经使能了DAC输出。定时器每来一次更新事件DMA就搬运一个数据到DAC波形频率为f TIM_CLK / (PSC 1) / (ARR 1) / 256假设TIM_CLK 72MHzPSC 0ARR 281则定时器频率 72MHz / 282 ≈ 255kHz再除以256个点正弦波频率约997Hz。调整PSC和ARR就能在很大范围内改频率。实测下来这种方式生成的波形稳定度很高抖动比纯软件循环写DAC小了一个量级。3.5 输出波形的谐波失真来源用FFT功能看DAC输出的正弦波频谱会发现在主峰之外还有若干个较小的峰值这些就是谐波。谐波的主要来源有三个一是量化噪声。12位DAC在低电平区域的量化步长约为VREF/4095对正弦波这种连续信号来说量化误差会表现为宽带的噪声底和少量谐波。二是查找表点数太少。256点正弦表本身是对理想正弦波的近似如果输出频率较高点数不足导致的阶梯效应会产生高频谐波。要改善就增加表长度比如1024点或2048点代价是占用更多Flash空间。三是定时器抖动。DMA在定时器触发下搬运数据理论上间隔均匀但总线仲裁、中断抢占等会引入微小的时间抖动。这种抖动也会在频谱上体现为噪声升高。实际项目中我通常会在DAC输出后面加一个有源低通滤波器截止频率设在输出信号最高频率的2~3倍。这样既能滤除高频量化噪声又能保留信号本身的谐波成分不至于严重失真。4. 联调中的信号质量问题纹波、跳动和软件滤波ADC和DAC同时跑的时候问题往往不是单个模块的而是整个模拟链路的。我在一块板子上同时跑双通道ADC采集和DAC波形输出时遇到的最典型问题就是只要DAC在输出ADC的采样值就开始不定期跳动。4.1 噪声源分析一块板子上同时跑ADC和DAC噪声从哪来先把噪声来源理清楚排查才有方向。我总结了几类第一类是通过电源耦合。DAC输出状态切换时内部电阻网络和输出缓冲会对电源产生电流脉冲这个脉冲经过电源走线电阻和电感会形成电压跌落或尖峰。如果ADC的参考电压和DAC共用同一条电源走线ADC的转换结果自然就被干扰了。第二类是通过地线耦合。数字电路的开关电流比如GPIO翻转、DMA搬数据、串口发送会在地平面上产生噪声。假如模拟地和数字地没有分割或者分割后单点连接位置不合理ADC采样瞬间采样电容采到的电压就包含了地噪声差。第三类是电磁辐射耦合。DAC输出引脚和ADC输入引脚在PCB上相邻且平行走线较长时DAC引脚上的高频分量会通过寄生电容串扰到ADC输入端。这种干扰在示波器上看起来像尖峰在ADC结果上表现为不规则的跳变。4.2 一次完整的采样值跳动排查链路我拿一次实际排查过程做例子完整走一遍。这个排查顺序可以复用到大多数ADC/DAC信号质量问题中。现象ADC采集一个稳定的1.5V直流信号实测读值在1200到1280之间来回跳幅度大约80个LSB。万用表测量输入电压非常稳定问题不在信号源。排查步骤第一步检查软件配置。确认ADC时钟没有超频、采样时间不是最短档、DMA没有和别的外设冲突。这一步排除软件配置问题没有改善。第二步断开外部输入把ADC输入引脚直接短接到GND。理论上应该读到接近0的值但实测仍有跳动大约在20到60之间。这说明噪声是从ADC内部或引脚外部耦合进来的不是输入信号原始的问题。第三步把ADC输入引脚短接到VREF。读值应该在4095附近但实测在3980到4050之间跳。这个现象透露了一个关键信息即使输入引脚接到理想的参考电压转换结果也不稳定。第四步用示波器AC耦合看VREF引脚上的纹波。发现VREF上叠加了幅度约50mV、频率约30kHz的周期性纹波。这个频率和我板子上DC-DC开关电源的开关频率对上了。到这里问题基本锁定电源纹波通过VREF进入了ADC转换链路。解决方案是给VREF加二级滤波用一个10Ω电阻串联、10μF钽电容并联1μF陶瓷电容对地构成RC低通截止频率约1.6kHz把30kHz的开关纹波衰减掉。同时把DC-DC的输出经过π型滤波再给数字部分供电也让DAC的电源更干净一些。改完硬件之后再把输入引脚接到GND读值跳到0~3 LSB短接VREF读值稳定在4090~4095。问题解决。这段排查链路的要点在于先软件后硬件先数字后模拟用短接法缩小范围最后用示波器锁定噪声源。别一上来就怀疑ADC坏了或者SPI通信不对按顺序来能省很多时间。4.3 软件滤波中值、滑动平均、限幅融合硬件改好之后软件滤波依然是嵌入式采集里不能省略的一环。尤其是传感器信号本身带有随机噪声时软件滤波是成本最低、见效最快的方案。我常用的三种基础滤波各自有不同的适用场景中值滤波适合去除尖峰脉冲。一次取N个样本排序取中间值对突发性干扰非常有效但会引入较大的滞后。滑动平均适合平滑随机噪声计算量小响应快但对尖峰脉冲无能为力。限幅滤波适合抑制突变做法是如果当前值与上一次值的差超过阈值就用上一次值或线性插值代替。三者结合能对付绝大多数场景。下面是一个我常用的三级融合滤波代码先限幅再中值再滑动平均#define FILTER_N 5 // 中值滤波窗口 #define SLIDING_N 8 // 滑动平均窗口 #define LIMIT_THRESHOLD 100 // 限幅阈值 uint32_t adc_value_filtered(uint32_t new_value) { static uint32_t last_value 0; static uint32_t median_buffer[FILTER_N]; static uint32_t sliding_buffer[SLIDING_N]; static uint8_t median_index 0; static uint8_t sliding_index 0; uint32_t temp[FILTER_N]; uint32_t sum 0; // 第一步限幅 if (last_value ! 0 (new_value last_value LIMIT_THRESHOLD || new_value last_value - LIMIT_THRESHOLD)) { new_value last_value; } last_value new_value; // 第二步中值滤波 median_buffer[median_index] new_value; median_index (median_index 1) % FILTER_N; for (int i 0; i FILTER_N; i) { temp[i] median_buffer[i]; } // 冒泡排序取中间值数据量小足够快 for (int i 0; i FILTER_N - 1; i) { for (int j i 1; j FILTER_N; j) { if (temp[j] temp[i]) { uint32_t t temp[i]; temp[i] temp[j]; temp[j] t; } } } uint32_t median_value temp[FILTER_N / 2]; // 第三步滑动平均 sliding_buffer[sliding_index] median_value; sliding_index (sliding_index 1) % SLIDING_N; for (int i 0; i SLIDING_N; i) { sum sliding_buffer[i]; } return sum / SLIDING_N; }这个滤波组合的实际效果是对随机白噪声衰减约一个数量级对尖峰干扰完全抑制对真实的缓变信号延迟约2~3个采样周期。如果你做闭环控制滤波的延迟要纳入系统延迟预算中分析否则可能引发稳定性问题。4.4 信噪比和有效位数为什么12位ADC很难真的做到12位很多人在选型时只看ADC“多少位”但实际性能要看的是有效位数ENOBEffective Number of Bits。12位ADC在理想条件下信噪比约为74dB实际电路中电源噪声、参考电压噪声、PCB布局不良都会让SNR下降ENOB也就跟着降。ENOB (SNR - 1.76) / 6.02如果你的系统实测信噪比只有60dBENOB约9.7位也就是说你买了一个12位ADC实际可用精度只有10位左右。多出来的那2位被噪声吃掉了。所以热搜词里那句“20位ADC需要电源精度”——高分辨率ADC对电源纹波的要求是极为苛刻的。20位ADC的LSB电压在5V满量程时约为4.8μV如果电源纹波有1mV直接就把最小的4位吞没了。这种场景下参考电压源和电源滤波器成了系统里最重要的器件。对于STM32内置的12位ADC我的经验法则是如果PCB布局和电源处理得当可以把有效位数做到10.5位左右如果什么都不做就拿杜邦线加面包板飞线可能只有8到9位。别抱怨芯片不好先看看自己给的电源条件配不配得上这个精度。4.5 测量工具本身也会骗人调试过程中还有一类问题来自测量手段本身。示波器探头的地线夹如果用鳄鱼夹加十几厘米长的线探头回路会形成一个大环天线几十MHz的共模噪声会被耦合进测量结果让你误以为DAC输出真有那么多噪声。我做过一个对比实验同样的DAC方波输出用短地线弹簧探头测振铃幅度约5%换成长地线夹测振铃幅度变成15%。正确的测量方式是使用探头自带的短地线弹簧或者用接地环。测量ADC参考电压纹波时探头应直接接触VREF引脚旁边的测试点而不是隔着一根长线去接GND。测量高频波形时还可以在探头尖端并联一个几十pF的小电容形成高频补偿。这个细节虽然看起来很小但在排查信号质量时如果误把测量工件的伪影当成真实问题来调电路会浪费大量时间。5. 玩法延伸定时器触发、任意波形发生器和ADC-DAC闭环基础功能跑通之后这个系统能玩的花样就多了。这里分享三个我实际做过、并且觉得很有价值的延伸方向每一个都能让这套ADCDAC组合发挥更大的威力。5.1 用定时器触发ADC获得精确采样率在前面的DMA连续采样方案里ADC是“自由运行”的采样间隔由ADC转换时间决定无法做到严格等间隔。但很多算法比如FFT、数字滤波要求采样间隔必须严格均匀这时就要用定时器触发ADC。在CubeMX里把ADC的Trigger Source触发源配置为某个定时器的更新事件比如TIM2 Update Event然后在定时器配置里把更新频率设为你想要的采样率。启动顺序是先启动定时器再启动ADC的DMA采集HAL_TIM_Base_Start(htim2); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 4);定时器每产生一次更新事件ADC就启动一次转换转换完成后由DMA搬走数据。这样采样间隔完全由定时器决定不受主循环代码执行时间影响。我在做音频采集时把采样率精确设到48kHz数据直接给FFT做频谱分析效果比自由运行方式好得多。5.2 DACDMA定时器生成任意波形正弦波查表法的本质是把“波形形状”存成一段数据再按固定的时间间隔送出去。这个方法对任意波形都适用。只要把正弦表换成任意形状的表就能输出任意波形。比如要生成三角波可以用一个计数器在0到4095之间循环递增递减要生成自定义的任意波形可以先用Python把波形数据离线算好生成一个数组烧录到Flash里。如果需要输出连续可变的波形比如电子琴里的音符只要把表的内容指向不同音色的数据就能实现简单的波表合成器。在MCU Flash够用的情况下我甚至试过把一段真实的音频数据变成数组用DAC循环播放。44.1kHz采样率、8位精度、10秒音频只要约440KB FlashF407完全放得下。把音频数据填进DMA环形缓冲区再用定时器触发出来的声音虽然离Hi-Fi还有距离但作为自制的“单片机录音机”已经很有意思了。5.3 ADCDAC闭环从测量到控制的最后一公里把ADC和DAC放在同一个系统里最典型的应用就是闭环控制。一个简单的例子是自动增益控制AGCADC采集信号幅度DAC输出一个增益控制电压给模拟可变增益放大器保持ADC输入信号幅度基本恒定。我在做的一个简易信号发生器项目就用到了类似的闭环DAC输出一个幅度固定的正弦波ADC监测实际输出电压根据测量结果修正DAC的增益系数。这样即使用户负载发生变化导致输出衰减系统也能自动补回来整个过程相当于做了一次“数字反馈模拟输出”。这个闭环的实时性要求取决于控制带宽。低速应用比如几十Hz以下可以直接在主循环里完成高速应用比如音频频段的AGC就需要把反馈算法搬进定时器中断或DMA回调里执行。这也是为什么在前面几章一直强调DMA和定时器触发——它们不仅让采样更稳也给了闭环控制更干净的时间基准。5.4 代码工程化的一点经验延伸玩法多了之后代码组织就变得重要了。我踩过的几个坑可以供参考第一个坑是DMA缓冲区地址对齐。部分DMA外设要求缓冲区地址按半字对齐如果编译器把数组分配到了一个未对齐的地址DMA搬运会直接错误或性能下降。解决方法是给数组加上__attribute__((aligned(4)))或者用ALIGN_32BYTES宏。第二个坑是CubeMX重新生成代码时把用户手写的代码覆盖了。后来我遵循两个约定一是用户代码必须放在CubeMX保留的USER CODE BEGIN和USER CODE END注释块之间二是所有DMA回调函数里的业务逻辑只做标志位通知不直接在回调里做耗时处理。第三个坑是不要在中断或DMA回调里调用HAL_DAC_SetValue这类函数。DMA回调本身是在DMA中断里执行的如果你在回调里又操作DAC外设可能会触发优先级反转或死锁。正确做法是回调里只置标志位主循环里再根据标志位去设置DAC。第四个坑是浮点运算在无FPU的F103上开销很大。如果一个采样周期内要做浮点滤波或浮点校准计算主循环可能跟不上采样速率。解决办法是全部用定点运算或者换带FPU的F4系列或者把算法分解到多个采样周期里分时完成。这个优化思路在做高采样率实时处理时尤其重要。关于这套系统的最终调试心得做ADC采集和DAC输出这个组合项目整体上给我最大的感受是功能跑通不难难的是把信号质量做到位。你在数据手册上看到的12位精度、74dB信噪比全部是理想电源和理想布局下才能达到的。实际做出来的有效位数完全取决于你对参考电压、电源滤波、地线分割和采样时间的处理。在一个完整的ADC/DAC项目里我最后整理出来的优先级是先把电源和地做好再把参考电压独立干净然后配好RC滤波和运放跟随最后才是代码和软件滤波。代码里面优先把DMA和定时器用好让硬件自己干活CPU只在数据准备好的时候去做有意义的事——比如控制算法、数据存储或者人机交互。反过来如果一开始就整天纠结软件滤波算法却不去检查VREF引脚上的纹波和地线布局那相当于在噪声和问题的源头还没堵住的情况下试图在结果端做复杂的补救。两者不是完全互相替代的方案硬件底子打好了软件滤波才真的有效软件滤波用好了硬件上的小瑕疵也能被掩盖一部分。调试工具的重要性也不亚于设计本身。一台带宽够的示波器能让你直接看到过冲、振铃、纹波和串扰省去大量猜测时间。如果没有示波器哪怕只用万用表多测几个点也比盲调代码要强得多。这套ADCDAC系统的后续扩展空间还很大。串口或者以太网把采集数据传到上位机做分析和显示PID控制算法把ADC测量值和DAC输出执行器连成闭环甚至多个STM32之间通过ADC采样和DAC输出互相通信组成一个模拟域的信号链路——每一种玩法都值得单独开一个项目来折腾。在这个基础上剩下的就交给你的具体应用场景决定了。本文还有配套的精品资源点击获取

相关新闻