STM32-ADC(独立模式、双重模式)+DMA读取数据+部分基础知识

一、ADC基础知识部分

(一)ADC简介

  • ADC是把外部模拟电压通过ADC转换器转化为数字形式
  • STM32f103 系列
    1、有 3 个独立的 ADC(1/2/3)
    2、精度(分辨率)为 12 位
    3、每个 ADC有 18 个通道,其中有16个外部通道(不同引脚数的STM32外部通道数可能有所差异),其余两个是内部通道

(二)ADC的功能框图

  • 功能框图

    可以分为输入电压,输入通道,转换顺序,触发源,转换时间数据寄存器,中断七部分。
    在这里插入图片描述

  • 电压输入范围

    决定输入电压的引脚:VREF-,VREF+,VSSA,VDDA
    输入电压:VREF-<=VIN<=VREF+
    其中VREF-和VREF+是单片机的GPIO引脚
    功能框图
    在这里插入图片描述
    VSSA是模拟地,VDDA是模拟电源
    VSSA,VREF-接地,VDDA,VREF+接3.3V
    ADC是测量外部的模拟量:ADC的电压输入范围是0-3.3V

  • 模拟量计算

    在这里插入图片描述

  • ADC可测电压范围变大

    如果我们想让输入的电压范围变宽,去到可以测试负电压或者更高的正电压,我们可以在外部加一个电压调理电路,把需要转换的电压抬升或者降压到 0~3.3V,这样 ADC 就可以测量。

    例如:能够测量-10V-10V电压在这里插入图片描述

  • ADC通道引脚图(重要)
    在这里插入图片描述

    ADC1/ADC2通道对应的引脚
    在这里插入图片描述
    所有通道里面最多可以设置16个规则通道,最多设置4个注入通道(是从16个通道里面任意挑选四个即可)

在这里插入图片描述

  • 通道类型

    1、规则通道:就是很遵守规则的意思,一般用这种

    2、注入通道:插队的意思(类似于中断),是一种规则通道在转换时强行插入要转换(所以插入通道只有在规则通道存在时才会出现)

  • 通道转换顺序

    1、规则序列
    在这里插入图片描述
    SQ3决定通道1-6的转换顺序
    SQ2决定通道7-12个的转换顺序
    SQ1决定通道13-16个的转换顺序,最后还可以设置需要转换多少个通道
    需要使用多少通道,通道的转换顺序如何,是通过规则序列寄存器设置的

    2、注入序列
    **注入序列图**

  • 触发源

    在这里插入图片描述
    1、软件触发
    ADC_CR2:控制寄存器2
    SWSTART:控制规则通道
    JSSWSTART:控制注入通道
    2、外部事件触发(内部定时器/外部IO)
    ADC_CR2:控制寄存器2
    选择触发源EXTSE[2:0]/JEXTSEL
    控制开关(激活)EXTEN/JEXTEN

  • 转换时间

    PCLK2一般设置72M
    在这里插入图片描述

  • 采样时间
    在这里插入图片描述
    在这里插入图片描述

  • 数据寄存器

    一切准备就绪后, ADC 转换后的数据根据转换组的不同,规则组的数据放在 ADC_DR 寄存器,注入组的数据放在 JDRx。

    1、规则数据寄存器(ADC_DR-32位)
    在这里插入图片描述

    ADC_DR寄存器独立模式下只使用低16位
    ADC_DR寄存器双重模式下才会用到高16位

    ADC的数据寄存器ADC_DR如何使用?

    ADC 规则组数据寄存器 ADC_DR 只有一个,是一个 32 位的寄存器。

    低16位只用于存放独立模式转换完成数据(例如只使用ADC1或ADC2或ADC3)

    高16位双重模式时使用的(假若使用ADC1和ADC2同时采集数据,则ADC1的数据放到低16位,ADC2的数据放到高16位)

    为什么使用DMA读取数据?

    规则通道可以有 16 个这么多,可规则数据寄存器只有一个,如果使用多通道转换,那转换的数据就全部都挤在了 DR 里面,前一个时间点转换的通道数据,就会被下一个时间点的另外一个通道转换的数据覆盖掉,所以当通道转换完成后就应该把数据取走,或者开启 DMA 模式,把数据传输到内存里面,不然就会造成数据的覆盖。最常用的做法就是开启 DMA 传输。
    使用DMA读取数据:
    多通道采集会出现数据覆盖的现象,可以使用DMA进行数据传输
    在这里插入图片描述
    假若同时有三个通道进行采集数据:(使用DMA很好的解决了数据的传输问题,不会出现覆盖现象)

    第一个通道采集完数据放到DR寄存器中,通过DMA迅速把数据搬移到16位数组的a[0];

    第二个通道采集完数据放到DR寄存器中,覆盖了第一个通道的数据,通过DMA迅速把数据搬移到16位数组的a[1];

    第三个通道采集完数据放到DR寄存器中,覆盖了第二个通道存放的数据,通过DMA迅速把数据搬移到16位数组的a[3];

    如果使用DMA,则ADC1和ADC3可以(ADC2不具备DMA功能)

    2、注入数据寄存器(ADC_JDRx)
    在这里插入图片描述


ADC 注入数据寄存器 (ADC_JDRx)低16位有效
由于注入通道有四个,所以有四个这样的寄存器

  • 中断
    在这里插入图片描述
    EOC:规则通道转换完成标志位
    JEOC:注入通道转换完成标志位
    AWD:时模拟看门狗

二、结构体和固件库函数

(一)ADC初始化结构体

ADC初始化结构体
在这里插入图片描述

  • 工作模式

    1、同步规则模式(同时采集一个或多个通道)
    ADC1和ADC2同时转换一个规则通道组,其中ADC1是主,ADC2是从。ADC1转换的结果放在ADC1_DR的低16位,ADC2的转换结果放在ADC1_DR的高16位
    同时转换含义:是指ADC1和ADC2的转换进度一样,当采集结束放到ADC1_DR寄存器之后通过DMA传到SRAM
    在这里插入图片描述
    ADC1和ADC2也可以采集两个,即各自采集完自己的通道1之后,可以再去采集通道2

    2、快速交叉模式:

    ADC1和ADC2交替采集一个规则通道(通常只为一个通道),当ADC2触发之后,ADC1需要等待7个ADCCLK之后才能触发当ADC1采集完去转换的时候,ADC2又去采集了,ADC2转换的时候,ADC1又去采集了(交叉采集)

    3、慢速交叉模式:

    ADC1和ADC2交替采集一个规则通道(通常只为一个通道),当ADC2触发之后,ADC1需要等待14个ADCCLK之后才能触发

    4、交替触发模式:

    ADC1和ADC2轮流采集注入通道,当ADC1的所有通道采集完毕之后,再去采集ADC2的通道,如此循环下去

  • 扫描模式(ADC_CR1–SCAN)

    主要针对多通道,如果多个通道,先扫描通道1,再去扫描通道2,再去扫描通道3;单通道的话不需要开启扫描模式

  • 连续转换模式(ADC_CR2–CON)

    采集完一次之后继续采集,一直采集下去

  • 外部触发选择(ADC_CR2)

  • 数据对齐(ADC_CR2)

    一般右对齐

  • 转换多少个通道

    根据实际的转换通道个数填写

(二)ADC常用固件库函数

  • 初始化函数

    ADC_Init();

  • ADC时钟配置(ADCCLK)主要是设置几分频–决定了采样时间和转换时间

    RCC_ADCCLKConfig();

    ADC_RegularChannelConfig();

  • ADC使能

    ADC_Cmd();

  • ADC软件触发

    ADC_SoftwareStartConvCmd();

  • ADC外部触发

    ADC_ExternalTrigConvCmd();

  • 决定配置哪个ADC,在ADC转换完毕之后是否去开启DMA(多通道采集数据为防止数据被覆盖,所以要开启DMA)

    ADC_DMACmd();

三、ADC读取电压值

(一)独立模式-单通道-中断读取

单通道采集,实现开发板上电位器电压的采集并通过串口打印至 PC 端串口调试助手。单通道采集使用AD 转换完成中断,在中断服务函数中读取数据,不使用 DMA 传输,在多通道采集时才使用 DMA 传输。

  • 编程要点:

    1-初始化ADC用到的GPIO
    2-初始化ADC初始化结构体
    3-配置ADC时钟,配置通道的转换顺序和采样时间
    4-使能ADC转换完成中断,配置中断优先级
    5-使能ADC,准备开始转换
    6-校准ADC
    7-软件触发ADC,真正开始转换
    8-编写中断服务函数,读取ADC转换数据
    9-编写main函数,把转换的数据打印出来

  • 使能引脚时钟和ADC1时钟

    使用ADC1的通道1,其引脚是PA1(因此要使能GPIOA的时钟和ADC1的时钟)

     //1、使能时钟
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1 ,ENABLE);
    
  • ADC1通道1对应的引脚初始化

    PA1配置为模拟输入

     //2、GPIO引脚初始化
     GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1;//ADC1的通道1是PA1
     GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
     GPIO_Init(GPIOA, &GPIO_InitStruct);
    
  • ADC初始化函数配置

    ADC 工作参数具体配置为:只使用一个ADC1,设置为独立模式、单通道采集不需要扫描、启动连续转换、使用内部软件触发无需外部触发事件、使用右对齐数据格式、由于只有一个通道1,所以转换通道为 1个

     //3、ADC初始化
     ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;//独立模式
     ADC_InitStruct.ADC_ScanConvMode=DISABLE;//单通道的话不需要开启扫描模式
     ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;//开启连续转换
     ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;//数据右对齐
     ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//不选择外部触发
     ADC_InitStruct.ADC_NbrOfChannel=1;//要转换的通道数目(单通道只有一个)
     ADC_Init( ADC1, &ADC_InitStruct);
    
  • 配置ADC工作时钟

    RCC_ADCCLKConfig() 函数用来配置 ADC 的工作时钟,接收一个参数,设置的是 PCLK2 的分频
    系数, ADC 的时钟最大不能超过 14M。

     //4、配置ADC时钟
     RCC_ADCCLKConfig(RCC_PCLK2_Div8);//设置为8分频,则ADC的时钟是9M
    
  • 配置转换顺序和采样时间

    由于只有一个通道,所以转换顺序配置为1即可

     //5、配置转换顺序和采样时间
     //参数1:哪一个ADC;参数2:通道;参数3:转换顺序(此时只有一个配置为1);参数4:采样时间
     ADC_RegularChannelConfig(ADC1, ADC_Channel_1,1,ADC_SampleTime_55Cycles5);
    
  • 中断优先级配置

    配置中断源和中断优先级

     //6、中断优先级配置
     NVIC_InitStruct.NVIC_IRQChannel=ADC1_2_IRQn;
     NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
     NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
     NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
     NVIC_Init(&NVIC_InitStruct);
    
  • 使能ADC中断

     //6、使能ADC中断
     //ADC_IT_EOC-规则通道 ADC_IT_JEOC-注入通道中断,ADC_IT_AWD-看门狗
     ADC_ITConfig( ADC1, ADC_IT_EOC, ENABLE);//使能规则通道中断
    
  • 使能ADC转换、校准,软件触发ADC转换

     //7、使能ADC开始转换
      ADC_Cmd(ADC1, ENABLE);
      
      //8、开始校准ADC
      ADC_StartCalibration(ADC1);//校准ADC
     	
     //等待校准完毕
     while(ADC_GetCalibrationStatus(ADC1));
     	
     //9、软件触发ADC开始转换
     ADC_SoftwareStartConvCmd(ADC1,ENABLE);
    
  • ADC初始化函数总体代码:

     u16 ADC_ConValue;//ADC采集的数值
     //adc初始化
     void ADCx_Init(void)
     {
     	//gpio初始化结构体
     	GPIO_InitTypeDef GPIO_InitStruct;
     	//adc初始化结构体
     	ADC_InitTypeDef ADC_InitStruct;
     	//中断结构体
     	NVIC_InitTypeDef NVIC_InitStruct;
     	//1、使能引脚时钟和ADC1时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1 ,ENABLE);
     	
     	
     	//2、GPIO引脚初始化
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1;//ADC1的通道1是PA1
     	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
     	GPIO_Init(GPIOA, &GPIO_InitStruct);
     	
     	//3、ADC初始化
     	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;//独立模式
     	ADC_InitStruct.ADC_ScanConvMode=DISABLE;//单通道的话不需要开启扫描模式
     	ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;//开启连续转换
     	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;//数据右对齐
     	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//不选择外部触发
     	ADC_InitStruct.ADC_NbrOfChannel=1;//要转换的通道数目(单通道只有一个)
     	ADC_Init( ADC1, &ADC_InitStruct);
     
     
     	//4、配置ADC时钟
     	RCC_ADCCLKConfig(RCC_PCLK2_Div8);//设置为8分频,则ADC的时钟是9M
     	
     	//5、配置转换顺序和采样时间
     	//参数1:哪一个ADC;参数2:通道;参数3:转换顺序(此时只有一个配置为1);参数4:采样时间
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_1,1,ADC_SampleTime_55Cycles5);
     
     
     	//6、中断优先级配置
     	NVIC_InitStruct.NVIC_IRQChannel=ADC1_2_IRQn;
     	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
     	NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
     	NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
     	NVIC_Init(&NVIC_InitStruct);
     	
     	//6、使能ADC中断
     	//ADC_IT_EOC-规则通道 ADC_IT_JEOC-注入通道中断,ADC_IT_AWD-看门狗
     	ADC_ITConfig( ADC1, ADC_IT_EOC, ENABLE);//使能规则通道中断
     	
     	//7、使能ADC开始转换
     	 ADC_Cmd(ADC1, ENABLE);
     	 
     	 //8、开始校准ADC
     	 ADC_StartCalibration(ADC1);//校准ADC
     		
     	//等待校准完毕
     	while(ADC_GetCalibrationStatus(ADC1));
     		
     	//9、软件触发ADC开始转换
     	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
     		 
     }
    
  • 编写中断服务函数,读取ADC数据

    我们使能了 ADC 转换完成中断,在 ADC 转换完成后就会进入中断服务函数,我们在中断服务函数内直接读取 ADC 转换结果保存在变量 ADC_ConValue( ADC_ConValue变量在前面定义的16位变量)

     void ADC1_2_IRQHandler (void)//在启动头文件中找到
     {
     	//判断中断是否真的来了(初始化函数使能的便是ADC_IT_EOC中断))
     	if(ADC_GetITStatus( ADC1, ADC_IT_EOC)!=RESET)
     	{
     		//中断真的来了就要读取转换值了
     		 ADC_ConValue=ADC_GetConversionValue(ADC1);//读取转换的值
     		
     	}
     	ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);//清除中断
     }	
    
  • 主函数文件内容

     float ADC_ConverValuelocal;//存放转换的数值
     int main(void)
     {	
     
     	delay_init();	    	 //延时函数初始化	  
     	NVIC_Configuration(); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
     	uart_init(9600);	 //串口初始化为9600
      	LED_Init();			     //LED端口初始化
     	KEY_Init();          //初始化与按键连接的硬件接口
     	ADCx_Init();
     	printf("ADC_TEST\r\n");
     	while(1)
     	{
     			ADC_ConverValuelocal=(float)ADC_ConValue/4096*3.3;
     			printf("The current AD_Value=0x%04x\r\n",ADC_ConValue);//打印采集到的数据(十六进制数据打印)
     			printf("The current AD_Value=%f V\r\n",ADC_ConverValuelocal);//打印转换后的电压值
     			printf("\r\n");
     			delay_ms(1500);
     	}
     }
    
  • 实验验证:

    PA1默认情况下电压值、PA1引脚接3.3V时的电压值、PA1接GND时电压值
    在这里插入图片描述

(二)独立模式-单通道-DMA读取

在上部分代码上做修改:(一)独立模式-单通道-中断读取

  • 思路

    ADC 转换结果数据使用 DMA 方式传输至指定的存储区,这样取代单通道实验使用中断服务的读取方法。

    由于使用DMA读取,所以和中断相关的就可以删除了,中断优先级配置,中断服务函数都不需要了。

  • void ADCx_Init(void)需要删除的代码部分

    中断优先级配置代码,使能ADC中断代码(删除内容如下)

     //6、中断优先级配置
     NVIC_InitStruct.NVIC_IRQChannel=ADC1_2_IRQn;
     NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
     NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
     NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
     NVIC_Init(&NVIC_InitStruct);
     
     //6、使能ADC中断
     //ADC_IT_EOC-规则通道 ADC_IT_JEOC-注入通道中断,ADC_IT_AWD-看门狗
     ADC_ITConfig( ADC1, ADC_IT_EOC, ENABLE);//使能规则通道中断
    

    void ADCx_Init(void)函数删减后代码如下

     u16 ADC_ConValue;//ADC采集的数值
     
     //adc初始化
     void ADCx_Init(void)
     {
     	//gpio初始化结构体
     	GPIO_InitTypeDef GPIO_InitStruct;
     	//adc初始化结构体
     	ADC_InitTypeDef ADC_InitStruct;
     //	//中断结构体
     //	NVIC_InitTypeDef NVIC_InitStruct;
     	//1、使能引脚时钟和ADC1时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1 ,ENABLE);
     	
     	
     	//2、GPIO引脚初始化
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1;
     	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
     	GPIO_Init(GPIOA, &GPIO_InitStruct);
     	
     	//3、ADC初始化
     	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;//独立模式
     	ADC_InitStruct.ADC_ScanConvMode=DISABLE;//单通道的话不需要开启扫描模式
     	ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;//开启连续转换
     	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;//数据右对齐
     	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//不选择外部触发
     	ADC_InitStruct.ADC_NbrOfChannel=1;//要转换的通道数目
     	ADC_Init( ADC1, &ADC_InitStruct);
     
     
     	//4、配置ADC时钟
     	RCC_ADCCLKConfig(RCC_PCLK2_Div8);//设置位8分频,则ADC的时钟是9M
     	
     	//5、配置转换顺序和采样时间
     	//参数1:哪一个ADC;参数2:通道;参数3:转换顺序(此时只有一个配置为1);参数4:采样时间
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_1,1,ADC_SampleTime_55Cycles5);
     
     
     //	//6、中断优先级配置
     //	NVIC_InitStruct.NVIC_IRQChannel=ADC1_2_IRQn;
     //	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
     //	NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
     //	NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
     //	NVIC_Init(&NVIC_InitStruct);
     //	
     //	//6、使能ADC中断
     //	//ADC_IT_EOC-规则通道 ADC_IT_JEOC-注入通道中断,ADC_IT_AWD-看门狗
     //	ADC_ITConfig( ADC1, ADC_IT_EOC, ENABLE);//使能规则通道中断
     	
     		ADC_DMACmd(ADC1,ENABLE);//使能ADC的DMA
     	
     	//7、使能ADC开始转换
     	 ADC_Cmd(ADC1, ENABLE);
     	 
     	 //8、开始校准ADC
     	  ADC_StartCalibration(ADC1);//校准ADC
     		
     		//等待校准完毕
     		while(ADC_GetCalibrationStatus(ADC1));
     		
     		//9、软件触发ADC开始转换
     		ADC_SoftwareStartConvCmd(ADC1,ENABLE);
     		 
     }
    
  • 中断服务函数全部代码删除(注释掉也可以)

     void ADC1_2_IRQHandler (void)//在启动头文件中找到
     {
     	//判断中断是否真的来了
     	if(ADC_GetITStatus( ADC1, ADC_IT_EOC)!=RESET)
     	{
     		//中断真的来了就要读取转换值了
     		 ADC_ConValue=ADC_GetConversionValue(ADC1);//读取转换的值
     		
     	}
     	ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);//清除中断
     }	
    

    ADC初始化部分结束之后还需要加入DMA初始化部分
    (直接复制DMA的源文件和头文件进行修改)

    DMA学习笔记链接-STM32-DMA数据传输(USART-ADC-数组)

  • DMA部分

    ADC1对应的是DMA1的通道1,由于是使用使用DMA读取ADC1的数值
    1、数据从哪里来到哪里去中?
    外设地址是ADC1的数据寄存器ADC_DR地址,存储器地址是自定义的16位变量ADC_ConValue的地址,传输方向是外设到存储器(外设ADC是源头DMA_DIR_PeripheralSRC)

    2、对于传多少,单位是多少?
    传输数目是1
    由于外设地址ADC_DR只有一个,所以外设地址不需要递增(DMA_PeripheralInc_Disable),传输数据的宽度是16位,即两个字节(DMA_PeripheralDataSize_HalfWord),存储器地址只是一个变量的地址,所以地址不需要递增,传输数据的宽度和外设宽度保持一致,也是16位两个字节

    DMA初始化

     //使用的是ADC1,对应的DMA1通道是通道1
     //ADC1对应的DMA1通道初始化
     void ADC_DMA_config(void)
     {
     	//1-要初始化结构体肯定要定义一个结构体变量
     	DMA_InitTypeDef DMA_InitStruct;
     	//2、配置DMA时钟
     	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA1时钟 
     	
     	
     	//数据从哪里来到哪里去(配置3个)
     	DMA_InitStruct.DMA_PeripheralBaseAddr=(uint32_t)(&(ADC1->DR));//外设地址(ADC的数据寄存器地址)
     
     	DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)(&ADC_ConValue);//存储器地址 (是自定的一个变量)
     
     	DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方式(外设ADC作为源)
     	
     	//传多少,单位是多少
     	DMA_InitStruct.DMA_BufferSize= 1;//传输数目
     	
     	DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Disable ;//外设地址不递增
     	DMA_InitStruct.DMA_PeripheralDataSize= DMA_PeripheralDataSize_HalfWord  ;//数据宽度(16位数据是两个字节)
     	
     	//配置memory
     	DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Disable ;//内存地址不递增
     	DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord ;
     	
     	
     	//配置模式和优先级
     	DMA_InitStruct.DMA_Mode=DMA_Mode_Circular ;//循环模式
     	DMA_InitStruct.DMA_Priority=DMA_Priority_High ;//共有四种优先级
     	DMA_InitStruct.DMA_M2M=DMA_M2M_Disable;
     	DMA_Init(DMA1_Channel1, &DMA_InitStruct);//ADC1对应DMA1的通道1
     
     	
     //	DMA_ClearFlag(DMA1_FLAG_TC1);//先将这个标志位清楚
     
     
     	DMA_Cmd(DMA1_Channel1, ENABLE);
     	
     	
     	
     }
    

    配置模式和优先级(上面代码的一部分)
    由于是ADC采集数据,所以要开启循环模式,源源不断的采集,只有一个通道,任何一个优先级都可以
    不是存储器到存储器模式(而是外设到存储器模式)

     DMA_InitStruct.DMA_Mode=DMA_Mode_Circular ;//循环模式
     DMA_InitStruct.DMA_Priority=DMA_Priority_High ;//共有四种优先级
     DMA_InitStruct.DMA_M2M=DMA_M2M_Disable;
     DMA_Init(DMA1_Channel1, &DMA_InitStruct);//ADC1对应DMA1的通道1
     DMA_Cmd(DMA1_Channel1, ENABLE);//使能dma1的通道1
    
  • 主函数部分内容

     float ADC_ConverValuelocal;//存放转换的数值
     int main(void)
     {	
     
     	delay_init();	    	 //延时函数初始化	  
     	NVIC_Configuration(); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
     	uart_init(9600);	 //串口初始化为9600
      	LED_Init();			     //LED端口初始化
     	KEY_Init();          //初始化与按键连接的硬件接口
     	ADC_DMA_config();
     	ADCx_Init();
     	
     		while(1)
     		{
     			ADC_ConverValuelocal=(float)ADC_ConValue/4096*3.3;
     			printf("The current AD_Value=0x%04x\r\n",ADC_ConValue);
     			printf("The current AD_Value=%f V\r\n",ADC_ConverValuelocal);
     			printf("\r\n");
     			delay_ms(1500);
     		}
     			
     }
    

    实验效果和上一部分(一)独立模式-单通道-中断读取电压值一样的,只不过此处将中断采集读取数据修改为了DMA读取数据(DMA读取数据迅速,不占用CPU资源)

(三)独立模式-多通道-DMA读取

在上面(二)部分的代码上进行添加修改

使用独立模式时,数据寄存器只有一个,可使用ADC1和ADC3(因为ADC2没有DMA功能)

  • 要实现的功能和思路
    设置ADC1六个通道进行数据采集,使用扫描模式(上一次单通道并没有开启扫描模式),由于有6个通道所以定义一个6个变量的数组,DMA存储器设置为地址递增(上一次只有一个通道,没有开启地址递增,此次是数组有多个地址,所以开启了递增)
    思路如下:
    首先第一个通道采集完数据,放到DR寄存器的低16位(DR的高十六位只有双重模式下才使用),发出DMA请求后,将数据存放在自定义数组的第一个元素位置a[0];
    接下来进行第二个通道采集数据,采集完数据之后放到DR寄存器的低16位覆盖掉上次的数据,发出DMA请求,将数据存放在自定义数组的第二个元素位置a[1];
    依次循环,直到所有通道采集完数据,将采集的数据放到数组中。
    在这里插入图片描述

  • ADC初始化部分需要修改的代码:

    1、修改变量为数组
    2、ADC1的通道1-6分别对应引脚PA1-PA6
    3、ADC要转换的通道数目改为6个
    4、ADC的初始化中要设置为扫描模式—多通道设置扫描模式
    5、ADC配置通道的转换顺序和采样时间–每一个通道都要进行设置
    6、DMA传输数目和通道个数是一样的–6个
    7、存储器地址是自定义的数组(存储器地址递增)

  • 代码详细修改如下

    1、uint16_t ADC_ConValue变量修改为数组

     uint16_t ADC_ConValue[6]={0,0,0,0,0,0};//ADC采集的数值
    

    2、GPIO引脚
    使用ADC1的1-6通道,所以GPIO引脚初始化需要额外加入另外五个引脚(ADC1的通道1-通道6对应引脚是PA1-PA6)

     //2、GPIO引脚初始化(初始化ADC1的6个通道引脚)
     GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1;
     GPIO_InitStruct.GPIO_Pin=GPIO_Pin_2;
     GPIO_InitStruct.GPIO_Pin=GPIO_Pin_3;
     GPIO_InitStruct.GPIO_Pin=GPIO_Pin_4;
     GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
     GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6;
     GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
     GPIO_Init(GPIOA, &GPIO_InitStruct);
    

    3、有六个通道所以ADC转换的通道数需要修改为6个

     ADC_InitStruct.ADC_NbrOfChannel=6;//要转换的通道数目是6个
    

    4、ADC的转换顺序和采样时间需要一个一个的配置

     //5、配置转换顺序和采样时间(需要修改)--每一个通道都要进行设置
     //参数1:哪一个ADC;参数2:通道;参数3:转换顺序(数字越小优先级越高);参数4:采样时间
     ADC_RegularChannelConfig(ADC1, ADC_Channel_1,1,ADC_SampleTime_55Cycles5);//通道1第一个转换
     ADC_RegularChannelConfig(ADC1, ADC_Channel_2,2,ADC_SampleTime_55Cycles5);//通道2第二个转换
     ADC_RegularChannelConfig(ADC1, ADC_Channel_3,3,ADC_SampleTime_55Cycles5);//通道3第三个转换
     ADC_RegularChannelConfig(ADC1, ADC_Channel_4,4,ADC_SampleTime_55Cycles5);
     ADC_RegularChannelConfig(ADC1, ADC_Channel_5,5,ADC_SampleTime_55Cycles5);
     ADC_RegularChannelConfig(ADC1, ADC_Channel_6,6,ADC_SampleTime_55Cycles5);
    
  • ADC初始化部分整合代码:

     uint16_t ADC_ConValue[6]={0,0,0,0,0,0};//ADC采集的数值
     
     //多通道数据采集使用ADC1的1-6通道分别对应引脚是PA1-PA6
     //adc初始化
     void ADCx_Init(void)
     {
     	//gpio初始化结构体
     	GPIO_InitTypeDef GPIO_InitStruct;
     	//adc初始化结构体
     	ADC_InitTypeDef ADC_InitStruct;
     //	//中断结构体
     //	NVIC_InitTypeDef NVIC_InitStruct;
     	//1、使能引脚时钟和ADC1时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1 ,ENABLE);
     	
     	
     	//2、GPIO引脚初始化(初始化ADC1的6个通道引脚)
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1;
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_2;
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_3;
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_4;
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
     	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6;
     	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
     	GPIO_Init(GPIOA, &GPIO_InitStruct);
     	
     	//3、ADC初始化
     	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;//独立模式
     	ADC_InitStruct.ADC_ScanConvMode=ENABLE;//多通道的话需要开启扫描模式
     	ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;//开启连续转换
     	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;//数据右对齐
     	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//不选择外部触发
     	ADC_InitStruct.ADC_NbrOfChannel=6;//要转换的通道数目是6个
     	ADC_Init( ADC1, &ADC_InitStruct);
     
     
     	//4、配置ADC时钟
     	RCC_ADCCLKConfig(RCC_PCLK2_Div8);//设置位8分频,则ADC的时钟是9M
     	
     	//5、配置转换顺序和采样时间(需要修改)--每一个通道都要进行设置
     	//参数1:哪一个ADC;参数2:通道;参数3:转换顺序(数字越小优先级越高);参数4:采样时间
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_1,1,ADC_SampleTime_55Cycles5);//通道1第一个转换
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_2,2,ADC_SampleTime_55Cycles5);//通道2第二个转换
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_3,3,ADC_SampleTime_55Cycles5);//通道3第三个转换
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_4,4,ADC_SampleTime_55Cycles5);
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_5,5,ADC_SampleTime_55Cycles5);
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_6,6,ADC_SampleTime_55Cycles5);
     
     	
     	//6使能ADC的DMA----这是相对于中断采集数据来说时新加的
     	ADC_DMACmd(ADC1,ENABLE);//使能ADC的DMA
     	
     	//7、使能ADC开始转换
     	 ADC_Cmd(ADC1, ENABLE);
     	 
     	 //8、开始校准ADC
     	  ADC_StartCalibration(ADC1);//校准ADC
     		
     		//等待校准完毕
     		while(ADC_GetCalibrationStatus(ADC1));
     		
     		//9、软件触发ADC开始转换
     		ADC_SoftwareStartConvCmd(ADC1,ENABLE);
     		 
     }
    
  • DMA初始化部分需要修改

    1、修改存储器地址
    外设地址依旧只有一个ADC_DR,所以只需要修改存储器地址,我们需要将采集到的数据放到数组中,所以存储器地址写数组的首地址

     DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)ADC_ConValue;//存储器地址 (数组名就是变量)
    

    2、由于是6个通道,所以传输数目改为6

     DMA_InitStruct.DMA_BufferSize= 6;//传输数目(和开启的通道数是一样的)
    

    3、开启储存器地址递增
    外设地址只有一个,地址无需递增,而存储器位置是一个数组,所以需要开启地址递增

     DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable ;//内存地址递增(由于是数组所以是递增)
    
  • DMA初始化部分代码整合如下

     //使用的是ADC1,对应的DMA1通道是通道1
     				
     void ADC_DMA_config(void)
     {
     	//1-要初始化结构体肯定要定义一个结构体变量
     	DMA_InitTypeDef DMA_InitStruct;
     	//2、配置DMA时钟
     	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA1时钟 
     	
     	
     	//由于寄存器都是32位的所以都强制类型转换为32位
     	//数据从哪里来到哪里去(配置3个)
     	DMA_InitStruct.DMA_PeripheralBaseAddr=(uint32_t)(&(ADC1->DR));//外设地址(ADC的数据寄存器地址)
     
     	DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)ADC_ConValue;//存储器地址 (数组名就是变量)
     
     	DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方式(外设ADC作为源)
     	
     	//传多少,单位是多少
     	DMA_InitStruct.DMA_BufferSize= 6;//传输数目(和开启的通道数是一样的)
     	
     	DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Disable ;//外设地址不递增
     	DMA_InitStruct.DMA_PeripheralDataSize= DMA_PeripheralDataSize_HalfWord  ;//数据宽度(16位数据是两个字节)
     	
     	//配置memory
     	DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable ;//内存地址递增(由于是数组所以是递增)
     	DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord ;
     	
     	
     	//配置模式和优先级
     	DMA_InitStruct.DMA_Mode=DMA_Mode_Circular ;//循环模式
     	DMA_InitStruct.DMA_Priority=DMA_Priority_High ;//共有四种优先级
     	DMA_InitStruct.DMA_M2M=DMA_M2M_Disable;
     	DMA_Init(DMA1_Channel1, &DMA_InitStruct);//ADC1对应DMA1的通道1
     
     	DMA_Cmd(DMA1_Channel1, ENABLE);
     
     }
    
  • 主函数部分

     float ADC_ConverValuelocal[6];//存放转换的数值
     int main(void)
     {	
     	int i;
     	delay_init();	    	 //延时函数初始化	  
     	NVIC_Configuration(); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
     	uart_init(9600);	 //串口初始化为9600
      	LED_Init();			     //LED端口初始化
     	KEY_Init();          //初始化与按键连接的硬件接口
     	ADC_DMA_config();
     	ADCx_Init();
     	
     		while(1)
     		{
     			//读取数据依次存放到数组中
     			for(i=0;i<6;i++)
     			{
     				ADC_ConverValuelocal[i]=(float)ADC_ConValue[i]/4096*3.3;
     			}
     			
     			//依次打印数组中的数据
     			for(i=0;i<6;i++)
     			{
     				printf("The current AD_Value[%d]=%f V\r\n",i,ADC_ConverValuelocal[i]);
     			}
     			printf("\r\n");
     			delay_ms(1500);
     		}
     			
     
     }
    
  • 实验效果:

    将PA1-PA6依次接到3.3v和GND即可观察到电压值接近于3.3V或者等于0V
    在这里插入图片描述

(四)双重模式-同步规则模式-DMA读取

  • ADC和通道

    ADC1和ADC2同时使用
    ADC1开启了通道1
    ADC2开启了通道5

  • 采样阶段和转换阶段

    AD 转换包括采样阶段和转换阶段,在采样阶段才对通道数据进行采集;而在转换阶段只是将采集到的数据进行转换为数字量输出,此刻通道数据变化不会改变转换结果。

  • 独立模式和双重模式采集效率

    独立模式的 ADC 采集需要在一个通道采集并且转换完成后才会进行下一个通道的采集。而双重ADC 的机制就是使用两个 ADC 同时采样一个或者多个通道。双重 ADC 模式较独立模式一个最大的优势就是提高了采样率,弥补了单个 ADC 采样不够快的缺点。

  • 独立模式和双重模式下数据存储

    1、独立模式
    数据存放到DR寄存器低16位,高16位不使用。
    2、双重模式下
    ADC1采集的数据放到DR寄存器的低16位
    ADC2采集的数据放到DR寄存器的高16位

    ADC 工作模式要设置为同步规则模式;两个 ADC 的通道的采样时间需要一致; ADC1 设置为软件触发; ADC2 设置为外部触发。

    DMA和ADC初始化部分大同小异

  • ADC初始化部分需要添加的部分
    在独立模式的基础上开启ADC2的时钟,配置好对应的ADC2的通道5引脚,使能ADC2的外部触发。

     //使能ADC2的外部触发
     ADC_ExternalTrigConvCmd( ADC2, ENABLE);
    
  • DMA初始化部分

    在独立模式的基础上修改一下存储器存储数据的位数,由原来的16位改为32位的(之所以没有定义一个变量,而是定义成了一个数组的形式,是为了双重模式下,开启多个通道时修改方便)

     uint32_t ADC_ConValue[1]//这是一个32位数据的数组,只有一个元素
    
  • 主函数内容:

    1、定义两个变量
    首先定义两个16位变量,要存放ADC1和ADC2读取的数值

     u16 temp0,temp1;//存放两个ADC读取的数值
    

    2、数据分离
    双重模式下,ADC1读取的数值放到DR寄存器的低16位,ADC2读取的数值存放到DR寄存器的高16位,定义了一个32位的数组(存放到了第一个元素中)

    ADC1和ADC2的数值都分别放到了数组ADC_ConValue第一个元素的低16位和高16位,需要把高16位数据和低16位数据分离出来

     //取出寄存器的高16位--ADC2值
     emp0=(ADC_ConValue[0]&0xffff0000)>>16;
     //取出ADC的低16位---adc1值
     temp1=ADC_ConValue[0]&0xffff;
    

    3、电压值转换
    将分离转换后的电压值存储到数组中

     ADC_ConverValuelocal[0]=(float)temp0/4096*3.3;//adc2的值
     					
     ADC_ConverValuelocal[1]=(float)temp1/4096*3.3;//adc1的值
    
  • 主函数整合代码

     float ADC_ConverValuelocal[2];//存放转换的数值
     int main(void)
     {	
     //	int i;
     	u16 temp0,temp1;
     	delay_init();	    	 //延时函数初始化	  
     	NVIC_Configuration(); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
     	uart_init(9600);	 //串口初始化为9600
      	LED_Init();			     //LED端口初始化
     	KEY_Init();          //初始化与按键连接的硬件接口
     	ADC_DMA_config();
     	ADCx_Init();
     	
     		while(1)
     		{
     			//取出寄存器的高16位--ADC2值
     			temp0=(ADC_ConValue[0]&0xffff0000)>>16;
     			//取出ADC的低16位---adc1值
     			temp1=ADC_ConValue[0]&0xffff;
     			
     		  ADC_ConverValuelocal[0]=(float)temp0/4096*3.3;//adc2的值
     			
     			ADC_ConverValuelocal[1]=(float)temp1/4096*3.3;//adc1的值
     
     
     			
     			printf("The current ADC1_Value=%f V\r\n",ADC_ConverValuelocal[1]);//adc1的转换值
     			printf("The current ADC2_Value=%f V\r\n",ADC_ConverValuelocal[0]);//adc2的转换值
     	
     
     			printf("\r\n");
     			delay_ms(1500);
     		}
     			
     
     }
    

(五)双重模式-同步规则-多通道-DMA读取

  • ADC和通道

    ADC1和ADC2同时使用
    ADC1开启了通道1,通道2
    ADC2开启了通道5,通道6

(六)使用STM32的内部通道获取温度

  • 内部温度传感器

    STM32 有一个内部的温度传感器,可以用来测量 CPU 及周围的温度(TA)。该温度传感器在内部和 ADCx_IN16 输入通道相连接,此通道把传感器输出的电压转换成数字值。温度传感器模拟输入推荐采样时间是 17.1μ s。 STM32 的内部温度传感器支持的温度范围为: -40~125度。精度比较差,为±1.5℃左右。

    在第二个实验基础做修改(独立模式-单通道-DMA读取)除了把GPIOA初始化引脚的代码去掉外,还需要做以下修改:

  • 配置通道16的转换顺序

    STM32 的内部温度传感器固定的连接在 ADC 的通道 16 上

     //4、配置转换顺序和采样时间
     //参数1:哪一个ADC;参数2:通道;参数3:转换顺序;参数4:采样时间
     ADC_RegularChannelConfig(ADC1, ADC_Channel_16,1,ADC_SampleTime_239Cycles5 );
    
  • 开启内部温度传感器
    我们要使用 STM32 的内部温度传感器,必须先激活 ADC 的内部通道

     //5、开启内部温度传感器
     ADC_TempSensorVrefintCmd(ENABLE);
    
  • ADC初始化整合代码

     u16 ADC_ConValue;//ADC采集的数值
     
     //adc初始化
     void ADCx_Init(void)
     {
     
     	//adc初始化结构体
     	ADC_InitTypeDef ADC_InitStruct;
     
     	//1、使能引脚时钟和ADC1时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 ,ENABLE);
     	
     	
     	//2、ADC初始化
     	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;//独立模式
     	ADC_InitStruct.ADC_ScanConvMode=DISABLE;//单通道的话不需要开启扫描模式
     	ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;//开启连续转换
     	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;//数据右对齐
     	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//不选择外部触发
     	ADC_InitStruct.ADC_NbrOfChannel=1;//要转换的通道数目
     	ADC_Init( ADC1, &ADC_InitStruct);
     
     	//3、配置ADC时钟
     	RCC_ADCCLKConfig(RCC_PCLK2_Div8);//设置位8分频,则ADC的时钟是9M
     	
     	//4、配置转换顺序和采样时间
     	//参数1:哪一个ADC;参数2:通道;参数3:转换顺序;参数4:采样时间
     	ADC_RegularChannelConfig(ADC1, ADC_Channel_16,1,ADC_SampleTime_239Cycles5 );
     	//5、开启内部温度传感器
     	ADC_TempSensorVrefintCmd(ENABLE);
     
     	//5使能ADC的DMA
     	ADC_DMACmd(ADC1,ENABLE);//使能ADC的DMA
     	
     	//6、使能ADC开始转换
     	 ADC_Cmd(ADC1, ENABLE);
     	 
     	 //7、复位一下校准
     	 ADC_ResetCalibration(ADC1); //重置指定的 ADC1 的复位寄存器
     	while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位完成
     	 
     	 //7、开始校准ADC
     	 ADC_StartCalibration(ADC1);//校准ADC
     		
     	//等待校准完毕
     	while(ADC_GetCalibrationStatus(ADC1));
     		
     	//8、软件触发ADC开始转换
     	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
     		 
     }
    
  • DMA初始化部分不需要做修改
    温度数值的计算公式如下
    在这里插入图片描述

     temp=(1.43-ADC_ConverValuelocal)/0.0043+25;//将电压值转换为温度值
    
  • 主函数部分:

     float ADC_ConverValuelocal;//存放转换的电压数值
     
     float temp;//存放转换的温度数值
     int main(void)
     {	
     
     	delay_init();	    	 //延时函数初始化	  
     	NVIC_Configuration(); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
     	uart_init(9600);	 //串口初始化为9600
      	LED_Init();			     //LED端口初始化
     	KEY_Init();          //初始化与按键连接的硬件接口
     	ADC_DMA_config();
     	ADCx_Init();
     	
     		while(1)
     		{
     			ADC_ConverValuelocal=(float)ADC_ConValue/4096*3.3;//温度传感器的电压值
     			
     			temp=(1.43-ADC_ConverValuelocal)/0.0043+25;//将电压值转换为温度值
     			
     			printf("The current ADC_Value=%.2f\r\n",ADC_ConverValuelocal);//打印转换的电压值
     			printf("The current temp=%.2f \r\n",temp);//打印转换的温度值
     			printf("\r\n");
     			delay_ms(1500);
     		}
     			
     
     }
    
  • 实验效果

    在这里插入图片描述

五、代码下载

代码下载链接
代码如下:
在这里插入图片描述

评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

永栀哇

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

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

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

打赏作者

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

抵扣说明:

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

余额充值