Mục tiêu của hướng dẫn này là:
- Tìm hiểu cấu trúc và cú pháp của tệp mô tả định dạng nhị phân.
- Tìm hiểu cách nội dung mô tả định dạng tệp nhị phân khớp với nội dung mô tả ISA.
- Viết các mô tả nhị phân cho tập hợp con của lệnh RiscV RV32I.
Tổng quan
Mã hoá hướng dẫn nhị phân RiscV
Mã hoá lệnh nhị phân là cách tiêu chuẩn để mã hoá lệnh thực thi trên vi xử lý. Các tệp này thường được lưu trữ trong một tệp thực thi, thường ở định dạng ELF. Hướng dẫn có thể có chiều rộng cố định hoặc chiều rộng biến.
Thông thường, các hướng dẫn sử dụng một nhóm nhỏ các định dạng mã hoá, trong đó mỗi định dạng được tuỳ chỉnh cho loại hướng dẫn được mã hoá. Ví dụ: lệnh đăng ký-đăng ký có thể sử dụng một định dạng giúp tối đa hoá số lượng mã opcode có sẵn, trong khi lệnh đăng ký-ngay lập tức sử dụng một định dạng khác để đánh đổi số lượng mã opcode có sẵn nhằm tăng kích thước của mã ngay lập tức có thể được mã hoá. Lệnh nhánh và lệnh nhảy hầu như luôn sử dụng các định dạng giúp tối đa hoá kích thước của lệnh tức thì để hỗ trợ các nhánh có độ dời lớn hơn.
Các định dạng lệnh mà các lệnh chúng ta muốn giải mã trong trình mô phỏng RiscV là:
Định dạng R-Type, dùng cho hướng dẫn khi đăng ký tài khoản:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | rd | mã opcode |
Định dạng I-Type, dùng cho các lệnh đăng ký-ngay lập tức, lệnh tải và lệnh jalr
, 12 bit ngay lập tức.
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | mã opcode |
Định dạng I-Type chuyên biệt, dùng để chuyển với hướng dẫn tức thì, 5 bit tức thì:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | rd | mã opcode |
Định dạng U-Type, dùng cho các lệnh tức thì dài (lui
, auipc
), tức thì 20 bit:
31..12 | 11..7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | rd | mã opcode |
Định dạng loại B, dùng cho các nhánh có điều kiện, tức thì 12 bit.
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 | mã opcode |
Định dạng J-Type, dùng cho lệnh jal
, 20 bit tức thì.
31 | 30..21 | 20 | 19..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
imm | imm | imm | imm | rd | mã opcode |
Định dạng S-Type, dùng cho các lệnh lưu trữ, 12 bit tức thì.
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm | rs2 | rs1 | func3 | imm | mã opcode |
Như bạn có thể thấy từ các định dạng này, tất cả các lệnh này đều có độ dài 32 bit và 7 bit thấp trong mỗi định dạng là trường opcode. Ngoài ra, hãy lưu ý rằng mặc dù một số định dạng có cùng kích thước ngay lập tức, nhưng các bit của các định dạng này được lấy từ các phần khác nhau của lệnh. Như chúng ta sẽ thấy, định dạng thông số kỹ thuật của bộ giải mã nhị phân có thể thể hiện điều này.
Mô tả về cách mã hoá nhị phân
Phương thức mã hoá nhị phân của lệnh được thể hiện ở tệp mô tả ở định dạng nhị phân (.bin_fmt
). Tệp này mô tả quá trình mã hoá nhị phân của các lệnh trong ISA để có thể tạo trình giải mã lệnh ở định dạng nhị phân. Bộ giải mã được tạo xác định mã opcode, trích xuất giá trị của toán hạng và các trường tức thì để cung cấp thông tin cần thiết cho bộ giải mã không phân biệt mã hoá ISA được mô tả trong hướng dẫn trước.
Trong hướng dẫn này, chúng ta sẽ viết một tệp mô tả mã hoá nhị phân cho một tập hợp con của các lệnh RiscV32I cần thiết để mô phỏng các lệnh được sử dụng trong một chương trình "Hello World" nhỏ. Để biết thêm thông tin chi tiết về RiscV ISA, hãy xem Thông số kỹ thuật Risc-V{.external}.
Hãy bắt đầu bằng cách mở tệp:
riscv_bin_decoder/riscv32i.bin_fmt
.
Nội dung của tệp được chia thành nhiều phần.
Trước tiên, hãy xem định nghĩa 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;
};
Định nghĩa bộ giải mã của chúng ta chỉ định tên của bộ giải mã RiscV32I
, cũng như bốn thông tin bổ sung. Tệp đầu tiên là namespace
, xác định không gian tên nơi mã được tạo sẽ được đặt. Thứ hai, opcode_enum
, tên cách tham chiếu loại liệt kê opcode do bộ giải mã ISA tạo trong mã được tạo. Thứ ba, includes {}
chỉ định bao gồm các tệp cần thiết cho mã được tạo cho bộ giải mã này.
Trong trường hợp của chúng ta, đây là tệp do bộ giải mã ISA tạo ra từ hướng dẫn trước.
Bạn có thể chỉ định các tệp bao gồm bổ sung trong định nghĩa includes {}
ở phạm vi toàn cục. Điều này hữu ích nếu bạn xác định nhiều bộ giải mã và tất cả các bộ giải mã đó đều cần bao gồm một số tệp giống nhau. Thứ tư là danh sách tên của các nhóm lệnh tạo thành các lệnh mà bộ giải mã được tạo. Trong trường hợp này, chỉ có một: RiscVInst32
.
Tiếp theo là ba định nghĩa về định dạng. Các giá trị này đại diện cho nhiều định dạng lệnh cho một từ lệnh 32 bit mà các lệnh đã xác định trong tệp sử dụng.
// 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];
};
Phần đầu tiên xác định định dạng lệnh rộng 32 bit có tên là Inst32Format
, có hai trường: bits
(rộng 25 bit) và opcode
(rộng 7 bit). Mỗi trường là unsigned
, nghĩa là giá trị sẽ được mở rộng bằng 0 khi được trích xuất và đặt vào loại số nguyên C++. Tổng chiều rộng của các trường bit phải bằng với chiều rộng của định dạng. Công cụ này sẽ báo lỗi nếu có bất đồng. Định dạng này không bắt nguồn từ bất kỳ định dạng nào khác, vì vậy, đây được coi là định dạng cấp cao nhất.
Định dạng thứ hai xác định định dạng lệnh rộng 32 bit có tên là IType
, bắt nguồn từ Inst32Format
, giúp hai định dạng này có liên quan với nhau. Định dạng này chứa 5 trường: imm12
, rs1
, func3
, rd
và opcode
. Trường imm12
là signed
, nghĩa là giá trị sẽ được mở rộng dấu khi giá trị được trích xuất và đặt trong loại số nguyên C++. Lưu ý rằng IType.opcode
có cùng thuộc tính đã ký/chưa ký và tham chiếu đến cùng một bit từ lệnh với Inst32Format.opcode
.
Định dạng thứ ba là định dạng tuỳ chỉnh chỉ được sử dụng theo lệnh fence
. Đây là lệnh đã được chỉ định và chúng ta không cần lo lắng trong hướng dẫn này.
Điểm chính: Sử dụng lại tên trường ở các định dạng liên quan khác nhau, miễn là các tên đó đại diện cho cùng một bit và có cùng thuộc tính đã ký/chưa ký.
Sau các phần định nghĩa trong riscv32i.bin_fmt
sẽ có phần định nghĩa nhóm lệnh. Tất cả lệnh trong một nhóm lệnh phải có cùng độ dài bit và sử dụng định dạng bắt nguồn (có thể gián tiếp) từ cùng một định dạng lệnh cấp cao nhất. Khi một ISA có thể có các lệnh với nhiều độ dài, thì một nhóm lệnh khác nhau sẽ được sử dụng cho mỗi độ dài. Ngoài ra, nếu quá trình giải mã ISA mục tiêu phụ thuộc vào chế độ thực thi, chẳng hạn như lệnh Arm so với lệnh Thumb, thì mỗi chế độ cần có một nhóm lệnh riêng. Trình phân tích cú pháp bin_fmt
tạo ra một bộ giải mã nhị phân cho từng nhóm lệnh.
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;
};
Nhóm lệnh xác định tên RiscV32I
, chiều rộng [32]
, tên của loại liệt kê mã opcode để sử dụng "OpcodeEnum"
và định dạng lệnh cơ sở. Loại liệt kê mã opcode phải giống với loại do bộ giải mã lệnh độc lập về định dạng tạo ra trong hướng dẫn về bộ giải mã ISA.
Mỗi nội dung mô tả về cách mã hoá lệnh bao gồm 3 phần:
- Tên mã opcode phải giống với tên được dùng trong phần mô tả bộ giải mã lệnh để hai tên này hoạt động cùng nhau.
- Định dạng hướng dẫn để sử dụng cho mã opcode. Đây là định dạng được dùng để đáp ứng các tệp tham chiếu đến trường bit trong phần cuối cùng.
- Danh sách các quy tắc ràng buộc trường bit được phân tách bằng dấu phẩy,
==
,!=
,<
,<=
,>
và>=
. Tất cả các quy tắc này đều phải đúng để mã opcode có thể khớp thành công với từ lệnh.
Trình phân tích cú pháp .bin_fmt
sử dụng tất cả thông tin này để tạo một bộ giải mã:
- Cung cấp các hàm trích xuất (đã ký/chưa ký) phù hợp với từng trường bit ở mọi định dạng. Các hàm trích xuất được đặt trong không gian tên được đặt theo phiên bản snake-case của tên định dạng. Ví dụ: các hàm trích xuất cho định dạng
IType
được đặt trong không gian têni_type
. Mỗi hàm trích xuất được khai báo làinline
, lấy kiểuuint_t
hẹp nhất chứa chiều rộng của định dạng và trả về kiểuint_t
(đối với chữ ký) hoặcuint_t
(đối với chữ ký không có chữ ký) hẹp nhất chứa chiều rộng trường được trích xuất. Ví dụ:
inline uint8_t ExtractOpcode(uint32_t value) {
return value & 0x7f;
}
- Hàm giải mã cho mỗi nhóm lệnh. Phương thức này trả về giá trị thuộc loại
OpcodeEnum
và lấy loạiuint_t
hẹp nhất chứa chiều rộng của định dạng nhóm lệnh.
Thực hiện quá trình dựng ban đầu
Thay đổi thư mục thành riscv_bin_decoder
và tạo dự án bằng lệnh sau:
$ cd riscv_bin_decoder
$ bazel build :all
Bây giờ, hãy thay đổi thư mục của bạn trở lại thư mục gốc của kho lưu trữ, sau đó hãy xem các nguồn đã được tạo. Để làm việc đó, hãy thay đổi thư mục thành bazel-out/k8-fastbuild/bin/riscv_bin_decoder
(giả sử bạn đang sử dụng máy chủ x86 – đối với các máy chủ khác, k8-fastbuild sẽ là một chuỗi khác).
$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_bin_decoder
riscv32i_bin_decoder.h
riscv32i_bin_decoder.cc
Tệp tiêu đề đã tạo (.h)
Mở tệp riscv32i_bin_decoder.h
. Phần đầu của tệp chứa các trình bảo vệ nguyên mẫu tiêu chuẩn, bao gồm các tệp, khai báo không gian tên. Tiếp theo đó là một hàm trợ giúp được tạo mẫu trong không gian tên internal
. Hàm này được dùng để trích xuất các trường bit từ các định dạng quá dài để vừa với số nguyên C++ 64 bit.
#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
Sau phần ban đầu, có một tập hợp gồm 3 không gian tên, mỗi không gian tên cho một phần khai báo format
trong tệp riscv32i.bin_fmt
:
namespace fence {
...
} // namespace fence
namespace i_type {
...
} // namespace i_type
namespace inst32_format {
...
} // namespace inst32_format
Trong mỗi không gian tên này, hàm trích xuất bit inline
cho từng trường bit ở định dạng đó được xác định. Ngoài ra, định dạng cơ sở sẽ sao chép các hàm trích xuất từ các định dạng con, trong đó 1) tên trường chỉ xuất hiện trong một tên trường duy nhất hoặc 2) trong đó các tên trường tham chiếu đến cùng một trường loại (vị trí đã ký/chưa ký và vị trí bit) ở mỗi định dạng xuất hiện. Điều này cho phép các trường bit mô tả cùng một bit được trích xuất bằng các hàm trong không gian tên định dạng cấp cao nhất.
Các hàm trong không gian tên i_type
được hiển thị dưới đây:
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
Cuối cùng, bạn cần khai báo phần khai báo hàm của hàm giải mã cho nhóm lệnh RiscVInst32
. Hàm này lấy một số không dấu 32 bit làm giá trị của từ lệnh và trả về thành phần lớp liệt kê OpcodeEnum
khớp hoặc OpcodeEnum::kNone
nếu không có kết quả khớp.
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Tệp nguồn được tạo (.cc)
Bây giờ, hãy mở riscv32i_bin_decoder.cc
. Phần đầu tiên của tệp chứa nội dung khai báo #include
và không gian tên, tiếp theo là nội dung khai báo hàm giải mã:
#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
được dùng cho các thao tác giải mã trống, tức là các thao tác trả về OpcodeEnum::kNone
. Ba hàm còn lại tạo thành bộ giải mã đã tạo. Bộ giải mã tổng thể hoạt động theo kiểu phân cấp. Một tập hợp các bit trong từ lệnh được tính toán để phân biệt giữa các lệnh hoặc nhóm lệnh ở cấp cao nhất. Các bit không cần tiếp giáp nhau. Số bit xác định kích thước của bảng tra cứu được điền sẵn bằng các hàm giải mã cấp hai. Bạn có thể xem nội dung này trong phần tiếp theo của tệp:
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,
...
};
Cuối cùng, các hàm giải mã được xác định:
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;
}
Trong trường hợp này, chỉ có 4 lệnh được xác định, chỉ có một cấp giải mã và bảng tra cứu rất thưa thớt. Khi thêm các lệnh, cấu trúc của bộ giải mã sẽ thay đổi và số lượng cấp trong hệ phân cấp bảng bộ giải mã có thể tăng lên.
Thêm hướng dẫn ALU đăng ký-đăng ký
Giờ là lúc thêm một số hướng dẫn mới vào tệp riscv32i.bin_fmt
. Nhóm lệnh đầu tiên là các lệnh ALU đăng ký-đăng ký như add
, and
, v.v. Trên RiscV32, tất cả các lệnh này đều sử dụng định dạng lệnh nhị phân loại R:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | rd | mã hoạt động |
Việc đầu tiên chúng ta cần làm là thêm định dạng. Hãy tiếp tục và mở
riscv32i.bin_fmt
trong trình chỉnh sửa yêu thích của bạn. Ngay sau Inst32Format
, hãy thêm một định dạng có tên là RType
bắt nguồn từ Inst32Format
. Tất cả các trường bit trong RType
đều là unsigned
. Sử dụng tên, chiều rộng bit và thứ tự (từ trái sang phải) trong bảng ở trên để xác định định dạng. Nếu bạn cần gợi ý hoặc muốn xem giải pháp đầy đủ, hãy nhấp vào đây.
Tiếp theo, chúng ta cần thêm hướng dẫn. Hướng dẫn như sau:
add
– thêm số nguyên.and
– toán tử and bit.or
– hoặc bit.sll
– logic dịch sang trái.sltu
– đặt dấu nhỏ hơn, không dấu.sub
– trừ số nguyên.xor
– xor bit.
Mã hoá của các giá trị này là:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 | tên mã opcode |
---|---|---|---|---|---|---|
000 0000 | rs2 | rs1 | 000 | rd | 011 0011 | thêm |
000 0000 | rs2 | rs1 | 111 | rd | 011 0011 | và |
000 0000 | rs2 | rs1 | 110 | ngày | 011 0011 | hoặc |
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 | rd | 011 0011 | xor |
func7 | func3 | mã hoạt động |
Thêm các định nghĩa hướng dẫn này trước các hướng dẫn khác trong nhóm hướng dẫn RiscVInst32
. Chuỗi nhị phân được chỉ định bằng tiền tố dẫn đầu là 0b
(tương tự như 0x
cho số thập lục phân). Để dễ đọc các chuỗi dài gồm các chữ số nhị phân, bạn cũng có thể chèn dấu ngoặc đơn '
làm dấu phân cách chữ số khi thấy phù hợp.
Mỗi định nghĩa hướng dẫn này sẽ có 3 quy tắc ràng buộc, cụ thể là trên func7
, func3
và opcode
. Đối với tất cả trừ sub
, quy tắc ràng buộc func7
sẽ là:
func7 == 0b000'0000
Quy tắc ràng buộc func3
thay đổi tuỳ theo hầu hết các lệnh. Đối với add
và sub
, mã này là:
func3 == 0b000
Quy tắc ràng buộc opcode
giống nhau cho từng hướng dẫn sau:
opcode == 0b011'0011
Hãy nhớ kết thúc mỗi dòng bằng dấu chấm phẩy ;
.
Giải pháp hoàn chỉnh có tại đây.
Bây giờ, hãy tiếp tục tạo dự án như trước và mở tệp riscv32i_bin_decoder.cc
đã tạo. Bạn sẽ thấy các hàm giải mã bổ sung đã được tạo để xử lý các lệnh mới. Trong hầu hết các trường hợp, các hàm này tương tự như các hàm đã được tạo trước đó, nhưng hãy xem DecodeRiscVInst32_0_c
dùng để giải mã 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];
}
Trong hàm này, một bảng giải mã tĩnh được tạo và một giá trị tra cứu được trích xuất từ từ lệnh để chọn chỉ mục thích hợp. Thao tác này sẽ thêm một lớp thứ hai trong hệ phân cấp bộ giải mã lệnh, nhưng vì bạn có thể tra cứu trực tiếp mã opcode trong bảng mà không cần so sánh thêm, nên mã opcode này được nội tuyến trong hàm này thay vì yêu cầu một lệnh gọi hàm khác.
Thêm hướng dẫn ALU với giá trị tức thì
Tập hợp lệnh tiếp theo mà chúng ta sẽ thêm là các lệnh ALU sử dụng giá trị tức thì thay vì một trong các thanh ghi. Có ba nhóm lệnh này (dựa trên trường trực tiếp): lệnh trực tiếp loại I có giá trị trực tiếp đã ký 12 bit, lệnh trực tiếp loại I chuyên biệt cho các lệnh dịch và lệnh trực tiếp loại U, với giá trị trực tiếp chưa ký 20 bit. Các định dạng được hiển thị bên dưới:
Định dạng tức thì loại I:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | mã opcode |
Định dạng tức thì chuyên biệt của Loại I:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | rd | mã opcode |
Định dạng tức thì loại U:
31..12 | 11..7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | rd | mã opcode |
Định dạng I-Type đã tồn tại trong riscv32i.bin_fmt
, vì vậy, bạn không cần thêm định dạng đó.
Nếu so sánh định dạng I-Type chuyên biệt với định dạng R-Type mà chúng ta đã xác định trong bài tập trước, chúng ta thấy rằng sự khác biệt duy nhất là các trường rs2
được đổi tên thành uimm5
. Thay vì thêm một định dạng hoàn toàn mới, chúng ta có thể mở rộng định dạng R-Type. Chúng ta không thể thêm trường khác vì điều đó sẽ làm tăng chiều rộng của định dạng, nhưng chúng ta có thể thêm lớp phủ. Lớp phủ là một bí danh cho một nhóm bit trong định dạng và có thể được dùng để kết hợp nhiều trình tự con của định dạng thành một thực thể được đặt tên riêng biệt. Hiệu ứng phụ là mã được tạo giờ đây cũng sẽ bao gồm một hàm trích xuất cho lớp phủ, ngoài các hàm cho các trường. Trong trường hợp này, khi cả rs2
và uimm5
đều không được ký, điều này sẽ không tạo ra nhiều khác biệt ngoại trừ việc làm rõ rằng trường này được dùng làm trường ngay lập tức. Để thêm lớp phủ có tên uimm5
vào định dạng R-Type, hãy thêm nội dung sau vào sau trường cuối cùng:
overlays:
unsigned uimm5[5] = rs2;
Định dạng mới duy nhất chúng tôi cần thêm là định dạng U-Type. Trước khi thêm định dạng, hãy xem xét hai lệnh sử dụng định dạng đó: auipc
và lui
. Cả hai đều dịch chuyển giá trị tức thì 20 bit sang trái 12 trước khi sử dụng giá trị đó để thêm pc vào (auipc
) hoặc ghi trực tiếp vào thanh ghi (lui
). Bằng cách sử dụng lớp phủ, chúng ta có thể cung cấp phiên bản đã dịch chuyển trước của giá trị tức thì, dịch chuyển một chút phép tính từ việc thực thi lệnh sang giải mã lệnh. Trước tiên, hãy thêm định dạng theo các trường được chỉ định trong bảng ở trên. Sau đó, chúng ta có thể thêm lớp phủ sau:
overlays:
unsigned uimm32[32] = uimm20, 0b0000'0000'0000;
Cú pháp lớp phủ cho phép chúng ta nối không chỉ các trường mà còn cả các giá trị cố định. Trong trường hợp này, chúng ta nối chuỗi này với 12 số 0, do đó dịch chuyển chuỗi này sang trái 12.
Các hướng dẫn Loại I mà chúng ta cần thêm là:
addi
– Thêm ngay lập tức.andi
– Bitwise và có tính tức thì.ori
– Bitwise hoặc với immediate.xori
– Bitwise xor với giá trị tức thì.
Mã hoá của các giá trị này là:
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 | ngày | 001 0011 | ori |
imm12 | rs1 | 100 | ngày | 001 0011 | xori |
func3 | mã hoạt động |
Hướng dẫn R-Type (Loại I chuyên dụng) mà chúng tôi cần thêm là:
slli
– Di chuyển sang trái logic theo ngay lập tức.srai
– Di chuyển sang phải số học theo giá trị tức thì.srli
– Di chuyển sang phải logic theo ngay lập tức.
Mã hoá của các giá trị này là:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 | tên mã opcode |
---|---|---|---|---|---|---|
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 | mã opcode |
Các hướng dẫn Loại U mà chúng ta cần thêm là:
auipc
– Thêm giá trị tức thì trên vào pc.lui
– Tải ngay trên.
Mã hoá của các giá trị này là:
31..12 | 11..7 | 6..0 | tên mã hoạt động |
---|---|---|---|
uimm20 | rd | 001 0111 | auipc |
uimm20 | rd | 011 0111 | lui |
mã hoạt động |
Hãy tiếp tục thực hiện các thay đổi rồi tạo bản dựng. Kiểm tra kết quả đã tạo. Giống như trước đây, bạn có thể kiểm tra công việc của mình dựa trên riscv32i.bin_fmt.
Thêm hướng dẫn nhánh và nhảy-và-liên kết
Tập hợp các lệnh tiếp theo cần được xác định là lệnh nhánh có điều kiện, lệnh nhảy và liên kết và lệnh đăng ký nhảy và liên kết.
Các nhánh có điều kiện mà chúng ta thêm vào đều sử dụng phương thức mã hoá Loại B.
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm7 | rs2 | rs1 | func3 | imm5 | mã opcode |
Mặc dù bố cục của mã hoá Loại B giống hệt với bố cục của mã hoá Loại R, nhưng chúng tôi chọn sử dụng một loại định dạng mới để phù hợp với tài liệu RiscV.
Tuy nhiên, bạn cũng có thể chỉ cần thêm một lớp phủ để ngay lập tức nhận được độ dời nhánh thích hợp bằng cách sử dụng các trường func7
và rd
của quá trình mã hoá Loại R.
Bạn cần thêm định dạng BType
với các trường như đã chỉ định ở trên, nhưng chưa đủ. Như bạn có thể thấy, lệnh tức thì được chia thành hai trường hướng dẫn.
Hơn nữa, các lệnh nhánh không coi đây là một chuỗi nối đơn giản của hai trường. Thay vào đó, mỗi trường được phân vùng thêm và các phân vùng này được nối theo thứ tự khác. Cuối cùng, giá trị đó được dịch chuyển sang trái một bên để có được độ lệch căn chỉnh 16 bit.
Trình tự bit trong từ lệnh dùng để tạo lệnh tức thì là: 31, 7, 30..25, 11..8. Giá trị này tương ứng với các tham chiếu trường phụ sau đây, trong đó chỉ mục hoặc dải ô chỉ định các bit trong trường, được đánh số từ phải sang trái, tức là
imm7[6]
là msb của imm7
, và imm5[0]
là lsb của imm5
.
imm7[6], imm5[0], imm7[5..0], imm5[4..1]
Việc đưa thao tác thao tác bit này vào các lệnh nhánh có hai hạn chế lớn. Trước tiên, nó liên kết việc triển khai hàm ngữ nghĩa với các chi tiết trong bản trình bày lệnh nhị phân. Thứ hai, việc này làm tăng thêm mức hao tổn thời gian chạy. Câu trả lời là thêm một lớp phủ vào định dạng BType
, bao gồm cả dấu "0" để tính đến sự dịch chuyển sang trái.
overlays:
signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;
Lưu ý rằng lớp phủ đã được ký nên sẽ tự động được mở rộng ký khi được trích xuất từ từ hướng dẫn.
Lệnh nhảy và liên kết (ngay lập tức) sử dụng phương thức mã hoá Kiểu J:
31..12 | 11..7 | 6..0 |
---|---|---|
20 | 5 | 7 |
imm20 | ngày | mã opcode |
Đây cũng là một định dạng dễ thêm, nhưng xin nhắc lại, lệnh ngay lập tức mà lệnh này sử dụng không đơn giản như vẻ bề ngoài. Các trình tự bit dùng để tạo thành toán tử ngay lập tức đầy đủ là: 31, 19..12, 20, 30..21 và toán tử ngay lập tức cuối cùng được dịch sang trái một bit để căn chỉnh nửa từ. Giải pháp cho trường hợp này là thêm một lớp phủ khác (21 bit để tính đến sự dịch chuyển sang trái) vào định dạng:
overlays:
signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;
Như bạn có thể thấy, cú pháp cho lớp phủ hỗ trợ việc chỉ định nhiều dải ô trong một trường theo định dạng viết tắt. Ngoài ra, nếu không sử dụng tên trường, các số bit sẽ tham chiếu đến chính từ lệnh, vì vậy, bạn cũng có thể viết như sau:
signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;
Cuối cùng, lệnh nhảy và liên kết (đăng ký) sử dụng định dạng loại I như đã sử dụng trước đó.
Định dạng tức thì loại I:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | mã opcode |
Lần này, bạn không cần thay đổi định dạng.
Các lệnh nhánh mà chúng ta cần thêm là:
beq
– Nhánh nếu bằng.bge
– Nhánh nếu lớn hơn hoặc bằng.bgeu
– Nhánh nếu lớn hơn hoặc bằng số không dấu.blt
– Nhánh nếu nhỏ hơn.bltu
– Nhánh nếu nhỏ hơn số không dấu.bne
– Nhánh nếu không bằng.
Các mã này được mã hoá như sau:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 | tên mã 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 | mã opcode |
Lệnh jal
được mã hoá như sau:
31..12 | 11..7 | 6..0 | tên mã hoạt động |
---|---|---|---|
imm20 | rd | 110 1111 | jal |
mã opcode |
Lệnh jalr
được mã hoá như sau:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | rd | 110 0111 | jalr |
func3 | mã opcode |
Hãy tiếp tục và điều chỉnh rồi tạo bản dựng. Kiểm tra kết quả đã tạo. Giống như trước, bạn có thể kiểm tra công việc của mình dựa trên riscv32i.bin_fmt.
Thêm hướng dẫn về cửa hàng
Lệnh lưu trữ sử dụng phương thức mã hoá Loại S, giống với phương thức mã hoá Loại B mà lệnh nhánh sử dụng, ngoại trừ thành phần của lệnh tức thì. Chúng ta chọn thêm định dạng SType
để luôn phù hợp với tài liệu RiscV.
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm7 | rs2 | rs1 | func3 | imm5 | mã opcode |
Đối với định dạng SType
, dữ liệu ngay lập tức sẽ là sự nối thẳng giữa 2 trường tức thì, vì vậy, thông số kỹ thuật của lớp phủ chỉ đơn giản là:
overlays:
signed s_imm[12] = imm7, imm5;
Xin lưu ý rằng bạn không cần chỉ định phạm vi bit khi nối toàn bộ các trường.
Hướng dẫn lưu trữ được mã hoá như sau:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 | tên mã opcode |
---|---|---|---|---|---|---|
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 | mã opcode |
Hãy tiếp tục và điều chỉnh rồi tạo bản dựng. Kiểm tra kết quả đã tạo. Giống như trước, bạn có thể kiểm tra công việc của mình dựa trên riscv32i.bin_fmt.
Thêm hướng dẫn tải
Lệnh tải sử dụng định dạng I-Type. Bạn không cần thực hiện thay đổi nào ở đó.
Các bộ mã hoá là:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | ngày | 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 | ngày | 000 0011 | lw |
func3 | mã opcode |
Hãy tiếp tục thực hiện các thay đổi rồi tạo bản dựng. Kiểm tra dữ liệu đầu ra đã tạo. Cũng như trước đây, bạn có thể kiểm tra công việc của mình dựa trên riscv32i.bin_fmt.
Đây là phần kết thúc của hướng dẫn này. Chúng tôi hy vọng hướng dẫn này hữu ích với bạn.