מטרות המדריך הזה הן:
- לומדים את המבנה והתחביר של קובץ התיאור בפורמט הבינארי.
- הסבר על האופן שבו התיאור של הפורמט הבינארי תואם לתיאור של ISA
- כותבים את התיאורים הבינאריים לקבוצת המשנה RiscV RV32I של ההוראות.
סקירה כללית
קידוד הוראות בינאריות של RiscV
קידוד הוראה בינארית הוא הדרך הסטנדרטית לקידוד הוראות במיקרו-מעבד. בדרך כלל הם מאוחסנים בקובץ הפעלה, בדרך כלל בפורמט ELF. ההוראות יכולות להיות ברוחב קבוע או משתנה רוחב.
בדרך כלל ההוראות משתמשות בקבוצה קטנה של פורמטים של קידוד, בכל פורמט מותאם אישית לסוג ההוראות המקודדות. לדוגמה, רישום-רישום עשויים להשתמש בפורמט אחד שמגדיל את מספר קודי התפעול הזמינים, ואילו הוראות רישום מיידיות משתמשות באחרת שמחליפה את את ה-opcodes הזמינים להגדלת גודל הקובץ המיידי שאפשר לקודד. הוראות הסתעפות ודילוג כמעט תמיד משתמשות בפורמטים שמגדילים את הגודל המיידית כדי לתמוך בהסתעפויות עם קיזוזים גדולים יותר.
הפורמטים של ההוראות, שבהם נעשה שימוש בהוראות שאנחנו רוצים לפענח ב-RiscV הם:
פורמט R-Type, משמש להוראות רישום:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | שלישי | קוד פעולה |
פורמט I-Type המשמש להוראות מיידיות לרישום, וכן להוראות טעינה
ההוראה jalr
, 12 ביט מיידית.
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | שלישי | קוד פעולה |
פורמט 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 | שלישי | קוד פעולה |
פורמט U-Type, משמש להוראות מיידיות למשך זמן ארוך (lui
, auipc
), 20 ביט
מיידי:
31..12 | 11..7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | שלישי | קוד פעולה |
פורמט B-Type, שמשמש להסתעפויות מותנות, ב-12 ביט מיידי.
31 | 30..25 | 24..20 | 19..15 | 14..12 | 11..8 | 7 | 6..0 |
---|---|---|---|---|---|---|---|
1 | 6 | 5 | 5 | 3 | 4 | 1 | 7 |
IM | IM | rs2 | rs1 | func3 | IM | IM | קוד פעולה |
פורמט J-Type, משמש להוראה בנושא jal
, 20 ביט מיידי.
31 | 30..21 | 20 | 19..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
IM | IM | IM | IM | שלישי | קוד פעולה |
פורמט S-Type, משמש להוראות בחנות, 12 ביט מיידי.
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
IM | rs2 | rs1 | func3 | IM | קוד פעולה |
כפי שניתן לראות בפורמטים האלה, כל ההוראות האלה הן באורך 32 סיביות, 7 הביטים הנמוכים בכל פורמט הוא שדה ה-opcode. כמו כן שימו לב שבזמן יש מספר פורמטים של מודעות בגודל זהה, והביטים שלהם נלקחים חלקים שונים בהוראה. כמו שנראה, המפענח הבינארי של המפרט את הפורמט הזה יכול לבטא זאת.
תיאור של קידוד בינארי
הקידוד הבינארי של ההוראה מבוטא בפורמט הבינארי.
(.bin_fmt
) קובץ תיאור. הוא מתאר את הקידוד הבינארי
ב-ISA, כדי שמפענח הוראה בפורמט בינארי יוכל להיות
שנוצר. המפענח שנוצר קובע את ה-opcode, מחלץ את הערך של
באופרנד ובשדות מיידיים, כדי לספק את המידע שדרוש ל-ISA
למקודד-מפענח שתואר במדריך הקודם.
במדריך הזה נכתוב קובץ תיאור של קידוד בינארי עבור קבוצת משנה מההוראות של RiscV32I הנדרשות כדי לדמות את ההוראות שבהן נעשה שימוש "העולם שלי" הקטן בתוכנית. לפרטים נוספים על ה-RiscV ISA, ראו מפרטי 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
, שמציין את האופן שבו סוג ספירת opcode שנוצר
ממפענח ה-ISA צריך להפנות בתוך הקוד שנוצר. שלישית,
השדה includes {}
מציין את קובצי ההכללה הנדרשים לקוד שנוצר עבור
של המפענח הזה.
במקרה שלנו, זה הקובץ שמופק על ידי מפענח ה-ISA
במדריך הקודם.
אפשר לציין קובצי הכללה נוספים ב-includes {}
בהיקף גלובלי
להגדרה. האפשרות הזאת שימושית אם הגדרתם כמה מפענחים, וכולם צריכים
כדי לכלול חלק מהקבצים האלה. הרביעית היא רשימה של שמות ההוראה
שמרכיבות את ההוראות שעבורן נוצר המפענח. ב
יש רק מקרה אחד: 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
כולל
אותו מאפיין חתום/לא חתום, והוא מתייחס לאותם ביטים של מילים להוראה
בתור Inst32Format.opcode
.
הפורמט השלישי הוא פורמט מותאם אישית שנמצא בשימוש רק ב-fence
הוראה, שהיא הוראה שכבר צוינה ואין לנו
במדריך הזה כדי לפתור את הבעיה.
נקודה חשובה: שימוש חוזר בשמות של שדות בפורמטים קשורים שונים, כל עוד שמייצגים את אותם ביטים ויש להם אותו מאפיין חתום/לא חתום.
אחרי הגדרות הפורמט ב-riscv32i.bin_fmt
מגיעה קבוצת הוראות
להגדרה. כל ההוראות בקבוצת הוראות צריכות להיות זהות
באורך הסיביות, ולהשתמש בפורמט שגוזר (אולי בעקיפין)
בפורמט של הוראה ברמה העליונה. כשב-ISA יכולות להיות הוראות עם פונקציות
אורכים, קבוצת הוראות שונה לכל אורך. בנוסף,
אם פענוח ה-ISA של היעד תלוי במצב ביצוע, למשל 'זרוע' לעומת 'אגודל'
הוראות, צריך קבוצת הוראות נפרדת לכל מצב.
המנתח 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"
, והוראה בסיסית
הפורמט. סוג ספירת הקוד של הקוד צריך להיות זהה לסוג המופק על ידי
בפורמט של מפענח הוראה עצמאית, מכוסה במדריך ב-ISA
וממפענח.
כל תיאור של קידוד הוראות מורכב מ-3 חלקים:
- שם ה-opcode, שחייב להיות זהה לשם שבו נעשה שימוש בהוראה בתיאור של המפענח, כדי ששניהם יפעלו יחד.
- פורמט ההוראה שבו צריך להשתמש ל-opcode. זה הפורמט ששימשו למתן אזכורים לשדות ביטים בחלק האחרון.
- רשימה מופרדת בפסיקים של אילוצים בשדות ביט,
==
,!=
,<
,<=
,>
, ו->=
חייבים להיות TRUE כדי שה-opcode יוכל להתאים בהצלחה מילת הוראות.
המנתח של .bin_fmt
משתמש בכל המידע הזה כדי לבנות מפענח, ש:
- מספקת פונקציות חילוץ (חתומות/לא חתומות) בהתאם לכל ביט
בכל פורמט. פונקציות החילוץ ממוקמות במרחבי שמות
שקיבלה את השם שלו בגרסת תרחיש הנחש של שם הפורמט. לדוגמה,
פונקציות חילוץ עבור הפורמט
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)
Host (מארח) - עבור מארחים אחרים, ה-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
. החלק הראשון של הקובץ מכיל תקן
הגנה על בסיס סטנדרטי (boilerplate), כולל קבצים והצהרות על מרחב שמות. לאחר מכן
יש פונקציית עזר בתבנית במרחב השמות internal
. הפונקציה הזו
משמש לחילוץ שדות ביט מפורמטים שהם ארוכים מדי מכדי להתאים לפורמט C++ של 64 סיביות
מספר שלם.
#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) כאשר
שמות השדות מתייחסים לאותו שדה סוג (מיקום חתום/לא חתום ומיקומי ביט)
בכל אחד מהפורמטים שבהם הם מופיעים. הפעולה הזו מפעילה שדות ביטים שמתארים את אותם
סיביות שיחולצו באמצעות פונקציות במרחב השמות של הפורמט ברמה העליונה.
הפונקציות במרחב השמות 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
עכשיו הגיע הזמן להוסיף כמה הוראות חדשות לקובץ 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 | שלישי | קוד פעולה |
הדבר הראשון שאנחנו צריכים לעשות הוא להוסיף את הפורמט. פותחים את האפליקציה
riscv32i.bin_fmt
בעורך המועדף עליך. מיד אחרי שה-Inst32Format
מאפשר
מוסיפים פורמט בשם RType
שנובע מ-Inst32Format
. כל שדות הביטים
בRType
הם unsigned
. צריך לציין את השמות, את רוחב הסיביות ואת הסדר שלהן (מימין לשמאל)
מהטבלה שלמעלה כדי להגדיר את הפורמט. אם אתם צריכים רמז או אם אתם רוצים לראות
את הפתרון המלא,
יש ללחוץ כאן.
בשלב הבא צריך להוסיף את ההוראות. ההוראות הן:
add
– הוספת מספר שלם.and
- ברמת הסיביות ו-.or
– ברמת הסיביות או,sll
– הזזת לוגיקה שמאלה.sltu
- הגדרה של 'פחות מ-', ללא חתימה.sub
– החסרה של מספרים שלמים.xor
– xor ברמת הסיביות.
הקידודים שלהם:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 | שם opcode |
---|---|---|---|---|---|---|
000 0000 | rs2 | rs1 | 000 | שלישי | 011 0011 | add |
000 0000 | rs2 | rs1 | 111 | שלישי | 011 0011 | וגם |
000 0000 | rs2 | rs1 | 110 | שלישי | 011 0011 | או |
000 0000 | rs2 | rs1 | 001 | שלישי | 011 0011 | sll |
000 0000 | rs2 | rs1 | 011 | שלישי | 011 0011 | סלטו |
010 0000 | rs2 | rs1 | 000 | שלישי | 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];
}
בפונקציה הזו נוצרת טבלת פענוח סטטית, וערך החיפוש מחולץ ממילת ההוראה כדי לבחור את האינדקס המתאים. הפעולה הזו מוסיפה אבל מכיוון שה-opcode יכול להיות את החיפוש ישירות בטבלה ללא השוואות נוספות, בניגוד לדרישה להפעלת פונקציה נוספת.
הוספת הוראות של 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 | שלישי | קוד פעולה |
הפורמט המיידי המיוחד I-Type:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | שלישי | קוד פעולה |
הפורמט המיידי מסוג U:
31..12 | 11..7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | שלישי | קוד פעולה |
הפורמט I-Type כבר קיים ב-riscv32i.bin_fmt
, לכן לא צריך
מוסיפים את הפורמט הזה.
אם נשווה בין פורמט I-Type המיוחד לבין פורמט R-Type שהגדרנו,
בתרגיל הקודם, אנחנו רואים שההבדל היחיד הוא שהשדות rs2
השם החדש הוא uimm5
. במקום להוסיף פורמט חדש לגמרי, אנחנו יכולים להרחיב
פורמט R-Type. לא ניתן להוסיף עוד שדה, כי זה יגדיל את הרוחב של
את הפורמט, אבל ניתן להוסיף שכבת-על. שכבת-על היא כינוי של קבוצה של
ביטים בפורמט, ואפשר להשתמש בהם כדי לשלב
לישות נפרדת עם שם. תופעת הלוואי שהקוד שנוצר
יכלול גם פונקציית חילוץ עבור שכבת-העל,
לשדות. במקרה כזה, אם rs2
וגם uimm5
לא חתומים על החתימה
לא יכולה ליצור הבדל, חוץ מאשר לציין במפורש שנעשה שימוש בשדה
באופן מיידי. כדי להוסיף שכבת-על בשם uimm5
לפורמט R-Type, צריך להוסיף את
אחרי השדה האחרון:
overlays:
unsigned uimm5[5] = rs2;
הפורמט החדש היחיד שעלינו להוסיף הוא הפורמט U-Type. לפני שנוסיף את
נחשוב על שתי ההוראות שעושות שימוש בפורמט הזה: auipc
lui
. שני המדדים האלה מחליפים את הערך המיידי של 20 ביט שנשאר ב-12 לפני השימוש
כדי להוסיף את המחשב למחשב (auipc
) או לכתוב אותו ישירות לרשמת
(lui
). באמצעות שכבת-על אנחנו יכולים לספק גרסה משתנה מראש של
העברה של מעט חישובים מביצוע של הוראה לביצוע של הוראה
לפענח אותו. קודם צריך להוסיף את הפורמט בהתאם לשדות שצוינו בטבלה
למעלה. לאחר מכן נוכל להוסיף את שכבת-העל הבאה:
overlays:
unsigned uimm32[32] = uimm20, 0b0000'0000'0000;
התחביר של שכבת-על מאפשר לנו לשרשר, לא רק שדות, אלא ליטרלים: נו. במקרה הזה אנחנו משרשרים אותו ל-12 אפסים, ולמעשה מזיזים אותו שמאלה עד 12.
ההוראות מסוג I-Type שעלינו להוסיף הן:
addi
– הוספה מיידית.andi
– סיביות ובאופן מיידי.ori
– סיביות או באופן מיידי.xori
– ערך Xor של כלשהו באופן מיידי.
הקידודים שלהם:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | שלישי | 001 0011 | Addi |
imm12 | rs1 | 111 | שלישי | 001 0011 | אנדי |
imm12 | rs1 | 110 | שלישי | 001 0011 | אורי |
imm12 | rs1 | 100 | שלישי | 001 0011 | Xori |
func3 | קוד פעולה |
ההוראות מסוג R (סוג I-Type מיוחד) שעלינו להוסיף הן:
slli
– שינוי היגיון שמאלה באופן מיידי.srai
- שינוי שיטת החשבון הימנית באופן מיידי.srli
– שינוי הלוגיקה הימנית מייד.
הקידודים שלהם:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 | שם opcode |
---|---|---|---|---|---|---|
000 0000 | uimm5 | rs1 | 001 | שלישי | 001 0011 | slli |
010 0000 | uimm5 | rs1 | 101 | שלישי | 001 0011 | סראי |
000 0000 | uimm5 | rs1 | 101 | שלישי | 001 0011 | Srli |
func7 | func3 | קוד פעולה |
ההוראות מסוג U שעלינו להוסיף הן:
auipc
- הוספה של למעלה מיידית למחשב PC.lui
– טעינה מיידית למעלה.
הקידודים שלהם:
31..12 | 11..7 | 6..0 | שם opcode |
---|---|---|---|
uimm20 | שלישי | 001 0111 | auipc |
uimm20 | שלישי | 011 0111 | Lui |
קוד פעולה |
ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.bin_fmt.
הוספת הוראות הסתעפות וקפיצה וקישור
הקבוצה הבאה של ההוראות שצריך להגדיר היא ההסתעפות המותנית הוראות הדילוג והקישור, והרישום של 'קפיצה וקישור' הוראות להתאמה אישית.
ההסתעפויות המותנות שאנחנו מוסיפים משתמשות בקידוד מסוג B-Type.
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
באמצעות הקידוד.
הוספת פורמט 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-Type:
31..12 | 11..7 | 6..0 |
---|---|---|
20 | 5 | 7 |
imm20 | שלישי | קוד פעולה |
זהו גם פורמט שקל להוסיף, אבל שוב, הפורמט המיידי שבו משתמשים ההוראה לא פשוטה כמו שנדמה. רצפי הביטים שמשמשים האנשים המיידיים המלאים הם: 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 | שלישי | קוד פעולה |
הפעם לא צריך לבצע שינוי בפורמט.
הוראות ההסתעפות שעלינו להוסיף הן:
beq
– הסתעפות אם שווה.bge
– הסתעפות אם גדול מ- או שווה.bgeu
- הסתעפות אם היא גדולה או שווה ללא חתימה.blt
– הסתעפות אם פחות מ-.bltu
- הסתעפות אם פחות מאשר ללא חתימה.bne
– הסתעפות אם לא שווה ל-.
הם מקודדים כך:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 | שם opcode |
---|---|---|---|---|---|---|
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 | שלישי | 110 1111 | חלול |
קוד פעולה |
ההוראה jalr
מקודדת כך:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | שלישי | 110 0111 | Jalr |
func3 | קוד פעולה |
ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.bin_fmt.
הוספת הוראות לחנות
הוראות החנות משתמשות בקידוד S-Type, שזהה ל-B-Type
הקידוד שבו נעשה שימוש בהוראות ההסתעפות, מלבד הקומפוזיציה של
באופן מיידי. אנחנו בוחרים להוסיף את הפורמט 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 | שם opcode |
---|---|---|---|---|---|---|
imm7 | rs2 | rs1 | 000 | imm5 | 010 0011 | sb |
imm7 | rs2 | rs1 | 001 | imm5 | 010 0011 | ד' |
imm7 | rs2 | rs1 | 010 | imm5 | 010 0011 | sw |
func3 | קוד פעולה |
ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.bin_fmt.
הוספת הוראות טעינה
הוראות הטעינה משתמשות בפורמט I-Type. לא צריך לבצע שם שינויים.
הקידודים הם:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | שלישי | 000 0011 | lb |
imm12 | rs1 | 100 | שלישי | 000 0011 | ליבו |
imm12 | rs1 | 001 | שלישי | 000 0011 | לה |
imm12 | rs1 | 101 | שלישי | 000 0011 | להו |
imm12 | rs1 | 010 | שלישי | 000 0011 | lw |
func3 | קוד פעולה |
ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.bin_fmt.
סיימנו את המדריך הזה, אנחנו מקווים שהוא עזר.