文章目录
-
- 摘要
- 前言
- 一、项目概述
-
- [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 的减半现象
参考资料
- STMicroelectronics, 《STM32F10x Reference Manual (RM0008)》第 11 章 Dual ADC mode、Table 66 外部触发映射、Table 78 DMA 请求映射
- STMicroelectronics, AN3116《STM32 ADC modes and their applications》--- 双 ADC 各模式的官方应用说明
- STMicroelectronics, AN2834《How to get the best ADC accuracy in STM32 microcontrollers》--- 精度优化与噪声归因方法