Teensy 3.6 ADC/DMA Question

Status
Not open for further replies.

Bill Greiman

Well-known member
I would like to use DMA with Teeny 3 ADCs in a mode similar to STM32 "circular" mode. Is there a way to do this?

In STM32 "circular" mode I use a 1024 byte buffer and set up a DMA channel to continuously read the ADC.

The DMA controller produces an interrupt at mid buffer and end of buffer. At end of buffer it restarts at beginning of buffer.

This simple interrupt routine writes a continuous stream of 512 byte blocks to a FIFO.

Code:
// uint32_t to insure 4-byte alignment.
static uint32_t dmaBuf[2][128];

static uint32_t dmaCount;

static void DMA1_CH1_Event() {                                                                                                        static void DMA1_CH1_Event() {
  if (fifoCount < FIFO_DIM) {
    // memcpy takes 7 microseconds on STM32F103 at 72 MHz.
    memcpy(fifoBuf[fifoHead], dmaBuf[dmaCount & 1], 512);
    fifoHead = fifoHead < (FIFO_DIM - 1) ? fifoHead + 1 : 0;
    fifoCount++;
    dmaCount++;
  } else {
    // Overrun error.
  }
}

I have been testing my new exFAT library.

It only requires 6 KB of FIFO on an ebay STM32F103C8 board, which has 20KB of RAM, to log a sequence of ADC channels to an SD at nearly 1MHz.

I would like to go faster on Teensy 3.6 and try SDIO.
 
Hi Bill,

I am hoping others who have more direct experience doing what you want will give a more complete answer as I have not done exactly what you have mentioned. However I have done some playing around with the DMA with the SPI code as well as with my version of ili9341_t3n library which added a dma update... But I have not tried anything with DMA with the ADC...

Again I have not done the circular code directly, but I do know that it is supported. If you look at the DMAChannel.h(cpp) files you will find members for setting up circular (SourceCircular, or more likely for your case destinationCircular). Queue.

You can also setup for interrupts at completion and/or midpoint, with the interruptAtCompletion and the interruptAtHalf.

Again I have not used the circular buffer directly, but have used the linking of buffers, which worked in a circular way, like what FrankB did with the DMA version of the ILI9341 library. I did not go circular as the maximum count for a buffer is either 32767 or 511, depending on some different options, and the display is 320*240*2 bytes...

Also if you do chain buffers, you can setup additional interrupts if needed.

Again sorry not much help here.

Kurt
 
Is there a way to do this?

Yes. Multiple ways, actually.

In STM32 "circular" mode I use a 1024 byte buffer and set up a DMA channel to continuously read the ADC.

The DMA controller produces an interrupt at mid buffer and end of buffer. At end of buffer it restarts at beginning of buffer.

You're going to have to look at the DMA TCD registers in the reference manual. Or maybe you can make use of DMAChannel.h helper functions. But the way most libs use DMAChannel is merely to allocate the channel (so they play nicely with other libs using DMA) and then access its TCD registers.

There is an option for a circular buffer.

But they way you've described is probably done in most cases by setting the destination last address offset register to adjust the destination back to the beginning of the buffer, and the flags register to leave the channel enabled. Then the next hardware request gets serviced, because the DMA channel automatically sets itself back to its original settings with the final destination adjust. Of course, there's also a flag for the interrupts at half and full. The ISR function can tell which half is being written by just looking at the destination register.

This is how most of the audio library DMA works. If you're looking for an example to start, those might help.
 
Thanks, looks like I have lots of examples for Teensy 3.

Now I need an architecture for all the boards I want to support. I think I will use a HAL, hardware adaptation layer, model from an RTOS.

I plan to support STM32F1, STM32F4, Teensy 3, and probably SAM3/Due initially. People are starting to use lots of ebay Due clones, that cost about $13. I may do an interrupt version for AVR.

The HAL I use a lot has a abstract level with these calls for ADCs with interrupt or DMA data readout.
Code:
void 	adcInit (void)
 	ADC Driver initialization. More...
 
void 	adcObjectInit (ADCDriver *adcp)
 	Initializes the standard part of a ADCDriver structure. More...
 
void 	adcStart (ADCDriver *adcp, const ADCConfig *config)
 	Configures and activates the ADC peripheral. More...
 
void 	adcStop (ADCDriver *adcp)
 	Deactivates the ADC peripheral. More...
 
void 	adcStartConversion (ADCDriver *adcp, const ADCConversionGroup *grpp, adcsample_t *samples, size_t depth)
 	Starts an ADC conversion. More...
 
void 	adcStartConversionI (ADCDriver *adcp, const ADCConversionGroup *grpp, adcsample_t *samples, size_t depth)
 	Starts an ADC conversion in an interrupt handler. More...
 
void 	adcStopConversion (ADCDriver *adcp)
 	Stops an ongoing conversion. More...
 
void 	adcStopConversionI (ADCDriver *adcp)
 	Stops an ongoing conversion in an interrupt handler.. More...
 
msg_t 	adcConvert (ADCDriver *adcp, const ADCConversionGroup *grpp, adcsample_t *samples, size_t depth)
 	Performs an ADC conversion. More...

There is an abstract level that specifies the mandatory fields for each of the types. Here are the mandatory fields for ADCConversionGroup.

Code:
typedef struct {
  /**
   * @brief   Enables the circular buffer mode for the group.
   */
  bool                      circular;
  /**
   * @brief   Number of the analog channels belonging to the conversion group.
   */
  adc_channels_num_t        num_channels;
  /**
   * @brief   Callback function associated to the group or @p NULL.
   */
  adccallback_t             end_cb;
  /**
   * @brief   Error callback or @p NULL.
   */
  adcerrorcallback_t        error_cb;
  /* End of the mandatory fields.*/
} ADCConversionGroup;

The low level drivers for each architecture defines the full type. Here is STM32F1 ADCConversionGroup.
Code:
typedef struct {
  /**
   * @brief   Enables the circular buffer mode for the group.
   */
  bool                      circular;
  /**
   * @brief   Number of the analog channels belonging to the conversion group.
   */
  adc_channels_num_t        num_channels;
  /**
   * @brief   Callback function associated to the group or @p NULL.
   */
  adccallback_t             end_cb;
  /**
   * @brief   Error callback or @p NULL.
   */
  adcerrorcallback_t        error_cb;
  /* End of the mandatory fields.*/
  /**
   * @brief   ADC CR1 register initialization data.
   * @note    All the required bits must be defined into this field except
   *          @p ADC_CR1_SCAN that is enforced inside the driver.
   */
  uint32_t                  cr1;
  /**
   * @brief   ADC CR2 register initialization data.
   * @note    All the required bits must be defined into this field except
   *          @p ADC_CR2_DMA, @p ADC_CR2_CONT and @p ADC_CR2_ADON that are
   *          enforced inside the driver.
   */
  uint32_t                  cr2;
  /**
   * @brief   ADC SMPR1 register initialization data.
   * @details In this field must be specified the sample times for channels
   *          10...17.
   */
  uint32_t                  smpr1;
  /**
   * @brief   ADC SMPR2 register initialization data.
   * @details In this field must be specified the sample times for channels
   *          0...9.
   */
  uint32_t                  smpr2;
  /**
   * @brief   ADC SQR1 register initialization data.
   * @details Conversion group sequence 13...16 + sequence length.
   */
  uint32_t                  sqr1;
  /**
   * @brief   ADC SQR2 register initialization data.
   * @details Conversion group sequence 7...12.
   */
  uint32_t                  sqr2;
  /**
   * @brief   ADC SQR3 register initialization data.
   * @details Conversion group sequence 1...6.
   */
  uint32_t                  sqr3;
} ADCConversionGroup;
 
I decided to try logging with a dummy DMA device to make sure exFAT with FIFO_SDIO is fast enough. Looks like ADCs won't be a problem.

Logging at 10 MB/sec seems to work fine with a quality Samsung SD.

I attached the test program. It allocates a FIFO with 400 sectors which uses 204,800 bytes of SRAM.

I used a timer to simulate a DMA device that reads 512 bytes and generates an interrupt every 50 microseconds

Here is the simulated ISR with a circular two sector DMA buffer :
Code:
// Dummy ISR.
void callback() {
  if (fifoCount < FIFO_DIM) {
    // Takes about 3 microseconds on teensy 3.6.
    memcpy(fifoBuf[fifoHead], dmaBuf[dmaCount & 1], 512);
    dmaCount++;
    fifoHead = fifoHead < (FIFO_DIM - 1) ? fifoHead + 1 : 0;
    fifoCount++;
    if (fifoCount > maxFifoCount) {
      maxFifoCount = fifoCount;
    }
  } else {
    // overrun
    overrun = true;
  }
}

Here are results for a few 64GB exFAT microSD cards. Samsung Pro cards are best, SanDisk Extreme Pro cost more but are not as good and Samsung Evo doesn't cut it.

Samsung cards use about half the FIFO. The max used is maxFifoCount.
HTML:
Type any character to begin

FIFO_DIM = 400
FreeStack: 49595
Type any character to stop

Samsung Pro Select 64 GB
197 maxFifoCount
3435397120 bytes
335.49 seconds
10.24 MB/sec

SanDisk Extreme Pro 64GB
241 maxFifoCount
1588078592 bytes
155.09 seconds
10.24 MB/sec

Second Samsung Pro Select 64 GB
146 maxFifoCount
1842427904 bytes
179.92 seconds
10.24 MB/sec

Old Samsung Pro+
164 maxFifoCount
1266618880 bytes
123.69 seconds
10.24 MB/sec

Samsung EVO 64GB fails.
Overrun ERROR!!
400 maxFifoCount
41157120 bytes
4.07 seconds
10.12 MB/sec
 

Attachments

  • Teensy36IsrLogger.ino
    3.5 KB · Views: 199
Nice, if i'm looking at my notes correctly you could do +/-1.4Msps between ADC0 and ADC1 combined at 12Bit.

It should be possible to easily do 1.4Msps.

I attached a first cut prototype for ADC0. It's pretty rough, just a test.

I hacked Paul's Audio ADC0 program. I am overclocking the ADC by a factor of 2.5 over the 24 MHz max and cutting sample and hold cycles to the minimum.

This runs the ADC with a clock of F_BUS, 60MHz. The result is 3.0 Msps with 12-bit samples. I scaled the points in the plot by 3.3/4095

I just wanted the speed, not accuracy. The results are better than I expected. Here is a quick Excel plot of a 50kHz sine with 60 points per cycle.

TeensyTest.jpg

I have a very good scope and it measures Vpp and Voff very close to the plot. The signal generator isn't too accurate for high impedance loads.

The program uses only 70 of the 400 fifo sectors that I allocate. I am pleased with exFAT performance.

Code:
FIFO_DIM = 400
FreeStack: 49599
Type any character to stop

70 maxFifoCount
1983390720 bytes
1046.39 seconds
6.00 MB/sec
 

Attachments

  • Teensy36AdcDmaLogger.ino
    5.7 KB · Views: 417
Last edited:
The time constant on the Teensy 3 ADC is something like 25nS, so if you drive it with a decent source it wont take long to get charged. And I would not expect to much noise if the source is decent.

The fastest possible sample speed is 4 ADC clock cycles(dont recall if it is constant or goes up after 1 sample). And if we go for say 5 time constants(the standard in textbooks) we would need 125nS minimum, so say 32Mhz on the ADC clock with 4 ADC clocks per sample. Probably safer at 24Mhz to reduce noise and possible hardware issues.
This also assumes you dont mind loosing some of your ADC voltage at 5 time constants you will loose about 28 off your 12bit value since the cap is not fully charged..
See the below image for time constants vs percentage of charge.

ADC Time Constant Losses.JPG
 
The time constant on the Teensy 3 ADC is something like 25nS, so if you drive it with a decent source it wont take long to get charged. And I would not expect to much noise if the source is decent.

My test is kind of a cheat. I used a signal generator designed to drive a 50 ohm load. The 50 kHz sine at a sampling rate of 3 Msps means you don't really see S/H performance. You don't need to change the cap voltage much since there are 60 sample and hold times for a cycle.

My main reason for the test was to show that I didn't have a bug that scrambled the values in the circular dma buffer or FIFO. I was glad to see the sine pattern when I read the file.

I did find a bug in truncate(). I allocate a 8GiB file then truncate it at the end. I didn't sync properly in truncate so file system could be corrupt if the file was not accessed and closed properly.

I try to do tests during development but nothing beats real apps for revealing bugs.
 
I ran a test with a square wave. Interesting noise. It is max about every 256 points, probably when I write a sector. No surprise, no signal averaging short S/H. No 50 ohm load, dangling wires, USB power, ...

TeensySquare.jpg

Data is attached. No ideal 12-bit 74 dB SNR here.

A closer view.

TeensySquareMag.jpg
 

Attachments

  • squareData.txt
    11 KB · Views: 178
Last edited:
That may work unless the noise is induced on the ADC via the Data lines, which is something I have seen with SPI coms to ILI9341 and reading ADC's with/without external ref at the same time.
 
Hi Bill, I`m searching a code like yours, I want read analog values and write it fastest as possible in SD. I don't have so much experience like you to do the code, May you share your code?
When you say that you can Read analog and Write it on SD with 3Msps, its each ADC channel or combined two? If I use like 4 ch analogRead, 2 per channel I may expect 750Ksps or 1.5Msps?
Thnak you
 
Status
Not open for further replies.
Back
Top