बाइनरी निर्देश डिकोडर ट्यूटोरियल

इस ट्यूटोरियल का मकसद ये है:

  • बाइनरी फ़ॉर्मैट की जानकारी वाली फ़ाइल के स्ट्रक्चर और सिंटैक्स के बारे में जानें.
  • जानें कि बाइनरी फ़ॉर्मैट की जानकारी, आईएसए फ़ॉर्मैट की जानकारी से कैसे मेल खाती है.
  • निर्देशों के RiscV RV32I सबसेट के लिए, बाइनरी ब्यौरे लिखें.

खास जानकारी

RiscV बाइनरी निर्देश एन्कोडिंग

बाइनरी निर्देश कोड में बदलना, निर्देशों को कोड में बदलने का स्टैंडर्ड तरीका है, ताकि उन्हें माइक्रोप्रोसेसर पर चलाया जा सके. आम तौर पर, इन्हें किसी ऐसी फ़ाइल में सेव किया जाता है जिसे चलाया जा सकता है. आम तौर पर, इन्हें ELF फ़ॉर्मैट में सेव किया जाता है. निर्देश या तो तय चौड़ाई या अलग-अलग चौड़ाई वाले हो सकते हैं.

आम तौर पर, निर्देशों में कोड में बदलने के लिए इस्तेमाल होने वाले फ़ॉर्मैट के छोटे सेट का इस्तेमाल किया जाता है. हर फ़ॉर्मैट को कोड में बदले गए निर्देशों के टाइप के हिसाब से बनाया जाता है. उदाहरण के लिए, रजिस्टर-रजिस्टर निर्देशों में, उपलब्ध ऑपरेंड कोड की संख्या को बढ़ाने के लिए एक फ़ॉर्मैट का इस्तेमाल किया जा सकता है. वहीं, रजिस्टर-इमीडिएट निर्देशों में, उपलब्ध ऑपरेंड कोड की संख्या को कम करके, कोड में बदले जा सकने वाले इमीडिएट के साइज़ को बढ़ाया जा सकता है. ब्रांच और जंप निर्देश ज़्यादातर ऐसे फ़ॉर्मैट का इस्तेमाल करते हैं जो बड़े ऑफ़सेट वाली ब्रांच को सपोर्ट करने के लिए तुरंत साइज़ को बड़ा कर देते हैं.

हम अपने 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-टाइप फ़ॉर्मैट, जिसका इस्तेमाल तुरंत निर्देशों के साथ शिफ़्ट करने के लिए किया जाता है, 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 ऑपकोड

बी-टाइप फ़ॉर्मैट, जिसका इस्तेमाल कंडिशनल ब्रांच के लिए किया जाता है, जो 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-टाइप फ़ॉर्मैट, जिसका इस्तेमाल 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 बिट के होते हैं. साथ ही, हर फ़ॉर्मैट में सबसे नीचे मौजूद सात बिट, ऑपरेंड कोड फ़ील्ड होते हैं. यह भी ध्यान दें कि कई फ़ॉर्मैट में एक जैसे साइज़ के इमिडिएट होते हैं, लेकिन उनके बिट निर्देश के अलग-अलग हिस्सों से लिए जाते हैं. जैसा कि हम देख सकते हैं कि बाइनरी डिकोडर के स्पेसिफ़िकेशन वाले फ़ॉर्मैट से, इसे साफ़ तौर पर बताया जा सकता है.

बाइनरी एन्कोडिंग की जानकारी

निर्देश की बाइनरी एन्कोडिंग, बाइनरी फ़ॉर्मैट (.bin_fmt) की जानकारी वाली फ़ाइल में दी जाती है. यह आईएसए में निर्देशों के बाइनरी कोड के बारे में बताता है, ताकि बाइनरी फ़ॉर्मैट निर्देश डिकोडर जनरेट किया जा सके. जनरेट किया गया डिकोडर, ऑपकोड को तय करता है, ऑपरेटर और रीटेल फ़ील्ड की वैल्यू एक्सट्रैक्ट करता है, ताकि पिछले ट्यूटोरियल में बताए गए आईएसए एन्कोडिंग-एग्नोस्टिक डिकोडर के लिए ज़रूरी जानकारी दी जा सके.

इस ट्यूटोरियल में, हम RiscV32I निर्देशों के सबसेट के लिए, बाइनरी कोडिंग की जानकारी वाली फ़ाइल लिखेंगे. यह फ़ाइल, छोटे "Hello World" प्रोग्राम में इस्तेमाल किए गए निर्देशों को सिम्युलेट करने के लिए ज़रूरी है. 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, जो बताता है कि आईएसए डिकोडर से जनरेट किए गए ऑपरेंड कोड के टाइप को जनरेट किए गए कोड में कैसे रेफ़र किया जाना चाहिए. तीसरा, includes {} इस डिकोडर के लिए जनरेट किए गए कोड के लिए ज़रूरी फ़ाइलों को शामिल करता है. हमारे मामले में, इस फ़ाइल को पिछले ट्यूटोरियल से आईएसए डिकोडर ने बनाया है. ग्लोबल स्कोप वाली 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++ इंटिजर टाइप में डालने पर, वैल्यू को शून्य तक बढ़ा दिया जाएगा. बिटफ़ील्ड की चौड़ाई का कुल योग, फ़ॉर्मैट की चौड़ाई के बराबर होना चाहिए. अगर आपको किसी तरह का असहमत होगा, तो इस टूल से आपको गड़बड़ी का पता चलेगा. यह फ़ॉर्मैट किसी दूसरे फ़ॉर्मैट से नहीं मिलता है. इसलिए, इसे टॉप लेवल फ़ॉर्मैट माना जाता है.

दूसरा, IType नाम के 32 बिट वाले निर्देश फ़ॉर्मैट के बारे में बताता है, जो Inst32Format से लिया गया है. इससे, इन दोनों फ़ॉर्मैट को मिलता-जुलता बनाता है. इस फ़ॉर्मैट में पांच फ़ील्ड होते हैं: imm12, rs1, func3, rd, और opcode. imm12 फ़ील्ड का नाम signed है. इसका मतलब है कि वैल्यू को निकालकर C++ इंटिजर टाइप में डालने पर, वैल्यू को साइन-एक्सटेंड किया जाएगा. ध्यान दें कि IType.opcode में, साइन वाला और बिना साइन वाला, दोनों एट्रिब्यूट एक जैसे होते हैं. साथ ही, Inst32Format.opcode की तरह ही, इनमें भी निर्देश वाले शब्द के बिट एक जैसे होते हैं.

तीसरा फ़ॉर्मैट एक कस्टम फ़ॉर्मैट है, जिसका इस्तेमाल सिर्फ़ fence निर्देश के लिए किया जाता है. यह निर्देश पहले से तय होता है और हमें इस ट्यूटोरियल में इसकी चिंता करने की ज़रूरत नहीं है.

अहम जानकारी: फ़ील्ड के नामों को मिलते-जुलते फ़ॉर्मैट में फिर से इस्तेमाल करें. इसके लिए, यह ज़रूरी है कि वे एक जैसे बिट के बारे में बताते हों और उन पर हस्ताक्षर या साइन नहीं किया गया एट्रिब्यूट भी एक जैसा हो.

riscv32i.bin_fmt में फ़ॉर्मैट की परिभाषाओं के बाद, निर्देशों के ग्रुप की परिभाषा आती है. निर्देश ग्रुप में मौजूद सभी निर्देशों की बिट-लंबाई एक ही होनी चाहिए. साथ ही, ऐसे फ़ॉर्मैट का इस्तेमाल करना चाहिए जो एक ही टॉप लेवल निर्देश फ़ॉर्मैट से (शायद अप्रत्यक्ष रूप से) लिया गया हो. जब किसी आईएसए में अलग-अलग लंबाई वाले निर्देश हो सकते हैं, तो हर लंबाई के लिए अलग निर्देश ग्रुप का इस्तेमाल किया जाता है. इसके अलावा, अगर टारगेट आईएसए डिकोडिंग, 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], इस्तेमाल किए जाने वाले ऑपरेंड कोड के एनोटेशन टाइप का नाम "OpcodeEnum", और बुनियादी निर्देश फ़ॉर्मैट तय किया जाता है. ऑपरेंड कोड की गिनती का टाइप, वही होना चाहिए जो आईएसए डिकोडर के ट्यूटोरियल में बताया गया है.

निर्देश को कोड में बदलने की जानकारी में तीन हिस्से होते हैं:

  • ऑपकोड का नाम वही होना चाहिए जो दोनों को एक साथ काम करने के लिए, निर्देश डिकोडर की जानकारी में इस्तेमाल किया गया था.
  • ऑपरेंड कोड के लिए इस्तेमाल किया जाने वाला निर्देश फ़ॉर्मैट. इस फ़ॉर्मैट का इस्तेमाल, आखिरी हिस्से में बिटफ़ील्ड के रेफ़रंस देने के लिए किया जाता है.
  • बिट फ़ील्ड कंस्ट्रेंट, ==, !=, <, <=, >, और >= की कॉमा-सेपरेटेड लिस्ट, ये सभी सही होने चाहिए, ताकि ऑपकोड, निर्देश के शब्द से मैच कर सके.

.bin_fmt पार्सर, इस जानकारी का इस्तेमाल करके एक डिकोडर बनाता है, जो:

  • हर फ़ॉर्मैट में हर बिट फ़ील्ड के लिए, निकालने के फ़ंक्शन (साइन वाला/बिना साइन वाला) उपलब्ध कराता है. एक्सट्रैक्टर फ़ंक्शन को नेमस्पेस में रखा जाता है. इनका नाम, फ़ॉर्मैट के नाम के स्नेक-केस वर्शन से लिया जाता है. उदाहरण के लिए, IType फ़ॉर्मैट के लिए, एक्सट्रैक्टर फ़ंक्शन को नेमस्पेस i_type में रखा जाता है. हर एक्सट्रैक्टर फ़ंक्शन को inline के तौर पर दिखाया जाता है. यह फ़ंक्शन, फ़ॉर्मैट की चौड़ाई को दिखाने के लिए सबसे छोटा uint_t टाइप इस्तेमाल करता है. साथ ही, यह फ़ंक्शन, निकाले गए फ़ील्ड की चौड़ाई को दिखाने के लिए सबसे छोटा int_t (साइन के लिए) और uint_t (साइन के बिना) टाइप इस्तेमाल करता है. उदाहरण:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • हर निर्देश ग्रुप के लिए डिकोड फ़ंक्शन. यह OpcodeEnum टाइप की वैल्यू दिखाता है. साथ ही, निर्देश ग्रुप फ़ॉर्मैट की चौड़ाई को बनाए रखने के लिए, सबसे छोटा uint_t टाइप इस्तेमाल करता है.

शुरुआती बिल्ड करना

डायरेक्ट्री को 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

शुरुआती सेक्शन के बाद, तीन नेमस्पेस का एक सेट होता है. यह सेट, riscv32i.bin_fmt फ़ाइल में मौजूद हर format एलान के लिए होता है:


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;
}

इस मामले में, जहां सिर्फ़ चार निर्देश दिए गए हैं, वहां सिर्फ़ एक लेवल की डिकोड की गई जानकारी और एक बहुत ही स्पैर्स लुकअप टेबल होगी. निर्देश जोड़ने पर, डिकोडर का स्ट्रक्चर बदल जाएगा और डिकोडर टेबल हैरारकी में लेवल की संख्या बढ़ सकती है.


रजिस्टर करने के लिए ALU के निर्देश जोड़ना

अब riscv32i.bin_fmt फ़ाइल में कुछ नए निर्देश जोड़ने का समय आ गया है. निर्देशों का पहला ग्रुप, रजिस्टर-रजिस्टर एएलयू निर्देश होता है. जैसे, 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 - बिटवाइज़ ऐंड.
  • or - बिट के हिसाब से OR.
  • sll - बाईं ओर शिफ़्ट लॉजिकल.
  • sltu - सेट कम-से-कम, बिना हस्ताक्षर वाला.
  • sub - पूर्णांक घटाना.
  • xor - बिटवाइज़ एक्सओआर.

इनकी एन्कोडिंग ये हैं:

31..25 24..20 19..15 14..12 11..7 6..0 ऑपकोड का नाम
000 0000 rs2 rs1 000 rd 011 0011 जोड़ें
000 0000 rs2 rs1 111 rd 011 0011 और
000 0000 rs2 rs1 110 rd 011 0011 या
000 0000 rs2 rs1 001 तीसरा 011 0011 sll
000 0000 rs2 rs1 011 rd 011 0011 एसएलटीयू
010 0000 rs2 rs1 000 rd 011 0011 बदले में खेलने वाला खिलाड़ी
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];
}

इस फ़ंक्शन में एक स्टैटिक डिकोड टेबल जनरेट होती है और सही इंडेक्स चुनने के लिए, निर्देश शब्द से एक लुकअप वैल्यू निकाली जाती है. इससे निर्देश डिकोडर के लेआउट में एक दूसरी लेयर जुड़ जाती है. हालांकि, ऑपरेंड कोड को किसी और तुलना के बिना सीधे टेबल में देखा जा सकता है. इसलिए, इसे इस फ़ंक्शन में इनलाइन किया जाता है, ताकि किसी दूसरे फ़ंक्शन को कॉल करने की ज़रूरत न पड़े.


तुरंत लागू होने वाले निर्देशों के साथ एएलयू निर्देश जोड़ना

हम निर्देशों का अगला सेट जोड़ेंगे. ये एएलयू निर्देश हैं, जो किसी रजिस्टर के बजाय, तुरंत वैल्यू का इस्तेमाल करते हैं. इन निर्देशों के तीन ग्रुप होते हैं (इमीडिएट फ़ील्ड के आधार पर): 12 बिट के साइन वाले इमीडिएट वाले I-टाइप इमीडिएट निर्देश, शिफ़्ट के लिए खास I-टाइप इमीडिएट निर्देश, और 20-बिट के बिना साइन वाले इमीडिएट वैल्यू वाले U-टाइप इमीडिएट. फ़ॉर्मैट यहां दिखाए गए हैं:

I-टाइप इंस्टैंट फ़ॉर्मैट:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd ऑपकोड

खास I-टाइप इमीडिएट फ़ॉर्मैट:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd ऑपकोड

यू-टाइप इमीडिएट फ़ॉर्मैट:

31..12 11..7 6..0
20 5 7
uimm20 rd ऑपकोड

I-टाइप फ़ॉर्मैट, riscv32i.bin_fmt में पहले से मौजूद है. इसलिए, उस फ़ॉर्मैट को जोड़ने की ज़रूरत नहीं है.

अगर हम खास तरह के आई-टाइप फ़ॉर्मैट की तुलना, पिछले अभ्यास में बताए गए आर-टाइप फ़ॉर्मैट से करते हैं, तो हमें पता चलता है कि rs2 फ़ील्ड का नाम बदलकर uimm5 कर दिया गया है. नया फ़ॉर्मैट जोड़ने के बजाय, हम R-Type फ़ॉर्मैट को बेहतर बना सकते हैं. हम कोई दूसरा फ़ील्ड नहीं जोड़ सकते, क्योंकि इससे फ़ॉर्मैट की चौड़ाई बढ़ जाएगी. हालांकि, हम ओवरले जोड़ सकते हैं. ओवरले, फ़ॉर्मैट में बिट के सेट का दूसरा नाम होता है. इसका इस्तेमाल, फ़ॉर्मैट के कई सबसीक्वेंस को अलग-अलग नाम वाली इकाई में जोड़ने के लिए किया जा सकता है. इसका असर यह होगा कि जनरेट किए गए कोड में, अब फ़ील्ड के साथ-साथ ओवरले के लिए भी डेटा निकालने का फ़ंक्शन शामिल होगा. इस मामले में, जब rs2 और uimm5, दोनों को साइन नहीं किया जाता है, तो इसका कोई असर नहीं पड़ता है. हालांकि, सिर्फ़ यह साफ़ तौर पर बताया जाता है कि फ़ील्ड का इस्तेमाल तुरंत किया जा रहा है. R-टाइप फ़ॉर्मैट में uimm5 नाम का ओवरले जोड़ने के लिए, आखिरी फ़ील्ड के बाद यह जोड़ें:

  overlays:
    unsigned uimm5[5] = rs2;

हमें सिर्फ़ U-टाइप फ़ॉर्मैट जोड़ना है. फ़ॉर्मैट जोड़ने से पहले, आइए उन दो निर्देशों पर ध्यान दें जिनमें उस फ़ॉर्मैट का इस्तेमाल किया गया है: auipc और lui. ये दोनों, 20-बिट की इमीडिएट वैल्यू को 12 बाईट बाईं ओर शिफ़्ट करते हैं. इसके बाद, इस वैल्यू का इस्तेमाल पीसी (auipc) में जोड़ने या सीधे रजिस्टर (lui) में लिखने के लिए किया जाता है. ओवरले का इस्तेमाल करके, हम इमीडिएट वैल्यू का पहले से शिफ़्ट किया गया वर्शन दे सकते हैं. इससे, निर्देशों को लागू करने से लेकर निर्देशों को डिकोड करने तक की गिनती में थोड़ा बदलाव होता है. सबसे पहले, ऊपर दी गई टेबल में बताए गए फ़ील्ड के हिसाब से फ़ॉर्मैट जोड़ें. इसके बाद, हम इस तरह का ओवरले जोड़ सकते हैं:

  overlays:
    unsigned uimm32[32] = uimm20, 0b0000'0000'0000;

ओवरले सिंटैक्स की मदद से हम फ़ील्ड के साथ-साथ, लिटरल वैल्यू को भी एक साथ जोड़ सकते हैं. इस मामले में, हम इसे 12 शून्य के साथ जोड़ते हैं. इसका मतलब है कि इसे बाईं ओर 12 से शिफ़्ट किया जाता है.

हमें I-टाइप के ये निर्देश जोड़ने हैं:

  • addi - तुरंत जोड़ें.
  • andi - बिट के अनुसार और तुरंत.
  • ori - बिटवाइज़ या तुरंत लागू होने वाले फ़ंक्शन के साथ.
  • xori - बिटवाइज़ एक्सओआर, जिसमें इमीडिएट वैल्यू शामिल है.

इनकी एन्कोडिंग हैं:

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 तीसरा 001 0011 ori
imm12 rs1 100 rd 001 0011 xori
func3 ऑपकोड

हमें R-टाइप (खास I-टाइप) के ये निर्देश जोड़ने हैं:

  • 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 सराय
000 0000 uimm5 rs1 101 rd 001 0011 srli
func7 func3 ऑपकोड

हमें U-Type के लिए ये निर्देश जोड़ने होंगे:

  • auipc - पीसी पर ऊपरी इमेज जोड़ें.
  • lui - तुरंत सबसे ऊपर लोड करें.

इनकी एन्कोडिंग ये हैं:

31..12 11..7 6..0 ऑपकोड का नाम
uimm20 तीसरा 001 0111 auipc
uimm20 rd 011 0111 lui
ऑपकोड

बदलाव करें और फिर बिल्ड करें. जनरेट किया गया आउटपुट देखें. पहले की तरह ही, 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-टाइप एन्कोडिंग, R-टाइप एन्कोडिंग के लेआउट से मेल खाती है. हालांकि, हमने RiscV दस्तावेज़ के साथ अलाइन करने के लिए, नए फ़ॉर्मैट टाइप का इस्तेमाल करने का विकल्प चुना है. हालांकि, R-टाइप एन्कोडिंग के func7 और rd फ़ील्ड का इस्तेमाल करके, तुरंत सही शाखा के डिसप्लेसमेंट को देखने के लिए, सिर्फ़ एक ओवरले जोड़ा जा सकता था.

ऊपर बताए गए फ़ील्ड के साथ फ़ॉर्मैट BType जोड़ना ज़रूरी है, लेकिन यह ज़रूरी नहीं है कि आपने सिर्फ़ यही फ़ील्ड जोड़े हों. जैसा कि आपको दिख रहा है, पहला निर्देश दो निर्देशों के फ़ील्ड में बंटा हुआ है. इसके अलावा, ब्रांच के निर्देश इसे दोनों फ़ील्ड को जोड़कर एक आसान प्रोसेस नहीं मानते हैं. इसके बजाय, हर फ़ील्ड को अलग-अलग हिस्सों में बांटा जाता है और इन हिस्सों को अलग क्रम में जोड़ा जाता है. आखिर में, 16-बिट अलाइन किए गए ऑफ़सेट को पाने के लिए, उस वैल्यू को एक बार बाईं ओर शिफ़्ट किया जाता है.

इंस्टरक्शन वर्ड में बिट का क्रम, इमिडिएट फ़ॉर्म करने के लिए इस्तेमाल किया जाता है: 31, 7, 30..25, 11..8. यह इन सब-फ़ील्ड रेफ़रंस से मेल खाता है, जहां इंडेक्स या रेंज, फ़ील्ड में बिट की जानकारी देते हैं. इनका नंबर दाईं से बाईं ओर होता है, जैसे कि imm7[6] का मतलब imm7 के msb से है और imm5[0] का मतलब imm5 के lsb से है.

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-टाइप फ़ॉर्मैट का इस्तेमाल करता है.

I-टाइप इंस्टैंट फ़ॉर्मैट:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd ऑपकोड

इस बार, फ़ॉर्मैट में कोई बदलाव नहीं करना होगा.

हमें ये निर्देश जोड़ने होंगे:

  • beq - बराबर होने पर शाखा.
  • bge - अगर वैल्यू इससे ज़्यादा या इसके बराबर है, तो ब्रैंच पर जाएं.
  • bgeu - बिना साइन वाले वैल्यू से ज़्यादा या उसके बराबर होने पर ब्रांच पर जाएं.
  • blt - अगर वैल्यू कम है, तो शाखा.
  • bltu - अगर हस्ताक्षर नहीं किए गए हैं, तो शाखा.
  • 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 छोटा
func3 ऑपकोड

jal निर्देश को इस तरह से कोड में बदला जाता है:

31..12 11..7 6..0 ऑपकोड का नाम
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 ऑपकोड

बदलाव करें और फिर बिल्ड करें. जनरेट किया गया आउटपुट देखें. पहले की तरह ही, riscv32i.bin_fmt के हिसाब से अपने काम की जांच की जा सकती है.


स्टोर के लिए निर्देश जोड़ना

स्टोर के निर्देश में एस-टाइप एन्कोडिंग का इस्तेमाल किया जाता है, जो ब्रांच के निर्देशों में इस्तेमाल होने वाले बी-टाइप एन्कोडिंग की तरह होता है. हालांकि, इसमें तुरंत लागू होने वाली प्रोसेस शामिल नहीं है. हमने RiscV दस्तावेज़ के साथ अलाइन रहने के लिए, SType फ़ॉर्मैट जोड़ने का विकल्प चुना है.

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 ऑपकोड

बदलाव करें और फिर बिल्ड करें. जनरेट किया गया आउटपुट देखें. पहले की तरह ही, 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 rd 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 के ख़िलाफ़ अपने काम की जांच की जा सकती है.

इस ट्यूटोरियल में यह जानकारी दी गई है. हमें उम्मीद है कि यह आपके लिए मददगार रहा होगा.