STM32 ADC+DMA+ITC+Усреднение

Происходит плановая доработка железяки и разборки с АЦП в STM32, хочется использовать DMA и написать простой код. А также имеет место импульсное питание и собственно показания скачут, т.е. нужно все усреднять желательно по нескольким десяткам значений на каждое измерение.

Сигналы медленно изменяющиеся (считаем что сопротивление терморезистора в помещении — десятки секунд), но требующие определенной точности измерений.
Сигналов естественно несколько, а именно 8.
Идея такая имеем переменную uint32 в которую тупо суммируем некоторое количество сэмплов АЦП (они у нас до 4095), а потом делим на количество сэмплов и получаем искомую величину, т.к. считаем, что сигнал медленный то данные с АЦП адекватны, хотя попадают они в буфер не последовательно с одного канала, а сначала одна пачка со всех, следующая итд.


//ноги и клоки настраиваются в другом месте сейчас это не сильно принципиально

//глобальные переменные
#define ADC_CH 8  //количество каналов
#define OVER_SAMPL 32 //количество семплов для усреднения
uint32_t ADC_SUM[ADC_CH]; //буффер для суммирования 
uint16_t curr_sample;//считаем количество семлов
uint16_t AI[ADC_CH],ADC_VAL[ADC_CH];//АI- финальный буфер, ADC_VAL- в нее пишем DMA

//Настраиваем АЦП на работу в режиме DMA с прерыванием по Transfer complete
void Setup_ADC1(void)
{

	//==Definitions==
	#define ADC1_DR_Address    ((uint32_t)0x4001244C)


	NVIC_InitTypeDef  NVIC_InitStructure;
	DMA_InitTypeDef DMA_InitStructure; //Variable used to setup the DMA

	ADC_InitTypeDef ADC_InitStructure;

	//--Enable DMA1 clock--
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

	//==Configure DMA1 - Channel1==
	   DMA_DeInit(DMA1_Channel1); //Set DMA registers to default values
	   DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address; //Address of peripheral the DMA must map to
	   DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) & ADC_VAL; //Variable to which ADC values will be stored
	   DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	   DMA_InitStructure.DMA_BufferSize = ADC_CH; //Buffer size (8 because we using 8 channels)
	   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;


	   //Настройка Прерывание -по окончании трансфера

	   NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn ;
	   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	   NVIC_Init(&NVIC_InitStructure);

	   DMA_ITConfig(DMA1_Channel1, DMA1_IT_TC1, ENABLE);
	   DMA_Init(DMA1_Channel1, &DMA_InitStructure); //Initialise the DMA


	   //Настройка параметров  ADC1 - Channel 0 -7

	      ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	      ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	      ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	      ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	      ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	      ADC_InitStructure.ADC_NbrOfChannel = ADC_CH; //We using 8 channels

	      ADC_Init(ADC1, &ADC_InitStructure); //Initialise ADC1

	      //Порядок оцифровки

	      #define ADC_SampleTime ADC_SampleTime_239Cycles5
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_9, 1, ADC_SampleTime);
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 2, ADC_SampleTime);
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_15, 3, ADC_SampleTime);
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_14, 4, ADC_SampleTime);
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_7, 5, ADC_SampleTime);
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 6, ADC_SampleTime);
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 7, ADC_SampleTime);
	      ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 8, ADC_SampleTime);


	      ADC_DMACmd(ADC1, ENABLE); //Enable ADC1 DMA
	      ADC_Cmd(ADC1, ENABLE); //Enable ADC1


	       //Калибровка ADC1

	         //Enable ADC1 reset calibaration register
	         ADC_ResetCalibration(ADC1);
	         while (ADC_GetResetCalibrationStatus(ADC1)); //Check the end of ADC1 reset calibration register

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


}



Где нито в main стартуем наш АЦП и прерывания


   ADC_SoftwareStartConvCmd(ADC1, ENABLE);
   DMA_Cmd(DMA1_Channel1, ENABLE); 


В прерывании суммируем наши показания и инкрементируем curr_sample до получения OVER_SAMPL, если достигаем ее все выключаем и готовы отдать данные


void DMA1_Channel1_IRQHandler(void)
{
   DMA_ClearITPendingBit( DMA1_IT_TC1);
   DMA_ITConfig(DMA1_Channel1, DMA1_IT_TC1, DISABLE);

   if(curr_sample<OVER_SAMPL)
   {
   ADC_SUM[0]+=ADC_VAL[0];
   ADC_SUM[1]+=ADC_VAL[1];
   ADC_SUM[2]+=ADC_VAL[2];
   ADC_SUM[3]+=ADC_VAL[3];
   ADC_SUM[4]+=ADC_VAL[4];
   ADC_SUM[5]+=ADC_VAL[5];
   ADC_SUM[6]+=ADC_VAL[6];
   ADC_SUM[7]+=ADC_VAL[7];
   }

   curr_sample++;

   DMA_ITConfig(DMA1_Channel1, DMA1_IT_TC1, ENABLE);

   if(curr_sample>=OVER_SAMPL)
   DMA_ITConfig(DMA1_Channel1, DMA1_IT_TC1, DISABLE);

}



И где то раз 300 в секунду у меня вызывается промежуточная обработка результатов -по сути усреднение.
Если мы готовы отдать данные то обрабатываем их и заново разрешаем прерывания.


void ADC_AVERAGING(void)
{
 int16_t i;

 if(curr_sample>=OVER_SAMPL)
 {
    for(i=0;i<ADC_CH;i++)
   {
   //averaging
   AI[i]=(AI[i]*2+(ADC_SUM[i]/OVER_SAMPL))/3;
   ADC_SUM[i]=0;
   }
   curr_sample=0;
   DMA_ITConfig(DMA1_Channel1, DMA1_IT_TC1, ENABLE);

 }

}



Данные из массива AI уходят на каналы аналоговых входов и еще раз усредняются уже раз в секунду при выдаче окончательного результата.

Целью данного решения было вынести все ожидания за скобки т.е. воспользоваться DMA и сделать из говна конфетку получить приемлемые результаты на не очень чистом импульсном питании с простыми процедурами обработки. Если имеется более простое решение или косяки то велкам!
  • +1
  • 23 января 2013, 19:02
  • GYUR22

Комментарии (40)

RSS свернуть / развернуть
Нужно запихнуть под кат все листинги.
0
спасиб забыл чето
0
AI[i]=(AI[i]*2+(ADC_SUM[i]/OVER_SAMPL))/3;
А компилятор догадался заменить это сдвигом?
0
хз я экспериментировал с весом переменных и не всегда все можно заменить сдвигом, да и не обязательно в данном случае
0
Я как раз примерно это выложил недавно :-) примерно те же цели — термисторы, помещения… :-) Еще переменную составляющую считает (RMS)
0
Как вариант, можно просто с помощью ДМА пихать все измерения в длинный массив (8*32 в конкретном случае), а усреднение производить в один проход в основном потоке.
0
Как запихать это в длинный массив только с помощью DMA?
0
ДМА в режиме автоматического рестарта по сигналу от АЦП с длинной буфера 8*32, а не 8. У АЦП аналогичный режим (Continious mode). Единственный неприятный момент — данные постоянно обновляются. Но если не делать именно фильтрацию, а усреднение, то это нормально.
0
дык он все равно переписывает данные то автомтически, а не складывает?
0
Я не говорил, что он складывает. Смысл один раз в прерывание войти, а не 32 раза.
0
Инкрементировать указатель?
0
В двух вложенных циклах, или одним как из обработки прерывания в коде выше.
0
Я даже дважды за одно заполнение буфера считаю — чтобы не суммировать «по живому» — half-transfer и transfer-complete.
0
А АЦП относительно аналогового питания измеряет?
Тогда не мешало бы еще внутренний REF измерять и относительно него калибровать значения полученные. Иначе все описанные телодвижения с усреднением не имеют никакого смысла — питание чаще всего сильно разнится на всем температурном диапазоне.
При калибровке относительно внутреннего REF даже без усреднения получаются достаточно точные измерения, прыгает не сильно.
0
У меня были какие то смутные проблемы с REF пока не морочился с ним еще раз, но измерения сопротивления через делитель/подтяжку не зависит от напряжения. Измерение напряжения да зависит, но по предварительным экспериментам (с подогревом стабилизатора) может устроить и точность без REF.
Покажите свое решение если не трудно.
0
ADCout (uint) — уже откалиброваные данные (которые надо получить)
ADCin (uint)- сырые данные
ADCref (uint)- измеренный внутренний референс. Он на 17м канале сидит вроде бы.

А дальше следим за руками:
1. ADCin = (Vin/Vdd)*4095 (измерение относительно питания, 12 бит)
2. ADCref = (Vref/Vdd)*4095 (Vref смотрим в даташите, не помню сколько)
3. Выведем Vin:
Vin = Vref*ADCin/ADCref
4. Но это бесполезные в целочисленной арифметике вольты. А нам нужны попугаи:
ADCout = (Vin/Vdd) * 4095 (эмулируем АЦП )))) )
ADCout = (ADCin * (Vref*4095/Vdd) )/ ADCref
5. Обозначим K = Vref*4095/Vdd (коэффициент типа uint)
6. Итоговый вариант:
ADCout = ((u32)ADCin * K)/ADCref;
K — какой коэффициентугодно можно. Но для красоты и предсказуемости данных я его высчитываю относительно номинального напряжения питания.

Как уже было подмечено, калибровку делаем после каждого измерения. А потом хоть заусредняйтесь.
Еще момент (было в сообществе кстати), на точность измерения очень сильно влияет сэмплинг тайм. На ваш вкус. Но для рефа мы делаем не менее 28.5 циклов тактирования АЦП.
Как-то так
0
Я вспомнил — у меня была засада с REF потому что он себя вел непредсказуемо при включении PWM каналов — скакал аки сайгак, а вообще у меня на начальном этапе была подобная идея, только как мне кажется время между измерениями опоры и каналов может пройти немало и можем получить совсем нето.
0
У нас stm32f103. Выборка 70 кГц — за это время опрашивается два канала по 7.5 циклов, 1 канал за 41.5 циклов и ref за 28.5 циклов. И всё ништяк.
0
А влияет зараза все что можно…
1. Если мерять рядом на каналах 200ом и 100к то при втыкании-вытыкании первого при частоте CPU24 / ADC24/4 и цикле зарядки 77 — показания 100к ощутимо скачут
2. у меня плата с 18мкм фольгой — есть подозрение что это сильно влияет на пульсации по питанию (видно осцилографом)
3. Компьютерный USB осцил ваще сума сходит только бешенные пулсации 30-60мв (там не должно быть просто в таком случае никаких измерений ) при подключении к земле и замыкании на землю щупов всего в паре сантиметров друг от друга. Приходится мерять Velleman PDS10 — но у него вероятно не хватает скорости ацп для точной картины
0
Ах да, у нас система: выпрямитель на 48 вольт, 1 кВт. Два преобразователя на борту: PFC и полумост. То есть ШИМа там от души )))
0
По моему, ты где-то потерял объявление ADC_VAL. Плюс, неясно, что такое AI — в отличие от остальных переменных она не прокомментирована.
Я правильно понимаю, что вся вон та портянка инициализации настраивает DMA на копирование в массив ADC_VAL значений из АЦП, по одному для каждого канала, а прерывание вызывается по завершению этого копирования и переносит результаты из ADC_VAL в ADC_SUM?
0
  • avatar
  • Vga
  • 24 января 2013, 08:32
Да именно так.
Переменную поправил и прокомментировал.
0
вполне логично, почти всегда делаю. но есть нюансы:
1. период измерений желательно делать кратным 20мс, т.е. в вашем случае 320+-0,1мс, тогда хорошо отфильтровывается помеха 50Гц.
2. при необходимости можно расширить диапазон ацп, т.е. если просуммировать 16 измерений, а потом поделить не на 16, а на 4, получим 14-битный результат. Теорию ищем по ключеовому слову «Enhancing ADC resolution by oversampling», работает при наличии белого шума.
3. если измерения абсолютные и нужна хорошая точность, а питание не очень стабильное, тогда сначала вычисляю значение питания для каждого сэмпла, потом корректирую КАЖДЫЙ сэмпл, потом все суммирую. если нужно поточнее, тогда на один из каналов внешний хороший опорник, хотя обычно достаточно внутреннего.
0
  • avatar
  • AVF
  • 24 января 2013, 11:00
Только там реально шум надо самому делать и подмешивать к основному сигналу. Если на входе АЦП стабильно висит сигнал равный, например, 122.2 отсчетам АЦП (а мы будем видеть 122), то сложение пяти значений не увеличит точность
0
АЦП ж вроде и сам шумит, и довольно неплохо — обычно на пару-тройку единиц. Не помню только, можно ли этот шум для оверсэмплинга использовать, но вроде таки можно.
0
Пару-тройку лет назад поляк из СТМ рассказывал, что поднимал точность измерений АЦП на их камне до честных 20 бит с помощью подмешивания сигнала, генерируемого ЦАП. По-моему про пилу речь шла. А вот собственному шуму АЦП я бы не стал доверять. Может быть и зря
0
По-моему про пилу речь шла.
Логичней тогда уж использовать режим, когда АЦП псевдослучайный шум генерирует. Вроде есть такой.
0
а ссылки не осталось?
0
Не знаю про поляков, а вот у производителя AN2668
Improving STM32F101xx and STM32F103xx ADC resolution by oversampling
0
Идея такая имеем переменную uint32 в которую тупо суммируем некоторое количество сэмплов АЦП (они у нас до 4096), а потом делим на количество сэмплов и получаем искомую величину
Я бы посоветовал сделать немного иначе:
1)записать результаты 8 измерений
2)найти 2 максимальных и 2 минимальных значения и «выбросить» их из массива
3)считать среднее арифметическое для оставшихся значений
4)profit!
0
Отбросить max и min значения — очень даже правильно. Но с учетом того, что рядом киловаттный импульсный преобразователь стоит — 8 измерений может не хватить. Я в похожей ситуации остановился аж на 32-х с отбрасыванием 10 минимальных и 10 максимальных значений. И то не всегда хватало: проложат монтажники кабеля по другому — и все, прощай точность. Использовался Silabs-овский C8051F040.
0
Медианный фильтр напоминает.
0
Он самый)
0
Для какого это STM32?
Меня F407 интересует.
0
Делал для 100,103,
а что должно сильно изменится для остальных?
0
Лично не проверял, но много сообщений, что F1x сильно отличается от остальных.
0
AI[i]=(AI[i]*2+(ADC_SUM[i]/OVER_SAMPL))/3;
А не пугает такая медленная сходимость?
Если ADC_SUM вдруг станет 0, то даже за 10 усреднений AI уменьшится всего в ~57 раз.
Я правда не понял как часто делается такое усреднение, если это делать 300 раз в секунду, то нормально, но если 1 раз/сек, то уже как-то…
0
У меня в проекте делается 300 раз в секунду, но мои процессы они медленные секунды -десятки секунд поэтому можно было и реже, но так уж получилось сел на хвост уже имеющемуся событию — обновлению семисегментного дисплея.
0
что ж вы делаете, содомиты:)?
adc1_dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.