Corso su ARM: migrando dagli 8 bit alla scoperta del Cortex M0+

Inizia la terza puntata del nostro corso di programmazione su ARM e dopo aver introdotto le tematiche e parlato dell'architettura e della sua evoluzione, oggi il nostro viaggio continua affrontando un aspetto fondamentale ovvero la programmazione. Come cambia l'accesso alle variabili ed ai registri, come vengono modificate le istruzioni e in definitiva come si può lavorare su architetture più complesse. Oggi vedremo come si fa a passare dagli 8 bit al Cortex M0+. Siete pronti?

Dicevamo terzo appuntamento, prima di passare all'analisi della scheda e all'ambiente di lavoro dovremmo utilizzare per effettuare la programmazione. Scopo della puntata sarà quindi dare uno sguardo alle variabili, alla sintassi ma anche cercare di capire come approcciarsi a questi microcontrollori.

Come introduzione al problema, possiamo dire che il processore ARM Cortex™-M0+ non ha soltanto il vantaggio di essere una delle soluzioni più interessanti dal punto di vista energetico e del consumo di potenza ma anche dal punto di vista della compatibilità dei tool e del set di istruzioni. La maggiore profondità di rappresentazione dei dati implica anche prestazioni di livello davvero superiore, specie per via del fatto che il livello di integrazione su area occupata di silicio garantisce rese mai raggiunte prima, specialmente da professori ad 8 bit.
A tal proposito, molti avranno avuto a che fare con Arduino ed alcuni sicuramente avranno scritto firmware per microcontrollori Atmel. Molto probabilmente tanti avranno proprio cominciato con questi micro ed è proprio loro che questa puntata si rivolge maggiormente.

Vedremo infatti come sarà possibile arrivare con facilità a padroneggiare l'architettura Cortex senza dover necessariamente passare attraverso il microcontrollore a 16 bit.
Tutto questo sarà possibile anche per via del fatto che il Cortex-M0+ dispone di una vasta gamma di opzioni che renderanno lo sviluppo molto più flessibile.

Il primo e più ovvio vantaggio del passaggio di 32 bit e la profondità del codice, naturalmente. Si intuisce facilmente che maggiore è il numero di bit che devono comporre il dato, minore sarà il numero di istruzioni che sarà necessario fornire il che implica una densità del codice molto maggiore.
Il primo effetto che si ottiene è quello di un significativo abbassamento della quantità di memoria richiesta, per l'appunto massimizzando anche la dotazione di memoria Flash presente che rimane disponibile per altri scopi.
Ad ogni modo, in merito a questo argomento, c'è da fare una precisazione: è convinzione comune che i microcontrollore ad 8 bit utilizzino rigidamente istruzioni ad 8 bit mentre che i microcontrollori basati sul processori ARM Cortex-M utilizzino istruzioni a 32 bit. In verità, però, microcontrollori come PIC18 e PIC16 utilizzano istruzioni che sono, rispettivamente, da 16 e 14 bit.
L'architettura 8051, anche se alcune istruzioni hanno lunghezza pari ad 1 byte, contano diverse altre che sono lunghe due o tre volte tanto. Lo stesso tipo di considerazioni si applicano anche ad architetture a 16 bit in cui alcune istruzioni possono richiedere 6 byte o addirittura di più.
Tutto questo va detto perché l'ARM Cortex-M3 ed M0 utilizzano, come abbiamo avuto modo di precisare, la tecnologia ARM Thumb®-2 che ha proprio questo scopo: aumentare la densità del codice. Grazie a cio, questo tipo di processori supportano le istruzioni di base della versione Thumb precedente, che sono a 16 bit, ed altre e più performanti istruzioni a 32.
In tanti casi un compilatore C# utilizzerà proprio le istruzioni a 16 bit in luogo di quelle a 32, ovviamente solo quando possibile.

La scelta dell'utilizzo di un'istruzione piuttosto che di un'altra suggerisce che ci sia la possibilità di rendere molto efficiente sia la programmazione sia la compilazione e questo si accompagna con il fatto che alcune istruzioni per i processori Cortex-M sono più efficienti. Ci sono casi, infatti, in cui una singola istruzione Thumb equivale a diverse istruzioni per un microcontrollore ad 8 o 16-bit. Questo vuol dire che un altro vantaggio dei dispositivi Cortex-M è che con codici più piccoli è possibile completare lo stesso task avendo una richiesta di velocità su bus più bassa.

Tanto per chiarirci le idee, diamo un'occhiata a questa tabella all'interno della quale viene fatta una comparazione tra operazioni a 16 bit in varie architetture:

8-bit example

16-bit example

ARM

Cortex-M

MOV A, XL ; 2 bytes

MOV B, YL ; 3 bytes

MUL AB; 1 byte

MOV R0, A; 1 byte

MOV R1, B; 3 bytes

MOV A, XL ; 2 bytes

MOV B, YH ; 3 bytes

MUL AB; 1 byte

ADD A, R1; 1 byte

MOV R1, A; 1 byte

MOV A, B ; 2 bytes

ADDC A, #0 ; 2 bytes

MOV R2, A; 1 byte

MOV A, XH ; 2 bytes

MOV B, YL ; 3 bytes

MUL AB; 1 byte

ADD A, R1; 1 byte

MOV R1, A; 1 byte

MOV A, B ; 2 bytes

ADDC A, R2 ; 1 bytes

MOV R2, A; 1 byte

MOV A, XH ; 2 bytes

MOV B, YH ; 3 bytes

MUL AB; 1 byte

ADD A, R2; 1 byte

MOV R2, A; 1 byte

MOV A, B ; 2 bytes

ADDC A, #0 ; 2 bytes

MOV R3, A; 1 byte

MOV R4,&0130h

MOV R5,&0138h

MOV SumLo,R6

MOV SumHi,R7

(Operands are moved to and from a memory mapped hardware multiply unit)

MULS r0,r1,r0

altra cosa che è veramente importante notare è che i processori Cortex-M  supportano il trasferimento dei dati sia per gli 8 sia per i 16-bit, rendendo così efficiente l'utilizzo della memoria. Questo vuol dire che i programmatori possono continuare ad utilizzare lo stesso tipo di dati e che questi verranno effettivamente correttamente indirizzati.

I vantaggi dal punto di vista energetico sono notevoli anche perché la richiesta di prodotti che consumino di meno, che siano alimentati a batteria e che nel contempo siano anche più performanti cresce costantemente. Basti pensare alla gestione di interfacce quali USB, Bluetooth, IEEE 802.15 e tante altre.
Tutto questo senza nominare i sensori coinvolti nella gestione anche di funzioni piuttosto semplici come la rotazione dello schermo, quindi i vari accelerometri, sensori capacitivi di contatto e così via dicendo. Tutte queste esigenze, anche molto diverse tra di loro, hanno portato al bisogno di integrare funzioni di gestione delle informazioni digitali e di pre-processing.

La maggior parte dei dispositivi ad 8 bit non offre le performance adatte a questi compiti e questo per il numero di operazioni che il processore è in grado di svolgere in un'unità di tempo elementare, naturalmente stiamo parlando di secondi e frazioni. 16 MHz cominciano a diventare molto pochi e pertanto agli sviluppatori è richiesto di cercare delle alternative. Sistemi che abbiano un duty-cycle più lungo (fase attiva) oppure frequenze di clock maggiori per riuscire ad equiparare le prestazioni di un dispositivo a 32 bit.

Tutto sommato, questi sistemi richiedono un certo sforzo di apprendimento ma lo sviluppo di applicazioni per microcontrollori basati su processori ARM Cortex può essere molto più semplice di quanto è stato fatto per il microcontrollore ad 8 bit, anche se sembra un paradosso, non solo è possibile lavorare con questi processori in C ma ci sono anche una serie di features dedicate al debug che possono rivelarsi molto utili nella risoluzione dei problemi del software.
Tutto questo senza considerare minimamente il supporto della comunità ed il numero di sviluppatori che lavora su questi sistemi. Freescale, in particolare, con la sua grande esperienza e la sua comunità così attiva fornisce un ottimo supporto praticamente per tutti i livelli di competenza.

Qualche dettaglio in più

In questa sezione vedremo di approfondire quanto detto finora allo scopo di chiarire al meglio le differenze cui abbiamo accennato.
L'ARM7, come abbiamo avuto modo di dire tante volte, è un'architettura a 32 bit in cui i Data pathes e le istruzioni sono parole da 32 bit.
Si tratta di un'architettura in cui sia le istruzioni sia i dati utilizzano bus per la comunicazione anch'essi da 32 bit. Come abbiamo detto non sono gli unici tipi di istruzione dal momento che c'è un'ottimizzazione basata su istruzioni a 16 bit rivenienti da Thumb.
Abbiamo parlato di ottimizzazione della densità ma non abbiamo dato una dimensione di quanto questo si senta; stiamo parlando più o meno del 30% della dimensione dell'intero codice.
Per quanto riguarda le istruzioni Thumb, dopo il fetching di un'istruzione da 16 bit dalla memoria, esse vengono decompresse fino ad "occupare" 32 bit prima di essere decodificate ed eseguite (ovvero prima delle fasi di decode ed execute). Questo significa che, di fatto, tutte le operazioni sono a 32 bit.

Per quanto riguarda i dati, essi possono essere ordinati secondo il criterio little endian oppure big endian e vengono definite le word, tipicamente profonde 4 byte ovvero 32 bit, le halfword, da 16 bit, ed i byte, che non hanno bisogno di presentazioni.

Esistono, per il processore, sette distinte modalità operative ovvero:

  • user: modalità senza privilegi molto comune per l'esecuzione della maggior parte dei task;
  • FIQ: vi si entra quando avviene un interrupt ad alta priorità;
  • IRQ: gestione ordinaria degli interrupt;
  • Supervisor: modalità protetta per il sistema operativo;
  • System: modalità con privilegi che utilizza gli stessi registri della modalità "user";
  • Abort: utilizzata per gestire le violazioni di accesso alla memoria;
  • Undefined: utilizzata per gestire istruzioni non definite.

I registri sono in tutto 37 e si differenziano in quelli general purpose, 31 in tutto di cui uno è il Program Counter (PC) e gli altri restano generici, e quelli di stato. Di questi ultimi, uno identifica lo stato del programma attualmente in corso mentre cinque lavorano su programmi salvati.
L'accesso a tutti questi registri non è possibile contemporaneamente ma saranno lo stato e la modalità operativa del processore a determinare a quali registi si potrà accedere volta per volta.
Sempre parlando di registri, a seconda della modalità del processo uno di questi banchi sarà sicuramente accessibile: il PC (r15), lo Stack Pointer (r13), il registro Subroutine link (r14), i registri che vanno da r0 ad r7 e da r8 ad r12 ed infine il Current Program Status Register.
Di tutti i registri, quelli da r8 ad r15 non sono parte di Thumb e pertanto il programmatore dispone di un accesso limitato che abilita soltanto di memorizzazione temporanea molto rapide.

I registri iniziano con alcuni "Condition Code Flags":

  • N: negativo oppure "minore di";
  • Z: zero;
  • C: riporto (oppure risultato di un'operazione di shift);
  • V: overflow.

I bit successivi sono riservati mentre l'ultimo gruppo di otto e fatto da bit di controllo; essi possono essere:

  • I: IRQ interrupt disable;
  • F: FIQ interrupt disable;
  • T bit: Thumb mode (se attivo) oppure ARM mode (se non inizializzato).

Le modalità sono, nell'ordine in cui sono state numerate prima, le seguenti: 10000 (user), 10001 (FIQ), 10010 (IRQ), 10011 (Supervisor), 10111 (Abort), 11011 (Undefined) e 11111 (System).

Parliamo del Program Counter

Come abbiamo detto, esistono l'ARM state ed il Thumb state ed, a seconda di quale il processore sta eseguendo, esistono alcune differenze; nel primo caso, tutte le istruzioni sono a 32 bit e devono essere word aligned. I bit [31:2] contengono il PC mentre quelli [1:0] sono nulli.
Nel caso del Thumb state, tutte le istruzioni sono da 16 bit e devono essere halfword aligned. In questo caso soltanto il bit [0] è nullo.

Esistono delle eccezioni a quello che abbiamo appena scritto ed avvengono ogni qual volta il normale flusso di esecuzione di un programma venga sospeso temporaneamente per esempio da un servizio che riguardi una periferica. Prima di gestire l'eccezione viene conservato lo stato corrente in maniera tale che il programma originario possa essere ripreso quando la routine di interruzione sia terminata.

Le eccezioni sono descritte anche in funzione della loro priorità in maniera tale che se due eccezioni si verificano contemporaneamente è sempre possibile gestirle. La più alta in ordine di importanza per l'operazione di RESET mentre la meno importante è il SWI, cioè il Software Input.

Le istruzioni

Vediamo di entrare nel merito delle istruzioni e di dare ulteriori informazioni al riguardo. Innanzitutto possiamo dire che molte di esse vengono eseguite in un singolo ciclo e sono soggette, sempre, a condizioni. Come abbiamo visto prima, infatti, un'eccezione può interrompere l'esecuzione di un'istruzione.
ARM è un'architettura di load/store che utilizza un set di istruzioni ridotto, descritto dall'acronimo RISC, tramite i registri.
Operazioni di caricamento oppure di memorizzazione su più registri in una singola istruzione possono comunque essere realizzate.

Le istruzioni possibili sono:

  • EQ: equal;
  • NE: not equal;
  • CS/HS: carry set/unsigned higher or same
  • CC/LO: carry clear/unsigned lower;
  • MI: negativo;
  • PL: positivo o nullo;
  • VS: overflow;
  • VC: non overflow;
  • HI: unsigned higher;
  • LS: unsigned lower or same;
  • GE: confronto tra segnali (superiore o uguale);
  • LT: confronto tra segnali (inferiore);
  • GT: confronto tra segnali (superiore);
  • LE: confronto tra segnali (inferiore oppure uguale);
  • AL: sempre.

Vediamo alcuni esempi di istruzione, cominciando con una alcune operazioni sui dati:

SUB -> r0, r1, #5 -> r0:=r1-5
ADD -> r2, r3, r3, LSL #2 -> r2:=r3+(r3,LSL #2)
ADDS -> r4, r4, #0x20 -> r4:=r4+32 (ed imposto il flag)
ADDED -> r5, r5, r6 -> r5:=r5+r6 (se uguale)

se dobbiamo lavorare sulla memoria, invece, tipicamente eseguiremo:

LDR -> r0, [r1,#4] -> r0:=[r1+4]
STRNEB r2, [r3,r4] -> [r3,r4]:=r2
LDRSH r5, [r6,#2] -> r5:=[r6+2]

Nel set di istruzioni Thumb, invece, abbiamo diversi tipi di istruzione divisi in due categorie: branch e data processing.
Nel primo caso esistono istruzioni non condizionali (che occupano circa 2 kB), condizionali (da 256 byte), branch con link (da 4 MB, due istruzioni), branch ed exchange (che cambia verso l'ARM state) ed infine gli ultimi due casi contemporaneamente per cui si ottiene un'istruzione di branch ed exchange with link.

Per quanto riguarda il data processing, stiamo parlando di un subset di istruzioni provenienti dall'ARM mode e che non vengono eseguite con un condizioni ma si limitano ad aggiornare dei flag.
I tipi di istruzione prevedono anche operazioni di load e store, anche multiple (load/store su una list of registers oppure push/pop).

Breakpoint e SWI sono le istruzioni che generano eventuali eccezioni.

Quello che abbiamo detto riguardava l'architettura ARM7. È il momento, quindi, di dare uno sguardo alle differenze con l'ARMv6-M. Vediamo, quindi, alcune delle istruzioni per Cortex-M0+ che riportano anche il numero di cicli coinvolti.

Operation

Description

Assembler

Cycles

Move

8-bit immediate

MOVS Rd, #<imm>

1

Lo to Lo

MOVS Rd, Rm

1

Any to Any

MOV Rd, Rm

1

Any to PC

MOV PC, Rm

2

Add

3-bit immediate

ADDS Rd, Rn, #<imm>

1

All registers Lo

ADDS Rd, Rn, Rm

1

Any to Any

ADD Rd, Rd, Rm

1

Any to PC

ADD PC, PC, Rm

2

8-bit immediate

ADDS Rd, Rd, #<imm>

1

With carry

ADCS Rd, Rd, Rm

1

Immediate to SP

ADD SP, SP, #<imm>

1

Form address from SP

ADD Rd, SP, #<imm>

1

Form address from PC

ADR Rd, <label>

1

Subtract

Lo and Lo

SUBS Rd, Rn, Rm

1

3-bit immediate

SUBS Rd, Rn, #<imm>

1

8-bit immediate

SUBS Rd, Rd, #<imm>

1

With carry

SBCS Rd, Rd, Rm

1

Immediate from SP

SUB SP, SP, #<imm>

1

Negate

RSBS Rd, Rn, #0

1

Multiply

Multiply

MULS Rd, Rm, Rd

1 or 32

Compare

Compare

CMP Rn, Rm

1

Negative

CMN Rn, Rm

1

Immediate

CMP Rn, #<imm>

1

Logical

AND

ANDS Rd, Rd, Rm

1

Exclusive OR

EORS Rd, Rd, Rm

1

OR

ORRS Rd, Rd, Rm

1

Bit clear

BICS Rd, Rd, Rm

1

Move NOT

MVNS Rd, Rm

1

AND test

TST Rn, Rm

1

Shift

Logical shift left by immediate

LSLS Rd, Rm, #<shift>

1

Logical shift left by register

LSLS Rd, Rd, Rs

1

Logical shift right by immediate

LSRS Rd, Rm, #<shift>

1

Logical shift right by register

LSRS Rd, Rd, Rs

1

Arithmetic shift right

ASRS Rd, Rm, #<shift>

1

Arithmetic shift right by register

ASRS Rd, Rd, Rs

1

Rotate

Rotate right by register

RORS Rd, Rd, Rs

1

Load

Word, immediate offset

LDR Rd, [Rn, #<imm>]

2 or 1

Halfword, immediate offset

LDRH Rd, [Rn, #<imm>]

2 or 1

Byte, immediate offset

LDRB Rd, [Rn, #<imm>]

2 or 1

Word, register offset

LDR Rd, [Rn, Rm]

2 or 1

Halfword, register offset

LDRH Rd, [Rn, Rm]

2 or 1

Signed halfword, register offset

LDRSH Rd, [Rn, Rm]

2 or 1

Byte, register offset

LDRB Rd, [Rn, Rm]

2 or 1

Signed byte, register offset

LDRSB Rd, [Rn, Rm]

2 or 1

PC-relative

LDR Rd, <label>

2 or 1

SP-relative

LDR Rd, [SP, #<imm>]

2 or 1

Multiple, excluding base

LDM Rn!, {<loreglist>}

1+N

Multiple, including base

LDM Rn, {<loreglist>}

1+N

Store

Word, immediate offset

STR Rd, [Rn, #<imm>]

2 or 1

Halfword, immediate offset

STRH Rd, [Rn, #<imm>]

2 or 1

Byte, immediate offset

STRB Rd, [Rn, #<imm>]

2 or 1

Word, register offset

STR Rd, [Rn, Rm]

2 or 1

Halfword, register offset

STRH Rd, [Rn, Rm]

2 or 1

Byte, register offset

STRB Rd, [Rn, Rm]

2 or 1

SP-relative

STR Rd, [SP, #<imm>]

2 or 1

Multiple

STM Rn!, {<loreglist>}

1+N

Push

Push

PUSH {<loreglist>}

1+N

Push with link register

PUSH {<loreglist>, LR}

1+N

Pop

Pop

POP {<loreglist>}

1+N

Pop and return

POP {<loreglist>, PC}

3+N

Se per il prodotto il numero di cicli dipende dall'implementazione dei dati in gioco, il numero di cicli varia in maniera considerevole per le operazioni di caricamento, memorizzazione, push e pop dal momento che N rappresenta il numero di elementi coinvolti.

Naturalmente esistono molte altre istruzioni che vengono utilizzate ma lo scopo di questa tabella era riportare alcune delle operazioni fondamentali.

Continuiamo il confronto parlando della gestione delle eccezioni che, come abbiamo già visto, rappresenta un punto cardine. Il processore implementa una gestione delle eccezioni abbastanza avanzata soprattutto per quanto riguarda eventuali interrupt. Scopo fondamentale è quello di minimizzare tempi di latenza; a tale scopo il processore abbandona qualsiasi istruzione che preveda caricamenti o memorizzazione multiple in favore di istruzioni di interrupt pendenti. Una volta che questa operazione sia stata svolta, il processo ricomincia ad eseguire le istruzioni che aveva lasciato.

L'implementazione del processore può assicurare che un numero prefissato di cicli sia sempre quello richiesto per rilevare segnali di interrupt e che il professore sia in grado di eseguire operazioni di fetch sulla stessa. Se questo succede, l'interrupt a priorità più alta è “jitter-free”. Approfondire questo genere di dizione esula dallo scopo di questo articolo; per questo vi consigliamo caldamente di consultare la documentazione ufficiale, che potete trovare qui.

Al fine di ridurre i tempi di latenza ma anche il jitter, i Cortex-M0+ implementa sia interrupt late-arrival sia tail-chaining. Questi meccanismi sono definiti dall'architettura ARMv6-M e rappresentano meccanismi efficaci di gestione. Il peggior caso di latenza è pari a 15 cicli.

Il modello delle eccezioni per il processore ha un comportamento “implementation-definied” che si aggiunge a quello dell'architettura e che prevede: eccezioni on stacking da HardFault a NMI lockup (priorità NMI) e eccezioni on unstacking da NMI ad HardFault Lockup (priorità HardFault).

Concludendo

Siamo in chiusura di questa lunga ed abbastanza complessa puntata del nostro corso. Riguardo i tipi di codice, la loro densità e le differenze sostanziali che esistono tra le versioni abbiamo avuto molto di farci un'idea abbastanza chiara di come si possa scrivere un programma facendo sempre particolare attenzione all'accesso ai registi opportuni. Naturalmente, la scelta della scheda e quindi del particolare processore renderà tutto questo un'utile guida di base ma sarà necessario studiare approfonditamente la documentazione relativa. Ed è proprio con questo spirito che, la prossima puntata, cercheremo di analizzare al meglio la scheda con la quale avremo a che fare nella terza parte del corso.
Per chi non ricordasse la scheda in questione è la FRDM-KL46Z e sarà in palio, come promesso nel nostro amatissimo Review4U 2.0.

Alla prossima.

 

                    

Per ulteriori informazioni potete scrivere a questa email: [email protected]

Scarica subito una copia gratis
Tags:

3 Commenti

  1. Avatar photo Bazinga 13 Ottobre 2013
  2. Avatar photo Piero Boccadoro 25 Ottobre 2013
  3. Avatar photo Mario Toma 13 Marzo 2015

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend