Измерение частоты на STM32

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

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

Наш сегодняшний стенд: генератор частоты JDS6600 и BluePill

Наш сегодняшний стенд: генератор частоты JDS6600 и BluePill

Начальные условия: частота входного сигнала от 0 до 10 кГц. Микроконтроллер STM32F103C8T6, всем известная плата bluepill. Библиотека HAL. Источником сигнала, частота которого будет измеряться, для проверки работоспособности будет служить двухканальный генератор частоты JDS6600. Я буду использовать модуль CH340G (Преобразователь USB — UART) для передачи данных в терминал (terminal v1.9b) для наглядности. В него я буду посылать полученное после обработки значение.

Рассмотрим первый способ — измерение частоты с помощью таймера.

Подключение: Генератор частоты напрямую подключен к выводам микроконтроллера PA0 и PA2. Выводы земли объединены.

Настройка проекта: перейдем в STM32IDE и создаем проект под наш контроллер. После настройки источника тактирования включаем UART для отправки значений:

Настройка UART

Настройка UART

Частоту тактирования настроил на 64 МГц.

eb6c0c80bd8764560706ee494f5a98e4.JPG

В примере я буду использовать Timer 2. Настроим таймер на обработку сразу двух сигналов:

b73707afa6df77b2c4c53ec7d14f4232.jpg

Канал 1 — это основной (direct) канал, а канал 2 это косвенный (indirect). Косвенный канал не имеет отдельного вывода. Аналогично для канала 3 и 4. Захват частоты происходит следующим образом — первый канал реагирует на передний фронт, и мы фиксируем время начала нового периода. После второй канал фиксирует задний фронт, и мы фиксируем время окончания импульса. Затем первый канал фиксирует начало нового периода / окончание предыдущего. Имея эти три временные точки, можно посчитать длительность периода и скважность. Значение делителя и периода Timer 2:

4196839429c66586c0a9cc540f3fad5f.jpg

Значение предделителя определяет шаг, с которым будет считать таймер. Чем меньше шаг, тем точнее результат, но тогда можно столкнуться с тем, что в длительность одного периода таймера не будет помещаться захватываемый сигнал. При частоте тактирования таймера 64 МГц и предделителе = 64 (предделитель в программе выставляется на 1 меньше) получаем, что длительность одного тика таймера равна микросекунде. Период (Counter periode) определяет, до какого значения будет считать таймер до переполнения. В данной задаче лучше его оставить в максимальном значение.

Фильтр входного сигнала (Input Filter) используется для устранения помех и шумов, которые могут возникать на входе таймера. Он позволяет фильтровать входной сигнал и принимать во внимание только стабильные и длительные изменения сигнала, игнорируя кратковременные нежелательные помехи. Если входной сигнал стабилен и не подвержен сильным помехам, можно установить значение фильтра на минимальное или даже отключить его. Однако, при работе с шумными или нестабильными входными сигналами, может потребоваться увеличение значения фильтра для надежной и стабильной работы таймера. Я выставляю Input Filter в максимум. Включим прерывание таймера:

ffb29521f66f9f7edbaec60cc1dcc0f3.jpg

Пишем код: в начале заведем глобальные переменные для хранения длительностей:

volatile uint16_t start1 = 0, end_imp1 = 0, end_per1 = 0, start2 = 0, end_imp2 = 0, end_per2 = 0;

где в переменной start будет храниться значение начала периода, end_imp будет храниться значение конца импульса, end_ per будет храниться значение конца периода. 1 и 2 соответственно CH1+CH2 и CH3+CH4.

Картинка для понимания, что в каждой переменной будет храниться.

Картинка для понимания, что в каждой переменной будет храниться.

volatile uint16_t period1 = 0, fill_factor1 = 0, long_imp1 = 0, period2 = 0, fill_factor2 = 0, long_imp2 = 0;
volatile uint16_t freq1 = 0, freq2 = 0;
volatile uint8_t flag_IC = 0;

Тут будем хранить вычисленные переменные и будем выставлять флаг о том, что пришел сигнал. Далее включим все каналы нашего таймера и заведем массив для отправки по UART:

  /* USER CODE BEGIN 2 */
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2);
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_3);
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_4);

После срабатывания прерывания по захвату программа будет попадать в callback (эта функция, которая вызывается после определенного события/прерывания) функцию, которая описана ниже:

/* USER CODE BEGIN 4 */
/*----------------------------------------------------------------------------*/
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == TIM2)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
		{
			start1 = end_per1;
			end_per1 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
			end_imp1 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_2);
		}
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_3)
		{
			start2 = end_per2;
			end_per2 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_3);
			end_imp2 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_4);
		}
		flag_IC = 1;
	}
}

В ней выставляется флаг «flag_IC», по которому будем определять приход новых данных. В отдельной функции будем вычислять значение периода и коэффициента заполнения:

/*----------------------------------------------------------------------------*/
void CALC_FREQ (void)
{
	// длительность периода в импульсах (1 импульс = 1 мкс)
	if (end_per1 > start1) 
	{
		period1 = end_per1 - start1;
		long_imp1 = end_imp1 - start1;
		if (period1 > 0)
		{
			freq1 = 1000000 / period1;
			fill_factor1 = (long_imp1 * 100) / period1;
		}
	}
	if (end_per2 > start2)
	{
		period2 = end_per2 - start2;
		long_imp2 = end_imp2 - start2;
		if (period2 > 0)
		{
			freq2 = 1000000 / period2;
			fill_factor2 = (long_imp2 * 100) / period2;
		}
	}
}

Условие «if (end_per1 > start1)» нужно для того, чтобы не обсчитывать импульсы, которые попадают на переполнение таймера, когда начало периода было в одном цикле таймера, а конец в другом. Но обсчёт такого случая тоже можно реализовать.

В основном цикле отслеживаем значение флага, как только он стал равен единице, вычисляем значение частоты и коэффициента заполнения, после отправляем данные по UART в терминал и сбрасываем значение флага:

if(flag_IC == 1)
	{
		CALC_FREQ();
		snprintf(buff, 80, "Freq 1 = %d fill_factor 1 = %d  Freq 2 = %d fill_factor 2 = %d\r\n", freq1, fill_factor1, freq2, fill_factor2);
		HAL_UART_Transmit_IT(&huart1, buff, strlen(buff));
		HAL_Delay(500);
		flag_IC = 0;
	}

Я запускаю генератор частоты с вот такими настройками:

57dd6909bfea2fdbed857280851a1e36.jpg

Оправляем два прямоугольных сигнала с частотой 1кГц и 850 Гц. Теперь посмотрим, что нам пришло на терминал:

30e9a2858494b783ab140e4777d4feaf.jpg

В терминале выставляем скорость 115200 и получаем результат. Теперь проверим этот же код, но на динамический сигнал. Я покрутил значение частоты на первом канале от 1 кГц до 8 кГц и обратно с шагом в 1 кГц и вот что получил:

3a6396e379e593ed11e5ca230e467e89.jpg

В принципе работает, но в реальных системах частота может менять сильно быстрее, чем я кручу ручку, так что тест с динамическим сигналом правильно считать упрощенным.

Достоинства и недостатки:

+

-

Есть возможность измерить скважность импульса.

Необходимы таймеры, по одному таймеру на два входа. Маленького контроллера под большое количество сигналов может просто не хватить, так как многие другие задачи тоже требуют свободных таймеров.

В отличие от других методов, может за 1 период посчитать частоту сигнала (при отключенном фильтре).

Ограничение диапазона захвата частоты. В данном примере таймер считает до 65 мс, и если период сигнала больше этого значения, например 100 мс (10 Гц), то таймер не сможет его захватить без дополнительных программных ухищрений.

Если входной сигнал пропал — значения частоты не обнулится само по себе. Можно проверять, меняется ли значение частоты, и если нет, то обнулять значение частоты, или же выставлять какой-нибудь флаг при попадании в callback.

Теперь рассмотрим вариант измерения значения частоты с использованием вывода EXTI.

Выводы EXTI (External Interrupt) у микроконтроллеров STM32 предназначены для обработки внешних прерываний от различных источников. EXTI позволяет реагировать на изменения состояния внешних сигналов и генерировать прерывания для обработки этих событий. Логика работы довольно проста — каждый раз, когда приходит импульс, мы по прерыванию будем инкрементировать значение импульсов, а раз в секунду смотреть, сколько импульсов пришло. Так и получим необходимую нам частоту.

Подключение: генератор частоты подключен к выводам PA0 и PA1.

Настройка проекта: В настройках контроллера включаем выводы PA0 и PA1 как выводы EXTI. Также включаем прерывание у них. Прерывание будет срабатывать на восходящий фронт сигнала.

d1db6175b88883a6187dcda8d1d5b163.jpg

Также надо включить таймер, который будет отсчитывать одну секунду, в прерывании которого мы будем фиксировать нашу входную частоту:

c67c21f67a99a67a4d82c0935dda88ec.jpg

Пишем код:

/* USER CODE BEGIN PV */
volatile uint32_t num_imp1 = 0, num_imp2 = 0;
uint32_t freq1 = 0, freq2 = 0;
volatile uint8_t  flag_timer = 0;

Заводим переменные. Первые две будут инкрементироваться каждый раз, когда приходит импульс. Каждую секунду по прерыванию таймера будет вывешиваться флаг, при изменении которого мы будем фиксировать значение частоты и сбрасывать счетчики импульсов. Вот так будет выглядеть callback по EXTI выводам:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_0)
	  num_imp1++;
  if (GPIO_Pin == GPIO_PIN_1)
	  num_imp2++;
} 

И вот так по таймеру:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
	flag_timer = 1;
	freq1 = num_imp1;
	freq2 = num_imp2;
	num_imp1 = 0;
	num_imp2 = 0;
}

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

if (flag_timer == 1)
{
	snprintf(buff, 80, "Freq 1 = %d Freq 2 = %d\r\n", freq1, freq2);
	HAL_UART_Transmit_IT(&huart1, buff, strlen(buff));
	flag_timer = 0;
}

Подадим 1 кГц на первый канал и 5 кГц на втором:

6f477d2a2486b3261601972b1c26d413.jpg

Вот результат захвата динамического сигнала (канал ch2 статичный):

c703a9f60171c7e9f9958439916d6a79.jpg

Отлично, все работает. Теперь о достоинствах и недостатках:

+

-

Довольно простой метод

Данные приходят довольно редко — в данном случае раз в секунду

Нужен всего один таймер

По умолчанию сразу получаем усредненный результат

И последний рассмотренный мною метод — захват с помощью ETR2. При настройке источника тактирования таймера можно выбрать вывод ETR2 (External Trigger Input 2), и тогда таймер в качестве источника тактирования будет использовать внешний источник. На этот вывод будет подаваться замеряемый сигнал.
Реализуем частотомер второго рода. Основной принцип работы частотомера второго рода заключается в сравнении частоты входящего сигнала с частотой, генерируемой внутри прибора, в нашем случае длительность периода внешнего сигнала будет измеряться в кол-ве тиков другого таймера.

Заводим timer 2, который просто считает импульсы с максимально возможной частотой. По приходу первого импульса на ETR2 запускается timer 4. Когда на timer 2 придет, допустим, сотый импульс, мы фиксируем, сколько импульсов насчитал timer 4. Делим это значение на 100 и получаем период захватываемого сигнала. Картинка для чуть лучшего понимания:

94e4e9816f5a593b4f1050335675338c.jpg

Настройка проекта: настроим таймера 2, на который будет подаваться входной сигнал. Считает до 100 го импульса, а потом счет сбрасывается и начинается новый период. Именно в этот момент мы будем фиксировать кол-во отсчитанного времени на 100 периодов входного сигнала.

b500a2144bee18c05b901224162e48ca.jpg

Настройка таймера 4 для отсчета времени:

6b9ff1b604b57a8d9256c000dce863a7.jpg

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

/* USER CODE BEGIN PV */
volatile uint16_t num_per_tim = 0;    // кол-во прошедших периодов таймера
volatile uint16_t time_tim = 0;       // сюда будем записывать время с таймера, когда при-шел 10 импульс
volatile uint16_t per_tim = 0;        // сюда будем записывать кол-во периодов таймера
volatile uint8_t  flag_data = 0;      // по этому флагу будем отслеживать момент, когда пора считать значение частоты

Создаем callback для обработки данных. Суть в том, что таймер 4 считает с частотой больше частоты входного сигнала и будет много раз переполняться. Кол-во переполнений таймера 4 как раз и будем хранить в переменной num_per_tim, поэтому в callback таймера 4 необходимо каждый раз инкрементировать счетчик периодов таймера. А в callback таймера 2 программа будет попадать, когда пришли все 100 импульсов и будет фиксироваться затраченное количество импульсов таймера 4.

/* USER CODE BEGIN 4 */
/*----------------------------------------------------------------------------*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	// если таймер досчитал до конца, не получив все 100 импульсов на вход Timer 2 ETR2, то увеличиваем счетчик кол-ва периодов
	if (htim->Instance == TIM4)
		num_per_tim++;
	if (htim->Instance == TIM2)
	{
		time_tim = __HAL_TIM_GET_COUNTER(&htim4);
		per_tim  = num_per_tim;
		num_per_tim = 0;                                   // сброс счетчика периодов
		TIM4->CNT = 0;                                     // сброс счета таймера
		flag_data = 1;
	}
}

Теперь, отслеживая состояние flag_data можно считать частоту:

if (flag_data == 1)
{
	float freq = 0;
	float period = time_tim + per_tim * 65536 + 1;             // период в тиках таймера
	if (period != 0)                                           // на всякий исключим деление на ноль
	{
		period /= 100;                                         // длительность одного периода
		freq = 72000000 / period;
	}
	else freq = 0;
	snprintf(buff, 80, "Freq = %.3f\r\n", freq);
	HAL_UART_Transmit_IT(&huart1, buff, strlen(buff));
	flag_data = 0;
	HAL_Delay(500);
}

В начале вычисляем суммарное потраченное время, а затем делим его на 100 и вуаля, значение частоты готово. Подадим на вывод PA0 сигнал с частотой 10 кГц:

673d224977417ea2a813be4043c3c621.jpg

Теоретически, этот метод должен быть с самым большим диапазоном частоты входного сигнала. Попробуем 100 кГц:

7bbd985d70c43762f7c1533b9fa794dd.jpg

Ну и 1 МГц:

8c56a8c34dab15574d99c54b664b8a22.jpg

Последний результат уже с большой погрешностью, но, тем не менее, работает.

Достоинства и недостатки:

+

-

Больший диапазон относительно других

Нужен целый таймер с выводом ETR на каждый отдельный таймер для счета

Конечно, тут описаны не все возможные способы измерения частоты входного сигнала, а только несколько, с которыми я столкнулся.  Некоторые из них были проверены только на макете и в реальных задачах могут вести себя отлично от того, что получилось у меня.

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

© Habrahabr.ru