NU32v2: Counters and Timers

From Mech
Jump to navigationJump to search

THIS PAGE REFERS TO A PRE-RELEASE VERSION OF THE NU32 PIC32 DEVELOPMENT BOARD. FOR INFORMATION, SAMPLE CODE, AND VIDEOS RELATED TO THE PRODUCTION VERSION (2016 AND LATER), AND TO THE CORRESPONDING BOOK "EMBEDDED COMPUTING AND MECHATRONICS WITH THE PIC32 MICROCONTROLLER," VISIT THE NU32 PAGE.


The basic function of counters is quite simple: they count the number of pulses they have received since they were last reset (or last "rolled over"). If the pulses come from a fixed frequency clock, they can be thought of as timers, hence the words counter and timer are often used interchangeably (as in this article). I prefer to call them "counters," but Microchip's documentation mostly refers to them as timers, so we'll adopt that terminology. The pulses that are being counted may come from the internal peripheral bus clock or external sources.

Overview

The PIC32 is equipped with 5 16-bit timers (Timer1, Timer2, Timer3, Timer4, and Timer5). A timer increments based on a clock signal, which may come from an internal source (the peripheral bus clock, 80 MHz for us) or an external source (such as an encoder). Each timer x has an associated pin TxCK for the external source. In addition, a prescaler determines how many clock pulses must be received before the counter increments. If the prescaler is set to 1:1, the counter increments on each clock pulse; if it is set to 1:4, it increments every fourth pulse. Both the clock source type (internal and external) and the prescaler value are set in software.

Each 16-bit timer can count from 0 to 2^16 - 1 = 65535 = 0xFFFF, at which point it "rolls over" to 0 again. The timer can also be made to roll over at any count less than 0xFFFF by setting the period of the timer roll-over in software. We can also configure the timer to generate an interrupt when this roll-over event occurs. In this way, by using the peripheral bus clock as the clock source, we can generate precisely timed events, such as the execution of a control law in feedback control.

These features are common to each of the timers. However, we can separate the timers into two types: Type A and Type B. Added features of each type of timer are indicated below

  • Type A Timer: Only Timer 1 is a Type A timer, and it can take prescaler values of 1:1, 1:8, 1:64, and 1:256. A Type A timer is capable of counting external pulses asynchronously. This means that the counter can be configured to run independently of the peripheral bus clock---the counter can increment even in the absence of the PB clock. (This is in contrast to synchronous counting, where even if the events being counted are external, the counter only increments on the PB clock rising edge,i.e., the external rising edge is "synchronized" to the PB clock.) One use of asynchronous counting is to count even when the CPU is in SLEEP mode and not providing a PB clock signal. Thus a Type A timer can be used in conjunction with the Real-Time Clock and Calendar (RTCC) to keep track of seconds, minutes, hours, days, and years, even when the PIC is in SLEEP mode. To do this, the secondary oscillator pins SOSCO and SOSCI should be connected to a 32.768 kHz oscillator (2^15 pulses every second). We will rarely need the asynchronous counting capability.
  • Type B Timer: Timers 2, 3, 4, and 5 are Type B, and can take prescaler values of 1:1, 1:2, 1:4, 1:8, 1:16, 1:32, 1:64, and 1:256. Type B timers add two useful capabilities: the ability to trigger an analog-to-digital conversion when the timer rolls over, and the ability to cascade two timers to make a single 32-bit timer (Timer 23 and Timer 45). The former capability is useful when collecting analog input signals at fixed time intervals without CPU intervention. The latter capability allows us to count up to 2^32 > 4 billion. In this case, the even numbered timer (Timer 2 or 4) is the master timer, and its roll-over signal is the input to the slave timer (Timer 3 or 5). Thus the even numbered timer contains the least significant 16 bits and the odd numbered timer contains the most significant 16 bits.

Details

The functions of the timers are controlled by special function registers (SFRs). Each of these SFRs has 32 bits on the PIC32, but for many, not all of the bits are relevant. Depending on whether these bits are 0 or 1, different actions or settings are performed for Timer x:

  • TxCON: This SFR configures the behavior of the timer, including whether it is on or not, the prescaler value, the source of the clock (internal or external). For Type A timers, it also indicates whether it will operate in synchronous or asynchronous mode. For even-numbered Type B timers, this SFR determines whether two timers are cascaded to create a 32-bit timer.
  • TMRx: This register stores the 16-bit count. It resets to 0 when the timer reaches the number stored in PRx.
  • PRx: 16-bit "period register": the value where TMRx rolls over (resets to zero).

Counter interrupts are controlled with the following SFRs:

  • IEC0: Five bits of this SFR are relevant to the timers, one for each timer. If the bit is 1, the interrupt is enabled and an interrupt will be generated at timer roll-over. If the bit is 0, no interrupts will be generated. (Other bits in this SFR are reserved for other interrupt sources.)
  • IFS0: Five bits of this SFR are relevant to the timers, one for each timer. If the bit is 1, the timer has generated an interrupt request. If the bit is 0, it has not. Your ISR must set this bit back to zero ("clear the flag").
  • IPCx: Three bits of this SFR specify the priority of the interrupt associated with timer x, and two bits set the subpriority of the interrupt. The priority can take values 1..7, and higher numbers mean higher priority. Thus if a timer with priority 2 generates an interrupt and its interrupt service routine (ISR) is called, then a timer with priority 4 generates an interrupt, the CPU will jump to the higher priority ISR, finish its execution, and then return to the original ISR.

If interrupts are enabled, a timer interrupt is generated when the timer rolls over. Thus if the clock input is a fixed frequency clock (like the PB clock), then the period register PRx, together with the timer prescaler (N >=1), determines the frequency of the interrupts. The period of the interrupt is

Period = (PRx+1)*Tpb*N,

where Tpb is the period of the peripheral bus. Thus for an 80 MHz PB clock, a maximum prescaler value of N=256, and a 16-bit timer, the maximum timer period between interrupts is 65536*(1/80 M)*256 = 0.21 seconds. For a 32-bit timer, it increases to nearly 4 hours.

More details can be found in Section 14 of the Reference Manual.

Library Functions

The peripheral library offers a number of function calls to simplify setting up and using the timers. The declarations of these functions can be found in pic32-libs/include/peripheral/timer.h.

Each timer X (replace X with the timer number you wish to use) has the following functions:

  • OpenTimerX(config, period): where config is a value selected by using bitwise OR | with the constants given in the header file. config chooses whether the time is on or off, the prescaler, the source (internal or external), and whether this timer is the master timer of a 32-bit timer pair. period is the desired PRX value which determines the roll-over period.
  • CloseTimerX(): this disables the timer
  • ConfigIntTimerX(config): this function configures the interrupt based on config which is a value selected by using | with the constants given in the header file.
  • EnableIntTX(): enables the interrupt
  • DisableIntTX(): disables the interrupt
  • SetPriorityIntX(priority): sets the priority of the interrupt
  • ReadTimerX(): returns the value of the timer
  • WriteTimerX(value): loads a given value to the timer
  • ReadPeriodX(): returns the PRX value
  • WritePeriodX(value): loads a given value to the PRX register

Timers 2 and 3 as well as 4 and 5 can be combined to create 32-bit timers. If using a 32-bit timer, replace the X in the functions above with 23 or 45.

Sample Code

Accurate Timing

Here's a simple example of code to time how many cycles it takes to compute a code snippet (provided it's less than 2^16 cycles; otherwise we need to cascade two timers).

  int elapsed; float time; int i;
  OpenTimer2(T2_ON | T2_SOURCE_INT | T2_PS_1_8, 0xFFFF); // will overflow in 1/(80000000Hz/8/65535) = 0.0066s

  WriteTimer2(0);
  for (i=0;i<10000;i++) {}
  elapsed = ReadTimer2();
  time = (float)elapsed / 80000000.0 * 8.0;
  sprintf(RS232_Out_Buffer, "counting to 10000 takes %d timer cycles \tor %f s\r\n", elapsed, time);
  WriteString(UART3, RS232_Out_Buffer);

Time-based Interrupts

For each interrupt, we need to write an interrupt service routine (ISR), which basically means when an interrupt is generated jump to and execute a given set of code before returning. The standard way to write an ISR is the following

void __ISR(vector, iplx) yHandler(void){

// code to execute

// clear the interrupt flag
   function to clear interrupt flag
}

where vector refers to a constant defined in the .h file for your particular processor (p32mx795f512l.h), iplx is the interrupt priority level where x is the level (1..7, higher numbers indicating higher priority). yHandler can be anything you want, for example we can call the timer2 handler as Timer2Handler. There is a function associated with each interrupt that is used to clear the flag, which allows the program to return to where it was before the interrupt was generated. Without clearing the flag, the program will never exit the interrupt.

This example is going to toggle 2 LEDs at a given frequency using interrupts with timer 2. We are going to use the functions OpenTimer2() and ConfigIntTimer2() to initialize the timer and interrupts. You will see in the example code that there is also a function INTEnableSystemMultiVectoredInt() which is used to set up multi-vectored interrupts.

/* Timer_Simple.c
 *
 * This example toggles 2 LEDs on and off using a timer interrupt. 
 * 
 */
#include <plib.h>
					
#define SYS_FREQ  	80000000		// clock frequency set by bootloader
#define LED_0		LATGbits.LATG12 // mnemonic constants for LEDs on NU32v2
#define LED_1		LATGbits.LATG13

// Define constants for the interrupt frequency (Hz) and the prescalar
// Note that (SYS_FREQ / INT_FREQ / PRE_SCALER - 1) needs to be less than 2^16-1
#define INT_FREQ	5
#define PRE_SCALER	256		

void initLEDs(void);

//int count = 0;

int main(void) {
  // SYSTEMConfig() optimizes performance for our clock frequency.
  // in Microchip\MPLAB C32\pic32-libs\include\peripheral\system.h
  SYSTEMConfig(SYS_FREQ, SYS_CFG_ALL);
    
  initLEDs(); 	// set LED pins as outputs

  LED_0 = 1; // low output turns an LED on; see NU32v2 circuit
  LED_1 = 0; // high output turns an LED off
	
  // configure Timer 2 using internal clock with given prescaler
  OpenTimer2(T2_ON | T2_SOURCE_INT | T2_PS_1_256, SYS_FREQ/INT_FREQ/PRE_SCALER - 1);

  // set up the timer interrupt with a priority of 2
  ConfigIntTimer2(T2_INT_ON | T2_INT_PRIOR_2);

  // enable multi-vector interrupts
  INTEnableSystemMultiVectoredInt();  

  while(1) {		// infinite loop
  }
  return 0;
} // end main

/* initLEDs()
 *
 * initLEDs sets RG12 and RG13 to outputs to control the LEDs on the NU32v2.
 */
void initLEDs(void) {
  LATG |= 0x3000;		// Initializes G12 and G13 to 1 (off for NU32v2 board)
  TRISG &= 0xCFFF;	// set the two pins to inputs
				
}

/* ISR for Timer 2*/
void __ISR(_TIMER_2_VECTOR, ipl2) Timer2Handler(void){
    
  LED_0 = ~LED_0; // toggle LED_0
  LED_1 = ~LED_1;	// toggle LED_1

  // clear the interrupt flag
  mT2ClearIntFlag();
}

From this example, we can see how to set up timer 2 at a frequency of 5 Hz with an interrupt priority of 2.

Now let's modify this simple time-based ISR to toggle the leds at two fixed time periods, one at period T, other at period kT, where k is an integer, by using a counter i that increments each time the ISR is called, and when i reaches k, does the less frequent code and sets i back to zero. For this example, create a global int count and initialize it to zero.

 int count = 0;

Use this ISR instead of the previous, in order to have LED0 toggle at 5 Hz and LED1 toggle at 1 Hz.

/* ISR for Timer 2*/
void __ISR(_TIMER_2_VECTOR, ipl2) Timer2Handler(void){
    
  LED_0 = ~LED_0; // toggle LED_0  
  
  count++;
	
  if(count == 5){
    LED_1 = ~LED_1;	// toggle LED_1
		count = 0;
		}
  // clear the interrupt flag
  mT2ClearIntFlag();
}

Imagine that we wanted to toggle the LEDs at 1 Hz by using a 32-bit timer. We can combine timers 2 and 3 or timers 4 and 5 to create a 32-bit timer. Let's use a prescaler of 1:1, the period register then should be PR = (1 sec) (80000000 Hz) (1) -1 = 79999999 < 2^32 - 1.

Replace the timer initialization with

 // configure Timer 2 as 32-bit timer using internal clock, 1:1 prescaler
OpenTimer23(T23_ON | T23_SOURCE_INT | T23_PS_1_1, 79999999);	

// set up the timer interrupt with a priority of 2
ConfigIntTimer23(T23_INT_ON | T23_INT_PRIOR_2);

Replace the ISR with

// ISR for Timer 23	
// Note that the interrupt vector needs to be _TIMER_3_VECTOR
void __ISR(_TIMER_3_VECTOR, ipl2) Timer23Handler(void) {
		
  LED_0 = ~LED_0; // toggle LED_0
  LED_1 = ~LED_1; // toggle LED_1

  // clear the interrupt flag
  mT23ClearIntFlag();
}

In summary, this example shows you how to set up a time based interrupt with a given frequency and prescaler. It also shows you how to create a 32-bit timer.

External Counter

This code is going to set up an external counter to count the number of high pulses on the T5CK pin. The counts will be displayed in binary on the NU32v2 board and will reset after 4 button pushes. You need a simple circuit consisting of 3.3V connected to a switch connected to a resistor (1K) connected to ground. Connect a wire from T5CK to the junction of the resistor and the switch.

The main elements of this code are the initialization

OpenTimer5( T5_ON | T5_PS_1_1 | T5_SOURCE_EXT, 0xFFFF);

which sets up Timer 5 with an external clock source and a prescaler of 1:1. The PR value is the value that you want the counter to roll-over / generate an interrupt. We will not be setting up interrupts, so this number does not really matter.

We will read the counter with the function

 ReadTimer5()

We will reset the counter to zero with

WriteTimer5(0);

More Information

There is another timer available for our use: the core timer associated with the CPU. This counter increments by one every two clock cycles (see Section 2 of the Reference Manual). The current value can be accessed using ReadCoreTimer(), which returns an unsigned int. We can also write a value to the counter using WriteCoreTimer(unsigned int). You can set the period of the timer in the COMPARE register using OpenCoreTimer(unsigned int), and create interrupts on the period overflow.