简介:一份面向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 位对应约 74dB,16 位约 98dB。这个式子说明一件事:N 位分辨率并不等于 N 位有效精度,DSP 程序里那些「读出来 12 位、算完还能用 12 位」的假设,要打一个问号。
下表列出常见位数的理想边界,便于评估 DSP 算法最终能压到多少噪声:
| ADC 位数 | 理想 SNR | 满量程正弦下的理论有效位数 |
|---|---|---|
| 10 位 | 62 dB | 10 bit |
| 12 位 | 74 dB | 12 bit |
| 14 位 | 86 dB | 14 bit |
| 16 位 | 98 dB | 16 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,能把大多数板级噪声压住,同时不影响低频信号。
如果前端用的是 Σ-Δ ADC,RC 设计更要谨慎。Σ-Δ 调制器输入级对电容做高频充放电,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 通道,可以在运行时反推实际 VDDA:
uint16_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、N=256 时,每个 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 为噪声。
- 计算 SNR:
20 * 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 正下方把地平面切开,否则回流电流绕路会产生压差。数字信号线跨越地分割槽时,噪声耦合会明显增大。
如果前端是 Σ-Δ ADC,RC 网络的截止频率必须低于调制器开关频率的一半,否则调制器输入级的电荷反冲会让 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 采样链路的行为变成确定性的,后续调滤波参数和闭环参数才有效果。
本文还有配套的精品资源,点击获取