【STM32系列文章】
STM32-01-认识单片机
STM32-02-基础知识
STM32-03-HAL库
STM32-04-时钟树
STM32-05-SYSTEM文件夹
STM32-06-GPIO
STM32-07-外部中断
STM32-08-串口
STM32-09-IWDG和WWDG
STM32-10-定时器
STM32-11-电容触摸按键
STM32-12-OLED模块
STM32-13-MPU
STM32-14-FSMC_LCD
STM32-15-DMA
STM32-16-ADC
STM32-17-DAC
文章目录
STM32-16-ADC
1. ADC简介
-
什么是ADC:
ADC
,Analog-to-digital converter
,即模拟/数字转换器,可以将外部的模拟信号转换为数字信号。STM32F103系列芯片拥有3个ADC(C8T6只有2个),这些ADC可以独立使用,其中 ADC1和ADC2还可以组成双重模式(提高采样率)。STM32的ADC是12位逐次逼近型的模拟数字转换器。它有18个通道,可测量16个外部和2个内部信号源,其中ADC3根据CPU引脚的不同其通道数也不同,一般有8个外部通道。ADC中的各个通道的AD转换可以单次、连续、扫描或间断模式执行。ADC的结果可以以左对齐或者右对齐存储在16位数据寄存器中。
在嵌入式系统中,ADC通常用于采集传感器的数据,例如压力、温度、图像传感器等参数,被转换成电压后,通过ADC转换为数字量,供单片机处理,单片机根据处理结果进行相关控制和显示。
-
主要特性:
- 12位分辨率
- 转换结束、注入转换结束和发生模拟看门狗时产生中断
- 单次和连续转换模式
- 自校准
- 带内嵌数据一致性的数据对齐
- 采样间隔可以按通道分别编程
- 规则转换和注入转换均有外部触发选项
- 间断模式
- 双重模式
- ADC转换时间:时钟为72MHz为1.17us
- ADC供电要求:2.4V到3.6V
- ADC输入范围:Vref-<=Vin<=Vref+
- 规则通道转换期间有DMA请求产生
-
常见的ADC类型:
ADC电路类型 优点 缺点 并联比较型 转换速度最快 成本高、功耗高,分辨率低 逐次逼近型 结构简单,功耗低 转换速度较慢 并联比较型(Flash型)和逐次逼近型(SAR型)是两种常见的模数转换器(ADC)架构,它们在工作原理、速度、功耗、精度等方面有明显的区别。
并联比较型ADC(Flash ADC)
工作原理:
- 并联比较型ADC使用一组并行的比较器,将输入模拟信号同时与参考电压分压网络进行比较。
- 每个比较器对应一个特定的电压范围,输出一个二进制码。
- 比较器的输出直接决定ADC的数字输出,无需逐步逼近。
特点:
- 速度快:由于所有比较器同时工作,转换时间非常短,适合高频信号采样。
- 高功耗:大量并行比较器和电阻网络消耗较多电能。
- 高复杂度:电路复杂度随分辨率成指数增长,如8位ADC需要255个比较器。
- 应用场景:高速数据采集、视频处理、通信系统等。
逐次逼近型ADC(SAR ADC)
工作原理:
- 逐次逼近型ADC通过一个逐次逼近寄存器(SAR)和一个比较器工作。
- 它逐步逼近输入模拟信号,从最高位到最低位,逐位进行比较。
- 每次比较后,根据比较结果决定下一个比较的方向,直到所有位都确定。
特点:
- 速度适中:转换速度适中,比Flash ADC慢,但比积分型ADC快。
- 低功耗:由于只使用一个比较器,功耗较低。
- 高分辨率:适合高分辨率应用,电路复杂度与分辨率成线性关系。
- 应用场景:低功耗应用、音频采样、精密测量仪器等。
特点 并联比较型ADC 逐次逼近型ADC 转换速度 非常快(纳秒级) 适中(微秒级) 功耗 高 低 电路复杂度 高(比较器数量多) 适中(逐次逼近寄存器) 分辨率 适中(通常8位或以下) 高(可达16位或以上) 典型应用 高速数据采集、视频处理 音频采样、精密测量 并联比较型ADC适合需要极高转换速度的应用,而逐次逼近型ADC则在功耗和分辨率之间提供了良好的平衡,适合多种精密测量和中速数据采集的应用。
-
ADC的特性参数
- 分辨率: 表示ADC能辨别的最小模拟量,用二进制位表示
- 转换时间: 表示完成一次A/D转换所需要的时间,转换时间越短,采样率越高
- 精度: 最小刻度基础上叠加各种误差的参数,精度受ADC性能、温度和气压等影响
- 量化误差: 用数字量近似表示模拟量,采用四舍五入原则,此过程产生的误差为量化误差
2. ADC工作原理
ADC框图
-
①输入电压
-
②输入通道
确定好ADC输入电压后,通过ADC输入通道把外部电压输送到ADC转换器中,下面是ADC1~3对应的通道。
-
③转换顺序
在STM32的ADC转换过程中,规则组和注入组是两种不同的ADC转换序列,它们在配置、使用场景和优先级等方面存在显著区别。
规则组(Regular Group)
定义和特点:
- 规则组是常规的ADC转换序列,通常用于连续的、周期性的ADC采样。
- 可以包含多个通道,这些通道按照配置的顺序依次进行转换。
- 规则组的转换可以由多种触发源启动,如软件触发、定时器触发等。
应用场景:
- 常用于采集传感器数据、定期采样信号等场景。
- 适用于实时性要求较高的应用,但相对注入组来说,优先级较低。
触发源:
- 支持多种触发源,包括软件触发和硬件触发(如定时器事件)。
- 可设置为连续转换模式或单次转换模式。
注入组(Injected Group)
定义和特点:
- 注入组是为了在规则转换之外进行临时或优先转换的一种机制。
- 通常包含少量通道,用于处理突发性或优先级更高的ADC采样需求。
- 注入组转换有独立的触发源,与规则组转换互不干扰。
应用场景:
- 常用于优先级更高的采样需求,如监测关键参数的变化、快速响应事件等。
- 注入组的转换结果可以通过中断或DMA方式快速处理,适用于突发事件的处理。
触发源:
- 注入组有独立的触发源,可以是外部事件、定时器触发等。
- 通常由特定的事件触发,不会像规则组那样进行连续转换。
总结:
特点 规则组(Regular Group) 注入组(Injected Group) 转换触发 多种触发源(软件、定时器等) 独立触发源(外部事件、定时器等) 转换顺序 配置的通道顺序 配置的少量优先通道 优先级 相对较低 相对较高 应用场景 定期采样、传感器数据采集 关键参数监测、突发事件处理 转换模式 连续或单次转换 通常为单次转换 结果处理 常规方式处理 可通过中断或DMA快速处理
规则序列
规则组最多允许16个输入通道进行转换,那么就需要设置通道转换的顺序,即规则序列。
规则序列寄存器控制关系如下:
注入序列
注入组最大允许4个通道输入,它的注入序列由JSQR寄存器配置。
注入序列寄存器控制关系如下:
-
④触发源
规则组外部触发使用方法是将
EXTTRIG
位置 1,并且通过EXTSET[2:0]
位选择规则组启动转换的触发源。如果EXTSET[2:0]
位设置为 111,那么可以通过SWSTART
为启动 ADC 转换, 相当于软件触发。注入组外部触发使用方法是将
JEXTTRIG
位置 1,并且通过JEXTSET[2:0]
位选择注入组启动转换的触发源。如果JEXTSET[2:0]
位设置为 111,那么可以通过JSWSTART
为启动 ADC 转换,相当于软件触发。
-
⑤转换时间
ADC的输入时钟是由PCLK2经过分频产生的,分频系数是由
RCC_CFGR
寄存器的ADCPRE[1:0]
位设置的,可选择 2/4/8/16 分频。需要注意的是,ADC 的输入时钟频率最大值是14MHz,如果超过这个值将会导致 ADC 的转换结果准确度下降。 一般我们设置PCLK2
为 72MHz。为了不超过 ADC 的最大输入时钟频率 14MHz,我们设置 ADC 的预分频器分频系数为 6,就可以得到 ADC 的输入时钟频率为 72MHz/6,即 12MHz。
-
⑥数据寄存器
ADC 规则数据寄存器(ADC_DR)
ADC规则组数据寄存器ADC_DR是一个32位的寄存器,独立模式时只使用到该寄存器低16位保存ADC1/2/3的规则转换数据。在双ADC模式下,高16位用于保存
ADC2
转换的数据,低16位用于保存ADC1
转换的数据。因为ADC的精度是12位的,ADC_DR寄存器无论高16位还是低16w位,存放数据的位宽都是16位的,所以允许选择数据对齐方式。由ADC_CR2寄存器的ALIGN位设置数据对齐方式,可选择:右对齐或者左对齐。
规则组16个输入通道只对应一个数据寄存器,所以通道转换完成后要及时取出数据,比较常用的方法是使用DMA模式。当规则组的转换结束后,就会产生DMA请求,这样就可以把数据及时搬运到用户指定的目的地址存放。只有ADC1和ADC3可以产生DMA请求,而ADC2转换的数据可以通过双ADC模式,利用ADC1的DMA功能传输。
ADC 注入数据寄存器x(ADC_JDRx)(x=1~4)
ADC注入数据寄存器有4个,注入组最多有4个输入通道,刚好每个通道都有自己对应的数据寄存器。
ADC_JDRx
寄存器是32位的,低16位有效,高16位保留,数据也同样需要选择对齐方式。也是由ADC _CR2
寄存器的ALIGN
位设置数据对齐方式,可选择:右对齐或者左对齐。 -
⑦中断
中断事件 事件标志 使能控制位 规则通道转换结束 EOC EOCIE 注入通道转换结束 JEOC JEOCIE 设置了模拟看门狗状态位 AWD AWDIE 规则组和注入组的转换结束后,除了可以产生中断外,还可以产生 DMA 请求,我们利用DMA 及时把转换好的数据传输到指定的内存里,防止数据被覆盖。
-
⑧单次转换模式和连续转换模式
-
⑨扫描模式
3. 单通道ADC采集实验
1. 相关寄存器
-
ADC控制寄存器1(ADC_CR1)
-
ADC控制寄存器2(ADC_CR2)
-
ADC采样事件寄存器1(ADC_SMPR1)
-
ADC采集事件寄存器2(ADC_SMPR2)
ADC 采样时间设置需要由两个寄存器设置,
ADC_SMPR1
和ADC_SMPR2
,分别设置通道 10~17 和通道 0~9 的采样时间,每个通道用 3 个位设置。 -
ADC规则序列寄存器1(ADC_SQR1)
ADC_SQR2
ADC_SQR3
L[3:0]用于设置规则组序列的长度,取值范围:015,表示规则组的长度是116。本实验只用了1个输入通道,所以L[3:0]位设置为0000即可。
SQ13[4:0]~SQ16[4:0]位设置规则组序列的第 13~16 个转换编号,第 1~12 个转换编号的设置请查看 ADC_SQR2 和 ADC_SQR3 寄存器。
-
ADC规则数据寄存器(ADC_DR)
-
ADC状态寄存器(ADC_SR)
2. 配置步骤
-
配置ADC工作参数、ADC校准
HAL_ADC_Init() HAL_ADCEx_Calibration_Start()
-
ADC MSP初始化
HAL_ADC_MspInit()
-
配置ADC相应通道相关参数
HAL_ADC_ConfigChannel()
-
启动A/D转换
HAL_ADC_Start()
-
等待规则通道转换完成
HAL_ADC_PollForConversion()
-
获取规则通道A/D转换结果
HAL_ADC_GetValue()
-
具体函数功能
函数 主要寄存器 主要功能 HAL_ADC_Init() CR1、CR2 配置ADC工作参数 HAL_ADCEx_Calibration_Start() CR2 ADC校准 HAL_ADC_MspInit() 无 存放NVIC、CLOCK、GPIO初始化代码 HAL_RCCEx_PeriphCLKConfig() RCC_CFGR 设置扩展外设时钟,如:ADC、RTC等 HAL_ADC_ConfigChannel() SQRx、SMPRx 配置ADC相应通道的相关参数 HAL_ADC_Start() CR2 启动A/D转换 HAL_ADC_PollForConversion() SR 等待规则通道转换完成 HAL_ADC_GetValue() DR 获取规则通道A/D转换结果
3. 代码实现
-
功能: 采集ADC1通道1(PA1)上面的电压,并在LCD模块上面显示ADC规则数据寄存器12位的转换值以及将该值换算成电压后的电压值。使用杜邦线将ADC和RV1排针连接,使得PA1连接到电位器上,然后将ADC采集到的数据和转换后的电压值在TFTLCD屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0闪烁,提示程序运行。
-
ADC单通道初始化函数
void adc_init(void) { ADC_ChannelConfTypeDef adc_ch_conf; g_adc_handle.Instance = ADC1; //选择ADC1 g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; //数据对齐方式为右对齐 g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; //非扫描模式,仅使用一个通道 g_adc_handle.Init.ContinuousConvMode = DISABLE; //关闭连续转换模式 g_adc_handle.Init.NbrOfConversion = 1; //通道数选择1个 g_adc_handle.Init.DiscontinuousConvMode = DISABLE; //禁止规则通道组间断模式 g_adc_handle.Init.NbrOfDiscConversion = 0; //配置间断模式的规则通道个数 g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发方式 HAL_ADC_Init(&g_adc_handle); //进行初始化 HAL_ADCEx_Calibration_Start(&g_adc_handle); //校准ADC adc_ch_conf.Channel = ADC_CHANNEL_1; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_handle, &adc_ch_conf); //通道配置 }
g_adc_handle.Instance = ADC1;
: 选择ADC1作为转换的ADC模块。g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;
: 数据右对齐。g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;
: 禁用扫描模式,仅使用一个通道。g_adc_handle.Init.ContinuousConvMode = DISABLE;
: 禁用连续转换模式。g_adc_handle.Init.NbrOfConversion = 1;
: 仅使用一个转换通道。g_adc_handle.Init.DiscontinuousConvMode = DISABLE;
: 禁用间断模式。g_adc_handle.Init.NbrOfDiscConversion = 0;
: 设置间断模式的规则通道数为0。g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;
: 设置软件触发转换。adc_ch_conf.Channel = ADC_CHANNEL_1;
: 选择ADC通道1。adc_ch_conf.Rank = ADC_REGULAR_RANK_1;
: 设置通道1为规则组的第一个转换通道。adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
: 采样时间为239.5个ADC时钟周期。STM32的ADC1模块进行初始化和通道配置,为后续的ADC数据采集做好准备。具体来说,初始化配置包括设置数据对齐方式、转换模式和触发方式等。通过校准确保ADC转换的精度,然后配置一个规则组通道,使得ADC可以在指定通道上进行采样并转换。
-
ADC MSP初始化函数
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { GPIO_InitTypeDef gpio_init_struct; RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; //使能GPIOA和ADC1的时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_ADC1_CLK_ENABLE(); // 配置GPIOA的引脚1为模拟输入模式 gpio_init_struct.Pin = GPIO_PIN_1; gpio_init_struct.Mode = GPIO_MODE_ANALOG; //模拟输入 HAL_GPIO_Init(GPIOA, &gpio_init_struct); //配置ADC的时钟源和分频系数 adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; //ADC外设时钟 adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;//分频因子6,时钟为72M/6=12MHz HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); //设置ADC时钟 } }
配置ADC1的时钟源和分频系数。这里选择了RCC的ADC外设时钟,并将ADC的时钟频率设置为系统时钟(72MHz)除以6,即12MHz。这有助于确保ADC1以适当的频率运行,从而保证其性能和精度。
-
启动ADC并获取转换结果函数
uint32_t adc_get_result(void) { // 启动A/D转换 HAL_ADC_Start(&g_adc_handle); // 开启ADC // 等待规则通道转换完成 HAL_ADC_PollForConversion(&g_adc_handle, 10); // 轮询转换,等待完成 // 获取规则通道A/D转换结果 return (uint16_t)HAL_ADC_GetValue(&g_adc_handle); // 返回最近一次规则组的转换结果 }
-
启动A/D转换:
HAL_ADC_Start(&g_adc_handle);
g_adc_handle
是先前定义的并初始化好的 ADC 句柄。此操作会开始对选定的ADC通道进行模数转换。 -
等待规则通道转换完成:
HAL_ADC_PollForConversion(&g_adc_handle, 10);
调用
HAL_ADC_PollForConversion
函数进行轮询,等待转换完成。这里设置了一个超时值为10个时钟周期。如果在超时时间内转换完成,函数会返回并继续执行;否则,它会阻塞直到超时或者转换完成。 -
获取规则通道A/D转换结果:
return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
调用
HAL_ADC_GetValue
函数获取ADC转换结果。返回的值是最近一次转换的结果,类型为uint16_t
,因为一般ADC的结果是16位。
-
-
主函数
int main(void) { uint16_t adcx; //ADC换算后的数字量 float temp; //转换后的电压值 HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ delay_init(72); /* 延时初始化 */ usart_init(115200); /* 串口初始化为115200 */ led_init(); /* 初始化LED */ lcd_init(); /* 初始化LCD */ oled_init(); //OLED初始化 adc_init(); lcd_show_string(110, 50, 240, 16, 32, "STM32", GREEN); lcd_show_string(105, 90, 240, 16, 24, "ADC TEST", GREEN); lcd_show_string(30, 150, 240, 16, 24, "CH1_VAL:", BLUE); lcd_show_string(30, 180, 240, 16, 24, "CH1_VOL:0.000V", BLUE); oled_show_string(10,0,"ADC TEST",24); oled_show_string(0, 25, "CH1_VAL:", 16); oled_show_string(0, 41, "CH1_VOL:0.000V", 16); oled_refresh_gram(); while (1) { adcx = adc_get_result(); //获取ADC转换后的结果 lcd_show_xnum(126, 150, adcx, 5, 24, 0, BLUE); oled_show_num(65, 25, adcx, 5, 16); //在LCD和OLED上显示电压值整数部分 temp = (float)adcx * (3.3 / 4096); adcx = temp; lcd_show_xnum(126, 180, adcx, 1, 24, 0, BLUE); oled_show_num(64, 41, adcx, 1, 16); //在LCD和OLED上显示电压值小数部分 temp -= adcx; temp *= 1000; lcd_show_xnum(150, 180, temp, 3, 24, 0x80, BLUE); oled_show_num(80, 41, temp, 3, 16); LED0_TOGGLE(); delay_ms(100); oled_refresh_gram(); } }
实验结果:
作用:
- 实现ADC转换,并将结果显示在LCD和OLED上,实时显示ADC通道的数值和对应的电压值。
影响:
- 可以实时监控ADC通道的输入电压,适用于需要监测模拟信号的应用,如传感器数据读取。
- 通过LED的切换,可以直观地知道系统是否在运行。
- 延时和轮询会占用CPU资源,对实时性有一定影响,如果有更高的实时性要求,可以考虑使用中断或DMA。
4. 单通道ADC采集(DMA读取)实验
-
功能: 使用 ADC 采集(DMA 读取)通道 1(PA1)上面的电压,在 LCD 和OLED模块上面显示 ADC 转换值以及换算成电压后的电压值。使用短路帽将 ADC 和 RV1 排针连接,使得 PA1 连接到电位器上,然后将 ADC 采集到的数据和转换后的电压值在 TFTLCD 屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0 闪烁,提示程序运行。
-
ADC_DMA初始化函数
void adc_dma_init(uint32_t mar) { ADC_ChannelConfTypeDef adc_ch_conf = {0}; __HAL_RCC_DMA1_CLK_ENABLE(); g_dma_adc_handle.Instance = DMA1_Channel1; g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; g_dma_adc_handle.Init.Mode = DMA_NORMAL; g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; HAL_DMA_Init(&g_dma_adc_handle); //2.将DMA和ADC句柄连接起来 __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle); g_adc_dma_handle.Instance = ADC1; //选择ADC1 g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; //数据对齐方式为右对齐 g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; //非扫描模式,仅使用一个通道 g_adc_dma_handle.Init.ContinuousConvMode = ENABLE; //打开连续转换模式 g_adc_dma_handle.Init.NbrOfConversion = 1; //通道数选择1个 g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE; //禁止规则通道组间断模式 g_adc_dma_handle.Init.NbrOfDiscConversion = 0; //配置间断模式的规则通道个数 g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发方式 HAL_ADC_Init(&g_adc_dma_handle); //进行初始化 HAL_ADCEx_Calibration_Start(&g_adc_dma_handle); //校准ADC adc_ch_conf.Channel = ADC_CHANNEL_1; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf); //通道配置 HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0); HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0); }
总结:
- 启用DMA1时钟
__HAL_RCC_DMA1_CLK_ENABLE();
- 使能DMA1的时钟,确保DMA1能够正常工作。
- 配置DMA
g_dma_adc_handle.Instance = DMA1_Channel1; g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; // 数据从外设到内存 g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增 g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增 g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据对齐为16位 g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 内存数据对齐为16位 g_dma_adc_handle.Init.Mode = DMA_NORMAL; // 普通模式 g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; // 优先级为中等 HAL_DMA_Init(&g_dma_adc_handle);
- 初始化
DMA1的通道1
,配置DMA的数据传输方向、地址递增模式、数据对齐方式、传输模式和优先级。
- 将DMA和ADC句柄连接起来
__HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);
- 将DMA句柄与ADC句柄连接,使ADC能够使用DMA传输数据。
- 初始化ADC
g_adc_dma_handle.Instance = ADC1; // 选择ADC1 g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐 g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; // 非扫描模式 g_adc_dma_handle.Init.ContinuousConvMode = ENABLE; // 连续转换模式 g_adc_dma_handle.Init.NbrOfConversion = 1; // 单通道 g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE; // 禁止间断模式 g_adc_dma_handle.Init.NbrOfDiscConversion = 0; // 间断模式转换数 g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发 HAL_ADC_Init(&g_adc_dma_handle);
- 配置并初始化
ADC1
,包括数据对齐方式、扫描模式、连续转换模式、转换通道数、间断模式和触发方式。
- 校准ADC
HAL_ADCEx_Calibration_Start(&g_adc_dma_handle);
- 启动ADC的校准,以提高转换精度。
- 配置ADC通道
adc_ch_conf.Channel = ADC_CHANNEL_1; // 通道1 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; // 规则组第1个转换 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 采样时间 HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);
- 配置ADC的输入通道、转换序列和采样时间。
- 配置DMA中断优先级并启用中断
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
- 设置DMA1通道1中断的优先级,并启用中断。
- 启动DMA传输和ADC DMA模式
HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0); HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0);
- 启动DMA传输,将ADC数据寄存器的地址作为源地址,
mar
作为目标地址,并指定传输数据量。 - 启动ADC的DMA模式,使得ADC转换完成后通过DMA传输数据。
代码的功能:
- 使能DMA和ADC时钟:确保DMA和ADC正常工作。
- 配置DMA:设置DMA传输的方向、地址递增模式、数据对齐方式、传输模式和优先级。
- 初始化ADC:配置ADC的工作模式、转换方式和触发方式。
- 校准ADC:提高ADC转换的精度。
- 配置ADC通道:指定ADC的输入通道和采样时间。
- 配置中断:设置DMA中断的优先级并启用中断,以便在数据传输完成后处理中断。
- 启动DMA传输和ADC DMA模式:实现ADC转换结果通过DMA自动传输到指定内存地址,提高数据传输效率和系统性能。
对程序的影响:
- 提高数据传输效率:使用DMA自动传输数据,减少CPU的负担。
- 自动化数据采集:通过DMA实现ADC数据的自动传输,无需手动处理数据搬运。
- 实时数据处理:配置中断处理机制,可以在数据传输完成后立即处理,提高系统的实时性和响应速度。
-
ADC MSP初始化函数
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { GPIO_InitTypeDef gpio_init_struct; RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; //使能时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_ADC1_CLK_ENABLE(); //配置工作模式 gpio_init_struct.Pin = GPIO_PIN_1; gpio_init_struct.Mode = GPIO_MODE_ANALOG; //模拟输入 HAL_GPIO_Init(GPIOA, &gpio_init_struct); adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; //ADC外设时钟 adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6; //分频因子6,时钟为72M/6=12MHz HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); //设置ADC时钟 } }
-
使能一次ADC DMA传输函数
void adc_dma_enable(uint16_t cndtr) { ADC1->CR2 &= ~(1 << 0); //CONT位置1,关闭连续转换模式 DMA1_Channel1->CCR &= ~(1 << 0); //禁用DMA通道1 while(DMA1_Channel1->CCR & (1 << 0)); //等待DMA通道1禁用完成 DMA1_Channel1->CNDTR = cndtr; //设置DMA传输数据量 DMA1_Channel1->CCR |= 1 << 0; //启用DMA通道1 ADC1->CR2 |= 1 << 0; //开启ADC1 ADC1->CR2 |= 1 << 22; //启用ADC1的DMA请求 }
总结:
-
关闭ADC连续转换模式:
ADC1->CR2 &= ~(1 << 0);
- 清除ADC1控制寄存器2(CR2)的第0位,关闭连续转换模式。
-
禁用DMA通道1:
DMA1_Channel1->CCR &= ~(1 << 0);
- 清除DMA1通道1控制寄存器(CCR)的第0位,禁用DMA通道1。
-
等待DMA通道1禁用完成:
while(DMA1_Channel1->CCR & (1 << 0));
- 使用while循环等待,直到DMA1通道1控制寄存器的第0位被清除,确保DMA通道1已完全禁用。
-
设置DMA传输数据量:
DMA1_Channel1->CNDTR = cndtr;
- 将传入的参数
cndtr
赋值给DMA1通道1的数据传输数量寄存器(CNDTR),指定DMA传输的数据量。
- 将传入的参数
-
启用DMA通道1:
DMA1_Channel1->CCR |= 1 << 0;
- 设置DMA1通道1控制寄存器的第0位,启用DMA通道1。
-
开启ADC1:
ADC1->CR2 |= 1 << 0;
- 设置ADC1控制寄存器2(CR2)的第0位,开启ADC1。
- 启用ADC1的DMA请求:
ADC1->CR2 |= 1 << 22;
- 设置ADC1控制寄存器2(CR2)的第22位,启用ADC1的DMA请求。这样,ADC转换完成后将自动通过DMA将数据传输到指定的存储位置。
作用:
- 启用DMA:通过DMA自动传输ADC转换的数据,减少CPU的负担,提高数据传输效率。
- 启用ADC连续转换模式:让ADC能够连续进行多次转换,而无需每次都启动转换。-
-
-
ADC DMA采集中断服务函数
void DMA1_Channel1_IRQHandler(void) { if(DMA1->ISR & (1<<1)) { g_adc_dma_sta = 1; //清除中断标志位 DMA1->IFCR |= (1 << 1); } }
总结:
-
中断处理:
- 该函数是DMA1通道1的中断处理函数。当DMA1通道1触发中断时,系统会调用这个函数。
-
检查中断标志:
if(DMA1->ISR & (1<<1))
:检查DMA1的中断状态寄存器(ISR)
的第1位是否被置位。这一位表示DMA1通道1的传输完成中断。如果该位被置位,说明DMA1通道1完成了数据传输。
-
设置状态标志:
g_adc_dma_sta = 1;
:设置一个全局标志位g_adc_dma_sta
,表示DMA传输已完成。这个标志位可以在主程序或者其他地方用来判断DMA传输状态。
-
清除中断标志:
DMA1->IFCR |= (1 << 1);
:清除DMA1通道1的传输完成中断标志位。这一步是必须的,否则中断处理程序会再次触发。
作用总结:
- 响应DMA传输完成中断:当DMA1通道1完成数据传输时,会触发中断。
- 设置传输完成标志:通过设置
g_adc_dma_sta
,主程序或其他代码可以检测到DMA传输已完成,并进行后续处理。 - 清除中断标志位:清除中断标志位,以便系统能处理下一次DMA传输完成中断。
-
-
主函数
int main(void) { uint16_t i; uint16_t adcx; uint32_t sum; float temp; HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ delay_init(72); /* 延时初始化 */ usart_init(115200); /* 串口初始化为115200 */ led_init(); /* 初始化LED */ lcd_init(); /* 初始化LCD */ oled_init(); adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */ oled_show_string(10,0,"ADC TEST",24); oled_show_string(0, 25, "CH1_VAL:", 16); oled_show_string(0, 41, "CH1_VOL:0.000V", 16); oled_refresh_gram(); lcd_show_string(110, 50, 240, 16, 32, "STM32", GREEN); lcd_show_string(105, 90, 240, 16, 24, "ADC TEST", GREEN); lcd_show_string(30, 150, 240, 16, 24, "CH1_VAL:", BLUE); lcd_show_string(30, 180, 240, 16, 24, "CH1_VOL:0.000V", BLUE); adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动ADC DMA采集 */ while (1) { if (g_adc_dma_sta == 1) { /* 计算DMA 采集到的ADC数据的平均值 */ sum = 0; for (i = 0; i < ADC_DMA_BUF_SIZE; i++) /* 累加 */ { sum += g_adc_dma_buf[i]; printf("%d\r\n", sum); } adcx = sum / ADC_DMA_BUF_SIZE; /* 取平均值 */ /* 显示结果 */ lcd_show_xnum(126, 150, adcx, 5, 24, 0, BLUE); /* 显示ADCC采样后的原始值 */ oled_show_num(65, 25, adcx, 5, 16); temp = (float)adcx * (3.3 / 4096); /* 获取计算后的带小数的实际电压值,比如3.1111 */ adcx = temp; /* 赋值整数部分给adcx变量,因为adcx为u16整形 */ lcd_show_xnum(126, 180, adcx, 1, 24, 0, BLUE); /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */ oled_show_num(64, 41, adcx, 1, 16); temp -= adcx; /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */ temp *= 1000; /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */ lcd_show_xnum(150, 180, temp, 3, 24, 0x80, BLUE); /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */ oled_show_num(80, 41, temp, 3, 16); g_adc_dma_sta = 0; /* 清除DMA采集完成状态标志 */ adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动下一次ADC DMA采集 */ } LED0_TOGGLE(); delay_ms(100); oled_refresh_gram(); } }
总结:
adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
初始化ADC和DMA,使得ADC可以通过DMA方式进行数据采集,采集的数据存储在
g_adc_dma_buf
缓冲区中。adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动ADC DMA采集 */
启动ADC DMA采集,使得ADC开始连续采集数据并通过DMA传输到缓冲区
g_adc_dma_buf
。代码通过初始化和配置STM32的各种外设,实现了ADC数据的DMA采集,并在LCD和OLED上显示采集到的ADC数据和转换后的电压值。同时,通过中断和DMA机制实现了高效的数据采集和处理。
什么时候采集,什么时候传输
ADC采集的控制:
- ADC的连续转换模式被启用(
g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;
)。这意味着ADC会连续地进行转换。 - 但是,ADC的转换触发和数据读取是通过DMA控制的。当你调用
adc_dma_enable
函数时,ADC和DMA都被启动,并开始进行数据采集和传输。
DMA传输的控制:
- 当你停止DMA传输(例如,通过禁用DMA通道),ADC可能会继续进行转换,但是这些转换结果不会通过DMA传输到内存。这是因为DMA传输是负责将ADC转换结果传输到指定的内存缓冲区(
g_adc_dma_buf
)。 - 如果DMA被禁用,但ADC继续进行转换,结果仍会存储在ADC的数据寄存器(DR)中,但不会被传输到内存。
- ADC的连续转换模式被启用(
5. 多通道ADC采集(DMA读取)实验
-
功能:使用ADC1采集(DMA读取)通道1/2/3/4/5/6的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0/PA1/PA2/PA3/PA4/PA5到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
-
多通道ADC初始化函数
void adc_nch_dma_init(uint32_t mar) { ADC_ChannelConfTypeDef adc_ch_conf = {0}; __HAL_RCC_DMA1_CLK_ENABLE(); //使能DMA1时钟 g_dma_adc_handle.Instance = DMA1_Channel1; //初始化DMA1的通道1 g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; //数据传输方向设置为外设到内存 g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; //外设地址不递增 g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; //内存地址递增 g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; //数据对齐使用半字 g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; //数据对齐使用半字 g_dma_adc_handle.Init.Mode = DMA_NORMAL; //传输模式配置为正常模式 g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; //优先级设置为中等 HAL_DMA_Init(&g_dma_adc_handle); //2.将DMA和ADC句柄连接起来 __HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_adc_handle); g_adc_nch_dma_handle.Instance = ADC1; //选择ADC1 g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; //数据对齐方式为右对齐 g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE; //扫描模式,多个通道 g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE; //打开连续转换模式 g_adc_nch_dma_handle.Init.NbrOfConversion = 6; //通道数选择1个 g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE; //禁止规则通道组间断模式 g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0; //配置间断模式的规则通道个数 g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发方式 HAL_ADC_Init(&g_adc_nch_dma_handle); //进行初始化 HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle); //校准ADC adc_ch_conf.Channel = ADC_CHANNEL_0; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_1; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_2; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_2; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_3; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_3; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_4; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_4; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_5; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_5; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_6; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0); HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0); }
多通道ADC转换与单通道ADC转换的区别:
DMA的相关配置不需要改动;
ADC的配置要使能扫描模式,通道数设置为6个
g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE; //打开连续转换模式 g_adc_nch_dma_handle.Init.NbrOfConversion = 6; //通道数选择6个
设置6个通道序列:
adc_ch_conf.Channel = ADC_CHANNEL_0; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_1; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_2; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_2; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_3; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_3; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_4; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_4; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_5; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置 adc_ch_conf.Channel = ADC_CHANNEL_5; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_6; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); //通道配置
-
ADC MSP初始化函数
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { GPIO_InitTypeDef gpio_init_struct; RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; //使能时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_ADC1_CLK_ENABLE(); //配置工作模式 gpio_init_struct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; gpio_init_struct.Mode = GPIO_MODE_ANALOG; //模拟输入 HAL_GPIO_Init(GPIOA, &gpio_init_struct); adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; //ADC外设时钟 adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6; //分频因子6,时钟为72M/6=12MHz HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); //设置ADC时钟 } }
-
使能一次ADC DMA传输函数
void adc_dma_enable(uint16_t cndtr) { ADC1->CR2 &= ~(1 << 0); //CONT位置1,关闭连续转换模式 DMA1_Channel1->CCR &= ~(1 << 0); //禁用DMA通道1 while(DMA1_Channel1->CCR & (1 << 0)); //等待DMA通道1禁用完成 DMA1_Channel1->CNDTR = cndtr; //设置DMA传输数据量 DMA1_Channel1->CCR |= 1 << 0; //启用DMA通道1 ADC1->CR2 |= 1 << 0; //开启ADC1 ADC1->CR2 |= 1 << 22; //启用ADC1的DMA请求 }
-
ADC DMA采集中断服务函数
void DMA1_Channel1_IRQHandler(void) { if(DMA1->ISR & (1<<1)) { g_adc_dma_sta = 1; //清除中断标志位 DMA1->IFCR |= (1 << 1); } }
-
主函数
int main(void) { uint16_t i,j; uint16_t adcx; uint32_t sum; float temp; HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ delay_init(72); /* 延时初始化 */ usart_init(115200); /* 串口初始化为115200 */ led_init(); /* 初始化LED */ lcd_init(); /* 初始化LCD */ adc_nch_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */ lcd_show_string(30, 50, 200, 16, 16, "STM32", RED); lcd_show_string(30, 70, 200, 16, 16, "ADC 6CH DMA TEST", RED); lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED); lcd_show_string(30, 110, 200, 12, 12, "ADC1_CH0_VAL:", BLUE); lcd_show_string(30, 122, 200, 12, 12, "ADC1_CH0_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */ lcd_show_string(30, 140, 200, 12, 12, "ADC1_CH1_VAL:", BLUE); lcd_show_string(30, 152, 200, 12, 12, "ADC1_CH1_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */ lcd_show_string(30, 170, 200, 12, 12, "ADC1_CH2_VAL:", BLUE); lcd_show_string(30, 182, 200, 12, 12, "ADC1_CH2_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */ lcd_show_string(30, 200, 200, 12, 12, "ADC1_CH3_VAL:", BLUE); lcd_show_string(30, 212, 200, 12, 12, "ADC1_CH3_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */ lcd_show_string(30, 230, 200, 12, 12, "ADC1_CH4_VAL:", BLUE); lcd_show_string(30, 242, 200, 12, 12, "ADC1_CH4_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */ lcd_show_string(30, 260, 200, 12, 12, "ADC1_CH5_VAL:", BLUE); lcd_show_string(30, 272, 200, 12, 12, "ADC1_CH5_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */ adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动ADC DMA采集 */ while (1) { if (g_adc_dma_sta == 1) { /* 循环显示通道0~通道5的结果 */ for(j = 0; j < 6; j++) /* 遍历6个通道 */ { sum = 0; /* 清零 */ for (i = 0; i < ADC_DMA_BUF_SIZE / 6; i++) /* 每个通道采集了50次数据,进行50次累加 */ { sum += g_adc_dma_buf[(6 * i) + j]; /* 相同通道的转换数据累加 */ } adcx = sum / (ADC_DMA_BUF_SIZE / 6); /* 取平均值 */ /* 显示结果 */ lcd_show_xnum(108, 110 + (j * 30), adcx, 4, 12, 0, BLUE); /* 显示ADC采样后的原始值 */ temp = (float)adcx * (3.3 / 4096); /* 获取计算后的带小数的实际电压值,比如3.1111 */ adcx = temp; /* 赋值整数部分给adcx变量,因为adcx为u16整形 */ lcd_show_xnum(108, 122 + (j * 30), adcx, 1, 12, 0, BLUE); /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */ temp -= adcx; /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */ temp *= 1000; /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */ lcd_show_xnum(120, 122 + (j * 30), temp, 3, 12, 0X80, BLUE);/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */ } g_adc_dma_sta = 0; /* 清除DMA采集完成状态标志 */ adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动下一次ADC DMA采集 */ } LED0_TOGGLE(); delay_ms(100); } }
6. 单通道ADC过采样实验
-
功能:使用ADC1通道1(PA1),通过软件方式实现16位分辨率采集外部电压,并在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA1到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
-
单通道ADC初始化函数
void adc_dma_init(uint32_t mar) { ADC_ChannelConfTypeDef adc_ch_conf = {0}; __HAL_RCC_DMA1_CLK_ENABLE(); //使能DMA1时钟 g_dma_adc_handle.Instance = DMA1_Channel1; //初始化DMA1的通道1 g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; //数据传输方向设置为外设到内存 g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; //外设地址不递增 g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; //内存地址递增 g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; //数据对齐使用半字 g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; //数据对齐使用半字 g_dma_adc_handle.Init.Mode = DMA_NORMAL; //传输模式配置为正常模式 g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; //优先级设置为中等 HAL_DMA_Init(&g_dma_adc_handle); //2.将DMA和ADC句柄连接起来 __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle); g_adc_dma_handle.Instance = ADC1; //选择ADC1 g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; //数据对齐方式为右对齐 g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; //非扫描模式,仅使用一个通道 g_adc_dma_handle.Init.ContinuousConvMode = ENABLE; //打开连续转换模式 g_adc_dma_handle.Init.NbrOfConversion = 1; //通道数选择1个 g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE; //禁止规则通道组间断模式 g_adc_dma_handle.Init.NbrOfDiscConversion = 0; //配置间断模式的规则通道个数 g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发方式 HAL_ADC_Init(&g_adc_dma_handle); //进行初始化 HAL_ADCEx_Calibration_Start(&g_adc_dma_handle); //校准ADC adc_ch_conf.Channel = ADC_CHANNEL_1; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_1CYCLE_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf); //通道配置 HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0); HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0); }
与前面的实验相比,此函数只需要修改采样时间即可
adc_ch_conf.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
-
主函数循环代码
while (1) { if (g_adc_dma_sta == 1) { /* 计算DMA 采集到的ADC数据的平均值 */ sum = 0; for (i = 0; i < ADC_DMA_BUF_SIZE; i++) /* 累加 */ { sum += g_adc_dma_buf[i]; } adcx = sum / (ADC_DMA_BUF_SIZE / ADC_OVERSAMPLE_TIMES); /* 取平均值 */ adcx >>= 4; /* 除以2^4倍, 得到12+4位 ADC精度值, 注意: 提高 N bit精度, 需要 >> N */ /* 显示结果 */ lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE); /* 显示ADC采样后的原始值 */ temp = (float)adcx * (3.3 / 65536); /* 获取计算后的带小数的实际电压值,比如3.1111 */ adcx = temp; /* 赋值整数部分给adcx变量,因为adcx为u16整形 */ lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE); /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */ temp -= adcx; /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */ temp *= 1000; /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */ lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE); /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */ g_adc_dma_sta = 0; /* 清除DMA采集完成状态标志 */ adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动下一次ADC DMA采集 */ } LED0_TOGGLE(); delay_ms(100); }
在主函数的循环代码中, 通过增加采样次数和进行移位操作,提高了ADC采样精度。
提高精度的原理
-
增加采样次数:
通过增加采样次数来进行过采样。过采样可以降低噪声,提高信噪比,从而提高分辨率。将多个采样值相加并取平均值,可以增加有效位数。
-
移位操作:
通过移位操作,将结果右移,以提高分辨率。增加N位精度,需要对累加后的结果右移N位。
-
7. 内部温度传感器实验
-
功能: 通过ADC的通道16读取STM32F103内部温度传感器的电压值,并将其转换为温度值,显示在TFTLCD屏上。LED0闪烁用于提示程序正在运行。
-
温度计算方法:
-
ADC内部温度传感器初始化函数
void adc_temperature_init(void) { ADC_ChannelConfTypeDef adc_ch_conf; g_adc_handle.Instance = ADC1; //选择ADC1 g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; //数据对齐方式为右对齐 g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; //非扫描模式,仅使用一个通道 g_adc_handle.Init.ContinuousConvMode = DISABLE; //关闭连续转换模式 g_adc_handle.Init.NbrOfConversion = 1; //通道数选择1个 g_adc_handle.Init.DiscontinuousConvMode = DISABLE; //禁止规则通道组间断模式 g_adc_handle.Init.NbrOfDiscConversion = 0; //配置间断模式的规则通道个数 g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发方式 HAL_ADC_Init(&g_adc_handle); //进行初始化 HAL_ADCEx_Calibration_Start(&g_adc_handle); //校准ADC adc_ch_conf.Channel = ADC_CHANNEL_16; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_handle, &adc_ch_conf); //通道配置 }
-
ADC MSP初始化函数
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; //使能时钟 __HAL_RCC_ADC1_CLK_ENABLE(); adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; //ADC外设时钟 adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6; //分频因子6,时钟为72M/6=12MHz HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); //设置ADC时钟 } }
-
使能一次ADC DMA传输函数
uint32_t adc_get_result() { //4.启动A/D转换 HAL_ADC_Start(&g_adc_handle); //开启ADC //5.等待规则通道转换完成 HAL_ADC_PollForConversion(&g_adc_handle, 10); //轮询转换 //6.获取规则通道A/D转换结果 return (uint16_t)HAL_ADC_GetValue(&g_adc_handle); //返回最近一次规则组的转换结果 }
-
获取内部温度传感器的结果函数
short adc_get_temperature(void) { uint32_t adcx; adcx = adc_get_result(); short result; double temperature; temperature = adcx * (3.3 / 4096); temperature = (1.43 - temperature) / 0.0043 + 25; result = temperature * 100; return result; }
-
主函数
int main(void) { short temp; HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ delay_init(72); /* 延时初始化 */ usart_init(115200); /* 串口初始化为115200 */ led_init(); /* 初始化LED */ lcd_init(); /* 初始化LCD */ adc_temperature_init(); /* 初始化ADC内部温度传感器采集 */ lcd_show_string(30, 50, 200, 16, 16, "STM32", RED); lcd_show_string(30, 70, 200, 16, 16, "Temperature TEST", RED); lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED); lcd_show_string(30, 120, 200, 16, 16, "TEMPERATE: 00.00C", BLUE); while (1) { temp = adc_get_temperature(); /* 得到温度值 */ if (temp < 0) { temp = -temp; lcd_show_string(30 + 10 * 8, 120, 16, 16, 16, "-", BLUE); /* 显示负号 */ } else { lcd_show_string(30 + 10 * 8, 120, 16, 16, 16, " ", BLUE); /* 无符号 */ } lcd_show_xnum(30 + 11 * 8, 120, temp / 100, 2, 16, 0, BLUE); /* 显示整数部分 */ lcd_show_xnum(30 + 14 * 8, 120, temp % 100, 2, 16, 0X80, BLUE); /* 显示小数部分 */ LED0_TOGGLE(); /* LED0闪烁,提示程序运行 */ delay_ms(250); } }
8. 光敏传感器实验
-
光敏传感器简介
光敏传感器是最常见的传感器之一,它的种类繁多,主要有:光电管、光电倍增管、光敏电阻、光敏三极管、太阳能电池、红外线传感器、紫外线传感器、光纤式光电传感器、色彩传感器、CCD和CMOS图像传感器等。光传感器是目前产量最多、应用最广的传感器之一,它在自动控制和非电量电测技术中占有非常重要的地位。
光敏传感器是利用光敏元件将光信号转换为电信号的传感器,它的敏感波长在可见光波长附近,包括红外线波长和紫外线波长。光传感器不只局限于对光的探测,它还可以作为探测元件组成其他传感器,对许多非电量进行检测,只要将这些非电量转换为光信号的变化即可。
STM32F103战舰开发板板载了一个光敏二极管(光敏电阻),作为光敏传感器,它对光的变化非常敏感。光敏二极管也叫光电二极管。光敏二极管与半导体二极管在结构上是类似的,其管芯是一个具有光敏特征的PN结,具有单向导电性,因此工作时需加上反向电压。无光照时,有很小的饱和反向漏电流,即暗电流,此时光敏二极管截止。当受到光照时,饱和反向漏电流大大增加,形成光电流,它随入射光强度的变化而变化。当光线照射PN结时,可以使PN结中产生电子一空穴对,使少数载流子的密度增加。这些载流子在反向电压下漂移(PN结中多子扩散少子漂移),使反向电流增加。因此可以利用光照强弱来改变电路中的电流。
-
功能:
通过ADC3的通道6(
PF8
)读取光敏传感器(LS1)的电压值,并转换为0~100的光线强度值,显示在LCD模块上面。光线越亮,值越大;光线越暗,值越小。 -
硬件原理图:
-
ADC单通道初始化函数
void adc3_init(void) { ADC_ChannelConfTypeDef adc_ch_conf; g_adc_handle.Instance = ADC3; //选择ADC1 g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; //数据对齐方式为右对齐 g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; //非扫描模式,仅使用一个通道 g_adc_handle.Init.ContinuousConvMode = DISABLE; //关闭连续转换模式 g_adc_handle.Init.NbrOfConversion = 1; //通道数选择1个 g_adc_handle.Init.DiscontinuousConvMode = DISABLE; //禁止规则通道组间断模式 g_adc_handle.Init.NbrOfDiscConversion = 0; //配置间断模式的规则通道个数 g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发方式 HAL_ADC_Init(&g_adc_handle); //进行初始化 HAL_ADCEx_Calibration_Start(&g_adc_handle); //校准ADC adc_ch_conf.Channel = ADC_CHANNEL_6; //输入通道 adc_ch_conf.Rank = ADC_REGULAR_RANK_1; //转换序列 adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间 HAL_ADC_ConfigChannel(&g_adc_handle, &adc_ch_conf); //通道配置 }
-
ADC MSP初始化函数
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC3) { GPIO_InitTypeDef gpio_init_struct; RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; //使能时钟 __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_ADC3_CLK_ENABLE(); //配置工作模式 gpio_init_struct.Pin = GPIO_PIN_8; gpio_init_struct.Mode = GPIO_MODE_ANALOG; //模拟输入 HAL_GPIO_Init(GPIOF, &gpio_init_struct); adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; //ADC外设时钟 adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6; //分频因子6,时钟为72M/6=12MHz HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); //设置ADC时钟 } }
-
获得ADC转换后的结果函数
uint32_t adc_get_result(void) { //4.启动A/D转换 HAL_ADC_Start(&g_adc_handle); //开启ADC //5.等待规则通道转换完成 HAL_ADC_PollForConversion(&g_adc_handle, 10); //轮询转换 //6.获取规则通道A/D转换结果 return (uint16_t)HAL_ADC_GetValue(&g_adc_handle); //返回最近一次规则组的转换结果 }
-
读取光敏传感器值
uint8_t lsens_get_val(void) { uint32_t temp_val; temp_val = adc_get_result(); temp_val = temp_val / 40; if(temp_val > 100) temp_val = 100; return (uint8_t)(100-temp_val); }
-
主函数
int main(void) { uint8_t i; short adcx[10] = {0}; int temp = 0; HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ delay_init(72); /* 延时初始化 */ usart_init(115200); /* 串口初始化为115200 */ led_init(); /* 初始化LED */ lcd_init(); /* 初始化LCD */ adc3_init(); /* 初始化ADC3 */ lcd_show_string(30, 50, 200, 16, 16, "STM32", RED); lcd_show_string(30, 70, 200, 16, 16, "LSENS TEST", RED); lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED); lcd_show_string(30, 110, 200, 16, 16, "LSENS_VAL:", BLUE); while (1) { for(i = 0;i < 10; i++) { adcx[i] = lsens_get_val(); delay_ms(5); temp += adcx[i]; } temp = temp / 10; if(temp > 100) temp = 100; lcd_show_xnum(30 + 10 * 8, 110, temp, 3, 16, 0, BLUE); /* 显示ADC的值 */ temp = 0; LED0_TOGGLE(); /* LED0闪烁,提示程序运行 */ delay_ms(200); } }
-
实验结果
声明:资料来源(战舰STM32F103ZET6开发板资源包)
- Cortex-M3权威指南(中文).pdf
- STM32F10xxx参考手册_V10(中文版).pdf
- STM32F103 战舰开发指南V1.3.pdf
- STM32F103ZET6(中文版).pdf
- 战舰V4 硬件参考手册_V1.0.pdf