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

摘要

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 以字为单位搬进内存缓冲区,最后软件拆包还原成交错样本流。

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_buf[2048]

交错样本流
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 寄存器 ADCPRE[1:0]=10)。算下来:

单次转换时间 = 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 寄存器的 DUALMOD[3:0](bit19:16)选择,共 10 种组合。交错模式对应编码 0111:每次触发事件启动 ADC1 转换,ADC2 由硬件自动延迟 7 个 ADCCLK 后对同一通道启动采样,不需要任何软件参与。

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

交错采样间隔 = 7 / 12MHz = 0.583μs
等效采样率 = 12MHz / 7 = 1.714 MSPS

用一次触发周期内的时序来看两路转换器如何"咬合":

DMA1_Ch1 ADC2 ADC1 TIM3 TRGO DMA1_Ch1 ADC2 ADC1 TIM3 TRGO 触发周期=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交错+DMA1.71MSPS~0%仅单通道,需手写寄存器
外扩并行ADC(如AD9226)20MSPS+取决于接口PCB布线、电源、成本全面上升

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

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

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

为什么触发源选 TIM3_TRGO 的 Update 事件。 交错模式要求触发间隔严格等长,软件触发和 ADC 连续模式都做不到这一点(连续模式的节拍由 ADC 内部状态机决定,且无法与 ADC2 的跟随逻辑对齐)。定时器触发链路是纯硬件的:TIM3 计数到 ARR 溢出产生 Update 事件,经 CR2 的 MMS[2:0]=010 选通为 TRGO 输出,直接送到 ADC 的触发输入。RM0008 Table 66 列出了外部触发源与 EXTSEL[2: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 1ADC1
Channel 2SPI1_RX / TIM1_CH1 等
Channel 3SPI1_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必须共地,否则叠加在信号上的共模漂移会以采样率为周期呈现
电源开发板 5VUSB 供电观测纹波场景建议用电池或线性电源,开关电源的自身纹波会耦合进 ADC 参考电压
去耦VDDA 与 GND 之间100nF + 1μF 各一颗VDDA 的清洁度直接决定 ENOB 上限

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

五、软件设计

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

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

外设配置项取值对应寄存器/字段
RCCADC 预分频PCLK2/6 = 12MHzRCC_CFGR 的 ADCPRE[1:0]=10
TIM3预分频/自动重装PSC=0,ARR=8372MHz/84 = 857.14kHz 触发频率
TIM3触发输出Update 事件CR2 的 MMS[2:0]=010
ADC1触发源TIM3_TRGOCR2 的 EXTSEL[2:0]=011(Table 66),EXTTRIG=1
ADC1采样时间1.5 周期SMPR2 的 SMP0[2:0]=000
ADC1扫描模式关(单通道)CR1 的 SCAN=0
ADC1DMA 请求使能CR2 的 DMA=1
★ ADC1双ADC模式交错模式CR1 的 DUALMOD[3:0]=0111,手动写入
ADC2触发源无外部触发CR2 的 EXTSEL 保持复位值,EXTTRIG=0
ADC2采样时间1.5 周期(与ADC1一致)SMPR2 的 SMP0[2:0]=000
DMA1_Ch1数据宽度外设32位/内存32位CCR 的 PSIZE[1:0]=10,MSIZE[1: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 不联动"的形式暴露,下图是实测验证过的安全顺序:

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 核心代码实现

/* 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μs1.167μs<0.1%
单ADC有效采样率857.1 kSPS856.8 kSPS-0.03%HSE晶振±20ppm、ARR取整
交错等效采样率1714.3 kSPS1713.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.514 cyc83857.1kHz1.714 MSPS10.3
7.520 cyc119600.0kHz1.200 MSPS10.6
13.526 cyc155461.5kHz0.923 MSPS10.8
28.541 cyc245292.7kHz0.585 MSPS11.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,发现 PSIZE[1: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 发现 DUALMOD[3: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 mV10.3无感
源阻抗 1kΩ9 mV9.8轻度劣化
源阻抗 10kΩ38 mV8.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.DUALMOD[3:0],F4 在 CCR.DUAL[4: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》— 精度优化与噪声归因方法
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LCG元

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值