1

I'm going to be writing some performance critical code and have started trying to get an understanding of timers and how much "work" the Due CPU can do per second. To help get started with this, I wrote a bare sketch that simply increments a counter whilst a timer which ticks every second dumps the value of the counter to the serial log and then resets the counter. Here's the code I'm using:

#include <Arduino.h>

void EnableTimer7TickEverySecond();

volatile uint32_t gCounter = 0;

void setup()
{
    Serial.begin(9600);

    EnableTimer7TickEverySecond();

//  while ( true )
//  {
//      ++gCounter;
//  }
}

void loop()
{
    ++gCounter;
}

void EnableTimer7TickEverySecond()
{
    pmc_set_writeprotect(false);
    pmc_enable_periph_clk(ID_TC7);
    TC_Configure(TC2, 1, TC_CMR_WAVE | 
        TC_CMR_WAVSEL_UP_RC | TC_CMR_TCCLKS_TIMER_CLOCK1);
    TC_SetRC(TC2, 1, 656000 * 64);
    TC_Start(TC2, 1);
    TC2->TC_CHANNEL[1].TC_IER = TC_IER_CPCS;
    TC2->TC_CHANNEL[1].TC_IDR = ~TC_IER_CPCS;
    NVIC_EnableIRQ(TC7_IRQn);
}

void TC7_Handler()
{
    TC_GetStatus( TC2, 1 );

    Serial.println( (uint32_t)gCounter );

    gCounter = 0;
}

When I run this, I get values averaging 446,000 in the serial log for the increments per second. That is way lower than I was expecting. On an ARM chip running at 84MHz, I'd expect at least 14 million increments as few ARM instructions take more than 2 clock cycles and an increment should involve a read from memory, addition and a write back to memory (approx 6 clock cycles by my reckoning).

The timer is configured correctly, the TX led flashes regularly at 1 second intervals as the Serial.println is called. I've checked for overflow on the counter (oddly, I found that the number of increments when working with a uint64_t was slightly higher). I've also tried not calling Serial.begin() until I'm about to write the value to the log (and then only writing the value once) and whilst there is a slight increase in "performance", it doesn't account for the whole difference between expectation and reality.

My best guess would be that loop() is called on some sort of timer or has a built in default delay, so it occurred to me that if I comment out the counter increment in loop() and instead comment in the infinite while loop in setup(), I should be able to counteract any delay caused by how frequently the Arduino IDE calls loop(). I have seen other examples that never bother with loop() and just stick all their code in setup().

I do see a performance increase with this setup, but only to around 9 million, not the 14+ million I would expect. Oddly though, sometimes, randomly, the value is 2 or 3 times the "usual" value. Not between 2 and 3 times, but exactly 2 or 3 times. This happens regardless of whether I am incrementing the counter inside the loop() function or my own loop inside setup(). It is not a case of the timer "skipping", the output is written every second like clockwork.

So, my questions are:

  • What does the Arduino library do to make loop() so inefficient?

  • Is there any harm in keeping all your code inside an infinite loop in setup() and never returning out of it?

  • Why would I sometimes see the number of increments being 2 or 3 times higher than usual? Could the counter be getting cached (despite being marked as volatile) and not reset properly? If so, how can I prevent this?

3

What does the Arduino library do to make loop() so inefficient?

Alot. For a start it's a function, so it has to push registers onto the stack before it runs, and pop them off afterwards. Secondly there is the "serial event" system outside the loop. In main.cpp in the core you can see how loop is called:

setup();

for (;;)
{
    loop();
    if (serialEventRun) serialEventRun();
}

Is there any harm in keeping all your code inside an infinite loop in setup() and never returning out of it?

If you don't use serial events, no harm whatsoever.

Why would I sometimes see the number of increments being 2 or 3 times higher than usual? Could the counter be getting cached (despite being marked as volatile) and not reset properly? If so, how can I prevent this?

My guess is the operations you are using are not atomic. This is, the interrupt is able to be fired right in the middle of doing the series of instructions that perform the increment. The temporary registers end up not being the same as they were at the start of the operation, and consequently the value is corrupted. You should wrap the increment in a critical section (i.e., disable interrupts before it, and enable them afterwards).

To better analyse the speed of execution you can look at the generated assembly code of your program. If we look at the disassembled main() function for the bit where it loops you can get this:

80596:  f7ff fe05   bl  801a4 <loop>
8059a:  4b04        ldr r3, [pc, #16]   ; (805ac <main+0x30>)
8059c:  2b00        cmp r3, #0
8059e:  d0fa        beq.n   80596 <main+0x1a>
805a0:  f7ff feec   bl  8037c <_Z14serialEventRunv>
805a4:  e7f7        b.n 80596 <main+0x1a>
805a6:  bf00        nop

loop() consists of this:

801a4:  4b02        ldr r3, [pc, #8]    ; (801b0 <loop+0xc>)
801a6:  681a        ldr r2, [r3, #0]
801a8:  3201        adds    r2, #1
801aa:  601a        str r2, [r3, #0]
801ac:  4770        bx  lr
801ae:  bf00        nop

If we feed that into main() where it calls loop() the finished code looks like:

80596:  f7ff fe05   bl  801a4 <loop>
801a4:  4b02        ldr r3, [pc, #8]    ; (801b0 <loop+0xc>)
801a6:  681a        ldr r2, [r3, #0]
801a8:  3201        adds    r2, #1
801aa:  601a        str r2, [r3, #0]
801ac:  4770        bx  lr
801ae:  bf00        nop
8059a:  4b04        ldr r3, [pc, #16]   ; (805ac <main+0x30>)
8059c:  2b00        cmp r3, #0
8059e:  d0fa        beq.n   80596 <main+0x1a>
805a0:  f7ff feec   bl  8037c <_Z14serialEventRunv>
805a4:  e7f7        b.n 80596 <main+0x1a>
805a6:  bf00        nop

So that is what gets executed for every single iteration of loop(). If we (incorrectly) assume that each instruction takes 1 clock tick, that's 13 ticks. 84,000,000 / 13 = ~6.4 million loops per second.

Now - if we map the instructions against actually how many clock cycles they take, we can get a clearer picture:

bl                  1+P  (P = 1 to 3)
ldr (pc relative)   2
ldr (word)          2
adds                1
str (word)          2
bx                  1+P  (P = 1 to 3)
nop                 1
ldr (pc relative)   2
cmp                 1
beq.n               1 or 1+P (P = 1 to 3)
bl                  1+P (Only if serial event runs)
b.n                 1 or 1+P (P = 1 to 3)
nop                 1

So taking a worst case scenario where P = 3 (pipeline re-fill clock counts, that is), and no serial event running, we can calculate a total number of clock cycles of 4+2+2+1+2+4+1+2+1+4+0+4+1 = 28.

84,000,000 / 28 = 3,000,000 loops per second.

But hang on - that's still much more than you're seeing. That's because you're still only looking at the small picture. You're looking at just your sketch and forgetting what else there is going on.

Every millisecond the SysTick_Handler() gets called which does various tasks, including calling the TimeTick_Increment() routine to deal with millis(). That's 1,000 times per second, and that eats into your processing time somewhat.

Another important point is it can't be incrementing while you're running your interrupt. The more you do in your interrupt (and using serial is a very heavy-weight thing to do in an interrupt) the less of your 1-second slice you get for doing your incrementing.

As an experiment, why not try disabling all the interrupts except just the one you are using for your timer and see how it changes the value.

  • Thanks for the extremely detailed answer. Where do you get the disassembly from? I find it interesting that it appears to be 16bit THUMB instructions rather than the 32bit ARM instructions. I was planning to optimise a certain section of my code using ARM assembler. – Mick Waites May 15 '15 at 12:54
  • Yes, it's thumb. The compiler is called with the -mthumb option. To get a disassembly you need to use arm-none-eabi-objdump -h -S <elf file> – Majenko May 15 '15 at 12:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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