millis
)The goal in this article is to implement a timer which keeps track of ‘microcontroller time’. Inspired by a work project which required that a message was sent each one second for the duration of an operation. The operation could theoretically last for up to 10 minutes.
Arduino users will recognise this as the millis() function, with a far less stringent maximum time requirement. However the implementation will actually provide the same maximum time (around 50 days) as the Arduino version.
The requirement to be reset through application code won’t be utilised in this simple example, but it was necessary for the work project. I’m not sure if Arduino provides an easy way to do this but there are certainly scenarios in which it could be useful.
Checking through the Features section of the datasheet (page 4-5) there are three Peripherals related to time - one type A timer/counter, four type B timer/counters and a real-time counter. From the Table of Contents (page 6-11) these features are covered in sections 20, 21 and 22 (pages 185-276).
There is only one timer of type A. It appears to be quite complex, as it can be set up to trigger things a four different points in the timing cycle, can be set up to count downwards or upwards, etc… Having these four different trigger points could be very useful for something like controlling pulse width modulation (PWM) on multiple pins. For further proof of its complexity, compare the register list on page 197 to the register lists we’ll look at later. Since there is only one timer of this type and it seems to be better suited to tasks other than the simple timing task in which we are interested in, let’s move on.
The ATMEGA4809 has four timers of type B. Checking the Features list for the timers themselves (21.1) one might get the impression that these timers are actually quite complex, since they support up to eight different modes. However, because each of these modes are focused on one very specific tasks, one a mode is selected then the setup is actually very straightforward.
To understand the modes we can read through section 21.3.3.1, but
first it is necessary to understand the five defined values in section
21.3.1 - BOTTOM
, MAX
, TOP
,
CNT
, CCMP
- and the fundamental operation of a
timer/counter such as this.
CNT
is the fundamental timer/counter value. This is
directly incremented by some clock source, which is just an
input that alternates between on and off with a certain frequency. Each
time the clock source switches from negative to positive the counter
value is incremented by one. Figure 21-2 illustrates where the clock
sources can come from - either CLK_PER
(that’s the
peripheral clock, matching the fundamental CPU frequency),
CLK_PER/2
or the type A clock. Since we are not using the
latter, so we can ignore it.
Suppose we use CLK_PER
and suppose we run the CPU at the
maximum frequency of 20 MHz. Then CNT
will be increased by
one every 20000000 times each second. Equivalently the value will be
incremented by one each 50 (= 1/20000000) nanoseconds. Starting at 0 ns
the value be 0, at 50 ns 1, at 100 ns 2, and so on.
Note that we started at 0, which is BOTTOM
, or the
minimum value that the counter can take on. The counter is stored in a
16 register the value 0 occurs when all bits in the register are zero -
we are treating it as a 16 bit unsigned integer. The maximum value of a
16 bit unsigned integer is 2^16 - 1 = 65535 = 0xffff, which is the
MAX
value. That is the maximum possible value that the
counter can reach. If the counter ever reaches this value then the
counter is said to overflow and it will start again at zero.
We do not always want the counter to be able to reach its maximum
value. As we’ll see shortly, when working on an application when a
regular action needs to take place (such as keeping time) the point at
which the moment at which this overflow occurs is one of the moments at
which we can carry out that regular action. Assuming again that the
counter is ‘clocked’ (incremented) at CLK_PER
, if we rely
on the counter reaching MAX
we can only ever carry out that
action each 65536 / 20000000 = 3.2768 microseconds. That’s not very
flexible. We also don’t want to have to deal with fractional values in
microcontroller code if possible because they are very slow and memory
hungry compared to integer values. Additionally, if we can only update a
timer each 3ish milliseconds, then we have no hope of meeting our 1
millisecond precision requirement!
So we find the the value TOP
. TOP
is a user
definable value at which the ‘reset’ event will occur. Suppose we set a
TOP
value of 1000 - then CNT
will never exceed
this value. Instead it will count 0 ns - 0; 50 ns - 1; 100 ns - 2; …;
49950 ns - 999; 50000 ns - 1000; 50050 ns - 0; 50100 ns - 1; … Note
carefully here that, since CNT
starts from 0, counting to
1000 means that the counter actually ‘ticks’ 1001 times, and hence
produces a reset period of 50050 ns and not 50000 ns. By setting the TOP
value the reset period can in theory be anywhere between 50 ns and the
3ish millisecond value earlier.
It is actually not necessary to understand what the CCMP
value means for this application - it is used for some of the other
timer modes that won’t be discussed here.
Returning to the modes section (21.3.3.1), luckily the very first mode listed is the one most suited for our application. Periodic Interrupt Mode is detailed in section 21.3.3.1.1. I’ll quote directly from the datasheet.
In the Periodic Interrupt mode, the counter counts to the capture value and restarts from BOTTOM. A CAPT interrupt and event is generated when the counter is equal to
TOP
.
This is exactly the behaviour we were just discussing - the counter
starts at BOTTOM
then counts to TOP
at which
point ‘something’ happens and the counter is reset to zero. The
‘something’ here is actually two ‘somethings’ in the ATMEGA4809 - one is
an ‘event’ being fired, which we won’t use for this application, other
is an ‘interrupt’ being called, which we will use. The reason that we
won’t use events here is explained a little later.
An interrupt is a piece of code, function, or routine which is run immediately when a certain condition holds or a certain event occurs. Remember that a microcontroller such as this is essentially single threaded and hence can only run one stream of instructions at once. When the piece of code in an interrupt needs to be run immediately, it is necessary to stop executing the code in the main program loop to make way for the interrupt code. The main loop is ‘interrupted’ hence the name.
I’ve slightly oversimplified the situation by saying immediately, since this is not necessarily always true. For the purposes of this application we can however assume that it is executed immediately.
I don’t wish to make this article too tedious, so since the other 7 modes are no useful for this application, let’s not discuss them further.
It is useful to read through the rest of the timer setup documentation to understand what other settings we might need.
In section 21.3.3.2 the output of the timer is described for various
modes. The Periodic Interrupt Timer* that we will be
utilising does not produce a direct output, just the firing of the
interrupt, so we will not need this feature. We see then that the
CCMPEN
bit can be set to 0 to disable outputs.
The Noise Canceler described in 23.3.3.3 states explicitly that this is used for inputs through the events channel. In fact the noise canceler is used to ensure good input when taking input to the clock from some external source, probably through one of the microcontroller pins. Since we are not using events and since we are using an internal clock as the clock source, we will not need to use this feature either.
The following section describes the timer’s events. Events are used in the ATMEGA4809 for direct signalling and communication between two peripheral features (such as the timer and the input/output pins) without the communication having to pass through the CPU. The event system is designed to allow maximum accuracy by passing data as quickly as possible between peripherals and to reduce the burden on the CPU since it no longer has to expend cycles on passing these data. Events are not useful in this application because the timer does not have to communicate with any other peripheral systems - only the CPU clock and the CPU itself.
The section 21.3.5 does not provide much further detail on the interrupt than we have already found. However it does mention two pieces of information we will need when configuring the timer and writing the interrupt code.
The documentation nodes that the INTFLAGS
register has a
flag raised when the interrupt occurs. It notes that the documentation
for the register containing the flags describes how to clear the flags,
which is something we’ll need to watch out for.
It also notes that we must enable the interrupt in the
INTCTRL
register for the timer, so we’ll need to ensure
that we work out how to do that.
We won’t be allowing the device to sleep so we won’t worry about the
sleep mode control. Just note that if we later use these timers in an
application where sleep mode is utilised, we would need to choose
whether to keep the timer running during sleep mode or not, and set the
appropriate bit in the CTRLA
register correspondingly.
Now that we understand how the timer peripheral works, we can devise a plan to utilise it to get the specified functionality. We’ve found a mode which allows us to perform an action at a regular interval, which we can also choose.
The requirements make an interval of 1 millisecond the obvious choice. We cannot choose a longer interval than this and retain millisecond precision. Choosing a smaller value will allow the program to track even less than 1 millisecond intervals, but will also cause the CPU to do extra work with no extra benefit for us.
Recall that the TOP value sets the reset period for the timer. Recall also that we intend to use the CPU clock (at 20 MHz) as the clock source for the timer, which ‘ticks’ each 50 nanoseconds. The number of ticks in a millisecond is thus (1 x 1000 x 1000) / 50 = 20000. The timer needs to reset after 20000 ticks - hence TOP should be set to 19999 (remember that 0 counts as one tick).
The action we take at this regular interval should allow us, in the
application code, to keep track of the total number of milliseconds that
has passed since power up. We’ll need a variable to keep track of this,
I’ll call it milliseconds, which will be initialised to zero at the
start of the program. Recall that it needs to hold 20 minutes worth of
milliseconds, which is 20 x 60 x 1000 = 1200000. Unsigned integers
aren’t guaranteed to be able to contain this value, so we will use an
unsigned long which must be at least 32 bits long. In fact, in modern C
embedded programming its best to be explicit about the size, so we’ll
use a uint32_t
. This can hold values up to 2^32 - 1 =
4294967295. If we convert that into days 4294967295 / (1000 * 60 * 60 *
24) = c. 49.7, which must be where the Arduino 50 days figure arises.
All we need to do in our regularly occurring interrupt code, then, is
increment the milliseconds variable.
To test that the timer has been implemented properly we’ll honor the spirit of every first microcontroller project, and make a blinky! We’ll attach a LED to one of the pins and switch that LED on and off at 500 millisecond intervals (for a total period of one second), based on the timer implementation described below.
Before starting on writing the C code to program the device I find it useful to go through each peripheral register in order to note down: during setup, which bits on which registers must be set to certain values, and which can be left as default; during the main loop which bits/registers might need to be written to/read from.
Note that each register gives a ‘Reset’ value, which is the default value. If the programmer does not set a specific value for the register/bit it will default to this value on power up.
The RUNSTDBY
bit should be set when the peripheral
should continue to run if the device is in standby mode. Since we will
not be using standby mode in this application, this bit can be left with
its default value of 0 (don’t run in standby).
SYNCUPD
is used to synchronise this B type timer with
the type A timer. The type A timer is not being used in this
application, so this can be left with its default value of 0 (don’t
synchronise with timer A).
CLKSEL
is used to set the clock source for the device.
This is the rate at which CNT will increment. The calculations above
were all based on the timer being clocked at the same rate as the CPU,
so we will use CLK_PER
. This requires the two
CLKSEL
bytes to be 0b00
. The other two options
allow half the CPU clock speed and the clock source for timer A to be
used if desired.
The ENABLE
bit must be set to 1 in order for this timer
to actually work.
The value to be put in this register is thus 0b00000001
.
Note that the greyed out bits which don’t correspond to a particular
setting should generally be set to 0.
The ASYNC
bit is used to enable an asynchronous mode
when using single shot mode. Since we are not using this mode, we can
leave this bit as its default value of 0.
CCMPINT
is used to set an initial value when the timer
is connected directly to a pin output. The timer is not directly
controlling pins in our application, so this may be left at its default
value of 0.
The compare/capture output is used by various other modes of the
timer, for operations such as frequency detection or pulse width
measurement. Since nothing is being measured in the mode we’re utilising
for out application, we can leave the CCMPEN
bit as its
default 0.
The CNTMODE
bits are used to set which of the eight
timer modes are being used. We see that periodic interrupt mode requires
a value of 0b000
, so this is what we will set.
A value of 0b00000000
should thus be set in the
CTRLB
register.
This register is used to enable various features related to events such as them events themselves, or noise cancellation. Since this application is not utilising events we will not have to set anything in this register and can just leave it with its default value.
The interrupt control register only has one bit, which we need to set
in order to interrupts to be enabled. Thus we must write the value
0b00000001
to this register.
Bit 0 in this register is set when an interrupt is generated. The
conditions for the interrupt occuring for each mode are explained in
detail here, but as we already know for periodic interrupt mode it will
be fired when CNT
reaches the TOP
value. The
critical takeaway from here is how to clear the interrupt flag. Directly
from the datasheet:
This bit is cleared by writing a ‘1’ to it or when the Capture register is read in Capture mode.
Since we are not in capture mode, the bit must be cleared by writing a one to the register. This must be done as part of the interrupt, otherwise the interrupt will be generated continuously.
This register contains a single bit which can tell us whether the counter is running (1) or not (0). Since the timer will be enabled at all times in our application we will not need to use this register.
Bit 0 in this register is used to determine whether the timer will continue to run and generate events when the CPU is halted during debugging. We won’t be using the on-chip debugger when running this application so we will leave it as default 0 (the timer doesn’t run during debug).
This register is used as temporary storage of a single byte when the
16 bit registers (CNT
and CCMP
) for the
peripheral are read from or written to, to allow that operation to
happen in a single CPU cycle. Implementing this operation is
automatically handled by the compiler/CPU so we won’t need to think
further about this register.
A 16 bit register where the actual counter value CNT
is
stored. We won’t need to read from or write to this register for the
purposes of this program, since all of our timekeeping is handled in the
interrupt, so will allow the timer to manage this variable as it sees
fit.
This register can have a few meanings depending upon the mode chosen. From the datasheet:
In Periodic Interrupt/Time-Out and
Single-Shot mode, this register acts as the
TOP
value
Therefore we will need to load our TOP
value of 19999
into this register in order to produce interrupts at the required
intervals.
First we’ll use the MCC to set up the microcontroller apart from the timer. All settings can be left as default apart from three points that need to be changed.
The CPU prescaler should be set to 1x and not 6x. This setting scales down the incoming oscillations (in this case from the 20 MHz oscillator) before the clock signal reaches the CPU and peripherals, thereby causing the system to run at a reduced speed.
This is where I made my first big mistake and was confused when my blinky was not blinking with the correct period of one second. I assumed the default when using the internal 20 MHz oscillator would be to run the CPU at that frequency. I’m really surprised that the default prescaler is 6x.
Interrupts need to be enabled globally since we wish to use interrupts in our program. I believe if this global setting is not activated then those interrupts will not work at all.
We’ll set one of the pins as an output. Say pin 20 (port D, 0). This is where we will attach the LED.
Clicking on Generate and closing the MCC, a main.c file will have been generated with some mcc header files imported and a call to the initialisation function generated by the MCC inserted into the main function.
In order to have access to some helper variables and functions, we’ll include the io.h and interrupt.h header files provided by Microchip. This will bring convenient definitions to help us access the registers we’ll need to set up and a convenient way to set up the interrupt code.
After the call to the MCC generated initialisation function we’ll start to set up the timer by writing the values we determined earlier to the appropriate registers. Though it’s not necessarily required, out of caution I would generally write all other registers first and the register which includes the timer enable bit last, so everything is in place before the timer is actually turned one.
TCB0.INTCTRL = 0b00000001;
TCB0.CTRLB = 0b00000000;
TCB0.CCMP = 19999;
TCB0.CTRLA = 0b00000001;
Just as a reminder: INTCTRL
enables the interrupt on
‘reset’, CTRLB
disables a series of features used for other
timer modes, CCMP
is the value of the timer where we want
to reset to 0 and sets the interrupt generation period and
CTRLA
sets the clock source (CPU clock, here) and enables
the timer peripheral.
The definitions for these registers (TCB0
,
CTRLB
, …) are brought in from the avr header files that we
included earlier. The approach variable names to use here are quite
straightforward to figure out because they match the terminology used in
the datasheet perfectly.
That should be enough to get the timer running and firing interrupts every 1 millisecond. Now we need to keep track of time based on those interrupts. First we define a variable outside of the scope of the main function and initialise it to zero.
unsigned long milliseconds = 0UL;
Then we will write the code run when the interrupt is fired to increment this counter. This must also come outside of the scope of the main function.
ISR(TCB0_INT_vect) {
milliseconds++;
TCB0.INTFLAGS = 0b00000001;
}
Again the definitions here which were not introduced by us came from the included header files.
ISR stands for interrupt service routine and is used to tell
the compiler which code when a certain interrupt is fired. The interrupt
to which to attach this code is given by the argument - here
TCB0_INT_vect
, the interrupt vector for the first type B
timer - and the code is run is included in a block after.
This ISR is the same one that is commonly used in Arduino programming, but the documentation for the interrupt handling macros is available in the avr-libc pages.
Another issue that held me up for a while was the placement of the call
to ISR
. Originally I included it within the main function
block (since the XC8 compiler appears to support nested functions
usually). The program will not compile if the ISR
macro is
included in the main function rather than outside of its scope. Due to
this the milliseconds
variable must also be globally
scoped.
The name TCB0_INT_vect
took me some time to work out.
Unlike the peripheral and register names these interrupt names are not
clearly indicated in the datasheet. I was finally able to find them in
some header files included with the XC8 compiler. In Windows, the file
is available at the path
Program Files (x86)\Microchip\xc8\<version number>\dfp\include\avr\iom4809.h
.
Note how the interrupt flags are manually cleared by setting the INTFLAGS register to one.
I was originally confused as to why my one second period blinky actually
looked like a constantly illuminated LED, but not at full strength,
almost as thought it were being PWM’d. From this I assumed that the
switching period was actually very, very short. I eventually discovered
that I was trying to clear the interrupt flag with TCB0.INTFLAGS =
0b00000000;
which I automatically typed without thinking (‘clear
means set to zero’). The interrupt therefore wasn’t cleared and was
re-run almost immediately after exiting the interrupt service routine.
All that’s left is to implement a simple code snippet in the main loop to turn the LED alternatively off and on each 500 milliseconds.
while (1) {
if (milliseconds % 1000 < 500) {
PORTD.OUT = 0b00000001;
} else {
PORTD.OUT = 0b00000000;
}
}
Well, strictly speaking, we turn the LED on continuously within one 500 millisecond window, then turn the LED off continuously within the following 500 millisecond window with this code, but it achieves the desired effect.
That’s it. Upload the code, sit back and enjoy our blinky, implemented the hard way!
Let’s come back to the timer resetting functionality. Suppose we are
coming up to the ~50 day limit for this timing approach. Suppose on the
48th day, exactly as 48 days have passed, we record the 48 day period by
incrementing another counter variable (call it longerPeriods) and reset
milliseconds to zero. Now the total number of milliseconds that have
passed since the device initialised is given by (longerPeriods * 48 * 24
* 60 * 60 * 1000) + milliseconds. Using a uint64_t
this can
count up to 18446744073709551615. That’s 18446744073709551615 / (1000 *
60 * 60 * 24 * 365 * 1000) = somewhere around 584942 millenia. Probably
no one needs to count up that far - more practically this can deliver
the required precision for months or years.
I’m quite sure that the compiler will properly guess the type
uint16_t
when assigning the TOP value to the 16 bit
register. If I’m feeling paranoid, and because it also allows us to
bring better practice into out code, I would actually move the TOP value
into a #define
directive with an explicit cast. Thus
somewhere in the pre-amble.
#define TIMER_B_0_TOP (uint16_t) 19999;
Then assigning to the register.
TCB0.CCMP = TIMER_B_0_TOP;
Again, I’m quite sure that the compiler will properly take care of assinging the rights bits to the right part of the 16 bit register. But if you’re paranoid and to satisfy yourself that the least significant 8 bits go to the lower part of the register and similar for the most significant 8 bits/higher part of the register the two bytes can be individually addressed.
TCB0.CCMPL = TIMER_B_0_TOP & 0b0000000011111111;
TCB0.CCMPH = (TIMER_B_0_TOP & 0b1111111100000000) >> 8;