1описание метеодатчика bl999 и его информационного протокола
Датчик BL999 – это недорогой датчик температуры и влажности, который используется в комплекте с домашними метеостанциями. Датчик может работать как в комнате, так и на улице. Периодически он передаёт метеостанции по радиоканалу данные измерений и отчёт о своём состоянии. Подобные погодные датчики сейчас очень распространены. Рассматриваемый сенсор BL999 имеет следующие характеристики:
- диапазон измеряемых температур: −40… 50°C;
- диапазон измеряемой влажности: 1…99%;
- период измерений: 30 сек;
- рабочая радиочастота: 433,325 МГц;
- число каналов: 3;
- рабочее расстояние: до 30 м на открытых пространствах.
К одной метеостанции можно подключить до трёх таких датчиков. Номер (канал) датчика устанавливается переключателем, который расположен под съёмной крышкой батарейного отсека (трёхпозиционная кнопка SW1 на фото ниже). Фактически, канал здесь – это просто признак в структуре пакета данных датчика, никакого физического смысла (например, изменение рабочей частоты) он в себе не несёт.
Чтобы лучше понять протокол датчика, с помощью которого он отправляет данные метеостанции, можно попытаться воспользоваться радиоприёмником и разбираться с тем, что приходит из радиоэфира. Но на популярной частоте 433 МГц работает множество бытовых устройств, и приёмник будет ловить большое количество посторонних шумов. Этот факт не позволит нам спокойно изучить протокол датчика.
Поэтому давайте для начала разберём датчик и подключимся осциллографом прямо к выходу, который генерирует цифровой сигнал непосредственно перед отправкой на передающую антенну. Землю можно найти возле «минуса» батареи в отсеке для батарей, а сигнальный провод подключим к верхнему выводу платы, как на фотографии.
Чтобы изучить генерируемый датчиком сигнал, нужен хороший осциллограф. Данные отправляются пакетами длительностью примерно 500…600 мс. Вот как выглядит типичный пакет с датчика BL999 на экране осциллографа.
Снимок экрана для этого пакета. Здесь красным цветом показан аналоговый сигнал, а голубым – оцифрованный сигнал, без присущих аналоговым сигналам искажений.
Вот представлены 4 оцифрованных информационных пакета, сгенерированных датчиком. Эти пакеты пришли друг за другом с разницей в 30 секунд. Именно с такой периодичностью датчик 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-ти битах:
Полубайт также называют «ниббл» (англ. nibble) или тетрада. Это единица измерения информации, содержащая четыре бита.
Давайте разберём реальный пример, и на его основе расшифруем закодированные в нём данные. Возьмём одну группу из 36-ти битов из вот такого пакета, пришедшего от датчика BL999:
В пакете, согласно схеме, присутствуют следующие части:
Обозначение | Номера битов | Описание | Значение из примера |
---|---|---|---|
ID | 35…32, 29…28 | Это идентификатор датчика. Он задаётся произвольным образом и изменяется при каждом включении. | 0101_11 = 23 |
Chan | 31…30 | Номер канала датчика. Кодируется обычным двоичным кодом: «01» – 1, «10» – 2, «11» – 3. | 01 = 1ый канал |
Bat | 27 | Уровень заряда батареи: «0» – норма, «1» – низкий заряд. | 0 = норма |
? | 26…24 | Нет данных. | 100 |
Temperature | 23…12 | Данные температуры. Число записано в обратном порядке и умножено на 10. Отрицательные температуры, кроме этого, хранятся в дополнительном коде (*). | 0111_1111_0000 обращение 0000_1111_1110 = 254 деление на 10 25,4°C |
Humidity | 11…4 | Влажность. Записывается как результат вычитания из 100, в дополнительном коде (*). | 0000_1101 обращение 1011_0000 инверсия битов 0100_1111=79 1 =80 вычитание из 100% 100 − 80 = 20% |
Checksum | 3…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».
Когда получим значение времени, запишем его в модуль 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 такое (слева в DIP-корпусе, справа – в планарном):
Название вывода 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 – это, конечно же, воспользоваться одной из множества готовых библиотек для Arduino, например, этой (она приложена также архивом внизу статьи). Она позволяет выставлять время и считывать его, а также записывать и читать данные из ПЗУ часов.
Думаю, что объяснять, как использовать библиотеку для Arduino, не нужно. В библиотеке есть два примера, в которых подробно расписано, как использовать часы DS1302. Поэтому давайте попробуем разобраться, как работать с часами DS1302 без сторонних библиотек.
Для обмена с микросхемой DS1302 используется последовательный интерфейс, похожий на SPI. Диаграмма передачи данных показана ниже. Видно, что во время чтения или записи данных сначала следует выставить логическую «1» на линии CE. Затем сгенерировать 16 тактовых синхронизирующих импульсов. В это время передаются 16 бит информации.
В первых 8-ми битах передаётся команда (командный байт), а следующие 8 бит – данные. Структура командного байта показана ниже. В нём старший бит всегда «1», младший – признак операции (чтение RD=1 или запись WR=0), а остальные биты – это адрес регистра, с которым взаимодействуем.
Кроме того, DS1302 поддерживает множественную передачу (burst mode). Для этого следует удерживать высокий уровень на линии CE и генерировать необходимое число тактовых импульсов. Данные будут читаться (или записываться) из регистров или ПЗУ последовательно, начиная с заданного адреса и далее.
Предлагаю для изучения DS1302 воспользоваться отладочной платой с микросхемой FT2232H и программы SPI via FTDI. Это позволит избежать постоянного программирования Arduino и проводить все эксперименты с часами «на лету».
Единственная сложность в том, что микросхема FT2232H использует 3.3-вольтовую логику, а часы DS1302 – 5-вольтовую. Но ничего страшного, воспользуемся преобразователем логического уровня, благо стоит он копейки, и в применении исключительно прост.
У него есть две стороны: одна отвечает за низковольтовую часть (LV), другая – за высоковольтную (HV). У него есть 4 низковольтных входа-выхода (LV1…LV4) и соответствующие им 4 высоковольтных входа-выхода (HV1…HV4).
К высоковольтной стороне преобразователя подключается модуль DS1302, к низковольтной – микросхема FT2232H. Соответствие выводов такое: CLK – ADBUS0, DAT – ADBUS1 и ADBUS2, RST – ADBUS3. Подключаем соответственно через преобразователь напряжения. Вот так это выглядит вживую:
Когда собрали схему, запустим программу SPI via FTDI и в меню «Устройство» выберем интерфейс SPI, потом нажмём «Подключить». Теперь в левой части главного окна, в рамке «Настройки SPI» снимем галочки с CS active LOW (активация часов DS1302 высоким уровнем, вывод CE) и MSB first (передача байта старшим битом вперёд). Остальные параметры оставим как есть.
Теперь попробуем прочитать 1 байт из регистра секунд 0x81. Он должен меняться каждую секунду, и мы сразу увидим, что наша схема работает. Для чтения регистра секунд настройки программы будут такие (обратите внимание на раздел «Чтение»):
Чтобы увидеть принятые данные, нужно нажать на кнопку с изображением таблицы слева от кнопки «Прочитать».
Чтобы прочитать данные всех регистров, нужно отправить команду BF и запросить столько регистров, сколько нужно. Все данные о дате хранятся в 7-ми регистрах, а восьмой – данные о запрете записи (WP, write protect).
Кстати, если вместо числа «1» ввести число раз «0» (справа от кнопки чтения), то программа будет постоянно опрашивать часы DS1302, и вы увидите в таблице принятых данных как идёт время часов DS1302.
Для записи данных в ПЗУ часов DS1302 в режиме множественной передачи (не по одному байту) следует отправить команду FE и дальше нужные данные. Для чтения данных из ПЗУ в режиме множественной передачи нужно отправить команду FF:
Теперь мы можем устанавливать время на часах DS1302, читать его, а также работать с постоянной энергонезависимой памятью часов. Приведённых примеров должно быть достаточно, чтобы реализовать всё это на Arduino без использования сторонних библиотек.
4подключение к arduino модуля с инфракрасным приёмником
ИК датчик может состоять из одного только инфракрасного приёмника, как в этом случае:
Такой сенсор используется для детектирования и считывания различных инфракрасных сигналов. Например, таким датчиком можно принять управляющие сигналы ИК пульта от телевизора или другой бытовой техники. На модуле присутствует светодиод, который загорается, когда на приёмник попадает инфракрасное излучение. На выхода модуля – цифровой сигнал, который показывает, падает ли на сенсор ИК излучение или нет.
К Arduino модуль с ИК приёмником подключается тоже очень просто:
Пин модуля | Пин Arduino | Назначение |
---|---|---|
DAT | Любой цифровой | Признак наличия ИК излучения на входе приёмника |
VCC | 5V | Питание |
GND | GND | Земля |
Напишем скетч, в котором будем просто показывать с помощью встроенного светодиода, что на входе приёмника присутствует ИК излучение. В данном модуле аналогично с ранее рассмотренным на выходе 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, направить на ИК приёмник ИК пульт и нажимать на нём разные кнопки, то мы увидим, что светодиод нашего индикатора быстро мигает. Разные кнопки – по-разному мигает.
Очевидно, что каждая команда закодирована своей бинарной последовательностью. Хотелось бы увидеть, какие именно команды приходят от пульта. Но прежде чем ответить на этот вопрос, нужно посмотреть другим способом, что же отправляет пульт. А именно – с помощью осциллографа.
На осциллограмме видна серия «пачек» импульсов примерно одинаковой длительности. Каждая «пачка» состоит из 24-х импульсов.
В таком виде довольно трудно увидеть, какой сигнал передаётся от пульта ДУ. Прелесть нашего приёмника в том, что он выполняет рутинную работу по оцифровке аналогового инфракрасного сигнала и выдаёт уже «красивый» цифровой сигнал. Давайте посмотрим его на осциллографе.
Вот так выглядит посылка пульта целиком. Здесь жёлтая линия – аналоговый сигнал пульта ДУ, голубая – цифровой сигнал с выхода ИК приёмника. Видно, что продолжительность передачи составляет примерно 120 мс. Очевидно, время будет несколько варьироваться исходя из того, какие биты присутствуют в пакете.
При большем приближении видно, что высокочастотное заполнение, которое имеется в аналоговом сигнале, в цифровом сигнале с ИК приёмника отсутствует. Приёмник прекрасно справляется со своей задачей и показывает чистый цифровой сигнал. Видна последовательность коротких и длинных прямоугольных импульсов. Длительность коротких импульсов примерно 1,2 мс, длинных – в 2 раза больше.
Мы уже видели подобный сигнал, когда разбирали сигнал комнатной метеостанции. Возможно, здесь применяется тот же способ кодирования информации: короткие импульсы – это логический ноль, длинные – логическая единица. На следующем видео можно посмотреть пакет целиком:
Если зарисовать этот пакет, то получится как-то так:
Дальнейшие исследования показали, что все пакеты данного пульта ДУ состоят из двух пачек импульсов. Причём первая всегда содержит 35 бит, вторая – 32.
Есть несколько вариантов, как поступить для получения цифровых данных пакета:
- опрашивать пакет через равные промежутки времени (т.н. «стробирование»), а затем принимать решение, это логический «0» или «1»;
- ловить фронты импульсов (детектор фронта), затем определять их длительность и также принимать решение, какой это бит.
Напомню, что будем считать короткие импульсы логическим нулём, длинные – логической единицей.
Для реализации первого варианта понятно, с какой частотой необходимо опрашивать ИК датчик, чтобы принимать с него корректные данные: 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;
}
Исходный код программы (скетча)
void setup() {
Wire.begin(); //старт связи по протоколу I2C
Serial.begin(9600); //старт последовательной связи со скоростью 9600
qmc.init(); //инициализация датчика QMC5883
for (int thisPin=0; thisPin <= led_count; thisPin ){ //навигация по всем контактам в массиве
pinMode(ledPins[thisPin],OUTPUT); //задаем им режим работы на вывод данных
}
}
void loop() { //бесконечный цикл
int x,y,z;
qmc.read(&x,&y,&z); //считываем значения X, Y и Z с датчика
int heading=atan2(x, y)/0.0174532925; // рассчитываем направление в градусах используя значения X и Y
//преобразуем результат в диапазон от 0 до 360
if(heading < 0)
heading =360;
heading = 360-heading;
Serial.println(heading); //передаем значение направления в последовательный порт связи для целей отладки
//в зависимости от значения рассчитанного направления печатаем результат (для отладки) и зажигаем соответствующий светодиод
if (heading > 338 || heading < 22)
{
Serial.println(«NORTH»);
digitalWrite(ledPins[0],HIGH);
}
if (heading > 22 && heading < 68)
{
Serial.println(«NORTH-EAST»);
digitalWrite(ledPins[7],HIGH);
}
if (heading > 68 && heading < 113)
{
Serial.println(«EAST»);
digitalWrite(ledPins[6],HIGH);
}
if (heading > 113 && heading < 158)
{
Serial.println(«SOUTH-EAST»);
digitalWrite(ledPins[5],HIGH);
}
if (heading > 158 && heading < 203)
{
Serial.println(«SOUTH»);
digitalWrite(ledPins[4],HIGH);
}
if (heading > 203 && heading < 248)
{
Serial.println(«SOTUH-WEST»);
digitalWrite(ledPins[3],HIGH);
}
if (heading > 248 && heading < 293)
{
Serial.println(«WEST»);
digitalWrite(ledPins[2],HIGH);
}
if (heading > 293 && heading < 338)
{
Serial.println(«NORTH-WEST»);
digitalWrite(ledPins[1],HIGH);
}
delay(500); // обновление данных каждые полсекунды
//выключаем все светодиоды
for (int thisPin=0; thisPin <= led_count; thisPin ){
digitalWrite(ledPins[thisPin],LOW);
}
}
/* * Program for Arduino Digital Compass using QMC5883 * Project by: Aswinth Raj * Dated: 1-11-2023 * Website: http://www.circuitdigest.com/ * Lib. from https://radiocopter.ru/keepworking/Mecha_QMC5883L * WARNING: This code works only for QMC5883 Sensor which is commonly being sold as HMC5883 read article to find the actual name of the sensor you have. */ #include <Wire.h> //библиотека для работы по протоколу I2C #include <MechaQMC5883.h> //библиотека QMC5883 для работы с датчиком QMC583, но не с HMC5883 MechaQMC5883qmc;//создаем объект для работы с датчиком (магнитометром), даем ему имя qmc intledPins[]={2,3,4,5,6,7,8,9};// массив выходных контактов, к которым подключены светодиоды charled_count=7;//общее число используемых контактов для светодиодов voidsetup(){ Wire.begin();//старт связи по протоколу I2C Serial.begin(9600);//старт последовательной связи со скоростью 9600 qmc.init();//инициализация датчика QMC5883 for(intthisPin=0;thisPin<=led_count;thisPin ){//навигация по всем контактам в массиве pinMode(ledPins[thisPin],OUTPUT);//задаем им режим работы на вывод данных } } voidloop(){//бесконечный цикл intx,y,z; qmc.read(&x,&y,&z);//считываем значения X, Y и Z с датчика intheading=atan2(x,y)/0.0174532925;// рассчитываем направление в градусах используя значения X и Y //преобразуем результат в диапазон от 0 до 360 if(heading<0) heading =360; heading=360—heading; Serial.println(heading);//передаем значение направления в последовательный порт связи для целей отладки //в зависимости от значения рассчитанного направления печатаем результат (для отладки) и зажигаем соответствующий светодиод if(heading>338||heading<22) { Serial.println(«NORTH»); digitalWrite(ledPins[0],HIGH); } if(heading>22&&heading<68) { Serial.println(«NORTH-EAST»); digitalWrite(ledPins[7],HIGH); } if(heading>68&&heading<113) { Serial.println(«EAST»); digitalWrite(ledPins[6],HIGH); } if(heading>113&&heading<158) { Serial.println(«SOUTH-EAST»); digitalWrite(ledPins[5],HIGH); } if(heading>158&&heading<203) { Serial.println(«SOUTH»); digitalWrite(ledPins[4],HIGH); } if(heading>203&&heading<248) { Serial.println(«SOTUH-WEST»); digitalWrite(ledPins[3],HIGH); } if(heading>248&&heading<293) { Serial.println(«WEST»); digitalWrite(ledPins[2],HIGH); } if(heading>293&&heading<338) { Serial.println(«NORTH-WEST»); digitalWrite(ledPins[1],HIGH); } delay(500);// обновление данных каждые полсекунды //выключаем все светодиоды for(intthisPin=0;thisPin<=led_count;thisPin ){ digitalWrite(ledPins[thisPin],LOW); } } |