- 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.
Hvis du er nysgjerrig på assemblerspråk og føler at RISC-V er veien å følge, har du kommet til rett sted. Å komme i gang med assembler i RISC-V er mer tilgjengelig enn det ser ut til hvis du forstår verktøyene, programmeringsmodellen og noen viktige arkitekturregler.
I de følgende linjene har jeg kombinert det beste fra flere kilder: praktiske øvelser med Jupiter-lignende simulatorer, konvensjoner for det grunnleggende RV32I-repertoaret , eksempler på løkker og rekursjon, systemkall og til og med en titt på designet til en RISC-V CPU i VHDL (med ALU, minnekontroll og tilstandsmaskin), i tillegg til en gjennomgang av verktøykjeder og koblingsskript.
Hva er en RISC-V-assembler, og hvordan skiller den seg fra maskinspråk?
Selv om begge er knyttet til maskinvaren , er maskinspråk rent binært (enere og nuller) som CPU-en tolker direkte, mens assembler bruker mnemonikk og mer lesbare symboler som en assembler deretter oversetter til binært.
RISC-V definerer en åpen ISA med et svært rent basisinstruksjonssett. RV32I-profilen (32-bit) inkluderer 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 32-bits generelle registre (x0–x31) ; x0 leses alltid som 0 og kan ikke skrives til. I tillegg brukes aliaser som a0–a7 (argumenter), t0–t6 (midlertidig) og s0–s11 (lagret), som er 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 etiketten __start , som du må deklarere som synlig (for eksempel med .globl) for å markere startpunktet.
Etiketter slutter med et kolon , du kan bare legge inn én instruksjon per linje, og kommentarer kan starte med # eller ; slik at assembleren ignorerer dem.
Verktøy og simulatorer: Jupiter og grunnleggende arbeidsflyt
For ukomplisert øving har du Jupiter-simulatoren/assembleren , 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 Editor-fanen. Etter lagring setter du sammen programmet med F3 og kjører det for å feilsøke flyten instruksjon for instruksjon, ved å bruke register- og minnevisningene for å forstå maskinens tilstand.
Programmer må avsluttes med et exit-kall: et exit ecall, som konfigurerer a0 med koden 10 (exit). I RISC-V tilsvarer ecalls systemkall eller systemfeller.
Minimumsstruktur for et program og systemkall
Den typiske strukturen i akademiske eksempler definerer et startpunkt, utfører oppgaven og avslutter med et ecall. Ecall-argumentene går vanligvis i a0–a2 og tjenestevelgeren i a7, avhengig av miljøet.
På et Linux RISC-V-system kan du for eksempel skrive ut ved hjelp av write syscall og avslutte med riktig kode. For write bruker du a0 (fd), a1 (buffer), a2 (lengde) og a7 med tjenestenummeret . For å avslutte forbereder du a0 med returkoden og a7 med exit-nummeret.
# 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 I/O-tjeneste , endrer du antallet e-kall og registre i henhold til miljødokumentasjonen.
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 det er positivt og 1 hvis det er negativt ; med RV32I løser en sammenligning med 0 og en set-on-less-than-setning problemet i en enkelt, gjennomtenkt instruksjon.
En annen veldig nyttig øvelse er å liste opp faktorene til et tall: iterer fra 1 til n, skriv ut divisorene, og returner hvor mange det var . Du vil øve på betingede grener, divisjoner (eller gjentatte subtraksjoner) og løkker med addisjoner og sammenligninger.
Å jobbe med strenger krever minnehåndtering: man besøker hvert tegn i en streng i minnet og konverterer små bokstaver til store bokstaver hvis de passer innenfor ASCII-området. Til slutt returneres strengens opprinnelige adresse.
Løkker, funksjoner og rekursjon: faktorial, Fibonacci og Hanoi-tårnet
Når du designer løkker, tenk på tre blokker: betingelse, kropp og trinn. Med beq/bne/bge og ubetingede hopp jal/j, bygges while/for-løkker uten mystikk, støttet av addisjon 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: lagre ra hvis du skal kjede sammen flere kall , behold s0–s11 hvis du endrer dem, og bruk stakken med sp som beveger seg i ordmultipler.
Faktorial er den klassiske rekursive funksjonen: basistilfellet n==0 returnerer 1 ; ellers kaller den faktorial(n-1) og multipliserer med n. Den beskytter ra og poster lagret på stakken før kallet og gjenoppretter 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 er nyttig for å øve på både rekursjon med to kall og en effektiv iterativ versjon med akkumulatorvariabler. Og hvis du ønsker en utfordring i flyt- og parameterkontroll, oversett en Towers of Hanoi -løsning til samling med fire argumenter: disker, kildetårn, destinasjonstårn og hjelpetårn; respekter rekkefølgen på kallene og vis hvert trekk.
Minnetilgang, arrayer og strengmanipulering
I RISC-V gjøres minnetilgang med lasting/lagring: lw/sw for ord, lh/sh for halve ord og lb/sb for byte , med signerte eller usignerte varianter i lastingene (lb vs lbu, lh vs lhu).
For å gå gjennom matriser med heltall, bruk forskyvninger på 4 byte per indeks; for strenger, flytt byte for byte til du 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 er interessert i å fordype deg i silisium, bygger dette utdanningsprosjektet en RV32I CPU i VHDL, syntetiserbar på en lav- til mellomregister FPGA. Den inkluderer program-ROM, data-RAM og en enkel GPIO-pin for å drive en LED.
Kjernen implementerer basisrepertoaret (uten M/A/C-utvidelser eller CSR-er), bruker en 32-bits adressebuss og tillater 8/16/32-bits minnetilgang med fortegnsutvidelse der det er passende. Designet skiller tydelig registre, ALU, minnekontroller og tilstandsmaskin.
ALU, skift og ideen om "forsinket lasting"
ALU beskrives i kombinasjonstermer med operasjoner som addisjon, subtraksjon, XOR, OR, AND, sammenligninger (med og uten fortegn) og logiske/aritmetiske skift.
For å spare LUT-er på FPGA-er implementeres flerbitsskift ved å iterere 1-bitsskift kontrollert av tilstandsmaskinen : du forbruker flere sykluser, men du reduserer logikkressursene.
I synkrone kretser observeres endringer på klokkekantene. Konseptet med "forsinket last" minner oss 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, noe som forenkler prosessorgrensesnittet . Kontrolleren mottar AddressIn (32 bits), DataIn, bredden (byte/halvord/ord), fortegnsutvidelsessignalet, WE (lese/skrive) og Start for å starte transaksjoner.
Når operasjonen er fullført, settes ReadyOut til 1, og hvis det var en leseoperasjon , inneholder DataOut dataene (utvidet med fortegnet når det blir forespurt). Hvis det var en skriveoperasjon, lagres dataene i RAM.
Et eksempel på en praktisk mapping: ROM fra 0x0000 til 0x0FFF , én GPIO-byte ved 0x1000 (bit 0 til en pin), og RAM fra 0x1001 til 0x1FFF . Med 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 dekoder for å velge skrivedestinasjonen fra ALU-en og bevare resten.
Multipleksere styrer ALU-innganger (operander og operasjon), signaler til minnekontrolleren (bredder, adresse, startkontroll og lese/skrive) og spesialregistre: PC, IR og en hjelpeteller for iterative skift.
Tilstandsmaskinen starter med reset , henter instruksjonen som PC peker til (leser 4 byte), laster den inn i IR når den er klar og sender den til utførelsesnodene: ALU (én instruksjon i 1 syklus unntatt for shifts), load/store, branches og jumps, samt spesielle instruksjoner som ebreak.
Verktøykjede på tvers av verktøy, kobling og feilsøking
For å generere RV32I-binærfiler, bruk en cross-GCC (target riscv32-none-elf) . Kompiler C/C++/ASM-kilder, koble til et skript som definerer minnekartet, og konverter utdataene til formatet ROM/FPGA-en din forventer.
Et enkelt koblingsskript kan plassere .text 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 stakkpekeren på slutten av RAM (f.eks. 0x1FFC) før main kalles.
/* 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 demontere ELF-filen og sjekke adressene ; for eksempel vil du se oppstartsprosessen ved 0x00000000 med instruksjoner som `lui`, `addi` og `jal`, og overgangen til hovedprogrammet ditt. For VHDL-simulering genererer GHDL spor som du kan åpne med GtkWave.
Etter verifisering i simulering, kjør designet på en FPGA (Quartus eller en annen verktøykjede). Hvis RAM-en er utledet som interne blokker og koden er tydelig RTL , bør du kunne syntetisere uten overraskelser, selv på eldre enheter.
Praktiske påminnelser og typiske feil når du starter opp
Ikke glem at x0 alltid er null ; å skrive til den har ingen effekt, og å lese den returnerer 0. Bruk dette til din fordel i addisjoner, sammenligninger og registeropprydding.
Når du implementerer funksjoner, lagrer du ra og eventuelle modifiserte sN-registre , og administrerer stakken med ordjusterte sp-addition/subtract-operasjoner. Ved retur, gjenoppretter du i omvendt rekkefølge og hopper til ra ved hjelp av jr.
I simulatorer som Jupiter, sjekk at `__start` er global og at du avslutter med en korrekt `ecall` (`a0=10` for å avslutte). Hvis noe ikke starter, sjekk etiketten, globaliteten og kompiler på nytt (F3).
I I/O-øvelser, respekter miljøets protokoll : hvilke registre som bærer parametere, tjenestenummeret og om en adresse eller en umiddelbar verdi forventes. Bruk simulatoren eller operativsystemdokumentasjonen.
Med et solid fundament i ISA (RV32I, registre og ABI), en brukervennlig simulator som Jupiter, og et økende antall eksempler (negativer, faktorer, store bokstaver, løkker, faktorialer, Fibonacci og Hanoi), slutter RISC-V assemblerspråk å være en barriere og blir et rikt grunnlag for å forstå hvordan en CPU tenker. Og hvis du begir deg ut i VHDL, vil du se hvordan ALU, minne og kontroll passer sammen : fra instruksjonshenting og lazy loading til minnegrensesnittet og et kart med ROM, RAM og GPIO som lar deg få en LED til å blinke med din egen prosessor.
Lidenskapelig forfatter om verden av bytes og teknologi generelt. Jeg elsker å dele kunnskapen min gjennom å skrive, og det er det jeg skal gjøre i denne bloggen, vise deg alle de mest interessante tingene om dingser, programvare, maskinvare, teknologiske trender og mer. Målet mitt er å hjelpe deg med å navigere i den digitale verden på en enkel og underholdende måte.