要想正常进行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");
这里必须要写的就是第一行,标注为开源,否则内核不会允许加载这个驱动