3.2.1 任务分析
本任务要设计一个可对智能小车供电电池的电压进行监测的应用程
序,智能小车供电电路如图 3-2-1 所示。
图 3-2-1 中,供电电池的电压为 12.6 V,通过“PIN”端接入。电池
电压经过分压后,通过“VM_ADC”与智能小车 MCU 的 PA6 引脚相连,
作为模数转换器(Analog-to-Digital Converter,ADC)采集输入。
要求每隔 3s 对电池电压值进行采集,并将采集到的电压值通过串口
发送至上位机显示,显示格式样例为“10.78 V”,数值须精确到小数点后
两位。
本任务涉及的知识点有:
STM32F4 系列微控制器 ADC 外设的工作原理;
ADC 外设的编程配置步骤。
3.2.2 知识链接
1.ADC 简介
ADC 是一种可将连续变化的模拟信号转换为离散的数字信号的器件,其可将温度、压力、
声音或者图像等模拟信号转换成更易存储、处理和发射的数字信号。
STM32F407ZGT6 型号微控制器有3 个ADC,可工作在独立、双重或三重模式下,以满足多种
不同的应用需求。每个ADC 都具有 19 个复用通道,可测量16 个外部信号源、2 个内部信号源以及V BAT 通道的信号,转换精度可配置为 12bit、10bit、8bit 或 6bit,转换结果存储在一个可左对齐或右
对齐的16 位数据寄存器中。图3-2-2 展示了单个STM32F4 系列微控制器ADC 的结构框图。
2.ADC 的功能分析
接下来对图 3-2-2 中的各部分功能进行分析。
(1)ADC 的输入电压范围
ADC 的输入电压 V IN 的范围是: V REF- ≤ V IN ≤ V REF+ ,由图 3-2-3 中的 V REF+ 、 V REF- 、 V DDA 和 V SSA
这 4 个外部引脚的电压决定,这 4 个外部引脚对应的输入电压范围如表 3-2-1 所示。
(2)ADC 的输入通道
单个ADC的输入通道多达19个,其中包括16个外部通道,如图3-2-4中所示的ADCx_IN0、
ADCx_IN1、……、ADCx_IN15。这 16 个外部通道分别连接着不同的 GPIO 端口,如表 3-2-2
所示。
(3)ADC 的转换顺序
STM32F4 系列微控制器的 ADC 转换分为两个通道:规则通道和注入通道。规则通道相当于
正常运行的程序,注入通道相当于中断。正如中断可以打断正常运行的程序,注入通道的 ADC
转换可以打断规则通道的 ADC 转换,只有等注入通道的 ADC 转换完成后,规则通道的 ADC 转
换才能继续运行。
规则通道的转换顺序由规则序列寄存器 SQR3、SQR2 和 SQR1 控制,注入通道的转换顺序
由注入序列寄存器(JSQR)控制。图 3-2-5 展示了 ADC 的两个通道。
(4)ADC 的输入时钟与采样周期
STM32F4 系列微控制器的 ADC 输入时钟如图 3-2-6 所示,由该图可知,STM32F4 的 ADC
输入时钟(ADCCLK)由 PCLK2 经过 ADC 预分频器产生。根据数据手册显示,当 V DDA 范围为
2.4 V~3.6 V 时,ADCCLK 最大值为 36 MHz,典型值为 30 MHz。分频系数由 ADC 通用控制寄
存器(ADC_CCR)中的“ADCPRE[1:0]”位段设置,可设置的值有 2、4、6 和 8。当 PCLK2
为 84 MHz 时,若设置 ADC 预分频器的分频系数为 4,则 ADCCLK 的时钟频率为 21 MHz,对
应一个时钟周期的时间 Tp(1/ADCCLK)等于 0.0476 μs。
A/D 转换需要若干个时钟周期才可完成采样,具体的采样时间可通过 ADC 采样时间寄存器
ADC_SMPR1 和 ADC_SMPR2 中的“SMP[2:0]”位段进行设置,允许设置为 3 个、15 个或 28
个时钟周期等,值越小代表采样时间越短,速度越快。
一次 A/D 转换所需的总时间 T conv =采样时间+数据处理时间(12 T p),因此当 ADCCLK 设置为
21 MHz,采样时间设置为3 个时钟周期,可计算出最短的转换时间 T conv = 15 × T p = 0.7142 μs。
(5)ADC 的触发方式
图 3-2-7 显示了 ADC 所支持的触发方式,从图中可以看到 ADC 支持多种外部事件触发方
式,包括定时器触发和外部 GPIO 中断。具体选择哪种触发方式,可通过 ADC 控制寄存器 2
(ADC_CR2)进行配置,即对规则组和注入组分别进行配置。另外,该寄存器还可对触发极性进
行配置,如上升沿检测、下降沿检测等。
除了图 3-2-7 中显示的触发方式外,ADC 还支持软件触发,该触发由 ADC_CR2 的
“SWSTART”位进行控制,控制的前提是“ADON”位先配置为 1。一次转换结束后,硬件会自
动将“SWSTART”位置零。
(6)ADC 的数据寄存器
ADC 转换完毕后,结果数据存放在相应的数据寄存器中。ADC 的数据寄存器如图 3-2-8
所示,图中展示了两种 ADC 数据寄存器:规则数据寄存
器(ADC_DR)和注入数据寄存器(ADC_JDRx)。上述两种数据寄存器用于独立转换模式的结果存放,双重和三重转换模式的结果则存放于通用 ADC_CDR 中。ADC_DR 只有一个,32bit 长,低 16 位有效,仅适用于独立模式。由于 STM32F4 系列微控制器的 ADC 最大精度是 12 位,因此存放数据时允许设置左对齐或者右对齐,即左对齐存放在 ADC_DR 的[15:4]位,右对齐存放在 ADC_DR 的[11:0]位。由于只有一个,因此当使用多通道转换时,ADC_DR 中存放的数据应及时取走,否则将会被另一个通道转换结果覆盖。常用开启 DMA 传输的方式解决该问题,无须 MCU 参与即可直接将数据转移到内存空间中存放。
ADC_JDRx 有 4 个,正好对应注入组的 4 个通道,因此不会出现类似的“数据被覆盖”的
情况。ADC_JDRx 在使用过程中也需要设置数据位左对齐或者右对齐。
(7)ADC 的中断控制
从图 3-2-9 中可以看到,ADC 转换结束后,支持产生 4 种中断:DMA 溢出中断、规则转
换结束中断、注入转换结束中断和模拟“看门狗”事件中断。它们的事件标志和使能控制位
如表 3-2-3 所示。
规则转换和注入转换结束后,除了可通过产生中断方式处理转换结果之外,还可产生 DMA 请求
以把转换好的数据直接转存至内存中。这对于独立模式、双重模式或三重模式的多通道转换而言是
非常必要的,既可简化程序又可提高运行效率
3.STM32F4 系列微控制器的 ADC 编程配置步骤
(1)使能 ADC 时钟,配置外部通道对应的引脚为模拟输入模式
STM32F407 系列微控制器的 3 个 ADC 外设均挂载在 APB2 上,可调用以下函数使能 ADC1
外设时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能 ADC1 时钟
GPIO 工作模式的配置与前述各任务相似,需要注意的是:GPIO 复用为 ADC 外设需要将工
作模式设置为“模拟输入”,不需要调用 GPIO_PinAFConfig()函数以设置引脚复用映射。配置
PA6 引脚为模拟电压输入的参考代码如下:
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // 使能 GPIOA 时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //PA6 为外部通道 6
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; // 模拟输入(注意)
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // 不带上下拉
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化
(2)配置 ADC 的通用控制寄存器
ADC 的通用控制寄存器(ADC_CCR)主要对“ADC 工作模式”“两次采样中间的间隔时间”
“是否启用 DMA 传输”“ADC 预分频器分频系数”等内容进行配置。在标准外设库中,通过调用
ADC_CommonInit()函数来实现 ADC_CCR 的初始化,示例代码如下:
ADC_CommonInitTypeDef ADC_CommonInitStructure;
/* ADC 工作在独立模式 */
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
/* 两次采样中间的间隔时间为 5 个时钟周期 */
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
/* 禁用 DMA 传输 */
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
/* ADC 预分频器分频系数为 4 */
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;
ADC_CommonInit(&ADC_CommonInitStructure);
(3)初始化 ADC 的参数
ADC 的参数通过控制寄存器 ADC_CR1 和 ADC_CR2 进行配置,下面给出一段使用软件触
发、启用单转换通道、分辨率为 12bit、禁用扫描模式、数据右对齐的配置示例。
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 分辨率 12bit 模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 非扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 关闭连续转换
/* 禁止触发检测,使用软件触发 */
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_NbrOfConversion = 1; // 启用规则序列 1 个转换通道
ADC_Init(ADC1, &ADC_InitStructure); //ADC 初始化
(4)设置 ADC 转换通道、转换顺序和采样周期
在标准外设库中,规则序列中的 ADC 转换通道、转换顺序和采样周期的配置可通过调用
ADC_RegularChannelConfig()函数完成,该函数的原型定义如下:
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel,
uint8_t Rank, uint8_t ADC_SampleTime) ;
第一个参数配置 ADCx 外设,第二个参数配置 ADC 通道号,第三个参数配置转换顺序,第
四个参数配置采样周期。下列示例可配置 ADC1 的通道号为 6,转换顺序为 1,采样时间为 56
个时钟周期。
ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 1, ADC_SampleTime_56Cycles);
(5)使能 ADC 转换结束中断
如果要在 ADC 转换结束后产生中断,且在中断服务程序中读取转换结果,则应调用标准外
设库的 ADC_ITConfig()函数使能 ADC 的转换结束中断 ADC_IT_EOC。使能中断后,还应使用
NVIC 配置中断源的优先级,示例代码如下:
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); // 使能 ADC 的转换结束中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 配置优先级
NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn; // 配置中断源 ADC_IRQn
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
(6)使能 ADC 并设置触发方式
配置好 ADC 的各项参数之后,使能 ADC 并设置相应的触发方式,相关示例代码如下:
ADC_Cmd(ADC1, ENABLE); // 使能 ADC1
ADC_SoftwareStartConv(ADC1); // 开始 ADC 转换,软件触发
(7)编写 ADC 转换完成中断服务程序
一旦使能 ADC 转换结束中断,当 ADC 转换结束后,MCU 会执行中断服务程序。STM32F4
系列微控制器的 ADC 中断服务程序的入口均为“ADC_IRQHandler”,因此在中断服务程序的入
口应判断发生了哪种中断,并在其出口处清除中断标志位。具体程序框架如下:
void ADC_IRQHandler(void)
{
if (ADC_GetITStatus(ADC1,ADC_IT_EOC) == SET)
{
ADC_ConvertedValue = ADC_GetConversionValue(ADC1);
}
ADC_ClearITPendingBit(ADC1,ADC_IT_EOC);
}
在上述示例程序中,通过 ADC_GetITStatus()函数判断是否发生了 ADC 转换结束中断,如
果是,则读取 ADC 转换结果,并清除转换结束中断标志位。
3.2.3 任务实施
接下来分别用查询方式和中断方式实现智能小车电池电压的监测。
1.用查询方式实现智能小车电池电压的监测
(1)编写 ADC 外设的初始化程序
复制一份 task3.1_Timer_Interrupt_GetTrackData 工程,并将其重命名为“task3.2_ADC_
PowerMonitor”。在“HARDWARE”文件夹下新建“ADC”子文件夹,新建“adc.c”和“adc.h”
两个文件,将它们加入工程中,并配置头文件包含路径。
在“adc.c”文件中输入以下代码:
#include "adc.h"
#include "delay.h"
/**
* @brief ADC 转换初始化
* @param None
* @retval None
*/
void MyADC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_CommonInitTypeDef ADC_CommonInitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // 使能 GPIOA 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能 ADC1 时钟
/* 初始化 ADC 输入通道 GPIO */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //PA5 通道 5
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; // 模拟输入
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // 不带上下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,ENABLE); //ADC1 复位
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,DISABLE); // 复位结束
/* 配置 ADC_CCR */
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
/* 两次采样之间的间隔时间为 5 个时钟周期 */
ADC_CommonInitStructure.ADC_TwoSamplingDelay =
ADC_TwoSamplingDelay_5Cycles;
/* 不使能 DMA */
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
/* 分频系数 4 ADCCLK=PCLK2/4=84/4=21Mhz , ADC 时钟不要超过 36 MHz */
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;
ADC_CommonInit(&ADC_CommonInitStructure);
/* 初始化 ADC 相关参数 */
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; //12 位模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 非扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 关闭连续转换
/* 禁止外部触发检测,使用软件触发方式 */
ADC_InitStructure.ADC_ExternalTrigConvEdge =
ADC_ExternalTrigConvEdge_None;ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_NbrOfConversion = 1; // 启用 1 个转换通道
ADC_Init(ADC1, &ADC_InitStructure); //ADC 配置生效
ADC_Cmd(ADC1, ENABLE); // 使能 ADC
}
/**
* @brief 单次 ADC 采样
* @param ch:ADC 的通道编号 (ADC_Channel_0 ~ ADC_Channel_16)
* @retval 单次采样结果
*/
uint16_t Get_Adc(uint8_t ch)
{
/* 设置指定 ADC 的规则组通道,一个序列,采样时间 480 个时钟周期 */
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_480Cycles );
ADC_SoftwareStartConv(ADC1);// 使能指定的 ADC1 的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC )); // 等待转换结束
return ADC_GetConversionValue(ADC1); // 返回 ADC1 规则组的转换结果
}
/**
* @brief 多次 ADC 采样取平均值
* @param ch:ADC 的通道编号 (ADC_Channel_0 ~ ADC_Channel_16)
* @param times: 采样次数
* @retval 采样结果均值
*/
uint16_t Get_Adc_Average(uint8_t ch,uint8_t times)
{
u32 temp_val=0;
uint8_t t;
for(t=0; t<times; t++)
{
temp_val += Get_Adc(ch);
delay_ms(5);
}
return temp_val/times;
}
对上述代码片段的关键点解析如下。
① 采用软件方式触发:代码第 43、60 和 61 行。
② 每次触发只进行一次 ADC 转换:代码第 41 行(关闭连续转换)。
③ 查询方式实现 ADC:代码第 62 行(查询 EOC 转换结束标志位是否被置 1)。
接下来在“adc.h”文件中输入以下代码:
#ifndef __ADC_H
#define __ADC_H
#include "sys.h"
void MyADC_Init(void); //ADC 通道初始化
uint16_t Get_Adc(uint8_t ch); // 单次 ADC 转换某个通道
uint16_t Get_Adc_Average(uint8_t ch,uint8_t times); // 多次 ADC 转换取平均值
void ADC_NVIC_Config(void); // 配置 ADC 中断优先级
#endif
(2)编写 main()函数
在“main.c”文件中输入以下代码:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "adc.h"
uint16_t adcValue = 0; // 存放 ADC 采样的原始值 (0~4096)
float adcVoltage = 0; // 存放 ADC 采样计算后的电压值
char myString[50] = {0};
int main(void)
{
delay_init(168); // 延时函数初始化
LED_Init(); //LED 端口初始化
USART1_Init(115200); //USART1 初始化
MyADC_Init(); //ADC 初始化
while(1)
{
/* 调用多次 ADC 采样取平均值函数,采样 ADC1 的通道 5 */
adcValue = Get_Adc_Average(ADC_Channel_5, 20);
/* 采样值转换为电压值 */
adcVoltage = adcValue * 3.3 / 4096 * 11;
sprintf(myString," 采样值为: %d, 电压值为: %.2f V\r\n", \
adcValue, adcVoltage);
printf("%s",myString);
delay_ms(250);
}
}
2.用中断方式实现智能小车电池电压的监测
相比于采用查询方式实现ADC,采用中断方式实现ADC需要对程序进行以下几方面的修改。
(1)开启 ADC 连续转换功能
采用中断方式实现 ADC 将不再调用“单次 ADC 转换函数 Get_Adc()”,因此可开启 ADC 连
续转换功能。将“adc.c”文件的第 41 行代码修改为:
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 开启连续转换
(2)增加 ADC 中断初始化相关代码
在“adc.c”文件的 MyADC_Init()函数结尾增加以下代码:
extern uint16_t ADC_ConvertedValue;
/**
* @brief ADC 中断优先级配置函数
* @param None
* @return None
*/
void ADC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/**
* @brief ADC 中断服务函数
* @param None
* @return None
*/
void ADC_IRQHandler(void)
{
if (ADC_GetITStatus(ADC1,ADC_IT_EOC) == SET)
{
ADC_ConvertedValue = ADC_GetConversionValue(ADC1);
}
ADC_ClearITPendingBit(ADC1,ADC_IT_EOC);
}
(3)编写 main()函数
在“main.c”中输入以下代码:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "adc.h"
uint16_t adcValue = 0; // 存放 ADC 采样的原始值 (0~4096)
float adcVoltage = 0; // 存放 ADC 采样计算后的电压值
char myString[50] = {0};
uint16_t ADC_ConvertedValue = 0;
int main(void)
{
delay_init(168); // 延时函数初始化
LED_Init(); //LED 端口初始化
USART1_Init(115200); //USART1 初始化
MyADC_Init(); //ADC 初始化
while(1)
{
adcVoltage = ADC_ConvertedValue * 3.3 / 4096;
sprintf(myString," 采样值为: %d, 电压值为: %.2f V \r\n", \
ADC_ConvertedValue, adcVoltage);
printf("%s",myString);
delay_ms(2000);
}
}
3.观察试验现象
应用程序编译无误后,将其下载至开发板运行。打开上位机的串口调试助手,可观察到如图
3-2-10 所示的电池电压监测结果。
注:采用查询方式与采用中断方式实现 ADC 的效果相同。