• Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Vi mạch và Ứng dụng

  • Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Vi mạch và Ứng dụng

  • Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Vi mạch và Ứng dụng

Hiển thị các bài đăng có nhãn cpu. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn cpu. Hiển thị tất cả bài đăng

Thứ Bảy, 30 tháng 9, 2017

[Basic Knowledge][Bài 5] Hướng dẫn phân tích thiết kế lõi IP step-by-step - Mô phỏng cơ bản mức block

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
scpu_top cpu (/*AUTOINST*/
// Inputs
.clk (clk),
.rst_n (rst_n));

  • Gán binary code cho bộ nhớ RAM
initial begin
`include "scpu_init_mem.h"
end

  • Tạo reset và giới hạn thời gian chạy mô phỏng
initial begin
  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)
always #10 clk = ~clk;
  • 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
7. Source code ví dụ CPU 8 bit sau khi kiểm tra cơ bản
Link download: RTL code CPU 8 bit SCPU
Pass (nếu có): nguyenquanicd

Chủ Nhật, 24 tháng 9, 2017

[Basic Knowledge][Bài 4] Hướng dẫn phân tích thiết kế lõi IP step-by-step - Mô tả và kiểm tra RTL code

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).


Trong bài 1, bài 2 và bài 3 chúng ta đã hiểu đến bước phân tích chi tiết từ khối trong một thiết kế vi mạch. Bài này chúng ta sẽ thực hiện mô tả RTL code cho toàn bộ một lõi IP trên ví dụ CPU 8 bit SCPU đã phân tích trong những bài trước.
1. Hướng dẫn mô tả RTL code sử dụng Verilog HDL
1.1 Xác định cấu trúc thứ bậc (hierachy)
Để mô tả RTL code cho một lõi IP hay một thiết kế nói chung, một số bước chính cần được thực hiện như sau:
  1. Xác định cấu trúc thứ bậc của các file
  2. Mô tả RTL code cho từng file, những file có cấp thấp nhất trong cấu trúc sẽ được mô tả trước, các file có cấp cao hơn sẽ được mô tả sau. Thứ tự mô tả RTL code là bottom-to-top. Vì các cấp cao hơn thường sẽ gọi (instance) các file cấp thấp hơn và file cấp cao nhất (top) thường chỉ mô tả các kết nối của các file cấp thấp hơn.
Hình 1. Xác định cấu trúc thứ bậc của các file RTL code sẽ mô tả
Trong ví dụ về CPU 8 bit, chúng ta có 3 khối FETCH, DECODER và EXECUTE. Tương ứng, chúng ta sẽ có 3 file RTL code để mô tả 3 khối này là scpu_fetch.v, scpu_decoder.v và scpu_execute.v. Bên cạch đó là một file để kết nối 3 khối trên là scpu_top.v. Ngoài ra, nếu trong thiết kế có các tham số và định nghĩa được sử dụng trong nhiều file khác nhau thì có thể có thêm các file header để chứa chúng, ví dụ như scpu_define.h, scpu_parameter.h.
Căn cứ theo cấu trúc thứ bậc trên thì các file scpu_fetch.v, scpu_decoder.v và scpu_execute.v sẽ được mô tả trước, file scpu_top sẽ được mô tả sau. Các file scpu_define.h, scpu_parameter.h có thể được mô tả song song với các file trước đó hoặc mô tả sau cùng để tập hợp các tham số và định nghĩa dùng chung.
Ở đây có một số quy định chung để tiện theo dõi như sau:
  1. Mỗi khối sẽ được mô tả bằng một cặp từ khóa module/endmodule nên một khối tương ứng với một module
  2. Mỗi module sẽ được mô tả trong một file riêng tuy nhiên các bạn hoàn toàn có thể mô tả chúng trong cùng một file ".v"
  3. Tên file và tên module sẽ trùng nhau để dễ quản lý. Ví dụ, file scpu_fetch.v sẽ chứa module có tên scpu_fetch.
  4. Tên file và tên module ở đây được đặt theo nguyên tắc <tên module top>_<tên module con>. Ví dụ, scpu_fetch thì scpu là ký hiệu chung chỉ module top, fetch là tên đại diện cho khối chức năng (module con)
  5. File header sẽ có đuôi ".h" chứa các tham số hoặc định nghĩa dùng chung 
1.2 Mô tả RTL code cho từng file
Như đã trình bày, một module ở đây được hiểu là một khối. Một module được mô tả trong một cặp từ khóa module/endmodule. Trước khi đi vào mô tả chi tiết từng file RTL code, chúng ta sẽ xem một file RTL code thường có những thành phần nào.
Một module thường chứa các thành phần cơ bản như sau:
1. Ghi chú ban đầu (file header)
2. Khai báo tiền tổng hợp (pre-synthesis define)
3. Khai báo module
4. Khai báo hằng số (parameter)
5. Khai báo tín hiệu giao tiếp (in/out interface)
6. Khai báo biến và tín hiệu nội (internal signal)
7. Mô tả thân module (module body) 
8. Khai báo endmodule

Trong các thành phần trên thì 3, 5, 7 và 8 là các thành phần bắt buộc phải có. Các thành phần khác có thể có hoặc không tùy vào trường hợp cụ thể. Để minh họa, sau đây, file scpu_fetch.v sẽ được mô tả.

Ghi chú ban đầu là các comment mô tả các thông tin liên quan đến file RTL code như tên công ty, tên dự án, chức năng của file RTL code, tên tác giả, ngày tạo file, các điểm chỉnh sửa trong file so với các phiên bản trước, thông tin quy định về việc phân phối và sử dụng file RTL code và các thông tin liên quan khác. Nội dung phần này sẽ khác nhau tùy quy định từng công ty. Nội dung phần ghi chú ban đầu được đặt sau dấu “//” hoặc trong cặp dấu “/*” và “*/ .
Hình 2. Ví dụ về ghi chú đầu file RTL code của khối FETCH
Khai báo tiền tổng hợp là các chỉ dẫn như `define, `timescale hoặc `include các tập tin chứa các khai báo `define và `timescale được sử dụng trong tập tin RTL code. Ví dụ trong thiết kế SCPU, định nghĩa sau được sử dụng để tạo độ trễ khi mô tả mạch tuần tự:
`define DLY #1
Định nghĩa trên dùng để tạo độ trễ 1 đơn vị thời gian trước khi một tín hiệu cập nhật giá trị theo cạnh lên xung clock khi chạy mô phỏng RTL code.
Hình 3. Tạo độ trễ sau cạnh lên xung clock cho các tín hiệu hoạt động theo xung clock
Trong một số trường hợp mô phỏng RTL code, trình mô phỏng cho kết quả không chính xác khi ngõ vào và cạnh lênh xung clock cùng thay đổi tại một thời điểm. Vì vậy, mỗi tín hiệu hoạt động theo xung clock được delay để khi giá trị của nó truyền đến các mạch khác, cũng sử dụng xung clock, thì giá trị này không thay đổi cùng lúc với cạch lên xung clock.
Hình 4. Trình mô phỏng cho kết quả sai ở d_out khi d_in đổi giá trị cùng lúc với cạnh lên clk
Khai báo module là sẽ đi kèm với khai báo endmodule để định nghĩa một khối. ĐI cùng với khai báo module là tên module và danh sách các tín hiệu giao tiếp (input/output/inout) của một module. 
Khai báo hằng số sử dụng từ khóa parameter, localparam hoặc gọi các tập tin chứa khai báo hằng số sử dụng trong RTL code. Khai báo hằng số nằm bên trong khai báo module/endmodule.
Hình 4. Khai báo tiền tổng hợp, khai báo module và khai báo hằng số cho khối FETCH
Khai báo tín hiệu giao tiếp là khai báo các tín hiệu sẽ kết nối đến các module khác bằng các từ khóa input, output hoặc inout. Các tín hiệu giao tiếp này có được từ sơ đồ tín hiệu giao tiếp khi phân tích lõi IP. Một tín hiệu giao tiếp cần có các thông tin quan trọng sau:
  • Chiều tín hiệu (input/output/inout)
  • Loại tín hiệu (reg, wire, ...)
  • Độ rộng tín hiệu

Hình 5. Sơ đồ tín hiệu giao tiếp của các khối

Hình 6. Khai báo tín hiệu giao tiếp trong module scpu_fetch của khối FETCH
Khai báo tín hiệu giao tiếp nội là khai báo các biến nội chỉ được sử dụng trong module và không kết nối đến bất kỳ module nào khác. Phần thân module có thể được mô tả trước khi khai báo phần này vì khi mô tả các mạch nguyên lý cho phần thân module, các biến nội có thể sẽ phát sinh thêm để giúp việc mô tả RTL code dễ dàng hơn. Lưu ý, việc mô tả phần thân module trước không có nghĩ là đoạn code của thân module nằm trên đoạn code khai báo tín hiệu giao tiếp nội trong một module mà vẫn theo nguyên tắc chung là "một biến phải khai báo trước khi sử dụng". Tín hiệu giao tiếp nội cần 2 thông tin quan trọng là loại tín hiệu và độ rộng tín hiệu.
Hình 7. Khai báo tín hiệu giao tiếp nội của khối FETCH
Mô tả thân module là căn cứ trên các mạch nguyên lý và các phân tích chi tiết cấu trúc từng khối để viết code.
Hình 8. Mô tả RTL code cho thanh ghi IR, DR và một phần bộ nhớ memory
Hình 9. Sự tương ứng giữa RTL code và mạch nguyên lý của thanh ghi IR
Hình 10. Sự tương ứng giữa RTL code và mạch nguyên lý của MEMORY
Khai báo endmodule là từ khóa kết thúc việc mô tả RTL code cho một khối.
Hình 11. Khai báo endmodule của khối FETCH
Đối với module có gọi một module khác thì "việc gọi" được viết trong phần thân module và cách gọi đầy đủ như sau:
Hình 12. Gọi và kết nối một module trong một module khác
2. Kiểm tra cú pháp và luật thiết kế (design rules) RTL code
Bước cuối cùng trong công đoạn phân tích thiết kế là kiểm tra các tập tin RTL code trên các phần mềm chuyên dụng. Hai điểm chính mà RTL code phải được kiểm tra là cú pháp và luật thiết kế. Trong đó:
  • Cú pháp là những quy định của ngôn ngữ mô tả phần cứng mà RTL code phải tuân thủ.
  • Luật thiết kế là những quy định khác nhằm hạn chế các nguyên nhân gây ra hoạt động không mong muốn hoặc giúp bạn cải thiện code style (cách viết RTL code) tốt hơn.  

Hình 13. Kiểm tra RTL code với phần mềm LEDA của Synopsys
Thường sẽ có 2 cấp độ chính là:

  • Những cảnh báo phải sửa: nếu không sửa sẽ không thể tổng hợp hay mô phỏng được
  • Những cảnh báo không cần sửa: những lỗi này là không bắt buộc phải sửa nhưng người thiết kế phải kiểm tra từng cảnh báo của phần mềm để đảm bảo những "cảnh báo" đó không phải là một "lỗi"
3. RTL code (Phiên bản chưa mô phỏng cơ bản)
Link download RTL code của SCPU: CPU 8 bit SCPU
pass (nếu có): nguyenquanicd
Các file:
  • scpu_top.v
  • scpu_fetch.v
  • scpu_decoder.v
  • scpu_execute.v
  • scpu_define.h
  • scpu_parameter.h

Hình 14. Kết quả tổng hợp trên FPGA bằng Quartus
Đến đây, chúng ta đã có phiên bản RTL code ban đầu của một lõi IP. Bước cuối cùng là mô phỏng và sửa lỗi cơ bản sẽ được trình bày ở bài tiếp theo.

Thứ Hai, 18 tháng 9, 2017

[Basic Knowledge][Bài 3] Hướng dẫn phân tích thiết kế lõi IP step-by-step - Phân tích mạch nguyên lý

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).

Trong bài 1 và bài 2, chúng ta đã hiểu đến bước phân tích chi tiết từ khối trong một thiết kế vi mạch. Bài này sẽ thực hiện lý thuyết đã trình bày trong bài 2 trên ví dụ CPU 8 bit đơn giản tên SCPU.
1. Tổng quan
Ở hai bài trước, chúng ta đã thực hiện qua các bước sau:
  1. Nghiên cứu và tìm hiểu
  2. Phân tích tổng quan
  3. Phân tích chi tiết
    1. Tìm các tín hiệu giao tiếp giữa các khối chức năng
Trong bài này, chúng ta tiếp tục thực hiện bước 2 của "phân tích chi tiết" là "phân tích cấu trúc" cho từng khối chức năng. Việc phân tích cấu trúc thực chất là tìm sự phụ thuộc của ngõ ra (output) theo các ngõ vào (input). "Sự phụ thuộc" này có thể được thể hiện bằng 1 trong các cách như đã trình bày ở bài 2 nhưng trong bài này chỉ sử dụng mạch nguyên lý mức cổng logic để thể hiện.
Để phân tích một ngõ ra, một số vấn đề cần được quan tâm:
  1. Tín hiệu ngõ là từ mạch tổ hợp hay tuần tự?
    1. Nếu là mạch tuần tự thì có cần reset không?
      1. Nếu có reset thì loại reset là đồng bộ hay bất đồng bộ?
      2. Giá trị reset là bao nhiêu?
    2. Nếu là mạch tổ hợp thì chỉ cần chuyển xuống câu hỏi 2 và 3
  2. Tín hiệu ngõ ra có thể mang những giá trị nào?
  3. Tín hiệu nào quyết định giá trị của ngõ ra?
Xét lại ví dụ về CPU 8 bit SCPU, hiện tại chúng ta có được các thông tin quan trọng sau đây.
Hình 1. Hoạt động của các lệnh SCPU
Hình 2. Sơ đồ khối SCPU

Hình 3. Sơ đồ giao tiếp của các khối
Trước khi vào phân tích cấu trúc từng khối, các bạn nên xem lại phần tìm tín hiệu giao tiếp giữa các khối của bài 2 để hiểu chức năng của từng tín hiệu.
2. Phân tích chi tiết khối FETCH

Như đã trình bày, việc phất tích chi tiết cấu trúc một khối chức năng là tìm mạch nguyên lý cho từng ngõ ra theo các ngõ vào.
Khối FETCH có 2 tín hiệu ngõ ra, fetch_ir[7:0] và fetch_dr[7:0], nên chỉ cần tìm mạch nguyên lý cho 2 tin hiệu này là kết thúc bước phân tích cấu trúc của khối FETCH.

2.1 Phân tích tín hiệu fetch_ir[7:0]
Tín hiệu này cung cấp giá trị mã lệnh cho khối DECODER. Nó được lấy từ một thanh ghi và là mạch tuần tự. Giá trị của tín hiệu giải mã các tín hiệu điều khiển nên cần reset. Giá trị reset của thanh ghi sẽ là mã lệnh NOP, b0111_0000. Trong thiết kế này, tín hiệu reset rst_n sẽ tích cực mức thấp và là reset đồng bộ. Tín hiệu chỉ lấy giá trị từ ngõ ra MEMORY. Tín hiệu dc_load_ir sẽ quyết định khi nào nó được cập nhật giá trị mới.
Hình 4. Mạch nguyên lý tín hiệu fetch_ir[7:0]
Ở đây, dc_load_ir là ngõ vào khối FETCH nên không cần phân tích thêm. Tín hiệu mem_dout[7:0] là ngõ ra của bộ nhớ MEMORY. Do bộ nhớ này không có sẵn nên phần này cũng phân tích luôn mô hình bộ nhớ này để sử dụng. Hoạt động của bộ nhớ các bạn xem lại bài 1. Mô hình cấu trúc chi tiết của bộ nhớ như sau:
Hình 5. Cấu trúc chi tiết của khối bộ nhớ MEMORY
Khối MEMORY đơn giản gồm 3 phần:
  • write_decoder: giải mã tín hiệu ghi vào mảng ô nhớ
  • mem_array: mảng ô nhớ gồm 256 ô, mỗi ô 8 bit
  • read_encoder: lựa chọn giá trị đọc từ mảng ô nhớ bằng địa chỉ truy xuất
Trong đó:
  • Tín hiệu cho phép ghi mem_wr là dc_mem_wr, ngõ vào khối FETCH nên không cần phân tích thêm
  • Bus dữ liệu ghi vào MEMORYlà dc_rd[7:0] vì MEMORY chỉ được ghi bằng giá trị Rd trong lệnh SWR và SWI, ngõ vào khối FETCH nên không cần phân tích thêm
Địa chỉ truy xuất MEMORY mem_addr[7:0] có thể mang các giá trị sau:
  • Giá trị PC để truy xuất mã lệnh của chương trình
  • Giá trị thanh ghi DR đối với lệnh đọc bộ nhớ LWI và ghi bộ nhớ SWI sử dụng địa chỉ là IMM
  • Giá trị thanh ghi Rs đối với lệnh đọc bộ nhớ LWR và ghi bộ nhớ SWR sử dụng địa chỉ là giá trị thanh ghi Rs
Tín hiệu để lựa chọn giá trị địa chỉ của MEMORY là dc_addr_sel[1:0].
Hình 6. Mạch nguyên lý tạo địa chỉ truy xuất MEMORY
Trong mạch tạo tín hiệu mem_addr[7:0], pc[7:0] và fetch_dr[7:0] không phải là ngõ vào khối FETCH.
pc[7:0] là một thanh ghi có giá trị reset ban đầu là 0 để chỉ đến ô nhớ bắt đầu chương trình. pc[7:0] được cập nhật giá trị mới khi dc_load_pc tích cực và giá trị cập nhật là giá trị IMM chứa trong thanh ghi DR hoặc giá trị sau khi tăng 1 đơn vị.
Hình 7. Mạch nguyên lý thanh ghi pc[7:0]
fetch_dr[7:0] là thành ghi DR dùng để lưu giá trị IMM được đọc từ bộ nhớ trong các lệnh có IMM. Thanh ghi này cập nhật giá trị mới khi dc_load_dr tích cực và không cần reset vì đây là thanh ghi không dùng để tạo tín hiệu điều khiển.
Hình 8. Thanh ghi DR
Đến đây, có thể thấy ngõ ra fetch_ir[7:0] được phân tích ban đầu đã có mối liên hệ hoàn toàn theo các ngõ vào, nghĩa là không có tín hiệu nào là chưa xác định được sự phụ thuộc theo ngõ vào. Quá trình phân tích cho fetch_ir[7:0] kết thúc.

 2.2 Phân tích tín hiệu fetch_mem_dout[7:0]

Tín hiệu này chính là ngõ ra của bộ nhớ mem_dout[7:0].
Hình 9. Tín hiệu fetch_mem_dout[7:0]
Quá trình phân tích cấu trúc khối FETCH kết thúc vì tất cả các ngõ ra đã được xác định theo các ngõ vào
3. Phân tích chi tiết khối DECODER

Tương tự với khối DECODER, từng ngõ ra sẽ được phân tích chi tiết. Bạn có thể chọn một ngõ ra bất kỳ để thực hiện. Ở đây, các ngõ ra sẽ được chọn theo thứ tự từ trên xuống như hình sơ đồ tín hiệu đã minh họa.

3.1 Phân tích tín hiệu dc_load_pc
Theo bảng phân tích hoạt động của các lệnh thì PC được cập nhật ở chu kỳ 1 và chu kỳ 3 của một lệnh. Để xác định các chu kỳ hoạt động, một bộ đếm chu kỳ sẽ được sử dụng, gọi là ctrl_counter[1:0]. Chu kỳ 1 tương ứng với bộ đếm bằng 0, chu kỳ 2 bộ đếm bằng 1 và chu kỳ 3 bộ đếm bằng 2.
Hình 10. Bộ đếm số chu kỳ hoạt động của lệnh ctrl_counter[1:0]
Thời điểm xóa bộ đếm về 0 phụ thuộc vào lệnh đó là lệnh nào, lệnh NOP là lệnh 1 chu kỳ, nhóm lệnh không có IMM là 2 chu kỳ và nhóm lệnh có IMM là 3 chu kỳ.
dc_load_pc sẽ tích cực khi bộ đến bằng 0 hoặc bằng 2, tức là khác 1.
Hình 11. Tín hiệu dc_load_pc
3.2 Phân tích tín hiệu dc_imm
Tín hiệu này được sử dụng để chọn giá trị mà PC sẽ được cập nhật. Đối với các lệnh nhảy, ở chu kỳ thứ 3, PC có thể được nạp giá trị bằng giá trị IMM chứa trong thanh ghi DR. Như vậy, tín hiệu này sẽ hoạt động ở chu kỳ thứ 3 và tùy vào điều kiện của từng lệnh nhảy để tích cực phù hợp.
Hình 12. Mạch nguyên lý tín hiệu dc_imm
3.3 Phân tích tín hiệu dc_addr_sel[1:0]
Đây là tín hiệu chọn địa chỉ cho MEMORY khối FETCH. Đối với lệnh LWR và SWR, ở chu kỳ 2, địa chỉ của MEMORY sẽ là giá trị thanh ghi Rs. Đối với lệnh LWI và SWI, ở chu kỳ 3, thì địa chỉ của MEMORY sẽ là giá trị thanh ghi DR. Tận dụng giá trị ctrl_counter[1:0] để thực hiện mạch cho tín hiệu dc_addr_sel[1:0]. Trường hợp còn lại, địa chỉ MEMORY luôn là PC.
Hình 13. Mạch nguyên lý tín hiệu dc_addr_sel[1:0]
3.4 Phân tích tín hiệu dc_rs[7:0] và dc_rd[7:0]
Đây là hai tín hiệu được lựa chọn từ 4 thanh ghi R0, R1, R2 và R3 dựa trên giá trị trường Rs và Rd của mã lệnh.
Hình 14. Mạch nguyên lý của tín hiệu dc_rs[7:0] và dc_rd[7:0]
Trong đó, các thanh ghi R0, R1, R2 và R3 có mạch nguyên lý như sau:
Hình 15. Mạch nguyên lý của các thanh ghi R0, R1, R2 và R3
3.5 Phân tích tín hiệu dc_mem_wr
Đây là tín hiệu cho phép ghi vào MEMORY ở khối FETCH. Tín hiệu chỉ tích cực ở lệnh SWR, chu kỳ 2 và lệnh SWI, chu kỳ 3.
Hình 16. Tín hiệu dc_mem_wr

3.6 Phân tích tín hiệu dc_load_dr
Đây là tín hiệu cho phép nạp thanh ghi DR, xảy ra ở chu kỳ 2 của các lệnh có IMM.
Hình 17. Mạch nguyên lý tín hiệu dc_load_dr

3.7 Phân tích tín hiệu dc_load_ir
Tín hiệu này chỉ tích cực ở chu kỳ thứ nhất để nạp thanh ghi IR.
Hình 18. TÍn hiệu dc_load_ir

3.8 Phân tích tín hiệu dc_op[1:0]
Với chức năng lựa chọn toán tử thực thi ở khối EXECUTE. Dựa vào bảng mã lệnh, toán tử chỉ có 4 loại là AND, OR, ADD và SUB. Hai bit thấp của opcode sẽ được sử dụng để tạo tín hiệu này.
Hình 19. Tín hiệu lựa chọn toán tử cho khối EXECUTE
4. Phân tích chi tiết khối EXECUTE

Khối EXCUTE chỉ chứa một ALU thực hiện bốn phép tính AND, OR, ADD và SUB. Khối này chỉ có một tín hiệu ngõ ra là kết quả của các phép tính trên.
HÌnh 20. Mạch nguyên lý tín hiệu ex_out[7:0]
Việc phân tích cấu trúc chi tiết từng khối của CPU 8 bit SCPU đã hoàn thành. Bước tiếp theo là dựa trên các mạch nguyên lý để mô tả RTL code. Phần này sẽ được trình bày cụ thể trong bài tiếp theo

**Mọi ý kiến trao đổi và góp ý xin comment dưới bài viết.


Chủ Nhật, 17 tháng 9, 2017

[Basic Knowledge][Bài 2] Hướng dẫn phân tích thiết kế lõi IP step-by-step - Tìm tín hiệu giao tiếp giữa các khối chức năng

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).

Trong bài 1 chúng ta đã thực hiện qua 2 trong 5 bước phân tích thiết kế một lõi IP. Trong bài 2, chúng ta sẽ thực hiện phân tích chi tiết từng mạch nguyên lý của một lõi IP để tạo ra bản thiết kế hoàn chỉnh.

1. Lý thuyết về phân tích chi tiết thiết kế
1.1 Hướng dẫn

Phân tích chi tiết là bước mô tả cụ thể mạch nguyên lý, cấu trúc, hoạt động và tín hiệu giao tiếp của từng khối chức năng đã được phân tích trong bước phân tích tổng quan.

Mục đích của phân tích chi tiết là tạo ra bản mô tả chi tiết của tất cả các thành phần thiết kế. Bản mô tả này sẽ được sử dụng để viết RTL code cho thiết kế. Mức độ chi tiết của bản mô tả là phải đủ để một kỹ sư khác, người không thực hiện bản thiết kế này, có thể vẫn mô tả được RTL code mà không cần hiểu rõ chi tiết thiết kế.
    Cách thức thực hiện gồm 2 bước chính:
    1. Xác định các tín hiệu giao tiếp (chân, port) giữa các khối để cụ thể hóa từng tín hiệu giao tiếp với tên tín hiệu, chức năng, độ rộng, mức tích cực.
    2. Phân tích chi tiết từng ngõ ra của từng khối theo các tín hiệu ngõ vào. Bước này là tìm mối liên hệ giữa ngõ ra với các tín hiệu ngõ vào.
    Xác định các tín hiệu giao tiếp (chân, port) giữa các khối

    Sau khi phân chia khối chức năng trong bước "phân tích tổng quan", hai thông tin quan trọng có được là:
    1. Chức năng cơ bản của một khối.
    2. Mối liên hệ cơ bản giữa các khối cho biết khối nào cần nối đến khối nào và để làm gì?
    Ví dụ, quay lại ví dụ về CPU 8 bit SCPU, khối PC là khối có chức năng tính toán giá trị PC truy xuất bộ nhớ chương trình. Trong khi đó, khối memory chính là bộ nhớ chương trình nên khối PC cần được kết nối đến khối memory để cung cấp thông tin địa chỉ truy xuất bộ nhớ.

    Tuy nhiên, sơ đồ khối chưa cung cấp cụ thể số số lượng và chức năng các tín hiệu giao tiếp cần phải có nên trước khi phân tích mạch nguyên lý cho một khối, các tín hiệu giao tiếp này cần được xác định.

    Việc xác định tín hiệu giao tiếp giữa các khối căn cứ chính dựa trên chức năng cơ bản của khối. Hai vấn đề cần được giải đáp khi xác định các tín hiệu giao tiêp giữa các khối là "Thông tin cần phải có để thực hiện chức năng là gì?" và "thông tin này lấy từ đâu?".

    Tên một tín hiệu nên được đặt như sau:
    <tên khối nguồn>_<chức năng>_<*>
    Trong đó:
    • Tên khối nguồn: là ký hiệu viết tắt hoặc tên của khối tạo ra tín hiệu này. Nhờ thông tin này, chúng ta sẽ biết tín hiệu này được tạo ra ở đâu. Ví dụ: dc - decode, ex - execute, fectch,....
    • Chức năng: ký hiệu viết tắt thể gợi nhớ chức năng của. Ví dụ: wr - write, rd - read, ....
    • *: kí hiệu cung cấp thông tin khác. Ví dụ: n - tích cực mức thấp, reg - tín hiệu là ngõ ra FF, ...
    Phân tích chi tiết từng ngõ ra của từng khối


    Một phân tích chi tiết có thể sử dụng nhiều hình thức thể hiện khác nhau như:
    • Mạch nguyên lý chi tiết tới mức cổng (Logic gate schematic)
    • Máy trạng thái hữu hạn FSM (Finite State Machine)
    • Bảng sự thật (Truth table)
    • Biểu thức (expression)
    • Sơ đồ định thời (Timing diagram)
    • Đoạn văn (Description text)
    **Chú ý: Việc phân tich thiết kế nói chung và phân tích chi tiết nói riêng không đảm bảo đúng 100% ngay từ đầu mà trong quá trình thực hiện công việc này có thể phải lặp lại nhiều lần để sửa lỗi và hoàn thiện thiết kế. Tuy nhiên, việc phân tích càng kỹ và chi tiết sẽ giúp việc chỉnh sửa thiết kế dễ dàng và nhanh chóng hơn khi có vấn đề phát sinh.

    1.2 Minh họa các hình thức thể hiện khác nhau khi phân tích chi tiết

    Để minh họa các hình thức thể hiện khi phân tích chi tiết, chúng ta thực hiện ví dụ đơn giản là "Thiết kế bộ phát hiện số chẵn bit 1 trong chuỗi dữ liệu nối tiếp đồng bộ theo xung clock". Bộ này tên E1_CHK có các tín hiệu giao tiếp như sau:
    1. clk: xung clock đồng bộ
    2. rst_n: tín hiệu reset đồng bộ tích cực mức 0
    3. s_in: dữ liệu ngõ vào nối tiếp đồng bộ theo clk
    4. chk_even: tín hiệu báo phát hiện số bit 1 là chẵn, tích cực mức 1

    Hình 1. Sơ đồ tín hiệu giao tiếp của bộ phát hiện số chẵn bit 1
    Biểu diễn mạch nguyên lý của E1_CH như sau:
    Hình 2. Mạch nguyên lý của bộ phát hiện số chẵn bit 1
    Sau khi reset, có 0 số bit 1, chk_even=1. Cứ mỗi lần phát hiện 1 bit 1 thì chk_even sẽ đảo giá trị vì số bit 1 sẽ đổi từ chẵn qua lẻ hoặc từ lẻ qua chẵn xen kẽ

    Biểu diễn dưới dạng FSM như sau:
    Hình 3. FSM phát hiện số bit 1 chẵn
    FSM chỉ có 2 trạng thái EVEN và ODD được chuyển qua lại khi ngõ vào s_in=1. Tín hiệu chk_even=1 khi FSM trong trạng thái EVEN.

    Biểu diễn dưới dạng bảng sự thật như sau:


    Hình 4. Bảng sự thật của thiết kế E1_CHK
    Bảng sự thật mô tả hoạt động của E1_CHK theo cạnh clk và các giá trị ngõ vào. "No change" nghĩa là giá trị chk_even giữ nguyên giá trị mà nó đang giữ trước đó.

    Biểu diễn dưới dạng một sơ đồ định thời như sau:
    Hình 5. Một sơ đồ định thời của thiết kế E1_CHK


    Sơ đồ định thời mô tả các trường hợp:
    • Bắt đầu tại thời điểm 0, chưa có xung clk, giá trị các tín hiệu là don't care. Ngõ vào rst_n và s_in không ảnh hưởng đến ngõ ra. Ngõ ra chk_even mang giá trị tùy định, không biết trước mức logic.
    • Tại thời điểm 1, cạnh xung clock đầu tiên, rst_n=0, không quan tâm giá trị s_in, ngõ ra chk_even được reset về "1"
    • Tại thời điểm 2, rst_n=1, lúc này s_in được xét đến và s_in=0, ngõ ra chk_even không thay đổi giá trị
    • Tại thời điểm 3, rst_n=1, s_in=1, ngõ ra chk_even từ "1" chuyển sang "0"
    • Tại thời điểm 4, rst_n=1, s_in=0, ngõ ra chk_even giữ gí trị "0" trước đó
    • Tại thời điểm 5, rst_n=1, s_in=1, ngõ ra chk_even từ "0" chuyển sang "1"
    • Tại thời điểm 6, rst_n=1, s_in=1, ngõ ra chk_even từ "1" chuyển sang "0"
    Với mô tả định thời trên, chúng ta thấy ngõ ra chk_even đảo giá trị mỗi khi s_in=1 và không đang reset, rst_n=1.

    Mô tả bằng biểu thức là cách dùng biểu thức Boolean hoặc biểu thức đại số (cộng, trừ, nhân, chia, ...) để mô tả các phần mạch tổ hợp. Một mô tả bằng biểu thức của E1_CHK như sau:

    chk_even_next = /rst_n + rst_n./s_in.chk_even + rst_n.s_in./chk_even

    Hình 6. Mô tả phần mạch tổ hợp của E1_CHK bằng biểu thức
    Biểu thức trên đây được được suy ra từ bảng sự thật sau sử dụng phương pháp tổng các tích SOP (Sum of Product).
    Hình 7. Bảng sự thật để suy ra biểu thức chk_even_next

    Mạch nguyên lý tương đương của biểu diễn biểu thức trên như sau:
    Hình 8. Mạch nguyên lý tương đương của biểu thức hình 6

    Mô tả bằng đoạn văn (description text) là cách dùng câu văn mô tả từng điều kiện, trường hợp hoạt động của thiết kế. Cách này chỉ nên áp dụng để chú thích cho những trường hợp cần được chú ý đặc biệt để nhấn mạnh hoặc một thiết kế nhỏ. Đối với các thiết kế phức tạp, việc chỉ sử dụng câu chữ để mô tả một bản thiết kế sẽ không trực quan, thậm chí khó hiểu. Với ví dụ trên, một cách mô tả bằng text như sau:
    • Nếu không có xung clk, giá trị các tín hiệu là don't care (không quan tâm đến mức logic của các tín hiệu
    • Thiết kế hoạt động theo cạnh lên xung clock
    • Tại cạnh lên xung clock, nếu rst_n=0, ngõ ra chk_even luôn được reset về "1"
    • Tại cạnh lên xung clock, nếu rst_n=1, s_in=0 ngõ ra chk_even giữ nguyên giá trị trước đó
    • Tại cạnh lên xung clock, nếu rst_n=1, s_in=1 ngõ ra chk_even sẽ đảo giá trị.
    Trong tất cả các cách thể hiện trên, việc mô tả mạch bằng mạch nguyên lý và FSM được sử dụng nhiều vì cách thể hiện trực quan và dễ dàng cho việc mô tả RTL code sau này. Khi mô phỏng và kiểm tra, việc debug thiết kế cũng trở nên dễ hơn.
    Mô tả bừng sơ đồ định thời thường dùng để mô tả hoạt động của các giao tiếp hoặc minh họa hoạt động của một số tín hiệu yêu cầu đặt biệt về định thời.
    Mô tả bằng bẳng sự thật cũng hay sử dụng trong trường hợp mạch đơn giản và số đầu vào ít. Nếu số đầu vào quá nhiều, khoảng hơn 10 input, thì biểu diễn bảng sự thật hoặc biểu thức sẽ phức tạp và khó đọc hiểu.
    Mô tả bằng đoạn văn thì thường dùng để chú thích các trường hợp đặc biệt. Ví dụ, trong một thiết kế, tất cả các thanh ghi được reset đều có giá trị 0 nhưng một vài thanh ghi đặc biệt được reset đến 1 giá trị khác 0 thì việc chú thích thêm là cần thiết để lưu ý.

    2. Phân tích chi tiết cho ví dụ CPU 8 bit SCPU
    Trước khi tham khảo phần phân tích chi tiết cho CPU 8 bit, bạn đọc đọc cần lưu ý là thiết kế này sẽ được thực hiện sẽ áp dụng đúng như các bước hướng dẫn để thực hiện. Nghĩa là, tác giả thực hiện như thế nào thì sẽ mô tả chính xác lại như vậy và không đưa ngay một kết quả đúng hoàn toàn ngay từ đầu. Sau bước này, chúng ta còn thực hiện mô tả RTL code và mô phỏng cơ bản để kiểm tra để sửa những lỗi phát sinh nếu có. Mục đích, bạn đọc có thể tham khảo cách làm, đánh giá và áp dụng.

    Ở bài trước, chúng ta đã phân tích sơ đồ khối cho thiết kế SCPU như hình sau. Công việc tiếp theo là từng bước chi tiết hóa sơ đồ nguyên lý và mô tả cho từng khối chức năng trong sơ đồ này.

    Hình 9. Sơ đồ khối lõi CPU 8 bit SCPU
    Cấu trúc từng lênh như sau:
    Hình 10. Cấu trúc các lệnh của CPU 8 bit SCPU
    Tuy đã có sơ đồ khối và cấu trúc mã lệnh nhưng việc bắt đầu từ đâu cũng sẽ là khó trả lời đối với các bạn mới bắt đầu. Chúng ta có 3 khối Fetch, Decoder và Execute. Như đã trình bày, đầu tiên, các tín hiệu giao tiếp cơ bản giữa các khối cần được xác định. Sau đó, cấu trúc chi tiết từng khối sẽ được phân tích. Tuy nhiên ở đây, hai vấn đề sau sẽ được tìm hiểu trước:
    1. Giao tiếp bộ nhớ, đọc và ghi, memory như thế nào?
    2. Các lệnh hoạt động như thế nào khi đi qua từng khối Fetch, Decoder, Execute?
    Bạn có thể thắc mắc, tạo sao biết tìm hiểu hai vấn đề trên trước mà không bắt tay vô phân tích luôn từng khối Fetch, Decoder và Execute như đã hướng dẫn? Câu trả lời là bạn có thể bắt tay luôn vào phân tích mà không cần tìm hiểu trước hai phần trên. Trong quá trình phân tích bạn cũng sẽ đụng phải hai vấn đề trên và lúc đó, bạn sẽ bắt đầu giải quyết nó. Bạn có thể nhận thấy, việc bắt đầu phân tích từ đâu? như thế nào? là khác nhau tùy vào kinh nghiệm và cách nhìn vấn đề của mỗi người thiết kế. Một điểm chung duy nhất giữa những người thiết kế là tìm cách đáp ứng tốt nhất yêu cầu đặt ra.

    2.1 Phân tích phần giao tiếp với bộ nhớ

    Đầu tiên, chúng ta xác định xem lệnh được lưu trong bộ nhớ như thế nào? Trong thiết kế này, chúng ta sử dụng bộ nhớ có dung lượng 256 byte, mỗi ô nhớ là 8 bit. Trong khi đó, mã lệnh có độ dài 16 bit. Như vậy, một mã lệnh sẽ được chia làm 2 phần là 8 bit MSB (bit 15 đến 8) và 8 bit LSB (bit 7 đến 0) để lưu trong 2 ô nhớ.
    Quan sát mã lệnh, các bạn sẽ thấy có 2 nhóm lệnh là nhóm có IMM và nhóm không có IMM. Nhóm có IMM làm nhóm bắt buộc phải dùng 2 ô nhớ liên tiếp nhau để lưu mã lệnh. Nhóm không có IMM thì chỉ cần 1 ô nhớ để lưu 8 bit MSB vì 8 bit LSB không sử dụng.
    Hình 11. Cách lưu mã lệnh trong bộ nhớ
    Giả sử rằng bộ nhớ hoạt động trong 1 chu kỳ xung clock clk. Nghĩa là, khi một lệnh đọc/ghi được đưa đến bộ nhớ ở một cạnh lên xung clock thì ngay cạnh lên xung clock tiếp theo bộ nhớ đã thực hiện xong.
    Hình 12. Sơ đồ tín hiệu giao tiếp và định thời của bộ nhớ
    Lưu ý, đây chỉ là một ví dụ về mô hình bộ nhớ sẽ sử dụng. Bạn hoàn toàn có thể sử dụng mô hình bộ nhớ khác. Tuy nhiên, việc thay đổi memory có thể ảnh hưởng đến cấu trúc chi tiết từng khối.

    2.2 Phân tích hoạt động của các lệnh

    Đối với CPU việc phân tích hoạt động của từng lệnh nhằm mục đích trả lời các câu hỏi sau:
    1. Lệnh sẽ được xử lý như thế nào trong CPU?
    2. Lệnh sẽ tác động đến phần nào trong CPU?
    3. Lệnh đó hoạt động trong mấy chu kỳ xung clock?
    Trong thiết kế CPU, việc tìm hiểu này gọi là là phân tích đường dữ liệu (datapath) của từng lệnh. Như đã trình bày ở trên, chúng ta có 2 nhóm lệnh chính là nhóm có IMM (2 byte) và nhóm không có IMM (1 byte). Nhóm 2 byte phải đọc bộ nhớ 2 lần mới thực thi xong một lệnh. Nhóm 1 byte chỉ cần đọc bộ nhớ 1 lần nên nhóm 2 byte sẽ tốn nhiều chu kỳ xung clock clk hơn để thực hiện 1 lệnh.
    Hình 13. Bảng phân tích chu kỳ hoạt động của từng lện
    Chu kỳ hoạt động của các lệnh như sau:
    1. Chu kỳ 1: Các lệnh được fetch vào thanh ghi lệnh IR. Đồng thời PC tăng một đơn vị chỉ đến ô nhớ tiếp theo. Chu kỳ này áp dụng cho tất cả các lệnh. Khối FETCH hoạt động trong chu kỳ này.
      1. Lệnh NOP là lệnh không thực thi tác vụ nào sẽ được kết thúc trong chu kỳ này.
    2. Chu kỳ 2: Lệnh đã có trong IR được giải mã để tạo ra các tín hiệu điều khiển, khối DECODER hoạt động. Đồng thời:
      1. Lệnh được thực thi nếu là lệnh 1 byte, khối EXCUTE hoạt động
      2. Đọc bộ nhớ lưu giá trị IMM vào thanh ghi DR nếu là lệnh 2 byte
    3. Chu kỳ 3: Thực thi lệnh 2 byte cập nhật giá trị PC mới, khối FETCH hoạt động vì khối này chứa PC.
    2.3 Phân tích các tín hiệu giao tiếp cơ bản giữa các khối

    Việc hiểu rõ giao tiếp bộ nhớ và hoạt động từng lệnh sẽ giúp việc phân tích các tín hiệu giao tiếp dễ dàng hơn. Để xác định tín hiệu giao tiếp, chúng ta lần lượt tìm tín hiệu ngõ vào của từng khối bằng cách trả lời 2 câu hỏi sau:
    1. Chức năng này cần thông tin nào?
    2. Thông tin được lấy từ đâu?
    Chúng ta chỉ cần tập trung xác định tín hiệu ngõ vào. Tín hiệu ngõ ra của một khối chính là tín hiệu ngõ vào mà khối khác yêu cầu.
    Quy định chung, các tín hiệu điều khiển đều tích cực mức 1 trừ khi có chú thích khác.
    Ở thiết kế này, chúng ta có 3 khối chính FETCH, DECODER và EXECUTE. Chúng ta sẽ thực hiện lần lượt cho từng khối theo thứ tự đã liệt kê. Lưu ý, bạn có thể bắt đầu với bất kỳ khối nào, không cần phải theo thứ tự này.

    Tìm tín hiệu ngõ vào khối FETCH

    Khối FETCH chứa các khối con là MEMORY, PC, DR và IR. Các chức năng chính của khối này là:
    1. Tính toán PC
    2. Truy cập MEMORY
    3. Cập nhật DR
    4. Cập nhật IR
    Với chức năng tính toán PC, chúng ta sẽ thực hiện như sau:
      1. Chức năng "tính toán PC" cần thông tin nào?
        1. PC là một thanh ghi => Cần xung clock clk
        2. PC cần reset về đầu chương trình để chạy => Cần tín hiệu reset
        3. PC cần được cập nhật giá trị => Cần tín hiệu cho biết khi nào PC cập nhật giá trị
        4. PC sẽ cập nhật giá trị giá trị như sau:
          1. PC tự tăng giá trị lên 1 đơn vị => không cần tín hiệu ngoài
          2. PC có thể lấy giá trị từ IMM được lưu trong thanh ghi DR ở các lệnh JEQ, JNE, JGT, JLT, JMP => DR chứa trong FETCH nên không cần tín hiệu ngoài
        5. Do PC nhận 2 đầu vào, tự tăng hoặc từ DR, nên cần có tín hiệu để chọn giá trị PC sẽ được cập nhật
      2. Thông tin trên lấy từ đâu?
        1. Xung clock clk: Được cấp từ ngoài CPU
        2. Tín hiệu reset rst_n: Được cấp từ ngoài CPU
        3. Tín hiệu cập nhật PC: Được cấp từ khối DECODER vì khối này giải mã lệnh sẽ biết lệnh đó là lệnh nào và thực hiện mấy chu kỳ => đặt tên dc_load_pc
        4. Tín hiệu lựa chọn giá trị PC sẽ được cập nhật: Được cấp từ khối DECODER vì khối này giải mã lệnh sẽ biết lệnh đó là lệnh nào => đặt tên dc_imm
      Với chức năng truy cập MEMORY, chúng ta thực hiện như sau:
      1. Chức năng "truy cập MEMORY" cân thông tin nào?
        1. MEMORY là bộ nhớ đồng bộ => cần xung clock clk
        2. MEMORY cần thông tin truy cập là đọc hay ghi => tín tín hiệu điều khiển các năng đọc/ghi
        3. MEMORY cần địa chỉ truy cập có thể là
          1. PC => không cần tín hiệu ngoài
          2. IMM được lưu trong DR đối với lệnh LI => không cần tín hiệu ngoài
          3. Địa chỉ lưu trong 1 ô nhớ của MEMORY đối với lệnh LWR, SWR, LWI hoặc SWI => cần giá trị thanh ghi Rs đối với lệnh LWR và SWR
        4. Do địa chỉ MEMORY có thể là 1 trong 3 giá trị trên nên cần tín hiệu lựa chọn giá trị đúng
        5. MEMORY cần bus dữ liệu ghi => tín hiệu bus dữ liệu ghi
      2. Thông tin trên lấy từ đâu?
        1. Xung clock clk
        2. Tín hiệu điều khiển đọc/ghi: Được cấp từ khối DECODER vì khối này giải mã lệnh sẽ biết lệnh nào đọc hay ghi bộ nhớ => đặt tên dc_mem_wr. Nếu tín hiệu này bằng 1 sẽ là tác vụ ghi MEMORY và ngược lại là đọc MEMORY
        3. Địa chỉ của MEMORY => Chỉ có giá trị thanh ghi Rs là tín hiệu ngoài cần lấy từ khối DECODER, dặt tên là dc_rs[7:0]
        4. Tín hiệu lựa chọn giá trị địa chỉ => Được cấp từ khối DECODER vì khối này giải mã lệnh sẽ biết lệnh ghi/đọc lấy giá trị địa chỉ từ đâu => đặt tên là dc_addr_sel[1:0]. Tín hiệu 2 bit vì có 3 giá trị cần được xác định
        5. Bus dữ liệu ghi => Lấy từ DECODER vì chỉ có 2 lệnh ghi vào MEMORY là SWR và SWI đều lấy giá trị Rd để ghi vào MEMORY => đặt tên là dc_rd[7:0]
      Tương tự, với chức năng cập nhật DR và IR, cả 2 chức năng này đều lấy giá trị từ MEMORY và cần một tín hiệu điều khiển để biết khi nào cập nhật IR và khi nào cập nhật DR:
      1. Tín hiệu điều khiển cập nhật giá trị IR lấy từ DECODER dc_load_ir
      2. Tín hiệu điều khiển cập nhật giá trị DR lấy từ DECODER dc_load_dr
      Hình 14. Các tín hiệu giao tiếp sau khi tìm tín hiệu ngõ vào của khối FETCH
      Tìm tín hiệu ngõ vào khối DECODER


      DECODER là khối có chức năng:
      1. Giải mã lệnh và tạo tín hiệu điều khiển các khối khác
      2. Chứa các thanh ghi R0, R1, R2 và R3
      Các bạn hãy thực hiện tương tự để tìm các tín hiệu ngõ vào. Ở đây, các tín hiệu chỉ được mô tả lại chức năng:
      1. Xung clock clk
      2. Tín hiệu reset rst_n tích cực mức thấp
      3. fetch_ir[7:0] mã lệnh lấy từ khối FETCH
      4. fetch_dr[7:0] là giá trị IMM lấy từ thanh ghi DR, khối DECODER sử dụng giá trị này để nạp cho các thanh ghi R0, R1, R2, R3 ở lệnh LI
      5. ex_dout[7:0] giá trị tính toán từ khối EXECUTE để cập nhật giá trị các thanh ghi R0, R1, R2 hoặc R3 ở lệnh AND, OR, ADD, SUB, LWR, MOV, LWI, LI.
      6. fetch_mem_dout[7:0] giá trị đọc MEMORYlấy từ khối FETCH vì lệnh LWI và LWR dữ liệu đọc từ MEMORY được lấy và lưu vào thanh ghi Rd
      Hình 15. Các tín hiệu giao tiếp sau khi tìm tín hiệu ngõ vào cho khối DECODER
      Tìm tín hiệu ngõ vào khối EXECUTE

      Khối EXECUTE thực hiện chức năng chính là tính toán số học và tạo kết quả cập nhật cho các thanh ghi R0, R1, R2 và R3. Các tín hiệu ngõ vào mà khối EXECUTE cần là:
      1. dc_rs[7:0] Giá trị thanh ghi Rs trong các lệnh tính toán
      2. dc_rd[7:0] Giá trị thanh ghi Rd trong các lệnh tính toán
      3. dc_op[1:0] Để lựa chọn phép toán sẽ thực thi là AND, OR, ADD hay SUB
      Hình 16. Sơ đồ tín hiệu giao tiếp giữa các khối sau khi tìm các tín hiệu ngõ vào cho khối EXECUTE
      Đến đây, chúng ta kết thúc bước tìm tín hiệu giao tiếp giữa các khối chức năng, một bước của phân tích chi tiết thiết kế. Bước tiếp theo sẽ là phân tích chi tiết từng khối dựa trên các tín hiệu giao tiếp trên.

      ** Mọi ý kiến đóng góp và trao đổi giúp các bài viết được hoàn thiện và chính xác hơn, bạn đọc có thể comment dưới bài viết hoặc email.