Генерирование и чтение сигналов
Начнём с самого простого: генерация импульса заданной длины, такое часто бывает нужно. Проще всего сделать это на delay()
и delayMicroseconds()
:
void setup() { pinMode(3, OUTPUT); digitalWrite(3, HIGH); // вкл delayMicroseconds(500); // ждём 500 мкс digitalWrite(3, LOW); // выкл }
Нужно помнить, что digitalWrite()
сам по себе выполняется в районе 3.6 мкс (58 тактов процессора). Для ускорения можно использовать например библиотеку directIO или прямую работу с регистрами портов.
Генерирование квадратного сигнала
Программное
Квадратный сигнал может быть использован для тактирования и управления, а также для генерации звука через усилитель. Самый базовый пример, Blink, по сути тоже является генератором квадратного сигнала:
void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) delay(1000); // wait for a second digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW delay(1000); // wait for a second }
Если заменить 1000
например на 10
, то получится квадратный сигнал с частотой 50 Гц. Этот способ называется программной генерацией сигнала, то есть микроконтроллер своими силами считает время и сам вручную дёргает ногой. Это как мешает работе остального кода, так и остальной код может сбивать частоту. Такую генерацию можно сделать более мене асинхронной на миллисе:
void setup() { pinMode(LED_BUILTIN, OUTPUT); } uint32_t tmr; bool state; void loop() { if (millis() - tmr >= 10) { // таймер 10 мс tmr += 10; state = !state; digitalWrite(LED_BUILTIN, state); } }
На практике такой способ используется редко, потому что на высокой частоте остальной код программы будет мешать генерации и частота будет плавать.
Функция tone()
В ядре Arduino есть встроенная функция для полуаппаратной генерации квадратного сигнала - tone(pin, frequency, duration)
:
pin
- цифровой пин, с которого будет генерироваться сигнал.frequency
- частота в Герцах. Диапазон 31.. 65'535 Гцduration
- продолжительность сигнала в миллисекундах. Опциональный параметр, если не указывать - сигнал будет генерироваться без остановки.
Для ручной остановки генерации сигнала можно вызвать noTone()
. Также у генерации при помощи tone()
есть особенности:
- Генерация является полуаппаратной: пин дёргается МК "вручную" по прерыванию таймера, что на высокой частоте может чуть тормозить код.
- Генерация использует Timer 2, перенастройка или использование его для других целей (включая ШИМ на пинах D3 и D11 у Nano) отключит активную генерацию или изменит её частоту.
- При вызове
tone()
таймер перенастраивается на генерацию, то есть можно использовать таймер в своих целях между вызовамиtone()
.
- При вызове
- Генерация работает только на одном пине в один момент времени, причём для включения генерации на другом пине нужно сначала отключить текущую генерацию, то есть вызвать
noTone()
.
ШИМ сигнал
Аппаратный таймер позволяет генерировать квадратный сигнал аппаратно и полностью асинхронно работе остального кода, не тратя ни такта процессорного времени: время считается самим таймером, и сам же таймер управляет состоянием ноги МК. Для генерации ШИМ сигнала в среде Arduino есть функция analogWrite(pin, duty)
, подробнее мы говорили в ней в уроке про ШИМ. Чтобы сделать ШИМ квадратным, нужно запустить его с duty
, равной 128
. Что касается частоты полученного сигнала, то Ардуино настраивает таймеры так, что частота в зависимости от таймера может быть 490 или 980 Гц. Частоту можно изменить с довольно большим шагом, об этом мы говорили в уроке про увеличение частоты ШИМ.
Аппаратный таймер
Можно вручную настроить аппаратный таймер на генерацию квадратного сигнала. Тонкости настройки регистров таймера мы в рамках этих уроков не разбираем, но это можно сделать и при помощи библиотеки, например GyverTimers. Работу библиотеки мы разбирали в уроке о прерываниях таймера. Данная библиотека позволяет настроить генерацию квадратного сигнала с максимально возможной точностью и частотой, а также поднять на одном таймере генерацию двух или трёх (Arduino MEGA) меандров со смещением по фазе. Пример:
// Пример генерации меандра на таймере 2 , канале B (D3 на Arduino UNO) #include "GyverTimers.h" void setup() { pinMode(3, OUTPUT); // настроить пин как выход // из-за особенности генерации меандра таймером // частоту нужно указывать в два раза больше нужной! Timer2.setFrequency(500 * 2); // настроить частоту таймера в Гц Timer2.outputEnable(CHANNEL_B, TOGGLE_PIN); // в момент срабатывания таймера пин будет переключаться } void loop() { }
ШИМ сигнал
Аппаратный
Для генерации ШИМ сигнала с заданным заполнением есть стандартная функция analogWrite(pin, duty)
, подробнее обсуждали в уроке про ШИМ сигнал, а частоту можно изменить перенастройкой таймера, как в уроке об увеличении частоты ШИМ. На самом деле таймеры позволяют настроить ШИМ сигнал с более точной или более высокой частотой и другими диапазонами заполнения (до 10 бит), но в ядре Arduino это не предусмотрено. Если такое будет нужно, можно воспользоваться библиотекой GyverPWM. Пример:
pinMode(10, OUTPUT); // запустить ШИМ на D10 с частотой 150'000 Гц, режим FAST_PWM PWM_frequency(10, 150000, FAST_PWM);
Программный ШИМ
Программная генерация ШИМ сигнала может пригодиться, если не хватает лишнего таймера или частота ШИМ низкая и не повлияет на остальной код, а он на неё. ШИМ сигнал на "миллисе" можно организовать вот таким образом, переключая выход по двум периодам:
Здесь мы на каждом вызове считаем новый период переключения, тратя на это какое-то время. Можно считать период в отдельной функции, а сам ШИМ генерировать отдельно. Реализацию можно посмотреть в библиотеке PWMrelay.
Полуаппаратный ШИМ
Можно снизить нагрузку на процессор, отдав счёт времени аппаратному таймеру. Примеры на базе GyverTimers (для ATmega328, 2560):
Как известно, digitalWrite()
является очень тяжёлой и долгой функцией, и для генерации софт ШИМ рекомендуется заменить её чем-то более быстрым, например прямым обращением к регистру или вот такой конструкцией (для ATmega328p):
void digitalWriteFast(uint8_t pin, bool x) { if (pin < 8) { bitWrite(PORTD, pin, x); } else if (pin < 14) { bitWrite(PORTB, (pin - 8), x); } else if (pin < 20) { bitWrite(PORTC, (pin - 14), x); } }
Если не хватает количества стандартных ШИМ-выходов, можно поднять полуаппаратный ШИМ на таймере на несколько пинов сразу:
Этот алгоритм является не самым оптимальным, более интересный можно посмотреть в GyverHacks.
Примечание: во всех трёх алгоритмах используется проверка совпадения со счётчиком counter == pwm_duty
. Это сильно снижает использование процессорного времени в прерывании, но при резком уменьшении заполнения может приводить к одиночным "вспышкам" заполнения до максимума, так как условие не выполнится. Для более плавной работы можно сделать counter >= pwm_duty
, тогда условие будет каждый раз "подстраиваться" под новое значение заполнения, но установка пина будет осуществляться на каждом тике!
Можно ввести буферизацию заполнения ШИМ и брать новое значение только при нулевом значении счётчика, это решит проблему:
Можно применить буферизацию и к остальным алгоритмам.
Библиотека Servo
Как известно, RC сервоприводы управляются при помощи ШИМ сигнала с частотой ~50 Гц и длительностью импульса от~500 до ~2500 микросекунд. В стандартной библиотеке Servo.h реализована генерация полуаппаратного ШИМ сигнала, причём количество пинов можно менять во время работы. Библиотеку можно использовать как генерацию ШИМ, если его параметры подходят для использования.
Чтение сигналов
Чтение цифрового сигнала сводится к измерению времени между его импульсами, то есть изменениями состояния HIGH-LOW: так можно измерить период и частоту квадратного сигнала, заполнение и частоту ШИМ и вообще любой другой сигнал.
Функция pulseIn()
В ядре Ардуино есть готовые функции для измерения импульсов:
pulseIn(pin, value, timeout)
- для импульсов от 10 мкс до ~3 минут, работает на счёте тактов процессора, лучше работает при отключенных прерываниях, более точно измеряет короткие импульсы.pulseInLong(pin, value, timeout)
- для импульсов от 10 мкс до ~3 минут, основано наmicros()
(т.е. на Таймере 0), не работает при отключенных прерываниях, более точно измеряет длинные импульсы.
Измеренная мной точность на коротких импульсах: 0.5 мкс
Обе функции возвращают длину импульса в микросекундах. Возвращают 0, если импульса не было и был достигнут тайм-аут. Обе функции блокирующие, то есть останавливают выполнение кода, пока не поймают импульс или не завершатся по тайм-ауту. Аргументы:
pin
- цифровой пин (GPIO), на котором ожидается импульс.value
- направление импульса,HIGH
илиLOW
.timeout
- тайм-аут ожидания импульса в микросекундах. Необязательный параметр, по умолчанию равен 1'000'000 мкс (1 секунда).
Как это работает: пусть мы настроили импульс на HIGH, функция будет ожидать изменение значения с LOW
на HIGH
. Если скачок с LOW
на HIGH
не произошёл за время, установленное тайм-аутом, функция завершит выполнение и вернёт 0.
Для превращения длины импульса (мкс) в частоту (Гц) достаточно поделить на него секунду (точнее, 1'000'000 мкс).
Измеряем сигналы вручную
Библиотека тахометра
Также предлагаю использовать класс тахометра, оформленный в виде библиотеки. Скачать можно с гитхаб. Также прикладываю здесь:
Пользоваться этим тахометром очень просто: вызываем метод тик при срабатывании датчика оборотов, например во внешнем аппаратном или PCINT прерывании, а затем забираем результат в любое время при помощи методов getRPM(), getHz() и getPeriod(). Внутри библиотеки можно настроить параметр _TACHO_TICKS_AMOUNT, отвечающий за количество тиков, время которых будет измерено для расчёта.#define TACH_PIN 2 // пин тахометра (желательна внешняя подтяжка 10к к VCC) #include "Tacho.h" Tacho tacho; void setup() { Serial.begin(9600); pinMode(TACH_PIN, INPUT_PULLUP); // тахо пин вентилятора тянем к VCC attachInterrupt(0, isr, FALLING); // настраиваем прерывание } // обработчик прерывания void isr() { tacho.tick(); // дёргаем тик } void loop() { // выводим два раза в секунду Serial.println(tacho.getRPM()); // вывод оборотов в минуту //Serial.println(tacho.getHz()); // вывод Герц //Serial.println(tacho.getPeriod()); // вывод периода delay(500); }
"Запоминаем" сигнал
Также можно очень просто запомнить цифровой сигнал в Arduino для дальнейшего воспроизведения и исследования. Вот пример, который будет работать на любом пине (используется digitalRead() и micros()):
Измерение начнётся по изменению сигнала и продлится до тех пор, пока не переполнится буфер или работа не завершится по таймауту (в примере выше 1 секунда с последнего импульса). После этого в монитор порта будет выведен начальный уровень сигнала fval
и тайминги каждого следующего фронта/спада (изменения). Для примера я подключил ИК приёмник и нажал кнопку на пульте:
Точность измерения должна быть около 5 мкс на AVR, так как используется тяжёлое чтение пина и микрос. Лучше переписать на прерывания по CHANGE
и завести отдельный таймер, тогда точность можно повысить в сотни раз. Код не привожу, так как для разных платформ он будет разный.
Полученные данные можно использовать для анализа интерфейсов и протоколов, а также можно "воспроизвести" запись. В примере ниже я делаю это отдельным скетчем, вставив тайминги и начальное значение сигнала из лога предыдущего примера.
Для визуализации подключил дешёвый китайский логический анализатор (ссылка на али, ещё одна) на указанный пин:
Отлично! Прекрасно виден NEC протокол, его 4-х байтный пакет и код повтора.
Можно реализовать чтение пакета и его вывод в одной программе, запись дампа в EEPROM/SD для воспроизведения по кнопке и прочих сценариев работы, получив дубликатор цифрового сигнала.
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])