Рейтинг@Mail.ru
STM32. Уроки по программированию STM32F4. Урок № 5. Работа с АЦП+DMA + фильтр скользящее среднее.
Войти
или
Зарегистрироваться
Главная Файлы Видеоматериалы Форум
Карта сайта
Главная -> РУБРИКИ: -> Программирование STM32F4 -> STM32. Уроки по программированию STM32F4. Урок № 5. Работа с АЦП+DMA + фильтр скользящее среднее.

Статья опубликована: 2016-02-03/13:38:44-admin

STM32. Уроки по программированию STM32F4. Урок № 5. Работа с АЦП+DMA + фильтр скользящее среднее.



Начало здесь:


STM32. Уроки по программированию STM32F4. Урок № 0. Вводный. Описание. Установка IDE.


STM32. Уроки по программированию STM32F4. Урок № 1. Система тактирования STM32F4.


STM32. Уроки по программированию STM32F4. Урок № 2. Мигание светодиодом STM32F4.


STM32. Уроки по программированию STM32F4. Урок № 0. Update № 1.Портирование из STM32CubeMX в SW4STM32.


STM32. Уроки по программированию STM32F4. Урок № 3. Системный таймер SysTick STM32F4.


STM32. Уроки по программированию STM32F4. Урок № 4. Программный многозадачный таймер STM32F4.




 

Данная статья является продолжением цикла видео уроков по изучению МК STM32 на базе STM32F407. На данном уроке разберем модуль АЦП. Его работу и передачу данных с каналов АЦП через DMA в регистры ОЗУ. Дальнейшей фильтрацией по методу скользящего среднего.

 

Вместо предисловия.

 

В рунете много написано статей, постов, обзоров, касаемых АЦП МК STM32. Можно заглянуть сюда, сюда или посмотреть уроки здесь. Нагуглить еще пару десятков статей не предоставляет трудностей. Поэтому детальный разбор модуля АЦП в этой статье показан не будет. К тому же, при использовании библиотеки HAL от ST, нам необходимо научится работать с АЦП, а каким образом там и что устроено, отодвинем на второй план. Любознательность должна привести читателя (считай зрителя) к самостоятельному рассмотрению устройства АЦП и его возможности.

 

Предисловие.

 

Просто получить данные с каналов АЦП, скопировать их в ОЗУ в реальных устройствах является недостаточным. В зависимости от исследуемого (измеряемого) сигнала необходимо фильтровать значения поступающие с АЦП. Необходимость фильтрации обуславливается множеством факторов:

 

  • Источник сигнала содержит шумы,
  • Источник опорного напряжения не блещет стабильностью,
  • Схемотехника входных цепей не идеальна,
  • Топология печатной платы разработана не оптимально,
  • И т.д.

 

В зависимости от задач и фильтры применяются различные. Разнообразие фильтров и их реализаций для МК великое множество. От программных ФНЧ до режекторных  фильтров Баттерворта, а также все возможные КИХ и БИХ фильтры. Перечислять можно, до бесконечности, очень долго. Прочитать можно тут, здесь и так далее.

 

В данной статье рассмотрим реализацию фильтра по методу скользящего среднего. Прочитать про него можно в ВИКИПЕДИИ.

 

АЦП в МК.

 

Совершим краткий обзор модуля АЦП.

 

Начать следует с того что контроллер может измерять напряжения только с определённых ножек в названии которых присутствует слово ADC1_INx, где х некоторый уникальный номер канала. АЦП контроллера может опрашивать каждый из каналов поочередно.

 

В STM32 есть два варианта чтения данных с ножек ADC1_INx. Первый называется "Регулярные каналы" (regular channels в даташите). Использование этого метода опроса состоит в том, что АЦП опрашивает по очереди некоторый заранее настроенный список каналов, после каждого опроса результат записывается в один и тот же регистр. Это означает, что нужно своевременно забирать результат преобразования из этого регистра, в противном случае результат будет перетираться.

 

Совсем иначе дело обстоит с "Инжектированными каналами" (Injected channels). В случае использования этого метода опроса можно записывать результат измерения каждого канала АЦП в свой отдельный регистр ничего не перезатирая. Но, к сожалению, таких регистров всего 4, а ножек АЦП у контроллера всегда больше.

 

При использовании регулярных каналов, необходимо своевременно забирать данные с модуля АЦП, для того чтоб получать актуальные данные и не путать значения по каналам. В STM32 есть замечательный модуль DMA, который может эту рутинную работу взять на себя. И без путаницы распихать значения по каналам в указанные регистры в ОЗУ, а по прерыванию от DMA забирать эти данные из указанного нами буфера.

 

Вот и про ДМА разговор пошел.

 

Тут тоже стоит сказать, что возможности ДМА обширны. Это аппаратный модуль, располагающийся на системной шине и имеющий (как и процессор) доступ ко всей памяти и периферийным модулям. Он сам умеет получать данные и копировать их в нужное место без малейшего использования процессора. Можно сказать, он предоставляет периферийным устройствам унифицированный канал прямого, самостоятельного доступа к памяти.

 

Возможны три направления передачи:

 

  1. Периферия → память — для приёма данных и складирования их в буфер (к примеру, оцифровка АЦП)
  2. Память → периферия — для передачи данных из буфера (к примеру, вывод аналогового сигнала в ЦАП)
  3. Память → память — простое копирование блока данных в другое место памяти, аппаратный memcpy.

 

Периферийному модулю всегда требуется какое-то время на обработку данных, поэтому он тактирует передачу с помощью сигнала Event. В случае же передачи «память → память» копирование происходит с максимальной возможной скоростью, которая стремится к скорости работы DMA, но не превышает 50% загрузки системной шины. К сожалению, из-за этого ограничения копирование памяти через DMA всё-таки происходит на 30% медленнее, чем оптимизированная версия процессорной функции memcpy, зато оно не отвлекает процессор от другой работы.

 

Можно передавать байт (8 бит), пол-слова (16 бит) или слово (32 бит) за один раз. К примеру, для передачи по UART, SPI и I2C обычно достаточно 8 бит, но для 9-битного режима UART потребуется уже передача по 16 бит. Копировать массив в памяти так и вообще выгоднее по 32 бита — по 4 байта за раз.

 

Расписывать более подробно не станем. Инфу по ДМА всегда можно нагуглить.

 

STM32CubeMX.

 

Для работы с АЦП + ДМА в режиме одиночного сканирования регулярных каналов произведем необходимые настройки в среде КУБа.

Картинка кликабельна.

 

Настраиваем пины как аналоговые входы. Указываем, каким АЦП будем обрабатывать данные, в данном случае каналы с первого по третий подключены к ADC1.

 

Конфигурация (настройка АЦП).

 

Картинка кликабельна.

 

Рассмотрим по порядку, какие параметры нам предлагает настроить КУБ.

 

Resolution - разрешающая способность. Максимально возможная 12 бит, но можно искусственно занизить разрядность получаемых данных тем самым уменьшить время преобразования, то есть увеличить максимальную частоту обработки сигнала. В данном случае максимальная разрядность.

 

Data alignment - выравнивание данных. Так как получаемые данные 12 битные, то в 16 битном регистре их можно выровнять как по правому флангу, так и по левому. В данном примере выравнивание правое, то есть старшие 4 бита в 16 битном регистре будут всегда «0».

 

Scan conversion mode - режим сканирования. Подразумевает под собой, что АЦП автоматически опрашивает все настроенные каналы последовательно, и после полного цикла вызывает прерывание об окончании преобразований, если такое включено. В данном случае, мы будем по очереди опрашивать три регулярных канала АЦП.

 

  Continuous conversion mode - режим непрерывной преобразования. Данный режим обеспечивает непрерывное преобразование с АЦП. То есть по окончанию сканирования настроенные каналов, АЦП начинает новое преобразование. Циклическое. В данном случае этот режим нам не нужен.

 

Для настройки регулярных каналов, указывается их количество, затем для каждого ранга выбирается канал и указывается время выборки.  От времени выборки влияет точность показаний АЦП. Время выборки зависит от входного импеданса. В данном случае поставлено на максимум. Погоня за временем преобразованием не в ущерб точности не велась.

 

Вроде с АЦП не так уж сложно. Перейдем к настройки ДМА.

Картинка кликабельна.

 

Тут вообще все просто. Выбираем источник – в нашем случае ADC1. Выбираем режим работы, указываем, необходим ли нам инкремент адреса и длину транзакции. В данном случае у нас режим нормальный, то есть не зацикленный. Использовать FIFO буфер не будем. Инкремент адреса памяти. Длина транзакции 16 бит, пол слова.

 

Прерывания от ДМА включим. А прерывание от АЦП включать нет необходимости.

 

Что нам сгенерил КУБ?

 

 

 

Картинка кликабельна.

 

Функции для инициализации АЦП и ДМА. В функции инициализации ДМА только настройка прерываний.

 

В функции АЦП все необходимые настройки по работе АЦП, ДМА.

Картинка кликабельна.

 

Настройка самого АЦП и регулярных каналов.

 

Картинка кликабельна.

 

А здесь настраиваются пины, и работа с ДМА.

 

АЦП и ДМА настроили. Пора переходить с основной мысли – обрабатывать сигналы с помощью АЦП + ДМА и проводить фильтрацию полученных данных по методу скользящего среднего.

 

Запуск АЦП + ДМА.

 

На прошлом уроке мы написали модуль программных многозадачных таймеров на основе системного таймера SysTick. Не будем изменять код из предыдущего урока, пусть светодиоды мигают. Добавим программный таймер. Циклический, период 5 мс.

 

//----------------------------------------------------------
  /*
   * Урок №5. ADC+DMA+Filter
   * */

  OnSwTimer(&TIM[4],SWTIMER_MODE_CYCLE,5);
  TIM[4].Off=0;
  TIM[4].On=1;

 

По срабатыванию программного таймера TIM[4], будем запускать преобразование регулярных каналов АЦП через ДМА. Выглядит это следующим образом.

 

	  /*
	   * Урок №5.
	   * */
	  if (GetStatusSwTimer(&TIM[4])){
		  HAL_ADC_Start_DMA(&hadc1,(uint32_t *)&ADC_value,3);
	  }

 

После того как ДМА закончит работу, возникнет прерывание.

 

Лирическое отступление.

При выполнении функции HAL_ADC_Start_DMA  включаются все возможные прерывания от ДМА, по окончанию транзакции, по заполнению половины буфера, при возникших ошибках. Нам прерывание от заполнения половины буфера не нужно, поэтому вопреки  КУБа, запретим данное прерывание.

  /* Enable the Half transfer complete interrupt */
  //__HAL_DMA_ENABLE_IT(hdma, DMA_IT_HT);

Находится данное действие  в функции:

 HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, 
uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)

 

По прерыванию от ДМА, сигнализирующее об окончании транзакции, то есть полное заполнение буфера, взводим флаг о готовности данных с АЦП.

 

В основном цикле по этому флагу останавливаем ДМА, и производим расчеты с помощью фильтров.

 

	  if (flag_adc_dma){
		  flag_adc_dma=0;
		  HAL_ADC_Stop_DMA(&hadc1);
		  ADC13=ADC_value[2];
		  ADC12=ADC_value[1];
		  ADC11=ADC_value[0];
		  filter_11=filter_sred(ADC11,buf_11,&F11);
		  filter_12=filter_sred(ADC12,buf_12,&F12);
		  filter_13=filter_sred(ADC13,buf_13,&F13);
	  }

 

 И уже с отфильтрованными значениями, хранящимися в переменных  filter_11, filter_12, filter_13 можно производить необходимые манипуляции.

 

Фильтр скользящее среднее.

 

Фильтр скользящее среднее является разновидностью математического усреднения по заданному количеству точек. То есть формула математического среднего имеет вид:

 

Из этой формулы следует, что мы получаем один отсчет выходного значения при N входных. Для скользящего среднего этот недостаток не присущ.

 

На каждой итерации, новый отсчет заменяет самое старое значение в массиве, участвующей в сумме и происходит вычисление среднего по всем элементам массива. То есть на каждое новый входной отсчет получаем новый выходной отсчет.

 

У данного метода есть недостаток, пока количество итераций меньше глубины окна (размерности массива) участвующий в усреднении, выходные данные не соответствуют истине. Но после заполнения массива, усреднение выполняется с заданной точностью. Размер окна (размерность массива) влияет на качество фильтрации. Чем больше размер массива, тем сильнее фильтр, лучше сглаживает изменения входного сигнала.

 

Рассмотрим программную реализацию в данном уроке.

 

Хидер модуля фильтра скользящее среднее:

 

#define COUNT_FILTER	256

typedef union
{
    unsigned int Val;
    struct
    {
    	unsigned Flag:1;		//Флаг заполнения суммы
    	unsigned Index:8;		//Хранение индекса буфера
    	unsigned Filter_sum:23;	//Регистр суммы для усреднения
    } Reg;
} FILTER_REG;

unsigned short int	filter_sred(unsigned short int ADC_val, 
unsigned short int* buf, FILTER_REG* filter_reg);

 

Обозначаем глубину окна (размерность массива) через #define. Ограничение составляет в 256. Это ограничение вытекает из структуры, которая используется для расчета фильтра.

 

Сумма 12 битных отсчетов АЦП в количестве 256 значений будет иметь размерность 2^20. Для хранения текущего положения индекса в массиве зададимся одним байтом. Итого уже 2^28 используется. Один бит выделим для флага, в котором будем хранить данные о заполнении массива. То есть пока итерации работы фильтра меньше его размерности, то флаг не выставлен, после заполнения массива, устанавливаем флаг, и немного изменяет алгоритм обработки.

 

Итого вписались в 32 битную переменную. Ограничение в 256 элементов массива выливается из использования 8 битного значения переменной, в который хранится текущий индекс массива. Если повысить разрядность данной переменной, то можно увеличить размерность массива.

 

Предупреждение: Пока массив не будет полностью заполнен актуальными данными, значения после фильтра не являются достоверными.

 

Рассмотрим исходный код.

 

unsigned short int	filter_sred(unsigned short int ADC_val, 
unsigned short int* buf, FILTER_REG* filter_reg){
	if (filter_reg->Reg.Flag){
		filter_reg->Reg.Filter_sum-=buf[filter_reg->Reg.Index];
		filter_reg->Reg.Filter_sum+=ADC_val;
		buf[filter_reg->Reg.Index]=ADC_val;
		if (filter_reg->Reg.Index>=COUNT_FILTER-1){
			filter_reg->Reg.Index=0;
		}
		else{
			filter_reg->Reg.Index++;
		}
	}
	else{
		filter_reg->Reg.Filter_sum+=ADC_val;
		buf[filter_reg->Reg.Index]=ADC_val;
		if (filter_reg->Reg.Index>=COUNT_FILTER-1){
			filter_reg->Reg.Index=0;
			filter_reg->Reg.Flag=1;
		}
		else{
			filter_reg->Reg.Index++;
		}
	}
	return (filter_reg->Reg.Filter_sum/COUNT_FILTER);
}

 

Пока массив не заполнен входными данными, мы к сумме прибавляем текущий отсчет, в массив записываем текущий отсчет, индекс массива смещаем на единицу, если достигли конца массива, то индекс обнуляем и взводим флаг полного заполнения массива.

 

При взводе флага, у нас в регистре фильтра хранится сумма всех элементов массива. Далее, при последующих итерациях, мы из готовой суммы вычитаем самый старый элемент массива, прибавляем текущее входное значение, записываем это значение в самую старую ячейку массива (в ту которую вычитали из суммы), смещаем индекс массива и производим вычисление среднего значения путем деления суммы на количество элементов.

 

Вот таким не хитрым способом мы организовали скользящее среднее. Если присмотреться, то данный код можно оптимизировать. Хотя и данная реализация занимает всего 196 байт флеша, при стандартной настройки оптимизации компилятора - Os.

 

 

 

В данном окне отображается текущее значение, считанное с АЦП и значение после фильтра. Как видно значение после фильтра имеет значение в 2085 единиц, тогда как считанное значение с АЦП = 2080 единиц.

 

В другой момент времени, значение после фильтра остается неизменным, тогда как считанное значение с АЦП изменилось на 6 единиц.

 

Архив с проектом данного урока можно скачать отсюда.

 

Видео урока №5. Работа с АЦП+DMA + фильтр скользящее среднее.

 

 

З.Ы. коментарии, вопросы и предложения складываем тут

 

 

 

 


Продолжение здесь:


STM32. Уроки по программированию STM32F4. Урок № 6. Работа с таймерами TIM7 и TIM1.



Просмотров: 21310



Комментарии: (3)

Пользователь Rubi_ в 2017-03-31 16:50:18 сказал:

Е мое, я так и не разобрал как у Вас средне скользящее считается по коду, зачем нужно было все так усложнять? Вот мой вариант

const int  calc_afr_mass_size = 255; // размерность массива для хранения промежуточных знач
int afr_mass[calc_afr_mass_size ]; // сам массив
int calc_afr_avg_result = 0; // переменная где будет хранится полученное среднее

...........
calc_afr_mass[calc_afr_mass_size-1] = HAL_ADC_GetValue(&hadc1); // по прерыванию получаем результат в последний элемент массива 
...........

for (int i = 0; i < calc_afr_mass_size-1; i++) {
        calc_afr_avg_result += calc_afr_mass[i]; // проходим по массиву складывая значения
        calc_afr_mass[i] = calc_afr_mass[i+1];  // сдвигаем элементы массива, т.е. первый равен второму и т.д.
    }  
      calc_afr_avg_result = calc_afr_avg_result/calc_afr_mass_size; // собственно получаем среднее
Ответить

Пользователь admin в 2017-03-31 19:49:25 сказал:

Здравствуйте!

Во-первых, не хватает обнудение переменной

calc_afr_avg_result = 0;

Во-вторых у Вас на каждом новом вычислении происходит проход цикла с суммированием и перестановкой, ресурсоемко достаточно.

В моем варианте прохода в цикле нет, и все вычисление сводится к тому, что из суммы вычитается самый старый элемент, прибавляется новый, он занимает место самого старого значения в массиве и сдвигается индекс массива, т.е. три математические операции плюс присваевание. Скорость выполнения, уверен, будет выше.

        filter_reg->Reg.Filter_sum-=buf[filter_reg->Reg.Index];
        filter_reg->Reg.Filter_sum+=ADC_val;
        buf[filter_reg->Reg.Index]=ADC_val;
        if (filter_reg->Reg.Index>=COUNT_FILTER-1){
            filter_reg->Reg.Index=0;
        }
        else{
            filter_reg->Reg.Index++;
        
Ответить

Пользователь Rubi_ в 2017-04-01 06:20:30 сказал:

Здравствуйте!

Во-первых, не хватает обнудение переменной

Понятно, там много чего не хватает, например функции main, цикла while и т.д. Характерезует однако, дальше не вижу для себя смысла продолжать разговор на этот счет с такимм подходом. 

 

Ответить

Оставить комментарий

Да, Я Хочу Всегда Быть В Курсе Новых Событий На Сайте!

Подпишитесь прямо сейчас, и получайте обновления на свой E-Mail:

Ваш E-Mail в безопасности


Рекомендованные статьи:



Продолжаем изучение МК STM32F407 на базе STM32F4 Discovery. На данном уроке разберем таймер общего назначения. В данном МК их два: TIM6 и TIM7. Во всех семействах они присутствуют, так что настройка данного типа таймеров практически ничем не отличается, от камня к камню, так что код переносится банальным копипастом.


Разработка модуля программных многозадачных таймеров на STM32.


РУБРИКИ:








Последняя статья:

Часть I. Статья №6. Верстка подвала – блока футтер

На прошлом уроке мы сверстали основной блок контент. Оформили  статьи, которые отображаются на главной странице. Оформили их стилями, не входящими в макет сайта, но вписывающимися в общий дизайн сайта. На сегодняшнем уроке мы оформим подвал сайта. В данный блок войдут ссылки на стандартные страницы любого сайта, а также копирайт.

Читать далее »


Справка Обратная связь Вопросы и ответы Контакты RSS-лента © 2013-2016, ДРУиД - Дом Рационально-Умный и Душевный
Рейтинг@Mail.ru Яндекс.Метрика