Bootloader em microcontroladores STM32F0

Bootloader

Introdução

Apesar de todas as técnicas inventadas até hoje, todo software produzido é suscetível a conter erros. Em desenvolvimento de programas para computadores, existe a possibilidade de se enviar novas versões ao cliente, que ao simples clique do mouse todo erro encontrado é corrigido. Assim como programas tradicionais, programas feitos para sistemas embarcados podem também conter erros. Contudo a atualização nestes tipos de sistemas nem sempre é trivial como em computadores pessoais. 

Para se fazer um upgrade em sistemas embarcados, há a necessidade de se desenvolver um programa que (em teoria) é programado apenas uma vez e é o primeiro programa a ser executado após um reset. Esse programa é conhecido como bootloader, e todo cuidado é pouco caso deseje-se atualizá-lo. Se durante a atualização do bootloader houver alguma falha, todo o sistema estará comprometido. Isto porque após um reset não existirá nenhum programa válido na memória do processador.

Em sistemas embarcados maiores, normalmente esse programa já foi desenvolvido e fornece inúmeras funcionalidades, como, por exemplo, o famoso Das U-Boot. Também existem microcontroladores que já possuem um bootloader pré-programado, disponibilizando atualização de firmware via SPI ou UART, por exemplo. Porém muitas vezes há a necessidade de se desenvolver um bootloader próprio e esse é o assunto deste post.

Estrutura de um sistema com bootloader

Nest post usaremos o microcontrolador STM32F030CC, que é um ARM Cortex-M0 com 256 KB de flash. Como mencionado anteriormente, o bootloader é um programa que deveria ser programado uma única vez durante a vida do produto e é o primeiro programa a ser executado. Este microcontrolador possui o seguinte mapa de memória.

bootloader: memory map do stm32f0
Figura 1 - Mapa de memória do STM32F0

Após um reset, esse microcontrolador busca dados a partir do endereço 0x00000000. Porém note que a memória flash começa apenas no endereço 0x08000000. Neste microcontrolador o endereço 0x00000000 pode ser remapeado em 3 diferentes memórias: flash, RAM ou system memory, que nada mais é que um bootloader pré-programado.

Isso significa que se por exemplo a RAM for remapeada no endereço 0x00000000, acessando-se o endereço 0x00000100 ou 0x20000100 o microcontrolador acessaria os mesmos dados. Após o reset por padrão a memória flash é mapeada no endereço 0x00000000. Porém caso a memória flash não possua nada gravado na memória inicial 0x08000000, o microcontrolador automaticamente remapeia o bootloader interno para que este seja executado.

Sabendo-se disso, deve-se preparar o bootloader para ser armazenado a partir do início da flash e também gravar o programa principal em uma área da memória após o bootloader. Como exemplo, poderíamos separar os primeiros 4 KB da flash para o bootloader e o restante para o programa principal como na figura abaixo:

bootloader: Flash do STM32F0 dividida em duas seções
Figura 2 - Flash do STM32F0 dividida em duas seções

Vetores de interrupção

O microcontrolador STM32F030CC possui uma tabela de vetores de interrupção que está localizada nos primeiros 192 Bytes de memória. 

bootloader: Tabela de vetores de interrupção do STM32F0
Figura 3 - Tabela de vetores de interrupção

Como pode-se ver na imagem acima, o primeiro endereço armazena o endereço do início do stack pointer. O segundo endereço armazena o endereço da rotina a tratar a interrupção de reset. Isso será importante pois no programa principal teremos que remapear os vetores de interrupção, o que será abordado neste post mais adiante.

Linkeditor (linker)

Todo código compilado é linkeditado pelo linker. O linker precisa de um script (arquivo .ld), onde ele busca instruções para onde armazenar o programa. No gcc, a memória é definida na seção MEMORY. No nosso exemplo, iremos separar 4 KB iniciais para o bootloader e o restante para o programa principal. Lembrando que esse script pode e deve ser usado tanto para o bootloader como para o programa principal.

Há duas memórias definidas na flash, BOOTLOADER com 4 KB que inicia em 0x08000000 e outra região FIRMWARE que inicia em 0x08001000 com 252 KB.

Também foi criado um símbolo pra indicar onde começa o programa principal. Isso é apenas para evitar de ter números mágicos no programa e também por ser mais fácil de manter caso haja a necessidade de se mudar algo.

Aqui uma ressalva, pois alguns scripts usam a flash interna com diferentes nomes, neste exemplo o nome dado foi "ROM". Um pouco abaixo no script, na seção SECTIONS, vê-se que o código é alocado em endereço acima da ROM através do comando >ROM. Para facilitar o reuso do arquivo de linker, usamos o comando REGION_ALIAS que nada mais é que dar um segundo nome à tua área de memória. Ainda no exemplo acima, ROM será substituído por BOOTLOADER que foi definido na seção REGION sendo a região de memória que começa em 0x08001000.

É importante notar que deve-se orientar o linker a não usar os primeiros 192 bytes da RAM, pois estes serão usados para conter a tabela de vetores de interrupção na aplicação principal. Logo indicamos o início da RAM em 0x200000C0 com um tamanho de 32 KB - 192 bytes. A figura 4 (cortesia do nosso colega Alain Mouette), simplifica o que foi explicado.

O bootloader

O bootloader, na maioria dos casos, deve ser um programa simples e acima de tudo robusto, que tem apenas o propósito de receber uma nova imagem e reprogramar a flash. Isso para que ele ocupe o menor espaço possível e que não possua erros.

A imagem pode ser um arquivo .hex que normalmente é gerado pelos compiladores. Não é foco deste post explicar como receber uma imagem, armazená-la e copiar na flash interna. Normalmente esse processo é trivial e mecânico e não será detalhado. De uma forma geral, o processo de atualização consiste apenas em enviar a imagem para o microcontrolador que pode salvar em uma EEPROM / flash externa ou ainda mesmo, numa parte da flash interna. É prudente calcular um CRC da imagem para se assegurar que a imagem recebida e armazenada não contenha erros. Uma vez que a imagem tenha sido recebida corretamente, basta copiá-la nos endereços indicados pelo arquivo .hex.

Uma vez a imagem reprogramada, o bootloader deve tomar alguns cuidados antes de saltar para o programa principal: 

  1. Deve-se reconfigurar o stack pointer;
  2. Deve-se desabilitar todas as interrupções dos periféricos e também a interrupção global. 

Programa principal

Este é o programa que será executado normalmente no sistema. Porém o sistema é composto de um bootloader que é programado primeiro. Dessa forma todos os vetores de interrupção estão referenciados a ele. Isso significa que se durante a execução do programa principal uma interrupção ocorrer, o programa irá saltar para um endereço definido na parte do bootloader.

Logo há a necessidade de se remapear a tabela de vetores. Num core ARM Cortex-M0 isso é feito copiando-se a tabela de vetores de interrupção na RAM e remapear o endereço 0x00000000 à RAM como explicado anteriormente. Quando uma interrupção ocorrer e tentar acessar um vetor, na verdade ele estará acessando a RAM que agora possui um endereço para o programa principal.

Esse remapeamento do vetor de interrupção deve ser feito o mais cedo possível, antes que qualquer interrupção seja habilitada. Um exemplo de como fazer está listado abaixo, lembrando que para os microcontroladores da STMicro, existe a ferramenta STM32CubeMX que fornece toda uma API de acesso ao hardware facilitando o desenvolvimento.

bootloader: Remapeamento tabela de interrupções do STM32F0
Figura 4 - Remapeamento da tabela de interrupções do STM32F0

Conclusão

Atualmente os sistemas embarcados estão cada vez mais complexos e os problemas de erros de programação são cada vez mais comuns. Há então a necessidade de se poder atualizar um sistema sem que seja necessário um programador ou alguma ferramenta específica e de preferência remotamente. O uso de bootloaders facilita essa tarefa.

Ao se desenvolver um bootloader deve-se tomar vários cuidados essenciais que em suma estão listados abaixo:

  1. Criar duas regiões de memória, uma para o bootloader e outra para o programa principal no script do linker;
  2. Processar um arquivo imagem e enviar ao bootloader, o qual deverá salvar em algum tipo de mídia, seja uma flash ou EEPROM externa ou mesmo numa parte reservada da flash interna. Essa imagem deve ser verificada se foi bem recebida antes de ser gravada;
  3. Após a atualização do firmware, deve-se desativar todos as interrupções e resetar o stack pointer antes de saltar para o programa principal;
  4. No programa principal, deve-se remapear a tabela de vetores de interrupção o mais cedo possível e apenas depois configurar os periféricos e habilitar as interrupções.

Neste post apenas abordamos os principais passos para o desenvolvimento de um sistema com bootloader. Não foi detalhado aqui questões de segurança como criptografia da imagem ou ainda mecanismos para deixar o bootloader mais robusto em caso de falhas. Isso é uma escolha pessoal de cada desenvolvedor e também ligado às limitações do sistema em si.

Caros leitores, vocês têm usado algum tipo de bootloader em seus sistemas? Usam algum tipo de criptografia para se proteger contra a pirataria? Alguma dica de como deixar o sistema mais robusto? Deixem seus comentários.

Referências

[1] Tabela de vetores de interrupção

[2] GNU Linker

[3] Formato Intel hex

NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Marcelo Jo
Engenheiro eletrônico com 10 anos de experiência em sistemas embarcados, pós graduado em redes de computadores e atualmente cursando mestrado em sistemas de visão por computador na universidade Laval no Canadá. Compartilha seu conhecimento neste portal quando tem tempo livre e quando não está curtindo a vida com sua mulher e os 3 filhos.

10
Deixe um comentário

avatar
 
4 Comment threads
6 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
4 Comment authors
Acacio GomesMarcelo JoRafael GebertRafael Dias Recent comment authors
  Notificações  
recentes antigos mais votados
Notificar
Acacio Gomes
Visitante
Acácio Gomes

Muito bom artigo!

Atualmente estou desenvolvendo um bootloader para um sistema, mas irei reservar três regiões, uma pequena para o bootloader, e duas para firmware, uma delas sendo utilizada como backup. Irei implementar uma espécie de swap de memória para atualização do firmware.

Muito obrigado pelo artigo, me serviu para confirmação de algumas informações e acrescentar algumas outras!

Marcelo Jo
Visitante
Marcelo Jo

Que beleza!
Onde vc está guardando a imagem? Uma idéia também seria deixar armazenada na flash até o próximo upgrade. No bootloader vc poderia sempre verificar se a o programa principal está correto antes de saltar pra ele. Em caso de problema, bastaria recopiar a imagem para a flash interna. O lado ruim é o tempo de boot que aumentaria pois seria necessário verificar se o programa principal está ok. Veja se o teu sistema requer todo esse cuidado ou não.

Acacio Gomes
Visitante
Acácio Gomes

Sim, esta é uma das opções que estou analisando.
Muito obrigado Marcelo!

Rafael Gebert
Visitante
Rafael Gebert

Marcelo, não sabia que dava para remapear apenas o vetor de interrupção para a memória RAM, isso simplifica muito o bootloader (em microcontroladores pic24 não dá para fazer isso... eu sempre tinha que regravar a tabela do endereço 0x0000 até o 0x0400 e depois saltar para o endereço do app)!
Ao fazer isso (remapear para a ram) ocorre alguma consequência, como perda de desempenho, ou tanto faz?
Obrigado!

Marcelo Jo
Visitante
Marcelo Jo

E aí Rafael,

Sim, isso é no cortex-M0. No M0+, M3, etc já é feito de outra forma (mais fácil). No PIC funciona diferente mesmo. Eu já fiz um bootloader pro PIC24 também.

O lado ruim q eu vejo de se usar na RAM é caso ela seja corrompida por algum ponteiro errado no teu código. Quanto ao desempenho, não muda nada não.

Rafael Gebert
Visitante
Rafael Gebert

Humm... realmente pode ser perigoso em função de ponteiros...
Existe outra forma mais fácil? Puxa... isso inclui também os M4? você teria algum link? Obrigado!

Marcelo Jo
Visitante
Marcelo Jo

Sim... M4 também. Tem um registro chamado VTOR (Vector table offset register) que na verdade vc indica o offset do vetor de interrupção. Aí vc não precisa ter que copiar tudo pra RAM e remapear a RAM no endereço 0x0000 0000.

Dá uma olhada aqui

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0553a/BABIFJFG.html

Rafael Gebert
Visitante
Rafael Gebert

obrigado! 😉

Rafael Gebert
Visitante
Rafael Gebert

Concordo com o Rafael! O Marcelo fez um excelente artigo! Para um produto embarcado, o bootloader é obrigatório! Acho importente realçar a questão de segurança... nunca se deve fazer um bootloader que receba o arquivo .hex sem estar protegido com criptografia... Como sou meio paranóico com isso, além da criptografia eu ainda retiro todas as funções básicas da aplicação e remapeio para a área do bootloader, funções simples como memcpy, memset (que não vão mudar nunca)... a vantegem de fazer isso é que se alguém quebrar a criptografia do arquivo hex, ainda não vão conseguir piratear o equipamento porque no… Leia mais »

Rafael Dias
Visitante
Rafael Dias

muito bom.