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
1 |
# apt install gcc build-essential make |
Installiamo la toolchain avr
1 |
# 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 )
1 |
# 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
1 2 3 4 5 |
# 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*
1 2 3 4 5 |
# 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
1 2 3 4 5 6 7 8 9 10 11 |
# 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
1 2 3 4 5 |
int main() { while (1) { ; } } |
Ora compiliamolo con avr-gcc dichiarando qual’è il device per cui generare il codice binario con l’opzione -mmcu
1 |
# avr-gcc -mmcu=atmega328p -o test.out test.c |
Prima di poter essere inserito nel micro, il file binario va convertito in hex code
1 |
# 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… )
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 |
# 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 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
1 2 3 4 5 6 7 8 |
# 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
1 |
# avr-objdump -j .sec1 -d -m avr5 flash.hex > flash.dump |
Il contenuto del file flash.dump è il seguente
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 |
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
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 |
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 |
1 |
# make flash |