十六、一个基本的GPIO驱动程序

前言

前面学习和了解了字符设备和文件系统是怎么关联起来的,这篇文章就来实践一下,加深前面的理解。我们写一个基本的GPIO引脚驱动用于驱动板载的LED灯和蜂鸣器,并在文件系统里面创建字符设备文件节点,用于控制GPIO引脚。

驱动代码

c 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
// 提供struct cdev结构体定义
#include <linux/cdev.h>
// 提供dev_t定义
#include <linux/types.h>
// 提供struct file_operations定义
#include <linux/fs.h>
// 提供ioremap相关定义
#include <asm/io.h>
// 提供字符串比较函数
#include <linux/string.h>
// 提供copy_from_user函数
#include <asm/uaccess.h>
// 提供sysfs_streq函数
#include <linux/device.h>

/* 原理图信息
LED:  GPIO1_IO03, Low-Triggerd
BEEP: GPIO5_IO01, Low-Triggerd
*/

/* 寄存器设置
开启GPIO时钟:
LED:  CCGR1 0x020C_406C [CG13]=b[27:26]=0b_11, GPIO1_CLK_ENABLE
BEEP: CCGR1 0x020C_406C [CG15]=b[31:30]=0b_11, GPIO5_CLK_ENABLE

0b_00 - 所有模式下关闭时钟
0b_01 - 仅在运行模式(Run mode)下打开
0b_10 - 保留
0b_11 - 在除了停止模式(Stop mode)外的所有模式下都打开

引脚复用为GPIO:
LED:  SW_MUX_CTL_PAD_GPIO1_IO03   0x020E_0068 b[3:0]=0b_0101, GPIO1_IO03
BEEP: SW_MUX_CTL_PAD_SNVS_TAMPER1 0x0229_000C b[3:0]=0b_0101, GPIO5_IO01

引脚电气属性:
LED:  SW_PAD_CTL_PAD_GPIO1_IO03   0x020E_02F4 default=0x000010B0
BEEP: SW_PAD_CTL_PAD_SNVS_TAMPER1 0x0229_0050 default=0x000110A0

GPIO方向:
LED:  GPIO1_GDIR 0x0209_C004 b[3]=0b_1, OUTPUT
BEEP: GPIO5_GDIR 0x020A_C004 b[1]=0b_1, OUTPUT

GPIO电平:
LED:  GPIO1_DR 0x0209_C000 b[3]
BEEP: GPIO5_DR 0x020A_C000 b[1]
*/

#define GPIO_NUM (2)

typedef struct
{
    u32 reg_addr_clk;       // GPIO的时钟使能寄存器
    int reg_bit_clk;        // GPIO引脚的时钟使能位偏移

    u32 reg_addr_iomux;     // GPIO引脚复用寄存器

    u32 reg_addr_pad;       // GPIO引脚电气属性寄存器   
    u32 pad_default;        // GPIO引脚默认电气属性

    u32 reg_addr_gdir;      // GPIO引脚方向寄存器
    int reg_bit_gdir;       // GPIO引脚的方向位偏移

    u32 reg_addr_dr;        // GPIO引脚电平值寄存器
    int reg_bit_dr;         // GPIO引脚电平值位偏移

    int active_level;       // GPIO引脚的有效电平
} gpio_info_t;

typedef struct
{
    void __iomem *vir_addr_clk;
    void __iomem *vir_addr_iomux;
    void __iomem *vir_addr_pad;
    void __iomem *vir_addr_gdir;
    void __iomem *vir_addr_dr;
} gpio_vir_addr_t;

typedef struct
{
    dev_t gpio_dev;
    struct cdev gpio_cdev;
} chrdev_info_t;

const gpio_info_t gpios[GPIO_NUM] = 
{
    {
        .reg_addr_clk = 0x020C406C,
        .reg_bit_clk = 26,
        .reg_addr_iomux = 0x020E0068,
        .reg_addr_pad =  0x020E02F4,
        .pad_default = 0x000010B0,
        .reg_addr_gdir = 0x0209C004,
        .reg_bit_gdir = 3,
        .reg_addr_dr = 0x0209C000,
        .reg_bit_dr = 3,
        .active_level = 0,
    },
    {
        .reg_addr_clk = 0x020C406C,
        .reg_bit_clk = 30,
        .reg_addr_iomux = 0x0229000C,
        .reg_addr_pad =  0x02290050,
        .pad_default = 0x000110A0,
        .reg_addr_gdir = 0x020AC004,
        .reg_bit_gdir = 1,
        .reg_addr_dr = 0x020AC000,
        .reg_bit_dr = 1,
        .active_level = 0,
    }
};

gpio_vir_addr_t vir_addrs[GPIO_NUM] = { { NULL}, { NULL } };
chrdev_info_t gpio_chrdev;

static int init_gpios(void)
{
    int i;
    u32 val;

    for (i = 0; i < GPIO_NUM; i++)
    {
        vir_addrs[i].vir_addr_clk = ioremap(gpios[i].reg_addr_clk, 4);
        vir_addrs[i].vir_addr_iomux = ioremap(gpios[i].reg_addr_iomux, 4);
        vir_addrs[i].vir_addr_pad = ioremap(gpios[i].reg_addr_pad, 4);
        vir_addrs[i].vir_addr_gdir = ioremap(gpios[i].reg_addr_gdir, 4);
        vir_addrs[i].vir_addr_dr = ioremap(gpios[i].reg_addr_dr, 4);

        if (!vir_addrs[i].vir_addr_clk || !vir_addrs[i].vir_addr_iomux ||
            !vir_addrs[i].vir_addr_pad || !vir_addrs[i].vir_addr_gdir ||
            !vir_addrs[i].vir_addr_dr)
            goto error;
    }

    for (i = 0; i < GPIO_NUM; i++)
    {
        // 使能GPIO口时钟
        val = readl(vir_addrs[i].vir_addr_clk);
        val &= ~(0x3 << gpios[i].reg_bit_clk);
        writel(val | (0x3 << gpios[i].reg_bit_clk), vir_addrs[i].vir_addr_clk);

        // 设置引脚复用为GPIO
        writel(0x5, vir_addrs[i].vir_addr_iomux);

        // 恢复引脚的默认电气属性
        writel(gpios[i].pad_default, vir_addrs[i].vir_addr_pad);

        // 设置GPIO为输出
        val = readl(vir_addrs[i].vir_addr_gdir);
        val &= ~(0x1 << gpios[i].reg_bit_gdir);
        writel(val | (0x1 << gpios[i].reg_bit_gdir), vir_addrs[i].vir_addr_gdir);

        // 输出无效电平
        val = readl(vir_addrs[i].vir_addr_dr);
        val &= ~(0x1 << gpios[i].reg_bit_dr);
        writel(val | ((gpios[i].active_level ? 0x0 : 0x1) << gpios[i].reg_bit_dr), vir_addrs[i].vir_addr_dr);
    }

    return 0;

error:
    for (i = 0; i < GPIO_NUM; i++)
    {
        if (vir_addrs[i].vir_addr_clk)
        {
            iounmap(vir_addrs[i].vir_addr_clk);
            vir_addrs[i].vir_addr_clk = NULL;
        }
        if (vir_addrs[i].vir_addr_iomux)
        {
            iounmap(vir_addrs[i].vir_addr_iomux);
            vir_addrs[i].vir_addr_iomux = NULL;
        }
        if (vir_addrs[i].vir_addr_pad)
        {
            iounmap(vir_addrs[i].vir_addr_pad);
            vir_addrs[i].vir_addr_pad = NULL;
        }
        if (vir_addrs[i].vir_addr_gdir)
        {
            iounmap(vir_addrs[i].vir_addr_gdir);
            vir_addrs[i].vir_addr_gdir = NULL;
        }
        if (vir_addrs[i].vir_addr_dr)
        {
            iounmap(vir_addrs[i].vir_addr_dr);
            vir_addrs[i].vir_addr_dr = NULL;
        }
    }

    return -1;
}

static void deinit_gpios(void)
{
    int i;
    u32 val;

    for (i = 0; i < GPIO_NUM; i++)
    {
        // 输出无效电平
        val = readl(vir_addrs[i].vir_addr_dr);
        val &= ~(0x1 << gpios[i].reg_bit_dr);
        writel(val | ((gpios[i].active_level ? 0x0 : 0x1) << gpios[i].reg_bit_dr), vir_addrs[i].vir_addr_dr);
    }

    for (i = 0; i < GPIO_NUM; i++)
    {
        // 释放页表
        iounmap(vir_addrs[i].vir_addr_clk);
        iounmap(vir_addrs[i].vir_addr_iomux);
        iounmap(vir_addrs[i].vir_addr_pad);
        iounmap(vir_addrs[i].vir_addr_gdir);
        iounmap(vir_addrs[i].vir_addr_dr);

        vir_addrs[i].vir_addr_clk = NULL;
        vir_addrs[i].vir_addr_iomux = NULL;
        vir_addrs[i].vir_addr_pad = NULL;
        vir_addrs[i].vir_addr_gdir = NULL;
        vir_addrs[i].vir_addr_dr = NULL;
    }
}

static void set_gpio_value(int index, int value)
{
    u32 val;

    val = readl(vir_addrs[index].vir_addr_dr);
    val &= ~(0x1 << gpios[index].reg_bit_dr);
    writel(val | ((value ? 0x1 : 0x0) << gpios[index].reg_bit_dr), vir_addrs[index].vir_addr_dr);
}

static int gpio_open(struct inode *inode, struct file *file)
{
    int minor = MINOR(inode->i_rdev);

    if (minor >= GPIO_NUM)
    {
        printk(KERN_ERR "invalid file node\n");
        return -1;
    }

    return 0;
}

static int gpio_release(struct inode * inode, struct file * file)
{
    int minor = MINOR(inode->i_rdev);

    if (minor >= GPIO_NUM)
    {
        printk(KERN_ERR "invalid file node\n");
        return -1;
    }

    return 0;
}

static ssize_t gpio_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos)
{
    char databuf[32] = { '\0' };
    size_t datalen = 0;
    int minor = MINOR(file->f_inode->i_rdev);

    if (minor >= GPIO_NUM)
    {
        printk(KERN_ERR "invalid file node\n");
        return -1;
    }

    datalen = count >= sizeof(databuf) - 1 ? sizeof(databuf) - 1 : count;

    if (copy_from_user(databuf, buffer, datalen))
    {
        printk(KERN_ERR	"copy from user failed\n");
        return -1;
    }

    if (sysfs_streq(databuf, "ON"))
    {
        set_gpio_value(minor, gpios[minor].active_level ? 1 : 0);
    }
    else if (sysfs_streq(databuf, "OFF"))
    {
        set_gpio_value(minor, gpios[minor].active_level ? 0 : 1);
    }
    else
    {
        printk(KERN_WARNING "unsupported command: %s\n", databuf);
    }

    return count;
}

static const struct file_operations gpio_fops =
{ 
    .open = gpio_open,
    .release = gpio_release,
    .write = gpio_write,
};

static int __init gpio_basic_init(void)
{
    int ret;

    // 申请设备号
    ret = alloc_chrdev_region(&gpio_chrdev.gpio_dev, 0, GPIO_NUM, "gpios");
    if (ret < 0)
    {
        printk(KERN_ERR "can't allocate dev number\n");
        return -1;
    }

    // 设置字符设备驱动对象,添加操作函数集
    gpio_chrdev.gpio_cdev.owner = THIS_MODULE;
    cdev_init(&gpio_chrdev.gpio_cdev, &gpio_fops);

    // 注册字符设备驱动,绑定设备号与字符设备驱动对象
    cdev_add(&gpio_chrdev.gpio_cdev, gpio_chrdev.gpio_dev, GPIO_NUM);

    // 初始化GPIO
    if (init_gpios() != 0)
        goto error;

    return 0;

error:
    cdev_del(&gpio_chrdev.gpio_cdev);
    unregister_chrdev_region(gpio_chrdev.gpio_dev, GPIO_NUM);

    return -1;
}

static void __exit gpio_basic_exit(void)
{
    deinit_gpios();
    cdev_del(&gpio_chrdev.gpio_cdev);
    unregister_chrdev_region(gpio_chrdev.gpio_dev, GPIO_NUM);
}

module_init(gpio_basic_init);
module_exit(gpio_basic_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("David");
MODULE_DESCRIPTION("basic gpio driver");
MODULE_ALIAS("Basic GPIO");

上一篇文章我们总结了要加载字符设备驱动应该做些什么工作,这里补充具体调用了什么函数来完成这些工作:

  1. 我们要创建一个代表字符设备驱动的struct cdev结构体,因为这个结构体会在其它地方被引用,所以它的生命周期为设备驱动存续期间,应该定义成一个字符设备驱动里的一个全局变量或者在堆内存上分配空间,这里我们就是将其定义在全局结构体gpio_chrdev里面。
  2. 定义字符设备操作函数集合struct file_operations结构体,其生命周期也为设备驱动存续期间,这里我们就将其定义成一个全局变量gpio_fops ,并实现了其中的openreleasewrite成员。
  3. 从内核里为这个字符设备驱动分配一个主设备号,并指定其下管理多少个子设备,内核会将分配的设备号信息记录到chrdevs哈希表中。这里我们使用int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)函数让内核为我们自动分配一个主设备号,并指定次设备数量为想要控制的GPIO引脚数量。
  4. 将字符设备操作函数集合struct file_operations指定给字符设备驱动对象struct cdev,这里使用函数void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  5. 往内核里注册这个字符设备驱动对象struct cdev并指定主设备号dev,内核会在probes哈希表里添加主设备号dev和字符设备驱动对象struct cdev的映射。这里使用函数int cdev_add(struct cdev *p, dev_t dev, unsigned count)

Makefile

bash 复制代码
KERNEL_DIR := /home/david/share/driver-uboot-linux/linux

ARCH := arm
CROSS_COMPILE := arm-linux-gnueabihf-

export ARCH CROSS_COMPILE

module_name := gpio_basic
install_dir := /home/david/share/nfs/rootfs/modules/$(module_name)

obj-m := $(module_name).o

.PHONY: all clean install

all:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

install: all
	@if [ ! -d "$(install_dir)" ]; then \
        echo "Creating $(install_dir) directory..."; \
        mkdir -p $(install_dir); \
	fi

	cp *.ko $(install_dir)

加载驱动模块

bash 复制代码
insmod gpio_basic.ko

查看主设备号如下:

bash 复制代码
/modules/gpio_basic # cat /proc/devices | grep gpio
248 gpios

创建字符设备文件节点

bash 复制代码
mknod /dev/led c 248 0
mknod /dev/beep c 248 1

测试

bash 复制代码
echo ON > /dev/led
echo OFF > /dev/led
echo ON > /dev/beep
echo OFF > /dev/beep

经验证可以正常控制LED灯和蜂鸣器。

删除设备文件节点

bash 复制代码
rm /dev/led
rm /dev/beep

卸载驱动模块

bash 复制代码
rmmod gpio_basic
相关推荐
水天需0101 小时前
Vim 搜索和替换详解
linux
ModestCoder_1 小时前
Ubuntu 22.04,Isaac Sim 5.1.0 + Isaac Lab 2.3.0 Conda 环境安装指南
linux·ubuntu·conda
水天需0101 小时前
Vim 学习全面指南
linux
9ilk1 小时前
【Linux】--- 多路转接select / poll / epoll
linux·运维·网络
赖small强2 小时前
【Linux驱动开发】Linux 中断机制深度解析:原理、监控与实战
linux·中断·硬件中断
buyutang_2 小时前
Linux 网络编程:TCP协议Socket开发全流程,理解多线程多进程实现的多连接网络通讯模型
linux·网络·tcp/ip
小猫挖掘机(绝版)2 小时前
在Ubuntu 20.04 部署DiffPhysDrone并在Airsim仿真完整流程
linux·ubuntu·自动驾驶·无人机·端到端
初圣魔门首席弟子2 小时前
第六章、[特殊字符] HTTP 深度进阶:报文格式 + 服务器实现(从理论到代码)
linux·网络·c++
zl0_00_02 小时前
isctf2025 部分wp
linux·前端·javascript