- Include RV32I: registri, ABI e controllo di flusso con ecall.
- Esercizi con Giove ed esercizi: negativo, fattori, catene, ricorsione.
- Master cross toolchain, copione collegamento e debug con objdump.

Se sei curioso di conoscere l'assembler e pensi che RISC-V sia la strada giusta, sei nel posto giusto. Iniziare con ASM su RISC-V è più conveniente di quanto sembri Se capisci gli strumenti, il modello di programmazione e alcune regole fondamentali dell'architettura.
Nelle righe seguenti ho combinato il meglio di diverse fonti: pratiche con simulatori di tipo Giove, convenzioni del repertorio di base RV32I, esempi di loop e ricorsione, chiamate di sistema e persino uno sguardo alla progettazione della CPU RISC-V in VHDL (con ALU, controllo della memoria e macchina a stati), oltre a una revisione degli script cross-toolchain e di collegamento.
Che cos'è l'assembler RISC-V e in che cosa differisce dal linguaggio macchina?
Sebbene entrambi siano collegati al hardware, Il linguaggio macchina è puro binario (uno e zero) che la CPU interpreta direttamente, mentre l'assembler utilizza mnemonici e Simboli più leggibile di quanto un assembler traduca in binario.
RISC-V definisce un ISA aperto con un repertorio di basi molto pulito. Il profilo RV32I (32 bit) include 39 istruzioni per l'utente con notevole ortogonalità, separando l'accesso alla memoria dal calcolo puro e con un eccellente supporto in GCC/LLVM.
Registri, accordi e punto di ingresso
In RV32I hai 32 registri di uso generale (x0–x31) 32 bit; x0 viene sempre letto come 0 e non è possibile scriverci sopra. Alias come a0–a7 (argomenti), t0–t6 (temporanei) o s0–s11 (salvati) sono utili anche per seguire l'ABI.
A seconda dell'ambiente o del simulatore, il programma può iniziare da un'etichetta specifica. In Jupiter, i programmi iniziano dal tag globale __start., che devi dichiarare come visibile (ad esempio, con .globl) per contrassegnare il punto di ingresso.
Le i tag terminano con due punti, puoi inserire solo un'istruzione per riga e i commenti possono iniziare con # o ;, quindi l'assembler li ignora.
Strumenti e simulatori: Jupiter e flusso di lavoro di base
Per praticare senza complicazioni, hai la Simulatore/assemblatore di Giove, uno strumento grafico ispirato a SPIM/MARS/VENUS che facilita la modifica, l'assemblaggio e l'esecuzione in un unico ambiente.
In Jupiter puoi creare, modificare ed eliminare file nella scheda Editor. Dopo aver salvato, assemblare con F3 ed eseguire per eseguire il debug del flusso istruzione per istruzione, utilizzando registri e viste di memoria per comprendere lo stato della macchina.
I programmi devono terminare con una chiamata all'ambiente: uscita chiamata impostazione a0 con codice 10 (uscita). In RISC-V, le ecall sono equivalenti alle chiamate di sistema o alle trappole per l'ambiente/sistema.
Struttura minima di un programma e chiamate di sistema
La struttura tipica negli esempi accademici definisce un punto di partenza, esegue il lavoro e termina con una chiamata elettronica. gli argomenti ecall di solito viaggiano in a0–a2 e il selettore di servizio in a7, a seconda dell'ambiente.
In uno Linux Ad esempio, con RISC-V è possibile stampare con la syscall write e uscire con il codice appropriato. Per scrivere a0 (fd), a1 (buffer), a2 (lunghezza) e a7 vengono utilizzati con il numero di servizioInfine, a0 viene impostato sul codice di ritorno e a7 sul numero di uscita.
# Ejemplo mínimo (Linux RISC-V) para escribir y salir
.global _start
_start:
addi a0, x0, 1 # fd = 1 (stdout)
la a1, msg # a1 = &msg
addi a2, x0, 12 # a2 = longitud
addi a7, x0, 64 # write
ecall
addi a0, x0, 0 # return code
addi a7, x0, 93 # exit
ecall
.data
msg: .ascii "Hola mundo\n"
Se lavori al di fuori di Linux, come ad esempio in simulatori didattici con il proprio servizio IoT, modificare il numero eCall e i registri in base alla documentazione dell'ambiente.
Esercizi iniziali per aiutarti a familiarizzare con i condizionali, i cicli e la memoria
Un tipico esercizio di riscaldamento consiste nel rilevare se un numero intero è negativo. È possibile restituire 0 se è positivo e 1 se è negativo.; con RV32I, un confronto con 0 e un set-on-less-than risolve il problema in una singola istruzione ben congegnata.
Un altro esercizio molto utile è quello di elencare i fattori di un numero: attraversa da 1 a n, stampa i divisori e restituisce quanti ce n'eranoEserciterai le diramazioni condizionali, la divisione (o la sottrazione ripetuta) e i cicli con addizione e confronto.
Lavorare con le stringhe ti obbliga a gestire la memoria: Visita ogni carattere di una stringa in memoria e converti sul posto le lettere minuscole in maiuscole se rientrano nell'intervallo ASCII. Al termine, restituisce l'indirizzo originale della stringa.
Cicli, funzioni e ricorsione: fattoriale, Fibonacci e Torre di Hanoi
Quando si progettano i loop, bisogna pensare a tre blocchi: condizione, corpo e passaggio. Con beq/bne/bge e salti incondizionati jal/j while/for vengono costruiti senza mistero, basandosi su aggiunte e confronti.
.text
.globl __start
__start:
li t0, 0 # i
li t1, 10 # max
cond:
bge t0, t1, end # si i >= max, salta
# cuerpo: usar a0/a1 segun servicio IO del entorno
addi t0, t0, 1 # i++
j cond
end:
li a0, 10
ecall
Nelle chiamate di funzione, rispettare l'ABI: risparmia se hai intenzione di concatenare più chiamate, conserva s0–s11 se li modifichi e usa lo stack con sp che si sposta in multipli di una parola.
Il fattoriale è la classica ricorsione: caso base n==0 restituisce 1; in caso contrario, chiama factorial(n-1) e moltiplica per n. Proteggi ra e i registri salvati sullo stack prima della chiamata e ripristinali al ritorno.
factorial:
beq a0, x0, base
addi sp, sp, -8
sw ra, 4(sp)
sw s0, 0(sp)
mv s0, a0
addi a0, a0, -1
jal factorial
mul a0, a0, s0
lw s0, 0(sp)
lw ra, 4(sp)
addi sp, sp, 8
jr ra
base:
li a0, 1
jr ra
Fibonacci è utile per praticare entrambi ricorsione con due chiamate Come una versione iterativa efficiente con variabili accumulatore. E se vuoi una sfida di controllo del flusso e dei parametri, traduci una soluzione in assembler. Torri di Hanoi con quattro argomenti: dischi, sorgente, destinazione e torre ausiliaria; rispetta l'ordine di chiamata e visualizza ogni movimento.
Accesso alla memoria, array e manipolazione delle stringhe
In RISC-V, l'accesso alla memoria avviene tramite load/store: lw/sw per le parole, lh/sh per le mezze parole e lb/sb per i byte, con varianti firmate o non firmate nelle accuse (lb vs lbu, lh vs lhu).
Per attraversare array di interi, utilizzare offset di 4 byte per indice; per le stringhe di testo, avanza byte per byte finché non trova il terminatore se la convenzione lo richiede (ad esempio, \0). Ricordarsi di salvare gli indirizzi di base e di gestire i puntatori con addi/auipc/la in modo appropriato.
Progettazione di una CPU RV32I da zero: una panoramica di alto livello
Se hai voglia di scendere fino al silicio, un progetto educativo costruisce un CPU RV32I in VHDL, sintetizzabile in FPGA Gamma medio-bassa. Include ROM di programma, RAM dati e un semplice GPIO per accendere un LED.
Il kernel implementa il repertorio di base (senza estensioni M/A/C o CSR), utilizza un bus di indirizzi a 32 bit e consente l'accesso alla memoria con segno esteso a 8/16/32 bit, ove appropriato. Il progetto separa chiaramente registri, ALU, controller di memoria e macchina a stati.
ALU, turni e l'idea del "carico ritardato"
L'ALU è descritta in modo combinatorio con operazioni come addizione, sottrazione, XOR, OR, AND, confronti (con segno e senza segno) e spostamenti logici/aritmetici.
Per salvare le LUT in FPGA, vengono implementati spostamenti multi-bit iterando spostamenti di 1 bit controllati dalla macchina a stati: si consumano diversi cicli, ma si riducono le risorse logiche.
Nei circuiti sincroni, le variazioni si osservano sui fronti del clock. Il concetto di "carico ritardato" ricorda che ciò che viene selezionato da un multiplexer impatta sul registro nel ciclo successivo, un aspetto chiave nella progettazione della macchina a stati fetch-decode-execute.
Controller e mappa della memoria: ROM, RAM e GPIO
Un blocco di memoria integra ROM e RAM in uno spazio contiguo, semplificando l'interfaccia del processoreIl controller riceve AddressIn (32 bit), DataIn, la larghezza (byte/metà/parola), il segnale di estensione del segno, WE (lettura/scrittura) e Start per avviare le transazioni.
Quando l'operazione è finita, ReadyOut è impostato su 1 e, se è stato letto, DataOut contiene i dati (con segno esteso quando richiesto). Se sono stati scritti, i dati rimangono nella RAM.
Un esempio di mappa pratica: ROM da 0x0000 a 0x0FFF, un byte GPIO a 0x1000 (bit 0 su un pin) e RAM da 0x1001 a 0x1FFFCon questo è possibile creare un lampeggiatore scrivendo e commutando il bit di output.
Registri, multiplexer e macchine a stati
La CPU definisce 32 registri di uso generale istanziati con array in VHDL, con un decodificatore per selezionare la destinazione di scrittura dall'ALU e mantenere il resto.
I multiplexer governano gli input ALU (operandi e funzionamento), i segnali al controller di memoria (larghezze, indirizzo, controllo di avvio e lettura/scrittura) e i registri speciali: PC, IR e un contatore ausiliario per spostamenti iterativi.
La macchina statale inizia con azzerare, recupera l'istruzione indicata dal PC (lettura a 4 byte), viene caricato in IR quando pronto e passa ai nodi di esecuzione: ALU (un'istruzione in 1 ciclo eccetto per gli spostamenti), caricamento/memorizzazione, diramazioni e salti, oltre a istruzioni speciali come ebreak.
Cross-toolchain, collegamento e debug
Per generare i binari RV32I, utilizzare un Cross GCC (target riscv32-none-elf)Si compilano sorgenti C/C++/ASM, si collega uno script che definisce la mappa di memoria e si converte l'output nel formato previsto dalla ROM/FPGA.
Un semplice script hook può posizionare .testo nella ROM da 0x0000 e .data nella RAM da 0x1004 (se 0x1000–0x1003 è occupato dai registri GPIO). La routine di avvio può essere "nuda" e posizionare il puntatore dello stack alla fine della RAM (ad esempio 0x1FFC) prima di chiamare main.
/* Mapa simple
* ROM: 0x00000000 - 0x00000FFF
* GPIO: 0x00001000 - 0x00001003
* RAM: 0x00001004 - 0x00001FFF
*/
SECTIONS {
. = 0x00000000;
.text : { *(.startup) *(.text) *(.text.*) *(.rodata*) }
. = 0x00001004;
.data : { *(.data) *(.data.*) }
}
Con riscv32-none-elf-objdump puoi smontare l'ELF e controllare gli indirizzi; ad esempio, vedrai il Boot a 0x00000000 con istruzioni come lui/addi/jal e la transizione al file principale. Per la simulazione VHDL, GHDL genera tracce che è possibile aprire con GtkWave.
Dopo la verifica nella simulazione, trasferire il progetto su un FPGA (Quartus o altra toolchain). Se la RAM viene dedotta come blocchi interni e il codice è chiaro RTL, dovresti sintetizzare senza sorprese, anche sui dispositivi più datati.
Promemoria pratici ed errori tipici all'inizio
Non dimenticarlo x0 è sempre zero; la scrittura non ha alcun effetto, mentre la lettura restituisce 0. Utilizza questo a tuo vantaggio per aggiunte, confronti e pulizie del registro.
Quando si implementano le funzionalità, salva i record ra e sN che modifichie gestisce lo stack con addizioni/sottrazioni allineate alle parole fino a sp. Al ritorno, ripristina in ordine inverso e salta con jr ra.
Nei simulatori come Jupiter, controlla che __start è globale e lo termini con ecall corretto (a0=10 per uscire). Se qualcosa non si avvia, controlla l'etichetta, la globalità e ricompila (F3).
Negli esercizi con IO, rispettare il protocollo dell'ambiente: quali registri trasportano i parametri, il numero di servizio e se è previsto un indirizzo o un valore immediato. Utilizzare il simulatore o la documentazione del sistema operativo.
Con una chiara base ISA (RV32I, registri e ABI), un simulatore comodo come Jupiter e un numero crescente di esempi (negativo, fattori, maiuscolo, cicli, fattoriale, Fibonacci e Hanoi), l'assembler RISC-V smette di essere un muro e diventa un terreno fertile per comprendere come ragiona una CPU. E se osate passare al VHDL, vedrete come ALU, memoria e controllo si integrano tra loro.: dal recupero delle istruzioni e dal caricamento differito alle interfacce di memoria e una mappa con ROM, RAM e GPIO che consente di far lampeggiare un LED con il proprio processore.
Scrittore appassionato del mondo dei byte e della tecnologia in generale. Adoro condividere le mie conoscenze attraverso la scrittura, ed è quello che farò in questo blog, mostrarti tutte le cose più interessanti su gadget, software, hardware, tendenze tecnologiche e altro ancora. Il mio obiettivo è aiutarti a navigare nel mondo digitale in modo semplice e divertente.
