ÍNDICE DE CONTEÚDO
Introdução
O desenvolvimento de software para sistemas embarcados é cercado por uma multitude de ambientes de desenvolvimentos que exigem versões específicas de ferramentas ou de código de bibliotecas. Isso, por muitas vezes, dificulta o processo de desenvolvimento, pois pode ser que uma placa de um fabricante X exija que esteja instalado em sua máquina versões de ferramentas que não funcionariam para uma placa de um fabricante Y, assim exigindo a mudança de versões das ferramentas frequentemente para atender os requisitos de cada ambiente de desenvolvimento.
Além do mais, o grau de dificuldade aumentaria quando fosse necessário desenvolver software colaborativamente em máquinas distintas, com diferentes sistemas operacionais (Ubuntu vs. Windows vs. Mac), versões distintas do mesmo sistema operacional (Ubuntu 20.04 vs. Ubuntu 22.04) ou até mesmo diferentes arquiteturas de computador (x86 vs. ARM).
O efeito que isso causa são horas dedicas a tarefa de suporte para garantir que o ambiente de desenvolvimento esteja funcionando minimamente, sem que erros de compilação ocorram devido ao uso de uma versão incorreta da ferramenta que regula o sistema de compilação ou ao uso de versões inválidas de uma biblioteca. Por exemplo, a versão do CMake baixada pelo repositório padrão do Ubuntu 20.04 e 22.04 possuem divergências:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
usuario@local:~$ docker run -it ubuntu:20.04 root@conteiner:~# apt-get update root@conteiner:~# apt-get install -y cmake root@conteiner:~# cmake --version cmake version 3.16.3 CMake suite maintained and supported by Kitware (kitware.com/cmake). usuario@local:~$ docker run -it ubuntu:22.04 root@conteiner:~# apt-get update root@conteiner:~# apt-get install -y cmake root@conteiner:~# cmake --version cmake version 3.22.1 CMake suite maintained and supported by Kitware (kitware.com/cmake). |
Isso pode ser o suficiente para causar confusão, pois alguns projetos de software podem utilizar a flag cmake_minimum_required para especificar a versão mínima do CMake.
Embarcados Experience 2024: Evento Presencial
Participe do Embarcados Experience 2024 em São Paulo. Conhecimento técnico, palestras, workshops e oportunidade de networking com profissionais experientes.
Docker
Nesse ponto que entra o Docker para ajudar. O Docker permite criar contêineres que isolam o ambiente da aplicação e seus pacotes do ambiente da máquina local. Os contêineres são uma espécie de virtualização e que podem ser confundidos com uma máquina virtual, mas que são bastante diferentes (Contêineres vs. máquinas virtuais). Em resumo, uma máquina virtual executa um sistema operacional completo, incluindo o kernel, enquanto um contêiner é bem mais leve e compartilha do próprio kernel da máquina local para executar aplicações isoladas do ambiente do sistema operacional da máquina local (Docker Docs). No final, um contêiner é nada mais que um processo na máquina local que possui um sistema de arquivos dedicado e isolado.
Com contêineres um pode ajustar quais exatos pacotes e suas versões que devem ser utilizadas para garantir o correto funcionamento de uma aplicação, seja ela um webserver, aplicações em cloud, processamento de dados e ambientes de desenvolvimento e testes.
Uma vez criado um ambiente de desenvolvimento e testes com o Docker, a equipe de desenvolvimento pode tanto se beneficiar do ambiente para desenvolvimento local quanto na nuvem. Além do mais, esse mesmo ambiente pode ser utilizado para automatizar o processo de compilação e lançamento da aplicação com pipelines de CI/CD (Continuous Integration/Continuous Delivery).
Experimento para setup de um Ambiente de desenvolvimento com Docker
Para exemplificar, vamos construir passo-a-passo um ambiente para desenvolvimento com o microcontrolador ESP32 usando o ESP-IDF.
Os materiais necessários para esse tutorial serão:
- Docker (será utilizada apenas a interface por linha de comandos), que pode ser instalada seguindo as instruções oficiais (Install Docker Engine)
- Máquina Linux para desenvolvimento, apesar do experimento ter sido desenvolvido utilizando o Ubuntu 20.04 LTS, o ambiente de compilação deverá funcionar para qualquer sistema operacional (exceto a etapa de carregamento do binário final, que no Windows a etapa de exposição de um dispositivo USB para o contêiner é dificultado)
- ESP32 com cabo para programação (plataforma alvo para ambiente com microcontrolador).
O objetivo será preparar o ambiente para que as seguintes tarefas sejam realizadas de dentro do ambiente do contêiner:
- Compilação dos binários da aplicação
- Carregamento dos binários da aplicação na plataforma alvo (etapa exclusiva para máquina local Linux)
- Monitoramento do funcionamento da aplicação por porta serial (etapa exclusiva para máquina local Linux).
Conhecendo o Dockerfile
O ambiente pode ser montado instanciando um contêiner baseado em uma distribuição Linux, como o Ubuntu, com o seguinte comando abaixo:
1 2 3 |
usuario@local:~$ docker run -it ubuntu:22.04 root@conteiner:~# apt-get update root@conteiner:~# apt-get install -y ... |
Assim, uma máquina Ubuntu 22.04 com configurações mínimas estará pronta para ser configurada com os pacotes necessários para seu ambiente de desenvolvimento. Porém, esta não é a ação correta para longo prazo, pois assim que a instância desse contêiner for deletada, os comandos de configuração executados dentro também serão perdidos.
O Dockerfile é um arquivo que pode ser utilizado como forma de rastrear todos os comandos utilizados para configurar seu contêiner, bem como quais pacotes foram instalados. Esse mesmo Dockerfile pode ser compartilhado facilmente para que o ambiente de desenvolvimento seja estabelecido em outras máquinas.
Quando o Dockerfile estiver pronto, ao invés de invocar seu contêiner usando uma imagem padrão, como a imagem Ubuntu utilizada logo acima, você será capaz de utilizar sua imagem customizada. Primeiramente é necessário compilar a imagem customizada passando o caminho do Dockerfile:
1 |
usuario@local:~$ docker build -t img_customizada:1.0 <caminho/para/dockerfile> |
Após isso, basta invocá-la:
1 |
usuario@local:~$ docker run -it img_customizada:1.0 |
Descrição do ambiente de desenvolvimento para ESP32
Normalmente o fabricante da placa de desenvolvimento que você vai trabalhar já possui o passo-a-passo para montar o ambiente de desenvolvimento na sua máquina local. Resta a você adaptar os comandos de configuração para o Dockerfile. No caso do ESP32, as instruções podem ser encontradas no guia Get Started da Espressif.
O que recomendo fazer para auxiliar na construção do Dockerfile é abrir um contêiner vazio baseado em alguma distribuição (Ubuntu 22.04), como foi feito no primeiro comando da sessão “Conhecendo o Dockerfile”. Assim, antes de registrar o comando no Dockerfile, você primeiramente o testa nesse contêiner temporário para validar a eficácia desse comando. Isso pois, pode ocorrer de alguns comandos padrões de configuração padrão emitidos pelo fabricante considerarem que algumas ferramentas já vêm por padrão, quando em alguns casos o contêiner pode não vir com essa ferramenta, pelo fato de possuir uma configuração mínima.
Imagem base
O primeiro campo será a imagem base em que o ambiente de desenvolvimento se baseará. Neste caso será utilizada uma imagem de uma distribuição Linux (Ubuntu 22.04):
1 |
FROM ubuntu:22.04 |
Pré-requisitos
O guia Get Started oferece comandos para instalação dos pré-requisitos para diversas distribuições Linux. Aqui estaremos interessados nos comandos para a distribuição Ubuntu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
RUN apt-get update RUN apt-get install -y \ git \ wget \ flex \ bison \ gperf \ python3 \ python3-pip \ python3-venv \ cmake \ ninja-build \ ccache \ libffi-dev \ libssl-dev \ dfu-util \ libusb-1.0-0 |
Após instalados os pacotes necessários, uma boa prática é deletar o cache do gerenciador de pacotes (apt-get), assim reduzindo o tamanho da imagem final.
1 |
RUN rm -rf /var/lib/apt/lists/* |
Baixar ESP-IDF
Para compilação de aplicações para o ESP32, será necessário baixar as bibliotecas de software disponibilizadas pela Espressif direto do repositório do ESP-IDF.
1 2 3 |
RUN mkdir -p /root/esp WORKDIR /root/esp RUN git clone -b v5.3 --recursive https://github.com/espressif/esp-idf.git |
Com isso, a pasta esp será criada e dentro dela será baixado o repositório do ESP-IDF.
Configuração das ferramentas
Outra etapa importante é a configuração das ferramentas de compilação, carregamento e depuração.
1 2 |
WORKDIR /root/esp/esp-idf RUN ./install.sh esp32 |
Configurações finais de execução
Essa é a etapa final que consiste em alguns comandos em ordem de realizar as seguintes ações:
- Comando a ser executado na inicialização do contêiner (iniciar terminal com bash).
- Criação de pasta para mapear os projetos em desenvolvimento
- Criação de alias para comando de exportação do ambiente do ESP-IDF
- Definição de variável de ambiente do IDF_PATH
- Execução de script de ponto de entrada quando da criação de uma instância para essa imagem (entrypoint.sh realiza a exportação do ambiente do ESP-IDF)
- Comando a ser executado na inicialização do contêiner (iniciar terminal com bash).
1 2 3 4 5 |
RUN mkdir /root/projects RUN echo "alias get_idf='source /root/esp/esp-idf/export.sh'" >> /root/.bashrc ENV IDF_PATH=/root/esp/esp-idf ENTRYPOINT [ "/root/esp/esp-idf/tools/docker/entrypoint.sh" ] CMD [ "/bin/bash" ] |
Esses comandos preparam o ambiente final do contêiner para sua correta execução.
Utilizando o ambiente de desenvolvimento
Compilação da imagem do contêiner
Após preenchido o Dockerfile, a imagem pode ser compilada com o comando abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
usuario@local:~$ docker build -t esp32_ambiente:1.0 <caminho/para/dockerfile> [+] Building 581.8s (15/15) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 924B 0.0s => [internal] load metadata for docker.io/library/ubuntu:22.04 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [ 1/11] FROM docker.io/library/ubuntu:22.04 0.0s => CACHED [ 2/11] RUN apt-get update 0.0s => CACHED [ 3/11] RUN apt-get install -y git wget flex bison gperf python3... 0.0s => CACHED [ 4/11] RUN rm -rf /var/lib/apt/lists/* 0.0s => [ 5/11] RUN mkdir -p /root/esp 0.2s => [ 6/11] WORKDIR /root/esp 0.0s => [ 7/11] RUN git clone -b v5.3 --recursive https://github.com/espressif/esp-idf.git 458.9s => [ 8/11] WORKDIR /root/esp/esp-idf 0.0s => [ 9/11] RUN ./install.sh esp32 116.3s => [10/11] RUN mkdir /root/projects 0.2s => [11/11] RUN echo "alias get_idf='source /root/esp/esp-idf/export.sh'" >> /root/.bashrc 0.4s => exporting to image 5.6s => => exporting layers 5.6s => => writing image sha256:adab4db4d0c94abb747843cd9b7535827d27cdde07be6f102188e5e9d99c50fb 0.0s => => naming to docker.io/library/ esp32_ambiente:1.0 0.0s |
A imagem pode ser visualizada:
1 2 3 |
usuario@local:~$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE esp32_ambiente 1.0 adab4db4d0c9 10 seconds ago 3.59GB |
Antes de executar a imagem, caso seja de interesse carregar a imagem compilada na placa de desenvolvimento, será necessário descobrir qual dispositivo serial da máquina local deverá ser mapeado para ser enxergado dentro do contêiner Docker. Para isso, você deve monitorar as mensagens do kernel da sua máquina enquanto conecta a placa na porta USB:
1 2 3 4 5 6 7 8 9 |
usuario@local:~$ dmesg -w [14543.511438] usb 3-3: new full-speed USB device number 5 using xhci_hcd [14543.695250] usb 3-3: New USB device found, idVendor=10c4, idProduct=ea60, bcdDevice= 1.00 [14543.695259] usb 3-3: New USB device strings: Mfr=1, Product=2, SerialNumber=3 [14543.695263] usb 3-3: Product: CP2102 USB to UART Bridge Controller [14543.695265] usb 3-3: Manufacturer: Silicon Labs [14543.695268] usb 3-3: SerialNumber: 0001 [14543.705453] cp210x 3-3:1.0: cp210x converter detected [14543.706410] usb 3-3: cp210x converter now attached to ttyUSB0 |
Neste caso o dispositivo é o /dev/ttyUSB0
.
Outro passo importante é utilizar os volumes do Docker para garantir a persistência de novos arquivos criados no contêiner em tempo de compilação. Por exemplo, vamos mapear a pasta /root/projects
do contêiner para a pasta local /home/usuario/projeto-teste
, assim tudo que for feito na pasta correspondente do contêiner refletirá na pasta local.
Testando a imagem
O comando final para inicializar o contêiner seria (com placa de desenvolvimento conectada):
1 |
usuario@local:~$ docker run -it --volume /home/usuario/projeto-teste:/root/projects --device=/dev/ttyUSB0 esp32_ambiente:1.0 |
Que irá: 1. utilizar o terminal atual para interagir com o contêiner (-it
); 2. mapear uma pasta do usuário local para uma pasta do contêiner (--volume
); 3. mapear dispositivo USB para contêiner (--device=
); e 4. usando a imagem compilada (esp32_ambiente:1.0
).
Por fim, compilamos um hello_world
para testar o ambiente:
1 2 3 4 5 6 7 8 9 10 |
root@conteiner:~/projects# cp -r $IDF_PATH/examples/get-started/hello_world . root@conteiner:~/projects# cd hello_world/ root@conteiner:~/projects/hello_world# idf.py build flash monitor ... I (303) main_task: Started on CPU0 I (313) main_task: Calling app_main() Hello world! This is esp32 chip with 2 CPU core(s), WiFi/BTBLE, silicon revision v1.0, 2MB external flash Minimum free heap size: 305104 bytes ... |
Assim, confirmando que o ambiente está funcionando corretamente.
Conclusão
Esse artigo teve o objetivo de introduzir o uso do Docker para o desenvolvimento de software embarcado, mostrando que com poucos passos é capaz de criar uma imagem que contém toda a configuração do ambiente de desenvolvimento e que pode ser compartilhada facilmente para desenvolvimento colaborativo e para uso em automações.
Referências
Para maiores informações, visite os links:
- Instalação do Docker
- Documentação do Docker
- Configuração de ambiente de desenvolvimento do ESP-IDF
- ESP-IDF
Saiba mais
ESP32: Instalação do ESP-IDF no Windows Subsystem for Linux (WSL)
Distribuindo uma aplicação com o Dockerfile
Imagem de capa adaptada de Unsplash com ícones open source do Iconduck.