EECS 373 Lab 5: Clocks, Timers, and Counters

Copyright © 2010-2011, Thomas Schmid, Matt Smith, Ye-Sheng Kuo, Lohit Yerva, and Prabal Dutta

rev 2/11/14

Schedule

See Posted Schedule

Pre-Lab Assignment

  1. Read through this  C Language tutorial.
  2. Read through the lab 5 in-lab and post-lab.
  3. Read Chapter 11: Timer_A from MSP430x1xx Family User's Guide

In-Lab and Post Lab Assignment Overview


Assignment one (In-Lab) consists of several exercises developing software in C for a custom Verilog timer. There is no In-lab demo for this part, but you must submit answers to questions with supporting data.

Assignment two (Post -Lab) consists of a measurement application using an ultra sonic distance sensor and one of the custom timers you worked with in assignment 1. 

In-Lab

Edit Libero Tool Profile (Be sure to do this before starting a new Libero Project)

Before starting a new project, you will need to add SoftConsole to the tool profile. Under project, select tool profiles. Select Software IDE. Add a profile by clicking the green plus button. Name it SoftConsole v3.3. Select SoftConsole for the tool integration. Finally browse for the SoftConsole executable in. Actel/Libero_v9.1\SoftConsole\Eclipse\eclipse.exe

It should look like this.


Save this and make sure SoftConsole v3.3 is selected as the software IDE.

Create a New Libero Project

As you did before, create a new Libero Project. In the following example, the project is called lab5.

Configure the Fabric Clock

You will need to provide a fabric clock as you have in the past, but this time we will want to configure the clock to be 40MHz. Open the MSS component in your new design and open the MSS clock configurator. Provide the selections as shown below. .


Configure the MSS Component

Next, configure your MSS as shown below. 

  1. Add  APB3 master interface
  2. Add a master reset to the fabric
  3. Add UART_0 
  4. De-select everything else
Generate the MSS component (MSS tab must be selected) when you are finished and update the the lab_MSS instance.

Create a Verilog Timer with an APB3 Interface

Next we will create a timer with the following Verilog. Notice that  this Verilog consists of a timer function module and a module that provides the APB3 interface. Simply put both modules in the same file and create the component. Create a component for your project and provide it with an APB3 slave bus interface like you did in lab 3. Refer back to lab 3 for details.
// timer.v

module timer(
pclk,
nreset,
bus_write_en,
bus_read_en,
bus_addr,
bus_write_data, //data_in
bus_read_data //data_out
);

input pclk, nreset, bus_write_en, bus_read_en;
input [7:0] bus_addr;
input [31:0] bus_write_data;
output reg [31:0] bus_read_data;

reg [31:0] overflowReg;
reg [31:0] counterReg;
reg [31:0] controlReg;
reg [31:0] nextCounter;
reg overflowReset; // Resets counterReg when new overflow value is written

wire timerEn; // Timer Enable

//Control Bits
assign timerEn = controlReg[0];

always@(posedge pclk)
if(~nreset)
begin
overflowReset <= 1'b0;
controlReg <= 32'h00000000;
overflowReg <= 32'h00000000;
end
else begin
if(bus_write_en) begin : WRITE
case(bus_addr[3:2])
2'b00: // Timer Overflow Register
begin
overflowReg <= bus_write_data;
overflowReset <= 1'b1;
end
2'b01: // Timer Value, Read Only
begin
overflowReset <= 1'b0;
end
2'b10: // Timer Control
begin
controlReg <= bus_write_data;
overflowReset <= 1'b0;
end
2'b11: // Spare
begin
overflowReset <= 1'b0;
end
endcase
end
else if(bus_read_en) begin : READ
case(bus_addr[3:2])
2'b00: // Timer Overflow register
begin
bus_read_data <= overflowReg;
  end
2'b01: // Timer Value, Read Only
begin
bus_read_data <= counterReg;
end
2'b10: // Timer Control
begin
bus_read_data <= controlReg;
end
2'b11: // Spare
begin
end
endcase
end
else
overflowReset <= 1'b0;
end

assign timerEn = controlReg[0];

always@*
nextCounter <= counterReg + 1;

always@(posedge pclk)
if(~nreset)
begin
counterReg <= 32'h00000000;
end
else begin
if(overflowReset)
begin
counterReg <= 32'h00000000;
end
else if(timerEn)
begin
if(counterReg == overflowReg)
counterReg <= 32'h00000000;
else
counterReg <= nextCounter;
end
end
endmodule
// timerWrapper.v

module timerWrapper(// APB Bus Interface

PCLK,
PENABLE,
PSEL,
PRESETN,
PWRITE,
PREADY,
PSLVERR,
PADDR,
PWDATA,
PRDATA,
// Test Interface
TPS);

// APB Bus Interface
input PCLK,PENABLE, PSEL, PRESETN, PWRITE;
input [31:0] PWDATA;
input [7:0] PADDR;
output [31:0] PRDATA;
output PREADY, PSLVERR;

// Test Interface
output [4:0] TPS; // Use for your debugging

assign BUS_WRITE_EN = (PENABLE && PWRITE && PSEL);
assign BUS_READ_EN = (!PWRITE && PSEL); //Data is ready during first cycle to make it availble on the bus when PENABLE is asserted

assign PREADY = 1'b1;
assign PSLVERR = 1'b0;

timer timer_0( .pclk(PCLK),
.nreset(PRESETN),
.bus_write_en(BUS_WRITE_EN),
.bus_read_en(BUS_READ_EN),
.bus_addr(PADDR),
.bus_write_data(PWDATA),
.bus_read_data(PRDATA)
);

endmodule

Add APB3 bus core with one slave port. Refer back to lab 3 as neccessary for details. It should look something like this when you are finished. 

You can connect the test points (TPS) to the user IO pins for Debugging.  You do not have to assign the UART pins. They are automatically assigned.

Our timer is very simple and has only 3 registers defined:

Offset Type Reset State Name Description
0x0 Read/Write 0 overflowReg Timer counter gets reset (to 0) when it reaches the overflow value
0x4 Read 0 counterReg Timer Counter Value
0x8 R/W 0 controlReg Timer Control
Bit 0: Enable bit (timerEn)

The control bit 0 enables or disables the incrementing counter. When enabled, the counter will increment up to the value stored in the overflowReg register. Once reached, it resets to 0 and starts counting again. The counterReg register contains the current value of the counter.

Program the FPGA with this design.

Creating a C Project

By specifying SoftConsole as part of the tool flow earlier, Libero will automatically create a Softconsole workspace in the Libero project directory and import drivers for the MSS components. You can open SoftConsole to this workspace by clicking on  Write Application Code in the Libero Tool Flow window. 

SoftConsole should open with the following project directories.

As you can see, a starting point SoftConsole project is automatically generated. The application directory is populated with with a simple while(1) main function and standard includes. The platform directory contains a more comprehensive version of the startup code used in the assembly projects. You do not have specify the linker path or provide the linker.ld file as you did previously.  These are created automatically when the SoftConsole project is created from Libero. Lastly,  the UART drivers are included under drivers for the the MSS UART component.

Enable UART output for printf() in C.

Next, we have to define a C Symbol in the project properties. This symbol tells the compiler to send the output characters of printf() to the UART_0 driver. The driver uses the UART_0 hardware to send the characters to the computer terminal (Hyperterminal or Putty from lab4).

Select the lab5_MSS_MSS_CM3_0_hw_platform, right click on your project and select Properties. Then expand the C/C++ Build entry and select Settings. Under GNU C Compiler select Symbols. On the right hand side, click the paper with the plus sign and add the variable ACTEL_STDIO_THRU_UART.

Lastly, you will not need to include any of the uart.h files. The include paths are all automatically created when you the SoftConsole project is created from Libero. Now you can use standard printf() statements. 

Finally, lets set the -O1 for GCC so that it doesn't generate highly inefficient code and hard to read assembly.

Compile and Debug

Before going any further, make sure your SoftConsole project is working. Add a printf line to your main while loop to print "Hello World" using the following line.

printf("Hello World\n\r");

To build the project, select the applicaiton directory  lab5_MSS_MSS_CM3_0_app, right click and Build Project. Check for errors and correct as necessary. 

To configure the Debugger, select the application directory  lab5_MSS_MSS_CM3_0_app, right click and select Debug As. The configuration should automatically fill in the fields as follows.

Check and see if you are getting a hello message on one of the terminal programs "Fun Terminal" or "PUTTY". Setting should be

Port COM3
BAUD 57600
Data 8 bits
Parity none
Stop bit 1
Flow Control none

Add Timer Functions

Add new files for main.c, mytimer.c, and mytimer.h.

Following is the content for main.c:

#include <stdio.h>
#include "drivers/mss_uart/mss_uart.h"
#include "mytimer.h"

int main()
{
/* Setup MYTIMER */
MYTIMER_init();
MYTIMER_setOverflowVal((1<<31)); // Set compare to a big number
MYTIMER_enable();

while(1)
{
uint32_t i;
printf("Time: %lu\r\n", MYTIMER_getCounterVal()); // Standard printf() now work
for(i=1e6; i>0; i--); // busy wait
}
return 0;
Following is the content for mytimer.h:
#ifndef MYTIMER_H_  // Only define once
#define MYTIMER_H_ // Only define once

#include "CMSIS/a2fxxxm3.h"

#define MYTIMER_BASE (FPGA_FABRIC_BASE + 0x0)

// The technique of using a structure declaration
// to describe the device register layout and names is
// very common practice. Notice that there aren't actually
// any objects of that type defined, so the declaration
// simply indicates the structure without using up any store.

typedef struct
{
uint32_t overflow; // Offset 0x0
uint32_t counter; // Offset 0x4
uint32_t control; // Offset 0x8
} mytimer_t;

#define MYTIMER_ENABLE_MASK 0x00000001UL

// Using the mytimer_t structure we can make the
// compiler do the offset mapping for us.
// To access the device registers, an appropriately
// cast constant is used as if it were pointing to
// such a structure, but of course it points to memory addresses instead.
// Look at at mytimer.c
// Look at the the functions's disassembly
// in .lst file under the Debug folder

#define MYTIMER ((mytimer_t *) MYTIMER_BASE)

/**
* Initialize the MYTIMER
*/
void MYTIMER_init();

/**
* Start MYTIMER
*/

void MYTIMER_enable();
/**
* Stop MYTIMER
*/

void MYTIMER_disable();

/**
* Set the limit to which the timer counts.
*/

void MYTIMER_setOverflowVal(uint32_t value);
/**
* Read the counter value of the timer.
*/

uint32_t MYTIMER_getCounterVal();

#endif /* MYTIMER_H_ */
Following is the content for mytimer.c:
#include "mytimer.h"

void MYTIMER_init()
{
// we don't have to do anything.
}

void MYTIMER_enable()
{
MYTIMER->control |= MYTIMER_ENABLE_MASK;
}

void MYTIMER_disable()
{
MYTIMER->control &= ~MYTIMER_ENABLE_MASK;
}

void MYTIMER_setOverflowVal(uint32_t value)
{
// Yes it's inefficient, but it's written this way to
// show you the C to assembly mapping.
uint32_t * timerAddr = (uint32_t*)(MYTIMER);
*timerAddr = value; // overflowReg is at offset 0x0
}

uint32_t MYTIMER_getCounterVal()
{

// Yes it's inefficient, but it's written this way to
// show you the C to assembly mapping.
uint32_t * timerAddr = (uint32_t*)(MYTIMER);
return *(timerAddr+1); // counterReg is at offset 0x4
}

The technique of using a structure declaration to describe the device register layout and names is very common practice. Notice that there aren't actually any objects of that type defined, so the declaration simply indicates the structure without using up any store.

To access the device registers, an appropriately cast constant is used as if it were pointing to such a structure, but of course it points to memory addresses instead. Using a simple structure, we index all the registers available on the timer peripheral. For example, look at the files in drivers/mss_uart. You will see that they follow a similar structure.

Look at the disassembly in <project_name>.lst file in the Debug folder. Find the assembly that was generated for the MYTIMER_enable(), MYTIMER_setOverflowVal(uint32_t value) , and MYTIMER_getCounterVal() functions. Map the generated assembly back to the C code in the functions.

Don't forget to define the ACTEL_STDIO_THRU_UART to allow printf to work. See above. 

Put a breakpoint on the printf function line in main and restart the debug session. Invoke the instruction stepping feature. Resume the debugger. You will notice that the for loop does not exist in the disassembled code. The C optimizer (-O1 flag) notices that the loop variable i is not used after it is incremented, therefore it gets rid of the i and the for loop that goes with it. You can trick to compiler into keeping i by declearing it to be volatile. Add the volatile keyword before uint32_t i;. It tells the compiler to not make assumptions about i, because it can be used outside of the for loop. Try making the variable volatile. Read through How to Use C's volatile Keyword.

Connect the serial terminal application to the serial port of the eval board. You should now see numbers being printed on your terminal.


Several questions follow in the In-Lab. Use this answer sheet or create your own and provide notes or answers for submission in the Post lab.

Q1: You can see that the numbers overflow after a certain amount of time. Measure that time using a stop watch and calculate the clock frequency. Write down your result. Does it agree with the frequency specified in the MSS Configurator Clock Manager block? Explain in two sentences providing your data and caculations.

Timed Interrupts

So far, your timer unit provides you time through a software interface. We will now add some more functionality. One of the basic functions of a timer is a timed interrupt. You program the timer unit with a specific time, and an interrupt will fire upon reaching that time.

In the last lab, you learned how to create interrupts on the FPGA, and how to process them in the interrupt service routine. Let's add two interrupt sources to our timer, both sharing the same interrupt line FABINT. An interrupt register will indicate in software which interrupt actually fired. This is very similar to the last lab where we had two interrupt sources, the two switches, sharing the FABINT interrupt line.

The two interrupt sources should fire when the timer reaches two specific values:

  1. When the timer overflows and goes back to 0, i.e., when it reaches the Overflow value
  2. When the timer reaches a specific Compare value that is less than the overflow value.
The first interrupt is easy to implement. For the second interrupt, you will have to add a new register to your timer that contains the Compare value.

Together with the interrupts, we will also need some more control bits

  1. InterruptEnable: Enables interrupts
  2. CompareEnable: Enable the Compare interrupt
  3. OverflowEnable: Enable the Overflow interrupt

In summary, we will make the following modifications to the code:

  1. Add the FABINT interrupt inside the MSS Configurator
  2. Add an output interrupt port to the timer.v module
  3. Add a pulsed and synchronized to PCLK interrupt to the module.
  4. Add two new registers to the module. Note that we will have to widen the address bus for this. One register will be the interrupt status, the other the Compare register.
  5. Add the control signals to enable/disable the two interrupts
  6. Add the overflow interrupt logic, making sure to check for the interrupt enable signals.
  7. Add the reset interrupt register and maaking sure that it gets reset upon a read from the Cortex-M3.
  8. Add the compare interrupt and update the interrupt status register.
  9. Update the timer unit in the smart designer, re-add the bus definition, and connect the FABINT.

We perform all but the first step(s) above, use the following verilog code: timerInt.v and timerIntWrap.v. The functionality for these modules is complete. You do not have to modify them. Replace your previous timer.v file with this implementation and update your smart designer correspondingly.

Don't forget to add the FAB_INT to your MSS component and regnereate!

Now our modified timer has 5 registers:

Offset Type Reset State Name Description
0x00 Read/Write 0 overflowReg Timer counter gets reset (to 0) when it reaches the overflow value
0x04 Read 0 counterReg Timer Counter Value
0x08 R/W 0 controlReg Timer Control
Bit 0: Timer Enable bit (timerEn)
Bit 1: Interrupt Enable bit (interruptEn)
Bit 2: Compare Enable bit (compareEn)
Bit 3: Overflow Enable bit (overflowEn)
0x0c R/W 0 compareReg Timer Compare Value (only works if less than Overflow val)
0x10 Read 0 interrupt_status Which interrupt fired?

A few interrupt enable examples.
1) To enable a compare that generates an interrupt, set the interrupt enable bit and the compare overflow bit.
2) To enable both they compare and overflow interrupt, set the interrupt enable bit, the compare bit and the overflow bit.
3)  To enable an overflow that generates an interrupt, set the interrupt enable bit and the overflow enable bit.

SoftConsole Code

Having more functionality in your peripheral necessitates to update your timer driver. Add the following additional functions form this header file to your library. Make sure that the different enable and disable functions really toggle the right bits in the configuration register.:

/**
* Enable all interrupts
*/
void MYTIMER_enable_allInterrupts();
/**
* Disable all interrupts
*/

void MYTIMER_disable_allInterrupts();
/**
* Enable compare interrupt
*/

void MYTIMER_enable_compareInt();
/**
* Disable compare interrupt
*/

void MYTIMER_disable_compareInt();
/**
* Set Compare value
*/

void MYTIMER_setCompareVal(uint32_t compare);
/**
* Enable overflow interrupt
*/

void MYTIMER_enable_overflowInt();
/**
* Disable overflow interrupt
*/

void MYTIMER_disable_overflowInt();
/**
* Interrupt status
*/

uint32_t MYTIMER_getInterrupt_status();

Implement the missing MYTIMER functions in your timer.c file.

Once implemented, replace your main function with the following code. Notice that we added the Fabric interrupt handler too:

uint32_t count;
__attribute__ ((interrupt)) void Fabric_IRQHandler( void )
{
uint32_t time = MYTIMER_getCounterVal();
uint32_t status = MYTIMER_getInterrupt_status();
count--;
printf("Interrupt occurred at %lu FABINT \n\r", time);
if(status & 0x01)
{
printf("Overflow latency %ld\n\r", 0-time);
}
if(status & 0x02)
{
printf("Compare latency %ld\n\r", (1<<27) - time);
}
NVIC_ClearPendingIRQ( Fabric_IRQn );
}
int main()
{
/* Setup MYTIMER */
MYTIMER_init();
MYTIMER_setOverflowVal((1<<28));
MYTIMER_setCompareVal((1<<27)); // 50% of overflow

MYTIMER_enable_overflowInt();
MYTIMER_enable_compareInt();
MYTIMER_enable_allInterrupts();

NVIC_EnableIRQ(Fabric_IRQn);

MYTIMER_enable();

count = 1e6;
while(1){
if(!count){ // Global variable checking
// count decremented inside the interrupt service routine
MYTIMER_disable(); // Disable after 1e6 interrupts
}
}
return 0;
}
Do a clean build and program your eval board with the application and connect the serial terminal. Observe the latency and convert it to seconds, either in your output by modifying the printf functions, or on paper. 

Q2: How does the latency compare to the latency measured with the oscilloscope in Lab 4? Remember that we measured the Fabric Interrupt latency in lab 4 that was initiated with a push button switch. The latency was measured on the scope by measuring the time between the FABINT and a GPIO signal driven high in the interrupt service routine. The timer is measuring latency by starting a  counter when FABINT is asserted and reading the count on entry of the interrupt service routine. The count difference times the clk period (period for 40MHz) is the latency time. Explain in two sentences why these values are different?

Pause the program, turn on Instruction Stepping, and look at the disassembly. Notice that it is looping at a branch to itself loop:

0x200004e4 <main+82>: b.n 0x200004e4 <main+82>

This is basically the while(1) loop in code. This time the C optimizer assumed that count cannot change (it cannot make the connection between count in the interrupt routine and count here.) Therefore, it moves the if(!count) outside of the while(1) and checks count once.  Make count volatile, and notice that it gets rid of the self loop. Use instruction stepping to step through the full loop now. Notice that it loads and checks count every time. Keep this mind when setting the compilier optimizaion using variables.  See How to Use C's volatile Keyword for more information. 

Pulse Width Modulated Signals

We have almost all the pieces together for a Pulse Width Modulated (PWM) signal generator. PWM signals are very common in embedded systems to deliver a variable amount of energy to an end system, such as LEDs, robotic servos, heating plates, etc. There are two parameters to a PWM system: frequency and duty-cycle. The frequency indicates how often we have a positive edge, while the duty-cycle describes the amount of high time vs. low time of the signal, and is usually expressed in percent. For example, a system that has a pulse every second, then stays on high for 100 milliseconds, before going low for 900 milliseconds would have a duty cycle of 10%.

With the timer unit we have, it is really easy to generate a PWM system. We just have to set a signal to high when we get to the Compare value, and set it back to low when we reach Overflow. In that respect, the Overflow value will determine the period, or frequency, of our signal, while the Compare value determines the duty cycle. Software writes the period and duty-cycle values to the Overflow and Compare registers, respectively.

Modify the timer unit and add the PWM output. Add a control register bit that can enable and disable that output. Connect the PWM output to one of the LEDs on your eval board.

Add PWM capabilities to your timer library, and write an application that generates a PWM signal at 100Hz and changes the duty cycle from 0 to 100% over 1 second. Observe the signal on an oscilloscope and the LED. Describe your observation in a few sentences and include a screenshot of the oscilloscope. Include this with the answers to your questions. 

Capturing Time

The compare capabilities of a timer allow timed events to happen. The Capture capabilities allow the inverse, i.e., they measure the time of external events. As you saw in the last lab, the handling of interrupt service routines can have significant latency and jitter. Thus, we can not just read the time in software once we entered an interrupt service routine. The capture unit instead grabs the current time inside the timer unit and stores it in a register. It then signals to the core using the timer interrupt that a capture happened. The software can then, at its own convenience, read the capture register.

Let's add capture capabilities to our timer unit:

Use the following file as a reference to implement the above capabilities: timerCapt.v and timerWrapCapt.v. This code is complete. You do not have to modify it. Make sure you connect one of the switches to the capture input. Synthesize, place and route, and program the eval board with your new code.

We now have to add the new capabilities to our timer library. Implement the following new functions in your mytimer.c file.

/**
* Enable Capture
*/
void MYTIMER_enable_capture();
/**
* Disable Capture
*/
void MYTIMER_disable_capture();

/**
* Read the synchronous capture value
*/

uint32_t MYTIMER_get_sync_capture();
/**
* Read the asynchronous capture value
*/

uint32_t MYTIMER_get_async_capture();

Keep in mind that the count register is reset when the count register equals the overrun register. Initialize the overrun register to a value greater than your expected count range (or just max value 0xFFFFFFFF) to allow the counter operation.

Write a little application that upon pressing the switch outputs the synchronous and asynchronous capture times, including the difference between them.

Q3: What do you observe between the synchronous and asynchronous capture values? Explain in a couple of sentences and provide  output examples.

Post-Lab Assignment

For the post lab assignment  you will develop a simple range measurement system that uses an ultrasonic sensor.  The sensor works by measuring the time it takes a sound pulse to travel from the transducer to the target and back.  To use the sensor, you simple provide a 10us pulse to a "start" pin then observe a response pulse on the "echo" pin. The distance to the target is proportional to the high time of the echo pulse given by the following equation.

distance (cm) = echo pulse width (us) /58

A typical signal sequence is shown below.



Your final design should provide an output using the terminal window showing the distance to a target in cm.  If there is no target detected (a measurement of greater than 25ms) the output should indicate no target detected.

Design Suggestions
1) Generate the Trigger Signal with either the PWM hardware provided in lab 3. Remember, the clock source is 40 MHz.

2) Measure the Echo Signal with the capture timer.
To do this you will need to generate a pulse on the rising and falling edge of the echo signal to create the capture signal. The following Verilog creates a pulse 3 clock cycles long at the rising and falling edges of the input signal. The reason the pulse is several clock cycles high is so it can be sampled by the capture sync logic.
module pulser(
input sig,
input pclk,
output edges);

reg [5:0] syncer;

always@(posedge pclk)
begin
/* syncer <= (syncer<<1) + sig; */

syncer[0] <= sig;
syncer[1] <= syncer[0];
syncer[2] <= syncer[1];
syncer[3] <= syncer[2];
syncer[4] <= syncer[3];
syncer[5] <= syncer[4];
end
/* create a pulse 3 clocks high on the rising and falling edges of sig */
wire [2:0] temp = (syncer[2:0] & ~syncer[5:3]) | (~syncer[2:0] & syncer[5:3]);
assign edges = temp[0];
endmodule




If you observe the output signal edge with respect to the echo signal you should see something like the following diagram. 



3) You should be generating an interrupt on every capture event, so read the capture register on successive pulses and take the difference to determine the pulse width. You will not know which two successive edge pulses you are measuring between so you will have to do some calculations in the software to determine this. (HINT: the echo signal will not exceed 25ms if there is a target present.

Physical Connections to the Sensor
You will need to connect the sensor to user IO pins  to generate the Trigger and measure the Echo signals. You may choose any pins that are convenient.

You will also need to supply GND and 5 volts to the sensor. Connect them to the GND and 5 volt pins (VCC) of the expansion board. Plug the sensor into the protoboard and use headers pins and jumpers like you did in previous labs to make the connections.

The signals are labeled on the sensor.





DEMONSTRATION : Demonstrate the operation of your measurement system to the lab staff.