LDM的BDD架构,使得DTB成为最合适的设备信息记录文件。
OF_MATCH_STYLE是推荐的platform_driver的工作方式。
当一个PDEV需要和PDRV配对时,会检查compatible权项。
我们知道,一个驱动模块配置了OF_MATCH_TABLE,在这个表里登记了OF_DEVICE_ID的条目。
struct of_device_id{
char compatible[128];
const void *data;
}
来看一个例子,
static const struct of_device_id imx_uart_dt_ids[] = {
{
.compatible = "fsl, imx6q-uart",
},
{
.compatible = "fsl, imx1-uart",
},
{
.compatible = "fsl, imx21-uart",
},
{}
};
static struct platform_driver serial_imx_pltdrv = {
.driver = {
.name = "imx-uart",
.of_match_table = imx_uart_dt_ids,
},
};
MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);
我们定义了全局的表格,OF_DEVICE_ID_MATCH_TABLE,并将这个表格配置给了我们定义的PDRV。
最后,我们通过内核提供的宏MODULE_DEVICE_TABLE将这个表格注册到内核中,从而使DTB能够使用。
probe中是如何使用DTB中的权项的呢?
我们知道,dev中有个成员of_node,这是一个句柄,指向device_node。
所以我们常见到这样的引用形式,
struct device_node* devnp;
devnp = pdev->dev.of_node;
resp = of_get_property(devnp, "fsl,dte-mode", NULL);
通过DEV中的成员of_node,找到DTB中的DEVICE_NODE。
of_get_property函数用来获取property。
对于通用的RESOURCE,通常不需要手工调用of_get_property函数。
例如,对于MEM,IRQ,DMA,等等,只需要调用
platform_get_mem,
platform_get_irq,
platform_get_dma即可。
因为他们在内部已经首先就判断了,是否需要从OF_DEVICE_NODE 中获取资源。
例如
int platform_get_irq(struct platform_device *pdev, unsigned int num)
{
int ret;
struct resource *rp;
if(pdev->dev.of_node){
ret = of_irq_get(pdev->dev.of_node, num);
if(ret > 0)
return ret;
}
rp = platform_get_resource(pdev, IORESOURCE_IRQ, num);
return (rp? r->start : -ENXIO);
}
从中我们可以看到,函数首先就尝试从OF_NODE中获取资源。
但是对于除了MEM,IRQ,DMA之外的资源,我们仍然需要使用OFAPI来获取资源。
在内核启动的过程中,会解析DTB。
of_scan_flat_dt函数,会遍历DTB,并利用辅助函数来获取需要的信息。
例如:
early_init_dt_scan_chosen函数,会解析DTB中的chosen节点,并得到bootargs等参数。
early_init_dt_scan_memory函数,会决定可用的MEM的大小,以及地址范围。
在init_machine函数中,会调用of_platform_populate函数。
of_platform_populate这个函数会遍历DTB中的设备节点,并把匹配的设备节点转换成PDEV,然后注册到内核中。
在BDD框架下,PDEV注册时,会触发BUS的match,进而触发PDRV的加载过程。
当DTB中所有的PDEV都被注册成功,所有需要的PDRV也注册成功后,硬件平台也就初始化了。
DTB是由DTC编译而来,DTC将DTS编译成DTB。
对于系统移植人员而言,只需要关注DTS即可。
连接系统移植工程师和驱动开发工程师的桥梁,就是说明文档DTBinding。
DTBinding中具体规定了DTS中的每个权项应该怎么定义其含义。从而给驱动开发工程师一个指导文件,让驱动开发工程师知道如何使用权项。
这些文档位于/Documentation/devicetree/bindings目录下。又有很多子目录,例如/Documentation/devicetree/bindings/i2c/i2c-xiic.txt,这个文档描述了Xilinx的IIC控制器。
下面我们来简单介绍常用的DTS中的语法与权项。
1)RootCompatible。它用来定义整个板子的兼容性,内核在启动时,根据根兼容性来选取合适的machine_desc结构体。
2)NodeCompatible。它用来定义根节点下的其他子节点的兼容性。子节点兼容性用来在match时,和匹配的PDRV绑定。
3)NodeName。它用来定义节点的名称。例如:cpu@0,cpu@1,serial@101f0000,serial@101f2000。等等。
左侧表示设备的功能,右侧表示设备所在的总线上的基地址。
4)NodeLabel。它用来对NodeName打上标签。之后,通过&NodeLabel的方式来引用Node。
也就是说: &NodeLabel = NodeName
例如:
gpio3:gpio@48057000{
compatible = "ti,omap4-gpio" ;
reg = <0x48057000, 0x200>;
interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
};
在这个例子中,NodeName是gpio@48057000,NodeLabel是gpio3。
Label命名时,用DeviceType+Index的方式进行。如上例gpio3.
5)ALIASESNode。用来为NodeName指定NameAlias。注意,NameAlias不是NodeLabel。Label在使用时,需要加&。而Alias使用时,不需要加&。
aliases{
spi0 = &spi_0;
iic0 = &iic_0;
};
6)CHOSENNode。用来传递启动参数。例如:
chosen{
bootargs = "console=ttySAC2, 115200"
};
7)RegionAddress。通过ranges 和reg进行地址编码映射。
例如:
/{
compatible = "acme,coyotes-revenge"
#address-cells = <1>;
#size-cells = <1>;
uart0:serial@101f0000{
compatible = "arm,pl011";
reg = <0x101f0000 0x1000>;
interrupts = <1 0>;
};
};
这个例子中,uart0位于根节点中,所以它使用的是根节点的总线地址空间。根节点的总线地址空间的地址描述向量,是由一个ADDRCELL和一个SIZECELL构成的二维向量。第一列,表示BASEADDR,第二列,表示SIZE。
再例如:
/{
compatible = "acme,coyotes-revenge"
#address-cells = <1>;
#size-cells = <1>;
external_bus{
#address-cells = <2>;
#size-cells = <1>;
ranges = <
0 0 0x10100000 0x10000
1 0 0x10160000 0x10000
2 0 0x30000000 0x1000000
>;
eth0:ethernet@0,0{
compatible = "smc, smc91c111";
reg = <0 0 0x1000>;
interrupts = <5 2>;
};
i2c0:i2c@1,0{
compatible = "acme, a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
interrupts = <6 2>;
rtc0:rtc@58{
compatible = "maxim, ds1338"
reg = <58>;
interrupt = <7 3>;
};
};
flash0:flash@2,0{
compatible = "samsung, k8f1315ebm", "cfi-flash";
reg = <2 0 0x400000>;
};
};
};
这个例子中,经过了多级地址映射。
首先是external_bus,它是一个桥接器或者总线控制器。它位于根节点下,所以访问它时,是在根节点的总线地址空间进行的。它的上游总线接口,响应根节点的总线地址。而下游总线接口,则把对应的上游总线地址进行地址转换后,变成下游总线接口的合法地址。
我们看到,它的下游总线地址的描述向量,是由两个ADDRCELL和一个SIZECELL构成的三维向量来描述。驱动程序负责解析并转换这个三维向量。
ranges定义了下游总线地址的转换规则。前两列是下游地址的基地址DOWNBASEADDR,第三列是上游地址的基地址UPBASEADDR,第四列是地址域的大小SIZE。
然后是挂载在external_bus上的eth0和flash0。他们接收的上游总线,是external_bus。但是他们并没有下游总线。所以他们不进行地址转换。
他们的节点中,定义了权项reg,来指定他们怎么使用下游总线的地址空间。
再看挂载在external_bus上的i2c0。它是一个IIC总线控制器。它接收的上游总线,是external_bus。但是它有下游总线。所以它要进行地址转换。它的节点中,定义了下游总线地址的描述向量,是由一个ADDRCELL构成的单地址单元。每一个子节点,不能使用地址区间,而只能使用某个确定的单地址。
RTC0,是挂载在I2C0的下游总线上的设备,所以它只有上游总线地址,并没有下游总线地址。RTC0接受一个上游总线的单地址。
8)Interrupts。
/{
compatible = "acme,coyotes-revenge"
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
intc: interrupt-controller@10140000{
compatible = "arm,pl190";
reg = <0x10140000 0x1000>;
interrupt-controller;
#interrupt-cells = <2>;
};
};
在根节点下,定义了一个INTC,用权项interrupt-controller表示。并定义了interrupt-cells,说明中断描述条目是一个二维向量。
在根节点下,指定了根节点的interrupt-parent权项,就是我们定义的INTC这个节点。
当节点没有指定interrupt-parent权项时,默认从其父节点继承。
9)GPIO。
gpio0:gpio@0{
compatible = "fsl,imx28-gpio";
interrupts = <127>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
gpio1:gpio@1{
compatible = "fsl,imx28-gpio";
interrupts = <126>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
sd0:sdhci@c8000400{
status = "okay";
cd-gpios = <&gpio0 1 0>;
wp-gpios = <&gpio0 2 0>;
power-gpios = <&gpio0 3 0>;
bus-width = <4>;
};
定义了两个GPIOCTRL。gpio0和gpio1。
他们占用了父中断号127和126。同时他们自己也是中断控制器。并定义了自己的中断描述条目。如果他们的子中断节点发出了合法的中断给他们,他们会分别向自己的父中断节点发出属于自己的中断号的中断。对于gpio0是127,对于gpio1是126。
他们各自都是gpioctrl。并定义了自己的GPIO描述条目。
定义了一个sd0,它是SD控制器,它使用了GPIO资源。
引用的GPIO资源的描述向量,是一个三维向量,第一列,是GPIOCTRL的PHandle,第二列和第三列是这个GPIOCTRL所定义的GPIO描述条目,这是一个二维向量。
10)CLOCK。与GPIO类似。
在驱动中,只需要使用OF_API,就可以读取DTB中的权项,并转换成RESOURCE 。
在drivers/of目录下,存放这OFAPI的实现代码。
例如:
of_find_compatible_node,
of_property_read_u32_array,
of_property_read_string_index,
of_property_read_bool,
of_iomap,
of_address_to_resource,
irq_of_parse_and_map,
of_find_device_by_node,
有了以上基础知识,我们来看看DTB的引入,对原有的BSP带来了哪些改变。
由于DTB在每次内核启动时动态创建DEVICE_NODE,所以对于设备的修改,只需要修改DTS,并重新生成DTB,而不用重新编译内核。内核总是从DTB中获取设备信息,所以DTB的修改,仅仅是使得内核的遍历过程有些改变,但是内核代码不用重新编译。
之前设备和驱动的绑定,都是在init函数中手工指定,但是现在内核代管了DTB的遍历,所以驱动只需要提供Callback给内核就可以了。当内核遍历到匹配的设备,需要调用Callback的时候,再调用Callback。一个Callback,所有设备都可以用的上。这就是驱动不需要每次都修改的原因。
这个核心的Callback,就是platform_driver中的probe函数。PDRV.probe函数。
内核会传入一个PDEV的句柄给PDRV.probe。通过OFAPI,可以获取PDEV的硬件资源信息。
在模块加卸载时,驱动模块的动作起点是module_init指定的init函数。但是在OFSTYLE下的设备加卸载,驱动的动作起点是PDRV.probe函数。
所以,模块加卸载时,init函数只负责初始化和注册PDRV,而设备加卸载则推迟到内核遍历DTB的时候,由PDRV.probe负责初始化和注册PDEV。
这样就实现了DRIVER和DEVICE的分离。init函数只需要负责注册驱动,而不需要负责注册设备。probe函数来负责注册设备。
大多数场合,PDRV.probe更多的是扮演一个BOOT的角色。它是驱动一切动作的起点。
例如,对于一个Multi-AccessInterface-Device,在probe里,对DEVICE的其他身份进行初始化和注册。例如一个CDEV,它同时也是一个PDEV,那么在probe函数中,就需要初始化CDEV,并注册到内核中,之后,用户就能够看到这个CDEV,并使用这个CDEV。