LPC2148 ADC Tutorial: Reading a Potentiometer and LM35 on AD0.1
Most real projects measure something analog: a temperature, a light level, a battery voltage or the position of a knob. The LPC2148 contains two 10-bit successive approximation ADCs, AD0 and AD1, with up to 14 input channels between them. In this tutorial you will read a potentiometer on channel AD0.1, convert the result to millivolts, then reuse the same code to measure temperature with an LM35 sensor and print both values on a serial terminal.
How the LPC2148 ADC Works
A 10-bit ADC divides the range from 0 V to the reference voltage VREF into 1024 steps. On most LPC2148 boards VREF is tied to 3.3 V, so one step is about 3.22 mV. The converter needs a clock of 4.5 MHz or less, derived from PCLK through a programmable divider, and in the default mode it takes 11 ADC clocks to complete one 10-bit conversion.
Count = Vin x 1023 / VREF Vin(mV) = Count x 3300 / 1023 (VREF = 3.3 V) Step = 3300 mV / 1024 = 3.22 mV ADC clock = PCLK / (CLKDIV + 1) PCLK = 15 MHz, CLKDIV = 3 -> 15 / 4 = 3.75 MHz (below 4.5 MHz limit)
The ADC input pins share functions with GPIO. To use a pin as an analog input, you must select the AD function in the pin connect block, otherwise the converter sees nothing useful. The ADC block must also be powered: on the LPC2148 the PCONP bit for AD0 is cleared after reset, which is a very common reason for an ADC that always reads zero.
AD0 Registers
| Register / field | Bits | Meaning |
|---|---|---|
| PCONP.PCAD0 | 12 | Set to 1 to power AD0 |
| PINSEL1 for P0.28 | 25:24 = 01 | Selects AD0.1 on P0.28 |
| AD0CR.SEL | 7:0 | One bit per channel; bit 1 selects AD0.1 |
| AD0CR.CLKDIV | 15:8 | PCLK divider, ADC clock = PCLK / (CLKDIV + 1) |
| AD0CR.BURST | 16 | 0 = software-started conversions, 1 = continuous scanning |
| AD0CR.CLKS | 19:17 | 000 = 11 clocks, 10-bit result |
| AD0CR.PDN | 21 | 1 = ADC operational, 0 = power-down |
| AD0CR.START | 26:24 | 001 = start a conversion now |
| AD0GDR.RESULT | 15:6 | Most recent 10-bit result |
| AD0GDR.DONE | 31 | Set when a conversion completes; cleared when the register is read |
Circuit
| Component | Connection |
|---|---|
| 10 k potentiometer | Ends to 3.3 V and GND, wiper to P0.28 (AD0.1) |
| LM35 temperature sensor | VS to 5 V, GND to GND, VOUT to P0.29 (AD0.2) |
| VREF pin | 3.3 V (already connected on most boards) |
| UART0 | USB-serial adapter or onboard serial port at 9600 baud |
Program: Read Two Channels and Print Them
The program initialises UART0 (as described in the UART tutorial), configures AD0, and once every half second reads channel 1 and channel 2. It prints the raw count, the voltage in millivolts and the LM35 temperature. Integer arithmetic is used throughout because ARM7 has no floating point unit and software float adds code size.
#include <lpc214x.h>
#include <stdio.h>
/* ---------- UART0, 9600 baud with PCLK = 15 MHz ---------- */
static void uart0_init(void)
{
PCONP |= (1u << 3);
PINSEL0 = (PINSEL0 & ~0x0Fu) | 0x05u; /* TXD0, RXD0 */
U0LCR = 0x83;
U0DLL = 98; /* 15 MHz / (16 x 9600) */
U0DLM = 0;
U0LCR = 0x03;
}
int fputc(int ch, FILE *f) /* tick Use MicroLIB */
{
(void)f;
if (ch == '\n') {
while (!(U0LSR & 0x20));
U0THR = '\r';
}
while (!(U0LSR & 0x20));
U0THR = ch;
return ch;
}
/* ---------- ADC0 ---------- */
static void adc0_init(void)
{
PCONP |= (1u << 12); /* power AD0 */
/* P0.28 -> AD0.1 (bits 25:24 = 01), P0.29 -> AD0.2 (bits 27:26 = 01) */
PINSEL1 = (PINSEL1 & ~(0xFu << 24)) | (0x5u << 24);
AD0CR = (3u << 8) /* CLKDIV = 3 -> 3.75 MHz ADC clock */
| (1u << 21); /* PDN = 1, ADC operational */
}
/* Start one conversion on channel ch (0 to 7) and return 0 to 1023 */
static unsigned int adc0_read(unsigned int ch)
{
unsigned int gdr;
AD0CR = (AD0CR & ~(0xFFu | (7u << 24))) /* clear SEL and START */
| (1u << ch) /* select channel */
| (1u << 24); /* START = 001 */
do {
gdr = AD0GDR;
} while (!(gdr & (1u << 31))); /* wait for DONE */
AD0CR &= ~(7u << 24); /* stop */
return (gdr >> 6) & 0x3FF;
}
static void delay_ms(unsigned int ms)
{
volatile unsigned int i, j;
for (i = 0; i < ms; i++)
for (j = 0; j < 5000; j++);
}
int main(void)
{
unsigned int pot, lm, pot_mv, lm_mv;
uart0_init();
adc0_init();
printf("LPC2148 ADC demo\n");
while (1) {
pot = adc0_read(1);
lm = adc0_read(2);
pot_mv = pot * 3300u / 1023u;
lm_mv = lm * 3300u / 1023u;
/* LM35: 10 mV per degree C; print with one decimal */
printf("POT=%4u (%4u mV) LM35=%4u mV T=%u.%u C\n",
pot, pot_mv, lm_mv, lm_mv / 10u, lm_mv % 10u);
delay_ms(500);
}
}
How the Code Works
adc0_init powers the ADC, routes P0.28 and P0.29 to their analog functions with one write to PINSEL1, and sets the clock divider and PDN bit. No channel is selected yet. adc0_read clears the SEL and START fields, sets the bit for the requested channel and writes 001 to START, which begins a single conversion. It then polls the global data register AD0GDR until the DONE bit (bit 31) is set and extracts the 10-bit result from bits 15 to 6.
Reading AD0GDR into a local variable once per loop matters. DONE is cleared by the read, so testing the register and then reading it again could return a value with DONE already cleared. Using the same copy for both the test and the result avoids this.
Worked Example
Suppose the room is at 28 degrees Celsius. The LM35 outputs 280 mV. The ADC count is 280 x 1023 / 3300, which is about 86. Converting back gives 86 x 3300 / 1023 = 277 mV, so the program prints 27.7 C. The difference comes from the 3.22 mV step size: each count represents about a third of a degree. For better temperature resolution you can amplify the LM35 output with an op-amp, or average several readings to reduce noise.
Troubleshooting
- Always reads 0: PCONP bit 12 not set, PDN bit cleared, or the pin is still in GPIO mode.
- Reading changes when you touch the wire: the input is floating; check the potentiometer wiper connection.
- Value jumps by 5 to 10 counts: add a 100 nF capacitor from the ADC pin to ground close to the chip and average 8 or 16 samples.
- Full-scale reading is lower than 1023: VREF on your board may not be 3.3 V; measure it and use the real value in the conversion.
- Program hangs inside adc0_read: START bits were never written, or the ADC clock divider is zero and the clock is out of range.
Summary
To use the LPC2148 ADC, power AD0 in PCONP, select the analog function in PINSEL1, set CLKDIV so the ADC clock stays at or below 4.5 MHz, and set PDN. Each conversion is started by writing the channel bit and START = 001 to AD0CR, then waiting for DONE in AD0GDR. The 10-bit result maps 0 to 3.3 V into 0 to 1023, which you convert to millivolts with integer maths.