树莓派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>

#define GPIO_LED_PIN 516
#define DEV_NAME "gpio_led"
#define DEV_COUNT 1
struct gpio_led_dev {
	dev_t devid;
	struct cdev cdev;
	struct class *class;
	struct device *device;
	int gpio_pin;
};

static struct gpio_led_dev led_dev;
static int gpio_led_open(struct inode *inode,struct file *filp)
{
	filp->private_data =&led_dev;
	printk(KERN_INFO "GPIO LED device opened!\n");
	return 0;
}

static ssize_t gpio_led_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos)
{
	struct gpio_led_dev *dev=filp->private_data;
	char kbuf[2]={0};
	int ret;
	if(count > sizeof(kbuf))
	{
		count=sizeof(kbuf);
	}
	if(!buf)
	{
		return -EINVAL;
	}

	ret=copy_from_user(kbuf,buf,count);
	if(ret < 0)
	{	
		printk(KERN_ERR "copy_from_user failed!\n");
        	return -EFAULT;
	}

	switch(kbuf[0])
	{
		case '0':
			gpio_set_value(dev->gpio_pin,0);
			printk(KERN_INFO "GPIO LED OFF (low level)\n");
			break;
		case'1':
			gpio_set_value(dev->gpio_pin,1);
			printk(KERN_INFO "GPIO LED ON (high level)\n");
			break;
		default:
			printk(KERN_ERR "Invalid input! Only support '0' or '1'\n");
			return -EINVAL;
	}

	return count;	
}
static int gpio_led_release(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "GPIO LED device closed!\n");
    return 0;
}

/* ===================== 4. 定义file_operations结构体 ===================== */
static const struct file_operations gpio_led_fops = {
    .owner = THIS_MODULE,
    .open = gpio_led_open,
    .write = gpio_led_write,
    .release = gpio_led_release,
};
static int __init gpio_led_init(void)
{
	int ret;
	ret=alloc_chrdev_region(&led_dev.devid,0,DEV_COUNT,DEV_NAME);
	if(ret<0)
	{
		printk(KERN_ERR "alloc_chrdev_region failed! ret = %d\n", ret);
		goto err_devid;
	}
    printk(KERN_INFO "alloc_chrdev_region success! major = %d, minor = %d\n",
           MAJOR(led_dev.devid), MINOR(led_dev.devid));
    	cdev_init(&led_dev.cdev, &gpio_led_fops);
    	led_dev.cdev.owner = THIS_MODULE;
    	ret = cdev_add(&led_dev.cdev, led_dev.devid, DEV_COUNT);
	if(ret<0)
	{
		printk(KERN_ERR "cdev_add failed! ret = %d\n", ret);
		goto err_cdev;
	}

	led_dev.class=class_create(DEV_NAME);
	if(IS_ERR(led_dev.class))
	{
		printk(KERN_ERR "class_create failed!\n");
       	 	ret = PTR_ERR(led_dev.class);
        	goto err_class;
	}
	
	led_dev.device=device_create(led_dev.class,NULL,led_dev.devid,NULL,DEV_NAME);
	if(IS_ERR(led_dev.device))
	{
		printk(KERN_ERR "device_create failed!\n");
       		ret = PTR_ERR(led_dev.device);
        	goto err_device;
	}

	led_dev.gpio_pin=GPIO_LED_PIN;
	ret=gpio_request(led_dev.gpio_pin,DEV_NAME);
	if(ret<0)
	{
        	printk(KERN_ERR "gpio_request failed! ret = %d\n", ret);
        	goto err_gpio;
	}
	
	ret=gpio_direction_output(led_dev.gpio_pin,0);
	if (ret < 0) 
	{
        	printk(KERN_ERR "gpio_direction_output failed!\n");
        	goto err_gpio_dir;
    	}
	printk(KERN_INFO "GPIO LED driver init success!\n");
    	return 0;
err_gpio_dir:
    gpio_free(led_dev.gpio_pin);
err_gpio:
    device_destroy(led_dev.class, led_dev.devid);
err_device:
    class_destroy(led_dev.class);
err_class:
    cdev_del(&led_dev.cdev);
err_cdev:
    unregister_chrdev_region(led_dev.devid, DEV_COUNT);
err_devid:
    return ret;
}
static void __exit gpio_led_exit(void)
{
	gpio_set_value(led_dev.gpio_pin,0);
	gpio_free(led_dev.gpio_pin);

	device_destroy(led_dev.class,led_dev.devid);
	class_destroy(led_dev.class);

	cdev_del(&led_dev.cdev);

	unregister_chrdev_region(led_dev.devid,DEV_COUNT);

	printk(KERN_INFO "GPIO LED driver exit success!\n");
}
module_init(gpio_led_init);
module_exit(gpio_led_exit);

MODULE_LICENSE("GPL");

接下来我们一部分一部分的讲

定义设备相关结构体

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>

#define GPIO_LED_PIN 516//定义要操作的BCM编码
#define DEV_NAME "gpio_led"// /dev下的设备名
#define DEV_COUNT 1 //设备数量
struct gpio_led_dev {
	dev_t devid;//存储设备的主次设备号
	struct cdev cdev;//cdev是Linux字符设备的核心结构体,封装了设备的操作函数集和设备号,是字符设备注册到内核的关键
	struct class *class;//用于在sysfs中创建设备类,为自动生成设备节点 /dev 下提供基础
	struct device *device;//表示具体的设备实例,最终会触发udev/mdev创建 /dev 下的设备文件,让用户层能通过文件操作访问设备
	int gpio_pin;//存储LED对应的GPIO引脚号,将驱动的软件逻辑与硬件GPIO引脚绑定,方便在读写操作中控制GPIO
};

static struct gpio_led_dev led_dev;//创建相关的结构体变量

这一部分定义的宏定义,gpio_led_dev结构体,核心目的是集中管理GPIO LED字符设备的所有相关资源与状态,让驱动的资源管理更加规整,代码逻辑更加清晰,

open函数

c 复制代码
static int gpio_led_open(struct inode *inode,struct file *filp)
{
	filp->private_data =&led_dev;
	printk(KERN_INFO "GPIO LED device opened!\n");
	return 0;
}

这段代码则是有三个核心作用:
1.将LED驱动的全局设备结构体led_dev 绑定到内核文件句柄filp->private_data,为驱动后续的read/write等操作传递硬件设备信息
2.通过printk打印内核日志,进行提示,方便驱动调试和问题排查
3.状态反馈,返回0,向内核和用户层确认设备打开成功,完成设备打开的流程

提醒:这里我是直接使用&取地址,其实这不规范,尽管能运行,但是不是标准,正确的做法应该是使用内核提供的container_of函数去找结构体的地址

write函数

c 复制代码
static ssize_t gpio_led_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos)
{
	struct gpio_led_dev *dev=filp->private_data;
	char kbuf[2]={0};
	int ret;
	if(count > sizeof(kbuf))
	{
		count=sizeof(kbuf);
	}
	if(!buf)
	{
		return -EINVAL;
	}

	ret=copy_from_user(kbuf,buf,count);
	if(ret < 0)
	{	
		printk(KERN_ERR "copy_from_user failed!\n");
        	return -EFAULT;
	}

	switch(kbuf[0])
	{
		case '0':
			gpio_set_value(dev->gpio_pin,0);
			printk(KERN_INFO "GPIO LED OFF (low level)\n");
			break;
		case'1':
			gpio_set_value(dev->gpio_pin,1);
			printk(KERN_INFO "GPIO LED ON (high level)\n");
			break;
		default:
			printk(KERN_ERR "Invalid input! Only support '0' or '1'\n");
			return -EINVAL;
	}

	return count;	
}

这段代码的核心功能为:

c 复制代码
struct gpio_led_dev *dev=filp->private_data;

1.表示通过open函数绑定的private_data,拿到LED对应的GPIO引脚号,这是后续控制硬件的基础

c 复制代码
	if(count > sizeof(kbuf))
	{
		count=sizeof(kbuf);
	}
	if(!buf)
	{
		return -EINVAL;
	}

2.这部分则是限制用户输入长度:最多接收1个字符;并且检查用户传的buf是否为空,避免空指针错误

c 复制代码
ret=copy_from_user(kbuf,buf,count);

3.将用户层数据传到内核层,用户层和内核层内存空间隔离,必须用copy_from_user(内核提供的安全函数),把用户层传来的指令复制到内核的kbuf中

c 复制代码
	switch(kbuf[0])
	{
		case '0':
			gpio_set_value(dev->gpio_pin,0);
			printk(KERN_INFO "GPIO LED OFF (low level)\n");
			break;
		case'1':
			gpio_set_value(dev->gpio_pin,1);
			printk(KERN_INFO "GPIO LED ON (high level)\n");
			break;
		default:
			printk(KERN_ERR "Invalid input! Only support '0' or '1'\n");
			return -EINVAL;
	}

这部分则是通过switch判断用户输入的字符:输入'0'表示输出低电平,输入'1'表示输出高电平,其他输入则报错并返回无效指令

c 复制代码
return count;

返回count表示数据已成功处理

release函数

c 复制代码
static int gpio_led_release(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "GPIO LED device closed!\n");
    return 0;
}

这段代码是file_operations操作集中的设备关闭回调函数,核心作用是对应用户层调用close()关闭设备文件时,执行驱动的收尾/清理逻辑
当我们关闭设备时,这里只做了一件事情,打印内核日志,提示用户设备已被关闭

定义file_operations结构体

c 复制代码
/* ===================== 4. 定义file_operations结构体 ===================== */
static const struct file_operations gpio_led_fops = {
    .owner = THIS_MODULE,
    .open = gpio_led_open,
    .write = gpio_led_write,
    .release = gpio_led_release,
};

为什么要定义这个结构体:因为内核只认识自己的规则,不认识我们自定义的函数名,所以这个结构就是使得内核知道当用户调用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_release:调用gpio_led_release;

init函数

c 复制代码
static int __init gpio_led_init(void)
{
	int ret;
	ret=alloc_chrdev_region(&led_dev.devid,0,DEV_COUNT,DEV_NAME);
	if(ret<0)
	{
		printk(KERN_ERR "alloc_chrdev_region failed! ret = %d\n", ret);
		goto err_devid;
	}
    printk(KERN_INFO "alloc_chrdev_region success! major = %d, minor = %d\n",
           MAJOR(led_dev.devid), MINOR(led_dev.devid));
    	cdev_init(&led_dev.cdev, &gpio_led_fops);
    	led_dev.cdev.owner = THIS_MODULE;
    	ret = cdev_add(&led_dev.cdev, led_dev.devid, DEV_COUNT);
	if(ret<0)
	{
		printk(KERN_ERR "cdev_add failed! ret = %d\n", ret);
		goto err_cdev;
	}

	led_dev.class=class_create(DEV_NAME);
	if(IS_ERR(led_dev.class))
	{
		printk(KERN_ERR "class_create failed!\n");
       	 	ret = PTR_ERR(led_dev.class);
        	goto err_class;
	}
	
	led_dev.device=device_create(led_dev.class,NULL,led_dev.devid,NULL,DEV_NAME);
	if(IS_ERR(led_dev.device))
	{
		printk(KERN_ERR "device_create failed!\n");
       		ret = PTR_ERR(led_dev.device);
        	goto err_device;
	}

	led_dev.gpio_pin=GPIO_LED_PIN;
	ret=gpio_request(led_dev.gpio_pin,DEV_NAME);
	if(ret<0)
	{
        	printk(KERN_ERR "gpio_request failed! ret = %d\n", ret);
        	goto err_gpio;
	}
	
	ret=gpio_direction_output(led_dev.gpio_pin,0);
	if (ret < 0) 
	{
        	printk(KERN_ERR "gpio_direction_output failed!\n");
        	goto err_gpio_dir;
    	}
	printk(KERN_INFO "GPIO LED driver init success!\n");
    	return 0;
err_gpio_dir:
    gpio_free(led_dev.gpio_pin);
err_gpio:
    device_destroy(led_dev.class, led_dev.devid);
err_device:
    class_destroy(led_dev.class);
err_class:
    cdev_del(&led_dev.cdev);
err_cdev:
    unregister_chrdev_region(led_dev.devid, DEV_COUNT);
err_devid:
    return ret;
}

这里的 __init是内核专属的宏,作用有两个:
1.标记这个函数是驱动的初始化入口函数,内核加载驱动模块时,会优先执行带 __init的函数
2.内核初始化完成后,会自动释放这个函数占用的内存空间,节省内核资源

这里的代码严格按照内核层->设备层->硬件层的顺序初始化,不能乱

c 复制代码
	int ret;
	ret=alloc_chrdev_region(&led_dev.devid,0,DEV_COUNT,DEV_NAME);
	if(ret<0)
	{
		printk(KERN_ERR "alloc_chrdev_region failed! ret = %d\n", ret);
		goto err_devid;
	}
    printk(KERN_INFO "alloc_chrdev_region success! major = %d, minor = %d\n",
           MAJOR(led_dev.devid), MINOR(led_dev.devid));

1.这里的代码是向内核动态申请设备号,内核通过这个编号区分不同的硬件设备,led_dev.devid存储申请到的设备号,MAJOR()是取主设备号,MINOR()是取次设备号

c 复制代码
    	cdev_init(&led_dev.cdev, &gpio_led_fops);
    	led_dev.cdev.owner = THIS_MODULE;
    	ret = cdev_add(&led_dev.cdev, led_dev.devid, DEV_COUNT);
	if(ret<0)
	{
		printk(KERN_ERR "cdev_add failed! ret = %d\n", ret);
		goto err_cdev;
	}

2.初始化 + 注册字符设备 cdev_init + cdev_add:这里将我们之前写的gpio_led_fops结构体真正用上了,cdev是Linux内核中字符设备的核心结构体,代表一个字符设备实体;cdev_init初始化这个cdev结构体,把我们前面的gpio_led_fops函数映射表绑定到这个字符设备上,内核之后就知道这个设备号对应的驱动函数是哪些;cdev_add把初始化好的字符设备,正式注册到Linux内核的字符设备列表中;到了这一步后,内核层面的驱动就注册完成了,内核能识别这个led设备,但用户层还看不到访问不到这个设备

c 复制代码
	led_dev.class=class_create(DEV_NAME);
	if(IS_ERR(led_dev.class))
	{
		printk(KERN_ERR "class_create failed!\n");
       	 	ret = PTR_ERR(led_dev.class);
        	goto err_class;
	}

3.创建设备类 class_create:这里的核心作用是在内核中创建一个设备类,为生成用户层设备文件铺路,class是内核的设备分类管理结构体,作用是把同类型的设备归类,并且如果没有class,就无法在/dev目录下生成设备文件,IS_ERR()是内核判断指针是否合法的标准函数,判断类是否创建成功;如果失败了的话,那么就跳转到err_class,执行cdev_del删除字符设备+释放设备号

c 复制代码
	led_dev.device=device_create(led_dev.class,NULL,led_dev.devid,NULL,DEV_NAME);
	if(IS_ERR(led_dev.device))
	{
		printk(KERN_ERR "device_create failed!\n");
       		ret = PTR_ERR(led_dev.device);
        	goto err_device;
	}

4.创建设备节点文件 device_create:核心作用是在/dev目录下,生成可被用户层访问的设备节点文件/dev/gpio_led,这是用户层能操作驱动的核心关键,执行完这行后,在/dev下就可以看到我们创建的gpio_led

c 复制代码
	led_dev.gpio_pin=GPIO_LED_PIN;
	ret=gpio_request(led_dev.gpio_pin,DEV_NAME);
	if(ret<0)
	{
        	printk(KERN_ERR "gpio_request failed! ret = %d\n", ret);
        	goto err_gpio;
	}

5.申请 GPIO 引脚资源 gpio_request:核心作用是向内核申请独占使用指定的GPIO引脚,防止资源冲突,GPIO_LED_PIN是我们自己定义的宏

c 复制代码
	ret=gpio_direction_output(led_dev.gpio_pin,0);
	if (ret < 0) 
	{
        	printk(KERN_ERR "gpio_direction_output failed!\n");
        	goto err_gpio_dir;
    	}

6.配置 GPIO 引脚为输出模式 gpio_direction_output:核心作用是配置GPIO引脚的工作模式+初始化电平状态,第一个参数是申请的GPIO引脚,第二个是GPIO引脚的初始化电平,0为低电平,1为高电平

c 复制代码
err_gpio_dir:
    gpio_free(led_dev.gpio_pin);
err_gpio:
    device_destroy(led_dev.class, led_dev.devid);
err_device:
    class_destroy(led_dev.class);
err_class:
    cdev_del(&led_dev.cdev);
err_cdev:
    unregister_chrdev_region(led_dev.devid, DEV_COUNT);
err_devid:

这段是驱动开发的重点,核心规则是倒序释放资源,驱动初始化是从上到下申请资源,资源释放必须是从下到上倒序释放,这个目的是防止内存泄漏,资源占用,保证内核的资源干净

gpio_led_exit 卸载函数

c 复制代码
static void __exit gpio_led_exit(void)
{
	gpio_set_value(led_dev.gpio_pin,0);
	gpio_free(led_dev.gpio_pin);

	device_destroy(led_dev.class,led_dev.devid);
	class_destroy(led_dev.class);

	cdev_del(&led_dev.cdev);

	unregister_chrdev_region(led_dev.devid,DEV_COUNT);

	printk(KERN_INFO "GPIO LED driver exit success!\n");
}

__exit是内核专属红,和初始化的 __init配对出现,标记这个函数是驱动的卸载出口函数,内核卸载驱动时只会执行带该宏的函数

一、释放硬件GPIO资源

gpio_set_value(led_dev.gpio_pin,0);

gpio_free(led_dev.gpio_pin);

1.gpio_set_value(led_dev.gpio_pin,0); 在卸载驱动前,强制把LED对应的GPIO引脚拉低
2.gpio_free(led_dev.gpio_pin):归还申请的GPIO引脚资源,和初始化里的gpio_request是严格反向操作

二、销毁用户层设备文件相关资源

device_destroy(led_dev.class,led_dev.devid);

class_destroy(led_dev.class);

1.device_destroy(...)核心作用是删除/dev目录下的设备节点文件/dev/gpio_led,和初始化里的device_create()是反向操作
2.class_destroy(led_dev.class)核心作用是销毁内核中创建的设备类,和初始化的class_create是反向操作,这里执行顺序有要求,必须先删除设备文件,再销毁设备类,和初始化先创建类再创建文件的顺序相反

三、注销【内核层字符设备相关资源】(对应初始化的 cdev 操作,反向执行)

cdev_del(&led_dev.cdev);

这里的核心作用是从Linux内核的字符设备列表中,删除我们注册的LED字符设备,和初始化里的cdev_add()是反向操作

四、注销【内核设备号资源】(对应初始化的设备号申请,反向执行)

unregister_chrdev_region(led_dev.devid,DEV_COUNT);

这里的核心作用是把初始化申请的主+次设备号归还给内核

模块信息

c 复制代码
module_init(gpio_led_init);
module_exit(gpio_led_exit);

MODULE_LICENSE("GPL");

module_init(gpio_led_init);

这个部分的核心作用是向Linux内核注册我们写的驱动的初始化入口函数,告诉内核当我们加载这个驱动时,执行的初始化函数是gpio_led_init

module_exit(gpio_led_exit);

这个部分的核心作用是向Linux内核注册我们写的驱动的卸载出口函数,告诉内核当我们卸载这个驱动时,执行的卸载函数是gpio_led_exit

MODULE_LICENSE("GPL");

这个部分是向Linux内核声明当前驱动模块的开源协议版本,如果没有,驱动一定会加载失败

相关推荐
luckily灬3 小时前
Docker执行hello-world报错&Docker镜像源DNS解析异常处理
linux·docker
REDcker4 小时前
C++ 崩溃堆栈捕获库详解
linux·开发语言·c++·tcp/ip·架构·崩溃·堆栈
技术小李...4 小时前
Linux7.2安装Lsync3.1.2文件同步服务
linux·lsync
Frank_refuel4 小时前
Linux常用指令详解
linux·运维·服务器
橘色的喵4 小时前
解决 VMware Ubuntu 22.04 安装搜狗输入法后鼠标焦点自动跳出/被抢占问题
linux·ubuntu·计算机外设
hkNaruto4 小时前
【linux】Linux系统中双连字符 --的主要作用
linux·运维·服务器
oMcLin4 小时前
Ubuntu 24.04系统 防火墙配置问题导致 MySQL 无法远程连接:firewalld 与 iptables 的冲突排查
linux·mysql·ubuntu
wdfk_prog5 小时前
[Linux]学习笔记系列 -- [fs]pidfs
linux·笔记·学习
Run_Teenage5 小时前
Linux:自主Shell命令行解释器
linux·运维·服务器