drivers_day08

回顾:

谈谈中断:

1.为什么有中断

轮询

中断

2.中断硬件触发过程

中断控制器

3.中断的处理流程

异常向量表

保存现场

执行服务程序

恢复现场

4.linux中断编程

内核已经实现的内容:

异常向量表

保存现场

恢复现场

驱动开发关注内容:

申请中断硬件资源:中断号

注册中断对应的服务程序:中断处理函数

reqeust_irq/free_irq

5.linux内核对中断处理函数的要求:

中断不属于进程,独立于进程,不参与进程的调度切换;中断不能直接和用户进行数据的交互,需要配合系统调用!

linux内核对于硬件中断无优先级;

linux内核硬件中断优先级高于软件中断,软件中断又高于进程

,所以如果某一个硬件中断的处理函数长时间的占有CPU资源,导致别的硬件中断,软中断,进程就无法获取CPU资源,影响linux系统的并发能力和响应能力。

linux内核要求中断的处理函数执行的速度要快!

linux内核要求中断的处理函数不能做阻塞动作!

6.在某些时候中断处理函数的执行时间可能会很长,为了提供系统的响应能力和并发能力,这时需要考虑使用linux内核提供的顶半部和底半部机制来优化中断的执行过程。

顶半部:其实就是中断处理函数,做一些耗时较少,紧急的事情,其他耗时的内容让底半部来做!这个过程是不可中断的!

当然还要做登记底半部的内容,就是告诉CPU,顶半部执行完毕以后,请你在将来适当的时候去处理底半部的内容。

底半部:做原先中断处理函数中较为耗时,不紧急的内容,这个过程可以被打断!

底半部的实现机制:

1.tasklet

实现基于软中断,并且能够指定优先级!运行中断上下文中,tasklet对应的延时函数不能进行休眠动作!

2.工作队列

运行进程上下文中,所以对应的延时函数能够进行休眠操作。

3.软中断

软中断对应的延时函数能够同时运行在多个CPU上,而tasklet不行;

软中断对应的延时函数必须设计为可重入!

软中断的实现不能以模块的形式动态加载和卸载!

 

****************************************************************************************

Day 08

硬件定时器:一般硬件定时器集成在CPU的内部,有的可以使用外置的硬件定时器芯片;

特点:

可以人为通过编程来设置硬件定时器的工作频率;

硬件定时器一旦设定好了工作频率,只要上电,那么硬件定时器就会周期性的给CPU输出一个中断信号,称这个中断信号为时钟中断;

linux内核已经实现好了时钟中断对应的服务程序,这个服务程序也称之为时钟中断服务函数;

既然硬件定时器周期性的给CPU产生时钟中断,那么对应的中断服务程序就会被内核周期性的调用;

时钟中断服务函数做如下内容:

1.更新系统的运行时间,更新jiffies_64(jiffies)

2.更新实际时间

3.检查当前进程的时间片是否用完,决定是否需要重新调度新进程

4.检查是否有超时的软件定时器,如果有处理这个超时的软件定时器

5...

 

概念:

HZ:常数,最终会将这个常数设置为硬件定时器的工作频率,对于ARM平台,HZ=100,表明一秒钟产生100次的时钟中断;

tick:1/HZ,表明产生一次时钟中断的时间间隔为10ms

jiffies:是linux内核的全局变量,在内核任何一个文件中都能访问这个变量(unsignedlong),它用来记录自开机以来发生了多少次时钟中断,没发生一次时钟中断,jiffies加1;一般linux内核用jiffies来表示时间!例如:

unsigned long timeout = jiffies + HZ/2;

jiffies:表示当前系统运行时间;

HZ/2:表示时间间隔为500ms

timeout:表示500ms以后的系统运行时间;

 

注意:

jiffies是一个32位的变量,如果HZ=100,497天以后就会发生溢出(回绕)问题,一般内核使用jiffies_64(64位的变量)用来记录系统运行时间,而jiffies用来描述时间间隔。

jiffies = jiffies_64 & 0xFFFFFFFF;

 

回绕问题:

 unsigned long timeout = jiffies + HZ/2; //加上HZ/2timeout有可能发生回绕

//执行一些任务

//检查是否花的时间过长

if (timeout > jiffies) {//此时的jiffies仍然很大,但由于发生回绕timeout很小

//没有超时,很好

} else {

//超时了,发生错误

}

linux内核解决jiffies回绕问题使用:

time_after/time_before

#define time_after(a,b)((long)(b) - (long)(a) < 0))

#define time_before(a,b)((long)(a) - (long)(b) < 0))

切记:硬件定时器处理的最小时间为10ms,那么jiffies它能处理的时间也就是10ms。

 

***********************************************************************************

linux内核软件定时器

1.linux内核描述定时器使用的数据结构

struct timer_list {

       unsigned long expires; //定时器的超时时间,例如如果设置超时时间的间隔为5秒;expires = jiffies + 5*HZ

       void (*function)(unsigned long);//定时器的处理函数,当超时时间到期,内核就会执行定时器的处理函数,定时器到期内核会将定时器删除,也就是说定时器的处理函数只执行一次;

       unsigned long data; //给定时器处理函数传递的参数,一般传递指针

};

 

如何使用内核定时器?

1.分配定时器对象

   struct timer_list mytimer;

2.初始化定时器对象

   init_timer(struct timer *timer);

   函数功能:初始化定时器

   参数:分配的定时器对象指针

   注意:这个函数不会初始化expires,function,data这个三个字段,这三个字段需要程序员自己去指定,例如:

  init_timer(&mytimer);

  mytimer.expires = jiffies + 5*HZ

  mytimer.function = mytimer_funtion;

  mytimer.data = (unsigned long)&mydata;

3.向内核添加注册定时器

   add_timer(&mytimer); //一旦添加完毕,内核就开始对这个定时器进行倒计时!时钟中断处理函数每隔10ms检查一次定时器是否到期,如果到期,内核执行对应的定时器处理函数,并且将定时器进行删除。

4.删除定时器

   del_timer(&mytimer); //定时器到期,内核会帮你删除定时器,如果定时器没到期,可以使用此方法进行删除。

5.修改定时器的超时时间

  mod_timer(&mytimer, jiffies + 2*HZ); //设置定时器的超时时间为2秒以后

   相当于= del_timer 先将原先定时器删除

   +expires = jiffies + 2*HZ  设置新的超时时间

   +add_timer  重新添加定时器

注意:千万不能用以上三步骤来实现mod_timer,以上三步骤的执行路径不是原子的,有可能被打断!

 

6.注意:定时器的实现基于软中断,所以定时器的处理函数同样不能进行休眠操作!

 

7.如果想让定时器的处理函数重复执行,循环执行,只需在定时器处理函数中重新添加定时器即可!

 

案例1:利用定时器,每隔2秒钟打印"hello ,tarena"

案例2:利用定时器,每个2秒开关灯;

案例3:利用模块参数的知识(不能采用字符设备驱动框架)来动态修改灯的闪烁频率;2000ms,1000ms,500ms,200ms.

注意:加载完驱动模块以后,无需卸载的情况下,动态修改闪烁的频率!提示:权限为0或者非0

 

实验步骤:

insmod mytimer_drv.ko speed = 2000 //2秒闪烁

echo 500 >/sys/module/mytimer_drv/parameters/speed //500ms

 

毫秒和jiffies转换:

unsigned long timeout = jiffies +msecs_to_jiffies(500);

unsigned long timeout = jiffies + HZ/2;

*********************************************************

linux内核延时函数:

"延时":忙等待,休眠等待

忙等待:CPU不干别的事,原地空转;

休眠等待:休眠只针对进程,休眠是让进程休眠,而非CPU休眠,CPU有可能去处理中断,有可能在处理别的进程;

 

忙等待涉及的函数:

void ndelay(unsigned long nsecs); //纳秒级延时

void udelay(unsigned long usecs);//微秒级延时

void mdelay(unsigned long msecs);//微秒级延时

参数都是延时的时间间隔!

这三个函数的实现并不依赖于硬件定时器,因为硬件定时器处理的时间最小是10ms。利用BogoMIPS,这个参数通过cat /proc/cpuinfo来查看,表明一秒钟执行多少个百万指令集;

注意:如果延时的时间大于10ms,一般不再使用mdelay.

 

休眠设计的延时函数:

msleep:进程休眠,进程不可打断(休眠期间不处理信号),一旦休眠结束会处理之前的信号

msleep_interruptible:进程休眠,进程休眠期间可被信号中断

ssleep:和msleep一样!

 

利用jiffies和定时器来实现延时;

 

schedule_timeout(5*HZ);进程休眠,指定超时时间为5秒

********************************************************************************

linux内核并发和竞态:

1.概念:

并发:多个执行单元同时发生;

注意:执行单元包括硬件中断,软件中断,多进程

竞态:并发的多个执行单元同时访问共享资源引起的竞争状态

形成竞态三个条件:

一定要有并发情况

一定要有共享资源

       硬件资源(小到寄存器的某个bit位)

       软件上的全局变量,例如open_cnt

并发的多个执行单元要同时访问共享资源

解决办法:

互斥访问:当多个执行单元对共享资源进行访问时,只能允许一个执行单元对共享资源进行访问其他执行单元被禁止访问!

临界区访问共享资源的代码区域,所以互斥访问就是对临界区的互斥访问!例如:

     

由于linux内核支持进程之间的抢占,当A在执行以上临界区时,一定要进行互斥访问,不让B进程发生抢占情况,保证A的执行路径不被打断!      

总结:互斥访问本质上其实就是让临界区的执行路径具有原子性(不能被打断!)

 

问题:如何做到一个执行单元在访问临界区时,其他执行单元不能打断正在访问临界区的执行单元的路径呢?

强调:执行单元(包括硬件中断和软中断和进程)

 

2.linux内核产生竞态的情形:

第一种情形是多核(多CPU),多个CPU它们共享系统总线,共享内存,外存,系统IO,导致竞态;

第二种情形是CPU的进程之间的抢占必须具备抢占,抢占的原因是进程能够指定优先级),由于linux内核支持进程的抢占,多个进程访问共享资源,并且有抢占,也会导致竞态;(多进程没有抢占,则不会有竞态)

第三种情形是中断(硬件中断,软中断)和进程也会形成竞态(中断的优先级高于进程)

第四种情形是中断和中断。硬件中断的优先级高于软中断,软中断又分(2个)优先级!

 

3.linux内核解决竞态的方法:

这些方法的本质目的就是让临界区的访问具有原子性!

1.中断屏蔽:屏蔽掉硬件中断和软中断

   能够解决的竞态情形:

   进程与进程的之间的抢占(由于linux进程的调度,抢占都是基于软中断来实现)

    中断和进程

    中断和中断

   linux内核提供了相关的中断屏蔽的方法:

    1.屏蔽中断

      unsigned long flags;

      local_irq_disable(); //屏蔽中断

     local_irq_save(floags);//屏蔽中断并且保存中断状态到flags中

    2.使能中断

     local_irq_enable(); //使能中断

     local_irq_restore(flags); //使能中断,并且从flags中恢复屏蔽中断前保存的中断状态

   linux内核中断屏蔽使用方法:

    1.临界区之前屏蔽中断

    2.执行临界区,中断不能打断,进程的抢占也不能发生

    3.使能中断

    例如:

       staticint open_cnt = 1;

       intuart_open(struct inode *inode,

                     structfile *file)

       {

              unsigned long flags;

              local_irq_save(flags);//屏蔽中断

              if (--open_cnt != 0) {

                     printk(“已被打开!\n”);

                     open_cnt++;

              local_irq_restore(flags);//使能中断

                     return–EBUSY;

              }

              local_irq_restore(flags);//使能中断     

              printk(“打开成功\n”);

              return 0;

       }

 

2.原子操作

3.自旋锁

4.信号量

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

#include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <avr/eeprom.h> #include <string.h> #define delay_ms(x) _delay_ms(x) // LCD 相关引脚定义 #define LCD_RS PC0 #define LCD_RW PC1 #define LCD_E PC2 #define LCD_D4 PA4 #define LCD_D5 PA5 #define LCD_D6 PA6 #define LCD_D7 PA7 // 按键引脚定义(使用PORTD) #define SET_BUTTON PIND0 #define ADD_BUTTON PIND1 #define SUB_BUTTON PIND2 // LED 引脚定义 #define LED_PIN PB3 // EEPROM存储地址 #define EEPROM_TIME_HOUR_ADDR 0x00 #define EEPROM_TIME_MINUTE_ADDR 0x01 #define EEPROM_DATE_YEAR_ADDR 0x02 #define EEPROM_DATE_MONTH_ADDR 0x03 #define EEPROM_DATE_DAY_ADDR 0x04 // 全局变量 volatile uint8_t hour = 12; // 小时(0-23) volatile uint8_t minute = 0; // 分钟(0-59) volatile uint8_t second = 0; // 秒(0-59) volatile uint8_t year = 23; // 年份(后两位) volatile uint8_t month = 1; // 月份(1-12) volatile uint8_t day = 1; // 日期(1-31) volatile uint8_t set_mode = 0; // 0:正常显示, 1:设置小时, 2:设置分钟, 3:设置年, 4:设置月, 5:设置日 volatile uint8_t blink_state = 0; // 闪烁状态 volatile uint8_t timer1_counter = 0; // 定时器计数器 // 闹钟结构 typedef struct { uint8_t hour; uint8_t minute; uint8_t enabled; } Alarm; Alarm alarm1 = {8, 0, 0}; // 闹钟1,默认8:00,关闭 Alarm alarm2 = {12, 0, 0}; // 闹钟2,默认12:00,关闭 // LCD初始化 void LCD_Init() { // 设置数据端口为输出 DDRA |= (1<<LCD_D4) | (1<<LCD_D5) | (1<<LCD_D6) | (1<<LCD_D7); // 设置控制端口为输出 DDRC |= (1<<LCD_RS) | (1<<LCD_RW) | (1<<LCD_E); // 初始化序列 delay_ms(50); // 等待LCD上电稳定 // 4位模式初始化 LCD_Write_Command(0x33); LCD_Write_Command(0x32); LCD_Write_Command(0x28); // 4位模式,2行显示,5x8点阵 LCD_Write_Command(0x0C); // 显示开,光标关,闪烁关 LCD_Write_Command(0x06); // 增量模式,不移位 LCD_Write_Command(0x01); // 清屏 delay_ms(2); } // 向LCD发送命令 void LCD_Write_Command(unsigned char cmd) { PORTC &= ~(1<<LCD_RS); // RS=0 命令模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (cmd & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((cmd << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 向LCD发送数据 void LCD_Write_Data(unsigned char data) { PORTC |= (1<<LCD_RS); // RS=1 数据模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (data & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((data << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 在LCD上显示字符串 void LCD_Display_String(char *str) { while (*str) { LCD_Write_Data(*str++); } } // 设置LCD光标位置 void LCD_Set_Cursor(uint8_t row, uint8_t col) { uint8_t address; if (row == 0) { address = 0x80 + col; } else { address = 0xC0 + col; } LCD_Write_Command(address); } // 显示时间 void Display_Time() { char buffer[16]; // 第一行显示时间 LCD_Set_Cursor(0, 4); sprintf(buffer, "%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); // 第二行显示日期 LCD_Set_Cursor(1, 4); sprintf(buffer, "20%02d-%02d-%02d", year, month, day); LCD_Display_String(buffer); } // 显示设置模式 void Display_Set_Mode() { char buffer[16]; LCD_Set_Cursor(0, 0); LCD_Display_String("Set:"); switch(set_mode) { case 1: // 设置小时 if(blink_state) { sprintf(buffer, "Hour: %02d", hour); } else { sprintf(buffer, "Hour: "); } break; case 2: // 设置分钟 if(blink_state) { sprintf(buffer, "Minute:%02d", minute); } else { sprintf(buffer, "Minute: "); } break; case 3: // 设置年 if(blink_state) { sprintf(buffer, "Year: 20%02d", year); } else { sprintf(buffer, "Year: "); } break; case 4: // 设置月 if(blink_state) { sprintf(buffer, "Month: %02d", month); } else { sprintf(buffer, "Month: "); } break; case 5: // 设置日 if(blink_state) { sprintf(buffer, "Day: %02d", day); } else { sprintf(buffer, "Day: "); } break; default: sprintf(buffer, "Normal Mode "); } LCD_Set_Cursor(0, 5); LCD_Display_String(buffer); // 仍然显示时间日期 LCD_Set_Cursor(1, 0); sprintf(buffer, "Time:%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); } // 从EEPROM加载时间日期 void Load_Time_From_EEPROM() { hour = eeprom_read_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR); minute = eeprom_read_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR); year = eeprom_read_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR); month = eeprom_read_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR); day = eeprom_read_byte((uint8_t*)EEPROM_DATE_DAY_ADDR); // 检查读取的值是否有效 if(hour > 23) hour = 0; if(minute > 59) minute = 0; if(year > 99) year = 23; if(month == 0 || month > 12) month = 1; if(day == 0 || day > 31) day = 1; } // 保存时间日期到EEPROM void Save_Time_To_EEPROM() { eeprom_update_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR, hour); eeprom_update_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR, minute); eeprom_update_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR, year); eeprom_update_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR, month); eeprom_update_byte((uint8_t*)EEPROM_DATE_DAY_ADDR, day); } // 初始化定时器1 (1秒中断) void Timer1_Init() { // 设置定时器1为CTC模式 TCCR1B |= (1 << WGM12); // 设置预分频为1024 TCCR1B |= (1 << CS12) | (1 << CS10); // 设置比较值 (16MHz/1024 = 15625 ticks/sec, 15625 ticks = 1秒) OCR1A = 15625; // 启用比较匹配中断 TIMSK |= (1 << OCIE1A); } // 初始化按键引脚 void Buttons_Init() { // 设置按键引脚为输入,启用上拉电阻 DDRD &= ~((1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON)); PORTD |= (1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON); } // 按键扫描 void Key_Scan() { static uint8_t last_set_state = 1; static uint8_t last_add_state = 1; static uint8_t last_sub_state = 1; uint8_t current_set_state = PIND & (1<<SET_BUTTON); uint8_t current_add_state = PIND & (1<<ADD_BUTTON); uint8_t current_sub_state = PIND & (1<<SUB_BUTTON); // 检测设置按键按下 if(last_set_state && !current_set_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SET_BUTTON))) { set_mode++; if(set_mode > 5) set_mode = 0; if(set_mode == 0) { // 退出设置模式,保存时间到EEPROM Save_Time_To_EEPROM(); } } } // 只在设置模式下检测加减按键 if(set_mode > 0) { // 检测增加按键按下 if(last_add_state && !current_add_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<ADD_BUTTON))) { switch(set_mode) { case 1: // 增加小时 hour = (hour + 1) % 24; break; case 2: // 增加分钟 minute = (minute + 1) % 60; break; case 3: // 增加年 year = (year + 1) % 100; break; case 4: // 增加月 month = (month % 12) + 1; break; case 5: // 增加日 day = (day % 31) + 1; break; } } } // 检测减少按键按下 if(last_sub_state && !current_sub_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SUB_BUTTON))) { switch(set_mode) { case 1: // 减少小时 hour = (hour == 0) ? 23 : hour - 1; break; case 2: // 减少分钟 minute = (minute == 0) ? 59 : minute - 1; break; case 3: // 减少年 year = (year == 0) ? 99 : year - 1; break; case 4: // 减少月 month = (month == 1) ? 12 : month - 1; break; case 5: // 减少日 day = (day == 1) ? 31 : day - 1; break; } } } } last_set_state = current_set_state; last_add_state = current_add_state; last_sub_state = current_sub_state; } // 检查闹钟 void Check_Alarms() { static uint8_t alarm1_triggered = 0; static uint8_t alarm2_triggered = 0; // 检查闹钟1 if(alarm1.enabled && !alarm1_triggered && hour == alarm1.hour && minute == alarm1.minute && second == 0) { alarm1_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 检查闹钟2 if(alarm2.enabled && !alarm2_triggered && hour == alarm2.hour && minute == alarm2.minute && second == 0) { alarm2_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 每分钟重置闹钟触发标志 if(second == 0) { alarm1_triggered = 0; alarm2_triggered = 0; } // 如果LED亮起,检测按键关闭 if(PORTB & (1<<LED_PIN)) { if(!(PIND & (1<<SET_BUTTON)) || !(PIND & (1<<ADD_BUTTON)) || !(PIND & (1<<SUB_BUTTON))) { PORTB &= ~(1<<LED_PIN); // 关闭LED } } } // 定时器1比较匹配中断服务程序 ISR(TIMER1_COMPA_vect) { // 更新时间 second++; if(second >= 60) { second = 0; minute++; if(minute >= 60) { minute = 0; hour++; if(hour >= 24) { hour = 0; // 日期增加逻辑 day++; uint8_t max_day = 31; // 处理不同月份的天数 if(month == 4 || month == 6 || month == 9 || month == 11) { max_day = 30; } else if(month == 2) { // 简单处理2月天数(不考虑闰年) max_day = 28; } if(day > max_day) { day = 1; month++; if(month > 12) { month = 1; year++; if(year > 99) year = 0; } } } } } // 更新闪烁状态 (0.5秒周期) timer1_counter++; if(timer1_counter >= 5) { // 10次 = 1秒 (0.1秒中断) timer1_counter = 0; blink_state = !blink_state; } // 检查闹钟 Check_Alarms(); } int main(void) { // 初始化端口 DDRB |= (1<<LED_PIN); // LED引脚为输出 PORTB &= ~(1<<LED_PIN); // 初始关闭LED // 初始化按键 Buttons_Init(); // 初始化LCD LCD_Init(); // 从EEPROM加载时间 Load_Time_From_EEPROM(); // 初始化定时器 Timer1_Init(); // 启用全局中断 sei(); // 清屏并显示欢迎信息 LCD_Write_Command(0x01); LCD_Set_Cursor(0, 3); LCD_Display_String("AVR Clock"); LCD_Set_Cursor(1, 2); LCD_Display_String("Initializing..."); delay_ms(1000); LCD_Write_Command(0x01); while(1) { // 扫描按键 Key_Scan(); // 更新显示 if(set_mode == 0) { Display_Time(); } else { Display_Set_Mode(); } // 短暂延迟 delay_ms(100); } return 0; }使用的atmega128单片机,不改动引脚的使用
05-27
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值