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

ATMega328P Application Section / Boot Section

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

Fuse bits

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

Customizando Bootloader

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.

Usando o Bootloader Customizado

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:

enter image description here

Bonus Round - Usando Código do Boot Section no Application Section

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.

Informação Bônus - Arquiteturas Modernas

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

Endereçando o Program Counter

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 pacote binutils-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: