Reading ADC samples via DMA Mar 2016
Now that we have seen how to push out values to the DAC without CPU intervention… can we do the same for acquiring ADC sample data? The answer is a resounding “yes, of course!”
And it’s not even hard, requiring less than two dozen lines of code (full details on GitHub):
: adc1-dma ( addr count pin rate -- ) \ continuous DMA-based conversion 3 +timer +adc adc drop \ perform one conversion to set up the ADC 2dup 0 fill \ clear sampling buffer 0 bit RCC-AHBENR bis! \ DMA1EN clock enable 2/ DMA1-CNDTR1 ! \ 2-byte entries DMA1-CMAR1 ! \ write to address passed as input ADC1-DR DMA1-CPAR1 ! \ read from ADC1 [...] DMA1-CCR1 ! [...] ADC1-CR2 ! ;
The setup calls the “
+adc” and “
adc” words, defined earlier for simple
polled use of the ADC, and also sets up a timer (again, to define the
sampling rate) and the relevant DMA channel.
Let’s have some fun. Let’s first start the DAC via DMA to generate a sine wave, and let’s then also set up the ADC to read and sample this signal back into memory. As set up here, the ADC’s DMA channel saves its data in a circular fashion and keeps on overwriting old data until reconfigured.
And while we’re at it, let’s also plot that acquired data on the Hy-MiniSTM32V’s LCD screen - to create a little one-channel scope (but without triggering, so the screen won’t show a stable image while this code is running). Here is the main logic (see GitHub for the whole story, as usual):
602 buffer: trace : scope ( -- ) \ very crude continuous ADC capture w/ graph plot tft-init clear border grid TFT-BL ios! 11 dac1-awg adc-buffer PB0 501 adc1-dma begin \ grab and draw the trace 301 0 do adc-buffer drop i 2* + h@ 20 / 1+ dup trace i 2* + h! \ also save a copy in a buffer i pp loop 40 ms \ leave the trace on the screen for a while \ bail out on key press, with the trace stil showing key? 0= while \ clear the trace again tft-bg @ tft-fg ! 301 0 do trace i 2* + h@ i pp loop $FFFF tft-fg ! grid \ redraw the grid repeat ;
pp” is shorthand, defined as “
: pp ( x y ) 10 + swap 20 + swap
The DAC is fed sine wave samples at 0.5 MHz, and the ADC is driven by a timer running at 502 cycles, i.e. about 71.7 KHz (just because that gave a reasonably stable display - there’s clearly aliasing involved at these two rates). The DAC has a 4096-sample buffer, the ADC has only 301.
begin ... key? 0= while ... repeat” loop then produces an
oscilloscope-like result on the screen, continuously refreshed at about 20
frames per second. By tinkering a bit with the “
border” and “
grid” code, we
can actually add a pretty neat graticule to this screen as well:
The ADC clock is set to 12 MHz (72 MHz on APB1 with prescaler 6), i.e. under the 14 MHz limit. The max sample rate for this setup is ≈ 833 KHz (each measurement needs 14 clock cycles). This corresponds to a minimum timer value of 43 (timers are on APB1, which is clocked at 36 MHz).
Let’s examine the above main loop in a bit more detail:
- the “
301 0 do .. loop” code displays 301 samples from the ADC acquisition buffer
- we also save a copy of these displayed values in a secondary
- do nothing for 40 milliseconds - this is to leave the image on the screen for a while
- rewrite the trace onto the screen once more, but now using the background colour (black)
- redraw the dotted grid inside the box, since some of the dots may have been overwritten
- rinse and repeat
The logic behind this approach is that clearing the entire display on every pass produces a highly flickering result, as display updates are not synchronised to what the LCD controller is doing. With 30 ms to clear the screen, we’d see part of the screen blanked out, and that at every pass.
So instead, we write the pixels of the trace as we capture them, leave them on the screen for a while, and then clear those (and only those!) pixels again. That’s 250x fewer pixels to update.
Bingo - a crude-and-simple (but pretty!) capture of analog data, constantly updated on the LCD. When a key is pressed, the loop exits and leaves the last trace on the screen. As mentioned before, there’s no triggering, no config, no scaling, no line-drawing interpolation in this demo.
With a loop which only takes 8 ms (plus the 40 ms wait), there is ample processor “headroom” for all kinds of improvements. Filtering, decimation, smoothing, sinx/x traces? Go for it …
Note how the DAC and ADC hardware is driven entirely by the two DMA engines, with the CPU free to perform the main logic and rendering. All this took under 500 lines of Forth code.
DMA is like having a multi-processor under the hood, all inside that one little STM32F103!