使用了设备树的pinctrl和gpio子系统
应用层访问驱动层过程
过程大致如下:
在虚拟文件系统VFS中查找对应与字符设备对应struct inode节点
遍历散列表cdev_map,根据inod节点中的cdev_t设备号找到cdev对象
创建struct file对象
初始化struct file对象,将struct file对象中的file_operations成员指向struct cdev对象中的file_operation成员
回调open函数
简介
1、字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个个字节,按照字节流进行读写操作的设备。(例:按键,电池等,IIC,SPI,LCD)。这些设备的驱动就叫字符设备驱动。
在Linux下一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相对应的设备节点(文件),应用程序通过对这个“/dev/xxx”的文件进行操作,这个xxx是具体的驱动文件名字。比如/dev/led,可以通过read来读取当前灯的状态(开或者关),write可以写数据,用来控制灯开或者关,open和close就是打开或者关闭这个led驱动
在 Linux 内核文件 include/linux/fs.h 中
有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合(上面那几个函数都在里面)
字符设备驱动框架
一、注册模块加载与卸载函数
module_exit(leddriver_exit);//注册模块卸载函数
static void __exit leddriver_exit(void)
{
i2c_del_driver(&leddriver_driver);//关联到驱动结构体
}
module_init(xxx_init); //注册模块加载函数
static int __init xxx_init(void)//入口函数关联到驱动结构体
{
int ret = 0;
ret = i2c_add_driver(ap3216c_driver);
}
insmod drv.ko //加载驱动模块
rmmod drv.ko //卸载驱动模块
后面使用这两个将之加载
用来指定加载驱动时要执行的模块加载函数;xxx_init
module_init
宏用于定义模块加载时要执行的初始化函数。当模块加载到内核中时,内核会在初始化过程中调用这个初始化函数,以执行特定的设置、分配资源、注册设备等操作。我们知道Linux很庞大,驱动-只是它启动过程的一小部分,还有很多如内存管理、调度、算法等等。那么每次需要添加一个设备的驱动就要在启动的main函数中加初始化,就很不灵活。同时内核系统庞大,多人协同不方便,修改内核启动代码容易出错。所以在内核中利用宏来处理我们所定义的初始化代码,然后在Linux内核启动过程中统一 一个地方来调用我们定义的初始化代码,做到灵活,统一可控的代码结构
二、 (内存映射)
将物理地址转换成虚拟地址
三、.注册字符设备驱动
1、注册驱动设备号到内核(设备号注册内核中,/proc/)
2、生成驱动设备节点(与用户交互,/dev/)
两种创建设备号的方法
静态创建(设置好设备号)
register_chrdev_region(dev_t from, unsigned count, const char *name);
from: 自定义的 dev_t 类型设备号 表示设备号的起始值(主+次)
count: 申请设备的数量(次设备)
name: 申请的设备名称
函数返回值:申请成功返回 0,申请失败返回负数
int major = 200;
int minor = 0;
dev_t devid = MKDEV(major, minor); // 第一个参数是主设备号,第二个参数是次设备号
register_chrdev_region(devid, 1, "test");
动态创建(主设备号自动分配,次设备号指定起始范围)
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
dev 是设备号(主+次)
baseminor 次设备的起始地址
count 注册数量
name 设备的名字
可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以baseminor为起始地址地址开始递增。一般baseminor为0,也就是说次设备号从0开始。
2、字符设备结构体初始化
cdev两个重要结构体
1、ops设备操作结构体
2、设备号
cdev_init(&newchrled.cdev, &newchrled_fops)
第一个参数是字符设备结构体(也就是字符设备的操作函数)
第二个参数是操作函数结构体
3、字符设备注册进内核(cdev添加进内核并且绑定设备号)
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
//第一个参数是设备结构体字符设备指针
//第二个参数是设备号
//第三个是设备个数
本质是将cdev写入内核中的一个哈希表中,表中同一个主设备号的会在一条链表上
cat /proc/devices
:可以查看已经注册的驱动程序,但是只显示主设备号
4、自动创建设备节点
下面是创建设备文件,先创建出一个类,再调用device_create 函数创建出/dev/目录下对应的设备节点,这样在加载模块的时候,用户空间的udev就会自动响应device_create函数去创建设备节点
也就是/dev/led这种
创建出一个逻辑类
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
创建出设备节点
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
1、每个设备节点都有一个innode,indode是linux文件管理系统维护的一个结构体
i_rdev是该设备的设备号,i_cdev是该设备的cdev结构体
2、每一个打开的文件都会有一个file结构体,要是一个文件打开多次就会有多个file结构体,所有的file都会指向一个inode结构体
4、注册模块卸载函数
module_exit(xxx_exit); //注册模块卸载函数
几个重要的结构体
1、字符设备结构体
内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的散列表cdev_map来管理当前系统中的所有字符设备
//字符设备结构体
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
设备结构体,存放一些很杂的东西
struct gpioled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev 字符设备结构体*/
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int led_gpio; /* led所使用的GPIO编号 */
};
2、设备操作结构体
应用层代码的write之后的函数会通过系统函数调用到设备操作结构体中的write函数
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
驱动层次的操作硬件函数
tatic ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 */
if(ledstat == LEDON) {
led_switch(LEDON); /* 打开 LED 灯 */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* 关闭 LED 灯 */
}
return 0;
}
read和write函数时,需要使用copy_to_user函数以及copy_from_user函数来进行数据访问,写入/读取成功函数返回0,失败则会返回未被拷贝的字节数。
平台总线框架
将驱动框架 与硬件资源分离,
编写platform驱动需要的一些东西
0、寄存器地址定义、因为这里是用地址映射,用虚拟地址进行操作 //传统字符设备驱动
1、设备结构体
2、设备具体操作函数
3、字符设备驱动操作集(file_operations)
4、platform驱动的probe函数,驱动与设备匹配后此函数就会执行(注册字符设备驱动,初始化设备(寄存器地址映射、设备))
5、remove()(卸载字符设备驱动,取消寄存器地址映射)
6、匹配列表(如果使用设备树的话通过此匹配表进行驱动匹配)
7、platform平台驱动结构体(其中包含name(其中name移动要和设备字段相对应),匹配列表,probe和remove)
8、驱动模块的加载/卸载
platform驱动结构体(probe、remove、匹配项)
static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c" /无设备树匹配函数
.of_match_table = ap3216c_of_match, //设备树匹配函数
},
.id_table = ap3216c_id,
};
static int led_probe(struct platform_device *dev)
{
int i = 0;
int ressize[5];
u32 val = 0;
struct resource *ledsource[5];
printk("led driver and device has matched!\r\n");
/* 1、获取资源 */
for (i = 0; i < 5; i++) {
ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i); /* 依次MEM类型资源 */
if (!ledsource[i]) {
dev_err(&dev->dev, "No MEM resource for always on\n");
return -ENXIO;
}
ressize[i] = resource_size(ledsource[i]);
}
/* 2、初始化LED */
/* 寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, ressize[0]);
SW_MUX_GPIO1_IO03 = ioremap(ledsource[1]->start, ressize[1]);
SW_PAD_GPIO1_IO03 = ioremap(ledsource[2]->start, ressize[2]);
GPIO1_DR = ioremap(ledsource[3]->start, ressize[3]);
GPIO1_GDIR = ioremap(ledsource[4]->start, ressize[4]);
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/* 设置GPIO1_IO03复用功能,将其复用为GPIO1_IO03 */
writel(5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/* 默认关闭LED1 */
val = readl(GPIO1_DR);
val |= (1 << 3) ;
writel(val, GPIO1_DR);
/* 注册字符设备驱动 */
/*1、创建设备号 */
if (leddev.major) { /* 定义了设备号 */
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME); /* 申请设备号 */
leddev.major = MAJOR(leddev.devid); /* 获取分配号的主设备号 */
}
/* 2、初始化cdev */
leddev.cdev.owner = THIS_MODULE;
cdev_init(&leddev.cdev, &led_fops);
/* 3、添加一个cdev */
cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
/* 4、创建类 */
leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
if (IS_ERR(leddev.class)) {
return PTR_ERR(leddev.class);
}
/* 5、创建设备 */
leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
if (IS_ERR(leddev.device)) {
return PTR_ERR(leddev.device);
}
return 0;
}
led_remove
static int led_remove(struct platform_device *dev)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
cdev_del(&leddev.cdev);/* 删除cdev */
unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */
device_destroy(leddev.class, leddev.devid);
class_destroy(leddev.class);
return 0;
}
Pinctrl子系统与GPIO子系统(设备树)
设备树(基本信息与配置信息)
设备树中是一些设备的信息,比如分辨率、 CPU 架构、主频、外设寄存器地址范
围,比如 UART、IIC 等等。
DTS是设备树源码,DTB是前者编译得到的二进制文件
dts 可重用部分
dtsi 每个芯片的特别部分((使用头文件引用dts这种适合所有平台的共有设置))
pinctrl 与gpio子系统(对设备树中众多的信息进行了管理)
pinctrl 负责管理引脚的功能,gpio子系统管理gpio的输入,输出属性
pinctrl 管理设备树中的引脚的配置信息主要是复用信息,gpio子系统管理设备树中gpio引进的配置信息(输入,输出,输出值)
在驱动代码中可以通过这些子系统的函数,获得来着设备树的关键配置信息。
设备树与驱动代码
在启动过程中,引导加载器(bootloader)会加载设备树文件,并将其传递给内核。
内核在启动时会解析设备树,提取其中的硬件配置信息,并根据这些信息完成相应的设备的初始化和驱动的加载
pinctrl、gpio子系统在驱动程序中的应用
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/cdev.h> // cdev_init
#include <linux/device.h> // device_create
#include <linux/err.h> // IS_ERR
#include <asm/io.h> // ioremap、iounmap
#include <linux/of.h> // 获取设备树属性 API
#include <linux/of_address.h> // of_ioremap
#include <linux/of_gpio.h> // of_get_named_gpio
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
/* 寄存器虚拟地址 */
static void __iomem* CCM_CCGR1;
static void __iomem* IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
static void __iomem* IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03;
static void __iomem* GPIO1_GDIR;
static void __iomem* GPIO1_DR;
static u32 val;
enum LED_STAT {
LED_ON,
LED_OFF
};
struct chrdev_led_t{
struct class* class; /* 设备节点所属类 */
struct device* driver_node; /* 驱动文件节点 */
struct cdev dev; /* 字符设备 */
dev_t devid; /* 设备号 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node* gpioNode; /* 设备树节点 */
int gpioNum; /* gpio 引脚编号 */
};
static struct chrdev_led_t chrdev_led;
/*
* @description : 打开设备
* @param – pinode : 传递给驱动的 inode
* @param - pfile : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *pinode, struct file *pfile)
{
/* 用户实现具体功能 */
printk("open chrdevbase\n");
pfile->private_data = &chrdev_led;
return 0;
}
/*
* @description : 从设备读取数据
* @param - pfile : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 缓冲区长度
* @param - offset : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *pfile, char __user *buf, size_t cnt, loff_t *offset)
{
/* 用户实现具体功能 */
struct chrdev_led_t* pdev = pfile->private_data;
const char* msg = "hello, user";
int ret = copy_to_user(buf, msg, cnt);
if(ret == 0)
{
printk("kernel send data ok!\n");
}
else
{
printk("kernel send data failed!\n");
}
return 0;
}
/*
* @description : 向设备写数据
* @param - pfile : 要打开的设备文件(文件描述符)
* @param - buf : 要给设备写入的数据(用户缓冲区)
* @param - cnt : 要写入的数据长度
* @param - offset : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *pfile, const char __user *buf, size_t cnt, loff_t *offset)
{
// 获取模块数据
struct chrdev_led_t* pdev = pfile->private_data;
printk("write chrdevbase\n");
u8 databuf[1];
u8 ledstat;
u32 ret = 0;
// 将数据从用户缓冲区拷贝到内核缓冲区
ret = copy_from_user(databuf, buf, cnt);
if(ret != 0)
return 0;
ledstat = buf[0] - '0';
printk("led state: %d\n", ledstat);
gpio_set_value();
if (ledstat == LED_ON)
{
gpio_set_value(pdev->gpioNum, 0);
}
else if(ledstat == LED_OFF)
{
gpio_set_value(pdev->gpioNum, 1);
}
return cnt;
}
/*
* @description : 关闭/释放设备
* @param - pfile : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release (struct inode *pinode, struct file * pfile)
{
/* 用户实现具体功能 */
printk("close chrdevbase\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
u32 ret = 0;
const char* outstr;
u32 regData[10];
/* 通过设备树获取到寄存器地址 */
// 获取节点
chrdev_led.gpioNode = of_find_node_by_path("/gpio-led");
if(chrdev_led.gpioNode == NULL)
{
printk("node cannot be found!\n");
return -1;
}
// 读取gpio编号
chrdev_led.gpioNum = of_get_named_gpio(chrdev_led.gpioNode, "led-gpio", 0);
if (chrdev_led.gpioNum < 0)
{
printk("gpio property fetch failed!\n");
return -1;
}
printk("led-gpio num = %d\r\n", chrdev_led.gpioNum);
// 配置 GPIO1_IO03 为输出且高电平,默认关闭LED
ret = gpio_direction_output(chrdev_led.gpioNum, 1);
if (ret < 0)
{
printk("gpio set failed!\n");
return -1;
}
/* 1. 注册设备号 */
if (chrdev_led.major)
{
chrdev_led.devid = MKDEV(chrdev_led.major, 0);
ret = register_chrdev_region(chrdev_led.devid, 1, CHRDEVBASE_NAME);
}
else
{
ret = alloc_chrdev_region(&chrdev_led.devid, 0, 1, CHRDEVBASE_NAME);
chrdev_led.major = MAJOR(chrdev_led.devid);
chrdev_led.minor = MINOR(chrdev_led.devid);
}
/* 2. 初始化字符设备 */
chrdev_led.dev.owner = THIS_MODULE;
cdev_init(&chrdev_led.dev, &chrdevbase_fops); // 初始化字符设备
/* 3. 将字符设备添加到内核 */
cdev_add(&chrdev_led.dev, chrdev_led.devid, 1); // 将字符设备添加到内核
/* 自动创建设备节点 */
// 设备节点所属类
chrdev_led.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
if (IS_ERR(chrdev_led.class))
{
return PTR_ERR(chrdev_led.class);
}
// 创建驱动文件节点
chrdev_led.driver_node = device_create(chrdev_led.class, NULL, chrdev_led.devid, NULL, CHRDEVBASE_NAME);
if (IS_ERR(chrdev_led.driver_node))
{
return PTR_ERR(chrdev_led.driver_node);
}
printk("chrdevbase init!\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
/* 取消虚拟地址和物理地址的映射 */
iounmap(CCM_CCGR1);
iounmap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
iounmap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03);
iounmap(GPIO1_GDIR);
iounmap(GPIO1_DR);
/* 注销字符设备 */
unregister_chrdev_region(chrdev_led.devid, 1); // 注销设备号
cdev_del(&chrdev_led.dev); // 卸载字符设备
device_destroy(chrdev_led.class, chrdev_led.devid); // 删除节点
class_destroy(chrdev_led.class); // 删除类
printk("chrdevbase exit!\n");
}
/*
* 将上面两个函数指定为驱动的入口和出口函数
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/*
* LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author_name");
设备树节点解析过程
驱动内核中存在类似结构体,等到设备树解析的时候,系统会自动创建实例生成各种信息结构体
一、设备树节点转化为platform_device的过程
1、dts文件转化过程
dts文件编译成为dtb文件之后供给内核解析,设备树中的每个节点都会转化为device_node节点,其中满足某些条件的节点将会被转化为platform_device节点
2、dts中的节点转化为platform_device节点的条件
只需包含下面的任意一个条件就能转化为platform_device节点
(1)根节点下的含有compatible属性的子节点,compatible属性是用于匹配驱动
如:节点key1和节点led将会被转回为platform_device节点
(2)如果节点中的compatible属性包含了"simple-bus"或者"simple-mdf"或者"isa"或者"arm,amba-bus",并且该节点的子节点包含compatible属性,那么该子节点就能转化为platform_device节点(IIC、SPI节点下的子节点即使满足条件也不应被转化为platform_device节点,应该交由对应的总线驱动程序去处理,而不是platform总线)
二、设备树节点转化为platform_device后匹配驱动的过程
1、转化之后节点信息的保存
设备树节点在转化为platform_device 节点之后
信息保存在 patform_device -> dev -> of_note节点中
of_node是一个device_node结构体,里面存放着节点的name、type和properties(属性链表)
2、匹配驱动
在platform_driver->driver中,有一个of_match_table成员,是一个of_device_id的结构体数组,每一个数组元素都存放着name、type和compatible用于匹配设备树节点生成的platform_device
匹配时:
(1)匹配patform_device -> dev -> of_note->properties(属性)->compatible属性的值 与platform_driver->driver->of_match_table中每一个节点->compatible成员的值 ,成功则关联
(2)匹配patform_device -> dev -> of_note->type 与platform_driver->driver->of_match_table中每一个节点->type成员的值 ,成功则关联
(3)匹配patform_device -> dev -> of_note->name 与 platform_driver->driver->of_match_table中每一个节点->name成员的值 ,成功则关联