Advice on getting ADC data out

ChibiOS public support forum for all topics not covered by a specific support forum.

Moderators: RoccoMarco, lbednarz, utzig, tfAteba, barthess

IgorEE
Posts: 21
Joined: Fri Nov 14, 2014 4:40 pm

Advice on getting ADC data out

Postby IgorEE » Sat Feb 07, 2015 9:58 pm

Hi,

I'm new to RTOS concepts and I'm playing around with ChibiOS on and off as I have free time hoping to get acquainted enough with it to use it for projects soon enough.

I've hit a bit of a problem that I would like some advice on.

I want to sample 6 ADC channels at 100kHz each, but for the time being I am starting out slower. My problem is that I do not know how to get the data out fast enough or how to synchronize data capture and output properly. Currently, I have tried a few different approaches (using a GPT timer, using a thread that sends out data) and I've been the most successful with the approach below (stripped to a minimum code example and outputting a single ADC channel data):

Code: Select all

/*
    ChibiOS/RT - Copyright (C) 2006-2013 Giovanni Di Sirio

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "ch.h"
#include "hal.h"

#include "chprintf.h"

#include "usbcfg.h"

/* ADC conversion group settings */
#define ADC_GRP_NUM_CHANNELS   6
#define ADC_GRP_BUF_DEPTH      1
/* Buffer for ADC samples */
static adcsample_t samples[ADC_GRP_NUM_CHANNELS * ADC_GRP_BUF_DEPTH];

/* Virtual serial port over USB.*/
SerialUSBDriver SDU1;

/*
 * ADC conversion group.
 * Mode:        Circular buffer, 1 samples of 6 channels, SW triggered.
 * Channels:    IN10, IN11, IN14, IN15, IN8, IN9
 */
static const ADCConversionGroup adcgrpcfg = {
  TRUE,
  ADC_GRP_NUM_CHANNELS,
  NULL,
  NULL,
  0,                        /* CR1 */
  ADC_CR2_SWSTART,          /* CR2 */
  ADC_SMPR1_SMP_AN10(ADC_SAMPLE_15) | ADC_SMPR1_SMP_AN11(ADC_SAMPLE_15) |
  ADC_SMPR1_SMP_AN14(ADC_SAMPLE_15) | ADC_SMPR1_SMP_AN15(ADC_SAMPLE_15),
  ADC_SMPR2_SMP_AN8(ADC_SAMPLE_15) | ADC_SMPR2_SMP_AN9(ADC_SAMPLE_15),
  ADC_SQR1_NUM_CH(ADC_GRP_NUM_CHANNELS),
  0,                        /* SQR2 */
  ADC_SQR3_SQ1_N(ADC_CHANNEL_IN10) | ADC_SQR3_SQ2_N(ADC_CHANNEL_IN11) |
  ADC_SQR3_SQ3_N(ADC_CHANNEL_IN14) | ADC_SQR3_SQ4_N(ADC_CHANNEL_IN15) |
  ADC_SQR3_SQ5_N(ADC_CHANNEL_IN8) | ADC_SQR3_SQ6_N(ADC_CHANNEL_IN9),
};




/*===========================================================================*/
/* Initialization and main thread.                                           */
/*===========================================================================*/

/*
 * Application entry point.
 */
int main(void) {
  halInit();
  chSysInit();
  /*
   * Initializes a serial-over-USB CDC driver.
   */
  sduObjectInit(&SDU1);
  sduStart(&SDU1, &serusbcfg);

  /*
   * Activates the USB driver and then the USB bus pull-up on D+.
   * Note, a delay is inserted in order to not have to disconnect the cable
   * after a reset.
   */
  usbDisconnectBus(serusbcfg.usbp);
  chThdSleepMilliseconds(1000);
  usbStart(serusbcfg.usbp, &usbcfg);
  usbConnectBus(serusbcfg.usbp);

  /* Setup pins for ADC */
  palSetGroupMode(GPIOC, PAL_PORT_BIT(0) | PAL_PORT_BIT(1) |
                         PAL_PORT_BIT(4) | PAL_PORT_BIT(5),
                    0, PAL_MODE_INPUT_ANALOG);
  palSetGroupMode(GPIOB, PAL_PORT_BIT(0) | PAL_PORT_BIT(1),
                    0, PAL_MODE_INPUT_ANALOG);
  adcStart(&ADCD1, NULL);
  adcStartConversion(&ADCD1, &adcgrpcfg, samples, ADC_GRP_BUF_DEPTH);


 while (TRUE) {
     chprintf(((BaseSequentialStream *)&SDU1), "%d\r\n", samples[0]);

 }
}


As you can see I am using the USB CDC driver to send out the data (I figured this is faster than a standard UART?). The data I am feeding is a 100Hz sinewave, and the sampled data is attached. As you can see, the first two periods of the sinewave are output nicely, at a much higher rate than needed, but then something happens and I start losing samples.

My questions are as follows:
1) What do you think happens that I start losing samples?
2) What advice can you give me on getting ADC data out of the device fast enough? What interface would you recommend? I realise what I am currently doing is quite rubbish.
3) Is it possible to stream 6 channels at 100kHz each (600 000 data points, each a adcsample_t (16 bits) = 9.6Mbit) out of the device somehow to a host computer?
4) Any examples or resources you can point me to in regards to synchronous sampling and streaming out the data? Not necessarily ChibiOS specific, just general strategies.

Thank you very much for any help,
Cheers,
-Igor
Attachments
SineSamples.png
Sampled Data
SineSamples.png (53.31 KiB) Viewed 6443 times

User avatar
Giovanni
Site Admin
Posts: 14457
Joined: Wed May 27, 2009 8:48 am
Location: Salerno, Italy
Has thanked: 1076 times
Been thanked: 922 times
Contact:

Re: Advice on getting ADC data out

Postby Giovanni » Sat Feb 07, 2015 10:52 pm

Hi,

It is not just matter of "fast enough", you also have to synchronize your thread with the output of the ADC.

The ADC driver uses callbacks for this, a callback is called twice for each conversion, the first time when the first half buffer is filled, the second time at the end of the second half then the conversion restarts.

This is done in order to allow you to "process" half buffer while the other half is being filled.

There are several approaches you could use:
1) Signaling a semaphore from the callback. Waiting on the semaphore in the thread.
2) Setting a flag from the callback. Polling on the flag in the thread (not elegant nor efficient).
3) Queing the ADC data from the callback into a MailBox, the thread would read the other side of the mailbox (this introduces also a buffering mechanism, I think it is the preferred approach).

Giovanni

IgorEE
Posts: 21
Joined: Fri Nov 14, 2014 4:40 pm

Re: Advice on getting ADC data out

Postby IgorEE » Mon Feb 09, 2015 2:26 pm

Hi Giovanni,

Thank you for your reply and information. I didn't post earlier as I took some time to run some tests and read up a little on mailboxes as I am not familiar with such RTOS constructs (yet).

I have settled on using 10kHz sampling rate.
I have run a test transmitting a buffer of 200 ADC samples over USB and I have measured (with a scope) that this takes just under 5mS (it varies a little bit, but always less than 5mS).

I am thinking of implementing the following strategy - so please advise if I am doing something stupid before I sink a lot of time into trying to get this implemented. Currently, I will speak in terms of a single ADC channel for simplicity.

I have the ADC setup for continuous conversions.
I have a GPT that triggers a callback every 0.1 mS (for a 10kHz sampling rate). In the GPT callback, I will copy the value of interest from the ADC samples array into a mailbox. The mailbox will be able to hold upto 300 ADC samples. I will have a USBComms thread which will run every 20mS. The USBComms thread will need 5mS or slightly longer to fetch all the mailbox messages (200 of them) and chprintf them to the USB comms stream. This way I am collecting samples all the time in mailbox messages and periodically emptying the mailbox via USB Comms.

I fear this might not work though, as the GPT callback which posts the messages will not be running while I am executing the USBComms thread. Is that right? Or will the callback interrupt the USB comms process a number of time during those 5mS that the comms require?

Basically, I am still unsure how to queue up ADC samples in a buffer somewhere, then periodically empty that buffer via USBComms, but keep filling the buffer in the background while the comms are running. By a buffer I mean some kind of FIFO queue ideally.

Ditching the GPT part and using solely the ADC callback would perhaps work better, but then I can not slow down the ADC clock sufficiently to obtain a 10kHz sampling rate. From what I've gathered the minimum the ADC clock can run is 600kHz, which is all right, as I can sample multiple channels, and use various times for ADC_CYCLES for each channel, but I still doubt I can obtain exactly 10kHz sampling rate.

Hence, any more advice or pointers would be much appreciated as I think i'm either overthinking this or just missing something? If required, this is on a STM32F407 device (STM32F4-Discovery kit).
Thanks,
-Igor

User avatar
Giovanni
Site Admin
Posts: 14457
Joined: Wed May 27, 2009 8:48 am
Location: Salerno, Italy
Has thanked: 1076 times
Been thanked: 922 times
Contact:

Re: Advice on getting ADC data out

Postby Giovanni » Mon Feb 09, 2015 2:34 pm

Hi,

Mailboxes are FIFO structures, you post from one side and fetch from the other side.

You don't need GPT callbacks, the ADC driver is able to give you callbacks at proper time (one call for each half-buffer filled).

Giovanni

IgorEE
Posts: 21
Joined: Fri Nov 14, 2014 4:40 pm

Re: Advice on getting ADC data out

Postby IgorEE » Mon Feb 09, 2015 6:24 pm

Hi Giovanni,

Thanks for the support.

I've implemented the passing of samples from the adc callback to the communication thread via mailboxes. I am currently outputting only a single channel via USB CDC driver.

I've managed to achieve a sample rate of over 25kHz and to transmit this over USB CDC to the computer succesfully. I haven't tried to go faster yet, but I think it should be possible. The program below does this.

Code: Select all

/*
    ChibiOS/RT - Copyright (C) 2006-2013 Giovanni Di Sirio

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "ch.h"
#include "hal.h"

#include "chprintf.h"

#include "usbcfg.h"

/* ADC conversion group settings */
#define ADC_GRP_NUM_CHANNELS   6
#define ADC_GRP_BUF_DEPTH      1
/* Mailbox */
#define MAILBOX_SIZE 400

/* Buffer for ADC samples */
static adcsample_t samples[ADC_GRP_NUM_CHANNELS * ADC_GRP_BUF_DEPTH];
static int state = 0;

static Mailbox mb[1];
static msg_t b[MAILBOX_SIZE];

/* Virtual serial port over USB.*/
SerialUSBDriver SDU1;

static WORKING_AREA(waUSBCommsThread, 128);

static msg_t USBCommsThread(void *arg) {
  msg_t msg;
  (void)arg;

  chRegSetThreadName("USBComms");

  while (TRUE) {
    palSetPad(GPIOD, GPIOD_LED4);
    chMBFetch(&mb[0], &msg, TIME_INFINITE);
    chprintf(((BaseSequentialStream *)&SDU1), "%d\r\n", msg);
    palClearPad(GPIOD, GPIOD_LED4);
  }
  return 0;
}

static void adccb(ADCDriver *adcp, adcsample_t *buffer, size_t n) {
  palSetPad(GPIOD, GPIOD_LED6);
  (void)adcp;
  (void)buffer;
  (void)n;
  if (state == 0) {
    chSysLockFromIsr();
    chMBPostI(&mb[0], (msg_t)samples[0]);
    state = 1;
    chSysUnlockFromIsr();
  } else {
    state = 0;
  }
  palClearPad(GPIOD, GPIOD_LED6);
}

/*
 * ADC conversion group.
 * Mode:        Circular buffer, 1 samples of 6 channels, SW triggered.
 * Channels:    IN10, IN11, IN14, IN15, IN8, IN9
 */
static const ADCConversionGroup adcgrpcfg = {
  TRUE,
  ADC_GRP_NUM_CHANNELS,
  adccb,
  NULL,
  0,                        /* CR1 */
  ADC_CR2_SWSTART,          /* CR2 */
  ADC_SMPR1_SMP_AN10(ADC_SAMPLE_56) | ADC_SMPR1_SMP_AN11(ADC_SAMPLE_56) |
  ADC_SMPR1_SMP_AN14(ADC_SAMPLE_56) | ADC_SMPR1_SMP_AN15(ADC_SAMPLE_56),
  ADC_SMPR2_SMP_AN8(ADC_SAMPLE_56) | ADC_SMPR2_SMP_AN9(ADC_SAMPLE_56),
  ADC_SQR1_NUM_CH(ADC_GRP_NUM_CHANNELS),
  0,                        /* SQR2 */
  ADC_SQR3_SQ1_N(ADC_CHANNEL_IN10) | ADC_SQR3_SQ2_N(ADC_CHANNEL_IN11) |
  ADC_SQR3_SQ3_N(ADC_CHANNEL_IN14) | ADC_SQR3_SQ4_N(ADC_CHANNEL_IN15) |
  ADC_SQR3_SQ5_N(ADC_CHANNEL_IN8) | ADC_SQR3_SQ6_N(ADC_CHANNEL_IN9),
};

/*===========================================================================*/
/* Initialization and main thread.                                           */
/*===========================================================================*/

/*
 * Application entry point.
 */
int main(void) {
  halInit();
  chSysInit();
  /*
   * Initializes a serial-over-USB CDC driver.
   */
  sduObjectInit(&SDU1);
  sduStart(&SDU1, &serusbcfg);

  /*
   * Activates the USB driver and then the USB bus pull-up on D+.
   * Note, a delay is inserted in order to not have to disconnect the cable
   * after a reset.
   */
  usbDisconnectBus(serusbcfg.usbp);
  chThdSleepMilliseconds(1000);
  usbStart(serusbcfg.usbp, &usbcfg);
  usbConnectBus(serusbcfg.usbp);

  /* Setup pins for ADC */
  palSetGroupMode(GPIOC, PAL_PORT_BIT(0) | PAL_PORT_BIT(1) |
                         PAL_PORT_BIT(4) | PAL_PORT_BIT(5),
                    0, PAL_MODE_INPUT_ANALOG);
  palSetGroupMode(GPIOB, PAL_PORT_BIT(0) | PAL_PORT_BIT(1),
                    0, PAL_MODE_INPUT_ANALOG);
  adcStart(&ADCD1, NULL);
  adcStartConversion(&ADCD1, &adcgrpcfg, samples, ADC_GRP_BUF_DEPTH);
  /* Initialize Mailbox */
  chMBInit(&mb[0], &b[0], MAILBOX_SIZE);
  /* Create Comms Thread */
  chThdCreateStatic(waUSBCommsThread, sizeof(waUSBCommsThread),
                    NORMALPRIO, USBCommsThread, NULL);

  while (TRUE) {
     chThdSleepMilliseconds(5000);
  }
}


I still do not understand one thing which I would appreciate some help with. I do not understand how is the ADC clock running or how exactly sampling works. Let us take the example program above, and the Stm32f4 DISCOVERY kit.

The SYSCLK is 168MHz
I am using the following option in mcuconf.h:

Code: Select all

#define STM32_ADC_ADCPRE                    ADC_CCR_ADCPRE_DIV4

To my understanding, this means the ADC clock will be 168MHz / 4 = 42 MHz -> 2.3809e-8 seconds per cycle
I have 6 channels that are sampled, each with the option ADC_SAMPLE_56. So for one channel it takes 56 ADC Clock cycles = 56 * 2.3809e-8 = 1.333uS.
Since there are 6 channels, the complete conversion takes 1.333uS*6 = 8uS. The adc callback fires on buffer half full and buffer full, so I will have two callbacks during a full conversion cycle of all channels, one callback after 4uS and the final callback after 8uS.

However, this is not what I am seeing a at all. I have an oscilloscope connected to LED6 to monitor the callback triggering time. The frequency the callback fires at is 51.4735kHz, so the full conversion cycle is running at 51.4735kHz / 2 = 25.736kHz. Attached is the image of the captured data where the input to the ADC is a function generator running at 1kHz. As can be seen, and I have verified this with the data, there are about 25 data points for each period of the input sinewave which matches with the 25.736kHz callback observed on the oscilloscope.

So why is my math above wrong?
Thanks,
-Igor
Attachments
SineSamples.png
SineSamples.png (39.93 KiB) Viewed 6410 times

User avatar
Giovanni
Site Admin
Posts: 14457
Joined: Wed May 27, 2009 8:48 am
Location: Salerno, Italy
Has thanked: 1076 times
Been thanked: 922 times
Contact:

Re: Advice on getting ADC data out

Postby Giovanni » Mon Feb 09, 2015 6:40 pm

Hi,

About your code, I recommend initializing the mailbox before starting the conversion or you could get a callback when the MB is not yet initialized.

About the conversion time, it is not 56 cycles but 56+12, see the RM.

Finally about the buffer, you have a buffer depth of 1, so only the final callback is called. For even values > 1 even the intermediate callback is called. For example for N=8 the callback is called after filling lines 0..3, then again after filling 4..7. The OS is able to handle interrupts even well under 5uS depending on the ISR code weight.

Not sure about the conversion times, the DS states that the maximum ADC clock is 36MhZ.

Giovanni

colin
Posts: 149
Joined: Thu Dec 22, 2011 7:44 pm

Re: Advice on getting ADC data out

Postby colin » Mon Feb 09, 2015 7:25 pm

One other comment regarding sending data at a high rate out of the microcontroller to a host: It would be significantly faster to send the data in raw binary form rather than in ASCII text form. The 16-bit values would be 2 bytes instead of about 6 bytes, so the channel bandwidth requirements would be much lower. Also, having the MCU convert each value to textual form is not an insignificant demand on the MCU processing time as well.

IgorEE
Posts: 21
Joined: Fri Nov 14, 2014 4:40 pm

Re: Advice on getting ADC data out

Postby IgorEE » Mon Feb 09, 2015 7:34 pm

Hi Giovanni,

Thanks for the tips!

I (hopefully) have one final question. As I need to set a precise sampling rate for my application, or at least need to know the exact sampling rate, I need to figure out what the ADCCLK is.

The RM states the ADCCLK is derived from APB2 CLK and the ADC prescaler. So the question is what is the APB2 clock set to in the STM32F4-Discovery examples in ChibiOS? Where is this defined?

Knowing this I can hopefully figure out why my math above does not work.

Thanks!
-Igor
P.S. Yes, thanks for pointing out there are additional 12 cycles per conversion as well!

User avatar
Giovanni
Site Admin
Posts: 14457
Joined: Wed May 27, 2009 8:48 am
Location: Salerno, Italy
Has thanked: 1076 times
Been thanked: 922 times
Contact:

Re: Advice on getting ADC data out

Postby Giovanni » Mon Feb 09, 2015 8:26 pm

Hi,

All the prescaler settings are in mcuconf.h. I suggest to compare the values with the clock tree image in the RM under the RCC section.

The defaults are:

Code: Select all

#define STM32_PPRE1                         STM32_PPRE1_DIV4
#define STM32_PPRE2                         STM32_PPRE2_DIV2


About ADC timings, you could also trigger the ADC using a timer. See the "ADC discontinuous mode", I never tried it personally but it should work.

Giovanni

IgorEE
Posts: 21
Joined: Fri Nov 14, 2014 4:40 pm

Re: Advice on getting ADC data out

Postby IgorEE » Tue Feb 10, 2015 2:41 pm

Hi Giovanni,

Thanks for the help. I looked up the clock config in mcuconf.h and I think I figured out the timing.

For reference, here's what I think is going on: (on STM32F4-Doscovery).
Input external oscillator is 8Mhz so HSE = 8MHz.
It enters the PLL with values set by mcuconf.h as follows: PLLM = /8, PLLN= *336, PLLP=/2
so we have 8Mhz/8 = 1Mhz * 336 / 2 = 168MHz. So SYSCLK is 168MHz
Then APB1 and APB2 prescalers set in mcuconf are /4 and /2 respectively giving us APB1 clock 42MHz and APB2 clock 84 Mhz
ADC clock is derived from APB4 clock and ADC clock prescaler set in mcuconf. In my case, I set the prescaler to /4
so ADC clock is 84MHz / 4 = 21 MHz

I am sampling each channel in 56cycles plus 12 cycles of "setup" time specified in the RM. Hence each channel takes 68 cycles to convert. There are 6 channels so 68*6 = 408 cycles for conversion of all channels.
this means 408 * (1/21MHz) = 1.9428e-5 seconds -> 51.470 KHz sampling. On the oscilloscope I see the adc callback firing at 51.473 KHz so that is pretty close. There seems to be a few more undocumented clock cycles somewhere that account for that extra 0.003KHz I am seeing in reality. The ADC callback fires only once as my adc buffer depth is 1 (as mentioned by Giovanni above).

So there, I think that demystifies the timing of the ADC.

Now, I'm gonna implement something that will sample six channels at precise sample rate and write a quick article on it. I hope that's fine with you Giovanni, and I'll post back here when I finish.

Thanks for all the help,
Cheers,
-Igor


Return to “General Support”

Who is online

Users browsing this forum: No registered users and 49 guests