Search code examples
adcraspberry-pi-pico

How to use external voltage reference on RPi Pico VREF pin


I want to use ADAFRUIT LM4040 module as External Voltage Reference with Pico VREF pin

With Arduino boards I have to use 'analogReference' command to specify that I'm using an EXTERNAL voltage reference. something like this:

'''

float ref_voltage = 4.096;

void setup() {
  analogReference(EXTERNAL);
}

void loop(){
   adc_value = analogRead(ANALOG_IN_PIN);
   
   adc_voltage  = (adc_value * ref_voltage) / 1024.0; 
}

'''

How can I do this with pico? I've read all articles, pico datasheet, etc. and all they say is you CAN use External Voltage Reference on VREF pin, but HOW?

The 'analogReference' command does exist in 'Common.h' file for both Arduino Mbed OS RP2040 Boards and Raspberry Pi Pico / RP2040 by Earle F. Philhower, III. but they don't compile

I'd prefer to use arduino IDE or C++SDK because the next step, I'm going to try pico w bluetooth which at the moment only exist in these two platforms.

Thanks


Solution

  • On the Raspberry Pi Pico, you cannot use an external ADC reference voltage higher than 3.0v (not 3.3V and not 4.096V). See the RaspberryPi Pico DataSheet - 4.3. Using the ADC:

    For much improved ADC performance, an external 3.0V shunt reference, such as LM4040, can be connected from the ADC_VREF pin to ground. Note that if doing this the ADC range is limited to 0-3.0V signals (rather than 0-3.3V), and the shunt reference will draw continuous current through the 200Ω filter resistor (3.3V-3.0V)/200 = ~1.5mA.

    "How Do I Do It?"

    The easiest way I have found to use ADC with either the internal or external reference (hooked up as specified in the datasheet). Then simply to connect your external reference and measure the actual voltage at the VREF pin. Use that measured value as your actual VREF value.

    Then it is a simple matter of initializing ADC and then performing an adc_read() on your selected ADC input and performing the conversion for whatever you are using it for. A short snippet of how I have done it reading a joystick type potentiometer for DC motor-control is similar to:

    #include <stdio.h>
    #include <stdlib.h>
    #include "pico/stdlib.h"
    #include "hardware/adc.h"
    #include "hardware/pwm.h"
    ...
    #define ADC0_PIN    26    /* ADC0 is GPIO26 */
    #define ADC1_PIN    27
    #define ADC2_PIN    28
    
    #define ADCSCALE  4080   /* for 25MHz 256 range (with good potentiometer) */
    #define ADCZERO     18   /* for 25MHz 256 range */
    #define ADCVREF   3.19f  /* actual voltage measured at pin (internal Ref) */
    ...
    static volatile uint16_t  adcval,
                              adclast,
                              pwmCCR0;    /* pwmCCR0 only global for test */
    
    enum { ADC0, ADC1, ADC2, ADC3, ADCTEMP };
    ...
    int main (void) {
    
      stdio_init_all();
    
      adc_init ();                /* initialize ADC */
    
      adc_gpio_init (ADC0_PIN);   /* init gpio 26 as input pin,  0.0-3.3V */
      adc_select_input (ADC0);    /* select input on ADC0 */
    
      ...
    
     while (1) {
        adcval = adc_read();    /* read ADC value each iteration
                                 * note: consider moving to ADC isr and using
                                 * ADC FIFO.
                                 */
    
        sleep_ms (50);          /* no need to read more frequently */
      }
    }
    

    For most purposes, I set an exclusive interrupt handler to actually update the value of whatever I'm controlling. In this case I simply wanted the propagate the ADC conversion value on each PWM wrap out to a motor controller to control a pair of DC motors. That involves writing the function you want called each time the PWM signal wraps back to zero (or use a repeating timer function, or whatever other repeating event you want to use to set the frequency of handling new ADC values)

    The snippet of how I accomplish that is to write the function to handle the ADC values -- do that however you need in your code. Here I did something similar to:

    /** exclusive PWM isr handler */
    void on_pwm_wrap()
    {
      uint32_t val = adcval;
    
      /* no need to process further if value hasn't changed */
      if (abs (adcval - adclast) < 5) {   /* using ~ 0.2% threshold */
        /* clear pwm interrupt flag */
        pwm_clear_irq (pwm_gpio_to_slice_num (PICO_DEFAULT_LED_PIN));
    
        return;
      }
    
      /* subtract ADCZERO or set to 0 */
      val = adcval > ADCZERO ? adcval - ADCZERO : 0;
    
      uint32_t rawval = (val * (PWMCCR + 1)) / ADCSCALE;      /* 32-bit scaledval*/
      uint16_t scaledval = rawval > PWMCCR ? PWMCCR : rawval; /* limit to 16-bit */
      ...
      /* Updating motor controller */
      ...
      /* save last adc value */
      adclast = adcval;
    }
    

    And then in main() all you do is set the function as the exclusive handler for whatever interrupt you want to use. Here it was:

      irq_set_exclusive_handler (PWM_IRQ_WRAP, on_pwm_wrap);
      irq_set_enabled (PWM_IRQ_WRAP, true);
    

    Worked like a champ. Let me know if you have further questions.