Programming ATmega328P ADC in C
Here it is explained how the ADC(Analog to Digital Converter) of ATmega328P microcontroller works and how to program the ADC in C programming language. To illustrate how it works and how to write ADC program, we will read temperature data from a LM35 temperature sensor at ADC 0 pin. This read temperature is then displayed on LCD.
The following is the schematic diagram of interfacing ATmega328P ADC0 channel with temperature sensor and connecting LCD on ATmega328 port D.
The ATmega328P microcontroller has 6 channels for ADC[5:0] as shown below.
Here in the above example circuit diagram ADC0 channel(Port C pin 0) is used but we can use any other channel as we will see in the C program code.
ATmega328 ADC block diagram
The following is the block diagram of ATmega328P ADC hardware which shows how the successive ADC hardware is connected to different parts of the microcontroller such as clock source, registers, logic controller etc.
The ADC is a 10-bit successive approximation ADC so when analog signal
is sampled and converted to digital values(often called quantization
level) the analog signal is represented as one of the value in the
range 0 to 1023(\(2^{10}-1\)).
ATmega328P ADC Registers
To configure ADC we must configure the ADC associated registers which are as follows.
1. ADMUX – ADC Multiplexer Selection Register
2. ADCSRA – ADC Control and Status Register A3. ADCSRB – ADC Control and Status Register B
4. ADCL and ADCH – The ADC Data Register
4. DIDR0 – Digital Input Disable Register 0
ATmega328P ADC C Program
In this example we will use the ADC0 channel to read in temperature sensor data and display it on 16x2LCD. The following is the C program code to do this.
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
//LCD pin definition
#define E (1<<PD3)
#define RS (1<<PD2)
#define D4 (1<<PD4)
#define D5 (1<<PD5)
#define D6 (1<<PD6)
#define D7 (1<<PD7)
//ADC function prototypes
void adcinit();
int adcread(unsigned char channel);
//LCD function prototypes
void lcdinit();
void latch(void);
void lcdcmd(uint8_t cmd);
void lcdchar(uint8_t data);
void lcdprint(char *str);
void lcdsetcursor(uint8_t x, uint8_t y);
int main(){
lcdinit();
adcinit();
char temp[10];
float celsius;
lcdsetcursor(1,1);
lcdprint("Temperature:");
while (1){
celsius = (adcread(0)*4.88);
celsius = (celsius/10.00);
dtostrf(celsius, 6, 2, temp);
lcdsetcursor(1,2);
lcdprint(temp);
lcdchar(0xDF);
lcdprint("C");
}
return 0;
}
void adcinit(){
// set REFS1 = 0 |REFS0 = 1 (Vref as AVCC pin) | ADLAR = 0(right adjusted) | MUX4 to MUX0 is 0000 for ADC0
ADMUX = 0b01000000;
//enable ADC module, set prescalar of 128 which gives CLK/128
ADCSRA |= (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);
}
int adcread(unsigned char channel){
/* set input channel to read */
ADMUX = 0x40 | (channel & 0x07); // 0100 0000 | (channel & 0000 0100)
/* Start ADC conversion */
ADCSRA |= (1<<ADSC);
/* Wait until end of conversion by polling ADC interrupt flag */
while (!(ADCSRA & (1<<ADIF)));
/* Clear interrupt flag */
ADCSRA |= (1<<ADIF);
_delay_ms(1); /* Wait a little bit */
/* Return ADC word */
return ADCW;
}
void lcdinit(){
//initialize LCD
DDRD |= E | RS | D4 | D5 | D6 | D7;
//Send Pulse to Latch the data
latch();
_delay_ms(2); //delay for stable power
// Command to set up the LCD
lcdcmd(0x33);
_delay_us(100);
lcdcmd(0x32);
_delay_us(100);
lcdcmd(0x28); // 2 lines 5x7 matrix dot
_delay_us(100);
//lcdcmd(0x0E); // display ON, Cursor ON
lcdcmd(0x0C); // display ON, Cursor ON
_delay_us(100);
lcdcmd(0x01); //clear LCD
_delay_ms(20); //wait
lcdcmd(0x06); //shift cursor to right
_delay_ms(10);
}
void latch(void){
PORTD |= E; //send high
_delay_us(500); //wait
PORTD &= ~E; //send low
_delay_us(500); //wait
}
void lcdcmd(uint8_t cmd){
PORTD = (PORTD & 0x0F) | (cmd & 0xF0); // send high nibble
PORTD &= ~RS; //send 0 to select command register
latch(); //latch the data
PORTD = (PORTD & 0x0F) | (cmd<<4); //send low nibble
latch(); //latch the data
}
void lcdchar(uint8_t data){
PORTD = (PORTD & 0x0F) | (data & 0xF0); // send high nibble
PORTD |= RS; //send 1 to select data register
latch();
PORTD = (PORTD & 0x0F) | (data<<4); // send high nibble
latch();
}
void lcdprint(char *str){
uint8_t k=0;
while(str[k] != 0){
lcdchar(str[k]);
k++;
}
}
void lcdsetcursor(uint8_t x, uint8_t y){
uint8_t firstcharadr[] = {0x80, 0xC0, 0x94, 0xD4};
lcdcmd(firstcharadr[y-1] + x-1);
_delay_us(1000);
}
In the above code, we have mainly two parts of the program. One part has to do with ADC and the other has to do with LCD. The LCD part of the program is not explained here.
The ADC part consist of mainly two functions called the adcinit() and adcread().
The adcinit() function is used to initialize the ADC hardware of the ATmega328P which is below.
void adcinit(){
// set REFS1=0,REFS0=1 (Vref as AVCC pin), ADLAR = 0(right adjusted),MUX4 to MUX0 is 0000 for ADC0
ADMUX = 0b01000000;
//enable ADC module, set prescalar of 128 which gives CLK/128
ADCSRA |= (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);
}
In the above code, the ADMUX register is used to configure the voltage reference by setting the REFS1 to 0 and setting the REFS0 bit to 1. The following is table of the voltage reference source select bits.
The ADLAR(ADC Left Adjust Result) bit is set to 0 which means that the result of the ADC conversion is stored as right adjusted in the ADC data registers(ADCH and ADCL) as shown below.
The MUX bits selects which ADC channel to use. Here they are all set to 0 which means the ADC channel 0 is select in the initialization. The bit combination of MUX[3:0] is shown below.
Similarly in the adcinit() function, the ADCSRA(ADC Control and Status Register A) is configured to enable the ADC by setting the ADEN bit to 1. The prescalar of 128 is set by setting the ADPS2, ADPS1 and ADPS0 bits all to 1. The ADCSRA register bits was provided above. The pre-scalar bits and their setting is shown below.
The bits sets the ADC clock as illustrated below.
So if the clock frequency is 16MHz then using the pre-scalar of 128 which is the highest value the ADC clock frequency is 125KHz.
\[F_{ADC} = \frac{F_{CPU}}{128} = \frac{16MHz}{128} = 125KHz\]
The adcread() function is used to read the analog signal value at the specified ADC channel and is given below.
int adcread(unsigned char channel){
/* set input channel to read */
ADMUX = 0x40 | (channel & 0x07); // 0100 0000 | (channel & 0000 0100)
/* Start ADC conversion */
ADCSRA |= (1<<ADSC);
/* Wait until end of conversion by polling ADC interrupt flag */
while (!(ADCSRA & (1<<ADIF)));
/* Clear interrupt flag */
ADCSRA |= (1<<ADIF);
_delay_ms(1); /* Wait a little bit */
/* Return ADC word */
return ADCW;
}
The function adcread() accepts as input the channel number 0 to 6 and this is used to activate the corresponding ADC channel. As was explained earlier, the ADMUX register has MUX[0:3] which are used to select specific ADC channel. After the channel is selected the ADC conversion is started by setting the ADSC bits located in the ADCSRA to 1. Then we wait until the ADIF(ADC Interrupt Flag) flag located in the ADCSRA register to set to 1 by polling the ADIF bit. After that we clear the ADIF bit and return the value stored in the ADC data registers(ADCH and ADCL). The complete 10 bits is returned using the ADCW value. This can be used because the ADCW is defined in the AVR compiler as combination of content of ADCH and ADCL.
In the main() function we use these two function adcinit() and adcread() to initialize the ADC hardware and to read the ADC data register and display on the LCD.
int main(){
lcdinit();
adcinit();
char temp[10];
float celsius;
lcdsetcursor(1,1);
lcdprint("Temperature:");
while (1){
celsius = (adcread(0)*4.88);
celsius = (celsius/10.00);
dtostrf(celsius, 6, 2, temp);
lcdsetcursor(1,2);
lcdprint(temp);
lcdchar(0xDF);
lcdprint("C");
}
return 0;
}
The lcdinit() and adcinit() are used to initialize the LCD and the ADC hardware. The temp[] and celcius are array and float variable to store data. The using lcd function we print string temperature on the LCD.
In the while() loop we read the temperature data using the adcread() on specific channel and then convert it to float value. This float temperature value is then converted from float to string using the dtostrf() function. For using dtostrf() function we need the stdlib.h library. The string value is stored in the previously declared temp array. Then we print the string value stored in the temp array to the LCD. The hex value 0xDF is used for displaying the degree symbol on the LCD.
The next step is to compile and upload the code into the microcontroller. If you don't know which ATmega328p programming software to use then see the atmega328p isp programming tutorial. And if you have difficulty with checking this ADC program is working or not, you can try the atmega328p led blink program.
This tutorial described how to program Atmega328P microcontroller ADC in C using LM35 temperature data as an example. For other similar tutorials see the following.
- Reading and displaying temperature using ATmega32
- LM35 Temperature Sensor with ATmega32 and LCD
- Interrupt based Temperature Reading and Display with ATmega32
Post a Comment