Analog Sensors

In the interests of providing relevant information for these articles, some of these will cover the work I’m doing on the robot I’m building. This article will discuss analog inputs, such as those used by the ultrasonic ranging sensor (or URS, which we’ll focus on) and infrared distance sensor the robot is using. The example code is in 05_urs directory in the example code.

The URS uses ultrasonic “pings” to figure out how far objects in front of it are. The robot’s URS is constantly pinging, and the navigation system uses the distance measurement to determine if there are obstacles in its path and which direction it should turn. The navigation system wants a numeric value for distance, but the URS returns an analog value corresponding to the distance: a voltage between some reference value and ground that corresponds to the distance measured. The analog-to-digital converter takes this voltage and provides a numeric value.

Analog input, in general, works by providing a variable voltage on one of the analog pins. The ADC uses a reference voltage as a base to figure out what the highest possible value is, and the AVR’s analog-to-digital converter (ADC) converts between the analog voltage returned from the input to a digital value using a successive approximation ADC.

Using analog input is more involved than digital input; there are both hardware and software considerations that need to be factored in:

Both of the sensors that are used for proximity measurement on the robot return a distance; with a digital pin (limited to logic highs and lows), they would have to communicate the distance over some sort of digital protocol (such as I2C or SPI), which requires the use of multiple pins, or communicate the distance by using up several pins to provide a binary distance value. The analog pins allow an input in the 8- or 10-bit range on a single pin; among other applications, this works well for distance measurements.

For the robot, the battery power supply is stable enough that it just uses that as the analog reference voltage.

A small demo setup

Usually, when writing software in general it helps to build a test case or proof-of-concept of an idea before adding it into a larger system. In this article, we’ll mostly look at the PoC setup below:

A Maxbotics EZ4 is connected directly to the Arduino; the robot actually uses an ATmega2560, but using the ATmega328P-based Uno works well for the proof-of-concept. There are only three connections needed: power, ground, and an analog signal connected to ADC3; the only code that needs to be written is the code for regularly reading distance measurements. With this particular sensor, setting it up like this will cause it to continually take ranging measurements.

Initialisating the ADC

The URS can take readings once every 49ms, according to the datasheet. With that in mind, we’ll use a timer to regularly trigger pings. The ADC also has a “conversion completed” interrupt that we’ll use to update the latest readings from the URS. This use of interrupts will follow a general pattern: we’ll write background tasks as interrupt-driven components.

Here’s the ADC initialisation code:


#define URS_CHANNEL	ADC3D


/*
 * init_ADC prepares the ADC for use with the URS.
 */
static void
init_ADC(void)
{
	/* Use Vcc (the main power supply) as the reference. */
	ADMUX = _BV(REFS0);

	/* Left-align the results, which gives 8-bit precision. */
	ADMUX |= _BV(ADLAR);

	/* Select the URS in the channel multiplexor.*/
	ADMUX |= URS_CHANNEL;

	/*
	 * We really don't need a high sample rate, so we use a
	 * high prescale. A prescale of 128 with a 16 MHz clock
	 * means it samples at a rate of 125 kHz.
	 */
	ADCSRA = _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0);

	/* Enable the ADC. */
	ADCSRA |= _BV(ADEN);

	/* Disable digital inputs on the URS channel. */
	DIDR0 |= _BV(URS_CHANNEL);

	/* Kick off the first conversion. */
	ADCSRA |= _BV(ADSC);
}

The first thing that needs to be done is to set up the ADC multiplexor. Only three bits are used to select channels on the ATmega328P (see page 263 of the datasheet); the others are used in the configuration of the ADC. The REFS0 bit uses the main power bus as the reference voltage; it expects a capacitor to be connected to AREF (this is a decoupling capacitor used to smooth out power noise; Designing Embedded Hardware has a good explanation of decoupling capacitors). The demo doesn’t use one, but in a complex system, it’s a good idea.

The URS datasheet also mentions that the sensor returns values in the range of 0-255; we can tell from this that we only need 8-bit precision. The ADLAR bit tells the microcontroller to left-align results (see page 265); normally, the ADC provides 10-bit resolution, which requires a 16-bit register to store the results of the conversion. Reading a 10-bit result from the ADC is done with value = ADCL + (ADCH << 8) (page 265 notes that ADCL must be read before ADCH). However, for an 8-bit result, we only need to read ADCH. You’ll need to consult the datasheet for your sensor to determine what your conversion size requirements are.

The last part of setting up ADMUX is to set up the channel (the ADC term for which pin is being used) that will be used for the conversion; only one can be converted at a time. We’ll discuss a strategy later for building systems with multiple analog inputs. Page 263 gives the appropriate multiplexor bit for the channel the sensor is connected to (ADC3). One of the common mistakes that I’ve found myself making here is to set the bit with _BV(URS_CHANNEL): this sets bit 3, which is actually ADC8 in this case. It will virtually always result in the wrong channel being selected.

The ADC has two status registers; the B register contains conversion triggering setup that we won’t use here.

Similar to the timer, the ADC has a prescalar: this controls how fast the results are sampled. The sensor isn’t high precision, so we’ll stick to the highest prescaler value to reduce the burden on the microcontroller and save power. A prescaler value of 128 with the clock frequency of 16 MHz gives us a sample rate of 125 kHz, and it’s selected by setting bits ADPS2..0. The ADC enable bit (ADEN) turns on the ADC; in a low-power setting, this could be disabled when not in use as long as the increased time required for the first conversion is acceptable.

The DIDR register controls the digital buffer for the ADC pins: page 266 notes that the “corresponding PIN Register bit will always read as zero when this bit is set… to reduce power consumption.”

After setting up the ADC, the first conversion is kicked off: this will allow us to avoid the slower first read later. Note that interrupts aren’t enabled, and we haven’t enabled the ADC conversion completion interrupt yet, so no interrupt will be triggered when this is complete. Its only purpose is to “prime” the ADC.

We’ll also use a timer to control the ADC; however, the URS’s refresh rate of 49ms means we have to adjust the prescaler. A prescaler of 8 requires OCR1A to be set to 98000, which is too large to fit into the 16-bit register. We’ll bump up the prescaler to 64; now, 4 microseconds elapse for every timer tick which means OCR1A should be set to 12250 (0.25 ticks / μs × 49000 μs → 12250 ticks).

static void
init_timer1(void)
{
	/*
	 * Set the waveform generation mode to CTC with OCR1A as the
	 * top value.
	 */
	TCCR1B |= _BV(WGM12);

	/* Use a prescaler of 64. */
	TCCR1B |= _BV(CS11) | _BV(CS10);

	/* Trigger an interrupt on output compare A. */
	TIMSK1 |= _BV(OCIE1A);

	/* Set the output compare register to the update interval. */
	OCR1A = URS_CYCLE;
}

Timer ISR

The timer is used to trigger conversions: the URS can only provide readings every 49ms, after which a conversion should be kicked off.

/*
 * Timer1 is set up with a prescaler of 64, which means 4 microseconds
 * per tick. The URS can range once every 49ms, which translates
 * to a timer value of 12250.
 */
#define URS_CYCLE	12250


/*
 * The Timer1 ISR triggers an ADC conversion every URS_CYCLE ticks.
 */
ISR(TIMER1_COMPA_vect)
{
	/* Clear pending ADC interrupts. */
	ADCSRA |= _BV(ADIF);

	/* Trigger an ADC conversion. */
	ADCSRA |= _BV(ADSC);

	/* Enable the ADC interrupt. */
	ADCSRA |= _BV(ADIE);
}

The ISR should clear out any pending ADC interrupts first, then begin the process of kicking off a conversion. Setting the ADSC bit in ADCSRA (“ADC start conversion”) begins the conversion, and setting ADIE enables the ADC conversion complete interrupt.

The ADC ISR

Once a conversion has completed, we’ll want to store the result of the conversion and turn off the ADC interrupt until the next time it is triggered.

struct reading {
	/* val contains the last measurement from the ADC. */
	uint8_t		val;

	/* count stores the number of conversions that have occurred. */
	uint16_t	count;
};
volatile struct reading	sensor = {0, 0};


ISR(ADC_vect)
{
	/*
	 * Read the distance measurement into the sensor readout
	 * structure.
	 */
	sensor.val = ADCH;
	sensor.count++;

	/*
	 * Turn off the ADC interrupt and clear any pending
	 * interrupts.
	 */
	ADCSRA |= _BV(ADIF);
	ADCSRA &= ~_BV(ADIE);
}

The reason a count is provided with the value is so that other parts of the program can determine if a new measurement has been taken (by comparing to a previous count value) or if measurements have occurred (the count will be non-zero). The sensor only stores the value of ADCH because it is an 8-bit left-aligned value, otherwise it would need to get the value of the ADC as described previously. The avr/io.h header also defines the ADC macro that reads the 16-bit value correctly. We could do that here, as the value will always be in the 8-bit range, but it’s better to keep the code clear as to exactly what it’s doing. Reading the code later and seeing that only ADCH is being read will remind future you that the sensor only uses 8-bit resolution.

Great success

The final program (in 05_urs) reports distance measurements using the serial port:

Terminal ready
Boot OK.
URS reading #   50: 37
URS reading #  101: 37
URS reading #  152: 37
URS reading #  204: 37
URS reading #  255: 37
URS reading #  306: 37
URS reading #  357: 3

The distance changed when I waved my hand in front of the sensor, though it’s difficult to see from over there. You’ll have to take my word for it (or build this example).

Dealing with multiple analog inputs

The ADC can only perform a conversion on one channel at a time. What if multiple sensors are being used? One approach, if they’re all using the same timer cycle, is to set up an array of inputs, and cycle through them.

/*
 * The first three ADC channels (ADC0, ADC1, ADC2) are used.
 */
#define CHANNEL_COUNT	3
static uint16_t	readings[CHANNEL_COUNT] = {0, 0, 0};
static uint8_t  channel = 0;


ISR(ADC_vect)
{
	/*
	 * If there are channels still left to be read, then
	 * set the ADC to the new channel and kick off a new
	 * conversion.
	 */
	if (channel != sizeof(channels)) {
		readings[channel] = ADC;
		channel++;

		/*
		 * Clear the channel selection bits and set the active
		 * channel.
		 */
		ADMUX = (ADMUX & 0xF0) | channel;
		ADCSRA |= _BV(ADSC);
	}
	/*
	 * If all the channels have been read, then disable the
	 * interrupt.
	 */
	else {
		/*
		 * Turn off the ADC interrupt and clear any pending
		 * interrupts.
		 */
		ADCSRA |= _BV(ADIF);
		ADCSRA &= ~_BV(ADIE);

		/* Reset the channel counter. */
		channels = 0;
	}
}

In order to support this, we need to store a list of the last value from the ADC. In order to save on memory, we’re going to use the ADCs starting at 0 and going to CHANNEL_COUNT. This allows us to save some memory by not having to store channel IDs.

Inside the ISR, we’re going to chain interrupts in a way. First, we need to get the reading from the ADC (remember, the ISR is called when the conversion is done and a value is ready). Then we’ll move to the next channel and kick off the next conversion, which will trigger another interrupt on completion. As part of the channel setting, we clear the bottom four bits, which are used to select the ADC channel in our code; that is, unless the onboard temperature sensor is being used. This is a bit more work, but is well covered in section 23.8 of the datasheet.