Всем доброго времени суток! По запросам читателей блога сегодня будет статья, посвященная обработке и фильтрации данных, а именно данных АЦП, которые мы получили в предыдущей статье. Статья будет чисто практическая - мы возьмем наш проект с АЦП, модернизируем его и посмотрим, чего удастся этим достичь. Сразу же хочу уточнить, что мы сегодня не будем рассматривать разные варианты фильтрации, просто поставим конкретную задачу и рассмотрим конкретное ее решение.
Итак, в упомянутой статье мы реализовали сбор данных с 6-ти каналов АЦП микроконтроллера STM32. Все работает отлично, но есть одно "но" - данные зашумлены... Даже если просто в отладчике посмотреть на значения переменных, становится очевидным, что эти самые значения постоянно меняются, пусть и не в очень значительной степени. Так вот, наша цель на сегодня - стабилизировать показания АЦП. И давайте, задействуем один канал вместо шести - с точки зрения задачи и работы разницы никакой, но для расчетов и разбора так будет прозрачнее.
И сегодня мы воспользуемся очень простым, но от этого не менее действенным способом - конечно же, усредним. Есть один важный момент, который позволяет нам это сделать - а именно очень малое время, которое требуется модулю ADC для выполнения своей работы. Как вы помните, для STM32F10x время одного преобразования составляет:
T_{conv} = Sampling Time + 12.5\medspace cycles
Sampling Time у нас равно 41.5 циклам, и его можно даже еще и уменьшить (минимально допустимое значение - 1.5 цикла). Но пока этого делать не будем, чтобы не нарушать общности примера, ведь существуют ситуации, когда уменьшать его нельзя (и снова отсылка к предыдущей статье, где мы подробно обсудили, в чем суть этого параметра). Получаем время преобразования:
T_{conv} = 41.5 + 12.5\medspace cycles = 54\medspace cycles
Частота тактирования модуля ADC у нас составляет 12 Мгц, это значит что длительность одного цикла в микросекундах:
T_{cycle} = \frac{1\medspace с}{12000000} = \frac{1000000\medspace мкс}{12000000} = 0.0833\medspace мкс
Тогда время выполнения АЦП преобразования будет таким:
T_{conv} = 54\medspace \cdot \medspace 0.0833\medspace мкс = 4.45\medspace мкс
То есть каждые 4.45 мкс мы имеем новое измеренное значение напряжения. И в подавляющем большинстве задач нам не нужно использовать новое значение так часто. Смотрите сами:
- Пусть мы измеряем значение напряжения аккумулятора в неком устройстве. Это значение нам необходимо вывести на дисплей, чтобы пользователь знал текущее состояние аккумулятора и когда необходимо поставить устройство на зарядку. Очевидно, что нет ни малейшей необходимости обновлять это число каждые 4.45 мкс. Вполне достаточно делать это, например, раз в секунду (хотя и это не особо оправдано, и период обновления может быть увеличен гораздо больше).
- Или такой пример - у нас есть джойстик, который выдает разное аналоговое напряжение в зависимости от отклонения ручки. Нам нужно измерять это напряжение, чтобы затем отправить величину какому-нибудь исполнительному механизму, двигателю и т. п. Таким образом, мы даем пользователю возможность управлять этим механизмом. И снова такая же ситуация - нет необходимости отправлять новое значение так часто (каждые 4.45 мкс).
Да и более того, и в первом, и во втором примере, даже при желании отправлять новые данные сразу по мере поступления, это будет невозможно. По той простой причине, что дисплею такого незначительного времени не хватит, чтобы обновить выводимое значение. Также если мы управляем двигателем, к примеру, по USART, данные просто не успеют физически добраться до адресата.
Таким образом, из этого следует вывод - мы можем совершенно безболезненно собрать определенное количество значений АЦП, усреднить их и уже результат отправить по назначению. Благодаря этому мы сгладим шум в данных, либо полностью его устраним. И в 90 процентах (а то и больше) задач этого будет достаточно, и дополнительных алгоритмов обработки сырых данных вводить не потребуется!
В общем-то сейчас мы это реализуем, посмотрим на результаты и немного поэкспериментируем. За основу берем созданный нами ранее проект и сразу же добавляем необходимые переменные:
/* USER CODE BEGIN PD */ #define START_ADC_PERIOD_MS 1 #define CALC_ADC_PERIOD_MS 100 /* USER CODE END PD */ /* USER CODE BEGIN PV */ uint16_t adcData; uint8_t adcReady = 1; uint32_t startAdcCnt = START_ADC_PERIOD_MS; uint32_t calcAdcCnt = CALC_ADC_PERIOD_MS; uint32_t adcSamplesCnt = 0; uint32_t adcDataSum = 0; uint16_t adcDataFinal = 0; float adcVoltage = 0; /* USER CODE END PV */
Разбираемся по порядку:
START_ADC_PERIOD_MS
- это период в миллисекундах для запуска новых преобразований. То есть каждую миллисекунду мы будем запускать наш модуль АЦП снова.CALC_ADC_PERIOD_MS
- а это период, опять же в миллисекундах, по истечению которого мы будем рассчитывать среднее значение по всем накопленным результатам.adcData
- переменная для хранения "сырых" данных с АЦП.adcReady
- дополнительный флаг, который будет сигнализировать о том, что запущенное преобразование завершено.startAdcCnt
,calcAdcCnt
- вспомогательные счетчики. Их будем использовать для запуска периодических операций, дальше из кода все будет понятно.adcSamplesCnt
- это переменная-счетчик, соответствующая количеству измеренных значений, накопленному на текущий момент.adcDataSum
- переменная для накопления данных. Идея такая - каждое новое полученное значение АЦП мы будем прибавлять к текущему значению этой переменной. Таким образом, здесь у нас будет храниться сумма всех значений, которую мы потом разделим на количество значений, и получим среднее - все очевидно )adcDataFinal
,adcVoltage
- и, наконец, здесь будет храниться итоговый результат, то есть отфильтрованные показания АЦП. Вторая переменная для результата в Вольтах.
Двигаемся дальше. Работа по сбору данных у нас будет в callback-функции, которая вызывается по окончании одного преобразования:
/* USER CODE BEGIN 4 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc->Instance == ADC1) { adcDataSum += adcData; adcSamplesCnt++; adcReady = 1; } } /* USER CODE END 4 */
Каждый раз по окончанию преобразования мы действуем так:
- накапливаем значение в переменной
adcDataSum
- соответственно увеличиваем счетчик значений
- и выставляем флаг готовности АЦП в единицу.
В теле основного цикла программы - while(1)
- мы будем запускать новые преобразования и обрабатывать собранные данные. Запускать будем каждые START_ADC_PERIOD_MS
мс, а обрабатывать - каждые CALC_ADC_PERIOD_MS
мс:
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if (startAdcCnt == 0) { if (adcReady == 1) { HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adcData, 1); adcReady = 0; startAdcCnt = START_ADC_PERIOD_MS; } } if (calcAdcCnt == 0) { adcDataFinal = adcDataSum / adcSamplesCnt; adcVoltage = adcDataFinal * 3.3 / 4095; adcDataSum = 0; adcSamplesCnt = 0; calcAdcCnt = CALC_ADC_PERIOD_MS; } } /* USER CODE END 3 */
Как видите, мы записываем в переменные startAdcCnt
и calcAdcCnt
величину периода, и, как только счетчики досчитали до нуля, выполняем необходимые операции. Весь вопрос в том, где мы будем уменьшать эти счетчики. И поскольку у нас в программе уже есть обработчик прерывания SysTick_Handler
, который вызывается каждую миллисекунду, то в нем и будем работать. Но перед этим объявим переменные в файле stm32f1xx_it.c:
/* USER CODE BEGIN EV */ extern uint32_t startAdcCnt; extern uint32_t calcAdcCnt; /* USER CODE END EV */
В этом же файле находится и обработчик прерывания SysTick_Handler
, добавляем:
/** * @brief This function handles System tick timer. */ void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE BEGIN SysTick_IRQn 1 */ if (startAdcCnt > 0) { startAdcCnt--; } if (calcAdcCnt > 0) { calcAdcCnt--; } /* USER CODE END SysTick_IRQn 1 */ }
Как видите, все максимально просто, запустив программу, можно увидеть отличия в значениях переменных adcData
и adcDataFinal
. Значение первой из них будет меняться, а усредненное значение остается стабильным. Собственно, это и было нашей целью. Теперь можно будет использовать эту тестовую программу в качестве шаблона в проектах с АЦП. Меняя значения:
#define START_ADC_PERIOD_MS 1 #define CALC_ADC_PERIOD_MS 100
Можно настроить временные промежутки под конкретную задачу. На этом заканчиваем сегодняшнюю статью, по традиции выкладываю готовый проект: ссылка.
Отличный пример и описание, сделал для сбора данных от 4 каналов, работает на все 100!
Спасибо!
Отлично!