Генерирование и чтение сигналов

Начнём с самого простого: генерация импульса заданной длины, такое часто бывает нужно. Проще всего сделать это на 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.. 8’000’000 Гц, целые числа. С увеличением частоты растёт шаг изменения реальной частоты.
  • 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);

Программный ШИМ


Программная генерация ШИМ сигнала может пригодиться, если не хватает лишнего таймера или частота ШИМ низкая и не повлияет на остальной код, а он на неё. ШИМ сигнал на “миллисе” можно организовать вот таким образом, переключая выход по двум периодам:

Софт ШИМ на millis()

Функцию PWMgen(заполнение) в данной реализации нужно вызывать как можно чаще в основном цикле программы:

#define PWM_PRD 20   // период ШИМ, мс
#define PWM_PIN 13   // пин ШИМ

void setup() {
  pinMode(PWM_PIN, OUTPUT);
}
void loop() {
  PWMgen(analogRead(0) / 4);  // берём значение ШИМ с А0, 0.. 255
}

void PWMgen(byte duty) {
  static uint32_t tmr;
  static bool flag;
  if (duty == 0) digitalWrite(PWM_PIN, LOW);
  else if (duty == 255) digitalWrite(PWM_PIN, HIGH);
  else {
    int prd = (PWM_PRD * duty) >> 8;  // равносильно / 255
    if (millis() - tmr >= (flag ? prd : PWM_PRD - prd)) {
      tmr = millis();
      digitalWrite(PWM_PIN, flag = !flag);
    }
  }
}

Здесь мы на каждом вызове считаем новый период переключения, тратя на это какое-то время. Можно считать период в отдельной функции, а сам ШИМ генерировать отдельно. Реализацию можно посмотреть в библиотеке PWMrelay.

Полуаппаратный ШИМ


Можно снизить нагрузку на процессор, отдав счёт времени аппаратному таймеру. Примеры на базе GyverTimers (для ATmega328, 2560):

8 бит ШИМ
// частота ШИМ = ~ (частота вызова tick / 256);
#define PWM_FREQ 100    // частота ШИМ
#define PWM_PIN 13      // пин ШИМ

#include <GyverTimers.h>        // подключаем библиотеку аппаратных таймеров
volatile uint8_t pwm_duty = 0;  // переменная для хранения заполнения шим 0-255
volatile uint8_t counter = 0;   // аналог счетного регистра таймера

void setup() {
  Timer2.setFrequency(256L * PWM_FREQ);  // частота прерываний
  Timer2.enableISR();                    // включаем прерывания таймера
  pinMode(PWM_PIN, OUTPUT);              // пин как выход
}

void loop() {
  // задаём скважность ШИМ потенциометром с пина А0
  pwm_duty = analogRead(0) / 4;   // 1023 -> 255

  // задержка задания значения, чтобы избежать скачков
  delay(100);
}

// прерывание таймера
ISR(TIMER2_A) {
  if (!pwm_duty) digitalWrite(PWM_PIN, LOW);    // заполнение ШИМ == 0
  else {
    if (!counter) digitalWrite(PWM_PIN, HIGH);  // при обнулении счётчика HIGH
    else if (counter == pwm_duty) digitalWrite(PWM_PIN, LOW); // при совпадении LOW
    counter++;
  }
}
Настройка разрядности
// частота ШИМ = ~ (частота вызова tick / (глубина ШИМ + 1));
#define PWM_DEPTH 100   // разрядность ШИМ
#define PWM_PIN 13      // пин ШИМ

#include <GyverTimers.h>          // подключаем библиотеку аппаратных таймеров
volatile uint8_t pwm_duty = 0;    // переменная для хранения заполнения шим 0-255
volatile uint8_t counter = 0;   // аналог счетного регистра таймера

void setup() {
  Timer2.setPeriod(10);           // прерывания по таймеру с частотой 100 кгц
  Timer2.enableISR();             // включаем прерывания таймера 2
  pinMode(PWM_PIN, OUTPUT);       // устанавливаем пин на выход
}

void loop() {
  // получить заполнение ШИМ с потенциометра A0
  pwm_duty = map(analogRead(A0), 0, 1023, 0, PWM_DEPTH);

  // задержка задания значения, чтобы избежать скачков
  delay(100);
}

ISR(TIMER2_A) {
  if (!pwm_duty) digitalWrite(PWM_PIN, LOW);    // заполнение ШИМ == 0
  else {
    if (!counter) digitalWrite(PWM_PIN, HIGH);  // при обнулении счётчика HIGH
    else if (counter == pwm_duty) digitalWrite(PWM_PIN, LOW); // при совпадении LOW
    if (++counter > PWM_DEPTH) counter = 0;     // проверка на переполнение
  }
}

Как известно, 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);
  }
}

Если не хватает количества стандартных ШИМ-выходов, можно поднять полуаппаратный ШИМ на таймере на несколько пинов сразу:

Многоканальный софт ШИМ
#define PWM_FREQ 100                        // частота ШИМ
const uint8_t pwm_pins[] = {2, 3, 4, 5};    // пины

#include <GyverTimers.h>              // подключаем библиотеку аппаратных таймеров
const uint8_t PWM_PINS = sizeof(pwm_pins);
volatile uint8_t pwm_duty[PWM_PINS];  // переменная для хранения заполнения шим 0-255
volatile uint8_t counter;             // счётчик

void setup() {
  Timer2.setFrequency(256L * PWM_FREQ); // прерывания по таймеру
  Timer2.enableISR();                   // включаем прерывания таймера 2

  // устанавливаем пины на выход
  for (int i = 0; i < PWM_PINS; i++) pinMode(pwm_pins[i], OUTPUT);

  // заполнение
  pwm_duty[0] = 10;
  pwm_duty[1] = 20;
  pwm_duty[2] = 128;
  pwm_duty[3] = 128;
}

void loop() {
}

// прерывание таймера
ISR(TIMER2_A) {
  for (int i = 0; i < PWM_PINS; i++) {
    if (!pwm_duty[i]) digitalWrite(pwm_pins[i], LOW); // заполнение ШИМ == 0
    else {
      if (!counter) digitalWrite(pwm_pins[i], HIGH);  // при обнулении счётчика HIGH
      else if (counter == pwm_duty[i]) digitalWrite(pwm_pins[i], LOW); // при совпадении LOW
    }
  }
  counter++;
}

Этот алгоритм является не самым оптимальным, более интересный можно посмотреть в GyverHacks.

Примечание: во всех трёх алгоритмах используется проверка совпадения со счётчиком counter == pwm_duty. Это сильно снижает использование процессорного времени в прерывании, но при резком уменьшении заполнения может приводить к одиночным “вспышкам” заполнения до максимума, так как условие не выполнится. Для более плавной работы можно сделать counter >= pwm_duty, тогда условие будет каждый раз “подстраиваться” под новое значение заполнения, но установка пина будет осуществляться на каждом тике!

Можно ввести буферизацию заполнения ШИМ и брать новое значение только при нулевом значении счётчика, это решит проблему:

ШИМ с буферизацией
// софт шим с буферизацией
#define PWM_FREQ 100    // частота ШИМ
#define PWM_PIN 13      // пин ШИМ

#include <GyverTimers.h>        // подключаем библиотеку аппаратных таймеров
volatile uint8_t pwm_duty = 0;  // переменная для хранения заполнения шим 0-255
volatile uint8_t counter = 0;   // аналог счетного регистра таймера
volatile uint8_t real_duty = 0;

void setup() {
  Timer2.setFrequency(256L * PWM_FREQ);  // частота прерываний
  Timer2.enableISR();                    // включаем прерывания таймера
  pinMode(PWM_PIN, OUTPUT);              // пин как выход
}

void loop() {
  // задаём скважность ШИМ потенциометром с пина А0
  pwm_duty = analogRead(0) / 4;   // 1023 -> 255
}

// прерывание таймера
ISR(TIMER2_A) {
  if (!pwm_duty) digitalWrite(PWM_PIN, LOW);  // заполнение ШИМ == 0
  else {
    if (!counter) {                 // при счётчике == 0
      digitalWrite(PWM_PIN, HIGH);  // HIGH
      real_duty = pwm_duty;         // берём из "буфера"
    } else if (counter == real_duty) digitalWrite(PWM_PIN, LOW); // при совпадении LOW
    counter++;
  }
}

Можно применить буферизацию и к остальным алгоритмам.

Библиотека 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 мкс).

Измеряем сигналы вручную


Квадратный сигнал, вручную

Квадратный сигнал можно “измерить” вот таким образом:

// измеряем время между импульсами на 2 пине
#define READ_PIN 2

void setup() {
  Serial.begin(9600); 
}

bool flag = false;
uint32_t tmr;
void loop() {
  bool state = digitalRead(READ_PIN);
  if (state && !flag) {   // фронт LOW-HIGH
    flag = true;
    tmr = micros();
  }
  if (!state && flag) {   // фронт HIGH-LOW
    flag = false;
    // выводим период
    Serial.println(micros() - tmr);
  }
}

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

Квадратный сигнал, вручную, оптимизировано

Считаем импульсы и иногда делаем расчёт:

// измеряем время между импульсами на 2 пине
// расчёт каждые 500мс
#define READ_PIN 2

void setup() {
  Serial.begin(9600);
}

bool flag = false;
uint32_t tmr;
int counter = 0;

void loop() {
  bool state =  digitalRead(READ_PIN);
  if (state && !flag) {   // фронт LOW-HIGH
    flag = true;
    counter++;
  }
  if (!state && flag) {   // фронт HIGH-LOW
    flag = false;
  }

  if (millis() - tmr >= 500) {
    long prd = (millis() - tmr) / counter;
    counter = 0;
    Serial.println(prd);  // в миллисекундах
    tmr = millis();
  }
}
Квадратный сигнал, на аппаратном прерывании
В предыдущих примерах мы вручную опрашиваем пин, что не позволит измерить высокую частоту или короткий импульс. Давайте подключим прерывания:

// измеряем время между импульсами на 2 пине
// расчёт каждые 500мс

void setup() {
  Serial.begin(9600);
  attachInterrupt(0, isr, RISING);  // прерывание на D2
}

uint32_t tmr;
volatile int counter = 0;

void isr() {
  counter++;  // импульсы считаем тут
}

void loop() {
  if (millis() - tmr >= 500) {
    long prd = (millis() - tmr) / counter;
    counter = 0;
    Serial.println(prd);  // в миллисекундах
    tmr = millis();
  }
}
Квадратный сигнал, считаем аппаратным таймером
Также импульсы можно считать  полностью аппаратно, при помощи тактового входа таймера. Такая возможность есть только у Таймера 1 (на Нано/Уно), такой таймер там один и его конечно жалко использовать для такой ерунды. Тактовый вход таймера на платах Уно/Нано находится на D5, то есть частоту подаём именно сюда:

// измеряем время между импульсами на D5 пине
// расчёт каждые 500мс

void setup() {
  Serial.begin(9600);
  // настройка таймера 1 на внешнее тактирование
  TCCR1A = 0;
  TCCR1B = 0b00000110;
}

uint32_t tmr;
void loop() {
  if (millis() - tmr >= 500) {
    long prd = (millis() - tmr) / TCNT1;
    prd = 1000 / prd;
    TCNT1 = 0;      // TCNT1 - счётчик таймера 1
    Serial.println(prd);  // в миллисекундах
    tmr = millis();
  }
}
Измерение заполнения ШИМ сигнала

Также рассмотрим измерение параметров ШИМ сигнала, например на прерываниях и micros():

// измеряем скважность ШИМ на 2 пине
// расчёт каждые 500мс

void setup() {
  Serial.begin(9600);
  attachInterrupt(0, isr, RISING); // прерывание на D2
}

volatile int pulse, period;
volatile bool state = false;
volatile uint32_t tmr;
uint32_t showTmr;

void isr() {
  if (!state) {
    state = true;
    attachInterrupt(0, isr, FALLING);
    period = micros() - tmr;  // период ШИМ
    tmr = micros();
  } else {
    state = false;
    attachInterrupt(0, isr, RISING);
    pulse = micros() - tmr; // импульс HIGH
  }
}

void loop() {
  if (millis() - showTmr >= 500) {
    // выводим скважность в процентах
    Serial.println(100.0 * pulse / period);
    showTmr = millis();
  }
}

Библиотека тахометра


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

Tachometer
// класс тахометра v1.1
// встроенный медианный фильтр
// вызывай tick() в прерывании по фронту
// забирай getRPM() частоту в оборотах в минуту
// забирай getHz() частоту в Герцах
// забирай getPeriod() период в мкс

#define _TACHO_TICKS_AMOUNT 10	  // количество тиков для счёта времени
#define _TACHO_TIMEOUT 1000000	  // таймаут прерываний (мкс), после которого считаем что вращение прекратилось

class Tacho {
  public:
    void tick() {   // tachoTime - время в мкс каждых _TACHO_TICKS_AMOUNT тиков
      if (!ticks--) {
        ticks = _TACHO_TICKS_AMOUNT - 1;
        tachoTime = micros() - tachoTimer;
        tachoTimer += tachoTime;  //== tachoTimer = micros();
        ready = true;
      }
    }

    uint16_t getRPM() {
      if (ready) {	// если готовы новые данные
        ready = false;
        if (tachoTime != 0) rpm = (uint32_t)_TACHO_TICKS_AMOUNT * 60000000 / median3(tachoTime);
      }
      if (micros() - tachoTimer > _TACHO_TIMEOUT) rpm = 0;
      return rpm;
    }

    float getHz() {
      if (ready) {  // если готовы новые данные
        ready = false;
        if (tachoTime != 0) hz = (float)_TACHO_TICKS_AMOUNT * 1000000 / median3(tachoTime);
      }
      if (micros() - tachoTimer > _TACHO_TIMEOUT) hz = 0;
      return hz;
    }

    uint32_t getPeriod() {
      return median3(tachoTime);
    }

  private:
    // быстрая медиана
    long median3(long value) {
      buf[counter] = value;
      if (++counter > 2) counter = 0;
      if ((buf[0] <= buf[1]) && (buf[0] <= buf[2])) return (buf[1] <= buf[2]) ? buf[1] : buf[2];
      else {
        if ((buf[1] <= buf[0]) && (buf[1] <= buf[2])) return (buf[0] <= buf[2]) ? buf[0] : buf[2];
        else return (buf[0] <= buf[1]) ? buf[0] : buf[1];
      }
    }

    volatile uint32_t tachoTime = 100000;   // для плавного старта значений
    volatile uint32_t tachoTimer = micros();
    volatile int ticks = 0;
    volatile bool ready = false;
    uint32_t buf[3] = {100000, 100000, 100000}; // для плавного старта значений
    byte counter = 0;
    int rpm = 0;
    float hz = 0.0;
};
Пользоваться этим тахометром очень просто: вызываем метод тик при срабатывании датчика оборотов, например во внешнем аппаратном или 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()):

Дамп сигнала
#define IN_PIN 2        // входной пин
#define LOG_SIZE 200    // размер лога (штук)
// массив измерений. Для импульсов короче 65мс можно сделать uint16_t
uint32_t us[LOG_SIZE];

// =========
uint32_t prevUs;
int count = 0;
bool first, prevS, flag = 0;

void setup() {
  Serial.begin(9600);
  delay(100);
  // запомнили начальное состояние
  first = prevS = digitalRead(IN_PIN);
}

void loop() {
  // читаем время и пин
  uint32_t now = micros();
  bool s = digitalRead(IN_PIN);

  if (prevS != s) {   // состояние пина изменилось
    prevS = s;        // запомнили
    if (!flag) {      // триггер (первое срабатывание)
      flag = 1;       // запомнили
      prevUs = now;   // время первого
    } else {                      // измеряем следующие
      us[count] = now - prevUs;   // время импульса
      prevUs = now;               // запомнили время
      count++;
      // буфер переполнен - вывод лога
      if (count >= LOG_SIZE) logOut();
    }
  }
  // таймаут 1 секунда (сигнал пропал) - вывод лога
  if (flag && now - prevUs > 1000000) logOut();
}

void logOut() {
  // выводим стартовое состояние пина
  Serial.print("fval: ");
  Serial.println(first);

  // и периоды
  for (int i = 0; i < count; i++) {
    Serial.print(us[i]);
    Serial.println(',');
  }
  for (;;);   // висим тут
}

Измерение начнётся по изменению сигнала и продлится до тех пор, пока не переполнится буфер или работа не завершится по таймауту (в примере выше 1 секунда с последнего импульса). После этого в монитор порта будет выведен начальный уровень сигнала fval и тайминги каждого следующего фронта/спада (изменения). Для примера я подключил ИК приёмник и нажал кнопку на пульте:

Точность измерения должна быть около 5 мкс на AVR, так как используется тяжёлое чтение пина и микрос. Лучше переписать на прерывания по CHANGE и завести отдельный таймер, тогда точность можно повысить в сотни раз. Код не привожу, так как для разных платформ он будет разный.

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

Воспроизводим запись
#define OUT_PIN 3   // пин для вывода

bool fval = 1;      // стартовый сигнал
// массив времён (лог предыдущего скетча)
uint32_t times[] = {
  9060,
  4488,
  632,
  520,
  // ....
};

void setup() {
  pinMode(OUT_PIN, OUTPUT);
  digitalWrite(OUT_PIN, fval);  // подали начальное

  // запускаем остальные
  for (int i = 0; i < sizeof(times) / 4; i++) {
    digitalWrite(OUT_PIN, fval = !fval);  // инверсия и вывод
    delayMicroseconds(times[i]);
  }
}

void loop() {
}

Для визуализации подключил дешёвый китайский логический анализатор (ссылка на али, ещё одна) на указанный пин:

Отлично! Прекрасно виден NEC протокол, его 4-х байтный пакет и код повтора.

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

Важные страницы


  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
Вперёд Фильтрация сигналов
Подписаться
Уведомить о
guest
4 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии