Interesting results from various ADC sample times

@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:

5 Likes

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_GPIO_Save_Pin_Mode(PIN_MAP[pin].pin_mode);
    HAL_Pin_Mode(pin, AN_INPUT);
  }

  if (adcInitFirstTime == true)
  {
    HAL_ADC_DMA_Init();
    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_DeInit(DMA1_Channel1);
  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
  while(ADC_GetResetCalibrationStatus(ADC1));

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

}
```

@WebDust21, PR = pull request in https://github.com/spark/firmware

@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.

Ok no worries @WebDust21 your code posted above should be pretty easy to insert locally and test. If you’d like to learn more about Git and Github, there are literally a bazillion videos on youtube. You can also read the various official docs (harder).

This thread started recently and I have high hopes that we can get more folks up-to-speed on github.

1 Like

What is the status of this improvement? I am trying to use the photon for power measurement with a current sensor and I am getting +/- 20 counts of “noise”. I know all about decoupling caps and input impedance but I’d really like to try a version with this dual interleaved mode turned off and where a single call to A/D sample doesn’t execute 10 samples.

Since I’m using a photon instead of a core I’m not sure how to make this firmware change.

I would also like to be able to set ADC mode for Photon as described here: Why dual slow interleaved?

I am experiencing issues with ADCs when combined with SPI use on pins A5, A4, A3 and A2 for chip select - would the conclusion of this thread provide a reason as to why the analogReads I see for AC current sampling are all over the place and perhaps a solution? At the moment I am exploring an I2C external ADC.

Has this fix been implemented…There are probably 100 posts indicating that the ADC isn’t accurate due to the implementation. Some folks have fixed it by building localy but their changes are not available on the IDE. I just tested my code again (building with the IDE) and I’m still getting 20 counts of noise.

Honestly if you need an accurate ADC I think you’ll have to either build locally or buy an external ADC. Since I can’t get the code to build locally I’ve given up several times.

That is 0.4% (or ±0.2%) which I’d say is not bad considering possible influences as DC quality, temperature, signal to be measured, measuring setup (saying this not knowing your setup).

Measuring analog signals properly with higher precission is more involved than just picking a sample.
It needs a fair bit of statistics too.

BTW, have you checked the output signal of your sensor with an oscilloscope?

1 Like

I’m using analogRead() to check the output of a variable 5k resistor (one turn pot) that seems to be a little wonky.
I am using a proton system firmware version 0.6.2, programming via the Web based IDE. I am flashing the proton several times a day while fine tuning my code. My Particle Reference Documents still support the ability to adjust the ADC sample times.

What is the end result of this “Interesting results…” conversation? Has the subsequent firmware updates addressed this Issue?

Hi @jack23233

Maybe you should show us your wiring (photos are great) and your code and we can help more. I have tried the various samples times and got reasonable results.

Just as a start, your 5k variable resistor needs to be configured like a voltage divider with +3.3V on one leg, ground on the other, and the ADC input connected to the wiper.

You also have to understand that the on-chip ADC is an environment with lots of potential noise from the WiFi radio and the ADC does not have a separate clean power supply. I see +/- a few counts out of 4096 due to these kinds of issues. So the ADC works great for most purposes but if you are looking for super accurate measurements you might have to add an external ADC and power supply conditioning. Long wires or wires over the antenna and other problems can hurt performance and so you have to be a bit careful.

Thanks for responding, BKO.
First, my wiring is set up just as you describe. I’ve used this in other setups with the Spark, Photon, arduino with “success”.
I’m using this to track the position of a solar panel, and drive a 12vdc motor to move the panel to the ‘correct’ position. I read the pot, drive the motor a bit, read the pot. And repeat as needed, then goto sleep for 30 minutes, and repeat the process.

Occasionally, the motor would drive the wrong way. Checking the serial.prints, my reads would show counts being inconsistent with expectations. That led to my search of the forum.

Your comments seem to indicate that the problems in this thread have been addressed.
Right now, my project is a messy hack on my office desk. :flushed: however, after setting up a second photon and making a test setup, I found the same issue, and ‘fixed’ it by reading 3 times for an average, with 250msec between reads. I’ve incorporated the fix in my project code,thus avoiding the sample rate issue.

As aside note, there appears to be no examples of how to issue a sample rate adjustment in the reference docs.

So far, my project is performing as expected.

Hi @jack23233

Are you using the ADC with the default sample rate? Or do you change it for your readings?

I do not think many people have played with the ADC settings and they were provided in the firmware because a few people wanted them and it was easy for Particle to provide. I just checked github and there are no currently open bugs in this area.

A lot of the “problems” with the ADC actually related to the input on the ST-Micro ARM chip being unbuffered and having a relatively low input impedance. These problems are easily fixed with an op-amp in my experience. You have to drive the chip with a source impedance of less than around 20k ohm to get good readings.

At the time I tried using the various sample rates, I was trying to get a faster set of readings but then @rickkas7 wrote some nice code that did that better and I used his instead. I remember making a spreadsheet with all the settings and some test values but I’m sure I don’t have it anymore.

Are you sure to only read the ADC when your motor is not moving? That could be another source of noise. Conducted noise and droop on the power supply directly affects the ADC readings, so that would be the first thing I would look at. RF shielding and wire routing would be next.

I am using the default sample rate.

When I wrote my project code, I was somewhat aware and concerned about the noise and “problems” with ADC from my previous projects, and from reviewing other forum comments. I was aware that my read/drive cycling could be suspect. That led me to investigating the Particle Reference and Forum. This “Interesting results…” thread was both scary and reassuring at the same time.

I do not need fast, just consistency. I am confident that my fix of 3 reads averaged and delays between reads will provide the stability needed for this backyard project.

As I move from prototype to installation, I will be sure to keep your comments in mind.

Thank you for your assistance.

1 Like