ÍNDICE DE CONTEÚDO
- Primeiros passos com ESP32 utilizando MCUboot como bootloader
- Firmware update utilizando MCUboot no ESP32
No mundo embarcado, atualização de firmware é um essencial e extenso tema que nos últimos anos vem ganhando mais e mais importância. Eficiência e segurança nunca foram mais discutidas, ao passo que soluções IoT crescem exponencialmente em números e complexidade, assim como a preocupação em fazê-las seguras, atualizáveis (em campo!) e ambas as condições através de formas eficientes.
Há pouco tempo atrás, o MCUboot surgiu como um bootloader de código aberto para sistemas pequenos e de baixo custo, que se propôs a simplificar e padronizar soluções para os problemas mencionados. Ele começou como um subprojeto do RTOS Apache Mynewt quando seus desenvolvedores decidiram separar os desenvolvimentos do bootloader e do OS. Mais tarde foi portado para o Zephyr RTOS e se tornou seu bootloader de uso padrão.
A Espressif Systems vem expandindo seu suporte a outros RTOSes, como Zephyr e NuttX como uma opção atrativa para uso em seus SoCs, e agora esse interessante bootloader alternativo está sendo desenvolvido. Recentemente um porte para os SoCs da Espressif foi adicionado ao projeto do MCUboot e o suporte básico para o amplamente usado ESP32 está disponível.
Este guia tem como objetivo instruir os primeiros passos com MCUboot no seu projeto baseado em ESP32 e apresentar a configuração de ambiente, configurações adicionais requeridas no lado da aplicação, o processo de build e flash do dispositivo. A placa ESP32 DevKitC foi utilizada para preparar este guia.
O que é MCUboot?
Como dito anteriormente, o MCUboot é um bootloader seguro e open source para microcontroladores de 32 bits. O projeto define uma infra-estrutura comum para o boot seguro e sua arquitetura contempla:
- Layout de flash do sistema: a organização da memória flash é bem definida, utilizando o conceito de “slots” para conter a imagem principal e a imagem de atualização em diferentes regiões da flash, bem como uma área de scratch para auxiliar no processo de swap (permuta) da atualização de firmware.
- Atualização de firmware fácil e segura: é um processo simples em que, seja qual for o agente atualizador, é apenas necessário assinar a imagem corretamente e gravá-la no slot correto da flash. MCUboot se encarregará do swap entre a imagem atual e a mais nova, e também da segurança do processo (integridade, autenticidade e confidencialidade).
- Segurança: MCUboot permite o boot e atualização seguras do firmware utilizando:
- Integridade da imagem através da verificação de hash (SHA256).
- Autenticidade da fonte através da validação de assinatura de chaves assimétricas, como RSA 2048 e 3072, ECDSA e ed25519.
- Confidencialidade de dados da imagem (encriptação/desencriptação) durante o transporte e/ou durante o armazenamento da mesma em flash externa. O MCUboot possui suporte para encriptação/desencriptação on-the-fly durante o processo de atualização e utiliza algoritmos AES ou ECIES para isso.
- Tolerância a falhas: o mecanismo de swap permite a recuperação/reversão de imagens caso um problema durante o processo ocorra, como um reset no meio de um swap durante uma atualização. Além disso, se por alguma razão uma nova imagem tenha sido atualizada e inicializada, mas não se sinalizou como “ok”, o MCUboot possui um mecanismo de rollback para reverter à imagem anterior, uma vez que ela é mantida após o swap.
O projeto se propõe a padronizar esses pontos de forma compreensível.
MCUboot não está atrelado a um sistema operacional ou hardware único. Ele depende das camadas de portabilidade de hardware existentes no SO alvo.
Vamos dar uma olhada na visão geral alto-nível do processo de boot:
Pontos importantes a serem notados:
- O slot primário (Primary slot) é onde a imagem principal inicializável se encontra, a execução do firmware corrente sempre ocorre a partir dele.
- O slot secundário (Secondary slot) é utilizado para atualizações. Ele armazena a imagem de atualização recebida e, após a permuta, armazena a imagem original, garantindo que a ação possa ser revertida caso ocorra algum problema.
- A região de scratch é utilizada para auxiliar a permuta de imagens no processo de atualização.
- Um cabeçalho e um sufixo de dados são adicionados à imagem para guardar e rastrear informações gerais, estados da permuta e estados da atualização.
Veja mais na página oficial do MCUboot.
Preparando o ambiente
Primeiramente, precisamos preparar o ambiente de desenvolvimento. Este guia pressupõe o uso do Linux (Ubuntu 20.04.2 LTS).
Adicionalmente, assegure-se de que possui Git, Python3, pip e CMake instalados. Caso não tenha, você pode executar os seguintes comandos (este passo é opcional):
1 2 3 4 |
sudo apt update sudo apt upgrade sudo apt install git python3 python3-pip cmake ninja-build |
- Faça o clone do repositório:
1 |
git clone -b feature/esp32s2_support https://github.com/almir-okato/mcuboot.git |
- Instale as dependências adicionais necessárias pelo MCUboot:
1 2 |
cd mcuboot pip3 install -r scripts/requirements.txt |
- Atualize os submódulos necessários pelo porte da Espressif. Este passo pode levar um tempo, pois irá baixar a versão do ESP-IDF que contém a HAL (camada de abstração de hardware) para o ESP32 e a toolchain usada para compilação. Também é baixada a biblioteca mbedtls requerida pelo MCUboot.
1 2 |
git submodule update --init --recursive boot/espressif/hal/esp-idf git submodule update --init --recursive ext/mbedtls |
- Agora precisamos instalar as dependências do IDF e configurar as variáveis de ambiente. Este passo pode levar algum tempo:
- Obs.: Se você já instalou o IDF alguma vez, verifique qual é a toolchain que está instalada. Você pode executar
ls ~/.espressif/tools/xtensa-esp32-elf/
para checar se o diretório “esp-2020r3-8.4.0” já existe. Caso o diretório não exista, execute os comandos abaixo. Se você tem alguma outra versão (esp-2021r1-8.4.0, por exemplo), você deve mover este diretório para outro lugar para que a toolchain compatível com o porte da Espressif seja instalado na pasta citada.
1 2 3 4 |
cd boot/espressif/hal/esp-idf ./install.sh . ./export.sh cd ../.. |
Compilação e Flash do MCUboot na ESP32
Agora que temos tudo preparado, vamos compilar nosso bootloader.
- Compile e gere o ELF do bootloader:
1 2 3 |
cmake -DCMAKE_TOOLCHAIN_FILE=tools/toolchain-esp32.cmake -DMCUBOOT_TARGET=esp32 -B build -GNinja cmake --build build/ |
- Converta o ELF para o binário final do bootloader, preparada para o flash:
1 |
esptool.py --chip esp32 elf2image --flash_mode dio --flash_freq 40m -o build/mcuboot_esp32.bin build/mcuboot_esp32.elf |
- Finalmente, faça o flash do MCUboot na sua placa:
1 |
esptool.py -p /dev/ttyUSB0 -b 2000000 --before default_reset --after hard_reset --chip esp32 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x1000 build/mcuboot_esp32.bin |
Você deve ajustar a porta USB (-p /dev/ttyUSB0
) e o baud rate (-b 2000000
) de acordo com a conexão de sua placa.
Agora podemos verificar a saída serial na porta UART (a mesma que utilizamos para o flash):
1 |
idf_monitor.py -d --port /dev/ttyUSB0 build/mcuboot_esp32.elf |
Como ainda não fizemos o flash de nenhuma imagem de aplicação ainda, veremos o seguinte log:
Preparação para o flash de uma aplicação
Para qualquer imagem de aplicação que você use com o MCUboot, será necessário assiná-la e prepará-la adequadamente, e o imgtool
é utilizado para isso. É uma ferramenta que adiciona o cabeçalho e sufixo necessários ao binário, assina o firmware e também pode gerar as chaves para serem utilizadas no processo. imgtool
pode ser encontrado em <MCUBOOT_DIR>/scripts/imgtool.py
(<MCUBOOT_DIR> é o caminho para o diretório onde o repositório foi clonado), ou você também pode instalar a ferramenta da seguinte forma (opcional):
1 |
pip3 install imgtool |
Há algumas chaves de exemplo no repositório que são utilizadas por padrão. É crucial que você nunca use essas chaves-exemplo em produção já que a chave privada se encontra aberta no repositório. Mais informações sobre como gerar e gerenciar chaves de criptografia e assinatura em imgtool.
Assinatura e flash da aplicação
Estou deixando aqui dois exemplos de aplicação para serem testadas com o MCUboot, uma do Zephyr e outra do NuttX. Você também pode criar uma aplicação “do zero” para ambos, mas certifique-se de habilitar a configuração de compatibilidade com o MCUboot em qual RTOS escolher.
- Zephyr – Hello World app: zephyr.bin
- NuttX – nsh: nuttx.bin
Agora precisamos assinar o binário. Para isso utilizaremos o imgtool
.
Como você pode notar abaixo, estamos utilizando o imgtool
provido pelo repositório do MCUboot, mas você também pode utilizar aquele instalado através do pip
. Substitua <app.bin> por “nuttx.bin” ou “zephyr.bin”, dependendo do binário escolhido, sendo ele o objeto de origem e “signed.bin” o objeto assinado final:
1 |
../../scripts/imgtool.py sign --align 4 -v 0 -H 32 --pad-header -S 0x00100000 <app.bin> signed.bin |
De forma alternativa, se você instalou imgtool
através do pip
:
1 |
imgtool sign --align 4 -v 0 -H 32 --pad-header -S 0x00100000 <app.bin> signed.bin |
Uma explicação rápida sobre o que a ação do imgtool
e seus parâmetros estão fazendo:
--align 4
: Especifica o alinhamento da flash como 4 bytes (palavra de 32 bit).-v 0
: Especifica a versão da imagem, neste caso especificamos como ‘0’.-H 32
: Especifica o tamanho do cabeçalho do MCUboot que será adicionado ao binário.--pad-header
: Indica que o cabeçalho do MCUboot precisa explicitamente ser adicionado pela ferramenta (o build do Zephyr para algumas plataformas já adiciona o espaço necessário preenchido com ‘0’s e podem não necessitar do parâmetro).-S 0x00100000
: Indica o tamanho do slot, assim a ferramenta também conseguirá adicionar a região de sufixo adequadamente.
Não estamos lidando com atualizações ainda. Se este é o seu caso, há outros parâmetros requeridos para que isso seja feito. Eles serão abordados nas próximas partes desta série sobre MCUboot no ESP32.
O próximo passo é fazer o flash do dispositivo:
1 |
esptool.py -p /dev/ttyUSB0 -b 2000000 --before default_reset --after hard_reset --chip esp32 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x10000 signed.bin |
Verificando a saída serial, finalmente podemos ver:
- Se você escolheu “zephyr.bin”:
- Se você escolheu “nuttx.bin”:
Assim, o MCUboot carregou a aplicação exemplo do slot primário. Note que nós fizemos o flash manualmente no endereço 0x10000, o qual é o endereço esperado do slot primário pelo bootloader. Veremos um pouco mais sobre a organização da flash na próxima seção.
Organização da Flash
O MCUboot define uma organização para a flash e uma área da flash pode conter múltiplas imagens executáveis dependendo da configuração de boot e atualização. Cada área de imagem contém dois slots de imagem: um primário e um secundário, e por padrão o bootloader executa apenas a imagem do slot primário. O slot secundário é onde a imagem de atualização recebida é colocada antes de ser “instalada”, seu conteúdo é então permutado com o slot primário ou irá sobrescrevê-lo no processo de atualização. Portanto, podemos identificar quatro tipos de áreas de flash no layout:
ÁREA | ID | DESCRIÇÃO |
FLASH_AREA_BOOTLOADER | 0 | Esta é a área para o próprio bootloader |
FLASH_AREA_IMAGE_PRIMARY(0) | 1 | Slot primário para a imagem executável |
FLASH_AREA_IMAGE_SECONDARY(0) | 2 | Slot secundário para a imagem recebida |
FLASH_AREA_IMAGE_SCRATCH | 3 | Esta área é necessária para permitir a permuta de imagens de forma confiável e deve ter um tamanho o suficiente para armazenar pelo menos o maior setor a ser permutado. |
O MCUboot também suporta o uso de múltiplas imagens e podemos definir outras áreas de imagem, cada qual com seus respectivos slots primários e secundários.
O porte da Espressif no momento suporta apenas uma área de imagem com seus dois slots, então a flash foi particionada de acordo:
ÁREA | ENDEREÇO | TAMANHO |
BOOTLOADER | 0x1000 | 0xF000 |
PRIMARY SLOT | 0x10000 | 0x100000 |
SECONDARY SLOT | 0x110000 | 0x100000 |
SCRATCH | 0x210000 | 0x40000 |
A informação sobre o layout da flash é colocada no arquivo bootloader.conf
do porte da Espressif para MCUboot. Os endereços e tamanhos podem ser alterados, no entanto, as seguintes regras devem ser respeitadas:
- O endereço do bootloader deve ser mantido, pois por padrão é para onde o ESP32 realiza o jump após um reset.
- Nenhum dos slots devem se sobrepor.
- Os slots primário e secundário devem possuir o mesmo tamanho.
- A área auxiliar de scratch deve possuir tamanho o suficiente para armazenar pelo menos o tamanho do maior setor que será permutado, devendo ser então o mesmo tamanho do maior setor da flash.
- A aplicação e o agente de atualização devem ter conhecimento deste layout para que tudo possa ser posicionado adequadamente.
Em nosso exemplo, 0x10000 é o endereço para o slot primário, de onde o bootloader irá carregar e inicializar a imagem. Se quisermos atualizar o dispositivo, o agente de atualização deve estar ciente da organização da flash, assinar a imagem e colocá-la no slot secundário. Note que o processo de assinatura será um pouco diferente do exemplo deste tutorial. Mais informações sobre atualização de imagem serão abordadas no futuro.
Conclusão
MCUboot provê uma estrutura sólida e define um fluxo padrão para atualização de firmware e boot seguro. Portanto, uma vez que essas funcionalidades estão implementadas como parte do bootloader, elas podem ser facilmente habilitadas sem muitas modificações no desenvolvimento de um firmware.
Além disso, é um projeto open source, o que traz todas as vantagens de ser desenvolvido por uma comunidade comumente interessada, mas heterogênea, com respostas rápidas para solução de problemas e desenvolvimento engajado.
Vimos neste guia o processo de build do bootloader MCUboot para ESP32, vimos como assinar uma imagem e também como a flash deve ser organizada. O próximo passo é entender como as atualizações funcionam no MCUboot e como utilizar esta funcionalidade apropriadamente.
Top, ainda não acabei de fazer tudo mas já vou deixar meu comentário pra não esquecer, na hora de baixar o meu funcionou com baud 115200:
esptool.py –p /dev/ttyUSB0 –b 115200 —before default_reset —after hard_reset —chip esp32 write_flash —flash_mode dio —flash_size detect —flash_freq 40m 0x1000 build/mcuboot_esp32.bin
Com 4000000 ele da erro após alterar o baud, diz que não recebeu resposta pela serial….
Ótimo artigo!
Nice work! Can you also share the zephyr configuration you used to build the hello_world example? I can run either the hello_world example or mcuboot. Booting hello_world with mcuboot fails. I guess devicetree/Kconfig settings are missing.
Thanks Marc!
Yes, some changes are needed in Zephyr in order to make it run properly with the MCUboot port for ESP32, we are still working on that before submitting it on Zephyr upstream. As soon as we get it submitted I’ll let you know!
Meanwhile, you can try NuttX too, see the following links:
https://github.com/apache/incubator-nuttx
https://github.com/apache/incubator-nuttx/pull/4324
Hi Marc, please see the PR opened for Zephyr: https://github.com/zephyrproject-rtos/zephyr/pull/37872, it adds support for the MCUboot port for ESP32.
Hello Almir, thanks for the update. I was able to build and run different Zephyr examples with code from the PR and upstream MCUboot. Great to see progress on ESP32 and Zephyr. My next step will be trying to get actual FOTA working.
Muito bom! Parabéns!
Obrigado Rubens!