Internal timings and data propagation delays in AVR timers/counters and IO pins - Page 1
Forum section: electronics / general

author: magic
author_rank: Super Contributor
timestamp: January 17, 2021, 08:39:00 pm
content: This is one area of AVR microcontrollers where I found the documentation to be somewhat lacking.What happens when the CPU loads the TCNTn register, does it get the pre-incerement or the post-increment value?How many cycles does it take to detect and count an external event?How many cycles for input capture? What's the exact value that gets captured?When does the freshly counted or captured event become visible to CPU instructions reading the timer's registers?I wrote a small program which tries to answer those questions. It runs a sequence of simple tests, which set up a timer, do some operations on it and then watch what happens to its registers in the following clock cycles. The tests are written in inline assembly to ensure that I know which exact instructions are executed and when.I will list the tests, their short description and my conclusions. A full listing of the program and its output (ATmega32U4) will be pasted at the end.1. TCNT0 to TCNT0 delayHere I simply reset a free-running counter to zero and read it back three times in the next three clocks.It turns out that the value that I read immediately after zeroing is zero, and later it increments. This indicates that in each cycle, the timer services the CPU read request before updating its count.2. TCNT0 LDS/STS delaySame thing, but the counter is reset and immediately read back using various combinations of the slower, 2-cycle LDS/STS instructions and the 1-cycle IN/OUT instructions used before.The results consistently suggest that both LDS and STS perform the I/O during the second cycle of their execution.This is relevant because on most devices, LDS/STS is the only way to access timers other than TC0.3. TCNT1L to TCNT1L delayI repeat similar test on timer/counter1. This one can only be accessed with LDS/STS, so I reset it with STS and then read it back after zero, one and two cycles with LDS. (In separate runs, out of necessity).The results are consistent with previous findings: LDS/STS appears to execute in its second cycle and reads the pre-increment value.4. PORTD to PIND delayThis is actually documented in the datasheet, but it never harms to verify. I write PORTD and wait for the change to appear in PIND.The first cycle after the write still sees the old value. The second cycle sees the new value. This is the expected behavior, because for each cycle the PINx register holds values sampled by a hidden latch in the middle (falling edge) of the preceding cycle, according to the datasheet.5. PORTD to TCNT0 delayI set timer/counter0 to count external clock, strobe its input pin T0 and wait for its count to increment.The CPU sees increased count on the third cycle. It appears that the first cycle is spent sampling the input signal (see point 4), in the second cycle the counter increments after the CPU already read zero, and the third cycle is when the CPU can see the new count.6, 7. PORTD to ICR1L delayNow input capture on timer/counter1. The timer counts system clock and its ICP1 pin is strobed. I read the ICR1L register at various delays using LDS.The captured value is the same which would become visible to the CPU in TCNT1 register on the second cycle after PORTD is written, but ICR1 is only updated with that value on the third cycle.My interpretation: on the second cycle the counter captures its pre-increment value, just as if it were being read by the CPU, and writes this value to ICR1 at the end of that cycle. Then the CPU can see it in the next clock.The test program:Code:#include <stdio.h>#include <avr/io.h>#define IO(x) _SFR_IO_ADDR(x)#define asm __asm__int uart_out(char c, FILE *s) {while (!(UCSR1A & 1<<UDRE1));UDR1 = c;return 0;}void uart_init() {// UART at F_CPU/16UBRR1 = 0;//UCSR1A = 1<<U2X1;UCSR1B = 1<<RXEN1|1<<TXEN1;UCSR1C = 1<<UCSZ10|1<<UCSZ11;}FILE uart = FDEV_SETUP_STREAM(uart_out, NULL, _FDEV_SETUP_RW);int main(void) {// no prescaleCLKPR = 1<<CLKPCE;CLKPR = 0;DDRD = 1<<4|1<<7;// PD4/ICP1, PD7/T0uart_init();stdout = &uart;printf("--- cnttest ---\n");{unsigned char x, y, z;TCCR0A = 0;TCCR0B = 1<<CS00;// sys clkasm volatile ("out%3, r1$""in%0, %3$""in%1, %3$""in%2, %3$": "=r"(x), "=r"(y), "=r"(z): "i"(IO(TCNT0)));printf("TCNT0->TCNT0 delay: %d %d %d\n", x, y, z);}{unsigned char x, y, z;TCCR0A = 0;TCCR0B = 1<<CS00;// sys clkasm volatile ("out%3, r1$""lds%0, %4$""sts%4, r1$""in%1, %3$""sts%4, r1$""lds%2, %4$": "=r"(x), "=r"(y), "=r"(z): "i"(IO(TCNT0)), "i"(&TCNT0));printf("TCNT0 LDS/STS delay: %d %d %d\n", x, y, z);}{unsigned char x, y, z;TCCR1A = 0;TCCR1B = 1<<CS10;// sys clkasm volatile ("sts%3, r1$""lds%0, %3$""lds%1, %3$""sts%3, r1$""nop$""lds%2, %3$": "=r"(x), "=r"(y), "=r"(z): "i"(&TCNT1L));printf("TCNT1L->TCNT1L delay: %d %d %d\n", x, y, z);}{unsigned char x, y, z;PORTD = 1<<4|1<<7;asm volatile ("out%3, r1$""in%0, %4$""in%1, %4$""in%2, %4$": "=r"(x), "=r"(y), "=r"(z): "i"(IO(PORTD)), "i"(IO(PIND)));x &= 1<<4|1<<7;y &= 1<<4|1<<7;z &= 1<<4|1<<7;printf("PORTD->PIND delay: %d %d %d\n", x, y, z);}{unsigned char x, y, z;PORTD = 1<<7;// T0TCCR0A = 0;TCCR0B = 1<<CS02|1<<CS01;// ext clk fallingasm volatile ("out%3, r1$""out%4, r1$""in%0, %3$""in%1, %3$""in%2, %3$": "=r"(x), "=r"(y), "=r"(z): "i"(IO(TCNT0)), "i"(IO(PORTD)));printf("PORTD->TCNT0 delay: %d %d %d\n", x, y, z);}{unsigned char x, y, z;PORTD = 1<<4;// ICP1TCCR1A = 0;TCCR1B = 1<<CS10;// sys clkICR1 = 0;asm volatile ("sts%3, r1$""out%5, r1$""lds%0, %4$""lds%1, %4$""lds%2, %3$": "=r"(x), "=r"(y), "=r"(z): "i"(&TCNT1L), "i"(&ICR1L), "i"IO(PORTD));printf("PORTD->ICR1L delay (0): %d %d %d\n", x, y, z);}{unsigned char x, y, z;PORTD = 1<<4;// ICP1TCCR1A = 0;TCCR1B = 1<<CS10;// sys clkICR1 = 0;asm volatile ("sts%3, r1$""out%5, r1$""nop$""lds%0, %4$""lds%1, %4$""lds%2, %3$": "=r"(x), "=r"(y), "=r"(z): "i"(&TCNT1L), "i"(&ICR1L), "i"IO(PORTD));printf("PORTD->ICR1L delay (1): %d %d %d\n", x, y, z);}for (volatile long i = 0; i < 100000; i++);return main();}And its raw output:Code:--- cnttest ---TCNT0->TCNT0 delay: 0 1 2TCNT0 LDS/STS delay: 1 0 1TCNT1L->TCNT1L delay: 1 3 2PORTD->PIND delay: 144 0 0PORTD->TCNT0 delay: 0 0 1PORTD->ICR1L delay (0): 0 2 6PORTD->ICR1L delay (1): 2 2 7

author: Doctorandus_P
author_rank: Super Contributor
timestamp: January 19, 2021, 04:54:32 pm
content: I do not do much with AVR's these days, and I don't like methods of programming where single cycle accuracy is important. That was fun 15 years ago, but these days it makes much more practical sense to use a uC that runs at a few hundred MHz. As a learning experience it is of course valid to study such things.AVR instructions "officially" execute in one clockcycle (I have long suspected it takes 2 clock cycles with a simple 2 stage pipeline) and this makes your questions partly irrelevant.The AVR's do have synchonizers on their input pins, which are 2 cascaded flipflops, and this results into a delay between (re) setting an output pin and reading back it's state.

author: magic
author_rank: Super Contributor
timestamp: January 19, 2021, 05:40:32 pm
content: I needed it to correlate events measured by input capture with events timed internally by reading the count register of the timer from the CPU.I can imagine that my actual task could probably be solved alternatively using multiple timers with multiple input capture units, but it wouldn't be a trivial solution either because of the annoying nature of the problem.The 2 stage pipeline (fetch, execute) is documented in the datasheet and very easy to account for. I wrote some cycle-accurate AVR assembly, including branches, and the documentation and instruction timing tables never failed me. Instructions execute at 1IPC except for those which occupy more than one word in program memory (obvious), those which need more than one cycle to execute (documented) and taken branches (obvious and documented). It's all 100% predictable and dead simple, unlike some more complex cores like ARM.The input sampling circuit is also precisely documented in the datasheet. It was a sanity check to verify if my understanding and testing methods are valid.