STM32-16-ADC

STM32-01-认识单片机
STM32-02-基础知识
STM32-03-HAL库
STM32-04-时钟树
STM32-05-SYSTEM文件夹
STM32-06-GPIO
STM32-07-外部中断
STM32-08-串口
STM32-09-IWDG和WWDG
STM32-10-定时器
STM32-11-电容触摸按键
STM32-12-OLED模块
STM32-13-MPU
STM32-14-FSMC_LCD
STM32-15-DMA

STM32-16-ADC

1. ADC简介

  • 什么是ADC:

    ADCAnalog-to-digital converter,即模拟/数字转换器,可以将外部的模拟信号转换为数字信号。STM32F103系列芯片拥有3ADC(C8T6只有2个),这些ADC可以独立使用,其中 ADC1和ADC2还可以组成双重模式提高采样率)。STM32的ADC是12逐次逼近型的模拟数字转换器。它有18个通道,可测量16个外部和2个内部信号源,其中ADC3根据CPU引脚的不同其通道数也不同,一般有8个外部通道。ADC中的各个通道的AD转换可以单次连续扫描间断模式执行。ADC的结果可以以左对齐或者右对齐存储在16数据寄存器中。
    在这里插入图片描述

    在嵌入式系统中,ADC通常用于采集传感器的数据,例如压力、温度、图像传感器等参数,被转换成电压后,通过ADC转换为数字量,供单片机处理,单片机根据处理结果进行相关控制和显示。

  • 主要特性:

    1. 12位分辨率
    2. 转换结束、注入转换结束和发生模拟看门狗时产生中断
    3. 单次和连续转换模式
    4. 自校准
    5. 带内嵌数据一致性的数据对齐
    6. 采样间隔可以按通道分别编程
    7. 规则转换和注入转换均有外部触发选项
    8. 间断模式
    9. 双重模式
    10. ADC转换时间:时钟为72MHz为1.17us
    11. ADC供电要求:2.4V到3.6V
    12. ADC输入范围:Vref-<=Vin<=Vref+
    13. 规则通道转换期间有DMA请求产生
  • 常见的ADC类型:

    ADC电路类型优点缺点
    并联比较型转换速度最快成本高、功耗高,分辨率低
    逐次逼近型结构简单,功耗低转换速度较慢

    并联比较型(Flash型)和逐次逼近型(SAR型)是两种常见的模数转换器(ADC)架构,它们在工作原理、速度、功耗、精度等方面有明显的区别。

    并联比较型ADC(Flash ADC)
    在这里插入图片描述

    工作原理

    • 并联比较型ADC使用一组并行的比较器,将输入模拟信号同时与参考电压分压网络进行比较。
    • 每个比较器对应一个特定的电压范围,输出一个二进制码。
    • 比较器的输出直接决定ADC的数字输出,无需逐步逼近。

    特点

    • 速度快:由于所有比较器同时工作,转换时间非常短,适合高频信号采样。
    • 高功耗:大量并行比较器和电阻网络消耗较多电能。
    • 高复杂度:电路复杂度随分辨率成指数增长,如8位ADC需要255个比较器。
    • 应用场景:高速数据采集、视频处理、通信系统等。

    逐次逼近型ADC(SAR ADC)
    在这里插入图片描述

    工作原理

    • 逐次逼近型ADC通过一个逐次逼近寄存器(SAR)和一个比较器工作。
    • 它逐步逼近输入模拟信号,从最高位到最低位,逐位进行比较。
    • 每次比较后,根据比较结果决定下一个比较的方向,直到所有位都确定。

    特点

    • 速度适中:转换速度适中,比Flash ADC慢,但比积分型ADC快。
    • 低功耗:由于只使用一个比较器,功耗较低。
    • 高分辨率:适合高分辨率应用,电路复杂度与分辨率成线性关系。
    • 应用场景:低功耗应用、音频采样、精密测量仪器等。
    特点并联比较型ADC逐次逼近型ADC
    转换速度非常快(纳秒级)适中(微秒级)
    功耗
    电路复杂度高(比较器数量多)适中(逐次逼近寄存器)
    分辨率适中(通常8位或以下)高(可达16位或以上)
    典型应用高速数据采集、视频处理音频采样、精密测量

    并联比较型ADC适合需要极高转换速度的应用,而逐次逼近型ADC则在功耗和分辨率之间提供了良好的平衡,适合多种精密测量中速数据采集的应用。

  • ADC的特性参数

    • 分辨率: 表示ADC能辨别的最小模拟量,用二进制位表示
    • 转换时间: 表示完成一次A/D转换所需要的时间,转换时间越短,采样率越高
    • 精度: 最小刻度基础上叠加各种误差的参数,精度受ADC性能、温度和气压等影响
    • 量化误差: 用数字量近似表示模拟量,采用四舍五入原则,此过程产生的误差为量化误差

2. ADC工作原理

ADC框图
在这里插入图片描述

  • ①输入电压
    在这里插入图片描述

  • ②输入通道

    确定好ADC输入电压后,通过ADC输入通道把外部电压输送到ADC转换器中,下面是ADC1~3对应的通道。
    在这里插入图片描述

  • ③转换顺序

    在STM32的ADC转换过程中,规则组和注入组是两种不同的ADC转换序列,它们在配置、使用场景和优先级等方面存在显著区别。

    规则组(Regular Group)

    定义和特点

    • 规则组是常规的ADC转换序列,通常用于连续的、周期性的ADC采样。
    • 可以包含多个通道,这些通道按照配置的顺序依次进行转换。
    • 规则组的转换可以由多种触发源启动,如软件触发、定时器触发等。

    应用场景

    • 常用于采集传感器数据定期采样信号等场景。
    • 适用于实时性要求较高的应用,但相对注入组来说,优先级较低。

    触发源

    • 支持多种触发源,包括软件触发和硬件触发(如定时器事件)。
    • 可设置为连续转换模式或单次转换模式。

    注入组(Injected Group)

    定义和特点

    • 注入组是为了在规则转换之外进行临时或优先转换的一种机制。
    • 通常包含少量通道,用于处理突发性或优先级更高的ADC采样需求。
    • 注入组转换有独立的触发源,与规则组转换互不干扰。

    应用场景

    • 常用于优先级更高的采样需求,如监测关键参数的变化快速响应事件等。
    • 注入组的转换结果可以通过中断DMA方式快速处理,适用于突发事件的处理。

    触发源

    • 注入组有独立的触发源,可以是外部事件、定时器触发等。
    • 通常由特定的事件触发,不会像规则组那样进行连续转换。

    总结:

    特点规则组(Regular Group)注入组(Injected Group)
    转换触发多种触发源(软件、定时器等)独立触发源(外部事件、定时器等)
    转换顺序配置的通道顺序配置的少量优先通道
    优先级相对较低相对较高
    应用场景定期采样、传感器数据采集关键参数监测、突发事件处理
    转换模式连续或单次转换通常为单次转换
    结果处理常规方式处理可通过中断或DMA快速处理

在这里插入图片描述

规则序列

规则组最多允许16个输入通道进行转换,那么就需要设置通道转换的顺序,即规则序列。

规则序列寄存器控制关系如下:
在这里插入图片描述

注入序列

注入组最大允许4个通道输入,它的注入序列由JSQR寄存器配置。

注入序列寄存器控制关系如下:
在这里插入图片描述

  • ④触发源
    在这里插入图片描述

    规则组外部触发使用方法是将 EXTTRIG 位置 1,并且通过 EXTSET[2:0]位选择规则组启动转换的触发源。如果 EXTSET[2:0]位设置为 111,那么可以通过 SWSTART 为启动 ADC 转换, 相当于软件触发。

    注入组外部触发使用方法是将 JEXTTRIG 位置 1,并且通过 JEXTSET[2:0]位选择注入组启动转换的触发源。如果 JEXTSET[2:0]位设置为 111,那么可以通过 JSWSTART 为启动 ADC 转换,相当于软件触发。
    在这里插入图片描述

  • ⑤转换时间

    ADC的输入时钟是由PCLK2经过分频产生的,分频系数是由RCC_CFGR寄存器的ADCPRE[1:0]位设置的,可选择 2/4/8/16 分频。需要注意的是,ADC 的输入时钟频率最大值是14MHz,如果超过这个值将会导致 ADC 的转换结果准确度下降。 一般我们设置 PCLK272MHz。为了不超过 ADC 的最大输入时钟频率 14MHz,我们设置 ADC 的预分频器分频系数为 6,就可以得到 ADC 的输入时钟频率为 72MHz/6,即 12MHz
    在这里插入图片描述

在这里插入图片描述

  • ⑥数据寄存器
    在这里插入图片描述

    ADC 规则数据寄存器(ADC_DR)

    ADC规则组数据寄存器ADC_DR是一个32位的寄存器,独立模式时只使用到该寄存器低16位保存ADC1/2/3的规则转换数据。在双ADC模式下,高16位用于保存ADC2转换的数据,低16位用于保存ADC1转换的数据。

    因为ADC的精度是12位的,ADC_DR寄存器无论高16位还是低16w位,存放数据的位宽都是16位的,所以允许选择数据对齐方式。由ADC_CR2寄存器的ALIGN位设置数据对齐方式,可选择:右对齐或者左对齐

    规则组16个输入通道只对应一个数据寄存器,所以通道转换完成后要及时取出数据,比较常用的方法是使用DMA模式。当规则组的转换结束后,就会产生DMA请求,这样就可以把数据及时搬运到用户指定的目的地址存放。只有ADC1和ADC3可以产生DMA请求,而ADC2转换的数据可以通过双ADC模式,利用ADC1的DMA功能传输。

    ADC 注入数据寄存器x(ADC_JDRx)(x=1~4)

    ADC注入数据寄存器有4个,注入组最多有4个输入通道,刚好每个通道都有自己对应的数据寄存器。ADC_JDRx寄存器是32位的,低16位有效,高16位保留,数据也同样需要选择对齐方式。也是由ADC _CR2寄存器的ALIGN位设置数据对齐方式,可选择:右对齐或者左对齐。

  • ⑦中断

    中断事件事件标志使能控制位
    规则通道转换结束EOCEOCIE
    注入通道转换结束JEOCJEOCIE
    设置了模拟看门狗状态位AWDAWDIE

    规则组和注入组的转换结束后,除了可以产生中断外,还可以产生 DMA 请求,我们利用DMA 及时把转换好的数据传输到指定的内存里,防止数据被覆盖。

  • ⑧单次转换模式和连续转换模式
    在这里插入图片描述

  • ⑨扫描模式
    在这里插入图片描述

3. 单通道ADC采集实验

1. 相关寄存器
  • ADC控制寄存器1(ADC_CR1)
    在这里插入图片描述

  • ADC控制寄存器2(ADC_CR2)
    在这里插入图片描述

  • ADC采样事件寄存器1(ADC_SMPR1)
    在这里插入图片描述

  • ADC采集事件寄存器2(ADC_SMPR2)
    在这里插入图片描述

    ADC 采样时间设置需要由两个寄存器设置,ADC_SMPR1ADC_SMPR2,分别设置通道 10~17 和通道 0~9 的采样时间,每个通道用 3 个位设置。

  • ADC规则序列寄存器1(ADC_SQR1)
    在这里插入图片描述

    ADC_SQR2
    在这里插入图片描述

    ADC_SQR3
    在这里插入图片描述

    L[3:0]用于设置规则组序列的长度,取值范围:015,表示规则组的长度是116。本实验只用了1个输入通道,所以L[3:0]位设置为0000即可。

    SQ13[4:0]~SQ16[4:0]位设置规则组序列的第 13~16 个转换编号,第 1~12 个转换编号的设置请查看 ADC_SQR2 和 ADC_SQR3 寄存器。

  • ADC规则数据寄存器(ADC_DR)
    在这里插入图片描述

  • ADC状态寄存器(ADC_SR)
    在这里插入图片描述

2. 配置步骤
  1. 配置ADC工作参数、ADC校准

    HAL_ADC_Init()
    HAL_ADCEx_Calibration_Start()
    
  2. ADC MSP初始化

    HAL_ADC_MspInit()     
    
  3. 配置ADC相应通道相关参数

    HAL_ADC_ConfigChannel()
    
  4. 启动A/D转换

    HAL_ADC_Start()
    
  5. 等待规则通道转换完成

    HAL_ADC_PollForConversion()
    
  6. 获取规则通道A/D转换结果

    HAL_ADC_GetValue()
    
  • 具体函数功能

    函数主要寄存器主要功能
    HAL_ADC_Init()CR1、CR2配置ADC工作参数
    HAL_ADCEx_Calibration_Start()CR2ADC校准
    HAL_ADC_MspInit()存放NVIC、CLOCK、GPIO初始化代码
    HAL_RCCEx_PeriphCLKConfig()RCC_CFGR设置扩展外设时钟,如:ADC、RTC等
    HAL_ADC_ConfigChannel()SQRx、SMPRx配置ADC相应通道的相关参数
    HAL_ADC_Start()CR2启动A/D转换
    HAL_ADC_PollForConversion()SR等待规则通道转换完成
    HAL_ADC_GetValue()DR获取规则通道A/D转换结果
3. 代码实现
  • 功能: 采集ADC1通道1(PA1)上面的电压,并在LCD模块上面显示ADC规则数据寄存器12位的转换值以及将该值换算成电压后的电压值。使用杜邦线将ADC和RV1排针连接,使得PA1连接到电位器上,然后将ADC采集到的数据和转换后的电压值在TFTLCD屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0闪烁,提示程序运行。

  • ADC单通道初始化函数

    void adc_init(void)
    {
        ADC_ChannelConfTypeDef adc_ch_conf;
        
        g_adc_handle.Instance = ADC1;                             //选择ADC1
        g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;        //数据对齐方式为右对齐
        g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;        //非扫描模式,仅使用一个通道
        g_adc_handle.Init.ContinuousConvMode = DISABLE;           //关闭连续转换模式
        g_adc_handle.Init.NbrOfConversion = 1;                    //通道数选择1个
        g_adc_handle.Init.DiscontinuousConvMode = DISABLE;        //禁止规则通道组间断模式  
        g_adc_handle.Init.NbrOfDiscConversion = 0;                //配置间断模式的规则通道个数
        g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;  //软件触发方式
        HAL_ADC_Init(&g_adc_handle);   //进行初始化
        
        HAL_ADCEx_Calibration_Start(&g_adc_handle);   //校准ADC
        
        adc_ch_conf.Channel = ADC_CHANNEL_1;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_handle, &adc_ch_conf);    //通道配置
    }
    

    g_adc_handle.Instance = ADC1;: 选择ADC1作为转换的ADC模块。

    g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;: 数据右对齐。

    g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;: 禁用扫描模式,仅使用一个通道。

    g_adc_handle.Init.ContinuousConvMode = DISABLE;: 禁用连续转换模式。

    g_adc_handle.Init.NbrOfConversion = 1;: 仅使用一个转换通道。

    g_adc_handle.Init.DiscontinuousConvMode = DISABLE;: 禁用间断模式。

    g_adc_handle.Init.NbrOfDiscConversion = 0;: 设置间断模式的规则通道数为0。

    g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;: 设置软件触发转换。

    adc_ch_conf.Channel = ADC_CHANNEL_1;: 选择ADC通道1。

    adc_ch_conf.Rank = ADC_REGULAR_RANK_1;: 设置通道1为规则组的第一个转换通道。

    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;: 采样时间为239.5个ADC时钟周期。

    STM32的ADC1模块进行初始化和通道配置,为后续的ADC数据采集做好准备。具体来说,初始化配置包括设置数据对齐方式、转换模式和触发方式等。通过校准确保ADC转换的精度,然后配置一个规则组通道,使得ADC可以在指定通道上进行采样并转换。

  • ADC MSP初始化函数

    void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
    {
        if(hadc->Instance == ADC1)
        {
            
            GPIO_InitTypeDef gpio_init_struct;
            RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
            
            //使能GPIOA和ADC1的时钟
            __HAL_RCC_GPIOA_CLK_ENABLE();
            __HAL_RCC_ADC1_CLK_ENABLE();
            
            // 配置GPIOA的引脚1为模拟输入模式
            gpio_init_struct.Pin = GPIO_PIN_1;
            gpio_init_struct.Mode = GPIO_MODE_ANALOG;  //模拟输入
            HAL_GPIO_Init(GPIOA, &gpio_init_struct);
           
            //配置ADC的时钟源和分频系数
            adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;  //ADC外设时钟
            adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;//分频因子6,时钟为72M/6=12MHz  
            HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);  //设置ADC时钟
        }
    }
    

    配置ADC1的时钟源和分频系数。这里选择了RCC的ADC外设时钟,并将ADC的时钟频率设置为系统时钟(72MHz)除以6,即12MHz。这有助于确保ADC1以适当的频率运行,从而保证其性能和精度。

  • 启动ADC并获取转换结果函数

    uint32_t adc_get_result(void)
    {
        // 启动A/D转换
        HAL_ADC_Start(&g_adc_handle);   // 开启ADC
        // 等待规则通道转换完成
        HAL_ADC_PollForConversion(&g_adc_handle, 10);   // 轮询转换,等待完成
        // 获取规则通道A/D转换结果
        return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);  // 返回最近一次规则组的转换结果
    }
    
    1. 启动A/D转换:

      HAL_ADC_Start(&g_adc_handle);
      

      g_adc_handle 是先前定义的并初始化好的 ADC 句柄。此操作会开始对选定的ADC通道进行模数转换。

    2. 等待规则通道转换完成:

      HAL_ADC_PollForConversion(&g_adc_handle, 10);
      

      调用 HAL_ADC_PollForConversion 函数进行轮询,等待转换完成。这里设置了一个超时值为10个时钟周期。如果在超时时间内转换完成,函数会返回并继续执行;否则,它会阻塞直到超时或者转换完成。

    3. 获取规则通道A/D转换结果:

      return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
      

      调用 HAL_ADC_GetValue 函数获取ADC转换结果。返回的值是最近一次转换的结果,类型为 uint16_t,因为一般ADC的结果是16位。

  • 主函数

    int main(void)
    {
        uint16_t adcx; //ADC换算后的数字量
        float temp;  //转换后的电压值
    
        HAL_Init();                         /* 初始化HAL库 */
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72);                     /* 延时初始化 */
        usart_init(115200);                 /* 串口初始化为115200 */
        led_init();                         /* 初始化LED */
        lcd_init();                         /* 初始化LCD */
        oled_init();                        //OLED初始化
        adc_init();
    
        lcd_show_string(110, 50, 240, 16, 32, "STM32", GREEN);
        lcd_show_string(105, 90, 240, 16, 24, "ADC TEST", GREEN);
        lcd_show_string(30, 150, 240, 16, 24, "CH1_VAL:", BLUE);
        lcd_show_string(30, 180, 240, 16, 24, "CH1_VOL:0.000V", BLUE);
        
        oled_show_string(10,0,"ADC TEST",24);
        oled_show_string(0, 25, "CH1_VAL:", 16);
        oled_show_string(0, 41, "CH1_VOL:0.000V", 16);
        oled_refresh_gram();
    
    
        while (1)
        {
            adcx = adc_get_result();  //获取ADC转换后的结果
            lcd_show_xnum(126, 150, adcx, 5, 24, 0, BLUE);
            oled_show_num(65, 25, adcx, 5, 16);
            
            //在LCD和OLED上显示电压值整数部分
            temp = (float)adcx * (3.3 / 4096);
            adcx = temp;
            lcd_show_xnum(126, 180, adcx, 1, 24, 0, BLUE);
            oled_show_num(64, 41, adcx, 1, 16);
            
            //在LCD和OLED上显示电压值小数部分
            temp -= adcx;
            temp *= 1000;
            lcd_show_xnum(150, 180, temp, 3, 24, 0x80, BLUE);
            oled_show_num(80, 41, temp, 3, 16);
            
            LED0_TOGGLE(); 
            delay_ms(100);
            
            oled_refresh_gram();
        }
    }
    

    实验结果:

    作用

    • 实现ADC转换,并将结果显示在LCD和OLED上,实时显示ADC通道的数值和对应的电压值。

    影响

    • 可以实时监控ADC通道的输入电压,适用于需要监测模拟信号的应用,如传感器数据读取。
    • 通过LED的切换,可以直观地知道系统是否在运行。
    • 延时和轮询会占用CPU资源,对实时性有一定影响,如果有更高的实时性要求,可以考虑使用中断或DMA。

4. 单通道ADC采集(DMA读取)实验

  • 功能: 使用 ADC 采集(DMA 读取)通道 1(PA1)上面的电压,在 LCD 和OLED模块上面显示 ADC 转换值以及换算成电压后的电压值。使用短路帽将 ADC 和 RV1 排针连接,使得 PA1 连接到电位器上,然后将 ADC 采集到的数据和转换后的电压值在 TFTLCD 屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0 闪烁,提示程序运行。

  • ADC_DMA初始化函数

    void adc_dma_init(uint32_t mar)
    {
        ADC_ChannelConfTypeDef adc_ch_conf = {0};
        __HAL_RCC_DMA1_CLK_ENABLE();
          
        g_dma_adc_handle.Instance = DMA1_Channel1;
        g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
        g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;
        g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;
        g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
        g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
        g_dma_adc_handle.Init.Mode = DMA_NORMAL;
        g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;  
        HAL_DMA_Init(&g_dma_adc_handle);
        
        //2.将DMA和ADC句柄连接起来
        __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);
        
                
        g_adc_dma_handle.Instance = ADC1;                             //选择ADC1
        g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;        //数据对齐方式为右对齐
        g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;        //非扫描模式,仅使用一个通道
        g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;            //打开连续转换模式
        g_adc_dma_handle.Init.NbrOfConversion = 1;                    //通道数选择1个
        g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;        //禁止规则通道组间断模式  
        g_adc_dma_handle.Init.NbrOfDiscConversion = 0;                //配置间断模式的规则通道个数
        g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;  //软件触发方式
        HAL_ADC_Init(&g_adc_dma_handle);   //进行初始化
        
        HAL_ADCEx_Calibration_Start(&g_adc_dma_handle);   //校准ADC
        
        adc_ch_conf.Channel = ADC_CHANNEL_1;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);    //通道配置
        
        HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3);
        HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
        
        HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
        HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0);
    }
    

    总结:

    1. 启用DMA1时钟
    __HAL_RCC_DMA1_CLK_ENABLE();
    
    • 使能DMA1的时钟,确保DMA1能够正常工作。
    1. 配置DMA
    g_dma_adc_handle.Instance = DMA1_Channel1;
    g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;     // 数据从外设到内存
    g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;         // 外设地址不递增
    g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;             // 内存地址递增
    g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据对齐为16位
    g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 内存数据对齐为16位
    g_dma_adc_handle.Init.Mode = DMA_NORMAL;                    // 普通模式
    g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;       // 优先级为中等
    HAL_DMA_Init(&g_dma_adc_handle);
    
    • 初始化DMA1的通道1,配置DMA的数据传输方向地址递增模式数据对齐方式传输模式优先级
    1. 将DMA和ADC句柄连接起来
    __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);
    
    • 将DMA句柄与ADC句柄连接,使ADC能够使用DMA传输数据。
    1. 初始化ADC
    g_adc_dma_handle.Instance = ADC1;                            // 选择ADC1
    g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;       // 数据右对齐
    g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;       // 非扫描模式
    g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;           // 连续转换模式
    g_adc_dma_handle.Init.NbrOfConversion = 1;                   // 单通道
    g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;       // 禁止间断模式
    g_adc_dma_handle.Init.NbrOfDiscConversion = 0;               // 间断模式转换数
    g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
    HAL_ADC_Init(&g_adc_dma_handle);
    
    • 配置并初始化ADC1,包括数据对齐方式扫描模式连续转换模式转换通道数间断模式触发方式
    1. 校准ADC
    HAL_ADCEx_Calibration_Start(&g_adc_dma_handle);
    
    • 启动ADC的校准,以提高转换精度。
    1. 配置ADC通道
    adc_ch_conf.Channel = ADC_CHANNEL_1;                       // 通道1
    adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                     // 规则组第1个转换
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;     // 采样时间
    HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);
    
    • 配置ADC的输入通道转换序列采样时间
    1. 配置DMA中断优先级并启用中断
    HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3);
    HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
    
    • 设置DMA1通道1中断的优先级,并启用中断。
    1. 启动DMA传输和ADC DMA模式
    HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
    HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0);
    
    • 启动DMA传输,将ADC数据寄存器的地址作为源地址,mar作为目标地址,并指定传输数据量。
    • 启动ADC的DMA模式,使得ADC转换完成后通过DMA传输数据。

    代码的功能:

    • 使能DMA和ADC时钟:确保DMA和ADC正常工作。
    • 配置DMA:设置DMA传输的方向、地址递增模式、数据对齐方式、传输模式和优先级。
    • 初始化ADC:配置ADC的工作模式、转换方式和触发方式。
    • 校准ADC:提高ADC转换的精度。
    • 配置ADC通道:指定ADC的输入通道和采样时间。
    • 配置中断:设置DMA中断的优先级并启用中断,以便在数据传输完成后处理中断。
    • 启动DMA传输和ADC DMA模式:实现ADC转换结果通过DMA自动传输到指定内存地址,提高数据传输效率和系统性能。

    对程序的影响:

    • 提高数据传输效率:使用DMA自动传输数据,减少CPU的负担。
    • 自动化数据采集:通过DMA实现ADC数据的自动传输,无需手动处理数据搬运。
    • 实时数据处理:配置中断处理机制,可以在数据传输完成后立即处理,提高系统的实时性和响应速度。
  • ADC MSP初始化函数

    void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
    {
        if(hadc->Instance == ADC1)
        {
            GPIO_InitTypeDef gpio_init_struct;
            RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
            
            //使能时钟
            __HAL_RCC_GPIOA_CLK_ENABLE();
            __HAL_RCC_ADC1_CLK_ENABLE();
            
            //配置工作模式
            gpio_init_struct.Pin = GPIO_PIN_1;
            gpio_init_struct.Mode = GPIO_MODE_ANALOG;  //模拟输入
            HAL_GPIO_Init(GPIOA, &gpio_init_struct);
           
            adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;  //ADC外设时钟
            adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;     //分频因子6,时钟为72M/6=12MHz  
            HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);  //设置ADC时钟
        }
    }
    
  • 使能一次ADC DMA传输函数

    void adc_dma_enable(uint16_t cndtr)
    {
        ADC1->CR2 &= ~(1 << 0);  //CONT位置1,关闭连续转换模式
        
        DMA1_Channel1->CCR &= ~(1 << 0);      //禁用DMA通道1
        while(DMA1_Channel1->CCR & (1 << 0)); //等待DMA通道1禁用完成
        
        DMA1_Channel1->CNDTR = cndtr;         //设置DMA传输数据量
        DMA1_Channel1->CCR |= 1 << 0;         //启用DMA通道1
        
        ADC1->CR2 |= 1 << 0;   //开启ADC1
        ADC1->CR2 |= 1 << 22;  //启用ADC1的DMA请求
    }
    

    总结:

    1. 关闭ADC连续转换模式

      ADC1->CR2 &= ~(1 << 0);
      
    • 清除ADC1控制寄存器2(CR2)的第0位,关闭连续转换模式。
    1. 禁用DMA通道1

      DMA1_Channel1->CCR &= ~(1 << 0);
      
      • 清除DMA1通道1控制寄存器(CCR)的第0位,禁用DMA通道1。
    2. 等待DMA通道1禁用完成

      while(DMA1_Channel1->CCR & (1 << 0));
      
      • 使用while循环等待,直到DMA1通道1控制寄存器的第0位被清除,确保DMA通道1已完全禁用。
    3. 设置DMA传输数据量

      DMA1_Channel1->CNDTR = cndtr;
      
      • 将传入的参数 cndtr 赋值给DMA1通道1的数据传输数量寄存器(CNDTR),指定DMA传输的数据量。
    4. 启用DMA通道1

      DMA1_Channel1->CCR |= 1 << 0;
      
      • 设置DMA1通道1控制寄存器的第0位,启用DMA通道1。
    5. 开启ADC1

      ADC1->CR2 |= 1 << 0;
      
    • 设置ADC1控制寄存器2(CR2)的第0位,开启ADC1。
    1. 启用ADC1的DMA请求
      ADC1->CR2 |= 1 << 22;
      
      • 设置ADC1控制寄存器2(CR2)的第22位,启用ADC1的DMA请求。这样,ADC转换完成后将自动通过DMA将数据传输到指定的存储位置。

    作用:

    • 启用DMA:通过DMA自动传输ADC转换的数据,减少CPU的负担,提高数据传输效率。
    • 启用ADC连续转换模式:让ADC能够连续进行多次转换,而无需每次都启动转换。-
  • ADC DMA采集中断服务函数

    void DMA1_Channel1_IRQHandler(void)
    {
        if(DMA1->ISR & (1<<1))
        {
            g_adc_dma_sta = 1;
            //清除中断标志位
            DMA1->IFCR |= (1 << 1);
        }
        
    }
    

    总结:

    1. 中断处理

      • 该函数是DMA1通道1的中断处理函数。当DMA1通道1触发中断时,系统会调用这个函数。
    2. 检查中断标志

      • if(DMA1->ISR & (1<<1)):检查DMA1的中断状态寄存器(ISR)的第1位是否被置位。这一位表示DMA1通道1的传输完成中断。如果该位被置位,说明DMA1通道1完成了数据传输。
    3. 设置状态标志

      • g_adc_dma_sta = 1;:设置一个全局标志位 g_adc_dma_sta,表示DMA传输已完成。这个标志位可以在主程序或者其他地方用来判断DMA传输状态。
    4. 清除中断标志

      • DMA1->IFCR |= (1 << 1);:清除DMA1通道1的传输完成中断标志位。这一步是必须的,否则中断处理程序会再次触发。

    作用总结:

    • 响应DMA传输完成中断:当DMA1通道1完成数据传输时,会触发中断。
    • 设置传输完成标志:通过设置 g_adc_dma_sta,主程序或其他代码可以检测到DMA传输已完成,并进行后续处理。
    • 清除中断标志位:清除中断标志位,以便系统能处理下一次DMA传输完成中断。
  • 主函数

    int main(void)
    {
        uint16_t i;
        uint16_t adcx;
        uint32_t sum;
        float temp;
    
        HAL_Init();                             /* 初始化HAL库 */
        sys_stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟, 72Mhz */
        delay_init(72);                         /* 延时初始化 */
        usart_init(115200);                     /* 串口初始化为115200 */
        led_init();                             /* 初始化LED */
        lcd_init();                             /* 初始化LCD */
        oled_init();
    
        adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
    
        oled_show_string(10,0,"ADC TEST",24);
        oled_show_string(0, 25, "CH1_VAL:", 16);
        oled_show_string(0, 41, "CH1_VOL:0.000V", 16);
        oled_refresh_gram();
        
        lcd_show_string(110, 50, 240, 16, 32, "STM32", GREEN);
        lcd_show_string(105, 90, 240, 16, 24, "ADC TEST", GREEN);
        lcd_show_string(30, 150, 240, 16, 24, "CH1_VAL:", BLUE);
        lcd_show_string(30, 180, 240, 16, 24, "CH1_VOL:0.000V", BLUE);
    
        adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动ADC DMA采集 */
    
        while (1)
        {
            if (g_adc_dma_sta == 1)
            {
                /* 计算DMA 采集到的ADC数据的平均值 */
                sum = 0;
    
                for (i = 0; i < ADC_DMA_BUF_SIZE; i++)   /* 累加 */
                {
                    sum += g_adc_dma_buf[i];
                    printf("%d\r\n", sum);
                }
    
                adcx = sum / ADC_DMA_BUF_SIZE;           /* 取平均值 */
    
                /* 显示结果 */
                lcd_show_xnum(126, 150, adcx, 5, 24, 0, BLUE);     /* 显示ADCC采样后的原始值 */
                oled_show_num(65, 25, adcx, 5, 16);
    
                temp = (float)adcx * (3.3 / 4096);                  /* 获取计算后的带小数的实际电压值,比如3.1111 */
                adcx = temp;                                        /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
                lcd_show_xnum(126, 180, adcx, 1, 24, 0, BLUE);     /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
                oled_show_num(64, 41, adcx, 1, 16);
    
                temp -= adcx;                                       /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
                temp *= 1000;                                       /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */
                lcd_show_xnum(150, 180, temp, 3, 24, 0x80, BLUE);  /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
                oled_show_num(80, 41, temp, 3, 16);
    
                g_adc_dma_sta = 0;                                  /* 清除DMA采集完成状态标志 */
                adc_dma_enable(ADC_DMA_BUF_SIZE);                   /* 启动下一次ADC DMA采集 */
            }
    
            LED0_TOGGLE();
            delay_ms(100);
            oled_refresh_gram();
        }
    }
    

    总结:

    adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
    

    初始化ADC和DMA,使得ADC可以通过DMA方式进行数据采集,采集的数据存储在 g_adc_dma_buf 缓冲区中。

    adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动ADC DMA采集 */
    

    启动ADC DMA采集,使得ADC开始连续采集数据并通过DMA传输到缓冲区 g_adc_dma_buf

    代码通过初始化和配置STM32的各种外设,实现了ADC数据的DMA采集,并在LCD和OLED上显示采集到的ADC数据和转换后的电压值。同时,通过中断和DMA机制实现了高效的数据采集和处理。

    什么时候采集,什么时候传输

    ADC采集的控制

    • ADC的连续转换模式被启用(g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;)。这意味着ADC会连续地进行转换。
    • 但是,ADC的转换触发和数据读取是通过DMA控制的。当你调用adc_dma_enable函数时,ADC和DMA都被启动,并开始进行数据采集和传输。

    DMA传输的控制

    • 当你停止DMA传输(例如,通过禁用DMA通道),ADC可能会继续进行转换,但是这些转换结果不会通过DMA传输到内存。这是因为DMA传输是负责将ADC转换结果传输到指定的内存缓冲区(g_adc_dma_buf)。
    • 如果DMA被禁用,但ADC继续进行转换,结果仍会存储在ADC的数据寄存器(DR)中,但不会被传输到内存。

5. 多通道ADC采集(DMA读取)实验

  • 功能:使用ADC1采集(DMA读取)通道1/2/3/4/5/6的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0/PA1/PA2/PA3/PA4/PA5到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。

  • 多通道ADC初始化函数

    void adc_nch_dma_init(uint32_t mar)
    {
        ADC_ChannelConfTypeDef adc_ch_conf = {0};
        __HAL_RCC_DMA1_CLK_ENABLE();   //使能DMA1时钟
          
        g_dma_adc_handle.Instance = DMA1_Channel1;   //初始化DMA1的通道1
        g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;  //数据传输方向设置为外设到内存
        g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;  //外设地址不递增
        g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;      //内存地址递增
        g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  //数据对齐使用半字
        g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;     //数据对齐使用半字
        g_dma_adc_handle.Init.Mode = DMA_NORMAL;   //传输模式配置为正常模式
        g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;    //优先级设置为中等
        HAL_DMA_Init(&g_dma_adc_handle);
        
        //2.将DMA和ADC句柄连接起来
        __HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_adc_handle);
        
                
        g_adc_nch_dma_handle.Instance = ADC1;                             //选择ADC1
        g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;        //数据对齐方式为右对齐
        g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;        //扫描模式,多个通道
        g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE;            //打开连续转换模式
        g_adc_nch_dma_handle.Init.NbrOfConversion = 6;                    //通道数选择1个
        g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE;        //禁止规则通道组间断模式  
        g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0;                //配置间断模式的规则通道个数
        g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;  //软件触发方式
        HAL_ADC_Init(&g_adc_nch_dma_handle);   //进行初始化
        
        HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle);   //校准ADC
        
        adc_ch_conf.Channel = ADC_CHANNEL_0;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
        
        adc_ch_conf.Channel = ADC_CHANNEL_1;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_2;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
        
        adc_ch_conf.Channel = ADC_CHANNEL_2;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_3;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
        
        adc_ch_conf.Channel = ADC_CHANNEL_3;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_4;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
        
        adc_ch_conf.Channel = ADC_CHANNEL_4;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_5;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
        
        adc_ch_conf.Channel = ADC_CHANNEL_5;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_6;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
        
        HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3);
        HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
        
        HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
        HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0);
    }
    

    多通道ADC转换与单通道ADC转换的区别:

    DMA的相关配置不需要改动;

    ADC的配置要使能扫描模式通道数设置为6个

    g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE;    //打开连续转换模式
    g_adc_nch_dma_handle.Init.NbrOfConversion = 6;            //通道数选择6个
    

    设置6个通道序列:

    adc_ch_conf.Channel = ADC_CHANNEL_0;                   //输入通道
    adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 //转换序列
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
    
    adc_ch_conf.Channel = ADC_CHANNEL_1;                   //输入通道
    adc_ch_conf.Rank = ADC_REGULAR_RANK_2;                 //转换序列
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
    
    adc_ch_conf.Channel = ADC_CHANNEL_2;                   //输入通道
    adc_ch_conf.Rank = ADC_REGULAR_RANK_3;                 //转换序列
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
    
    adc_ch_conf.Channel = ADC_CHANNEL_3;                   //输入通道
    adc_ch_conf.Rank = ADC_REGULAR_RANK_4;                 //转换序列
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
    
    adc_ch_conf.Channel = ADC_CHANNEL_4;                   //输入通道
    adc_ch_conf.Rank = ADC_REGULAR_RANK_5;                 //转换序列
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
    
    adc_ch_conf.Channel = ADC_CHANNEL_5;                   //输入通道
    adc_ch_conf.Rank = ADC_REGULAR_RANK_6;                 //转换序列
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);    //通道配置
    
  • ADC MSP初始化函数

    void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
    {
        if(hadc->Instance == ADC1)
        {
            GPIO_InitTypeDef gpio_init_struct;
            RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
            
            //使能时钟
            __HAL_RCC_GPIOA_CLK_ENABLE();
            __HAL_RCC_ADC1_CLK_ENABLE();
            
            //配置工作模式
            gpio_init_struct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
            gpio_init_struct.Mode = GPIO_MODE_ANALOG;  //模拟输入
            HAL_GPIO_Init(GPIOA, &gpio_init_struct);
           
            adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;  //ADC外设时钟
            adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;     //分频因子6,时钟为72M/6=12MHz  
            HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);  //设置ADC时钟
        }
    }
    
  • 使能一次ADC DMA传输函数

    void adc_dma_enable(uint16_t cndtr)
    {
        ADC1->CR2 &= ~(1 << 0);  //CONT位置1,关闭连续转换模式
        
        DMA1_Channel1->CCR &= ~(1 << 0);      //禁用DMA通道1
        while(DMA1_Channel1->CCR & (1 << 0)); //等待DMA通道1禁用完成
        
        DMA1_Channel1->CNDTR = cndtr;         //设置DMA传输数据量
        DMA1_Channel1->CCR |= 1 << 0;         //启用DMA通道1
        
        ADC1->CR2 |= 1 << 0;   //开启ADC1
        ADC1->CR2 |= 1 << 22;  //启用ADC1的DMA请求
    }
    
  • ADC DMA采集中断服务函数

    void DMA1_Channel1_IRQHandler(void)
    {
        if(DMA1->ISR & (1<<1))
        {
            g_adc_dma_sta = 1;
            //清除中断标志位
            DMA1->IFCR |= (1 << 1);
        } 
    }
    
  • 主函数

    int main(void)
    {
        uint16_t i,j;
        uint16_t adcx;
        uint32_t sum;
        float temp;
    
        HAL_Init();                                 /* 初始化HAL库 */
        sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟, 72Mhz */
        delay_init(72);                             /* 延时初始化 */
        usart_init(115200);                         /* 串口初始化为115200 */
        led_init();                                 /* 初始化LED */
        lcd_init();                                 /* 初始化LCD */
    
        adc_nch_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
    
        lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30,  70, 200, 16, 16, "ADC 6CH DMA TEST", RED);
        lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    
        lcd_show_string(30, 110, 200, 12, 12, "ADC1_CH0_VAL:", BLUE);
        lcd_show_string(30, 122, 200, 12, 12, "ADC1_CH0_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */
        
        lcd_show_string(30, 140, 200, 12, 12, "ADC1_CH1_VAL:", BLUE);
        lcd_show_string(30, 152, 200, 12, 12, "ADC1_CH1_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */
    
        lcd_show_string(30, 170, 200, 12, 12, "ADC1_CH2_VAL:", BLUE);
        lcd_show_string(30, 182, 200, 12, 12, "ADC1_CH2_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */
    
        lcd_show_string(30, 200, 200, 12, 12, "ADC1_CH3_VAL:", BLUE);
        lcd_show_string(30, 212, 200, 12, 12, "ADC1_CH3_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */
    
        lcd_show_string(30, 230, 200, 12, 12, "ADC1_CH4_VAL:", BLUE);
        lcd_show_string(30, 242, 200, 12, 12, "ADC1_CH4_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */
    
        lcd_show_string(30, 260, 200, 12, 12, "ADC1_CH5_VAL:", BLUE);
        lcd_show_string(30, 272, 200, 12, 12, "ADC1_CH5_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */
    
        adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动ADC DMA采集 */
    
        while (1)
        {
            if (g_adc_dma_sta == 1)
            {
                /* 循环显示通道0~通道5的结果 */
                for(j = 0; j < 6; j++)  /* 遍历6个通道 */
                {
                    sum = 0; /* 清零 */
                    for (i = 0; i < ADC_DMA_BUF_SIZE / 6; i++)  /* 每个通道采集了50次数据,进行50次累加 */
                    {
                        sum += g_adc_dma_buf[(6 * i) + j];      /* 相同通道的转换数据累加 */
                    }
                    adcx = sum / (ADC_DMA_BUF_SIZE / 6);        /* 取平均值 */
                    
                    /* 显示结果 */
                    lcd_show_xnum(108, 110 + (j * 30), adcx, 4, 12, 0, BLUE);   /* 显示ADC采样后的原始值 */
    
                    temp = (float)adcx * (3.3 / 4096);  /* 获取计算后的带小数的实际电压值,比如3.1111 */
                    adcx = temp;                        /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
                    lcd_show_xnum(108, 122 + (j * 30), adcx, 1, 12, 0, BLUE);   /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
    
                    temp -= adcx;                       /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
                    temp *= 1000;                       /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */
                    lcd_show_xnum(120, 122 + (j * 30), temp, 3, 12, 0X80, BLUE);/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
                }
     
                g_adc_dma_sta = 0;                      /* 清除DMA采集完成状态标志 */
                adc_dma_enable(ADC_DMA_BUF_SIZE);       /* 启动下一次ADC DMA采集 */
            }
    
            LED0_TOGGLE();
            delay_ms(100);
        }
    }
    

6. 单通道ADC过采样实验

  • 功能:使用ADC1通道1(PA1),通过软件方式实现16位分辨率采集外部电压,并在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA1到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。

  • 单通道ADC初始化函数

    void adc_dma_init(uint32_t mar)
    {
        ADC_ChannelConfTypeDef adc_ch_conf = {0};
        __HAL_RCC_DMA1_CLK_ENABLE();   //使能DMA1时钟
          
        g_dma_adc_handle.Instance = DMA1_Channel1;   //初始化DMA1的通道1
        g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;  //数据传输方向设置为外设到内存
        g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;  //外设地址不递增
        g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;      //内存地址递增
        g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  //数据对齐使用半字
        g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;     //数据对齐使用半字
        g_dma_adc_handle.Init.Mode = DMA_NORMAL;   //传输模式配置为正常模式
        g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;    //优先级设置为中等
        HAL_DMA_Init(&g_dma_adc_handle);
        
        //2.将DMA和ADC句柄连接起来
        __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);
        
                
        g_adc_dma_handle.Instance = ADC1;                             //选择ADC1
        g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;        //数据对齐方式为右对齐
        g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;        //非扫描模式,仅使用一个通道
        g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;            //打开连续转换模式
        g_adc_dma_handle.Init.NbrOfConversion = 1;                    //通道数选择1个
        g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;        //禁止规则通道组间断模式  
        g_adc_dma_handle.Init.NbrOfDiscConversion = 0;                //配置间断模式的规则通道个数
        g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;  //软件触发方式
        HAL_ADC_Init(&g_adc_dma_handle);   //进行初始化
        
        HAL_ADCEx_Calibration_Start(&g_adc_dma_handle);   //校准ADC
        
        adc_ch_conf.Channel = ADC_CHANNEL_1;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_1CYCLE_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);    //通道配置
        
        HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 3, 3);
        HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
        
        HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
        HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0);
    }
    

    与前面的实验相比,此函数只需要修改采样时间即可

    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
    
  • 主函数循环代码

    while (1)
        {
            if (g_adc_dma_sta == 1)
            {
                /* 计算DMA 采集到的ADC数据的平均值 */
                sum = 0;
    
                for (i = 0; i < ADC_DMA_BUF_SIZE; i++)   /* 累加 */
                {
                    sum += g_adc_dma_buf[i];
                }
    
                adcx = sum / (ADC_DMA_BUF_SIZE / ADC_OVERSAMPLE_TIMES); /* 取平均值 */
                adcx >>= 4;   /* 除以2^4倍, 得到12+4位 ADC精度值, 注意: 提高 N bit精度, 需要 >> N */
    
                /* 显示结果 */
                lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE);      /* 显示ADC采样后的原始值 */
    
                temp = (float)adcx * (3.3 / 65536);                 /* 获取计算后的带小数的实际电压值,比如3.1111 */
                adcx = temp;                                        /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
                lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);      /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
    
                temp -= adcx;                                       /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
                temp *= 1000;                                       /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */
                lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);   /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
    
                g_adc_dma_sta = 0;                                  /* 清除DMA采集完成状态标志 */
                adc_dma_enable(ADC_DMA_BUF_SIZE);                   /* 启动下一次ADC DMA采集 */
            }
    
            LED0_TOGGLE();
            delay_ms(100);
        }
    

    在主函数的循环代码中, 通过增加采样次数和进行移位操作,提高了ADC采样精度。

    提高精度的原理

    1. 增加采样次数

      通过增加采样次数来进行过采样。过采样可以降低噪声,提高信噪比,从而提高分辨率。将多个采样值相加并取平均值,可以增加有效位数。

    2. 移位操作

      通过移位操作,将结果右移,以提高分辨率。增加N位精度,需要对累加后的结果右移N位。

7. 内部温度传感器实验

  • 功能: 通过ADC的通道16读取STM32F103内部温度传感器的电压值,并将其转换为温度值,显示在TFTLCD屏上。LED0闪烁用于提示程序正在运行。

  • 温度计算方法:
    在这里插入图片描述

  • ADC内部温度传感器初始化函数

    void adc_temperature_init(void)
    {
        ADC_ChannelConfTypeDef adc_ch_conf;
        
        g_adc_handle.Instance = ADC1;                             //选择ADC1
        g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;        //数据对齐方式为右对齐
        g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;        //非扫描模式,仅使用一个通道
        g_adc_handle.Init.ContinuousConvMode = DISABLE;           //关闭连续转换模式
        g_adc_handle.Init.NbrOfConversion = 1;                    //通道数选择1个
        g_adc_handle.Init.DiscontinuousConvMode = DISABLE;        //禁止规则通道组间断模式  
        g_adc_handle.Init.NbrOfDiscConversion = 0;                //配置间断模式的规则通道个数
        g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;  //软件触发方式
        HAL_ADC_Init(&g_adc_handle);   //进行初始化
        
        HAL_ADCEx_Calibration_Start(&g_adc_handle);   //校准ADC
        
        adc_ch_conf.Channel = ADC_CHANNEL_16;                  //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_handle, &adc_ch_conf);    //通道配置
    }
    
  • ADC MSP初始化函数

    void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
    {
        if(hadc->Instance == ADC1)
        {
            RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
            
            //使能时钟
            __HAL_RCC_ADC1_CLK_ENABLE();
                   
            adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;  //ADC外设时钟
            adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;     //分频因子6,时钟为72M/6=12MHz  
            HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);  //设置ADC时钟
        }
    }
    
  • 使能一次ADC DMA传输函数

    uint32_t adc_get_result()
    {
        //4.启动A/D转换
        HAL_ADC_Start(&g_adc_handle);   //开启ADC
        //5.等待规则通道转换完成
        HAL_ADC_PollForConversion(&g_adc_handle, 10);   //轮询转换
        //6.获取规则通道A/D转换结果
        return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);  //返回最近一次规则组的转换结果
    }
    
  • 获取内部温度传感器的结果函数

    short adc_get_temperature(void)
    {
        uint32_t adcx;
        
        adcx = adc_get_result();
        short result;
        double temperature;
        
        temperature = adcx * (3.3 / 4096);
        
        temperature = (1.43 - temperature) / 0.0043 + 25;
        result = temperature * 100;
        
        return result;
    }
    
  • 主函数

    int main(void)
    {
        short temp;
    
        HAL_Init();                                 /* 初始化HAL库 */
        sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟, 72Mhz */
        delay_init(72);                             /* 延时初始化 */
        usart_init(115200);                         /* 串口初始化为115200 */
        led_init();                                 /* 初始化LED */
        lcd_init();                                 /* 初始化LCD */
        adc_temperature_init();                     /* 初始化ADC内部温度传感器采集 */
    
        lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30,  70, 200, 16, 16, "Temperature TEST", RED);
        lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
        lcd_show_string(30, 120, 200, 16, 16, "TEMPERATE: 00.00C", BLUE);
    
        while (1)
        {
    
            temp = adc_get_temperature();   /* 得到温度值 */
    
            if (temp < 0)
            {
                temp = -temp;
                lcd_show_string(30 + 10 * 8, 120, 16, 16, 16, "-", BLUE);   /* 显示负号 */
            }
            else
            {
                lcd_show_string(30 + 10 * 8, 120, 16, 16, 16, " ", BLUE);   /* 无符号 */
            }
            lcd_show_xnum(30 + 11 * 8, 120, temp / 100, 2, 16, 0, BLUE);    /* 显示整数部分 */
            lcd_show_xnum(30 + 14 * 8, 120, temp % 100, 2, 16, 0X80, BLUE); /* 显示小数部分 */
            
            LED0_TOGGLE();  /* LED0闪烁,提示程序运行 */
            delay_ms(250);
        }
    }
    

8. 光敏传感器实验

  • 光敏传感器简介

    光敏传感器是最常见的传感器之一,它的种类繁多,主要有:光电管、光电倍增管、光敏电阻、光敏三极管、太阳能电池、红外线传感器、紫外线传感器、光纤式光电传感器、色彩传感器、CCD和CMOS图像传感器等。光传感器是目前产量最多、应用最广的传感器之一,它在自动控制和非电量电测技术中占有非常重要的地位。

    光敏传感器是利用光敏元件光信号转换为电信号的传感器,它的敏感波长在可见光波长附近,包括红外线波长和紫外线波长。光传感器不只局限于对光的探测,它还可以作为探测元件组成其他传感器,对许多非电量进行检测,只要将这些非电量转换为光信号的变化即可。

    STM32F103战舰开发板板载了一个光敏二极管光敏电阻),作为光敏传感器,它对光的变化非常敏感。光敏二极管也叫光电二极管。光敏二极管与半导体二极管在结构上是类似的,其管芯是一个具有光敏特征的PN结,具有单向导电性,因此工作时需加上反向电压。无光照时,有很小的饱和反向漏电流,即暗电流,此时光敏二极管截止。当受到光照时,饱和反向漏电流大大增加,形成光电流,它随入射光强度的变化而变化。当光线照射PN结时,可以使PN结中产生电子一空穴对,使少数载流子的密度增加。这些载流子在反向电压下漂移(PN结中多子扩散少子漂移),使反向电流增加。因此可以利用光照强弱来改变电路中的电流

在这里插入图片描述

  • 功能:

    通过ADC3通道6(PF8)读取光敏传感器(LS1)的电压值,并转换为0~100的光线强度值,显示在LCD模块上面。光线越亮,值越大;光线越暗,值越小。

  • 硬件原理图:
    在这里插入图片描述

  • ADC单通道初始化函数

    void adc3_init(void)
    {
        ADC_ChannelConfTypeDef adc_ch_conf;
        
        g_adc_handle.Instance = ADC3;                             //选择ADC1
        g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;        //数据对齐方式为右对齐
        g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;        //非扫描模式,仅使用一个通道
        g_adc_handle.Init.ContinuousConvMode = DISABLE;           //关闭连续转换模式
        g_adc_handle.Init.NbrOfConversion = 1;                    //通道数选择1个
        g_adc_handle.Init.DiscontinuousConvMode = DISABLE;        //禁止规则通道组间断模式  
        g_adc_handle.Init.NbrOfDiscConversion = 0;                //配置间断模式的规则通道个数
        g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;  //软件触发方式
        HAL_ADC_Init(&g_adc_handle);   //进行初始化
        
        HAL_ADCEx_Calibration_Start(&g_adc_handle);   //校准ADC
        
        adc_ch_conf.Channel = ADC_CHANNEL_6;                   //输入通道
        adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 //转换序列
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; //采样时间
        HAL_ADC_ConfigChannel(&g_adc_handle, &adc_ch_conf);    //通道配置
    }
    
  • ADC MSP初始化函数

    void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
    {
        if(hadc->Instance == ADC3)
        {
            GPIO_InitTypeDef gpio_init_struct;
            RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
            
            //使能时钟
            __HAL_RCC_GPIOF_CLK_ENABLE();
            __HAL_RCC_ADC3_CLK_ENABLE();
            
            //配置工作模式
            gpio_init_struct.Pin = GPIO_PIN_8;
            gpio_init_struct.Mode = GPIO_MODE_ANALOG;  //模拟输入
            HAL_GPIO_Init(GPIOF, &gpio_init_struct);
           
            adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;  //ADC外设时钟
            adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;     //分频因子6,时钟为72M/6=12MHz  
            HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);  //设置ADC时钟
        }
    }
    
  • 获得ADC转换后的结果函数

    uint32_t adc_get_result(void)
    {
        //4.启动A/D转换
        HAL_ADC_Start(&g_adc_handle);   //开启ADC
        //5.等待规则通道转换完成
        HAL_ADC_PollForConversion(&g_adc_handle, 10);   //轮询转换
        //6.获取规则通道A/D转换结果
        return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);  //返回最近一次规则组的转换结果
    }
    
  • 读取光敏传感器值

    uint8_t lsens_get_val(void)
    {
        uint32_t temp_val;
        
        temp_val = adc_get_result();
        temp_val = temp_val / 40;
        
        if(temp_val > 100)
            temp_val = 100;
        
        return (uint8_t)(100-temp_val);
    }
    
  • 主函数

    int main(void)
    {
        uint8_t i;
        short adcx[10] = {0};
        int temp = 0;
    
        HAL_Init();                                 /* 初始化HAL库 */
        sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟, 72Mhz */
        delay_init(72);                             /* 延时初始化 */
        usart_init(115200);                         /* 串口初始化为115200 */
        led_init();                                 /* 初始化LED */
        lcd_init();                                 /* 初始化LCD */
        adc3_init();                                /* 初始化ADC3 */
    
        lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30,  70, 200, 16, 16, "LSENS TEST", RED);
        lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
        lcd_show_string(30, 110, 200, 16, 16, "LSENS_VAL:", BLUE);
    
        while (1)
        { 
            for(i = 0;i < 10; i++)
            {
                adcx[i] = lsens_get_val();
                delay_ms(5);
                temp += adcx[i];
            }
            temp = temp / 10;
            if(temp > 100)
                temp = 100;
    
            lcd_show_xnum(30 + 10 * 8, 110, temp, 3, 16, 0, BLUE); /* 显示ADC的值 */
            temp = 0;
            LED0_TOGGLE();                                         /* LED0闪烁,提示程序运行 */
            delay_ms(200);
        }
    }
    
  • 实验结果

声明:资料来源(战舰STM32F103ZET6开发板资源包)

  1. Cortex-M3权威指南(中文).pdf
  2. STM32F10xxx参考手册_V10(中文版).pdf
  3. STM32F103 战舰开发指南V1.3.pdf
  4. STM32F103ZET6(中文版).pdf
  5. 战舰V4 硬件参考手册_V1.0.pdf
  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值