单片机学习!
目录
[4.1 开启RCC时钟](#4.1 开启RCC时钟)
[4.2 配置GPIO](#4.2 配置GPIO)
[4.3 配置多路开关](#4.3 配置多路开关)
[4.4 结构体初始化ADC](#4.4 结构体初始化ADC)
[4.5 DMA参数初始化配置](#4.5 DMA参数初始化配置)
[4.5.1 外设站点的三个参数](#4.5.1 外设站点的三个参数)
[4.5.2 存储器站点的三个参数](#4.5.2 存储器站点的三个参数)
[4.5.3 传输方向](#4.5.3 传输方向)
[4.5.4 传输计数器](#4.5.4 传输计数器)
[4.5.5 是否使用自动重装](#4.5.5 是否使用自动重装)
[4.5.6 选择触发源](#4.5.6 选择触发源)
[4.5.7 通道优先级](#4.5.7 通道优先级)
[4.5.8 DMA_Init](#4.5.8 DMA_Init)
[4.6 DMA开关控制](#4.6 DMA开关控制)
[4.7 开启ADC到DMA的输出](#4.7 开启ADC到DMA的输出)
[4.8 开启ADC电源](#4.8 开启ADC电源)
[4.9 ADC进行校准](#4.9 ADC进行校准)
[4.9.1 复位校准](#4.9.1 复位校准)
[4.9.2 等待复位校准完成](#4.9.2 等待复位校准完成)
[4.9.3 开始校准](#4.9.3 开始校准)
[4.9.4 等待校准完成](#4.9.4 等待校准完成)
[4.10 启动AD转换与DMA转运函数设计](#4.10 启动AD转换与DMA转运函数设计)
[4.10.1 ADC软件触发转换。](#4.10.1 ADC软件触发转换。)
[4.10.2 传输计数器赋值](#4.10.2 传输计数器赋值)
[4.10.3 标志位查看/清除](#4.10.3 标志位查看/清除)
[4.10.4 启动函数代码](#4.10.4 启动函数代码)
一、DMA配置步骤
初始化步骤:
第一步,RCC开启DMA的时钟。
第二步,参数初始化配置。直接调用DMA_Init,初始化各个参数,包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源、通道优先级。这些所有的参数,通过一个结构体,就可以配置好了。
第三步,开关控制,调用DMA_Cmd函数,给指定的通道使能,就完成了。
- 如果选择的是硬件触发,需要在对应的外设调用一下XXX_DMACmd函数,开启一下触发信号的输出;
- 如果需要DMA的中断,那就调用DMA_ITConfig,开启中断输出。再在NVIC里配置相应的中断通道,然后写中断函数就可以了。
最后,在运行的过程中,如果转运完成,传输计数器清0了,这时再想给传输计数器赋值的话,就DMA失能 -> 写传输计数器 -> DMA使能,这样就行了。
二、ADC配置步骤
参照结构框图,在原理上将ADC外设运转起来。
第一步:开启RCC时钟,包括ADC和GPIO的时钟。另外ADCCLK的分频器也需要配置。
第二步:配置GPIO,把需要用到的GPIO配置成模拟输入的模式。
第三步:配置多路开关,把通道接入到规则组列表里。
第四步:配置ADC转换器,库函数中用结构体来配置电路参数,包括ADC是单次转换还是连续转换;扫描还是非扫描;有几个通道;触发源是什么;数据对齐是左对齐还是右对齐。
- 如果需要模拟看门狗,可以用几个库函数来配置阈值和监测通道;
- 如果需要开启中断,在中断输出控制里用 ITConfig 函数开启对应的中断输出,然后再在NVIC里配置一下优先级。这样就能触发中断了。
最后:开关控制,调用一下ADC_Cmd函数,开启ADC.
在开启ADC之后,根据STM32手册的建议,还可以对ADC进行一下校准,这样可以减小误差。
在ADC工作的时候,如果想要软件触发转换,有库函数可以触发;如果想读取转换结果,也会有函数可以读取结果。
三、DMA+AD多通道框图
下图对应的任务是ADC扫描模式+DMA。图左边是ADC扫描模式的执行流程,有7个通道,触发一次后,7个通道依次进行AD转换。然后转换结果都放到ADC_DR数据寄存器里面。DMA要做的就是,在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增。这样数据就不会被覆盖了。
DMA基本结构参数配置:
首先 是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。
- 外设地址 应该写入 ADC_DR 这个寄存器地址,存储器地址可以在 SRAM 中定义一个数组 ADValue ,然后把 ADValue 的地址当作存储器的地址;
- 数据宽度,因为 ADC_DR 和 SRAM 数组需要的数据类型都是uint16_t,所以数据宽度都是16位的半字传输;
- 地址是否自增,应该是外设地址不自增,存储器地址自增。
第二 是方向参数,根据任务图,应该是外设站点转运到存储器站点了。
第三 是传输计数器 和是否要自动重装。
传输计数器这里有7个通道,所以计数7次。
计数器是否自动重装,这需要看ADC的配置。
ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止;
ADC如果是连续扫描,那DMA的传输计数器就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作。
第四 是触发选择部分,这里 ADC_DR 的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转化完成同步。那DMA的触发要选择ADC的硬件触发。
硬件触发这里需要说明一下。在ADC扫描模式下,每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以程序不太好判断某一个通道转换完成的时机是什么时候。这里虽然单个通道转换完成后,不产生任何标志位和中断,但是应该会产生DMA请求,去触发DMA转运。
最后 ,给DMA使能,调用DMA_Cmd函数之后,数据就会从 ADC_DR 数据寄存器转运到 SRAM 数组了。
一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描模式有个数据覆盖的特征,或者可以说这个数据覆盖的问题是ADC固有的缺陷,而这个缺陷也使得ADC和DMA成为了最常见的伙伴。ADC对DMA的需求是非常强烈的,其它一些外设使用DMA可以提高效率,是锦上添花的操作,但是不使用也是可以的,顶多损失一些性能。但是ADC的扫描模式,如果不使用DMA,功能都会受到很大的限制。所以ADC和DMA的结合最为常见。
四、DMA+AD多通道函数设计详细步骤
这里代码使用的是ADC的扫描模式,加DMA数据转运,执行流程如图:
4.1 开启RCC时钟
第一步:开启RCC时钟。包括ADC和GPIO的时钟。另外ADCCLK的分频器也需要配置。
代码示例:
cpp
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//还需要开启PA0口的时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置ADCCLK
RCC_APB2PeriphClockCmd函数开启ADC1的时钟,ADC都是APB2上的设备,所以这里用APB2开启时钟的函数。
RCC_APB2PeriphClockCmd函数用于开启PA0口的时钟。
以上时钟就配置好了,还需要配置ADCCLK,用RCC_ADCCLKConfig/函数来配置,函数有四个参数分别是2、4、6、8分频:
- RCC_PCLK2_Div2: ADC clock = PCLK2/2
- RCC_PCLK2_Div4: ADC clock = PCLK2/4
- RCC_PCLK2_Div6: ADC clock = PCLK2/6
- RCC_PCLK2_Div8: ADC clock = PCLK2/8
函数参数配置好之后,ADC的CLOCK=PCLK2/2、PCLK2/4、PCLK2/6、PCLK2/8,参数的PCLK2就是APB2时钟的意思。
代码示例中选择6分频,分频之后,ADCCLK=72MHz/6=12MHz
4.2 配置GPIO
第二步:配置GPIO,把需要用到的GPIO配置成模拟输入的模式。
代码示例:
cpp
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AIN;//选择模拟输入
GPIO_InitStruct.GPIO_Pin= GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
代码示例中选择GPIO_Mode_AIN模拟输入这个模式,在GPIO_Mode_AIN模式下,GPIO是无效的,断开GPIO口,防止GPIO口的输入输出对模拟电压造成干扰。GPIO_Mode_AIN模式就是ADC的专属模式。
4**.3 配置多路开关**
第三步:配置多路开关,把通道接入到规则组列表里。
用ADC_RegularChannelConfig函数可选择规则组的输入通道。
函数参数:
- 第1个参数:选择ADC。
- 第2个参数:指定通道,通道0~通道17.
- 第3个参数:Rank,规则组序列器里的次序,在1~16之间。若只有PA0一个通道,使用的是非扫描模式,那指定的通道就放在第一个序列1的位置。
- 第4个参数:指定通道的采样时间,采样时间参数根据需求调整,需要更快的转换,就选择小的参数;需要更稳定的转换,就选择大的参数。
如果想在序列2的位置写入其他通道,就可以复制一下这个代码,把序列数改成2,然后指定你想要的通道,若还需要继续填充序列,可以再复制这个函数,修改序列和通道,另外每个通道也可以设置不同的采样时间,在函数最后一个参数修改即可。
代码示例:
cpp
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);
代码示例的配置是:在ADC1中,在规则组菜单列表的第一个位置,写入通道0、1、2、3通道,指定通道的采样时间参数选择的采样时间为55.5个ADCCLK的周期。
因为需要扫描PA0到PA3这4个通道,所以调用四次ADC_RegularChannelConfig函数。通道0放在序列1的位置;通道1放在序列2的位置;通道2放在序列3的位置;通道3放在序列4的位置。
需要转运的1~4号空位,填上了0~3这4个通道。通道和次序可以任意修改,修改后最终结果存放的顺序也会相应变化。
4.4 结构体初始化ADC
第四步、用结构体初始化ADC。这里使用ADC,单次转换,扫描模式。
代码示例:
cpp
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;//ADC的工作模式
ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;//数据对齐
ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//外部触发转换选择
ADC_InitStructure.ADC_ContinuousConvMode=DISABLE;//连续/单次转换模式
ADC_InitStructure.ADC_ScanConvMode=ENABLE;//扫描/非扫描转换模式
ADC_InitStructure.ADC_NbrOfChannel=4;//通道数目
ADC_Init(ADC1,&ADC_InitStructure);
ADC_Mode 是ADC的工作模式,这个参数是配置ADC是工作在独立模式还是双ADC模式,代码示例选择独立模式。
ADC_DataAlign 数据对齐,这里介绍是指定ADC数据是左对齐还是右对齐。
- ADC_DataAlign_Right右对齐;
- ADC_DataAlign_Left左对齐
ADC_ExternalTrigConv外部触发转换选择,就是触发控制的触发源,定义用于启动规则组转换的外部触发源。参数对应结构框图中的外部触发源选择。代码示例选择 ADC_ExternalTrigConv_None 参数,就是不使用外部触发,也就是使用内部软件触发的意思。
ADC_ContinuousConvMode 连续/单次转换模式,这个参数可以选择是ENABLE连续转换模式还是DISABLE单次转换模式。
ADC_ScanConvMode扫描/非扫描转换模式,这个参数可以选择是ENABLE扫描模式(多通道)还是DISABLE非扫描模式(单通道)。
ADC_NbrOfChannel通道数目,这个是在指定扫描模式下,总共会有几个通道需要扫描,参数必须在1~16之间。这个参数仅在扫描模式下使用。因为需要扫描PA0到PA3这4个通道,所以通道数目填4.
后三个参数设置可以对应四种模式:
- 单次转换非扫描模式。
- 连续转换非扫描模式。
- 单次转换扫描模式。
- 连续转换扫描模式。
这里配置的是单次转换,扫描模式。
4.5 DMA参数初始化配置
第五步,参数初始化配置。直接调用DMA_Init,初始化各个参数。
参数包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源、通道优先级。
这些所有的参数,通过一个结构体,就可以配置好了。
代码示例:
cpp
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//起始地址
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//数据宽度
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//是否自增
DMA_InitStructure.DMA_MemoryBaseAddr=AddrB;//起始地址
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;//数据宽度
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//是否自增
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
DMA_InitStructure.DMA_BufferSize=Size;//传输计数器
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;//否使用自动重装
DMA_InitStructure.DMA_M2M=DMA_M2M_Enable;//选择硬件触发还是软件触发
DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
4.5.1 外设站点的三个参数
外设站点的三个参数:
- 起始地址
- 数据宽度
- 地址是否自增
- 起始地址
DMA_PeripheralBaseAddr 起始地址,外设站点的基地址。这在里要写一个32位的地址,比如 0x20000000 这样的地址。
外设站点的起始地址是DMA转运的源头,AD转换完成后数据结果都放在ADC_DR寄存器里。所以外设站点的起始地址就填ADC_DR寄存器的地址。
ADC1的DR寄存器地址是0x4001 244C,所以可以直接填0x4001244C。但是库函数已经算好了具体地址,一般寄存器地址也不需要算好后填实际值,所以可以对用 ADC1->DR 的方法取ADC_DR寄存器地址,再强转为uint32_t类型。也就是这样: (uint32_t)&ADC1->DR ,得到的结果就是0x4001244C。
代码示例:
cpp
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//起始地址
这样外设站点的地址就完成了。
- 数据宽度
DMA_PeripheralDataSize数据宽度,函数定义中介绍是,指定数据宽度的参数可以是以下值:
- DMA_PeripheralDataSize_Byte Byte,字节,就是uint8_t ;
- DMA_PeripheralDataSize_HalfWord HalfWord,半字,就是uint16_t ;
- DMA_PeripheralDataSize_Word Word,字,就是uint32_t 。
这里需要DR寄存器低16位的数据,所以就填入DMA_PeripheralDataSize_HalfWord,半字。
- 地址是否自增
DMA_PeripheralInc 地址是否自增。函数定义中解释是,指定外设地址是自增或者不是。
参数取值:
- DMA_PeripheralInc_Enable 自增
- DMA_PeripheralInc_Disable 不自增
这里不自增,始终转运同一个位置的数据,所以这里选择 DMA_PeripheralInc_Disable 。
这样外设站点的参数就配置好了:
cpp
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//起始地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//是否自增
4.5.2 存储器站点的三个参数
存储器站点的三个参数:
- 起始地址
- 数据宽度
- 地址是否自增
- 起始地址
DMA_MemoryBaseAddr 起始地址,存储器站点的基地址。存储器地址可以在 SRAM 中定义一个数组 ADValue ,然后把 ADValue 的地址当作存储器的地址。
这里需要把数据存在SRAM数组里,所以现在代码块最前面定义一个数组AD_Value,然后在这里把AD_Value数组的地址作为目的地,把数组的地址强转为uint32_t,
示例:
cpp
DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;
这样存储器站点的地址就完成了。
- 数据宽度
DMA_MemoryDataSize 数据宽度,和外设站点一样也选择HalfWord参数,以半字传输。
- DMA_MemoryDataSize_Byte Byte,字节,就是uint8_t ;
- DMA_MemoryDataSize_HalfWord HalfWord,半字,就是uint16_t ;
- DMA_MemoryDataSize_Word Word,字,就是uint32_t 。
- 地址是否自增
DMA_MemoryInc 地址是否自增,存储器地址是自增的,每转运一次挪一个坑。这里选择地址自增。
- DMA_MemoryInc_Enable 自增
- DMA_MemoryInc_Disable 不自增
这样存储器站点的参数就配置好了:
cpp
DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;//起始地址
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;//数据宽度
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//是否自增
4.5.3 传输方向
DMA_DIR 传输方向,函数定义解释是,指定外设站点是源端还是目的地。
- DMA_DIR_PeripheralDST 外设站点作为DST,destination,目的地,外设站点作为目的地,其实就是传输方向是存储器站点到外设站点。
- DMA_DIR_PeripheralSRC 外设站点作为SRC,source,源头,外设站点作为源头,也就是传输方向是外设站点到存储器站点。
传输方向是外设站点到存储器站点,所以这里选择 DMA_DIR_PeripheralSRC参数,外设站点作为数据源。
代码示例:
cpp
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
4.5.4 传输计数器
DMA_BufferSize 缓存区大小,其实就是传输计数器。
DMA_BufferSize 在函数定义解释是,以数据单元指定缓存区大小,数据单元等于外设数据宽度或者存储器数据宽度,数据宽度取决于传输方向。
以数据单元指定缓存区大小,就是说需要传送几个数据单元,这个数据单元等于传输源站点的DataSize,简单理解就是,DMA_BufferSize就是传输计数器,指定传输几次。
可以查看DMA_Init函数的源码,DMA_BufferSize参数其实就是直接赋值给了传输计数器的寄存器,它的取值是0~65535.
因为有4个ADC通道,传输4次,所以这里DMA_BufferSize给4。
代码示例:
cpp
DMA_InitStructure.DMA_BufferSize=4;//传输数量给4
这样传输次数就完成了。
4.5.5 是否使用自动重装
DMA_Mode 传输模式,其实就是指定传输计数器是否使用自动重装。
DMA_Mode 在函数定义中的解释是,指定操作方式有对应参数取值列表。
配置 DMA_Mode 参数还有一个注意事项,函数定义中写的是循环模式,也就是自动重装,不能应用在存储器到存储器的情况下。也就是之前博文说的,自动重装和软件触发不能同时使用,如果同时使用,DMA就会连续触发,永远也不会停下来。
- DMA_Mode_Circular 循环模式,就是传输计数器自动重装
- DMA_Mode_Normal 正常模式,就是传输计数器不自动重装,自减到0后停下来。
这里可以给正常的单次模式,也可以自动重装的循环模式。这里就先将 DMA_Mode 的配置选择正常模式DMA_Mode_Normal。
代码示例:
cpp
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;//否使用自动重装
4.5.6 选择触发源
DMA_M2M 选择是否存储到存储器,其实就是选择硬件触发还是软件触发。
DMA_M2M 在函数定义中的解释是,DMA是否应用于存储器到存储器的转运模式,存储器到存储器的转运模式就是软件触发。
- DMA_M2M_Enable 使用软件触发。
- DMA_M2M_Disable 不使用软件触发,也就是使用硬件触发。
这里不使用软件触发,需要硬件触发,触发源为ADC1。
因为 ADC_DR 的值是在ADC单个通道转换完成后才会有效,DMA转运的时机,需要和ADC单个通道转化完成同步。每个通道的AD转换好了,叫DMA一下,DMA再去转运数据,这样才是合适的时机。
所以选择 DMA_M2M_Enable 使用软件触发。
代码示例:
cpp
DMA_InitStructure.DMA_M2M=DMA_M2M_Enable;//选择触发源
4.5.7 通道优先级
DMA_Priority 优先级,按照参数要求,给一个优先级。
DMA_Priority 在函数定义中的解释是,指定通道的软件优先级。
- DMA_Priority_VeryHigh 非常高
- DMA_Priority_High 高
- DMA_Priority_Medium 中等
- DMA_Priority_Low 低
这里配置DMA_Priority_Medium,中等。
代码示例:
cpp
DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
4.5.8 DMA_Init
DMA_Init 函数参数配置:
第一个参数:DMAy_Channelx,y可以是1或2,用来选择是哪个DMA;对于DMA1,x可以是1~7,或者对于DMA2,x可以是1~5.x用来选择是哪一个通道。
DMA_Init函数的第一个参数,既选择了是哪个DMA,也选择了是DMA的哪个通道。
这里通道不能任意选择,需要看DMA1请求映像图。
可以看到ADC1的硬件触发只接在了DMA1的通道1上。所以这里必须要使用DMA1的通道1,其它通道都不行。所以 DMAy_Channelx,这里y写为1,选择DMA1;x选择通道,x给1,通道1.
第二个参数的位置放 DMA_InitStructure 结构体的地址,这样就是把结构体指定的参数,配置到DMA1的通道1里面去。
代码示例:
cpp
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
4.6 DMA开关控制
第六步,开关控制,调用DMA_Cmd函数,给指定的通道使能,就完成了。
代码示例:
cpp
DMA_Cmd(DMA1_Channel1,ENABLE);
使能DMA之后,来看一下DMA转运的三个条件:
- 第一个,传输计数器不为0,满足;
- 第三个,DMA使能,满足;
- 第二个,触发源有信号,不满足。因为这里配置的是硬件触发,ADC还没启动,就不会有触发信号。
所以这里DMA使能之后,不会立刻工作。最后在ADC使能之前还有一个事需要做,就是开启ADC到DMA的输出。
4.7 开启ADC到DMA的输出
第七步,开启ADC到DMA的输出。有三个硬件触发源,具体使用哪个,取决于哪个硬件触发源的DMA输出开启了。
这里调用 ADC_DMACmd 函数,开启DMA触发信号。
代码示例:
cpp
ADC_DMACmd(ADC1,ENABLE);
4.8 开启ADC电源
第五步、开启ADC电源,调用一下ADC_Cmd函数,开启ADC。
代码示例:
cpp
ADC_Cmd(ADC1,ENABLE);
以上配置完后ADC准备就绪。
4.9 ADC进行校准
在开启ADC电源之后,根据手册的建议,还需要对ADC进行校准,校准分为以下四步。
- 复位校准
- 等待复位校准完成
- 开始校准
- 等待校准完成
4.9.1 复位校准
代码示例:
cpp
ADC_ResetCalibration(ADC1);//复位校准
4.9.2 等待复位校准完成
代码示例:
cpp
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_GetResetCalibrationStatus函数是返回复位校准的状态,要等待复位完成的话,还需要加一个while循环,若没校准完成的话,就在这个while空循环里一直等待。
获取的标志位和是否校准完成的对应关系需参考++函数定义++ 和++寄存器说明++。
函数定义:
ADC_GetResetCalibrationStatus函数定义中返回值的说明是,ADC复位校准寄存器的状态,SET或RESET。
函数代码:
cpp
/**
* @brief Gets the selected ADC reset calibration registers status.
* @param ADCx: where x can be 1, 2 or 3 to select the ADC peripheral.
* @retval The new state of ADC reset calibration registers (SET or RESET).
*/
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx)
{
FlagStatus bitstatus = RESET;
/* Check the parameters */
assert_param(IS_ADC_ALL_PERIPH(ADCx));
/* Check the status of RSTCAL bit */
if ((ADCx->CR2 & CR2_RSTCAL_Set) != (uint32_t)RESET)
{
/* RSTCAL bit is set */
bitstatus = SET;
}
else
{
/* RSTCAL bit is reset */
bitstatus = RESET;
}
/* Return the RSTCAL bit status */
return bitstatus;
}
在此函数代码中也可以看出来,它获取的就是CR2寄存器里的RSTCAL标志位。
寄存器说明:
在ADC的CR2寄存器里RSTCAL复位校准位的说明是该位由软件设置并由硬件清除,在校准寄存器被初始化后该位将被清除。
所以该标志位的用法就是软件置改位为1,那硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清0.
因为校准的第一条代码ADC_ResetCalibration(ADC1);开始复位校准,就是将RSTCAL标志位置1,然后获取复位校准状态,就是读取RSTCAL标志位这一位,所以在读取这一位的时候:
- 如果它是1,那就需要一直空循环等待;
- 如果它变为0了,那就说明复位校准完成,可以跳出等待了。
所以校准第二条代码while(ADC_GetResetCalibrationStatus(ADC1)==SET);中while的条件就是,获取标志位函数 ADC_GetResetCalibrationStatus 的返回值是不是==SET,如果等于SET,while条件为真,就会一直空循环。一旦标志位被硬件清0了,这个空循环就会自动跳出来。这样就实现了等待复位校准完成的效果。这里==SET也是可以省略的,因为返回值SET直接作为条件和是不是==SET作为条件效果是一样的。
4.9.3 开始校准
代码示例:
cpp
ADC_StartCalibration(ADC1);
调用ADC_StartCalibration函数就开始校准了,之后内部电路就会自动进行校准。
4.9.4 等待校准完成
cpp
while(ADC_GetCalibrationStatus(ADC1)==SET);//调用函数获取校准状态
调用ADC_GetCalibrationStatus函数获取校准状态,也需要将函数放于while循环内,和校准的第二步同理,循环条件是,校准标志位是不是==SET,这样就可以等待校准是否完成了。
4.10 启动AD转换与DMA转运函数设计
上文描述的代码设置使ADC和DMA配合工作的配置完成了。以下设计启动AD转换,获取DMA转运结果的函数块。
4.10.1 ADC软件触发转换。
因为现在ADC还是单次模式,所以还需要软件触发一下ADC开始。其他的ADC函数就不需要了。
调用ADC_SoftwareStartConvCmd函数,实现软件触发。
代码示例:
cpp
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
调用ADC_SoftwareStartConvCmd函数之后就可以触发,ADC就已经开始进行转换了。转换需要一段时间。
4.10.2 传输计数器赋值
因为DMA也是正常的单次模式,所以在触发ADC之前,需要再启动一次DMA转运,就需要重新再重新写入一下传输计数器。
重新给传输计数器赋值必须要
- 先给DMA失能;
- 然后给传输计数器赋值;
- 最后再给DMA使能。
代码示例:
cpp
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,4);
DMA_Cmd(DMA1_Channel1,ENABLE);
......
}
- 调用DMA_Cmd函数,使DMA失能。
- 第一个参数还是选择DMA1的通道1;
- 第二个参数给DISABLE。
- 然后就可以给传输计数器赋值了,调用DMA_SetCurrDataCounter函数。
- 第一个参数,选择DMA和通道。
- 第二个参数,指定要给传输计数器写入的值,也就是传输次数,给4。
- 最后,再次调用DMA_Cmd函数,给DMA使能。
- 第一个参数还是选择DMA1的通道1;
- 第二个参数给ENABLE。
4.10.3 标志位查看/清除
最后,等待ADC转换和DMA转运完成。因为DMA转运总是在ADC转换之后的,所以可以写入等待DMA转运完成的代码,等待ADC转换完成的代码就不需要了。
等待转运完成,可以通过查看标志位来确定转运是否完成。
因为转运也是要花一些时间的,等待转运完成调用 DMA_GetFlagStatus函数可以查看标志位。
DMA_GetFlagStatus函数中总共四种标志位,以DMA1的通道1举例,其它所有的通道,都是这4种标志位。
- DMA1_FLAG_GL1:全局标志位
- DMA1_FLAG_TC1:转运完成标志位
- DMA1_FLAG_HT1:转运过半标志位
- DMA1_FLAG_TE1:转运错误标志位
代码示例:
cpp
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
这里需要检查DMA1通道1转换完成的标志位,所以选择DMA1_FLAG_TC1参数。
转运完成之后,标志位会置1,所以需要加一个while循环,等待这个标志位==RESET,如果没有完成,就一直循环等待,这样就实现了等待转运完成的效果了。
标志位置1之后,不要忘记清除标志位,这个标志位需要手动清除。调用 DMA_ClearFlag函数。
4.10.4 启动函数代码
代码示例:
cpp
void AD_GetValue(void)//函数不需要参数和返回值
{
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,4);
DMA_Cmd(DMA1_Channel1,ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
这样当调用一下AD_GetValue函数,ADC开始转换,连续扫描4个通道,DMA也同步进行转运,AD转换结果依次放在AD_Value数组里。
五、DMA+AD多通道代码
代码示例:
cpp
uint16_t AD_Value[4];
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启PA0口的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//开启DMA的时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AIN;
GPIO_InitStruct.GPIO_Pin= GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;//ADC的工作模式
ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;//数据对齐
ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//外部触发转换选择
ADC_InitStructure.ADC_ContinuousConvMode=DISABLE;//连续/单次转换模式
ADC_InitStructure.ADC_ScanConvMode=ENABLE;//扫描/非扫描转换模式
ADC_InitStructure.ADC_NbrOfChannel=4;//通道数目
ADC_Init(ADC1,&ADC_InitStructure);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//起始地址
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//数据宽度
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;//是否自增
DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;//起始地址
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;//数据宽度
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//是否自增
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
DMA_InitStructure.DMA_BufferSize=4;//传输数量给4
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;//传输模式
DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;//选择是否存储到存储器
DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
DMA_Cmd(DMA1_Channel1,ENABLE);//使能
ADC_DMACmd(ADC1,ENABLE);//开启DMA触发信号
ADC_Cmd(ADC1,ENABLE);//开启ADC电源
ADC_ResetCalibration(ADC1);//复位校准
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_StartCalibration(ADC1);//开始校准
while(ADC_GetCalibrationStatus(ADC1)==SET);//获取校准状态
}
void AD_GetValue(void)//函数不需要参数和返回值了
{
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,4);
DMA_Cmd(DMA1_Channel1,ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了DMA+AD多通道的函数设计。