The Microchip PIC18 Microcontroller family is the Microchip highest performance 8-bit class microcontroller. Powered by advanced RISC CPU, this PIC18 microcontroller family could deliver up to 16 MIPS computing power compared to the other Microchip 8-bit microcontroller family such as PIC10, PIC12 and PIC16 which only could deliver up to 5 MIPS. The PIC18 microcontroller architecture is optimized to be programmed in C language and supporting many advance industrial standard interface peripherals such as I2C, SPI, UART, USB, CAN, Ethernet, LCD and Touch Screen; this make the PIC18 microcontroller family become a popular choice to the new 8-bit embedded system design.
The Microchip C18 compiler is designed to fully utilize the PIC18 microcontroller capabilities, together with Microchip C18 royalty free advanced library such as USB, TCP/IP stack and ZigBee wireless network protocol make the C18 compiler is the best choice to many embedded system designer and hobbyists.
On this tutorial we will learn of how to program the PIC18LF14K50 microcontroller ADC peripheral using the Microchip C18 compiler. The principal we learn here could be applied to the other Microchip PIC18 microcontroller family as well. On this tutorial we will use this following simplify ermicro PICJazz 20PIN board schematic.
After placing the PIC18LF14K50 to the 20 pin socket; using the female to female cable jumper that come with the PICJazz 20PIN board we could easily reconfigure the user trimport and user switch to RA4 and RA5 respectively.
On this tutorial we will use two different approaches to program the PIC18 microcontroller family analog to digital converter (ADC) peripheral. The first one is to use the standard C programming code to manipulate the PIC18LF14K50 ADC peripheral registers and the second one is to use the Microchip C18 ADC wrap-up library. Now take a look on our first code:
/* *************************************************************************** ** File Name : adc.c ** Version : 1.0 ** Description : Analog to Digital Converter ** Author : RWB ** Target : PICJazz 20PIN Board: PIC18LF14K50 ** Compiler : Microchip C18 v3.34 C Compiler ** IDE : Microchip MPLAB IDE v8.30 ** Programmer : Microchip PICKit2 Programmer ** Last Updated : 28 Oct 2009 ** ***************************************************************************/ #include #include
/* ** PIC18LF14K50 Configuration Bit: ** ** FOSC = IRC - Internal RC Oscillator ** CPUDIV = NOCLKDIV - No CPU System Clock divide ** PLLEN = OFF - PLL is under software control ** FCMEN = OFF - Fail-Safe Clock Monitor disabled ** BOREN = OFF - Brown-out Reset disabled in hardware and software ** WDTEN = OFF - WDT is controlled by SWDTEN bit of the WDTCON register ** MCLRE = ON - MCLR pin enabled, RE3 input pin disabled ** LVP = OFF - Single-Supply ICSP disabled */ #pragma config FOSC = IRC, CPUDIV = NOCLKDIV, PLLEN = OFF #pragma config FCMEN = OFF, BOREN = OFF #pragma config WDTEN = OFF, MCLRE = ON, LVP = OFF
// Delay in 1 ms (approximately) for 16 MHz Internal Clock void delay_ms(unsigned int ms) { do { Delay1KTCYx(4); } while(--ms); }
void main(void) { unsigned char chSign,chEye,chType,iCount; unsigned int iDelay; unsigned char led_patern[] = {0b00000000,0b00000001, 0b00000011,0b00000111, 0b00001111,0b00001111, 0b00001110,0b00001100, 0b00001000,0b00000000};
OSCCON=0x70; // Select 16 MHz internal clock
TRISC = 0x00; // Set All on PORTC as Output TRISA = 0x30; // Input for RA4 and RA5 ANSEL = 0b00001000; // Set PORT AN3 to analog input ANSELH = 0; // Set PORT AN8 to AN11 as Digital I/O
/* Init ADC */ ADCON0=0b00001101; // ADC port channel 3 (AN3), Enable ADC ADCON1=0b00000000; // Use Internal Voltage Reference (Vdd and Vss) ADCON2=0b10101011; // Right justify result, 12 TAD, Select the FRC for 16 MHz
chEye=0x01; // Initial Eye Variables with 0000 0001 chSign=0; iDelay=100; chType=0; iCount=0; for(;;) { ADCON0bits.GO=1; while (ADCON0bits.GO); // Wait conversion done
iDelay=ADRESL; // Get the 8 bit LSB result iDelay += (ADRESH << 8); // Get the 2 bit MSB result
// Display the LED if (PORTAbits.RA5 == 0) { chType=~chType; chSign=0; }
if (chType == 0) { PORTC=led_patern[iCount++]; delay_ms(iDelay); // Call Delay function if(iCount == 10) iCount=0; } else { if (chSign == 0) { PORTC=chEye; delay_ms(iDelay); // Call Delay function chEye=chEye << 1; if (chEye > 0x04) chSign=1; } else { PORTC=chEye; delay_ms(iDelay); // Call Delay function chEye=chEye >> 1; if (chEye <= 0x01) chSign=0; } } } }
/* EOF: adc.c */
The Analog to Digital Converter C Code
This program basically will show the running LED pattern attached to RC0, RC1, RC2 and RC3 ports on the PIC18LF14K50 microcontroller; the display speed is controlled by the voltage value reads from the user’s trimport on the port RA4 (AN3 - analog input channel 3). This voltage value will be converted by the PIC18LF14K50 ADC peripheral and passing the converted numeric value as the delay argument on the delay_ms() function inside the loop. Pressing the user’s switch will change the running LED pattern.
The user’s trimport work as the voltage divider circuit and provide voltage input level to the microcontroller analog port (AN3); therefore by changing the trimmer means we change the voltage level input and this also will change the running LED speed.
The user’s switch works as a toggle switch, by pressing it once will switch to the second running LED pattern; pressing once again will switch back to the first running LED pattern.
Just for your reference, you could read of how to program the Microchip PIC16 microcontroller family ADC peripheral using HITEC PICC on my previous posted blog PIC Analog to Digital Converter C Programming. Ok now let’s take a look at the PIC18LF14K50 microcontroller ADC peripheral registers (for more information please refers to the datasheet).
1. ADCON0: A/D CONTROL REGISTER 0
The function of this register is to control the microcontroller ADC operation such as power on the ADC circuit, start converting, channel selection, ADC voltage reference selection and ADC result format presentation selection.
The CHS3, CHS2, CHS1 and CHS0 bits are used to select the ADC input channel, by setting all these bits to “0011” means we choose the channel 3 or AN3 (PIN 3) port which connected to the user’s trimport.
ADCON0=0b00001101; // ADC port channel 3 (AN3), Enable ADC
Powering the ADC circuit is simply turning on the ADON bit by setting it to logical “1” and to instruct the PIC microcontroller to start the conversion we just turn on the GO/DONE bit (logical “1“) and wait until this bit turn off when the PIC18LF14K50 microcontroller ADC peripheral done with the conversion; we use the C while statement to wait the ADC conversion as this following code:
ADCON0bits.GO=1; while (ADCON0bits.GO); // Wait conversion done
2. ADCON1: A/D CONTROL REGISTER 1
The PIC18LF14K50 microcontroller ADC peripheral uses the successive approximation method to do the conversion; this method required the reference voltage in order to work. The ADCON1 register is use to tell the ADC peripheral whether we want to use the internal or external voltage references.
By setting all of these bits to logical “0“, we simply tell the PIC18LF44K50 microcontroller ADC peripheral to use its own internal voltage reference for the conversion.
ADCON1=0b00000000; // Use Internal Voltage Reference (Vdd and Vss)
3. ADCON2: A/D CONTROL REGISTER 2
The ADCON2 register is use to set of how the 10-bit ADC result will be presented in two 8-bit registers (ADRESH and ADRESL) and to select the ADC acquisition time and the ADC conversion clock.
By setting the ADFM bit to logical “1” we use the “right justified” result. This mean the higher 2 bits value will be place in the ADRESH register and the lower 8 bits value are in the ADRESL register. By using the C left shifting operation, we could get this 10-bit value as this following code:
iDelay = ADRESL; // Get the 8 bit LSB result iDelay += (ADRESH << 8); // Get the 2 bit MSB result
In order for ADC circuit inside the PIC18LF44K50 microcontroller to meet its specified accuracy than ADC clock period (ADCS) and the ADC acquisition time (ACQT) should be set accordingly
The recommended minimum value for the ADC acquisition time before the ADC circuit start to do the conversion is about 7.45 us and when we use the PIC8LF14K50 microcontroller internal oscillator the time required to convert 1-bit analog data is about 1.7 us; therefore it would be safe to choose twice the minimum acquisition time:
TACQT = 2 x 7.45 us = 14.9 us
The closest ACQT setting for this purpose is to set the ACQT to 12 TAD which give us
TACQT = 12 TAD = 12 x 1.7 us = 20.4 us
If you are not using the TACQT (ACQT2 = 0, ACQT1= 0 and ACQT0 = 0) you have to do the delay in program before starting the AD conversion, therefore it always safer to use the PIC18LF14K50 microcontroller ACQT feature. The following is the complete ADCON2 setup:
ADCON2=0b10101011; // Right justify result, 12 TAD, Select the FRC for 16 MHz
The PIC18LF14K50 microcontroller ADC peripheral is also capable of generating interrupt when it finish the conversion by setting the ADC interrupt flag ADIF bit to logical “1” in PIR1 register, but for the purpose of this tutorial we will not use this facility.
Initial the Internal Oscillator clock and ADC Port
One of the most important setup before we could use the PIC ADC peripheral is to configure the PIC18LF14K50 microcontroller internal clock and ports.
By setting the IRCF2, IRCF1 and IRCF0 bits to logical “1” on the OSCCON register we choose the 16 MHz internal clock from the high frequency internal oscillator (HFINTOSC):
OSCCON=0x70; // Select 16 MHz internal clock
For the analog input this can be done by setting the TRISA, ANSEL and ANSELH registers as the following code:
TRISC = 0x00; // Set All on PORTC as Output TRISA = 0x30; // Input for RA4 and RA5 ANSEL = 0b00001000; // Set PORT AN3 to analog input ANSELH = 0; // Set PORT AN8 to AN11 as Digital I/
Because the we use both RA4 and RA5 ports as an input ports (RA4 for user’s trimport and RA5 for user’s switch) then we turn on the tristate input gate on these ports by setting the TRISA to 0×30 and enabling the AN3 analog input selection by setting the ANSEL to 0b00001000 (0×08 hex value). For the LED’s, we just enabling all of the PORTC tristate gate ports as an output by setting the TRISC register to all zero.
Downloading your C18 ADC project Code
After compiling and simulating your code hook up your PICKit2 programmer to the PICJazz 20PIN board ICSP port turn on the PICJazz 20PIN power. From the MPLAB IDE menu select Programmer -> Select Programmer -> Pickit2 it will automatically configure the connection and display it on the PICkit2 tab Output windows:
You could get the information about the size of your program by opening the adc.map file in your project directory or you could use the MPLAB IDE Memory Usage Gauge facility from the MPLAB IDE menu select View -> Memory Usage Gauge as follow:
Now you are ready to down load the code from MPLAB IDE menu select Programmer -> Program; this will down load the HEX code into the PICJazz 20PIN board:
You could see the running code on the video at the end of this tutorial.
Using the Microchip C18 ADC Peripheral Library
As I mention before that on this tutorial we are going to use the alternative approach to code the PIC18LF14K50 microcontroller ADC peripheral, where we are going to use the Microchip C18 ADC wrap-up library (version 3.34). Using this library supposedly will be much easier than coding the PIC18F14K50 microcontroller ADC peripheral registers directly especially for the beginners; …hmm let’s find out how ease this approach comparing to the first one.
Looking at the Microchip C18 library (MPLAB_C18_Libraries_51297f.pdf) document the ADC library could be used as the above diagram. First we have to use the OpenADC() function to initial the PIC18LF14K50 microcontroller ADC peripheral registers. From the library we know that this function is depend on the microcontroller type we used; therefore to get more detail information about this ADC library we have to read other C18 peripheral library document file found at \doc\periph-lib\AD Converter.htm. From this reference document you will get 12 version of the OpenADC() function and for the PIC18LF14K50 microcontroller we have to use the ADC_V10 OpenADC() function as follow (taken from the AD Converter.htm document):
Function: Configure the A/D convertor. Include: adc.h Prototype:
void OpenADC(unsigned char config, unsigned char config2 , unsigned char config3, unsigned int portconfig);
Code Example:
With AND mask: OpenADC( ADC_FOSC_32 & ADC_RIGHT_JUST & ADC_12_TAD, ADC_CH0 & ADC_REF_VDD_VSS & ADC_INT_ON, ADC_10ANA);
With OR mask: OpenADC( ADC_FOSC_32 | ADC_RIGHT_JUST | ADC_12_TAD, ADC_CH0 | ADC_REF_VDD_VSS | ADC_INT_ON, ADC_10ANA);
This ADC_V10 OpenADC() function will accept 4 arguments: config, config2, config3 and portconfig; you should remember here for other PIC18 microcontroller type the OpenADC() function version may accept only 2 or 3 arguments instead of 4 arguments therefore its important to know which OpenADC() function to be used when you use the other type of PIC18 microcontroller family. There is no general OpenADC() function that work to all PIC18 microcontroller type.
The ADC_V10 OpenADC() function first argument (config) is used to set the A/D Clock Source, A/D result justification and the A/D acquisition time. The second argument (config2) is used to set the A/D channel, A/D interrupt and some miscellaneous option. The third argument (config3) is used to set the A/D Vref+ and Vref- configuration and last the fourth argument (portconfig) is used to set the analog channel port (i.e. ANSEL and ANSELH registers).
Now we continue to look at the ADC_V10 OpenADC() function code example and you will notice that the example only use 3 arguments instead of 4 arguments? Is this mean that the example document give us a wrong information? Now its time to open the ADC_V10 OpenADC() function wrap-up library to find out how this thing really work. Opening the \src\pmc_common\ADC\adcopen.c file and here what we get for the ADC_V10 OpenADC() function:
#elif defined (ADC_V10) void OpenADC( unsigned char config, unsigned char config2, unsigned char config3, unsigned int portconfig) { ADCON0 = 0; ADCON1 = 0; ADCON2 = 0;
ADCON0 = (config2 >> 1) & 0b00111100; //channel selection ADCON1 = (config3 & 0b00001100) | //Positive Voltage Reference Configuration bits (config3 & 0b00000011); //Negative Voltage Reference Configuration bits ADCON2 = (config & 0b10000000) | ((config >> 4) & 0b00000111) | //A/D Conversion Clock Select bits ((config << 2) & 0b00111000); //A/D Acquisition Time Select bits
ANSEL = portconfig; //Didn't Change ANSELH = (portconfig >> 8); //Didn't Change if( config2 & 0b10000000 ) //interrupt enable check { PIR1bits.ADIF = 0; //Clear the ADC Interrupt bit PIE1bits.ADIE = 1; //Enable the ADC Interrupt INTCONbits.PEIE = 1; //Peripheral Interrupt Enable } //A/D Conversion Status bit--A/D converter module is operating ADCON0bits.ADON = 1; }
Hmm,.. this ADC_V10 OpenADC() function use 4 arguments after all (the example document is wrong) and this code seem similar to what we have already done on the first example; by carefully reading the necessary bits definition in the \h\adc.h file and used them with the AND mask as the arguments to the ADC_V10 OpenADC() function; do some debugging on the required PIC18LF14K50 registers, finally I came with this following C code:
/* *************************************************************************** ** File Name : adc2.c ** Version : 1.1 ** Description : Analog to Digital Converter ** Using C18 ADC Peripheral Wrap-up Library ** Author : RWB ** Target : PICJazz 20PIN Board: PIC18LF14K50 ** Compiler : Microchip C18 v3.34 C Compiler ** IDE : Microchip MPLAB IDE v8.30 ** Programmer : PICKit2 ** Last Updated : 28 Oct 2009 ** ***************************************************************************/ #include #include #include
/* ** PIC18LF14K50 Configuration Bit: ** ** FOSC = IRC - Internal RC Oscillator ** CPUDIV = NOCLKDIV - No CPU System Clock divide ** PLLEN = OFF - PLL is under software control ** FCMEN = OFF - Fail-Safe Clock Monitor disabled ** BOREN = OFF - Brown-out Reset disabled in hardware and software ** WDTEN = OFF - WDT is controlled by SWDTEN bit of the WDTCON register ** MCLRE = ON - MCLR pin enabled, RE3 input pin disabled ** LVP = OFF - Single-Supply ICSP disabled */ #pragma config FOSC = IRC, CPUDIV = NOCLKDIV, PLLEN = OFF #pragma config FCMEN = OFF, BOREN = OFF #pragma config WDTEN = OFF, MCLRE = ON, LVP = OFF
// Delay in ms (approximately) for 16 MHz Internal Clock void delay_ms(unsigned int ms) { do { Delay1KTCYx(4); } while(--ms); }
void main(void) { unsigned char chSign,chEye,chType,iCount; unsigned int iDelay; unsigned char led_patern[] = {0b00000000,0b00000001, 0b00000011,0b00000111, 0b00001111,0b00001111, 0b00001110,0b00001100, 0b00001000,0b00000000};
OSCCON=0x70; // Select 16 MHz internal clock
TRISC = 0x00; // Set All on PORTC as Output TRISA = 0x30; // Input for RA4 and RA5 /* Init ADC */ /* ** Original: ** #define ADC_REF_VDD_VDD 0b11111001 // ADC voltage source VREF+ = AVDD ** ** We redefine this definition to ADC_REF_VDD_VDD_X */ #define ADC_REF_VDD_VDD_X 0b11110011 // ADC voltage source VREF+ = AVDD
OpenADC(ADC_FOSC_RC & ADC_RIGHT_JUST & ADC_12_TAD, ADC_CH3 & ADC_INT_OFF, ADC_REF_VDD_VDD_X & ADC_REF_VDD_VSS, 0b00001000);
chEye=0x01; // Initial Eye Variables with 0000 0001 chSign=0; iDelay=100; chType=0; iCount=0; for(;;) { ConvertADC(); // Start conversion while(BusyADC()); // Wait for completion iDelay = ReadADC(); // Read result // Display the LED if (PORTAbits.RA5 == 0) { chType=~chType; chSign=0; }
if (chType == 0) { PORTC=led_patern[iCount++]; delay_ms(iDelay); // Call Delay function if(iCount == 10) iCount=0; } else { if (chSign == 0) { PORTC=chEye; delay_ms(iDelay); // Call Delay function chEye=chEye << 1; if (chEye > 0x04) chSign=1; } else { PORTC=chEye; delay_ms(iDelay); // Call Delay function chEye=chEye >> 1; if (chEye <= 0x01) chSign=0; } } } CloseADC(); // Disable A/D converter }
/* EOF: adc2.c */
As you’ve seen from the above code, I had to redefine the third argument definition ADC_REF_VDD_VDD (0b11111001) to ADC_REF_VDD_VDD_X (0b11110011) to make the ADC_V10 OpenADC() function to set the PIC18LF14K50 microcontroller ADCON1 register correctly (using the internal voltage reference). I think this is a bug for the ADC_V10 type on the Microchip C18 version 3.34 ADC peripheral libraries.
For starting and reading the ADC channel using this library is just a little bit easier comparing to the first approach as shown on this following code:
ConvertADC(); // Start conversion while(BusyADC()); // Wait for completion iDelay = ReadADC(); // Read result
Just for curiosity, I put this following memory usage gauge to compare the memory usage between using the Microchip C18 ADC peripheral library approach and without using it; and it’s obvious that using the first approach give you a lesser HEX code size.
After compiling and downloading the HEX code to the PICJazz 20PIN board you will have similar result as shown on this following video:
The Final Though
Is obviously the Microchip C18 (version 3.34) ADC peripheral library is not ease to use at all and certainly is not intended to be used by the beginners; as you will have to read many documents and must have a good understanding of the PIC18 microcontroller ADC principal in order to make this library work for you. My suggestion is to avoid using the Microchip C18 ADC peripheral library (especially if you are really not understand the PIC18 microcontroller ADC peripheral) and use direct register manipulation as shown on the first approach to handle your PIC18 microcontroller ADC project.