河曲智叟 发表于 2024-7-28 03:41:30

Linux字符设备驱动-详解与实操:驱动架构、设备树、Pinctrl子系统和GPIO子

怎样编写一个驱动程序:

(1)确定主设备号
(2)定义自己的file_operations结构体:
                        包含对应的open(drv_open)/read(drv_read)等设备操作函数,必要到内核中去注册
(3)实现对应的open/read/write等函数,填入file_operations结构体
(4)把file_operations结构体告诉内核:注册驱动程序
                        要把file_operations结构体注册到内核中,应用程序才气调用对应的操作函数
(5)谁来注册驱动程序啊?得由一个入口函数:安装设备驱动程序时,就会去调用这个入口函数
                        用register_chrdev(major(设备号),file_operations)
                        注册的设备放入chrdevs[ ]数组里记录
(6)有入口函数就应该有出口函数:卸载驱动程序时,会去调用这个出口函数
                        用unregister_chardev(    )
                        从chrdevs[ ]数组中去掉该设备
(7)其他美满“提供设备信息,自动创建立备节点
                        设备节点创建:class_create、device_create
每一步的代码详解

驱动框架:最简朴的驱动程序-单个LED

1、字符设备名字在注册设备号时指定:“100ask_led”
https://img-blog.csdnimg.cn/direct/6f74ae57c2db400dbbf68dea058ab72c.png
字符设备驱动注册
register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
        第一个参数:主设备号,0由系统分配
        第二个参数:字符设备名字
        第三个参数:要注册的file_operation结构体
返回值:major为0时,返回一个自动分配的设备号
https://img-blog.csdnimg.cn/direct/03fab7c902ca4ae39f32a6dc527a0ad1.png
设备号分配为240
2、类和设备节点创建
类:同一个类别的设备,比如led中的led0、led1
设备节点:具体有几个设备,通过对具体的设备节点操作
每个设备节点都是一个文件,对设备的操作也是对文件的读取和写入
https://img-blog.csdnimg.cn/direct/cff21102a2c345fd980e4f392d4fdc47.png
(1)设备类创建:
class_create(struct module *owner, const char *name)        
        第一个参数:拥有这个类的指针,‘THIS_MODULE’宏,代表当前加载的模块
        第二参数:设备类名字。该名字位于/sys/class目录下。
是udev等用户空间工具用来识别和操作设备的关键标识。
https://img-blog.csdnimg.cn/direct/71bd73579d3148489d342ecbbd0a78a0.png
sys/class中可以查看已经创建的类
(2)设备节点创建:
device_create(struct class *class, struct device *parent,
     dev_t devt, void *drvdata, const char *fmt, ...)
                第一个参数:设备类的指针
                第二个参数:父设备的指针,一样寻常为NULL
                第三个参数:设备类型和编号的组成,MKDEV宏生成,代表设备的主设备号和次设备号
                第四个参数:用于通报数据,可设为NULL。
                第五个参数:设备节点的名字
https://img-blog.csdnimg.cn/direct/8969ef951d69456285cbf6a70fe306db.png
/dev/中可查看创建的设备
3、字符设备驱动、设备类、设备节点的销毁
https://img-blog.csdnimg.cn/direct/6a2992f4fa7b43d2ae4cf07d057bf2ea.png
(1)设备节点销毁
device_destroy():第一个参数,设备类指针,哪个设备类。第二个参数:MKDEV宏,指定设备的主设备号和次设备号
(2)设备类销毁
class_destroy():第一个参数,设备类指针。
(3)设备驱动销毁
unregister_chrdev():第一个参数,要销毁的主设备号,第二个参数,字符设备名字
4、寄存器变量:
(1)寄存器变量定义
https://img-blog.csdnimg.cn/direct/f0526f66d065452191c08c7d91f3c312.png
volatile防止编译器优化
定义的变量为指针(地址),赋值的值就是指向一个地址
(2)寄存器变量映射
https://img-blog.csdnimg.cn/direct/abd4c673f8534e6ca548a72b8eb2deec.png
映射到实际的地址空间中
ioremap():
        第一个参数:必要映射的物理地址起始点
        第二个参数:要映射的内存区域大小,单元字节
(3)对寄存器的操作
https://img-blog.csdnimg.cn/direct/fd6461bbb6da46c084d75119ec49dacf.png
https://img-blog.csdnimg.cn/direct/92d50b9b57c0431abae6596d3e382e70.png
https://img-blog.csdnimg.cn/direct/dd618c2986584a8c8ce5519bb3d1056f.png
5、file_operation函数
(1)结构体成员初始化
https://img-blog.csdnimg.cn/direct/824c6304da574e7aba83c7d883b2720e.png
操作符(.)用于访问结构体的成员,.fb_fillrect = cfb_fillrect这行代码是在初始化一个结构体时使用的语法,这种语法特别用于结构体的直接初始化。
在C语言中,当你使用一个函数的名称而不加任何括号时,你实际上是在引用那个函数的地址。(即函数名作为地址使用)

结构体在内核中的定义:
struct file_operations {
    struct module *owner;
    ssize_t            (*write)(struct file *, const char __user *, size_t, loff_t *);
    int                 (*open)(struct inode *, struct file *);
    // 其他成员...
};
(2)成员变量初始化的函数:
https://img-blog.csdnimg.cn/direct/f0ade7c854714904bb3b44869b2cefd9.png

https://img-blog.csdnimg.cn/direct/4b4adba3335f4ae6b4be5f4acf84c2a1.png
a、设备打开操作函数
led_open():
        第一个参数:指向‘inode’结构体
                              ‘inode’中记录了文件数据自己外的所有信息,比如文件权限、文件大小等
                              ‘inode’结构体可以获取主、次设备号
                               通过‘inode’的‘i_cdev’字段,驱动可以访问与设备相关联的‘cdev’结构,该结构                                 包含设备的核心信息和操作
        第二个参数:指向‘struct file’结构体的指针,表示用户打开的文件形貌符。
                              该结构体是用户程序与内核之间的关键接口。
b、写操作函数:
led_write():
        第一个参数:指向‘struct file’结构体的指针,表示打开的文件对象
                                此中的‘private_data’字段可用来存储特定于设备的数据
        第二个参数:要写入设备的数据缓冲区
        第三个参数:buf的数据字节数
        第四个参数:文件当前位置的偏移量(不支持寻址的设备不重要)
6、应用层:
https://img-blog.csdnimg.cn/direct/31c80ff5bd5a43d68cb5af7dc227dd73.png
argc:通报给程序的参数总数
argv:指针数组,每个元素指向一个字符数组的指针(字符串)
https://img-blog.csdnimg.cn/direct/a9b102dcb6c74863beb450f8dc45f996.png
argv:/dev/myled  设备节点名字
fd:文件形貌符,整数,标识要写入的文件或设备
https://img-blog.csdnimg.cn/direct/2c055f6a868b402a8f1421ed69a39038.png
argv:on
fd:文件形貌符
&status:指向数据缓冲区的指针
Count:要写入的字节数,指定‘buf’中有多少数据
驱动代码:led_drv.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include <asm/io.h>


static int major;
static struct class *led_class;

/* registers */
//IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3地址:0x02290000 + 0x14
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;

//GPIO5_GDIR地址:0x020AC004
static volatile unsigned int *GPIO5_GDIR;

//GPIO5_DR地址:0x020AC000
static volatile unsigned int *GPIO5_DR;

static ssize_t led_write(struct file *filp, const char __user *buf,
                       size_t count, loff_t *ppos)
{
        char val;
        int ret;
        /* copy_from_user : get data from app */
        ret = copy_from_user(&val, buf, 1);
        /* to set gpio register: out 1/0 */
        if(val)
        {
                /* set gpio to let led on*/
                *GPIO5_DR &= ~(1<<3);                                //输出低电平,根据原理图低电平点亮
        }
        else
        {
                /* set gpio to let led off*/
                *GPIO5_DR |= (1<<3);                //输出高电平
        }
        return 1;
}


static int led_open(struct inode *inode, struct file *filp)
{
        /* enable gpio5
       * configure gpio5_io3 as gpio
       * configure gpio5_io3 as output
       */
        *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf;   //先清零低4位
        *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x05;   //低4位配置101

        *GPIO5_GDIR |= (1<<3);
       
        return 0;
}


static struct file_operations led_fops = {
        .owner                = THIS_MODULE,
        .write                = led_write,
        .open                = led_open,
};
/* 入口函数 */
static int __init led_init(void)
{
        printk("%s %s %d/n", __FILE__, __FUNCTION__, __LINE__);

        /* 加载字符驱动 */
        major = register_chrdev(0, "100ask_led", &led_fops);

        /* ioremap */
        //IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3地址:0x02290000 + 0x14
        IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);   //映射4个字节

        //GPIO5_GDIR地址:0x020AC004
        GPIO5_GDIR = ioremap(0x020AC004, 4);

        //GPIO5_DR地址:0x020AC000
        GPIO5_DR = ioremap(0x020AC000, 4);

        /* 创建设备节点 */
        led_class = class_create(THIS_MODULE, "myled");                     //要在THIS_MODULE下创建类
        device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled0");
        //device_create(led_class, NULL, MKDEV(major, 1), NULL, "myled1");
               
        return 0;
}

static void __exit led_exit(void)
{
        /* iounmap */
        iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
        iounmap(GPIO5_GDIR);
        iounmap(GPIO5_DR);
       
        /* 销毁设备节点 */
        device_destroy(led_class, MKDEV(major, 0));
        class_destroy(led_class);
       
        /* 销毁字符设备驱动 */
        unregister_chrdev(major, "100ask_led");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL"); 应用代码:ledtest.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>


// ledtest /dev/myled0 on
// ledtest /dev/myled0 off

int main(int argc, char **argv)
{
        int fd;
        char status = 0;

        if(argc != 3)

        {
                printf("Usage: %s <dev> <on/off>\n", argv);
                printf("   eg: %s /dev/myled0 on\n", argv);
                printf("   eg: %s /dev/myled0 off\n", argv);
                return -1;
        }
       
        //open
        fd = open(argv, O_RDWR);
        if (fd < 0)
        {
                printf("can not open %s \n", argv);
        }

       
        //write
        if(strcmp(argv, "on")==0)
        {
                status = 1;
        }

        write(fd, &status, 1);
        return 0;
}
多个按键:

要对GPIO5_1和GPIO4_14配置(key1和key2)

1、定义GPIO通用设置的结构体(按照手册中寄存器的位置)
https://img-blog.csdnimg.cn/direct/8cb4c7cdb01540c1892ef150ffaae801.png
(1)GPIO_DR:Data register                    
数据寄存器(读取或写入GPIO引脚当前状态)
(2)GPIO_GDIR:GPIO direction             
方向寄存器(配置GPIO输入或输出)
(3)GPIO_PSR:Pad sample register        
 读取GPIO引脚当前状态(高电平或低电平) 
(4)GPIO_ICR1, GPIO_ICR2:Interrupt control registers
终端控制寄存器(配置GPIO引脚终端触发方式和其他中断相关设置)
(5)GPIO_EDGE_SEL:Edge select register
边沿选择寄存器(配置GPIO引脚的边沿触发类型)
(6)GPIO_IMR:Interrupt mask register
中断屏蔽寄存器(配置哪些GPIO引脚中断应该被屏蔽)
(7)GPIO_ISR:Interrupt status register
中断状态寄存器(表现当前GPIO引脚的中断状态)
2、配置过程:
(1)定义要寄存器变量(要修改的寄存器)
GPIO时钟使能、GPIO模式配置、GPIO模式下引脚配置
https://img-blog.csdnimg.cn/direct/0a85cd8818bc4124b53231f6bd4c235f.png
(2)手册中找到相应的地址,重映射
https://img-blog.csdnimg.cn/direct/76cfb0eba03c48c1abd1ee4222dde812.png
CCM_CCGR1中30-31为默认保留位,默认使能GPIO5
CCM_CCGR3中12-13位为gpio_4的时钟配置
https://img-blog.csdnimg.cn/direct/a213baa5b9924daf8fc5b72f4c8ad9d3.png

https://img-blog.csdnimg.cn/direct/7111fb5144524f59910adfe7d6671d92.png
https://img-blog.csdnimg.cn/direct/c702ec0a8d0546abb5fd49515cae52d7.png

https://img-blog.csdnimg.cn/direct/add1d3fa0cef456782ae8a35075fd6b1.png
对GPIO各寄存器的映射(按照地址顺序下来,只必要找到首个寄存器GPIOxx_DR的地址就好)
https://img-blog.csdnimg.cn/direct/b4774467029d4611b23d348d91e8d6c7.png
(3)给寄存器赋值
https://img-blog.csdnimg.cn/direct/856bfdc215784ef79bec78e6d37d8c39.png
时钟配置11,第30、31位:(3<<30),对gpio的访问,从结构体中找到对应的成员变量即可
https://img-blog.csdnimg.cn/direct/ab1fb7f1f360435aaeb5fb1d73fbb115.png

https://img-blog.csdnimg.cn/direct/062d67d2374144c2a370dbb864314c1b.png

https://img-blog.csdnimg.cn/direct/c3b8fb0718e4416a97c7493330e67b27.png
Linux内核中因为MMU的存在不能直接访问物理地址,必要通过ioremap把假造地址映射到对应的物理地址,我们对假造地址的访问,会自动修改到对应的物理地址。而结构体寄存器会映射整块对应大小的内存,里面的假造地址和物理地址也是一一对应的关系
3、怎样指定操作哪个设备
(1)通过传参which,指定该类下要操作的设备
init举行寄存器的初始化,read实现Key状态的读取(状态寄存器的读取)
CCM_CCGR1若未有值(未映射),则先完成寄存器的映射。
https://img-blog.csdnimg.cn/direct/a414c7a5e484430daa0770e9d56bb68a.png

https://img-blog.csdnimg.cn/direct/583cac6744d448ca89d41d6a711569a5.png

https://img-blog.csdnimg.cn/direct/d7cce30f3c6f4ed188976e980f72f4f6.png
(2)operation操作函数中,获取要操作的次设备号,调用init或read函数
https://img-blog.csdnimg.cn/direct/d12d425fbe8141c680bc4bce74187c11.png
从inode设备节点中,获取次设备号,在调用操作函数
若无inode,则先从file结构体中获取inode结构体,再从中获取次设备号。
(3)设备的创建和销毁:
创建
https://img-blog.csdnimg.cn/direct/f1d61ac0461c47799cd522a1d0723797.png
销毁
https://img-blog.csdnimg.cn/direct/d979fbd077294981a5bd04336d015ec7.png
新字符设备注册方式:

cdev结构体无论传统的还是新的注册方式都必要,传统会自动创建和注册cdev结构,新的(即设备模子)必要手动分配和初始化cdev结构,然后将其注册到内核中可以或许手动配置注册cdev,更加灵活和具备可操作性。
相比与传统的register_chrdev(),新的register_chrdev_region()、alloc_chrdev_region优缺点对比:


[*]提供对主设备和次设备号的精确控制,适合管理大量设备实例。(传统方式只能管理主设备号)
[*]分离设备号注册和设备操作设置,在设备号分配乐成后再举行设备操作的注册。(传统方式会将设备操作一起注册)
[*]更好地与当代内核的设备模子(如cdev)集成。(传统方式中不会单独对cdev设置,会自动注册cdev)
1、cdev字符设备结构
https://img-blog.csdnimg.cn/direct/71eeec3f4fa44df68e3b8579dfd7e588.png
有两个最重要的成员变量


[*]ops:设备文件操作函数指针
[*]dev:设备号
2、新字符设备结构注册方式
定义一个设备结构体来记录信息(只是为了方便记录和设置信息,非内审定义,是用户自己定义的)
https://img-blog.csdnimg.cn/direct/5f8d65aa45094127b8c889205f95e039.png
https://img-blog.csdnimg.cn/direct/030946bdefc2462eb040677944acec50.png
(1)创建立备号
https://img-blog.csdnimg.cn/direct/1e459b0c8e2f447197d4d59ba99c1a93.png
若事先定义了设备号


[*]将设备号赋值给devid(dev_t类型)
register_chrdev_region(dev_t from, unsigned count, const char *name);
        Linux内核中静态分配一个或多个一连的设备号给字符设备
        第一个参数:要注册的设备号的起始值,包含主次设备号
        第二个参数:要注册的设备号的数量(若支持多个次设备号)
        第三个参数:与设备号范围关联的名称,通常与驱动程序名字雷同
若没有定义


[*]则必要申请设备号
[*]申请乐成后,把申请到的主次设备号记录回设备结构体,方便后续调用。
alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
        Linux内核中动态分配一个或多个一连的设备号给字符设备。
        相比于 register_chrdev_region 提供了更大的灵活性,不必要指定具体的起始主设备号,而是由内核动态分配,确保不会与现有的设备号辩论。
        第一个参数:指向 dev_t 类型变量的指针,该变量在函数实行乐成后将包含分配的设备号。
        第二个参数:请求分配的次设备号,通常设为0,表示从0开始分配。
        第三个参数:必要分配的一连设备号的数量(必要的设备实例数量)
        第四个参数:与设备号关联的名称。
(2)初始化cdev
https://img-blog.csdnimg.cn/direct/5ac1fdae937942ea8c6e7e60a8d6fbc1.png
cdev_init(struct cdev *cdev, const struct file_operations *fops);
        主要任务是将字符设备结构体与对应的文件操作关联起来
        第一个参数:指向字符设备结构体的指针
        第二个参数:指向文件操作结构体的指针
(3)将cdev添加到内核的设备模子中
https://img-blog.csdnimg.cn/direct/c537c690e3fa42198607468c1c5b4f63.png
cdev_add(struct cdev *dev, dev_t num, unsigned int count);
        初始化完成后,cdev_add在系统中注册一个字符设备,使其处于激活状态,从而答应用户空间通过设备文件访问设备。这个过程包罗将 cdev 结构链接到内核的设备表中。
        第一个参数:已经初始化的'cdev'结构体的指针。
        第二个参数:设备号,包罗主设备号和起始的次设备号。
        第三个参数:表示与此字符设备相关的次设备号的数量。
在cdev_add是将 cdev 结构体与指定的设备号关联起来,并将其注册到内核中,使之成为系统的一部分。这个关联过程并不涉及在 cdev 结构体内部修改或设置一个 dev 成员,而是在内核的设备管理结构中建立关联。但这个设备号不是作为 cdev 结构的一部分直接存储的。
(4)创建类和设备
https://img-blog.csdnimg.cn/direct/e0e28470ef2e445096ab7d6186b73ddd.png
https://img-blog.csdnimg.cn/direct/dca779fe57f142439cf5a21192e675ae.png
如果设备驱动必要管理多个设备实例,大概想在系统中更高效地使用设备号资源,使用register_chrdev_region()通常是更好的选择。这不仅可以避免次设备号的浪费,还可以保证设备号分配的明确性和系统资源的合理利用。相反,如果驱动只管理单一设备或对设备号管理的精确性要求不高,register_chrdev()可能仍然足够使用。但建议养成设备号合理管理和系统资源最优化的好习惯。
设备树:

在设备树中指定硬件资源的形貌(对LED操作)

设备树加载后,可以进入/proc/device-tree/目录下查看根节点下的节点。
1、设备树dts文件:添加led节点
https://img-blog.csdnimg.cn/direct/0c47ea8285fc4f5db66a908e264bcaab.png
设备树中的节点由一堆属性组成,不同设备必要的属性不同
(1)#address-cells、#size-cells属性:
                cell指一个32位的数值,
                #address-cells决定子节点reg属性中地址信息所占用的字长(32字节)
                #size-cells决定子节点reg属性中长度信息所占的字长
https://img-blog.csdnimg.cn/direct/4e9a57536d324c18af863ea486ed91c2.png
该节点用1个数表示地址,1个数表示大小
(2)compatible属性:
                值为字符串列表,可能有多个驱动支持它(多个字符串)
                将设备和驱动绑定起来,通过compatible中的名字找到要加载probe的驱动模块
(3)status属性:
设备状态
                “okay”:      表明设备是可操作的
                “disabled”:表示当前设备不可操作(禁用掉),但在将来可变为可操作的
                “fail”:         表明设备不可操作,检测到错误,不大可能变为可操作
                “fail-sss”:   含义与“fail”雷同,背面的sss部分是检测到的错误内容
(4)reg属性:
                reg属性的值一样寻常是(address,length)对。
                一样寻常用于形貌设备地址空间资源信息,一样寻常是某个外设的寄存器地址范围信息。
2、驱动程序:获取硬件资源(获取设备树中的属性数据)
led_init()函数中
https://img-blog.csdnimg.cn/direct/e38a02dd5fa147d88bb614e58170d9a8.png
(1)获取设备节点:alphaled
https://img-blog.csdnimg.cn/direct/f680a1d18a734a6eb77c05c825dded29.png
获取设备节点后,才气获取属性内容
of_find_node_by_path():
用于操作设备树的函数,主要作用是根据给定的设备树路径来查找并返回对应的设备树节点。
                参数:设备树中的一个路径,根节点开始的绝对路径。
                dtsled.nd为device_node结构体的指针
(2)获取设备节点属性,包罗reg
获取compatible属性内容
https://img-blog.csdnimg.cn/direct/7bdac82d66434aa687a5b5331dd4729a.png
获取status属性内容
https://img-blog.csdnimg.cn/direct/eba3bf5b8faa435a90fccd0514e8969e.png
获取reg属性内容,必须要先获取设备节点dtsled.nd
https://img-blog.csdnimg.cn/direct/5f86f7cd0fd84c6d945e7b8f38663e9c.png
of_property_read_u32_array( );
从设备节点中读取一个包含多个'u32'元素的属性到一个用户提供的数组中。
                第一个参数:指向设备树节点的指针
                第二个参数:要读取的设备树属性的名称。
                第三个参数:用户提供的数组,用于存储读取到的数据。
                第四个参数:希望读取到的元素的数量。
(3)寄存器地址映射
https://img-blog.csdnimg.cn/direct/0d878ac4d2b144fdaf8ed6ea6760c113.png
其他对寄存器的操作一样

Pinctrl和GPIO子系统:

省去了对硬件的寄存器的复杂配置(对LED操作)

Pinctrl:设置pin的复用和电气属性
GPIO子系统:若pinctcl子系统将一个pin复用为GPIO的话,接下里就会用到gpio子系统了
用于初始化gpio并提供相应的API函数,对gpio操作
设备树:
1、pinctrl节点:
在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点
https://img-blog.csdnimg.cn/direct/ac7e459ce89d4d50abbf4448d4005087.png
(1)label:node-name@unit-address
                label:方便访问节点,可以通过&label来访问这个节点。
                node-name:节点名字。
                unit-address:设备的地址或寄存器首地址,若节点没有地址或寄存器,可以不要。
(2)MX6UL_PAD_GPIO1_IO03__GPIO1_IO03
                MX6UL                   - 表示芯片或处置惩罚器的型号
                PAD_GPIO1_IO03   - 引脚的物理位置或名称
                GPIO1_IO03            - 引脚的复勤奋能,复用为GPIO1的第03号引脚
(3)0x10B0:引脚配置寄存器的值,定义了引脚的各种电气属性。
                如上拉/下拉电阻、驱动能力、速度等
                在这一步已经完成了引脚的配置
2、LED设备节点
在根节点“/”下创建LED灯节点,节点名为“gpioled”
https://img-blog.csdnimg.cn/direct/cdd11aaf93ac48ce8aab87c86095a21d.png
(1)pinctrl-0属性,6行:设置LED灯所使用的PIN对应的pinctrl节点。
要检测PIN是否被其他外设使用,要将其关掉(status="disabled"等)。
(2)led-gpio属性,7行:指定LED灯所使用的GPIO,GPIO1的IO03,低电平有用。
驱动程序中会获取led-gpio属性内容得到GPIO编号,因为gpio子系统的API操作函数必要GPIO编号。
驱动程序:
1、设备节点结构体定义:
https://img-blog.csdnimg.cn/direct/28bd73723b3c4d92bcb8a2dc7cd73a72.png
https://img-blog.csdnimg.cn/direct/936cb4c57a684c0ea53a3fb21b15263e.png
2、gpio子系统控制引脚
(1)获取设备节点:
https://img-blog.csdnimg.cn/direct/e7ad075d5bcb41e786a8127d103cf501.png
(2)获取设备树中的gpio属性,得到LED所使用的GPIO编号
https://img-blog.csdnimg.cn/direct/6e75bc727d6842a78bef833f518859ab.png
of_get_named_gpio():
读取设备树中定义的GPIO属性,将其转换为GPIO编号。通过此编号可以在驱动中请求、配置和控制GPIO。
                第一个参数:指向‘device_node’的指针,表示当前设备的设备树节点
                第二个参数:节点中GPIO属性的名字。
                第三个参数:索引,表示有多个GPIO同一属性下定义是,应选择哪一个。
(3)设置gpio为输出,并且输出高电平,默认关闭LED灯
(在对gpio设置前要先用gpio_request()申请这个gpio的控制权,确保资源不辩论)
https://img-blog.csdnimg.cn/direct/02a657fe875841cfadac15efa7d2162f.png
https://img-blog.csdnimg.cn/direct/07919734d14e4c57ad9e2d43a5deca2e.png
gpio_direction_output():
Linux内核中提供的操作GPIO的函数,设置指定引脚为输出模式。
                第一个参数:GPIO引脚的编号,标识要设置哪个引脚
                第二个参数:设置GPIO引脚的初始电平,“1”高电平
(4)设置gpio的值
https://img-blog.csdnimg.cn/direct/95f85fe70e5241659df3861012922f76.png
dev是指向gpioled_dev结构体的指针
gpio_set_value():
设置指定GPIO引脚的输出电平。
                第一个参数:GPIO引脚编号。
                第二个参数:设置GPIO引脚的初始电平。

Platform设备驱动(无设备树程序):

驱动的分层和分离。

将驱动中的硬件部分分离出来为platform_device,保留的驱动为platform_driver,通过总线匹配注册probe。设备树实质上就是platform_device这部分,替代了原先必要每次编写和加载的.c文件,每次自动加载和生成platform_device。

platform结构体详解、定义和框架可参考上篇博客:
Linux驱动进化:传统模子、设备总线驱动模子、设备树-CSDN博客
1、leddevice.c硬件资源(paltform_device):
(1)resource
https://img-blog.csdnimg.cn/direct/5c52b9a6c5574d5fa9b61381c6b7479c.png
https://img-blog.csdnimg.cn/direct/eb06d456d8854f68b850a4f2a5bceb39.png
https://img-blog.csdnimg.cn/direct/4f03af74f6c04b0fa6739a28784cda34.png
struct resource:
                start:资源的起始地址
                end:资源的竣事地址
                name:资源的名称
                flags:资源类型和属性标志
(2)platform_device结构体
https://img-blog.csdnimg.cn/direct/1d19ba9d5d1144c9a50752c94346ae3e.png
名字为“imx6ul-led”以和platform_deriver配对
(3)注册platform_device结构体
便于通过总线bus和platform_driver配对
https://img-blog.csdnimg.cn/direct/dc304ab6ce3a4acf888fcd370f5aea93.png
2、platform_driver:
(1)从platform_device中获取资源(led_probe函数)
https://img-blog.csdnimg.cn/direct/bbdc4d60d08344b18cd0c8f2c3e57e22.png
https://img-blog.csdnimg.cn/direct/2f0d0faed7cb42e1addd40c2ecc9567e.png
https://img-blog.csdnimg.cn/direct/d5636fdcafd44de08a16f4131bff0a52.png
platform_get_resource():
用于从平台设备中获取特定类型和序号的资源。        
                第一个参数:指向“platform_device”结构的指针,表示正在被操作的平台设备。
                第二个参数:资源类型标志,指定要获取的资源类型。IORESOURCE_MEM表示一个内                                         存类型的资源。
                第三个参数:资源的索引,设备的资源有多个,要获取哪一个。
(2)初始化LED
https://img-blog.csdnimg.cn/direct/7196da1587cc47a18045ac5d1f5d57c3.png
设置寄存器的初始值:···省略
(3)platform_driver结构体
https://img-blog.csdnimg.cn/direct/454ff985d9b3428a9a3180db81d40453.png
通过名字"imx6ul-led"和device匹配。
(4)注册platform_driver
通过bus总线和platform_driver配对后会自动调用probe函数完成资源初始化
https://img-blog.csdnimg.cn/direct/63b30b9bcc8440d2b72e7f3f4350ac5c.png
3、匹配过程:
驱动模块加载完后,到/sys/bus/platform/drivers/目录下
https://img-blog.csdnimg.cn/direct/a2cad29e73cc4edb9a26a09f0adae8a3.png
led的设备文件在/sys/bus/platform/devices/目录下
https://img-blog.csdnimg.cn/direct/6cae1b6e9dc7490ab18186897ab382f2.png
驱动和设备匹配
https://img-blog.csdnimg.cn/direct/ea98590fe469406cbcdbce20300cea6b.png

设备树下的Platform设备驱动:

前面已经实现了设备树(加载设备树时会自动生成platform_device),这里只看platform_driver
leddriver.c
1、初始化IO
(1)从设备树中获取设备节点
https://img-blog.csdnimg.cn/direct/999fb1a4be5b44e5a751cccf016df77b.png
(2)获取设备树中的gpio属性,得到LED所使用的GPIO编号
https://img-blog.csdnimg.cn/direct/287abb1041e64fc8a1fa99085000feea.png
(3) Linux 内核中用于请求一个 GPIO(通用输入输出)引脚的使用权。之后操作gpio引脚
https://img-blog.csdnimg.cn/direct/790374a599544432827ef8b6a9f347ec.png
gpio_request():
Linux内核中用于请求一个GPIO引脚的使用权
                第一个参数:要申请的GPIO引脚的编号。
                第二个参数:请求的GPIO引脚相关联的标签或名称,可自定义。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Linux字符设备驱动-详解与实操:驱动架构、设备树、Pinctrl子系统和GPIO子