Raspberry RP2040: lampeggio LED in assembly con Visual Studio Code

Raspberry RP2040

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