Det du trenger for å begynne å programmere i RISC-V assembler

Siste oppdatering: 08/10/2025
Forfatter: Isaac
  • Inkluderer RV32I: registre, ABI og flytkontroll med ecall.
  • Øvelse med Jupiter og øvelser: negativ, faktorer, kjeder, rekursjon.
  • Master kryssverktøykjede, script lenking og feilsøking med objdump.

RISC-V Assembler: Krav og Komme i gang

Hvis du er nysgjerrig på assembler og føler at RISC-V er veien å gå, har du kommet til rett sted. Det er rimeligere å komme i gang med ASM på RISC-V enn det ser ut til Hvis du forstår verktøyene, modellen til programmering og noen viktige arkitekturregler.

I de følgende linjene har jeg kombinert det beste fra flere kilder: praksis med Jupiter-type simulatorer, konvensjoner i RV32I-basisrepertoaret, løkke- og rekursjonseksempler, systemkall, og til og med en titt på RISC-V CPU-design i VHDL (med ALU, minnekontroll og tilstandsmaskin), pluss en gjennomgang av verktøykjede- og koblingsskript.

Hva er en RISC-V-assembler, og hvordan skiller den seg fra maskinspråk?

Selv om begge er knyttet til maskinvare, Maskinspråk er rent binært (enere og nuller) som CPU-en tolker direkte, mens assembleren bruker mnemonikk og symboler mer lesbar enn en assembler, og oversetter deretter til binærformat.

RISC-V definerer en åpen ISA med et veldig rent basisrepertoar. RV32I-profilen (32-bit) inneholder 39 brukerinstruksjoner med bemerkelsesverdig ortogonalitet, som skiller minnetilgang fra ren beregning, og med utmerket støtte i GCC/LLVM.

Registreringer, avtaler og inngangspunkt

I RV32I har du 32 generelle registre (x0–x31) 32-bit; x0 leses alltid som 0 og kan ikke skrives til. Aliaser som a0–a7 (argumenter), t0–t6 (midlertidige filer) eller s0–s11 ​​​​(lagret) er også nyttige for å følge ABI.

Avhengig av miljøet eller simulatoren, kan programmet starte med en bestemt etikett. I Jupiter starter programmer med den globale __start-taggen., som du må deklarere som synlig (for eksempel med .globl) for å markere inngangspunktet.

Las tagger slutter med kolon, du kan bare legge inn én instruksjon per linje, og kommentarer kan startes med # eller ;, slik at assembleren ignorerer dem.

Verktøy og simulatorer: Jupiter og grunnleggende arbeidsflyt

For å øve uten komplikasjoner, har du Jupiter-simulator/assembler, et grafisk verktøy inspirert av SPIM/MARS/VENUS som forenkler redigering, montering og utførelse i ett enkelt miljø.

I Jupiter kan du opprette, redigere og slette filer i Redigeringsprogrammet-fanen. Etter lagring, sett sammen med F3 og kjør å feilsøke flyten instruksjon for instruksjon, ved å bruke register- og minnevisninger for å forstå maskinens tilstand.

Programmer må avsluttes med en oppfordring til miljøet: utgangsanropsinnstilling a0 med kode 10 (utgang). I RISC-V tilsvarer tilbakekallinger systemkall eller feller til miljøet/systemet.

Minimumsstruktur for et program og systemkall

Den typiske strukturen i akademiske eksempler definerer et utgangspunkt, gjør jobben og avsluttes med et «ecall». Recall-argumenter beveger seg vanligvis i a0–a2 og tjenestevelgeren i a7, avhengig av miljøet.

I en Linux RISC-V, for eksempel, kan du skrive ut med write syscall og avslutte med riktig kode. For skriving brukes a0 (fd), a1 (buffer), a2 (lengde) og a7 med servicenummeretTil slutt settes a0 til returkoden og a7 til utgangsnummeret.

# 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"

Hvis du jobber utenfor Linux, for eksempel i pedagogiske simulatorer med egen IoT-tjeneste, endre e-call-nummeret og registrene i henhold til miljødokumentasjonen.

  Få Windows 8.1 ISO-filer [USB- og DVD-oppsett]

Innledende øvelser som hjelper deg å bli komfortabel med kondisjonaliser, løkker og hukommelse

En typisk oppvarming er å oppdage om et heltall er negativt. Du kan returnere 0 hvis den er positiv og 1 hvis den er negativ.; med RV32I løser en sammenligning med 0 og en sett-på-mindre-enn problemet i én gjennomtenkt instruksjon.

En annen veldig nyttig øvelse er å liste opp faktorer av et tall: går fra 1 til n, skriver ut divisorene og returnerer hvor mange det varDu vil øve på betingede grener, divisjon (eller gjentatt subtraksjon) og løkker med addisjon og sammenligning.

Å jobbe med strenger tvinger deg til å håndtere minne: Gå til hvert tegn i en streng i minnet og konverter små bokstaver til store bokstaver på stedet hvis de passer innenfor ASCII-området. Når det er fullført, returneres den opprinnelige adressen til strengen.

Løkker, funksjoner og rekursjon: faktorial, Fibonacci og Hanoi-tårnet

Når du designer løkker, tenk på tre blokker: tilstand, kropp og trinn. Med beq/bne/bge og ubetingede hopp bygges jal/j mens/for uten mystikk, avhengig av addi og sammenligninger.

.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

I funksjonskall, respekter ABI: spar hvis du skal kjede flere samtaler, bevarer s0–s11 ​​​​hvis du endrer dem, og bruker stakken med sp som beveger seg i multipler av et ord.

Faktorial er den klassiske rekursjonen: basistilfellet n==0 returnerer 1; ellers, kall faktorial(n-1) og multipliser med n. Beskytt ra og registre lagret på stakken før kallet og gjenopprett dem ved retur.

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-metoden er nyttig for å øve på begge deler rekursjon med to kall som en effektiv iterativ versjon med akkumulatorvariabler. Og hvis du ønsker en utfordring med flyt- og parameterkontroll, oversett en løsning til assembler. Tårnene i Hanoi med fire argumenter: disker, kilde, destinasjon og hjelpetårn; den respekterer anropsrekkefølgen og viser hver bevegelse.

Minnetilgang, arrayer og strengmanipulering

I RISC-V gjøres minnetilgang med load/store: lw/sw for ord, lh/sh for halvord og lb/sb for byte, med signerte eller usignerte varianter i anklagene (lb vs lbu, lh vs lhu).

  Project Zomboid på Android: Kan du spille det på mobil?

For å gå gjennom heltallsmatriser, bruk 4-byte forskyvninger per indeks; for tekststrenger, går videre byte for byte til den finner terminatoren hvis konvensjonen krever det (f.eks. \0). Husk å lagre basisadresser og håndtere pekere med addi/auipc/la etter behov.

Designe en RV32I CPU fra bunnen av: En oversikt på høyt nivå

Hvis du har lyst til å gå ned til silisium, bygger et utdanningsprosjekt en RV32I CPU i VHDL, syntetiserbar i FPGA Lavt til mellomregister. Inkluderer program-ROM, data-RAM og en enkel GPIO for å tenne en LED.

Kjernen implementerer basisrepertoaret (uten M/A/C-utvidelser eller CSR-er), bruker en 32-bits adressebuss og tillater 8-/16-/32-bits fortegnsutvidet minnetilgang der det er passende. Designet skiller tydelig registre, ALU, minnekontroller og tilstandsmaskin.

ALU, skift og ideen om "forsinket lasting"

ALU beskrives i kombinasjon med operasjoner som addisjon, subtraksjon, XOR, OR, AND, sammenligninger (med og uten fortegn) og logiske/aritmetiske skift.

For å spare LUT-er i FPGA implementeres flerbitskift itererende 1-bits skift kontrollert av tilstandsmaskinenDu bruker flere sykluser, men du reduserer logiske ressurser.

I synkrone kretser observeres endringer på klokkekantene. Konseptet «forsinket last» minner om at det som velges av en multiplekser påvirker registeret i neste syklus., et sentralt aspekt når man designer hente-dekode-utføre-tilstandsmaskinen.

Minnekontroller og -kart: ROM, RAM og GPIO

En minneblokk integrerer ROM og RAM i et sammenhengende rom, forenkling av prosessorgrensesnittetKontrolleren mottar AddressIn (32 bits), DataIn, bredden (byte/halvord/ord), fortegnsutvidelsessignalet, WE (lese/skrive) og Start for å starte transaksjoner.

Når operasjonen er over, ReadyOut er satt til 1, og hvis den har blitt lest, DataOut inneholder dataene (fortegnsutvidet når det forespørres). Hvis de ble skrevet, forblir dataene i RAM.

Et eksempel på et praktisk kart: ROM fra 0x0000 til 0x0FFF, en GPIO-byte ved 0x1000 (bit 0 til en pinne) og RAM fra 0x1001 til 0x1FFFMed dette kan du lage en blinker ved å skrive og veksle mellom utgangsbiten.

Registre, multipleksere og tilstandsmaskiner

CPU-en definerer 32 generelle registre instansiert med arrayer i VHDL, med en dekoderen for å velge skrivedestinasjonen fra ALU-en og beholde resten.

Multipleksere styrer ALU-innganger (operander og operasjon), signalene til minnekontrolleren (bredder, adresse, startkontroll og lese/skrive) og spesialregistrene: PC, IR og en hjelpeteller for iterative skift.

Tilstandsmaskinen starter med tilbakestille, henter instruksjonen som PC-en peker på (4-byte-lesing), den lastes inn i IR når den er klar og sendes til utførelsesnodene: ALU (én instruksjon i 1 syklus unntatt skift), last/lagring, forgreninger og hopp, i tillegg til spesielle instruksjoner som ebreak.

  Hvordan sette opp Bluetooth i Windows 10

Verktøykjede på tvers av verktøy, kobling og feilsøking

For å generere RV32I-binærfiler, bruk en Kryss GCC (mål riscv32-none-elf)Du kompilerer C/C++/ASM-kilder, kobler til et skript som definerer minnekartet, og konverterer utdataene til den formen ROM/FPGA-en din forventer.

Et enkelt hook-skript kan plassere .tekst i ROM fra 0x0000 og .data i RAM fra 0x1004 (hvis 0x1000–0x1003 er opptatt av GPIO-registre). Oppstartsrutinen kan være «naken» og plassere stakkpeker på slutten av RAM (f.eks. 0x1FFC) før du ringer hovedtelefonen.

/* Mapa simple
 * ROM: 0x00000000 - 0x00000FFF
 * GPIO: 0x00001000 - 0x00001003
 * RAM: 0x00001004 - 0x00001FFF
 */
SECTIONS {
  . = 0x00000000;
  .text : { *(.startup) *(.text) *(.text.*) *(.rodata*) }
  . = 0x00001004;
  .data : { *(.data) *(.data.*) }
}

Med riscv32-none-elf-objdump kan du demonter ELF-en og sjekk adressene; for eksempel vil du se boot ved 0x00000000 med instruksjoner som lui/addi/jal og overgangen til hovedinnstillingen din. For VHDL-simulering genererer GHDL spor som du kan åpne med GtkWave.

Etter verifisering i simulering, ta designet til en FPGA (Quartus eller annen verktøykjede). Hvis RAM utledes som interne blokker og koden er klar RTL, bør du syntetisere uten overraskelser, selv på veteranenheter.

Praktiske påminnelser og typiske feil når du starter opp

Ikke glem det x0 er alltid null; å skrive til den har ingen effekt, og å lese den returnerer 0. Bruk dette til din fordel i tillegg, sammenligninger og registeropprydding.

Når du implementerer funksjoner, lagre RA- og sN-poster du endrer, og administrerer stakken med ordjusterte addisjoner/subtraksjoner til sp. Ved retur gjenoppretter den i omvendt rekkefølge og hopper med jr ra.

I simulatorer som Jupiter, sjekk at __start er global, og du avslutter den med ecall riktig (a0=10 for å avslutte). Hvis noe ikke starter, sjekk etiketten, globaliteten og kompiler på nytt (F3).

I øvelser med IO, respekter miljøets protokoll: hvilke registre som inneholder parametere, tjenestenummeret og om det forventes en adresse eller umiddelbar verdi. Bruk simulatoren eller dokumentasjonen for operativsystemet.

Med en tydelig ISA-base (RV32I, registre og ABI), en komfortabel simulator som Jupiter, og økende eksempler (negativ, faktorer, store bokstaver, løkker, faktorial, Fibonacci og Hanoi), slutter RISC-V-assembleren å være en vegg og blir et saftig terreng for å forstå hvordan en CPU tenker. Og hvis du tør å gå ned til VHDL, vil du se hvordan ALU, minne og kontroll passer sammen.fra instruksjonshenting og lazy loading til minnegrensesnitt og et kart med ROM, RAM og GPIO som lar deg blinke en LED med din egen prosessor.

De beste programmene å programmere
Relatert artikkel:
De 7 beste programmene å programmere