Què necessites per començar a programar a assemblador RISC-V

  • Entorn llest: Júpiter per practicar ASM, riscv32-none-elf per compilar i GHDL+GtkWave per simular.
  • Domina bucles, condicionals i funcions amb ABI RISC-V i maneig de pila correcte.
  • ECALL segons entorn: Jupiter (codis simples) vs Linux (a0..a2 i a7 amb syscalls).
  • Dóna el salt: compila C/C++ a binari, genera ROM i executa en una CPU RV32I a FPGA.

assemblador RISC-V

Si us pica el cuquet del baix nivell i voleu aprendre a programar en assemblador sobre arquitectures modernes, RISC-V és una de les millors portes d'entrada. Aquesta ISA oberta, amb gran tracció a la indústria i l'acadèmia, permet practicar des de simuladors senzills fins a executar-lo en una FPGA, passant per toolchains complets per compilar C/C++ i examinar l'ASM generat.

En aquesta guia pràctica t'explico, de forma pas a pas i amb un enfocament molt terrenal, què necessites per començar a programar a assemblador RISC-V: les eines, el flux de treball, exemples clau (condicionals, bucles, funcions, trucades al sistema), exercicis típics de laboratori i, si t'animes, una ullada a com s'implementa una CPU RV32I i com executar el teu propi binari en un nucli sintetitzat a FPGA.

Què és l'assemblador RISC-V i com es relaciona amb el llenguatge màquina

RISC-V defineix una arquitectura de conjunt d'instruccions (ISA) oberta: el repertori base RV32I inclou 39 instruccions molt ortogonals i fàcils dimplementar. L'assemblador (ASM) és un llenguatge de baix nivell que utilitza mnemònics com add, sub, lw, sw, jal, etc., alineats amb aquesta ISA. El codi màquina, per sota, són els bits que entén la CPU; l'assemblador és la seva representació llegible, més propera al maquinari que qualsevol llenguatge d'alt nivell.

Si ja veniu de C, notareu que l'ASM no s'executa tal qual: cal acoblar-ho i enllaçar-ho per produir un binari. A canvi, et permet controlar registres, modes d'encaminament i trucades al sistema amb precisió quirúrgica. I si treballes amb un simulador docent, veuràs “ecall” com a mecanisme d'entrada/sortida i finalització, amb convencions específiques segons l'entorn (p. ex., Jupiter davant de Linux).

Eines i entorn: simuladors, toolchain i FPGA

Per començar ràpid, el simulador gràfic Jupiter és ideal. És un assemblador/simulador pensat per a docència, inspirat en SPIM/MARS/VENUS i usat en assignatures universitàries. Amb ell pots escriure, assemblar i executar programes RV32I sense configurar tot un toolchain des de zero.

Si vols anar un pas més enllà, t'interessa el toolchain bare-metall: riscv32-none-elf (GCC/LLVM) per compilar C/C++ a binaris RISC-V, i utilitats com a objdump per desassemblar. Per a simulació de maquinari, GHDL et permet compilar VHDL, executar i bolcar senyals en un fitxer .ghw per inspeccionar-los amb GtkWave. I, si t'animes a maquinari real, pots sintetitzar una CPU RV32I a una FPGA amb entorns del fabricant (p. ex., Quartus d'Intel) o toolchains lliures.

Primeres passes amb Jupiter: flux bàsic i normes de l'assemblador

Júpiter simplifica la corba d'aprenentatge. Crees i edites fitxers a la pestanya Editor, i tot programa comença a l'etiqueta global __start. Assegureu-vos de declarar-la amb directiva .globl (sí, és .globl, no .global). Les etiquetes acaben amb dos punts i els comentaris poden començar amb # o ;.

Un parell de regles útils de lentorn: una sola instrucció per línia, i quan tinguis llest el codi, guarda i prem F3 per acoblar i poder executar-lo. Els programes han de finalitzar amb una trucada ecall de sortida; a Jupiter, posar 10 en a0 assenyala la fi del programa, de manera anàloga a un “èxit”.

Minimalment, el teu esquelet ASM a Jupiter es podria veure així, amb el punt d'entrada clar i la finalització per ecall: és la base de la resta d'exercicis.

.text
.globl __start
__start:
  li a0, 10     # código 10: terminar
  ecall         # finalizar programa

Conveni de trucades (ABI) i maneig de la pila

Programar funcions en assemblador requereix respectar el conveni: els arguments solen arribar a a0..a7, el resultat sol tornar-se en a0, i les trucades han de preservar adreces de retorn (ra) i registres salvats (s0..s11). Per això, el stack (sp) és el teu aliat: reserva espai en entrar i restaura en sortir.

Algunes instruccions que utilitzaràs tota l'estona: li i la per carregar immediats i adreces, add/addi per a sumes, lw/sw per a accés a memòria, salts incondicionals j/jal i retorns jr ra, a més de condicionals com beq/bne/bge. Aquí tens un recordatori ràpid amb exemples típics:

# cargar inmediato y una dirección
li t1, 5
la t1, foo

# aritmética y actualización de puntero de pila
add t3, t1, t2
addi sp, sp, -8   # reservar 8 bytes en stack
sw ra, 4(sp)      # salvar ra
sw s0, 0(sp)      # salvar s0

# acceso a memoria con base+desplazamiento
lw t1, 8(sp)
sw a0, 8(sp)

# saltos y comparaciones
beq t1, t2, etiqueta
j etiqueta
jal funcion
jr ra

Un bucle clàssic a RISC-V pot estructurar-se amb claredat, separant condició, cos i step. A Jupiter, a més, pots imprimir valors amb ecall segons el codi que carreguis a a0:

.text
.globl __start
__start:
  li t0, 0      # i
  li t1, 10     # max
cond:
  bge t0, t1, endLoop
body:
  mv a1, t0     # pasar i en a1
  li a0, 1      # código ecall para imprimir entero
  ecall
step:
  addi t0, t0, 1
  j cond
endLoop:
  li a0, 10     # código ecall para salir
  ecall

Per a funcions recursives, cuida el segó/restaurat de registres i ra. Factorial és l'exemple canònic que t'obliga a pensar en l'stack frame ia tornar el control a la direcció correcta:

.text
.globl __start
__start:
  li a0, 5          # factorial(5)
  jal factorial
  # ... aquí podrías imprimir a0 ...
  li a0, 10
  ecall

factorial:
  # a0 trae n; ra tiene la dirección de retorno; sp apunta a tope de pila
  bne a0, x0, notZero
  li a0, 1          # factorial(0) = 1
  jr ra
notZero:
  addi sp, sp, -8
  sw s0, 0(sp)
  sw ra, 4(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

Entrades/sortides amb ecall: diferències entre Jupiter i Linux

La instrucció ecall serveix per invocar serveis de lentorn. A Jupiter, codis senzills en a0 (p. ex., 1 imprimir sencer, 4 imprimir cadena, 10 sortir) controlen les operacions disponibles. A Linux, en canvi, a0..a2 solen contenir paràmetres, a7 el nombre de syscall, i la semàntica correspon a les trucades del kernel (write, exit, etc.).

Aquest “Hola món” per a Linux il·lustra el patró: prepares els registres a0..a2 i a7 i llances ecall. Fixa't a la directiva .global i al punt d'entrada _start:

# a0-a2: argumentos; a7: número de syscall
.global _start
_start:
  addi a0, x0, 1     # 1 = stdout
  la a1, holamundo   # puntero al mensaje
  addi a2, x0, 11    # longitud
  addi a7, x0, 64    # write
  ecall
  addi a0, x0, 0     # return code 0
  addi a7, x0, 93    # exit
  ecall
.data
holamundo: .ascii "Hola mundo\n"

Si el teu objectiu és practicar lògica de control, memòria i funcions, Jupiter et dóna feedback instantani i, a més, molts laboratoris inclouen autograder per validar la solució. Si vols practicar interacció amb el sistema real, compilaràs per a Linux i utilitzaràs les syscalls del nucli.

Exercicis d'arrencada: condicionals, cicles i funcions

Un conjunt clàssic d'exercicis per començar a RISC-V ASM cobreix tres pilars: condicionals, bucles i trucades a funció, amb focus en el maneig correcte de registres i la pila:

  • Negatiu: funció que torna 0 si el nombre és positiu i 1 si és negatiu. Rep l'argument a a0 i torna a a0, sense destruir registres no volàtils.
  • Factor: recórrer els divisors d'un nombre, imprimint-los durant l'execució i retornant-ne la quantitat total. Practicaràs cicles, divisió/mod i trucades a ecall per imprimir.
  • Upper: donat el punter a un string, recórrer-lo i convertir minúscules a majúscules in-place. Tornar la mateixa adreça; si moveu el punter durant el bucle, restaureu-lo abans de retornar.

Per a tots tres, respecta el conveni de pas de paràmetres i retorn, i acaba el programa amb ecall de sortida quan ho provis a Jupiter. Amb aquests exercicis cobreixes flux de control, memòria i funcions amb “estat”.

Aprofundint: de la ISA RV32I a una CPU sintetitzable

RISC-V destaca per la seva obertura: qualsevol pot implementar un nucli RV32I. Hi ha dissenys educatius que demostren pas a pas com construir una CPU base que executa programes reals, compilats amb GCC/LLVM per a riscv32-none-elf. L'experiència ensenya molt sobre el que passa «sota el capó» quan executes el teu assemblador.

La implementació típica inclou un controlador de memòria que abstrau ROM i RAM, interconnectat amb el nucli. La interfície del controlador sol tenir:

  • AddressIn (32 bits): adreça a accedir. Definiu l'origen de l'accés d'instrucció o dades.
  • DataIn (32 bits): dada a escriure. Per a mitja paraula, només es fan servir 16 bits LSB; per a byte, 8 LSB. S'ignora en lectura.
  • WidthIn: 0=byte, 1=mitja paraula (16 bits), 2 o 3=paraula (32 bits). Control de mida.
  • ExtendSignIn: si en llegir 8/16 bits cal estendre el signe a DataOut. S'ignora en escriptures.
  • WEIn: 0=lectura, 1=escriptura. Adreça de l'accés.
  • StartIn: flanc dinici; en posar-lo a 1 s'arrenca la transacció, sincronitzada al rellotge.

Quan ReadyOut=1, l'operació ha conclòs: en lectura, DataOut conté la dada (amb extensió de signe si escau); en escriptura, la dada ja està en memòria. Aquesta capa permet intercanviar RAM interna de FPGA, SDRAM o PSRAM externa sense tocar el nucli.

Una organització docent senzilla defineix tres fonts VHDL: ROM.vhd (4 KB), RAM.vhd (4 KB) i Memory.vhd (8 KB) que integra ambdues amb un espai contigu (ROM a 0x0000..0x0FFF, RAM a 0x1001..0x1FFF) i un GPIO mapejat a 0x1000 (bit 0 a un pin). El controlador MemoryController.vhd instància «Memory» i ofereix la interfície al nucli.

Sobre el nucli: la CPU conté 32 registres de 32 bits (x0..x31), amb x0 lligat a zero i no escrivible. A VHDL és habitual modelar-los amb arrays i blocs generat per evitar replicar lògica a mà i un descodificador de 5 a 32 per seleccionar quin registre rep la sortida de l'ALU.

L'ALU s'implementa combinacionalment amb un selector (ALUSel) per a operacions com a suma, resta, XOR, OR, AND, desplaçaments (SLL, SRL, SRA) i comparacions (LT, LTU, EQ, GE, GEU, NE). Per estalviar LUTs a FPGA, una tècnica popular és implementar desplaçaments d'1 bit i repetir-los cicles N mitjançant la màquina d'estats; s'incrementa latència, però es redueix consum de recursos.

El control s'articula amb multiplexors per a entrades de l'ALU (ALUIn1/2 i ALUSel), selecció de registre destinació (RegSelForALUOut), senyals cap al controlador de memòria (MCWidthIn, MCAddressIn, MCStartIn, MCWEIn, MCExtendSignIn, MCDataIn), i registres especials PC, IR i un Counter per comptar desplaçaments. Tot això, dirigit per una màquina d'estats amb 23 estats.

Un concepte clau en aquesta FSM és «càrrega retardada»: l'efecte de seleccionar una entrada de MUX es materialitza al següent flanc de rellotge. Per exemple, en carregar IR amb la instrucció que arriba de memòria, la seqüència passa per estats de fetch (llançar lectura a la direcció de PC), esperar ReadyOut, moure DataOut a IR i, ja al següent cicle, descodificar i executar.

El camí de fetch típic: en reset es força PC=RESET_VECTOR (0x00000000), després es configura el controlador per llegir 4 bytes a la direcció de PC, s'espera a ReadyOut i es carrega IR. A partir d'aquí, estats diferents gestionen ALU d'un cicle, desplaçaments multicicle, càrregues/emmagatzematges, bifurcacions, salts i especials (una implementació docent pot fer que ebreak aturi el processador a propòsit).

Compilar codi real i executar-lo al teu RISC-V

Una ruta de prova de concepte molt didàctica és compilar un programa C/C++ amb el compilador creuat riscv32-none-elf, generar el binari i bolcar-lo a una ROM VHDL. Després simules a GHDL i analitzes senyals a GtkWave; si tot va fi, sintetitzes en una FPGA i veus el sistema funcionant en silici.

Primer, un linker script adaptat al teu mapa: ROM de 0x00000000 a 0x00000FFF, GPIO a 0x00001000 i RAM de 0x00001001 a 0x00001FFF. Per simplicitat, pots col·locar .text (inclosa una secció .startup) a ROM i .data a RAM, deixant la inicialització de dades fora si vols escurçar la primera versió.

Amb aquest mapa, una rutina d'arrencada minimalista col·loca la pila al final de SRAM i invoca main; marcada com a «naked» ia la secció .startup per ubicar-la a RESET_VECTOR. Després de compilar, objdump permet veure l'ASM real que executarà la teva CPU (lui/addi per construir sp, jal a main, etc.).

Un exemple de blinker clàssic consisteix a alternar el bit 0 del GPIO mapejat: una espera curta per depurar en simulador (GHDL+GtkWave) i, en maquinari real, augmentar el recompte perquè el parpelleig sigui perceptible. El Makefile pot produir un .bin i un script que converteixi aquest binari en inicialització de ROM.vhd; una vegada integrat, compiles tot el VHDL, simules i després sintetitzes.

Aquesta aproximació docent funciona fins i tot en FPGA veteranes (p. ex., una Intel Cyclone II), on la RAM interna s'infereix amb la plantilla recomanada i el disseny pot rondar un 66% de recursos. El benefici pedagògic és enorme: veure com PC avança, com es disparen lectures (mcstartin), ReadyOut valida dades, IR captura instruccions i com es propaga cada salt o jal a través de la FSM.

Lectures, pràctiques i autograder: un full de ruta

En entorns acadèmics, el més habitual és que tinguis objectius clars: practicar condicionals i cicles, escriure funcions respectant el conveni i manejar memòria. Les guies solen aportar plantilles, un simulador (Jupiter), indicacions dinstal·lació i un autograder per corregir.

Per preparar l'entorn, accepteu l'assignació a Github Classroom si així us ho demanen, cloneu el repositori i obriu Jupiter. Recorda que __start ha de ser global, que els comentaris poden ser amb # o ;, que hi ha una instrucció per línia, i que has de finalitzar amb ecall (codi 10 a a0). Compila amb F3 i executa proves. Si no arrenca, el remei clàssic és reiniciar la màquina.

Sobre el format esperat de cada exercici, moltes guies inclouen captures i especifiquen: per exemple, Factor imprimeix els divisors separats per espais i retorna el compte; Upper ha de recórrer el string i transformar només minúscules a majúscules, sense tocar espais, dígits o signes de puntuació, i tornar el punter original.

L'avaluació sol repartir punts per sèrie (10/40/50) i pots executar un check per veure la nota de l'autograder. Quan estiguis satisfet, fes add/commit/push i puja la URL del repo on t'indiquin. Aquesta disciplina de cicle de vida t'acostuma a validar i lliurar amb rigor.

Més exercicis per consolidar: Fibonacci, Hanoi i lectura de teclat

Quan controlis el bàsic, treballa en tres clàssics addicionals: fibonacci.s, hanoi.si syscall.s (o una altra variant que llegiu del teclat i repeteixi una cadena).

  • Fibonacci: pots fer-ho recursiu o iteratiu; si ho fas recursiu, compte amb el cost i amb preservar ra/s0; iteratiu t'exercita bucles i sumes.
  • Hanoi: traducció de la funció recursiva a ASM. Preserva context i arguments entre trucades: stack frame disciplinat. Imprimeix moviments «origen → destí» amb ecall.
  • Lectura i repetició: llegeix un sencer i una cadena i imprimeix la cadena N vegades. A Jupiter, utilitza els codis ecall adequats disponibles a la teva pràctica; a Linux, prepara a7 i a0..a2 per a read/write.

Aquests exercicis consoliden pas de paràmetres, bucles i E/S. T'obliguen a pensar en la interfície amb l'entorn (Jupiter vs Linux), ia estructurar l'ASM perquè sigui llegible i mantenible.

Detalls fins d'implementació: registres, ALU i estats

Tornant al nucli RV32I educatiu, val la pena repassar diversos detalls fins que quadren el que veus en programar amb com ho executa el maquinari: la taula d'operacions d'ALU seleccionada per ALUSel (ADD, SUB, XOR, OR, AND, SLL, SRL, SRA, comparacions signades i sense signe), la «identitat» com a cas per defecte, i el «truc» d'usar un comptador (Counter) per acumular desplaçaments multicicle.

La lògica de registres amb generate produeix un descodificador de 5→32, i el cas RegSelForALUOut=00000 no fa res (x0 no es pot escriure, sempre val zero). El PC, IR i Counter tenen els seus MUX propis, orquestrats per la FSM: des de reset, fetch, decode/execute (ALU d'un cicle o bucles per a shift), càrregues/emmagatzematges, branques condicionals, jal/jalr i especials com a ebreak.

En accés a memòria de dades, és fonamental la coordinació MUX→Controlador: MCWidthIn (8/16/32 bits), MCWEIn (R/W), MCAddressIn (des de registres o PC), MCExtendSignIn (per a LB/LH signats) i MCStartIn. Només quan ReadyOut=1 has de capturar DataOut i avançar d’estat. Això alinea la teva mentalitat de programador ASM amb la realitat maquinari temporal.

Tot això connecta directament amb el que observes a la simulació: cada vegada que PC avança, es dispara una lectura d'instrucció, MCReadyOut indica que podeu carregar IR, ia partir d'aquí la instrucció fa el seu efecte (p. ex., «lui x2,0x2» seguit de «addi x2,x2,-4» per preparar sp, «jal x1, …» per trucar a main). Veure'l a GtkWave enganxa molt.

Recursos, dependències i consells finals

Per reproduir aquesta experiència necessites poques dependències: GHDL per compilar VHDL i GtkWave per analitzar senyals. Per al compilador creuat, qualsevol GCC riscv32-none-elf et serveix (pots compilar el teu o instal·lar-ne un de preconstruït). I per portar el nucli a una FPGA, utilitza l'entorn del teu fabricant (per exemple, Quartus a Intel/Altera) o toolchains lliures compatibles amb el dispositiu.

A més, val la pena llegir guies i apunts de RISC-V (per exemple, manuals pràctics i green cards), consultar llibres de programació, i practicar amb laboratoris que inclouen Jupiter i autograder. Mantingues una rutina: planifica, implementa, prova amb casos límit, i després integra en projectes majors (com el blinker a FPGA).

Amb tot aquest recorregut, ja tens l'essencial per arrencar: el perquè de l'assemblador davant del codi màquina, com muntar entorn amb Jupiter o Linux, els patrons de bucles, condicionals i funcions amb maneig correcte de la pila, i una finestra a la implementació maquinari per entendre millor el que passa quan executes cada instrucció.

Si el teu és aprendre fent, comença per Negative, Factor i Upper, segueix amb Fibonacci/Hanoi i un programa amb lectura de teclat, i quan estiguis a gust, compila un C++ senzill, bolca la ROM a VHDL, simula a GHDL i fa el salt a FPGA. És un viatge de menys a més en què cada peça encaixa amb la següent, i la satisfacció de veure el teu propi codi movent un GPIO o parpellejant un LED no té preu.

millors llibres programació
Article relacionat:
Millors llibres programació de cada llenguatge de programació