上一篇文章中,介绍了总线设备驱动模型,并基于总线设备驱动模型改造了led驱动程序。考虑到每一个单板所用的资源可能有所不同,以led为例,使用同一芯片的每个单板,如果每一个单板对应的led引脚不同导致需要分别定义一个对应的资源文件来描述这个引脚,并且该文件会被编译进内核,进而导致内核变得臃肿。因此引入设备树来描述不同单板的资源,内核可以读取设备树文件生成platform_device,驱动程序可以读取内核从设备树生成的platform_device来获取资源。因此,我们在编写好设备树文件后,只需要专注于驱动代码的编写即可。即使如此,核心也还是离不开platform_device和platform_driver,需要理解内核如何生成platform_device我们才能更好地使用platform_device来编写驱动代码。因此,设备树的引入其实是将资源文件和内核分离,以避免内核的臃肿。
文章分为两部分:第一部分简单介绍设备树及其语法,第二部分记录如何使用设备树编写led驱动程序
1、设备树简介
设备树实际是一棵由总线连接的树,如下图所示。
设备树中系统总线就相当于根节点,各个设备是子节点,并且可以进行嵌套。
设备树使用设备树文件(dts: device tree source)描述,需要将它编译成为dtb(device tree blob)文件才能给内核使用。
设备树的核心是树形结构和属性,树形结构就如上图所示,属性就是用于描述节点的信息,比如寄存器地址,内存大小,节点名字,配置信息,引脚编号等等信息,格式是 属性名 = 属性值。
下图是一个设备树示例
该设备树对应的设备树文件如下图。
在上述设备树代码中,每个节点的定义格式如下:
点击查看代码
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
点击查看代码
/dts-v1/;
/ {
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};
点击查看代码
// 在根节点之外使用 label 引用 node:
&uart0 {
status = “disabled”;
};
点击查看代码
或在根节点之外使用全路径:
&{/uart@fe001000} {
status = “disabled”;
};
显然前者更方便。
需要注意在引用时加上取址符&,如果使用全路径的话还要用花括号把路径括起来
在设备树中非常重要的一个部分就是属性properties,它描述了节点信息,而节点本质就是物理设备的映射。
属性分为标准属性和自定义属性,所谓标准属性其实就是给内核中设备树相关的函数使用的,因为它在代码里写了对应的属性名,所以设备树为了能够让内核函数使用就把那些属性规定为标准属性,但是二者在格式上并没有太大的区别,都是:属性名 = 属性值,有2种格式:
(1)属性有值
点击查看代码
[label:] property-name = value;
(2)属性没有值
点击查看代码
[label:] property-name;
属性值有三种形式:以32位数字为一个单元的数组、字符串、以单字节为一个单元的数组,其中数组元素个数可以是1
arrays of cells(1 个或多个 32 位数据, 64 位数据使用 2 个 32 位数据表示),
string(字符串),
bytestring(1 个或多个字节)
(1)** Arrays of cells : cell 就是一个 32 位的数据,用尖括号包围起来**
点击查看代码
interrupts = <17 0xc>;
64bit 数据使用 2 个 cell 来表示,用尖括号包围起来:
点击查看代码
clock-frequency = <0x00000001 0x00000000>;
(2)** A null-terminated string (有结束符的字符串),用双引号包围起来:**
点击查看代码
compatible = "simple-bus";
(3)A bytestring(字节序列) ,用中括号包围起来:
点击查看代码
local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示
local-mac-address = [000012345678]; // 每个 byte 使用 2 个 16 进制数来表示
也可以是三种属性值形式的混合,用逗号隔开
点击查看代码
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";
常用标准属性
(1)address-cells: 地址(address) 要用多少个 32 位数来表示;
(2)size-cells: size 要用多少个 32 位数来表示;
点击查看代码
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
比如上述节点定义了一段内存,#address-cells = <1>表示使用1个32位数字表示寄存器地址,#size-cells = <1>表示用一个32位数字表示内存大小,因此memory节点中reg属性使用两个32位数字分别表示地址和大小
(3)compatible:表示节点也即所映射的设备兼容的驱动的名称,该节点所对应的platform_device会根据该属性的值与驱动名称进行匹配。一个设备可以由多个驱动支持,按照属性值出现的顺序进行优先排序匹配。
点击查看代码
led {
compatible = “A”, “B”, “C”;
};
(4)model:compatible 属性是一个字符串列表,表示可以你的硬件兼容 A、 B、 C 等驱
动;
model 用来准确地定义这个硬件是什么。
比如根节点中可以这样写:
点击查看代码
{
compatible = "samsung,smdk2440", "samsung,mini2440";
model = "jz2440_v3";
};
(5)status:dtsi 文件中定义了很多设备,但是在你的板子上某些设备是没有的。这时
你可以给这个设备节点添加一个 status 属性,设置为“disabled”:
点击查看代码
&uart1 {
status = "disabled";
};
(6)reg:reg 的本意是 register,用来表示寄存器地址。
但是在设备树里,它可以用来描述一段空间。反正对于 ARM 系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。reg 属性的值,是一系列的“ address size”,用多少个 32 位的数来表示
address 和 size,由其父节点的#address-cells、 #size-cells 决定。
点击查看代码
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
内核是如何对设备树进行处理
内核对设备树的处理流程主要分为以下四步进行:
(1)dts 在 PC 机上被编译为 dtb 文件;
(2)u-boot 把 dtb 文件传给内核;
(3)内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;
(4)对于某些 device_node 结构体,会被转换为 platform_device 结构体。
device_node结构体定义如下:
其中属性name和属性type已经过时,一般不建议使用。根节点被保存在全局变量 of_root 中,从 of_root 开始可以访问到任意节点。节点的属性使用链表存储,property是头结点,每个property节点存储了属性的名字、长度、值、下一个属性节点指针、id等信息。
并不是所有device_node都会被内核转换成platform_device,device_node节点要转换成platform_device需要满足以下条件:
(1)根节点下含有 compatile 属性的子节点
(2)含有特定 compatible 属性的节点的子节点
如果一个节点的 compatible 属性,它的值是这 4 者之一: "simplebus","simple-mfd","isa","arm,amba-bus", 那 么 它 的 子 结 点 ( 需含compatible 属性)也可以转换为 platform_device。
(3)总线 I2C、 SPI 节点下的子节点: 不转换为 platform_device
某个总线下到子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为 platform_device。
比如以下的节点中:
⚫ /mytest 会被转换为 platform_device, 因为它兼容"simple-bus";它的子节点/mytest/mytest@0 也会被转换为 platform_device
⚫ /i2c 节点一般表示 i2c 控制器, 它会被转换为 platform_device, 在内核中有对应的 platform_driver;
⚫ /i2c/at24c02 节点不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver 决定, 一般是被创建为一个 i2c_client。
⚫ 类似的也有/spi 节点, 它一般也是用来表示 SPI 控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
⚫ /spi/flash@0 节点不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver 决定, 一般是被创建为一个 spi_device。
点击查看代码
{
mytest {
compatile = "mytest", "simple-bus";
mytest@0 {
compatile = "mytest_0";
};
};
i2c {
compatile = "samsung,i2c";
at24c02 {
compatile = "at24c02";
};
};
spi {
compatile = "samsung,spi";
flash@0 {
compatible = "winbond,w25q32dw";
spi-max-frequency = <25000000>;
reg = <0>;
};
};
};
在上一篇的总线设备驱动模型中,我们通过struct resource结构体定义资源,在device_node转换为platform_device时,resource结构体的内容来自于 device_node 的 reg,interrupts属性。同时,在platform_device结构体中platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性,比如device_node中的property就包含了节点的各种属性。
从设备树转换得来的platform_device会被注册进内核里,以后当我们每注册一个 platform_driver 时,它们就会两两确定能否配对,如果能配对成功就调用 platform_driver 的 probe 函数。
platform_device和platform_driver完整的匹配过程如下:
相比原来没有设备树的总线设备驱动模型其实就多了一个设备树的匹配过程。
其中第二个就是通过设备树进行比较。
platform_device的dev结构体中有一个指针of_node指向device_node结构体
同时,驱动为了能够与设备树生成的platform_device结构体匹配,platform_driver的driver结构体中of_match_table指向一个结构体数组(也可以认为是指向一个结构体),该结构体定义如下:
使用设备树信息来判断 dev 和 drv 是否配对时:首先,如果 of_match_table 中含有 compatible 值,就跟 dev 的 compatile属性比较,若一致则成功,否则返回失败;
其次,如果 of_match_table 中含有 type 值,就跟 dev 的 device_type 属性比较,若一致则成功,否则返回失败;
最后,如果 of_match_table 中含有 name 值,就跟 dev 的 name 属性比较,若一致则成功,否则返回失败。
而设备树中建议不再使用 devcie_type 和 name 属性,所以基本上只使用设备节点的 compatible 属性来寻找匹配的 platform_driver。
下面的图概括了包含设备树时的总线设备驱动模型中platform_device和platform_driver的匹配过程:
2、基于设备树的LED驱动程序
设备树文件被内核处理后会生成platform_device结构体,因此基于设备树的LED驱动程序分为三个部分:(1)在设备树文件中添加led节点;(2)编写底层led驱动platform_driver;(3)编写上层驱动led_driver
基于上一篇《基于总线设备驱动模型的LED驱动程序》,这里只需要完成第一部分,以及修改原来的led底层驱动程序,使其能够从设备树获取寄存器资源进行配置。
(1)在设备树文件中添加led节点
点击查看代码
myled {
compatible = "led_driver";
pin = <GROUP_PIN(1, 3)>;
pin_reg = <
0x20C406C /*CCM_CCGR1*/
0x20E0068 /*IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03*/
0X209C004 /*GPIO1_GDIR*/
0X209C000 /*0X209C000*/
>;
};
在根节点下添加名为myled的节点,并使用pin,pin_reg属性指定led使用的引脚以及涉及的寄存器地址,使用compatible指定匹配的驱动的名字。
GROUP_PIN是一个宏,需要在设备树源文件的根节点前定义
点击查看代码
#define GROUP_PIN(g, p) ((g<<16) | (p))
设备树文件一般在内核目录下:arch/arm/boot/dts
添加子节点后重新编译一下设备树文件make dtbs
然后将生成的dtb文件拷贝到内核启动的目录下,以便内核使用新的设备树文件生成节点
(2)修改下层驱动代码imx6ull_chip_gpio.c
主要修改三个地方,probe函数,remove函数,以及将led_resource结构体指针改成结构体数组,因为我们从设备树获取地址后需要把它进行保存,因此需要分配内存空间(而之前使用.c定义资源文件时,在资源文件中定义了led_resource结构体变量,所以在之前的驱动文件中只需要获取指针即可。)
修改后probe函数和remove函数
点击查看代码
static int imx6ull_chip_gpio_led_demo_probe(struct platform_device *dev)
{
/* 驱动匹配成功后就会执行,因此寄存器资源的获取应该在这个函数中进行
如果是先加载设备后加载驱动,那么在加载驱动后匹配设备时,会对每个匹配的设备都执行一次这个函数
本次实验使用设备树生成platform_device,在该函数中需要执行
(1)根据dev获取device_node节点,由dev中的dev.of_node指向它
(2)根据获取的device_node节点获取寄存器信息
(3)创建设备节点
*/
struct device_node* np;
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//获取device_node
np = dev->dev.of_node;
if(!np)
{
printk("device_node error\n");
return -1;
}
//获取寄存器信息,保存到led_resource结构体中
err = of_property_read_u32(np, "pin", &g_ledpins[g_ledcnt].group_pin);
err = of_property_read_u32_index(np, "pin_reg", 0, &g_ledpins[g_ledcnt].CCM_CCGR1);
err = of_property_read_u32_index(np, "pin_reg", 1, &g_ledpins[g_ledcnt].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
err = of_property_read_u32_index(np, "pin_reg", 2, &g_ledpins[g_ledcnt].GPIO1_GDIR);
err = of_property_read_u32_index(np, "pin_reg", 3, &g_ledpins[g_ledcnt].GPIO1_DR);
//创建设备节点
led_device_register(g_ledcnt);
//设备节点数+1
g_ledcnt++;
return 0;
}
static int imx6ull_chip_gpio_led_demo_remove(struct platform_device *dev)
{
//当匹配该驱动的设备卸载时执行,因此应该在该函数内销毁设备
//若直接卸载驱动,则每个设备都执行一次这个函数
int i;
int err;
struct device_node* np;
int led_pin;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* device_destroy */
np = dev->dev.of_node;
err = of_property_read_u32(np, "pin", &led_pin);
for (i = 0; i < g_ledcnt; i++)
{
if (g_ledpins[i].group_pin == led_pin)
{
led_device_unregister(i);
g_ledpins[i].group_pin = -1;
break;
};
}
for (i = 0; i < g_ledcnt; i++)
{
if (g_ledpins[i].group_pin != -1)
break;
}
if (i == g_ledcnt)
g_ledcnt = 0;
return 0;
}
此外,还需要添加of_match_table数组,以确保驱动和设备的匹配
点击查看代码
static struct of_device_id myleds[] = {
{
.compatible = "led_driver"
},
{},
};
struct platform_driver led_driver = {
.probe = imx6ull_chip_gpio_led_demo_probe, //注册设备时如果匹配到该驱动就执行这个函数
.remove = imx6ull_chip_gpio_led_demo_remove, //当匹配该驱动的设备卸载时执行,因此应该在该函数内销毁设备
.driver = {
.name = "alientek_led",
.of_match_table = myleds, //platform_driver中必须提供这一项,因为设备树生成的platform_device会和platform_driver中的这一项进行匹配
},
};
最终完整的代码如下
点击查看代码
/*
下层驱动程序,从下层的资源文件获取寄存器、引脚等信息,实现寄存器配置、操作
1、实现platform_driver结构体并实现入口、出口函数
2、实现led_operation结构体,并给出上层驱动获取该结构体的接口。注意,这里存在交叉引用的问题,因此这个函数最终变为上层提供接口,下层向上层注册该结构体
*/
#include <linux/module.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/ioport.h"
#include "linux/jump_label.h"
#include "linux/mod_devicetable.h"
#include "linux/of.h"
#include "linux/platform_device.h"
#include "asm/io.h"
#include "linux/of.h"
#include "led_opr.h"
#include "led_resource.h"
#include "led_driver.h" //包含上层驱动的对外的函数接口声明
static struct led_resource g_ledpins[10]; //led_resource数组,用于接收从资源文件获取的led_resource结构体。g_ledpins是指针数组是因为相应的结构体已经在资源文件创建了,这里只是接收引用
static int g_ledcnt = 0;
struct registers led_registers[10]; //接收寄存器组映射后的地址,这里没有定义成指针,是因为我们需要创建出实体
/*
实现led_operation结构体
*/
static int imx6ull_chip_gpio_led_init(unsigned int which) //选择哪个LED,使用次设备号进行编号
{
int val;
if(!led_registers[which].CCM_CCGR1)
{
led_registers[which].CCM_CCGR1 = ioremap(g_ledpins[which].CCM_CCGR1, 4);
led_registers[which].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(g_ledpins[which].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03, 4);
led_registers[which].GPIO1_GDIR = ioremap(g_ledpins[which].GPIO1_GDIR, 4);
led_registers[which].GPIO1_DR = ioremap(g_ledpins[which].GPIO1_DR, 4);
}
/*
使能GPIO1_IO03时钟
CCGR1[27:26] = b11
20C_406Ch
*/
val = *led_registers[which].CCM_CCGR1;
val |= (0x3 << 26);
*(led_registers[which].CCM_CCGR1) = val;
/*
配置IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03为GPIO模式
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03[3:0] = b0101
20E_0068h
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03暂时不使用,这是配置引脚的模式,比如上升沿,上下拉电阻等
*/
val = *led_registers[which].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
val &= ~(0xF); //低四位清零
val |= (0x5); //低四位赋值b0101
*(led_registers[which].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03) = val;
/*
配置GPIO1_GDIR寄存器使GPIO1_IO03配置为输出
GPIO1_GDIR[3] = 1
209_C004h
*/
*(led_registers[which].GPIO1_DR) |= (0x1 << 3);
return 1;
}
static int imx6ull_chip_gpio_led_ctl(unsigned int which, char status) //控制led状态
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/*
根据status修改GPIO1_IO03的输出
GPIO1_DR
209_C000h
*/
if(status)
{
*led_registers[which].GPIO1_DR &= ~(1 << 3);
printk("led on\n");
}
else {
*led_registers[which].GPIO1_DR |= (1 << 3);
printk("led off\n");
}
return 0;
}
struct led_operations led_opr = {
.init = imx6ull_chip_gpio_led_init,
.ctl = imx6ull_chip_gpio_led_ctl,
};
static int imx6ull_chip_gpio_led_demo_probe(struct platform_device *dev)
{
/* 驱动匹配成功后就会执行,因此寄存器资源的获取应该在这个函数中进行
如果是先加载设备后加载驱动,那么在加载驱动后匹配设备时,会对每个匹配的设备都执行一次这个函数
本次实验使用设备树生成platform_device,在该函数中需要执行
(1)根据dev获取device_node节点,由dev中的dev.of_node指向它
(2)根据获取的device_node节点获取寄存器信息
(3)创建设备节点
*/
struct device_node* np;
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//获取device_node
np = dev->dev.of_node;
if(!np)
{
printk("device_node error\n");
return -1;
}
//获取寄存器信息,保存到led_resource结构体中
err = of_property_read_u32(np, "pin", &g_ledpins[g_ledcnt].group_pin);
err = of_property_read_u32_index(np, "pin_reg", 0, &g_ledpins[g_ledcnt].CCM_CCGR1);
err = of_property_read_u32_index(np, "pin_reg", 1, &g_ledpins[g_ledcnt].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
err = of_property_read_u32_index(np, "pin_reg", 2, &g_ledpins[g_ledcnt].GPIO1_GDIR);
err = of_property_read_u32_index(np, "pin_reg", 3, &g_ledpins[g_ledcnt].GPIO1_DR);
//创建设备节点
led_device_register(g_ledcnt);
//设备节点数+1
g_ledcnt++;
return 0;
}
static int imx6ull_chip_gpio_led_demo_remove(struct platform_device *dev)
{
//当匹配该驱动的设备卸载时执行,因此应该在该函数内销毁设备
//若直接卸载驱动,则每个设备都执行一次这个函数
int i;
int err;
struct device_node* np;
int led_pin;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* device_destroy */
np = dev->dev.of_node;
err = of_property_read_u32(np, "pin", &led_pin);
for (i = 0; i < g_ledcnt; i++)
{
if (g_ledpins[i].group_pin == led_pin)
{
led_device_unregister(i);
g_ledpins[i].group_pin = -1;
break;
};
}
for (i = 0; i < g_ledcnt; i++)
{
if (g_ledpins[i].group_pin != -1)
break;
}
if (i == g_ledcnt)
g_ledcnt = 0;
return 0;
}
static struct of_device_id myleds[] = {
{
.compatible = "led_driver"
},
{},
};
struct platform_driver led_driver = {
.probe = imx6ull_chip_gpio_led_demo_probe, //注册设备时如果匹配到该驱动就执行这个函数
.remove = imx6ull_chip_gpio_led_demo_remove, //当匹配该驱动的设备卸载时执行,因此应该在该函数内销毁设备
.driver = {
.name = "alientek_led",
.of_match_table = myleds, //platform_driver中必须提供这一项,因为设备树生成的platform_device会和platform_driver中的这一项进行匹配
},
};
static int led_driver_init(void)
{
int err;
err = platform_driver_register(&led_driver);
register_led_operation(&led_opr); //向上层注册led_operation结构体
return 0;
}
static void led_driver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
至此,led驱动的各种写法算是基本完成了。虽然是一个简单的led驱动程序,但是从中是可以引申出很多的知识,最重要的是学会驱动开发的流程、框架。以后遇到的各种复杂的驱动,究其本质,基本的东西其实也是所谓的“led”
标签:led,驱动程序,driver,总线,platform,device,节点,设备 From: https://www.cnblogs.com/starstxg/p/18155527