Something I’ve been looking at recently is the use of FPGA’s or Field Programable Gate Arrays. These devices can be programmed to layout custom logic gates allowing the virtual design of custom CPU’s or digital hardware such as ethernet controllers. Another use is the ability to create virtual hardware that’s laid out in such a way to re-create old retro hardware such as Spectrum’s or Comodore 64’s
For retro hardware the main project for this is the Mister project,
however that’s deisgned for use on the DE10 board, in my case I’m using the Xilinx Art-A7 100T
My end goal is to try out amaranth as a python based HDL (Hardware Description Language) which transpiles down to Verilog.
But as a first step before that is to get a very basic Xilinx project working and see if I can remotely program a board connected to an RPI
I’ve placed a download here for the example below
Setting up a Demo Vivado Project
First lets setup a very basic Xilinx based project to blink an LED
Setting up a new Project
- Launch the Xilinx Vivado IDE
- File -> Project -> New
- Project Name: xilinx-example1
- Directory: Choose a directory to place the project into
- Select RTL Project
- Tick the box for “Do not specify Sources at this time”
- On the next screen select the Boards Tab
- Click Refresh, this will download the latest list of available boards
- Filter the list to the 100t board
- Click the Download button
We should now have an empty project with no sources targeted at the Arty A7-100T board
Adding System Verilog Source
Next we’re going to add a basic design to blink an LED within System Verilog
- Select Add Source
- Select the option for Add or Create design sources
- Select Create File
- Select System Verilog
- Give it a filename of
led_blink1
This should automatically add an extension of .sv - You might also have to right click the file and “Set as Top”
- The new file will now show up under Design Sources
- Open up the file then paste in the below code
- Then click Save
module led_blinker (
input logic clk, // 100 MHz clock input
output logic led = 0 // LED output
);
logic [26:0] counter = 0;
// Counter process: counts the clock cycles
always_ff @(posedge clk) begin
counter <= counter +1'b1;
// Set to 10 for simulation, 100_000_000 for live
if (counter == 10) begin
counter <= '0; // Reset the counter after reaching MAX_COUNT
led <= ~led; // Toggle the LED every 1 second
end
end
endmodule
We’ve set the counter limit above to 10 to get a better view during the simulation.
When flashing the actual board we’ll change this to 1000000 for a longer 1 second delay.
We should now have some System Verilog in for flashing an LED.
If using git you may want to add in a .gitignore file to exclude the cache directory.
Setting up a Simulation File
Next we’re going to add something to simulate the design
- Select Add Source
- Select the option for Add or Create simulation sources
- Select Create File
- Select System Verilog
- Give it a filename of
tb_led_blink
for the test bench
This should automatically add an extension of .sv
- The new file will now show under Simulation Sources
- Open up the file then paste in the below code
- Then click Save
`timescale 1ns/1ps
module tb_led_blinker();
// Clock signal for the DUT
logic clk;
// LED output from the DUT
logic led;
// Instantiate the DUT (Device Under Test)
led_blinker uut (
.clk(clk),
.led(led)
);
// Clock generation: toggles the clock every half period (5ns for 100 MHz)
always begin
#5 clk = ~clk; // Toggle the clock every half period
end
// Testbench procedure
initial begin
// Initialize signals
clk = 0;
$monitor("At Time: %0dns, LED: %b", $time, led); // Monitor LED State
#1000005 $finish;
end
endmodule
Running the Simulation
We can now run the simulation
Click on the zoom out button and full width button.
This should give us a better view of the logic state of the lines.
- The top trace represents the 100Mhz Clock
- The bottom trace represents the output to the LED toggling on and off
Simulation Time
To give a bit of background on the different units of time in use.
The default time length for a simulation is 1000ns (nano seconds).
For those not familiar with engineering scales it tends to work in groups of 3 so
- 1 ms (milli second) = 0.001 seconds
- 1 us (micro second) = 0.000,001 seconds
- 1 ns (nano second) = 0.000,000,001 seconds
- 1 ps (pico second) = 0.000,000,000,001 seconds
So for 1000ns this is 1 micro second or 0.000,001 seconds. The default clock is 100Mhz or million cycles per second.
If we want to calculate how quickly the clock will change 1 / 100,000,000 = 10 ns for each clock cycle.
The simulation length can be chantged under Tools -> Settings
Setting up a Constraints File
Next we need to map pins from within the design to IO pins on the board.
The way we do this is by using something called a constraints file, these files tend to have an xdc extension.
The best way to imagine this is a file that maps external IO pins from the FPGA device to variables within the Verilog design.
There’s a list of different files here for different boards
- https://github.com/Digilent/digilent-xdc/tree/master
- For this example we’ll be using the one for the Art A7-100T board
https://github.com/Digilent/digilent-xdc/blob/master/Arty-A7-100-Master.xdc
To create the constraints file
- Select Add Source
- Select the option for Add or Create constraints
- Select Create File the same as before
- Give it a file name of something like
design
The xdc extension will be added automatically - Open up the constraints file and paste in the contents from Arty-A7-100-Master.xdc
Most of this will be commented out already, normally we could just uncomment out some of the lines we need.
For this example just copy the following in at the top.
This will import the clock as the clk
variable and set the output pin for the led
.
set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { clk }]; #IO_L12P_T1_MRCC_35 Sch=gclk[100]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports { clk }];
set_property -dict { PACKAGE_PIN H5 IOSTANDARD LVCMOS33 } [get_ports { led }]; #IO_L24N_T3_35 Sch=led[4]
Next up the counter value from 10 to 1,000,000 within led_blinker.sv
// Set to 10 for simulation, 100_000_000 for live
if (counter == 100_000_000) begin
The final step before we actually program the real board is to click “Generate Bitstream”
Remotely Connecting to the FPGA via an Rpi
For the next step I’d like to be able to remotely program a board using a Rpi.
In this case I’m using an Rpi4 with a fanless heatsink and with the latest version of ubuntu installed.
Typically with xilinx tools they have something called the hw_server which can be installed onto a remote machine.
However this doesn’t currently work with the AARCH64 / ARM platofrm.
To get around this we can use something called the openFPGALoader this has an option to connect to a board and act as a xvc server.
This won’t be as fast as the hw_server since that uses a newer protocol but the xvc protocol is well documented and useable from the Xilinx tools remotely.
- https://github.com/trabucayre/openFPGALoader
- https://trabucayre.github.io/openFPGALoader/compatibility/board.html#compatibility-boards
Installing the tools
First we need to install openfpgaloader
sudo apt-install openfpgaloader
Running the XVC Server
Next if we plug in the board is should show up as a couple of usb serial ports ttyUSB0 and ttyUSB1.
One of these is the port we can use to program the board, the second is a uart that can be wired into the fpga directly for testing.
To run openFPGALoader in xvc server mode
openFPGALoader -b arty_a7_100t --xvc
This is now showing us that we need to connect to port 3721 for the IP address of the box
Connecting to the XVC Server
From the Vivado tools we can now connect to the xvc server and upload a bitstream
Select Open Hardware Manager
Select Auto Connect.
At this stage the xilinx will connect to the hw_server on the local machine.
Right click on localhost and select Add Xilinx Virtual Cable.
Enter in the IP address of the Rpi and change the port to 3721.
The same as shown before
This should now show a connection to the board.
We can now select Program Device
and upload the bitstream to the board remotely.
Other Commands
Some other commands of interest
# If we want to reset the board after it's been programmed
openFPGALoader -b arty_a7_100t -r
# Load the bitstream in via a file
openFPGALoader -b arty_a7_100t bitstream.bit