Ferramentas e técnicas para gerenciar device tree

- por Sergio Prado

Categorias: Linux Tags: , ,

Para fazer o boot do kernel Linux em algumas plataformas de hardware (ARM, PPC, etc), é necessário um arquivo chamado device tree. Neste arquivo, é feita toda a descrição do hardware para o sistema operacional, incluindo o tipo e quantidade de CPUs, quantidade e endereço inicial de memória RAM, barramentos existentes e dispositivos de I/O conectados aos barramentos. Durante o boot, o kernel Linux irá interpretar este arquivo para identificar o hardware e instanciar os drivers correspondentes.

Para quem estiver interessado nos conceitos do device tree, há algumas semanas apresentei um webinar em parceria com a Toradex que está disponível online nas versões português e inglês.

O uso do device tree, em especial na arquitetura ARM, trouxe diversas vantagens, permitindo gerar uma imagem do kernel mais genérica e independente das características da plataforma de hardware, além de facilitar bastante a manutenção do código-fonte do kernel.

Por outro lado, um desenvolvedor de kernel ganhou novas responsabilidades, incluindo a necessidade de aprender a sintaxe e a semântica do device tree para adicionar o suporte a novas placas e dispositivos de hardware no kernel Linux.

E para trabalhar com device tree, podemos contar com a ajuda de algumas ferramentas, que têm evoluído a cada nova versão do kernel. Neste artigo, vou apresentar as ferramentas dtc, dt_to_config e dtx_diff, todas disponíveis no diretório scripts/dtc/, dentro do código-fonte do kernel.

$ ls scripts/dtc/
checks.c  dtc          dtc-lexer.lex.c          dtc-parser.tab.c          dtc-parser.tab.o  fdtdump.c   flattree.o        libfdt      Makefile.dtc  treesource.c          util.h
checks.o  dtc.c        dtc-lexer.lex.c_shipped  dtc-parser.tab.c_shipped  dtc-parser.y      fdtget.c    fstree.c          livetree.c  srcpos.c      treesource.o          util.o
data.c    dtc.h        dtc-lexer.lex.o          dtc-parser.tab.h          dt_to_config      fdtput.c    fstree.o          livetree.o  srcpos.h      update-dtc-source.sh  version_gen.h
data.o    dtc-lexer.l  dtc.o                    dtc-parser.tab.h_shipped  dtx_diff          flattree.c  include-prefixes  Makefile    srcpos.o      util.c

Caso você queira replicar os meus testes na sua máquina, é bom ressaltar que todos os comandos abaixo foram executados na raiz do código-fonte do kernel Linux 4.12.8, clonado a partir do repositório mainline do Linus Torvalds.

Para facilitar o acesso às ferramentas, eu coloquei o diretório onde elas se encontram no PATH do meu ambiente:

$ export PATH=$PWD/scripts/dtc/:$PATH

DTC

O dtc é o compilador do device tree.

Normalmente não é necessário chamá-lo diretamente, já que os makefiles do kernel possuem os targets para compilar o device tree, conforme exemplo abaixo para a Beaglebone Black:

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- am335x-boneblack.dtb

Mas nada nos impede de compilar o device tree diretamente executando o dtc. Existe apenas um complicador aqui. Antes de ser compilado, o arquivo-fonte de device tree (DTS) precisa passar por um pré-processador devido ao uso de diretivas de pré-processamento. Portanto, precisamos rodar antes o cpp para pré-processar o DTS e gerar um arquivo intermediário, que então é compilado com o dtc para gerar o DTB.

$ cpp -Iinclude -E -P -x assembler-with-cpp arch/arm/boot/dts/am335x-boneblack.dts arch/arm/boot/dts/am335x-boneblack.dts.tmp
$ dtc -I dts -O dtb -o arch/arm/boot/dts/am335x-boneblack.dtb arch/arm/boot/dts/am335x-boneblack.dts.tmp

Podemos também executar a mesma operação em um único comando:

$ cpp -Iinclude -E -P -x assembler-with-cpp arch/arm/boot/dts/am335x-boneblack.dts | dtc -I dts -O dtb -i arch/arm/boot/dts -o arch/arm/boot/dts/am335x-boneblack.dtb

Bom, o fato é que o dtc faz muito mais do que compilar o DTS e gerar o DTB.

Um device tree de uma placa normalmente é composto por um conjunto de arquivos para organizar e facilitar a manutenção. O arquivo principal tem extensão DTS, e pode incluir arquivos com extensão DTSI, que por sua vez também podem incluir outros arquivos DTSI. Por exemplo, o device tree da Beaglebone Black (am335x-boneblack.dts) inclui outros três arquivos (am335x-boneblack-common.dtsi, am335x-bone-common.dtsi, am33xx.dtsi).

Este mecanismo de inclusão deixa o código do device tree bastante modular, porém dificulta o desenvolvimento, tornando às vezes mais complicado identificar a localização de um determinado nó, o conteúdo de uma determinada propriedade, etc. Seria legal se pudéssemos ter um arquivo único do device tree com o valor final dos nós e propriedades.

O dtc pode nos ajudar neste caso. Se passarmos para ele o DTB, ele é capaz de gerar o DTS na forma de um arquivo único, facilitando bastante a análise do conteúdo final do device tree.

$ dtc -I dtb -O dts -o out.dts arch/arm/boot/dts/am335x-boneblack.dtb

É claro que perderemos algumas informações no processo, como os comentários e os labels, que não vão para o DTB.

DT_TO_CONFIG

O dt_to_config é uma ferramenta capaz de identificar o código-fonte do driver responsável por um nó do device tree e sua respectiva opção de configuração no kernel.

Se executarmos esta ferramenta em um device tree qualquer:

$ dt_to_config am335x-boneblack.dts

Ela irá retornar todos os nós existentes do device tree (um nó por linha), o código-fonte do driver responsável por este nó e o nome da opção de configuração do kernel para habilitar este driver:

-d-c--------- : /clk_mcasp0 : gpio-gate-clock : drivers/clk/clk-gpio.c : CONFIG_COMMON_CLK : none
-dDc--------- : /clk_mcasp0_fixed : fixed-clock : arch/powerpc/platforms/512x/clock-commonclk.c : CONFIG_COMMON_CLK : none
-dDc--------- : /clk_mcasp0_fixed : fixed-clock : drivers/clk/clk-fixed-rate.c : CONFIG_COMMON_CLK : none
-dDc--------- : /clk_mcasp0_fixed : fixed-clock : drivers/clk/clk-u300.c : CONFIG_ARCH_U300 : none
-dDc--------- : /clk_mcasp0_fixed : fixed-clock : drivers/clk/imx/clk-imx27.c : CONFIG_SOC_IMX27 : none
-dDc--------- : /clk_mcasp0_fixed : fixed-clock : drivers/clk/imx/clk-imx31.c : CONFIG_SOC_IMX31 : none
-dDc----x---- : /clk_mcasp0_fixed : fixed-clock : drivers/clk/ti/clk.c : CONFIG_ARCH_OMAP2PLUS && obj-y : none
-dDc--------- : /clk_mcasp0_fixed : fixed-clock : drivers/usb/host/xhci-mtk.c : CONFIG_USB_XHCI_MTK : none
-dDc----x---- : /clk_mcasp0_fixed : fixed-clock : drivers/usb/mtu3/mtu3_plat.c : mtu3-y : none
[...]

Imagine por exemplo que você tenha declarado no device tree o sensor de temperatura TMP007 no barramento I2C2, conforme abaixo:

&i2c2 {
        tmp007@40 {
                compatible = "ti,tmp007";
                reg = ;
        };
};

Como você descobre onde está o código-fonte do driver deste sensor, e qual a opção de configuração do kernel para habilitá-lo? Simples, usando a ferramenta dt_to_config:

$ dt_to_config arch/arm/boot/dts/am335x-boneblack.dts | grep tmp
-d-c--------- : /ocp/i2c@4819c000/tmp007@40 : ti,tmp007 : drivers/iio/temperature/tmp007.c : CONFIG_TMP007 : none

Perceba que o primeiro campo retornado por esta ferramenta é um conjunto de flags que representa o estado atual do respectivo nó do device tree. A documentação destas flags pode ser exibida no help da ferramenta:

$ dt_to_config -h
[...]
 
FLAG values:
     M   multiple compatibles found for this node
     d   driver found for this compatible
     D   multiple drivers found for this compatible
     c   kernel config found for this driver
     C   multiple config options found for this driver
     E   node is not enabled
     W   compatible is white listed
     H   matching driver and/or kernel config is hard coded
     x   kernel config hard coded in Makefile
     n   one or more kernel config file options is not set
     m   one or more kernel config file options is set to 'm'
     y   one or more kernel config file options is set to 'y'
     F   one of more kernel config file options fails to have correct value
 
[...]

Estas flags são muito úteis.

Imagine que você queira identificar todos os nós desabilitados do device tree. Para isso, basta utilizar a opção –include-flag e filtrar pela flag E:

$ dt_to_config --include-flag E arch/arm/boot/dts/am335x-boneblack.dts

Esta ferramenta também é capaz de gerar um arquivo de configuração completo do kernel (.config) a partir de um device tree. Mas por diversos motivos (múltiplos drivers para o mesmo nó, drivers que ainda não suportam device tree, etc), ainda pode exigir intervenção humana. Este fato é ressaltado na documentação da ferramenta:

This program uses heuristics to guess which driver(s) support each compatible string and which config option(s) enables the driver(s). Do not believe that the reported information is fully correct. This program is intended to aid the process of determining the proper kernel configuration for a device tree, but this is not a fully automated process — human involvement may still be required!

DTX_DIFF

Por fim, a ferramenta dtx_diff é capaz de comparar dois arquivos de device tree e indicar as diferenças entre eles.

Por exemplo, qual a diferença entre o hardware da Beaglebone Black e o hardware da Beaglebone Black Wireless? O dtx_diff pode responder esta pergunta:

$ dtx_diff arch/arm/boot/dts/am335x-boneblack.dts arch/arm/boot/dts/am335x-boneblack-wireless.dts
 
--- /dev/fd/63	2017-08-24 16:24:51.762886933 -0300
+++ /dev/fd/62	2017-08-24 16:24:51.766886933 -0300
@@ -3,9 +3,9 @@
 / {
 	#address-cells = ;
 	#size-cells = ;
-	compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
+	compatible = "ti,am335x-bone-black-wireless", "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
 	interrupt-parent = ;
-	model = "TI AM335x BeagleBone Black";
+	model = "TI AM335x BeagleBone Black Wireless";
 
 	aliases {
 		d_can0 = "/ocp/can@481cc000";
@@ -33,19 +33,19 @@
 
[...]

Com o dtx_diff, também é possível (e mais fácil) gerar um arquivo consolidado do device tree:

$ dtx_diff arch/arm/boot/dts/am335x-boneblack.dts > out.dts

Outro caso de uso interessante do dtx_diff é identificar se o bootloader alterou o device tree antes de passá-lo para o kernel.

O Linux exporta uma visão do device tree através de diretórios e arquivos em /proc/device-tree:

# ls /proc/device-tree/
#address-cells    clk_mcasp0_fixed  leds              opp-table
#size-cells       compatible        memory@80000000   pmu
aliases           cpus              model             soc
chosen            fixedregulator0   name              sound
clk_mcasp0        interrupt-parent  ocp

Podemos copiar este diretório para nossa máquina de desenvolvimento e utilizar o dtx_diff para compará-lo com o arquivo de device tree (DTB) original:

$ dtx_diff arch/arm/boot/dts/am335x-boneblack.dtb device-tree/
 
--- /dev/fd/63	2017-08-24 16:34:58.942893103 -0300
+++ /dev/fd/62	2017-08-24 16:34:58.942893103 -0300
@@ -28,6 +28,7 @@
 	};
 
 	chosen {
+		bootargs = "console=ttyO0,115200n8 ip=192.168.0.2 root=/dev/nfs nfsroot=192.168.0.1:/mnt/nfs/bbb rw rootwait capemgr.disable_partno=BB-BONELT-HDMI,BB-BONELT-HDMIN";
 		stdout-path = "/ocp/serial@44e09000";
 	};
 
[...]
 
 	memory@80000000 {
 		device_type = "memory";
-		reg = ;
+		reg = ;
 	};
 
[...]

Veja neste exemplo que o bootloader faz algumas alterações no device tree, incluindo a linha de comandos do kernel e o tamanho da memória RAM. Legal, não?

Estas três ferramentas são realmente de grande ajuda, e podem facilitar bastante o trabalho de desenvolvimento e depuração de sistemas com device tree.

Lição do dia? Know your tools! :-)

Um abraço,

Sergio Prado

Faça um Comentário

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