09 DMA配合ADC多通道

[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通道连接的外设:

img

下面是DMA2通道连接的外设:

img

可以看到要让DMA转运外设的内容需要看这个DMA的通道是不是连接到该外设,如果没有连接,你进行配置是没办法触发的,所以要对应着来进行配置。

2.DMA的内部结构

这里还是借用人家的图,因为我画的不是很清晰,人家的还是很清晰:

img

这里可以看到有两个东西,一个是外设寄存器,外设寄存器主要是外部的设备,另一个是存储器,是在内部的,这个其实可以抽象成一个是A另一个是B,就是这个样子:

img

我们可以设置从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_PeripheralDSTB到A
DMA_DIR_PeripheralSRCA到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_GLyDMA全局标志位
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对于大量数据转运有着很大的帮助,并且节约了大量的时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恰柠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值