Ultrasonic Anemometer Part 24: New Microcontroller and Software Controlled Gain

It’s been almost three weeks since my last post and some further progress has been made. I’ve upgraded the microcontroller and can now control the gain of the second amplifier stage in software. But let’s look at the changes in some more detail.

20160625_AnemometerDriver_008

New microcontroller: PIC32MX250

I mentioned last time that I was running out of code space, i.e. flash memory on the PIC32MC220. That particular model has only 32k of flash and I’m using the free version of the XC32 compiler. That free version only performs limited amounts of optimization and therefore produces rather large and slow code compared to the standard and professional version. But the other two are rather costly (around USD 500 and 1000, respectively) and are not really an option here.  And at least so far I’ve always had all the speed I needed so the only issue was flash memory.

So the straight forward solution was to upgrade the microcontroller to an otherwise identical model with more memory. The 250 I’m using now has four times more of both flash (now 128k) and ram (now 32k). I unsoldered the old chip using a hot air gun and soldered in a PIC32MX250 in its place. Now we have all the flash we need to continue our journey.

20160625_AnemometerDriver_005

Getting I2C to work

My design uses a dual op amp for the necessary amplification of the received signal. The first stage is ground-referenced and uses a simple resistive divider to set the gain (currently set at 11).

The second stage is biased to 1.65V (i.e. half way between ground and 3.3 volts) and has its gain controlled by a I2C digipot. The idea is to have the PIC control the digipot so it can adjust the gain as needed to compensate for any effect that might change the amplitude of the received signal.

So in order to control our digipot we first need to get the I2C interface working on the PIC. There are two (identical) I2C peripherals on the PIC32MX2xx and this design uses one of them (#2) exclusively to communicate to the digipot. The other one (#1) is then available to interface to the outside world.

I’ve written a set of simple functions to use the I2C interface. So far I’ve only implemented  master transmit mode since that’s the only thing we need here.

  • void app_i2c_internal_init(void);
  • bool app_i2c_internal_write(uint8_t address, uint8_t *data, uint8_t length);
  • void app_i2c_internal_writeDigipot(uint8_t value);

The functions are entirely non-blocking so they can be called from within the interrupt service routines that do the measurement.

Fixing a design bug

Unfortunately, I made a mistake in the schematic when I referenced the feedback loop of the second op amp stage to ground instead of the mentioned 1.65 volts. Now let’s look at what that did to the signal. First, below is the signal with the digipot at 0 (of 256). The gain is set to one and all is well.

AmpError_Pot0

But at a setting of 64, some clipping starts to occur as shown below.

AmpError_Pot64

With the gain turned up to 4 with a digipot setting of 192 things get really uggly with a completely unrecognizable output signal.

AmpError_Pot192

I was extremely lucky that the correct signal was available right next to the pin where I needed it so all I had to do was to cut the ground connection with a carving knife and to connect the correct signal using a tiny bit of wire.  The change is hardly visible on the photo below. Can you spot it? It’s almost at the center of the photo at pin 5 (bottom right) of the small 8-pin IC. It now connects to the 10k resistor at the bottom instead of the ground plane on its right.

20160626_AnemometerDriver_009

Controlling the gain in software

With that error fixed we can now set the gain as we please. Here some examples. Yellow is the (unamplified) input signal whereas green is the output of the (fixed) first amplifier stage.  The purple signal is calculated as the difference between the DAC+ and DAC- signals. Those two signals is what we feed into the PIC to be measured.

With the gain at 1 (digipot at 0) we get a about 2.6 volts peak-peak. A bit little but sufficient.

Amplifier_Pot0

This is probably about what we want. Digipot at 50 resulting in a peak-peak amplitude of a bit above 3 volts.

Amplifier_Pot50

Needless to say that we can jerk the gain up far more than that if we want.  Below is an example where a gain of 4 results in a seriously clipped signal.

Amplifier_Pot192

Now let’s look a a situation where such a high gain might actually be useful. In the example below I’ve lowered the input voltage to only 5 volts. As a result the received signal is only 90mV in amplitude instead of the 220mV seen above. That’s easily compensated with a bit of help from our digipot. With a setting of 170 we get a nice, clean 3 volt output signal.

Amplifier_Vin5_Pot170

Calculating the necessary gain

For the examples above I’ve explicitly set the gain in the code. But what we really want is to have the software calculate and set the necessary gain automatically.

So I’ve implemented a simple algorithm to do so. I set some target value for the peak-to-peak amplitude of the signal measured by the DAC. Knowing the amplitude and digipot setting of the last measurement I calculate the optimal gain and digipot setting for the next measurement.

AmplitudesGain1

Above is the signal without this automatic gain setting in place (gain=1). As you can see, the peak-to-peak amplitudes vary quite a bit between transducer pairs. So the gain calculations, too, work on a transducer pair basis. Each direction gets its own setting to compensate for different transducer sensitivities.

Below is a screenshot of the result. Notice how now all transducer pairs have identical (and somewhat larger) amplitudes.

EqualAmplitudes

That’s it for today. Click here for my next post.

Ultrasonic Anemometer Part 23: First successful measurements

In my last post I was happy to report that I managed to get the USB interface to work. This interface has since proved to be extremely valuable in software development and testing. While the device is taking measurements you can look at the results (or intermediate results) at your PC in real time. You can even log large amounts of data to a .csv file and inspect the results in Excel.

20160514_StandaloneAnemometer_049

But let’s look at the developements in a bit more detail. Last time the device was sending pulses and capturing some of the resulting zero-crossings but not much more than that. And in a not very structured way.

Measuring amplitude

Now it’s not only capturing the times when the zero-crossings occur but also measures the amplitude of the half-waves in between. In order to do that an interrupt is generated at every zero-crossing capture. Inside that interrupt service routine time of the zero-crossing is saved (as previously) and the ADC is started. In order to measure the amplitude at its maximum (or minimum in case of a negative half-wave), the ADC first samples the input for a 6.25 microseconds (which corresponds to a quarter-wave at 40kHz). I’ve chosen the sampling period slightly shorter to compensate for the time from when the zero-crossing occured unil the ADC is started. After the sampling period the internal sample-and-hold amplifier switches from sample to hold and the actual analog-to-digital conversion takes place.

The PIC’s datasheet advertises a maximum sampling rate of 1 million samples per second. Beware, though, that this requires separate Vref+ and Vref- connections and doesn’t work in differential mode anyway. In my configuration without separate analog references and differential measurements the maximum sampling rate drops to 400k samples per second. And the reported conversion time doesn’t include the sampling time yet. I wasn’t aware of that but luckily the PIC’s ADC is still more than fast enough for what I’m doing here.

20160428_StandaloneAnemometer_028

Finding the absolute phase

Capturing the zero-crossings is relatively easy and the results are very precise. But there’s a challenge: there are hundreds of zero crossings for each burst of pulses sent. How do you know which zero-crossings to use?

The Arduino-based anemometer used an analog envelope detector to solve this problem. After averaging plenty of samples this works most of the time. But even after many attempts to tweek the analog circuit the envelope detector approach was never as reliable as I wanted it to be.

My standalone anemometer has an entirely different approach. Being able to digitally measure the amplitude of each half-wave we can tackle the challenge in software. We have a series of measurements and we have an idea of that the signal we are looking for looks like. Looking the scope screenshot below you could easily and reliably determine where the maximum amplitude occurs. That’s what we need to teach the software to do.

ZCD_Capture

One could, of course, just loop through the all the amplitudes and find the maximum. But there are a lot of similar amplitudes and so any noise would make the result unreliable.

I did a bit of reading on digital signal processing (DSP) and realized that this is a classic DSP problem. The solution is a matched filter. You take the signal you are looking for (also known as kernel or template) and multiply it sample by sample with the captured signal and sum up the results. Do that for every possible (or reasonable) overlap of signal and kernel and find the position where the result reaches its maximum.

If you’re new to the subject (like I am), Steven W. Smith’s Digital Signal Processing is a great place to start. It’s even available online for free at dspguide.com. The method is also referred as convolution or correlation depending on who you ask. This wikipedia article gives a nice introduction to the subject.

Implementing the matched filter

I’ve parameterized the software to capture 34 zero crossings as well as the n=33 amplitudes in between them. The kernel consists of k=17 data points with a peak in the middle.

There are n-k+1=17 possibilities to entirely overlap the kernel with the signal. We can skip all the odd numbers where a negative kernel value would be multiplied with a positive sample value and vice versa. This leaves us with 9 possible positions to chose from.

For each position we need to do 17 multiplications and 16 additions. And we need to do that for every single measurement, i.e. 512 times per second. That sounds like a lot of work and it probably is. Luckily, since this is a very common DSP task, microcontrollers like the PIC32 have a special instruction for this. It’s called MAC – Multiply Accumulate and executes in a single clock cycle. The result is just amazing. The corresponding ISR takes less than 30 microseconds, including some housekeeping tasks as can be seen in the screenshot below.

Once the maximum amplitude is found, the 16 zero-crossings around it (i.e. the 8 before and the 8 after) are summed up and the sum is saved as the result of that measurement.

ConvolutionTime

Averaging measurements

The goal is to report wind speed and temperature four times per second. Since there are 512 measurements per second we can use up to 128 individual measurements  (32 in each direction) to do our calculations.

So we have plenty of data to identify and exclude outliers and do some averaging of the remaining samples. Robust statistics is the key word here, click here for the corresponding wikipedia article.

For now I’m doing something reasonably simple: First the 32 results per direction are sorted. The 12 lowest and 12 highest results are ignored and only the 8 results in the middle are summed up.

This might seem wasteful but it makes the average very robust against outliers and still results in 16 x 8 = 128 zero-crossings being averaged. The resolution of each zero-crossing is equal to 1 / 48MHz or about 20.83 nanoseconds. Summing up 128 of them gives us a resolution of far below a nanosecond. Beware however that resolution doesn not imply accuracy.

As a last step, the resulting sum is multiplied with 125 and then divided by 768 to make the unit 1 nanosecond. So every 250 milliseconds we finally have 4 time-of-flight measurements with a resolution of 1 nanosecond. That’s what we will use to calculate the winds speed as well as wind direction and temperature.

This sorting and averaging step is a bit more costly in terms of computation time. It takes around 600 microseconds to complete (see below). But unlike the convolution, it only has to be performed 4 times per second so this is more than fine.

CalculatingAverageToF

Calculating wind speed

We are finally in a position to calculate the actual wind speed. There are various ways of doing this. For now I’ve just done the simple approach of taking the difference in time-of-flight, assuming room temperature and solving for wind speed. This means solving a quadratic equation for which I have resorted to floating point math and using <math.h> for the square root function.

I don’t have my wind tunnel any more so doing any testing was difficult. But one thing was soon obvious: at zero wind speed, the time-of-flight measurements match extremely well and are extrordinary stable from measurement to measurement. Also, looking at the intermediate results (i.e. the 128 single measurements before averaging) shows that there are basically no outliers present. I could randomly pick a measurement and still get a very reasonable value.

Something seems to be systematically wrong with the first of the 128 measurements but I haven’t had time to look into this. Otherwise, the results are impressively stable. And I’m only using a relatively primitive kernel for the matched filter.

USB data logging

As I’ve mentioned at the beginning, the USB interface lets me do some serious data logging even at this early stage of development. Here’s a list of what I can do by sending a character to the device from a USB terminal.

  • ‘Z’: Get all the 34 zero-crossings of a single measurement
  • ‘A’: Get all 33 amplitudes of a single measurement
  • ‘F’: Get a full single measurement, i.e. 34 zero-crossings plus 33 amplitudes
  • ‘T’: Get all the 4 x 32 = 128 time-of-flights for a measurement series
  • ‘R’: Get the 4 averaged time-of-flights as well as wind speed and temperature

Some versions of these commands also let you specify the direction you’re interested in as well as how many sets of data you want to receive. This makes it easy to log large amounts of data that you can use to test possible algorithms on your PC before you implement them on the PIC.

20160428_StandaloneAnemometer_025

Next steps

The next step is to get the I2C digipot working so I can control the amplification in software. My idea is to aim for a maximum amplitude of around 3 volts independent of wind speed and so on.

There’s also plenty of work to do to improve the algorithm that calculates wind speed and temperature. And then I also have to implement the I2C and SPI interfaces that let the anemometer communicate to other embedded devices like an Arduino or Raspberry Pi.

Having used floating point math and <math.h> I’m also running out of flash memory. I’m currently using 93% of flash (32kB total) and 52% of ram (8kB total). There will be a slightly revised board (Rev B) that uses a PIC32MX250 which is identical to the MX220 used here but has four times as much flash and ram.

That’s it for today. On the overview page you find the software at the stage of developement just described as well as some examples of logged data (all at zero wind speed) as .csv files.

The next post on this project is here: Part 24.