字符设备驱动

字符设备驱动开发的流程 字符设备驱动开发,开发流程,驱动框架,具体实现步骤。 阅读详情

字符设备驱动程序是由一个cdev结构描述的,其定义为:
 struct cdev {
      struct kobject kobj;//内嵌的kobject
         struct module *owner;
         const struct file_operations *ops;
         struct list_head list;//与字符设备文件对应的索引节点链表的头,该链表用于收集相同字符设备驱动程序
         //所对应的设备文件的索引节点。
         dev_t dev;//初始主设备号和次设备号
         unsigned int count;//设备号的范围大小
 };
内核提供了cdev_alloc()函数,用于动态的分配cdev描述符,并初始化内嵌的kobject数据结构,因此引用计数器的值变为0
时会自动释放该描述符。
struct cdev *cdev_alloc(void)
 {
         struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
         if (p) {
                 INIT_LIST_HEAD(&p->list);
                 kobject_init(&p->kobj, &ktype_cdev_dynamic);
         }
         return p;
 }
在添加进系统之前则要调用cdev_init()函数来对cdev进行初始化。
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
        memset(cdev, 0, sizeof *cdev);
        INIT_LIST_HEAD(&cdev->list);
        kobject_init(&cdev->kobj, &ktype_cdev_default);
        cdev->ops = fops;
}   
cdev_add()函数向系统添加一个cdev字符设备。
 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
     p->dev = dev;
     p->count = count;
  return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
使用alloc_chrdev_region()和register_chrdev_region()函数为驱动分配任意范围内的设备号。
使用register_chrdev()函数分配一个固定的设备号范围.在这种情形下,设备驱动程序不必调用cdev_add()函数。
unregister_chrdev_region(dev_t from,unsigned count)
<b>2.常见函数</b>
copy_to_user(buf, &global_var, sizeof(int));
copy_from_user(&global_var, buf, sizeof(int));
可以通过filp->private_data传递私有数据。
3.驱动中的并发操作
(1)屏蔽中断
local_irq_disable() ;          //屏蔽中断
local_irq_enabale();     //打开中断
只能禁止和使能本CPU的中断,所以并不能解决SMP引发的竞争。
(2)原子操作
// 设置原子变量的值
void atomic_set(atomic_t *v, int i);  // 设置原子变量的值为i
atomic_t v = ATOMIC_INIT(0);  // 定义原子变量v,并初始化为0
// 获取原子变量的值
atomic_read(atomic_t *v);  // 返回原子变量的值
// 原子变量加/减
void atomic_add(int i, atomic_t *v);  // 原子变量加i
void atomic_sub(int i, atomic_t *v);  // 原子变量减i
// 原子变量自增/自减
void atomic_inc(atomic_t *v);  // 原子变量增加1
void atomic_dec(atomic_t *v);  // 原子变量减少1
 // 操作并测试:对原子变量进行自增、自减和减操作后(没有加)测试其是否为0,为0则返回true,否则返回false
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
// 操作并返回: 对原子变量进行加/减和自增/自减操作,并返回新的值
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
  位原子操作:
// 设置位
void set_bit(nr, void *addr);  // 设置addr地址的第nr位,即将位写1
// 清除位
void clear_bit(nr, void *addr);  // 清除addr地址的第nr位,即将位写0
// 改变位
void change_bit(nr, void *addr);  // 对addr地址的第nr位取反
// 测试位
test_bit(nr, void *addr); // 返回addr地址的第nr位
// 测试并操作:等同于执行test_bit(nr, void *addr)后再执行xxx_bit(nr, void *addr)
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
(3)自旋锁(spin lock)——“在原地打转”
// 定义自旋锁
spinlock_t spin;
// 初始化自旋锁
spin_lock_init(lock);
// 获得自旋锁:若能立即获得锁,它获得锁并返回,否则,自旋,直到该锁持有者释放
spin_lock(lock);
// 尝试获得自旋锁:若能立即获得锁,它获得并返回真,否则立即返回假,不再自旋
spin_trylock(lock);
// 释放自旋锁: 与spin_lock(lock)和spin_trylock(lock)配对使用
spin_unlock(lock);
自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。
为防止这种影响,需要用到自旋锁的衍生:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
(4)信号量
当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
// 定义信号量
struct semaphore sem;
// 初始化信号量:
// 初始化信号量,并设置sem的值为val
void sema_init(struct semaphore *sem, int val);
// 初始化一个用于互斥的信号量,sem的值设置为1。等同于sema_init(struct semaphore *sem, 1)
void init_MUTEX(struct semaphore *sem);
// 等同于sema_init(struct semaphore *sem, 0)
void init_MUTEX_LOCKED(struct semaphore *sem);
// 下面两个宏是定义并初始化信号量的“快捷方式”:
DECLEAR_MUTEX(name)
DECLEAR_MUTEX_LOCKED(name)
//  获得信号量:
//  用于获得信号量,它会导致睡眠,不能在中断上下文使用
void down(struct semaphore *sem);
// 类似down(),因为down()而进入休眠的进程不能被信号打断,而因为down_interruptible()而进入休眠的进程能被信号打断,
// 信号也会导致该函数返回,此时返回值非0
void down_interruptible(struct semaphore *sem);
// 尝试获得信号量sem,若立即获得,它就获得该信号量并返回0,否则,返回非0.它不会导致调用者睡眠,可在中断上下文使用
int down_trylock(struct semaphore *sem);
//  使用down_interruptible()获取信号量时,对返回值一般会进行检查,若非0,通常立即返回-ERESTARTSYS
// 释放信号量
// 释放信号量sem, 唤醒等待者
void up(struct semaphore *sem);
(5)完成量(completion)提供了一种比信号量更好的同步机制,它用于一个执行单元等待另一个执行单元执行完某事。
completion相关操作:
// 定义完成量
struct completion my_completion;
// 初始化completion
init_completion(&my_completion);
// 定义和初始化快捷方式:
DECLEAR_COMPLETION(my_completion);
// 等待一个completion被唤醒
void wait_for_completion(struct completion *c);
// 唤醒完成量
void cmplete(struct completion *c);
void cmplete_all(struct completion *c);
3.阻塞和非阻塞
(1)阻塞
使用等待队列实现阻塞I/O。信号量在内核中就是依赖等待队列来实现的。
/定义初始化‘等待队列头’
Wait_queue_head_t  my_queue;
Init_wait_queue_head(&my_queue);
DECLARE_WAIT_QUEUE_HEAD (name);
//定义并初始化一个等待队列
DECLARE_WAITQUEUE(name, tsk);
//将等待队列添加到等待队列头Q指向的等待队列链表中
Void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
//将等待队列从等待队列头q指向的等待队列链表中移除
Void fastcall remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);
//等待事件,queue为等待队列头,timeout以jiffy为单位
Wait_event(queue, condition);
Wait_event_interruptible(queue, condition);
Wait_event_timeout(queue, condition, timeout);
Wait_event_interruptible_timeout(queue, condition, timeout);
//唤醒队列
Void wake_up(wait_queue_head_t *q);
Void wake_up_interruptible (wait_queu_head_t *q);
注意:wake_up可唤醒wait_event或者wait_event_interruptible,但是    wake_up_interruptible只能唤醒wait_event_interruptible。
//在等待队列上睡眠
Sleep_on(wait_queue_head_t *q);
Interruptible_sleep_on(wait_queue_head_t *q);
注意:sleep_on或者interruptible_sleep_on函数的作用就是将目前进程的状态设置成TASK_UNINTERRUPTIBLE或者INTERRUPTIBLE。并定义
一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q引导的队列别唤醒或者接到信号。Sleep_on函数应该与wake_up成对使用,
interruptible_sleep_on应该与wake_up_interruptble 成对使用。
set_current_state()(设置当前进程的运行状态)和signal_pending();current代表当前进程。
(2)非阻塞使用select()轮询
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
struct fd_set为一个集合,存放的是文件描述符(filedescriptor),毫无疑问一个socket就是一个文件描述符。
fd_set操作,比如:
FD_ZERO(fd_set *); 清空集合
FD_SET(int, fd_set *); 将一个给定的文件描述符加入集合之中
FD_CLR(int, fd_set*); 将一个给定的文件描述符从集合中删除
检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。

struct timeval 是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒。 具体解释select的参数:
int maxfdp:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!
fd_set* readfds:是指向fd_set结构的指针,监视这些文件描述符的读变化的,可以传入NULL值,表示不关心任何文件的读变化。
fd_set* writefds:是指向fd_set结构的指针,监视这些文件描述符的写变化的。可以传入NULL值,表示不关心任何文件的写变化。
fd_set * errorfds:同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout:是select的超时时间,这个参数至关重要,它可以使select处于三种状态:
若将NULL以形参传入,就将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了。
select 返回值:
负值:select错误
正值:某些文件可读写或出错
0:等待超时,没有可读写或错误的文件

深入理解Linux字符设备驱动 文章从上层应用访问字符设备驱动开始,一步步地深入分析Linux字符设备的软件层次、组成框架和交互、如何编写驱动、设备文件的创建和mdev原理,对Linux字符设备驱动有全面的讲解。 阅读详情

相关推荐

字符设备驱动开发

字符设备驱动开发

m0_69211839的博客 2533

Linux字符设备驱动入门(一)

先亮一下装备: 平台:VMware 7.0 + Linux ubuntu 3.0.0-12-generic 编译器:gcc 参考资料:LDD 3 功能:实现简单的字符操作(从用户空间向内核空间写入一串字符;从内核空间读一个字符到内核空间)         众所周知,字符设备是linux下最基本,也是最常用到的设备,它是学习Linux驱动入门最好的选择,计算机的东西很多都是相通的,掌握

My embedded world 5825

Nuttx 字符设备驱动

Nuttx采用VFS,和linux一样的设计思路,一切设备皆文件。Nuttx系统可以在nsh终端下通过”ls /dev”查看系统已注册的设备。对设备的操作就是对文件的操作。Nuttx下的设备驱动就是实现这种对文件操作的接口,屏蔽了对设备本身的访问的复杂性,呈现简单的接口,如open(), read(), write()等。字符设备是指在I/O传输过程中以字符为单位进行传输的设备,如键盘、鼠标、以及一

chengwenyang的专栏 4666

虚拟文件系统VFS的内核数据结构及联系

VFS: Linux支持各种各样的文件系统格式,如ext2、ext3、reiserfs、FAT、NTFS、iso9660等等,不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式,然而这些文件系统都可以mount到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用ls命 令看起来是一样的,读写操作用起来也都是一样的,这是怎么做到的呢?Linux内核在各种不同的文件系统格

heiworld的专栏 1019

从SVF文件到高效验证:Formality中set_svf命令的隐藏技巧与实战解析

本文深入解析Formality中set_svf命令的高级应用技巧,涵盖SVF文件生成策略、多文件加载优化及验证效率提升方法。通过-append和-ordered选项的实战应用,帮助工程师解决芯片验证中的常见问题,实现验证通过率与运行效率的双重提升。

mqtt6iot的博客 819

SVPWM原理与MCU/FPGA实现:从数学推导到工程实战

空间矢量调制(SVPWM)是实现高性能电机驱动控制的核心算法之一,其通过六个基本电压矢量与零矢量的伏秒平衡,在一个PWM周期内合成任意方向的旋转电压圆,相比传统SPWM可提升约15%的直流电压利用率。该技术不仅涉及扇区判断、作用时间计算、过调制限幅等数学原理,还在MCU与FPGA两种硬件平台上有不同的工程实现路径,包括中心对齐PWM配置、死区插入、定点数流水线设计等。在电动工具、无人机、机器人及车载执行器等电池供电场景中,SVPWM直接决定续航与扭矩表现。本文从数学原理出发,详细拆解MCU和FPGA实现细节

weixin_34265814的博客 301

linux中文件描述符fd和文件指针flip的理解

linux中文件描述符fd和文件指针flip的理解 简单归纳:fd只是一个整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。 文件描述符的操作(如: open)返回的是一个文件描述符,内核会在每个进程空间中维护一个文件描述符表, 所有打开的文件都将通过此表中的文件描述符来引用;  而流(如: fopen)返回的是

woxinyijiuw的专栏 910

Ansys Lumerical | 薄膜铌酸锂电光调制器的仿真与优化

本文详细介绍了使用Ansys Lumerical进行薄膜铌酸锂电光调制器仿真与优化的全流程。从基础原理到电学仿真、光学模拟技巧,再到设计优化实战经验,帮助工程师掌握这一先进光电器件的仿真方法。重点解析了薄膜铌酸锂的Pockels效应和Mach-Zehnder结构设计,并提供了常见问题排查指南和最新发展动态。

vulkan6gpu的博客 1196

【Linux驱动】字符设备驱动

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。编写一个驱动程序的时候需要调用上层驱动函数的时候需要调用底层实现,并且底层需要留给上层接口,并暴露出去。Linux内核文件中有一个叫做。#endif上层程序使用open。

Julian的博客 1174

对于基本的捕鱼游戏设计思路(五)——鱼

今天讲捕鱼游戏里鱼的部分,也是比较关键的一部分。 一步一步来,先分析游戏,首先鱼是从屏幕外生成,然后它有个移动的动作,而且分为不同的类型。 然后就可以进入代码部分,首先设置一个计时器,控制时间生成鱼,而鱼分种类,所以就用到随机函数,随机输出一个值,传入switch语句,从而生成不同的鱼。 void GameScene::xiaoyuupdate(float dt){ Size visibl

Cineze的博客 1万+

AI驱动芯片设计的端到端验证:AI应用架构师的全流程指南

想象你要建造一座1000层的超级摩天大楼,每个房间都有独特的功能,管道、电路、电梯系统交织如迷宫。传统的检查方式是派1000个工程师逐层逐房手动检查,耗时十年且仍可能遗漏火灾隐患。这就是今天芯片设计面临的困境:7nm芯片含百亿晶体管,相当于百亿个"房间",传统验证方法(人工编写测试用例、随机仿真)已难以应对。本文的目的,是为AI应用架构师提供一份"AI驱动芯片验证导航图",范围涵盖从验证需求分析到芯片签核的全流程,包括AI模型如何融入传统验证工具链、数据如何驱动验证效率提升、以及端到端闭环系统的构建方法。

2501_92132293的博客 1240

字符设备驱动实验

一、字符设备基础 字符设备:是指只能一个字节一个字节进行读写操作的设备,不能随机读取设备中的某一数据、读取数据要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED等。 一般每个字符设备或者块设备都会在/dev目录(可以是任意目录,这样是为了统一)下对应一个设备文件。linux用户层程序通过设备文件来使用驱动程序操作字符设备或块设备 二、驱动设备基础 驱动...

Wanggj的博客 3789

【Block总结】DiffAttention,差分注意力融入CLIP架构,抑制噪声,增强对相关特征的关注|即插即用

DiffCLIP通过微小的结构改动实现了显著的性能提升,为多模态模型的轻量化与高效化提供了新范式。

AI浩 1561

【Linux 驱动篇(一)】字符设备驱动开发

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

K3169的博客 5665

基于高斯两步移动搜寻法(2SFCA)的城市绿地可达性分析

第二步,对于每一个街道i,给定空间距离阈值d0,便形成另一个空间作用域,同样对于落在空间作用域内的每块绿地l的供给比率(Rl)利用高斯方程赋以权重,然后对这些加权后的供给比率(Rl)进行加和,便得到每个街道i的绿地可达性Ai。图3与图4是高斯两步移动搜索法的示例。在图3中,以d0为阈值,有1、2、3 三个街道中心点落在了绿地a的空间作用域内。同样在图4中,以d0为阈值,有a、c两个绿地中心点落在了街道2的空间作用域内,根据公式(3),对a、c高斯方程加权后的供需比率进行累加,便得到了街道2 的绿地可达性。

2301_77310370的博客 2404

使用 SAP 自家的 AI 工具 Joule 辅助 SAP Fiori 应用开发

SAP Joule 不请自答,解释说标记了接口的 UI Component,具备进行异步内容创建的能力,比如其通过 rootView 分配的视图,在运行时可以异步创建。目前 SAP Business Application Studio 里的 SAP Joule 支持下面这八种上下文,绝大部分都和 SAP UI5 开发相关,剩下的则是代码仓库和 SAP 帮助文档的搜索。SAP Joule 回答的大意是:我只负责回答 SAP UI5 开发相关的问题,不要和我扯其他有的没的。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1094

嵌入式linux驱动开发-字符设备驱动

驱动字符设备驱动简介字符设备驱动开发步骤驱动模块的加载和卸载 之前在嵌入式系统移植停留了一段时间,枯燥地看uboot、kernel和跟文件系统的源码感觉没有成就感,在这之前已经对嵌入式linux的启动流程有了大体的了解,因此先跳到驱动开发,体验学习一下子哈!!。 Linux 中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。 字符设备驱动:IO口的驱动,比如点灯、 I2C、 SPI、音频等。 块设备驱动:存储器设备的驱动,比如 EMMC、 NAND、 SD 卡和 U 盘等存储设备 网络设备驱动:网络驱

平平酱的博客 3652

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

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

linux驱动——字符设备驱动总结

定义一个目标设备结构体,这个结构体中包含如下变量待分配的设备号dev_t;该字符设备对应的struct cdev结构体;设备类class;struct device设备结构体主次设备号整型变量int major;int minor;定义文件操作函数file_operations结构体。自定义实现设备需要的操作函数实现驱动入口函数xxx_init,在驱动入口函数完成具体字符设备的初始化,譬如相关寄存器位的设置。然后开始注册字符设备驱动。。。。1、

weixin_46066367的博客 910

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

字符设备驱动—1

字符设备通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,通过字符(一个接一个的字符)以字节流方式向用户程序传递数据。字符设备驱动通过/dev目录下的特殊文件公开设备的属性和功能,通过这个文件可以在设备和用户程序之间进行数据的交换。

m0_72622181的博客 1761

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Linux 字符设备驱动开发模板

介绍字符驱动开发的基本模式

安迪西_嵌入式攻城狮 3699

Linux驱动——字符设备驱动

1.使用什么工具来写驱动代码:安装source insight工具(1)找到软件提示把工具安装激活(2)把Linux内核代码解压到windows目录中(3)打开工具添加查看的项目第一个对话框:第一个文本框:输入工程名字第二个文本框:路径(默认)第二个对话框: 在项目源码位置,选择 Linux内核代码位置 点击OK 第三个对话框: 选择需要查看的Linux内核代码的包含的源码文件(要查看的内核目录)需要添加的目录:driver/iicinclude。

Linux驱动开发工程师,熟悉Linux/Android底层驱动、TP、显示、IIC、网络等通用外设调试适配,Linux上层应用开发。常用平台瑞芯微、全志、恩智浦等。 3439

字符设备驱动基础知识

一、字符设备结构体(cdev) 在Linux内核中, 使用cdev结构体来描述一个字符设备。 struct cdev { struct kobject kobj; //内嵌的kobject对象 struct module *owner;//所属模块 const struct file_operations *ops; //文件操作结构体 struct list_head list; de...

嵌入式软件开发交流 2万+
上一篇: linux中.config,makefile,kconfig
下一篇: 中断/IO/DMA
快乐的小鸟
博客等级 码龄14年 25粉丝 40原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值