文章目录
- 前言
- 一、逐次逼近型ADC
-
- [1.1 ADC是什么](#1.1 ADC是什么)
- [1.2 单片机里的ADC](#1.2 单片机里的ADC)
- [1.3 ADC的分辨率](#1.3 ADC的分辨率)
- [1.4 逐次逼近型ADC简介](#1.4 逐次逼近型ADC简介)
- [1.5 采样保持电路](#1.5 采样保持电路)
- [1.6 逐次逼近型 ADC示例](#1.6 逐次逼近型 ADC示例)
- 二、ADC模块的结构框图
-
- [2.1 ADC的多路复用](#2.1 ADC的多路复用)
- [2.2 ADC的常规序列](#2.2 ADC的常规序列)
- [2.3 ADC的注入序列](#2.3 ADC的注入序列)
- [2.4 常规序列和注入序列优先级的说明](#2.4 常规序列和注入序列优先级的说明)
- 三、采样时间和转换时间
-
- [3.1 ADC的时钟频率](#3.1 ADC的时钟频率)
- [3.2 转换时间的计算方法](#3.2 转换时间的计算方法)
- [3.3 采样时间和信号源内阻的关系](#3.3 采样时间和信号源内阻的关系)
- [3.4 信号源内阻的计算方法](#3.4 信号源内阻的计算方法)
- [3.5 采样时间的计算方法](#3.5 采样时间的计算方法)
- 四、常规序列单通道转换
-
- [4.1 实验介绍](#4.1 实验介绍)
- [4.2 实验顺序](#4.2 实验顺序)
-
- [4.2.1 **步骤一:初始化IO引脚**](#4.2.1 步骤一:初始化IO引脚)
- 4.2.2**步骤二:配置ADC的时钟**
- [4.2.3 **步骤三:初始化ADC的基本参数**](#4.2.3 步骤三:初始化ADC的基本参数)
- [4.2.4 步骤四:配置常规序列](#4.2.4 步骤四:配置常规序列)
- [4.2.5 步骤五: 启动并读取转换结果](#4.2.5 步骤五: 启动并读取转换结果)
- 五、注入序列单通道转换
- 总结
- 附录
前言
ADC是STM32F1重要的片上外设,本文对逐次逼近型ADC,ADC模块的内部结构,采样时间和转换时间做了介绍。
此外,介绍了常规序列和注入序列,并分别做实验进行了验证,代码放在附录。
本文作为学习笔记,参考B站UP"铁头山羊视频"所写。
一、逐次逼近型ADC
1.1 ADC是什么

首先来看什么是模拟信号和数字信号
以气温为例,自然界中温度是随时间一直变化的,它连续不断,这种就是模拟信号。
气象站测量一天温度,会在每个时间段测量温度,然后记录,这样记录的温度是离散的,这种就是数字信号。
ADC(Analog-Digital Converter)模拟-数字转换器,就是一座桥梁。它的作用是将外部连续变化的模拟电压信号,按照一定的精度,转换成单片机能听懂的数字数值。
1.2 单片机里的ADC
STM32F1单片机内部有ADC外设,功能十分强大。
例如,STM32F103C8T6 它的ADC资源:ADC1、ADC2,10个外部输入通道。
它的主要参数为:
- 精度(分辨率) :12位,逐次逼近型ADC。意味着它把 0V~3.3V 分成了 2 12 = 4095 2^{12} = 4095 212=4095份,数值范围0~4095。
- 输入电压范围:0~3.3V,如果我们要测量大于3.3V的电压,可以通过串联电阻分压的方法来测量,绝对不能直接高于3.3V的电压!
- 通道数:最多18个通道(16个外部GPIO引脚 + 1个内部温度传感器 + 1个内部参照电压)。
- 转换模式:单次转换(测一次就停);连续转换(测完一次马上测下一次,不停歇);扫描模式(一次性测量多个通道)。
1.3 ADC的分辨率

"采样深度"通常指的是ADC的分辨率(Resolution),也叫位宽(Bit Depth)。它决定了ADC测量的"精细程度"。
我们可以把它想象成一把尺子。
假设要测量 0V 到 3.3V 的电压:
(1)如果采样深度很低(比如 2-bit) ,系统只能把 0-3.3V 分成 2 2 = 4 2^2 = 4 22=4份,尺子的刻度非常稀疏:0 V,1.1 V,2.2 V,3.3 V。如果输入1.5 V,ADC可能读出"1.1V"或者"2.2V",误差巨大。
(2)如果采样深度很高(比如 STM32F1 的 12-bit) ,系统把 0-3.3V 分成了 2 12 = 4096 2^{12} = 4096 212=4096份,尺子刻度非常紧密,它能感知到极微小的电压变化。
ADC的采样深度越深,转换的结果越精细,采样深度是衡量ADC性能的重要指标。
1.4 逐次逼近型ADC简介

ADC的类型如上图所示,我们用的STM32F1(以及F4、H7等绝大多数单片机)内部集成的ADC,全部都是逐次逼近型的。
逐次逼近型ADC(Successive Approximation Register ADC,简称 SAR ADC)是嵌入式领域最常用的一种ADC架构。

逐次逼近型ADC的工作原理,本质上就是算法里的二分查找。
下面,我们以天平称重来理解它的工作原理
左盘放着未知的物品(比如69g)。
右盘放着砝码,由于我们不知道物品的重量,我们先放最重的砝码 ,也就是50g的砝码。这样处理的结果,天平往左倾斜。保留这个砝码,最高位记为 1。
接着,我们再加一个次重的砝码(25g),现在右盘总重 = 50 g + 25 g = 75 g = 50g + 25g = 75g =50g+25g=75g,右盘重量大于左盘,天平右倾。我们要拿走这个砝码,第二位记为 0。
再加一个更轻的砝码(12.5g),现在右盘总重 = 50 g + 12.5 g = 62.5 g = 50g + 12.5g = 62.5g =50g+12.5g=62.5g 小于左盘,天平还是左倾,保留这个砝码,第三位记为 1。
最后加上最小的砝码(6.25g),现在右盘总重 = 50 g + 12.5 g + 6.25 g = 68.75 g = 50g + 12.5g + 6.25g= 68.75g =50g+12.5g+6.25g=68.75g 小于左盘,天平保持平衡微微左倾,可以认为左右两边近似相等,保留这个砝码,最低位记为 1。
我们得到的结果是1011,然后将这个数乘上对应的重量,就可以得出物品测出的重量68.75g。
从这个过程我们可以看出不同重量的砝码越多,测出的物体重量越精准,也就是单片机的ADC分辨率越高,转换的结果越精准。

STM32 芯片内部的 ADC 模块主要由三个部分组成来实现上述过程:
- 比较器 (Comparator) : 天平的指针。用来判断 V i n V_{in} Vin 是比 DAC电压大,还是小。
- 电压发生器: 砝码生成器。它负责产生每一次"试探"的电压。
- 结果寄存器: 操作员。它控制放哪个砝码,并记录最终的二进制数。
要比较电压的大小,首先要得到输入的模拟信号的大小,这一步由采样保持电路(Sample and Hold)完成,下面我们对它做出介绍。
1.5 采样保持电路

在STM32的芯片内部,ADC模块的入口处并不是直接连着那个复杂的"天平"(比较器),而是先连着一个微小的电容(通常只有几个皮法,pF级别)。
我们可以建立这样一个物理模型:
- 外部信号源:供水的大水管(电压高低代表水压大小)。
- 采样开关:水龙头(由单片机控制开关)。
- 内部采样电容 ( C A D C C_{ADC} CADC):一个小量杯。
- ADC转换核心 (SAR):称重员。

ADC对信号的处理过程,严格分成了两步:
第一阶段:采样 (Sampling) ------ "打开水龙头"
单片机闭合内部的采样开关,外部引脚(GPIO)与内部的小电容连通了。
外部的电荷开始向内部电容充电。内部电容的电压(水位)会迅速上升,直到和外部引脚的电压完全相等 。
这个过程不是瞬间完成的!它需要时间(采样时间)。
第二阶段:保持 (Holding) ------ "关上水龙头,开始称重"
采样时间结束,单片机断开采样开关。内部小电容与外部世界彻底隔绝了。
这时候,哪怕外部引脚的电压突然变了(比如从2V变成了3V),内部小电容里的电压依然保持在断开那一瞬间的值(2V)。
SAR(逐次逼近逻辑)开始测量这个小电容上的电压。因为它很稳定,所以测量结果会很准。
这个过程也需要时间(转换时间)。
采样保持电路的作用,就是为了给"逐次逼近"过程提供一个静止不动的电压样本。 就像用相机拍照,快门一按(采样),画面定格(保持),然后你再慢慢去洗照片(转换)。
1.6 逐次逼近型 ADC示例

第一步:采样与保持
假设本次输入信号的电压为2.21V,采样保持电路的开关闭合, C A D C C_{ADC} CADC 充电。
经过合适的采样时间后, C A D C C_{ADC} CADC的电压与输入电压相等,断开开关。由于"虚断",比较器的V+电压(即 C A D C C_{ADC} CADC的电压不变)。
第二步:逐步逼近输入电压
ADC模块开始工作,为了方便演示,我们假设结果寄存器一共有四位,将3.3V电压分成15份,每份大小为0.22V.最高位大小为 2 3 ∗ 0.22 v = 1.76 V 2^3 * 0.22v = 1.76V 23∗0.22v=1.76V,最低位大小为 2 0 ∗ 0.22 V = 0.22 V 2^0 * 0.22V = 0.22V 20∗0.22V=0.22V.
首先让 b 3 = 1 b3=1 b3=1,那么比较寄存器 V − = 1.76 V < V + = 2.21 V V_{-} = 1.76V < V_{+} = 2.21V V−=1.76V<V+=2.21V,所以保留这个砝码,b3记为1.
接着让 b 2 = 1 b2=1 b2=1,那么那么比较寄存器 V − = 1.76 V + 0.88 V = 2.64 V > V + = 2.21 V V_{-} = 1.76V+0.88V=2.64V > V_{+} = 2.21V V−=1.76V+0.88V=2.64V>V+=2.21V,所以拿走这个砝码,b2记为0.
然后让 b 3 = 1 b3=1 b3=1,那么那么比较寄存器 V − = 1.76 V + 0.44 V = 2.20 V < V + = 2.21 V V_{-} = 1.76V+0.44V=2.20V < V_{+} = 2.21V V−=1.76V+0.44V=2.20V<V+=2.21V,所以保留这个砝码,b1记为1.
最后让 b 4 = 1 b4=1 b4=1,那么那么比较寄存器 V − = 1.76 V + 0.44 V + 0.22 V = 2.42 V > V + = 2.21 V V_{-} = 1.76V+0.44V + 0.22V =2.42V > V_{+} = 2.21V V−=1.76V+0.44V+0.22V=2.42V>V+=2.21V,所以拿走这个砝码,b0记为0.
那么得结果寄存器里存储的值为1010,我们把它读出来,进行转换得出 1 ∗ 1.76 V + 0 ∗ 0.88 V + 1 ∗ 0.44 V + 0 ∗ 0.22 V = 2.20 V 1*1.76V + 0*0.88V + 1*0.44V + 0*0.22V = 2.20V 1∗1.76V+0∗0.88V+1∗0.44V+0∗0.22V=2.20V,ADC测得的电压为2.20V.
二、ADC模块的结构框图
2.1 ADC的多路复用
在STM32F1系列中,有ADC1 和 ADC2 两个外设资源 ,它们的关系非常像"双胞胎兄弟"。
从硬件结构 上看,它们的核心参数(12位精度、转换速度、SAR架构)是完全一样 的。但是,它们在功能定位和权限上有一些关键的区别。
简单总结就是:ADC1 是"老大哥"(主),ADC2 是"小老弟"(从) 。这里以ADC1来介绍STM32F1的ADC内部结构,它功能最全,配置最简单。

我们在介绍采样保持电路的时候说过有,内部这样一个开关,闭合开关,对输入信号进行采集。断开开关,电容 C A D C C_{ADC} CADC 两端电压不变,ADC模块逐渐逼近输入信号的大小。

我们将这个开关外提,把后面看成一个整体"逐次逼近型ADC"。

单片机不止一个引脚可以输入信号,我们把对应通道的开关闭合,就可以使用12位逐次逼近型ADC将模拟信号进行转换成数字信号,从结果寄存器中把数据读取出来即可。
比如,模拟信号1来了 ,我们闭合这一路开关,转换完成,将它从结果寄存器读取出来,断开开关;模拟信号2来了,我们将通道2开关闭合,等到转换完成,将它从结果寄存器读取出来。
这样,就实现了ADC的多路复用。

这里要注意,STM32F103C8T6的封装是LQFP-48 ,它没有PC0,PC1,PC2和PC3引脚 。

到此为止,上图红框部分内容介绍完毕,可以看出它没有通道10到通道13这几个引脚。
即12位逐次逼近型ADC的原理,采样与保持电路,多路复用。
上图所示为ADC的内部结构框图,上面为ADC的常规序列,下面为ADC的注入序列。其实二者的结构相差不大,接下来首先介绍ADC的常规序列。
2.2 ADC的常规序列

STM32 的 ADC 模块为了能同时有序地处理多个通道(比如同时测电压、温度、光照),把所有的测量任务分成了两个"工作组":
常规序列 (Regular Group) ------ 就像"普通挂号"。
注入序列 (Injected Group) ------ 就像"急诊插队"。

把ADC的常规序列(Regular Group)单独拿出来看,这是我们平时最常用的模式。
这就像银行柜台前的普通排队队伍 ,最多可以安排 16 个通道排队。我们可以规定谁排第一(Rank 1),谁排第二(Rank 2)。ADC 会老老实实地按顺序一个个测。
比如这张表里,我们规定1)通道7,采样时间0.1us;2)通道8采样时间0.2us;3)通道9,采样时间0.1us
那么单片机的ADC在受到外部触发后,会依次采样通道7,通道8,通道9.
注意:STM32F1的常规序列虽然能排16个号,但只有一个数据寄存器。
当第1个通道转换完,数据放入寄存器;紧接着第2个通道转换完,会直接覆盖掉第1个通道的数据。如果你用常规序列转换多个通道,必须配合 DMA,后续再做介绍,这里先不考虑。

比如我们选择ADC常规序列的外部触发为TIM3_TRGO,对TIM3,将PSC设为71,计数器设为上计数,那么每 1us CNT的值+1;将ARR的值设为999,那么每1ms会触发update事件。
从模式控制器设为Update模式,那么每个Update事件,TRGO输出脉冲,即每隔1ms TIM3的TRGO向外输出脉冲 。

外部触发为TIM_TRGO,常规序列规定对通道7,采样0.1us;对通道8,采样时间0.2us;对通道9,采样时间0.1us。
按照这种设置,每0.1ms来一个外部触发TIM3_TRGO信号,ADC先对通道7进行采样,然后对通道8进行采样,再次对通道9进行采样,循环往复。
黄色部分为采样时间,通道8的采样时间最长。
2.3 ADC的注入序列

注入序列 (Injected Group) ,顾名思义,"注入"就是硬生生插进去的意思。它就像医院的急诊 VIP,或者马路上的救护车。
它与常规序列的使用基本相同,但有几点区别:
- 它数量更少,最多只能安排 4 个通道。
- 它自带寄存器,它有 4个独立的数据寄存器,每个通道有单独的结果寄存器,这是最大的优势。4个通道的数据各回各家,不会互相覆盖。所以读取注入组数据不需要DMA,直接读寄存器就行。
- 它的优先级更高,它可以打断"常规序列"。如果常规组正在干活,注入组来了,常规组必须暂停(Hold),等注入组测完这4个,常规组才能继续测剩下的。
2.4 常规序列和注入序列优先级的说明

这里举例说明常规序列和注入序列的优先级
常规序列选择 TIM3_TRGO 每1ms 触发一次,规定:(1)先采集ch0,采样时间 0.1 us (2)采集ch1,采样时间 0.2 us (3)采集ch2,采样时间 0.1 us
注入序列,选择软件启动,规定:(1)先采集ch3,采样时间 0.1 us (2)采集ch4,采样时间 0.1 us
开启ADC,受到常规序列外部触发,ADC依次对PA0进行采样,采样时间0.1us;对PA1进行采样,采样时间0.2us;对PA2进行采样,采样时间0.1us.
再下个周期,先受到常规序列外部触发,ADC依次对PA0进行采样,采样时间0.1us;对PA1进行采样,这里注入序列被触发,打断常规序列的采样,ADC对PA3进行采样,采样时间0.1us;对PA4进行采样,采样时间0.1us. 注入组测完,常规组继续测剩下的PA0和PA1。
第三个周期,注入序列没有触发信号,常规组正常工作。
有 DMA 配合常规序列不就无敌了吗?还要注入组干嘛?注入序列适合说明场景下使用呢?
场景一:电机控制(最经典用法)
在控制无刷电机时,需要在 PWM 波形的特定时刻 (比如高电平中间)去测量电流。这个时间点要求极其精确,不能等常规队伍慢慢排队。
做法: 用定时器触发 ADC 的注入组。时间一到,立马插队测量电流,测完存在独立寄存器里,CPU 空了再去读。
场景二:紧急监测
常规组在循环扫描一般的传感器(温度、电位器),而注入组用来监测"过压保护"。
做法:一旦触发保护机制,注入组立即执行,CPU 可以在注入组的中断里第一时间关断电源。
场景三:简单的多通道采集(不想用 DMA)
如果你只想测 2-4 个通道,又觉得配置 DMA 太麻烦,代码太难写。
做法: 直接把这几个通道配置到注入组。因为它们有独立寄存器,不会覆盖,所以你可以在转换完成后,依次去读 JDR1, JDR2... 既简单又安全。
三、采样时间和转换时间

在介绍ADC模块的时候,我们提到过,采样时间和转换时间的概念。
采样时间,开关闭合的时间长度,即给电容充电的时间。
转换时间,对采样点进行转换所消耗的时间。比较器V-端得出与V+端最接近的值,所消耗的时间。
3.1 ADC的时钟频率

ADC 是一个数字外设,它工作需要时钟脉冲。
它的时钟不是凭空产生的,而是来自 APB2 总线时钟(通常是 72MHz),经过一个 分频器 (Prescaler) 降频得到的。
如图所示,标准库默认情况下,将各部分时钟频率设为最大值 。如图,APB2时钟频率为72MHz,但是STM32F1 的手册规定"ADC 的时钟频率不能超过 14 MHz"。 如果超过 14MHz,ADC 的精度会大幅下降,测出来的数就不准了。
因此,我们需要对ADC模块的时钟频率进行分频。
系统时钟 (System Core Clock) = 72 MHz
APB2 总线时钟 (PCLK2) = 72 MHz
ADC 分频设置 :
如果选择 /4: 72 / 4 = 18 M H z 72/4 = 18 MHz 72/4=18MHz,超频了,不推荐。
如果选择 /6: 72 / 6 = 12 M H z 72/6 = 12 MHz 72/6=12MHz,最佳选择,最接近 14MHz 且不超标 。
如果选择 /8: 72 / 8 = 9 M H z 72/8 = 9 MHz 72/8=9MHz,可以,但速度慢点。
结论,我们要对 ADC1 的时钟源APB2进行6分频,得到12 MHz的ADC时钟。
3.2 转换时间的计算方法

对于 STM32F1(12位分辨率),需要比较12次,也就是12个时钟周期(cycle)。类比就是调整砝码,我们的单片机有12个大小不同的砝码,我们要逐个比较。
0.5个额外的周期,没有明确的解释,那就按照默认的值设置。
转换时间 = 12 c y c l e + 0.5 c y c l e = 12.5 c y c l e 转换时间 = 12 cycle +0.5 cycle = 12.5 cycle 转换时间=12cycle+0.5cycle=12.5cycle
3.3 采样时间和信号源内阻的关系

回忆一下前面说的"小量杯(内部电容)",采样时间就是打开水龙头给量杯注水的时间。
STM32F1 允许你针对每个通道独立设置采样时间,共有 8 种选择,它的大小与信号源内阻有关 。

如图所示,为18650动力电池。它的内阻一般在 10 m Ω 10mΩ 10mΩ,简化成右边的电路。
其实,对于任意的信号源(电信号),它都是有内阻的。

那么信号采样就可表示为上图。
信号源内阻越大,那么当采样电路的开关闭合时,流过的电流就越小,则电容 C A D C C_{ADC} CADC充电速度下降,采样时间也就越长。
小结:信号源内阻越大,采样周期越长。
3.4 信号源内阻的计算方法

我们接下来会用光敏传感器做实验,STM32的模拟输入连接AO引脚,进行ADC转换。
从光敏传感器模块的电路图可以看出,光敏电阻R2与10K的电阻R1串联,我们要的信号是A0引脚的输出,那么如何确定光敏传感器模块的内阻呢(信号源内阻)。

对于左图,我们将其化简,得到中间的图形,R1与R2是串联关系,AO的电压即R2两端的电压, U A O = V C C ∗ R 2 / ( R 1 + R 2 ) U_{AO} = VCC *R_{2}/(R_1+R_2) UAO=VCC∗R2/(R1+R2)。
利用戴维南等效定理,将VCC短路,那么从AO看过去,R1和R2是并联关系,算出等效电阻 R = R 1 / / R 2 R=R_1 // R_2 R=R1//R2,如右图所示。因为R1=10K,R2与R1是并联关系,所以等效电阻R的值必定小于等于10K,那我们取R=10K.
3.5 采样时间的计算方法

查看数据手册DS5319,信号源内阻的计算公式如上图所示,我们将其变形,得到下面采样时间的计算公式 。

信号源内阻,需要我们自行计算,在3.4节,我们算出等效电阻R=10K,这里信号源内阻我们按10K算。
ADC采样电阻,ADC采样电容,采样深度需要查看数据手册,这里的值是根据数据手册给出。
STM32F1 的手册规定,ADC 的时钟频率不能超过 14 MHz。时钟频率我们选择6分频,将其设置为了12MHz。
由此,得出采样时间为10.24 cycle。
注意,之前的转换时间是12.5 cycle。
总转换时间 = 采样时间 + 转换时间 总转换时间 = 采样时间 + 转换时间 总转换时间=采样时间+转换时间
四、常规序列单通道转换
4.1 实验介绍

本次实验,我们用单片机的ADC1 测量光敏传感器的AO输出,光照强,光敏电阻减小,AO输出电压低,我们点亮板载LED;否则熄灭LED。

拟采用常规序列,单通道,软件触发进行转换,使用通道0采样AO信号。
4.2 实验顺序

4.2.1 步骤一:初始化IO引脚
我们应该将PA0配置为模拟输入,其它三种输入为数字模式。
4.2.2步骤二:配置ADC的时钟

如之前介绍的一样,我们要对ADC1的分频器设置为6分频,这样,ADC1的时钟频率为12MHz,才能符合单片机的规定。
4.2.3 步骤三:初始化ADC的基本参数

ADC模块的相关接口如上图所示,我们这次用到的是通用和常规序列相关接口。

1.ADC_ContinuousConvMode (连续转换模式)
作用:决定是转换一次就停止,还是一直不停地转换。
取值:
DISABLE:单次转换。触发一次,转换一次,然后停止。下一次转换需要再次触发。
ENABLE:连续转换。触发开启后,ADC 会一轮接一轮地不停转换,直到软件停止。
2.ADC_DataAlign (数据对齐方式)
作用:STM32 的 ADC 通常是 12 位的,但数据寄存器是 16 位的,数据放左边还是右边,稍后做出说明。
3.ADC_ExternalTrigConv (外部触发选择)
作用:决定是什么信号启动 ADC 转换。
常见值:
ADC_ExternalTrigConv_None:软件触发(最常用)。通过代码 ADC_SoftwareStartConvCmd 启动。
ADC_ExternalTrigConv_T1_CC1 等:由定时器(Timer)或外部中断线(EXTI)的信号触发转换。
4.ADC_Mode (ADC 工作模式)
作用:设置 ADC 是独立工作,还是与其他 ADC(如 ADC1 和 ADC2)配合工作。
常见值:
ADC_Mode_Independent:独立模式(最常用)。ADC1、ADC2 各自独立工作,互不干扰。
ADC_Mode_RegInjecSimult、ADC_Mode_RegSimult 等:双 ADC 模式(同步注入、同步规则等),用于同时采集或交替采集以提高采样率。
5.ADC_NbrOfChannel (转换通道数量)
作用:规定了规则组(Regular Group)中有多少个通道需要转换。
取值:1 到 16。
注意:仅在扫描模式下该值才有实际意义;非扫描模式下通常设为 1。
6.ADC_ScanConvMode (扫描模式)
作用:决定是只转换一个通道,还是扫描一组通道。
取值:
DISABLE:单通道模式。只转换规则组里的第一个通道。
ENABLE:扫描模式。会依次转换规则组中配置的所有通道(由 ADC_NbrOfChannel 指定数量)。
注意:如果使用多通道,必须开启此模式,并且通常配合 DMA 读取数据,防止数据被覆盖。

我们的单片机是12位逐次逼近型ADC,存放数据的寄存器是16位的,那么就有左对齐和右对齐两种对齐方式。
按照使用习惯,我们一般选择右对齐,读取到的值即为 0~4095。
小结:我们要配置ADC1,使用独立模式,单次转换,软件触发,右对齐。
4.2.4 步骤四:配置常规序列

首先,通过 ADC_RegularChannelConfig 函数指定"通道0"排在序列的"第1位",采样时间为之前计算的10.24cycle,我们取最接近的参数,ADC_SampleTime_13Cycles5。
其次,闭合外部触发开关。
最后,闭合ADC的总开关。
4.2.5 步骤五: 启动并读取转换结果
在main函数中,
首先,对EOC标志位清零
其次,通过软件启动方式发送脉冲,即ADC_SoftwareStartConvCmd(ADC1, ENABLE);
接着,等待常规序列转换完成 while(ADC_GetFlagStatus(ADC1,AD_FLAG_EOC) == RESET);
最后,读取转换的结果,uint16_t adc_value = ADC_GetConversionValue(ADC1); 读取后 EOC 会自动清除

把读出的结果转换成实际电压,
测得的电压 寄存器读到的实际值 = 单片机的电压 单片机寄存器的量程 \frac{测得的电压}{寄存器读到的实际值} = \frac{单片机的电压}{单片机寄存器的量程} 寄存器读到的实际值测得的电压=单片机寄存器的量程单片机的电压
测得的电压 寄存器读到的实际值 = 3.3 V 2 12 − 1 \frac{测得的电压}{寄存器读到的实际值} = \frac{3.3V}{2^{12}-1} 寄存器读到的实际值测得的电压=212−13.3V
测得的电压 = 寄存器读到的实际值 ∗ 3.3 V 4095 测得的电压 = 寄存器读到的实际值*\frac{3.3V}{4095} 测得的电压=寄存器读到的实际值∗40953.3V
实验现象:

用串口打印出来数据,白天光照好,光敏电阻组织小,AO电压小;遮住光敏电阻,阻值会变大。

光照弱,板载LED灭;光照强,板载LED亮,实验成功。
五、注入序列单通道转换

在第四章,我们使用ADC的常规序列进行了单通道转换的实验。
本章,我们使用ADC的注入序列,继续用光敏传感器完成单通道的转换,触发方式选择TIM1_TRGO触发,步骤大部分与常规序列相同。

要使用TIM1_TRGO触发,首先要对TIM1的时基单元进行配置,然后从模式控制器的TRGO选择为Update模式。效果为每来一个TRGO脉冲,单片机对通道0进行采样和转换。

下面说明配置定时器1的TRGO,我们设计每1ms产生一个TRGO信号。
那么将PSC设为71,得到1MHz的时钟,即每1us计数值CNT+1,讲ARR设为999,RCR设为0,那么每1ms产生一次Update事件。从模式控制器的TRGO设为Update模式,那么每1ms产生一个脉冲信号。

上图是注入序列的编程接口,与常规序列不同,(1)这里要另外设置注入序列的长度(2)另外选择注入序列外部触发信号(3)另外使能注入序列的外部触发。
将第四章代码稍作修改,即可完成,实验现象如下。

我们用串口将数据发送到电脑上,使用VOFA观察波形变化,不停地遮挡光敏电阻,它的波形如图所示.
不遮挡时电压为1V左右,遮挡后,电压为3v左右,符合预期,实验完成。
注意,调用串口向VOFA打印数据,发送的内容里只能是数据,不能有其它字符,不然能VOFA虽然接收数据,但是波形一直是0,可能会乱码。
总结
以上就是本文全部内容,如果想使用扫描模式,即同时对通道0和通道1的模拟信号进行转换 ,建议采用常规序列+DMA;或者直接使用注入序列,因为它每个通道有单独的寄存器,数据不会被其它通道覆盖。
关于连续模式,ADC的连续转换模式:ADC 一旦启动转换,完成一次后立即自动开始下一次转换,周而复始,直到软件手动停止它。它需要配合DMA进行使用。后面会补充这部分内容。
至此,ADC的学习完成,STM32标准库的视频也独立复现了一遍,也算跟着山羊哥入门了STM32,对各个模块有了清晰的认识。
接下来,会开始做小项目,愿明年的这个时候,有个好的结果!!!
附录
实验一代码
这里放上第四章常规序列单通道转换的代码,串口部分使用的是我封装好的代码,使用的是USART1,会附在后面,可以看我之前的笔记,也可以看铁头山羊B站的视频,讲的无比清晰。
main函数
c
#include "stm32f10x.h" // Device header
#include "ADC.h"
#include "usart.h"
void OnBoardLED_Init(void);
int main(void)
{
usart_Init();
OnBoardLED_Init();
MyADC1_Init();
while(1)
{
//#1.清除EOC标志位
ADC_ClearFlag(ADC1, ADC_FLAG_EOC);
//#2.通过软件启动方式发送脉冲
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
//#3.等待常规序列转换完成
uint16_t Timeout=10000;
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET){
Timeout--;
if(Timeout<=0){
break;//超时跳出,避免死机
}
}
//#4.读取转换的结果
uint16_t adc_value = ADC_GetConversionValue(ADC1);
float voltage = adc_value * 3.3f / 4095.0f;
//#5.实验现象
if(voltage>=1.5){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);//光照弱,灭灯
}else{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);//光照强,亮灯
}
My_USART_Printf(USART1, "当前电压为:%.3f V\r\n",voltage);
}
}
/*
简介:初始化板载LED
*/
void OnBoardLED_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct ={0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}
ADC.c文件
c
#include "ADC.h"
/*
简介:ADC1常规序列单通道转换
*/
void MyADC1_Init(void){
//#1.开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置分频器的分频系数(6分频)
//#2.初始化PA0引脚,模拟输入
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//#3.初始化ADC的基本参数
ADC_InitTypeDef ADC_InitStruct = {0};
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;//单次转换
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//独立模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//软件触发
ADC_InitStruct.ADC_NbrOfChannel = 1;//转换通道数为1,非扫描模式通常设为1
ADC_InitStruct.ADC_ScanConvMode = DISABLE;//非扫描模式,单通道模式
ADC_Init(ADC1,&ADC_InitStruct);
//#4.配置常规序列
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_13Cycles5);//配置常规序列的通道
ADC_ExternalTrigConvCmd(ADC1,ENABLE);//闭合外部触发开关
//#5.闭合ADC的总开关
ADC_Cmd(ADC1,ENABLE);
}
ADC.h文件
c
#ifndef __ADC_H
#define __ADC_H
#include "stm32f10x.h" // Device header
void MyADC1_Init(void);
#endif
usart.c文件
c
#include "usart.h"
/*
@简介:初始化USART1模块
@参数:无
@返回值:无
*/
void usart_Init(void){
//#1.初始化串口引脚,PA9-Tx-复用推挽输出,PA10-Rx-上拉输入
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//#2.初始化USART模块
//波特率115200,数据位长度8位,一位停止位,无校验位,收发双向,没有用到硬件流控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
USART_InitTypeDef USART_InitStruct = {0};
USART_InitStruct.USART_BaudRate = 115200;//波特率
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//收发双向
USART_InitStruct.USART_Parity = USART_Parity_No;//校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1;//停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b;//数据位长度
USART_Init(USART1,&USART_InitStruct);
USART_Cmd(USART1,ENABLE);//使能USART模块,闭合总开关
}
/*
@简介:使用串口一次性发送多个字节
@参数:USARTx -> 要使用的串口
@参数:pData ->要发送的数据(数组)
@参数:Size -> 要发送数据的数量,单位是字节
@返回值:无
*/
void My_USART_SendBytes(USART_TypeDef* USARTx, const uint8_t* pData, uint16_t Size){
for(uint16_t i=0; i<Size; i++){
//#1.等待发送数据寄存器为空,当不为空(RESET)时,等待
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
//#2.将要发送的数据写入发送数据寄存器当中
USART_SendData(USARTx, pData[i]);
}
//#3.等待数据发送完毕,RESET说明没有完成,继续等待
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
}
/*
@简介:使用串口发送一个字节的数据
@参数:USARTx -> 要使用的串口
@参数:Data ->要发送的数据
@返回值:无
*/
void My_USART_SendByte(USART_TypeDef* USARTx, const uint8_t Data){
My_USART_SendBytes(USARTx, &Data, 1);
}
/*
@简介:使用串口发送一个字符
@参数:USARTx -> 要使用的串口
@参数:C ->要发送的字符
@返回值:无
*/
void My_USART_SendChar(USART_TypeDef* USARTx, const char C){
My_USART_SendBytes(USARTx, (const uint8_t*)&C, 1);
}
/*
@简介:使用串口发送字符串
@参数:USARTx -> 要使用的串口
@参数:Str ->要发送的字符串
@返回值:无
*/
void My_USART_SendString(USART_TypeDef *USARTx, const char *Str)
{
My_USART_SendBytes(USARTx, (const uint8_t *)Str, strlen(Str));
}
/*
@简介:通过串口格式化打印字符串,这部分没懂怎么写的
@参数 USARTx:串口名称,如USART1, USART2, USART3 ...
@参数 Format:字符串的格式
@参数 ... :可变参数
*/
void My_USART_Printf(USART_TypeDef *USARTx, const char *Format, ...)
{
char format_buffer[128];
va_list argptr;
__va_start(argptr, Format);
vsprintf(format_buffer, Format, argptr);
__va_end(argptr);
My_USART_SendString(USARTx, format_buffer);
}
/*
@简介:完成printf的重定向
@参数:无
@返回值:无
*/
int fputc(int ch, FILE* f){
//#1. 等待发送数据寄存器(TDR)为空
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
//#2. 发送字符
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
/*
@简介:接收数据,根据需要,进行修改
@参数:无
@返回值:无
*/
void usart_Receive(void){
//#1.等待接收数据寄存器非空
while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == RESET);
//#2.接收数据
uint8_t byteEcvd = USART_ReceiveData(USART1);
//#3.处理数据
if(byteEcvd == '0'){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
}else if(byteEcvd == '1'){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}
}
usart.h文件
c
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "string.h"
#include "stdarg.h"
#include "delay.h"
#define LINE_SEPERATOR_CR 0x00 // 回车 \r
#define LINE_SEPERATOR_LF 0x01 // 换行 \n
#define LINE_SEPERATOR_CRLF 0x02 // 回车+换行 \r\n
void usart_Init(void);
void My_USART_SendByte(USART_TypeDef* USARTx, const uint8_t Data);
void My_USART_SendBytes(USART_TypeDef* USARTx, const uint8_t* pData, uint16_t Size);
void My_USART_SendChar(USART_TypeDef* USARTx, const char C);
void My_USART_SendString(USART_TypeDef *USARTx, const char *Str);
void My_USART_Printf(USART_TypeDef *USARTx, const char *Format, ...);
void usart_Receive(void);
//没看懂这部分怎么写的,使用前调用delay.h头文件
//uint8_t My_USART_ReceiveByte(USART_TypeDef *USARTx);
//uint16_t My_USART_ReceiveBytes(USART_TypeDef *USARTx, uint8_t *pDataOut, uint16_t Size, int Timeout);
//int My_USART_ReceiveLine(USART_TypeDef *USARTx, char *pStrOut, uint16_t MaxLength, uint16_t LineSeperator, int Timeout);
#endif
实验二代码
串口usart的代码在实验一给出,这里不再重复。
main函数
c
#include "stm32f10x.h" // Device header
#include "ADC.h"
#include "usart.h"
#include "TIM1_TRGO.h"
void OnBoardLED_Init(void);
int main(void)
{
usart_Init();
OnBoardLED_Init();
MyADC1_Init();
MyTIM1_Init();
while(1)
{
//#1.等待注入序列转换完成
uint16_t Timeout=10000;
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_JEOC)==RESET){
Timeout--;
if(Timeout<=0){
break;//超时跳出,避免死机
}
}
//#2.清除JEOC标志位
ADC_ClearFlag(ADC1, ADC_FLAG_JEOC);
//#3.读取转换的结果
uint16_t adc_value = ADC_GetInjectedConversionValue(ADC1,ADC_InjectedChannel_1);
float voltage = adc_value * 3.3f / 4095.0f;
//#5.实验现象
if(voltage>=1.5){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);//光照弱,灭灯
}else{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);//光照强,亮灯
}
My_USART_Printf(USART1, "%.3f\n",voltage);
}
}
/*
简介:初始化板载LED
*/
void OnBoardLED_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct ={0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}
注入序列ADC.c文件
c
#include "ADC.h"
/*
简介:ADC1注入序列,定时器触发
*/
void MyADC1_Init(void){
//#1.开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置分频器的分频系数(6分频)
//#2.初始化PA0引脚,模拟输入
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//#3.初始化ADC的基本参数
ADC_InitTypeDef ADC_InitStruct = {0};
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;//单次转换
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//独立模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//软件触发
ADC_InitStruct.ADC_NbrOfChannel = 1;//转换通道数为1,非扫描模式通常设为1
ADC_InitStruct.ADC_ScanConvMode = DISABLE;//非扫描模式,单通道模式
ADC_Init(ADC1,&ADC_InitStruct);
//#4.配置注入序列
ADC_InjectedSequencerLengthConfig(ADC1,1);//设置注入序列的长度
ADC_InjectedChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_13Cycles5);//配置注入序列的通道
ADC_ExternalTrigInjectedConvConfig(ADC1,ADC_ExternalTrigInjecConv_T1_TRGO);//配置注入序列的触发源TIM1_TRGO
ADC_ExternalTrigInjectedConvCmd(ADC1, ENABLE);//使能注入组的外部触发功能
//#5.闭合ADC的总开关
ADC_Cmd(ADC1,ENABLE);
}
注入序列.h文件
c
#ifndef __ADC_H
#define __ADC_H
#include "stm32f10x.h" // Device header
void MyADC1_Init(void);
#endif
定时器TRGO的代码
c
#include "TIM1_TRGO.h"
/*
简介:初始化TIM1,将其TRGO配置为Update模式
*/
void MyTIM1_Init(void){
//#1.开启TIM1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
//#2.初始化时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0};
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;//上计数
TIM_TimeBaseInitStruct.TIM_Period = 1000-1;//重装载寄存器
TIM_TimeBaseInitStruct.TIM_Prescaler = 72-1;//预分频器
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;//重复计数器
TIM_TimeBaseInit(TIM1,&TIM_TimeBaseInitStruct);
//#3.设置TRGO为Update模式
TIM_SelectOutputTrigger(TIM1,TIM_TRGOSource_Update);
//#4.使能TIM1
TIM_Cmd(TIM1,ENABLE);
}
定时器TRGO.h代码
c
#ifndef __TIM1_TRGO_H
#define __TIM1_TRGO_H
#include "stm32f10x.h" // Device header
void MyTIM1_Init(void);
#endif