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:-
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).
Use peripheral hardware such as the timer/counter unit or SPI.
Add an external chip such as a prescaler to divide the frequency, or a shift register (eg. CD4031) to capture the waveform.
val = ((PIND & (1 << PIND6)) == (1 << PIND6));
, orval = ((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\$val
anddata
are not used so they will be optimized away. \$\endgroup\$