December 19, 2024

Vivado and Tcl - a custom AXI peripheral

The idea here is to do something that is sort of like a combination of two things that have already been done. We have used an AXI_GPIO block that gives us access from the PS to a set of registers that manipulate signals in the PL. We have used a HDL block which is a wrapper around some verilog we want to use.

In its barest state, a custom AXI peripheral is a group of registers that can be accessed from the PS. Unlike the AXI_GPIO, these registers don't conform to any protocol and need to be customized. This is done by editing the verilog templates that vivado generates and perhaps adding additional verilog files.

Begin using the GUI

This sort of feels like cheating (in lieu of diving into writing Tcl), but this is new territory and it will save time to let the GUI show us what Tcl is needed.

We launch vivado and let the new project wizard guide us. We need to remember our part number as we are eschewing specifying the board. It is xc7z010-1clg400.
Nothing special here and the Tcl is:

create_project custom1 /u1/home/tom/vivado/custom1 -part xc7z010clg400-1
set_property  ip_repo_paths  /u1/home/tom/vivado/ip_repo [current_project]
update_ip_catalog
Now we use Tools on the top menu and select "create and package new IP". We want a new AXI4 peripheral. I call it "fancyled". I will use AXI4 Lite and I want 8 registers, not 4. I tell it to "add IP to the repository".
The Tcl for this is:
create_peripheral user.org user fancyled 1.0 -dir /u1/home/tom/vivado/ip_repo
add_peripheral_interface S00_AXI -interface_mode slave -axi_type lite [ipx::find_open_core user.org:user:fancyled:1.0]
set_property VALUE 8 [ipx::get_bus_parameters WIZ_NUM_REG -of_objects [ipx::get_bus_interfaces S00_AXI -of_objects [ipx::find_open_core user.org:user:fancyled:1.0]]]
generate_peripheral -driver -bfm_example_design -debug_hw_example_design [ipx::find_open_core user.org:user:fancyled:1.0]
write_peripheral [ipx::find_open_core user.org:user:fancyled:1.0]
set_property  ip_repo_paths  {/u1/home/tom/vivado/ip_repo/fancyled_1_0 /u1/home/tom/vivado/ip_repo} [current_project]
update_ip_catalog -rebuild
Now lets create a block design and add this to it. We go with the default "design_1" for the name. I use the big "+" to add IP and I find "fancyled" among the options.
The block appears, and the Tcl is:
create_bd_design "design_1"
update_compile_order -fileset sources_1
startgroup
create_bd_cell -type ip -vlnv user.org:user:fancyled:1.0 fancyled_0
endgroup
Now let's add the Zynq block and run both block automation and connection automation. This pulls in the "rst_ps7_0_50M" reset block and an "axi_smc" block. It also makes connections to the (useless) DDR and FIXED_IO.
The Tcl is like so:
startgroup
create_bd_cell -type ip -vlnv xilinx.com:ip:processing_system7:5.5 processing_system7_0
endgroup
apply_bd_automation -rule xilinx.com:bd_rule:axi4 -config { Clk_master {Auto} Clk_slave {Auto} Clk_xbar {Auto} Master {/processing_system7_0/M_AXI_GP0} Slave {/fancyled_0/S00_AXI} ddr_seg {Auto} intc_ip {New AXI SmartConnect} master_apm {0}}  [get_bd_intf_pins fancyled_0/S00_AXI]
--- Slave segment '/fancyled_0/S00_AXI/S00_AXI_reg' is being assigned into address space '/processing_system7_0/Data' at <0x43C0_0000 [ 64K ]>.
apply_bd_automation -rule xilinx.com:bd_rule:processing_system7 -config {make_external "FIXED_IO, DDR" Master "Disable" Slave "Disable" }  [get_bd_cells processing_system7_0]
regenerate_bd_layout
In the above, I retain the "comment" that shows where our AXI peripheral has been placed in the address space.

What do we have so far?

We could create our HDL wrapper and build this. We would end up with 8 registers that we could read and write, but which would do nothing. Making them do something involves editing the verilog files.

Notice however that our custom AXI block is not part of our project. The files reside in the ip_repo area. This is perhaps good for code reuse. However, one of my goals is to be able to package up all the files for a project without dependencies and be able to put all the files on Github.

As an experiment, I create and package another IP, taking all the defaults and watching closely to see if there is an option to just include it in the current project. There does not seem to be.
I see this as it creates the IP:

create_peripheral user.org user myip 1.0 -dir /u1/home/tom/vivado/ip_repo
add_peripheral_interface S00_AXI -interface_mode slave -axi_type lite [ipx::find_open_core user.org:user:myip:1.0]
generate_peripheral -driver -bfm_example_design -debug_hw_example_design [ipx::find_open_core user.org:user:myip:1.0]
write_peripheral [ipx::find_open_core user.org:user:myip:1.0]
set_property  ip_repo_paths  {/u1/home/tom/vivado/ip_repo/myip_1_0 /u1/home/tom/vivado/ip_repo/fancyled_1_0 /u1/home/tom/vivado/ip_repo} [current_project]
update_ip_catalog -rebuild
ipx::edit_ip_in_project -upgrade true -name edit_myip_v1_0 -directory /u1/home/tom/vivado/ip_repo /u1/home/tom/vivado/ip_repo/myip_1_0/component.xml
I see no hints here for what I would like to do.

Hack on the Verilog

This is here:
pwd
/home/tom/vivado/ip_repo/fancyled_1_0/hdl
ls -l
-rw-r--r-- 1 tom tom 18524 Dec 19 12:12 fancyled_slave_lite_v1_0_S00_AXI.v
-rw-r--r-- 1 tom tom  2233 Dec 19 12:12 fancyled.v
I was going to add an input port "clock" to bring in FCLK_CLK0 (which is a 50 Mhz clock), but it is already available as "s00_axi_aclk". And it gets passed to the inner module as S_AXI_ACLK. Lots of stuff in the inner module gets triggered via:
always @(posedge S_AXI_ACLK)
I do what I have done several times before, namely make register 0 a read only register that returns an ID code. I will use 32'habcd0012, so I do this on reset:
slv_reg0 <= 32'habcd0012;
I also add "code" to accumulate a count at 1 Hz in slv_reg1.

Generate the bitstream

We do the HDL wrapper thing, then click "Generate Bitstream" at the bottom of the "flow navigator".

I get errors when I simply try to write values into slv_reg1 and slv_reg2 saying that these nets have multiple drivers. What might work is to comment these out:

		slv_reg0 <= slv_reg0;
        // slv_reg1 <= slv_reg1;
        // slv_reg2 <= slv_reg2;
        slv_reg3 <= slv_reg3;
No. That doesn't do it. The message is:
Multiple Driver Nets .... slv_reg2[0] has multiple drivers (31 more like this)
I tried several other things, but still get the error. I ran into this in a prior project. The solution then was only to set the slv_reg* value immediately before the value was read. Vivado 2024 generates a tangle of ternary if statements to handle the register reads, whereas Vivado 2022 used a case statment. But that really doesn't matter. What I did successfully in another project where I was letting reg3 be reg1+reg2 was to update reg3 whenever I handled the write for reg1 or reg2.

What I need to do is to look at some other peoples examples and see how they handle returning values in these registers.

The "element14" fellow sets up one register to read his switches. It is worth extracting his code and looking at it carefully. Among other things, he sets up this little segment to handle the one register he reads from:
always @( posedge S_AXI_ACLK )
	begin
	  if ( S_AXI_ARESETN == 1'b0 )
	  begin
		slv_reg1 <= 0;
	  end
	  else
	  begin
	    slv_reg1 <= switches;
	  end
    end
We edit in changes as per his example and try again.

Where are my leds?

I add a 4 bit wide output port named "leds" expecting it to show up on my block diagram, but nothing shows up. There is no trouble synthesizing the design (I suppose it is perfectly fine to have outputs that aren't actually connected to anything. The above discussion suggests that the game is to "edit the component.xml file to add the port, taking an educated guess on the formatting." Before I do that, I want to add a constraint file and see if everything just gets connected up without having to have anything extra on the block model. I add the constraint file "leds.xdc" and regenerate the bitstream. This does not work. I am going to have to figure out how to get those signals.

I can read my counters in reg2 and reg3 just fine now though, which is nice.

Constant block

I decide the drive the leds with all ones to nicely turn them off (why not). It turns out there is a constant block available. I customize it to make it 4 wide and give it the value of 1, then make a port named "leds" which is outputs connect them up, and generate the bitstream. This makes them all come on full brightness, which is odd. I change the value to 0 and regenerate the bitstream. This also makes them all come on with full brightness. This makes no sense - it is as if the constant block is always 0.

Counter and slice

Now I drag in a binary counter, make it 27 bit, fed by FCLK0. And a slice that pulls off bits 26:23 of that, and feeds those 4 bits to the leds port. I generate the bitstream. This works great! The values of the bits should be inverted, but it is doing exactly what it should. I don't know why the constant block is broken, but this counter business is sort of more fun.

There is a block "utility vector logic". It can be invert, and, or, xor. I make it a 4 bit wide inverter, patch it in, and generate the bitstream.

Hack on component.xml

I am surprised that his is needed, but so far this is the only method I have found to get "leds" displayed on my custom block. There probably is some undocumented (or poorly documented) proper way to do this, but I have yet to figure it out. This file is 798 lines. It is XML, which is an exercise in poor design and verbosity, but that is the least of our worries here and now. Near the end of this file is a section that declares "ports". I just find the last "port" in this section, dupicate it, and change the name to "leds" and the direction to "out". I leave it as a "wire". This looks like:
<spirit:port>
        <spirit:name>leds</spirit:name>
        <spirit:wire>
          <spirit:direction>out</spirit:direction>
          <spirit:wireTypeDefs>
            <spirit:wireTypeDef>
              <spirit:typeName>wire</spirit:typeName>
              <spirit:viewNameRef>xilinx_verilogsynthesis</spirit:viewNameRef>
              <spirit:viewNameRef>xilinx_verilogbehavioralsimulation</spirit:viewNameRef>
            </spirit:wireTypeDef>
          </spirit:wireTypeDefs>
        </spirit:wire>
</spirit:port>
Once I do this and go through the rigamarole to update the IP catalog, an "leds" port appears on my block diagram. Hurrah! We connect this to the external "leds" port, now leaving the vector logic block output unconnected, and we generate the bitstream, which works!

And now we test it. It sort of works. We get only one of the four LED blinking. There must be some way to indicate that the port has 4 signals.

Indeed, I look at some of the port declarations for some of the AXI bug signals and discover that this is the trick. Add this just after the "direction" entry above:

<spirit:vector>
            <spirit:left spirit:format="long">3</spirit:left>
            <spirit:right spirit:format="long">0</spirit:right>
</spirit:vector>
I made these edits, save the modified XML, then in vivado: This is the rigamarole when you do an external edit on something in the IP catalog.
It is a much nicer alternative than what I used to do, which was to delete then add back the block I modified.

It works!

Now all four LED are flashing. As things are now, I simply write to "reg1" in my newly minted AXI peripheral. So If I get annoyed by these crazy bright LED flashing (and I do) it is easy to just shut them off by changing code in the PS. I'll note that it is an order of magnitude faster and easier to change the C code than to change the FPGA. Synthesis takes a long time (minutes), whereas compiling and rebooting takes a few seconds.

A quick note about something that would be obvious to the Verilog veteran. To place a value on leds in my verilog, I do this:

output wire [3:0] leds,
reg [31:0]    slv_reg1;
assign leds = slv_reg1;
Here we are assigning from a 32 bit register to a 4 bit vector. Verilog doesn't mind this construct at all, and it uses the low 4 bits of the 32 bit register to drive "leds", so in my C code I can do this to turn off all four LED:
ap->reg1 = 0xf;
The next thing I want to investigate are the issues with adding extra verilog files to my Custom IP. Can I just add a file, or will modifications to component.xml be required?

Add an extra file

To be explicit, I call it "extra.v" and place a module named "pwm" inside of it. I reference the "pwm" module inside of fancyled_slave_lite_v1_0_S00_AXI.v. This does not work, it doesn't know anything about "extra.v" and complains that it cannot find the module "pwm".

I add the following line to the start of fancyled_slave_lite_v1_0_S00_AXI.v.

`include "extra.v"
Note the "backtick" at the start of `include. Yes, this is a backtick, not a single quote.

It fails with the error "cannot open include file "extra.v". Maybe there is some kind of search path issue. For whatever reason it cannot find this file in the same directory as the file including it.

When in doubt, hack on component.xml to specify an extra source file

Towards the end of this file are a bunch of FileSet entries. We can see where the two files fancyled_slave_lite_v1_0_S00_AXI.v and fancyled.v are named as VerilogSource -- in two sections, one for synthesis, one for simulation. So we just imitate these entries and add "extra.v".

This works!!
I hate it that clicking "OK" when bitstream generation finishes, launches into "view generated design", which just wastes my time. I interrupt with Ctrl-C, but this turns out to be a bad idea (see below).

For the heck of it, I find the string "Pre-Production" and change it to "Professional". This does not work. It needs to be one of some enumeration that I know nothing about. A bit of searching tells me that "Production" is a recommended alternative.

However this has led to all kinds of hell. It is now telling me that fancyled is "Locked by user". I revert to Pre-Production and roll up my sleeves. It is still in a snit. I exit vivado and restart. This seemed fix things. The problem seemed to start when I tried to interrupt the "view design". Clearly it is best to let things run their course, even if wrong and unwanted.

The bitstream generation finishes, but I am again bitten by clicking OK.
... sigh

I am happy that now when I edit extra.v (external to vivado using vim), vivado recognizes the file change when I save from vim and tells me that the model is out of date, so I can run through the update rigamarole.

More reading

The following resources look worthwhile:
Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org