• 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

    Vi mạch và Ứng dụng

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

    Vi mạch và Ứng dụng

Thứ Bảy, 14 tháng 9, 2019

[UVM] Bài 6 - Mô tả hoạt động của Monitor và Scoreboard

Bài viết này mô tả giao tiếp và chức năng của Monitor và Scoreboard trong môi trường UVM đã được build ở bài 5. Bài viết tập trung vào việc giải thích làm thế nào một transaction được truyền từ Monitor đến Scoreboard và làm thế nào Scoreboard kiểm tra dữ liệu truyền nhận giữa hai UART trong môi trường. Bên cạnh đó, chức năng chi tiết của Monitor cũng sẽ được trình bày.
Tham khảo các bài viết trước ở đây.

1) Kết nối của Monitor và Scoreboard
1.1) Các giao tiếp và ports
Trước khi đi vào chức năng chi tiết của Monitor và Scoreboard. Nội dung phần này mô tả chi tiết kết nối của Monitor và Scoreboard trong môi trường.
Hình 1: Sơ đồ khối của môi trường UVM
Trong môi trường này, uart_0, còn gọi là UART-TX, kết nối với Agent coApbMasterAgentTx còn uart_1, còn gọi là UART-RX, kết nối với Agent coApbMasterAgentRx. Chú ý, việc đặt tên -TX và -RX không có nghĩa là uart_0 chỉ truyền còn uart_1 chỉ nhận. Hai UART này có chức năng truyền và nhận như nhau.
Quan sát hình minh họa trên đây, bạn sẽ thấy Monitor có các kết nối sau:
  • APB interface có tên instance là vifApbMaster để giám sát tất cả các transaction đọc/ghi
  • Interrupt interface có tên instance là vifInterrupt để giám sát tất cả các tín hiệu interrupt
  • Analysis port có tên instance là ap_toScoreboard để gửi các transaction có kiểu dữ liệu là cApbTransaction trên APB interface đến Scoreboard
Bên cạnh đó, giữa Monitor và Scoreboard còn một analysis port khác tên preset_toScoreboard được dùng để gửi thông tin reset cho Scoreboard. Kiểu dữ liệu gửi trên port này là logic. Cái này không thể hiện trên hình minh họa nên các bạn chú ý khi đọc code.
Trong MonitorcApbMasterMonitor.sv, các thành phần kết nối trên được khai báo như sau:
//Declare analysis ports
uvm_analysis_port #(logic) preset_toScoreboard;
uvm_analysis_port #(cApbTransaction) ap_toScoreboard;
//Declare the monitored interfaces
virtual interface ifApbMaster vifApbMaster;
virtual interface ifInterrupt vifInterrupt;
Hai analysis port sẽ được tạo trong build_phase
ap_toScoreboard = new("ap_toScoreboard", this); preset_toScoreboard = new("preset_toScoreboard", this);
Scoreboard có các kết nối như sau:
  • Port tên aimp_frmMonitorTX để nối với analysis port ap_toScoreboard của Monitor trên Agent coApbMasterAgentTx.
  • Port tên aimp_frmMonitorRX để nối với analysis port ap_toScoreboard của Monitor trên Agent coApbMasterAgentRx.
  • Port tên aimp_resetfrmTX để nối với analysis port preset_toScoreboard của Monitor trên Agent coApbMasterAgentTx.
Scoreboard không có port kết nối với analysis port preset_toScoreboard của Monitor trên Agent coApbMasterAgentRx vì trong môi trường này, chân preset_n của hai UART được sử dụng chung một nguồn. Khi reset xảy ra, toàn bộ DUT sẽ được reset.
Trong ScoreboardcScoreboard.sv, các thành phần kết nối được khai báo như sau:
uvm_analysis_imp_frmMonitorTX #(cApbTransaction, cScoreboard) aimp_frmMonitorTX;
uvm_analysis_imp_frmMonitorRX #(cApbTransaction, cScoreboard) aimp_frmMonitorRX;
uvm_analysis_imp_resetfrmTX #(logic, cScoreboard) aimp_resetfrmTX;
Các port này sẽ được tạo trong build_phase của Scoreboard như sau:
imp_frmMonitorTX = new("aimp_frmMonitorTX", this);
aimp_frmMonitorRX = new("aimp_frmMonitorRX", this);
aimp_resetfrmTX = new("aimp_resetfrmTX", this);
Chú ý, phần hậu tố khi khai báo port (tô màu vàng) phải được đăng ký để sử dụng như sau:
`uvm_analysis_imp_decl(_frmMonitorTX)
`uvm_analysis_imp_decl(_frmMonitorRX)
`uvm_analysis_imp_decl(_resetfrmTX)
1.2) Kết nối
Monitor kết nối với đến APB interface và interrupt interface thông qua khai báo uvm_config_db testTop.sv.
//Connect APB interface
uvm_config_db#(virtual interface ifApbMaster)::set(null,"uvm_test_top.coEnv.coApbMasterAgentTx*","vifApbMaster",vifApbMaster_Tx);
uvm_config_db#(virtual interface ifApbMaster)::set(null,"uvm_test_top.coEnv.coApbMasterAgentRx*","vifApbMaster",vifApbMaster_Rx);
//Connect Interrupt interface
uvm_config_db#(virtual interface ifInterrupt)::set(null,"uvm_test_top.coEnv.coApbMasterAgentTx*","vifInterrupt",vifInterrupt_Tx);
uvm_config_db#(virtual interface ifInterrupt)::set(null,"uvm_test_top.coEnv.coApbMasterAgentRx*","vifInterrupt",vifInterrupt_Rx);
Các khai báo uvm_config_db giúp kết nối các instance của interface khai báo ở testTop.sv kết nối đến các thành phần UVM trong các Agent coApbMasterAgentTx và coApbMasterAgentRx.
Trong build_phase của Monitor, các kết nối sẽ được kiểm tra lại để đảm bảo các kết nối này đã tồn tại.
//Check the APB connection
if(!uvm_config_db#(virtual interface ifApbMaster)::get(this,"","vifApbMaster",vifApbMaster)) begin
  `uvm_error("cApbMasterDriver","Can NOT get vifApbMaster!!!")
end
//Check the interrupt connection
if(!uvm_config_db#(virtual interface ifInterrupt)::get(this,"","vifInterrupt",vifInterrupt)) begin
  `uvm_error("cVSequencer","Can NOT get vifInterrupt!!!")
end
Một thông điệp lỗi sẽ báo thông qua macro `uvm_error nếu kết nối với interface mong muốn không được tìm thấy.
2) Cấu trúc và chức năng của Monitor
Monitor (cApbMasterMonitor.sv) được mở rộng từ class uvm_monitor và xây dựng một số chức năng riêng trong hai phase:

  1. build_phase
    1. Kiếm tra kết nối của Monitor trong môi trường. Cụ thể, hai interface vifApbMaster và vifInterrupt được kiểm tra.
    2. Tạo đối tượng của các TLM analysis port. Cụ thể, hai port được tạo là ap_toScoreboard và preset_toScoreboard.
    3. Tạo đối tượng coApbTransaction để lưu lại các transaction trên APB interface. Chú ý, đối tượng này phải cùng kiểu dữ liệu với kiểu dữ liệu sẽ được gửi qua analysis port ap_toScoreboard , kiểu dữ liệu này là cApbTransaction.
  2. run_phase sẽ thực hiện các task sau song song 
    1. collect_data() giám sát APB interface, phát hiện read/write transfer trên APB interface và lưu lại trong đối tượng coApbTransaction. Sau đó, method write() được sử dụng để gửi gói coApbTransaction đến TLM analysis port ap_toScoreboard. Phía Scoreboard sẽ nhận và xử lý
    2. detect_reset() giám sát tín hiệu reset, preset_n trong testTop.sv, của môi trường, lưu lại trong biến preset_n và gửi giá trị này qua analysis port preset_toScoreboard bằng method write().
    3. monitor_ifEn() giám sát các APB transaction ghi vào thanh ghi interrupt enable của DUT, DUT gồm 2 UART là UART-TX và UART-RX. Nếu phát hiện transaction ghi vào thanh ghi interrtupt enable, địa chỉ offset 16'h0010, giá trị ghi vào các bit interrupt enable sẽ được lau lại trong biến ifEn[4:0]. Biến này sẽ được dùng để điều khiển task detect_intf() với mục đích xác định xem một tín hiệu interrupt tích cực, chuyển từ mức 0 sang mức 1, có đúng hay không.
    4. detect_intf() giám sát tất cả các tín hiệu interrupt thông qua interface vifInterrupt. Nếu một interrupt tích cực nhưng không được enable, `uvm_error sẽ thông báo. Nếu một interrupt tích cực và được enable, một `uvm_warning sẽ thông báo cho người test biết để kiểm tra lại xem có đúng như mong muốn hay không. Một biến ifSta được sử dụng để đảm bảo các thông điệp `uvm_error và `uvm_warning chỉ in ra một lần khi interrupt bắt đầu tích cực. Nếu không có biến này, các thông điệp sẽ được in liên tục trong suốt quá trình tín hiệu interrupt tích cực sau mỗi cạnh lên xung clock.
Hình 2: Cấu trúc và chức năng chính của Monitor (cApbMasterMonitor.sv)
3) Cấu trúc và chức năng của Scoreboard
Scoreboard (cScoreboard.sv) được mở rộng từ class uvm_scoreboard và thêm các chức năng riêng như sau:

  1. build_phase tạo ra các đối tượng của analysis implementation port gồm:
    1. aimp_frmMonitorTX kết nối với analysis port ap_toScoreboard của Monitor trong agent coApbMasterAgentTx, xem cEnv.sv.
    2. aimp_frmMonitorRX kết nối với analysis port ap_toScoreboard của Monitor trong agent coApbMasterAgentRx, xem cEnv.sv.
    3. aimp_resetfrmTX kết nối với analysis port preset_toScoreboard của Monitor trong agent coApbMasterAgentTx, xem cEnv.sv. Như đã trình bày ở trên, do reset của hệ thống là chung nên chỉ cần kết nối một port để giám sát trạng thái reset, không cần kết nối port đến agent coApbMasterAgentRx.
  2. run_phase thực thi các task write<suffix> với suffix được khai báo bởi `uvm_analysis_imp_decl. Các task này gọi là các "analysis implementation" dùng để xử lý các transaction nhận trên analysis implementation port. Các bạn xem lại mục TLM trong bài viết số 3.
    1. write_resetfrmTX() giám sát trạng thái reset được Monitor gửi đến là tích cực cờ trạng thái rst_flg=1 nếu reset tích cực mức 0.
    2. write_frmMonitorTX() giám sát việc đọc dữ liệu từ thanh ghi dữ liệu của UART-TX (uart_0), địa chỉ offset 16'h000C. Nếu có một tranasaction đọc thanh ghi dữ liệu, dữ liệu đọc trên prdata[7:0] sẽ so sánh với dữ liệu truyền phía UART-RX (uart_1). Nếu giá trị dữ liệu đọc trùng khớp thì thông điệp báo SUCCESS sẽ được in ra, nếu dữ liệu đọc bị sai thì thông điệp báo lỗi FAIL sẽ được in ra.
    3. write_frmMonitorRX() giám sát việc đọc dữ liệu từ thanh ghi dữ liệu của UART-RX (uart_1), địa chỉ offset 16'h000C. Nếu có một tranasaction đọc thanh ghi dữ liệu, dữ liệu đọc trên prdata[7:0] sẽ so sánh với dữ liệu truyền phía UART-TX (uart_0). Nếu giá trị dữ liệu đọc trùng khớp thì thông điệp báo SUCCESS sẽ được in ra, nếu dữ liệu đọc bị sai thì thông điệp báo lỗi FAIL sẽ được in ra.
  3. report_phase sẽ kiểm tra lại số lượng dữ liệu mong muốn truyền trên UART-TX và UART-RX. Nếu vẫn còn dữ liệu cần truyền ở UART-TX nhưng chưa được đọc và kiểm tra trên UART-RX hoặc ngược lại thì Scoreboard sẽ cảnh báo với `uvm_warning. Chú ý, đây không phải là một lỗi (error) vì nó phụ thuộc vào mục đích test của người viết testbench (testcase).
Hình 3: Cấu trúc và chức năng của Scoreboard (cScoreboard.sv)
Để kiểm tra dữ liệu truyền nhận giữa 2 UART, Scoreboard thực hiện như sau:
  1. Lưu lại dữ liệu cần truyền trên mỗi UART trong một hàng đợi (queue), nó tương tự như một FIFO
  2. Tại UART phía đối diện, UART nhận, mỗi dữ liệu đọc ra sẽ được so sánh với dữ liệu trong FIFO đã lưu ở bước trên.
  3. Sau mỗi lần so sánh, dữ liệu đã so sánh trong FIFO sẽ được xóa.
Thuật toán chi tiết để kiểm tra dữ liệu truyền từ UART-TX đến UART-RX sẽ được trình bày sau đây. Chiều từ UART-RX đến UART-TX thực hiện tương tự nên sẽ không trình bày chi tiết.
Đầu tiên, phần xử lý cập nhật dữ liệu truyền vào hàng đợi (FIFO) như sau:
1. Kiểm tra trạng thái reset thông qua cờ rst_flg Nếu cờ này bằng 1, tức là có reset, thì Scoreboard sẽ khởi động lại và xóa toàn bộ FIFO lưu dữ liệu truyền
queueTransTX.delete();
uartEnTX = 1'b0;
2. Nếu không phải trong trạng thái reset, rst_flg=0, bit báo trạng thái enable của UART-TX sẽ luôn được cập nhật đầu tiên nếu có bất cứ transaction ghi đến thanh ghi SE, offset là 16'h0004.
if (TransOnTX.pwrite && (TransOnTX.paddr[15:0] == 16'h0004)) begin
  uartEnTX = TransOnTX.pwdata[0];
end
 3. Nếu UART-TX được enable, uartEnTX=1, và có transaction ghi vào thanh ghi dữ liệu, địa chỉ offset là 16'h000C thì dữ liệu này là dữ liệu cần truyền nên sẽ lưu vào cuối FIFO. Chú ý, chỉ lưu 8 bit LSB, 24 bit đầu là 0.
else if (TransOnTX.pwrite && (TransOnTX.paddr[15:0] == 16'h000C) && uartEnTX) begin  queueTransTX.push_back(TransOnTX.pwdata & 32'h0000_00ff);
end
Phần xử lý lưu dữ liệu truyền vào FIFO của UART-TX được thực hiện bởi method write_frmMonitorTX. Method này lấy APB transaction từ Monitor cảu agent coApbMasterAgentTx.
Hình 4: Giai thuật cập nhật dữ liệu truyền của Scoreboard
Tiếp theo, phần xử lý kiểm tra dữ liệu đọc, dữ liệu truyền từ UART-TC đến UART-RX, trên UART-RX được thực hiện như sau:
1. Kiểm tra trạng thái reset thông qua cờ rst_flg Nếu cờ này bằng 1, tức là có reset, thì Scoreboard sẽ không thực thi quá trình kiểm tra dữ liệu đọc.
2. Nếu không phải trong trạng thái reset, rst_flg=0, bit báo trạng thái enable của UART-RX sẽ luôn được cập nhật đầu tiên nếu có bất cứ transaction ghi đến thanh ghi SE, offset là 16'h0004.
if (TransOnRX.pwrite && (TransOnRX.paddr[15:0] == 16'h04)) begin
  uartEnRX = TransOnRX.pwdata[0];
end
 3. Nếu UART-RX được enable, uartEnRX=1, và có transaction đọc từ thanh ghi dữ liệu, địa chỉ offset là 16'h000C thì dữ liệu này là dữ liệu sẽ được kiểm tra.
4. Lấy phần tử đầu tiên từ FIFO lưu dữ liệu truyền của UART-TX, queueTransTX.
queueCompRX = queueTransTX[0];
4. So sánh 8 bit dữ liệu đọc được với phần tử đầu tiên lấy từ FIFO.
  • Nếu hai giá trị bằng nhau, in ra thông điệp báo SUCCESS
  • Nếu hai giá trị khác nhau, in ra thông điệp báo FAIL
if ((TransOnRX.prdata & 32'h0000_00ff) == queueCompRX) begin  `uvm_info("SB INFO", $sformatf("[%t] SUCCESS on UART-RX: transfer data = %2h, queueTransTX size = %d", $time, TransOnRX.prdata, queueTransTX.size()), UVM_LOW);
endelse begin  `uvm_error("SB ERROR", $sformatf("[%t] FAIL on UART-RX: read data = %2h, expected data =%2h, queueTransTX size = %d", $time, TransOnRX.prdata, queueCompRX, queueTransTX.size()))
end
5. Kiểm tra lại số phần tử trong FIFO queueTransTX.
  • Nếu FIFO rỗng, in ra thông điệp báo dữ liệu đọc được không phải là dữ liệu được truyền từ UART-TX. Điều này xảy ra khi phát một transaction đọc thanh ghi dữ liệu khi nó rỗng.
  • Nếu FIFO không rỗng, thực hiện xóa phần tử đầu tiên của FIFO, phần tử vừa dùng để so sánh ở bước trên
if (queueTransTX.size() != 0) begin
  queueTransTX.delete(0);
endelse begin  uvm_warning("SB UNFINISH-RX", "Read data but do NOT have any transmitted data");
end
Phần xử lý kiểm tra dữ liệu đọc trên UART-RX được thực hiện trong method write_frmMonitorRX.
Hình 5: Giải thuật kiểm tra dữ liệu nhận trên UART của Scoreboard
Các bạn hãy tải source code trên Github để vừa đọc vừa so sánh. Mọi góp ý, các bạn có thể comment dưới bài viết.

Dữ liệu có thể dowload:
Môi trường UVM bản Draff trên Github

Lịch sử cập nhật:
1/ 2019.09.15 - Tạo lần đầu

Danh sách tác giả:
1. Phạm Thanh Trâm
2. Đoàn Đức Hoàng (email: hoangbk154@gmail.com)
3. Trương Công Hoàng Việt
4. Nguyễn Sinh Tơn
5. Nguyễn Hùng Quân

Thứ Năm, 12 tháng 9, 2019

[CRC] Bài 5 - Các thông số ảnh hưởng đến kết quả tính CRC

Bài viết này trình bày khái quát lại về các vấn đề liên quan đến việc tính CRC (Cyclic Redundancy Code) bao gồm các thông số tính CRC, giá trị khởi tạo và các cách biểu diễn đa thức sinh. Bài viết sẽ giải thích chi tiết sự ảnh hưởng của các thông số này đến việc tính CRC. Đồng thời, thực hiện 1 bộ tính CRC hỗ trợ đầy đủ việc thiết lập các thông số tính CRC bằng Verilog HDL.
1) Đặt vấn đề
Trong các bài viết trước, tác giả có trình bày về lý thuyết cơ bản và các cách thiết kế bộ tính CRC:
Trong các bài viết trên, các bộ tính CRC có đặc điểm như sau:
  • Chuỗi bit đa thức sinh được biểu diễn theo cách thông thường (Normal representation)
  • Giá trị khởi tạo ban đầu của thanh ghi CRC trong các bộ tính CRC là 0
Trên thực tế, kết quả tính toán CRC sẽ phụ thuộc vào một số thông số cấu hình. Với cùng một đa thức sinh, nếu các thông số cấu hình khác nhau, kết quả tính CRC sẽ khác nhau. Tùy vào giao thức truyền thông, việc tính toán CRC theo cấu hình nào sẽ được quy định cụ thể.
2) Các thông số ảnh hưởng đến việc tính toán CRC
2.1) Cách biểu diễn chuỗi bit đa thức sinh (Polynomial Generator representation)
Như đã nói ở phần 1, các bộ tính CRC đang sử dụng chuỗi bit đa thức sinh theo cách biểu diễn thông thường. Một đa thức sinh có nhiều cách biểu diễn chuỗi bit khác nhau. Mỗi cách biểu diễn sẽ cho giá trị chuỗi bit đa thức sinh khác nhau.
Đa thức sinh tổng quát của bộ tính CRC-n như sau:
Trong đó, an là 1 hoặc 0.
Chuỗi bit của đa thức sinh có các cách biểu diễn sau:
  • Cách biểu diễn thông thường (normal representation)
  • Cách biểu diễn đảo ngược (reversed representation)
  • Cách biểu diễn Koopman (Koopman representation)
  • Cách biểu diễn nghịch đảo (Reciprocal representation)
Xét đa thức sinh của một CRC-8 sau đây:
Ví dụ 1 - Đa thức sinh CRC-8
Đa thức sinh này sẽ được sử dụng để minh họa cho các cách biểu diễn chuỗi bit đa thức sinh.
Cách biểu diễn thông thường sẽ loại bỏ bit phần tử có trọng số cao nhất x^n. Trong một đa thức sinh, an luôn bằng 1. Chuỗi bit đa thức sinh, tính từ trái qua phải, sẽ chứa các bit từ n-1 đến 0. Áp dụng vào ví dụ 1, x^8 sẽ được loại bỏ, chuỗi bit đa thức sinh sẽ từ x^7 đến x^0 là:
1101_0101 (0xD5)
Cách biểu diễn đảo ngược sẽ loại bỏ bit phần tử có trọng số cao nhất x^n. Chuỗi bit đa thức sinh, tính từ trái qua phải, sẽ chứa các bit từ 0 đến n-1. Thứ tự này ngược với cách biểu diễn thông thường. Áp dụng vào ví dụ 1, x^8 sẽ được loại bỏ, chuỗi bit đa thức sinh sẽ từ x^0 đến x^7 là:
1010_1011 (0xAB)
Cách biểu diễn Koopman sẽ loại bỏ bit phần tử có trọng số thấp nhất x^0. Trong một đa thức sinh, a0 luôn bằng 1 giống như an. Chuỗi bit đa thức sinh, tính từ trái qua phải, sẽ chứa các bit từ n đến 1. Áp dụng vào ví dụ 1, x^0 sẽ được loại bỏ, chuỗi bit đa thức sinh sẽ từ x^8 đến x^1 là:
1110_1010 (0xEA)
Các biểu diễn nghịch đảo sẽ đảo lại toàn bộ thứ tự bit của đa thức sinh, x^0 thành x^n, x^1 thành x^n-1, ... sau đó, bit đầu tiên bên trái sẽ bị loại bỏ. Chuỗi bit đa thức sinh, tính từ trái qua phải, sẽ chứa các bit từ 1 đến n. Áp dụng vào ví dụ 1, x^0 sẽ được loại bỏ, chuỗi bit đa thức sinh sẽ từ x^1 đến x^8 là:
0101_0111 (0x57)
Như vậy, cùng với một chuỗi dữ liệu, giá trị CRC sẽ khác nhau nếu chuỗi bit đa thức sinh được biểu diễn theo các cách khác nhau.
2.2) Giá trị khởi tạo ban đầu (Initial Value)
Trong các thiết kế trình bày ở những bài trước, giá trị khởi tạo của các bộ tính là 0.
Giá trị khởi tạo là giá trị sẽ được XOR với những bit đầu tiên của chuỗi dữ liệu trước khi tính CRC, bước này còn gọi là "Initial XOR". Giá trị khởi tạo có số bit bằng với số bit của chuỗi CRC. 
Ví dụ, xét chuỗi dữ liệu như sau:
10011010_10111100

Nếu chuỗi trên được đưa vào bộ tính CRC-8 với giá trị khởi tạo là 0 thì chuỗi bit dùng để tính toán CRC chính là chuỗi bit nguyên thủy trên. Nếu việc tính CRC yêu cầu giá trị khởi tạo là 8'hff thì byte đầu tiên của chuỗi bit đầu vào sẽ được XOR với 8'hff trước khi dùng để tính toán CRC. Chuỗi bit đầu vào trở thành:
01100101_10111100

Như vậy, với các giá trị khởi tạo khác nhau, kết quả tính CRC sẽ khác nhau.
2.3) Thứ tự bit của các byte dữ liệu đầu vào
Trong các thiết kế ở những bài trước, các bit của chuỗi dữ liệu đầu vào được xử lý theo thứ tự thông thường. Thứ tự bit thông thường là thứ tự bit được đưa vào bộ tính CRC và bộ tính CRC không thay đổi thứ tự này.
Tùy giao thức, thứ tự bit trong từng byte dữ liệu (8 bit) có thể bị đảo ngược trước khi tính CRC. Việc đảo ngược thứ tự bit trong từng byte gọi là "Reflected Input".
Ví dụ, xét chuỗi dữ liệu như sau:
10011010_10111100

Nếu tính CRC với cơ chế "Reflected input", thì chuỗi bit đầu vào sẽ được biến đổi thành:
01011001_00111101
Từng nhóm 8 bit sẽ bị đảo ngược vị trí bit. Như vậy, tính CRC với "normal input" và "reflected input" sẽ cho kết quả chuỗi CRC khác nhau.
2.4) Thứ tự bit của kết quả tính CRC
Trong các thiết kế ở những bài trước, thứ tự bit của chuỗi CRC (kết quả tính CRC) là thứ tự thông thường. Thứ tự thông thường là thứ tự tự nhiên của chuỗi CRC sau khi tính toán. Trong thứ tự này, không bit nào bị thay đổi vị trí hiện tại của nó.
Tùy giao thức, thứ tự bit của toàn bộ chuỗi CRC có thể bị đảo ngược, "MSB đến LSB" bị đảo thành "LSB đến MSB". Việc đảo ngược thứ tự bit của chuỗi CRC gọi là "Reflected Output". Lưu ý, việc đảo ngược thực hiện trên toàn bộ chuỗi CRC chứ không phải theo từng byte như "Reflected input". Ví dụ, nếu ta có một chuỗi kết quả tính CRC thông thường như sau:
10011010_10111100

Nếu tính CRC với cơ chế "Reflected output", thì giá trị tính toán CRC sẽ là:
00111101_01011001

Như vậy, tính CRC với "normal output" và "reflected output" sẽ cho kết quả chuỗi CRC khác nhau.
2.5) XOR ngõ ra
Trong các thiết kế ở những bài trước, kết quả tính CRC được lấy trực tiếp từ thanh ghi tính toán CRC mà không có bất kỳ sự điều chỉnh giá trị nào. Kết quả này tạm gọi là kết quả thông thường (normal result).
Tùy giao thức, kết quả thông thường có thể bị điều chỉnh bằng cách XOR với một giá trị cố định, gọi là "Final XOR hay XOR output". Ví dụ, nếu ta có một chuỗi kết quả tính CRC thông thường như sau:
10011010_10111100

Nếu tính CRC dùng cơ chế "Final XOR" với giá trị được XOR là:
11111111_11111111

thì giá trị tính toán CRC sẽ là:
01100101_0100011

Kết quả thông thường ứng với trường hợp XOR với giá trị toàn bit 0. Như vậy, hằng số XOR ngõ ra khác nhau sẽ tạo ra chuỗi bit CRC khác nhau.
3) Thứ tự áp dụng các thông số tính toán trong quá trình tính CRC
Vấn đề tiếp theo cần quan tâm là khi có nhiều thông số cùng được áp dụng vào phép tính CRC thì thông số nào sẽ được áp dụng trước, thông số nào sẽ áp dụng sau? Ví dụ, một giao thức vừa yêu cầu có "Initial Value", vừa có "Reflected input" thì dữ liệu ngõ vào sẽ được XOR với giá trị khởi tạo trước rồi mới đảo bit hay sẽ được đảo bit trước rồi mới XOR?
Sau đây là thứ tự áp dụng các thông số trong tính toán CRC:
  1. Dữ liệu đầu vào
  2. Đảo thứ tự bit trong từng byte (Reflected input)
  3. XOR với giá trị khởi tạo (Initial XOR)
  4. Tính toán CRC với chuỗi đa thức sinh
  5. Đảo thự tự bit chuỗi CRC (Reflected output)
  6. XOR với giá trị kết thúc (Final XOR)
  7. Kết quả chuỗi CRC

Hình 1: Thứ tự áp dụng các thông số tính CRC
4) Bộ tính CRC song song hỗ trợ đầy đủ các thông số tùy chọn
Trong bài 3, tác giả đã trình bày về cách thực hiện một bộ tính CRC song song toàn phần. Thiết kế nào sẽ được điều chỉnh lại hỗ trợ đầy đủ các tùy chọn tính CRC như đã trình bày.
Các port được thêm vào gồm:
  • initXorValue: Nạp giá trị cho bước "Initial XOR". Khi giá trị toàn bit 0 thì ngõ vào dữ liệu xem như không XOR với giá trị khởi tạo.
  • refInEn: Cho phép đảo thứ tự bit trong từng byte của dữ liệu ngõ vào. Tích cực mức 1. Khi ctrlEn=1refInEn=1 thì chuỗi dữ liệu ngõ vào sẽ được đảo thứ tự bit.
  • refOutEn: Cho phép đảo thứ tự bit chuỗi CRC ngõ ra. Tích cực mức 1. Khi ctrlEn=1refOutEn=1 thì chuỗi CRC sẽ được đảo thứ tự bit sau khi tính toán.
  • finalXorValue: Nạp giá trị cho bước "Final XOR". Khi giá trị toàn bit 0 thì chuỗi CRC (ngõ ra) xem như không XOR.
Ví dụ 2 - RTL code của "Reflected input"

generate
 genvar j;
 for (j = 0; j < (DWIDTH/8); j=j+1) begin
   assign refInput[j*8+0] = dataIn[j*8+7];
   assign refInput[j*8+1] = dataIn[j*8+6];
   assign refInput[j*8+2] = dataIn[j*8+5];
   assign refInput[j*8+3] = dataIn[j*8+4];
   assign refInput[j*8+4] = dataIn[j*8+3];
   assign refInput[j*8+5] = dataIn[j*8+2];
   assign refInput[j*8+6] = dataIn[j*8+1];
   assign refInput[j*8+7] = dataIn[j*8+0];
 end
endgenerate

Ví dụ 3 - RTL code của "Initial XOR" sau bước "Reflected Input"

assign xorInitInput = (refInEn? 
  refInput[DWIDTH-1:0]: dataIn) 
  ^ {initXorValue[CRC_WIDTH-1:0],
    {DWIDTH-CRC_WIDTH{1'b0}}};

refInEn được sử dụng để xác định xem bước "Reflected Input" có được áp dụng hay không. Nếu không, dữ liệu không đảo dataIn sẽ được XOR.

Ví dụ 4 - RTL code của "Reflected Output"

generate
  genvar k;
  for (k = 0; k < CRC_WIDTH; k=k+1) begin
    assign refOutput[k] = crcSeq[CRC_WIDTH-1-k];
  end
endgenerate

crcSeq là thanh ghi chứa giá trị chuỗi CRC trước khi đảo.

Ví dụ 5 - RTL code của "Final XOR"

always @ (posedge clk) begin
 if (~rstN)
  invOutEn <= 1'b0;
 else if (ctrlEn)
  invOutEn <= refOutEn;
end
assign crcOut[CRC_WIDTH-1:0] = 
  (invOutEn? refOutput: crcSeq)
  ^ finalXorValue[CRC_WIDTH-1:0];

incOutEn là bit sẽ cập nhật giá trị điều khiển từ refOutEn. Nếu incOutEn=1 thì chuỗi kết quả CRC trước khi XOR sẽ bị đảo. Nếu incOutEn=1 thì chuỗi kết quả CRC trước khi XOR sẽ lấy từ thanh ghi tính toán thông thường crcSeq.
Các bạn hãy tải code về để xem chi tiết.
5) Kết quả mô phỏng
Sau đây là một kết quả mô phỏng minh họa với chuỗi đa thức sinh được chọn là 0x1021
  • Trường hợp 1, tính CRC thông thường, không áp dụng Initial XOR, Reflected Input, Reflected Output và Final XOR
# --GenPoly:     1021 
# Initial value:     0000
# Final XOR value:     0000
# Data input: 9abcdef0 
# refInEn: 0
# refOutEn: 0
# CRC result:     fc9d
  • Trường hợp 2, tính CRC áp dụng Initial XOR khác 0
# --GenPoly:     1021 
# Initial value:     1d0f 
# Final XOR value:     0000
# Data input: 9abcdef0 
# refInEn: 0
# refOutEn: 0
# CRC result:     f28d
#
  • Trường hợp 3, tính CRC áp dụng Initial XOR và Final XOR khác 0
# --GenPoly:     1021 
# Initial value:     ffff 
# Final XOR value:     ffff 
# Data input: 9abcdef0 
# refInEn: 0
# refOutEn: 0
# CRC result:     87a2
  • Trường hợp 4, tính CRC áp dụng Initial XOR và Final XOR khác 0, Reflected input và Reflected output
# --GenPoly:     1021 
# Initial value:     ffff 
# Final XOR value:     ffff 
# Data input: 9abcdef0 
# refInEn:
# refOutEn: 1
# CRC result:     d3fa

Các bạn hãy dùng testbench đơn giản sau đây để thử:
module tbCrcParallelFull;
  parameter CRC_WIDTH = 16;
  parameter DWIDTH    = 32; //Must be n byte with n=1,2,3,4,...
  parameter TMP_WIDTH = DWIDTH*CRC_WIDTH;
  //
  //Inputs
  //
  reg clk;    //System clock
  reg rstN;   //System Reset
  reg ctrlEn; //CRC enable
  reg [DWIDTH-1:0] dataIn; //Data input
  reg [CRC_WIDTH-1:0] genPoly; //Generator Polynominal
  reg [CRC_WIDTH-1:0] initXorValue; //Initial value, set 0 if don't use
  reg refInEn;  //Reverse bit order in each input byte
  reg refOutEn; //Reverse bit order of CRC output
  reg [CRC_WIDTH-1:0] finalXorValue; //Final XOR value, set 0 if don't use
  //
  //Outputs
  //
  wire [CRC_WIDTH-1:0] crcOut;
  wire crcReady;
  //
  crcParallelFull crcParallelFull (
   clk,
   rstN,
   ctrlEn,
   dataIn,
   genPoly,
   initXorValue,
   refInEn,
   refOutEn,
   finalXorValue,
   crcOut,
   crcReady
   );
  //
  initial begin
    clk = 0;
    forever #5 clk = ~clk;
  end

  initial begin
    rstN = 1'b0;
    #20
    rstN = 1'b1;
  end

  initial begin
    ctrlEn = 1'b0;
    genPoly = 16'h1021;
    #26
    initXorValue = 16'h0000;
    finalXorValue = 16'h0000;
    dataIn = 32'h9abc_def0;
    ctrlEn = 1'b1;
    refInEn  = 1'b0;
    refOutEn = 1'b0;
    #10
    ctrlEn = 1'b0;
    refInEn  = 1'b0;
    refOutEn = 1'b0;
    #20
    initXorValue = 16'h1D0F;
    finalXorValue = 16'h0000;
    dataIn = 32'h9abc_def0;
    ctrlEn = 1'b1;
    refInEn  = 1'b0;
    refOutEn = 1'b0;
    #10
    ctrlEn = 1'b0;
    refInEn  = 1'b0;
    refOutEn = 1'b0;
    #20
    initXorValue = 16'hffff;
    finalXorValue = 16'hffff;
    dataIn = 32'h9abc_def0;
    ctrlEn = 1'b1;
    refInEn  = 1'b0;
    refOutEn = 1'b0;
    #10
    ctrlEn = 1'b0;
    refInEn  = 1'b0;
    refOutEn = 1'b0;
    #20
    initXorValue = 16'hffff;
    finalXorValue = 16'hffff;
    dataIn = 32'h9abc_def0;
    ctrlEn = 1'b1;
    refInEn  = 1'b1;
    refOutEn = 1'b1;
    #10
    ctrlEn = 1'b0;
    refInEn  = 1'b0;
    refOutEn = 1'b0;
    #200
    $stop;
  end
  reg crcReadyReg;
  wire risingCrcReady;
  reg refInEnReg, refOutEnReg;
  always @ (posedge clk) begin
    if (ctrlEn) begin
      refInEnReg <= refInEn;
      refOutEnReg <= refOutEn;
    end
  end
  always @ (posedge clk, negedge rstN) begin
    if (~rstN) crcReadyReg <= 1;
    else crcReadyReg <= crcReady;
  end
  assign risingCrcReady = ~crcReadyReg & crcReady;
  always @ (posedge clk) begin
   if (rstN & risingCrcReady)
    $display ("--GenPoly: %8h \nInitial value: %8h \nFinal XOR value: %8h \nData input: %8h \nrefInEn: %b \nrefOutEn: %b\nCRC result: %8h\n",
    genPoly, initXorValue, finalXorValue, dataIn, refInEnReg, refOutEnReg, crcOut);
  end
endmodule
Tác giả chọn các phép thử trên để bạn đọc có thể so sánh trên website tính CRC online sau:
https://crccalc.com/

Trong đó:
  • Trường hợp 1 là CRC-16/XMODEM
  • Trường hợp 2 là CRC-16/AUG-CCITT
  • Trường hợp 3 là CRC-16/GENIBUS
  • Trường hợp 4 là CRC-16/X-25
Như vậy, việc tính CRC phụ thuộc vào nhiều thông số cấu hình khác nhau. Mỗi giao thức (chuẩn) sẽ quy định cụ thể các thông số này.

Dữ liệu có thể tải:
Source code trên Github

Lịch sử cập nhật:
1) 2019.Sep.12 - Tạo lần đầu

Thứ Ba, 3 tháng 9, 2019

[CRC] Bài 4 - Bộ tính CRC song song từng phần

Bài viết này trình bày một cách thiết kế bộ tính CRC song song nhưng có chút khác biệt so với bài bài 3. Để giảm số chu kỳ tính toán CRC nhưng không tạo ra mạch tổ hợp quá lớn, chuỗi dữ liệu ngõ vào sẽ được chia nhỏ thành từng đoạn để tính CRC. Cách tính này tạm gọi là tính CRC song song từng phần.
1) Nguyên lý tính CRC từng phần
Việc tính CRC nối tiếp như bài 1 hoặc bài 2 làm số chu kỳ tính toán cho một chuỗi dữ liệu lớn. Việc tính CRC song song toàn phần, toàn bộ chuỗi dữ liệu được đưa qua mạch tính CRC và cho kết quả CRC trong một lần tính, làm cho mạch tổ hợp tính CRC lớn (bài 3).
Một phương pháp giúp cân bằng giữa số chu kỳ tính CRC và độ lớn của mạch tổ hợp tính CRC là tính song song từng phần. Với cách tính này, chuỗi dữ liệu ngõ vào sẽ được chia nhỏ thành từng phần với một độ dài xác định. Mỗi phần này sẽ được đưa vào mạch tính CRC song song.
Nguyên lý tính CRC song song từng phần được thực hiện như sau:
  1. Chia chuỗi dữ liệu ngõ vào thành từng nhóm có số bit bằng với số bit của chuỗi CRC
  2. Thực hiện XOR một nhóm bit của chuỗi dữ liệu đầu vào với giá trị chuỗi CRC hiện tại. Với lần XOR đầu tiên, giá trị chuỗi CRC hiện tại là giá trị khởi tạo ban đầu. Trong bài viết này, giá trị này bằng 0.
  3. Kết quả phép XOR ở bước 2 được đưa đến mạch tính CRC song song. Kết quả của bước này sẽ được lưu lại để sử dụng cho lần tính tiếp theo. Lần tính tiếp theo là lần tính cho nhóm bit kế tiếp và lặp lại từ bước 2.
Hình 1: Minh họa nguyên lý tính CRC từng phần
2) Phân tích thiết kế
Nguyên lý thiết kế như đã trình bày sẽ được cụ thể hóa trong hình minh họa sau đây với số bit dữ liệu đầu vào là 32, DWIDTH=32, và số bit CRC là 8, CRC_WIDTH=8.
Hình 2: Sơ đồ nguyên lý mạch tính CRC từng phần với trường hợp DWIDTH=32 và CRC_WIDTD=8
Trong sơ đồ trên, các bước tính toán CRC như sau:

  • (1) Store input data: Chuỗi dữ liệu đầu vào được lưu vào một thanh ghi trước khi bắt đầu tính CRC, trong ví dụ này, đây là thanh ghi 32 bit. Nó là một thanh ghi dịch. Thanh ghi sẽ dịch CRC_WIDTH bit sau mỗi chu kỳ xung clock. Trong ví dụ này, nó dịch 8 bit.
  • (2) Create input: Mỗi nhóm 8 bit sẽ được XOR với giá trị CRC hiện tại lấy từ thanh ghi crcSeq[CRC_WIDTH-1:0]. Sau đó ráp với một chuỗi giá trị khởi tạo bằng số bit chuỗi CRC, trong ví dụ này nó là 8 bit 0. Phần logic này là mạch tổ hợp.
  • (3) Parallel calculation: Là một mạch tính CRC cho nhóm 8 bit dữ liệu và 8 bit 0. Phần logic này là mạch tổ hợp.
  • (4) Latch CRC result: Phần logic lưu lại giá trị CRC sau mỗi lần tính cho từng nhóm 8 bit.
Trong ví dụ này, dữ liệu đầu vào là 32 bit, việc tính toán sẽ lặp trong 4 chu kỳ, mỗi chu kỳ tính toán trên 8 bit dữ liệu. Kết quả sẽ có ở thanh ghi CRC, crcSeq, ở chu kỳ thứ 5.

Các bạn hãy chú ý một số điểm khác biệt so với mạch nguyên lý  tính CRC song song toàn phần ở bài 3 như sau:

  • Thanh ghi lưu dữ liệu ngõ vào là một thanh ghi dịch chứ không phải chỉ là thanh ghi lưu dữ liệu vì nó cần dịch tới nhóm bit tiếp theo cho mỗi lần tính
  • Trước từng nhóm bit được ráp thành chuỗi đầu vào cho mạch tính song song, nó được XOR với thanh ghi chứa giá trị CRC hiện tại
  • Logic thanh ghi chốt giá trị CRC, crcSeq, có thêm một MUX điều khiển bởi ctrlEn để xóa thanh ghi này về 0 cho mỗi lần tính CRC mới.
Hình 3: Thanh ghi dịch lưu dữ liệu ngõ vào
Thanh ghi dịch dataInReg lấy dữ liệu ngõ vào mới khi ctrlEn=1 và dịch trái CRC_WIDTH bit khi tính toán, ứng với crcReady=0.
Hình 4: Bit báo trạng thái crcReady và bộ đếm tính CRC
Bộ đếm shiftCounter sẽ giám sát số chu kỳ tính CRC, bộ đến có số bit COUNTERW bằng log2 của độ rộng bit dữ liệu chia cho độ rộng chuỗi CRC. Số chu kỳ tính toán CRC CALNUM bằng độ rộng bit dữ liệu chia cho độ rộng chuỗi CRC. Nếu số bit dữ liệu là 32, chuỗi CRC là 8 bit thì số chu kỳ tính toán là 32/8=4 và độ rộng bộ đếm là log2(4)=2.
Bit trạng thái crcReady sẽ luôn bằng 1 khi bộ tính CRC sẵn sàng. Khi ctrlEn=1, quá trình tính toán bắt đầu, crcReady bị xóa về 0. Nó được thiết lập lại mức 1 khi giá trị bộ đếm giá sát chu kỳ tính CRC đã đếm đủ số chu kỳ cần tính, setReady=1.
3) Kết quả mô phỏng
Một kết quả mô phỏng cho bộ tính CRC từng phần thể hiện sau đây:

  • DWIDTH=32
  • CRC_WIDTH=8
  • Đa thức sinh sử dụng là x^8+x^2+x^1+1 ứng với giá trị GenPoly=8'h07 trong biểu diễn đa thức sinh thông thường.


Hình 5: Một waveform của bộ tính CRC từng phần với cấu hình chuỗi CRC là 8 bit và dữ liệu đầu vào là 32 bit
Lưu ý, bộ tính CRC này chỉ hỗ trợ việc tính toán CRC với số bit dữ liệu cần tính chia hết cho số bit CRC, DWIDTH/CRC_WIDTH là một số nguyên dương.
Một cách khác để thực hiện bộ tính CRC từng phần là thay thế logic mạch tính CRC song song bằng một RAM/ROM chứa sẵn giá trị CRC của tất cả các nhóm bit. Trong ví dụ này, nhóm bit có độ dài là 8 thì cần một bộ nhớ chứa 2^8=256 giá trị. Mỗi lần tính, 8 bit dữ liệu sau cổng XOR ở bước tính (2) sẽ là địa chỉ để chọn ô chứa giá trị CRC tương ứng từ bộ nhớ RAM/ROM. Cách làm này giống như việc tra bảng sự thật (lookup table). Để làm được cách này, tất cả các giá trị có thể từ 8'h00 đến 8'hff phải được tính toán trước và lưu trong RAM/ROM trước khi bộ tính CRC hoạt động.
Hình 6: Một cách thực hiện bộ tính CRC song song từng phần khác, trường hợp CRC_WIDTH=8

Web tính CRC onlline dùng để kiểm chứng kết quả:
https://crccalc.com/

Dữ liệu có thể download:
Source code trên Github

Lịch sử cập nhật:
1) 2019.09.03 - Tạo lần đầu

Chủ Nhật, 1 tháng 9, 2019

[Perl] Làm thế nào để tạo ra một định dạng script chuyên nghiệp

Script là một phần không thể thiếu trong công việc hằng ngày của một kỹ sư làm việc trong lĩnh vực vi mạch. Script xuất hiện trong mọi công đoạn thiết kế từ front-end đến back-end. Script là công cụ mạnh giúp rút ngắn thời gian thực hiện một công việc. Bài viết này trình bày một phương pháp và cách thức tham khảo để có thể xây dựng một định dạng script chuyên ngiệp. Ngôn ngữ được sử dụng trong bài viết này là PERL. Thông qua ví dụ minh họa, bạn có thể biết thêm một số thành phần Perl hay được dùng thường xuyên như đọc thư mục, đọc file, tạo file, ghi file, subrountine, kiểm tra mẫu dữ liệu trong một chuỗi ký tự, các biến hệ thống, ...
Lưu ý, phương pháp được trình bày trong bài viết này chỉ là một tham khảo giúp các bạn mới bắt đầu có thể hiểu, lựa chọn và xây dựng cách thức của riêng mình.
1) Script là gì?
Trước hết, chúng ta đi qua một số khái niệm cơ bản để hiểu về mục đích, ý nghĩa và cách sử dụng script.
Script là một chương trình tập hợp các lệnh (instruction hoặc command) sẽ được "dịch" và "thực thi" thông qua một phần mềm hay chương trình khác. Nội dung của script sẽ được "dịch" và "thực thi" từng dòng, nghĩa là được "thông dịch" (interpreted), chứ không phải biên dịch (compiled).
"Thông dịch" nghĩa là từng dòng code sẽ được một chương trình hoặc phần mềm khác "dịch" và "thực thi" tuần tự. Khác với "biên dịch", tất cả các code được chuyển từ ngôn ngữ lập trình thành một file thực thi, file .exe. File thực thi này là một chương trình độc lập được dùng để chạy các tác vụ mong muốn.
Ngôn ngữ dùng để tạo ra script gọi là ngôn ngữ script, ví dụ như Shell, Perl, Ruby, Python, JavaScript, .... Ngôn ngữ dùng để tạo ra chương trình độc lập gọi là ngôn ngữ lập trình, ví dụ như C/C++, Visual Basic, ...
Phần mềm (chương trình) dùng để dịch và thực thi script gọi là trình thông dịch (interpreter) như trình thông dịch C-shell, trình thông dịch Perl, ... Ví dụ, để thực thi một Perl script tên perl_ex.pl, chúng ta cần chỉ rõ trình thông dịch sẽ được sử dụng để dịch và thực thi script này bằng cách gọi trình thông dịch trên terminal:
perl perl_ex.pl
hoặc khai báo đường dẫn của trình thông dịch ở đầu file trong Perl script, ví dụ như:
#!/usr/bin/perl
Sau đó, gọi và thực thi trực tiếp Perl script trên terminal:
./perl_ex.pl
Trong cả hai cách trên thì trình thông dịch Perl sẽ được gọi để dịch và thực thi code trong file perl_ex.pl.

Phần mềm (chương trình) dùng để chuyển ngôn ngữ lập trình thành ngôn ngữ máy gọi là trình biên dịch (compiler), ví dụ như gcc/g++ là một trình biên dịch C/C++, ...
Hình 1: Sự khác nhau giữa quá trình thực thi một script file và program file
Script sinh ra để điều khiển, gọi và thực thi các thành phần có sẵn. Thành phần có sẵn ở đây chính là các chương trình (phần mềm) đã được cài đặt trên thiết bị. Script giúp kết nối các chương trình độc lập với nhau và thực thi chúng theo một trình tự, điều kiện nhất định do người tạo script lựa chọn. Từ đó, script giúp giảm bớt thời gian thực hiện các thao tác thủ công, tối ưu hóa công việc.
Ví dụ, bạn cần thay thế một cụm từ ABCD thành XYZ trong 100 file. Nếu làm thủ công, bạn cần thực hiện 100 lần các thao tác sau:
  • Mở file bằng một editor như Notepad
  • Dùng chức năng thay thế của editor để chuyển ABCD thành XYZ hoặc tệ hơn là thay thế từng cụm từ trong file bằng cách đánh máy
  • Lưu lại file
  • Đóng file
Tất cả các thao tác trên có thể thay bằng 1 script. Bạn chỉ cần chạy script để nó tự động thực hiện các thao tác thay thế ABCD thành XYZ trên 100 file.
2) Thế nào là một định dạng script chuyên nghiệp?
Một script chuyên nghiệp đương nhiên phải thực hiện được các tác vụ (chức năng) được mong muốn một cách chính xác, nhanh nhất có thể và chiếm ít tài nguyên hệ thống. Điều này tùy vào khả năng và trình độ của mỗi người. Trong hiểu biết hạn hẹp của mình, tác giả không đề cập đến vấn đề này.
Vậy ngoài vấn đề trên, một script có cấu trúc chuyên nghiệp cần có những thông tin phụ kèm theo, càng chi tiết càng tốt, để giúp người dùng script có thể dễ dàng hiểu và sử dụng script. Đồng thời, ngoài kết quả chính, script còn có thể tạo ra các kết quả phụ giúp người dùng script hoặc dùng kết quả của script có thể dễ dàng trace* hoặc debug**.
*trace - tra cứu và tìm hiểu nguồn gốc của một vấn đề, hiện tượng.
**debug - Giải thích nguyên nhân sinh ra một vấn đề, hiện tượng.
Bạn có thể đã từng gặp một số vấn đề sau đây khi sử dụng một script do người khác viết:
  1. Không biết rõ script hỗ trợ các tùy chọn (option) nào
  2. Các thông điệp trong quá trình chạy như cảnh báo lỗi, hiển thị trạng thái, ... của script không lưu vào log file để có thể trace khi cần
  3. Xem một kết quả do script tạo nhưng không thể biết ai tạo? tạo lúc nào? thư mục chứa source ở đâu?
  4. Script thiếu những comment chỉ dẫn tối thiểu để có thể đọc hiểu và chỉnh sửa khi cần
  5. Không thể biết một kết quả ngõ ra được tạo từ script nào khi dùng một tool là tổ hợp của nhiều file script khác nhau.
Như vậy, một script chuyên nghiệp ngoài vấn đề "chuyên môn" là đảm bảo đúng chức năng thì cần có thêm "tính minh bạch". Sự minh bạch thể hiện ở việc script có thể dễ dàng được sử dụng, đọc hiểu và kiểm tra kết quả bởi người sử dụng khác.
3) Giới thiệu một cấu trúc Perl script chuyên nghiệp
Trong phần này tác giả trình bày về một định dạng (format), có thể hiểu là cấu trúc, của script mà theo tác giả là sẽ làm cho một script trở nên chuyên nghiệp hơn. Cấu trúc này gồm:
  1. Header của script: Chứa những thông tin cơ bản liên quan đến script như chức năng chính của script, tác giả, lịch sử cập nhật, ... Đây là các thông tin tổng quan ban đầu giúp người dùng nhanh chóng hiểu chức năng chính của script và các lưu ý quan trọng nếu có.
  2. Khởi tạo một log file: Tạo một log file để ghi lại quá trình hoạt động của script và những thông điệp hỗ trợ người sử dụng có thể trace hoặc debug
  3. Tạo biến chứa tên của script hiện tại với mục đích:
    • Gọi lại chính script này để thực thi nếu cần
    • In thông điệp hỗ trợ việc trace và debug
  4. Kiểm tra đối số của script như kiểm tra số lượng đối số và tính hợp lệ khác nếu cần. Ở bước này, nếu đối số của script không thỏa mãn một vài tiêu chí cơ bản thì script sẽ ngừng thực thi và thông báo đến người dùng.
  5. Tạo các biến chứa thông tin chung như tên người dùng script, thời điểm chạy script, đường dẫn thư mục chạy script, ... với mục đích hỗ trợ việc trace và debug
  6. Tạo header chứa các thông tin sẽ in trên terminal trong quá trình chạy hoặc in trong log file hoặc in trong file kết quả để hỗ trợ việc trace và debug
  7. Khai báo tất cả các biến toàn cục dùng trong suốt quá trình chạy script. Các biến cục bộ hoặc biến tạm chỉ dùng trong một đoạn xử lý cụ thẻ của script thì chỉ nên khai báo trong đoạn xử lý đó. Điều này giúp giảm tài nguyên phần cứng khi chạy script.
  8. So sánh các đối số để gán giá trị phù hợp cho các biến toàn cục khai báo ở bước 7. Giá trị các biến này sẽ điều khiển việc thực thi chức năng chính của script ở các bước sau.
  9. Code thực thi các chức năng chính
  10. Đóng log file được khởi tạo ở bước 2. sSau khi khởi tạo ở bước 2, log file có thể được ghi những thông tin do người tạo script quyết định. Đến bước này, sau khi script đã thực thi xong toàn bộ các chức năng chính. Log file phải được đóng lại. Chú ý, việc đóng log file luôn nằm sau bước 9 để các bước từ 3 đến 9 có thể ghi vào log file khi cần.
  11. Code của tất cả các chương trình con (subrountine) sử dụng ở các bước trên. Các subrountine là các chương trình con thực thi một chức năng nào đó, thường lặp lại nhiều lần, trong script và thường đặt cuối các script. Các phần code khác của script sẽ gọi subroutine để yêu cầu nó thực thi tại thời điểm thích hợp với đối số thích hợp nếu có.
Trong đó, đối số của script là các giá trị tùy chọn (option) được script hỗ trợ. Các giá trị này sẽ được khai báo trên dòng lệnh khi chạy script. Ví dụ như:
./hello_perl.pl -f inFile.txt
Trong ví dụ trên, script hello_perl.pl nhận 2 đối số là "-f" và "inFile.txt".
Hình 2: Cấu trúc một script Perl "chuyên nghiệp"
Để minh họa các thành phần cấu trúc của script, chúng ta hãy thực hiện một ví dụ "Viết một script đọc các file RTL code để lấy danh sách các port (input, output và inout). In danh sách này ra một file và sắp xếp theo thứ tự input->output->inout.". Script này sẽ có tên là getPort.pl với 11 thành phần đã liệt kê trên đây được thể hiện như sau:
Phần 1: Header của script
#(1) Header of Script
#----------------------------------
#Author :  Nguyen Hung Quan
#Website:  http://nguyenquanicd.blogspot.com/
#Function: List all input, output and inout of a module
#Note: Do NOT support RTL file with comment block /* */
#Reversion and History:
# v0.0 - Create firstly
#----------------------------------

Header chỉ là các comment.

Phần 2: Khởi tạo log file
my $myLog = "logGetPort.log";
system "/usr/bin/rm -f logGetPort.log";
open (LOGFILE, ">$myLog") or die because $!;

Biến $myLog chứa tên của log file sẽ được tạo ra là logGetPort.log. Hàm system() được sử dụng để gọi lệnh remove (rm) của hệ thống để xóa file logGetPort.log nếu nó đã được tạo trước đó. Bạn có thể viết như sau:
system "rm -f logGetPort.log";
Không cần chỉ rõ đường dẫn đến file thực thi (.exe) của lệnh này nếu đường dẫn của nó đã có trong biến môi trường $PATH của hệ điều hành. Khi đó, bạn có thể thực thi lệnh trên terminal:
rm -f logGetPort.log
Bạn có thể kiểm tra giá trị biến $PATH bằng lệnh:
echo $PATH
Logic được khởi tạo bằng lệnh open, trong đó:
  • LOGFILE là từ khóa do người dùng đặt sẽ lưu handle của file được tạo ra. Một file sẽ được ghi và đọc thông qua handle này.
  • $myLog là  biến chứa tên file sẽ tạo
  • Dấu ">" chỉ ra đây là file sẽ được tạo mới. Nếu không có ">" thì hàm open sẽ hiểu là "mở" một file đã tồn tại.
Nếu file không thể tạo được vì lý do nào đó, "or die", thông điệp lỗi sẽ có trong biến hệ thống $! sẽ được in ra terminal.

Phần 3: Tạo biến chứa tên của script
my $myScript   = $0;
$myScript =~ s/\.\///;

$0 là biến hệ thống chứa tên chương trình đang chạy. Khi script này được thực thi thì $0 chứa tên của chính script này. Tuy nhiên giá trị đó sẽ là "./getPort.pl" nên dòng lệnh thứ 2 sẽ remove "./" để lấy đúng tên script. Chú ý, toán tử "=~" là toán tử kiểm tra các mẫu ký tự (pattern-matching operator). Ở đây, $myScript sẽ được kiểm tra mẫu ký tự "./" và thay thế bằng một ký tự trống, nghìa là xóa "./" bằng toán tử thay thế (subsituation operator).
s/<mẫu bị thay thế>/<mẫu thay thế>/  

Phần 4: Kiểm tra đối số của script
my $argNum  = @ARGV;
if ($argNum == 0) {
  printLog (LOGFILE, "[ERROR] Missing options 
    - please fill your options\n");
  system "./$myScript -help";
  exit;
}

@ARGV là biến mảng của hệ thống chứa các đối số gán cho một chương trình thực thi trên terminal (command line). Ví dụ:
/hello_perl.pl -f inFile.txt
@ARGV sẽ chứa 2 đối số là "-f" và "inFile.txt". Gán một mảng đến một biến vô hướng như $argNum thì biến vô hướng $argNum sẽ chứa số lượng đối số của mảng. Như ví dụ này, $argNum sẽ bằng 2.
Đoạn code if sẽ kiểm tra số lượng đối số có bằng 0. Nếu bằng 0 thì nó sẽ gọi lại chính script này đê thực thi với tùy chọn "-help" với mục đích in ra thông tin các option của script cho người dùng lựa chọn và thoát script với lệnh exit.

Phần 5: Biến chứa thông tin chung
#Account name
my $wName = getlogin();
#Generation time
my $wTime = localtime();
#Working directory
my $wDir  = $ENV{PWD};

Như các comment trong code:
  • $wName sẽ chứa tên người chạy script (user ID) bằng cách dùng function getlogin().
  • $wTime sẽ chứa thời điểm chạy script bằng cách dùng function localtime()
  • $wDir sẽ chứa đường dẫn thư mục mà script đang được thực thi lấy từ biến môi trường %ENV

Phần 6: Tạo header cho output
my $headerFile;
$headerFile .= "#----------------------------------\n";
$headerFile .= "#Author              : $wName\n";
$headerFile .= "#Date                : $wTime\n";
$headerFile .= "#Working directory   : $wDir\n";
$headerFile .= "#Number of arguments : $argNum\n";
$headerFile .= "#Submitted command   : $myScript @ARGV\n";
$headerFile .= "#----------------------------------\n";
printLog (LOGFILE, $headerFile);

Biến $headerFile sẽ lưu các thông tin như người dùng script, thời gian sử dụng, thư mục làm việc, đối số và lệnh đã được sử dụng. Chú ý, ".=" là toán tử nối chuỗi và gán. Nó nối chuỗi bên phải dấu ".=" vào chuỗi đang chứa trong $headerFile. Sau đó, gán chuỗi đã nối vào $headerFile.
$headerFile sẽ được ghi vào log file bằng một subrountine tên printLog(). Subrountine này được định nghĩa trong phần 11.

Phần 7: Biến toàn cục
my $rmComment = 0;
my $mode = "file";
my $myRtl;
my $myDir;
my $outLine;
my $outFile = "outputFile.txt";

$rmComment là biến điều khiển việc có xóa comment trong dòng code khai báo port đi hay không, bằng 1 là "có xóa".
$mode là biến chỉ ra chế độ mà script đang thực thi là cho 1 file code duy nhất hay cho một thư mục chứa nhiều file code. Nếu script thực thi cho 1 thư mục, biến này sẽ được gán lại thành "dir".
$myRtl chứa tên file RTL code là đầu vào của script
$myDir chứa tên thư mục RTL code, gồm nhiều file RTL code, là đầu vào của script
$outLine là biến chứa thông tin sẽ được in vào file kết quả
$outFile là biến chứa tên file kết quả sẽ được tạo ra. File này sẽ chứa nội dụng lưu trong biến $outLine.

Phần 8: Quét và kiểm tra đối số
while ($argNum != 0) {
  my $arg = shift(@ARGV); #Get an argument
  switch($arg) {
    #Get RTL file
    case "-f" {
      $myRtl = shift(@ARGV);
    }
    #Get RTL directory
    case "-d" {
      $myDir = shift(@ARGV);
      $mode = "dir";
    }
    #Set option "remove comment"
    case "-rc" {
      $rmComment = 1;
    }
    case "-help" {
      printLog (LOGFILE, "#----------------------------------\n");
      printLog (LOGFILE, "Format: $myScript 
        < option 0="" > < value 0="" > ... <
        option n="" > < value n="" >\n");
      printLog (LOGFILE, "  Option:\n");
      printLog (LOGFILE, "  -f    : Declare an RTL file (.sv .v).\n
          Example \"-f test.v\"\n");
      printLog (LOGFILE, "  -d    : Declare an RTL directory 
          which contains RTL files (only .sv or .v).\n
          Example \"-d working/rtlIn\"\n");
      printLog (LOGFILE, "  -rc   : Remove comments.
          Default is \"no-remove\".\n
          Example \"-rc\"\n");
      printLog (LOGFILE, "  -help : Show all options.\n");
      printLog (LOGFILE, "#----------------------------------\n");
      exit;
    }
    else {
      printLog (LOGFILE, "[ERROR]Do NOT support the option: $arg\n");
      system "./$myScript -help";
      exit;
    }
  }
  $argNum  = @ARGV; #Update number of arguments
}

switch được sử dụng để kiểm tra các giá trị tùy chọn (đối số) được cấp cho script khi chạy. Tùy vào mỗi case mà các biến tương ứng sẽ được gán phù hợp. Script sẽ ngừng thực thi tiếp (exit) nếu đối số không thuộc các tùy chọn đã cho (nhánh else) hoặc tùy chọn có chứa "-help".
Hình 3: Flowchart của đoạn code "quét và kiểm tra các đối số"
getPort.pl hỗ trợ các tùy chọn sau:
  • -f : Khai báo tên file RTL
  • -d : Khai báo thư mục chứa nhiều file RTL (không dùng chung với -f)
  • -rc : Xóa các comment (nếu có) ở các dòng code khai báo port
  • -help: In đầy đủ các option của script
Với cách làm này, chúng ta dễ dàng thiết lập các giá trị điều khiển các chức năng chính của script. Việc hỗ trợ "-help" sẽ giúp người dùng script dễ dàng tra cứu các tùy chọn của script để sử dụng.
Để sử dụng được switch, bạn cần cài đặt gói hỗ trợ sau:

Sau đó khai báo "use Switch;" ở đầu script và sau khai báo phần mềm Perl. Cách cài đặt một package Perl, các bạn có thể tham khảo ở bài viết này.
switch được đặt trong một vòng lặp while. Vòng lặp này được thực thi nếu số lượng đối số chứa trong @ARGV khác 0. Với mỗi lần thực thi, đầu tiên, mảng @ARGV sẽ được dịch để lấy đối số gán cho biến $arg. tùy vào giá trị của $arg mà giá trị tiếp theo của mảng @ARGV sẽ được lấy tiếp bằng function shift và gán vào biến tương ứng. Ví dụ như, nếu $arg bằng "-f" thì giá trị kế tiếp được shift từ @ARGV là tên file RTL code sẽ được gán cho biến $myRtl.
Sau mỗi lần kiểm tra đối số, số lượng giá trị của mảng @ARGV sẽ bị giảm dần và được gán lại cho biến $argNum sau mỗi lần lặp. Khi $argNum=0, nghĩa là tất cả các đối số đã được kiểm tra và sử dụng, vòng lặp while sẽ bị thoát.

Phần 9: Chức năng chính của script
if ($mode eq "dir") {
  printLog(LOGFILE, "--- START reading RTL directory: $myDir\n");
  #Get all file names and store to an array
  opendir (DIR, $myDir) or die because $!;
  my @fileList = readdir(DIR);
  closedir (DIR);
  #Scan all RTL files
  my $fileCount = 0;
  foreach $myRtl (@fileList) {
    if (($myRtl =~ /.sv$/) || ($myRtl =~ /.v$/)) {
      $fileCount++;
      #Return a unused signal array
      printLog(LOGFILE, "+ RTL file $fileCount    : $myRtl\n");
      $myRtl = "$myDir/$myRtl";
      $lineOut .= "$fileCount) $myRtl\n";
      $lineOut .= getPort($myRtl, $rmComment);
      $lineOut .= "\n";
    }
    printLog (LOGFILE, "\n");
  }
  printLog(LOGFILE, "--- END reading RTL directory: $myDir\n\n");
}
else {
  printLog(LOGFILE, "--- START reading RTL file: $myRtl\n");
  $lineOut .= "  $myRtl\n";
  $lineOut = getPort($myRtl, $rmComment);
  printLog(LOGFILE, "--- END reading RTL file: $myRtl\n\n");
}

open (OFILE, " > $outFile") or die because $!;
print OFILE $lineOut;
close(OFILE);

Phần chức năng chính của chương trình gồm 2 đoạn code:

  • Đoạn if/else đọc thư mục chứa RTL hoặc một file RTL để lấy danh sách port lưu vào biến $lineOut
  • Đoạn in biến $lineOut vào file outputFile.txt thông quan handle OFILE.

Phần code này có sử dụng một subrountine là getPort(). Nó sẽ dùng 2 đối số đầu vào là một đường dẫn file RTL $myRtl và biến xác định xem comment có được xóa hay không $rmComment.
Hình 4: Flowchart phần chức năng chính của script
Các bạn hãy so sánh flowchart trên và code để hiểu rõ hơn.

Phần 10: Đóng log file
close (LOGFILE);

Sau khi quá trình thực thi script hoàn tất, log file đã tạo ở phần 2 và được ghi nội dung trong các phần từ 3 đến 9 thông qua subrountine printLog() sẽ được đóng lại.

Phần 11: Định nghĩa Subrountine
#
#Print to a log file
#
sub printLog {
  my $fileHandle = shift;
  my $msg        = shift;
  #
  print "$msg";
  print $fileHandle $msg;
} #printLog
#
#Read a RTL file and return a unused signal array
#
sub getPort {
  my $fileIn = shift;
  my $opIn   = shift;
  my $portIn;
  my $portOut;
  my $portInout;
  my $portList;
  #
  open (RTL, $fileIn) or die because $!;
  foreach my $line (< RTL >) {
    chomp($line);
    #Pre-operate
    #Remove START spaces
    $line =~ s/^\s+//;
    #Replace tab or many spaces to one space
$line =~ s/\s+/ /g;
    #Remove END space
    $line =~ s/\s+$//;
    #Post-operate
    #Do NOT care the comment line
    if ($line !~ /^\/\//) {
      #Remove comment at END of a line
      if (($opIn == 1) && ($line =~ /\/\//)) {
        my @wordArray = split(/\/\//, $line);
        $line = $wordArray[0];
      }
      #
      if ($line =~ /^input /) { 
        $portIn .= "$line\n";
      }
      elsif ($line =~ /^output /) {
        $portOut .= "$line\n";
      }
      elsif ($line =~ /^inout /)  {
        $portInout .= "$line\n";
      }
    }
  }
  close (RTL);
  #
  if (defined($portIn)) {
    $portList = "$portIn";
  }
  if (defined($portOut)) {
    $portList .= "$portOut";
  }
  if (defined($portInout)) {
    $portList .= "$portInout";
  }
  #
  $portList; #This is the returned value
} #getPort

Subrountine printLog sẽ in thông điệp mà bạn muốn ra terminal và vào log file.
Hình 5: Flowchart của subrountine printLog()
Subrountine getPort sẽ đọc một RTL file và trích xuất danh sách khai báo port của module. Sau khi sắp xếp chúng theo thứ tự input->output->inout, danh sách khai báo này được trả về cho chương trình cha thông qua $portList..
Hình 6: Flowchart rút gọn của getPort()
4) Một số kết quả chạy trên cygwin terminal
4.1) Kết quả chạy lệnh "./getPort.pl -help"

#----------------------------------
#Author              : 8560w
#Date                : Mon Sep  2 12:02:14 2019
#Working directory   : /cygdrive/d/20.Project/7.Perl
#Number of arguments : 1
#Submitted command   : getPort.pl -help
#----------------------------------
#----------------------------------
Format: getPort.pl < option 0 > < value 0 > 
        ... < option N > < value N >
  Option:
  -f    : Declare an RTL file (only .sv or .v).
          Example "-f test.v"
  -d    : Declare an RTL directory which contains RTL files
          (only .sv or .v).
          Example "-d working/rtlIn"
  -rc   : Remove comments. Default is "no-remove".
          Example "-rc"
  -help : Show all options.
#----------------------------------

4.2) Kết quả chạy lệnh "./getPort.pl -d rtl"
Hiển thị trên terminal và trong log file:

#----------------------------------
#Author              : 8560w
#Date                : Mon Sep  2 12:03:46 2019
#Working directory   : /cygdrive/d/20.Project/7.Perl
#Number of arguments : 2
#Submitted command   : getPort.pl -d rtl
#----------------------------------
--- START reading RTL directory: rtl


+ RTL file 1    : uart_apb_if.v

+ RTL file 2    : uart_receiver.v

+ RTL file 3    : uart_top.v

+ RTL file 4    : uart_transmitter.v

--- END reading RTL directory: rtl

Hiển thị trong output file outputFile.txt là danh sách port của tất cả các file RTL trong thư mục rtl là uart_apb_if.v, uart_receiver.v, uart_top.v và uart_transmitter.v.

Nhận xét, scritp này hỗ trợ các đặc điểm sau giúp việc sử dụng script, trace và debug dễ dàng hơn:
  • Kiểm tra đối số, cảnh báo người dùng script nếu có sai sót
  • Hỗ trợ tùy chọn "-help" cho phép người dùng tra cứu tùy chọn của script
  • Tạo log file
  • In ra thông tin tên người dùng, thời gian, vị trí và command mà người dùng dử dụng
  • Đánh dấu và chú thích chức năng từng đoạn code giúp người dùng script có thể đọc hiểu và chỉnh sửa nếu cần
Dữ liệu có thể tải:

Lịch sử cập nhật:
1) 2019.Sep.02 - Tạo lần đầu

Thứ Ba, 27 tháng 8, 2019

[CRC] Bài 3 - Bộ tính CRC song song toàn phần

Bài viết này trình bày về cách thiết kế bộ tính CRC song song toàn phần. Với cách thiết kế này, giá trị CRC có thể có ngay lập tức trong cùng chu kỳ với ngõ vào hoặc chỉ delay một vài chu kỳ để giúp timing đường dữ liệu của bộ tính CRC tốt hơn. 
1) Phân tích vấn đề và mô tả thiết kế
Bài 1bài 2 trình bày về hai thiết kế CRC nối tiếp. Thiết kế CRC nối tiếp có ưu điểm là tài nguyên ít và mạch tổ hợp giữa các Flip-Flop không lớn. Tuy nhiên, vấn đề lớn của việc tính CRC nối tiếp chính là tốc độ tính toán càng chậm khi số bit dữ liệu càng dài.
Để giải quyết vấn đề trên, một cách thiết kế khác là tính CRC song song. Nguyên tắc cơ bản của việc tính CRC song song là xếp chồng nhiều mạch tổ hợp tính CRC lên nhau. Ngõ ra của mạch tổ hợp thứ nhất sẽ là ngõ vào của mạch tổ hợp thứ hai và cứ tiếp tục như vậy cho đến khi giá trị CRC được tính hoàn chỉnh.
Điểm khác biệt chính giữa tính CRC nối tiếp và CRC song song là ở số lượng mạch tổ hợp tính toán CRC. Quay lại thiết kế ở bài 2, việc tính nối tiếp dùng một thanh ghi dịch để lưu dữ liệu trong suốt quá trình tính toán. Tại mỗi chu kỳ giá trị thanh ghi dịch được đưa qua một mạch tính cố định. Mạch này dựa trên bit MSB của thanh ghi dịch để tính giá trị kế tiếp nạp vào thanh ghi dịch. Cách thiết kế này chỉ cần 1 mạch tính CRC.
Hình 1: Mạch nguyên lý tính CRC nối tiếp chỉ dùng một mạch tổ hợp cho việc tính giá trị CRC kế tiếp
Phần mạch tổ hợp dùng cho việc tạo giá trị kế tiếp cho thanh ghi dịch tính CRC được đóng khung trong hình trên. Chú ý, các mạch tổ hợp khác là để điều khiển quá trình tính toán CRC. Với thiết kế ở bài 2, nếu dữ liệu cần tính là 16 bit thì bộ tính toán cần 16 chu kỳ để tính được CRC.
Đối với CRC song song, thay vì chờ kết quả tính toán của chu kỳ trước nạp vào crcSeq rồi với tiếp tục tính cho chu kỳ sau thì nhiều mạch tổ hợp sẽ được xếp chồng nối tiếp với nhau để tạo ra giá trị CRC cuối cùng. Nếu dữ liệu đầu vào là 16 bit thì số lượng mạch tổ hợp cần dùng là 16 mạch, có cấu trúc như mạch tổ hợp dùng để tính CRC nối tiếp.
Hình 2: Mạch nguyên lý tính CRC song song, minh họa cho trường hợp số bit dữ liệu cần tính là 16 và số bit của chuỗi CRC là 8
Việc tính CRC song song sẽ gồm các phần sau:
  1. Thanh ghi lưu lưu dữ liệu ngõ vào (Store input data): thanh ghi này lưu lại dữ liệu cần tính mỗi khi có yêu cầu tính CRC. Nếu có thanh ghi này làm cho việc tính CRC trễ 1 chu kỳ. Nếu không dùng thanh ghi này, dữ liệu ngõ vào sẽ đưa trực tiếp đến các mạch tổ hợp dùng để tính CRC và không bị trễ 1 chu kỳ.
  2. Tạo ngõ vào cho mạch tổ hợp tính CRC (Create input): Ngõ vào này gồm các bit dữ liệu và các bit 0 được thêm vào. Số lượng bit 0 thêm vào bằng độ dài chuỗi CRC.
  3. Mạch tổ hợp tính CRC song song (Parallel calculation): Ngõ ra của mạch tính toán trước, ngõ ra của MUX, là ngõ vào của mạch tính toán kế tiếp, MUX kế tiếp. Chú ý, vòng tròn "XOR GP" trong hình là mạch XOR giữa kết quả tính của 1 tầng cộng 1 bit thêm với đa thưc sinh.
  4. Thanh ghi lưu kết quả CRC (Latch CRC result): Thanh ghi này lấy giá trị của mạch tính cuối cùng (mạch này không bao gồm MUX điều khiển bởi crcReady). Nếu không dùng thanh ghi này, kết quả tính CRC có thể lấy trực tiếp từ mạch tổ hợp cuối cùng bằng cách loại bỏ MUX điều khiển bởi crcReady.
Như vậy, nếu không sử dụng thanh ghi để chốt dữ liệu ngõ vào và chốt giá trị CRC ngõ ra thì bộ tính CRC sẽ cho kết quả ngay lập tức, cùng chu kỳ với giá trị ngõ vào. Tuy nhiên điều này có thể làm timing của bộ tính không tốt vì mạch tổ hợp lớn nên delay trên mạch cao. Mạch tính này càng lớn khi số lượng bit đầu vào càng cao.
Nếu chỉ sử dụng một thanh ghi để chốt dữ liệu đầu vào hoặc chốt giá trị CRC đầu ra thì giá trị CRC sẽ xuất hiện sau ngõ vào 1 chu kỳ.
Nếu sử dụng cả thanh ghi chốt ngõ vào và ngõ ra thì bộ tính CRC cần 2 chu kỳ để tính toán nhưng điều này sẽ giúp timing bộ tính tốt hơn. Tùy vào vị trí mà bộ tính CRC được tích hợp trong hệ thống mà bạn có cách lựa chọn thiết kế phù hợp.
Trong bài viết này, tác giả chọn cách thiết kế có cả FF chốt ngõ vào và FF chốt ngõ ra.
3) Mô tả chi tiết thiết kế
Thiết kế gồm các tín hiệu giao tiếp như bài 2:
  • clk: clock đồng bộ
  • rstN: reset tích cực mức thấp
  • ctrlEn: Enable bộ tính CRC. Tích cực trong 1 chu kỳ để báo có dữ liệu cần tính CRC. Dữ liệu sẽ được đưa vào trên dataIn.
  • dataIn: Dữ liệu cần tính CRC
  • genPoly: Giá trị đa thức sinh sử dụng để tính CRC
  • crcReady: Tích cực mức 1 khi bộ tính CRC rảnh hoặc sau khi tính xong CRC
  • crcSeq: Kết quả chuỗi CRC
Ngoài mạch nguyên lý như đã trình bày ở phần tổng quan, thiết kế còn có 2 mạch khác là thanh ghi lưu lại giá trị đa thức sinh và bit lái ngõ ra crcReady.
Hình 3: Thanh ghi lưu giá trị đa thức sinh khi ctrlEn=1
Thanh ghi đa thức sinh sẽ là ngõ vào của các bộ "XOR GP" dùng ở mạch tính CRC.
Hình 4: Mạch nguyên lý của tín hiệu crcReady
Vì việc tính CRC trong 2 chu kỳ nên mỗi lần tính, ctrlEn=1 trong 1 chu kỳ, thì crcReady chỉ bằng 0 trong 1 chu kỳ.
Về RTL code, phần hơi rắc rối là phần code cho mạch tổ hợp tính CRC. Phần mạch này chỉ khó ở việc lựa chọn đúng vị trí bit cho vòng lặp tạo mạch tổ hợp tính CRC. Các bạn hãy tải code về và so sánh với mạch nguyên lý hình 2 để hiểu rõ hơn.
Ví dụ 1: RTL code mạch tổ hợp tính CRC

generate
  genvar i;
  assign subCrc[TMP_WIDTH-1:(TMP_WIDTH-1)-(CRC_WIDTH-1)]
    = dataInCal[DWIDTH+CRC_WIDTH-1]?
      dataInCal[DWIDTH+CRC_WIDTH-2:
      (DWIDTH+CRC_WIDTH-1)-(CRC_WIDTH-1)-1]
      ^ GenPolyReg[CRC_WIDTH-1:0]
      :dataInCal[DWIDTH+CRC_WIDTH-2:
      (DWIDTH+CRC_WIDTH-1)-(CRC_WIDTH-1)-1];
  for (i=1; i < DWIDTH; i=i+1) begin: CrcCal
    assign subCrc[TMP_WIDTH-1-(i*CRC_WIDTH):
        (TMP_WIDTH-1)-(i*CRC_WIDTH)-(CRC_WIDTH-1)]
      = subCrc[TMP_WIDTH-1-(i-1)*CRC_WIDTH]? 
        {subCrc[(TMP_WIDTH-1)-((i-1)*CRC_WIDTH)-1:
        (TMP_WIDTH-1)-(i*CRC_WIDTH-1)],
        dataInCal[(DWIDTH+CRC_WIDTH-1)-CRC_WIDTH-i]}
       ^ GenPolyReg[CRC_WIDTH-1:0]
       :{subCrc[(TMP_WIDTH-1)-((i-1)*CRC_WIDTH)-1:
        (TMP_WIDTH-1)-(i*CRC_WIDTH-1)],
       dataInCal[(DWIDTH+CRC_WIDTH-1)-CRC_WIDTH-i]};
  end
endgenerate

Trong đoạn code trên:
  • CRC_WIDTH: Số bit chuỗi CRC
  • DWIDTH: Số bit dữ liệu cần tính CRC
  • TMP_WIDTH = DWIDTH*CRC_WIDTH
4) Kết quả mô phỏng
Source có kèm 1 testbench đơn giản minh họa việc tính CRC. Khi chạy testbench này, kết quả tính toán sẽ như sau:
Hình 5: Kết quả tính toán trên cửa sổ Transcript của QuestaSim
Hình 6: Waveform của testbench
Ở đây, một đa thức sinh 8 bit được sử dụng là: 0x07
Ba chuỗi dữ liệu, mỗi chuỗi 16 bit có giá trị lần lượt là:
  • 0x0102 -> CRC là 0x1b
  • 0xa522 -> CRC là 0xb7
  • 0xf0e5 -> CRC là 0xa1
5) Nhận xét
Cách tính CRC song song toàn phần cho kết quả nhanh nhất nhưng tốn nhiều tài nguyên và mạch tổ hợp lớn. Chú ý, việc delay 0, 1, 2 hay nhiều chu kỳ hơn hoàn toàn cho bạn có thể quyết định. Nó tùy vào việc bạn chia đường dữ liệu của mạch tính CRC thành bào nhiêu đoạn để chèn FlipFlop.

Web tính CRC onlline dùng để kiểm chứng kết quả:
https://crccalc.com/

Dữ liệu có thể download:
Source code trên Github

Lịch sử cập nhật:
1) 2019.08.27 - Tạo lần đầu