1. Quy trình thực thi lệnh trong YEMU
Quy trình thực thi lệnh trong YEMU được chia thành 3 bước cơ bản: Lấy lệnh (Fetch) → Giải mã (Decode) → Thực thi (Execute).
- Lấy lệnh: YEMU sử dụng một mảng lệnh được định nghĩa sẵn (
M[NMEM]). Mỗi lần lấy lệnh, con trỏ lệnh sẽ tuần tự trỏ tới phần tử tiếp theo trong mảng này.
uint8_t M[NMEM] = {
0b11100110, // load 6# | R[0] <- M[y]
0b00000100, // mov r1, r0 | R[1] <- R[0]
0b11100101, // load 5# | R[0] <- M[x]
0b00010001, // add r0, r1 | R[0] <- R[0] + R[1]
0b11110111, // store 7# | M[z] <- R[0]
0b00010000, // x = 16
0b00100001, // y = 33
0b00000000, // z = 0
};
- Giải mã: Dựa vào mã lệnh đã lấy được, YEMU thực hiện giải mã. Vì YEMU chỉ hỗ trợ 2 loại lệnh (R-type và M-type) nên thao tác giải mã được thực hiện đơn giản qua cấu trúc
switch-case. Các cấu trúcDECODE_RvàDECODE_Mđược định nghĩa sẵn để lấy ra các toán hạng nhưrd,src1,src2.
#define DECODE_R(inst) uint8_t rt = (inst).rtype.rt, rs = (inst).rtype.rs
#define DECODE_M(inst) uint8_t addr = (inst).mtype.addr
- Thực thi: Sau khi giải mã, YEMU thực hiện lệnh tương ứng trong mỗi nhánh
case, sau đó tăngpclên 1 đơn vị để chuẩn bị cho lần lấy lệnh tiếp theo. Quá trình này lặp lại cho đến khi gặp lệnh kết thúc.
case 0b0000: { DECODE_R(this); R[rt] = R[rs]; break; }
case 0b0001: { DECODE_R(this); R[rt] += R[rs]; break; }
case 0b1110: { DECODE_M(this); R[0] = M[addr]; break; }
case 0b1111: { DECODE_M(this); M[addr] = R[0]; break; }
default: // halt
2. Quy trình thực thi lệnh trong NEMU
Nhìn chung, NEMU cũng tuân theo quy trình 3 bước: Lấy lệnh - Giải mã - Thực thi. Tuy nhiên, mỗi bước đều phức tạp hơn YEMU rất nhiều.
- Lấy lệnh: NEMU kiểm tra sự tồn tại của file nhị phân (
.bin). Nếu không có, nó sử dụng mảng lệnh định nghĩa sẵn và đọc lệnh thông qua hàmpmem_read(). Bản chất đây là thao tác đọc từ bộ nhớ. - Giải mã: Quá trình này phức tạp hơn YEMU do kiến trúc RISC-V 32-bit hỗ trợ nhiều kiểu lệnh: I, U, S, J, R, B. Mỗi kiểu lệnh có cấu trúc giải mã riêng, được định nghĩa bằng các macro.
#define src1R() do { *src1 = R(rs1); } while (0)
#define src2R() do { *src2 = R(rs2); } while (0)
#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)
#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)
#define immJ() do { *imm = SEXT((BITS(i,31,31)<<19 |BITS(i,30,21)|BITS(i,20,20)<<10|\
(BITS(i,19,12)<<11))<<1, 21); } while(0)
#define immB() do {*imm = SEXT((BITS(i,31,31)<<11|BITS(i,30,25)<<4|BITS(i,11,8)|BITS(i,8,7)<<10)<<1,13);} while(0)
case TYPE_I: src1R(); immI(); break;
case TYPE_U: immU(); break;
case TYPE_S: src1R(); src2R(); immS(); break;
case TYPE_J: immJ(); break;
case TYPE_R: src1R(); src2R(); break;
case TYPE_B: src1R(); src2R(); immB(); break;
- Thực thi: NEMU sử dụng các macro
INSTPATđể ánh xạ mã lệnh tới hành vi thực thi. Về bản chất, đây cũng là một cấu trúcswitch-casenhưng linh hoạt hơn.
INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi , I, R(rd) = src1 + imm);
INSTPAT("??????? ????? ????? ??? ????? 01101 11", lui , U, R(rd) = imm );
INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);
Sau khi thực thi, pc được cập nhật thành next_pc. Nếu chưa kết thúc, quá trình lặp lại từ bước lấy lệnh.
3. Cách thức hoạt động của trò chơi gõ chữ mini
Trò chơi hoạt động dựa trên vòng lặp chính: Làm mới màn hình → Nhận đầu vào → Cập nhật trạng thái trò chơi.
- Khởi tạo: Hàm
main()khởi tạo giao diện IOE (Input/Output Engine) cho 26 phím chữ cái, sau đó khởi tạo màn hình VGA với màu nền tím đồng nhất. - Vòng lặp chính: Trong vòng lặp
while(1), trò chơi thực hiện 3 bước:- Hiển thị chữ: Các chữ cái mới xuất hiện trên màn hình VGA, đồng thời vị trí của các chữ đang rơi được cập nhật.
- Xử lý chữ chạm đáy: Các chữ rơi chạm đáy màn hình sẽ bị loại bỏ và tính là "miss".
- Nhận đầu vào: Sử dụng giao diện
KEYBRDcó sẵn để kiểm tra xem phím vừa nhấn có trùng với chữ cái đang rơi không (check_hit()).
- Cập nhật VGA: Cuối mỗi vòng lặp, trò chơi đọc buffer hiện tại, ghi trạng thái mới vào buffer để chuẩn bị cho lần hiển thị tiếp theo.
4. Phân tích lỗi khi bỏ static và inline khỏi hàm inst_fetch()
Hàm inst_fetch() được định nghĩa với cả hai từ khóa static và inline. Khi thử nghiệm bỏ một trong hai hoặc cả hai, kết quả biên dịch như sau:
- Bỏ
static: Không có lỗi. Lý do là trình biên dịch có thể tạo ra các ký hiệu yếu (weak symbols). Với inline function, nếu có nhiều định nghĩa, chỉ một trong số chúng được giữ lại khi liên kết. - Bỏ
inline: Không có lỗi. Vìstaticvẫn còn, hàm có internal linkage (chỉ hiển thị trong file nguồn hiện tại). Do đó, không xảy ra xung đột khi liên kết nhiều file object. - Bỏ cả
staticvàinline: Gây ra lỗi liên kết (linker error):multiple definition of 'inst_fetch'. Lỗi này xuất hiện vì hàm mất đi internal linkage và cũng không được tạo thành weak symbol. Kết quả là nhiều file object cùng định nghĩa một hàm có tên giống nhau, dẫn đến xung đột.