Linux e o suporte à device tree – Parte 1
- por Sergio Prado
O uso do Linux em plataformas ARM cresceu tão rápido que o código em arch/arm/ virou em pouco tempo uma “terra sem lei”. Muito código duplicado, apoiado em poucos frameworks, sem padrão nenhum. Até que um dia Linus Torvalds demonstrou toda a sua revolta (que refletia também a revolta de muitos outros desenvolvedores do kernel para ARM):
“No concentrated effort to have a framework for things… since we try to support a lot of the ARM architecture, it’s been a painful thing for me to see, look at the x86 tree and ARM tree and it’s many times bigger. It’s not constrained by this nice platform thing, it just has random crap all over it. And it was getting to me[…] I just snapped, and instead of running around naked with a chainsaw like I usually do, I started talking to people and a lot of people admitted it’s a problem.”
O problema foi agravado pela quantidade crescente de SoCs ARM usando o kernel Linux no mercado, de smartphones à tablets e netbooks.
Então as coisas começaram a mudar. Os desenvolvedores da plataforma ARM começaram a criar diversos frameworks (pinctrl, clocks, etc) e aos poucos começaram organizaram o código. A Linaro, uma organização sem fins lucrativos focada na plataforma ARM, teve um papel fundamental neste trabalho realizado no suporte do kernel Linux em plataformas ARM.
Um dos grandes problemas que a plataforma ARM vem enfrentando é a enorme quantidade de código duplicado para os diferentes SoCs e placas suportadas pelo kernel. Cada SoC e placa suportada pelo kernel possui diferentes configurações de CPU, memória e periféricos. E cada configuração possui um procedimento para iniciar e configurar os periféricos no boot, sem padrão nenhum, tudo codificado dentro do kernel.
Além dos problemas com duplicação de código, qualquer alteração em um dispositivo de hardware requer uma recompilação do kernel. Pelo mesmo motivo, fica difícil liberar uma mesma imagem do kernel que possa suportar diferentes configurações de SoC e placa, como por exemplo uma única imagem do kernel que possa funcionar na Beagleboard e no i.MX53 QSB.
O kernel para ARM precisava então de um mecanismo para identificar a topologia e configuração do hardware (CPU, memória, dispositivos de I/O) em tempo de execução. A solução completa veio na versão 3.7 do kernel, com a funcionalidade de Device Tree.
Mas antes de estudarmos como o Device Tree funciona, vamos entender melhor o problema e estudar o mecanismo atual usado pelo kernel em plataformas ARM para identificar e inicializar os dispositivos de hardware conectados ao sistema.
DRIVERS E BARRAMENTOS
Existem basicamente duas formas de conectar um dispositivo de hardware ao sistema:
- Um dispositivo de hardware podem estar conectado à um determinado barramento do sistema (I2C, SPI, PCI, USB, etc).
- Um dispositivo pode estar ligado diretamente à CPU, sendo acessível via comandos de I/O ou registradores mapeados em memória.
Quando o dispositivo esta conectado à um barramento, o driver que trata este dispositivo deve usar as funções disponibilizadas pelo kernel para conversar com o barramento. Ou seja, o driver não deve acessar diretamente o barramento, e sim usar a API do kernel para isso.
Desta forma, um driver de dispositivo conectado ao barramento I2C deve implementar um i2c_driver e usar funções do barramento I2C como a i2c_smbus_write_byte_data() para enviar um byte para o barramento. Da mesma forma, um dispositivo USB deve implementar um usb_driver e usar funções como a usb_submit_urb() para enviar um pacote para o barramento USB.
E no caso de dispositivos conectados diretamente à CPU? Para estes casos, foi criada uma abstração chamada platform driver, permitindo com que um driver possa se registrar em um “barramento virtual” do sistema, já que fisicamente o barramento não existe. Desta forma, um driver de dispositivo conectado diretamente à CPU deve implementar e registrar no kernel uma estrutura do tipo platform_driver.
Seja um i2c_driver, usb_driver ou platform_driver, uma funcionalidade interessante da infraestrutura de barramentos do kernel é a capacidade de prover informações dos recursos de hardware usado pelos dispositivo conectados ao sistema.
DRIVERS E RECURSOS DE HARDWARE
Dispositivos de hardware usam recursos da máquina, sejam portas de I/O, linhas de interrupção ou endereços de I/O mapeados em memória. E os drivers que tratam estes dispositivos precisam conhecer todos os recursos necessários para acessar determinado hardware corretamente.
Por exemplo, um driver de porta serial integrado à um SoC ARM precisa conhecer o endereço de I/O mapeado em memória dos registradores de acesso à UART do chip, assim como um dispositivo PCI precisa conhecer a linha de interrupção ao qual ele foi associado.
Estas informações são codificadas diretamente no driver do dispositivo? Não, porque se estivesse, o driver seria dependente destas informações, e consequentemente não seria portável. Isso quebraria umas das regras de desenvolvimento do kernel: todo código fora do diretório arch/ deve ser portável!
Quem então resolve este problema é a infraestrutura de barramento, separando a implementação do driver da definição do dispositivo de hardware!
OK, Sergio. Mas como isso funciona?
Para exemplificar, vamos dar uma olhada no driver da porta serial implementado para a linha i.MX da Freescale (kernel 2.6.35 disponível no BSP da Freescale).
PLATFORM DRIVER
O driver da porta serial implementado para a linha i.MX da Freescale encontra-se nos fontes do kernel em drivers/serial/mxc_uart.c. Como ele gerencia um dispositivo de hardware conectado diretamente à CPU, ele é implementado através de um platform driver, conforme abaixo:
1 2 3 4 5 6 7 8 |
static struct platform_driver mxcuart_driver = { .driver = { .name = "mxcintuart", }, .probe = mxcuart_probe, .remove = mxcuart_remove, [...] }; |
Um platform driver deve registrar no mínimo as funções de callback probe() e remove(). Veremos mais sobre estas funções adiante.
E na inicialização do driver, o platform_driver é registrado com a função platform_driver_register():
1 2 3 4 5 6 |
static int __init mxcuart_init(void) { [...] ret = platform_driver_register(&mxcuart_driver); [...] } |
A partir deste momento, o kernel sabe que existe um driver para tratar dispositivos do tipo “mxcintuart” (vide nome do driver na estrutura platform_driver). Mas ele ainda não sabe que existe um dispositivo deste tipo no sistema. Onde então este dispositivo é definido? Na implementação do platform device!
PLATFORM DEVICE
Um platform device é normalmente definido no machine layer da plataforma (arch/arm/). No nosso exemplo, o platform device da porta serial esta implementado no arquivo arch/arm/mach-mx5/serial.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
static struct resource mxc_uart_resources1[] = { { .start = UART1_BASE_ADDR, .end = UART1_BASE_ADDR + 0x0B5, .flags = IORESOURCE_MEM, }, { .start = MXC_INT_UART1, .flags = IORESOURCE_IRQ, }, }; static struct platform_device mxc_uart_device1 = { .name = "mxcintuart", .id = 0, .num_resources = ARRAY_SIZE(mxc_uart_resources1), .resource = mxc_uart_resources1, .dev = { .platform_data = &mxc_ports[0], }, }; |
Perceba que a estrutura platform_device armazena outra estrutura do tipo resource, que contém as informações do dispositivo de hardware (no nosso caso uma região de I/O mapeada em memória e uma linha de IRQ).
Na inicialização do placa, a função mxc_init_uart() é executada, registrando o dispositivo no sistema com a função platform_device_register():
1 2 3 4 5 6 |
static int __init mxc_init_uart(void) { [...] platform_device_register(&mxc_uart_device1); [...] } |
É aqui que a mágica acontece. Já existe um platform_driver registrado no sistema com o nome “mxcintuart“. Agora existe também um platform_device registrado no sistema com o mesmo nome “mxcintuart“. O kernel irá fazer o match do platform_driver com o platform_device pelo nomes registrados e automaticamente chamará a função probe() do driver!
A FUNÇÃO PROBE()
A função probe() tem o objetivo de inicializar o dispositivo, mapear I/O em memória e registrar rotinas de tratamento de interrupção se necessário. A infraestrutura de barramento normalmente provê mecanismos para ler endereçamento de I/O, número de interrupções e outras informações específicas do dispositivo. No nosso caso, a rotina probe() do driver da porta serial usa as funções platform_get_resource() e platform_get_irq() providas pelo platform device para ler respectivamente o endereço de I/O mapeado em memória e a linha de IRQ da porta serial:
1 2 3 4 5 6 7 8 9 10 |
static int mxcuart_probe(struct platform_device *pdev) { [...] res = platform_get_resource(pdev, IORESOURCE_MEM, 0); [...] mxc_ports[id]->irqs[0] = platform_get_irq(pdev, 1); [...] } |
Com as informações de acesso ao dispositivo de hardware (endereço de I/O e linha de interrupção), o driver pode acessar diretamente o hardware.
Usando as abstrações de platform_driver e platform_device, mantemos o driver genérico e portável. Se por acaso em um outro SoC a linha de IRQ da porta serial mudar, não precisamos alterar o driver, mas sim apenas alterar a definição da estrutura resource no platform_device!
E FUNCIONA?
Sim, este mecanismo funciona muito bem. Mas ele tem algumas deficiências.
As informações dos dispositivos estão codificadas dentro do kernel (veja o exemplo da estrutura resource mais acima). Se você quiser por exemplo incluir um dispositivo SPI ou alterar o endereço de um dispositivo I2C, precisa criar ou alterar a definição dos platform devices correspondentes e recompilar o kernel. A situação ficar pior se você quiser ter uma única imagem do kernel para diferentes plataformas de mesma arquitetura (ex: i.MX53 da Freescale e OMAP da Texas).
Para estes casos, o kernel precisa de um mecanismo para identificar a topologia e a configuração do hardware em tempo de execução. Em arquiteturas x86 este processo funciona perfeitamente com a ajuda da BIOS/UEFI. Já em outras arquiteturas como PPC e ARM, a solução para este problema chama-se Device Tree.
Mas este é um assunto para a segunda parte deste artigo!
Até lá!
Sergio Prado