简介:这份资料围绕STM32F373的SDADC(逐次逼近型模数转换器)展开,面向嵌入式开发者和工业控制方向的学习者,帮助理解16位高精度模拟采集的配置方法,以及双SDADC同步采样、多通道扫描等实际用法。包内共159个文件,以C源文件(stm32f37x_sdadc.c等)、头文件为主,配合hex/axf固件输出、uvproj工程文件和map映射文件,便于直接打开工程查看初始化流程与中断处理逻辑,约3.16MB,结构紧凑。已有1800余人学习,适用于传感器数据采集、环境监测、医疗设备等需要同时读取多路模拟量的场景。通过示例工程可以掌握单次/连续转换触发方式、采样时间设置、校准与噪声抑制功能,以及双SDADC并行采集时如何保证采样率一致,为实际项目中的多通道同步测量提供可直接借鉴的代码框架与调试思路。
1. 为什么STM32F373的SDADC值得单独写一篇:标称16位,上电却读不出16位
STM32F373 这颗芯片最吸引人的不是 72MHz 主频,而是集成了两个独立的 Σ-Δ 型 ADC(SDADC1 / SDADC2),标称分辨率 16 位,输出寄存器却是 24 位,专门用来吃高精度传感器这类场景:称重、桥式压力、电流检测、热电偶冷端补偿、电池电压监测。但几乎每一个第一次接触 F373 SDADC 的人都会遇到同一个现象:按普通 ADC 那套「使能、启动、读寄存器」流程操作,读回来的数据跳动很大,有效分辨率也就 12 位左右。问题几乎都不在芯片本身,而在参考电压选择、增益搭配、滤波阶数和偏移校准这四件事上没配对。这篇文章就是把这四件事逐一拆开,给出一套能在开发板上直接复现的配置方法和参数边界。
2. 看懂F373的SDADC数据通路:从差分输入到24位原始码要过几道关
SDADC 不是逐次逼近型 ADC,它是 Σ-Δ 调制器加数字抽取滤波器的组合。要调好它,得先知道数据在芯片内部走哪条路,否则出了问题你连该查哪一段都不知道。
2.1 两条独立转换链:SDADC1与SDADC2的通道差异
F373 的 SDADC 一共有两个独立的单元,简称为 SDADC1 和 SDADC2。每个单元前面有一个多路输入开关,可以从一组外部输入中选一个通道做转换。外部输入既可以配成单端输入,也可以配成差分输入对;差分输入在低噪声场景下几乎是必选,因为共模噪声会被调制器直接抑制掉。
每个 SDADC 单元内部还带两个特殊通道:一个接内部温度传感器,一个接内部参考电压。这意味着你不需要额外占用外部引脚就能做粗略的温度监测和参考电压自检。两路 SDADC 在寄存器层面完全独立,可以分别设置不同的增益、不同的滤波阶数,这就给了你「一路跑快、一路跑准」的搭配空间。
需要特别提醒的是,SDADC 的输入通道编号并不一定等于引脚编号。外部通道对应的具体引脚,取决于封装和该器件的复用功能映射表。同一个通道号在不同封装上可能落在不同引脚上,所以拿到一块新板子,第一件事是按型号和封装查数据手册里的 AF 映射,而不是凭经验接。
2.2 参考电压、增益与时钟:决定分辨率的三组关键变量
SDADC 的输出满量程由参考电压 VREF 和输入增益共同决定,可选的参考源有三类,各有适用场景。我在项目里最常见的做法是优先用内部参考,原因后面避坑章会细说。
| 参考源 | 满量程范围 | 适用情况 |
|---|---|---|
| VDDA | ±VDDA / 增益 | 3.3V 电源较干净、对精度要求不极端时 |
| 内部参考电压 | ±内部VREF / 增益 | 对 VDDA 噪声敏感,追求稳定有效分辨率时推荐 |
| 外部 VREF 引脚 | ±外部基准 / 增益 | 需要与外部基准源保持一致时 |
增益档位常见有 0.5、1、2、4 四档。增益越高,满量程越小,对应同样一位 LSB 代表的电压越小。但增益高不意味着精度一定高,因为输入信号的直流偏置和噪声同样会被放大。小信号传感器可以先选 4 倍增益,而像电池电压这种本身就接近满量程的信号,选 1 倍甚至 0.5 倍更合适。
时钟这块是最容易被忽略的。SDADC 的调制器时钟 fMOD 不是随便取值的,芯片要求 fMOD 落在 1.28MHz 到 6.4MHz 之间。以 72MHz 系统时钟为例,APB2 也是 72MHz,经 SDADC 时钟分频后,只有÷16 得到 4.5MHz、÷32 得到 2.25MHz 两个选项是合法的。
这个约束直接决定了你最终能拿到的输出数据率。数据率的上限就是 fMOD 除以滤波器的抽取倍数,抽取得越多,数据越干净,但每秒能取到的样本数越少。所以「高精度」和「高速度」在 SDADC 上是同一个时钟资源上的取舍,不是两个独立可调的东西。
2.3 数字滤波器与输出码制:FORD 参数到底是什么
Σ-Δ 调制器输出的是一路高速 1bit 流,必须经过数字抽取滤波器才能变成可读的二进制码。F373 内部集成的是一组 sinc 滤波器,阶数由 FORD 字段控制,可选 1 到 4 阶。FORD 选得越高,阻带衰减越陡,对带内噪声压制越狠,但建立时间也更长。
输出数据率的计算公式可以写成:
fDATA = fMOD /(128 × FORD)
以 fMOD = 2.25MHz 为例:FORD=1 时约 17.6kSPS,FORD=2 时约 8.8kSPS,FORD=3 时约 5.9kSPS,FORD=4 时约 4.4kSPS。同一个公式翻过来用,就是你评估系统吞吐量的依据。
输出数据寄存器是 24 位,差分输入下采用二进制补码。0 附近代表差分输入接近共模电平,正满量程和负满量程分别对应正向和反向的最大差分电压。很多人第一次读回数据发现是「负数」,其实不是采样错误,而是补码本来就有符号。后面换算物理量时,一定要把这个符号保留下来。
3. 用HAL把STM32F373 SDADC跑起来:初始化、通道配置与读取的最小代码
这一章直接给可编译的 HAL 风格代码。F373 在 Cube 生态里对应的是 STM32CubeF3 固件包,SDADC 外设驱动位于stm32f3xx_hal_sdadc.h/.c。不同小版本的 HAL 头文件,字段名可能略有差异,编译报错时以你本机的头文件为准。
3.1 初始化前的四步准备:时钟、GPIO复用、参考电压与使能顺序
SDADC 是模拟外设,初始化顺序比普通数字外设讲究。我习惯把它拆成四步。
第一步,打开 SDADC 外设的时钟。SDADC1 挂在 APB2 总线上,使能后还建议做一次外设复位,确保寄存器回到干净状态。第二步,把实际用到的输入引脚配置为模拟输入模式。具体引脚号查数据手册的 AF 映射表,不同封装不一样。第三步,预留 2 到 5 毫秒的延时,让模拟部分供电稳定。第四步,再配置参考电压和时钟分频,最后使能 SDADC。
顺序上,先复位再延时再配置,能避开不少上电瞬间的偶发异常。有些人图省事把所有初始化写在一起,结果就是同一份代码,有的板子正常,有的板子首次读数明显偏大,重启又好了。这类「玄学」问题多半是模拟域还没稳定就开始转换。
3.2 最小驱动代码:启动单通道差分转换并读取24位原始码
下面这段代码是我在一个模拟项目里用的最小模板:初始化 SDADC1,配置 0 号外部通道为差分模式,读取一次原始码。它不包含 DMA,适合先验证硬件通路。
#include "stm32f3xx_hal.h" static SDADC_HandleTypeDef hsdadc1; /* 初始化SDADC1,使用内部参考,时钟分频32 */ static void SDADC_InitHardware(void) { __HAL_RCC_SDADC1_CLK_ENABLE(); /* 打开SDADC1的APB2时钟 */ hsdadc1.Instance = SDADC1; hsdadc1.Init.ClockDiv = SDADC_CLOCK_DIV32; /* 72MHz/32 = 2.25MHz */ hsdadc1.Init.VrefMux = SDADC_VREF_INTERNAL; /* 使用内部参考电压 */ hsdadc1.Init.FastMode = SDADC_FAST_MODE_DISABLE; hsdadc1.Init.LowPowerMode = SDADC_LOW_POWER_DISABLE; /* 如果当前CubeF3版本的Init结构体里没有ScanMode字段, 就把这一行删掉,扫描模式在ConfigChannel阶段配置 */ hsdadc1.Init.ScanMode = SDADC_SCAN_MODE_SINGLE; if (HAL_SDADC_Init(&hsdadc1) != HAL_OK) { while (1); /* 初始化失败:查时钟和GPIO */ } } /* 读取指定SDADC通道一次,返回24位原始码 */ static int32_t SDADC_ReadOnce(uint32_t channel) { SDADC_ConfTypeDef conf = {0}; conf.InputMode = SDADC_INPUT_MODE_DIFF; /* 差分输入 */ conf.Gain = SDADC_GAIN_1; /* 1倍增益,满量程±内部参考 */ conf.Offset = 0; /* 校准后回填 */ conf.CommonMode = SDADC_COMMON_MODE_NONE; /* 差分模式无需共模设置 */ if (HAL_SDADC_ConfigChannel(&hsdadc1, &conf, channel) != HAL_OK) { return -1; /* 通道参数非法 */ } if (HAL_SDADC_Start(&hsdadc1, channel) != HAL_OK) { return -1; } if (HAL_SDADC_PollForConversion(&hsdadc1, 100) != HAL_OK) { HAL_SDADC_Stop(&hsdadc1); return -1; /* 超时未完成 */ } int32_t raw = HAL_SDADC_GetValue(&hsdadc1); HAL_SDADC_Stop(&hsdadc1); return raw; }代码的逻辑并不复杂,但有几个参数要特别说明。ClockDiv选择 ÷32,fMOD 为 2.25MHz,这是前面算过的最稳妥档位。VrefMux选内部参考,目的是绕开 VDDA 上的数字噪声。Gain选 1 倍,适合先确认链路是否通,之后再根据信号幅度调整。
PollForConversion的超时参数,我习惯给 100 毫秒。这个值远大于单次转换时间,超时基本只会在硬件没接好或者时钟配置错误时出现,不会误触发。GetValue返回的是 24 位有符号数,直接赋值给 int32_t 即可。
3.3 偏移校准与电压换算:从原始码到能用的物理量
SDADC 最容易被忽略的是偏移误差。哪怕输入差分对短接在一起,读出来的数也不是 0,可能偏几十甚至几百个 LSB。这个误差来自调制器的直流偏置、输入开关的导通压差和 PCB 布线不对称。
F373 内部提供了硬件校准触发位,可以按参考手册流程启动。但我实际项目里更常用的是软件标定法:把正负输入端在外部短接,读若干次数取平均,把这个平均值回填到通道配置的Offset字段。这样做的好处是它连 PCB 上的失配一起补偿了。
/* 外部短接差分输入端后,采集16次取均值作为偏移补偿 */ static int32_t SDADC_CalibrateOffset(uint32_t channel) { SDADC_ConfTypeDef conf = {0}; int64_t sum = 0; int32_t sample; conf.InputMode = SDADC_INPUT_MODE_DIFF; conf.Gain = SDADC_GAIN_1; conf.CommonMode = SDADC_COMMON_MODE_NONE; conf.Offset = 0; HAL_SDADC_ConfigChannel(&hsdadc1, &conf, channel); for (int i = 0; i < 16; i++) { sample = SDADC_ReadOnce(channel); if (sample < 0) sample = 0; /* 防止异常值混入 */ sum += sample; } conf.Offset = (int16_t)(sum / 16); HAL_SDADC_ConfigChannel(&hsdadc1, &conf, channel); return conf.Offset; }校准完成后,电压换算要用带符号的原始码。差分输入下,满量程对应正负参考电压除以增益。最可靠的做法不是背公式,而是用两点标定:短接输入测偏移,再接一个已知精密电压测斜率。斜率单位是伏每 LSB,之后任意读数乘斜率加偏移,就是实际电压。
提示:如果
Offset字段在你当前的 HAL 头文件里是 uint16_t,注意强制转换时保留补码符号位,不要把负数变成很大的正数。
4. SDADC 可调参数怎么选:采样率、滤波阶数与扫描模式
初始化代码能跑通之后,下一步就是针对实际需求调参数。这里最容易犯的错是把 SDADC 当成普通 ADC,什么参数都往最大精度调,最后发现采样率不够用,或者数据周期性地带上一个毛刺。
4.1 采样率与有效分辨率的权衡:FORD 应该怎么选
以 fMOD = 2.25MHz 为例,FORD 与输出数据率的关系如下表。看这张表的时候,要把「数据率」理解为最终每秒能拿到多少个转换结果,而不是调制器速度。
| FORD 阶数 | 输出数据率 @ fMOD=2.25MHz | 典型场景 |
|---|---|---|
| 1 阶 | 约 17.6 kSPS | 动态信号监测、快速巡检 |
| 2 阶 | 约 8.8 kSPS | 通用传感器采样 |
| 3 阶 | 约 5.9 kSPS | 称重、压力、温度采集 |
| 4 阶 | 约 4.4 kSPS | 需要最干净输出的精密测量 |
工程上的实际选择逻辑是这样的:如果信号带宽只有几赫兹,比如称重传感器的输出经过了一级低通滤波,那直接选 4 阶,把噪声压到最低。如果还要监测 100Hz 级别的振动信号,选 4 阶就明显不够,1 阶或 2 阶更实际。
这里有个常见的误解:FORD 选 4 阶,等效分辨率一定比 1 阶高吗?不完全是。4 阶 sinc 滤波器对带外噪声抑制更好,但建立时间更长。如果系统在频繁切换通道,或者信号本身带有快速变化的阶跃,高阶滤波器反而会把阶跃响应拉长,造成动态误差。所以选阶数要看整个链路的信号特征,不能只看静态噪声指标。
4.2 通道切换与扫描模式:时序比参数更值得花时间
当系统需要采集多个通道时,SDADC 支持扫描模式,也就是按通道序号依次转换。扫描模式本身不复杂,复杂的是切换通道后的数据时序。
sinc 滤波器有建立时间,切换到一个新通道后,滤波器的内部状态还残留着上一通道的信息,前几个转换结果是不准的。这个现象在 FORD 越高时越明显。我常用的做法是:每次切换通道后,丢弃前 2 到 4 个样本,再从后续数据里取值。如果用的是中断方式,就在断点里数样本个数,前几个只读不用。
扫描模式的周期要这样估算:总时间等于通道数乘以单通道转换时间,再乘以每个通道需要丢弃的样本数。比如 4 个通道、FORD=3、每个通道丢弃 2 个样本,实际有效数据率会明显低于单通道的理论值。设计系统吞吐量时,一定要把这部分冗余算进去,否则留给上层算法的数据量会不够。
4.3 DMA搬运与缓冲设计:连续采集不丢数据的关键
多通道连续采集时,CPU 逐个读取会占用大量时间,而且容易漏掉转换完成事件。DMA 是标准解,但缓冲区和传输长度的设计有讲究。
/* 连续扫描4个通道,每个通道采集8次,DMA搬运 */ #define SDADC_CH_COUNT 4 #define SDADC_SAMPLE_COUNT 8 static uint32_t scan_buf[SDADC_CH_COUNT * SDADC_SAMPLE_COUNT]; /* 配置SDADC进入连续扫描模式后启动DMA */ HAL_SDADC_Start_DMA(&hsdadc1, scan_buf, SDADC_CH_COUNT * SDADC_SAMPLE_COUNT);DMA 的传输单位必须是 32 位,因为 SDADC 的数据寄存器是 24 位右对齐,按 16 位搬运会把数据截断。缓冲区长度是通道数乘以每通道样本数,这一点看起来简单,但实际项目里经常出现只按通道数配了缓冲区,导致 DMA 写越界的情况。
启动 DMA 后,我一般会在 DMA 传输完成中断里取数据,然后立即启动下一轮。如果系统里中断负载高,改用 DMA 半传输中断也可以,但要注意半传输中断的边界正好落在哪个通道上,否则数据处理逻辑会把两组不完整的数据拼在一起。
注意:连续扫描模式下,每一轮扫描结束后立即开始下一轮,中间没有任何空闲间隔。如果你的应用需要精确等间隔采样,这种模式反而合适;如果需要每轮之间留出处理时间,就要用单次扫描模式手动触发。
5. SDADC 避坑指南:我从 F373 上踩过的五个实测问题
以下问题都来自实际调试过程,每条按「现象、原因、解决」三要素写。如果你在 F373 上读出异常数据,对照这一章排查,通常能省下大半天时间。
5.1 数据噪声大、有效分辨率上不去:查参考电压和引脚状态
现象:同一通道重复采集,结果跳动几百个 LSB,有效分辨率连 14 位都不到。把增益调低、滤波阶数调高,改善也有限。
原因有两类。第一类是参考电压选了 VDDA,而 VDDA 上有数字电路耦合过来的噪声,等于把电源噪声直接当作基准引入测量链路。第二类是外部引脚没有接任何信号源,处于悬空状态,差分输入本身没有确定电平,读出来的完全是噪声。
解决:参考电压优先切换为内部参考。引脚方面,未使用的 SDADC 输入引脚不要悬空,要么接地,要么在代码里不使能对应通道。测量时先用一个干净信号源接入差分输入,确认噪声水平正常后再接真实传感器。
5.2 首次上电读数明显偏移:偏移校准没做或做晚了
现象:同一套硬件、同一份代码,第一次上电读出来的数值比正常值偏大,复位之后又恢复正常。或者冷启动和热启动的读数对不上。
原因:SDADC 的模拟偏置在上电后需要一段时间才能稳定,而且内部偏移误差是逐芯片不同的,没做校准就直接使用,等于把这个误差原样带进了测量结果。
解决:每次上电初始化完成、模拟域稳定后,先执行一次偏移校准,再开始正式采集。外部短接条件允许的话,就用上一章的软件标定法;不允许的话,也要触发一次内部校准流程。校准值最好能存到非易失区,避免每次上电都重复同样的校准操作省时间。
5.3 通道切换后第一笔数据周期性异常:滤波器还没建立完成
现象:扫描模式下,每个通道的第一个样本总是明显偏离真实值,后面的样本正常。如果用 DMA 连续搬运,看起来就像数据里混进了周期性的毛刺。
原因:sinc 滤波器在切换通道后需要几个采样周期才能冲刷掉上一通道的状态。FORD 越高,需要的周期越多。DMA 拿到的是滤波器当前输出,不会替你做这个清洗动作。
解决:在软件里丢弃每个通道的前几个样本,再取有效值。样本数按 FORD 调整,FORD 越高多丢几个。这个坑最容易在「扫描模式 + DMA + 只看最终结果」的组合里被忽略,因为在单通道模式下不容易复现。
5.4 读回数据恒为 0 或满码:查差分接法和增益配置
现象:某个通道读回来不是 0 就是接近满量程的固定值,调整输入电压没有响应。
原因:差分输入接反了,或者输入共模电压超出允许范围。接反时,正负信号颠倒,输出变成负满量程,看起来像满码。另一个可能原因是增益档位和参考电压搭配后,满量程远小于实际输入信号,输入早就饱和了。
解决:先用示波器确认两个差分引脚的电压关系,区分是接反还是共模超限。然后把增益调到最低档重新测试,排除饱和问题。代码里把所有通道配置的增益统一打印出来,检查有没有哪个通道用了之前残留的配置。
5.5 扫描 + DMA 丢数据:缓冲区长度和触发时机没对齐
现象:DMA 传输完成中断触发后,解析缓冲区的数据,发现通道顺序错乱,或者某一个通道的数据偶尔缺失。加大超时时间没有用。
原因:扫描模式的通道数量和 DMA 传输长度不一致。DMA 是按固定长度搬运的,如果长度不是通道数的整数倍,一轮扫描的数据就会截断在中间,下一轮从错误的位置开始,表现为通道错位。
解决:缓冲区长度严格等于通道数乘以每通道采集次数,并且这个乘积保持不变。每次修改扫描通道数,同步修改 DMA 长度。调试时在缓冲区头部放一个起始标志,用第一通道的已知特征值校验帧对齐,能快速确认是不是这个问题。
6. 把 SDADC 用得更稳:两点标定与比率测量的实战技巧
6.1 两点标定消除增益误差:让读数在高温下也准确
SDADC 的增益误差和内部参考电压的温度系数,单靠校准偏移补偿不掉。我习惯在硬件调试阶段做一次两点标定:差分输入端短接,记下偏移;再输入一个已知精密电压,记下读数。两点就能算出斜率和截距,标定结果存到片上 Flash,上电后读出来修正每个转换结果。
计算公式就是最简单的线性修正:实际值 = (原始码 - 偏移) × 斜率。斜率单位是伏每 LSB,这一步比查手册理论公式可靠得多,因为它把 PCB 走线、接插件接触电阻的误差一起吸收掉了。做完标定,整个链路就是一个可以量化的测量系统,后面换传感器或者改增益,只需要重做一次标定。
6.2 比率测量消除参考漂移:把 VREF 的波动从结果里抹掉
如果系统里参考电压本身会随温度或负载漂移,最有效的办法是比率测量:用同一个 SDADC 轮流采参考电压通道和信号通道,计算比值而不是绝对值。这样参考漂移在分子分母上同时出现,被约掉。
F373 的 SDADC 自带内部参考通道,正好可以拿来当这个比值参考。采样时序上,我会先采参考通道,再采信号通道,两者间隔控制在几十毫秒内,保证漂移量小到可以忽略。这个方法实现成本极低,但对长期稳定性的提升非常明显。它也是我每次做精密采集项目时的默认手段,比单纯堆滤波阶数更管用。
一个曾经的教训:我在某个模拟项目里,因为没在通道切换后丢弃前两个样本,产线测试数据每隔固定周期就出现一次偏大,排查了整整两天才定位到是 sinc 滤波器建立时间的问题。从那以后,凡是 SDADC 做多通道扫描,我都会先把丢弃逻辑写好再去调其他参数。希望这篇 STM32F373 SDADC 的落地笔记能帮你绕开这些我已经踩过的坑,把时间花在真正影响结果的地方。
本文还有配套的精品资源,点击获取