Linux设备驱动程序设计

Linux设备驱动程序设计(一) Linux驱动程序概述 Linux设备驱动程序是为特定的硬件提供给应用程序的一组标准化接口,它隐藏了设备工作的细节。用户程序通过标准化系统调用,这些调用和特定的硬件是无关的,再由Linux内核调用特定的设备驱动程序操作和控制特定的实际的硬件设备。其中,Linux系统设备分为三种类型: 字符设备(character device) 块设备(block device) 网络接口(network interface) 本篇文章中,主要谈论字符设备驱动程序的设计。 字符设备是能够像字节流一样被访问的设备,一般不 阅读详情

Linux设备驱动的基本概念

      系统调用是操作系统内核和应用程序之间的接口;设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下功能:

1、对设备初始化和释放。

2、把数据从内核传送到硬件和从硬件读取数据。

3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据。

4、检测和处理设备出现的错误。

   在Linux操作系统下有两类主要的设备文件类型:字符设备和块设备。

   它们的区别是:在对字符设备发出读/写请求时,实际的硬件I/O就紧接着发生了。块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。

   用户进程是通过设备文件来与实际的硬件打交道,每个设备文件都有其文件属性(c/b),表示是字符设备还是块设备。

   每个文件都有两个设备号,第一个是主设备号,标识驱动程序;第二个是次设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用次设备号来区分它们。设备文件的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到设备驱动程序。

    注意:在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度,也就是说,系统必须再驱动程序的子函数返回后才能进行其他的工作。如果驱动程序陷入死循环,那么整个内核系统就会崩溃只有重新启动机器。

Linux设备驱动程序设计实例

Linux系统中,设备驱动程序是操作系统内核的重要组成部分,在与硬件设备之间建立了标准的抽象接口。通过这个接口,用户可以像处理普通文件一样,对硬件设备进行打开(open)、关闭(close)、读写(read/write)等操作。通过分析和设计设备驱动程序,可以深入理解Linux系统和进行系统开发。本文通过一个简单的例子来说明设备驱动程序的设计。

1、程序清单

#ifndef __KERNEL__
# define __KERNEL__           //按内核模块编译
#endif
#ifndef MODULE
# define MODULE        //设备驱动程序模块编译
#endif
#define DEVICE_NAME "MyDev"   
#define OPENSPK 1
#define CLOSESPK 2

//必要的头文件
#include <linux/module.h> //同kernel.h,最基本的内核模块头文件
#include <linux/kernel.h> //同module.h,最基本的内核模块头文件
#include <linux/sched.h>  //这里包含了进行正确性检查的宏
#include <linux/fs.h>    //文件系统所必需的头文件
#include <asm/uaccess.h> //这里包含了内核空间与用户空间进行数据交换时的
                            函数宏

#include <asm/io.h>    //I/O访问

int my_major=0;         //主设备号
static int Device_Open=0;
static char Message[]="This is from device driver";
char *Message_Ptr;

int my_open(struct inode *inode, struct file *file)
{//每当应用程序用open打开设备时,此函数被调用
 printk ("ndevice_open(%p,%p)n", inode, file);
 if (Device_Open)
  return -EBUSY;   //同时只能由一个应用程序打开
 Device_Open++;
 MOD_INC_USE_COUNT;  //设备打开期间禁止卸载
 return 0;
}

static void my_release(struct inode *inode, struct file *file)
{//每当应用程序用close关闭设备时,此函数被调用
 printk ("ndevice_release(%p,%p)n", inode, file);
 Device_Open --;
 MOD_DEC_USE_COUNT;  //引用计数减1
}

ssize_t my_read (struct file *f,char *buf,int size,loff_t off)
{//每当应用程序用read访问设备时,此函数被调用
  int bytes_read=0;    
#ifdef DEBUG
  printk("nmy_read is called. User buffer is %p,
size is dn",buf,size);
#endif
if (verify_area(VERIFY_WRITE,buf,size)==-EFAULT) 
  return -EFAULT;
    Message_Ptr=Message;
    while(size && *Message_Ptr)
    {    
        if(put_user(*(Message_Ptr++),buf++)) //写数据到用户空间
            return -EINVAL;
        size --;
        bytes_read++;
    }
    return bytes_read; 
}

ssize_t my_write (struct file *f,const char *buf, int size,loff_t off)
{//每当应用程序用write访问设备时,此函数被调用
  int i;
    unsigned char uc;
#ifdef DEBUG
 printk("nmy_write is called. User buffer is %p,size is dn",buf,size);
#endif
if (verify_area(VERIFY_WRITE,buf,size)==-EFAULT) 
return -EFAULT;
    printk("nData below is from user program:n");
    for (i=0;i<size;i++)
        if(!get_user(uc,buf++))       //从用户空间读数据
            printk("%02x ",uc);
    return size; 
}

int my_ioctl(struct inode *inod,struct file *f,unsigned int arg1,
             unsigned int arg2)
{//每当应用程序用ioctl访问设备时,此函数被调用
#ifdef DEBUG
  printk("nmy_ioctl is called. Parameter is %p,size is %dn",arg1);
#endif
  switch (arg1)
   {
    case OPENSPK:
        printk("nNow,open PC's speaker.n");
        outb(inb(0x61)|3,0x61); //打开计算机的扬声器
        break;
    case CLOSESPK:
        printk("nNow,close PC's speaker.");
        outb(inb(0x61)&0xfc,0x61);//关闭计算机的扬声器
        break;
   }
}

struct file_operations my_fops = {
  NULL,     /* lseek */
  my_read,
  my_write,
  NULL,
  NULL,
  my_ioctl,
  NULL,
  my_open,
  my_release,
 /* nothing more, fill with NULLs */
};

int init_module(void)
{//每当装配设备驱动程序时,系统自动调用此函数
  int result;
  result = register_chrdev(my_major,DEVICE_NAME,&my_fops); 
  if (result < 0) return result;
  if (my_major == 0) 
     my_major = result; 
     printk("nRegister Ok. major-number=%dn",result);
     return 0;
}

void cleanup_module(void)
{//每当卸载设备驱动程序时,系统自动调用此函数
  printk("nunloadn");
   unregister_chrdev(my_major, DEVICE_NAME);
}

2、设备驱动程序设计

    Linux设备分为字符设备、块设备和网络设备。字符设备是不需要缓冲而直接
读写的设备,如串口、键盘、鼠标等,本例就是字符设备驱动程序;块设备的访问
通常需要缓冲来支持,以数据块为单位来读写,如磁盘设备等;网络设备是通过套
接字来访问的特殊设备。

1) 设备驱动程序和内核与应用程序的接口
    无论哪种类型的设备,Linux都是通过在内核中维护特殊的设备控制块来与设
备驱动程序接口的。在字符设备和块设备的控制块中,有一个重要的数据结构
file_operations,该结构中包含了驱动程序提供给应用程序访问硬件设备的各种
方法,其定义如下(参见fs.h):

struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);    
//响应应用程序中lseek调用的函数指针
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
//响应应用程序中read调用的函数指针
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
//响应应用程序中write调用的函数指针
int (*readdir) (struct file *, void *, filldir_t);
//响应应用程序中readdir调用的函数指针
unsigned int (*poll) (struct file *, struct poll_table_struct *);
//响应应用程序中select调用的函数指针
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
//响应应用程序中ioctl调用的函数指针
int (*mmap) (struct file *, struct vm_area_struct *);
//响应应用程序中mmap调用的函数指针
int (*open) (struct inode *, struct file *);  
//响应应用程序中open调用的函数指针
int (*flush) (struct file *);       
int (*release) (struct inode *, struct file *); 
//响应应用程序中close调用的函数指针
int (*fsync) (struct file *, struct dentry *); 
int (*fasync) (int, struct file *, int);
int (*check_media_change) (kdev_t dev);
int (*revalidate) (kdev_t dev);
int (*lock) (struct file *, int, struct file_lock *);
};

多数情况下,只需为上面结构中的少数方法编写服务函数,其他均设为NULL即可。

    每一个可装配的设备驱动程序都必须有init_module和cleanup_module两个函数,装载和卸载设备时内核自动调用这两个函数。在init_module中,除了可以对硬件设备进行检查和初始化外,还必须调用register_* 函数将设备登记到系统中。本例中是通过register_chrdev来登记的,如果是块设备或网络设备则应该用
register_blkdev和register_netdev来登记。Register_chrdev 的主要功能是将设
备名和结构file_operations登记到系统的设备控制块中。

2) 与应用程序的数据交换
    由于设备驱动程序工作在内核存储空间,不能简单地用"="、"memcpy"等方法与应用程序交换数据。在头文件uaccess.h中定义了方法put_user(x, ptr)和
get_user(x, ptr),用于内核空间与用户空间的数据交换。值x的类型根据指针
ptr的类型确定,请参见源代码中的my_read与my_write函数。

3) 与硬件设备的接口

    Linux中为设备驱动程序访问I/O端口、硬件中断和DMA提供了简便方法,相应的头文件分别为io.h、irq.h、dma.h。由于篇辐限制,本例中只涉及到I/O端口访问。Linux提供的I/O端口访问方法主要有:inb()、inw()、outb()、outw()、inb_p()、inw_p()、outb_p()、outw_p()等。要注意的是,设备驱动程序在使用端口前,应该先用check_region()检查该端口的占用情况,如果指定的端口可用,则再用request_region()向系统登记。说明check_region()、request_region()的头文件是ioport.h。

4) 内存分配

    设备驱动程序作为内核的一部分,不能使用虚拟内存,必须利用内核提供的
kmalloc()与kfree()来申请和释放内核存储空间。Kmalloc()带两个参数,第一个
要申请的是内存数量,在早期的版本中,这个数量必须是2的整数幂,如128、256
。关于kmalloc()与kfree()的用法,可参考内核源程序中的malloc.h与slab.c程序
。

3、程序的编译和访问

   本例在Linux 2.2.x.x中可以成功编译和运行。先用下面的命令行进行编译:
gcc -Wall -O2 -c MyDev.c该命令行中参数-Wall告诉编译程序显示警告信息;参数-O2是关于代码优化的设置,注意内核模块必须优化;参数-c规定只进行编译和汇编,不进行连接。正确编译后,形成MyDev.o文件。可以输入命insmod MyDev.o来装载此程序。如果装配成功,则显示信息:Register Ok.major-number=xx。利用命令lsmod可以看到该模块被装配到系统中。为了访问该模块,应该用命令mknode来创建设备文件。下面的应用程序可以对创建的设备文件进行访问。
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#define DEVICE_NAME MyDev
#define OPENSPK 1
#define CLOSESPK 2
char buf[128];
int main(){
 int f=open(DEVICE_NAME,O_RDRW);
 if (f==-1) return 1;
 printf("nHit enter key to read device...");
  read(f,buf,128); printf(buf);
  printf("nHit enter key to write device ...");
  write(f,"test",4);
  printf("nHit enter key to open PC's speaker...");
  ioctl(f,OPENSPK);
  printf("nHit enter key to close PC's speaker...");
  ioctl(f,CLOSESPK);
 close(f);
}
 

Linux设备驱动程序中注意的问题

1、I/O端口

   和硬件打交道离不开I/O端口,在Linux下,操作系统没有对I/O端口屏蔽,也就是是说,任意驱动程序都可以对任意的I/O口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。有两个重要的kernel函数可以保证驱动程序做到这一点。

check_region(int io_port,int off_set)

这个函数查看系统的I/O表,看是否有别的驱动程序在占用某段I/O端口。

参数1:I/O端口的基地址。

参数2:I/O端口占用的范围。

返回值:0-没有被占用;非0-已经被占用。

request_region(int io_port,int off_set,char* devname)

如果这段I/O口没有被占用,在驱动程序中就可以使用它。在使用之前,必须向系统登记,以防其被其他程序占用,登记后,在/proc/ioports文件中可以看到登记的I/O口。

参数1:I/O端口的基地址。
参数2:I/O端口占用的范围。
参数3:使用这段I/O地址的设备名。
在对I/O端口登记后,就可以放心地使用inb(),outb()之类的函数来访问了。
 
2、内存操作
   在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者使用get_free_pages直接申请页。释放内存使用的是kfree或free_pages。
 
3中断处理
   同处理I/O端口一样,要使用一个中断,必须先向系统登记。
int request_irq(unsigned int irq,
                void(*handle)(int,void*,struct pt_regs*),
                unsigned int long flags,
                const char* device);
irq:要申请的中断。
bandle:中断处理函数指针。
flags:SA_INTERRUPT-请求一个快速中断;0-正常中断。
device:设备名。
如果登记成功,返回0,这时在/proc/interrupts文件中可以看到请求的中断。
 
4、和设备文件对话
   驱动程序提供了对设备操作的接口,同时在程序中实现了基本操作所需要的基本函数,用户程序通过访问设备文件的方式对设备间接操作,Linux系统提供了ioctl()函数可以很方便的实现这一操作。
int ioctl(int fd,int cmd,...);
fd:用户程序打开设备时使用open函数返回的文件标识符。
cmd:用户程序对设备的控制命令。
省略号:一些补充参数,一般多于一个,有或没有是和cmd的意义相关的。
注意:cmd设备的控制命令选择需要根据Linux文档所提供的标准控制字中选择合适
      的控制字,如果选择不合适会造成和系统中的其他设备冲突。

Trackback: http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=1555708

 

Linux设备驱动程序开发详解:源码剖析与实践 本文还有配套的精品资源,点击获取 简介:Linux设备驱动程序是硬件与操作系统内核之间的桥梁,负责硬件设备的管理和控制。本资源集合了深入的Linux驱动开发知识,包含字符设备、块设备和网络设备驱动的源代码以及相关教程。涉及设备模型、驱动注册、中断处理、I/O操作、设备文件和总线设备框架等核心概念。通过源码的阅读和分析,学习设备驱动程序的初始化、资源管理、中断处理函数和设备文... 阅读详情

相关推荐

Linux设备驱动程序设计入门.pdf.tar.gz

秉承《Linux Device Drivers》一书以实例为主的风格,但是实例的背景将非常简单,以求使读者能将集中精力于 Linux 设备驱动本身,理解 Linux 内核模块、Linux 设备驱动的结构、Linux 设备驱动中的并发控制等内容

最全Linux驱动开发全流程详细解析(持续更新)

Linux驱动开发详细解析

拉依达不拉跨的博客 11万+

基于Linux虚拟字符设备驱动设计与实现

文档+代码

Linux下字符设备驱动开发以及流程介绍

Linux内核设计哲学是把所有的东西都抽象成文件进行访问,这样对设备的访问都是通过文件I/O来进行操作。字符设备、块设备、网络设备。字符设备对数据的处理按照字节流的形式进行的。典型的字符设备:串口、键盘、触摸屏、摄像头、I2C、SPI、声卡等;应用程序能够使用系统IO函数open、write、read、lseek、close…来就行访问。

m0_51429770的博客 2812

Linux内核设计与实现+原书第3版 Linux设备驱动程序(中文版第三版)

Linux内核设计与实现+原书第3版 Linux设备驱动程序(中文版第三版)两本关于驱动程序编码的书籍下载

Android15 AndroidV冻结和解冻的场景

cgroup 最初由 Google 工程师 Paul Menage 和 Rohit Seth 在 2006 年提出,是一种细粒度资源控制的Linux内核机制。于 2007 年合并到 Linux 内核主线中。然而 Google 原生系统直到 Android 11 或更高版本上才支持 CACHE 应用的 CPU 冻结功能。当应用切换到后台并且没有其他活动时,系统会在一定时间内通过状态判断,将进程 ID 迁移到冻结的 cgroup 节点上,实现冻结 CACHE 应用。

苏法迪的专栏 1786

Linux设备驱动程序(三)——字符驱动

本章的目的是编写一个完整的字符设备驱动,我们开发一个字符驱动是因为这一类适合大部分简单硬件设备,字符驱动也比块驱动易于理解。贯穿本章,我们展示从一个真实设备驱动提取的代码片段:scull区域装载的简单字符工具。scull 是一个字符驱动,操作一块内存区域好像它是一个设备,在本章,因为 scull 的这个特殊之处,我们可互换地使用“设备”这个词和"scull 使用的内存区"。scull 的优势在于它不依赖硬件,scull 只是操作一些从内核分配的内存。

明天你好的博客 2911

Python介绍与安装

Python介绍与安装

acegi13579的专栏 3221

一文讲透Linux网络设备驱动框架及编写步骤

一文讲透Linux网络设备驱动框架及编写步骤

qq_41483419的博客 5307

毕设分享|基于16单片机的指纹识别电子密码锁设计

我们今天的工作生活中,很多的场合都需要身份的认证,而传统的基于标志的身份认证技术由于受到证件伪造以及密码破解等手段的威胁,逐渐表现得有些力不从心。怎样保护自己的重要文件资料,如何保证自己的隐私不被泄露?在传统的身份认证中,我们往往使用密码加密法,但是这种方法只是"防君子不防小人"。在高明的黑客眼里,由几个字符组成的密码脆弱得不堪一击。现在,科技的发展让我们有了新的选择——生物识别技术。将生物识别技术应用于笔记本、门锁等方面,可以对文件、财产起保护作用,并且可以进行身份识别。

Sen_091的博客 1021

基于ENVI5.3的Landsat条带去除、辐射定标、大气校正

基于ENVI5.3的Landsat条带去除、辐射定标、大气校正坏带修复辐射定标大气校正 坏带修复 1.下载landsat_gapfill插件,放入“安装目录\Exelis\ENVI53\extensions”,重启ENVI 2. 加载拟处理文件(MTL.txt格式) 3.打开landsat_gapfill 4.选择存放位置并命名文件 5.选择处理文件 辐射定标 1.选择工具Radiometric Calibration 2.选择已修复坏带文件 3.修改参数,选择输出位置及命名文件 大气校正 1

u010198950的博客 1万+

Linux设备驱动程序设计实例

导读:   Linux系统中,设备驱动程序是操作系统内核的重要组成部分,在 与硬件设备之间   建立了标准的抽象接口。通过这个接口,用户可以像处理普通文件一样,对硬件设   备进行打开(open)、关闭(close)、读写(read/write)等操作。通过分析和设计设   备驱动程序,可以深入理解Linux系统和进行系统开发。本文通过一个简单的例子   来说明设备驱动程序的设计。   1、  

PasserbyX的专栏 2063

计算机毕业设计springboot基于微信平台的二次元手办商场小程序【附源码】

选题背景: 随着二次元文化的兴起和普及,越来越多的人开始对二次元角色产生了浓厚的兴趣和喜爱。手办作为二次元文化的重要载体之一,成为了许多二次元爱好者收藏和展示的对象。然而,在传统的线下手办商场中,消费者往往面临着时间、空间和选择的限制。因此,基于微信平台的二次元手办商场小程序应运而生。 选题意义: 首先,基于微信平台的二次元手办商场小程序能够打破地域限制,使得用户可以随时随地通过手机进行购物。无论是在家中、上班途中还是旅行中,用户都能够方便地浏览和购买自己喜欢的手办,极大地提高了购物的便利性和灵活性。

拉期程序 452

linux设备驱动程序设计实例

linux设备驱动程序设计实例Linux系统中,设备驱动程序是操作系统内核的重要组成部分,在 与硬件设备之间建立了标准的抽象接口。通过这个接口,用户可以像处理普通文件一样,对硬件设备进行打开(open)、关闭(close)、读写(read/write)等操作。通过分析和设计设备驱动程序,可以深入理解Linux系统和进行系统开发。本文通过一个简单的例子来说明设备驱动程序的设计。1、 程序清单//

zxq80的专栏 1705

Dify与第三方平台(飞书、微信、钉钉)LangBot机器人对接实战

调用链路:飞书用户发消息 → 飞书开放平台事件回调 → LangBot 服务 → Dify 应用接口 → Dify 知识库 + 大模型生成答案 → 原路返回飞书聊天框Dify:承载知识库、提示词配置、大模型调用能力;LangBot:多 IM 渠道适配器,统一对接各大 IM 开放平台与 Dify API;飞书开放平台:提供机器人消息收发、事件推送能力。LangBot 作为中间转发服务,一端对接 Dify 应用 API,一端对接飞书开放平台机器人接口;

悦分享 268

RH850从0搭建Autosar开发环境【3X】- Davinci Configurator之BswM模块配置详解(上)

RH850从0搭建Autosar开发环境【3X】- Davinci Configurator之BswM模块配置详解(上)

weixin_42412049的博客 739

《嵌入式Linux开发实用教程》——第4章 Linux设备驱动程序设计 4.1 设备驱动概述...

本节书摘来自异步社区《嵌入式Linux开发实用教程》一书中的第4章,第4.1节,作者 朱兆祺,李强,袁晋蓉,更多章节内容可以访问云栖社区“异步社区”公众号查看。 第4章 Linux设备驱动程序设计 4.1 设备驱动概述 Linux系统将设备分成3种基本类型:字符设备、块设备、网络接口。 (1)字符设备 字符设备是一个能够像字节流一样被访问的设备,字符终端...

weixin_34321977的博客 98

【深度学习】什么是自回归模型?

自回归模型是一种重要的时间序列模型,通过利用过去的值来预测当前或未来的值。它在统计学、经济学、自然语言处理等领域有广泛的应用。自回归模型的简单性和可解释性使其成为许多应用的首选,但在更复杂的情况下,可能需要结合其他模型如移动平均模型(MA模型)或混合模型(如ARIMA)来更好地捕捉时间序列中的模式。扩散模型(Diffusion Model)和自回归模型(Autoregressive Model)是两种不同的生成模型,各自有独特的特点和应用场景。

q742971636的博客 1670

基于嵌入式linux设备驱动程序设计

 Linux为是一个成熟而稳定的操作系统。将Linux植入嵌入式设备具有众多的优点,包括可剪裁和容易移植等,所以Linux操作系统在嵌入式领域获得了广泛的应用。嵌入式Linux一直是嵌入式领域的研究热点,与PC架构不同,嵌入式系统的硬件具有多样性和差异性,嵌入式系统的开发需要对特定系统进行硬件设计,同时还要针对这些硬件来编写驱动程序。Linux内核就是通过驱动程序来同外围设备打交道的,系统设计

Joe_KingKiller的专栏 939

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

下一篇: In_interrupt()和In_irq()
jerrykinki
博客等级 码龄19年 6粉丝 5原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值