Merancang Ring Buffer FIFO VHDL yang Kuat di Block RAM
Buffer sirkular adalah konstruksi populer untuk membuat antrian dalam bahasa pemrograman berurutan, tetapi juga dapat diimplementasikan dalam perangkat keras. Dalam artikel ini, kami akan membuat ring buffer di VHDL untuk mengimplementasikan FIFO di block RAM.
Ada banyak keputusan desain yang harus Anda buat saat mengimplementasikan FIFO. Antarmuka seperti apa yang Anda butuhkan? Apakah Anda terbatas oleh sumber daya? Apakah harus tahan terhadap over-read dan overwrite? Apakah latensi dapat diterima? Itu beberapa pertanyaan yang muncul dalam pikiran saya saat diminta untuk membuat FIFO.
Ada banyak implementasi FIFO gratis online, serta generator FIFO seperti Xilinx LogiCORE. Tetapi masih banyak insinyur yang lebih suka mengimplementasikan FIFO sendiri. Karena meskipun semuanya melakukan tugas antrian dasar yang sama, mereka dapat sangat berbeda saat mempertimbangkan detailnya.
Cara kerja ring buffer
Ring buffer adalah implementasi FIFO yang menggunakan memori kontigu untuk menyimpan data yang ditampung dengan perpindahan data minimal. Elemen baru tetap berada di lokasi memori yang sama dari waktu penulisan hingga dibaca dan dihapus dari FIFO.
Dua counter digunakan untuk melacak lokasi dan jumlah elemen dalam FIFO. Counter ini merujuk ke offset dari awal ruang memori tempat data disimpan. Dalam VHDL, ini akan menjadi indeks ke sel array. Untuk sisa artikel ini, kami akan merujuk ke counter ini sebagai pointer.
Dua pointer ini adalah pointer head dan tail. Head selalu menunjuk ke slot memori yang akan berisi data berikutnya yang ditulis, sementara tail merujuk ke elemen berikutnya yang akan dibaca dari FIFO. Ada varian lain, tetapi ini yang akan kita gunakan.
Empty state
Jika head dan tail menunjuk ke elemen yang sama, berarti FIFO kosong. Gambar di atas menunjukkan contoh FIFO dengan delapan slot. Keduanya head dan tail pointer menunjuk ke elemen 0, menunjukkan bahwa FIFO kosong. Ini adalah keadaan awal ring buffer.
Perhatikan bahwa FIFO masih akan kosong jika keduanya pointer berada pada indeks lain, misalnya 3. Untuk setiap penulisan, head pointer bergerak satu tempat ke depan. Tail pointer ditambah setiap kali pengguna FIFO membaca elemen.
Ketika salah satu pointer berada pada indeks tertinggi, penulisan atau pembaca berikutnya akan menyebabkan pointer kembali ke indeks terendah. Ini adalah keindahan ring buffer, data tidak bergerak, hanya pointer yang bergerak.
Head leads tail
Gambar di atas menunjukkan ring buffer yang sama setelah lima penulisan. Tail pointer masih berada pada slot nomor 0, tetapi head pointer telah bergerak ke slot nomor 5. Slot yang berisi data diwarnai biru muda dalam ilustrasi. Tail pointer akan berada pada elemen tertua, sementara head menunjuk ke slot kosong berikutnya.
Ketika head memiliki indeks lebih tinggi daripada tail, kita dapat menghitung jumlah elemen dalam ring buffer dengan mengurangi tail dari head. Dalam gambar di atas, hasilnya adalah lima elemen.
Tail leads head
Mengurangi head dari tail hanya berfungsi jika head memimpin tail. Dalam gambar di atas, head berada pada indeks 2 sementara tail berada pada indeks 5. Jadi, jika kita melakukan perhitungan sederhana ini, kita mendapatkan 2 – 5 = -3, yang tidak masuk akal.
Solusinya adalah menambal head dengan jumlah total slot dalam FIFO, 8 dalam kasus ini. Perhitungan sekarang menghasilkan (2 + 8) – 5 = 5, yang merupakan jawaban yang benar.
Tail akan selalu mengejar head, itulah cara kerja ring buffer. Setengah waktu tail akan memiliki indeks lebih tinggi daripada head. Data disimpan di antara keduanya, seperti yang ditunjukkan oleh warna biru muda dalam gambar di atas.
Full state
Ring buffer penuh akan memiliki tail menunjuk ke indeks tepat setelah head. Konsekuensi dari skema ini adalah kita tidak bisa menggunakan semua slot untuk menyimpan data, harus ada setidaknya satu slot kosong. Gambar di atas menunjukkan situasi di mana ring buffer penuh. Slot yang terbuka, namun tidak dapat digunakan, diwarnai kuning.
Sinyal kosong/penuh khusus juga dapat digunakan untuk menunjukkan bahwa ring buffer penuh. Ini akan memungkinkan semua slot memori untuk menyimpan data, tetapi memerlukan logika tambahan dalam bentuk register dan tabel pencarian (LUT). Oleh karena itu, kami akan menggunakan skema keep one open untuk implementasi ring buffer FIFO kami, karena ini hanya membuang block RAM yang lebih murah.
Implementasi ring buffer FIFO
Cara Anda mendefinisikan sinyal antarmuka ke dan dari FIFO Anda akan membatasi jumlah implementasi yang mungkin dari ring buffer Anda. Dalam contoh kami, kami akan menggunakan variasi dari antarmuka read/write enable dan empty/full/valid klasik.
Akan ada bus write data di sisi input yang membawa data yang akan didorong ke FIFO. Akan juga ada sinyal write enable, yang saat diaktifkan, akan menyebabkan FIFO untuk men-sampel data input.
Di sisi output akan ada sinyal read data dan read valid yang dikontrol oleh FIFO. Ini juga akan memiliki sinyal read enable yang dikontrol oleh pengguna downstream FIFO.
Sinyal kontrol empty dan full adalah bagian dari antarmuka FIFO klasik, kami akan menggunakannya juga. Mereka dikontrol oleh FIFO, dan tujuannya adalah untuk mengkomunikasikan keadaan FIFO ke pembaca dan penulis.
Back pressure
Masalahnya dengan menunggu sampai FIFO kosong atau penuh sebelum bertindak adalah logika antarmuka tidak akan memiliki waktu untuk bereaksi. Logika sekuensial bekerja pada dasar siklus clock ke siklus clock, tepi naik clock secara efektif memisahkan peristiwa dalam desain Anda menjadi langkah waktu.
Salah satu solusi adalah menyertakan sinyal almost empty dan almost full yang mendahului sinyal asli satu siklus clock. Ini memberikan logika eksternal waktu untuk bereaksi, bahkan saat membaca atau menulis secara terus-menerus.
Dalam implementasi kami, sinyal pendahulu akan bernama empty_next dan full_next, cukup karena saya lebih suka menggunakan postfix daripada prefix nama.
The entity
Gambar di bawah ini menunjukkan entitas ring buffer FIFO kami. Selain sinyal input dan output di port, ini memiliki dua konstanta generik. Generik RAM_WIDTH mendefinisikan jumlah bit dalam kata input dan output, jumlah bit yang akan terdapat di setiap slot memori.
Generik RAM_DEPTH mendefinisikan jumlah slot yang akan disediakan untuk ring buffer. Karena satu slot disediakan untuk menunjukkan bahwa ring buffer penuh, kapasitas FIFO akan RAM_DEPTH – 1. Konstanta RAM_DEPTH harus disesuaikan dengan kedalaman RAM pada FPGA target. RAM yang tidak terpakai dalam primitif block RAM akan terbuang, tidak dapat dibagikan dengan logika lain di FPGA.
entity ring_buffer is generic ( RAM_WIDTH : natural; RAM_DEPTH : natural ); port ( clk : in std_logic; rst : in std_logic; -- Write port wr_en : in std_logic; wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0); -- Read port rd_en : in std_logic; rd_valid : out std_logic; rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0); -- Flags empty : out std_logic; empty_next : out std_logic; full : out std_logic; full_next : out std_logic; -- The number of elements in the FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer;
Selain clock dan reset, deklarasi port akan mencakup port baca/tulis data/enable klasik. Digunakan oleh modul upstream dan downstream untuk mendorong data baru ke FIFO, dan untuk mengeluarkan elemen tertua dari FIFO.
Sinyal rd_valid diaktifkan oleh FIFO ketika port rd_data berisi data yang valid. Peristiwa ini tertunda satu siklus clock setelah pulsa pada sinyal rd_en. Kami akan membicarakan lebih banyak tentang mengapa harus demikian di akhir artikel ini.
Kemudian datang flag empty/full yang ditetapkan oleh FIFO. Sinyal empty_next akan diaktifkan ketika tersisa 1 atau 0 elemen, sementara empty hanya aktif ketika ada 0 elemen dalam FIFO. Sebaliknya, sinyal full_next akan menunjukkan bahwa ada ruang untuk 1 atau 0 elemen lagi, sementara full hanya diaktifkan ketika FIFO tidak dapat menampung elemen data lain.
Terakhir, ada output fill_count. Ini adalah integer yang akan mencerminkan jumlah elemen yang saat ini disimpan dalam FIFO. Saya menyertakan output sinyal ini hanya karena akan kita gunakan secara internal dalam modul. Mengeluarkannya melalui entitas pada dasarnya gratis, dan pengguna dapat memilih untuk meninggalkan sinyal ini tidak terhubung saat membuat instance modul ini.
The declarative region
Di dalam area deklaratif file VHDL, kami akan mendeklarasikan tipe kustom, subtype, sejumlah sinyal, dan prosedur untuk penggunaan internal dalam modul ring buffer.
type ram_type is array (0 to RAM_DEPTH - 1) of std_logic_vector(wr_data'range); signal ram : ram_type; subtype index_type is integer range ram_type'range; signal head : index_type; signal tail : index_type; signal empty_i : std_logic; signal full_i : std_logic; signal fill_count_i : integer range RAM_DEPTH - 1 downto 0; -- Increment and wrap procedure incr(signal index : inout index_type) is begin if index = index_type'high then index <= index_type'low; else index <= index + 1; end if; end procedure;
Pertama, kami mendeklarasikan tipe baru untuk memodelkan RAM kami. Tipe ram_type adalah array vektor, yang diukur oleh input generik. Tipe baru digunakan pada baris berikutnya untuk mendeklarasikan sinyal ram yang akan menyimpan data di ring buffer.
Di blok kode berikutnya, kami mendeklarasikan index_type, subtype dari integer. Rentangnya akan diatur secara tidak langsung oleh generik RAM_DEPTH. Di bawah deklarasi subtype, kami menggunakan tipe indeks untuk mendeklarasikan dua sinyal baru, pointer head dan tail.
Selanjutnya diikuti blok deklarasi sinyal yang merupakan salinan internal dari sinyal entitas. Mereka memiliki nama dasar yang sama dengan sinyal entitas tetapi ditambahi dengan _i untuk menunjukkan bahwa untuk penggunaan internal. Kami menggunakan pendekatan ini karena dianggap gaya buruk untuk menggunakan mode inout pada sinyal entitas, meskipun ini akan memiliki efek yang sama.
Terakhir, kami mendeklarasikan prosedur bernama incr yang mengambil sinyal index_type sebagai parameter. Subprogram ini akan digunakan untuk menambah head dan pointer tail, dan membungkusnya kembali ke 0 ketika mereka berada pada nilai tertinggi. Head dan tail adalah subtype dari integer, yang biasanya tidak mendukung perilaku pembungkusan. Kami akan menggunakan prosedur untuk mengatasi masalah ini.
Concurrent statements
Di bagian atas arsitektur, kami mendeklarasikan pernyataan paralel kami. Saya suka mengumpulkan tugasan sinyal satu baris ini sebelum proses normal karena mereka mudah terlewat. Pernyataan paralel sebenarnya adalah bentuk proses, Anda dapat membaca lebih lanjut tentang pernyataan paralel di sini:
How to create a Concurrent Statement in VHDL
-- Copy internal signals to output empty <= empty_i; full <= full_i; fill_count <= fill_count_i; -- Set the flags empty_i <= '1' when fill_count_i = 0 else '0'; empty_next <= '1' when fill_count_i <= 1 else '0'; full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0'; full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0';
Di blok tugasan paralel pertama, kami menyalin versi internal sinyal entitas ke output. Baris-baris ini akan memastikan bahwa sinyal entitas mengikuti versi internal pada waktu yang sama, tetapi dengan satu penundaan siklus delta dalam simulasi.
Blok terakhir dari pernyataan paralel adalah tempat kami menetapkan flag output, menunjukkan keadaan penuh/kosong ring buffer. Kami mendasarkan perhitungan pada generik RAM_DEPTH dan pada sinyal fill_count. Kedalaman RAM adalah konstanta yang tidak akan berubah. Oleh karena itu, flag akan berubah hanya sebagai hasil dari pembaruan jumlah terisi.
Updating the head pointer
Fungsi dasar pointer head adalah bertambah setiap kali sinyal write enable diaktifkan dari luar modul ini. Kami melakukan ini dengan melewatkan sinyal head ke prosedur incr yang disebutkan sebelumnya.
PROC_HEAD : process(clk) begin if rising_edge(clk) then if rst = '1' then head <= 0; else if wr_en = '1' and full_i = '0' then incr(head); end if; end if; end if; end process;
Kode kami mengandung pernyataan tambahan and full_i = '0' untuk melindungi dari overwrite. Logika ini dapat dihilangkan jika Anda yakin bahwa sumber data tidak akan pernah mencoba menulis ke FIFO saat penuh. Tanpa perlindungan ini, overwrite akan menyebabkan ring buffer menjadi kosong lagi.
Jika pointer head ditambah sementara ring buffer penuh, head akan menunjuk ke elemen yang sama dengan tail. Jadi, modul akan “lupa” data yang terkandung, dan tampilan isi FIFO akan kosong.
Dengan mengevaluasi sinyal full_i sebelum menambah pointer head, itu hanya akan lupa nilai yang ditimpa. Saya pikir solusi ini lebih baik. Namun, bagaimanapun, jika overwrite pernah terjadi, itu indikasi kejanggalan di luar modul ini.
Updating the tail pointer
Pointer tail ditambah dengan cara yang sama seperti pointer head, tetapi input read_en digunakan sebagai pemicu. Sama seperti dengan overwrite, kami melindungi dari over-read dengan menyertakan and empty_i = '0' dalam ekspresi Boolean.
PROC_TAIL : process(clk) begin if rising_edge(clk) then if rst = '1' then tail <= 0; rd_valid <= '0'; else rd_valid <= '0'; if rd_en = '1' and empty_i = '0' then incr(tail); rd_valid <= '1'; end if; end if; end if; end process;
Selain itu, kami memancarkan sinyal rd_valid pada setiap pembacaan yang valid. Data baca selalu valid pada siklus clock setelah rd_en diaktifkan, jika FIFO tidak kosong. Dengan pengetahuan ini, sebenarnya tidak ada kebutuhan untuk sinyal ini, tetapi kami akan menyertakannya untuk kenyamanan. Sinyal rd_valid akan dioptimalkan dalam sintesis jika dibiarkan tidak terhubung saat modul dibuat instance.
Inferring block RAM
Untuk membuat alat sintesis menyimpulkan block RAM, kami harus mendeklarasikan port baca dan tulis dalam proses sinkron tanpa reset. Kami akan membaca dan menulis ke RAM pada setiap siklus clock, dan membiarkan sinyal kontrol menangani penggunaan data ini.
PROC_RAM : process(clk) begin if rising_edge(clk) then ram(head) <= wr_data; rd_data <= ram(tail); end if; end process;
Proses ini tidak tahu kapan penulisan berikutnya akan terjadi, tetapi tidak perlu tahu. Sebaliknya, kami hanya terus menulis. Ketika sinyal head ditambah sebagai hasil penulisan, kami mulai menulis ke slot berikutnya. Ini akan secara efektif mengunci nilai yang ditulis.
Updating the fill count
Sinyal fill_count digunakan untuk menghasilkan sinyal penuh dan kosong, yang pada gilirannya digunakan untuk mencegah overwrite dan over-read FIFO. Counter terisi diperbarui oleh proses kombinatorial yang sensitif terhadap pointer head dan tail, tetapi sinyal-sinyal ini hanya diperbarui pada tepi naik clock. Oleh karena itu, jumlah terisi juga akan berubah segera setelah tepi clock.
PROC_COUNT : process(head, tail) begin if head < tail then fill_count_i <= head - tail + RAM_DEPTH; else fill_count_i <= head - tail; end if; end process;
Jumlah terisi dihitung dengan cara sederhana dengan mengurangi tail dari head. Jika indeks tail lebih besar dari head, kita harus menambahkan nilai konstanta RAM_DEPTH untuk mendapatkan jumlah elemen yang benar yang saat ini ada di ring buffer.
The complete VHDL code for the ring buffer FIFO
library ieee; use ieee.std_logic_1164.all; entity ring_buffer is generic ( RAM_WIDTH : natural; RAM_DEPTH : natural ); port ( clk : in std_logic; rst : in std_logic; -- Write port wr_en : in std_logic; wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0); -- Read port rd_en : in std_logic; rd_valid : out std_logic; rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0); -- Flags empty : out std_logic; empty_next : out std_logic; full : out std_logic; full_next : out std_logic; -- The number of elements in the FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer; architecture rtl of ring_buffer is type ram_type is array (0 to RAM_DEPTH - 1) of std_logic_vector(wr_data'range); signal ram : ram_type; subtype index_type is integer range ram_type'range; signal head : index_type; signal tail : index_type; signal empty_i : std_logic; signal full_i : std_logic; signal fill_count_i : integer range RAM_DEPTH - 1 downto 0; -- Increment and wrap procedure incr(signal index : inout index_type) is begin if index = index_type'high then index <= index_type'low; else index <= index + 1; end if; end procedure; begin -- Copy internal signals to output empty <= empty_i; full <= full_i; fill_count <= fill_count_i; -- Set the flags empty_i <= '1' when fill_count_i = 0 else '0'; empty_next <= '1' when fill_count_i <= 1 else '0'; full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0'; full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0'; -- Update the head pointer in write PROC_HEAD : process(clk) begin if rising_edge(clk) then if rst = '1' then head <= 0; else if wr_en = '1' and full_i = '0' then incr(head); end if; end if; end if; end process; -- Update the tail pointer on read and pulse valid PROC_TAIL : process(clk) begin if rising_edge(clk) then if rst = '1' then tail <= 0; rd_valid <= '0'; else rd_valid <= '0'; if rd_en = '1' and empty_i = '0' then incr(tail); rd_valid <= '1'; end if; end if; end if; end process; -- Write to and read from the RAM PROC_RAM : process(clk) begin if rising_edge(clk) then ram(head) <= wr_data; rd_data <= ram(tail); end if; end process; -- Update the fill count PROC_COUNT : process(head, tail) begin if head < tail then fill_count_i <= head - tail + RAM_DEPTH; else fill_count_i <= head - tail; end if; end process; end architecture;
Kode di atas menunjukkan kode lengkap untuk ring buffer FIFO. Anda dapat mengisi formulir di bawah ini untuk menerima file proyek ModelSim serta testbench yang dikirimkan kepada Anda segera.
The testbench
FIFO dibuat instance dalam testbench sederhana untuk mendemonstrasikan cara kerjanya. Anda dapat mengunduh kode sumber testbench bersama dengan proyek ModelSim dengan menggunakan formulir di bawah ini.
Input generik telah diatur ke nilai berikut:
RAM_WIDTH: 16
RAM_DEPTH: 256
Testbench pertama kali mereset FIFO. Setelah reset dilepaskan, testbench menulis nilai berurutan (1-255) ke FIFO sampai penuh. Terakhir, FIFO dikosongkan sebelum tes selesai.
Kita dapat melihat waveform untuk seluruh jalankan testbench dalam gambar di bawah. Sinyal fill_count ditampilkan sebagai nilai analog dalam waveform untuk menggambarkan tingkat isi FIFO dengan lebih baik.
Head, tail, dan jumlah terisi adalah 0 pada awal simulasi. Pada titik di mana sinyal full diaktifkan, head memiliki nilai 255, dan begitu pula sinyal fill_count. Jumlah terisi hanya naik hingga 255 meskipun kita memiliki kedalaman RAM 256. Itu karena kita menggunakan metode keep one open untuk membedakan antara penuh dan kosong, seperti yang kami bahas sebelumnya dalam artikel ini.
Pada titik balik di mana kita berhenti menulis ke FIFO dan mulai membaca darinya, nilai head beku sementara tail dan jumlah terisi mulai menurun. Terakhir, di akhir simulasi ketika FIFO kosong, keduanya head dan tail memiliki nilai 255 sementara jumlah terisi adalah 0.
Testbench ini tidak boleh dianggap memadai untuk apa pun selain demonstrasi. Ini tidak memiliki perilaku self-checking atau logika untuk memverifikasi bahwa output dari FIFO benar pada setiap saat.
Kami akan menggunakan modul ini dalam artikel minggu depan ketika kita mempelajari subjek constrained random verification. Ini adalah strategi pengujian yang berbeda dari tes terarah yang lebih umum digunakan. Singkatnya, testbench akan melakukan interaksi acak dengan DUT (device under test), dan perilaku DUT harus diverifikasi oleh proses testbench terpisah. Terakhir, ketika sejumlah peristiwa yang telah ditentukan telah terjadi, tes selesai.
Klik di sini untuk membaca tulisan lanjutan blog: Constrained random verification
Synthesizing in Vivado
Saya melakukan sintesis ring buffer di Xilinx Vivado karena itu adalah alat implementasi FPGA yang paling populer. Namun, ini harus bekerja pada semua arsitektur FPGA yang memiliki block RAM dual-port.
Kami harus menetapkan beberapa nilai ke input generik untuk dapat mengimplementasikan ring buffer sebagai modul mandiri. Ini dilakukan di Vivado dengan menggunakan menu Settings → General → Generics/Parameters, seperti ditunjukkan dalam gambar di bawah.
Nilai untuk RAM_WIDTH diatur ke 16, yang sama seperti dalam simulasi. Namun, saya telah mengatur RAM_DEPTH ke 2048 karena ini adalah kedalaman maksimum dari primitif RAMB36E1 dalam arsitektur Xilinx Zynq yang saya pilih. Kami bisa memilih nilai yang lebih rendah, tetapi ini masih akan menggunakan jumlah block RAM yang sama. Nilai yang lebih tinggi akan menghasilkan lebih dari satu block RAM yang digunakan.
Gambar di bawah ini menunjukkan penggunaan sumber daya pasca-implementasi, seperti yang dilaporkan oleh Vivado. Ring buffer kami memang telah mengkonsumsi satu block RAM dan sejumlah LUT serta flip-flops.
Ditching the valid signal
Anda mungkin bertanya-tanya apakah penundaan satu siklus clock antara rd_en dan sinyal rd_valid sebenarnya diperlukan. Bagaimanapun, data sudah ada di rd_data ketika kita mengaktifkan sinyal rd_en. Tidak bisakah kita hanya menggunakan nilai ini dan membiarkan ring buffer melompat ke elemen berikutnya pada siklus clock berikutnya saat kita membaca dari FIFO?
Secara ketat, kita tidak memerlukan sinyal valid. Saya menyertakan sinyal ini hanya untuk kenyamanan. Hal yang krusial adalah kita harus menunggu hingga siklus clock setelah kita mengaktifkan sinyal rd_en, jika tidak RAM tidak akan memiliki waktu untuk bereaksi.
Block RAM di FPGA adalah komponen sinkron penuh, mereka membutuhkan tepi clock baik untuk membaca maupun menulis data. Clock baca dan tulis tidak harus berasal dari sumber clock yang sama, tetapi harus ada tepi clock. Selain itu, tidak boleh ada logika antara output RAM dan register berikutnya (flip-flops). Ini karena register yang digunakan untuk meng-clock output RAM ada di dalam primitif block RAM.
Gambar di atas menunjukkan diagram timing bagaimana nilai menyebar dari input wr_data di ring buffer kami, melalui RAM, dan akhirnya muncul pada output rd_data. Karena setiap sinyal di-sampel pada tepi naik clock, memakan waktu tiga siklus clock sejak kita mulai mendorong port tulis sebelum muncul pada port baca. Dan satu siklus clock tambahan berlalu sebelum modul penerima dapat menggunakan data ini.
Reducing the latency
Ada cara untuk mengurangi masalah ini, tetapi ini dengan biaya sumber daya tambahan yang digunakan di FPGA. Mari kita coba eksperimen untuk memotong satu penundaan siklus clock dari port baca ring buffer kami. Dalam cuplikan kode di bawah ini kami telah mengubah output rd_data dari proses sinkron menjadi proses kombinatorial yang sensitif terhadap sinyal ram dan tail.
PROC_READ : process(ram, tail) begin rd_data <= ram(tail); end process;
Sayangnya, kode ini tidak dapat dipetakan ke block RAM karena mungkin ada logika kombinatorial antara output RAM dan register downstream pertama pada sinyal rd_data.
Gambar di bawah ini menunjukkan penggunaan sumber daya seperti yang dilaporkan oleh Vivado. Block RAM telah digantikan oleh LUTRAM; bentuk RAM terdistribusi yang diimplementasikan dalam LUT. Penggunaan LUT melonjak dari 37 LUT menjadi 947. Tabel pencarian dan flip-flop lebih mahal dari block RAM, itulah alasan mengapa kita memiliki block RAM pada pertama kalinya.
Ada banyak cara untuk mengimplementasikan ring buffer FIFO di block RAM. Anda mungkin dapat menghemat penundaan siklus clock tambahan dengan menggunakan desain lain, tetapi ini akan dengan biaya logika pendukung tambahan. Untuk sebagian besar aplikasi, ring buffer yang disajikan dalam artikel ini akan cukup.
Update: How to create a ring buffer FIFO in block RAM using the AXI ready/valid handshake
Dalam tulisan blog berikutnya, kami akan membuat testbench yang lebih baik untuk modul ring buffer dengan menggunakan constrained random verification.
Klik di sini untuk membaca tulisan lanjutan blog: Constrained random verification