
简介一份面向TI 28346浮点DSP的ADC采样处理示例工程适合嵌入式与数字信号处理开发者用于掌握片内ADC模块配置、采样率设定、中断服务及数字滤波等关键流程。资源共93个文件压缩包仅421KB包含DSP2834x外设头文件、C源程序、汇编启动文件、CCS工程配置及编译产物obj/out/map结构完整。已有256人学习下载。通过工程可学习ADC与XINTF接口协同、PIE中断、CPU定时器、DMA在实时采集中的配合并参考链接命令文件与内存分配快速搭建CCS开发环境。代码涵盖库函数调用与寄存器级配置涉及采样保持、触发方式、结果排序及FIR/IIR滤波兼顾转换时间、噪声抑制与算法效率适合有一定DSP基础、希望深入ADC全流程的开发者。1. ADC 采样到 DSP 程序整条采集链路上最容易算错的一环一个很常见的场景示波器上看到传感器电压稳定在 1V可单片机 ADC 读回来的码值算成电压只有 0.93V而且还在缓慢漂移。DSP 程序里已经写了滑动平均毛刺依然存在改滤波窗口也无济于事。问题往往出在 ADC 的前端参数上而不是算法本身。所谓 ADC_dsp程序不是简单地把「读 ADC」和「跑 DSP 算法」拼在一起而是要把采样周期选择、多通道采集、码值换算、数字滤波、误差校准和频域验证连成一条完整链路。任何一个环节参数失配后续 DSP 处理都会放大偏差。本文面向用 MCU 或 DSP 处理模拟信号的嵌入式工程师从采样周期的时域边界讲起落到 STM32 上的 DMA 采集、滤波函数写法、漂移校准最后给出一套可复现的 FFT 信噪比验证方法。每一步都给出参数和可抄的代码。2. ADC 采样周期与数据特征DSP 程序必须知道的时域边界2.1 采样周期、采样保持与量化噪声决定 DSP 能「看到」什么SAR ADC 的完整转换过程分为采样阶段和转换阶段。采样阶段内部开关闭合采样电容充电到输入电压转换阶段开关断开逐次逼近逻辑把电容上的电压转成数字码。DSP 程序拿到的是转换阶段结束后的码值但如果采样阶段时间不够采样电容根本没充到真实电压读到的数自然偏低。这就是「单片机 ADC 输入口电压为 1V采样得到的值是多少」这类问题的常见陷阱按 12 位、3.3V 参考电压算应该是 1241 左右实际可能差出几十个 LSB原因多半是信号源内阻偏大而 SamplingTime 又设得太短。量化噪声是第二个没法在 DSP 侧回避的量。理想 N 位 ADC 在满量程正弦输入下的信噪比为SNR 6.02N 1.76 dB12 位对应约 74dB16 位约 98dB。这个式子说明一件事N 位分辨率并不等于 N 位有效精度DSP 程序里那些「读出来 12 位、算完还能用 12 位」的假设要打一个问号。下表列出常见位数的理想边界便于评估 DSP 算法最终能压到多少噪声ADC 位数理想 SNR满量程正弦下的理论有效位数10 位62 dB10 bit12 位74 dB12 bit14 位86 dB14 bit16 位98 dB16 bit实际的 SNR 还要扣掉时钟抖动、电源噪声、参考电压噪声DSP 侧的滤波最多只能逼近这个上限不可能超过它。2.2 SAR ADC 采样周期配置速查采样时间调到多少才合适以 STM32G4 系列的 ADC 为例配置单个通道采样时间的 HAL 代码是这样ADC_ChannelConfTypeDef ch {0}; ch.Channel ADC_CHANNEL_3; /* 传感器信号接在 ADC 输入引脚 */ ch.SamplingTime ADC_SAMPLINGTIME_640CYCLES_5; /* 采样阶段持续 640.5 个 ADC 时钟 */ ch.SingleDiff ADC_SINGLE_ENDED; /* 单端输入地与 ADC 共地 */ HAL_ADC_ConfigChannel(hadc1, ch, 0);640CYCLES_5是 ADC 时钟周期数不是微秒。如果 ADC 时钟是 60MHz这一段采样时间大约 10.7µs对绝大多数高阻信号源都足够。实际项目中我一般按信号源内阻选采样周期参考对照如下信号源特征建议采样周期说明运放输出、内阻小于 100Ω2.5 ~ 6.5 cycles吞吐率优先读数稳定传感器直接接引脚、内阻 1~10kΩ12.5 ~ 47.5 cycles需要中间档兼顾速度和精度电阻分压网络、内阻 10kΩ 以上640.5 cycles采样电容充电时间要足够采样周期拉长的代价是吞吐率下降。DSP 程序如果同时采多路信号一般用 DMA 扫描批量搬运而不是在中断里逐个读否则 CPU 会一直花在等待转换上处理实时算法的时间就不够了。2.3 信号调理与抗混叠进 DSP 前的最后一道模拟闸混叠是数字信号处理里最绕不过去的问题高于fs/2的频率分量会被折叠回低频DSP 侧加任何数字滤波都滤不回来。采样率定了之后抗混叠只能靠 ADC 前面的模拟 RC 完成。常见做法是 R 取信号源内阻加一个限流电阻C 取 ADC 采样电容的十倍以上。比如信号源是 10kΩ 分压网络串联 200Ω 后接 10nF 电容截止频率大约在几百 kHz能把大多数板级噪声压住同时不影响低频信号。如果前端用的是 Σ-Δ ADCRC 设计更要谨慎。Σ-Δ 调制器输入级对电容做高频充放电RC 网络既承担抗混叠又负责缓冲电荷反冲取值过小会让输入驱动电路不稳读数出现周期性跳变。提示很多工程师习惯把「ADC 读数不准」直接丢给 DSP 滤波解决但采样保持时间不足和混叠这两类误差数字域无能为力只能在模拟前端修正。3. 将 ADC 数据送进 DSP 管线STM32 多通道 DMA 最小实现3.1 配置 ADC 多通道 DMA 采集的关键字段做实时采集时我一般不用「每次转换进一次中断」的方式而是让 ADC 与 DMA 配合工作。规则组扫描模式下多个通道按 Rank 顺序依次转换转换结果统一放进同一个数据寄存器DMA 在每次转换完成后把结果搬到内存CPU 只在整批数据收齐后处理一次。关键初始化字段如下以 STM32G4 的 HAL 为例hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; /* ADC 时钟从 APB 分频得到 */ hadc1.Init.Resolution ADC_RESOLUTION_12B; /* 12 位输出码值范围 0~4095 */ hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; /* 规则组扫描多个通道 */ hadc1.Init.ContinuousConvMode ENABLE; /* 连续转换配合 DMA 批量搬运 */ hadc1.Init.NbrOfConversion 3; /* 通道数必须与 DMA 数组长度一致 */ HAL_ADC_Init(hadc1);ClockPrescaler决定 ADC 采样周期对应的实际时间Resolution决定满量程码值NbrOfConversion则直接影响 DMA 搬运次数。这三个字段如果和实际硬件不匹配采集到的数组会整体错位或永远只有第一个通道有数据。通道配置和 DMA 启动代码如下ADC_ChannelConfTypeDef ch {0}; ch.Channel ADC_CHANNEL_0; /* 第 1 个转换序列对应 adc_buf[0] */ ch.Rank ADC_REGULAR_RANK_1; ch.SamplingTime ADC_SAMPLINGTIME_12CYCLES_5; HAL_ADC_ConfigChannel(hadc1, ch, 0); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 3); /* 连续搬运 3 个通道结果 */adc_buf建议声明为uint32_t数组因为 HAL 的 DMA 接口要求地址按 32 位对齐而 ADC 结果寄存器本身是 32 位容器12 位数据放在低 16 位。转换完成后DSP 处理逻辑放在回调函数里void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { process_adc_data(adc_buf); /* 整批数据就绪交给 DSP 算法 */ } }规则组扫描模式对应普通多通道轮询场景。如果需要更高优先级的单点采样可以用注入通道注入通道不占用扫描序列适合在电机控制里按特定时刻插入一次采样。3.2 把 ADC 码值换算成电压参考电压与零点不能想当然DSP 程序里最常见的换算写法是float v_ref 3.3f; /* 万用表实测 VREF 电压不要直接用标称值 */ float voltage (float)adc_value * v_ref / 4095.0f;这里有两个隐含假设一是参考电压严格等于 3.3V二是零点严格为 0。实际板上 VREF 可能差 10~30mV对应 12 位下 10 多个 LSB 的误差。如果只是做 FFT 频谱分析码值可以直接用但换算成温度、电流这些物理量时参考电压偏差会直接进入最终结果。STM32 内部一般带 VREFINT 通道可以在运行时反推实际供电电压这部分放到第 4 章校准流程里展开。3.3 C 语言 ADC 值滤波函数滑动平均、中值与一阶 IIR 怎么选工程项目里最常用的三个滤波函数按场景分开写。滑动平均适合平稳信号对白噪声有效代码如下/* 环形缓冲滑动平均每次只更新一个点避免全量求和 */ float adc_moving_average(uint16_t latest, uint16_t *buf, uint8_t len) { static uint8_t idx 0; static uint32_t sum 0; if (idx len) idx 0; sum - buf[idx]; /* 移出最旧值 */ buf[idx] latest; /* 新值覆盖旧值 */ sum latest; /* 累加新值 */ idx; return (float)sum / len; }中值滤波对脉冲干扰效果好适合工业现场的毛刺/* 冒泡排序取中值n 一般为 5 或 7窗口大了延迟明显 */ uint16_t adc_median_filter(uint16_t *array, uint8_t n) { uint8_t i, j; for (i 0; i n - 1; i) for (j i 1; j n; j) if (array[j] array[i]) { uint16_t t array[i]; array[i] array[j]; array[j] t; } return array[n / 2]; }一阶 IIR 低通计算量最小适合放进 DSP 实时环路/* alpha 约等于 1 / (1 2 * pi * fc * Ts)fc 为截止频率Ts 为采样周期 */ float iir_lp(float x, float *y_prev, float alpha) { *y_prev alpha * (x - *y_prev); return *y_prev; }滤波器类型适合噪声代价推荐场景滑动平均白噪声信号边缘变缓温度、压力缓变信号中值滤波脉冲、毛刺延迟随窗口增长电源电压监测、继电器动作瞬间一阶 IIR宽带噪声相位非线性电流环、速度环实时处理提示滤波器不建议叠太多层。实际项目中「一阶 IIR 滑动平均」已经偏多两层以上基本是在掩盖前端问题不如回头检查采样周期和参考电压。4. 处理 ADC 数据漂移与校准DSP 程序调参时真正要盯的参数4.1 ADC 数据漂移的来源与两点校准流程ADC 数据漂移最常见的几个来源温度变化引起增益漂移、参考电压随负载变化、采样电容电荷注入、PCB 受潮导致漏电流。排查时先把输入引脚接地连续读一个小时看码值是否有固定偏置再把输入引脚接到一个精密电压源看满量程附近是否偏小。两种现象对应的是零点偏移和增益误差。常见做法是两点校准把「满量程比例换算」改成「实测斜率和截距」void adc_calibrate(float raw_lo, float raw_hi, float v_lo, float v_hi, float *gain, float *offset) { *gain (v_hi - v_lo) / (raw_hi - raw_lo); /* 每 LSB 对应的实际电压 */ *offset v_lo - (*gain) * raw_lo; /* 零点偏移电压 */ } float adc_to_voltage(uint16_t raw, float gain, float offset) { return raw * gain offset; }校准时用两个已知电压一个是接近零点的 0.1V一个是接近满量程的 3.0V。raw_lo 和 raw_hi 是这两个电压下的实际读数v_lo 和 v_hi 是精密万用表测到的电压。校准完成后DSP 程序里的所有换算统一走adc_to_voltage而不是每次重新计算。针对参考电压漂移STM32 内部有 VREFINT 通道可以在运行时反推实际 VDDAuint16_t vrefint adc_read_value(ADC_CHANNEL_VREFINT); /* 内部 1.2V 参考通道 */ float vdda 1.2f * 4095.0f / (float)vrefint; /* 反推实际供电电压 */如果 vrefint 读数大于理论值说明 VDDA 实际偏低后续换算应该使用算出来的 vdda 而不是标称 3.3V。这个方法可以放进主循环定期执行代价是每个周期多一次 ADC 转换对 DSP 实时性影响很小。另外如果平台是 TI 的 C2000 系列 DSP结果寄存器的读取有一个坑。部分器件上 CLA 读取 ADC 结果寄存器时读到的可能是尚未应用 ADCOFFTRIM 的原始码值直接用来做换算会带固定偏差。常见做法是核对参考手册中「CLA 访问结果寄存器」的小节确认初始化时是否需要对读回路径做处理。验证方法很简单触发一次采样比较 CPU 读取和 CLA 读取的结果差值为固定常数说明问题存在。4.2 用 FFT 验证 ADC 信噪比与有效位数DSP 程序里验证 ADC 链路质量最直接的方法是 FFT。以 CMSIS-DSP 库为例256 点实数 FFT 的骨架如下arm_rfft_fast_instance_f32 fft; arm_rfft_fast_init_f32(fft, 256); /* 初始化 256 点 FFT */ arm_rfft_fast_f32(fft, input, output, 0); /* 0 表示正变换 */ arm_cmplx_mag_f32(output, mag, 128); /* 取各频率 bin 的幅值 */频率分辨率为fs / N。例如采样率 50kHz、N256 时每个 bin 对应约 195Hz。FFT 之前加汉宁窗可以抑制频谱泄漏for (uint16_t n 0; n 256; n) input[n] * 0.5f * (1.0f - cosf(2.0f * PI * n / 255.0f));验证步骤信号发生器输出 1kHz 正弦经运放调理后接到 ADC 输入。DSP 程序连续采样 256 点完成 FFT。记录 1kHz 对应 bin 的幅值为信号其余 bin 的 RMS 为噪声。计算 SNR20 * log10(信号RMS / 噪声RMS)。计算有效位数ENOB (SNR - 1.76) / 6.02。如果算出来的 ENOB 比理论值低 3 位以上优先检查采样周期是否太短其次查参考电压纹波。FFT 结果里如果 50Hz 附近有尖峰多半是电源工频耦合如果噪底整体抬高且无明显尖峰大概率是采样时钟抖动或前端电阻热噪声。4.3 电源噪声、时钟抖动与 PCB 布局三个动作抠回 ADC 信噪比ADC 和 DAC 混合电路里电源噪声和时钟抖动对 SNR 的影响经常被低估。PCB 布局上有三个动作优先级最高模拟电源和参考电压单独滤波。ADC 的 VDDA 和 VREF 不要直接接到数字电源网络用磁珠加 RC 隔离电容靠近 ADC 引脚放置至少一个 1µF 陶瓷电容加一个 100nF 高频去耦。采样时钟和高速数字线远离模拟输入。DSP 的时钟信号如果贴着 ADC 输入走线时钟抖动会直接叠加到采样时刻上输入信号频率越高SNR 损失越大。必要时在时钟线两侧包地。模拟地与数字地单点连接。不要在 ADC 正下方把地平面切开否则回流电流绕路会产生压差。数字信号线跨越地分割槽时噪声耦合会明显增大。如果前端是 Σ-Δ ADCRC 网络的截止频率必须低于调制器开关频率的一半否则调制器输入级的电荷反冲会让 RC 充放电波形失真读出的数据呈周期性跳变。DSP 侧的数字滤波解决不了这个模拟问题。5. 把 ADC DSP 程序收稳一套能复现的验证流程ADC 采集链路改动后我用一套固定流程验证按顺序执行能快速定位问题。第一步用精密电压源分别给 0.1V、1.5V、3.0V读 DSP 程序换算后的电压值。三个点的误差都小于 1 LSB 说明增益和零点校准到位只有低端偏说明零点有问题只有高端偏说明增益有问题。第二步把 ADC 输入接到一个精密电阻分压网络保持恒温每小时记录一次均值连续 8 小时观察漂移范围是否超过 2 LSB。超过的话优先检查参考电压和 PCB 漏电。第三步验证采样时刻的确定性在主循环里翻转一个 GPIO用示波器同时看 GPIO 和 ADC 转换完成事件测量从触发采样到 DMA 回调之间的时延是否固定。在电机控制和数字电源这类闭环应用里采样时刻与 PWM 中心对齐比采样速度更重要。STM32 高级定时器可以配置成中心对齐模式并用 PWM 触发 ADC 启动采样DSP 程序在固定时刻读取电流值保证每个开关周期采到的相位点一致。如果直接在中断里「有空就采」不同周期里采到的电流点位置不同PI 参数调得再好也会振荡。最后一个常用技巧在 DSP 代码里给每一批 ADC 数据打上时间戳记录 DMA 完成时刻的系统节拍值后续所有滤波和 FFT 操作都以这个时间戳为基准而不是在回调里临时读取节拍。这样能排除中断嵌套造成的延迟波动让整个 ADC 采样链路的行为变成确定性的后续调滤波参数和闭环参数才有效果。本文还有配套的精品资源点击获取