设备树
设备树功能简介
设备树的文件类型及编译方式
stm32mp157a-fsmp1a.dts --------------类似于--------->xxx.c
stm32mp15xx-fsmp1x.dtsi --------------类似于--------->xxx.h
|
DTC (make dtbs)
|
stm32mp157a-fsmp1a.dtb 内核能够使用的二进制文件
设备树如何被内核驱动使用
setenv bootcmd tftp 0xc2000040 uImage\;
tftp 0xc4000000 stm32mp157a-fsmp1a.dtb\;
bootm 0xc2000040 - 0xc4000000
在Linux内核启动的时候会解析设备树,会以树状的形式将设备信息存放在内存上。
驱动如果想要使用设备信息,只需要获取到对应的设备树节点,从内部获取设备信息即可。
设备树的语法格式
https://elinux.org/Device_Tree_Usage
设备树的组成
设备树是有节点和属性组成的树状结构,属性就是键值对,节点可以包含属性和子节点。
设备树的节点的组成
<name>[@<unit-address>]
name:节点的类型名,最多不超过31个字符
unit_address:这个是节点内部描述的地址,如果节点内有reg属性就要写地址,如果没有就不写地址
节点特性:
1.节点可以取别名
demo:mynode@0x12345678{};//demo就是起的别名
2.节点可以合并
设备树在解析的时候会将同名的节点合并在一起,同名结点被合并时如果键值相同,则后者把前者的值覆盖掉。
3.节点可以被引用
&demo{ }; //在引用的结点里面添加的键值对,最后就被解析到主结点了
4.节点中的属性可以被删除
/delete-property/uint;
查看结果:
为什么要有删除属性?(dtsi结尾的文件内容一般情况下不要动)
我们的文件中有内核自带的设备树文件,如果我们想要用某结点的话,我们不能删除,防止后来的人没有办法使用。
一般的情况下是使用同名结点合并的规则,把属性delete掉。这样真实的文件里面的内容没有被删掉。
设备树中属性的组成
属性是简单的键值对,其中的值可以为空,也可以包含任意字节流。虽然数据类型没有编码到数据结构中,但是有一些基本的数据表示可以在设备树源文件中表示。
文本字符串(以空结尾)用双引号表示:
string-property = "a string";
'Cells’是由尖括号分隔的32位无符号整数:
cell-property = <0xbeef 123 0xabcd1234>;
二进制数据(16进制表示的单字节的数)用方括号表示:
binary-property = [00 01 23 45 67];
不同表示形式的数据可以用逗号连接在一起:
mixed-property = "a string", [01 23 45 67], <0x12345678>;
逗号也用于创建字符串列表:
string-list = "red fish", "blue fish";
默认含义的键值对:
1.compatible = “厂商,设备名”;
–如果在结点里面有这样一个键值对,我们就可以通过compatible获取这个结点。
–如果我们想要使用这个键值对,就要在我们的设备树文件 stm32mp157a-fsmp1a.dts 里面加这个键值对 compatible = “厂商,设备名”;例如: compatible = “hqyj,mynode”; 例如下边的 通过compatible获取节点 的例子。
–回到内核顶层目录下 make dtbs 重新编译设备树。
–之后要把设备树拷贝过去
–linux@ubuntu:~/linux-5.10.61$ cat copy.sh
#!\bin\bash
cp arch/arm/boot/uImage ~/tftpboot/
cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/tftpboot/
2.reg属性 reg是用来描述节点的地址信息的,
需要配合#address-cells和#size-cells才能够使用
(#不是注释而是固定的语法)
#address-cells修饰reg一个元素组成中 地址成员个数
#size-cells修饰reg一个元素组成中 长度个数
例如:reg=<0x12345678 1> reg=< #address-cells #size-cells>
某一个地址只有地址的值,没有长度的时候,#size-cells有可能是0.
例如:iic从基地址0x40 所以 #size-cells=<0>
但是 一般情况下 #address-cells=<1> 和 #size-cells=<1> 都为1
设备树语法实例
/dts-v1/; //设备树的版本号
/ { // /{}; 根节点
node1 { // node1{}; 根节点的子节点
a-string-property = "A string"; //属性
a-string-list-property = "first string", "second string";
a-byte-data-property = [01 23 34 56];
child-node1 { //node1节点的子节点
first-child-property; //空属性,只起到标识作用
second-child-property = <1>;
a-string-property = "Hello, world";
};
child-node2 { //node1节点的子节点
};
};
node2 { // node2{}; 根节点的子节点
an-empty-property; //空属性,只起到标识作用
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 { //node2节点的子节点
};
};
};
设备节点添加
1.在stm32mp157a-fsmp1a.dts添加节点
linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
2.添加的节点如下
mynode@0x12345678{
a-string = "hello DC23041 everyone!";
uint = <0x11223344 0xaabbccdd>;
binary-data = [00 0c 29 7b f9 be];
mixed-data = "list",<0x12345678>,[11 22 33];
};
3.编译设备树
make dtbs
4.将设备树文件拷贝到tftpboot目录下
cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/tftpboot/
5.重启开发板,让内核解析设备树
6.在/proc/device-tree目录下可以看到添加的设备树节点
驱动如何使用设备信息
设备树节点结构体及相关函数
1.节点结构体device_node
struct device_node {
const char *name; //节点名 "mynode"
const char *full_name; //节点全名 "mynode@0x12345678"
struct property *properties; //属性,在一个节点内所有的属性构成一条单链表
struct device_node *parent; //父节点
struct device_node *child; //子节点
struct device_node *sibling; //兄弟节点
};
2.属性结构体property
struct property {
char *name; //键
int length; //值的字节数
void *value; //值
struct property *next; //指向下一个属性
};
3.获取节点的函数
struct device_node *of_find_node_by_path(const char *path)
功能:通过节点的路径获取节点
参数:
@path:节点的路径 "/mynode@0x12345678"
返回值:成功返回节点的首地址,失败返回NULL
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name)
功能:通过节点的名字获取节点
参数:
@from:填写NULL,表示从根节点开始解析
@name:节点名
返回值:成功返回节点的首地址,失败返回NULL
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compatible)
功能:通过compatible获取节点
参数:
@from:NULL,表示从根节点开始解析
@type:NULL
@compatible:厂商和设备名
返回值:成功返回节点的首地址,失败返回NULL
4.获取属性的函数
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
功能:根据属性名获取属性结构体
参数:
@np:节点指针
@name:键
@lenp:获取到的值的长度
返回值:成功返回property结构体指针,失败返回NULL
设备树节点获取实例
通过路径名字匹配到节点
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
// a-string = "hello DC23041 everyone!";
// uint = <0x11223344 0xaabbccdd>;
// binary-data = [00 0c 29 7b f9 be];
// mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;
static int __init mynode_init(void)
{
// node = of_find_node_by_path("/mynode@0x12345678");
node = of_find_node_by_name(NULL, "mynode");
if (node == NULL) {
printk("find node failed!\n");
return -ENODATA;
}
printk("key=%s,value=%s\n", node->properties->name, (char*)node->properties->value);
printk("key=%s,value=%#x,%#x\n", node->properties->next->name,
__be32_to_cpup(node->properties->next->value),
__be32_to_cpup(node->properties->next->value+4));
printk("len = %d\n",node->properties->next->length);
return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");
通过compatible获取节点
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
// compatible = "hqyj,mynode";
// a-string = "hello DC23041 everyone!";
// uint = <0x11223344 0xaabbccdd>;
// binary-data = [00 0c 29 7b f9 be];
// mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;
static int __init mynode_init(void)
{
node = of_find_compatible_node(NULL, NULL, "hqyj,mynode");
if (node == NULL) {
printk("find node failed!\n");
return -ENODATA;
}
printk("key=%s,value=%s\n", node->properties->name, (char*)node->properties->value);
printk("key=%s,value=%#x,%#x\n", node->properties->next->next->name,
__be32_to_cpup(node->properties->next->next->value),
__be32_to_cpup(node->properties->next->next->value + 4));
return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");
通过of_find_property获取属性结构体
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
// compatible = "hqyj,mynode";
// a-string = "hello DC23041 everyone!";
// uint = <0x11223344 0xaabbccdd>;
// binary-data = [00 0c 29 7b f9 be];
// mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;
struct property *prop;
static int __init mynode_init(void)
{
node = of_find_compatible_node(NULL, NULL, "hqyj,mynode");
if (node == NULL) {
printk("find node failed!\n");
return -ENODATA;
}
prop = of_find_property(node, "uint", NULL);
if (prop == NULL) {
printk("find property failed!\n");
return -ENODATA;
}
printk("key: %s, value: %#x\n", prop->name,__be32_to_cpup(prop->value));
return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");
如何把点灯的代码写成设备树的形式?
gpio子系统
可以借助内核提供的gpio子系统点灯,相当于设备信息和驱动分离了。
gpio子系统框架
我们只需要写设备驱动层就可以了。
厂商驱动层根据设备驱动的需求来完成底层硬件的操作。给灯做一个编号,当上层来操作的时候,厂商驱动层只需要知道编号即可拿到地址,比如说是0x50006000。找到地址之后做地址映射,根据需求写值。<=>厂商驱动层相当于完成了我们之前点灯的时候地址映射操作寄存器的过程。
所以,我们使用 gpio子系统点灯的时候完全不用管地址是如何映射的。
厂商驱动层为上层提供了gpio号。设备驱动层只需要传入gpio号和操作即可。
又由于不同的厂商封装的接口不同,这时又需要核心层,屏蔽底层厂商实现的细节为上层提供统一的接口。
我们只需要知道,调用核心层给我们提供的那些接口就可以了。
gpio子系统的API
核心层提供的接口:
int of_get_named_gpio(struct device_node *np,
const char *propname, int index)
功能:解析设备树上的属性对应的gpio号
参数:
@np:节点的指针
@propname:键
@index:索引号
返回值:成功返回gpio号,失败返回错误码
int gpio_request(unsigned gpio, const char *label)
功能:申请要使用的gpio---申请是在解决竞态问题
参数:
@gpio:gpio编号(编号在设备树上)
@label:取名字(NULL)
返回值:成功返回0,失败返回错误码
int gpio_direction_input(unsigned gpio)
功能:设置管脚的方向为输入
参数:
@gpio:gpio编号
返回值:成功返回0,失败返回错误码
int gpio_direction_output(unsigned gpio, int value)
功能:设置管脚的方向为输出
参数:
@gpio:gpio编号
@value:1高 0低电平
返回值:成功返回0,失败返回错误码
void gpio_set_value(unsigned gpio, int value)
功能:设置电平的状态
参数:
@gpio:gpio编号
@value:1高 0低电平
返回值:无
int gpio_get_value(unsigned gpio)
功能:获取电平的状态
参数:
@gpio:gpio编号
返回值:1高电平,0低电平
void gpio_free(unsigned gpio)
功能:释放gpio
参数:
@gpio:gpio编号
返回值:无
问题:设备树中怎样描述gpio?设备树中描述gpio之后怎么解析结点?需要把设备树中的东西转换成gpio号。
gpio子系统的设备树
此时的键和值都不能随便写。
每一个结点的设备树添加步骤:
1.画出硬件连接原理图
2.找出控制器的设备树
3.参考内核帮助文档编写自己的设备树
画出硬件连接原理图
所有的控制器设备树在内核中都是现成的。
我们要做的是告诉他LED灯接的gpioe控制器。把灯和控制器对接起来。
找出控制器的设备树
找出gpioe或者gpiof设备树,要知道内核中在那个位置描述的,因为在控制器设备树中有 修饰结点如何填充的方法 以及 控制器是否使能。
arch/arm/boot/dts/stm32mp151.dtsi 在此路径下找这个文件
(stm32mp157.dtsi-->stm32mp153.dtsi-->stm32mp151.dtsi)
gpioe: gpio@50006000 { //gpioe是起的别名
gpio-controller; //空属性,标识当前的节点是gpio控制器的节点
#gpio-cells = <2>; //修饰子节点使用当前节点成员个数是2
interrupt-controller; //中断 删除不看
#interrupt-cells = <2>; //中断 删除不看
reg = <0x4000 0x400>; //控制器地址及长度
clocks = <&rcc GPIOE>; //时钟
st,bank-name = "GPIOE"; //GPIOE节点
status = "disabled"; //控制器状态 "disabled"没有使能 "okay"使能
}; //控制器已经使能了
我们实际关注的只有两个 一个是#开头的,另一个是status(控制器状态 disabled没有使能,okay 使能)
status
最终有没有使能不能只看这一个值,因为有#include好多的dtsi文件。有可能其他的使能了。
那怎么看有没有使能status?
1.在gpioe对应的设备树文件中找到它的上一级 pinctrl 。
2.在 cd /proc/device-tree 这个目录下没有找到pinctrl 。
3.于是去找pinctrl的上一级 是soc 。
4.在 cd /proc/device-tree 这个目录下找到了soc 。
5.进入到soc目录下,找到了pin-controller@50002000 ,cd 进入。
6.之后再从[root@fsmp1a /proc/device-tree/soc/pin-controller@50002000] # ls 这个目录下,找gpio@50006000.cd 进入后 cat status 查看控制器状态。
(查看到的状态可能和我们看到的gpioe中的状态不一样,可能是由于结点合并的性质)
#gpio-cells = <2>; //修饰子节点使用当前节点成员个数是2 。
#开头一般是修饰**的个数。
解析 reg = <0x4000 0x400>;
在父节点描述的地址的区间
ranges = <0 0x50002000 0xa400>; 其实是0x50002000+4000刚好等于gpioe的起始地址0x50006000
参考内核帮助文档编写自己的设备树
编写自己的设备树的前提是已经知道这个成员个数
#gpio-cells = <2>; //修饰子节点使用当前节点成员个数是2
1.查找设备树的帮助文档。所有的帮助文档都在linux@ubuntu:~/linux-5.10.61$ cd Documentation/ 这个目录下。找到设备树的帮助文档 devicetree 下边的bindings 。
linux@ubuntu:~/linux-5.10.61/Documentation/devicetree/bindings$ ls
2.找gpio
linux@ubuntu:~/linux-5.10.61/Documentation/devicetree/bindings/gpio$ ls
8xxx_gpio.txt gpio-max77620.txt gpio-xra1403.txt
abilis,tb10x-gpio.txt gpio-mm-lantiq.txt gpio-zevio.txt
brcm,bcm6345-gpio.txt gpio-moxtet.txt gpio-zynq.txt
brcm,brcmstb-gpio.txt gpio-mpc8xxx.txt ibm,ppc4xx-gpio.txt
brcm,kona-gpio.txt gpio-mvebu.txt intel,ixp4xx-gpio.txt
brcm,xgs-iproc-gpio.yaml gpio-mxs.yaml kontron,sl28cpld-gpio.yaml
cavium-octeon-gpio.txt gpio-nmk.txt mediatek,mt7621-gpio.txt
这里是gpio对应的不同板子的文件。我们要找的是st 或者stm32.
发现并没有,那么我们去找通用文档 gpio.txt
//一个键对应一个值
myleds{
led1 = <&gpioe 10 0>;
led2 = <&gpiof 10 0>;
led3 = <&gpioe 8 0>;
};
myleds.c —扩展板对应的三盏灯亮
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
//gpio子系统点灯
// myleds{
// led1 = <&gpioe 10 0>; //10管脚编号 0默认状态
// led2 = <&gpiof 10 0>; //10管脚编号 0默认状态
// led3 = <&gpioe 8 0>; //8管脚编号 0默认状态
// };
struct device_node* node;
int gpiono[3];
char* name[] = { "led1", "led2", "led3" };
static int __init myleds_init(void)
{
int i,ret;
// 1.获取设备树节点
if ((node = of_find_node_by_path("/myleds/extend_leds")) == NULL) {
printk("of_find_node_by_path error!\n");
return -EINVAL;
}
// if((node = of_find_node_by_name(NULL, "myleds"))==NULL){
// printk("of_find_node_by_path error!\n");
// return -EINVAL;
// }
for (i = 0; i < ARRAY_SIZE(gpiono); i++) {
// 2.解析得到gpio编号
gpiono[i] = of_get_named_gpio(node, name[i], 0);
if(gpiono[i] < 0){
printk("of_get_named_gpio error!\n");
return -EINVAL;
}
// 3.申请gpio
ret = gpio_request(gpiono[i], name[i]);
if(ret){
printk("gpio_request error!\n");
return -EINVAL;
}
// 4.设置gpio为输出
ret = gpio_direction_output(gpiono[i], 0);
if(ret){
printk("gpio_direction_output error!\n");
return -EINVAL;
}
// 5.设置gpio输出高电平
gpio_set_value(gpiono[i], 1);
}
return 0;
}
static void __exit myleds_exit(void)
{
int i;
for (i = 0; i < ARRAY_SIZE(gpiono); i++) {
gpio_set_value(gpiono[i], 0); //关闭led
gpio_free(gpiono[i]); //释放gpio
}
}
module_init(myleds_init);
module_exit(myleds_exit);
MODULE_LICENSE("GPL");
上述可以控制灯的亮灭,但是我们的灯亮灭不应该在安装的时候亮,卸载的时候灭。而是应该把灯的亮灭的逻辑提供给用户。所以我们要向控制灯亮灭,要提供一个接口。套一个字符设备驱动。
gpio子系统点灯实例
myleds.h
#ifndef __MYLEDS_H__
#define __MYLEDS_H__
#define LED_ON _IO('o',0)
#define LED_OFF _IO('o',1)
#endif
myleds.c
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include "myleds.h"
// myleds{
// led1 = <&gpioe 10 0>;
// led2 = <&gpiof 10 0>;
// led3 = <&gpioe 8 0>;
// };
#define CNAME "myleds"
int major = 0;
struct class* cls;
struct device* dev;
struct device_node* node;
int gpiono;
// open
int myleds_open(struct inode* inode, struct file* filp)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
// ioctl
long myleds_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
switch(cmd){
case LED_ON:
gpio_set_value(gpiono, 1);
break;
case LED_OFF:
gpio_set_value(gpiono, 0);
break;
}
return 0;
}
// close
int myleds_close(struct inode* inode, struct file* filp)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = {
.open = myleds_open,
.unlocked_ioctl = myleds_ioctl,
.release = myleds_close,
};
static int __init myleds_init(void)
{
int ret;
// 1.获取节点
node = of_find_node_by_path("/myleds");
if (node == NULL) {
printk("find node failed!\n");
ret = -ENODATA;
goto err1;
}
// 2.解析得到gpio号
gpiono = of_get_named_gpio(node, name[i], 0);
if (gpiono < 0) {
printk("get gpio failed!\n");
ret = gpiono;
goto err1;
}
// 3.申请gpio
ret = gpio_request(gpiono, NULL);
if (ret) {
printk("request gpio failed!\n");
goto err1;
}
// 4.设置gpio为输出
ret = gpio_direction_output(gpiono, 0);
if (ret) {
printk("set gpio direction failed!\n");
goto err2;
}
// 5.注册字符设备驱动
major = register_chrdev(0, CNAME, &fops);
if (major < 0) {
printk("register_chrdev failed!\n");
goto err2;
}
// 6.创建设备节点
cls = class_create(THIS_MODULE, CNAME);
if (IS_ERR(cls)) {
printk("class_create failed!\n");
ret = PTR_ERR(cls);
goto err3;
}
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
if (IS_ERR(dev)) {
printk("device_create failed!\n");
ret = PTR_ERR(dev);
goto err4;
}
return 0;
err4:
class_destroy(cls);
err3:
unregister_chrdev(major, CNAME);
err2:
gpio_free(gpiono);
err1:
return ret;
}
static void __exit myleds_exit(void)
{
device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
unregister_chrdev(major, CNAME);
gpio_set_value(gpiono, 0);
gpio_free(gpiono);
}
module_init(myleds_init);
module_exit(myleds_exit);
MODULE_LICENSE("GPL");
test.c
#include "myleds.h"
#include <head.h>
#include <sys/ioctl.h>
int main(int argc, const char* argv[])
{
int fd;
if ((fd = open("/dev/myleds", O_RDWR)) == -1)
PRINT_ERR("open error");
while (1) {
if (ioctl(fd, LED_ON))
PRINT_ERR("ioctl error");
sleep(1);
if (ioctl(fd, LED_OFF))
PRINT_ERR("ioctl error");
sleep(1);
}
close(fd);
return 0;
}
gpio子系统的练习
练习:使用gpio子系统将开发板上的六盏灯点亮
myleds{
core_leds{
led1 = <&gpioz 5 0>;
led2 = <&gpioz 6 0>;
led3 = <&gpioz 7 0>;
};
extend_leds{
led1 = <&gpioe 10 0>;
led2 = <&gpiof 10 0>;
led3 = <&gpioe 8 0>;
};
};
modprobe安装命令使用方法
另外一种模块的安装方式。(insmod)
在内核目录下执行如下命令
make modules_install INSTALL_MOD_PATH=/home/linux/rootfs
执行之后会把所有的.ko文件放在/lib/modules/5.10.61/extra/ 路径下
修改Makefile
为了把我们的驱动拷贝到对应的目录下,修改makefile.
arch?=arm
modname?=demo
ifeq ($(arch),arm)
KERNELDIR:= /home/linux/linux-5.10.61
else
KERNELDIR := /lib/modules/$(shell uname -r)/build/
endif
PWD := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules
install:
make -C $(KERNELDIR) M=$(PWD) modules_install INSTALL_MOD_PATH=/home/linux/rootfs
clean:
make -C $(KERNELDIR) M=$(PWD) clean
obj-m:= $(modname).o #编译编译的模块是demo
执行的语句:make install
make install:模块会被拷贝到开发板/lib/modules/5.10.61/extra/目录下
内核的模块在Kernal目录下。
安装卸载模块的命令
安装命令:modprobe 模块名(直接写模块名没有.ko)
卸载命令:modprobe -r 模块名(直接写模块名没有.ko)
modprobe和insmod的区别?
1.insmod安装模块的时候不会安装依赖模块,modprobe会安装依赖模块
2.insmod安装的时候可以指定模块路径安装,modprobe安装模块的时候都是/lib/modules/5.10.61/kernel extra
(比如说B依赖A ,使用insmod安装的时候,只会安装B然后报错;
但是modprobe会把A和B都安装上)