DMA (Direct Memory Access) je mocný nástroj, který nám umožnuje manipulovat s pamětí nezávisle na hlavním procesoru. V tomto článku si ukážeme, jak velmi rychle měřit pomocí analogově digitálního převodníku ADC. A v tom nám pomůže právě DMA.
Tento příklad využívá DMA k načtení velkého množství vzorků z ADC.
ADC nastavujeme do režimu průběžného vzorkování (free-running) s rychlostí 0,5 Msps. ADC je řízen hodinami o frekvenci 48MHz a odečtení jedné hodnoty trvá 96 cyklů. (Hodiny Pico běží standardně na frekvenci 125 MHz). Takže skutečně můžeme měřit s frekvencí 500 KHz.
\(\frac{48000000}{96} = 500000 Hz\)
Kanál DMA bude propojen s FIFO pamětí vzorků ADC.
ADC nakonfigurujeme tak, aby vzorky posouval doprava s 8bitovou přesností; díky tomu můžeme data pomocí DMA ukládat do bajtového bufferu.
Příklad lze rozšířit využitím funkce „round-robin“ ADC, která umožňuje současné vzorkování dvou kanálů, přičemž každý z nich pracuje rychlostí 0,25 Msps.
Bylo by dobré mít k dispozici nějaké analogové vzorky, které lze měřit. Tento příklad proto také generuje trojúhelníkový průběh signálu pomocí 6bitového odporového DAC (převodníku), pomocí něhož budeme generovat signál, jež budeme měřit. Případně můžete tuto část kódu ignorovat a na vstup ADC přivést vlastní signál.

Operační zesilovač LM358 není nezbytný. Do obvodu je vložen jenom jako ukázka, jak je možné použít výstup s digitálně analogového odporového převodníku. Náš odporový DAC funguje poměrně dobře, jenom musíme mít na paměti, že ho lze zatížit jenom poměrně vysokou impedancí. Proto operační zesilovač v neinvertujícím zapojení, který tomu vyhovuje. Pokud byste někdy potřebovali vytvářet analogový signál z digitálního (třeba pro generování nějakého zvuku), je lepší použít specializovaný obvod, třeba Microchip MCP4802/4812/4822.

CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
project(adc_dma_capture C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
add_executable(adc_dma_capture
dma_capture.c
)
pico_generate_pio_header(adc_dma_capture ${CMAKE_CURRENT_LIST_DIR}/resistor_dac.pio)
target_link_libraries(adc_dma_capture
pico_stdlib
hardware_adc
hardware_dma
# For the dummy output:
hardware_pio
pico_multicore
)
pico_enable_stdio_usb(adc_dma_capture 1)
pico_enable_stdio_uart(adc_dma_capture 0)
# create map/bin/hex file etc.
pico_add_extra_outputs(adc_dma_capture)
dma_capture.c
/**
* Copyright (c) 2021 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdio.h>
#include <math.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"
// 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
//
// This could be extended to use the ADC's round robin feature to sample two
// channels concurrently at 0.25 Msps each.
//
// 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 CAPTURE_DEPTH 2000
#define ADC_VREF 3.3*2
#define ADC_RANGE (1<<8)
#define ADC_CONVERT (ADC_VREF / (ADC_RANGE - 1))
#define PI 3.1415926
uint8_t capture_buf[CAPTURE_DEPTH];
void core1_main();
int main() {
stdio_init_all();
sleep_ms(2000);
// printf("\n\nDMA capture\n---------------------------------------------\n\n");
// 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);
adc_init();
adc_select_input(CAPTURE_CHANNEL);
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.
true // Shift each sample to 8 bits when pushing to FIFO
);
// 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(1000);
// 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 byte addresses
channel_config_set_transfer_data_size(&cfg, DMA_SIZE_8);
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,
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("%.2f\n", capture_buf[i] * ADC_CONVERT);
sleep_ms(10);
// if (i % 10 == 9)
// printf("\n");
}
}
// ----------------------------------------------------------------------------
// Code for driving the "DAC" output for us to measure
// Core 1 is just going to sit and drive samples out continuously. 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);
// Sinus
// for(int i = 0; ; ++i) {
// int j = (sin(PI*i/360)+1)*(32);
// pio_sm_put_blocking(pio, sm, j);
// }
}
}
resistor_dac.pio
;
; Copyright (c) 2021 Raspberry Pi (Trading) Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;
.program resistor_dac_5bit
; Drive one of the 5-bit resistor DACs on the VGA reference board. (this isn't
; a good way to do VGA -- just want a nice sawtooth for the ADC example!)
out pins, 6
% c-sdk {
#include "hardware/clocks.h"
static inline void resistor_dac_5bit_program_init(PIO pio, uint sm, uint offset,
uint sample_rate_hz, uint pin_base) {
pio_sm_set_pins_with_mask(pio, sm, 0, 0x1fu << pin_base);
pio_sm_set_pindirs_with_mask(pio, sm, ~0u, 0x1fu << pin_base);
for (int i = 0; i < 6; ++i)
pio_gpio_init(pio, pin_base + i);
pio_sm_config c = resistor_dac_5bit_program_get_default_config(offset);
sm_config_set_out_pins(&c, pin_base, 6);
// Shift to right, autopull threshold 5
sm_config_set_out_shift(&c, true, true, 6);
// Deeper FIFO as we're not doing any RX
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
float div = (float)clock_get_hz(clk_sys) / sample_rate_hz;
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}
Program tiskne na sériovou linku naměřené hodnoty z ADC, můžeme si je zobrazit graficky.
Program pro vykreslení grafu z naměřených hodnot
plotter.py
#!/usr/bin/env python3
# Grabs raw data from the Pico's UART and plots it as received
# Install dependencies:
# python3 -m pip install pyserial matplotlib
# Usage: python3 plotter <port>
# eg. python3 plotter /dev/ttyACM0
# see matplotlib animation API for more: https://matplotlib.org/stable/api/animation_api.html
import serial
import sys
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.lines import Line2D
# disable toolbar
plt.rcParams['toolbar'] = 'None'
class Plotter:
def __init__(self, ax):
self.ax = ax
self.maxt = 2000
self.tdata = [0]
self.ydata = [3.3/2]
self.line = Line2D(self.tdata, self.ydata)
self.ax.add_line(self.line)
self.ax.set_ylim(0, 3.3)
self.ax.set_xlim(0, self.maxt)
def update(self, y):
lastt = self.tdata[-1]
if lastt - self.tdata[0] >= self.maxt: # drop old frames
self.tdata = self.tdata[1:]
self.ydata = self.ydata[1:]
self.ax.set_xlim(self.tdata[0], self.tdata[0] + self.maxt)
t = lastt + 1
self.tdata.append(t)
self.ydata.append(y)
self.line.set_data(self.tdata, self.ydata)
return self.line,
def serial_getter():
# grab fresh ADC values
# note sometimes UART drops chars so we try a max of 5 times
# to get proper data
while True:
for i in range(5):
line = ser.readline()
try:
line = float(line)
except ValueError:
continue
break
yield line
if len(sys.argv) < 2:
raise Exception("Ruh roh..no port specified!")
ser = serial.Serial(sys.argv[1], 115200, timeout=1)
fig, ax = plt.subplots()
plotter = Plotter(ax)
ani = animation.FuncAnimation(fig, plotter.update, serial_getter, interval=1,
blit=True, cache_frame_data=False)
ax.set_xlabel("Samples")
ax.set_ylabel("Voltage (V)")
fig.canvas.manager.set_window_title('ADC capture example')
fig.tight_layout()
plt.show()
Postup vytvoření krásného grafu měřených hodnot:
$ ./plotter.py /dev/ttyACM0
