This article describes a mistake that can occur in a software sampling algorithm, regardless of your chip, hardware, or what is being measured. If you sample as fast as your code can run, rather than based on a timer, then the speed of your code must not be affected by the signal.
I made a tool that measures duty cycle and bit rates, such as for sensors and serial communication. However, I discovered that the poor accuracy wouldn’t have passed my own tests. When fed a crystal-controlled 50% duty-cycle signal, the tool displayed 41.16%. That’s horrible.
Let’s look at the sampling code:
if ( PinIsHigh(TIMER_CAPTURE_PORTIN, TIMER_CAPTURE_PIN) ) { gSamplesHigh++; } gSamplesTotal++;
In summary: Count the number of times the pin is high. Count the number of samples. Later on, display gSamplesHigh*100/gSamplesTotal to show the duty cycle percentage.
That code seems pretty innocuous. Assuming the code gets called often enough, I don’t see what could be wrong with it.
To debug the problem, I toggle a spare pin every time a sample is taken. Let’s see what the digital scope sees:
Inconsistent sampling rate due to counting high states only
The top line is the input signal. Plenty slow enough for adequate sampling. No noise. Sure looks like 50%.
The next line is the pin being toggled each time a sample is measured. Hmm... notice that the pin toggles faster when the input signal is low. (On the right, the pin stops toggling when the display is updated -- but that’s not the issue here.)
When the input pin is high, the microcontroller has to spend some time incrementing a counter. Because the chip has more work to do when the pin is high, it ends up taking fewer samples. Thus, the number of high samples is underrepresented.
Even though we don’t need to count low samples, let’s add in a low counter to balance the workload.
if ( PinIsHigh(TIMER_CAPTURE_PORTIN, TIMER_CAPTURE_PIN) ) { gSamplesHigh++; } else { gSamplesLow++; }
Now the display shows 49.66% for a 50% duty-cycle signal. Not bad. But, why isn’t it perfect?
Well, after incrementing gSamplesHigh, the microcontroller needs to jump over the gSamplesLow++ instruction. So, the high branch still has slightly more work to do.
if ( PinIsHigh(TIMER_CAPTURE_PORTIN, TIMER_CAPTURE_PIN) ) { gSamplesHigh++; } if ( PinIsLow(TIMER_CAPTURE_PORTIN, TIMER_CAPTURE_PIN) ) { gSamplesLow++; }
Now the display shows 50.18% for a 50% duty-cycle signal. Better, but still wrong.
In this case, I believe that the high path is now starving the low path during transitions. That is, if the signal is just about to switch from high to low, then high takes long enough that it is consuming some of low’s sample period.
Instead of checking the pin directly twice, lets sample the pin only once.
sample = TIMER_CAPTURE_PORTIN; if ( BitIsSet(sample, TIMER_CAPTURE_PIN)) { gSamplesHigh++; } if ( BitIsClear(sample, TIMER_CAPTURE_PIN)) { gSamplesLow++; }
Now the display shows 49.99% for a 50% duty-cycle signal. Very nice. The same number of clock cycles are executed regardless of whether the sample is high or low.
Consistent sampling by symmetrical code
When we first approached this problem, the time it took to increment gSamplesHigh significantly altered the result. Why? Shouldn’t that be a fairly fast operation?
Let’s peek at the compiled assembly code for only “gSamplesHigh++":
E081 LDI R24,1 E090 LDI R25,0 E0A0 LDI R26,0 E0B0 LDI R27,0 9040 00B3 LDS R4,_gSamplesHigh+2 9050 00B4 LDS R5,_gSamplesHigh+3 9020 00B1 LDS R2,_gSamplesHigh 9030 00B2 LDS R3,_gSamplesHigh+1 0E28 ADD R2,R24 1E39 ADC R3,R25 1E4A ADC R4,R26 1E5B ADC R5,R27 9230 00B2 STS _gSamplesHigh+1,R3 9220 00B1 STS _gSamplesHigh,R2 9250 00B4 STS _gSamplesHigh+3,R5 9240 00B3 STS _gSamplesHigh+2,R4
Ouch. I expected only four instructions. That’s what I get for incrementing a 32-bit global variable on an 8-bit microcontroller. No wonder it was taking so long and affecting the results.
Let’s use a single byte local variable, and only add the local results when we’ve obtained a bunch.
sample = TIMER_CAPTURE_PORTIN; if ( BitIsSet(sample, TIMER_CAPTURE_PIN)) { samplesHigh++; } if ( BitIsClear(sample, TIMER_CAPTURE_PIN)) { samplesLow++; } samplesTotal++; if ( samplesTotal > 250 ) { gSamplesHigh += samplesHigh; gSamplesTotal += samplesTotal; samplesHigh = 0; samplesTotal = 0; }
Rather than examining the assembly code, the performance improvement will be displayed by the digital scope.
Consistent sampling at double rate
So, with a couple of improvements, the code now samples accurately at twice the rate. Let’s just hope an optimizing compiler doesn’t undo some of these 'fixes’.
While I was at it, I designed a printed circuit board to replace the generic protoboard version. It looks much more professional.
Bits per second PCB