【嵌入式环境下linux内核及驱动学习笔记-(12-设备树操作函数)】

目录

  • 1、设备树对应的数据结构
    • 1.1 struct device_node
    • 1.2 struct property
  • 2、设备树操作函数
    • 2.1 查找节点的函数
      • 2.1.1 of_find_node_by_path
      • 2.1.2 of_find_node_by_name
      • 2.1.3 of_find_node_by_type
      • 2.1.4 of_find_compatible_node
      • 2.1.5 of_find_node_by_phandle
      • 2.1.6 of_get_child_by_name
      • 2.1.7 of_find_node_with_property
      • 2.1.8 of_get_parent
      • 2.1.9 of_get_next_parent
      • 2.1.10 of_get_next_child
      • 2.1.11 of_get_next_available_child
    • 2.2 操作属性的函数
      • 2.2.1 of_find_property
      • 2.2.2 of_property_read_bool
      • 2.2.3 of_property_read_u8/u16/u32/u64_array
      • 2.2.4 of_property_read_string等
      • 2.2.5 of_add_property
      • 2.2.6 of_remove_property
      • 2.2.7 of_update_property
      • 2.2.8 of_get_proterty
    • 2.3 兼容性相关函数
      • 2.3.1 of_device_is_compatible
      • 2.3.2 of_machine_is_compatilbe
    • 2.4 GPIO属性函数
      • 2.4.1 of_get_named_gpio
      • 2.4.2 of_get_gpio
      • 2.4.3 of_gpio_named_count
      • 2.4.4 of_gpio_count
    • 2.5 GPIO操作函数
      • 2.5.1 gpio_request
      • 2.5.2 gpio_request_one
      • 2.5.3 gpio_request_array
      • 2.5.4 gpio_free
      • 2.5.5 gpio_free_array
      • 2.5.6 gpio_direction_input
      • 2.5.7 gpio_direction_output
      • 2.5.8 gpio_set_debounce
      • 2.5.9 gpio_get_value
      • 2.5.10 gpio_set_value
      • 2.5.11 gpio_cansleep
      • 2.5.12 gpio_get_value_cansleep
      • 2.5.13 gpio_set_value_cansleep
    • 2.6 中断相关
      • 2.6.1 of_irq_find_parent
      • 2.6.2 irq_of_parse_and_map
  • 3 通过设备树操作LED
    • 3.1 电路原理及手册数据
    • 3.2 添加设备树的节点
      • 3.3 驱动程序代码
    • 3.4 public.h
    • 3.5 test.c
    • 3.6 测试步骤
  • 4 设备树的编译与反编译操作

学习Linux设备树API的最权威和最好的资源是官方文档。主要有:

  1. Documentation/devicetree/usage-model.txt: 这是介绍设备树API用法的最主要文档,描述了各种API函数的具体用法,是学习设备树API必读文档。
  2. include/linux/of.h: 这是设备树API的主要头文件,包含所有的API函数原型,并且有很详细的注释,直接阅读这个头文件也能学习到很多设备树API的相关知识。
  3. drivers/of/base.c: 这个源文件实现了大部分设备树API函数,代码也同样有很详细的注释,通过阅读源代码也能加深理解。

1、设备树对应的数据结构

与设备树相关的主要数据结构体有:
1、 struct device_node: 表示一个设备树节点,包含节点名、设备类型、兼容字符串、父节点指针、子节点列表、属性列表等信息。这是最重要的设备树数据结构。
2、struct of_device_id: 表示一个设备兼容字符串,常用于of_match_node()等函数来匹配节点。
3、 struct property: 表示一个设备树属性,包含属性名、属性数据、数据长度等信息。属性通过链表连接到设备节点上。
4、 struct of_gpio_chip: 代表一个GPIO控制器,用于GPIO的设备树绑定,将GPIO控制器和设备树节点关联起来。
5、 struct of_phandle_args: 用于表示一个phandle属性指向的节点中的多个参数(interrupts属性等)。
6、 struct of_reconfig_data: 用于描述设备的reconfigure能力,比如电源管理相关的重新配置(性能状态变更)。
7、 struct of_device: 表示从设备树节点转换来的设备,通过of_platform_device_create()或者of_device_alloc()等函数从device_node得到。然后可以通过of_device转换为普通的platform_device。
8、 struct of_dma_filter_info: 用于描述设备树节点上的dma-ranges属性,用于设置DMA区域过滤

1.1 struct device_node

struct device_node {
	const char *name;
	const char *type;
	phandle phandle;
	const char *full_name;

	struct	property *properties;
	struct	property *deadprops;	/* removed properties */
	struct	device_node *parent;
	struct	device_node *child;
	struct	device_node *sibling;
	struct	device_node *next;	/* next device of same type */
	struct	device_node *allnext;	/* next in list of all nodes */
	struct	proc_dir_entry *pde;	/* this node's proc directory */
	struct	kref kref;
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};

1.2 struct property

struct property {
	char	*name;
	int	length;
	void	*value;
	struct property *next;
	unsigned long _flags;
	unsigned int unique_id;
};

2、设备树操作函数

include/linux/of.h: 这是设备树API的主要头文件,包含所有的API函数原型。这些函数的具体定义在drivers/of/目录下,有address.c , base.c , device.c , irq.c ,platform.c等程序文件里

2.1 查找节点的函数

以下这些函数在/include/linux/of.h头文件中定义

2.1.1 of_find_node_by_path

/include/linux/of.h
struct device_node *of_find_node_by_path(const char *path);
  • path:设备树路径,以“/”分隔,例如"/soc/uart@40013000"
    这个函数会在当前的设备树中查找指定路径的节点。
    如果找到,则返回该节点的struct device_node *,否则返回NULL。
    这个函数提供了一种更方便的方式来查找设备树节点,无需逐级遍历。例如,要查找UART节点,使用这个函数可以直接:
struct device_node *uart_node;
uart_node = of_find_node_by_path("/soc/uart@40013000");

2.1.2 of_find_node_by_name

/include/linux/of.h
struct device_node *of_find_node_by_name(struct device_node *from,
	const char *name);
  • from: 起始搜索节点,如果为NULL则从设备树根节点开始搜索
  • name: 要搜索的节点名称
    这个函数会在from节点及其子节点中查找名称为name的节点。如果找到,则返回该节点的struct device_node *,否则返回NULL。
    例如,要查找名为"uart"的节点,可以:
struct device_node *uart_node;
uart_node = of_find_node_by_name(NULL, "uart");

这会在整个设备树中查找uart节点。
而:

struct device_node *soc_node;
soc_node = of_find_node_by_path("/soc");
uart_node = of_find_node_by_name(soc_node, "uart");

这会在soc节点下查找uart节点。
所以,of_find_node_by_name()提供了一种简单的方式来根据名称查找设备树节点,同样提高了代码的易读性。
和of_find_node_by_path()一起,这两个函数提供了根据路径和名称两种最常用的方式来查找设备树节点,大大简化了设备树解析的代码。

2.1.3 of_find_node_by_type

struct device_node *of_find_node_by_type(struct device_node *from,
	const char *type);
  • from: 起始搜索节点,如果为NULL则从设备树根节点开始搜索
  • type: 要搜索的节点类型,在设备树中定义,如"memory", “cpu”, "pci"等
    这个函数会在from节点下查找类型为type的第一个节点。如果找到,返回该节点的struct device_node *,否则返回NULL。
    例如,要查找类型为"pci"的第一个节点,可以:
struct device_node *pci_node;
pci_node = of_find_node_by_type(NULL, "pci");

这会在整个设备树中查找类型为pci的第一个节点。
而:

struct device_node *soc_node; 
soc_node = of_find_node_by_path("/soc");
pci_node = of_find_node_by_type(soc_node, "pci");

这会在soc节点下查找类型为pci的第一个节点。
所以,of_find_node_by_type()提供了一种简单的方式来根据类型查找设备树节点,同样提高了代码的易读性。
和of_find_node_by_path()、of_find_node_by_name()一起,这三个函数提供了根据路径、名称和类型三种最常用的方式来查找设备树节点,可以覆盖大部分节点查找需求。

2.1.4 of_find_compatible_node

struct device_node *of_find_compatible_node(struct device_node *from,
	const char *type, const char *compat);
  • from: 起始搜索节点,如果为NULL则从设备树根节点开始搜索
  • type: 要搜索的节点类型,如"spi", “i2c”, NULL表示不考虑类型
  • compatible: 要搜索的节点compatible属性
    这个函数会在from节点下查找类型为type,且compatible属性匹配compatible参数的第一个节点。
    例如,要查找兼容的SPI从节点,可以:
struct device_node *spi_node;
spi_node = of_find_compatible_node(NULL, "spi", "spidev");

这会在整个设备树中查找类型为spi,且compatible属性包含"spidev"的第一个节点。
而:

struct device_node *soc_node;  
soc_node = of_find_node_by_path("/soc");
spi_node = of_find_compatible_node(soc_node, "spi", "spidev");

这会在soc节点下查找该节点。
所以,of_find_compatible_node()提供了一种根据类型和兼容性查找设备树节点的方法。这在需要查找某类兼容设备的节点时非常有用,如I2C、SPI、USB等总线下的设备节点。

2.1.5 of_find_node_by_phandle

struct device_node *of_find_node_by_phandle(phandle handle);
  • ph: 要查找的节点的phandle属性值
    这个函数会在当前设备树中查找phandle属性值为ph的节点。如果找到,返回该节点的struct device_node *,否则返回NULL。
    phandle是设备树节点的一个属性,其值为整型,用来唯一标识一个设备树节点。of_find_node_by_phandle()利用这个属性来查找指定节点。
    例如,如果一个SPI设备节点的定义如下:
    dts
spi1: spi@10010000 {
    compatible = "vendor,spi";
    reg = <0x10010000 0x1000>;
    phandle = <1>;
};

我们可以通过phandle的值1来查找这个节点:

struct device_node *spi_node;  
spi_node = of_find_node_by_phandle(1);

这会返回spi1节点的设备节点struct device_node *。
所以,of_find_node_by_phandle()给我们提供了另一种根据唯一标识来查找设备树节点的方法。
优点:

  • 根据唯一标识phandle查找节点,查找结果唯一
  • 类似于according to path查找,直接使用phandle值查找,简单易读
    局限:
  • 需要节点定义了phandle属性,没有phandle属性的节点无法通过该函数查找
  • 如果phandle值重复,会导致错误的节点被查找到
  • 性能可能会差于according to path、name等查找,需要遍历所有节点并比较phandle属性
    所以,当设备树节点定义了phandle属性,且phandle值唯一时,of_find_node_by_phandle()是一种非常有用的查找节点方法,可以简化代码和提高可读性。

2.1.6 of_get_child_by_name

struct device_node *of_get_child_by_name(const struct device_node *node,
					const char *name);
  • np: 父设备节点
  • name: 要查找的子节点名称
    这个函数会在np节点的子节点中查找名称为name的第一个节点。如果找到,返回该节点的struct device_node *,否则返回NULL。
    例如,如果有如下设备树:
dts
soc {
    serial1: serial@10010000 { ... };
    serial2: serial@10020000 { ... }; 
};

我们可以如下查找serial1节点:

struct device_node *soc_np, *serial1_np;
soc_np = of_find_node_by_path("/soc");
serial1_np = of_get_child_by_name(soc_np, "serial1");

这会在soc节点的子节点中查找名为"serial1"的第一个节点,并返回。
所以,of_get_child_by_name()为我们提供了一种在父节点下根据名称查找子节点的方法。

2.1.7 of_find_node_with_property

struct device_node *of_find_node_with_property(
	struct device_node *from, const char *prop_name);
  • from: 起始搜索节点,如果为NULL则从设备树根节点开始搜索
  • prop_name: 要搜索的属性名称
    这个函数会在from节点下查找第一个具有prop_name属性的节点。如果找到,返回该节点的struct device_node *,否则返回NULL。
    例如,如果有如下设备树:
dts
soc {
    serial1: serial@10010000 {
        status = "okay";
    };
    serial2: serial@10020000 {
        status = "disabled"; 
    };
};

我们可以如下查找第一个状态为okay的serial节点:

struct device_node *soc_np, *serial1_np;
soc_np = of_find_node_by_path("/soc");
serial1_np = of_find_node_with_property(soc_np, "status", "okay");

这会在soc节点下查找第一个具有status属性且status值为okay的节点,并返回serial1节点。
所以,of_find_node_with_property()提供了一种根据节点属性查找设备树节点的方法。这在需要查找某类具有特定属性的节点时非常有用。

2.1.8 of_get_parent

struct device_node *of_get_parent(const struct device_node *node);
  • np: 子设备节点
    这个函数会返回np节点的父节点的struct device_node *。如果np节点是设备树根节点,则返回NULL。
    例如,如果有如下设备树:
dts
soc {
    serial1: serial@10010000 { ... }; 
};

我们可以如下获取serial1节点的父节点soc:

struct device_node *serial1_np, *soc_np;
serial1_np = of_find_node_by_path("/soc/serial1");
soc_np = of_get_parent(serial1_np);

这会返回serial1节点的父节点soc的设备节点struct device_node *。
所以,of_get_parent()为我们提供了一种简单获取设备树节点父节点的方法。

2.1.9 of_get_next_parent

struct device_node *of_get_next_parent(struct device_node *node);
  • np: 子设备节点
    这个函数会返回np节点的父节点的父节点,即np的祖父节点。如果np节点是设备树根节点或已经是最高层节点,则返回NULL。
    例如,如果有如下设备树:
dts
soc {
    serial1: serial@10010000 { ... };
}; 

我们可以如下获取serial1节点的祖父节点:

struct device_node *serial1_np, *soc_np, *parent_np;
serial1_np = of_find_node_by_path("/soc/serial1");
soc_np = of_get_parent(serial1_np);     // 获取父节点soc
parent_np = of_get_next_parent(serial1_np); // 获取祖父节点

这会返回soc节点的父节点,这里为NULL,因为soc已经是最高层节点。
如果设备树更为复杂:

dts
root {
    soc {
        serial1: serial@10010000 { ... };
    };
};

那么上述代码会返回root节点,作为serial1的祖父节点。
所以,of_get_next_parent()为我们提供了一种简单获取设备树节点的下一级父节点(即祖父节点)的方法。
与of_get_parent()相比,其主要优势是可以直接获取更高层的祖先节点,而不需多次调用。但其局限也更大,仅能获取下一级父节点,

2.1.10 of_get_next_child

struct device_node *of_get_next_child(const struct device_node *node,
   				     struct device_node *prev);

of_get_next_child()是一个用来获取设备树节点的下一个子节点的函数。
其函数原型为:
c
struct device_node *of_get_next_child(const struct device_node *np,
struct device_node *child)

  • np: 父设备节点
  • child: 上一个子节点,如果为NULL,则返回父节点的第一个子节点
    这个函数会返回np节点的下一个子节点的设备节点struct device_node *。如果没有更多子节点,则返回NULL。
    例如,如果有如下设备树:
dts
soc {
    serial1: serial@10010000 { ... }; 
    serial2: serial@10020000 { ... }; 
};

我们可以如下依次获取soc节点的所有子节点:

struct device_node *soc_np, *child_np;
soc_np = of_find_node_by_path("/soc");
child_np = of_get_next_child(soc_np, NULL); // 获取第一个子节点serial1
child_np = of_get_next_child(soc_np, child_np); // 获取下一个子节点serial2 
child_np = of_get_next_child(soc_np, child_np); // 获取下一个子节点,NULL

这会依次返回serial1、serial2和NULL三个设备节点。
所以,of_get_next_child()为我们提供了一种简单的获取设备树节点的下一个子节点的方法。通过循环调用,我们可以遍历某父节点下的所有子节点。

2.1.11 of_get_next_available_child

 struct device_node *of_get_next_available_child(
	const struct device_node *node, struct device_node *prev);
  • np: 父设备节点
  • child: 上一个子节点,如果为NULL,则返回父节点的第一个可用子节点
    这个函数会返回np节点的下一个可用子节点的设备节点struct device_node *。如果没有更多可用子节点,则返回NULL。
    这里的“可用子节点”指的是不含属性"status = "disabled"的子节点。
    例如,如果有如下设备树:
dts
soc {
    serial1: serial@10010000 { 
        status = "okay";
    };
    serial2: serial@10020000 {
        status = "disabled"; 
    }; 
    serial3: serial@10030000;    
};

我们可以如下依次获取soc节点的所有可用子节点:

struct device_node *soc_np, *child_np;
soc_np = of_find_node_by_path("/soc");
child_np = of_get_next_available_child(soc_np, NULL); 
// 获取第一个可用子节点serial1
child_np = of_get_next_available_child(soc_np, child_np);
// 获取下一个可用子节点serial3
child_np = of_get_next_available_child(soc_np, child_np); // 返回NULL

这会依次返回serial1和serial3两个设备节点,而跳过disabled的serial2节点。
所以,of_get_next_available_child()为我们提供了一种简单的获取设备树节点的下一个可用子节点的方法。通过循环调用,我们可以遍历某父节点下的所有可用子节点。

2.2 操作属性的函数

2.2.1 of_find_property

struct property *of_find_property(const struct device_node *np,
					 const char *name,
					 int *lenp);
  • np: 设备树节点
  • name: 属性名称
  • lenp:属性值长度,可选,如果不需要可以传入NULL
    这个函数会在np节点的属性列表中查找名称为name的第一个属性。如果找到,返回该属性的struct property *,同时通过lenp返回属性值的长度;否则返回NULL。
    例如,如果有如下设备树:
serial1: serial@10010000 {
    status = "okay";
    clock-frequency = <2048000>;
};

我们可以如下查找serial1节点的status属性:

struct device_node *serial1_np;
struct property *prop;
int len;
serial1_np = of_find_node_by_path("/serial1");
prop = of_find_property(serial1_np, "status", &len);

这会在serial1节点的属性中查找名为status的第一个属性,返回该属性的struct property *,同时len会被赋值为6(“okay”的长度)。
所以,of_find_property()为我们提供了一种在设备树节点中查找指定属性的方法。这对需要解析设备树节点的属性非常有用。

2.2.2 of_property_read_bool

of_property_read_bool()是一个用来读取设备树节点的bool类型属性的函数。
其函数原型为:

int of_property_read_bool(const struct device_node *np, const char *propname)
  • np: 设备树节点
  • propname: 属性名称
    这个函数会在np节点的属性列表中查找名称为propname的bool类型属性。如果找到并且其值为1或"true",则返回1;否则返回0。
    例如,如果有如下设备树:
serial1: serial@10010000 {
    status = "okay"; 
    enabled = <1>; 
};

我们可以如下读取serial1节点的enabled bool属性:

struct device_node *serial1_np;  
int enabled;
serial1_np = of_find_node_by_path("/serial1");
enabled = of_property_read_bool(serial1_np, "enabled");  

这会在serial1节点的属性列表中查找名为enabled的bool属性,并返回其值为1,所以enabled将被赋值为1。
所以,of_property_read_bool()为我们提供了一种简单读取设备树节点bool类型属性的方法。

2.2.3 of_property_read_u8/u16/u32/u64_array

 int of_property_read_u8_array(const struct device_node *np,
			const char *propname, u8 *out_values, size_t sz);
 int of_property_read_u16_array(const struct device_node *np,
			const char *propname, u16 *out_values, size_t sz);
 int of_property_read_u32_array(const struct device_node *np,
				      const char *propname,
				      u32 *out_values,
				      size_t sz);
 int of_property_read_u64(const struct device_node *np,
				const char *propname, u64 *out_value);
  • np: 设备树节点
  • propname: 属性名称
  • out_values:属性值缓存区
  • sz:缓存区大小
    这四个函数会在np节点的属性列表中查找名称为propname的u8/u16/u32/u64数组类型属性。如果找到,则将属性值按数组形式读取到out_values缓存区,数组大小为sz。读取成功则返回0,否则返回错误码。
    例如,如果有如下设备树:
serial1: serial@10010000 {
    ints = <1 2 3 4>; 
    reg = <0x01 0x20 0x101 0x4000>;
};  

我们可以如下读取serial1节点的数组属性:

u32 ints[4];
u32 reg[4];
struct device_node *serial1_np;  
serial1_np = of_find_node_by_path("/serial1");
of_property_read_u32_array(serial1_np, "ints", ints, 4);
of_property_read_u32_array(serial1_np, "reg", reg, 4); 

这会读取ints属性值为{1, 2, 3, 4}和reg属性值为{0x01, 0x20, 0x101, 0x4000},并存入数组ints和reg。
所以,这四个函数为我们提供了一种简单读取设备树节点u8/u16/u32/u64数组属性的方法。

2.2.4 of_property_read_string等

int of_property_read_string(const struct device_node *np, const char *propname,
                            const char **out_string);
                            
int of_property_match_string(const struct device_node *np, const char *propname, 
                             const char *string);
                             
int of_property_read_string_helper(const struct device_node *np, 
                                   const char *propname, char *out_string, 
                                   size_t sz, int index);  
  • np: 设备树节点
  • propname: 属性名称
  • out_string:属性值字符串指针
  • string: 需要匹配的字符串
  • out_string:属性值字符串缓存区
  • sz: 缓存区大小
  • index: 字符串在属性值中的索引

这三个函数的主要作用及区别如下:
of_property_read_string():读取np节点的propname字符串属性,并通过out_string返回其值指针。
of_property_match_string():在np节点的propname字符串属性值中匹配string,匹配成功则返回0,否则返回-EINVAL。
of_property_read_string_helper():读取np节点的propname字符串属性中索引为index的字符串,存入out_string缓存区,大小为sz。

所以,它们分别提供了读取设备树字符串属性、匹配字符串属性及读取字符串属性数组成员的方法。
例如,如果有如下设备树:

serial1: serial@10010000 {
    status = "okay";    
    strings = "str1", "str2"; 
};

我们可以如下使用这三个函数:

const char *status;   
const char str1[10];
of_property_read_string(serial1_np, "status", &status); 
// status = "okay"
  
ret = of_property_match_string(serial1_np, "status", "okay");
// ret = 0 
  
ret = of_property_read_string_helper(serial1_np, "strings", 
                                     str1, 10, 0);  
// str1 = "str1", ret = 0   

这会分别读取status为"okay"、匹配status值为"okay"成功及读取strings属性第0个字符串为"str1"。
所以,这三个函数为我们提供了一种简单处理设备树字符串属性的方法。

2.2.5 of_add_property

of_add_property()是一个用来向设备树节点添加属性的函数。
其函数原型为:

int of_add_property(struct device_node *np, const char *name, 
                    const void *value, int length)
  • np: 设备树节点
  • name: 属性名称
  • value: 属性值
  • length: 属性值长度
    这个函数会向np节点添加一个名称为name、值为value、长度为length的属性。
    成功返回0,失败返回错误码。
    例如,如果有如下设备树:
serial1: serial@10010000 {
};

我们可以如下向serial1节点添加两个属性:

struct device_node *serial1_np;  
serial1_np = of_find_node_by_path("/serial1");
of_add_property(serial1_np, "status", "okay", 6);
of_add_property(serial1_np, "int", &val, 4);  // val = 0x12345678  

这会向serial1节点添加:

  • 名称为status、值为”okay”、长度为6的属性
  • 名称为int、值为0x12345678、长度为4的属性
    添加成功后,设备树会变为:
serial1: serial@10010000 {
    status = "okay";
    int = <0x12345678>; 
};

所以,of_add_property()为我们提供了一种向已经存在的设备树节点添加属性的方法。这对需要动态生成或修改设备树非常有用。

2.2.6 of_remove_property

of_remove_property()是一个用来从设备树节点删除属性的函数。
其函数原型为:

int of_remove_property(struct device_node *np, const char *name);
  • np: 设备树节点
  • name: 属性名称
    这个函数会从np节点删除名称为name的属性。成功返回0,失败返回错误码。
    例如,如果有如下设备树:
serial1: serial@10010000 {  
    status = "okay";
    int = <0x12345678>; 
};

我们可以如下从serial1节点删除status属性:

struct device_node *serial1_np;  
serial1_np = of_find_node_by_path("/serial1");
of_remove_property(serial1_np, "status"); 

这会从serial1节点删除名称为status的属性。删除成功后,设备树会变为:

serial1: serial@10010000 { 
    int = <0x12345678>;  
};

所以,of_remove_property()为我们提供了一种从已经存在的设备树节点删除属性的方法。这对需要动态生成或修改设备树非常有用。

2.2.7 of_update_property

of_update_property()是一个用来更新设备树节点属性的函数。
其函数原型为:

int of_update_property(struct device_node *np, const char *name,
                    const void *value, int length)  
  • np: 设备树节点
  • name: 属性名称
  • value: 新属性值
  • length: 新属性值长度
    这个函数会更新np节点名称为name的属性值为value,长度为length。成功返回0,失败返回错误码。
    如果名称为name的属性不存在,则添加该属性;如果存在但值不同,则更新其值。
    例如,如果有如下设备树:
serial1: serial@10010000 {  
    status = "okay";
    int = <0x12345678>;
};   

我们可以如下更新serial1节点的status属性,并添加一个new_prop属性:

struct device_node *serial1_np;  
serial1_np = of_find_node_by_path("/serial1");
of_update_property(serial1_np, "status", "working", 8);  
of_update_property(serial1_np, "new_prop", "prop_val", 10);

这会更新status属性值为"working",并添加new_prop属性值为"prop_val"。更新成功后,设备树会变为:

serial1: serial@10010000 { 
    status = "working";
    int = <0x12345678>;
    new_prop = "prop_val";
};

所以,of_update_property()为我们提供了一种更新已经存在的设备树节点属性或添加新属性的方法。这对需要动态生成或修改设备树非常有用。

2.2.8 of_get_proterty

of_get_property()函数定义在include/linux/of.h头文件中。
函数原型为:

const void *of_get_property(const struct device_node *np, const char *name, int *lenp)

参数:

  • np: 要获取属性的设备节点。
  • name: 属性的名称。
  • lenp: 可选,用于返回属性值的长度。
    返回值:
  • 成功时,返回属性的值的指针。
  • 失败时,返回NULL。
    这个函数的作用是在给定的设备节点np中查找名为name的属性,并返回该属性的值。
    例子:
const __be32 *reg;
int len;
reg = of_get_property(np, "reg", &len);
if (reg && len > 0) {
    // 处理reg属性的值 
}

这个例子获取np节点的reg属性的值,如果成功获取,则reg指向属性值,len包含其字节长度。
of_get_property()函数是访问设备树属性的基本接口之一。通过这个函数,可以获取任意节点的任意属性的值和长度,这为后续解析设备树数据提供基础。
特殊地,如果一个属性显式地定义了0长度,那么这个函数仍然会返回其值,只是len的值为0。这可以用于节点中定义"标志"属性。
如果一个属性不存在,或者由于某种原因无法访问,该函数返回NULL,这需要调用者进行检查和处理。

2.3 兼容性相关函数

2.3.1 of_device_is_compatible

of_device_is_compatible()是用来检查设备树节点与给定的设备兼容性列表是否匹配的函数。
其函数原型为:

int of_device_is_compatible(const struct device_node *device, 
                            const char * const *compat)
  • device: 设备树节点
  • compat: 设备兼容性列表
    这个函数会检查device节点的兼容性属性是否在compat列表中。如果是,则返回1,否则返回0。
    例如,如果有如下设备树:
serial1: serial@10010000 {
    compatible = "acme,serial";
    status = "okay"; 
};

我们可以如下检查serial1节点与"acme,serial"和"foo,bar"的兼容性:


struct device_node *serial1_np;  
const char *compat[] = {"acme,serial", "foo,bar"}; 
serial1_np = of_find_node_by_path("/serial1");
ret = of_device_is_compatible(serial1_np, compat);

这会返回ret = 1,因为serial1节点的兼容性属性"acme,serial"在compat列表中。
如果再检查与"bar,foo"的兼容性:

const char *compat[] = {"bar,foo"};
ret = of_device_is_compatible(serial1_np, compat);

则会返回ret = 0,因为"bar,foo"不在serial1节点的兼容性属性列表中。
所以,of_device_is_compatible()为我们提供了一种简单检查设备树节点与给定设备兼容性列表的匹配关系的方法。

2.3.2 of_machine_is_compatilbe

of_machine_is_compatible()是用来检查机器设备树节点与给定的机器兼容性列表是否匹配的函数。
其函数原型为:

int of_machine_is_compatible(const char * const *compat)
  • compat: 机器兼容性列表
    这个函数会在全局机器设备树中查找第一个“根”设备节点(一般是“/”),并检查其兼容性属性是否在compat列表中。如果是,则返回1,否则返回0。
    例如,如果有如下机器设备树:
/ {
    model = "ACME board";
    compatible = "acme,board", "generic-board";
};

我们可以如下检查与"acme,board"和"foo,bar"的兼容性:

const char *compat[] = {"acme,board", "foo,bar"};  
ret = of_machine_is_compatible(compat); 

这会返回ret = 1,因为机器设备树根节点的兼容性属性"acme,board"在compat列表中。
如果检查与"bar,foo"的兼容性:

const char *compat[] = {"bar,foo"};
ret = of_machine_is_compatible(compat);

则会返回ret = 0,因为"bar,foo"不在根节点的兼容性属性列表中。
所以,of_machine_is_compatible()为我们提供了一种简单检查机器设备树与给定机器兼容性列表的匹配关系的方法。

2.4 GPIO属性函数

/include/linux/of_gpio.h

2.4.1 of_get_named_gpio

of_get_named_gpio()是用于从设备树中获取命名GPIO资源的函数。
其函数原型为:

int of_get_named_gpio(struct device_node *np, const char *propname, int index)
  • np: 设备树节点
  • propname: GPIO属性名称
  • index: GPIO资源索引
    这个函数会在np节点中查找名称为propname的GPIO属性,并返回索引为index的GPIO资源。
    成功返回GPIO编号,失败返回<0的错误码。
    例如,如果有如下设备树:
gpio1: gpio@10010000 {  
    gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>,    
             <&gpio2 20 GPIO_ACTIVE_LOW>;
};

我们可以如下获取gpio1节点gpios属性中的第二个GPIO资源:

struct device_node *gpio1_np;
int gpio;
gpio1_np = of_find_node_by_path("/gpio1");
gpio = of_get_named_gpio(gpio1_np, "gpios", 1); 

这会返回gpio = 20,即gpios属性第二个GPIO资源。
如果索引为0,则会返回第一个GPIO资源gpio = 10。
如果索引超出范围,则会返回负的错误码。
所以,of_get_named_gpio()为我们提供了一种从设备树中获取命名GPIO资源的简单方法。

of_get_named_gpio()返回的GPIO编号并不是随机的,而是基于Devices Tree中GPIO信息,参考内核中的GPIO控制器注册信息计算得到的。通过这个编号,我们就可以申请和控制对应的GPIO口。
GPIO的使用需要明确理解编号与硬件的对应关系。不同的硬件平台,这些信息是不一样的。

2.4.2 of_get_gpio

of_get_gpio()是用于从设备树中获取任意GPIO资源(命名或非命名)的函数。
其函数原型为:

int of_get_gpio(struct device_node *np, int index)
  • np: 设备树节点
  • index: GPIO资源索引
    这个函数会遍历np节点及其子节点的所有GPIO属性(如gpios、gpio-hog等),并返回索引为index的GPIO资源。成功返回GPIO编号,失败返回<0的错误码。
    例如,如果有如下设备树:
gpio1: gpio@10010000 {  
    gpios = <10 GPIO_ACTIVE_HIGH>,    
             <20 GPIO_ACTIVE_LOW>;  
             gpio-hog;  // non-named GPIO  
};

我们可以如下获取gpio1节点的第2个和第3个GPIO资源:

struct device_node *gpio1_np;
int gpio;
gpio1_np = of_find_node_by_path("/gpio1");
gpio = of_get_gpio(gpio1_np, 1);  // Get 2nd GPIO (20)  
gpio = of_get_gpio(gpio1_np, 2);  // Get 3rd GPIO (from gpio-hog)

这会返回gpio = 20(命名GPIO)和gpio = 非命名GPIO。
如果索引超出gpio1节点的GPIO数,则会返回负的错误码。
所以,of_get_gpio()为我们提供了一种从设备树中获取任意GPIO资源(命名或非命名)的简单方法。

2.4.3 of_gpio_named_count

of_gpio_named_count()是用于获取设备树节点中命名GPIO的数量的函数。
其函数原型为:

int of_gpio_named_count(struct device_node *np, const char *propname)
  • np: 设备树节点
  • propname: GPIO属性名称,通常为"gpios"
    这个函数会在np节点中查找名称为propname的GPIO属性,并返回其中GPIO资源的数量。成功返回GPIO数,失败返回<0的错误码。
    例如,如果有如下设备树:
gpio1: gpio@10010000 {  
    gpios = <10 GPIO_ACTIVE_HIGH>,    
             <20 GPIO_ACTIVE_LOW>;  
             gpio-hog;  
};

我们可以如下获取gpio1节点gpios属性中的GPIO数量:

struct device_node *gpio1_np;
int count;  
gpio1_np = of_find_node_by_path("/gpio1");
count = of_gpio_named_count(gpio1_np, "gpios");

这会返回count = 2,因为gpios属性定义了两个GPIO资源。
如果propname为其他名称,则会返回错误码,因为gpio1节点没有其他命名GPIO属性。
所以,of_gpio_named_count()为我们提供了一种简单获取设备树节点中命名GPIO数量的方法。

2.4.4 of_gpio_count

of_gpio_count()是用于获取设备树节点中所有GPIO(命名或非命名)的数量的函数。
其函数原型为:

int of_gpio_count(struct device_node *np) 
  • np: 设备树节点
    这个函数会遍历np节点及其子节点的所有GPIO属性(如gpios、gpio-hog等),并返回其中GPIO资源的总数量。成功返回GPIO数,失败返回<0的错误码。
    例如,如果有如下设备树:
gpio1: gpio@10010000 {  
    gpios = <10 GPIO_ACTIVE_HIGH>,    
             <20 GPIO_ACTIVE_LOW>;  
             gpio-hog;  
};

我们可以如下获取gpio1节点的全部GPIO数量:

struct device_node *gpio1_np;
int count;  
gpio1_np = of_find_node_by_path("/gpio1");
count = of_gpio_count(gpio1_np);

这会返回count = 3,因为gpio1节点定义了2个命名GPIO和1个非命名GPIO。
所以,of_gpio_count()为我们提供了一种简单获取设备树节点中全部GPIO(命名和非命名)数量的方法。

2.5 GPIO操作函数

以下函数在include/linux/gpio.h中声明

2.5.1 gpio_request

gpio_request()是申请使用GPIO的函数。
其函数原型为:

int gpio_request(unsigned int gpio, const char *label)
  • gpio: 要申请的GPIO编号
  • label: 申请者标识,用于识别GPIO的使用方向
    这个函数会申请使用GPIO,如果成功,则返回0,否则返回错误码。
    申请GPIO的目的是为了避免GPIO被其他驱动重复使用,导致冲突。申请后,该GPIO将处于未初始化状态,需使用gpio_direction_input/output()设置方向,并使用gpio_set_value()等函数进行操作。
    申请完成后,其他驱动将无法再申请该GPIO,直到 gpio_free()释放。
    例如:
#define GPIO_LED  10

ret = gpio_request(GPIO_LED, "sys_led");   // Request GPIO_LED
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_LED, ret);
    return -1; 
}
gpio_direction_output(GPIO_LED, 0);   // Set GPIO_LED as output
gpio_set_value(GPIO_LED, 1);         // Turn on GPIO_LED

首先申请使用GPIO_LED,如果成功,则配置为输出并点亮LED。
其他驱动在该GPIO被申请后, gpio_request()将返回忙错误,无法再申请使用。
所以,gpio_request()为我们提供了一种简单的申请使用某个GPIO并避免资源冲突的机制。

2.5.2 gpio_request_one

gpio_request_one()是申请使用单个GPIO的简化函数。
其函数原型为:

int gpio_request_one(unsigned int gpio, unsigned long flags, const char *label)
  • gpio: 要申请的GPIO编号
  • flags: 附加标志,可设置
    • flags参数可以取的值主要有:
      1. GPIOF_IN:配置GPIO为输入方向。这是默认配置,如果没有指定flags参数,GPIO默认为输入。
      1. GPIOF_OUT_INIT_LOW: 配置GPIO为输出,并初始化为低电平。
      1. GPIOF_OUT_INIT_HIGH: 配置GPIO为输出,并初始化为高电平。
      1. GPIOF_DIR_IN: 同GPIOF_IN,配置GPIO为输入。
      1. GPIOF_DIR_OUT: 配置GPIO为输出。
      1. GPIOF_OPEN_DRAIN: 配置GPIO为开漏输出。
      1. GPIOF_OPEN_SOURCE: 配置GPIO为开源输出。
    • 除此之外,flags还可以是以上几个配置的OR组合,从而定义GPIO的复合行为。例如:GPIOF_OUT_INIT_HIGH | GPIOF_OPEN_DRAIN
  • label: 申请者标识,用于识别GPIO的使用方向

这个函数会申请使用GPIO,同时根据flags设置GPIO方向和电平极性,
如果成功,则返回0,否则返回错误码。
与gpio_request()相比,gpio_request_one()在申请GPIO的同时完成了初始化配置,简化了GPIO申请和设置的步骤。其他方面,包括申请目的、成功/失败返回值、限制等都与gpio_request()相同。
例如:

#define GPIO_LED  10

ret = gpio_request_one(GPIO_LED, GPIOF_OUT_INIT_LOW, "sys_led");
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_LED, ret);
    return -1; 
}

gpio_set_value(GPIO_LED, 1);         // Turn on GPIO_LED

这会申请GPIO_LED,同时配置为输出且初始化为低电平。所以点亮LED后,无须再调用gpio_direction_output()配置方向。
所以,gpio_request_one()为我们提供了一种简单的申请使用某个GPIO,同时完成初始化配置的机制。这是正确使用GPIO的最简单方式。

2.5.3 gpio_request_array

gpio_request_array()是申请使用GPIO数组的函数。
其函数原型为:

int gpio_request_array(const struct gpio *array, size_t num)
  • array: 指向GPIO配置数组的指针
  • num: 数组中GPIO的数量
    这个函数会根据array数组申请使用num个GPIO,如果全部成功,则返回0,否则返回错误码。
    array数组的格式为:
struct gpio {
    unsigned int gpio;
    unsigned long flags;
    const char *label;
};
  • gpio: 要申请的GPIO编号
  • flags: 附加标志,可设置GPIO_IN、GPIO_OUT、GPIO_ACTIVE_LOW等
  • label: 申请者标识,用于识别GPIO的使用方向
    所以,该函数可以批量申请多个GPIO,同时为每个GPIO指定flags和label,实现简单的初始化配置。
    例如:
struct gpio leds[] = {
    {10, GPIOF_OUT_INIT_LOW,  "led1"},
    {20, GPIOF_OUT_INIT_HIGH, "led2"},
    {30, GPIOF_IN,            "button1"}
};

ret = gpio_request_array(leds, ARRAY_SIZE(leds));
if (ret) {
    printk("Failed to request GPIOs,error=%d\n", ret);
    return -1; 
}

这会申请3个GPIO,GPIO10和GPIO20配置为输出,GPIO30配置为输入。
所以,gpio_request_array()为我们提供了一种简单的批量申请多个GPIO并简单初始化的机制。

2.5.4 gpio_free

gpio_free()是释放已申请的GPIO的函数。
其函数原型为:

void gpio_free(unsigned int gpio)
  • gpio: 要释放的GPIO编号
    这个函数会释放之前通过gpio_request()、gpio_request_one()或gpio_request_array()申请的GPIO。
    释放GPIO的目的是为了允许其他驱动重新申请使用这些GPIO资源。只有通过上述函数成功申请的GPIO才需要释放。
    例如:
#define GPIO_LED  10

ret = gpio_request(GPIO_LED, "sys_led");   // Request GPIO_LED
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_LED, ret);
    return -1; 
}
// ...
gpio_set_value(GPIO_LED, 0);             // Turn off GPIO_LED
gpio_free(GPIO_LED);                     // Free GPIO_LED

首先申请使用GPIO_LED,然后在使用完成后释放之。
其他驱动在该GPIO被释放后,gpio_request()将再次申请成功, regain 使用权。
所以,gpio_free()为我们提供了一种简单的释放某个申请使用过的GPIO资源的机制。这是正确使用GPIO的最后一步。

2.5.5 gpio_free_array

gpio_free_array()是释放已申请的GPIO数组的函数。
其函数原型为:

void gpio_free_array(const struct gpio *array, size_t num) 
  • array: GPIO配置数组,与gpio_request_array()中的array相同
  • num: 数组中GPIO的数量
    这个函数会根据array数组释放num个之前通过gpio_request_array()申请的GPIO。
    与gpio_free()相比,gpio_free_array()可以批量释放一组GPIO。其他方面,包括释放目的、限制等都相同。
    例如:
struct gpio leds[] = {
    {10, GPIOF_OUT_INIT_LOW,  "led1"},
    {20, GPIOF_OUT_INIT_HIGH, "led2"},
    {30, GPIOF_OUT_INIT_LOW,  "led3"},
};  

ret = gpio_request_array(leds, ARRAY_SIZE(leds));  
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", ret, ret);   
    return -1; 
}
// ...
gpio_set_value(10, 0);        // Turn off led1 
gpio_set_value(20, 0);        // Turn off led2
gpio_set_value(30, 0);        // Turn off led3
gpio_free_array(leds, ARRAY_SIZE(leds)); // Free leds 

这会释放之前通过gpio_request_array()申请的GPIO 10、20和30。
所以,gpio_free_array()为我们提供了一种简单的批量释放一组GPIO资源的机制。

2.5.6 gpio_direction_input

gpio_direction_input()是设置GPIO方向为输入的函数。
其函数原型为:

int gpio_direction_input(unsigned int gpio)
  • gpio: 要设置方向的GPIO编号
    这个函数会将指定的GPIO配置为输入模式。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请的GPIO才可以调用此函数设置方向。
    设置GPIO输入方向的目的是为了后续可以通过gpio_get_value()等函数读取GPIO输入电平值。
    例如:
#define GPIO_BUTTON 10

ret = gpio_request(GPIO_BUTTON, "sys_button");    // Request GPIO_BUTTON  
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_BUTTON, ret);
    return -1; 
}
gpio_direction_input(GPIO_BUTTON);                 // Set GPIO_BUTTON as input
val = gpio_get_value(GPIO_BUTTON);                // Read GPIO_BUTTON input level

首先申请使用GPIO_BUTTON,然后配置为输入方向,最后读取其输入电平值。
其他驱动无法再配置该GPIO的方向,直到其被释放。所以,该GPIO一旦被设置为输入,则保持输入状态直到资源释放。
所以,gpio_direction_input()为我们提供了一种简单的将GPIO配置为输入模式的机制。这是正确使用GPIO输入的第一步。

2.5.7 gpio_direction_output

gpio_direction_output()是设置GPIO方向为输出的函数。
其函数原型为:

int gpio_direction_output(unsigned int gpio, int value)
  • gpio: 要设置方向的GPIO编号
  • value: 设置为输出后的初始值,0或1
    这个函数会将指定的GPIO配置为输出模式,同时设置其输出值为value。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请的GPIO才可以调用此函数设置方向。
    设置GPIO输出方向的目的是为了后续可以通过gpio_set_value()等函数修改和控制GPIO输出电平。
    例如:
#define GPIO_LED 10  

ret = gpio_request(GPIO_LED, "sys_led");      // Request GPIO_LED   
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_LED, ret); 
    return -1; 
}
gpio_direction_output(GPIO_LED, 0);            // Set GPIO_LED as output and low  
gpio_set_value(GPIO_LED, 1);                  // Set GPIO_LED to high 

首先申请使用GPIO_LED,然后配置为输出方向且初始值为低电平,最后设置为高电平点亮LED。
其他驱动无法再配置该GPIO的方向,直到其被释放。所以,该GPIO一旦被设置为输出,则保持输出状态直到资源释放。
所以,gpio_direction_output()为我们提供了一种简单的将GPIO配置为输出模式的机制。这是正确使用GPIO输出的第一步。

2.5.8 gpio_set_debounce

gpio_set_debounce()是设置GPIO防抖时间的函数。
其函数原型为:

int gpio_set_debounce(unsigned gpio, unsigned debounce)
  • gpio: 要设置防抖时间的GPIO编号
  • debounce: 防抖时间,单位ms
    这个函数会为指定的GPIO设置软件防抖,以过滤掉持续时间小于debounce的GPIO电平变化。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请的GPIO才可以调用此函数设置防抖时间。
    设置GPIO防抖时间的目的是为了过滤掉GPIO输入值过快的抖动变化,提高GPIO输入读值的稳定性。该函数只对配置为输入方向的GPIO有效。
    例如:
#define GPIO_BUTTON 10  

ret = gpio_request(GPIO_BUTTON, "sys_button");   // Request GPIO_BUTTON
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_BUTTON, ret);
    return -1;  
}
gpio_direction_input(GPIO_BUTTON);                // Set GPIO_BUTTON as input 
gpio_set_debounce(GPIO_BUTTON, 200);             // Set debounce time as 200ms
val = gpio_get_value(GPIO_BUTTON);               // Read stable input level

首先申请GPIO_BUTTON并配置为输入,然后设置防抖时间为200ms,最后读取稳定的输入值。
如果BUTTON在短于200ms内出现多次抖动,将被过滤,读取到的val值不变。只有在BUTTON输入稳定200ms后,val值才随BUTTON真实变化而改变。
所以,gpio_set_debounce()为我们提供了一种简单的为GPIO输入设置软件防抖的机制。这有助于提高GPIO输入读取值的正确性。

2.5.9 gpio_get_value

gpio_get_value()是读取GPIO输入值的函数。
其函数原型为:

int gpio_get_value(unsigned int gpio)
  • gpio: 要读取输入值的GPIO编号
    这个函数会读取指定GPIO的输入值。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请且配置为输入方向的GPIO才可以调用此函数读取输入值。
    读取GPIO输入值的目的是获取GPIO连接设备的电平信号。GPIO输入值只有两种:0或1,对应LOW和HIGH电平。
    例如:
#define GPIO_BUTTON 10  

ret = gpio_request(GPIO_BUTTON, "sys_button");   // Request GPIO_BUTTON  
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_BUTTON, ret);
    return -1;  
}
gpio_direction_input(GPIO_BUTTON);                // Set GPIO_BUTTON as input
val = gpio_get_value(GPIO_BUTTON);               // Read GPIO_BUTTON input level  
if (val) 
    printk("Button pressed!\n");
else
    printk("Button released!\n");

首先申请GPIO_BUTTON并配置为输入,然后读取其输入值,并根据不同值判断按钮是否被按下。
GPIO输入值的变化可被随时读取,仅需再次调用gpio_get_value()。但当GPIO配置为输出方向时,读取的值始终为输出值,而非输入。
所以,gpio_get_value()为我们提供了一种简单的读取GPIO输入电平值的机制。这是使用GPIO输入的主要方法之一。

2.5.10 gpio_set_value

gpio_set_value()是设置GPIO输出值的函数。
其函数原型为:

void gpio_set_value(unsigned int gpio, int value)  
  • gpio: 要设置输出值的GPIO编号
  • value: 要设置的输出值,0或1
    这个函数会设置指定GPIO的输出值为value。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请且配置为输出方向的GPIO才可以调用此函数设置输出值。
    设置GPIO输出值的目的是控制GPIO连接设备的工作状态。GPIO输出值只有两种:0或1,对应LOW和HIGH电平。
    例如:
#define GPIO_LED 10   

ret = gpio_request(GPIO_LED, "sys_led");          // Request GPIO_LED   
if (ret) {
    printk("Failed to request GPIO_%d,error=%d\n", GPIO_LED, ret); 
    return -1;  
}
gpio_direction_output(GPIO_LED, 0);               // Set GPIO_LED as output and low
gpio_set_value(GPIO_LED, 1);                     // Turn on GPIO_LED  
msleep(500);                                    // Delay 0.5s
gpio_set_value(GPIO_LED, 0);                     // Turn off GPIO_LED

首先申请GPIO_LED并配置为输出,然后设置输出值为高电平点亮LED,延时0.5s后再设置输出值为低电平熄灭LED。
GPIO输出值可被随时修改,仅需再次调用gpio_set_value()。但当GPIO配置为输入方向时,设置的值不会起任何作用。
所以,gpio_set_value()为我们提供了一种简单的设置GPIO输出电平值的机制。这是使用GPIO输出的主要方法之一。

2.5.11 gpio_cansleep

gpio_cansleep()是判断GPIO是否允许睡眠的函数。
其函数原型为:

int gpio_cansleep(unsigned int gpio)
  • gpio: 要判断的GPIO编号
    这个函数会检查指定的GPIO是否允许在睡眠上下文中访问。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请的GPIO才可以调用此函数判断。
    判断GPIO是否允许睡眠的目的是为了避免在不适当的上下文访问GPIO,导致系统错误。
    在Linux内核中,有两类上下文:
  1. 睡眠上下文:允许睡眠或调度的上下文,如进程上下文
  2. 原子上下文:不允许睡眠或调度的上下文,如中断上下文
    所以,任何驱动中使用GPIO的代码都应首先调用gpio_cansleep()判断当前上下文,如果返回非0(真),则可以安全地访问GPIO,否则只能limited地访问GPIO。
#define GPIO_LED 10   

void led_blink(void)
{
    ret = gpio_request(GPIO_LED, "sys_led");      // Request GPIO
    if (ret) {
        printk("Failed to request GPIO_%d,error=%d\n", GPIO_LED, ret); 
        return;
    }
    
    if (gpio_cansleep(GPIO_LED)) {               // In sleep context
        gpio_direction_output(GPIO_LED, 0);      // Set as output 
        // Use GPIO ...
    } else {
        gpio_set_value(GPIO_LED, 0);             // Limited use in non-sleep context 
    }
 gpio_free(GPIO_LED);                           // Release GPIO
}

可以看到,我首先申请了GPIO,然后判断上下文,根据判断配置方向或设置值,最后释放GPIO。

2.5.12 gpio_get_value_cansleep

gpio_get_value_cansleep()是在睡眠上下文中读取GPIO输入值的函数。
其函数原型为:

int gpio_get_value_cansleep(unsigned int gpio)  
  • gpio: 要读取输入值的GPIO编号
    这个函数会在睡眠上下文中读取指定GPIO的输入值。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请且配置为输入方向的GPIO才可以调用此函数读取输入值。
    读取GPIO输入值的目的是获取GPIO连接设备的电平信号。GPIO输入值只有两种:0或1,对应LOW和HIGH电平。
    与gpio_get_value()不同的是,gpio_get_value_cansleep()只能在睡眠上下文中调用,即只有在可以睡眠/调度的上下文(如进程上下文)中才可使用。在不允许睡眠的上下文(如中断上下文)中调用此函数会导致错误。
    例如:
#define GPIO_BUTTON 10  

void button_pressed(void)
{
    ret = gpio_request(GPIO_BUTTON, "sys_button");   // Request GPIO_BUTTON  
    if (ret) {
        printk("Failed to request GPIO_%d,error=%d\n", GPIO_BUTTON, ret);
        return;  
    }
    gpio_direction_input(GPIO_BUTTON);                // Set GPIO_BUTTON as input  
    if (gpio_get_value_cansleep(GPIO_BUTTON))         // Read input level,in sleep context   
        printk("Button pressed!\n");
}

在button_pressed()进程上下文函数中,首先申请GPIO_BUTTON并配置为输入,然后读取其输入值,并根据不同值判断按钮是否被按下。
由于此函数只能在睡眠上下文调用,所以如果在中断上下文中调用button_pressed(),则会出错。这是与gpio_get_value()的主要区别。
所以,gpio_get_value_cansleep()为我们提供了一种在睡眠上下文中读取GPIO输入值的方法。这有助于区分GPIO的不同访问上下文。

2.5.13 gpio_set_value_cansleep

gpio_set_value_cansleep()是在睡眠上下文中设置GPIO输出值的函数。
其函数原型为:

void gpio_set_value_cansleep(unsigned int gpio, int value)   
  • gpio: 要设置输出值的GPIO编号
  • value: 要设置的输出值,0或1
    这个函数会在睡眠上下文中设置指定GPIO的输出值为value。只有通过gpio_request()、gpio_request_one()或gpio_request_array()成功申请且配置为输出方向的GPIO才可以调用此函数设置输出值。
    设置GPIO输出值的目的是控制GPIO连接设备的工作状态。GPIO输出值只有两种:0或1,对应LOW和HIGH电平。
    与gpio_set_value()不同的是,gpio_set_value_cansleep()只能在睡眠上下文中调用,即只有在可以睡眠/调度的上下文(如进程上下文)中才可使用。在不允许睡眠的上下文(如中断上下文)中调用此函数会导致错误。
    例如:
#define GPIO_LED 10  

void led_blink(void)  
{
    ret = gpio_request(GPIO_LED, "sys_led");          // Request GPIO_LED   
    if (ret) {
        printk("Failed to request GPIO_%d,error=%d\n", GPIO_LED, ret);  
        return;  
    }
    gpio_direction_output(GPIO_LED, 0);               // Set as output 
    gpio_set_value_cansleep(GPIO_LED, 1);            // Turn on LED, in sleep context
    msleep(500);                                    // Delay 0.5s
    gpio_set_value_cansleep(GPIO_LED, 0);           // Turn off LED
}

在led_blink()进程上下文函数中,首先申请GPIO_LED并配置为输出,然后设置输出值为高电平点亮LED,延时0.5s后再设置输出值为低电平熄灭LED。
由于此函数只能在睡眠上下文调用,所以如果在中断上下文中调用led_blink(),则会出错。这是与gpio_set_value()的主要区别。
所以,gpio_set_value_cansleep()为我们提供了一种在睡眠上下文中设置GPIO输出值的方法。这有助于区分GPIO的不同访问上下文。

2.6 中断相关

/include/linux/of_irq.h

2.6.1 of_irq_find_parent

include/linux/of.h头文件中,它的原型为:

struct device_node *of_irq_find_parent(struct device_node *child);

该函数的作用是:查找child节点中interrupt-parent属性指定的节点,或查找其自然父节点。并从这些节点中找到#interrupt-cells属性,那么该节点就是中断父节点,并被返回。
of_irq_find_parent()会返回找到的中断父控制器节点的设备节点指针,如果没有找到则返回NULL。
该函数的使用方法是:

struct device_node *node, *parent;

node = of_find_node_by_path("/interrupt"); // 获取中断节点
parent = of_irq_find_parent(node);      // 查找其中断父节点

if (parent) 
    /* 从parent中获取中断控制器信息,执行相关操作 */
else
    pr_err("No interrupt parent found!\n");

以上代码会查找名为/interrupt的节点的中断父控制器,并执行相关操作。

of_irq_find_parent()函数的工作机制是:

  1. 获取参数child指定的设备节点的interrupt-parent属性;
  2. 查找interrupt-parent属性是否存在,
    2.1 如果存在interrupt-parent属性,则查找这个parent节点是否包含#interrupt-cells属性,包含这个parent就是中断父节点;不包含就到parent节点里继续第2步。
    2.2 如果不存在interrupt-parent属性,则查找这个parent节点的自然父节点,并在自然父节点里查找是否包含#interrupt-cells属性,包含的话这个自然父节点就是中断父节点,不包含就重复第2步。
  3. 当找到一个节点包含"#interrupt-cells"属性的节点;
  4. 该函数使用of_node_get()和of_node_put()进行节点引用计数管理;
  5. 如果均未找到中断父节点,则返回NULL。

2.6.2 irq_of_parse_and_map

/include/linux/of_irq.h

irq_of_parse_and_map()是从设备树中解析中断信息并映射中断号的函数。
其函数原型为:

int irq_of_parse_and_map(struct device_node *dev, int index)
  • dev: 要解析的设备节点
  • index: 要解析的中断索引
    这个函数会在指定的设备节点dev中解析index索引对应的中断信息,并根据解析结果映射一个实际的中断号返回。
    解析中断信息的目的是获取设备树中关于中断的详细数据,如中断类型、触发方式、父中断号等,并据此计算得到实际的中断号。
    中断在Linux内核中是通过中断号来标识和管理的。中断号通常从0开始连续分配,但设备树中对中断的描述却较为抽象。所以,内核需要一个机制将设备树中的中断描述解析为实际的中断号,方便中断的申请和处理。
    irq_of_parse_and_map()就是内核中实现该机制的函数之一。它可以解析设备树中的interrupt、interrupts或interrupt-parent等节点,计算得到实际的中断号。
    例如,对于下面的设备树:
dts
/* exynos4412.dtsi */
interrupt-parent = <&gic>;                /* The GIC */
 
/* samsung-i2c.dtsi */
i2c3: i2c@13820000 {
  interrupts = <0 25 4>, <1 25 4>;        /* I2C0 and I2C1 share IRQ */  
                                         /* IRQ number is 25 + 4 = 29 */
};

可以使用此函数如下解析和映射i2c@13820000的中断:

struct device_node *np = of_find_compatible_node(NULL, NULL, "samsung,i2c");
int irq = irq_of_parse_and_map(np, 0);   /* Parse the first interrupt */

/* irq will be 29 */
此例中,irq_of_parse_and_map()解析了np设备节点(即i2c@13820000)的第一个中断描述<0 25 4>,计算出实际的中断号29并返回。
所以,irq_of_parse_and_map()为我们提供了一种从设备树中解析中断信息并映射中断号的机制。这对中断的申请和注册至关重要。

3 通过设备树操作LED

3.1 电路原理及手册数据

【嵌入式环境下linux内核及驱动学习笔记-(12-设备树操作函数)】_第1张图片
这里每个SOC上的GPIO引脚(GPX2_7 ,GPX1_0,GPF_4,GPF3_5)分别控制着LED2-LED5。当GPIO引脚高电平时,开关三极管导通,LED二级管发光。反之则关闭。因此,驱动程序主要是通过控制GPIO引脚输出低电平或高电平来控制LED的亮灭。

在此,因为要引入了设备树,不需要像【嵌入式环境下linux内核及驱动学习笔记-(10-内核内存管理)】3.3所述的作法,这里不需要知道每个gpio寄存器的地址及操作方法。

3.2 添加设备树的节点

在设备树源文件的根节点下添加本设备的节点(该节点中包含本设备用到的资源信息)

…/linux3.14/arch/arm/boot/dts/exynos4412-fs4412.dts

fs4412-leds {
       compatible = "fs4412,led2-5";
       led2-gpio = <&gpx2 7 0>;
       led3-gpio = <&gpx1 0 0>;
       led4-gpio = <&gpf3 4 0>;
       led5-gpio = <&gpf3 5 0>;
};

3.3 驱动程序代码

/*************************************************************************
	> File Name: led-tree.c
 ************************************************************************/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "public.h"


/*1、定义重要的变量及结构体*/
struct x_dev_t {
    struct cdev  my_dev;  //cdev设备描述结构体变量
    atomic_t have_open;   //记录驱动是否被打开的原子变量
    int led2gpio, led3gpio , led4gpio, led5gpio; //设备树gpio编号   
};

struct x_dev_t *pcdev;

/*所有驱动函数声明*/

long unlocked_ioctl (struct file *, unsigned int, unsigned long);
int open (struct inode *, struct file *);
int release (struct inode *, struct file *);

//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
    .owner = THIS_MODULE,
    .open = open,
    .release = release,
    .unlocked_ioctl = unlocked_ioctl,
};

int  led_init(struct x_dev_t *pdev){
    //从设备树里读出节点,以及gpio号
    struct device_node *dnode = of_find_node_by_path("/fs4412-led");
    if (!dnode){
        printk("driver:  fs4412-led node is failed\n");
        return -1;
    }
    pdev->led2gpio = of_get_named_gpio(dnode , "led2-gpio" , 0);
    if (pdev->led2gpio > 0){
        gpio_request_one(pdev->led2gpio , GPIOF_OUT_INIT_LOW , "led2");
    }
    pdev->led3gpio = of_get_named_gpio(dnode , "led3-gpio" , 0);
    if (pdev->led3gpio > 0){
        gpio_request_one(pdev->led3gpio , GPIOF_OUT_INIT_LOW , "led3");
    }
    pdev->led4gpio = of_get_named_gpio(dnode , "led4-gpio" , 0);
    if (pdev->led4gpio > 0){
        gpio_request_one(pdev->led4gpio , GPIOF_OUT_INIT_LOW , "led4");
    }
    pdev->led5gpio = of_get_named_gpio(dnode , "led5-gpio" , 0);
    if (pdev->led5gpio > 0){
        gpio_request_one(pdev->led5gpio , GPIOF_OUT_INIT_LOW , "led5");
    }
    return 0;
}
void led_cntl(struct x_dev_t *p , int on_off){
    gpio_set_value(p->led2gpio , on_off);
    gpio_set_value(p->led3gpio , on_off);
    gpio_set_value(p->led4gpio , on_off);
    gpio_set_value(p->led5gpio , on_off);
}

static int __init my_init(void){
    int unsucc =0;
    dev_t devno;
    int major,minor;
    pcdev = kzalloc(sizeof(struct x_dev_t), GFP_KERNEL);
    /*2、创建 devno */
    unsucc = alloc_chrdev_region(&devno , 0 , 1 , "led2");
    if (unsucc){
        printk(" creating devno  faild\n");
        return -1;
    }
    major = MAJOR(devno);
    minor = MINOR(devno);
    printk("devno major = %d ; minor = %d;\n",major , minor);

    /*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
    /*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
    cdev_init(&pcdev->my_dev , &fops);
    pcdev->my_dev.owner = THIS_MODULE;
    /*4、注册cdev结构体到内核链表中*/
    unsucc = cdev_add(&pcdev->my_dev,devno,1);
    if (unsucc){
        printk("cdev add faild \n");
        return -1;
    }
    //初始化原子量have_open为1
    atomic_set(&pcdev->have_open,1);
    
    //初始化led2
    if (led_init(pcdev)){
        printk("driver: initalization led is failed\n");
        return -1;
    }else{

        printk("the driver led2 initalization completed\n");
    }

    return 0;
}

static void  __exit my_exit(void)
{
    cdev_del(&pcdev->my_dev);
    unregister_chrdev_region(pcdev->my_dev.dev , 1);
    gpio_free(pcdev->led2gpio);
    gpio_free(pcdev->led3gpio);
    gpio_free(pcdev->led4gpio);
    gpio_free(pcdev->led5gpio);
    printk("***************the driver timer-second  exit************\n");
}

/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){
    struct x_dev_t *p = container_of(pnode->i_cdev,struct x_dev_t , my_dev);
    pf->private_data = (void *)p;
    //在open函数中对原子量have_open进行减1并检测。=0,允许打开文件,<0则不允许打开
    if (atomic_dec_and_test(&p->have_open)){
        printk("led2  driver is opened\n");
        return 0 ;
    }else{
        printk("device led2 can't be opened again\n");
        atomic_inc(&p->have_open);//原子量=-1,记得这里要把原子量加回到0
        return -1;
    }   
}
/*file_operations结构全成员函数.release的具体实现*/
int release(struct inode *pnode , struct file *pf){
    struct x_dev_t *p = (struct x_dev_t *)pf->private_data;
    printk("led2 is closed \n");
    atomic_set(&p->have_open,1);
    return 0;
}

long unlocked_ioctl (struct file *pf, unsigned int cmd, unsigned long arg){
    struct x_dev_t *p = pf->private_data;
    switch(cmd){
        case LED_ON:
            led_cntl(p,1);
            break;
        case LED_OFF:
            led_cntl(p,0);
            break;
        default:
            break;
    }
    return 0;
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");

3.4 public.h

#ifndef _H_PUBLIC_
#define _H_PUBLIC_

#include 

#define LED_ON _IO('a',1)
#define LED_OFF _IO('a',0)

#endif

3.5 test.c

/*************************************************************************
	> File Name: test.c
 ************************************************************************/
#include
#include 
#include 
#include 
#include 
#include "public.h"

int  main (int argc , char **argv){
    int fd0,fd1;

    if (argc <2){
        printf("argument is too less\n");
        return -1;
    }else{
        fd0 = open(argv[1] , O_RDONLY );
        while (fd0){

            printf("led on......\n");
            ioctl(fd0,LED_ON);
            sleep(2);
            printf("led off......\n");
            ioctl(fd0,LED_OFF);
            sleep(2);
        }
        
    }
    close(fd0);
    return 0;


}

3.6 测试步骤

1、修改设备树并编译

  • 修改linux内核目录中arch/arm/boot/dts/exynos4412-fs4412.dts,把3.2中的节点添加到设备树文件里,并做为根的子节点。
  • 即到内核的顶目录,如 /linux3.14 ,执行 编译命令: make dtbs
  • 拷贝编译后的dtb文件到开发板的下载目录中 cp arch/arm/boot/dts/exynos4412-fs4412.dtb /tftpboot -rfx
  • 编译驱动文件led-tree.c和测试程序test.c

2、运行步骤

  • 打开开发板,进入系统后
  • dmesg -c
  • clear
  • lsmod
  • insmod /drv/led-tree.ko
  • mknod /dev/led-tree c 251 0
  • chmod 777 /dev/led-tree
  • ./test.elf /dev/led-tree

3、效果
开发板上的4个led应该每1秒闪灭一次

4 设备树的编译与反编译操作

DTB(Device Tree Blob)文件是编译后的设备树文件,它以二进制格式存储。我们可以通过如下步骤将其反编译回DTS(Device Tree Source)文件,用于读取与编辑:
1、 安装Device Tree Compiler(dtc)工具。dtc是内核工具,用于编译DTS文件生成DTB文件和反编译DTB文件生成DTS文件。

sudo apt install device-tree-compiler

2、 获取目标系统的DTB文件,一般在启动分区或内核镜像中。可以通过U盘启动系统后,从proc/device-tree获取,或通过其他方式读取该文件。

3、 运行dtc工具的反编译命令,反编译DTB文件。命令格式为:
dtc -I dtb -O dts -o output.dts input.dtb
-I dtb:输入文件格式为dtb (大写的i)
-O dts:输出文件格式为dts
-o output.dts:输出的DTS文件名称
input.dtb:输入的DTB文件名称

例如,反编译名为exynos4412-odroidxu.dtb的文件,命令为:
dtc -I dtb -O dts -o exynos4412-odroidxu.dts exynos4412-odroidxu.dtb

4、 反编译成功后,会生成exynos4412-odroidxu.dts文件,这就是对应于exynos4412-odroidxu.dtb的原始DTS设备树文件,可以打开查看与编辑。

5、 编辑DTS文件后,可以再次使用DTC工具编译为DTB文件,然后替换系统原有的DTB文件,实现对设备树的修改。

你可能感兴趣的:(Linux内核与驱动,linux,嵌入式,内核与驱动,设备树,操作函数)