В этой небольшой статье хочу рассказать вам о различных методах измерения частоты прямоугольного сигнала с помощью микроконтроллера 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 МГц.

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

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

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

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

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

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;
	}

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

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

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

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

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

+

-

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

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

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

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

 

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

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

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

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

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

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

Пишем код:

/* 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 кГц на втором:

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

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

+

-

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

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

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

 

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

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

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

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

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

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

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

/* 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 кГц:

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

Ну и 1 МГц:

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

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

+

-

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

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

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

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

Ссылка на проекты

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


  1. AndreyDmitriev
    03.11.2023 15:22
    +3

    Это называется "когда мысли сходятся". Мне по работе для пары тестов понадобилось, "слепил на коленке" из ардуинки:

    Там правда не только частотомер, но и пара простеньких триггеров.


  1. Grey83
    03.11.2023 15:22
    +5

    Собрал когда-то на синей таблетке (и ILI9341) осциллограф. Хорошая штука, жаль у автора кроме схемы соединения и прошивки ничего нет. Даже назначения 5 кнопок не описаны. =(
    Вот только это на оф.сайте:

    • Максимальная частота оцифровки 4.27 мГц (Интерлив, ДМА, 120 мГц тактовая частота)

    • Синхронизация: фронт, спад, max, min

    • Входное напряжение 0 ~ 3В

    • Дисплей SPI TFT 2.2" (2.4")) 320x240

    • Цифровой люминофор ( 0.1сек/дел ~ 10сек/дел)

    • FFT спектр сигнала

    • Заморозка экрана (Freezing)

    • Генератор прямоугольных импульсов 0.1Гц ~ 20мГц

    • Пять кнопок управления

    В инетах есть такая инфа по управлению:

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

    Оставшиеся две кнопки совместно со "Shiftom" замораживает осциллограмму (появляется символ снежинки) последняя кнопка управляет яркостью - выкл./средняя/высокая.

    Так и валяется который год в коробке без дела (собирал чисто поиграться, ну и на вход больше 3В подавать нельзя, я полагаю).

    Осциллограф (STM32F103C8T6 + ILI9341), схема соединений
    Осциллограф (STM32F103C8T6 + ILI9341), схема соединений


    1. Nick_Shl
      03.11.2023 15:22

      А код есть? Интересно глянуть. Я тоже когда-то осциллограф делал, но не выкладывал никуда.


      1. Grey83
        03.11.2023 15:22

        На оф.сайте только бинарник можно скачать.
        Я сам бы поглядеть исходник не отказался (для общего развития).


  1. IbhSvenssen
    03.11.2023 15:22
    +9

    Касаткин А.С. Обработка сигналов частотных датчиков. М.: Энергия, 1966.

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


  1. vk6677
    03.11.2023 15:22

    Вроде в STM есть в таймерах аппаратный захват ШИМ. И частота и скважность.


  1. Nick_Shl
    03.11.2023 15:22

    Не понятно зачем в вашем захвате по прерыванию таймер с прерыванием вообще нужен. Можно SYSTICK обойтись. Настроить на разрешение в 1 мс(дефолтное в HAL), если у вас все в суперлуп крутится и проходит быстрее чем за 1 мс или если есть RTOS которая может дёргать задачу раз в 1 мс - этого достаточно. На частоте 10 кГц погрешность будет всего около 10 Гц.

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


  1. 0xdead926e
    03.11.2023 15:22
    +3

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

    на скриншоте на вход PLL подан клок от внутреннего RC-генератора. он не очень-то точен и температурно стабилен. переключи на HSE (:

    а еще у того камня есть аппаратный usb, можно было навернуть usb-cdc и гонять по нему, а не по usb-uart. только не помню вот, пофиксили ли ст тот баг с забытой поддержкой переключения скорости потока в их библиотеке. без этого в линуксе не заработает.


    1. nicher Автор
      03.11.2023 15:22

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


  1. Anzorik_228
    03.11.2023 15:22
    +4

    Вы описали довольно типовую и распространённую на просторах интернета задачу. Конечно, неплохая статья, при условии, что вы только начали разбираться с микроконтроллером STM32. В остальном есть более интересные источники для изучения таймер и частотомеров новичками. Однако автор указал сложать статьи "простая", что отметает всякую строгую критику. Единственное что встаёт вопрос "зачем".

    Более интересным было бы увидеть измерение частоты не прямоугольного сигнала (что уже ну очень банально), а также синусоидального, треугольного и конечно шумовую состовляющую переменного сигнала. Каждый этот тип сигнала можно сгенерировать как с внешнего генератора, но более интересно выдать их с ЦАПа более навороченной stm. Если вы уже занялись серией F1, возьмите тогда к примеру stm32f103ZE, имеющий два канала ЦАП на борту и уже попробуйте сделать свою задачу более интересной. Такое устройство смогло бы стать хорошим конспектом (шпаргалкой) для множества типовых задач и опорой для более сложных.

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

    Из недостатков: тема статьи является довольно заезженой и уже имеется достаточно сведений на изветсных платформах по STM32, типа istarik, easyelectronics и controller's Tech. К тому же видно, что автор лишь переработал информацию из других статей. Для более "крепкого" содержания хотелось бы видеть ссылки на RM и Datasheet, тогда было бы видно, что автор полностью разобрался в вопросе.


    1. Nick_Shl
      03.11.2023 15:22
      +1

      Более интересным было бы увидеть измерение частоты не прямоугольного сигнала (что уже ну очень банально)

      Что там интересного? Та же банальщина - запускаем АЦП в "Single-channel continuous conversion" режиме, настраиваем analog watchdog на нужное значение и получаем практически те же те же прерывания что и для прямоугольного сигнала на цифровой ножке. Ну почти - analog watchdog работает на превышении или пренижении значения(аналог прерывания по уровню, а не по фронту), так что в прерывании нужно постоянно менять значения регистров analog watchdog'а.


      1. Anzorik_228
        03.11.2023 15:22
        +1

        Ну вот вы сами и ответили на мой вопрос;) Это и хорошо было бы поместить в одну статью, начинающим очень удобно было бы добавить такую статью в закладки


  1. VT100
    03.11.2023 15:22

    Думаю, плашка "Tutorial" тут ещё неуместна.


  1. sargon5000
    03.11.2023 15:22
    +2

    • Максимальная частота оцифровки 4.27 мГц (Интерлив, ДМА, 120 мГц тактовая частота)

      Автор путает миллигерцы и мегагерцы.


  1. GreedyHamster
    03.11.2023 15:22

    Делал в своё время частотомер на AT90S2313. Контроллер, дешифратор (ИД7, если не изменяет склероз), светодиодный индикатор на 8 разрядов. Программу писал на ASM'е в AVRStudio. Измерял частоту, период, длину положительного и отрицательного импульсов до 1 MHz (режим измерения переключался последовательно нажатием одной кнопки). Вроде даже плата до сих пор в кладовке где-то лежит.