La presente guida mira a illustrare come gestire il lampeggio del LED presente sulla scheda del modulo Raspberry RP2040 interamente in assembly. Prima di illustrare e commentare il codice, di seguito gli step per la creazione del progetto in Visual Studio Code.
1 – Da “Raspberry Pico Pi Project” creare un nuovo progetto C/C++:

2 – Definire le impostazioni di base (Nome, Tipo scheda, Percorso, versione SDK) e lasciare le altre come proposto:

3 – A piacimento possiamo aggiungere un nuovo file per il codice assembly oppure rinominare il file “LAD_Lamp_ASM.c” cambiando l’estensione in “s” e quindi cancellando il relativo codice contenuto in esso. Per aggiungere un nuovo file:

4 – Nel file “CMakeLists.txt” è necessario modificare la riga “add_executable(LED_Lamp_ASM LED_Lamp_ASM.c )” ovviamente in “add_executable(LED_Lamp_ASM LED_Lamp_ASM.s )”.
A questo punto non ci resta che inserire il codice assembly nel file “LED_Lamp_ASM.s”:
.thumb_func
.global main
main:
@ carico R0 con l'indirizzo 0x400140cc
MOV R0, #0x40 @ R0 contiene 0x00000040
LSL R0, R0, #12 @ shift a sinistra di 12 posizioni, R0 contiene 0x00040000
ADD R0, #0x14 @ R0 contiene 0x00040014
LSL R0, R0, #12 @ shift a sinistra di 12 posizioni, R0 contiene 0x40014000
ADD R0, #0xcc @ R0 contiene 0x400140cc
@ carico R1 con il valore 5 per la funzione SIO
MOV R1, #0x05 @ R1 contiene 0x00000005
@ imposto il registro GPIO25_CTRL
STR R1, [R0]
@ carico R0 con l'indirizzo 0xd0000000
MOV R0, #0xd0 @ R0 contiene 0x000000d0
LSL R0, R0, #24 @ shift a sinistra di 24 posizioni, R0 contiene 0xd0000000
@ carico R1 con un valore tale da avere il bit a 1 nella posizione 25 per la modalità output del pin
MOV R1, #0x01 @ R1 contiene 0x00000001
LSL R1, R1, #25 @ shift a sinistra di 25 posizioni, R1 contiene 0x02000000 b00000010000000000000000000000000
@ imposto il registro GPIO_OE_SET
STR R1, [R0, #0x24] @ con offset rispetto all'indirizzo in R0
loop:
STR R1, [R0, #0x14] @ imposto il pin 25 alto
BL delay_500ms_start @ richiama il codice per il ritardo
STR R1, [R0, #0x18] @ imposto il pin 25 basso
BL delay_500ms_start @ richiama il codice per il ritardo
B loop @ ripete il ciclo
delay_500ms_start:
@ carico R7 con il valore 0x00fdad66 per il ritardo di 500ms
MOV R7, #0xfd @ R7 contiene 0x000000fd
LSL R7, R7, #8 @ shift a sinistra di 8 posizioni, R7 contiene 0x0000fd00
ADD R7, #0xad @ R7 contiene 0x0000fdad
LSL R7, R7, #8 @ shift a sinistra di 8 posizioni, R7 contiene 0x00fdad00
ADD R7, #0x66 @ R7 contiene 0x00fdad66
delay_500ms_ciclo:
SUB R7, R7, #1 @ sottrae 1 da R7
CMP R7, #0 @ compara il contenuto di R7 con zero e aggiorna il flag Z
BNE delay_500ms_ciclo @ ripete il ciclo se il flag Z non è 1
BX LR @ ritorna al punto di chiamata
Di seguito il commento del codice assembly. Molti aspetti sono trascurati, come il significato di ogni istruzione e la struttura del microcontrollore. Per questi fare riferimento sul sito ufficiale di ARM (cercando “The Cortex M0 Instruction Set”) e di Raspberry (cercando “RP2040 datasheet”).
Per poter utilizzare i pin come semplici input/output, è necessario impostarli sulla funzione SIO (single-cycle IO). Il codice numerico di tale funzione è 5. Nell’esempio è utilizzato il pin 25, quindi è necessario impostare con il valore 5 il campo “FUNCSEL” del registro di controllo di tale pin “GPIO25_CTRL”. L’indirizzo di tale registro è 0x400140cc. Nel codice, dalla riga 6 alla riga 10, viene memorizzato nel registro R0 della CPU tale indirizzo. Successivamente, alla riga 12 viene memorizzato nel registro R1 il valore 5 (la posizione a destra coincide con la posizione del campo “FUNCSEL”, quindi non è necessario traslarlo). Infine, alla riga 14 avviene il settaggio del registro di controllo.
La successiva operazione è quella di specificare la modalità di funzionamento del pin, ovvero come output. Il registro interessato è “GPIO_OE” dove ogni bit corrisponde ad un pin e se 0 indica input mentre se 1 indica output. Per semplicità è utilizzato “GPIO_OE_SET” in quanto permette di modificare un singolo bit attraverso una “maschera” eseguita in OR logico con il registro stesso. Nel codice, dalla riga 17 alla riga 18, viene memorizzato nel registro R0 della CPU l’indirizzo base 0xd0000000. Successivamente, dalla riga 20 alla riga 21, viene memorizzato il valore 0x02000000 nel registro R1. Tale valore, se convertito in binario corrisponde ad una serie di zeri ad eccezione della posizione 25 dove è presente 1. Infine, alla riga 23 avviene il settaggio del registro “GPIO_OE_SET” (che si trova con un offset di 0x24 rispetto al registro di base contenuto in R0).
Dalla riga 26 alla riga 30 abbiamo le istruzioni per il lampeggio del LED a ciclo infinito. Si dovrebbe intervenire sul registro “GPIO_OUT” per settare l’output del relativo pin su alto o basso, ma analogamente a quanto già fatto con la “maschera” sul registro “GPIO_OE_SET” facciamo la stessa cosa sul registro “GPIO_OUT_SET” per impostare il livello alto e sul registro “GPIO_OUT_CLR” per impostare il livello basso. Con la riga 26 utilizziamo la “maschera” nel registro R1 sul registro “GPIO_OUT_SET” (che si trova con un offset di 0x14 rispetto al registro di base contenuto in R0); con la riga 28 utilizziamo la “maschera” nel registro R1 sul registro “GPIO_OUT_CLR” (che si trova con un offset di 0x18 rispetto al registro di base contenuto in R0); con le righe 27 e 29 richiamiamo il codice relativo all’attesa di 500 ms.
Per quanto riguarda l’attesa di 500 ms bisogna considerare un po’ di cose. Innanzitutto è necessario conoscere la velocità di esecuzione del codice da parte del microcontrollore. Dal datasheet si evince che l’RP2040 esegue un’istruzione ad ogni ciclo di clock (la gran parte, alcune richiedono più di un ciclo di clock). E’ necessario sapere la frequenza del clock e dai file .h dell’SDK si evince che viene impostato a 125MHz. Quindi l’RP2040 è in grado di eseguire 125.000.000 istruzioni al secondo (sempre la gran parte). Nel nostro caso, per l’attesa di 500 ms, dobbiamo cercare il modo di far fare al microcontrollore 66.500.000 istruzioni da 1 ciclo. L’idea è quella di caricare in un registro un valore e poi decrementarlo fino a zero di modo tale da “impegnare” la CPU per il tempo che vogliamo. Dalla riga 34 alla riga 38 viene caricato nel registro R7 il valore 0x00fdad66 che non corrisponde a 66.500.000 bensì a 16.624.998. Questo perché il decremento e il successivo controllo richiedono 4 cicli di clock (1 per SUB, 1 per CMP e 2 per BNE). Quindi 16.624.998 decrementi per 4 cicli equivale a 66.499.992 più 5 clock per il settaggio di R7 più 3 clock per l’istruzione di ritorno BX fa in totale 66.500.000.
Di seguito lo stesso codice ottimizzato:
.thumb_func
.global main
main:
LDR R0, pin25ctrl
MOV R1, #0x05
STR R1, [R0] @ settaggio del registro GPIO25_CTRL
LDR R0, siobase
MOV R1, #0x01
LSL R1, R1, #25
STR R1, [R0, #0x24] @ settaggio del registro GPIO_OE_SET
loop:
STR R1, [R0, #0x14] @ settaggio del registro GPIO_OUT_SET
BL delay_500ms_start
STR R1, [R0, #0x18] @ settaggio del registro GPIO_OUT_CLR
BL delay_500ms_start
B loop
delay_500ms_start:
LDR R7, valore500ms
delay_500ms_ciclo:
SUB R7, R7, #1
CMP R7, #0
BNE delay_500ms_ciclo
BX LR
.align 4
pin25ctrl: .word 0x400140cc
siobase: .word 0xd0000000
valore500ms: .word 0x00fdad66