📜 ⬆️ ⬇️

Beginners: a microcontroller counter in 2/3 microsecond increments and several days overflow

Often, when a microcontroller device is in operation, there is a need to count the “anthropomorphic” time - how many fractions of a second the LED lights up, the maximum time interval double-click, etc. In general, count not only nano- and microseconds, but tens of milliseconds, or even seconds , minutes and even hours (I'm afraid to say about the day ...).
At the same time, microcontrollers often need to deal with microseconds at the same time — impulse periods, debounce, and so on.
There are also devices that operate continuously for many hours and even days - aviation technology, automotive, downhole devices (there we are sometimes talking about continuous work for several days). In these cases, it is not allowed to overflow timers and 8-bit variables.
I would like to combine all this into one elegant and universal solution - to have a means of measuring time with microsecond accuracy that does not overflow for several days.
Why not? I suffered for some time and gave birth to a solution for AVR 8-bit microcontrollers. For this, I used an 8-bit timer-counter and a 4-byte variable. I am not working with PICs and AT89 now, but I am not friends with other embedded platforms. However, if readers help, I will do it for them.
Advantages - the code is extremely repeatable (I already do the 5th device with it); simplicity in work (interruptions for client part of work are not used); the client part of the code is conditionally platform-independent; in the interrupt - one operation of summation (but, truth, for 4-hbyte value); no external device - real-time timer.
I found one drawback - one such busy and always necessary timer is busy ...
The article will be interesting first of all for beginners - I did not discover America here.


Theory


So, I have an Atmega16A based device with 12MHz quartz. We take its timer counter 0. This is an eight-bit timer - that's enough for us. Why? We consider:
  1. take 12 MHz from quartz and take the division factor by 8 - we get a frequency of 1500 KHz;
  2. We take the CTC mode (reset by coincidence) and set the interrupt to match 150 - we get the interrupt response frequency 10 KHz;
  3. on this very interrupt, increment the variable (we get an increment every 0.1 millisecond);
  4. if it is an unsigned 32-bit value, then it will overflow approximately after
    • 429496729.6 milliseconds;
    • 42949.7 seconds;
    • 7158.3 minutes;
    • 119.3 hours;
    • 4.97 days.

In other words, such a solution creates for me a timer with an accuracy of 0.1 millisecond for (almost) 5 days (it is necessary here, however, to take into account that real quartz has an error - more on this later). And if you also analyze the value of the actual timer 0 - it is incremented every 2/3 microseconds - then you can get a counter with an accuracy of 0.67 microseconds.
Enough? I - for the eyes. Using a 0.1 millisecond counter, I’m in my projects:

And all this calmly intercepts with ONE ATmega16 CONTROLLER! And this is not an Assembler, but a cross-platform Xi! And no external real time counter!
Not bad, huh?

Setup for AVR


How to do it all in AVR?
First of all, we set up an external variable, which I call “DecMilliSecond”:
//  main.h typedef unsigned long dword; //  32-  extern volatile dword dmsec; // 0.1msec //  main.c volatile dword dmsec; 

As @ no-smoking correctly noted, this variable must be volatile so that its compiler does not try to optimize.
I initialize this variable in a function:
 dmsec = 0; 

Next, I set the timer operation mode 0:
 // .  0 – 0.1msec Timer0_Mode (TIMER_Mode_CTC | TIMER0_Clk_8); Timer0_Cntr (149); Timer_Int (Timer0_Cmp); 

At the same time in some MCU_init.h I declare everything that is necessary:
 //  mcu_init.h #include <mega16.h> // . TIMSK #define Timer0_Cmp (1 << 1) //   0 // . TCCRn #define WGM1 (1 << 3) #define CS1 (1 << 1) // .     0 #define TIMER0_Clk_8 CS1 //  8 // .    #define TIMER_Mode_CTC WGM1 // CTC (  ) // .   #define Timer_Int(Mode) TIMSK = (Mode) #define Timer0_Mode(Mode) TCCR0 = (Mode) #define Timer0_Cntr(Cntr) OCR0 = (Cntr) 

Well and further, when it is possible, I allow interruptions:
 #asm ("SEI") 

It remains to describe the interruption. It is simpler than all the previous ones:
 #include <mega16.h> interrupt [TIM0_COMP] Timer0_Compare (void) { ++dmsec; } 

All, the timer is described, configured and running!
')

PIC Setup


This is what my dear PICs prompted me:

On peaks, this is easily repeated using the Timer2 module. It is in it that there is a similar interrupt function by coincidence.

PR2 = 75 - the value at which the timer is reset and generates an interrupt
T2CON.T2CKPS = 2 - Prescaler 1:16
T2CON.T2OUTPS = 0 - no postscaller
T2CON.TMR2ON = on - the timer is on

IPR1.TMR2IP = 1 - high priority interrupt
PIR1.TMR2IF = off - reset interrupt flag
PIE1.TMR2IE = on - enable interrupt by coincidence TMR2 and PR2
INTCON.GIE = ​​on - enable interrupt handling

As you can see, the prescaler is 2 times more here, because PR2 is 2 times less.
These settings will generate interrupts with a frequency of 10 kHz at a system frequency of 48 MHz (the timer goes Fosc / 4) - the standard frequency for USB Full Speed.

Using


The code for the client of this timer is obtained cross-platform (if not to refer to the value of timer 0 in AVR).
Here is the USB sharing code snippet:
 #include "main.h" //   dmsec, next_USB_timeout #include "FT245R.h" //      USB #include "..\Protocol.h" //     -  // ** // **    USB // ** void AnalyzeUSB (void) { #define RECEIVE_BYTE(B) while (!FT245R_IsToRead)\ { if (dmsec > end_analyze) return; }\ B = FT245_ReadByte (); #define RECEIVE_WORD(W) //   2  #define RECEIVE_DWORD(W) //   4  dword end_analyze, d; NewAnalyze: if (!FT245R_IsToRead) //  ? return; end_analyze = dmsec + max_USB_timeout; // timeout    next_USB_timeout = dmsec + MaxSilence_PC_DEV; // timeout    RECEIVE_BYTE (b) //   switch (b) { case SetFullState: RECEIVE_DWORD (d); //   is_initialized = 1; //  ChangeIndicator (); break; } // switch (pack) goto NewAnalyze; #undef RECEIVE_BYTE //  #define #undef RECEIVE_WORD #undef RECEIVE_DWORD } 

The macro functions RECEIVE_BYTE, RECEIVE_WORD, RECEIVE_DWORD implement reading procedures taking into account timeout for this phase of the exchange. In the end, if something is hanging on the other side, the microcontroller will not hibernate. Please note - WatchDog is not needed! And all thanks to the variable / constant max_USB_timeout, which sets the timeout to within 0.1 millisecond.
Similarly, the analysis of "silence in the air" variable next_USB_timeout. This allows the microcontroller to 1) find out that the computer has disappeared somewhere, 2) to signal something about this (in my case, the “error” LED lights up). Constant / variable MaxSilence_PC_DEV allows you to vary the concept of "silence" in the widest limits - from a fraction of a millisecond to several days.
All other moments are implemented in the same way.
If you need to use the microsecond counter, then the comparison function appears there:
 #define GetUSec(A,B) { #asm ("CLI"); A = dmsec; B = TCNT0; #asm ("SEI"); } // ** // **         2/3usec // ** dword Difference (dword prev_dmsec, byte prev_usec) { dword cur_dmsec; byte cur_usec; dword dif; // .    GetUSec (cur_dmsec, cur_usec); //   dif = cur_dmsec - prev_dmsec; dif <<= 8; if (cur_usec < prev_usec) dif += 255 + (dword) cur_usec - prev_usec; else dif += cur_usec - prev_usec; return dif; } 

The previous moment of time is transferred to the function - the previous value of dmsec and timer 0.
First, we stop interrupts with the GetUSec macro so that the dmsec value and the counter value do not deteriorate at the time of copying. And copy the current time.
Next, we bring the time difference to the 2/3 microsecond format, taking into account the overflow.
Well, we return this time.
And then we use it in the usual if to control debounce and other activities. Just do not forget to also suspend interruptions while checking the current time point - or rather, use the GetUSec macro.

results


This timer turned out to be a highly convenient solution for me. I think it will be useful to you. And I applied it in the following projects:

Dear users from other platforms can prompt me the initialization code of the corresponding timer, as well as the rules for access to it - I'll add it here. It is possible that for other platforms it will be necessary to pick up other times. But in any case, it should be something within a few microseconds for the timer itself and something multiple of 100 microseconds for the counter variable. For, as it turned out, sometimes one millisecond is not enough.

UPD

A few words about the stability of quartz resonators


As the nerudo corrected me absolutely exactly in the comments, it can be counted for almost 5 days with a step of 2/3 microseconds, but the error of these calculations is not zero ...
Take that quartz HC49 / S at 12 MHz that I use. The manufacturer KSS declared accuracy + -15 ... + -50 x 1e-6 - this is the same ppm . If I am not mistaken, this means that in 1 second an error of 15 microseconds runs through. So, in 4.97 days we will get an error of 268 milliseconds. If we take quartz more abruptly - say, with 1 ppm, then we get 18 milliseconds in the same time.
Not a fountain on one side. On the other hand, we do not make a high-end chronometer! At least, I do not set myself such a task. We must take into account this fact.
We faced this problem - how to achieve an accuracy of 1 millisecond in 45 minutes between two separated devices. But about it already another time.

Source: https://habr.com/ru/post/112630/


All Articles