Ferramentas e técnicas para gerenciar device tree
- por Sergio Prado
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