Da ARM7 a Cortex-M3

Il processore ARM Cortex-M3 a 32 bit è una soluzione basata su ARMv7-M in grado di garantire elevate prestazioni insieme con un basso costo ed un alto rendimento. Il modello M3 è particolarmente utilizzato nel campo automotive dove sono richiesti elevati criteri di affidabilità.

La famiglia dei processori ARM sono soluzioni basate su tecnologia System-on-Chip (SoC) in grado di soddisfare le diverse esigenze del mercato: dalle applicazioni di home networking alle tecnologie wireless, dalle applicazioni automotive al segmento consumer. In modo particolare la famiglia Cortex-M3 di ARM è una soluzione in grado di fornire risposte concrete alle diverse esigenze tecnologiche. Come si è detto il Cortex-M3 si basa su ARMv7 ed è una proposta divisa in tre profili: la linea identificata con il profilo A risulta particolarmente in-dicata per le soluzioni che richiedono l’uso di applicazioni e sistemi operativi particolarmente complessi, il profilo R è, al contrario, orientato alle applicazioni in cui si richiedono requisiti stringenti in fatto di real-time.  Infine, il profilo M è stato realizzato per le soluzioni che richiedono un uso ottimizzato delle risorse. La tabella 1 mostra le differenze tra il Cortex-M3 con ARM7TDMI-S.

Tabella 1 – Confronto tra le diverse soluzioni possibili

Tabella 1 – Confronto tra le diverse soluzioni possibili

Il processore  Cortex-M3 è la parte centrale del chip (figura 1 e 2).

Figura 1: Cortex-M3.

Figura 1: Cortex-M3.

 

Figura 2: Cortex-M3.

Figura 2: Cortex-M3.

Per consentire la gestione ottimale delle risorse esistono diversi moduli funzionali.  Il chip Cortex-M3 è fornito sotto licenza da ARM mentre gli altri moduli, dal modulo di memoria al controllore degli I/O, sono messi a punto dal costruttore di ogni singola applicazione allo scopo di ottenere un processore il più possibile ritagliato per ogni singola esigenza. Il Cortex-M3 rappresenta un’architettura complessa e di largo respiro. In questo articolo cercheremo di mettere in evidenza le diverse possibilità offerte dal core e vedremo quali considerazioni occorrono tenere presenti per portare un’applicazione scritta per un altro core in un ambiente Cortex-M3. La famiglia Cortex-M3 è fornita da ARM in diverse specifiche configurazioni. Ad esempio è possibile definire la quantità di interrupt esterni e/o il loro livello di priorità per ogni singola sorgente. Non solo, ARM fornisce differenti modelli di Cortex-M3 che differiscono in base alla presenza o meno di moduli hardware. Così è possibile utilizzare core Cortex-M3 con o senza un MPU o, in alternativa, con o senza ETM. È possibile anche utilizzare, a seconda del modello considerato, differenti interfacce di debug: da connessioni seriali a JTAG. Inoltre, ARM propone core con differenti tagli di memoria flash, da alcuni kbyte fino ad arrivare fino nell’ordine di megabyte. Oppure sono anche disponibili varianti del core in base alla velocità del clock. Per ogni modello il prezzo sicuramente è differente. Al momento esistono differenti revisioni del Cortex-M3 e ognuno propone differenti prestazioni. Così, la revisione 1 propone una gestione più flessibile dello stack permettendo di abilitare le eccezioni al fine di segnalare accessi non allineati in memoria: possibilità offerta mediante il bit STKALIGN nel registro NVIC (configuration control register). La revisione 2 del core, poi, permette ad esempio l’uso del registro AUXFAULT o Auxiliary Fault status register. Nella revisione 1 del core, il campo VECTPENDING nel registro NVIC (control e status register) può essere modificato dal valore del bit C_MASKINTS del registro NVIC (debug halting Control & Status Register). Infatti, il campo C_ MASKINTS influisce direttamente sul valore del VECTPENDING: un valore pari a 1 di C_MASKINTS può resettare, valore pari a 0, il corrispondente VECTPENDING. A volte può essere utile conoscere la revisione del core, magari per definire procedure di inizializzazione particolari. Questa possibilità è offerta ricorrendo al registro CPU ID Base presente nel NVIC, dalla lettura degli ultimi quattro bit si ottiene l’informazione desiderata (tabella 2).

Tabella 3 – CPU ID Base register (0xE000ED00)

Tabella 2 – CPU ID Base register (0xE000ED00)

Per utilizzare un processore è necessario disporre di un ambiente di sviluppo. La tabella 3 mostra alcuni ambienti disponibili per differenti soluzioni: da commerciali ad ambienti di lavoro open-source.

Tabella 2 – Sistemi di sviluppo che supportano Cortex-M3

Tabella 3 – Sistemi di sviluppo che supportano Cortex-M3

Cortex-m3

Il Cortex-M3 esegue codice compatibile Thumb-2 e le sue caratteristiche tecniche sono mostrate nella tabella 1, mentre la figura 3 mostra le differenze tra diversi Cortex.

Figura 3: differenze tra Cortex.

Figura 3: differenze tra Cortex.

 

Figura 4: Memory Map.

Figura 4: Memory Map.

Il Cortex-M3 garantisce l’uso di un NVIC o Nested Vectored Interrupt Controller, un MPU o Memory Protection Unit o un Debug Access Port, DAP. Inoltre,  il Cortex-M3 utilizza una mappa di memoria che definiremo statica al contrario degli altri core ARM. Grazie a questa staticità è possibile portare il software tra i diversi  core Cortex-M3. Lo spazio di indirizzamento è diviso in una serie di differenti sezioni come mostra la figura e la tabella 4.

Tabella 4 – Mappa di memoria

Tabella 4 – Mappa di memoria

Il Cortex-M3 è in grado ad accedere ad un singolo bit di dati attraverso un meccanismo chiamato bit-banding. In sostanza, la mappa di memoria prevede due porzioni da 1 MB di area chiamata bit-banding nella porzione SRAM e nello spazio di indirizzamento riservato alle periferiche, figura 5.

Figura 5: bit-banding.

Figura 5: bit-banding.

Le operazioni di load/store sugli indirizzi definiti in questo modo sono traslate, automaticamente, in operazioni su singoli bit. Queste operazioni sono atomiche, nel senso che non possono essere interrompibili da altre attività su bus. Il  Cortex-M3 for nisce un altro modulo funzionale davvero utile: il Nested Vectored Interrupt Controller (NVIC). Il modulo NVIC è in grado di supportare 240 interrupt esterni fino a 256 differenti livelli di priorità.  I livelli possono, inoltre, essere modificati in modo dinamico. Il core garantisce il  pieno supporto della gestione sui livelli e sul fronte. Lo stato del processore è automaticamente, e in maniera perfettamente trasparente, salvata sullo stack in relazione alla singola sorgente di interrupt e, al termine, ne garantisce il pieno recupero al termine della gestione anche se nidificata. Il modulo funzionale NVIC garantisce anche la concatenazione degli interrupt. La tabella dei vettori presente in Cortex-M3 è differente rispetto agli altri ARM. Al reset lo stack pointer e l’indirizzo del gestore del reset deve essere posizionato  alla locazione zero e 0X4 (l’indirizzo 0x contiene il valore del registro r13, lo stack pointer, mentre la locazione 0x4 contiene l’indirizzo di partenza del programma da eseguire,  il bit LSB deve essere messo a 1 per indicare Thumb). Il Memory Protection Unit (MPU) è invece un modulo opzionale per il Cortex-M3. Il modulo è responsabile della protezione delle zone di memoria attraverso l’uso di segregazione dove l’accesso è permesso solo a coloro che hanno gli stessi privilegi della sezione. Grazie all’MPU è possibile definire fino a otto differenti regioni divise, a loro volta, in sotto-regioni. Il  Debug Access Port (DAP) è un’altra prerogativa del core. Il DAP utilizza un’interfaccia AHB-AP per comunicare con altri processori e periferiche. Al momento esistono due differenti implementazioni: il  Serial W ire JTAG Debug Port (SWJ-DP) e il  Serial W ire Debug Port (SW-DP).  Il Cortex-M3 è in grado di implementare le due funzionalità. Come ogni processore ARM che si rispetti anche il Cortex-M3 offre diverse prerogative di debug: dalla possibilità di gestire breakpoint per consentire  il debug di codice presente in ROM o in flash alla definizione del watchpoint e della modalità di traccia per monitorare lo stato delle variabili. Inoltre, nel Cortex-M3 è presente un ITM (Instrumentation Trace Macrocell). Il DAP, Debug Access Port, è connesso con il sistema,  compreso il core, per mezzo di un bus a matrice in questo modo è possibile accedere alle differenti risorse presenti, ad esempio la lettura di una singola variabile, senza per questo dover fermare  il processore.

Da arm7  a cortex-m3

Una delle attività più interessanti è, a mio avviso, è il porting del codice scritto da, per esempio ARM7, al core Cortex-M3. Un’attività di questo tipo è divisa in diverse fasi. Una delle prime cose da fare è quella di definire e mettere a punto lo startup code della nostra applicazione. Tipicamente, la procedura di startup è un modulo software, lanciato al reset, in grado di inizializzare dispositivi particolari. Questa funzionalità è tipica di ogni applicazione o core utilizzato. Nella migliore delle ipotesi è sufficiente specificare l’entry point del nostro programma C (la funzione _main()) all’indirizzo di reset del processore all’associato reset handler della vector table. A questo punto, all’interno della funzione main, è possibile inserire tutte le gestioni dedicate. Questa possibilità però non è sempre permessa; infatti, può succedere di dover utilizzare dispostivi che richiedono inizializzazioni particolari, magari scritte in assembly, da fare prima della nostra applicazione. In questo caso, una volta attivato il modulo assembly al termine sarà necessario spostare il controllo dell’esecuzione  alla funzione main. Tipicamente l’hardware che deve essere inizializzato prima dell’attivazione della funzione main comprendo no dispositivi quali MPU. Per questa ragione occorre modificare la procedura di startup per includere o escludere moduli di questo tipo. Per il core Cortex-M3 è necessario definire una tabella dei vettori come mostrata nel listato 1.

#define STACK_TOP 0x20000800
#define NVIC_CCR ((volatile unsigned long *)(0xE000ED14))
// Declare functions
void mputs(char *string1);
void mputc(char mychar);
int main(void);
void nmi_handler(void);
void hardfault_handler(void);
// Define the vector table
__attribute__ ((section(“vectors”)))
void (* const VectorArray[])(void) _ {
STACK_TOP,
main,
handler_INMI,
handler_FHARD
};
// Start of main program
int main(void)
{
const char *helloworld[]_“Hello world\n”;
*NVIC_CCR _ *NVIC_CCR | 0x200; /* Set STKALIGN in NVIC */
mputs(*helloworld);
while(1);
return(0);
}
//Dummy handler: dummy
void handler_INMI(void)
{
return;
}
//Dummy handler: dummy
void handler_FHARD(void)
{
return;
}
Listato 1 – esempio

Il listato 1 mostra un possibile gestore delle eccezioni, in particolare in un core Cortex-M3 deve essere sempre presente. La nostra applicazione è particolarmente semplice tanto da richiedere un gestore semplice e compatto. La tabella dei vettori è definita attraverso  il codice __attribute__ e non è specificato dove risiede la tabella stessa (a che indirizzo è mappato), a questo riguardo si utilizza uno script utilizzato dal linker. Di solito in un programma C si utilizza     irq per creare un gestore di interrupt che lavora su di un ARM7. Tra Cortex-M3 e ARM7 esistono delle piccole differenze nella gestione di un interrupt, ad esempio su Cortex-M3 non è necessario fare il salvataggio dei registri all’indirizzo di ritor no. Per questa ragione la keyword __irq può anche essere rimossa, ma si preferisce utilizzarla per ragioni di comprensione del codice, così:

__irq void SysIntHandler(void)
{
printf(“——SysInthandler Interrupt ——-“);
}

La gestione delle eccezioni nel Cortex-M3 è estremamente flessibile. È il core che si occupa di gestire la priorità delle eccezioni, del loro annidamento e del congelamento del contesto. In questo modo la gestione è flessibile e si minimizza la latenza. Un interrupt rimane abilitato dal core ogni volta che entra in una routine di gestione dell’evento. Viceversa, se un interrupt è disabilitato al ritorno del gestore questo non sarà automaticamente abilitato dal processore, ma occorre che il software stesso si preoccupi di riabilitare l’eccezione. Questo particolare meccanismo di gestione dell’eccezione comporta dei sicuri impatti sul codice e sulle sezioni critiche. Successivamente è necessario ricompilare il codice C per la variante Thumb-2 settando opportunamente gli switch di compilazione. É preferibile utilizzare sistemi automatici in questo modo si riduce la possibilità di introdurre errori nel processo di compilazione. È necessario stare attenti qualora si utilizzino librerie particolari; infatti, è opportuno controllare direttamente per evitare di utilizzare istruzioni ARM incompatibili nell’immagine finale. A questo riguardo può risultare utile inserire nel codice alcuni flag per facilitare il porting del codice stesso. L’uso di direttive pragma (#pragma arm o #pragma thumb) in ambiente Cortex-M3 non sono più applicabili e quindi sono da rimuovere. Non solo, particolare attenzione va fatta nell’uso dell’inline assembly tali  da evitare di inserire codice non più compatibile. Infatti, il codice assembly deve essere particolarmente esaminato perché diventa reale la possibilità di inserire opcode non più supportate. Le eventuali direttive, ARM o CODE32, devono essere drasticamente rimosse o modificate nelle direttive THUMB. Sicuramente la maggior parte del codice assembly può essere utilizzato senza difficoltà anche se devono essere inserite le opzioni corrette durante l’operazione di generazione del codice binario. La presenza delle direttive CODE16, poi, sono da valutare attentamente e devono essere assemblate senza warning per portarli. Attenzione alle istruzioni condizionali assembler: utilizzare istruzioni IT. Così, l’istruzione ADDSNE r0,r0,r1 seguita da un’istruzione senza condizione deve diventare IT NE seguite da ADDS r0,r0,r1. Le attività di porting del codice devono anche prevedere la possibilità di sostituire le istruzioni specifiche del core, quali istruzioni del co-processore SWP. Il Cortex-M3 non ha, infatti, istruzioni SWP (istruzioni di swap) utilizzate in processori ARM, come ARM7TDIMI, per implementare operazioni semaforiche, ma, nel nostro contesto, dovranno essere sostituite da operazioni con accesso esclusivo come LDREX e STREX. Occorre fare particolare attenzione all’uso del registro PSR (program Status Register) e verificare che le operazioni svolte non generano situazioni indeterminate con il Cortex-M3. Altro aspetto fondamentale sono i modi di indirizzamento; infatti, esistono possibilità che non sono supportati dalle istruzioni LDM e STM. Inoltre, occorre verificare che le istruzioni utilizzate non hanno particolare restrizioni in Thumb-2, ad esempio con le istruzioni LDR e STR. Può anche succedere di dover riscrivere porzioni di codice allo scopo di rispettare il modello di programmazione del Cortex-M3. Infatti, qualsiasi operazione che, si ritiene, modifica lo stato e il modo di funzionamento deve essere rivisto al fine di rispettare il comportamento architetturale del Cortex-M3. Non solo, è necessario anche dedicare particolare cura nell’uso del PC; infatti, il Thumb-2 utilizza un sistema misto da 16 e 32 bit: il valore del Program Counter, PC, è sempre l’indirizzo dell’istruzione corrente maggiorato di 4 quando è utilizzato in un’operazione di trattamento dati. Tutti gli script file utilizzati per costruire l’applicazione dovranno essere risistemati per definire la nuova mappa di memoria prevista e per posizionare le strutture per il system Control Space. Qualora il nostro progetto richieda un timer allora è necessario gestire la funzionalità di SysSick fornito dal System Control Space. Il  listato 2 mostra un’ipotetica realizzazione del System Control Register. Terminata la fase di porting possiamo anche pensare di utilizzare le nuove prerogative del Cortex-M3. La maggior parte sono inserite in maniera automatica attraverso il  compilatore C che implementa le nuove features del core.

typedef volatile struct {
int MasterCtrl;
int IntCtrlType;
int zReserved008_00c[2];
/* Reserved space */
struct {
int Ctrl;
int Reload;
int Value;
int Calibration;
} SysTick;
int zReserved020_0fc[(0x100-0x20)/4];
/* Reserved space */
/* Offset 0x0100 */
struct {
int Enable[32];
int Disable[32];
int Set[32];
int Clear[32];
int Active[64];
int Priority[64];
} NVIC;
int zReserved0x500_0xcfc[(0xd00-0x500)/4];
/* Reserved space */
/* Offset 0x0d00 */
int CPUID;
int IRQcontrolState;
int ExceptionTableOffset;
int AIRC;
int SysCtrl;
int ConfigCtrl;
int SystemPriority[3];
int SystemHandlerCtrlAndState;
int ConfigurableFaultStatus;
int HardFaultStatus;
int DebugFaultStatus;
int MemManageAddress;
int BusFaultAddress;
int AuxFaultStatus;
int zReserved0xd40_0xd90[(0xd90-0xd40)/4];
/* Reserved space */
/* Offset 0x0d90 */
struct {
int Type;
int Ctrl;
int RegionNumber;
int RegionBaseAddr;
int RegionAttrSize;
} MPU;
Listato 2 – System Control Register

Una risposta

  1. Stefano Lovati Stefano Lovati 13 gennaio 2019

Scrivi un commento

EOS-Academy
Abbonati ora!