[Home] [Donate!] [Контакты]

Частотомер на основе микроконтроллера STM32

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

При этом, строго говоря, на самом деле мы определяем период входного сигнала. А точнее, среднее значение периода за время измерения. Так, если за m периодов входного сигнала мы насчитали n импульсов эталонного генератора, частота которого равна F0, то среднее значение для одного периода входного сигнала T = n / (m F0). Однако, зная период, нетрудно вычислить и частоту: $$ F = \frac 1 T = F_0 \frac m n. $$

Применяя этот метод, легко создать хороший частотомер лишь на одном микроконтроллере, используя его таймеры. В качестве примера построим на основе микроконтроллера STM32F100RB (тактовая частота всего 24 МГц) частотомер со следующими характеристиками:
диапазон измеряемых частот от 0.015 Гц до 10 МГц;
перестраиваемое время на одно измерение, от 0.1 мс до 170 с;
точность счёта (без учёта погрешности, вносимой тактовым генератором микроконтроллера) не хуже 5*10-8 при времени измерения 1 с и тактовой частоте микроконтроллера 24 МГц (т.е., например, при измерении частоты 1 МГц, точность метода составит около 0.05 Гц при времени измерения 1 с; 0.5 Гц при времени измерения 0.1 с и т.д.).

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

Характеристики могут быть существенно улучшены при незначительной модернизации схемы и программы устройства. Особенно интересна возможность реализации непрерывного счёта и конвейеризации измерения частоты, за счёт чего увеличивается количество измерений в единицу времени без уменьшения интервала измерения (и точности). Благодаря этому получаем намного больше информации о сигнале. Структура частотомера и его программа при этом не только не усложняются, но даже несколько упрощаются.

Оглавление
Частотомер на основе микроконтроллера STM32
Введение
Возможные проблемы при реализации
Пример реализации частотомера на STM32F100RB
Программа для частотомера на STM32
Комментарии к программе
Источники информации и дополнительная литература
Смотрите также
Частотомер на основе микроконтроллера STM32. Конвейерный принцип измерения частоты
Таймеры в микроконтроллерах STM

Введение

Итак, для измерения частоты входного сигнала, берём интервал времени, содержащий m периодов сигнала и, смотрим, какое количество n импульсов эталонного генератора с частотой F0 укладывается в этот интервал времени (рис. %img:rcd). Частоту сигнала определяем как $$ F = \frac 1 T = F_0 \frac m n. $$

Измерение частоты сигнала методом обратного счёта.
Рис. %img:rcd

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

Перед выполнением измерения следует задать желаемый интервал измерения \( \tau \), т.е. время, отводимое на одно измерение. Эту величину можно изменять в широких пределах, в зависимости от требований к скорости измерения и точности результата. Относительная погрешность измерения без учёта ошибки, возникающей из-за нестабильности эталонного генератора (только погрешность счёта) может быть оценена как $$ \varepsilon = 1 / n \approx \frac 1 {\tau F_0}. $$ Здесь мы учли то, что абсолютная точность подсчёта n составляет \( \pm 1 \). Как видим, увеличение интервала измерения снижает относительную погрешность счёта (этого же можно добиться при неизменном интервале, увеличивая частоту эталонного генератора).

Также от выбранного интервала измерения зависит минимальная частота сигнала, которую частотомер способен измерить (нижний предел измерения). Для гарантированно успешного измерения, интервал измерения должен быть хотя бы вдвое больше периода измеряемого сигнала (для большей надёжности лучше, если условие выполняется с запасом), т.е. $$ {\tau}_{min} \gt 2 / F_{min}. $$ При выполнении этого условия, независимо от начальной фазы входного сигнала, в интервал измерения попадёт, как минимум, два фронта сигнала (что позволит выделить хотя бы один период сигнала). Например, при нижнем пределе 10 Гц, интервал измерение должен быть не менее 0.2 с. А при выборе интервала в 1 мс (для проведения высокоскоростных измерений), нижний предел прибора составит 2000 Гц.

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

Например, на рис. %img:rcd, в заданный интервал измерения попадают 4 фронта сигнала, между которыми заключены m = 3 полных периода сигнала, которые и образуют действительный интервал.

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

Итак, завершив измерение, мы определяем две величины: количество периодов входного сигнала m, определяющих время счёта и количество импульсов n опорного генератора за это время. Зная частоту опорного генератора, рассчитываем частоту сигнала.

Структурная схема простейшего частотомера на таймере MCU STM32.
Рис. %img:sfm

Таймеры микроконтроллеров STM32 прекрасно подходят для реализации описанной выше идеи измерения частоты (рис. %img:sfm). Таймеры общего назначения этих микроконтроллеров имеют так называемые каналы, которые способны фиксировать текущее значение счётчика таймера по фронту внешнего сигнала.

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

По каждому фронту входного сигнала таймер будет фиксировать текущее значение счётчика в специализированном регистре канала. Наша задача - сохранить результат первой фиксации n1, затем дождаться, пока истечёт интервал измерения, сохранить результат последней фиксации n2 и общее количество фиксаций c. Тогда количество импульсов от первой до последней фиксации составит n2 - n1. Количество периодов входного сигнала между первой и последней фиксацией будет равно c - 1 (так как количество отрезков разбиения на 1 меньше количества точек разбиения). Частоту сигнала вычисляем как $$ F = F_0 \frac {c - 1} {n2 - n1}. $$

Если c < 2, значит, нам не удалось выделить полный период за выбранный интервал измерения (слишком низкая частота входного сигнала для данного интервала измерения).

Возможные проблемы при реализации

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

Во-первых, в большинстве случаев счётчики таймеров имеют маленькую разрядность (обычно 16 бит, только в некоторых моделях 32 бита и то не для всех таймеров). Максимальное значение, до которого могут вести счёт 16-разрядные счётчики, составляет всего лишь 0xFFFF = 65535. Поэтому счётчики будут часто переполняться, и мы не сможем определить истинный результат счёта, прочитав значение из счётчика или прочитав зафиксированное в канале значение. Значит, нужно программно расширять разрядность счётчика, производя учёт переполнений в отдельной переменной, значение которой будет обновляться соответствующим обработчиком прерываний от таймера.

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

Уточнение истинного момента захвата счётчика.
Рис. %img:capt1

Но обработчик, учитывающий переполнения счётчика, не всегда имеет достаточно информации, чтобы определить, когда произошла фиксация - до переполнения или после. Конечно, если зафиксировано большое значение, больше текущего значения счётчика, это означает, что фиксация произошла до переполнения и тогда можно восстановить истинное значение результата счёта на момент фиксации (рис. %img:capt1). Ситуация усложняется, если зафиксированное значение невелико. Тогда фиксация могла произойти и после данного переполнения, и вскоре после предыдущего переполнения (но после того, как во время предыдущего вызова обработчика, он проверял наличие новых фиксаций). В такой ситуации нельзя сделать однозначного вывода о моменте фиксации* (рис. %img:capt2).

* Можно ввести проверку дополнительного условия: если зафиксированное значение очень мало, близко к 0, то фиксация должна была произойти после переполнения (иначе фиксация должна была бы произойти через очень малое время после предыдущего переполнения, но тогда обработчик успел бы её учесть во время предыдущего вызова). Однако, в любом случае, для результата фиксации существует диапазон значений, в котором возникает неоднозначность в определении момента фиксации.

Уточнение истинного момента захвата счётчика.
Рис. %img:capt2

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

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

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

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

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

В-четвёртых, может возникнуть проблема с подсчётом количества фиксаций в канале за интервал измерения. Если требуется измерение только низких частот, эту задачу вполне можно возложить на обработчик прерывания (разрешив прерывания при выполнении фиксации в канале; на таких низких частотах не будет проблем и с чтением результата первой фиксации, даже не нужно привлекать DMA). Достоинство такого подхода - для реализации частотомера требуется только лишь один таймер. Но этот вариант совершенно непригоден для универсального частотомера, способного работать на любых частотах в пределах скоростных возможностей таймера.

Тогда можно предложить другой способ. В микроконтроллерах STM32 таймеры можно объединять в цепочки, так что внутренний выходной сигнал (TRGO) одного таймера (ведущего) будет управлять другим таймером (ведомым). Например, можно настроить таймер так, чтобы он формировал выходной импульс при каждой фиксации значения в канале. А управляемый этим таймером другой, ведомый таймер, можно настроить на подсчёт этих импульсов. Таким образом, будет решена не только задача подсчёта количества фиксаций (то есть количества фронтов входного сигнала), но и синхронизация остановки подсчёта фронтов с моментом последней фиксации. Когда мы отключаем канал в ведущем таймере, фиксации прекращаются, а значит, формирование выходных импульсов ведущего таймера также прекращается. Следовательно, с этого момента значение счётчика ведомого таймера не изменяется. В счётчике остаётся значение - количество подсчитанных фронтов входного сигнала за интервал измерения (за истинный интервал измерения) с точностью до поправки на переполнения.

Пример реализации частотомера на STM32F100RB

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

Рассмотрим пример построения точного и быстрого частотомера на базе достаточно простого микроконтроллера STM32F100RB с максимальной тактовой частотой 24 МГц (в примере работает на этой частоте). Разумеется, необязательно использовать именно этот микроконтроллер, как раз наоборот, STM32F100RB здесь далеко не оптимальный вариант: с одной стороны, он довольно низкочастотный (на большинстве других устройств удастся построить частотомер с большим верхним пределом измерения); с другой стороны - он имеет очень богатую периферию, совершенно избыточную в данном случае (лучше выбрать микроконтроллер попроще и подешевле, но побыстрее)*. Здесь выбор был продиктован не соображениями оптимизации, а тем, какой микроконтроллер оказался под рукой.

* Предполагается, что таймеры в выбранном микроконтроллере имеют 16-разрядные счётчики. В случае 32-разрядных счётчиков, большинство используемых здесь ухищрений становятся ненужными; программная реализация частотомера значительно упрощается.

С точки зрения схемы, рассматриваемый частотомер предельно прост. Состоит из микроконтроллера и его минимальной обвязки, обеспечивающей питание и генерацию тактовых сигналов. Вход прибора - вывод PA1 микроконтроллера; вход цифровой (TTL / CMOS 3.3V совместимый). Для работы с аналоговым сигналом, следует преобразовать сигнал в импульсный, дополнив схему блоком формирования импульсного сигнала из аналогового. Для этого достаточно воспользоваться любым подходящим по частотным параметрам усилителем-ограничителем; в некоторых случаях аналоговый сигнал может подаваться на вход PA1 непосредственно, с учётом того, что на входах микроконтроллера имеются собственные триггеры Шмитта. Итак, будем считать, что исследуемый сигнал изначально импульсный или уже преобразован в импульсный и сосредоточимся на цифровой части прибора.

Частотомер на микроконтроллере STM32.
Рис. %img:cir1*

* Примечания.
1. Конденсаторы C4..C7 размещаются по возможности ближе к выводам питания микроконтроллера VDD, VSS.
2. Кварцевый резонатор X2 с частотой 32768 Гц нужен для работы часов реального времени; если часы не используются, установка элементов X2, C13, C14 необязательна.
3. Вывод сброса NRST не требует внешнего смещения, поскольку микроконтроллер имеет внутренний подтягивающий резистор на этом выводе.
4. На схеме не изображён индикатор, предполагается подключение отдельного модуля индикации через интерфейс UART, SPI или I2C; не исключается также возможность подключения многоразрядного семисегментного индикатора; в процессе тестирования использовалось подключение частотомера к компьютеру через UART (UART-USB преобразователь).

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

Частотомер на основе оценочной платы с микроконтроллером STM32.
Рис. %img:cir2

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

Структурная схема частотомера на микроконтроллере STM32.
Рис. %img:bd2

На рисунке блок OSC условно изображает стабилизированную кварцевым резонатором систему генерации тактового сигнала микроконтроллера. Импульсы тактового сигнала подсчитываются счётчиком в таймере TIM2. По каждому фронту входного сигнала, частоту которого измеряем, значение счётчика таймера TIM2 фиксируется в регистре CCR1 первого канала этого таймера. Таймер TIM2 настроен таким образом, что при каждой фиксации он формирует импульс на своём внутреннем выходе TRGO. Таймеры TIM2, TIM1 сконфигурированы для совместной работы; TIM2 является ведущим, TIM1 - ведомым. Таймер TIM1 подсчитывает импульсы на выходе TRGO таймера TIM2, т.е. количество фиксаций в его канале (иначе говоря, количество фронтов входного сигнала). Одновременно сигнал TRGO таймера TIM2 используется как запрос на пересылку данных с помощью DMA. Система DMA настраивается на пересылку единственного значения: первое зафиксированное в канале значение сохраняется в предназначенной для этого переменной в памяти, после чего DMA автоматически останавливается.

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

Используемая схема определяет верхний предел измерения прибора: максимальная измеряемая частота составляет примерно 10 МГц, при условии, что сигнал близок к меандру, т.е. длительность импульсов не слишком сильно отличается от длительности пауз между ними, иначе говоря, скважность близка к 2 (соответственно, коэффициент заполнения близок к 0.5).

Ограничение связано с тем, что в качестве входа частотомера используется вход таймера, управляющий каналом таймера. Вход таймера имеет схему ресинхронизации, задача которой - привязать моменты переключения входного сигнала к тактовому сигналу (подробнее смотрите "Устройство таймера. Обработка внешних входных сигналов"). Если промежуток времени между двумя последовательными переключениями уровня сигнала составляет менее периода тактового сигнала таймера, то схема ресинхронизации может "не заметить" переключение. Чтобы этого не происходило, как длительность входного импульса, так и длительность паузы между импульсами, должны быть больше периода тактового сигнала. Тогда гарантируется работа без пропуска импульсов. Следствием указанного условия является то, что период входного сигнала не должен быть менее двух периодов тактового сигнала, а частота сигнала, соответственно не должна быть более половины тактовой частоты (12 МГц при тактовой частоте 24 МГц, но лучше с запасом; поэтому мы выбираем значение 10 МГц). Следует иметь в виду, что ограничение по частоте является ориентировочным и не является достаточным условием нормальной работы. Как было отмечено, важно, чтобы как длительность импульса, так и паузы между импульсами были больше периода тактового сигнала; если длительности сильно отличаются, максимальная частота будет намного меньше.

Увеличить верхний предел измерения можно, включив на входе частотомера один или несколько последовательно соединённых T-триггеров. Кроме того, что каждый триггер вдвое снижает частоту, он также обеспечивают выравнивание длительности импульса и длительности паузы (скважность выходного сигнала триггера получается равной 2, рис. %img:tt1), по крайней мере, для регулярного входного сигнала, без резких скачков частоты (как на рис. %img:tt2).

Преобразование сигнала T-триггером.
Рис. %img:tt1

Преобразование сигнала T-триггером.
Рис. %img:tt2

Было бы выгодно использовать в качестве входа частотомер вход ETR таймера, на котором установлен асинхронный управляемый делитель частоты. Тогда без проблем получили бы верхний предел, равный тактовой частоте микроконтроллера (24 МГц в данном случае), без строгих ограничений на скважность импульсов. Но вход ETR не может управлять фиксацией в каналах. Вход ETR всё же можно использовать, но тогда придётся задействовать дополнительный таймер, который на основе сигнала со своего ETR входа с помощью своего канала будет формировать сигнал на выходе, и уже частота сигнала с этого выхода будет измеряться так, как это было описано выше.

Программа для частотомера на STM32

Итак, в качестве основного таймера, который считает импульсы опорного генератора и выполняет фиксации значений своего счётчика по входному сигналу, выбран TIM2. Для управления первым каналом таймера используется вывод PA0 или PA1. Здесь будем использовать PA1 (в моей отладочной плате PA0 занят кнопкой). Заметим, что PA1 вывод связан со входом TI2 таймера TIM2, но сигнал TI2 с помощью настроек таймера можно перенаправить на канал 1.

Выбор первого канала для фиксации обусловлен тем, что таймер можно настроить на формирование триггерного импульса на внутреннем выходе TRGO таймера именно в ответ на фиксации в первом канале (для этого следует задать битовому полю MMS в регистре TIM2->CR2 значение 0x3).

Второй канал таймера используем для генерации дополнительного прерывания, предшествующего прерыванию по переполнению счётчика. Канал работает в режиме сравнения, в его регистр CCR2 записывается значение (ARR+1)/2, по умолчанию 0x8000. Это значит, что дополнительное прерывание будет возникать при достижении счётчиком половины от переполняющего значения (т.е. прерывание возникает как раз посередине между переполнениями).

Использование таймера TIM2 и его канала 1 для фиксации диктует выбор используемого канала DMA, это будет канал 5 устройства DMA1.

Для подсчёта количества фиксаций в канале 1 таймера TIM2 можно использовать любой таймер, способный работать в качестве ведомого для TIM2. Например, пусть это будет TIM1.

Начальная инициализация оборудования выполняется в функции fm_init, которая вызывается в начале main. Здесь выполняется настройка NVIC (задаётся приоритет используемых прерываний и разрешается их обработка); включается тактовый сигнал для задействованных в работе периферийных устройств (DMA1, TIM1, TIM2, GPIOA); выполняется базовая настройка таймеров и DMA (задаются те настройки, которые будут оставаться неизменными на протяжении всей работы).

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

Каждое измерение частоты требует вызова двух функций: measure и get_result (до первого измерения следует вызвать fm_init() для начальной инициализации средств измерения частоты). Выполнение измерения в два шага позволяет освободить микроконтроллер для выполнения других задач на время измерения. Функция measure запускает измерение и сразу завершается, само измерение происходит в фоновом режиме. Пока идёт измерение, можно занять микроконтроллер любой другой полезной работой. Когда измерение завершится, используем get_result для считывания результата.

Рассмотрим подробнее использование каждой из этих функций.

bool measure(uint32_t ticks)
Аргумент ticks задаёт желаемый интервал измерения в тактах (или периодах) сигнала на счётном входе таймера TIM2, т.е. в периодах сигнала эталонного генератора. В данном случае - просто в периодах тактового сигнала микроконтроллера. Возможно, не очень удобно то, что интервал, заданный в единицах времени, нужно преобразовывать в такты (ticks = SystemCoreClock * t, здесь SystemCoreClock - частота тактового сигнала на счётном входе таймера TIM2; t - заданное время измерения). Зато удобно сразу оценивать относительную погрешность счёта, которая составляет 1.0 / ticks.

Например, при тактовой частоте 24 МГц, чтобы получить интервал измерения 1 с, следует задать ticks = 24000000. При этом относительная погрешность счёта окажется не более 5*10-8 (при использовании типа float для вычислений, стоит иметь в виду, что указанная точность находится уже на пределе возможностей этого типа).

Значение для ticks при желании можно задать очень малым (вплоть до 0), но не стоит при этом рассчитывать на то, что интервал будет выдержан с высокой точностью. Во-первых, хотя интервал отсчитывается при помощи таймера достаточно точно, но завершение измерения происходит в обработчике прерывания. От момента срабатывания таймера до момента фактического завершения измерения может пройти порядка 100 тактов (около 4 мкс при тактовой частоте микроконтроллера 24 МГц). К счастью, точность, с которой отмеряется интервал, не влияет на точность измерения (зато на точность влияет абсолютное значение интервала измерения - чем короче интервал, тем хуже точность; не стоит без особой необходимости задавать сверхкороткие интервалы измерения). Во-вторых, сам метод измерения предполагает, что действительный интервал измерения может отличаться от заданного (так как он всегда будет образован целым количеством периодов сигнала, частоту которого измеряем).

Функция measure выполняет инициализацию переменных, используемых в ходе измерения; обновляет настройки таймеров и DMA; запускает таймеры; разрешает работу каналов таймера TIM2. Тем самым она запускает новое измерение частоты. После этого функция сразу завершается, не дожидаясь завершения измерения, которое будет происходить в фоновом режиме. Благодаря этому, пока происходит измерение частоты, можно выполнять другую полезную работу. О завершении измерения можно узнать по установленной в true глобальной переменной fN2e.

Функция measure возвращает true в случае успешного запуска нового измерения. Если же в момент вызова уже происходит измерение (запущенное ранее и ещё не закончившееся), функция завершается, не вмешиваясь в текущее измерение, и возвращает значение false (это свидетельствует об ошибке использования функции - она была вызвана несвоевременно).

Когда измерение завершено и установлена переменная-флаг fN2e, можно запросить результаты измерения, для этого используется функция
int get_result(uint32_t &N1, uint32_t &N2).
Функцию допускается вызывать и досрочно, не дожидаясь завершения измерения (в том числе и сразу после вызова measure), но тогда она будет ждать установки fN2e, опрашивая состояние переменной в программном цикле. Если мы не хотим, чтобы функция приостанавливала выполнение программы, то должны вызывать её, только убедившись, что переменная fN2e установлена в true.

Возвращаемое функцией значение - код ошибки:
0 - нет ошибок; в N1, N2 помещены верные значения.
-1 - не удалось выполнить измерение, так как за интервал измерения обнаружено менее двух фронтов входного сигнала (для выполнения измерения необходимо выделить хотя бы один целый период входного сигнала, а для этого нужно получить два фронта сигнала); причиной ошибки является выбор слишком малого интервала измерения для данной частоты сигнала; для гарантированно успешного измерения частоты сигнала, выбранный интервал измерения должен хотя бы вдвое превышать максимально возможный период входного сигнала.

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

Частота входного сигнала тогда может быть рассчитана, например, так:

float f = SystemCoreClock * (float)N1 / N2;

Полная последовательность действий для измерения частоты сигнала, допустим, с интервалом измерения 0.1 с (и с ожиданием завершения измерения в функции get_result), может быть примерно такой:

// Интервал измерения в тактах системного тактового сигнала.
const uint32_t mi = SystemCoreClock * 0.1;

// Запускаем измерение.
measure(mi);

// Ждём завершения измерения и считываем результат.
uint32_t N1 = 0, N2 = 0;
int err = get_result(N1, N2);

// Если измерение выполнено успешно, вычисляем частоту
// (в случае ошибки будет получено нулевое значение частоты).
float f = 0;
if(err == 0)
    f = SystemCoreClock * (float)N1 / N2;

То, что функция get_result возвращает необработанные данные, а не вычисляет частоту сама - сделано намеренно, чтобы не ограничивать пользователя в выборе типов для вычислений и результата. В зависимости от требуемой точности, ожидаемого диапазона значений, имеющихся в распоряжении вычислительных ресурсов, это может быть и тип double, и float, и числа с фиксированной точкой.

Далее приведён полный текст программы для измерения частоты (смотрите также комментарии после текста программы).

// Частотомер 0.015Гц..10МГц.
// Отлажено на MCU: STM32F100RB; кварц 8МГц; тактовая частота 24МГц.
// Вход прибора: PA1 вывод MCU (цифровой TTL/CMOS3.3V совместимый).
// (PA1 - вход канала TIM2_CH2 или TIM1_CH1).

#include "stm32f10x.h"

// Объявление функций для инициализации средств отображения и
// вывода результатов (определения функций - в отдельном файле).
int display_init();
int display(uint32_t fclk, uint32_t m, uint32_t n, uint32_t err);

// ********************************************************************
// Глобальные переменные, используемые обработчиком прерывания
// TIM2_IRQHandler для собственных нужд, а также для взаимодействия
// обработчика и основной программы.
// Определение этих переменных как глобальных упрощает выполнение их
// начальной инициализации, которая необходима перед каждым запуском
// измерения.
// ********************************************************************

// Предыдущее значение количества подсчитанных фронтов входного сигнала:
// сравнивая с текущим количеством (подсчитанных с начала измерения),
// обработчик TIM2_IRQHandler определяет, были ли с момента предыдущего
// вызова обнаружены новые фронты входного сигнала.
// При каждом вызове обработчик обновляет переменную текущим значением.
volatile uint32_t N1_prev=0;

// Корректирующее значение для счётчика TIM1->CNT, учитывающее
// переполнения счётчика; действительное значение количества импульсов
// равно (N1_correction + TIM1->CNT)
volatile uint32_t N1_correction = 0;

// Корректирующее значение для счётчика TIM2->CNT, учитывающее
// переполнения счётчика.
volatile uint32_t N2_correction = 0;

// Корректирующее значение счётчика для момента, когда фиксируется
// значение N2b_naked (фиксация по первому фронту входного сигнала).
volatile uint32_t N2b_correction = 0;

// Значение счётчика TIM2->CNT в момент обнаружения первого фронта
// входного сигнала (соответствующая ему поправка на переполнения
// хранится в переменной N2b_correction).
// Значение записывается в переменную системой DMA.
volatile uint32_t N2b_naked = 0;

// Количество учтённых фронтов сигнала за интервал измерения
// (истинное значение, с учётом переполнений счётчика).
volatile uint32_t N1e = 0;

// Значение счётчика таймера TIM2 в момент последней фиксации (по
// последнему учитываемому фронту входного сигнала); скорректировано
// с учётом количества переполнений счётчика.
volatile uint32_t N2e = 0;

// Флаг, устанавливаемый в true после того как в переменные N1e, N2e
// записываются результаты, полученные в процессе измерения
// (устанавливается в false перед началом измерения в функции measure).
volatile bool fN2e = true;

// Время, отводимое на измерение в тактах сигнала на счётном входе TIM2.
// За точную выдержку длительности интервала измерения отвечает канал 3
// таймера TIM2.
uint32_t time_slot = 0;

// Учёт переполнений счётчика в таймере TIM1.
extern "C" void TIM1_UP_TIM16_IRQHandler()
{
    if(TIM1->SR & TIM_SR_UIF)
    {
        TIM1->SR = ~TIM_SR_UIF;
        N1_correction += TIM1->ARR + 1; // По умолчанию 0x10000.
    }
}

// Таймер TIM2 настроен таким образом, что прерывание возникает
// при переполнении счётчика и при достижении счётчиком половины
// переполняющего значения (за это отвечает канал 2).
// Это необходимо для правильного определения корректирующего значения
// в момент выполнения фиксации в канале 1 (для учёта переполнений).

// Обработчик прерываний от TIM2 выполняет основную работу по измерению.
// Вызывается при переполнении счётчика; "полупереполнении" и
// при срабатывании схемы сравнения в канале 3.
extern "C" void TIM2_IRQHandler()
{
    // Если отведённое время истекло, завершаем измерение.
    if(TIM2->SR & TIM_SR_CC3IF)
    {
        TIM2->SR = ~TIM_SR_CC3IF;    // Сброс флага.
        uint32_t t = N2_correction + TIM2->CCR3;

        // Учёт только что произошедшего переполнения.
        if((TIM2->SR & TIM_SR_UIF) &&
                TIM2->CCR3 < TIM2->CNT)
            t += TIM2->ARR + 1;
        if(t >= time_slot)
        {
            // Отключаем канал 1 таймера (с этого момента прекращаются
            // фиксации значения счётчика по фронту входного сигнала).
            TIM2->CCER &= ~TIM_CCER_CC1E;

            // Останавливаем таймеры.
            TIM2->CR1 &= ~TIM_CR1_CEN;
            TIM1->CR1 &= ~TIM_CR1_CEN;

            // При следующем измерении необходимо разрешить генерацию
            // запросов DMA от таймера, но перед этим её обязательно
            // следует запретить (цикл запрет/разрешение обязателен, чтобы
            // избежать ложного срабатывания канала DMA
            // при последующем его включении).
            TIM2->DIER &= ~TIM_DIER_CC1DE;

            // Также отключаем канал DMA до следующего измерения
            // (для нового измерения нужно будет перенастроить канал DMA,
            // что возможно только при отключённом канале).
            DMA1_Channel5->CCR &= ~DMA_CCR5_EN;
        }
        else        // Если время не истекло, выходим из обработчика, а
            return; // если истекло - продолжаем завершающие действия.
    }

    // Маска для интересующих нас флагов
    // (переполнение и сравнение в канале 2).
    const uint16_t mask = TIM_SR_UIF | TIM_SR_CC2IF;

    // Запоминаем значение из регистра состояния таймера TIM2.
    uint16_t sr = TIM2->SR;

    // Сразу же сбрасываем установленные обрабатываемые флаги.
    TIM2->SR = ~(sr & mask);

    // Если произошло переполнение счётчика, учитываем его.
    if(sr & TIM_SR_UIF)
        N2_correction += TIM2->ARR + 1;

    // Определяем текущее значение счётчика фиксаций в канале 1 таймера
    // (подсчёт количества фиксаций ведёт счётчик таймера TIM1).
    uint32_t N1 = N1_correction + TIM1->CNT;

    // Корректируем текущее значение при обнаружении переполнения.
    if(N1 < N1_prev)
        N1 += TIM1->ARR + 1;

    // Проверяем наличие новых фиксаций с момента последнего выполнения
    // данного обработчика.
    if(N1 != N1_prev)
    {
        // Если обнаружена первая фиксация, определяем и сохраняем
        // корректирующее значение для неё.
        if(N1_prev == 0)
        {
            N2b_correction = N2_correction;
            // Проверяем, не произошла ли эта фиксация до переполнения.
            if((sr & TIM_SR_UIF) &&
                    N2b_naked > TIM2->CNT)
                N2b_correction -= TIM2->ARR + 1;
        }

        // Любая фиксация может оказаться последней, поэтому всегда
        // обновляем переменную N2e (при этом учитываем, что
        // фиксация могла произойти и до последнего переполнения).
        uint32_t ccr = TIM2->CCR1;
        N2e = N2_correction + ccr;
        if((sr & TIM_SR_UIF) &&
                ccr > TIM2->CNT)
            N2e -= TIM2->ARR + 1;
    }

    // Значение N1 становится "предыдущим"
    // при следующем вызове обработчика.
    N1_prev = N1;

    // Если канал 1 отключён, значит, измерение уже завершено и можно
    // готовить результаты (счётчики остановлены, можно не опасаться
    // несогласованности значений).
    if(!(TIM2->CCER & TIM_CCER_CC1E))
    {
        // Количество учтённых фронтов входного сигнала.
        N1e = N1_correction + TIM1->CNT;
        if(TIM1->SR & TIM_SR_UIF)
        {
            TIM1->SR = ~TIM_SR_UIF;
            N1e += TIM1->ARR + 1;
        }

        // Устанавливаем флаг завершения измерения.
        fN2e = true;
    }
}

// Функция записывает значения в аргументы, передаваемые по ссылке:
// N1 - количество учтённых периодов входного сигнала в интервале
// измерения;
// N2 - количество импульсов опорного генератора (тактовых импульсов
// таймера TIM2) за N1 периодов входного сигнала;
// тогда частота входного сигнала f=f0*N1/N2, где f0 - частота опорного
// генератора.
// Важно!
// Если измерение после вызова measure ещё не завершено, функция ждёт
// завершения измерения (ждёт установки флага fN2e).
// Возвращает код ошибки:
// 0 - без ошибок;
// -1 - не удалось измерить частоту, так как в интервал измерения
// попало менее двух фронтов сигнала (слишком низкая частота сигнала
// для выбранного интервала измерения).
int get_result(uint32_t &N1, uint32_t &N2)
{
    N1 = 0;
    N2 = 0;

    // Если процесс измерения ещё не завершён, ждём его завершения.
    while(!fN2e) {}

    // Слишком низкая частота для выбранного интервала измерения.
    if(N1e < 2)
        return -1;

    // Количество периодов сигнала на 1 меньше количества подсчитанных
    // фронтов.
    N1 = N1e - 1;

    // Количество тактов опорного генератора за N1 период входного
    // сигнала равно разности между значениями счётчика таймера TIM2
    // в момент последней и первой фиксации (с учётом переполнений).
    N2 = N2e - (N2b_correction + N2b_naked);

    return 0;
}

// Запустить процесс измерения.
// Функция не дожидается завершения измерения; запустив измерение в
// фоновом режиме, она сразу завершает своё выполнение.
// Само измерение происходит за счёт работы таймеров и их обработчиков
// прерываний.
// Если функция вызвана до завершения предыдущего измерения, она
// возвращает false; выполнение ранее запущенного измерения продолжается.
// ticks задаёт желаемый интервал измерения в тактах сигнала на счётном
// входе таймера TIM2.
bool measure(uint32_t ticks)
{
    // Ошибка, если предыдущее измерение ещё не завершилось.
    if(!fN2e)
        return false;

    // Выполняем начальную инициализацию переменных.
    time_slot = ticks;
    N1_prev = 0;
    N1_correction = 0;
    N2_correction = 0;
    N2b_correction = 0;
    N2b_naked = 0;
    N2e = 0;
    fN2e = false;

    // На всякий случай сбрасываем флаг фиксации в канале 1 таймера TIM2.
    TIM2->SR = ~TIM_SR_CC1IF;

    // Последовательность отключение/включение генерации запроса DMA
    // сбрасывает те запросы, которые были сформированы при
    // остановленном канале DMA.
    TIM2->DIER |= TIM_DIER_CC1DE;

    // Разрешаем счёт таймеру TIM1 (импульсы на счётный вход не будут
    // поступать, пока не включен канал 1 таймера TIM2).
    TIM1->CNT = 0;
    TIM1->CR1 |= TIM_CR1_CEN;

    // Готовим канал DMA к пересылке одного значения из TIM2->CCR1 в
    // переменную N2b_naked (основная инициализация DMA выполняется
    // один раз при старте программы; при каждом измерении остаётся
    // только указать количество пересылок и включить DMA).
    DMA1_Channel5->CNDTR = 1;
    DMA1_Channel5->CCR |= DMA_CCR5_EN;

    // Запускаем с 0 счёт в таймере TIM2 (на счётный вход поступает
    // тактовый сигнал микроконтроллера).
    TIM2->CNT = 0;
    TIM2->CCR2 = (TIM2->ARR + 1) >> 1; // По умолчанию 0x8000.
    TIM2->CCR3 = ticks & 0xFFFF;
    TIM2->CR1 |= TIM_CR1_CEN;

    // Разрешаем работу каналов 1, 2 и 3 в таймере TIM2.
    // В канале 1 фиксируется значение счётчика таймера по каждому
    // фронту входного сигнала (а также при этом на 1 увеличивается
    // счётчик в TIM1);
    // канал 2 используется для генерации дополнительного прерывания
    // посередине между переполнениями;
    // канал 3 служит для своевременного завершения измерения.
    TIM2->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E;

    return true;
}

// Начальная инициализация используемых для измерения частоты
// периферийных устройств микроконтроллера.
void fm_init()
{
    // Настраиваем приоритеты прерываний и разрешаем их обработку
    // (предполагается, что прерывание от TIM2 будет более
    // высокоприоритетным).
    NVIC_SetPriority(TIM1_UP_TIM16_IRQn, 1);
    NVIC_SetPriority(TIM2_IRQn, 0);
    NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn);
    NVIC_EnableIRQ(TIM2_IRQn);

    // Включаем тактовый сигнал для используемых устройств
    // (DMA, TIM1, TIM2, GPIOA).
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    RCC->APB1ENR |=
            RCC_APB1ENR_TIM2EN;

    RCC->APB2ENR |=
            RCC_APB2ENR_TIM1EN |
            RCC_APB2ENR_IOPAEN;

    // Настраиваем вход PA1 (TIM2_CH2) (как цифровой вход с подтяжкой).
    GPIOA->CRL =
            GPIOA->CRL & ~0xF0|
                          0x80;

    // Настраиваем таймер TIM2:
    // таймер тактируется от шины (источник опорного
    // сигнала - системный тактовый сигнал);
    // по переднему фронту исследуемого сигнала производим
    // фиксацию состояния счётчика таймера;
    // сигнал захвата в канале 1 таймера используем в качестве TRGO.
    TIM2->ARR = 0xFFFF;
    TIM2->CR2 |= 0x3 << 4;  // The trigger output (TRGO) send
                            // a positive pulse when the CC1IF flag is to be set.
    TIM2->CCMR1 = 2;        // CC1 channel is configured as input, IC1 is mapped on TI2.

    TIM2->DIER |=
            TIM_DIER_CC1DE |    // DMA запрос при фиксации в канале 1.
            TIM_DIER_UIE |      // Прерывание при переполнении.
            TIM_DIER_CC2IE |    // Прерывание, если CNT==CCR2.
            TIM_DIER_CC3IE;     // Прерывание, если CNT==CCR3.

    // Базовая настройка DMA для сохранения первого результата фиксации.
    // TIM2_CH1 - DMA1 Ch5
    // Сброс значимых битов регистра CCR.
    DMA1_Channel5->CCR &= ~0x7FFF;
    // Устанавливаем нужные биты.
    // PL[1:0].MSIZE[1:0].PSIZE[1:0]=11.10.01
    // (priority level = very high;
    // MSIZE=32bits, PSIZE=16bits).
    DMA1_Channel5->CCR |= 0x39 << 8;

    DMA1_Channel5->CMAR = (uint32_t)&N2b_naked;
    DMA1_Channel5->CPAR = (uint32_t)&TIM2->CCR1;

    // Таймер TIM1 будет подсчитывать количество захватов в TIM2.
    TIM1->ARR = 0xFFFF;
    TIM1->SMCR |= (1 << 4) | 0x7;   //TS=1 (ITR1 selected - TRGO of TIM2); SMS=bx111.
    TIM1->DIER |= TIM_DIER_UIE;     // Прерывание по переполнению.
}

int main(void)
{
    // Инициализируем частотомер.
    fm_init();
    
    // Инициализируем средство отображения информации.
    display_init();

    // Готово. Можно приступать к измерениям частоты.

    while(true)
    {
        // Запуск очередного измерения, интервал измерения 0.5 с.
        measure(12000000);
        
        // Ожидание завершения измерения, считывание результата и
        // его отображение.
        uint32_t N1, N2;
        int err = get_result(N1, N2);
        display(SystemCoreClock, N1, N2, err);
    }
}

Комментарии к программе

*** Предложенная программа не претендует на идеальность, ни с точки зрения стиля, ни с точки зрения применяемых технических решений. Автор не гарантирует отсутствия ошибок и даже того, что код способен выполнять заявленные функции; снимает с себя ответственность за любые последствия от использования или неиспользования данного кода, полученную и недополученную прибыль и т.д., и т.п... ***

Всё самое важное, что касается измерения, происходит в обработчике прерывания TIM2_IRQHandler от таймера TIM2, главного таймера частотомера. Для взаимодействия основной программы и обработчика используется довольно большое количество глобальных переменных:
volatile uint32_t N1_prev;
volatile uint32_t N1_correction;
volatile uint32_t N2_correction;
volatile uint32_t N2b_correction;
volatile uint32_t N2b_naked;
volatile uint32_t N1e;
volatile uint32_t N2e;
volatile bool fN2e;
uint32_t time_slot;

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

Кратко о назначении перечисленных переменных.

volatile uint32_t N1_prev;
Вообще, переменные с именами вида N1xxx используются для сохранения результатов счёта таймера TIM1 в определённые моменты времени (соответственно, переменные N2xxx относятся к результатам счёта TIM2). В частности, N1_prev используется для хранения значения таймера TIM1, считанного при предыдущем выполнении обработчика прерывания от TIM2. Переменная служит для того, чтобы обработчик TIM2_IRQHandler мог определить, происходили ли фиксации в канале TIM2 (которые и подсчитывает TIM1) с момента предыдущего считывания значения счётчика TIM1 (во время предыдущего вызова этого обработчика). Для определения, считываем текущее значение счётчика, сравниваем с предыдущим из N1_prev. Если значения отличаются - были фиксации, которые нужно учесть в обработчике. Если не отличаются - фиксаций не было. После выполнения проверки, присваиваем переменной N1_prev уже считанное текущее значение, которое станет "предыдущим" при следующем вызове обработчика.

Естественно, при определении текущего результат счёта таймера TIM1, требуется учёт переполнений счётчика. Выполняется это просто: если текущее значение больше предыдущего, то переполнения не было и значение корректно; если текущее значение меньше - переполнение было и требуется корректировка на коэффициент пересчёта счётчика в таймере TIM1, т.е. на TIM1->ARR + 1 (по умолчанию это 0xFFFF+1). Благодаря такому алгоритму, при выполнении в обработчике фрагмента

uint32_t N1 = N1_correction + TIM1->CNT;

который считывает текущее (истинное) значение счётчика в TIM1, совершенно не важно, успел ли обработчик переполнения от TIM1 обновить переменную N1_correction или не успел - в любом случае, мы обнаружим неучтённое переполнение и при необходимости скорректируем значение.

Теперь о том, зачем такие сложности с обнаружением фиксаций, если, как известно, существует флаг, устанавливаемый аппаратно при выполнении фиксации (TIM2->SR, бит TIM_SR_CC1IF). Дело в том, что, во-первых, при выполнении первой фиксации, происходит пересылка данных средствами DMA и DMA самостоятельно сбрасывает бит. Это уже требует введения дополнительных программных проверок, кроме проверки флага. Во-вторых, между чтением флага и его сбросом (для обнаружения последующих фиксаций), может произойти ещё одна (или даже не одна) фиксация. Но мы сбросим флаг, и если после сброса не будет фиксаций, при последующем вызове обработчика, проверив флаг, ошибочно придём к выводу, что фиксаций не было. Если это происходит в конце измерения (для последней фиксации), она не будет должным образом скорректирована. Это очень серьёзная ошибка, сильно искажающая результат, но возникающая не всегда, а потому трудно выявляемая.

В предлагаемом же варианте, фактически мы каждый раз вычисляем "флаг" фиксации, поэтому пропуски невозможны: мы сначала считываем текущее количество фиксаций и запоминаем, а затем приступаем к обработке фактически последней. Возможна ситуация, когда после считывания счётчика TIM1 и обнаружения неучтённых фиксаций, происходят новые фиксации, и мы обрабатываем результат последней из них. Тем не менее, при следующем вызове обработчика TIM2_IRQHandler мы опять обнаружим наличие необработанных фиксаций, хотя фактически обработка была выполнена. Но повторная обработка не представляет никакой опасности, она не изменяет результата; важно, что нет пропусков события.

volatile uint32_t N1_correction;
Переменная содержит поправку для значения счётчика TIM1, учитывающая переполнения счётчика (т.е. при переполнении мы увеличиваем переменную сразу на коэффициент пересчёта счётчика); истинное значение количества подсчитанных импульсов к данному моменту будет равно
N1_correction + TIM1->CNT;

volatile uint32_t N2_correction;
Поправка для таймера счётчика TIM2; истинное значение количества импульсов, подсчитанных таймером, составляет
N2_correction + TIM2->CNT;

volatile uint32_t N2b_correction;
Переменная содержит корректирующее значение для результата первой фиксации (корректирующее значение для счётчика в TIM2 в тот момент, когда происходит первая фиксация). Переменная необходима, потому как при низкочастотном сигнале счётчик может успеть переполниться до того, как произойдёт первая фиксация.

volatile uint32_t N2b_naked;
В эту переменную система DMA помещает результат первой фиксации, т.е. значение счётчика таймера в момент, соответствующий первому фронту входного сигнала. Значение должно быть скорректировано с учётом переполнений счётчика. Истинное значение составит
N2b_correction + N2b_naked;

volatile uint32_t N1e;
Когда измерение завершается, обработчик прерывания TIM2_IRQHandler помещает в эту переменную подсчитанное за интервал измерения количество фронтов входного сигнала (иначе говоря, количество фиксаций от первой учтённой до последней, включая первую и последнюю). Это значение на 1 больше периодов входного сигнала, образующих истинный интервал измерения.

volatile uint32_t N2e;
Количество импульсов опорного генератора, подсчитанных таймером TIM2 на момент последней учтённой фиксации (верное значение, с учётом переполнений).

volatile bool fN2e;
Присваивая этой переменной значение true, обработчик прерывания TIM2_IRQHandler тем самым сообщает основной программе, что измерение завершено и результаты готовы для считывания.

uint32_t time_slot;
Переменная содержит заданное время измерения в тактах сигнала на счётном входе TIM2.

Далее в тексте программы следует определение функций:
extern "C" void TIM1_UP_TIM16_IRQHandler();
extern "C" void TIM2_IRQHandler();
int get_result(uint32_t &N1, uint32_t &N2);
bool measure(uint32_t ticks);
void fm_init();
int main();
int dispaly_init();
int display(uint32_t fclk, uint32_t m, uint32_t n, uint32_t err);

extern "C" void TIM1_UP_TIM16_IRQHandler();
Функция является обработчиком прерываний от таймера TIM1, она просто учитывает переполнения счётчика таймера, обновляя значение глобальной переменной N1_correction. Заметим, что переменная содержит не количество переполнений, а корректирующее значение, сложив которое с текущим значением счётчика, сразу получим истинное значение результата счёта. Так как переполняющее счётчик значение равно TIM1->ARR + 1, то при каждом переполнении для обновления переменной выполняется код
N1_correction+=TIM1->ARR + 1;

extern "C" void TIM2_IRQHandler();
Функция является обработчиком прерываний от таймера TIM2, на эту функцию возложена основная работа по измерению частоты входного сигнала; измерение идёт в фоновом режиме, поэтому одновременно с ним микроконтроллер может заниматься другими делами.

При настройке NVIC для прерываний от TIM2 задаётся более высокий приоритет, чем для прерываний от TIM1 для того, чтобы при переполнении счётчика таймера TIM1, не прерывалась работа обработчика TIM2_IRQHandler.

Обработчик TIM2_IRQHandler ведёт учёт переполнений счётчика таймера TIM2, обновляя переменную N2_correction. Но это лишь малая часть того, что делает обработчик. Самая главная его задача - отслеживать уже произошедшие в канале 1 таймера фиксации и сопоставлять соответствующие им корректирующие значения, учитывающие переполнения счётчика. Также обработчик следит за временем, прошедшим с начала измерения и останавливает измерение, когда заданное время истекает. Время может отмерять отдельный таймер, но здесь эту функцию выполняет третий канал, который работает в режиме сравнения. Если следует остановить измерение после ticks импульсов на счётном входе таймера, то с учётом переполнений, понятно, что в этот момент в счётчике будет значение ticks % 0x10000 (остаток от деления ticks на модуль пересчёта, здесь оно также оказывается равным ticks & 0xFFFF). Поэтому проверять, не истекло ли время, следует всякий раз, когда значение в счётчике становится равным ticks & 0xFFFF. Для этого в регистр сравнения канала 3 таймера, перед запуском измерения, помещаем значение ticks & 0xFFFF и, обрабатывая прерывание от канала, сравниваем прошедшее время (в тактах) с заданным.

Если время на измерение вышло, останавливаем счёт: сначала отключаем канал 1 таймера, тем самым запрещая дальнейшее выполнение фиксаций в канале по фронтам входного сигнала, после чего можем остановить счётчики таймеров (с этого момента можем быть уверены, что больше переполнения счётчиков происходить не будут), обработать результаты последней фиксации и установить флаг завершения измерения (присвоить true глобальной переменной fN2e).

Как уже отмечалось, самая важная задача обработчика - определять корректирующие значения (учитывающие переполнения) для первой и последней фиксации. Корректирующие значения определяются всякий раз, когда обнаруживается, что с момента последнего вызова обработчика изменилось количество фронтов, подсчитанных счётчиком TIM1. Смотрим, если прерывание произошло по "полупереполнению", то, несомненно, последней фиксации соответствует текущее корректирующее значение. Если обрабатывается прерывание по переполнению, уточняем, произошла ли фиксация до переполнения или уже после (по величине зафиксированного значения в сравнении с текущим значением счётчика). Если фиксация была после переполнения, за корректирующее значение для фиксации выбираем текущее корректирующее значение, иначе - предыдущее (до переполнения). Определив корректирующее значение, складываем его с результатом последней фиксации и сохраняем результат в глобальной переменной N2e. Измерение может быть остановлено в любой момент, и последняя учтённая фиксация может оказаться вообще последней на интервале измерения. Но регулярное обновление N2e гарантирует, что переменная будет содержать актуальное значение.

Корректирующее значение первой фиксации определяется аналогичным образом (поскольку первая фиксация была выполнена между предыдущим вызовом обработчика и данным). Разумеется, это делается только один раз за измерение. Обработчик обнаруживает то, что первая фиксация произошла по выполнению условия
N1_prev == 0 && N1 != 0.
Для первой фиксации корректирующее значение и нескорректированный результат фиксации сохраняются в отдельные переменные (нескорректированный результат фиксации сохраняется с помощью DMA).

int get_result(uint32_t &N1, uint32_t &N2);
Функция помещает в свои аргументы, передаваемые по ссылке:
N1 - количество периодов входного сигнала, образующих действительный интервал измерения;
N2 - количество импульсов эталонного генератора (в нашем случае - тактового сигнала микроконтроллера) за этот интервал, образованный N1 периодами входного сигнала.

Возвращаемое значение:
0 - в случае успешного завершения;
-1 - в случае ошибки, когда в заданный интервал измерения не "поместился" даже один период входного сигнала (период, ограниченный двумя последовательными фронтами); это означает, что выбранный интервал слишком мал для измерения столь низкой частоты. Для гарантии успешного измерения должно быть $$ f_{min} \gt 2 / \tau. $$

Если измерение не завершено, функция ожидает его завершения для получения результатов.

Значения N1, N2 функция определяет по результатам работы обработчика прерываний от TIM2, которые помещаются обработчиком в глобальные переменные.
N1 - это просто уменьшенный на 1 результат счёта таймера TIM1 с учётом поправки на переполнения (счётчик подсчитывает фронты сигнала; количество периодов, заключённых между первым и последним фронтом будет на единицу меньше; учёт начального значения не требуется, поскольку счётчик изначально устанавливается на 0).
N2 определяется как разность между результатами последней и первой фиксации в канале 1 таймера TIM2 (с учётом переполнений).

bool measure(uint32_t ticks);
Функция запускает новое измерение (если в данный момент не происходит ещё не завершившееся измерение) и сразу завершается, освобождая микроконтроллер для выполнения других вычислений, пока идёт измерение. Для запуска измерения, функция выполняет инициализацию глобальных переменных, используемых в процессе работы обработчиков прерываний от таймеров; выполняет те действия по инициализации оборудования, которые требуется выполнять каждый раз заново (например, каждый раз нужно задавать количество передаваемых с помощью DMA данных); разрешает счёт для таймеров; разрешает работу каналов таймера TIM2. С момента включения каналов TIM2 измерение начинается, а функция возвращает управление вызвавшей программе.

void fm_init();
Функция отвечает за начальную инициализацию периферийных устройств микроконтроллера, задействованных в измерении частоты. Вызывается однократно, до первого измерения. Например, в начале основной функции main.

int main();
Основная функция программы. Выполняет начальную инициализацию частотомера путём вызова fm_init, затем приступает к циклическому измерению частоты с выводом результатов каждого измерения на дисплей.

int dispaly_init();
int display(uint32_t fclk, uint32_t m, uint32_t n, uint32_t err);
Функции для начальной инициализации индикатора и вывода на него результатов измерений. Здесь они только объявлены, но не определены. Определяются функции в отдельном файле; их реализация зависит от используемого индикатора. Можно выводить результат на семисегментный индикатор достаточной разрядности; текстовый или графический дисплей с интерфейсом SPI или I2C; можно передавать данные на компьютер, например, через UART.

Источники информации и дополнительная литература

  1. "AN4776 Application note. General-purpose timer cookbook", STMicroelectronics; DocID028459
  2. "Frequency Measurements: How-To Guide"; NI (NATIONAL INSTRUMENTS CORP.), https://www.ni.com/tutorial/7111/en/
  3. "Автоматическая обработка сигналов частотных датчиков"; А. С. Касаткин; Энергия, 1966
  4. "Искусство схемотехники", т. 3; П. Хоровиц, У. Хилл; Мир, 1993

Смотрите далее:
Частотомер на основе микроконтроллера STM32. Конвейерный принцип измерения частоты

hamper, 2020-12-16
  Рейтинг@Mail.ru