7
\$\begingroup\$

I'm using an ATmega328P to read the state of a digital input by using the following code section written in C (there can be alternative ways, but this is an example). val is a uint8_t variable type, and it stores the state of the digital input pin:

Here is the part of the code:

if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
    val = 1;
} else {
    val = 0;
}

I set the clock as:

#define F_CPU   16000000UL

Imagine the digital input is an ON/OFF pulse train with 50% duty cycle and we gradually increase the frequency of it. At some point at a certain frequency the code above should not be able to capture the digital input state correctly.

  1. How can we estimate roughly the maximum pulse frequency the above code can handle to read the state correct?
  2. Should we find how many clock cycles it uses and multiply it by the clock frequency?
  3. And if so, how can I do it in practice?

    int main(void) {
    
        DDRD = B0100000;
        DDRD |= 1<<5;
    
        while (1) {
    
            unsigned long data = 0;
            uint8_t val;
    
            for (int i=0; i<25; i++) {
                data <<= 1;
                PORTD &= ~(1 << 5);
                _delay_us(2);
                PORTD |= (1 << 5);
                _delay_us(2);
    
                if ( (PIND & (1 << PIND6)) == (1 << PIND6) ) {
                    val = 1;
                } 
                else {
                    val = 0;
                }
    
                data |= val;
            }            
    
            // The rest of the code
    
        }
    }
    
\$\endgroup\$
  • 1
    \$\begingroup\$ I expect this will capture the digital input state correctly, every time it runs, at any frequency. The question is, how often can you run this code? \$\endgroup\$ – user253751 Jan 9 at 12:10
  • 5
    \$\begingroup\$ You can write your code as just val = ((PIND & (1 << PIND6)) == (1 << PIND6));, or val = ((PIND >> PIND6) & 1); (and probably a bunch of other ways). You should look at your compiler output to see whether you get different assembly code in each case and which one is fastest. \$\endgroup\$ – The Photon Jan 9 at 21:05
  • 4
    \$\begingroup\$ Have you tried a hardware profiler to measure instead of estimate? \$\endgroup\$ – Mast Jan 10 at 10:25
  • \$\begingroup\$ Your new code has the same problem as the old code:- val and data are not used so they will be optimized away. \$\endgroup\$ – Bruce Abbott Jan 12 at 7:14
22
\$\begingroup\$
  1. Since the code snippet you're interested in isn't big, you could disassemble your compiled code, look at all the assembly instructions and count how many cycles they need. You can find the number of cycles for each instruction in the datasheet.

  2. If you have an oscilloscope, you can turn on a pin before the if statement and turn it off after your code snippet. (Using direct port manipulation PORTB, not the Arduino library function) With a scope you can see how long it takes to run the code.

  3. Use the micros() function in the Arduino library. Place one before and after the code snippet. However here you will have a couple of microseconds overhead since the 'micros()' has to run as well.

  4. Use a debugger or hardware simulator that can count cycles. Put a breakpoint on the first statement of the code snippet and one on the statement after the snippet. delta_t = cycles / clock_freq (in line with Oldfart's answer)

\$\endgroup\$
  • 3
    \$\begingroup\$ There is also overhead when turning pins on and off. The overhead is especially bad when going through the Arduino library. I think I would stick with micros(). \$\endgroup\$ – Dampmaskin Jan 9 at 11:47
  • 9
    \$\begingroup\$ @Dampmaskin since OP used PIND6 I figured he won't use the Arduino library and just use PORTB6. Using this, the overhead is much smaller than micros() \$\endgroup\$ – Swedgin Jan 9 at 11:49
  • 4
    \$\begingroup\$ @ty_1917 It doesn't matter which editor you use, it was a reply to the other comment. Thing is, Arduino library functions have a lot of overhead. So when toggling using bit setting/clearing PORTB, it will be lot faster than toggling using the Arduino library function. I trust my scope more than using the micros() functions. \$\endgroup\$ – Swedgin Jan 9 at 12:15
  • 3
    \$\begingroup\$ One thing to watch out for is the compiler optimizing your code away. In the example given, 'val' is not used, so no code is generated to set it! \$\endgroup\$ – Bruce Abbott Jan 9 at 18:44
  • 5
    \$\begingroup\$ @old_timer there is no such issue on AVR — port writes take one cycle (if you need to read, mask, and write that's three) and hit immediately. \$\endgroup\$ – hobbs Jan 10 at 7:41
14
\$\begingroup\$

Let's do it!

Say we have the code

int main(void)
{
    volatile uint8_t val = 0;

    while (1) 
    {
        if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
            val = 1;
        } else {
            val = 0;
        }
    }
}

Say we use AVR GCC with optimization flag -O1, then the disassembly of the relevant section looks like this:

val = 1;
00000046  LDI R24,0x01      Load immediate, 1 Clock Cycle
    if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
00000047  SBIS 0x09,6       Skip if bit in I/O register set, 1/2 Clock Cycle 
00000048  RJMP PC+0x0003        Relative jump, 2 Clock Cycle
        val = 1;
00000049  STD Y+1,R24       Store indirect with displacement, 2 Clock Cycle 
0000004A  RJMP PC-0x0003        Relative jump, 2 Clock Cycle
        val = 0;
0000004B  STD Y+1,R1        Store indirect with displacement, 2 Clock Cycle 
0000004C  RJMP PC-0x0005        Relative jump, 2 Clock Cycle

I added the comments with the number of clock cycles based on the datasheet page 281ff. Note, that SBIS can take 1 or 2 cycles dependant on whether the next instruction is skipped or not.

So we see, that the if-branch (lines 0x47, 0x49, 0x4A) takes 6 clock cycles, and the else branch (lines 0x47, 0x48, 0x4B, 0x4C) takes 7 clock cycles.

Now let's take the longer one. With 16MHz it takes (7/16e6) seconds, i.e. you sample with a frequency of 16e6/7 Hz. Since you want to always have at least one sample point at low/high, you need to sample with >2x of your signal frequeny, i.e. your signal frequency must be <16e6/(7*2) Hz which is ~1Mhz.

Now notice, that this is a purely virtual example, since val is set correctly, but there is no way that you can test it. You need to somehow output val, which will add extra clock cycles.

\$\endgroup\$
  • \$\begingroup\$ Is it better or any advantage to declare as volatile uint8_t outside the while loop as you did? In my code it was declared as uint8_t val = 0; inside the while loop. So should I declare as volatile uint8_t and outside the while loop as you did? \$\endgroup\$ – ty_1917 Jan 10 at 1:08
  • 3
    \$\begingroup\$ @ty_1917: Shouldn’t make any difference, yar only declared it as volatile to prevent the compiler from optimising it away (since the variable is not read in his minimal example). You can always look at the assembly code (or benchmark the code) to make sure. \$\endgroup\$ – Michael Jan 10 at 10:03
  • 2
    \$\begingroup\$ This is good exercise and exactly how you should do it - always look at the machine code generated. However, I think the use of a volatile result variable in this example might screw up the optimization a bit, the only part that needs to be volatile is the register itself. Check out this example. It's effectively 4 instructions with -O2 on. \$\endgroup\$ – Lundin Jan 10 at 12:44
  • 2
    \$\begingroup\$ @ty_1917 volatile in this case only serves as an artificial way to get the compiler to generate any code at all with optimization enabled. If not for volatile, the whole code would get removed by the optimizer. However, as indicated by my comment above, volatile has a slight impact on performance too. It is better to do as I did in the godbolt link above and disassemble an isolated function - then the compiler is forced to generate the code, because it doesn't know how/if that function will be called. \$\endgroup\$ – Lundin Jan 10 at 12:47
  • 2
    \$\begingroup\$ Also note that when writing in C, not asm, the performance of a snippet can depend on surrounding code, depending on how the compiler optimizes it into earlier / later code. (Even if you do keep the types the same). And of course it depends on optimization level, which most answers didn't even mention. \$\endgroup\$ – Peter Cordes Jan 10 at 19:40
11
\$\begingroup\$

I normally use the built-in simulator of Atmel Studio which has a cycle counter in the processor status window. This is a combined screenshot from stepping though the code:

Enter image description here]

As you can see, the cycle counter is 18 before and 22 after stepping through the two statements. So according to the simulator it takes 4 cycles.

You can use this to step through the whole loop.

\$\endgroup\$
  • \$\begingroup\$ Is the clock frequency 16MHz or 1MHz? I set it as 16MHz but as in your screen shot it shows 1MHz under the "Processor Status". It takes 4 cycles but what is the frequency per cycle? \$\endgroup\$ – ty_1917 Jan 10 at 10:36
  • 8
    \$\begingroup\$ The number of cycles to execute instructions is independent of the frequency. \$\endgroup\$ – Oldfart Jan 10 at 10:38
  • \$\begingroup\$ Yes fine, but I wonder how long it takes per cycle. Because that determines the max pulse train input frequency to be read which I ask in my question. \$\endgroup\$ – ty_1917 Jan 10 at 10:39
  • \$\begingroup\$ if ((PIND & (1 << PIND6)) == (1 << PIND6)) { val = 1; } else { val = 0; } this reads the state and if the input pulse train is 1GHz it cannot read the state for instance correct because the mcu clock is much slower than 1GHz. Im trying to figure out what is the max freq it can read the pin state correctly. \$\endgroup\$ – ty_1917 Jan 10 at 10:41
  • 1
    \$\begingroup\$ Yar has already giving example calculations how to get from CPU cycles and clock frequency to sample rate. Also take note of her/his warning about output val. \$\endgroup\$ – Oldfart Jan 10 at 10:54
8
\$\begingroup\$

If speed is important for this code, the following is probably noteworthy:

You could just write

val = (PIND & (1 << PIND6)) != 0;

or

val = 1 & (PIND >> PIND6);

I guess the last one is shorter/faster.

Concerning speed/time estimation: Either

  • let your compiler generate an assembler listing file (*.lst) or
  • look at the disassembled code

and then look up and add the execution times (clock cycles) of the instructions.

What frequencies your code can "handle" of course depends on how often it is called, i.e., it depends on the speed of surrounding/calling code (i.e., how often the code snippet is visited), not only on the code snippet itself.

\$\endgroup\$
  • 14
    \$\begingroup\$ "I guess the last one is shorter/faster." - I tried all 3 variations in AVR GCC 5.4.0 with optimization Os, and they generated identical code (4 instructions/cycles). \$\endgroup\$ – Bruce Abbott Jan 9 at 19:04
  • 6
    \$\begingroup\$ That's a sign that the compiler is doing a good job. \$\endgroup\$ – Curd Jan 9 at 19:13
  • 4
    \$\begingroup\$ So write the most readable code, which is probably bool val = PIND & (1u << PIND6);. \$\endgroup\$ – Lundin Jan 10 at 12:59
  • \$\begingroup\$ @Lundin: but that line gives different results: if input is high val will be 1 << PIND6 instead of 1. \$\endgroup\$ – Curd Jan 10 at 13:09
  • 3
    \$\begingroup\$ @Curd No. Use standard C bool from stdbool.h which expands to _Bool. \$\endgroup\$ – Lundin Jan 10 at 13:40
0
\$\begingroup\$

How can we estimate roughly the maximum pulse frequency the above code can handle to read the state correct?

It will always read the state correctly. The question I think you are trying to ask is what is the maximum frequency that it can 'measure' without missing any highs or lows.

Should we find how many clock cycles it uses and multiply it by the clock frequency?

Basically yes. The important factor is the time between each read of the port. Note that depending on what the machine code does this may not always be the same, so you should use the maximum time between reads.

And if so, how can I do it in practice?

You can disassemble the code and work out how much time each instruction takes, or step through it in the simulator, or run the code in an actual ATmega328p and monitor the physical output (which might be eg. toggling an output pin or displaying the frequency on an LCD screen).

Note that the results are critically dependent on what machine code the compiler generates. With optimizations on any variables that don't contribute to output may be optimized away, and other seemingly trivial changes can have a large effect on the amount of code generated. Therefore the only guaranteed accurate way to test your code is in its entirety. Running small snippets of isolated code can give a very misleading idea of performance in the finished application.

For example, here is the listing for the code in your question:-

int main(void) {
  86:   89 e1           ldi r24, 0x19   ; 25
  88:   90 e0           ldi r25, 0x00   ; 0

        uint8_t val;

        for (int i=0; i<25; i++) {
            data <<= 1;
            PORTD &= ~(1 << 5);
  8a:   5d 98           cbi 0x0b, 5 ; 11
 //           _delay_us(2);
            PORTD |= (1 << 5);
  8c:   5d 9a           sbi 0x0b, 5 ; 11
 //           _delay_us(2);

            if ( (PIND & (1 << PIND6)) == (1 << PIND6) ) {
  8e:   29 b1           in  r18, 0x09   ; 9
  90:   01 97           sbiw    r24, 0x01   ; 1

    while (1) {

        uint8_t val;

        for (int i=0; i<25; i++) {
  92:   d9 f7           brne    .-10        ; 0x8a <main+0xa>
  94:   f8 cf           rjmp    .-16        ; 0x86 <main+0x6>

No code is generated for val and data, and the inner loop only has 5 instructions taking 9 cycles. With a 16 MHz clock the inner loop time is 62.5 ns * 9 = 562.5 ns, which should be able to keep up with an input frequency of ~888 kHz.

Next I output data to PORTD, which forces the compiler to generate code for it:-

    while (1) {

        uint8_t val;

        for (int i=0; i<25; i++) {
            data <<= 1;
  90:   88 0f           add r24, r24
  92:   99 1f           adc r25, r25
  94:   aa 1f           adc r26, r26
  96:   bb 1f           adc r27, r27
            PORTD &= ~(1 << 5);
  98:   5d 98           cbi 0x0b, 5 ; 11
 //           _delay_us(2);
            PORTD |= (1 << 5);
  9a:   5d 9a           sbi 0x0b, 5 ; 11
 //           _delay_us(2);

            if ( (PIND & (1 << PIND6)) == (1 << PIND6) ) {
  9c:   49 b1           in  r20, 0x09   ; 9
            } 
            else {
                val = 0;
            }

            data |= val;
  9e:   46 fb           bst r20, 6
  a0:   44 27           eor r20, r20
  a2:   40 f9           bld r20, 0
  a4:   84 2b           or  r24, r20
  a6:   21 50           subi    r18, 0x01   ; 1
  a8:   31 09           sbc r19, r1

    while (1) {

        uint8_t val;

        for (int i=0; i<25; i++) {
  aa:   91 f7           brne    .-28        ; 0x90 <main+0x10>
            }

            data |= val;
        }            

    PORTD = (uint8_t) data;
  ac:   8b b9           out 0x0b, r24   ; 11

        // The rest of the code

    }
  ae:   ee cf           rjmp    .-36        ; 0x8c <main+0xc>

The inner loop now has 14 instructions taking 17 cycles, and the maximum frequency it can accurately follow is almost halved.

Finally I make data static to force the compiler to store it in memory (which might be required for a more complex program):-

    while (1) {

        uint8_t val;

        for (int i=0; i<25; i++) {
            data <<= 1;
  9a:   40 91 00 01     lds r20, 0x0100 ; 0x800100 <_edata>
  9e:   50 91 01 01     lds r21, 0x0101 ; 0x800101 <_edata+0x1>
  a2:   60 91 02 01     lds r22, 0x0102 ; 0x800102 <_edata+0x2>
  a6:   70 91 03 01     lds r23, 0x0103 ; 0x800103 <_edata+0x3>
  aa:   44 0f           add r20, r20
  ac:   55 1f           adc r21, r21
  ae:   66 1f           adc r22, r22
  b0:   77 1f           adc r23, r23
  b2:   40 93 00 01     sts 0x0100, r20 ; 0x800100 <_edata>
  b6:   50 93 01 01     sts 0x0101, r21 ; 0x800101 <_edata+0x1>
  ba:   60 93 02 01     sts 0x0102, r22 ; 0x800102 <_edata+0x2>
  be:   70 93 03 01     sts 0x0103, r23 ; 0x800103 <_edata+0x3>
            PORTD &= ~(1 << 5);
  c2:   5d 98           cbi 0x0b, 5 ; 11
 //           _delay_us(2);
            PORTD |= (1 << 5);
  c4:   5d 9a           sbi 0x0b, 5 ; 11
 //           _delay_us(2);

            if ( (PIND & (1 << PIND6)) == (1 << PIND6) ) {
  c6:   29 b1           in  r18, 0x09   ; 9
            } 
            else {
                val = 0;
            }

            data |= val;
  c8:   26 fb           bst r18, 6
  ca:   22 27           eor r18, r18
  cc:   20 f9           bld r18, 0
  ce:   40 91 00 01     lds r20, 0x0100 ; 0x800100 <_edata>
  d2:   50 91 01 01     lds r21, 0x0101 ; 0x800101 <_edata+0x1>
  d6:   60 91 02 01     lds r22, 0x0102 ; 0x800102 <_edata+0x2>
  da:   70 91 03 01     lds r23, 0x0103 ; 0x800103 <_edata+0x3>
  de:   42 2b           or  r20, r18
  e0:   40 93 00 01     sts 0x0100, r20 ; 0x800100 <_edata>
  e4:   50 93 01 01     sts 0x0101, r21 ; 0x800101 <_edata+0x1>
  e8:   60 93 02 01     sts 0x0102, r22 ; 0x800102 <_edata+0x2>
  ec:   70 93 03 01     sts 0x0103, r23 ; 0x800103 <_edata+0x3>
  f0:   01 97           sbiw    r24, 0x01   ; 1

    while (1) {

        uint8_t val;

        for (int i=0; i<25; i++) {
  f2:   99 f6           brne    .-90        ; 0x9a <main+0xa>
  f4:   d0 cf           rjmp    .-96        ; 0x96 <main+0x6>

The inner loop code has now ballooned to 29 instructions taking 49 cycles, reducing the maximum measurable frequency to ~163 kHz. That simple addition of the static keyword was enough to make it over 5 times slower. But this is the realistic speed you might expect when the code is used in a larger application.

If you need the fastest possible speed independent of compiler quirks then you have 3 options:-

  1. Write finely crafted assembler code that uses each instruction in the most efficient way possible (other non-critical code can still be written in C).

  2. Use peripheral hardware such as the timer/counter unit or SPI.

  3. Add an external chip such as a prescaler to divide the frequency, or a shift register (eg. CD4031) to capture the waveform.

\$\endgroup\$

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.