
简介面向嵌入式音频开发者与电子工程学习者这份打包的C源文件提供了在微控制器上实现ADC采集与PDM编码的参考代码。adc.c内不仅包含ADC外设的初始化、采样率与分辨率配置、转换完成中断服务还涵盖从连续模拟信号到脉冲密度调制信号的转换与输出逻辑可作为低功耗音频如无线耳机、智能音箱前端采集模块的设计模板。资源包仅含1个c文件大小仅2KB代码紧凑适合快速移植到个人项目中。目前已有128人学习浏览适合具有基础单片机经验、希望深入理解采样量化及PDM编码细节的开发者下载参考。通过阅读该源码可直观掌握从寄存器操作到PDM流生成的关键流程节省自行查阅手册与反复调试的时间。1. 从 adc.c 看 ADC 与 PDM 的隐蔽耦合拿到adc.rar里那份adc.c时我原以为只是一个常规的 ADC 驱动打开才发现里面同时出现了 PDM 脉冲密度调制的生成与解码逻辑。很多做音频嵌入式的同事会把 ADC 和 PDM 当成两条独立链路——前端采样归前端数字麦克风归数字麦克风——但实际上PDM 本质上就是 ΣΔ ADC 的中间产物只是被单独引出来作为传输编码罢了。想透彻理解这份源码得同时吃透两件事ADC 的采样与量化过程如何决定信号质量以及 PDM 为什么能靠脉冲密度而不是脉冲宽度来表达幅度。适合正在做低功耗音频采集、数字麦克风接口或传感器前端的人参考新手能顺着代码理清从模拟到数字的完整路径熟手也能在寄存器配置和滤波策略上对照自己的实现。2. ADC 采样链路设计从采样定理到寄存器配置2.1 采样、量化与编码的三级拆解adc.c里最先出现的是一组对 ADC 硬件初始化的操作包括使能时钟、配置采样时间、设置分辨率和触发源。要理解这些寄存器为什么这样配得先回到 ADC 最底层的三步采样Sample、量化Quantize、编码Encode。采样是把连续时间信号在时间轴上打点。奈奎斯特定理要求采样频率至少为信号最高频率的两倍否则频谱混叠高频分量会折叠进低频区这是所有后续处理失真的根源。量化把每个采样点的电压映射到有限的数字等级n 位 ADC 有 (2^n) 个等级LSB 对应的电压就是满量程除以 (2^n)。编码则是把量化等级转成二进制补码或偏移二进制码供 CPU 或 DMA 读取。在低功耗音频场景下很多工程师直接选用 16 位、采样率 48 kHz 的配置期望达到 96 dB 的理想信噪比。但理想量化信噪比公式 (SNR6.02n1.76) dB 只对满幅正弦波成立实际信号幅度只要低 6 dB有效位数就少 1 位。这就是为什么adc.c中会有专门的增益配置段——不是随手写的而是为了把信号幅度顶到接近满量程减少量化噪声的相对占比。void adc_init(ADC_TypeDef *ADCx, uint32_t ch_mask, uint32_t sample_time) { ADC_InitTypeDef init {0}; init.DataAlign ADC_DATAALIGN_RIGHT; // 右对齐低 16 位有效 init.ScanConvMode ENABLE; // 扫描模式用于多通道 init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T0_TRGO; init.NbrOfConversion count_bits(ch_mask); // 转换通道数 init.DiscontinuousConvMode DISABLE; // 关闭不连续模式 HAL_ADC_Init(ADCx, init); for (int i 0; i 12; i) { if (ch_mask (1U i)) { ADC_ChannelConfTypeDef ch_conf {0}; ch_conf.Channel i; ch_conf.Rank ADC_REGULAR_RANK_1 i; ch_conf.SamplingTime sample_time; // 单位ADC 时钟周期 ch_conf.OffsetNumber ADC_OFFSET_NONE; HAL_ADC_ConfigChannel(ADCx, ch_conf); } } HAL_ADC_Start_DMA(ADCx, (uint32_t *)adc_buf, count_bits(ch_mask) * DMA_BUF_LEN); }这段代码里SamplingTime是采样周期即 ADC 内部采样保持电容充电的时间。这个参数直接决定输入阻抗匹配源阻抗越高需要的采样时间越长。ScanConvMode开启后ADC 会按 Rank 顺序依次转换各通道配合 DMA 可以做到多个通道连续采集而 CPU 不介入。ExternalTrigConv设置为定时器触发是为了让 ADC 采样时刻与 PWM 输出对齐这在后续 PDM 解码时非常有用因为 PDM 的位时钟与采样时钟需要保持确定的相位关系。2.2 SAR ADC 与 ΣΔ ADC 的取舍adc.c中如果直接配置的是逐次逼近型SARADC那么采样率和分辨率之间存在明确折中。SAR ADC 每个转换周期需要 n 个时钟完成逐位逼近12 位精度至少 12 个时钟周期加上采样阶段实际转换速率受限于时钟频率。而 ΣΔ ADC 用远高于奈奎斯特频率的过采样率换取分辨率再用数字抽取滤波器把带外噪声滤掉换来的是高精度但低带宽。PDM 麦克风内部就是一个 1 位 ΣΔ ADC它输出的是持续的高速 0/1 流脉冲密度与模拟输入幅度成正比。这和adc.c里的传统 ADC 并不冲突——很多低功耗音频系统用外部 PDM 麦克风采集语音用内置 SAR ADC 采集电池电压和环境温度两者各司其职。但如果你想用普通 ADC 实现类 PDM 编码原理上也可以对同一信号高倍过采样输出 1 位量化结果这就是最简单的一阶 ΣΔ 调制器。参数SAR ADCΣΔ ADC分辨率8~16 位16~24 位采样率可达 MSPS 级通常低于 100 kSPS过采样无有OSR 通常 64~256延迟单次转换结束即出结果抽取滤波引入群延迟适用场景多通道扫描、高速采样音频、精密测量在adc.c的调试过程中如果发现采样值持续跳动先检查SamplingTime是否过短其次是电源引脚上的去耦电容是否足够。SAR ADC 的输入电容在采样瞬间会形成一个瞬态电流尖峰如果源阻抗过大这个尖峰无法在采样窗口内稳定读数就会偏大或抖动。3. PDM 编码与解码从脉冲密度到 PCM 数据3.1 PDM 为什么不需要高精度 DACPDM 的直观理解是用固定频率的脉冲序列通过脉冲密度的高低来表达模拟信号的幅度。一个正弦波的正半周对应高密度脉冲负半周对应低密度脉冲甚至全是 0。和 PWM 不同PDM 的脉冲宽度恒定变化的是单位时间内的脉冲数量。这个特性带来一个直接好处——传输链路里不需要精密的 DAC只要用简单的低通滤波器就能恢复原始模拟信号。ΣΔ 调制器是 PDM 的核心。一阶 ΣΔ 调制器的离散时间表达式为 (y[n] x[n] e[n] - e[n-1])其中 (e[n]) 是量化误差。这里最关键的是量化误差被做了差分处理低频段的噪声被衰减高频段的噪声被放大这就是噪声整形。后面接一个低通数字滤波器把高频噪声滤掉剩下的就是高信噪比的基带信号。adc.c中实现 PDM 生成时用的正是这个原理只是量化器退化为 1 位比较器。int16_t sigma_delta_modulate(float input, float *integrator, int order) { float v input - (*integrator 0 ? 1.0f : -1.0f); *integrator v; if (order 2) { // 二阶积分器进一步整形噪声 static float integrator2 0.0f; integrator2 (*integrator 0 ? 1.0f : -1.0f); *integrator input - (integrator2 0 ? 1.0f : -1.0f); } return (*integrator 0) ? 1 : 0; }这段代码实现了简单的一阶和二阶噪声整形。input是归一化到 [-1, 1] 的浮点采样值integrator是状态变量。每个时钟周期输出一个 1 或 0这个比特流经过低通滤波后可以还原出原始波形的近似值。一阶结构实现简单但成形后的噪声谱在低频段以 20 dB/decade 上升二阶结构把斜率提升到 40 dB/decade低频噪声进一步压低代价是稳定性变差输入信号过大会引发振荡。3.2 PDM 解码CIC 滤波器与抽取从 PDM 麦克风读到的原始比特流不能直接当音频用它每秒几兆比特得经过抽取滤波器降采样到 48 kHz 并转换成 16 位 PCM。最常用的抽取滤波器是级联积分梳状CIC滤波器它不需要乘法器只用加法器和延迟线非常适合资源受限的 MCU。CIC 滤波器由两部分组成积分器工作在过采样时钟域梳状滤波器工作在降采样后的时钟域。抽取因子 R 等于过采样率微分延迟 M 通常取 1 或 2级数 N 与调制器阶数相关。三阶 CIC 配合一阶 ΣΔ 调制器的效果一般最好搭配二阶或三阶调制器否则通带内的噪声整形不够低频信噪比偏低。void cic_decimator(uint8_t *pdm_in, int16_t *pcm_out, uint32_t n_samples, uint32_t decim_factor) { int32_t integrator1 0, integrator2 0, integrator3 0; int32_t comb1 0, comb2 0, comb3 0; for (uint32_t i 0; i n_samples; i) { // 积分器在过采样率下累加 integrator1 (pdm_in[i] ? 1 : -1); integrator2 integrator1; integrator3 integrator2; if ((i 1) % decim_factor 0) { // 抽取后进入梳状部分 int32_t diff1 integrator3 - comb1; comb1 integrator3; int32_t diff2 diff1 - comb2; comb2 diff1; int32_t diff3 diff2 - comb3; comb3 diff2; // 归一化后输出 16 位 PCM pcm_out[(i 1) / decim_factor - 1] (int16_t)(diff3 / (int32_t)pow(decim_factor, 3)); } } }这段三阶 CIC 代码中integrator系列的累加会持续增长所以必须用足够宽的整型变量否则溢出会让输出产生周期性毛刺。输出端的除法用于归一化增益系数理论上等于 (R^N)实际工程中会用移位代替除法因为抽取因子往往是 2 的幂。CIC 的通带滚降比较明显如果对频响平坦度有要求后面还需要级联一个 FIR 补偿滤波器但这会显著增加计算量低功耗设备上经常省略。PDM 解码常见的坑是比特流极性接反导致输出波形上下颠倒。听起来不一定会察觉但在频谱上表现为直流分量异常。排查方法很简单给麦克风输入已知的正弦波看解码后的数据和原始数据的相关系数如果接近 -1说明极性需要取反。4. ADC 值滤波与工程级落地的隐藏细节4.1 C 语言实现 ADC 值滤波的三种方案adc.c里 ADC 转换结果通常不是直接可用的——无论是 SAR ADC 的器件噪声还是 PDM 解调后的残余高频分量都需要先经过数字滤波。开发板上电后 ADC 读数抖动超过十几个 LSB 是常态不做处理直接拿去控制逻辑会影响判断阈值。常见的滤波手段有滑动平均、中值滤波和限幅滤波各有各的适用条件。滑动平均适合抑制随机白噪声但会引入与窗口长度成正比的相位延迟这在实时控制回路上会造成稳定性问题。中值滤波擅长剔除离群的毛刺脉冲但对连续平稳噪声的改善有限。限幅滤波则依赖经验阈值适合已知信号变化率的场景比如温度传感器每秒最多上升几度超过阈值就认为是尖峰干扰。int16_t adc_moving_average(int16_t new_sample) { static int16_t buf[16] {0}; static uint8_t idx 0; static int32_t sum 0; sum - buf[idx]; buf[idx] new_sample; sum buf[idx]; idx (idx 1) 0x0F; // 16 点环形缓冲按位与取模 return (int16_t)(sum 4); // 等价于除以 16 } int16_t adc_median_filter(int16_t *samples, uint8_t len) { uint8_t i, j; int16_t tmp; for (i 0; i len - 1; i) { for (j i 1; j len; j) { if (samples[j] samples[i]) { tmp samples[i]; samples[i] samples[j]; samples[j] tmp; } } } return samples[len / 2]; // 冒泡排序后取中值 }滑动平均的窗口长度需要根据采样率和信号带宽来选。假设 ADC 采样率 1 kHz信号最高频率 10 Hz那么 50 个点的窗口可以把 10 Hz 以上的分量压掉约 20 dB但相位延迟会达到 25 ms。用于按键检测这类低速信号没问题用于电流环就不行。中值滤波的窗口一般取 3、5、7奇数保证有明确的中位点。代码里我故意只贴了冒泡排序版本实际项目里数据量大时可以换成快速选择算法复杂度从 O(n²) 降到 O(n)。adc_data_drift是很多工程师头疼的问题——ADC 数据漂移。排除电源和基准源温漂因素后最常见的原因是 ADC 参考电压引脚上的噪声。参考电压VREF稍有波动转换结果就会跟着偏。对策是把 VREF 引脚连接到独立的线性稳压器或高精度基准源并在靠近引脚处放置 1 μF 和 0.1 μF 的去耦电容。adc.c里如果没有这段配置建议自行加上。4.2 PCB 布局时钟抖动与电源噪声的规避回到热搜词里那个值得展开的话题——ADC/DAC 电路设计中 PCB 布局对性能的影响。时钟抖动是 ADC 信噪比的大敌。对于满幅高频输入信号抖动引起的信噪比恶化量约为 (SNR_{jitter}20\log_{10}(2\pi f_{in} t_{jitter}))意味着输入频率每提升 10 倍同样的抖动会让 SNR 恶化 20 dB。PDM 麦克风的位时钟尤其敏感因为它是 1 位量化所有幅度信息都藏在脉冲的时间轴上时钟抖动直接转化为等效输入噪声。布局时遵循“地平面完整、模拟与数字分区、时钟走线远离模拟输入”三原则。ADC 的模拟输入走线下方不能有数字信号跨越否则数字信号的开关噪声会耦合进模拟信号路径。时钟线需要用包地处理即两侧铺设地线并打过孔连接形成屏蔽。电源走线在进入 ADC 电源引脚前依次经过 10 μF 钽电容和 0.1 μF 陶瓷电容前者用于低频储能后者用于高频去耦两者缺一不可。RC 滤波是 ADC 前端最简单也最容易被忽略的环节。SAR ADC 的驱动运放输出端到 ADC 输入引脚之间通常需要插入 RC 低通滤波器取值经验是 R 在几十到几百欧姆C 取 1~10 nF通常取 5 nF 附近。R 和 ADC 内部采样电容构成一个 RC 充电回路如果 R 太大而采样时间不足电容没充满就开始转换结果自然偏小。ΣΔ ADC 前端则是另一套逻辑——需要把带外噪声尽可能滤掉否则这些高频分量经调制器折返后落入基带拉低信噪比所以 RC 的截止频率要比奈奎斯特频率低一个数量级。5. 验证 ADC/PDM 链路信噪比、DNL 与频谱测量5.1 用 FFT 快速评估采样链路质量代码写完装到板子上不能只看输出数值有没有规律要量化判断链路有没有达到预期性能。最有用的手段是频谱分析给 ADC 输入一个 1 kHz 满幅正弦波采集 4096 点数据做 FFT 后观察频谱。理想的频谱只有 1 kHz 处一根谱线实际会出现谐波和噪底。信号幅度与噪底平均幅值之差就是信噪比谐波幅度与基波幅度之比是 THD总谐波失真这两个指标直接决定了链路质量。如果 FFT 频谱上出现非整数倍的杂散峰大概率是时钟抖动或电源耦合引起的。这时候用示波器观察 ADC 的转换触发信号和电源纹波能快速定位。如果杂散频率与开关电源的开关频率一致问题在电源如果与某个数字总线的通信频率一致问题在串扰。PDM 麦克风链路的验证方式类似不过输入激励源要换成声学信号并且需要先经 CIC 抽取后做 FFT。5.2 斜坡法仿真 ADC 的 DNL热搜词里提到“如果通过斜坡法仿真 ADC 的 DNL”这是 ADC 静态指标测试里的关键操作。DNL微分非线性表示实际转换台阶宽度与理想 1 LSB 的偏差。测试原理不复杂给 ADC 输入一个缓慢上升的斜坡电压理想情况下每个输出码出现的次数完全相等。实际器件中每个码出现的次数会正负偏离平均值DNL 就是最大偏离量。import numpy as np # 假设已经采集到一组斜坡输入下的 ADC 输出码 codes np.load(adc_ramp_codes.npy) hist, _ np.histogram(codes, binsnp.arange(0, 4096)) avg np.mean(hist) # 每个码的理想出现次数 for code in range(1, 4095): # 跳过两端不完整的码 dnl (hist[code] - avg) / avg if abs(dnl) 0.5: print(fCode {code}: DNL {dnl:.3f} LSB - 非线性超限)avg是理想情况下每个输出码对应的采样次数。DNL 超过 ±0.5 LSB 意味着存在丢码——某些输入电压区间根本没有对应的输出码这在精密测量场景是不可接受的。斜坡电压的斜率必须足够缓保证每个码至少出现几十次否则统计误差会淹没真实的 DNL 值。ADC 的动态指标还有 ENOB有效位数它综合考虑了信噪比和 THD计算公式为 (ENOB(SNR_{measured}-1.76)/6.02)。实测 ENOB 往往比数据手册标称值低 1~2 位这通常在正常范围——数据手册测的是理想条件实际板子上的时钟抖动、电源噪声和参考源纹波都会折算进有效位数里。5.3 PDM 链路的阻抗匹配与最终调优PDM 麦克风的时钟频率通常在 1.2~4.8 MHz 之间MCU 的 PDM 接口或 GPIO 模拟主控需要提供低抖动的时钟。当走线长度超过 5 cm 时需要考虑串接 33~100 Ω 的电阻来匹配阻抗、抑制振铃。这个电阻的位置应该尽量靠近主控端起到源端匹配的作用。adc.c里若包含 PDM 使能配置通常会有PDM_CLK_DIV寄存器设置它决定过采样率。过采样率越高抽取后的信噪比越好但 MCU 的 DMA 带宽消耗也越大两者需要权衡。测试 PDM 麦克风链路时最简单的方法是用一个已知幅度的 1 kHz 正弦波声源激励然后解码采集到的 PCM 数据对比幅度和波形。用直方图检查每个 PCM 码值出现次数如果出现明显的双峰且中间码值很少说明 CIC 滤波器的增益归一化不对动态范围没有被充分压满。调整归一化系数后重新采集双峰间距会逐渐闭合。ADC 和 PDM 的链路验证本质都是建立“输入已知 → 输出可预期”的闭环。先确保单点正确再放大到全频段扫描那些在时域里看起来乱跳的数据在频域里往往一眼就能看出问题所在——采样时钟边沿旁边的毛刺、电源纹波对应的尖峰、混叠导致的镜像频率都会忠实地反映在频谱图上。拿到adc.c后先别急着跑先把第一节里的SamplingTime和触发源设置对再往后调滤波器顺序反了问题会很难排查。本文还有配套的精品资源点击获取