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;
比较重要的就是:
- owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
- llseek 函数用于修改文件当前的读写位置。
- read 函数用于读取设备文件
- write 函数用于向设备文件写入(发送)数据。
- poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
- unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
- compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上, 32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl
- mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
- open 函数用于打开设备文件。
- release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应
- fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
4.2、驱动模块的加载和卸载
Linux 驱动有两种运行方式
- 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序
- 将驱动编译成模块(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 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
- major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号
- name:设备名字,指向一串字符串
- fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
- major: 要注销的设备对应的主设备号。
- 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 个参数:
- dev:保存申请到的设备号
- baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
- count: 要申请的设备号数量。
- name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
- from:要释放的设备号。
- 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 函数有两个参数和一个返回值,这些参数和返回值的含义如下:
- res_cookie:要映射的物理起始地址。
- size:要映射的内存空间大小。
- 返回值: __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 一共有两个参数,
- 参数 owner 一般为 THIS_MODULE,
- 参数 name 是类名字。
- 返回值是个指向结构体 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 是个可变参数函数
- 参数 class 就是设备要到创建哪个类下面;
- 参数 parent 是父设备,一般为 NULL,也就是没有父设备;
- 参数 devt 是设备号;
- 参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;
- 参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。
- 返回值就是创建好的设备。
device_destroy删除掉创建的设备
- 参数 classs 是要删除的设备所处的类,
- 参数 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 结构的指针。
