“1,2,3,定时器”——跟我一起写STM32(第八期)

10 “1,2,3,定时器”

我们要周期地做一件事情,或者想要精确地定时,就要用到它——定时器。
STM32的定时器分为三种——基础定时器、通用定时器、高级定时器。
它们如同修仙等级一般,也是依次加强。
按照笔者的STM32型号(STM32F103ZET6)就有两个基本定时器(TIM6、TIM7),和两个高级定时器(TIM1、TIM8),其他的就是通用定时器。
这里对定时器的讲解非常地浅显,定时器博大精深,大家可以自行查阅资料。

10.1 基础定时器

基础定时器,顾名思义,它很基础。
定时器的最基础的核心就是计数器。

这些定时器挂在APB1或者APB2总线上

CLK就从这里而来,接着它会遇上PSC预分频器,让它不要太快,方便我们计数定时。
接着我们CNT计数器就开始从0开始加加加,达到ARR自动重装载值时,就是算溢出,时间到了,完成了一次定时。

TIM6/TIM7是16位计数器,这里的16位的意思就是:CNT寄存器是16位(0-65535),不过它的PSC预分频器也是16位(取值1-65536对应寄存器值是0-65535)。
在更新事件(计数器溢出)时,会产生中断/DMA等。
就像我们刚刚说的那样:(基础定时器只有递增计数模式)
达到ARR就溢出(这里对溢出之后的处理是清0)

知道了这些,我们就可以开始配置我们的定时器了
这里以TIM6为例:

这里选择Prescaler分频和Counter Period周期其实就是配置我们之前说的PSC寄存器和ARR寄存器。
但是由于寄存器从0开始计数,所以公式为:

  • 𝑇out是定时器溢出时间
  • 𝐹𝑡是定时器的时钟源频率
  • 𝐴𝑅𝑅是自动重装载寄存器的值
  • 𝑃𝑆𝐶是预分频器寄存器的值

其实也很好理解,如笔者上图的配置,定时器时钟源是72MHZ,则意为72000000次为1s,经过7200的分频,10000次为1s,而周期是5000溢出一次,就是溢出一次的时间为5000/10000=0.5s
(平时在配置的时候记得不要超过16位极限:65535)

接着选择Tigger Event Selection触发输出信号源选择,就是选择定时器溢出后产生什么信号:

  1. Reset:定时器复位
  2. Enable:定时器使能
  3. Update Event:更新事件(一般用于ADC、DAC等)
    这里我们并没有用定时器去触发什么外设,所以我们选择复位信号。

auto-reload preload 这个是是否采用自动重装载寄存器的预装载功能。这是定时器寄存器的一种特性:影子寄存器。我们操作某些寄存器时,它不会立即生效。是因为它拥有一个影子寄存器,实际发生作用的是影子寄存器,而我们写入寄存器的值要等到新的事件或者下一次触发才会写入影子寄存器。这个预装载设置就是把寄存器直接写入影子寄存器。一般不是要动态修改ARR的值,这个设置对定时器工作没什么影响。

接着我们使能中断:

在stm32f1xx_it.c找到TIM6_IRQHandler(void)中的HAL_TIM_IRQHandler(&htim6);
进入它的定义,找到HAL_TIM_PeriodElapsedCallback,这是一个定时溢出的中断回调函数
我们把它在一个地方重写就行了。
这里笔者是在main.c:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance==TIM6)
        led1 = !led1;
}

对了,这里关于灯的驱动为了方便,使用了位带操作,可以参考第三期

当然,一旦我们启动定时器(是需要手动启动的)
LED1就开始0.5s闪起来了

HAL_TIM_Base_Start_IT(&htim6);

Amazing,这也太简单了吧
当你想要修改它的计数周期的时候,你也可以直接操作寄存器

TIM6->ARR = 100-1;
TIM6->PSC = 7200-1;
HAL_TIM_Base_Start_IT(&htim6);

不过,直接操作寄存器是没有HAL库保护的,这可能出现一些底层的驱动错误,这需要注意(怎么这么晚才说!!!)

10.2 通用定时器

10.2.1 版本新增

一代版本一代神,通用定时器版本新增:
通用定时器相比基础定时器,先是计数模式上多了两种选择:

功能上也获得了增强:

  • 具有多路独立通道,可用于输入捕获/输出比较。
  • 可以自定义计数器时钟源。
  • 使用外部信号控制定时器且可实现多个定时器互连的同步电路
  • 支持编码器和霍尔传感器电路等

可以实现PWM输出、输入捕获、输出比较、脉冲计数等。

10.2.2 PWM输出

通用定时器输出PWM的实质就是

假设:递增计数模式
ARR:自动重装载寄存器的值
CCRx:捕获/比较寄存器x的值

当CNT < CCRx,IO输出0
当CNT >= CCRx,IO输出1

这样就可以完成一次PWM输出

我们选择TIM3作为实验的主角

让小灯作为TIM_CH2的复用,这让我们可以PWM实现呼吸灯的效果
接着配置它:

和基础定时器一样配置基础的部分
接着选择PWM模式

Pulse就是配置上图中CCRx的值,这个数除以ARR得到即为PWM占空比。
CH Polarity是配置有效电平的极性,这里选择低电平。
生成工程

开启定时器

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);

编写呼吸灯代码:

 /* USER CODE BEGIN WHILE */
	uint16_t ledrpwmval = 0;
    uint8_t dir = 1;
    while (1)
    {
        delay_ms(10);
        
        if (dir)ledrpwmval++;               /* dir==1 ledrpwmval递增 */
        else ledrpwmval--;                  /* dir==0 ledrpwmval递减 */

        if (ledrpwmval > 300)dir = 0;       /* ledrpwmval到达300后,方向为递减 */
        if (ledrpwmval == 0)dir = 1;        /* ledrpwmval递减到0后,方向改为递增 */

        /* 修改比较值控制占空比 */
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, ledrpwmval);
    /* USER CODE END WHILE */

10.2.3 输入捕获

通用定时器输入捕获的原理就是

捕获到相应触发信号,就产生事件,记录计数值。
以捕获测量高电平脉宽为例
假设:递增计数模式
ARR:自动重装载寄存器的值
CCRx1:t1时间点CCRx的值
CCRx2:t2时间点CCRx的值
我们也可以通过这些值来计算高电平的脉宽。
T = N * (ARR+1) + CCRx2

用代码呈现就是:

我们选择上升沿捕获
Input Filter是设置滤波,类似按键消抖。

按实际情况配置GPIO上下拉(mode也选不了)

生成工程

编写计数溢出中断回调函数和捕获中断回调函数:

/*cap_sta:
 * [7]  :0,没有成功的捕获;1,成功捕获到一次.
 * [6]  :0,还没捕获到上升沿;1,已经捕获到上升沿.
 * [5:0]:捕获上升沿后溢出的次数,最多溢出63次,所以最长捕获值 = 63*65536 + 65535 = 4194303
 *       注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),也只按16位使用
 *       按1us的计数频率,最长溢出时间为:4194303 us, 约4.19秒
 *
 *      (说明一下:正常32位定时器来说,1us计数器加1,溢出时间:4294秒)
 * */
u8 cap_sta = 0;    /* 输入捕获状态 */
u16 cap_val = 0;   /* 输入捕获值 */

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance==TIM5)
        if ((cap_sta & 0x80) == 0)//没有完成完整事件
            if(cap_sta & 0x40)//捕获到了上升沿
            {
                if((cap_sta & 0x3F) == 0x3F)//捕获次数超过了最大限度
                {
                    TIM_RESET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1); /* 设置极性前,一定要先清除原来的设置!! */
                    TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);//捕捉极性设置为上升沿
                    cap_sta |= 0X80;          /* 直接标记成功捕获了一次 */
                    cap_val = 0XFFFF;         /* 时间设置为最大 */
                }
                else
                {
                    cap_sta++;
                }
            }

}

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM5)
    {
        if((cap_sta & 0x80) == 0)   //没有完成完整事件
        {
            if((cap_sta & 0x40) == 0)    //第一次遇上上升沿
            {
                cap_sta = 0;
                cap_val = 0;
                cap_sta |= 0x40; //标记捕获到了上升沿
                __HAL_TIM_DISABLE(htim);//关闭定时器
                __HAL_TIM_SET_COUNTER(htim, 0);//计数器清0,重新有效记时间
                TIM_RESET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1); /* 设置极性前,一定要先清除原来的设置!! */
                TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING);//捕捉极性设置为下降沿
                __HAL_TIM_ENABLE(htim);//开启定时器
            }

            else    //第二次捕捉,判断为是下降沿
            {
                cap_sta |= 0x80;    //标记完成完整事件(即上升沿加上下降沿)
                cap_val = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);//记录这个时候的值
                TIM_RESET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1); /* 设置极性前,一定要先清除原来的设置!! */
                TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);//捕捉极性设置为上升沿
            }
        }
    }
}

这里没有申请多个变量来表示状态,而是把四个状态用cap_sta的6,7位表示,再用剩下的6位去记录ARR溢出次数。
这是单片机编程中很常见的节约内存的写法,掌握这种位的操作来节约变量不仅能节约单片机本就不多的空间,也能极大程度地锻炼我们的位运算能力,从而更好地操作寄存器。

我们不仅要使能IC中断(捕获中断),也要使能溢出中断来记录溢出次数。

 HAL_TIM_IC_Start_IT(&htim5, TIM_CHANNEL_1);
 __HAL_TIM_ENABLE_IT(&htim5, TIM_IT_UPDATE);

将得到的时间用串口打印出来

	/* USER CODE BEGIN WHILE */
	 u32 temp;

    while (1)
    {
        if(cap_sta & 0x80)
        {
            temp = cap_sta & 0x3F;
            temp *= 65535;
            temp +=cap_val;
            printf("HIGH: %ld us",temp);
            cap_sta = 0;
        }
        /* USER CODE END WHILE */
}

这样就能记录一次高电平从上升沿到下降沿的时间了。

10.2.4 输出比较

输出比较的实质也很简单:
就是一旦CNT等于CCR的值,CH输出的电平就反转一次(有很多选项,反转只是其中一种)。

有朋友发现了,这也是一个更大的PWM。
是的,这个PWM的周期或者频率由ARR决定,但占空比固定50%,相位由CCR决定。

相位是什么

上代码吧:

Mode:输出比较模式,有冻结、有效电平、翻转等等,这里选择Toggle on match,就是在计数器等于CCR时,实现CH1输出翻转。
Pulse:也就是CCR的值
其他的在PWM实验中都是一致的(其实PWM也算是输出比较的一种吧)。
生成工程

使能定时器

HAL_TIM_Base_Start(&htim4);				//使能TIM4
HAL_TIM_OC_Start(&htim4, TIM_CHANNEL_1);//使能输出比较

这样就vans,确实有点点简单。(用Cube工具生成代码的话)

当然也可以修改相位

__HAL_TIM_SET_COMPARE(&htim8, TIM_CHANNEL_1, 250);

但它不会立即生效,因为我们没有图像化配置时没有使能预加载功能。
不过我们可以用代码打开它,这样修改相位就能立刻写入影子寄存器,立刻生效了。

 __HAL_TIM_ENABLE_OCxPRELOAD(&htim4, TIM_CHANNEL_1);

10.2.5 脉冲计数

细心的朋友可能又已经发现,当我们从基础定时器切换为通用定时器时,我们的配置框也多了一些选项:

Slave Mode从模式选择


总共有5中,Disable、外部时钟模式1,复位模式,门级驱动模式,触发模式

详情可以参考:
STM32Cube学习一 TIME定时器SlaveMode设置讲解

我们使用外部时钟模式1,来记录上升沿的次数。


Trigger Source

ITRx和定时器的级联(级联:多个定时器相连)有关,我们暂时先不管它。
我们选择外部时钟模式1,就主要看TI1F_ED和TI1FP1、TI2FP2
TI1F_ED来自边沿检测器,它会被上升沿和下降沿都触发一次
而TI1FP1、TI2FP2只能记录上升沿或者下降沿。

这里我们选择TI1FP1捕捉单边沿。

详情可以参考:
【STM32技巧】(1)STM32定时器8种触发源之ITR0~ITR3说明(关于级联的)
理解通用定时器

Clock Source
计数器时钟源:
Disable,就是用内部时钟,来自APB提供的时钟
外部时钟模式1:外部输入引脚(TIx),来自定时器通道1或者通道2引脚的信号
外部时钟模式2:外部输入引脚(ETR),可以复用为TIMx_ETR的IO引脚(芯片数据手册可以查复用)
内部触发输入(ITRx),用于定时器级联

这里我们选择了外部时钟模式,这里就无法配置了。

我们配置Cube工程:

配置通道GPIO

生成工程

启动定时器:

HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_1);

编写计数代码:

/* USER CODE BEGIN WHILE */
    u16 curcnt;
    u16 oldcnt = 0;
    HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_1);
    while (1)
    {
        curcnt = __HAL_TIM_GET_COUNTER(&htim2);
        if(oldcnt != curcnt)
        {
            oldcnt = curcnt;
            printf("CNT:%d\r\n",oldcnt);
        }
/* USER CODE END WHILE */

10.2.6 输入PWM

虽说我们可以使用输入捕获来测量输入的PWM,但HAL库为了我们的方便,加了一些常用的函数和配置。
就比如测量输入PWM。
我们先用之前学的小知识,生成一个PWM

接着我们再拿一个定时器来测量它。
选择Combined Channel 中的PWM输入:

配置好时基。
剩下的基本上无法更改。
可以看出这里CH1为捕获上升沿,CH2为捕获下降沿。(也是更改不了)

生成工程

先放波

HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);

接着我们来测波
这里我们仅使能CH1,在一次中断中完成测量。

HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
HAL_TIM_IC_Start(&htim3, TIM_CHANNEL_2);

编写中断捕获回调函数

u8 cap_sta = 0;     /*1表示OK,0表示没完*/
u8 cap_hval = 0;    /* 高电平脉宽 */
u8 cap_cval = 0;    /* 周期 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM3)
    {
        if(cap_sta == 0)
            if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
            {
                cap_hval = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2) + 1 + 1;
                //第一个+1,是因为从模式RESET每次清0从0来开始,第二个+1是纠正系数
                cap_cval = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1) + 1 + 1;
                cap_sta = 1;
            }


    }
}

原理就是因为从模式设置为RESET,所以每次从0开始。
第一次捕获到上升沿开始,捕获值为0.
第二次捕获到下降沿,捕获值为u8 cap_hval ,这个过程为高电平
第三次捕获到上升沿,捕获值为u8 cap_cval ,即整个周期

我们将结果显示出来

/* USER CODE BEGIN WHILE */
    while (1)
    {
        if (cap_sta)
        {
            lcd_show_num(0,0,cap_hval,7);
            lcd_show_num(0,20,cap_cval,7);

            __HAL_TIM_DISABLE_IT(&htim3, TIM_DMA_CC1);
            cap_sta = 0;
            cap_cval = 0;
            cap_hval = 0;
            __HAL_TIM_ENABLE_IT(&htim3, TIM_DMA_CC1);
        }
    /* USER CODE END WHILE */

这里贴心地关闭了中断,是为了防止在初始化同时被中断,导致初始化失败就开始下一次捕获。

使用HAL库的宏关闭中断也有很多限制,这里我们学习正点原子的做法,使用汇编函数,进行全局中断使能或关闭。
我们在sys.c里添加汇编函数:

void __attribute__((noinline)) INTX_DISABLE(void)
{
    __asm__("cpsid i \t\n"
            "bx lr");
}

void __attribute__((noinline)) INTX_ENABLE(void)
{
    __asm__("cpsie i \t\n"
            "bx lr");
}

sys.h添加声明

void INTX_DISABLE(void);//关闭所有中断
void INTX_ENABLE(void);	//开启所有中断

于是,刚刚的初始化可以改为:

	INTX_DISABLE();
	cap_sta = 0;
	cap_cval = 0;
	cap_hval = 0;
	INTX_ENABLE();

这样,我们就完成了一次测量PWM波的周期和高电平脉宽的实验。

10.3 高级定时器

10.3.1 版本新增

一代版本一代神,高级定时器版本新增:

  • 完美继承基础定时器和通用定时器特性
  • 重复计数器
  • 死区时间带可编程的互补输出
  • 断路输入,用于将定时器的输出信号置于用户可选的安全配置中

重复计数器

不再是计数溢出即产生UEV事件了,而是提前设置好REP寄存器,每次计数溢出就是给REP寄存器减1,直到REP寄存器为0,再产生UVE事件,再中断什么的了。

互补输出

可以看见TIMx_CHx(x:1-3)除了上个版本的输出通道,还得到了互补输出TIMx_CHxN通道(但通道4没有互补通道)
DTG寄存器用于设置死区时间

断路功能

又叫刹车功能,顾名思义,可以实现出现一些情况及时刹车。

10.3.2 重复计数器

刚刚说过,计数器每次上溢或下溢都能使重复计数器减1,减到0时,再发生一次溢出就会产生更新事件。
也就是说
如果设置RCR(重复计数器)为N,更新事件将在N+1次溢出时发生

例如,我们编写输出指定个数的PWM
这里的RCR就是指定个数-1,不过笔者打算后面在程序中动态修改它,故这里先选择0。


使能更新事件中断

生成工程

这里我们要用更新事件中断,所以使能它。

 HAL_TIM_PWM_Start(&htim8, TIM_CHANNEL_1);
__HAL_TIM_ENABLE_IT(&htim8,TIM_IT_UPDATE);

编写溢出中断回调函数(这里的溢出中断事件要等RCR为0了才中断)

static u8 n = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM8)
    {
        if(n)
        {
            TIM8->RCR = n - 1;
            HAL_TIM_GenerateEvent(htim, TIM_EVENTSOURCE_UPDATE);//软件产生事件,目的是写入影子寄存器
            __HAL_TIM_ENABLE(htim);
            n = 0;
        }
        else
        {
            TIM8->CR1 &= ~(1 << 0);//不使用HAL库官方宏
        }

    }
}

这里不用HAL库官方宏的原因是__HAL_TIM_DISABLE(htim);不能直接关闭定时器,比较麻烦(其实比较安全,这里也算笔者偷懒了)

/**
  * @brief  Disable the TIM peripheral.
  * @param  __HANDLE__ TIM handle
  * @retval None
  */
#define __HAL_TIM_DISABLE(__HANDLE__) \
  do { \
    if (((__HANDLE__)->Instance->CCER & TIM_CCER_CCxE_MASK) == 0UL) \
    { \
      if(((__HANDLE__)->Instance->CCER & TIM_CCER_CCxNE_MASK) == 0UL) \
      { \
        (__HANDLE__)->Instance->CR1 &= ~(TIM_CR1_CEN); \
      } \
    } \
  } while(0)

可以看出,HAL库官方宏要判断关闭通道和互补通道才能执行关闭语句。
附上提到的寄存器的手册:

接着我们编写设置RCR值的函数

void tim_set_pwm_n(u8 _n)
{
    n = _n;
    HAL_TIM_GenerateEvent(&htim8, TIM_EVENTSOURCE_UPDATE);//软件产生事件,目的是写入影子寄存器
    __HAL_TIM_ENABLE(&htim8);
}

这里用软件产生事件,目的是快速进入下一轮事件,快速写入影子寄存器,立马生效。

 tim_set_pwm_n(5);

就能让相应的5次PWM出现了。

10.3.3 互补输出+死区控制+刹车断路

10.3.3.1 互补输出和死区控制

从图片上理解一下互补输出和死区

PWM1和PWM2就是互补输出
虚线区域就是死区时间

互补输出和死区控制一般应用在H桥,用于控制电机正反转

导通两个OC1,正转
导通两个OC1N,反转
(不能交叉导通,会断路)
但由于元器件是有延迟特性,所以需要加上死区时间控制

10.3.3.2 死区时间计算

STM32 TIM高级定时器死区时间的计算

主要就是:

  1. 确定t𝐷𝑇𝑆的值

  2. 判断DTG[7:5],选择计算公式

  3. 代入选择的公式计算

举个栗子(F1为例):DTG[7:0]=250,tDST选择4分频:
250,即二进制:1111 1010,选第四条
Tdts = 1/tDST = 1/(72000000/4)
DT = (32+26)*16*(Tdts) = (32+26)*16*(55.56) ns=51.55968us

10.3.3.3 刹车断路

使能刹车功能:将TIMx_BDTR的BKE位置1,刹车输入信号极性由BKP位设置

使能刹车功能后:由TIMx_BDTR的MOE、OSSI、OSSR位,
TIMx_CR2的OISx、OISxN位,TIMx_CCER的CCxE、CCxNE位控制OCx和OCxN输出状态
无论何时,OCx和OCxN输出都不能同时处在有效电平

  1. MOE位被清零,OCx和OCxN为无效、空闲或复位状态(OSSI位选择)
  2. OCx和OCxN的状态:由相关控制位状态决定,
    当使用互补输出时:根据情况自动控制输出电平,参考参考手册使用刹车(断路)功能小节
  3. BIF位置1,如果使能了BIE位,还会产生刹车中断;如果使能了TDE位,会产生DMA请求
  4. 如果AOE位置 1,在下一个 更新事件UEV时,MOE位被自动置 1


10.3.3.3 Cube的快速配置

基本上STM32Cube都能为我们生成大半个江山,所以重点就到了配置STM32Cube上了。


CKD=4,不同于PSC=4(这个语句 不是对频率进行分频,分频是对于PSC的控制。这个只是选择后面tim涉及到的滤波器的时钟信号。比如1的话,就认为滤波器的时钟等于定时器时钟)
具体应用:
STM32 时钟分割TIM_ClockDivision配置及使用详细说明
接着使能BRK刹车断路
设置极性为高电平
Automatic Output State:刹车结束后自动恢复输出
设置死区时间为100
(选择公式一:DT=5556ns=5.556us)

通道有效电平为高电平
设置两个输出的空闲状态(刹车后的状态):四个都为低电平,这样H桥就不导通了。

不过,我们之前把刹车引脚极性设置为高电平,为了让它不默认刹车断路,我们还要配置GPIO

将刹车引脚PA6设置为下拉

生成工程

同时使能互补输出:

HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);

看一下效果:


至此,我们便完成了对高级定时器互补输出+死区控制+刹车断路的粗浅认识了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值