Device Tree Hacking

From OLPC
Revision as of 19:18, 12 October 2016 by Wmb@firmworks.com (talk | contribs) (Added lesson template so TOC appears)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
Mitch Bradley's Forth and
Open Firmware Lessons:

You might need to modify the Open Firmware device tree for testing a new kernel driver. Ultimately, such mods should be incorporated in an OFW build, but quick hacks are often useful for testing new ideas or for debugging.

Inspecting Properties

ok dev /leds
ok .properties
ok device-end

That assumes there is already a /leds device node. The dev command "goes into" that device node so you can see and amend its properties and methods. dev does not activate the associated hardware device, it just lets you poke around inside the device node. device-end gets out of the device node, so its internals are no longer visible to the Forth interpreter. dend is shorthand for device-end, for us lazy typists.

.properties displays that node's property list.

The fundamental data type of a property value is "array of bytes", in which various other data types may be encoded. One common data type is "integer", a 32-bit binary integer encoded as 4 bytes in big-endian order. There is no "tagging" to indicate what data types are inside the array of bytes - the producer and consumer of a property just have to agree. Multiple data items, possibly of different types, can be present in one property value, by concatenation, with no extra padding for alignment.

.properties has heuristics to display property values sensibly - for example it is pretty good at guessing when integer values are present. But sometimes .properties guesses wrong, or elides the trailing bytes for long values. If you want to see the raw data of a property value, you can say:

ok " reg" get-property throw cdump

The throw handles a found/not_found flag cleanly, so you won't get any output if you supply a property name that doesn't exist.

Adding or Modifying a Property to an Existing Node

ok dev /leds
ok d# 123  " blink-time" integer-property
ok device-end

The integer-property line creates a property named "blink-time" whose value is a single integer, in this case (decimal) 123. You can use either "d# nnn" for decimal numbers or "h# nnn" for hex numbers. If there is already a property named "blink-time", its value will be replaced with the new value.

You have to be inside a device node to make a property. From now on, I'm going to omit the implied "dev nodename .. device-end" and just talk about the property creation commands.

String-valued Properties

ok " This is a string" " my-string-prop" string-property

That creates (or modifies) a property named "my-string-prop", giving it the value "This is a string". The encoding of a string inside a property value is a null-terminated sequence of 8-bit characters.

Multi-valued Properties

A property value can include multiple data items, possibly of different types. The property creation words we have seen so far - integer-property and string-property - are shorthand commands for frequently-used cases where the property value contains only one item. For multiple-valued properties, you usually have to resort to more primitive (and more flexible) commands.

ok  d# 123 encode-int  d# 456 encode-int encode+  h# 1a5 encode-int encode+  " my-int-list" property

This illustrates several primitives. property is the fundamental property creation primitive. It assumes that all encoding and concatenation of higher-level data types into a property value byte array has already been done.

In this example, we first encode the integer value "123" into a property array with encode-int (leaving the address and length on the Forth stack). Then we encode a second integer value "456" into a property array with encode-int (also leaving its address and length on the stack). encode+ concatenates the two arrays into one combined array, coalescing the two address,length pairs into one. Finally, we do the same with the integer "0x1a5". The net result is a single byte array containing the three encoded integers.

encode+ doesn't care what data types it is concatenating. You can put mixed types into one property, as with:

ok d# 999 encode-int  " Test test test" encode-string encode+  d# 888 encode-int encode+  " my-mixed-type" property

Linux bindings generally avoid mixed-type properties, preferring arrays of the same basic type.

The phrase "encode-int encode+" can become tiresome, so OFW driver writers often define a Forth shorthand name for it:

ok : +i encode-int encode+  ;
ok d# 123 encode-int  d# 456 +i  h# 1a5 +i  " my-int-list" property


Phandle Properties

A phandle is a numerical (as opposed to text string) reference to another device node. phandles are often used to refer to device nodes for interrupt controllers, gpio controllers, and clock controllers.

GPIO Specifiers

ok " /gpio" encode-phandle  d# 47 +i  1 +i  " cd-gpios" property

encode-phandle finds the device node named by its string argument (in this case "/gpio"), gets its numerical phandle value, and encodes that number into a byte array as a integer. In this case we follow the phandle value with a GPIO number (31) and a flag value (1), using the shorthand command +i that we defined above.

That's the most common way of specifying a GPIO in Linux device trees - the phandle of a gpio controller node, followed by the gpio number and a flag (0 for active high, 1 for active low).

Sometimes you need to specify multiple GPIO references in one property, in which case you just continue concatening the various values:

ok " /gpio@d4293000" encode-phandle  d# 42 +i  0 +i
ok " /gpio@e7000000" encode-phandle encode+  d# 16 +i  1 +i
ok " my-multi-gpios" property

Note that we had to say encode+ after the second encode-phandle because we are concatenating onto the end of a array that is already in progress.

Note also that it is okay to continue across multiple lines. Partial results are stored on the Forth stack in postfix fashion, so line breaks are interchangeable with spaces (except inside a string - you can't substitute a line break for the space that follows an opening double-quote).

The number of integers after the phandle in a Linux GPIO specifier is usually 2, but not always. The "/gpio" node that the phandle points to contains a #gpio-cells whose integer value gives that number of post-phandle integers (cells). Unusual GPIO controllers can declare that they need a different number of cells in order to fully specify the characteristics of a given GPIO.

Clock Specifiers

Clocks are specified with a pattern similar to GPIOs, except:

  • #clock-cells is usually 1, so there is usually only one integer following each phandle
  • The property name for nodes that use clocks (clock "consumers") is just "clocks", as opposed to "xxx-gpios" where many values for xxx are possible.
  • Clock consumers also have a "clock-names" property whose value is a list of strings. When the driver code asks for a named clock, the framework code searches that list to find a match, then uses the corresponding (the same index within the list) phandle+integer value in the "clocks" property.

For example:

 ok " /main-clocks" encode-phandle  5 +i
 ok " /aux-clocks" encode-phandle encode+  3 +i  " clocks" property
 ok
 ok " codec-clock" encode-string
 ok " control-clock" encode-string encode+  " clock-names" property

That declares that the device uses a clock named "codec-clock" corresponding (due to its position at the beginning of the list) with clock number 5 in the clock provider implemented by the "/main-clocks" node. It also uses "control-clock" corresponding to clock number 3 in the "/aux-clocks" clock provider.

Interrupt Specifiers

The pattern for interrupts is somewhat different. Instead of encoding a phandle for each individual interrupt, the "target interrupt controller" for interrupts is determined according to a set of rules:

  • If there is an "interrupt-parent" property, its phandle value is the interrupt controller.
  • Otherwise, the device node's parents are searched (recursively) until either an interrupt controller node or an "interrupt-parent" property is found.

This rule caters to the common case where device nodes are underneath a parent bus node that defines bus-specific interrupt numbering and semantics.

The "interrupts" properties themselves consist of a list of integer tuples. The number of integers in each tuple is given by the interrupt controller node's "#interrupt-cells" property. If "#interrupt-cells" is 1, the integer value is usually just the interrupt number. If "#interrupt-cells" is 2, the additional value typically contains flag bits that indicate things like active_low/active_high, edge_triggered/level_triggered, etc.

Here's an example:

 ok " /interrupt-controller" encode-phandle " interrupt-parent" property
 ok d# 10  " interrupts"  integer-property

This device's parent node does not handle interrupt distribution, so we have to supply an interrupt-parent property to point to the interrupt controller. That interrupt controller node has "#interrupt-cells" = 1, and we only need to specify one interrupt, so we can use the shorthand integer-property to create the single-integer property value.

Compatible Properties

The standard property compatible tells the OS which device drivers might be suitable for the device. It is a list of names (null-terminated strings), ordered from most-specific first to least-specific last. To bind a device driver to the device, the OS searches that list, looking for a name that matches one of its supported device drivers. (The reason for having a list is so that devices that are compatible with legacy programming models can "fall back" to older "generic" drivers when newer drivers are not available. This "fallback" feature is not so useful for Linux, where releasing new kernels is the norm, but it was quite useful for commercial OSs that were released independently of hardware.)

You could create the compatible property with a sequence involving encode-string and encode+, but there is a shorthand command that is easier:

 ok " isa-ide" +compatible
 ok " pci-dma-ide" +compatible

+compatible makes a compatible property if necessary, then prepends the string to its value. So each successive call to +compatible will add its string to the beginning of the list. The reason for this prepending behavior is so you can start out with a generic OFW driver that claims compatibility with a baseline programming model, then later, perhaps in a separate file, elaborate that driver to make it more specific to a particular hardware variant.

Aliases

The device tree has a special (not tied to one hardware device) node named /aliases. It contains properties whose names are shorthands for full pathnames. For example, the property name ext might have the (string) value "/sd/sdhci@d4281000/disk". That serves several purposes in addition to the obvious reduction in typing.

First, it lets you use a common name for an important device whose actual pathname might vary across platforms in a family. For example, OLPC uses the shorthand name "int" to refer to the primary internal storage device, whose actual pathname is different on every OLPC machine.

Secondly, it can be used to solve the longstanding Linux problem of associating specific user-visible device names (like ttyS0) with specific hardware instances. Historically, such device names had incrementing-number suffixes based on a poorly-specified "probe order", which tended to change when other devices were plugged and unplugged.

This "devname-to-instance" binding problem can be solved with device tree aliases as follows. Device drivers for a given class of devices can look in the /aliases node for aliases matching a class-specific name template. For example, serial drivers can look for names matching "serialN", where N is a small integer, associating "ttySN" with the device instance named by serialNs property value (a pathname string). See the Linux function "of_alias_get_id()".

You can create aliases with commands described above ("dev /aliases " /uart@10000" " serial0" string-property device-end") but there is an easier way:

 ok devalias serial0 /uart@1000

devalias does all the device node selection and property creation stuff for you. It's suitable for associating a well-known name with a well-known pathname. If you need to create an association dynamically based on some run-time determination, you have to fall back to lower level commands.

Automation with /boot/olpc.fth

For a long debugging session, you would probably prefer not to type a bunch of device-tree modification commands over and over. You can put any of the above commands in /boot/olpc.fth, after the initial comment line and before the final line that invokes the "boot" command.

The commands can go as-is outside of a colon definition (i.e. not between ": whatever" and its closing ";"). If you want to use the commands inside a colon definition, you'll run into problems with the "dev" command. The trouble with "dev" is that its argument (e.g. the "/leds" in "dev /leds") is prefix, not postfix as with most Forth words. Prefix Forth commands interact surprisingly with the Forth compiler (specifically, "dev", when compiled inside a Forth definition, tries to get its argument later, at run-time, instead of when the definition is being compiled).

The easiest workaround is to say, for example:

 : foo
    " dev /leds" evaluate
    d# 123 " my-int" integer-property
    device-end
 ;

The evaluate trick compiles the full "dev" command, along with its "/leds" argument, into a string, then evaluate later interprets that whole string. The only other command above that would need this trick is devalias - which is also prefix instead of postfix.

 : foo
     " devalias serial1 /uart@d4017000" evaluate
 ;

Note that there are other ways to do this without evaluate, but the evaluate technique is just fine for experimentation.

Automation with dl

You can write commands into a host file with a text editor, then paste the commands to a serial-line ok prompt in a terminal emulator. That works fine for short commands, typically ones that fit inside UART FIFOs. For longer commands, it runs the risk of overrunning the UART input buffer, which bottlenecks on echoing back the characters as they are being typed. OFW has a dl command (think "download") for this purpose.

If you type

 ok dl

OFW will receive characters from the serial line, without echoing, until a ^D (control-D) is received. The characters are stored in a memory buffer (at load-base) and that buffer is interpreted after the ^D is received. So you type dl, then paste the commands into the terminal emulator, then type ^D. Or you can use the terminal emulator's "send file in ascii" command to send from a file instead of from a paste buffer.

Either way, you can interpret very long command sequences this way - even entire device drivers.

Automation with Some Other File

You could store your command sequence in a file on some medium like a USB stick and interpret with the fl (shorthand for fload) command:

 ok fl u:\myfile.fth

You could do it from the network if you wanted:

 ok fl http:\\myserver\myfile.fth

When You Are Done

Please report back your mods to the OFW team for inclusion in an OFW release.