Bạn đang muốn bắt tay vào thiết kế một lõi IP (IP core) nhưng không biết bắt đầu từ đâu? Bài viết này là một phần trong chuỗi bài viết hướng dẫn bạn đi từng bước để có thể thiết kế được một lõi IP hoàn chỉnh. Phương pháp được sử dụng ở đây là top-down, phân tích từ tổng quan đến chi tiết. Ví dụ được trình bày là một thiết kế CPU cơ bản (simple CPU).
1. Tổng quan
Sau bài 4, chúng ta đã có thể viết được hoàn chỉnh RTL code của một thiết kế. Trước khi thiết kế được mô phỏng kiểm tra một cách toàn diện bởi chính người thiết kế hoặc người khác, người thiết kế sẽ thực hiện một số mô phỏng cơ bản để kiểm tra các chức năng cơ bản của thiết kế.
Mô phỏng cơ bản hay còn gọi là mô phỏng kiểm tra mức block được thực hiện ở mức RTL code. Đây không phải là một mô phỏng hoàn chỉnh để kiểm tra toàn bộ các chức năng (function) của RTL code mà là để tìm và sửa những lỗi cơ bản trong hoạt động thông thường của một thiết kế.
Môi trường kiểm tra ở bước này có thể đơn giản hoặc phức tạp tùy vào thời gian và mục tiêu kiểm tra của người thiết kế. Các mẫu test (testbench hoặc testcase) không cần phải phức tạp và cũng không cần bao phủ (cover) toàn bộ thiết kế.
Với quan điểm của tôi, trong bước này, việc kiểm tra bằng waveform là rất cần thiết để xác thực các điểm cần test về giá trị, độ rộng tín hiệu, vị trí xuất hiện,... trước khi sử dụng các phương pháp thay thế khác.
2. Mô tả một cách kiểm tra cơ bản cho ví dụ CPU 8 bit SCPU
Đối với ví dụ CPU 8 bit SCPU, mục tiêu kiểm tra chính sẽ được nhắm đến là tất cả các lệnh hoạt động đúng theo từng chu kỳ đã phân tích. Chúng ta cần một chương trình chứa tất cả các lệnh để có thể kiểm tra mục tiêu này.
Một môi trường kiểm tra cơ bản có thể như sau:
- Một chương trình (mã binary) nạp trong bộ nhớ (MEMORY) RAM của khối FETCH để cho CPU chạy.
- Một nguồn cung cấp clock và reset để CPU chạy
- Quan sát việc thực thi từng lệnh theo từng chu kỳ trên Waveform. Trong đó, tập trùng vào việc kiểm tra giá trị các thanh ghi (IR, DR, R0, R1, R2, R3) và bộ nhớ theo từng chu kỳ hoạt động của một lệnh.
Cấu trúc môi trường đơn giản như sau:
- DUT: toàn bộ thiết kế SCPU
- Testbench gồm:
- Binary code của chương trình sẽ nạp trong MEMORY ở khối FETCH để CPU chạy
- Bộ tạo clock và reset
- Gọi DUT
- Monitor là waveform viewer bất kỳ để xem kết quả và kiểm tra từng lệnh
Hình 1. Cấu trúc môi trường mô phỏng cơ bản |
3. Tạo một file binary code
Thực tế, người lập trình CPU sẽ dùng ngôn ngữ C/C++ hoặc hợp ngữ Assembly để viết chương trình cho một vi điều khiển MCU hoặc vi xử lý CPU. Sau đó, code này sẽ được biên dịch thành binary code để CPU thực thi.
Binary code được nạp vào bộ nhớ chương trình (program memory) nằm bên trong chip hoặc bên ngoài chip. Đây là bộ nhớ không bốc hơi (không mất dữ liệu khi bị ngắt nguồn) như ROM hay Flash. Sau khi cấp nguồn và reset, CPU bắt đầu lấy từng lệnh từ bộ nhớ chương trình để thực thi. Lệnh từ bộ nhớ chương trình có thể được thực thi trực tiếp (đọc ra và chạy) hoặc được load đến một bộ nhớ tạm (RAM) có tốc độ truy xuất nhanh hơn bộ nhớ chương trình để chờ thực thi.
Hình 2. Quá trình lập trình CPU thực tế |
Đối với ví dụ SCPU, chúng ta không có trình biên dịch để thực hiện việc chuyển đổi từ code C/C++ hay Assembly thành binary. Vì vậy, sau khi viết một chương trình với các lệnh Assemply, chúng ta sẽ tự tạo file binary dựa trên mã code và hoạt động của từng lệnh.
Việc "nạp code" trên thực tế sẽ được thay bằng việc gán trực tiếp các giá trị binary vào từ ô nhớ của bộ nhớ RAM ở khối FETCH.
Nội dung file binary dùng để kiểm tra như sau (phía sau mỗi mã code có chú thích mã Assembly tương ứng):
cpu.fetch.mem_array[0] = 8'b1110_0000; //LI R0, haa;
cpu.fetch.mem_array[1] = 8'b1010_1010; //haa
cpu.fetch.mem_array[2] = 8'b1110_0100; //LI R1, h55;
cpu.fetch.mem_array[3] = 8'b0101_0101; //h55
cpu.fetch.mem_array[4] = 8'b1110_1000; //LI R2, h88;
cpu.fetch.mem_array[5] = 8'b1000_1000; //h88
cpu.fetch.mem_array[6] = 8'b1110_1100; //LI R3, h99;
cpu.fetch.mem_array[7] = 8'b1001_1001; //h99
cpu.fetch.mem_array[8] = 8'b0111_0000; //NOP
cpu.fetch.mem_array[9] = 8'b0111_0000; //NOP
cpu.fetch.mem_array[10] = 8'b0111_0000; //NOP
cpu.fetch.mem_array[11] = 8'b0000_0001; //AND R0, R1 => R0 = h00
cpu.fetch.mem_array[12] = 8'b0001_1011; //OR R2, R3 => R2 = h99
cpu.fetch.mem_array[13] = 8'b0010_0110; //ADD R1, R2 => R1 = hee
cpu.fetch.mem_array[14] = 8'b0011_1011; //SUB R2, R3 => R2 = h99 - h99 = h00
cpu.fetch.mem_array[15] = 8'b0100_0000; //LWR R0, R0 => R0 = mem[R0] = mem[h00] = he0
cpu.fetch.mem_array[16] = 8'b0101_0111; //SW R1, R3 => mem[R3] = mem[h99] = mem[153] = R1 = hee
cpu.fetch.mem_array[17] = 8'b0110_1100; //MOV R3, R0 => R3 = R0 = he0
cpu.fetch.mem_array[18] = 8'b0111_0000; //NOP
cpu.fetch.mem_array[19] = 8'b1000_0100; //JEQ R1, IMM
cpu.fetch.mem_array[20] = 8'b1111_0000; //IMM = hf0 => Do not jump because R1 = hee
cpu.fetch.mem_array[21] = 8'b1110_0100; //LI R1, IMM => R1 = h00
cpu.fetch.mem_array[22] = 8'b0000_0000; //IMM = h00
cpu.fetch.mem_array[23] = 8'b1000_0100; //JEQ R1, IMM
cpu.fetch.mem_array[24] = 8'b0001_1010; //IMM = h1a = 26 => jump to mem[26]
cpu.fetch.mem_array[25] = 8'b0110_0010; //MOV R0, R2 (do not execute because JEQ)
cpu.fetch.mem_array[26] = 8'b1001_1000; //JNE R2, IMM => Do not jump because R2 = 0
cpu.fetch.mem_array[27] = 8'b0001_1111; //IMM = h1f = 31
cpu.fetch.mem_array[28] = 8'b0000_0010; //AND R0, R1 => R0 = he0 & h00 = h00
cpu.fetch.mem_array[29] = 8'b1001_1100; //JNE R3, IMM => jump because R3 != 0
cpu.fetch.mem_array[30] = 8'b0010_0000; //IMM = h1f = 32 => jump to mem[32]
cpu.fetch.mem_array[31] = 8'b0110_0010; //MOV R0, R2 (do not execute because JEQ)
cpu.fetch.mem_array[32] = 8'b1010_0010; //JGT R0, IMM => Do not jump because R0 = 0
cpu.fetch.mem_array[33] = 8'b0010_1101; //IMM = 45
cpu.fetch.mem_array[34] = 8'b1110_0000; //LI R0, h88;
cpu.fetch.mem_array[35] = 8'b1000_1000; //R0 = h88
cpu.fetch.mem_array[36] = 8'b1010_0000; //JGT R0, IMM => jump because R0 = h88 > 0
cpu.fetch.mem_array[37] = 8'b0010_1101; //IMM = 45
//From [38] to [43], they are executed when jumping from [43] by JLT
cpu.fetch.mem_array[38] = 8'b1100_1000; //LWI R2, IMM =>R2 = mem[40] = hd8
cpu.fetch.mem_array[39] = 8'b0010_1000; //IMM = 40 = h28
cpu.fetch.mem_array[40] = 8'b1101_1000; //SWI R2, IMM => mem[IMM] = mem[h3c] = mem[60] = R2 = hd8
cpu.fetch.mem_array[41] = 8'b0011_1100; //IMM = h3c
cpu.fetch.mem_array[42] = 8'b1111_0000; //JMP IMM => Jump to the start of the program [0]
cpu.fetch.mem_array[43] = 8'b0000_0000; // IMM = 0
//
//
cpu.fetch.mem_array[45] = 8'b1011_0000; //JLT R0, IMM => Do not jump because R0 = h88 > 0
cpu.fetch.mem_array[46] = 8'b1111_0000; //IMM = hf0
cpu.fetch.mem_array[47] = 8'b0011_0100; //SUB R1, R0 => R1 = R1 - R0 = h00 - h88 = -h88 (R1[8] == 1)
cpu.fetch.mem_array[48] = 8'b0110_0001; //MOV R0, R1 => R0 = R1 = -h88
cpu.fetch.mem_array[49] = 8'b1011_0000; //JLT R0, IMM => Do not jump because R0 = -h88 < 0
cpu.fetch.mem_array[50] = 8'b0010_0110; //IMM = 38 => jump to [38]
4. Tạo một file testbench
Một testbench đơn giản cho ví dụ này gồm các phần sau:
- Gọi DUT: gọi module scpu_top
// Inputs
.clk (clk),
.rst_n (rst_n));
- Gán binary code cho bộ nhớ RAM
`include "scpu_init_mem.h"
end
- Tạo reset và giới hạn thời gian chạy mô phỏng
clk = 1'b0;
rst_n = 1'b0;
#51
rst_n = 1'b1;
#END_SIM_TIME
$stop;
end
- Tạo clock: ví dụ này tạo clock có chu kỳ 20 đơn vị thời gian mặc định của trình mô phỏng (thường là ps)
- Tạo một biến chứa tên lệnh tương ứng với từng opcode được giải mã để quan sát trên waveform dễ hơn
reg [31:0] inst_name;
always @ (*) begin
case (cpu.dec.opcode[3:0])
OP_AND: inst_name = "AND";
OP_OR : inst_name = "OR";
OP_ADD: inst_name = "ADD";
OP_SUB: inst_name = "SUB";
OP_LWR: inst_name = "LWR";
OP_SWR: inst_name = "SWR";
OP_MOV: inst_name = "MOV";
OP_NOP: inst_name = "NOP";
OP_JEQ: inst_name = "JEQ";
OP_JNE: inst_name = "JNE";
OP_JGT: inst_name = "JGT";
OP_JLT: inst_name = "JLT";
OP_LWI: inst_name = "LWI";
OP_SWI: inst_name = "SWI";
OP_LI : inst_name = "LI";
OP_JMP: inst_name = "JMP";
endcase
end
5. Mô phỏng và sửa lỗi
Một số điểm chỉnh sửa quan trọng so với RTL code phiên bản 1 trong bài 4 sẽ được liệt kê sau đây để các bạn có thể so sánh và tìm hiểu.
5.1 Chỉnh sửa khối FETCH
Điểm 1: Sửa giá trị reset của thanh ghi fetch_ir[7:0] từ b00000000 thành b01110000 (NOP).
Điểm 2: Sửa tín hiệu mem_out[7:0] để lấy đúng giá trị trong trường hợp lấy giá trị IMM từ địa chỉ chứa trong DR (mem_out và fetch_mem_dout không còn chung 1 mạch
Điểm 3: Sửa mạch tạo pc cho trường hợp thực thi các lệnh nhảy
Với các điều chỉnh trên, bảng các chu kỳ thực thi lệnh được điều chỉnh lại như sau cho phù hợp:
Hình 3. Bảng các chu kỳ thực thi lệnh |
5.2 Chỉnh sửa khối DECODER
Điểm 1: Sửa độ rộng ctrl_counter từ 3 bit xuống 2 bit vì RTL code viết sai so với phân tích chi tiết.
Điểm 2: Sửa tín hiệu dc_load_ir tích cực cùng với tín hiệu clr_counter
Điểm 3: Sửa tín hiệu dc_load_pc tích cực khi dc_load_ir hoặc dc_load_dr tích cực
5.3 Chỉnh sửa khối chung cho khối DECODER và EXECUTE
Điểm chỉnh sửa này liên quan đến lệnh nhảy có điều kiện JLT. Lệnh nhảy sẽ thực hiện rẽ nhánh (PC = DR + 1) nếu giá trị thanh ghi Rd < 0. Trong phân tích thiết kế và RTL code phiên bản 1 không có bit báo hiệu giá trị âm cho các thanh ghi khi tính toán nên lệnh nhảy này không thể thực hiện được.
Để thực hiện bit báo giá trị âm của một thanh ghi thì mỗi thanh ghi R0, R1, R2 và R3 sẽ được tăng thêm 1 bit, từ 7 bit thành 8 bit để chứa bit dấu, gòn là cờ Negative. Khối EXECUTE cũng sẽ thực hiện các phép toán kèm theo bit dấu này.
Khi bit thứ 8 của một thanh ghi, cờ Negative, bằng 1 tức là giá trị của thanh ghi đó là giá trị âm.
Mạch tín hiệu jump_en khối DECODER được sử lại như sau:
Các chỉnh sửa khác về việc tăng độ rộng các thanh ghi R0, R1, R2 và R3 các bạn xem thêm trong RTL code của khối DECODER và EXECUTE.
6. Xem kết quả mô phỏng trên waveform
Phần mềm sử dụng là Questa Sim-64 10.2c. Dưới đây là một waveform minh họa việc quan sát thực thi một số lệnh. Ví dụ, sau các lệnh LI, giá trị IMM được ghi vào các thanh ghi R0, R1, R2 và R3 (xem và so sánh với nội dung file binary code).
Hình 4. Waveform kiểm tra thực thi các lệnh |
Link download: RTL code CPU 8 bit SCPU
Pass (nếu có): nguyenquanicd
0 bình luận:
Đăng nhận xét