0

Metro M4 high speed ADC (using DMA)
Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.

Metro M4 high speed ADC (using DMA)

by BryonMiller on Wed Jun 13, 2018 2:37 pm

Hi Everyone,

I've been having fun with my Metro M4 Express. One of the things I wanted to play around with getting analog samples at very high rates. I developed the code to enable a DMA channel and connect it to one of the analog inputs (A0..A5). I thought I'd share it in case anyone might find it useful.

See the attached zip with a directory containing adcdma.ino and a utility file called adcdmautil.ino. I'd be curious to hear back from anyone who might try this out.

Bryon
Attachments
adcdma.zip
(4.74 KiB) Downloaded 59 times

BryonMiller
 
Posts: 155
Joined: Fri Mar 04, 2016 10:34 am

Re: Metro M4 high speed ADC (using DMA)

by adafruit_support_mike on Thu Jun 14, 2018 3:41 am

Interesting.. thanks for posting the code!

adafruit_support_mike
 
Posts: 55974
Joined: Thu Feb 11, 2010 2:51 pm

Re: Metro M4 high speed ADC (using DMA)

by jrmclaugh on Wed Aug 01, 2018 11:41 pm

Hi BryonMiller!

I've gotten your sample running and am now trying to adapt it to my app.

I can't seem to find how to set/calculate the sample rate and whenever I try to do any actual processing on the data it locks up the Metro. Presumably it's too much work in the interrupt, but I'm not clear how to reduce frequency of that work to a manageable level. I'm investigating now, but any tips?

The useful registers seem to be the GCLK->PCHCTRL[ADC1_GCLK_ID], CTRLA PRESCALER bit, and the SAMPCTRL.

jrmclaugh
 
Posts: 4
Joined: Fri Jul 13, 2018 12:32 am

Re: Metro M4 high speed ADC (using DMA)

by BryonMiller on Thu Aug 02, 2018 10:21 am

Hello jrmclaugh,

I can't seem to find how to set/calculate the sample rate

You're right I did not provide a way to slow it down. I'd have to rescan the SAMD51 datasheet and recreate my thought process when I came up with this and see if I could post an example of slower sampling.

Rather than that let me tell you what I did. Full disclosure - I have not actually used the 1MSPS data but I did something very similar on a M0 machine. I made an oscilloscope with a feather M0. In this case the maximum sampling rate I could muster was 0.5M SPS. Many times, when using the 'scope, you don't need full speed, you might only need a subset of the samples. It all depended on the "sweep speed" desired. But almost always I would have a trigger condition I wanted satisfied. So I would scan each full-speed buffer looking for the trigger. Once the trigger occurred I would take points out of the full-speed buffer spaced at a sample-spacing corresponding to the desired sweep speed and place them into a sweep buffer. All calculations requiring anything but +/- math or greater/less comparisons (especially any floating point calculations) were performed in the main task, not the interrupt routine.

I also measured and saved the microseconds at the start of the interrupt routine and also at the end. With this data I could get a measurement of how busy the machine was. As you might imagine, the busiest time was when searching the entire buffer looking for the trigger. The actual act of taking a point out of the full-speed-buffer and saving it in the sweep buffer was relatively fast. While looking for the trigger condition I could easily have the M0 more than half consumed in the interrupt routine. The M4 is faster than the M0 so even with twice as much data per second I think moving my 'scope to a M4 would go smoothly. (I'm waiting for the M4 Feather to come into stock to find out.)

So for your case try to pull the data out of the high-speed buffer at whatever spacing corresponds to your needs. But no matter what, you are going to need enough of a machine to process the data and make results available at whatever speed you decide.

Post back here with your results!
Bryon

BryonMiller
 
Posts: 155
Joined: Fri Mar 04, 2016 10:34 am

Re: Metro M4 high speed ADC (using DMA)

by DanaK6JQ on Thu Aug 09, 2018 4:02 pm

BryonMiller wrote:Hello jrmclaugh,

I can't seem to find how to set/calculate the sample rate

You're right I did not provide a way to slow it down. I'd have to rescan the SAMD51 datasheet and recreate my thought process when I came up with this and see if I could post an example of slower sampling.

Rather than that let me tell you what I did. Full disclosure - I have not actually used the 1MSPS data but I did something very similar on a M0 machine.


I've done something similar, though without DMA, on the Metro M4 Express. I haplessly figured I would use Atmel START (Atmel Studio 7, Atmel-ICE) to port a DSP modem project I already have running on STM32F4xx parts to the SAMD51; the hardware averaging in the ADC is appealing.

Because I want a precise sampling rate for DSP, I configured a DPLL and Generic Clock to produce 9600 events/second, and configured that even to trigger conversion on the ADC. That's working as expected; note that you don't configure the ADC for free-running, and you don't need to start conversion, the event does it for you.

DanaK6JQ
 
Posts: 18
Joined: Thu Jun 15, 2017 2:38 pm

Re: Metro M4 high speed ADC (using DMA)

by Pumpa0815 on Thu Aug 23, 2018 9:45 am

Hi Bryon,

thanks for this post. It's really helpful. I got it running on my metro m4 express.

I would like to continuously acquire data and send it to my computer to write it to a file. I am quite new to low-level programming and couldn't figure it out myself. Therefore, I would highly appreciate your advise!

Thanks!!

Pumpa0815
 
Posts: 3
Joined: Thu Aug 23, 2018 7:01 am

Re: Metro M4 high speed ADC (using DMA)

by BryonMiller on Thu Aug 23, 2018 3:21 pm

Hi Pumpa0815.

I found you had another post where you were trying to read analog values and write them to Serial at 11kHz. Assuming that is what you're trying to do you could :

In the interrupt routine go through the array that was last filled and take every 91th point and write that value to a FIFO buffer. In your main routine watch for the FIFO buffer to show data is available. When it is, write all of the newly available data to the Serial port. Then you can do the bulk of further processing in your host computer.

Enhancement might be to do some filtering in the interrupt routine. I think you'll really want to minimize what you do here but some averaging of the 91 samples leading up to "this" point may or may not be helpful.

But I think your biggest problem might be trying to cram that much data through the serial port. Let's say your getting 3 digit ADC samples. That means you'll be writing 5 characters to Serial (3 characters for the digits plus a CR and LF). 5 characters at 11khz is over 500,000 baud. I'm not saying this is impossible but it isn't obvious to me that it will work.

You could just try that part of it by making a simple M4 test program that tries to push that much static data and keeps track of whether it overflows or not.

Good luck,
Bryon

BryonMiller
 
Posts: 155
Joined: Fri Mar 04, 2016 10:34 am

Re: Metro M4 high speed ADC (using DMA)

by BDL on Wed Mar 06, 2019 11:53 am

Hi Bryon,

From viewtopic.php?f=57&t=147971&p=732888#p732888 you said it might be best to reply about your M4 Express routine here.

I have a couple of questions for you. They are to test my understanding of what you have written.
1. It seems that you are running a ping pong sort of buffer, to maintain the sample rate (or at least have time to transfer the data). Is this correct?
2. In the dma_callback function, you are doing a check for sync error (incomplete buffer fill) and copying the buffer memory to the destination memory ?
3. If I run this script, on my Feather M4 Express, it indicates that the call_back time is 77us. At a sample rate of 1us, a 1K buffer would fill in 1024us, leaving 947us to perform any functions?

For my application, I need a more modest sample rate, of 100 KHz. I've designed and built a 6 pole anti-alias analog low pass filter. At fs/2, the filter response is down -72dB. If I were to slow things down, it would be in the adc_init function, in adcdamutil, correct? As a first pass, I'd try changing the CTLA.bit.PRESCALER, and maybe the SAMP.CTRL.reg.

I like the idea of the ping pong buffers. The only issue would be doing an FFT and detection in 10 ms. That might be hard, especially if there is a median filter.

If the execution time is too slow, may to revert to a single triggered dma burst. So far I can't see an easy way of doing that. It is surely possible, so I will think about it some more.

In my M0 sketch, there was a quick analysis whether or not to do the FFT (simple time domain threshold) or to collect another 10 ms of data. If there was a good capture, sampling was stopped until the detection/spectral analysis was complete. If there was not enough energy, then the sampling would restart. Unfortunately there is a bug in it. After about 10 minutes goes by, it quits. Somewhere it dies and it no longer loops. (I have a blinker function at the start of every pass of the loop.) However, within that 10 minutes, it seems to work ok.

Your sketch is a really nice piece of code. Thanks for posting it. I have learned a lot. Once I have some code that does something useful, I'll post it.

Last (kind of dumb question), how do you create a project/sketch, with multiple files in it? I'm trying to create my own, and the IDE insist on creating a unique sketch folder for each!

Regards, BDL

BDL
 
Posts: 19
Joined: Wed Jan 09, 2019 10:45 pm

Re: Metro M4 high speed ADC (using DMA)

by BryonMiller on Wed Mar 06, 2019 3:52 pm

Hello BDL,
1. It seems that you are running a ping pong sort of buffer, to maintain the sample rate (or at least have time to transfer the data). Is this correct?
Notionally there are two 1K buffers, the DMA is setup to fill the first buffer and once it is done it causes an interrupt and, without missing a sample, starts filling the second buffer. Once second is filled it interrupts again and starts back on the first buffer. I've never heard this method called a ping pong buffer, I've always called it a double buffer. I like ping pong better.
2. In the dma_callback function, you are doing a check for sync error (incomplete buffer fill) and copying the buffer memory to the destination memory ?
I was not considering an incomplete buffer fill. Not sure how that would happen. What I meant by checking for sync errors was once an interrupt occurs you have to start processing the buffer that was just filled. Saying this another way is that you better not be processing the same buffer that is currently being filled. If you get a sync error it means the software has gotten out of phase with the hardware. I've never seen it happen but I've only run a relatively small amount of software.

Given no error, yes my example merely copies the most recently filled buffer to another array that could presumably be used by non-interrupt software to do whatever you want. My original use for this software was to display the data on a LCD screen making my own oscilloscope. Before I would display the data I'd go through the buffer and look for a "trigger" condition; once I found the trigger I'd copy the thereafter data into an array that would eventually be displayed. If the 'scope was set to a sweep rate less than the maximum I would not copy every single subsequent sample but rather the samples that corresponding to the sweep rate. So I would always keep the ADC running at max rate but only save at a lower rate. The copying could easily span across several DMA buffers before I had the right amount of data for the screen.

3. If I run this script, on my Feather M4 Express, it indicates that the call_back time is 77us. At a sample rate of 1us, a 1K buffer would fill in 1024us, leaving 947us to perform any functions?
Yep, but part of that 77us is used for copying. If your going to do something different you might not have to do the copying and could possibly have a little more time.

For my application, I need a more modest sample rate, of 100 KHz. I've designed and built a 6 pole anti-alias analog low pass filter. At fs/2, the filter response is down -72dB. If I were to slow things down, it would be in the adc_init function, in adcdamutil, correct? As a first pass, I'd try changing the CTLA.bit.PRESCALER, and maybe the SAMP.CTRL.reg.
Yeah, its all happening in adc_init. When I first wrote this I was deep into the bowels of the M4 documents. Now I can hardly remember how to spell it. But I 'think' I'd look into SAMPCTRL.reg. Seems like using a larger number actually allowed the ADC to settle a little before sampling. May be a good thing, especially if your analog source is not a low-impedance source. You could also look at averaging as a way to improve the sample quality and slow down the DMA.

I like the idea of the ping pong buffers. The only issue would be doing an FFT and detection in 10 ms. That might be hard, especially if there is a median filter.
I'm afraid I don't know what you mean by this.

If the execution time is too slow, may to revert to a single triggered dma burst. So far I can't see an easy way of doing that. It is surely possible, so I will think about it some more.
If you want to capture only one block of data and you have enough RAM to hold the entire block I can't think of a reason why you would need ping pong; presumably just ping. So you'd just set up one DMA descriptor (rather than two like I did). You can keep the DMA controller from over writing your one data block by changing the line that says
Code: Select all | TOGGLE FULL SIZE
myDMA.loop(true);

to
Code: Select all | TOGGLE FULL SIZE
myDMA.loop(false);


Your sketch is a really nice piece of code. Thanks for posting it. I have learned a lot. Once I have some code that does something useful, I'll post it.
Thanks I appreciate that and I'd be interesting to see what you do.

Last (kind of dumb question), how do you create a project/sketch, with multiple files in it? I'm trying to create my own, and the IDE insist on creating a unique sketch folder for each!
Just make a single folder called "adcdma" and inside that folder copy the two .ino files. (The main file must have the same name as the folder). The IDE will open the two files as two separate tabs.

Regard,
Bryon

BryonMiller
 
Posts: 155
Joined: Fri Mar 04, 2016 10:34 am

Re: Metro M4 high speed ADC (using DMA)

by BDL on Wed Mar 06, 2019 7:31 pm

Thanks for such a detailed reply, it is greatly appreciated.
I'm afraid I don't know what you mean by this.

For each buffer, I do an FFT, and a median filter on the FFT output to estimate the noise level. (On an M0 doing a float median filter is really slow) Peaks which exceed the estimated noise level by a factor of 20dB are "detected".
I just added in the arm fft to the dma callback function. It runs pretty quickly. Here is a snippet of the code for the FFT. My median filter and detection are a work in progress.
Before this code one needs to #define ARM_MATH_CM4, and #include <arm_math.h>. This allows the use of the special arm code that is FPU aware.
Code: Select all | TOGGLE FULL SIZE
for (int i= 0;i<DATA_LENGTH;i++) {
      buffer_memory1[2*i] = (float)destination_memory1[i]*3.3/4096;      // convert to float and scale
      buffer_memory1[2*i+1] = 0.0f;                                                             // set imaginary part of memory to zero
      // this loads the real part into the buffer evens, and 0 to the odds
    }
// try the fft here!
arm_cfft_radix4_instance_f32 fft_inst;
arm_cfft_radix4_init_f32(&fft_inst, FFT_SIZE, 0, 1);
arm_cfft_radix4_f32(&fft_inst, buffer_memory1);
arm_cmplx_mag_squared_f32( buffer_memory1, outbuf, FFT_SIZE/2 );  // compute the magnitude squared = power
for (int i=0; i<FFT_SIZE/2; i++) {
      outbuf[i] = 10.0f*log10f(outbuf[i]);  // already power due to magsq  // convert to dBs
}

Something in your code I don't understand is in setup
Code: Select all | TOGGLE FULL SIZE
  while(transfer_cnt == 0); // Chill until 1st DMA transfer completes

  digitalWrite(LED_BUILTIN, LOW);
  int cnttemp = transfer_cnt;
  while (cnttemp==transfer_cnt);
  microt = micros();
  cnttemp = transfer_cnt;
  while (cnttemp==transfer_cnt);
  microt = micros() - microt;   // Elapsed time

  Serial.print("Timing Done! ");
  Serial.print(microt);
  Serial.println(" microseconds");
  Serial.print("transfer_cnt ");
  Serial.println(transfer_cnt);

What is this part of the code doing, and what is its significance? How much information is being transferred? What is microt mean for this example, the time to transfer X? What is X? I see transfer_cnt prints out with a value of 3. This confuses me a bit. I was hoping it would be the time to do DATA_LENGTH conversions and write to memory. The time values that are output aren't consistent with my changes. I've increased ADCx->CTRLA.bit.PRESCALER = ADC_CTRLA_PRESCALER_DIV32_Val; from your value and I expected a longer time. I think you used DIV2.

Thanks so much for your input. I've learned a lot from you.

BDL
 
Posts: 19
Joined: Wed Jan 09, 2019 10:45 pm

Re: Metro M4 high speed ADC (using DMA)

by BryonMiller on Thu Mar 07, 2019 10:01 am

Something in your code I don't understand is in setup
while(transfer_cnt == 0); // Chill until 1st DMA transfer completes
digitalWrite(LED_BUILTIN, LOW);
transfer_cnt is incremented each time there is a DMA interrupt. transfer_cnt is initialized to zero. The DMA controller has just been started so this 'while' is waiting until the DMA has finished filling the first buffer for the first time. Once it does it turns off the LED. I initially put the LED change in the code when I was developing and I wanted an easy way to see if I would be getting an interrupt from the DMA controller.
int cnttemp = transfer_cnt;
while (cnttemp==transfer_cnt);
now that an interrupt occurred we are going to see how long between transfer_cnt changes. This bit of code was initially intended to go elsewhere, not necessarily right after a transfer_cnt sync, so it waits for another transfer_cnt change. Here it is presumably 1, but maybe more depending on how long it took to do the digitalWrite. while loop waits again for it to change.
microt = micros();
cnttemp = transfer_cnt;
while (cnttemp==transfer_cnt);
Once it changes read the M4 time. microt stands for 'time in microseconds'
cnttemp = transfer_cnt;
while (cnttemp==transfer_cnt);
microt = micros() - microt; // Elapsed time
read transfer_cnt again and wait for it to change again; once it does calculate the time between the previous change and this latest change. that time (in microseconds) is how long it took the ADC & DMA to fill one of the buffers.
Code: Select all | TOGGLE FULL SIZE
Serial.print("Timing Done! ");
  Serial.print(microt);
  Serial.println(" microseconds");
  Serial.print("transfer_cnt ");
  Serial.println(transfer_cnt);
Print out the time and, just for the heck of it, print transfer_cnt
The time values that are output aren't consistent with my changes. I've increased ADCx->CTRLA.bit.PRESCALER = ADC_CTRLA_PRESCALER_DIV32_Val; from your value and I expected a longer time.
You'll have to figure this out. I've really been resisting digging into this myself. Let us know what you find.

BryonMiller
 
Posts: 155
Joined: Fri Mar 04, 2016 10:34 am

Re: Metro M4 high speed ADC (using DMA)

by BDL on Thu Mar 07, 2019 3:13 pm

I reverted to your code. That way it is possible for me to test the basic core SW. I made one change to your dump routine, where the output format was changed to DEC (not HEX). Its just easier for me to read and make sense of.

I connected pin A4 to pin A1 on the M4FX (M4 Feather Express). I also attached a volt meter to A1.

I compiled & uploaded the file to the M4FX. The timing was 1024 microseconds which would be expected if the conversion time for a single sample was 1 us. Pressed s, and saw the Dest Mem2 was around 220, which doesn't make much sense. Dest Mem1 is also reading around the same number.

Then I pressed < to set analog out (A1) to zero. My voltmeter reads 4.8mV, which is close enough to zero for me. If I press s, Dest2 and Dest1 memory read values that seem to be pretty close to 220. Basically, it seems that the ADC A4 is not reading pin A1.

If I directly connect GND to A4, then I also read values of about 220.

So I thought, maybe the sampling is not occuring on A4. Could it be a different pin? So I connected a ground to A3, then A2. Each time I would press s to read back the values. It appears that the SW is sampling from pin A2, not A4. GND at pin A2 results in s output of mostly 1's. AREF at pin A2 results in s of full scale. So if I connect pin A1 to A2, and select A4 in the SW, the ADC (connected to pin A2) is now sampling the DAC at A1.

If I were to hazard a guess, there is a bug in either adc_init, or adcdma_init. Quickly looking at it, I'd guess it is in adcdma_init in the case statements where the mux is selected. I don't yet understand the mux yet to comment or fix it. Back to the manual...

BDL
 
Posts: 19
Joined: Wed Jan 09, 2019 10:45 pm

Re: Metro M4 high speed ADC (using DMA)

by BryonMiller on Fri Mar 08, 2019 11:26 am

OMG BDL, You're right! I developed and posted for the Metro M4 and I didn't even have a Feather M4 to try it on. I just assumed it would work. Turns out, even though Metro M4 & Feather M4 have same processor they are wired differently from the MCU pins to the board pins. I did not properly account for the variant.

I think you could just use "how-it-works" but I remember I was trying to use the second ADC for some of the inputs and that might fail.

The Arduino code never uses ADC1. The reason I wanted to was so that someone could do a normal analogRead on an input that was not connected to the DMA and have everything work. If the DMA reading is using ADC0 you would not be able to do an analogRead without forever after screwing up the DMA.

I'll try to dig into this as soon as possible but hopefully you can continue to try to figure out how to slow it down in the meantime.

Bryon

BryonMiller
 
Posts: 155
Joined: Fri Mar 04, 2016 10:34 am

Re: Metro M4 high speed ADC (using DMA)

by BDL on Fri Mar 08, 2019 12:12 pm

I just realized I forgot to post this last night. Since I got your basic code working on A2, I jumped back to my code with CMSIS FFTs and stuff. I just hooked up my front end to A2, rather than A4. (While keeping A4 in the code - confused yet?)

On the gross inconsistent timing: one cause was a global variable getting clobbered. (microt) Made microt local - timing is at least moderately consistent.
Have any ideas on making the sampling more consistent? Inconsistent sampling converts to noise, which masks signal.

Ten (10) Successive restarts of my modified program: in us
10735, 10721, 11667, 11058, 11348, 10993, 10775, 11082, 10779, 10784

Mean timing: 10994.2 us for 1024 12 bit samples. If the sample rate is constant (and I have no way to know this right now) the mean time per sample is 10.736 us, or a mean fs = 93,140 Hz. I'm trying to hit 100 KHz, so at least I'm in the ball park. Standard deviation was 295us for the 1024 samples. Seems like a lot. The jitter may be a function of the chip / ADC architecture, but it would be nice to minimize it.

Today, I just measured the timing and it was 8101 microseconds. I wish I knew what was going on. It could be as simple as a bug I introduced.

Nonetheless, making some good progress. As noted above, the front end hardware is now connected to the Feather M4 Express. I cut the jumper between Aref and 3V, and connected my reference to Aref. The signal from the front end is biased to 1.65V and I see that when I do a dump. Unfortunately, I can't quite output the data fast enough before a sync error if I use floats. The serial port is now at 500000 baud. However, if the data is type cast to short it just fits. That is just fine for now. Or I could temporarily slow things down even further just for initial debug.

Looking at a string of 500 numbers is hard to interpret what is going on. (fft output is actually 512 samples) Is there a version of the plotter that can send commands back to the Arduino? The standard Arduino Serial plotter does not seem to have that ability.

BDL
 
Posts: 19
Joined: Wed Jan 09, 2019 10:45 pm

Re: Metro M4 high speed ADC (using DMA)

by BryonMiller on Sun Mar 10, 2019 2:59 pm

Hi BDL,

I've been working on another project so I know I'm slow getting back to this. I realized what was wrong with the Feather M4 analog in so I'm posting another version that, I believe, works with either a Metro M4 or a Feather M4. If you try this version let me know if something looks wrong.

adcdma(posted to AdaFruit)V2.zip
(4.75 KiB) Downloaded 3 times

Regarding the changes you're making, it really does not seem right that the timing is inconsistent. Nor does it make sense to me that microt was getting clobbered when it was a global. What would have clobbered it? I changed "SAMPCTRL", "AVGCTRL.reg" and "SAMPCTRL.reg". Any change slowed the sampling down as anticipated. All changes were consistent within a few microseconds. I suspect your timing is also consistent but your measurement method is making it look funny.

Is there a version of the plotter that can send commands back to the Arduino?
I don't know of one but I'm not really the person to ask. If you want an answer from someone else you would probably be best to open a new post. I doubt anyone else is reading this one by now.

BryonMiller
 
Posts: 155
Joined: Fri Mar 04, 2016 10:34 am

Please be positive and constructive with your questions and comments.