Programmazione microcontrollori AVR con linux

Condividi:

In questo articolo configureremo un ambiente linux per la programmazione di microcontrollori AVR. Utilizzeremo come programmatore seriale una scheda Arduino 1 rev 3 senza l’IDE di Arduino in combinazione con avrdude. Il microcontrollore di destinazione sarà l’ Atmega328P presente sulla scheda.

Preparazione dell’ ambiente

Per prima cosa installiamo ( se non è già presente ) la toolchain base per la programmazione in C

# apt install gcc build-essential make

Installiamo la toolchain avr

# apt install gcc-avr binutils-avr avr-libc gdb-avr

Installiamo avrdude e dato che la scheda Arduino viene collegata tramite usb, anche libusb-dev ( se non è già presente )

# apt install avrdude libusb-dev
Test di collegamento

Colleghiamo la scheda Arduino tramite una presa usb e controlliamo che venga rilevata dal sistema tramite lsusb

# lsusb
..
..
Bus 002 Device 002: ID 2341:0043 Arduino SA Uno R3 (CDC ACM)
..

Vediamo che la scheda è stata rilevata, quindi cerchiamo quale interfaccia tty è stata associata. In /dev/ dovremmo trovare un file chiamato ttyUSB* o ttyACM*

# ls /dev/tty*
..
..
/dev/ttyACM0
..

Ricordiamoci di questo file perchè sarà quello che dovremo fornire ad avrdude per comunicare con il programmatore seriale.

Ora che sembra tutto funzionante, proviamo a leggere la device signature del micro presente sulla scheda Arduino.

Dovremo fornire ad avrdude tre parametri:

    • Il programmer-id, ovvero il tipo di programmatore che è elencato nel file /etc/avrdude.conf con l’opzione -c
    • Il partno, ovvero il micro di destinazione sempre elencato nel file di configurazione, con l’opzione -p
    • La porta da utilizzare, con l’opzione -P

Tutti questi parametri resteranno gli stessi ogni volta che utilizzeremo avrdude per comunicare con il programmatore.

Nel nostro caso quindi eseguiremo

# avrdude -c arduino -p m328p -P /dev/ttyACM0

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f (probably m328p)

avrdude: safemode: Fuses OK (E:00, H:00, L:00)

avrdude done.  Thank you.

avrdude ha letto la device signature del micro montato sulla scheda Arduino rilevando ovviamente che si tratta di un Atmega328p

 Programmiamo il microcontrollore

Scriviamo un semplice programma C composto da un banale ciclo while

int main() {
  while (1) {
    ;
  }
}

Ora compiliamolo con avr-gcc dichiarando qual’è il device per cui generare il codice binario con l’opzione -mmcu

# avr-gcc -mmcu=atmega328p -o test.out test.c

Prima di poter essere inserito nel micro, il file binario va convertito in hex code

# avr-objcopy -j .text -j .data -O ihex test.out test.hex

Ora possiamo programmare la memoria flash del micro con file hex appena generato. L’opzione -U dice quale operazione eseguire. Il parametro è formato da 4 campi separati da due punti memtype:op:filename:filefmt

    • Il primo è il tipo di memoria su cui operare ( eeprom, efuse, flash, fuse, hfuse, lfuse, lock, signature, fuseN, application, apptable, boot, prodsig, usersig )
    • Il secondo è il tipo di operazione da eseguire ( r=lettura della memoria, w=scrittura leggendo da file, v=verifica che il contenuto della memoria sia identico al contenuto del file )
    • Il terzo è il nome del file
    • Il quarto opzionale è il formato del file ( i=Intel Hex, r=raw binary, e=ELF, a=auto detect, ecc… )
# avrdude -c arduino -p m328p -P /dev/ttyACM0 -U flash:w:test.hex

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "test.hex"
avrdude: input file test.hex auto detected as Intel Hex
avrdude: writing flash (142 bytes):

Writing | ################################################## | 100% 0.04s

avrdude: 142 bytes of flash written
avrdude: verifying flash memory against test.hex:
avrdude: load data flash data from input file test.hex:
avrdude: input file test.hex auto detected as Intel Hex
avrdude: input file test.hex contains 142 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.04s

avrdude: verifying ...
avrdude: 142 bytes of flash verified

avrdude: safemode: Fuses OK (E:00, H:00, L:00)

avrdude done.  Thank you.

avrdude legge il file, lo scrive nella flash ed esegue la verifica che il codice che è presente nella memoria flash sia quello che è stato scritto.

Lettura della flash del microcontrollore

Con avrdude possiamo anche leggere il contenuto di un microcontrollore, nel nostro caso quello appena programmato. Possiamo leggere il contenuto di varie parti della memoria di un micro: i fuse bits, la flash, la eprom ecc… In questo caso eseguiremo un dump della memoria flash e decompileremo il codice macchina.

Per prima cosa la lettura della flash salvando il contenuto in un file

# avrdude -c arduino -p m328p -P /dev/ttyACM0 -U flash:r:flash.hex:i

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: reading flash memory:

Reading | ################################################## | 100% 4.64s

avrdude: writing output file "flash.hex"

avrdude: safemode: Fuses OK (E:00, H:00, L:00)

avrdude done.  Thank you.

Nel file flash.hex ora è presente in formato Intel Hex il contenuto della flash del microcontrollore

Disassembliamo il contenuto della flash

Analizziamo il file flash.hex mostrando il contenuto della sezione headers

# avr-objdump -h flash.hex

flash.hex:     formato del file ihex

Sezioni:
Ind Nome          Dimens    VMA       LMA       Pos file  Allin
  0 .sec1         00008000  00000000  00000000  00000000  2**0
                  CONTENTS, ALLOC, LOAD

Notiamo che è presente solo una sezione denominata .sec1

Ora possiamo leggere questa sezione disassemblandone il contenuto e salvando tutto su un file

# avr-objdump -j .sec1 -d -m avr5 flash.hex > flash.dump

Il contenuto del file flash.dump è il seguente

flash.hex:     formato del file ihex


Disassemblamento della sezione .sec1:

00000000 <.sec1>:
       0:       0c 94 34 00     jmp     0x68    ;  0x68
       4:       0c 94 3e 00     jmp     0x7c    ;  0x7c
       8:       0c 94 3e 00     jmp     0x7c    ;  0x7c
       c:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      10:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      14:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      18:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      1c:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      20:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      24:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      28:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      2c:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      30:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      34:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      38:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      3c:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      40:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      44:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      48:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      4c:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      50:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      54:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      58:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      5c:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      60:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      64:       0c 94 3e 00     jmp     0x7c    ;  0x7c
      68:       11 24           eor     r1, r1
      6a:       1f be           out     0x3f, r1        ; 63
      6c:       cf ef           ldi     r28, 0xFF       ; 255
      6e:       d8 e0           ldi     r29, 0x08       ; 8
      70:       de bf           out     0x3e, r29       ; 62
      72:       cd bf           out     0x3d, r28       ; 61
      74:       0e 94 40 00     call    0x80    ;  0x80
      78:       0c 94 45 00     jmp     0x8a    ;  0x8a
      7c:       0c 94 00 00     jmp     0       ;  0x0
      80:       cf 93           push    r28
      82:       df 93           push    r29
      84:       cd b7           in      r28, 0x3d       ; 61
      86:       de b7           in      r29, 0x3e       ; 62
      88:       ff cf           rjmp    .-2             ;  0x88
      8a:       f8 94           cli
      8c:       ff cf           rjmp    .-2             ;  0x8c
      8e:       ff ff           .word   0xffff  ; ????
  • 0x00: Qui inizia il vettore di interrupt che prevede un elemento per ogni interrupt della cpu. Il primo elemento del vettore è collegato all’ interrupt RESET. E’ la prima istruzione che esegue la mcu alla partenza o dopo un RESET e troviamo un salto all’indirizzo  0x68 da cui inizia il programma vero e proprio.
  • 0x04: Da qui fino a 0x64 si trovano tutti gli altri elementi del vettore di interrupt, quando sono programmati eseguono una jump all’indirizzo dove inizia la ISR relativa. Non avendo programmato nessuna ISR, da qui troviamo solo delle jump all’indirizzo 0x7c, dove troviamo una jump all’indirizzo 0x00 ovvero l’interrupt di RESET.
  • 0x68: Questo indirizzo ed il successivo inizializzano a 0 il registro di stato SREG che si trova all’indirizzo 0x3f
  • 0x6c: Questo indirizzo ed il successivo inizializzano i registri r28 ed r29 che rappresentano i byte basso ed alto dell’ Y register. Questo registro viene inizializzato con il valore 0x08ff ovvero l’ultimo indirizzo della SRAM interna
  • 0x70: Questo indirizzo ed il successivo inizializzano i registri SP_H ed SP_L dello stack pointer, che si trovano rispettivamente alle posizioni 0x3d e 0x3e, con i valori appena salvati sull’ Y register. Il motivo per cui viene inizializzato il puntatore di stack con l’ultimo indirizzo della SRAM è semplice: la stack cresce decrementando questo indirizzo e decresce incrementandolo. In sostanza ha preparato i puntatori dei registri Y e STACK anche se non sono state dichiarate variabili.
  • 0x74: Viene eseguita la call che lancia la funzione main del listato c
  • 0x78: Questa istruzione viene eseguita casomai terminasse la funzione main con un’istruzione ret, viene fatta una jump all’indirizzo 0x8a
  • 0x7c: Qui non ci si dovrebbe mai arrivare, nel caso ci si arrivasse, viene fatta una jump all’indirizzo 0x00 facendo ripartire tutto dall’inizio
  • 0x80: Qui inizia la funzione main. Da questo indirizzo fino a 0x86 vengono salvati sull’ Y register i valori di SP_L ed SP_H. Se ci fossero variabili dichiarate verrebbero puntate da questo valore.
  • 0x88: Esegue il jump relativo all’indirizzo 0x88, ovvero se stessa. Rappresenta il ciclo infinito while(1) del listato c
  • 0x8a: Qui il programma non arriverà mai. Il ciclo while infinito fa si che nel compilato manchino tutte le istruzioni per l’uscita dalla funzione main come la ret e tutte quelle per recuperare i valori salvati sulla stack. Viene eseguita una cli che disabilita il global interrupt flag
  • 0x8c: Anche qui il programma non arriva mai a meno che non sia stata eseguita la riga precedente, viene eseguito un altro loop infinito tramite jump relativo allo stesso indirizzo
  • 0x8e: Da qui in poi troviamo tutti op code 0xff perchè non è stato caricato nulla nella flash
Makefile

Possiamo includere i comandi di compilazione e caricamento in un makefile

CC=avr-gcc
MCU=atmega328p
CFLAGS=-mmcu=${MCU}
TARGET=test

OBJCOPY=avr-objcopy

AVRDUDE=avrdude
PARTNO=m328p
PROGRAMMER=arduino
PORT=/dev/ttyACM0

all: compile

${TARGET}.out: ${TARGET}.c
        ${CC} ${CFLAGS} -o $@ $^

${TARGET}.hex: ${TARGET}.out
        ${OBJCOPY} -j .text -j .data -O ihex $^ $@

compile: ${TARGET}.hex

flash: ${TARGET}.hex
        ${AVRDUDE} -p ${PARTNO} -c ${PROGRAMMER} -P ${PORT} -U flash:w:$^:i

clean:
        rm -f *.out *.hex
# make flash