BeagleBone and the 3.8 Kernel

= Beaglebone and the 3.8 Kernel=

Pantelis Antoniou 

Additional content by Tom King  Matt Porter 

v1.0

Introduction
The Beaglebone Black is the new addition to the Linux ARM development boards coming from Beagleboard.org, and due to the combination of affordable pricing and good hardware expandability has captured the attention of the maker community.

The Beaglebone Black is clearly a step up over the old version, affectionately now known as Beaglebone White; it is faster, with more memory and onboard eMMC and HDMI.

What is the most interesting part about the beaglebone family is it's expandability by using “Capes”, small add on boards that can stack, and allow easy access to the peripherals of the am3359 SoC. A plethora of capes have been produced for use for the Beaglebone White and most are compatible with the Black. Things are a bit complicated for capes that use the same pins as the onboard eMMC and the HDMI, to resolve this add-on capes have priority over the onboard peripherals.

The original beaglebone was shipped with a 3.2 kernel with a lot of patches and custom interfaces from Texas Instruments’s (TI) own kernel trees. Since this was based on a TI supported kernel, hardware compatibility was generally good and the capes worked. They were developed against it after all!

The trouble with board files
The old 3.2 Kernel worked pretty well, so why was a move made to 3.8, which causes so much grief to the beaglebone old timers?

The major problem with the 3.2 kernel is that uses a board-file, namely,

arch/arm/mach-omap2/board-am335xevm.c

It's pretty big (at 4K+ lines), and quite difficult to modify. The boardfile also supports, in addition to the beaglebone white, the am335x-evm and all the capes. Every new cape that is to be supported has to modify this file. Additionally you need to create the platform devices that it contains and register them. You have to make sure that you don't break any of the other boards (or capes) supported by the platform file.

This is a job for professional kernel developers, but even kernel developers have their limits as evidenced by the famous Linus rant that prompted the great cosmic shift to Device Tree:

Famous Linux rant about ARM churn

What has been decreed is simple: “No more new ARM board files”

So all new boards must support booting using Device Tree. The board file used in the new kernels is just arch/arm/mach-omap2/board-generic.c and supports booting kernels using omap2, omap3, omap4, omap5 and the am33xx family of the beaglebone.

This presented a difficult decision for the developers working on the beaglebone black Linux support:


 * 1) Ignore the new mainline dictums, carry on using 3.2 and make the jump to mainline sometime in the future. That would require more developer resources, when the time for mainline porting comes. On top of that most kernel developers don't bother with older vendor kernels, and any bug fix or new functionality has to be painfully back-ported from mainline.
 * 2) Bite the bullet, move to 3.8 using DT, and future proof the beaglebone by submitting everything for inclusion to mainline as soon as possible.

The trouble with Device Tree
What is Device Tree (DT) and why is it causing so many problems for new developers?

Device Tree Central

"The Device Tree is a data structure for describing hardware. Rather than hard coding every detail of a device into an operating system, many aspect of the hardware can be described in a data structure that is passed to the operating system at boot time." (Please take note of the 'boot time' reference, we'll come back to it later).

The pinmux subsystem and clocksource/clockevent frameworks are two examples of consolidated frameworks that have evolved from the ARM move to DT. Previously, each ARM "machine", such as mach-omap2/*, implemented their SoC-specific pinmux and timer/clock drivers within their machine directory. In converted ARM machines, these drivers now live in the appropriate drivers/* directory and share common code sanely with other similar drivers.

As it turns out DT isn't that new, it's a couple of decades old and has been used by Open Firmware based systems like Sun workstations and the original PowerPC based Apple Macs.

For developers the biggest change is that the kernel board file is now gone.

Simply put, every board has to be described in a DTS (foo.dts) file that is compiled via the DTC compiler to a binary format DTB (flattened device tree format) which gets parsed on boot by the kernel and the devices are created.

Therein lies the biggest change; there are no more platform device data for the device containing it's configuration. On a board file based platform what you did was:

In the foo_platform_data.h file:

struct foo_platform_data { u32 bar; };

In the board file:

struct foo_platform_data foo_pdata { .bar       = 5, };	struct platform_device foo_dev { .name = "foo", .id = -1, .dev.platform_data = &foo_pdata, };

And in the board setup function

platform_device_register(&foo_dev);

The driver gets access to the platform data in the probe function.

static int foo_probe(struct platform_device *pdev) {               struct foo_platform_data *pdata; pdata = pdev->dev.platform_data; if (pdata == NULL) /* ERROR */ ...               /* access pdata->bar for configuration */ ...       }        static struct platform_driver foo_driver = { .probe = foo_probe, ....               .driver = { .name = "foo", },               ...        };        module_platform_driver(&foo_driver);

This method no longer works; in a DT based system what you have to do come up with device driver bindings, which contain the configuration the driver requires.

You must add a device node in board.dts under the on-chip-peripherals(OCP) device node:

foo { compatible = "corp,foo"; bar = <5>; };

No change in the board file (generic anyway) needs to be made, but the device driver must be updated to support the DT method, with something similar to the following:

static struct foo_platform_data * foo_parse_dt(struct platform_device *pdev) {               struct device_node *node = pdev->dev.of_node; struct foo_platform_data *pdata; pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL); if (pdata == NULL) return NULL; /* out of memory */ /* no such property */ if (of_property_read_u32(node, "bar", &pdata->bar) != 0) return NULL; /* pdata->bar is filled in with 5 */ return pdata; }       static int foo_probe(struct platform_device *pdev) {               struct foo_platform_data *pdata;

if (pdev->dev.of_node != NULL) pdata = foo_parse_dt(pdev); else pdata = pdev->dev.platform_data;

if (pdata == NULL) /* error */ ...       }

static const struct of_device_id of_foo_match[] = { { .compatible = "corp,foo", }, { },       };

static struct platform_driver foo_driver = { .probe = foo_probe, ....               .driver = { .name = "foo", .of_match_table = of_match_ptr(of_foo_match), },               ...        };

module_platform_driver(&foo_driver);

The driver described above can support both the platform data model and the new DT method. Using a purely DT driver model can result in more flexibility, but we won't deal with it now. Frankly, the biggest change is that now a user (by compiling the DTS) can now configure his platform without having to recompile the kernel, which is a pretty big usability gain. We use this in the new 3.8 releases to boot both versions of the beaglebone using the same kernel, and only changing the DTB we boot with.

For a developer, the biggest change of using DT, is that it is purely data driven, and that custom platform drivers are frowned upon, opting instead to using generic frameworks, one of which is the pinmux framework.

In summary, the core of the change to DT is that logic that was previously accomplished in board files is moved into a mixture of generic frameworks, abstracted devices drivers, and data to drive that software. DT imposes structure upon the embedded developer and sometimes there is considerable pushback.

Device Tree's data driven model
The data driven model of device tree causes the most pain to the embedded developer steeped in the board file/platform method. Let's examine a concrete example, that deals with the simple subject of a driver/board pinmuxing.

Pinmuxing refers to the method of modern SoC multiplexing multiple peripheral functions to a rather limited set of physical pins/balls of the SoC package. The am335x is a prime example this, having many pins with up to 8 possible peripheral functions.

For a peripheral driver to work the correct muxing configuration must be applied to the affected pins; since they are so many, and configuration is complex, a special omap specific driver has been written and has been used on the 3.2 kernel provided by TI.

Typically in the board setup file, the mux configuration for the peripheral is applied before registering the platform device.

For example the pin mux for the a gpio configured as a led would be of the form

omap_mux_init_signal("gpmc_a2.gpio1_18",			OMAP_MUX_MODE7 | AM33XX_PIN_OUTPUT);

The omap mux driver also provides a user-space interface for changing the mux settings. The problem with pinmuxing is that it is potentially dangerous if you don't know exactly what kind of hardware is connected to the SoC pins, so in the mainline pinmuxing is performed via the drivers on probe time.

For the sake of making the example more concrete let's have a gpio also controlling power to the device. The device's driver is only supposed to apply the mux and turn on the power to the device by setting a gpio to high (please bear in mind this is not a real-world example, but explains the issues).

Let's examine the platform data example with a call back method.

In the foo_platform_data.h file:

struct foo_platform_data { u32 bar; void (*get_ready)(struct platform_device *pdev); };

In the board file:

/* magic GPIO number of FOO’s power pin */ #define FOO_POWER 11234

static void foo_get_ready(struct platform_device *pdev) {               int ret;

omap_mux_init_signal("gpmc_a2.gpio1_18",			OMAP_MUX_MODE7 | AM33XX_PIN_OUTPUT); ret = gpio_request(FOO_POWER, "foo-power"); if (ret >= 0) gpio_direction_output(FOO_POWER, 1); }       struct foo_platform_data foo_pdata { .bar = 5, .get_ready = foo_get_ready, };

The rest of the board file has no other changes.

The driver then can call the get_ready method via the callback function pointer.

static int foo_probe(struct platform_device *pdev) {               struct foo_platform_data *pdata;

pdata = pdev->dev.platform_data; if (pdata == NULL) /* ERROR */ ...

/* access pdata->bar for configuration */ ...

/* get foo ready */ (*pdata->get_ready)(pdev); }

Callback functions are impossible in a DT platform. Callbacks break the data only model of DT. What must be done is for the driver to be converted to using the generic gpio and pinmux bindings. On am33xx the generic pinctrl-single driver is used, and to be honest, is not as polished as the old omap mux driver. Nevertheless it is usable and used on many other boards as well, since it is widely supported in the ARM community. Again generic frameworks win over custom solutions.

The boot.dts file is updated with the pinmux nodes and the gpio bank nodes.

/* point to the to the conf_* module registers */ am33xx_pinmux: pinmux@44e10800 { compatible = "pinctrl-single"; reg = ; #address-cells = <1>; #size-cells = <0>; pinctrl-single,register-width = <32>; pinctrl-single,function-mask = ;

foo_pins: foo_pins { pinctrl-single,pins = < /* need to look into the TRM and find those values */ /* conf_gpmc_a2 module config reg offset and value */ 0x0A8 0x1f >;		};	};

gpio2: gpio@4804c000 { compatible = "ti,omap4-gpio"; ti,hwmods = "gpio2"; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <1>; reg = ; interrupts = <98>; };

foo { compatible = "corp,foo"; bar = <5>;

# the power control gpio power-gpio = <&gpio2 18 0x0>;

# the muxing pinctrl-names = "default"; pinctrl-0 = <&foo_pins>; };

The new nodes are the am33xx_pinmux node, and the gpio2 node. Note the added node in am33xx_pinmux that contains the pinmux config for the foo driver. Please note due to the OMAP's numbering of peripherals starting with 1 every numbered peripheral is +1 from what the AM3359 TRM states. There are patches against mainline that fix that, but not on 3.8 yet.

The foo driver only need minor changes, first the pinctrl consumer interface include header must be included.

#include 

Then on the probe function we need to apply the pinctrl configuration, while on the parse_dt function we need to set the gpio to the given value.

static struct foo_platform_data * foo_parse_dt(struct platform_device *pdev) {               struct device_node *node = pdev->dev.of_node; struct foo_platform_data *pdata; enum of_gpio_flags ofgpioflags; int ret, gpio;

pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL); if (pdata == NULL) return NULL; /* out of memory */

/* no such property */ if (of_property_read_u32(node, "bar", &pdata->bar) != 0) return NULL;

/* now get the power gpio and set it to one */ gpio = of_get_named_gpio_flags(pdev->dev.of_node, "power-gpio",                               0, &ofgpioflags); if (!IS_ERR_VALUE(gpio)) { gpioflags = GPIOF_DIR_OUT; if (ofgpioflags & OF_GPIO_ACTIVE_LOW) gpioflags |= GPIOF_INIT_LOW; else gpioflags |= GPIOF_INIT_HIGH; ret = devm_gpio_request_one(&pdev->dev, gpio,                                       gpioflags, "foo-power"); }

/* pdata->bar is filled in with 5 */ return pdata; }

static int foo_probe(struct platform_device *pdev) {               struct pinctrl *pinctrl;

...               /* apply the pinmux configuration */ pinctrl = devm_pinctrl_get_select_default(&pdev->dev); if (IS_ERR(pinctrl)) dev_warn(pdev->dev, "unable to select pin group\n");

if (pdev->dev.of_node != NULL) pdata = foo_parse_dt(pdev); else pdata = pdev->dev.platform_data;

….       }

Cape Manager requirements
Going over the device tree definition we see that the data structure is referred as parsed at boot-time. Beaglebone capes are not static; a different cape set might be connected each time the board boots, and the only way to find out what kind of cape is connected is to read the serial EEPROM present on each cape which contains identification information.

Beaglebone capes are mostly compatible between White and Black, but due to the presence of the eMMC flash and the HDMI framer, some special attention must be paid for capes that use the same resources.

The information stored on the EEPROM of each cape are a user readable name, serial number, part number, revision information and others. The only information that the cape manager uses to work are the part-number and the revision.

A cape-manager should perform the following:


 * 1) Scan the I2C bus for EEPROMs that correspond to the address defined in the beaglebone SRM. Those addresses are 0x54-0x57 on I2C1 bus, hence the 4 capes limitation.
 * 2) Read the EEPROM contents and parse them according to the spec. Retrieve the part-number and revision information of that cape.
 * 3) Match the PART-NUMBER:REVISION tuple to a configuration that would result in the system state being that as if the cape was directly attached to the base board, and perform any actions that result to it.

Additionally we need extra facilities that are useful for people developing capes:


 * An override facility so that prototyping capes with no EEPROM can be supported.
 * An option to load capes at runtime.
 * An option to unload capes at runtime.
 * An option to stop a cape from loading even if detected.
 * To guard against resource conflicts. A cape that uses the same resources as another loaded earlier should not be permitted to load.
 * To be able to forbid cape loading of capes that are not supported by a baseboard revision.
 * To be able to load 'virtual' capes that correspond to peripheral that are present on specific baseboard revisions.
 * To support the notion of a cape priority, so that in case of a conflict, the cape we define as higher priority gets loaded. This is important to allow plug in capes to take preference over capes that are part of the baseboard; i.e. when an LCD cape is attached it takes preference over the onboard HDMI virtual cape.
 * To not require source changes for any driver for a peripheral that resides on cape or onboard, i.e. the drivers should not be modified for use in a cape.
 * No kernel source changes must be required for a new cape to be supported.
 * To not require modification of the boot-loader, or of the base DTS.

Device Tree Overlays
The method used to dynamically load the cape definition to the running kernel is called a Device Tree Overlay. What a device tree overlay does is apply changes to the kernel's internal DT representation and trigger the changes that this action carries. For example adding a new enabled device node should result in a new device being created, while removing a node removes the device and so on.

The previous foo example, when reworked for usage with DT overlays is as follows:

The base.dts file has an ocp label of the on-board peripherals for the SoC.

/ {               /* On chip peripherals */ ocp: ocp { /* peripherals that are always instantiated */ peripheral0 { ... };               }        };

The overlay when loaded must result in the same device tree as in the previous example; the overlay must be the following:

/plugin/;       /* allow undefined label references and record them */ / {               ....        /* various properties for loader use; i.e. part id etc. */ fragment@0 { target = <&am33xx_pinmux>; __overlay__ { foo_pins: foo_pins { pinctrl-single,pins = < /* look into the TRM for values */ /* conf_gpmc_a2 module reg offset/value */ 0x0A8 0x1f >;                               };                        };                };

fragment@1 { target = <&ocp>; __overlay__ { /* bar peripheral */ foo { compatible = "corp,foo"; bar = <5>;

# the power control gpio power-gpio = <&gpio2 18 0x0>;

# the muxing pinctrl-names = "default"; pinctrl-0 = <&foo_pins>; };                       };                };        };

The overlay consists of two fragments, one targeting the am33xx_pinmux node, the other the ocp node. Note that there are no foo driver changes, not any sort of platform board file hacking. When this overlay is applied the foo device will be created, just as if it was declared in the base.dts.

More information about the internal of overlays and the resolution mechanism is located in Documentation/devicetree directory of the kernel.

Cape Manager and Device Tree Overlays
Device Tree overlays is a general purpose mechanism, which is not tied to any platform. For actual platforms like the beaglebone the mechanism must be supplemented by platform specific logic.

The platform logic of the beaglebone cape manager deals with the following:


 * Reading the EEPROMs of the capes for retrieval of the part numbers & revision.
 * Allowing runtime addition/removal of cape fragments.
 * Having options to control cape loading from the kernel command line
 * Having options to force loading of capes from the base DTS
 * Having mapping options of multiple cape part numbers & revisions to a single cape
 * Resource conflict management
 * Implement cape priorities so that in case of a conflict the winner can be selected
 * Verification of the cape compatibility with a base board
 * Various cape information display (i.e. serial# etc).
 * Automatic loading of capes based on base board version (i.e. auto loading HDMI/eMMC capes)

Lets add the beaglebone specific properties to the foo DT overlay and complete the cape.

/plugin/;       /* allow undefined label references and record them */ / {               /* compatible with both the original and the beaglebone black */ compatible = "ti,beaglebone", "ti,beaglebone-black";

/* part-number */ part-number = "BB-FOO-01";

/* version */ version = "00A0";

exclusive-use = "P8.43",       /* Header.Pin# */ "gpio2_8";       /* the hardware IP we use */

/* the fragments are the same as above */ ....       };

The compatible property lists the platforms that this cape supports. If an attempt is made to load an unsupported cape by this platform the attempt will fail.

Similarly, the part-number and version properties guard against an attempt of loading a bad cape.

The exclusive-use property is a pretty simple attempt at implementing resource management. What it does, is allow capes to declare free-form text resources; if another cape tries to load which uses the same resource, it will be prevented of doing so. The convention in the beaglebone cape format is that we reserve the header/pin and the hardware IP block the cape wants to use, in that case pin#43 of the P8 connector, and the gpio2_8 gpio hardware block.

The name of the cape should be of the ${PART_NUMBER}:${REV}.dts format and the compilation would be (on either the host or the beaglebone): $ dtc -O dtb -o BB-FOO-01-00A0.dtbo -b 0 -@ BB-FOO-01-00A0.dts

Note the required -@ option; it is not part of the upstream dtc, so it's a DT overlay specific patch that enables it. The beaglebone kernel has the patch already applied on it’s internal dtc compiler copy, as well as the angstrom image, but you need the following patches for other distro’s dtc packages.

Dynamic Overlays DTC patch

Putting the resulting BB-FOO-01-00A0.dtbo file in /lib/firmware we can enable it by

# echo BB-FOO-01 >/sys/devices/bone_capemgr*/slots

We can verify if it loaded with:

# cat /sys/devices/bone_capemgr*/slots 0: 54:PF--- 1: 55:PF--- 2: 56:PF--- 3: 57:PF--- 4: ff:P-O-L Bone-LT-eMMC-2G,00A0,Texas Instrument,BB-BONE-EMMC-2G 5: ff:P-O-L Bone-Black-HDMI,00A0,Texas Instrument,BB-BONELT-HDMI 6: ff:P-O-L Override Board Name,00A0,Override Manuf,BB-FOO-01

Slot #6 is the one we've loaded.

The cape can be removed by:

# echo -6 >/sys/devices/bone_capemgr*/slots

Cape-manager also supports a method of enabling (or disabling) a cape by a kernel (3.8 - 6/6/2013 or later) command line option.

capemgr.enable_partno=BB-FOO-01

Will automatically load the cape on boot, while

capemgr.disable_partno=BB-FOO-01

will disable the cape (if it's attempted to load automatically).

These kernel boot options are placed in the uEnv.txt file on the eMMC. An example line might look something like this (you cannot have multiple lines defining 'optargs'):

optargs=quiet capemgr.enable_partno=BB-FOO-01

Porting to the new DT kernel FAQ
'''Q: I have a custom kernel fork, based on a modified board file. Nothing works.'''

A: The biggest difference of-course is that there's no board file, so there are no platform data. You have to have device drivers with DT bindings. You can get by temporarily by creating a helper DT enabled platform device and allocate/fill the platform data from the probe method and register the platform device. But this is only a temporary fix, and DT bindings should be introduced at some point in the driver.

Q: The new pinmuxing method sucks; why can't I pinmux by the old method at runtime?

A: The old pinmux method was TI specific and although it made it in the mainline, it has no DT bindings and is duplicating functionality of the general pinmux framework. The new method is portable, but it's reliance on raw numbers is a pain. However it does offer safety, since if a device obtains the pin resources there's no way for them to be modified by some outside factor. To help in the transition a full set of peripheral drivers and their pinmux settings will be introduced shortly.

Additionally there is a pinmux helper driver that allows runtime switching between different per-state pinmux-settings. For example:

bar_pinmux_helper { compatible = "bone-pinmux-helper"; status = "okay"; pinctrl-names = "input", "output"; pinctrl-0 = <&bar_input_gpio_pins>; pinctrl-1 = <&bar_output_gpio_pins>; };

This device node creates a pinmux helper device which defines two pinmux states, named input and output. You can switch between them simply by

# echo input >/sys/devices/ocp*/bar_pinmux_helper*/state # echo output >/sys/devices/ocp*/bar_pinmux_helper*/state

Note that there is a very slim chance for this driver to be admitted in mainline, but you can use it if it helps you port to the new kernel.

'''Q: How do I support any kind GPIO/I2C/SPI etc. device. It is completely different.'''

A: It is. Examples will be provided on how to do most of the standard peripheral configurations. What you get for the change is the ability to create any kind of device without having to recompile the kernel.

Q: My LCD/GPMC/$RANDOM based device doesn't work on the black.

A: The eMMC and HDMI parts of the Black are using the same pins. If you define a cape with the proper resource definitions, the user cape will take precedence. Hardware restrictions might apply (i.e. pin loading issues etc).

Q: Why can't you carry on using the board file method, seems to work just fine.

A: The writing was on the wall; no way we could ever get the beaglebone in the mainline using it. On top of that any new cape that is produced, either by TI/CCO or the community had to patch the board file to make it work. While this is doable when you have a couple of capes, when you expect dozens of capes, most of them produced by community members, having to expect them to hack the board file, is completely out of the question.

'''Q: The pinmux values are really hard to figure out from the manuals. One has to hunt in the TRM, the datasheet of the specific processor, and the beaglebone SRM to figure them out.'''

A: There are resources with the muxing options. Please see: Pinmux Tables

'''Q: Too many words! Grog angry! Concrete example!'''

A: Example of an RS232 cape that uses UART2:

/*	 * Copyright (C) 2013 Matt Ranostay  *	 * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. */	/dts-v1/; /plugin/;

/ {		compatible = "ti,beaglebone", "ti,beaglebone-black";

/* identification */ part-number = "BB-BONE-RS232"; version = "00A0"; /* state the resources this cape uses */ exclusive-use = /* the pin header uses */ "P9.22",               /* rs232: uart2_rxd */ "P9.21",               /* rs232: uart2_txd */ /* the hardware IP uses */ "uart2";

fragment@0 { target = <&am33xx_pinmux>; __overlay__ { uart_pins: pinmux_uart_pins { pinctrl-single,pins = < 0x150       0x21        /* spi0_sclk.uart2_rxd | * MODE1 | PULL_UP */ 0x154       0x01        /* spi0_d0.uart2_txd | MODE1 */ >;				};			};		};

fragment@1 { target = <&uart3>;       /* remember the +1 peculiarity */ __overlay__ { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart_pins>; };		};	};

This file is located in the kernel sources at

firmware/capes/BB-BONE-RS232-00A0.dts

Compile with:

$ dtc -O dtb -o BB-BONE-RS232-00A0.dtbo -b 0 -@ BB-BONE-RS232-00A0.dts

Copy to /lib/firmware:

# cp BB-BONE-RS232-00A0.dtbo /lib/firmware

Activate:

# echo BB-BONE-RS232 >/sys/devices/bone_capemgr*/slots

'''Q: I still can’t figure it out! Any pointers out there on how to do stuff?'''

A: A lot of examples in firmware/capes exist

UART virtual capes: firmware/capes/BB-UART*.dts I2C virtual capes: firmware/capes/BB-I2C*.dts SPI virtual capes: firmware/capes/BB-SPI*.dts

# echo BB-UART5 >/sys/devices/bone_capemgr*/slots

Configures UART5, a new /dev/ttyOx device will be created (devices are created sequentially) and will be ready to use.

Similarly

# echo BB-I2C1 >/sys/devices/bone_capemgr*/slots

Creates a new I2C bus master. Note that when you have your own I2C devices that you need to access, you can either use the user-space I2C API, or create a device node for your device in the cape. The example capes have commented out examples on how to do that. All those virtual capes can be used as a base for your own experimentation.

Q: I still hate Device Tree!!!

A: Sorry, we cannot help you