Skip to main content

FPGA Zero to Hero Vol 6 EBAZ4205 Chronicles

·3072 words·15 mins· loading ·
Table of Contents

FPGA Zero to Hero Vol 6 EBAZ4205 Chronicles
#

In my previous blog posts Link and Link I created a basic Verilog module and used the JTAG to test it and booted Linux on the board. As promised I will continue the series and this time create a custom AXI lite peripheral and access it from within the Linux kernel running on the board. I will access the peripheral from within the Petalinux running on the board. But I found out that the older version of Petalinux and Vivado specifically did not work as expected with Ubuntu 22.04. I rolled back to Petalinux 2021.2 and Vivado 2021.2 to get it working. Sometime the latest version of the software does not work as expected with older version of Ubuntu linux. The result might be different for you if you are using a different version of the linux distribution.

NOTE: My current system configuration is:

  • OS: Ubuntu 22.04
  • VIVADO Version: Vivado v2021.2 (64-bit)
  • PETALINUX Version: Petalinux 2021.2

What is AXI Lite
#

AXI Lite is a simplified version of the AXI (Advanced eXtensible Interface) protocol which is part of family of bus protocols ARM Advanced Microcontroller Bus Architecture (AMBA) standard. It is widely used in FPGA designs for communication between FPGA PL and PS (Processing System). The lite version is designed for low-bandwidth, low-latency communication and is often used for control and status registers in peripherals. For our simple cases we can use AXI Lite to communicate with the peripherals like GPIO, SPI, I2C, etc.

AXI Lite provides a simpler interface compared to the full AXI protocol, making it easier to implement and use in designs where high throughput is not required. It supports single-word transactions and has a reduced set of signals, which makes it suitable for simple control applications.

AXI4-Lite Interface Signals
#

AXI4-Lite uses five channels:

  • Read Address
  • Read Data
  • Write Address
  • Write Data
  • Write Response

To know about the AXI4-Lite interface signals in detail you can refer to the Xilinx AXI4-Lite documentation also ARM AMBA AXI4-Lite documentation might be useful.

There is a good tutorial that explains the AXI4-Lite interface in detail for custom AXI-lite interface.

My aim is to create a AXI lite peripheral and access it from within the Linux kernel running on the board. I will not go into details of the AXI4-Lite interface as it is beyond the scope of this blog post. If in future I get time I will create a separate blog post on the AXI4-Lite, AXI and AXI streams interface in detail and how to use it in FPGA designs.

Vivado HW AXI Lite IP Creation
#

I will be creating a custom AXI lite peripheral that can be used to switch on/off the LEDs on the EBAZ4205 board. The peripheral will use only 1 register that can be accessed from the Linux kernel running on the board. The last 2 bits of the register will be used to control the LEDs on the board. The first bit will be used to switch on/off the first LED, the second bit will be used to switch on/off the second LED. The LEDs are the one next to the ethernet port on the board.

To create a custom AXI lite peripheral we need to create a new Vivado project. I will not go into details of creating a Vivado project as I have already covered it in my previous blog Link. I will just provide the steps to create a custom AXI lite peripheral and use it in the Vivado project.

To create a custom AXI lite peripheral assuming you have already created a Vivado project you need to follow these steps:

Go to Menu bar and go Tools -> Create and Package IP -> Create New AXI4 Peripheral. This will open the Create and Package IP wizard as shown below. Select Create a new AXI4 peripheral

Create AXI Lite IP

Press Next and this will open the Periphereral Details wizard. The most important fields are Name, and IP Location. You can fill in the details as per your requirement. For this example, I will use the following details:

Peripheral Details

Press Next and this will open the Add Interface wizard. Here you can select the type of peripheral you want to create. For this example, I will select AXI4-Lite as the peripheral type and Slave mode as we want to create a slave peripheral that can be accessed by the master (Zynq PS). And for now we don’t need many Registers so we can leave the Number of Registers field as 4. which is the default value minimum possible value that you can put there. You can change it later if you want to add more registers to the peripheral.

Add Interface

Press Next and this will open the Create Peripheral wizard. You can see the summary of the peripheral you are going to create. Select Edit IP as we want to add custom code to the IP and press Finish to create the peripheral. This should open a new Vivado project with the peripheral created for editing the IP.

Create Peripheral

Now we need to add the custom code to the peripheral. To do this, go to the IP Sources tab in the Sources window and click on the “+” button or right click on Design Sources and click on Add Sources… to add a new file. The process is similar to adding a new file in any other Vivado project and I have couvered it in detail in my pervious blog Link. I will create a new Verilog file named led_ip.v and add the following code to it:

`timescale 1ns / 1ps

module led_ip(
        input wire [31:0] slv_reg,
        input wire clk,
        output reg led_1,
        output reg led_2
    );

    always @(posedge clk) begin
        if(slv_reg[0] == 1) begin
            led_1 <= 1;
        end else begin
            led_1 <= 0;
        end
        if(slv_reg[1] == 1) begin
            led_2 <= 1;
        end else begin
            led_2 <= 0;
        end
    end
endmodule

Now what it is doing is it is taking the slv_reg input which is the register that we will be accessing from the Linux kernel and checking the first two bits of the register. If the first bit is set to 1 then it will switch on the first LED and if the second bit is set to 1 then it will switch on the second LED. The slv_reg input is a 32-bit register that we will be accessing from the Linux kernel.

Now we need to connect this module to the AXI lite interface. To do this, we need to add an instance of the led_ip module in the LED_IP_v1_0_S00_AXI.v file and connect it to the AXI lite interface signals. Also we need to create two output wires to connect the LEDs. Go to the LED_IP_v1_0_S00_AXI.v file and add the following code:

To add the custom output just add the following lines to the LED_IP_v1_0_S00_AXI.v file where you see the comment // User to add ports here as shown below:

	module LED_IP_v1_0_S00_AXI #
	(
		// Users to add parameters here

		// User parameters ends
		// Do not modify the parameters beyond this line

		// Width of S_AXI data bus
		parameter integer C_S_AXI_DATA_WIDTH	= 32,
		// Width of S_AXI address bus
		parameter integer C_S_AXI_ADDR_WIDTH	= 4
	)
	(
		// Users to add ports here
		output wire led_1,
		output wire led_2,
		// User ports ends
    ...
    ...

and to add the instance of the led_ip module just add the lines shown below to the LED_IP_v1_0_S00_AXI.v file where you see the comment // Add user logic here it should be in the end of the file:

    // Add user logic here ...
    led_ip l1 (
        .slv_reg(slv_reg),
        .clk(s_axi_aclk),
        .led_1(led_1),
        .led_2(led_2)
    );
    // User logic ends

    endmodule
```verilog

We need to do same to the top level module of the AXI lite peripheral so that we can connect the led_ip module to the AXI lite interface signals.

Open file LED_IP_v1_0.v and we need to add the output wires for the LEDs and also instantiate the led_ip module. Add the following lines to the LED_IP_v1_0.v file where you see the comment // User to add ports here it should look like this:


`timescale 1 ns / 1 ps

	module LED_IP_v1_0 #
	(
		// Users to add parameters here
		// User parameters ends
		// Do not modify the parameters beyond this line
		// Parameters of Axi Slave Bus Interface S00_AXI
		parameter integer C_S00_AXI_DATA_WIDTH	= 32,
		parameter integer C_S00_AXI_ADDR_WIDTH	= 4
	)
	(
		// Users to add ports here
		output wire led_1,
		output wire led_2,
		// User ports ends
		// Do not modify the ports beyond this line
		// Ports of Axi Slave Bus Interface S00_AXI
		input wire  s00_axi_aclk,
		input wire  s00_axi_aresetn,
		input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_awaddr,
		input wire [2 : 0] s00_axi_awprot,
		input wire  s00_axi_awvalid,
		output wire  s00_axi_awready,
		input wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_wdata,
		input wire [(C_S00_AXI_DATA_WIDTH/8)-1 : 0] s00_axi_wstrb,
		input wire  s00_axi_wvalid,
		output wire  s00_axi_wready,
		output wire [1 : 0] s00_axi_bresp,
		output wire  s00_axi_bvalid,
		input wire  s00_axi_bready,
		input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_araddr,
		input wire [2 : 0] s00_axi_arprot,
		input wire  s00_axi_arvalid,
		output wire  s00_axi_arready,
		output wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_rdata,
		output wire [1 : 0] s00_axi_rresp,
		output wire  s00_axi_rvalid,
		input wire  s00_axi_rready
	);
	
	//wire led_1;
	//wire led_2;
// Instantiation of Axi Bus Interface S00_AXI
	LED_IP_v1_0_S00_AXI # ( 
		.C_S_AXI_DATA_WIDTH(C_S00_AXI_DATA_WIDTH),
		.C_S_AXI_ADDR_WIDTH(C_S00_AXI_ADDR_WIDTH)
	) LED_IP_v1_0_S00_AXI_inst (
		.S_AXI_ACLK(s00_axi_aclk),
		.S_AXI_ARESETN(s00_axi_aresetn),
		.S_AXI_AWADDR(s00_axi_awaddr),
		.S_AXI_AWPROT(s00_axi_awprot),
		.S_AXI_AWVALID(s00_axi_awvalid),
		.S_AXI_AWREADY(s00_axi_awready),
		.S_AXI_WDATA(s00_axi_wdata),
		.S_AXI_WSTRB(s00_axi_wstrb),
		.S_AXI_WVALID(s00_axi_wvalid),
		.S_AXI_WREADY(s00_axi_wready),
		.S_AXI_BRESP(s00_axi_bresp),
		.S_AXI_BVALID(s00_axi_bvalid),
		.S_AXI_BREADY(s00_axi_bready),
		.S_AXI_ARADDR(s00_axi_araddr),
		.S_AXI_ARPROT(s00_axi_arprot),
		.S_AXI_ARVALID(s00_axi_arvalid),
		.S_AXI_ARREADY(s00_axi_arready),
		.S_AXI_RDATA(s00_axi_rdata),
		.S_AXI_RRESP(s00_axi_rresp),
		.S_AXI_RVALID(s00_axi_rvalid),
		.S_AXI_RREADY(s00_axi_rready),
		.led_1(led_1),
		.led_2(led_2)
	);
	// Add user logic here
	// User logic ends

	endmodule

See that I have added two output wires led_1 and led_2 to the top level module added it to LED_IP_v1_0_S00_AXI_inst instance. This will connect the led_ip module to the AXI lite interface signals and also to the output wires that we will be using to control the LEDs on the board.

Now we need to package the IP so that we can use it in the Vivado project. To do this double click on component.xml file in the IP Sources tab and this will open the IP Packager window. Notice all the pakaging steps should be green checked to work properly. If you see any edit symbol as shown then you need to do the specified steps to fix the errors or merge changes.

IP Packager Merge Files

Finally package the IP by clicking on the Package IP button in the IP Packager window. This will create a new IP in the Vivado project that you can use in your design.

Package IP

Add IP to Vivado Project
#

First we need to enable a AXI Master interface on the Zynq PS to communicate with the AXI lite peripheral. To do this, open the Block Design in your Vivado project and double click on the Zynq7 Processing System IP block. This will open the Re-customize IP window. In the M_AXI_GP0 section, enable the AXI4-Lite interface by checking the box next to it. This will allow the Zynq PS to communicate with the AXI lite peripheral.

Enable AXI Lite Interface

Open your Vivado project. In the IP Integrator canvas, right-click and select Add IP. Search for your newly created IP (e.g., LED_IP_v1_0) and add it to the design.

Add IP to Vivado Project

Once you have done the above steps the Run Connection Automation should show up press it and let Vivado connect the IP to the Zynq PS. If it does not show up then you can manually connect the IP to the Zynq PS by connecting the M_AXI_GP0 interface of the Zynq PS to the S00_AXI interface of the AXI lite peripheral with AXI interconnect.

Connect IP to Zynq PS

it should add AXI interconnect and Process System Reset blocks to the design. The AXI interconnect block is used to connect the AXI lite peripheral to the Zynq PS and the Process System Reset block is used to generate reset signal.

Connect IP to Zynq PS

No we neet to connect the output wires of the AXI lite peripheral to the LEDs on the board. The process is described in my previous blog Link. You can refer to that blog post for the details.

Connect LEDs to AXI Lite Peripheral

We need to add the led_1 and led_2 output wires to the constraints file so that we can use them to control the LEDs on the board. To do this, open the Constraints tab in the Sources window and double click on the constraints.xdc file. This will open the constraints file in the editor. Add the following lines to the constraints file:

# Green LED
set_property IOSTANDARD LVCMOS33 [get_ports {led_1_0}]
set_property PACKAGE_PIN W13 [get_ports {led_1_0}]

# Red LED
set_property IOSTANDARD LVCMOS33 [get_ports {led_2_0}]
set_property PACKAGE_PIN W14 [get_ports {led_2_0}]

Finally the block design should look like this:

Block Design

Now we can synthesize, implement and generate the bitstream for the design. To do this, go to the Flow Navigator and click on Run Synthesis. Once the synthesis is complete, click on Run Implementation and then click on Generate Bitstream. Export Hardware and follow the steps from my previous blog Link to create a kernel image and boot the board with the new bitstream.

Petalinux Configuration and Rebuild
#

Rebuild the Petalinux project to include the new AXI lite peripheral. If you have not created a Petalinux project then you can refer to my previous blog Link for the details.

Accessing the AXI Lite Peripheral from Linux Kernel
#

We can access the AXI lite peripheral from the Linux kernel using the /dev/mem interface. The /dev/mem interface allows us to access the physical memory of the system and read/write to the registers of the AXI lite peripheral.

To access the AXI lite peripheral, we need to know its physical address. You can get it from the Device Tree file of the Petalinux project. The physical address of the AXI lite peripheral is defined in the Device Tree file.

You can find it by looking at components/plnx_workspace/device-tree/device-tree/pl.dtsi file in the Petalinux project. The entry for the AXI lite peripheral should look like this:

dts/ {
        amba_pl: amba_pl {
                #address-cells = <1>;
                #size-cells = <1>;
                compatible = "simple-bus";
                ranges ;
                LED_IP_0: LED_IP@43c00000 {
                        clock-names = "s00_axi_aclk";
                        clocks = <&clkc 15>;
                        compatible = "xlnx,LED-IP-1.0";
                        reg = <0x43c00000 0x10000>;
                        xlnx,s00-axi-addr-width = <0x4>;
                        xlnx,s00-axi-data-width = <0x20>;
                };
        };
};

The physical address of the AXI lite peripheral is 0x43c00000 and the size is 0x10000 (64 KiB). We will use this address to access the AXI lite peripheral from the Linux kernel.

To access the AXI lite peripheral from the Linux kernel, we can use the following C code snippet. This code will toggle the first two bits of the register to switch on/off the LEDs on the board.

#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>

#define LED_IP_BASE_PHYS  0x43C00000UL   /* address from pl.dtsi*/
#define LED_IP_MAP_SIZE   0x10000        /* 64 KiB window      */

#define REG_LED_CTRL      0x00           /* first register*/

static inline void busy_wait_ms(unsigned ms)
{
    struct timespec ts = { .tv_sec = ms / 1000,
                           .tv_nsec = (ms % 1000) * 1000 * 1000 };
    nanosleep(&ts, NULL);
}

int main(void)
{
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open /dev/mem");
        return EXIT_FAILURE;
    }

    void *virt = mmap(NULL, LED_IP_MAP_SIZE, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, LED_IP_BASE_PHYS);
    if (virt == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return EXIT_FAILURE;
    }

    volatile uint32_t *reg = (volatile uint32_t *)virt;

    /* Simple blink loop: toggle bit 0 of REG_LED_CTRL ten times */
    puts("Blinking LEDs …");
    for (int i = 0; i < 10; ++i) {
        reg[REG_LED_CTRL / 4] = 0x03;   /* LEDs on */
        busy_wait_ms(250);
        reg[REG_LED_CTRL / 4] = 0x0;    /* LEDs off */
        busy_wait_ms(250);
    }

    close(fd);
    return EXIT_SUCCESS;
}

Now to build the above code there are different ways to do it. You can create a new Petalinux application and add the above code to it or you can just compile it using the cross compiler provided by the Petalinux project.

You can also create a petalinux sdk using the following command:

petalinux-build --sdk

This will create required toolchain and you can use it to cross-compile the above code. and move it to the EBAZ4205 board with scp command.

You can also include the gcc compiler on the board itself. To use the gcc compiler on the board you need to enable it in the Petalinux project. To do that you need to configure rootfs in the Petalinux project. You can do this by running the following command:

petalinux-config -c rootfs

This will open the Petalinux Rootfs Configuration menu. Then navigate to packagegroup-core-buildessential and enable it by checking the box next to it. This will include the gcc compiler in the root filesystem of the Petalinux project.

petalinux-config -c rootfs
  Filesystem Packages --->
     misc --->
        [*] packagegroup-core-buildessential

compile the code using the following command:

gcc -o led_control led_control.c

And then run the code on the board using the following command:

sudo ./led_control

This will toggle the first two bits of the register and switch on/off the LEDs on the board. You should see the first two LEDs blinking on the board.

I did the same steps on the EBAZ4205 board and it worked as expected. The first two LEDs on the board started blinking. You can modify the code to control the LEDs as per your requirement.

LEDs On Off

Future Blog Posts
#

I would give a homework to the readers to create a custom AXI-lite peripheral that can be used to control uses the SPI module we created in previous blog post Link and access it from within the Linux kernel running on the board. You can use the same steps as described in this blog post to create the AXI-lite peripheral and access it from within the Linux kernel.

I would like to continue the series and create more complex AXI based IP and access them from within the Linux kernel running on the board. I will also cover how to create custom device drivers for the AXI peripherals (Non-Lite version) and AXI streams and access them from user space applications.

In future blog posts I want to create a Neural Network Accelerator using the AXI interface and access it from within the Linux kernel running on the board. I will also cover how to create custom device drivers for the neural network accelerator and access it from user space applications. Also posible is to create a device driver for the LED_IP and access it from within the Linux kernel running on the board. Unfortunately It might take some time due to my busy schedule. But I will try to cover it as soon as possible.


Tags: #FPGA #Zynq #Xilinx #Altera #Verilog #VHDL #Hardware #Programming #DigitalDesign