Platform Devices and Drivers

Platform Devices and Drivers


https://www.kernel.org/doc/Documentation/driver-model/platform.txt
https://lwn.net/Articles/448499/

For further information, see <linux/platform_device.h>.

Busses like PCI which have discoverability built into them; any device sitting on a PCI bus can tell the system what sort of device it is and where its resources are. So the kernel can, at boot time, enumerate the devices available and everything Just Works.
Platform devices are not discoverable.
The kernel needs to provide ways to be told about the hardware that is actually present and not discoverable.

At the lowest level, every device in a Linux system is represented by an instance of struct device and associated with a struct device_driver.

struct device {
 struct device  *parent;
        ...
 void  *platform_data; /* Platform specific data, device
        core doesn't touch it */
        ...
 struct device_node *of_node; /* associated device tree node */
        ...
};

struct device_driver {
  const char  *name;
  struct bus_type  *bus;
  struct module  *owner;
  const char  *mod_name; /* used for built-in modules */
  bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
  enum probe_type probe_type;
  const struct of_device_id *of_match_table;
  const struct acpi_device_id *acpi_match_table;

  int (*probe) (struct device *dev);
  int (*remove) (struct device *dev);
  void (*shutdown) (struct device *dev);
  int (*suspend) (struct device *dev, pm_message_t state);
  int (*resume) (struct device *dev);
  const struct attribute_group **groups;

  const struct dev_pm_ops *pm;
  void (*coredump) (struct device *dev);

  struct driver_private *p;
};
The device structure contains the information that the device model core needs to model the system.
of_node is related to Open Firmware it holds the information of a device tree.

Most subsystems, however, track additional information about the devices they host. As a result, it is rare for devices to be represented by bare device structures; instead, that structure is usually embedded within a higher-level representation of the device.

The driver model interface to the pseudo platform bus is composed of platform_device and platform_driver.

Platform devices


Platform devices are usually directly addressed from a CPU bus. For ex., legacy port-based devices.
Platform devices are given a name, used in driver binding, and a list of resources such as addresses and IRQs.
A platform device is represented by struct platform_device (<linux/platform_device.h>):

struct platform_device {
 const char *name;
 u32  id;
 bool  id_auto;
 struct device dev;
 u32  num_resources;
 struct resource *resource;
 const struct platform_device_id *id_entry;
 char *driver_override; /* Driver name to force a match */
 /* MFD cell pointer */
 struct mfd_cell *mfd_cell;
 /* arch specific additions */
 struct pdev_archdata archdata;
};

Platform drivers


Platform drivers follow the standard driver model convention:
  • discovery/enumeration is handled outside the drivers
  • drivers provide probe() and remove() methods


struct platform_driver {
 int (*probe)(struct platform_device *);
 int (*remove)(struct platform_device *);
 void (*shutdown)(struct platform_device *);
 int (*suspend)(struct platform_device *, pm_message_t state);
 int (*suspend_late)(struct platform_device *, pm_message_t state);
 int (*resume_early)(struct platform_device *);
 int (*resume)(struct platform_device *);
 struct device_driver driver;
 const struct platform_device_id *id_table;
 bool prevent_deferred_probe;
};
At a minimum, the probe() and remove() callbacks must be supplied; the other callbacks have to do with power management and should be provided if they are relevant.
For ex.,

    static struct platform_driver brcmuart_platform_driver = {
      .driver  = {
           .name = "bcm7271-uart",
           .of_match_table = brcmuart_dt_ids,
      },
      .probe  = brcmuart_probe,
      .remove  = brcmuart_remove,
    };
module_platform_driver(brcmuart_platform_driver);
Note that probe() should in general verify that the specified device hardware actually exists, in common situations where the device is known not to be hot-pluggable, the probe() routine can live in an init section.

Platform drivers register themselves the normal way, platform_driver can be registered by 2 ways:
  • int platform_driver_register(struct platform_driver *drv)
  • If you are not sure if the platform device exists. As soon as this call succeeds, the driver's probe() function can be called with new devices. For ex., you can build a kernel module whose module init function is just to call platform_driver_register(struct platform_driver *drv). You can let the probe() to do the initialization work such as IRQ and memory requests. When the module is loaded, the drv->probe() function will be called to do the initialization work.
  • int platform_driver_probe(struct platform_driver *drv, int (*probe)(struct platform_device *))
  • If the device is known not to be hot-pluggable, the probe() routine can live in an init section.

The macro module_platform_driver() will pass the platform_driver structure to another macro platform_driver_register() then to __platform_driver_register() function:

int __platform_driver_register(struct platform_driver *drv,
    struct module *owner)
{
 drv->driver.owner = owner;
 drv->driver.bus = &platform_bus_type;
 drv->driver.probe = platform_drv_probe;
 drv->driver.remove = platform_drv_remove;
 drv->driver.shutdown = platform_drv_shutdown;

 return driver_register(&drv->driver);
}
EXPORT_SYMBOL_GPL(__platform_driver_register);

static int platform_drv_probe(struct device *_dev)
{
 struct platform_driver *drv = to_platform_driver(_dev->driver);
 struct platform_device *dev = to_platform_device(_dev);
 int ret;

 ret = of_clk_set_defaults(_dev->of_node, false);
 if (ret < 0)
  return ret;

 ret = dev_pm_domain_attach(_dev, true);
 if (ret != -EPROBE_DEFER) {
  if (drv->probe) {
   ret = drv->probe(dev);
   if (ret)
    dev_pm_domain_detach(_dev, true);
  } else {
   /* don't fail if just dev_pm_domain_attach failed */
   ret = 0;
  }
 }

 if (drv->prevent_deferred_probe && ret == -EPROBE_DEFER) {
  dev_warn(_dev, "probe deferral not supported\n");
  ret = -ENXIO;
 }

 return ret;
}
In drivers/base/driver.c:


/**
 * driver_find - locate driver on a bus by its name.
 * @name: name of the driver.
 * @bus: bus to scan for the driver.
 *
 * Call kset_find_obj() to iterate over list of drivers on
 * a bus to find driver by name. Return driver if found.
 *
 * This routine provides no locking to prevent the driver it returns
 * from being unregistered or unloaded while the caller is using it.
 * The caller is responsible for preventing this.
 */
struct device_driver *driver_find(const char *name, struct bus_type *bus)
{
 struct kobject *k = kset_find_obj(bus->p->drivers_kset, name);
 struct driver_private *priv;

 if (k) {
  /* Drop reference added by kset_find_obj() */
  kobject_put(k);
  priv = to_driver(k);
  return priv->driver;
 }
 return NULL;
}
EXPORT_SYMBOL_GPL(driver_find);

/**
 * bus_add_driver - Add a driver to the bus.
 * @drv: driver.
 */
int bus_add_driver(struct device_driver *drv)
{
 struct bus_type *bus;
 struct driver_private *priv;
 int error = 0;

 bus = bus_get(drv->bus);
 if (!bus)
  return -EINVAL;

 pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);

 priv = kzalloc(sizeof(*priv), GFP_KERNEL);
 if (!priv) {
  error = -ENOMEM;
  goto out_put_bus;
 }
 klist_init(&priv->klist_devices, NULL, NULL);
 priv->driver = drv;
 drv->p = priv;
 priv->kobj.kset = bus->p->drivers_kset;
 error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,
         "%s", drv->name);
 if (error)
  goto out_unregister;

 klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
 if (drv->bus->p->drivers_autoprobe) {
  if (driver_allows_async_probing(drv)) {
   pr_debug("bus: '%s': probing driver %s asynchronously\n",
    drv->bus->name, drv->name);
   async_schedule(driver_attach_async, drv);
  } else {
   error = driver_attach(drv);
   if (error)
    goto out_unregister;
  }
 }
 module_add_driver(drv->owner, drv);

 error = driver_create_file(drv, &driver_attr_uevent);
 if (error) {
  printk(KERN_ERR "%s: uevent attr (%s) failed\n",
   __func__, drv->name);
 }
 error = driver_add_groups(drv, bus->drv_groups);
 if (error) {
  /* How the hell do we get out of this pickle? Give up */
  printk(KERN_ERR "%s: driver_create_groups(%s) failed\n",
   __func__, drv->name);
 }

 if (!drv->suppress_bind_attrs) {
  error = add_bind_files(drv);
  if (error) {
   /* Ditto */
   printk(KERN_ERR "%s: add_bind_files(%s) failed\n",
    __func__, drv->name);
  }
 }

 return 0;

out_unregister:
 kobject_put(&priv->kobj);
 kfree(drv->p);
 drv->p = NULL;
out_put_bus:
 bus_put(bus);
 return error;
}

int driver_register(struct device_driver *drv)
{
 int ret;
 struct device_driver *other;

 BUG_ON(!drv->bus->p);

 if ((drv->bus->probe && drv->probe) ||
     (drv->bus->remove && drv->remove) ||
     (drv->bus->shutdown && drv->shutdown))
  printk(KERN_WARNING "Driver '%s' needs updating - please use "
   "bus_type methods\n", drv->name);

 other = driver_find(drv->name, drv->bus);
 if (other) {
  printk(KERN_ERR "Error: Driver '%s' is already registered, "
   "aborting...\n", drv->name);
  return -EBUSY;
 }

 ret = bus_add_driver(drv);
 if (ret)
  return ret;
 ret = driver_add_groups(drv, drv->groups);
 if (ret) {
  bus_remove_driver(drv);
  return ret;
 }
 kobject_uevent(&drv->p->kobj, KOBJ_ADD);

 return ret;
}
EXPORT_SYMBOL_GPL(driver_register);
With this setup, any device identifying itself as "bcm7271-uart" will be bound to this driver; no ID table is needed.

Kernel modules can be composed of several platform drivers. A platform core provides helpers to register and unregister an array of drivers:
  • int __platform_register_drivers(struct platform_driver * const *drivers, unsigned int count, struct module *owner);
  • void platform_unregister_drivers(struct platform_driver * const *drivers, unsigned int count);
If one of the drivers fails to register, all drivers registered up to that point will be unregistered in reverse order.

Device Enumeration


To bind the platform device to a driver, the device must be registered with same name which driver is registered.
For ex.,
  • Platform device drive
    
    #include <linux/module.h>
    #include <linux/kernel.h>
    //For platform drivers....
    #include <linux/platform_device.h>
    
    
    #define DEVICE_NAME "hello"
    
    MODULE_LICENSE("GPL");
    
    /* These Addresses defined by the Hardware Designer */
    #define HELLO_MEM_START_ADDRESS 0x100000
    #define HELLO_MEM_END_ADDRESS   0x1FFFFF
    
    #define HELLO_IRQNUM   6
    
    /* Specifying my resources information */
    static struct resource hello_resources[] = {
            {
                    .start          = HELLO_MEM_START_ADDRESS,
                    .end            = HELLO_MEM_END_ADDRESS,
                    .flags          = IORESOURCE_MEM,
            },
     {
                    .start          = HELLO_IRQNUM,
                    .end            = HELLO_IRQNUM,
                    .flags          = IORESOURCE_IRQ,
            }
    
     };
    
    
    static struct platform_device hello_device = {
            .name           = DEVICE_NAME,
            .id             = -1,
            .num_resources  = ARRAY_SIZE(hello_resources),
            .resource       = hello_resources,
    };
    
    int helloDev_init(void)
    {
     printk(KERN_ALERT "\n Register te hello Platform (device).... \n");
     platform_device_register(&hello_device);
     return 0;
    }
    
    void helloDev_exit(void)
    {
     platform_device_unregister(&hello_device);
     printk(KERN_ALERT "\n Ungister the hello Platform(device) ... \n");
    }
    
    module_init(helloDev_init);
    module_exit(helloDev_exit);
    
  • Platform driver
  • 
    
    
    #include <linux/init.h>
    #include <linux/module.h>
    MODULE_LICENSE("Dual BSD/GPL");
    
    #include <linux/platform_device.h>
    
    #define DRIVER_NAME "hello"
    
    static int hello_probe(struct platform_device *pdev)
    {
    
     printk(KERN_ALERT "hello_probe():\n");
    
     return 0;
    }
    
    static int hello_remove(struct platform_device *pdev)
    {
     printk(KERN_ALERT "hello_remove():\n");
     return 0;
    }
    
    static const struct of_device_id hello_of_match[] = {
     { .compatible = "hello" },
     { },
    };
    
    static struct platform_driver hello_driver =
    {
     .probe = hello_probe,
     .remove = hello_remove,
     .driver = {
       .name = DRIVER_NAME,
       .of_match_table = hello_of_match,
     }
    };
    
    static int hello_init(void)
    {
     printk(KERN_ALERT "hello_init():Hello\n");
     return platform_driver_register(&hello_driver);
    }
    
    static void hello_exit(void)
    {
     printk(KERN_ALERT "hello_exit():Bye\n");
     platform_driver_unregister(&hello_driver);
    }
    
    module_init(hello_init);
    module_exit(hello_exit);
    
    
Test:
  • sudo insmod testDrv.ko; sudo rmmod testDrv.ko
  • 
    hello_init():Hello
    hello_exit():Bye
    
  • sudo insmod testDev.ko; sudo insmod testDrv.ko; sudo rmmod testDrv.ko; sudo rmmod testDev.ko
  • 
    Register the hello Platform (device).... 
    hello_init():Hello
    hello_probe():
    hello_exit():Bye
    hello_remove():
    Ungister the hello Platform(device) ... 
    
    With the platform registered to the kernel, the probe() function inside the testDrv.ko will be called.
As a rule, platform specific (and often board-specific) setup code will register platform devices:

 int platform_device_register(struct platform_device *pdev);

 int platform_add_devices(struct platform_device **pdevs, int ndev);

In some cases, boot firmware will export tables(device tree) describing the devices that are populated on a given board.
Without such tables, often the only way for system setup code to set up the correct devices is to build a kernel for a specific target board.
In many cases, the memory and IRQ resources associated with the platform device are not enough to let the device's driver work. Board setup code will often provide additional information using the device's platform_data field to hold additional information. This board-specific structures describing devices and how they are connected to the SoC. This can include available ports, chip variants, preferred modes, default initialization, additional pin roles, and so on.

Example

Platform devices and device trees


Linux and the Device Tree


This article describes how Linux uses the device tree. An overview of the device tree data format can be found on the device tree usage page at devicetree.org.

The "Open Firmware Device Tree", or simply Device Tree (DT), is a data structure and language for describing hardware. More specifically, it is a description of hardware that is readable by an operating system so that the operating system doesn't need to hard code details of the machine.
A common set of usage conventions, called 'bindings', is defined for how data should appear in the tree to describe typical hardware characteristics.

Data Model


First and foremost, the kernel will use data in the DT to identify the specific machine, the kernel must identify the
machine during early boot so that it has the opportunity to run machine-specific fixups.
In the majority of cases, the machine identity is irrelevant, and the kernel will instead select setup code based on the machine's core CPU or SoC.
As ARM as an example, setup_arch() in arch/arm/kernel/setup.c will call setup_machine_fdt() in arch/arm/kernel/devtree.c which searches through the machine_desc table and selects the machine_desc which best matches the device tree data. The best match is determined by looking at the 'compatible' property in the root device tree node, and comparing it with the dt_compat list in struct machine_desc ( defined in arch/arm/include/asm/mach/arch.h ):

struct machine_desc {
...
 const char *const  *dt_compat; /* array of device tree 'compatible' strings */
...
 bool   (*smp_init)(void);
 void   (*fixup)(struct tag *, char **);
 void   (*dt_fixup)(void);
 long long  (*pv_fixup)(void);
 void   (*reserve)(void);/* reserve mem blocks */
 void   (*map_io)(void);/* IO mapping function */
 void   (*init_early)(void);
 void   (*init_irq)(void);
 void   (*init_time)(void);
 void   (*init_machine)(void);
 void   (*init_late)(void);
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
 void   (*handle_irq)(struct pt_regs *);
#endif
 void   (*restart)(enum reboot_mode, const char *);
};
The 'compatible' property contains a sorted list of strings starting with the exact name of the machine, followed by an optional list of boards which is compatible with.
As TI as an example, TI BeagleBoard and its successor, the BeagleBoard xM board may have the following 'compatible' property in their DT 's root respectively:

  compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
  compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";
The list is sorted from most specific (exact board) to least specific (SoC family).
Any string used in a compatible property must be documented as to what it indicates. Add documentation for compatible strings in Documentation/devicetree/bindings.
The kernel looks to see if any of the dt_compat list entries appear in the DT's compatible property. After searching the entire table of machine_descs, setup_machine_fdt() returns the 'most compatible' machine_desc based on which entry in the compatible property each machine_desc matches against. If no matching machine_desc is found, then it returns NULL.

A single machine_desc can support a large number of boards if they all use the same SoC, or same family of SoCs.


drivers/of/fdt.c:


int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,
         int depth, void *data)
{
 int l;
 const char *p;

 pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);

 if (depth != 1 || !data ||
     (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
  return 0;

 early_init_dt_check_for_initrd(node);

 /* Retrieve command line */
 p = of_get_flat_dt_prop(node, "bootargs", &l);
 if (p != NULL && l > 0)
  strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));

 /*
  * CONFIG_CMDLINE is meant to be a default in case nothing else
  * managed to set the command line, unless CONFIG_CMDLINE_FORCE
  * is set in which case we override whatever was found earlier.
  */
#ifdef CONFIG_CMDLINE
#if defined(CONFIG_CMDLINE_EXTEND)
 strlcat(data, " ", COMMAND_LINE_SIZE);
 strlcat(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#elif defined(CONFIG_CMDLINE_FORCE)
 strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#else
 /* No arguments from boot loader, use kernel's  cmdl*/
 if (!((char *)data)[0])
  strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif
#endif /* CONFIG_CMDLINE */

 pr_debug("Command line is: %s\n", (char*)data);

 /* break now */
 return 1;
}

int __init of_scan_flat_dt(int (*it)(unsigned long node,
         const char *uname, int depth,
         void *data),
      void *data)
{
 const void *blob = initial_boot_params;
 const char *pathp;
 int offset, rc = 0, depth = -1;

 if (!blob)
  return 0;

 for (offset = fdt_next_node(blob, -1, &depth);
      offset >= 0 && depth >= 0 && !rc;
      offset = fdt_next_node(blob, offset, &depth)) {

  pathp = fdt_get_name(blob, offset, NULL);
  if (*pathp == '/')
   pathp = kbasename(pathp);
  rc = it(offset, pathp, depth, data);
 }
 return rc;
}

void __init early_init_dt_scan_nodes(void)
{
 /* Retrieve various information from the /chosen node */
 of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

 /* Initialize {size,address}-cells info */
 of_scan_flat_dt(early_init_dt_scan_root, NULL);

 /* Setup memory, calling early_init_dt_add_memory_arch */
 of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

bool __init early_init_dt_scan(void *params)
{
 bool status;

 status = early_init_dt_verify(params);
 if (!status)
  return false;

 early_init_dt_scan_nodes();
 return true;
}

During early boot, the architecture setup code calls of_scan_flat_dt() several times with different helper callbacks to parse device tree data before paging is setup. The of_scan_flat_dt() code scans through the device tree and uses the helpers to extract information required during early boot.

Typically the early_init_dt_scan_chosen() helper is used to parse the chosen node including kernel parameters, most of runtime configuration data is contained in the /chosen node,

 chosen {
  bootargs = "console=ttyS0,115200 loglevel=8";
  initrd-start = <0xc8000000>;
  initrd-end = <0xc8200000>;
 };
early_init_dt_scan_root() is used to initialize the DT address space model, and early_init_dt_scan_memory() to determine the size and location of usable RAM.

On ARM, the function setup_machine_fdt() is responsible for early scanning of the device tree after selecting the correct machine_desc that supports the board.
arch/arm64/kernel/setup.c:

static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
 void *dt_virt = fixmap_remap_fdt(dt_phys);
 const char *name;

 if (!dt_virt || !early_init_dt_scan(dt_virt)) {
  pr_crit("\n"
   "Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
   "The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
   "\nPlease check your bootloader.",
   &dt_phys, dt_virt);

  while (true)
   cpu_relax();
 }

 name = of_flat_dt_get_machine_name();
 if (!name)
  return;

 pr_info("Machine model: %s\n", name);
 dump_stack_set_arch_desc("%s (DT)", name);
}
After the board has been identified, and after the early configuration data has been parsed, then kernel initialization can proceed in the normal way. At some point in this process, unflatten_device_tree() is called to convert the data into a more efficient runtime representation.
Machine-specific setup hooks will get called, like .init_early(), .init_irq() and .init_machine() hooks in the struct machine_desc for ARM:
  • .init_early() is used for any machine-specific setup that needs to be executed early in the boot process
  • .init_irq() is used to set up interrupt handling
  • .init_machine() is primarily responsible for populating the Linux device model with data about the platform. The list of devices can be obtained by parsing the DT, and allocating device structures dynamically.
    .init_machine() is responsible for registering a block of platform_devices, platform devices roughly correspond to device nodes at the root of the tree and children of simple memory mapped bus nodes.

Here is part of the device tree for the NVIDIA Tegra board:

/{
 compatible = "nvidia,harmony", "nvidia,tegra20";
 #address-cells = <1>;
 #size-cells = <1>;
 interrupt-parent = <&intc>;

 chosen { };
 aliases { };

 memory {
  device_type = "memory";
  reg = <0x00000000 0x40000000>;
 };

 soc {
  compatible = "nvidia,tegra20-soc", "simple-bus";
  #address-cells = <1>;
  #size-cells = <1>;
  ranges;

  intc: interrupt-controller@50041000 {
   compatible = "nvidia,tegra20-gic";
   interrupt-controller;
   #interrupt-cells = <1>;
   reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;
  };

  serial@70006300 {
   compatible = "nvidia,tegra20-uart";
   reg = <0x70006300 0x100>;
   interrupts = <122>;
  };

  i2s1: i2s@70002800 {
   compatible = "nvidia,tegra20-i2s";
   reg = <0x70002800 0x100>;
   interrupts = <77>;
   codec = <&wm8903>;
  };

  i2c@7000c000 {
   compatible = "nvidia,tegra20-i2c";
   #address-cells = <1>;
   #size-cells = <0>;
   reg = <0x7000c000 0x100>;
   interrupts = <70>;

   wm8903: codec@1a {
    compatible = "wlf,wm8903";
    reg = <0x1a>;
    interrupts = <347>;
   };
  };
 };

 sound {
  compatible = "nvidia,harmony-sound";
  i2s-controller = <&i2s1>;
  i2s-codec = <&wm8903>;
 };
};
The kernel starts at the root of the tree and looks for nodes that have a 'compatible' property.
  • The /chosen, /aliases, and /memory nodes are informational nodes that don't describe devices
  • It can be assumed that any node at the root of the tree is either directly attached to the processor bus, or is a miscellaneous system device that cannot be described any other way.
For each of these nodes, Linux allocates and registers a platform_device, which in turn may get bound to a platform_driver.
Linux models devices with the assumption that devices are children of a bus controller. The devices which do not require a specific type of parent device are platform_devices, which will happily live at the base of the Linux /sys/devices tree. Therefore, if a DT node is at the root of the tree, then it really probably is best registered as a platform_device.

/sys/devices is part of the sysfs virtual filesystem; it presents devices as directories arranged in a hierarchy.
Each device (or "thing") in the Linux device model can have a "parent" device. When a device represents a piece of hardware, the parent often represents a connection to that device. This parent knows how to interpret the "address" of the device, and how to send instructions to that address, or how to transfer data to or from that address. When a device represents a piece of hardware, the primary service it needs from a parent device is communication with the CPU.

Linux BSP calls of_platform_populate(NULL, NULL, NULL, NULL) to kick off discovery of devices at the root of the tree.
For Linux DT support, the generic behavior is for child devices to be registered by the parent's device driver at driver .probe() time. Therefore, an i2c bus device driver will register a i2c_client for each child node.


The biggest disadvantage of platform devices is the need to instantiate these devices in code.

A device tree is a description of a specific system's hardware configuration.
The device tree comes in three forms:
  • A text file (*.dts) — “source”
  • A device tree looks like the following form:
    
    /dts-v1/;
    / {
      #address-cells = <1>;
      #size-cells = <1>;
      compatible = "xlnx,zynq-zed";
      interrupt-parent = <&gic>;
      model = "Xillinux for Zedboard";
      aliases {
        serial0 = &ps7_uart_1;
      } ;
      chosen {
        bootargs = "consoleblank=0 root=/dev/mmcblk0p2 rw rootwait earlyprintk";
        linux,stdout-path = "/axi@0/uart@E0001000";
      };
    
      cpus {
    
          [ ... CPU definitions ... ]
    
       } ;
    
      ps7_axi_interconnect_0: axi@0 {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "xlnx,ps7-axi-interconnect-1.00.a", "simple-bus";
        ranges ;
    
          [ ... Peripheral definitions ... ]
    
      } ;
    } ;
    
    Each curly bracket is represented as a directory having the name of the string coming just before it.
    • the version declaration
    • the device tree starts with a slash
    • From the DTS compiler’s point of view
    • These curly brackets enclose deeper hierarchies in the tree. The string before the colon is the label, which is possibly referred to within the DTS file, but doesn’t appear in the DTB.
    • Kernel walk down this tree, and grab the desired information from certain paths
  • A binary blob (*.dtb) — “object code”
  • In a normal flow, the DTS file is edited and compiled into a DTB file using a special compiler which comes with the Linux kernel sources. The device tree compiler is under scripts/dtc and can be downloaded and built separately with
    
    $ git clone git://git.kernel.org/pub/scm/utils/dtc/dtc.git dtc
    $ cd dtc
    $ make
    
    To create the blob file my-tree.dtb from my-tree.dts, the path to the cross compiler must be set before doing it:
    
    $ scripts/dtc/dtc -I dts -O dtb -o /path/to/my-tree.dtb /path/to/my-tree.dts
    
  • A file system in a running Linux’ /proc/device-tree directory — “debug and reverse engineering information”
  • 
    # hexdump -C '/proc/device-tree/#size-cells'
    00000000  00 00 00 01                                       |....|
    00000004
    
    # cat '/proc/device-tree/axi@0/compatible'
    xlnx,ps7-axi-interconnect-1.00.asimple-bus
    
The blob of device tree is passed to the kernel at boot time; the kernel then reads through it to learn about what kind of system it is actually running on so that the kernel can kick off the right driver to handle it . With luck, device trees will abstract the differences between systems into boot-time data and allow generic kernels to run on a much wider variety of hardware.
If the device tree includes a platform device, that device will be instantiated and matched against a driver. The memory-mapped I/O and interrupt resources will be marshalled from the device tree description and made available to the device's probe() function in the usual way.
Device names appearing in the device tree (in the "compatible" property) tend to take a standardized form which does not necessarily match the name given to the driver in the Linux kernel, the kernel provides an of_device_id structure which can be used for this purpose.
include/linux/mod_devicetable.h:


/*
 * Struct used for matching a device
 */
struct of_device_id {
        char    name[32];
        char    type[32];
        char    compatible[128];
        const void *data;
};


static const struct of_device_id brcmuart_dt_ids[] = {
 { .compatible = "brcm,bcm7271-uart" },
 { }
};
When the platform driver is declared, it stores a pointer to this table in the driver substructure:

static struct platform_driver bcmuart_platform_driver = {
  /* ... */
  .driver = {
      .name = "bcm7271-uart",
      .of_match_table = brcmuart_dt_ids
  }
};
The driver can also declare the ID table as a device table to enable autoloading of the module as the device tree is instantiated:

    MODULE_DEVICE_TABLE(of, my_of_ids);
In include/linux/module.h:

#ifdef MODULE
/* Creates an alias so file2alias.c can find device table. */
#define MODULE_DEVICE_TABLE(type, name)     \
extern const typeof(name) __mod_##type##__##name##_device_table  \
  __attribute__ ((unused, alias(__stringify(name))))
#else  /* !MODULE */
#define MODULE_DEVICE_TABLE(type, name)
#endif
  1. Driver for each device exposes its information using the API MODULE_DEVICE_TABLE. Each device has a unique type and device name.
  2. At compilation time, the build process extracts this information out of the driver and builds a device table
  3. When the device is plugged in, the kernel checks this device table to see if any driver is available for the particular device type/name. If yes then it loads that driver and initializes the device.
Drivers expecting platform data should check the dev.platform_data pointer :
  • If there is a non-null value there, the driver has been instantiated in the traditional way and device tree does not enter into the picture, the platform_data should be used in the usual way.
  • If, however, the driver has been instantiated from the device tree code, the platform_data pointer will be null, indicating that the information must be acquired from the device tree directly.
  • In this case, the driver will find a device_node pointer in the platform devices dev.of_node field. The various device tree access functions (of_get_property(), primarily) can then be used to extract the needed information from the device tree.
In drivers/base/platform.c:


struct device platform_bus = {
 .init_name = "platform",
};
EXPORT_SYMBOL_GPL(platform_bus);

struct bus_type platform_bus_type = {
 .name  = "platform",
 .dev_groups = platform_dev_groups,
 .match  = platform_match,
 .uevent  = platform_uevent,
 .pm  = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);


int platform_device_register(struct platform_device *pdev)
{
 device_initialize(&pdev->dev);
 arch_setup_pdev_archdata(pdev);
 return platform_device_add(pdev);
}
EXPORT_SYMBOL_GPL(platform_device_register);

int platform_add_devices(struct platform_device **devs, int num)
{
 int i, ret = 0;

 for (i = 0; i < num; i++) {
  ret = platform_device_register(devs[i]);
  if (ret) {
   while (--i >= 0)
    platform_device_unregister(devs[i]);
   break;
  }
 }

 return ret;
}
EXPORT_SYMBOL_GPL(platform_add_devices);

static const struct platform_device_id *platform_match_id(
   const struct platform_device_id *id,
   struct platform_device *pdev)
{
 while (id->name[0]) {
  if (strcmp(pdev->name, id->name) == 0) {
   pdev->id_entry = id;
   return id;
  }
  id++;
 }
 return NULL;
}

/**
 * platform_match - bind platform device to platform driver.
 *
 * Platform device IDs are assumed to be encoded like this:

  "<name><instance>"

  where 
    <name> is a short description of the type of device, like "pci" or "floppy"
    <instance> is the enumerated instance of the device, like '0' or '42'.  

  Driver IDs are simply "<name>".  
  So, extract the <name> from the platform_device structure, and compare it against the name of the driver. 
 */
static int platform_match(struct device *dev, struct device_driver *drv)
{
 struct platform_device *pdev = to_platform_device(dev);
 struct platform_driver *pdrv = to_platform_driver(drv);

 /* When driver_override is set, only bind to the matching driver */
 if (pdev->driver_override)
  return !strcmp(pdev->driver_override, drv->name);

 /* Attempt an OF style match first */
 if (of_driver_match_device(dev, drv))
  return 1;

 /* Then try ACPI style match */
 if (acpi_driver_match_device(dev, drv))
  return 1;

 /* Then try to match against the id table */
 if (pdrv->id_table)
  return platform_match_id(pdrv->id_table, pdev) != NULL;

 /* fall-back to driver name match */
 return (strcmp(pdev->name, drv->name) == 0);
}

static struct platform_device * __init
early_platform_match(struct early_platform_driver *epdrv, int id)
{
 struct platform_device *pd;

 list_for_each_entry(pd, &early_platform_device_list, dev.devres_head)
  if (platform_match(&pd->dev, &epdrv->pdrv->driver))
   if (pd->id == id)
    return pd;

 return NULL;
}

  • The driver can provide a way for the platform bus code to bind actual devices to the driver by using platform_driver.id_table:
    
        struct  platform_device_id  {
          char name[PLATFORM_NAME_SIZE];
          kernel_ulong_t driver_data;
        };
    
    If an ID table is present, the platform bus code will scan through it to find a device whose name matches the name in an ID table entry.

Legacy Drivers: Device Probing

Some systems do not have the bus-level support for dynamic configuration (PCI, USB), or device tables provided by the boot firmware . There are some APIs to support such legacy drivers.(Avoid using these calls except devices are not hot-pluggable.)
  • struct platform_device *platform_device_alloc( const char *name, int id)
  • You can use platform_device_alloc() to dynamically allocate a device then initialize with resources and platform_device_register().
  • struct platform_device *platform_device_register_simple( const char *name, int id, struct resource *res, unsigned int nres)
  • This is a one-step call to allocate and register a device.

Device Naming and Driver Binding

The platform_device.dev.bus_id is the canonical name for the devices. It's built from two components:
  • platform_device.name
  • This is also used to for driver matching.
  • platform_device.id
  • This is the device instance number, or else "-1" to indicate there's only one.

留言

熱門文章