Learn To Write Code For 8051, Arduino, AVR, dsPIC, PIC, STM32 ARM Microcontroller, etc.
Coding Embedded Controller With C/C++.
Printed Circuit Board (PCB) Project For Electronics Hobbyists.
A digital IC, 74HC165 is a parallel in serial out shift registers. It has 8 parallel input and one serial output. It's commonly used for inputs expanding when the controller run out of digital I/O. It's communication interface's just like an SPI protocol. But without using the SPI module of the MCU, we can use software emulation to function an SPI-like protocol.
Pins diagram
Timing diagram of 74HC165
The load pin used for loading the 8-bit parallel inputs D0 to D7. The clock pin used for scanning all the inputs and shift them out serially via Serial Out pin.
The 74HC165 clock pin connects to RE0. The load pin connects to RE1.
And the serial out pin connects to RE2. PortD represents the digital data.
Source code:
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
char serialRead(){
char read=0,temp=0;
/*Raise load pin*/
RE1=1;
for(int i=0;i<8;i++){
/*Shift data left*/
temp<<=1;
/*if serial out high
set read high*/
read=((RE2==1)?1:0);
/*clock pulse*/
RE0=0;
for(int k=0;k<100;k++);
RE0=1;
/*load data into variable*/
temp|=read;
}
/*Lower load pin*/
RE1=0;
return temp;
}
void main(){
/*Port configuration*/
PORTD=0x00;
PORTE=0x00;
TRISD=0x00;
TRISE=0x04;
/*Clear analog function
on PortE*/
ANSEL=0x00;
while(1){
/*Read the digital inputs*/
PORTD=serialRead();
}
}
A snap shot of running program, The 74HC165 loads 0x0F.
The 74HC595 shift registers is a serial in parallel out shift register type. We just use three digital pins to send the data the register. Using these three wires could drive multiple of this IC.
A Light dependent resistor (LDR) is a resistor whose resistance varies depend on the light source it absorbs. Its electrical resistance decrease up on the intensity of light. With this feature we can create an electric circuit to create a variable output voltage fed to the MCU ADC. It usually used for controlling the output lamp in house.
Some LDR I posses.
A negative temperature coefficient thermistor (NTC) is a resistor whose resistance decrease up on the heat it absorbs. It usually used for controlling the output cooling fan speed.
Some 10k NTC I use for my own workshop
In this example, I use an LDR for turning a lamp on and off. An NTC is used for turning the output fan on and off.
LDR create a variable output analog voltage fed to AN12. Its output is a lamp controlled
by RD7. NTC fed an analog output voltage to AN11 with the corresponding RD0 DC fan
motor output.
The program use timer 0 to schedule the ADC reading every one second. ADC conversion doesn't use any ADC interrupt.
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
void portSetup(void){
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTD=0x00;
TRISB0=1;
TRISB4=1;
TRISD=0x00;
}
void adcSetup(void){
/*Result is right justify*/
ADFM=1;
/*By default is analog,
but again set it to analog*/
ANS12=1;
ANS11=1;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
GO=1;
while(GO);
}
void interruptSetup(void){
/*Select FOSC*/
T0CS=0;
/*Select timer 0 Prescaler*/
PSA=0;
/*Enable Timer 0 Overflow
interrupt*/
T0IE=1;
/*Turn on Global interrupt
Control*/
GIE=1;
/*Clear interrupt flag*/
T0IF=0;
/*Clear timer 0 register*/
TMR0=0;
}
unsigned int adcResult;
unsigned char oneSecondTick=0;
void main(void){
portSetup();
adcSetup();
interruptSetup();
while(1){
/*If it's one second*/
if(oneSecondTick>=15){
/*Select NTC*/
ADCON0bits.CHS=0x0B;
__delay_ms(10);
GO=1;
while(GO);
adcResult=(ADRESH<<8)+ADRESL;
RD0=(adcResult>512)?0:1;
/*Select LDR*/
ADCON0bits.CHS=0x0C;
__delay_ms(10);
GO=1;
while(GO);
adcResult=(ADRESH<<8)+ADRESL;
RD7=(adcResult<512)?0:1;
oneSecondTick=0;
}
}
}
void interrupt _ISR(void){
/*If timer 0 Overflow*/
if(T0IF){
oneSecondTick+=1;
T0IF=0;
}
}
It is just a software simulation. In the physical hardware test we can find an appropriate value of ADC result to fit the application.
The ADC module could trigger an interrupt flag when the reading is completed. Using the interrupt makes the running tasks more effective since it doesn't need to wait for reading completion in the main program loop.
To use the ADC interrupt, we must enable the,
Global interrupt control bit (GIE)of the INTCON register
ADC interrupt enable bit (ADIE) of the PIE1 register,
and clear the ADC complete reading interrupt flag (ADIF) of the PIR1 register.
To read the ADC using interrupt method,
set 'GO' of the ADCON0 to '1'
test the ADIF flag
if ADIF = 1, we can get the ADC result
clear ADIF
In this example, I use ADC interrupt to return a completed ADC reading result. Timer 0 is used for initiating the ADC reading for every one second. Both ADC and timer use interrupt ISR.
Analog voltage input fed to AN12. A multiplexed SSD display the ADC reading
result in decimal.
C source code's here:
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
void driveDisplays(unsigned int result){
unsigned char ssd[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,
0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
//Digit 1 1000's
PORTD=0x00;
PORTC=ssd[result/1000];
if(result>=1000)PORTD=0x01;
__delay_ms(10);
//Digit 2 100's
PORTD=0x00;
PORTC=ssd[(result%1000)/100];
if(result>=100)
PORTD=0x02;
__delay_ms(10);
//Digit 3 10's
PORTD=0x00;
PORTC=ssd[(result%100)/10];
if(result>=10)
PORTD=0x04;
__delay_ms(10);
//Digit 4 1's
PORTD=0x00;
PORTC=ssd[result%10];
PORTD=0x08;
__delay_ms(10);
}
unsigned int readADC(void){
GO=1;
while(GO);
__delay_ms(10);
return (ADRESH<<8)+ADRESL;
}
void portSetup(void){
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
TRISB=0x01;
TRISC=0x00;
TRISD=0x00;
}
void adcSetup(void){
/*Result is right justify*/
ADFM=1;
/*By default is analog,
but again set it to analog*/
ANS12=1;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
/*Select AN12 RB0*/
ADCON0bits.CHS=0b1100;
}
void interruptSetup(void){
/*Select FOSC*/
T0CS=0;
/*Select timer 0 Prescaler*/
PSA=0;
/*Enable Timer 0 Overflow
interrupt*/
T0IE=1;
/*Turn on Global interrupt
Control*/
GIE=1;
/*Enable Peripheral Interrupt*/
PEIE=1;
/*Enable ADC complete reading
interrupt*/
ADIE=1;
/*Clear ADC interrupt Flag*/
ADIF=0;
/*Clear interrupt flag*/
T0IF=0;
/*Clear timer 0 register*/
TMR0=0;
}
unsigned int adcResult;
unsigned char oneSecondTick=0;
void main(void){
portSetup();
adcSetup();
interruptSetup();
while(1){
driveDisplays(adcResult);
}
}
void interrupt _ISR(void){
/*If timer 0 Overflow*/
if(T0IF){
oneSecondTick+=1;
T0IF=0;
}
/*If it's one second*/
if(oneSecondTick>=15){
oneSecondTick=0;
GO=1;
}
/*If ADC reading completed*/
if(ADIF){
adcResult=(ADRESH<<8)+ADRESL;
ADIF=0;
}
}
The ADC reading acquires 921 digital value result in decimal.
The ADC module's resolution is up to 10-bit, yield an acceptable measuring accuracy with the voltage within 50 V DC.
In this example, I use this ADC module to measure a DC voltage within -25 V to +25 V. So the total voltage magnitude is 50 V.
Since the maximum reference voltage is 5 V in magnitude. I use a voltage divider circuit to scale the input voltage to the lower 5 V.
From the voltage divider:
The V(ADC) is at 5 V maximum. V(MEASURE) could be 50 V in magnitude. So we divide the V(MEASURE) by 11.
But in the program simulation I don't use 11. I use 9 because it cause a lot of error.
In the real hardware test, you can try 11.
The voltmeter display the measuring voltage on a five-digit multiplexed SSD. The ADC reading is scheduled every one second due the timer 0 scheduling. The analog DC input voltage is fed from a -25 V and +25 V voltage terminals, giving a 50 V DC magnitude.
Schematic diagram a digital two poles voltmeter. A POT used for adjusting the analog input
voltage, varies from -25 V to +25 V.
Source code:
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
void driveDisplays( int analogRead){
unsigned char ssd[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,
0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
float voltage;
int _voltage;
/*Voltage Calculation total reference is
+5V with centered ground*/
voltage=((5.0*((float)(analogRead)/1024))-2.5)/9.1;
//voltage/=11;
_voltage=(int)(voltage*10000);
/*Check negative voltage*/
if(_voltage&0x8000){
_voltage=-_voltage;
PORTD=0x00;
PORTC=0x40;
PORTD=0x01;
__delay_ms(10);
}
/*Decimal Place*/
PORTD=0x00;
PORTC=ssd[_voltage/1000];
if(_voltage>=1000)
PORTD=0x02;
__delay_ms(10);
/*Floating point*/
PORTD=0x00;
PORTC=ssd[(_voltage%1000)/100]|0x80;
PORTD=0x04;
__delay_ms(10);
PORTD=0x00;
PORTC=ssd[(_voltage%100)/10];
PORTD=0x08;
__delay_ms(10);
PORTD=0x00;
PORTC=ssd[_voltage%10];
PORTD=0x10;
__delay_ms(10);
}
int readADC(void){
GO=1;
while(GO);
return (ADRESH<<8)+ADRESL;
}
void portInit(void){
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
/*RB4 for analog input*/
TRISB4=1;
TRISC=0x00;
TRISD=0x00;
}
void adcInit(void){
/*Select external Voltage reference
+2.5V to -2.5V*/
VCFG1=1;
VCFG0=1;
/*Result is right justify*/
ADFM=1;
/*By default is analog,
but again set it to analog*/
ANS11=1;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
/*Select AN11 RB4*/
ADCON0bits.CHS=0x0B;
/*initiate a conversion*/
GO=1;
/*Wait until GO=0 "done"*/
while(GO);
}
void timerInit(void){
/*Select FOSC*/
T0CS=0;
/*Select timer 0 Prescaler*/
PSA=0;
/*Enable Timer 0 Overflow
interrupt*/
T0IE=1;
/*Turn on Global interrupt
Control*/
GIE=1;
/*Clear interrupt flage*/
T0IF=0;
/*Clear timer 0 register*/
TMR0=0;
}
int adcResult;
int counter=0;
void main(void){
portInit();
adcInit();
timerInit();
while(1){
driveDisplays(adcResult);
}
}
void interrupt _ISR(void){
if(T0IF){
counter++;
T0IF=0;
}
/* If it's one second*/
if(counter>=15){
adcResult=readADC();
counter=0;
}
}
A screen shot shows a positive voltage reading 24.84 V nearest to the positive terminal.
A screen shot shows a negative voltage reading -24.89 V nearest to the negative terminal.
LM35 is an analog temperature sensor built in an integrated circuit. It could work in voltage range of -55 to +150 degree Celsius. It could be supply by a DC voltage between 4 to 30 V. It analog output is the temperature data. It is a linear output, 10 mV per degree Celsius.
Using the MCU ADC, it's easy to access rather using a complex protocol like one-wire or I2C bus.
LM35 IC in TO-92 package and its pin out.
To find the analog temperature data, we can find the digital ADC result per 10 mV. Or we can find the total voltage reading, multiplied by 100 to get the equivalent temperature in degree Celsius.
In this example, I use the ADC module to read the temperature data between -55 to 150 degree Celsius. The internal ADC module of PIC16F887 use -2.5 V to +2.5 V reference voltage, to make the device be able to read both negative and positive voltage.
I use timer 0 overflow interrupt to schedule the ADC reading for every one second.
The SSD is multiplexed with seven digits. Each digit turn on for every 5 milli seconds.
Schematic diagram. The LM35 connects to RB4/AN11 to convert the analog
temperature data. The display is multiplexed with seven digits SSD.
Source code's here:
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
#define ssdRate 5
void driveDisplays( int analogRead){
unsigned char ssd[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,
0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
float voltage;
int _voltage;
/*Voltage Calculation total reference is
+5V with centered ground*/
voltage=(5*((float)(analogRead)/1024))-2.5;
voltage*=10;
/*Now _voltage becomes temperature,
multiply it by 100 to convert it
to the SSD data*/
_voltage=(voltage*100);
/*Check negative voltage*/
if(_voltage&0x8000){
_voltage=-_voltage;
PORTD=0x00;
PORTC=0x40;
if((_voltage<1000)|(_voltage>-1000))
PORTD=0x02;
else
PORTD=0x01;
__delay_ms(ssdRate);
}
/*Decimal Place*/
if((_voltage>=1000)|(_voltage<=-1000)){
PORTD=0x00;
PORTC=ssd[_voltage/1000];
PORTD=0x02;
__delay_ms(ssdRate);
}
PORTD=0x00;
PORTC=ssd[(_voltage%1000)/100];
if(_voltage>=100)
PORTD=0x04;
__delay_ms(ssdRate);
/*Floating point*/
PORTD=0x00;
PORTC=ssd[(_voltage%100)/10]|0x80;
PORTD=0x08;
__delay_ms(ssdRate);
PORTD=0x00;
PORTC=ssd[_voltage%10];
PORTD=0x10;
__delay_ms(ssdRate);
PORTD=0x00;
PORTC=0x63;
PORTD=0x20;
__delay_ms(ssdRate);
PORTD=0x00;
PORTC=0x39;
PORTD=0x40;
__delay_ms(ssdRate);
}
int readADC(void){
GO=1;
while(GO);
/*Return a 10-bit result*/
return (ADRESH<<8)+ADRESL;
}
void portSetup(void){
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
/*RB4 for analog input*/
TRISB4=1;
TRISC=0x00;
TRISD=0x00;
}
void timer0Setup(void){
/*Select FOSC*/
T0CS=0;
/*Select timer 0 Prescaler*/
PSA=0;
/*Enable Timer 0 Overflow
interrupt*/
T0IE=1;
/*Turn on Global interrupt
Control*/
GIE=1;
/*Clear interrupt flage*/
T0IF=0;
/*Clear timer 0 register*/
TMR0=0;
}
void adcSetup(void){
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
/*RB4 for analog input*/
TRISB4=1;
TRISC=0x00;
TRISD=0x00;
/*Select external Voltage reference
+2.5V to -2.5V*/
VCFG1=1;
VCFG0=1;
/*Result is right justify*/
ADFM=1;
/*By default is analog,
but again set it to analog*/
ANS11=1;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
/*Select AN11 RB4*/
ADCON0bits.CHS=0x0B;
/*initiate a conversion*/
GO=1;
/*Wait until GO=0 "done"*/
while(GO);
}
By default two references voltages of the ADC internally connected to VDD and VSS, supplying a +5 V reference voltage for the ADC module. We can change the reference voltage to any value in the total range of +5 V DC.
The VREF+ and VREF- pin could be programmed to externally connect to any voltage reference. By assigning the 'VCFG0' to '1' allows the VREF+ pin connect to external positive voltage. Similarly, by assigning the 'VCFG1' to '1' allows the VREF- pin connect to external negative voltage.
For this example, I connect the VREF+ pin to +2.5 V and the VREF- pin to -2.5 V. Hence, the total reference voltage for the ADC module is,
The step voltage size is 0.00488 V. In this case the the lowest voltage of -2.5 V gives the ADC reading result 0. While the maximum voltage +2.5 V gives the ADC reading result 1023. The zero analog input voltage yields the ADC reading result to 512.
Schematic diagram. RA3/AN3 is used for positive voltage reference connects to +2.5 V.
RA2/AN2 is used for negative voltage reference connects to -2.5 V. The voltage reading displays
between -2.5 V to +2.5 V.
Source code:
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
void driveDisplays( int analogRead){
unsigned char ssd[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,
0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
float voltage;
int _voltage;
/*Voltage Calculation total reference is
+5V with centered ground*/
voltage=(5*((float)(analogRead)/1024))-2.5;
_voltage=(voltage*100);
/*Check negative voltage*/
if(_voltage&0x8000){
_voltage=-_voltage;
PORTD=0x00;
PORTC=0x40;
PORTD=0x01;
__delay_ms(10);
}
/*Decimal Place*/
PORTD=0x00;
PORTC=ssd[_voltage/100]|0x80;
PORTD=0x02;
__delay_ms(10);
/*Floating point*/
PORTD=0x00;
PORTC=ssd[(_voltage%100)/10];
PORTD=0x04;
__delay_ms(10);
PORTD=0x00;
PORTC=ssd[_voltage%10];
PORTD=0x08;
__delay_ms(10);
}
int readADC(void){
GO=1;
while(GO);
__delay_ms(10);
return (ADRESH<<8)+ADRESL;
}
void main(void){
int adcResult;
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
/*RB4 for analog input*/
TRISB4=1;
TRISC=0x00;
TRISD=0x00;
/*Select external Voltage reference
+2.5V to -2.5V*/
VCFG1=1;
VCFG0=1;
/*Result is right justify*/
ADFM=1;
/*By default is analog,
but again set it to analog*/
ANS11=1;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
/*Select AN11 RB4*/
ADCON0bits.CHS=0x0B;
ADON=1;
/*initiate a conversion*/
GO=1;
/*Wait until GO=0 "done"*/
while(GO);
while(1){
adcResult=readADC();
driveDisplays(adcResult);
}
}
The screen-shot shows the analog voltage input of -1.49 V.
As we can see in the previous post, the analog input channels could be selected by the CHS bits of the ADCON0 register.
In this example I use the ADC module to read four input analog pins RA0, RA1, RA2 and RA3. The selection is made by A 2 positions DIP switch. This DIP switch could make four values. This value make the selection of analog channel.
The overall process is 80 percents similar to the previous one.
Schematic diagram. Four Potentiometers fed their analog input voltage to
RA0 to RA3. DSW1 is DIP switch make the analog channel selection
for the ADC module conversion. A four-digit multiplexed SSD displays
a decimal representation of each 10-bit analog resolution.
Source code is here.
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
void driveDisplays(unsigned int cnt){
unsigned char ssd[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,
0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
//Digit 4 1000's
PORTD=0x00;
PORTC=ssd[cnt/1000];
PORTD=0x01;
__delay_ms(10);
//Digit 3 100's
PORTD=0x00;
PORTC=ssd[(cnt%1000)/100];
PORTD=0x02;
__delay_ms(10);
//Digit 2 10's
PORTD=0x00;
PORTC=ssd[(cnt%100)/10];
PORTD=0x04;
__delay_ms(10);
//Digit 1 1's
PORTD=0x00;
PORTC=ssd[cnt%10];
PORTD=0x08;
__delay_ms(10);
}
unsigned int readADC(void){
ADCON0bits.CHS=PORTB;
GO=1;
while(GO);
__delay_ms(10);
return (ADRESH<<8)+ADRESL;
}
void main(void){
unsigned int adcResult;
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
TRISB=0x03;
TRISC=0x00;
TRISD=0x00;
/*PORTB has no analog inputs*/
ANSELH=0x00;
/*Enable Weak Pull ups*/
nRBPU=0;
/*Turn on RB0 and RB1
Weak Pull Up resistor*/
WPUB=0x03;
/*Result is right justify*/
ADFM=1;
/*Set some analog pins of PORTA*/
ANSEL=0x0F;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
/*By default select AN0*/
ADCON0bits.CHS=0b0000;
/*initiate a conversion*/
GO=1;
/*Wait until GO=0 "done"*/
while(GO);
while(1){
adcResult=readADC();
driveDisplays(adcResult);
}
}
A simulation screen shot. The ADC module is reading the analog voltage
input fed from RA3. The result is 2.5 V which is 511 in digital representation.
In this previous post, we introduced about using ADC module of PIC16F887. Generally, the voltage references VREF+ and VREF- are internally wired to VDD and VSS, respectively. PIC16F887 could be supplied in a range of 2.0 V to 5.5 V. Generally, the supply voltage is +5 V because most external devices work at +5 V.
Step size is an analog voltage per unit of the ADC resolution.
For VREF+ = +5 V (VDD) and the VREF- = 0 V (VSS), we get the step size of
For example if the ADC result equal to 512, the voltage is,
Similarly, we get find the digital result equivalent to the analog input voltage,
In this example, I use a POT to adjust the analog input voltage varies from 0 to +5 V. To find the voltage we can use the equations listed above. But we can use another short form as,
In the C program, we must use the floating point to measure the analog voltage.
Schematic diagram. RB0 is AN12 analog input, measuring the input voltage
from 0 to +5 V. PORTC and PORTD display the measuring voltage with
one floating point.
The C source code:
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
void driveDisplays(unsigned int analogRead){
unsigned char ssd[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,
0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
float voltage;
int _voltage;
/*Voltage Calculation*/
voltage=5*((float)analogRead/1024);
_voltage=voltage*10;
/*Decimal Place*/
PORTD=0x00;
PORTC=ssd[_voltage/10]|0x80;
PORTD=0x01;
__delay_ms(10);
/*Floating point*/
PORTD=0x00;
PORTC=ssd[_voltage%10];
PORTD=0x02;
__delay_ms(10);
}
unsigned int readADC(void){
GO=1;
while(GO);
__delay_ms(10);
return (ADRESH<<8)+ADRESL;
}
void main(void){
unsigned int adcResult;
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
TRISB=0x01;
TRISC=0x00;
TRISD=0x00;
/*Result is right justify*/
ADFM=1;
/*By default is analog,
but again set it to analog*/
ANS12=1;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
/*Select AN12 RB0*/
ADCON0bits.CHS=0b1100;
/*initiate a conversion*/
GO=1;
/*Wait until GO=0 "done"*/
while(GO);
while(1){
adcResult=readADC();
driveDisplays(adcResult);
__delay_ms(20);
}
}
Simulation screen shot. The measuring voltage displays 3.9 V DC.
An analog to digital converter (ADC) module converts an analog voltage to a digital binary equivalence value. In PIC16F887 the built-in ADC module use a successive approximation type. I has up to 14 analog channels with reference voltage selection. The ADC resolution is 10-bit, made up of two distinct 8-bit registers.
Some pins of PortA and PortB could be used for analog inputs.
Analog input pins of PIC16F887
There are two option of ADC clocks, MCU clock and the ADC built-in RC oscillator (FRC). The FRC gives a frequency of 500 kHz maximum. Using the MCU clock with the maximum clocking frequency, the ADC could complete the conversion time at 100 nano seconds.
A simplified scheme of ADC module in PIC16F887. An original detailed block diagram could be found in PIC16F887 datasheet.
ADC module takes up to 12 cycles to complete the conversion to make the 10-bit result ready. Since the 10-bit result consists of two 8-bit registers. It could be arranged in two margins, left or right.
Up on the completion, an interrupt flag relates to ADC is set. With this advantage, we can use interrupt routine to process the analog to digital conversion.
There are some registers to work with ADC module:
Input output port register (PORTx),
Port direction register (TRISx),
ANSEL and ANSELH registers,
ADCON0 and ADCON1 register,
related interrupt registers,
ADC result registers.
Some registers listed above have already discussed in previous posts. So we are listing some not-ready-listed registers.
This register set the analog inputs pins of some of PortA and PortE. Setting any bit to '1', assigning the corresponding pin to analog input. By default they are '1'.
ANSELH controls the analog input pin at PortB. It has only 6 analog input pins while the remaining two are digital. By default, they are set to '1'.
This is the first register to control the operation of ADC module.
ADC Conversion Clock Select bits ADCS[1:0], is a two-bit clock selection for ADC. The clock source for ADC could fed from MCU clock with the 2, 8 and 32 clock division. Another clock source could fed from internal ADC oscillator with the maximum frequency of 500 kHz. Setting ADCS to 0x03, the ADC clock is fed from it's internal oscillator.
Analog Channel Select bits CHS[3:0], could select between the 14 analog input channels. These bits could select the reference capacitor for analog comparator and the 0.6 V fixed voltage reference.
ADC Conversion Status bits GO/nDone, initiate a start of conversion when it's set to '1'. It will clear to '0' when the conversion completed. We can also use this bit to check the status of the conversion.
ADC Enable bit ADON, turn on the ADC module when set to '1'. Otherwise it's turned off.
ADCON1 configures additional setting of the ADC operation.
ADC Conversion Result format Select bit ADFM, arranges the margin of the ADC conversion result. It effect the ADRESL and ADRESH register. When it's '0' (default) the result is right justified.
Negative voltage reference bit VCFG1, set the negative voltage reference for ADC. Setting it to '1', the negative voltage reference must supply to VREF- pin. By default, it's '0' and the negative voltage reference pin is internally wired to VSS of the MCU.
Positive voltage reference bit VCFG0, set the positive voltage reference for ADC. Setting it to '1', the positive voltage reference must supply to VREF+ pin. By default, it's '0' and the positive voltage reference pin is internally wired to VDD of the MCU.
The ADC Result High Register ADRESH, store the result of ADC conversion. In the picture above, it's assumed that the result format is right justified (ADFM=1).
ADRESL is a pair register with ADRESH. Joining them together to make a 10-bit ADC resolution. In this picture we assume that the result format is right justified (ADFM=1).
Programming the ADC in XC8
To access to the ADC module, we must setting up the ADC module and work with the conversion process.
To set up the ADC module:
set the related pin to input
By default those related pins are analog inputs. But we can do it optionally, by setting any bits of the ANSEL and ANSELH register. These two registers contain the analog and digital pins selection as listed above.
Select the ADC clock source by setting the ADCS bits of the ADCON0 register.
Since there are 14 analog channels. Only one active channel is selected for reading the analog voltage value. So we must select one channel by setting the 4-bit CHS bits of the ADCON0 register.
Turn on the ADC module by setting the ADON bit of the ADCON0 register to '1'.
Two resolution register ADRESL and ADRESH pair store the conversion result. We must set it to right or left justification as preferred by assigning the value to ADFM bit of the ADCON1 register.
Select the voltage reference by setting any value to the VCFG1 and VCFG0 bits of the ADCON1 register.
To start a conversion:
If we are not yet select the analog channel, do it with the CHS bits.
Wait for some period to make the capacitor properly work
Set the GO/nDone bit to '1' to start the conversion
Wait for the completion by checking the GO/nDone bit.
Get the result from ADRESH and ADRESL register pair.
Programming Example
In this example I read an analog voltage value from AN12 input channel. The result is display in decimal (0 to 1023 for 10-bit value) on a multiplexed SSD.
The ADC clock is driven from its dedicated internal oscillator. The analog value is fed from a POT ranging from 0 to +5 V. The voltage references is internally wired to VSS and VDD of the MCU.
Schematic diagram. RB0 or AN12 is an analog input reading pin, reading an analog
value from the POT. The POT works with a passive low pass filter withe any
calculation. The result is displayed on a 4-digit multiplexed SSD ranging from
0 to 1023 in decimal value.
Source code is here.
#include<xc.h>
// PIC16F887 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = XT
#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 = ON
#pragma config FCMEN = ON
#pragma config LVP = ON
// CONFIG2
#pragma config BOR4V = BOR40V
#pragma config WRT = OFF
/*_XTAL_FREQ use for __delay*/
#define _XTAL_FREQ 4000000
void driveDisplays(unsigned int cnt){
unsigned char ssd[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,
0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
//Digit 4 1000's
PORTD=0x00;
PORTC=ssd[cnt/1000];
PORTD=0x01;
__delay_ms(10);
//Digit 3 100's
PORTD=0x00;
PORTC=ssd[(cnt%1000)/100];
PORTD=0x02;
__delay_ms(10);
//Digit 2 10's
PORTD=0x00;
PORTC=ssd[(cnt%100)/10];
PORTD=0x04;
__delay_ms(10);
//Digit 1 1's
PORTD=0x00;
PORTC=ssd[cnt%10];
PORTD=0x08;
__delay_ms(10);
}
unsigned int readADC(void){
GO=1;
while(GO);
__delay_ms(10);
return (ADRESH<<8)+ADRESL;
}
void main(void){
unsigned int adcResult;
/*Analog and digital Port
Configuration*/
PORTB=0x00;
PORTC=0x00;
PORTD=0x00;
TRISB=0x01;
TRISC=0x00;
TRISD=0x00;
/*Result is right justify*/
ADFM=1;
/*By default is analog,
but again set it to analog*/
ANS12=1;
/*Select FRC Clock of ADC module*/
ADCON0bits.ADCS=0x03;
/*Turn on ADC Module*/
ADON=1;
/*Select AN12 RB0*/
ADCON0bits.CHS=0b1100;
/*initiate a conversion*/
GO=1;
/*Wait until GO=0 "done"*/
while(GO);
while(1){
adcResult=readADC();
driveDisplays(adcResult);
}
}
A simulation screen shot. The analog reading is 921 in decimal.