Оценка методов измерения низких частот на Arduino. Описание частотомера на микроконтроллере

Приводится описание частотомера, измеряющего частоту до 1 МГц, построенного на основе платы Arduino UNO. Но, прежде всего, хочу напомнить, что Arduino UNO это небольшая печатнаяплата, на которой расположен микроконтроллер ATMEGA328, а так же вся его «обвязка», необходимая для его работы, включая USB-программатор и источник питания.

Стоимость Arduino UNO лежит в пределах от 200 до 1000 рублей, в зависимости от места продажи. Конечно, самое дешевое на радиорынке и китайском интернет-посылторге «Aliexpress». Описание платы Arduino UNO, а также программного обеспечения для неё, и подключения к персональному компьютеру приводится в Л.1, так что, если кто не в курсе, обязательно прочтите сначала статью в Л.1.

Принципиальная схема

Схема частотомера показана на рис. 1. Как видно из схемы, к цифровым портам D2-D7 платы Arduino UNO подключен модуль жидкокристаллического индикатора Н1 типа 1602А.

А входной сигнал поступает через вполне понятный усилитель-формирователь на транзисторе VT1 и микросхеме D1 на порт D8. Питается входной усилитель-формирователь и ЖК-индикатор от стабилизатора напряжения 5V, имеющегося на плате Arduino UNO.

Рис. 1. Принципиальная схема частотомера (до 1МГц) на платформе Arduino UNO.

Но, вернемся к ЖК-индикатору. Индикатор представляет собой плату, на которой установлен собственно ЖК-дисплей и схема для его обслуживания, выполненная на двух безкорпусных микросхемах. Индикатор 1602А стандартный, на основе контроллера HD44780. Обозначение 1602А фактически значит, что он на две строки по 16 символов в строке.

Индикатор был куплен на «Aliexpress», найден по запросу «HD44780» (цены от 81 рубля).

Питание +5V на ЖК-индикатор поступает через вывод 2 его платы. Общий минус на выводы 3 и 1. Поскольку в индикатор планируется только передавать информацию от контроллера, а не наоборот, вывод 5 (RW) соединен с нулем. Данные на ЖК-индикатор будут поступать через его выводы 11-14 (выводы 7-10 не используются).

Выводы 15 и 16 служат для подключения подсветки ЖК-индикатора. На них подается напряжение 5V. Для управления ЖК-индикатором решено было использовать порты с D2 по D7 платы Arduino UNO. В принципе, можно и другие порты, но я вот так, решил использовать именно эти.

ЖК-индикатор подключили. Но просто так он работать не станет. Для того чтобы он взаимодействовал с Arduino UNO нужно в программу загрузить подпрограмму для его управления.

Программа

Такие подпрограммы почему-то называются «библиотеками», и в программном комплекте для Arduino UNO есть много разных «библиотек».

Таблица 1. Исходный код программы.

Для работы с ЖК-индикатором на основе HD44780 нужна библиотека LiquidCrystal. Поэтому программа для нашего частотомера (таблица 1) начинается с загрузки этой библиотеки:

Эта строка дает команду загрузить в Arduino UNO данную библиотеку. Затем, нужно назначить порты Arduino UNO, которые будут работать с ЖК-инди-катором. Как я уже сказал, я выбрал порты с D2 по D7. Эти порты назначены строкой:

LiquidCrystal led(2, 3, 4, 5, 6, 7);

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

Среди набора функций языка для программирования Arduino UNO есть такая функция: pulseln , перевести это можно как «входной импульс». Эта функция измеряет в микросекундах длительность положительного либо отрицательного перепада входного импульса. Так что измерение частоты здесь будет происходить через предварительное измерение периода.

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

В программе длительность положительного полупериода обозначена Htime, длительность отрицательного полупериода - Ltime, а длительность всего периода - Ttime.

Измерение полупериодов происходит в строках:

Htime=pulseln(8,HIGH);

Ltime=pulseln(8,LOW);

Затем, производится вычисление полного периода в строке:

Ttime=Htime+Ltime ;

Вычисление частоты, учитывая, что значение периода выражено в микросекундах, происходит здесь:

frequency=1000000/Ttime;

Затем, указывается строка ЖК-индика-тора (нижняя строка, это строка 1), в которую записывается результат:

lcd.setCursor (ОД) ;

И результат записывается в ЖК-индикатор:

lcd.print("hz");

Завершается рабочий цикл частотомера индикацией результата в течение одной секунды, вернее, паузой в одну секунду, в течение которой на табло остается

измеренное значение (время выражено в миллисекундах, поэтому 1 сек = 1000): delay(1000);

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

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

например, так: delay(2000);

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

Теперь о других деталях программы.

В строке:

pinMode(8, INPUT);

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

В строке:

lcd.begin(16,2);

указано что индикатор двухстрочный, по 16 знаков в строке.

Индикатор у нас двухстрочный. Одна строка остается свободной и в неё можно записать что угодно. Здесь в верхнюю строку (верхняя строка, это строка 0) записано слово «frequency»:

lcd.setCursor(0,0);

lcd.print("frequency");

К сожалению, китайский ЖК-индикатор по-русски не понимает, поэтому писать можно только латинскими буквами. Если попробовать написать кириллицей, например, слово «частота», он начинает гнусно ругаться китайскими иероглифами. Если на вход (порт D8 ARDUINO UNO) сигнал не поступает, на ЖК-индикаторе будет мигать: «infhz». Работу частотомера можно ускорить или замедлить, изменив время индикации, как это сказано выше.

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

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

frequency=1000000/Ttime;

подставив вместо 1000000 другое значение, или вообще изменив формулу вычисления результата, соответственно той, которая необходима для конкретного случая применения прибора. Соответственно, нужно будет изменить и единицу измерения, заменив её в строке: lcd.print("hz"); И изменить заголовок в верхней строке ЖК-индикатора, изменив его в строке: lcd.print("frequency");

Напряжение питания частотомера может быть от 7 до 12V. Питание подается на соответствующее гнездо на плате ARDUINO UNO. Напряжение может быть нестабилизированным (на плате есть собственный стабилизатор на 5V), но

обязательно хорошо отфильтрованным (пульсации должны быть минимальными). Питание возможно и от источника напряжением 5V через USB-порт, но в этом случае, напряжение должно быть не только хорошо отфильтрованным, но и стабилизированным.

Схема входного усилителя состоит из собственно входного усилителя на транзисторе VТ1 и триггера Шмитта на элементах микросхемы D1. Кстати, микросхему К561ЛЕ5 или К561ЛА7 можно заменить любой КМОП-микросхемой, в которой есть не менее трех инверторов, соответственно, изменив схему.

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

При питании от гальванической батареи, например, «Кроны» или её аналога напряжением 9V, с целью экономии источника питания можно отключить подсветку ЖК-индикатора. Для этого нужно в разрыв его вывода 15 или 16 включить выключатель, которым включать подсветку, если она нужна. Если же подсветка вообще не требуется, выводы 15 и 16 ЖК-индикатора можно вообще не подключать.

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

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

Каравкин В. РК-12-16.

Литература:

1. Каравкин В. Ёлочная мигалка на Arduino как средство от боязни микроконтроллеров. РК-11-2016.

Построенный . Он позволяет измерять частоты до 10 МГц в четырех автоматически переключаемых диапазонах. Наименьший диапазон имеет разрешение 1 Гц.

Технические характеристики частотомера

  • Диапазон 1: 9,999 кГц, разрешение 1 Гц.
  • Диапазон 2: 99,99 кГц, разрешение до 10 Гц.
  • Диапазон 3: 999.9 кГц, разрешение до 100 Гц.
  • Диапазон 4: 9999 кГц, разрешение до 1 кГц.

Описание частотомера на микроконтроллере

Микроконтроллер Attiny2313 работает от внешнего кварцевого генератора с тактовой частотой 20 МГц (это максимально допустимая частота). Точность измерения частотомера определяется точностью данного кварца. Минимальная длина полупериода измеряемого сигнала должна быть больше, чем период кварцевого генератора (это связано с ограничениями архитектуры микроконтроллера ATtiny2313). Следовательно, 50 процентов от тактовой частоты генератора составляет 10 МГц (это максимальное значение измеряемой частоты).

Установка фьюзов (в PonyProg):

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

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

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

Вариант 1. Переделываем pulseInLong()

На функции pulseIn() я сначала и зациклился - а нельзя ли ее приспособить к этому делу? В недрах папок Arduino (в файле wiring_pulse.c) обнаружился ее более продвинутый вариант под названием pulseInLong(). Введен он был, как я выяснил, где-то около версии 1.6.5, но чем этот вариант лучше оригинальной функции, так и не понял. Судя по тому, что функция не введена в официальный перечень - или ничем, или имеет какие-то невыясненные ограничения. Но структура ее мне показалась более прозрачной и проще поддающейся переделке в нужном направлении. Выглядит вызов функции так же, как и pulseIn():

Unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout)
В функции последовательно работают три условно-бесконечных цикла (с принудительным выходом по заданному timeout). В первом цикле ожидается перепад в состояние, заданное параметром state (HIGH или LOW) - чтобы пропустить текущий импульс, в середину которого мы, возможно, попали. Во втором ожидается начало следующего периода (обратный перепад), после чего определяется количество микросекунд на старте. Наконец, третий цикл - измерительный, ожидается опять перепад в состояние state, фиксируется разность количества микросекунд и возвращается в значении функции.

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

#define Tone_PIN 12 // выход частоты – см. в тексте #define IN_PIN 8 //вход обнаружения частоты volatile unsigned long ttime = 0; //Период срабатывания датчика unsigned long periodInLong(uint8_t pin, uint8_t state, unsigned long timeout) { // cache the port and bit of the pin in order to speed up the // pulse width measuring loop and achieve finer resolution. calling // digitalRead() instead yields much coarser resolution. uint8_t bit = digitalPinToBitMask(pin); uint8_t port = digitalPinToPort(pin); uint8_t stateMask = (state ? bit: 0); unsigned long startMicros = micros(); // wait for any previous pulse to end while ((*portInputRegister(port) & bit) != stateMask) { if (micros() - startMicros > timeout) return 0; } // wait for the pulse to start while ((*portInputRegister(port) & bit) == stateMask) { if (micros() - startMicros > timeout) return 0; } unsigned long start = micros(); // wait for the pulse to stop while ((*portInputRegister(port) & bit) != stateMask) { if (micros() - startMicros > timeout) return 0; } // wait for the pulse to start while ((*portInputRegister(port) & bit) == stateMask) { if (micros() - startMicros > timeout) return 0; } return micros() - start; } void setup() { pinMode(IR_PIN, OUTPUT); //на выход pinMode(IN_PIN, INPUT); //вывод обнаружения частоты на вход Serial.begin(9600); // tone(Tone_PIN, 1000); } void loop() { ttime=periodInLong(IN_PIN, LOW, 1000000); //ожидание 1 сек Serial.println(ttime); if (ttime!=0) {//на случай, если частота пропала float f = 1000000/float(ttime); // Вычисляем частоту сигнала в Гц Serial.println(f,1);} delay(500); }
Обратите внимание на вывод Tone_PIN и закомментированный вызов функции tone() в разделе setup(). Это сделано для проверки еще одного обстоятельства, о чем в конце статьи.

Для проверки работы на вывод 8 (произвольно выбранный в качестве IN_PIN) подавался сигнал от самодельного генератора на основе часового кварца и счетчика-делителя 561ИЕ16. На выходе его мы получаем частоты, кратные степеням двойки, от 2 до 2048 Гц, а также 16384 Гц (и при желании, еще 32768 Гц прямо с генератора).

Результаты выборки последовательных измерений для частот 2, 8, 64, а также 2048 и 16384 герца объединены в одну таблицу на рисунке (верхняя строчка - длительность в микросекундах, следующая - рассчитанная частота):

Как мы видим из этих данных, способ вполне удовлетворительно работает для низких частот (ниже примерно 100 герц), но «дребезжит» на высоких частотах. Это нормально, если вспомнить, что функция micros() завязана на переполнения и счетчики Timer0, а вызов ее и манипуляции с длинным целым занимают значительное время. Простые условно-бесконечные циклы в контроллерах в принципе не очень надежная штука.

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

Способ 2. Идеологически правильный: задействуем Timer1

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

Способ состоит в том, что мы запускаем 16-разрядный Timer1 сразу в двух режимах: счета и захвата событий. При счете удобно установить делитель тактовой частоты 1/8, тогда в 16-мегагерцовом контролере время подсчитывать будем тиками по половине микросекунды. При переполнении (65536 половин микросекунды) вызывается прерывание переполнения, которое инкрементирует счетчик третьего разряда (байта) длинного числа. Трех байтовых разрядов (16777216 половин микросекунды или около 8 секунд) вполне достаточно для наших целей подсчета периода частоты порядка единиц-десятков герц. По захвату события перепада уровня мы фиксируем трехразрядное число тиков, прошедших с предыдущего такого события (собирая его из значений регистров таймера плюс третий старший разряд), обнуляем все переменные и счетные регистры и ждем следующего перепада. По идее надо бы еще очищать счетчики предделителя тактовой частоты, но они все равно изменятся при работающем Timer0 (предделитель для таймеров 0 и 1 общий), а при делителе 1/8 эта ошибка будет незначимой.

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

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

Я предполагаю, что найдется немало народу, который захочет оспорить это утверждение. В Сети масса источников, выдвигающих тезис о том, что прерывания применять опасно, потому что якобы можно что-то потерять. Глубоко ошибочное мнение: все ровно наоборот - как раз в цикле loop() потерять легко, а в прерываниях очень трудно. Правильная программа должна работать преимущественно на прерываниях (за исключением процедур, которые там нельзя использовать - вроде команды sleep). Только тогда из контроллера можно выжать максимум возможного. Внутри контролера не бывает функций, которые длятся настолько долго, чтобы существенно помешать другим функциям в других прерываниях, даже если это операции c числами типа long или float. Самая долгая из операций - деление чисел типа long - выполняется примерно за 670-680 тактов , то есть где-то за 42 микросекунды и она редко бывает больше чем одна на все прерывание. Вот обмен с внешней средой длится гораздо дольше: так, передача байта со скоростью 9600 длится примерно миллисекунду. Но длинные процедуры обмена с ожиданием ответа вполне можно расставить в программе так, чтобы не мешать измерительным или иным операциям, критичным ко времени. А если все-таки ваш контроллер оказывается забит длительными вычислительными процедурами, то это значит, что неверно выбрана платформа: переходите на 32 разряда или вообще на Raspberry Pi.

Разберемся в этом плане с нашим примером подробнее. Сам подсчет тиков в Timer1 происходит аппаратно и ни от чего не зависит. Прерывания переполнения для наших условий (1/8 тактовой частоты 16 МГц) происходят каждые 32 миллисекунды. Само прерывание состоит из единственной операции инкрементирования переменной третьего разряда размером в один байт (см. далее). Если бы я реализовывал это на ассемблере, то хранил бы третий разряд в рабочем регистре и операция заняла бы ровно один такт. В случае Arduino (и вообще реализаций на С) переменная хранится в ОЗУ, потому еще теряется несколько тактов на извлечение/сохранение в памяти. Плюс вызов прерывания (7 тактов) и возврат (4 такта), то есть вся длительность процедуры составляет порядка микросекунды или чуть более. От длительности промежутка между переполнениями (32 мс) это составляет примерно 0,003%. И какова вероятность того, что некое случайное событие (например, нажатие внешней кнопки) произойдет именно в этот момент? Даже если вы будете все время нажимать на кнопку так быстро, как только сможете, вам едва ли удастся добиться совпадения.

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

Вот что способно реально помешать нашим прерываниям - это периодическое обновление функции millis() через прерывание переполнения Timer0, которое возникает каждую миллисекунду и длится несколько микросекунд (см. эту функцию в файле wiring.c). Относительно системного времени наши прерывания возникают также в случайный момент, но вероятность наткнуться на прерывание Timer0 составляет уже порядка процента, что немало. Если хотите пойти на поводу вашего перфекционизма, то на время измерений следует, строго говоря, отключать Timer0. Но если учесть, что максимальная ошибка составляет единицы микросекунд, а мы измеряем периоды длительностью от тысяч до сотен тысяч микросекунд, то на эту ошибку можно не обращать внимания.

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


Скетч для проверки этой идеи выглядит следующим образом:

#define Tone_PIN 12 // выход частоты– см. в тексте #define IN_PIN 8 //вход обнаружения частоты /ICP1 - для памяти volatile byte time2 = 0; //старший разряд времени, два младших - регистры таймера volatile unsigned long ttime = 0; //Время срабатывания датчика volatile unsigned long time_old = 0; //Предыдущее время volatile uint8_t flag=0; void setup() { pinMode(IR_PIN, OUTPUT); //на выход pinMode(IN_PIN, INPUT); //вывод обнаружения частоты на вход // установка регистров таймера1 TCCR1A = 0; // Устанавливаем захват по фронту и делитель 1/8 к тактовой частоте 16МГц TCCR1B =1< При выводе результатов мы учли, что один тик таймера равен 0,5 микросекунды. Кроме того, здесь введен флаг, который на время вывода препятствует изменению подсчитанной величины периода. На сам процесс измерений он не повлияет. Большая задержка в конце setup обусловлена необходимостью выждать некоторое время, иначе первые измерения будут ошибочными. Минимальная ее величина равна периоду измеряемых колебаний.

А где таймаут на срабатывание?

Кстати, действительно, а что будет, если частоты на входе нет вовсе? Эта ситуация никак не отрабатывается, потому что для безопасного выполнения программы это не требуется. Если оставить вход 8 подключенным к какому-либо потенциалу, то значение периода (переменная ttime) просто не будет меняться – в ней задержится то, что было ранее. Если это была какая-то частота, она и будет демонстрироваться. Если с момента загрузки ни одного импульса не проскочило, то будет ноль (на этот случай и ограничение вывода). А если, кстати, оставить вход 8 висящим в воздухе, то c довольно высокой точностью будет измеряться помеха 50 Гц.

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


Результаты измерений для тех же частот 2, 8, 64, а также 2048 и 16384 герца объединены в таблицу:

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

Способ 3. Самый простой: по внешнему событию

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

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

Скетч, реализующий эту идею, выглядит таким образом:

#define Tone_PIN 12 // выход частоты– см. в тексте #define IN_PIN 2 //вход обнаружения частоты volatile unsigned long ttime = 0; //Время срабатывания датчика volatile unsigned long time_old = 0; //Предыдущее время volatile uint8_t flag=0; void setup() { pinMode(IR_PIN, OUTPUT); //на выход pinMode(IN_PIN, INPUT); //вывод обнаружения частоты на вход attachInterrupt(0, impuls, RISING); //Прерывание по нарастающему фронту на D2 Serial.begin(9600); // tone(Tone_PIN, 8); delay(1000); } void impuls(){ if(flag!=1) ttime =micros()-time_old; time_old = micros(); } void loop() { flag=1; //чтобы ttime не изменилось в процессе вывода Serial.println(ttime); if (ttime!=0) {//на случай отсутствия частоты float f = 1000000/float(ttime); // Вычисляем частоту сигнала в Гц Serial.println(f,1);} flag=0; delay(500); }
Вход прерывания здесь будет другой - вывод номера 2 (вывод PD2). Как и ранее, флаг защищает от изменения переменой ttime в процессе вывода. Касательно отсутствия частоты на входе здесь действительны те же соображения, что и в предыдущем случае. Результаты измерения тех же частот представлены в таблице:


Как мы видим, здесь ошибка в измерениях находится в полном соответствии с разрешением функции micros(), равным 4 мкс - то есть фактически результат колеблется в пределах плюс-минус одного тика системного таймера, все законно. Это не лучшим образом сказывается на измерениях высоких частот, но для нашего диапазона вполне подходит. Потому этот способ можно рекомендовать для обычных применений.

Измерение контроллером частоты, генерируемой им самим

В общем-то это чисто познавательная задача, вероятно, не имеющая практических применений. Но мне стало интересно: а что будет, если измеряемая частота исходит от самого контролера? Удобно для этого использовать функцию tone(), которая занимает Timer2, и, следовательно, не будет пересекаться ни с системным временем, ни с Timer1 в случае его использования для измерения. Именно для этого в каждом из скетчей вставлена эта функция, работающая через вывод Tone_PIN.

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

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

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

Последнее время мне очень часто требуется измерять частоту, уж очень много электронных проектов я делаю и поэтому появилось нужда в измерительном приборе - частотомере. Покупать данный прибор - я ещё школьник в 8 классе учусь а такая техника очень дорогая для меня. Сильно большие частоты мне измерять пока нет необходимости, хотя в скором времени возможно будет нужно. И поэтому я решил сделать свой частотомер своими руками! Стремясь к минимализму за основу взял AVR микроконтроллер ATtiny2313 и ЖКИ 16*1. Набросал проект в , написал прошивку и нарисовал принципиальную схему:

Собственно ничего сложного, всё очень просто. Собрал всё на бредборде, кто не знает это - макетная плата с механическими контактами. Проверил, работает! Вот фото отчёт:

Ну теперь надо реализовать прибор, сделать печатную плату и поместить в корпус.

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

Самое главное это хорошо и качественно припаять микроконтроллер, ведь он в SOIC корпусе.

Не проблема, и мельче паяли! Главное не переборщить припоя и не жалеть канифоли.

Запаиваем остальные детальки, вот что получилось:

Кстати, от лишнего канифоля на плате можно избавиться с помощью технического спирта. Так намного лучше:

После сборки прошиваем микроконтроллер, я прошивал с помощью программы программатором . Вот фьюз биты:

Подключить программатор к микроконтроллеру можно проводками, подключить их к разъёму для ЖКИ:

А reset припаять:

Распиновку подключения программатора к микроконтроллера не привожу, я думаю вы её знаете. После прошивки и установки фьюз-битов, устанавливаем ЖКИ и подаём питание на устройство:

Заработало, отлично! Теперь устанавливаем устройство в корпус:

Как вы видите я свой частотомер сделал на базе своего , дело в том что я себе собрал более навороченный велокомпьютер (с большим дисплеем на Atmega32, скоро про него напишу статью) а из этого и решил сделать частотомер, только плату переделал. И конечно видео работы устройства:

На видео видно что в качестве генератора я использую компьютер и программу .

Список радиоэлементов

Обозначение Тип Номинал Количество Примечание Магазин Мой блокнот
IC1 МК AVR 8-бит

ATtiny2313-20PU

1 В блокнот
C1, C2 Конденсатор 22 пФ 2 В блокнот
С3 Конденсатор 0.1 мкФ 1 В блокнот
R1 Резистор

1 кОм

1 В блокнот
R2 Резистор

4.7 кОм

1 В блокнот
R3 Резистор

20 Ом

1 В блокнот
LCD ЖК индикатор 16*1 WH1601A 1 С совместимым HD44780 контроллером В блокнот
Z1 Кварц 16 МГц 1 В блокнот
Вход Разьём PBS-40 1

Конструктивно прибор состоит из дисплея, образованного семью 7-сегментными светодиодными индикаторами, микроконтроллера и нескольких транзисторов и резисторов. Микроконтроллер выполняет все необходимые функции, поэтому применение каких-либо дополнительных микросхем не требуется.

Принципиальная схема прибора достаточно проста и изображена на Рисунке 2. Проект в формате Eagle (принципиальная схема и печатная плата) доступен для скачивания в секции загрузок.

Выполняемые микроконтроллером задачи просты и очевидны: подсчет количества импульсов на входе за 1 секунду и отображение результата на 7-разрядном индикаторе. Самый важный момент здесь - это точность задающего генератора (временная база), которая обеспечивается встроенным 16-разрядным таймером Timer1 в режиме очистки по совпадению (CTC mode). Второй, 8-разрядный, таймер-счетчик работает в режиме подсчета количества импульсов на своем входе T0. Каждые 256 импульсов вызывают прерывание, обработчик которого инкрементирует значение коэффициента. Когда с помощью 16-разрядного таймера достигается длительность 1 с, происходит прерывание, но в этом случае в обработчике прерывания коэффициент умножается на 256 (сдвиг влево на 8 бит). Остальное количество импульсов, зарегестрированное счетчиком, добавляется к результату умножения. Полученное значение затем разбивается на отдельные цифры, которые отображаются на отдельном индикаторе в соответствующем разряде. После этого, непосредственно перед выходом из обработчика прерывания, оба счетчика одновременно сбрасываются и цикл измерения повторяется. В «свободное время» микроконтроллер занимается выводом информации на индикатор методом мультиплексирования. В исходном коде программы микроконтроллера автор дал дополнительные комментарии, которые помогут детально разобраться в алгоритме работы микроконтроллера.

Разрешение и точность измерений

Точность измерений зависит от источника тактовой частоты для микроконтроллера. Сам по себе программный код может вносить погрешность (добавление одного импульса) на высоких частотах, но это практически не влияет на результат измерений. Кварцевый резонатор, который используется в приборе, должен быть хорошего качества и иметь минимальную погрешность. Наилучшим выбором будет резонатор, частота которого делится на 1024, например 16 МГц или 22.1184 МГц. Чтобы получить диапазон измерения до 10 МГц необходимо использовать кварцевый резонатор на частоту 21 МГц и выше (для 16 МГц, как на схеме, диапазон измерений становится немного ниже 8 МГц). Кварцевый резонатор на частоту 22.1184 МГц идеально подходит для нашего прибора, однако приобретение именно такого с минимальной погрешностью для многих радиолюбителей будет сложной задачей. В таком случае можно использовать кварцевый резонатор на другую частоту (например, 25 МГц), но необходимо выполнить процедуру калибровки задающего генератора с помощью осциллографа с поддержкой аппаратных измерений и подстроечного конденсатора в цепи кварцевого резонатора (Рисунок 3, 4).

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

Входной сигнал

В общем случае на вход прибора может подаваться сигнал любой формы с амплитудой 0 … 5 В, а не только прямоугольные импульсы. Можно подавать синусоидальный или треугольный сигнал; импульс определяется по спадающему фронту на уровне 0.8 В. Обратите внимание: вход частотомера не защищен от высокого напряжения и не подтянут к питанию, это вход с высоким сопротивлением, не нагружающим исследуемую цепь. Диапазон измерений может быть расширен до 100 МГц с разрешением 10 Гц, если применить на входе соответствующий высокоскоростной делитель частоты.

Дисплей

В приборе в качестве дисплея используются семь светодиодных 7-сегментных индикаторов с общим анодом. Если яркость свечения индикаторов будет недостаточной, можно изменить номинал резисторов, ограничивающих ток через сегменты. Однако не забывайте, что величина импульсного тока для каждого вывода микроконтроллера не должна превышать 40 мА (индикаторы тоже имеют свой рабочий ток, о его величине не стоит забывать). На схеме автор указал номинал этих резисторов 100 Ом. Незначимые нули при отображении результата измерения гасятся, что делает считывание показаний более комфортным.

Печатная плата

Двухсторонняя печатная плата имеет размеры 109 × 23 мм. В бесплатной версии среды проектирования печатных плат Eagle в библиотеке компонентов отсутствуют семисегментные светодиодные индикаторы, поэтому они были нарисованы автором вручную. Как видно на фотографиях (Рисунки 5, 6, 7) авторского варианта печатной платы, дополнительно необходимо выполнить несколько соединений монтажным проводом. Одно соединение на лицевой стороне платы - питание на вывод Vcc микроконтроллера (через отверстие в плате). Еще два соединения на нижней стороне платы, которые используются для подключения выводов сегмента десятичной точки индикаторов в 4 и 7 разряде через резисторы 330 Ом на «землю». Для внутрисхемного программирования микроконтроллера автор использовал 6-выводный разъем (на схеме это разъем изображен в виде составного JP3 и JP4), расположенный в верхней части печатной платы. Этот разъем не обязательно припаивать к плате, микроконтроллер можно запрограммировать любым доступным способом.

Загрузки

Принципиальная схема и рисунок печтаной платы, исходный код и прошивки микроконтроллера -


Top