Timer with Interrupts


Tutorial Overview

In this tutorial we will add code to a peripheral template generated by the Peripheral Wizard to create a simple timer. The peripheral will generate an interrupt when the timer expires. The Microblaze will process the interrupt through an interrupt handler function which gets called whenever the interrupt occurs. In this example, we will make the LEDs flash by using the interrupt handler function to switch the state of the LEDs and reset the timer.

Figure: The Timer peripheral

The timer will use two registers, one to store the delay period and the other for starting, stopping and checking if the timer has expired. We will call the first register the delay register and the second register the control register. The delay register will be set by the software application to determine how long of a delay it requires. This value will remain in the delay register unchanged and the software application will be able to read this value if for some reason it needs to. The control register will only use the first two bits, being bit 0 and bit 1. Bit 0 will be read-only and will be used by the timer peripheral to signal to the software application that the timer has expired. Bit 1 will be read and writeable, and it will be used by the software application to make the timer “run”. When a “1” is written to this bit, the timer will start counting down from the delay value. When a “0” is written to this bit, the timer will reset.

This tutorial contains screenshots to guide you through the entire implementation process. Click on the images to view a higher resolution.

Requirements

Before following this tutorial, you will need to do the following:

  • Buy an ML505/ML506/ML507 or XUPV5 board if you don’t already have one. Xilinx supplies the ML50x boards, but the best deal is the XUPV5 from Digilent. Click the Digilent link for more information.

Create the Basic Project

Follow these steps to create the basic project:

  1. Open XPS. From the dialog box, select “Base System Builder wizard” and OK.
  2. You will be asked to specify which folder to place the project. Click “Browse” and create a new folder for the project. Click “OK”.  
  3. We are given the choice to create a new project or to create one using the template of another project. Tick “I would like to create a new design” and click “Next”.
  4. On the “Select Board” page, select “Xilinx” as the board vendor. Then select the board you are using (eg. “Virtex 5 ML505 Evaluation Platform”). Select “1” as the board revision. Click “Next”. (Note: If you are using the XUPV5 (ML509) board, you might find that it is not listed. Select “Virtex 5 ML505 Evaluation Platform” instead.)  
  5. On the “Select Processor” page, we normally have a choice between using the PowerPC “hard” processor, or the Microblaze “soft” processor. Since the Virtex-5 does not contain any PowerPCs, we can only select Microblaze. Click “Next”.  
  6. On the “Configure Microblaze” page, select the clock frequency to be 125MHz. For the BRAM local memory, select “64KB”. We will use the RS232 port for debugging rather than the JTAG, so select “No debug”. Click “Next”.  
  7. In selecting the Additional IO Interfaces, leave RS232_Uart_1 and LEDs_8Bit ticked and un-tick everything else.  
  8. On the “Add Internal Peripherals” page, click “Next”.  
  9. On the “Software Setup” page, select RS232_Uart_1 for both STDIN and STDOUT. Un-tick “Memory Test” and leave “Peripheral Test” ticked. Click “Next”.  
  10. Click “Generate”.
  11. Click “Finish”.
  12. If you are using the XUPV5 (ML509) board and you selected “Virtex 5 ML505 Evaluation Platform” in step 4 above, you must select the right Virtex-5 version in the project settings. Select “Project->Project Options” and set the Virtex-5 version to “XC5VLX110T”, package “FFG1136” and speed grade “-1”.

Create the Timer Peripheral

We now create our Timer peripheral using the Peripheral Wizard.

  1. Select from the menu “Hardware->Create or Import Peripheral”. Click “Next”.
  2. Select “Create templates for a new peripheral” and click “Next”.  
  3. We must now decide where to place the files for the peripheral. They can be placed within this project, or they can be made accessible to other projects. Select “To an XPS project”. Click “Next”.
  4. On the “Name and Version” page, type my_timer for the peripheral name. Click “Next”.  
  5. On the “Bus Interface” page, select “Processor Local Bus” (PLB) and click “Next”.  
  6. On the “IPIF Services” page, select “Interrupt control”, “User logic software register” and “Include data phase timer”. Un-tick everything else and click “Next”.  
  7. On the “Slave Interface” page, click “Next”.
  8. On the “Interrupt Service” page, leave the defaults and click “Next”.  
  9. On the “User S/W Register” page, choose “2” for the number of software accessible registers.  
  10. On the “IP Interconnect” page we can customize our connection to the PLB but we will leave everything as is for simplicity. Click “Next”.
  11. On the “Peripheral Simulation Support” page, we can specify if we want the wizard to create a simulation platform for our peripheral. Click “Next” without ticking the option to generate.
  12. After the “Peripheral Implementation Support” page, the wizard will generate all the template files for us. Tick “Generate ISE and XST project files” and “Generate template driver files”. Click “Next”.
  13. Click “Finish”. Now our templates are created and we can modify them to include the code for the timer.

Modify the Timer Peripheral

Now we want to add a timer to this peripheral template and connect it up with the registers that the Peripheral Wizard created for us.

  1. Select from the menu “File->Open” and look in the project folder.
  2. Open the folders: pcores\my_timer_v1_00_a\hdl\vhdl. This folder contains two source files that describe our peripheral my_timer.vhd and user_logic.vhd. The first file is the main part of the peripheral and it implements the interface to the OPB. The second file is where we place our custom logic to make the peripheral do what we need it to do. This part is instantiated by the first file.
  3. Open the file user_logic.vhd. We will need to modify this source code to include our timer code.
  4. Find the line of code that says --USER signal declarations added here and add the following lines of code just below.
-- Timer signals and components

signal timer_count : std_logic_vector(0 to C_SLV_DWIDTH-1);
signal timer_expired : std_logic;
signal timer_run : std_logic;
  1. Find the line of code that says --USER logic implementation added here and add the following lines of code just below.
-- Timer connections
timer_run <= slv_reg1(1);

-- Timer process - times the delay between bursts
process (Bus2IP_Clk, Bus2IP_Reset)
begin
  -- if the peripheral is told to reset, then reset the timer
  if Bus2IP_Reset = '1' then
    timer_count <= (others => '0');
    timer_expired <= '1';
  -- otherwise, if there is a clock event, run the timer
  elsif Bus2IP_Clk'event and Bus2IP_Clk = '1' then
    -- if the timer is not running, then reset the timer
    if timer_run = '0' then
      timer_count <= slv_reg0;
      timer_expired <= '0';
    -- if the timer count is not zero then decrease the count
    elsif timer_count /= 0 then
      timer_count <= timer_count - 1;
      timer_expired <= '0';
    -- otherwise, the timer has expired
    else
      timer_expired <= '1';
    end if;
  end if;
end process;

-- Interrupt connections
IP2Bus_IntrEvent(0) <= timer_expired;
  1. Find the line of code that says “when “01” => slv_ip2bus_data <= slv_reg1” and replace it with the three lines of code below. Now, when the timer control register is read from, bit 0 will represent the timer_expired signal.
when "01" =>
  slv_ip2bus_data(1 to C_SLV_DWIDTH-1) <= slv_reg1(1 to C_SLV_DWIDTH-1);
  slv_ip2bus_data(0) <= timer_expired;
  1. Now we need to remove the example code that generates interrupts. The peripheral wizard automatically generates some VHDL code to generate interrupts in our user_logic.vhd file. As we use the timer_expired signal to generate interrupts, we must delete the example code. Comment out or delete all the code between the line -- Example code to generate user logic interrupts and the line IP2Bus_IntrEvent <= intr_counter; inclusively.
  2. Save and close the file.

Import the Timer Peripheral

Now we will use the Peripheral Wizard again, but this time using the import function.

  1. Select from the menu “Hardware->Create or Import Peripheral” and click “Next”.
  2. Select “Import existing peripheral” and click “Next”.  
  3. Select “To an XPS project”, ensure that the folder chosen is the project folder, and click “Next”.
  4. For the name of the peripheral, type my_timer. Tick “Use version” and select the same version number that we originally created. Click “Next”. It will ask if we are willing to overwrite the existing peripheral and we should answer “Yes”.  
  5. Tick “HDL source files” and click “Next”.  
  6. Select “Use existing Peripheral Analysis Order file (*.pao)” and click “Browse”. From the project folder, go to pcores\my_timer_v1_00_a\data and select the my_timer_v2_1_0.pao file. Click “Next”.  
  7. On the “HDL analysis information” page, click “Next”. The wizard will mention if any errors are found in the design.
  8. On the “Bus Interfaces” page, tick “PLB Slave” and click “Next”.  
  9. On the “SPLB: Port” page, click “Next”.
  10. On the “SPLB: Parameter” page, click “Next”.
  11. On the “Identify Interrupt Signals” page, leave the defaults and click “Next”.  
  12. On the “Parameter Attributes” page, click “Next”.
  13. On the “Port Attributes” page, click “Next”.
  14. Click “Finish”.

The timer peripheral is now ready to use and it should be accessible through the “IP Catalog->Project Local pcores” in the XPS interface. Note that although we can access it through the IP Catalog, other projects will not find it there because it is only associated with our project, as we specified in the Peripheral Wizard.

Create an Instance of the Peripheral

Now we are ready to create an instance of the peripheral into our project.

  1. From the “IP Catalog” find the my_timer IP core in the “Project Local pcores” group. Right click on the core and select “Add IP”.  
  2. From the “System Assembly View” using the “Bus Interface” filter, connect the my_timer_0 to the PLB bus.  
  3. Click on the “Addresses” filter. Change the “Size” for my_timer_0 to 64K. Then click “Generate Addresses”.

Now we have created an instance of the Timer peripheral in our design.

Create an Instance of the Interrupt Controller

Now we need to include an interrupt controller into our design to detect the interrupt requests from the my_timer peripheral and relay them to the Microblaze. We could of course, not use the interrupt controller and directly connect the my_timer interrupt request signal (IRQ) to the Microblaze. The benefit of using the interrupt controller is that we are able to manage multiple interrupt sources if we wish to do so in the future.

  1. From the “IP Catalog” find the “XPS Interrupt Controller” IP core in the “Clock, Reset and Interrupt” group. Right click on the core and select “Add IP”.  
  2. From the “System Assembly View” using the “Bus Interface” filter, connect the xps_intc_0 to the PLB bus.  
  3. Click on the “Addresses” filter. Change the “Size” for xps_intc_0 to 64K. Then click “Generate Addresses”.

Now we have created an instance of the Interrupt Controller peripheral in our design.

Connect the Interrupt Request (IRQ) signals

We now need to connect the my_timer IRQ signal to the interrupt controller, and then connect the interrupt controller IRQ signal to the Microblaze.

  1. Click on the “Ports” filter. Click on the “+” for my_timer_0 to view its ports. Click on the “Net” field for the IP2INTC_Irpt port. Type my_timer_irq in this field and press “Enter”.  
  2. Click on the “+” for xps_intc_0 to view its ports. Click on the “Net” field for the “Irq” port. Type xps_intc_irq in this field and press “Enter”.  
  3. Still under xps_intc_0, click on the button in the “Net” field for the “Intr” port. In the dialog box that opens, select my_timer_irq in the left panel called “Potential Interrupt Connections” and click the “+” button to move it to the right panel called “Connected Interrupts”. Click “OK”.  
  4. Still within the “Ports” filter, click on the “+” for the microblaze_0 to view the Microblaze ports. Click on the “Net” field for the “INTERRUPT” port. Using the drop-down menu, select xps_intc_irq for this field. This connects the interrupt controller IRQ signal to the IRQ input of the Microblaze.

Now we have connected the my_timer IRQ signal to the interrupt controller, and the interrupt controller’s IRQ signal to the IRQ input of the Microblaze. This completes our hardware connections for interrupt capability.

Modify the Software Application

The software application will contain the main program and the interrupt handler function to be called when an interrupt occurs. It must create the interrupt vector table which tells the Microblaze what code to run when an interrupt is generated. It also must enable the Timer peripheral to generate interrupts, and the Microblaze to respond to interrupts.

  1. From the “Applications” tab, open “Sources” within the “Project: TestApp_Peripheral” tree. Open the TestApp_Peripheral.c source file.
  2. Replace all the code in this file with the following source and save the file.
#include "xparameters.h"
#include "xbasic_types.h"
#include "xgpio.h"
#include "xstatus.h"
#include "my_timer.h"
#include "xintc.h"

#define TIMER_RESET   0x00000000
#define TIMER_RUN     0x40000000
#define TIMER_EXPIRED 0x80000000
#define TIMER_HALFSEC 0x02FAF080

XGpio GpioOutput;
Xuint32 my_timer;
unsigned int *my_timer_p =
         (unsigned int *) XPAR_MY_TIMER_0_BASEADDR;

//----------------------------------------------------
// INTERRUPT HANDLER FUNCTION
//----------------------------------------------------
void MY_TIMER_Intr_Handler(void * baseaddr_p)
{
  static Xuint32 led_data;
  Xuint32 baseaddr;
  Xuint32 IpStatus;

  baseaddr = (Xuint32) baseaddr_p;

  // Get the timer interrupt status
  IpStatus = MY_TIMER_mReadReg(baseaddr,
                      MY_TIMER_INTR_IPISR_OFFSET);

  // If timer caused the interrupt then switch LEDs
  if (IpStatus){
    // If LEDs are ON then turn OFF and vice versa
    led_data = led_data ^ 0xFF;
    XGpio_DiscreteWrite(&GpioOutput, 1, led_data);
    // Reset the timer and hence the interrupt
    MY_TIMER_mWriteSlaveReg1(baseaddr, 0, TIMER_RESET);
    MY_TIMER_mWriteSlaveReg1(baseaddr, 0, TIMER_RUN);
  }
}

//----------------------------------------------------
// MAIN FUNCTION
//----------------------------------------------------
int main (void)
{
  Xuint32 status;

  // Clear screen
  xil_printf("%c[2J",27);
  xil_printf("  Timer Project from ");
  xil_printf("http://www.fpgadeveloper.com\r\n");
  xil_printf("  --------------------------------");
  xil_printf("-----------------\r\n\r\n");

  //----------------------------------------------------
  // REGISTER INTERRUPT HANDLER
  //----------------------------------------------------
  xil_printf("  - Registering interrupt handler\r\n");
  XIntc_RegisterHandler(XPAR_XPS_INTC_0_BASEADDR,
    XPAR_XPS_INTC_0_MY_TIMER_0_IP2INTC_IRPT_INTR,
    MY_TIMER_Intr_Handler,
    (void *)XPAR_MY_TIMER_0_BASEADDR);

  //----------------------------------------------------
  // INITIALIZE THE TIMER PERIPHERAL
  //----------------------------------------------------
  xil_printf("  - Initializing the timer peripheral\r\n");
  // Check that the my_timer peripheral exists
  XASSERT_NONVOID(my_timer_p != XNULL);
  my_timer = (Xuint32) my_timer_p;
  // Load the delay register with the delay time of 0.5s
  MY_TIMER_mWriteSlaveReg0(my_timer, 0, TIMER_HALFSEC);

  //----------------------------------------------------
  // SETUP THE LEDs
  //----------------------------------------------------
  xil_printf("  - Setting up the LEDs\r\n");
  // Initialize the GPIO driver
  status = XGpio_Initialize(&GpioOutput,
                            XPAR_LEDS_8BIT_DEVICE_ID);
  if (status != XST_SUCCESS)
    return XST_FAILURE;
  // Set the direction for all signals to be outputs
  XGpio_SetDataDirection(&GpioOutput, 1, 0x0);
  // Turn the LEDs ON
  XGpio_DiscreteWrite(&GpioOutput, 1, 0xFF);

  //----------------------------------------------------
  // ENABLE INTERRUPTS
  //----------------------------------------------------
  xil_printf("  - Enabling interrupts\r\n");
  // Enable INTC interrupts
  XIntc_mMasterEnable(XPAR_XPS_INTC_0_BASEADDR);
  XIntc_mEnableIntr(XPAR_XPS_INTC_0_BASEADDR,
    XPAR_MY_TIMER_0_IP2INTC_IRPT_MASK);
  // Enable timer interrupts
  MY_TIMER_EnableInterrupt(my_timer_p);
  // Enable Microblaze interrupts
  microblaze_enable_interrupts();

  // Start the timer
  xil_printf("  - Starting the timer\r\n\r\n");
  MY_TIMER_mWriteSlaveReg1(my_timer, 0, TIMER_RUN);

  xil_printf("    SUCCESS!\r\n");
  while(1){
  }
}
  1. Save and close the file.
  2. Generate a new linker script by right clicking “Project: TestApp_Peripheral” and clicking “Generate Linker Script”. Click “OK”.

Download and Test the Project

  1. Open a Hyperterminal window with the required settings. For the correct settings, see Hyperterminal Settings.
  2. Turn on the ML505 board.
  3. From the XPS software, select “Device Configuration->Download Bitstream”.

The Hyperterminal output should look as shown in the image below:

The LEDs should be flashing once every second. The timer peripheral is set for a delay period of half a second. Each call to the interrupt handler changes the state of the LEDs and resets the timer for another half a second. The result is that the LEDs remain ON for half a second and OFF for half a second.

The project folder for this tutorial can be downloaded in a compressed ZIP file TimerInterrupts-EDK10-1.zip . Right-click on the link and select “Save Link As”.


See also