A Tutorial on the Device Tree

A TUTORIAL ON THE DEVICE TREE

Part I

Picture this: 
The bootloader has just copied the Linux kernel into the processor’s SDRAM. It then jumps to the kernel’s entry point. The kernel is now just like any bare-metal application running on a processor. It needs to configure the processor. It needs to set up virtual memory. It needs to print something to the console. 

But how? All these operations are carried out by writing to registers, but how does the Linux kernel know their addresses? How does it know how many cores it can run on? How much memory it can access?

The straightforward solution is platform-specific boot routines in the kernel’s sources, which are enabled by kernel configuration parameters. This is fine for everything that is usually fixed, such as the internal registers on an x86 processor, or the access of the BIOS on a PC. But when it comes to things that tend to change, for example the PCI/PCIe peripherals on a PC computer, it’s desirable to let the kernel learn about them in run-time.

The ARM architecture has become a major headache in the Linux community: Even though the processors share the same compiler and many functionalities, each embodiment (i.e. chip) has its own addresses for the registers, and a slightly different configuration. On top of that, each board has its own set of external components. The result is a wild forest of header files, patches and special configuration parameters in the kernel tree, each combination matching a specific board with a specific chip containing an ARM processor. In short, it has turned out to be an ugly and unmaintainable pile of hacks which nobody is really fond of.

On top of that, each kernel binary is compiled for a specific chip on a specific board, which is more or less like compiling the kernel for each PC motherboard on the market. So there was a wish to compile the kernel for all ARM processors, and let the kernel somehow detect its hardware and apply the right drivers as needed. Exactly as it does with a PC.

But how? On a PC, the initial registers are hardcoded, and the rest of the information is supplied by the BIOS. So it’s easy to auto-detect your hardware when another piece of software tells you what you have. ARM processors don’t have a BIOS. The Linux kernel has only itself to trust.

So the chosen solution was a device tree, also referred to as Open Firmware (abbreviated OF) or Flattened Device Tree (FDT). This is essentially a data structure in byte code format (that is, not human-readable) which contains information that is helpful to the kernel when booting up. The boot loader copies that chunk of data into a known address in the RAM before jumping to the kernel’s entry point.

I defined the device tree somewhat vaguely, but it’s exactly how things are: Even though there are strict conventions (which isn't always followed completely), there is no rigid rule for what can go into the device tree and where it must be put. Any routine in the kernel may look up any parameter in any path in the device tree. It's the choice of the programmer what is parametrized, and where the parameter is best placed in the tree.

Adopting the standard tree structure allows using a convenient API for fetching specific data. For example, there is a clear and cut convention for how to define peripherals on the bus, and an API for getting the essential information the driver needs: Addresses, interrupts and custom variables. More about that later.

To most of us, the device tree is where we inform the kernel about a specific piece of hardware (i.e. PL logic) we’ve added or removed, so that the kernel can kick off the right driver to handle it (or refrain from doing so, if the hardware was removed). This is also where specific information about the hardware is conveyed.

Compiling the device tree

The device tree comes in three forms:
  • A text file (*.dts) — “source”
  • A binary blob (*.dtb) — “object code”
  • A file system in a running Linux’ /proc/device-tree directory — “debug and reverse engineering information”
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 can be downloaded and built separately with


  $ git clone git://www.jdl.com/software/dtc.git dtc
$ cd dtc
$ make
  
but I’ll assume below that the kernel source’s dtc is used.
Otherwise, install the device-tree-compiler package:

  sudo apt-get install device-tree-compiler
  
This package includes a tool fdtdump which can be used to decode the DTB file.

The dtc compiler is a binary application, which is compiled to run on the host’s platform (i.e. it’s not cross compiled).
If the kernel hasn’t been compiled on the host, there’s a need to at least compile the DTS compiler:

  • set up a configuration for the kernel
  • Copy a c.onfig in the kernel tree’s root directory or generate a new .config:
    
            $ make ARCH=$(ARCH) menuconfig
            
  • generate the DTS compiler
  • 
            $ make ARCH=$(ARCH) scripts
            

The syntax of the device tree’s language is described here.
Note that this language doesn’t execute anything, but like XML, it’s just a syntax to organize data.

The compilation from DTS to DTB:

  1. Change directory to the Linux kernel source tree’s root
  2. 
            $ cd /usr/src/kernels/linux/
            
  3. Build
  4. The following creates the blob file my-tree.dtb.
    
            $ scripts/dtc/dtc -I dts -O dtb -o /path/to/my-tree.dtb /path/to/my-tree.dts
            
    If the path to the cross compiler hasn’t been set, this will end with an error. (This doesn’t matter.)
Reverse compilation is also possible.
To obtain a text file from a DTB blob, go something like:

  $ scripts/dtc/dtc scripts/dtc/dtc -I dtb -O dts -o /path/to/fromdtb.dts /path/to/booted_with_this.dtb
  
The DTS file is fine for compilation back to a DTB, but it’s better to work with original DTS files, since references made by labels in the original DTS appear as numbers in the reverse-compiled DTS.

You can set CONFIG_PROC_DEVICETREE to be able to see the device tree information in /proc after booting.
For newer kernels where the CONFIG_PROC_DEVICETREE option does not exist, /proc/device-tree will be created if CONFIG_PROC_FS is set to 'Y'.
We can be obtained in DTS format from the running system with:


# cd /usr/src/kernels/linux/
# scripts/dtc/dtc -I fs -O dts -o ~/effective.dts /proc/device-tree/
The output file goes to the home directory.

Part II

The structure of a device tree

A device tree for Linux running on Zynq typically has 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_ddr_0: memory@0 {
    device_type = "memory";
    reg = < 0x0 0x20000000 >;
  } ;
  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 ... ]

  } ;
} ;
This is the device tree used with Xillinux, with two parts cut out: The one describing the CPUs (because it isn’t interesting) and the one defining the peripherals (because it’s long, and we’ll get down to its details later on).

After the version declaration, the device tree starts with a slash, saying “this is the tree’s root”, and then there are assignments within curly brackets. From the DTS compiler’s point of view, these curly brackets enclose deeper hierarchies in the tree (think directories in a file system). It will be the kernel code’s job to walk down this tree, and grab the desired information from certain paths (like reading from a file after reaching the desired path in a file system, if you like).

It’s important to remember that the tree’s structure is built just the way the kernel expects to find it. The assignments have no specific meaning to the compiler. In fact, many assignments are ignored by the kernel, just like a file in a file system is ignored if no program cares to open it.

Accessing the data from user space

The hierarchy of the device tree is also implemented in the kernel’s /proc/device-tree: Each curly bracket is represented as a directory having the name of the string coming just before it.

The device tree is as listed above:

# hexdump -C '/proc/device-tree/axi@0/compatible'
00000000  78 6c 6e 78 2c 70 73 37  2d 61 78 69 2d 69 6e 74  |xlnx,ps7-axi-int|
00000010  65 72 63 6f 6e 6e 65 63  74 2d 31 2e 30 30 2e 61  |erconnect-1.00.a|
00000020  00 73 69 6d 70 6c 65 2d  62 75 73 00              |.simple-bus.|
0000002c

# cat '/proc/device-tree/axi@0/compatible'
xlnx,ps7-axi-interconnect-1.00.asimple-bus

Note the axi@0 element’s definition in the device tree listing above. It says “ps7_axi_interconnect_0: axi@0″. The string before the colon is the label, which is possibly referred to within the DTS file, but doesn’t appear in the DTB. As just said, It’s the string close to the curly brackets that defines the name of the hierarchy (and hence also the directory).

As this example demonstrates, the assignments turn into plain files in the /proc filesystem. If there is no assignment (e.g. “ranges” under “axi@0″), an empty file is created.

What the examples above show, is that the device tree can be used conveniently to convey information to user space programs as well as code within the Linux kernel, as the /proc/device-tree pseudo-filesystem makes this information accessible. Needless to say, there is a dedicated API in the kernel for accessing the device tree’s structure and data.

And you may have noted that the integer is represented in Big Endian. The Zynq’s processor runs Little Endian. Just a small thing to keep in mind.

Another example,

  • the source of dts
  • 
    	n_apb@e4000000 {
    		i2c0: i2c@e4008000 {
    ...
    			tlv320aic32x4: codec@18 {
    ...
                    ldoin-supply = <&reg33>;
    ...
    			};
    		};        
            
  • the run-time device tree
  • 
    $ ls -l /proc/device-tree/n_apb@e4000000/i2c@e4008000/codec@18
    total 0
    ...
    -r--r--r-- 1 root root  4 Mar 24 14:03  ldoin-supply
    ...
            

Boot parameters in the device tree

In general, there are 3 sources for the kernel boot command line :
  • Those given as CONFIG_CMDLINE in the kernel configuration
  • Those passed on by the boot loader (typically U-boot on ARM processors, LILO or GRUB on x86)
  • Those included in the device tree, under chosen/bootargs (see listing above)
Which one is used depends on kernel configuration parameters.

The chosen UART for kernel boot messages is hardcoded in the initialization routine. As a matter of fact, boot messages will appear on the UART even if the ps7_uart_1: serial@e0001000 entry in the device tree is deleted altogether (but the UART won't be available as /dev/ttyPS0).

The somewhat misleading "aliases" and "linux,stdout-path" assignments are leftovers from other architectures, and have no significance at this time.

Defining Peripherals

It’s likely that you’re reading this because you want to write a Linux driver for your own peripheral. The recommended book for learning the basics is the famous Linux Device Drivers. But before jumping into writing a device driver of your own, allow me to share rule number one for writing drivers for Linux: Never write a device driver for Linux.

Rather, find a well-maintained driver for some other hardware with similar functionality, and hack it. This is not just easier, but you’re likely to avoid problems you’re not even aware of. Copying snippets of code from other drivers will make your own understandable to others, portable, and with a better chance to be accepted into the kernel tree.

So the key is understanding what another driver is doing, and then adjust the parts that are related. In case of doubt, do what everyone else is doing. Creativity and personal style are not helpful.

Now back to the device tree. Let’s look at the segment that was omitted an in part II:

  ps7_axi_interconnect_0: axi@0 {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "xlnx,ps7-axi-interconnect-1.00.a", "simple-bus";
    ranges ;
    gic: interrupt-controller@f8f01000 {
      #interrupt-cells = < 3 >;
      compatible = "arm,cortex-a9-gic";
      interrupt-controller ;
      reg = < 0xf8f01000 0x1000  >,< 0xf8f00100 0x100  >;
    } ;
    pl310: pl310-controller@f8f02000 {
      arm,data-latency = < 3 2 2 >;
      arm,tag-latency = < 2 2 2 >;
      cache-level = < 2 >;
      cache-unified ;
      compatible = "arm,pl310-cache";
      interrupts = < 0 34 4 >;
      reg = < 0xf8f02000 0x1000 >;
    } ;

      [ ... more items ... ]

    xillybus_0: xillybus@50000000 {
      compatible = "xlnx,xillybus-1.00.a";
      reg = < 0x50000000 0x1000 >;
      interrupts = < 0 59 1 >;
      interrupt-parent = <&gic>;
      xlnx,max-burst-len = <0x10>;
      xlnx,native-data-width = <0x20>;
      xlnx,slv-awidth = <0x20>;
      xlnx,slv-dwidth = <0x20>;
      xlnx,use-wstrb = <0x1>;
    } ;
  } ;

Only the first two devices from the original file are shown, and also the last one, which we’ll focus on. As mentioned earlier, this is an excerpt from the full DTS file which is available as /boot/devicetree-3.3.0-xillinux-1.0.dts in Xillinux’ file system.

Let’s pay attention to the first entry in the list: It’s the Zynq processor’s interrupt controller. The existence of this entry makes sure that the interrupt controller’s driver is loaded. Note that its label is gic”. This label will be referenced in every device that uses interrupts.

We are now finally in position to talk about the interesting stuff: How all this works together with the Linux code.

Relation to the kernel driver

Four things must happen to have our device driver alive and kicking:
  • The driver loaded by the kernel when the hardware is present (i.e. declared in the device tree)
  • The driver needs to know the physical addresses allocated to the device
  • The driver needs to know which interrupt(s) the device will trigger, so it can register interrupt handlers
  • Application-specific information needs to be retrieved
The kernel has an API for accessing the device tree directly, but it’s much easier to use the dedicated interface for device drivers, which is highly influenced by the API used for PCI/PCIe drivers.
Let’s consider the xillybus_0 entry, which is rather typical for custom logic attached to the AXI bus.

The label and node name

First, the label (”xillybus_0”) and entry’s name (”xillybus@50000000″).
The label could have been omitted altogether, and the entry’s node name should stick to this format (some-name@address), so that a consistent entry is generated in /sys for this case:

/sys/devices/axi.0/50000000.xillybus/
  
The data in this device tree entry will appear in:
  
/proc/device-tree/axi@0/xillybus@50000000/
  
but that’s definitely not the way to access it from within the kernel.

Making the driver autoload

The first assignment in the node,
    
  compatible = “xlnx,xillybus-1.00.a
is the most crucial one: It’s the link between the hardware and its driver.

During the boot process:

  • When the kernel starts up, it kicks off compiled-in drivers that match “compatible” entries it finds in the device tree.
  • At a later stage (when /lib/modules is available), all kernel modules that match “compatible” entries in the device tree are loaded.
The ‘compatible’ property contains a sorted list of strings starting with the exact name of the machine, followed by an optional list of boards it is compatible with sorted from most compatible to least. For example, the root compatible properties for the TI BeagleBoard and its successor, the BeagleBoard xM board might look like, respectively:

compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";
Where “ti,omap3-beagleboard-xm” specifies the exact model, it also claims that it compatible with the OMAP 3450 SoC, and the omap3 family of SoCs in general. You’ll notice that the list is sorted from most specific (exact board) to least specific (SoC family).

The connection between a kernel driver and the “compatible” entries it should be attached to, is made by a code segment as follows in the driver’s source code:

static struct of_device_id xillybus_of_match[] __devinitdata = {
  { .compatible = "xlnx,xillybus-1.00.a", },
  {}
};

MODULE_DEVICE_TABLE(of, xillybus_of_match);
The above declares that the current driver matches a certain “compatible” entry, and leaves the rest to the kernel infrastructure.
Note that there’s a NULL struct entry in the list: It’s possible to define multiple “compatible” strings, all of which will cause the current driver to load, so this list must be terminated with a NULL ( “{}” in the list, as shown above).

Also, near the bottom of the frive code, something like this is necessary:

static struct platform_driver xillybus_platform_driver = {
  .probe = xilly_drv_probe,
  .remove = xilly_drv_remove,
  .driver = {
    .name = "xillybus",
    .owner = THIS_MODULE,
    .of_match_table = xillybus_of_match,
  },
};

And then platform_driver_register(&xillybus_platform_driver) must be called in the module’s initialization function.
This informs the kernel about the probing function xilly_drv_probe() to be called if hardware matching (xillybus_of_match) has been found.

To the kernel, the “compatible” string is just a string that needs to be equal (as in strcmp) to what some driver has declared. The “xlnx,” prefix is just a way to keep clear of name clashes.

By the way, a peripheral entry in the device tree may declare several “compatible” strings. Also, it’s possible that more than one driver will be eligible for a certain peripheral entry, in which case they are all probed until one of them returns success on the probing function. Which one is given the opportunity first is not defined.

It’s also possible to require a match with the hardware’s name and type, but this is not used often.

An important thing to note when writing kernel modules, is that the automatic loading mechanism (modprobe, actually) depends on an entry for the “compatible” string in /lib/modules/{kernel version}/modules.ofmap and other definition files in the same directory. The correct way to make this happen for your own module, is to copy the *.ko file to somewhere under the relevant /lib/modules/{kernel version}/kernel/drivers/ directory and go

# depmod -a
on the target platform, with that certain kernel version loaded (or define which kernel version to depmod).

Part IV

Getting the resources

Having the kernel module driver loaded, it’s time to get control of the hardware’s resources. That is, being able to read and write to the registers, and receiving its interrupts.

Resources are tree-like, allowing nesting etc..


struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    struct resource *parent, *sibling, *child;
};
IO resources have these defined flags.

#define IORESOURCE_BITS        0x000000ff    /* Bus-specific bits */
#define IORESOURCE_TYPE_BITS    0x00001f00    /* Resource type */
#define IORESOURCE_IO        0x00000100    /* PCI/ISA I/O ports */
#define IORESOURCE_MEM        0x00000200 
#define IORESOURCE_REG        0x00000300    /* Register offsets */
#define IORESOURCE_IRQ        0x00000400
#define IORESOURCE_DMA        0x00000800
#define IORESOURCE_BUS        0x00001000

Still on the same entry in the device tree,

    xillybus_0: xillybus@50000000 {
      compatible = "xlnx,xillybus-1.00.a";
      reg = < 0x50000000 0x1000 >;
      interrupts = < 0 59 1 >;
      interrupt-parent = <&gic>;

      xlnx,max-burst-len = <0x10>;
      xlnx,native-data-width = <0x20>;
      xlnx,slv-awidth = <0x20>;
      xlnx,slv-dwidth = <0x20>;
      xlnx,use-wstrb = <0x1>;
    } ;
we now focus on the part marked in bold.

The driver typically takes ownership of the hardware’s resource management in the probing function.
Look at the skeleton of the probing function :

static int __devinit xilly_drv_probe(struct platform_device *op)
{
  const struct of_device_id *match;

  match = of_match_device(xillybus_of_match, &op->dev);

  if (!match)
    return -EINVAL;
The first operation is a sanity check, verifying that the probe was called on a device that is relevant. This is probably not really necessary, but this check appears in many drivers.

Accessing registers

  int rc = 0;
  struct resource res;
  void *registers;

  rc = of_address_to_resource(&op->dev.of_node, 0, &res);
  if (rc) {
    /* Fail */
  }

  if  (!request_mem_region(res.start, resource_size(&res), "xillybus")) {
    /* Fail */
  }

  registers = of_iomap(op->dev.of_node, 0);

  if (!registers) {
    /* Fail */
  }
  • of_address_to_resource() - Translate device tree address and return as resource
  • The device tree's node may define the reserved memory region, use the function can convert that to a struct resource.
    
    int of_address_to_resource	(	struct device_node * 	dev,
                                    int 	index,
                                    struct resource * 	r 
    )	
        
    In our example,
    
    	reg = < 0x50000000 0x1000 >
        
    This means that the allocated chunk starts at physical address 0x50000000 and has the size of 0x1000 bytes.
    of_address_to_resource() will therefore set res.start = 0x50000000 and res.end = 0x50000fff.
  • request_mem_region() - to register the specific memory segment
  • request_mem_region() tells the kernel that your driver is going to use this range of I/O addresses, which will prevent other drivers to make any overlapping call to the same region through request_mem_region. This mechanism does not do any kind of mapping, it's a pure reservation mechanism, which relies on the fact that all kernel device drivers must be nice, and they must call request_mem_region, check the return value, and behave properly in case of error.
  • of_iomap() - Maps the memory mapped IO for a given device_node
  • It's a combination of of_address_to_resource() and ioremap(), and is essentially equivalent to ioremap(res.start, resource_size(&res)).
    
    void __iomem *of_iomap(struct device_node *np, int index)
    {
        struct resource res;
    
        if (of_address_to_resource(np, index, &res))
            return NULL;
    
        return ioremap(res.start, resource_size(&res));
    }    
        

Attaching the interrupt handler

The driver’s side to this is quite simple. Something like this:

 irq = irq_of_parse_and_map(op->dev.of_node, 0);
 rc = request_irq(irq, xillybus_isr, 0, "xillybus", op->dev);  
The irq_of_parse_and_map() call merely looks up the interrupt’s specification in the device tree (more about this below) and returns its identifying number, as request_irq() expects to have it (”irq” matches the enumeration in /proc/interrupts as well). The second argument, zero, says that the first interrupt given in the device tree should be taken.

And then request_irq() registers the interrupt handler.

The device tree declaration goes something like (copied from above):


      interrupts = < 0 59 1 >;
      interrupt-parent = <&gic>;  
There are three numbers assigned to “interrupt”:
  • The first number (zero) is a flag indicating if the interrupt is an SPI (shared peripheral interrupt).
  • A nonzero value means it is an SPI.
  • The second number is related to the interrupt number.
  • The third number is the type of interrupt.
  • Three values are possible:
    • 0 — Leave it as it was (power-up default or what the bootloader set it to, if it did)
    • 1 — Rising edge
    • 4 — Level sensitive, active high
Finally, the interrupt-parent assignment. It should always point to the interrupt controller, which is referenced by &gic. On device trees that were reverse compiled from a DTB file, a number will appear instead of this reference, typically 0x1.

Part V

Application-specific data

As mentioned earlier, the device tree is commonly used to carry specific information, so that a single driver can manage similar pieces of hardware.
For example,
  • an LCD display driver
  • the information about its pixel dimensions and maybe even physical dimensions may appear in the device tree.
  • a serial port interface hardware (i.e. RS-232, UART)
  • clock frequency is driving the logic.
In the simplest, and most common form, this information is conveyed by a simple assignment in the peripheral’s entry, e.g.
      xlnx,slv-awidth = <0x20>;

The “xlnx,” prefix merely protects against name collisions. The name string is arbitrary, but some kind of prefix is recommended at least for the sake of clarity.

To grab this information, the kernel code says something like


void *ptr;
  ptr = of_get_property(op->dev.of_node, "xlnx,slv-awidth", NULL);

  if (!ptr) {
    /* Couldn't find the entry */
  }  
The third argument in the call, NULL, is possibly a pointer to an int variable, to which the length of the data is written.

To access the actual data, assuming that it’s a number, go


int value;

value = be32_to_cpup(ptr);
The be32_to_cpup reads the data from the address given by “ptr”, converts from Big Endian to the processor’s native Little Endian, so “value” will contain the integer one expects it to contain. There are plenty of other API functions for reading arrays etc. See drivers/of/base.c in the Linux sources.

The Analysis of of_get_mac_address()

include/linux/of.h


struct device_node {
	const char *name;
	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;
	struct	device_node *sibling;
#if defined(CONFIG_OF_KOBJ)
	struct	kobject kobj;
#endif
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};  
  

drivers/of/of_net.c


/**
 * Search the device tree for the best MAC address to use.  'mac-address' is
 * checked first, because that is supposed to contain to "most recent" MAC
 * address. If that isn't set, then 'local-mac-address' is checked next,
 * because that is the default address. If that isn't set, then the obsolete
 * 'address' is checked, just in case we're using an old device tree. If any
 * of the above isn't set, then try to get MAC address from nvmem cell named
 * 'mac-address'.
 *
 * Note that the 'address' property is supposed to contain a virtual address of
 * the register set, but some DTS files have redefined that property to be the
 * MAC address.
 *
 * All-zero MAC addresses are rejected, because those could be properties that
 * exist in the device tree, but were not set by U-Boot.  For example, the
 * DTS could define 'mac-address' and 'local-mac-address', with zero MAC
 * addresses.  Some older U-Boots only initialized 'local-mac-address'.  In
 * this case, the real MAC is in 'local-mac-address', and 'mac-address' exists
 * but is all zeros.
 *
 * Return: Will be a valid pointer on success and ERR_PTR in case of error.
*/
const void *of_get_mac_address(struct device_node *np)
{
	const void *addr;

	addr = of_get_mac_addr(np, "mac-address");
	if (addr)
		return addr;

	addr = of_get_mac_addr(np, "local-mac-address");
	if (addr)
		return addr;

	addr = of_get_mac_addr(np, "address");
	if (addr)
		return addr;

	return of_get_mac_addr_nvmem(np);
}
EXPORT_SYMBOL(of_get_mac_address);

static const void *of_get_mac_addr(struct device_node *np, const char *name)
{
	struct property *pp = of_find_property(np, name, NULL);

	if (pp && pp->length == ETH_ALEN && is_valid_ether_addr(pp->value))
		return pp->value;
	return NULL;
}
  

Ambarella Linux

DTS

boards/s6lm_pineapple/bsp/s6lm_pineapple.dts

...
                mac0: ethernet@e000e000 {
                        pinctrl-0 = <&rgmii_pins &enet_2nd_ref_clk_pins_c>;
                        amb,tx-clk-invert;
                        phy@0 {
                                reg = <0>;
                                rst-gpios = <&pca9539 11 0>;
                                txen-skew-ps = <0>;
                                rxdv-skew-ps = <0>;
                                rxd0-skew-ps = <0>;
                                rxd1-skew-ps = <0>;
                                rxd2-skew-ps = <0>;
                                rxd3-skew-ps = <0>;
                                txd0-skew-ps = <0>;
                                txd1-skew-ps = <0>;
                                txd2-skew-ps = <0>;
                                txd3-skew-ps = <0>;
                        };
                };
...
  	

Driver

cv2x_linux_sdk_3.0/ambarella/kernel/linux-5.4/drivers/net/ethernet/ambarella/ambarella_main.c

static int ambeth_of_parse(struct device_node *np, struct ambeth_info *lp)
{
        struct device *dev = lp->ndev->dev.parent;
        struct device_node *phy_np;
        enum of_gpio_flags flags;
        int ret_val, hw_intf, mask, val = 0;

        lp->id = of_alias_get_id(np, "ethernet");
        if (lp->id >= ETH_INSTANCES) {
                dev_err(dev, "Invalid ethernet ID %d!\n", lp->id);
                return -ENXIO;
        }

        for_each_child_of_node(np, phy_np) {
                if (!phy_np->name || of_node_cmp(phy_np->name, "phy"))
                        continue;

...
}

static int ambeth_drv_probe(struct platform_device *pdev)
{
...
        ret_val = ambeth_of_parse(np, lp);
        if (ret_val < 0){
                goto ambeth_drv_probe_free_netdev;
        }

...
        macaddr = of_get_mac_address(pdev->dev.of_node);
        if (!IS_ERR_OR_NULL(macaddr)){
                dev_info(&pdev->dev, "Cop MAC from DTS\n");
                memcpy(ndev->dev_addr, macaddr, ETH_ALEN);
        }
        if (!is_valid_ether_addr(ndev->dev_addr)){
                dev_info(&pdev->dev, "Generate a random MAC\n");
                eth_hw_addr_random(ndev);
        }
...
}

static const struct of_device_id ambarella_eth_dt_ids[] = {
        { .compatible = "ambarella,eth" },
        { /* sentinel */ }
};

MODULE_DEVICE_TABLE(of, ambarella_eth_dt_ids);

static struct platform_driver ambeth_driver = {
        .probe          = ambeth_drv_probe,
        .remove         = ambeth_drv_remove,
#ifdef CONFIG_PM
        .suspend        = ambeth_drv_suspend,
        .resume         = ambeth_drv_resume,
#endif
        .driver = {
                .name   = "ambarella-eth",
                .owner  = THIS_MODULE,
                .of_match_table = ambarella_eth_dt_ids,
        },
};

module_platform_driver(ambeth_driver);  
  	

Linux內核DTB文件啟動的幾種方式

學習 DT device tree 以 ST 的開發板 STM32F429i-disc1 為例


留言

匿名表示…
Thank you so much! Awesome. It was a great help.
Gamsahamnida!

熱門文章