Lesson 11: Timing Events

Back in lesson 8, we learned about the MSP430 timer module and created a timer library which is being used to blink the LED. The first capture/compare block of Timer_A1 is set to compare mode, meaning that the timer ticks away and is compared against the user defined value in TA1CCR0. When the two values are equal an interrupt fires, resets the counter and invokes our timer library from the ISR. In this lesson, we will learn how to configure the second capture/compare block in the same timer to capture mode. In this mode, the timer can be triggered by a configurable event (either hardware or software) such that when the event is detected, the current timer value is copied to the TA1CCR1 register where it can be read by the software. This mode of operation can be used to capture timestamps for events, or time the difference between two events. We will use the capture mechanism to create a simple stopwatch. The existing timer module will be updated to configure the capture block and associated interrupts and a new menu option will be added to control the stopwatch.

Configuring the timer capture block

Since we are reusing Timer_A1 from lesson 8, we will briefly review the configuration of the timer:

  • the timer module is clocked at MCLK/2 = 500000Hz
  • the timer module set to ‘up’ mode
  • TA1CCR0 is set to 50000 cycles which results in an interval of 100ms
  • each timer interval triggers an interrupt which increments the _timer_tick variable

Based on our existing code, we already have a coarse method to measure time using the value of the _timer_tick variable. By saving the value of _timer_tick at two points in time and then subtracting them, we could implement a stopwatch with 100ms resolution. However, our stopwatch will require better resolution than this. What is the highest resolution we can attain? We know that the frequency of the timer module is 500000Hz, therefore the period of the timer is 2us (microseconds) – the maximum resolution we could theoretically support. However, in practice this level of accuracy for a human triggered stopwatch is meaningless since the average reaction time for a human is in the hundreds of milliseconds (Source: Google. Note: if we were making an optical triggered stopwatch, a resolution in  the microseconds may be feasible). Our stopwatch will be limited to millisecond resolution.

To implement a higher resolution stopwatch, we will use the existing _timer_tick (100ms) in conjunction with the capture feature of the timer module. The timer will still use the interval set in TA1CCR0 even though we will be configuring the second capture/compare block to capture mode. Therefore, the captured value, which will be saved by the hardware in TA1CCR1, will always be between 0 and 100ms. By combining the two values we can obtain millisecond resolution.  The following code should be inserted at the end of timer_init (after enabling CCIE) in src/timer.c to initialize capture block 1:

    TA1CCTL1 = CM_3 | CCIS_2 | SCS | CAP | CCIE;

This line of code sets the second capture/compare block to capture mode. The trigger is configured to be software initiated. The capture can be set to synchronous or asynchronous mode. Synchronous mode means that the capture will occur on the next clock cycle, i.e. the capture is synchronous with the clock. An asynchronous capture means that the capture occurs immediately, potentially asynchronously to the clock. TI recommends using synchronous mode in order to avoid race conditions, so we will adhere to this recommendation. Finally, the interrupt is enabled so that the code can determine when the capture is complete.

You might be wondering why we need the interrupt at all when the code could just initiate a capture and poll until it is done. Initially I thought this would be simplest implementation, however I later determined that it opens the door to a potential race condition. Remember that even while the capture is occurring, the timer is still running. It is possible that the timer reaches TA1CCR0 and the compare interrupt fires. If this happens, the _timer_tick variable will increment and would not be representative of the the value exactly when the capture occurred. Therefore, no matter when the software reads the _timer_tick, it is at risk of obtaining an incorrect value. By using the capture interrupt, the code will enter the ISR which ensures that the compare interrupt won’t fire until the ISR is complete. During this time the value of _timer_tick and the captured value can be stored for processing later.

Before jumping into the capture routine, we will define a new structure used to store the time.

struct time
{
    unsigned int sec;
    unsigned int ms;
};

If we simply used an unsigned integer to store the captured time in milliseconds, the measurable time would be limited to a duration of 65535ms – just over a minute – before an overflow of the 16-bit integer occurs. This limitation wouldn’t make our stopwatch particularly useful. There is the option of using a 32-bit integer instead which would accommodate a much larger range. However, since it is not the native type of the CPU, it would impose a significant performance impact on any calculations which the application may have to perform using it. Instead, we can separate the measurement into seconds and milliseconds. This form also makes it simpler for the application to display time in a standard format. The new function timer_capture will perform the capture, calculate the total number of milliseconds since the timer started, and then format the value to fit into the time structure which will be passed back to the application.

int timer_capture(struct time *time)
{
    int err = -1;

    if (time != NULL ) {
        uint32_t ms;

        /* Toggle the capture input select to trigger a capture event */
        TA1CCTL1 ^= 0x1000;

        /**
         * Wait for the capture to complete
         */
        while (_capture_flag == 0);

        /* Save the number of ms from the timer tick */
        ms = (uint32_t) _capture_tick * 100;

        /* Save captured timer value in ms */
        ms += ((uint32_t) _capture_taccr1 * 2) / 1000;

        /* Save the number of milliseconds */
        time->ms = ms % 1000;

        /* Save number of seconds */
        time->sec = ms / 1000;

        /* Reset _capture_flag for next capture */
        _capture_flag = 0;

        err = 0;
    }    

    return err;
}

The application is responsible for allocating the time structure and passing it to the timer_capture function. If the pointer to the structure is valid, the capture input select is toggled. Since the capture is software initiated, the input needs to be manually toggled between GND and Vcc. The capture block is configured to trigger on both the rising and falling edges so each toggle will result in a capture. Even though the capture interrupt is being used the function is blocking, meaning that it must wait for the capture to complete before returning. Therefore, it needs some mechanism to determine when the capture has completed. At this point it is important to note the three variables which have been added to the file:

static volatile uint16_t _capture_tick = 0;
static volatile uint16_t _capture_ta1ccr1 = 0;
static volatile int _capture_flag = 0;

The variable _capture_tick will be used to store the value of _timer_tick when the capture occurs while _capture_ta1ccr1 will store the value captured in TA1CCR1. The variable _capture_flag will indicate that the capture is complete. All of these will be set in the ISR which we will look at shortly.

When the capture is complete (_capture_flag is set), the value from _capture_tick can be converted to milliseconds by multiplying by 100. Then, the _capture_ta1ccr1 can be converted to milliseconds by multiplying by 2 (remember the timer period is 2 microseconds) and then dividing by 1000. There is a very important concept here that must be well understood. Either of these calculations could result in an integer overflow if the value is sufficiently large. In the first calculation, the _capture_tick only needs to be 655 or greater (just over a minute) before multiplying by 100 would result in an integer which does not fit in 16 bits. Similarly, multiplying _capture_ta1ccr1 by 2 would cause an overflow when the value is above 32767. You might be wondering why not divide by 1000 first in order to avoid this overflow. Well, that could impact the accuracy of the calculation. Let’s quickly take a look at how this can happen. Let say we have 23593 in _capture_ta1ccr1. Multiply by two and divide by 1000 using a calculator and the result is 47.186, which represented as an integer would result in 47ms – only 0.186ms of error. Now turn that calculation around and divide by 1000 first. The result of the division is 23.593, which gets truncated to 23 since it is an integer value. Then multiply by 2 to obtain the millisecond value of 46 – over 1 millisecond of error. Ok, so its really not much error compared to the reaction time of the person controlling the stopwatch,  but it’s a principle you have to be aware of when performing calculations.

So how do we address these integer overflows? One solution is to cast the 16-bit integer to a 32-bit integer. The MSP430 does not have native support for 32-bit integers, however the compiler has functions in it’s math libraries which can handle 32-bit multiplication, division, etc… There may be a substantial performance impact, but since these calculations are not time sensitive the accuracy takes precedent. To correctly perform these calculations, we declare the variable ‘ms’ where the result will be stored as a uint32_t, which is guaranteed to be 32 bits wide. Then every time the 16-bit variable is used, it must be casted to a uint32_t as well. If there is a calculation with more than a single operation, the intermediate value may overflow as well. Remember the CPU can only perform one calculation at a time. By casting to uint32_t immediately before the variable in question each time it is used, you are telling the compiler that even the intermediate value should be stored as 32 bits, otherwise it will default to the size of the variable being multiplied (16 bits). Casting the whole expression at the beginning for example:

ms += (uint32_t) (_capture_ta1ccr1 * 2) / 1000;

is wrong since the cast only applies the result of the complex calculation, not the single operation that results in the overflow.

Now that the total number of milliseconds is calculated and stored as a 32-bit unsigned integer, the value can be divided into seconds and milliseconds to fill the time structure. Finally, the _capture_flag is cleared so the next time the function is called it will be initialized correctly.

The last modification to the timer module is the ISR. We need to implement a new ISR because only capture/compare block 0 is serviced by the existing interrupt. The remaining interrupts are all handled by the TAIV interrupt/register.

__attribute__((interrupt(TIMER1_A1_VECTOR))) void timer1_taiv_isr(void)
{
    /* Check for TACCR1 interrupt */
    if (TA1IV & TA1IV_TACCR1) {
        /* Save timer values */
        _capture_tick = _timer_tick;
        _capture_ta1ccr1 = TA1CCR1;

        /* Set capture flag */
        _capture_flag = 1;
    }
}

We check to be sure that the pending interrupt is for the correct source – capture/compare block 1. When reading the TAIV register, keep in mind that the highest priority pending interrupt is automatically cleared when the TAIV register is read from or written to. In the ISR, we save the current value of _timer_tick as well as the captured value. No calculations are done in the interrupt handler to ensure it exits as quickly as possible. Only the _capture_flag is set to indicate to the software that the capture has completed and the saved values are the most recent.

Adding the stopwatch to the menu

The stopwatch will be implemented using the menu. Although the capture module does have the ability to use hardware events to trigger the capture, we do not have any free buttons which are connected to the supported pins. Instead, we are using a software initiated capture which will be triggered by a key press. Pressing any key will take the first capture, and pressing any key again will take the second capture. The difference between the two captures is the result of the stopwatch.

First lets add a new add a new menu option in the main menu called ‘stopwatch’:

static const struct menu_item main_menu[] =
{
    {"Set blinking frequency", set_blink_freq},
    {"Stopwatch", stopwatch},
};

The menu option invokes the following function:

static int stopwatch(void)
{
    struct time start_time;
    struct time end_time;

    uart_puts("\nPress any key to start/stop the stopwatch: ");

    /* Wait to start */
    while (uart_getchar() == -1) {watchdog_pet();}

    if (timer_capture(&start_time) == 0) {
        uart_puts("\nRunning...");

        /* Wait to stop */
        while (uart_getchar() == -1) {watchdog_pet();}

        if (timer_capture(&end_time) == 0) {
            size_t i;
            char time_str[] = "00000:000";
            unsigned int sec = end_time.sec - start_time.sec;
            unsigned int ms = end_time.ms - start_time.ms;

            /* Convert the seconds to a string */
            for (i = 4; (i > 0) && (sec > 0); i--) {
                time_str[i] = sec % 10 + '0';
                sec /= 10;
            }

            /* Convert the milliseconds to a string */
            for (i = 8; (i > 5) && (ms > 0); i--) {
                time_str[i] = ms % 10 + '0';
                ms /= 10;
            }

            /* Display the result */
            time_str[sizeof(time_str) - 1] = '\0';
            uart_puts("\nTime: ");
            uart_puts(time_str);
        }
    }

    return 0;
}

Using the uart_getchar function, we wait until a valid character is received. While waiting, the watchdog must be pet. This may introduce some error, but we know that the watchdog_pet function is small and should execute in the order of microseconds (hint – use objdump to see that the function is only three instructions). Therefore, we can assume this delay will be negligible. When the first key press is received, timer_capture is called to invoke the capture and the result is saved in the start_time variable. This is repeated once more to obtain the end_time. Then the difference between the two is calculated in terms of seconds and milliseconds.

Once we have obtained the result, it can be displayed to the user. The seconds field is an unsigned integer (16 bits) and therefore has a limit of 65535, meaning we need a maximum of 5 digits before the decimal. Since the remainder is in milliseconds, it will require a maximum of 3 digits. The array of characters time_str is sized to accommodate these values when converted to ASCII as well as the colon separator. In each case, starting from the least significant digit and working up, the value can be converted to ASCII by taking the modulus 10 and adding it to the ASCII value of ‘0’. Then the value is divided by 10 to get the next digit and the process is repeated until it can’t be divided any further. As an example, say you want to display the value 53:

53 % 10 = 3
3 + ‘0’= ‘3’
53 / 10 = 5
5 % 10 = 5
5 + ‘0’ = ‘5’
5 / 10 = 0

And now that the value is 0 we stop. The ASCII characters are stored in time_str starting from the least significant digit and moving up. Displaying the ASCII characters in reverse order gives “53”. The same procedure is repeated for the milliseconds value and the string is then printed out to the console so the user can see the result.

Possible sources of error and relationship tor requirement and design

It is important when you design a system to identify any potential sources of error and evaluate them in order to ensure that the design meets your requirements. In the case of the stopwatch, the implementation should be accurate enough such that any error is negligible compared to the error of human reaction time, which as mentioned earlier, is in the hundreds of milliseconds range.

I can identify three potential sources of error, and will justify that the amount of error introduced is negligible.

  1. The error from pressing the button on the PC keyboard to the reception by the MSP430
    • There is some error introduced starting from when the user presses the key until the the UART transmission occurs, the duration of the UART transmission, and finally the interrupt latency at the MSP430. However, since a key press is required to both start and stop the stopwatch, some of these errors cancel out. Both the duration of the UART transmission and interrupt latency are deterministic and constant. Therefore,  the only variable between the two key presses will be the PC. Since the PC is running order of magnitudes faster than our required accuracy, I would consider it safe to assume that the difference will not vary unless the PC resource usage spikes in between starting and stopping the timer. The best way eliminate this error is by using hardware switches connected to the capture input module. You could add buttons and configure the timer to trigger on one of the edges to achieve a more accurate measurement.
  2. The time delay between the software retrieval of a key press and the initialization of the capture
    • Once stopwatch menu option has been invoked, the application waits for the user input. While it is waiting, the watchdog must be pet. This repeats in a loop until there is actually a character to retrieve – i.e. uart_getchar returns a positive value. When the key is pressed, the software could be at any point in this loop. Once the character is received and the software exits the loop, the timer_capture function which will add some additional overhead. However, again this error is deterministic and will be cancelled out. Therefore, there the only error is caused by the while loop. As I mentioned earlier, the watchdog_pet function is only 3 instructions, therefore even with the overhead of the branching instructions in the while loop, it is unlikely the error would ever reach close to 1 ms. That being said, the hardware solution in (1) would eliminate this error  as well.
  3. Inaccuracy of calculations (rounding errors, etc…)
    • The inaccuracy of calculations can play a role in some error. The measurements of the capture time are in microseconds and when performing conversions between microseconds and milliseconds, there will obviously be some loss of accuracy and hence error. However, the error will be on the order of microseconds up to a maximum of <1ms.

All together, the worst case scenario might add an error of a couple milliseconds. Using an extremely good reaction time of 100ms, this would put us in the range of <3% accuracy, which is pretty good. Review the code yourself. If you can identify any more sources of error, let me know in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *