Linux e o suporte à device tree – Parte 2

- por Sergio Prado

Categorias: Beagleboard-xM, i.MX53 Quick Start Board, Linux Tags: , , , , ,

No artigo anterior estudamos como o kernel Linux identifica e inicializa os dispositivos de hardware durante o boot do sistema. Vimos que, pelo fato das informações dos dispositivos e da topologia de hardware estarem codificadas estaticamente dentro do kernel, qualquer mudança no hardware requer alteração dos fontes e recompilação do kernel. Pelo mesmo motivo, é muito difícil manter uma única imagem do kernel que seja inicializável por diferentes SoCs e plataformas.

A solução para este problema chama-se Device Tree.

DEVICE TREE

Em linhas gerais, o device tree é uma estrutura de dados capaz de descrever o hardware disponível no sistema. Durante o processo de boot, o bootloader passa o device tree para o sistema operacional, que usa as informações disponíveis no device tree para identificar a topologia do hardware e inicializar os dispositivos em tempo de execução.

Com o device tree é possível descrever o tipo e a quantidade de CPUs presentes no sistema, o endereço base e o tamanho da memória RAM, os barramentos e dispositivos conectados aos barramentos, além de informações específicas de cada dispositivo como linhas de interrupção, endereços de I/O, etc.

O device tree foi criado originalmente pelo projeto Open Firmware, um padrão de firmware de boot desenvolvido inicialmente pela Sun, como parte de um método de comunicação entre o Open Firmware e um sistema operacional.

Como o Open Firmware é bastante usado por plataformas PowerPC e SPARC, o suporte à device tree no Linux para estas plataformas é bem antigo. Em 2005, no trabalho de integração do PPC 32 bits e 64 bits, tornou-se obrigatório o uso do DT em todas plataformas PPC. Para isso, criou-se uma variação do device tree chamada Flattened Device Tree (FDT), que transforma o device tree, originalmente um arquivo texto, em um arquivo binário chamado DTB (Device Tree Binary ou Device Tree Blob).

Algum tempo se passou e a funcionalidade de Flattened Device Tree foi padronizada no kernel, começando a ser utilizada em diversas outras plataformas além do PPC, incluindo ARM, Microblaze e MIPS. Bootloaders como o U-Boot foram alterados de forma que pudessem passar o DTB para o kernel no boot do sistema. Em específico, na versão 3.7 do kernel, tornou-se obrigatório o uso de Device Tree em plataformas ARM.

Mas como isso funciona na prática?

DEVICE TREE NA PRÁTICA

Cada plataforma ARM possui um arquivo de especificação do device tree, disponível em arch/arm/boot/dts:

$ ls arch/arm/boot/dts/
aks-cdu.dts                     at91sam9x35ek.dts         exynos5440.dtsi           imx6q-sabresd.dts            msm8960-cdp.dts                 samsung_k3pe0e000b.dtsi   tegra30-cardhu-a02.dts
am335x-bone.dts                 at91sam9x5cm.dtsi         exynos5440-ssdk5440.dts   integratorap.dts             omap2420.dtsi                   sh7372.dtsi               tegra30-cardhu-a04.dts
am335x-evm.dts                  at91sam9x5.dtsi           ge863-pro3.dtsi           integratorcp.dts             omap2420-h4.dts                 sh7372-mackerel.dts       tegra30-cardhu.dtsi
am335x-evmsk.dts                at91sam9x5ek.dtsi         highbank.dts              integrator.dtsi              omap2430.dtsi                   sh73a0-kzm9g.dts          tegra30.dtsi
am33xx.dtsi                     bcm11351-brt.dts          href.dtsi                 kirkwood-6281.dtsi           omap2.dtsi                      skeleton.dtsi             testcases
am3517-evm.dts                  bcm11351.dtsi             hrefprev60.dts            kirkwood-6282.dtsi           omap36xx.dtsi                   snowball.dts              tny_a9260_common.dtsi
am3517_mt_ventoux.dts           bcm2835.dtsi              hrefv60plus.dts           kirkwood-98dx4122.dtsi       omap3-beagle.dts                socfpga_cyclone5.dts      tny_a9260.dts
animeo_ip.dts                   bcm2835-rpi-b.dts         imx23.dtsi                kirkwood-dns320.dts          omap3-beagle-xm.dts             socfpga.dtsi              tny_a9263.dts
armada-370-db.dts               ccu9540.dts               imx23-evk.dts             kirkwood-dns325.dts          omap3.dtsi                      spear1310.dtsi            tny_a9g20.dts
...

A especificação do device tree é um arquivo texto, que deve ser convertido em um binário (DTB) antes de ser utilizado. Para isso, o kernel fornece um mecanismo para gerar o DTB, usando uma ferramenta chamada dtc (device tree compiler). Exemplo de geração do dtb para a Beagleboard-xM:

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- omap3-beagle-xm.dtb

Será gerado o arquivo dtb no diretório arch/arm/boot/dts. Agora é só transferir este arquivo para o bootloader e configurá-lo para passar o DTB para o kernel no boot do sistema. O bootloader faz isso copiando o DTB para um endereço específico da memória RAM e passando este endereço para o kernel através de um registrador da CPU (r2 em arquiteturas ARM).

Os mais atentos podem perceber que este mecanismo de carga do DTB é bem parecido com o mecanismo do initrd.

Da mesma forma, se o bootloader não suportar a carga do DTB, é possível gerar uma imagem do kernel compilada estaticamente com o DTB, bastando para isso habilitar a opção CONFIG_ARM_APPENDED_DTB na configuração do kernel.

Que tal estudar agora a especificação de um device tree?

Para exemplificar, vamos dar uma olhada em alguns trechos do device tree da Beagleboard-xM, disponível nos fontes do kernel em arch/arm/boot/dts/omap3-beagle-xm.dts.

DEVICE TREE NA BEAGLEBOARD-XM

Estruturalmente, os dados de um device tree são representados através de uma árvore composta por vários nós. Cada nó contém uma ou mais propriedades, encapsulando dados. Pode-se ter nós dentro outros nós, possibilitando representar de forma hierárquica a topologia de hardware do sistema.

Vamos dar uma olhada em um trecho do device tree da Beagleboard-xM:

/ {
        model = "TI OMAP3 BeagleBoard xM";
        compatible = "ti,omap3-beagle-xm, ti,omap3-beagle", "ti,omap3";
 
        memory {
                device_type = "memory";
                reg = <0x80000000 0x20000000>; /* 512 MB */
        };
 
        leds {
                compatible = "gpio-leds";
                pmu_stat {
                        label = "beagleboard::pmu_stat";
                        gpios = <&twl_gpio 19 0>; /* LEDB */
                };
 
                heartbeat {
                        label = "beagleboard::usr0";
                        gpios = <&gpio5 22 0>; /* 150 -> D6 LED */
                        linux,default-trigger = "heartbeat";
                };
 
                mmc {
                        label = "beagleboard::usr1";
                        gpios = <&gpio5 21 0>; /* 149 -> D7 LED */
                        linux,default-trigger = "mmc0";
                };
        };
 
        sound {
                compatible = "ti,omap-twl4030";
                ti,model = "omap3beagle";
 
                ti,mcbsp = <&mcbsp2>;
                ti,codec = <&twl_audio>;
        };
};

O device tree começa definindo a plataforma através das propriedades “model” e “compatible” em seu nó principal.

A propriedade “model” é usada apenas de forma indicativa, para exibir por exemplo o nome da plataforma no boot do kernel. Já a propriedade “compatible” define a compatibilidade da plataforma, no formato “fabricante,modelo“. A lista de compatibilidades vai da mais específica (ti,omap3-beagle-xm) à mais genérica (ti,omap3). O kernel usa esta lista de compatibilidades para saber com qual plataforma esta trabalhando e o que deve executar/inicializar durante o processo de boot.

Dentro deste nó, temos ainda a definição da memória do sistema, dos leds e da controladora de som da Beagleboard-xM.

Mais para frente no mesmo device tree temos a definição do 3o. barramento I2C:

&i2c3 {
        clock-frequency = <100000>;
 
        /*
         * Display monitor features are burnt in the EEPROM
         * as EDID data.
         */
        eeprom@50 {
                compatible = "ti,eeprom";
                reg = ;
        };
};

O nó chama-se i2c3. Dentro deste nó temos a especificação da frequência do barramento definida pela propriedade “clock-frequency“. Ainda dentro deste nó temos um outro nó especificando o dispositivo “eeprom” conectado ao endereço 0x50 do barramento I2C. É a propriedade “compatible” que define o nome do driver que irá tratar este dispositivo. Quando um driver se registrar no sistema declarando ser compatível com “ti,eeprom“, o kernel irá chamar a função probe() deste driver, para que ele possa inicializar a e2prom conectada no endereço 0x50 do barramento I2C!

Existe uma certa convenção das propriedades que podem ser usadas em um nó. Esta convenção é chamada de “binding“. Por exemplo, este é o binding para dispositivos I2C em plataformas OMAP (disponível em Documentation/devicetree/bindings/i2c/i2c-omap.txt):

I2C for OMAP platforms
 
Required properties :
- compatible : Must be "ti,omap3-i2c" or "ti,omap4-i2c"
- ti,hwmods : Must be "i2c<n>", n being the instance number (1-based)
- #address-cells = <1>;
- #size-cells = <0>;
 
Recommended properties :
- clock-frequency : Desired I2C bus clock frequency in Hz. Otherwise
  the default 100 kHz frequency will be used.
 
Optional properties:
- Child nodes conforming to i2c bus binding
 
Note: Current implementation will fetch base address, irq and dma
from omap hwmod data base during device registration.
Future plan is to migrate hwmod data base contents into device tree
blob so that, all the required data will be used from device tree dts
file.
 
Examples :
 
i2c1: i2c@0 {
    compatible = "ti,omap3-i2c";
    #address-cells = <1>;
    #size-cells = <0>;
    ti,hwmods = "i2c1";
    clock-frequency = <400000>;
};

Desta forma, e seguindo a convenção, nenhuma alteração é necessária no kernel para tratar determinada propriedade de um nó. Ao criar um device tree para a sua plataforma, tente então evitar ao máximo a criação de novos bindings no device tree!

E como ficaria a definição das portas seriais no i.MX53 que vimos no artigo anterior?

DEVICE TREE NO IMX53

Estas definições estão em arch/arm/boot/dts/imx53.dtsi:

uart1: serial@53fbc000 {
        compatible = "fsl,imx53-uart", "fsl,imx21-uart";
        reg = <0x53fbc000 0x4000>;
        interrupts = <31>;
        clocks = <&clks 28>, <&clks 29>;
        clock-names = "ipg", "per";
        status = "disabled";
};

A propriedade “reg” representa o endereço de I/O mapeado em memória usado pela UART, assim como a propriedade “interrupt” representa a linha de IRQ. Durante o boot, o kernel irá procurar um driver que tenha sido registrado com a propriedade “compatible” identica à registrada no device tree (“fsl,imx53-uart” ou “fsl,imx21-uart“). Quando ele encontrar, a função probe() do driver será chamada!

Desta forma, para um driver ser compatível com o device tree, é necessária a definição da estrutura of_device_id. Veja o exemplo abaixo para a implementação do driver da UART para a linha i.MX da Freescale em drivers/tty/serial/imx.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static struct of_device_id imx_uart_dt_ids[] = {
        { .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], },
        { .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
        { /* sentinel */ }
};
 
static struct platform_driver serial_imx_driver = {
        .probe          = serial_imx_probe,
        .remove         = serial_imx_remove,
        [...]
        .driver         = {
                [...]
                .of_match_table = imx_uart_dt_ids,
        },
};

Você pode encontrar mais informações sobre o device tree na documentação do kernel em Documentation/devicetree/booting-without-of.txt, ou na página de especificação do Device Tree.

Agora só falta ver se tudo isso funciona na prática! No próximo artigo, iremos testar o uso do device tree na Beagleboard-xM e no i.MX53 QSB.

Até lá!

Sergio Prado

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.