When you are using some kind of processor, there are peripherals that can be accessed. Usability, or the ways that you can configure those peripherals, is limited to the chip manufacturer's decision. From my experience, is possible that the developer needs some characteristics that peripheral does no have. In this cases, is the developer who has to develop according the peripheral. In case of using a Xilinx SOC or MPSOC, this problem disappears, since if we need a peripheral with some specific characteristics, we can create it, and assign addresses to it as a native peripheral, and this is really powerful. In this tutorial I will show you how to create a peripheral, and the driver to manage it from Bare-Metal and Petalinux.
SOCs and MPSOCsFirst of all, we need to know what is exactly a SOC and MPSOC. SOC is the acronym of System On Chip. A SOC includes inside an entire system built with some different peripherals. In case of Xilinx SOC, inside we can found a single or dual core ARM Cortex A9, Processing System (PS), and an FPGA or Programable Logic (PL). PS, as all processors, has its own peripherals like SPI, I2C, UART... and all these peripherals are connected to an AMBA bus, a high speed bus, that also has connections to the PL. The peripheral we are going to create will be connected to this bus through the AXI Interconnect blocks, that are the door between PL and PS.
Xilinx also has in their portfolio a more complex device, the MPSOC. Philosophy of this devices is similar than the SOC, but have some differences. The most important one is the amount of devices inside the chip. In this case we will have dual or quad core ARM A9 (APU), dual core Cortex R5 (RPU), a GPU (only in some devices), and the FPGA (PL). In this case, the PL has peripherals of Ultrascale series like UltraRAM.
On MPSOC, all processors and PL are connected to the same communications bus, so the peripheral that we are going to create will be visible for all processors.
Creating the HDL for our peripheral.In this case, we will create a peripheral that compute the integer square root of a value. first we need to define the Register Map of our peripheral. That is all the information that out peripheral will receive from the PS, and all the information that our peripheral will send to the PS. Absolute addresses will be determined by Vivado on Implementation process, so the addresses that we design, later, will have an offset. remember also that addresses point to a byte, as the processor work with 32 bits words, one register and next has a increase of 4 in the address.
Control Register:
StatusRegister:
Once we have defined the registers, we can start to design the HDL. Instantiation of HDL module is as follows.
square_root_finder_v1_0 square_root_finder_inst0 (
.clk(),
.rstn(),
.i32_control_register(),
.i32_data(),
.o32_status_register(),
.or32_data()
);
The complete code is on Github.
Creating the AXI IP.When we have the HDL code of our peripheral, we will create the IP. For that, we need to open Vivado, create a new project and then go to
Tools > Create and Package new IP.
Then select Create a new AXI4 peripheral.
Fill your IP details.
And then select the interface type, in this case will use AXI4 Lite, if our IP will be the Master or the Slave of the communication, and the number of registers, in this case 4.
Finally, add the IP to the repository for can be accessed from block design.
Now, we have an empty IP created, and available to add in the block design. Next step is make the connections between AXI and our HDL.
Connecting our design to AXI bus.From the IP catalog, we are able to find out new IP, but it is empty, so we have edit it. For that, open the IP catalog, right click over the IP and select Edit in IP Packager.
This will open a new temporally project where we can connect out design to the AXI bus.
From the sources window, add your HDL design the the project.
Then you can find 3 sources in the project. One is your design you just added. The source which name is corresponding to you IP, is the top module of the AXI IP, and inside it, there is an instantiation of the module that implements the AXI protocol. What we need to do is to instantiate the HDL we design, in the top module. At the end of that source, you will find a tag indicating where you have to copy your instantiation.
Now we have out design inside the AXI IP top module, now we have to connect it. First we have to connect signal s00_ax_aclk to clock input of our design, and s00_axi_resetn to the reset input of our design. Notice that reset signal from AXI is inverted.
Now inside the module that implements the AXI protocol, we have to found the registers declaration.
In this case, we have 2 input registers (input_data and control), and 2 output registers (output_data and status). For the input register to the root finder, we need to comment their declaration, since this register will be covert to outputs.
Then, we have to create the output registers, corresponding to the commented slv_reg and the input registers for transfer data from IP to the PS.
In case of the output register, we are done, but the input register needs to be sent through AXI. To do that, we have to go to the read operation of the AXI interface, around line 379, and overwrite slv_reg2 and slv_reg3 for our input registers.
Then add new inputs and output to the AXI module instantiation.
and connect new registers from AXI module
to your module.
Now we will fill the IP documentation. First we need to assign a category to our IP, and fill all data that we want to share
Next, we have to add all families which our IP will be compatible.
Finally, merge changes of all steps.
And package IP and close the project.
Once we have out IP created, we can create a block design and add it to the design.
Addresses assigned by Vivado once the design is implemented, can be found in the Address Editor tab.
When we create an AXI IP, Vivado assign addresses to all registers, in this case we have 4 registers, and the access to them will be performed through pointers. Xilinx offer us a library (xil_io.h) with some macros to access registers. The library I developed uses this macros.
/* Library for manage square_root_finder management */
#include "xil_io.h" /* Xilinx library for pointer functions */
/* Global registers declaration */
#define SQUARE_ROOT_FINDER_IP_OFFSET_INPUT_VALUE 0x80000000
#define SQUARE_ROOT_FINDER_IP_OFFSET_CONTROL_REGISTER 0x80000004
#define SQUARE_ROOT_FINDER_IP_OFFSET_STATUS_REGISTER 0x80000008
#define SQUARE_ROOT_FINDER_IP_OFFSET_OUTPUT_VALUE 0x8000000c
/* Detail register declaration */
#define SQUARE_ROOT_FINDER_IP_CONTROL_START 0x00000001
#define SQUARE_ROOT_FINDER_IP_STATUS_END 0x00000001
#define SQUARE_ROOT_FINDER_IP_STATUS_END_SHIFT 0
#define SQUARE_ROOT_FINDER_IP_STATUS_RUN 0x00000002
#define SQUARE_ROOT_FINDER_IP_STATUS_RUN_SHIFT 1
#define SQUARE_ROOT_FINDER_IP_STATUS_ERR 0x00000004
#define SQUARE_ROOT_FINDER_IP_STATUS_ERR_SHIFT 2
/* Management macros */
#define square_root_write_value(x) Xil_Out32(SQUARE_ROOT_FINDER_IP_OFFSET_INPUT_VALUE, x)
#define square_root_start() Xil_Out32(SQUARE_ROOT_FINDER_IP_OFFSET_CONTROL_REGISTER, 1)
#define square_root_read_end() (Xil_in32(SQUARE_ROOT_FINDER_IP_OFFSET_STATUS_REGISTER)>>SQUARE_ROOT_FINDER_IP_STATUS_END_SHIFT) & 0x1
#define square_root_read_end() (Xil_in32(SQUARE_ROOT_FINDER_IP_OFFSET_STATUS_REGISTER)>>SQUARE_ROOT_FINDER_IP_STATUS_RUN_SHIFT) & 0x1
#define square_root_read_end() (Xil_in32(SQUARE_ROOT_FINDER_IP_OFFSET_STATUS_REGISTER)>>SQUARE_ROOT_FINDER_IP_STATUS_ERR_SHIFT) & 0x1
#define square_root_read_value() (Xil_in32(SQUARE_ROOT_FINDER_IP_OFFSET_OUTPUT_VALUE))
Application for Petalinux.To manage the IP from Petalinux, also we need to access to the IP addresses and write and read different registers. To do that, we will use mmap function available on Unix. The next application is executed with one argument where the user write the value which wants to compute the square root, and application return 0 if the error bit is set, or directly the computed value.
/* Square root IP compute */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
typedef long long int u64;
int main(int argc, char **argv) {
/* IP addresses definition */
unsigned int axi_size = 0x10000;
off_t axi_pbase = 0x80000000; /* physical base address */
u64 *axi_vptr;
int fd;
if (argc>3)
printf("Too much arguments. Only 1 arguments needed \n");
value = atoi(argv[1]);
if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) == -1) {
printf("Access memory error");
return(0);
}
axi_vptr = (u64 *)mmap(NULL, axi_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, axi_pbase);
axi_vptr[0] = value;
axi_vptr[1] = 0X1;
while(!(axi_vptr[2]&0x1));
if (axi_vptr[2]&0x4 > 0) /* ERROR detected */
return 0;
else
return axi_vptr[3]; /* Return value */
}
ConclusionsWorking with SOC and MPSOC offer to developer a high flexibility to develop heir own peripherals, or accelerators. The thing I like the most of working with SOC and MPSOC is the way how, from an Operating System like Petalinux, where the minimum time step is 1ms, we can manage peripherals as fast as the programmable logic can.
Comments