09 DMA配合ADC多通道

news2024/11/15 10:14:39

[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对于大量数据转运有着很大的帮助,并且节约了大量的时间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1995053.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从分散到集中:TSINGSEE青犀EasyCVR视频汇聚网关在视频整体监控解决方案中的整合作用

边缘计算视频汇聚网关是基于开放式、大融合、全兼容、标准化的设计架构理念&#xff0c;依据《安全防范视频监控联网系统信息传输、交换、控制技术要求》&#xff08;GB/T28181-2011&#xff09;标准开发&#xff0c;集流媒体转发、视频编码、视频管理、标准通信协议、网络穿透…

【MAUI】系统主题方案

文章目录 概述具体AppThemeBindingResourceDictionaryApplication.Current.Resources.MergedDictionariesDynamicResource 来源 概述 主要有两种&#xff1a;AppThemeBinding 和ResourceDictionaryApplication.Current.Resources.MergedDictionariesDynamicResource 具体 Ap…

视频美颜SDK的核心技术与直播美颜插件的开发详解

本篇文章&#xff0c;小编将深入探讨视频美颜SDK的核心技术以及如何开发高效的直播美颜插件。 一、视频美颜SDK的核心技术 视频美颜SDK的核心在于其实时图像处理能力&#xff0c;它通过一系列复杂的算法&#xff0c;实现对视频图像的增强和优化。以下是几项关键技术&#xff…

【产品推荐】高性能隔离接口芯片——CMT83085

产品概述 CMT83085是华普微精心打造的一款高可靠性隔离接口芯片&#xff0c;它集成了先进的数字隔离技术和RS-485通信接口&#xff0c;即基于数字隔离技术的高可靠性半双工 RS-485 收发器&#xff0c;专为需要高安全性和长距离数据传输的应用场景设计。 该芯片不仅具备出色的…

鸿蒙开发5.0【应用异常处理】运维

应用异常处理 介绍 本示例介绍了通过应用事件打点hiAppEvent获取上一次应用异常信息的方法&#xff0c;主要分为应用崩溃、应用卡死两种。 效果图预览 使用说明 点击构建应用崩溃事件&#xff0c;3s之后应用退出&#xff0c;然后打开应用进入应用异常页面&#xff0c;隔1mi…

Java学习笔记(二十):反射、动态代理、日志、类加载器、xml、单元测试Junit、注解

目录 一、反射 1.1 反射的概述&#xff1a; 1.2 学习反射到底学什么&#xff1f; 1.3 获取字节码文件对象的三种方式 1.4 字节码文件和字节码文件对象 1.5 获取构造方法 1.6 获取构造方法并创建对象 1.7 获取成员变量 1.8 获取成员变量并获取值和修改值 1.9 获取成员…

002集——C#基本语法——C#学习笔记

C# 是一种面向对象的编程语言。在面向对象的程序设计方法中&#xff0c;程序由各种相互交互的对象组成。相同种类的对象通常具有相同的类型&#xff0c;或者说&#xff0c;是在相同的 class 中。 例如&#xff0c;以 Rectangle&#xff08;矩形&#xff09;对象为例。它具有 le…

一文读懂如何选择视频孪生三维建模方式及建模精度等级

导言/INTRODUCTION 三维模型是视频孪生应用的基础&#xff0c;建模方式与模型精度将直接影响到最终孪生场景的呈现和应用效果。各种建模方式和模型精度在成本、场景还原真实度、实施周期方面都有自己的特点&#xff0c;因而有着各自的优劣势和适用场景&#xff0c;同一场景可能…

基于Hadoop的国内手机销售大数据分析与可视化研究【百万数据集】

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主项目介绍 绪论研究背景研究目的研究意义 相关技术理论介绍Hadoop相关理论HIve数据仓库flume组件介绍sqoop组件介绍Pyecharts介绍 数据来源及处理数据介绍数据预处理 Hadoop集群搭建Hadoop全…

安装python+python的基础语法

安装python python2为内置&#xff0c;安装python3----3.6.8 最新安装3.12使用源码安装 1.查看yum源&#xff0c;epel [rootpython01 ~]# yum list installed |grep epel 2.安装python3 [rootpython01 ~]# yum -y install python3 3.查看版本 [rootpython01 ~]# python…

计算机硬件 课程导读

目录 一、老师介绍 二、课程目标 三、课程大纲 一、老师介绍 学问小小谢 我是一个热爱分享知识的人&#xff0c;我深信知识的力量能够启迪思考&#xff0c;丰富生活。 欢迎每一位对知识有渴望的朋友&#xff0c;如果你对我的创作感兴趣&#xff0c;或者我们有着共同的兴趣点&…

vue3组件之间通讯

1. props&#xff0c;实现父组件向子组件通讯。 父组件 &#xff1a;对子组件属性绑定要通讯的数据。 子组件&#xff1a;通过defineProps来进行数据接收。 2. emit&#xff0c;实现子组件向父组件通讯。 子组件&#xff1a;通过emit创建向父组件传递数据事件 父组件&#…

Chromium编译指南2024 - Android篇:环境准备(二)

1.引言 在前面的章节中&#xff0c;我们详细介绍了编译 Chromium for Android 所需的系统和硬件要求。在确保您的开发环境符合这些基本要求后&#xff0c;接下来我们将重点讲解如何具体配置您的开发环境&#xff0c;以便顺利编译 Chromium。本篇文章将涵盖从更改软件源到安装基…

[matlab] 鲸鱼优化算法优化KNN分类器的特征选择

目录 引言 智能优化算法概述 智能优化算法在KNN特征选择中的应用 应用步骤 UCI数据集 鲸鱼优化算法 一、算法背景与原理 二、算法组成与步骤 三、算法特点与优势 四、应用与挑战 代码实现 鲸鱼优化算法 主程序 打印结果 引言 智能优化算法在优化KNN&#xff08;…

5、关于kali搭建vulhub

Vulhub是一个基于Docker和Docker-compose的漏洞靶场环境&#xff0c;所以搭建vulhub分三步&#xff1a; 1、安装docker 2、安装docker-compose 3、安装vulhub 一、安装步骤 1、安装docker 因为kali太久没用&#xff0c;所以需要先更新软件列表最新源 apt-get update 安装do…

C++ vector的基本使用

目录 1. vector的定义 2. 迭代器iterator的使用 3. vector空间增长问题 (1). size与capacity (2). empty与resize与reserve 4. vector的增删查改 (1) . push_back和pop_back (2). find与insert与erase (3). swap与operator[] 5. vector迭代器失效问题 (1). 改变空间 (…

爱玛电动车今年多次抽查不合格:营收增速放缓承压,拟50亿扩产能

《港湾商业观察》廖紫雯 7月26日&#xff0c;市场监管总局发布《2024年上半年电动自行车产品质量国家监督专项抽查结果情况通报》&#xff0c;爱玛电动车五次上榜。 除却一直以来被多次诟病的电动车产品质量问题外&#xff0c;业绩层面上&#xff0c;近两年数据来看&#xff…

从格斗项目的着装和格斗术,理解巴黎奥运会上的拳击、跆拳道、柔道、摔跤之间到底有什么区别?

文章目录 引言I 柔道着装格斗术II 摔跤装备“摔跤耳”格斗术:古典式摔跤和自由式摔跤III 跆拳道装备格斗术等级段位制(“十级九段制”)IV 拳击装备格斗术拳击手小结引言 8月1日巴黎,柔道女子-78公斤级比赛结束,意大利贝兰迪夺得金牌,以色列拉尼尔摘得银牌,中国选手马振昭和…

SAM-Med2D 大模型学习笔记(续):训练自己数据集

1、前言、数据集介绍 SAM-Med2D大模型介绍参考上文&#xff1a;第三章&#xff1a;SAM-Med2D大模型复现-CSDN博客 本文将使用SAM-Med2D大模型训练自己的数据集 关于SAM-Med2D大模型官方demo数据集的介绍上文已经介绍过&#xff0c;这里简单回顾下 其中data_demo为数据集的目…

你的工作环境,选对劳保鞋了吗?守护安全,从脚下开始!

在众多的工作场所中&#xff0c;我们穿梭于不同的工作环境&#xff0c;从繁忙的工厂车间到复杂的建筑工地&#xff0c;再到需要精细操作的实验室……每一步都承载着对安全的期许和对效率的追求。但你是否意识到&#xff0c;脚下那双不起眼的劳保鞋&#xff0c;其实是守护你安全…