linux驱动程序

96 阅读24分钟

驱动程序固定格式

  1. 依赖<linux/init.h>和<linux/module.h>
  2. 入口函数 module_init(fun_init) 是int静态函数
  3. 出口函数 module_exit(fun_exit) 是voidt静态函数
  4. 许可证 MODULE_LICCENSE("GPL")

固定格式

#include <linux/init.h> 
#include <linux/module.h>
// 入口文件
static int __init fun_init(void){
    return 0;
}
module_init(fun_init);
// 出口文件
static int __exit fun_exit(void){
    return 0;
}
module_exit(fun_exit);
MODULE_LICCENSE("GPL");

通用编译文件

  1. 编译指令 make arch=x86/arm modename=1_hello(文件名不带后缀)
  2. 清除编译后文件 make clean
  3. 加载模块 sudo insmod 1_hello.ko
  4. 加载模块和依赖 sudo modprobe -a 1_hello.ko // -r卸载 -l列出已加载 -v显示详细信息 -a加载所有依赖 -F显示模块配置
  5. 卸载模块 sudo rmmod 1_hello.ko
  6. 查看模块信息 sudo lsmod 1_hello.ko
  7. 查看调试信息(后十条,后创建的在后) dmesg | tail
  8. 查看驱动程序是否在运行(前十条,后创建的在前) lsmod | head
#arch 如果是第一次出现 赋值 arm架构,如果不是 命令行有参数传进来 按照参数指定 
arch ?= arm
modename ?= test
ifeq ($(arch),arm)
#源代码目录准备
#板载源代码
KERNELDIR:=/home/linux/quDong/linux-5.4.31
else
#乌班图源代码
KERNELDIR:=/lib/modules/5.4.0-26-generic/build
endif

#执行一个shell 命令
PWD:=$(shell pwd)

#换行 tab键
#模块位置
all:
	make -C $(KERNELDIR) M=$(PWD) modules
#依赖  命令
clean:
	make -C $(KERNELDIR) M=$(PWD) clean
#最后形成的模块名字是什么?
#指定要构建的模块名字 对应的 1_hello这个源文件
obj-m:=$(modename).o

驱动程序内打印信息

  • 格式 1是级别1~7,参数二打印内容,参数参以后都是打印内容的值,参数一和参数二没有逗号
  • printk(1"xxxx%d",6);
  • 等级可以不写,不写等级默认为4
  • 在黑白终端中,要打印级别高于4级才显示
  • 基本上和c语言的printf一样,只是多了个等级
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
// 入口文件
static int __init fun_init(void)
{
    printk(KERN_EMERG"1\n");
    printk(KERN_ALERT "2\n");
    printk(KERN_CRIT"3\n");
    printk(KERN_ERR"4\n");
    printk(KERN_WARNING"5\n");
    printk(KERN_NOTICE"6\n");
    printk(KERN_INFO"7\n");
    printk(KERN_DEBUG"7\n");
    return 0;
}
// 出口文件
static void __exit fun_exit(void)
{
   printk("exit");
}
module_init(fun_init);
module_exit(fun_exit);
MODULE_LICENSE("GPL");

驱动程序传参

执行驱动程序时传递参数

  1. sudo insmod chuancan.ko a=10 b=500 // 指定上通过文件后x=值传递,可以传多个
  2. 只能传递基本类型(char,bool,int,long,short,byte,ushort,uint),数组array,字符串string(charp)
  3. 依赖头文件<linux/moduleparam.h>
  4. 接收基本类型 module_param
  5. 接收数组类型 module_param_array
  6. 接收字符串类型 module_param_string
  7. 接收函数的三个/四个参数 变量名称 变量类型 长度(基本类型没有) 权限(一般设置为0664)
  8. 写入参数说明MODULE_PARM_DESC(arr, "An array of integers")
  9. 查看参数说明指令 modinfo chuancan.ko
  10. 三种类型的参数指令 sudo insmod 4_params.ko a=123 b="hello" arr=10,20,30
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>

int a = 100;
module_param(a, int, 0664);
MODULE_PARM_DESC(a, "a说明");

// 修改:字符串参数使用字符数组
char b[100] = "test";
module_param_string(b, b, sizeof(b), 0664);
MODULE_PARM_DESC(b, "b说明");

// 数组参数
int arr[10] = {0};  // 最多接收10个元素
int arr_len = 0;
module_param_array(arr, int, &arr_len, 0664);
MODULE_PARM_DESC(arr, "arr说明");

// 入口函数
static int __init fun_init(void)
{
    int i;
    printk(KERN_INFO "a = %d, str = %s\n", a, b);

    for (i = 0; i < arr_len; i++) {
        printk(KERN_INFO "arr[%d] = %d\n", i, arr[i]);
    }

    return 0;
}

// 退出函数
static void __exit fun_exit(void)
{
    printk(KERN_INFO "Module exit.\n");
}

module_init(fun_init);
module_exit(fun_exit);

MODULE_LICENSE("GPL");

导出符号表(通过导出符号表,实现驱动模块和驱动模块之间的函数相互调用)

  1. 依赖头文件<linux/export.h>
  2. 导出符号表会有部分安全问题,例如:符号重定义、版本依赖、版权问题、安全漏洞等
  3. 导出函数方法EXPORT_SYMBOL_GPL(funadd);//funadd是要导出的函数

操作顺序

  • 安装要先安装导出模块再安装使用模块,卸载要相反
安装顺序
  1. 安装导出模块 sudo insmod a.ko
  2. 查看 dmesg | tail
  3. 安装使用模块 sudo insmod b.ko
  4. 查看 dmesg | tail
卸载顺序
  1. 卸载导出模块 sudo rmmod b.ko
  2. 查看 dmesg | tail
  3. 卸载使用模块 sudo rmmod a.ko
  4. 查看 dmesg | tail

导出模块

#include <linux/init.h>
#include <linux/module.h>
// 导出符号表头文件
#include <linux/export.h>
// a + b 的和
int funadd(int a, int b)
{
    return a + b;
}

// 导出符号表
EXPORT_SYMBOL_GPL(funadd);
// 入口文件
static int __init fun_init(void)
{
    printk("这里导出符号表funadd\n");
    return 0;
}
// 出口文件
static void __exit fun_exit(void)
{
    printk("exit");
}
module_init(fun_init);
module_exit(fun_exit);
MODULE_LICENSE("GPL");

使用模块

#include <linux/init.h>
#include <linux/module.h>
int funadd(int a,int b);

// 入口文件
static int __init fun_init(void)
{
   printk("这里使用符号表funadd\n");
   printk("res = %d\n",funadd(100,200));
    return 0;
}
// 出口文件
static void __exit fun_exit(void)
{
   printk("exit");
}
module_init(fun_init);
module_exit(fun_exit);
MODULE_LICENSE("GPL");

应用程序调用字符设备驱动程序的函数

  1. 要给驱动程序分配一个主设备号major
  2. 分配设备号依赖头文件<linux/fs.h>
  3. 可以调用api生成,不会重复
  4. 入口程序生成字符设备驱动函数主设备号
  • register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
  1. 出口函数释放设备号
  2. 创建文件操作结构体 struct file_operations {}
  3. 文件操作结构体有一大堆成员函数,常用的四个开关读写open、release、read、write

运行流程

  1. 编译驱动程序,编译程序里需要打印主设备号
  2. 安装驱动程序 sudo insmod cdev.ko
  3. 查看信息,看看主设备号是多少 dmesg | tail
  4. 根据主设备号手动创建设备节点(也可以在代码里创建) sudo mknod /dev/mycdev c 240 0
  5. 编译应用程序,在编译程序调用对应方法,看是否会执行驱动程序的代码(可以在驱动程序增加打印)

驱动程序源码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
int major;              // 主设备号
#define CNAME "chardev" // 字符设备驱动名称

int myopen(struct inode *node, struct file *file)
{
    printk("this is my %s test \n", __func__);
    return 0;
}

int myclose(struct inode *node, struct file *file)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t myread(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t mywrite(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    printk("this is my%s\n", __func__);
    return 0;
}

const struct file_operations fop = {
    .open = myopen,
    .release = myclose,
    .write = mywrite,
    .read = myread
    };

static int __init fun_init(void)
{
    printk("this is %s-%d test\n", __func__, __LINE__);
    major = register_chrdev(0, CNAME, &fop);
    if (major < 0)
    {
        printk("字符设备注册失败\n");
        return -1;
    }
    else
    {
        printk("字符设备注册成功,major:%d\n", major); // 打印主设备号
    }
    return 0;
}
static void __exit fun_exit(void)
{
    unregister_chrdev(major, CNAME);
    printk("this is %s %d exit\n", __func__, __LINE__);
}
module_init(fun_init);
module_exit(fun_exit);
// 证书信息
MODULE_LICENSE("GPL");

应用程序源代码

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/fcntl.h>

int main()
{
    int fd = open("/dev/mycdev", O_RDWR);
    if (fd < 0)
    {
        perror("open error\n");
    }
    printf("open /dev/mycdevs success\n");
    char buf[128] = "Write succ";
    write(fd, buf, strlen(buf));
    close(fd);
    return 0;
}

驱动程序和应用程序相互传递数据

  1. 依赖头文件#include <linux/uaccess.h>
  2. 在驱动文件的write函数中调用copy_from_user函数将应用程序的数据传递到驱动程序
  3. 在驱动文件的read函数中调用copy_to_user函数将驱动程序的数据传递到应用程序
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
int major;              // 主设备号
#define CNAME "chardev" // 字符设备驱动名称
// 内核缓冲区
char kbuf[128] = "";
int myopen(struct inode *node, struct file *file)
{
    printk("this is my %s test \n", __func__);
    return 0;
}

int myclose(struct inode *node, struct file *file)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t myread(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_to_user(ubuf, kbuf, size);
    if (ret < 0)
    {
        printk("copy to user error\n");
        return -EIO;
    }
    return size;
}
ssize_t mywrite(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_from_user(kbuf, ubuf, size);
    if (ret < 0)
    {
        printk("copy from user error\n");
        return -EIO;
    }
    return size;
}

const struct file_operations fop = {
    .open = myopen,
    .release = myclose,
    .write = mywrite,
    .read = myread};

static int __init fun_init(void)
{
    printk("this is %s-%d test\n", __func__, __LINE__);
    major = register_chrdev(0, CNAME, &fop);
    if (major < 0)
    {
        printk("字符设备注册失败\n");
        return -1;
    }
    else
    {
        printk("字符设备注册成功,major:%d\n", major);
    }
    return 0;
}
static void __exit fun_exit(void)
{
    unregister_chrdev(major, CNAME);
    printk("this is %s %d exit\n", __func__, __LINE__);
}
module_init(fun_init);
module_exit(fun_exit);
// 证书信息
MODULE_LICENSE("GPL");

代码中创建设备节点

  1. 依赖头文件<linux/device.h>和<linux/fs.h>
  2. 创建目录指针struct class *cls 和设备文件指针 struct device *dev
  3. 在入口函数中,先创建目录class_create(THIS_MODULE,CNAME)// 参数一当前内核模块(基本上固定是THIS_MODULE),参数二类名
  4. 生成32位综合设备号MKDEV(major,0);//参数1:设备号,参数2:次设备号
  5. 在入口函数中,再创建设备文件device_create(cls,NULL,MKDEV(major,0),NULL,CNAME),参数1:父目录指针,参数2:一般NULL,参数3:设备号,参数4:传递的数据,参数5:文件名称
  6. 设备文件创建失败要销毁类class_destroy(cls)
  7. 出口函数要先销毁设备再销毁类
  8. 应用程序open的设备文件名要和驱动程序用代码创建的文件名一致
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/errno.h>
int major;              // 主设备号
#define CNAME "chardev" // 字符设备驱动名称
// 内核缓冲区
char kbuf[128] = "";
int myopen(struct inode *node, struct file *file)
{
    printk("this is my %s test \n", __func__);
    return 0;
}

int myclose(struct inode *node, struct file *file)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t myread(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_to_user(ubuf, kbuf, size);
    if (ret < 0)
    {
        printk("copy to user error\n");
        return -EIO;
    }
    return size;
}
ssize_t mywrite(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_from_user(kbuf, ubuf, size);
    if (ret < 0)
    {
        printk("copy from user error\n");
        return -EIO;
    }
    return size;
}

const struct file_operations fop = {
    .open = myopen,
    .release = myclose,
    .write = mywrite,
    .read = myread
};

// 创建设备目录指针
struct class *cls;

// 创建设备文件指针
struct device *dev;
static int __init fun_init(void)
{
    printk("this is %s-%d test\n", __func__, __LINE__);
    major = register_chrdev(0, CNAME, &fop);
    if (major < 0)
    {
        printk("字符设备注册失败\n");
        return -1;
    }
    
        printk("字符设备注册成功,major:%d\n", major);
        cls = class_create(THIS_MODULE,CNAME);
    if(IS_ERR(cls)){
        printk("class create error\n");
        return PTR_ERR(cls);
    }
    // 创建设备文件
    dev= device_create(cls,NULL,MKDEV(major,0),NULL,CNAME);
    if(IS_ERR(dev)){
        // 销毁目录
        class_destroy(cls);
        // 注销
        unregister_chrdev(major,CNAME);
        printk("class create error\n");
        return PTR_ERR(dev);
    }
    printk("设备目录和设备文件创建成功\n");
    return 0;
}
static void __exit fun_exit(void)
{
    unregister_chrdev(major, CNAME);
    class_destroy(cls);
    printk("this is %s %d exit\n", __func__, __LINE__);
}
module_init(fun_init);
module_exit(fun_exit);
// 证书信息
MODULE_LICENSE("GPL");

ioremap驱动程序操作寄存器

  1. 依赖头文件<linux/io.h>
  2. 通过void *ioremap(unsigned long phys_addr, unsigned long size)拿到寄存器指针,参数1物理地址,参数2大小
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/io.h>
// 内核缓冲区
char kbuf[128] = "";
#define RCC_BASE 0x50000000 // AHB4总线 GPIOE
#define AHB4_CLOCK_ENABLE (RCC_BASE+0xA28) // AHB4总线地址
// PE10 - LED1
#define GPIOE_BASE 0x50006000
#define GPIO_MODER (GPIOE_BASE+0x00) // 模式寄存器
#define GPIO_OPTYE (GPIOE_BASE+0x04) // 输出类型寄存器
#define GPIO_OUTDATA (GPIOE_BASE+0x14) // 输出数据寄存器

unsigned int *ahb4_clock_enable = NULL;
unsigned int *gpioe_mode = NULL;
unsigned int *gpioe_10_odr = NULL;




int major;              // 主设备号
#define CNAME "leddev" // 字符设备驱动名称

int myopen(struct inode *node, struct file *file)
{
    printk("this is my %s test \n", __func__);
    // 在open函数中完成映射
    // GPIOE时钟 ok
    ahb4_clock_enable = (unsigned int *)ioremap(AHB4_CLOCK_ENABLE,sizeof(unsigned int));
    if(ahb4_clock_enable == NULL){
        printk("ioremap clock error");
        return -1;
    }
    // 驱动第四个位置的GPIOE
    *ahb4_clock_enable |= (1 << 4);
    printk("rcc_ahb4_clockenable success %d\n",__LINE__);

    // 在write函数中 完成控制
    gpioe_mode = (unsigned int *)ioremap(GPIO_MODER,sizeof(unsigned int));
    if(gpioe_mode == NULL ){
        printk("gpioe_mode error\n");
        return -1;
    }
    printk("gpioe_mode set success %d\n",__LINE__);
    // 模式寄存器:01输出模式
    *gpioe_mode &=~(0x11<<(2*10)); //清空初始化
    // 赋值0x11
    *gpioe_mode|=0x11<<(2*10);
    // 输出数据寄存器
    gpioe_10_odr = (unsigned int *)ioremap(GPIO_OUTDATA,sizeof(unsigned int));
    if(gpioe_10_odr==NULL){
        printk("gpioe_10_odr error\n");
        return -1;
    }
    printk("gpioe_10_odr set success%d\n",__LINE__);
    // 输出数据,默认给低电平
    *gpioe_10_odr &= ~(0x1<<10); // 关灯
    // *gpioe_10_odr |=(0x1 << 10);

    return 0;
}

int myclose(struct inode *node, struct file *file)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t myread(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_to_user(ubuf, kbuf, size);
    if (ret < 0)
    {
        printk("copy to user error\n");
        return -EIO;
    }
    return size;
}
ssize_t mywrite(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_from_user(kbuf, ubuf, size);
    if (ret < 0)
    {
        printk("copy from user error\n");
        return -EIO;
    }
    // 判断kbuf当中是否有数据来开关led
    if(kbuf[0]==1){
        *gpioe_10_odr |=(0x1<<10);
    }else{
        *gpioe_10_odr &= ~(0x1 << 10);
    }
    return size;
}

const struct file_operations fop = {
    .open = myopen,
    .release = myclose,
    .write = mywrite,
    .read = myread
};

// 创建设备目录指针
struct class *cls;

// 创建设备文件指针
struct device *dev;
static int __init fun_init(void)
{
    printk("this is %s-%d test\n", __func__, __LINE__);
    major = register_chrdev(0, CNAME, &fop);
    if (major < 0)
    {
        printk("字符设备注册失败\n");
        return -1;
    }
    
        printk("字符设备注册成功,major:%d\n", major);
        cls = class_create(THIS_MODULE,CNAME);
    if(IS_ERR(cls)){
        printk("class create error\n");
        return PTR_ERR(cls);
    }
    // 创建设备文件
    dev= device_create(cls,NULL,MKDEV(major,0),NULL,CNAME);
    if(IS_ERR(dev)){
        // 销毁目录
        class_destroy(cls);
        // 注销
        unregister_chrdev(major,CNAME);
        printk("class create error\n");
        return PTR_ERR(dev);
    }
    printk("设备目录和设备文件创建成功\n");
    return 0;
}
static void __exit fun_exit(void)
{
    unregister_chrdev(major, CNAME);
    class_destroy(cls);
    printk("this is %s %d exit\n", __func__, __LINE__);
}
module_init(fun_init);
module_exit(fun_exit);
// 证书信息
MODULE_LICENSE("GPL");

ioctl驱动程序操作寄存器

  1. 依赖头文件,驱动程序是<linux/ioctl.h>,应用程序是<sys/ioctl.h>
  2. 最好写一个头文件,使用带参数的宏定义调用_IO('a',1);命令码的_IO方法生成设备控制指令标识符
  3. 设备文件指针major = register_chrdev(0, CNAME, &fop);的第三个参数的结构体,增加.unlocked_ioctl = myioclt
  4. 定义myiclt函数,参数一文件指针,参数二应用程序传过来的参数,函数内通过不同的参数,给寄存器设置不同的数据,函数执行完成后会通过设备文件自动将数据写入到寄存器
  5. 应用程序通过ioctl(fd, LED1_ON);调用业务程序,实现业务代码

h文件

#ifndef __IOCTL__
#define __IOCTL__
// 自定义命令码写好,16位命令类型,8位命令序号,8位参数类型
#define LED1_ON _IO('a', 1)
#define LED1_OFF _IO('a', 0)
#endif

c文件

#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/ioctl.h>
#include "10_ioctl.h"
// 内核缓冲区
char kbuf[128] = "";
#define RCC_BASE 0x50000000                  // AHB4总线 GPIOE
#define AHB4_CLOCK_ENABLE (RCC_BASE + 0xA28) // AHB4总线地址
// PE10 - LED1
#define GPIOE_BASE 0x50006000
#define GPIO_MODER (GPIOE_BASE + 0x00)   // 模式寄存器
#define GPIO_OPTYE (GPIOE_BASE + 0x04)   // 输出类型寄存器
#define GPIO_OUTDATA (GPIOE_BASE + 0x14) // 输出数据寄存器

unsigned int *ahb4_clock_enable = NULL;
unsigned int *gpioe_mode = NULL;
unsigned int *gpioe_10_odr = NULL;

int major;             // 主设备号
#define CNAME "leddev" // 字符设备驱动名称

int myopen(struct inode *node, struct file *file)
{
    printk("this is my %s test \n", __func__);
    // 在open函数中完成映射
    // GPIOE时钟 ok
    ahb4_clock_enable = (unsigned int *)ioremap(AHB4_CLOCK_ENABLE, sizeof(unsigned int));
    if (ahb4_clock_enable == NULL)
    {
        printk("ioremap clock error");
        return -1;
    }
    // 驱动第四个位置的GPIOE
    *ahb4_clock_enable |= (1 << 4);
    printk("rcc_ahb4_clockenable success %d\n", __LINE__);

    // 在write函数中 完成控制
    gpioe_mode = (unsigned int *)ioremap(GPIO_MODER, sizeof(unsigned int));
    if (gpioe_mode == NULL)
    {
        printk("gpioe_mode error\n");
        return -1;
    }
    printk("gpioe_mode set success %d\n", __LINE__);
    // 模式寄存器:01输出模式
    *gpioe_mode &= ~(0x03 << (2 * 10)); // 清空初始化
    // 赋值0x11
    *gpioe_mode |= 0x11 << (2 * 10);
    // 输出数据寄存器
    gpioe_10_odr = (unsigned int *)ioremap(GPIO_OUTDATA, sizeof(unsigned int));
    if (gpioe_10_odr == NULL)
    {
        printk("gpioe_10_odr error\n");
        return -1;
    }
    printk("gpioe_10_odr set success%d\n", __LINE__);
    // 输出数据,默认给低电平
    *gpioe_10_odr &= ~(0x1 << 10); // 关灯
    // *gpioe_10_odr |=(0x1 << 10);

    return 0;
}

int myclose(struct inode *node, struct file *file)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t myread(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_to_user(ubuf, kbuf, size);
    if (ret < 0)
    {
        printk("copy to user error\n");
        return -EIO;
    }
    return size;
}
ssize_t mywrite(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_from_user(kbuf, ubuf, size);
    if (ret < 0)
    {
        printk("copy from user error\n");
        return -EIO;
    }
    // 判断kbuf当中是否有数据来开关led
    if (kbuf[0] == 1)
    {
        *gpioe_10_odr |= (0x1 << 10);
    }
    else
    {
        *gpioe_10_odr &= ~(0x1 << 10);
    }
    return size;
}
long myioclt(struct file *file, unsigned int cmd, unsigned long args)
{
    switch (cmd)
    {
    case LED1_ON:
        *gpioe_10_odr |= (0x1 << 10);
        break;
    case LED1_OFF:
        *gpioe_10_odr &= ~(0x1 << 10);
        break;

    default:
        break;
    }
    return 0;
};

const struct file_operations fop = {
    .open = myopen,
    .release = myclose,
    .write = mywrite,
    .read = myread,
    .unlocked_ioctl = myioclt
    };

// 创建设备目录指针
struct class *cls;

// 创建设备文件指针
struct device *dev;
static int __init fun_init(void)
{
    printk("this is %s-%d test\n", __func__, __LINE__);
    major = register_chrdev(0, CNAME, &fop);
    if (major < 0)
    {
        printk("字符设备注册失败\n");
        return -1;
    }

    printk("字符设备注册成功,major:%d\n", major);
    cls = class_create(THIS_MODULE, CNAME);
    if (IS_ERR(cls))
    {
        printk("class create error\n");
        return PTR_ERR(cls);
    }
    // 创建设备文件
    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
    if (IS_ERR(dev))
    {
        // 销毁目录
        class_destroy(cls);
        // 注销
        unregister_chrdev(major, CNAME);
        printk("class create error\n");
        return PTR_ERR(dev);
    }
    printk("设备目录和设备文件创建成功\n");
    return 0;
}
static void __exit fun_exit(void)
{
    unregister_chrdev(major, CNAME);
    class_destroy(cls);
    printk("this is %s %d exit\n", __func__, __LINE__);
}
module_init(fun_init);
module_exit(fun_exit);
// 证书信息
MODULE_LICENSE("GPL");

应用程序

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include "10_ioctl.h"
#define CNAME "/dev/leddev"
int main()
{
    int fd = open(CNAME, O_RDWR);
    if (fd < 0)
    {
        perror("open error\n");
        return -1;
    }
    printf("open success");
    while (1)
    {
        ioctl(fd, LED1_ON);
        sleep(1);
        ioctl(fd, LED1_OFF);
        sleep(1);
    }
    close(fd);
    return 0;
}

内核中产生竞态

  1. 多个应用程序同时访问驱动程序的临界资源的时候,竞态就会产生。
  2. 竞态是由多个执行流(进程、线程或中断)对共享资源访问顺序的不确定,导致结果不可预测,例如多个线程同时访问和修改共享资源,就会造成数据错误等场景
  3. 通过中断屏蔽、自旋锁、信号量、互斥体、原子操作等方式
  4. 个人理解,多个线程同时修改一个变量,没办法保证执行顺序,用一些方法控制执行顺序

自旋锁

  1. 在驱动文件定义自旋锁变量 spinlock_t lock = {0}
  2. 在入口函数初始化自旋锁 spin_lock_init(&lock);
  3. 在打开文件的驱动函数中上锁 spin_lock(&lock);
  4. 在设备关闭函数中解锁 spin_unlock(&lock); 驱动程序
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/io.h>
// 内核缓冲区
char kbuf[128] = "";
#define RCC_BASE 0x50000000                  // AHB4总线 GPIOE
#define AHB4_CLOCK_ENABLE (RCC_BASE + 0xA28) // AHB4总线地址
// PE10 - LED1
#define GPIOE_BASE 0x50006000
#define GPIO_MODER (GPIOE_BASE + 0x00)   // 模式寄存器
#define GPIO_OPTYE (GPIOE_BASE + 0x04)   // 输出类型寄存器
#define GPIO_OUTDATA (GPIOE_BASE + 0x14) // 输出数据寄存器

unsigned int *ahb4_clock_enable = NULL;
unsigned int *gpioe_mode = NULL;
unsigned int *gpioe_10_odr = NULL;

int major;             // 主设备号
#define CNAME "leddev" // 字符设备驱动名称

spinlock_t lock = {0};

int myopen(struct inode * node, struct file *file)
{
    printk("this is my %s test \n", __func__);
    // 初始化自旋锁
    spin_lock_init(&lock);
    spin_lock(&lock);
    printk("spin lock success\n");
    return 0;
}

int myclose(struct inode *node, struct file *file)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t myread(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_to_user(ubuf, kbuf, size);
    if (ret < 0)
    {
        printk("copy to user error\n");
        return -EIO;
    }
    spin_unlock(&lock);
    printk("spin unlock success\n");
    return size;
}
ssize_t mywrite(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_from_user(kbuf, ubuf, size);
    if (ret < 0)
    {
        printk("copy from user error\n");
        return -EIO;
    }
    return size;
}


const struct file_operations fop = {
    .open = myopen,
    .release = myclose,
    .write = mywrite,
    .read = myread,
};

// 创建设备目录指针
struct class *cls;

// 创建设备文件指针
struct device *dev;
static int __init fun_init(void)
{
    printk("this is %s-%d test\n", __func__, __LINE__);
    major = register_chrdev(0, CNAME, &fop);
    if (major < 0)
    {
        printk("字符设备注册失败\n");
        return -1;
    }

    printk("字符设备注册成功,major:%d\n", major);
    cls = class_create(THIS_MODULE, CNAME);
    if (IS_ERR(cls))
    {
        printk("class create error\n");
        return PTR_ERR(cls);
    }
    // 创建设备文件
    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
    if (IS_ERR(dev))
    {
        // 销毁目录
        class_destroy(cls);
        // 注销
        unregister_chrdev(major, CNAME);
        printk("class create error\n");
        return PTR_ERR(dev);
    }
    printk("设备目录和设备文件创建成功\n");
    
    return 0;
}
static void __exit fun_exit(void)
{
    unregister_chrdev(major, CNAME);
    class_destroy(cls);
    printk("this is %s %d exit\n", __func__, __LINE__);
}
module_init(fun_init);
module_exit(fun_exit);
// 证书信息
MODULE_LICENSE("GPL");

应用程序

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/fcntl.h>
#define CNAME "/dev/leddev"

int main()
{
    int fd = open(CNAME, O_RDWR);
    if (fd < 0)
    {
        perror("open error\n");
        return -1;
    }
    printf("open success");
    char *str = "demo";
    write(fd, str, strlen(str));
    sleep(100);
    char buf[128] = "";
    read(fd, buf, sizeof(buf));
    close(fd);
    return 0;
}

信号量

  1. 在驱动文件定义信号量 struct semaphore sem;
  2. 在入口函数初始化信号量 void sema_init(struct semaphore *sem, int val) //初始化信号量
  3. 在写函数上锁 void down(struct semaphore *sem); //上锁 阻塞方式 p操作
  4. 在读函数解锁 void up(struct semaphore *sem);

驱动程序

#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/io.h>
// 内核缓冲区
char kbuf[128] = "";
int major;             // 主设备号
#define CNAME "leddev" // 字符设备驱动名称
//定义一个信号量
struct semaphore sem;

int myopen(struct inode * node, struct file *file)
{
    printk("this is my %s test \n", __func__);
    return 0;
}

int myclose(struct inode *node, struct file *file)
{
    printk("this is my%s\n", __func__);
    return 0;
}
ssize_t myread(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_to_user(ubuf, kbuf, size);
    if (ret < 0)
    {
        printk("copy to user error\n");
        return -EIO;
    }
    //v操作
    printk("解锁\n");
	up(&sem);
    return size;
}
ssize_t mywrite(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is my%s\n", __func__);
    if (size > sizeof(kbuf))
    {
        size = sizeof(kbuf);
    }
    ret = copy_from_user(kbuf, ubuf, size);
    if (ret < 0)
    {
        printk("copy from user error\n");
        return -EIO;
    }
    //上锁
	down(&sem);
    printk("上锁\n");
    return size;
}


const struct file_operations fop = {
    .open = myopen,
    .release = myclose,
    .write = mywrite,
    .read = myread,
};

// 创建设备目录指针
struct class *cls;

// 创建设备文件指针
struct device *dev;
static int __init fun_init(void)
{
    printk("this is %s-%d test\n", __func__, __LINE__);
    major = register_chrdev(0, CNAME, &fop);
    if (major < 0)
    {
        printk("字符设备注册失败\n");
        return -1;
    }

    printk("字符设备注册成功,major:%d\n", major);
    cls = class_create(THIS_MODULE, CNAME);
    if (IS_ERR(cls))
    {
        printk("class create error\n");
        return PTR_ERR(cls);
    }
    // 创建设备文件
    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
    if (IS_ERR(dev))
    {
        // 销毁目录
        class_destroy(cls);
        // 注销
        unregister_chrdev(major, CNAME);
        printk("class create error\n");
        return PTR_ERR(dev);
    }
    printk("设备目录和设备文件创建成功\n");
    //初始化信号量
	sema_init(&sem,1);
    printk("初始化信号量成功\n");
    return 0;
}
static void __exit fun_exit(void)
{
    unregister_chrdev(major, CNAME);
    //销毁设备
	device_destroy(cls,MKDEV(major,0));
    class_destroy(cls);
    printk("this is %s %d exit\n", __func__, __LINE__);
}
module_init(fun_init);
module_exit(fun_exit);
// 证书信息
MODULE_LICENSE("GPL");

应用程序

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/fcntl.h>
#define CNAME "/dev/leddev"

int main()
{
    int fd = open(CNAME, O_RDWR);
    if (fd < 0)
    {
        perror("open error\n");
        return -1;
    }
    printf("open success\n");
    char *str = "demo";
    write(fd, str, strlen(str));
    printf("p操作成功\n");
    printf("正在使用临界资源\n");
    sleep(20);
    char buf[128] = "";
    read(fd, buf, sizeof(buf));
    printf("v操作成功\n");
    close(fd);
    return 0;
}

I/O模型

  1. IO模型决定了用户程序如何与内核交互获取数据的方式,特别是当数据没准备好不可用的状态下

阻塞I/O

  1. 阻塞I/O就是当数据未准备好时,进程会被挂起,也就是阻塞,一直等待数据准备好才返回
  2. 例如用read读数据的时候,如果没有数据,当前进程就会被挂起,内核不会返回,会一直等待,直到有数据可读
  3. 阻塞的时候cpu可以去调度其他任务
  4. 对应的方法有wait_event_interruptible(wq,condition);
  5. 一般用在网络通信、串口接收、中断过来的数据等

非阻塞I/O

  1. 非阻塞I/O是当数据未准备好时,不等待,立即返回一个错误或状态码。
  2. read调用立即返回,即使没有数据,用户程序需要自己判断是否成功读取到了数据,一般需要轮询加延时处理

I/O多路复用

  1. 使用一个线程,同时监听多个I/O通道,一旦其中某一个I/O就绪,就立刻处理,不必为每个I/O建立一个线程
  2. 一般用在网络服务器、多设备数据收集、实时系统监测多个输入源等场景

设备树

  1. Linux设备树(Device Tree)是一种描述硬件配置的数据结构,用于将硬件信息传递给操作系统内核
  2. 一般设备树用来配置io口、adc、spi等外设
  3. 依赖头文件#include <linux/of.h>
  4. 设备树文件目录 linux-5.4.31/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
  5. 增加节点在/{}内添加myleds{led1 = <&gpioe 10 0>;};
  6. 编辑完以后需要在linux-5.4.31文件夹目录下执行编译命令make dtbs
  7. 编译完成后,将arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb文件拷到tftpboot共享文件夹
  8. 启动开发板,在/proc/device-tree/目录内ls,能看到自己增加的节点
  9. 驱动文件读取设备树文件的节点node = of_find_node_by_path("/lanbinNode@0x123456");
  10. 驱动文件读取属性of_property_read_string(node, "astring", &out_string);
  11. 不同类型属性有不同的读取方法of_property_read_u32_array、of_property_read_u8_array等

驱动文件

#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/export.h>
// cp 拷贝相关函数
#include <linux/uaccess.h>
#include <linux/errno.h>
// 延时函数
#include <linux/delay.h>
#include <linux/of.h>
int major;
// 字符设备名称 + 驱动设备名称
#define CNAME "mycdev"
// 设备树节点指针
struct device_node *node;
// 属性指针
struct property *pp;

// 入口文件
static int __init fun_init(void)
{
    int i = 0;
    // 返回指针
    const char *out_string;
    // 这里不能做指针 要做一个数组
    u32 out_values[2];
    // 二进制数据有4组
    uint8_t bindata[4];
    // 1 通过路径获取节点
    // 在双引号中填写 节点全称
    // 有个"/""
    node = of_find_node_by_path("/lanbinNode@0x123456");
    if (node == NULL)
    {
        printk("of find node by path error\n");
        return -EINVAL;
    }
    // 只能在开发板测试,因为上位机的设备数里面没有这个代码
    printk("of find node by path success\n");
    // 2 通过节点结构体  获取键值对
    of_property_read_string(node, "astring", &out_string);
    printk("out string : %s\n", out_string);
    // 3 解析数组
    // 设备节点的地址
    // 属性名称
    // 输出数据首地址
    // 成员个数
    of_property_read_u32_array(node, "uint", out_values, 2);
    for (i = 0; i < 2; i++)
    {
        printk("uint:%#x\n", out_values[i]);
    }

    // 4 读数组 二进制数据  4个
    // 设备节点指针
    // 属性名称
    // 输出数据首地址
    // 成员个数
    of_property_read_u8_array(node, "binddata", bindata, 4);
    // 把二进制数据转换成大断?
    for (i = 0; i < 4; i++)
    {
        // 格式化输出二进制数据 用16进制数据代替二进制数据输出
        // 0 填充不足的位数
        // 2 最小宽度为2个字符
        // x小写16进制数据
        printk("bindata:%02x\n", bindata[i]);
    }
    return 0;
}
// 出口文件
static void __exit fun_exit(void)
{
    printk("exit");
}
module_init(fun_init);
module_exit(fun_exit);
MODULE_LICENSE("GPL");

设备树文件

/dts-v1/;
#include "stm32mp157.dtsi"
#include "stm32mp15xa.dtsi"
#include "stm32mp15-pinctrl.dtsi"
#include "stm32mp15xxaa-pinctrl.dtsi"
#include "stm32mp15xx-fsmp1x.dtsi"
/ {
    model = "HQYJ STM32MP157 FSMP1A Discovery Board";
    compatible = "st,stm32mp157a-chb", "hqyj,stm32mp157";
    
    aliases {
        serial0 = &uart4;
    };

    chosen {
        stdout-path = "serial0:115200n8";
    };

    reserved-memory {
        gpu_reserved: gpu@da000000 {
            reg = <0xda000000 0x4000000>;
            no-map;
        };

        optee_memory: optee@0xde000000 {
            reg = <0xde000000 0x02000000>;
            no-map;
        };
    };
    
    lanbinNode@0x123456{
        astring = "hello cs2502ban";    
        uint = <0x1234 0x5678>;        
        bindata = [01 02 34 66];       
        bindata2 = /bits/ 8 <0x01 0x02 0x34 0x66>;
    }; 

    myleds{
        compatible = "linuxKaiFaBan,myleds";
        led1 = <&gpioe 10 0>;
        led2 = <&gpiof 10 0>;
        led3 = <&gpioe 8 0>;
    };

};

&optee {
    status = "okay";
};

GPIO子系统

  1. linux内核的gpio子系统是一套用于管理和控制io口的软件框架。
  2. 依赖头文件<linux/of.h>和<linux/of_gpio.h>
  3. 通过在设备树上根据芯片数据手册,定义io口,例如led1=<&gpioe 10 0> //pe10 初始值为0
  4. 驱动程序中gpio_no = of_get_named_gpio(node,"led1",0);获取io口软件编号
  5. 驱动程序中gpio_request(gpio_no,NULL)申请使用编号
  6. 驱动程序中设置io口模式为输出gpio_direction_output(gpio_no,0);
  7. 设置io口值gpio_set_value(gpio_no,1);
  8. 释放io口gpio_free(gpio_no);
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/export.h>
// 文件操作结构体的头文件不能少
#include <linux/fs.h>
// cp 拷贝相关函数
#include <linux/uaccess.h>
#include <linux/errno.h>
// 自动创建节点
#include <linux/device.h>
// 映射头文件
#include <linux/io.h>
// 延时函数
#include <linux/delay.h>
#include <linux/of.h>
#include <linux/of_gpio.h>

int major;
// 字符设备名称 + 驱动设备名称
#define CNAME "mycdev"
// 设备树节点指针
struct device_node *node;
// 属性指针
struct property *pp;

// 目录文件夹结构体类型
struct class *cls = NULL;

// 设备节点结构体类型
struct device *dev = NULL;

int gpio_no, gpio_no2, gpio_no3;
// 打开
// int (*open) (struct inode *, struct file *);
int mycdev_open(struct inode *node, struct file *file)
{
    printk("this is %s test\n", __func__);
    return 0;
}
// 关闭
// int (*release) (struct inode *, struct file *);
int mycdev_close(struct inode *node, struct file *file)
{
    printk("this is %s test\n", __func__);
    return 0;
}

// 内核空间缓冲区
char kbuf[128] = "";
// 读
// 文件用户缓冲区地址 大小 偏移量
// 应用层的read函数调用到此函数
// ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t mycdev_read(struct file *file, char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is %s test\n", __func__);
    if (size > sizeof(kbuf))
        size = sizeof(kbuf);
    // 用户空间  内核空间  大小
    ret = copy_to_user(ubuf, kbuf, size);
    if (ret < 0)
    {
        printk("copy to user error\n");
        return -EIO;
    }

    // 返回实际写入的大小
    return size;
}
// 写
// ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
// 应用层的wirite函数 调用到此函数
ssize_t mycdev_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offs)
{
    int ret;
    printk("this is %s test\n", __func__);
    // 如果空间大 则用最大空间
    if (size > sizeof(kbuf))
        size = sizeof(kbuf);
    // 把用户空间数据写进内核空间
    ret = copy_from_user(kbuf, ubuf, size);
    if (ret < 0)
    {
        printk("copy form user error\n");
        return -EIO;
    }
    if (kbuf[0] == 0)
    {
        gpio_set_value(gpio_no, 0);
        gpio_set_value(gpio_no2, 0);
        gpio_set_value(gpio_no3, 0);
    }
    if (kbuf[0] == 1)
    {
        gpio_set_value(gpio_no, 1);
        gpio_set_value(gpio_no2, 1);
        gpio_set_value(gpio_no3, 1);
    }
    // 返回实际写入的大小
    return size;
}
// 文件操作结构体
const struct file_operations fops = {
    .open = mycdev_open,
    .release = mycdev_close,
    .read = mycdev_read,
    .write = mycdev_write};
// 入口文件
static int __init fun_init(void)
{
    // 返回指针
    const char *out_string;
    // 在此处注册
    // 系统分配 字符设备名字 结构体引用
    major = register_chrdev(0, CNAME, &fops);
    if (major < 0)
    {
        printk("register char dev error\n");
        return -1;
    }
    printk("设备注册成功 %d\n", major);

    // 提交目录
    cls = class_create(THIS_MODULE, CNAME);
    if (IS_ERR(cls))
    {
        printk("class create error\n");
        return PTR_ERR(cls);
    }
    printk("classcreate ok\n");
    // 创建设备文件
    // 目录句柄 此设备的父设备 设备号 传递的数据 节点名称
    // 主设备号与次设备号组成一个设备号
    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
    if (IS_ERR(dev))
    {
        // 删除目录
        class_destroy(cls);
        // 注销
        unregister_chrdev(major, CNAME);
        // 提示信息
        printk("device create error\n");
        return PTR_ERR(dev);
    }
    printk("device create ok\n");
    // 1 通过路径获取节点
    // 在双引号中填写 节点全称
    // 有个"/""
    node = of_find_node_by_path("/myleds");
    if (node == NULL)
    {
        printk("of find node by path error\n");
        return -EINVAL;
    }
    // 只能在开发板测试,因为上位机的设备数里面没有这个代码
    printk("of find node by path success\n");
    // 2 通过节点结构体  获取键值对
    of_property_read_string(node, "compatible", &out_string);
    printk("out string : %s\n", out_string);
    gpio_no = of_get_named_gpio(node, "led1", 0);
    if (gpio_no < 0)
    {
        printk("of_named_gpio err\n");
    }
    printk("of_named_gpio %d \n", gpio_no);
    if (gpio_request(gpio_no, NULL) < 0)
    {
        printk("gpio_request err\n");
    }
    gpio_no2 = of_get_named_gpio(node, "led2", 0);
    if (gpio_no2 < 0)
    {
        printk("of_named_gpio2 err\n");
    }
    printk("of_named_gpio2 %d \n", gpio_no2);
    if (gpio_request(gpio_no2, NULL) < 0)
    {
        printk("gpio_request2 err\n");
    }
    printk("gpio_request2 succ\n");
    gpio_no3 = of_get_named_gpio(node, "led3", 0);
    if (gpio_no3 < 0)
    {
        printk("of_named_gpio3 err\n");
    }
    printk("of_named_gpio3 %d \n", gpio_no3);
    if (gpio_request(gpio_no3, NULL) <0)
    {
        printk("gpio_request3 err\n");
    }
    printk("gpio_request3 succ\n");
    gpio_direction_output(gpio_no, 0); // 初始化gpio
    gpio_direction_output(gpio_no2, 0); // 初始化gpio
    gpio_direction_output(gpio_no3, 0); // 初始化gpio

    return 0;
}
// 出口文件
static void __exit fun_exit(void)
{
    printk("this is mycdev exit function\n");
    // 在此处注销
    // 主设备号 名字
    unregister_chrdev(major, CNAME);
    // 销毁设备 目录  设备号
    device_destroy(cls, MKDEV(major, 0));
    // 目录解除
    class_destroy(cls);

    gpio_set_value(gpio_no, 0);
    gpio_free(gpio_no);
    gpio_set_value(gpio_no2, 0);
    gpio_free(gpio_no2);
    gpio_set_value(gpio_no3, 0);
    gpio_free(gpio_no3);

    printk("exit");
}
module_init(fun_init);
module_exit(fun_exit);
MODULE_LICENSE("GPL");