Arduino millis() does not make use of one millisecond interrupt. Instead they make use of 1,024 µsec interrupts and manipulate. Any specific reason why this was implemented this way?

In one case, we can go for timer0 without prescaler and use 1 µsec interrupts. In such case, we have to use CTC mode with OCR0A=15. Inside ISR, we could update the microsecond counter and if it reaches 1,000, we could update the msec timer count. Otherwise we could go for directly 1 msec interrupt (maybe using timer0).

I wrote this code to get micros() and millis(). But there is some issue and I'm not able to get it working correctly.

#include <avr/io.h>
#include <avr/interrupt.h>

#ifndef F_CPU
#define F_CPU 16000000UL

volatile uint32_t oneMilliCounter;
volatile uint32_t oneMicroCounter;
extern void initializeTimer0CTC_Mode (void);

extern uint32_t millis(void);
extern uint32_t micros(void);

volatile uint32_t oneMilliCounter = 0;
volatile uint32_t oneMicroCounter = 0;

uint32_t millis(void) {
  uint32_t value;
  uint8_t sregCopy = SREG;
  value = oneMilliCounter;
  SREG = sregCopy;
  return (value);

uint32_t micros (void) {
  uint32_t value;
  uint8_t sregCopy = SREG;
  value = oneMicroCounter;
  SREG = sregCopy;
  return (value);

void initializeTimer0CTC_Mode (void) {
  OCR0A = 15; // Set for 1 usec.
  TCCR0A |= 1 << WGM01;  // CTC Mode
  TIMSK0 |= 1 << OCIE0A;
  TCCR0B |= (1 << CS00); // prescaler=1

  static uint32_t prevMicro;
  if (oneMicroCounter - prevMicro >= 1000)   {
    prevMicro = oneMicroCounter;

int main(void) {
  DDRD |= 1 << PD3;
  uint32_t prevTime = micros();
  while (1) {
    uint32_t currentTime = micros();
    if (currentTime - prevTime >= 1000) {
      PORTD ^= (1 << PD3);
      prevTime = currentTime;
  return (0);
  • What issues? What results are you getting? What do you expect?
    – user31481
    Jan 3, 2018 at 9:36

3 Answers 3


One microsecond is only 16 CPU cycles.

The CPU needs 4 cycles to prepare itself for servicing the interrupt (save the program counter, load the interrupt vector and clear the I bit in SREG). The interrupt vector itself is a jmp instruction that takes 2 cycles. When the ISR is done, it executes the reti instruction (return from interrupt) that takes 4 cycles. This means you have only 6 CPU cycles left for:

  • saving the context, i.e. pushing to the stack any register your ISR may modify (2 cycles per push)
  • doing the actual ISR work
  • restoring the context (2 cycles per pop)
  • doing also some useful work in the main program.

There is no way you can do half of this in only 6 cycles.

Edit: I did a couple of experiments to gain more insight on the cost of running an interrupt too often.

In the first experiment, I used your initializeTimer0CTC_Mode() function to interrupt every microsecond the following program:

// This ISR consists of a sigle `reti' instruction.

#include <util/delay.h>

int main(void)
    DDRD |= _BV(PD2);       // PD2 = digital 2 as output
    for (;;) {
        PIND |= _BV(PD2);   // toggle PD2
        _delay_us(499.75);  // 0.5 ms - 4 cycles

The program is meant to generate a 1 kHz signal on pin 2, and that's exactly what it does if the sei(); statement is commented out. With the sei(); statement, the frequency drops to 375 Hz. This shows that processing an empty interrupt (an ISR consisting solely of the reti instruction) costs 62.5% of the CPU power, leaving 37.5% to the main program. 62.5% is exactly 10 out of every 16 CPU cycles, which confirms the cycle count of the first part of this answer.

The second experiment was to compile and disassemble your ISR. Here is what I got:

    push r1
    push r0
    in   r0, 0x3f  ; 1 cycle
    push r0
    eor  r1, r1    ; 1 cycle
    push r18
    push r20
    push r21
    push r22
    push r23
    push r24
    push r25
    push r26
    push r27
; static uint32_t prevMicro;
; oneMicroCounter++;
    lds  r24, 0x0104
    lds  r25, 0x0105
    lds  r26, 0x0106
    lds  r27, 0x0107
    adiw r24, 0x01
    adc  r26, r1    ; 1 cycle
    adc  r27, r1    ; 1 cycle
    sts  0x0104, r24
    sts  0x0105, r25
    sts  0x0106, r26
    sts  0x0107, r27
; if (oneMicroCounter - prevMicro >= 1000)   {
    lds  r24, 0x0104
    lds  r25, 0x0105
    lds  r26, 0x0106
    lds  r27, 0x0107
    lds  r20, 0x0100
    lds  r21, 0x0101
    lds  r22, 0x0102
    lds  r23, 0x0103
    sub  r24, r20   ; 1 cycle
    sbc  r25, r21   ; 1 cycle
    sbc  r26, r22   ; 1 cycle
    sbc  r27, r23   ; 1 cycle
    cpi  r24, 0xE8  ; 1 cycle
    sbci r25, 0x03  ; 1 cycle
    cpc  r26, r1    ; 1 cycle
    cpc  r27, r1    ; 1 cycle
    brcs 0f  ; 1 cycle if the if() condition is true, 2 otherwise
;   prevMicro = oneMicroCounter;
    lds  r24, 0x0104
    lds  r25, 0x0105
    lds  r26, 0x0106
    lds  r27, 0x0107
    sts  0x0100, r24
    sts  0x0101, r25
    sts  0x0102, r26
    sts  0x0103, r27
;   oneMilliCounter++;
    lds  r24, 0x0108
    lds  r25, 0x0109
    lds  r26, 0x010A
    lds  r27, 0x010B
    adiw r24, 0x01
    adc  r26, r1    ; 1 cycle
    adc  r27, r1    ; 1 cycle
    sts  0x0108, r24
    sts  0x0109, r25
    sts  0x010A, r26
    sts  0x010B, r27
; } }
    pop  r27
    pop  r26
    pop  r25
    pop  r24
    pop  r23
    pop  r22
    pop  r21
    pop  r20
    pop  r18
    pop  r0
    out  0x3f, r0  ; 1 cycle
    pop  r0
    pop  r1
    reti           ; 4 cycles

Unless otherwise noted in comments, every instruction of this listing takes 2 cycles to execute. Note the large number of CPU registers saved (push) and restored (pop), and the large number of SRAM accesses (lds and sts): three 4-byte variables read and written back. Counting the total number of cycles taken by this ISR is left as an “exercise to the reader”.

  • 1
    ... And even if you could, imagine the performance hit on your sketch. It'd be starved of CPU cycles and never be able to do anything useful...
    – Majenko
    Jan 3, 2018 at 10:33

Using CTC mode would mean you can't use pin 9 as a PWM pin anymore.

Also, setting OCR0A to 15, would mean PWM on pin 10 could only have 15 different PWM values.

So, in essence, pins 9 and 10 can't be used for PWM.

  • Got it. So in addition to timer, we could get 1Hz (approximate) PWM with duty cycle decided by OCR0A/OCR0B on OC0A/OC0B
    – Rajesh
    Jan 3, 2018 at 11:11
  • Not on OC0A, since you can't change OCR0A without changing the frequency.
    – Gerben
    Jan 3, 2018 at 11:14
  • You could instead try and use the more capable timer1. You can then use ICR1 to set the TOP. That you can still use OCR1A and OCR1B to set the duty cycle. Though you'd have to change the analogWrite function, to map the 0..255 value to the 0..ICR1 value. (and not use any of the libraries that use timer1, like the servo library)
    – Gerben
    Jan 3, 2018 at 11:17
  • I think you meant 1kHz, not 1Hz.
    – Gerben
    Jan 3, 2018 at 11:17
  • Yes, I meant 1KHz. Thanks for correcting. But I just checked ans saw that duty cycle can be changed (Not the frequency) on OC0A. Below is the code outline. First line is inside setup() function: TCCR0A|=(1<<COM0A1)|(1<<COM0A0); void loop() { unsigned int cnt; for(cnt=0;cnt<255;cnt+=50) { OCR0A=cnt; _delay_ms(1000); } }
    – Rajesh
    Jan 3, 2018 at 12:16

Even if you could do what you are proposing, which you can't (see Edgar Bonet's excellent answer), imagine the performance hit running an interrupt every single microsecond would have on your code.

Every interrupt takes time away from your code.

Let's crunch some more numbers. If you wanted the millis/micros interrupt to take no more than 1% of the processing time, so it has no noticeable impact on your running code it would have to execute for only 10ns. That's 1% of 1000ns, or 0.000001 x 0.01.

Now assume it takes 50 clock cycles for a highly optimised version of your function to operate. That's a conservative estimate. That includes the time taken to prepare for, call and return from the interrupt.

For both those to hold true you would have to be able to execute 100 x 50 instructions in a microsecond. (To fill your 1uS you should be able to run your 1% interrupt 100 times, yes?) That's 5,000 x (1/0.000001) instructions per second, or 5,000,000,000 instructions per second.

Assuming a single clock cycle for each instruction, that means your chip would have to be running at 5GHz...!

Can you see the problem?

So we scale it back to something more realistic. Maybe a 10% performance hit instead of 1%. How would that be? Well, it's simple enough to say that it would require a chip running at 10% of the speed. 500 x (1/0.000001). So 500MHz.

Still a long long way away from the 16MHz that the Arduino's ATMega runs at.

So you can see the issue.

So we choose close to 1mS instead. That gives a much lower overhead and it's something that the 16MHz Arduino can cope with.

Why 1024 and not 1000? That's simple: 1000 is base 10. We, as humans, with two hands (usually) of (normally) 5 digits on each, love base 10. We thrive in base 10. However, computers with their 1s and 0s hate base 10. They much prefer working in base 2. It makes arithmetic really easy for them. And in interrupts we want the arithmetic to be easy.

Let's take a quick look at the maths involved and expand out the macros in the code.

Every tick the millis value is increased by MILLIS_INC, and any extra is added to a "fractional" count. What's MILLIS_INC? It's:


So let's expand that out - what's MICROSECONDS_PER_TIMER0_OVERFLOW?

#define MICROSECONDS_PER_TIMER0_OVERFLOW (clockCyclesToMicroseconds(64 * 256))

And what's clockCyclesToMicroseconds?

#define clockCyclesToMicroseconds(a) ( (a) / clockCyclesPerMicrosecond() )

What's clockCyclesPerMicrosecond()?

#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )

And, of course, F_CPU is (typically) 16,000,000.

So now we can put it all together.


#define MILLIS_INC ((clockCyclesToMicroseconds(64 * 256)) / 1000)
which becomes:

#define MILLIS_INC (((64 * 256)/clocksPerMicrosecond()) / 1000)

Which in turn becomes:
#define MILLIS_INC (((64 * 256)/(F_CPU/1000000L)) / 1000)

Which finally turns into:
#define MILLIS_INC (((64 * 256)/(16000000/1000000L)) / 1000)

So reduce those down a little:
#define MILLIS_INC ((16384 / 16) / 1000)

And finally:
#define MILLIS_INC (1.024)

And since we're talking integers here that is automatically truncated (by the compiler, not at runtime, so it's essentially "free") to 1.

But what about the 0.024ms...? That's simple. The "Fractional" amount is added to another counter - but not as 0.024 - instead as a FRACT_INC amount, which is calculated as ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3). That's 1024 % 100, or just 24, but then that's right shifted 3 times. That becomes 3. And when that count hits another limit it loops round and adds one to the millis count. That limit is FRACT_MAX, which is 1000 >> 3 or 125.

All nice and simple and straight forward. Variables, like the fractional count, are kept to just 8 bits (which is what you want on an 8 bit CPU...) and the majority of calculations are done by the compiler, not at runtime by the CPU. Everything else is simple addition / subtraction and comparisons. Very light. And it runs often enough to be good enough for the majority of uses. And given that, because it's just a timer interrupt and is subject to collision with other interrupts, and can't run when interrupts are disabled, it's only a rough figure anyway. Even the act of reading millis() can slow down the counting of milliseconds, since it disables interrupts while the value of the count is retreived (a so called "critical section").

And yes, it could be tweaked to run at precisely 1000uS intervals and do away with the fractional component - but why bother when it's never going to be that accurate anyway? Also the added complexity of calculating what 1000uS is for a timer configuration when you consider the vast array of speeds that Arduinos could potentially run at (16MHz for an Uno, 8MHz for the 3.3V boards, 1MHz for the default internal oscillator...) it's far simpler to say "This is what it ticks at, and this is the number of milliseconds per tick, with this bit left over" which is calculated at compile time, instead of having to work out prescalers and overflow counts, etc.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.