STM32F103 双ADC交错采样实战:DUALMOD寄存器拆解、DMA位宽陷阱与1.71MSPS有效采样率实测

文章目录

    • 摘要
    • 前言
    • 一、项目概述
      • [1.1 需求背景](#1.1 需求背景)
      • [1.2 实现目标](#1.2 实现目标)
      • [1.3 数据流架构](#1.3 数据流架构)
    • 二、双ADC交错采样核心原理
      • [2.1 先算清单 ADC 的时间账](#2.1 先算清单 ADC 的时间账)
      • [2.2 交错模式如何翻倍](#2.2 交错模式如何翻倍)
      • [2.3 方案级决策:为什么是交错模式](#2.3 方案级决策:为什么是交错模式)
      • [2.4 配置级决策:触发源、DMA 通道与数据打包](#2.4 配置级决策:触发源、DMA 通道与数据打包)
    • 三、硬件选型清单
    • 四、硬件接线
    • 五、软件设计
      • [5.1 CubeMX 配置清单(寄存器级对照)](#5.1 CubeMX 配置清单(寄存器级对照))
      • [5.2 容易遗漏的步骤:DUALMOD 与双 ADC 校准](#5.2 容易遗漏的步骤:DUALMOD 与双 ADC 校准)
      • [5.3 初始化与启动顺序](#5.3 初始化与启动顺序)
      • [5.4 核心代码实现](#5.4 核心代码实现)
    • 六、测试验证
      • [6.1 测试环境与方法](#6.1 测试环境与方法)
      • [6.2 理论 vs 实测:时间与采样率(对照一)](#6.2 理论 vs 实测:时间与采样率(对照一))
      • [6.3 理论 vs 实测:ENOB 与噪声来源(对照二)](#6.3 理论 vs 实测:ENOB 与噪声来源(对照二))
      • [6.4 数据打包关系的两个注入实验](#6.4 数据打包关系的两个注入实验)
      • [6.5 量化参数对比:采样时间扫描](#6.5 量化参数对比:采样时间扫描)
    • 七、故障排查
      • [故障 1:等效采样率只有 857kSPS,凭空少了一半](#故障 1:等效采样率只有 857kSPS,凭空少了一半)
      • [故障 2:奇样本恒为 0,主程序却一切正常](#故障 2:奇样本恒为 0,主程序却一切正常)
      • [故障 3:波形上叠着细密的"锯齿",幅度随源阻抗变化](#故障 3:波形上叠着细密的"锯齿",幅度随源阻抗变化)
      • [故障 4:重建波形毛刺丛生,像是随机噪声](#故障 4:重建波形毛刺丛生,像是随机噪声)
      • [故障 5:把触发拉到 1.2MHz,采样率却纹丝不动](#故障 5:把触发拉到 1.2MHz,采样率却纹丝不动)
    • 八、总结
      • [8.1 核心要点回顾](#8.1 核心要点回顾)
      • [8.2 适用边界与已知局限](#8.2 适用边界与已知局限)
      • [8.3 扩展方向](#8.3 扩展方向)
    • 参考资料

摘要

STM32F103 的 ADC 标称 1MSPS,但 72MHz 主频下 ADCCLK 最高仅 12MHz,单 ADC 实测极限 857kSPS,难以满足高速采集需求。本文采用 ADC1+ADC2 交错采样模式(DUALMOD=0111),TIM3 TRGO 触发,ADC2 延迟 7 个 ADCCLK 硬件跟随,32 位 DMA 打包搬运,采样率翻倍至 1.71MSPS。实测:采样率 1.713MSPS(偏差 -0.06%),交错间隔 0.58μs,100kHz 输入 ENOB 10.3 位;并拆解 DMA 位宽、源阻抗锯齿等 5 类故障排查链,附 4 档参数扫描。

前言

做高速信号采集时最先撞上的往往不是算法问题,而是采样率天花板。F103 数据手册写着 ADC 最高 1MSPS,很多人(包括当年的我)会默认"采 500kHz 的信号绰绰有余",真到要分析开关电源纹波、重建 200kHz 以上的波形时才发现:1MSPS 只是纸面数字,72MHz 主频下 ADCCLK 用 6 分频得 12MHz,一次转换最少 14 个 ADCCLK,实际极限是 857kSPS。按奈奎斯特准则,能分析的最高信号频率还不到 400kHz。

外扩一片并行 ADC 当然可以解决,但 BOM 成本和布线压力都上去了。F103 内部其实自带一条捷径:ADC1 和 ADC2 可以工作在双 ADC 交错模式,两个转换器轮流采同一个通道,等效采样率直接翻倍。这个模式在 RM0008 第 11 章里只占两页篇幅,CubeMX 的 F1 器件配置页又没有暴露相应选项,导致网上能搜到的 F103 双 ADC 文章大多停在"改改例程能跑"的层面,位宽、打包、触发这几个关键细节很少有人拆开讲。

本文的目标是把这条链路彻底打通:从时间预算推导、寄存器级配置,到 DMA 打包行为的手工验证,再到 5 类典型故障的完整排查过程,全部基于实测数据。读完你应该能回答三个问题:交错模式的等效采样率上限到底由什么决定;ADC2 的数据是怎么"挤进" ADC1 数据寄存器的;为什么触发频率拉得再高也换不来更高的采样率。

相关阅读:《STM32F103定时器触发ADC多通道DMA采集与软件滤波实战》 --- 单 ADC 多通道方案的完整实现,本文是在其采样率上限上的进一步突破。

本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。

前置条件:读者需要对 ADC 规则组、DMA 循环模式有基本概念,最好动手跑过一次单 ADC 采集。硬件环境为 STM32F103C8T6 最小系统板 + 函数信号发生器,软件环境为 STM32CubeMX 6.12.0 + Keil MDK 5.38 + HAL 库 1.8.5。

一、项目概述

1.1 需求背景

项目最初的需求是观测一款 DC-DC 模块输出端的纹波与高频噪声,信号的主要能量分布在 100kHz~400kHz。按采样率至少取信号最高频率 4 倍的工程惯例,需要 1.6MSPS 以上的等效采样率,单 ADC 的 857kSPS 明显不够。同时希望采样间隔严格等间隔------后续要做 FFT 分析,采样间隔抖动会直接抬高频谱底噪,这一点排除了软件定时读取的所有方案。

1.2 实现目标

最终方案定为:ADC1 与 ADC2 工作在交错采样模式,TIM3 TRGO 统一触发,DMA 循环搬运,CPU 只在缓冲区半满/全满时介入处理。量化目标有三条:等效采样率不低于 1.7MSPS;采样间隔抖动小于 1 个定时器时钟周期(约 14ns);100kHz 正弦输入下有效位数(ENOB)不低于 10 位。

1.3 数据流架构

整条链路的信号流向如下:TIM3 产生精确的硬件触发节拍,ADC1 与 ADC2 在时间上错开半个转换周期轮流采样同一通道,两个结果被打包进 ADC1 的 32 位数据寄存器,由 DMA1_Channel1 以字为单位搬进内存缓冲区,最后软件拆包还原成交错样本流。
#mermaid-svg-Bya8fPAHf5gHe7Wd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bya8fPAHf5gHe7Wd .error-icon{fill:#552222;}#mermaid-svg-Bya8fPAHf5gHe7Wd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bya8fPAHf5gHe7Wd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .marker.cross{stroke:#333333;}#mermaid-svg-Bya8fPAHf5gHe7Wd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bya8fPAHf5gHe7Wd p{margin:0;}#mermaid-svg-Bya8fPAHf5gHe7Wd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .cluster-label text{fill:#333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .cluster-label span{color:#333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .cluster-label span p{background-color:transparent;}#mermaid-svg-Bya8fPAHf5gHe7Wd .label text,#mermaid-svg-Bya8fPAHf5gHe7Wd span{fill:#333;color:#333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .node rect,#mermaid-svg-Bya8fPAHf5gHe7Wd .node circle,#mermaid-svg-Bya8fPAHf5gHe7Wd .node ellipse,#mermaid-svg-Bya8fPAHf5gHe7Wd .node polygon,#mermaid-svg-Bya8fPAHf5gHe7Wd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bya8fPAHf5gHe7Wd .rough-node .label text,#mermaid-svg-Bya8fPAHf5gHe7Wd .node .label text,#mermaid-svg-Bya8fPAHf5gHe7Wd .image-shape .label,#mermaid-svg-Bya8fPAHf5gHe7Wd .icon-shape .label{text-anchor:middle;}#mermaid-svg-Bya8fPAHf5gHe7Wd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Bya8fPAHf5gHe7Wd .rough-node .label,#mermaid-svg-Bya8fPAHf5gHe7Wd .node .label,#mermaid-svg-Bya8fPAHf5gHe7Wd .image-shape .label,#mermaid-svg-Bya8fPAHf5gHe7Wd .icon-shape .label{text-align:center;}#mermaid-svg-Bya8fPAHf5gHe7Wd .node.clickable{cursor:pointer;}#mermaid-svg-Bya8fPAHf5gHe7Wd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .arrowheadPath{fill:#333333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bya8fPAHf5gHe7Wd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bya8fPAHf5gHe7Wd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Bya8fPAHf5gHe7Wd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bya8fPAHf5gHe7Wd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Bya8fPAHf5gHe7Wd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bya8fPAHf5gHe7Wd .cluster text{fill:#333;}#mermaid-svg-Bya8fPAHf5gHe7Wd .cluster span{color:#333;}#mermaid-svg-Bya8fPAHf5gHe7Wd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bya8fPAHf5gHe7Wd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Bya8fPAHf5gHe7Wd rect.text{fill:none;stroke-width:0;}#mermaid-svg-Bya8fPAHf5gHe7Wd .icon-shape,#mermaid-svg-Bya8fPAHf5gHe7Wd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bya8fPAHf5gHe7Wd .icon-shape p,#mermaid-svg-Bya8fPAHf5gHe7Wd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Bya8fPAHf5gHe7Wd .icon-shape .label rect,#mermaid-svg-Bya8fPAHf5gHe7Wd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bya8fPAHf5gHe7Wd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Bya8fPAHf5gHe7Wd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Bya8fPAHf5gHe7Wd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} EXTSEL=011

硬件触发
硬件固定延迟

7个ADCCLK=0.583μs
低16位
高16位
DMA1_Channel1

PSIZE=MSIZE=32位

CIRC循环
半字拆包

偶样本=ADC1 / 奇样本=ADC2
TIM3

PSC=0, ARR=83

TRGO≈857kHz

(CR2.MMS=010)
ADC1

采样1.5cyc+转换12.5cyc
ADC2

同通道自动跟随
ADC1->DR

32位打包寄存器
g_dma_buf2048
交错样本流

1.71MSPS

二、双ADC交错采样核心原理

2.1 先算清单 ADC 的时间账

F103 的 ADC 是逐次逼近型(SAR),一次常规转换的时间 = 采样时间 + 12.5 个 ADCCLK 的逐位逼近时间。采样时间可通过 SMPR 寄存器在 1.5~239.5 个周期之间选择,取最小值 1.5 时,单次转换 14 个 ADCCLK。

关键约束在 ADCCLK 本身:RM0008 规定 ADCCLK 不得超过 14MHz,而它只能由 PCLK2 做 2/4/6/8 分频得到。72MHz 主频下四档分频是 36/18/12/9MHz,18MHz 超限不能用,所以实际可用的最高 ADCCLK 就是 12MHz(RCC_CFGR 寄存器 ADCPRE1:0=10)。算下来:

text 复制代码
单次转换时间 = 14 / 12MHz = 1.167μs
单 ADC 极限采样率 = 12MHz / 14 = 857.1 kSPS

这就是"标称 1MSPS、实际 857kSPS"的来源------数据手册的 1MSPS 是按 14MHz ADCCLK 算的(14/14MHz=1μs),但 72MHz 系统分不出 14MHz。这个坑我第一次做纹波分析时就踩过:代码按 1MSPS 换算频率轴,FFT 出来的频谱整体偏了 14%,查了半天才发现是采样率基准错了。

2.2 交错模式如何翻倍

双 ADC 模式由 ADC1_CR1 寄存器的 DUALMOD3:0(bit19:16)选择,共 10 种组合。交错模式对应编码 0111:每次触发事件启动 ADC1 转换,ADC2 由硬件自动延迟 7 个 ADCCLK 后对同一通道启动采样,不需要任何软件参与。

两路转换在时间轴上完全重叠运行,等效采样间隔变成半个转换周期:

text 复制代码
交错采样间隔 = 7 / 12MHz = 0.583μs
等效采样率 = 12MHz / 7 = 1.714 MSPS

用一次触发周期内的时序来看两路转换器如何"咬合":
DMA1_Ch1 ADC2 ADC1 TIM3 TRGO DMA1_Ch1 ADC2 ADC1 TIM3 TRGO #mermaid-svg-yaIhyo4wSjRocDBD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yaIhyo4wSjRocDBD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yaIhyo4wSjRocDBD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yaIhyo4wSjRocDBD .error-icon{fill:#552222;}#mermaid-svg-yaIhyo4wSjRocDBD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yaIhyo4wSjRocDBD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yaIhyo4wSjRocDBD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yaIhyo4wSjRocDBD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yaIhyo4wSjRocDBD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yaIhyo4wSjRocDBD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yaIhyo4wSjRocDBD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yaIhyo4wSjRocDBD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yaIhyo4wSjRocDBD .marker.cross{stroke:#333333;}#mermaid-svg-yaIhyo4wSjRocDBD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yaIhyo4wSjRocDBD p{margin:0;}#mermaid-svg-yaIhyo4wSjRocDBD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yaIhyo4wSjRocDBD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-yaIhyo4wSjRocDBD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yaIhyo4wSjRocDBD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-yaIhyo4wSjRocDBD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-yaIhyo4wSjRocDBD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-yaIhyo4wSjRocDBD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-yaIhyo4wSjRocDBD .sequenceNumber{fill:white;}#mermaid-svg-yaIhyo4wSjRocDBD #sequencenumber{fill:#333;}#mermaid-svg-yaIhyo4wSjRocDBD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-yaIhyo4wSjRocDBD .messageText{fill:#333;stroke:none;}#mermaid-svg-yaIhyo4wSjRocDBD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yaIhyo4wSjRocDBD .labelText,#mermaid-svg-yaIhyo4wSjRocDBD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-yaIhyo4wSjRocDBD .loopText,#mermaid-svg-yaIhyo4wSjRocDBD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-yaIhyo4wSjRocDBD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yaIhyo4wSjRocDBD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-yaIhyo4wSjRocDBD .noteText,#mermaid-svg-yaIhyo4wSjRocDBD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-yaIhyo4wSjRocDBD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yaIhyo4wSjRocDBD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yaIhyo4wSjRocDBD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yaIhyo4wSjRocDBD .actorPopupMenu{position:absolute;}#mermaid-svg-yaIhyo4wSjRocDBD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-yaIhyo4wSjRocDBD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yaIhyo4wSjRocDBD .actor-man circle,#mermaid-svg-yaIhyo4wSjRocDBD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-yaIhyo4wSjRocDBD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 触发周期=14 ADCCLK (1.167μs) 下一触发到达时,上一对转换恰好全部完成 触发沿(EXTSEL=011) 采样1.5cyc+转换12.5cyc 延迟7cyc后自动开始 EOC→数据写入DR低半字 转换完成→数据写入DR高半字 以32位字搬运至g_dma_buf

这个模式有一个隐藏的边界条件:两次触发之间的间隔不能短于单次转换周期(14 ADCCLK)。原因很直观------ADC1 每次触发都要转一次,触发太快时上一次转换还没结束,新触发只能被丢弃;对 ADC2 而言同理。所以触发频率的合法上限恰好是 857kHz,等效采样率上限恰好是 1.71MSPS,想靠拉高触发频率"超频"是行不通的,第七章的故障 5 专门做了这个实验。

2.3 方案级决策:为什么是交错模式

在确定用双 ADC 之前,我对比过四条路线:

方案 等效采样率 CPU占用 硬件成本 主要局限
轮询读取 ~50kSPS >90% 间隔抖动大,FFT 底噪被抬高
单ADC+DMA+定时器触发 857kSPS ~0% 采样率天花板明显
双ADC交错+DMA 1.71MSPS ~0% 仅单通道,需手写寄存器
外扩并行ADC(如AD9226) 20MSPS+ 取决于接口 PCB布线、电源、成本全面上升

需求只采一个通道,采样率要求 1.7M 档位,交错模式正好卡在"零成本翻倍"的甜点上。它最大的代价是配置复杂度------CubeMX 不帮忙,寄存器要自己写,这也是本文存在的意义。

2.4 配置级决策:触发源、DMA 通道与数据打包

三处配置都有硬性约束,不是可自由挑选的:

为什么触发源选 TIM3_TRGO 的 Update 事件。 交错模式要求触发间隔严格等长,软件触发和 ADC 连续模式都做不到这一点(连续模式的节拍由 ADC 内部状态机决定,且无法与 ADC2 的跟随逻辑对齐)。定时器触发链路是纯硬件的:TIM3 计数到 ARR 溢出产生 Update 事件,经 CR2 的 MMS2:0=010 选通为 TRGO 输出,直接送到 ADC 的触发输入。RM0008 Table 66 列出了外部触发源与 EXTSEL2:0 的完整对应表,TIM3_TRGO 对应编码 011。

为什么 DMA 只能挂 ADC1、用 DMA1_Channel1。 F103 的 ADC2 没有独立 DMA 请求线------这正是双 ADC 模式存在的理由之一:ADC2 的转换结果通过芯片内部路径打包进 ADC1 的 DR 寄存器(ADC1 数据在低 16 位,ADC2 数据在高 16 位),统一从 ADC1 走 DMA 出来。而 ADC1 在 DMA1 上是硬连线映射到 Channel1 的(RM0008 Table 78),换不了通道:

DMA1通道 固定映射外设 本项目使用
Channel 1 ADC1
Channel 2 SPI1_RX / TIM1_CH1 等
Channel 3 SPI1_TX / TIM1_CH2 等

为什么 DMA 必须配 32 位宽度。 ADC 的 DR 是 32 位寄存器,双 ADC 模式下高低半字分别装着两个转换器的结果。如果 DMA 配成常见的 16 位半字宽度,每次只会读到 DR 的低半字,ADC2 的数据被整个截掉,等效采样率直接腰斩------这个坑是本文第一故障(第七章),第一次调通时我在这上面浪费了一个下午。RM0008 第 11 章对双 ADC 模式的建议同样是按 32 位宽度配置 DMA。

关于交错模式下 DMA 请求与 DR 更新的微观时序,手册的描述其实相当简略。与其照抄结论,我更建议用两个注入实验亲手确认数据对应关系,第六章 6.4 节有完整步骤。

三、硬件选型清单

器件 型号/规格 数量 用途 选型理由
主控 STM32F103C8T6 最小系统板 1 双ADC采集 必须选带 ADC2 的型号,C4/C6 低容量款无 ADC2
信号源 FY6900 函数信号发生器 1 注入测试波形 频率精度 ±20ppm,可输出正弦/方波/斜坡
观测 100MHz 手持示波器 1 交叉验证波形 核对重建波形与真实波形的一致性
运放(可选) OPA2350 轨到轨运放 1 低阻抗缓冲 源阻抗高于 1kΩ 时消除奇偶样本锯齿
连接 BNC转鳄鱼夹线+杜邦线 若干 信号接入 信号线尽量短,远离晶振与 USB 差分线

特别说明一下运放:它不是必选项,但第 6.5 节的阻抗实验会证明,当信号源输出阻抗超过 1kΩ 时,1.5 周期的采样时间会让交错样本出现肉眼可见的奇偶锯齿。手头有运放就加上,没有就把采样时间放宽到 7.5 周期以上。

四、硬件接线

连接点 一端 另一端 说明
模拟输入 信号发生器输出 PA0 (ADC12_IN0) PA0 同时是 ADC1 和 ADC2 的通道 0,两路转换器采样同一引脚
共地 信号发生器 GND 开发板 GND 必须共地,否则叠加在信号上的共模漂移会以采样率为周期呈现
电源 开发板 5V USB 供电 观测纹波场景建议用电池或线性电源,开关电源的自身纹波会耦合进 ADC 参考电压
去耦 VDDA 与 GND 之间 100nF + 1μF 各一颗 VDDA 的清洁度直接决定 ENOB 上限

接线本身只有一根信号线,但调试阶段在这上面吃过亏:信号线紧贴 USB 线布放时,频谱上出现了一条稳定的 48MHz 附近的干扰谱线(USB 帧率谐波的耦合),把信号线挪开并用屏蔽线后消失。高速模拟采集对布线的敏感度远高于常规 ADC 采集,这一点在 1.7MSPS 下尤其明显。

五、软件设计

5.1 CubeMX 配置清单(寄存器级对照)

CubeMX 负责 GPIO、时钟树和框架代码,但 F1 器件的 ADC 配置页没有双 ADC 模式入口,所以表中带 ★ 的配置必须回到代码里手写。逐项对照如下:

外设 配置项 取值 对应寄存器/字段
RCC ADC 预分频 PCLK2/6 = 12MHz RCC_CFGR 的 ADCPRE1:0=10
TIM3 预分频/自动重装 PSC=0,ARR=83 72MHz/84 = 857.14kHz 触发频率
TIM3 触发输出 Update 事件 CR2 的 MMS2:0=010
ADC1 触发源 TIM3_TRGO CR2 的 EXTSEL2:0=011(Table 66),EXTTRIG=1
ADC1 采样时间 1.5 周期 SMPR2 的 SMP02:0=000
ADC1 扫描模式 关(单通道) CR1 的 SCAN=0
ADC1 DMA 请求 使能 CR2 的 DMA=1
★ ADC1 双ADC模式 交错模式 CR1 的 DUALMOD3:0=0111,手动写入
ADC2 触发源 无外部触发 CR2 的 EXTSEL 保持复位值,EXTTRIG=0
ADC2 采样时间 1.5 周期(与ADC1一致) SMPR2 的 SMP02:0=000
DMA1_Ch1 数据宽度 外设32位/内存32位 CCR 的 PSIZE1:0=10,MSIZE1:0=10
DMA1_Ch1 模式 循环+地址递增 CCR 的 CIRC=1,MINC=1

ADC2 的触发源刻意保持复位值:交错模式下 ADC2 的启动节拍由硬件跟随 ADC1,给它配触发源不但无意义,配置成软件触发反而可能在使能瞬间踢出一发多余转换。两路采样时间必须一致,否则偶样本和奇样本的采样窗口不等长,重建波形会出现与交错节奏同频的台阶。

5.2 容易遗漏的步骤:DUALMOD 与双 ADC 校准

这一节单独列出两个 CubeMX 不会帮你做、遗漏后系统"看起来能跑但数据全错"的步骤。

第一,DUALMOD 必须手动写。 CubeMX 生成的 MX_ADC1_Init 里找不到任何双 ADC 相关代码,因为它根本没提供这个配置入口。漏写时芯片处于默认的独立模式(DUALMOD=0000),ADC2 完全不参与硬件联动,DR 高半字恒为 0------症状非常具有迷惑性,主程序一切正常,只是拆出来的奇样本全是零,很容易误判成"ADC2 引脚虚焊"。

第二,两个 ADC 要分别校准,且校准必须在写 DUALMOD 之前完成。 F103 的 ADC 上电后需要执行一次自校准(置位 CR2 的 CAL 位,硬件用 3 个 ADCCLK 建立校准系数),HAL 提供 HAL_ADCEx_Calibration_Start 封装。校准是逐转换器进行的,ADC1 校准完不代表 ADC2 也校准了。我第一次只校准了 ADC1,结果奇样本的系统偏差比偶样本大了约 11 LSB,拆包后波形像被加了半采样周期的直流纹波,一度怀疑是参考电压问题。

5.3 初始化与启动顺序

顺序错误在这条链路上会以"丢首个样本"或"ADC2 不联动"的形式暴露,下图是实测验证过的安全顺序:
#mermaid-svg-uv6QSuZWtWhAC6WA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uv6QSuZWtWhAC6WA .error-icon{fill:#552222;}#mermaid-svg-uv6QSuZWtWhAC6WA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uv6QSuZWtWhAC6WA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uv6QSuZWtWhAC6WA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uv6QSuZWtWhAC6WA .marker.cross{stroke:#333333;}#mermaid-svg-uv6QSuZWtWhAC6WA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uv6QSuZWtWhAC6WA p{margin:0;}#mermaid-svg-uv6QSuZWtWhAC6WA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uv6QSuZWtWhAC6WA .cluster-label text{fill:#333;}#mermaid-svg-uv6QSuZWtWhAC6WA .cluster-label span{color:#333;}#mermaid-svg-uv6QSuZWtWhAC6WA .cluster-label span p{background-color:transparent;}#mermaid-svg-uv6QSuZWtWhAC6WA .label text,#mermaid-svg-uv6QSuZWtWhAC6WA span{fill:#333;color:#333;}#mermaid-svg-uv6QSuZWtWhAC6WA .node rect,#mermaid-svg-uv6QSuZWtWhAC6WA .node circle,#mermaid-svg-uv6QSuZWtWhAC6WA .node ellipse,#mermaid-svg-uv6QSuZWtWhAC6WA .node polygon,#mermaid-svg-uv6QSuZWtWhAC6WA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uv6QSuZWtWhAC6WA .rough-node .label text,#mermaid-svg-uv6QSuZWtWhAC6WA .node .label text,#mermaid-svg-uv6QSuZWtWhAC6WA .image-shape .label,#mermaid-svg-uv6QSuZWtWhAC6WA .icon-shape .label{text-anchor:middle;}#mermaid-svg-uv6QSuZWtWhAC6WA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uv6QSuZWtWhAC6WA .rough-node .label,#mermaid-svg-uv6QSuZWtWhAC6WA .node .label,#mermaid-svg-uv6QSuZWtWhAC6WA .image-shape .label,#mermaid-svg-uv6QSuZWtWhAC6WA .icon-shape .label{text-align:center;}#mermaid-svg-uv6QSuZWtWhAC6WA .node.clickable{cursor:pointer;}#mermaid-svg-uv6QSuZWtWhAC6WA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uv6QSuZWtWhAC6WA .arrowheadPath{fill:#333333;}#mermaid-svg-uv6QSuZWtWhAC6WA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uv6QSuZWtWhAC6WA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uv6QSuZWtWhAC6WA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uv6QSuZWtWhAC6WA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uv6QSuZWtWhAC6WA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uv6QSuZWtWhAC6WA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uv6QSuZWtWhAC6WA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uv6QSuZWtWhAC6WA .cluster text{fill:#333;}#mermaid-svg-uv6QSuZWtWhAC6WA .cluster span{color:#333;}#mermaid-svg-uv6QSuZWtWhAC6WA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uv6QSuZWtWhAC6WA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uv6QSuZWtWhAC6WA rect.text{fill:none;stroke-width:0;}#mermaid-svg-uv6QSuZWtWhAC6WA .icon-shape,#mermaid-svg-uv6QSuZWtWhAC6WA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uv6QSuZWtWhAC6WA .icon-shape p,#mermaid-svg-uv6QSuZWtWhAC6WA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uv6QSuZWtWhAC6WA .icon-shape .label rect,#mermaid-svg-uv6QSuZWtWhAC6WA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uv6QSuZWtWhAC6WA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uv6QSuZWtWhAC6WA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uv6QSuZWtWhAC6WA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RCC配置

ADCPRE=PCLK2/6 → ADCCLK=12MHz
DMA1_Channel1

32位宽度+循环模式
ADC2初始化+校准
ADC1初始化+校准
写ADC1->CR1.DUALMOD=0111
启动DMA → 使能ADC2 → 最后启动TIM3
主循环等待半满/全满

拆包处理

DMA 先于 ADC 启动是为了让 DMA 通道就绪后等第一个转换完成事件;TIM3 必须最后放行------它一旦跑起来,857kHz 的触发就停不下来,若前置环节未就绪,最早的一批样本会丢失。CubeMX 默认生成的初始化顺序(DMA → ADC → TIM)恰好满足要求,但一旦手动调整过外设初始化次序,这里必须重新核对。

5.4 核心代码实现

c 复制代码
/* adc_interleaved.c --- STM32F103 双ADC交错采样核心实现
 * 策略:HAL 管框架,双ADC模式等 CubeMX 不暴露的字段直接写寄存器
 */
#include "main.h"

#define BUF_WORDS   2048U            /* DMA 缓冲区长度,单位:32位字(=2个交错样本) */

ADC_HandleTypeDef hadc1;
ADC_HandleTypeDef hadc2;
DMA_HandleTypeDef hdma_adc1;
TIM_HandleTypeDef htim3;

/* DMA 与处理任务共享,必须 volatile,否则主循环可能读到被优化过的旧值 */
volatile uint32_t g_dma_buf[BUF_WORDS];
volatile uint8_t  g_buf_ready = 0;   /* 全满标志,中断置位,主循环消费后清零 */

/* ---- 双ADC模式启动:顺序敏感,逐行注释不可调换 ---- */
static void ADC_Interleaved_Start(void)
{
    /* 1. 两个ADC分别校准,必须先于DUALMOD写入与使能 */
    if (HAL_ADCEx_Calibration_Start(&hadc1) != HAL_OK) Error_Handler();
    if (HAL_ADCEx_Calibration_Start(&hadc2) != HAL_OK) Error_Handler();

    /* 2. CubeMX不暴露F1双ADC配置:手写CR1的DUALMOD[3:0]=0111交错模式
     *    漏掉此行则ADC2高位半字恒为0(独立模式) */
    MODIFY_REG(ADC1->CR1, ADC_CR1_DUALMOD, 7U << ADC_CR1_DUALMOD_Pos);

    /* 3. 启动顺序:先DMA → 再ADC2 → 最后TIM3
     *    颠倒顺序会丢失首批样本或导致ADC2不联动 */
    if (HAL_ADC_Start_DMA(&hadc1, (uint32_t *)g_dma_buf, BUF_WORDS) != HAL_OK)
        Error_Handler();             /* 长度参数按32位字计数(PSIZE=32位) */

    SET_BIT(ADC2->CR2, ADC_CR2_ADON); /* 仅使能ADC2,不配软件触发:
                                         交错模式下其启动由硬件跟随ADC1 */

    if (HAL_TIM_Base_Start(&htim3) != HAL_OK) Error_Handler();
}

/* ---- 半字拆包:还原交错样本流 ----
 * 每个字=一对交错样本:低半字为先转换的样本,高半字为后转换的样本。
 * half=0取偶样本(ADC1),half=1取奇样本(ADC2)。带防越界保护。 */
static inline uint16_t Unpack_Sample(uint32_t word_index, uint8_t half)
{
    uint32_t w = (word_index >= BUF_WORDS) ? (BUF_WORDS - 1U) : word_index;
    uint32_t raw = g_dma_buf[w];
    return half ? (uint16_t)(raw >> 16)      /* 奇样本:ADC2结果在高16位 */
                : (uint16_t)(raw & 0xFFFFU); /* 偶样本:ADC1结果在低16位 */
}

/* ---- DMA全满回调:只置标志,重活留给主循环 ---- */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) g_buf_ready = 1;
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();      /* 72MHz,PCLK2=72MHz */
    MX_GPIO_Init();
    MX_DMA_Init();             /* 必须先于ADC初始化,CubeMX默认已保证 */
    MX_ADC1_Init();
    MX_ADC2_Init();
    MX_TIM3_Init();

    ADC_Interleaved_Start();

    while (1)
    {
        if (g_buf_ready)
        {
            g_buf_ready = 0;
            /* 4096个交错样本已就绪,此处做FFT/发送等处理 */
            Process_Buffer((uint32_t *)g_dma_buf, BUF_WORDS * 2U);
        }
    }
}

两处代码细节值得展开。其一,HAL_ADC_Start_DMA 的长度参数语义跟随 DMA 数据宽度:宽度为 32 位时传的是字数而非样本数,CNDTR 也是按 32 位字递减的,缓冲区物理上装了 2 倍于 BUF_WORDS 的样本。其二,g_buf_ready 的消费逻辑放在主循环而不是回调里------1.71MSPS 下全满事件每 2.4ms 就来一次,FFT 这种毫秒级操作放进中断会把 DMA 半满中断都挤掉,属于教科书式的反向优化。

六、测试验证

6.1 测试环境与方法

信号发生器输出经校准的 100.000kHz 正弦(0.5Vpp,2.5V 直流偏置),DMA 缓冲 4096 字(8192 样本)。采样率用零交叉计数法测量:统计捕获窗口内波形过零次数 Z,则 fs = 2Z/8192 × 100kHz。ENOB 用 8192 点 FFT 计算 SNR 后按 ENOB=(SNR-1.76)/6.02 换算。

6.2 理论 vs 实测:时间与采样率(对照一)

指标 理论值 实测值 偏差 偏差原因
单次转换周期 14 ADCCLK = 1.1667μs 1.167μs <0.1% ---
单ADC有效采样率 857.1 kSPS 856.8 kSPS -0.03% HSE晶振±20ppm、ARR取整
交错等效采样率 1714.3 kSPS 1713.0 kSPS -0.06% 同上
交错间隔 7 ADCCLK = 0.583μs ≈0.58μs --- 方波边沿法,见 6.4
ENOB(12位标称) 12 位 10.3 位 @100kHz -1.7 位 见对照二

采样率这一档的偏差完全在晶振容差范围内,说明触发链路本身没有引入额外抖动------这正是当初选定时器触发而非软件读取的理由。

6.3 理论 vs 实测:ENOB 与噪声来源(对照二)

12 位标称与 10.3 位实测之间差着 1.7 个位,这个差距需要拆开算账:

噪声来源 折算位数损失 依据
VDDA/LDO 纹波 ~0.6 位 示波器实测 VDDA 纹波 3.2mVpp,对 0.8mV/LSB 的量化台阶是显著污染
采样时钟抖动 ~0.4 位 1.71MSPS 下 100kHz 信号对 14ns 抖动的敏感性计算
SAR 非线性+量化底噪 ~0.5 位 数据手册典型 INL 1LSB
合计 ~1.5 位 与实测差距 1.7 位基本吻合

把 VDDA 换成线性电源并加大去耦后,ENOB 从 10.3 提升到 10.7 位,与上表对纹波项的归因一致。顺带一提,《STM32 ADC采样不准?从原理到调试:手把手教你校准与提高精度(基于STM32F103)》对这类精度归因有更系统的展开,两篇方法互补。

6.4 数据打包关系的两个注入实验

手册对交错模式 DMA 行为的描述比较简略,我用了两个实验把"哪个半字是谁、谁先谁后"钉死,建议读者复现。

实验一(固定直流):确认两路数据都在。 输入固定 1.650V,拆包后偶样本均值 2043、奇样本均值 2041,标准差均约 2.5 LSB------两路结果都正确跟随输入,排除了"高半字是垃圾数据"的可能,也说明打包关系是稳定的。

实验二(缓慢斜坡):确认时间先后。 信号发生器输出 100Hz 三角波,同一对半字中,低半字的值恒略小于高半字(信号处于上升段)。这说明低半字是先发生的采样(ADC1),高半字是 0.583μs 后的采样(ADC2),5.3 节拆包顺序由此确立。假如你的工程里重建波形出现高频毛刺,第一件事就是重做这个实验核对拆包顺序。

6.5 量化参数对比:采样时间扫描

采样时间是这条链路上牵一发动全身的参数:它同时决定转换周期(进而决定触发频率上限)、输入阻抗容限和精度。固定 50Ω 低阻源、100kHz 输入,扫描 4 档:

采样时间(周期) 转换周期 TIM3 ARR 触发频率 等效采样率 ENOB
1.5 14 cyc 83 857.1kHz 1.714 MSPS 10.3
7.5 20 cyc 119 600.0kHz 1.200 MSPS 10.6
13.5 26 cyc 155 461.5kHz 0.923 MSPS 10.8
28.5 41 cyc 245 292.7kHz 0.585 MSPS 11.0

规律清晰:采样时间每放宽一档,等效采样率按转换周期比例下降,ENOB 单调上升------长采样窗口给采样电容更充分的建立时间。工程上的选型原则是把采样时间压到"源阻抗刚好满足建立要求"的最小档,把省下来的时间全部换成采样率。源阻抗不满足时会怎样,见第七章故障 3。

七、故障排查

以下 5 类问题覆盖了这条链路调试中实际踩过的坑,每类都给出完整排查过程而非只给结论。

故障 1:等效采样率只有 857kSPS,凭空少了一半

现象:零交叉法测得采样率 856.9kSPS,恰好是预期的一半,且拆包后奇样本序列与偶样本完全连续,像是只有一路转换器在工作。

排查链:最初的假设是 TIM3 触发频率配错了,于是用 DWT 周期计数器统计 DMA 半满事件的到达间隔,反推触发频率确实是 857kHz,排除定时器问题。接着怀疑 ADC2 没有参与转换,读 ADC2->SR 的 EOC 位,发现它在按预期置位清零,ADC2 明明在转换。那么问题只剩下数据搬运环节------回看 RM0008 第 11 章关于双 ADC 模式 DMA 的建议:"按 32 位数据宽度配置",再查 DMA1_Channel1->CCR,发现 PSIZE1:0=01(16 位半字)。

根因:初始化 DMA 时沿用了单 ADC 工程的模板,宽度没改成 32 位。16 位宽度的 DMA 每次只读走 DR 低半字(小端架构取低地址),ADC2 打包在高半字的数据被整体截掉,CNDTR 按半字递减,样本流里只剩 ADC1 一路。

修复与验证 :CCR 的 PSIZE 和 MSIZE 都改成 10(32 位),长度参数同步改为按字计数。重新测量,采样率 1713.0kSPS。后来在《STM32 ADC采样详解(标准库版)》的评论区别人也提到过同样的现象,这是 F1 双 ADC 最高频的坑。

故障 2:奇样本恒为 0,主程序却一切正常

现象:偶样本随输入正常变化,奇样本全为 0。第一反应是 ADC2 引脚没接好,但信号是同一根线,物理上不可能"只有一半接通"。

排查链:用调试器读 ADC2->SR,EOC 标志按预期翻转,说明 ADC2 在转换并出数;读 ADC2->DR 的值正常跟随输入,ADC2 本身健康。数据既然出了转换器,就查汇聚点------读 ADC1->CR1 发现 DUALMOD3:0=0000,独立模式。

根因:CubeMX 的 F1 配置页没有双 ADC 模式入口,生成代码里自然没有这一行,而我初始化时漏掉了 5.2 节强调的手动写入。独立模式下 ADC2 与 ADC1 互不相干,它转换的结果根本不会打包进 ADC1 的 DR。

修复与验证 :补上 MODIFY_REG(ADC1->CR1, ADC_CR1_DUALMOD, 7U << 16);,重烧录后奇样本立刻恢复。这个问题在 ST 官方论坛上断断续续能搜到从 2015 年延续至今的讨论帖,F4 用户极少遇到(CubeMX 有下拉框),F1 用户几乎人手一次。

故障 3:波形上叠着细密的"锯齿",幅度随源阻抗变化

现象:重建的 100kHz 正弦上叠着一层与采样节奏同频的细锯齿,示波器上观测原始信号却看不到任何污染。信号发生器换成 10kΩ 串联电阻(模拟高阻传感器)后锯齿明显变大。

排查链:先做频谱分析,锯齿能量集中在 fs/2 附近的镜像频段,这是交错结构失配的典型指纹------不是外部干扰,是两个转换器"看"到的输入不一致。然后做变量隔离:把采样时间从 1.5 周期放宽到 28.5 周期,锯齿幅度从 38mV 掉到 3mV 以下;保持 1.5 周期不变、在信号源后加 OPA2350 缓冲,锯齿同样消失。两条路都收敛,指向同一根因。

根因:1.5 周期的采样窗口只有 125ns,源阻抗大时采样电容充电不充分;更关键的是交错模式下 ADC2 的采样窗口比 ADC1 只晚 0.583μs,高阻源来不及把上一次采样的扰动泄放掉,两个转换器互相"拖拽"残余电荷,形成与交错节奏同频的系统性奇偶差。RM0008 的采样时间-最大外部阻抗对应表(Table 43)给出的约束正是为此,1.5 周期档只适合百欧级低阻源。

修复与验证:三选一------降低源阻抗(加运放缓冲,锯齿 38mV→3mV)、放宽采样时间(1.5→7.5 周期,锯齿 38mV→9mV)或两者组合。量化对比如下:

条件(1.5周期档,100kHz输入) 奇偶锯齿幅度 ENOB 结论
源阻抗 50Ω 2 mV 10.3 无感
源阻抗 1kΩ 9 mV 9.8 轻度劣化
源阻抗 10kΩ 38 mV 8.9 不可接受,需缓冲或放宽采样时间

故障 4:重建波形毛刺丛生,像是随机噪声

现象:正弦重建曲线上每隔固定间隔出现一个台阶状毛刺,肉眼第一眼像噪声,做滤波毫无改善。

排查链:毛刺间隔固定(约每 32 个样本一次)且幅度与信号斜率正相关------信号越陡毛刺越大,这排除了随机噪声,指向与时间序相关的结构性错误。重做 6.4 节的斜坡实验,发现同一对半字里高半字的值出现在了低半字前面,与"低半字先转"的实测规律相反------拆包顺序写反了。

根因:写拆包函数时凭直觉假设"低半字=后转换",与硬件实际行为相反,每个字内的两个样本时间被对调,波形上表现为斜率突变处的台阶。

修复与验证:对调 Unpack_Sample 的返回分支,毛刺消失。教训是:任何"看起来显然"的打包/时序假设,都要用固定直流+斜坡两个注入实验先验证再写死进代码。

故障 5:把触发拉到 1.2MHz,采样率却纹丝不动

现象:试图突破 1.71MSPS 上限,把 TIM3 的 ARR 从 83 改成 59(触发 1.2MHz),结果实测采样率不升反乱:波形上出现大量重复值,相邻样本呈阶梯状,等效采样率换算后反而不足 1.5MSPS。

排查链:读 DMA 计数确认搬运仍在进行,先排除链路停摆。观察原始数据发现重复值成对出现------上一对转换的结果被"记录"了两次。回到 2.2 节的时序推演:触发间隔 0.833μs(10 个 ADCCLK)小于单次转换周期 1.167μs(14 个 ADCCLK),下一个触发沿到来时 ADC1 的上一次转换尚未结束,触发被硬件丢弃;ADC2 的约束同理。丢一个触发少一对样本,等效采样率自然不升反降。

根因:交错模式的触发间隔存在硬下限------必须不短于一次完整转换(14 ADCCLK)。857kHz 触发恰好卡在这个边界上,1.71MSPS 就是 F103@72MHz 的物理上限,不是配置技巧能突破的。

修复与验证:ARR 恢复 83。若确实需要 2MSPS 以上,出路只有换外扩并行 ADC,或改用 ADC 时钟更充裕的型号(如 F4 的 2.4MSPS 单核 + 交错 4.8MSPS)。边界条件摸清楚后,这个"上限"反而成了设计依据:需求超过 1.7MSPS 的项目,方案评审阶段就该否掉 F103 单芯片路线。

八、总结

8.1 核心要点回顾

  • F103 的 ADC 标称 1MSPS 是按 14MHz ADCCLK 计算的纸面值,72MHz 主频下实际只有 857kSPS,所有采样率换算必须以实测为基准。
  • 双 ADC 交错模式(CR1.DUALMOD=0111)让 ADC2 硬件延迟 7 个 ADCCLK 自动跟随 ADC1,等效采样率翻倍至 1.71MSPS,代价是仅支持单通道且必须手写寄存器。
  • 数据打包关系:ADC1 结果在 DR 低半字、ADC2 在高半字,DMA 必须配 32 位宽度,拆包顺序用直流+斜坡两个注入实验确认。
  • 触发间隔硬下限为一次完整转换周期(14 ADCCLK),触发频率上限 857kHz,等效采样率上限 1.71MSPS,无法通过拉高触发突破。

8.2 适用边界与已知局限

这套方案的适用前提:单通道采集、等效采样率需求在 1M~1.7MSPS 之间、信号源阻抗可控(≤1kΩ 或允许放宽采样时间)。它不适用于多通道同步采集(两个转换器都锁定在同一通道上)、也不适用于 2MSPS 以上需求。已知局限有三:交错模式的镜像频段对两路失配敏感,通道一致性差时 fs/2 附近底噪会抬升;VDDA 清洁度对 ENOB 影响显著,电池/线性电源几乎是必需品;DMA 缓冲与处理之间的节奏需要用半满/全满双缓冲管理,缓冲过小会丢批。

8.3 扩展方向

下一步可以往三个方向走:一是把拆包后的样本流接入环形缓冲+FFT,做成完整的频谱仪前端;二是研究注入同步模式(DUALMOD=0011),实现两路不同通道的零相位差同步采样,适合电机双电阻电流采样;三是对比外部 16 位 SAR ADC(如 ADS8688)方案,量化"F103 内置交错 vs 外扩高精度"在成本与性能上的真实分界线。

相关阅读:《STM32 双ADC模式(交错模式)》 --- 双 ADC 模式的 HAL 配置路线,可与本文的寄存器路线互为印证;《简说 STM32的ADC》 --- TRGO/EXTSEL 触发链路的寄存器级推导。

如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员

版本备注

  • 硬件平台:STM32F103C8T6 最小系统板 + FY6900 函数信号发生器 + 100MHz 手持示波器
  • 软件版本:STM32CubeMX 6.12.0 + STM32CubeF1 HAL 库 V1.8.5 + Keil MDK 5.38(ARMCLANG V6)
  • 兼容说明:适用于 F103 全系带 ADC2 的型号(C8/RB/RC/ZE 等),无 ADC2 的 C4/C6 低容量款不适用;双 ADC 位定义与 F4 系列不同(F1 在 CR1.DUALMOD3:0,F4 在 CCR.DUAL4:0),代码不可直接移植
  • API 变更风险:HAL_ADC_Start_DMA 的长度参数按 DMA 数据宽度计数,V1.8.x 配 32 位宽度时传字数;从 V1.6.x 及以前的单 ADC 半字模板迁移时需同步修改 CNDTR 与缓冲区类型,否则会出现本文故障 1 的减半现象

参考资料

  1. STMicroelectronics, 《STM32F10x Reference Manual (RM0008)》第 11 章 Dual ADC mode、Table 66 外部触发映射、Table 78 DMA 请求映射
  2. STMicroelectronics, AN3116《STM32 ADC modes and their applications》--- 双 ADC 各模式的官方应用说明
  3. STMicroelectronics, AN2834《How to get the best ADC accuracy in STM32 microcontrollers》--- 精度优化与噪声归因方法
相关推荐
朝发如雪1 小时前
快速掌握单片机基础(5)(ARM基本概念)
单片机·arm
Industio_触觉智能2 小时前
瑞芯微RK3576开发板部署模型|端侧Agent工具调用实现拆解
嵌入式硬件·agent·智能硬件·开发板·瑞芯微·端侧部署·rk3576
星栖与芯10 小时前
LiteOS-M 切换汇编逐行图解(4):中断三件套与 HalTaskSchedule 触发
汇编·stm32·单片机·嵌入式硬件·harmonyos
G***技17 小时前
告别物联网碎片化:杰和LH707+LM2-100-V0联合方案给出标准答案
人工智能·嵌入式硬件
SKH.17 小时前
ARM汇编
嵌入式硬件·arm
深圳市恒锐丰科技杨生18 小时前
EG2134 300V 三相独立半桥栅极驱动芯片|屹晶 EGmicro
嵌入式硬件·硬件工程
朝发如雪18 小时前
快速掌握单片机基础(1)(嵌入式基本概念)
单片机
szxinmai主板定制专家18 小时前
【嵌入式实战】RK3588+GMSL多路相机采集方案|SerDes长距离低延时视频流落地教程
人工智能·单片机·机器人·边缘计算·zynq
LCG元18 小时前
STM32F103 SPI 驱动 W5500 以太网模块实战:硬件协议栈寄存器体系、Socket 状态机避坑与 TCP 吞吐实测
stm32·单片机·嵌入式硬件