Reading multiple analog pins simultaneously

Hello,

I'm wondering if it is possible to read two analog pins simultaneously. The reason is that I would like to capture instantaneous power being transmitted on a given conductor by measuring the voltage of that conductor and the current passing through it simultaneously.

Presumably, if I take those readings sequentially it will be a pretty good approximation of the actual instantaneous power, however if possible I'd like to measure them at the same time.

Is this possible?

From ST Microelectronics's STM32F205RGT6TR ARM Cortex M3 microcontroller datasheet: (page 40)

Additional logic functions embedded in the ADC interface allow:
• Simultaneous sample and hold
• Interleaved sample and hold

It seems that the chipset is able, but does the Particle firmware support simultaneous analog reading?

This pint goes to the first person to answer: :beer:

Firmware would support it but not Wiring.

You can still use the bare metal STM32F2xx functions/commands/macros

2 Likes

Here’s some example code to do that. What it does is use hardware ADC1 and ADC2 to sample two different signals at exactly the same time. It also uses a hardware timer and DMA to sample the data continuously. The example program samples the values 1000 times per second but to avoid overflowing the serial port, takes the mean of each group of 100 samples and prints that to serial. You could do other things like find the max for short current spikes, even do an RMS calculation for AC signals, if you wanted to.

#include "Particle.h"

//
// ADCDMA - Class to use Photon ADC in DMA Multi Mode
//
#include "adc_hal.h"
#include "gpio_hal.h"
#include "pinmap_hal.h"
#include "pinmap_impl.h"


// This is actually 60 (well, 59) to divide the 60 MHz system clock down to 1 MHz
// via the prescaler. Since we use TIM3, the timer is a 16-bit timer so this allows
// the sampling rate to go a little lower.
const uint16_t TIM_PRESCALER = (uint16_t)(60000000UL / 1000000UL) - 1;

// This is how fast to sample the ADC in Hz. The minimum is about 15 Hz unless
// you adjust the TIM_PRESCALER, then you can go lower.
const uint32_t SAMPLING_FREQ_HZ = 1000UL;

// This is the timer period passed to the hardware; it needs to be 0 - 65535.
// The 1000000UL constant is the frequency the prescaler scales to.
const uint16_t TIM_PERIOD = (uint16_t) (1000000UL / SAMPLING_FREQ_HZ) - 1;

// The sample buf is uint16_t samples. Two samples are taken each time the timer fires
// (SAMPLING_FREQ_HZ), one from each ADC. A double buffering system is used, so samples
// are buffered up using DMA then a flag is triggered for the buffer to be handled from
// loop. You want to buffer to be twice the number of samples you want to handle at each
// processing.
//
// For example, I wanted to output data 10 times per second so the serial port wouldn't
// overflow. Since we're sampling 1000 times per second, I would average 100 samples and
// display the average. So the buffer needs to be 400 samples long.
const size_t SAMPLE_BUF_SIZE = 400;

// These are the two pins to sample
const int SAMPLE_PIN_1 = A0;
const int SAMPLE_PIN_2 = A1;


uint16_t samples[SAMPLE_BUF_SIZE];

//
//
//
class ADCDMA {
public:
	ADCDMA(int pin1, int pin2, uint16_t *buf, size_t bufSize);
	virtual ~ADCDMA();

	void start(size_t freqHZ);
	void stop();

private:
	int pin1;
	int pin2;
	uint16_t *buf;
	size_t bufSize;
};

// Helpful post:
// https://my.st.com/public/STe2ecommunities/mcu/Lists/cortex_mx_stm32/Flat.aspx?RootFolder=https%3a%2f%2fmy%2est%2ecom%2fpublic%2fSTe2ecommunities%2fmcu%2fLists%2fcortex%5fmx%5fstm32%2fstm32f207%20ADC%2bTIMER%2bDMA%20%20Poor%20Peripheral%20Library%20Examples&FolderCTID=0x01200200770978C69A1141439FE559EB459D7580009C4E14902C3CDE46A77F0FFD06506F5B&currentviews=6249

ADCDMA::ADCDMA(int pin1, int pin2, uint16_t *buf, size_t bufSize) : pin1(pin1), pin2(pin2), buf(buf), bufSize(bufSize) {
}

ADCDMA::~ADCDMA() {

}

void ADCDMA::start(size_t freqHZ) {

	// Using Dual ADC Regular Simultaneous DMA Mode 1

	// Using Timer3. To change timers, make sure you edit all of:
	// RCC_APB1Periph_TIM3, TIM3, ADC_ExternalTrigConv_T3_TRGO

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

	// Set the pin as analog input
	// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
	// GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
	HAL_Pin_Mode(pin1, AN_INPUT);
	HAL_Pin_Mode(pin2, AN_INPUT);

	// Enable the DMA Stream IRQ Channel
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
	TIM_TimeBaseStructure.TIM_Period = TIM_PERIOD; // constant defined at the top of this file
	TIM_TimeBaseStructure.TIM_Prescaler = TIM_PRESCALER; // constant defined at the top of this file
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
	TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // ADC_ExternalTrigConv_T3_TRGO
	TIM_Cmd(TIM3, ENABLE);

	ADC_CommonInitTypeDef ADC_CommonInitStructure;
	ADC_InitTypeDef ADC_InitStructure;
	DMA_InitTypeDef DMA_InitStructure;

	// DMA2 Stream0 channel0 configuration
	DMA_InitStructure.DMA_Channel = DMA_Channel_0;
	DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buf;
	DMA_InitStructure.DMA_PeripheralBaseAddr =  0x40012308; // CDR_ADDRESS; Packed ADC1, ADC2;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
	DMA_InitStructure.DMA_BufferSize = bufSize;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
	DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
	DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
	DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
	DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
	DMA_Init(DMA2_Stream0, &DMA_InitStructure);

	// Don't enable DMA Stream Half / Transfer Complete interrupt
	// Since we want to write out of loop anyway, there's no real advantage to using the interrupt, and as
	// far as I can tell, you can't set the interrupt handler for DMA2_Stream0 without modifying
	// system firmware because there's no built-in handler for it.
	// DMA_ITConfig(DMA2_Stream0, DMA_IT_TC | DMA_IT_HT, ENABLE);

	DMA_Cmd(DMA2_Stream0, ENABLE);

	// ADC Common Init
	ADC_CommonInitStructure.ADC_Mode = ADC_DualMode_RegSimult;
	ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
	ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_1;
	ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
	ADC_CommonInit(&ADC_CommonInitStructure);

	// ADC1 configuration
	ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_NbrOfConversion = 1;
	ADC_Init(ADC1, &ADC_InitStructure);

	// ADC2 configuration - same
	ADC_Init(ADC2, &ADC_InitStructure);

	//
	ADC_RegularChannelConfig(ADC1, PIN_MAP[pin1].adc_channel, 1, ADC_SampleTime_15Cycles);
	ADC_RegularChannelConfig(ADC2, PIN_MAP[pin2].adc_channel, 1, ADC_SampleTime_15Cycles);

	// Enable DMA request after last transfer (Multi-ADC mode)
	ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE);

	// Enable ADCs
	ADC_Cmd(ADC1, ENABLE);
	ADC_Cmd(ADC2, ENABLE);

	ADC_SoftwareStartConv(ADC1);
}

void ADCDMA::stop() {
	// Stop the ADC
	ADC_Cmd(ADC1, DISABLE);
	ADC_Cmd(ADC2, DISABLE);

	DMA_Cmd(DMA2_Stream0, DISABLE);

	// Stop the timer
	TIM_Cmd(TIM3, DISABLE);
}

ADCDMA adcDMA(SAMPLE_PIN_1, SAMPLE_PIN_2, samples, SAMPLE_BUF_SIZE);

// End ADCDMA


void setup() {
	Serial.begin(9600);
	adcDMA.start(SAMPLING_FREQ_HZ);
}

void loop() {
	uint16_t *samplesToProcess = NULL;

	if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_HTIF0)) {
		DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_HTIF0);
		samplesToProcess = samples;
	}
	if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0)) {
		DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_TCIF0);
		samplesToProcess = &samples[SAMPLE_BUF_SIZE / 2];
	}
	if (samplesToProcess != NULL) {
		// There is a sample buffer to process. Because we only can write out samples so fast
		// over serial, we just print the mean of 100 samples here. This would also be good
		// place to check for min, max, median, whatever you wanted to, if you wanted to find
		// out more info about your signal. Or you could even calculate RMS here, for AC signals.
		uint32_t sum1 = 0;
		uint32_t sum2 = 0;

		for(size_t ii = 0; ii < SAMPLE_BUF_SIZE / 2;) {
			sum1 += samplesToProcess[ii++];
			sum2 += samplesToProcess[ii++];
		}

		uint32_t avg1 = sum1 / (SAMPLE_BUF_SIZE / 4);
		uint32_t avg2 = sum2 / (SAMPLE_BUF_SIZE / 4);

		Serial.printlnf("%d,%d", avg1, avg2);
	}


}

I tested it with this potentiometer setup, but obviously it would work with any pair of analog inputs.

5 Likes

Wow, thanks for the detailed response. A lot for me to unpack here… I had to google “DMA” and “IRQ”, to give you an idea of my level of familiarity with the Particle HAL.

How would you suggest I begin understanding what is going on here? I understand that somehow the above code configures direct memory access of the ADC subsystem to the system memory and that enables the ADC to sample continuously while the cpu runs independently (correct?). Then, once the DMA memory that has been allocated (i.e. the main buffer) to storing these samples is full, an interrupt is fired that tells the ADC to start writing to a second buffer? Then the main program loop() (i.e. the CPU) checks to see how big the buffer(s) are and if one of them has been filled up it takes the average of the values stored in the buffer and outputs it to the serial monitor. While this is happening the secondary buffer is simultaneously being filled by the ADC DMA stream.

Is that about the size of it from a 10,000 foot view?

1 Like

That’s basically it; the details are a little different, but conceptually, yes.

Also, this is a really advanced topic, bypassing all of the standard Particle/Arduino-style libraries and writing directly to the hardware STM32F2xx libraries. I don’t recommend doing this unless you have to for a specific use case, like measuring two values at exactly the same time.

A hardware timer is set to trigger the two ADCs at a precise interval. This makes sure they happen at the same time, and regularly, regardless of what the main CPU is doing.

The ADCs are configured to write to a circular buffer in main memory using DMA in auto-increment mode. Each time the timer fires they add a sample. Also, since there are two of them, they’re configured to interleave their results (ADC1, ADC2, ADC1, ADC2) even though they’re sampled at exactly the same time.

The buffer is configured to flag on half-full (and full). This could use an interrupt, however the code actually just polls those flags out of loop. Whenever the data is ready, the code running in the main loop processes half a buffer of samples while the ADC is writing to the other half of the buffer.

I just averaged the samples because I couldn’t output all of the data values fast enough to the serial debug output.

5 Likes

Hello @rickkas7
Excellent example :slight_smile:
Whether this can be used for more than two analog inputs?

Thanks! There is a third ADC on the STM32F205, so I think you should be able to monitor 3 signals at exactly the same time, but that’s it. Also, ADC3 is only connected to A1, A2, and A7 (WKP), so there’s that restriction as well. I’ve never tried it, but as far as I know it should work.

1 Like

This thread is great, I'm learning a lot. I'm looking at the Particle.h and Application.h source files and wondering if these are the only default header files (which in turn #include a whole bunch of header files from Wiring). In other words, is "Particle.h" the only header file that one typically needs to include if we are writing new classes in separate .cpp and .h files that utilize core spark core functionality?

In your example code above, you need to include those HAL libraries because, as @ScruffR said:

Is there some place where I can find a guide to navigating and working with the various folders and files at github.com/spark/firmware and the whole GitHub structure/design flow that spark is implementing? The only thing I've found is the readme file in spark/firmware, which links to various other readme files and describes how to build firmware locally (as opposed to in the cloud).

Rick

I have adapted your code to measure the difference between two pressure sensors in order to quantify the effect of a filter used for infection control. I am sampling at 500 Hz and recording the data to SD card using SdFAT. Thanks.

I currently use a software timer to trigger a/d collection at 200 Hz in our research devices and we see some jitter associated with synching the SdFAT file. I am concerned this is likely to increase when we start transmitting data via 3G for real time monitoring. Hence my interest in your DMA code.

I update a display at a 5 Hz rate so this also lends itself nicely to your double buffered DMA solution.

I have been sufficiently impressed that I am considering replacing my software timer based adc collection with your DMA version. However I only need to sample a single channel.

While I understand the functionality you produced I am not sure about my ability to convert your demo code from dual to single channel. It is the DMA capability that I am after in this instance - something which the standard Particle firmware does not, for understandable reasons, provide.

Would it be possible for you to provide me with a single channel version?

Thanks
Bruce

The Photon Audio Sample 3 does a single channel double-buffered ADC DMA read. It reads at 32000 samples per second and writes out the data by TCP to a local server, but the ADC setup would work for anything. The ADC code would work on the Electron, but the cellular modem isn’t fast enough to transmit at that sample rate.

Thanks. A quick scan and I can see its origins in your earlier code. I should be able to extract relevant adc code from the audio example.

I will also look at the data transfer component a little later. My needs are much less demanding than the audio data rates.

I have been looking at using an Electron or Boron for telemedicine applications since that avoids the need for a patient to enter their wifi credentials. Although we record at 200 Hz we only display data for patients at 5 Hz. I may be able to pack the data into a Particle.Publish message (certainly if we only want a 5 Hz display updated at 1 second intervals).but your technique may be useful for dumping data files once the units are back in the lab.

Thaanks for your assistance.