1. ADC介绍
1.1 什么是ADC(可以理解为一个"电压表")
• 全称:Analog-to-Digital Converter,指模拟/数字转换器。
• ADC可以将引脚上连续变化的模拟电压 转换为内存中存储的数字变量,是建立模拟电路到数字电路的桥梁。
• 简单理解ADC就是一个转换器。如图:

• 12 位 ADC 是一种逐次逼近型模拟数字转换器 (0~4095)。它有多达18 个通道,可测量 16 个外部和 2 个内部信号源。各通道的 A/D 转换可以单次、连续、扫描或间断 模式执行。 ADC 的结果可以左对齐或右对齐 方式存储在16 位数据寄存器 **(ADC_DR)**中。
• ADC 的输入时钟不得超过 14MHz ,它是由 PCLK2经分频产生。
• STM32F103C8T6 ADC资源:ADC1 、 ADC2 , 10 个外部输入通道。
1.2 ADC工作原理(逐次逼近型),如图:

• 内部的"控制与定时"让"逐次逼近寄存器"在"D/A转换器(DAC)"产生一个模拟量与外界输入的模拟量进行比较,如果比外界的小了,那么重新生成比之前大的模拟量再次进行比较;如果比外界的大了,那么生成比之前小的模拟量进行比较,直到接近或者相等为止,最后将最接近的模拟量放进输出缓冲器中。
1.3 ADC特性参数
• ADC的性能指标:
• 量程 :能测量的电压范围(0-4095)(12 位的 ADC)
• 分辨率 :ADC能辨别 的最小模拟量 ,通常以输出二进制数的位数 表示,比如:8、10、12、16位等;位数越多,分辨率越高,一般来说分辨率越高,转化时间越长
• 转化时间 :从转换开始 到获得稳定的数字量 输出所需要的时间称为转换时间 。转换时间越长,转换结果相对越准确,但是转换速度就越慢。
1.4 ADC特性
• 12 位精度下转换速度可高达1MHZ
• ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中 ( 通常还是使用右对齐多 )
• 供电电压:VSSA :0V,VDDA :2.4V~3.6V。
• ADC输入范围:VREF- ≤ VIN ≤ VREF+,0~3.3V。
2. ADC的一些细节
2.1 输入通道
• 总共 2 个 ADC(ADC1,ADC2),每个 ADC 有 18 个转换通道: 16 个外部通道、 2 个内部通道 (温度传感器、内部参考电压),如图:

• 虽然两个ADC的IO基本一样,但是可以采用分时复用来使用,使用的好的话,甚至测量值也会更好。
• 外部的16个通道在转换时又分为规则通道和注入通道 ,其中规则通道最多有 16 路,注入通道最多有 4 路。
2.2 规则组/注入组
• 如图,规则组(规则通道)

• 类似于"流水线",把要测的通道放在规则组里面,按照转换顺序,一个一个进行转换。注意(通道可以重复,例如,在第一个转换中是通道1,在后面也可以再次出现)。
2.3 注入组(注入通道)

• 类似于"中断",在正常按照顺序转换时,突然插入了几个要转换的通道。注意(插入的注入组通道也可以重复)。
2.4 转换顺序
• 每个 ADC 规则通道只有一个数据寄存器 ,16 个规则通道一起共用这个寄存器 ,所以需要指定规则转换通道的转换顺序。
• 规则通道中的转换顺序 由三个寄存器控制:SQR1 、 SQR2 、 SQR3 ,它们都是32位寄存器。SQR寄存器控制着转换通道的数目和转换顺序 ,只要在对应的寄存器位SQx 中写入相应的通道,这个通道就是第x个转换。

• 理解:在三个寄存器设置在那个 通道是第几个 转换。例如SQ1 中设置通道 5 为第一个转换。
• SQL[3:0]就是需要转换多少个通道。
• 和规则通道转换顺序的控制一样,注入通道的转换也是通过注入寄存器来控制 ,只不过只有一个 JSQR 寄存器来控制转换的顺序,控制关系如下:

• 注入序列的转换顺序是从JSQx[ 4 : 0 ](x=4-JL[1:0])开始。只有当JL=4的时候,注入通道的转换顺序才会按照JSQ1、JSQ2、JSQ3、JSQ4的顺序执行。
• 如果设置转换通道数目小于4,例如设置为3,则转换顺序是JSQR2→JSQR3→JSQR4,而不是从JSQR1开始。
2.5 触发转换的方法
• 通过向控制寄存器ADC-CR2 的 ADON 位写 1 来开启 ADC ,再将 SWSTART 位置 1 ,启动规则通道转换(软件)
• 也可以通过外部事件(如定时器)进行转换(硬件),如图:

2.6 转换时间
• ADC 是挂载在 APB2 总线 (PCLK2 )上的,经过分频器得到 ADC 时钟( ADCCLK ) ,最高 14 MHz。
• 转换时间 = 采样时间+12.5周期。(周期等于频率的倒数)。
• 12.5个周期是固定的 ,一般我们设置 PCLK2=72M ,经过 ADC 预分频器能分频到最大的时钟只能是12M,如果采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。但是也有些芯片可以算出最短为1us,因为最大时钟可以到14M。
2.7 中断及事件,如图:

• 注意:DMA 只适用于规则通道。
2.8 校准
• ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。在校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。
• 通过设置ADC_CR2 寄存器的 CAL 位启动校准 。一旦校准结束, CAL位被硬件 复位,可以开始正常转换。建议在上电时执行一次 ADC 校准 。校准阶段结束后,校准码储存在 ADC_DR 中。
2.9 单次转换和连续转换
• 单次转换:只转换一次。
• 连续转换:转换一次之后,立马进行下一次转换。
2.10 扫描模式
• 关闭扫描模式:只转换ADC_SQRx或ADC_JSQR选中的第一个通道。
• 打开扫描模式:扫描所有被ADC_SQRx或ADC_JSQR选中的所有通道。
3. 实战单通道采集
• 配置ADC工作参数 HAL_ADC_Init()
• ADC校准 HAL_ADCEx_Calibration_Start()
• Msp初始化(NVIC,CLOCK,GPIO),HAL_ADC_MspInit()
• 配置ADC通道(转换位置和采样时间)HAL_ADC_ConfigChannel()
• 启动ADC转换HAL_ADC_Start()
• 等待规则组转换完成HAL_ADC_PollForConversion()//阻塞等待
• 获取规则通道AD转换结果 HAL_ADC_GetValue()
• adc.c
cpp
#include "adc.h"
ADC_HandleTypeDef adc_handle = {0};
void adc_init(){
adc_handle.Instance = ADC1;//选择的是ADC1来进行转换
adc_handle.Init.ContinuousConvMode = DISABLE;//不连续
adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;//右对齐
adc_handle.Init.DiscontinuousConvMode = DISABLE;//间断也不开
adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;//软件触发
adc_handle.Init.NbrOfConversion = 1;//转换数量
adc_handle.Init.NbrOfDiscConversion = 0;//间断模式下的转换数量
adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;//不扫描
HAL_ADC_Init(&adc_handle);
HAL_ADCEx_Calibration_Start(&adc_handle);//校准
}
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc){
if(hadc->Instance == ADC1){
GPIO_InitTypeDef gpio_init = {0};
RCC_PeriphCLKInitTypeDef rcc_pct_init = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
__HAL_RCC_ADC1_CLK_ENABLE();
gpio_init.Mode = GPIO_MODE_ANALOG;//模式选择模拟输入
gpio_init.Pin = GPIO_PIN_1;//推挽输出
HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
rcc_pct_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
rcc_pct_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;
HAL_RCCEx_PeriphCLKConfig(&rcc_pct_init);
}
}
void adc_config_channel(ADC_HandleTypeDef* hadc,uint32_t ch,uint32_t rank,uint32_t stime){//配置通道
ADC_ChannelConfTypeDef adc_config = {0};
adc_config.Channel =ch;
adc_config.Rank = rank;
adc_config.SamplingTime = stime;//转换时间
HAL_ADC_ConfigChannel(hadc,&adc_config);//配置通道
}
uint32_t get_adc_value(uint32_t ch){
adc_config_channel(&adc_handle,ch,ADC_REGULAR_RANK_1,ADC_SAMPLETIME_239CYCLES_5);
HAL_ADC_Start(&adc_handle);//开始转换。
HAL_ADC_PollForConversion(&adc_handle,10);//用于在轮询模式(阻塞模式)下实现模数转换。
//该函数会阻塞当前线程,
//直到模数转换完成并且数据准备就绪,然后将转换结果存储在给定的变量中并返回。
return (uint16_t)HAL_ADC_GetValue(&adc_handle);
}
• main.c
cpp
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "adc.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
adc_init();
printf("hello world\r\n");
while(1)//流水灯实验
{
printf("adc recv : %.2f\r\n",(float)get_adc_value(ADC_CHANNEL_1) / 4096 * 3.3);//通过轮询得方式获得转化后得结果
delay_ms(500);
}
}
4. ADC单通道采集 + DMA读取
• 配置ADC工作参数 HAL_ADC_Init() ADC校准 HAL_ADCEx_Calibration_Start()
• 配置DMA,将DMA与ADC句柄联系起来HAL_DMA_Init(),__HAL_LINKDMA()
• Msp初始化(NVIC,CLOCK,GPIO),HAL_ADC_MspInit()
• 配置ADC通道(转换位置和采样时间)HAL_ADC_ConfigChannel()
• 触发ADC转换,DMA传输数据HAL_ADC_Start_DMA()
• adc.c
cpp
#include "adc.h"
ADC_HandleTypeDef adc_handle = {0};
DMA_HandleTypeDef dma_handle = {0};
void adc_init(){
adc_handle.Instance = ADC1;
adc_handle.Init.ContinuousConvMode = ENABLE;
adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;
adc_handle.Init.DiscontinuousConvMode = DISABLE;
adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;
adc_handle.Init.NbrOfConversion = 1;
adc_handle.Init.NbrOfDiscConversion = 0;
adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;
HAL_ADC_Init(&adc_handle);
HAL_ADCEx_Calibration_Start(&adc_handle);
}
void dma_init(){
__HAL_RCC_DMA1_CLK_ENABLE();
dma_handle.Instance = DMA1_Channel1;
dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;//为什么是半字?,因为12位的ADC
dma_handle.Init.MemInc = DMA_MINC_ENABLE;
dma_handle.Init.Mode = DMA_CIRCULAR;//连续转化
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;
dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;
HAL_DMA_Init(&dma_handle);
__HAL_LINKDMA(&adc_handle,DMA_Handle,dma_handle);//ADC要和DMA链接
}
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc){
if(hadc->Instance == ADC1){
GPIO_InitTypeDef gpio_init = {0};
RCC_PeriphCLKInitTypeDef rcc_pct_init = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
__HAL_RCC_ADC1_CLK_ENABLE();
gpio_init.Mode = GPIO_MODE_ANALOG;
gpio_init.Pin = GPIO_PIN_1;//推挽输出
HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
rcc_pct_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
rcc_pct_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;
HAL_RCCEx_PeriphCLKConfig(&rcc_pct_init);
}
}
void adc_config_channel(ADC_HandleTypeDef* hadc,uint32_t ch,uint32_t rank,uint32_t stime){
ADC_ChannelConfTypeDef adc_config = {0};
adc_config.Channel =ch;
adc_config.Rank = rank;
adc_config.SamplingTime = stime;
HAL_ADC_ConfigChannel(hadc,&adc_config);
}
void adc_dma_init(uint32_t *pdata){
adc_init();
dma_init();
adc_config_channel(&adc_handle,ADC_CHANNEL_1,ADC_REGULAR_RANK_1,ADC_SAMPLETIME_239CYCLES_5);
HAL_ADC_Start_DMA(&adc_handle,pdata,1);
}
• mian.c
cpp
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "adc.h"
uint16_t pdata = 0;
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
adc_dma_init((uint32_t *)&pdata);//不用while轮询,用了DMA_CIRCULAR模式
printf("hello world\r\n");
while(1)//流水灯实验
{
printf("adc recv : %.2f\r\n",(float)pdata / 4096 * 3.3);
delay_ms(500);
}
}
5. ADC多通道采集 + DMA读取
• 基于上述修改就行了
• adc.c
cpp
#include "adc.h"
ADC_HandleTypeDef adc_handle = {0};
DMA_HandleTypeDef dma_handle = {0};
void adc_init(){
adc_handle.Instance = ADC1;
adc_handle.Init.ContinuousConvMode = ENABLE;
adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;
adc_handle.Init.DiscontinuousConvMode = DISABLE;
adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;
adc_handle.Init.NbrOfConversion = 4;
adc_handle.Init.NbrOfDiscConversion = 0;
adc_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;
HAL_ADC_Init(&adc_handle);
HAL_ADCEx_Calibration_Start(&adc_handle);
}
void dma_init(){
__HAL_RCC_DMA1_CLK_ENABLE();
dma_handle.Instance = DMA1_Channel1;
dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
dma_handle.Init.MemInc = DMA_MINC_ENABLE;
dma_handle.Init.Mode = DMA_CIRCULAR;
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;
dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;
HAL_DMA_Init(&dma_handle);
__HAL_LINKDMA(&adc_handle,DMA_Handle,dma_handle);
}
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc){
if(hadc->Instance == ADC1){
GPIO_InitTypeDef gpio_init = {0};
RCC_PeriphCLKInitTypeDef rcc_pct_init = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
__HAL_RCC_ADC1_CLK_ENABLE();
gpio_init.Mode = GPIO_MODE_ANALOG;
gpio_init.Pin = GPIO_PIN_0 | GPIO_PIN_1 |GPIO_PIN_2 |GPIO_PIN_3 ;//推挽输出
HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
rcc_pct_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
rcc_pct_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;
HAL_RCCEx_PeriphCLKConfig(&rcc_pct_init);
}
}
void adc_config_channel(ADC_HandleTypeDef* hadc,uint32_t ch,uint32_t rank,uint32_t stime){
ADC_ChannelConfTypeDef adc_config = {0};
adc_config.Channel =ch;
adc_config.Rank = rank;
adc_config.SamplingTime = stime;
HAL_ADC_ConfigChannel(hadc,&adc_config);
}
void adc_dma_init(uint32_t *pdata){
adc_init();
dma_init();
adc_config_channel(&adc_handle,ADC_CHANNEL_0,ADC_REGULAR_RANK_1,ADC_SAMPLETIME_239CYCLES_5);
adc_config_channel(&adc_handle,ADC_CHANNEL_1,ADC_REGULAR_RANK_2,ADC_SAMPLETIME_239CYCLES_5);
adc_config_channel(&adc_handle,ADC_CHANNEL_2,ADC_REGULAR_RANK_3,ADC_SAMPLETIME_239CYCLES_5);
adc_config_channel(&adc_handle,ADC_CHANNEL_3,ADC_REGULAR_RANK_4,ADC_SAMPLETIME_239CYCLES_5);
HAL_ADC_Start_DMA(&adc_handle,pdata,4);
}
• main.c
cpp
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "adc.h"
uint16_t pdata[4] = {0};
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
adc_dma_init((uint32_t *)pdata);
printf("hello world\r\n");
while(1)//流水灯实验
{
printf("adc recv1 : %.2f\r\n",(float)pdata[0] / 4096 * 3.3);
printf("adc recv2 : %.2f\r\n",(float)pdata[1] / 4096 * 3.3);
printf("adc recv3 : %.2f\r\n",(float)pdata[2] / 4096 * 3.3);
printf("adc recv4 : %.2f\r\n\r\n",(float)pdata[3] / 4096 * 3.3);
delay_ms(500);
}
}