Multi input ADC including in an interrupt prog structure question

Started by TimB, Feb 03, 2022, 09:22 PM

Previous topic - Next topic

TimB


Hi...

I'm trying to figure out a system for some code I'm writing.

This is my requirement
Sample an ADC channel every 1000hz
Sample 2 ADC channels at 100hz
Sample 1 ADC channel at say 4 hz

Now the 100hz and 4 hz are fine as they will be in normal code. The 1000hz is an issue though. In another prog in the interrupt routine I just set the conversion going and left the interrupt, At the start of the next interrupt I read the result, restart it etc.

Now the issue is that in my main code I want to read the ADC. It will of cause mean the 1000hz adc result to be effected.
The best system I can come up with is to do the following in a ADC read sub in the main code

1 Check if the go/done bit is set, if not wait.
2 Copy the content of the adc result regs to a var and set a flag to tell the interrupt the data it needs is in the variable. It reads the result from there and clears the flag
3 Perform the ADC conversion I require


To prevent the interrupt from interrupting this process I will disable interrupts before this routine and re-enable at the end.

Can people think of a better way?

Thanks Tim

top204

You may be able to use a Compare Special Event interrupt, which can also automatically carry out an ADC sampling when the interrupt happens.

A simple program for a Compare Special Event interrupt is shown in the sample program "C:\Users\User Name\PDS\Samples\New Samples\Compare Special Event Interrupt.bas"

shantanu@india

1000 conversions per second is steep.. do you get stable readings? I would have used external dedicated ADC
Regards
Shantanu

TimB


Some more info

The 1000hz sampling is looking at an optical sensor it is a yes/no operation, Is signal below threshold yes or no, no real accurace required.
This a programming operation not a design of hardware. Remember that...

The issue is not the timing of the adc reading. There is already a 1000hz interrupt. The issue is that perfuming the ADC read is time consuming eg 20us + and I really do not want to waste my time doing the conversion in the interrupt. My int code is around 1us long and do not want to make it 20 longer.
The other issue is that performing the conversion will hijack the ADC read of another being performed.

I think the solution I came up with is probably the best I can come up with.

BTE the device is a pic18f25k80 and is 12 bit.

shantanu@india

If you have a @1msec(1000usec) interrupt routine that lasts for only 1usec then there's nothing to worry. Start your respective ADC conversions @ 1 msec, 100 msec and 250 msec and your ADC values would be nicely readable after 20 usecs in the main routine just by checking the go/ done bit. Why are you waiting for the go/done bit to change state? If your main routine is not very long then there's no point waiting and you can let the program flow freely. You can identify which ADC value is ready by reading one of the three flags that are set in the interrupt routine and resetting it after the read operation is over.
I don't know your hardware so you are the best judge..
Regards
Shantanu

SCV

Not sure which device you intend to use, but let the ADC run continuously and pick off the latest result in the ISR.

Yasin

Quote from: TimB on Feb 04, 2022, 08:20 AMIs signal below threshold yes or no, no real accurace required.
This a programming operation not a design of hardware. Remember that...

Using the mcu's comparator may be the solution. However, if an analog value is desired, the measured signal can be connected to another analog pin and the value can be read more slowly. Just an idea. I hope I explained correctly. My English is weak.


shantanu@india

Yasin's idea is good..comparison is faster than measuring
Regards
Shantanu

tumbleweed

Quote from: Yasin on Feb 04, 2022, 11:20 AMUsing the mcu's comparator may be the solution. However, if an analog value is desired, the measured signal can be connected to another analog pin and the value can be read more slowly.

You can still use the ADC to read a comparator's input pin voltage, so you don't need a second connection.
If it's just a simple threshold value, perhaps using the comparator CVref DAC would suffice? That gives you 32 steps, if that's close enough.

Stephen Moss

You might want to take a look at the 18F2431, it can sample two inputs simultaneously, but only convert them sequentially as there is only 1 ADC, has a 4 word buffer for holding ADC results and can do simultaneous or sequential sampling and you can select which sample triggers the interrupt.
Additionally, if I recall there is a auto conversion where it will continuously and automatically take sample after sample, obviously that makes the ADC a little trickier to use than the standard ADC, but between the auto sampling loop, the 4 word result buffer and interrupt selection that may enable you to mitigate some of the problems of slowing the 1000Hz sampling as you could read four results at a time. 

I had a project in mind for it but never actually got around to it so do not have any code for using the ADC on it.

As Yasin indicated if the 1000Hz sample is just a Yes/No then a comparator may be the solution and thereby remove a lot of ADC overhead as it could be sampled on a 10mS timer interrupt, maybe keep a count of your interrupts and every 1 kick of an ADC sample, if using the device above just sample both the 100Hz & 4Hz inputs simultaneously converting the 100Hz sample every time but only convert both the 100Hz & the 4Hz every 25 interrupts (250mS)

TimB

Quote from: Stephen Moss on Feb 04, 2022, 01:22 PMYou might want to take a look at the 18F2431,

"This a programming operation not a design of hardware. Remember that..."


top204

Try the program below Tim. It will read AN0 every 1000Hz, AN1 every 100Hz, and AN2 every 4Hz, using a Special Eveent interrupt that occurs every 1 ms.

To fine tune it, you can change the values of the counters within the interrupt handler. Or make the interrupt occur faster so the counters can be tweaked even further, including a counter for the 1000Hz reading. The problem with having only one shared ADC peripheral, is that each reading must be sequential, and that can cause some time lags. There are devices that allow multiple readings at one time, so each interrupt could set them to read all three channels (if possible), and only load a global variable with an ADC reading if the time is OK for it, based upon interrupt iterations.

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Create a Compare Interrupt using the Special Event mechanism
' Compares the 16-bit value of Timer3 with the contents of 16-bit registers CCPR2L\H
' Triggers the interrupt when Timer3 reaches the value held in CCPR2L\H
'
' Within the interrupt, it reads ADC channels AN0 every 1000Hz, AN1 every 100Hz and AN2 every 4Hz
'
' Written by Les Johnson for the Positron8 BASIC compiler.
'
    Device = 18F25K20
    Declare Xtal = 64
    On_Hardware_Interrupt GoTo ISR_Handler          ' Point the interrupt to its handler routine
'
' Setup USART1
'   
    Declare Hserial_Baud = 115200                   ' Set the Baud rate for USART1
    Declare HRSOut_Pin = PORTC.6                    ' Set the TX pin

    Include "Amicus18_Timers.inc"                   ' Include the Amicus18 Timer routines into the program
    Include "Amicus18_ADC.inc"                      ' Load the Amicus18 ADC routines into the program  
'
' Create some variables
'   
    Dim ADC_t1KHz_Triggered As Bit                  ' Set true when a 1000Hz ADC reading has occured (must be reset in the main program)
    Dim ADC_t100Hz_Triggered As Bit                 ' Set true when a 100Hz ADC reading has occured (must be Reset in the Main program)
    Dim ADC_t4Hz_Triggered As Bit                   ' Set true when a 4Hz ADC reading has occured (must be Reset in the Main program)
   
    Dim ADC_w100Hz_Counter As Word Access           ' Counts the ms until 100Hz is reached
    Dim ADC_w4Hz_Counter As Word Access             ' Counts the ms until 4Hz is reached
    
    Dim ADC_w1KHz_Result As Word Access             ' Holds the result of the 1000Hz ADC read
    Dim ADC_w100Hz_Result As Word Access            ' Holds the result of the 100Hz ADC read
    Dim ADC_w4Hz_Result As Word Access              ' Holds the result of the 4Hz ADC read
   
    Dim wCCPR2 As CCPR2L.Word                       ' Combine CCPR2L\H into a 16-bit register
    Dim wADRES As ADRESL.Word                       ' Combine ADRESL\H into a 16-bit register

$ifndef True
    $define True 1
$endif
$ifndef False
    $define False 0
$endif  
 
'--------------------------------------------------------------------------------------------------
' The main program loop starts here
' Read 3 ADC channels from a Special Event Interrupt
' And transmit the values to a serial terminal
'
Main:
    Setup()                                         ' Setup the program
   
    Do                                              ' Create an endless loop
        If ADC_t1KHz_Triggered = True Then          ' Has a 1000Hz ADC reading occured on AN0?
            HRSOutLn "1KHz=", Dec ADC_w1KHz_Result  ' Yes. So transmit its value to a serial terminal
            ADC_t1KHz_Triggered = False             ' Reset its flag so another ADC reading will occur
        EndIf
       
        If ADC_t100Hz_Triggered = True Then         ' Has a 100Hz ADC reading occured on AN1?
            HRSOutLn "100Hz=", Dec ADC_w100Hz_Result' Yes. So transmit its value to a serial terminal
            ADC_t100Hz_Triggered = False            ' Reset its flag so another ADC reading will occur
        EndIf
       
        If ADC_t4Hz_Triggered = True Then           ' Has a 4Hz ADC reading occured on AN2?
           HRSOutLn "4Hz=", Dec ADC_w4Hz_Result     ' Yes. So transmit its value to a serial terminal
           ADC_t4Hz_Triggered = False               ' Reset its flag so another ADC reading will occur
        EndIf
        DelayMS 100                                 ' Create a delay so the values can be seen more clearly on the serial terminal
    Loop                                            ' Do it forever

'--------------------------------------------------------------------------------------------------
' Setup the program
' Input     : None
' Output    : None
' Notes     : None
'
Proc Setup()
'
' Open the ADC
'
    OpenADC(ADC_FOSC_RC & ADC_RIGHT_JUST & ADC_2_TAD, ADC_REF_VDD_VSS, ADC_3ANA)
   
    CCP2CON = %00001011                             ' Compare mode: trigger special event, reset timer
'
' Open Timer3
'
    OpenTimer3(TIMER_INT_OFF & T3_16BIT_RW & T3_PS_1_8 & T3_SYNC_EXT_OFF & T3_SOURCE_INT & T3_SOURCE_CCP)
    $define cPrescalerValue 8                       ' Alter this to match the prescaler parameter above. i.e. 4 for T3_PS_1_4, 8 for T3_PS_1_8
    $define cMicroSeconds 1000                      ' Interrupt rate (in uS)
'
' Calculate the value to place into the CCPRx register in order to achieve a certain interrupt rate (in us)
'
    $define cCCPR_Value $eval (cMicroSeconds / cPrescalerValue) * (_xtal / 4)

$if cCCPR_Value > 65535
    $error "Value too large for interrupt duration"
$elseif cCCPR_Value = 0
    $error "Value too small for interrupt duration"
$endif

    wCCPR2 = cCCPR_Value                            ' Load CCPR2L\H with the value to trigger an interrupt at a certain duration
    PIE2bits_CCP2IE = 1                             ' Enable the Special Event Interrupt on CCP2
    INTCONbits_PEIE = 1                             ' Enable Peripheral Interrupts
    INTCONbits_GIE = 1                              ' Enable Global Interrupts

    ADC_w4Hz_Counter = 0                            ' Reset the Counter for the ms for the 4Hz ADC reading
    ADC_w100Hz_Counter = 0                          ' Reset the Counter for the ms for the 100Hz ADC reading
    ADC_t4Hz_Triggered = False                      ' Reset the ADC read flag for the 4Hz reading
    ADC_t100Hz_Triggered = False                    ' Reset the ADC read flag for the 100Hz reading
    ADC_t1KHz_Triggered = False                     ' Reset the ADC read flag for the 1000Hz reading
EndProc

'--------------------------------------------------------------------------------------------------
' Special Event Interrupt Handler
' Input     : None
' Output    : Reads ADC channel AN0 every 1ms (1000Hz) into variable ADC_w1KHz_Result
'           : Reads ADC channel AN1 every 10ms (100Hz) into variable ADC_w100Hz_Result
'           : Reads ADC channel AN2 every 250ms (4Hz) into variable ADC_w4Hz_Result
'           : Sets bit ADC_t1KHz_Triggered when a 1000Hz ADC reading has taken place
'           : Sets bit ADC_t100Hz_Triggered when a 100Hz ADC reading has taken place
'           : Sets bit ADC_t4Hz_Triggered when a 4Hz ADC reading has taken place
' Notes     : Triggers every 1ms
'
ISR_Handler:
    Context Save

    If PIR2bits_CCP2IF = 1 Then                     ' Was it a Compare Special Event on CCP2 that triggered the interrupt?          
        Inc ADC_w100Hz_Counter                      ' Yes. So increment the 100Hz counter
        Inc ADC_w4Hz_Counter                        ' Increment the 4Hz counter
       
        If ADC_t1KHz_Triggered = False Then         ' Is the flag false?
            ADCON0 = %00000011                      ' \
            Repeat : Until ADCON0bits_Go = 0        ' | Yes. So enable the ADC and read AN0 into variable ADC_w1KHz_Result
            ADC_w1KHz_Result = wADRES               ' /
            ADC_t1KHz_Triggered = True              ' Set its trigger flag (must be reset in the main program)
        EndIf
       
        If ADC_w100Hz_Counter = 10 Then             ' Has 10 interrupt iterations happened (100Hz)?
            If ADC_t100Hz_Triggered = False Then    ' Yes. So is its flag false?
                ADCON0 = %00000111                  ' \
                Repeat : Until ADCON0bits_Go = 0    ' | Yes. So Enable the ADC and read AN1 into variable ADC_w100Hz_Result
                ADC_w100Hz_Result = wADRES          ' /      
                ADC_t100Hz_Triggered = True         ' Set its trigger flag (must be Reset in the main program)
            EndIf
            ADC_w100Hz_Counter = 0                  ' Reset the counter      
        EndIf
       
        If ADC_w4Hz_Counter = 250 Then              ' Has 250 interrupt iterations happened (4Hz)?
            If ADC_t4Hz_Triggered = False Then      ' Yes. So is its flag false?
                ADCON0 = %00001011                  ' \
                Repeat : Until ADCON0bits_Go = 0    ' | Yes. So Enable the ADC and read AN2 into variable ADC_w4Hz_Result
                ADC_w4Hz_Result = wADRES            ' /          
                ADC_t4Hz_Triggered = True           ' Set its trigger flag (must be Reset in the main program)
            EndIf
            ADC_w4Hz_Counter = 0                    ' Reset the counter          
        EndIf
       
        PIR2bits_CCP2IF = 0                         ' Clear the interrupt flag
    EndIf
'
' << More Interrupt Conditions and Code Here >>
'
    Context Restore                                 ' Exit the interrupt
   
'--------------------------------------------------------------------------------------------------
' Setup the fuses for the 4xPLL
'
Config_Start
    FOSC = HSPLL        ' HS oscillator, PLL enabled and under software control
    Debug = Off         ' Background debugger disabled' RB6 and RB7 configured as general purpose I/O pins
    XINST = Off         ' Instruction set extension and Indexed Addressing mode disabled (Legacy mode)
    STVREN = Off        ' Reset on stack overflow/underflow disabled
    WDTEN = Off         ' WDT disabled (control is placed on SWDTEN bit)
    FCMEN = Off         ' Fail-Safe Clock Monitor disabled
    IESO = Off          ' Two-Speed Start-up disabled
    WDTPS = 128         ' Watchdog is 1:128
    BOREN = Off         ' Brown-out Reset disabled in hardware and software
    BORV = 18           ' VBOR set to 1.8 V nominal
    MCLRE = On          ' MCLR pin enabled, RE3 input pin disabled
    HFOFST = Off        ' The system clock is held Off until the HF-INTOSC is stable.
    LPT1OSC = Off       ' T1 operates in standard power mode
    PBADEN = Off        ' PORTB<4:0> pins are configured as digital I/O on Reset
    CCP2MX = PORTC      ' CCP2 input/output is multiplexed with RC1
    LVP = Off           ' Single-Supply ICSP disabled
    Cp0 = Off           ' Block 0 (000800-001FFFh) not code-protected
    CP1 = Off           ' Block 1 (002000-003FFFh) not code-protected
    CPB = Off           ' Boot block (000000-0007FFh) not code-protected
    CPD = Off           ' Data eeprom not code-protected
    WRT0 = Off          ' Block 0 (000800-001FFFh) not write-protected
    WRT1 = Off          ' Block 1 (002000-003FFFh) not write-protected
    WRTB = Off          ' Boot block (000000-0007FFh) not write-protected
    WRTC = Off          ' Configuration registers (300000-3000FFh) not write-protected
    WRTD = Off          ' Data eeprom not write-protected
    EBTR0 = Off         ' Block 0 (000800-001FFFh) not protected from table reads executed in other blocks
    EBTR1 = Off         ' Block 1 (002000-003FFFh) not protected from table reads executed in other blocks
    EBTRB = Off         ' Boot block (000000-0007FFh) not protected from table reads executed in other blocks
Config_End

It's written for the PIC18F25K20 device so I could simulate it, but it can be adapted to any 18F device if it is suitable for you.

TimB



Thanks Les,

That was a lot of work and really appreciated.  I did manage it using the system I proposed before
The 2 main routines are below

The only issue I can see is that the interrupt may be delayed when I turn of interrupts but the way the tmr reg is reloaded it takes that into account


; This is in the interrupt routine running at 1000hz

;--------------------------------------
;      Bubble detect routine
;--------------------------------------

        If bLastADCChan = cBubbleSensor then                                       ; Was a conversion done outside of this routine?
            BubbleSensorResult = wADCREG                                           ; No so just grab the last result from the ADC reg
        Else
            BubbleSensorResult = wLastADC
        Endif                                                                      ; Yes so read the result as seen by the other code

        ADCON0 = cConfigNstartBubble                                               ; Start the ADC conversion for the bubble sensor

        bLastADCChan = cBubbleSensor                                               ; Indicate that this ADC conversion id for the bubble detector

;----------------------------------------


; This is the routine to read the ADC in the main code
Proc ReadADC(pChannel as byte),word

    InterruptsOFF                                                         ; Turn of interrupts

    If bLastADCChan = cBubbleSensor then                                  ; If the last ADC read was the bubble then
        While ADCON0bits_GO  = cTrue                                      ; Wait for it to finish
        Wend
        wLastADC = wADCREG                                                ; Store the result
    Endif

    bLastADCChan = pChannel                                               ; Indicate what the next read is for (basically not the bubble)

    ADCON0 = (pChannel << 2)| $03

    While ADCON0bits_GO  = cTrue
    Wend

    Result = wADCREG

    InterruptsOn
Endproc