Hoje conheceremos um pouco mais sobre um dos bootloaders mais utilizados em Linux Embarcado, o U-boot. Vamos aprender como iniciar o U-boot na Raspberry Pi e ver alguns recursos interessantes que ele oferece, como inicializar o kernel e o rootfs pela rede, recursos esses que podem ser muito importantes no início do desenvolvimento de um produto com Linux Embarcado, pois é comum precisarmos ficar alterando o sistema de arquivos da nossa placa.
E no caso de uma Raspberry Pi, o sistema de arquivos fica armazenado em um SD Card. Então imagina se, para toda alteração, tivéssemos que tirar o SD Card da Raspberry Pi, inserir no PC, fazer a alteração e inserir novamente na placa. Não seria nada produtivo todo esse processo, não é mesmo?
No entanto, podemos melhorar esse processo, por exemplo, enviando as alterações por SSH, utilizando os comandos scp ou rsync. Já não precisaríamos tirar o SD Card da Raspberry Pi. Mas se eu falar que dá pra melhorar ainda mais? É o que mostrarei nesse post, como seria um workflow mais próximo do ideal no desenvolvimento de um produto com Linux Embarcado.
Estrutura do Linux Embarcado
Em Linux Embarcado, temos três componentes essenciais que formam a sua estrutura, são eles:
- Bootloader – responsável por inicializar um setup básico do hardware e carregar o kernel.
- Kernel – responsável por gerenciar CPU, memória e I/O.
- Rotfs – toda a estrutura de arquivos da distribuição, incluindo a biblioteca C padrão que faz interface com o kernel e as bibliotecas e aplicações do usuário.
E para algumas arquiteturas como ARM, que é o caso da Raspberry Pi, também tem um componente bastante importante que é o device tree, responsável por descrever o hardware ao kernel. Quem passa o device tree para o kernel é o bootloader.
Bootloader U-boot
Atualmente, um dos bootloaders mais utilizados (se não for o mais utilizado) em Linux Embarcado é o Das U-Boot. Utilizaremos ele para carregar o kernel e o rootfs pela rede. Porém, a Raspberry Pi, possui um mecanismo de boot um pouco diferente de outras placas.
Além disso, algumas informações a Broadcom (fabricante do processador da Raspberry Pi) não fornece, portanto não conseguimos trocar integralmente o bootloader da Raspberry pelo U-boot. Porém conseguimos fazer com que o bootloader da Raspberry Pi carregue o U-Boot, que por sua vez, carregará o kernel.
Abaixo um comparativo do boot process de uma Raspberry Pi 3 Model B+ com uma BeagleBone Black. Perceba que na Raspberry Pi, o boot é comandado pela GPU e não pela CPU.
Rootfs
Após inicializar o kernel, precisamos passar como parâmetro o caminho do rootfs. Quando utilizamos uma distribuição pronta, como o Raspbian, o rootfs fica localizado na segunda partição do SD Card. Mas, no nosso caso, queremos carregar o rootfs pela rede. Esse processo é conhecido como NFS (Network File System), que é um protocolo que permite compartilhar arquivos e diretórios entre hosts ligados na mesma rede.
Então a ideia é na Raspberry Pi possuir apenas os bootloaders (broadcom e u-boot) e trabalhar com o sistema de arquivos diretamente na nossa máquina. Portanto, kernel, device trees e rootfs devem estar localizados no host. E para carregar o kernel e os device trees pela rede, o U-boot utiliza o protocolo TFTP (Trivial File Transfer Protocol), que é utilizado para transferir arquivos entre hosts em uma rede.
Bom, explicado como as coisas funcionam e o que queremos, então, hora de colocar a mão na massa!
Importante: Os testes abaixo foram realizados em uma máquina com Linux (Linux Mint, no meu caso), caso você não tenha um linux instalado em seu PC, pode baixar uma VM com Ubuntu para acompanhar.
Boot pela rede com U-Boot e Raspberry Pi
Primeiro vamos instalar algumas dependências:
sudo apt-get install git nfs-kernel-server tftpd-hpa make build-essential flex bison
Vamos baixar o código fonte do Das U-Boot para compilá-lo para a Raspberry Pi.
mkdir ~/raspberry; cd ~/raspberry git clone https://github.com/u-boot/u-boot cd ~/raspberry/u-boot git checkout v2020.04
Para poder compilar o U-boot para a Raspberry Pi, precisaremos de uma toolchain para cross-compilar os binários do U-boot da nossa máquina (arquitetura x86) para o target (arquitetura ARM). Vamos baixar uma toolchain da Linaro, que é uma organização que trabalha no porte de ferramentas para a arquitetura ARM:
mkdir -p ~/raspberry/tools; cd ~/raspberry/tools wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz tar xfv gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
Agora já podemos compilar o U-Boot para gerar os binários que utilizaremos na Raspberry Pi:
Importante: Aqui estou compilando para a Raspberry Pi 3 B+, por isso estou digitando make_rpi_3_b_plus_defconfig, para verificar as opções disponíveis digite: ls ~/raspberry/u-boot/configs/rpi*
cd ~/raspberry/u-boot make rpi_3_b_plus_defconfig make CROSS_COMPILE=~/raspberry/tools/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
Depois de gerado, vamos copiar o U-boot para o SD Card. Insira o SD Card no PC e verifique como foi reconhecido:
Importante: No meu SD Card já possui uma imagem do Raspbian Buster Lite.
lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sdb 8:32 1 3,7G 0 disk ├─sdb2 8:34 1 3,4G 0 part /media/fernando/disk └─sdb1 8:33 1 256M 0 part /media/fernando/boot1 sda 8:0 0 1,8T 0 disk ├─sda2 8:2 0 1K 0 part ├─sda5 8:5 0 15,8G 0 part [SWAP] └─sda1 8:1 0 1,8T 0 part /
No meu caso, eu tenho o HD da máquina em /dev/sda e o SD Card foi reconhecido como /dev/sdb e possui duas partições, sdb1 é a partição de boot, onde devemos copiar o U-Boot e sdb2 é a partição que contém o rootfs.
Desmonte o SD Card com o comando:
sudo umount -f /dev/sdb*
Vamos criar uma pasta para montar a partição de boot do SD Card no PC:
mkdir -p ~/raspberry/sdcard/boot
Montar a partição na pasta criada:
sudo mount /dev/sdb1 ~/raspberry/sdcard/boot
Agora podemos copiar o binário do U-Boot para o SD Card:
cp ~/raspberry/u-boot/u-boot.bin ~/raspberry/sdcard/boot/
Para o bootloader da Raspberry Pi carregar o U-Boot ao invés do kernel, precisamos modificar o arquivo config.txt:
cd ~/raspberry/sdcard/boot echo -e "kernel=u-boot.bin" | sudo tee -a config.txt
Para carregar o kernel, precisamos gerar um script que o U-Boot possa interpretar.
Importante: na terceira linha da sequência de comandos, insira a imagem do kernel de acordo com a Raspberry Pi que estiver usando:
- RPi 1: kernel.img
- RPi2 e RPi3 32 bits: kernel7.img
- RPi4 32 bits: kernel7l.img
- RPi3 64 bits e RPi4 64 bits: kernel8.img
Na quinta linha insira o nome do seu usuário:
cd ~/raspberry cat << "EOF" > boot.cmd setenv serverip 192.168.0.10 setenv ipaddr 192.168.0.11 setenv bootargs earlyprintk console=tty1 console=ttyAMA0 root=/dev/nfs rw nfsroot=192.168.0.10:/home/fernando/raspberry/rootfs rootfstype=nfs rootwait ip=192.168.0.11 tftpboot ${kernel_addr_r} kernel7.img bootz ${kernel_addr_r} - ${fdt_addr} EOF ~/raspberry/u-boot/tools/mkimage -C none -A arm -T script -d ~/raspberry/boot.cmd ~/raspberry/boot.scr
Copiando o script gerado para o SD Card:
sudo cp ~/raspberry/boot.scr ~/raspberry/sdcard/boot/
Vamos criar o diretório onde ficará o kernel e os device trees:
mkdir ~/raspberry/boot
Agora vamos copiar o kernel e os device trees para o nosso diretório de boot:
sudo cp ~/raspberry/sdcard/boot/*.dtb ~/raspberry/boot/ sudo cp ~/raspberry/sdcard/boot/kernel* ~/raspberry/boot/
Vamos criar o diretório onde ficará nosso rootfs e o diretório para montar a segunda partição do SD Card:
mkdir ~/raspberry/rootfs; mkdir ~/raspberry/sdcard/rootfs
Montando o rootfs do Raspbian em nosso PC:
sudo mount /dev/sdb2 ~/raspberry/sdcard/rootfs
E copiar para o nosso diretório de rootfs:
sudo cp -ar ~/raspberry/sdcard/rootfs/* ~/raspberry/rootfs/
Para a Raspberry Pi ter acesso ao diretório que está nosso rootfs, precisamos exportá-lo com o seguinte comando:
echo -e "/home/$USER/raspberry/rootfs/ *(rw,sync,no_root_squash,no_subtree_check)" | sudo tee -a /etc/exports
Editar o arquivo /etc/default/tftpd-hpa, inserir o caminho do diretório que contém o kernel e os device trees.
Importante: Deve ser colocado o nome de usuário que você estiver logado, no meu caso, o nome de usuário é fernando
TFTP_DIRECTORY="/home/fernando/raspberry/boot"
Reiniciando os servidores NFS e TFTP:
sudo service nfs-kernel-server restart sudo service tftpd-hpa restart
Agora podemos desmontar o SD Card e inseri-lo novamente na Raspberry Pi.
sudo umount -f /dev/sdb*
Pronto! Agora ao ligar a Raspberry Pi, o U-Boot carregará o kernel e o rootfs que estão no PC e não os que estão no SD Card, assim toda alteração que você fizer na sua máquina será refletido na Raspberry Pi.
Importante: Para esse teste, o cabo de rede deve estar conectando o PC à Raspberry Pi, como a imagem abaixo ilustra. E também, o IP do PC deve ser fixado em 192.168.0.10.
Se você monitorar a serial da Raspberry Pi, vai ver as mensagens do U-Boot, carregando e inicializando o kernel:
Conclusão
Uma das vantagens de se fazer o boot pela rede, é que podemos usar todo o processamento da nossa máquina (que geralmente é maior que o do nosso target) no desenvolvimento de aplicações, evitando ter que escrever código e/ou compilar diretamente na Raspberry Pi.
Isso facilita o deploy das aplicações e também o teste de uma infinidade de ferramentas na Raspberry Pi sem ter que se preocupar com o tamanho do SD Card (se você tiver espaço no PC, claro). Também, as aplicações ficam centralizadas no seu PC, qualquer alteração que você fizer, é fácil rastrear. E ao finalizar o desenvolvimento do produto, é só copiar o rootfs para o SD Card.
Não é uma boa prática ter ferramentas de desenvolvimento na Raspberry Pi, pois um sistema embarcado é feito para um propósito específico. Portanto, o ideal é desenvolver o projeto integralmente no PC e fazer deploy para o target. Ferramentas de desenvolvimento no target só vão inflar o sistema de arquivos e poderão causar perda de performance.
Abaixo, uma lista de artigos aqui do blog que vocês podem praticar esse método:
- Sistema de monitoramento com Raspberry Pi
- Inicialização de Aplicação durante o boot com Systemd na Raspberry Pi
- Controlando GPIO em linguagem nodeJS na Raspberry Pi
- Escrevendo mensagens com o módulo Matriz de Led 8×16 e Raspberry Pi
- Sockets TCP em C
- Como desenvolver um cliente MQTT em C na Raspberry Pi
E você já conhecia o U-boot? E o NFS? Como você desenvolve aplicações para a sua Raspberry, desenvolve diretamente na placa ou fazendo deploy do seu PC? Deixe aqui seus comentários e compartilhe suas experiências.