raspberrypi / pico-examples

BSD 3-Clause "New" or "Revised" License
2.64k stars 781 forks source link

Unclear how to use multiple ADC channels #190

Open davidedelvento opened 2 years ago

davidedelvento commented 2 years ago

According to https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__adc.html the Pico has 1 ADC with a 5 channel mux in front and one could use adc_set_round_robin() to alternatively read from each.

A comment in https://github.com/raspberrypi/pico-examples/blob/master/adc/dma_capture/dma_capture.c says

// This could be extended to use the ADC's round robin feature to sample two
// channels concurrently at 0.25 Msps each.

But does not provide any clue on how to do that correctly. For example, is it possible to have two separate DMA capture buffers, which would be ideal? If so how? The SDK documentation does not say and seems to imply it's indeed not possible. If it is not possible to have separate capture buffers, the data in there will be all mingled from the various channels, and separating it will require some time. This is a problem if one needs a continuous capture. In such a case, once the DMA finishes, it needs to be restarted as soon as possible, ideally immediately. Would be possible to use a double capture buffer, alternating between one and the other to avoid waiting for the draining and separation of the original one? If so, how? If not, are all of the SDK channel configuration calls extremely fast compared to the sampling rate to make it a moot point? If so, it'd need to be mention.

The examples do not show any of this. It's straightforward to do round robin in the hello_adc case in an infinite loop, like in the microphone_adc example. Why is the microphone_adc doing single conversions in a loop with sleep? For such a use case, a DMA approach seems the best way to proceed, to get a constant and consistent sample rate.... Or am I missing something and single conversions is the way to go? Round robin in single conversion mode would be trivial to implement.

aallan commented 2 years ago

Why is the microphone_adc doing single conversions in a loop with sleep?

Probably because it was written as an introductory generic example to show how to use the ADC.

davidedelvento commented 2 years ago

Why is the microphone_adc doing single conversions in a loop with sleep?

Probably because it was written as an introductory generic example to show how to use the ADC.

Thanks for the guess.

How about the many other (more relevant) questions I asked? No interest in having a decent example on how to use more than one ADC channel?

aallan commented 2 years ago

Thanks for the guess.

It wasn't a guess.

How about the many other (more relevant) questions I asked? No interest in having a decent example on how to use more than one ADC channel?

It's Christmas🎄🎁. The office is locked and closed, we're back at our desks starting from the 4th January.

davidedelvento commented 2 years ago

Thanks for the guess.

It wasn't a guess.

I said "guess" since you wrote "probably".

How about the many other (more relevant) questions I asked? No interest in having a decent example on how to use more than one ADC channel?

It's Christmas. The office is locked and closed, we're back at our desks starting from the 4th January.

Sounds great. Fine to wait. Happy Holidays!

redlegjed commented 2 years ago

I adapted the example to use the round robin ADC capture. The code is below if you are interested.

I put the ADC capturing in a loop, so that it can be run continuously. It just requires hitting a key after the captured data is printed out. Note: I'm using USB serial to printout, for reasons I don't understand the code will block at this line

while (!tud_cdc_connected()) { sleep_ms(100);  }

Connecting a serial comms program (Putty in my case) at this point will break out of the loop and move the program forward. I generally run the program in debug mode so I can single step through it.

#include <stdio.h>
#include "pico/stdlib.h"
// For ADC input:
#include "hardware/adc.h"
#include "hardware/dma.h"
// For resistor DAC output:
#include "pico/multicore.h"
#include "hardware/pio.h"
#include "resistor_dac.pio.h"
// For USB output
#include <tusb.h>

// This example uses the DMA to capture many samples from the ADC.
//
// - We are putting the ADC in free-running capture mode at 0.5 Msps
//
// - A DMA channel will be attached to the ADC sample FIFO
//
// - Configure the ADC to right-shift samples to 8 bits of significance, so we
//   can DMA into a byte buffer [Removed this so 12bit values can be used]
//
// This could be extended to use the ADC's round robin feature to sample two
// channels concurrently at 0.25 Msps each. [It has in this version]
//
// It would be nice to have some analog samples to measure! This example also
// drives waves out through a 5-bit resistor DAC, as found on the reference
// VGA board. If you have that board, you can take an M-F jumper wire from
// GPIO 26 to the Green pin on the VGA connector (top row, next-but-rightmost
// hole). Or you can ignore that part of the code and connect your own signal
// to the ADC input.

// Channel 0 is GPIO26
#define CAPTURE_CHANNEL 0
#define NUM_CHANNELS 3
#define CAPTURE_DEPTH 100

uint16_t capture_buf[CAPTURE_DEPTH];

void core1_main();

int main() {
    char chr;
    int ch,ch_mask=0;

    stdio_init_all();
    // Wait for USB
    // - seem to have to open putty to unblock this line (??)
    while (!tud_cdc_connected()) { sleep_ms(100);  }

    // Send core 1 off to start driving the "DAC" whilst we configure the ADC.
    multicore_launch_core1(core1_main);

    // Init GPIO for analogue use: hi-Z, no pulls, disable digital input buffer.
    // adc_gpio_init(26 + CAPTURE_CHANNEL);

    for (ch=0;ch<NUM_CHANNELS;ch++)
    {
        adc_gpio_init(26 + ch);
        ch_mask += 1 << ch;
    }

    adc_init();
    // adc_select_input(CAPTURE_CHANNEL); // Use for single channel measurement
    adc_set_round_robin(ch_mask); // Use for 2 channel measurement : ch 0 & 1 -> 0b0011 = 3
    adc_fifo_setup(
        true,    // Write each completed conversion to the sample FIFO
        true,    // Enable DMA data request (DREQ)
        1,       // DREQ (and IRQ) asserted when at least 1 sample present
        false,   // We won't see the ERR bit because of 8 bit reads; disable.
        false     // Shift each sample to 8 bits when pushing to FIFO [true] Changed to false for 12bit values
    );

    // Divisor of 0 -> full speed. Free-running capture with the divider is
    // equivalent to pressing the ADC_CS_START_ONCE button once per `div + 1`
    // cycles (div not necessarily an integer). Each conversion takes 96
    // cycles, so in general you want a divider of 0 (hold down the button
    // continuously) or > 95 (take samples less frequently than 96 cycle
    // intervals). This is all timed by the 48 MHz ADC clock.
    adc_set_clkdiv(0);

    printf("Arming DMA\n");
    // sleep_ms(100);
    // Set up the DMA to start transferring data as soon as it appears in FIFO
    uint dma_chan = dma_claim_unused_channel(true);
    dma_channel_config cfg = dma_channel_get_default_config(dma_chan);

    // Reading from constant address, writing to incrementing 2 byte addresses
    channel_config_set_transfer_data_size(&cfg, DMA_SIZE_16);
    channel_config_set_read_increment(&cfg, false);
    channel_config_set_write_increment(&cfg, true);

    // Pace transfers based on availability of ADC samples
    channel_config_set_dreq(&cfg, DREQ_ADC);

    while (true)
    {
        // Set starting ADC channel 
        adc_select_input(CAPTURE_CHANNEL);

        // Set/Reset DMA transfer
        dma_channel_configure(dma_chan, &cfg,
            capture_buf,    // dst
            &adc_hw->fifo,  // src
            CAPTURE_DEPTH,  // transfer count
            true            // start immediately
        );

        printf("Starting capture\n");
        adc_run(true);

        // Once DMA finishes, stop any new conversions from starting, and clean up
        // the FIFO in case the ADC was still mid-conversion.
        dma_channel_wait_for_finish_blocking(dma_chan);
        printf("Capture finished\n");
        adc_run(false);
        adc_fifo_drain();

        // Print samples to stdout so you can display them in pyplot, excel, matlab
        for (int i = 0; i < CAPTURE_DEPTH; ++i) {
            // printf("%-3d, ", capture_buf[i]);
            printf("%.3f, ", capture_buf[i]*3.3/4095);
            if (i % 10 == 9)
                printf("\n");
        }
        printf("End of data: hit a key\n");
        chr = getchar();
        printf("Finished\n");
    }
}

// ----------------------------------------------------------------------------
// Code for driving the "DAC" output for us to measure

// Core 1 is just going to sit and drive samples out continously. PIO provides
// consistent sample frequency.

#define OUTPUT_FREQ_KHZ 5
#define SAMPLE_WIDTH 5
// This is the green channel on the VGA board
#define DAC_PIN_BASE 0

void core1_main() {
    PIO pio = pio0;
    uint sm = pio_claim_unused_sm(pio0, true);
    uint offset = pio_add_program(pio0, &resistor_dac_5bit_program);
    resistor_dac_5bit_program_init(pio0, sm, offset,
        OUTPUT_FREQ_KHZ * 1000 * 2 * (1 << SAMPLE_WIDTH), DAC_PIN_BASE);
    while (true) {
        // Triangle wave
        for (int i = 0; i < (1 << SAMPLE_WIDTH); ++i)
            pio_sm_put_blocking(pio, sm, i);
        for (int i = 0; i < (1 << SAMPLE_WIDTH); ++i)
            pio_sm_put_blocking(pio, sm, (1 << SAMPLE_WIDTH) - 1 - i);
    }
}
alien999999999 commented 1 year ago

in the while loop, you are doing adc_select_input(CAPTURE_CHANNEL); won't that just disable the round robin? Anyone interested in having continuous round_robin IRQ drive example? also, does the dma block on DREQ, so it's not a problem to start it early? before the fifo is filled?

My problem is, my pico is doing other things, so i need all of this to work IRQ driven

redlegjed commented 1 year ago

I think adc_select_input(CAPTURE_CHANNEL); sets the starting ADC channel in the round robin. All ADC channels are read in one iteration of the loop.

From the Pico C SDK manual in the "hardware_adc" section:

Although there is only one ADC you can specify the input to it using the adc_select_input() function. In round robin mode (adc_rrobin()) will use that input and move to the next one after a read.

So at the end of one iteration the ADC input will be the last channel in the round robin, hence the need to reset at the start.

rmoh12 commented 1 month ago

I adapted the example to use the round robin ADC capture. The code is below if you are interested.

I put the ADC capturing in a loop, so that it can be run continuously. It just requires hitting a key after the captured data is printed out. Note: I'm using USB serial to printout, for reasons I don't understand the code will block at this line

while (!tud_cdc_connected()) { sleep_ms(100);  }

Connecting a serial comms program (Putty in my case) at this point will break out of the loop and move the program forward. I generally run the program in debug mode so I can single step through it.

#include <stdio.h>
#include "pico/stdlib.h"
// For ADC input:
#include "hardware/adc.h"
#include "hardware/dma.h"
// For resistor DAC output:
#include "pico/multicore.h"
#include "hardware/pio.h"
#include "resistor_dac.pio.h"
// For USB output
#include <tusb.h>

// This example uses the DMA to capture many samples from the ADC.
//
// - We are putting the ADC in free-running capture mode at 0.5 Msps
//
// - A DMA channel will be attached to the ADC sample FIFO
//
// - Configure the ADC to right-shift samples to 8 bits of significance, so we
//   can DMA into a byte buffer [Removed this so 12bit values can be used]
//
// This could be extended to use the ADC's round robin feature to sample two
// channels concurrently at 0.25 Msps each. [It has in this version]
//
// It would be nice to have some analog samples to measure! This example also
// drives waves out through a 5-bit resistor DAC, as found on the reference
// VGA board. If you have that board, you can take an M-F jumper wire from
// GPIO 26 to the Green pin on the VGA connector (top row, next-but-rightmost
// hole). Or you can ignore that part of the code and connect your own signal
// to the ADC input.

// Channel 0 is GPIO26
#define CAPTURE_CHANNEL 0
#define NUM_CHANNELS 3
#define CAPTURE_DEPTH 100

uint16_t capture_buf[CAPTURE_DEPTH];

void core1_main();

int main() {
    char chr;
    int ch,ch_mask=0;

    stdio_init_all();
    // Wait for USB
    // - seem to have to open putty to unblock this line (??)
    while (!tud_cdc_connected()) { sleep_ms(100);  }

    // Send core 1 off to start driving the "DAC" whilst we configure the ADC.
    multicore_launch_core1(core1_main);

    // Init GPIO for analogue use: hi-Z, no pulls, disable digital input buffer.
    // adc_gpio_init(26 + CAPTURE_CHANNEL);

    for (ch=0;ch<NUM_CHANNELS;ch++)
    {
        adc_gpio_init(26 + ch);
        ch_mask += 1 << ch;
    }

    adc_init();
    // adc_select_input(CAPTURE_CHANNEL); // Use for single channel measurement
    adc_set_round_robin(ch_mask); // Use for 2 channel measurement : ch 0 & 1 -> 0b0011 = 3
    adc_fifo_setup(
        true,    // Write each completed conversion to the sample FIFO
        true,    // Enable DMA data request (DREQ)
        1,       // DREQ (and IRQ) asserted when at least 1 sample present
        false,   // We won't see the ERR bit because of 8 bit reads; disable.
        false     // Shift each sample to 8 bits when pushing to FIFO [true] Changed to false for 12bit values
    );

    // Divisor of 0 -> full speed. Free-running capture with the divider is
    // equivalent to pressing the ADC_CS_START_ONCE button once per `div + 1`
    // cycles (div not necessarily an integer). Each conversion takes 96
    // cycles, so in general you want a divider of 0 (hold down the button
    // continuously) or > 95 (take samples less frequently than 96 cycle
    // intervals). This is all timed by the 48 MHz ADC clock.
    adc_set_clkdiv(0);

    printf("Arming DMA\n");
    // sleep_ms(100);
    // Set up the DMA to start transferring data as soon as it appears in FIFO
    uint dma_chan = dma_claim_unused_channel(true);
    dma_channel_config cfg = dma_channel_get_default_config(dma_chan);

    // Reading from constant address, writing to incrementing 2 byte addresses
    channel_config_set_transfer_data_size(&cfg, DMA_SIZE_16);
    channel_config_set_read_increment(&cfg, false);
    channel_config_set_write_increment(&cfg, true);

    // Pace transfers based on availability of ADC samples
    channel_config_set_dreq(&cfg, DREQ_ADC);

    while (true)
    {
        // Set starting ADC channel 
        adc_select_input(CAPTURE_CHANNEL);

        // Set/Reset DMA transfer
        dma_channel_configure(dma_chan, &cfg,
            capture_buf,    // dst
            &adc_hw->fifo,  // src
            CAPTURE_DEPTH,  // transfer count
            true            // start immediately
        );

        printf("Starting capture\n");
        adc_run(true);

        // Once DMA finishes, stop any new conversions from starting, and clean up
        // the FIFO in case the ADC was still mid-conversion.
        dma_channel_wait_for_finish_blocking(dma_chan);
        printf("Capture finished\n");
        adc_run(false);
        adc_fifo_drain();

        // Print samples to stdout so you can display them in pyplot, excel, matlab
        for (int i = 0; i < CAPTURE_DEPTH; ++i) {
            // printf("%-3d, ", capture_buf[i]);
            printf("%.3f, ", capture_buf[i]*3.3/4095);
            if (i % 10 == 9)
                printf("\n");
        }
        printf("End of data: hit a key\n");
        chr = getchar();
        printf("Finished\n");
    }
}

// ----------------------------------------------------------------------------
// Code for driving the "DAC" output for us to measure

// Core 1 is just going to sit and drive samples out continously. PIO provides
// consistent sample frequency.

#define OUTPUT_FREQ_KHZ 5
#define SAMPLE_WIDTH 5
// This is the green channel on the VGA board
#define DAC_PIN_BASE 0

void core1_main() {
    PIO pio = pio0;
    uint sm = pio_claim_unused_sm(pio0, true);
    uint offset = pio_add_program(pio0, &resistor_dac_5bit_program);
    resistor_dac_5bit_program_init(pio0, sm, offset,
        OUTPUT_FREQ_KHZ * 1000 * 2 * (1 << SAMPLE_WIDTH), DAC_PIN_BASE);
    while (true) {
        // Triangle wave
        for (int i = 0; i < (1 << SAMPLE_WIDTH); ++i)
            pio_sm_put_blocking(pio, sm, i);
        for (int i = 0; i < (1 << SAMPLE_WIDTH); ++i)
            pio_sm_put_blocking(pio, sm, (1 << SAMPLE_WIDTH) - 1 - i);
    }
}

hi , im new to github. is this code work for you ? or any one ? i checked this code on wokwi simulator and did not get a suitable answer. I also tried to change the main example code to read the two channels adc0 and adc1 of pins 26 and 27 and run it in the wokwi simulator. https://wokwi.com/projects/398859749474407425 According to the r2040 data sheet, I expected the two-channel input values ​​to appear one at a time in the FIFO and consequently in the buffer. But only adc0 information (pin26) is shown in the output. Can anyone help? Did I make a mistake in changing the code?

davidedelvento commented 1 month ago

What I got working is https://github.com/davidedelvento/Mybrid/tree/main/RaspberryPiPico which obviously is more than ADC alone, but not much more. Look at the pico-piano.c However I've now moved to something else and not using the R2040 anymore, so I can't be more helpful than this.

rmoh12 commented 1 month ago

What I got working is https://github.com/davidedelvento/Mybrid/tree/main/RaspberryPiPico which obviously is more than ADC alone, but not much more. Look at the pico-piano.c However I've now moved to something else and not using the R2040 anymore, so I can't be more helpful than this.

hi, thank you for your quick reply. I was curious to know what you are working on now and why you left pico? Anyway, I read your code, it's a funny and interesting method, I will definitely try it, but it has nothing to do with the method of reading multiple ADC channels with DMA. I am working on a two channel oscilloscope with pico. Of course, I made it with macropython before, but it is important that the reading time between two ADC channels is very short, so I had to work again with c language after a long time. For now, I stopped here and I don't know where the problem is. I was hoping someone here would know

lurch commented 1 month ago

i checked this code on wokwi simulator and did not get a suitable answer.

Wokwi simulator is a 3rd-party product, and so it may or may not behave the same as actual RP2040 hardware. If you're able to run your code on e.g. a Raspberry Pi Pico (and you might want to also try using a Raspberry Pi Debug Probe for debugging) we might be able to give you more help. You could also try asking on the forums which are much more active than GitHub.

redlegjed commented 1 month ago

I can confirm that the round robin code above does work on a real Pico.

I tested it with 3 ADCs capturing the 3 different voltages (from 4 x10k resistors in series). The results are shown below. The data shows the voltages from ADC0, ADC1, ADC2 being repeated over and over. There are clearly 3 different voltages indicating that the round robin action is working.

Starting capture
Capture finished
2.218, 1.127, 0.624, 2.208, 1.125, 0.652, 2.214, 1.127, 0.671, 2.208, 
1.126, 0.690, 2.206, 1.123, 0.704, 2.201, 1.123, 0.717, 2.198, 1.119, 
0.729, 2.194, 1.118, 0.738, 2.192, 1.118, 0.746, 2.188, 1.115, 0.754, 
2.185, 1.114, 0.759, 2.182, 1.111, 0.765, 2.179, 1.110, 0.770, 2.177, 
1.108, 0.774, 2.173, 1.109, 0.777, 2.171, 1.107, 0.780, 2.169, 1.106, 
0.782, 2.168, 1.105, 0.787, 2.183, 1.116, 0.804, 2.206, 1.125, 0.805, 
2.205, 1.123, 0.807, 2.215, 1.129, 0.810, 2.211, 1.125, 0.808, 2.207, 
1.125, 0.807, 2.202, 1.131, 0.812, 2.213, 1.128, 0.810, 2.209, 1.126, 
0.808, 2.206, 1.122, 0.807, 2.201, 1.122, 0.807, 2.198, 1.119, 0.806, 
2.194, 1.119, 0.804, 2.192, 1.117, 0.803, 2.188, 1.114, 0.803, 2.185, 
End of data: hit a key

I slightly modified the code to strip out the use of the 2nd core to generate a signal. The capture and the code are in this file adc_dma_capture.md

rmoh12 commented 1 month ago

Hi to everyone Thank you for your answers. Yes, you are right, unfortunately, the wokwi simulator does not work properly in this regard. I also tested this issue in the real environment and it works correctly. Thank you again.