首页 > 编程语言 >基于pinctrl和GPIO子系统的按键驱动程序

基于pinctrl和GPIO子系统的按键驱动程序

时间:2024-05-10 12:55:06浏览次数:22  
标签:__ 驱动程序 引脚 pinctrl key GPIO 子系统

嵌入式驱动开发中pinctrl和GPIO子系统使用频率非常高,其中pinctrl子系统主要用于复用和配置引脚,GPIO子系统用于设置GPIO的输入/输出,向引脚写入数据或者从引脚读取数据。一个引脚可以复用为多种不同的功能,因此要使用GPIO子系统首先要先把引脚配置为GPIO功能。下面将分为两部分记录如何基于pinctrl子系统和GPIO子系统编写按键驱动程序。

本实验基于imx6ull开发板

1、向设备树添加内容
(1)使用pinctrl子系统复用引脚为GPIO功能
基于pinctrl和GPIO子系统进行驱动开发本质上还是基于设备树的总线设备驱动模型,但是此时设备树中节点代码的格式要遵守对应芯片厂家提供的pinctrl和GPIO子系统的格式,这样他们的软件才能正确运行。
pinctrl子系统对应的引脚的复用、配置,因此它必然要获取引脚相关的寄存器信息和配置信息,因此要在设备树中pinctrl对应的节点下添加子节点用于描述要用到的引脚。

使用NXP提供的i.MX Pins Tool v6工具,可以方便的得到配置pinctrl子系统设备树节点的代码。
在前面的按键驱动笔记中已经给出了按键的原理图,根据引脚名去芯片参考手册可以查到引脚属于GPIO1_IO18,打开i.MX Pins Tool v6工具后,在GPIO1组中找到IO18,并点击选中(打勾),如下图所示。

在图中的右侧可以看到右侧iomux节点下有一个子节点

点击查看代码
BOARD_InitPins: BOARD_InitPinsGrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6UL_PAD_UART1_CTS_B__GPIO1_IO18          0x000010B0
            >;
        };
把这个子节点代码复制到我们的设备树下的iomuxc节点下,并改一下名称就可以。iomux节点可以理解为就是pinctrl子系统对应的设备树节点,它会到这里来寻找引脚的复用、配置信息,并且它的格式要和它的代码相适应。 将代码复制到设备树中iomuxc节点下并改名后如下:
点击查看代码
pinctrl_key: keyGrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6UL_PAD_UART1_CTS_B__GPIO1_IO18          0x000010B0
            >;
        };

在上述代码中,fsl,pins是芯片提供的pinctrl子系统规定的属性名MX6UL_PAD_UART1_CTS_B__GPIO1_IO18表示了UART1_CTS_B引脚对应的一组寄存器,它是一个宏定义,猜测它应该包含了引脚对应的复用寄存器、配置寄存器地址。具体的分析过程参考正点原子驱动开发文档中的pinctrl和gpio子系统章节。

(2)使用GPIO子系统
向iomux添加子节点是为了使用引脚的相关寄存器信息,接下来还要在设备树的根节点下添加设备对应的子节点(因为我们刚才在iomux节点下添加的子节点并不会被转换为platform_device),并引用iomux节点下的内容。这个过程与基于设备树的总线设备模型是类似,只不过我们没有直接在设备节点下写上寄存器信息,而是引用了定义好的节点的信息。
下面是在根节点下创建设备节点,并引用前面在iomux下添加的子节点

点击查看代码
pinctrl_gpio_mykey {
			#address-cells = <1>;
			#size-cells = <1>;
			compatible = "mykey_driver";	/*用于驱动匹配*/
			pinctrl-names = "default";	/*pinctrl-前缀的属性是给pinctrl子系统使用的,-gpio后缀的属性是给GPIO子系统使用的*/
			pinctrl-0 = <&pinctrl_key>;
			key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;	/*这里设置了低电平有效,因此引脚物理状态和逻辑值相反*/
			status = "okay";
		};

在上面代码中,pinctrl-names = "default";表示它只有一个默认状态default,对应这个状态pinctrl子系统所使用的节点信息由pinctrl-0指定,而pinctrl-0又引用了pinctrl_key节点,因此最终pinctrl子系统使用pinctrl_key节点中定义的属性来进行寄存器的设置从而实现引脚的复用、配置。
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>表示使用gpio1组的IO18,并且低电平有效,其中-gpio后缀是必须的,因此GPIO子系统会根据这个后缀找到对应的属性,&gpio1表示使用GPIO1组,它是一个GPIO控制器,里面包含了这一组GPIO的寄存器地址信息,根据18就可以找到GPIO1_IO18的寄存器地址信息。
status = "okay";就是使能这个节点,让它能够被内核转换为platform_device,如果设置为disable,就是屏蔽这个节点,相当于注释掉了

添加好设备树节点信息后,在设备树文件中全局搜索GPIO1_IO18和gpio1 18,查看是否有其它节点也使用了这个引脚,如果有就注释掉,否则会导致引脚冲突。

2、基于pinctrl和GPIO子系统的驱动程序代码编写
按照规定的格式写好设备树文件后,在驱动代码中并不需要调用pinctrl子系统,因为在内核加载设备树文件时,就已经根据设备树中节点的信息转换为platform_device并初始化好引脚的复用、配置了。我们要做的其实就是调用GPIO子系统提供的函数对GPIO进行方向设置、读写GPIO即可,下面是代码

点击查看代码
/*
本次实验是基于pinctrl子系统和gpio子系统进行按键驱动程序编写
platform_device由设备树生成并由pinctrl子系统所使用的引脚进行复用为gpio
在该文件中实现:
(1)定义platform_drvier结构体变量并填充内容
    (a)实现probe函数:从设备树获取gpio引脚信息、注册驱动、创建设备类class,创建设备节点
    (b)实现remove函数:释放gpio引脚、删除设备节点、删除设备类class、删除节点
(2)实现file_operations结构体的内容,并注册驱动
    (a)实现open、read、write、release函数
*/

#include "asm/uaccess.h"
#include "linux/export.h"
#include "linux/mod_devicetable.h"
#include <linux/module.h>
#include <linux/platform_device.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>


static int major;  //主设备号
static struct class *key_class;
static struct gpio_desc *key_gpio;  //在使用gpdio子系统时,需要通过gpio-desc这个结构体来获取引脚


/*实现file_operations结构体
实现open read 和close函数
*/

ssize_t key_read(struct file *file, char __user *user, size_t size, loff_t *offset)
{   
    /*读取按键状态并传递到用户空间*/
    char state;    
    int err;
    // printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__ );

    /*
    在原理图中,按键按下时是低电平,但是因为设备树中设置了低电平有效,所以引脚物理状态和gpiod_get_value的返回值相反
    因此按键按下时返回的是1,不按时返回0
    */
    state = gpiod_get_value(key_gpio);  //读取GPIO状态
    // state = ~state; //直接取反,并不会从1变成0。通过输出发现,不按下时返回0,取反后输出255,按下时返回1,取反后是254因为1的二进制是0b00000001,取反后是0b11111110,对应10进制就是254
    state ^= (1);   //使用异或取反
    printk("state = %d\n", state);
    
    err = copy_to_user(user, &state, 1);

    return 1;   //读取按键的状态,只需要一个字节的char字符表示就行
}

int key_open(struct inode *node, struct file *file)
{
    /*应用程序打开按键对应的设备文件时会调用这个函数,此时相当于要开始使用设备了,那么需要执行一些初始化操作
        配置所用的引脚为输入
    */
    printk("%s %s %d",__FILE__, __FUNCTION__, __LINE__ );

    gpiod_direction_input(key_gpio);    //设置GPIO为输入

    return 0;
}

int key_close(struct inode *node, struct file *file)
{
    /*可以将引脚恢复为高电平输出状态*/
    printk("%s %s %d",__FILE__, __FUNCTION__, __LINE__ );
    gpiod_direction_output(key_gpio, 0);    //设置gpio为输出,并默认输出高电平

    return 0;
}

struct file_operations key_oprs = {
    .owner = THIS_MODULE,
    .open = key_open,
    .release = key_close,
    .read = key_read,

};




/*实现platform_driver结构体,主要填充两个函数
(1)probe,设备和驱动相互匹配时调用
(2)remove函数 卸载设备时调用,如果直接卸载驱动,那么每个匹配的设备都会执行这个函数一次
*/

int key_probe(struct platform_device *pdev)
{
    /*设备和驱动匹配时执行,因此在这里要注册设备的操作函数,创建类,创建设备,要使用GPIO子系统获取引脚*/
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    /*获取按键对应的gpio*/
    key_gpio = gpiod_get(&pdev->dev, "key", 0);
    if (IS_ERR(key_gpio)) {
		dev_err(&pdev->dev, "Failed to get GPIO for key\n");
		return PTR_ERR(key_gpio);
	}

    /*向内核注册file_operations结构体*/
    major = register_chrdev(0, "key_driver", &key_oprs);

    //创建class
    key_class = class_create(THIS_MODULE, "key_class");
    if (IS_ERR(key_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "key_driver");
		gpiod_put(key_gpio);
		return PTR_ERR(key_class);
	}

    /*创建设备节点*/
    device_create(key_class, NULL, MKDEV(major, 0), NULL, "key_0");


    return 0;
}
int key_remove(struct platform_device *pdev)
{
    /*与probe函数的操作相反*/
    
    device_destroy(key_class, MKDEV(major, 0));

    class_destroy(key_class);

    unregister_chrdev(major, "key_driver");

    gpiod_put(key_gpio);    //释放引脚

    return 0;
}

/*创建匹配表,驱动能够与设备树中下列节点匹配*/
static const struct of_device_id keys_match_table[] = {
    {
        .compatible = "mykey_driver"
    }
};

struct platform_driver key_drv = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "mykey",        //driver中的.name字段必须加上,不然运行时报错,暂时不懂得为什么
        .of_match_table = keys_match_table,
    }
};

static int __init key_drv_init(void)
{

    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    err = platform_driver_register(&key_drv);
    printk("err = %d", err);
    return err;
}

static void __exit key_drv_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    platform_driver_unregister(&key_drv);
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");

在代码编写过程遇到的一些细节问题都已经写在注释里了,这里再总结一下:
(1)~是按位取反,如果只是把某一位或某几位取反,应该用异或^;
(2)在设备树中定义gpio1 18 是低电平有效,因此gpiod_get_value函数读取到的GPIO逻辑值和真实的引脚物理值相反;
(3)copy_to_user(user, &state, 1)中的state在定义时不需要声明为volatile,否则会产生警告,而且传入的是地址,因此每次都是从state的内存处取值,不用担心被编译器优化导致数据与预期不符;
(4)在platform_driver的driver字段中的name字段必须赋值,否则在编译时不报错,但是驱动运行时会产生空指针错误,因为驱动和设备匹配时首先比较platform_driver中的driver中的name和platform_device中的override;
(5)在驱动中读取按键状态时,不用防抖,驱动应该只提供读取功能,防抖应该是在应用程序中实现。

应用程序代码如下,在里面调用了按键驱动和led驱动,使用按键实现灯的亮灭操作

点击查看代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char** argv)
{
    int fd_button, fd_led;
    char val;
    char state;

    /*判断参数*/
    if(argc != 3)
    {
        printf("Usage: %s <buton_dev> <led_dev>\n", argv[0]);
    }

    fd_button = open(argv[1], O_RDWR);
    fd_led = open(argv[2], O_RDWR);
    if(fd_button == -1 || fd_led == -1)
    {
        printf("can not open file %s\n", argv[1]);
        return -1;
    }

    /*读文件, 控制led灯*/
    state = 0;
    write(fd_led, &state, 1);
    while(1)
    {
        read(fd_button, &val, 1);
        if(val == 0)
        {
            usleep(10000);
            read(fd_button, &val, 1);
            if(val == 0)
            {
                state ^= (1);
            }
            write(fd_led, &state, 1);
            // printf("led state: %d", state);
            sleep(1);
        }

    }
    close(fd_button);
    close(fd_led);


    return 0;
}

至此,基于pinctrl子系统和GPIO子系统的按键驱动程序已经完成,可以看到,使用pinctrl和GPIO子系统可以简化驱动开发工作,而不用直接操作寄存器。但是不管如何,直接操作寄存器是驱动开发本质,只有掌握了本质才能用好别人的工具,要知其然,更要知其所以然。

标签:__,驱动程序,引脚,pinctrl,key,GPIO,子系统
From: https://www.cnblogs.com/starstxg/p/18183819

相关文章

  • 初探pinctrl子系统和GPIO子系统
    前言:在前面的led驱动程序和按键驱动程序中,无论是最传统的方法,还是总线设备驱动模型,还是基于设备树的总线设备驱动模型,都是直接操作寄存器的方法。驱动开发的本质确实是操作寄存器,但是一个芯片有几百个引脚,只是操作少数的几个引脚还好,如果是大量的引脚,比如LCD接口的引脚几十个,一个......
  • 学习记录+vcode+GPIO例程+正点原子 DNESP32S3 开发板教程-IDF 版
    第一个程序:UART模式和JTAG模式,配置完成不同。配置主要就是.vscode文件夹中 c_cpp_properties.json,tasks.json,launch.json,settings.json四个文件。一个想法:备份UART模式和JTAG模式的配置文件,用时直接文件替换。简单粗暴方式是.vscode文件夹替换。当然每次要选好串口、设置目标......
  • Windows备份和恢复驱动程序详解
    在进行系统重装后,驱动程序的重新安装通常是一项繁琐的任务。为了简化这一过程并降低数据丢失的风险,建议对Windows驱动程序进行备份。以下是一个指南,用于备份和恢复驱动程序。备份驱动程序启动PowerShell:首先,点击“开始”菜单,在搜索框中输入“WindowsPowerShell”,并以管理员权......
  • 基于总线设备驱动模型的按键读取驱动程序
    本次实验基于总线设备驱动模型实现按键驱动程序的编写,给上层应用程序提供检测按键是否按下的操作接口,上层应用根据按键是否按下控制led的亮灭。所以上层应用程序会同时使用led和按键的驱动接口,但是对于下层驱动而言,这二者是分离的,因此只需要专注于编写按键驱动程序就可以了。在正......
  • 设备驱动程序简介
    设备驱动程序简介概述:Linux驱动程序的核心:作为一个黑盒子,使某个特定硬件相应相应的内部编程接口,如posix。其需要隐藏体的工作细节。Linux驱动程序编写为什么简单:其是使独立于内核的其他部分而建立的,其是模块化的。驱动程序的作用驱动程序的妥协:在编写驱动程序所需要的时......
  • CentOS7安装NVIDIA GPU驱动程序和CUDA工具包
      1.查看本地环境检查GPU型号lspci|grep-invidia查看linux系统版本uname-m&&cat/etc/redhat-release禁用nouveaulsmod|grepnouveau#打开如下文件sudovim/usr/lib/modprobe.d/dist-blacklist.conf#写入以下内容blacklistnouveauoptionsnouveaumodes......
  • GPIO模式
    GPIO模式输出模式推挽输出用PMOS和NMOS协同工作电压输出为VDD和VSS芯片内部电压驱动驱动能力弱开漏输出只有NMOS工作NMOS断低通高借助外部电压驱动复用推挽输出复用开漏输出为什么复用?引脚除了输入同时还可以做片上外设(I2C、SPI等)输入模式上拉输入默认为高电平......
  • docker 日志驱动程序
    loggingdriver说明Docker中的日志驱动程序(loggingdriver)用于控制容器的日志记录方式,允许您将容器中生成的日志发送到不同的目标,如标准输出、文件、远程日志服务器等。loggingdriver类型none: 容器没有可用的日志,并且dockerlogs不返回任何输出。local: 日志以旨......
  • 微雪 esp32c3 深度睡眠和 gpio 唤醒
    当项目由电源适配器供电时,我们一般不会太关心功耗。但是,如果要使用电池为项目供电,则需要精打细算。esp32深度睡眠在深度睡眠模式下,CPU、大多数RAM和所有数字外围设备都可以关闭。从深度睡眠中出来后,芯片通过复位重新启动,并从一开始就开始执行程序。系统无法自动进入深度睡眠......
  • led驱动程序改造-基于设备树的总线驱动模型
    上一篇文章中,介绍了总线设备驱动模型,并基于总线设备驱动模型改造了led驱动程序。考虑到每一个单板所用的资源可能有所不同,以led为例,使用同一芯片的每个单板,如果每一个单板对应的led引脚不同导致需要分别定义一个对应的资源文件来描述这个引脚,并且该文件会被编译进内核,进而导致内核......