Lesson 3 - Analog Input (ADC)
How to received and interpret analog data (i.e., 'interacting with the Real world')
In our previous tutorial (on GPIO) we learned how to turn an LED on or off and check whether a button is currently pressed or not. As useful as that is, it's probably going to get dull pretty quickly since you can only do so many things with a simple button or LED. The reality is that a great many of the devices that you will want to interact with aren't going to be digital, or consist of only two possible 'states' (On/Off, True/False, etc.). Almost every dial or knob on any modern electronic device, for example, is probably analog. Since our microcontrollers are digital, what that means is that we need to find a way to convert those analog signals into something 'digital' that our microcontroller can actually understand. That's where Analog to Digital Converters (ADCs) come in, and thankfully for us the LPC2148 has two of them built in.
ADCs essentially act as a bridge (or a 'translator') between the messy outside analog world and the cozy, black and white (green and white?) digital world our microcontrollers live in. They work by converting voltage to a numeric value that the microcontroller can understand. For example, with an internal voltage of 3.3V (which is the Vref on the LPC2148) and your ADC set to return the maximum 10-bit data (meaning you have possible values between 0 and 1023), 0.0V would return 0, 3.3V (or higher) would return 1023, and 1.65V would return ~512.
They only work in one direction (reading data from outside and sending it 'into' the chip), but life without them would be a lot more challenging ... or at the very least a lot more expensive (analog devices are often much cheaper than their digital counterparts). In this lesson, we're going to show you how to use several analog devices to accomplish some common tasks with an analog to digital converter:
- Determine the current position on a simple rotary dial (aka 'potentiometer')
- Determine where a user is currently touching a touch screen
- Measure the distance between two points with an ultrasonic range finder
Before we jump into the examples above, though, let's get started with the absolute basics. In the case of ADC, we're actually going to work backwards: we'll start with a simple working example of how to use ADC, and then explain what's going on in this code. The reason for this is that ADC -- and most other peripherals on the LPC2148 -- requires a little bit more effort to use than GPIO, since it needs to be properly 'configured' and enabled before it can be used. If you're just getting started, though, it's more motivating to see something working before diving into all the little details of how to make it work.
Initialise ADC0.3, and constantly read it's current value.
#include "lpc214x.h"
// This example assumes that PCLK is 12Mhz!
int main(void)
{
// Initialise ADC 0, Channel 3
adcInit0_3();
// Constantly read the results of ADC0.3
int results = 0;
while (1)
{
results = adcRead0_3();
}
}
// Initialise ADC Converter 0, Channel 3
void adcInit0_3(void)
{
// Force pin 0.30 to function as AD0.3
PCB_PINSEL1 = (PCB_PINSEL1 & ~PCB_PINSEL1_P030_MASK) | PCB_PINSEL1_P030_AD03;
// Enable power for ADC0
SCB_PCONP |= SCB_PCONP_PCAD0;
// Initialise ADC converter
AD0_CR = AD_CR_CLKS10 // 10-bit precision
| AD_CR_PDN // Exit power-down mode
| ((3 - 1) << AD_CR_CLKDIVSHIFT) // 4.0MHz Clock (12.0MHz / 3)
| AD_CR_SEL3; // Use channel 3
}
// Read the current value of ADC0.3
int adcRead0_3(void)
{
// Deselect all channels and stop all conversions
AD0_CR &= ~(AD_CR_START_MASK | AD_CR_SELMASK);
// Select channel 3
AD0_CR |= (AD_CR_START_NONE | AD_CR_SEL3);
// Manually start conversions (rather than waiting on an external input)
AD0_CR |= AD_CR_START_NOW;
// Wait for the conversion to complete
while (!(AD0_DR3 & AD_DR_DONE))
;
// Return the processed results
return ((AD0_DR3 & AD_DR_RESULTMASK) >> AD_DR_RESULTSHIFT);
}
The easiest way to test the ADC code above is with a 'potentiometer' (often referred to simply as a 'pot'). A potentiometer is a simple device that changes its' 'resistance' as you adjust it. It may have 10.0K Ohms resistance at one end, for example, and 0 Ohms at the other. Since this 'resistance' will affect the amount of current available on the ADC line, we can quickly determine where the dial is currently positioned between the two extremes.
Conveniently, there is a potentiometer hooked up to AD0.3 on the Olimex LPC-P2148 development board (which we can see by looking at the schematic in the very top right-hand corner [labelled 'AN-TR']). To test out the code above, simply run it and monitor the value of 'results' in the main method (either by setting a breakpoint, or by creating a 'watch').
If you are using
Crossworks, you should also be able to see the results of the A/D conversion by adjusting your code in 'main' as follows:
while (1)
{
results = adcRead0_3();
debug_printf("%d\n", results);
}
As you turn the potentiometer with a small screw-driver, you should see the current position of the dial getting 'converted' from analog to digital and receive a value between 0 (at one end) and 1023 at the other end. And presto ... you have a fully working 10-bit analog to digital converter! Now that we've got that out of the way, we can get on with boring you with all the dull details of how to make it happen!
Step 1: Configuring the ADC (ADCR)
Looking at the example code above, you can see that before we can read any results from the ADC we need to configure it (using "adcInit0_3"). In this particular case, we're using AD0.3, which means we are using A/D Converter 0 (the LPC2148 has two ADCs named AD0 and AD1), and channel 3 (out of a possible 8 channels). We chose this particular device and channel because the Olimex LPC-P2148 development board already has a 10K potentiometer on AD0.3 and the pin is conveniently 'broken-out' (you'll find a pin labelled AD03 towards the bottom of the prototyping area on the board).
One of the interesting features of ARM microcontrollers is that each pin can perform up to 4 different 'functions' ... though only one at a time! If you're not sure how this works, or what the advantages of this are, we discuss this in more detail in our description of PINSEL (a set of registers that we can use to indicate exactly which function we would like each pin to perform). In the case of AD0.3 (located on pin 15 of the lpc2148), the pin can be configure to perform any of the following functions (see page 71 of the LPC2148 User's Manual):
- P0.30 - General Purpose Input/Output 0.30
- AD0.3 - Analog/Digital Converter 0, Input 3
- EINT3 - External Interrupt 3
- CAP0.0- Capture input for Timer 0, Channel 0
This 'pin selection' is accomplished with the following line of code (again, feel free to consult the PINSEL page for more details):
// Force pin 0.30 to function as AD0.3
PCB_PINSEL1 = (PCB_PINSEL1 & ~PCB_PINSEL1_P030_MASK) | PCB_PINSEL1_P030_AD03;
It's always good practice to make sure that a peripheral is powered on (using the PCONP register) before trying to use it. This can be accomplished for ADC0 with the following line of code:
// Enable power for ADC0
SCB_PCONP |= SCB_PCONP_PCAD0;
Configuring the A/D Converter is probably the most complicated part, since you need to know certain things about the way your microcontroller is currently set up (primarilly, the value of PCLK, which determines the 'speed' at which your microcontroller is running). You also need to do a little bit of math to set everything up properly. It isn't complicated once you understand it, but it can be intimidating at first to know what all these values mean, and which ones you should be using in your specific situation.
To configure the A/D Converter, we need to pass a specific 32-bit value to the appropriate ADCR, or "Analog/Digital Control Register" (see p.270-272 of the LPC2148 User Manual for more details). This 'control register' (defined in lpc214x.h as 'AD0_CR' and 'AD1_CR') manages the configuration of our A/D converter, and determines a variety of things, including:
- SEL - Which channel should be used (0..7)
- CLKDIV - A value to divide PCLK by to determine which speed the A/D Converter should operate at (up to a maximum of 4.5MHz)
- CLKS - How precise the conversion results should be (between 3 and 10 bits)
- PDN - Whether the A/D Converter is currently active (1) or sleeping (0)
The 32-bit Analog/Digital Control Register has the following format:
| Function | - | EDGE | START | - | PDN | - | CLKS | BURST | CLKDIV | SEL |
| ADCR Bit(s) | 31..28 | 27 | 26..24 | 23.22 | 21 | 20 | 19..17 | 16 | 15..8 | 7..0 |
Unfortunately, that may not make very much sense to you, but without going into every little detail of every possible configuration option (see p.270-272 for the User's Manual for full details), we'll try to explain the register values that are being used in the example at the beginning of this tutorial: SEL, CLKDIV, CLKS and PDN.
SEL
This set of 8 bits corresponds to the 8 different 'channels' available on either A/D converter. You can indicate which channel you wish to use by setting it's appropriate bit to '1'. For example, since we are using AD0.3 in this case (channel 3), we would pass the following value to AD0_CR:
| - | EDGE | START | - | PDN | - | CLKS | BURST | CLKDIV | SEL |
| AD0_CR | **** | * | *** | ** | * | * | *** | * | ******** | 00001000 |
CLKDIV
The A/D Converters on the LPC2148 are able to run at a maximum speed of 4.5MHz. The conversion speed is selectable by the user, but the only catch is that to arrive at a number equal to or less than 4.5MHz, we need to 'divide' our PCLK (the speed at which our microprocessor is running) by a fixed number, which we provide (in binary format) using the 8 CLKDIV bits.
The default PCLK for your microcontroller is 12MHz (since the Olimex LPC-P2148 has a 12.0MHz crystal installed on it), but this can be 'multiplied' up to 60MHz, so you need to know what 'speed' you have set your mcu to before providing a value in CLKDIV.
We are going to assume that we are running at 12.0MHz (since we haven't covered how to adjust the mcu speed yet!). In order to stay below the ADC's maximum speed of 4.5MHz, we would need to divide our PCLK (12MHz) by 3, which will give us 4MHz (the closest value we can have with a 12MHz clock since we need to divide by a whole number). In order to avoid a 'divide by 0' error, though, the A/D control register will add one to whatever value you supply. This means that if we want to divide the PCLK by 3, we actually need to provide '2', which will be adjusted up one to '3' by the control register. (This is one of the little details that can cause your software to malfunction if you don't read the user manual and pay close attention to how to configure your peripherals.)
This means that if we were running at 12.0MHz, we could achieve a 4.0MHz A/D conversion speed by setting the following CLKDIV bits in AD0_CR ("00000010" being the binary equivalent of 2):
| - | EDGE | START | - | PDN | - | CLKS | BURST | CLKDIV | SEL |
| AD0_CR | **** | * | *** | ** | * | * | *** | * | 00000010 | ******** |
And what if we were running at 48.0MHz, for example? We would be able to achieve a maximum conversion speed of 4.36MHz by dividing 48.0MHz by 11, so we would provide '10' to the CLKDIV (10 + 1 = 11), as follows ('00001010' being the binary equivalent of 10):
| - | EDGE | START | - | PDN | - | CLKS | BURST | CLKDIV | SEL |
| AD0_CR | **** | * | *** | ** | * | * | *** | * | 00001010 | ******** |
CLKS
These three bits are used to indicate the range of values used when converting analog data. You can set the 'precision' of the results from 3-bits (values from 0-7) up to 10-bits (values from 0-1023), depending on your requirements. You simply need to provide one of the following values to indicate how many 'bits' to use for the conversion:
000 = 10-bits (0..1023)
001 = 9-bits (0..511)
010 = 8-bits (0..255)
011 = 7-bits (0..127)
100 = 6-bits (0..63)
101 = 5-bits (0..31)
110 = 4-bits (0..15)
111 = 3-bits (0..7)
For example, to get the maximum 10-bit 'range', we would provide the following value to the CLKS field:
| - | EDGE | START | - | PDN | - | CLKS | BURST | CLKDIV | SEL |
| AD0_CR | **** | * | *** | ** | * | * | 000 | * | ******** | ******** |
PDN
PDN (short for 'Power-Down') indicates whether the ADC should be in 'Power-Down' mode (0), or actively converting data (1). Since we want to convert data right away, we could tell the ADC to go out of power-down mode (it's default value) by setting this bit to 1 as follows:
| - | EDGE | START | - | PDN | - | CLKS | BURST | CLKDIV | SEL |
| AD0_CR | **** | * | *** | ** | 1 | * | *** | * | ******** | ******** |
How does all this relate to our sample code? If we take another look at the configuration values we are passing to AD0_CR we can see that we are setting all four of the fields described above as follows:
// Initialise ADC converter
// (10-bit accuracy, ADC active, 4.0MHz clock, channel 3 selected)
AD0_CR = AD_CR_CLKS10 | AD_CR_PDN | ((3 - 1) << AD_CR_CLKDIVSHIFT) | AD_CR_SEL3;
This code is using a number of 'aliases' that are defined in lpc214x.h (take a look in the header file to see the corresponding values). We could just as easily have passed the hexadecimal equivalent of these configuration settings to AD0_CR:
AD0_CR = 0x00200208; // = 0000 0 000 00 1 0 000 0 00000010 00001000
The two lines of code would be identical when compiled. The difference is that the first line (the one used in our example) is a little bit easier to understand and to modify later if we need to do so, which is good. It's beyond the scope of this (already lengthy!) tutorial to start describing aliases and bit operators, etc., but a quick summary of the aliases we are using here might be helpful:
- AD_CR_CLKS10: Sets the bit-accuracy of the converted values to the maximum 10-bit (values between 0 and 1023)
- AD_CR_PDN: Exits 'powered-down' mode, activating the ADC
- ((3 - 1) << AD_CR_CLKDIVSHIFT): Set the CLK divider (12MHz/3) and shifts it to the left to it's appropriate position
- AD_CR_SEL3: Selects AD0 channel 3
Step 2: Reading the Conversion Results
Each ADC channel has it's own dedicated data register (ADDR0..7) that we can use to 'read' the results of the analog to digital conversion, as well to check whether the current conversion is complete or not. The 32-bit value has the following format:
| Function | DONE | OVERRUN | - | RESULT | - |
| ADDR Bit(s) | 31 | 30 | 29..16 | 15..6 | 5..0 |
DONE (Bit 31)
This bit is set to 1 when an A/D conversion is complete. For accurate results, you need to wait until this value is 1 before reading the RESULT bits. (Please note that this value is cleared when you read this register.)
OVERRUN (Bit 30)
While not relevant to the examples used in this tutorial, this value with be 1 if the results of one or more conversions were lost when converting in BURST mode. See the User's Manual for further details. (As with DONE, this bit will be cleared when you read this register.)
RESULTS (Bits 15..6)
If DONE is 1 (meaning the conversion is complete), these 10 bits will contain a binary number representing the results of our analog to digital conversion. It works by measuring the voltage on the analog input pin divided by the voltage on the Vref pin. Zero means that the voltage on the analog input pin was less than, equal to or close to GND (Vssa), and 0x3FF (or 0011 1111 1111) indicates that the voltage on the analog input pin was close to, equal to or greater than the the voltage on the Vref pin. Anything value between these two extremes will be returned as a 10-bit number (between 0 and 1023).
Before we can read the results from the A/D Data Register, we first need to perform the following steps (to start the actual conversion process by re-configuring ADCR):
This can be accomplished by modifying the A/D Control Register (see ADCR above) as follows:
AD0_CR &= ~(AD_CR_START_MASK | AD_CR_SELMASK);
What you are doing here is setting the START bits in ADCR to 000 (stopping any conversions) and disabling any channels that may have been previously selected. This essentially 'resets' our ADC to a blank, inactive state.
Since we have previously deselected all channels, we need to re-enable the specific channel that we wish to use in this conversion. In this particular case, we can enable channel 3 with the following line of code:
AD0_CR |= (AD_CR_START_NONE | AD_CR_SEL3);
The ADC can be configured to start in two ways: Manually (as we will do here), or when some sort of internal hardware event occurs such as an external interrupt (for example when a button connected to an EINT pin is pressed), or some other form of interrupt. (For details on starting the conversion when an interrupt occurs, please see chapter 17 of the LPC2148 User's Manual. Interrupts will also be covered in a later tutorial.)
In our case, the easiest solution is to simply tell the ADC to start converting manually, which is as simple as setting the appropriate START bits (26..24) in the ADC Control Register (see ADCR above). To start converting now, we need to send 001 to the three-bit START block in ADCR, which can be done with the following line of code (AD_CR_START_NOW is defined as '001' in LPC214x.h):
AD0_CR |= AD_CR_START_NOW;
Now that the analog to digital converter is properly configured and has been started (on channel 3 in this case), we can wait for the results on the appropriate AD Data Register ... ADDR3 in this particular case. As we mentionned above, we know when a conversion is complete because bit 31 (DONE) of ADDR0..7 will be set to 1. As such, we simply need to wait until we encounter a 1 value on this bit, which can be done with the following line of code:
while (!(AD0_DR3 & AD_DR_DONE))
;
This will cause the code to endlessly loop until the conversion is complete, and then move on to the next line once the conversion is finished.
The last step (now that we know the conversion is complete) is simply to read the 10-bit value stored in the appropriate ADDR register, and do whatever we need to do with it. This last step can be accomplished by reading bits 6 through 15 (15..6) of ADDR3 (since we are using Channel 3 in this example), and then 'shifting' the results to the right 6 places to give a normal 10-bit value (AD_DR_RESULTSHIFT is defined as 6 in lpc214x.h since we need to shift the results 6 positions to the right):
unsigned int results = ((AD0_DR3 & AD_DR_RESULTMASK) >> AD_DR_RESULTSHIFT);
return results;
That's all that's involved in manually starting an A/D conversion and reading the results. If you wish to continually read the results of the ADC, you can simply place the appropriate code in a method/function and continually call it, as we are doing in the example presented at the very beginning of this tutorial.
To make sure that this all make sense to you, try going back to the full example we gave you at the beginning of this article. Reading over the code line by line, can you understand what we're doing and why? If not, simply look at the line(s) you don't understand, and try to find the part of this article that explains that particular step, and why it's necessary or why are are using the values that we assigned. Feel free to consult the complete Crossworks Project that contains this example. It can be downloaded at the bottom of this page and also includes the lpc214x.h file used in all of our examples.