Embedded Linux device drivers: Discovering the hardware configuration

Editor's Note: Embedded Linux has consistently ranked among the top operating systems used in embedded system design. With the rapid growth in interest in the Internet of Things (IoT), the ability of embedded Linux to serve multiple roles will prove vital in supporting diverse needs at each layer of the IoT application hierarchy. In turn, the ability of engineers to master embedded Linux systems will become critical for achieving rapid, reliable development of more sophisticated systems. In Mastering Embedded Linux Programming – Second Edition, author Chris Simmonds takes the reader on a detailed tour across the breadth and depth of this important operating system, using detailed examples to illustrate each key point.

Adapted from Mastering Embedded Linux Programming – Second Edition, by Chris Simmonds.


Chapter 9. Interfacing with Device Drivers (Continued)
By Chris Simmonds

Discovering the hardware configuration

The dummy driver demonstrates the structure of a device driver, but it lacks interaction with real hardware since it only manipulates memory structures. Device drivers are usually written to interact with hardware. Part of that is being able to discover the hardware in the first place, bearing in mind that it maybe at different addresses in different configurations.

In some cases, the hardware provides the information itself. Devices on a discoverable bus such as PCI or USB have a query mode, which returns resource requirements and a unique identifier. The kernel matches the identifier and possibly other characteristics with the device drivers, and marries them up.

However, most of the hardware blocks on an embedded board do not have such identifiers. You have to provide the information yourself in the form of a device tree or as C structures known as platform data .

In the standard driver model for Linux, device drivers register themselves with the appropriate subsystem: PCI, USB, open firmware (device tree), platform device, and so on. The registration includes an identifier and a callback function called a probe function that is called if there is a match between the ID of the hardware and the ID of the driver. For PCI and USB, the ID is based on the vendor and the product IDs of the devices; for device tree and platform devices, it is a name (an text string).

Device trees

I gave you an introduction to device trees in Chapter 3 , All About Bootloaders . Here, I want to show you how the Linux device drivers hook up with this information.

As an example, I will use the ARM Versatile board, arch/arm/boot/dts/versatile- ab.dts , for which the Ethernet adapter is defined here:

  net@10010000 {     compatible = "smsc,lan91c111";     reg = <0x10010000 0x10000>;     interrupts = <25>;};

The platform data

In the absence of device tree support, there is a fallback method of describing hardware using C structures, known as the platform data.

Each piece of hardware is described by struct platform_device , which has a name and a pointer to an array of resources. The type of the resource is determined by flags, which include the following:

  • IORESOURCE_MEM : This is the physical address of a region of memory

  • IORESOURCE_IO : This is the physical address or port number of IO registers

  • IORESOURCE_IRQ : This is the interrupt number

Here is an example of the platform data for an Ethernet controller taken from arch/arm/mach-versatile/core.c, which has been edited for clarity:

  #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,};

It has a memory area of 64 KB and an interrupt. The platform data has to be registered with the kernel, usually when the board is initialized:

  void __init versatile_init(void)  {    platform_device_register(&versatile_flash_device);    platform_device_register(&versatile_i2c_device);    platform_device_register(&smc91x_device);     [ ...]

Linking hardware with device drivers

You have seen in the preceding section how an Ethernet adapter is described using a device tree and using platform data. The corresponding driver code is in drivers/net/ethernet/smsc/smc91x.c , and it works with both the device tree and platform data. Here is the initialization code, once again edited for clarity:

  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);

When the driver is initialized, it calls platform_driver_register() , pointing to struct platform_driver , in which there is a callback to a probe function, a driver name, smc91x , and a pointer to struct of_device_id .

If this driver has been configured by the device tree, the kernel will look for a match between the compatible property in the device tree node and the string pointed to by the compatible structure element. For each match, it calls the probe function.

On the other hand, if it was configured through platform data, the probe function will be called for each match on the string pointed to by driver.name .

The probe function extracts information about the interface:

  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;[...] 
}

The calls to platform_get_resource() extract the memory and irq information from either the device tree or the platform data. It is up to the driver to map the memory and install the interrupt handler. The third parameter, which is zero in both of the previous cases, comes into play if there is more than one resource of that particular type.

Device trees allow you to configure more than just basic memory ranges and interrupts. There is a section of code in the probe function that extracts optional parameters from the device tree. In this snippet, it gets the register-io-width property:

  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);    [...]}

For most drivers, specific bindings are documented in Documentation/devicetree/bindings . For this particular driver, the information is in Documentation/devicetree/bindings/net/smsc911x.txt .

The main thing to remember here is that drivers should register a probe function and enough information for the kernel to call the probe , as it finds matches with the hardware it knows about. The linkage between the hardware described by the device tree and the device driver is through the compatible property. The linkage between platform data and a driver is through the name.

Additional reading

The following resources have further information about the topics introduced in this chapter:

  • Linux Kernel Development, 3rd edition by Robert Love.

  • Linux Weekly News , https://lwn.net/, especially the kernel news section.

  • Essential Linux Device Drivers, 1st edition (27 Mar. 2008), by Sreekrishnan Venkateswaran

Summary

Device drivers have the job of handling devices, usually physical hardware but sometimes virtual interfaces, and presenting them to user space in a consistent and useful way. Linux device drivers fall into three broad categories: character, block, and network. Of the three, the character driver interface is the most flexible and therefore, the most common. Linux drivers fit into a framework known as the driver model, which is exposed through sysfs . Pretty much the entire state of the devices and drivers is visible in /sys .

Each embedded system has its own unique set of hardware interfaces and requirements. Linux provides drivers for most standard interfaces, and by selecting the right kernel configuration, you can get a working target board very quickly. This leaves you with the non-standard components for which you will have to add your own device support.

In some cases, you can sidestep the issue by using generic drivers for GPIO, I2C, and so on, and write user space code to do the work. I recommend this as a starting point, as it gives you the chance to get familiar with the hardware without writing kernel code. Writing kernel drivers is not particularly difficult, but if you do you need to code carefully so as not to compromise the stability of the system.

I have talked about writing the kernel driver code: if you go down this route, you will inevitably want to know how to check whether or not it is working correctly and detect any bugs. I will cover that topic in Chapter 14 , Debugging with GDB .

The next chapter is all about user space initialization and the different options you have for the init program, from the simple BusyBox to the complex systems.

Reprinted with permission from Packt Publishing. Copyright © 2017 Packt Publishing


Chris Simmonds is a software consultant and trainer living in southern England. He has almost two decades of experience in designing and building open-source embedded systems. He is the founder and chief consultant at 2net Ltd, which provides professional training and mentoring services in embedded Linux, Linux device drivers, and Android platform development. He has trained engineers at many of the biggest companies in the embedded world, including ARM, Qualcomm, Intel, Ericsson, and General Dynamics. He is a frequent presenter at open source and embedded conferences, including the Embedded Linux Conference and Embedded World. You can see some of his work on the Inner Penguin blog at www.2net.co.uk.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.