驱动笔记(自己总结的)

vscode:
https://note.youdao.com/ynoteshare/index.html?id=ea4796f0aa4344e11bae0d5bdfe32cc8&type=notebook&_time=1652253468820

DC22021-Linux设备驱动开发
https://note.youdao.com/ynoteshare/index.html?id=48935e109f3c8562ff6ffa57dfb988ad&type=notebook&_time=1654736812939

驱动:给应用层提供接口,控制硬件

ARM裸机驱动和驱动的区别?
ARM裸机驱动:开发的时候不需要内核的加持,直接从头到尾自己来编写代码即可
驱动:在linux内核基础上编写的基于内核来操作硬件的代码才是真实的驱动

字符设备驱动:按照字节流访问,只能顺序访问,不能无序访问的设备属于字符设备驱动(90%)
块设备驱动:按照block访问(512byte),可以顺序访问也可以无需访问的设备属于块设备驱动
网卡设备驱动:网卡设备驱动没有设备文件,通过网络协议栈发送给网卡驱动实现数据收发的代码就是网卡驱动

Makefile

arm?=x86
modname?=demo
ifeq($(arch),arm)
KERNELDIR:= /home/linux/linux-5.10.61   #在开发板上安装
else
KERNELDIR:= /lib/modules/$(shell uname -r)/build #在ubuntu上安装
endif
PWD:=$(shell pwd)
all:
 make -C $(KERNELDIR) M=$(PWD) modules
 @#从当前目录切换到内核的顶层目录下
 @#读取内核顶层目录下的Makefile文件
 @#执行make
 @#make M=$(PWD) modules
 @#make modules:模块化编译的命令,M指定只编译当前目录下的目录
clean:
 make -C $(KERNELDIR) M=$(PWD) clean
 @#清除编译的中间文件

obj-m:=modname
@obj-m:=hello.o
@hello-bojs += $(modname).o a.o

内核模块(day1)

内核三要素

入口:在入口分配资源 static int __init 名字(void)

出口:在出口释放资源 static void __exit 名字(void)

许可证:遵从GPL协议

//将入口函数的地址告诉给内核
module_init(demo_init);
//将出口函数的地址告诉给内核
module_exit(demo_exit);
//许可证
MODULE_LICENSE("GPL");
//模块的作者
MODULE_AUTHOR("azy azy_hd@163.com");

操作命令

sudo insmod demo.ko //安装模块
lsmod //查看模块 可以查看状态(OE+)
sudo rmmod demo //卸载模块
dmesg //主动查看内核中的所有目录(高于终端的级别,颜色红色)
sudo dmesg -c/-C  //清空内核打印信息,-c回显后清楚,-C直切清楚
mknod /dev/myled c 241 0 //分配驱动节点
    	//myled是字符设备驱动的名字
    	//c/d :c是字符设备驱动,b是块设备驱动
    	//241:主设备号,0次设备号
modinfo xxx.ko //查看打印信息

内核模块中接收的函数

module_param(name,type,perm)
功能:接收命令行传递的参数
参数:
    @name:变量名
    @type:变量类型
        /* Standard types are:
         *	byte, hexint, short, ushort, int, uint, long, ulong
         *	charp: a character pointer
         *	bool: a bool, values 0/1, y/n, Y/N.
         *	invbool: the above, only sense-reversed (N = true).
         */
	@perm:修饰文件的权限(0664module_param_array(name,type,nump,perm)
功能:接受命令行传递的数组
参数:
    @name:数组名
    @type:数组中成员类型
    @nump:传递的成员的个数
    @perm:修饰文件的权限(0664MODULE_PARM_DESC(_parm, desc)
功能:对变量信息的详细描述(可以通过modinfo xxx.ko查看)
参数:
    @_parm:变量名
    @desc:描述的字符串
#include <linux/init.h>
#include <linux/module.h>

int backlight = 127;
module_param(backlight, int, 0664);
MODULE_PARM_DESC(backlight, "this is backlight var range[0-255]");

char a = 'A';
module_param(a, byte, 0664);
MODULE_PARM_DESC(a, "this is char var");

char* p = "helloworld";
module_param(p, charp, 0664);
MODULE_PARM_DESC(p, "this is char* var");

int ww[10] = {0};
int len;
module_param_array(ww,int,&len,0664);
MODULE_PARM_DESC(ww, "this is int [10] var");

static int __init mycdev_init(void)
{
    int i;
    //shift+alt+f  代码对其
    //追代码:ctrl+鼠标左键
    //回退:alt + <-
    //打开终端:ctrl + `
    //终端和代码块切换 ctrl+j
    //多行注释  ctrl + /
    printk("backlight = %d\n", backlight);
    printk("a = %d\n", a);
    printk("p = %s\n", p);
    for(i=0;i<len;i++){
        printk("ww[%d] = %d\n",i,ww[i]);
    }
    return 0;
}
static void __exit mycdev_exit(void)
{
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");

导出符号表API

EXPORT_SYMBOL_GPL(sym) 
功能:导出符号表
参数:
    @sym:函数名
注:
   在编译的时候需要将A中的Module.symbers拷贝到B中,然后在编译B,

demoA:提供add函数

#include<linux/init.h>
#include<linux/module.h>

int add(int a,int b){
    return a+b;
}
EXPORT_SYMBOL_GPL(add)
static int __init mycdev_init(void){
    return 0;
}
static void __exit mycdev_exit(void){
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GOL");

demoB.c

#include<linux/init.h>
#include<linux/module.h>
extern int add(int,int);
static int __init mycdev_init(void){
    prink("demoB usm=%d\n",add(100,200));
    return 0;
}
static void _exit mycdev_exit(void){
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");

编译demodA时会生成一个Module.symbers符号表,拷贝(内核5.4)到demodB目录下,然后编译demodB

如果内核在5.10的内核取消了拷贝字符表的方式,需要使用 KBUILD_EXTRA_SYMBOLS变量指定符号表的绝对路径 ,放到Makefile中

KBUILD_EXTRA_SYMBOLS := /home/linux/work/day1/04export/demoA/Module.symvers

需要先安装demoA,在安装demoB模块,卸载需要demoB然后在卸载demoA

字符设备驱动(day2)

#inculude<linux/fs.h>

int register_chrdev(unsigned int major, const char *name,
                   	const struct file_operations *fops)
功能:注册字符设备驱动
参数:
    @major:主设备号
          major > 0 :静态指定设备号
            major = 0 :系统自动分配主设备号
            次设备号的区间{0,255}256)
     @name:字符设备驱动的名字
      linux@ubuntu:/dev/input$ cat /proc/devices 
            Character devices:
              1    mem
              4    /dev/vc/0
              4    tty
              4    ttyS
              5    /dev/tty     
              主设备号 设备驱动的名字
                  
@fops:操作方法结构体
     struct file_operations {
         int (*open) (struct inode *, struct file *);
         ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
         ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
         int (*release) (struct inode *, struct file *);
     }
返回值:
      major > 0 :成功返回0,失败返回错误码
      major = 0 :成功返回主设备号,失败返回错误码            
         通过EIO可以找到内核中的错误码
void unregister_chrdev(unsigned int major, const char *name)
功能:注销字符设备驱动
参数:
    @major:主设备号
    @name:设备的名字
返回值:无
int major=0;
const struct file_operations fops = {
    .open = mycdev_open,
    .release = mycdev_close,
};
static int __init mycdev_init(void){
    // 1.注册字符设备驱动
    major = register_chrdev(0, CNAME, &fops);
    if(major < 0){
        printk("register char device driver error\n");
        return major;
    }
    printk("register success major = %d\n",major);
    return 0;
}
static void __exit mycdev_exit(void){
    //注销字符设备驱动
    unregister_chrdev(major,CNAME);
}

地址映射

#include <linux/io.h>
void  *ioremap(unsigned long offset, unsigned long size)
 功能:将物理地址映射成虚拟地址
 参数:
    @offset:物理地址
    @size:大小
 返回值:成功返回虚拟地址,失败返回NULL
        
void iounmap(volatile void  *addr)
功能:取消地址映射
参数:
     @addr:映射得到的地址
返回值:无
virt_moder = ioremap(PHY_LED1_MODER, 4);
    if (virt_moder == NULL) {
        printk("ioremap moder register error\n");
        return -ENOMEM;
    }

传输数据

#include <linux/uaccess.h>

int copy_to_user(void __user volatile *to, const void *from,
          unsigned long n)
功能:将数据从内核空间拷贝到用户空间 (mycdev_read)
参数:
    @to:用户空间的地址 (__user告诉编译器这个地址是用户空间的地址)
 	@from:内核的地址 //传的数据的地址
    @n:大小(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数 EINVAL
        
int copy_from_user(void *to, const void __user volatile *from,
     unsigned long n)
功能:将数据从用户空间拷贝到内核空间 (mycdev_write)
参数:
    @to:内核空间的地址
 	@from:用户的地址 
    @n:大小(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数

自动创建设备节点

(day3)

#include <linux/device.h>
struct class *class_create(owner, name)
功能:向上层提交目录名
参数:
    @owner:THIS_MODULE(这个是和编译器及驱动安装相关的宏)
    @name:目录名 //添加写的宏NAME "myled"
返回值:成功返回结构体的首地址,失败返回错误码指针     
struct class *cls = class_create(THIS_MODULE,"hello");
if(IS_ERR(cls)){
    printk("class create error\n");
    return PTR_ERR(cls);
}
struct device *device_create(struct class *class, struct device *parent,
        dev_t devt, void *drvdata, const char *fmt, ...)
功能:向上层提交设备信息
参数:
    @class:目录的句柄
    @parent:NULL
    @devt:设备号
         MKDEV(major,minor)  //合成设备号
         MAJOR(dev)   //根据设备号得到主设备号
         MINOR(dev)   //根据设备号得到次设备号
 @drvdata:NULL 
    @fmt:设备节点的名字 "uart%d",i 
返回值:成功返回结构体的首地址,失败返回错误码指针
void class_destroy(struct class *cls)
功能:销毁目录
参数:
     @cls:目录的句柄
返回值:无
void device_destroy(struct class *class, dev_t devt)  
功能:销毁目录下的信息
参数:
     @cls:目录的句柄
     @devt:设备号
返回值:无
struct class *led_class;
struct device *led_device;

	led_class=class_create(THIS_MODULE,NAME);
    if(IS_ERR(led_class)){
        printk("class create error\n");
        goto ERR3;
    }
    led_device=device_create(led_class,NULL,led_dev,NULL,NAME);
    if(IS_ERR(led_device)){
        printk("device create error\n");
        goto ERR4;
    }
#if 0
    for(i=0;i<count;i++){
        dev=device_create(cls,NULL,MKDEV(major,i),NULL,"led%d",i);
        if(IS_ERR(dev)){
            printk("device create error\n");
            goto ERR5;
        }
    }
#endif
    device_destroy(led_class,led_device);
    class_destroy(led_class);
#if 0
    for(--i;i>=0;i--){
        device_destroy(cls,MKDEV(major,i));
    }
    class_destroy(cls);
#endif

ioctl函数

(day3)

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
功能:设备的控制
参数:
    @fd     :文件描述符
    @request:命令码
            见下面的解释
    @...   :可变参数,如果传,传递地址 //把发送过来的数据给这个参数
————————————————————————————————————————
fops:long (*unlocked_ioctl) (struct file *, unsigned int cmd , unsigned long arg);
功能:内核中实现的ioctl(request->cmd   ...->arg)
参数:
    @cmd:用户传递过来的命令码
    @arg:用户传递过来的地址
返回值:成功返回0,失败返回错误码
#define LED_ON _IOW('l',1,int)
#define LED_OFF _IOW('l',0,int)
#define GET_CMD_SIZE(cmd) ((cmd>>16)&0x3fff)//右移16位和0x3fff取余就可以得到大小了

//用户层:
int main(int argc,const char * argv[]){
    int fd;
    char buf[128] = {0};
    int which = 1;
    if((fd = open("/dev/ttt",O_RDWR))==-1){
        PRINT_ERR("open mycdev error");
    }
    while(1){
        ioctl(fd,LED_ON,&which);//LED_ON对应着驱动的第二个参数,which对印着第三个参数
        sleep(1);
        ioctl(fd,LED_OFF,&which);
        sleep(1);
    }
    
    close(fd);
    return 0;
}
//底层
long myled_ioctl(struct file* file,unsigned int cmd, unsigned long arg){
    int which, ret;
    switch (cmd) {
    case LED_ON:
        ret = copy_from_user(&which, (void*)arg, GET_CMD_SIZE(LED_ON));
        if (ret) {
            printk("copy data form user error\n");
            return -EINVAL;
        }
        printk("which = %d,LED%d ON\n", which, which);
        if(which == 1) LED1_ON;
        break;
    case LED_OFF:
        ret = copy_from_user(&which, (void*)arg, GET_CMD_SIZE(LED_ON));
        if (ret) {
            printk("copy data form user error\n");
            return -EINVAL;
        }
        printk("which = %d,LED%d OFF\n", which, which);
        if(which == 1) LED1_OFF;
        break;
    }

    return 0;
}
//1.
const struct file_operations fops = {
    .open = myled_open,
    .unlocked_ioctl = myled_ioctl,
    .release = myled_close,
};

分步流程

(day3)

#include<linux/cdev.h>
1.分配对象
    struct cdev {
        struct module *owner;  //THIS_MODULE
        const struct file_operations *ops; //操作方法结构体
        struct list_head list;  //内核的链表
        dev_t dev;              //设备号
        unsigned int count;    //设备的个数
    } ;
 struct cdev *cdev; //分配对象
 struct cdev *pcdev = cdev_alloc();

 struct cdev *cdev_alloc(void)
    功能:为cdev的结构体指针分配内存
    参数:
        @无
    返回值:成功返回结构体的首地址,失败返回NULL
2.初始化对象
    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    功能:字符设备驱动的初始化
    参数:
        @cdev:cdev结构体指针
        @fops:操作方法结构体
    返回值:无            
3.申请设备号          
     int register_chrdev_region(dev_t from, unsigned count, const char *name)
     功能:静态指定设备号
     参数:
          @from:设备号的起始值 MKDEV(major,minor)
          @count:个数
          @name:设备的名字  cat /proc/devices查看
     返回值:成功返回0,失败返回错误码     
     
     int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
     功能:动态申请设备号 
     参数:
          @dev:申请到的设备号
          @baseminor:次设备号的起始值
          @count:个数
          @name:设备的名字  cat /proc/devices查看
     返回值:成功返回0,失败返回错误码                
4.对象注册
    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    功能:字符设备驱动的注册
    参数:
        @p:cdev的结构体指针
        @dev:申请到的设备号 
        @count:设备的个数
 返回值:成功返回0,失败返回错误码          
————————————————————————————————————————
void cdev_del(struct cdev *p)
功能:注销字符设备驱动
参数:
    @p:cdev的指针
返回值:无
void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
    @from:设备号的开始值 minor
    @count:个数
返回值:无
void kfree(const void *objp)
#include<linux/slab.h>
功能:释放cdev_alloc申请的内存
参数:
    @objp:cdev的首地址
返回值:无
//1.分配对象
struct cdev *cdev;//定义
cdev=cdev_alloc();//
//2.初始化对象
cdev_init(cdev,操作方法结构体fops)//没有返回值,操作方法结构体struct file_operations fops={};
//3.申请设备号 动态分配
denv_t devno; int minor //初始化 MKDEV(major,minot)
int count=0//申请设备号的个数
#denfine NAME "myled"
alloc_chrdev_region(&devno,&minor,count,NAME);
//4.对象注册
cdev_add(cdev,devno,count);
#define NAME "myled"
struct cdev *led_cdev;
dev_t led_dev;
int major;

const struct file_operations ofps={
};
static int __init mycdev_init(void){

    int ret=0;
    led_cdev=cdev_alloc();//
    if(NULL==led_cdev){
        printk("cdev_alloc error\n");
        goto ERR;
    }
    cdev_init(led_cdev,&ofps);
    ret=alloc_chrdev_region(&led_dev,major,0,NAME);//申请设备号
    if(ret!=0){
        printk("alloc chrdev region error\n");
        goto ERR1;
    }
    ret=cdev_add(led_cdev,led_dev,1);//注册设备号,申请的子设备号+1
    if(ret!=0){
        printk("cdev add error\n");
        goto ERR2;
    }
    return 0;
ERR3:
    cdev_del(led_cdev);
ERR2:
    unregister_chrdev_region(led_dev,0);
ERR1:
    kfree(led_cdev);
ERR:
    return -1;
}

静态和并发(day4)

中断屏蔽?

local_irq_disable();  //关闭中断
//临界资源
local_irq_enable();  //开启中断

自旋锁(重点)

spinlock_t lock; //定义自旋锁
void spin_lock_init(spinlock_t *lock) //初始化自旋锁
void spin_lock(spinlock_t *lock)     //上锁
void spin_unlock(spinlock_t *lock)   //解锁

信号量(重点)

struct semaphore lock; //定义信号量
void sema_init(struct semaphore *sem, int val)
//初始化信号量,只有val初始化为1的时候才有互斥的效果。
//如果val初始化为0,同步机制
void down(struct semaphore *sem); //上锁
int  down_interruptible(struct semaphore *sem); //休眠的时候可以被信号打断
int  down_killable(struct semaphore *sem);      //当进程休眠的时候可以被kill掉
int  down_trylock(struct semaphore *sem);       //尝试获取锁
//获取到锁的时候返回0,失败返回1,即使没有资源也不会休眠
int  down_timeout(struct semaphore *sem, long jiffies); //休眠的时候加上超时检查
//如果在超时时间到了还没有获取到信号量,返回-ETIME错误码,否则返回0
void up(struct semaphore *sem);   //解锁

互斥体(会用)

struct mutex lock; //定义互斥体
mutex_init(&lock); //初始化互斥体
void mutex_lock(struct mutex *lock); //上锁
int mutex_trylock(struct mutex *lock); //尝试获取互斥体,成功返回1,失败返回0
void mutex_unlock(struct mutex *lock);//解锁

原子(会用)

atomic_t lock = ATOMIC_INIT(1);  //定义并初始化原子变量
atomic_dec_and_test(&lock);     //上锁
//减去1和0比较,如果结果为0表示获取锁成功了(成功返回真),否则获取锁失败了(失败返回假)
atomic_inc(&lock)        //解锁
    
atomic_t lock = ATOMIC_INIT(-1);  //定义并初始化原子变量
atomic_inc_and_test(&lock);//上锁
//加1和0比较,如果结果为0表示获取锁成功了(成功返回真),否则获取锁失败了(失败返回假)
atomic_dec(&lock)        //解锁

IO模型(day4下、5)

非阻塞

如果驱动提供的是非阻塞的IO模型,当调用read函数读取数据的时候,不管数据是否准备好read函数都应该立即返回。

user:
 open("/dev/mycdev0",O_RDWR|O_NONBLOCK) //以非阻塞的方式开发
    read(fd,buf,sizeof(buf));
--------------------------------------------
kernel:
 mycdev_read(file){
        if(file->f_flags & O_NONBLOCK){
            //非阻塞
            //从硬件中读取数据,将数据返回到用户空间即可
        }
    }

阻塞

如果驱动提供的是阻塞的IO模型,当调用read函数读取数据的时候,如果数据没有准备好阻塞等待(进程休眠),如果硬件的数据准备好了,会产生硬件中断,在中断处理函数中唤醒休眠的进程。将准备好的数据拷贝到用户空间

user:
 open("/dev/mycdev0",O_RDWR) //以阻塞的方式开发
    read(fd,buf,sizeof(buf));
--------------------------------------------
kernel:
 mycdev_read(file){
        if(file->f_flags & O_NONBLOCK){
            //非阻塞
        }else{
          //阻塞
            //判断数据是否准备好,如果数据没有准备好,让进程休眠
        }
    }
1.定义等待队列头
    wait_queue_head_t wq;
2.初始化等待队列头
    init_waitqueue_head(&wq); 
3.如果数据没有准备好就休眠
    wait_event(wq, condition)                //让进入不可中断的休眠态
    wait_event_interruptible(wq, condition) //让进程进入可中断的休眠状态
    //condition代表数据是否准备好的一个变量,如果是真代表数据准备好了,不需要休眠,
    //如果为假表示数据没有准备好,进程需要休眠
4.唤醒
    condition=1;  //如果不设置为真,即使唤醒了也会再次进入休眠状态
    wake_up(&wq);
 wake_up_interruptible(&wq);

IO多路复用(day5)

在同一个APP应用程序同时监听多个硬件的数据,此时就需要使用IO多路复用机制中的select/poll/epoll来完成多个文件描述符的监听的过程,如果所有的文件描述符对应的数据都没有准备好,进程休眠。如果有一个或者多个硬件的数据准备好了,就会唤醒这个休眠的进程。select/poll/epoll就会返回,返回后从表中找到数据准备好的文件描述符,从对应的文件描述符中将数据拿到即可。

fd1 = open("/dev/mycdev0",O_RDWR);
fd2 = open("/dev/input/mouse0",O_RDWR);

fd_set rfds;  //定义表
FD_ZERO(&rfds); //清空表
FD_SET(fd1,&rfds); //将fd1放入表中
FD_SET(fd2,&rfds); //将fd2放入表中
int select(maxfd+1,&rfds,NULL,NULL,NULL);

if(FD_ISSET(fd1,&rfds)){
    read(fd1,buf1,sizeof(buf1));
}    
if(FD_ISSET(fd2,&rfds)){
    read(fd2,buf2,sizeof(buf2));
}              
------------------------------------------------------------------
driver:fops: (应用层的select/poll/epoll都会调用驱动的同一个poll函数)
__poll_t (*poll) (struct file *file, struct poll_table_struct *wait)
{
     grep ".poll =" * -nR   //通过这种方式参考内核的实现
       //1.定义一个mask的变量初值为0
        __poll_t mask=0;
       //2.调用poll_wait   (阻塞相关)
        poll_wait(file,&wq,wait) ;
       //3.判断数据是否准备好
         if(condition)
             mask |= EPOLLIN;
     4.返回mask
            return mask;
}
1.写好非阻塞后
2.写poll函数
__poll_t mycdev_poll(struct file *file,
     struct poll_table_struct *wait)
{
    __poll_t mask=0;
 //1.向上提交等待队列头
    poll_wait(file,&wq,wait);
 
    //2.返回EPOLLIN表示数据准备好了,返回0表示数据没有准备好
    if(condition)
        mask |= EPOLLIN;
    
    return mask;
}
select(表)

1.select监听的最大文件描述是1024个
2.select有清空表的过程,需要反复从用户空间向内核空间拷贝表,效率低
3.当有文件描述符的对应驱动的数据准备好的时候,需要再次遍历找到准备好的文件描述符,效率低

poll:(结构体数组)

1.poll监听的最大文件描述没有个数限制
2.poll没有清空表的过程效率高
3.当有文件描述符的对应驱动的数据准备好的时候,需要再次遍历找到准备好的文件描述符,效率低

epoll:(红黑树)

1.epoll监听的最大文件描述没有个数限制
2.epoll没有清空表的过程效率高
3.epoll当在休眠的时候,如果有驱动的数据准备好,epoll能直接拿到准备好的文件描述符,不需要遍历,效率高。epoll是本世纪效率最高,最好用的IO多路复用机制

异步通知

在进程中注册一个信号处理函数,如果硬件的数据准备好的时候,会产生中断,在中断处理函数中给这个进程发送信号即可。如果内核没有发出信号应用程序,不需要阻塞,运行自己特有的代码即可。

//1.注册信号处理函数
signal(SIGIO ,信号处理函数)
//2.通过fcntl调用到底层的fasync函数
unsigned int flags = fcntl(fd,F_GETFL);   
fcntl(fd,F_SETFL,flags|FASYNC);
//3.将当前的进程号告诉给内核
fcntl(fd,F_SETOWN,getpid());
-----------------------------------------
fops:
 int (*fasync) (int fd, struct file *file, int on)
    {
        return fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp) 
        //功能:完成发信号前的初始化
    }
    void kill_fasync(struct fasync_struct **fp, int sig, int band)    
     //向进程发送信号,如果当数据准备好后发送这个信号,就会发送数据
struct fasync_struct *fapp;
kill_fasync(&fapp,SIGIO,POLL_IN);//发送信号告诉用户可以读数据  POLL_IN=1
int mycdev_fasync(int fd, struct file *file, int on){
    return fasync_helper(fd,file,on,&fapp);
}
const struct file_operations fops = {
    .open = mycdev_open,
    .read = mycdev_read,
    .write = mycdev_write,
    .fasync = mycdev_fasync, //当收到用户发来的信号会执行这个函数
    .release = mycdev_close,
};

设备树

设备树(Device Tree)是⼀种描述硬件信息的数据结构,设备树的源文件是dts或dtsi的文件这些文件经过DTC编译生成DTB的二进制文件。在Linux内核启动的时候就会解析这个DTB文件,将这个DTB中的文件解析成一种树状的结构,驱动就可以从这个结构体获取到设备信息,进而操作硬件。

特点:

1.在设备树中同名的节点会被合并
2.设备树的节点可以去别名

取别名的方法1:
node:mynode@0x12345678{   
};
注:node就是mynode@12345678的别名
取别名的方法2:
aliases {
    node1=&node;
};

3.节点可以被其他引用

&node{  
};

4.节点中的建可以被删除

node:mynode@0x12345678{
  p1="123";
};
&node{
     /delete-property/p1;   //删除节点中的p1键值对
}

关于设备树节点的命令和资料

设备树节点
	linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
编译设备树 在linux-5.10.61目录下
    make dtbs
拷贝到tftp中
    cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/tftpboot
在内核启动之后,可以在如下位置找到节点,并且在这个目录下会根据属性名创建文件
    /proc/device-tree/mynode@0x12345678 //mynode@0x12345678设备树的名
 	
    stm32mp151.dtsi  //根设备树
 	stm32mp15xx-dkx.dtsi //别人写的
 /home/linux/linux-5.10.61/Documentation/devicetree/bindings/i2c //都是别人写的
2.参考内核的帮助文档编写自己的设备树
    stm32mp157a-fsmp1a.dts

设备树API

//每一个节点被内核解析后,内核就会为这个节点创建一个device_node的结构体
struct device_node {
 const char *name;       //节点名 mynode
 const char *full_name;  //节点全名 mynode@0x12345678
 struct property *properties; //节点内的属性,这个property是一条链表
 struct device_node *parent;  //父节点
 struct device_node *child;   //子节点
 struct device_node *sibling; //兄弟节点
};
struct property {
 char *name;  //键
 int length;     //值的长度
 void *value; //值
 struct property *next; //指向下一个键值对
}
struct device_node *of_find_node_by_path(const char *path)
功能:通过路径或者节点结构体
参数:
    @path:路径
返回值:成功返回节点的首地址,失败返回NULL
        
struct device_node *of_get_child_by_name(const struct device_node *node,const char *name)
功能:从父节点解析子节点
参数:
    @node:父节点node指针
    @name:子节点名
返回值:成功返回子节点指针,失败返回NULL
        
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
功能:通过节点名获取节点
参数:
    @from: NULL,从根节点往下解析
 @name:节点名
返回值:成功返回节点的首地址,失败返回NULL
     
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
功能:根据 compatible来获取节点
参数:
    @from:NULL,从根开始解析
    @type:NULL
    @compatible:"厂商,设备名"
返回值:成功返回节点的首地址,失败返回NULL  
        
struct property *of_find_property(const struct device_node *np,const char *name,
      int *lenp)
功能:根据键的名字获取值
参数:
    @np:节点首地址
 @name:键的名字
 @lenp:获取到的值的长度
返回值:成功返回property结构体指针,失败返回NULL
     
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)
功能:从property中获取一个u32的类型的数
参数:
     @np:节点指针
     @propname:键
     @index:索引号
     @out_value:获取到的结果
返回值:成功返回0,失败返回错误码
         
int of_property_read_variable_u8_array(const struct device_node *np,const char *propname, u8 *out_values,size_t sz_min, size_t sz_max)
功能:获取一个u8类型的数组
参数:
     @np:节点指针
     @propname:键
     @out_value:获取到的结果
     @sz_min:最小的下标
  @sz_max:最大下标
返回值:成功返回0,失败返回错误码
      
int of_property_read_string_index(const struct device_node *np,const char *propname,      int index, const char **output)
功能:根据索引号获取字符串
参数:
     @np:节点指针
     @propname:键
     @index:索引号
     @output:获取到的结果
返回值:成功返回0,失败返回错误码  

gpio子系统

编写设备树

编写设备树的一般需要在一下目录中查找

/home/linux/linux-5.10.61/Documentation/devicetree/bindings/gpio/gpio.txt

gpio子系统API

int of_get_named_gpio(struct device_node *np,const char *propname, int index)
功能:根据节点结构体解析gpio编号
参数:
    @np:节点指针  //获取节点后的node
    @propname://"led"
    @index:索引号
返回值:成功返回gpio的编号,失败返回错误码
        
int gpio_request(unsigned gpio, const char *label)
功能:申请要使用的gpio
参数:
    @gpio:gpio的编号
    @label:标签名  //NULL
返回值:成功返回0,失败返回错误码
        
int gpio_direction_input(unsigned gpio)
功能:设置gpio的为输入
参数:
    @gpio:gpio的编号
返回值:成功返回0,失败返回错误码   
        
int gpio_direction_output(unsigned gpio, int value)
功能:设置gpio的为输出
参数:
    @gpio:gpio的编号
    @value:默认电平的状态 1高电平  0低电平
返回值:成功返回0,失败返回错误码
        
int gpio_get_value(unsigned gpio)
功能:读取管脚的值
参数:
    @gpio:gpio的编号
返回值:1高电平  0低电平      
        
void gpio_set_value(unsigned gpio, int value)    
功能:设置输出电平的值
参数:
     @gpio:gpio的编号
     @value:1高电平  0低电平
返回值:无
         
void gpio_free(unsigned gpio)
功能:释放gpio
参数:
     @gpio:gpio的编号
返回值:无    

内核定时器

当前时间如何获取?
jiffies;内核时钟节拍数,从内核启动开始,一直在增加的一个数。

定时器加1走的时间?
定时器的频率配置在内核的.config中保存着

CONFIG_HZ=100
定时器加1走的时间是10ms

1.分配对象
    struct timer_list {
        struct hlist_node entry;     //内核链表
        unsigned long  expires;   //定时的时间
        void (*function)(struct timer_list *); //定时器处理函数(定时到了执行的函数)
        unsigned long  data; //向定时器处理函数传递的参数
        u32			flags;        //0
    };
	struct timer_list mytimer; //定义
2.对象初始化
     mytimer.expires = jiffies+HZ;  //定时时间是1s
     timer_setup(&mytimer, 定时器处理函数, 0); //定时器初始化函数
3.启动
   void add_timer(struct timer_list *timer)   //注册并启动定时器
   int mod_timer(struct timer_list *timer, unsigned long expires) //再次启动定时器
4.注销
 int del_timer(struct timer_list *timer)  //删除定时器

新版gpio子系统接口

struct gpio_desc *gpiod_get_from_of_node(struct device_node *node,  
      const char *propname, int index,
      enum gpiod_flags dflags,
      const char *label)  //一步到位
    @node: 解析设备树的node号
    @propname:键的名字"led"
    @index:索引号
    @dflags:赋初值GPIOD_OUT_HIGH ,可以通过enum gpiod_flags dflags中的gpio_flags追到
    @label:标签名 0NULL
int gpiod_request(struct gpio_desc *desc, const char *label)
int gpiod_direction_input(struct gpio_desc *desc)
int gpiod_direction_output(struct gpio_desc *desc, int value)
int gpiod_get_value(const struct gpio_desc *desc)
void gpiod_set_value(struct gpio_desc *desc, int value)
void gpiod_put(struct gpio_desc *desc) //释放
//获取设备树的节点	
        node=of_find_node_by_path("/mydemo");
    if(NULL==node){
        printk("of find node by path error\n");
        goto ERR5;
    }
    //解析gpio
    for(i=0;i<3;i++){
        led_gpio[i]=gpiod_get_from_of_node(node,"led",i,GPIOD_OUT_LOW,0);//GPIOD_OUT_LOW初始化灯为灭
        if(IS_ERR(led_gpio[i])){
            printk("gpid get from of nade error\n");
            goto ERR6;
        }
    }
for(i=0;i<3;i++){
    gpiod_put(led_gpio[i]);//释放GPIO
}

中断(day7)

中断设备树解析函数

struct device_node *of_find_node_by_path(const char *path)
功能:通过路径或者节点结构体
参数:
    @path:路径
返回值:成功返回节点的首地址,失败返回NULL        
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
功能:解析得到软中断号
参数:
    @dev:节点指针
    @index:索引号
返回值:成功返回大于0的值,失败返回0
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
功能:注册中断
参数:
    @irq:软中断号
    @handler:中断处理函数的函数指针
        //中断处理函数
         irqreturn_t key_irq_handle(int irq, void *dev)
         {
          return IRQ_NONE	; //中断没有处理
    return IRQ_HANDLED; //中断处理完成了
     }
 @flags:中断触发方式
         IRQF_TRIGGER_RISING  
            IRQF_TRIGGER_FALLING 
            IRQF_TRIGGER_HIG 
            IRQF_TRIGGER_LOW
         IRQF_SHARED  //共享中断
 @name:中断的名字   //cat /proc/interrupts 
 @dev:向中断处理函数传递的参数
返回值:成功返回0,失败返回错误码
void *free_irq(unsigned int irq, void *dev)
功能:释放中断
参数:
     @irq:软中断号
  @dev:向中断处理函数传递的参数
返回值:返回request_irq的第4个参数name
#define IRQNAME "key_irq" //中断的名字
struct device_node* node;	//设备树的节点
unsigned int irqno;		//软终端号

irqreturn_t key_irq_handle(int irq, void *dev)//中断处理函数第二个参数
	{
    	return IRQ_HANDLED; //中断处理完成了
     }
// 1.获取节点
    node = of_find_node_by_path("/myirqs");
    if (node == NULL) {
        printk("get node error\n");
        return -EINVAL;
    }
    // 2.根据节点获取软中断号
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        irqno[i] = irq_of_parse_and_map(node, i);
        if (irqno[i] == 0) {
            printk("get irq number error\n");
            return -EAGAIN;
        }
        // 3.注册中断
        ret = request_irq(irqno[i], key_irq_handle, 
            IRQF_TRIGGER_FALLING, name[i], (void *)i);
        if (ret) {
            printk("request irq error\n");
            return ret;
        }
    }
static void __exit mycdev_exit(void){
    int i;
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        free_irq(irqno[i], (void *)i);
    }
}

中断底半部

在中断处理函数中不能够做延时或者耗时的操作,但是有的时候有希望在中断到来的时候做尽可能的操作,所以两者是矛盾的,内核为了解决这个矛盾推出中断底半部的机制。分别是软中断(32),tasklet,工作队列。

中断顶部 :紧急的,不耗时的操作

中断底半部:不紧急的,耗时的操作

例如:在网卡中断到来的时候,会从网络上读取数据包,这个数据读取的过程就是耗时操作,所以就可以把这个耗时操作放在中断底半部中完成。

tasklet机制

tasklet:tasklet是基于软中断实现的,但是没有个数限制,因为是通过链表实现的。

tasklet工作与中断上下文,在底半部处理函数中可以做耗时操作,但是不能做延时或者休眠的操作。tasklet是中断的一个部分,不能够脱离中断执行

1.分配对象
    struct tasklet_struct //初始化
    {
        struct tasklet_struct *next; //指向下一个成员
        unsigned long state;         //状态
        atomic_t count;              //计数值
        bool use_callback;           //use_callback 真callback    假func
        union {
            void (*func)(unsigned long data);   //底半部处理函数
            void (*callback)(struct tasklet_struct *t);
        };
        unsigned long data;  //向底半部处理函数传递参数
    };
2.初始化对象
    void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)  //旧版本
 void tasklet_setup(struct tasklet_struct *t,
     void (*callback)(struct tasklet_struct *))   //新版本
3.调用执行
     void tasklet_schedule(struct tasklet_struct *t)
struct tasklet_struct mytask;//定义 
void tasklet_callback(struct tasklet_struct* task){
    int i=30;
    while(--i){
        printk("i = %d\n",i);
    }
}
irqreturn_t key_irq_handle(int irq, void* dev){ //注册中断的第二个参数
    tasklet_schedule(&mytask);//唤醒中断
    return IRQ_HANDLED;
}
//初始化tasklet
 tasklet_setup(&mytask, tasklet_callback);
工作队列机制

工作队列:工作队列工作与进程上下文,没有个数限制,可以脱离中断单独执行。
工作队列中可以有延时,耗时,甚至休眠的操作。
在内核启动的时候默认会启动一个events的线程,这个线程默认处于休眠状态,如果你想让这个线程执行你的代码,只需要将work提交到这个线程维护的队列中。唤醒这个休眠的线程,这个线程就会执行你的驱动的代码。

1.分配对象
    struct work_struct {
        atomic_long_t data;  //携带的数据
        struct list_head entry; //构成队列
        work_func_t func;  //底半部处理函数
    };
    typedef void (*work_func_t)(struct work_struct *work);//不用管 
 struct work_struct work; //定义
2.对象初始化
    void work_func(struct work_struct *work){
 	}
    INIT_WORK(&work, work_func)//初始化    
3.调用执行
     bool schedule_work(struct work_struct *work)
struct work_struct work;//定义
void work_func(struct work_struct *work){
    int i=30;
    while(--i){
        mdelay(500);
        printk("i = %d\n",i);
    }
}
irqreturn_t key_irq_handle(int irq, void* dev){
    schedule_work(&work); //唤醒
    return IRQ_HANDLED;
}
//初始化工作队列
    INIT_WORK(&work,work_func);

platform总线驱动

在linux内核中,将驱动分成了三个部分device,bus,driver,device是用来描述硬件设备的bus就是用来描述总线的,driver就相当于是驱动。device会被放到内核的klist_device链表中,driver会被放在klist_driver链表中通过bus完成匹配过程。当匹配成功之后会执行driver中的probe函数,当device和driver分离的时候就会执行driver中的remove函数。

API

只需要看驱动端就可以了,设备端后期演变成设备树

1.分配并初始化对象
    struct platform_driver  drv={ //操作方法结构体
        int (*probe)(struct platform_device *);    //匹配成功执行的函数
        int (*remove)(struct platform_device *); //分离的时候执行的函数
        struct device_driver driver;   //父类
        const struct platform_device_id *id_table;  //2.idtable匹配
    };
     struct device_driver {
      		const char  *name;                        //1.名字匹配 随便写
            const struct of_device_id *of_match_table; //3.设备树匹配
        }2.注册
     platform_driver_register(drv)  
 3.注销
 void platform_driver_unregister(struct platform_driver *);
以下获取中断的设备信息
struct resource *platform_get_resource(struct platform_device *dev,unsigned int type, unsigned int index)
功能:在驱动中获取设备信息
参数:
    @dev :platform_device的结构体指针
   @type:资源的类型
   @index:同类型资源的索引号
返回值:成功返回resource的结构体指针,失败返回NULL      
int platform_get_irq(struct platform_device *dev, unsigned int index)
功能:获取中断类型的资源
参数:
    @dev :platform_device的结构体指针
    @index:中断类型资源的索引号    
返回值:成功返回中断号,失败返回错误码

一建注册宏

#define module_platform_driver(__platform_driver) //使用他就可以了,不需要添加__init,__exit
 module_driver(__platform_driver, platform_driver_register,
    platform_driver_unregister)
        
#define module_driver(__driver, __register, __unregister, ...) 
static int __init pdrv_init(void) 
{ 
 return platform_driver_register(&pdrv); 
} 
module_init(pdrv_init); 
static void __exit pdrv_exit(void) 
{ 
 platform_driver_unregister(&pdrv); 
} 
module_exit(pdrv_exit);
struct resource *res;
int irqno;
int pdrv_probe(struct platform_device*pdev){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    return 0;
}
int pdrv_remove(struct platform_device*pdev){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    return 0;
}
struct platform_driver pdrv = {
    .probe = pdrv_probe,
    .remove = pdrv_remove,
    .driver = {
        .name = "duangduangduang",
    },
};
module_platform_driver(pdrv);//使用它代替了init,exit
MODULE_LICENSE("GPL");

MODULE_DEVICE_TABLE

MODULE_DEVICE_TABLE(总线类型,idtable数组首地址); //MODULE_DEVICE_TABLE(of,oftable )
这个宏实现一个热插拔的效果,如果在驱动中实现了这宏,在安装
设备的时候,会自动将驱动一并安装上。例如在向ubuntu上插入U盘
的时候会将U盘的驱动安装成功。

测试过程如下:
1.先将pdev.ko和pdrv.ko放到ubuntu的如下目录中
/lib/modules/你的内核版本/kernel/drivers/platform/
2.执行depmod -a命令,让内核重新检索文件的位置
3.安装pdev.ko,pdrv.ko会被自动安装

const struct of_device_id oftable[] = {
    {.compatible = "hqyj,myplatform",},
    {/*end*/}
};
MODULE_DEVICE_TABLE(of,oftable);
struct platform_driver pdrv = {
    .probe = pdrv_probe,
    .remove = pdrv_remove,
    .driver = {
        .name = "duangduangduang",
        .of_match_table = oftable,
    },
};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");

platform中匹配的方法

const struct of_device_id *of_match_table;

struct of_device_id {
 char name[32];
 char type[32];
 char compatible[128];  //通过本选项和设备树完成匹配
 const void *data;
};

struct of_device_id oftable[] = {
    {.compatible = "xxx,xx0",},
    {.compatible = "xxx,xx1",},
    {.compatible = "xxx,xx2",},
    {/*end*/}
};
MODULE_DEVICE_TABLE(of,oftable);

struct platform_driver {
     .driver = {
         	.name="duangduang", //随便填 但是必须填写
            .of_match_table = oftable,
        },
};
struct resource *res;
int irqno,gpiono;
int pdrv_probe(struct platform_device*pdev){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    //能够解析设备树中的reg成员
    res = platform_get_resource(pdev,IORESOURCE_MEM,0);
    if(res == NULL){
        printk("platform get resource error\n");
        return -EINVAL;
    }
    
    //能够直接得到设备树上的中断号并解析为软中断号
    irqno = platform_get_irq(pdev,0);
    if(irqno < 0){
        printk("platform get irq error\n");
        return irqno;
    }

    printk("addr = %#x,irqno = %d\n",res->start,irqno);
    //
    gpiono = of_get_named_gpio(pdev->dev.of_node,"led1",0);//pdev->dev.of_node 设备节点
    if(gpiono < 0){
        printk("get gpio number error\n");
        return gpiono;
    }
    printk("gpio number = %d\n",gpiono);
    return 0;
}
int pdrv_remove(struct platform_device*pdev){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    return 0;
}
const struct of_device_id oftable[] = {
    {.compatible = "hqyj,myplatform",},
    {/*end*/}
};
MODULE_DEVICE_TABLE(of,oftable);
struct platform_driver pdrv = {
    .probe = pdrv_probe,
    .remove = pdrv_remove,
    .driver = {
        .name = "duangduangduang",
        .of_match_table = oftable, //中断
    },

};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");

i2c总线驱动(day9)

i2c总线分析

两根线:
sda:数据线 scl:数据线
四种信号:
起始信号:当scl为高电平的时候,sda从高到低的跳变
停止信号:当scl为高电平的时候,sda从低到高的跳变
应答信号:在第九个时钟周期的时候,sda低电平表示应答
非应答信号:在第九个时钟周期的时候,sda持续高电平表示非应答
写的时序:
start+(7位从机地址 1位的写0)+ack+reg(8bit或16bit)+ack+data(8bit或16bit)+ack+stop
读的时序:
start+(7位从机地址 1位的写0)+ack+reg(8bit或16bit)+ack
start+(7位从机地址 1位的读1)+ack+data(8bit或16bit)+NO ack+stop
i2c总线速率:
100K 低速 400K 全速 3.4M 高速
i2c总线特点:
i2c是半双工的,同步,串行,具备通过从机地址寻址从机和应答校验的总线协议

总线API

1.分配对象及初始化
    struct i2c_driver { 
        int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
        int (*remove)(struct i2c_client *client);
        struct device_driver driver;
    };
 struct device_driver {
  const char  *name;//随便写
        const struct of_device_id *of_match_table;
     		//struct of_device_id of_match_table[]={
     			{.compatible="hqyj,si7006"}//设备树里写的名字
     				{}
 				}};
2.注册
    #define i2c_add_driver(driver) \
 i2c_register_driver(THIS_MODULE, driver)
3.注销
    void i2c_del_driver(struct i2c_driver *driver);
4.一键注册的宏
    module_i2c_driver(变量名);
int si7006_probe(struct i2c_client* client, const struct i2c_device_id* id){}
int si7006_remove(struct i2c_client* client){}
struct of_device_id oftable[] = {
    {.compatible = "hqyj,si7006",},
    {}
};
MODULE_DEVICE_TABLE(of, oftable);
struct i2c_driver si7006 = {
    .probe = si7006_probe,
    .remove = si7006_remove,
    .driver = {
        .name = "duang", //随便写,必须写
        .of_match_table = oftable,
    }
};
module_i2c_driver(si7006);
MODULE_LICENSE("GPL");

i2c相关结构体(day10)

当设备驱动(i2c_driver)和控制器驱动(i2c_adapter)就会在内核中创建一个i2c_client结构体,这个结构体就是记录那个adapter和那个driver匹配成功的对象。

//i2c 7位 8位 10位从机寻址过程
//https://www.totalphase.com/support/articles/200349176-7-bit-8-bit-and-10-bit-I2C-Slave-Addressing
struct i2c_client {  
    unsigned short flags; // 0写  1读
    unsigned short addr;  //从机地址  
    char name[I2C_NAME_SIZE]; //驱动的名字
    struct i2c_adapter *adapter;//控制器驱动的对象
    struct device dev;  //这是设备的对象
};

消息结构体

struct i2c_msg {
 __u16 addr;    //从机地址struct i2c_client中有从机地址
 __u16 flags;   //读写标志位  0写 1读
 __u16 len;    //消息长度
 __u8 *buf;    //消息首地址
};

封装读写的的消息

start+7位从机地址 1位的写0+ack+reg(8bit或16bit)+ack+data(8bit或16bit)+ack+stop
char w_buf[] = {reg,data};
struct i2c_msg w_msg = {
    .addr  = client->addr,
    .flags = 0,
    .len   = 2,
    .buf   = w_buf,
};
start+7位从机地址 1位的写0+ack+reg(8bit)+ack
start+7位从机地址 1位的读1+ack+data(8bit)+NO ack+stop
char r_buf[] = {reg};
char val;
struct i2c_msg r_msg[] = {
    [0] = {
        .addr = client->addr,
        .flags = 0,
        .len = 1,
        .buf = r_buf,
    },
    [1] = {
        .addr = client->addr,
        .flags = 1,
        .len = 1,
        .buf = &val; //读取的数据给val
    },
};

消息发送函数

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
功能:消息的发送函数
参数:
    @adap:控制器的结构体对象//struct i2c_client 中有
    @msgs:消息结构体的首地址//struct i2c_mag结构体 r_msg
	@num:消息结构体的个数//ARRAY_SIZE(r_msg)
返回值:成功返回num,否则就是失败

stm32mp157a

i2c读取温湿度

0xe5 湿度 0xe3温度

设备树
0.i2c硬件的连接图 需要直到gpio引脚 和 从机地址
1.查看i2c控制器的设备树
 stm32mp151.dtsi  //根设备树
 stm32mp15xx-dkx.dtsi //别人写的
 /home/linux/linux-5.10.61/Documentation/devicetree/bindings/i2c //都是别人写的
2.参考内核的帮助文档编写自己的设备树
    stm32mp157a-fsmp1a.dts
温湿度代码
#ifndef __SI7006_H__
#define __SI7006_H__

#define GET_HUM  _IOR('l',2,int)
#define GET_TMP  _IOR('l',3,int)

#define HUM_ADDR 0xe5
#define TMP_ADDR 0xe3
#endif
//驱动

#define I2CNAME "si7006"
struct i2c_client* gclient;
int major = 0;
struct class* cls;
struct device* dev;

int i2c_read_hum_tmp(unsigned char reg){
    // 1.封装消息
    int ret;
    unsigned char r_buf[] = { reg };
    unsigned short val;
    struct i2c_msg r_msg[] = {
        [0] = {
            .addr = gclient->addr,
            .flags = 0,
            .len = 1,
            .buf = r_buf,
        },
        [1] = {
            .addr = gclient->addr,
            .flags = 1,
            .len = 2,
            .buf = (char *)&val,
        },
    };
    // 2.发送消息
    ret = i2c_transfer(gclient->adapter, r_msg, ARRAY_SIZE(r_msg));
    if (ret != ARRAY_SIZE(r_msg)) {
        printk("i2c read hum or temp error\n");
        return -EAGAIN;
    }

    return val >> 8 | val << 8;
}
long si7006_ioctl(struct file* file,unsigned int cmd, unsigned long arg){
    int ret, data;
    switch (cmd) {
    case GET_HUM:
        data = i2c_read_hum_tmp(HUM_ADDR);
        if (data < 0) {
            printk("i2c read error\n");
            return data;
        }
        data = data & 0xffff; //返回的是int类型的所以需要把其它的清空了
        ret = copy_to_user((void*)arg, &data, 4);
        if (ret) {
            printk("copy data to user error\n");
            return -EINVAL;
        }
        break;
    case GET_TMP:
        data = i2c_read_hum_tmp(TMP_ADDR);
        if (data < 0) {
            printk("i2c read error\n");
            return data;
        }
        data = data & 0xffff;
        ret = copy_to_user((void*)arg, &data, 4);
        if (ret) {
            printk("copy data to user error\n");
            return -EINVAL;
        }
        break;
    }
    return 0;
}

spi总线驱动

SPI 是串行外设接口(Serial Peripheral Interface)的缩写。 是 Motorola 公司推出的一种同步串行接口技术,是一种 高速的,全双工,同步的通信总线。
SPI优点
支持全双工通信
通信简单
数据传输速率快
1):高速、同步、全双工、非差分、总线式
2):主从机通信模式
缺点
没有指定的流控制,没有应答机制确认是否接收到数据,
所以跟IIC总线协议比较在数据的可靠性上有一定的缺陷。

spi时序解析

可以一主机多从机,具体和那个从机通讯通过cs片选决定。
MISO :主机输入,从机输出
MOSI :主机输出,从机输入
CLK :时钟线(只能主机控制)
CS :片选线

数据传输的四种方式:
CPOL(时钟极性) : 0:时钟起始位低电平 1:时钟起始为高电平
CPHA(时钟相位) :0:第一个时钟周期采样 1:第二个时钟周期采样

例如:

CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。

CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。

CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。

CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。

总线API

1.分配并初始化对象
 struct spi_driver {
  int   (*probe)(struct spi_device *spi);
  int   (*remove)(struct spi_device *spi);
  struct device_driver driver;
 };
     struct device_driver {
      const char *name;
            const struct of_device_id *of_match_table;
                //struct of_device_id  of_device_id={
         		//MODULE_DEVICE_TABLE(of,of_match);
                     {compatible="hqyj,ic2"},
                     {}
                 };
        }
2.注册 
    #define spi_register_driver(driver) \
 __spi_register_driver(THIS_MODULE, driver)
3.注销
     void spi_unregister_driver(struct spi_driver *sdrv)
4.一键注册的宏
    module_spi_driver(结构体变量名);

spi收发数据

给寄存器读写用的,

int spi_write(struct spi_device *spi, const void *buf, size_t len) //发数据
功能:发送数据
参数:
    @spi:probe函数的参数,需要把他弄成全局量使用
    @buf:数据
    @len:数据的大小
返回值:成功返回 0 | 失败返回错误码
int spi_read(struct spi_device *spi, void *buf, size_t len)       //接收数据
int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx,
  void *rxbuf, unsigned n_rx); //同时收发
#define NAME "m74hc595"
int major = 0;
struct class *cls;
struct device *dev;
struct spi_device *gspi;
u8 code[] = {
 0x3f, //0
 0x06, //1
 0x5b, //2
 0x4f, //3
 0x66, //4
 0x6d, //5
 0x7d, //6
 0x07, //7
 0x7f, //8
 0x6f, //9
 0x77, //A
 0x7c, //b
 0x39, //c
 0x5e, //d
 0x79, //e
 0x71, //f
};

u8 which[] = {
 0x1, //sg0
 0x2, //sg1
 0x4, //sg2
 0x8, //sg3
};

int m74hc595_open(struct inode *inode, struct file *file){
     printk("%s:%d\n",__func__,__LINE__);
     return 0;
}
long m74hc595_ioctl(struct file *file, unsigned int cmd, unsigned long args){ 
     switch(cmd){
      	case SEG_WHICH:
       		spi_write(gspi,&which[args],1);
       	break;
      	case SEG_DAT:
       		spi_write(gspi,&code[args],1);
       	break;
      	default: printk("ioctl error\n");break;
 	} 
 	return 0;
}
int m74hc595_close(struct inode *inode, struct file *file){
     printk("%s:%d\n",__func__,__LINE__);
     return 0;
}
struct file_operations fops = {
     .open = m74hc595_open,
     .unlocked_ioctl = m74hc595_ioctl,
     .release = m74hc595_close,
};

int m74hc595_probe(struct spi_device *spi){
 u8 buf[2] = {0xf,0x0};
 printk("%s:%d\n",__func__,__LINE__);
 gspi = spi;
 spi_write(gspi,buf,ARRAY_SIZE(buf));
 
 major = register_chrdev(0,NAME,&fops);
 if(major < 0){
  printk("register chrdev error\n");
  return major;
 }

 cls = class_create(THIS_MODULE,NAME);
 if(IS_ERR(cls)){
  printk("class create  error\n");
  return PTR_ERR(cls);
 }
 dev = device_create(cls,NULL,MKDEV(major,0),NULL,NAME);
 if(IS_ERR(dev)){
  printk("device create  error\n");
  return PTR_ERR(dev);
 }
 return 0;
}

int m74hc595_remove(struct spi_device *spi)
{
     printk("%s:%d\n",__func__,__LINE__);
     device_destroy(cls,MKDEV(major,0));
     class_destroy(cls);
     unregister_chrdev(major,NAME);
     return 0;
}

const struct of_device_id of_match[] = {
 {.compatible = "hqyj,m74hc595",},
 	{},
};
MODULE_DEVICE_TABLE(of,of_match);
struct spi_driver m74hc595 = {
     .probe = m74hc595_probe,
     .remove = m74hc595_remove,
     .driver = {
      .name = "hello_m74hc595",
      .of_match_table = of_match,
 	}, 
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");

PWM

蜂鸣器和风扇一样

struct pwm_device *pwm_get(struct device *dev, const char *con_id);
功能:申请一个PWM设备
参数:
 	@dev:platform_device中的dev
    @con_id:NULL
返回值:成功返回 设备指针 | 失败返回 ERR_PTR()的错误码
        
int pwm_config(struct pwm_device *pwm, int duty_ns,int period_ns);
功能:配置PWM周期和占空比
参数:
    @pwm:pwm设备指针
    @duty_ns:时间(纳秒)
    @period_ns:一个周期持续的时间(纳秒)
返回值: 成功返回 0 | 失败返回错误码
 
int pwm_enable(struct pwm_device *pwm); //先配置周期和占空比后使能
功能:使能pwm
参数:
    @pwm:pwm设备指针
返回值:成功 0 | 失败返回错误码
 
int pwm_disable(struct pwm_device *pwm);
功能:关闭PWM
参数:
    @pwm:pwm设备指针
返回值:

void pwm_free(struct pwm_device *pwm);
功能:释放申请的pwm
参数:
    @pwm:pwm的指针
返回值:
#define BEENAME "mybee"
#define HZ_TO_NANOSECONDS(x) (1000000000UL/(x))
/*
struct pwm_beeper{  
	struct pwm_device *pwm;
 	unsigned long period; 
};
struct pwm_beeper *beeper;  //使用这个结构体也可以
*/
struct cdev *bee_cdev;
dev_t bee_dev;
int minor=0;
struct class *bee_class;
struct device *bee_up_device;

struct pwm_device *bee_pwm;//pwm
unsigned long period; //周期

int bee_open (struct inode *inode, struct file *file){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    return 0;
}
long bee_ioctl (struct file *file, unsigned int cmd, unsigned long age){
    
    int ret;
    int value;

    ret=copy_from_user(&value,(void*)age,sizeof(value));
    if(ret<0){
        printk("copy from user error\n");
        return -1;
    }

    if(value==0){
        pwm_disable(bee_pwm);
    }else{
        period=HZ_TO_NANOSECONDS(value);
        ret=pwm_config(bee_pwm,period/2,period);
        if(ret){
            printk("pwm config error\n");
            return ret;
        }
        ret=pwm_enable(bee_pwm);
        if(ret){
            printk("pwm enable error\n");
            return ret;
        }
    }
    return 0;
}
int bee_close (struct inode *inode, struct file *file){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    pwm_disable(bee_pwm); //关闭PWM设备
    return 0;
}
static const struct file_operations fops={
    //操作方法结构体 使用的是ioctl
};
int bee_probe(struct platform_device *pdev){
    int ret=0;
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    
    bee_up_device=&pdev->dev;//因为类型一样,所以只是用它接了一下,可以分装成结构体上面有结构体
    //struct device *dev = &pdev->dev; //该这样写
    
    bee_pwm=pwm_get(bee_up_device,NULL); //申请PWM设备
    if(IS_ERR(bee_pwm)){
        printk("pwm get err or\n");
        goto ERR;
    }
    //低下是申请节点并注册
    bee_cdev=cdev_alloc();
    cdev_init(bee_cdev,&fops);
    ret=alloc_chrdev_region(&bee_dev,minor,0,BEENAME);
    ret=cdev_add(bee_cdev,bee_dev,1);
    bee_class=class_create(THIS_MODULE,BEENAME);
    bee_up_device=device_create(bee_class,NULL,bee_dev,NULL,BEENAME);
    return 0;
//这里回收没有写
}
int bee_remove(struct platform_device *pdev){
    device_destroy(bee_class,bee_dev);
    class_destroy(bee_class);
    pwm_disable(bee_pwm);//关闭PWM
	pwm_free(bee_pwm);//释放pwm
    cdev_del(bee_cdev);
    unregister_chrdev_region(bee_dev,1);
    kfree(bee_cdev);
    return 0;
}
//platform一建注册宏

块设备驱动(day10)

内核分配的函数

void *kmalloc(size_t s, gfp_t gfp)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
  flags:内存分配标志
  GFP_KERNEL:内核可能被休眠,用于进程上下文中
  GFP_ATOMIC:处理紧急的事务,用在中断上下文
返回值:对应虚拟地址
特点:最大128k , 分配虚拟地址,其虚拟地址空间连续,
      物理地址空间也是连续,分配的内存必须是2的次幂的形式
类似函数:kzalloc = kmalloc+memset(,0,):分配虚拟内存区并清零      
void kfree(const void *x)
功能:释放对应的虚拟内存
参数:x:虚拟内存的起始地址
返回值:无
    
void *vmalloc(unsigned long size)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
返回值:对应虚拟地址
特点:分配虚拟地址,其虚拟地址空间连续,
但是物理地址空间不一定连续    
void vfree(const void *addr)
功能:释放对应的虚拟内存
参数:addr:虚拟内存区的首地址
返回值:无    
    
unsigned long __get_free_page(gfp_t gfp)
功能:分配一个页的内存 4K   
void free_page(unsigned long addr)
释放一个页的内存
    
unsigned long __get_free_pages(gfp_t gfp_mask, get_order(57600))
功能:分配多个页的内存
57600-->2^n :第二个参数填写的是n
n = get_order(57600)
void free_pages(unsigned long addr, unsigned long order)
释放多个页的内存

步骤

|driver:gendisk
    1.分配对象 gendisk对象
    2.对象初始化
    3.初始化一个队列  head----request(read)----request(write)---...
     //4.硬盘设备的初始化
    5.注册、注销
函数
struct gendisk {   
        int major;   //块设备的主设备号 需要调用函数 register_blkdev
        int first_minor; //起始的次设备号,直接填
        int minors; //设备的个数,分区的个数,说明硬盘最多可以分多少分区 alloc_disk函数搞定了
        char disk_name[DISK_NAME_LEN]; //磁盘的名字,不用管alloc_disk已经完成必要的初始化了
        struct disk_part_tbl  *part_tbl; //磁盘的分区表的首地址,set_capacity需要设置一下↓
        struct hd_struct part0;//part0分区的描述  需要调用函数 ↓
        const struct block_device_operations *fops;//块设备的操作方法结构体 ↓
        struct request_queue *queue;//队列(重要)需要调用函数 ↓
        void *private_data;//私有数据,可以用它来传参
    };
    struct hd_struct { //分区的结构体
        sector_t start_sect; //起始的扇区号
        sector_t nr_sects;   //扇区的个数   
        int  partno;        //分区号
    };
    struct block_device_operations { //操作方法结构体
        int (*open) (struct block_device *, fmode_t);
        int (*release) (struct gendisk *, fmode_t);
        int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*getgeo)(struct block_device *, struct hd_geometry *); 
        //设置磁盘的磁头,磁道,扇区的个数的。hd_geometry
    } 
struct gendisk *alloc_disk(int minors) 
    //void put_disk(struct gendisk *disk)
    //归还引用计数
功能:分配gendisk的内存,然后完成必要的初始化
参数:
    @minors:分区的个数 //是gdendisk结构体里的
返回值:成功返回分配到的内存的首地址,失败返回NULL
int register_blkdev(unsigned int major, const char *name)
    //void unregister_blkdev(unsigned int major, const char *name)
功能:申请设备设备驱动的主设备号
参数:
   @major : 0:自动申请
                  >0 :静态指定
  @name  :名字  cat /proc/devices
返回值: 
     major=0 ;成功返回主设备号,失败返回错误码
     major>0 :成功返回0 ,失败返回错误码
void set_capacity(struct gendisk *disk, sector_t size)
    功能:设置磁盘的容量
struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,const struct blk_mq_ops *ops,unsigned int queue_depth,unsigned int set_flags)
    //void blk_cleanup_queue(struct request_queue *q)
功能:用于在给定队列深度的情况下使用mq ops设置队列的助手,以及通过mq ops标志传递的助手
参数:
   @被初始化的tag对象,tag被上层使用,里面包含硬件队列的个数,队列的操作方法结构体,标志位等,不用管直接定义使用,这个函数就是给blk_mq_tag_set *set初始化的给上层使用的
   @放入到tag中的操作方法结构体
   @ tag中指定支持的队列深度
   @将tag中队列的处理标志位,例如BLK_MQ_F_SHOULD_MERGE, BLK_MQ_F_BLOCKING等
   返回值:成功返回队列指针,失败返回错误码指针 
3.注册、注销
    void add_disk(struct gendisk *disk)
    //注册
    void del_gendisk(struct gendisk *disk)
    //注销
// 1.分配gendisk
struct gendisk* mydisk;    
	mydisk = alloc_disk(4);
    if (mydisk == NULL) {
        printk("alloc disk memory error\n");
        return -ENOMEM;
    }
// 2.对象初始化
#define BLKSIZE (1 * 1024 * 1024) // 1M

int major = 0;
    major = register_blkdev(0, DISKNAME);//分配节点
    if (major < 0) {
        printk("alloc block device number error\n");
        return major;
    }

struct gendisk* mydisk;
    set_capacity(mydisk, BLKSIZE >> 9);//设置磁盘的容量

struct request_queue* q;
struct blk_mq_tag_set set;
blk_status_t mydisk_queue_rq(struct blk_mq_hw_ctx* ctx, const struct blk_mq_queue_data* hd){
    //队列的读写操作
    return BLK_STS_OK;
}
struct blk_mq_ops mqops = { //操作方法结构体
    .queue_rq = mydisk_queue_rq,
};
    q = blk_mq_init_sq_queue(&set, &mqops, 2, BLK_MQ_F_SHOULD_MERGE);//
    if (IS_ERR(q)) {
        printk("mutil queue init error\n");
        return PTR_ERR(q);
    }

    mydisk->major = major; 
    mydisk->first_minor = 0; //次设备号
    strcpy(mydisk->disk_name, DISKNAME); //名字
    mydisk->fops = &fops; //操作方法结构体
    mydisk->queue = q;

#include <linux/blk-mq.h>
#include <linux/genhd.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/hdreg.h>

#define BLKSIZE (1 * 1024 * 1024) // 1M
#define DISKNAME "mydisk"
struct gendisk* mydisk;
int major = 0;
struct request_queue* q;
struct blk_mq_tag_set set;
char *disk_addr = NULL;  //磁盘的首地址

blk_status_t mydisk_queue_rq(struct blk_mq_hw_ctx* ctx,const struct blk_mq_queue_data* hd){
    //队列的读写操作
    return BLK_STS_OK;
}
struct blk_mq_ops mqops = {
    .queue_rq = mydisk_queue_rq,
};
int mydisk_open(struct block_device*blkdev, fmode_t mode){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    return 0;
}
int mydisk_getgeo(struct block_device *blkdev, struct hd_geometry *hd){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
    /*
    struct hd_geometry {
      unsigned char heads;  //磁头的个数
      unsigned char sectors; //扇区格式
      unsigned short cylinders;//磁道个数
    };*/
    hd->heads = 4;
    hd->cylinders = 16;
    hd->sectors = BLKSIZE/hd->heads/hd->cylinders/512;
    return 0;
}
void mydisk_close(struct gendisk*disk, fmode_t mode){
    printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);\
}
struct block_device_operations fops = {
    .open = mydisk_open,
    .release = mydisk_close,
    .getgeo = mydisk_getgeo,
};
static int __init mydisk_init(void){
    // 1.分配gendisk
    mydisk = alloc_disk(4);
    if (mydisk == NULL) {
        printk("alloc disk memory error\n");
        return -ENOMEM;
    }
    // 2.对象初始化
    major = register_blkdev(0, DISKNAME);
    if (major < 0) {
        printk("alloc block device number error\n");
        return major;
    }

    set_capacity(mydisk, BLKSIZE >> 9);

    q = blk_mq_init_sq_queue(&set, &mqops, 2, BLK_MQ_F_SHOULD_MERGE);
    if (IS_ERR(q)) {
        printk("mutil queue init error\n");
        return PTR_ERR(q);
    }

    mydisk->major = major;
    mydisk->first_minor = 0;
    strcpy(mydisk->disk_name, DISKNAME);
    mydisk->fops = &fops;
    mydisk->queue = q;
    // 3.分配1M的内存当成硬盘使用
    disk_addr = vmalloc(BLKSIZE);
    if(disk_addr == NULL){
        printk("alloc disk memory error\n");
        return -ENOMEM;
    } 
    // 4.注册
    add_disk(mydisk);

    return 0;
}
static void __exit mydisk_exit(void){
    del_gendisk(mydisk);
    vfree(disk_addr);
    blk_cleanup_queue(q);
    unregister_blkdev(major,DISKNAME);
    put_disk(mydisk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");

处理相关结构体

struct  request_queue{
    /*双向链表数据结构,将所有加入到队列的IO请求组建成一个双向链表*/
    struct  list_head  queue_head; 
    struct list_head    requeue_list; //request队列
    spinlock_t      requeue_lock;     //队列自旋锁
    unsigned long     nr_requests;     /* 最大的请求数量 */
    unsigned long     queue_flags;/*当前请求队列的状QUEUE_FLAG_STOPPED*/};

struct  request{
    struct list_head queuelist;/* 请求对象中的链表元素*/
    struct request_queue *q; /* 指向存放当前请求的请求队列*/
    unsigned int __data_len; /* 当前请求要求数据传输的总的数据量 */
    sector_t __sector;         /* 当前请求要求数据传输的块设备的起始扇区 */
    struct bio *bio;  /* bio对象所携带的信息转存至请求对象中*/
    struct bio *biotail; /* bio链表*/};
通常一个request请求可以包含多个bio,一个bio对应一个I/O请求  
 struct bio {  
    struct bio *bi_next;  /* 指向当前bio的下一个对象*/ 
    unsigned long  bi_flags;   /* 状态、命令等 */ 
    unsigned long bi_rw;   /* 表示READ/WRITE 读写*/ 
    struct block_device *bi_bdev;    /* 与请求相关联的块设备对象指针*/ 
    unsigned short bi_vcnt;  /* bi_io_vec数组中元素个数 */ 
    unsigned short bi_idx;  /* 当前处理的bi_io_vec数组元素索引 */
    unsigned int bi_size;  /* 本次传输需要传输的数据总量,byte(扇区大小整数倍) */ 
    struct bio_vec *bi_io_vec;/* 指向一个IO向量的数组,数组中的内各元素对应一个物理页的page对象 */
  };

struct bio_vec {  
    struct page  *bv_page; //指向用于数据传输的页面所对应的struct page对象
    unsigned int bv_len;   //表示当前要传输的数据大小  
    unsigned int bv_offset;//表示数据在页面内的偏移量 
};

队列处理函数

blk_mq_start_request(rq); //开始处理队列
blk_mq_end_request(rq, BLK_STS_OK); //结束队列处理
rq_for_each_segment(bvec, rq, iter) //从request->bio_vec
void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset; //将页地址转换为线性地址(内地址)
rq_data_dir(rq))   //从request获取本次读写的方向  WRITE 1   READ 0
dev_addr+(rq->__sector *512) //磁盘设备的地址

摄像头驱动

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值