Atualizar um microcontrolador à distancia é um recurso bem interessante e desejado em alguns casos. Assim, neste post, vou mostrar um caminho possível para permitir que um STM32 seja atualizado à distância utilizando o Firebase.
Preciso esclarecer que não pretendo criar um tutorial passo a passo com códigos. Minha ideia é indicar pontos importantes que observei ao tentar (e conseguir) implementar o procedimento por conta própria.
Ideia
É possível atualizar um microcontrolador remotamente de algumas formas diferentes, como via Bluetooth, módulo de RF ou via Internet. O processo que ensinarei aqui se baseia no acesso à internet. E a forma mais barata e fácil, que conheço, de permitir o acesso à internet a um dispositivo, é utilizando um esp8266. Por conta disso, esse foi o caminho que segui.
Aprendi a atualizar o STM32 à distância a partir de um projeto de um jogo eletrônico que resolvi desenvolver. Por conta disso, algumas informações que mostrarei fazem referência a esse projeto. Entretanto, fiz o processo funcionar primeiro utilizando placas de desenvolvimento (um NodeMcu e uma Bluepill adaptada). Veja abaixo a comprovação de que o sistema é capaz de atualizar remotamente.
Princípio de funcionamento [STM32]
Bootloader do STM32
Obs: O projeto que desenvolvi utiliza um stm32f303cbt6, então as explicações abaixo usam ele como referência.
Existe uma “nota de aplicação” da ST chamada AN2606, que informa que diversos microcontroladores STM32 são programados com um bootloader que tem o principal propósito de permitir a gravação da memória flash por meio de alguns periféricos (USART, I2C, SPI, USB etc). Esse bootloader fica localizado na memória de sistema (system memory) e ele é gravado durante o processo de produção dos microcontroladores.
Consulte a AN2606 para saber se o seu STM32 possui o bootloader.
De acordo com a página 62 do datasheet, a memória flash possui duas partes distintas:
- Bloco principal: armazena o código principal e ou dados do usuário (no caso de simular uma EEPROM).
- ‘Bloco de informações’: armazena os “option bytes” (bytes de configuração) e a memória de sistema.
Segundo a página 65 do datasheet, o único propósito aparente da memória de sistema é armazenar o bootloader, e ela é protegida de escrita e leitura. Ou seja, uma vez que o bootloader é gravado em fabricação, ele não pode ser apagado e nem lido.
Como acessar o bootloader
Tendo em vista o que foi falado acima, usar o bootloader parece uma ótima forma de atualizar o STM32 remotamente e é o que faremos. Agora precisamos entender como acessá-lo e utilizá-lo.
Novamente, lendo a página 62 do datasheet, vemos que o “Boot0” e o “Boot1” podem ser usados para selecionar qual memória o microcontrolador acessa quando ele é resetado. No meu caso, “Boot0” é um pino do STM32 e “Boot1” é um bit que pode ser configurado nos “option bytes” (dependendo “Boot1” pode ser um pino também). E, para acessar a memória de sistema (bootloader), devemos colocar o pino Boot0 em nível alto e configurar o Boot1 em 1 (consulte o datasheet do seu STM32, pois isso pode mudar).
Por padrão, o meu stm32f303cbt6 veio com o Boot1 configurado corretamente. Então, bastou alterar o estado do Boot0 (pino 44 do stm32f303cbt6). Por fim, resta apenas resetar o STM32 para que ele entre no bootloader e comece a aceitar os comandos para gravação da flash.
Entretanto, o acesso ao bootloader só é desejado na hora de atualizar o firmware e, em funcionamento normal, o pino Boot0 deve ser mantido em nível baixo para que o STM32 acesse o bloco principal da flash. É importante lembrar disso na hora de planejar a ligação entre o STM32 e o esp8266.
Configurando option bytes
Conforme dito acima, o meu STM32 veio com o Boot1 configurado corretamente. Mas, se esse não for o seu caso, você pode alterar os option bytes pelo programa STM32CubeProgrammer. É só abrir o programa, conectar com o STM32 e clicar no ícone escrito “OB”.
Pré requisito para comunicar com bootloader
Assim como foi descrito, a gravação da memória flash via bootloader pode ser feita por meio de alguns periféricos diferentes. Para saber qual ou quais podem ser usados, verifique a seção “Embedded boot loader” do datasheet do seu microcontrolador ou a nota AN2606.
Para o stm32f303cbt6, a página 63 do datasheet informa que pode-se utilizar o USART1, USART2 ou o USB. Como o esp8266 também possui um periférico UART, resolvi usar o USART1.
Além da escolha certa do periférico, é importante respeitar o tempo que o bootloader leva para ser iniciado. Na página 362 a 373 da AN2606, é possível conferir os tempos mínimos para diferentes microcontroladores.
Configurando UART para comunicar com bootloader
Para comunicar corretamente com o bootloader, o esp8266 precisa ajustar alguns parâmetros do UART, como baud rate e a quantidade de bits de dados de parada e de paridade. Na própria AN2606 é possível encontrar algumas informações, mas a nota de aplicação AN3155 possui mais detalhes sobre.
De acordo com a página 5, o UART deve ser configurado com 1 bit de início, 8 bits de dados, 1 bit de paridade par e 1 bit de parada. Além disso, é possível escolher o baud rate respeitando o limite descrito na página 6. Nela fala que a máxima velocidade testada é 115200, que foi a velocidade que acabei usando.
Cheguei a testar um baudrate de 500000 e 512000, mas não obtive sucesso. De toda forma, com o baudrate de 115200, um firmware de ~10 kbytes foi transferido em menos de 2 segundos e um de 81 kbytes (o caso do vídeo mostrado anteriormente) foi transferido em ~20 segundos, o que é relativamente rápido.
Início da comunicação com bootloader
Conforme a página 5 da AN3155, para iniciar a comunicação com o bootloader, o esp8266 deve enviar o byte 0x7F e aguardar um byte de confirmação (ACK). O ACK é um byte de valor 0x79. Assim que o esp8266 receber o ACK, o STM32 está pronto para receber comandos.
Comandos existentes para o STM32
Existem muitos comandos, mas, ao criar o código de atualização, me preocupei apenas com os comandos: Get (0x0), Write Memory (0x31) e Extended erase (0x44). A lista completa de comandos pode ser vista na página 7 da nota AN3155.
A ideia do meu código é, o esp8266 limpa toda a memória do STM32 com o comando Extended erase (0x44) e depois escreve na memória com o comando Write Memory (0x31). O ideal seria limpar apenas a região da memória que precisa ser escrita, mas, pra agilizar o desenvolvimento do código, resolvi simplesmente apagar toda a memória.
Acabei usando o comando Get (0x0) por dois motivos: para testar a comunicação inicialmente e para saber qual comando de apagar a memória o stm32f303cbt6 aceita (pode ser o 0x43 ou 0x44). Então, recomendo implementar o Get também pelos mesmos motivos.
Enviando comandos para o STM32
As página 8 em diante da nota AN3155 explicam como cada comando deve ser usado para o STM32. Cada um tem uma certa particularidade, então não vou explicar os detalhes. Mas aqui cabem duas explicações gerais:
- Envio inicial do comando:
Para enviar qualquer comando, é necessário enviar inicialmente 2 bytes: 1 com o código do comando e outro que corresponde ao complemento do código do comando. Isto é, se o comando for 0x31 (Write), envia-se o byte 0x31 seguido do byte 0xCE (complemento). O complemento nada mais é do que um XOR entre o byte do comando e o byte 0xFF (0x31 ^ 0xFF = 0xCE).
- Cálculo do checksum:
Alguns comandos, como o de escrita, exigem o envio de um byte de checksum utilizado para verificar erros na comunicação. O cálculo do checksum é feito em cima de todos os bytes enviado naquele instante. Para isso, aplica-se uma operação de XOR entre o 1º e 2º byte, aplica-se XOR entre o resultado destes 2 com o valor do 3º byte, novamente, faz-se o XOR do resultado com o valor do 4º byte e assim em diante.
Por exemplo, na hora de enviar o endereço para o STM32 quando se utiliza o comando Write, são necessários 4 bytes de endereço e 1 de checksum. Imaginando que o endereço seja 0x08000004, temos:
1º byte = 0x08.
2º e 3º byte = 0x0.
4º byte = 0x04.
E o cálculo do checksum segue o seguinte passo a passo:
- checksum = 1ºbyte ^ 2º byte.
- 0x08 ^ 0x00 = 0x08.
- checksum = checksum ^ 3º byte.
- 0x08 ^ 0x00 = 0x08.
- checksum = checksum ^ 4ºbyte.
- 0x08 ^ 0x04 = 0x0C.
Se houvessem mais bytes, o passo 3 se repetiria para o restante.
Exemplo real da comunicação
A imagem abaixo é uma captura que fiz com um analisador lógico mostrando a comunicação entre o esp8266 e o bootloader do STM32. Neste caso, o esp8266 executa comandos para limpar e depois gravar a memória do STM32.
Princípio de funcionamento [esp8266]
Os detalhes restritos ao STM32 foram abordados na seção anterior. Agora vejamos os detalhes do esp8266.
Ligação entre esp8266 e STM32
Existem alguns formatos diferentes para o esp8266 e um deles é o módulo esp12f, que é o que utilizei em meu projeto. Então, as ligações serão baseadas nele.
A partir do que foi falado em tópicos anteriores, precisamos de 4 conexões entre o STM32 e o esp8266:
- 1 pino para resetar o STM32.
- Optei por usar o GPIO14 do esp12f.
- É preciso escolher um pino do esp12f que não interfira involuntariamente com o reset do STM32. Digo isso, pois alguns pinos do esp8266 ficam em nível alto/baixo na reinicialização.
- Essa ligação não dispensou o resistor de pull-up no pino NRST do STM32.
- 1 pino para trocar o estado do pino Boot0 do STM32.
- Optei por usar o GPIO12 do esp12f.
- É preciso escolher um pino do esp12f que não interfira involuntariamente com o boot do STM32. Digo isso, pois alguns pinos do esp8266 ficam em nível alto/baixo na reinicialização.
- Mesmo com essa ligação, inseri um resistor de pull-down no pino Boot0 do STM32.
- 2 pinos para ligar periférico UART (esp8266) e USART1 (stm32).
- Aqui não temos como escolher, já que esse periférico fica atrelado aos pinos GPIO1 e GPIO3. No caso, o GPIO1 (TX do esp12f) foi ligado ao PA10 do STM32 (RX do STM32) e o GPIO3 (RX do esp12f) foi ligado ao PA9 do STM32 (TX do STM32).
Obtendo código atualizado
Podemos obter o firmware atualizado de alguns modos diferentes, mas o importante é que ele esteja armazenado em algum ‘lugar’ acessível pela Internet. Eu decidi fazer uso do Firebase, que é um servidor gratuito (até certo ponto) e seguro.
Além disso, o dispositivo precisa saber se ele tem a versão do firmware mais recente. Para isso, criei um arquivo txt no Firebase que contém o nome do dispositivo seguido da versão atual do firmware. Assim, o dispositivo compara qual é a versão atual armazenada na memória (gravei na EEPROM simulada do esp8266) com a versão disponível online.
Se a versão online for maior que a atual, o esp8266 baixa o código atualizado e guarda em sua memória interna (memória do sistema de arquivos).
Detalhes do Firebase
O processo de criar uma conta no Firebase e fazer upload de arquivos no “Storage” é bem simples e direto. Portanto, não pretendo explicá-lo.
O esp8266 armazena 3 links: os links dos firmwares mais recentes do STM32 e do esp8266 e o link do arquivo txt que informa o número da versão disponível. Entretanto, quando você faz upload de um arquivo pro Firebase, ele sempre gera um link diferente por conta do token de acesso. Para ignorar o token e poder usar um link único, mesmo quando fizer upload do firmware mais novo, tive que alterar as regras de acesso (apenas de leitura):
Com isso, posso ler o arquivo sem o token de acesso. E o link correto pode ser obtido indo em Storage -> clicando no arquivo desejado -> clicando em cima do token de acesso após aparecer a mensagem “Clique para copiar o URL…”. Entretanto, ainda é preciso remover o token deste link para obter o formato genérico que não se altera quando você fizer upload novamente. Para isso, apague o trecho “&token=” em diante.
Código de função para baixar arquivo
Abaixo, estão trechos do código que usei no esp12f (via IDE do Arduino) para fazer o download do arquivo do Firebase e gravá-lo na memória. Os trechos englobam a inclusão das bibliotecas que utilizei, uma função de inicialização (updater_init) e uma função para baixar e gravar o arquivo do Firebase (updater_download_stm_file), a qual retorna 1 se obteve sucesso e 0 senão.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | // Bibliotecas utilizadas #include <ESP8266HTTPClient.h> #include <LittleFS.h> // Instâncias utilizadas WiFiClientSecure client; HTTPClient https; void updater_init() { // Ignora o certificado SSL client.setInsecure(); // Inicializa o sistema de arquivos LittleFS.begin(); } uint8_t updater_download_stm_file() { uint8_t buff[128]; uint32_t size; uint32_t n_reads; uint8_t https_code; // Inicia comunicação if (!https.begin(client, UPDATER_STM_URL)) // Troque o UPDATER_STM_URL pelo link do arquivo { return 0; } https_code = https.GET(); // Verifica se o código está correto if (https_code != HTTP_CODE_OK && https_code != HTTP_CODE_MOVED_PERMANENTLY) { https.end(); return 0; } File file = LittleFS.open(UPDATER_STM_FILENAME, "w"); // Troque o UPDATER_STM_FILENAME pelo nome do arquivo (ex: "/stm.bin") if(!file) { file.close(); return 0; } size = https.getSize(); while(size) { // Lê 128 bytes a não ser que faltem menos bytes que isso n_reads = size < 128 ? size : 128; n_reads = client.readBytes(buff, n_reads); size -= n_reads; file.write(buff, n_reads); yield(); if(!client.connected()) { return 0; } } // Encerra as comunicações file.close(); https.end(); return 1; } |
Reforçando: não pretendo fazer um tutorial passo a passo neste post, então não mostrarei o código da comunicação entre STM32 e esp8266.
Bonus - Código para o esp8266 atualizar o próprio firmware
O foco deste post foi mostrar uma forma de atualizar o firmware do STM32 à distancia, mas, no meu projeto, acabei implementando também o recurso de atualizar o código do esp8266 à distância. Esse processo é até bem simples e também segue a ideia do Firebase que mostrei acima. Enfim, abaixo está o código da função que faz isso (as bibliotecas e a função de inicialização do código anterior são necessárias).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | uint8_t updater_update_esp() { uint8_t https_code; uint32_t content_len; uint32_t content_written; if (!https.begin(client, UPDATER_ESP_URL)) // Troque UPDATER_ESP_URL pelo link do firmware do esp8266 { return 0; } https_code = https.GET(); // Verifica se o código está correto if (https_code != HTTP_CODE_OK && https_code != HTTP_CODE_MOVED_PERMANENTLY) { https.end(); return 0; } // Verifica se tem espaço suficiente content_len = https.getSize(); if (!Update.begin(content_len)) { https.end(); return 0; } content_written = Update.writeStream(client); if (content_written != content_len) { https.end(); return 0; } if (!Update.end()) { https.end(); return 0; } // Terminou update if (!Update.isFinished()) { https.end(); return 0; } https.end(); // Opcional: resetar o ESP ao fim para forçar a atualização //ESP.restart(); return 1; } |
Publicação fantástica!!! Meus parabéns!
Brigadão, Chico!
Amigo, você poderia fonecer as funções de comunicação entre o ESP e o STM? apenas em relação à gravação do FW no STM.
Olá, Tiago. Vou pensar a respeito, pois pretendo utilizar esse projeto comercialmente eventualmente. Qual seria o objetivo do projeto que você está mexendo que precisa dessa funcionalidade?
Ei Fábio. Entendo totalmente sua preocupação! Eu não estou interessado nas funcionalidades do seu projeto em si. Seria possível você fornecer mais detalhes (códigos) sobre o processo em si do OTA? Eu estou desenvolvendo um projeto privado na área de medição e qualidade de energia elétrica. Não creio que tenha a ver com o seu projeto em si, a não ser que o seu projeto se trate especificamente sobre esta questão da atualização OTA do STM. No momento eu encontrei uma forma (no Goggle mesmo) de atualizar o STM através de um link aberto de download do arquivo .bin. O cara inclusive utiliza um repositório aberto do GitHub, mas eu gostaria de mais segurança no processo. Eu tenho um código capaz de atualizar o ESP32 através do Firebase. Nesse código eu utilizo várias etapas de segurança, como usuário, senha, API key e Storage Bucket ID. Minha intenção era unir o seu código ao meu, de forma que eu conseguisse atualizar tanto o ESP32 quanto o STM a partir do Firebase. Porém eu não vi essas etapas de segurança no código que você forneceu.
Olá, Tiago. Entendo que você não vá pegar o código para replicar meu projeto, mas o ponto de eu não querer compartilhar o código é também de querer proteger meu valor como profissional. Por exemplo, você está fazendo um projeto privado e não sei se está sendo pago para isso. Mas caso esteja, eu te passar o código seria como se estivesse fazendo trabalho de graça para você. A princípio não vejo problema algum em ajudar outras pessoas, mas sou novo e minha carreira na área dos sistemas embarcados ainda está sendo formada e preciso de criar garantias para ter um perfil diferenciado. E uma forma de fazer isso é ter conseguido fazer algo que poucos conseguiram. Em algum momento, quando tiver chegado num ponto legal da minha carreira pretendo sim disponibilizar diversos códigos e projetos que atualmente são privados.
De todo modo, respondendo suas dúvidas para tentar te dar uma luz: de fato o código do esp8266 que forneci não tem muita segurança, além do fato de usar https ao invés de http. Porém, como você já tem um código que baixa o binário do ESP32 para atualizar o próprio firmware, pode simplesmente reaproveitar esse código para baixar o binário do STM32. A não ser que você esteja utilizando a API automática do OTA (esp_https_ota). Enfim, acredito que essa parte de baixar o binário você consiga fazer sem problemas. O próximo passo seria pegar o conteúdo do arquivo binário e gravar no STM32. Independente se você fosse dar um ctrl c no meu código ou não, você precisaria entender como o processo funciona para reproduzi-lo adequadamente. E essa parte eu tentei descrever no tópico “Princípio de funcionamento [STM32]”. No meu caso, o que fiz foi criar uma biblioteca chamada stmbootloader que contém as funções informadas abaixo. É possível descobrir como criá-las lendo a nota de aplicação AN3155 e os outros detalhes que comentei no post.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
* @brief Inicia alguns parâmetros importantes
*/
void stmbootloader_init();
/**
* @brief Reinicia o stm32 para entrar no bootloader
*/
void stmbootloader_rstboot();
/**
* @brief Reinicia o stm32 sem entrar no bootloader
*/
void stmbootloader_rstnormal();
/**
* @brief Calcula checksum da mensagem
* @param msg Ponteiro que aponta para vetor da mensagem a ser enviada
* @param size Tamanho da mensagem
* @returns Valor do checksum
*/
uint8_t stmbootloader_checksum(uint8_t * msg, uint8_t size);
/**
* @brief Envia um comando e seu XOR
* @param cmd comando a ser enviado
*/
void stmbootloader_send_cmd(uint8_t cmd);
/**
* @brief Envia n bytes + checksum
* @param msg Ponteiro que aponta para vetor da mensagem a ser enviada
* @param size Tamanho da mensagem
*/
void stmbootloader_send_nbytes(uint8_t * msg, uint8_t size);
/**
* @brief Aguarda o recebimento do ACK
* @returns 1 se recebeu e 0 se não
*/
uint8_t stmbootloader_wait_ack();
/**
* @brief Aguarda uma resposta do dispositivo e armazena no buffer do struct 'stmbootloader'
* @returns 1 se recebeu algo e 0 se não
*/
uint8_t stmbootloader_wait_response();
/**
* @brief Ininicia o bootloader
* @returns 1 se conseguiu e 0 se não
*/
uint8_t stmbootloader_startcom();
/**
* @brief Executa o comando get e os dados são retornados no buffer do struct 'stmbootloader'
* @returns 1 se conseguiu e 0 se não
*/
uint8_t stmbootloader_cmd_get();
/**
* @brief Executa o comando write (memória flash)
* @param address endereço para escrever (é acrescido de STMBOOTLOADER_FLASH_ADDR automaticamente)
* @param msg Ponteiro que aponta para vetor da mensagem a ser escrita na memória
* @param size Tamanho da mensagem
* @returns 1 se conseguiu e 0 se não
*/
uint8_t stmbootloader_cmd_write(uint32_t address, uint8_t * msg, uint8_t size);
/**
* @brief Limpa toda a memória
* @returns 1 se conseguiu e 0 se não
*/
uint8_t stmbootloader_cmd_globalerase();
Muito obrigado, Fábio. Te desejo sucesso em sua carreira.
Eu já consegui realizar a implementação da atualização do firmware com todos os requisitos de segurança, tanto pro ESP32 quanto pro STM32 e ambos via Firebase. Assim que possível, pretendo postar a solução para ajudar a essa comunidade Open Source que tanto cresce.
Por nada. Show de bola, Tiago! Sucesso para você também.
Olá Tiago boa tarde! Tenho interesse em adquirir a solução e acesso aos resultados que obteve, como posso lhe contatar ?