Portando um driver de LED de flash para o kernel oficial

Traduções: en
Data de publicação: 20/11/2020
Rótulos: nexus5, kernel

Agora que eu já tinha um cabo serial funcionando para o meu Nexus 5, como descrito no artigo "Fazendo um cabo UART para o Nexus 5", eu estava pronto para a ação em ajudar a trazer o Nexus 5 para o kernel oficial.

Olhando a página de pendências do Brian Masney haviam algumas opções, mas a que eu decidi fazer foi a de lanterna traseira. Não tem hardware mais simples que um LED e seria fácil testar se estava funcionando.

Mas para a minha surpresa, o driver no kernel derivado (não-oficial) tinha mais de 3500 linhas! Mas ele não era só para o LED de flash, ele suportava múltiplos tipos de LED: WLED, Flash/Torch, RGB, MPP e KPDBL.

Para tornar o porte mais fácil e já que o flash seria o único que eu seria capaz de testar e ter certeza de que está funcionando, eu decidi criar um novo arquivo para o driver e copiar só o que precisava para o LED de flash.

Eu comecei copiando a função probe, compilando o driver e vendo quais erros de definições faltando apareciam. Se as definições tivessem "flash" ou "torch" no nome, eu também copiava elas, se não, eu só removia essas referências. Eu repeti isso até que eventualmente não haviam mais erros de definições faltando e meu driver tinha tudo que era necessário para o LED de flash.

Apesar disso, o código ainda tinha muitos erros de compilação, já que o driver era para o kernel 3.4 (derivado) e eu estava compilando para o 5.7.6 (oficial). Então eu fui passando por cada erro, usando as definições em ambos os kernels como referência, e fazendo as mudanças necessárias.

Com o driver compilando com sucesso, eu adicionei o CONFIG para ele e o habilitei como um módulo no defconfig usado pelo Nexus 5 (qcom_defconfig). Eu também vasculhei os arquivos de devicetree na árvore do kernel derivado para descobrir de quais nós eu precisava para descrever o hardware do LED de flash para o driver, e as propriedades necessárias neles, para que eu pudesse adicionar isso no kernel oficial.

Obs: Por conta da forma como devicetrees funcionam, pode ser que propriedades de um mesmo nó estejam espalhadas por múltiplos arquivos. Só depois eu descobri que eu poderia compilar o kernel derivado e gerar o fonte da devicetree completa a partir do binário com dtc -I dtb -O dts -o downstream_dt.dts kernel_lge_hammerhead/arch/arm/boot/msm8974-hammerhead-rev-11.dtb.

Tendo um driver que compila, uma devicetree válida e as configurações habilitando o driver, eu estava pronto para finalmente compilar meu kernel com o driver e gravar a imagem no celular. Então eu fiz um commit com as minhas alterações e fui para a batalha.

E claro que ele falhou. Na verdade, ele falhou tanto que o driver nem se vinculou ao dispositivo do LED. Já que eu não tinha um bom entendimento de devicetrees e de como ocorria a vinculação entre dispositivos e drivers, eu comecei a pesquisar sobre.

Um ótimo material que eu encontrei foi Solving Device Tree Issues (mais no eLinux). Inclusive foi usando o script dt_node_info que essa apresentação mostra que eu descobri que o dispositivo estava sendo carregado mas o driver não. Além disso, as técnicas de depuração mostradas, como habilitar prints de debug dinâmicos nas funções de probe me ajudaram a descobrir que a função probe do meu driver nem estava sendo chamada.

Depois de ler bastante, tanto documentação online quanto código de outros drivers, eu reparei que o meu driver estava se registrando no barramento SPMI, o que fazia sentido para mim já que ele precisa se comunicar por esse barramento, mas já que o nó do LED de flash na devicetree estava sendo registrado no barramento platform, meu driver também precisava se registrar nele, caso contrário eles nunca iriam se vincular. Então isso era uma das coisas que eu precisava mudar.

Mas fazer com que meu driver se registrasse no barramento platform, fazia com que eu não tivesse mais o ponteiro de spmi_device que eu precisava para usar as funções de SPMI para ler e escrever nos registradores. Mais uma vez, olhando nos drivers em volta, como o qcom-spmi-iadc, eu percebi que tinha esse tal de regmap que eu poderia usar para ler e escrever nos registradores por SPMI mas sem ser específico ao SPMI. Eu fiz o sensato e decidi experimentar!

Com essas alterações feitas, o driver agora estava se vinculando ao dispositivo, mas a função probe estava falhando com as seguintes mensagens:

[   14.547394] spmi spmi-0: pmic_arb_wait_for_done: transaction failed (0x3)
[   14.547405] qcom,leds-qpnp fc4cf000.spmi:pm8941@1:qcom,leds@d300: Unable to read from addr=5, rc(-5)
[   14.547503] qcom,leds-qpnp fc4cf000.spmi:pm8941@1:qcom,leds@d300: Regulator get failed(-517)
[   14.547512] qcom,leds-qpnp fc4cf000.spmi:pm8941@1:qcom,leds@d300: Unable to read flash config data

Já que era o regulador que estava falhando, e dado que eu tinha simplesmente copiado os nós dos reguladores da devicetree do kernel derivado, o problema claramente estava aí no meio. Eu precisava então descobrir quais reguladores que eram necessários para o LED funcionar, e adicionar eles no kernel oficial caso ainda não estivessem lá.

Nesse momento eu enviei um email para o Brian Masney pedindo uma luz, e o conselho dele foi que eu compilasse e testasse o kernel derivado. Afinal, ter algo que funciona para servir de referência, mesmo que seja bem desatualizado, não tem preço.

Usando uma versão mais antiga das ferramentas de compilação, como instruído no script de compilação, e depois de um pequeno problema, eu consegui compilar o kernel derivado, gravei ele e conferi que o LED e o driver derivado realmente funcionavam. Eu realmente deveria ter feito isso logo no começo... Já pensou se o próprio LED de flash estivesse com problema?

Então eu comecei a explorar o sysfs desse sistema para entender como ele funcionava. Eu encontrei o regulador que estava sendo usado pelo LED, cujo status ia para enabled sempre que eu ligava o LED usando echo 1 > /sys/class/leds/led\:flash_torch/brightness.

Tendo o nó do regulador e a devicetree no kernel derivado, e o driver do regulador (qcom_spmi-regulator) e a dtbinding dele no kernel oficial, eu comecei a fazer o trabalho de detetive.

Depois de um pouco de investigação eu finalmente descobri que, já que o endereço do regulador era 0xa000, o regulador chamado 8941_boost na árvore derivada na verdade é conhecido como s4 na árvore oficial, ou também pelo apelido pm8941_5v.

Eu ainda tinha que descobrir a verdadeira identidade do outro regulador, mas agora que eu já tinha ganhado um pouco de confiança de um trabalho de detetive bem feito, e com a ajuda de algumas dicas da devicetree, eu apostei tudo que que a verdadeira identidade do pm8941_chg_boost era 5vs1, também chamado de pm8941_5vs1.

Salvar, gravar, testar, eeee... nada. Ainda sim não funcionou, mas eu claramente tinha progredido. Agora a função probe estava sendo executada com sucesso, mas as operações de leitura e escrita nos registradores SPMI ainda estavam falhando:

[   13.346704] leds_qpnp:qpnp_leds_probe: Probe called!
[   13.346746] spmi spmi-0: pmic_arb_wait_for_done: transaction failed (0x3)
[   13.346760] qcom,leds-qpnp fc4cf000.spmi:pm8941@1:qcom,leds@d300: Unable to read from addr=5, rc(-5)
[   13.347250] leds_qpnp:qpnp_dump_regs: ===== led:flash_0 LED register dump start =====
[   13.347285] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x40 = 0x0
[   13.347319] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x41 = 0x0
[   13.347353] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x42 = 0x0
[   13.347385] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x43 = 0x0
[   13.347419] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x44 = 0x0
[   13.347445] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x48 = 0x0
[   13.347470] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x49 = 0x0
[   13.347496] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x4b = 0x0
[   13.347530] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x4c = 0x0
[   13.347563] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x4f = 0x0
[   13.347589] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x46 = 0x0
[   13.347614] leds_qpnp:qpnp_dump_regs: led:flash_0: 0x47 = 0x0
[   13.347622] leds_qpnp:qpnp_dump_regs: ===== led:flash_0 LED register dump end =====

Como eu estava confiante que a devicetree agora estava certa, eu voltei para o código do driver. Eu espalhei alguns pr_debug() pela função de probe e reparei que o valor do reg, que é lido da devicetree e usado como o endereço de base para todas as operações de leitura e escrita, estava com 0, sendo que ele deveria estar com 0xd300.

Ah. Sério?? Bom, não seria uma aventura completa sem eu adicionar meu próprio bug, né? 😝

Depois de consertar o bug, eu recompilei, regravei, reiniciei, retestei eee... UHUU!!!

Lanterna do Nexus 5 sendo ligada e desligada pela linha de comando

Ele não é liiindo? 😍

Mas não vamos nos deixar levar por essa luz maravilhosa. Agora que eu finalmente tinha um driver que de fato funcionava, eu rebaseei tudo em cima do ramo principal do kernel original (a essa altura o kernel já tinha avançado do 5.7 para o 5.9) para ter certeza de que tudo continuava funcionando e enviei um patch RFC!

E essa é a lenda de "Como eu portei um driver de LED de flash para o kernel oficial"! Mas calma, esse é o fim desse artigo, mas não é o fim da aventura. Ainda tem muitas coisas que eu preciso fazer para conseguir que essa série de patches adicionando o driver de fato seja incluída no kernel oficial (algumas delas eu ainda vou descobrir pelas respostas do email).

Já que meu objetivo aqui era sempre só conseguir fazer funcionar, mudando o mínimo possível, e só depois limpar e deixar ele decente, é isso que eu vou precisar começar a fazer agora 🙂.

Só uma última coisa. Esse artigo pode ter feito parecer que a resolução dos problemas foi bem direta, mas muito pelo contrário. Teve vários momentos em que eu não fazia ideia do que fazer, enrolei por várias semanas, e até pensei em desistir e partir para outro projeto.

Mas ainda bem que eu sempre continuei tentando e procurando por ajuda, porque por mais que tenha sido muito frustrante às vezes, também foi super divertido e eu aprendi demais. Eu não consigo nem expressar minha felicidade no momento que aquela luz finalmente ligou depois de 4 meses (com pausas) de trabalho.

E é isso! Espero estar de volta daqui a alguns meses com um novo artigo contando sobre a minha aventura em transformar um patch RFC em um driver oficial de fato 🙂. Te vejo lá!