第5章 CHAPTER 5 ADC开发 ADC(Analog To Digital Converter,模数转换器)指将连续变化的模拟信号转换为离散的数字信号的器件。STM32F4系列一般有3个ADC,每个ADC有12位、10位、8位和6位精度可选,每个ADC有16个外部通道、19个复用通道、两个用于内部源和VBAT通道的信号。这些通道的AD转换可在单次、连续、扫描或不连续采样模式下进行。ADC的结果存储在一个左对齐或右对齐的16位数据寄存器中。ADC具有独立模式、双重模式和三重模式,对于不同的AD转换要求几乎都有合适的模式可选。 本章从采集光照强度、单ADC扫描、ADC的DMA模式、双ADC交叉、ADC定时器触发,逐一展开,介绍相关原理并通过实例帮助读者掌握ADC开发能力。 视频18 5.1ADC: 采集光照强度 学习目标 掌握ARM CortexM系列芯片外设ADC的工作原理,通过配置STM32F407的ADC来完成光敏传感器电压采集。 5.1.1开发原理 1. ADC介绍 STM32F407ZGT6有3个ADC,每个ADC有12位、10位、8位和 6位精度可选,每个ADC有16个外部通道。另外还有用于内部ADC源和VBAT的通道。ADC具有独立模式、双重模式和三重模式。ADC功能非常强大。 ADC功能框图如图51所示(详情请参考STM32F4参考手册)。 图51ADC功能框图 1) 编号①电压输入范围 ADC输入范围为VREF-≤VIN≤VREF+。由VREF-、VREF+、VDDA、VSSA这4个外部引脚决定。我们在设计原理图的时候一般把VSSA和VREF-接地,把VREF+和VDDA接3V3,得到ADC的输入电压范围为0~3.3V。 如果想让输入的电压范围变宽,可以测试负电压或者更高的正电压,那么可以在外部加一个电压调理电路,把需要转换的电压抬升或者降压到0~3.3V,这样ADC就可以测量了。 2) 编号②输入通道 在确定好ADC输入电压之后,电压怎么输入到ADC?这里引入通道的概念。STM32的ADC有多达19个通道,其中外部的16个通道就是图51中的ADCx_IN0、ADCx_IN1、……、ADCx_IN5。这16个通道对应着不同的I/O口,具体是哪一个I/O口可以从手册查询到。其中ADC1/ADC2/ADC3还有内部通道: ADC1的通道ADC1_IN16连接到内部的VSS,通道ADC1_IN17连接到了内部参考电压VREFINT,通道ADC1_IN18连接到了芯片内部的温度传感器或者备用电源VBAT。ADC2和ADC3的通道16、17、18全部连接到了内部的VSS,如图52所示。 图52ADC输入通道 外部的16个通道在转换的时候又分为规则通道和注入通道,其中规则通道最多有16路,注入通道最多有4路。这两个通道的区别如下: (1) 规则通道。 顾名思义,规则通道就是遵守规则的通道,我们平时一般使用的就是这个通道。 (2) 注入通道。 注入,可以理解为插入、插队的意思,这是一种不安分的通道。它是一种在规则通道转换的时候可强行插入转换的一种通道。如果在规则通道转换过程中,有注入通道插队,那么要先转换完注入通道,等注入通道转换完成后,再回到规则通道的转换流程。这一点与中断程序处理很像。所以,注入通道只有在规则通道存在时才会出现。 3) 编号③转换顺序 (1) 规则序列。 规则序列寄存器有3个,分别为SQR3、SQR2、SQR1。SQR3控制着规则序列中的第1个到第6个转换,对应的位为SQ1[4:0]~SQ6[4:0],第一次转换的是位SQ1[4:0],如果通道16想第一次转换,那么在SQ1[4:0]写16即可。SQR2控制着规则序列中的第7到第12个转换,对应的位为SQ7[4:0]~SQ12[4:0],如果通道1向第8个转换,则SQ8[4:0]写1即可。SQR1控制着规则序列中的第13到第16个转换,对应位为SQ13[4:0]~SQ16[4:0]; 如果通道6向第10个转换,则SQ10[4:0]写6即可。具体使用多少个通道,由SQR1的位L[3:0]决定,最多16个通道,如图53所示。 图53规则序列寄存器 (2) 注入序列。 注入序列寄存器JSQR只有一个,最多支持4个通道,具体多少个由JSQR的JL[2:0]决定。如果JL的值小于4,则JSQR与SQR决定转换顺序的设置不一样,第一次转换的不是JSQR1[4:0],而是JSQRx[4:0],x=(4-JL),与SQR刚好相反。如果JL=00(1个转换),那么转换的顺序是从JSQR4[4:0]开始,而不是从JSQR1[4:0]开始,这个要注意,编程的时候不要搞错。当JL等于4时,与SQR一样,如图54所示。 图54注入序列寄存器 4) 触发源 通道选好了,转换的顺序也设置好了,接下来就该开始转换了。ADC转换可以由ADC控制寄存器ADC_CR2的ADON这个位来控制,写1的时候开始转换,写0的时候停止转换,这是最简单、最好理解的开启ADC转换的控制方式。 除了上面的控制方法,ADC还支持外部事件触发转换,这个触发包括内部定时器触发和外部I/O触发。触发源有很多,具体选择哪一种触发源,由ADC控制寄存器ADC_CR2的EXTSEL[2:0]和JEXTSEL[2:0]位来控制。EXTSEL[2:0]用于选择规则通道的触发源,JEXTSEL[2:0]用于选择注入通道的触发源。选定好触发源之后,触发源是否需要被触发,则由ADC控制寄存器ADC_CR2的EXTTRIG和JEXTTRIG这两位来决定。 如果使能了外部触发事件,则可以通过设置ADC控制寄存器ADC_CR2的EXTEN[1:0]和JEXTEN[1:0]来控制触发极性,可以有4种状态,分别是禁止触发检测、上升沿检测、下降沿检测以及上升沿和下降沿均检测。 5) 转换时间 (1) ADC时钟。 ADC输入时钟ADC_CLK由PCLK2经过分频产生,最大值为36MHz,典型值为30MHz,分频因子由ADC通用控制寄存器ADC_CCR的ADCPRE[1:0]设置,可设置的分频系数有2、4、6和8。注意,这里没有1分频。 (2) 采样时间。 ADC需要若干个ADC_CLK周期完成对输入的电压进行采样,采样的周期数可通过ADC采样时间寄存器ADC_SMPR1和ADC_SMPR2中的SMP[2:0]位设置,ADC_SMPR2控制的是通道0~9,ADC_SMPR1控制的是通道10~17。每个通道可以分别用不同的时间采样。其中采样周期最小是3个,即如果要以最快速度采样,那么应该设置采样周期为3个周期,这里说的周期就是1/ADC_CLK。 ADC的总转换时间跟ADC的输入时钟和采样时间有关,公式为: Tconv=采样时间+12个周期 当ADC_CLK=30MHz,即PCLK2为60MHz,ADC时钟为2分频,采样周期设置为3个周期,那么总的转换时为Tconv=3+12=15个周期=0.5μs。 一般设置PCLK2=84MHz,经过ADC预分频器能分频到最大的时钟只能是21MHz,采样周期设置为3个周期,算出最短的转换时间为0.7142μs,这个才是最常用的。 6) 数据寄存器 一切准备就绪后,ADC转换后的数据根据转换组的不同存放位置不同,规则组的数据放在ADC_DR寄存器,注入组的数据放在JDRx。如果是使用双重或者三重模式,那么规则组的数据是存放在通用规则寄存器ADC_CDR内的。 (1) 规则数据寄存器ADC_DR。 ADC规则组数据寄存器ADC_DR只有一个,是一个32位的寄存器,只有低16位有效并且只是用于以独立模式存放转换完成的数据。因为ADC的最大精度是12位,ADC_DR是16位有效,所以允许ADC存放数据时候选择左对齐或者右对齐,具体是以哪一种方式存放,由ADC_CR2的11位ALIGN设置。假如设置ADC精度为12位,如果设置数据为左对齐,那么AD转换完成的数据存放在ADC_DR寄存器的位[4:15]内; 如果为右对齐,则存放在ADC_DR寄存器的位[0:11]内。 规则通道可以有16个,可规则数据寄存器只有一个,如果使用多通道转换,那么转换的数据就全部都挤在了DR里面,前一个时间点转换的通道数据,就会被下一个时间点的另外一个通道转换的数据覆盖掉,所以通道转换完成后就应该把数据取走,或者开启DMA模式,把数据传输到内存里面,否则就会造成数据的覆盖。最常用的做法就是开启DMA传输。 如果没有使用DMA传输,那么一般需要使用ADC状态寄存器ADC_SR获取当前ADC转换的进度状态,进而进行程序控制。 (2) 注入数据寄存器ADC_JDRx。 ADC注入组最多有4个通道,刚好注入数据寄存器也有4个,每个通道对应着自己的寄存器,不会像规则寄存器那样产生数据覆盖的问题。ADC_JDRx是32位的,低16位有效,高16位保留,数据同样分为左对齐和右对齐,具体以哪一种方式存放,由ADC_CR2的11位ALIGN设置。 (3) 通用规则数据寄存器ADC_CDR。 规则数据寄存器ADC_DR是仅适用于独立模式的,而通用规则数据寄存器ADC_CDR是适用于双重模式和三重模式的。独立模式就是仅使用3个ADC的其中一个,双重模式就是同时使用ADC1和ADC2,而三重模式就是同时使用3个ADC。在双重或者三重模式下一般需要配合DMA数据传输使用。 7) 中断 (1) 转换结束中断。 数据转换结束后,可以产生中断,中断分为4种: 规则通道转换结束中断、注入转换通道转换结束中断、模拟看门狗中断和溢出中断。其中转换结束中断很好理解,跟我们平时接触的中断一样,有相应的中断标志位和中断使能位,还可以根据中断类型编写与之配套的中断服务程序。 (2) 模拟看门狗中断。 当被ADC转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断,前提是开启了模拟看门狗中断,其中低阈值和高阈值由ADC_LTR和ADC_HTR设置。例如,设置高阈值是2.5V,那么模拟电压超过2.5V的时候,就会产生模拟看门狗中断; 低阈值时也一样。 (3) 溢出中断。 如果发生DMA传输数据丢失,则会对ADC状态寄存器ADC_SR的OVR位置位; 如果同时使能了溢出中断,那么在转换结束后会产生一个溢出中断。 (4) DMA请求。 规则和注入通道转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据直接存储在内存里面。对于3种模式的AD转换使用DMA传输非常有必要,程序编程简化了很多。 8) 电压转换 模拟电压经过模数转换后,得到的是一个相对精度的数字值,如果通过串口以十六进制数据输出,可读性比较差,那么有时候就需要把数字电压转换成模拟电压,这样也可以与实际的模拟电压(用万用表测)对比,看看转换是否准确。 我们一般在设计原理图的时候会把ADC的输入电压范围设定为0~3.3V,如果设置ADC为12位的,那么12位满量程对应的就是3.3V,12位满量程对应的数字值是212。数值0对应的就是0V。如果转换后的数值为X,X对应的模拟电压为Y,那么会有如下等式成立: 212/3.3=X/Y,=>Y=(3.3×X)/212 2. 光敏传感器简介 光敏传感器是利用光敏元件将光信号转换为电信号的传感器,它的敏感波长在可见光波长附近,包括红外线波长和紫外线波长。光传感器不只局限于对光的探测,它还可以作为探测元件组成其他传感器,对许多非电量进行检测,只要将这些非电量转换为光信号的变化即可。 STM32F4开发板板载了一个光敏二极管(光敏电阻),作为光敏传感器,它对光的变化非常敏感。光敏二极管也称为光电二极管。光敏二极管与半导体二极管在结构上是类似的,其管芯是一个具有光敏特征的PN结,具有单向导电性,因此工作时需加上反向电压。无光照时,有很小的饱和反向漏电流,即暗电流,此时光敏二极管截止。当受到光照时,饱和反向漏电流大大增加,形成光电流,它随入射光强度的变化而变化。当光线照射PN结时,可以使PN结中产生电子空穴对,使少数载流子的密度增加。这些载流子在反向电压下漂移,使反向电流增加。因此可以利用光照强弱来改变电路中的电流。 利用这个电流变化,我们串接一个电阻,就可以转换成电压的变化,从而通过ADC读取电压值,判断外部光线的强弱。 5.1.2开发步骤 (1) 查找开发板电路原理图可知,光敏二极管连接在PF7引脚上,如图55所示。 图55光敏二极管连接图 (2) 查找STM32F4数据手册可知,PF7引脚需使用ADC3的通道5,如图56所示。 图56ADC引脚映射 (3) 定义ADC3初始化结构体句柄ADC3_Handler,并创建ADC3_Init()函数,初始化ADC3。首先调用函数HAL_ADC_Init()初始化ADC3,此处配置ADC为4分频,ADC时钟为21MHz,并配置ADC为软件触发,且为12位数据模式。然后调用HAL_ADC_ConfigChannel()函数,初始化ADC3的通道5。 ADC_HandleTypeDef ADC3_Handler; //ADC初始化函数 void ADC3_Init(void) { ADC3_Handler.Instance = ADC3; ADC3_Handler.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;//4分频,ADCCLK = PCLK2/4 =84/4=21MHz ADC3_Handler.Init.Resolution = ADC_RESOLUTION_12B;//12位模式 ADC3_Handler.Init.DataAlign = ADC_DATAALIGN_RIGHT;//右对齐 ADC3_Handler.Init.ScanConvMode = DISABLE; //扫描模式 ADC3_Handler.Init.EOCSelection = ADC_EOC_SINGLE_CONV; //开启EOC转换一次中断 ADC3_Handler.Init.ContinuousConvMode = DISABLE; //开启连续转换 ADC3_Handler.Init.NbrOfConversion = 1; //1个转换在规则序列 ADC3_Handler.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发 ADC3_Handler.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; //关闭外部触发器 ADC3_Handler.Init.DMAContinuousRequests = DISABLE; //关闭DMA请求 HAL_ADC_Init(&ADC3_Handler); //初始化 ADC_ChannelConfTypeDef ADC_ChanneConf; //电压采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_5; //通道5 ADC_ChanneConf.Rank = 1; //第一个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC3_Handler, &ADC_ChanneConf); } (4) 重定义HAL_ADC_MspInit()函数,用来初始化PF7引脚,此处设置引脚为模拟模式,然后使能ADC时钟。 //定义ADC底层驱动 void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_ADC3_CLK_ENABLE(); //使能ADC3时钟 __HAL_RCC_GPIOF_CLK_ENABLE(); //开启GPIOF时钟 GPIO_Initure.Pin = GPIO_PIN_7; //PF7 GPIO_Initure.Mode = GPIO_MODE_ANALOG; //模拟 GPIO_Initure.Pull = GPIO_NOPULL; //浮空 HAL_GPIO_Init(GPIOF, &GPIO_Initure); } (5) 创建Light_Value()函数,用来获取传感器测量电压值。函数首先调用HAL_ADC_Start()开启模数转换,然后调用HAL_ADC_GetValue()函数,读取ADC测量值。 //获取ADC电压值 uint16_t Light_Value(void) { HAL_ADC_Start(&ADC3_Handler); //开始ADC3 return (uint16_t)HAL_ADC_GetValue(&ADC3_Handler); //获取ADC电压值 } (6) 主函数main()程序如下: 第一步,初始化系统时钟和串口。 第二步,初始化ADC。 第三步,在while()循环中调用Light_Value()函数获取光照电压,并使用printf()将电压通过串口每隔100ms打印出来。 int main() { CLOCK_Init(); //时钟初始化 UART_Init(); //串口初始化 ADC3_Init(); //ADC3初始化 while(1) { printf("Light: %d\r\n", Light_Value()); //获取光照电压并通过串口打印 HAL_Delay(100); //延时100ms } } 5.1.3运行结果 将程序下载到开发板中,打开串口,可以看到串口调试助手打印出的电压值,光照越强,该值越小; 光照越弱,该值越大,如图57所示。 图57运行结果 练习 (1) 什么是ADC? (2) ADC具有哪些模式? (3) 配置其他ADC实现ADC数值转换。 视频19 5.2ADC: 单ADC扫描转换 学习目标 掌握ARM CortexM系列芯片外设ADC多通道扫描转换的工作原理,通过配置STM32F407的ADC多通道来分别测量ADC内部参考电压、引脚电压以及内部温度传感器电压并计算温度值。 5.2.1开发原理 STM32F407的ADC内部框图原理请参考5.1节,此处不再赘述。 对于STM32F407,温度传感器在内部和ADC1_IN16连接,内部测量电压引脚与ADC1_IN17连接,以此来分别转换传感器和内部电压为数字值。在不使用的情况下,温度传感器将处于断电模式。 温度传感器和参考电压通道框图如图58所示。 图58温度传感器和参考电压通道框图 利用以下公式得出温度: 温度(℃)=VSENSE-V25Avg_Slope+25 其中, V25=VSENSE在25℃时的数值。 Avg_Slope= 温度与VSENSE 曲线的平均斜率(单位为mV/℃或μV/℃)。 STM32F4数据手册中的温度传感器参数如表51所示。 表51温度传感器特点 符号参数最小值典型值最大值单位 TL(1)VSENSE与温度的线性关系—±1±2℃ Avg_Slope(1)平均斜率—2.5mV/℃ V25(1)25℃的电压—0.76V tSTART(2)启动时间—610μs TS_temp(2)ADC读取温度时的采样时间10——μs 注: (1) 由特性保证。 (2) 由设计保证。 该温度传感器的输出电压随温度线性变化。这个偏移线性函数取决于每个芯片上工艺变化。内部温度传感器更适合于检测温度的应用变化而不是绝对温度。如果必须有准确的温度读数,那么应该使用外部温度传感器。 内部参考电压校准参数如表52所示。 表52内部参考电压校准参数 符号参数内 存 地 址 VREFIN_CAL原始数据在30℃时获得,VDDA=3.3V0x1FFF 7A2A~0x1FFF 7A2B 地址0x1FFF 7A2A~0x1FFF 7A2B中的数据为器件电压VDDA为3.3V,温度为30℃时的电压校准值。 内部参考电压计算公式如下: VREF=3.3×VREFINT_CALVREFINT_DATA 其中,3.3为VDDA的电压,VREFINT_DATA为ADC测出的电压值。 通道电压计算公式如下: Vchannelx=VREFFULL_SCALE×ADC_DATAx 其中,VREF为ADC参考电压,ADC_DATAx为ADC测出的通道电压值,FULL_SCALE为ADC输出的最大数字值。 5.2.2开发步骤 (1) 本节选取ADC1_IN4为电压测量通道。 首先定义ADC初始化句柄,创建ADC初始化函数ADC_Init()。函数中调用HAL_ADC_Init()初始化ADC,此处初始化ADC为扫描模式,并设置规则序列为3个通道,使用软件触发。然后调用HAL_ADC_ConfigChannel()函数分别配置ADC的通道4、通道17、通道16,并依次配置采样顺序为1、2、3。 ADC_HandleTypeDef ADC1_Handler; //ADC句柄 //ADC初始化函数 void ADC_Init(void) { ADC1_Handler.Instance = ADC1; ADC1_Handler.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; //4分频,ADCCLK = PCLK2/4 =84/4=21MHz ADC1_Handler.Init.Resolution = ADC_RESOLUTION_12B; //12位模式 ADC1_Handler.Init.DataAlign = ADC_DATAALIGN_RIGHT; //右对齐 ADC1_Handler.Init.ScanConvMode = ENABLE; //扫描模式 ADC1_Handler.Init.EOCSelection = ADC_EOC_SINGLE_CONV; //开启EOC转换一次中断 ADC1_Handler.Init.ContinuousConvMode = DISABLE; //不开启连续转换 ADC1_Handler.Init.NbrOfConversion = 3; //3个转换在规则序列 ADC1_Handler.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发 ADC1_Handler.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; //使用软件触发 ADC1_Handler.Init.DMAContinuousRequests = DISABLE; //关闭DMA请求 HAL_ADC_Init(&ADC1_Handler); //初始化 ADC_ChannelConfTypeDef ADC_ChanneConf; //电压采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_4; //通道4 ADC_ChanneConf.Rank = 1; //第一个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC1_Handler, &ADC_ChanneConf); //参考电压采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_VREFINT; //参考电压通道 ADC_ChanneConf.Rank = 2; //第二个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC1_Handler, &ADC_ChanneConf); //温度传感器采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_16; //读取温度通道 ADC_ChanneConf.Rank = 3; //第三个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC1_Handler, &ADC_ChanneConf); } (2) 由STM32F4数据手册可知,ADC1_IN4通道对应引脚PA4(通道16和17为内部通道,不需要外部初始化引脚),如图59所示。 图59引脚映射 (3) 重定义HAL_ADC_MspInit()函数,初始化PA4引脚并使能ADC1。 //定义ADC底层驱动 void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_ADC1_CLK_ENABLE(); //使能ADC1时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); //开启GPIOA时钟 GPIO_Initure.Pin = GPIO_PIN_4; //PA4 GPIO_Initure.Mode = GPIO_MODE_ANALOG; //模拟 GPIO_Initure.Pull = GPIO_NOPULL; //浮空 HAL_GPIO_Init(GPIOA, &GPIO_Initure); } (4) 定义结构体ADC_DATA,分别存储通道电压、参考电压和温度的值。 typedef struct { float Vchannel4; float VREF; float temperature; }ADC_DATA; (5) 创建ADC_GetData()函数,用来处理电压数据。函数中首先调用HAL_ADC_Start(),然后调用函数HAL_ADC_GetValue()循环获取3个通道电压,最后根据公式计算出参考电压、通道电压和温度的值。 ADC_DATA adc_data; void ADC_GetData(void) { uint16_t adc_value[3]; //存储ADC测量的电压值 HAL_ADC_Start(&ADC1_Handler); //开始ADC for(uint8_t i=0;i<3;i++) { while(!__HAL_ADC_GET_FLAG(&ADC1_Handler, ADC_FLAG_EOC)); //等待转换完成标志位 adc_value[i] = (uint16_t)HAL_ADC_GetValue(&ADC1_Handler); //获取ADC电压值 } adc_data.VREF = 3.3 * VREFINT_CAL /adc_value[1]; //计算参考电压 adc_data.Vchannel4 = adc_data.VREF /4095 * adc_value[0]; //计算通道电压 adc_data.temperature = (((adc_data.VREF /4095 * adc_value[2]) - 0.76f) / 2.5f) + 25; //计算内部温度 } (6) 主函数main()程序如下: 第一步,初始化系统时钟、串口和ADC。 第二步,调用ADC_GetData()函数获取ADC电压并计算。 第三步,在while循环中分别将参考电压、通道电压和温度的值通过串口打印出来。 extern ADC_DATA adc_data; int main() { CLOCK_Init(); //时钟初始化 UART_Init(); //串口初始化 ADC_Init(); //ADC初始化 while(1) { ADC_GetData(); //ADC获取数据 printf("Vref: %.2f Vch4: %.2f Temp: %.2f\r\n", adc_data.VREF, adc_data.Vchannel4, adc_data.temperature); HAL_Delay(100); } } 5.2.3运行结果 将程序下载到开发板中,将USART1引脚通过USB转串口模块连接到计算机上,打开串口助手,配置好波特率、数据位、校验位、停止位等,单击打开串口。可以看到串口分别打印出参考电压、通道电压和温度传感器的值,可以取一根杜邦线将PA4引脚分别连接到3.3V电源和GND引脚,再观察一下数值,如图510所示。 图510运行结果 练习 (1) 简述单ADC实现过程。 (2) 在计算芯片内部温度的公式中各参数代表什么? (3) 配置其他ADC实现ADC数据转换。 视频20 5.3ADC: ADC的DMA模式 学习目标 掌握ARM CortexM系列芯片外设ADC的DMA工作原理,通过配置STM32F407的ADC和DMA来分别测量ADC内部参考电压、引脚电压以及内部温度传感器电压并计算温度值。 5.3.1开发原理 本节同5.2节类似,同样是测量ADC内部参考电压、引脚电压以及内部温度传感器电压值,区别是本节使用DMA传输的方式将ADC测得的3个通道电压直接传输到接收数据缓冲区中,从而直接计算数据缓冲区的数值即可,无须再调用函数读取ADC数据寄存器。 5.3.2开发步骤 (1) 首先创建ADC初始化结构体句柄ADC1_Handler,然后创建ADC数据接收缓冲区数组adc_value[3]。 (2) 查找DMA通道映射表,可知ADC1需使用DMA2,数据流0、通道0(见STM32F407参考手册),如图511所示。 图511DMA2映射表 (3) 创建ADC_DMA_Init()函数,分别初始化DMA和ADC。 第一步,调用HAL_DMA_Init()函数初始化DMA。 第二步,调用HAL_ADC_Init()函数初始化ADC,此处注意DMA的初始化句柄需要传入到ADC初始化结构体句柄,并使能DMA。 第三步,调用HAL_ADC_ConfigChannel()初始化ADC的3个通道。 第四步,调用HAL_ADC_Start_DMA()函数,传入数据缓冲区adc_value地址,开始DMA、ADC传输。 ADC_HandleTypeDef ADC1_Handler; //ADC句柄 uint16_t adc_value[3]; //存储ADC测量的电压值 //ADC初始化函数 void ADC_DMA_Init(void) { DMA_HandleTypeDef hdma_adc; __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc.Instance = DMA2_Stream0; //数据流 hdma_adc.Init.Channel = DMA_CHANNEL_0; //通道0 hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; //外设到内存 hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; //外设地址不自增 hdma_adc.Init.MemInc = DMA_MINC_ENABLE; //内存地址自增 hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; //一次传输半字 hdma_adc.Init.MemDataAlignment = DMA_PDATAALIGN_HALFWORD; //一次传输半字 hdma_adc.Init.Mode = DMA_CIRCULAR; //循环转换模式 hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; //优先级高 hdma_adc.Init.MemBurst = DMA_MBURST_SINGLE; //突发 hdma_adc.Init.PeriphBurst = DMA_PBURST_SINGLE; HAL_DMA_Init(&hdma_adc); ADC1_Handler.Instance = ADC1; ADC1_Handler.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; //4分频,ADCCLK = PCLK2/4 = //84/4=21MHz ADC1_Handler.Init.Resolution = ADC_RESOLUTION_12B; //12位模式 ADC1_Handler.Init.DataAlign = ADC_DATAALIGN_RIGHT; //右对齐 ADC1_Handler.Init.ScanConvMode = ENABLE; //扫描模式 ADC1_Handler.Init.EOCSelection = ADC_EOC_SINGLE_CONV; //开启EOC转换一次中断 ADC1_Handler.Init.NbrOfConversion = 3; //3个转换在规则序列 ADC1_Handler.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发 ADC1_Handler.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; //使用软件触发 ADC1_Handler.Init.ContinuousConvMode = ENABLE; //开启连续转换 ADC1_Handler.Init.DMAContinuousRequests = ENABLE; //开启DMA请求 ADC1_Handler.DMA_Handle = &hdma_adc; //传输DMA句柄 HAL_ADC_Init(&ADC1_Handler); //初始化 ADC_ChannelConfTypeDef ADC_ChanneConf; //电压采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_4; //通道4 ADC_ChanneConf.Rank = 1; //第一个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC1_Handler, &ADC_ChanneConf); //参考电压采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_VREFINT; //参考电压通道 ADC_ChanneConf.Rank = 2; //第二个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC1_Handler, &ADC_ChanneConf); //温度传感器采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_16; //读取温度通道 ADC_ChanneConf.Rank = 3; //第三个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC1_Handler, &ADC_ChanneConf); HAL_ADC_Start_DMA(&ADC1_Handler, (uint32_t*)adc_value, 3); //开启DMA ADC传输 } (4) 重定义HAL_ADC_MspInit()函数,初始化PA4引脚并使能ADC1。 //定义ADC底层驱动 void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_ADC1_CLK_ENABLE(); //使能ADC1时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); //开启GPIOA时钟 GPIO_Initure.Pin = GPIO_PIN_4; //PA4 GPIO_Initure.Mode = GPIO_MODE_ANALOG; //模拟 GPIO_Initure.Pull = GPIO_NOPULL; //浮空 HAL_GPIO_Init(GPIOA, &GPIO_Initure); } (5) 定义结构体ADC_DATA,分别存储通道电压、参考电压和温度的值。 typedef struct { float Vchannel4; float VREF; float temperature; }ADC_DATA; (6) 创建DMA_GetData()函数,根据公式和数据缓冲区中的电压值分别计算出参考电压、通道电压和温度的值。 ADC_DATA adc_data; void DMA_GetData(void) { adc_data.VREF = 3.3 * VREFINT_CAL /adc_value[1]; //计算参考电压 adc_data.Vchannel4 = adc_data.VREF /4095 * adc_value[0]; //计算通道电压 adc_data.temperature = (((adc_data.VREF /4095 * adc_value[2]) - 0.76f) / 2.5f) + 25; //计算内部温度 } (7) 主函数main()程序如下: 第一步,初始化系统时钟、串口和ADCDMA。 第二步,调用DMA_GetData ()函数分别计算参考电压、通道电压和温度的值。 第三步,在while循环中将3个值分别通过串口打印出来。 extern ADC_DATA adc_data; int main() { CLOCK_Init(); //时钟初始化 UART_Init(); //串口初始化 ADC_DMA_Init(); //ADCDMA初始化 while(1) { DMA_GetData(); //ADC-DMA获取数据 printf("Vref: %.2f Vch4: %.2f Temp: %.2f\r\n", adc_data.VREF, adc_data.Vchannel4, adc_data.temperature); HAL_Delay(100); } } 5.3.3运行结果 将程序下载到开发板中,找到USART1引脚通过USB转串口模块连接到计算机上,打开串口助手,配置好波特率、数据位、校验位、停止位等,单击打开串口。可以看到串口分别打印出参考电压、通道电压和温度传感器的值,可以取一根杜邦线将PA4引脚连接到3.3V电源或GND引脚,再观察一下数值,如图512所示。 图512运行结果 练习 (1) STM32F4系列的DMA有哪些传输模式? (2) 配置其他ADC实现ADC数据转换。 视频21 5.4ADC: 双重ADC交叉模式 学习目标 掌握ARM CortexM系列芯片外设多ADC工作模式的工作原理,通过配置STM32F407的双重ADC模式,使用ADC1和ADC2来同时测量引脚电压。 5.4.1开发原理 AD转换包括采样阶段和转换阶段,在采样阶段才对通道数据进行采集; 而在转换阶段只是将采集到的数据转换为数字量输出,此刻通道数据变化不会改变转换结果。独立模式的ADC采集需要在一个通道采集并且转换完成后才会进行下一个通道的采集。双重或者三重ADC的机制使用两个或以上ADC同时采样两个或以上不同通道的数据或者交叉采集两个或以上ADC同一通道的数据。双重或者三重ADC模式较独立模式一个最大的优势就是转换速度快。 在有两个或多个ADC模块的产品中,可以使用双重ADC模式和三重ADC模式。在多ADC模式里,根据ADC_CCR寄存器中MULTI[4:0]位所选的模式,转换的启动可以是按照“ADC1主,ADC2和ADC3从”的模式交替触发或同步触发。 在多重ADC模式下,当转换配置成由外部事件触发时,用户必须将其设置为仅触发主ADC,从ADC设置为由软件触发,这样可以防止意外地触发从ADC转换。 ADC有4种可能的模式: 同步注入模式。 同步规则模式。 交叉模式。 交替触发模式。 还有可以用下列组合方式: 同步注入模式+同步规则模式。 同步规则模式+交替触发模式。 在双重ADC模式下,ADC公共的数据寄存器(ADC_CDR)包含ADC1、ADC2和ADC3的规则转换数据,32位寄存器的所有位都将被使用。 在多DMA模式下,DMA支持3种模式: 1) DMA模式1 每次DMA请求(只有一个数据项是有效的),半字代表ADC转换数据项被传输。 在双重ADC模式下,第一个请求时,ADC1数据先被传输; 第二个请求时,ADC2数据被传输; 以此类推。 在三重ADC模式下,第一个请求时,ADC1数据被传输; 第二个请求时,ADC2数据被传输; 第三个请求时,ADC3被传输; 以此类推。 DMA模式1用于规则通道的三重ADC模式。 举一个例子: 规则通道的三重ADC模式: 产生3个DMA请求(一个请求对应一次转换数据项)。 第1个请求ADC_CDR[31:0] = ADC1_DR[15:0] 第2个请求ADC_CDR[31:0] = ADC2_DR[15:0] 第3个请求ADC_CDR[31:0] = ADC3_DR[15:0] 第4个请求 …… 2) DMA模式2 每次DMA请求(两个数据项有效)代表2个ADC转换数据项被传输,也就是一个字。 在双重ADC模式下,第一个请求时,ADC2和ADC1数据同时被传输(ADC2占高16位,ADC1占低16位),后面以此类推。 在三重ADC模式下,第一个请求时ADC2和ADC1数据被传输,第二个请求时ADC1和ADC3数据被传输,第三个请求时ADC3和ADC2数据被传输。 DMA模式2用于交叉模式和规则同步模式(仅双重ADC模式)。 举一个例子: 双重ADC交叉模式: 每次2个数据项有效时将产生DMA请求。 第1个请求ADC_CDR[31:0] = ADC2_DR[15:0] | ADC1_DR[15:0] 第2个请求ADC_CDR[31:0] = ADC2_DR[15:0] | ADC1_DR[15:0] …… 三重ADC交叉模式: 每次2个数据项有效时将产生DMA请求。 第1个请求ADC_CDR[31:0] = ADC2_DR[15:0] | ADC1_DR[15:0] 第2个请求ADC_CDR[31:0] = ADC1_DR[15:0] | ADC3_DR[15:0] 第3个请求ADC_CDR[31:0] = ADC3_DR[15:0] | ADC2_DR[15:0] 第4个请求ADC_CDR[31:0] = ADC2_DR[15:0] | ADC1_DR[15:0] …… 3) DMA模式3 这个模式和模式2类似,仅有的不同是每次DMA请求(两个数据项有效)两个字节代表两个ADC转换的数据项,也就是一个半字,数据传输顺序和DMA模式2类似。 双重ADC交叉模式: 每次两个数据项有效时将产生DMA请求。 第1个请求ADC_CDR[31:0] = ADC2_DR[7:0] | ADC1_DR[7:0] 第2个请求ADC_CDR[31:0] = ADC2_DR[7:0] | ADC1_DR[7:0] …… 三重ADC交叉模式: 每次两个数据项有效时将产生DMA请求。 第1个请求ADC_CDR[31:0] = ADC2_DR[7:0] | ADC1_DR[7:0] 第2个请求ADC_CDR[31:0] = ADC1_DR[7:0] | ADC3_DR[7:0] 第3个请求ADC_CDR[31:0] = ADC3_DR[7:0] | ADC2_DR[7:0] 第4个请求ADC_CDR[31:0] = ADC2_DR[7:0] | ADC1_DR[7:0] …… 5.4.2开发步骤 (1) 本节选取ADC1_IN4作为数据采集通道,首先分别定义ADC1和ADC2初始化结构体,以及接收数据存储变量。 ADC_HandleTypeDef AdcHandle1; //ADC1初始化句柄 ADC_HandleTypeDef AdcHandle2; //ADC2初始化句柄 uint16_t uhADCDualConvertedValue; //获取转换值的变量 (2) 创建MultiADC_Config()函数,用来初始化ADC1和ADC2以及相应配置。 第一步,调用HAL_DMA_Init()函数,初始化ADC的DMA传输。 第二步,调用HAL_ADC_Init()函数,初始化ADC1并初始化ADC1的通道4。 第三步,调用HAL_ADC_Init()函数,初始化ADC2并初始化ADC2的通道4。 第四步,调用HAL_ADCEx_MultiModeConfigChannel()函数,配置ADC为双重交叉模式,且配置DMA模式为模式3,每次DMA请求传输两个字节(ADC1和ADC2各一个字节)。最后开启ADC的传输。 //ADC1和ADC2初始化函数 void MultiADC_Config(void) { //DMA初始化 DMA_HandleTypeDef hdma_adc; __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc.Instance = DMA2_Stream0; //数据流 hdma_adc.Init.Channel= DMA_CHANNEL_0; //通道0 hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; //外设到内存 hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; //外设地址不自增 hdma_adc.Init.MemInc = DMA_MINC_ENABLE; //内存地址自增 hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; //一次传输半字 hdma_adc.Init.MemDataAlignment = DMA_PDATAALIGN_HALFWORD; //一次传输半字 hdma_adc.Init.Mode = DMA_CIRCULAR; //循环转换模式 hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; //优先级高 hdma_adc.Init.MemBurst = DMA_MBURST_SINGLE; //突发 hdma_adc.Init.PeriphBurst = DMA_PBURST_SINGLE; HAL_DMA_Init(&hdma_adc); //初始化ADC1 AdcHandle1.Instance = ADC1; AdcHandle1.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV4; //4分频 AdcHandle1.Init.Resolution = ADC_RESOLUTION_8B; //8bit AdcHandle1.Init.ScanConvMode = DISABLE; //扫描模式不使能 AdcHandle1.Init.ContinuousConvMode = ENABLE; //连续转换模式使能 AdcHandle1.Init.ExternalTrigConv = ADC_SOFTWARE_START; //软件触发 AdcHandle1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; //使用软件触发 AdcHandle1.Init.DataAlign = ADC_DATAALIGN_RIGHT; //数据右对齐 AdcHandle1.Init.NbrOfConversion = 1; AdcHandle1.Init.DMAContinuousRequests = ENABLE; //使用DMA AdcHandle1.DMA_Handle = &hdma_adc; //传递DMA句柄 AdcHandle1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; //开启EOC转换一次完成中断 HAL_ADC_Init(&AdcHandle1); //初始化ADC1 //配置ADC1通道 ADC_ChannelConfTypeDef ADC_ChanneConf; //ADC通道配置 ADC_ChanneConf.Channel = ADC_CHANNEL_4; //ADC1通道 ADC_ChanneConf.Rank = 1; //转换等级 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_3CYCLES; //采样周期 HAL_ADC_ConfigChannel(&AdcHandle1, &ADC_ChanneConf); //初始化ADC2 AdcHandle2.Instance = ADC2; AdcHandle2.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV4; AdcHandle2.Init.Resolution = ADC_RESOLUTION_8B; AdcHandle2.Init.ScanConvMode = DISABLE; AdcHandle2.Init.ContinuousConvMode = ENABLE; AdcHandle2.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; AdcHandle2.Init.ExternalTrigConv = ADC_SOFTWARE_START; AdcHandle2.Init.DataAlign = ADC_DATAALIGN_RIGHT; AdcHandle2.Init.NbrOfConversion = 1; AdcHandle2.Init.DMAContinuousRequests = ENABLE; AdcHandle2.DMA_Handle = &hdma_adc; //传递DMA句柄 AdcHandle2.Init.EOCSelection = ADC_EOC_SINGLE_CONV; HAL_ADC_Init(&AdcHandle2); //初始化ADC2 //配置ADC2通道 ADC_ChanneConf.Channel = ADC_CHANNEL_4; //ADC2通道 HAL_ADC_ConfigChannel(&AdcHandle2, &ADC_ChanneConf); //配置双重交叉模式 ADC_MultiModeTypeDefADC_Multimode; ADC_Multimode.Mode = ADC_DUALMODE_INTERL; //双重ADC交叉模式 ADC_Multimode.DMAAccessMode = ADC_DMAACCESSMODE_3; //每次DMA请求传输两个字节 ADC1/ADC2 //数据一起传输 ADC_CDR[15:0] = ADC2_DR[7:0] | ADC1_DR[7:0] ADC_Multimode.TwoSamplingDelay = ADC_TWOSAMPLINGDELAY_6CYCLES; //配置两个采样之间的延迟 HAL_ADCEx_MultiModeConfigChannel(&AdcHandle1, &ADC_Multimode); HAL_ADC_Start(&AdcHandle2); //开始ADC2 HAL_ADCEx_MultiModeStart_DMA(&AdcHandle1, (uint32_t*)&uhADCDualConvertedValue, 1); } (3) 由STM32F4数据手册可知,ADC1_IN4和ADC2_IN4通道都在PA4引脚,如图513所示。 图513引脚映射 (4) 重定义HAL_ADC_MspInit()函数,初始化PA4引脚,并使能ADC1和ADC2时钟。 //初始化ADC底层驱动引脚 void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_ADC1_CLK_ENABLE(); //使能ADC1时钟 __HAL_RCC_ADC2_CLK_ENABLE(); //使能ADC2时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); //开启GPIOA时钟 GPIO_Initure.Pin = GPIO_PIN_4; //PA4 GPIO_Initure.Mode = GPIO_MODE_ANALOG; //模拟 GPIO_Initure.Pull = GPIO_NOPULL; //浮空 HAL_GPIO_Init(GPIOA, &GPIO_Initure); } (5) 定义结构体ADC_DATA,用来存储ADC1和ADC2测出的数字值和计算后的电压。 typedef struct { uint8_t adc1_value; //存储ADC1测出的值 uint8_t adc2_value; //存储ADC1测出的值 float adc1_voltage; //存储计算得到的ADC1电压 float adc2_voltage; //存储计算得到的ADC2电压 }ADC_DATA; (6) 创建MultiADC_GetData()函数,用来处理ADC测得的数据。函数中首先获取数据变量uhADCDualConvertedValue的高8位数据和低8位数据,其中低8位为ADC1测出的数据,高8位为ADC2测出的数据。获取数据后分别计算ADC1和ADC2的电压。 //数据处理函数 void MultiADC_GetData(void) { //获取ADC1/ADC2测出数字值 adc_data.adc1_value = (uhADCDualConvertedValue & 0x00FF); adc_data.adc2_value = (uhADCDualConvertedValue >> 8); //计算ADC1/ADC2得到的电压 adc_data.adc1_voltage = adc_data.adc1_value * 3.3 / 255; adc_data.adc2_voltage = adc_data.adc2_value * 3.3 / 255; } (7) 主函数main()程序如下: 第一步,初始化系统时钟和串口。 第二步,调用MultiADC_Config()函数,初始化双重ADC及交叉模式。 第三步,调用MultiADC_GetData()函数,分别获取两个ADC的数据。 第四步,在while()循环中每隔100ms将ADC1和ADC2通过串口打印出来。 extern ADC_DATA adc_data; int main() { CLOCK_Init(); //时钟初始化 UART_Init(); //串口初始化 MultiADC_Config(); //双重ADC交叉模式初始化 while(1) { MultiADC_GetData(); //双重ADC交叉模式获取数据 printf("ADC1 voltage: %.2f ADC2 voltage: %.2f\r\n", adc_data.adc1_voltage, adc_data.adc2_voltage); HAL_Delay(100); } } 5.4.3运行结果 将程序下载到开发板中,将USART1引脚通过USB转串口模块连接到计算机上,打开串口助手,配置好波特率、数据位、校验位、停止位等,单击打开串口。可以看到串口中打印出ADC1和ADC2分别测出的电压值,如图514所示。 图514运行结果 练习 (1) 简述AD转换包括的采样阶段以及转换阶段。 (2) 简述在多重DMA模式下,DMA支持的3种模式。 (3) 配置其他ADC实现AD转换。 视频22 5.5ADC: 定时器触发模式 学习目标 掌握ARM CortexM系列芯片外设ADC定时器触发的工作原理,通过配置STM32F407的定时器和ADC,使用定时器间隔触发ADC来测量电压值。 5.5.1开发原理 本节使用定时器的方式来触发ADC测量电压值。首先选取定时器1为触发定时器,并配置定时器为PWM输出的方式,配置定时器的分频系数为16800,因定时器1的时钟为168M,所以定时器计数频率为168MHz/16800 = 10kHz; 设置定时器预装载值为10000,所以定时器的溢出时间为1s,设置PWM的比较值为5000,且ADC触发方式为上升沿触发,所以ADC将每隔1s进行一次测量。 5.5.2开发步骤 (1) 本节同样选取ADC1的通道4为测量电压通道。 (2) 创建TIM1_Config()函数,用来初始化定时器及PWM输出。 //定时器初始化函数 static void TIM1_Config(void) { TIM_HandleTypeDef TIM_Handle; //定时器初始化结构体变量 TIM_OC_InitTypeDef TIM_OC_Handle; //定时器输出初始化结构体变量 __TIM1_CLK_ENABLE();//使能定时器1时钟 //定时器初始化 TIM_Handle.Channel = HAL_TIM_ACTIVE_CHANNEL_1; //通道1 TIM_Handle.Instance = TIM1; //选择定时器1 TIM_Handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; //时钟1分频 TIM_Handle.Init.CounterMode = TIM_COUNTERMODE_UP; //向上计数模式 TIM_Handle.Init.Period =10000 - 1; //自动重装载值 TIM_Handle.Init.Prescaler =16800 - 1; //预分频系数 HAL_TIM_PWM_Init(&TIM_Handle); //初始化定时器 //定时器输出PWM初始化 TIM_OC_Handle.OCMode = TIM_OCMODE_PWM1; //模式选择PWM1 TIM_OC_Handle.OCPolarity = TIM_OCPOLARITY_LOW; //输出比较极性为低 TIM_OC_Handle.Pulse =5000; //设置比较值,此值用来确定占空比,默认比较值为自动重装载值 //的一半,即占空比为50% HAL_TIM_PWM_ConfigChannel(&TIM_Handle, &TIM_OC_Handle, TIM_CHANNEL_1); //配置PWM输出 HAL_TIM_PWM_Start(&TIM_Handle, TIM_CHANNEL_1); //开始PWM输出 } (3) 创建TIM_ADC_Init ()函数,用来初始化ADC。 第一步,调用HAL_ADC_Init()函数,初始化ADC1和ADC1的通道4,此时设置ADC为定时器1通道1的上升沿触发。 第二步,调用TIM1_Config()函数,初始化触发定时器,并开启ADC中断。 uint16_t adc_value; //存储变量 ADC_HandleTypeDef ADC1_Handler; //ADC句柄 //ADC初始化函数 void TIM_ADC_Init(void) { //初始化ADC ADC1_Handler.Instance = ADC1; ADC1_Handler.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; //4分频,ADCCLK = PCLK2/4 = //84/4=21MHz ADC1_Handler.Init.Resolution = ADC_RESOLUTION_12B; //12位模式 ADC1_Handler.Init.DataAlign = ADC_DATAALIGN_RIGHT; //右对齐 ADC1_Handler.Init.ScanConvMode = DISABLE; //不扫描模式 ADC1_Handler.Init.EOCSelection = ADC_EOC_SINGLE_CONV; //开启EOC转换一次中断 ADC1_Handler.Init.ContinuousConvMode = DISABLE; //不开启连续转换 ADC1_Handler.Init.NbrOfConversion = 1; //1个转换在规则序列 ADC1_Handler.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T1_CC1; //使用定时器1通道1触发 ADC1_Handler.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; //使用上升沿触发 ADC1_Handler.Init.DMAContinuousRequests = DISABLE; //不开启DMA请求 HAL_ADC_Init(&ADC1_Handler); //初始化 ADC_ChannelConfTypeDef ADC_ChanneConf; //电压采集通道初始化 ADC_ChanneConf.Channel = ADC_CHANNEL_4; //通道4 ADC_ChanneConf.Rank = 1; //第一个采样 ADC_ChanneConf.SamplingTime = ADC_SAMPLETIME_480CYCLES; //周期采样时间 HAL_ADC_ConfigChannel(&ADC1_Handler, &ADC_ChanneConf); TIM1_Config(); //初始化定时器 HAL_ADC_Start_IT(&ADC1_Handler); //开启DMA ADC传输 } (4) 由STM32F4数据手册可知,ADC1_IN4通道对应PA4引脚,如图515所示。 图515引脚映射 (5) 重定义HAL_ADC_MspInit()函数,初始化PA4引脚,并使能ADC1时钟,同时设置ADC的中断优先级并使能中断。 //初始化ADC底层驱动引脚 void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_ADC1_CLK_ENABLE(); //使能ADC1时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); //开启GPIOA时钟 GPIO_Initure.Pin = GPIO_PIN_4; //PA4 GPIO_Initure.Mode = GPIO_MODE_ANALOG; //模拟 GPIO_Initure.Pull = GPIO_NOPULL; //浮空 HAL_GPIO_Init(GPIOA, &GPIO_Initure); HAL_NVIC_SetPriority(ADC_IRQn, 0, 0); //使能ADC中断优先级 HAL_NVIC_EnableIRQ(ADC_IRQn); //使能ADC中断 } (6) 定义ADC的中断服务函数和数据转换完成回调函数,在回调函数中通过函数HAL_ADC_GetValue()获取ADC的测量数字值,然后计算得出电压值,最后通过串口打印出电压值。 //ADC中断服务函数 void ADC_IRQHandler(void) { HAL_ADC_IRQHandler(&ADC1_Handler); } //ADC转换完成回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { float adc_value1; adc_value = HAL_ADC_GetValue(&ADC1_Handler); //获取ADC测量值 adc_value1 = adc_value * 3.3 /4095; //计算ADC测量电压 printf("%.2f\r\n", adc_value1); //定时器触发ADC获取数据并通过串口打印 } (7) 主函数main()程序如下: 第一步,初始化系统时钟和串口。 第二步,初始化定时器和ADC。 int main() { CLOCK_Init(); //时钟初始化 UART_Init(); //串口初始化 TIM_ADC_Init(); //定时器触发ADC初始化 while(1) { } } 5.5.3运行结果 将程序下载到开发板中,将USART1引脚通过USB转串口模块连接到计算机上,打开串口助手,配置好波特率、数据位、校验位、停止位等,单击打开串口。可以看到串口中每隔1s打印出ADC1测出的电压值,如图516所示。 图516运行结果 练习 (1) 简述ADC定时器触发的过程。 (2) 配置其他ADC实现ADC数据转换。