FPGA基础入门【10】开发板Ethernet PHY局域网配置
- List item
上一篇教程介紹了NEXYS4 開發板中DDR2的使用方式,這一篇介紹不可或缺的網絡接口RJ45在FPGA開發板中的使用
FPGA基礎入門【10】開發板Ethernet PHY局域網配置
- 板載局域網芯片
- 網絡層級模型
- 芯片簡介
- 引腳定義
- 數據通路
- 芯片復位
- 控制寄存器
- 收發時序
- 邏輯設計
- 狀態機設計
- 頂層代碼設計
- 模擬仿真
- Testbench
- 仿真腳本
- 仿真結果
- 編譯燒寫
- 總結
板載局域網芯片
NEXYS 4上的局域網接口RJ45使用常見的LAN8720A物理層芯片,支持10兆網和100兆網,使用RMII(Reduced Media Independent Interface)。它的文檔在此:LAN8720A
NEXYS 4文檔中介紹說,使用EDK(Embedded Development Kit)的工程可以用axi_ethernetlit或者axi_ethernet IP訪問物理層芯片。使用EDK的意思是利用FPGA內自帶的ARM核,放入一個小型的Linux核心,比如Microblaze或Zynq,然后用軟件編程的形式收發數據。
網絡層級模型
網絡傳輸TCP/IP五層模型如下,最頂層的用戶產生數據,經過層層包裝到最底層的物理層傳輸出去,到接收端在經過層層拆包驗收后傳到接收端的用戶,各層負責把數據包往下傳(實線),但實際上是在和另一端的相對應層互相握手溝通(虛線)。這個過程包含很多的細節,除了物理層是由LAN8720A芯片和RJ45接口完成的以外,其他都是由FPGA內部邏輯或者ARM核中的軟件邏輯完成的。
這個系列的教程還沒有到將各種接口綜合到Linux核心的階段,EDK先向后放,我們優先用PL(programmable logic)部分測試其基礎功能,以此學習加強理解
芯片簡介
LAN8720A的外部連接應該如下,我們需要做的是寫一個簡單的10/100 Ethernet MAC,其他部分在開發板上已經存在
芯片內部結構如下,下面慢慢介紹
引腳定義
在開發板上和FPGA的連接如下
相關引腳簡介:
- TXD1, TXD0,MAC層輸出到物理層的信號
- TXEN,表示TXD[1:0]上有有效信號
- RXD1, RXD0,物理層傳給MAC層的信號
- RXERR,接收錯誤信號,該信號升高表示物理層收到了錯誤信號
- PHYAD0,物理層地址配置引腳,不拉低時則默認拉高
- MODE2, MODE1, MODE0,物理層模式配置引腳
- CRS_DV,有效信息接收信號,該信號升高表示物理層收到了有效信號
- MDIO,物理層SMI(Serial Management Interface)數據出入,不用時被拉高
- MDC,物理層SMI(Serial Management Interface)時鐘信號
- RESET#,低電平激發的硬件復位引腳,不用時被拉高
- INT#,中斷輸出
- REFCLKO,50MHz參考時鐘輸出
- CLKIN,50MHz時鐘輸入
- LED1,有效鏈接指示燈,RJ45接口上的綠燈,表示建立了有效連接,當CRS為高時會閃爍
- LED2,連接速度指示燈,RJ45接口上的黃燈,亮起表示在使用百兆網,滅表示在使用十兆網
在右側的信號是在RESET#信號拉低,也就是復位狀態是有效的,當復位結束,RESET#拉高,相應信號就被讀入芯片內部。這種模式被稱為configuration strap,不知道該怎么翻譯
在configuration strap時MODE引腳的配置就可以調配寄存器中相應位數的默認值,這樣常見的配置模式就不需要寫寄存器來配置,還是很方便的
數據通路
數據信號在芯片內部流通過程我們并不關心,不過還是簡單介紹一下。
發送端如下
接收端相反,只是需要多一個時鐘恢復電路,利用傳輸的信號本身,提取連續變化的部分經過反饋電路形成和信號同步的時鐘
芯片復位
LAN8720A的芯片復位分為硬件復位和軟件復位,硬件復位是把RESETn引腳拉低至少100us;軟件復位是給控制寄存器0的最高位[15]寫入1,并等待0.5s,具體操作之后就介紹。
這篇教程會在邏輯開始時先后把硬件復位和軟件復位都進行一遍。
控制寄存器
SMI串行管理接口讀寫數據的時序圖如下,MDC的頻率不固定,只要周期在400ns以上就好,也就是頻率在2.5MHz以下,保險起見我們選擇1MHz。并且我們選擇在時鐘下降沿輸出或者讀入數據,避免上升沿的沖突
PHY Address物理層地址默認是0,在復位狀態時把引腳PHYAD1拉高可以修改物理層地址到1,如果你需要更高的地址就需要配置寄存器了。當你有多個物理層芯片連在一起時就需要配置不同的物理層地址來分別訪問
Register Address寄存器地址和不同寄存器有關,寄存器表如下
在這之中最重要的寄存器是前5個
基礎控制寄存器,地址0,可讀寫:
- [15],軟件復位,默認為0,設為1即軟件復位開始
- [14],回路模式,默認為0,設為1即回路模式
- [13],速度選擇,0為十兆網,1為百兆網
- [12],自動速度識別使能,0為關閉自動速度識別,1為開啟自動速度識別
- [11],電源關閉,默認為0,設為1關閉電源
- [10],隔離模式,默認為0,設為1隔離物理層和RMII接口
- [9],重啟自動速度識別,默認為0,設為1重啟自動速度識別
- [8],雙工模式,0為半雙工,1為全雙工
- [7:0]是保留位
基礎狀態寄存器,地址1,僅可讀
這個寄存器只要看后面幾位就好,看自動速度識別是否完成,連接是否完成
物理層ID1寄存器,地址2,可讀寫
作為寄存器讀取測試使用,看其讀出默認數據是否正確
物理層ID2寄存器,地址3,可讀寫
同樣可以作為寄存器讀取測試使用
自動識別廣播寄存器,地址4,可讀寫
這個寄存器在復位模式配置MODE時需要注意,具體可以參考上面的模式選擇表
收發時序
在所有寄存器都配置完成后,我們就可以用下面這個時序圖來收發數據了。可以看出LAN8720A的收發數據是在時鐘下降沿發生的
邏輯設計
基礎思路是,先硬件復位,配置configuration strap,再進行軟件復位,讀取幾個寄存器以確保一切正常后等待CRS_DV引腳來讀取數據。這里不打算涉及負責的網絡協議,隨便找個網口連接后,看到有數據即可,不涉及數據包內部內容。
狀態機設計
根據上面的設計,做出如下狀態機,當中加入了為ChipScope準備的輔助開關
頂層代碼設計
module ethernet(input clk,input rst,input switch_continue,output reg led,// LAN8720A PHY chip portinout MDIO,output wire MDC,output reg RESETn,inout RXD1_MODE1,inout RXD0_MODE0,inout RXERR_PHYAD0,output reg TXD0,output reg TXD1,output reg TXEN,inout CRS_DV_MODE2,inout INT_REFCLKO,output reg CLKIN );引腳定義。里面加入了一個叫switch_continue的引腳,它被連接到第二個撥動開關上,用來把狀態機停在讀取寄存器之前,不然我們很難在ChipScope上抓取相應數據
這里我們第一次用到了inout這種端口定義,顧名思義是出入復用的引腳,它的用法如下(不包含在最終代碼中,只是示例)
雙向引腳一般有一個輸出使能引腳,輸出時連接到相應的寄存器輸出上,輸入則轉為高阻態1’bz,而輸入信號則賦值給另一個wire,傳遞出去或者到另一個寄存器
module bidirec (oe, clk, inp, outp, bidir);// Port Declarationinput oe; input clk; input [7:0] inp; output [7:0] outp; inout [7:0] bidir;reg [7:0] a; reg [7:0] b;assign bidir = oe ? a : 8'bZ ; assign outp = b;// Always Constructalways @ (posedge clk) beginb <= bidir;a <= inp; endendmodule輸出50MHz到LAN8720A的CLKIN引腳
// Clock to LAN8720A is 50MHz, need to be lowered down always @(posedge clk or posedge rst) beginif(rst) beginCLKIN <= 1'b0;endelse beginCLKIN <= ~CLKIN;end end雙向引腳配置
// Control of the bi-directional data reg [2:0] MODE; reg PHYAD0; reg INTSEL; reg strap_oe; wire [1:0] RXD; (* dont_touch = "true" *)reg [1:0] rxd_d; wire RXERR; (* dont_touch = "true" *)reg rxerr_d; wire CRS_DV; (* dont_touch = "true" *)reg crs_dv_d; wire INT;assign RXD1_MODE1 = (strap_oe) ? MODE[1] : 1'bz; assign RXD0_MODE0 = (strap_oe) ? MODE[0] : 1'bz; assign RXERR_PHYAD0 = (strap_oe) ? PHYAD0 : 1'bz; assign CRS_DV_MODE2 = (strap_oe) ? MODE[2] : 1'bz; assign INT_REFCLKO = (strap_oe) ? INTSEL : 1'bz;assign RXD = {RXD1_MODE1, RXD0_MODE0}; assign RXERR = RXERR_PHYAD0; assign CRS_DV = CRS_DV_MODE2; assign INT = INT_REFCLKO;狀態機定義
// State machine parameter IDLE = 4'd0; parameter RESET = 4'd1; parameter RDPHYID1 = 4'd2; parameter RDPHYID2 = 4'd3; parameter RESET_SOFT = 4'd4; parameter UNRESET_SOFT = 4'd5; parameter SETMODE = 4'd6; parameter UNRESET = 4'd7; parameter RD_BC0 = 4'd8; parameter RD_BS1 = 4'd9; parameter RX_TX = 4'd10;(* dont_touch = "true" *)reg [3:0] state; (* dont_touch = "true" *)reg [3:0] next_state; (* dont_touch = "true" *)reg [15:0] data_from_SMI;SMI讀寫模塊,在后面加入子模塊內容
// SMI management reg wrh_rdl; reg [4:0] reg_addr; reg [15:0] wr_data; wire [15:0] rd_data; reg SMI_start; wire SMI_complete; wire MDI; wire MDO; wire MD_OE;assign MDIO = (MD_OE) ? MDO : 1'bz; assign MDI = MDIO;SMI_manage SMI_manage(.clk(clk),.rst(rst),.mdc(MDC),.mdo(MDO),.mdi(MDI),.md_oe(MD_OE),.wrh_rdl(wrh_rdl),.reg_addr(reg_addr),.wr_data(wr_data),.rd_data(rd_data),.start(SMI_start),.complete(SMI_complete) );狀態機具體實現
always @(posedge clk) beginled <= INT; end// State machine always @(posedge clk or posedge rst) beginif(rst) beginstate <= IDLE;endelse beginstate <= next_state;end endreg [25:0] wait_count; (* dont_touch = "true" *)reg read_phase; (* dont_touch = "true" *)reg [3:0] read_data;always @(posedge clk) begincase(state)IDLE: beginnext_state <= RESET;RESETn <= 1'b0;reg_addr <= 5'd0;wrh_rdl <= 1'b0;SMI_start <= 1'b0;data_from_SMI <= 16'h0000;wait_count <= 26'd0;PHYAD0 <= 1'b0; // Set PHY address to 0INTSEL <= 1'b1; // REF_CLK In ModeMODE <= 3'b111;strap_oe <= 1'b1;read_phase <= 1'b0;read_data <= 4'd0;endRESET: beginnext_state <= SETMODE;RESETn <= 1'b0;end// Need to wait for 200us, which is 20000 clock cycles in 100MHzSETMODE: beginMODE <= 3'b111;PHYAD0 <= 1'b0;INTSEL <= 1'b1;if(wait_count < 26'd20000) beginwait_count <= wait_count + 26'd1;endelse beginnext_state <= UNRESET;endendUNRESET: beginstrap_oe <= 1'b0;RESETn <= 1'b1;wait_count <= 26'd0;if(switch_continue) beginnext_state <= RESET_SOFT;endendRESET_SOFT: beginif(SMI_complete) beginnext_state <= UNRESET_SOFT;SMI_start <= 1'b0;endelse beginSMI_start <= 1'b1;wrh_rdl <= 1'b1;reg_addr <= 5'd0;wr_data <= 16'h8000;endend// Need to be kept in software reset for about 0.5sUNRESET_SOFT: beginif(wait_count < 26'd50000000) beginwait_count <= wait_count + 26'd1;endelse beginnext_state <= RDPHYID1;endendRDPHYID1: beginif(SMI_complete) begindata_from_SMI <= rd_data;next_state <= RDPHYID2;SMI_start <= 1'b0;endelse beginSMI_start <= 1'b1;wrh_rdl <= 1'b0;reg_addr <= 5'd2;endendRDPHYID2: beginif(SMI_complete) begindata_from_SMI <= rd_data;next_state <= RD_BC0;SMI_start <= 1'b0;endelse beginSMI_start <= 1'b1;wrh_rdl <= 1'b0;reg_addr <= 5'd3;endendRD_BC0: beginif(SMI_complete) begindata_from_SMI <= rd_data;next_state <= RD_BS1;SMI_start <= 1'b0;endelse beginSMI_start <= 1'b1;wrh_rdl <= 1'b0;reg_addr <= 5'd0;endendRD_BS1: beginif(SMI_complete) begindata_from_SMI <= rd_data;next_state <= RX_TX;SMI_start <= 1'b0;endelse beginSMI_start <= 1'b1;wrh_rdl <= 1'b0;reg_addr <= 5'd1;endendRX_TX: beginSMI_start <= 1'b0;rxd_d <= RXD;rxerr_d <= RXERR;crs_dv_d <= CRS_DV;if(crs_dv_d) beginread_phase <= ~read_phase; // invert every time a new signal detectedif(read_phase) beginread_data[1:0] <= rxd_d;endelse beginread_data[3:2] <= rxd_d;endendenddefault: beginnext_state <= IDLE;endendcase endendmodule狀態機SMI接口控制子模塊
module SMI_manage(input clk,input rst,output reg mdc,output reg mdo,input mdi,output reg md_oe,input wrh_rdl,input [4:0] reg_addr,input [15:0] wr_data,input start,output reg [15:0] rd_data,output reg complete );接口定義
- clk, rst是時鐘和復位
- mdc時鐘,mdo輸出,mdi輸入,md_oe輸出使能
- wrh_rdl,高電平是寫指令,低電平時讀指令
- reg_addr,寄存器地址
- wr_data,寫指令用的16位數
- start,其上升沿作為一個指令的開始
- rd_data,一個讀指令讀出來的16位數
- complete,完成信號,表示可以開啟下一個指令
由使能信號控制的mdc時鐘輸出,每延遲50個時鐘反一次,把100MHz降頻到1MHz
// negative edge detection, MDIO read and write only happen at negative edge always @(posedge clk) beginmdc_d <= {mdc_d[0], mdc}; end assign mdc_negedge = (mdc_d == 2'b10) ? 1'b1 : 1'b0; assign mdc_posedge = (mdc_d == 2'b01) ? 1'b1 : 1'b0;// Detect the rising edge of input signal start reg [1:0] start_d; wire start_posedge;always @(posedge clk) beginstart_d <= {start_d[0],start}; end assign start_posedge = (start_d == 2'b01) ? 1'b1 : 1'b0;偵測mdc的下降沿和start信號的上升沿,避免信號持續時間長短導致的邏輯錯誤
// State machine with three parts reg [1:0] state; reg [5:0] md_count; reg [45:0] data1; reg [15:0] data2; reg complete_d;always @(posedge clk or posedge rst) beginif(rst) beginstate <= 2'b00;complete <= 1'b0;mdc_en <= 1'b0;md_count <= 6'd0;mdo <= 1'b1;md_oe <= 1'b0;data1 <= 46'd0;data2 <= 16'd0;rd_data <= 16'd0;endelse begincase(state)// Wait for start signal2'b00: beginmd_count <= 6'd0;md_oe <= 1'b0;mdo <= 1'b1;complete <= complete_d;complete_d <= 1'b0;if(start_posedge) beginstate <= 2'b01;mdc_en <= 1'b1;data1 <= {32'hFFFFFFFF, 2'b01, (wrh_rdl)?2'b01:2'b10, 5'h00, reg_addr};data2 <= wr_data;endend// Preamble, Start of Frame, OP Code, PHY addr, reg addr// length of 462'b01: beginmd_oe <= 1'b1;if(mdc_negedge) begin{mdo,data1} <= {data1,1'b0};endif(mdc_negedge & (md_count < 6'd45)) beginmd_count <= md_count + 6'd1;endelse if(mdc_negedge) beginmd_count <= 6'd0;state <= 2'b10;endend// Turn around// length of 22'b10: beginif(mdc_negedge && (md_count == 6'd0)) beginmd_count <= 6'd1;endelse if(mdc_negedge) beginmd_count <= 6'd0;md_oe <= wrh_rdl;state <= 2'b11;endend// Data to or from PHY// length of 162'b11: beginif(mdc_negedge) begin{mdo,data2} <= {data2,1'b0};rd_data <= {rd_data[14:0], mdi_d};endif(mdc_negedge & (md_count < 6'd15)) beginmd_count <= md_count + 6'd1;endelse if(mdc_negedge) beginmd_count <= 6'd0;state <= 2'b00;complete <= 1'b1;complete_d <= 1'b1;endendendcaseend endendmoduleSMI控制子模塊內部也有一個小的狀態機,用來控制SMI指令不同部分:前序、讀寫模式、物理層地址、寄存器地址、讀寫翻轉、讀/寫數據。
大致可以分為46位的輸入、翻轉、讀/寫數據和等待四部分,分別對應這個狀態機的各個部分
模擬仿真
Testbench
仿真用的testbench和前面的教程比較相似,沒有包括LAN8720A的仿真模塊(其實是沒找到),主要以后期的ChipScope為主。代碼如下
`timescale 1ns/1nsmodule tb_ethernet;reg clock; reg reset; wire led;initial beginclock = 1'b0;reset = 1'b0;// Reset for 1us#100 reset = 1'b1;#1000reset = 1'b0; end// Generate 100MHz clock signal always #5 clock <= ~clock;ethernet ethernet_top(.clk (clock),.rst (reset),.switch_continue (1'b1),.led (led),// LAN8720A PHY chip port.MDIO (),.MDC (),.RESETn (),.RXD1_MODE1 (),.RXD0_MODE0 (),.RXERR_PHYAD0 (),.TXD0 (),.TXD1 (),.TXEN (),.CRS_DV_MODE2 (),.INT_REFCLKO (),.CLKIN () );endmodule仿真腳本
寫一個簡單的仿真腳本sim.do,由于沒有調用Xilinx的IP,不需要包含庫文件和glbl.v:
vlib work vlog ../src/ethernet.v ../src/SMI_manage.v ./tb_ethernet.v vsim work.tb_ethernet -voptargs=+acc +notimingchecks log -depth 7 /tb_ethernet/* do wave.do run 1ms仿真結果
調用仿真腳本do sim.do后,得到如上方結果,放大可以看到SMI控制的具體細節,不過由于讀取引腳懸空,沒有連接任何信號,讀出來的是藍色代表的高阻態
編譯燒寫
新建一個叫ethernet的project,初始配置可以參考之前的教程。添加代碼文件ethernet.v和SMI_manage.v。
下一步加入約束constraint文件ethernet.xdc,同樣這是用標準模板取自己需要部分修改出來的(NEXYS 4 DDR Master XDC):
## This file is a general .xdc for the Nexys4 DDR Rev. C ## To use it in a project: ## - uncomment the lines corresponding to used pins ## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project## Clock signal set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk] create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]##Switchesset_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst] set_property -dict {PACKAGE_PIN L16 IOSTANDARD LVCMOS33} [get_ports switch_continue]## LEDsset_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports led]##SMSC Ethernet PHYset_property -dict {PACKAGE_PIN C9 IOSTANDARD LVCMOS33} [get_ports MDC] set_property -dict {PACKAGE_PIN A9 IOSTANDARD LVCMOS33} [get_ports MDIO] set_property -dict {PACKAGE_PIN B3 IOSTANDARD LVCMOS33} [get_ports RESETn] set_property -dict {PACKAGE_PIN D9 IOSTANDARD LVCMOS33} [get_ports CRS_DV_MODE2] set_property -dict {PACKAGE_PIN C10 IOSTANDARD LVCMOS33} [get_ports RXERR_PHYAD0] set_property -dict {PACKAGE_PIN C11 IOSTANDARD LVCMOS33} [get_ports RXD0_MODE0] set_property -dict {PACKAGE_PIN D10 IOSTANDARD LVCMOS33} [get_ports RXD1_MODE1] set_property -dict {PACKAGE_PIN B9 IOSTANDARD LVCMOS33} [get_ports TXEN] set_property -dict {PACKAGE_PIN A10 IOSTANDARD LVCMOS33} [get_ports TXD0] set_property -dict {PACKAGE_PIN A8 IOSTANDARD LVCMOS33} [get_ports TXD1] set_property -dict {PACKAGE_PIN D5 IOSTANDARD LVCMOS33} [get_ports CLKIN] set_property -dict {PACKAGE_PIN B8 IOSTANDARD LVCMOS33} [get_ports INT_REFCLKO]到這里可以點擊 Run Synthesis做綜合,幾秒鐘完成后用Set Up Debug配置ChipScope:
設置觀察長度為8192,因為持續時間會比較長。下面就可以Run Implementation和Generate Bitstream生成配置文件了。
和前面的教程一樣,USB線連接NEXYS4板子,開啟Hardware Manager,然后auto連接上板子,Program Device燒寫進程序,注意Debug probes file有對應的ltx文件,完成后用網線連接任意一臺主機或者貓到開發板上的RJ45接口,只要把RESET撥回到0,就可以看到它旁邊的兩個LED燈亮起:
下面看ChipScope抓取的結果,觀察前面的代碼,我們有加入一個輔助ChipScope的開關,用的是第二個開關,而復位是第一個開關。如此設計來觀察不同的寄存器讀取:
我們讀取了四個寄存器,它們在ChipScope里的結果分別如下
PHYID1讀出的結果是0x0007:
PHYID2讀出的結果是0xc0f1:
Basic Control基礎控制寄存器的結果是0x3000
Basic Status基礎狀態寄存器的結果是0x7809
查閱文檔可以看出讀出的結果和預期是相符的。
在這之后我們就可以看看是否從網線上讀出了什么數據,修改ChipScope的trigger為CRS_DV上升沿R,抓取到的數據如下:
可以清楚的看到LAN8720A接收到了一串數據包,放大可以看清具體數據,如果仔細分析甚至可以看到IP地址、MAC地址之類的信息,這個就不公開了,可以自己嘗試后自己分析
總結
這篇教程介紹的是NEXYS 4開發板上局域網物理層芯片LAN8720A的用法,這塊芯片因為配置簡單而非常常用,市面上可以買到的嵌入式局域網模塊很多都是基于這款芯片,因此這篇教程不只是針對FPGA的開發者,還可以讓其他嵌入式系統的學習者借鑒,比如想用Arduino控制類似局域網模塊的開發者。
NEXYS 4上已經介紹了一部分接口,剩下的有UART串口通信、USB接口、麥克風、Pmod通用接口、溫度傳感器、模數轉換ADC、音頻接口、視頻接口VGA。當全部介紹過之后,一個熟練的FPGA開發者就可以綜合利用板上的幾乎全部資源,再加上FPGA強大的并行計算能力,能做出很多ARM架構嵌入式系統無法完成的效果。
下一篇介紹UART串口通信,利用平時燒寫芯片的USB線和開發板通信
總結
以上是生活随笔為你收集整理的FPGA基础入门【10】开发板Ethernet PHY局域网配置的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Google Colaboratory:
- 下一篇: 2022-2027年中国安全仪表系统(S