VidorHDL

Saha Programlanabilir Kapı Dizileri, kısaca FPGA'lar , silikon dökümhaneleriyle ilgili maliyetleri ortadan kaldıran özel bir donanım oluşturmanın nispeten eski bir yoludur. Ne yazık ki, çip tasarımının karmaşıklığının çoğu hala var ve çoğu insan, tam olarak ihtiyaç duydukları donanımla tam olarak optimize edilmiş, verimli bir tasarıma sahip olmak yerine zorluklarını kabul etmek yerine raf yongalarını kullanmayı tercih ediyor. .

Başlayabileceğiniz çok sayıda kütüphanenin olduğu Yazılımda olduğu gibi, FPGA'lar için de IP blokları adı verilen “kütüphaneler” vardır, ancak bunlar genellikle oldukça pahalıdır ve standartlaştırılmış bir “tak ve çalıştır” arabiriminden yoksundur. sistemdeki her şey. Arduino'nun ürün yelpazesinde FPGA'ları tanıtmaya çalıştığı şey, özellikle karmaşıklığın çoğunu alan mikrodenetleyiciler için genişletilebilir bir çevre birimi seti sağlamak üzere programlanabilir donanımın esnekliğinden yararlanmaktır. Elbette bunu başarmak için bazı sınırlamalar uygulamak ve blokları birbirine bağlamak için standart bir yol tanımlamak gerekir, böylece otomatik olarak yapılabilir.

İlk adım, belirli bir kurallar kümesine kesin olarak cevap vermesi gereken bir dizi standart arayüz tanımlamaktır, ancak buna dalmadan önce ne tür arayüzlere ihtiyacımız olabileceğini tanımlamak önemlidir. Bir mikrodenetleyici ile arayüz oluşturduğumuz için tanımlamamız gereken ilk port, işlemciyi çevre birimleri ile bağlamak için bir veriyoludur. Bu tür bir veri yolu en azından sinyallerin aynı olduğu fakat ters yönlerde olduğu ana ve bağımlı tatlarda bulunmalıdır. Otobüsler ve master / slave mimarisi hakkında ek ayrıntılar için lütfen bu belgeye bakın .

Önemli ancak standartlaştırılamayan ikinci bir arayüz, dış dünyaya bağlanan giriş / çıkış sinyalleridir. Burada her bloğun kendi sinyal setini sağlayacağı için bir standart tanımlayamayız, ancak bir grupta bir kanal olarak adlandıracağımız bir grup sinyali bir araya getirebiliriz.

Son olarak, akış verileri taşıyan kullanışlı olabilecek üçüncü bir sınıf arabirim vardır. Bu durumda, sürekli bir veri akışını aktarmak istiyoruz, ancak alıcı blok bunu işleyemiyorsa akışı duraklatmak istiyoruz, bu nedenle verilerle birlikte benzer bir akış kontrol sinyallerine de ihtiyacımız var. bir UART'ta olur.

Okunabilirlik konusunda da biraz standartlaştırmak istediğimiz için bazı kodlama kuralları da ayarlamak istiyoruz. Burada elbette boşluklara / sekmelere, gösterime ve diğerlerine hükmeden birçok farklı din var, bu yüzden sevdiğimiz birini seçiyoruz…

Din hakkında konuşurken, aynı zamanda diller hakkında da konuşuyoruz… VHDL yerine (Sistem) Verilog'u tercih ediyoruz ve IP bloklarımızın çoğu kodlanmış durumda. Seçimimizin nedeni, Verilog'un genel olarak C'ye daha benzer olması ve parametrik bloklar oluşturmayı kolaylaştıran çok güzel yapılara izin vermesidir.

Kodlama Kuralları

  • Bildirilen her bir varlığın önünde, türünü tanımlaması, değişken adı tamamen büyük harf olması ve birden çok kelimenin alt çizgi ile ayrılması için bir önek kullanırız. Özellikle:


Önek Açıklama
w Kablo, tüm kombinasyon sinyalleri için, örneğin wDATA. Genellikle tel yönergesi ile tanımlanır
r Reg, tüm ardışık sinyaller için, örneğin rSHIFTER. Genellikle reg yönergesi ile tanımlanır
ben Giriş, modül bildirimindeki tüm giriş sinyalleri için, örneğin iCLK. Genellikle giriş yönergesi ile tanımlanır
Ö Çıkış, modül bildirimindeki tüm çıkış sinyalleri için, örneğin oREAD. Genellikle çıktı yönergesi ile tanımlanır
b İki yönlü, modül bildirimindeki tüm giriş sinyalleri için, örneğin bSDA. Tipik olarak, içeride ve yönergesi ile tanımlanmış
p Parametre, bloğu parametreleştirmek için kullanılabilen tüm parametreler için, örneğin pCHANNELS. Genellikle param yönergesi ile tanımlanır
c Sabit, sabit olan veya türetilen değerler olan ve bloğu parametrelendirmek için doğrudan kullanılamayan tüm tanımlar için. Örneğin cCHANNEL_BITS. Genellikle localparam yönergesi ile tanımlanır
e Bir veya daha fazla sinyal veya kayıt tarafından kullanılan tüm olası sabit değerler için numaralandırılmıştır. Örneğin, bir durum makinesi durumu eSTATE olarak tanımlanabilir. Genellikle enum yönergesi ile tanımlanır

  • Sekmelere göre boşlukları tercih ediyoruz! Bunun nedeni, sekme boyutu kodundan bağımsız olarak her zaman iyi görünmesidir.
  • Girinti iki boşluğa ayarlanmıştır.
  • Koşullu deyim blokları, içinde tek bir deyim olsa bile her zaman başlangıç / bitiş yapılarına sahip olmalı ve başlangıç / bitiş if / else ile aynı satırda olmalıdır
  • Aynı gruba ait sinyaller ortak bir önek paylaşacaktır

Arayüz prototipleri

Hafif Otobüs

Çevre birimleri birbirine bağlamak için bir otobüs. Geleneksel veri yolu 32 bit iken, adres veri yolu açıkta kalan kayıtların sayısına bağlı olarak değişken genişliktedir. Bir veri yolu aşağıdaki sinyalleri gerektirir:

işaret yön Genişlik Açıklama
Usta Köle
ADRES Ö ben var. Kayıt adresi, genişlik belirler
OKU Ö ben 1 Strobe oku
READ_DATA ben Ö 32 Veri okunuyor
YAZMAK Ö ben 1 Strobe yazma
WRITE_DATA Ö ben 32 Belirli bir adreste yazılacak veriler
BYTE_ENABLE Ö ben 4 32 bit sözcüğün hangi baytının gerçekten yazılacağını işaretlemek için isteğe bağlı sinyal
WAIT_REQUEST ben Ö 1 Çevre birimini işaretlemek için isteğe bağlı sinyal meşgul. Okuma ve yazma flaşları sadece bu sinyal açıklanmadığında geçerli kabul edilecektir.

Yazma döngüsünde kural olarak, ADRES ve WRITE_DATA, WRITE strobunun aynı saat döngüsünde kilitlenir. Buna karşılık, bir okuma döngüsünde READ_DATA, okunan ADRES strokunu hemen takip eden döngüde çevresel olarak sunulur.

Boru Hatlı Otobüs

Bir kerede birden fazla komutu işleyebilen ve isteklere değişken zamanda yanıt veren karmaşık blokları birbirine bağlamak için bir veri yolu. Bu veri yolu, aşağıdaki sinyalleri ekleyerek Hafif veri yolunu genişletir:

Bu davranış aynı zamanda 1 saat okuma gecikmesi olarak da adlandırılır ve temel olarak, çevre biriminin isteğe bağlı WAIT_REQUEST sinyalini kullanarak bir READ veya WRITE işlemine yanıt vermek için değişken sayıda saate sahip olmasına rağmen, bu işlemin diğer işlemleri gerçekleştirmesini engelleyen master'ı kilitleyeceği anlamına gelir. Bir bakıma bu, çoklu görev yapmak için işletim sistemine gelen gecikmelere karşı programlamada meşgul döngülerin kullanılmasına benzer olarak düşünülebilir.

işaret yön Genişlik Açıklama
Usta Köle
BURST_COUNT Ö ben var. Gerçekleştirilecek sıralı işlem sayısı
READ_DATAVALID ben Ö 1 Çevrebirim bu sinyali, ana verilere veri sağlandığında işaretlemek için kullanır. Herhangi bir gecikme ile iddia edilebilir ve süreklilik garantisi yoktur. Seri çekim boyutu 4 olan bir okuma işlemi, her READ stroku için 4 kez READ_DATAVALID değerini bildirir

Bu yaklaşımın temel avantajı, kaptanın her işlem için birden çok veri okuma veya yazma niyeti ile köle iletişim kurabilmesidir. Hem okuma hem de yazma darbeleri için aslında BURST_COUNT sinyali, çevre birimine işlemin ne kadar süreceğini bildirir.

Köle, bir işlemi kabul etmeye hazır olana kadar WAIT_REQUEST talep edecektir. Yazma durumunda, BURST_COUNT ve ADDRESS yalnızca ilk strokta örneklenir; bundan sonra çevre birimi WRITE strokunun istenen kelime sayısı için onaylanmasını bekler ve adresi otomatik olarak artırır. Okuma işlemleri için, WAIT_REQUEST belirtilmediğinde gerçekleştirilen tek bir READ stroku, çevre birimine, istenen sayıda kelime için READ_DATAVALID ifadesi eklenerek döndürülecek BURST_COUNT kelimeyi okumasını söyleyecektir. Bir okuma işlemi başlatıldıktan sonra, daha fazla işlemi kabul etmek veya etmemek çevre birimine bağlıdır, ancak genel olarak Pipelined Veriyolundan yararlanmak için en az iki eşzamanlı operasyona sahip olmak mümkün olmalıdır.

Akış arayüzü

Çok yakında

(Sistem) Verilog modülünün yapısı

Bir SystemVerilog modülü bildirimi birkaç şekilde yapılabilir, ancak çoğunlukla tercih ettiğimiz, blok girişlerinin derleme zamanında özelleştirilebilmesi için parametreleri kullanabileceğiniz formdur. Bu şöyle görünecektir:

module COUNTER #( pWIDTH=8 ) ( input iCLK, input iRESET, output reg [pWIDTH-1:0] oCOUNTER ); endmodule

Burada sadece modülün prototipini tanımladık ve giriş / çıkış portlarını tanımladık, şimdi modül başlığı ve endmodule deyimi arasına bir kod ekleyerek bazı faydalı mantık eklememiz gerekiyor.

Bir karşı örnekle başladığımızdan beri, buna devam edelim ve aslında onu uygulayan bir kod yazalım:

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

Yukarıdaki kod oldukça açıklayıcıdır… her pozitif saat kenarında, iRESET girişini yüksek görürsek sayacı sıfırlarız, aksi takdirde bir arttırırız ... bloğumuzu bilinen bir duruma geri yükleyen bir sıfırlama sinyaline sahip olmanın genellikle yararlı olduğunu ancak her zaman gerekli.

Şimdi… bu ilginç ama biraz zor bir şey yaptık… oCOUNTER'ı çıktı reg olarak ilan ettik, yani bunun sadece bir grup kablo değil, hafızası olduğunu söylüyoruz. Bu şekilde, “kayıtlı” atamayı kullanabiliriz; bu, atamanın bir sonraki saat döngüsüne kadar saklanacağı anlamına gelir.

Bunu yapabilmemizin başka bir yolu da modül bildirimindeki reg deyimini kaldırmak ve sayacı aşağıdaki gibi tanımlamaktır:

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

Bu temelde aynı şeydir, ancak bir kayıt tanımladık, üzerinde çalıştık ve daha sonra “sürekli” = çıkış sinyaline atadık. Buradaki fark, <=, sinyalin yalnızca saat kenarlarında değiştiği anlamına gelir = değeri sürekli olarak atar, böylece sinyal sonunda herhangi bir zamanda değişecektir, ancak bunu örnekte yaptığımız gibi atarsak, yalnızca saatin kenarları sonuçta ortaya çıkan sinyalin temelde sadece bir takma adıdır.

İlginçtir ki, donanım tanımlama dillerindeki diğer ifadeler gibi atamalar da paraleldir, bu da koddaki sıralarının hepsi paralel olarak yürütüldüğü için çok ilgili olmadığı anlamına gelir, böylece her zaman bloğundan önce de rCOUNTER'a oCOUNTER atayabiliriz. Buna geri döneceğiz çünkü düzenin önemli olmadığı doğru değil…

Sürekli atamaların bir başka ilginç kullanımı mantık denklemleri oluşturma olasılığıdır. Örneğin, sayacı şu şekilde yeniden yazabiliriz:

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

Temelde hala aynı şeyleri yapıyoruz ama bunu mantıksal olarak biraz daha açık hale getirecek şekilde yaptık. Temel olarak rCOUNTER artı bir değerine sürekli olarak wNEXT_COUNTER sinyalini atarız. Bu, wNEXT_COUNTER'ın rCOUNTER değeri değiştiği anda (neredeyse) hemen değişeceği anlamına gelir, ancak rCOUNTER yalnızca bir sonraki pozitif saat kenarında (<= ataması olduğu için) güncellenir, böylece sonuç yine de rCOUNTER'ın yalnızca saat kenarında değişmesidir.

Paralellik ve öncelik

Önceki bölümde yazdığımız gibi, tüm donanım açıklama dilleri paralel ifadeler kavramına sahiptir, bu da talimatları sırayla yürüten yazılım programlama dillerinin aksine, burada tüm talimatların aynı anda yürütüldüğü anlamına gelir. Örneğin, aşağıdaki kodu içeren bir blok yazarsak, kayıtların belirli bir saat kenarında birlikte değiştiğini görürüz:

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

Elbette her şey paralel olarak yürütülürse, basit bir durum makinesi oluşturarak yapılabilecek ifadeleri sıralı hale getirmenin bir yoluna ihtiyacımız var. Durum makinesi, girişler VE dahili durumuna dayalı çıkışlar üreten bir sistemdir. Bir anlamda, tezgahımızın önceki durumuna (rCOUNTER) göre değişen bir çıktıya (oCOUNTER) sahip olduğumuz için zaten bir durum makinesi idi, ancak daha ilginç bir şey yapalım ve belirli bir darbeyi oluşturan bir durum makinesi oluşturalım başladığımızda. Makinenin üç durumu olacaktır: eST_IDLE, eST_PULSE_HIGH ve eST_PULSE_LOW. EST_IDLE'de giriş komutunu örnekleyeceğiz ve alındığı zaman, pHIGH_COUNT ile parametrelendireceğimiz belirli sayıda saat için kalacağımız eST_PULSE_HIGH'e geçiyoruz, o zaman kalacağımız eST_PULSE_LOW'a geçeceğiz pLOW_COUNT ve sonra eST_IDLE'a geri dönelim ... Bunun kodda nasıl göründüğüne bir göz atalım:

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

Burada konuşmamız gereken birkaç yeni şey görüyoruz. Her şeyden önce enum kullanarak rSTATE değişkenini tanımlıyoruz. Bu, durumu sabit kodlu sayılar yerine kolayca anlaşılabilir değerlere atamanıza yardımcı olur ve tüm durum makinenizi yeniden yazmanıza gerek kalmadan durumları kolayca ekleyebilme avantajına sahiptir.

İkinci olarak, bir sinyalin durumuna bağlı olarak farklı davranışlar tanımlamamıza olanak tanıyan vaka / endcase bloğunu sunuyoruz. Sözdizimi C'ye çok benzer, bu nedenle çoğu okuyucuya aşina olmalıdır. Çeşitli büyük / küçük harf blokları içindeki ifadelerin hala paralel olarak yürütülebileceğini, ancak incelenen değişkenin farklı değerleriyle koşullandırıldığından, yalnızca bir kerede etkinleştirileceğini belirtmek önemlidir. EST_IDLE örneğine baktığımızda, iPULSE_REQ düzeyinin yükseldiğini hissedene kadar sonsuza kadar kaldığımızı görüyoruz, bu durumda durumu değiştiririz, sayacı yüksek durum dönemine sıfırlar ve nabzı çıkarmaya başlarız.

OPULSE kaydedildiğinden tekrar atanana kadar durumunu koruyacağını unutmayın. Bir sonraki durumda işler biraz daha karmaşıklaşır… her saatte sayacı azaltırız, sonra sayaç 0'a ulaşırsa durumu da değiştiririz, oPULSE değerini 0 olarak değiştiririz ve tekrar rCOUNTER atarız. İki ödev paralel olarak yürütüldüğünden, bunun ne anlama geldiğini ve yeterince HDL tümünün aynı kayıt defterinde yürütülmesi durumunda sadece sonuncunun gerçekten yürütüleceğini, yani yazdığımızın anlamının normalde sayacı azaltırız, ancak sayaç 0'a ulaştığında durumu değiştirir ve pLOW_COUNT olarak yeniden başlatırız.

Bu noktada eST_PULSE_LOW içinde olan şey, sayacı düşürdüğümüzde ve 0'a ulaşır ulaşmaz eST_IDLE'a geri döndüğümüzde oldukça netleşir. EST_IDLE rCOUNTER'a geri döndüğümüzde sonuç tekrar azaltılır, böylece sonuç, rCOUNTER'ın 0xff ( veya -1) eST_IDLE içinde, ancak iPULSE_REQ aldığımızda doğru değere sıfırlayacağımız için gerçekten umursamıyoruz.

EST_PULSE_LOW'dan çıkarken de rCOUNTER'ı sıfırlayabilmiş olsak da, HDL'de daha fazla şey kaynakları tüketeceği ve donanımımızı yavaşlatacağı için sadece gerçekten gerekli olanı yapmak her zaman daha iyidir. Başlangıçta bu tehlikeli görünebilir, ancak bazı deneyimlerle bunun nasıl yardımcı olabileceğini görmek kolaylaşacaktır. Aynı kavram sıfırlama mantığı için de geçerlidir. Gerçekten gerekli olmadıkça, nasıl uygulandığına bağlı olarak kaynakları tüketebilir ve sistem hızını kötüleştirebilir, bu nedenle dikkatli kullanılmalıdır.

Gerçek bir dünya örneği

Şimdi Vidor, PWM'de kullandığımız basit bir çevre biriminin gerçek dünya örneğine dalalım. Elde etmek istediğimiz, her bir PWM kanalının göreceli fazını tanımlama olanağına sahip birden fazla PWM çıkışı olan küçük bir blok oluşturmaktır.

Bunu yapmak için bir sayaca ve sayacın verilen değerlerin üstünde olduğunu bize söyleyen birkaç karşılaştırıcıya ihtiyacımız var, böylece çıktıları değiştirebiliriz. Ayrıca PWM frekansının programlanabilir olmasını istediğimizden, sayacın sistemimiz için kullandığımız tabandan farklı bir frekansta çalışmasını sağlamalıyız, böylece periyodu tam olarak ihtiyacımız olan şeydir. Bunu yapmak için, temel olarak UART'larda kullanılan baud hızı üreticilerine benzer şekilde taban saatini daha düşük bir değere bölen başka bir sayaç olan bir ön ölçekleyici kullanıyoruz .

Şimdi koda bakalım:

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

Burada öğrenilecek bir dizi yeni şey var, bu yüzden modül bildiriminden başlayalım. Burada adres veriyolunun gerekli bit genişliğini belirlemek için yerleşik bir fonksiyon kullanıyoruz. Amaç, adres aralığını kayıtlar için gereken minimum değerle sınırlamaktır, örneğin 10 kanal istiyorsak toplam 22 adrese ihtiyacımız var. Her adres biti kullanabileceğimiz adres sayısını iki katına çıkardığı için toplam 5 bite ihtiyacımız var, bu da toplam 32 bit'e neden oluyor. Bu parametriği yapabilmek için iADDRESS genişliğini $ clog2 (2 * pCHANNELS + 2) ve kayıtları 2 boyutlu bir dizi olarak tanımlıyoruz.

Aslında çok boyutlu bir dizi yapmanın iki yolu vardır ve burada “paketlenmemiş” bir dizgiyi kullanıyoruz, bu da temel olarak kayıt bildiriminin sol tarafına indeksler ekleyerek kayıtları ayrı varlıklar olarak tanımlamaktadır. Diğer bir şekilde, bu örnekte, indekslerin her ikisinin de bildirimin sol tarafında olduğu “paketlenmiş” kullanmıyoruz ve sonuç, 2D dizinin de birleşimini içeren tek bir büyük kayıt olarak görülebilmesidir. tüm kayıtlar.

Buradaki bir başka ilginç numara da kayıtları işleyen mantığı nasıl tanımladığımız. Her şeyden önce sadece yazma kayıtları uyguluyoruz, böylece iREAD ve iREAD_DATA sinyallerini bulamıyoruz İkinci olarak sadece ilk iki kaydın her zaman mevcut olduğu bir parametrik kayıt setine sahip olmak istedik, geri kalanı ise dinamik olarak tanımlandı ve işlendi uygulamak istediğimiz kanal sayısı. Bunu yapmak için, bir ikili sayıda en az anlamlı bitin sayının tek mi yoksa çift mi olduğunu tanımladığını not ederiz. Kanal başına iki kayıt yaptığımız için, adres 2'nin altında olup olmadığımıza bağlı olarak davranışımızı farklılaştırabildiğimiz için bu kullanışlı olur.

Adres 2'nin altındaysak, ön ölçekleyici sayısı ve karşı dönem olan ortak kayıtları uygularız. 2'nin üzerindeyse, LSB'yi yüksek veya düşük karşılaştırıcının değerini yazıp yazmadığımızı belirlemek için kullanırız.

Başka bir basit örnek

Bir şey öğrenebileceğimiz bir diğer basit örnek, kareleme kodlayıcıdır. PWM'den daha basit görünse de, önemsiz olmayan bazı zorlukları da ele alıyor. Dış dünyadan gelen sinyallerle uğraştığımız ilk sorun, iç saatimizle senkronize olduklarını garanti etmememizdir, bu nedenle, kayıtlardaki verilerin tanımlanmamasına ve bir saat döngüsü sırasında potansiyel olarak değişmesine neden olan metastabilite adı verilen bir fenomenle karşılaşabiliriz. Bunun nedeni, kayıt sırasında mandalın girişinde veriler değişiyorsa, kayıt herhangi bir zamanda 1 veya 0'a "çürümeye" neden olabilecek dengesiz bir duruma gelebilir. Bu nedenle, bir kayıt zinciri ekleyerek giriş sinyalini yeniden senkronize etmeliyiz, böylece ilk kayıt metastabil hale gelse bile, aşağıdakiler tüm mantığın “kontamine” olma riski olmadan müteakip mantığı besleyebilecek kararlı bir duruma sahip olacaktır. kararsız durum.

Burada yaptığımız bir başka ilginç şey ise, bir strobe ve kareleme sinyallerinden kodlayıcıdan bir yön belirlemek için sürekli atama kullanmaktır. Kodda, dalga formlarının nasıl göründüğünü gösteren basit grafikler vardır, ancak dalga formlarının nasıl ortaya çıktığını tam olarak anlamak için denklemlerin zaman içinde farklı noktalarda sinyal kullandığını düşünmeniz gerekir. Bu basitçe asenkron girişleri senkronize etmek için kullanılan kaydırma yazmacını kullanarak da onları geciktirmek için yapılır, böylece kaydırma yazmacının farklı bir noktasına dokunarak sinyalin önceki saatin nasıl olduğunu görebiliriz. Özellikle, eğer vardiya kaydının girişine doğru hareket edersek, “daha yeni” veriler alırız, sonuna doğru gidersek “daha eski” verileri alırız. Denklemlere bakarsak, mantıksal bir dışlayıcı olan ^ işlecini veya iki işlenen farklıysa 1 ve aksi takdirde 0 döndüren (XOR) kullanırız.

Dalga formlarına baktığımızda, A veya B'nin bir kenarı olduğunda strobun bir nabız ürettiğini görüyoruz ve bu, her bir sinyali gecikmeli versiyonuyla basitçe xoring ederek yapılır. Bunun yerine yön sinyali biraz daha karmaşıktır, ancak enkoderin dönme yönüne bağlı olarak strok sinyali yüksek olduğunda sürekli olarak 0 veya 1 olduğunu fark ederiz. Aslında yön sinyalinde darbeler olduğunu görüyoruz, ancak bunlar flaşlarla çakışmıyor, bu yüzden göz ardı edilecek.

İlk bakışta bariz görünmeyebilecek bir şey, denklemlerin tüm girişler için aynı mantığı paralel olarak hesaplamasıdır, aslında rRESYNC_ENCODER kayıtları, birinci indeks kaydırma kaydının ve ikincisinin kaydı tanımlayacak şekilde düzenlenmiş paket boyutlu dizilerdir. indeks kodlayıcı kanaldır. Bu, rRESYNC_ENCODER'a belirli bir indeksle referans verdiğimizde, endeks tarafından belirtilen saat miktarı kadar gecikmeli olarak tüm kodlayıcı girişlerini içeren tek boyutlu bir dizi seçtiğimiz anlamına gelir. Bu aynı zamanda bir dizide bitsel bir mantık işlemi gerçekleştirdiğimizde, aslında aynı anda birden fazla paralel mantık denklemi oluşturduğumuz anlamına gelir. Bunun sadece dizinin “paketlenmemiş” dizilerde olduğu gibi “paketlenmiş” olması nedeniyle yapılabileceğini ve elementlerin bu şekilde denklemlere katılamayacaklarını ve tek tek ele alınmaları gerektiğini unutmayın.

Diğer örnekler için yaptığımız gibi, blok çoklu girişler uygular ve bunu, izin sinyalini (yine kanal sayısı kadar geniş bir dizi olan) kontrol eden bir for döngüsü kullanarak yapar ve bu yüksek olduğunda yönü kontrol eder ve buna dayanarak o kanalın sayacını artırır veya azaltır. Bu kolayca? : C'de olduğu gibi çalışan operatör (koşullu ifade).

Son olarak veriyolu arayüzü oldukça basittir, çünkü sahip olduğumuz tek kayıtlar salt okunur sayaçlardır ve bunu sadece okuma sinyalini kontrol ederek ve adres tarafından endekslenmiş sayaç dizisiyle çıktı verilerini atayarak, tıpkı bir RAM gibi .

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

Muhtemelen, sayaç derinliğini ve kanal sayısını değiştirebileceğimiz ve kodun ilgili tüm mantığı çok okunabilir bir şekilde oluşturarak ölçeklendirebileceğimiz oldukça parametrik bir tasarımı tanımladığımız gibi, tasarım için ne kadar zarif ve özlü bir donanım açıklaması olabileceğinin harika bir örneğidir. yol. Tabii ki aynı şeyi yapmanın farklı yolları vardır ve bu aynı zamanda (sistem) Verilog'un yeteneklerinin biraz daha fazla anlaşılmasını gerektirdiği en açık olanıdır.