MCP3008 Tutorial 05: Sampling Audio Frequency Signals 02

In the last module, we achieved a sampling rate of 35khz with the MCP3008 ADC and an Arduino. How fast can we actually get?

Objectives

1. Understand the importance of the Nyquist Rate.
2. Learn other methods for increasing the speed of your MCP3008 subsystem.

Setup
For this module, you’ll need the following equipment:

1. Mate the Education Shield with your Arduino UNO R3. If you’re using the MCP3008 Breakout Board, connect 5V to 5V, 3V3 to 3.3V, GND to GND, AGND to GND, CS to Digital4, MOSI to Digital11, MISO to Digital12, and CLK to Digital13. That’s a lot of pins!

2. Connect your Arduino to your USB cable, and use the Arduino IDE to upload the “Bare Minimum” sketch from the Basics section of the Examples.

How Fast Do We Need To Be

The first example in the last module showed that 500 samples taken of something that happened over 1800 times isn’t nearly sufficient to genuinely represent the signal that was measured. The problem is even trickier than that. If the signal you’re sampling is sufficiently faster than the speed at which you’re measuring, you can actually generate really solid results, that are absolutely and completely wrong. This is called aliasing… just like an international man of mystery will assume an alias as a disguise, a signal can assume an alias to fool you into thinking it’s something it’s not. Take the really nice 660Hz graph we generated previously. Hidden inside that signal, is another entirely reproducible sine wave, that could have been measured if we were sampling at a slower rate.

It seems to reason then, that we must always sample at some speed faster than the signal we’re attempting to measure… or rather, we have to sample at some speed faster than the fastest signal we expect to measure. This rate is called the Nyquist Rate, and I’ll just boil it down to “you have to sample twice as fast as the fastest signal you expect to measure”.

This is the rule of thumb I’ve used, but this is the sort of thing that people at University spend months learning in detail. Should anyone wish to correct this or explain it in greater detail (but friendlier language than textbook English), feel free to send me a note!

This is important to note, that sampling the signal at this rate is useful for frequency detection. We could sample this as a single point in time if we just wanted to know what voltage the signal was at and do that with ease, but in order to detect a frequency, or a frequency change, or record changing frequencies, we have to sample at the speed necessary to verify that we’re getting accurate measurements. Knowing how to increase to that speed will be useful for other usages of the MCP3008 as well though.

Increasing Sampling Speed Further

After isolating the serial commands from our measuring function, we experienced a pretty big performance gain. There are other places we can look to try to cut more fat from the scenario. Let’s have a look at what the adc_single_channel_read function is doing electrically to see what’s taking the most time.

Clock, MISO and MOSI are handled by the ATMEGA328P microcontroller, which is doing all the SPI signaling when it receives the command to do so from the SPI library. Short of increasing the clock frequency to 4Mhz (above the max 3.6Mhz listed in the datasheet), I don’t think we could improve the actual signaling. However, if you measure the time it takes in between each chunk of SPI activity (remember, we’re constantly generating three frames, so what you’re seeing is two runs through the for loop on that scope screen capture), we’re taking 15µs to make up our minds to do some more SPI stuff, which is just as long as it takes to do the SPI stuff in the first place. That’s a TON of time.

Well, inside the code, we can’t eliminate any of the three SPI transfer commands without losing data, and we can’t really speed them up without increasing the clock speed above the datasheet max (your clock frequency has to be some whole number ratio of 16Mhz, the clock speed of the Arduino, which is why we can’t specify a clock speed of 3.6Mhz… it’s either 4Mhz or 2Mhz). The two things we should focus on are the SPI transaction begin/end commands and digitalWrite.

Since we’re only dealing with a single chip, it’s configuration never changes. We’re also not turning the SPI bus on and off to release the pins for other purposes, so we don’t really need to beginTransaction and endTransaction every time. Let’s sandwich that around the for loop in setup instead and take it out of the function.

With those commands removed from constant execution, let’s see how fast we are now.

A savings of about 2µs. That’s not too shabby, but there’s a nasty little secret hiding in plain sight.

Digital Write vs. Port Commands

Looking at the scope trace, inside that vast span of no activity the chip select pin goes high and then goes low again. It may or may not be obvious, but what we’re focused on is the end of our function and the start of it again. Assuming the assembly language code that assigns variables to memory spaces, which is what our variable declarations and “return” command are doing, take up only a few 16Mhz clock cycles (and can’t be eliminated anyway), the main thing that’s happening is we’re bringing CS high at the end of the function, but immediately bringing it low again at the start of the function, each time through the for loop, with really nothing to slow it down in between. And yet, the time between the line going high and the line going low is about 7µs, just to toggle the line. In that span of time we can execute nearly one and a half frames of SPI data transfer.

I’m going to execute this code and view it on the oscilloscope.

All this code does is take a pin and toggle it on and off as fast as the Arduino is capable. I tested every single pin on the Arduino, digital and analog, and measured the speed at which the pin would switch on and off on the oscilloscope.

Pin TypePin NumberToggle Speed
Analog094.7kHz
Analog194.7kHz
Analog294.7kHz
Analog394.7kHz
Analog494.7kHz
Analog594.7kHz
Digital088.3kHz
Digital188.3kHz
Digital288.3kHz
Digital369.1kHz
Digital488.3kHz
Digital571.0kHz
Digital671.0kHz
Digital788.3kHz
Digital888.3kHz
Digital973.1kHz
Digital1067.6kHz
Digital1169.1kHz
Digital1288.3kHz
Digital1388.3kHz

Even at the fastest switching speed, which was on the analog pins, the fastest we could hope to turn something on and off would have a high period of around 5µs, the ADC CS pin is on Digital 4 which is sort of middle range (being a non-PWM pin), and has a pulse width of 5.4µs.

This is all because of digitalWrite(). The command is very easy to use, easy to understand, but terribly slow on the scale of microcontroller activity. What we need to do, to accelerate the amount of samples we achieve per second, is to bypass the Arduino command and manipulate the pin directly.

Much like the I2C and SPI libraries abstract the direct register manipulation in the back end libraries with very easy to use commands on the front end IDE, the basic command libraries do the same thing.

We can accomplish the same thing that digitalWrite does by using the PORT command. Digital Pin 4 is located in the PORTD register, so we need to toggle the fifth bit (counting from zero) without touching the other bits in the register. To bring the pin low, we use PORTD = PORTD & 0xEF; command. To bring the pin high again, we use PORTD | 0x10;.

Here is a scope trace showing toggling of the ADC CS pin using the digitalWrite technique. Note the frequency of 88.3kHz and pulse width of 5.360µs at the bottom of the trace…

Now here’s the scope trace showing the toggling rate of the same pin using the PORT commands in the main loop. We’ve zoomed way in on the time domain to 200ns per division. The frequency measured is now 1Mhz and the pulse width is 872ns!

The high time seemed a little long to me, and I suspected that was a result of however the main loop is processed. When I placed the port commands into their own for loop, within the main loop (sort of equivalent to a “while (1)” endless loop), the command became much more symmetrical… and over twice as fast again… 2.66Mhz.

With this in hand, we should be able to increase our sampling rate dramatically. Previously, from CS going to low to CS going low took 27µs. By changing to PORT commands, that duty cycle is now 17µs.

That corresponds to 58.8ksps, a sampling frequency of 58.8kHz. Here’s how this has progressed…

1. Original Code: 182Hz sampling speed.
2. Removed serial commands: 35kHz sampling speed
3. Replaced digitalWrite: 58.8kHz sampling speed

This is the graph we had before we moved to the PORT commands…

This is the graph we had after we moved to the PORT commands…

There are half as many periods of the sine wave in the second graph because we’re sampling it almost twice as many times.

Here is the fully optimized code…