Dasar-dasar FPGA HDL - TeachMeSoft

Dasar-dasar FPGA HDL



Array Gerbang Programmable Lapangan, singkatnya, FPGA adalah cara yang relatif lama untuk membuat perangkat keras khusus yang menghilangkan biaya yang terkait dengan pengecoran silikon. Sayangnya sebagian besar kompleksitas desain chip masih ada dan ini adalah alasan mengapa kebanyakan orang lebih suka menggunakan chip rak, sering menerima keterbatasan mereka, daripada mengambil tantangan untuk memiliki desain yang dioptimalkan, efisien dengan perangkat keras yang mereka butuhkan. .

Seperti yang terjadi pada Perangkat Lunak, di mana ada banyak perpustakaan yang dapat Anda mulai, juga untuk FPGA ada "perpustakaan" yang disebut blok IP, namun ini biasanya cukup mahal dan tidak memiliki antarmuka "plug and play" standar yang menyebabkan sakit kepala saat mengintegrasikan semuanya dalam suatu sistem. Apa yang Arduino coba lakukan dengan memperkenalkan FPGA dalam lini produknya adalah untuk mengambil keuntungan dari fleksibilitas perangkat keras yang dapat diprogram khususnya untuk menyediakan serangkaian periferal yang dapat diperpanjang untuk mikrokontroler yang menghilangkan sebagian besar kerumitan. Tentu saja untuk mencapai ini, perlu menerapkan beberapa batasan dan menetapkan cara standar untuk menghubungkan blok sehingga dapat dilakukan secara otomatis.

Langkah pertama adalah mendefinisikan satu set antarmuka standar yang harus benar-benar menanggapi seperangkat aturan yang diberikan tetapi sebelum menyelami ini, penting untuk menentukan antarmuka apa yang mungkin kita butuhkan. Karena kita berinteraksi dengan mikrokontroler, port pertama yang perlu kita tentukan adalah bus untuk menghubungkan prosesor dengan periferal. Bus semacam itu setidaknya harus ada dalam citarasa master dan slave di mana sinyalnya sama tetapi dengan arah terbalik. Untuk beberapa detail tambahan tentang bus dan arsitektur master / slave silakan periksa dokumen ini .

Antarmuka kedua, yang penting tetapi tidak dapat distandarisasi adalah sinyal input / output yang terhubung ke dunia eksternal. Di sini kita tidak dapat mendefinisikan standar karena setiap blok akan menyediakan set sinyal sendiri namun kita hanya dapat menggabungkan satu set sinyal dalam kelompok yang kita sebut saluran.

Akhirnya ada antarmuka kelas tiga yang mungkin berguna yang membawa data streaming. Dalam hal ini kami ingin mentransfer aliran data yang berkelanjutan tetapi juga ingin dapat menghentikan sementara aliran jika blok penerima tidak dapat memprosesnya, maka bersama dengan data kami juga memerlukan beberapa jenis sinyal kontrol aliran yang cukup mirip terjadi pada UART.

Karena kami ingin menstandarisasi sedikit juga pada keterbacaan kami juga ingin menetapkan beberapa konvensi pengkodean. Di sini tentu saja ada banyak agama yang berbeda yang mengatur spasi / tab, notasi dan sebagainya, jadi kami memilih yang kami sukai ...

Berbicara tentang agama, pada akhirnya kami juga berbicara tentang bahasa ... kami lebih suka (Sistem) Verilog daripada VHDL dan sebagian besar blok IP kami diberi kode. Alasan pilihan kami adalah bahwa Verilog secara umum lebih mirip dengan C dan juga memungkinkan konstruksi yang sangat bagus yang memfasilitasi pembuatan blok parametrik.

Daftar Isi

  • Konvensi Pengkodean
  • Prototipe antarmuka
    • Lightweight Bus (Bus Ringan)
    • Bus Pipelined
    • Antarmuka streaming
  • Struktur modul Verilog (Sistem)
  • Paralelisme dan diutamakan
  • Contoh dunia nyata
  • Contoh sederhana lainnya

Konvensi Pengkodean


Kami menggunakan awalan di depan setiap entitas yang dideklarasikan sehingga mengidentifikasi jenisnya, nama variabel sepenuhnya huruf besar dan beberapa kata dipisahkan oleh garis bawah. Khususnya:


    AwalanDeskripsi
    wKawat, untuk semua sinyal kombinatorial, misalnya wDATA. Biasanya ditentukan dengan arahan kawat
    rReg, untuk semua sinyal berurutan, misalnya rSHIFTER. Biasanya ditentukan dengan arahan reg
    iInput, untuk semua sinyal input dalam deklarasi modul, misalnya iCLK. Biasanya ditentukan dengan arahan input
    oOutput, untuk semua sinyal output dalam deklarasi modul, misalnya OREAD. Biasanya ditentukan dengan arahan keluaran
    bDua arah, untuk semua sinyal keluar dalam deklarasi modul, misalnya bSDA. Biasanya didefinisikan dengan InOut direktif
    pParameter, untuk semua parameter yang dapat digunakan untuk parametrize blok, misalnya pCHANNELS. Biasanya didefinisikan dengan arahan param
    cKonstan, untuk semua definisi yang konstan atau merupakan nilai turunan dan tidak dapat langsung digunakan untuk menentukan parameter blok. Misalnya cCHANNEL_BITS. Biasanya didefinisikan dengan arahan localparam
    eDihitung, untuk semua kemungkinan nilai konstan yang digunakan oleh satu atau lebih sinyal atau register. Misalnya keadaan mesin negara dapat didefinisikan sebagai eSTATE. Biasanya ditentukan dengan arahan enum

    • Kami lebih suka spasi daripada tab! Alasannya adalah bahwa terlepas dari kode ukuran tab selalu terlihat bagus.
    • Lekukan diatur ke dua spasi.
    • Blok pernyataan bersyarat harus selalu memiliki konstruksi awal / akhir bahkan jika mereka memiliki satu pernyataan di dalamnya dan mulai / berakhir harus berada pada baris yang sama dari if / else
    • Sinyal milik kelompok yang sama harus berbagi awalan yang sama


    Prototipe antarmuka


    Lightweight Bus (Bus Ringan)
    Bus untuk menyambungkan periferal. Dengan konvensi, bus data adalah 32 bit sedangkan bus alamat lebar variabel, berdasarkan jumlah register yang diekspos. Bus membutuhkan serangkaian sinyal berikut:


    SinyalDesciptionLebarDeskripsi
    MasterSlave
    ALAMATHAIsayavar.Mendaftar alamat, menentukan lebar
    BACAHAIsaya1Baca strobo
    READ_DATAsayaHAI32Data sedang dibaca
    MENULISHAIsaya1Menulis strobo
    WRITE_DATAHAIsaya32Data untuk ditulis di alamat yang diberikan
    BYTE_ENABLEHAIsaya4Sinyal opsional untuk menandai byte kata 32 bit mana yang sebenarnya akan ditulis
    WAIT_REQUESTsayaHAI1Sinyal opsional untuk menandai periferal sedang sibuk. Membaca dan menulis strobe akan dianggap sah hanya jika sinyal ini tidak ditegaskan.
    Dengan konvensi dalam siklus tulis, ADDRESS dan WRITE_DATA terkunci dalam siklus clock yang sama dari strobo WRITE. Sebaliknya, dalam siklus baca READ_DATA disajikan oleh perangkat pada siklus segera setelah strobo READ, yang juga akan menunjukkan ADDRESS sedang dibaca.


    Pipelined Bus
    Bus untuk menghubungkan blok kompleks yang dapat menangani lebih dari satu perintah pada waktu dan merespons dalam waktu variabel untuk permintaan. Bus ini memperluas bus Ringan dengan menambahkan sinyal berikut:

    Perilaku ini juga disebut sebagai latensi pembacaan 1 jam dan pada dasarnya berarti bahwa sementara periferal masih dapat memiliki jumlah jam variabel untuk merespons operasi BACA atau MENULIS menggunakan sinyal WAIT_REQUEST opsional, ini akan mengunci master mencegahnya melakukan operasi lain. Dengan cara ini dapat dianggap mirip dengan menggunakan loop sibuk dalam pemrograman versus penundaan yang menghasilkan OS untuk melakukan multitasking.


    SinyalDescriptionLebarDeskripsi
    MasterSlave
    BURST_COUNT0Ivar.Jumlah operasi berurutan yang harus dilakukan
    READ_DATAVALIDI01Periferal menggunakan sinyal ini untuk menandai ketika data disediakan untuk dikuasai. Dapat ditegaskan dengan penundaan apa pun dan tidak ada jaminan tentang keberlangsungan. Operasi baca dengan ukuran burst 4 akan menyatakan 4 kali READ_DATAVALID per setiap strobo BACA
    Keuntungan utama dari pendekatan ini adalah bahwa master dapat berkomunikasi dengan budak maksud membaca atau menulis beberapa data untuk setiap transaksi. Baik untuk membaca maupun menulis, sebenarnya sinyal BURST_COUNT memberi tahu perangkat berapa lama transaksi akan berlangsung.

    Budak akan menyatakan WAIT_REQUEST hingga siap menerima operasi. Dalam hal penulisan, BURST_COUNT dan ADDRESS disampel hanya pada strobo pertama, setelah itu periferal akan mengharapkan strobo WRITE ditegaskan untuk jumlah kata yang diminta dan secara otomatis akan menambah alamat. Untuk operasi baca, satu strobo READ, dilakukan ketika WAIT_REQUEST tidak ditegaskan, akan memberi tahu periferal untuk membaca BURST_COUNT kata yang akan dikembalikan dengan menyatakan READ_DATAVALID untuk jumlah kata yang diminta. Setelah operasi baca telah dimulai, tergantung pada periferal untuk menerima atau tidak lebih banyak operasi, tetapi secara umum seharusnya dimungkinkan untuk memiliki setidaknya dua operasi bersamaan untuk mendapat manfaat dari Pipelined Bus.


    Antarmuka streaming
    Segera akan datang



    Struktur modul Verilog (Sistem)


    Sebuah SystemVerilog modul deklarasi dapat dilakukan dengan beberapa cara tapi yang kita kebanyakan lebih suka adalah bentuk di mana Anda dapat menggunakan parameter sehingga input blok dapat disesuaikan pada waktu kompilasi. Ini akan terlihat seperti:

    module COUNTER #(
      pWIDTH=8
    ) (
      input                   iCLK,
      input                   iRESET,
      output reg [pWIDTH-1:0] oCOUNTER
    );
    
    endmodule
    
    Di sini kita hanya mendefinisikan prototipe modul dan mendefinisikan port input / output, sekarang kita harus menambahkan beberapa logika yang berguna dengan menambahkan beberapa kode antara header modul dan pernyataan endmodule.

    Karena kita mulai dengan contoh penghitung, mari kita lanjutkan dengan itu dan menulis beberapa kode yang benar-benar mengimplementasikannya:

    module COUNTER #(
      pWIDTH=8
    ) (
      input               iCLK,
      input               iRESET,
      output [pWIDTH-1:0] oCOUNTER
    );
    
    always @(posedge iCLK)
    begin
      if (iRESET) begin
        oCOUNTER<=0;
      end else begin
        oCOUNTER<= oCOUNTER+1;
      end
    end
    
    endmodule
    
    Kode di atas cukup jelas ... di setiap tepi jam positif, jika kita melihat input tinggi iRESET kita mereset penghitung, jika tidak kita menambahnya dengan satu ... perhatikan bahwa memiliki sinyal reset mengembalikan blok kita ke keadaan yang diketahui sering berguna tetapi tidak selalu dibutuhkan.

    Sekarang ... ini menarik tetapi kami melakukan sesuatu yang agak rumit ... kami menyatakan oCOUNTER sebagai keluaran reg, yang berarti kami mengatakan ini bukan hanya sekelompok kabel tetapi memiliki memori. Dengan cara ini kita dapat menggunakan <= tugas yang "terdaftar" yang berarti bahwa tugas tersebut akan disimpan selama siklus jam berikutnya.

    Cara lain yang bisa kita lakukan adalah menghapus pernyataan reg dalam deklarasi modul dan mendefinisikan penghitung sebagai berikut:

    module COUNTER #(
      pWIDTH=8
    ) (
      input               iCLK,
      input               iRESET,
      output [pWIDTH-1:0] oCOUNTER
    );
    
    reg [pWIDTH-1:0] rCOUNTER;
    
    always @(posedge iCLK)
    begin
      if (iRESET) begin
        rCOUNTER<=0;
      end else begin
        rCOUNTER<= rCOUNTER+1;
      end
    end
    
    assign oCOUNTER=rCOUNTER;
    
    endmodule
    
    Ini pada dasarnya adalah hal yang sama tetapi kami mendefinisikan register, mengerjakannya dan kemudian ditugaskan dengan "kontinu" = menugaskannya ke sinyal output. Perbedaannya di sini adalah bahwa sementara <= berarti sinyal hanya berubah pada clock edge = memberikan nilai terus menerus sehingga sinyal pada akhirnya akan berubah setiap saat, namun jika kita menetapkannya seperti yang kita lakukan dalam contoh ke register yang hanya berubah pada Jam tepi sinyal yang dihasilkan pada dasarnya hanya sebuah alias.

    Tugas yang menarik, seperti pernyataan lain dalam bahasa deskripsi perangkat keras adalah paralel, yang berarti bahwa urutannya dalam kode tidak begitu relevan karena semuanya dieksekusi secara paralel sehingga kami dapat menugaskan oCOUNTER ke rCOUNTER juga sebelum blok selalu. Kami akan kembali ke sini karena tidak sepenuhnya benar bahwa pesanan tidak masalah ...

    Penggunaan lain yang menarik dari penugasan kontinu adalah kemungkinan untuk membuat persamaan logika. Misalnya kita bisa menulis ulang penghitung dengan cara berikut:

    module COUNTER #(
      pWIDTH=8
    ) (
      input               iCLK,
      input               iRESET,
      output [pWIDTH-1:0] oCOUNTER
    );
    
    reg [pWIDTH-1:0] rCOUNTER;
    wire [pWIDTH-1:0] wNEXT_COUNTER;
    
    assign wNEXT_COUNTER = rCOUNTER+1;
    assign oCOUNTER = rCOUNTER;
    
    always @(posedge iCLK)
    begin
      if (iRESET) begin
        rCOUNTER<=0;
      end else begin
        rCOUNTER<= wNEXT_COUNTER;
      end
    end
    
    endmodule
    
    Kami pada dasarnya masih melakukan hal yang sama tetapi kami telah melakukannya dengan cara yang membuatnya sedikit lebih jelas secara logis. Pada dasarnya kami menetapkan secara kontinyu sinyal wNEXT_COUNTER dengan nilai rCOUNTER plus satu. Ini berarti bahwa wNEXT_COUNTER akan (hampir) segera berubah segera setelah rCOUNTER mengubah nilai namun rCOUNTER akan diperbarui hanya pada edge clock positif berikutnya (karena memiliki <= penugasan) sehingga hasilnya tetap bahwa rCOUNTER hanya berubah pada edge clock.


    Paralelisme dan diutamakan


    Seperti yang kami tulis pada bab sebelumnya, semua bahasa deskripsi perangkat keras memiliki konsep pernyataan paralel yang berarti bahwa berbeda dengan bahasa pemrograman perangkat lunak yang mengeksekusi instruksi secara berurutan, di sini semua instruksi dieksekusi pada waktu yang sama. Sebagai contoh jika kita menulis sebuah blok yang memiliki kode di bawah ini kita akan melihat register berubah bersama di tepi jam tertentu:

    reg [pWIDTH-1:0] rCOUNT_UP, rCOUNT_DOWN;
    
    always @(posedge iCLK)
    begin
      if (iRESET) begin
        rCOUNT_UP<=0;
        rCOUNT_DOWN<=0;
      end else begin
        rCOUNT_UP<= rCOUNT_UP+1;
        rCOUNT_DOWN<= rCOUNT_DOWN-1;
      end
    end
    
    Tentu saja jika semuanya dieksekusi secara paralel, kita perlu memiliki cara untuk mengurutkan pernyataan yang dapat dilakukan dengan membuat mesin keadaan sederhana. Mesin keadaan adalah sistem yang menghasilkan keluaran berdasarkan input DAN keadaan internalnya. Dalam arti counter kami sudah menjadi mesin negara karena kami memiliki output (oCOUNTER) yang berubah berdasarkan keadaan mesin sebelumnya (rCOUNTER), namun mari kita lakukan sesuatu yang lebih menarik dan buat mesin negara yang menciptakan denyut nadi yang diberikan panjang ketika kita memulainya. Mesin akan memiliki tiga status: eST_IDLE, eST_PULSE_HIGH dan eST_PULSE_LOW. Di eST_IDLE kami akan mencicipi perintah input dan ketika itu diterima, kami beralih ke eST_PULSE_HIGH, tempat kami akan tinggal selama jumlah jam yang diberikan, yang akan kami parametrize dengan pHIGH_COUNT,


    module PULSE_GEN #(
      pWIDTH=8,
      pHIGH_COUNT=240,
      pLOW_COUNT=40 
    ) (
      input       iCLK,
      input       iRESET,
      input       iPULSE_REQ,
      output reg  oPULSE
    );
    
    reg [pWIDTH-1:0] rCOUNTER;
    enum reg [1:0] {
      eST_IDLE,
      eST_PULSE_HIGH,
      eST_PULSE_LOW
    } rSTATE;
    
    always @(posedge iCLK)
    begin
      if (iRESET) begin
        rSTATE<=eST_IDLE;
      end else begin
        case (rSTATE)
          eST_IDLE: begin
            if (iPULSE_REQ) begin
              rSTATE<= eST_PULSE_HIGH;
              oPULSE<= 1;
              rCOUNTER <= pHIGH_COUNT-1;
            end
          end
          eST_PULSE_HIGH: begin
            rCOUNTER<= rCOUNTER-1;
            if (rCOUNTER==0) begin
              rSTATE<= eST_PULSE_LOW;
              oPULSE<= 0;
              rCOUNTER<= pLOW_COUNT-1;
            end
          end
          eST_PULSE_LOW: begin
            rCOUNTER<= rCOUNTER-1;
            if (rCOUNTER==0) begin
              rSTATE<= eST_IDLE;
            end
          end
        endcase
      end
    end
    
    endmodule
    
    Di sini kita melihat sejumlah hal baru yang perlu kita bicarakan. Pertama-tama kita mendefinisikan variabel rSTATE menggunakan enum. Ini membantu dalam menetapkan status ke nilai-nilai yang mudah dimengerti daripada angka-angka yang dikode keras dan memiliki keuntungan bahwa Anda dapat memasukkan status dengan mudah tanpa perlu menulis ulang semua mesin status Anda.

    Kedua kami memperkenalkan blok case / endcase yang memungkinkan kami untuk mendefinisikan perilaku yang berbeda tergantung pada keadaan sinyal. Sintaksnya sangat mirip dengan C sehingga harus akrab bagi sebagian besar pembaca. Penting untuk dicatat bahwa pernyataan dalam berbagai blok kasus masih akan dieksekusi secara paralel tetapi karena mereka dikondisikan oleh nilai-nilai berbeda dari variabel yang diperiksa, hanya satu per satu waktu yang akan diaktifkan. Melihat kasing eST_IDLE kita melihat bahwa kita tetap berada dalam keadaan selamanya sampai kita merasakan iPULSE_REQ menjadi tinggi, dalam hal ini kita mengubah keadaan, mengatur ulang penghitung ke periode keadaan tinggi dan mulai mengeluarkan pulsa.

    Perhatikan bahwa sejak oPULSE terdaftar, ia akan mempertahankan statusnya hingga ditetapkan lagi. Dalam keadaan selanjutnya, hal-hal menjadi sedikit lebih rumit ... pada setiap jam kita mengurangi penghitung, maka jika penghitung mencapai 0 kita juga mengubah keadaan, ubah oPULSE menjadi 0 DAN kita tetapkan rCOUNTER lagi. Karena kedua penugasan dieksekusi secara paralel, kita perlu tahu apa artinya ini dan cukup beruntung semua mandat HDL bahwa jika dua pernyataan paralel dieksekusi pada register yang sama hanya yang terakhir akan benar-benar dieksekusi sehingga makna dari apa yang baru saja kita tulis adalah bahwa biasanya kami mengurangi penghitung tetapi ketika penghitung mencapai 0 kami mengubah status dan menginisialisasi ulang ke pLOW_COUNT.

    Pada titik ini apa yang terjadi di eST_PULSE_LOW menjadi cukup jelas karena kami baru saja mengurangi penghitung dan kembali ke eST_IDLE segera setelah mencapai 0. Perhatikan bahwa ketika kami kembali ke eST_IDLE rCOUNTER dikurangi lagi sehingga hasilnya adalah rCOUNTER akan menjadi 0xff ( atau -1) di eST_IDLE tetapi kami tidak terlalu peduli karena kami akan mengatur ulang ke nilai yang tepat ketika kami menerima iPULSE_REQ.

    Meskipun kami juga dapat mengatur ulang rCOUNTER ketika keluar dari eST_PULSE_LOW, dalam HDL selalu lebih baik untuk melakukan hanya apa yang benar-benar diperlukan karena apa pun yang lebih banyak akan menghabiskan sumber daya dan membuat perangkat keras kami lebih lambat. Pada awalnya ini mungkin tampak berbahaya tetapi dengan beberapa pengalaman akan menjadi mudah untuk melihat bagaimana ini dapat membantu. Konsep yang sama ini berlaku untuk mereset logika. Kecuali jika itu benar-benar diperlukan, tergantung bagaimana penerapannya, ia dapat menghabiskan sumber daya dan memperburuk kecepatan sistem sehingga harus digunakan dengan hati-hati.


    Contoh dunia nyata


    Sekarang mari kita selami contoh dunia nyata dari perangkat sederhana yang kita gunakan di Vidor, PWM. Apa yang ingin kami capai adalah membuat blok kecil dengan beberapa output PWM dengan kemungkinan untuk menentukan fase relatif dari masing-masing saluran PWM.

    Untuk melakukannya kita perlu penghitung dan beberapa pembanding yang memberi tahu kita ketika penghitung di atas diberi nilai sehingga kita dapat beralih keluaran. Karena kita juga ingin agar frekuensi PWM dapat diprogram, kita perlu memiliki penghitung yang beroperasi pada frekuensi yang berbeda dari basis yang kita gunakan untuk sistem kita sehingga periode itu persis seperti yang kita butuhkan. Untuk melakukannya, kami menggunakan prescaler yang pada dasarnya adalah penghitung lain yang membagi jam dasar ke nilai yang lebih rendah dengan cara yang mirip dengan generator baud rate yang digunakan dalam UART .

    Sekarang mari kita lihat kodenya:

    module PWM #(
      parameter pCHANNELS=16,
      parameter pPRESCALER_BITS=32,
      parameter pMATCH_BITS=32
    
    ) 
    (
      input                              iCLK,
      input                              iRESET,
    
      input [$clog2(2*pCHANNELS+2)-1:0]  iADDRESS,
      input [31:0]                       iWRITE_DATA,
      input                              iWRITE,
    
      output reg [pCHANNELS-1:0]         oPWM
    );
    
    // register declaration  
    reg [pPRESCALER_BITS-1:0] rPRESCALER_CNT;
    reg [pPRESCALER_BITS-1:0] rPRESCALER_MAX;
    
    reg [pMATCH_BITS-1:0] rPERIOD_CNT;
    reg [pMATCH_BITS-1:0] rPERIOD_MAX;
    reg [pMATCH_BITS-1:0] rMATCH_H [pCHANNELS-1:0];
    reg [pMATCH_BITS-1:0] rMATCH_L [pCHANNELS-1:0];
    reg rTICK;
    
    integer i;
    
    always @(posedge iCLK)
    begin
      // logic to interface with bus.
      // register map is as follows:
      // 0: prescaler value
      // 1: PWM period
      // even registers >=2: value at which PWM output is set high
      // odd registers >=2: value at which PWM output is set low
      if (iWRITE) begin
        // the following statement is executed only if address is >=2. case on iADDRESS[0]
        // selects if address is odd (iADDRESS[0]=1) or even (iADDRESS[0]=0)
        if (iADDRESS>=2) case (iADDRESS[0])
          0: rMATCH_H[iADDRESS[CLogB2(pCHANNELS):1]-1]<= iWRITE_DATA;
          1: rMATCH_L[iADDRESS[CLogB2(pCHANNELS):1]-1]<= iWRITE_DATA;
        endcase
        else begin
          // we get here if iADDRESS<2
         case (iADDRESS[0])
          0: rPRESCALER_MAX<=iWRITE_DATA;
          1: rPERIOD_MAX<=iWRITE_DATA;
        endcase
        end
      end
    
      // prescaler is always incrementing       
      rPRESCALER_CNT<=rPRESCALER_CNT+1;
      rTICK<=0;
      if (rPRESCALER_CNT>= rPRESCALER_MAX) begin
        // if prescaler is equal or greater than the max value
        // we reset it and set the tick flag which will trigger the rest of the logic
        // note that tick lasts only one clock cycle as it is reset by the rTICK<= 0 above
        rPRESCALER_CNT<=0;
        rTICK <=1;
      end
      if (rTICK) begin
        // we get here each time rPRESCALER_CNT is reset. from here we increment the PWM
        // counter which is then clocked at a lower frequency.
        rPERIOD_CNT<=rPERIOD_CNT+1;
        if (rPERIOD_CNT>=rPERIOD_MAX) begin
          // and of course we reset the counter when we reach the max period.
          rPERIOD_CNT<=0;
        end
      end
    
      // this block implements the parallel comparators that actually generate the PWM outputs
      // the for loop actually generates an array of logic that compares the counter with
      // the high and low match values for each channel and set the output accordingly.
      for (i=0;i<pCHANNELS;i=i+1) begin
        if (rMATCH_H[i]==rPERIOD_CNT)
          oPWM[i] <=1;
        if (rMATCH_L[i]==rPERIOD_CNT)
          oPWM[i] <=0;
      end
    end
    
    endmodule
    
    Ada beberapa hal baru di sini untuk dipelajari, jadi mari kita mulai dari deklarasi modul. Di sini kita menggunakan fungsi bawaan untuk menetapkan lebar bit yang diperlukan dari bus alamat. Tujuannya adalah untuk membatasi rentang alamat ke minimum yang diperlukan untuk register, jadi misalnya jika kita ingin 10 saluran, kita membutuhkan total 22 alamat. Karena setiap bit alamat menggandakan jumlah alamat yang dapat kita gunakan, kita membutuhkan total 5 bit yang menghasilkan 32 alamat total. Untuk membuat parametrik ini, kami mendefinisikan lebar iADDRESS sebagai $ clog2 (2 * pCHANNELS + 2) dan kami mendefinisikan register sebagai array 2 dimensi.

    Sebenarnya ada dua cara untuk membuat array multidimensi dan di sini kita menggunakan array "unpacked", yang pada dasarnya mendefinisikan register sebagai entitas terpisah dengan menambahkan indeks di sisi kiri deklarasi register. Cara lain, kita tidak menggunakan dalam contoh ini adalah yang "dikemas" di mana indeks berada di sisi kiri deklarasi dan hasilnya adalah bahwa array 2D juga dapat dilihat sebagai register besar tunggal yang berisi rangkaian dari semua register.

    Trik lain yang menarik di sini adalah bagaimana kita mendefinisikan logika yang menangani register. Pertama-tama kami hanya menerapkan register tulis saja sehingga Anda tidak akan menemukan sinyal iREAD dan iREAD_DATA Kedua, kami ingin memiliki set register parametrik di mana hanya dua register pertama yang selalu ada sedangkan sisanya didefinisikan dan ditangani secara dinamis berdasarkan pada jumlah saluran yang ingin kami implementasikan. Untuk melakukannya, kita perhatikan bahwa dalam bilangan biner, bit paling tidak penting menentukan apakah bilangan tersebut ganjil atau genap. Karena kami memiliki dua register per saluran, ini berguna karena kami dapat membedakan perilaku kami tergantung pada apakah kami di bawah alamat 2 atau tidak.

    Jika kami di bawah alamat 2, kami menerapkan register umum yang merupakan hitungan prescaler dan periode penghitung. Jika kita di atas 2 kita menggunakan LSB untuk memutuskan apakah kita menulis nilai untuk komparator tinggi atau rendah.


    Contoh sederhana lainnya


    Contoh sederhana lain yang dapat kita pelajari adalah encoder quadrature. Meskipun mungkin tampak lebih sederhana daripada PWM itu juga mengatasi beberapa tantangan yang tidak sepele. Masalah pertama yang kami temui berurusan dengan sinyal dari dunia eksternal adalah bahwa kami tidak memiliki jaminan mereka sinkron dengan jam internal kami sehingga kami dapat menemukan fenomena yang disebut metastabilitas yang menyebabkan data dalam register tidak terdefinisi dan berpotensi berubah selama siklus clock. Alasan untuk ini adalah bahwa jika data berubah pada input register ketika kita mengunci itu register mungkin masuk ke keadaan tidak stabil yang dapat "membusuk" menjadi 1 atau 0 setiap saat.

    Hal lain yang menarik yang kami lakukan di sini adalah bahwa kami menggunakan penugasan berkelanjutan untuk menentukan strobo dan arah dari sinyal quadrature keluar dari enkoder. Dalam kode ada grafik sederhana yang menunjukkan bagaimana bentuk gelombang, walaupun untuk memahami sepenuhnya bagaimana bentuk gelombang, Anda harus mempertimbangkan bahwa persamaan menggunakan sinyal pada titik yang berbeda dalam waktu. Hal ini dilakukan dengan hanya menggunakan register geser yang digunakan untuk menyinkronkan input asinkron juga untuk menunda mereka sehingga mengetuk ke titik yang berbeda dari register geser kita dapat melihat bagaimana sinyal adalah jam sebelumnya. Secara khusus, jika kita bergerak menuju input register geser, kita mendapatkan data "yang lebih baru" sedangkan jika kita bergerak ke arah akhir kita mendapatkan data "yang lebih tua".

    Melihat bentuk gelombang kita melihat bahwa strobo menghasilkan pulsa setiap kali A atau B memiliki tepi dan ini dilakukan dengan hanya xoring setiap sinyal dengan versi tertunda. Sinyal arahnya agak sedikit lebih rumit tetapi kami melihat bahwa itu adalah konstan baik 0 atau 1 ketika sinyal strobo tinggi tergantung pada arah yang berputar encoder. Sebenarnya kita melihat ada pulsa pada sinyal arah tetapi ini tidak bersamaan dengan strobo sehingga mereka akan diabaikan.

    Satu hal yang mungkin tidak terlihat jelas pada pandangan pertama adalah bahwa persamaannya secara parsial menghitung logika yang sama untuk semua input, pada kenyataannya register rRESYNC_ENCODER dikemas dengan susunan dua dimensi yang diatur sehingga indeks pertama mengidentifikasi ketukan pada register geser dan yang kedua. index adalah saluran encoder. Ini berarti bahwa setiap kali kami mereferensikan rRESYNC_ENCODER dengan indeks tertentu, kami memilih array mono-dimensi yang berisi semua input encoder sekaligus ditunda oleh jumlah jam yang ditentukan oleh indeks. Ini juga berarti bahwa ketika kita melakukan operasi logika bitwise pada array kita sebenarnya instantiating beberapa persamaan logika paralel pada saat yang sama.

    Seperti yang kami lakukan untuk contoh lain, blok mengimplementasikan beberapa input dan melakukan ini dengan menggunakan for loop yang memeriksa sinyal aktif (yang lagi-lagi adalah array selebar jumlah saluran) dan ketika itu tinggi ia memeriksa arah dan berdasarkan itu menambah atau mengurangi penghitung untuk saluran itu. Ini mudah dilakukan dengan menggunakan? : operator (ekspresi kondisional) yang bekerja persis seperti di C.

    Akhirnya antarmuka bus cukup sederhana karena satu-satunya register yang kita miliki adalah read-only counters dan kita dapat mengimplementasikannya hanya dengan memeriksa sinyal read dan menugaskan data output dengan array counter yang diindeks oleh alamat, cukup mirip dengan RAM. .

    module QUAD_ENCODER #(
        pENCODERS=2,
        pENCODER_PRECISION=32
      )(
        input                             iCLK,
        input                             iRESET,
        // AVALON SLAVE INTERFACE
        input  [$clog2(pENCODERS)-1:0]   iAVL_ADDRESS,
        input                             iAVL_READ,
        output reg [31:0]                 oAVL_READ_DATA,
        // ENCODER INPUTS
        input [pENCODERS-1:0]             iENCODER_A,
        input [pENCODERS-1:0]             iENCODER_B
      );
    
    
    // bidimensional arrays containing encoder input states at 4 different points in time
    // the first two delay taps are used to synchronize inputs with the internal clocks
    // while the other two are used to compare two points in time of those signals.
    reg [3:0][pENCODERS-1:0] rRESYNC_ENCODER_A,rRESYNC_ENCODER_B;
    
    // bidimensional arrays containing the counters for each channel
    reg [pENCODERS-1:0][pENCODER_PRECISION-1:0] rSTEPS;
    
    // encoder decrementing
    // A       __----____----__
    // B       ____----____----
    // ENABLE  __-_-_-_-_-_-_-_
    // DIR     __---_---_---_--
    //     
    // encoder incrementing
    // A       ____----____----
    // B       __----____----__
    // ENABLE  __-_-_-_-_-_-_-_
    // DIR     ___-___-___-___-
    
    wire [pENCODERS-1:0] wENABLE =  rRESYNC_ENCODER_A[2]^rRESYNC_ENCODER_A[3]^rRESYNC_ENCODER_B[2]^rRESYNC_ENCODER_B[3];
    wire [pENCODERS-1:0] wDIRECTION = rRESYNC_ENCODER_A[2]^rRESYNC_ENCODER_B[3];
    
    integer i;
    
    initial rSTEPS <=0;
    
    always @(posedge iCLK)
    begin
      if (iRESET) begin
        rSTEPS<=0;
        rRESYNC_ENCODER_A<=0;
        rRESYNC_ENCODER_B<=0;
      end
      else begin
        // implement shift registers for each channel. since arrays are packed we can treat that as a monodimensional array
        // and by adding inputs at the bottom we are effectively shifting data by one bit
        rRESYNC_ENCODER_A<={rRESYNC_ENCODER_A,iENCODER_A};
        rRESYNC_ENCODER_B<={rRESYNC_ENCODER_B,iENCODER_B};
    
        for (i=0;i<pENCODERS;i=i+1)
        begin
          // if strobe is high..
          if (wENABLE[i])
            // increment or decrement based on direction
            rSTEPS[i] <= rSTEPS[i]+ ((wDIRECTION[i]) ? 1 : -1);
        end
        // if slave interface is being read...
        if (iAVL_READ)
        begin
          // return the value of the counter indexed by the address
          oAVL_READ_DATA<= rSTEPS[iAVL_ADDRESS];
        end
      end
    end
    
    endmodule
    
    Ini mungkin merupakan contoh yang bagus tentang bagaimana elegan dan ringkas dapat menjadi deskripsi perangkat keras untuk desain seperti saat kami mendeskripsikan desain yang sangat parametrik di mana kami dapat mengubah kedalaman penghitung dan jumlah saluran dan kode meningkat sesuai dengan itu menghasilkan semua logika terkait dalam sangat mudah dibaca cara. Tentu saja ada beberapa cara berbeda untuk melakukan hal yang sama dan ini salah satu yang paling ringkas bahwa pada saat yang sama membutuhkan sedikit lebih banyak pemahaman tentang kemampuan (sistem) Verilog.


    SUMBER

    • [1] https://www.arduino.cc/en/Tutorial/VidorHDL


      Disqus comments