The Danger of Maximum Software Sampling Rates

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

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

Consistent sampling by symmetrical code

Performance Improvement

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

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’.

Printed Circuit Board

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

Bits per second PCB