Why dual slow interleaved?

What is the use case for using dual slow interleaved mode to implement analogRead()?

One of the first things I tried when I got my Spark Core was to connect a TMP36 analog temperature sensor like the one in the Spark Maker Kit. I didn’t use a capacitor.

There are 0.78 volts between the A0 and GND pins according to my digital multimeter. That’s a reading of 28 degrees Celsius (0.78 × 100 - 50) which is warm but conceivable.

But from analogRead() I get the following values:

692 micros 65
332 micros 34
331 micros 34
342 micros 32
331 micros 33
329 micros 32
331 micros 32
331 micros 33
330 micros 36
331 micros 33

… which is only about 0.27 volts (value × 3.3 / 4095). To be precise I guess I should scale by the actual reference voltage (between the 3.3* and GND pins) but regardless these values are way off.

Here’s the code I used to get them:

#include "application.h"

uint16_t ADC_ConvertedValue;

void setup()
{
    Serial.begin(9600);

    while (!Serial.available());
}

void loop()
{
    long start = micros();

    ADC_ConvertedValue = analogRead(A0);

    long end = micros();

    Serial.print(ADC_ConvertedValue);
    Serial.print(" micros ");
    Serial.println(end - start);

    delay(1000);
}

I tried calling the Standard Peripherals Library directly to see what’s going on. analogRead() uses dual slow interleaved mode to take 20 samples (one every 14 ADC clock cycles or 1.17 microseconds) and returns the average.

See ADC modes and their applications section 2.3 and Reference Manual section 11.9.4

Here’s an example of 20 samples:

923
932
910
963
862
889
793
829
716
756
644
680
580
612
519
546
465
490
431
449
micros 26

Here’s the code I used to get them:

#include "application.h"

#define ADC1_DR_Address ((uint32_t) 0x4001244c)

uint16_t ADC_ConvertedValue[20];

void setup()
{
    GPIO_InitTypeDef GPIO_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;

    Serial.begin(9600);

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1 | RCC_APB2Periph_ADC2, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    DMA_DeInit(DMA1_Channel1);
    DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &ADC_ConvertedValue;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = 10;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel1, ENABLE);

    ADC_InitStructure.ADC_Mode = ADC_Mode_SlowInterl;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_7Cycles5);

    ADC_InitStructure.ADC_Mode = ADC_Mode_SlowInterl;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC2, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC2, ADC_Channel_0, 1, ADC_SampleTime_7Cycles5);

    ADC_ExternalTrigConvCmd(ADC2, ENABLE);

    ADC_DMACmd(ADC1, ENABLE);

    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1));

    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1));

    ADC_Cmd(ADC2, ENABLE);

    ADC_ResetCalibration(ADC2);
    while (ADC_GetResetCalibrationStatus(ADC2));

    ADC_StartCalibration(ADC2);
    while (ADC_GetCalibrationStatus(ADC2));

    while (!Serial.available());

    long start = micros();

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);

    while (!DMA_GetFlagStatus(DMA1_FLAG_TC1));
    DMA_ClearFlag(DMA1_FLAG_TC1);

    long end = micros();

    for (int i = 0; i < 20; i++) Serial.println(ADC_ConvertedValue[i]);

    Serial.print("micros ");
    Serial.println(end - start);
}

I tried increasing the sampling time. See How to get the best ADC accuracy sections 2.2.6 and 3.4.3 and these topics:



I figure you might as well use the 13Cycles5 sampling time since dual slow interleaved mode takes samples every 14 ADC clock cycles either way so it doesn’t much change the analogRead() time? analogRead() waits for the last sample, the difference between 7Cycles5 and 13Cycles5 is 0.5 microseconds (6 cycles / 12 MHz) each sample starts at the same time regardless so the last sample doesn’t start for 22.17 microseconds (19 samples × 14 cycles / 12 MHz)

But that didn’t improve the values much. I guess sampling every 1.17 microseconds (14 cycles) is just too fast for this source? (TMP36 without capacitor)

Here’s an example of 20 samples at 13Cycles5:

948
953
899
928
840
870
781
809
727
753
683
704
648
663
619
634
599
609
583
590
micros 26

But if I drop the dual slow interleaved mode I get the following values:

938 micros 4
932 micros 3
931 micros 4
931 micros 3
933 micros 4
931 micros 4
933 micros 3
931 micros 4
932 micros 3
932 micros 3

… which is about 0.75 volts much closer to my multimeter (0.78 volts)

Here’s the code I used to get them:

#include "application.h"

#define ADC1_DR_Address ((uint32_t) 0x4001244c)

uint16_t ADC_ConvertedValue;

void setup()
{
    GPIO_InitTypeDef GPIO_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;

    Serial.begin(9600);

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    DMA_DeInit(DMA1_Channel1);
    DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &ADC_ConvertedValue;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = 1;
    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_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel1, ENABLE);

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_7Cycles5);

    ADC_DMACmd(ADC1, ENABLE);

    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1));

    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1));

    while (!Serial.available());
}

void loop()
{
    long start = micros();

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);

    while (!DMA_GetFlagStatus(DMA1_FLAG_TC1));
    DMA_ClearFlag(DMA1_FLAG_TC1);

    long end = micros();

    Serial.print(ADC_ConvertedValue);
    Serial.print(" micros ");
    Serial.println(end - start);

    delay(1000);
}

So I’m wondering what’s the use case for using dual slow interleaved mode? Because it doesn’t work with the TMP36. (I guess it does work if I add a capacitor but what’s the point?)

Also if I increase the sampling time to 239Cycles5 (the maximum) I get the following values:

961 micros 23
960 micros 23
960 micros 23
961 micros 23
960 micros 23
960 micros 23
960 micros 23
960 micros 23
960 micros 23
959 micros 23

… which is about 0.77 volts even closer to my multimeter (0.78 volts) and still faster than the current analogRead() implementation (23 microseconds for 239Cycles5 vs. 26 microseconds for 20 dual slow interleaved mode samples and > 30 microseconds for analogRead())

1 Like

Dual slow interleaved was an attempt to get the ADC input impedance to be higher, but I’m pretty sure I’ve been over this before somewhere… it doesn’t help more than it hurts? I forget now, it’s been a while. I swear the impedance was higher with the 41.5 cycle time and normal ADC configuration. Definitely use the 0.01uF cap though with the TMP36 sensor.

Hmm why does dual slow interleaved mode theoretically increase the maximum external impedance (RAIN max)?

Why add a capacitor? How can I tell from the datasheet what the TMP36 impedance is? The values you got experimentally with 239Cycles5 sampling time are acceptable. See the graph in the following topic. Thanks for the thorough testing!

See this post... it really doesn't. But the theory was if you need to sample at a fast rate, you are using two A/D converters on the same input in an interleaved fashion which effectively doubles the impedance (or so) that you would get if you used the same sample rate with only one A/D converter.

Adding the capacitor effectively lowers the actual impedance that the ADC "sees". This is especially helpful if the ADC is setup with a very low effective impedance, like it originally was when the Spark Core first shipped.

Eh, somewhere in there is says it's low... I don't think it gives you a number per say. Ah, it's on the first page... just a little blurb that it's low. No concrete numbers. Basically it's up to you to calculate the impedance based on what you hook up to it's output.

Right, but that's a pretty long sample time... and now the maximum sample rate is 14.5 cycles, so 239.5 is achievable.

All that said, I have this unsettled feeling about it all :slight_smile: I'd like to give you some more concrete info, but this is all I can offer currently.

Yeah I haven't read this anywhere else. Dual slow interleaved mode has no effect on the maximum external input impedance (RAIN max). At 7Cycles5 sampling time RAIN max is identical in dual slow interleaved mode and in single conversion mode.

And dual slow interleaved mode precludes sampling at a fast rate because it takes 20 samples and returns the average. It takes samples every 14 ADC clock cycles (regardless) so the final sample doesn't begin for 19 samples × 14 cycles. The final sample takes sampling time (the minimum is 1Cycles5) plus successive approximation or conversion time (12.5 ADC clock cycles 1 cycle per bit see the Datasheet section 5.3.18 and How to get the best ADC accuracy section 1.1) so analogRead() returns values at most every 280 cycles or 23.3 microseconds (280 cycles / 12 MHz)

At least in case of the TMP36 without capacitor dual slow interleaved mode makes accurate values impossible whatever the sampling time because it takes multiple samples 14 ADC clock cycles or 1.17 microseconds apart. The first sample is okay but subsequent samples diminish so that the average is garbage. (I don't know why they diminish. Maybe there's some capacitance in the TMP36 and 1.17 microseconds isn't long enough for it to charge? See How to get the best ADC accuracy figure 36. But I don't know this stuff and speculating will only confuse.)

It's still faster than the current implementation using dual slow interleaved mode. Currently the final sample doesn't begin for 19 samples × 14 cycles which greater than 239Cycles5.

@satishgn please comment. Any insight here?

A good description about the effect of “dual slow interleaved mode” on input impedance can be found on page 11 of this application note: http://www.st.com/st-web-ui/static/active/en/resource/technical/document/application_note/CD00258017.pdf

But in some cases where higher accuracy is required, the concept of oversampling and decimation can be used i.e. oversampling the input signal with the maximum 1 MHz ADC capability and decimating the input signal to enhance its resolution. So using this method the 12 bit resolution can be further enhanced to 15-16 bit thus avoiding a pre-amplifier circuit while measuring low voltage signals :smile: We can implement this as an alternative for those looking for higher resolution conversions.

The ADC on STM32 is quite powerful supporting wide variety of modes but the standard Arduino’s analogRead() API limits the actual use without us providing additional API to take advantage of its advanced feature.

1 Like

Thanks but in that Example of application the increase in maximum external input impedance (RAIN max) comes from the change made to the sampling time (from 1.2 kΩ at 1Cycles5 to 19 kΩ at 13Cycles5) and not from dual slow interleaved mode directly. You get the same increase if you change the sampling time without adding dual slow interleaved mode. Only the sampling time is important.

(The reason you can't use single conversion mode in that specific application is because in it they are reading a 500 kHz signal. With 13Cycles5 sampling time and single conversion mode and 14 MHz ADC clock frequency the fastest they can theoretically sample is 538 kHz which is too slow for that application.)

Ah! I see. For every doubling of the sampling frequency you get an extra 1/2 bit of resolution. So by taking 20 samples you expect to get an extra 2 bits of resolution or 14 bits total (assuming there's sufficient white noise to toggle the input by at least 1/2 LSB and the noise is Gaussian and the signal doesn't vary by more than 1/2 LSB during an oversampling period) see Improving ADC resolution by oversampling

But then you take the average which is only 12 bits. See section 3.2:

Note that normal averaging does not increase the resolution of the conversion because the sum of m N-bit samples divided per m is an N-bit representation of the sample.

If there's currently no advantage to using dual slow interleaved mode for analogRead() then what about removing it so analogRead() can work with circuits where sampling 1.17 microseconds apart is too fast?

For specific applications where there is some advantage, implementing advanced features with an alternative or additional API might make sense. Or in those cases the user could use the Standard Peripherals Library API or just call analogRead() in rapid succession for circuits that satisfy the conditions for oversampling (if you don't also need 13Cycles5 sampling time then this is almost as fast as dual slow interleaved mode)

I submitted a pull request to make analogRead() simply take and return one sample.

  • It’s faster than the current dual slow interleaved mode implementation (even with the maximum sampling time)
  • The maximum external input impedance is greater than the current implementation
  • The resolution is no less than the current implementation (12 bits)
  • It works with circuits where sampling too fast can be a problem like the TMP36

Probably a good idea is to add an additional method:

setADCMode(uint32_t ADC_Mode);

where ADC_Mode can be one of the following:
ADC_Mode_Independent
ADC_Mode_RegInjecSimult
ADC_Mode_RegSimult_AlterTrig
ADC_Mode_InjecSimult_FastInterl
ADC_Mode_InjecSimult_SlowInterl
ADC_Mode_InjecSimult
ADC_Mode_RegSimult
ADC_Mode_FastInterl
ADC_Mode_SlowInterl
ADC_Mode_AlterTrig

So if someone wants to have application specific requirement,
call setADCMode() and/or setADCSampleTime() once in the setup
before repeatedly calling analogRead() in the loop.

If setADCMode() is not called in the setup, analogRead should default to Slow-Interleaved or Independent so as not to break existing user code.

4 Likes

@nottheoilrig, can you please tell us how did you manage to build your code in which you configured manually DMA and ADC?
I’m trying to build it locally but the default make command didn’t recognize the firmware libraries, particularly, the Standard Peripherals Library.

This is a sample of build errors I got:


/Volumes/SSD2/luballe/Documents/eSense/ADCdualMode4/main.cpp: In function ‘void setup()’:
/Volumes/SSD2/luballe/Documents/eSense/ADCdualMode4/main.cpp:16:22: error: ‘RCC_PCLK2_Div6’ was not declared in this scope
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
^
/Volumes/SSD2/luballe/Documents/eSense/ADCdualMode4/main.cpp:16:36: error: ‘RCC_ADCCLKConfig’ was not declared in this scope
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
^

Thanks a lot for your help.

Luis Ballesteros

Hello!

I see that you’re building for the Electron. This post predates the Photon/Electron. I briefly checked this morning, and the code still builds with the latest firmware if you’re building for the Spark Core (make PLATFORM=core) however the Core had a different microcontroller from the Photon/Electron (STM32F103CB vs. STM32F205RG) and I see that the Standard Peripheral Library is also a bit different.

ADC modes and their applications is great because it explains the modes in detail and comes with example code, however AFAICT the code examples (at least) are particular to the STM32F10x. I searched briefly but didn’t find a resource for the STM32F2xx, so I guess you’d need either to port those examples or learn how to configure the STM32F2xx from some other resource(s).

The STM32F2xx Standard Peripheral Library comes with some ADC code examples (STM32F2xx_StdPeriph_Lib_V1.1.0/Project/STM32F2xx_StdPeriph_Examples/ADC/). I briefly checked and they build with the Electron firmware, no problem. There’s also the ADC chapter in the Reference manual (section 10).

Does that help you out?

Hi @nottheoilrig!
Thanks a lot for your time this morning building the source and answering my question.
Effectively when I compile with the PLATFORM=core flag it works. I will follow your suggestions and start working porting the examples.

Your answer really helps me out. As soon as I get any good result I’ll post it.
Thanks again.

Luis Ballesteros

1 Like