• 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

Chủ Nhật, 22 tháng 10, 2017

[IP core][Bài 1 - Phân tích tổng quan] Lõi IP điều khiển bộ nhớ EEPROM AT93C46 có giao tiếp APB

Bài viết này trình bày về một thiết kế điều khiển truy xuất bộ nhớ EEPROM theo giao thức đọc ghi nối tiếp. lõi IP có giao tiếp với bus hệ thống APB. Bộ nhớ EEPROM là AT93C46 của hãng Atmel.
Nội dung bài 1 giới thiệu về bộ nhớ AT93C46, giao thức APB và phân tích tổng quan lõi IP sẽ thiết kế.

1. Tổng quan về bộ nhớ EEPROM AT93C46
AT93C646 là loại bộ nhớ EEPROM có thể lập trình được. Bộ nhớ có dung lượng 1024 bit:
  1. 64 word, mỗi word 16 bit khi chân ORG nối đến VCC
  2. 128 word, mỗi word 8 bit khi chân ORG nối đến GROUND
Giao tiếp của bộ nhớ là loại nối tiếp với các chân đơn giản như sau:
Hình 1. Các chân của bộ nhớ EEPROM AT93C46
Nguyên tắc truy xuất của AT93C46 là "một lệnh hợp lệnh bắt đầu được phát cùng lúc với cạnh lên của chân CS". Một lệnh bắt đầu với 1 bit START, theo sau là mã lệnh và địa chỉ ô nhớ.
Các lệnh mà bộ nhớ EEPROM hỗ trợ gồm:
  • READ: Lệnh đọc chứa địa chỉ ô nhớ sẽ đọc. Sau khi nhận lệnh đọc, địa chỉ sẽ được giải mã mà dữ liệu ô nhớ được dịch từng bit ra chân DO. Đầu tiên, 1 bit 0 sẽ được dịch ra trên DO. Sau đó, bit MSB, D15 hoặc D7, được dịch ra trên DO. Dữ liệu ở chân DO được dịch ra theo cạnh lên SK.
  • EWEN: Lệnh cho phép chế độ xóa hoặc ghi bộ nhớ. Khi cấp nguồn cho bộ nhớ, chế độ cho phép xóa hoặc ghi sẽ mặc định bị tắt để đảm bảo tính toàn vẹn của dữ liệu. Lệnh này phải được phát trước khi sử dụng lệnh xóa hoặc ghi bộ nhớ. Chú ý, bộ nhớ chỉ thoát khỏi trạng thái cho phép xóa hoặc ghi khi lệnh tắt chế độ xóa/ghi được phát hoặc bộ nhớ bị ngắt nguồn.
  • ERASE: Lệnh xóa tất cả các bit của một ô nhớ về "1". Sau khi nhận được lệnh xóa, bộ nhớ sẽ bắt đầu thực hiện quá trình xóa. Để kiểm tra trạng thái xóa bộ nhớ, chân CS sẽ được kéo xuống mức thấp trong it nhất 250 ns sau khi phát lệnh xóa, sau đó CS sẽ tích cực mức "1" và kiểm tra giá trị logic trên DO. Nếu DO = 1 thì EEPROM đã xóa xong và một lệnh mới có thể được phát.
  • WRITE: Lệnh ghi tất cả các bit của một ô nhớ. Sau khi bit dữ liệu cuối cùng được dịch vào EEPROM trên DI, bộ nhớ bắt đầu vào trạng thái ghi. Tương tự lệnh xóa, việc ghi bộ nhớ cần một khoảng thời gian để thực hiện. Sau khi ghi, chân CS phải được kéo xuống mức thấp ít nhất trong 250 ns, sau đó kéo lên lại mức "1" và kiểm tra trạng thái DO. Nếu DO = 0, quá trình ghi chưa hoàn thành. Nếu DO = 1, quá trình ghi đã hoàn thành.
  • ERAL: Xóa tất cả các bit của bộ nhớ về 1. Kiểm tra trạng thái xóa tương tự lệnh xóa.
  • WRAL: lệnh ghi tất cả các bit của bộ nhớ. Kiểm tra trạng thái xóa tương tự lệnh ghi.
  • EWDS: Lệnh tắt chế độ lập trình (xóa/ghi) của bộ nhớ
Hình 2. Bảng mã lệnh của AT93C46
Để thiết kế được bộ điều khiển bộ nhớ EEPROM, một phần quan trọng cần quan tâm là timing của các tín hiệu giao tiếp.
Hình 3. Định thời giao tiếp của bộ nhớ AT93C46
Các bạn xem thêm datasheet để biết chi tiết các thông số trên. Ở đây, một số ý chính sẽ được tóm lược:
  1. Tùy vào điện áp cấp cho AT93C46 mà timing của các tín hiệu sẽ có giới hạn khác nhau. Ví dụ, với tầm điện áp từ 4.5V đến 5.5V, tần số xung SK tối đa là 2 Mhz tương ứng chu kỳ 0.5 us = 500 ns. Nếu sử dụng điện áp từ 2.7V đến dưới 4.5V thì điện áp tối đa chỉ 1 Mhz tương ứng chu kỳ 1 us = 1000 ns. Nếu sử dụng điện áp từ 1.8V đến dưới 2.7V, tần số xung SK tối đa là 0.25 Mhz tương ứng chu kỳ 4 us = 4000 ns
  2. Tất cả các chỉ số timing được liệt kê phải được đảm bảo. So sánh với datasheet thì các giá trị timing phải đảm bảo lớn hơn hoặc bằng giá trị MIN và nhỏ hơn hoặc bằng giá trị MAX
Hình 4. Bảng các thông số timing của EEPROM
2. Giao tiếp APB - AMBA 2.0
Giao tiếp APB (Advanced Peripheral Bus) là một trong bộ giao tiếp AMBA (Advanced Microcontroller Bus Architecture). AMBA là một cấu trúc bus hệ thống sử dụng trong các vi điều khiển được phát triển bởi ARM. AMBA có nhiều cấu trúc bus khác nhau như AXI, AHB, ASB, APB. Trong đó, APB là bus dùng để giao tiếp với các ngoại vi không đòi hỏi tốc độ xử lý nhanh như UART, SPI, I2C, ... 
APB có giao tiếp đơn giản gồm các tín hiệu như sau:
  1. PCLK: Tín hiệu clock đồng bộ hoạt động theo cạnh lên
  2. PADDR: địa chỉ thanh ghi được truy xuất
  3. PWRITE: tín hiệu cho phép ghi
  4. PSEL: tín hiệu cho phép
  5. PENABLE: tín hiệu báo pha ENABLE của một truy xuất đọc hoặc ghi
  6. PWDATA: dữ liệu ghi 
  7. PRDATA: dữ liệu đọc
Hình 5. Giao tiếp APB - Ghi (trái) và Đọc (phải)
Master APB: thành phần lái các tín hiệu điều khiển của bus APB. Master APB thường là một bus khác có tốc độ cao hơn trong hệ thống bus, ví dụ như AXI, AHB, ... tuy nhiên nó cũng có thể là một master đầu cuối có giao tiếp trực tiếp đến bus APB.
Slave APB: thành phần nhận các tín hiệu điều khiển của bus APB. Bộ điều khiển EEPROM trong bài này là một slave APB
Hình 6. Minh họa về kết nối của APB bus
Hình minh họa sau đây là một ví dụ về kết nối bus hệ thống. Trong đó, master APB là DMA-230 và bus AHB.
Hình 7. Minh họa một kết nối bus hệ thống trong vi điều khiển (https://developer.arm.com)
Hoạt động của giao tiếp APB đơn giản gồm 3 pha (trạng thái):
  1. IDLE: khi PSEL = 0. Lúc này, PENABLE cũng phải bằng "0" và slave APB không quan tâm đến giá trị các tín hiệu điều khiển còn lại trên bus như PWRITE, PWDATA hay PADDR
  2. SETUP: khi PSEL = 1 và PENABLE = 0. Lúc này, các tín hiệu điều khiển trên bus phải có giá trị hợp lệ. Cá tín hiệu điều khiển bao gồm PADDR, PWRITE và PWDATA nếu là một truy cập "ghi".
  3. ENABLE: khi PSEL = 1 và PENABLE = 0. Lúc này, slave APB phải nhận xong các giá điều khiển và phải lái PRDATA đến giá trị hợp lệ nếu đó là một truy cập "đọc"
Chú ý, mỗi truy cập "đọc" hoặc "ghi" từ master APB đến slave APB phải qua 2 bước là SETUP và ENABLE. Sau bước ENABLE nếu không có truy cập nào khác thì trạng thái truy cập sẽ về IDLE nhưng nếu master APB tiếp tục có truy cập tiếp theo thì có thể lái trực tiếp qua trạng thái SETUP.
Hình 8. Các trạng thái hoạt động của bus APB
3. Phân tích tổng quan lõi IP truy xuất bộ nhớ
Bộ điều khiển truy xuất bộ nhớ sẽ có những đặc điểm chính sau:

  1. Giao tiếp bus APB - AMBA 2.0 trong hệ thống bus 32-bit
  2. Cho phép cấu hình timing giao tiếp bộ nhớ
  3. Giao tiếp với EEPROM thông qua 5 chân ORG, CS, SK, DI và DO
Hình 9. Sơ đồ tín hiệu giao tiếp của lõi IP APB_EEPROM_CTRL
Để thiết kế bộ điều khiển truy xuất bộ nhớ, phần quan trọng nhất là phân tích các yêu cầu về timing bộ điều khiển sao cho đáp ứng yêu cầu trong datasheet.
Tần số fSK: 
Thiết kế sử dụng clock đồng bộ pclk. Để tạo ra xung SK phù hợp, một thanh ghi sẽ được sử dụng cho phép thiết lập hệ số để chia clock pclk thành xung SK với duty cycle là 50%, nghĩa là độ rộng mức cao và mức thấp trong 1 chu kỳ SK bằng nhau. Bạn có thể chọn duty cycle khác nhưng việc chọn duty cycle 50% giúp cho việc thiết kế bộ chia clock đơn giản hơn.

Hình 10. Minh họa về duty cycle
Độ rộng mức cao (fSKH) và độ rộng mức thấp (fSKL):
Vì chúng ta sẽ thiết kế tạo xung SK có duty cycle là 50% nên chỉ cần chu kỳ fSK thỏa mãn là 2 thông số này thỏa mãn. Ví dụ, ở điều kiện 4.5V <= Vcc <= 5.5V, tần số fSK được thiết lập tối đa là 2 Mhz thì chu kỳ là 500 ns. Vì duty cycle là 50% nên, mỗi chu kỳ SK có 250 ns mức "1" và 250 ns mức "0" và thỏa mãn yêu cầu về độ rộng fSKH và fSKL.

Thời gian giữa cạnh lên CS và cạnh lên đầu tiên của SK (tCSS):
Trường hợp xấu nhất, tCSS tối thiếu bằng 200 ns nên một nửa xung SK sẽ được dùng để tạo độ trễ này. Một nữa xung SK, cho trường hợp nhanh nhất bằng 250ns khi fSK = 2 Mhz sẽ luôn thỏa mãn yêu cầu và giúp thiết kế đơn giản hơn.

Thời gian giữa cạnh xuống xung SK cuối cùng và cạnh xuống của CS (tCSH):
Độ trễ yêu cầu là 0 ns, tức là không cần trễ. Nếu trong thiết kế, chúng ta cho CS xuống cùng với cạnh xuống SK thì cạnh xuống CS tại chân EEPROM có thể xuất hiện trước cạnh xuống xung SK vì độ trễ của SK khi kết nối từ bộ điều khiển đến EEPROM có thể lớn hơn độ trễ của CS. Để tránh điều này, thiết kế sẽ sẽ điều khiển cạnh xuống CS sau cạnh xuống SK một hoặc một vài xung clock hệ thống pclk.

Thời gian ổn định của dữ liệu trên đường DI trước (tDIS) và sau (tDIH) cạnh lên của SK:
Cả 2 thời gian này tối thiểu phải bằng 400 ns cho trường hợp xấu nhất. Trong thiết kế, để đảm bảo timing này, dữ liệu trên đường DI sẽ được dịch ra bằng cạnh xuống xung SK. Như vậy, dữ liệu sẽ được giữ ổn định trước cạnh lên SK nửa chu kỳ và sau cạnh lên SK nửa chu kỳ.

Thời gian trễ của dữ liệu trên DO là tPD0 và tPD1 trong lệnh READ:
Để đảm bảo lấy mẫu đúng dữ liệu trả về từ EEPROM trên DO, thiết kế sẽ lấy mẫu dữ liệu theo cạnh xuống SK. Ví dụ, trường hợp điện áp EEPROM sử dụng trong tầm rộng từ 1.8V <= VCC <= 5.5V, cả 2 độ trễ này đều là 1000 ns. Nếu sử dụng tần số fSK lớn nhất là 0.25 Mhz thì chu kỳ là 4000 ns, nửa chu kỳ là 2000 ns. Nếu trừ đi độ trễ thì thời gian dữ liệu ổn định trước cạnh xuống SK là 1000 ns và đảm bảo lấy mẫu đúng dữ liệu trên DO.

Thời gian để DO có giá trị bit trạng thái hợp lệ tSV:
Trong chế độ lập trình, ghi hoặc xóa, bit trạng thái chỉ hợp lệ sau khi CS tích cực một khoảng thời gian bằng tSV. Nghĩa là, sau khoảng thời gian này mới được phép kiểm tra giá trị bit trạng thái. Vì DO sẽ được lấy mẫu bằng cạnh xuống SK nên khoảng thời gian từ cạnh lên CS đến cạnh xuống SK đầu tiên là 1 chu kỳ SK. Như vậy, thời điểm lấy mẫu luôn lớn hơn tSV. Ví dụ, trường hợp 1.8V <= VCC <= 5.5V, giá trị lớn nhất của tSV = 1000 ns nhưng 1 chu kỳ SK nhỏ nhất bằng 4000 ns ứng với fSK lớn nhất là 0.25 Mhz nên luôn thỏa mãn yêu cầu.

Thời gian để DO quay về trạng thái tổng trở cao high-Z sau khi CS xuống mức "0" là tDF:
Sau khi CS xuống mức thấp thì phải đợi sau thời gian này để DO trở về trạng thái high-Z trước khi tích cực CS để thực hiện truy xuất tiếp theo. Bên cạnh đó, datasheet cũng quy định thời gian giữa 2 lần truy xuất EEPROM là tCS, tính từ cạnh xuống CS của lần truy xuất trước đến cạnh lên CS của lần truy xuất tiếp theo, nên khoảng thời gian chờ giữa 2 lần truy xuất phải lớn hơn hoặc bằng max(min(tCS), max(tDF)).
Trong thiết kế, một chu kỳ SK sẽ được chọn để làm khoảng cách giữa 2 lần truy xuất bộ nhớ EEPROM.

Sơ đồ timing của thiết kế điều khiển EEPROM sau khi phân tích như hình sau:
Hình 11. Sơ đồ timing giao tiếp EEPROM của bộ điều khiển
Căn cứ trên chức năng của lõi IP, sơ đồ khối có thể được phân chia như sau:

  • APB_INTF: khối giao tiếp và đáp ứng giao thức APB. Khối này sẽ chứa các thanh ghi cấu hình và trạng thái của lõi IP trong quá trình hoạt động từ đó tạo các tín hiệu điều khiển các khối khác.
  • CLOCK_GEN: khối tạo txung SK, tín hiệu dịch bit trên ec_di và tín hiệu lấy mẫu bit trên eeprom_do.
  • EEPROM_INTF: khối tạo tín hiệu điều khiển EEPROM

Hình 12. Sơ đồ khối lõi IP điều khiển bộ nhớ EEPROM
Kết thúc bài 1, chúng ta có thể hình dung được tổng quan về lõi IP sẽ thiết kế. Đồng thời, các đặc điểm về timing tín hiệu cũng được phân tích để thiết kế có thể đáp ứng được yêu cầu của bộ nhớ.

Thứ Hai, 9 tháng 10, 2017

[CRC] Bài 1 - Lý thuyết về CRC và mạch tính CRC nối tiếp

Nội dung bài viết này trình bày về lý thuyết tạo và và kiểm tra CRC. Phương pháp thực hiện mạch tính CRC nối tiếp. Đồng thời, bài viết đưa ra một RTL code tính CRC có thể lựa chọn độ dài đa thức sinh (generator polynomial) trước khi tổng hợp và gán giá trị đa thức sinh mong muốn khi sử dụng.

1. Tổng quan
Việc truyền một dữ liệu trong một môi trường từ điểm này đến điểm khác, ví dụ như truyền dữ liệu giữa hay máy tính trong một mạng, luôn tiềm ẩn nhiều yếu tố làm dữ liệu truyền bị sai. Cơ chế phát hiện lỗi dữ liệu là không thể thiếu đối với các giao thức có độ tin cậy cao.
Phương pháp chung để kiểm tra lỗi dữ liệu là thêm các bit kiểm tra kèm theo dữ liệu được truyền theo một quy tắc đã được quy định trước. Bộ truyền dữ liệu sẽ tạo ra các bit kiểm tra từ giá trị dữ liệu cần truyền và gắn nó với dữ liệu truyền. Bộ nhận sẽ nhận dữ liệu và tính toán lại các bit kiểm tra để so sánh với các bit kiểm tra mà nó nhận được. Nếu hai kết quả khác nhau thì đây là một lỗi.
Một phương pháp kiểm tra đơn giản đó chính là sử dụng parity bit. Parity bit là phương pháp sử dụng 1 bit để kiểm tra số bit "1" hoặc "0" của chuỗi dữ liệu là "chẵn" hoặc "lẻ". Ví dụ sau đây minh họa việc kiểm tra parity chẵn, tổng số bit 1 trong chuỗi được truyền đi bao gồm cả dữ liệu và bit kiểm tra luôn là một số chẵn.
  • Tạo bit parity: XOR tất cả các bit dữ liệu
  • Kiểm tra parity: XOR tất cả các bit dữ liệu và bit parity. Nếu giá trị là 1 thì chuỗi dữ liệu nhận bị sai.
Một số điểm cần lưu ý:
  • Bit sai có thể là bit dữ liệu hoặc bit parity
  • Chỉ phát hiện được nếu số bit sai là số lẻ (sai 1, 3, 5, 7, ... bit)
Hình 1. Tạo và kiểm tra Parity chẵn cho chuỗi dữ liệu 8 bit
Phương pháp kiểm tra parity đơn giản nhưng độ tin cậy kém nên được ứng dụng cho các giao thức có tốc độ truyền dữ liệu chậm hoặc số lượng bit dữ liệu cần kiểm tra ít ví dụ như giao thức UART.
CRC (Cyclic Redundancy Code) là một phương pháp phổ biến có độ tin cậy cao hơn nhiều so với sử dụng bi parity. CRC được ứng dụng trong nhiều giao thức có khối lượng dữ liệu truyền lớn hoặc tốc độ truyền dữ liệu cao như CAN, Ethernet, giao tiếp RF 15693, ...
2. Lý thuyết về tính toán CRC
Giá trị chuỗi bit kiểm tra hay chuỗi CRC là số dư của phép chia của chuỗi bit dữ liệu cho một chuỗi bit đa thức sinh (Generator Polynomial). Đa thức sinh là số chia sẽ khác nhau tùy vào mỗi giao thức quy định. Phép chia trong tính toán CRC sử dụng cách tính modulo-2. Modulo-2 thực chất là XOR hai số hạng.
Giả sử đa thức chuỗi dữ liệu cần truyền là M(x):
Đa thức sinh là G(x):
Trong đó:
  • am và an bằng 1 hoặc 0
  • Độ dài chuỗi CRC bằng độ dài đa thức sinh trừ 1 và bằng số mũ lớn nhất của đa thức sinh và bằng n.
Để tạo CRC, chuỗi dữ liệu cần truyền sẽ được mở rộng thêm n bit về phía bên phải:
Điều này, tương ứng với việc dịch trái n bit chuỗi dữ liệu M(x).
Cuối cùng, chia T(x) cho G(x) và lấy số dư. Số dư chính là chuỗi CRC n bit.
Kiểm tra CRC được thực hiện bằng 1 trong 2 cách sau:
  • Lấy chuỗi dữ liệu có cả các bit kiểm tra CRC chia cho đa thức sinh. Nếu số dư khác "0" thì dữ liệu nhận bị lỗi.
  • Tách chuỗi dữ liệu và chuỗi CRC riêng. Chỉ lấy chuỗi dữ liệu chia cho đa thức sinh rồi lấy số dư phép chia so sánh với chuỗi CRC. Nếu hai chuỗi khác nhau thì dữ liệu nhận bị lỗi.
Ví dụ về tính toán CRC-4, tương ứng với số bit kiểm tra là 4 bit, với đa thức sinh như sau:
x^4 + x + 1 (b10011)

Chuỗi dữ liệu cần truyền có 8 bit như sau:
x^7 + x^5 + x (b1010_0010)
Chuỗi dữ liệu trước khi chia sẽ được mở rộng thêm 4 bit "0":
x^11 + x^9 + x^5 (b1010_0010_0000)
Hình 2. Tính chuỗi CRC
Việc kiểm tra CRC được thực hiện trên chuỗi dữ liệu có đính kèm CRC như sau:
Hình 3. Kiểm tra CRC - trường hợp nhận đúng, số dư bằng 0
Hình 4. Kiểm tra CRC bằng cách chia chuỗi dữ liệu có CRC với đa thức sinh - trường hợp sai 1 bit và trường hợp sai 2 bit, số dư khác 0
Bộ nhận sẽ không phát hiện được lỗi dữ liệu khi chuỗi dữ liệu bị sai và chuỗi CRC cũng sai trùng với giá trị CRC của chuỗi dữ liệu bị sai. Tuy nhiên, xác suất để xảy ra đúng trường hợp này là thấp. Xác suất này càng thấp khi chuỗi CRC càng dài.
Hình 6. Kiểm tra CRC không phát hiện được lỗi khi cả dữ liệu và CRC cùng sai đồng thời
Xét đa thức sinh g(x) = x + 1, đây là đa thức CRC-1, tính CRC cho chuỗi 8 bit b10100010 và chuỗi b10011111.
Hình 7. Tính CRC-1
So sánh kết quả với phương pháp tính parity chẵn đã trình bày phía trên chúng ta có thể nhận thấy sự tương đồng. CRC-1 chính là phương pháp kiểm tra parity.
3. Mạch nguyên lý tính CRC
Xem lại các ví dụ đã trình bày trên đây, CRC được tính theo nguyên tắc:
  • Nếu bit MSB của lần tính hiện tại bằng 1 thì nó sẽ được XOR (modulo-2) với đa thức sinh
  • Nếu bit MSB của lần tính hiện tại bằng 0 thì nó sẽ không đổi
Hình 8. Vị trí có MSB = 1 được XOR với đa thức sinh
Để thực hiện mạch CRC-1, ngoài cách XOR tất cả các bit dữ liệu đầu vào như đã trình bày ở phần trên, chúng ta có thể thực hiện dựa trên nguyên lý của việc chia đa thức như hình trên. Mạch cần 2 FF để lưu giá trị sau mỗi lần XOR và mạch sẽ dịch 1 bit sau mỗi lần XOR để lấy 1 bit dữ liệu mới như hình sau:
Hình 9. Mạch nguyên lý của CRC-1
Ở hình trên, bit MSB sẽ điều khiển MUX chọn có XOR với đa thức sinh x+1 hay không? Tuy nhiên, sau mỗi chu kỳ tính, bit MSB luôn bị loại bỏ nên mạch MUX và XOR của bit MSB là không cần thiết. Mạch được rút gọn như hình sau:

Hình 10. Mạch nguyên lý CRC-1 (bỏ mạch tính ở bit MSB)
Xét mạch MUX, nếu bit MSB bằng 1 thì bit 0 XOR với 1, nếu bit MSB bằng 0 thì tương ứng với việc bit 0 XOR với 0 nên mạch MUX được loại bỏ để thay bằng bit 1 XOR bit 0.
Hình 11. Mạch nguyên lý CRC-1 (bỏ mạch MUX)
Bit 0 chỉ dùng để lưu giá trị bit dịch vào nên cũng có thể loại bỏ.
Hình 12. Mạch nguyên lý CRC-1 (bỏ FF đầu vào)
Ở đây, bit CRC chỉ có 1 bit nên việc thêm 1 bit 0 ở chuỗi dữ liệu đầu vào để tính CRC cũng không cần thiết vì giá trị nào XOR với 0 cũng bằng chính nó.
Hình 12. Mạch nguyên lý CRC-1
Biểu diễn thường thấy cho mạch tính CRC như sau:
Hình 13. Mạch nguyên lý CRC-1 với biểu diễn thông thường
Tương tự, xét lại mạch CRC-4 có đa thức sinh x^4 + x + 1, mạch nguyên lý tính CRC-4 như sau (lưu ý, vị trí XOR với "0" thì loại bỏ cả MUX và cổng XOR):
Hình 14. Mạch nguyên lý tính CRC-4
5. RTL code tính CRC nối tiếp
5.1 Nhận xét
Qua hai ví dụ trên đây, nhận xét chung như sau:
  • Tại vị trí mà bit đa thức sinh bằng "0" thì chỉ là phép dịch bit
  • Tại vị trí mà bit đa thức sinh bằng "1" thì được chèn cổng XOR
  • Dữ liệu nối tiếp để tính CRC dịch từ MSB đến LSB với số lần dịch bằng độ dài dữ liệu cộng độ dài giá trị CRC. Ví dụ, dữ liệu 8 bit dùng CRC-4 thì số lần dịch là 12 lần với 4 bit cuối là 4 bit 0 được thêm vào chuỗi dữ liệu.
5.2 Phân tích module tạo và kiểm tra CRC
Căn cứ vào những nhận xét trên, một thiết kế thực hiện tính CRC tổng quát được thực hiện như sau:
  1. Sử dụng một define CRC_CTRL_POLY để cho phép tạo tín hiệu input điều khiển giá trị của đa thức sinh nếu muốn. Chú ý, độ rộng tín hiệu điều khiển bằng số bit CRC vằ bằng số mũ lớn nhất của đa thức sinh. Ví dụ, nếu đa thức sinh là x^4 + x + 1 thì độ rộng tín hiệu là 4 bit và giá trị gán cho tín hiệu điều khiển là 4'b0011 (bỏ bit 1 của x^4)
  2. Sử dụng một define CRC_CHECKER để cho phép tạo chức năng kiểm tra CRC
  3. Sử dụng một parameter CRC_GPW_MAX cho phép cấu hình độ rộng đa thức sinh. Độ rộng đa thức sinh bằng số mũ lớn nhất của đa thức sinh. Ví dụ, nếu đa thức sinh là x^4 + x + 1 thì CRC_GPW_MAX = 4
  4. Sử dụng một parameter CRC_POLY_VALUE cho phép gán giá trị đa thức sinh sẽ sử dụng nếu không sử dụng tín hiệu điều khiển được tạo ra bởi định nghĩa CRC_CTRL_POLY. Ví dụ, nếu không định nghĩa CRC_CTRL_POLY, đa thức sinh là x^4 + x + 1 thì CRC_GPW_MAX = 4 và giá trị CRC_POLY_VALUE = 4'b0011
Sơ đồ tín hiệu giao tiếp của module CRC như sau:
Hình 15. Sơ đồ tín hiệu của module tính CRC
Hai tín hiệu ctrl_en và chk_en sẽ điều khiển chức năng tạo và kiểm tra CRC như sau, khi tín hiệu ctrl_en tích cực, dữ liệu dùng để tạo CRC hoặc được kiểm tra CRC sẽ bắt đầu dịch vào data_in. ctrl_en sẽ tích cực bằng số bit cần dịch trên data_in.
  • Nếu chk_en = 0 thì khi ctrl_en = 0, crc_seq sẽ giữ giá trị chuỗi CRC trong 1 chu kỳ xung clock
  • Nếu chk_en = 1 thì khi ctrl_en = 0, crc_error sẽ báo lỗi CRC
    • crc_error = 1 thì chuỗi kiểm tra bị lỗi CRC
    • crc_error = 0 thì chuỗi kiểm tra không bị lỗi
Mạch tổng quát của từng bit trong thanh ghi chứa giá trị CRC như sau:
Hình 16. Mạch tổng quát tính giá trị bit CRC thứ i
Riêng bit 0 có đầu vào là data_in có mạch như sau:
Hình 17. Mạch tổng quát tính giá trị bit CRC 0
Khi có định nghĩa CRC_CHECKER, mạch kiểm tra lỗi CRC sẽ được tạo ra như sau:
Hình 18. Mạch kiểm tra lỗi CRC
5.3 RTL code
Link download RTL code và testbench: CRC RTL code
pass (nếu có): nguyenquanicd

5.4 Kết quả mô phỏng
Hình 19. Mô phỏng tạo và kiểm tra CRC
Đa thức sinh: x^4 + x + 1 tương ứng với việc gán ctrl_poly_en = 4'b0011
Dữ liệu dùng để tạo CRC: 1010_0110 sau khi thêm 4 bit "0" là 1010_0110_0000 => Kết quả tính CRC là 1110
Dữ liệu dùng để kiểm tra CRC: 1010_0110_1110. Trong đó, 4 bit LSB 1110 là chuỗi CRC => Kết quả kiểm tra CRC là crc_error = 0

Lưu ý, đa thức sinh có nhiều cách biểu diễn khác nhau. Trong bài viết này chỉ dùng cách biểu diễn thông thường (normal representation) từ MSB đến LSB.

Lịch sử cập nhật:
1) 2019.08.26 - Thêm lưu ý về cách biểu diễn đa thức sinh

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

Thứ Bảy, 23 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 1bà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`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.