Pokud vás zajímá nízkoúrovňové programování a chcete se naučit programovat v assemblerovém jazyce na moderních architekturách, je RISC-V jedním z nejlepších vstupních bodů. Tento otevřený ISA, s velkou podporou v průmyslu i akademické sféře, umožňuje procvičovat si práci od jednoduchých simulátorů až po spuštění na FPGA, a to prostřednictvím kompletních nástrojů pro kompilaci C/C++ a zkoumání vygenerovaného ASM.
V tomto praktickém průvodci vám krok za krokem a s velmi pozemským přístupem řeknu, Co potřebujete k zahájení programování v assembleru RISC-V: nástroje, pracovní postup, klíčové příklady (podmíněné výrazy, smyčky, funkce, systémová volání), typická laboratorní cvičení a pokud máte chuť, pohled na implementaci CPU RV32I a na to, jak spustit vlastní binární soubor na jádru syntetizovaném pomocí FPGA.
Co je assembler RISC-V a jaký je jeho vztah ke strojovému jazyku?
RISC-V definuje architekturu otevřené instrukční sady (ISA): Základní repertoár RV32I obsahuje 39 instrukcí Velmi ortogonální a snadno implementovatelný. Assembler (ASM) je nízkoúrovňový jazyk, který používá mnemotechnické zkratky jako add, sub, lw, sw, jal atd., zarovnané s daným ISA. Základní strojový kód tvoří bity, kterým rozumí CPU; assembler je jeho lidsky čitelná reprezentace. blíže k hardwaru než jakýkoli jiný jazyk vyšší úrovně.
Pokud přecházíte z jazyka C, všimnete si, že ASM neběží tak, jak je: musí se to sestavit a propojit k vytvoření binárního souboru. Na oplátku vám to umožňuje ovládat registry, adresovací režimy a systémová volání s chirurgickou přesností. A pokud pracujete s výukovým simulátorem, uvidíte „ecall“ jako mechanismus vstupu/výstupu a ukončení, se specifickými konvencemi v závislosti na prostředí (např. Jupiter vs. Linux).
Nástroje a prostředí: simulátory, toolchain a FPGA
Pro rychlý začátek je ideální grafický simulátor Jupiter. Je to assembler/simulátor určený pro výuku, inspirovaný programy SPIM/MARS/VENUS a používaný v univerzitních kurzech. S ním můžete psát, sestavovat a spouštět programy RV32I bez nutnosti konfigurovat celou sadu nástrojů od nuly.
Pokud chcete jít ještě o krok dál, mohl by vás zajímat systém nástrojů na holém kovu: riscv32-none-elf (GCC/LLVM) pro kompilaci binárních souborů C/C++ do RISC-V a utility jako objdump pro disassemblér. Pro simulaci hardwaru umožňuje GHDL kompilovat VHDL, spustit jej a uložit signály do souboru .ghw pro kontrolu pomocí GtkWave. A pokud máte zájem o skutečný hardware, Můžete syntetizovat CPU RV32I do FPGA s prostředími výrobců (např. Intel Quartus) nebo bezplatnými nástroji.
Začínáme s Jupiterem: Základní pravidla toku a assembleru
Jupiter zjednodušuje křivku učení. Soubory vytváříte a upravujete na kartě Editora každý program začíná globálním tagem __start. Ujistěte se, že jej deklarujete pomocí direktivy .globl (ano, je to .globl, ne .global). Tagy končí dvojtečkou a komentáře mohou začínat znakem # nebo ;.
Pár užitečných pravidel pro životní prostředí: jedna instrukce na řádek, a až budete připraveni, uložte jej a stisknutím klávesy F3 jej sestavte a spusťte. Programy musí končit voláním ukončení ecall; v Jupiteru nastavení 10 na a0 signalizuje konec programu, podobně jako „ukončení“.
Minimálně by kostra vašeho ASM na Jupiteru mohla vypadat takto, s volným vstupním bodem a ukončením pomocí ecall: Je to základ pro ostatní cviky.
.text
.globl __start
__start:
li a0, 10 # código 10: terminar
ecall # finalizar programa
Konvence volání (ABI) a správa zásobníku
Programování funkcí v assembleru vyžaduje respektování konvence: Argumenty obvykle přicházejí v a0..a7Výsledek se obvykle vrací v a0 a volání musí zachovat návratové adresy (ra) a uložené registry (s0..s11). K tomu je vám k dispozici zásobník (sp): rezervuje si místo při vstupu a obnoví ho při výstupu.
Některé instrukce, které budete používat neustále: li a la pro načtení okamžitých položek a adres, add/addi pro sčítání, lw/sw pro přístup do paměti, nepodmíněné skoky j/jal a návraty jr ra, a také podmíněné výrazy jako beq/bne/bge. Zde je rychlá připomínka s typickými příklady:
# 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
Klasickou smyčku v RISC-V lze strukturovat jasně, oddělující podmínku, tělo a krokV Jupiteru můžete také vytisknout hodnoty pomocí ecall na základě kódu, který načtete do 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
U rekurzivních funkcí se postarejte o ukládání/obnovování registrů a ra. Faktoriál je kanonický příklad což vás nutí přemýšlet o rámci zásobníku a vrácení řízení na správnou adresu:
.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
Vstup/výstup s eCall: Rozdíly mezi Jupiterem a Linuxem
Instrukce ecall se používá k vyvolání služeb z prostředí. V Jupiteru, jednoduché kódy v a0 (např. 1 vypíše celé číslo, 4 vypíše řetězec, 10 ukončí) ovládat dostupné operace. V Linuxu však a0..a2 obvykle obsahují parametry, a7 číslo systémového volání a sémantika odpovídá voláním jádra (write, exit atd.).
Toto „Hello World“ pro Linux ilustruje tento vzorec: připravíte registry a0..a2 a a7 a spustíte ecall. Všimněte si direktivy .global a vstupního bodu _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"
Pokud je vaším cílem procvičování logiky řízení, paměti a funkcí, Jupiter vám dává okamžitou zpětnou vazbu A mnoho laboratoří obsahuje autograder pro ověření vašeho řešení. Pokud si chcete procvičit interakci se skutečným systémem, budete kompilovat pro Linux a používat systémová volání jádra.
Úvodní cvičení: podmíněné výrazy, smyčky a funkce
Klasická sada cvičení pro začátek v RISC-V ASM pokrývá tři pilíře: podmíněné výrazy, smyčky a volání funkcí, se zaměřením na správnou správu registrů a zásobníků:
- Záporné: funkce, která vrací 0, pokud je číslo kladné, a 1, pokud je záporné. Přijímá argument v a0 a vrací ho v a0, aniž by došlo ke zničení energeticky nezávislých záznamů.
- Faktor: Projde dělitele čísla, vypíše je za běhu a vrátí celkovou částku. Procvičíte si cykly, dělení/mod a volání metody ecalls. k tisku
- Horní: Je-li zadán ukazatel na řetězec, provede se jím procházení a na místě se převedou malá písmena na velká. Vrátit stejnou adresu; pokud během smyčky pohnete ukazatelem, před návratem jej resetujte.
U všech tří respektuje konvenci předávání parametrů a vracení. a ukončí program příkazem exit ecall když to vyzkoušíte na Jupiteru. Tato cvičení zahrnují tok řízení, paměť a stavové funkce.
Hlouběji: od RV32I ISA k syntetizovatelnému CPU
RISC-V vyniká svou otevřeností: jádro RV32I může implementovat kdokoli. Existují vzdělávací návrhy, které krok za krokem demonstrují, jak sestavit základní CPU, na kterém se spouští skutečné programy, zkompilováno s GCC/LLVM pro riscv32-none-elfZkušenosti vás hodně naučí o tom, co se děje „pod kapotou“, když spouštíte assembler.
Typická implementace zahrnuje řadič paměti, který abstrahuje ROM a RAM, propojeno s jádremRozhraní daného řadiče má obvykle:
- AddressIn (32 bitů): adresa pro přístup. Definuje původ přístupu instrukce nebo dat.
- DataIn (32 bitů): Data k zápisu. Pro půlslova se používá pouze 16 LSB bitů; pro bajty se používá 8 LSB bitů. Ignorováno při čtení.
- WidthIn: 0=bajt, 1=polovina slova (16 bitů), 2 nebo 3=slovo (32 bitů). Ovládání velikosti.
- ExtendSignIn: Zda rozšířit přihlášení v DataOut při čtení 8/16 bitů. V písemných pracích je to ignorováno.
- WEIn: 0=čtení, 1=zápis. Směr přístupu.
- StartIn: počáteční hrana; nastavení na 1 spustí transakci, synchronizované s hodinami.
Pokud je ReadyOut=1, operace je dokončena: Při čtení obsahuje DataOut data (s případným znaménkem); při zápisu jsou data již v paměti. Tato vrstva umožňuje přepínat mezi interní pamětí FPGA RAM, SDRAM nebo externí pamětí PSRAM bez nutnosti zásahu do jádra.
Jednoduchá výuková organizace definuje tři zdroje VHDL: ROM.vhd (4 KB), RAM.vhd (4 KB) a Memory.vhd (8 KB) který se integruje jak se souvislým prostorem (ROM na 0x0000..0x0FFF, RAM na 0x1001..0x1FFF), tak s GPIO namapovaným na 0x1000 (bit 0 na pin). Řadič MemoryController.vhd vytváří instanci „paměti“ a poskytuje rozhraní pro jádro.
O jádru: CPU obsahuje 32 32bitových registrů (x0..x31), přičemž x0 je vázán na nulu a nelze do nich zapisovat. Ve VHDL je běžné modelovat je pomocí polí a generovat bloky. aby se zabránilo ruční replikaci logiky, a dekodér 5 až 32 pro výběr registru, který přijímá výstup z ALU.
ALU je implementována kombinačně se selektorem (ALUSel) pro operace jako sčítání, odčítání, XOR, OR, AND, posuny (SLL, SRL, SRA) a srovnání (LT, LTU, EQ, GE, GEU, NE)Pro úsporu LUT v FPGA je oblíbenou technikou implementace 1bitových posunů a jejich opakování v N cyklech pomocí stavového automatu; to sice zvyšuje latenci, ale... spotřeba zdrojů je snížena.
Řízení je artikulováno pomocí multiplexorů pro vstupy ALU (ALUIn1/2 a ALUSel), výběr cílového registru (RegSelForALUOut), signály do řadiče paměti (MCWidthIn, MCAddressIn, MCStartIn, MCWEIn, MCExtendSignIn, MCDataIn) a speciální registry PC, IR a čítač pro počítání posunů. To vše je řízeno stavovým automatem s přibližně 23 stavy.
Klíčovým konceptem v tomto FSM je „zpožděné načítání“: Efekt výběru vstupu MUX se projeví na další hraně hodinového signálu.Například při načítání instrukce přicházející z paměti do IR prochází sekvence stavy načítání (spuštění čtení na adrese PC), čekání na ReadyOut, přesunutí DataOut do IR a v dalším cyklu dekódování a provádění.
Typická cesta načítání: při resetu vynutíte PC=RESET_VECTOR (0x00000000), poté nakonfigurujete ovladač tak, aby četl 4 bajty na adrese PC, Čeká se na ReadyOut a IR se načte.Odtud různé stavy spravují jednocyklové ALU, vícecyklové posuny, načítání/ukládání, větvení, skoky a „speciální operace“ (implementace učení může způsobit, že ebreak úmyslně zastaví procesor).
Zkompilujte skutečný kód a spusťte ho na vašem RISC-V
Velmi poučnou cestou k „proof of concept“ je kompilace programu v C/C++ pomocí křížového kompilátoru riscv32-none-elf. vygenerovat binární soubor a uložit ho do VHDL ROMPak simulujete v GHDL a analyzujete signály v GtkWave; pokud vše půjde dobře, syntetizujete je v FPGA a sledujete, jak systém běží v křemíku.
Nejprve, linkerový skript přizpůsobený vaší mapě: ROM z 0x00000000 na 0x00000FFF, GPIO na 0x00001000 a RAM od 0x00001001 do 0x00001FFF. Pro zjednodušení můžete do ROM umístit soubor .text (včetně sekce .startup) a do RAM soubor .data, přičemž inicializaci dat vynecháte, pokud chcete zkrátit první verzi.
S touto mapou minimalistická bootstrap rutina umístí zásobník na konec SRAM a vyvolá main; označeno jako „naked“ a v sekci .startup umístit jej do RESET_VECTOR. Po kompilaci vám objdump umožní vidět skutečný ASM, který váš procesor spustí (lui/addi pro sestavení sp, jal pro main atd.).
Klasickým příkladem blinkru je přepnutí bitu 0 mapovaného GPIO: krátké čekání na ladění v simulátoru (GHDL+GtkWave) a na reálném hardwaru zvýšit počet tak, aby bylo blikání znatelné. Makefile může vytvořit soubor .bin a skript, který tento binární soubor převede na soubor ROM initialization.vhd; po integraci, Zkompilujete celý VHDL, simulujete a poté syntetizujete..
Tento přístup k učení funguje i na starších FPGA (např. Intel Cyclone II), kde je interní RAM odvozena pomocí doporučené šablony a návrh může být efektivní z hlediska zdrojů až z 66 %. Pedagogický přínos je obrovský: podívejte se, jak počítač postupuje, jak se spouští čtení (mcstartin), ReadyOut ověřuje data, IR zachycuje instrukce a jak se každý skok nebo přeskok šíří přes FSM.
Čtení, postupy a autograder: Plán
V akademickém prostředí je běžné mít jasné cíle: Procvičování podmíněných výrazů a cyklů, psaní funkcí s ohledem na konvence a spravovat paměť. Průvodci obvykle poskytují šablony, simulátor (Jupiter), instalační pokyny a autograder pro opravy.
Pro přípravu prostředí přijměte úkol v Github Classroom, pokud se zobrazí výzva, naklonujte repozitář a otevřete Jupiter. Nezapomeňte, že __start musí být globální., že komentáře mohou být # nebo ;, že na řádek je jedna instrukce a že musíte ukončit příkazem ecall (kód 10 v a0). Zkompilujte s F3 a spusťte testy. Pokud se systém nespustí, klasickým řešením je restart počítače.
Pokud jde o očekávaný formát každého cvičení, mnoho průvodců obsahuje snímky obrazovky a specifikuje: Například Faktor vypíše dělitele oddělené mezerami a vrací počet; Upper by měl procházet řetězec a transformovat pouze malá písmena na velká, bez doteku mezer, číslic nebo interpunkčních znamének, a vrátit původní ukazatel.
Hodnocení obvykle rozděluje body za série (10/40/50) a Můžete si spustit kontrolu a zobrazit skóre autograderu.Až budete spokojeni, přidejte/odešlete/odešlete a nahrajte URL adresu repozitáře, kamkoli je to uvedeno. Tato disciplína životního cyklu vás zvykne na důsledné ověřování a doručování.
Další cvičení k posílení: Fibonacciho, Hanoi a čtení z klávesnice
Jakmile zvládnete základy, začněte pracovat na třech dalších klasikách: fibonacci.s, hanoi.sy systémové volání.s (nebo jiná varianta, která čte z klávesnice a opakuje řetězec).
- Fibonacci: Můžete to udělat rekurzivní nebo iterativní; pokud to uděláte rekurzivní, Buďte opatrní s náklady a se zachováním ra/s0iterativní cvičení, smyčky a sčítání.
- Hanoi: Překlad rekurzivní funkce do ASM. Zachovává kontext a argumenty mezi voláními: disciplinovaný rámec zásobníkuVytiskne pohyby „původ → cíl“ s ecalall.
- Čtení a opakování: Přečte celé číslo a řetězec a vypíše řetězec N-krát. Na Jupiteru použijte příslušné kódy pro e-volání dostupné ve vaší praxi; v Linuxu připravte a7 a a0..a2 pro čtení/zápis.
Tato cvičení konsolidují předávání parametrů, smyčky a I/O operace. Nutí vás přemýšlet o propojení s prostředím (Jupiter vs. Linux) a strukturovat ASM tak, aby byl čitelný a snadno udržovatelný.
Jemné detaily implementace: registry, ALU a stavy
Zpět k výukovému jádru RV32I stojí za zvážení několik detailů, které sladí to, co vidíte při programování, s tím, jak hardware funguje: operační tabulka ALU vybrané pomocí ALUSel (ADD, SUB, XOR, OR, AND, SLL, SRL, SRA, porovnání se znaménkem a bez znaménka), „identita“ jako výchozí případ a „trik“ použití čítače k akumulaci vícecyklových posunů.
Logika registrů s generováním vytváří dekodér 5→32 a případ RegSelForALUOut=00000 nic nedělá (x0 není zapisovatelné, vždy se rovná nule). PC, IR a Counter mají své vlastní MUXy, řízené automatickým automatem (FSM): z resetu, načtení, dekódování/spuštění (jednocyklové ALU nebo posuvné smyčky), načítání/ukládání, podmíněné větvení, jal/jalr a speciální operace jako ebreak.
Při přístupu do datové paměti je nezbytná koordinace MUX→Controller: MCWidthIn (8/16/32 bitů), MCWEIn (R/W), MCAddressIn (z registrů nebo PC), MCExtendSignIn (pro LB/LH se znaménkem) a MCStartIn. Pouze když je ReadyOut=1, měli byste zachytit DataOut. a pokročilý stav. To sladí myšlení vašeho ASM programátora s časovou hardwarovou realitou.
To vše přímo souvisí s tím, co pozorujete v simulaci: pokaždé, když se PC posune vpřed, je spuštěno čtení instrukce, MCReadyOut vám oznámí, že můžete načíst IR, a od té chvíle se instrukce projeví (např. «lui x2,0x2» následované «addi x2,x2,-4» pro přípravu sp, «jal x1,…» pro volání main). Vidět toto v GtkWave je velmi návykové.
Zdroje, závislosti a závěrečné tipy
Pro reprodukci této zkušenosti potřebujete několik závislostí: GHDL pro kompilaci VHDL a GtkWave pro analýzu signálůPro cross-compiler postačí libovolný GCC riscv32-none-elf (můžete si zkompilovat vlastní nebo nainstalovat předpřipravený). Pro portování jádra na FPGA použijte prostředí od výrobce (např. Quartus na Intelu/Alteře) nebo bezplatné sady nástrojů kompatibilní s vaším zařízením.
Kromě toho se vyplatí přečíst si příručky a poznámky k RISC-V (např. návody a zelené karty) a konzultovat knihy o programovánía procvičujte si s Laboratoře včetně Jupiteru a AutograderuDodržujte rutinu: plánujte, implementujte, testujte s okrajovými případy a poté integrujte do větších projektů (jako je Blinker na FPGA).
S těmito všemi informacemi již máte základní informace pro začátek: proč se používá assembler oproti strojovému kódu, jak nastavit prostředí s Jupiterem nebo Linuxem, vzory smyček, podmíněné výrazy a funkce se správnou manipulací se zásobníkem a nahlédnutím do hardwarové implementace pro lepší pochopení toho, co se stane při spuštění každé instrukce.
Pokud je pro vás učení praxí prioritou, začněte se zápornými, násobitelnými a horními číslicemi, poté přejděte na Fibonacciho/Hanojské rovnice a program pro čtení textu z klávesnice. Až si zvyknete, zkompilujte jednoduchý kód v C++, uložte ROM do VHDL, simulujte v GHDL a poté přejděte na FPGA. Je to cesta od méně k více, v níž každá část zapadá do té další.a uspokojení z toho, když vidíte, jak váš vlastní kód pohybuje GPIO nebo bliká LED, je k nezaplacení.