FPGA Development Manual
1 Using and Adding Pango IP Cores
1.1 Experiment Introduction
Experiment objective: Understand how the PDS software installs IPs, uses IPs, and views IP manuals.
Experiment environment: Window11 PDS2022.2-SP6.4 Chip model: PG2L50H-484
1.2 Experiment Principle
1.2.1 IP Installation
After the PDS software is installed, PDS comes with some basic IPs; other IPs require the user to download the IP installation package and install the IP.

After opening PDS, click the IP icon in the red box above.

Then in the tab that pops up, click File->Update... in the upper-left corner.

Click Add Package in the upper-left corner.

As shown above, these are the installation files for the PCIE IP; their suffix is .iar. After selecting the corresponding file, click Open in the lower-right corner. Then tick the checkmark in front, and click Install.

Afterwards, you can see the just-installed IP in the panel on the left. Note that if you find a warning pops up after installation and the left panel shows no change, that means the device family does not support the IP you installed — because your project may be LOGOS, LOGOS2, or Tian2 series, and different chip families use slightly different IPs, so pay attention to this.
1.2.2 Instantiating an IP and viewing the IP manual

Continue clicking the icon in the red box above.

Select the IP you want to generate — here FIFO is used as an example, as shown in red box 1. Red box 2 is used to fill in the name of the generated IP. Click red box 3 to generate the IP and pop up the IP configuration interface, as shown below:

The popup asks whether we want to add this IP to the project; just click YES. If we don't know how to use the IP, we can open the official reference manual to view it, as shown below:

Select the IP you want to view, then click the icon in red box 1 — the official reference document will pop up automatically.

After we finish configuring our IP, click Generate at red box 1 in the upper-left corner.


With no errors, generation has succeeded.

At the same time, the tool will automatically pop up an IP instantiation template for us to use. You only need to add this instantiation template into your own project to use the IP we generated.
2 Key-controlled LED Experiment
2.1 Experiment Introduction
Experiment objective: From creating a project to writing code, completing pin constraints, and finally generating a bitstream to download onto the development board — implement Key0 controlling led0 blinking and Key1 controlling led1 on/off.
Experiment environment: Window11 PDS2022.2-SP6.4 Chip model: PG2L50H-484
2.2 Experiment Principle
Everyone should be familiar with the common hour/minute/second time-keeping carry;
1 hour = 60 minutes = 3600 seconds; when the hour hand rotates 1 hour, the second hand ticks 3600 times.
The clock signal in a digital circuit also has a fixed rhythm; the start-to-end time of this rhythm is what we usually call the period (T).
In a digital system we usually care about the clock frequency. The relationship between frequency and period is:
f=1/T
And the crystal oscillator on this development board provides a 25 MHz single-ended clock.
So its period is about 40 ns. In our FPGA design, our always block typically assigns data on the rising edge of the clock, so we can define a variable that increments by 1 on every rising edge of the clock, turning it into a counter. Each increment by 1 means 40 ns has elapsed, so to time 1 s we only need it to count up to 24999999 — since we count from 0, counting to 24999999 is exactly one second. By analogy, 12499999 is 0.5 s.

The figure above is the schematic of the 2 LEDs on the development board.

The figure above is the schematic of the 2 push-buttons on the development board.
KEY0 controls LED0 to toggle its state every 1 s; KEY1 controls LED1 on/off state. (High level is represented by 1; low level is represented by 0.)
2.3 Interface List
top.v top-level module interface list:
| Port | I/O | Width | Description |
|---|---|---|---|
| sys_clk | input | 1 | System clock 25MHZ |
| key0 | input | 1 | User button 0 |
| key1 | input | 1 | User button 1 |
| led_0 | output | 1 | LED control signal |
| led_1 | output | 1 | LED control signal |
btn_deb_fix.v button debounce module interface list:
| Port | I/O | Width | Description |
|---|---|---|---|
| BTN_WIDTH | parameter | 4 | Number of buttons |
| sys_clk | input | 1 | System clock 25MHZ |
| rst_n | input | 1 | Global reset |
| btn_in | input | BTN_WIDTH | User button input |
| btn_deb_fix | output | BTN_WIDTH | Output after button debounce (pulse signal) |
2.4 Project Description
The project framework is as follows:

This project mainly implements button-controlled LED state. Button 0 controls led0 blinking; button 1 controls led1 on/off.
First, both key0 and key1 pass through the button debounce module, because the development board uses mechanical buttons, so every press produces bouncing; without debouncing this would cause misdetection. After debouncing, every button press produces a high level lasting one clk — i.e. key0_flag and key1_flag. key0_flag controls whether the 1 s counter is enabled to start led0 blinking; key1_flag directly controls LED1 toggling — each press of key1 toggles the LED state once.
2.5 Code Module Description
//key0->led0 闪烁
//key1->key1 翻转
module top(
input wire sys_clk "," //系统时钟25MHZ
input wire key0 ","
input wire key1 ","
output reg led_0 ","
output reg led_1
);
//----------------------------------parameter----------------------------------------
parameter CNT_MAX = 32'd25_000_000 ; //1s计数
//----------------------------------reg----------------------------------------------
reg [7:0] rsn_cnt 0 ; //复位计数器
reg [31:0] cnt_1s ; //计数器
reg led0_en ; //led0闪烁使能
//----------------------------------wire----------------------------------------------
wire rst_n ; "//复位信号,低电平有效"
wire key0_flag ; //按键按下后的上升沿
wire key1_flag ; //按键按下后的上升沿
//----------------------------------always & assign----------------------------------------------
//产生复位
always@(posedge sys_clk) begin
if(rsn_cnt >=100)
rsn_cnt <= rsn_cnt;
else
rsn_cnt <= rsn_cnt + 1'b1;
end
assign rst_n = (rsn_cnt>=100)?1'b1:1'b0 ;
//每按下一次key0进行一次翻转
always@(posedge sys_clk) begin
if(!rst_n)
led0_en <= 1'd0;
else if(key0_flag)
led0_en <= ~led0_en;
end
//计数1s
always@(posedge sys_clk) begin
if(!rst_n)
cnt_1s <= 32'd0;
else if(led0_en) //led0闪烁使能
begin
if(cnt_1s == CNT_MAX-1) //1秒
cnt_1s <= 32'd0;
else
cnt_1s <= cnt_1s + 1'b1;
end
else
cnt_1s <= 32'd0;
end
//led0 1s闪烁
always@(posedge sys_clk) begin
if(!rst_n)
led_0 <= 1'd0;
else if(led0_en)
begin
if(cnt_1s == CNT_MAX-1) //1s翻转led
led_0 <= ~led_0;
else
led_0 <= led_0;
end
else
led_0 <= 1'd0;
end
//led1翻转
always@(posedge sys_clk) begin
if(!rst_n)
led_1 <= 1'd0;
else if(key1_flag)
led_1 <= ~led_1;
end
//----------------------------------instance----------------------------------------------
//按键消抖模块
btn_deb_fix#(
BTN_WIDTH ( 4'd2 ) //2个按键
)u_btn_deb_fix(
sys_clk ( sys_clk "),"
rst_n ( rst_n "),"
btn_in ( "{key1,key0}" "),"
btn_deb_fix ( "{key1_flag,key0_flag}" )
);
endmoduleCNT_MAX defines a maximum count value. Because our system clock is 25 MHz — i.e. 25000000 — to make the LED blink every 1 s we count from 0 to 24999999 and then toggle the LED once.
In lines 26–31, a reset signal is generated after counting 100 cycles of the system clock, to provide reset for subsequent modules and sequential logic.
In lines 43–55, counting of one second begins only when led0_en is pulled high; otherwise the counter stays at 0.
In lines 82–89, a button debounce module is instantiated. After a button is pressed and released, it produces a pulse signal — i.e. key0_flag and key1_flag — where key0_flag controls led0 blinking and key1_flag controls led1 toggling.
//按键消抖
`define UD #1
module btn_deb_fix#(
parameter BTN_WIDTH = 4'd8 //按键数量
)
(
input sys_clk ","
input wire rst_n ","
input [BTN_WIDTH-1:0] btn_in ","
output reg [BTN_WIDTH-1:0] btn_deb_fix
);
//----------------------------------parameter----------------------------------------
parameter CNT_20MS_MAX = 32'd500_000 ; //20MS计数
//----------------------------------reg----------------------------------------------
reg [23:0] cnt[BTN_WIDTH-1:0]; //计数器
reg [BTN_WIDTH-1:0] btn_in_reg ; //寄存按键信号
//打一拍
always @(posedge sys_clk) begin
btn_in_reg <= btn_in;
end
//----------------------------------消抖主要逻辑----------------------------------------------
genvar i;
generate
begin
for(i=0;i<BTN_WIDTH;i=i+1)
begin
always @(posedge sys_clk) begin
if(!rst_n)
cnt[i] <= 24'd0;
if (btn_in_reg[i] == 1'b0) //按下时 计数20ms时归零
cnt[i] <= 24'd0;
else if(cnt[i]==CNT_20MS_MAX) //抖动区间有效时计数
cnt[i] <= cnt[i];
else
cnt[i] <= cnt[i] + 1'b1;
end
always @(posedge sys_clk) begin
if(!rst_n)
btn_deb_fix[i] <= 1'd0;
else if(cnt[i]==CNT_20MS_MAX-1) //消抖后输出一个clk的高电平
btn_deb_fix[i] <= 1'b1;
else
btn_deb_fix[i] <= 1'b0;
end
end
end
endgenerate
endmoduleThis part is the button debounce module. The parameter defines the number of button inputs; the module output produces a pulse signal — i.e. a high level lasting one clk.
In lines 30–50, cnt continuously counts for 20 ms; when the button is pressed, cnt resets to 0 and counts up from 0 to 20 ms. When it reaches 20 ms, it outputs a one-clk high level — setting btn_deb_fix to 1 — and holds it for only one clk.
2.6 Experiment Steps
This section describes in detail the specific steps from creating a new project to downloading the program; subsequent projects will not be explained in such detail.
2.6.1 Open the PDS software and create a project
Step 1: Open the PDS software, click NEW Project, then complete the new-project setup.

Step 2: Click NEXT.

Step 3: Create a project named led_water in the corresponding directory, then click Next.
Creating a new project mainly includes setting the project name and path, project type, project files, and device information.
Project Name is the project file name, which defaults to project. (Only letters, digits, underscore (_), dash (-), dot (.) are allowed.)
Project Location is used to select the working path for the new project. The folder name allows only letters, digits, underscore (_), dash (-), dot (.), @, ~, comma, +, =, #, and space — but a space must not appear at the start or end of the path name. That is, this is the path where the project files are placed.
Create Preject Subdirectory makes the project file name part of the working directory.

Step 4: Select RTL project and click Next.
RTL Project is used to create an RTL project. The new project can perform synthesize, device map, place& route, report timing, report power, generate netlist, and generate bitstream, etc.
Post-Synthesize Project is used to create a post-synthesis project. The new project can perform device map, place& route, report timing, report power, generate netlist, and generate bitstream, etc.

Step 5: Click Next.
This interface allows you to use Add Files and Add Directories to add RTL source files and create new RTL source files, as well as adjust the RTL file compile order. Add Files adds selected files; Add Directories adds all suitable files in the selected folder. If you tick Add source from subdirecotires below, it adds all suitable files in subdirectories as well. You can also skip adding files by clicking NEXT directly.

Step 6: Click Next.

Step 7: Click Next.

Step 8: Select the device family, model, package, speed, and synthesis tool, then click Next.
In synthesize tool you can choose the synthesis tool as Synplify Pro or ADS; we use the ADS synthesis tool in this experiment.

Step 9: Click Finish in the summary to complete project creation.

2.6.2 Add design files
The PDS software interface is shown below:

Double-click Designs to create the previously designed module in a file, or add the previously edited Verilog file to the project:

Add files to the project:
In the window, click Add Files to select files to add to the project;

Create a new file in the project:
- In the window, click Create File;

- Select Verilog Design File; the file name matches the module name; default path; click OK;

- Click OK;

- Click Cancel;

- The new file opens by default; copy the previously designed code into it;

- Click save to finish creating the new file.

Ctrl+S to save.

Double-click Designs.

Click Add Files;

Add the btn_deb_fix.v module — i.e. the button debounce module.

Click OK.
2.6.3 Compile
You can run the Compile flow in any of the following ways:
(1) Double-click Compile in the Flow to synthesize;
(2) Right-click Compile and click Run to synthesize;

2.6.4 Project Constraints
Click Tools and choose User Constraint Editor (Timing and Logic), or click the toolbar icon, User Constraint Editor (Timing and Logic), and select Pre Synthesize UCE, as shown below.

User Constraint Editor (Timing and Logic) under Tools

The User Constraint Editor (Timing and Logic) icon in the toolbar
2.6.4.1 Clock constraints
After opening the UCE, select Timing Constraints, then select Create Clock to add the reference clock. The reference clock is generally the on-board clock entered through an input port.

In the popup, name the clock, associate the clock pin, and add the clock parameters. Clicking OK creates a clock constraint; Reset resets this page. After creation it looks as shown below:

The clock provided to the development board is 25 MHz, i.e. a 40 ns period.

2.6.4.2 Physical constraints
After opening the UCE, select Device, then select I/O, and edit the IO assignments according to the schematic.

After editing the IO assignments according to the schematic, click save — this generates an .fdc file, completing the constraints.
2.6.5 Synthesize
The Synthesize flow can be run in the following ways:
(1) Double-click Synthesize in the Flow to synthesize;
(2) Right-click Synthesize and click Run to synthesize;
After completing Synthesize, you will see the following:

2.6.6 Device Map
The main purpose of Device Map is to map the design onto specific model sub-cells (LUT, FF, Carry, etc.). The Device Map flow can be run in the following ways:
(1) Double-click Device Map directly;
(2) Right-click Device Map and click Run;
After completing Device Map, you will see the following:

2.6.7 Place & Route
Place & Route performs the actual placement and routing of design modules according to user constraints and physical constraints. The Place & Route flow can be run in the following ways:
(1) Double-click Place & Route directly;
(2) Right-click Place & Route and click Run;
After completing Device Map, you will see the following:

2.6.8 Generate Bitstream
Generate Bitstream produces a binary bitstream file. The Generate Bitstream flow can be run in the following ways:
(1) Double-click Generate Bitstream directly;
(2) Right-click Generate Bitstream and click Run;
After the above operations, a bitstream file will be produced. Running Generate Bitstream, the interface is as shown below:

2.6.9 Download the generated bitstream file
Click Tools and select Configuration, or click the Configuration icon in the toolbar, as shown below.

Configuration under Tools

The Configuration icon in the toolbar
After opening Configuration, select Scan Device directly to perform the JTAG chain scan. Once the chain initializes successfully, all devices scanned on the chain are displayed in the work area, and the device attributes window shows the current device's information. A dialog pops up showing the configuration files that can be added for the device:

Chain initialization succeeded
In the dialog, select the bitstream file to add that configuration file; the absolute path of the loaded file is displayed and shown in the message bar, as shown below:

Download the bitstream file

When all 4 signals are 1, the download succeeded.
The development board is also equipped with an external flash. To flash the program onto the board, the bitstream file must first be converted into an .sfc file.
First click the Covert File option under the Operations section on the Configuration page.

After clicking, the following screen appears. On the Generate Flash Programing File page, select the manufacturer and model of the corresponding Flash device, then at BitStreamFile select the path to the bitstream file and click OK. (If the flash device in use is not in the selectable flash list, you must manually add the corresponding flash model; for the procedure, refer to the development-board download and flashing instructions.)

After successfully converting to the .sfc file, the page is as shown below; click OK.

The user can right-click the position below and click Scan Outer Flash.

The page displays the model of the Flash mounted on the board. Click the .sfc file and click OPEN.

After right-clicking the position below, click Program.

Successful Flash programming is as shown below:

Now power off the board and re-apply power. If pressing key0 and key1 produces the corresponding experimental behavior, flashing succeeded. (Wait about 15 s.)
The final on-board result is as follows.
3 Pango Clock Resources — Phase-Locked Loop
3.1 Experiment Introduction
Experiment objective: Understand the basic usage of the PLL IP.
Experiment environment: Window11 PDS2022.2-SP6.4 Chip model: PG2L50H-484
3.2 Experiment Principle
3.2.1 PLL Introduction
A phase-locked loop (PLL) is a feedback-control circuit whose characteristic is using an externally input reference signal to control the frequency and phase of the oscillation signal inside the loop. Because a PLL can automatically track the input signal's frequency with its output signal's frequency, PLLs are commonly used in closed-loop tracking circuits. While the PLL is operating, when the output signal's frequency equals the input signal's frequency, the output voltage and input voltage maintain a fixed phase difference — i.e. the phase of the output voltage is "locked" to that of the input voltage. This is the origin of the name "phase-locked loop".
A PLL has powerful capabilities: it can perform arbitrary frequency division, frequency multiplication, phase adjustment, and duty-cycle adjustment on the clock signal input to the FPGA, thereby outputting a desired clock. In addition, in some complex projects, even when we don't need to modify any clock parameters, we often use a PLL to optimize clock jitter in order to obtain a more stable clock signal. It is precisely because these PLL capabilities are what we need in actual design — and cannot be achieved by writing code — that the PLL IP core has become one of the most commonly used IP cores in program design.
The PLL IP is an IP designed by Pango based on PLL and clock-network resources. Through different parameter configurations, it can implement clock frequency adjustment, phase adjustment, synchronization, frequency synthesis, and more.
3.2.2 IP Configuration
First click the "IP" icon in the shortcut toolbar to enter the IP instantiation settings.

Then select PLL in the IP directory, give a name to this instantiated IP in Instance name, and click Customise to enter the IP configuration page. The operation is illustrated below:

The PLL can be used in two modes: Basic and Advanced. In Advanced mode, the PLL's internal parameters are fully exposed; you must fill in the input divider, output divider, duty cycle, phase, feedback divider, etc. yourself to configure it correctly. In Basic mode, the user does not need to worry about the PLL's internal parameters — just enter the desired frequency, phase, duty cycle, etc., and the IP will automatically compute the optimal configuration parameters. If there is no special requirement, Basic mode is recommended for configuring the PLL. In this experiment we choose Basic Configuration.

Next, the basic configuration:
In Public Configurations, set the input clock frequency to 25 MHz.
Under the Clockout0 Configurations tab, tick to enable clkout0 and set the output frequency to 50 MHz.
Under the Clockout1 Configurations tab, tick to enable clkout1 and set the output frequency to 100 MHz.
Under the Clockout2 Configurations tab, tick to enable clkout2, set the output frequency to 100 MHz, and set the phase offset to 180 degrees.
Other options can use default settings; if you have other needs, consult the IP manual. This experiment only covers the basic usage of the IP:

Click Generate in the upper-left corner to generate the IP.

3.3 Code Design
The module interface list is as follows:
PLL IP usage experiment module interface table
| Port | I/O | Width | Description |
|---|---|---|---|
| sys_clk | input | 1 | System clock |
| clkout0 | output | 1 | 54 MHz clock |
| clkout1 | output | 1 | 81 MHz clock |
| clkout2 | output | 1 | 81 MHz clock, phase offset 180 degrees |
| lock | output | 1 | Clock-locked signal; when high, indicates the IP core output clock is stable. |
PLL_TEST top-level code:
module PLL_TEST(
input sys_clk ,
output clkout0 ,
output clkout1 ,
output clkout2 ,
output lock
);
PLL PLL_U0 (
.clkout0 (clkout0 ),// output
.clkout1 (clkout1 ),// output
.clkout2 (clkout2 ),// output
.lock (lock ),// output
.clkin1 (sys_clk ) // input
);
endmoduleThis module instantiates the PLL IP core; the function is simple and is not explained further here.
PLL_tb test code:
timescale 1ns / 1ps
module PLL_tb();
reg sys_clk ;
wire clkout0 ;
wire clkout1 ;
wire clkout2 ;
wire lock ;
initial
begin
#2
sys_clk <= 0 ;
end
parameter CLK_FREQ = 25;//Mhz
always # ( 1000/CLK_FREQ/2 ) sys_clk = ~sys_clk ;
PLL_TEST u_PLL_TEST(
.sys_clk (sys_clk ),
.clkout0 (clkout0 ),
.clkout1 (clkout1 ),
.clkout2 (clkout2 ),
.lock (lock )
);
endmoduleThe timescale defines the simulation time unit and time precision of the module. The time unit is 1 nanosecond and the precision is 1 picosecond.
The initial block initializes the system clock. Two nanoseconds after the simulation starts, the system clock sys_clk is set to 0. This defines a known initial state at the start of the simulation.
The code defines a clock-frequency parameter CLK_FREQ of 25 MHz and uses an always block to toggle the system clock signal. The logic in the always block toggles sys_clk every 40 nanoseconds, thereby generating a 25 MHz square-wave clock signal. This clock signal drives the PLL_TEST module under test.
Finally, the various signals of the testbench are connected to the PLL_TEST module. This includes connecting the generated system clock sys_clk to the clock input of PLL_TEST, and bringing out PLL_TEST's output signals clkout0, clkout1, clkout2, and lock using wires for observation.
3.4 PDS and Modelsim Co-simulation
PDS supports co-simulation with third-party simulators such as Modelsim or QuestaSim. Modelsim is the more commonly used simulator, so we use PDS together with Modelsim for co-simulation.
Next, select Project -> Project Setting to open the project settings and prepare to configure co-simulation.

Select the Simulation tab. Red box 1 selects the path to the simulation library just compiled; red box 2 selects the Modelsim launch path; then click OK.
Right-click the file to be simulated and select Run Behavior Simulation to start behavioral simulation.

After running, Modelsim opens automatically and the simulation executes. If there are no errors, it succeeded. If errors appear, check the PDS and Modelsim configuration.

3.5 Experimental Phenomena
Click Wave to observe the PLL output signals:

Using the cursor to measure clkout0, we find one clock period is 20 ns — i.e. 50 MHz.

You can see clkout1's clock frequency is 100 MHz, and it is 180° out of phase with clkout2 — consistent with the settings. Note that the PLL's output clocks should only be used after the clock-lock signal lock is valid; the clocks output before lock goes high are indeterminate.
4 Using ROM, RAM, and FIFO
4.1 Experiment Introduction
Experiment objective: Master the use of RAM, ROM, and FIFO IPs on the Pango platform.
Experiment environment: Window11 PDS2022.2-SP6.4 Chip model: PG2L50H-484
4.2 Experiment Principle
Whether it's the Logos series or the Logos2 series, the IP configuration as well as the modes and functions are identical — they don't have differences like the PLL's dynamic-configuration and internal-feedback options. So RAM, ROM, and FIFO are universal.
4.2.1 RAM Introduction
RAM is random-access memory. During operation it can write data to any address and read data from any address. It can be used for data caching, clock-domain crossing, and storing intermediate algorithm results, among other things.
Note that the PDS IP configuration tool provides two different RAMs: one is Distributed RAM and the other is DRM Based RAM. Distributed RAM is a RAM built from LUT (lookup-table) resources — this kind of RAM consumes a lot of LUT resources, so it is typically used only for relatively small storage to save DRM resources. DRM Based RAM is a RAM built from on-chip DRM resources; it does not occupy logic resources and is fast, so DRM Based RAM is used in most designs.
RAM is divided into three types, as shown in the following table:
| RAM type | Characteristics |
|---|---|
| Single-port RAM | Only one port can read/write. There is only one read/write port and one address port |
| Pseudo dual-port RAM | Has wr and rd ports; as the name implies, wr can only write and rd can only read |
| True dual-port RAM | Provides A and B ports; both ports can independently read and write |
Note that when using true dual-port, avoid reading and writing the same address at the same time — this will cause the write to fail, and should be avoided in logic design.
Below we present the commonly used RAM configuration as an introduction. We typically use the pseudo dual-port RAM for design, as shown below:

The following figure shows the IP configuration:

Note that if you tick Enable Output Register (output register), the output data is delayed by one clock cycle.
For the meaning of each port, refer to the official manual; you can also view the IP manual yourself, as shown below:

DRM Resource Type: Configures which resource the RAM IP core is built from. Different chip models have different selectable resources — some are 9K, some 18K, some 36K. If there is no special case, just use AUTO.
4.2.1.1 RAM Read/Write Timing
When configured in different modes, the RAM's read/write timing differs. True dual-port and single-port RAM each have three modes, while pseudo dual-port has only one. Since the true dual-port and single-port configurations are the same, we use true dual-port as the example.
There are three modes: NORMAL_WRITE (normal mode), TRANSPARENT_WRITE (transparent write), and READ_BEFORE_WRITE (read-priority mode).

Pseudo dual-port does not belong to the above three modes; it has its own unique mode. The difference between these modes lies in the read/write timing. Next, we analyze the read/write timing.
The following timing diagrams are all from the official IP manual, and none of them enable the output register. Note that wr_en = 1 means write data, and 0 means read data.
(1) NORMAL_WRITE

In NORMAL_WRITE mode, you can see that when the clock rising edge arrives and both clk_en and wr_en are high, the data is written to the corresponding address — as at time 1 in the figure. Then looking at the read-data port: when wr_en is not 0, a_rd_data is always in the Don't Care state; when the clock rising edge arrives, clk_en is high, and wr_en is low, a_rd_data outputs the data at the current a_addr — i.e. Mem(ADDR1) and D0 in ADDR0.
(2) READ_BEFORE_WRITE

In READ_BEFORE_WRITE mode, at time 1, when the clock rising edge arrives and both clk_en and wr_en are high, D0 is written into ADDR0. But notice a_rd_data and a_addr at this time — a_wr_en is not 0, yet a_rd_data still outputs the previous data of ADDR0 (because it's not outputting D0). Afterwards, a_wr_en goes low — now it reads data. At time 3, the data of ADDR0 is read out and a_rd_data outputs D0.
So to summarize, this mode outputs the original data of the address currently being written while a write operation is in progress. That's why it's fittingly called read-priority mode — it prioritizes reading out the original data.
(3) Transparent_Write

In Transparent_Write mode, at time 1, when the clock rising edge arrives and both clk_en and wr_en are high, D0 is written into ADDR0. But notice a_rd_data and a_addr at this time — a_wr_en is not 0, yet a_rd_data directly outputs D0. Afterwards a_wr_en goes low, entering the read state; at time 2, the data of ADDR0 is read out again and D0 is output.
Analyzing and summarizing the situation at time 1, we can conclude that in this mode, when we perform a write operation, the read port immediately outputs the data we are writing. Hence it is called transparent-write mode.
(4) Pseudo dual-port read/write timing
Note: wr_en = 1 is a write operation; 0 is a read operation.
The pseudo dual-port read/write timing differs from all three above. Let's analyze the timing in the figure below:

Note time 1: both wr_en and wr_clk_en are high, so it's a write operation. At time 1, D0 is written to address ADDR0. Notice rd_addr and rd_data at this time — rd_addr is ADDR2, and during the write operation rd_data likewise outputs the data in ADDR2, while wr_en is still high. Next look at times 2 and 3: wr_en is 0 and rd_clk_en is high, so these are read operations — ADDR1 and ADDR0 data are read out respectively. Then rd_clk_en goes low, the read clock is inactive, and rd_data keeps outputting D0.
To summarize, focusing on time 1: at time 1 D0 is written to ADDR0, yet the read port outputs the data at ADDR2. Observing carefully, we can conclude: when the pseudo dual-port RAM performs a write operation, it outputs the data at the address the read port currently points to. Isn't that a bit like transparent write? Except transparent write outputs the data being written, whereas pseudo dual-port outputs the data at the address the read port points to.
2.1.1.2 ROM Introduction
ROM is read-only memory. During program execution it can only be read, not written, so we should configure its initial value at initialization — generally by importing a .dat file when generating the IP to configure the initial value.
Note that the PDS IP configuration tool provides two different ROMs: one is Distributed ROM and the other is DRM Based ROM. Distributed ROM is a ROM built from LUT (lookup-table) resources — this kind of ROM consumes a lot of LUT resources, so it is typically used only for relatively small storage to save DRM resources. DRM Based ROM is a ROM built from on-chip DRM resources; it does not occupy logic resources and is fast, so DRM Based ROM is used in most designs.
Below we present the commonly used ROM configuration as an introduction. Since it can only be read, these are all single-port ROMs, as shown below:
The following figure shows the IP configuration:

Note that if you tick Enable Output Register (output register), the output data is delayed by one clock cycle.
At the same time, you can see the Enable Init option is ticked by default and cannot be unchecked.
The format of the imported data can only be binary or hexadecimal; the demo selects hexadecimal.
For the meaning of each port, refer to the official manual; you can also view the IP manual yourself, as shown below:

Generally we only need four signals: addr, rd_data, clk, and rst.
The following timing diagrams are all from the official IP manual, and none of them enable the output register.
4.2.1.2 ROM Read Timing

You can see the timing is very simple. For example at time T1, when the clk rising edge arrives and clk_en is high, providing the address to be read makes rd_data output the data. Without ticking the output-enable register, rd_data's output has a delay — the specific time can be seen in simulation — so we obtain the ROM's read value at the rising edge of the next clock cycle, i.e. the rising edge of T2.
So the overall timing is very simple: if clk_en is ticked, clk_en must be high to read data; if clk_en is not ticked, ROM data is read continuously according to the address.
4.2.2 FIFO Introduction
FIFO is first-in-first-out. In an FPGA, the role of a FIFO is a buffer with first-in-first-out characteristics for the stored data, often used for data caching or for cross-clock-domain data transfer. The biggest difference between FIFO and RAM is that FIFO does not need addresses — it uses sequential write-in and sequential read-out.
In the Pango IP tool there are Distribute FIFO and DRM FIFO — these are simply built from different resources. The former, Distribute FIFO (distributed FIFO), is built from on-chip LUT resources, while DRM FIFO is built from on-chip DRM resources. A DRM FIFO has better performance than one built from LUT resources — not only larger capacity but also more configurable features.
This chapter focuses on DRM Based FIFO.
Note: Once a FIFO is full, you must not continue writing data — otherwise a write overflow will occur.
Note: Once a FIFO is empty, you must not continue reading data — otherwise a read overflow will occur.
Below we present the commonly used FIFO configuration as an introduction.


Note that if you tick Enable Output Register (output register), the output data is delayed by one clock cycle.
FIFO Type has two options, SYNC and ASYNC. The first is a synchronous FIFO, where the read and write ports share a single clock and reset; the other is an asynchronous FIFO, where the read/write clocks and resets are all independent. In everyday design, the asynchronous FIFO is more commonly used, because the synchronous FIFO and asynchronous FIFO have identical read/write timing — only the read/write port clocks and resets differ. When the asynchronous FIFO's read and write ports use the same clock and reset, the asynchronous FIFO and synchronous FIFO are essentially the same.
Reset Type can also be SYNC or ASYNC. In SYNC mode, the reset takes effect only when sampled by the clock's rising edge; in ASYNC mode, the FIFO resets immediately once the reset is asserted.
For the remaining port descriptions, refer to the official IP manual, as shown below:

Here rd_water_level and wr_water_level represent the "amount of readable data" and "amount of data already written" respectively — their meaning is the same as Xilinx's wr_data_count and rd_data_count for FIFOs.

Only when we tick Enable Almost Full Water Level and Enable Almost Empty Water Level can we see rd_water_level and wr_water_level. The Almost Full Numbers setting means that when 1020 data items have been written, the Almost Full signal goes high; the Almost Empty Numbers setting means that when 4 readable data items remain, the Almost Empty signal goes high.
FIFO also supports mixed bit-widths — for example a 16-bit write port and an 8-bit read port. If you write 16'h0102, you will read out 8'h02 then 8'h01 — the low bits are read out first.
If the write port is 8-bit and the read port is 16-bit: when you write 8'h01 and then 8'h02, you read out 16'h0201 — the data written first is placed in the low bits.
4.2.2.1 FIFO Read/Write Timing
Because the synchronous FIFO and the asynchronous FIFO have identical read/write timing, we use the asynchronous FIFO's read/write timing diagrams for the introduction.
Note: Reset is active high. None of the read data enables Enable Output Register (output register).
(1) FIFO write timing when not full

You can see that at time 1, the reset signal is low (in working state); at the rising edge of wr_clk with wr_en high, data D0 is written into the FIFO, and wr_water_level changes from 0 to 1, indicating one data item has been written. At this point, look at the empty signal on the read port: at time 3 the empty signal goes from high to low, meaning the read port now has data to read and the FIFO is no longer empty. Note that rd_clk and wr_clk are different; from the write at time 1 to the empty going low at time 3, three rd_clk have elapsed.
So we can conclude: rd_water_level lags wr_water_level by three rd_clk.
(2) FIFO write timing when almost full

When almost full, we mainly analyze the full and almost_full signals. Suppose Almost Full Numbers is set to N-2; at time 1, N-6 data items have already been written, meaning 6 more writes will fill the FIFO. From time 1 to time 2 a total of 4 data items are written, so when wr_water_level becomes N-2 the condition is met — Almost Full goes high, and after two more data writes the FIFO is full, so two clock cycles later the Full signal goes high.
(3) FIFO read timing in the full state

In the full state, the FIFO already has N data items. At state 1, on the rising edge of rd_clk with rd_en high, data is read out from the FIFO (data output has a delay — 0.2 ns in simulation). Now rd_water_level becomes N-1 and rd_data outputs D0. Then look at time 2: the full signal goes low. We can check: from time 1 to time 2, three wr_clk elapse before the write port can determine that the data amount is no longer full. So we can conclude: wr_water_level lags rd_water_level by three wr_clk.
(4) FIFO read timing when almost empty

At time 1, 4 readable data items remain. Suppose Almost Empty Number is set to 2; at time 1 and time 2, two data items are read out respectively, so at time 2 the readable data amount drops to 2 — meeting the Almost Empty Number trigger condition — so the almost_empty signal goes high. Two clock cycles later — i.e. after reading two more data items — the FIFO becomes empty, which is state 3, and the empty signal goes high.
2.1.2 Interface list
This section describes the interfaces of each top-level module.
ram_test_top.v
| Port | I/O | Width | Description |
|---|---|---|---|
| wr_clk | input | 1 | Write clock |
| rd_clk | input | 1 | Read clock |
| rst_n | input | 1 | Global reset |
| rw_en | input | 1 | 1: write op, 0: read op |
| wr_addr | input | 5 | Write address |
| rd_addr | input | 5 | Read address |
| Wr_data | input | 8 | Data written to RAM |
| Rd_data | output | 8 | Data read out from RAM |
rom_test_top.v
| Port | I/O | Width | Description |
|---|---|---|---|
| rd_clk | input | 1 | Read clock |
| rst_n | input | 1 | Global reset |
| rd_addr | input | 10 | Read address |
| rd_data | input | 64 | Data read out from ROM |
fifo_test_top.v
| Port | I/O | Width | Description |
|---|---|---|---|
| sys_clk | input | 1 | Write/read clock |
| rst_n | input | 1 | Global reset |
| wr_addr | input | 8 | Data written to FIFO |
| wr_en | input | 1 | Write enable |
| rd_en | input | 1 | Read enable |
| wr_water_level | output | 8 | Amount of data written to FIFO |
| rd_water_level | output | 8 | Amount of data readable from FIFO |
| Rd_data | output | 8 | Data read out from FIFO |
4.3 Project Description
None
4.4 Code Simulation Description
This time the top-level module is really just instantiating the IP and bringing out the ports, so most of the code is in the testbench. We therefore go straight to the simulation code.
4.4.1 RAM Simulation Test
`timescale 1ns/1ns
module ram test tb()
reg sys clk
reg rd clk
reg rst n
reg rw en //读写使能信号
reg [7:0] wr data
reg [4:0] wr addr
reg [4:0] rd addr
wire [7:0] rd data
reg [1:0] state
initial
begin
rst n <= 1'd0
sys clk <= 1'd0
rd clk <= 1'd0
#20
rst n <= 1'd1
end
//读写控制
always@(posedge sys clk or negedge rst n) begin
if(!rst n)
begin
state <= 2'd0
wr data <= 8'd0
rw en <= 1'd0
wr addr <= 8'd0
rd addr <= 8'd0
end
else
begin
case(state)
2'd0:begin
rw en <= 1'd1
state <= 2'd1
end
2'd1:begin
if(wr addr == 5'd31)
begin
rw en <= 1'd0
state <= 2'd2
wr data <= 8'd0
wr addr <= 5'd0
rd addr <= 5'd0
end
else
begin
state <= 2'd1
wr data <= wr data+1'b1
rd addr <= rd addr+1'b1
wr addr <= wr addr+1'b1
end
end
2'd2:begin
if(rd addr == 5'd31)
begin
state <= 2'd3
rd addr <= 5'd0
end
else
begin
state <= 2'd2
rd addr <= rd addr+1'b1
end
end
2'd3:begin
state <= 2'd0
end
default: state <= 2'd0
endcase
end
end
//50MHZ
always#10 sys clk = ~sys clk
//
GTP GRS GRS INST(
GRS N(1'b1)
)
ram test top u ram test top(
wr clk ( sys clk "),"
rd clk ( sys clk "),"
rst n ( rst n "),"
rw en ( rw en "),"
wr addr ( wr addr "),"
rd addr ( rd addr "),"
wr data ( wr data "),"
rd data ( rd data )
)
endmoduleBasic testbench operations will not be explained in detail here; we focus only on the key logic. Lines 27–80 of the code are the RAM read/write control state machine, mainly used to control the generation of read/write addresses and enables and the data to be written. Here we explain only the main implemented functions. First, lines 38–42 of the code — i.e. when state=0 — pull rw_en high and jump to state 1, entering the write operation (clk_en is not enabled, so we can ignore it). The next clock cycle begins writing data (note this is sequential logic, edge-sampled, so writing begins on the next clock cycle) — i.e. state=1 keeps writing data into the RAM. Lines 44–60 of the code are the write operation: you can see that when wr_addr is not equal to 31, wr_data and wr_addr increment by 1 (rd_addr is incremented here too; see the video explanation — mainly to verify the pseudo dual-port timing). When wr_addr equals 31, on the next clock cycle the data is cleared and the state jumps; in the current clock cycle it still writes data into address 31, so in this clock cycle a total of 32 data items are written (from address 0 to address 31). That is, state 1 completes writing 32 data items and then jumps to the state=2 logic. Lines 61–72 of the code — i.e. state=2 — increment rd_addr on the rising edge of each cycle until rd_addr=31, then on the next clock cycle clear the address and jump state, while in the current clock cycle it continues to read the data of address 31 — completing reading of addresses 0–31, a total of 32 data items. So this state mainly completes reading 32 data items, and on the next clock cycle jumps to state=3. state=3 — its main role is to wait one clock cycle and then jump back to state=0, acting as a delay.

The figure above is the waveform of the written data; the data increments from 0 to 31, and the addresses also go from 0 to 31.

The figure above is the read-data waveform; data 0–31 is read out from addresses 0–31.
For the specific waveform you can watch the simulation video or try simulating yourself, and read the code according to the waveform. Because this is sequential logic, beginners reading only text may be confused about why one more data item is read at rd_addr=31. It is recommended to simulate directly, or watch the simulation part of the video explanation, which helps with quick understanding.
A one-line summary: assignments in sequential logic always take effect on the next clock cycle. So the operation performed at rd_addr=31 is sampled and takes effect on the next clock cycle. Therefore the current clock still reads one more data item from the RAM.
4.4.2 ROM Simulation Test
`timescale 1ns/1ns
module rom_test_tb();
reg sys_clk;
reg rst_n;
reg [9:0] rd_addr;
wire [63:0] rd_data;
initial
begin
rst_n <= 1'd0;
sys_clk <= 1'd0;
#20
rst_n <= 1'd1;
end
//50MHZ
always#10 sys_clk = ~sys_clk;
//
GTP_GRS GRS_INST(
.GRS_N(1'b1)
) ;
always@(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
rd_addr <= 10'd0;
else
rd_addr <= #2 rd_addr + 1'b1;
end
rom_test_top u_rom_test_top(
.rd_clk ( sys_clk ),
.rst_n ( rst_n ),
.rd_addr ( rd_addr ),
.rd_data ( rd_data )
);
endmoduleLines 31–36 instantiate the ROM top-level module, which really just calls the ROM IP and brings out the signals to ports — no logic operations.
Lines 24–29 use an always block to continuously generate addresses and feed them to the ROM IP to read out data. Because clk_en is not ticked, data is read out continuously after the ROM reset completes. So there is no complex logic — just increment the address from 0 continuously and read out the data.

The figure above is the waveform of the read-out data; you can see the read-out data is consistent with the data in the dat file.

4.4.3 FIFO Simulation Test
`timescale 1ns/1ns
module fifo_test_tb();
reg sys_clk;
reg rst_n;
reg [7:0] wr_data;
reg wr_en;
reg rd_en;
reg rd_state; //读状态
reg wr_state;
wire [7:0] rd_data;
reg [7:0] rd_cnt;
wire [7:0] rd_water_level;
wire [7:0] wr_water_level;
initial
begin
rst_n <= 1'd0;
sys_clk <= 1'd0;
#20
rst_n <= 1'd1;
end
always#10 sys_clk = ~sys_clk; //50MHZ
always@(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
begin
wr_state <= 1'd0;
wr_en <= 1'd0;
wr_data <= 8'd0;
end
else
begin
case(wr_state)
1'd0: if(wr_water_level == 127) //128个数据
begin
wr_en <= #2 1'd0;
wr_data <= #2 8'd0;
wr_state <= #2 1'd1;
end
else
begin
wr_en <= #2 1'd1;
wr_data <= #2 wr_data+1'b1;
wr_state <= #2 1'd0;
end
1'd1: if(rd_cnt == 127)
wr_state <= #2 1'd0;
default: wr_state <=1'd0;
endcase
end
end
always@(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
begin
rd_state<= 1'd0;
rd_en <= 1'd0;
rd_cnt <= 8'd0;
end
else
begin
case(rd_state)
1'd0: if(rd_water_level >= 8'd128) //等待128个数据
begin
rd_state <= #2 1'd1;
rd_en <= #2 1'd1;
end
else
begin
rd_cnt <= #2 8'd0;
rd_state <= #2 1'd0;
end
1'd1: begin
rd_cnt <= #2 rd_cnt + 1'b1;
if(rd_cnt == 127)
begin
rd_en <= #2 1'd0;
rd_state <= #2 1'd0;
end
end
default: rd_state <= 1'd0;
endcase
end
end
GTP_GRS GRS_INST(
GRS_N(1'b1)
) ;
fifo_test_top u_fifo_test_top(
sys_clk ( sys_clk "),"
rst_n ( rst_n "),"
wr_data ( wr_data "),"
wr_en ( wr_en "),"
rd_en ( rd_en "),"
wr_water_level ( wr_water_level "),"
rd_water_level ( rd_water_level "),"
rd_data ( rd_data )
);
endmoduleBasic testbench operations will not be explained in detail here; we focus only on the key logic. The whole design is split into the control of two states — read and write — and completes writing 128 data items and reading 128 data items respectively. Because FIFO does not need addresses, only enable signals need to be generated.
First, the write state: in wr_state=0, the write enable is pulled high and wr_data is continuously incremented to write data into the FIFO. When wr_water_level=127, the write enable is pulled low, the write data is reset to 0, and the write state jumps to 1. Note that one more data item is written at this point, so a total of 128 data items have been written. The operations of pulling the write enable low, resetting write data to 0, and jumping the write state to 1 will be sampled and take effect on the next clock cycle. Afterwards, in wr_state=1, it keeps waiting on rd_cnt — this condition judges that when 128 data items have been read out, wr_state jumps back to state 0.
Next, the read state: in rd_state=0, once the amount of readable data exceeds 128 (inclusive), the state jumps to rd_state=1 and starts reading data. At the same time, in rd_state=1, a variable rd_cnt is used to count the read-out data. rd_cnt counts from 0; when rd_cnt=127, one more data item will be read from the FIFO, so a total of 128 data items have been read. On the next clock cycle both rd_en and rd_state will be set to 0.

The waveform of the written data is as above — 128 data items are written, from 1 to 128.

The waveform of the read data is as above — 128 data items are read out, from 1 to 128.
For the specific waveform you can watch the simulation video or try simulating yourself, and read the code according to the waveform. Because this is sequential logic, beginners reading only text may be confused about why one more data item is read at rd_cnt=127. It is recommended to simulate directly, or watch the simulation part of the video explanation, which helps with quick understanding.
A one-line summary: assignments in sequential logic always take effect on the next clock cycle. So the operation performed at rd_cnt=127 is sampled and takes effect on the next clock cycle. Therefore rd_en is still 1 on the current clock, and one more data item is read from the FIFO.
5 DDR3 Read/Write Experiment Routine
5.1 Experiment Introduction
Experiment objective: Complete the DDR3 read/write test.
Experiment environment: Window11 PDS2022.2-SP6.4 Chip model: PG2L50H-484
5.2 Experiment Principle
The development board integrates one 4 Gbit (512 MB) DDR3 chip, model MT41K256M16. The DDR3 bus width is 16 bit. The maximum data rate of the DDR3 SDRAM is 1066 Mbps.
5.2.1 DDR3 Controller Introduction
PG2L50H provides users with a complete DDR memory controller solution. The configuration is flexible, and the DDR memory control is implemented in soft IP, with the following features:
- Supports DDR3
- Supports x8, x16 Memory Device
- Maximum bit width of 32 bit
- Supports a streamlined AXI4 bus protocol
- One AXI4 256-bit Host Port
- Supports Self_refresh, Power down
- Supports Bypass DDRC
- Supports DDR3 Write Leveling and DQS Gate Training
- DDR3 maximum rate up to 1066 Mbps
5.3 Project Description
After installing PDS, you must manually add the DDR3 IP. Follow these steps:
DDR3 IP file: PG2L_IP\PG2L_IP\DDR3\ips2l_hmic_s_v1_10.iar
5.3.1 DDR3 Read/Write Example Project
Open the PDS software, create a new project ddr3_test, click the icon below to open the IP Compiler;

Select the DDR3 IP, name it ddr3_test, then click Customize;

In the DDR3 settings interface, set Step 1 as follows:

Set Step 2 as follows. You need to create a new DDR3 model yourself; choose MT41K256M16XX as the template, and keep the Timing parameters, addresses, and Drive Options consistent with the figure below.

Set Step 3 as follows: tick Custom Control/Address Group; pin constraints refer to the schematic:


Reminder:
When configuring the IP core, in step 3: pin/bank options, the correspondence between the Group Number in the pin settings and the schematic is as shown below:
R5 means BANK5; G1 means Group Number is 1.

Step 4 is a summary; click Generate to generate the DDR3 IP;

Close this project and open the Example project at this path:
Xxxxx\ddr3_test\ip_core\ddr3\pnr

Open the top-level file; you need to modify the top-level file. See the detailed code for specifics. The figure below is the modified top-level file.

For pins other than those already constrained in "Step 3", modify them against the schematic using the UCE tool. For porting, you can directly refer to the project's fdc file for porting.

The following pins can be assigned to LEDs to facilitate observing the experimental phenomena:

You can view the IP core's user guide in the following way to understand the Example module composition:

5.4 Experimental Phenomena
Download the program; you can see LED1 is steady-on, LED3 blinks, LED4 blinks, and LED5 is steady-on;
| Signal name | Reference description | LED number |
|---|---|---|
| err_flag_led | Data-check error signal | 3 |
| heart_beat_led | Heartbeat signal | 4 |
Reminder:
The heart_beat_led signal blinking indicates the ddrphy system clock is normal.

The err_flag_led signal blinking indicates no data-detection errors. This can be found in the IP core data manual.

If normal, err_flag_led blinks faster than heart_beat_led.
6 Optical-Fiber Communication Test Experiment Routine
6.1 Experiment Introduction
Experiment objective: Implement data transmit/receive between optical modules via an optical-fiber connection.
Experiment environment: Window11 PDS2022.2-SP6.4 Chip model: PG2L50H-484
6.2 Experiment Principle
PG2L100H has a built-in high-speed serial interface module with line rates up to 6.6 Gbps — i.e. HSSTLP — containing 1 HSSTLP, a total of 4 full-duplex transceiver LANEs. Besides the PMA, HSSTLP also integrates rich PCS features and can be flexibly applied to various serial-protocol standards. Inside the product, each HSST supports 1–4 full-duplex transceiver LANEs. The main features of HSST include:
- Supports DataRate: 0.6 Gbps–6.6 Gbps
- Flexible reference-clock selection
- Independent configuration of TX and RX data rates
- Programmable output swing and de-emphasis
- Receiver adaptive linear equalizer Logos2 series FPGA device data manual
- PMA Rx supports SSC
- Data path supports data widths: 8bit only, 10bit only, 8b10b, 16bit only, 20bit only, 32bit only, 40bit only, 64b66b/64b67b, etc.
- Flexibly configurable PCS, supporting PCI Express GEN1, PCI Express GEN2, XAUI, Gigabit Ethernet, CPRI, SRIO, and other protocols
- Flexible Word Alignment
- Supports RxClock Slip to ensure fixed Receive Latency
- Supports protocol-standard 8b10b encode/decode
- Supports protocol-standard 64b66b/64b67b data adaptation
- Flexible CTC scheme
- Supports x2 and x4 Channel Bonding
- HSSTLP configuration supports dynamic modification
- Near-end loopback and far-end loopback modes
- Built-in PRBS function
- Adaptation
6.3 Project Description
6.3.1 Install the HSST IP core
After installing PDS, you must manually add the HSST IP. Follow these steps:
(1) HSST IP file: select 1_9.iar
(2) IP installation steps: see "Tool Usage \ 03_IP Core Installation and Viewing the User Guide"

6.3.2 Optical-fiber communication test routine
Open the PDS software, create a new project hsst_test, click the icon below to open the IP Compiler;

Select the HSST IP, name it, then click Customize;

In the HSST settings interface, set Protocol and Rate as follows: Channel0, Channel1, and Channel3 are DISABLE; Channel2 is set to Fullduplex (full duplex). For Protocol choose CUSTOMERIZEDX1 (custom mode); for both TX Line Rate and RX Line Rate choose 6.25 Gbps; for Encoder choose 8B10B; Data Width choose 32 bit; for the clock choose Diff_REFck0, and select 125 MHz.

Set Alignment and CTC as follows: for Word Align Mode choose CUSTOMERIZED_MODE. For the control word (COMMA code-group select) choose K28.5; for CTC_MODE choose Bypassed.

Set Misc as follows: choose 25 MHz for the clock, keep the rest at defaults, then click Generate to generate the HSST IP;

Close this project and open the Example project at this path: hsst_test\hsst_test\ipcore\hsst_test\pnr\example_design

To run on the development board, you must modify the reset of the top-level file hsst_test_dut_top. See the routine's top-level file for details:

The figure above is the top-level file before modification.
The figure below is the top-level file after modification. tx_disable must be pulled low to enable the SFP transmit function.


The figure above shows part of the pin constraints; for specifics, see the project's fdc file. Note that the red boxes in the figure — the position constraints for hsst_lane and hsst_pll — must be added. The differential data pins do not need to be constrained, but the reference clock provided to hsst must be constrained — i.e. (i_p_refckn_0 and i_p_refckp_0).

To observe whether the transmit/receive data has errors, perform a Debugger insert-core operation.
Select o_p_clk2core_tx_2 as the clock.
You can view the IP core's user guide in the following way to understand the Example module composition:

6.4 Experimental Phenomena
Note: routine location: hsst_test\hsst_test\ipcore\hsst_test\pnr\example_design

Plug both ends of the optical fiber into the SFP ports (the user needs to purchase optical modules), then perform Debugger online debugging. You can see in the window that the sent and received data are identical.

Observe tx_data and rx0_data_align — they are identical, indicating there is no problem with transmit/receive.
7 Ethernet Transmission Experiment Routine
7.1 Experiment Introduction
Experiment objective: Complete the Ethernet communication test.
Experiment environment: Window11 PDS2022.2-SP6.4 Hardware environment: PG2L50H-484
7.2 Experiment Principle
7.2.1 Development Board Ethernet Interface Introduction
The development board uses a YT8521SH-CA Ethernet PHY to implement a 10/100/1000 Ethernet port. To use it, you need an optical-to-electrical conversion module; connect the computer's network port to the electrical port of the optical-to-electrical conversion module with a network cable to complete communication.
7.2.2 Ethernet Protocol Introduction
7.2.2.1 Ethernet Frame Format

Preamble: 8 bytes, 7 consecutive 8'h55 plus 1 8'hd5, indicating the start of a frame, used for synchronization between the two parties' devices.
Destination MAC address: 6 bytes, stores the physical address (MAC address) of the destination device; Source MAC address: 6 bytes, stores the physical address of the sending device.
Type: 2 bytes, used to specify the protocol type; commonly 0800 indicates the IP protocol, 0806 indicates the ARP protocol, and 8035 indicates the RARP protocol.
Data: 46 to 1500 bytes, minimum 46 bytes — if insufficient, it must be padded to 46 bytes. For example, the IP protocol layer is contained within the data portion, including its IP header and data.
FCS: frame trailer, 4 bytes, called the frame check sequence, using 32-bit CRC, which checks from the destination MAC address field to the data field.
Going further, taking the UDP protocol as an example, its structure is as follows. Besides the 14 bytes of the Ethernet header, the data portion contains the IP header, UDP header, and application data — totaling 46–1500 bytes.

7.2.2.2 ARP Datagram Format
ARP — the Address Resolution Protocol — obtains the physical address from the IP address. A host sends an ARP request broadcast (MAC address 48'hff_ff_ff_ff_ff_ff) containing the destination IP address to hosts on the network, and receives a reply message to determine the target's physical address. After receiving the reply, it saves the IP address and physical address into a cache for a period of time; next time, it directly queries the ARP cache to save resources. The following is the ARP datagram format.

Frame type: the ARP frame type is the two-byte 0806;
Hardware type: indicates the link-layer network type; 1 is Ethernet;
Protocol type: indicates the address type to be converted; using 0x0800 IP type, the subsequent hardware-address length and protocol-address length correspond to 6 and 4 respectively;
In the OP field, 1 indicates an ARP request and 2 indicates an ARP reply.
For example: |ff ff ff ff ff ff|00 0a 35 01 fe c0|08 06|00 01|08 00|06|04|00 01|00 0a 35 01 fe c0|c0 a8 00 02| ff ff ff ff ff ff|c0 a8 00 03|
Indicates an ARP request sent to the address 192.168.0.3.
|00 0a 35 01 fe c0 | 60 ab c1 a2 d5 15 |08 06|00 01|08 00|06|04|00 02| 60 ab c1 a2 d5 15|c0 a8 00 03|00 0a 35 01 fe c0|c0 a8 00 02|
Indicates an ARP reply sent to the address 192.168.0.2.
7.2.2.3 IP Packet Format
Because the UDP protocol packet is just one kind of IP packet, let's introduce the IP packet data format. The following figure shows the IP packet header format. The first 20 bytes of the header are fixed; the rest is variable.

Version: 4 bits, indicates the IP protocol version; the current IP protocol version number is 4 (i.e. IPv4);
Header length: 4 bits; the maximum representable value is 15 units (one unit is 4 bytes), so the maximum IP header length is 60 bytes;
Differentiated services: 8 bits, used to obtain better service. In the old standard this was called the type of service, but in practice it was never used. In 1998 this field was renamed Differentiated Services. It only takes effect when Differentiated Services (DiffServ) is in use; in general this field is not used;
Total length: 16 bits, indicates the length of the header plus data in bytes, so the maximum datagram length is 65535 bytes. The total length must not exceed the maximum transmission unit (MTU);
Identification: 16 bits; it is a counter used to generate datagram identification;
Flag: 3 bits; currently only the first two bits are meaningful.
The lowest bit of the flag field is MF (More Fragment): MF=1 means there are "more fragments" afterward; MF=0 means the last fragment.
The middle bit of the flag field is DF (Don't Fragment): fragmentation is allowed only when DF=0.
Fragment offset: 12 bits, indicates the relative position of a fragment in the original group after a longer group has been fragmented. The fragment offset is in units of 8 bytes;
Time to live: 8 bits, denoted TTL (Time To Live), the maximum number of routers a datagram can pass through in the network. The TTL field is an 8-bit field initially set by the sender. The recommended initial value is specified by the Assigned Numbers RFC; the current value is 64. The TTL is often set to the maximum value 255 when sending ICMP echo replies;
Protocol: 8 bits, indicates which protocol the data carried by this datagram uses, so that the destination host's IP layer knows which process to deliver the data portion to. 1 indicates the ICMP protocol, 2 indicates the IGMP protocol, 6 indicates the TCP protocol, and 17 indicates the UDP protocol;
Header checksum: 16 bits, only checks the datagram header, not the data portion. It uses binary one's-complement addition — i.e. the 16-bit data are added, then the carry is added to the low 16 bits, repeating until the carry is 0, and finally the 16 bits are inverted;
Source address and destination address: each 4 bytes, recording the source and destination addresses respectively.
7.2.2.4 UDP Protocol
UDP is the abbreviation of User Datagram Protocol. UDP provides only a basic, low-latency communication called a datagram. A datagram is a self-addressing packet that travels from the sender to the receiver. The UDP protocol is often used in scenarios with high data-transmission-speed requirements, such as image transmission and network-monitoring data exchange.
UDP header format:
The UDP header consists of 4 fields, each occupying 2 bytes, as follows:

① UDP source port number
② Destination port number
③ Datagram length
④ Checksum
The UDP protocol uses port numbers to reserve separate data-transmission channels for different applications. The sending side sends the UDP datagram out through the source port, while the receiving side receives data through the destination port.
The datagram length is the total number of bytes including the header and data portion. Because the header length is fixed, this field is mainly used to compute the variable-length data portion (also called the data payload). The maximum datagram length varies depending on the operating environment. Theoretically, the maximum datagram length including the header is 65535 bytes. However, some practical applications often limit the datagram size, sometimes down to 8192 bytes.
The UDP protocol uses the checksum in the header to ensure data safety. The checksum is first computed at the sender by a special algorithm, and is recomputed at the receiver after delivery. If a datagram is tampered with by a third party during transmission or damaged due to line noise, etc., the sender's and receiver's checksum computations will not match, so the UDP protocol can detect errors. Although UDP provides error detection, when an error is detected there is no error correction — it simply discards the damaged message segment or provides warning information to the application.
7.2.2.5 Ping Function
The UDP protocol uses the checksum in the header to ensure data safety. The checksum is first computed at the sender by a special algorithm, and is recomputed at the receiver after delivery. If a datagram is tampered with by a third party during transmission or damaged due to line noise, etc., the sender's and receiver's checksum computations will not match, so the UDP protocol can detect errors. Although UDP provides error detection, when an error is detected there is no error correction — it simply discards the damaged message segment or provides warning information to the application.


7.3 SMI (MDC/MDIO) Bus Interface
The Serial Management Interface, also called the MII Management Interface, consists of two signal lines: MDC and MDIO. MDIO is a management interface for the PHY, used to read/write the PHY's registers to control the PHY's behavior or obtain the PHY's status. MDC provides the clock for MDIO and is provided by the MAC end — in this experiment, the FPGA end. In the RTL8211EG documentation, you can see the MDC period is at least 400 ns, i.e. a maximum clock of 2.5 MHz.

7.3.1 SMI Frame Format
The following shows the SMI read/write frame format:

| Name | Description |
|---|---|
| Preamble | Sent by the MAC as 32 consecutive logical "1"s, synchronized to the MDC signal; used for synchronization between MAC and PHY; |
| ST | Frame start bit, fixed at 01 |
| OP | Opcode: 10 means read, 01 means write |
| PHYAD | PHY address, 5 bits |
| REGAD | Register address, 5 bits |
| TA | Turn Around, MDIO direction turnaround. In the write state, no direction turnaround is needed and the value is 10. In the read state, the MAC output is high-impedance and in the second cycle the PHY pulls MDIO low |
| DATA | 16-bit data |
| IDLE | Idle state; MDIO is high-impedance in this state, pulled high by an external pull-up resistor |
7.3.2 Read Timing

You can see that in the Turn Around state, MDIO is high-impedance in the first cycle and pulled low by the PHY in the second cycle.
7.3.3 Write Timing

To ensure data is correctly sampled, data is prepared before the MDC rising edge; in this experiment, data is sent on the falling edge and received on the rising edge.
7.4 Experiment Design
This experiment designs a Verilog program using Gigabit Ethernet RGMII communication as an example. It first sends preset UDP data to the network, sending once per second. The program is split into two parts — transmit and receive — and implements ARP and UDP functions.
7.4.1 Transmit Part
7.4.1.1 MAC Layer Transmit
In the transmit part, mac_tx.v is the MAC-layer transmit module. First, in the SEND_START state, it waits for the mac_tx_ready signal; if valid, it means the IP or ARP data is ready and transmission can begin. Then it enters the send-preamble state; when finished it sends mac_data_req to request IP or ARP data, then enters the send-data state, and finally enters the send-CRC state. During data transmission, CRC computation is performed simultaneously. After the preamble is completed, the upper-layer protocol data is sent out; at this time these upper-layer data are also fed into the CRC32 module for sequence generation. The upper-layer protocol gives a data-output-complete flag signal — at this point mac_tx knows data transmission is complete and needs to end the CRC32 sequence generation. Now it extracts the FCS and sends it out after the data, connecting: preamble --- data (MAC frame) ---- FCS. Then it jumps to the end state, and returns to the IDLE state to wait for the next send request.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| mac_frame_data | input | 8 | Data from IP or ARP |
| mac_tx_req | input | 1 | MAC transmit request |
| mac_tx_ready | input | 1 | IP or ARP data ready |
| mac_tx_end | input | 1 | IP or ARP data transfer complete |
| mac_tx_data | output | 8 | Send data to PHY |
| mac_send_end | output | 1 | MAC data send end |
| mac_data_valid | output | 1 | MAC data-valid signal, i.e. gmii_tx_en |
| mac_data_req | output | 1 | MAC layer requests data from IP or ARP |
| mac_tx_ack | output | 1 | MAC layer transmit ack for UPPER request |
7.4.1.2 MAC Transmit Mode
In the project, mac_tx_mode.v selects the transmit mode — selecting the corresponding signals and data according to whether the mode is IP or ARP.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| mac_send_end | input | 1 | MAC transmit end |
| arp_tx_req | input | 1 | ARP transmit request |
| arp_tx_ready | input | 1 | ARP data ready |
| arp_tx_data | input | 8 | ARP data |
| arp_tx_end | input | 1 | ARP data sending to MAC layer ended |
| arp_tx_ack | input | 1 | ARP transmit response signal |
| ip_tx_req | input | 1 | IP transmit request |
| ip_tx_ready | input | 1 | IP data ready |
| ip_tx_data | input | 8 | IP data |
| ip_tx_end | input | 1 | IP data sending to MAC layer ended |
| mac_tx_ready | output | 1 | MAC data-ready signal |
| ip_tx_ack | output | 1 | IP transmit response signal |
| mac_tx_ack | output | 1 | MAC transmit response signal |
| mac_tx_req | output | 1 | MAC transmit request |
| mac_tx_data | output | 8 | MAC transmit data |
| mac_tx_end | output | 1 | MAC data send end |
7.4.1.3 ARP Transmit
In the transmit part, arp_tx.v is the ARP transmit module. In the IDLE state it waits for an ARP send request or ARP reply-request signal; then it enters the request or reply wait state, notifies the MAC layer that data is ready, waits for the mac_data_req signal, and then enters the request or reply data-send state. Because data is less than 46 bytes, it must be padded to 46 bytes before sending.

| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| dest_mac_addr | input | 48 | Destination MAC address to send |
| sour_mac_addr | input | 48 | Source MAC address to send |
| sour_ip_addr | input | 32 | Source IP address to send |
| dest_ip_addr | input | 32 | Destination IP address to send |
| mac_data_req | input | 1 | MAC layer data-request signal |
| arp_request_req | input | 1 | ARP request signal |
| arp_reply_ack | output | 1 | ARP reply acknowledgment signal |
| arp_reply_req | input | 1 | ARP reply request signal |
| arp_rec_sour_ip_addr | input | 32 | ARP received source IP address; placed into destination IP address on reply |
| arp_rec_sour_mac_addr | input | 48 | ARP received source MAC address; placed into destination MAC address on reply |
| mac_send_end | input | 1 | MAC transmit end |
| mac_tx_ack | input | 1 | MAC transmit ack |
| arp_tx_ready | output | 1 | ARP data ready |
| arp_tx_data | output | 8 | ARP transmit data |
| arp_tx_end | output | 1 | ARP data send end |
| arp_tx_req | output | 1 | ARP transmit request signal |
7.4.1.4 IP Layer Transmit
In the transmit part, ip_tx.v is the IP-layer transmit module. In the IDLE state, if ip_tx_req is valid — i.e. a UDP or ICMP send-request signal — it enters the wait-for-send-data-length state, then enters the generate-checksum state. The checksum adds all IP-header data as 16-bit values; the carry is then added to the low 16 bits, repeated until the carry is 0, and then the low 16 bits are inverted to obtain the checksum result.
After generating the checksum, it waits for the MAC layer's data request, starts sending data, and requests UDP or ICMP data just before finishing sending the IP header. When sending is complete, it returns to the IDLE state.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| dest_mac_addr | input | 48 | Destination MAC address to send |
| sour_mac_addr | input | 48 | Source MAC address to send |
| sour_ip_addr | input | 32 | Source IP address to send |
| dest_ip_addr | input | 32 | Destination IP address to send |
| ttl | input | 8 | Time to live |
| ip_send_type | input | 8 | Upper-layer protocol number, e.g. UDP, ICMPP |
| upper_layer_data | output | 8 | Data from UDP or ICMP |
| upper_data_req | input | 1 | Request data from upper layer |
| mac_tx_ack | input | 1 | MAC transmit ack |
| mac_send_end | input | 1 | MAC transmit-end signal |
| mac_data_req | input | 1 | MAC layer data-request signal |
| upper_tx_ready | input | 1 | Upper-layer UDP or ICMP data ready |
| ip_tx_req | input | 1 | Transmit request, from the upper layer |
| ip_send_data_length | input | 16 | Total length of transmitted data |
| ip_tx_ack | output | Generate IP transmit ack | |
| ip_tx_ready | output | 1 | IP data ready |
| ip_tx_data | output | 8 | IP data |
| ip_tx_end | output | 1 | IP data sending to MAC layer ended |
7.4.1.5 IP Transmit Mode
In the project, ip_tx_mode.v selects the transmit mode — selecting the corresponding signals and data according to whether the mode is UDP or ICMP.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| mac_send_end | input | MAC data send end | |
| udp_tx_req | input | 1 | UDP transmit request |
| udp_tx_ready | input | 1 | UDP data ready |
| udp_tx_data | input | 8 | UDP transmit data |
| udp_send_data_length | input | 16 | UDP transmit data length |
| udp_tx_ack | output | 1 | Output UDP transmit ack |
| icmp_tx_req | input | 1 | ICMP transmit request |
| icmp_tx_ready | input | 1 | ICMP data ready |
| icmp_tx_data | input | 8 | ICMP transmit data |
| icmp_send_data_length | input | 16 | ICMP transmit data length |
| icmp_tx_ack | output | 1 | ICMP transmit ack |
| ip_tx_ack | input | 1 | IP transmit ack |
| ip_tx_req | input | 1 | IP transmit request |
| ip_tx_ready | output | 1 | IP data ready |
| ip_tx_data | output | 8 | IP data |
| ip_send_type | output | 8 | Upper-layer protocol number, e.g. UDP, ICMP |
| ip_send_data_length | output | 16 | Total length of transmitted data |
7.4.1.6 UDP Transmit
In the transmit part, udp_tx.v is the UDP transmit module.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| app_data_in_valid | input | 1 | Data-output valid signal received from external |
| app_data_in | input | 8 | Data received from external |
| app_data_length | input | 16 | Length of the current packet received from external (excluding udp, ip, mac headers) |
| udp_dest_port | input | 16 | Source port number of the packet received from external |
| app_data_request | input | 1 | User-interface data-send request |
| udp_send_ready | output | 1 | UDP data send ready |
| udp_send_ack | output | 1 | UDP data send ack |
| ip_send_ready | input | 1 | IP data send ready |
| ip_send_ack | input | 1 | IP data send ack |
| udp_send_request | output | 1 | User-interface data-send request |
| udp_data_out_valid | output | 1 | Transmitted data-output valid signal |
| udp_data_out | output | 8 | Transmitted data output |
| udp_packet_length | output | 16 | Length of the current packet (excluding udp, ip, mac headers) |
7.4.2 Receive Part
7.4.2.1 MAC Layer Receive
In the receive part, mac_rx.v is the MAC-layer receive file. First, in the IDLE state, when rx_en goes high it enters the REC_PREAMBLE preamble state to receive the preamble. Then it enters the receive-MAC-header state — i.e. destination MAC address, source MAC address, and type — caches them, and in this state judges whether the preamble is correct; if wrong it enters the REC_ERROR error state. In the REC_IDENTIFY state it judges whether the type is IP (8'h0800) or ARP (8'h0806). It then enters the receive-data state, passes the data to the IP or ARP module, waits for IP or ARP data reception to complete, and then receives the CRC data. During data reception it also performs CRC processing on the received data and compares the result with the received CRC data to judge whether the data was received correctly — if correct it ends, otherwise it enters the ERROR state.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| rx_en | input | 1 | Start-receive enable |
| mac_rx_datain | input | 8 | Received data |
| checksum_err | input | 1 | IP-layer checksum error signal |
| ip_rx_end | input | 1 | IP receive end |
| arp_rx_end | input | 1 | ARP receive end |
| ip_rx_req | output | 1 | IP receive request |
| arp_rx_req | input | 1 | Request ARP receive |
| mac_rx_dataout | output | 8 | MAC layer receive data output to IP or ARP |
| mac_rec_error | output | 1 | MAC layer receive error |
| mac_rx_dest_mac_addr | output | 48 | MAC received destination IP address |
| mac_rx_sour_mac_addr | output | 48 | MAC received source IP address |
7.4.2.2 ARP Receive
In the project, arp_rx.v is the ARP receive module, implementing ARP data reception. In the IDLE state, upon receiving the arp_rx_req signal from the MAC layer, it enters the ARP receive state. In this state it extracts the destination MAC address, source MAC address, destination IP address, and source IP address, and judges whether the OP code is a request or a reply. If it is a request, it judges whether the received destination IP address is the local address; if so, it sends the reply-request signal arp_reply_req; if not, it ignores it. If OP is a reply, it judges whether the received destination IP address and destination MAC address match the local ones; if so, it pulls the arp_found signal high to indicate that the other party's address has been received. It then stores the other party's MAC address and IP address into the ARP cache.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| local_ip_addr | input | 32 | Local IP address |
| local_mac_addr | input | 48 | Local MAC address |
| arp_rx_data | input | 8 | ARP receive data |
| arp_rx_req | input | 1 | ARP receive request |
| arp_rx_end | output | 1 | ARP receive complete |
| arp_reply_ack | input | 1 | ARP reply acknowledgment |
| arp_reply_req | output | 1 | ARP reply request |
| arp_rec_sour_ip_addr | input | 32 | ARP received source IP address |
| arp_rec_sour_mac_addr | input | 48 | ARP received source MAC address |
| arp_found | output | 1 | ARP request/reply received correctly |
7.4.2.3 IP Layer Receive Module
In the project, ip_rx is the IP-layer receive module, implementing IP-layer data reception, information extraction, and checksum verification. First, in the IDLE state, it checks the ip_rx_req signal from the MAC layer and enters the receive-IP-header state. In REC_HEADER0 it first extracts the header length and total IP length; in REC_HEADER1 it extracts the destination IP address, source IP address, and protocol type, and based on the protocol type sends udp_rx_req or icmp_rx_req. While receiving the header it also performs checksum verification: it adds all the received header data into a 32-bit register, then adds the high 16 bits to the low 16 bits repeatedly until the high 16 bits are 0, then inverts the low 16 bits and judges whether it is 0; if 0, the check is correct, otherwise wrong — it enters the IDLE state, discards this frame, and waits for the next reception.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| local_ip_addr | input | 32 | Local IP address |
| local_mac_addr | input | 48 | Local MAC address |
| ip_rx_data | input | 8 | Data received from the MAC layer |
| ip_rx_req | input | 1 | IP receive request signal sent by MAC layer |
| mac_rx_dest_mac_addr | input | 48 | MAC layer received destination MAC address |
| udp_rx_req | output | 1 | UDP receive request signal |
| icmp_rx_req | output | 1 | ICMP receive request signal |
| ip_addr_check_error | output | 1 | Address-check error signal |
| upper_layer_data_length | output | 16 | Upper-layer protocol data length |
| ip_total_data_length | output | 16 | Total data length |
| net_protocol | output | 8 | Network protocol number |
| ip_rec_source_addr | output | 32 | IP-layer received source IP address |
| ip_rec_dest_addr | output | 32 | IP-layer received destination IP address |
| ip_rx_end | output | 1 | IP-layer receive end |
| ip_checksum_error | output | 1 | IP-layer checksum-check error signal |
7.4.2.4 UDP Receive
In the project, udp_rx.v is the UDP receive module. This module first receives the UDP header, then the data portion, and performs UDP checksum verification during reception. If the UDP data is an odd number of bytes, when computing the checksum it appends 8'h00 after the last byte and computes the checksum. The verification method is the same as the IP checksum. If verification passes, the udp_rec_data_valid signal is pulled high to indicate that the received UDP data is valid; otherwise it is invalid, and it waits for the next reception.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| udp_rx_data | input | 8 | UDP receive data |
| udp_rx_req | input | 1 | UDP receive request |
| ip_checksum_error | input | 1 | IP-layer checksum-check error signal |
| ip_addr_check_error | input | 1 | Address-check error signal |
| udp_rec_rdata | output | 8 | UDP receive read data |
| udp_rec_data_length | output | 16 | UDP receive data length |
| udp_rec_data_valid | output | 1 | UDP receive data valid |
7.4.3 Other Parts
7.4.3.1 ICMP Reply
In the project, icmp_reply.v implements the ping function. It first receives the ICMP data sent from another device, judges whether the type is an Echo Request; if so, stores the data into RAM, computes the checksum, and judges whether the checksum is correct; if correct, it enters the send state and sends the data out.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| mac_send_end | input | 1 | MAC transmit-end signal |
| ip_tx_ack | input | 1 | IP transmit ack |
| icmp_rx_data | input | 8 | ICMP receive data |
| icmp_rx_req | input | 1 | ICMP receive request |
| icmp_rev_error | input | 1 | Receive error signal |
| upper_layer_data_length | input | 16 | Upper-layer protocol length |
| icmp_data_req | input | 1 | Request ICMP data |
| icmp_tx_ready | output | 1 | ICMP transmit ready |
| icmp_tx_data | output | 8 | ICMP transmit data |
| icmp_tx_end | output | 1 | ICMP transmit end |
| icmp_tx_req | output | 1 | ICMP transmit request |
7.4.3.2 ARP Cache
In the project, arp_cache.v is the ARP cache module. It caches the IP and MAC addresses of other devices that have been received. Before sending data, it queries whether the destination address exists; if not, it sends an ARP request to the destination address and waits for a reply. In the design file, only one cache slot is implemented; it can be extended if needed.
| Signal name | Direction | Width | Description |
|---|---|---|---|
| clk | input | 1 | System clock |
| rstn | input | 1 | Active-low reset |
| arp_found | input | 1 | ARP reply received correctly |
| arp_rec_source_ip_addr | input | 32 | ARP received source IP address |
| arp_rec_source_mac_addr | input | 48 | ARP received source MAC address |
| dest_ip_addr | input | 32 | Destination IP address |
| dest_mac_addr | output | 48 | Destination MAC address |
| mac_not_exist | output | 1 | No MAC address exists for the destination address |
7.4.3.3 CRC Verification Module (crc.v)
CRC32 verification is computed starting from the destination MAC address up to the last data of a packet. Some websites can auto-generate Verilog files for the CRC algorithm: https://bues.ch/cms/hacking/crcgen.html

7.5 Experimental Phenomena
Plug an optical-to-electrical conversion module into the SFP port, then connect it to the PC's network port with a network cable;
Set the receiver (PC) IP address to 192.168.0.3 and the development board's IP address to 192.168.0.2, as shown below:

Through the command prompt, enter arp -a; you can find IP: 192.168.0.2, MAC: a0_b1_c2_d3_e1_e1;

Verify via Wireshark packet capture that the data link is properly connected and data transmission is normal. Open Wireshark on the PC from the materials package; after re-flashing and capturing packets, you can see the interaction process shown below.
After the connection is successfully established, the data packet "www.meyesemi.com" is sent continuously, as shown below:


Ping function test — as shown above, ping basically does not drop packets.
8 PCIe-based DMA/PIO Control Experiment
8.1 Function and Performance
The PCIe DMA implements the business function of memory read/write requests and converts the AXI4-Stream interface into a RAM read/write interface for user convenience. On the CPU side, users can read/write the RAM inside the FPGA via both PIO and DMA;
The DMA application scenario is shown in Figure 1;

The PCIe DMA routine supports the following main functions;
- Supports Gen1x1, Gen1x2, Gen1x4, Gen2x1, Gen2x2, Gen2x4;
- Supports memory read operations (Mrd);
- Supports memory write operations (Mwr);
- Supports Max Payload Size of 128 Byte, 256 Byte, 512 Byte, 1024 Byte data transfer;
- Converts the AXI4-Stream interface into a RAM read/write interface.
8.2 Principle and Implementation
8.2.1 RTL Implementation of the DMA

The PCIe DMA mainly consists of uart2apb and pcie_dma. The main function of uart2apb is: to bring in external control to configure the working mode of PCIe; by default it is in EP mode controlled by the RC for DMA and PIO. The main function of the pcie_dma module is: to receive the TLP packet data output by the PCIe hard IP, and to assemble the data to be sent to the RC into TLP packets and send them to the PCIe hard IP; internally, the data written to the FPGA via PIO and DMA is stored in different BRAMs.
8.2.2 PCIe DMA Module

The DMA operation flow is shown below:


The AXI4-Stream Master interface timing for PCIe data interaction is shown below; the first cycle of axis_master_tdata is the TLP Header, and axis_master_tkeep is all 1s in the TLP Header field;




The AXI4-Stream slave interface timing for PCIe data interaction is shown above; once the axis_slave bus starts sending data, axis_slave0/1/2_tvalid must stay high until the last data transfer is complete (axis_slave0/1/2_tlast high pulse) before it can be pulled low.




8.2.2.1 dma_rx_top module
This module's main function is to parse the TLP packets from PCIe and save the data (PIO/DMA) written by the RC to the EP into BRAM for later use;

- dma_tlp_rcv module
This module's main function is to receive and parse the TLP protocol packets from the AXIS_Master interface, outputting the parsed payload data and the channel-identification information to the subsequent mwr_wr_ctrl, cpld_wr_ctrl, and dma_ctrl modules; it completes the first functional stage of PCIe interaction;
- mwr_wr_ctrl module
This module's main function is to pre-process the parsed mwr-type (mwr, IOwr) packet data, output the signals corresponding to the BRAM write interface, and then write them into Bar0_BRAM; for that RC system, this module is the PIO write-conversion module;
- cpld_wr_ctrl module
After the RC receives the Mrd packet sent by the EP, the DMA bus on the RC reads data from storage, assembles it into a Cpld packet, and sends it down to the EP. This module's main function is to pre-process the parsed Cpld packet data, output the signals corresponding to the BRAM write interface, and write them into Bar1_BRAM; for that RC system, this module is the DMA_RD write-conversion module;
- BRAM module
Both BRAM modules call the FPGA's Block RAM. During testing, the RC must first perform a write operation to the RAM; only after that can the read-up data correspond one-to-one;
8.2.2.2 dma_tx_top module
This module's function is to send data from the FPGA to the PCIe interface; there are 3 AXIS interfaces in total. In this test program, the 3 axes each send different types of packets; AXIS_slave0 connects the Cpld packets fed back by the FPGA, acknowledging the Mrd/IOrd packets issued by the RC; AXIS_slave1 connects the Mrd packets actively initiated by the FPGA, where the FPGA fetches RC DDR data; AXIS_slave2 connects the Mwr packets actively initiated by the FPGA, where the FPGA writes data to the RC DDR;

- cpld_tx_ctrl module
Triggered by the Cpld-related control signals parsed by the RX_TOP module, this module starts assembling Cpld packets, sends a fetch request to cpld_tx_rd_ctrl, and after receiving the data assembles a Cpld TLP packet and outputs it to the PCIe hard core;
- cpld_tx_rd_ctrl module
After receiving the fetch request initiated by cpld_tx_ctrl, this module converts it into BRAM read-interface signals, sends a read request to the dma_rx_top module, and after receiving the data transfers it to cpld_tx_ctrl;
- mwr_tx_ctrl module
After receiving the DMA_CTRL module's mwr request, data length, and the RC's corresponding address, this module starts preparing to send an Mwr packet, sends a fetch request to mwr_tx_rd_ctrl, and after receiving the data assembles an mwr TLP packet and outputs it to the PCIe hard core;
- mwr_tx_rd_ctrl module
After receiving the fetch request initiated by mwr_tx_ctrl, this module converts it into BRAM read-interface signals, sends a read request to the dma_rx_top module, and after receiving the data transfers it to mwr_tx_ctrl;
- mrd_tx_ctrl module
After receiving the DMA_CTRL module's mrd request, data length, and the RC's corresponding address, this module starts preparing to send an Mrd packet, assembles an mwr TLP packet, and outputs it to the PCIe hard core;
8.2.2.3 dma_ctrl module
The RC issues the relevant control signals through the Bar1 register space to control the FPGA for the corresponding DMA channel control; the corresponding control registers and parsing are as follows;
| Register address | Function name | Description | Parsing |
|---|---|---|---|
| Bar1_base_addr + 0x100 | Dma_cmd_reg | DMA command register | [9:0] : DMA transfer length [16] : DMA address length (0: 32bit, 1: 64bit) [24] : DMA packet type (0: Mrd, 1: Mwr) |
| Bar1_base_addr + 0x110 | Dma_cmd_l_addr | DMA transfer address (low) | [1:0] : reserved [31:0] : dma_addr[31:2] |
| Bar1_base_addr + 0x120 | Dma_cmd_h_addr | DMA transfer address (high) | High 32 bits of the MEM access address |
8.3 Project Introduction
- Simulation reference design The reference-design simulation block diagram is shown in the figure.

Simulation environment: third-party simulation software. Run script: sim.bat Simulation filelist: pango_pcie_top_filelist.f. The simulation waveform is shown below.

- On-board testing
Before the docking test, confirm the following operations are correct:
- Ensure the driver is installed successfully.
- Confirm the FPGA firmware has been flashed to Flash;
- Ensure the design-project fdc constraints are correct. The reference-design fdc constraint file already provides Gen1x1 mode and Gen2x2 mode.
