In this Atmega328P ADC tutorial it is shown how to write C program to acquire analog signal using interrupt service routine and display on LCD. The analog signal can be acquired using either polling method(software method) or interrupt method(hardware method). The ADC polling method of acquiring analog signal with ATmega328P was illustrated in the previous tutorial Programming ATmega328P ADC in C.
Here we will learn how to use interrupt feature of the ADC(Analog to Digital Converter). We will read the temperature sensor data from LM35 temperature sensor and use the interrupt service routine to acquire the data and then display on LCD.
The following is the hardware diagram used in this tutorial.
In the above diagram, the LM35 data pin is connected to ADC channel 2 which is Port C pin 2. The 16x2 LCD is connected to Port D pins.
The ATmega328P has 6 analog input channel which is shown below.
The steps to programming ADC of Atmega328P are as follows.
1. main() function
- call adcinit()
- setup the global interrupt
- start ADC conversion in main()2. adcinit() - function to initialize ADC hardware
- setup the voltage reference for the ADC
- select whether to use left or right adjusted ADC value
- select the ADC channel to use
- enable the ADC
- enable ADC interrupt
- setup the ADC pre-scalar
3. ISR() (Interrupt Service Routine)
- call adcread()
read the ADC value from ADCW(i.e ADCH and ADCL)
- convert the ADC value from float to string for display
- send value to the LCD
- start ADC conversion
Once the program is started, the main() function is executed. Below is the main() function.
int main(){
lcdinit();
adcinit();
sei(); //enable global interrupt
lcdsetcursor(1,1);
lcdprint("Temperature:");
ADCSRA |= (1<<ADSC); //start ADC conversion
while (1);
return 0;
}
In the main() function the adcinit() function is called.
The following is the adcinit() function. In this function we setup the voltage reference for the ADC, we set the left or right adjustment for the ADC data word, select the initial ADC channel, enable the ADC hardware, enable the ADC interrupt and set the pre-scalar.
void adcinit(){
//set the voltage reference using REFS1 and REFS0 bits and select the ADC channel using the MUX bits
ADMUX = 0b01000000; //REFS1=0|REFS0=1(Vref as AVCC pin),ADLAR=0(right adjusted)|MUX4 to MUX0 is 0000 for ADC0
//enable ADC module, set prescalar of 128 which gives CLK/128
ADCSRA |= (1<<ADEN) | (1<<ADIE) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);
}
The steps to setup the voltage reference for the ADC, to select whether to use left or right adjusted ADC value and to select which ADC channel to use is done by configuring the ADMUX register. The ADMUX register is shown below.
The combination of REFS1 and REFS0 bits configures which voltage reference to use as dictated by the following table.
Here we have used REFS1=0 and REFS0=0, which means that we will use the AVCC as the voltage reference for the ADC hardware.
The ADLAR bit is set to 0 which means we are using right adjusted value in the ADC data registers as shown below.
Similarly the MUX[0:3] bits selects which analog channel we will use. Here we have initialized the ADC to use the channel 0.
Then the other initialization of ADC are to enable the ADC, to enable ADC interrupt and to setup the ADC pre-scalar. These are done by configuring the ADCSRA register bits. The ADC is enabled by setting the ADEN to 1, the interrupt is enabled by setting the ADIE bit to 1 and the ADC pre-scalar is set to all 1 using the the ADPS2, ADPS1 and ADPS0 bits. By setting ADC pre-scalar bits to 1 we will have pre-scalar of 128 as shown by the table below.
The ADC clock or the sampling frequency is set using these bits which divides the clock frequency by the pre-scalar value. This is illustrated by the diagram below.
The clock frequency used here 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\]
Once the ADC initialization is complete in the main() function, the global interrupt is enabled using the sei() function. Then we display a "Temperature:" text on LCD. After that we start the ADC conversion by setting the ADSC bit in ADCSRA register. Then we have the infinite while loop.
The ADC starts and temperature data from the LM35 temperature sensor is read
and stored in the ADC data register. The program waits for interrupt
triggered by the ADC hardware. Once there is ADC data available to read, the interrupt is triggered and the ISR() function gets invoked.
The following is the ISR() function
ISR(ADC_vect){
celsius = (adcread(2)*4.9);
celsius = (celsius/10.00);
dtostrf(celsius, 6, 2, temp);
lcdsetcursor(1,2);
lcdprint(temp);
lcdchar(0xDF);
lcdprint("C");
ADCSRA |= (1<<ADSC); //start ADC conversion
}
In the ISR() function, adcread() function is called with channel number as parameter which is 2 in this example.
The following is the adcread() function
int adcread(char channel){
// set input channel to read
ADMUX = 0x40 | (channel & 0x07); // 0100 0000 | (channel & 0000 0100)
_delay_ms(1); //Wait for 1ms
return ADCW; // Return ADC word
}
The adcread() accepts channel number as parameter and sets up that channel number as the ADC channel to use by setting up the MUX[0:3] bits in the ADMUX register. Since this function adcread() gets executed after the data is available in the ADC data registers(ADCH and ADCL), we can can read and return the ADC data word by returning ADCW.
Once this ADC data is returned it gets multiplied with 4.9 representing the high level voltage at the ADC hardware. The resulting number is stored in global variable celsius which gets divided by 10 for formatting purpose. The float data celsius is converted to string and stored in global array variable temp using the dtostrf(). To use the dtostrf() function we have to use the stdlib.h library. The string converted temperature data is then display on the LCD. The 0xDF character is used to represent degree symbol. At the end we start the ADC conversion again.
Atmega328p ADC interrupt program
Following is the complete code for displaying temperature data on LCD using the ADC interrupt of ATmega328p.
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdlib.h>
#define E (1<<PD3)
#define RS (1<<PD2)
//ADC function prototypes
void adcinit();
int adcread(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);
char temp[10];
float celsius;
int main(){
lcdinit();
adcinit();
sei(); //enable global interrupt
lcdsetcursor(1,1);
lcdprint("Temperature:");
ADCSRA |= (1<<ADSC); //start ADC conversion
while (1);
return 0;
}
ISR(ADC_vect){
celsius = (adcread(2)*4.9);
celsius = (celsius/10.00);
dtostrf(celsius, 6, 2, temp);
lcdsetcursor(1,2);
lcdprint(temp);
lcdchar(0xDF);
lcdprint("C");
ADCSRA |= (1<<ADSC); //start ADC conversion
}
void adcinit(){
//set the voltage reference using REFS1 and REFS0 bits and select the ADC channel using the MUX bits
ADMUX = 0b01000000; //REFS1=0,REFS0=1(Vref as AVCC pin),ADLAR = 0(right adjusted),MUX4 to MUX0 is 0000 for ADC0
//enable ADC module, set prescalar of 128 which gives CLK/128
ADCSRA |= (1<<ADEN) | (1<<ADIE) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);
}
int adcread(char channel){
// set input channel to read
ADMUX = 0x40 | (channel & 0x07); // 0100 0000 | (channel & 0000 0100)
_delay_ms(1); //Wait for 1ms
return ADCW; // Return ADC word
}
void lcdinit(){
//initialize PORTs for LCD
DDRD |= (1<<PD2) | (1<<PD3) | (1<<PD4) | (1<<PD5) | (1<<PD6) | (1<<PD7);
//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(1);
}
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);
}
You need to compile and upload the above code into the microcontroller using atmega328p programming software. If you are having difficulty in deploying and checking out whether it is working or not you can try first simple atmega328p led blink program.
In this way we can program the interrupt of ADC hardware of ATmega328p micrcontroller.This simple example only illustrates how to display the temperature on LCD. A better project would be use the ATmega328P UART to send the temperature data to the PC for recording purpose.