SAM4S timer counter bug

Discussion around product based on ARM Cortex M4 core.

Moderators: nferre, ncollot

mciholas
Posts: 9
Joined: Sat Apr 06, 2013 5:00 pm

SAM4S timer counter bug

Mon Apr 27, 2015 7:43 pm

We think there is a bug in the SAM4S timer counters.

We need a 32 bit counter on a SAM4S8B.  Since Atmel annoyingly doesn't provide 32 bit counters, we wanted to make one by chaining TC0 and TC1.

First try at this was using BURST mode, TC0 outputs TIOA0 as high on 0xffff and low on 0x0000, so it creates a single pulse enable for TC1.  Thus as TC0 counts from 0xffff to 0x0000, TC1 should increment by one.  By using BURST mode, the two counters should always increment one the same clock edge.

Results are that this works EXCEPT for the VERY FIRST PULSE.  That is, TC0 counts 0x0000 to 0xffff, then 0x0000, and TC1 does NOT increment until TC0 AGAIN rolls over.  After the missed first pulse, the counters work fine.

We verified that TIOA0 does the RIGHT thing by enabling the output pin and watching it.  It clocks high on the first wrap but TC1 does not increment.

Second try was to clock TC1 on the negative edge of TIOA0, that is not using BURST mode.  This creates a small delay in TC1 updating.  But the results were the same, TC1 does NOT increment on the first TIOA0 falling edge, but then works on every edge after that.

So the results in both cases are a 32 bit counter that, when it starts, counts like this:

0x0000 0000
...
0x0000 ffff
0x0000 0000 <- missed TC1 increment ***********
...
0x0000 ffff
0x0001 0000 <- correct TC1 increment
...
0x0001 ffff
0x0002 0000 <- correct TC1 increment 
...
0xffff ffff <- max count
0x0000 0000 <- correct TC1 increment which wraps
...
0x0000 ffff
0x0001 0000 <- proper wrap

WTF?

It sure seems like TC1 ignores the first clock edge it gets no matter if it is gated in BURST mode or given to it directly.  Is this a bug or did we do something wrong?  Again, we VERIFIED TIOA0 is doing the right thing with a logic analyze on the output pin.  The first pulse is THERE, and TC1 does NOT increment.

Here is the code for BURST setup:

Code: Select all

    static void _InitUpperTimeChannel(void) {
        sysclk_enable_peripheral_clock(ID_TC1);
        tc_init(TC0, 1, TC_CMR_TCCLKS_TIMER_CLOCK2
                        | TC_CMR_WAVE
                        | TC_CMR_BURST_XC1);
        tc_set_block_mode(TC0, TC_BMR_TC1XC1S_TIOA0);
    }

    static void _InitLowerTimeChannel(void) {
        sysclk_enable_peripheral_clock(ID_TC0);
        tc_init(TC0, 0, TC_CMR_TCCLKS_TIMER_CLOCK2
                        | TC_CMR_ACPA_SET
                        | TC_CMR_ACPC_CLEAR
                        | TC_CMR_WAVE);
        tc_write_rc(TC0, 0, 0);
        tc_write_ra(TC0, 0, UINT16_MAX);
    }

    void Timer_Create(void) {
        _InitLowerTimeChannel();
        _InitUpperTimeChannel();
        tc_start(TC0, 1);
        tc_start(TC0, 0);
    }

    uint32_t Timer_GetTicks(void) {
        uint32_t _upper_first_read, _upper_second_read, _lower;
        _upper_first_read = (volatile)TC0->TC_CHANNEL[1].TC_CV;
        _lower = (volatile)TC0->TC_CHANNEL[0].TC_CV;
       
        // Read a second time to make sure the counters did not increment during the read instruction
        _upper_second_read = (volatile)TC0->TC_CHANNEL[1].TC_CV;
       
        // If the upper counter incremented, read channel 0 again to make sure the time reading is valid
        if (_upper_first_read != _upper_second_read) {
            _lower = (volatile)TC0->TC_CHANNEL[0].TC_CV;
        }
        return _lower | (_upper_second_read << 16);
    }
Here is the code change for straight clocking setup:

Code: Select all

static void _InitUpperTimeChannel(void) {
    sysclk_enable_peripheral_clock(ID_TC1);
    tc_init(TC0, 1, TC_CMR_TCCLKS_XC1
                    | TC_CMR_WAVE
                    | TC_CMR_CLKI);
    tc_set_block_mode(TC0, TC_BMR_TC1XC1S_TIOA0);
}
Any help would be appreciated.  Can others try this on their boards?

Mike C.
mciholas
Posts: 9
Joined: Sat Apr 06, 2013 5:00 pm

Re: SAM4S timer counter bug

Fri May 08, 2015 11:41 pm

We have developed a work around.  Basically, we generate a false early pulse to TC1.  That pulse is then lost by TC1 but subsequent increments are handled properly.

Here is the code to initialize and start the counter system:

Code: Select all

    void Timer_Create(void) {

        // initialize timer counters TC0 and TC1 to form a 32 bit running counter
        //
        // At all times, the TC1:TC0 set of timer counts will represents a constantly incrementing
        // 32 bit counter.  Due to use of the BURST feature, there is no race condition on update
        // of the high order counter when the low order counter wraps.
        //
        // Note that the timers have a bug such that they fail to count the very first clock pulse.
        // To deal with this, we create a false first pulse to the high order timer and then reset
        // the trip points such that it works on lower counter wrap.

        // enable peripheral clocks
        sysclk_enable_peripheral_clock(ID_TC0);
        sysclk_enable_peripheral_clock(ID_TC1);

        // configure TC0 low order, TC1 high order, TC1 burst, increment on TIOA0 enable
        tc_init(TC0, 0, TC_CMR_TCCLKS_TIMER_CLOCK2 | TC_CMR_WAVE | TC_CMR_ACPA_SET | TC_CMR_ACPC_CLEAR);
        tc_init(TC0, 1, TC_CMR_TCCLKS_TIMER_CLOCK2 | TC_CMR_WAVE | TC_CMR_BURST_XC1);
        tc_set_block_mode(TC0, TC_BMR_TC1XC1S_TIOA0);

        // setup to generate early first pulse that is lost due to bug in timer hardware
        tc_write_ra(TC0, 0, 0x0001);
        tc_write_rc(TC0, 0, 0x0002);

        // start the counters
        tc_start(TC0, 1);
        tc_start(TC0, 0);

        // busy wait until first lost pulse is generated, then reset compare trip to TC0 wrap
        while (((volatile)TC0->TC_CHANNEL[0].TC_CV) < 0x0002);

        // reset TIOA0 compare points to wrap of TC0
        tc_write_ra(TC0, 0, 0xffff);
        tc_write_rc(TC0, 0, 0x0000);
    }
The idea is to create the one lost pulse early in TC0 count, then wait for that to pass, and then reset the compare registers so that the system will work after that.  TIOA0 will go high on TC0 0x0001, low on 0x0002, thus creating the first lost pulse.  Once we count past that, we reset so TIOA0 goes high on 0xffff and low on 0x0000, thus creating the proper enable to increment TC1 with BURST mode.

We can't do anything very early in the count sequence, but that is okay since this only occurs at start up and we can eat a few cycles before putting the counter to work.  Once we get past the startup, everything works as expected.

This does suggest that anyone using the counter for external events or other ways in which you expect to increment on each event will be disappointed that the first event is missed.  We can work around that for our use, but in some other uses, this seems quite broken.

Also, we improved the capture jitter of the reading function by using the captured data more intelligently.  It assumes no interrupt occurs in the reading section that lasts longer than half the TC0 wrap time, which is pretty safe.  You can make it very safe by putting a critical section around that part if desired.

Code: Select all

    uint32_t Timer_Ticks(void) {
        register uint32_t upper1, upper2, lower;

        // read counter values in sequence quickly
        // returned time is when we read lower
        upper1 = (volatile)TC0->TC_CHANNEL[1].TC_CV;
        lower = (volatile)TC0->TC_CHANNEL[0].TC_CV;
        upper2 = (volatile)TC0->TC_CHANNEL[1].TC_CV;

        // test for upper change, if none, report results
        // if change, then use lower range to fix results
        if ((upper1 == upper2) || (lower > 0x8000))
            return ((upper1 << 16) | lower);
        else
            return ((upper2 << 16) | lower);
    }
Mike C.

Return to “SAM4 Cortex-M4 MCU”

Who is online

Users browsing this forum: No registered users and 1 guest