STM32单片机学习(8)

1.数模转换 —ADC

1.1 关于 ADC

1.1.1 ADC 理论知识

自然界的信号几乎都是模拟信号,比如光亮、温度、压力、声音,而为了方便存储、处理,计算机里面都是数字的 0/1 信号,将模拟信号(连续信号)转换为数字信号(离散信号)的器件就叫模数转换器(Analog-to-Digital Converter,ADC)。

按原理可分为:并行比较型 A/D 转换器(FLASH ADC)、逐次比较型 A/D 转换器(SAR ADC)和双积分式 A/D转换器(Double Integral ADC)。

A/D转换过程通常为4步:采样、保持、量化和编码。如图 32.1.1 所示,一个连续的电压信号ui(t)通过一个由方波CPs控制的开关S之后施加到电容C上,由于电容两端的电压不会突变,可知在S断开时C将维持ui(t)在开关断开瞬间的电压一段时间,直到开关S再次打开。这样,一个模拟的电压信号就转换成了采样展宽信号us(t),其中CPs的频率就是采样频率fs。最后,由ADC的数字编码电路将采样展宽信号us(t)转换成n位的数字量dn-1 : d0并输出。

  • 图 1.1.1 A/D 转换过程
    在这里插入图片描述

采样是对模拟信号周期性地抽取样值,使模拟信号转化为时间上离散的脉冲信号。采样频率(fs)越高,采样越密集,采样值越多,也就越接近模拟信号。为确保采样后的信号能够还原模拟信号,根据香农-奈奎斯特(Shannon & Nyquist)采样定理,采样频率必须大于等于2倍输入模拟信号的最高截止频率(fImax ):
在这里插入图片描述
ADC的主要有三个性能指标:分辨率、转换时间和转换精度。

  • 分辨率:又称为转换精度,指ADC能分辨的最小电压,通常使用二进制有效位表示,反应了ADC
    对输入模拟量微小变化的分辨能力。当最大输入电压一定时,位数越多,量化单位越小,误差越小,分辨率越高。比如一个12位的ADC,参考电压为3.3V,则其能分辨的最小电压为:

  • 转换时间:其倒数为转换速率,指ADC从控制信号到来开始,到输出端得到稳定的数字信号所经历的时间。转换时间通常与ADC类型有关,双积分型ADC的转换时间一般为几十毫秒,属于低速ADC;逐次逼近型ADC的转换时间一般为几十微妙,属于中速ADC;并联比较型ADC的转换时间一般为几十纳秒,属于高速ADC。

  • 转换精度:指ADC输出的数字量所表示的模拟值与实际输入的模拟量之间的偏差,通常为1个或半个最小数字量的模拟变化量,表示为1LSB或1/2LSB。

1.1.2 STM32 的ADC

STM32F10x系列内部有三个12位逐次逼近型ADC,拥有多达18个通道,可测量16个外部模拟输入源和2个内部信号源的A/D转换。每个通道的A/D转换可采用单次、连续、扫描或间断模式执行。ADC的的转换结果为12位的二进制数,可按左对齐或右对齐存储在16位数据寄存器中。ADC具有模拟看门狗特性,允许应用程序检测输入电压是否超过用户自定义的阈值上限或下限。

ADC内部结构如图 1.1.2 所示,可以看作七部分组成。

① ADC 参考电压引脚
V DDA 和V SSA 为专门为模拟电路设计的独立电源,以过滤和屏蔽来自PCB上的毛刺和干扰。ADC为提高转换精确度,使用V DDA 和V SSA 供电。
V REF+ 和V REF- 为ADC基准参考电压(Voltage Reference),将输入的待测量电压与基准参考电压比较(输入信号的电压范围需满足V REF- ≤V IN ≤V REF+ ,2.4V<V REF+ <V DDA <3.6V),从而得知测量电压的大小。基准参考电压的准确度,直接影响测量结果,在高要求场合,通常使用独立的电压基准芯片提供参考电压。一般情况下,将V REF+ 与V DDA 连接,V REF- 与V SSA 连接,得到3.3V的参考电压。此时,输入电压V IN 与转换后的数字量关系为:
在这里插入图片描述
如果想测量更高电压,比如5V电压。可以使用输入范围较大的ADC外置ADC芯片,通过I 2 C等接口将测量结果返回给MCU。也可以使用简单的电阻分压,分压到3.3V以下,将采集的电压再按比例换算,得到实际电压。

② ADC 输入引脚
ADCx_IN0~ADCx_IN15为ADC的输入信号通道,每个输入通道连接一个GPIO引脚。需要GPIO设置为对应模拟输入的复用模式,如表 32.1.1 为各ADC通道所对应引脚。
其中ADC1有两个内部通道:通道17连接内部参考电压V REFINT ,通道16连接了芯片内部温度传感器。

  • 图 1.1.2 STM32 ADC 内部结构图
    在这里插入图片描述
  • 表 1.1.1 ADC 通道对应复用引脚
    在这里插入图片描述
    ③ ADC 通道
    STM32的ADC有16个模拟输入通道,可分为两组模式进行使用和规划。
  • 规则通道组:最多支持16个通道,在该组的ADC通道,根据序列寄存器SQRx(x=1-3)的配置顺序,依次转换。如表 32.1.2 所示,SQR3控制通道顺序1-6,SQR2控制通道顺序7~12,SQR1控制通道顺序13-16。举个例子,一个智慧农业项目里,有五个大棚需要监控温度,每个大棚对应一个ADC 通 道 , 于 是 有 ADC1_IN1-5 , 共 五 个 通 道 。 设 置 SQ1[4:0]为 ADC1_IN1 , SQ2[4:0] 为ADC1_IN2、……、SQ5[4:0]为ADC1_IN5,ADC_SQR1的SQL[3:0]为5。触发ADC采样后,ADC1_IN1-5,依次按规则通道序列的顺序采集。

表 1.1.2 规则通道序列寄存器
在这里插入图片描述

  • 注入通道组:最多支持4个通道,在该组的ADC通道,根据注入序列寄存器JSQR的配置顺序,抢先规则通道的ADC转换。如表 32.1.3 所示,JSQR控制通道顺序1~4。继续前面智慧农业项目举的例子,在五个大棚中,有个蔬菜(假设对应ADC1_IN5)需要随时立即查看其温度。设置JSQ4[4:0]为ADC1_IN5,JL[1:0]=0,则当规则通道正在依次转换时,ADC1_IN5可注入到转换序列中,优先转换。

  • 表 32.1.3 注入通道序列寄存器
    在这里插入图片描述
    ④ ADC 触发
    A/D转换需要触发信号才能开始工作,触发信号的产生方式通常由两种。

  • 软件触发:由软件编程控制,使能触发启动位;

  • 外部触发:规则通道组的外部触发源可以是定时器TIM1_CH1~TIM1_CH3、外部中断线EXTI_11等,通过EXTSEL[2:0]选择触发源,ADC_CR2的EXTRIG位激活;注入通道组的外部触发源可以可以是定时器TIM1_CH4、外部中断线EXTI_15等,通过JEXTSEL[2:0]选择触发源,ADC_CR2的JEXTTRIG位激活;

⑤ ADC 中断
ADC在每个通道转换结束后,可产生相应的中断请求。若ADC_CR1寄存器的EOCIE位被置1,注入通道或规则通道转换结束后,将产生EOC中断;若ADC_CR1寄存器的JEOCIE位被置1,注入通道转换结束后,将产生JEOC中断;若ADC_CR1寄存器的AWDIE位被置1,超出阈值或低于阈值,将产生AWD中断。

⑥ ADC 数据
ADC在转换结束后,将数据存入相应的数据寄存器。规则通道组中的转换结果,存放在16位的ADC_DR规则通道数据寄存器中,该寄存器只有一个,因此转换完成后(查询ADC_SR状态寄存器获取当前ADC转换进度状态),应立即读取出来,以免被覆盖。或者开启DMA模式,将数据直接保存到指定地址的内存里。

注意,不是所有ADC的规则通道组转换结束后都能产生DMA请求,只有ADC1和ADC3能产生DMA请求,ADC2转换数据可以在双ADC模式中使用ADC1的DMA请求。注入通道组中的转换结果,存放在16位的ADC_JDRx注入通道数据寄存器中,该寄存器有四个,对应每一个转换通道,不存在覆盖问题。

ADC转换的结果是一个12位的二进制数,而寄存器是16位的,因此涉及到数据对齐问题。ADC对齐方式有左对齐和右对齐两种,通常使用右对齐方式,因为数据传输一般是从最低位开始(对应右边开始)。例如,16位数据寄存器,数据采用右对齐方式,则转换的数据范围为0~2^12 -1,即0-4095。

⑦ ADC 时钟
由时钟树可知,ADC时钟ADCCLK由APB2(PLCK2)经ADC预分频器分频得到,分频值可设置为2n(1≤n≤4)。APB2总线时钟通常为72MHz,根据《参考手册》可知,ADC最大工作频率为14MHz,因此通常设置分频值为6,ADCCLK为12MHz。

A/D转换在采样时信号需要保持一段时间,采样时间越长,转换结果越稳定,但转换速率也就越慢。STM32的ADC每个通道的采样时间都可以进行设置,可设置为采样周期的1.5倍、7.5倍、13.5倍、28.5倍、41.5倍、55.5倍、71.5倍或239.5倍。ADC转换总时间可以表示为:

ADC转换总时间= 采样时间+12.5个周期时间

当采样时间为1.5个周期,ADCCLK为12MHz,则
在这里插入图片描述

当采样时间为1.5个周期,ADCCLK为14MHz(ADCCLK最大值),则
在这里插入图片描述
即STM32的ADC最短转换时间为1us,但常用的为1.17us。

最后,再介绍一下ADC的四种转换模式:单次模式、连续模式、扫描模式和间断模式。

  • 单次模式:ADC只执行一次转换;
  • 连续模式:当前ADC转换结束后,立即进入下一个转换;
  • 扫描模式:用来扫描一组通道。通道可以来自规则通道组,也可来自注入通道组。开启扫描模式后,ADC将自动扫描该组所有通道,如此时转换模式设置为单次转换,则扫描本组所有通道后,ADC自动停止;若将转换模式设置为连续模式,则在扫描本组所有通道后,再从第一个通道开始扫描;
  • 间断模式:用来间歇转换一组通道。通道可以来自规则通道组,也可来自注入通道组。假设该组包含0、1、2、3、5、6、7、8,共8个通道,而设置转换通道数为3,则第一次触发,转换0、1、2通道;第二次触发,转换3、5、6通道;第三次触发,转换7、8通道,并产生EOC中断;

1.2 硬件设计

如图 1.2.1 为开发板ADC采集滑动电阻电压部分的原理图。W1为滑动电阻(电位器),阻值范围为0~10K。R107为1K电阻,与W1串联分压,则当W1为10K时,R107分压0.3V,W1分压3V,即ADC采集的W1电压范围为0-3V。

ADC引脚接在PC0上,该脚可作为任意ADC的通道10,假设就为ADC1_IN10。

  • 图 32.2.1 ADC 采集滑动电阻电压电路
    在这里插入图片描述

1.3 软件设计

1.3.1 软件设计思路

实验目的:本实验通过使用ADC采集滑动电阻器W1两端的电压,用户可调整W1的阻值,使其两端的电压发生变化,从而ADC采集到的值也发生变化,体验ADC的功能效果。

  1. 初始化ADC属性相关参数:数据对齐方式、工作模式、触发源等;
  2. 初始化ADC涉及的硬件相关参数:相关时钟、引脚、DMA、中断等;
  3. 编写ADC转换完成中断处理函数,通知ADC转换完成;
  4. 主函数编写控制逻辑:通过按键,启动ADC采集,ADC后的数据通过DMA直接存放到指定内存地址;

1.3.2 软件设计讲解

  1. 初始化ADC

ADC的相关参数设置比较多,包含数据对齐方式、工作模式、触发源等,如代码段 1.3.1 所示。

代码段 32.3.1 ADC 初始化(driver_adc.c)

/*
 * 函数名:void ADC_Init(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:无
 * 函数作用:初始化 ADC1 的参数:触发方式、数据格式、转换模式,通道配置及其采样时间
 *
*/
void ADC_Init(void)
{
	ADC_ChannelConfTypeDef sConfig;
	
	hadc1.Instance = ADC1; // 选择 ADC
	hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 选择 ADC 采集到的数据对齐格式
	hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 失能 ADC 的扫描模式
	hadc1.Init.ContinuousConvMode = ENABLE; // 使能 ADC 的连续转换模式
	hadc1.Init.NbrOfConversion = 1; // 使能 ScanConvMode 的情况下,用于设置规则通道转换序列数
	hadc1.Init.DiscontinuousConvMode = DISABLE; // 失能规则组转换序列的间断模式
	hadc1.Init.NbrOfDiscConversion = 1; // 使能 DiscontinuousConvMode 的情况下,用于设置子组的大小
	hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 外部触发源的选择(软件触发 ADC 采样)
	
	// 使用库函数对 ADC 参数初始化
	if(HAL_ADC_Init(&hadc1) != HAL_OK)
	{
		Error_Handler(); // 初始化失败
	}
	
	sConfig.Channel = ADC1_IN_CHANNEL; // 选择本 ADC 通道
	sConfig.Rank = ADC_REGULAR_RANK_1; // 放在规则通道组的第一个位置
	/*
	 * 采样总时间计算:(SamplingTime+12.5)/fadc
	 * fadc 在源文件 system_clk.c 中进行了配置,是系统时钟的 6 分频,fsystem=72MH-->fadc=12MHz
	 * f_sample = fadc/((SamplingTime+12.5)) = 600kHz
	 * 在设计采样率的时候,如果采样的是一段波形,必须满足奈奎斯特采样定理;如果采样的仅是一个电压值,根据时间要求设计即可
	 * 当使用定时器触发时,定时器的频率要与此采样率基本保持一致,可以慢一点单不能快过采样率
	*/
	sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;// 选择采样时间 采样周期的 7.5 倍
	
	// 使用库函数对此通道进行配置
	if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
	{
		Error_Handler(); // 通道配置失败
	}
}
  • 13行:选择要设置的ADC;
  • 14行:选择ADC采集到的数据对齐格式,这里设置为右对齐;
  • 15行:失能ADC的扫描模式,本实验只涉及一个ADC,无需对通道组进行扫描;
  • 16行:使能ADC连续转换模式,本实验使用DMA进行多次采集,需要该ADC连续转换;
  • 17行:使能ScanConvMode的情况下,用于设置规则通道转换序列数,本实验不涉及;
  • 18行:失能规则组转换序列的间断模式,本实验不需要间断扫描;
  • 19行:使能DiscontinuousConvMode的情况下,用于设置子组的大小,本实验不涉及;
  • 20行:选择触发源,这里选择软件触发即可;
  • 23~26行:按前面设置的ADC属性进行初始化;
  • 28行:选择本ADC通道;
  • 29行:设置本ADC通道放在规则通道组的第一个转换位置;
  • 37行:设置通道的采样时间,采样时间越长,数据越稳定,但速率也就越慢,这里设置采样周期的7.5倍;
  • 40~43行:对以上设置进行配置;

前面使用“HAL_ADC_Init()”初始化ADC时,将会调用“HAL_ADC_MspInit()”,该函数需要用户自
己添加硬件相关初始化内容进行重构,如代码段 1.3.2 所示。

代码段 32.3.2 ADC 硬件相关初始化(driver_adc.c)

/*
 * 函数名:HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
 * 输入参数:无
 * 输出参数:hadc-ADC 句柄
 * 返回值:无
 * 函数作用:使能 ADC 句柄对应的 ADC 的时钟及其引脚配置
 * 函数说明:此函数本身是一个弱函数,在使用库函数 HAL_ADC_Init()的时候会被调用,用户需在自己的工程中重构此函数
*/
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
{
	// 定义 GPIO 的结构体变量
	RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
	GPIO_InitTypeDef GPIO_InitStruct = {0};
	
	PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC; // 选择要设置的外设 ADC
	PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6; // 选择 ADC 时钟源 72MHz/6=12MHz
	if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
	{
		Error_Handler();
	}
	
	if(hadc->Instance == ADC1) // 根据句柄判断进来的是哪一路 ADC
	{
		__HAL_RCC_ADC1_CLK_ENABLE(); // 使能 ADC1 的时钟
		ADC1_PORT_CLK_EN(); // 使能 ADC1 对应的 GPIO 的时钟
		__HAL_RCC_DMA1_CLK_ENABLE(); // 使能 DMA 时钟
		
		GPIO_InitStruct.Pin = ADC1_IN_PIN; // 选择 ADC1 的输入引脚
		GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 将引脚配置为模拟输入复用模式
		HAL_GPIO_Init(ADC1_IN_PORT, &GPIO_InitStruct); // 初始化选择的引脚
		
		hdma_adc1.Instance = DMA1_Channel1; // 选择要设置的 DMA(DMA1 的通道 1 可用于 ADC1)
		hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;// 外设到内存
		hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增
		hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
		hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据半字对齐
		hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 内存数据半字对齐
		hdma_adc1.Init.Mode = DMA_NORMAL; // DMA 正常模式
		hdma_adc1.Init.Priority = DMA_PRIORITY_LOW; // DMA 优先级低
		if (HAL_DMA_Init(&hdma_adc1) != HAL_OK) // 初始化以上参数
		{
			Error_Handler();
		}
		
		__HAL_LINKDMA(hadc,DMA_Handle,hdma_adc1); // 将 DMA 与 adc1 联系起来
		
		HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); // 设置 DMA1 中断优先级
		HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); // 使能 DMA1 中断
	}
}
  • 15行:选择要设置时钟的外设,这里选择外设ADC;
  • 16行:选择ADC时钟源,为PCLK2的6分频,即72MHz/6=12MHz;
  • 17~19行:根据以上设置配置外设时钟;
  • 22行:根据句柄判断是否为ADC1,如果是,才进行后面的硬件设置;
  • 24行:使能ADC1时钟;
  • 25行:使能ADC1对应的GPIO端口时钟;
  • 26行:使能DMA时钟;
  • 28行:选择要设置的ADC1引脚;
  • 29行:将引脚配置为模拟输入复用模式;
  • 30行:按以上参数,初始化该引脚;
  • 32行:选择要设置的DMA通道,由前面DMA章节可知,DMA1的通道1用于ADC1;
  • 33行:设置DMA方向:外设到内存;
  • 34行:设置DMA传输源地址的外设地址不递增;
  • 35行:设置DMA传输目标地址的内存地址地址;
  • 36行:设置外设数据按半字对齐;
  • 37行:设置内存数据按半字对齐;
  • 38行:设置DMA为正常模式;
  • 39行:设置DMA优先级为低优先级;
  • 40~43行:按以上设置初始化DMA;
  • 45行:将DMA于adc1联系起来;
  • 47行:设置DMA1中断优先级;
  • 48行:使能DMA1中断;
  1. 中断处理
    当设置好ADC和DMA后,启动ADC传输,ADC将开始采集转换,当转换完成后,会产生转换完成中断,同时产生DMA请求,DMA再将转换的数据,搬运到指定地址。这里需要为中断入口设置HAL库提供的中断处理函数,如代码段 1.3.3 所示。

代码段 1.3.3 中断处理函数(driver_adc.c)

/*
* 函数名:void DMA1_Channel1_IRQHandler(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:DMA1 通道 1 中断的中断处理函数
*/
void DMA1_Channel1_IRQHandler(void)
{
	HAL_DMA_IRQHandler(&hdma_adc1);
}

/*
 * 函数名:void ADC1_2_IRQHandler(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:无
 * 函数作用:ADC1、2 中断处理函数
*/
void ADC1_2_IRQHandler(void)
{
	HAL_ADC_IRQHandler(&hadc1);
}

/*
 * 函数名:void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
 * 输入参数:hadc-ADC 句柄
 * 输出参数:无
 * 返回值:无
 * 函数作用:ADC 转换完成回调函数,修改转换完成标志位
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc->Instance==ADC1)
	{
		conv_flag = 1;
	}
}
  1. 主函数控制逻辑
    在主函数里,初始化系统时钟、打印串口、按键和ADC后,循环检测按键是否按下。当按键按下后,使用HAL库提供的“HAL_ADC_Start_DMA()”启动ADC转换,待检测到转换完成,打印转换结果,如代码段1.3.4 所示。

代码段 1.3.4 主函数控制逻辑(main.c)

// 初始化 ADC
ADC_Init();

printf("**********************************************\n\r");
printf("-->嘻嘻嘻:www.xxx.net\n\r");
printf("-->DMA 处理 ADC 采样数据实验\n\r");
printf("**********************************************\n\r");

while(1)
{
	if(step == 0) // 按键按下
	{
		step = 0xFF;
		conv_flag = 0; // 转换是否完成标志
		
		HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_data, 100); // 使用 DMA 方式启动 ADC,采集 100 次
	}
	if(conv_flag == 1) // 转换完成
	{
		conv_flag = 0;
		
		printf("第%d 次采样结果:\n\r", run_cnt);
		for(i=0;i<100;i++) // 打印结果
		{
			printf("ADC_Data[%d]=%.2f V\n\r", i, (adc_data[i]*3.3/4096)); // 换算成对应电压值
		}
		run_cnt++;
	}
}

2. 内部温度传感器

2.1 关于内部温度传感器

STM32F1内部有一个温度传感器,可以监测CPU以及芯片周围的温度。该温度传感器与ADC1的通道16内部相连,温度传感器将温度转换为对应的电压,再由ADC转换成数字值。测量温度范围为-40℃~125℃,精度为±1.5℃。

如图 2.1.1 所示为内部温度传感器通道框图,通过TSVREFE控制位选择温度传感器,通过ADC1_IN16通道转换,得到转换数据。根据参考手册提供的典型值和计算公式如下:
在这里插入图片描述
其中,V 25 为温度传感器在25℃的电压值,典型值为1.43V;V SENSE 为当前采样计数出来的电压值;Avg_Slope是温度和V SENSE 的斜率,典型值为4.3mV/℃。

  • 图 2.1.1 内部温度传感器通道框图

2.2 硬件设计

内部温度传感器不涉及硬件设计。

2.3 软件设计

2.3.1 软件设计思路

实验目的:本实验通过ADC获取MCU内部温度传感器的温度。

  1. 初始化ADC属性相关参数:数据对齐方式、工作模式、触发源等;
  2. 初始化ADC涉及的硬件相关参数:相关时钟;
  3. 启动ADC采集,获取数据并进行计算,得到温度;
  4. 主函数编写控制逻辑:通过按键,打印MCU内部温度传感器的温度;

2.3.2软件设计讲解

  1. 初始化ADC
    ADC属性初始化部分和前面ADC实验基本一致,如代码段2.3.1 所示。

代码段 33.3.1 ADC 初始化(driver_adc.c)

/*
 * 函数名:void ADC_Init(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:无
 * 函数作用:初始化 ADC1 的参数:触发方式、数据格式、转换模式,通道配置及其采样时间
 *
*/
void ADC_Init(void)
{
	ADC_ChannelConfTypeDef sConfig;
	
	hadc1.Instance = ADC1; // 选择 ADC
	hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 选择 ADC 采集到的数据对齐格式
	hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 失能 ADC 的扫描模式
	hadc1.Init.ContinuousConvMode = DISABLE; // 失能 ADC 的连续转换模式
	hadc1.Init.NbrOfConversion = 1; // 使能 ScanConvMode 的情况下,用于设置规则通道转换序列数
	hadc1.Init.DiscontinuousConvMode = DISABLE; // 失能规则组转换序列的间断模式
	hadc1.Init.NbrOfDiscConversion = 1; // 使能 DiscontinuousConvMode 的情况下,用于设置子组的大小
	hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 外部触发源的选择(软件触发 ADC 采样)
	
	// 使用库函数对 ADC 参数初始化
	if(HAL_ADC_Init(&hadc1) != HAL_OK)
	{
		Error_Handler(); // 初始化失败
	}
	
	sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
	sConfig.Rank = ADC_REGULAR_RANK_1; // 放在规则通道组的第一个位置
	sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;// 选择采样时间
	
	// 使用库函数对此通道进行配置
	if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
	{
		Error_Handler(); // 通道配置失败
	}
	
	if(HAL_ADCEx_Calibration_Start(&hadc1) != HAL_OK) // ADC 自动校准
	{
		Error_Handler();
	}
}
  • 16行:失能ADC连续转换模式,本实验每次只采集一次,不用连续转换;
  • 39~42行:在使用前,对ADC先进行校准;

“HAL_ADC_MspInit()”里不再需要对GPIO引脚设置,采集数据数量不多,也不需要设置DMA,如代码段 1.3.2 所示。

代码段 33.3.2 ADC 硬件相关初始化(driver_adc.c)

/*
	 * 函数名:HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
	 * 输入参数:无
	 * 输出参数:hadc-ADC 句柄
	 * 返回值:无
	 * 函数作用:使能 ADC 句柄对应的 ADC 的时钟及其引脚配置
	 * 函数说明:此函数本身是一个弱函数,在使用库函数 HAL_ADC_Init()的时候会被调用,用户需在自己的工程中重构此函数
	*/
	void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
	{
		RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
		
		PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC; // 选择要设置的外设 ADC
		PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6; // 选择 ADC 时钟源 72MHz/6=12MHz
		if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
		{
			Error_Handler();
		}
		
		if(hadc->Instance == ADC1) // 根据句柄判断进来的是哪一路 ADC
		{
			__HAL_RCC_ADC1_CLK_ENABLE(); // 使能 ADC1 的时钟
		}
	}
  1. 获取数据
    所有的初始化完成后,便可以获取ADC数据,如代码段 33.3.3 所示。因为查询比较简单,没有使用中断,通过“HAL_ADC_PollForConversion()”查询等待ADC转换完成。

代码段1.3.3 获取内部温度传感器数据(driver_adc.c)

/*
 * 函数名:float GetAdcAnlogValue(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:返回一个 float 型值,即采样到的模拟电压值
 * 函数作用:获取 ADC 的采样到的内部温度
 *
 * 备注:此处参考电压为 3.3V
*/
float GetTemptureValue(void)
{
	uint16_t nData = 0;
	float nValue = 0.0;
	
	HAL_ADC_Start(&hadc1); // 启动 ADC1
	HAL_ADC_PollForConversion(&hadc1, 250); // 等待 ADC 转换结束
	nData = HAL_ADC_GetValue(&hadc1); // 得到转换数据
	nValue = (1.43 - nData*3.3/4096)*1000/4.3 + 25;
	
	// 返回计算得到的模拟电压值
	return nValue;
}
  1. 主函数控制逻辑
    在主函数里,先初始化时钟、串口、按键和ADC,检测按键状态,如果按下,调用“GetTemptureValue()”获取数并打印,如代码段1.3.4 所示。

代码段 1.3.4 主函数控制逻辑(main.c)

// 初始化 ADC
ADC_Init();

printf("**********************************************\n\r");
printf("-->嘻嘻嘻:www.xxx.net\n\r");
printf("-->采样芯片内部温度实验\n\r");
printf("**********************************************\n\r");

while(1)
{
	if(step==0)
	{
		step = 0xFF;
		value = GetTemptureValue(); // 获取采样值并使用 Debug 串口打印出来
		printf("芯片内部温度:%.2f℃ \n\r", value);
	}
}

3. 模数转换 —DAC

3.1 关于DAC

3.1.1 DAC 理论知识

计算机里处理的都是数字 0/1 信号,而自然界几乎都是模拟信号,比如音频信号、无线传输信号等,这就要求计算机具有模拟信号的输出能力,将数字信号(离散信号)转换为模拟信号(连续信号)的器件就叫数模转换器(Digital-to-Analog Converter,DAC)。

按原理可分为:Nyquist 型和过采样型。Nyquist 型转换器按其结构又可大致分为电阻分压型、R_2R 型、电荷分配型和电流驱动型。

如图 3.1.1 为DAC转换过程,输入的数字编码Din(dn1 : d0),按其权值大小转换成相应的模拟量并相加结果Aout与数字量Din成正比,即实现了D/A转换。

  • 图 3.1.1 D/A 转换过程
    在这里插入图片描述
    DAC的主要有三个性能指标:分辨率、建立时间和转换精度。
  • 分辨率:指DAC能输出的最小变化量,通常使用二进制有效位表示,反应了DAC输出模拟量的最
    小变化值。当输出量程一定时,位数越多,量化单位越小,误差越小,分辨率越高。比如一个12位的DAC,参考电压为3.3V,则其能输出的最小电压为:
    在这里插入图片描述
  • 建立时间:指DAC从输入信号变化开始,到输出模拟信号达到满刻度值(±1/2LSB)所需时间。根据建立时间得长短,可以将DAC分为超高速(<1μS)、高速(101μS)、中速(10010μS)、
    低速(≥100μS)。
  • 转换精度:指DAC输出的模拟电压实际值与理论值之间的偏差,通常为1个或半个最小数字量的变化量,表示为1LSB或1/2LSB。

3.1.2 STM32 的DAC

STM32F10x系列内部有两个8位或12位单调输出的DAC,每个DAC有一个输出通道。两个通道可同时转换,也可分别转换,每个通道都能触发DMA。DAC的的转换结果为8位或12位的二进制数,当工作在8位模式时,数据只能右对齐,当工作在12模式时,数据可以左对齐或右对齐。

  • 图 34.1.2 STM32 DAC 内部结构图
    在这里插入图片描述
    DAC内部结构比较简单,如图 3.1.2 所示,可以看作四部分组成。

① DAC 参考电压引脚
V DDA 和V SSA 为专门为模拟电路设计的独立电源,以过滤和屏蔽来自PCB上的毛刺和干扰。DAC为提高精度也使用V DDA 和V SSA 供电。

V REF+ 为DAC基准参考电压(Voltage Reference),输出的电压范围满足V SSA ≤V OUT ≤V REF+ ,2.4V<V REF+<V DDA <3.6V)。一般情况下,将V REF+ 与V DDA 连接,V SSA 连地,因此输出电压范围为0~3.3V。此时,当DAC位12位模式时,输出电压V IN 与数字量关系为:
在这里插入图片描述
② DAC 输出 引脚
DAC_OUTx(x=1~2)为DAC输出通道,DAC1_OUT对应PA4引脚,DAC2_OUT对应PA5引脚。使能DACx后(设置DAC_CR寄存器ENx为1),GPIO引脚(PA4或PA5)将连接到DAC_OUTx,经过启动唤醒时间t WAKEUP才能达到稳定。为了避免寄生电流消耗,通常将PA4/PA5引脚配置为模拟输入模式(AIN)。

③ DAC 控制逻辑
数据输出寄存器DORx将数据写入数模转换器,最后由ADC_OUTx输出。这里DORx是只读寄存器,它的数据来源于数据保存寄存器DHRx,因此我们实际需要操作的是DHRx寄存器。

根据DAC位数、对齐方式、单/双模式的不同,需要向不同的DHRx写入数据,如表 34.1.1 所示,为DHRx寄存器功能描述。假设当前为单通道模式,DAC1的数据为12为右对齐,则需要向DHR12R1写数据,才能传到DOR1,最终作用在DAC_OUT1(PA4引脚)上。

  • 表 3.1.1 DHRx 寄存器功能描述
    在这里插入图片描述
    TENx控制DAC硬件触发,如果未设置该位(TENx=0),则经过1个APB1时钟周期后,DAC_DHRx寄存器中存储的数据将自动转移到DAC_DORx寄存器;如果设置该位(TENx=1)且触发条件到来,则经过3个APB1时钟周期后,DAC_DHRx寄存器中存储的数据将自动转移到DAC_DORx寄存器。

WAVEx[1:0]控制DAC三角波的生成,MAMPx[3:0]决定三角波的振幅,不实验不涉及生成三角波,关闭即可。

DAC的两个通道都能产生DMA请求。将DMAENx置位使能,当发生外部触发时,就会产生DMA请求,DHRx寄存器数据将转移到DORx寄存器。

④ DAC 触发源
D/A转换需要触发信号才能开始工作,触发信号的产生方式通常由两钟。

  • 软件触发:将SWTRIGx位置1,转换立即开始,DHRx寄存器数据经过一个APB1时钟周期转移到DORx寄存器,SWTRIG由硬件复位,等待下一次转换;
  • 外部触发:支持定时器TIMx(x=2、4~8)、外部中断线EXTI_9触发。当外补时间发生时DHRx寄存器数据经过三个APB1时钟周期转移到DORx寄存器;

3.2 硬件设计

在开发板上,AC1_OUT对应的PA4引脚,DAC2_OUT对应的PA5引脚都复用为硬件SPI引脚,为了避免干扰,这两个引脚没有再连接其它功能模块。在本实验中,使用DAC1(PA4)输出DAC波形,ADC1_IN12(PC2)采集该波形,因此使用杜邦线将图 3.3.1 中编号34排针上的PC2和PA4连接即可。

3.3 软件设计

3.3.1软件设计思路

实验目的:本实验使用DAC输出模拟信号波形,ADC采集该模拟信号,通过串口发送采集数据给
Windows平台的上位机,上位机再将收到数据以图形化显示出来展现效果。

  1. 初始化定时器:配置定时器TIM2周期,产生TRGO输出;
  2. 初始化DAC:使能相关时钟、配置相关引脚、启用DMA,触发方式为TIM2触发;
  3. 初始化ADC:使能相关时钟、配置相关引脚、启用DMA;
  4. 主函数编写控制逻辑:启动DAC输出正弦波,启动ADC采样,最后将ADC采样数据通过串口发送给上位机;

3.3.2 软件设计讲解

  1. 初始化定时器

定时器的目的是触发DAC,让DAC以定时器的周期输出数据,大部分设置和前面定时器实现us延时类似,如代码段 3.3.1 所示。
代码段 3.3.1 TIM2 初始化(driver_timer.c)

/*
 * 函数名:void DACTimerInit(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:无
 * 函数作用:初始化定时器
*/
void DACTimerInit(void)
{
	TIM_MasterConfigTypeDef sMasterConfig = {0};
	
	// 定时器基本功能配置
	htim2.Instance = TIM2; // 使用定时器 2
	htim2.Init.Prescaler = 24*10-1; // 预分频系数 PSC=240-1
	htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
	htim2.Init.Period = 1000-1; // 自动装载器 ARR 的值(范围:0~0xFFFF)
	htim2.Init.ClockDivision = 0; // 时钟分频(与输入采样相关)
	//htim2.Init.RepetitionCounter = 0; // 重复计数器值,仅存在于高级定时器
	htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; // 不自动重新装载
	
	// 初始化上面的参数
	if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
	{
		Error_Handler();
	}
	
	sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; // 产生更新事件时,会在 TRGO 产生输出
	sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; // 主/从模式选择
	if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
	{
		Error_Handler();
	}
}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
	__HAL_RCC_TIM2_CLK_ENABLE(); // 使能 TIM2 时钟
}
  • 13~25行:将TIM2正常初始化;
  • 27~32行:设置TIM2在更新时间时,也产生TRGO,才能触发DAC;
  1. 初始化DAC

DAC的初始化依旧分两部分,一部分DAC属性相关设置初始化,另一部分是硬件相关初始化。属性相关初始化如代码段 3.3.2 所示。

代码段 3.3.2 DAC 初始化(driver_dac.c)

/********************************************************
 * 函数名:void DAC_Init(uint16_t channel)
 * 输入参数:channel-选择 DAC 的通道;1/2
 * 输出参数:无
 * 返回值:无
 * 函数作用:初始化 DAC,配置通道、触发方式
********************************************************/
void DAC_Init(void)
{
	DAC_ChannelConfTypeDef sConfig; // 定义 DAC 的通道设置结构体对象
	
	hdac_ch.Instance = DAC; // 选择 DAC1
	if(HAL_DAC_Init(&hdac_ch) != HAL_OK) // 初始化 DAC
	{
		Error_Handler();
	}
	
	sConfig.DAC_Trigger = DAC_TRIGGER_T2_TRGO; // 选择触发方式(由 TIM2 触发)
	sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE; // 不使用输出缓存
	
	if (HAL_DAC_ConfigChannel(&hdac_ch, &sConfig, DAC_CHANNEL_1) != HAL_OK) // 配置通道设置
	{
		Error_Handler();
	}
}
  • 12行:选择要设置的DAC,这里为DAC1;
  • 13行:DAC的结构体“DAC_HandleTypeDef”没有需要设置,这里直接初始化;
  • 18行:设置触发方式,选择由TIM2 TRGO触发;
  • 19行:不使用输出缓存;
  • 21~24行:使用以上参数配置该DAC通道;

“HAL_DAC_Init()”调用“HAL_DAC_MspInit()”进行硬件相关初始化,如代码段 3.3.3 所示。

代码段 3.3.3 DAC 硬件相关初始化(driver_dac.c)

/********************************************************
 * 函数名:void HAL_DAC_MspInit(DAC_HandleTypeDef *hdac)
 * 输入参数:hdac-DAC 结构体句柄
 * 输出参数:无
 * 返回值:无
 * 函数作用:初始化 DAC 的引脚、使能时钟
********************************************************/
void HAL_DAC_MspInit(DAC_HandleTypeDef *hdac)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	
	__HAL_RCC_DAC_CLK_ENABLE(); // 使能 DAC 时钟
	__HAL_RCC_DMA2_CLK_ENABLE(); // 使能 DMA2 时钟
	__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟
	
	GPIO_InitStruct.Pin = GPIO_PIN_4; // PA4
	GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 需要设置为模拟输入
	GPIO_InitStruct.Pull = GPIO_NOPULL; // 不上拉也不下拉
	HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化 PA4
	
	hdma_dac.Instance = DMA2_Channel3; // DMA2 通道 3 用于 DAC1
	hdma_dac.Init.Direction = DMA_MEMORY_TO_PERIPH; // 内存到外设
	hdma_dac.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增
	hdma_dac.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
	hdma_dac.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据半字对齐
	hdma_dac.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 内存数据半字对齐
	hdma_dac.Init.Mode = DMA_CIRCULAR; // DMA 循环模式
	hdma_dac.Init.Priority = DMA_PRIORITY_HIGH; // DMA 优先级高
	if (HAL_DMA_Init(&hdma_dac) != HAL_OK) // 初始化以上参数
	{
		Error_Handler();
	}
	
	__HAL_LINKDMA(hdac, DMA_Handle1, hdma_dac); // 将 DMA 与 dac 联系起来
	HAL_NVIC_SetPriority(DMA2_Channel3_IRQn, 1, 0); // 设置 DMA2 中断优先级
	HAL_NVIC_EnableIRQ(DMA2_Channel3_IRQn); // 使能 DMA2 中断
}
  • 12~14行:使能DAC、MA2、GPIOA时钟;
  • 16~19行:设置PA4为输入模拟模式;
  • 21~33行:配置DAC1的DMA;
  • 21行:由前面DMA章节可知,DMA2通道3用于DAC1传输
  • 22~24行:DAC要设置的数据在内存,因此DMA方向为内存到外设;
  • 25~26行:数据格式都是半字对齐,16位;
  • 27行:DMA模式为循环模式,一直循环将内存数据放到DAC;
  • 34行:将DMA与dac关联;
  • 35~36行:设置DMA2中断优先级和使能;

初始化完成后,使用HAL库提供的“HAL_DAC_Start_DMA()”启动DAC传输,需要指定DAC通道、要输出的数据地址、数据长度,以及对齐方式,如代码段 3.3.4 所示。

代码段 3.3.4 设置 DAC 输出值并启动(driver_dac.c)

/********************************************************
 * 函数名:void SetDacValue(uint32_t *value, uint16_t length)
 * 输入参数:value-设置 DAC 的输出值:0~4095
 * length-DAC 输出长度
 * 输出参数:无
 * 返回值:无
 * 函数作用:设置 DAC 的输出值
********************************************************/
void SetDacValue(uint32_t *value, uint16_t length)
{
	// 使用库函数使合法的设定值生效
	if(HAL_DAC_Start_DMA(&hdac_ch, DAC_CHANNEL_1, (uint32_t*)value, length, DAC_ALIGN_12B_R) != HAL_OK)
	{
		Error_Handler();
	}
	DAC_TimerStart(); // 启动定时器,周期性触发 DAC 输出
}
  • 12行:启动DAC输出;
  • 16行:启动定时器,周期性触发DAC输出;

启动DAC和定时器后,每个定时器周期,DAC都输出一次数据,每次传输完后,都会调用DAC转换完成回调函数,在此回调函数里修改完成完成转换标志位,如代码段 3.3.5 所示。
代码段 3.3.5 DAC 中断处理函数(driver_dac.c)

/*
 * 函数名:void DMA2_Channel3_IRQHandler(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:无
 * 函数作用:DMA2 通道 3 中断的中断处理函数
*/
void DMA2_Channel3_IRQHandler(void)
{
	HAL_DMA_IRQHandler(&hdma_dac);
}

/*
 * 函数名:void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac)
 * 输入参数:hdac-DAC 句柄
 * 输出参数:无
 * 返回值:无
 * 函数作用:DAC 转换完成回调函数
*/
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac)
{
	dac_flag = 1;
}
  1. 初始化ADC

DAC将在PA4引脚上,输出模拟信号,为了方便观察,这里使用ADC再采集DAC输出的数据。使用任意ADC都可以,这里使用PC2引脚的ADC1_IN12。ADC初始化设置部分,和前面ADC实验基本一致,这里不再赘述,如代码段 3.3.6 所示。

代码段 3.3.6 ADC 初始化(driver_adc.c)

/*
 * 函数名:void ADC_Init(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:无
 * 函数作用:初始化 ADC1 的参数:触发方式、数据格式、转换模式,通道配置及其采样时间
 *
*/
void ADC_Init(void)
{
	ADC_ChannelConfTypeDef sConfig;
	
	hadc1.Instance = ADC1; // 选择 ADC
	hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 选择 ADC 采集到的数据对齐格式
	hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 失能 ADC 的扫描模式
	hadc1.Init.ContinuousConvMode = ENABLE; // 使能 ADC 的连续转换模式
	hadc1.Init.NbrOfConversion = 1; // 使能 ScanConvMode 的情况下,用于设置规则通道转换序列数
	hadc1.Init.DiscontinuousConvMode = DISABLE; // 失能 ADC 非连续转换模式
	hadc1.Init.NbrOfDiscConversion = 1; // 使能 DiscontinuousConvMode 的情况下,用于设置子组的大小
	hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 外部触发源的选择(软件触发 ADC 采样)
	
	// 使用库函数对 ADC 参数初始化
	if(HAL_ADC_Init(&hadc1) != HAL_OK)
	{
		Error_Handler(); // 初始化失败
	}
	
	sConfig.Channel = ADC1_IN_CHANNEL;
	sConfig.Rank = ADC_REGULAR_RANK_1; // 放在规则通道组的第一个位置
	/*
	 * 采样总时间计算:(SamplingTime+12.5)/fadc
	 * fadc 在源文件 system_clk.c 中进行了配置,是系统时钟的 6 分频,fsystem=72MH-->fadc=12MHz
	 * f_sample = fadc/((SamplingTime+12.5)) = 600kHz
	 * 在设计采样率的时候,如果采样的是一段波形,必须满足奈奎斯特采样定理;如果采样的仅是一个电压值,根据时间要求设计即可
	 * 当使用定时器触发时,定时器的频率要与此采样率基本保持一致,可以慢一点单不能快过采样率
	*/
	sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;// 选择采样时间 采样周期的 7.5 倍
	
	// 使用库函数对此通道进行配置
	if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
	{
		Error_Handler(); // 通道配置失败
	}
}

/*
 * 函数名:HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
 * 输入参数:无
 * 输出参数:hadc-ADC 句柄
 * 返回值:无
 * 函数作用:使能 ADC 句柄对应的 ADC 的时钟及其引脚配置
 * 函数说明:此函数本身是一个弱函数,在使用库函数 HAL_ADC_Init()的时候会被调用,用户需在自己的工程中重构此函数
*/
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
{
	// 定义 GPIO 的结构体变量
	RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
	GPIO_InitTypeDef GPIO_InitStruct = {0};
	
	PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC; // 选择要设置的外设 ADC
	PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6; // 选择 ADC 时钟源 72MHz/6=12MHz
	if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
	{
		Error_Handler();
	}
	
	if(hadc->Instance == ADC1) // 根据句柄判断进来的是哪一路 ADC
	{
		__HAL_RCC_ADC1_CLK_ENABLE(); // 使能 ADC1 的时钟
		ADC1_PORT_CLK_EN(); // 使能 ADC1 对应的 GPIO 的时钟
		__HAL_RCC_DMA1_CLK_ENABLE(); // 使能 DMA 时钟
		
		GPIO_InitStruct.Pin = ADC1_IN_PIN; // 选择 ADC1 的输入引脚
		GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; //将引脚配置为模拟输入复用模式
		HAL_GPIO_Init(ADC1_IN_PORT, &GPIO_InitStruct); // 初始化选择的引脚
		
		hdma_adc1.Instance = DMA1_Channel1; // 选择要设置的 DMA(DMA1 的通道 1 可用于 ADC1)
		hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设到内存
		hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增
		hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
		hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据半字对齐
		hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 内存数据半字对齐
		hdma_adc1.Init.Mode = DMA_CIRCULAR; // DMA 循环模式
		hdma_adc1.Init.Priority = DMA_PRIORITY_LOW; // DMA 优先级低
		if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
		{
			Error_Handler();
		}
		
		__HAL_LINKDMA(hadc,DMA_Handle,hdma_adc1); // 将 DMA 与 adc1 联系起来
		
		HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); // 设置 DMA1 中断优先级
		HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); // 使能 DMA1 中断
	}
}

ADC采集完后会进入中断,在转换完成回调函数里修改转换完成标志,如代码段 3.3.7 所示。

代码段 3.3.7 ADC 中断函数(driver_adc.c)

/*
 * 函数名:void DMA1_Channel1_IRQHandler(void)
 * 输入参数:无
 * 输出参数:无
 * 返回值:无
 * 函数作用:DMA1 通道 1 中断的中断处理函数
*/
void DMA1_Channel1_IRQHandler(void)
{
	HAL_DMA_IRQHandler(&hdma_adc1);
}

/*
 * 函数名:void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
 * 输入参数:hadc-ADC 句柄
 * 输出参数:无
 * 返回值:无
 * 函数作用:ADC 转换完成回调函数
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc->Instance==ADC1)
	{
		conv_flag = 1;
	}
}
  1. 主函数 主函数 控制逻辑
    主函数先后初始化定时器、DAC和ADC。启动DAC输出,ADC采集,查询DAC输出完成标志和ADC采集完成标志,待完成后将ADC采集数据通过串口发出,如代码段 3.3.8 所示。

代码段 3.3.8 主函数控制逻辑(main.c)

// 初始化定时器和 ADC
DACTimerInit();
DAC_Init();

// 初始化 ADC
ADC_Init();

SetDacValue((uint32_t*)SINData, 100); // 启动 DAC 输出
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_data, 200); // 启动 ADC 采样

while(1)
{
	dac_flag = 0;
	conv_flag = 0;
	
	while( (!dac_flag) && (!conv_flag) ); // 等待 DAC 输出和 ADC 采样都完成
	HAL_UART_Transmit(&husart, (uint8_t*)adc_data, 200*2, 300); // 将采集的 ADC 数据通过串口发送给上位机
}

使用“SetDacValue()”设置DAC输出值,需要提前准备输出数据。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 如果要学习STM32单片机,有很多可用的学习资料和资源供你参考和学习。首先,STMicroelectronics官方网站是最好的信息来源之一。在官方网站上,你可以找到Data Briefs、Technical Notes、Application Notes和User Manuals等多种文档,这些文档可以帮助你了解不同型号的STM32单片机,并提供详细的技术细节和应用示例。 此外,STMicroelectronics还提供了免费的配套开发工具和软件,如STM32CubeIDE、STM32CubeMX和HAL库等。这些工具可以帮助你开发、调试和烧写STM32单片机的代码,并提供丰富的代码库和实例,方便你快速入门。 除了官方资料外,网络上还有大量的STM32单片机学习资料和教程。你可以通过搜索引擎找到许多相关博客、论坛和视频教程,其中包括了解STM32单片机的基础知识、使用各种开发环境和编程语言进行开发,以及实际项目的应用示例等。这些资源可以帮助你深入学习STM32单片机的各个方面,并解决你在学习和项目中遇到的问题。 同时,还有一些出版的教材和参考书籍,如《精通STM32单片机》、《STM32权威指南》等,这些书籍以系统化的方式解释了STM32单片机的原理和应用,可以作为深入学习的参考资料。 总之,STM32单片机学习资料是丰富多样的,从官方资料到网络资源、教程和书籍都是很好的学习参考。结合多种源的学习材料和实践经验,你可以更好地掌握STM32单片机的开发和应用。 ### 回答2: STM32是一种广泛应用于嵌入式系统开发的32位单片机系列,具有高性能、低功耗和丰富的外设资源。学习STM32单片机需要掌握其基本原理、应用开发和编程技术等方面的知识。 首先,可以通过阅读官方提供的STM32单片机资料来进行学习。STMicroelectronics公司为STM32系列提供了官方的技术手册、应用笔记、教程和参考设计等资料,其中包含了单片机的内部结构、外设使用方法以及开发工具的介绍,有助于初学者对单片机的基本概念和应用进行了解。 其次,可以参考一些经典的STM32单片机编程教程和实例进行学习。在互联网上有很多相关的学习资源,包括视频教程、电子书和在线课程等,这些资源可以帮助初学者快速掌握STM32单片机的编程技巧和开发流程,了解如何使用STM32 HAL库和CubeMX软件进行开发。 此外,参加STM32单片机的实践项目和实验也是非常重要的学习方式。可以利用开发板或者仿真软件进行实验,从简单的LED闪烁开始,逐步深入学习各种外设的使用方法,例如串口通信、PWM输出和ADC采集等,通过实际操作来加深对STM32单片机的理解和应用。 最后,与其他STM32单片机学习者进行交流和探讨也是学习的重要途径。可以加入相关的技术社区、论坛或者参加线下的技术交流活动,与其他爱好者一起交流心得、解决问题和分享经验,共同进步。 综上所述,学习STM32单片机需要结合官方资料、编程教程、实践项目和交流讨论等多种方式,通过理论学习和实践操作相结合的方式来提高自己的技能和能力。只有不断学习和实践,才能逐步掌握STM32单片机的应用开发技术,发挥出其强大的功能。 ### 回答3: STM32单片机是一款由意法半导体公司推出的32位ARM Cortex-M系列微控制器。学习STM32单片机需要掌握一定的电子基础知识和C语言编程能力。以下是一些可供学习STM32单片机的资料推荐: 1. 官方资料:意法半导体官方网站提供了丰富的STM32单片机系列产品的技术文档、数据手册、应用笔记以及示例代码等,这些资料对于初学者和进阶者都非常有帮助。 2. 教材和教程:市面上有很多针对STM32单片机的教材和教程,其中一些是由专业人士撰写的,具有系统性和深度,适合系统学习。另外,也有一些网上的教程、博客和视频教程,可以提供实际操作示例和案例分析。 3. 社区论坛和博客:STM32单片机学习过程中,遇到问题时可以向社区论坛提问和交流。ST社区、电子爱好者论坛、知乎等地都有相关的技术讨论区,可以从其他人的经验中获得帮助。此外,还有一些博客是由学习STM32的爱好者写的,分享各种学习心得和项目经验。 4. 实验平台和开发板:购买一块能够容易上手的STM32开发板,如ST-Link V3 Mini开发板等,这样可以借助官方提供的开发环境和示例程序,快速上手进行实验和开发。 5. 项目实战:在学习的过程中,可以选择一些具体的项目进行实战。可以从简单的LED闪烁开始,逐步扩展到涉及串口通信、蓝牙、传感器和外设等更复杂的项目。 总之,学习STM32单片机需要结合官方资料、教材和教程、社区讨论和项目实战等多种资源,根据自己的兴趣和基础情况选择合适的学习路径,坚持实践,不断积累经验,就能够逐渐掌握STM32单片机的原理和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值