Para estudar alguns aspectos da arquitetura do microcontrolador do Arduino UNO, o ATMega328P
, fiz algumas modificações no Bootloader adicionando um "contador de flash". Toda vez que fizermos um upload de sketch, utilizando o Arduino IDE, ou Platform IO, AVRDude, nosso bootloader irá incrementar um contador. Assim, podemos monitorar quantas vezes um microcontrolador foi "flashado".
Para começar vamos entender alguns aspectos do bootloader. Sabemos que é o bootloader do Arduino o programa responsável por conversar, via serial, com o computador, receber e armazenar um sketch na memória flash do microcontrolador. Agora, pense comigo. Aonde esse programa é armazenado? E aqui estou me referindo ao programa do bootloader. O ATMega328P
tem apenas uma memória flash. Daí, vem a primeira característica interessante desse microcontrolador. Tanto aplicação, nossos sketches, quanto bootloader estão armazenados no mesmo espaço de memória, os 32kb de flash do ATMega328P
. E o interessante é que o microcontrolador consegue gravar na memória enquanto está lendo dessa mesma memória. Isso é conhecido como "auto programação". E parando para pensar isso pode gerar alguns conflitos. Para resolver isso e implementar algumas funcionalidades de segurança, a memória flash do ATMega328P
pode ser dividida em duas sessões. Uma sessão de aplicação e uma de boot:
Apenas o código do boot section, bootloader, que pode executar instruções SPM
(store program memory).
O tamanho da sessão de boot é configurada pelos fuse bits do ATMega328P
:
Os bits bootsz0
e bootsz1
do banco HIGH
nos dão 4 possibilidades para o boot section:
BOOTSZ0 | BOOTSZ1 | SIZE (words) |
---|---|---|
0 | 0 | 256 |
0 | 1 | 512 |
1 | 0 | 1024 |
1 | 1 | 2048 |
O Arduino UNO geralmente vem com um bootloader otimizado (Optiboot). E o boot section é configurado para usar apenas 512
words. No caso do meu bootloader customizado eu estou usando 1024
words, pois estou usando como base o bootloader do Arduino Duemilanove. O Optiboot
tem muitos hacks pra caber em 512
words, e é necessário um conhecimento maior sobre a arquitetura AVR e as ferramentas de compilação. Por isso optei pelo bootloader do Duemilanove, o código é mais fácil de compreender 😅.
Para deixar o código melhor organizado, e também mais fácil de ser lido e entendível, primeiramente dividindo DEFINE
, cabeçalhos, inline assembly em diferentes arquivos. O código está público no meu Github: https://github.com/microhobby/aboot
⚠️ Meu objetivo aqui não é explicar todo o código do bootloader, nem que você saia dessa leitura um especialista nesse código. Meu objetivo é mostrar e provar alguns conceitos, customizando (de forma porca e não otimizada) esse código.
Para que nosso bootloader pudesse gravar e incrementar um contador toda vez que recebesse um novo sketch, adicionei a seguinte função implements.h#L133:
void increment_flash_counter ()
{
/* only increment one time */
if (flash_count_var == 0xFF) {
flash_count_var = flash_count();
/* increment */
flash_count_var++;
flash_count_update(flash_count_var);
}
}
Estou usando o último byte da EEPROM
como nosso contador que será incrementado a primeira vez que essa função for chamada. E adicionei a chamada da função no loop principal onde o bootloader processa os bits da serial e escreve esses bits na flash main.cpp#L209:
asm_flash_data();
increment_flash_counter();
Com um AVR USBasp
conectado no seu Arduino UNO você pode usar a configuração do repositório para o Platform IO platformio.ini. Ele já está configurado para gravar o bootloader e setar os fuses.
Para testar se nosso bootloader está incrementando o contador corretamente vamos gravar um novo sketch no Arduino. Estou usando o seguinte código (arduinoFlashCounter):
#include <Arduino.h>
#include <EEPROM.h>
void setup()
{
pinMode(13, OUTPUT);
Serial.begin(115200);
EEPROM.begin();
int flash_count = EEPROM.read(512);
Serial.print("Flash counter EEPROM :::: ");
Serial.println(flash_count);
Serial.println("******************");
EEPROM.end();
}
void loop()
{
digitalWrite(13, !digitalRead(13));
delay(500);
}
Em suma esse sketch está lendo o último byte da EEPROM
, que é nosso contador de flash, e escrevendo o valor na serial. Realizando o upload desse sketch, ao final podemos abrir um serial monitor e verificar quantos uploads já tivemos nesse microcontrolador:
Já que estamos aqui estudando algumas características internas do ATMega328P
vamos além. Sabendo que o microcontrolador tem apenas uma memória de armazenamento flash e que essa memória é dividida em duas sessões, mas de forma lógica, não física, será que é possível acessar uma sessão executando código em outra sessão? Enquanto estamos rodando o código do bootloader podemos escrever no application section. E se eu quiser acessar código do meu boot section dentro do meu application section? Para saber se isso é possível vamos dar uma olhada mais de perto na arquitetura da CPU do ATMega328P
, mais especificamente no Program Counter
:
O Program Counter
do ATMega328P
tem 14 bits (0x3FFF), que é o suficiente para acessar 16K endereços. A memória flash tem 32Kb e para o AVR isso está organizado em 16K endereços de 16bits. Para entender melhor como o AVR e Program Counter
do ATMega328P
funcionam vamos fazer uma comparação com a arquitetura utilizada geralmente nos nossos computadores:
Geralmente nós temos uma memória de armazenamento, persistente, um HD ou SSD. No HD que estão armazenados os programas. Agora para que a CPU consiga rodar as instruções primeiramente esse programa tem que ser carregado do HD para a memória principal, memória RAM. Geralmente nessas arquiteturas temos apenas um barramento acessando um mesmo espaço de memória, memória RAM, que armazena instruções e dados. Isso é uma característica da arquitetura de Von Neumann.
⚠️ Mas não podemos dizer que Intel x86-64 e arm Cortex A são puramente classificadas como uma arquitetura Von Neumann. Arquiteturas modernas tendem a usar múltiplos aspectos e diferentes abordagens para otimizar e aumentar a performance.
No caso do AVR do nosso ATMega328P
:
Temos dois espaços de memória, um para instruções (flash) e outro para dados (SRAM). E, com dois espaços de memória, também temos dois barramentos: um para instruções (flash) e outro para dados (SRAM). Assim o ATMega328P
consegue executar uma instrução por ciclo de clock. Isso é uma característica da arquitetura Harvard. Enquanto uma instrução está sendo executada a próxima está sendo carregada. E como vimos acima o Program Counter
aponta direto para endereços de memória da flash. O que temos na flash são instruções AVR, 16 bits, prontas para serem carregadas e executadas pela CPU.
Agora que já conhecemos as principais características das arquiteturas Von Neumann e Harvard, podemos notar porque arquiteturas modernas, Intel x86-64, arm, não podem ser classificadas como puramente arquiteturas Von Neumann ou Harvard. Apesar de se ter uma memória principal que compartilha o mesmo espaço para programas (instruções) e dados (Von Neumann). Na CPU são implementadas memórias cache. E nestas arquiteturas modernas, geralmente temos caches separados para dados (D Cache) e instruções (I Cache):
E com isso também se implementa dois barramentos para cada cache (Harvard).
Agora já temos informações o bastante para provar o conceito de "se é possível acessar código do boot section dentro do application section". Então vamos lá.
No nosso código de teste estamos acessando a EEPROM
para ler o último byte, o "contador de flash", mas nós já temos uma função para ler isso no boot section:
int flash_count (void);
Ótimo, sabemos que nossa memória flash armazena as instruções AVR prontas para serem executadas e que o Program Counter
do ATMega328P
aponta direto para endereços na flash. E já que não há uma barreira física entre boot section e application section, o que temos que fazer é descobrir em que endereço a função flash_count
está armazenada e apontar o Program Counter
para esse endereço. Simples! 🙃
Para descobrir em que endereço da flash o flash_count
foi armazenado, podemos usar o seguinte comando no repositório do bootloader:
avr-objdump .pio/build/castellino/firmware.elf -x
⚠️ Para ter esse comando disponível você irá precisar do pacotebinutils-avr
instalado.
O avr-objdump
recebe como argumentos o caminho do firmware.elf
do nosso bootloader e o -x
que irá listar todos os cabeçalhos do arquivo ELF. Mas por agora o que interessa é apenas o cabeçalho da função flash_count
, então vamos filtrar o retorno do comando:
avr-objdump .pio/build/castellino/firmware.elf -x | grep "flash_count$"
Com isso teremos no retorno algo parecido com isso:
00007ac8 g F .text 0000000c flash_count
Pronto, temos o endereço onde a função estará armazenada 0x7ac8
. Para apontar o Program Counter
nesse endereço podemos escrever o seguinte:
int (*flashCounter)(void) = (int(*)()) 0x7ac8;
E por fim podemos usar essa função, endereço do boot section, dentro do nosso sketch de teste:
#include <Arduino.h>
#include <EEPROM.h>
int (*flashCounter)(void) = (int(*)()) 0x7ac8;
void setup()
{
pinMode(13, OUTPUT);
Serial.begin(115200);
EEPROM.begin();
int flash_count = EEPROM.read(512);
Serial.print("Flash counter EEPROM :::: ");
Serial.println(flash_count);
Serial.println("******************");
Serial.print("Flash counter Boot Section :::: ");
Serial.println(flashCounter());
EEPROM.end();
}
void loop()
{
digitalWrite(13, !digitalRead(13));
delay(500);
}
Compile, faça o upload e teste. E daí você vai perceber que isso NÃO VAI FUNCIONAR! 🙄
Se prestarmos atenção 0x7ac8
não cabe em 14 bits, e o Program Counter
tem apenas 14 bits. Então esse endereço não é válido. Mas porque C&*&%$# o avr-objdump
está me retornando esse endereço então? 🤔
Para as ferramentas do binutils-avr
e avr-gcc
, no arquivo ELF espera-se que flash seja representado por endereços contínuos. Então, temos 32K endereços, um para cada bit. Mas pro core do AVR são 16K endereços e em cada endereço 16 bits. Para obter um endereço válido para o Program Counter
do ATMega328P
basta dividir o endereço do header da função por 2:
int (*flashCounter)(void) = (int(*)()) (0x7ac8/2);
Compile, faça o upload e teste. Sucesso 🥳! Estamos acessando instruções que foram armazenadas no boot section mas reutilizando isso no application section: