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

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

Цифровые методы измерения частоты весьма просты в реализации и обеспечивают высокую точность. Особенно следует выделить метод обратного счёта - он не только точный (причём точность сохраняется во всём диапазоне измеряемых частот), но и способен обеспечить высокую скорость измерения. Метод заключается в следующем (рис. %img:rcp). Формируется интервал времени, состоящий из целого количества m периодов исследуемого сигнала, и подсчитывается количество импульсов n эталонного генератора с частотой F0 за этот интервал времени. Величина m выбирается таким образом, чтобы время счёта было примерно равно заданному желаемому интервалу измерения (подобрать величину можно прямо в процессе измерения). По результатам счёта вычисляем среднее значение периода сигнала (или частоты сигнала) за интервал измерения: $$ T = \frac n {m F_0}, \\ f = F_0 \frac m n. $$

Принцип метода обратного счёта для измерения частоты.
Рис. %img:rcp

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

Здесь будут рассмотрены преимущественно теоретические идеи построения частотомера и приведена упрощённая программная реализация данного метода измерения частоты. Практическое построение прибора рассматривается в статье:
Частотомер на основе микроконтроллера STM32 с конвейерным измерением частоты - 2

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

Введение

Что немаловажно, метод обратного счёта прост в реализации. Например, возможна следующая процедура измерения (рис. %img:alg). Используются два счётчика. Один счётчик подсчитывает фронты входного сигнала, но счёт происходит только в пределах интервала измерения. По первому фронту, попадающему в интервал измерения, разрешается счёт для второго счётчика, который подсчитывает импульсы от эталонного генератора. По каждому фронту входного сигнала, попадающему в интервал измерения, текущее значение из второго счётчика запоминается в специализированном регистре. Когда завершается интервал измерения, останавливаются оба счётчика и прекращается фиксация значений второго счётчика в регистре. В результате, в первом счётчике окажется значение, равное количеству фронтов сигнала, попавших в интервал измерения, т.е. m + 1 (количество фронтов на 1 больше количества периодов). В фиксирующем регистре будет значение из второго счётчика на момент последнего фронта входного сигнала (попавшего в интервал измерения). А с учётом того, что счёт был запущен по первому фронту, то значение в регистре будет равно количеству импульсов эталонного генератора за время от первого до последнего фронта входного сигнала, попадающих в интервал измерения, т.е. n.

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

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

При реализации возможны различные вариации описанного алгоритма. Например, оба счётчика могут запускаться одновременно в начале заданного интервала измерения. Для фиксации значений счётчика импульсов эталонного генератора тогда следует использовать два регистра: один фиксирует значение счётчика n1 только по первому фронту входного сигнала; второй - по всем фронтам, а значит, после завершения заданного интервала измерения, в нём останется значение счётчика n2, зафиксированного по последнему фронту, попавшему в интервал. Количество импульсов опорного генератора, между первым и последним фронтом в интервале измерения составит n = n2 - n1. Впрочем, это несущественные детали, не затрагивающие основную идею метода.

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

В то же время нельзя не заметить очевидный изъян подобного подхода к измерениям: имеются значительные непроизводительные расходы времени на запуск, остановку, повторную инициализацию оборудования, когда собственно измерения частоты не происходит. Схематически это изображено на рис. %img:gap, где затрачиваемое на измерение время обозначено \( \tau \), при этом промежуток времени между измерениями составляет \( \Delta \tau \).

Непроизводительная трата времени на остановку/запуск измерения частоты.
Рис. %img:gap

Если измеряемая частота неизменна, или при визуальном считывании результатов, когда скорость восприятия информации сильно ограничена возможностями пользователя прибора, это не столь существенно. Но при автоматизированном контроле сигналов оказывается, что мы теряем очень большое количество информации об изменениях частоты контролируемого сигнала. Исправить ситуацию можно, добиваясь минимизации интервала между измерениями, в идеале этот интервал должен быть нулевым (рис. %img:gap0).

Снижение непроизводительных затрат времени при измерении частоты.
Рис. %img:gap0

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

Конвейерное измерение частоты

Не будем останавливаться на достигнутом. Ведь даже при нулевом "зазоре" между измерениями, мы всё равно теряем много информации о сигнале. Получая только усреднённое значение частоты для интервала измерения, мы остаёмся в полном неведении о том, что происходило в пределах интервала.

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

Единственное, что остаётся - начинать новое измерение, не дожидаясь завершения предыдущего (рис. %img:pip). Тогда за время, отводимое на одно измерение, мы можем запустить несколько новых измерений. Благодаря этому, при том же самом времени на одно измерение, результаты будем получать в несколько раз чаще и получим определённые сведения о поведении сигнала в пределах интервала измерения.

Конвейерное измерение частоты.
Рис. %img:pip

На рисунке показано, как используя три синхронизированных между собой измерителя частоты, втрое увеличить частоту получения результатов, не изменяя длительности интервала измерения. Рассмотрим работу системы, начиная с момента запуска (вертикальная линия слева на рисунке). В этот момент начинает своё измерение прибор P1 (его первое измерение помечено индексом 1). Интервал измерения составляет \( \tau \). Но мы не будем дожидаться завершения интервала и через время \( \delta = \tau / 3 \) от начала отсчёта, запустим измерение на устройстве P2 (измерение 2 на рисунке). А ещё через время \( \delta \) начинает измерение P3 (соответственно, измерение 3). Пока что мы не получали результатов - требуется некоторое время на начальную "загрузку" нашего конвейера из частотомеров. Но ещё через время \( \delta \) уже завершится интервал измерения у P1, и от него получим первый результат (после чего P1 немедленно приступает к следующему измерению, которое обозначено индексом 4 на рисунке). Теперь будем постоянно получать новые результаты с интервалом \( \delta \): следующим будет результат от P2, у которого завершится интервал измерения 2 (и сразу же начнётся интервал 5); затем поступят данные от P3 (интервал 3); после чего снова завершит измерение P1 (завершается интервал 4, начинается интервал 7); потом опять получаем результат от P2 (за интервал 5); затем от P3 (за интервал 6) и т.д. Результаты измерения частоты будут следовать непрерывным потоком с интервалом \( \delta = \tau / 3 \).

Подобная схема измерений может быть обобщена на случай произвольного количества k измерителей, которые при интервале измерения \( \tau \), позволяют получать результаты измерений с интервалом в k раз меньше, т.е. \( \delta = \tau / k \). Частота, с которой будут поступать результаты, соответственно, будет в k раз больше, чем при использовании одиночного измерителя с таким же интервалом измерения \( \tau \).

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

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

Реализация частотомера с непрерывным измерением

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

Рассмотрим, как реализуется измерение без остановки счёта. Начнём со случая однократных измерений. Если используется микроконтроллер, в качестве счётчиков используем таймеры. Один таймер, обозначим его TIMm, будет подчитывать фронты входного сигнала; другой таймер, TIMn, будет считать импульсы эталонного генератора (рис. %img:bd). Для определения частоты, нам потребуется одновременно считывать значения счётчиков обоих таймеров по фронту входного сигнала. Так как останавливать счёт мы не можем, будем использовать фиксацию счётчиков по внешнему сигналу. Обычно таймеры в микроконтроллерах предоставляют такую возможность. В частности, в таймерах микроконтроллеров STM для этого используются так называемые каналы, которые выполняют функцию фиксации при работе в режиме входа.

Блок-схема частотомера с непрерывным или конвейерным измерением.
Рис. %img:bd

Итак, по каждому фронту входного сигнала значения счётчиков в таймерах TIMm и TIMn записываются в их регистры фиксации. Для однократного измерения частоты, нам потребуется дважды считывать эти регистры. Один раз это делаем вначале интервала измерения и при этом получаем некоторые начальные значения m1, n1. Второй раз читаем регистры через промежуток времени, равный интервалу измерения, получаем значения m2, n2. Тогда количество периодов входного сигнала между первой и последней фиксациями, для которых мы считывали результаты, составит m = m2 - m1 (точно); количество периодов эталонного генератора между этими фиксациями, соответственно, n = n2 - n1 (с точностью до 1). По известным величинам m, n, частота сигнала вычисляется элементарно.

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

Причём отключение каналов должно быть выполнено строго одновременно для обоих таймеров. В случае микроконтроллеров STM, одновременности отключения каналов очень просто достичь, используя возможность объединения таймеров для совместной работы за счёт наличия внутренних связей.

Таймер TIMm для этого настраивается ведущим, он будет управлять таймером TIMn, который настраивается как ведомый. Для TIMm в качестве внутреннего выходного сигнала TRGO выбираем сигнал фиксации в канале 1 этого таймера; тогда в ответ на каждую фиксацию в канале, формируется выходной импульс. Для таймера TIMn в качестве входного сигнала, управляющего фиксацией в его канале, выбираем внутренний вход ITRx, связанный с выходом TRGO таймера TIMm. Тогда для одновременной остановки фиксации в каналах обоих таймеров, оказывается, достаточно отключить только канал ведущего таймера TIMm. При этом фиксации в нём прекращаются, а значит, не будут формироваться импульсы на TRGO таймера TIMm. То есть, фиксации во втором таймере TIMn также не будут происходить (хотя канал остаётся включённым), так теперь на него не поступает управляющий сигнал. И хотя счёт в таймерах продолжается, в регистрах обоих каналов остаются последние зафиксированные значения, считывать которые нам ничего не будет мешать.

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

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

Реальный и заданный интервал измерения при использовании непрерывного метода обратного счёта.
Рис. %img:int

Измерение предполагает двукратное считывание результатов фиксации в таймерах: в начале заданного интервала измерения (точка A на рисунке) и в конце интервала (точка B). Реальные моменты фиксации предшествуют моментам считывания и отстоят от них на некоторые промежутки времени, обозначенные на рисунке как \( \Delta {\tau}_1, \, \Delta {\tau}_2 \), которые могут изменяться в пределах 0..T, где T - период входного сигнала. Эти промежутки зависят от частоты сигнала, начальной фазы, длительности интервала и, если специально не синхронизировать заданный интервал измерения с входным сигналом, изменяются от измерения к измерению, принимая различные значения из указанного диапазона. Первый промежуток удлиняет реальный интервал измерения, второй - укорачивает. В итоге, реальный интервал измерения может отличаться от заданного на \( \pm T \) и его начало сдвинуто влево от заданного интервала на величину \( 0 \ldots T \). Обычно период сигнала мал по сравнению с интервалом измерения (когда входной сигнал не слишком низкочастотный) и отклонения реального интервала от заданного невелики. Но при низкочастотных входных сигналах (измерения вблизи нижнего предела), период сигнала может быть сопоставим с интервалом измерения, и отклонения становятся заметными. Следует иметь это в виду. Но опять же, это не проблема конкретной реализации, это особенность метода в целом.

Итак, рассмотрели, как выполняется измерение по запросу, но без остановки счёта. Что касается непрерывного процесса измерения, то он реализуется элементарно при такой же схеме построения частотомера. С заданным интервалом \( \tau \) считываем зафиксированные в таймерах значения и по считанным значениям и значениям, считанным в предыдущий раз, определяем величины m, n, по которым вычисляем частоту входного сигнала. Затем выжидаем ещё один интервал и повторяем действия; очевидно, считанные в этот раз значения для нового интервала становятся "предыдущими".

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

Реализация конвейерного частотомера

Предположим, задан интервал измерения, равный \( \tau \). При этом мы хотим получать k результатов измерения частоты за один интервал, т.е. результаты должны поступать с интервалом опроса \( \delta = \tau / k \).

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

Единственное, чем будет отличаться процесс измерений - опросы будем выполнять чаще, с интервалом \( \delta \). Результаты опросов, т.е. пары значений {mj, nj}, считанных из каналов таймеров, будем помещать в массив размером k + 1 элементов (пар значений).

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

Нетрудно заметить, что моменты, когда считывались пары значений, которые в данный момент находятся в начале и в конце заполненного массива, отстоят друг от друга как раз на время, равное интервалу измерения: \( k \delta = k \tau / k = \tau \). Значит, записав в конец массива очередной элемент по результатам последнего выполненного опроса, мы получаем возможность найти по первому и последнему элементам массива величины, по которым можем рассчитать частоту сигнала (разумеется, если массив уже полон после последней записи в него): $$ m = m_k - m_0, \\ n = n_k - n_0, \\ f = F_0 m / n. $$ Частота будет рассчитана как усреднённая величина для заданного интервала \( \tau \), завершившегося в момент выполнения последнего опроса.

Через время, равное интервалу опроса, мы поместим в конец массива очередной элемент. И снова, первый и последний элементы массива будут соответствовать началу и концу только что завершившегося интервала измерения, который оказывается сдвинут на интервал опроса относительно предыдущего интервала измерения для предыдущего значения частоты. А так как опросы выполняем с интервалом \( \delta \), с таким же интервалом будем получать новые измеренные значения частоты.

Организация конвейерного измерения частоты.
Рис. %img:ppr

На (рис. %img:ppr) схематически изображён процесс измерений для случая, когда выполняется по 3 опроса на интервал, т.е. интервал между опросами в 3 раза меньше заданного интервала измерения.

Массив в данном случае будет содержать k + 1 = 4 элемента. Например, после выполнения опроса D, первый элемент массива будет содержать результаты опроса A, последний - результаты опроса D, т.е. опросы для начала и конца интервала, обозначенного на рисунке индексом 1. По этим опросам не составляет труда вычислить частоту для данного интервала измерения. Через один интервал опроса, крайними элементами массива станут элементы, полученные по опросам B и E, которые соответствуют началу и концу интервала с индексом 2 и по которым можно вычислить усреднённую частоту для этого интервала. И так далее.

Таким образом, одно простое устройство способно выполнить то же самое, что и целый конвейер измерителей, рассмотренный ранее (рис. %img:pip).

Пример конвейерного измерения частоты с использованием микроконтроллера STM32

Построим частотомер с непрерывным конвейерным измерением частоты на основе микроконтроллера STM32F100RB (тактовая частота микроконтроллера 24 МГц). Вход - цифровой, TTL /CMOS 3.3V совместимый; в качестве входа используем вывод PA8 микроконтроллера (к которому привязан вход TI1 таймера TIM1 и который может управлять каналом 1 таймера). В качестве опорного сигнала будем использовать системный тактовый сигнал микроконтроллера (так как используется стабилизация частоты с помощью кварцевого резонатора, то частота тактового сигнала достаточно стабильна и достаточно точно установлена).

Частота входного сигнала не должна превышать 10 МГц, что обусловлено наличием схемы ресинхронизации на входе таймера, которая преобразует входной сигнал, синхронизируя моменты переключения с тактовым сигналом. Принцип действия схемы ограничивает максимальную частоту входного сигнала, она должна быть менее половины тактовой частоты. Если быть более точным, требуется, чтобы и длительность импульса, и длительность паузы между импульсами входного сигнала превышали период тактового сигнала. В противном случае возможны пропуски импульсов входного сигнала в процессе преобразования, что приведёт к получению неверного результата для частоты.

Минимальная измеряемая частота (нижний предел измерения) зависит от выбранного интервала измерения. Указать точное соотношение между ними не получиться из-за используемого метода опроса с остановкой фиксации в таймерах частотомера. Временное отключение фиксации приводит к пропуску некоторых фронтов. Это не влияет на точность, но приводит к тому, что не все измерения оказываются успешными. При измерениях вблизи нижнего предела, иногда будут возвращаться в качестве результатов для m, n нулевые значения, которые следует отбросить; последующие ненулевые значения будут корректными. Условно за нижний предел примем $$ f_{min} = 1 / \tau, $$ когда значительная часть измерений будет заканчиваться успешно (смотрите также "Недостатки предлагаемой реализации. Пути улучшения"). Если бы не приходилось отключать фиксацию в каналах, при выполнении условия все измерения были бы успешными. С отключениями, все измерения будут успешными при выполнении условия $$ f \gt 1 / \delta, $$ но оно несколько избыточно, всё же лучше допускать более низкие частоты с контролем корректности результата.

Максимально достижимый интервал измерения в данной реализации будет составлять около 170 с (но так как действительный интервал может отклоняться от заданного как в меньшую, так и в большую сторону на величину, достигающую периода измеряемого сигнала, а максимальный период может достигать интервала измерения, то не следует задавать интервал измерения более 85 с).

Минимальный интервал измерения и минимальный интервал опроса составляют порядка нескольких десятков микросекунд.

Частотомер построим на таймерах TIM1 и TIM2 (можно использовать любые другие таймеры, имеющие хотя бы 1 канал). Таймер TIM1 будет считать фронты входного сигнала; таймер TIM2 - тактовые импульсы (рис. %img:pipfm32).

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

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

Таймер TIM1 сконфигурируем таким образом, что его входной сигнал с входа TI1 одновременно поступал на счётный вход и управлял фиксацией в канале 1 таймера. Кроме того, таймер настроим как ведущий, чтобы он формировал выходной сигнал на внутреннем выходе TRGO: при каждой фиксации в канале таймера на выходе TRGO будет появляться импульс. Этот сигнал будет управлять таймером TIM2 (внутренний выход TIM1_TRGO подключён к внутреннему входу TIM2_ITR0), который в нашем случае настроим как подчинённый. По сигналу от таймера TIM1, таймер TIM2 выполняет фиксацию в своём канале 1. За счёт этого по каждому фронту входного сигнала выполняется одновременная фиксация значений счётчиков в обоих таймерах. А, кроме того, мы получаем возможность одновременно разрешать и запрещать фиксацию в обоих таймерах. Отключив канал 1 в TIM1, мы не только останавливаем фиксации в таймере TIM1, но и в таймере TIM2, так как если не происходят фиксации в первом таймере, то и не формируются импульсы на внутреннем выходе TRGO этого таймера. Аналогично, включив канал 1 в TIM1, мы одновременно с этим разрешим фиксации в таймере TIM2.

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

Кроме основных таймеров частотомера, нам понадобятся вспомогательные, в качестве которых будем использовать таймеры TIM6 и TIM7. Они являются базовыми таймерами, т.е. таймерами с минимальной функциональностью, которой, тем не менее, вполне достаточно для наших целей. С помощью дополнительного таймера TIM6, будем следить за переполнениями основных таймеров частотомера (TIM1, TIM2), учитывать произошедшие переполнения и, таким образом, программно расширим разрядность счётчиков в таймерах до 32 бит. Кроме того, по этому таймеру будем следить за фиксациями в таймерах, сопоставляя последним зафиксированным значениям соответствующие им верные корректирующие значения, учитывающие переполнения счётчиков на момент фиксации. Здесь мы отказываемся от учёта переполнений путём обработки прерываний по переполнению от самих счётчиков, чтобы минимизировать количество асинхронно возникающих событий, а значит, упростить программу. Для того чтобы не происходило пропусков переполнений в отслеживаемых таймерах, таймер TIM6 должен срабатывать (иначе говоря, его обработчик должен вызываться) чаще, чем происходят переполнения, за которыми ведётся контроль. Чаще всего будет переполняться TIM2, считающий импульсы тактового сигнала (TIM1 считает импульсы входного сигнала, частота которых из-за наличия схемы ресинхронизации для внешних сигналов на входе таймера не может превышать половины тактовой частоты), поэтому будем ориентироваться на TIM2. Если TIM6 будет срабатывать (переполняться) вдвое чаще, чем будет переполняться TIM2, то будет обеспечена высокая надёжность работы системы и значительная устойчивость частотомера к вычислительным нагрузкам на микроконтроллер со стороны кода пользователя. Для выполнения условия нужно просто поместить в регистр ARR таймера TIM6 значение (TIM2->ARR + 1) / 2 - 1, т.е. 0x7FFF.

Ещё один таймер, TIM7, выделен, чтобы отмерять интервалы опроса. Конечно, возможны различные варианты, например, вместо данного таймера (а также вместо TIM6) может использоваться системный таймер. Или можно выполнять отсчёты по внешнему сигналу.

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

Прежде всего, необходимо инициализировать все средства, используемые для измерения частоты с помощью вызова функции
void fm_init();

После этого запускаем процесс измерения с помощью функции
bool fm_start(uint32_t timer_div1, uint32_t timer_div2, uint32_t k);

Перед вызовом функции следует определиться с интервалом измерения и количеством отсчётов на интервал измерения. От этого зависят значения, передаваемые функции в качестве аргументов. Рассмотрим подробнее вопрос правильного выбора значений для аргументов функции.

Опрос таймеров TIM1, TIM2 для получения результатов последней фиксации происходит по таймеру TIM7 (когда таймер переполняется, вызывается обработчик прерывания, который выполняет все необходимые действия). Соответственно, интервал опроса определяется настройками таймера TIM7: установленным коэффициентом предделителя (настраивается с помощью регистра PSC) и коэффициентом пересчёта счётчика (настраивается регистром ARR). А настраивается таймер TIM7 в соответствии с переданными функции fm_start аргументами timer_div1 и timer_div2. Интервал опроса будет равен
δ = timer_div1 * timer_div2 / SystemCoreClock;
где SystemCoreClock - тактовая частота микроконтроллера, в данном случае 24 МГц.

Исходя из аппаратных ограничений таймера, требования к значениям аргументов следующие:
timer_div1 - значение должно находиться в пределах 1..65536;
timer_div2 - значение должно находиться в пределах 2..65536.
Один и тот же интервал опроса может быть получен путём комбинирования разных значений делителей. Вероятно, такой способ указания требуемого интервала в виде делителя, который задаётся как произведение двух множителей, не слишком удобен. Но это обусловлено особенностями используемых аппаратных средств.

Аргумент k определяет количество отсчётов на интервал измерения, от этой величины зависит размер буфера, который составляет k + 1 элементов. Каждый элемент - это 8 байт данных (два слова по 4 байта). Если задать слишком большое значение k, может не хватить памяти.

Пример: тактовая частота 24 МГц; требуемый интервал измерения 0.2 с; количество опросов на интервал измерения 10. Следовательно, интервал опроса составляет 0.02 с, а значит, произведение timer_div1 * timer_div2 должно быть равно 480000. С учётом того, что каждый из множителей не должен превышать максимального значения 65536, для запуска измерений может быть использован, например, следующий вызов функции:
fm_start(480, 1000, 10);
или
fm_start(8, 60000, 10);
или любой другой из множества возможных вариантов.

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

Также следует учитывать, что интервал измерения (интервал измерения в k раз больше интервала опроса) не может быть слишком большим. Поскольку количество импульсов эталонного генератора выражается в данной реализации частотомера 32-битным числом, то это количество за интервал измерения должно быть менее 232. В нашем случае эталонным является тактовый сигнал, поэтому должно выполняться условие
timer_div1 * timer_div2 * k < 232,
желательно с запасом, учитывая, что действительный интервал измерения может отклоняться от заданного. Будем ориентироваться на требование
timer_div1 * timer_div2 * k < 231

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

Как только процесс измерений запущен, измерения далее будут происходить непрерывно, в фоновом режиме; с интервалом опроса будут приходить новые данные, на основе которых можем вычислять новое значение частоты. Но прежде, чем вычислить первое значение частоты, следует дождаться готовности буфера (установки в true флага dbuf.is_ready). От запуска до готовности проходит некоторое время, от одного до полутора интервалов измерения (полтора интервала - в худшем случае вблизи нижнего предела измерения). В последующем, чтобы узнать, что в буфере появились новые данные для вычисления результата очередного измерения, можно следить за счётчиком записей в буфер, вызывая dbuf.get_cnt().

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

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

// Инициализация частотомера.
fm_init();

// Запуск измерений
// (интервал опроса 0.02с; интервал измерения 0.2с; 10 измерений на 
// интервал измерения).
fm_start(480, 1000, 10);

// Предыдущее значение счётчика записей в буфер.
uint32_t cnt0 = 0;

uint32_t m, n, cnt, c0;

// Начинаем бесконечный цикл опроса буфера.
while(true)
{
    if(get_result(m, n, cnt, c0) && cnt0 != cnt && n != 0)
    {
        cnt0=cnt;
        
        // Далее можем рассчитать среднее значение частоты сигнала
        // за последний только что завершившийся интервал измерения.
        // float f=(float)SystemCoreClock*m/n;
        
        // Выполняем требуемые действия с полученным результатом...
    }
}        

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

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

/*
***********************************************************************
Частотомер с пределами измерения 0.02Гц..10МГц.
Конвейерное измерение частоты (за 1 интервал измерения можно получить
несколько результатов измерения частоты).
Отлажено на MCU: STM32F100RB; кварц 8МГц; тактовая частота 24МГц.

Вход: PA8 (TIM1_CH1)

***********************************************************************
*/

#include "stm32f10x.h"

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

// Переполняющее значение по умолчанию для 16-разрядного счётчика
// таймера (ARR+1, по умолчанию ARR=0xFFFF).
#define CNT_OVR 0x10000

// Тип для результата опроса таймеров частотомера.
class Data
{
public:
    // Результат фиксации счётчика фронтов входного сигнала.
    uint32_t m;
    // Результат фиксации счётчика импульсов опорного генератора.
    uint32_t n;
};

// Буфер для хранения заданного количества результатов опроса;
// новые значения помещаются в конец буфера; когда буфер полон,
// перед записью нового значения все элементы сдвигаются на 1 позицию
// к началу (первый элемент при этом теряется).
// Имеется возможность доступа к первому и последнему элементам.
class DBuf
{
private:
    // Указатель на начало внутреннего буфера.
    Data *p0;
    // Указатель на текущую позицию для записи.
    Data *p;
    // Максимальное количество элементов, помещающихся в буфер.
    uint32_t m_size;
    // Счётчик количества записей в буфер.
    uint32_t cnt;
    // Флаг, устанавливаемый, когда количество элементов в буфере
    // достигло максимального.
    bool full;
public:
    // Флаг, устанавливаемый частотомером, когда данных в буфере
    // накоплено достаточно для вычисления первого значения частоты.
    bool is_ready;
    // Поле для хранения количества переполнений 32-битного целого для
    // подсчёта импульсов опорного генератора к моменту первой в буфере
    // фиксации (от момента запуска частотомера).
    uint32_t c0;

    DBuf():p0(0), p(0), m_size(0), cnt(0), full(false),
            is_ready(false), c0(0) {}

    DBuf(uint32_t _size):p0(0), p(0), m_size(0), cnt(0),
            full(false), is_ready(false), c0(0)
    {
        if(_size != 0)
        {
            p = p0 = new Data[_size];
            m_size = _size;
        }
    }

    // Очистить буфер и установить нулевую максимальную длину.
    void clear() volatile
    {
        delete[] p0;
        p0 = 0;
        p = 0;
        m_size = 0;
        cnt = 0;
        full = false;
        is_ready = false;
        c0 = 0;
    }

    // Удалить все данные из буфера и задать новую максимальную длину.
    void reset(uint32_t _size) volatile
    {
        clear();
        if(_size != 0)
        {
            p = p0 = new Data[_size];
            m_size = _size;
        }
    }

    // Возвращает true, если количество элементов в буфере
    // равно максимальному.
    inline bool is_full() const volatile {return full;}

    // Прочитать счётчик количества записей в буфер.
    inline uint32_t get_cnt() const volatile {return cnt;}

    // Возвращает текущее количество элементов в буфере.
    uint32_t size() const volatile
    {
        return (full) ? m_size : p-p0;
    }

    // Возвращает максимальное количество элементов в буфере.
    inline uint32_t max_size() const volatile {return m_size;}

    // Поместить элемент в конец буфера; если буфер полон - сместить
    // все элементы по направлению к началу на 1 позицию (первый
    // элемент будет потерян) и записать данный элемент в конец буфера.
    void push(const Data &d) volatile
    {
        if(m_size != 0)
        {
            *p = d;
            ++p;
            if(p >= p0 + m_size)
            {
                full = true;
                p = p0;
            }
            cnt++;
        }
    }

    // Получить указатель на первый элемент буфера (указатель
    // действителен до следующей записи в буфер, сброса или
    // изменения размера).
    Data *first() volatile
    {
        if(!full)
            return (p == p0) ? 0 : p0;

        return p;
    }

    // Получить указатель на последний элемент в буфере (указатель
    // действителен до следующей записи в буфер, сброса или
    // изменения размера).
    Data *last() volatile
    {
        if(!full)
            return (p == p0) ? 0 : p-1;
        Data *px = p - 1;
        if(px < p0)
            px += m_size;
        return px;
    }
};

// *************************************************************
// Глобальные переменные, используемые обработчиками прерываний.
// *************************************************************

// Предыдущее значение счётчика фронтов входного сигнала.
volatile uint16_t m_cnt_prev = 0;

// Последнее зафиксированное значение счётчика фронтов.
volatile uint16_t m_capt = 0;

// Предыдущее значение счётчика импульсов опорного генератора.
volatile uint16_t n_cnt_prev = 0;

// Последнее зафиксированное значение счётчика импульсов опорного
// генератора.
volatile uint16_t n_capt = 0;

// Корректирующее значение для текущего значения счётчика фронтов,
// учитывающее переполнения счётчика.
volatile uint32_t m_corr = 0;

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

// Корректирующее значение для текущего значения счётчика импульсов
// опорного генератора.
volatile uint32_t n_corr = 0;

// Корректирующее значение для последнего захваченного значения
// счётчика импульсов опорного генератора.
volatile uint32_t n_capt_corr = 0;

// Буфер для хранения результатов опроса.
volatile DBuf dbuf;

// Функция обновляет глобальные переменные в соответствии с текущим
// состоянием таймеров частотомера.
// Только для внутреннего использования (вызывается из обработчиков
// прерываний от TIM6 и TIM7).
void _update_fm_vars()
{
    // Читаем значения из каналов и текущие значения счётчиков
    // в таймерах (порядок важен: сначала результаты фиксации, потом
    // значения счётчиков).

    // Читаем значения, зафиксированные в каналах таймера.
    // Важно! Маловероятно, но возможно, что фиксация произойдёт
    // после чтения регистра канала в TIM1, но перед чтением регистра
    // канала в TIM2, в результате чего m_ccr1 и n_ccr1 станут
    // несогласованными (будут принадлежать к разным фиксациям).
    // Это не приведёт к ошибке, так окончательный результат будет
    // получен после остановки каналов, что гарантирует синхронизацию
    // считанных значений.
    uint32_t m_ccr1 = TIM1->CCR1;
    uint32_t n_ccr1 = TIM2->CCR1;

    // Читаем текущие значения счётчиков для выявления переполнения
    // путём сравнения со значениями, считанными в прошлый раз.
    // Значение из TIM1 уменьшаем на 1, так как фиксируемое значение
    // на 1 меньше текущего значения счётчика (по фронту входного
    // сигнала сначала происходит фиксация значения, затем счётчик
    // инкрементируется). А смещённое на 1 значение счётчика будет
    // совпадать с фиксируемым и переполняться они будут одновременно -
    // это удобно.
    uint32_t m_cnt = (uint16_t)(TIM1->CNT - 1);
    uint32_t n_cnt = TIM2->CNT;

    // Проверяем счётчики на переполнение и обновляем переменные для
    // хранения предыдущего значения (если текущее значение меньше
    // предыдущего, значит, произошло переполнение; более одного
    // переполнения не могло произойти, так как функция вызывается
    // достаточно часто).
    bool m_ovf = m_cnt < m_cnt_prev;
    bool n_ovf = n_cnt < n_cnt_prev;

    m_cnt_prev = m_cnt;
    n_cnt_prev = n_cnt;

    // Проверяем каналы на наличие фиксаций с момента предыдущей
    // проверки (фиксация была, если изменилось захваченное значение
    // в TIM1, считающем фронты входного сигнала).
    bool captf = (m_ccr1 != m_capt);

    // Если была фиксация, сохраняем захваченные значения и
    // соответствующие им корректирующие значения в глобальных
    // переменных.
    if(captf)
    {
        m_capt = m_ccr1;
        n_capt = n_ccr1;
        // Если переполнения не было - текущее корректирующее значение
        // является актуальным для фиксации; иначе - следует уточнить,
        // когда была фиксация (до переполнения или после).
        m_capt_corr = m_corr;
        n_capt_corr = n_corr;
        if(m_ovf && m_ccr1 <= m_cnt)
            m_capt_corr += CNT_OVR;
        if(n_ovf && n_ccr1 <= n_cnt)
            n_capt_corr += CNT_OVR;
    }

    // Обновляем текущие корректирующие значения в случае, если было
    // обнаружено переполнение.
    if(m_ovf)
        m_corr += CNT_OVR;
    if(n_ovf)
        n_corr += CNT_OVR;
}

// Обработчик прерываний от таймера TIM6;
// по таймеру TIM6 происходит вызов функции _update_fm_vars;
// таймер срабатывает не реже, чем два раза за наименьший интервал
// между последовательными переполнениями счётчика в таймере TIM2 или
// TIM1, благодаря чему без пропусков производится учёт переполнений
// и своевременно выявляются и учитываются фиксации в каналах таймеров.
// *** Не используются обработчики прерываний от таймеров TIM1, TIM2
// для обработки переполнений их счётчиков для минимизации количества
// асинхронно возникающих событий. Код становится более простым и
// понятным.
extern "C" void TIM6_DAC_IRQHandler()
{
    if(TIM6->SR & TIM_SR_UIF)
    {
        TIM6->SR = ~TIM_SR_UIF;

        _update_fm_vars();
    }
}

// Обработчик прерываний от таймера TIM7.
// По таймеру TIM7 выполняются опросы - чтение последних результатов
// фиксации (для этого временно останавливается фиксация в каналах).
// Каждый опрос эквивалентен запуску независимого измерителя частоты.
// По каждому опросу становится доступным результат измерения, с
// момента запуска которого прошло время, равное интервалу измерения.
extern "C" void TIM7_IRQHandler()
{
    TIM7->SR = ~TIM_SR_UIF;

    // Временно останавливаем фиксации в таймерах частотомера, отключив
    // канал 1 в TIM1.
    TIM1->CCER &= ~TIM_CCER_CC1E;

    // Обрабатываем результат последней фиксации.
    _update_fm_vars();

    // Разрешаем фиксации в таймерах (глобальные переменные в
    // безопасности - пока выполняется этот обработчик, не может быть
    // вызван обработчик от таймера TIM6, по которому обновляются
    // переменные).
    TIM1->CCER |= TIM_CCER_CC1E;

    // Читаем и сохраняем в буфер результаты опроса.
    uint32_t m = m_capt + m_capt_corr;
    uint32_t n = n_capt + n_capt_corr;

    uint32_t n0 = 0;
    if(dbuf.size() > 0)
        n0 = dbuf.first()->n;

    dbuf.push({m, n});

    // Проверяем готовность буфера к вычислению первого значения
    // частоты (буфер должен быть полон и первый элемент
    // соответствовать моменту после первой обработанной фиксации).
    const Data *p1 = dbuf.first();
    if(p1->m != 0 && dbuf.is_full())
        dbuf.is_ready=true;

    // Учёт количества переполнений для dbuf.first()->n
    if(n0 > p1->n)
        dbuf.c0++;
}

// Инициализация используемых для измерения частоты периферийных
// устройств в MCU; вызывается однократно перед запуском измерений;
// предполагается, что перед вызовом все используемые устройства
// находятся в начальном состоянии (как после сброса).
void fm_init()
{
    // Настраиваем NVIC: задаём приоритеты прерываний и
    // разрешаем обработку нужных прерываний.

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

    const uint32_t level=(1 << __NVIC_PRIO_BITS) - 1;

    NVIC_SetPriority(TIM6_DAC_IRQn, level);
    NVIC_SetPriority(TIM7_IRQn, level);

    NVIC_EnableIRQ(TIM6_DAC_IRQn);
    NVIC_EnableIRQ(TIM7_IRQn);

    // Включаем тактовый сигнал для используемой периферии.
    RCC->APB1ENR |=
            RCC_APB1ENR_TIM2EN |
            RCC_APB1ENR_TIM6EN |
            RCC_APB1ENR_TIM7EN;
    RCC->APB2ENR |=
            RCC_APB2ENR_IOPAEN |
            RCC_APB2ENR_TIM1EN;


    // Настраиваем вход PA8 (TIM1_CH1)
    // (как цифровой вход с подтяжкой).
    GPIOA->CRH =
            GPIOD->CRH & ~0xF | 0x8;

    // Настраиваем таймеры.

    // Таймер TIM1 считает фронты входного сигнала и одновременно
    // выполняет фиксацию своего счётчика по каждому фронту.
    // На внутреннем выходе TRGO формируется импульс при каждой
    // фиксации в канале (т.е. импульс на каждый фронт входного
    // сигнала, но только при включённом канале).
    TIM1->CR2 |= 3 << 4;    // MMS=011 ==> TRGO = Compare pulse
    TIM1->SMCR |=
            (5 << 4) |      // TS=101 (TI1FP1)
            7;              // SMS=111 (External mode clock 1)
    TIM1->CCMR1 |= 1;       // CC1S=01, IC1=TI1

    // Таймер TIM2 считает импульсы системного тактового сигнала.
    // Фиксация в канале таймера производится по импульсу на внутреннем
    // входе TRGIO, который подключён к выходу TRGO таймера TIM1
    // (т.е. при фиксации в канале таймера TIM1, одновременно
    // происходит фиксация в канале TIM2; при отключении канала в TIM1,
    // одновременно останавливается и фиксация в канале таймера TIM2).
    // CC1S=11: IC1=TRC=TRGI0 (SMCR_TS=00 ==> TRC=TRGIO)
    TIM2->CCMR1 |= 3;

    // Переполнение таймера TIM6 и генерация прерывания будет
    // происходить вдвое чаще, чем переполнение счётчика в TIM2
    // (и более чем вдове чаще, чем переполнение в TIM1).
    // Этого достаточно для правильного учёта переполнений счётчиков
    // в TIM1 и TIM2 и для своевременного учёта результатов фиксации
    // в таймерах.
    TIM6->ARR = 0x7FFF;

}

// Остановить измерения.
void fm_stop()
{
    TIM7->CR1 &= ~TIM_CR1_CEN;
    TIM6->CR1 &= ~TIM_CR1_CEN;
    TIM1->CCER &= ~TIM_CCER_CC1E;
    TIM1->CR1 &= ~TIM_CR1_CEN;
    TIM2->CCER &= ~TIM_CCER_CC1E;
    TIM2->CR1 &= ~TIM_CR1_CEN;

    TIM6->DIER &= ~TIM_DIER_UIE;
    TIM7->DIER &= ~TIM_DIER_UIE;
}

// Запуск измерений (или перезапуск с новыми параметрами).
// timer_div1, timer_div2 определяют интервал опроса,
// который равен timer_div1*timer_div2/SystemCoreClock, где
// timer_div1=1..65536; timer_div2=2..65536.
// При выходе аргументов за допустимые пределы, функция завершается,
// возвращая false.
// k - количество опросов на один интервал измерения, следовательно,
// длительность интервала измерения составляет
// k*timer_div1*timer_div2/SystemCoreClock
bool fm_start(uint32_t timer_div1, uint32_t timer_div2, uint32_t k)
{
    // Проверить аргументы на корректность.
    if(timer_div1 - 1 > 0xFFFF || timer_div2 - 2 > 0xFFFE || k == 0)
        return false;

    // Остановить измерения, если они запущены.
    if(TIM1->CCER & TIM_CCER_CC1E)
        fm_stop();

    // Инициализация глобальных переменных.
    m_cnt_prev = 0;
    m_capt = 0;
    m_corr = 0;
    m_capt_corr = 0;

    n_cnt_prev = 0;
    n_capt = 0;
    n_corr = 0;
    n_capt_corr = 0;

    dbuf.reset(k + 1);

    // Подготовка периферии и запуск.
    TIM2->CNT = 0;
    TIM2->CR1 |= TIM_CR1_CEN;
    TIM2->CCER |= TIM_CCER_CC1E;

    // Устанавливаем начальное значение счётчика 1, чтобы первое
    // зафиксированное в таймере значение по первому фронту сигнала
    // было равно 1.
    TIM1->CNT = 1;
    TIM1->CR1 |= TIM_CR1_CEN;
    TIM1->CCER |= TIM_CCER_CC1E;

    TIM6->CNT = 0;
    TIM6->CR1 |= TIM_CR1_CEN;

    TIM7->PSC = timer_div1 - 1;
    TIM7->EGR = TIM_EGR_UG;
    TIM7->ARR = timer_div2 - 1;
    TIM7->CR1 |= TIM_CR1_CEN;

    TIM6->DIER |= TIM_DIER_UIE;
    TIM7->SR = ~TIM_SR_UIF;
    TIM7->DIER |= TIM_DIER_UIE;

    return true;
}

// Получить результаты для последнего завершённого интервала измерения
// (вычисляются по первому и последнему элементу в буфере).
// Непосредственный доступ к глобальным переменным, без использования
// данной функции недопустим, т.к. в любой момент переменные могут быть
// изменены в обработчике прерывания (допускается чтение флага
// dbuf.is_ready и вызов метода dbuf.get_cnt()).
// Результаты возвращаются в аргументах, передаваемых по ссылке:
// m - количество периодов входного сигнала за интервал измерения;
// n - количество импульсов опорного генератора (тактового сигнала) за
// интервал измерения;
// cnt - счётчик количества записей в буфер (можно использовать для
// быстрой проверки того, был ли модифицирован буфер или его содержимое
// не изменялось);
// c0 - количество переполнений 32-разрядного целого без знака с
// количеством импульсов тактового сигнала от момента запуска
// измерений до начала того интервала измерения, для которого
// возвращаются m и n (для точной и однозначной временной привязки
// интервала измерения).
// Функция возвращает false в случае ошибки (недостаточно данных в
// буфере - с момента запуска частотомера прошло меньше времени, чем
// составляет длительность одного интервала измерения).
bool get_result(uint32_t &m, uint32_t &n, uint32_t &cnt, uint32_t &c0)
{
    m = 0;
    n = 0;
    cnt = dbuf.get_cnt();
    c0 = dbuf.c0;
    bool r = dbuf.is_ready;
    if(r)
    {
        __disable_irq();
        cnt = dbuf.get_cnt();
        c0 = dbuf.c0;
        volatile const Data *p1 = dbuf.first();
        volatile const Data *p2 = dbuf.last();
        m = p2->m - p1->m;
        n = p2->n - p1->n;
        __enable_irq();
    }
    return r;
}

int main()
{
    // Инициализация оборудования.
    display_init();
    fm_init();

    // Интервал опроса 0.1с; 5 опросов на интервал измерения, т.е.
    // интервал измерения составляет 0.5с.
    fm_start(240, 10000, 5);

    // Предыдущее значение счётчика записей в буфер.
    uint32_t cnt0=0;

    while(true)
    {
        uint32_t m, n, cnt, c0;
        if(get_result(m, n, cnt, c0)&&cnt0!=cnt)
        {
            cnt0=cnt;

            // Далее можем рассчитать среднее значение частоты сигнала
            // за последний только что завершившийся интервал измерения.
            // float f=(float)SystemCoreClock*m/n;

            // Или выполнить иные действия с новыми полученными данными.
            display(SystemCoreClock, m, n);
        }
    }

    // Нет возврата из main, поэтому оператор return необязателен.
    // return 0;
}

Пояснения к тексту программы

Важнейший элемент программы частотомера - массив с результатами опроса таймеров, который представлен в программе в виде буфера (глобальная переменная dbuf типа DBuf). Среди методов, определённых для буфера, особенно следует отметить следующие:
void DBuf::push(const Data &d) volatile;
Data *DBuf::first() volatile;
Data *DBuf::last() volatile;

push помещает очередной элемент в конец буфера;
first используется для доступа к первому элементу буфера (в программе осуществляется только доступ для чтения);
last используется для доступа к последнему элементу (в программе выполняется только чтение).

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

Максимальное количество элементов в буфере задаётся при конструировании объекта:
DBuf::DBuf(uint32_t _size);
оно может быть впоследствии изменено с помощью метода
void DBuf::reset(uint32_t _size) volatile;
который также удаляет всё содержимое буфера.

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

bool is_full() const volatile;
возвращает true, если текущее количество элементов в буфере равно максимальному (буфер полон и добавление каждого нового элемента будет сопровождаться удалением первого) и возвращает false, если буфер ещё не полон.

uint32_t get_cnt() const volatile;
возвращает количество вызовов метода push к данному моменту; что может быть полезно для того, чтобы быстро определить, изменилось ли содержимое буфера с момента последней проверки.

uint32_t size() const volatile;
возвращает количество элементов в буфере в данный момент.

Также буфер содержит два члена, которые уже относятся не столько к самому буферу, сколько к процессу измерения частоты:
bool is_ready; устанавливается в true, когда данные в буфере готовы для вычисления первого значения частоты (во-первых, буфер должен быть заполнен; во-вторых, первый элемент в буфере должен соответствовать отсчёту, выполненному после того, как произошла хотя бы одна фиксация);
uint32_t c0;
счётчик переполнений 32-битового целого (без знака) значения, используемого для подсчёта импульсов от эталонного генератора (т.е. импульсов тактового сигнала микроконтроллера в данном случае); этот счётчик и 32-битовое значение текущего количества подсчитанных импульсов образуют 64-битовое значение, которое позволяет определять момент начала текущего интервала измерения (относительно момента запуска измерений). 64-битовое значение переполняется очень редко, поэтому исключается неоднозначность в интерпретации величины.

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

// Предыдущее значение счётчика фронтов входного сигнала.
volatile uint16_t m_cnt_prev;

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

// Предыдущее значение счётчика импульсов опорного генератора.
volatile uint16_t n_cnt_prev;

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

// Корректирующее значение для текущего значения счётчика фронтов,
// учитывающее переполнения счётчика.
volatile uint32_t m_corr;

Это значение в сумме с m_cnt_prev даёт действительное значение количество подсчитанных фронтов в момент считывания m_cnt_prev (точнее говоря, младшие 32 бита истинного значения). За счёт данной корректирующей переменной происходит программное расширение счётчика до 32 бит.

// Корректирующее значение для текущего значения счётчика импульсов
// опорного генератора.
volatile uint32_t n_corr = 0;
В сумме с n_cnt_prev даёт 32-битовое значение количества подсчитанных импульсов эталонного генератора (здесь - тактовых импульсов) в момент считывания n_cnt_prev, расширяя, таким образом, разрядность счётчика.

// Последнее зафиксированное значение счётчика фронтов.
volatile uint16_t m_capt;

// Корректирующее значение для последнего результата фиксации счётчика
// фронтов входного сигнала.
volatile uint32_t m_capt_corr;
В сумме эти две величины дают последний результат фиксации счётчика фронтов входного сигнала в виде 32-битового значения. Актуальность значения гарантируется только после вызова функции _update_fm_vars при запрещённой фиксации в каналах таймеров частотомера.

// Последнее зафиксированное значение счётчика импульсов опорного
// генератора.
volatile uint16_t n_capt;

// Корректирующее значение для последнего захваченного значения
// счётчика импульсов опорного генератора.
volatile uint32_t n_capt_corr;
В сумме эти две величины дают последний результат фиксации счётчика импульсов тактового сигнала в виде 32-битового значения. Актуальность значения гарантируется только после вызова функции _update_fm_vars при запрещённой фиксации в каналах таймеров частотомера.

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

Теперь кратко рассмотрим определяемые в программе функции.

void _update_fm_vars();
данная, уже неоднократно упоминавшаяся функция, анализирует текущее состояние таймеров частотомера TIM1, TIM2, сравнивает с предыдущим, выявляет переполнения счётчиков и фиксации в каналах таймеров. В соответствии с полученной информацией обновляет глобальные переменные. То есть в функции сосредоточен основной код частотомера. Функция служебная, вызывается только из обработчика прерываний от таймера TIM6 (для контроля за переполнениями и фиксациями) и из обработчика прерываний от TIM7 (вызывается для периодического, с интервалом опроса, считывания результатов измерения при временно остановленных каналах таймеров TIM1, TIM2). Для того чтобы не происходило неучтённых переполнений счётчиков, функция должна вызываться чаще, чем происходят переполнения в TIM1, TIM2. Здесь вызов происходит, как минимум, вдвое чаще, что обеспечивает достаточно надёжную работу даже при наличии умеренно высокой вычислительной нагрузки от стороннего кода. Благодаря этой функции удаётся избежать дублирования одного и того же кода в разных обработчиках. Функция не должна вызываться откуда-либо ещё, кроме указанных обработчиков.

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

extern "C" void TIM6_DAC_IRQHandler();
Обработчик прерывания от таймера TIM6. Единственная его полезная работа - вызов функции _update_fm_vars. За счёт периодического вызова обработчика, своевременно выявляются переполнения счётчиков в таймерах TIM1, TIM2 и обнаруживаются новые фиксации, которым сопоставляются верные корректирующие значения.

extern "C" void TIM7_IRQHandler();
Обработчик прерывания от таймера TIM7. Как и предыдущий обработчик, он также вызывает функцию _update_fm_vars, но делает это при временно отключённой фиксации в TIM1 и TIM2 (для того, чтобы результаты опроса таймеров были синхронизированы, относились к одной фиксации). Затем фиксации снова разрешаются, а на основе полученных данных, в буфер помещается очередной элемент. Здесь же проверяется выполнение условия готовности буфера и если буфер содержит достаточно данных для вычисления средней частоты за последний интервал измерения, устанавливается флаг dbuf.is_ready. А также в этом обработчике выявляются переполнения 32-битового целого значения с количеством подсчитанных импульсов опорного генератора (т.е. в данном случае импульсов тактового генератора) на момент начала последнего интервала измерения и при обнаружении переполнения обновляется значение dbuf.c0.

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

bool fm_start(uint32_t timer_div1, uint32_t timer_div2, uint32_t k);
Функция запускает частотомер (или перезапускает с новыми параметрами, если он уже запущен); аргументы задают интервал опроса и длительность интервала измерения в интервалах опроса. Интервал опроса составляет $$ \delta = \text{timer_div1 * timer_div2 / SystemCoreClock}; $$ интервал измерения $$ \tau = k \delta = \text{k * timer_div1 * timer_div2 / SystemCoreClock}. $$ Новые данные будут поступать с интервалом, равным интервалу опроса, таким образом, интервал опроса определяет, как часто сможем получать результаты измерений частоты. От интервала измерения зависит погрешность счёта и нижний предел измерения частоты: чем больше интервал, тем меньше относительная погрешность и ниже частота входного сигнала, которую ещё будет способен измерить частотомер. Относительная погрешность может быть оценена как $$ \varepsilon \approx \frac 1 {F_0 \tau}, $$ где F0 = SystemCoreClock, частота опорного генератора, т.е. здесь - тактовая частота микроконтроллера.

Нижний предел измерения (если допускается, что некоторые измерения не будут успешными) $$ f_{min} = 1 / \tau. $$

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

Имеется ряд ограничений на значения аргументов функции.
Значение timer_div1 должно находиться в пределах 1..65536;
значение timer_div2 должно находиться в пределах 2..65536;
k должно быть больше нуля.

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

Кроме того, следует учитывать, что при запуске измерения частоты, требуется разместить буфер в памяти. Размер буфера составляет k + 1 элементов, каждый элемент имеет размер 8 байт. Понятно, что выделяемая для буфера память не должна превышать имеющегося в распоряжении свободного её объёма.

void fm_stop();
Функция останавливает измерение частоты. Может быть вызвана, если в дальнейшем не требуется получать результаты измерения.

bool get_result(uint32_t &m, uint32_t &n, uint32_t &cnt, uint32_t &c0);
Функция используется для получения величин m и n (количество периодов входного сигнала за интервал измерения и количество импульсов опорного генератора за интервал измерения), по которым вычисляется среднее значение частоты за последний интервал измерения. Величины определяются по первому и последнему элементу буфера и возвращаются в аргументах, передаваемых по ссылке. Непосредственный доступ к элементам буфера из программы недопустим, так как в любой момент содержимое буфера может измениться при обработке прерывания от таймеров частотомера (в данной функции это предотвращается с помощью временного запрета прерываний). Непосредственный доступ допускается только для чтения флага готовности dbuf.is_ready и вызова метода dbuf.get_cnt() для получения текущего значения счётчика записей в буфер.

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

Функция возвращает значение true в случае успешного завершения. Возвращает false, если буфер ещё не готов (накопилось недостаточно данных для вычисления первого результата измерения); от момента запуска частотомера до момента готовности буфера проходит от одного до полутора интервалов измерения, при условии наличия корректного сигнала на входе частотомера (с частотой в допустимых пределах). В любом случае, даже если возвращается false, величинам cnt и c0 будут присвоены корректные значения; m, n в случае ошибки получают нулевые значения.

int main();
В основной функции программы демонстрируется, как можно использовать частотомер. Здесь выполняется инициализация и запуск частотомера (в данном случае с интервалом опроса 0.1 с, интервалом измерения 0.5 с; 5 опросов на интервал измерения). После чего в бесконечном цикле опрашивается состояние буфера, при появлении новых данных вычисляется частота сигнала и полученное значение отображается на индикаторе (функции для работы с индикатором не определены в данном файле).

Недостатки предлагаемой реализации. Пути улучшения

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

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

Во-вторых, определённые неприятности могут возникнуть в связи с тем, что на время опроса отключается фиксация в таймерах частотомера. Это приводит к проблемам при измерении низких частот. Если бы опрос проводился без отключения каналов, минимальная частота входного сигнала, которая гарантировано могла быть измерена без сбоев, определялась бы из того условия, что на интервал измерения должен приходиться хотя бы один фронт сигнала (в противном случае все элементы буфера заполнятся одинаковыми элементами и результатом запроса m и n окажутся нулевые величины). Условие можно записать в следующем виде: $$ f \gt 1 / \tau. $$ Даже в случае отключения каналов, большинство измерений при выполнении этого условия, будут завершаться успешно. Неуспешным измерение может быть в том случае, если в какой-то интервал измерения попадает один фронт, и он приходится как раз на промежуток времени, когда фиксация отключена. С ростом частоты, количество фронтов входного сигнала на интервале измерения увеличивается, и если фиксация какого-либо фронта пропускается, это компенсируется последующей фиксацией. Правда возможно, хотя и крайне маловероятно, что все фронты в данном интервале измерения попадут в промежутки времени, когда фиксации отключены. Следует отметить, что отдельные неуспешные измерения не нарушают работу частотомера и последующие ненулевые результаты будут корректными.

Можно быть уверенным, что все измерения будут успешными, если $$ f \gt 1 / \delta, \\ \delta = \tau / k. $$ Кстати, при этом условии становится полностью оправданным использование заданного интервала опроса \( \delta \); при меньших частотах некоторые измерения оказываются избыточными (имеется определённое число повторяющихся результатов, из-за того, что интервал опроса меньше периода сигнала, т.е. запросы следуют чаще, чем в принципе может обновиться информация о частоте сигнала).

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

Как практически устранить недостатки рассмотренного частотомера и собрать полноценный законченный прибор, готовый к использованию, смотрите далее:
Частотомер на основе микроконтроллера STM32 с конвейерным измерением частоты - 2

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

  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
hamper, 2021-01-05
  Рейтинг@Mail.ru