Linux 内核给我们提供了一系列的函数来获 取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”。

一、设备树节点数据类型

设备都是以节点的形式“挂”到设备树上的,每个节点都包含某个设备的属性信息,Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中

struct device_node {
	const char *name;                /* 节点名称 */
	const char *type;                /* 设备类型 */
	phandle phandle;
	const char *full_name;           /* 节点全名 */
	struct fwnode_handle fwnode;

	struct	property *properties;    /* 属性 */
	struct	property *deadprops;	 /* removed properties */
	struct	device_node *parent;     /* 父节点 */
	struct	device_node *child;      /* 子节点 */
    ...
};

其中 property 结构体用于描述设备树节点的属性信息,该结构体也定义在 include/linux/of.h

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

二、查找设备树节点

1、of_find_node_by_path

通过设备树的绝对路径来查找指定的节点,比如根目录下的 chosen 节点的路径为 " /chosen "

/**
 * param - path: 从哪个节点开始向下查找
 * return: 失败返回NULL,成功返回找到的节点
 */
struct device_node *of_find_node_by_path(const char *path)

示例: 获取根节点下的 alientek_led 节点

struct device_node* dtsNode;

/* 获取设备节点: /alientek_led */
chrdev.dtsNode = of_find_node_by_path("/alientek_led");
if(chrdev.dtsNode == NULL)
{
	printk("node cannot be found!\n");
	return -1;
}

三、获取设备树节点的属性值

1、of_find_property(以结构体的形式)

of_find_property 用于获取某个设备节点包含的指定属性

/**
 * param - np: 设备节点
 * param - name: 属性名
 * param - lenp: 输出型参数,表示属性值的字节数
 * return: 失败返回NULL,成功返回获取到的属性
 */
property *of_find_property(const struct device_node *np,
                           const char *name,
                           int *lenp)

示例:获取 compatible 属性

struct property* proper;

proper = of_find_property(dtsNode, "compatible", NULL);
if(proper == NULL)
{
	printk("compatible property read failed!\n");
	return -1;
}
printk("compatible: %s\n", (char*)proper->value);

2、of_property_read_string(以字符串的形式)

of_property_read_string 函数用于读取属性中字符串值。

/**
 * param - np: 设备节点
 * param - propname: 属性名
 * param - out_string: 输出型参数,表示读取到的字符串值
 * return: 成功返回 0,失败返回负值
 */
int of_property_read_string(struct device_node *np, 
                            const char *propname,
                            const char **out_string)

示例:获取 status 属性

int ret = of_property_read_string(dtsNode, "status", &str);
if(ret < 0)
{
	printk("status property read failed!\n");
	return -1;
}
printk("status: %s\n", str);

3、of_property_read_u32_array(以数组的形式)

of_property_read_u32_array 用于读取属性中 u32 类型的数组数据,比如大多数的 reg 属性就是数组数据。类似的还有 

  • of_property_read_u8_array:读取属性中 u8 类型的数组数据
  • of_property_read_u16_array:读取属性中 u16 类型的数组数据
  • of_property_read_u32_array:读取属性中 u32 类型的数组数据
  • of_property_read_u64_array:读取属性中 u64 类型的数组数据
/**
 * param - np: 设备节点
 * param - propname: 属性名
 * param - out_string: 输出型参数,表示读取到的u32类型数组
 * param - sz: 输入型参数,表示要读取的数组元素数量
 * return: 成功返回 0,以下几种情况表示读取失败
           1、-EINVAL 表示属性不存在
           2、-ENODATA 表示没有要读取的数据
           3、-EOVERFLOW 表示属性值列表太小
 */
int of_property_read_u32_array(const struct device_node *np,
                               const char *propname, 
                               u32 *out_values,
                               size_t sz)

示例:获取 reg 属性

u32 regData[14];    // 数组的大小取决于 reg 中数据个数

int ret = of_property_read_u32_array(dtsNode, "reg", regData, 10);
if(ret < 0)
{
	printk("reg property read failed!\n");
	return -1;
}

四、地址映射转换

Linux系统为了保护物理内存,防止进程直接访问物理地址修改数据,一般会在上电时初始化 MMU(内存管理单元)。我们在设备树中填入的寄存器地址是物理地址,由于 MMU 的存在,无法直接访问物理地址。

因此,我们需要先得到物理地址在Linux系统中的虚拟地址,通过虚拟地址来间接访问物理地址。

1、ioremap

ioremap 函数用于将物理地址映射到虚拟地址。该宏的定义在 arch/arm/include/asm/io.h 文件中

#define ioremap(cookie,size)		__arm_ioremap((cookie), (size), MT_DEVICE)

extern void __iomem *__arm_ioremap(phys_addr_t, size_t, unsigned int);

cookie:要映射的物理起始地址

size:要映射的内存空间大小

返回值:__iomem 类型的指针,指向映射后虚拟地址

static void __iomem* CCM_CCGR1;
static void __iomem* SW_MUX_GPIO1_IO03;
static void __iomem* SW_PAD_GPIO1_IO03;
static void __iomem* GPIO1_DR;
static void __iomem* GPIO1_GDIR;

// 一般需要搭配 of_property_read_u32_array 使用 
CCM_CCGR1 = ioremap(regData[0], regData[1]);
SW_MUX_GPIO1_IO03 = ioremap(regData[2], regData[3]);
SW_PAD_GPIO1_IO03 = ioremap(regData[4], regData[5]);
GPIO1_GDIR = ioremap(regData[6], regData[7]);
GPIO1_DR = ioremap(regData[8], regData[9]);

2、of_iomap

of_iomap  的作用和ioremap一样,但 of_iomap 更多的需要搭配设备树使用,可以通过 of_iomap 获取 reg 属性中的物理地址,并自动转换成虚拟地址。因为 reg 可能存在多个地址,所以需要通过 参数 index 指定获取哪个地址。

该函数的声明在 linux/of_address.h 目录下

void __iomem *of_iomap(struct device_node *device, int index);

device: 设备树节点

index: 要获取虚拟地址映射的段,如果reg只有一段,index设为 0 

static void __iomem* CCM_CCGR1;
static void __iomem* SW_MUX_GPIO1_IO03;
static void __iomem* SW_PAD_GPIO1_IO03;
static void __iomem* GPIO1_DR;
static void __iomem* GPIO1_GDIR;

// of_iomap 先从设备树节点获取到 reg 属性的内容,然后再映射到虚拟地址
CCM_CCGR1 = of_iomap(dtsNode, 0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsNode, 1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsNode, 2);
GPIO1_GDIR = of_iomap(dtsNode, 3);
GPIO1_DR = of_iomap(dtsNode, 4);

五、读写虚拟内存

即便是虚拟内存,一般也可以直接通过指针解引用来访问某个虚拟地址的内容,但是 Linux 内核不建议这么做,推荐使用 readl 和 writel 函数来读写内存。

1、readb / readw / readl 

读操作函数有以下几个,readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作

  • 参数 addr:要读取数据的内存地址
  • 返回值:读取到的数据。如果是 readb,一次读取 1一个字节的数据
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

2、writeb / writew / writel

写操作函数有如下几个,writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作

  • 参数 value:要写入的数据
  • 参数 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)

3、实际应用

一般读写操作函数是搭配使用的,比如我们要修改 CCM_CCGR1 寄存器中 bit 26、bit27 的配置(假设是置 11),通常思路是先将 bit 26、bit 27 清零,然后重新赋值

u32 val;

val = readl(CCM_CCGR1);    // 读取原本的值
val &= ~(3 << 26);         // 仅清除 bit 26、bit 27的配置
val |= (3 << 26);          // bit 26、bit 27赋予新值
writel(val, CCM_CCGR1);    // 再写回到原本的地址

注意:先读取原本内容的目的是,避免对其他 bit 位的配置造成影响

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐