Example request: DMA ping-pong
Would be nice with an example application of DMA ping-pong (in which the DMA alternates between two buffers so that one can be written to while the other is being processed).
I'm implementing exactly this in a ringbuffer in https://github.com/biemster/pico-I2S, but it is not working yet.
@jonathangjertsen , could you provide a little more detail on what you'd like to see in an example? For example, where does the data come from that's being scattered to the two buffers? Does completion of the processing trigger the next DMA transfer?
@ajf58
A typical example is continuously capturing ADC data at very high speed without any gaps. There is an example of using the DMA and ADC together, but it's just a one-shot.
I like this figure from TI's various processor datasheets (in this case taken from the TM4C1294NCPDT). I think it uses some generic ARM component for the DMA which may or may not be similar to the one used by the Pico
Thanks.
One approach to alternating between the two ping and pong buffers is to reconfigure the DMA channel from the IRQ handler once one buffer is full. For example, modifying the example in dma/channel_irq/channel_irq.c, we can can demonstrate ping-ponging between two buffers, allowing the on chip temperature to be oversampled and averaged. N.b. I've not dug into the RP2040 datasheet to determine whether this oversampling and averaging would have the desired effect of improving the resolution in the temperature reading, but it's a useful backdrop for the problem.
#include <stdio.h>
#include <stdint.h>
#include "pico/stdlib.h"
#include "hardware/adc.h"
#include "hardware/dma.h"
#include "hardware/irq.h"
#include "hardware/timer.h"
#define CAPTURE_DEPTH 256
// Replace capture_buf[CAPTURE_DEPTH] with a ping and pong register
uint16_t ping[CAPTURE_DEPTH];
uint16_t pong[CAPTURE_DEPTH];
bool write_ping = true;
int dma_chan;
// Written to in the IRQ context, read from the main loop.
volatile uint32_t sum = 0;
void dma_handler() {
// Clear the interrupt request.
dma_hw->ints0 = 1u << dma_chan;
void *write_addr;
uint16_t *read_addr;
if (write_ping) {
// Ping was being written to, and has completed. Now read from ping, and write to pong.
write_ping = false;
read_addr = ping;
write_addr = pong;
} else {
write_ping = true;
read_addr = pong;
write_addr = ping;
}
// Kick off the next transfer.
dma_channel_set_write_addr(dma_chan, write_addr, true);
// Process what's in the read buffer.
uint32_t temp = 0;
for (size_t i = 0; i < CAPTURE_DEPTH; i++) {
// Mask the bottom 12 bits.
temp += read_addr[i] & 0xFFF;
}
// Oversampled, so divide down.
temp /= CAPTURE_DEPTH;
// Update.
sum = temp;
}
int main()
{
stdio_init_all();
#ifdef PICO_DEFAULT_LED_PIN
gpio_init(PICO_DEFAULT_LED_PIN);
gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
#endif
/* Initialize hardware AD converter, enable onboard temperature sensor and
* select its channel (do this once for efficiency, but beware that this
* is a global operation). */
adc_init();
adc_set_temp_sensor_enabled(true);
adc_select_input(4);
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,
false // Don't shift, masking values is handled in dma_handler()'s processing of the read buffer.
);
adc_set_clkdiv(0);
sleep_ms(1000);
printf("Arming DMA\n");
// Set up the DMA to start transferring data as soon as it appears in FIFO
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 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);
dma_channel_configure(dma_chan, &cfg,
ping, // dst
&adc_hw->fifo, // src
CAPTURE_DEPTH, // transfer count
false // do not start, wait for dma_handler() to be called
);
printf("Starting capture\n");
adc_run(true);
// Tell the DMA to raise IRQ line 0 when the channel finishes a block
dma_channel_set_irq0_enabled(dma_chan, true);
// Configure the processor to run dma_handler() when DMA IRQ 0 is asserted
irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
irq_set_enabled(DMA_IRQ_0, true);
// Manually call the handler once, to trigger the first transfer
dma_handler();
float old_temp = 0;
while (true) {
/* 12-bit conversion, assume max value == ADC_VREF == 3.3 V */
const float conversionFactor = 3.3f / (1 << 12);
float adc = (float) sum * conversionFactor;
float tempC = 27.0f - (adc - 0.706f) / 0.001721f;
if (old_temp != tempC) {
printf("Onboard temperature = %.03f %c\n", tempC, TEMPERATURE_UNITS);
}
old_temp = tempC;
#ifdef PICO_DEFAULT_LED_PIN
gpio_put(PICO_DEFAULT_LED_PIN, 1);
sleep_ms(10);
gpio_put(PICO_DEFAULT_LED_PIN, 0);
#endif
sleep_ms(490);
}
return 0;
}
Some example output (yup, reading the first uninitialized data gives a nonsense value):
Arming DMA
Starting capture
Onboard temperature = 437.227 C
Onboard temperature = 14.499 C
Onboard temperature = 14.967 C
Onboard temperature = 15.435 C
There's a good example of DMA ping-pong writing to two buffers from ADC in https://github.com/zapta/simple_stepper_motor_analyzer/blob/main/platformio/src/acquisition/adc_dma.cpp
There's a messier examlple in https://github.com/kevinjwalters/galactic-bluetooth-audio/blob/spectrogram-tuner-dmaadc/uad/audiosourcedmaadc.cpp