rk3576(5)之编些简单GPIO驱动

1、简介

rk3576+buildroot+设备树GPIO驱动编写。

移植了系统,总要编写一个驱动吧,之前学习驱动的时候,文档主要都是使用正点原子的,但是自己看了每一个寄存器以及相关配置,建议自己学习的人也要动手写一下,很多地方都是看着简单但是动手以后发觉并不了解的

2、GPIO寄存器

2.1、选取引脚

首先选取一个GPIO口,我选择的是Pin Name是GPIO4_A4 ,Pin是1C5, 在数据手册Rockchip_RK3576_Datasheet_V1.1-20240430.pdf 里看一下这个引脚**,**

可以看到这个引脚有很多功能,所以肯定是需要指定作为GPIO口的。

2.2、查看寄存器信息

打开RockChip官方文档Rockchip_RK3576_TRM_Part1_V1.2_20240624.pdf,Chapter 21 GPIO章节,第一个就是寄存器的地址:

这代表了寄存器GPIOx的首地址,之后就是设置偏移量和大小了

2.3、GPIO_SWPORT_DR L/H

这是GPIO的SWPORT数据寄存器,分为低16位的SWPORT_DR_L和高16位的SWPORT_DR_H,主要作用是‌配置和控制I/O端口的输出状态

输出数据控制‌

  • 低16位寄存器(SWPORT_DR_L):对应I/O端口的低16位引脚,写入的数值会直接输出到对应引脚,1'b0对应低电平,1'b1对应高电平;读取该寄存器会返回最后一次写入的值。
  • 高16位寄存器(SWPORT_DR_H):对应I/O端口的高16位引脚,功能和低16位寄存器一致,负责控制高16位引脚的输出电平。

写入权限管理‌,两个寄存器的高16位(31:16)是写入掩码位,每个位对应低16位数据寄存器的一个位:

  • 1'b0:禁止对应低16位数据位的写入操作
  • 1'b1:允许对应低16位数据位的写入操作

这个设计可以避免误写寄存器,提升操作的安全性。

这里需要知道,一共有五组GPIO。每组GPIO有32个GPIO口,然后A0-A7,B0-B7是低16位,C0-C7,D0-D7是高16位

2.4、GPIO_SWPORT_DDR L/H

这个功能和设置电平一样,只是设置输入或者输出

2.5、GPIO_INT_EN L/H

用于设置中断,其它的与上面一致

2.6、GPIO_INT_MASK L/H

中断屏蔽控制器,写入1则屏蔽对应引脚中断,写入0则允许中断触发,其它的与上面一致

2.7、GPIO_INT_TYPE L/H

配置中断触发类型:

  • 写入1'b0:对应引脚为‌电平触发‌中断,中断状态由引脚电平高低决定
  • 写入1'b1:对应引脚为‌边沿触发‌中断,中断状态由引脚电平的上升沿或下降沿触发
  • 注意:写入1时配置为边沿触发,写入0时配置为电平触发

2.8、GPIO_INT_POLARITY L/H

配置中断触发极性:

  • 1'b0:配置为低电平有效或下降沿触发中断
  • 1'b1:配置为高电平有效或上升沿触发中断
  • 触发逻辑:向某一位写入1,则该引脚中断为上升沿/高电平有效;写入0则为下降沿/低电平有效。

2.9、GPIO_INT_BOTHEDGE L/H

中断边沿触发类型

  • 0:禁用双边沿检测,中断类型由int_type_low和int_polarity_low寄存器对应位决定
  • 1:启用双边沿检测,对应I/O端口会在外部输入信号的上升沿和下降沿都触发中断

此时int_type_low和int_polarity_low的配置会被忽略

2.10、GPIO_DENOUNCE L/H

中断源的外部信号是否需要去抖,消除异常毛刺

2.11、GPIO_DECLK_DIV_EN L/H

外部信号去抖功能是否使用内部分频时钟

2.12、GPIO_DECLK_DIV_CON L/H

外部信号去抖功能使用内部分频时钟分频系数

2.13、GPIO_INT_STATUS

中断状态

2.14、GPIO_INT_RAWSTATUS

中断真实状态

2.15、GPIO_PORT_EOI_L/H

清除中断标记位

2.16、GPIO_EXT_PORT

读取IO状态

2.17、GPIO_VER_ID

2.18、GPIO_STORE_ST_L/H

写入寄存器状态

2.19、GPIO_GPIO_REG_GROUP/1/2/3_L/H

功能:控制GPIO高16位,每一位对应一个GPIO引脚:

当virtual_en=1时:当是GPIO1偏移量就是0x1000

  • 0:该GPIO由GROUP0控制(偏移量0x0000)
  • 1:该GPIO由其他GROUP控制

当virtual_en=0时:当是GPIO1中断连接至gpio_int_flag1

  • 0:该GPIO中断连接至gpio_int_flag0
  • 1:该GPIO中断连接至其他中断源。

2.20、GPIO_GPIO_VIRTUAL_EN

控制GPIO虚拟功能开关:

  • 0:禁用虚拟功能
  • 1:启用虚拟功能,支持四种操作系统

3、GRF寄存器

GPIO复用设置在Chapter 5 General Register Files (GRF)里可以知道,

3.1、TOP_IOC_GPIO4A_IOMUX_SEL

设置引脚复用

3.2、VCCIO_IOC_GPIO4A_PULL

设置上下拉

3.3、VCCIO_IOC_GPIO4A_DS

设置驱动能力

3.4、VCCIO_IOC_GPIO4A_IE

输入缓存区使用

3.5、VCCIO_IOC_GPIO4A_SMT

3.6、VCCIO_IOC_GPIO4A_PDIS

禁止上下拉

4、字符设备驱动

首先需要知道应用层是如何调用驱动程序的,

  • 应用程序->库函数->库函数通道系统调用进入内核->内核执行驱动写好的接口函数->硬件

因为应用程序运行在用户空间, 驱动运行在内核空间,当我们想要操作驱动时,是需要使用系统调用的方法来实现的,系统调用主要就是c库的open、close、write、read等。所以当我们调用C库时:

  • 应用程序调用open函数->使用C库中的open函数->open系统调用->驱动的open函数

4.1、file_operations

所以这里有一个驱动很关键的文件结构体file_operations,file_operation 就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用,内容在kernel-6.1/include/linux/fs.h

复制代码
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
			unsigned int flags);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	void (*splice_eof)(struct file *file);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
	int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
	int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
				unsigned int poll_flags);
} __randomize_layout;

比较重要的就是:

  1. owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
  2. llseek 函数用于修改文件当前的读写位置。
  3. read 函数用于读取设备文件
  4. write 函数用于向设备文件写入(发送)数据。
  5. poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
  6. unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
  7. compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上, 32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl
  8. mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
  9. open 函数用于打开设备文件。
  10. release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应
  11. fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中

4.2、驱动模块的加载和卸载

Linux 驱动有两种运行方式

  1. 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序
  2. 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用"modprobe"或者"insmod"命令加载驱动模块

在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

复制代码
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

4.3、字符设备的注册和注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。

复制代码
static inline int register_chrdev(unsigned int major,
                                    const char *name,
                                    const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major,
                                    const char *name)

register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:

  1. major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号
  2. name:设备名字,指向一串字符串
  3. fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量

unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:

  1. major: 要注销的设备对应的主设备号。
  2. name: 要注销的设备对应的设备名

还有一个是动态

4.4、设备号

设备号的申请和归还,设备号的申请有两种方式,一种是自己指定设备号,一种是向内核申请设备号,其中设备号的组成分主设备号和次设备号两部分,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面,定义如下:

复制代码
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:

复制代码
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
  • 宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可
  • 宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
  • 宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

设备号的分配分动态和静态,上面使用的就是静态的,动态的如下

动态分配设备号

复制代码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)

函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:

  1. dev:保存申请到的设备号
  2. baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
  3. count: 要申请的设备号数量。
  4. name:设备名字。

注销字符设备之后要释放掉设备号,设备号释放函数如下:

  1. from:要释放的设备号。
  2. count: 表示从 from 开始,要释放的设备号数量。

4.5、地址映射

我使用的是GPIO4_A4,所以相关寄存器地址

复制代码
#define GPIO4_BASE                  (0x2AE40000)
#define TOP_IOC_BASE                (0x26044000)
#define VCCIO_IOC_BASE              (0x26046000)
#define GPIO4_DR_L                  (GPIO4_BASE + 0x0000)
#define GPIO4_DDR_L                 (GPIO4_BASE + 0x0008)
#define VCCIO_IOC_GPIO4A_PULL       (VCCIO_IOC_BASE + 0x0140)
#define VCCIO_IOC_GPIO4A_DS_H       (VCCIO_IOC_BASE + 0x0084)
#define TOP_IOC_GPIO4A_IOMUX_SEL_H  (TOP_IOC_BASE + 0x0084)

然后系统使用了MMU,我们需要使用地址转换函数

ioremap 函数

ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中,定义如下:

复制代码
void __iomem *ioremap(resource_size_t res_cookie, size_t size);

ioremap 函数有两个参数和一个返回值,这些参数和返回值的含义如下:

  1. res_cookie:要映射的物理起始地址。
  2. size:要映射的内存空间大小。
  3. 返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址。

iounmap 函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原型如下:

复制代码
void iounmap (volatile void __iomem *addr)

iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址

读操作函数

复制代码
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

写操作函数

复制代码
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址

4.6、代码实现

此时模块加载函数入口函数就是这个样子,但是这个方法特别不好,因为你需要自己创建设备节点,这里存在的问题就是可能你的这个设备节点是已经被使用的,还有就是次设备号的浪费 ,所以一般都用动态申请设备号的方法

复制代码
static int __init gpio_chrdev_init(void)
{
    u32 val = 0;
    int ret;
    /* 寄存器地址映射 */
    pGPIO4_DR_L =  ioremap(GPIO4_DR_L, 4);
    pGPIO4_DDR_L =  ioremap(GPIO4_DDR_L, 4);
    /* 设置为输出 */
    val = readl(pGPIO4_DDR_L);
    val &= ~(0X1 << 4); 
    val |= ((0X1 << 20) | (0X1 << 4));
    writel(val, pGPIO4_DDR_L);
    /* 设置 默认输出低电平 */
    val = readl(pGPIO4_DR_L);
    val &= ~(0X1 << 4); 
    val |= ((0X1 << 20) | (0X0 << 4));
    writel(val, pGPIO4_DR_L);
	retvalue = register_chrdev(200, DEV_NAME, &gpio_chrdev_fops);
    return 0;
}

5、新字符设备

5.1、字符设备结构

在 Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在 include/linux/cdev.h 文件中的定义如下:

复制代码
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
} __randomize_layout;

在 cdev 中有两个重要的成员变量: ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。

5.2、cdev_init 函数

定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化

复制代码
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  • 参数 cdev 就是要初始化的 cdev 结构体变量,
  • 参数 fops 就是字符设备文件操作函数集合

5.3、cdev_add 函数

cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备

复制代码
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  • 参数 p 指向要添加的字符设备(cdev 结构体变量),
  • 参数 dev 就是设备所使用的设备号,
  • 参数 count 是要添加的设备数量。

5.4、cdev_del 函数

卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备

复制代码
void cdev_del(struct cdev *p)

5.5、自动创建设备节点

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。

首先要创建一个 class 类, class 是个结构体,定义在文件include/linux/device.h 里面。 class_create 是类创建函数,内容如下:

复制代码
struct class *class_create (struct module *owner, const char *name)
void class_destroy(struct class *cls);

class_create 一共有两个参数,

  1. 参数 owner 一般为 THIS_MODULE,
  2. 参数 name 是类名字。
  3. 返回值是个指向结构体 class 的指针,也就是创建的类。

删除函数为 class_destroy,参数 cls 就是要删除的类。

创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备, device_create 函数原型如下:

复制代码
struct device *device_create(struct class *class, 
                            struct device *parent,
                            dev_t devt, 
                            void *drvdata, 
                            const char *fmt, ...)

void device_destroy(struct class *class, dev_t devt)

device_create 是个可变参数函数

  1. 参数 class 就是设备要到创建哪个类下面;
  2. 参数 parent 是父设备,一般为 NULL,也就是没有父设备;
  3. 参数 devt 是设备号;
  4. 参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;
  5. 参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。
  6. 返回值就是创建好的设备。

device_destroy删除掉创建的设备

  1. 参数 classs 是要删除的设备所处的类,
  2. 参数 devt 是要删除的设备号。

5.6、代码实现

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/platform_device.h>

//Pin Name是GPIO4_A4
#define DEV_NAME            "gpio_chrdev"
#define DEV_CNT                 (1)
#define OUTPUT_HIGH         1
#define OUTPUT_LOW          0
#define GPIO4_BASE                  (0x2AE40000)
#define TOP_IOC_BASE                (0x26044000)
#define VCCIO_IOC_BASE              (0x26046000)
#define GPIO4_DR_L                  (GPIO4_BASE + 0x0000)
#define GPIO4_DDR_L                 (GPIO4_BASE + 0x0008)
#define VCCIO_IOC_GPIO4A_PULL       (VCCIO_IOC_BASE + 0x0140)
#define VCCIO_IOC_GPIO4A_DS_H       (VCCIO_IOC_BASE + 0x0084)
#define TOP_IOC_GPIO4A_IOMUX_SEL_H  (TOP_IOC_BASE + 0x0084)
static void __iomem *pVCCIO_IOC_GPIO4A_PULL;
static void __iomem *pVCCIO_IOC_GPIO4A_DS_H;
static void __iomem *pTOP_IOC_GPIO4A_IOMUX_SEL_H;
static void __iomem *pGPIO4_DR_L;
static void __iomem *pGPIO4_DDR_L;

struct gpio_chrdev{
    dev_t devid; /* 设备号 */
    struct cdev cdev; /* cdev */
    struct class *class; /* 类 */
    struct device *device; /* 设备 */
    int major; /* 主设备号 */
    int minor; /* 次设备号 */
};
struct gpio_chrdev gpiochrdev;
void gpio_switch(u8 sta)
{
    u32 val = 0;
    if(sta == OUTPUT_HIGH) {
        val = readl(pGPIO4_DR_L);
        val &= ~(0X1 << 4); /* bit0 清零*/
        val |= ((0X1 << 20) | (0X1 << 4));
        writel(val, pGPIO4_DR_L);
    }else if(sta == OUTPUT_LOW) {
        val = readl(pGPIO4_DR_L);
        val &= ~(0X1 << 4); /* bit0 清零*/
        val |= ((0X1 << 20) | (0X0 << 4)); 
        writel(val, pGPIO4_DR_L);
    }
}
static ssize_t gpio_chrdev_read(struct file *filp, char __user *buf,
                        size_t cnt, loff_t *offt)
{
    return 0;
}
 static ssize_t gpio_chrdev_write(struct file *filp, const char __user *buf,
                            size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char stat;

    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }
    stat = databuf[0]; /* 获取状态值 */
    if(stat == OUTPUT_HIGH) {
        gpio_switch(OUTPUT_HIGH); 
    } else if(stat == OUTPUT_LOW) {
        gpio_switch(OUTPUT_LOW); 
    }
    return 0;
}
 static int gpio_chrdev_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &gpiochrdev; /* 设置私有数据 */
    return 0;
}
static int gpio_chrdev_release(struct inode *inode, struct file *filp)
{
    return 0;
}
static struct file_operations gpio_chrdev_fops = {
	.owner = THIS_MODULE,
	.open = gpio_chrdev_open,
	.release = gpio_chrdev_release,
	.write = gpio_chrdev_write,
	.read = gpio_chrdev_read,
};
void gpio_remap(void)
{
    pVCCIO_IOC_GPIO4A_PULL =  ioremap(VCCIO_IOC_GPIO4A_PULL, 4);
    pVCCIO_IOC_GPIO4A_DS_H =  ioremap(VCCIO_IOC_GPIO4A_DS_H, 4);
    pTOP_IOC_GPIO4A_IOMUX_SEL_H =  ioremap(TOP_IOC_GPIO4A_IOMUX_SEL_H, 4);
    pGPIO4_DR_L =  ioremap(GPIO4_DR_L, 4);
    pGPIO4_DDR_L =  ioremap(GPIO4_DDR_L, 4);
}
void gpio_unmap(void)
{
    /* 取消映射 */
    iounmap(pVCCIO_IOC_GPIO4A_PULL);
    iounmap(pVCCIO_IOC_GPIO4A_DS_H);
    iounmap(pTOP_IOC_GPIO4A_IOMUX_SEL_H);
    iounmap(pGPIO4_DR_L);
    iounmap(pGPIO4_DDR_L);
}
static int __init gpio_chrdev_init(void)
{
    u32 val = 0;
    int ret;
    /* 1、寄存器地址映射 */
    gpio_remap();
    /* 2、设置 GPIO4_A4 为 GPIO 功能。 */
    val = readl(pTOP_IOC_GPIO4A_IOMUX_SEL_H);
    val &= ~(0XF << 0);
    val |= ((0XF << 16) | (0X0 << 0));
    writel(val, pTOP_IOC_GPIO4A_IOMUX_SEL_H);
    /* 3、设置 GPIO4_A4 驱动能力为 level5 */
    val = readl(pVCCIO_IOC_GPIO4A_DS_H);
    val &= ~(0X7 << 0); 
    val |= ((0X7 << 16) | (0X5 << 0));
    writel(val, pVCCIO_IOC_GPIO4A_DS_H);
    /* 4、设置没有上下拉 */
    val = readl(pVCCIO_IOC_GPIO4A_PULL);
    val &= ~(0X3 << 8); 
    val |= ((0X3 << 24) | (0X0 << 8));
    writel(val, pVCCIO_IOC_GPIO4A_PULL);
    /* 5、设置为输出 */
    val = readl(pGPIO4_DDR_L);
    val &= ~(0X1 << 4); 
    val |= ((0X1 << 20) | (0X1 << 4));
    writel(val, pGPIO4_DDR_L);
    /* 6、设置 默认输出低电平 */
    val = readl(pGPIO4_DR_L);
    val &= ~(0X1 << 4); 
    val |= ((0X1 << 20) | (0X0 << 4));
    writel(val, pGPIO4_DR_L);

    ret = alloc_chrdev_region(&gpiochrdev.devid,0,DEV_CNT,DEV_NAME);
    if(ret < 0){
        printk("%s alloc_chrdev_region fail,ret = %d\r\n", DEV_NAME,ret);
        return 1;
    }
    gpiochrdev.major = MAJOR(gpiochrdev.devid);/* 获取主设备号 */
    gpiochrdev.minor = MINOR(gpiochrdev.devid);/* 获取次设备号 */

    printk("gpiochrdev major=%d,minor=%d\r\n",gpiochrdev.major,gpiochrdev.minor);
    cdev_init(&gpiochrdev.cdev, &gpio_chrdev_fops);
    gpiochrdev.cdev.owner = THIS_MODULE;
    ret = cdev_add(&gpiochrdev.cdev, gpiochrdev.devid,DEV_CNT);
    if(ret < 0){
        printk("%s cdev_add fail,ret = %d\r\n", DEV_NAME,ret);
        return 1;
    }
    /* 4、创建类 */
    gpiochrdev.class = class_create(THIS_MODULE, DEV_NAME);
    if (IS_ERR(gpiochrdev.class)) {
        printk("%s class_create fail\r\n", DEV_NAME);
        return 1;
    }
    /* 5、创建设备 */
    gpiochrdev.device = device_create(gpiochrdev.class, 
                                    NULL,
                                    gpiochrdev.devid, 
                                    NULL, 
                                    DEV_NAME);
    if (IS_ERR(gpiochrdev.device)) {
        printk("%s device_create fail\r\n", DEV_NAME);
        return 1;
    }
    return 0;
}

module_init(gpio_chrdev_init);
static void __exit gpio_chrdev_exit(void)
{
    /* 取消映射 */
    gpio_unmap();

    /* 注销字符设备驱动 */
    cdev_del(&gpiochrdev.cdev);/* 删除 cdev */
    unregister_chrdev_region(gpiochrdev.devid, DEV_CNT);

    device_destroy(gpiochrdev.class, gpiochrdev.devid);
    class_destroy(gpiochrdev.class);

}
module_exit(gpio_chrdev_exit);
MODULE_LICENSE("GPL");

6、其它相关的结构体

6.1、struct file结构体

内核中用 file 结构体来表示每个打开的文件,每打开一个文件,内核会创建一个结构体,并将对该文件上的操作函数传递给该结构体的成员变量 f_op

复制代码
struct file {
	union {
		struct llist_node	f_llist;
		struct rcu_head 	f_rcuhead;
		unsigned int 		f_iocb_flags;
	};
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	/*
	 * Protects f_ep, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct hlist_head	*f_ep;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
	errseq_t		f_sb_err; /* for syncfs */
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

主要使用的就是

  • f_op:存放与文件操作相关的一系列函数指针,如 open、 read、 wirte 等函数。
  • private_data:该指针变量只会用于设备驱动程序中,内核并不会对该成员进行操作。因此,在驱动程序中,通常用于指向描述设备的结构体

6.2、inode结构体

VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。内核使用 inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体 (即 file文件结构) 是不同的,我们可以使用多个 file 文件结构表示同一个文件的多个文件描述符,但此时,所有的这些 file 文件结构全部都必须只能指向一个 inode 结构体。

复制代码
struct inode {
	umode_t			i_mode;
	unsigned short		i_opflags;
	kuid_t			i_uid;
	kgid_t			i_gid;
	unsigned int		i_flags;

#ifdef CONFIG_FS_POSIX_ACL
	struct posix_acl	*i_acl;
	struct posix_acl	*i_default_acl;
#endif

	const struct inode_operations	*i_op;
	struct super_block	*i_sb;
	struct address_space	*i_mapping;

#ifdef CONFIG_SECURITY
	void			*i_security;
#endif

	/* Stat data, not accessed from path walking */
	unsigned long		i_ino;
	/*
	 * Filesystems may only read i_nlink directly.  They shall use the
	 * following functions for modification:
	 *
	 *    (set|clear|inc|drop)_nlink
	 *    inode_(inc|dec)_link_count
	 */
	union {
		const unsigned int i_nlink;
		unsigned int __i_nlink;
	};
	dev_t			i_rdev;
	loff_t			i_size;
	struct timespec64	i_atime;
	struct timespec64	i_mtime;
	struct timespec64	i_ctime;
	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	unsigned short          i_bytes;
	u8			i_blkbits;
	u8			i_write_hint;
	blkcnt_t		i_blocks;

#ifdef __NEED_I_SIZE_ORDERED
	seqcount_t		i_size_seqcount;
#endif

	/* Misc */
	unsigned long		i_state;
	struct rw_semaphore	i_rwsem;

	unsigned long		dirtied_when;	/* jiffies of first dirtying */
	unsigned long		dirtied_time_when;

	struct hlist_node	i_hash;
	struct list_head	i_io_list;	/* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
	struct bdi_writeback	*i_wb;		/* the associated cgroup wb */

	/* foreign inode detection, see wbc_detach_inode() */
	int			i_wb_frn_winner;
	u16			i_wb_frn_avg_time;
	u16			i_wb_frn_history;
#endif
	struct list_head	i_lru;		/* inode LRU list */
	struct list_head	i_sb_list;
	struct list_head	i_wb_list;	/* backing dev writeback list */
	union {
		struct hlist_head	i_dentry;
		struct rcu_head		i_rcu;
	};
	atomic64_t		i_version;
	atomic64_t		i_sequence; /* see futex */
	atomic_t		i_count;
	atomic_t		i_dio_count;
	atomic_t		i_writecount;
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)
	atomic_t		i_readcount; /* struct files open RO */
#endif
	union {
		const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
		void (*free_inode)(struct inode *);
	};
	struct file_lock_context	*i_flctx;
	struct address_space	i_data;
	struct list_head	i_devices;
	union {
		struct pipe_inode_info	*i_pipe;
		struct cdev		*i_cdev;
		char			*i_link;
		unsigned		i_dir_seq;
	};

	__u32			i_generation;

#ifdef CONFIG_FSNOTIFY
	__u32			i_fsnotify_mask; /* all events this inode cares about */
	struct fsnotify_mark_connector __rcu	*i_fsnotify_marks;
#endif

#ifdef CONFIG_FS_ENCRYPTION
	struct fscrypt_info	*i_crypt_info;
#endif

#ifdef CONFIG_FS_VERITY
	struct fsverity_info	*i_verity_info;
#endif

	void			*i_private; /* fs or device private pointer */
} __randomize_layout;

主要使用的就是

  • dev_t i_rdev: 表示设备文件的结点,这个域实际上包含了设备号。
  • struct cdev *i_cdev: struct cdev 是内核的一个内部结构,它是用来表示字符设备的,当 inode结点指向一个字符设备文件时,此域为一个指向 inode 结构的指针。
相关推荐
爱喝纯牛奶的柠檬3 小时前
【已验证】STM32采集声音传感器实现环境声实时监测
单片机·嵌入式硬件
我先去打把游戏先3 小时前
Git 一个本地仓库同时推送到两个远程仓库(私人 GitHub + 公司 Git)保姆级教程
git·vscode·单片机·嵌入式硬件·物联网·学习·github
悠哉悠哉愿意3 小时前
【物联网学习笔记】OLED
笔记·单片机·嵌入式硬件·物联网·学习
三佛科技-134163842123 小时前
融蜡机方案,脱毛热蜡机MCU控制方案开发
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
三佛科技-134163842123 小时前
智能小夜灯方案,智能遥控台灯方案开发MCU控制方案设计
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
誰能久伴不乏3 小时前
给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南
arm开发·c++·单片机·嵌入式硬件·arm
wdfk_prog3 小时前
MCU内核电压不稳导致程序跑飞的现象、原因与影响
数据库·单片机·嵌入式硬件
changzehai3 小时前
RustRover + J-Link 一键调试 STM32 教程
stm32·单片机·嵌入式硬件·rust·rustrover
学电子她就能回来吗4 小时前
liunx嵌入式基础:socket通信
linux·运维·服务器·人工智能·单片机·嵌入式硬件·学习