इस ट्यूटोरियल का मकसद ये है:
- जानें कि निर्देश सिमैंटिक लागू करने के लिए, सिमैंटिक फ़ंक्शन का इस्तेमाल कैसे किया जाता है.
- जानें कि सिमैंटिक फ़ंक्शन, आईएसए डिकोडर की जानकारी से कैसे जुड़े होते हैं.
- RiscV RV32I निर्देशों के लिए निर्देश सिमैंटिक फ़ंक्शन लिखें.
- एक छोटा "Hey World" चलाकर, फ़ाइनल सिम्युलेटर टेस्ट करें एक्ज़ीक्यूट किया जा सकता है.
सिमैंटिक फ़ंक्शन के बारे में खास जानकारी
MPACT-Sim में सिमेंटिक फ़ंक्शन, एक ऐसा फ़ंक्शन है जो कार्रवाई लागू करता है ताकि इसके खराब असर सिम्युलेटेड मोड में साफ़ तौर पर दिखें. ठीक उसी तरह, जैसे आपके नियम बनाने पर निर्देश के दुष्प्रभाव दिखाई देते हैं हार्डवेयर. डिकोड किए गए हर निर्देश को सिम्युलेटर के अंदर दिखाना इसमें कॉल करने लायक एक ऐसा एलिमेंट शामिल है जिसका इस्तेमाल सिमैंटिक फ़ंक्शन कॉल करने के लिए किया जाता है निर्देश दिए गए हों.
सिमैंटिक फ़ंक्शन में हस्ताक्षर void(Instruction *)
होता है. इसका मतलब है कि
फ़ंक्शन जो Instruction
क्लास के इंस्टेंस का पॉइंटर लेता है और
void
दिखाता है.
Instruction
क्लास की जानकारी इसमें दी गई है
instruction.h
सिमैंटिक फ़ंक्शन लिखने के मकसद से हमें खास तौर पर दिलचस्पी है
सोर्स और डेस्टिनेशन ऑपरेंड इंटरफ़ेस वेक्टर को
Source(int i)
और Destination(int i)
तरीके का कॉल.
सोर्स और डेस्टिनेशन ऑपरेंड इंटरफ़ेस नीचे दिखाए गए हैं:
// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
public:
// Methods for accessing the nth value element.
virtual bool AsBool(int index) = 0;
virtual int8_t AsInt8(int index) = 0;
virtual uint8_t AsUint8(int index) = 0;
virtual int16_t AsInt16(int index) = 0;
virtual uint16_t AsUint16(int) = 0;
virtual int32_t AsInt32(int index) = 0;
virtual uint32_t AsUint32(int index) = 0;
virtual int64_t AsInt64(int index) = 0;
virtual uint64_t AsUint64(int index) = 0;
// Return a pointer to the object instance that implements the state in
// question (or nullptr) if no such object "makes sense". This is used if
// the object requires additional manipulation - such as a fifo that needs
// to be pop'ed. If no such manipulation is required, nullptr should be
// returned.
virtual std::any GetObject() const = 0;
// Return the shape of the operand (the number of elements in each dimension).
// For instance {1} indicates a scalar quantity, whereas {128} indicates an
// 128 element vector quantity.
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
public:
virtual ~DestinationOperandInterface() = default;
// Allocates a data buffer with ownership, latency and delay line set up.
virtual DataBuffer *AllocateDataBuffer() = 0;
// Takes an existing data buffer, and initializes it for the destination
// as if AllocateDataBuffer had been called.
virtual void InitializeDataBuffer(DataBuffer *db) = 0;
// Allocates and initializes data buffer as if AllocateDataBuffer had been
// called, but also copies in the value from the current value of the
// destination.
virtual DataBuffer *CopyDataBuffer() = 0;
// Returns the latency associated with the destination operand.
virtual int latency() const = 0;
// Return a pointer to the object instance that implmements the state in
// question (or nullptr if no such object "makes sense").
virtual std::any GetObject() const = 0;
// Returns the order of the destination operand (size in each dimension).
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
};
सामान्य तीन ऑपरेंड के लिए सिमैंटिक फ़ंक्शन लिखने का बुनियादी तरीका
जैसे कि 32-बिट add
निर्देश वाला है:
void MyAddFunction(Instruction *inst) {
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
uint32_t c = a + b;
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, c);
db->Submit();
}
आइए, इस फ़ंक्शन के अलग-अलग हिस्सों की समीक्षा करें. दो पहली लाइनें
फ़ंक्शन का मुख्य भाग, सोर्स ऑपरेंड 0 और 1 से पढ़ता है. AsUint32(0)
कॉल
दिए गए डेटा को uint32_t
कलेक्शन के तौर पर समझता है और 0वें डेटा को फ़ेच करता है
एलिमेंट. यह सही है, चाहे मौजूदा रजिस्टर या वैल्यू
अरे वैल्यू है या नहीं. सोर्स ऑपरेंड का साइज़ (एलिमेंट में)
सोर्स ऑपरेंड तरीके shape()
से लिया गया है, जो एक वेक्टर दिखाता है
इसमें हर डाइमेंशन में एलिमेंट की संख्या शामिल होती है. यह तरीका इस्तेमाल करने पर, {1}
वैल्यू मिलती है
अदिश के लिए, 16 एलिमेंट वेक्टर के लिए {16}
, और 4x4 कलेक्शन के लिए {4, 4}
.
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
इसके बाद, uint32_t
नाम के अस्थायी c
को a + b
वैल्यू असाइन की जाती है.
अगली लाइन में, थोड़ी और जानकारी की ज़रूरत हो सकती है:
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
DataBuffer, रेफ़रंस के तौर पर गिनती किया जाने वाला एक ऑब्जेक्ट है. इसका इस्तेमाल वैल्यू को
सिम्युलेटेड स्टेट, जैसे कि रजिस्टर. यह तुलनात्मक रूप से टाइप नहीं किया गया है, हालांकि यह
जिस ऑब्जेक्ट से उसे असाइन किया गया है उसके आधार पर साइज़. इस मामले में, वह आकार
sizeof(uint32_t)
. यह कथन
वह गंतव्य जो इस गंतव्य ऑपरेंड का लक्ष्य है - इस मामले में एक
32-बिट पूर्णांक रजिस्टर. DataBuffer को इसकी शुरुआत
निर्देश के लिए सिस्टम को रेंडर होने में लगने वाला समय. यह निर्देश के दौरान बताया गया है
डिकोड करना.
अगली लाइन, डेटा बफ़र इंस्टेंस को uint32_t
कलेक्शन के तौर पर देखती है और
c
में सेव की गई वैल्यू को 0वें एलिमेंट पर लिखता है.
db->Set<uint32_t>(0, c);
आखिर में, आखिरी स्टेटमेंट में डेटा बफ़र को सिम्युलेटर पर सबमिट किया जाता है, ताकि इसका इस्तेमाल किया जा सके टारगेट मशीन स्थिति (इस मामले में एक रजिस्टर) की नई वैल्यू के बाद निर्देश को डिकोड करने पर सेट किए गए निर्देश की इंतज़ार का समय और डेस्टिनेशन ऑपरेंड वेक्टर में अपने-आप जानकारी भरी गई.
यह एक छोटा काम है, लेकिन इसमें थोड़ी बॉयलरप्लेट है ऐसा कोड जो निर्देश के बाद निर्देश लागू करते समय दोहराया जाता है. इसके अलावा, इससे निर्देश के असल मतलब समझने में भी दिक्कत हो सकती है. ऑर्डर में शामिल है वाक्याँश फ़ंक्शन को आसानी से लिखने के लिए, इसमें कई टेंप्लेट किए गए helper फ़ंक्शन हैं instruction_helpers.h देखें. ये सहायक, निर्देशों के लिए एक, दो या तीन वाले बॉयलरप्लेट कोड छिपा देते हैं सोर्स ऑपरेंड, और एक डेस्टिनेशन ऑपरेंड. चलिए, दोनों विकल्पों पर नज़र डालते हैं ऑपरेंड हेल्पर फ़ंक्शन:
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument1, Argument2)> operation) {
Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument, Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Result, Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
इस तरह के स्टेटमेंट का इस्तेमाल करने के बजाय, आपको दिखेगा कि:
uint32_t a = inst->Source(0)->AsUint32(0);
हेल्पर फ़ंक्शन, इनका इस्तेमाल करता है:
generic::GetInstructionSource<Argument>(instruction, 0);
GetInstructionSource
, टेंप्लेट वाले हेल्पर फ़ंक्शन का फ़ैमिली ग्रुप है, जो
इनका इस्तेमाल, निर्देश के सोर्स को टेंप्लेट वाले ऐक्सेस के तरीके देने के लिए किया जाता है
ऑपरेंड. उनके बिना, हर निर्देश हेल्पर फ़ंक्शन
हर टाइप के लिए एक विशेष टूल का इस्तेमाल करके, सोर्स ऑपरेंड को ऐक्सेस करने के लिए
As<int type>()
फ़ंक्शन का इस्तेमाल करना होगा. इन टेंप्लेट की परिभाषाएं देखी जा सकती हैं
इसमें फ़ंक्शन
instruction.h है.
जैसा कि देखा जा सकता है, लागू करने के तीन तरीके होते हैं. यह इस बात पर निर्भर करता है कि सोर्स
ऑपरेंड टाइप, डेस्टिनेशन जैसे ही होते हैं, चाहे डेस्टिनेशन
क्या वे अलग-अलग सोर्स से अलग हैं या वे सभी अलग-अलग हैं. का प्रत्येक वर्शन
यह फ़ंक्शन निर्देश इंस्टेंस पर ले जाता है. साथ ही, कॉल करने लायक
(जिसमें lambda फ़ंक्शन शामिल हैं). इसका मतलब है कि अब हम add
को फिर से लिख सकते हैं
सिमैंटिक फ़ंक्शन के बारे में नीचे बताया गया है:
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
बिल्ड में bazel build -c opt
और copts = ["-O3"]
के साथ कंपाइल किए जाने पर
फ़ाइल दिखाई देती है, तो यह किसी ओवरहेड के बिना पूरी तरह इनलाइन होनी चाहिए, जिससे हमें
कम शब्दों में जानकारी दे दी जाएगी.
जैसा कि एकल, द्विआधारी (बाइनरी) और अदिश अदिश (टेंरी) अदिशों के लिए सहायक फलन की जानकारी दी गई है वेक्टर इक्वेशन की जानकारी भी दी गई है. ये टूल, एक मददगार टेंप्लेट की तरह भी काम करते हैं ऐसे निर्देशों के लिए अपने सहायक बनाने के लिए जो सामान्य ढांचा में फ़िट नहीं होते.
शुरुआती बिल्ड
अगर आपने डायरेक्ट्री को riscv_semantic_functions
में नहीं बदला है, तो ऐसा करें
आखिरी चरण पूरा करें. इसके बाद, इस तरीके से प्रोजेक्ट बनाएं - यह बिल्ड कामयाब होना चाहिए.
$ bazel build :riscv32i
...<snip>...
कोई भी फ़ाइल जनरेट नहीं होती है, इसलिए यह वाकई में एक शुरुआती चरण है, पक्का करें कि सभी चीज़ें क्रम में हों.
तीन ऑपरेंड ALU निर्देश जोड़ें
अब कुछ सामान्य, 3-संरचित ALU के लिए सिमैंटिक फ़ंक्शन जोड़ते हैं
निर्देश. rv32i_instructions.cc
फ़ाइल खोलें और पक्का करें कि
जैसे-जैसे हम आगे बढ़ते हैं, वैसे-वैसे rv32i_instructions.h
फ़ाइल में नई परिभाषाएं जुड़ जाती हैं.
इसके लिए, हम ये निर्देश जोड़ेंगे:
add
- 32-बिट पूर्णांक जोड़ना.and
- 32-बिट बिट के अनुसार और.or
- 32-बिट की बिट के हिसाब से या.sll
- 32-बिट लॉजिकल शिफ़्ट बाईं ओर.sltu
- 32-बिट अनसाइन्ड सेट से कम.sra
- 32-बिट अंकगणितीय राइट शिफ़्ट.srl
- 32-बिट लॉजिकल राइट शिफ़्ट.sub
- 32-बिट पूर्णांक की वैल्यू घटाना.xor
- 32-बिट बिट के अनुसार xor.
अगर आपने पिछले ट्यूटोरियल में इस्तेमाल किए हैं, तो आपको याद होगा कि हमने इसमें रजिस्टर-रजिस्टर करने के निर्देशों और तुरंत रजिस्टर करने के निर्देशों के बीच डिकोडर. जब बात सिमैंटिक फ़ंक्शन की हो, तो हमें ऐसा करने की ज़रूरत नहीं होती. ऑपरेंड इंटरफ़ेस, किसी भी ऑपरेंड में मौजूद ऑपरेंड वैल्यू को पढ़ेगा है, रजिस्टर करना या तात्कालिक, सिमैंटिक फ़ंक्शन के साथ पूरी तरह से बुनियादी सोर्स ऑपरेंड क्या है.
sra
को छोड़कर, ऊपर दिए गए सभी निर्देश इस पर लागू हो सकते हैं
32-बिट अहस्ताक्षरित मान, इसलिए हम इनके लिए BinaryOp
टेम्प्लेट फ़ंक्शन का उपयोग कर सकते हैं
हमने पहले सिर्फ़ सिंगल टेंप्लेट टाइप आर्ग्युमेंट के साथ देखा था. इसे भरें
rv32i_instructions.cc
में फ़ंक्शन के मुख्य हिस्से करें. ध्यान दें कि केवल निम्न 5
शिफ़्ट के निर्देशों के दूसरे ऑपरेंड के बिट का इस्तेमाल शिफ़्ट के लिए किया जाता है
रकम. ऐसा नहीं होने पर, सभी कार्रवाइयां src0 op src1
फ़ॉर्मैट में होंगी:
add
:a + b
and
:a & b
or
:a | b
sll
:a << (b & 0x1f)
sltu
:(a < b) ? 1 : 0
srl
:a >> (b & 0x1f)
sub
:a - b
xor
:a ^ b
sra
के लिए हम तीन तर्क BinaryOp
टेंप्लेट का इस्तेमाल करेंगे. व्यू की मदद से,
टेंप्लेट, पहला टाइप आर्ग्युमेंट, नतीजे का टाइप uint32_t
है. दूसरा है
सोर्स ऑपरेंड का टाइप 0, इस मामले में int32_t
, और आखिरी टाइप है
सोर्स ऑपरेंड 1 से, इस मामले में uint32_t
. इससे sra
का शरीर बनता है
सिमैंटिक फ़ंक्शन:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
आगे बढ़ें और बदलाव करें और बिल्ड बनाएं. इनके हिसाब से अपने काम की जांच की जा सकती है rv32i_instructions.cc बहुत ज़रूरी है.
दो ऑपरेंड ALU निर्देश जोड़ें
सिर्फ़ दो 2-ऑपरेशन वाले ALU निर्देश होते हैं: lui
और auipc
. पहला
पहले से शिफ़्ट किए गए सोर्स ऑपरेंड को सीधे डेस्टिनेशन पर कॉपी करता है. बाद वाला
निर्देश के पते को लिखने से पहले
गंतव्य. निर्देश के पते को address()
तरीके से ऐक्सेस किया जा सकता है
Instruction
ऑब्जेक्ट में से.
सिर्फ़ एक सोर्स ऑपरेंड है, इसलिए हम इसके बजाय BinaryOp
का इस्तेमाल नहीं कर सकते
हमें UnaryOp
का इस्तेमाल करना होगा. चूंकि हम स्रोत और
गंतव्य ऑपरेंड को uint32_t
के रूप में हम एकल तर्क टेम्प्लेट का उपयोग कर सकते हैं
वर्शन है.
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
lui
के लिए सिमैंटिक फ़ंक्शन का मुख्य भाग उतना ही छोटा है जितना इसे हो सकता है,
बस सोर्स को लौटा दें. auipc
के सिमैंटिक फ़ंक्शन में नाबालिग की जानकारी दी जाती है
समस्या है, क्योंकि आपको Instruction
में address()
तरीके को ऐक्सेस करने की ज़रूरत है
इंस्टेंस. इसका जवाब है: instruction
को Lambda कैप्चर में जोड़कर,
Lambda फ़ंक्शन के मुख्य भाग में इस्तेमाल करने के लिए उपलब्ध है. पहले की तरह [](uint32_t a) { ...
}
के बजाय, Lambda फ़ंक्शन को [instruction](uint32_t a) { ... }
लिखा जाना चाहिए.
अब instruction
का इस्तेमाल, Lambda बॉडी में किया जा सकता है.
आगे बढ़ें और बदलाव करें और बिल्ड बनाएं. इनके हिसाब से अपने काम की जांच की जा सकती है rv32i_instructions.cc बहुत ज़रूरी है.
कंट्रोल फ़्लो में बदलाव करने के निर्देश जोड़ें
आपके लिए लागू किए जाने वाले कंट्रोल फ़्लो बदलने के निर्देश दो हिस्सों में बंटे हैं शर्तों के हिसाब से तय की गई, तुलना सही होती है) और जंप-ऐंड-लिंक निर्देश होते हैं, जिनका इस्तेमाल इन कामों के लिए किया जाता है फ़ंक्शन कॉल लागू करें (लिंक सेट करने पर -और-लिंक को हटा दिया जाता है शून्य पर रजिस्टर करते हैं, जिससे उन्हें कोई ऑपरेशन नहीं लिखा जाता.
कंडिशनल ब्रांच के लिए निर्देश जोड़ें
ब्रांच में निर्देश देने के लिए कोई हेल्पर फ़ंक्शन नहीं है, इसलिए इसके दो विकल्प हैं. सिमैंटिक फ़ंक्शन शुरू से लिखें या लोकल हेल्पर फ़ंक्शन लिखें. हमें छह ब्रांच निर्देशों को लागू करने की ज़रूरत है. इसलिए, बाद वाला कोड लागू प्रयास. ऐसा करने से पहले, चलिए किसी ब्रांच को लागू करने का तरीका देखते हैं शुरू से ही निर्देशों वाले सिमैंटिक फ़ंक्शन.
void MyConditionalBranchGreaterEqual(Instruction *instruction) {
int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
if (a >= b) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0,m target);
db->Submit();
}
}
ब्रांच के निर्देशों में सिर्फ़ ब्रांच की बात होती है
स्थिति, और दोनों में से हस्ताक्षर किए गए बनाम अहस्ताक्षरित 32 बिट पूर्णांक
सोर्स ऑपरेंड. इसका मतलब यह है कि हमें
सोर्स ऑपरेंड. हेल्पर फ़ंक्शन को खुद ही Instruction
लेने की ज़रूरत है
इंस्टेंस और कॉल करने लायक ऑब्जेक्ट जैसे std::function
जो bool
लौटाता है
पैरामीटर के तौर पर. हेल्पर फ़ंक्शन ऐसा दिखेगा:
template <typename OperandType>
static inline void BranchConditional(
Instruction *instruction,
std::function<bool(OperandType, OperandType)> cond) {
OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
if (cond(a, b)) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, target);
db->Submit();
}
}
अब हम bge
(साइन की गई ब्रांच बड़ी या बराबर) सिमैंटिक फ़ंक्शन लिख सकते हैं
जैसे:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
ब्रांच के लिए बाकी निर्देश यहां दिए गए हैं:
- Beq - ब्रांच बराबर है.
- Bgeu - ब्रांच ज़्यादा या इसके बराबर है (साइन नहीं किया गया).
- Blt - ब्रांच, इससे छोटी है (साइन किया गया).
- Bltu - ब्रांच, इससे छोटी है (साइन नहीं की गई).
- Bne - ब्रांच बराबर नहीं है.
इन सिमैंटिक फ़ंक्शन को लागू करने के लिए, आगे बढ़ें और बदलाव करें. फिर से बनाना होगा. इनके हिसाब से अपने काम की जांच की जा सकती है rv32i_instructions.cc बहुत ज़रूरी है.
जंप-ऐंड-लिंक के निर्देश जोड़ें
जंप और लिंक के लिए हेल्पर फ़ंक्शन लिखने का कोई मतलब नहीं है तो हमें इन्हें शुरुआत से लिखना होगा. चलिए, इनसे शुरू करते हैं वे उनके सिमैंटिक को देख रहे थे.
jal
निर्देश, सोर्स ऑपरेंड 0 से ऑफ़सेट लेता है और इसे
जंप टारगेट की गणना करने के लिए मौजूदा पीसी (निर्देश का पता) का इस्तेमाल करें. जंप टारगेट
डेस्टिनेशन ऑपरेंड 0 में लिखा जाता है. आइटम लौटाने का पता,
क्रम में चलने वाला अगला निर्देश. इसका हिसाब लगाने के लिए, मौजूदा
में दिए गए साइज़ की जानकारी भी देनी होगी. आइटम लौटाने का पता इस पते पर भेजा जाता है:
डेस्टिनेशन ऑपरेंड 1. निर्देश ऑब्जेक्ट पॉइंटर को शामिल करना न भूलें
लैम्डा कैप्चर किया जा सकता है.
jalr
निर्देश, सोर्स ऑपरेंड 0 और ऑफ़सेट के तौर पर बेस रजिस्टर लेता है
को सोर्स ऑपरेंड 1 के रूप में शामिल करता है और जंप टारगेट को कैलकुलेट करने के लिए उन्हें एक साथ जोड़ता है.
ऐसा न होने पर, यह jal
निर्देश के जैसा होगा.
निर्देश सिमैंटिक के इन ब्यौरों के आधार पर, दो सिमैंटिक लिखें फ़ंक्शन और बिल्ड. इनके हिसाब से अपने काम की जांच की जा सकती है rv32i_instructions.cc बहुत ज़रूरी है.
मेमोरी स्टोर के लिए निर्देश जोड़ें
हमें स्टोर के तीन निर्देश लागू करने होंगे: स्टोर बाइट
(sb
), आधा शब्द सेव करें (sh
), और शब्द सेव करें (sw
). स्टोर के लिए निर्देश
हमारे द्वारा अभी तक लागू किए गए निर्देशों से अलग है, क्योंकि वे
स्थानीय प्रोसेसर स्टेट पर लिखें. इसके बजाय वे किसी सिस्टम संसाधन पर लिखते हैं -
मुख्य मेमोरी. MPACT-Sim, मेमोरी को निर्देश ऑपरेंड के तौर पर इस्तेमाल नहीं करता.
इसलिए, मेमोरी का ऐक्सेस किसी दूसरे तरीके से ऐक्सेस करना होगा.
इसका जवाब है, MPACT-Sim ArchState
ऑब्जेक्ट में मेमोरी के ऐक्सेस के तरीके जोड़ना,
या ज़्यादा ठीक से, एक नया RiscV स्टेट ऑब्जेक्ट बनाएं, जो ArchState
से लिया गया हो
जहां इसे जोड़ा जा सकता है. ArchState
ऑब्जेक्ट, मुख्य संसाधनों को मैनेज करता है, जैसे कि
रजिस्टर और अन्य स्टेट ऑब्जेक्ट होते हैं. यह इन कामों में इस्तेमाल होने वाली देरी की लाइनों को भी मैनेज करता है
डेस्टिनेशन ऑपरेंड डेटा बफ़र को तब तक बफ़र करें, जब तक उन्हें वापस लिखा न जा सके
रजिस्टर ऑब्जेक्ट. ज़्यादातर निर्देश, आपकी जानकारी के बिना लागू किए जा सकते हैं
यह क्लास है, लेकिन कुछ, जैसे मेमोरी से जुड़ी कार्रवाइयां और दूसरे खास सिस्टम
निर्देशों के लिए फ़ंक्शन को इस स्टेट ऑब्जेक्ट में रखने की ज़रूरत है.
आइए, fence
निर्देश के लिए सिमैंटिक फ़ंक्शन पर नज़र डालते हैं. यह फ़ंक्शन
को उदाहरण के तौर पर rv32i_instructions.cc
में पहले ही लागू कर दिया गया है. fence
जब तक मेमोरी से जुड़ी कुछ कार्रवाइयों की वजह से
पूरा हुआ. इसका इस्तेमाल निर्देशों के बीच मेमोरी को क्रम में लगाने की गारंटी के लिए किया जाता है
और फिर, जो निर्देश से पहले एक्ज़ीक्यूट किए जाते हैं.
// Fence.
void RV32IFence(Instruction *instruction) {
uint32_t bits = instruction->Source(0)->AsUint32(0);
int fm = (bits >> 8) & 0xf;
int predecessor = (bits >> 4) & 0xf;
int successor = bits & 0xf;
auto *state = static_cast<RiscVState *>(instruction->state());
state->Fence(instruction, fm, predecessor, successor);
}
fence
निर्देश के सिमैंटिक फ़ंक्शन का मुख्य हिस्सा, आखिरी दो हैं
लाइन. सबसे पहले, स्टेट ऑब्जेक्ट को Instruction
में दिए गए तरीके का इस्तेमाल करके फ़ेच किया जाता है
क्लास और downcast<>
को RiscV खास डिराइव्ड क्लास में बदल दिया जाता है. इसके बाद, Fence
RiscVState
क्लास वाले तरीके का इस्तेमाल, फ़ेंस चलाने के लिए किया जाता है.
स्टोर के निर्देश इसी तरह काम करेंगे. सबसे पहले,
मेमोरी के ऐक्सेस का हिसाब, बेस और ऑफ़सेट निर्देश के सोर्स ऑपरेंड से लगाया जाता है,
तो स्टोर किया जाने वाला मान अगले सोर्स ऑपरेंड से फ़ेच किया जाता है. इसके बाद,
RiscV स्टेट ऑब्जेक्ट, state()
तरीके कॉल के ज़रिए लिया जाता है और
static_cast<>
और सही तरीका कॉल किया जाता है.
RiscVState
ऑब्जेक्ट StoreMemory
वाला तरीका बाकी है, लेकिन इसमें
ये कुछ बातें हैं जिनके बारे में हमें पता होना चाहिए:
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
जैसा कि हम देख सकते हैं कि इस तरीके में तीन पैरामीटर होते हैं, यानी स्टोर के पॉइंटर
निर्देश, स्टोर का पता, और DataBuffer
के लिए पॉइंटर
में स्टोर डेटा मौजूद होता है. ध्यान दें, किसी साइज़ की ज़रूरत नहीं है,
DataBuffer
इंस्टेंस में अपने-आप एक size()
तरीका होता है. हालांकि, इस तरह की कोई
निर्देश के लिए ऐक्सेस किए जा सकने वाले डेस्टिनेशन ऑपरेंड का इस्तेमाल
सही साइज़ का DataBuffer
इंस्टेंस असाइन करें. इसके बजाय, हमें यह करना होगा
DataBuffer
की ऐसी फ़ैक्ट्री का इस्तेमाल करें जो db_factory()
तरीके से मिली है
Instruction
इंस्टेंस. फ़ैक्ट्री में Allocate(int size)
तरीका इस्तेमाल किया गया है
जो ज़रूरी साइज़ का DataBuffer
इंस्टेंस दिखाता है. यहां एक उदाहरण दिया गया है
यह बताता है कि हाफ़-वर्ड स्टोर के लिए DataBuffer
इंस्टेंस असाइन करने के लिए, इसका इस्तेमाल कैसे करें
(ध्यान दें कि auto
एक C++ सुविधा है, जो दाईं ओर से टाइप का पता लगाती है
असाइनमेंट की ओर से नीचे दिए गए निर्देशों का पालन करें):
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
DataBuffer
इंस्टेंस मिलने के बाद, हम इस पर हमेशा की तरह जवाब दे सकते हैं:
db->Set<uint16_t>(0, value);
इसके बाद, इसे मेमोरी स्टोर इंटरफ़ेस में भेजें:
state->StoreMemory(instruction, address, db);
हमने अभी तक काम पूरा नहीं किया है. DataBuffer
इंस्टेंस को रेफ़रंस के तौर पर गिना जाता है. यह
आम तौर पर, इसे Submit
तरीके से समझा और मैनेज किया जाता है, ताकि
इस्तेमाल करने का सबसे आसान तरीका है. हालांकि, StoreMemory
इस तरीके से लिखा जाता है. यह काम करते समय, DataBuffer
इंस्टेंस को IncRef
करेगा
इस पर DecRef
. हालांकि, अगर सिमैंटिक फ़ंक्शन
अपनी पहचान फ़ाइल DecRef
बनाएं. इसके लिए, कभी भी फिर से दावा नहीं किया जाएगा. इसलिए, अंतिम पंक्ति में
इसे:
db->DecRef();
स्टोर फ़ंक्शन तीन तरह के होते हैं, और केवल एक चीज़ जो अलग होती है वह है
मेमोरी ऐक्सेस. अन्य स्थानीय लोगों के लिए यह एक अच्छा अवसर लग रहा है
टेंप्लेट वाला हेल्पर फ़ंक्शन. स्टोर फ़ंक्शन में सिर्फ़ एक चीज़ अलग है
स्टोर वैल्यू का टाइप है, इसलिए टेंप्लेट में इसे आर्ग्युमेंट के तौर पर शामिल करना चाहिए.
इसके अलावा, इसमें सिर्फ़ Instruction
इंस्टेंस पास होना चाहिए:
template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->Set<ValueType>(0, value);
state->StoreMemory(instruction, address, db);
db->DecRef();
}
आगे बढ़ें और स्टोर सिमैंटिक फ़ंक्शन को पूरा करें और बिल्ड करें. आप इसके ख़िलाफ़ काम करना rv32i_instructions.cc बहुत ज़रूरी है.
मेमोरी लोड करने के निर्देश जोड़ें
लोड करने के निर्देश नीचे दिए गए हैं, जिन्हें लागू करना ज़रूरी है:
lb
- बाइट लोड करें, किसी शब्द में हस्ताक्षर करें.lbu
- बिना साइन इन किए हुए बाइट लोड करें, शून्य एक्सटेंशन को एक शब्द में लोड करें.lh
- आधा शब्द लोड करें, हस्ताक्षर का एक शब्द बनाएं.lhu
- बिना हस्ताक्षर वाला आधा शब्द लोड करें, शून्य एक्सटेंशन को एक शब्द में लोड करें.lw
- शब्द लोड करें.
लोड करने के निर्देश, मॉडल करने के लिए सबसे जटिल निर्देश हैं
इस ट्यूटोरियल को देखें. वे स्टोर के निर्देशों से मिलते-जुलते होते हैं, ताकि
RiscVState
ऑब्जेक्ट को ऐक्सेस करता है, लेकिन हर लोड में जटिलता जोड़ता है
निर्देशों को दो अलग-अलग सिमैंटिक फ़ंक्शन में बांटा गया है. पहला है
स्टोर निर्देश की तरह है, जिसमें यह असरदार पते का पता लगाता है
और मेमोरी का ऐक्सेस शुरू करता है. दूसरा चरण तब लागू होता है, जब मेमोरी
ऐक्सेस पूरा हो जाता है और रजिस्टर डेस्टिनेशन में मेमोरी डेटा लिखता है
ऑपरेंड.
चलिए, RiscVState
में LoadMemory
तरीके का एलान करने वाले फ़ॉर्म पर नज़र डालते हैं:
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
StoreMemory
तरीके की तुलना में, LoadMemory
दो अतिरिक्त चरण
पैरामीटर: किसी Instruction
इंस्टेंस के लिए पॉइंटर और एक पॉइंटर
रेफ़रंस की गिनती context
ऑब्जेक्ट के तौर पर की गई. पहला नियम, बच्चे के लिए दिया गया निर्देश है,
रजिस्टर राइट-बैक लागू करता है (ISA डिकोडर ट्यूटोरियल में बताया गया है). यह
को मौजूदा Instruction
इंस्टेंस में, child()
तरीके का इस्तेमाल करके ऐक्सेस किया जाता है.
बाद वाला विकल्प, क्लास के इंस्टेंस का पॉइंटर है, जो इनसे लिया जाता है
ReferenceCount
, जो इस मामले में DataBuffer
इंस्टेंस सेव करता है, जो
में लोड किया गया डेटा शामिल नहीं होता है. कॉन्टेक्स्ट ऑब्जेक्ट इनके ज़रिए उपलब्ध होता है:
Instruction
ऑब्जेक्ट में context()
तरीका (हालांकि, ज़्यादातर निर्देशों के लिए
यह nullptr
पर सेट है).
RiscV मेमोरी लोड के लिए कॉन्टेक्स्ट ऑब्जेक्ट को इस स्ट्रक्चर के तौर पर तय किया गया है:
// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
~LoadContext() override {
if (value_db != nullptr) value_db->DecRef();
}
// Override the base class method so that the data buffer can be DecRef'ed
// when the context object is recycled.
void OnRefCountIsZero() override {
if (value_db != nullptr) value_db->DecRef();
value_db = nullptr;
// Call the base class method.
generic::ReferenceCount::OnRefCountIsZero();
}
// Data buffers for the value loaded from memory (byte, half, word, etc.).
DataBuffer *value_db = nullptr;
};
डेटा साइज़ (बाइट, और लोड किया गया मान साइन-आउट किया हुआ है या नहीं. कॉन्टेंट बनाने बाद में, सिर्फ़ चाइल्ड वाले निर्देश का इस्तेमाल किया जाता है. चलिए, टेंप्लेट बनाया जाता है सहायक फ़ंक्शन का इस्तेमाल करें. यह काफ़ी हद तक स्टोर निर्देश नहीं देता, लेकिन यह कोई मान पाने के लिए सोर्स ऑपरेंड को ऐक्सेस नहीं करेगा, और यह एक कॉन्टेक्स्ट ऑब्जेक्ट बनाएगा.
template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->set_latency(0);
auto *context = new riscv::LoadContext(db);
state->LoadMemory(instruction, address, db, instruction->child(), context);
context->DecRef();
}
जैसा कि आपको दिख रहा है, मुख्य अंतर यह है कि असाइन किया गया DataBuffer
इंस्टेंस
दोनों को एक पैरामीटर के रूप में LoadMemory
कॉल में पास किया जाता है और
LoadContext
ऑब्जेक्ट.
चाइल्ड निर्देश सिमैंटिक फ़ंक्शन एक-दूसरे से बहुत मिलते-जुलते हैं. सबसे पहले,
LoadContext
पाने के लिए, Instruction
तरीके को context()
पर कॉल करें और
LoadContext *
पर स्टैटिक कास्ट किया गया. दूसरा, वैल्यू (डेटा के मुताबिक
type) को लोड-डेटा DataBuffer
इंस्टेंस से पढ़ा जाता है. तीसरा, एक नया
DataBuffer
इंस्टेंस, डेस्टिनेशन ऑपरेंड से असाइन किया गया है. आख़िर में,
लोड की गई वैल्यू को नए DataBuffer
इंस्टेंस पर लिखा जाता है और Submit
किया जाता है.
एक बार फिर, टेंप्लेट वाला हेल्पर फ़ंक्शन एक अच्छा आइडिया है:
template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
auto *context = down_cast<riscv::LoadContext *>(instruction->context());
uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, value);
db->Submit();
}
अब इन आखिरी हेल्पर फ़ंक्शन और सिमैंटिक फ़ंक्शन को लागू करें. पेमेंट करें हर हेल्पर फ़ंक्शन के लिए, टेंप्लेट में आपके इस्तेमाल किए गए डेटा टाइप पर ध्यान दें कॉल करता है और यह कि वह लोड के आकार और हस्ताक्षर किए गए/हस्ताक्षरित नहीं किए गए प्रकार से मेल खाता है निर्देश दिए गए हों.
इनके हिसाब से अपने काम की जांच की जा सकती है rv32i_instructions.cc बहुत ज़रूरी है.
फ़ाइनल सिम्युलेटर बनाएं और चलाएं
अब जबकि हमने अंतिम सिम्युलेटर बनाने के लिए पूरी मेहनत कर ली है. कॉन्टेंट बनाने
C++ वाली टॉप लेवल लाइब्रेरी, जो इन ट्यूटोरियल में मौजूद सभी कामों को एक साथ जोड़ती हैं
other/
में मौजूद है. इस कोड को देखने पर ज़्यादा ध्यान देना ज़रूरी नहीं है. बुध
अगले ऐडवांस ट्यूटोरियल में उस विषय के बारे में जानेंगे.
अपनी वर्किंग डायरेक्ट्री को other/
में बदलें और बिल्ड करें. इसे बिना किसी रुकावट के बनाना चाहिए
गड़बड़ियां हैं.
$ cd ../other
$ bazel build :rv32i_sim
उस डायरेक्ट्री में एक सामान्य "हैलो वर्ल्ड" है फ़ाइल में प्रोग्राम
hello_rv32i.elf
. इस फ़ाइल पर सिम्युलेटर चलाने और नतीजे देखने के लिए:
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
आपको इन लाइनों के साथ कुछ दिखेगा:
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$
bazel
run :rv32i_sim -- -i other/hello_rv32i.elf
निर्देश का इस्तेमाल करके, सिम्युलेटर को इंटरैक्टिव मोड में भी चलाया जा सकता है. इससे पता चलता है कि
कमांड शेल. उपलब्ध निर्देशों को देखने के लिए, प्रॉम्प्ट पर help
टाइप करें.
$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000 addi ra, 0, 0
[0] > help
quit - exit command shell.
core [N] - direct subsequent commands to core N
(default: 0).
run - run program from current pc until a
breakpoint or exit. Wait until halted.
run free - run program in background from current pc
until breakpoint or exit.
wait - wait for any free run to complete.
step [N] - step [N] instructions (default: 1).
halt - halt a running program.
reg get NAME [FORMAT] - get the value or register NAME.
reg NAME [FORMAT] - get the value of register NAME.
reg set NAME VALUE - set register NAME to VALUE.
reg set NAME SYMBOL - set register NAME to value of SYMBOL.
mem get VALUE [FORMAT] - get memory from location VALUE according to
format. The format is a letter (o, d, u, x,
or X) followed by width (8, 16, 32, 64).
The default format is x32.
mem get SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem set VALUE [FORMAT] VALUE - set memory at location VALUE(1) to VALUE(2)
according to FORMAT. Default format is x32.
mem set SYMBOL [FORMAT] VALUE - set memory at location SYMBOL to VALUE
according to FORMAT. Default format is x32.
break set VALUE - set breakpoint at address VALUE.
break set SYMBOL - set breakpoint at value of SYMBOL.
break VALUE - set breakpoint at address VALUE.
break SYMBOL - set breakpoint at value of SYMBOL.
break clear VALUE - clear breakpoint at address VALUE.
break clear SYMBOL - clear breakpoint at value of SYMBOL.
break clear all - remove all breakpoints.
help - display this message.
_start:
80000000 addi ra, 0, 0
[0] >
इसी के साथ यह ट्यूटोरियल खत्म होता है. हमें उम्मीद है कि इससे आपको मदद मिली होगी.