Вариатор Тойота Королла отзывы: устройство и принцип работы, основные виды, плюсы и минусы, эксплуатация

Вариатор Тойота Королла отзывы: устройство и принцип работы, основные виды, плюсы и минусы, эксплуатация Роботы

1описание метеодатчика bl999 и его информационного протокола

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

  • диапазон измеряемых температур: −40… 50°C;
  • диапазон измеряемой влажности: 1…99%;
  • период измерений: 30 сек;
  • рабочая радиочастота: 433,325 МГц;
  • число каналов: 3;
  • рабочее расстояние: до 30 м на открытых пространствах.

К одной метеостанции можно подключить до трёх таких датчиков. Номер (канал) датчика устанавливается переключателем, который расположен под съёмной крышкой батарейного отсека (трёхпозиционная кнопка SW1 на фото ниже). Фактически, канал здесь – это просто признак в структуре пакета данных датчика, никакого физического смысла (например, изменение рабочей частоты) он в себе не несёт.

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

Внешний вид и внутренности метеодатчика BL999
Внешний вид и внутренности метеодатчика BL999

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

Смотрите про коптеры:  Стоит ли реклама о роботах-пылесосах? Раскрытие их недостатков
Места подключения щупа осциллографа к метеодатчику BL999
Места подключения щупа осциллографа к метеодатчику BL999

Чтобы изучить генерируемый датчиком сигнал, нужен хороший осциллограф. Данные отправляются пакетами длительностью примерно 500…600 мс. Вот как выглядит типичный пакет с датчика BL999 на экране осциллографа.

Изучение сигнала метеодатчика BL999
Изучение сигнала метеодатчика BL999

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

Осциллограмма типичного пакета метеодатчика BL999
Осциллограмма типичного пакета метеодатчика BL999

Вот представлены 4 оцифрованных информационных пакета, сгенерированных датчиком. Эти пакеты пришли друг за другом с разницей в 30 секунд. Именно с такой периодичностью датчик BL999 отсылает свои данные.

Информационные пакеты метеодатчика BL999
Информационные пакеты метеодатчика BL999

Посмотрим на этот сигнал. С первого взгляда бросается в глаза, что:

  • данные передаются пакетами;
  • каждый пакет начинается с короткого импульса, за которым следует относительно длительный промежуток времени с нулевым уровнем;
  • в каждом пакете присутствует 4 группы импульсов, разделённых такими же длительными паузами;
  • в каждой группе содержатся импульсы, следующие друг за другом через короткие или вдвое более длинные паузы;
  • всего имеются 3 вида промежутков между импульсами: самые короткие (условно назовём их типа A), вдвое более длинные (B) и вчетверо более длинные (C);
  • в каждой группе ровно по 37 импульсов;
  • все 4 группы каждого пакета одинаковые (содержат повторяющиеся последовательности импульсов).

Очевидно, что в данном случае применяется некое временное кодирование (скорее всего, фазо-импульсное или частотно-импульсное), когда значимая информация скрыта в длительности пауз между импульсами. В случае датчика BL999 короткая пауза между соседними импульсами (A) означает логический нуль, а длинная (B) – логическую единицу. Изучим сигнал более детально.

Как видно, в сигнале присутствует ряд коротких импульсов. Длительность всех импульсов одинакова и равна примерно 486 мкс. Длительность коротких промежутков (логический “0”) равна примерно 2,4 мс, длительность средних промежутков (логическая “1”) равна примерно 4,5 мс. Продолжительность самых длинных промежутков – около 9,4 мс.

Как уже было упомянуто, в пакете присутствуют 4 группы по 37 импульсов. Этими импульсами закодированы 36 битов, которые можно условно разбить на участки по 9 полубайтов. Следующий рисунок показывает, что закодировано в этих 36-ти битах:

Назначение битов информационного пакета метеодатчика BL999 в одной группе
Назначение битов информационного пакета метеодатчика BL999 в одной группе

Полубайт также называют «ниббл» (англ. nibble) или тетрада. Это единица измерения информации, содержащая четыре бита.

Давайте разберём реальный пример, и на его основе расшифруем закодированные в нём данные. Возьмём одну группу из 36-ти битов из вот такого пакета, пришедшего от датчика BL999:

Пример информационного пакета метеодатчика BL999
Пример информационного пакета метеодатчика BL999

В пакете, согласно схеме, присутствуют следующие части:

ОбозначениеНомера битовОписаниеЗначение из примера
ID35…32, 29…28Это идентификатор датчика. Он задаётся произвольным образом и изменяется при каждом включении.0101_11 = 23
Chan31…30Номер канала датчика. Кодируется обычным двоичным кодом: “01” – 1, “10” – 2, “11” – 3.01 = 1ый канал
Bat27Уровень заряда батареи: “0” – норма, “1” – низкий заряд.0 = норма
?26…24Нет данных.100
Temperature23…12Данные температуры. Число записано в обратном порядке и умножено на 10. Отрицательные температуры, кроме этого, хранятся в дополнительном коде (*).0111_1111_0000 обращение 0000_1111_1110 = 254 деление на 10 25,4°C
Humidity11…4Влажность. Записывается как результат вычитания из 100, в дополнительном коде (*).0000_1101 обращение 1011_0000 инверсия битов 0100_1111=79 1 =80 вычитание из 100% 100 − 80 = 20%
Checksum3…0Контрольная сумма. Вычисляется как сумма 8-ми полубайтов, записанных в обратном порядке. От получившегося числа берутся 4 младших разряда и также записываются в обратном порядке.0101 0111 0100 0111 1111 0000 0000 1101 0100 1010 1110 0010 1110 1111 0 0 1011 = 100_0010 обращаем 0010 0100

(*) Дополнительный код числа – это специальный вид представления чисел, который часто используется в вычислительной технике. Онлайн-калькулятор и хорошая статья на эту тему здесь.

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

2скетч ntp сервера для arduino

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

Общий алгоритм следующий. Сначала будем опрашивать приёмник спутникового сигнала, пока не получим от него NMEA пакет с корректным значением времени. Нужный нам пакет с временем начинается с заголовка “$GPRMC”.

Формат пакета NMEA с данными о времени
Формат пакета NMEA с данными о времени

Когда получим значение времени, запишем его в модуль RTC. Подробно работу с часами реального времени мы рассматривали здесь.

Далее запустим сервер и в цикле будем постоянно слушать входящие запросы по протоколу UDP на порту 123 (это стандартный порт протокола NTP). Как только сервер получит NTP запрос, прочитаем время из модуля часов реального времени, «упакуем» в ответный NTP пакет и отправим клиенту, который запросил время.

В конце статьи приложена программа для тестирования связи с NTP сервером.

Скетч сервера времени NTP и Arduino (разворачивается)
#define debug true // для вывода отладочных сообщений

#include <SoftwareSerial.h>
#include <Wire.h>
#include <Ethernet.h>
#include <EthernetUdp.h>

SoftwareSerial Serial1(10, 11);
EthernetUDP Udp;

// MAC, IP-адрес и порт NTP сервера:
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // задайте свой MAC
IPAddress ip(192, 168, 0, 147); // задайте свой IP
#define NTP_PORT 123 // стандартный порт, не менять

#define RTC_ADDR 0x68 // i2c адрес RTC

static const int NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];

int year;
byte month, day, hour, minute, second, hundredths;
unsigned long date, time, age;
uint32_t timestamp, tempval;

void setup() {
  Wire.begin(); // стартуем I2C
  
#if debug
  Serial.begin(115200);
#endif
  Serial1.begin(4800); // старт UART для GPS модуля

  getGpsTime(); // получаем время GPS
  writeRtc(); // записываем время в RTC

  // запускаем Ethernet шилд в режиме UDP:
  Ethernet.begin(mac, ip);
  Udp.begin(NTP_PORT);
#if debug
  Serial.println("NTP started");
#endif  
}

void loop() {
  processNTP(); // обрабатываем приходящие NTP запросы
}

String serStr; // строка для хранения пакетов от GPS приёмника

// Читает пакеты GPS приёмника из COM-порта и пытается найти в них время
// Если время найдено, возвращает True, иначе - False
void getGpsTime() {
  bool timeFound = false;
  while (!timeFound) {
    while (Serial1.available()>0) {
    char c = Serial1.read();
      if (c != 'n') {
        serStr.concat(c);
      } else {
        timeFound = decodeTime(serStr);
        serStr = "";
      }
    }
  }
}

// Декодирует вермя по NMEA пакету 
// и возвращает True в случае успеха и False в обратном случае
bool decodeTime(String s) {
#if debug
    Serial.println("NMEA Packet = "   s);
#endif
  if (s.substring(0,6)=="$GPRMC") {
    String validFlag = s.substring(18,20);
    // Ждём валидные данные (флаг "V" - данные не валидны, "A" - данные валидны):
    if (validFlag == "A") {
      String timeStr = s.substring(7,17); // строка времени в формате ччммсс.сс
      hour = timeStr.substring(0,2).toInt();
      minute = timeStr.substring(2,4).toInt();
      second = timeStr.substring(4,6).toInt();
      hundredths = timeStr.substring(7,10).toInt();

      // ищем индекс 4-ой запятой с конца, после которой идёт дата
      int commaIndex = 1;
      for (int i=0;i<5;i  ) {
        commaIndex = s.lastIndexOf(",", commaIndex-1);
      }
      String date = s.substring(commaIndex 1, commaIndex 7); // строка даты в формате ддммгг
      day = date.substring(0,2).toInt();
      month = date.substring(2,4).toInt();
      year = date.substring(4,6).toInt(); // передаются только десятки и единицы года
#if debug
    printDate();
#endif
      return true;
    }
  }
  return false;
}

// Запоминает время в RTC
void writeRtc() {
  byte arr[] = {0x00, dec2hex(second), dec2hex(minute), dec2hex(hour), 0x01, dec2hex(day), dec2hex(month), dec2hex(year)};
  Wire.beginTransmission(RTC_ADDR);
  Wire.write(arr, 8);
  Wire.endTransmission();
#if debug
  Serial.print("Set date: ");
  printDate();
#endif
}

// Преобразует число из dec представления в hex представление
byte dec2hex(byte b) {
  String bs = (String)b;
  byte res;
  if (bs.length()==2) {
    res = String(bs.charAt(0)).toInt() * 16   String(bs.charAt(1)).toInt();
  } else {
    res = String(bs.charAt(0)).toInt();
  }
#if debug
  Serial.println("dec "   (String)b   " = hex "   (String)res);
#endif  
  return res;
}

// Читает из RTC время и дату
void getRtcDate() {
  Wire.beginTransmission(RTC_ADDR);
  Wire.write(byte(0));
  Wire.endTransmission();
  
  Wire.beginTransmission(RTC_ADDR);
  Wire.requestFrom(RTC_ADDR, 7);
  byte t[7];
  int i = 0;
  while(Wire.available()) {
    t[i] = Wire.read();
    i  ;
  }
  Wire.endTransmission();
  second = t[0];
  minute = t[1];
  hour = t[2];
  day = t[4];
  month = t[5];
  year = t[6];
#if debug
  Serial.print("Get date: ");
  printDate();
#endif
}

// Обрабатывает запросы к NTP серверу
void processNTP() {
  int packetSize = Udp.parsePacket();
  if (packetSize) {
    Udp.read(packetBuffer, NTP_PACKET_SIZE);
    IPAddress remote = Udp.remoteIP();
    int portNum = Udp.remotePort();

#if debug
    Serial.println();
    Serial.print("Received UDP packet size ");
    Serial.println(packetSize);
    Serial.print("From ");

    for (int i=0; i<4; i  ) {
      Serial.print(remote[i], DEC);
      if (i<3) { Serial.print("."); }
    }
    Serial.print(", port ");
    Serial.print(portNum);

    byte LIVNMODE = packetBuffer[0];
    Serial.print("  LI, Vers, Mode :");
    Serial.print(packetBuffer[0], HEX);

    byte STRATUM = packetBuffer[1];
    Serial.print("  Stratum :");
    Serial.print(packetBuffer[1], HEX);

    byte POLLING = packetBuffer[2];
    Serial.print("  Polling :");
    Serial.print(packetBuffer[2], HEX);

    byte PRECISION = packetBuffer[3];
    Serial.print("  Precision :");
    Serial.println(packetBuffer[3], HEX);

    for (int z=0; z<NTP_PACKET_SIZE; z  ) {
      Serial.print(packetBuffer[z], HEX);
      if (((z 1) % 4) == 0) {
        Serial.println();
      }
    }
    Serial.println();
#endif

    // Упаковываем данные в ответный пакет:
    packetBuffer[0] = 0b00100100;   // версия, режим
    packetBuffer[1] = 1;   // стратум
    packetBuffer[2] = 6;   // интервал опроса
    packetBuffer[3] = 0xFA; // точность

    packetBuffer[7] = 0; // задержка
    packetBuffer[8] = 0;
    packetBuffer[9] = 8;
    packetBuffer[10] = 0;

    packetBuffer[11] = 0; // дисперсия
    packetBuffer[12] = 0;
    packetBuffer[13] = 0xC;
    packetBuffer[14] = 0;
      
    getRtcDate();
    timestamp = numberOfSecondsSince1900Epoch(year,month,day,hour,minute,second);

#if debug
    Serial.println("Timestamp = "   (String)timestamp);
#endif

    tempval = timestamp;

    packetBuffer[12] = 71; //"G";
    packetBuffer[13] = 80; //"P";
    packetBuffer[14] = 83; //"S";
    packetBuffer[15] = 0; //"0";

    // Относительное время 
    packetBuffer[16] = (tempval >> 24) & 0xFF;
    tempval = timestamp;
    packetBuffer[17] = (tempval >> 16) & 0xFF;
    tempval = timestamp;
    packetBuffer[18] = (tempval >> 8) & 0xFF;
    tempval = timestamp;
    packetBuffer[19] = (tempval) & 0xFF;

    packetBuffer[20] = 0;
    packetBuffer[21] = 0;
    packetBuffer[22] = 0;
    packetBuffer[23] = 0;

    // Копируем метку времени клиента 
    packetBuffer[24] = packetBuffer[40];
    packetBuffer[25] = packetBuffer[41];
    packetBuffer[26] = packetBuffer[42];
    packetBuffer[27] = packetBuffer[43];
    packetBuffer[28] = packetBuffer[44];
    packetBuffer[29] = packetBuffer[45];
    packetBuffer[30] = packetBuffer[46];
    packetBuffer[31] = packetBuffer[47];

    // Метка времени 
    packetBuffer[32] = (tempval >> 24) & 0xFF;
    tempval = timestamp;
    packetBuffer[33] = (tempval >> 16) & 0xFF;
    tempval = timestamp;
    packetBuffer[34] = (tempval >> 8) & 0xFF;
    tempval = timestamp;
    packetBuffer[35] = (tempval) & 0xFF;

    packetBuffer[36] = 0;
    packetBuffer[37] = 0;
    packetBuffer[38] = 0;
    packetBuffer[39] = 0;

    // Записываем метку времени 
    packetBuffer[40] = (tempval >> 24) & 0xFF;
    tempval = timestamp;
    packetBuffer[41] = (tempval >> 16) & 0xFF;
    tempval = timestamp;
    packetBuffer[42] = (tempval >> 8) & 0xFF;
    tempval = timestamp;
    packetBuffer[43] = (tempval) & 0xFF;

    packetBuffer[44] = 0;
    packetBuffer[45] = 0;
    packetBuffer[46] = 0;
    packetBuffer[47] = 0;

    // Отправляем NTP ответ 
    Udp.beginPacket(remote, portNum);
    Udp.write(packetBuffer, NTP_PACKET_SIZE);
    Udp.endPacket();
  }
}

// Выводит отформатированноую дату
void printDate() {
  char sz[32];
  sprintf(sz, "d.d.d d:d:d.d", day, month, year 2000, hour, minute, second, hundredths);
  Serial.println(sz);
}

const uint8_t daysInMonth [] PROGMEM = { 31,28,31,30,31,30,31,31,30,31,30,31 }; // число дней в месяцах
const unsigned long seventyYears = 2208988800UL; // перевод времени unix в эпоху

// Формирует метку времени от момента 01.01.1900
static unsigned long int numberOfSecondsSince1900Epoch(uint16_t y, uint8_t m, uint8_t d, uint8_t h, uint8_t mm, uint8_t s) {
  if (y >= 1970) { y -= 1970; }
  uint16_t days = d;
  for (uint8_t i=1; i<m;   i) {
    days  = pgm_read_byte(daysInMonth   i - 1);
  }
  if (m>2 && y%4 == 0) {   days; }
  days  = 365 * y   (y   3) / 4 - 1;
  return days*24L*3600L   h*3600L   mm*60L   s   seventyYears;
}

Функция getGpsTime() постоянно читает приходящие от ГНСС приёмника пакеты, и когда получает очередной пакет, проверяет, нет ли в нём валидных данных времени. Если время есть, то происходит его разбор. Также время можно сохранить в модуле RTC и таким образом проводить периодическую синхронизацию.

Проверка NMEA пакетов осуществляется в функции decodeTime().

Несколько слов о функции dec2hex(). В ней несколько извращённо число переводится из десятичного представления в 16-ное. Точнее, так. Модуль часов показывает время в виде, например, 16:52:08. Но здесь каждое из этих чисел не десятичное, а 16-ное.

То есть, в действительности это время в RTC хранится так: 0x16:0x52:0x08. А с GPS-приёмника мы получаем время в десятичном формате. И чтобы записать те же 16 часов в модуль RTC, нужно преобразовать десятичное 16 в шестнадцатеричное 0x16, что является десятичным 22.

3подключение к arduino модуля с часами реального времени ds1302

Модуль DS1302 может выглядеть, например, так:

Модуль часов реального времени DS1302
Модуль часов реального времени DS1302

На нижней стороне модуля никаких компонентов нет. Как видно, вся «обвязка» микросхемы DS1302 – это кварцевый резонатор.

Нижняя сторона модуля часов реального времени DS1302
Нижняя сторона модуля часов реального времени DS1302

Назначение выводов микросхемы DS1302 такое (слева в DIP-корпусе, справа – в планарном):

Выводы микросхемы DS1302
Выводы микросхемы DS1302
Назначение выводов RTC DS1302
Название вывода DS1302Назначение
X1, X2Входы для подачи частоты 32,768 кГц с кварцевого резонатора.
SCLKВход тактовой частоты последовательных данных.
I/OВход/выход последовательных данных.
CEВход выбора чипа. Активируется высоким уровнем.
VCC1Дополнительное резервное питание (например, от батареи) для сохранения настроек времени в ПЗУ, 3 В.
VCC2Первичное питание микросхемы, 5 В.
GNDЗемля

Соответствие выводов микросхемы DS1302 выводам модуля, думаю, очевидно: VCC – это первичное питание 5 В, GND – земля. CLK – вход тактовых импульсов. DAT – ввод/вывод последовательных данных. RST – это CE, который включает логику и показывает микросхеме RTC, что происходит обмен данными (чтение или запись).

Типичная схема подключения RTC микросхемы DS1302:

Типичная схема подключения микросхемы DS1302
Типичная схема подключения микросхемы DS1302

Самый простой способ управлять DS1302 – это, конечно же, воспользоваться одной из множества готовых библиотек для Arduino, например, этой (она приложена также архивом внизу статьи). Она позволяет выставлять время и считывать его, а также записывать и читать данные из ПЗУ часов.

Подключение DS1302 к Arduino
Подключение DS1302 к Arduino

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

Для обмена с микросхемой DS1302 используется последовательный интерфейс, похожий на SPI. Диаграмма передачи данных показана ниже. Видно, что во время чтения или записи данных сначала следует выставить логическую “1” на линии CE. Затем сгенерировать 16 тактовых синхронизирующих импульсов. В это время передаются 16 бит информации.

Диаграмма чтения и записи данных DS1302
Диаграмма чтения и записи данных DS1302

В первых 8-ми битах передаётся команда (командный байт), а следующие 8 бит – данные. Структура командного байта показана ниже. В нём старший бит всегда “1”, младший – признак операции (чтение RD=1 или запись WR=0), а остальные биты – это адрес регистра, с которым взаимодействуем.

Структура командного байта DS1302
Структура командного байта DS1302

Кроме того, DS1302 поддерживает множественную передачу (burst mode). Для этого следует удерживать высокий уровень на линии CE и генерировать необходимое число тактовых импульсов. Данные будут читаться (или записываться) из регистров или ПЗУ последовательно, начиная с заданного адреса и далее.

Карта регистров часов реального времени DS1302
Карта регистров часов реального времени DS1302

Предлагаю для изучения DS1302 воспользоваться отладочной платой с микросхемой FT2232H и программы SPI via FTDI. Это позволит избежать постоянного программирования Arduino и проводить все эксперименты с часами «на лету».

Единственная сложность в том, что микросхема FT2232H использует 3.3-вольтовую логику, а часы DS1302 – 5-вольтовую. Но ничего страшного, воспользуемся преобразователем логического уровня, благо стоит он копейки, и в применении исключительно прост.

У него есть две стороны: одна отвечает за низковольтовую часть (LV), другая – за высоковольтную (HV). У него есть 4 низковольтных входа-выхода (LV1…LV4) и соответствующие им 4 высоковольтных входа-выхода (HV1…HV4).

Схема подключения FT2232H к DS1302 через логик шифтер
Схема подключения FT2232H к DS1302 через логик шифтер

К высоковольтной стороне преобразователя подключается модуль DS1302, к низковольтной – микросхема FT2232H. Соответствие выводов такое: CLK – ADBUS0, DAT – ADBUS1 и ADBUS2, RST – ADBUS3. Подключаем соответственно через преобразователь напряжения. Вот так это выглядит вживую:

Управление часами DS1302 с помощью FT2232H
Управление часами DS1302 с помощью FT2232H

Когда собрали схему, запустим программу SPI via FTDI и в меню «Устройство» выберем интерфейс SPI, потом нажмём «Подключить». Теперь в левой части главного окна, в рамке «Настройки SPI» снимем галочки с CS active LOW (активация часов DS1302 высоким уровнем, вывод CE) и MSB first (передача байта старшим битом вперёд). Остальные параметры оставим как есть.

Теперь попробуем прочитать 1 байт из регистра секунд 0x81. Он должен меняться каждую секунду, и мы сразу увидим, что наша схема работает. Для чтения регистра секунд настройки программы будут такие (обратите внимание на раздел «Чтение»):

Чтение 1 байта из регистра секунд 0x81 часов DS1302
Чтение 1 байта из регистра секунд 0x81 часов DS1302

Чтобы увидеть принятые данные, нужно нажать на кнопку с изображением таблицы слева от кнопки «Прочитать».

Чтобы прочитать данные всех регистров, нужно отправить команду BF и запросить столько регистров, сколько нужно. Все данные о дате хранятся в 7-ми регистрах, а восьмой – данные о запрете записи (WP, write protect).

Чтение всех регистров часов DS1302 в режиме множественной передачи
Чтение всех регистров часов DS1302 в режиме множественной передачи (burst mode)

Кстати, если вместо числа “1” ввести число раз “0” (справа от кнопки чтения), то программа будет постоянно опрашивать часы DS1302, и вы увидите в таблице принятых данных как идёт время часов DS1302.

Для записи данных в ПЗУ часов DS1302 в режиме множественной передачи (не по одному байту) следует отправить команду FE и дальше нужные данные. Для чтения данных из ПЗУ в режиме множественной передачи нужно отправить команду FF:

Чтение ПЗУ часов DS1302 в режиме множественной передачи
Чтение ПЗУ часов DS1302 в режиме множественной передачи (burst mode)

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

4подключение к arduino модуля с инфракрасным приёмником

ИК датчик может состоять из одного только инфракрасного приёмника, как в этом случае:

ИК приёмник
ИК приёмник

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

К Arduino модуль с ИК приёмником подключается тоже очень просто:

Пин модуляПин ArduinoНазначение
DATЛюбой цифровойПризнак наличия ИК излучения на входе приёмника
VCC 5VПитание
GNDGNDЗемля
Подключение ИК приёмника к Arduino
Подключение ИК приёмника к Arduino

Напишем скетч, в котором будем просто показывать с помощью встроенного светодиода, что на входе приёмника присутствует ИК излучение. В данном модуле аналогично с ранее рассмотренным на выходе DAT уровень “0”, когда ИК излучение попадает на приёмник, и “1” когда ИК излучения нет.

const int ir = 2;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // это 13-ый вывод Arduino со встроенным светодиодом
  pinMode(ir, INPUT);
}

void loop() {
  int r = digitalRead(ir);
  digitalWrite(LED_BUILTIN, r!=HIGH); // зажигаем светодиод, если модуль среагировал на ИК излучение
  // в противном случае - гасим
}

Если загрузить этот скетч в Arduino, направить на ИК приёмник ИК пульт и нажимать на нём разные кнопки, то мы увидим, что светодиод нашего индикатора быстро мигает. Разные кнопки – по-разному мигает.

Чтение команд ИК пульта с Arduino
Чтение команд ИК пульта с Arduino

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

Осциллограф отображает часть команды ИК пульта
Осциллограф отображает часть команды ИК пульта

На осциллограмме видна серия «пачек» импульсов примерно одинаковой длительности. Каждая «пачка» состоит из 24-х импульсов.

Осциллограф отображает часть команды ИК пульта
Осциллограф отображает часть команды ИК пульта

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

Подключение выхода с ИК приёмника и выхода ИК пульта к осциллографу
Подключение выхода с ИК приёмника и выхода ИК пульта к осциллографу

Вот так выглядит посылка пульта целиком. Здесь жёлтая линия – аналоговый сигнал пульта ДУ, голубая – цифровой сигнал с выхода ИК приёмника. Видно, что продолжительность передачи составляет примерно 120 мс. Очевидно, время будет несколько варьироваться исходя из того, какие биты присутствуют в пакете.

Осциллограмма пакета с ИК пульта ДУ
Осциллограмма пакета с ИК пульта ДУ

При большем приближении видно, что высокочастотное заполнение, которое имеется в аналоговом сигнале, в цифровом сигнале с ИК приёмника отсутствует. Приёмник прекрасно справляется со своей задачей и показывает чистый цифровой сигнал. Видна последовательность коротких и длинных прямоугольных импульсов. Длительность коротких импульсов примерно 1,2 мс, длинных – в 2 раза больше.

Биты пакета ИК пульта, масштаб: 1 клетка – 200 мкс
Биты пакета ИК пульта, масштаб: 1 клетка – 200 мкс

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

Если зарисовать этот пакет, то получится как-то так:

Один из пакетов ИК пульта
Один из пакетов ИК пульта

Дальнейшие исследования показали, что все пакеты данного пульта ДУ состоят из двух пачек импульсов. Причём первая всегда содержит 35 бит, вторая – 32.

Есть несколько вариантов, как поступить для получения цифровых данных пакета:

  1. опрашивать пакет через равные промежутки времени (т.н. «стробирование»), а затем принимать решение, это логический “0” или “1”;
  2. ловить фронты импульсов (детектор фронта), затем определять их длительность и также принимать решение, какой это бит.

Напомню, что будем считать короткие импульсы логическим нулём, длинные – логической единицей.

Для реализации первого варианта понятно, с какой частотой необходимо опрашивать ИК датчик, чтобы принимать с него корректные данные: 600 мкс. Это время в два раза меньшее, чем длительность коротких импульсов сигнала (логических нулей). Или, если рассматривать с точки зрения частоты, опрашивать приёмник нужно в 2 раза большей частотой (вспомним Найквиста и Котельникова). Напишем скетч, реализующий вариант со стробированием.

Скетч для чтения пакета от ИК пульта методом стробирования
const int ir = 2; // с выхода ИК приёмника
int t = 600; // период стробирования, мкс

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(ir, INPUT);
}

void loop() {
  int r = digitalRead(ir); // читаем значение ИК сенсора
  digitalWrite(LED_BUILTIN, r!=HIGH); // зажигаем светодиод, если сенсор сработал
  // Если зафиксировали ИК излучение, обрабатываем команду с пульта:
  if (r==LOW) {
    precess_ir(); 
  }
}

// читает пакет ИК пульта
void precess_ir(){
  delay(13); // пропустим стандартное начало пакета
  byte bits[100]; // 100 бит должно хватить
  // читаем пакет
  for (int i=0; i<100; i  ){
    int bit = readBit();
    bits[i] = bit;    
  }
  // выводим пакет в монитор;
  for (int i=0; i<100; i  ){
    Serial.print(bits[i]);
  }
  Serial.println();
}

// читает 1 бит пакета
int readBit() {
  // дожидаемся уровня HIGH и ставим первый строб
  int r1;
  do { 
    r1 = digitalRead(ir);
  } while (r1 != HIGH);
  delayMicroseconds(t); // ждём

  // затем ставим второй строб
  int r2 = digitalRead(ir);
  delayMicroseconds(t);  // ждём
  if (r2 == LOW) {
    return 0;
  }
  else {
    // третий строб 
    delayMicroseconds(t);  // ждём
    return 1;
  }
}

Поэкспериментируем с данным скетчем и ИК приёмником. Загрузим скетч в память Ардуино. Запустим последовательный монитор. Нажмём на пульте несколько раз одну и ту же кнопку и посмотрим, что мы увидим в мониторе.

Выводим принятые пакеты ИК пульта в последовательный монитор
Выводим принятые пакеты ИК пульта в последовательный монитор

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

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

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

Скетч для чтения пакета от ИК пульта методом детекции фронтов
const int ir = 2;
int t_low = 600 10; // длительность "0" (с запасом), мкс
int t_max = t_low * 4; // таймаут, мкс

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(ir, INPUT);
}

void loop() {
  int r = digitalRead(ir);
  digitalWrite(LED_BUILTIN, r!=HIGH);
  // если зафиксировали ИК излучение, обрабатываем команду пульта
  if (r==LOW) {
    precess_ir();
  }
}

// читает пакет ИК пульта
void precess_ir() {
  delay(13); // пропустим стандартное начало пакета
  byte bits[100];
  for (int i=0; i<100; i  ){
    int bit = readBit();
    bits[i] = bit;
  }
  for (int i=0; i<100; i  ){
    Serial.print(bits[i]);
  }
  Serial.println();
}

// читает 1 бит пакета
int readBit() {
  int r1;
  do {
    r1 = digitalRead(ir);
  } while (r1 != HIGH); // ждём передний фронт импульса
  int t1 = micros(); // запоминаем время начала импульса

  int t2;
  int t; 
  do {
    r1 = digitalRead(ir);
    t2 = micros(); // запоминаем время опроса (оно же длительность импульса)
    t = t2 - t1; // длительность импульса
  } while ((r1 != LOW) && (t < t_max)); // ждём задний фронт импульса, но не больше таймаута
  //Serial.println(t); // можно вывести длительность импульса
  
  if (t < t_low) {
    return 0;
  }
  else {
    return 1;
  }
}

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

Загрузим скетч, запустим монитор, нажмём несколько раз ту же кнопку пульта.

Выводим принятые пакеты ИК пульта в последовательный монитор
Выводим принятые пакеты ИК пульта в последовательный монитор

Результат, как видно, более стабильный.

Filtering

Because the raw data contains a lot of noise we use certain filters on the output of the sensors to convert them to Quaternions (Madgwick/Mahony/Kalman):

void MadgwickQuaternionUpdate(float ax, float ay, float az, float gx, float gy, float gz, float mx, float my, float mz)
{
float q1 = q[0], q2 = q[1], q3 = q[2], q4 = q[3]; // short name local variable for readability
float norm;
float hx, hy, _2bx, _2bz;
float s1, s2, s3, s4;
float qDot1, qDot2, qDot3, qDot4;
// Auxiliary variables to avoid repeated arithmetic
float _2q1mx;
float _2q1my;
float _2q1mz;
float _2q2mx;
float _4bx;
float _4bz;
float _2q1 = 2.0f * q1;
float _2q2 = 2.0f * q2;
float _2q3 = 2.0f * q3;
float _2q4 = 2.0f * q4;
float _2q1q3 = 2.0f * q1 * q3;
float _2q3q4 = 2.0f * q3 * q4;
float q1q1 = q1 * q1;
float q1q2 = q1 * q2;
float q1q3 = q1 * q3;
float q1q4 = q1 * q4;
float q2q2 = q2 * q2;
float q2q3 = q2 * q3;
float q2q4 = q2 * q4;
float q3q3 = q3 * q3;
float q3q4 = q3 * q4;
float q4q4 = q4 * q4;
// Normalise accelerometer measurement
norm = sqrt(ax * ax ay * ay az * az);
if (norm == 0.0f) return; // handle NaN
norm = 1.0f/norm;
ax *= norm;
ay *= norm;
az *= norm;
// Normalise magnetometer measurement
norm = sqrt(mx * mx my * my mz * mz);
if (norm == 0.0f) return; // handle NaN
norm = 1.0f/norm;
mx *= norm;
my *= norm;
mz *= norm;
// Reference direction of Earth’s magnetic field
_2q1mx = 2.0f * q1 * mx;
_2q1my = 2.0f * q1 * my;
_2q1mz = 2.0f * q1 * mz;
_2q2mx = 2.0f * q2 * mx;
hx = mx * q1q1 – _2q1my * q4 _2q1mz * q3 mx * q2q2 _2q2 * my * q3 _2q2 * mz * q4 – mx * q3q3 – mx * q4q4;
hy = _2q1mx * q4 my * q1q1 – _2q1mz * q2 _2q2mx * q3 – my * q2q2 my * q3q3 _2q3 * mz * q4 – my * q4q4;
_2bx = sqrt(hx * hx hy * hy);
_2bz = -_2q1mx * q3 _2q1my * q2 mz * q1q1 _2q2mx * q4 – mz * q2q2 _2q3 * my * q4 – mz * q3q3 mz * q4q4;
_4bx = 2.0f * _2bx;
_4bz = 2.0f * _2bz;
// Gradient decent algorithm corrective step
s1 = -_2q3 * (2.0f * q2q4 – _2q1q3 – ax) _2q2 * (2.0f * q1q2 _2q3q4 – ay) – _2bz * q3 * (_2bx * (0.5f – q3q3 – q4q4) _2bz * (q2q4 – q1q3) – mx) (-_2bx * q4 _2bz * q2) * (_2bx * (q2q3 – q1q4) _2bz * (q1q2 q3q4) – my) _2bx * q3 * (_2bx * (q1q3 q2q4) _2bz * (0.5f – q2q2 – q3q3) – mz);
s2 = _2q4 * (2.0f * q2q4 – _2q1q3 – ax) _2q1 * (2.0f * q1q2 _2q3q4 – ay) – 4.0f * q2 * (1.0f – 2.0f * q2q2 – 2.0f * q3q3 – az) _2bz * q4 * (_2bx * (0.5f – q3q3 – q4q4) _2bz * (q2q4 – q1q3) – mx) (_2bx * q3 _2bz * q1) * (_2bx * (q2q3 – q1q4) _2bz * (q1q2 q3q4) – my) (_2bx * q4 – _4bz * q2) * (_2bx * (q1q3 q2q4) _2bz * (0.5f – q2q2 – q3q3) – mz);
s3 = -_2q1 * (2.0f * q2q4 – _2q1q3 – ax) _2q4 * (2.0f * q1q2 _2q3q4 – ay) – 4.0f * q3 * (1.0f – 2.0f * q2q2 – 2.0f * q3q3 – az) (-_4bx * q3 – _2bz * q1) * (_2bx * (0.5f – q3q3 – q4q4) _2bz * (q2q4 – q1q3) – mx) (_2bx * q2 _2bz * q4) * (_2bx * (q2q3 – q1q4) _2bz * (q1q2 q3q4) – my) (_2bx * q1 – _4bz * q3) * (_2bx * (q1q3 q2q4) _2bz * (0.5f – q2q2 – q3q3) – mz);
s4 = _2q2 * (2.0f * q2q4 – _2q1q3 – ax) _2q3 * (2.0f * q1q2 _2q3q4 – ay) (-_4bx * q4 _2bz * q2) * (_2bx * (0.5f – q3q3 – q4q4) _2bz * (q2q4 – q1q3) – mx) (-_2bx * q1 _2bz * q3) * (_2bx * (q2q3 – q1q4) _2bz * (q1q2 q3q4) – my) _2bx * q2 * (_2bx * (q1q3 q2q4) _2bz * (0.5f – q2q2 – q3q3) – mz);
norm = sqrt(s1 * s1 s2 * s2 s3 * s3 s4 * s4); // normalise step magnitude
norm = 1.0f/norm;
s1 *= norm;
s2 *= norm;
s3 *= norm;
s4 *= norm;
// Compute rate of change of quaternion
qDot1 = 0.5f * (-q2 * gx – q3 * gy – q4 * gz) – beta * s1;
qDot2 = 0.5f * (q1 * gx q3 * gz – q4 * gy) – beta * s2;
qDot3 = 0.5f * (q1 * gy – q2 * gz q4 * gx) – beta * s3;
qDot4 = 0.5f * (q1 * gz q2 * gy – q3 * gx) – beta * s4;
// Integrate to yield quaternion
q1 = qDot1 * deltat;
q2 = qDot2 * deltat;
q3 = qDot3 * deltat;
q4 = qDot4 * deltat;
norm = sqrt(q1 * q1 q2 * q2 q3 * q3 q4 * q4); // normalise quaternion
norm = 1.0f/norm;
q[0] = q1 * norm;
q[1] = q2 * norm;
q[2] = q3 * norm;
q[3] = q4 * norm;
}
// Similar to Madgwick scheme but uses proportional and integral filtering on the error between estimated reference vectors and
// measured ones.
void MahonyQuaternionUpdate(float ax, float ay, float az, float gx, float gy, float gz, float mx, float my, float mz)
{
float q1 = q[0], q2 = q[1], q3 = q[2], q4 = q[3]; // short name local variable for readability
float norm;
float hx, hy, bx, bz;
float vx, vy, vz, wx, wy, wz;
float ex, ey, ez;
float pa, pb, pc;
// Auxiliary variables to avoid repeated arithmetic
float q1q1 = q1 * q1;
float q1q2 = q1 * q2;
float q1q3 = q1 * q3;
float q1q4 = q1 * q4;
float q2q2 = q2 * q2;
float q2q3 = q2 * q3;
float q2q4 = q2 * q4;
float q3q3 = q3 * q3;
float q3q4 = q3 * q4;
float q4q4 = q4 * q4;
// Normalise accelerometer measurement
norm = sqrt(ax * ax ay * ay az * az);
if (norm == 0.0f) return; // handle NaN
norm = 1.0f / norm; // use reciprocal for division
ax *= norm;
ay *= norm;
az *= norm;
// Normalise magnetometer measurement
norm = sqrt(mx * mx my * my mz * mz);
if (norm == 0.0f) return; // handle NaN
norm = 1.0f / norm; // use reciprocal for division
mx *= norm;
my *= norm;
mz *= norm;
// Reference direction of Earth’s magnetic field
hx = 2.0f * mx * (0.5f – q3q3 – q4q4) 2.0f * my * (q2q3 – q1q4) 2.0f * mz * (q2q4 q1q3);
hy = 2.0f * mx * (q2q3 q1q4) 2.0f * my * (0.5f – q2q2 – q4q4) 2.0f * mz * (q3q4 – q1q2);
bx = sqrt((hx * hx) (hy * hy));
bz = 2.0f * mx * (q2q4 – q1q3) 2.0f * my * (q3q4 q1q2) 2.0f * mz * (0.5f – q2q2 – q3q3);
// Estimated direction of gravity and magnetic field
vx = 2.0f * (q2q4 – q1q3);
vy = 2.0f * (q1q2 q3q4);
vz = q1q1 – q2q2 – q3q3 q4q4;
wx = 2.0f * bx * (0.5f – q3q3 – q4q4) 2.0f * bz * (q2q4 – q1q3);
wy = 2.0f * bx * (q2q3 – q1q4) 2.0f * bz * (q1q2 q3q4);
wz = 2.0f * bx * (q1q3 q2q4) 2.0f * bz * (0.5f – q2q2 – q3q3);
// Error is cross product between estimated direction and measured direction of gravity
ex = (ay * vz – az * vy) (my * wz – mz * wy);
ey = (az * vx – ax * vz) (mz * wx – mx * wz);
ez = (ax * vy – ay * vx) (mx * wy – my * wx);
if (Ki > 0.0f)
{
eInt[0] = ex; // accumulate integral error
eInt[1] = ey;
eInt[2] = ez;
}
else
{
eInt[0] = 0.0f; // prevent integral wind up
eInt[1] = 0.0f;
eInt[2] = 0.0f;
}
// Apply feedback terms
gx = gx Kp * ex Ki * eInt[0];
gy = gy Kp * ey Ki * eInt[1];
gz = gz Kp * ez Ki * eInt[2];
// Integrate rate of change of quaternion
pa = q2;
pb = q3;
pc = q4;
q1 = q1 (-q2 * gx – q3 * gy – q4 * gz) * (0.5f * deltat);
q2 = pa (q1 * gx pb * gz – pc * gy) * (0.5f * deltat);
q3 = pb (q1 * gy – pa * gz pc * gx) * (0.5f * deltat);
q4 = pc (q1 * gz pa * gy – pb * gx) * (0.5f * deltat);
// Normalise quaternion
norm = sqrt(q1 * q1 q2 * q2 q3 * q3 q4 * q4);
norm = 1.0f / norm;
q[0] = q1 * norm;
q[1] = q2 * norm;
q[2] = q3 * norm;
q[3] = q4 * norm;
}

Исходный код программы (скетча)

Оцените статью
Радиокоптер.ру
Добавить комментарий