Какво ви е необходимо, за да започнете да програмирате на RISC-V асемблер

  • Готова среда: Jupiter за практикуване на ASM, riscv32-none-elf за компилиране и GHDL+GtkWave за симулиране.
  • Овладейте цикли, условни изрази и функции с RISC-V ABI и правилно боравене със стека.
  • ECALL според средата: Jupiter (прости кодове) срещу Linux (a0..a2 и a7 със системни повиквания).
  • Направете крачката: компилирайте C/C++ в двоичен файл, генерирайте ROM и го стартирайте на RV32I CPU в FPGA.

RISC-V асемблер

Ако сте любопитни за програмиране на ниско ниво и искате да научите асемблерно програмиране върху съвременни архитектури, RISC-V е една от най-добрите отправни точки. Този отворен ISA, с голям успех в индустрията и академичните среди, ви позволява да практикувате от прости симулатори до изпълнението им на FPGA, през пълни набори от инструменти за компилиране на C/C++ и изследване на генерирания ASM.

В това практическо ръководство ви казвам, стъпка по стъпка и с много земен подход, Какво ви е необходимо, за да започнете да програмирате на RISC-V асемблер: инструментите, работният процес, ключови примери (условни оператори, цикли, функции, системни извиквания), типични лабораторни упражнения и, ако желаете, поглед върху това как е реализиран RV32I процесор и как да стартирате свой собствен двоичен файл върху FPGA синтезирано ядро.

Какво е RISC-V асемблер и как се свързва с машинния език?

RISC-V дефинира архитектура с отворен набор от инструкции (ISA): Базовият репертоар на RV32I включва 39 инструкции Много ортогонален и лесен за имплементация. Асемблер (ASM) е език на ниско ниво, който използва мнемоники като add, sub, lw, sw, jal и др., съобразени с този ISA. Основният машинен код са битовете, които процесорът разбира; асемблерът е неговото четливо от човек представяне. по-близо до хардуера от който и да е език за програмиране от високо ниво.

Ако идвате от C, ще забележите, че ASM не работи както е: трябва да бъде сглобено и свързано да се създаде двоичен файл. В замяна, това ви позволява да контролирате регистри, режими на адресиране и системни повиквания с хирургическа прецизност. И ако работите със симулатор за обучение, ще видите „ecall“ като механизъм за вход/изход и прекратяване, със специфични конвенции в зависимост от средата (напр. Jupiter срещу Linux).

Инструменти и среда: симулатори, набор от инструменти и FPGA

За бърз старт, графичният симулатор на Jupiter е идеален. Това е асемблер/симулатор, предназначен за обучение, вдъхновен от SPIM/MARS/VENUS и използван в университетски курсове. С него можете да пишете, асемблирате и изпълнявате RV32I програми, без да конфигурирате цял набор от инструменти от нулата.

Ако искате да направите още една крачка напред, може да се интересувате от инструментална верига без вградени инструменти: riscv32-none-elf (GCC/LLVM) за компилиране на C/C++ в RISC-V двоични файлове и помощни програми като objdump за дизасемблер. За симулация на хардуер, GHDL ви позволява да компилирате VHDL, да го изпълните и да изхвърлите сигнали в .ghw файл за проверка с GtkWave. И ако сте готови за истински хардуер, Можете да синтезирате RV32I процесор в FPGA с производствени среди (напр. Intel Quartus) или безплатни набори от инструменти.

Първи стъпки с Jupiter: Основен поток и правила на асемблера

Юпитер опростява кривата на обучение. Създавате и редактирате файлове в раздела „Редактор“, и всяка програма започва с глобалния таг __start. Уверете се, че сте го декларирали с директива .globl (да, това е .globl, а не .global). Таговете завършват с двоеточие, а коментарите могат да започват с # или ;.

Няколко полезни правила за околната среда: една инструкция на ред, и когато сте готови, запазете го и натиснете F3, за да го асемблирате и стартирате. Програмите трябва да завършват с изходно извикване ecall; в Jupiter, задаването на 10 на a0 сигнализира за края на програмата, подобно на „изход“.

Минимално, вашият ASM скелет на Юпитер може да изглежда така, с отворена входна точка и прекратяване чрез ecall: Това е основата на останалите упражнения.

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

Конвенции за извикване (ABI) и управление на стека

Програмирането на функции на асемблер изисква спазване на конвенцията: Аргументите обикновено пристигат в a0..a7Резултатът обикновено се връща в a0, а извикванията трябва да запазят адресите за връщане (ra) и запазените регистри (s0..s11). За да направите това, стекът (sp) е ваш приятел: той резервира място при вход и го възстановява при изход.

Някои инструкции, които ще използвате през цялото време: li и la за зареждане на незабавни заявки и адреси, add/addi за събиране, lw/sw за достъп до паметта, безусловни преходи j/jal и връща jr ra, както и условни изрази като beq/bne/bge. Ето едно кратко напомняне с типични примери:

# 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

Класическият цикъл в RISC-V може да бъде структуриран ясно, разделяне на условието, тялото и стъпкатаВ Jupiter можете също да отпечатвате стойности с ecall въз основа на кода, който зареждате в 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

За рекурсивни функции, погрижете се за запазване/възстановяване на регистри и ra. Факториелът е каноничният пример което ви принуждава да мислите за стековия кадър и връщането на контрола на правилния адрес:

.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

Вход/Изход с ecall: Разлики между Jupiter и Linux

Инструкцията ecall се използва за извикване на услуги от средата. В Jupiter, прости кодове в a0 (напр. 1 отпечатва цяло число, 4 отпечатва низ, 10 изход) контролират наличните операции. В Linux обаче a0..a2 обикновено съдържат параметри, a7 е номерът на системното извикване, а семантиката съответства на извикванията на ядрото (write, exit и т.н.).

Това „Здравей, свят“ за Linux илюстрира модела: подготвяте регистрите a0..a2 и a7 и изпълнявате ecall. Обърнете внимание на директивата .global и входната точка _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"

Ако целта ви е да практикувате логиката на управлението, паметта и функциите, Юпитер ви дава незабавна обратна връзка И много лабораторни упражнения включват автоградатор за валидиране на вашето решение. Ако искате да практикувате взаимодействие с реалната система, ще компилирате за Linux и ще използвате системни извиквания на ядрото.

Упражнения за начало: условни изрази, цикли и функции

Класическият набор от упражнения за започване на работа с RISC-V ASM обхваща три стълба: условни изрази, цикли и извиквания на функции, с фокус върху правилното управление на регистрите и стека:

  • Отрицателно: функция, която връща 0, ако числото е положително, и 1, ако е отрицателно. Получава аргумента в a0 и връща в a0, без да се унищожават енергонезависими записи.
  • Фактор: Преминава през делители на число, отпечатва ги по време на изпълнение и връща общата сума. Ще практикувате цикли, деление/модификация и извиквания на ecall за отпечатване.
  • Горна: Като се има предвид указател към низ, той се обхожда и се преобразуват малки букви в главни на място. Върнете същия адрес; ако преместите показалеца по време на цикъла, нулирайте го преди да се върнете.

И за трите, спазва конвенцията за предаване на параметри и връщане, и завършва програмата с exit ecall когато го опитате на Юпитер. Тези упражнения обхващат контролния поток, паметта и функциите за отчитане на състоянието.

По-задълбочено проучване: от RV32I ISA до синтезируем CPU

RISC-V се откроява със своята отвореност: всеки може да внедри RV32I ядро. Съществуват образователни проекти, които демонстрират стъпка по стъпка как да се изгради базов процесор, който изпълнява реални програми, компилиран с GCC/LLVM за riscv32-none-elfОпитът ви учи много за това какво се случва „под капака“, когато стартирате асемблера си.

Типичната имплементация включва контролер на паметта, който абстрахира ROM и RAM, взаимосвързан с ядротоИнтерфейсът на този контролер обикновено има:

  • AddressIn (32 бита): адрес за достъп. Определя произхода на достъпа на инструкция или данни.
  • Входни данни (32 бита): Данни за запис. За полудуми се използват само 16 LSB бита; за байтове се използват 8 LSB бита. Игнорирано при четене.
  • WidthIn: 0=байт, 1=половин дума (16 бита), 2 или 3=дума (32 бита). Контрол на размера.
  • ExtendSignIn: Дали да се разшири входът в DataOut при четене на 8/16 бита. Това се игнорира в писанията.
  • WEIn: 0=четене, 1=запис. Посока на достъп.
  • StartIn: начален ръб; задаването му на 1 стартира транзакцията, синхронизиран с часовника.

Когато ReadyOut=1, операцията е завършена: При четене, DataOut съдържа данните (с разширение на знака, ако е приложимо); при запис данните вече са в паметта. Този слой ви позволява да сменяте вътрешна FPGA RAM, SDRAM или външна PSRAM памет, без да докосвате ядрото.

Една проста учебна организация определя три VHDL източника: ROM.vhd (4 KB), RAM.vhd (4 KB) и Memory.vhd (8 KB) който се интегрира както със съседно пространство (ROM на 0x0000..0x0FFF, RAM на 0x1001..0x1FFF), така и с GPIO, картографиран на 0x1000 (бит 0 към пин). Контролерът MemoryController.vhd създава "Памет" и предоставя интерфейса на ядрото.

Относно ядрото: Процесорът съдържа 32 32-битови регистъра (x0..x31), като x0 е свързан с нула и не може да се записва в него. Във VHDL е обичайно те да се моделират с масиви и да се генерират блокове. за да се избегне ръчното репликиране на логиката, и декодер от 5 до 32, за да се избере кой регистър да получава изхода от ALU.

ALU е реализиран комбинирано със селектор (ALUSel) за операции като събиране, изваждане, XOR, OR, AND, премествания (SLL, SRL, SRA) и сравнения (LT, LTU, EQ, GE, GEU, NE)За да се спестят LUT-ите в FPGA, популярна техника е да се реализират 1-битови отмествания и да се повтарят N цикъла, използвайки машината на състоянията; това увеличава латентността, но... потреблението на ресурси е намалено.

Управлението е артикулирано с мултиплексори за ALU входове (ALUIn1/2 и ALUSel), избор на целеви регистър (RegSelForALUOut), сигнали към контролера на паметта (MCWidthIn, MCAddressIn, MCStartIn, MCWEIn, MCExtendSignIn, MCDataIn) и специални регистри PC, IR и брояч за броене на отмествания. Всичко това се управлява от машина на състоянията с ~23 състояния.

Ключова концепция в този FSM е „забавено зареждане“: Ефектът от избирането на MUX вход се материализира на следващия фронт на тактовия импулс.Например, когато зарежда IR с инструкцията, пристигаща от паметта, последователността преминава през състоянията на извличане (стартиране на четене на адрес PC), изчакване на ReadyOut, преместване на DataOut към IR и, в следващия цикъл, декодиране и изпълнение.

Типичният път за извличане: при нулиране форсирате PC=RESET_VECTOR (0x00000000), след което конфигурирате драйвера да чете 4 байта на адрес PC, Изчаква се ReadyOut и IR се зареждаОттам, различните състояния управляват едноциклови ALU, многоциклови смени, зареждания/запазвания, разклонения, преходи и „специални“ (обучителната реализация може да накара ebreak да спре процесора нарочно).

Компилирайте реален код и го изпълнете на вашия RISC-V

Много образователен начин за „доказателство на концепцията“ е да се компилира C/C++ програма с крос компилатора riscv32-none-elf, генериране на двоичен файл и неговото прехвърляне във VHDL ROMСлед това симулирате в GHDL и анализирате сигналите в GtkWave; ако всичко върви добре, синтезирате в FPGA и виждате как системата работи в силиций.

Първо, линкер скрипт, адаптиран към вашата карта: ROM от 0x00000000 до 0x00000FFF, GPIO на 0x00001000 и RAM от 0x00001001 до 0x00001FFF. За по-лесно можете да поставите .text (включително .startup секция) в ROM и .data в RAM, като пропуснете инициализацията на данните, ако искате първата версия да е по-кратка.

С тази карта, минималистична рутина за първоначално зареждане поставя стека в края на SRAM и извиква main; маркиран като „гол“ и в секцията .startup за да го поставите в RESET_VECTOR. След компилиране, objdump ви позволява да видите действителния ASM, който вашият процесор ще изпълни (lui/addi за изграждане на sp, jal за main и т.н.).

Класически пример за мигач е превключването на бит 0 на картографирания GPIO: кратко изчакване за дебъгване в симулатора (GHDL+GtkWave) и, на реален хардуер, увеличете броя, така че трептенето да е забележимо. Makefile може да създаде .bin файл и скрипт, който преобразува този двоичен файл в ROM initialization.vhd; след интегриране, Компилирате целия VHDL, симулирате и след това синтезирате..

Този подход на обучение работи дори на по-стари FPGA (напр. Intel Cyclone II), където вътрешната RAM памет се извежда с помощта на препоръчителния шаблон и дизайнът може да бъде с около 66% ресурсоефективен. Педагогическата полза е огромна: вижте как компютърът напредва, как се задействат четенията (mcstartin), ReadyOut валидира данните, IR улавя инструкциите и как всеки скок или преход се разпространява през FSM.

Четения, практики и автогрейдер: Пътна карта

В академичната среда е обичайно да има ясни цели: Упражнявайте се с условни изрази и цикли, пишете функции, спазвайки конвенцията и управление на паметта. Ръководствата обикновено предоставят шаблони, симулатор (Jupiter), инструкции за инсталиране и автоградер за корекция.

За да подготвите средата си, приемете заданието в Github Classroom, ако бъдете подканени, клонирайте хранилището и отворете Jupiter. Не забравяйте, че __start трябва да е глобално, че коментарите могат да бъдат # или ;, че има по една инструкция на ред и че трябва да завършите с ecall (код 10 в a0). Компилирайте с F3 и изпълнете тестове. Ако не се стартира, класическото решение е да рестартирате машината.

Що се отнася до очаквания формат на всяко упражнение, много ръководства включват екранни снимки и уточняват: Например, Factor отпечатва делители, разделени с интервали и връща броя; Upper трябва да преглежда низа и да трансформира само малки букви в главни, без да докосва интервали, цифри или препинателни знаци, и да връща оригиналния указател.

Оценката обикновено разпределя точки по серии (10/40/50) и Можете да извършите проверка, за да видите резултата от автогрейдера.Когато сте доволни, направете добавяне/комитиране/пускане и качете URL адреса на хранилището, където е посочено. Тази дисциплина на жизнения цикъл ви кара да свикнете със строгата проверка и доставка.

Още упражнения за укрепване: Фибоначи, Ханой и четене от клавиатура

След като усвоите основите, работете върху три допълнителни класики: fibonacci.s, hanoi.sy системни извиквания.s (или друг вариант, който чете от клавиатурата и повтаря низ).

  • Фибоначи: Можете да го направите рекурсивен или итеративен; ако го направите рекурсивен, Бъдете внимателни с цената и със запазването на ra/s0итеративни упражнения, които ви позволяват да правите цикли и да добавяте.
  • Ханой: Преобразуване на рекурсивната функция в ASM. Запазва контекста и аргументите между извикванията: дисциплинирана рамка на стекаОтпечатва движения „начална точка → крайна точка“ с ecall.
  • Четене и повторение: Прочетете цяло число и низ и отпечатайте низа N пъти. На Юпитер използвайте съответните кодове за е-повикване налични във вашата практика; в Linux подгответе a7 и a0..a2 за четене/запис.

Тези упражнения консолидират предаването на параметри, циклите и входно/изходните операции. Те ви карат да мислите за взаимодействието с околната среда (Jupiter срещу Linux) и структурирайте ASM така, че да бъде четим и лесен за поддръжка.

Фини детайли на имплементацията: регистри, АЛУ и състояния

Връщайки се към образователното ядро ​​на RV32I, си струва да прегледаме няколко фини детайла, които съобразяват това, което виждате при програмиране, с начина, по който се изпълнява хардуерът: таблицата с операции на ALU избрани от ALUSel (ADD, SUB, XOR, OR, AND, SLL, SRL, SRA, сравнения със знак и без знак), „идентичността“ като случай по подразбиране и „трикът“ с използването на брояч за натрупване на многоциклови отмествания.

Регистровата логика с generate произвежда 5→32 декодер и случаят RegSelForALUOut=00000 не прави нищо (x0 не може да се записва, винаги е равно на нула). PC, IR и Counter имат свои собствени MUX-ове, оркестрирани от FSM: от нулиране, извличане, декодиране/изпълнение (едноциклови ALU или цикли на изместване), зареждания/запазвания, условни разклонения, jal/jalr и специални операции като ebreak.

При достъп до паметта с данни, координацията между MUX→Controller е от съществено значение: MCWidthIn (8/16/32 бита), MCWEIn (R/W), MCAddressIn (от регистри или PC), MCExtendSignIn (за подписан LB/LH) и MCStartIn. Само когато ReadyOut=1, трябва да заснемате DataOut и предварително състояние. Това синхронизира вашия начин на мислене като ASM програмист с реалността на хардуера в реалността.

Всичко това е пряко свързано с това, което наблюдавате в симулацията: всеки път, когато компютърът напредва, задейства се четене на инструкция, MCReadyOut ви казва, че можете да заредите IR и оттам нататък инструкцията влиза в сила (напр. «lui x2,0x2», последвано от «addi x2,x2,-4» за подготовка на sp, «jal x1,…», за извикване на main). Виждането на това в GtkWave е много пристрастяващо.

Ресурси, зависимости и последни съвети

За да възпроизведете това преживяване, ви трябват няколко зависимости: GHDL за компилиране на VHDL и GtkWave за анализ на сигналиЗа крос-компилатора, всеки GCC riscv32-none-elf ще свърши работа (можете да компилирате свой собствен или да инсталирате предварително изграден). За да пренесете ядрото към FPGA, използвайте средата на производителя (напр. Quartus на Intel/Altera) или безплатни инструменти, съвместими с вашето устройство.

Освен това си струва да прочетете ръководствата и бележките за RISC-V (напр. практически инструкции и зелени карти), да се консултирате книги за програмиранеи практикувайте с Лаборатории, включително Jupiter и AutograderПоддържайте рутина: планирайте, внедрявайте, тествайте с гранични случаи и след това интегрирайте в по-големи проекти (като Blinker на FPGA).

С цялата тази информация вече имате основните неща, за да започнете: защо се използва асемблер, а не машинен код, как да настроите среда с Jupiter или Linux, шаблони за цикли, условни изрази и функции с правилно обработване на стека и прозорец към хардуерната имплементация, за да се разбере по-добре какво се случва, когато се изпълнява всяка инструкция.

Ако ученето чрез действие е вашето призвание, започнете с Отрицателен, Делител и Горна граница, след това преминете към Фибоначи/Ханой и програма за четене на отпечатъци от клавиатура. Когато се почувствате комфортно, компилирайте прост C++ код, дампнете ROM файла във VHDL, симулирайте в GHDL и след това преминете към FPGA. Това е пътешествие от по-малко към повече, в което всяка част се вписва със следващата., а удовлетворението да видиш как твоят собствен код движи GPIO или мига светодиод е безценно.

най-добрите книги за програмиране
Свързана статия:
Най-добрите книги за програмиране за всеки език за програмиране