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”.

Option bytes do stm32

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:

  1. checksum = 1ºbyte ^ 2º byte.
    • 0x08 ^ 0x00 = 0x08.
  2. checksum = checksum ^ 3º byte.
    • 0x08 ^ 0x00 = 0x08.
  3. 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.

Esp8266 comunicando com 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):

Regras de acesso do firebase

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;
}