מטרות המדריך הזה הן:
- מידע על המבנה והתחביר של קובץ התיאור בפורמט בינארי.
- איך מתבצע ההתאמה בין תיאור הפורמט הבינארי לתיאור ה-ISA
- כתיבת התיאורים הבינאריים של קבוצת המשנה של ההוראות RiscV RV32I.
סקירה כללית
קידוד של הוראות בינאריות ב-RiscV
קידוד הוראות בינאריות הוא הדרך הרגילה לקידוד הוראות לביצוע במעבד מיקרו. בדרך כלל הם מאוחסנים בקובץ הפעלה, בדרך כלל בפורמט ELF. ההוראות יכולות להיות ברוחב קבוע או ברוחב משתנה.
בדרך כלל, ההוראות משתמשות בקבוצה קטנה של פורמטים לקידוד, וכל פורמט מותאם אישית לסוג ההוראות שמקודדות. לדוגמה, הוראות של רגולטור לרגולטור עשויות להשתמש בפורמט אחד שמגדיל את מספר קוד הפקודות הזמין, בעוד שהוראות של רגולטור ל-immediate משתמשות בפורמט אחר שמקטין את מספר קוד הפקודות הזמין כדי להגדיל את הגודל של ה-immediate שאפשר לקודד. כמעט תמיד, הוראות ההסתעפות והקפיצה משתמשות בפורמטים שמגדילים את הגודל של המאפיין המיידי כדי לתמוך בהסתעפויות עם סטיות גדולות יותר.
פורמטים ההוראות שבהם נעשה שימוש בהוראות שאנחנו רוצים לפענח בסימולטור RiscV הם:
פורמט מסוג R, המשמש להוראות של רגיסטרים לרגיסטרים:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | rd | קוד פעולה |
פורמט מסוג I, המשמש להוראות של זיכרון מיידי, להוראות טעינה ולהוראה jalr
, 12 ביט מיידיים.
31..20 | 19..15 | 14..12 | 11.7 | 6.0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | קוד פעולה |
פורמט I-Type מיוחד, המשמש למשמרת עם הוראות מיידיות, 5 ביט מיידי:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | rd | קוד פעולה |
פורמט מסוג U, המשמש להוראות מיידיות ארוכות (lui
, auipc
), מיידיות של 20 ביט:
31..12 | 11.7 | 6.0 |
---|---|---|
20 | 5 | 7 |
uimm20 | rd | קוד פעולה |
פורמט מסוג B, המשמש להסתעפויות מותנות, מיידיות של 12 ביט.
31 | 30..25 | 24..20 | 19..15 | 14..12 | 11..8 | 7 | 6..0 |
---|---|---|---|---|---|---|---|
1 | 6 | 5 | 5 | 3 | 4 | 1 | 7 |
imm | imm | rs2 | rs1 | func3 | imm | imm | קוד פעולה |
פורמט J-Type, משמש להוראה בנושא jal
, 20 ביט מיידי.
31 | 30..21 | 20 | 19..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
imm | imm | imm | imm | rd | קוד פעולה |
פורמט מסוג S, המשמש להוראות אחסון, מיידיות באורך 12 ביט.
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm | rs2 | rs1 | func3 | imm | קוד פעולה |
כפי שאפשר לראות מהפורמטים האלה, כל ההוראות האלה באורך 32 ביט, ו-7 הביטים הנמוכים בכל פורמט הם שדה קוד הפקודה. שימו לב גם שלמרות שלכמה פורמטים יש ערכים מיידיים באותו גודל, הביטים שלהם נלקחים מחלקים שונים של ההוראה. כפי שנראה בהמשך, פורמט המפרט של המפענח הבינארי יכול לבטא זאת.
תיאור הקידוד הבינארי
הקידוד הבינארי של ההוראה מבוטא בקובץ התיאור בפורמט בינארי (.bin_fmt
). הוא מתאר את הקידוד הבינארי של ההוראות ב-ISA כך שניתן יהיה ליצור מפענח הוראות בפורמט בינארי. המפענח שנוצר קובע את קוד הפקודה, מחלץ את הערך של שדות המשתנה והשדה המיידי, כדי לספק את המידע הנדרש למפענח ISA שאינו תלוי בקידוד שמתואר במדריך הקודם.
במדריך הזה נכתוב קובץ תיאור של קידוד בינארי לקבוצת משנה של ההוראות של RiscV32I שנדרשות כדי לדמות את ההוראות שבתוכנית 'Hello World' הקטנה. פרטים נוספים על ISA של RiscV זמינים במפרטי Risc-V{.external}.
קודם פותחים את הקובץ:
riscv_bin_decoder/riscv32i.bin_fmt
.
תוכן הקובץ מחולק לכמה חלקים.
הראשונה היא ההגדרה של decoder
.
decoder RiscV32I {
// The namespace in which code will be generated.
namespace mpact::sim::codelab;
// The name (including any namespace qualifiers) of the opcode enum type.
opcode_enum = "OpcodeEnum";
// Include files specific to this decoder.
includes {
#include "riscv_isa_decoder/solution/riscv32i_decoder.h"
}
// Instruction groups for which to generate decode functions.
RiscVInst32;
};
הגדרת המפענח שלנו מציינת את השם של המפענח RiscV32I
, וגם ארבעה פרטי מידע נוספים. הראשון הוא namespace
, שמגדיר את מרחב השמות שבו הקוד שנוצר יופיע. השני הוא opcode_enum
, שמציין איך צריך להפנות לסוג המניין של קוד הפקודה שנוצר על ידי מפענח ה-ISA בתוך הקוד שנוצר. השלישי: includes {}
מציין את קובצי ה-include שדרושים לקוד שנוצר בשביל המפענח הזה.
במקרה שלנו, זהו הקובץ שנוצר על ידי מפענח ה-ISA מהמדריך הקודם.
אפשר לציין קובצי include נוספים בהגדרת includes {}
ברמת ה-global. האפשרות הזו שימושית אם מוגדרים כמה מפענחים, וכולם צריכים לכלול חלק מאותם קבצים. הרביעית היא רשימת השמות של קבוצות ההוראות שמהן מורכב המפענח. במקרה שלנו יש רק אחד: RiscVInst32
.
לאחר מכן מופיעות שלוש הגדרות פורמט. אלה מייצגים פורמטים שונים של הוראות למילה של 32 ביט שמשמשות את ההוראות שכבר מוגדרות בקובץ.
// The generic RiscV 32 bit instruction format.
format Inst32Format[32] {
fields:
unsigned bits[25];
unsigned opcode[7];
};
// RiscV 32 bit instruction format used by a number of instructions
// needing a 12 bit immediate, including CSR instructions.
format IType[32] : Inst32Format {
fields:
signed imm12[12];
unsigned rs1[5];
unsigned func3[3];
unsigned rd[5];
unsigned opcode[7];
};
// RiscV instruction format used by fence instructions.
format Fence[32] : Inst32Format {
fields:
unsigned fm[4];
unsigned pred[4];
unsigned succ[4];
unsigned rs1[5];
unsigned func3[3];
unsigned rd[5];
unsigned opcode[7];
};
הראשון מגדיר פורמט של הוראות ברוחב 32 ביט בשם Inst32Format
, שיש לו שני שדות: bits
(רוחב 25 ביט) ו-opcode
(רוחב 7 ביט). כל שדה הוא unsigned
, כלומר הערך יורחב לאפס אחרי חילוץ הנתונים וימוקם בסוג מספר שלם C++. סכום מידות הרוחב של שדות הביטים חייב להיות שווה לרוחב הפורמט. הכלי יפיק שגיאה אם יש מחלוקת. הפורמט הזה לא נגזר מפורמט אחר, ולכן הוא נחשב לפורמט ברמה העליונה.
השיטה השנייה מגדירה פורמט הוראה רחב ב-32 ביט בשם IType
שנגזר מ-Inst32Format
, ושני הפורמטים האלה קשורים. הפורמט מכיל 5 שדות: imm12
, rs1
, func3
, rd
ו-opcode
. השדה imm12
הוא signed
, כלומר הערך יורחב לאחר חילוץ הערך וימוקם מסוג מספר שלם C++. שימו לב של-IType.opcode
יש את אותו מאפיין signed/unsigned, והוא מתייחס לאותם ביטים של מילה של הוראה כמו Inst32Format.opcode
.
הפורמט השלישי הוא פורמט מותאם אישית שמשמש רק את ההוראה fence
. זוהי הוראה שכבר צוינה, ולא צריך להדאיג לגביה במדריך הזה.
נקודה חשובה: אפשר לעשות שימוש חוזר בשמות שדות בפורמטים קשורים שונים, כל עוד הם מייצגים את אותם ביטים ויש להם את אותו מאפיין חתום/לא חתום.
אחרי הגדרות הפורמט ב-riscv32i.bin_fmt
מופיעה הגדרה של קבוצת הוראות. לכל ההוראות בקבוצת הוראות צריך להיות אותו אורך בייט, והן צריכות להשתמש בפורמט שמבוסס (אולי באופן עקיף) על אותו פורמט של הוראות ברמה העליונה. כש-ISA יכול לכלול הוראות באורך שונה, נעשה שימוש בקבוצת הוראות שונה לכל אורך. בנוסף, אם פענוח ה-ISA של היעד תלוי במצב ביצוע, כמו הוראות Arm לעומת הוראות Thumb, נדרשת קבוצת הוראות נפרדת לכל מצב. מנתח bin_fmt
יוצר מפענח בינארי לכל קבוצת הוראות.
instruction group RiscV32I[32] "OpcodeEnum" : Inst32Format {
fence : Fence : func3 == 0b000, opcode == 0b000'1111;
csrs : IType : func3 == 0b010, rs1 != 0, opcode == 0b111'0011;
csrw_nr : IType : func3 == 0b001, rd == 0, opcode == 0b111'0011;
csrs_nw : IType : func3 == 0b010, rs1 == 0, opcode == 0b111'0011;
};
קבוצת ההוראות מגדירה שם RiscV32I
, רוחב [32]
, את השם של סוג המניין של opcode שבו רוצים להשתמש "OpcodeEnum"
ותבנית של הוראה בסיסית. סוג ספירת ה-opcode צריך להיות זהה לזה שנוצר על ידי מפענח ההוראה העצמאית של הפורמט, שמופיע במדריך במפענח ISA.
כל תיאור של קידוד הוראות מורכב מ-3 חלקים:
- שם קוד הפקודה, שצריך להיות זהה לשם שצוין בתיאור של מפענח ההוראות כדי שהשניים יפעלו יחד.
- פורמט ההוראה שבו יש להשתמש עבור קוד הפקודה. זהו הפורמט שמשמש כדי לספק הפניות לשדות ביטים בחלק האחרון.
- רשימה מופרדת בפסיקים של אילוצים של שדות ביט,
==
,!=
,<
,<=
,>
ו->=
, שכל אחד מהם חייב להיות נכון כדי שקוד הפעולה יתאים למילה המבצעת.
מנתח ה-.bin_fmt
משתמש בכל המידע הזה כדי ליצור מפענח שמבצע את הפעולות הבאות:
- הפונקציה מספקת פונקציות חילוץ (signed/unsigned) בהתאם לכל שדה ביט בכל פורמט. פונקציות החילוץ ממוקמות במרחבי שמות בעלי שם בגרסת אות הנחש של שם הפורמט. לדוגמה, פונקציות החילוץ של הפורמט
IType
ממוקמות במרחב השמותi_type
. כל פונקציית חילוץ מוצהרת כ-inline
, מקבלת את סוגuint_t
הצר ביותר שמכיל את רוחב הפורמט ומחזירה את סוגint_t
(לסימון) אוuint_t
(ללא סימון) הצר ביותר שמכיל את רוחב השדה שחולץ. E.g.:
inline uint8_t ExtractOpcode(uint32_t value) {
return value & 0x7f;
}
- פונקציית פענוח לכל קבוצת הוראות. הפונקציה מחזירה ערך מסוג
OpcodeEnum
, ומשתמשת בסוגuint_t
הצר ביותר שמכיל את הרוחב של פורמט קבוצת ההוראות.
ביצוע build ראשוני
עוברים לספרייה riscv_bin_decoder
ובונים את הפרויקט באמצעות הפקודה הבאה:
$ cd riscv_bin_decoder
$ bazel build :all
עכשיו עוברים בספרייה חזרה לשורש המאגר, בודקים את המקורות שנוצרו. לשם כך, צריך לשנות את הספרייה ל-bazel-out/k8-fastbuild/bin/riscv_bin_decoder
(בהנחה שאתם נמצאים במארח x86 – למארחים אחרים, k8-fastbuild יהיה מחרוזת נוספת).
$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_bin_decoder
riscv32i_bin_decoder.h
riscv32i_bin_decoder.cc
קובץ הכותרת שנוצר (.h)
פותחים את riscv32i_bin_decoder.h
. החלק הראשון של הקובץ מכיל הגנה סטנדרטית, כולל קבצים והצהרות ממרחב שמות. לכן יש פונקציית עזר בתבנית במרחב השמות internal
. הפונקציה הזו משמשת לחילוץ שדות ביט מפורמטים ארוכים מדי מכדי שיוכלו להתאים למספר שלם של 64 ביט ב-C++.
#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H
#include <iostream>
#include <cstdint>
#include "third_party/absl/functional/any_invocable.h"
#include "learning/brain/research/mpact/sim/codelab/riscv_isa_decoder/solution/riscv32i_decoder.h"
namespace mpact {
namespace sim {
namespace codelab {
namespace internal {
template <typename T>
static inline T ExtractBits(const uint8_t *data, int data_size,
int bit_index, int width) {
if (width == 0) return 0;
int byte_pos = bit_index >> 3;
int end_byte = (bit_index + width - 1) >> 3;
int start_bit = bit_index & 0x7;
// If it is only from one byte, extract and return.
if (byte_pos == end_byte) {
uint8_t mask = 0xff >> start_bit;
return (mask & data[byte_pos]) >> (8 - start_bit - width);
}
// Extract from the first byte.
T val = 0;
val = data[byte_pos++] & 0xff >> start_bit;
int remainder = width - (8 - start_bit);
while (remainder >= 8) {
val = (val << 8) | data[byte_pos++];
remainder -= 8;
}
// Extract any remaining bits.
if (remainder > 0) {
val <<= remainder;
int shift = 8 - remainder;
uint8_t mask = 0b1111'1111 << shift;
val |= (data[byte_pos] & mask) >> shift;
}
return val;
}
} // namespace internal
אחרי הקטע הראשוני מופיעה קבוצה של שלושה מרחבי שמות, אחד לכל אחת מההצהרות format
בקובץ riscv32i.bin_fmt
:
namespace fence {
...
} // namespace fence
namespace i_type {
...
} // namespace i_type
namespace inst32_format {
...
} // namespace inst32_format
בכל אחד ממרחב השמות האלה מוגדרת פונקציית החילוץ של שדה הביטים inline
לכל שדה ביטים בפורמט הזה. בנוסף, בפורמט הבסיס יש כפילויות של פונקציות חילוץ מהפורמטים הצאצאים שבהם: 1) שמות השדות מופיעים רק בשם שדה אחד, או 2) שמות השדות מתייחסים לאותו שדה טיפוס (signed/unsigned ומיקומי ביט) בכל פורמט שבו הם מופיעים. כך ניתן לחלץ שדות ביט שמתארים את אותן ביטים באמצעות פונקציות במרחב השמות של הפורמט ברמה העליונה.
הפונקציות במרחב השמות i_type
מוצגות למטה:
namespace i_type {
inline uint8_t ExtractFunc3(uint32_t value) {
return (value >> 12) & 0x7;
}
inline int16_t ExtractImm12(uint32_t value) {
int16_t result = ( (value >> 20) & 0xfff) << 4;
result = result >> 4;
return result;
}
inline uint8_t ExtractOpcode(uint32_t value) {
return value & 0x7f;
}
inline uint8_t ExtractRd(uint32_t value) {
return (value >> 7) & 0x1f;
}
inline uint8_t ExtractRs1(uint32_t value) {
return (value >> 15) & 0x1f;
}
} // namespace i_type
לבסוף, מופיעה ההצהרה על הפונקציה של פונקציית הפענוח של קבוצת ההוראות RiscVInst32
. הפונקציה מקבלת ערך של 32 ביט ללא סימן בתור הערך של
מילת ההוראה ומחזירה את חבר סיווג המניין OpcodeEnum
שתואם, או את הערך OpcodeEnum::kNone
אם אין התאמה.
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
קובץ המקור שנוצר (.cc)
עכשיו פותחים את riscv32i_bin_decoder.cc
. החלק הראשון של הקובץ מכיל את ההצהרות על #include
ועל מרחב השמות, ואחריו ההצהרות על פונקציית הפענוח:
#include "riscv32i_bin_decoder.h"
namespace mpact {
namespace sim {
namespace codelab {
OpcodeEnum DecodeRiscVInst32None(uint32_t);
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word);
השדה DecodeRiscVInst32None
משמש לפעולות פענוח ריקות, כלומר הפעולות שמחזירות OpcodeEnum::kNone
. שלוש הפונקציות האחרות מהוות את המפענח שנוצר. המפענח הכולל פועל באופן היררכי. קבוצה של ביטים במילה של ההוראה מחושבת כדי להבדיל בין הוראות או קבוצות של הוראות ברמה העליונה. הביטים לא חייבים להיות רצופים. מספר הביטים קובע את הגודל של טבלת המיפוי שמאוכלסת בפונקציות של מפענח ברמה שנייה. הדבר מופיע בקטע הבא בקובץ:
absl::AnyInvocable<OpcodeEnum(uint32_t)> parse_group_RiscVInst32_0[kParseGroupRiscVInst32_0_Size] = {
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
&DecodeRiscVInst32None, &DecodeRiscVInst32_0_3,
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
...
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
&DecodeRiscVInst32_0_3c, &DecodeRiscVInst32None,
...
};
לבסוף, מגדירים את פונקציות המפענח:
OpcodeEnum DecodeRiscVInst32None(uint32_t) {
return OpcodeEnum::kNone;
}
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word) {
if ((inst_word & 0x4003) != 0x3) return OpcodeEnum::kNone;
uint32_t index;
index = (inst_word >> 2) & 0x1f;
index |= (inst_word >> 7) & 0x60;
return parse_group_RiscVInst32_0[index](inst_word);
}
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word) {
return OpcodeEnum::kFence;
}
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word) {
if ((inst_word & 0xf80) != 0x0) return OpcodeEnum::kNone;
return OpcodeEnum::kCsrwNr;
}
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word) {
uint32_t rs1_value = (inst_word >> 15) & 0x1f;
if (rs1_value != 0x0)
return OpcodeEnum::kCsrs;
if (rs1_value == 0x0)
return OpcodeEnum::kCsrsNw;
return OpcodeEnum::kNone;
}
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word) {
OpcodeEnum opcode;
opcode = DecodeRiscVInst32_0(inst_word);
return opcode;
}
במקרה הזה, שבו מוגדרות רק 4 הוראות, יש רק רמה אחת של פענוח וטבלת חיפוש דלילה מאוד. כשמוסיפים הוראות, המבנה של המפענח משתנה ומספר הרמות בהיררכיית טבלת המפענח עשוי לגדול.
הוספת הוראות ALU מסוג register-register
עכשיו הגיע הזמן להוסיף כמה הוראות חדשות לקובץ riscv32i.bin_fmt
. הקבוצה הראשונה של ההוראות היא הוראות ALU לרישום רישום, כמו add
, and
וכו'. ב-RiscV32, כולן משתמשות בפורמט בינארי מסוג R:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | rd | קוד פעולה |
השלב הראשון הוא להוסיף את הפורמט. פותחים את הקובץ riscv32i.bin_fmt
בכלי העריכה המועדף עליכם. אחרי ה-Inst32Format
אפשר להוסיף פורמט שנקרא RType
שמבוסס על Inst32Format
. כל שדות הבייטים ב-RType
הם unsigned
. כדי להגדיר את הפורמט, משתמשים בשמות, ברוחב הביטים ובסדר (משמאל לימין) שמופיעים בטבלה שלמעלה. לקבלת רמז או כדי לראות את הפתרון המלא, לחצו כאן.
בשלב הבא צריך להוסיף את ההוראות. ההוראות הן:
add
– הוספת מספר שלם.and
– AND ברמת הביטים.or
– או ברמת הביטים.sll
– shift שמאלי לוגי.sltu
– מוגדר פחות מ-, ללא סימן.sub
– הפחתה של מספר שלם.xor
– XOR ברמת הביטים.
הקידודים שלהם:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 | שם קוד הפעולה |
---|---|---|---|---|---|---|
000 0000 | rs2 | rs1 | 000 | rd | 011 0011 | add |
000 0000 | rs2 | rs1 | 111 | rd | 011 0011 | וגם |
000 0000 | rs2 | rs1 | 110 | rd | 011 0011 | או |
000 0000 | rs2 | rs1 | 001 | rd | 011 0011 | sll |
000 0000 | rs2 | rs1 | 011 | rd | 011 0011 | sltu |
010 0000 | rs2 | rs1 | 000 | rd | 011 0011 | sub |
000 0000 | rs2 | rs1 | 100 | שלישי | 011 0011 | xor |
func7 | func3 | קוד פעולה |
מוסיפים את הגדרות ההוראות האלה לפני שאר ההוראות בקבוצת ההוראות RiscVInst32
. מחרוזות בינאריות מצוינות עם קידומת 0b
(בדומה ל-0x
למספרים הקסדצימליים). כדי שיהיה קל יותר לקרוא מחרוזות ארוכות של ספרות בינאריות, אפשר גם להוסיף את גרש בודד '
כמפריד ספרה במקום המתאים.
בכל אחת מהגדרות ההוראות האלה יהיו שלושה אילוצים, כלומר func7
, func3
ו-opcode
. לכל הערכים מלבד sub
, האילוץ func7
יהיה:
func7 == 0b000'0000
האילוץ func3
משתנה ברוב ההוראות. עבור add
ו-sub
, הערך הוא:
func3 == 0b000
האילוץ opcode
זהה בכל אחת מההוראות האלה:
opcode == 0b011'0011
חשוב לסיים כל שורה בנקודה-פסיק ;
.
הפתרון המוגמר נמצא כאן.
עכשיו אפשר לבנות את הפרויקט כמו קודם ולפתוח את הקובץ riscv32i_bin_decoder.cc
שנוצר. תוכלו לראות שנוצרו פונקציות נוספות של מקודדים כדי לטפל בהוראות החדשות. ברוב המקרים הם דומים לאלה שנוצרו קודם, אבל כדאי להסתכל על DecodeRiscVInst32_0_c
שמשמש לפענוח של add
/sub
:
OpcodeEnum DecodeRiscVInst32_0_c(uint32_t inst_word) {
static constexpr OpcodeEnum opcodes[2] = {
OpcodeEnum::kAdd,
OpcodeEnum::kSub,
};
if ((inst_word & 0xbe000000) != 0x0) return OpcodeEnum::kNone;
uint32_t index;
index = (inst_word >> 30) & 0x1;
return opcodes[index];
}
בפונקציה הזו נוצרת טבלת פענוח סטטית, וערך חיפוש מחולץ ממילת ההוראה כדי לבחור את האינדקס המתאים. כך מתווספת שכבה שנייה בהיררכיית המפענח של ההוראות, אבל מכיוון שאפשר לחפש את קוד הפקודה ישירות בטבלה בלי השוואות נוספות, הוא מוטמע בפונקציה הזו במקום לדרוש קריאה נוספת לפונקציה.
הוספת הוראות ALU עם ערכים מיידיים
קבוצת ההוראות הבאה שנוסיף היא הוראות ALU שמשתמשות בערך מיידי במקום באחד מהמרשם. יש שלוש קבוצות של ההוראות האלה (על סמך השדה המיידי): ההוראות המיידיות מסוג I-Type עם חתימה מיידית של 12 ביט, ההוראות המיידיות מסוג I-Type מיידיות לשינויי כוונון, ו-U-Type מיידי, עם ערך מיידי של 20 ביט ללא חתימה. הפורמטים מפורטים בהמשך:
הפורמט המיידי של I-Type:
31..20 | 19..15 | 14..12 | 11.7 | 6.0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | קוד פעולה |
הפורמט המיידי המיוחד של I-Type:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | rd | קוד פעולה |
הפורמט המיידי מסוג U:
31..12 | 11.7 | 6.0 |
---|---|---|
20 | 5 | 7 |
uimm20 | rd | קוד פעולה |
הפורמט I-Type כבר קיים ב-riscv32i.bin_fmt
, כך שאין צורך להוסיף את הפורמט הזה.
אם משווים בין הפורמט המיוחד של סוג I לבין הפורמט של סוג R שהגדרנו בתרגיל הקודם, רואים שההבדל היחיד הוא ששדה rs2
עבר לשם uimm5
. במקום להוסיף פורמט חדש לגמרי, אפשר להרחיב את הפורמט R-Type. אי אפשר להוסיף עוד שדה, כי זה יגדיל את רוחב הפורמט, אבל אפשר להוסיף שכבת-על. שכבת-על היא כינוי לקבוצת ביטים בפורמט, וניתן להשתמש בה כדי לשלב כמה רצפי משנה של הפורמט לישות נפרדת בעלת שם. תופעת הלוואי היא שהקוד שנוצר יכלול עכשיו גם פונקציית חילוץ לשכבת-העל, בנוסף לפונקציות החילוץ של השדות. במקרה הזה, אם גם rs2
וגם uimm5
לא חתומים אין הבדל משמעותי, חוץ מאשר לציין במפורש שהשדה משמש כפעולה מיידית. כדי להוסיף שכבת-על בשם uimm5
לפורמט מסוג R, מוסיפים את הטקסט הבא אחרי השדה האחרון:
overlays:
unsigned uimm5[5] = rs2;
הפורמט החדש היחיד שצריך להוסיף הוא פורמט מסוג U. לפני שמוסיפים את הפורמט, נבחן את שתי ההוראות שמשתמשות בפורמט הזה: auipc
ו-lui
. בשתי השיטות האלה, הערך המיידי של 20 הביטים מוסט 12 ביט שמאלה לפני שמשתמשים בו כדי להוסיף את ה-PC (auipc
) או לכתוב אותו ישירות ברגולטור (lui
). באמצעות שכבת-על, אנחנו יכולים לספק גרסה של הערך המיידי שהוסט מראש, וכך להעביר חלק מהחישובים מהרצת ההוראות לפענוח ההוראות. קודם מוסיפים את הפורמט בהתאם לשדות שצוינו בטבלה שלמעלה. לאחר מכן נוכל להוסיף את שכבת-העל הבאה:
overlays:
unsigned uimm32[32] = uimm20, 0b0000'0000'0000;
התחביר של שכבת-העל מאפשר לנו לשרשר לא רק שדות, אלא גם ליטרלים. במקרה כזה, אנחנו מקשרים אותו ל-12 אפסים, ובעצם מעבירים אותו 12 ספרות שמאלה.
אלה ההוראות מסוג I שצריך להוסיף:
addi
– הוספה מיידית.andi
– סיביות ובאופן מיידי.ori
– ברמת הביטים או עם מיידיות.xori
– XOR בינארי עם ערך מיידי.
הקידודים שלהם:
31..20 | 19..15 | 14..12 | 11.7 | 6.0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | rd | 001 0011 | addi |
imm12 | rs1 | 111 | rd | 001 0011 | andi |
imm12 | rs1 | 110 | rd | 001 0011 | אורי |
imm12 | rs1 | 100 | שלישי | 001 0011 | Xori |
func3 | קוד פעולה |
הוראות ה-R-Type (הוראות I-Type מיוחדות) שצריך להוסיף הן:
slli
– שינוי היגיון שמאלה באופן מיידי.srai
- שינוי שיטת החשבון הימנית באופן מיידי.srli
– שינוי הלוגיקה הימנית מייד.
הקידודים שלהם הם:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 | שם קוד הפעולה |
---|---|---|---|---|---|---|
000 0000 | uimm5 | rs1 | 001 | rd | 001 0011 | slli |
010 0000 | uimm5 | rs1 | 101 | rd | 001 0011 | srai |
000 0000 | uimm5 | rs1 | 101 | rd | 001 0011 | srli |
func7 | func3 | קוד פעולה |
ההוראות מסוג U שעלינו להוסיף הן:
auipc
– הוספת 'מיידי עליון' למחשב.lui
– טעינה מיידית של נתונים מהזיכרון העליון.
הקידודים שלהם הם:
31..12 | 11.7 | 6..0 | שם opcode |
---|---|---|---|
uimm20 | rd | 001 0111 | auipc |
uimm20 | שלישי | 011 0111 | lui |
קוד פעולה |
מבצעים את השינויים הרצויים ומבצעים build. בודקים את הפלט שנוצר. בדיוק כמו קודם, אתם יכולים לבדוק את העבודה שלכם מול riscv32i.bin_fmt.
הוספת הוראות הסתעפות וקפיצה וקישור
קבוצת ההוראות הבאה שצריך להגדיר היא הוראות ההסתעפות מותנות, הוראה של קפיצה וקישור והוראה של קפיצה וקישור למרשם.
כל ההסתעפויות המותנות שאנחנו מוסיפים משתמשות בקידוד מסוג B.
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm7 | rs2 | rs1 | func3 | imm5 | קוד פעולה |
אמנם הקידוד B-Type זהה בפריסה לקידוד R-Type, אבל אנחנו בוחרים להשתמש בסוג פורמט חדש כדי שיתאים למסמכי התיעוד של RiscV.
אבל אפשר גם להוסיף שכבת-על כדי לקבל את ההזזה המתאימה של ההסתעפות באופן מיידי, באמצעות השדות func7
ו-rd
של קידוד R-Type.
צריך להוסיף פורמט BType
עם השדות שצוינו למעלה, אבל זה לא מספיק. כפי שאפשר לראות, הנתון המיידי מפוצל לשני שדות הוראות.
בנוסף, הוראות ההסתעפות לא מתייחסות לכך כשרשור פשוט של שני השדות. במקום זאת, כל שדה מחולק למחיצות נוספות, והמחיצות האלה משורשרות בסדר שונה. לבסוף, הערך הזה זז שמאלה באחד כדי לקבל היסט מיושר של 16 ביט.
רצף הביטים במילה של ההוראה שמשמשת ליצירת הערך המיידי הוא: 31, 7, 30..25, 11..8. זה תואם להפניות הבאות לשדות משנה, שבהן האינדקס או הטווח מציינים את הביטים בשדה, שממוספרים מימין לשמאל, כלומר:
imm7[6]
מתייחס ל-msb של imm7
, ו-imm5[0]
מתייחס ל-lsb של imm5
.
imm7[6], imm5[0], imm7[5..0], imm5[4..1]
לשילוב של מניפולציית הביטים הזו בהוראות ההסתעפות עצמן יש שני חסרונות גדולים. קודם כול, הוא מקשר את ההטמעה של הפונקציה הסמנטית לפרטים בייצוג של ההוראות הבינאריות. שנית, היא מוסיפה עוד עלות ריצה. התשובה היא להוסיף שכבת-על לפורמט BType
, כולל '0' בסוף כדי להביא בחשבון את ההזזה ימינה.
overlays:
signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;
שימו לב שהשכבה העליונה חתומה, כך שהיא תקבל הרחבה אוטומטית של הסימן כשהיא תוחזר ממילה הפקודה.
ההוראה 'קפיצה וקישור' (מיידית) משתמשת בקידוד מסוג J:
31..12 | 11.7 | 6.0 |
---|---|---|
20 | 5 | 7 |
imm20 | rd | קוד פעולה |
גם הפורמט הזה קל להוספה, אבל שוב, המאפיין המיידי שבו משתמשת ההוראה לא פשוט כמו שהוא נראה. רצפי הביטים שמשמשים ליצירת המאפיין המיידי המלא הם: 31, 19..12, 20, 30..21, והמאפיין המיידי הסופי מוסט שמאלה באחד לצורך התאמה למחצית מילה. הפתרון הוא להוסיף שכבת-על נוספת (21 ביט כדי להביא בחשבון את ההזזה ימינה) לפורמט:
overlays:
signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;
כפי שאפשר לראות, התחביר של שכבות-על תומך בציינה של כמה טווחים בשדה בפורמט מקוצר. בנוסף, אם לא משתמשים בשם של שדה, מספרי הביטים מתייחסים למילה של ההוראה עצמה, כך שאפשר לכתוב את המילה שלמעלה גם כך:
signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;
לבסוף, הדילוג והקישור (רישום) משתמש בפורמט I-type שבו השתמשתם בעבר.
הפורמט המיידי של I-Type:
31..20 | 19..15 | 14..12 | 11.7 | 6.0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | קוד פעולה |
הפעם לא צריך לבצע שינוי בפורמט.
הוראות ההסתעפות שצריך להוסיף הן:
beq
– חלוקה אם השוויון מתקיים.bge
– יצירת הסתעפות אם הערך גדול מ- או שווה ל-.bgeu
- הסתעפות אם היא גדולה או שווה ללא חתימה.blt
– הסתעפות אם הערך קטן מ-.bltu
– הסתעפות אם הערך קטן מ-unsigned.bne
– הסתעפות אם הערך לא שווה.
הם מקודדים באופן הבא:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 | שם קוד הפעולה |
---|---|---|---|---|---|---|
imm7 | rs2 | rs1 | 000 | imm5 | 110 0011 | beq |
imm7 | rs2 | rs1 | 101 | imm5 | 110 0011 | bge |
imm7 | rs2 | rs1 | 111 | imm5 | 110 0011 | Bgeu |
imm7 | rs2 | rs1 | 100 | imm5 | 110 0011 | blt |
imm7 | rs2 | rs1 | 110 | imm5 | 110 0011 | bltu |
imm7 | rs2 | rs1 | 001 | imm5 | 110 0011 | bne |
func3 | קוד פעולה |
ההוראה jal
מקודדת באופן הבא:
31..12 | 11.7 | 6..0 | שם opcode |
---|---|---|---|
imm20 | rd | 110 1111 | חלול |
קוד פעולה |
ההוראה jalr
מקודדת באופן הבא:
31..20 | 19..15 | 14..12 | 11.7 | 6.0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | rd | 110 0111 | Jalr |
func3 | קוד פעולה |
מבצעים את השינויים הרצויים ומבצעים build. בודקים את הפלט שנוצר. בדיוק כמו קודם, אפשר לבדוק את העבודה שלכם מול riscv32i.bin_fmt.
הוספת הוראות לחנות
הוראות האחסון משתמשות בקידוד מסוג S, שהוא זהה לקידוד מסוג B שמשמש את הוראות ההסתעפות, מלבד ההרכב של הערך המיידי. בחרנו להוסיף את הפורמט SType
כדי לשמור על תאימות למסמכי העזרה של RiscV.
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm7 | rs2 | rs1 | func3 | imm5 | קוד פעולה |
במקרה של הפורמט SType
, המאפיין המיידי הוא למרבה המזל שרשור ישיר של שני השדות המיידיים, כך שמפרט שכבת-העל הוא פשוט:
overlays:
signed s_imm[12] = imm7, imm5;
שימו לב: אין צורך בפרמטרים של טווח ביטים כשמקשרים בין שדות שלמים.
הוראות החנות מקודדות כך:
31..25 | 24..20 | 19..15 | 14..12 | 11.7 | 6..0 | שם קוד הפעולה |
---|---|---|---|---|---|---|
imm7 | rs2 | rs1 | 000 | imm5 | 010 0011 | sb |
imm7 | rs2 | rs1 | 001 | imm5 | 010 0011 | sh |
imm7 | rs2 | rs1 | 010 | imm5 | 010 0011 | sw |
func3 | קוד פעולה |
מבצעים את השינויים הרצויים ומבצעים build. בודקים את הפלט שנוצר. בדיוק כמו קודם, אפשר לבדוק את העבודה שלכם מול riscv32i.bin_fmt.
הוספת הוראות טעינה
הוראות הטעינה משתמשות בפורמט I-Type. אין צורך לבצע שינויים בנתונים האלה.
הקידודים הם:
31..20 | 19..15 | 14..12 | 11.7 | 6.0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | rd | 000 0011 | lb |
imm12 | rs1 | 100 | שלישי | 000 0011 | lbu |
imm12 | rs1 | 001 | rd | 000 0011 | lh |
imm12 | rs1 | 101 | rd | 000 0011 | lhu |
imm12 | rs1 | 010 | rd | 000 0011 | lw |
func3 | קוד פעולה |
ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. בדיוק כמו קודם, אתם יכולים לבדוק את העבודה מול riscv32i.bin_fmt.
סיימנו את המדריך הזה, אנחנו מקווים שהוא עזר.