嵌入式Linux设备驱动程序:发现硬件配置
Embedded Linux device drivers: Discovering the hardware configuration
Interfacing with Device Drivers
了解硬件配置
虚拟驱动程序演示了一个设备驱动程序的结构,但是由于它只操作内存结构,因此它缺乏与实际硬件的交互。设备驱动程序通常是用来与硬件交互的。部分原因是能够在第一时间发现硬件,记住它可能位于不同配置的不同地址。
在某些情况下,硬件本身提供信息。PCI或USB等可发现总线上的设备具有查询模式,该模式返回资源需求和唯一标识符。内核将标识符和可能的其他特征与设备驱动程序匹配,并将它们结合起来。
然而,嵌入式板上的大多数硬件块没有这样的标识符。您必须自己以设备树的形式或称为平台数据的C结构来提供信息。
在Linux的标准驱动程序模型中,设备驱动程序向相应的子系统注册:PCI、USB、开放固件(设备树)、平台设备等等。注册包括一个标识符和一个称为探测函数的回调函数,如果硬件ID和驱动程序的ID匹配,则调用该函数。对于PCI和USB,ID基于供应商和设备的产品ID;对于设备树和平台设备,它是一个名称(文本字符串)。
设备树
我在第三章中介绍了设备树,都是关于引导程序的。在这里,我想向您展示Linux设备驱动程序是如何与这些信息连接起来的。
作为一个例子,我将使用ARM多功能板,arch/ARM/boot/dts/Versatile-ab.dts公司,以太网适配器在此处定义:
net@10010000 { compatible = "smsc,lan91c111"; reg = <0x10010000 0x10000>; interrupts = <25>;};
平台数据
在没有设备树支持的情况下,有一种使用C结构描述硬件的后备方法,称为平台数据。
每个硬件由struct platform_device描述,它有一个名称和一个指向资源数组的指针。资源的类型由标志确定,这些标志包括:
IORESOURCE_MEM:这是内存区域的物理地址
IORESOURCE_IO:这是IO寄存器的物理地址或端口号
IORESOURCE_IRQ:这是中断号
下面是一个以太网控制器的平台数据示例,该数据取自arch/arm/mach versatile/core.c,为清晰起见,对其进行了编辑:
#define VERSATILE_ETH_BASE 0x10010000 #define IRQ_ETH 25 static struct resource smc91x_resources[] = { [0] = { .start = VERSATILE_ETH_BASE, .end = VERSATILE_ETH_BASE + SZ_64K - 1, .flags = IORESOURCE_MEM,},
[1] = { .start = IRQ_ETH, .end = IRQ_ETH, .flags = IORESOURCE_IRQ,},
}; static struct platform_device smc91x_device = { .name = "smc91x", .id = 0, .num_resources = ARRAY_SIZE(smc91x_resources), .resource = smc91x_resources,};
它有一个64KB的内存区和一个中断。平台数据必须在内核中注册,通常在板初始化时:
void __init versatile_init(void) { platform_device_register(&versatile_flash_device); platform_device_register(&versatile_i2c_device); platform_device_register(&smc91x_device); [ ...]
将硬件与设备驱动程序链接
在上一节中,您已经看到了如何使用设备树和平台数据来描述以太网适配器。相应的驱动程序代码在drivers/net/ethernet/smsc/smc91x.c中,它同时处理设备树和平台数据。以下是初始化代码,为清晰起见再次编辑:
static const struct of_device_id smc91x_match[] = { { .compatible = "smsc,lan91c94", }, { .compatible = "smsc,lan91c111", }, {}, }; MODULE_DEVICE_TABLE(of, smc91x_match); static struct platform_driver smc_driver = {.probe = smc_drv_probe,
.remove = smc_drv_remove,
.driver ={ .name = "smc91x", .of_match_table = of_match_ptr(smc91x_match), }, }; static int __init smc_driver_init(void) { return platform_driver_register(&smc_driver); } static void __exit smc_driver_exit(void) { platform_driver_unregister(&smc_driver); } module_init(smc_driver_init); module_exit(smc_driver_exit);
当驱动程序初始化时,它调用platform_driver_register(),指向struct platform_driver,其中有一个对探测函数的回调、一个驱动程序名smc91x和一个指向“设备”id的struct的指针。
如果这个驱动程序是由设备树配置的,内核将在设备树节点中的compatible属性和compatible structure元素所指向的字符串之间寻找匹配。对于每个匹配,它调用probe函数。
另一方面,如果它是通过平台数据配置的,则将为指向的字符串上的每个匹配调用probe函数驱动程序名.
probe函数提取有关接口的信息:
static int smc_drv_probe(struct platform_device *pdev) { struct smc91x_platdata *pd = dev_get_platdata(&pdev->dev); const struct of_device_id *match = NULL; struct resource *res, *ires; int irq;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); ires = platform_get_resource(pdev, IORESOURCE_IRQ, 0); [...] addr = ioremap(res->start, SMC_IO_EXTENT); irq = ires->start;[...]
}
对platform_get_resource()的调用从设备树或平台数据中提取内存和irq信息。由驱动程序映射内存并安装中断处理程序。第三个参数在前面的两种情况下都为零,如果有一个以上的特定类型的资源,则会起作用。
设备树允许您配置的不仅仅是基本内存范围和中断。probe函数中有一段代码从设备树中提取可选参数。在此代码段中,它获取register io width属性:
match = of_match_device(of_match_ptr(smc91x_match), &pdev->dev); if (match) { struct device_node *np = pdev->dev.of_node; u32 val; [...] of_property_read_u32(np, "reg-io-width", &val); [...]}
对于大多数驱动程序,在Documentation/deviceree/bindings中记录了特定的绑定。对于这个特定的驱动程序,信息在Documentation/deviceree/bindings/net/smsc911x.txt中。
这里要记住的主要一点是,驱动程序应该注册一个探测函数和足够的信息,以便内核调用探测,因为它发现与它所知道的硬件匹配。设备树描述的硬件和设备驱动程序之间的链接是通过compatible属性实现的。平台数据和驱动程序之间的链接是通过名称实现的。
摘要
设备驱动程序的工作是处理设备,通常是物理硬件,但有时是虚拟接口,并以一致和有用的方式将它们呈现给用户空间。Linux设备驱动程序分为三大类:字符、块和网络。在这三种接口中,字符驱动接口是最灵活的,因此也是最常见的。Linux驱动程序适合于一个称为驱动程序模型的框架,该模型通过sysfs公开。在/sys中几乎可以看到设备和驱动程序的整个状态。
每个嵌入式系统都有自己独特的硬件接口和需求集。Linux为大多数标准接口提供了驱动程序,通过选择正确的内核配置,您可以很快得到一个工作的目标板。这就给您留下了非标准组件,您必须添加自己的设备支持。
在某些情况下,您可以通过使用GPIO、I2C等的通用驱动程序来回避这个问题,并编写用户空间代码来完成这项工作。我建议将此作为一个起点,因为它让您有机会在不编写内核代码的情况下熟悉硬件。编写内核驱动程序并不是特别困难,但是如果你真的这么做了,你需要小心编码,以免损害系统的稳定性。
我已经讨论过如何编写内核驱动程序代码:如果你沿着这条路走下去,你将不可避免地想知道如何检查它是否正常工作并检测出任何错误。