# Zedboard + AD9361 Tutorial # Introduction ## FPGA 現場可程式化邏輯閘陣列(Field **Programmable** Gate Array, FPGA) 一種半客製化電路,可以用Verilog等硬體描述語言,在邏輯合成(logic synthesis)、布局(placement)、布線(routing)的工具軟體(Ex.Vivado)上,快速地燒錄至FPGA上進行測試。 可程式化邏輯元件可以被用來實現基本的邏輯閘數字電路,或者更複雜一些的組合邏輯功能。這些可編輯的元件裡通常也包含記憶元件,從而構成序向邏輯電路──任何時刻的穩態輸出取決於當前的輸入,並與前一時刻輸入形成的狀態有關。 使用者可以通過可編輯的連接,把FPGA內部的邏輯塊連接起來,所以FPGA可以完成使用者所需要的邏輯功能。 基於內部邏輯可以被使用者反覆修改的特性,FPGA的電晶體製程無法太小,因此效能與大小會比不上特定應用積體電路(Application Specific Integrated Circuit, **ASIC**)。但是,FPGA可以快速成品,可以反覆修改並除錯,所以方便設計與開發,然後將設計轉移到類似於專用積體電路的晶片上。 [[Reference](https://zh.wikipedia.org/wiki/%E7%8E%B0%E5%9C%BA%E5%8F%AF%E7%BC%96%E7%A8%8B%E9%80%BB%E8%BE%91%E9%97%A8%E9%98%B5%E5%88%97)] --- ## Vivado 可以用來配置FPGA的程式軟體,內部有許多已經完成的module可以直接使用。 使用Verilog。 ![](https://i.imgur.com/pgN5Re5.png) ### Verilog 硬體描述語言(**HDL**),用文字描述電路的語言。 原則上以C語言為基礎,可使用下面的連結做練習。 [Verilog練習題](https://hdlbits.01xz.net/wiki/Problem_sets#Verilog_Language) ### Creat Block design 以圖像化的方式表現出對硬體的設計,可以使用內建的module並直接從PIN角接線做設計。 可以用add source從外部加入已經寫好的module。 雙擊module可以查看及更改該module的屬性及參數設定。 1. Run Block Automation 對已存在的module接出預設的輸出。 2. Run Connection Automation 對已存在的多個module做自動接線。 自己做的IP沒有辦法用。 ### Creat HDL wrapper 把Block design中的所有設計整合為Verilog語言。 ### Run Synthesis 邏輯合成,將HDL轉換到的邏輯閘級別的電路連線網表的過程。 簡單來說是將HDL Wrapper轉換成電路懂的樣子。 可以在 **Windows $\to$ I/O ports** 中直接設定(選擇)Constraints。 ### Run Implementation 整合所有設定好的Constraints。 ### Creat Bigstream 製作燒錄用的檔案 .bit檔。 ### program device 將 .bit檔燒錄至FPGA上。 ## ZYNQ ZYNQ-7000 SoC ![](https://i.imgur.com/Uqdyouu.png) [[Reference](https://www.xilinx.com/products/silicon-devices/soc/zynq-7000.html)] ![](https://i.imgur.com/CeYkcYV.png) [[Reference](https://www.mouser.tw/new/xilinx/xilinx-zynq-7000-socs/)] 先進的FPGA系統單晶片(SoC)裝置,內建可編程邏輯(Programmable Logic, PL)結構結合Arm處理器,相當於FPGA+CPU的配置,以及如圖444,000個的邏輯元件、2,020個數位訊號處理(DSP)區塊或稱(slice)。 藉由處理器與PL結構的整合,整合式處理器可執行傳統的微控制器工作,包括管理PL結構和其他晶片上周邊裝置。因此,FPGA載入流程更貼近於傳統微控制器的開機流程。憑藉PYNQ環境,ZYNQ SoC有效簡化了複雜的FPGA開發流程。 [[Reference](https://www.digikey.tw/zh/articles/build-and-program-fpga-based-designs-quickly-python-jupyter-notebooks)] ## Jupyter 旨在「為數十種程式語言的互動式計算開發開源軟體,開放標準和服務」 Jupyter由Notebook Fronted, Jupyter Server及Kernel Protocol組成,如圖所示: ![](https://i.imgur.com/9jGtyh0.png) 主要包含兩項功能: 1. 網頁應用(Web Application) 基於瀏覽器(web-based)的互動創作及應用工具,包括可以計算、數學、文檔創作及豐富的多媒體輸出。 2. 文檔顯示(Notebook Documents) 顯示所有在上述Web Application當中的內容,包括計算的輸入/輸出、文件說明/解釋、數學運算及式子、圖片及所有豐富多媒體內容。 [[Reference 1](https://zh.wikipedia.org/wiki/Jupyter)] [[Reference 2](ttps://medium.com/python4u/jupyter-notebook%E5%AE%8C%E6%95%B4%E4%BB%8B%E7%B4%B9%E5%8F%8A%E5%AE%89%E8%A3%9D%E8%AA%AA%E6%98%8E-b8fcadba15f)] ## PYNQ Python+ZYNQ PYNQ是一套可以部屬Jupyter的linux平台,使用Python語言。 ![](https://i.imgur.com/AaE3O5P.png) [[Reference](http://www.pynq.io/)] 在PYNQ中,PL位元流會囊括到預先建構的函式庫中,稱為「重疊」。使用者透過與每個重疊相關聯的Python應用程式開發介面(API)來利用「重疊」功能。在開發過程中,使用者可以依照需求合併軟體函式庫與「重疊」,並透過自己的API來實作應用。在執行期間,處理器系統會照常執行軟體函式庫的程式碼,同時 PL結構會實作「重疊」所提供的功能。 此外,Python有龐大且持續發展的生態系統。使用者有機會在開放原始碼Python模組的儲存庫中找到支援服務或專用演算法所需的軟體函式庫。同時,由於PYNQ使用通用的C語言實作Python解譯器,所以使用者也能以C語言實作關鍵功能。 [[Reference](https://www.digikey.tw/zh/articles/build-and-program-fpga-based-designs-quickly-python-jupyter-notebooks)] --- # Programming Example: LED ## Block Design ![](https://i.imgur.com/b3zDayc.png) ### ZYNQ7 Processing System PS端 Run Block Automation接上輸出至DDR與fixed IO (MIO與DDR, CLK等)。 ### Processor System Reset 提供reset訊號 Run Block Automation會從外部接入一reset訊號,不過因為**ZYNQ7 Process System**已經有reset訊號了,所以改成由**ZYNQ7 Process System**輸出。 >老師:之前有直接用**ZYNQ7 Process System**輸出的reset去接硬體,但是好像有問題,所以才額外用**Processor System Reset**。 同時,把CLK連起來。 ### Counter AXI 用Add source從**NCU_SDR_Verilog_LIB_2022**資料夾加入已經寫好的**counter**。 CLK接線,然後連上reset和enable。 要除頻,所以把output的寬度改成26bits。 把tvalid接上tready,valid馬上就ready,然後只要enable high就會valid,達到real time的效果(每個CLK都跑的意思)。 >這裡ZYNQ7 Process System的CLK被給定為100MHz,如果counter沒辦法跑到100MHz的話,會有錯誤產生。 ### Slice 取出一串資料的其中某些值。 Counter的輸出為Bus,要接上**slice**才能取出用來控制LED燈的值。 設定上Data寬度是26,取最後一個bit。 Din Width: 26, Din from: 25, Din down to: 25, Dout width: 1 ## Constraints 1. Add constraints 於**Add source**的選項中選擇**Add constraints**,通常匯入檔案,也可以自己打。 ![](https://i.imgur.com/KofB3Ij.png) ```set_property -dict {PACKAGE_PIN T23 IOSTANDARD LVCMOS} [get_ports led_0]:``` set_property -dict {PIN腳位資訊} [get_ports 變數名稱] 2. I/O ports 執行 **Creat HDL Wrapper $\to$ Run Synthesis** 後,在上方 **Window $\to$ I/O Ports** 打開,會位於視窗下方。 這裡也可以設定PIN腳位 (紅色框框中可以選擇)。 ![](https://i.imgur.com/H7gY4NG.png) ## Synthesis 可以看到現在電路板被劃分出來使用的區域。 ![](https://i.imgur.com/tGrA5Bw.png) 在這個例子當中,我們使用LED的T22腳位,所以可以看到T22已經被劃分出來了。 ## Implement ![](https://i.imgur.com/qFujuf4.png) 可以看到為了使設計運作,畫出了FPGA中的設計。 可以把FPGA想像為一堆開關的集合,可以去配置這些開關使其達到使用者的設計。 現在亮起來的部分就是開關,也可以理解成這次的設計中所用到的資源。 ## Program Run Bigstream製作燒錄用的檔案.bit檔,檔案會在 **project $\to$ runs $\to$ implement** 裡面。 1. 用Vivado的Program Device(用USB直接連接J17-PROG腳位) 用 **Hardware Manager $\to$ Open target $\to$ Auto connect** 讓電腦連線到實體的板子(Zedboard),連線到的話如下圖: ![](https://i.imgur.com/Rsdp1u5.png) 接著**Program device**,系統會自動找到.bit檔,Program後會看到T22的LED開始閃爍,如下圖: ![](https://i.imgur.com/53FCj1N.jpg) 2. 用Jupyter 找到**project $\to$ runs $\to$ implement** 的.bit檔。 找到**srcs $\to$ source $\to$ block board $\to$ design $\to$ hardware** 的.hwh檔。 將兩個檔案改成相同的檔名丟入同一個資料夾。 ![](https://i.imgur.com/W8G7sDe.png) 在**New**中執行**Python3**。 用**Overlay**同樣可以執行燒錄。 ![](https://i.imgur.com/nqwTYPD.png) ``` from pynq import Overlay import time import numpy as np from pynq.lib import AxiGPIO ol = Overlay("./design_2022_04_12.bit") ol? ``` ## Switch 可以設計一個switch控制LED燈,重複上述過程,就可以用開關來控制LED燈了。 ![](https://i.imgur.com/BCrHTj7.png) ![](https://i.imgur.com/26k3Ggi.png) ![](https://i.imgur.com/wHAli3g.png) --- # DMA ![](https://i.imgur.com/8WX2N1l.png) [Reference](https://welkinchen.pixnet.net/blog/post/44175020-%E9%A9%85%E5%8B%95%E7%A8%8B%E5%BC%8F%E4%B8%AD%E7%9A%84dma%E5%95%8F%E9%A1%8C-) DMA(Direct Memory Access)是一項技術。 DMA允許I/O device在不經過CPU下,從主要記憶體(RAM)傳送或接收資料。 傳統上,外部存取記憶體需要透過CPU作中繼才能進入記憶體。 >傳統上:**CPU收到存儲Interrupt$\to$將資料複製進cache$\to$寫入新位置**。 但是這些存取要求會對CPU造成負擔,因為: 1. 外部設備的操作時脈可能異於CPU 2. CPU本身不用介入傳送,無須取指令、取值、儲存等動作 3. Interrupt令CPU需要停下當前動作 [Reference](https://hackmd.io/@Pieapplej/S1jWKKgN5#DMA%E7%B0%A1%E4%BB%8B) 簡而言之,DMA可以繞過CPU的指令直接搬運資料。 為了使搬運資料的過程不出錯,必需要知道: 1. 原始資料的位置 2. 目的地的位置 3. 搬運的資料長度 所以對於DMA來說,重點只有三個:兩個位置資料與一個資料長度。 [Reference](http://sharing-icdesign-experience.blogspot.com/2014/05/dma.html) ## Design ![](https://i.imgur.com/L8Swtum.png) ![](https://i.imgur.com/lgygNxc.png) 本次的目的要看DMA自傳自收以檢測結果。 >DMA自傳自收:DMA將內存的數據傳到IP中,再從IP傳輸回內存。 這裡使用**FIFO**作為暫存的IP。 設計上,用AXI-lite總線來配置AXI DMA。 **MM2S**: Memory Mapped to Stream,從DDR記憶體抓資料出來。 **S2MM**: Stream to Memory Mapped,把資料傳進DDR記憶體中。 **AXI**負責內存(DDR記憶體)和AXI DMA間的通信。 **AXIS**負責FIFO和DMA間的通信,一般來說有方向沒有地址,上游對下游的通信。 [Reference](https://www.twblogs.net/a/5e65406fbd9eee2117c6fd0f) ## IPs 1. ZYNQ7 Processing System 2. AXI GPIO (GPIO: leds 8bits) (GPIO2: sws 8bits) 3. AXI Direct Memory Access 4. AXI Stream Data FIFO (Or FIR) ## ZYNQ7 Processing System PS端 Run Block Automation接上輸出至DDR與fixed IO (MIO與DDR, CLK等)。 因為DMA的M_AXI_MM2S和M_AXI_S2MM要連到PS端上的Slave接口,所以在這裡要開啟Slave的Port。 ![](https://i.imgur.com/FiwZftC.png) ## AXI GPIO ![](https://i.imgur.com/gBeNZVc.png) GPIO(General-purpose input/output)可以被視為一個開關 輸入端有AXI, CLK和reset,皆為控制GPIO用,輸出端的結果與CLK無關 這裡的GPIO輸出可以直接選擇輸出為預設的選項或是自己設定,這裡選擇為預設的leds 8bits。 若打開Synthesized Desigen,可以看到作為LED燈的8的部分已經被劃分出來了。 ![](https://i.imgur.com/kLggHrd.png) ## AXI Interconnect ![](https://i.imgur.com/fGtSDPG.png) ![](https://i.imgur.com/bgraOoT.png) ### AXI AXI(Advanced eXtensible Interface)匯流排。 AXI總線分成**Master主機**和**Slave從機**兩端,常見的主機有CPU和DMA,從機則為DDR等等。 >Master就是Valid。 >Write address: Master。 >Read address: Master。 >Read Data Channel: Slave。 主機可以通過從機讀取或寫入儲存介質,但是從機無法主動向主機寫入數據。 AXI4有五個獨立通道:讀地址AR、讀數據R、寫地址AW、寫數據W、寫回復R。 Write transcation: ![](https://i.imgur.com/5NWLDvu.png) Read transcation: ![](https://i.imgur.com/hzUIDin.png) [Reference](https://zhuanlan.zhihu.com/p/45122977) ### Interconnect 因為AXI總線的寬度和**GPIO**的輸入寬度不同,這裡加入一個**AXI Interconnect**提供**GPIO**的輸入用。 再加入DMA之後,**Interconnect**同時提供**GPIO**和**DMA**用。 這裡的**Interconnect**為Crossbar,1個Slave接2個Master。 **Interconnect**的意涵為處理總線互聯,意即多Master與多Slave的狀況 基本的互連結構有三種: 1. 共享地址和數據總線 2. 共享地址總線,多個數據總線 3. 多個地址總線,多個數據總線 換句話說,一個(或多個)主機有Master控制器和一個(或多個)從機有Slave控制器之間,以**Interconnect**為橋梁制定一套互聯的規矩(數據、地址總線等)。 **AXI Interconnect**允許任意AXI主機和AXI從機的連接,根據數據寬度、CLK和AXI Sub-protocol進行轉換,當外部主機或從機的接口特性不同於互聯模塊內部的crossbar switch的接口特性時,相應的基本模塊(Infrastructure cores)就會被自動的引入來執行正確的轉換。 基本功能有: 1. AXI Crossbar: Master和Slave互聯。 2. AXI Width Converter: Master連接寬度不同的Slave。 3. AXI Clock Converter: Master連接時鐘域不同的Slave。 4. AXI Protocol Converter: Master連接不同協議(AXI3, AXI4, AXI-Lite, ...)的Slave。 5. AXI FIFO: Master和Slave間增加一組FIFO。 6. AXI Register Slice: Master和Slave間增加一組Instruction Pipeline。 7. AXI MMU: 提供AXI地址的decoding和remapping。 [Reference](https://www.twblogs.net/a/5e52cc23bd9eee2117c30ef6) ## AXI DMA ![](https://i.imgur.com/7JVbgfK.png) **S_AXI_LITE**: 由PS端傳過來,用來配置、設計DMA用。 **Read channel**: 從DDR記憶體讀資料出來,MM2S。 **Write channel**: 寫到DDR記憶體裡面,S2MM。 因為要自傳自收,所以兩者都enable。 資料由DDR記憶體從**M_AXI_MM2S**進到DMA,然後從**M_AXIS_MM2S**出來送往FIFO。 >**MM2S**就有設定Source的位置。 >**M_AXI_MM2S**: Read address,**M_AXIS_MM2S**: Write 資料由FIFO從**S_AXIS_S2MM**進到DMA,然後從**M_AXI_S2MM**出來送往DDR記憶體。 >**S2MM**就有設定destination的位置。 >**S_AXIS_S2MM**: Read Data,**M_AXI_S2MM**: Write address ## AXI Smart Connect ![](https://i.imgur.com/wcpVRSk.png) 因為從DDR記憶體讀進來丟到Stream,然後Stream又要寫回來到DDR記憶體,都是走同一個Port。 **AXI Smart Connect**會把要接到同一個Port的東西安排好(輪流OR之類的) 這裡把2個Slave接1個Master。 ## AXI4 Data Stream FIFO ![](https://i.imgur.com/33pePNM.png) FIFO(First In, First Out)先進先出法。 演算法,先進佇列的工作會先被完成,後到的要稍後。 當面對**不同時鐘域**或**不同寬度**間的資料傳輸時,可使用FIFO作為資料緩衝。 可以做為資料暫存的地方。 從**S_AXIS**接收DMA的**M_AXIS_MM2S**資料,來源由**DMA**的**M_AXI_MM2S**給定。 自傳自收,所以**M_AXIS**的輸出要接回DMA的**S_AXIS_S2MM**,做為資料輸入。 >在這裡FIFO會被寫做一個Processing System。 >從DDR來的資料,經過一段處理之後,再寫回另外的Memory address。 ## Jupyter ### Program 使用Jupyter進行燒錄與執行。 ![](https://i.imgur.com/vrsvORB.png) ```. from pynq import Overlay import time import numpy as np from pynq.lib import AxiGPIO ol = Overlay("./design_2022_04_12.bit") led_ip = ol.ip_dict['axi_gpio_LED'] leds = AxiGPIO(led_ip).channel1 mask = 0xffffffff leds.write(0xee, mask) ol? ``` 其中```ol```為Overlay的變數名稱,可以隨意更改。 ```led_ip = ol.ip_dict['axi_gpio_LED']```中的dict要依照Block Diagram中定義的名稱操作。 可以用```ol?```來確認燒錄是否完成。 同樣也可以看Zedboard板子上的LED燈是否亮起,判斷燒錄是否完成。 >```leds.write(0xee, mask)```所寫的為```8'hee=8'd238=8'b11101110```,所以亮燈應為11101110 ![](https://i.imgur.com/xCZs5zO.jpg) ### Configure 然後就可以寫DMA的code來啟動DMA。 這裡展現的PYNQ的優點,可以程式的與硬體互動,測試硬體。 Python的優點在跑完後狀態會留著,有修改的話不用全部重新跑。 ```. import pynq.lib.dma from pynq import Xlnk import numpy as np xlnk = Xlnk() dma = ol.axi_dma_0 input_buffer = xlnk.cma_array(shape=(5,), dtype=np.uint32) output_buffer = xlnk.cma_array(shape=(5,), dtype=np.uint32) for i in range(5): input_buffer[i] = i + 77 print(input_buffer) dma.sendchannel.transfer(input_buffer) dma.recvchannel.transfer(output_buffer) dma.sendchannel.wait() dma.recvchannel.wait() print(output_buffer) ``` >```xlnk = Xlnk()```表示取得物件。 >```xlnk.cma_array```是在allocate DMA,下面```input_buffer[i] = i + 77```輸入值。 >```dma.sendchannel.transfer(input_buffer)```的```send```是指Memory 到 Memory。 >```dma.recvchannel.transfer(output_buffer)```的```recv```是指Stream 到 Memory。 輸入從input_buffer丟到Hardware,由MM2S進到DMA。 變成M_AXIS接到FIFO的S_AXIS,再由FIFO的M_AXIS回到DMA的S_AXIS。 然後轉到S2MM,最後回到output_buffer變成輸出。 >```dma.sendchannel.wait()```和```dma.recvchannel.wait()```的```wait```表示等待。 >Hardware的assign都是non-blocking ── 同步執行。 >加上```wait```後變為blocking,須執行完上一行才能進入下一行。 >要等傳完資料才能接收資料的意思。 執行後的結果為下圖所示: ![](https://i.imgur.com/7nym4oZ.png) >如果把```dma.sendchannel.wait()```和```dma.recvchannel.wait()```注解掉的話,在這個case裡面,DMA可以照常運作。 ![](https://i.imgur.com/bIHunMs.png) 放```print(output_buffer)```到```dma.recvchannel.transfer(output_buffer)```上面,出來的結果應為```[0 0 0 0 0]```,表示還未輸出,證明確實為自傳自收。 ![](https://i.imgur.com/J2rVAer.png) ## FIR 可以把前一次設計中的FIFO換成FIR。 ![](https://i.imgur.com/5FbpuHL.png) FIR(Finite Impulse Response)有限脈衝響應是因果系統的移動平均濾波器。 所謂的濾波器,就是藉由排除掉輸入訊號中的某些頻率項,使輸出訊號變「好」 。 >例如:輸入有雜訊的弦波,在低通濾波器作用下,可以得到比較單純的弦波。 因果系統(Casual system):輸出只與過去或目前的訊號有關。 移動平均濾波器:輸出的值為過去與現在的值取加權平均的結果。 FIR的設計為第$n$個輸出為$M$個訊號與加權值$b_k$的和: $y[n]=\sum^M_0b_kx[n-k]$ 不同於IIR(infinite impulse response)無限脈衝響應。 IIR包含了前面的值與前面的濾波器的值(回授),所以IIR會考慮到前面所有的東西。 > 基於運作原理,FIR要等到$M+1$個訊號產生後才能產生訊號,所以會有unit delay,或稱作latency GPIO除了原本接上了LED之外,這次也接上了Switch。 所以從Synthesised Design中可以看到除了LED的區域外,Switch的區域也被畫出來了。 ![](https://i.imgur.com/XWgY9su.png) 本次的FIR由下列的程式碼給定: ```0 module FIR(CLK, RSTN, FIR_in_tvalid, FIR_in_tready, FIR_in_tlast, FIR_in_tdata, FIR_out_tvalid, FIR_out_tready, FIR_out_tlast, FIR_out_tdata); input CLK, RSTN; input FIR_in_tvalid, FIR_out_tready, FIR_in_tlast; output FIR_out_tvalid, FIR_in_tready, FIR_out_tlast; input [31:0] FIR_in_tdata; output [31:0] FIR_out_tdata; wire [7:0] h_0, h_1, h_2; reg [39:0] TDL [0:2]; wire [31:0] X, Y; wire [39:0] a [0:2]; always @ (posedge CLK) if (~RSTN) begin TDL[0] <= 0; TDL[1] <= 0; TDL[2] <= 0; end else begin TDL[0] <= a[0]; TDL[1] <= TDL[0] + a[1]; TDL[2] <= TDL[1] + a[2]; end assign X = FIR_in_tdata; assign h_0 = 32; assign h_1 = 64; assign h_2 = 16; assign a[0] = X*h_2; assign a[1] = X*h_1; assign a[2] = X*h_0; assign Y = TDL[2][39:8]; assign FIR_out_tdata = Y; assign FIR_out_tvalid = FIR_in_tvalid; assign FIR_in_tready = FIR_out_tready; assign FIR_out_tlast = FIR_in_tlast; endmodule ``` 這串程式碼當中可以看到一個變數:**tlast**。 前面有說過,對於DMA來說重要的東西有三個:2個地址和1個資料長度。 **tlast**對於DMA來說就是資料長度,意義為最後一筆資料。 只要DMA收到**tlast**,就會停止運作,所以**tlast**對於DMA來說是必要的。 在FIFO的例子當中,tlast已經被包在AXIS裡面了 完成後同樣我們可以用Python code去控制Zedboard。 ![](https://i.imgur.com/ErRLATs.png) In [1]:燒錄。 In [2]:控制LED燈與Switch。 ```. led_ip = ol.ip_dict['axi_gpio_led1_sw2'] leds = AxiGPIO(led_ip).channel1 mask = 0xffffffff leds.write(0x88, mask) SW = AxiGPIO(led_ip).channel2 while True: A = SW.read() print(A) ``` 在這裡```led.write```控制LED燈的量法,0x88=0d136=0b10001000 ```SW```為自訂的變數,表示Switch的讀數。 ```While True```為無線迴圈,```SW.read```會不斷重新讀取```SW```。 從```print(A)```數值可以看到```SW```由0x63=0b00111111變為0x127=0b01111111。 ![](https://i.imgur.com/rcah6CD.png) ```1 import pynq.lib.dma from pynq import Xlnk import numpy as np xlnk = Xlnk() dma = ol.axi_dma_0 N = 20 input_buffer = xlnk.cma_array(shape=(N,), dtype=np.uint32) output_buffer = xlnk.cma_array(shape=(N,), dtype=np.uint32) for i in range(N): input_buffer[i] = (i == 0) * 256 print(input_buffer) dma.sendchannel.transfer(input_buffer) print(output_buffer) dma.recvchannel.transfer(output_buffer) dma.sendchannel.wait() dma.recvchannel.wait() print(output_buffer) ``` 最後測試FIR與DMA。 Input定為長度20的矩陣,只有第1格有值256,如輸出的第一列所示。 分兩個不同的地方輸出Output,分別為DMA取資料前與取資料後。 取資料前的Output是沒有數值的,所以輸出應為0,如輸出的第二列所示。 取資料後的Output是通過FIR的數值。 FIR的權重前面設定取3項,分別為32, 64, 16,再加上FIR的特性會晚一個訊號才會動作。 因此結果如輸出的第三列所示,為0 32 64 16 0 ... 0。 --- # Matlab $\leftrightarrow$ ZYNQ 可以通過Jupyter使PC端的Matlab和ZYNQ做溝通。 算是Matlab和PS的互動,然後PS可以和PL做互動,達到控制SDR的目的。 原則上不需要做Block design,有ZYNQ然後開機就可以使用了。 ## Matlab to PYNQ ### Python code running on ZYNQ 原則上先在ZYNQ上設定完之後再去Matlab上操作。 ![](https://i.imgur.com/0Qdf2JG.png) ```0 import socket # HOST = '192.168.2.99' # default HOST = '192.168.50.220' # CIA router ctrl_port = 8000 data_port = 8001 server_data = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_data.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #作業系統會在伺服器socket被關閉或伺服器程序終止後馬上釋放該伺服器的埠, # 否則作業系統會保留幾分鐘該埠。 server_data.bind((HOST, data_port)) server_data.listen(5) server_ctrl = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_ctrl.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #作業系統會在伺服器socket被關閉或伺服器程序終止後馬上釋放該伺服器的埠, # 否則作業系統會保留幾分鐘該埠。 server_ctrl.bind((HOST, ctrl_port)) server_ctrl.listen(5) # 5 links print("wait") conn_data, addr_data = server_data.accept() conn_ctrl, addr_ctrl = server_ctrl.accept() M2P_data = np.zeros(16,dtype=np.uint16) conn_data.recv_into(M2P_data,4) print(M2P_data) ``` ```HOST```的IP位置要根據怎麼掛Zedboard來改。 Run之後會輸出wait,如上圖,表示系統在跑在等待,接著就可以去Matlab上操。 ### MATLAB code running on PC ```0 % Test of PYNQ TCP/IP global ctrl_client data_client % Control Port ctrl_client = tcpip('192.168.50.220', 8000, 'NetworkRole', 'client', 'Timeout', 10); %QAM %ctrl_client = tcpip('192.168.2.99', 8002, 'NetworkRole', 'client', 'Timeout', 5); %decoder fopen(ctrl_client); % Data Port data_client = tcpip('192.168.50.220', 8001, 'NetworkRole', 'client', 'Timeout', 10); %QAM %data_client = tcpip('192.168.2.99', 8003, 'NetworkRole', 'client', 'Timeout', 5); %decoder data_client.InputBufferSize = 2^20; data_client.OutputBufferSize = 2^20; data_client.ByteOrder='littleEndian'; %'bigEndian'; %; % Data_M2P= [10 12], fopen(data_client); fwrite(data_client,Data_M2P ,'int16') ``` 把上面的code輸入到Matlab的script中,記得要改```tcpip```的IP位置。 按下Run之後就可以把資料送出去,結果如下: ![](https://i.imgur.com/InkG1Np.png) 其中Jupyter中```conn_data.recv_into(M2P_data,4)```的最後面的```4```為最大可以接收的bits數量。 現在數字為4,所以如果去增加Matlab中```Data_M2P```後面的數值,ZYNQ也讀不到: ![](https://i.imgur.com/O5dL5ib.png) 數值改大後就讀的到了: ![](https://i.imgur.com/KUPiw1d.png) 原則上只要有連線到,之後讀數值只需要兩行而已: ```0 conn_data.recv_into(M2P_data,20) print(M2P_data) ``` 同樣的在Matlab上,只要有```fopen```成功後,寫新數值只需要兩行而已: ```0 Data_M2P= [10 12 1 2 3 4 7 8]; fwrite(data_client,Data_M2P ,'int16') ``` ![](https://i.imgur.com/xkbS474.png) 如此一來可以代表說,DATA可以寫給PS,然後PS再去做動作。 PS可以用DMA等方式把DATA丟到PL,PL就可以real time的播放。 ## PYNQ to Matlab ### Python Code ```0 P2M_data = np.array([1, 2, 3, 4],dtype=np.int16) conn_data.sendall(P2M_data) ``` 把[1 2 3 4]的資料丟出去。 ### MATLAB Code ```0 Data_P2M = fread(data_client,4,'int16') ``` 同樣,Matlab中```fread(data_client,4,'int16')```的```4```代表了讀的bits數。 執行完後就可以收到PYNQ丟出來的資料了。 ![](https://i.imgur.com/zE3XOku.png) --- # SPI SPI(Serial Peripheral Interface)串列周邊介面,是晶片通信的同步串行通信介面,是一種全雙工、高速、同步的通信總線。 可以一對一,也可以一主對多從。 ![](https://i.imgur.com/6RIhC6i.png) [Reference](https://zh.wikipedia.org/zh-tw/%E5%BA%8F%E5%88%97%E5%91%A8%E9%82%8A%E4%BB%8B%E9%9D%A2#/media/File:SPI_three_slaves.svg) 通常SPI有4條信號線以實現全雙工(或是3線的SPI為半雙工): 1. SCLK(Serial Clock) 串行時鐘,Master提供CLK給Slave。 2. MOSI(Master Output, Slave Input) 主發從收信號,Master時傳送資料到Slave;Slave時接收從Master來的資料。 3. MISO(Master Input, Slave Output) 主發從收信號,Master時接收從Slave傳來的資料;Slave時傳送資料至Master。 4. SS(Slave Select) 片選信號,在一主對多從的系統中,SS的高低電位可以用於選擇接收資料的裝置。 所有的傳輸都根據同一個頻率信號。 頻率信號由Master產生,Slave則用此頻率信號來對收到的東西進行同步。 所謂的同步指Slave沒有自己的CLK,CLK由Master傳到Slave,所以兩者會共用同一個CLK。 [Reference](http://wiki.csie.ncku.edu.tw/embedded/SPI) ## IPs 1. ZYNQ7 Processing System 2. AXI GPIO (axi_gpio_AD9361_SPI) 3. GPIO32 to AXIS24.v 4. AXIS DATA FIFO (independent CLKs) (Width=24, Depth=16) 5. AD9361 SPI Interface ## Design ![](https://i.imgur.com/U4wEku1.png) ![](https://i.imgur.com/m6G5ABF.png) ![](https://i.imgur.com/ejGAOHD.png) 把內容都包進**AD9361_SPI**的hierarchy中單純只是讓畫面看起來更簡潔。 ### ZYNQ7 Processing System Run block automation接上DDR和FIXED_IO。 ### AXI GPIO Run connection automation ![](https://i.imgur.com/CpMUiNv.png) 系統會自動把**processing system**的M_AXI接到**GPIO**的S_AXI 這裡因為GPIO的輸出是我們自己設定的模組,所以不勾。 中間會自動產生**Reset**和**Interconnector**。 ![](https://i.imgur.com/CgLpweb.png) **GPIO**開兩個channel,GPIO為All output,GPIO2為All input。 因為GPIO2的輸入資料是8 bits,所以要把寬度改成8。 養成好習慣,把名字改掉。 這邊可以改成**axi_gpio_AD9361_SPI**。 ### GPIO32 to AXIS24 **Add source $\to$ Add or creat design source $\to$ Add files** 加入NCU_SDR_Verilog_LIB_2022中已經寫好的**GPIO32_AXIS24**。 在Source中,有箭頭代表示hierarchy,打開發現裡面缺少**Ping_Pong_FIFO**。 用同樣的程序加入**Ping Pong FIFO**。 ![](https://i.imgur.com/rcXtPMg.png) 確認一下**GPIO32_AXIS24**的內容,**Ping Pong FIFO**不是AXI機制的FIFO。 同時也可以打開**Ping_Pong_FIFO**的內容,確認變數名稱一致。 ![](https://i.imgur.com/1Nx5gyx.png) 完成之後把**GPIO32_to_AXIS24**拉進Block diagram中。 ![](https://i.imgur.com/81g27fB.png) 接CLK之前先確認**processing system**的CLK頻率多少。 看Clock configuration的PL fabric clocks,為100MHz。 ![](https://i.imgur.com/1fOD1Np.png) 接上CLK和RSTN後,把All output的GPIO接到GPIO_in當中。 如果顯示no matching的話,把GPIO的加號打開,從裡面接線。 ### AXIS DATA FIFO 加入IP **AXIS Stream Data FIFO**。 避免占掉太多資源,FIFO depth改成16。 Independent clocls改為Yes。 ![](https://i.imgur.com/aXqMM9A.png) 把**GPIO32 to AXIS24**的master接到**FIFO**的S_AXIS。 Slave的CLK要接到100MHz的CLK上,RSTN也要接上。 Read的master的CLK要接到**GPIO32 to AXIS24**的四分之一CLK。 ![](https://i.imgur.com/SRZ7lfu.png) ### AD9361 SPI interface ![](https://i.imgur.com/DjUKsn2.png) ```0 module AD9361_SPI_Interface(CLK, RSTN, s_tdata, s_tvalid, s_tready, SPI_ENB, SPI_CLK, SPI_DI, SPI_DO, SPI_led); input CLK, RSTN; input [23:0] s_tdata; // input s_tvalid; output s_tready; input SPI_DO; output SPI_DI, SPI_ENB, SPI_CLK; output [7:0] SPI_led; wire [23:0] SPI_P2S_in; wire SPI_RST; reg [23:0] P2S_reg; reg [4:0] P2S_counter; reg P2S_state; reg [7:0] SPO_reg; wire P2S_empty, P2S_full; wire P2S_reset, P2S_WR; wire SPO_ENB; always@(posedge CLK) if (P2S_reset) P2S_state <= 0; else if (P2S_WR) P2S_state <= 1; always @(posedge CLK) if (P2S_reset) P2S_counter <= 0; else if (P2S_full) P2S_counter <= P2S_counter + 1; always @(posedge CLK) if (P2S_WR) P2S_reg <= SPI_P2S_in; always@(posedge CLK) if (SPI_RST) SPO_reg <= 8'b0; else if (SPO_ENB) SPO_reg <= {SPO_reg[6:0],SPI_DO}; assign SPI_P2S_in = s_tdata[23:0]; assign SPI_RST = ~RSTN; assign P2S_reset = SPI_RST | (P2S_full & (P2S_counter == 31)); assign P2S_empty = (P2S_state == 0); assign P2S_full = (P2S_state == 1); assign s_tready = ~P2S_full; assign P2S_WR = s_tvalid & s_tready; // Output wire [4:0] P2S_index; assign P2S_index = 23-P2S_counter; assign SPI_DI = P2S_reg[P2S_index]; assign SPI_CLK = CLK; /////SPI_ENB////////////////////////////////////// assign SPI_ENB = ~(P2S_full & (P2S_counter < 24)); assign SPO_ENB = P2S_full & ((P2S_counter >= 16) & (P2S_counter <= 23)); assign SPI_led = SPO_reg; endmodule ``` ![](https://i.imgur.com/s7Lpnrh.png) 加入NCU_SDR_Verilog_LIB_2022中已經寫好的**AD9361_SPI_interface**。 ```SPI_DI = SPI_MOSI```、```SPI_DO = SPI_MIOS``` 接上系統的RSTN。 CLK是接除以4的CLK,因為讀進來和寫出去都是這個速度。 Input要接FIFO出來的Async的地方。 Read的時候SPI_DO會去capture AD9361的8bits data,會存起來然後用SPI_led做呈現。 所以要把SPI_led的資料拉到GPIO2。 DI和DO是對AD9361來說是input和output,所以這邊是output和input。 接4條信號線全部都用make external,然後把預設的名字中的_0去掉,以符合constraint。 ## Constraint 完成接線後,可以先validate,然後creat HDL wrapper,set as top。 **Zedboard $\leftrightarrow$ AD9361 SPI** ```0 # constraints # Zedboard AD9361 SPI set_property -dict {PACKAGE_PIN F18 IOSTANDARD LVCMOS25 PULLTYPE PULLUP} [get_ports SPI_ENB]; ## D26 FMC_LPC_LA26_P set_property -dict {PACKAGE_PIN E18 IOSTANDARD LVCMOS25} [get_ports SPI_CLK]; ## D27 FMC_LPC_LA26_N set_property -dict {PACKAGE_PIN E21 IOSTANDARD LVCMOS25} [get_ports SPI_DI]; ## C26 FMC_LPC_LA27_P set_property -dict {PACKAGE_PIN D21 IOSTANDARD LVCMOS25} [get_ports SPI_DO]; ## C27 FMC_LPC_LA27_N ``` 這邊可以直接從NCU_SDR_Verilog_LIB_2022加入constraint file **Add source $\to$ Add or creat constraints $\to$ Add files**。 ![](https://i.imgur.com/wD6Y5Gr.png) 裡面主要是SPI的constraint,其他的可以多但是不能少。 這邊可以順便在加入一個GPIO做測試,開兩個channel給led和sws用。 Run connection automation ![](https://i.imgur.com/391ZRPC.png) Wrapper應該會自己更新。 完成後就可以Generate big stream。 ## Jupyter ```0 from pynq import Overlay import time import numpy as np from pynq.lib import AxiGPIO ol = Overlay("SPI.bit") ol? def PYNQ_SPIWrite(x, y): # PYNQ_SPIWrite(‘007’, ‘08’) mask = 0xffffffff ADI9361_Conf_comm = int(x + y, 16)+2**24+2**23 SPI_WR_channel.write(0x0, mask) SPI_WR_channel.write(ADI9361_Conf_comm, mask) #SPIWrite print(hex(ADI9361_Conf_comm)) print(x) def PYNQ_SPIRead(x): # PYNQ_SPIRead(‘007’) mask = 0xffffffff ADI9361_Conf_comm = int(x + '00', 16) + 2**24 SPI_WR_channel.write(0x0, mask) SPI_WR_channel.write(ADI9361_Conf_comm, mask) #SPIWrite SPI_DO = SPI_RD_channel.read() SPI_DO = hex(SPI_DO) print(SPI_DO) SPI_ip = ol.ip_dict['AD9361_SPI/axi_gpio_AD9361_SPI'] SPI_WR_channel = AxiGPIO(SPI_ip).channel1 SPI_RD_channel = AxiGPIO(SPI_ip).channel2 ``` 先燒錄,定義```PYNQ_SPIWrite```和```PYNQ_SPIRead```的函數。 其中```PYNQ_SPIWrite(x, y)```的```x```是Address```y```是Data。 把SPI的write channel ```SPI_WR_channel```定義到先前寫的**GPIO**的output channel。 把SPI的read channel ```SPI_RD_channel```定義到先前寫的**GPIO**的input channel。 需要注意的是,如果前面有用Create hierarchy把**GPIO**包起來的話,**GPIO**的路徑需要寫成```'AD9361_SPI/axi_gpio_AD9361_SPI'```,不能只有```'axi_gpio_AD9361_SPI'```。 ```0 PYNQ_SPIWrite('271', '09') PYNQ_SPIRead('271') ``` 燒錄與定義完後,可以試著寫東西進去然後讀。 這裡把```'09'```這個Data寫到```'271'```的位置上,然後再讀```'271'```的Data。 結果如下圖所示: ![](https://i.imgur.com/XdWNPX0.png) 第一段執行完之後應該除了```ol?```之外都不會有東西跑出來。 第二段可以看到讀出來的結果是寫進去的```0x9```。 ![](https://i.imgur.com/GVWgwFH.png) 若把寫進去的Data從```'09'```換成```'7D'```,則讀出來的東西就變成```'0x7d'```。 也可以利用```While```一直寫東西進去 ```0 i=0 while i<5: PYNQ_SPIWrite('00'+str(i), 'AA') i=i+1 #PYNQ_SPIRead('007') ``` --- # Tx Template (AD9361) 發射機,可以直接從上次做完的SPI加東西上去,這裡從頭開始做。 ![](https://i.imgur.com/TVhJU5k.png) ## Needs ### AD9361 Configuration (Programming) 1. GPIO32_to_AXIS24.v 2. Ping_Pong_FIFO.v 3. AD9361_SPI_Interface.v 4. Zedboard_AD9361_SPI.xdc 5. Zedboard_AD9361_Configuration.py 6. PYNQ_AD9361_Configuration.txt ### AD9361 DAC (TX) Interface 1. ADI_DAC_Interface_AXIS 2. DDR_IQ_DAC.v 3. DELAY_CLOCK.v 4. SG_LUT.v 5. Zedboard_AD9361_ADC_DAC.xdc ## Design ![](https://i.imgur.com/IPx8ocG.png) ![](https://i.imgur.com/rCDNrLB.png) ### ZYNQ7 Processing System Run block automation接上DDR和FIXED_IO。 ### AXI GPIO Run connection automation 開兩個GPIO: 1. GPIO:All outputs,Width: 32,寫資料用。 2. GPIO2:All inputs,Width: 8,讀資料用。 ### GPIO32 to AXIS24 **Add source $\to$ Add or create source $\to$ Create file** **File name: GPIO32_to_AXIS24** Module definition會自己寫,先Cancel。 ```0 module GPIO32_to_AXIS24(CLK, RSTN, GPIO_in, m_tdata, m_tvalid, m_tready, CLK_4); parameter DW_GPIO = 32, DW_AXIS = 24; input CLK, RSTN, m_tready; input [DW_GPIO-1:0] GPIO_in; output [DW_AXIS-1:0] m_tdata; output m_tvalid, CLK_4; reg DD; wire GPIO_wr, DD_in, PP_WR, PP_RD, PP_full, PP_empty; wire [DW_AXIS-1:0] PP_in, PP_out; always @(posedge CLK) if (~RSTN) DD <= 0; else if (~PP_full) DD <= DD_in; Ping_Pong_FIFO #(.DW(DW_AXIS)) PP (.CLK(CLK), .RSTN(RSTN), .PP_in(PP_in), .PP_WR(PP_WR), .PP_out(PP_out), .PP_RD(PP_RD), .PP_full(PP_full), .PP_empty(PP_empty)); assign GPIO_wr = ~DD & DD_in & ~PP_full; assign PP_WR = GPIO_wr; assign DD_in = GPIO_in[DW_AXIS]; assign PP_in = GPIO_in[DW_AXIS-1:0]; assign m_tdata = PP_out; assign m_tvalid = ~PP_empty; assign PP_RD = m_tvalid & m_tready; //////////////////////////////////////////////////////////////////////////// reg [1:0] CC; always @(posedge CLK) if (~RSTN) CC <= 0; else CC <= CC + 1; assign CLK_4 = CC[1]; endmodule ``` 打完後存檔,在Source列表中發現少了Ping Pong FIFO。 ### Ping Pong FIFO **Add source $\to$ Add or create source $\to$ Create file** **File name: Ping_Pong_FIFO** ```0 module Ping_Pong_FIFO(CLK, RSTN, PP_in, PP_WR, PP_out, PP_RD, PP_full, PP_empty); parameter DW = 16; input CLK, RSTN; input [DW-1:0] PP_in; input PP_WR, PP_RD; output [DW-1:0] PP_out; output PP_full, PP_empty; reg [DW-1:0] Ping_BUF, Pong_BUF; reg Ping_Load, Pong_Load, Ping_Pong_WR, Ping_Pong_RD; wire Ping_WR, Pong_WR; wire Ping_RD, Pong_RD; always @(posedge CLK) if (~RSTN) Ping_Pong_WR <= 0; else if (PP_WR) Ping_Pong_WR <= ~Ping_Pong_WR; always @(posedge CLK) if (~RSTN) Ping_Pong_RD <= 0; else if (PP_RD) Ping_Pong_RD <= ~Ping_Pong_RD; always @(posedge CLK) if (~RSTN | Ping_RD) Ping_Load <= 0; else if (Ping_WR) Ping_Load <= 1; always @(posedge CLK) if (~RSTN | Pong_RD) Pong_Load <= 0; else if (Pong_WR) Pong_Load <= 1; always @(posedge CLK) if (~RSTN) Ping_BUF <= 0; else if (Ping_WR) Ping_BUF <= PP_in; always @(posedge CLK) if (~RSTN) Pong_BUF <= 0; else if (Pong_WR) Pong_BUF <= PP_in; assign PP_full = Ping_Load & Pong_Load; assign PP_empty = ~Ping_Load & ~Pong_Load; assign Ping_WR = PP_WR & ~Ping_Pong_WR; assign Pong_WR = PP_WR & Ping_Pong_WR; assign Ping_RD = PP_RD & ~Ping_Pong_RD; assign Pong_RD = PP_RD & Ping_Pong_RD; assign PP_out = Ping_Pong_RD ? Pong_BUF : Ping_BUF; endmodule ``` 完成後可以把**GPIO32_to_AXIS24**拉進Block design。 接上CLK和RSTN。 GPIO_in為寫入信號,接上**GPIO**的GPIO,32 bits。 ### Async FIFO (Data FIFO) 寫進進去的CLK和讀出來的CLK不一樣,寫進去的CLK是系統的CLK;讀出來的CLK是除4的CLK。 原因出在SPI的速度比較慢,這也是換平台都需要用新的IP的原因。 加入一個AXIS Data FIFO,先設定。 輸出是24 bits 3 bytes,基本上符合初始設定,所以FIFO depth可以小一點,這裡設16。 因為Input的Slave的CLK跟Output是Master的CLK不一樣,所以Independent CLK要Yes。 **GPIO32_to_AXIS24**的Output master要接S_AXIS,CLK_4要接Master的CLK。 剩下的Slave的CLK和RSTN都接系統的。 這樣Data FIFO就完成了。 ### AD9361 SPI Interface **Add source $\to$ Add or create source $\to$ Create file** **File name: AD9361_SPI_Interface** ```0 module AD9361_SPI_Interface(CLK, RSTN, s_tdata, s_tvalid, s_tready, SPI_ENB, SPI_CLK, SPI_DI, SPI_DO, SPI_led); input CLK, RSTN; input [23:0] s_tdata; input s_tvalid; output s_tready; input SPI_DO; output SPI_DI, SPI_ENB, SPI_CLK; output [7:0] SPI_led; wire [23:0] SPI_P2S_in; wire SPI_RST; reg [23:0] P2S_reg; reg [4:0] P2S_counter; reg P2S_state; reg [7:0] SPO_reg; wire P2S_empty, P2S_full; wire P2S_reset, P2S_WR; wire SPO_ENB; always@(posedge CLK) if (P2S_reset) P2S_state <= 0; else if (P2S_WR) P2S_state <= 1; always @(posedge CLK) if (P2S_reset) P2S_counter <= 0; else if (P2S_full) P2S_counter <= P2S_counter + 1; always @(posedge CLK) if (P2S_WR) P2S_reg <= SPI_P2S_in; always@(posedge CLK) if (SPI_RST) SPO_reg <= 8'b0; else if (SPO_ENB) SPO_reg <= {SPO_reg[6:0],SPI_DO}; assign SPI_P2S_in = s_tdata[23:0]; assign SPI_RST = ~RSTN; assign P2S_reset = SPI_RST | (P2S_full & (P2S_counter == 31)); assign P2S_empty = (P2S_state == 0); assign P2S_full = (P2S_state == 1); assign s_tready = ~P2S_full; assign P2S_WR = s_tvalid & s_tready; // Output wire [4:0] P2S_index; assign P2S_index = 23-P2S_counter; assign SPI_DI = P2S_reg[P2S_index]; assign SPI_CLK = CLK; // SPI_ENB assign SPI_ENB = ~(P2S_full & (P2S_counter < 24)); assign SPO_ENB = P2S_full & ((P2S_counter >= 16) & (P2S_counter <= 23)); assign SPI_led = SPO_reg; endmodule ``` 完成後拉進Block design,如跳出SPI_CLK的Waring的話沒關係,那是Output之後會用到的變數。 **Async FIFO**的Master要接到這裡的Slave,因為Master前面的CLK是CLK_4,這裡也要接同一個比較慢的CLK。 RSTN接系統的,SPI_led是讀出來的資料,要接回**GPIO**的GPIO2,這樣才能用PS端去Access。 剩下4個腳位都Make external,要把名字預設的_0拿掉以符合Constraints。 SPI_DO是從AD9361讀資料回來的Port。 ### Zedboard_AD9361_SPI.xdc **Add source $\to$ Add or create Constraints $\to$ Create file** **File name: Zedboard_AD9361_SPI** ```0 # constraints # Zedboard AD9361 SPI set_property -dict {PACKAGE_PIN F18 IOSTANDARD LVCMOS25 PULLTYPE PULLUP} [get_ports SPI_ENB]; ## D26 FMC_LPC_LA26_P set_property -dict {PACKAGE_PIN E18 IOSTANDARD LVCMOS25} [get_ports SPI_CLK]; ## D27 FMC_LPC_LA26_N set_property -dict {PACKAGE_PIN E21 IOSTANDARD LVCMOS25} [get_ports SPI_DI]; ## C26 FMC_LPC_LA27_P set_property -dict {PACKAGE_PIN D21 IOSTANDARD LVCMOS25} [get_ports SPI_DO]; ## C27 FMC_LPC_LA27_N ``` 基本上就是接我們包裝的腳位和邏輯系統的腳位,同時定義輸出的電壓的型態。 這裡可以驗證SPI,跟上一個章節一樣,不過這邊先跳過。 接下來要接DAC的路徑。 ## DAC DAC(Digital-to-Analog Converter)數位類比轉換器 DAC會以均勻的時間間格輸出類比電壓值。 每完成一次轉換,轉換器的輸出值能夠迅速從上一個輸出值更新為當前的輸出值。 每次讀到一個輸入值會轉換並產生一個相對的輸出電壓,輸出電壓會保持恆定直到下一個輸入值轉換完成。 這種因取樣率不同而產生不同的訊號輸出類似階躍函數,會造成Nyquist frequency以上的諧波,以取樣率的倍數在頻域上產生頻譜鏡像,這些頻譜上的干擾可以藉由低通濾波器消除。 ![](https://i.imgur.com/W6auqBK.png) [Reference](https://zh.wikipedia.org/zh-tw/%E6%95%B8%E4%BD%8D%E9%A1%9E%E6%AF%94%E8%BD%89%E6%8F%9B%E5%99%A8) ![](https://i.imgur.com/CxFLjdu.png) 在NCU_SDR_Verilog_LIB_2022中可以直接加入已經寫好的Source,在AD9361_IP中。 ![](https://i.imgur.com/0IDNlv0.png) 不過這邊也可以自己寫。 ### ADI DAC Interface AXIS **Add source $\to$ Add or create source $\to$ Create file** **File name: ADI_DAC_Interface_AXIS** ```0 module ADI_DAC_Interface_AXIS( input RSTN, CLK, input DAC_TX_tvalid, output DAC_TX_tready, input [11:0] DAC_TX1_I, DAC_TX1_Q, DAC_TX2_I, DAC_TX2_Q, //AD9361 DAC port input ADI_DATA_CLK, output ADI_FB_CLK, ADI_TX_FRAME, output [11:0] ADI_P1_D); wire DAC_TX_en, DAC_TX_WR; assign DAC_TX_tready = DAC_TX_en; assign DAC_TX_WR = DAC_TX_tvalid & DAC_TX_tready; wire TX_CLK, TX_CLK_A; DELAY_CLOCK #(.DEPTH(4)) DC (.I(ADI_DATA_CLK), .O(ADI_FB_CLK)); assign TX_CLK = ADI_DATA_CLK; reg ADI_TX_Frame_reg; always@(posedge TX_CLK) if(~RSTN) ADI_TX_Frame_reg <= 0; else ADI_TX_Frame_reg <= ~ADI_TX_Frame_reg; assign ADI_TX_FRAME = ADI_TX_Frame_reg; //// Asynchronous FIFO Buffer for DAC wire DAC_BF_full, DAC_BF_WR, DAC_BF_empty, DAC_BF_RD; wire [47:0] DAC_BF_in, DAC_BF_out; Asyn_FIFO_48x512 DAC_BF (.rst(~RSTN), .wr_clk(CLK), .wr_en(DAC_BF_WR), .din(DAC_BF_in), .full(DAC_BF_full), .rd_clk(TX_CLK), .rd_en(DAC_BF_RD), .dout(DAC_BF_out), .empty(DAC_BF_empty) ); assign DAC_BF_in = {DAC_TX1_I, DAC_TX1_Q, DAC_TX2_I, DAC_TX2_Q}; assign DAC_BF_WR = DAC_TX_WR; assign DAC_TX_en = ~DAC_BF_full; assign DAC_BF_RD = ~DAC_BF_empty & ~ADI_TX_FRAME; /// Dual-IQ Interface wire [11:0] DAC_I_1, DAC_Q_1, DAC_I_2, DAC_Q_2; DDR_IQ_DAC IQ_TX (.I_in1(DAC_I_1), .Q_in1(DAC_Q_1), .I_in2(DAC_I_2), .Q_in2(DAC_Q_2), .DDR_out(ADI_P1_D), .ADI_FB_CLK(TX_CLK), .ADI_TX_FRAME(ADI_TX_FRAME)); assign DAC_I_1 = DAC_BF_out[47:36]; assign DAC_Q_1 = DAC_BF_out[35:24]; assign DAC_I_2 = DAC_BF_out[23:12]; assign DAC_Q_2 = DAC_BF_out[11:0]; endmodule ``` 其中```DELAY_CLOCK #(.DEPTH(4))```的DEPTH(4)滿critical的。 >老師說之前是用DEPTH(2),可能會有不一樣的結果。 在Source欄看應該會缺DELAY_CLOCK, DDR_IQ_DAC, Asyn_FIFO_48x512。 ### DELAY CLOCK **Add source $\to$ Add or create source $\to$ Create file** **File name: DELAY_CLOCK** ```0 `timescale 1ns/1ps module DELAY_CLOCK(I, O); parameter DEPTH = 0; //----------------------------------- input I; output O; //----------------------------------- (* KEEP="TRUE" *) wire [DEPTH:0] D; //----------------------------------- assign D[0] = I; BUFG U_BUFG(.I(D[DEPTH]), .O(O)); generate genvar i; for(i = 0; i < DEPTH; i = i + 1) begin : LEVEL LUT1 #(.INIT(2'b10)) U_LUT1(.I0(D[i]), .O(D[i+1])); end endgenerate endmodule ``` ### DDR IQ DAC **Add source $\to$ Add or create source $\to$ Create file** **File name: DDR_IQ_DAC** DDR是Double Data Rate。 ```0 module DDR_IQ_DAC(I_in1, Q_in1, I_in2, Q_in2, DDR_out, ADI_FB_CLK, ADI_TX_FRAME); input [11:0] I_in1, Q_in1, I_in2, Q_in2; input ADI_FB_CLK; input ADI_TX_FRAME; output [11:0] DDR_out; reg [11:0] DD_I, DD_Q, D_Q; reg [11:0] D_I2, D_Q2; always@(negedge ADI_FB_CLK) begin D_I2 <= I_in2; D_Q2 <= Q_in2; end always@(negedge ADI_FB_CLK) if (~ADI_TX_FRAME) begin DD_I <= I_in1; D_Q <= Q_in1; end else begin DD_I <= D_I2; D_Q <= D_Q2; end always@(posedge ADI_FB_CLK) begin DD_Q <= D_Q; end generate genvar i; for(i=0;i<=11;i=i+1) begin ODDR #(.DDR_CLK_EDGE("OPPOSITE_EDGE"), .INIT(0), .SRTYPE("SYNC")) U_ADI_FB_CLK (.R(1'b0), .S(1'b0), .CE(1'b1), .C(ADI_FB_CLK), .D1(DD_I[i]), .D2(DD_Q[i]), .Q(DDR_out[i])); end endgenerate endmodule ``` 其中```ODDR```是內建的IP,似乎有不同的IC這裡會換掉。 ### Asyn FIFO 48x512 48由AD9361的規格給定,4路的I_1, Q_1, I_2, Q_2都為12 bits。 普通在Block design的add IP會直接在Block design產生的IP,但是因為現在要給Source作為component使用,所以要使用左列的**IP catalog**。 **IP catalog $\to$ FIFO generator $\to$ Customize IP** ![](https://i.imgur.com/1HXoBUf.png) 在Interface type的地方,因為這邊是AXIS,所以選擇不是AXI的**Native** FIFO implementation要選用Independent clock的三者之一。 FPGA裡面有一些資源是hardware的Block RAM,另外Slice裡面有一些register是Distributed RAM。 不過Distributed RAM最好留給邏輯用,所以如果block RAM夠或是不要求速度太高,用Block RAM就好。 這裡選用**distributed RAM**,會只有一個rst,Block RAM會有wr_rst和rd_rst。 ![](https://i.imgur.com/MIfZp4Y.png) 接著看Native port。 標準的FIFO在read的時候會慢一個CLK才把資料送出來,而且CLK是real time CLK,是系統給的不是能自己控制的,有的時候加上buffer甚至可以慢到兩個CLK。 **First word fall through**是在memory外面加一些邏輯弄成像FIFO,不會慢一個CLK。 做FIFO最簡單的方式是circular buffer,基本上就是memory加上counter 例如設一個深度16的circular buffer,寫的時候從[0]開始寫,可以一邊寫一邊讀,這樣[0]就會是最舊的資料,就會產生一個延遲16的buffer。 不過有的時候深度會包含一些buffer在裡面,所如果要確保延遲的CLK數的話最好自己寫一個Counter。 寬度如上面所說的為48,深度則為512。 Xilinx的FPGA裡面以前block RAM一組是18k bits $\times$ 1,現在大部分是36k bits $\times$ 1。 這個數值是可以configurable,可以改成例如16k bits $\times$ 1也是用一組block RAM,但是深度的最小單位就是512,所以如果設定比512小其所占的memory變小,反而會浪廢掉後面所有的memory,所以這邊才會設深度為512。 這個問題可能可以藉由DMA等其他技術去解決。 ![](https://i.imgur.com/1t5zy2w.png) 接著把Component name改掉,改成跟上面缺的東西一樣的名字**Asyn_FIFO_48x512**,OK後在Generate之前,因為有多個Source都會用到它,把Synthesis options改成Global。 ![](https://i.imgur.com/puz4J4R.png) 完成後再確認一次是否有缺其他東西,把**ADI_DAC_Interface**拉到Block design中。 跟**SPI**的部分相比,這裡的**Async FIFO**就是建立在IP內的。 ![](https://i.imgur.com/xyOieWf.png) 輸出是ADI_FB_CLK、ADI_TX_FRAME和ADI_P1_D,輸入是ADI_DATA_CLK,這4個Port都跟AD9361直接連接,所以直接Make external。 CLK和RSTN都是接系統的,至此Template就完成了,現在缺的是信號源。 下一步可以拉**DDS**做信號源來驗證,也可以拉**SG_LUT**來作,前者會比後者複雜一些。 --- # Tx with DDS 以**DDS**做信號源。 信號$s[n]=A\cdot \exp(j\cdot\omega\cdot n)=A\cdot\exp(j\cdot\theta[n])$ $A$: 振幅,$\omega=\frac{2\pi}{16}\equiv\frac{2^{Q=16}}{16}=4096$,$\theta[n]=\theta[n-1]+\omega$,$\theta[0]=\theta_0$ initial phase。 ## DDS DDS(Direct Digital Synthesizer)直接頻率合成器 DDS使用數位方法產生類比波形,提供數位編成能力、更高的整合度與更低的成本。 DDS幾乎能瞬間改變頻率與相位,成為相當重要的穩定RF來源。 原理上以相位累加器為基礎,相位累加器會產生波形的瞬時相位。查找表可進行相位對振幅的轉換,再套用到DAC上,經過濾波後產生出所需的類比輸出。 ![](https://i.imgur.com/TR4iwZc.png) 輸出會接到DAC,但是DAC的輸入會成為一連串取樣值,因而產生階躍。而階躍會以取樣率的倍數在頻域上產生頻譜鏡像,所以DAC後的輸出要加上低通濾波器濾掉這些鏡像的頻譜響應。 [Reference](https://www.digikey.tw/zh/articles/the-basics-of-direct-digital-synthesizers-ddss) ### Phase accumulator 相位累加器由簡單的加法器組成,一般為24~48位元,用意在把$2\pi$用幾個位元表示。 $N$位元的相位累加器會把$\begin{Bmatrix} 0,2\pi \end{Bmatrix}$這個區間分成$2^N$等分,每等分代表一個相位差$\Delta\theta$。 每隔一個CLK,相位累加器會增加一次相位幅度,幅度由$\Delta\theta$與調諧字元值$M$決定。 調諧字元值$M$表示每一次相位增幅要取$M$個$\Delta\theta$,可以用下圖來理解。 ![](https://i.imgur.com/Cr4u1KZ.png) 所以DDS的輸出頻率由給定的$M$決定,可以用數學式來表達: $f_{out}=\frac{M}{2^N}\cdot f_{CLK}$ $M$的最大值通常定為$2^{N-1}$,以滿足Nyquist frequency。 在這個例子當中,是拿16 bits當$2\pi$,即為$N=16$。 [Reference](https://www.digikey.tw/zh/articles/the-basics-of-direct-digital-synthesizers-ddss) ### DDS 從NCU_SDR_Verilog_LIB_2022加入已經寫好的Source,在CORDIC_IP中。 ![](https://i.imgur.com/p6SJzXF.png) CORDIC rotation是一層一層的,AXIS是上層,First stage比較特別的所以獨立寫,剩下的都在STAGE中。 這個CORDIC rotation就是用CORDIC演算法去做到某種精準度的相位旋轉。 **DDS**也是獨立另外寫的。 **DDS**會需要Counter,這裡寫在裡面了,可以打開來確認如下: ```0 always @(posedge CLK) if (~RSTN) Phase_reg <= Phase_V; else if (DDS_en) Phase_reg <= Phase_reg + Freq_V; ``` >DDS_en是控制鍵,就算假定都是1也寫一下比較好。 確認一下**DDS**的內容設定: ```0 module Direct_Digital_Synthesizer(CLK, RSTN, m_axis_tvalid, m_axis_tready, m_axis_tdata, Frequency); input CLK, RSTN; input m_axis_tready; output m_axis_tvalid; output [31:0] m_axis_tdata; // {I_out,Q_out} input [15:0] Frequency; // parameter Frequency = 1024, parameter Phase_init = 0, I_init = 16384, Q_init = 0; ``` 可以發現這個**DDS**沒有```input```,只有初始設定```parameter```,所以需要改設定的話要到這裡面直接改。 這裡因為我們的Frequency要從外部的**GPIO**給,所以把```parameter```裡面的Frequency註解掉,補上module的參數和```input [15:0] Frequency;```。 完成後就可以把**DDS**丟進Block design中 ![](https://i.imgur.com/RTm1LW5.png) ## Connection 開始接線。 **DDS**的CLK和**DAC**的CLK接系統的CLK。 **DDS**的RSTN和**DAC**的RSTN接**Reset**的RSTN。 **DDS**作為Source,把Input從**DDS**的Master: m_axis接到**DAC**的Slave: DAC_TX: 1. tvalid接tvalid 2. tready接tready 3. tdata接2個**Slice** tdata 32 bits要接到 I 16 bits 和 Q 16 bits 一般的習慣是把 I 和 Q 合併成32 bits MSB 16 bits I + LSB 16 bits Q 4. tdata[31:0]接**Slice_0**接DAC_TX1_I[11:0] 信號的振幅是16384,可以參見**DDS**的設定,所以盡量抓前面的。 信號```Din From 31```取12 bits```Din Down To 20```,丟掉4 bits以防爆掉。 做法上是input拉大一點,減少運算上的誤差,丟資料出去的時候再只丟前面的就好,所以這邊丟掉的是[19:16]。 原先資料是16 bits的話,最高振幅不要超過2的15次方,-32768~32767。 5. DAC_TX2_Q[11:0]接**Slice_0**的out 另一路故意接相反。 6. tdata[31:0]接**Slice_1**接DAC_TX1_Q 複製前面**Slice**,這次要取 Q 的部分 信號```Din From 15```取12 bits```Din Down To 4```,丟掉[3:0]。 7. DAC_TX2_I[11:0]接**Slice_1**的out 另一路故意接相反。 8. 加入**AXI GPIO** 前面把**DDS**裡面的Frequency改成```input```,現在要用**GPIO**接上去。 在Configuration中,GPIO為All outputs,寬度為16 bits。 可以給Default output value,這樣在沒有config的時候也會有值,這裡先給定為```0x00001000```,如此一來剛開機還沒有動作的時候也不會是直流電。 改名字為**axi_gpio_DDS**。 9. Run Connection Automation GPIO是我們自己要接的不要勾,勾S_AXI就好。 10. **GPIO**接**DDS**的Frequency GPIO接**DDS**的Frequency時如果顯示no matching,展開GPIO應該就能接上了。 ## Generate Big Stream 至此發射機就完成了。 按下存檔的話,在Source欄應該可以看到**DAC**和**DDS**都被併到Design裡面去了。 Validate確認看看有沒有問題。 Create HDL Wrapper。 ![](https://i.imgur.com/5ZFb1Hb.png) 確認**ADC_DAC**和**SPI**的Constraint file有沒有加上去,內容也可以對一下。 確認沒有問題的話就可以Generate Big Stream。 ## Jupyter 記得確認變數名稱有沒有改過。 定義會用到的函數。 ```0 from pynq import Overlay import time import numpy as np from pynq.lib import AxiGPIO ol = Overlay("Tx_2022.04.22.bit") ol? def PYNQ_SPIWrite(x, y): mask = 0xffffffff ADI9361_Conf_comm = int(x + y, 16)+2**24+2**23 SPI_WR_channel.write(0x0, mask) SPI_WR_channel.write(ADI9361_Conf_comm, mask) #SPIWrite print(x) def PYNQ_SPIRead(x): mask = 0xffffffff ADI9361_Conf_comm = int(x + '00', 16) + 2**24 SPI_WR_channel.write(0x0, mask) SPI_WR_channel.write(ADI9361_Conf_comm, mask) #SPIWrite SPI_DO = SPI_RD_channel.read() SPI_DO = hex(SPI_DO) print(SPI_DO) def DDS_Write(x): DDS_Conf_comm = (2**16 + x) DDS_channel.write(0x0, mask) DDS_channel.write(DDS_Conf_comm, mask) #SPIWrite mask = 0xffffffff SPI_ip = ol.ip_dict['AD9361_SPI/axi_gpio_AD9361_SPI'] SPI_WR_channel = AxiGPIO(SPI_ip).channel1 SPI_RD_channel = AxiGPIO(SPI_ip).channel2 LED_sw_ip = ol.ip_dict['axi_gpio_LED_SW'] LED_channel = AxiGPIO(LED_sw_ip).channel1 SW_channel = AxiGPIO(LED_sw_ip).channel2 DDS_ip = ol.ip_dict['axi_gpio_DDS'] DDS_channel = AxiGPIO(DDS_ip).channel1 ``` 可以確認一下```ol?```裡面的變數名稱。 接著測試SPI可不可以Read和Write。 ```0 PYNQ_SPIWrite('271','09') PYNQ_SPIRead('271') a = SW_channel.read() print(a) LED_channel.write(a,mask) ``` 結果應該要跟**SPI**中的一樣,是的話代表有連上。 後要跑Matlab的Config。 ```0 import socket # HOST = '192.168.2.99' # default HOST = '192.168.50.39' ctrl_port = 8000 data_port = 8001 server_data = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_data.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #作業系統會在伺服器socket被關閉或伺服器程序終止後馬上釋放該伺服器的埠,否則作業系統會保留幾分鐘該埠。 server_data.bind((HOST, data_port)) server_data.listen(5) server_ctrl = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_ctrl.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #作業系統會在伺服器socket被關閉或伺服器程序終止後馬上釋放該伺服器的埠,否則作業系統會保留幾分鐘該埠。 server_ctrl.bind((HOST, ctrl_port)) server_ctrl.listen(5) #################### print("wait for MATLAB Input") ### conn_data, addr_data = server_data.accept() conn_ctrl, addr_ctrl = server_ctrl.accept() mask = 0xffffffff data = np.zeros(1,dtype=np.int) ADI = 0 # for i in range(16) : #conn_data.recv_into(data,16) # 16-bytes i = 0 while True: print("wait for MATLAB Input ") i = i + 1 print(i) conn_data.recv_into(data,4, socket.MSG_WAITALL) print("Receive from MATLAB Input") ADI = data[0] a = ADI.item() print(a) if a >= 25427968: print('Configuration Finished') break SPI_channel.write(0x0, mask) SPI_channel.write(a, mask) # SPIWrite 009,17 // Enable Clocks # data = conn_data.recv(16) ``` ```HOST```的IP位置要記得改。 這個Matlab的Config要配上一個Matlab的程式: >PYNQ_AD9361_PP_AGC.m >裡面tcpip的IP位置也要改。 Run這段的話結果應該會輸出:wait for MATLAB Input。 ![](https://i.imgur.com/WEerYZ6.png) 接著就到Matlab去跑上面的那個 .m檔。 ```0 global ctrl_client data_client ctrl_client = tcpip('192.168.2.99', 8000, 'NetworkRole', 'client', 'Timeout', 10); %QAM %ctrl_client = tcpip('192.168.2.99', 8002, 'NetworkRole', 'client', 'Timeout', 5); %decoder fopen(ctrl_client); data_client = tcpip('192.168.2.99', 8001, 'NetworkRole', 'client', 'Timeout', 10); %QAM %data_client = tcpip('192.168.2.99', 8003, 'NetworkRole', 'client', 'Timeout', 5); %decoder data_client.InputBufferSize = 2^20; data_client.OutputBufferSize = 2^20; data_client.ByteOrder='littleEndian'; %'bigEndian'; %; % fopen(data_client); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% PYNQ_SPIWrite('3DF', '01'); PYNQ_SPIWrite('2A6', '0E’); … ``` .m檔當中的```PYNQ_SPIWrite```函數可以由下面給定: ```0 % Addr: 10 bits address in hex % Data: 8 bits data in hex % e.g. PYNQ_SPIWrite('3DF', '01'); % data_client: TCP/IP client port function A=PYNQ_SPIWrite(Addr,Data) global data_client A=hex2dec(['18' Addr Data]); fwrite(data_client,A,'uint32') end ``` ```tcpip```的IP位置也要記得改,如果有成功的話應該會如下圖。 ![](https://i.imgur.com/9hd9cNc.png) 這邊總共寫入了2585行,這樣就算Condig完了。 ## Spectrum 完成前面的步驟且都沒有問題的話,接下來可以接上頻譜分析儀,應該看的到東西。 在Juypter中可以給定產生訊號的頻率大小: ```0 DDS_Write(2048) # check ``` 根據公式,$f_{out}=\frac{2048}{2^{16}}\cdot Fc=3.125$MHz Matlab的.m檔裡面也可以更改頻率: > PYNQ_SPIWrite('271','78')是2.4GHz? 也可以在Jupyter上面驗證: ```0 print('Tx Carrier Frequency') PYNQ_SPIRead('???') ... ``` --- # Tx with SG_LUT SG_LUT: Signal Generator Look Up Table。 ![](https://i.imgur.com/7eYOHLC.png) ![](https://i.imgur.com/DYBFtrd.png) ## SG LUT **Add source $\to$ Add or create source $\to$ Create file** **File name: SG_LUT** ```0 module SG_LUT(CLK, RSTN, SG_out_tdata_I, SG_out_tdata_Q, SG_out_tvalid, SG_out_tready); input CLK, RSTN; output [11:0] SG_out_tdata_I, SG_out_tdata_Q; output SG_out_tvalid; input SG_out_tready; wire [11:0] LUT_I [0:15]; wire [11:0] LUT_Q [0:15]; reg [3:0] LUT_addr; wire LUT_RD; always @(posedge CLK) if (~RSTN) LUT_addr <= 0; else if (LUT_RD) LUT_addr <= LUT_addr + 1; assign LUT_I[0] = 16384; // = ???; assign LUT_I[1] = 15137; // … assign LUT_I[2] = 11585; assign LUT_I[3] = 6270; assign LUT_I[4] = 0; assign LUT_I[5] = -6270; assign LUT_I[6] = -11585; assign LUT_I[7] = -15137; assign LUT_I[8] = -16384; assign LUT_I[9] = -15137; assign LUT_I[10] = -11585; assign LUT_I[11] = -6270; assign LUT_I[12] = 0; assign LUT_I[13] = 6270; assign LUT_I[14] = 11585; assign LUT_I[15] = 15137; assign LUT_Q[0] = 0; assign LUT_Q[1] = 0; assign LUT_Q[2] = 0; assign LUT_Q[3] = 0; assign LUT_Q[4] = 0; assign LUT_Q[5] = 0; assign LUT_Q[6] = 0; assign LUT_Q[7] = 0; assign LUT_Q[8] = 0; assign LUT_Q[9] = 0; assign LUT_Q[10] = 0; assign LUT_Q[11] = 0; assign LUT_Q[12] = 0; assign LUT_Q[13] = 0; assign LUT_Q[14] = 0; // … assign LUT_Q[15] = 0; // = ???; assign SG_out_tdata_I = LUT_I[LUT_addr]; assign SG_out_tdata_Q = LUT_Q[LUT_addr]; assign SG_out_tvalid = 1; assign LUT_RD = SG_out_tvalid & SG_out_tready; endmodule ``` 註解中的```???```要自己給定,這邊在Matlab給定一個信號,振幅$2^{14}$,頻率剛好1個週期: ```0 s_B = round(2^14*exp(j*2*pi/16*[0:15])); s_I = real(s_B).' ``` 把這裡面的值填入 I 就會變成上面的樣子,為求方便 Q 先都給0。 做好之後把**SG_LUT**拉出來到Block design。 CLK和RSTN接系統的,tvalid和tready接到**DAC**的tvalid和tready上。 I 和 Q 接到**DAC**的 I 和 Q 上,**DAC**的第二個 I 和 Q 接可以反著接回 Q 和 I。 ## Constraints **Add source $\to$ Add or create Constraints $\to$ Create file** **File name: Zedboard_AD9361_ADC_DAC** ```0 # constraints # Zedboard AD9361 LVCMOS DDR, FDD, Dual-Port set_property -dict {PACKAGE_PIN M19 IOSTANDARD LVCMOS25} [get_ports ADI_DATA_CLK]; ## G6 FMC_LPC_LA00_CC_P set_property -dict {PACKAGE_PIN N19 IOSTANDARD LVCMOS25} [get_ports ADI_RX_FRAME]; ## D8 FMC_LPC_LA01_CC_P set_property -dict {PACKAGE_PIN J21 IOSTANDARD LVCMOS25} [get_ports ADI_FB_CLK]; ## G12 FMC_LPC_LA08_P set_property -dict {PACKAGE_PIN R20 IOSTANDARD LVCMOS25} [get_ports ADI_TX_FRAME]; ## D14 FMC_LPC_LA09_P set_property -dict {PACKAGE_PIN N18 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[0] ]; ## H17 FMC_LPC_LA11_N set_property -dict {PACKAGE_PIN N17 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[1] ]; ## H16 FMC_LPC_LA11_P set_property -dict {PACKAGE_PIN P21 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[2] ]; ## G16 FMC_LPC_LA12_N set_property -dict {PACKAGE_PIN P20 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[3] ]; ## G15 FMC_LPC_LA12_P set_property -dict {PACKAGE_PIN M17 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[4] ]; ## D18 FMC_LPC_LA13_N set_property -dict {PACKAGE_PIN L17 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[5] ]; ## D17 FMC_LPC_LA13_P set_property -dict {PACKAGE_PIN T19 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[6] ]; ## C15 FMC_LPC_LA10_N set_property -dict {PACKAGE_PIN R19 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[7] ]; ## C14 FMC_LPC_LA10_P set_property -dict {PACKAGE_PIN K20 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[8] ]; ## C19 FMC_LPC_LA14_N set_property -dict {PACKAGE_PIN K19 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[9] ]; ## C18 FMC_LPC_LA14_P set_property -dict {PACKAGE_PIN J17 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[10] ]; ## H20 FMC_LPC_LA15_N set_property -dict {PACKAGE_PIN J16 IOSTANDARD LVCMOS25} [get_ports ADI_P0_D[11] ]; ## H19 FMC_LPC_LA15_P set_property -dict {PACKAGE_PIN P18 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[0] ]; ## H8 FMC_LPC_LA02_N set_property -dict {PACKAGE_PIN P17 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[1] ]; ## H7 FMC_LPC_LA02_P set_property -dict {PACKAGE_PIN P22 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[2] ]; ## G10 FMC_LPC_LA03_N set_property -dict {PACKAGE_PIN N22 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[3] ]; ## G9 FMC_LPC_LA03_P set_property -dict {PACKAGE_PIN M22 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[4] ]; ## H11 FMC_LPC_LA04_N set_property -dict {PACKAGE_PIN M21 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[5] ]; ## H10 FMC_LPC_LA04_P set_property -dict {PACKAGE_PIN K18 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[6] ]; ## D12 FMC_LPC_LA05_N set_property -dict {PACKAGE_PIN J18 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[7] ]; ## D11 FMC_LPC_LA05_P set_property -dict {PACKAGE_PIN L22 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[8] ]; ## C11 FMC_LPC_LA06_N set_property -dict {PACKAGE_PIN L21 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[9] ]; ## C10 FMC_LPC_LA06_P set_property -dict {PACKAGE_PIN T17 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[10] ]; ## H14 FMC_LPC_LA07_N set_property -dict {PACKAGE_PIN T16 IOSTANDARD LVCMOS25} [get_ports ADI_P1_D[11] ]; ## H13 FMC_LPC_LA07_P set_property -dict {PACKAGE_PIN J20 IOSTANDARD LVCMOS25} [get_ports ADI_ENABLE]; ## G18 FMC_LPC_LA16_P set_property -dict {PACKAGE_PIN K21 IOSTANDARD LVCMOS25} [get_ports ADI_TXNRX]; ## G19 FMC_LPC_LA16_N ``` 接下來就可以用Jupyter來驗證結果。 --- # Verification of DAC/ADC interface AD9361 Data interface ![](https://i.imgur.com/ehmFsKU.png) 用模擬的方式去做DAC和ADC的介面。 因為我們會用SPI的programing去控制AD9361的狀態,所以下面的enable和TXNRX不用去接。 ## DAC interface ![](https://i.imgur.com/6TZnMOy.png) 外接的CLK,上面我們現在的設定式30.72的兩倍所以大概60MHz;下面的CLK是系統設定的100MHz。 事實上系統的CLK設定不用到100MHz,至少要能夠讓Tx_Source使用(60/2MHz)就好。 ![](https://i.imgur.com/FxxJyKC.png) 2T2R就是會有兩對 I 和 Q channel。 設計上一個CLK會傳一個 I1 和一個 Q1,下一個CLK會傳一個 I2 和一個 Q2。 現在的CLK是由~60MHz的CLK給定。 所以Tx_Source的速度會是一半,就是大概30MHz。 需要的東西有: 1. ADI_DAC_Interface_AXIS.v 2. DELAY_CLOCK.v 3. DDR_IQ_DAC.v 4. Asyn_FIFO_48x512 (FIFO Generator IP) 5. Tx_Source Tx_Source可以用DDS,但是這邊只是模擬結果而已所以不用那麼複雜。 這邊的Source是用Counter: ![](https://i.imgur.com/Hxa1LM9.png) ```0 module Counter_AXIS(CLK, RSTN, C_en, m_axis_tvalid, m_axis_tready, m_axis_tdata, m_axis_tlast); parameter DW = 16; parameter C_init = 0, C_inc = 1; parameter CP_log2 = 4, CP_size = 2**CP_log2; input CLK, RSTN; output m_axis_tvalid, m_axis_tlast; input m_axis_tready, C_en; output [DW-1:0] m_axis_tdata; reg [DW-1:0] CC; wire CC_en, CC_last; always @(posedge CLK) if (~RSTN) CC <= C_init; else if (CC_en) CC <= CC + C_inc; assign m_axis_tvalid = C_en; assign CC_en = m_axis_tvalid & m_axis_tready; assign m_axis_tdata = CC; assign CC_last = (CC[CP_log2-1:0] == (CP_size - 1)); assign m_axis_tlast = CC_last; endmodule ``` ### Design ![](https://i.imgur.com/uZSVQ15.png) #### CLK source 從Source加入/NCU_SDR_Verilog_LIB_2022/AD9361裡的**CLK_RSTN_G**。 從Source拉出兩個CLK至Block diagram中,一個是CLK Data是AD9361提供的Data,所以可以改名叫**CLK_DATA**;另一個**CLK_RSTN_G_0**保留原來的設定,作為系統的CLK。 ![](https://i.imgur.com/imc3Udg.png) CLK裡面有Half period (Half P),5表示100MHz,所以**CLK_DATA**要提供60MHz的話要改成8。 Start (St)是讓Reset晚一點上來,模擬比較真實的狀態。 #### DAC Interface AXIS 從Source加入/NCU_SDR_Verilog_LIB_2022/AD9361裡的**ADI_DAC_Interface_AXIS**。 如果這裡是從新的Project開始做的話,要記得加上D**ELAY_CLK**、**DDR_IQ_DAC**和**Asyn_FIFO_48x512**。 拉進來之後,ADI_DATA_CLK接**CLK_DATA**的CLK、CLK接**CLK_RSTN_G_0**的CLK。 **CLK_RSTN_G_0**的RSTN接系統的RSTM,**CLK_DATA**的RSTN先不接。 CLK信號就接好了,接著就要產生Tx_Source去產生DAC的3個output: ![](https://i.imgur.com/OyA70yg.png) #### Counter 從Source加入/NCU_SDR_Verilog_LIB_2022裡的**Counter_AXIS**。 這邊拿**Counter_AXIS**作為Tx_Source。 CLK接上系統的CLK,RSTN接RSTN,因為RSTN會為高電位所以C_en也接上RSTN。 設定上先把Dw改成12,因為 I1 I2 Q1 Q2 都是12bits。 先做 I1 的Data,所以把名稱改成**SG_i1**,設定中C increment (C Inc)為+1。 填入想輸入的數值,這邊預設為自己的學號,所以在C lnit填入學號: 因為相對於12bits的大小,學號太大,所以這邊改成用mod(學號,2^12) ![](https://i.imgur.com/ToEMMIQ.png) 完成後把輸出的Data接到Tx1的 I ,然後複製起來做 I2 。 I2 的基本接線一樣,C Inc為+2,把輸出接到Tx2的 Q 就好。 Q1 的C Inc 為-1,Q2 的C Inc 為-2。 因為所有Counter的Valid都一直是High的,所以Valid接 I1 的就好。 Ready則是都接同一個就好了,就會變成用Ready來控制。 DAC的3個Output這邊Make external。 主要是因為寫出去的速度和讀進來的速度不一樣,所以要用Ready來控制。 這樣到時候就可以觀察 I1 I2 Q1 Q2 是怎麼跑的,時間點是不是對的。 完成後Validate然後Creat HDL wrapper。 ### Simulation 這次要看Simulation,所以要把**Source$\to$Simulation Source**裡面把這個Design Set as top。 完成後Run Simulation,Zoom fit應該會看到: ![](https://i.imgur.com/DivEbB8.png) 我們要關注的是ADI_P1_D_0的值,可以看到在前面有一些狀態造成比較晚才Ready出來,原因在Asyn FIFO上,不過這部分比較沒有掌握。 現在顯示的數字是hex的數字,可以**右鍵$\to$Radix$\to$Unsigned Decimal**看Dec的數值,這裡用Unsigned的原因在方便,不然在2048~4096之間的值會變負的。 放大之後可以跟前面**DAC interface**中的第二張圖做比較,看看順序是否為 I1 Q1 I2 Q2。 ![](https://i.imgur.com/4UpBhOP.png) 0之後的第一個數值應該要是打在C lnit上的數值,在這裡前面輸入的是2350。 按照先前的設定,1個Frame有2個CLK,每個Frame中依序為:[I1 +1][Q1 -1][I2 +2][Q2 -2]。 至此就可以算是驗證完DAC了。 ## ADC interface ![](https://i.imgur.com/iHUAm7m.png) 這邊一樣是AXIS的機制,此外為了要全速讀取,把Valid和Ready接起來。 讀出的出度跟在DAC寫進的速度一樣,都是60/2MHz。 ### Design ![](https://i.imgur.com/qf5OawR.png) ### ADC interface AXIS 從Source加入/NCU_SDR_Verilog_LIB_2022/AD9361裡的**ADI_ADC_Interface_AXIS**。 若之前沒有做過的話要在Source加入**DDR_IQ_ADC**。 接上系統的CLK和RSTN,自己的Valid和自己的Ready相接。 AD_DATA_CLK和**DAC interface**的ADI_FB_CLK相接,Rx_Frame接Tx_Frame,P0_D接P1_D。 實際上這些都是應該要接去AD9361的驗證板的FMC上的Connector,透過印刷電路板接到FPGA的特定腳位,所以把FPGA的特定角為寫出來就可以寫成Constraint file。 ADC的4路Input這邊Make external。 這樣就可以把DAC和ADC整合在一起。 ADC的驗證比較麻煩,在PYNQ的環境就要把資料錄到FIFO去再用DMA把資料讀回來,這個部分就是Rx Buffer Engine。 ### Simulation P1_D是在DAC已經看過的部分,這裡著重在Rx1_I Rx1_Q Rx2_I Rx2_Q,先把這5個port都變成Dec。 ![](https://i.imgur.com/klEkmVn.png) 雖然Rx比P1_D還有晚幾的CLK,不過可以仍然可以對數值。 RX1_I對到第1格,RX1_Q對到第2格,RX2_I對到第3格,RX2_Q對到第4格,符合所預想的。 至此ADC的驗證也算完成了。 這些都是模擬,至後就可以直接接AD9361的東西,到時候Constraints要弄好。 --- # Tx buffer engine Tx_Source原先是DDS或是Look Up Table提供信號。 如果我們希望特過Python或是Matlab寫到Hardware的PL上的記憶體空間,要重複播送的這一個模式就叫做**Tx buffer engine**。 ![](https://i.imgur.com/zm3w7Lc.png) 這個東西可以直接接Python或是**DMA**來做,但是需要先了解**Tx buffer engine**在幹嘛、要怎麼用。 模組最重要的部分是<font color="#f00">紅色</font>圈起來的mode的部分,會加上一個GPIO在這裡來控制功能。這裡也可以改成用Write的。 比較大的IP是Controller,記憶空間是上面的FIFO。 設計上**Counter**是主動Real time的,會一直Valid,所以控制交由TXBE_TB的輸入值來控制。 ## Design ![](https://i.imgur.com/C0b91a8.png) 實際使用上如上圖,Python寫到**DMA**由MM2S會接到TXBE_in上面,tdata會接到**DAC**上面。這邊先模擬。 步驟照著左上角做,0的reset可以清空FIFO換下一組資料使用。 ![](https://i.imgur.com/Z2ZBwdt.png) ### TXBE Controller ![](https://i.imgur.com/VUuOJES.png) ```0 module TXBE_Controller(CLK, RSTN, TXBE_mode_in, TXBE_in_tdata, TXBE_in_tready, TXBE_in_tvalid, TXBE_out_tdata, TXBE_out_tready, TXBE_out_tvalid, FF_in_tdata, FF_in_tready, FF_in_tvalid, FF_out_tdata, FF_out_tready, FF_out_tvalid, FF_rstn, TXBE_RST_mode, TXBE_WR_mode, TXBE_CPB_mode, TXBE_SPB_mode); parameter DW = 32; input CLK, RSTN; input [1:0] TXBE_mode_in; output TXBE_in_tready, TXBE_out_tvalid, FF_out_tready, FF_in_tvalid, FF_rstn; input TXBE_in_tvalid, TXBE_out_tready, FF_in_tready, FF_out_tvalid; input [DW-1:0] TXBE_in_tdata, FF_out_tdata; output [DW-1:0] TXBE_out_tdata, FF_in_tdata; output TXBE_RST_mode; // Buffer Reset mode output TXBE_WR_mode; // Buffer Write mode output TXBE_CPB_mode; // Buffer Cyclic Playback mode output TXBE_SPB_mode; // Buffer Single Playback mode // TX Mode/Parameter Configuration wire [1:0] TX_mode; assign TX_mode = TXBE_mode_in; assign TXBE_RST_mode = (TX_mode == 0); assign TXBE_WR_mode = (TX_mode == 1); assign TXBE_CPB_mode = (TX_mode == 2); assign TXBE_SPB_mode = (TX_mode == 3); // Data Path: assign FF_in_tdata = TXBE_CPB_mode ? FF_out_tdata : TXBE_in_tdata; assign TXBE_out_tdata = TXBE_out_tvalid ? FF_out_tdata : 0; // Control Signal: wire FF_RD, FF_WR; assign FF_WR = FF_in_tvalid & FF_in_tready; assign FF_RD = FF_out_tvalid & FF_out_tready; assign TXBE_in_tready = TXBE_WR_mode ? FF_in_tready : 0; assign TXBE_out_tvalid = (TXBE_CPB_mode | TXBE_SPB_mode) ? FF_out_tvalid : 0; assign FF_out_tready = (TXBE_CPB_mode | TXBE_SPB_mode) ? TXBE_out_tready : 0; assign FF_in_tvalid = TXBE_WR_mode ? TXBE_in_tvalid : (TXBE_CPB_mode & FF_RD); assign FF_rstn = RSTN & ~TXBE_RST_mode; endmodule ``` 如此一來就可以用程式軟體產生任意的波型,存在TX_BF_Engine裡面,重複播放出去。 Size由FIFO的大小決定。 ### TXBE TB 原則上就是設定延遲幾個CLK時要做什麼事,TXBE_mode要給多少值 ```0 module TXBE_TB(TXBE_mode_in); output [1:0] TXBE_mode_in; reg [1:0] A; initial begin A = 0; // Reset #800.1 A = 1; // Load #160 A = 0; #400 A = 1; #220 A = 2; // Continuous Playback #400 A = 0; #100 A = 1; #320 A = 2; #300 A = 3; end assign TXBE_mode_in = A; endmodule ``` 控制的設計(模式或是狀態): 1. TXBE_mode = 0 → Reset Reset buffer (reset FIFO) 2. TXBE_mode = 1 → Down Load 從TXBE_in寫信號進來,記憶體在FIFO中所以寫去FIFO。 寫到DMA沒有傳資料的話就會停止,就可以切換到下一個mode。 3. TXBE_mode = 2 → Continuous_Playback 播放,把剛才寫進FIFO的資料丟出去。 由FF_out進來的資料會經過某些處理與判斷由tdata傳出去。 要循環,所以FIFO丟出來的資料會順便再寫回FIFO。 4. TXBE_mode = 3 → Load & Single_Playback 現在這裡沒有Load。 主要就是播的時候不再寫回FIFO,所以FIFO播完就空了,不會有後續的輸出。 ## Simulation ![](https://i.imgur.com/Incyuno.png) 除了tdata為其他都是狀態。 Valid和Ready會相同,所以只接Valid出來。 上面的4個mode分別對應到TXBE_mode的4種狀態,螢幕上面顯示從左到右的狀態變化分別為: **Reset $\to$ Write $\to$ Reset $\to$ Write $\to$ Continuous Playback** 所以對照下來,看tdata: **Reset**時tdata沒有值$\to$**Write**時沒有值$\to$**Reset**時把剛才寫的值清掉$\to$**Write**時再寫值進去$\to$**Continuous Playback**時重複播放剛才寫進去的值。 接者把tdata有寫值的部分放大來看: ![](https://i.imgur.com/bA5RMEo.png) 根據前面**TXBE_TB**的設計:```#220 A = 2; // Continuous Playback``` **Continuous Playback**前面的**Write**有220個CLK,這邊可以看到**Write**的時間從1360ns至1580ns,確實為220個CLK。 接著是:```#400 A = 0;``` **Continuous Playback**從1580ns開始至1980ns,這邊呈現出來的是8~21的連續撥放。 雖然寫資料的個數有點不符預期(原本預期220個CLK應該要有22個數值),但是可以看到確實是循環撥放,推測原因出在FIFO的Valid設定上。 >有試過如果給**Write**設定的時間從#320變成#420,則數值數量確實會增加10個。 ![](https://i.imgur.com/POeJNOV.png) 最後可以看到**Single Playback**的結果,前面的**Continuous Playback**讀寫到27停下來切成**Single Playback**,所以**Single Playback**播放完所有的東西到27就沒東西了,後面就變成0。 # Rx buffer engine Rx buffer engine就是隨時去錄信號,構造上同樣是Controller $+$ FIFO。 ## Design ![](https://i.imgur.com/6qoVdi9.png) ![](https://i.imgur.com/5hpzEul.png) ![](https://i.imgur.com/Yw92tBy.png) ### Rx Controller ![](https://i.imgur.com/nG3ahHU.png) ```0 module RXBE_Controller(CLK, RSTN, RXBE_s_tvalid, RXBE_s_tready, RXBE_s_tdata, // RX Buffer Engine input (from ADC) RXBE_m_tvalid, RXBE_m_tready, RXBE_m_tlast, RXBE_m_tdata, // RX Buffer Engine output (to DMA) RX_FIFO_rstn, RX_FIFO_s_tvalid, RX_FIFO_s_tready, RX_FIFO_s_tdata, // FIFO input interface (output) RX_FIFO_m_tvalid, RX_FIFO_m_tready, RX_FIFO_m_tdata, // FIFO output interface (input) RXBE_mode, RXBE_capture_size, // RXBE Status RXBE_C_out, RXBE_state, RXBE_reset_mode, RXBE_capture_mode, RXBE_upload_mode); input CLK, RSTN; input RXBE_s_tvalid, RXBE_m_tready; output RXBE_s_tready, RXBE_m_tvalid, RXBE_m_tlast; input [23:0] RXBE_s_tdata; // from ADC output [31:0] RXBE_m_tdata; // to DMA input RX_FIFO_s_tready, RX_FIFO_m_tvalid; output RX_FIFO_m_tready, RX_FIFO_s_tvalid, RX_FIFO_rstn; input [23:0] RX_FIFO_m_tdata; output [23:0] RX_FIFO_s_tdata; input [1:0] RXBE_mode; // 0: reset, 1: carpture, 2: upload input [31:0] RXBE_capture_size; output [15:0] RXBE_C_out; output [7:0] RXBE_state; output RXBE_reset_mode, RXBE_capture_mode, RXBE_upload_mode; /// RXBE Status assign RXBE_reset_mode = (RXBE_mode == 0); assign RXBE_capture_mode = (RXBE_mode == 1); assign RXBE_upload_mode = (RXBE_mode == 2); assign RXBE_state = {RXBE_m_tvalid, RXBE_m_tready, RX_FIFO_m_tready, RX_FIFO_s_tready, RX_FIFO_m_tvalid, RXBE_upload_mode, RXBE_capture_mode, RXBE_reset_mode}; //// RXBE Input Interface assign RX_FIFO_s_tdata = RXBE_s_tdata; // ADC ==> FIFO input assign RXBE_s_tready = 1; /// RX FIFO Control reg [31:0] RXB_C; wire RXB_C_rst, RXB_C_inc, RXB_full, RXB_C_dec; wire RX_FIFO_WR, RX_FIFO_RD; always @(posedge CLK) if (RXB_C_rst) RXB_C <= 0; else if (RXB_C_inc) RXB_C <= RXB_C + 1; else if (RXB_C_dec) RXB_C <= RXB_C - 1; assign RXB_full = (RXB_C == RXBE_capture_size); assign RXB_C_rst = ~RSTN | RXBE_reset_mode; assign RX_FIFO_WR = RX_FIFO_s_tready & RX_FIFO_s_tvalid; assign RX_FIFO_RD = RX_FIFO_m_tvalid & RX_FIFO_m_tready; assign RXB_C_inc = RX_FIFO_WR; assign RXB_C_dec = RX_FIFO_RD; assign RX_FIFO_s_tvalid = RXBE_capture_mode & ~RXB_full & RXBE_s_tvalid; assign RX_FIFO_m_tready = RXBE_upload_mode & RXBE_m_tready; assign RX_FIFO_rstn = ~RXBE_reset_mode & RSTN; assign RXBE_C_out = RXB_C; //// RXBE Output Interface wire [11:0] I_in, Q_in; wire [15:0] I_out, Q_out; assign I_in = RX_FIFO_m_tdata[23:12]; assign Q_in = RX_FIFO_m_tdata[11:0]; assign I_out = {{4{I_in[11]}},I_in}; // Signed Extension assign Q_out = {{4{Q_in[11]}},Q_in}; // Signed Extension assign RXBE_m_tdata = {I_out, Q_out}; assign RXBE_m_tvalid = RXBE_upload_mode & RX_FIFO_m_tvalid; assign RXBE_m_tlast = RX_FIFO_RD & (RXB_C == 1); endmodule ``` ### RXBE TB 原則上就是設定延遲幾個CLK時要做什麼事,RXBE_mode要給多少值 ```0 module RXBE_TB(RXBE_mode_in, RXB_capture_size); output [1:0] RXBE_mode_in; output [31:0] RXB_capture_size; reg [1:0] A; reg [31:0] RXB_CS_reg; initial begin A = 0; // Reset mode RXB_CS_reg = 16; // RXB_capture_size = 16 #400.1 A = 1; // Capture mode #320 A = 2; // Up-Load mode #400 RXB_CS_reg = 32; // (Up-load done) RXB_capture_size = 32 #100 A = 1; // Capture mode #400 A = 2; // Up-Load mode #400 A = 1; #320 A = 2; #400 A = 1; #320 A = 2; end assign RXBE_mode_in = A; assign RXB_capture_size = RXB_CS_reg; endmodule ``` 控制的設計(模式或是狀態): 1. RX_BF_mode = 0 → Reset Reset buffer (reset FIFO),原則上用不太到,因為Upload後FIFO就空了。 2. RX_BF_mode = 1 → Capture-to-RX_Frame_size 錄信號至FIFO,Size滿了就會停止。 Size的大小由**FIFO** depth決定。 3. RX_BF_mode = 2 → Upload Capture 4. RX_BF_mode = 3 → Capture & Upload (尚未編輯) 如果處理速度夠高的話可以一直把資料傳出去,送到**DMA**的S2MM存到PS的DDR裡面,達到Real time的效果。 AD9361的 I Q channel一路是12 bits $=$ 3 bytes,所以如果DDR有1GB的話,可以錄1/3G samples。 ## Simulation ![](https://i.imgur.com/JbhoNXB.png) --- # Verification AD9361 DAC/ADC interface ## Design ![](https://i.imgur.com/YrSptS4.png) 範例檔案在**AD9361_TX_DDS_RX_Buffer_Engine_2022_5_3**中 ## Juypter 見影片VIII(4)。 --- # Tx buffer engine + Rx buffer engine + AD9361 ## Design ![](https://i.imgur.com/X20VUNg.png) ## Design Flow 1. ZYNQ7 Processing System Run Block Automation。 後面要接DMA,所以先打開High Performance (HP) Slave 的 S AXI HP0。 ### SPI 2. AXI GPIO SPI 開始SPI的部分。 取名:**axi_gpio_SPI**。 開兩個port,32 bits all outputs 和 8 bits all inputs。 Run Connection Automation,只要接S_AXI。 3. Add SPI Source ![](https://i.imgur.com/m8jJHRO.png) 少**Ping_Pong_FIFO**,再加。 4. GPIO32 to AXIS24 拉進來接線:**axi_gpio_SPI**輸出接GPIO_in、CLK和RSTN接系統的。 5. AXIS Data FIFO 改名**axis_data_fifo_SPI**。 資料寬度可以小,但是穩定性問題設為512,independent CLK: YES。 **GPIO32 to AXIS24**的Master輸出接S_AXIS,Slave的CLK和RSTN接系統的,Master的CLK接**GPIO32 to AXIS24**的CLK_4。 6. AD9361 SPI interface **AXIS DATA FIFO**的M_AXI接Slave輸入。 CLK接降速的CLK_4,RSTN接系統的RSTN。 SPI輸出接**axi_gpio_SPI**的8 bits輸入。 剩下Make external,名字把_0拿掉。 ### DAC 7. DAC interface 少**Asyn_FIFO_48x512**,**IP catalog $\to$ FIFO Generator**。 **Native $\to$ Independent Clocks Distributed RAM $\to$ First Word Fall Through $\to$ W48, D512 $\to$ Finish $\to$ Global**。 Input的ADI_DATA_CLK make external、三個Output make external,把_0去掉。 CLK和RSTN都接系統的。 ### DDS 8. AXI GPIO DDS TXBE 控制DDS和TXBE的mode。 取名:**axi_gpio_DDS_TX**。 第一個Port,給DDS的16 bits的All outputs、預設$2^8=0x00000100$。 第二個Port,給TXBE的3 bits的All outputs。 第3個bit: 0 $\to$ default DDS、第3個bit: 1 $\to$ TXBE。 Run Connection Automation,只要接S_AXI。 9. DDS 從LIB/CORDIC_IP中把所有DDS的相關Source都加進來。 CLK和RSTN都接系統的,frequency接**axi_gpio_DDS_TX**的16 bits的All outputs。 10. IQ Slice (16 to 12) 從LIB中加入Source: **IQ_Slice**。 ```0 module IQ_Slice(IQ_in, I_out, Q_out, IQ_out); parameter DW_in = 16, DW_out = 12; // DW_out <= DW_in input [2*DW_in-1:0] IQ_in; output [DW_out-1:0] I_out, Q_out; output [2*DW_out-1:0] IQ_out; assign I_out = IQ_in[(2*DW_in-1)-:DW_out]; // [31:20] assign Q_out = IQ_in[(DW_in-1)-:DW_out]; // [15:4] assign IQ_out = {I_out, Q_out}; endmodule ``` **DDS**的tdata接32 bits 的 IQ_in。 11. AXIS MUX 從LIB中加入Source: **M_AXIS_MUX**。 **Multiplexer**的作用是從多個輸入當中選擇一個輸出。 ```0 module M_AXIS_MUX(m1_axis_tdata, m1_axis_tvalid, m1_axis_tready, m2_axis_tdata, m2_axis_tvalid, m2_axis_tready, m_axis_tdata, m_axis_tvalid, m_axis_tready, MUX_sel); parameter DW = 16; input m1_axis_tvalid, m2_axis_tvalid, m_axis_tready; output m1_axis_tready, m2_axis_tready, m_axis_tvalid; input [DW-1:0] m1_axis_tdata, m2_axis_tdata; output [DW-1:0] m_axis_tdata; input MUX_sel; assign m_axis_tvalid = MUX_sel ? m2_axis_tvalid : m1_axis_tvalid; assign m_axis_tdata = MUX_sel ? m2_axis_tdata : m1_axis_tdata; assign m1_axis_tready = MUX_sel ? 0 : m_axis_tready; assign m2_axis_tready = MUX_sel ? m_axis_tready : 0; endmodule ``` 這裡用```MUX_sel```來決定要傳哪個資料與下游哪個東西要Ready。 ```DW```在上面被定義為16,但是在這裡配合TXBE的設定,要雙擊IP把DW改成24。 m1的tdata接**IQ_Slice**的24 bits IQ_out。 **DDS**的Valid和Ready接到m1的Valid和Ready,Output的Valid和Ready接到DAC的Tx Valid和Ready。 12. IQ Slice (IQ to I,Q) 複製一個**IQ_Slice**,把DW In改成12 bits。 把**M_AXIS_MUX**的Output tdata接到24 bits IQ_in,I_out接 I1, Q2、Q_out接 Q1, I2。 >目前的design應該長這樣↓ >![](https://i.imgur.com/n2mMb3U.png) ### TXBE 13. TXBE Controller 從LIB中加入Source: **TXBE_Controller_AD9361**。 這邊要確定**TXBE_Controller_AD9361**中Assign給S_I和S_Q的tdata目標範圍是否和前面**IQ_Slice**相同。 CLK和RSTN接系統的。 TXBE_out要接到**M_AXIS_MUX**的m2_axis。 14. AXIS Data FIFO TXBE 加入**AXIS Data FIFO**,改名**axis_data_fifo_TXBE**。 depth: 4096,Memory Type: Block RAM,DW 3 bytes。 把**TXBE_Controller**的FF_out接到M_AXIS、FF_in接到S_AXIS。 把**TXBE_Controller**的FF_rstn接到FIFO的RSTN,CLK接系統的。 15. Slice (TX_sel) 新增IP,輸入3 bits,從2到2取1 bit,取名:**xlslice_TX_sel**。 輸入端為**axi_gpio_DDS_TX**的第二個Port,輸出端接到**M_AXIS_MUX**的MUX_sel。 16. Slice (TX_mode) 複製**xlslice_TX_sel**,改取1到0取2 bits,取名:**xlslice_TX_mode**。 輸入端不變,輸出端為**TXBE_Controller**的TXBE_mode_in。 17. DMA AXI DMA,取消Enable Scatter Gather Engine,Buffer length設20,Burst size兩邊都開256。 取名**axi_dma_TXRX**,Run Connection Automation,會自動接上GPIO使可以從PS設定。 HP0的部分有MM2S和S2MM兩個部分,所以Run Connection Automation要兩次。 MM2S要接到**TXBE_Controller**的TXBE_in。 剩下部分與RXBE公用。 18. AD9361_SPI Create Hierarchy: ![](https://i.imgur.com/MVVPRVp.png) 19. AD9361_TX Create Hierarchy: ![](https://i.imgur.com/cDh3MMH.png) >發射機的部分完成後應該長這樣子↓ ![](https://i.imgur.com/ScgZzje.png) ### ADC 20. ADC interface ADI_DATA_CLK接上make external的**DAC interface**的ADI_DATA_CLK。 CLK和RSTN都接系統的。 ADI_RX_FRAME、ADI_P0_D和ADI_ENABLE make external然後把_0拿掉。 ### RXBE 21. AXIS GPIO RX 新增IP,取名:**axi_gpio_RX**。 開兩個Port,32 bits的All outputs預設0x00000100和2 bits的All outputs。 Run Connection Automation,只選S_AXI。 22. AXIS Data FIFO RX 新增IP,取名:**axis_data_fifo_RX**。 depth: 4096,Memory Type: Block RAM。 RXBE controller的FIFO_m接口應該是24 bits,所以DATA Width改成3 bytes。 CLK和RSTN都接系統的。 23. RXBE Controller 從Source拉進來。 RX_FIFO_s接到**axis_data_fifo_RX**的S_AXIS。 RX_FIFO_m接到**axis_data_fifo_RX**的M_AXIS。 **ADC interface**裡面有Asyn_FIFO,所以這邊CLK接系統的就好。RSTN也接系統的。 RXBE_capture_size接到**axi_gpio_RX**的第一個Port。 RXBE_mode接到**axi_gpio_RX**的第二個Port。 RXBE_s的Valid和Ready接**ADC interface**裡面ADC_RX的Valid和Ready。 RXBE_m要接到**axi_dma_TXRX**的S_AXIS_S2MM。 24. Concat 新增IP,將兩個輸入首尾相接,輸入和輸出的Width都是Auto。 這邊輸出的LSB是先輸入的Port,MSB是最後輸入的Port, 所以In0輸入為**ADC interface**的Q1 ,In1輸入為**ADC interface**的I1。 輸出接到**RXBE Controller**裡面RXBE_s的Data。 25. AXI GPIO State 單純靜態觀測RX的狀態,取名:**axi_gpio_RX_ST**,開兩個Port。 16 bits的All inputs接**RXBE Controller**的RXBE_C_out。 8 bits的All inputs接**RXBE Controller**的RXBE_state。 Run Connection Automation。 26. AD9361_RX Create Hierarchy: ![](https://i.imgur.com/7XVNDED.png) >完成後應該長這樣子↓ ![](https://i.imgur.com/Wo1Fb9k.png) Validate確認沒有少接,CLK的Warning沒關係。 27. Add Constraints file ![](https://i.imgur.com/oB9MkEM.png) 28. Create HDL Wrper 29. Set as top 30. Generate Big Stream ## Jupyter ### Import Library ```0 from pynq import Overlay import time import numpy as np from pynq.lib import AxiGPIO import matplotlib.pyplot as plt import pynq.lib.dma from pynq import Xlnk mask = 0xffffffff xlnk = Xlnk() ``` ### Overlay Hardware Design ```0 ol = Overlay("TXBE_RXBE_AD9361.bit") ol? ``` ### Hardware Design IPs (GPIO & DMA) ```0 # IP Blocks: 'axi_gpio_SPI', 'axi_gpio_DDS_TX', 'axi_gpio_RX', 'axi_gpio_RX_ST', 'axi_dma_TXRX' # TXBE FIFO (4096 I/Q samples), RXBE FIFO (4096 I/Q samples) SPI_ip = ol.ip_dict['axi_gpio_SPI'] AD9361_SPI_Output = AxiGPIO(SPI_ip).channel1 AD9361_SPI_Input = AxiGPIO(SPI_ip).channel2 DDS_TX_ip = ol.ip_dict['axi_gpio_DDS_TX'] DDS_Frequency = AxiGPIO(DDS_TX_ip).channel1 TXBE_mode = AxiGPIO(DDS_TX_ip).channel2 # bit[2]=0/1 ==> DDS/TXBE_out, bit[1:0]=0/1/2 ==> TX RST/TXBE_Load/TXBE_Play RXBE_IP = ol.ip_dict['axi_gpio_RX'] RXBE_Capture_size = AxiGPIO(RXBE_IP).channel1 # Default = 256 RXBE_mode = AxiGPIO(RXBE_IP).channel2 # 0/1/2 ==> reset/capture/up-load RXBE_ST_IP= ol.ip_dict['axi_gpio_RX_ST'] RXBE_Counter = AxiGPIO(RXBE_ST_IP).channel1 RXBE_Status = AxiGPIO(RXBE_ST_IP).channel2 dma = ol.axi_dma_TXRX ``` ```SPI_Output```是對我們而言的Output,是把資料送進SPI,反之```SPI_Input```是收資料回來。 ```RXBE_mode```開機是reset,capture收資料,up-load傳資料然後清空capture。 ```XBE_Counter```是RXBE寫了多少資料在裡面。 ### AD9361 SPI function & Testing ```0 def PYNQ_SPIWrite(x, y): # PYNQ_SPIWrite(‘007’, ‘08’) ADI9361_Conf_comm = int(x,16)*2**8 + int(y, 16)+2**24+2**23 AD9361_SPI_Output.write(0x0, mask) AD9361_SPI_Output.write(ADI9361_Conf_comm, mask) #SPIWrite print(x) def PYNQ_SPIRead(x): # PYNQ_SPIRead(‘007’) ADI9361_Conf_comm = int(x + '00', 16) + 2**24 AD9361_SPI_Output.write(0x0, mask) AD9361_SPI_Output.write(ADI9361_Conf_comm, mask) #SPIWrite SPI_DO = AD9361_SPI_Input.read() SPI_DO = hex(SPI_DO) print(SPI_DO) PYNQ_SPIWrite('007', 'cc') PYNQ_SPIRead('007') ``` ### AD9361 Configuration ```0 f = open("PYNQ_AD9361_Configuration.txt") line = f.readline() while line: print(line) a = int(line,16) AD9361_SPI_Output.write(0x0, mask) AD9361_SPI_Output.write(a, mask) time.sleep(0.001) line = f.readline() f.close() #print(line) ``` ```time.sleep```讓讀取有等待0.001秒,讓速度慢一點。 ### TX Source Functions ```0 def DDS_Write(x): DDS_Conf_comm = x DDS_Frequency.write(DDS_Conf_comm, mask) #SPIWrite def DDS_Frequency_Setting(f0): fs = 30.72 w0_fix=int(np.mod(np.round(f0/fs*2**16),2**16)) print(w0_fix) DDS_Write(w0_fix) def TX_Source_DDS(): # 0/1 ==> DDS/TXBE A = TXBE_mode.read() AA = np.mod(A,4) A = int(AA) print(A) TXBE_mode.write(A, mask) def TX_Source_TXBE(): # 0/1 ==> DDS/TXBE A = TXBE_mode.read() AA = np.mod(A,4) A = int(4 + AA) print(A) TXBE_mode.write(A, mask) def TXBE_RST(): A = TXBE_mode.read() AA = np.floor(A/4) A = int(AA*4 + 0) print(A) TXBE_mode.write(A, mask) def TXBE_Download(): A = TXBE_mode.read() AA = np.floor(A/4) A = AA*4 + 1 TXBE_mode.write(int(A), mask) def TXBE_Play(): A = TXBE_mode.read() AA = np.floor(A/4) A = int(AA*4 + 2) print(A) TXBE_mode.write(A, mask) def TXBE_Load_Signal(s_I, s_Q, N, NN): # N <= NN (NN <= 4096) TXBE_RST() # RST input_buffer = xlnk.cma_array(shape=(NN,), dtype=np.uint32) s_Ia = np.mod((np.round(s_I)), 2**16) s_Qa = np.mod((np.round(s_Q)), 2**16) for i in range(N): input_buffer[i] = s_Ia[i] * 2**16 + s_Qa[i] TXBE_Download() dma.sendchannel.transfer(input_buffer) dma.sendchannel.wait() ``` ```fs=30.72```事由前面的configuration給定的。 ```TXBE_Load_Signal```的N是取信號的第N個點,NN是Input buffer的size。 ### RX Buffer Engine Functions ```0 def RXBE_Capture_size_Conf(RXB_size): # RXB_size <= 4096 RXBE_Capture_size.write(RXB_size, mask) def RXBE_RST(): RXBE_mode.write(0x0, mask) def RXBE_Capture(): RXBE_mode.write(0x1, mask) def RXBE_Upload(): RXBE_mode.write(0x2, mask) def AD9361_RX_Capture(RXB_size): # global output_buffer output_buffer = xlnk.cma_array(shape=(RXB_size,), dtype=np.uint32) RXBE_Capture_size_Conf(RXB_size) RXBE_Capture() # Capture mode time.sleep(0.1) RXBE_Upload() # Up-Load mode # time.sleep(0.1) dma.recvchannel.transfer(output_buffer) dma.recvchannel.wait() s_Q = np.floor(np.mod(output_buffer[0:N], 2**16))#LSB s_I = np.floor(output_buffer[0:N]/2**16) #MSB s_I = s_I - (s_I>32767)*2**16 #12bits sign interger s_Q = s_Q - (s_Q>32767)*2**16 return s_I, s_Q ``` 最下面s_I和s_Q的部分在做把收到32 bits的轉換成16 bits的 I 和 Q channel。 ### Check the function of RX Buffer Engine 測試RX最容易出錯的部分。 ```0 # RXB_state = {RX_FIFO_s_tready, RX_FIFO_m_tvalid, RXB_upload_mode, RXB_capture_mode, RXB_reset_mode}; # RXB_state = {RXB_m_tvalid, RXB_m_tready, RX_FIFO_m_tready, RX_FIFO_s_tready, RX_FIFO_m_tvalid, RXB_upload_mode, RXB_capture_mode, RXB_reset_mode}; # '01010001' # '01011010' # '10011100' print(RXBE_Capture_size.read()) # Default of RXBE Capture Size = 256 print(hex(RXBE_Status.read())) # 0x51 print(RXBE_mode.read()) # 0 print(RXBE_Counter.read()) # 0 (RX Buffer is empty) ``` 測試Reset mode。 第二行Print的State參照上面註解的第二行,0x51=0b01010001。 ```0 RXBE_Capture() # Capture mode (default size = 256) RXBE_ST = RXBE_Status.read() print(RXBE_Capture_size.read()) # Should be 256 print(hex(RXBE_ST)) # Should be 0x5a print(RXBE_mode.read()) # Should be 1 print(RXBE_Counter.read()) # Should be 256 (RX Buffer is loaded) # If the result is incorrect # ==> relaod the bit and proceed all the cells in sequence agian ``` 測試Capture mode。 Size應該不變但是RXBE的State應該要變0x5a=0b01011010,RXBE_mode=1。 重點是Counter錄了256 bits所以要是256,若是0的話代表沒有錄東西,reload。 ```0 RXBE_Upload() # RXBE Uplaod mode RXB_size = RXBE_Capture_size.read() output_buffer = xlnk.cma_array(shape=(RXB_size,), dtype=np.uint32) dma.recvchannel.transfer(output_buffer) dma.recvchannel.wait() print(RXBE_Capture_size.read()) # 256 RXBE_ST = RXBE_Status.read() print(hex(RXBE_ST)) # 0x74 print(RXBE_mode.read()) # 2 print(RXBE_Counter.read()) # 0 (RX Buffer is empty) ``` 測試Upload mode。 RXBE的State應該要變0x74=0b,Counter因為讀完了所以會變0。 ### Experimenting the SDR Platform #### Setting DDS Frequency ```0 fs = 30.72*0.5 f0 = fs/64/4 DDS_Frequency_Setting(f0) #120khz ``` f0後面除上的數值是取樣率,或是說每幾個點一個週期。 #### Setting TX Source as **DDS** 以DDS作為信號源,和Setting TX Source as **TXBE**擇一。 ```0 TX_Source_DDS() ``` #### Setting TX Source as **TXBE** 以TXBE作為信號源,和Setting TX Source as **DDS**擇一。 ```0 #Down Load the Captured Signal to TX Buffer Engine N =1024 NN = 1024 TXBE_Load_Signal(s_I*2, s_Q*2, N, NN) TXBE_mode.read() ``` ```0 TX_Source_TXBE() TXBE_Play() ``` 切換源頭到TXBE,然後讓TXBE撥放,沒有TXBE_Play的話後面的結果會沒有信號或是直流狀態。 #### Setting TX/RX Carrier Frequency and Gain ```0 PYNQ_SPIWrite('271', '78'); #TX 2.4 ghz PYNQ_SPIWrite('231', '7D'); #RX 2.5 ghz ``` > 將"數值"輸入"位置"決定設定,意義要參照Register MAP,例如 > ![](https://i.imgur.com/mUKvaSo.png) > Tx的頻率位置是'271'數值給定為20MHz乘上'78'=0d120,為2.4GHz。 > ![](https://i.imgur.com/AQqTPa7.png) > Rx的頻率位置是'231'數值給定為20MHz乘上'7D'=0d125,為2.5GHz。 ### Caputure Signal and Display ```0 N = int(256*4) nn = np.array(range(N)) fig, ax = plt.subplots() s_I, s_Q = AD9361_RX_Capture(N) ax.plot(nn, s_I) ax.plot(nn, s_Q, linewidth=3.0) ax.set(xlim=(0, N), ylim=(-2048, 2048)) plt.show() print(s_I) print(s_Q) ``` Default size為256有 I1 I2 Q1 Q2 所以是1024。 ylim的範圍由12 bits所決定,所以是$\pm2^{11}$。 圖的部分,TX_Source為DDS時應為滿屏的弦波;TX_Source為TXBE時應為半屏弦波剩下為0。 ```0 np.savetxt('s_I.txt',s_I,fmt='%d') np.savetxt('s_Q.txt',s_Q,fmt='%d') ``` 把資料存成.txt檔。 ### Down load the captured signal to TXBE ```0 N = 512 NN = 512 TXBE_Load_Signal(s_I*2, s_Q*2, N, NN) ``` 乘2單純放大信號。 ### Control AD9361 Tx frequency ```0 # 頻率的設定範圍限制在 47MHz 到 6000 MHz # 輸入單位為 MHz def AD9361_TXRXC_Conf(Tx_C, Rx_C): f_ref = 80 # 計算 VCO Divider Tx_VCO = int(np.floor(np.log2(12000/Tx_C)))-1 Rx_VCO = int(np.floor(np.log2(12000/Rx_C)))-1 f = (Tx_VCO << 4) + Rx_VCO # 計算 Interger Word Tx_F_RFPLL = Tx_C * (2**((Tx_VCO)+1)) Rx_F_RFPLL = Rx_C * (2**((Rx_VCO)+1)) Tx_N_Integer = int(np.floor(Tx_F_RFPLL/f_ref)) Rx_N_Integer = int(np.floor(Rx_F_RFPLL/f_ref)) Tx_d = (Tx_N_Integer >> 8) Rx_d = (Rx_N_Integer >> 8) Tx_e = (Tx_N_Integer) & 0xff Rx_e = (Rx_N_Integer) & 0xff # 計算Fractional Word Tx_N_Frac = int(np.round(8388593*(Tx_F_RFPLL/f_ref - Tx_N_Integer))) Rx_N_Frac = int(np.round(8388593*(Rx_F_RFPLL/f_ref - Rx_N_Integer))) Tx_a = Tx_N_Frac & 0xff Rx_a = Rx_N_Frac & 0xff Tx_b = (Tx_N_Frac >> 8) & 0xff Rx_b = (Rx_N_Frac >> 8) & 0xff Tx_c = (Tx_N_Frac >> 16) & 0x7f Rx_c = (Rx_N_Frac >> 16) & 0x7f # 寫入 Tx Register PYNQ_SPIWrite('273', hex(Tx_a)) PYNQ_SPIWrite('274', hex(Tx_b)) PYNQ_SPIWrite('275', hex(Tx_c)) PYNQ_SPIWrite('272', hex(Tx_d)) PYNQ_SPIWrite('271', hex(Tx_e)) # 寫入Rx Register PYNQ_SPIWrite('233', hex(Rx_a)) PYNQ_SPIWrite('234', hex(Rx_b)) PYNQ_SPIWrite('235', hex(Rx_c)) PYNQ_SPIWrite('232', hex(Rx_d)) PYNQ_SPIWrite('231', hex(Rx_e)) PYNQ_SPIWrite('005', hex(f)) PYNQ_SPIWrite('014','23') ``` # Reference 訊號處理實驗室, 陳逸民 副教授, 軟體定義無線電平台技術 Technology of Software-Defined Radio Platform, CO6065