linux驱动开发(一)—GPIO驱动框架

本文详细介绍了Linux内核中GPIO模型的结构,包括structgpio_desc、structdavinci_gpio_controller和structgpio_chip等关键概念。重点阐述了如何在驱动开发中初始化和操作GPIO,包括GPIO申请、输入输出设置、获取值和设置值等步骤,并提供了实际的驱动实例,以矩阵键盘为例展示了如何利用GPIO进行输入输出操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

         GPIO驱动是Linux驱动开发中最基础、但却是很常用、很重要的驱动。比如你要点亮一个LED灯、键盘扫描、输出高低电平等等。而Linux内核的强大之处在于对最底层的GPIO硬件操作层的基础上封装了一些统一的GPIO操作接口,也就是所谓的GPIO驱动框架。这样开发人员可以调用这些接口去操作设备的IO口,不需要担心硬件平台的不同导致IO口的不同。

         今天,我主要讲的就是如何使用Linux内核封装好的GPIO接口函数在驱动开发中需要操作GPIO时候的使用。

          文章部分内容参考他人博客,特此声明!

概述

       GPIO是与硬件体系密切相关的,linux提供一个模型来让驱动统一处理GPIO,即各个板卡都有实现自己的gpio_chip控制模块:request, free, input,output, get,set,irq...然后把控制模块注册到内核中,这时会改变全局gpio数组:gpio_desc[]. 当用户请求gpio时,就会到这个数组中找到,并调用这个GPIO对应的gpio_chip的处理函数。gpio实现为一组可用的 gpio_chip, 由驱动传入对应 gpio的全局序号去 request, dataout ,datain, free. 这时会调用gpio_chip中具体的实现。

        GPIO是一组可控件的脚,由多个寄存器同时控制。通过设置对应的寄存器可以达到设置GPIO口对应状态与功能。数据状态,输入输出方向,清零,中断(哪个边沿触发), 一般是一组(bank)一组的。寄存器读写函数: __raw_writel() __raw_writeb() __raw_readl() __raw_readb()。

1. Linux内核中GPIO模型的结构

1.1 struct gpio_desc

//表示一个gpio口,含对应的gpio_chip.
//对于每一个gpio,都有一个gpio描述符,这个描述符包含了这个gpio所属的控制器即chip和一些标志,label等。

struct gpio_desc {
    struct gpio_chip    *chip;
    unsigned long       flags;
/* flag symbols are bit numbers */
#define FLAG_REQUESTED  0
#define FLAG_IS_OUT 1
#define FLAG_EXPORT 2   /* protected by sysfs_lock */
#define FLAG_SYSFS  3   /* exported via /sys/class/gpio/control */
#define FLAG_TRIG_FALL  4   /* trigger on falling edge */
#define FLAG_TRIG_RISE  5   /* trigger on rising edge */
#define FLAG_ACTIVE_LOW 6   /* value has active low */
#define FLAG_OPEN_DRAIN 7   /* Gpio is open drain type */
#define FLAG_OPEN_SOURCE 8  /* Gpio is open source type */
#define FLAG_USED_AS_IRQ 9  /* GPIO is connected to an IRQ */

#define ID_SHIFT    16  /* add new flags before this one */

#define GPIO_FLAGS_MASK     ((1 << ID_SHIFT) - 1)
#define GPIO_TRIGGER_MASK   (BIT(FLAG_TRIG_FALL) | BIT(FLAG_TRIG_RISE))

#ifdef CONFIG_DEBUG_FS
    const char      *label;
#endif
};
//采用了一个具有ARCH_NR_GPIOS大小的gpio描述符数组。这个描述符数组便代表了系统所有的gpio。
static struct gpio_desc gpio_desc[ARCH_NR_GPIOS];

1.2  struct davinci_gpio_controller

//一组GPIO控制器结构,例如GPIO0和GPIO1是一组(共32个GPIO口),共用一组寄存器,所以GPIO0和GPIO1荷载一起用chips[0]来控制
//假如有144个GPIO,分为4组(GPIO0~GPIO8),每组有2个banks(即GPIO0和GPIO1为1组),每组最多可以有32个GPIO,每组的控制寄存器空间有10个

struct davinci_gpio_controller {

    struct gpio_chip    chip;//每组对应的gpio_chip

    int            irq_base;//每组对应的中断

    spinlock_t        lock;//自旋锁

    void __iomem        *regs;//每组的寄存器地址

    void __iomem        *set_data;//设置数据寄存器地址

    void __iomem        *clr_data;//清除数据寄存器地址

    void __iomem        *in_data;//输入数据寄存器地址

}; 


1.3 struct gpio_chip

//每一个davinci_gpio_controller结构都对应于一个gpio_chip结构,gpio_chip既可看成是davinci_gpio_controller结构的补充
//表示一个gpio controller.通过这个结构抽象化所有的GPIO源,而让板上其它的模块可以用相同的接口调用使用这些GPIO。

struct gpio_chip {

    const char    *label;

    struct device    *dev;

    struct module    *owner; 

    int    (*request)(struct gpio_chip *chip,unsigned offset);//请求gpio

    void    *free)(struct gpio_chip *chip,unsigned offset);//释放gpio

    int    (*get_direction)(struct gpio_chip *chip,unsigned offset);

    int    (*direction_input)(struct gpio_chip *chip,unsigned offset);//配置gpio为输入,返回当前gpio状态

    int    (*get)(struct gpio_chip *chip,unsigned offset);//获取gpio的状态

    int    (*direction_output)(struct gpio_chip *chip,unsigned offset, int value);//配置gpio为输出,并设置为value

    int    (*set_debounce)(struct gpio_chip *chip,unsigned offset, unsigned debounce);//设置消抖动时间,尤其是gpio按键时有用

    void    (*set)(struct gpio_chip *chip,unsigned offset, int value);//设置gpio为value值

    int    (*to_irq)(struct gpio_chip *chip,unsigned offset);//把gpio号转换为中断号

    void    (*dbg_show)(struct seq_file *s,struct gpio_chip *chip);

    int    base;// 这个gpio控制器的gpio开始编号

    u16    ngpio;//这个gpio控制器说控制的gpio数

    const char    *const *names;

    unsigned    can_sleep:1;

    unsigned    exported:1;

 

#if defined(CONFIG_OF_GPIO)

    struct device_node *of_node;

    int of_gpio_n_cells;

    int (*of_xlate)(struct gpio_chip *gc,const struct of_phandle_args *gpiospec, u32 *flags);

#endif

#ifdef CONFIG_PINCTRL

    struct list_head pin_ranges;

#endif

};


1.4 struct davinci_gpio_regs

//GPIO寄存器结构

struct davinci_gpio_regs {

    u32 dir; // gpio方向设置寄存器 

    u32 out_data; // gpio设置为输出时,表示输出状态(0或1) 

    u32 set_data; // gpio设置为输出时,用于输出高电平 

    u32 clr_data; // gpio设置为输出时,用于输出低电平 

    u32 in_data; // gpio设置为输入时,用于读取输入值 

    u32 set_rising; // gpio中断上升沿触发设置 

    u32 clr_rising; // gpio中断上升沿触发清除 

    u32 set_falling; // gpio中断下降沿触发设置 

    u32 clr_falling; // gpio中断下降沿触发清除 

    u32 intstat; // gpio中断状态位,由硬件设置,可读取,写1时清除。 

};


1.5 struct gpio

struct gpio {

    unsigned gpio;//gpio号

    unsigned long flags;//gpio标志

    const char *label;//gpio名

};

2.  驱动开发中GPIO初始化操作

在实际的驱动开发中,根据板级资源和CPU手册,GPIO初始化一般需要以下三个步骤:

       1.设置IO口的复用模式,如果某个IO当作GPIO使用,那么就需要根据CPU手册去配置iomux(IO复用寄存器)为GPIO模式;

        2.设置IO口的输入输出方向,根据实际开发需求,将相应的GPIO配置为相应的输入输出方向;

        3.GPIO初始化赋值(输出高低电平)、拉高拉低操作;

2.1 GPIO申请

#########################################

#description:申请一个GPIO资源

#unsigned gpio:要申请的GPIO管脚号,为一个正整数

# const char *label:为申请的GPIO管脚取个名字                 

#########################################           

int gpio_request(unsigned gpio, const char *label);

int gpio_request_one(unsigned gpio, unsigned long flags, const char *label);

2.2 GPIO输入输出设置

#########################################

#description:设置某个GPIO的输入输出方向

#unsigned gpio:要设置的GPIO管脚号,为一个正整数

# int value:设置的值                

#########################################           

int gpio_direction_input(unsigned gpio);
 int gpio_direction_output(unsigned gpio, int value);

2.3 获取GPIO管脚的值和设置GPIO管脚的值

#########################################

#description:获取、设置某个GPIO的值

#unsigned gpio:要获取、设置的GPIO管脚号

# int value:设置的值                

#########################################

int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);

2.4 GPIO当作中断口使用

#########################################

#description:设置某个GPIO为中断口

#unsigned gpio:要设置中断的GPIO管脚号            

#########################################

int gpio_to_irq(unsigned gpio);

返回的值即中断编号可以传给request_irq()和free_irq(),内核通过调用该函数将gpio端口转换为中断,在用户空间也有类似方法。

3. GPIO驱动实例

3.1 以下GPIO驱动例子为矩阵键盘中对GPIO的操作

//设置某个管脚为输入

int set_key_input(unsigned int gpio)
{
    char name[32];
    sprintf(name, "GPIO%d", gpio);
    
    if(gpio_request(gpio,NULL) != 0)
    {
        printk("gpio request error!\n");
        return -1;
    }
    gpio_direction_input(gpio);
    gpio_free(gpio);
    return 0;
}

//设置某个管脚为输出

int set_key_output(unsigned int gpio,int value)
{
    char name[32];
    sprintf(name, "GPIO%d", gpio);

    if(gpio_request(gpio,NULL) != 0)
    {
        printk("gpio request error!\n");
        return -1;
    }
    gpio_direction_output(gpio,value);
    gpio_free(gpio);
    return 0;
}


//获取某个GPIO管脚的值

int get_key_value(unsigned int gpio)
{
    int value= -1;
    if (gpio_request(gpio, NULL) != 0)
    {
        printk("get_key_value err\n");        
        return -1;
    }
    value = gpio_get_value(gpio);
    gpio_free(gpio);

    return value;
}


//设置某个GPIO输出为低电平

int set_key_low(unsigned int gpio)
{

	if (gpio_request(gpio, NULL) != 0) {
		//printk("set_key_low request err\n");
		return -1;
	}
	gpio_direction_output(gpio, 0);
	__gpio_set_value(gpio, 0);
	gpio_free(gpio);

	return 0;
}

//拉高、拉低某个GPIO

//GPIO的拉高拉低操作内核没有提供通用的接口函数,这个需要驱动开发人员根据CPU手册的寄存器配置去封装拉高拉低函数,以下给出一个伪代码的例子:

//假设拉高GPIO1_IO01这个IO:

<span style="font-size:12px;">#define SET_PULL_UP 0x01
#define SET_PULL_DOWN 0x00
#define REG_GPIO_BASE 0x8e000000
#define GPIO1_IO01_OFFSET 0x400

int set_pull_up(unsigned int reg_base,unsigned int offset,int up)
{
     unsigned int gpio_base;
     unsigned int gpio;

     gpio_base = ioreamap(reg_base,SIZE_4K);//调用ioreamap映射GPIO空间到内存,映射大小根据实际需求而定
     gpio = gpio_base + offset; 
     gpio |= up;		//将某个GPIO拉高,根据具体的寄存器操作而定 
     __raw_writel(gpio,gpio_base + offset);
     return 0;  
}

set_pull_up(REG_GPIO_BASE,GPIO1_IO01_OFFSET,SET_PULL_UP);</span>


//拉低某个GPIO

#define SET_PULL_UP 0x01
#define SET_PULL_DOWN 0x00
#define REG_GPIO_BASE 0x8e000000
#define GPIO1_IO01_OFFSET 0x400

int set_pull_down(unsigned int reg_base,unsigned int offset,int down)
{
     unsigned int gpio_base;
     unsigned int gpio;

     gpio_base = ioreamap(reg_base,SIZE_4K);//调用ioreamap映射GPIO空间到内存,映射大小根据实际需求而定
     gpio = gpio_base + offset; 
     gpio &= down;		//将某个GPIO拉低,根据具体的寄存器操作而定 
     __raw_writel(gpio,gpio_base + offset);
     return 0;  
}

set_pull_up(REG_GPIO_BASE,GPIO1_IO01_OFFSET,SET_PULL_DOWN);

 

3.2 总结

         3.1中展示了基本的GPIO操作函数的编写,在实际的驱动开发中,比如对某个连接到CPU的GPIO管脚的外设模块需要初始化的时候,一般都是调用GPIO接口函数进行输入输出、拉高、拉低设置,读者可以参考3.1的例子根据实际开发需求进行修改。

<think>好的,我现在需要给用户介绍Linux驱动开发的入门知识。用户可能对这方面不太熟悉,所以得从基础开始讲起,同时要确保内容清晰易懂。首先,我需要回忆Linux驱动开发的基本概念,包括驱动的作用、分类,以及开发环境的搭建步骤。 接下来,得考虑用户的学习路径。通常,学习驱动开发需要有定的C语言和操作系统基础,特别是对Linux内核有定的了解。所以可能需要先提到这些前置知识,让用户评估自己是否准备好。 然后,应该分步骤介绍入门流程,比如环境准备、驱动类型、代码结构、编译加载方法、调试技巧等。这部分需要结构化,用列表或者分点说明,方便用户步步跟随。同时,要给出个简单的示例代码,比如Hello World模块,并解释关键部分,比如模块的初始化和退出函数,还有许可证声明。 还要注意驱动开发中的常见问题,比如内核编程与用户空间的区别,比如不能使用标准库函数,需要注意并发和内存管理。这些是新手容易犯错的地方,需要特别提醒。 可能还需要推荐些学习资源,比如书籍、在线文档和社区,这样用户可以有进步学习的途径。另外,强调实践的重要性,建议从简单例子开始,逐步深入。 在组织内容时,要确保逻辑清晰,先讲概念,再讲准备,接着是开发步骤,示例,注意事项,最后是学习资源。避免使用过于专业的术语,或者在使用时给出简单解释。同时,检查是否有遗漏的重要点,比如内核版本差异,或者设备树(Device Tree)在较新内核中的使用,这些可能需要提及。 最后,保持语言简洁,用口语化的中文,避免冗长,让用户能轻松理解。需要确认所有步骤都是当前通用的方法,不会因为内核版本过旧而失效。如果有不确定的地方,比如具体的内核API函数,可能需要查阅最新文档确保准确性。</think>Linux 驱动开发是嵌入式系统和操作系统领域的重要技能,以下是入门指南,帮助你理解基本概念和上手实践: --- ### **1. 前置知识** - **C语言基础**:驱动开发主要使用C语言(尤其是内核风格的C)。 - **Linux操作系统基础**:熟悉Linux命令行、文件系统、权限管理等。 - **计算机体系结构**:了解CPU、内存、外设(如GPIO、I2C、USB)的工作原理。 - **内核基础**:理解进程管理、内存管理、中断、内核模块等概念。 --- ### **2. Linux驱动的作用** 驱动是内核与硬件设备的桥梁,负责: - 初始化硬件(如配置寄存器) - 实现设备的数据读写 - 处理中断和DMA操作 - 通过标准接口(如`/dev`、`sysfs`)向用户空间暴露功能 --- ### **3. 驱动分类** - **字符设备**(如键盘、传感器):按字节流访问,常用 `open()`, `read()`, `write()` 接口。 - **块设备**(如硬盘):按数据块访问,支持缓存。 - **网络设备**(如网卡):通过套接字(Socket)通信。 - **平台设备**(如SoC内置外设):与芯片紧密相关,常用设备树(Device Tree)描述。 --- ### **4. 开发环境搭建** - **Linux系统**:推荐Ubuntu或Fedora(安装内核头文件:`sudo apt install linux-headers-$(uname -r)`)。 - **工具链**:GCC、Make、GDB调试工具。 - **内核源码**:下载与当前内核版本匹配的源码([kernel.org](https://www.kernel.org/))。 --- ### **5. 第驱动:Hello World** ```c #include <linux/init.h> #include <linux/module.h> // 模块加载函数 static int __init hello_init(void) { printk(KERN_INFO "Hello, Linux Driver!\n"); return 0; } // 模块卸载函数 static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye!\n"); } // 注册模块入口和出口 module_init(hello_init); module_exit(hello_exit); // 模块信息 MODULE_LICENSE("GPL"); // 开源协议(必须) MODULE_AUTHOR("Your Name"); // 作者 MODULE_DESCRIPTION("A simple example"); // 描述 ``` **编译驱动**: 编写 `Makefile`(注意使用内核的编译系统): ```makefile obj-m += hello.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean ``` **加载/卸载驱动**: ```bash sudo insmod hello.ko # 加载驱动 dmesg | tail # 查看内核日志 sudo rmmod hello # 卸载驱动 ``` --- ### **6. 驱动开发核心步骤** 1. **定义设备操作接口**:实现 `file_operations` 结构体(如 `.open`, `.read`, `.ioctl`)。 2. **注册设备**:使用 `register_chrdev()` 注册字符设备。 3. **处理硬件交互**:通过I/O端口、内存映射或内核API(如 `ioremap()`)访问硬件。 4. **处理中断**:注册中断处理函数 `request_irq()`。 5. **用户空间交互**:通过 `/dev/` 设备文件或 `sysfs` 提供访问接口。 --- ### **7. 调试技巧** - **printk**:内核日志输出(查看:`dmesg`)。 - **GDB + KGDB**:内核调试(需要配置调试内核)。 - **仿真工具**:QEMU模拟硬件环境。 - **动态探测**:`strace`、`perf` 跟踪系统调用。 --- ### **8. 学习资源** - **书籍**: - 《Linux设备驱动程序》(Linux Device Drivers, 简称LDD) - 《Linux内核设计与实现》 - **文档**: - [内核官方文档](https://www.kernel.org/doc/html/latest/) - [设备树(Device Tree)使用手册](https://www.devicetree.org/) - **实践项目**:从LED驱动、按键中断等简单硬件开始,逐步深入复杂设备(如SPI、USB)。 --- ### **9. 注意事项** - **内核编程限制**:不能使用用户空间的库(如glibc),需用内核API。 - **并发与同步**:注意多线程、中断上下文,使用信号量、互斥锁(`mutex`)等。 - **内存管理**:使用 `kmalloc()`/`kfree()` 而非 `malloc()`/`free()`,注意内存泄漏。 --- 通过以上步骤,你可以逐步掌握驱动开发的核心逻辑。建议从简单的字符设备入手,结合硬件实践,同时多阅读内核源码(如 `drivers/char/` 目录下的示例)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值