Linux e o suporte à device tree – Parte 2
- por Sergio Prado
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