timers Getting Started in Embedded Programming Pt 6
Here I am on a clear cool evening, by the fire outside with my laptop. Tonight I will talk about a new peripheral, the timer/counter. Periodically I will be interrupted to put another log on my fire, but it should not slow me down too much.
Timers and counters are almost the same peripheral with only the difference of what is causing the counter to count. If the counter is incrementing from a clock source, it becomes a timer because each count registers the passage of a precise unit of time. If the counter is incrementing from an unknown signal (perhaps not even a regular signal), it is simply a counter. Clearly, the difference between these is a matter of the use-case and not a matter of design. Through there are some technical details related to clocking any peripheral from an external "unknown" clock that is not synchronized with the clocks inside the microcontroller. We will happy ignore those details because the designers have done a good job of sorting them out.
Let us take a peek at a very simple timer on the PIC16F18446. Turn to page 348 of your PIC16F18446 data sheet and take a look at figure 25-1. (shown below)
This is the basic anatomy of a pretty vanilla timer. Of course most timers have many more features so this article is simply an introduction. On the left side of this image there are a number of clock sources entering a symbol that represents a multiplexer. A multiplexer is simply a device that can select one input and pass it to its output. The T0CS<2:0> signal below the multiplexer is shorthand for a 3-bit signal named T0CS. The slash 3 also indicates that that is a 3-bit signal. Each of the possible 3-bit codes is inside the multiplexer next to one of the inputs. This indicates the input you will select if you apply that code on the signal T0CS. Pretty simple. The inputs are from top to bottom (ignoring the reserved ones) SOSC (Secondary Oscillator), LFINTOSC (Low Frequency Internal Oscillator), HFINTOSC (High Frequency Internal Oscillator) Fosc/4 (The CPU Instruction Clock) an input pin (T0CKI) inverted or not-inverted.
Let us cover each section of this peripheral in a little more detail. Of course you should go to the data sheet to read all the information.
SOSC
The secondary oscillator is a second crystal oscillator on 2 I/O pins. A crystal oscillator is a type of clock that uses a piece of quartz crystal to produce a very accurate frequency. The secondary oscillator is designed to operate at 32.768kHz which by some coincidence is 2^15 counts per second. This makes keeping accurate track of seconds very easy and very low power. You could configure the hardware to wake up the CPU every second and spend most of your time in a low power sleep mode.
LFINTOSC and HFINTOSC
There are two internal oscillator in the PIC16F18446. The LFINTOSC is approximately 31kHz and is intended for low power low speed operation but not very accurate timing. The HFINTOSC is adjustable from 1-32MHz and is better than 5% accurate so it is often sufficient for most applications. Because these two oscillators are directly available to the timer, the CPU can be operating at a completely different frequency allowing high resolution timing of some events, while running the CPU at a completely different frequency.
FOSC/4
This option is the simplest option to select because most of the math you are doing for other peripherals is already at this frequency. If you are porting software for a previous PIC16 MCU, the timer may already be assumed to be at this frequency. Due to historical reasons, a PIC16 is often clocked at 4MHz. This makes the instruction clock 1MHz and each timer tick is 1us. Having a 1us tick makes many timing calculations trivial. If you were clocking at 32MHz, each tick would be 31ns which is much smaller but does not divide as nicely into base 10 values.
T0CKI
This option allows your system to measure time based upon an external clock. You might connect the timing wheel of an engine to this input pin and compute the RPM with a separate timer.
Prescaler
After the input multiplexer, there is an input pre-scaler. The goal of the pre-scaler is to reduce the input clock frequency to a slower frequency that may be more suitable for the application. The most prescallers are implemented as a chain of 'T' flip-flops. A T flip-flop simply changes its output (high to low or low to high) on each rising edge of an input signal. That makes a T Flip-Flop a divide by 2 circuit for a clock. If you have a chain of these and you use a multiplexer to decide which T flip flop to listen to, you get a very simple divider that can divide by some power of 2. i.e. 2, 4, 8, 16... with each frequency 1/2 of the previous one.
Synchronizer
The synchronizer ensures that input pulses that are NOT sourced by an oscillator related to FOSC are synchronized to FOSC. This synchronization ensures reliable pulses for counting or for any peripherals that are attached to the counter. However, synchronization requires the FOSC/4 clock source to be operating and that condition is not true in when the CPU is saving power in sleep. If you are building an alarm clock that must run on a tiny battery, you will want the timer to operate while the CPU is sleeping and to produce a wakeup interrupt at appropriate intervals. To do this, you disable synchronization. Once the CPU has been awakened, it is a good idea to activate synchronization or to avoid interacting with the counter while it is running.
TMR0 Body
The TMR0 body used to be a simple counter, but in more recent years it has gained 2 options. Either, the timer can be a 16-bit counter, or it can be an 8-bit counter with an 8-bit compare. The 8-bit compare allows the timer to be reset to zero on any 8-bit value. The 16-bit counter allows it to count for a longer period of time before an overflow. The output from the TMR0 body depends upon the module. In the 8-bit compare mode, the output will be set each time there is a compare match. In the 16-bit mode, the output will be set each time the counter rolls from 0xFFFF to 0x0000.
Output
The output from the core can be directed to other peripherals such as the CLC's, it can also be sent through a postscaler for further division and then create an interrupt or toggle an output on an I/O pin. The postscaler is different than the prescaler because it is not limited to powers of two. It is a counting divider and it can divide by any value between 1 and 16. We shall use that feature in the example.
Using the Timer
Timers can be used for a great number of things but one common thing is to produce a precise timing interval that does not depend upon your code. For instance, 2 lessons ago, we generated a PWM signal. The one way to do this was to set and clear the GPIO pin every so many instruction cycles. Unfortunately, as we added code to execute the PWM would get slower and slower. Additionally, it could get less reliable because the program could take different paths through the code. Using the PWM peripheral was the perfect solution, but another solution would be to use a timer. For instance, you could configure the timer to set the output after an interval. After that interval had elapsed, you could set a different interval to clear the output. By switching back and forth between the set interval and the clear interval, you would get a PWM output. Still more work than the PWM peripheral, but MUCH better than the pure software approach.
For this exercise we will use the timer to force our main loop to execute at a fixed time interval. We will instrument this loop and show that even as we add work to the loop, it still executes at the same time interval. This type of structure is called an exec loop and it is often used in embedded programming because it ensures that all the timing operations can be simple software counting in multiples of the loop period.
And here is the program.
void main(void) { TRISAbits.TRISA2 = 0; // Configure the TRISA2 as an output (the LED) T0CON1bits.ASYNC = 0; // Make sure the timer is synchronized T0CON1bits.CKPS = 5; // Configure the prescaler to divide by 32 T0CON1bits.CS = 2; // use the FOSC/4 clock for the input // the TMR0 clock should now be 250kHz TMR0H = 250; // Set the counter to reset to 0 when it reaches 250 (1ms) TMR0L = 0; // Clear the counter T0CON0bits.T0OUTPS = 9; // Configure the postscaler to divide by 10 T0CON0bits.T0EN = 1; // turn the timer on // the timer output should be every 10ms while(1) { LATAbits.LATA2 = 0; // Turn on the LED... this allows us to measure CPU time __delay_ms(5); // do some work... could be anything. LATAbits.LATA2 = 1; // Turn off the LED... Any extra time will be while the LED is off. while(! PIR0bits.TMR0IF ); // burn off the unused CPU time. This time remaining could be used as a CPU load indicator. PIR0bits.TMR0IF = 0; // clear the overflow flag so we can detect the next interval. } }
I chose to use a delay macro to represent doing useful work. In a "real" application, this area would be filled with all the various functions that need to be executed every 10 milliseconds. If you needed something run every 20 milliseconds you would execute that function every other time. In this way, many different rates can be easily accommodated so long as the total execution time does not exceed 10 milliseconds because that will stretch a executive cycle into the next interval and break the regular timing.
Special Considerations
One interesting effect in timers is they are often the first example of "concurrency issues" that many programmers encounter. Concurrency issues arise when two different systems access the same resource at the same time. Quite often you get unexpected results which can be seen as "random" behavior. In the code above I configured the timer in 8-bit mode and took advantage of the hardware compare feature so I never needed to look at the timer counter again. But let us imagine a slightly different scenario. Imagine that we needed to measure the lap time of a race car. When the car started the race we would start the timer. As the car crossed the start line, we would read the timer BUT WE WOULD NOT STOP IT. When the car finished the last lap, we could stop the timer and see the total time. IN this way we would have a record for every lap in the race. Simply by subtracting the time of completion for each lap, we would have each lap interval which would be valuable information for the race driver. Each time we read the timer without stopping it, we have an opportunity for a concurrency issue. For an 8-bit timer we can read the entire value with one instruction and there are no issues. However, the race is likely to last longer than we can count on 8-bits so we need a bigger timer. With a 16-bit timer we must perform 2 reads to get the entire value and now we encounter our problem.
In the picture above I have shown two scenarios where TMR0 in 16-bit mode is counting 1 count per instruction cycle. This is done to demonstrate the problem. Slowing down the counting rate does not really solve the problem but it can affect the frequency of the issue. In this example the blue cell indicates the first read while the red cell indicates the second read to get all 16-bits. When the counter was 251, the reads are successful, however when the counter is 255, the actual value we will read will be 511 which is about 2x the actual value. If we reverse the read order we have the same problem. One solution is to read the high, then the low and finally, read the high a second time. With these three data points and some math, it is possible to reconstruct the exact value at the time of the first read. Another solution is in hardware.
In the data sheet we see that there is some additional circuitry surrounding TMR0H. With this circuitry, the TMR0 will automatically read TMR0H from the counter into a holding register when TMR0L is read. So if you read TMR0L first and then TMR0H you will NEVER have the issue. Now consider the following line of C.
timerValue = TMR0
It is not clear from just this line of code which byte of TMR0 is read first. If it is the low byte this line is finished and perfect. However, if it is the high byte, then we still have a problem. One way to be perfectly clear in the code is the following:
timerValue = TMR0L; timerValue |= TMR0H << 8;
This code is guaranteed to read the registers in the correct order and should be no less efficient. The left shift by 8 will probably not happen explicitly because the compiler is smart enough to simply read the value of TMR0H and store it in the high byte of timerValue.
These concurrency issues can appear in many areas of computer programming. If your program is using interrupts then it is possible to see variables partially updated when an interrupt occurs causing the same concurrency issues. Some bigger computers use real-time operating systems to provide multi-tasking. Sharing variables between the tasks is another opportunity for concurrency issues. There are many solutions, for now just be aware that these exist and they will affect your future code.
Timer 0 is probably the easiest timer on a PICmicrocontroller. It has always been very basic and its simplicity makes it the best timer to play with as you learn how they work. Once you feel you have mastered timer 0, spend some time with timer 1 and see what additional features it has.
Once again, the project is in the attached files.
Good Luck.
0 Comments
Recommended Comments
There are no comments to display.