Interesting results from various ADC sample times

This may already have been discussed (in which case moderators, feel free to close it!) But I have been getting some interesting but repeatable results with the A/D converter on my Particle Core. Basically, when changing the sample time with setADCSampleTime, I’m a little confused.
From my reading on the topic of ADCs, the longer the sample time, the higher the input impedance, as a result of the internal charge capacitor on the ADC.

My test rig:

  • A4 shorted to 3.3v (regulator)
  • A5 connected to 3.3v with a 22K resistor; 0.1µF to GND.

ADC_SampleTime_1Cycles5 -> A4: 4095, no noticeable change over the default setting.
ADC_SampleTime_7Cycles5 -> A4: 4095 (default); A5 reads 3870
ADC_SampleTime_28Cycles5 -> A4: 4094. A5 reads 3978 (Better, more what I expected.)
ADC_SampleTime_41Cycles5 -> A4: 4094. A5 reads 3978 (same)
ADC_SampleTime_55Cycles5 -> A4: 3889. A5 reads 3815 (WORSE!)
ADC_SampleTime_71Cycles5 -> A4: 3890 (didn’t check A5)
ADC_SampleTime_239Cycles5 -> A4: 3870!! (ditto)

If I understand the ADC concept correctly, the impedance should go up with the longer sample time, but it appears to fall below zero, as you really can’t go less than a 2" length of wire.
Interestingly, if I remove the 0.1µF capacitor, the A5 readings get closer to the A4 readings. That could be explained by the Particle firmware repeatedly reading the value to get a smoother average (if I understood some other threads correctly), which would drain the capacitor.

I’m aware that some other users have really dug deep into the chip, writing system registers, etc. I’m not looking to do any of that (good luck porting your code to the Photon!), but found this interesting. Any comments? Or am I a little close to upside-down on this topic?

1 Like

@WebDust21, impedance is pain in the butt. This is why I will defer this topic to @BDub and @bko who did so much of the work in characterizing and explaining the Core ADC behavior. :smile:

Unfortunately much of the work I did happened before the newest change which implements the Dual Slow Interleaved ADC mode, AND apparently a 10 sample average. This does change things quite a bit.

Also from what I remember this mode is not effective at higher sample rates than XX… and possibly forcing the sample rate to be higher than XX could be what’s causing the issue. Looks like your results do tend to show this. If you search for my previous replies about Dual Slow Interleaved mode you’ll see some of the calculations.

I would start first by removing the 10 sample average, this should be something the user has the choice to implement. Since this averaging exists now though, maybe roll it into a configurable User API. ( It sounds like this was added as a band-aid to give better results, but just one bad reading will corrupt the whole 10 reading average.

Then re-characterize the readings, taking one sample per second and logging them.

Also, tie your A4 to the 3V3* pin, and A5 through 22k to the 3V3* pin. This is the VDDA (adc reference) of the Core.

I wish I had more time to look at this right now but this is the best I can offer. If someone else can verify that this is truly a bug (or if you insist that it is @WebDust21) , I’ll add it to the firmware issues to be addressed in the near future. Thanks!!

1 Like

I’m not trying to imply that it is a bug…I was just confused as to how much longer sample times resulted in “attenuated results.” 10-sample average, etc. is great; the values were pretty steady (+/- 1 count) during my testing.
I can fully understand attenuated results from SHORTER sample times (but from a short circuit to 3V3* ??), so was just wondering what was going on. I can use shorter sample times and get the full ADC range, so I’m not complaining.

Yes, the “regulator” I was referring to comes from the 3V3* pin on the Core, which is USB-powered from my computer.

I actually do think there is a bug if you can set the sample rate higher and change the readings with A4 shorted to 3V3*. In the case of A4, I would also like to see a 0.1uF capacitor added to the input. Just looking for confirmation of this.

OK. Wire straight from 3V3* to A4. 0.1µF capacitor from A4 to GND. Using (ADC_SampleTime_239Cycles5), and the reading is 3890 counts.

EDIT: I’m assuming that this is a longer (I.E. slower) sample time. I’m reading the ADC every second, so nothing fast. ADC_SampleTime_28Cycles5 works for me (4095 counts when tied to 3V3*, and much better with the resistor.) I’m assuming that that is a shorter (I.E. faster) sample time. Correct me if I’m wrong.

1 Like

@WebDust21, @BDub, here is a great (detailed) app note regarding the STM32 ADC. In section 3.4, High Impedance Source Measurement, the issue of averaging comes up but what may be problematic with ADC averaging is the time between successive samples.

1 Like

@peekay123 I can fully and completely understand low ADC counts from high impedance sources (I.E. 50K resistor). But a wire is as low-impedance as you can get, which is why I’m a little confused.

I’m going to add this as a firmware issue to work through in the near future. I know we can make this better :smile:

@WebDust21, agreed! I just wanted some sort of reference to go by. I agree with @BDub that this needs to be looked at :smile:

I have not looked at this in detail but as a clue for @BDub the numbers that @WebDust21 gets (~3890) can be explained by the 10 sample average code getting 9 samples at 4095 and 1 sample at 2048 (half scale).

We saw some similar problems in the distant past on the Core and the @satishgn updated the averaging code to interact better with the DMA channel. So I would be looking for a timing problem of that type. As @BDub said, turning off the averaging would help debug the problem but will add noise of plus or minus several counts.

Because of the way the switched-capacitor input works (and the diagram in that app note is simplified from what I have seen in the past) the input impedance is not returned to ground at each measurement cycle but to some pre-charged level in the switched capacitor bank. Essentially each bit in the SAR computation has a different input impedance relative to ground. You should plan on driving the input with a source impedance of less than 20k ohms for good results. A lot of folks have had trouble with these inputs and certain sensors, but a good op-amp buffer always fixes the impedance problems in my experience.

1 Like

Hah, that sounds very very familiar :wink:

Your op-amp suggestion saved me from going crazy. After humble beginnings with a lunchtime walk to get a lm358 through hole as an experiment, its progressed to the point of using LM324 in SMD for 4 inputs.

I now think that its quietly the reason why multiple projects have experienced odd readings as different boards (with varying onboard op-amps) have been used.

1 Like

@BDub. I don’t see any issues dating 27 days back on this subject. But if this counts as cross-posting (here and GitHub issues), by all means chew me out :smile:

Basically, after reading through some of the STM32F103 reference manual, it appears that the problem is entirely caused by the exclusive use of dual-ADC slow interleaved mode in analogRead. With that mode, the maximum permissible sample time is 14 ADCCLKs—explaining why selecting 239 ADCCLK sample time doesn’t quite work. With both ADC1 and ADC2 sampling, you have a lower input impedance, not a higher one.

I’m experimenting with changing the ADC clock scaler from 6 to 8 (= from 12MHz to 9MHz), and completely removing all dual slow interleaved mode code. (That rhymes…) Basically, as I understand it, if you have two ADCs alternating on the same input line, you’ve halved your input impedance. AFAIK the only purpose for interleaved mode on the STM32 is for faster sample rates, not higher input impedance. Will update this thread based on my findings.

@BDub, @satishgn, both of your names are in the code file, so I have permission to pester you! Just for good measure, @bko, @ScruffR, @kennethlimcp…the solution to the ADC problem is here!

I totally disabled the slow-interleaved ADC mode in a month-old LATEST build, modifying \core\adc_hal.c:

  • HAL_ADC_Read: Commented out all references to ADC2 in (ADC_RegularChannelConfig, ADC_ExternalTrigConvCmd)
  • HAL_ADC_Read: some optimizations to the sum/divide averaging routine, also removing the ADC_DualConvertedValues[i] >> 16 and the *2 from the final division
  • HAL_ADC_DMA_Init: Changed ADCLK to RCC_ADCCLKConfig(RCC_PCLK2_Div8); (instead of Div6. Don’t think that this is important)
  • HAL_ADC_DMA_Init: Disabled ADC2 clock
  • HAL_ADC_DMA_Init: Set ADC1 Mode to independent (ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;)
  • HAL_ADC_DMA_Init: As it’s no longer interleaved, I had to also enable the continuous conversion mode on ADC1
  • HAL_ADC_DMA_Init: Commented out all ADC2 initialization code.

With the above changes and ADC_SampleTime_239Cycles5, I get the following readings:

  • A4 to 3V3* -> 4095
  • A4 to 3V3* with 330K (no capacitor) -> 4065 (pretty high impedance if you ask me)
  • A4 to 3V3* with a 330K resistor and 0.1µF capacitor -> 4093
  • A0 to 3V3* -> 4084

Experimenting with ADC_SampleTime_1Cycles5:

  • A4 to 3V3* -> 4094
  • A4 to 3V3* with 330K (no capacitor) -> 2700 (looks low-impedance, alright!)
  • A4 to 3V3* with a 330K resistor and 0.1µF capacitor -> 4092
  • A0 to 3V3* -> 4083

Just for proof-of-principle, I replaced “adc_hal.c” with the latest LATEST “adc_hal.c” (making a backup of my changes, of course!) With my system code completely untouched (@ADC_SampleTime_239Cycles5) I get the following:

  • A4 to 3V3* -> 3890 counts
  • A4 to 3V3* with 330K (no capacitor) -> 3714
  • A4 to 3V3* with a 330K resistor and 0.1µF capacitor -> 3888
  • A0 to 3V3* -> alternating 4084 and 3879

And with ADC_SampleTime_1Cycles5…

  • A4 to 3V3* -> 4094
  • A4 to 3V3* with 330K (no capacitor) -> 2642 (ditto)
  • A4 to 3V3* with a 330K resistor and 0.1µF capacitor -> 3888
  • A0 to 3V3* -> 4083

I haven’t dug through the firmware to see if the Core’s A0 translates to PA0 on the STM32, but there is a documented nonfixable problem in the STM32F103 errata regarding a “glitch” on A0, as follows:

FWIW, I’m checking A0, A1, A3, A4, and when not connected to anything with the unmodified firmware they read as follows:

  • with ADC_SampleTime_239Cycles5: 4, 1823, 1986, 2034
  • with ADC_SampleTime_1Cycles5: 883, 2069, 2095, 2145

I don’t recall hitting A0 with a sledgehammer (and this is a different Core than I was using in the opening post, too!)

I hereby submit that the dual slow-interleaved mode should be completely removed from the official firmware. It appears to be the source of the problem documented in the opening post.

Oh, and by the way, removing all that code? It results in a slightly smaller firmware file. :wink:


I did all of my rigorous testing before dual slow interleaved mode and liked the results back then as well. You have my vote to revert the ADC mode :wink: I don’t believe it was DMA driven before, so your modifications that keep it DMA driven may be worthwhile. Do you want to wrap up your changes in a PR so that we can more easily test/compare the changes? Thanks for digging into this @WebDust21!

1 Like

What is a PR? I’m pretty sure it isn’t public relations :stuck_out_tongue_winking_eye:.

Here’s my modified copy of the last two subroutines in firmware-latest\hal\src\core\adc_hal.c

 * @brief Read the analog value of a pin.
 * Should return a 16-bit value, 0-65536 (0 = LOW, 65536 = HIGH)
 * Note: ADC is 12-bit. Currently it returns 0-4096
int32_t HAL_ADC_Read(uint16_t pin)

  if (adcChannelConfigured != PIN_MAP[pin].adc_channel)
    HAL_Pin_Mode(pin, AN_INPUT);

  if (adcInitFirstTime == true)
    adcInitFirstTime = false;

  if (adcChannelConfigured != PIN_MAP[pin].adc_channel)
    ADC_RegularChannelConfig(ADC1, PIN_MAP[pin].adc_channel, 1, ADC_Sample_Time);    // ADC1 regular channel configuration
    adcChannelConfigured = PIN_MAP[pin].adc_channel;                    // Save the ADC configured channel

  DMA_SetCurrDataCounter(DMA1_Channel1, ADC_DMA_BUFFERSIZE);    // Reset the number of data units in the DMA1 Channel1 transfer
  DMA_Cmd(DMA1_Channel1, ENABLE);                // Enable DMA1 Channel1
  ADC_DMACmd(ADC1, ENABLE);                    // Enable ADC1 DMA
  ADC_SoftwareStartConvCmd(ADC1, ENABLE);            // Start ADC1 Software Conversion

  // Wait for the 10-sample conversion to finish.
  while(!DMA_GetFlagStatus(DMA1_FLAG_TC1));    // Wait for DMA1 to finish the transfer

  // DMA done.  Clean up.
  DMA_ClearFlag(DMA1_FLAG_TC1);            // Clear the DMA1 "finished" flag
  ADC_DMACmd(ADC1, DISABLE);            // Disable ADC1 DMA trigger
  DMA_Cmd(DMA1_Channel1, DISABLE);          // Disable DMA1 Channel1

  uint32_t ADC_SummatedValue = 0;
  for(int i = 0; i < ADC_DMA_BUFFERSIZE; i++)
    ADC_SummatedValue += ADC_DualConvertedValues[i] & 0xFFFF;    // Retrieve the ADC1 converted value and add to ADC_SummatedValue

  // Return ADC averaged value
  return (uint16_t)(ADC_SummatedValue / ADC_DMA_BUFFERSIZE);

 * @brief Initialize the ADC peripheral.
void HAL_ADC_DMA_Init()
  //NOT using slow interleaved mode. . .to achieve higher input impedance.

  ADC_InitTypeDef ADC_InitStructure;
  DMA_InitTypeDef DMA_InitStructure;

  RCC_ADCCLKConfig(RCC_PCLK2_Div6);        //=12MHz ADCCLK; slowest is Div8 -> 9MHz ADCCLK

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);    // Enable DMA1 clock
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  // Enable ADC1 clock. 

  // DMA1 channel1 configuration
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)ADC1_DR_ADDRESS;
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADC_DualConvertedValues;
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
  DMA_InitStructure.DMA_BufferSize = ADC_DMA_BUFFERSIZE;
  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);

  // ADC1 configuration
  ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;    //NOT ADC_Mode_SlowInterl
  ADC_InitStructure.ADC_ScanConvMode = DISABLE;
  ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;    //Has to be enabled for INDEPENDENT mode to function
  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  ADC_InitStructure.ADC_NbrOfChannel = 1;
  ADC_Init(ADC1, &ADC_InitStructure);
  // If ContinuousConvMode is disabled in Independent, you'll only get one conversion.
  //Slow Interleaved mode does a continuous conversion regardless of the setting of this bit.

  ADC_Cmd(ADC1, ENABLE);      // Enable ADC1
  ADC_ResetCalibration(ADC1);   // Enable ADC1 reset calibration register

  // Check the end of ADC1 reset calibration register

  // Start ADC1 calibration
  while(ADC_GetCalibrationStatus(ADC1));  // Check the end of ADC1 calibration


@WebDust21, PR = pull request in

@wesner0019, @BDub…which branch should I make the pull request in? LATEST, DEVELOP, MASTER…???

EDIT: I’m utterly confused as to how GitHub works.

If you’re up for it please create a PR against the develop branch :sunglasses:

latest will probably merge just fine as well, if you don’t want to navigate the extra stuff required for develop.

Can you tell me how many feet there are, which foot goes in front of the other one, with what sequence, which angle, which weight, and at what time? I am absolutely, completely and totally confounded as to how to use GitHub.

To answer…no, I’m not up for it.