В этой небольшой статье хочу рассказать вам о различных методах измерения частоты прямоугольного сигнала с помощью микроконтроллера STM32.
В процессе работы над одной из железок возникла необходимость организовать несколько выводов, которые бы измеряли частоту входного сигнала. Опробовав несколько разных вариантов, я решил, что негоже примерам пылиться на задворках диска D и стоит ими поделиться с сообществом. Надеюсь кому-то, находящемся в похожей ситуации, этот материал будет полезен. Материал в первую очередь рассчитан на новичков.
![Наш сегодняшний стенд: генератор частоты JDS6600 и BluePill Наш сегодняшний стенд: генератор частоты JDS6600 и BluePill](https://habrastorage.org/getpro/habr/upload_files/9c5/d1a/9b9/9c5d1a9b9996e1a52530fe4d83e9477e.jpg)
Начальные условия: частота входного сигнала от 0 до 10 кГц. Микроконтроллер STM32F103C8T6, всем известная плата bluepill. Библиотека HAL. Источником сигнала, частота которого будет измеряться, для проверки работоспособности будет служить двухканальный генератор частоты JDS6600. Я буду использовать модуль CH340G (Преобразователь USB - UART) для передачи данных в терминал (terminal v1.9b) для наглядности. В него я буду посылать полученное после обработки значение.
Рассмотрим первый способ – измерение частоты с помощью таймера.
Подключение: Генератор частоты напрямую подключен к выводам микроконтроллера PA0 и PA2. Выводы земли объединены.
Настройка проекта: перейдем в STM32IDE и создаем проект под наш контроллер. После настройки источника тактирования включаем UART для отправки значений:
![Настройка UART Настройка UART](https://habrastorage.org/getpro/habr/upload_files/261/54e/5ec/26154e5ec3ad2a7e20eb2344a98dec9c.jpg)
Частоту тактирования настроил на 64 МГц.
В примере я буду использовать Timer 2. Настроим таймер на обработку сразу двух сигналов:
![](https://habrastorage.org/getpro/habr/upload_files/b73/707/afa/b73707afa6df77b2c4c53ec7d14f4232.jpg)
Канал 1 — это основной (direct) канал, а канал 2 это косвенный (indirect). Косвенный канал не имеет отдельного вывода. Аналогично для канала 3 и 4. Захват частоты происходит следующим образом – первый канал реагирует на передний фронт, и мы фиксируем время начала нового периода. После второй канал фиксирует задний фронт, и мы фиксируем время окончания импульса. Затем первый канал фиксирует начало нового периода / окончание предыдущего. Имея эти три временные точки, можно посчитать длительность периода и скважность. Значение делителя и периода Timer 2:
![](https://habrastorage.org/getpro/habr/upload_files/419/683/942/4196839429c66586c0a9cc540f3fad5f.jpg)
Значение предделителя определяет шаг, с которым будет считать таймер. Чем меньше шаг, тем точнее результат, но тогда можно столкнуться с тем, что в длительность одного периода таймера не будет помещаться захватываемый сигнал. При частоте тактирования таймера 64 МГц и предделителе = 64 (предделитель в программе выставляется на 1 меньше) получаем, что длительность одного тика таймера равна микросекунде. Период (Counter periode) определяет, до какого значения будет считать таймер до переполнения. В данной задаче лучше его оставить в максимальном значение.
Фильтр входного сигнала (Input Filter) используется для устранения помех и шумов, которые могут возникать на входе таймера. Он позволяет фильтровать входной сигнал и принимать во внимание только стабильные и длительные изменения сигнала, игнорируя кратковременные нежелательные помехи. Если входной сигнал стабилен и не подвержен сильным помехам, можно установить значение фильтра на минимальное или даже отключить его. Однако, при работе с шумными или нестабильными входными сигналами, может потребоваться увеличение значения фильтра для надежной и стабильной работы таймера. Я выставляю Input Filter в максимум. Включим прерывание таймера:
![](https://habrastorage.org/getpro/habr/upload_files/ffb/295/21f/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.
![Картинка для понимания, что в каждой переменной будет храниться. Картинка для понимания, что в каждой переменной будет храниться.](https://habrastorage.org/getpro/habr/upload_files/466/953/58b/46695358b850b1f8b9d1b2d163063c10.jpg)
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;
}
Я запускаю генератор частоты с вот такими настройками:
![](https://habrastorage.org/getpro/habr/upload_files/57d/d69/09b/57dd6909bfea2fdbed857280851a1e36.jpg)
Оправляем два прямоугольных сигнала с частотой 1кГц и 850 Гц. Теперь посмотрим, что нам пришло на терминал:
![](https://habrastorage.org/getpro/habr/upload_files/30e/9a2/858/30e9a2858494b783ab140e4777d4feaf.jpg)
В терминале выставляем скорость 115200 и получаем результат. Теперь проверим этот же код, но на динамический сигнал. Я покрутил значение частоты на первом канале от 1 кГц до 8 кГц и обратно с шагом в 1 кГц и вот что получил:
![](https://habrastorage.org/getpro/habr/upload_files/3a6/396/e37/3a6396e379e593ed11e5ca230e467e89.jpg)
В принципе работает, но в реальных системах частота может менять сильно быстрее, чем я кручу ручку, так что тест с динамическим сигналом правильно считать упрощенным.
Достоинства и недостатки:
+ | - |
Есть возможность измерить скважность импульса. | Необходимы таймеры, по одному таймеру на два входа. Маленького контроллера под большое количество сигналов может просто не хватить, так как многие другие задачи тоже требуют свободных таймеров. |
В отличие от других методов, может за 1 период посчитать частоту сигнала (при отключенном фильтре). | Ограничение диапазона захвата частоты. В данном примере таймер считает до 65 мс, и если период сигнала больше этого значения, например 100 мс (10 Гц), то таймер не сможет его захватить без дополнительных программных ухищрений. |
| Если входной сигнал пропал – значения частоты не обнулится само по себе. Можно проверять, меняется ли значение частоты, и если нет, то обнулять значение частоты, или же выставлять какой-нибудь флаг при попадании в callback. |
Теперь рассмотрим вариант измерения значения частоты с использованием вывода EXTI.
Выводы EXTI (External Interrupt) у микроконтроллеров STM32 предназначены для обработки внешних прерываний от различных источников. EXTI позволяет реагировать на изменения состояния внешних сигналов и генерировать прерывания для обработки этих событий. Логика работы довольно проста – каждый раз, когда приходит импульс, мы по прерыванию будем инкрементировать значение импульсов, а раз в секунду смотреть, сколько импульсов пришло. Так и получим необходимую нам частоту.
Подключение: генератор частоты подключен к выводам PA0 и PA1.
Настройка проекта: В настройках контроллера включаем выводы PA0 и PA1 как выводы EXTI. Также включаем прерывание у них. Прерывание будет срабатывать на восходящий фронт сигнала.
![](https://habrastorage.org/getpro/habr/upload_files/d1d/b61/75b/d1db6175b88883a6187dcda8d1d5b163.jpg)
Также надо включить таймер, который будет отсчитывать одну секунду, в прерывании которого мы будем фиксировать нашу входную частоту:
![](https://habrastorage.org/getpro/habr/upload_files/c67/c21/f67/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 кГц на втором:
![](https://habrastorage.org/getpro/habr/upload_files/6f4/77d/2a2/6f477d2a2486b3261601972b1c26d413.jpg)
Вот результат захвата динамического сигнала (канал ch2 статичный):
![](https://habrastorage.org/getpro/habr/upload_files/c70/3a9/f60/c703a9f60171c7e9f9958439916d6a79.jpg)
Отлично, все работает. Теперь о достоинствах и недостатках:
+ | - |
Довольно простой метод | Данные приходят довольно редко – в данном случае раз в секунду |
Нужен всего один таймер |
|
По умолчанию сразу получаем усредненный результат |
И последний рассмотренный мною метод – захват с помощью ETR2. При настройке источника тактирования таймера можно выбрать вывод ETR2 (External Trigger Input 2), и тогда таймер в качестве источника тактирования будет использовать внешний источник. На этот вывод будет подаваться замеряемый сигнал.
Реализуем частотомер второго рода. Основной принцип работы частотомера второго рода заключается в сравнении частоты входящего сигнала с частотой, генерируемой внутри прибора, в нашем случае длительность периода внешнего сигнала будет измеряться в кол-ве тиков другого таймера.
Заводим timer 2, который просто считает импульсы с максимально возможной частотой. По приходу первого импульса на ETR2 запускается timer 4. Когда на timer 2 придет, допустим, сотый импульс, мы фиксируем, сколько импульсов насчитал timer 4. Делим это значение на 100 и получаем период захватываемого сигнала. Картинка для чуть лучшего понимания:
![](https://habrastorage.org/getpro/habr/upload_files/94e/4e9/816/94e4e9816f5a593b4f1050335675338c.jpg)
Настройка проекта: настроим таймера 2, на который будет подаваться входной сигнал. Считает до 100го импульса, а потом счет сбрасывается и начинается новый период. Именно в этот момент мы будем фиксировать кол-во отсчитанного времени на 100 периодов входного сигнала.
![](https://habrastorage.org/getpro/habr/upload_files/b50/0a2/144/b500a2144bee18c05b901224162e48ca.jpg)
Настройка таймера 4 для отсчета времени:
![](https://habrastorage.org/getpro/habr/upload_files/6b9/ff1/b60/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 кГц:
![](https://habrastorage.org/getpro/habr/upload_files/673/d22/497/673d224977417ea2a813be4043c3c621.jpg)
Теоретически, этот метод должен быть с самым большим диапазоном частоты входного сигнала. Попробуем 100 кГц:
![](https://habrastorage.org/getpro/habr/upload_files/7bb/d98/5d7/7bbd985d70c43762f7c1533b9fa794dd.jpg)
Ну и 1 МГц:
![](https://habrastorage.org/getpro/habr/upload_files/8c5/6a8/c34/8c56a8c34dab15574d99c54b664b8a22.jpg)
Последний результат уже с большой погрешностью, но, тем не менее, работает.
Достоинства и недостатки:
+ | - |
Больший диапазон относительно других | Нужен целый таймер с выводом ETR на каждый отдельный таймер для счета |
Конечно, тут описаны не все возможные способы измерения частоты входного сигнала, а только несколько, с которыми я столкнулся. Некоторые из них были проверены только на макете и в реальных задачах могут вести себя отлично от того, что получилось у меня.
Для себя я выбрал вариант с выводами EXTI, так как в устройстве не требуется быстродействие и большая точность, но требуется много каналов захвата. Первый метод мне понравился меньше всех остальных, потому что в остальных вариантах мы получаем сразу некое усредненное значение, что мне подходило намного лучше.