Tutorial 15: A Software Stopwatch

In this tutorial, we will develop an ARM application that uses the stopwatch_controller IP which we designed in the last two tutorials.

Exporting the Project to SDK

tut15fig1From your stopwatch_controller project in Vivado, select File→Export→Export·Hardware… . A dialog box will appear. Make sure that Export to is set to <Local to Project> and the include bitstream box is checked.

tut15fig2

 

Then select File→Launch SDK to launch the Vivado Software Design Kit (SDK). When the dialog box appears click OK.

Creating a Software Project

After the SDK completes loading, we will need to add the stopwatch_controller repository to the software project. This lets the project use software drivers built for the stopwatch controller IP. Select Xilinx·Tools→Repositories to bring up the Repositories dialog box. Click the New button and select the stopwatch_controller_1.0 IP from the ip_repo directory. Click OK, and then OK again.

Next, we will create a new application. Select File→New→Application·Project. Enter Stopwatch for the project name and click Next. Select the Empty Application template and then click Finish. tut15fig3This will create Stopwatch and Stopwatch_bsp projects, and will also create a tab called system.mss. In the system.mss tab, click the Modify this BSP Settings button.

tut15fig4This will bring up the Board Support Package Settings dialog box. Select the drivers tab, then scroll down the component list until you see the stopwatch_controller component. tut15fig5

 

Select the stopwatch_controller. Then, in the Drivers column, select stopwatch_controller from the pull-down menu.

Writing the ARM code

tut15fig6Open up the Stopwatch project in the Project Explorer and right-click on the src folder. Select New→File. This will bring up the New File dialog box. Enter stopwatch.c for the file name and click Finish. You can now edit the file.

First, we need some header files: one for controlling the ZYNQ processor in general and the other to bring in items specific to our stopwatch controller. Here are the includes:

#include "stopwatch_controller.h"
#include "xparameters.h"

Next we want to make some definitions which give us some simple names for the addresses of the registers in our IP block. We will use the defines from the stopwatch_controller.h file but give them names with more meaning for us. Here is the code:

/* 	Define the base memory addresses of the stopwatch_controller IP core */
#define SW_BASE XPAR_STOPWATCH_CONTROLLER_0_S00_AXI_BASEADDR
#define SSD_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG0_OFFSET
#define LED_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG1_OFFSET
#define SWITCH_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG2_OFFSET
#define BTN_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG3_OFFSET
#define ENC_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG4_OFFSET
#define ENC_SW_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG5_OFFSET
#define ENC_BTN_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG6_OFFSET
#define TIMER_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG7_OFFSET

We are creating what is known as a bare metal software program. What this means is that there is no operating system running on the ARM while our program runs. It’s just our program, and nothing else. What that means is that our program should really never exit. Our goal is to have it run in a loop and never exit. Each time through the loop, the program should do whatever processing is required at that point in time. So we will declare our C main routine and a while loop that never exits. For now, we’ll read all six of our input registers at the beginning of the loop, do some processing, and then write both of our output registers at the end of the loop. Here is the basic outline with an empty loop:

int main(void){
  u32 ssd_val = 0;
  u32 led_val = 0;
  u32 switch_val = 0;
  u32 btn_val = 0;
  u32 enc_val = 0;
  u32 enc_sw_val = 0;
  u32 enc_btn_val = 0;
  u32 timer_val = 0;

  while(1){
    switch_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, SWITCH_ADDR);
    btn_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, BTN_ADDR);
    enc_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, ENC_ADDR);
    enc_sw_val= STOPWATCH_CONTROLLER_mReadReg(SW_BASE, ENC_SW_ADDR);
    enc_btn_val= STOPWATCH_CONTROLLER_mReadReg(SW_BASE, ENC_BTN_ADDR);
    timer_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, TIMER_ADDR);
    /* Actions go here */
    STOPWATCH_CONTROLLER_mWriteReg(SW_BASE, LED_ADDR, led_val);
    STOPWATCH_CONTROLLER_mWriteReg(SW_BASE, SSD_ADDR, ssd_val);
  }
  return 1;
}

The STOPWATCH_CONTROLLER_mReadReg function takes the base address of our stopwatch block and a register address, and returns the current value of the register. The STOPWATCH_CONTROLLER_mWriteReg function takes the same arguments and a value to write to the register. All that is left for us is to define some action to perform.

As a starter, let’s just take the encoder value and write it to the SSD and LED outputs. Here is the code for that:


	/* Actions go here */
	ssd_val = enc_val;
	led_val = enc_val;

All this does is assign the encoder value to the SSD and LED outputs. Go ahead and save the file. The SDK will automatically compile the code and you will see some warning about unused variables. You can ignore those.

Running the Program

tut15fig7Right-click on the Stopwatch application and select Run→Launch·on·Hardware·(GDB). You should see the value of the encoder register in the FPGA appear on the seven segment display and also in binary on the LEDs. This simple application demonstrates the power of combining an FPGA with an embedded processor. We have been able to take care of some simple tasks like maintaining the rotary encoder and controlling the seven segment display in the FPGA, while at the same time handling the higher-level control in a C program. In the next section we will add more stopwatch functions.

Counting the Seconds

Let’s add some more functions to our program. First, I want to keep around the rotary encoder display function. Let’s use the switch on the encoder to select the mode for our program. If the switch is in one position we will view the encoder value, if it is in the other we will display the stopwatch function. We will change the action section to have us an if statement:

		if (enc_sw_val) { /* encoder display */
			ssd_val = enc_val;
			led_val = enc_val;
		} else { /* stopwatch functions */		
		}

Now, how do we manage the stopwatch functions? The FPGA hardware is keeping a millisecond counter for us. That counter is 32 bits, so it will be able to count for almost 50 days. To display the counter, all our program needs to do is read the value, then decide which part of the counter to display. Like with our hardware example in tutorial 8, we will use the switch inputs to select which part of the counter we are displaying.

Our first task is to break our millisecond counter value down into hundredths of seconds, seconds, minutes, hours, and days. We keep a 32-bit unsigned value for each one, and calculate the value based on the timer value.

u32 hundredths = (timer_val/10)%100;
u32 seconds = (timer_val / 1000)%60;
u32 minutes = (timer_val / (60*1000)) % 60;
u32 hours = (timer_val / (60*60*1000)) % 24;
u32 days = (timer_val / (24*60*60*1000));

Next, we keep another 32-bit unsigned value to hold the time we want to display. We first set it to hundredths, then each subsequent larger time value based on the switch settings.

u32 time = hundredths;
if (switch_val&1) time = seconds;
if (switch_val&2) time = minutes;
if (switch_val&4) time = hours;
if (switch_val&8) time = days;

Finally, we need to display the time value. We need to keep in mind that the seven segment display displays hexadecimal values, so we need to convert the time value into BCD before displaying it. For the LEDs it makes more sense to display them in binary:

led_val = time;
ssd_val = ((time/10)<<4)|(time%10);

Start, Stop, and Clear

How do we start, stop, and clear the display? You may have noticed that, when you recompile and rerun your program, the time doesn’t reset. This is because the FPGA hardware is not being reloaded. The timer register counts independently of the software looking at it. We could add the function in the FPGA to start, stop, and clear the counter, but this isn’t really necessary. We can achieve all those effects in software.

But first, we need to get some terminology straight. When we look at our program we will refer to a number of sections. There is the definitions section before the main declaration which we use to make definitions. Then there is an initialization before the while loop where we initialize variables when the program first runs. Inside the while loop, we have an input section where we read all the relevant registers. Then we have a body section where we perform some computations. Finally we have an output section where we update the hardware register values. Let’s look at our code again and add comments at the beginning of each section. I will refer to these sections later when we add our additional features.

Here is how our code looks right now with the sections commented:

#include "stopwatch_controller.h"
#include "xparameters.h"

/* Definitions Section */

/* Define the base memory addresses of the stopwatch_controller IP core */
#define SW_BASE XPAR_STOPWATCH_CONTROLLER_0_S00_AXI_BASEADDR
#define SSD_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG0_OFFSET
#define LED_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG1_OFFSET
#define SWITCH_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG2_OFFSET
#define BTN_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG3_OFFSET
#define ENC_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG4_OFFSET
#define ENC_SW_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG5_OFFSET
#define ENC_BTN_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG6_OFFSET
#define TIMER_ADDR STOPWATCH_CONTROLLER_S00_AXI_SLV_REG7_OFFSET

int main(void){
	/* Initialization section */
	u32 ssd_val = 0;
	u32 led_val = 0;
	u32 switch_val = 0;
	u32 btn_val = 0;
	u32 enc_val = 0;
	u32 enc_sw_val = 0;
	u32 enc_btn_val = 0;
	u32 timer_val = 0;
	while(1){
		/* Input section */
		switch_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, SWITCH_ADDR);
		btn_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, BTN_ADDR);
		enc_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, ENC_ADDR);
		enc_sw_val= STOPWATCH_CONTROLLER_mReadReg(SW_BASE, ENC_SW_ADDR);
		enc_btn_val= STOPWATCH_CONTROLLER_mReadReg(SW_BASE, ENC_BTN_ADDR);
		timer_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, TIMER_ADDR);

		/* Computation section */
		if (enc_sw_val) { /* encoder display */
			ssd_val = enc_val;
			led_val = enc_val;
		} else { /* stopwatch functions */
			u32 hundredths = (timer_val/10)%100;
			u32 seconds = (timer_val / 1000)%60;
			u32 minutes = (timer_val / (60*1000)) % 60;
			u32 hours = (timer_val / (60*60*1000)) % 24;
			u32 days = (timer_val / (24*60*60*1000));
			u32 time = hundredths;
			if (switch_val&1) time = seconds;
			if (switch_val&2) time = minutes;
			if (switch_val&4) time = hours;
			if (switch_val&8) time = days;
			led_val = time;
			ssd_val = ((time/10)<<4)|(time%10);
		}

		/* Output section */
		STOPWATCH_CONTROLLER_mWriteReg(SW_BASE, LED_ADDR, led_val);
		STOPWATCH_CONTROLLER_mWriteReg(SW_BASE, SSD_ADDR, ssd_val);
	}
	return 1;
}

First, we know we will need to use the buttons to control the board behavior. Let’s add some define statements to the definitions section. These defines indicate a mask for each of the button bits in the FPGA register.

#define BTN_C 16
#define BTN_D 8
#define BTN_R 4
#define BTN_U 2
#define BTN_L 1

We want to compute when a button rises. We can do that by keeping track of the button value from the previous time through the look and checking if it is different the next time through the loop. So we will add a new variable called btn_val_prev to the initialization section. This should be initialized to the current value of the button register.

u32 btn_val_prev = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, BTN_ADDR);

Then in the input section, we will use a new variable called btn_val_rise to compare the new button value with the previous button value.

u32 btn_val_rise = ~btn_val_prev & btn_val;

Finally, in the output section, we need to transfer the current button value into the previous button value in preparation for the next time through the loop.

btn_val_prev = btn_val;

Clearing the counter

To clear the counter, we should just remember the timer value when the clear button is pressed, and then in the rest of the code, use the difference between the timer value and the value when it was last cleared as the actual timer value.

First, add a variable called timer_zero to the initialization section. This will hold the timer value whenever we clear the timer. In addition, it will initialize to the current timer value so when the program runs the timer starts at zero.

u32 timer_zero = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, TIMER_ADDR);

Then, in the input section, we modify the assignment to timer_val to subtract off the timer_zero value:

timer_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, TIMER_ADDR) - timer_zero;

Then in the computation section, we add a statement to set the timer_zero variable to the current timer value if the user presses the left button.

if (btn_val_rise&BTN_C) timer_zero = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, TIMER_ADDR);

Compile and run the program again. You should see the counter start at zero and clear whenever you press the left button.

Stopping the counter

It is pretty easy to stop the counter: we just stop reading the timer value. Just add a variable called stopped and initialize it with zero. Place this in the initialization section.

u32 stopped = 0;

Then in the input section, condition the assignment of timer_val based on not being stopped. Like this:

if (!stopped) timer_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, TIMER_ADDR) - timer_zero;

Finally, in the computation section, you need to add a test to set the stopped flag if the center button is pushed.

if (btn_val_rise&BTN_C) stopped = 1;

Go ahead and compile and run this. You should see the counter stop when you press the middle button.

Starting again

Stopping the count is great, but how do we start it up again? Well of course, we need to set the stopped flag back to zero. Even so, that’s not enough. If that were all we did, when we started, the count would jump forward as if we hadn’t stopped at all. This is not what we want. The solution is to adjust the timer_zero value as well. We basically need to add the amount of time we were stopped. Here is the code which we add to the computation section to do that when we press the right button:

if (stopped && btn_val_rise&BTN_R) {
    stopped = 0;
    timer_zero = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, TIMER_ADDR) - timer_val;
}

Other useful features

I also added code which sets the timer_zero value to zero by pressing the up button, and added a resume button which resumes the display without updating the timer_zero value. Try to add those features yourself.

Files

Here is a link to the stopwatch.c file that I used for this tutorial.

16 thoughts on “Tutorial 15: A Software Stopwatch

  1. Hi Pete,

    Sorry to bother again and again. In order to solve the problem encountered in the tutorial 17, I have reproduced the results from tutorial 13 to 16 (stopwatch tutorial) for the correctness in the creation of the co-process system. But I am still not able to reproduce the result.

    ILA is a very useful tool to debug the co-process system, so I have added the debug core to the stopwatch tutorial. I found that the signals of switches and buttons (i.e. input signals) can be detected by the ILA in the sense that the FPGA part might be working appropriately. Hence, I was doing a simple test that reads registers of the switches from FPGA by the ARM processor, but the

    switch_val = STOPWATCH_CONTROLLER_mReadReg(SW_BASE, SWITCH_ADDR)

    is always zero. No matter what the switch combinations are.

    From my side, the ARM processor cannot access these registers appropriately, I have no idea what the problem is. If you could give some hints, it would be very useful.

    BTW, I have done some simple tutorials which blink the LED in a sequential manner. I reproduced it successfully. I suppose the basic steps I have done should be right.

    Many Thanks

  2. Hi Pete,

    I finally solved the problem of the stopwatch design. As I said previously, the ARM processor cannot access the registers, the address of which (BASEADDR 0xFFFFFFFF and HIGHADDR 0x00000000) in xparameters.h is not right. This is why the ARM cannot read and write the registers. Just replace the addresses by the addresses of S00_AXI_reg, which is defined in the address editor in vivado 2015.4.

    I have no idea why it does happend. I guess it would be useful for some beginners in vivado.

    Thanks for the tutorials.

    Alex

  3. Alex,

    Thanks for the update on this. Sorry for not responding sooner. I have been in the middle of an office move that has kept me very busy.

    I need to go back through this tutorial with Vivado2015.4 and see what’s up. Maybe this is also what Akshatha ran into as well. Let me know how the next tutorial goes.

    Thanks,

    -Pete

  4. Hi Pete,
    I follow above steps and get an error : undefined reference to ‘Xil_In32’ and ‘Xil_Out32’. It seems that some head file cannot be found. Actually, the & are unresolved. Can you help me solve this problem?
    Thankyou very much.
    My version is 2017.1 and Ubuntu 14.04…

  5. Hi Pete,

    I can’t thank you enough. Your tutorials go straight to the point and I am learning a lot with each tutorial. Could you add some tutorial on how to handle I/O interrupts or some pointers on how to handle that?

  6. Hi Pete

    i am getting error ” binaries not found” on process Right-click on the Stopwatch application and select Run→Launch·on·Hardware·(GDB).

    version 2016.1

    pl help me in this regard and
    is other setting require in sdk like run configuration ,
    without encoder possible to see output of timer on led and seven segment.

    thanks
    mayank

  7. Were you able to get past the error where you exported the AXI interface? I don’t think this step will work unless you have successfully built the FPGA and downloaded it to the board.

  8. Hi Pete,

    Many thanks for putting time into creating these tutorials – I enjoy them greatly. I also thought, I would like to publicise my efforts somewhere for my own and, possibly, somebody else’s benefit and created a repository on [1]GitHub with software stopwatch.

    Once more, great thanks to you,
    Zdzislaw

    [1] https://github.com/zdzislaw-s/stopwatch_controller

  9. #including xil_io.h solves errors related to ‘Xil_In32’ and ‘Xil_Out32’ in vivado 2018.1

  10. Many thanks for your tutorials, I use VITIS(& vivado v2020.1),
    I could not get (find) “stopwatch_controller.h”, and I could not select stopwatch_controller ‘s driver @BSP.
    please help me~

  11. These tutorials need to be updated for the newer tools. I have never used Vitis and really have only used SDK for these tutorials, so there is not a lot of guidance I can offer. There must be a header file which Vitis creates that lets you know the addresses of all the IP in the FPGA. This is what you need to look for and include.

    I am super busy with paying work right now so I’m not sure when I’ll have the time to go through the tutorials and update them for the newer tools. But I’m sure that will happen at some point. If you gfigure out the answer though, please post it back here so that others will benefit.

    -Pete

  12. Dear Pete
    Seeing the comments above, it solved my problem.

    Thanks again for the good tutorial.

    -YoungSik

  13. Thanks for your detailed tutorial!

    I’m stuck at selecting a driver for stopwatch_controller like what YoungSik Kim says above.

    He seems to solve it by seeing above somewhere comments but I cannot find out where it is.

    I use Vitis 2020.1 and Vivado 2020.1 now.

    Please help me anyone?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.