树莓派4Linux 可操作多个gpio口驱动编写

要想正常进行gpio的使用,首先我们需要查看gpio口的占用情况,我们可以使用cat /sys/kernel/debug/gpio,查看所有的bcm引脚的占用状态,然后我们就可以找到bcm对应的实际gpio的引脚是否被占用了,下图是树莓派4的引脚对照表

当使用这段指令后我们会看到下面的场景

c 复制代码
sudo cat /sys/kernel/debug/gpio

这里的 ) 就表示对应的gpio口没有被占用,查看到了gpio口没有被占用的消息我们就可以编写代码了

完整代码

c 复制代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/errno.h>

static int gpio_pins[10]; //GPIO列表(默认无效值)
static int gpio_count = ARRAY_SIZE(gpio_pins); //gpio数量

module_param_array(gpio_pins,int,&gpio_count,0644);
MODULE_PARM_DESC(gpio_pins,"GPIO列表,格式:gpio_pins=516,517(至少指定1个)");

typedef struct {
	dev_t dev_num;
	struct cdev gpio_cdev;
	struct class *gpio_class;
	struct device *gpio_device;
	int *gpio_list;
	int *gpio_status;
}gpio_led_dev_t;

static gpio_led_dev_t *gpio_dev;
static const char *gpio_dev_name = "gpio_led_simple";
#define DEV_CLASS "gpio_led_simple_class"

// 补全open函数,赋值private_data
static int gpio_led_open(struct inode *inode, struct file *filp) {
    gpio_led_dev_t *dev = container_of(inode->i_cdev, gpio_led_dev_t, gpio_cdev);
    filp->private_data = dev; // 关键:给private_data赋值
    return 0;
}

static ssize_t gpio_led_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos){
	gpio_led_dev_t *dev=filp->private_data;
	char cmd[64]={0};
	int gpio_num,value,i;

	if(count >= sizeof(cmd) || copy_from_user(cmd,buf,count)){
		return -EINVAL;
	}

	if(sscanf(cmd,"set:%d:%d",&gpio_num,&value)==2){
		for(i=0;i<gpio_count;i++){
			if(dev->gpio_list[i]==gpio_num){
				if(value!=0&&value!=1) return -EINVAL;
				gpio_set_value(gpio_num,value);
				dev->gpio_status[i]=value;
				printk(KERN_INFO "GPIO%d 设为 %d\n", gpio_num, value);
				return count;
			}
		}
		return -EINVAL;
	}
	if(sscanf(cmd,"batch:%d",&value)==1){
		if(value!=0&&value!=1) return -EINVAL;
		for(i=0;i<gpio_count;i++){
			gpio_set_value(dev->gpio_list[i],value);
			dev->gpio_status[i]=value;
		}
		printk(KERN_INFO "所有GPIO批量设为 %d\n", value);
		return count;
	}
    printk(KERN_ERR "无效指令!支持:\n");
    printk(KERN_ERR "  set:<gpio号>:<0/1>  例:set:516:1\n");
    printk(KERN_ERR "  batch:<0/1>        例:batch:0\n");
    return -EINVAL;
		
}

static ssize_t gpio_led_read(struct file *filp,char __user *buf,size_t count,loff_t *ppos){
		gpio_led_dev_t *dev=filp->private_data;
		char status[256]={0};
		int len=0;

		for(int i=0;i<gpio_count;i++){
			   len += snprintf(status + len, sizeof(status) - len,
                     "GPIO%d:%d%s", dev->gpio_list[i], dev->gpio_status[i],
                     (i == gpio_count-1) ? "" : ",");

		}
		len += snprintf(status + len, sizeof(status) - len, "\n");
		if(copy_to_user(buf,status,len)) return -EFAULT;
		return len;
}

static const struct file_operations gpio_led_fops = {
    .owner = THIS_MODULE,
    .write = gpio_led_write,
    .read = gpio_led_read,
    .open = gpio_led_open,
};


static int gpio_char_dev_init(gpio_led_dev_t *dev){
	int ret;

	ret=alloc_chrdev_region(&dev->dev_num,0,1,gpio_dev_name);
	if(ret<0) return ret;

	cdev_init(&dev->gpio_cdev,&gpio_led_fops);
	dev->gpio_cdev.owner=THIS_MODULE;
	ret=cdev_add(&dev->gpio_cdev,dev->dev_num,1);
	if(ret<0){
		unregister_chrdev_region(dev->dev_num,1);
		return ret;
	}

	dev->gpio_class=class_create(DEV_CLASS);
	if(IS_ERR(dev->gpio_class)){
		cdev_del(&dev->gpio_cdev);
		unregister_chrdev_region(dev->dev_num,1);
		return PTR_ERR(dev->gpio_class);
	}

	dev->gpio_device=device_create(dev->gpio_class, NULL, dev->dev_num, NULL, gpio_dev_name);
	if (IS_ERR(dev->gpio_device)) {
        class_destroy(dev->gpio_class);
        cdev_del(&dev->gpio_cdev);
        unregister_chrdev_region(dev->dev_num, 1);
        return PTR_ERR(dev->gpio_device);
    }
	printk(KERN_INFO "设备节点创建成功:/dev/%s\n", dev_name);
    return 0;
}

static int __init gpio_led_init(void){
	int ret,i;
	if(gpio_count<=0||gpio_pins[0]== -1){
		printk(KERN_ERR "请指定GPIO,例:insmod xxx.ko gpio_pins=516,517\n");
        return -EINVAL;
	}

	gpio_dev=kzalloc(sizeof(gpio_led_dev_t),GFP_KERNEL);
	if(!gpio_dev) return -ENOMEM;

	gpio_dev->gpio_list = kmalloc_array(gpio_count,sizeof(int),GFP_KERNEL);
	gpio_dev->gpio_status=kmalloc_array(gpio_count,sizeof(int),GFP_KERNEL);
	if (!gpio_dev->gpio_list || !gpio_dev->gpio_status) {
        ret = -ENOMEM;
        goto err_mem;
    }
	for (i = 0; i < gpio_count; i++) {
        gpio_dev->gpio_list[i] = gpio_pins[i];
        ret = gpio_request(gpio_dev->gpio_list[i], gpio_dev_name);
        if (ret) {
            printk(KERN_ERR "申请GPIO%d失败:%d\n", gpio_dev->gpio_list[i], ret);
            goto err_gpio;
        }
        gpio_direction_output(gpio_dev->gpio_list[i], 0);
        gpio_dev->gpio_status[i] = 0;
        printk(KERN_INFO "GPIO%d 初始化完成(输出,初始电平0)\n", gpio_dev->gpio_list[i]);
    }

    // 初始化字符设备
    ret = gpio_char_dev_init(gpio_dev);
    if (ret) goto err_gpio;

    printk(KERN_INFO "简易通用GPIO驱动初始化成功,共控制 %d 个GPIO\n", gpio_count);
    return 0;

err_gpio:
    // 回滚:释放已申请的GPIO
    for (i = 0; i < gpio_count; i++) {
        if (gpio_dev->gpio_list[i] != -1) gpio_free(gpio_dev->gpio_list[i]);
    }
err_mem:
    // 释放内存
    kfree(gpio_dev->gpio_list);
    kfree(gpio_dev->gpio_status);
    kfree(gpio_dev);
    return ret;
}

// 6. 驱动出口(释放资源)
static void __exit gpio_led_exit(void) {
    // 销毁设备节点和类
    device_destroy(gpio_dev->gpio_class, gpio_dev->dev_num);
    class_destroy(gpio_dev->gpio_class);

    // 删除cdev并释放设备号
    cdev_del(&gpio_dev->gpio_cdev);
    unregister_chrdev_region(gpio_dev->dev_num, 1);

    // 释放GPIO资源
    for (int i = 0; i < gpio_count; i++) {
        gpio_free(gpio_dev->gpio_list[i]);
    }

    // 释放内存
    kfree(gpio_dev->gpio_list);
    kfree(gpio_dev->gpio_status);
    kfree(gpio_dev);

    printk(KERN_INFO "简易通用GPIO驱动已退出\n");
}

module_init(gpio_led_init);
module_exit(gpio_led_exit);

// 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Simple GPIO Driver");
MODULE_DESCRIPTION("Simple Universal Multi-GPIO Driver (Set/Read Level)");
MODULE_VERSION("1.0");

基本信息配置

static int gpio_pins[10]; //GPIO列表

static int gpio_count = ARRAY_SIZE(gpio_pins); //gpio数量

module_param_array(gpio_pins,int,&gpio_count,0644);

MODULE_PARM_DESC(gpio_pins,"GPIO列表,格式:gpio_pins=516,517(至少指定1个)");

逐行代码讲解
1.static int gpio_pins[10]:定义一个内核态静态整形数组,专门用来存储用户加载模块时传入的所有GPIO引脚编号
2.static int gpio_count = ARRAY_SIZE(gpio_pins):定义一个GPIO计数变量,初始化值是GPIO数组的总长度,这个变量是被内核接口修改的入参,非常关键!当模块加载传参后,内核会自动修改这个变量的值为我们实际传入的GPIO引脚的个数
3.module_param_array(数组名, 数组元素类型, 实际传入个数的指针, 文件权限掩码):这是内核提供的数组模块参数注册接口,是驱动能接收外部数组参数的唯一入口,如果没有这个宏,上面的数组和计数变量都是摆设
4.MODULE_PARM_DESC(gpio_pins,"GPIO列表,格式:gpio_pins=516,517(至少指定1个)"):这是给注册的模块参数添加说明文档,是辅助作用,但是必须写,是内核驱动的规范

结构体定义

c 复制代码
typedef struct {
	dev_t dev_num;//存储设备的主次设备号
	struct cdev gpio_cdev;//cdev是Linux字符设备的核心结构体,封装了设备的操作函数集和设备号,是字符设备注册到内核的关键
	struct class *gpio_class;//用于在sysfs中创建设备类,为自动生成设备节点 /dev 下提供基础
	struct device *gpio_device;//表示具体的设备实例,最终会触发udev/mdev创建 /dev 下的设备文件,让用户层能通过文件操作访问设备
	int *gpio_list;
	int *gpio_status;
}gpio_led_dev_t;
static gpio_led_dev_t *gpio_dev;//创建结构体变量
static const char *gpio_dev_name = "gpio_led_simple";
#define DEV_CLASS "gpio_led_simple_class"

这里重点讲一下结构体后面两个成员

*1.int gpio_list:存储驱动要操作的所有GPIO引脚编号的动态数组指针,对应前面定义的gpio_pins[10]数组

*2.int gpio_status:存储每个 GPIO 引脚「当前的状态值」的动态数组指针

open函数

c 复制代码
static int gpio_led_open(struct inode *inode, struct file *filp) {
    gpio_led_dev_t *dev = container_of(inode->i_cdev, gpio_led_dev_t, gpio_cdev);
    filp->private_data = dev; // 关键:给private_data赋值
    return 0;
}

这段代码则是有三个核心作用:
通过 inode->i_cdev 这个成员地址(gpio_led_dev_t里的gpio_cdev),反向计算出整个gpio_led_dev_t结构体变量的首地址,并把这个首地址赋值给结构体指针dev
2.把拿到的 整个 GPIO 设备结构体的首地址,赋值给 filp 的私有数据指针 private_data
3.状态反馈,返回0,向内核和用户层确认设备打开成功,完成设备打开的流程

write函数

c 复制代码
static ssize_t gpio_led_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos){
	gpio_led_dev_t *dev=filp->private_data;
	char cmd[64]={0};
	int gpio_num,value,i;

	if(count >= sizeof(cmd) || copy_from_user(cmd,buf,count)){
		return -EINVAL;
	}

	if(sscanf(cmd,"set:%d:%d",&gpio_num,&value)==2){
		for(i=0;i<gpio_count;i++){
			if(dev->gpio_list[i]==gpio_num){
				if(value!=0&&value!=1) return -EINVAL;
				gpio_set_value(gpio_num,value);
				dev->gpio_status[i]=value;
				printk(KERN_INFO "GPIO%d 设为 %d\n", gpio_num, value);
				return count;
			}
		}
		return -EINVAL;
	}
	if(sscanf(cmd,"batch:%d",&value)==1){
		if(value!=0&&value!=1) return -EINVAL;
		for(i=0;i<gpio_count;i++){
			gpio_set_value(dev->gpio_list[i],value);
			dev->gpio_status[i]=value;
		}
		printk(KERN_INFO "所有GPIO批量设为 %d\n", value);
		return count;
	}
    printk(KERN_ERR "无效指令!支持:\n");
    printk(KERN_ERR "  set:<gpio号>:<0/1>  例:set:516:1\n");
    printk(KERN_ERR "  batch:<0/1>        例:batch:0\n");
    return -EINVAL;
		
}

这部分代码可以分成三个部分来讲解

gpio_led_dev_t *dev=filp->private_data;

char cmd[64]={0};

int gpio_num,value,i;

1.表示通过open函数绑定的private_data,拿到相应的结构体信息

if(count >= sizeof(cmd) || copy_from_user(cmd,buf,count)){

return -EINVAL;

}

2.这部分则是限制用户输入长度:最多接收1个字符;并且检查用户传的buf是否为空,避免空指针错误并且将用户层数据传到内核层,用户层和内核层内存空间隔离,必须用copy_from_user(内核提供的安全函数),把用户层传来的指令复制到内核的buf中

if(sscanf(cmd,"set:%d:%d",&gpio_num,&value)==2){

for(i=0;i<gpio_count;i++){

if(dev->gpio_list[i]==gpio_num){

if(value!=0&&value!=1) return -EINVAL;

gpio_set_value(gpio_num,value);

dev->gpio_status[i]=value;

printk(KERN_INFO "GPIO%d 设为 %d\n", gpio_num, value);

return count;

}

}

return -EINVAL;

}

if(sscanf(cmd,"batch:%d",&value)==1){

if(value!=0&&value!=1) return -EINVAL;

for(i=0;i<gpio_count;i++){

gpio_set_value(dev->gpio_list[i],value);

dev->gpio_status[i]=value;

}

printk(KERN_INFO "所有GPIO批量设为 %d\n", value);

return count;

}

printk(KERN_ERR "无效指令!支持:\n");

printk(KERN_ERR " set:<gpio号>:<0/1> 例:set:516:1\n");

printk(KERN_ERR " batch:<0/1> 例:batch:0\n");

return -EINVAL;

}

2.这一部分则是检测用户层输入的信息,比如我们要操作516的电平输出的话输入 set:516,%d,比如要操作我们初始化的所有GPIO的输出电平的话就输入batch:%d,其他的指令都为无效指令

read函数

c 复制代码
static ssize_t gpio_led_read(struct file *filp,char __user *buf,size_t count,loff_t *ppos){
		gpio_led_dev_t *dev=filp->private_data;
		char status[256]={0};
		int len=0;

		for(int i=0;i<gpio_count;i++){
			   len += snprintf(status + len, sizeof(status) - len,
                     "GPIO%d:%d%s", dev->gpio_list[i], dev->gpio_status[i],
                     (i == gpio_count-1) ? "" : ",");

		}
		len += snprintf(status + len, sizeof(status) - len, "\n");
		if(copy_to_user(buf,status,len)) return -EFAULT;
		return len;
}

核心作用是:遍历驱动中所有已配置的 GPIO 引脚,拼接成 GPIOxxx:状态,GPIOxxx:状态 的格式化字符串(比如GPIO516:1,GPIO517:0),然后把这个状态字符串从内核空间拷贝到应用层空间,最终返回给应用层所有 GPIO 的实时电平状态

# 定义file_operations结构体

c 复制代码
static const struct file_operations gpio_led_fops = {
    .owner = THIS_MODULE,
    .write = gpio_led_write,
    .read = gpio_led_read,
    .open = gpio_led_open,
};

为什么要定义这个结构体:因为内核只认识自己的规则,不认识我们自定义的函数名,所以这个结构就是使得内核知道当用户调用open/release这些系统调用时,找到我们自己编写的函数,当我们在命令行写write时,内核会帮我们调用自己编写的write函数
1.static const修饰符的作用:限定这个结构体只在当前驱动.c文件中生效,不会被其他文件访问,避免多个驱动的file_operations结构体命名冲突
2...ower = THIS_MODULE:驱动必须写,固定不变,因为它的作用是声明这个file_operations结构体归属于当前的内核模块,内核通过这个标记,能知道这个驱动的归属,使得防止驱动模块在被使用是,被用户误卸载并且内核做资源管理,模块计数时,能精准识别驱动归属
3...open = gpio_led_open:用户层系统调用:调用我这里的gpio_led_open函数
4...write = gpio_led_write:调用我的gpio_led_write
5...release = gpio_led_read:调用gpio_led_read;

init函数

c 复制代码
static int gpio_char_dev_init(gpio_led_dev_t *dev){
	int ret;

	ret=alloc_chrdev_region(&dev->dev_num,0,1,gpio_dev_name);
	if(ret<0) return ret;

	cdev_init(&dev->gpio_cdev,&gpio_led_fops);
	dev->gpio_cdev.owner=THIS_MODULE;
	ret=cdev_add(&dev->gpio_cdev,dev->dev_num,1);
	if(ret<0){
		unregister_chrdev_region(dev->dev_num,1);
		return ret;
	}

	dev->gpio_class=class_create(DEV_CLASS);
	if(IS_ERR(dev->gpio_class)){
		cdev_del(&dev->gpio_cdev);
		unregister_chrdev_region(dev->dev_num,1);
		return PTR_ERR(dev->gpio_class);
	}

	dev->gpio_device=device_create(dev->gpio_class, NULL, dev->dev_num, NULL, gpio_dev_name);
	if (IS_ERR(dev->gpio_device)) {
        class_destroy(dev->gpio_class);
        cdev_del(&dev->gpio_cdev);
        unregister_chrdev_region(dev->dev_num, 1);
        return PTR_ERR(dev->gpio_device);
    }
	printk(KERN_INFO "设备节点创建成功:/dev/%s\n", dev_name);
    return 0;
}

static int __init gpio_led_init(void){
	int ret,i;
	if(gpio_count<=0||gpio_pins[0]== -1){
		printk(KERN_ERR "请指定GPIO,例:insmod xxx.ko gpio_pins=516,517\n");
        return -EINVAL;
	}

	gpio_dev=kzalloc(sizeof(gpio_led_dev_t),GFP_KERNEL);
	if(!gpio_dev) return -ENOMEM;

	gpio_dev->gpio_list = kmalloc_array(gpio_count,sizeof(int),GFP_KERNEL);
	gpio_dev->gpio_status=kmalloc_array(gpio_count,sizeof(int),GFP_KERNEL);
	if (!gpio_dev->gpio_list || !gpio_dev->gpio_status) {
        ret = -ENOMEM;
        goto err_mem;
    }
	for (i = 0; i < gpio_count; i++) {
        gpio_dev->gpio_list[i] = gpio_pins[i];
        ret = gpio_request(gpio_dev->gpio_list[i], gpio_dev_name);
        if (ret) {
            printk(KERN_ERR "申请GPIO%d失败:%d\n", gpio_dev->gpio_list[i], ret);
            goto err_gpio;
        }
        gpio_direction_output(gpio_dev->gpio_list[i], 0);
        gpio_dev->gpio_status[i] = 0;
        printk(KERN_INFO "GPIO%d 初始化完成(输出,初始电平0)\n", gpio_dev->gpio_list[i]);
    }

    // 初始化字符设备
    ret = gpio_char_dev_init(gpio_dev);
    if (ret) goto err_gpio;

    printk(KERN_INFO "简易通用GPIO驱动初始化成功,共控制 %d 个GPIO\n", gpio_count);
    return 0;

err_gpio:
    // 回滚:释放已申请的GPIO
    for (i = 0; i < gpio_count; i++) {
        if (gpio_dev->gpio_list[i] != -1) gpio_free(gpio_dev->gpio_list[i]);
    }
err_mem:
    // 释放内存
    kfree(gpio_dev->gpio_list);
    kfree(gpio_dev->gpio_status);
    kfree(gpio_dev);
    return ret;
}

我把初始化拆分为2个函数,分层完成任务

*1.gpio_char_dev_init(gpio_led_dev_t dev):纯内核层逻辑,只做一件事:完成 Linux 字符设备驱动的标准四步注册流程 → 申请设备号 → 初始化/注册cdev → 创建class类 → 创建device设备节点,最终在/dev目录生成硬件访问节点
2.驱动的真正入口,内核加载模块时insmod/modprobe会自动执行这个函数,是业务层 + 硬件层的总初始化,职责是GPIO 合法性校验 → 申请驱动结构体内存 → 申请 GPIO 引脚列表 / 状态数组内存 → 循环申请 GPIO 硬件资源 + 初始化 GPIO 为输出 → 调用字符设备注册函数 → 完整的错误回滚释放资源

exit函数

c 复制代码
static void __exit gpio_led_exit(void) {
    // 销毁设备节点和类
    device_destroy(gpio_dev->gpio_class, gpio_dev->dev_num);
    class_destroy(gpio_dev->gpio_class);

    // 删除cdev并释放设备号
    cdev_del(&gpio_dev->gpio_cdev);
    unregister_chrdev_region(gpio_dev->dev_num, 1);

    // 释放GPIO资源
    for (int i = 0; i < gpio_count; i++) {
        gpio_free(gpio_dev->gpio_list[i]);
    }

    // 释放内存
    kfree(gpio_dev->gpio_list);
    kfree(gpio_dev->gpio_status);
    kfree(gpio_dev);

    printk(KERN_INFO "简易通用GPIO驱动已退出\n");
}

核心作用是当执行 rmmod 驱动名.ko 卸载驱动模块时,内核自动调用gpio_led_exit函数按照「与申请资源完全相反的顺序」,释放驱动初始化阶段申请的所有内核资源 + 硬件资源 + 动态内存,杜绝内存泄漏、GPIO 资源占用、设备号残留、内核对象悬空等问题,保证内核干净无残留

模块信息

c 复制代码
// 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Simple GPIO Driver");
MODULE_DESCRIPTION("Simple Universal Multi-GPIO Driver (Set/Read Level)");
MODULE_VERSION("1.0");

这里必须要写的就是第一行,标注为开源,否则内核不会允许加载这个驱动

相关推荐
01传说5 小时前
Linux-yum源切换阿里centos7 实战好用
linux·运维·服务器
颜子鱼5 小时前
Linux字符设备驱动
linux·c语言·驱动开发
是娇娇公主~5 小时前
Redis 悲观锁与乐观锁
linux·redis·面试
晚风_END6 小时前
Linux|服务器运维|diff和vimdiff命令详解
linux·运维·服务器·开发语言·网络
HIT_Weston6 小时前
83、【Ubuntu】【Hugo】搭建私人博客:文章目录(二)
linux·运维·ubuntu
.普通人6 小时前
树莓派4Linux 单个gpio口驱动编写
linux
luckily灬6 小时前
Docker执行hello-world报错&Docker镜像源DNS解析异常处理
linux·docker
REDcker7 小时前
C++ 崩溃堆栈捕获库详解
linux·开发语言·c++·tcp/ip·架构·崩溃·堆栈
技术小李...7 小时前
Linux7.2安装Lsync3.1.2文件同步服务
linux·lsync