Analog to digital Conversion is an important feature of a microcontroller. Though previously, microcontrollers like 8051 did not have an internal ADC module, newer microcontrollers do possess this feature.
Signals like voltage, current as well as the output produced by different sensors are analog in nature. Sensors and transducers measure physical quantities and produce output in terms of varying voltages. These signals must be converted into binary data for the microcontroller to take appropriate action. This is where an ADC comes into the picture.
ADC in PIC16F887 microcontroller
PIC16F887 uses a 10-bit ADC module to convert an analog signal into binary form. So the maximum count that can be obtained is 2^10 which gives 1024. It consists of 14 Analog channels(AN0-AN13). The ADC voltage reference is software selectable which is generated internally or can be provided externally through Vref+ and Vref- pins.The ADC interrupt can be enabled or disabled. When enabled, interrupt occurs on completion of 1 full conversion.
There are a few registers associated with ADC in PIC16F887. Let us understand the role of each register.
Registers associated with ADC
- TRIS
- ANSEL-ANSELH
- ADCON0
- ADCON1
- ADRESH-ADRESL
TRIS register (TRISA, TRISB, TRISE)
TRIS registers are data direction registers which are used to configure a port pin either as an input or as an output. It must be set to 0 for using it as an output and must be set to 1 for using it as an input. For a pin to be used as analog, it must be set as an input. In pic16f887, there are in all 14 Analog channel with 5 on PORTA, 3 on PORTE and remaining channels on PORTB. So if a pin with an analog function on PORTA is to be used, then the respective bit must be set as an input using TRISA register.
ANSEL-ANSELH registers
ANSEL
ANS7 | ANS6 | ANS5 | ANS4 | ANS3 | ANS2 | ANS1 | ANS0 |
ANSELH
– | – | ANS13 | ANS12 | ANS11 | ANS10 | ANS9 | ANS8 |
These are a set of 2 registers. In the above two registers, you’ll find that the number of bits in these 2 registers is equal to the number of ADC channels, which is 14. If you look at the datasheet, the analog channels are labeled as AN0, AN1, AN2… AN13.
Bit ANS0 is the input mode bit for AN0. Similarly, bit ANS1 is the input mode bit for AN1, ANS2 for AN2, and it goes on till AN13.
ANSEL and ANSELH are used to set the input mode of a pin. While using an ADC channel, the associated ANSEL bit must be set to 1, and the rest must be cleared.
1- Analog input. Pin is assigned as analog input.
0- Digital I/O
ADCON Register: A/D control register 0
ADCS1 | ADCS0 | CHS3 | CHS2 | CHS1 | CHS0 | GO/D͞O͞N͞E | ADON |
ADCS1: ADCS0 – A/D Conversion Clock source
These bits are used to select the clock source for A/D conversion.
0:0 = Fosc/2
0:1 = Fosc/8
1:0 = Fosc/32
1:1 = Frc (Dedicated internal oscillator)
CHS3: CHS0 – ADC channel selection bit
ADC channel selection can be done using channel selection bits.
0000 = AN0
0001 = AN1
0010 = AN2
0011 = AN3
..
..
..
1100 = AN12
1101 = AN13
1110 = Cvref
1111 = Fixed Reference( 0.6V Fixed voltage reference)
GO/D͞O͞N͞E – A/D conversion bit
This bit is set by the programmer to begin Analog to digital conversion. This bit is automatically cleared when the conversion is completed. ADIF flag is set after one complete conversion. ADC interrupt will occur if enabled.
ADON – ADC enable bit
ADON must be set to enable ADC. ADC is disabled when ADON is cleared.
ADCON1 Register: A/D control register 1
ADFM | – | VCFG1 | VCFG0 | – | – | – | – |
ADFM – A/D result format select bit
1 = Right justified (See ADRESH-ADRESL para)
0 = Left justified
VCFG1: VCFG0 – These bits are used to set the reference voltage for ADC. The reference voltage can be provided externally through Vref+ and Vref- pin or can be set to VDD and VSS.
VCFG1 – Voltage reference bit
1 = Vref- pin
0 = Vss
VCFG0 – Voltage reference bit
1 = Vref+ pin
0 = Vdd
ADRESH – ADRESL registers
ADRESH and ADRESL are 2, 8-bit registers which are used to store the ADC conversion result. Since pic16f887 uses a 10 bit ADC, the value obtained after conversion would be a binary number containing 10-bits. This result is automatically stored in the ADRESH-ADRESL register pair. The way this result is stored depends on the bit ADFM in ADCON1 register.
When ADFM = 0, the higher 8 bits of the 10-bit result is stored in ADRESH and the remaining 2 bits are stored in ADRESL register.
When ADFM = 1, the higher 2 bits of the 10-bit result is stored in ADRESH register and the remaining 8 bits are stored in ADRESL register.
Steps involved in ADC programming
- Set the pin mode as input using the data direction registers (TRIS register).
- Select the input mode as analog by setting the appropriate ANSEL bit.
- Choose the ADC channel and select the ADC clock source using the ADCON0 register.
- Select result formatting and ADC reference voltage using the ADCON1 register.
- Wait for acquisition time.
Acquisition time is the time required for the holding capacitor in the sample and hold circuit to charge after each ADC conversion. - Set Go/D͞O͞N͞E bit to begin conversion.
- ADIF flag in PIR1 register is set and Go/D͞O͞N͞E bit is automatically cleared when the conversion is completed.
- Read ADC counts from ADRESH-ADRESL register
ADC program for PIC16f887 and the required circuit connections.
MPLAB X program for displaying ADC counts on LCD using PIC16F887 Microcontroller.
In the following program, no LCD libraries have been used. Everything is simply coded from top to bottom. If you’re not sure about how LCD is programmed, then I would recommend you to look at my previous tutorial Displaying message on character LCD.
#pragma config FOSC = INTRC_NOCLKOUT #pragma config WDTE = OFF #pragma config PWRTE = OFF #pragma config MCLRE = ON #pragma config CP = OFF #pragma config CPD = OFF #pragma config BOREN = ON #pragma config IESO = OFF #pragma config FCMEN = OFF #pragma config LVP = OFF #pragma config BOR4V = BOR40V #pragma config WRT = OFF #include <xc.h> #include<pic.h> #include<pic16f887.h> #define _XTAL_FREQ 4000000 //LCD FUNCTIONS void lcd_cmd(unsigned char c); void lcd_data(unsigned char d); void lcd_init(void); void enable(void); char str[] = "ADC counts"; void lcd_print(char *ptr); // ADC FUNCTIONS AND VARIABLE DECLERATIONS int ADC_read(void); void init_ADC(void); unsigned int get_count(void); unsigned int value; int arr_dec[17] ={0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; // DECIMAL ARRAY int arr_hex[3] = {}; // ARRAY TO HOLD INDIVIDUAL HEX VALUE char decimal_data[3] = {}; // ARRAY TO STORE INDIVIDUAL DECIMAL VALUE int power(int, int); // FUNCTION TO CALCULATE POWER ("RAISE TO") int main() { unsigned int count; int temp; TRISD = 0x00; TRISC = 0x00; TRISA = 0x01; PORTD = 0x00; PORTC = 0x00; PORTA = 0x00; ANSEL = 0b00000001; lcd_init(); lcd_cmd(0x80); lcd_print(str); // PRINT "ADC COUNTS" init_ADC(); while(1) { lcd_cmd(0xc0); value = ADC_read(); // READ ADC FROM CHANNEL 1; VALUE WILL BE // STORED IN HEXDECIMAL FORM for(int i=0; i<3; i++) { arr_hex[i] = (value%16); //SEPERATE INDIVIDUAL HEX BITS temp = arr_hex[i]; value /= 16; decimal_data[i] = arr_dec[temp]; // COMPARE HEX BITS WITH DECIMAL BIT ARRAY // TO STORE DECIMAL VALUES IN DECIMAL_DATA } count = get_count(); //FUNCTION TO CONVERT HEXADECIMAL VALUE TO DECIMAL lcd_data((count/1000)+48); // SEPERATE BITS AND PRINT count = count%1000; lcd_data((count/100)+48); count = count%100; lcd_data((count/10)+48); count = count%10; lcd_data((count)+48); __delay_ms(300); } } void init_ADC(void) { ADCON0 = 0x41; // Selecting ADC clock as Fosc/8; Channel 0; Enabling ADC module ADCON1 = 0x80; // Setting voltage reference to VSS and VDD: Right justified } int ADC_read(void) { ADCON0 = 0x41; GO_nDONE = 1; while(!GO_nDONE); return ((ADRESH<<8)+ADRESL); } void lcd_init(void) { // LCD initial initialization __delay_ms(15); lcd_cmd(0x03); __delay_ms(5); lcd_cmd(0x03); __delay_ms(5); lcd_cmd(0x03); __delay_ms(5); lcd_cmd(0x02); __delay_ms(5); // LCD command setting initialization lcd_cmd(0x28); // 4 bit interface length. 2 rows enabled. __delay_ms(5); lcd_cmd(0x10); // Move cursor or shift display __delay_ms(5); lcd_cmd(0x0c); // Enable display. Cursor off. __delay_ms(5); lcd_cmd(0x06); // Increment cursor position after each byte __delay_ms(5); lcd_cmd(0x01); // clear display __delay_ms(5); } void lcd_cmd(unsigned char c) { RD7 = 0; RD6 = 0; PORTC = (PORTC&0x0f)|(0xf0&c); enable(); PORTC = ((PORTC&0x0f)|(0x0f&c)<<4); enable(); } void lcd_data(unsigned char d) { RD7 = 1; RD6 = 0; PORTC = (PORTC&0x0f)|(0xf0&d); enable(); PORTC = ((PORTC&0x0f)|(0x0f&d)<<4); enable(); } void enable(void) { RD5 = 1; __delay_ms(5); RD5 = 0; } void lcd_print(char *ptr) { while(*ptr != '\0') { lcd_data(*ptr); ptr++; } } unsigned int get_count(void) // FUNCTION TO CONVERT HEX NUMBER IN DECIMAL { int i; unsigned int count=0; for(i=2; i>=0; i--) { count += (decimal_data[i]*power(16,i)); // FORMULA FOR CONVERSION } return count; } int power(int x, int y) // FUNCTION TO CALCULATE POWER { unsigned int result = 1; while(y!=0) { result = result*x; y--; } return result; }
3 Comments
Vinay S · August 27, 2019 at 11:08 am
Hi, thanks for the detailed article. I have a small question though.. How do i connect analog input directly to pin AN1? Say, if i have a transformer which gives secondary output as 3V, how do it connect it to pin RA0 here?
Also, if you could please help with the formula to calculate the voltage and display it in LCD, it would be nice. Thanks for your effort so far.
Moiz · September 7, 2019 at 7:09 pm
Glad you liked it.
Considering your output is properly rectified, connect a 4.9V Zener diode and 0.1uF ceramic cap in parallel with your ADC pin.
As for the formula, since the ADC is 12-bit, maximum count i.e 1023 corresponds to 5V.
So if you intend to display the voltage, you can use x = (5*ADC_Count)/1023. Where ADC_count is the obtained ADC value and x is the voltage you want to display.
sudhir · February 28, 2020 at 12:18 pm
This article is really helpful. Thanks alot.