Using a Resistor Divider to read and calculate voltages using the ADC peripheral

Started by top204, Oct 08, 2023, 09:56 AM

Previous topic - Next topic

top204

Sometimes, what seems obvious can be a bit confusing when getting down to the coding of a particular operation, and I found that out when requiring an adaptable method to measure a higher voltage using an ADC and a resistor divider to drop the higher voltage into the region the ADC could accept, and make the code so it was easier to adapt to different resistors and ADC settings.

The theory of a resistor divider is not so important for this post, and it can be found over and over again on the internet, but the calculations in code to convert a raw ADC value to a voltage value, and take into account the voltage drop of the resistor divider, can seem a bit daunting and can look rather confusing sometimes, so I have created a procedure that can be called and it will return a value that represents the voltage applied to the resistor divider's input.

It uses alias names for the resistors and the ADC's +vref voltage and the resolution of the ADC, so it can be changed and understood quite easily. The procedure returns the voltage as an integer value, so it is easy to use within comparisons in code, and faster to operate, as well as easier to adapt for other resistor values and ADC specifics.

The Voltage_Read() procedure can be used with any 14-bit core device that contains an ADC, or 18F device or PIC24 device or dsPIC33 device, because it is using standard expressions. It's just a matter of setting up the ADC peripheral for differing devices.

The demo code listing is below:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Read a voltage using a resistor divider attached to an ADC pin and serially transmit the ASCII voltage value.
' Uses only integer variables, so the code is compact and fast.
' ADC SFRs (Special Function Registers) and config fuses used in the program listing suit a PIC18F26K40 device.
'
' Written by Les Johnson for the Positron8 BASIC Compiler (08-10-2023).
' https://sites.google.com/view/rosetta-tech/home
' https://protoncompiler.com
'
' In the resistor divider, R1 is the top resistor and R2 is the bottom resistor. C1 is used to stabilise the voltage going into the ADC
'
' Voltage to be Measured In +
'     v
'     |
'     |
'    .-.
'    | | R1
'    | |
'    '-'
'     |
'     o--------o------o To ADC pin
'     |        |
'    .-.       |
'    | | R2   --- C1
'    | |      --- 100nF
'    '-'       |
'     |        |
'     o---------
'     |
'    ===
'    GND
'
    Device = 18F26K40                                           ' Tell the compiler what device to compile for
    Declare Xtal = 64                                           ' Tell the compiler what frequency the device is operating at (in MHz)
    Declare Create_Coff = True                                  ' Create a COF file for simulation in Proteus
'
' Setup USART1
'
    Declare Hserial1_Baud = 9600                                ' Set USART1 Baud rate
    Declare HRSOut1_Pin = PORTC.6                               ' Set the pin to use for USART1 TX
'
' Setup the pin and peripheral used for the ADC channel
'
$define Voltage_Read_Pin     PORTA.0                            ' Voltage Divider ADC pin (used as an ADC input)
$define Voltage_Read_ADC_SFR ANSELA.0                           ' The ADC analogue/digital SFR and bit that matches the pin used for the voltage reading above
'
' Create some ADC Input channel $defines for pins.
' These make it easier to choose the ADC channel in a program listing.
'
$define cANA0 0
$define cANA1 1
$define cANA2 2
$define cANA3 3
$define cANA4 4
$define cANA5 5
$define cANA6 6
$define cANA7 7

$define cANB0 8
$define cANB1 9
$define cANB2 10
$define cANB3 11
$define cANB4 12
$define cANB5 13
$define cANB6 14
$define cANB7 15

$define cANC0 16
$define cANC1 17
$define cANC2 18
$define cANC3 19
$define cANC4 20
$define cANC5 21
$define cANC6 22
$define cANC7 23
'
' Create global variables for a demo here
'
    Dim Global_wVoltage As Word                                 ' Holds the integer voltage value

'-------------------------------------------------------------------------------------------------------------
' A meta-macro to set the voltage reading's ADC channel pin as analogue
'
$define Voltage_Pin_Analogue() '
    PinInput Voltage_Read_Pin  '
    Voltage_Read_ADC_SFR = 1

'-------------------------------------------------------------------------------------------------------------
' A meta-macro to set the voltage reading's ADC channel pin as digital
'
$define Voltage_Pin_Digital() Voltage_Read_ADC_SFR = 0

'-------------------------------------------------------------------------------------------------------------
' The main program starts here
' Read a voltage from a resistor divider attached to an ADC pin and serially transmit the voltage value
'
Main:
    Setup()                                                     ' Setup the program's variables and peripherals

    Do                                                          ' Create a loop
        Global_wVoltage = Voltage_Read()                        ' Read the voltage from the ADC
        HRsout1Ln "Voltage = ", Dec Global_wVoltage / 10, ".",  ' \
                                Dec1 Global_wVoltage // 10, "V" ' / Transmit the voltage in ASCII to a serial terminal

        DelayMS 512                                             ' Delay so the values can be seen
    Loop                                                        ' Do it forever

'-------------------------------------------------------------------------------------------------------------
' Setup the program's variables and peripherals
' Input     : None
' Output    : None
' Notes     : None
'
Proc Setup()
    Oscillator_64MHz()                                          ' Set the device to operate with its internal oscillator at 64MHz
    ADC_Setup_Basic()                                           ' Setup the device's ADC peripheral
    Voltage_Pin_Analogue()                                      ' Make the voltage reading ADC pin an analogue input
EndProc

'-------------------------------------------------------------------------------------------------------------
' Read the ADC and convert its raw value into a voltage value
' Input     : None
' Output    : Returns the voltage as an integer, so a value of 102 for the voltage represents 10.2 volts
' Notes     : To display the voltage correctly, use a mechanism such as: HRsoutLn Dec VoltageResult / 10, ".", Dec1 VoltageResult // 10, "V"
'
Proc Voltage_Read(), Word
    Dim dVoltCalc  As Dword                                     ' Holds the 32-bit calculations to convert the ADC reading to a voltage value
    Dim wVoltage   As dVoltCalc.Word0                           ' Alias to hold the 16-bit calculations and the resulting voltage value
    Dim wADC_Value As dVoltCalc.Word1                           ' Alias to hold the raw ADC reading

    Symbol cVref         = 500000                               ' The +Vref voltage used by the ADC (multiplied by 100000)
    Symbol cADC_ResValue = 1024                                 ' The maximum value from the ADC's resolution
    Symbol cQuanta       = (cVref / cADC_ResValue)              ' The quantasisation value for the ADC's +Vref and the ADC's resolution value
    Symbol cR1           = 100000                               ' Resistance of R1 (in Ohms)
    Symbol cR2           = 10000                                ' Resistance of R2 (in Ohms)
    Symbol cRdivCalc     = ((cR2 * 100) / (cR1 + cR2))          ' A calculation for the resistor divider

    wADC_Value = ADC_Read10(cANA0)                              ' Read the ADC from channel AN0
    dVoltCalc = wADC_Value * cQuanta                            ' Convert the 10-bit ADC value into a 32-bit voltage value
    wVoltage = dVoltCalc / cRdivCalc                            ' Take the resistor divider into account for the final voltage calculation
    wVoltage = wVoltage / 100                                   ' Make the final voltage value a smaller integer value
    Result = wVoltage                                           ' Return the smaller voltage value
EndProc

'------------------------------------------------------------------------------------------------
' Get a 10-bit reading from the ADC on a PIC18F26K40 device
' Input     : pChan holds the ADC channel to read
' Output    : Returns the 10-bit ADC value. Also held in SFRs ADRESL and ADRESH
' Notes     : Sets the ADFM bit to right justified 10-bit operation
'
Proc ADC_Read10(pChan As WREG), ADC10_wADRES
Global Dim ADC10_wADRES As ADRESL.Word                          ' Create a global 16-bit SFR from SFRs ADRESL and ADRESH
    ADPCH = pChan                                               ' Load the channel into the relevant SFR
    ADCON0bits_ADFM = 1                                         ' Set the ADFM bit for right justified
    ADCON0bits_ADON = 1                                         ' Enable the ADC
    DelayUS 50                                                  ' A delay before sampling
    ADCON0bits_GO_DONE = 1                                      ' \
    Repeat : Until ADCON0bits_GO_DONE = 0                       ' / Start a sample and wait for it to finish
EndProc

'------------------------------------------------------------------------------
' Setup the ADC peripheral in standard mode on a PIC18F26K40 device
' Input     : None
' Output    : None
' Notes     : Set for 10-bit operation
'
Proc ADC_Setup_Basic()
    ADLTHL  = %00000000
    ADLTHH  = %00000000
    ADUTHL  = %00000000
    ADUTHH  = %00000000
    ADSTPTL = %00000000
    ADSTPTH = %00000000
    ADRPT   = %00000000
    ADPCH   = %00000000
    ADCAP   = %00000000
    ADCON0  = %10000100                                         ' Right justify for 10-bit operation
    ADCON1  = %00000000
    ADCON2  = %00000000                                         ' Setup for standard mode
    ADCON3  = %00000000
    ADSTAT  = %00000000
    ADREF   = %00000000                                         ' ADNREF VSS, ADPREF VDD
    ADACT   = %00000000
    ADCLK   = %00001111                                         ' fOSC/32 (FOSC / (2 * (n + 1)))
    ADACQ   = %00000000
EndProc

'------------------------------------------------------------------------------------------------
' Set the PIC18F26K40 device to internal 64MHz operation with an HFINTOSC_1MHZ config fuse
' Input     : None
' Output    : None
' Notes     : None
'
Proc Oscillator_64MHz()
    OSCCON1 = %01100000
    OSCCON3 = %00000000
    OSCEN   = %00000000
    OSCFRQ  = %00001000                                         ' 64MHz operation
    OSCTUNE = %00000000
    DelayMS 100
EndProc

'------------------------------------------------------------------------------------------------
' Setup the fuses to use the internal oscillator on a PIC18F26K40
'
Config_Start
    RSTOSC = HFINTOSC_1MHZ                                      ' With HFFRQ = 4MHz and CDIV = 4:1
    FEXTOSC = Off                                               ' External Oscillator not enabled
    WDTE = Off                                                  ' WDT disabled
    CLKOUTEN = Off                                              ' CLKOUT function is disabled
    CSWEN = On                                                  ' Writing to NOSC and NDIV is allowed
    FCMEN = Off                                                 ' Fail-Safe Clock Monitor disabled
    MCLRE = EXTMCLR                                             ' MCLR pin is MCLR
    PWRTE = On                                                  ' Power up timer enabled
    LPBOREN = off                                               ' LPBOREN disabled
    BOREN = Off                                                 ' Brown-out turned off
    BORV = VBOR_285                                             ' Brown-out reset voltage set to 2.85V
    ZCD = Off                                                   ' ZCD disabled. ZCD can be enabled by setting the ZCDSEN bit of ZCDCON
    PPS1WAY = Off                                               ' PPSLOCK bit can be set and cleared repeatedly (subject to the unlock sequence)
    STVREN = Off                                                ' Stack full/underflow will not cause Reset
    Debug = Off                                                 ' Background debugger disabled
    XINST = Off                                                 ' Extended Instruction Set and Indexed Addressing Mode disabled
    SCANE = Off                                                 ' Scanner module is Not available for use. SCANMD bit is ignored
    LVP = Off                                                   ' Low Voltage programming disabled
    WDTCPS = WDTCPS_16                                          ' Watchdog Divider ratio 1:2091752 (64 seconds)
    WDTCWS = WDTCWS_7                                           ' Window always open (100%). Software control. Keyed access not required
    WDTCCS = LFINTOSC                                           ' WDT reference clock is the 31.2kHz HFINTOSC output
    WRT0 = Off                                                  ' Block 0 (000800-001FFF) not write-protected
    WRT1 = Off                                                  ' Block 1 (002000-003FFF) not write-protected
    WRTC = Off                                                  ' Configuration registers (300000-30000Bh) not write-protected
    WRTB = Off                                                  ' Boot Block (000000-0007FF) write-protected
    WRTD = Off                                                  ' Data EEPROM not write-protected
    Cp = Off                                                    ' User NVM code protection disabled
    CPD = Off                                                   ' Data NVM code protection disabled
    EBTR0 = Off                                                 ' Block 0 (000800-001FFF) not protected from table reads executed in other blocks
    EBTR1 = Off                                                 ' Block 1 (002000-003FFF) not protected from table reads executed in other blocks
    EBTRB = Off                                                 ' Boot Block (000000-0007FF) not protected from table reads executed in other blocks
Config_End

One note about the resistor divider's operation that is important is that the ADC operates better if the resistor; R2 is around 10K to 47K in value. This is so it better matches the ADC's requirement and allows its internal capacitor to charge discharge with more efficiency. It will work with higher value resistors, but it may not give stable readings.

Below is a screenshot of the above program working in the proteus simulator. The source code files and proteus 8.16 project files are also attached below:

Voltage_Reader_18F26K40.jpg