[TOG]
前言
前面介绍了ADC数模转换,得到了内部的温度值和外部电压值,我感觉这样太消耗CPU的资源了,所以我准备用DMA来帮我从AD的数据寄存器中拿出数据出来,就不用再去读取AD的数据寄存器了。
一、什么是DMA
DMA叫做直接存储器存取,就不需要我们CPU通过软件将外部寄存器或者内部的存储器的数据读取到一个地方,而是由DMA控制器来进行操作,这种方法只需要让CPU为DMA配置好一些基本的配置后,当DMA接收到对应的信号就会自动的执行,省去了CPU进行这种毫无意义的操作。
一般使用DMA都是在大量数据处理和ADC多通道操作的地方,而我们这暂时只用到ADC,所以重点还是对于ADC多通道的DMA设置。
二、DMA介绍
1.stm32的DMA
在stm32f103c8t6中有2个DMA外设,分别是DMA1和DMA2,这两个DMA是连接在AHB
高速外部总线上的,所以我们要使用DMA开启的时钟要开启AHB
,而不是APB
了。
这两个DMA中又分别有着许多通道,在DMA1中有7个通道,在DMA2中有5个通道,我们使用DMA进行转运就是使用的是这些通道。
如果我们是对外设和存储器进行转运就需要找到这个外设所对应的通道是哪一个了,下图就是这个的介绍。
下图是DMA1通道连接的外设:
下面是DMA2通道连接的外设:
可以看到要让DMA转运外设的内容需要看这个DMA的通道是不是连接到该外设,如果没有连接,你进行配置是没办法触发的,所以要对应着来进行配置。
2.DMA的内部结构
这里还是借用人家的图,因为我画的不是很清晰,人家的还是很清晰:
这里可以看到有两个东西,一个是外设寄存器,外设寄存器主要是外部的设备,另一个是存储器,是在内部的,这个其实可以抽象成一个是A另一个是B,就是这个样子:
我们可以设置从A到B或者从B到A这个方向的,然后在A端和B端都可以设置三个东西,分别是起始地址、数据宽度、地址是否自增。
起始地址就是你需要转运的数据或者需要存储的空间它的地址是在哪里,在学指针的时候肯定说过,地址是一个变量在计算机中的实际位置,我们对这个地址所对应的空间赋值,那这个变量就得到这个值了。
然后数据宽度是指你这个需要传输的数据是什么类型的,是int还是char还是short,就是确定一个类型。
地址自增就是你给的这个地址,当一个数据传输过去了,它的地址会不会增加,可以自增也可以不自增。
再往下看,看到一个传输计数器,这个的功能是限制DMA传输的次数,当这个次数自减为0就结束这一次的数据传输,每次传输一个数据时这个寄存器中的值就会减一。
旁边的自动重装寄存器只有在硬件模式中可以用,当这个传输计数器为0后,自动重装寄存器就会把值再给传输计数器,这样就是一个循环执行,所以被称为硬件模式。
当然有硬件模式也就有软件模式,而软件模式是没有用这个自动重装寄存器的,因为软件模式是有软件进行控制的,当传输完成后软件会自动的对传输计数器进行赋值,然后再继续运行。
然后就是硬件选择和软件选择了,这里是由M2M进行控制的,硬件选择对应的就是上面的硬件模式,而软件选择对应的就是软件模式,这里就是使用M2M进行控制。
三、代码的编写
上面了解了内部结构后,我们就可以来进行软件编写了,这里分为两种,一个是数据转运,另一个是ADC多通道转运。
1.数据转运
实现来讲解一下数据转运,可能后面我需要DMA为我转运陀螺仪的信息,然后这里先介绍一下。
1.1 开启时钟
在使用stm32外设第一步都是要开启时钟,只有把时钟开启后下面的操作才会有意义。
上面提到了,DMA是挂载到AHB总线上的,所以我们要操作RCC_AHBPeriphClockCmd()
函数才可以打开AHB
总线上的外设,这里的代码如下:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1);
这里打开的是DMA1,也可以打开DMA2,代码如下:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2);
这里主要是取决于你使用的是那个DMA设备。
1.2 配置DMA结构体
DMA的配置和配置GPIO一样,首先是创建结构体,然后对结构体中的内容进行赋值,配置DMA的结构体如下:
typedef struct
{
uint32_t DMA_PeripheralBaseAddr; /*!< 指定 DMAy Channelx 的外设基址。 */
uint32_t DMA_MemoryBaseAddr; /*!< 指定 DMAy Channelx 的内存基址。 */
uint32_t DMA_DIR; /*!< 指定外围设备是源还是目标。
此参数的值可以是 @ref DMA_data_transfer_direction */
uint32_t DMA_BufferSize; /*!< 指定指定通道的缓冲区大小(以数据单位为单位)。
数据单元等于 DMA_PeripheralDataSize 中设置的配置
或根据转移方向DMA_MemoryDataSize成员。 */
uint32_t DMA_PeripheralInc; /*!< 指定外设地址寄存器是否递增。
此参数的值可以是 @ref DMA_peripheral_incremented_mode */
uint32_t DMA_MemoryInc; /*!< 指定内存地址寄存器是否递增。
此参数的值可以是 @ref DMA_memory_incremented_mode */
uint32_t DMA_PeripheralDataSize; /*!< 指定外围设备数据宽度。
此参数的值可以是 @ref DMA_peripheral_data_size */
uint32_t DMA_MemoryDataSize; /*!< 指定内存数据宽度。
此参数的值可以是 @ref DMA_memory_data_size */
uint32_t DMA_Mode; /*!< 指定 DMAy Channelx 的操作模式。
此参数的值可以是 @ref DMA_circular_normal_mode。
@note:如果内存到内存,则无法使用循环缓冲模式。
在选定的通道上配置数据传输 */
uint32_t DMA_Priority; /*!< 指定 DMAy Channelx 的软件优先级。
此参数的值可以是 @ref DMA_priority_level */
uint32_t DMA_M2M; /*!< 指定是否将 DMAy Channelx 用于内存到内存的传输。
此参数的值可以是 @ref DMA_memory_to_memory */
}DMA_InitTypeDef;
第一个参数DMA_PeripheralBaseAddr
是指定A端的地址。
第二个参数DMA_MemoryBaseAddr
是指定B端的地址。
第三个参数DMA_DIR
是指定传输方向,可以选下面这几个传输:
参数 | 介绍 |
---|---|
DMA_DIR_PeripheralDST | B到A |
DMA_DIR_PeripheralSRC | A到B |
第四个参数DMA_BufferSize
就是设置传输寄存器的,比如我这要传输10个数据,那这要填写10,也就是运行10次传输。
第五个参数DMA_PeripheralInc
是指A端的地址是否自增,如果这里填写自增,那传输一次,A端的地址就自增1,以此类推。
第六个参数DMA_MemoryInc
是指B端的地址是否自增,如果这里填写自增,那传输一次,B端的地址就自增1,以此类推。
第七个参数DMA_PeripheralDataSize
是指A端要传输的数据是什么类型的,可以选择字节8位、半字16位、字32位。
第八个参数DMA_MemoryDataSize
是指B端要传输的数据是什么类型的,可以选择字节8位、半字16位、字32位。
第九个参数DMA_Mode
是指是什么模式的,可以选择下面这两个参数:
参数 | 介绍 |
---|---|
DMA_Mode_Circular | 循环模式,只有在硬件触发才可以选择 |
DMA_Mode_Normal | 正常模式,没有自动重装寄存器 |
第十个参数DMA_Priority
是选择传输的优先级的。
第十一个参数DMA_M2M
用来指定传输的方式,下面是参数介绍:
参数 | 介绍 |
---|---|
DMA_M2M_Enable | 存储器到存储器 |
DMA_M2M_Disable | 外设到存储器 |
其实就是选择是软件模式还是硬件模式,如果选择DMA_M2M_Disable
就是硬件模式,如果选择DMA_M2M_Enable
就是软件模式。
这样我们就可以写一个初始化的代码了,这里是要数据的转运,而这个数据是在代码中定义的变量,在代码中定义的变量其实存放在存储器中的,所以这里是存储器到存储器,那么初始化代码就是这样的:
DMA_InitTypeDef DMA_InitStruct = {0};
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&arr_A; // A端地址 arr_A是一个长为10的字节数组,这里是把arr_A的地址存放进去
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // A端数据长度 读取的是字节也就是8位
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable; // A端地址是否自增 这里的数据寄存器需要自增地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&arr_B; // B端地址 从A端转移过来的数据存放位置的地址,这里是arr_B的地址,arr_B是一个长为10的字节数组
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // B端数据长度 读取的是字节也就是8位
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // B端地址是否自增 这里需要地址进行自增
DMA_InitStruct.DMA_BufferSize = 10; // 传输计数器的值
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; // 模式选择,这里选择正常模式
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向,A作为源端B作为目标端
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable; // 传输是存储器到存储器还是外设到存储器
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; // DMA转换的转换优先级
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
这样就初始化完成了,有很多地方大家需要注意一下,在填写A端和B端的字节的时候要区分前面的名称,我在写DMA转换ADC数据的时候就把B端的字节搞错了,我写成DMA_PeripheralDataSize_HalfWord
而这里得填写DMA_MemoryDataSize_HalfWord
,就因为这里的数据填写错误,导致读取的值不对。
1.3 开始转换
到这里大家可能就像直接使用DMA_Cmd
进行使能后运行DMA了,但是这里我们设置的是正常模式,当一次转换后计数器中的值就为0了,再进行转换是转换不了的,所以我们要用软件手动的为计数器中添加值。
当添加值后就可以继续运行了,那这里是不是可以直接用DMA_SetCurrDataCounter
函数进行设置值呢?
是不行的,stm32中规定,要软件为计数器中添加值之前是需要将DMA失能的,在失能后才能修改计数器中的值,所以这里我们要将DMA进行失能,然后修改后使能,这样DMA才可以继续运行,所以这里的运行代码为:
DMA_Cmd(DMA1_Channel1, DISABLE); // 失能DMA1的通道1
DMA_SetCurrDataCounter(10); // 修改计数器的值
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA1的通道1
这个运行函数可以写一个函数进行封装,当要进行转换的时候进行一次调用即可。
当然这样还不是很完善,我们让它运行后怎么知道运行完成了,我们好查看数据呢?
这里当传输完成后会置一个标志位,我们可以通过这个标志位来进行判断是否转换完成,这里的标志位有点多,每个通道都有一个对应的标志位,而且在每个过程中都有一个标志位的产生,这里就将过程中的标志位写出来,然后DMA编号和通道就用x和y表示:
标志位 | 解释 |
---|---|
DMAx_FLAG_GLy | DMA全局标志位 |
DMAx_FLAG_TCy | 传输完成标志位 |
DMAx_FLAG_HTy | 传输过半标志位 |
DMAx_FLAG_TEy | 传输错误标志位 |
然后用DMA_GetFlagStatus
函数判断这个标志位是否产生,当产生后就可以读取数据了,然后再手动清除一下这个标志位,使用DMA_ClearFlag
函数即可清除,该进后的代码如下:
DMA_Cmd(DMA1_Channel1, DISABLE); // 失能DMA1的通道1
DMA_SetCurrDataCounter(10); // 修改计数器的值
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA1的通道1
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1); // 手动清除标志位
这样的代码就比较完整了,然后传输完成后就访问arr_B
即可,就是数组的引用了,这个都比较简单。
2.ADC单通道转换
在ADC中也是一样的道理,首先配置ADC然后GPIO空,然后配置DMA即可。
2.1 开启时钟
首先这开启时钟,这里就把全部的时钟都进行打开,分别打开GPIO口、ADC和DMA的时钟,代码如下:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Perph_ADC1, ENABLE); // 开启ADC1的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 开启DMA的时钟
2.2 配置GPIO和ADC
这里是前面讲过的,所以这里直接就上代码即可:
ADC_InitTypeDef ADC_InitStruct = {0};
GPIO_InitTypeDef GPIO_InitStruct = {0};
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);
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 不连续转换
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 单独模式
ADC_InitStruct.ADC_NbrOfChannel = 1; // 通道数
ADC_InitStruct.ADC_ScanConvMode = DISABLE; // 不连续扫描
ADC_Init(ADC1, &ADC_InitStruct);
上面的配置是单通道,不连续转换的ADC。
2.3 配置DMA
接下来就是要配置DMA了,在这ADC是外设,所以在配置DMA的模式就要选择外设到存储器了,模式的话因为上面是软件触发非连续模式,这里因为选择了外设到存储器,所以需要注意一下哪一个DMA中的通道是连接到ADC1上,这边查看了一下是在DMA1中的通道1是连接到ADC1上的,代码如下:
DMA_InitTypeDef DMA_InitStruct = {0};
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // A端地址 ADC数据寄存器的地址
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // A端数据长度 读取的是半字也就是16位
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // A端地址是否自增 这里的数据寄存器不需要自增地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&adc_read_value; // B端地址 用一个数组来存放ADC读取的结果
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // B端数据长度 读取的是半字也就是16位
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // B端地址是否自增 这里需要地址进行自增
DMA_InitStruct.DMA_BufferSize = 1; // 传输计数器的值
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; // 模式选择,这里选择正常模式
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向,A到B
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; // 传输是外设到存储器
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; // DMA转换的转换优先级
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
这里因为选择了DMA_Mode_Normal
正常模式,所以没有自动重装寄存器,所以在每次需要使DMA运行的时候就要调用一下DMA_SetCurrDataCounter
为其重装值,这样DMA才可以再次运行。
2.4 使能ADC
配置完成后就可以使能ADC来和DMA了,然后再调用ADC的DMA使能,让DMA接收到ADC转换完成信号后开始转移数据。
这里先用ADC_Cmd
对ADC进行使能,然后用ADC_DMACmd
对ADC触发的DMA进行使能,然后使能或者失能DMA,这里一般是配置完成后先不启用DMA,等后面专门写一个DMA运行函数配置好计数器中的值后再使能。
ADC_Cmd(ADC1, ENABLE); // 使能ADC1
ADC_DMACmd(ADC1, ENABLE); // 使能ADC1的DMA
DMA_Cmd(DMA1_Channel1, DISABLE); // 先失能
2.5 DMA转移函数
这里就单独将DMA的运行拿出来,因为正常模式也就是软件模式下,DMA在进行一次转移后(计数器中的值为0)不会自动为计数器进行赋值的,需要软件手动的为计数器进行赋值。
并且这里的ADC是使用单独模式并且不是循环模式,需要软件进行触发才能进行一次ADC转换。
所以这要把这一步单独拿出来,这样使用DMA就很方便,代码如下:
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 添加需要采集的ADC到组中
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, 1); // 为计数器重装值
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发ADC转换
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1); // 手动清除标志位
这里当ADC转换完成后我们不用程序来进行读取,而是由DMA来进行读取,当转换完成后ADC会发送一个信号给DMA,这个是需要在上面使能ADC的DMA才可以让DMA接收这个信号的
3.ADC多通道转换
其实上面了解了单通道后,这里可以在此基础上进行改写,改写的方法也是很简单的,只不过就是在ADC_InitStruct.ADC_ContinuousConvMode
中给连续转换,然后在ADC_InitStruct.ADC_NbrOfChannel
填写要转换的通道数,最后改一下DMA的计数器的值即可,完整代码如下:
void Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Perph_ADC1, ENABLE); // 开启ADC1的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 开启DMA的时钟
ADC_InitTypeDef ADC_InitStruct = {0};
GPIO_InitTypeDef GPIO_InitStruct = {0};
DMA_InitTypeDef DMA_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 单独模式
ADC_InitStruct.ADC_NbrOfChannel = 2; // 通道数
ADC_InitStruct.ADC_ScanConvMode = DISABLE; // 不连续扫描
ADC_Init(ADC1, &ADC_InitStruct);
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // A端地址 ADC数据寄存器的地址
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // A端数据长度 读取的是半字也就是16位
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // A端地址是否自增 这里的数据寄存器不需要自增地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&adc_read_value; // B端地址 用一个数组来存放ADC读取的结果
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // B端数据长度 读取的是半字也就是16位
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // B端地址是否自增 这里需要地址进行自增
DMA_InitStruct.DMA_BufferSize = 2; // 传输计数器的值
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; // 模式选择,这里选择正常模式
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向,A到B
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; // 传输是外设到存储器
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; // DMA转换的转换优先级
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 添加需要采集的ADC到组中
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); // 添加需要采集的ADC到组中
ADC_Cmd(ADC1, ENABLE); // 使能ADC1
ADC_DMACmd(ADC1, ENABLE); // 使能ADC1的DMA
DMA_Cmd(DMA1_Channel1, DISABLE); // 先失能
}
void Start_DMA(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, 2); // 为计数器重装值
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发ADC转换
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1); // 手动清除标志位
}
在调用初始化完成后需要进行ADC转换并且使用DMA转移就可以调用上面的Start_DMA
函数进行。
4.ADC连续转换
上面的代码有点不智能,需要软件进行一次触发才能进行一次ADC转换并且还需要手动的为DMA计数器手动赋值,多多少少还是有软件控制的,而这里介绍一下,全过程都由硬件执行。
实现的方法很简单,让ADC配置为连续扫描模式,然后DMA配置为循环模式,然后配置完成后直接使能DMA并且来一次软件触发ADC,这样就可以让ADC连续的转换并且DMA不需要手动赋值就可以一直转换了。
代码如下:
void Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Perph_ADC1, ENABLE); // 开启ADC1的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 开启DMA的时钟
ADC_InitTypeDef ADC_InitStruct = {0};
GPIO_InitTypeDef GPIO_InitStruct = {0};
DMA_InitTypeDef DMA_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 单独模式
ADC_InitStruct.ADC_NbrOfChannel = 2; // 通道数
ADC_InitStruct.ADC_ScanConvMode = ENABLE; // 连续扫描
ADC_Init(ADC1, &ADC_InitStruct);
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // A端地址 ADC数据寄存器的地址
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // A端数据长度 读取的是半字也就是16位
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // A端地址是否自增 这里的数据寄存器不需要自增地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&adc_read_value; // B端地址 用一个数组来存放ADC读取的结果
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // B端数据长度 读取的是半字也就是16位
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // B端地址是否自增 这里需要地址进行自增
DMA_InitStruct.DMA_BufferSize = 2; // 传输计数器的值
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; // 模式选择,这里选择正常模式
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向,A到B
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; // 传输是外设到存储器
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; // DMA转换的转换优先级
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 添加需要采集的ADC到组中
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); // 添加需要采集的ADC到组中
ADC_Cmd(ADC1, ENABLE); // 使能ADC1
ADC_DMACmd(ADC1, ENABLE); // 使能ADC1的DMA
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发ADC转换
}
这样就可以让ADC连续转换多通道的ADC通道,并且可以让DMA自动进行数据转运不用再手动为其赋值,一直运行。
总结
使用DMA可以大量节约CPU的资源,对于大量的数据传递会消耗CPU大量的时间,在转移过程中就没办法执行其他功能,所以DMA对于大量数据转运有着很大的帮助,并且节约了大量的时间。