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 без использования сторонних библиотек.
3пример простейшей передачи данных по радиоканалу с помощью arduino
Особенностью радиопередачи является то, что длительные сигналы одного уровня передавать невозможно, передача будет срываться. Для более-менее устойчивой передачи необходимо передавать переменный сигнал. Причём необходимо каким-то образом выделять полезный сигнал из шума, который всегда присутствует в радиоэфире.
Для первого эксперимента возьмём стандартный скетч мигания светодиодом Blink и немного модифицируем его: каждые 5 секунд будем посылать команду с одного Arduino (передатчика) к другому (приёмнику). По принятии команды приёмник будет либо зажигать светодиод, если он погашен, либо гасить.
Первый скетч – для передатчика. Он довольно простой.
#define prd 4 // пин DATA передатчика FS1000A #define ledPin 13 // вывод встроенного светодиода Arduino void setup() { pinMode(ledPin, OUTPUT); pinMode(prd, OUTPUT); }void loop() { sendCommand(); // отправляем команду delay(5000); // делаем задержку на 5 сек } // посылает команду в эфир void sendCommand() { digitalWrite(ledPin, HIGH); // на время отправки команды зажигаем встроенный светодиод // команда представляет собой три импульса наподобие «тире-точка-тире» digitalWrite(prd, HIGH); delay(100); digitalWrite(prd, LOW); delay(50); digitalWrite(prd, HIGH); delay(50); digitalWrite(prd, LOW); delay(50); digitalWrite(prd, HIGH); delay(100); digitalWrite(prd, LOW); delay(50); digitalWrite(ledPin, LOW); // по окончании передачи команды гасим светодиод }
Временная диаграмма команды приведена на рисунке:
Скетч для приёмника, ввиду описанных выше причин, сложнее. Поэтому для начала давайте просто периодически читать данные на входе приёмника и выводить то, что принимаем, в последовательный порт.
#define prm 2 // пин DATA приёмника XY-MK-5V #define ledPin 13 // встроенный светодиод void setup() { Serial.begin(9600); pinMode(ledPin, OUTPUT); }void loop() { int data = digitalRead(prm); // читаем данные с входа приёмника Serial.println(data); delay(10); }
В мониторе последовательного порта мы увидим череду из быстро сменяющихся единиц и нулей. Если полученные за примерно 17 секунд данные отобразить графически, то увидим следующее:
Как видно, на входе приёмника постоянно присутствует шумовой сигнал. Моменты, когда излучает передатчик, легко отслеживаются на глаз (на рисунке они выделены синими пунктирными рамками). После окончания передачи ненадолго устанавливается нулевой уровень, но затем система автоматической регулировки усиления снова усиливает шумы, и на входе приёмника появляется хаотичная смена логических уровней.
Однако, выделение полезного сигнала из шума с помощью аппаратуры не так просто, как на глаз.
Существует т.н. теорема Котельникова, которая говорит о том, что при дискретизации аналогового сигнала потерь информации не будет только в том случае, если частота полезного сигнала равна половине или меньше частоты дискретизации (т.н. «частоты Найквиста»).
Для простоты возьмём период опроса данных (период дискретизации) со входа приёмника 50 мс – это число равно длительности самой короткой части команды, которую мы каждые 5 секунд отправляем передатчиком в радиоэфир. Снимая по одному отсчёту за 50 мс мы, конечно же, нарушаем теорему Котельникова (период дискретизации надо брать хотя бы 25 мс или меньше). Но для максимального упрощения примера оставим так и посмотрим, сможем ли выделить из шума в радиоэфире нашу команду.
Опрашивая данные с приёмника каждые 50 мс (зелёные вертикальные штрихи на рисунке – моменты опроса приёмника), мы должны увидеть последовательность “110101100”. При этом вероятность ложных срабатываний нашего детектора будет достаточно высока, т.к. есть немалый шанс, что в моменты считывания данных с приёмника случайный шумовой сигнал будут иметь точно такие же значения.
Если взять больше нулей, то вероятность ложных срабатываний уменьшится. Но надо понимать, что поиск совпадения большего по размерности массива будет занимать больше времени, и 50 мс станут уже не 50-тью, а несколько больше. Из-за этого постепенно будут сдвигаться моменты, в которые мы опрашиваем приёмник («убегание» частоты).
Кроме того, мы не можем гарантировать, что в моменты, когда мы ожидаем прихода нулей, на входе приёмника не возникнет какой-то случайный всплеск, и мы не распознаем пришедшую команду. То есть, получим пропуск искомой последовательности. Здесь надо добиться компромисса между числом ложных срабатываний и числом пропусков – оба эти условия нас не устраивают.
const int prm = 2; // пин входа приёмника XY-MK-5V const int ledPin = 13; // пин встроенного светодиода Arduino const int len = 14; // длина массивов bool state = false; // текущее состояние светодиода int pattern[len] = {1,1,0,1,0,1,1,0,0,0,0,0,0,0}; // эталонный массив - маска команды, которую нужно «словить» int testReg[len] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0}; // тестовый регистр - массив оцифрованных значений с входа приёмника void setup() { pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW); }void loop() { int data = digitalRead(prm); // читаем значение на входе приёмника ShiftReg(data, testReg); // вдвигаем полученное число в тестовый регистр if (IsCommandDetected) { // проверяем, нет ли в тестовом регистре искомой последовательности state = !state; // если есть, меняем состояние светодиода digitalWrite(ledPin, state); } delay(50); } // вдвигает в тестовый регистр новое значение void ShiftReg(int newVal, int *arr) { for (int i=0; i<len; i ) { arr[i] = testReg[i 1]; // смещаем значения в регистре на 1 позицию } arr[len-1] = newVal; // последнюю позицию заменяем только что принятым измерением } // проверяет, обнаружена ли команда на входе приёмника bool IsCommandDetected() { for (int i=0; i<len; i ) { if (testReg[i] != pattern[i]) { // почленно сравниваем 2 массива return false; } } return true; }
Функция ShiftReg() получает на вход два аргумента: текущее содержимое тестового регистра и последнее полученное со входа приёмника значение. Она сдвигает все значения в регистре на 1 позицию, а в младший разряд регистра помещает текущее принятое значение.
Таким образом, в регистре постоянно хранятся 16 (в данном конкретном случае) последних считанных с приёмника значений. Если мы посмотрим как меняется содержимое регистра, которое формируется функцией ShiftReg(), то увидим примерно следующее:
0000000000000000 0000000000000001 0000000000000010 0000000000000100 0000000000001000 0000000000010000 0000000000100001 0000000001000010 0000000010000101 0000000100001010 0000001000010101 0000010000101010 0000100001010101 0001000010101010 0010000101010100 0100001010101001 1000010101010010 0000101010100101 0001010101001010 0010101010010101 0101010100101010 1010101001010101 0101010010101011 1010100101010110 0101001010101101 1010010101011010 0100101010110101 1001010101101010 0010101011010101 0101010110101010 1010101101010101 0101011010101011 1010110101010110 0101101010101101 1011010101011010 0110101010110101 1101010101101010 1010101011010101 0101010110101010 1010101101010100 0101011010101000 1010110101010000 0101101010100001 1011010101000010 0110101010000101 1101010100001010 1010101000010101 0101010000101010 1010100001010101 0101000010101010 1010000101010100 0100001010101001 1000010101010010 ...и т.д.
Функция IsCommandDetected() почленно сравнивает два массива – эталонный массив (искомая последовательность) и содержимое тестового регистра (массив полученных с приёмника значений), и если массивы одинаковы, значит мы «поймали» команду. В таком случае меняем состояние встроенного светодиода.
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; } }
Здесь мы ввели таймаут, чтобы выходить из цикла в любом случае, даже если фронт импульса не пришёл. Это гарантирует, что мы не окажемся в бесконечном цикле ожидания.
Загрузим скетч, запустим монитор, нажмём несколько раз ту же кнопку пульта.
Результат, как видно, более стабильный.
4управление шаговым двигателем с помощью arduino
Шаговый двигатель позволяет вращать ротор на определённый угол. Это бывает полезно, когда необходимо задать положение какому-либо механизму или его узлу. Шагом двигателя называется минимальный угол, на который можно повернуть ротор двигателя. Угол поворота и направление движения задаются в управляющей программе.
Характеристики двигателя 28BYJ-48:
Характеристика | Значение |
---|---|
Количество фаз | 4 |
Напряжение питания | от 5 до 12 В |
Число шагов | 64 |
Размер шага | 5,625° |
Скорость вращения | 15 об./сек |
Крутящий момент | 450 г/см |
Модуль с микросхемой драйвера для управления шаговым двигателем выглядит так:
На входы IN1…IN4 подаются управляющие сигналы от Arduino. Используем любые 4 цифровых пина, например, D8…D11. На вход питания необходимо подать постоянное напряжение от 5 до 12 В. Двигателю желательно обеспечить отдельное питание. Но в данном случае, т.к. не планируется использовать двигатель на постоянной основе, можно подать питание и от Arduino.
Перемычка «Вкл/выкл» просто разрывает «плюс» питания, подаваемого на драйвер. В «боевом» изделии сюда можно, например, коммутировать питание с помощью реле, когда это необходимо, чтобы снизить потребление всего изделия. Итак, схема подключения будет такой:
Соберём всё по схеме.
Для Arduino «из коробки» существует готовая библиотека для управления шаговыми двигателями. Она называется Stepper. Можно посмотреть готовые примеры в среде разработки для Arduino: File Examples Stepper.
Они позволяют управлять шаговым двигателем, изменяя скорость и направление движения, поворачивать ротор на заданный угол. Как говорится – бери и пользуйся. Но давайте попробуем разобраться с принципом работы шагового двигателя самостоятельно, не применяя никаких библиотек.
Двигатель 28BYJ-48 имеет 4 фазы. Это означает, что у него имеются 4 электромагнитные катушки, которые под действием электрического тока притягивают сердечник. Если напряжение подавать на катушки поочерёдно, это заставит сердечник вращаться. Рисунок иллюстрирует данный принцип.
Здесь на (1) напряжение подано на катушки A и D, на (2) – на A и B, (3) – B и С, (4) – C и D. Далее цикл повторяется. И таким образом ротор двигателя вращается по кругу.
Напишем самый простой скетч для уравления шаговым двигателем. В нём просто будем вращать двигатель с постоянной скоростью в одном направлении, используя только что описанный принцип.
Простейший скетч управления шаговым двигателем (разворачивается)
// порты для подключения драйвера ULN2003 к Arduino #define in1 8 #define in2 9 #define in3 10 #define in4 11 int del = 5; // время задержки между импульсами void setup() { pinMode(in1, OUTPUT); pinMode(in2, OUTPUT); pinMode(in3, OUTPUT); pinMode(in4, OUTPUT); } void loop() { //фаза 1: digitalWrite(in1, HIGH); digitalWrite(in2, LOW); digitalWrite(in3, LOW); digitalWrite(in4, HIGH); delay(del); //фаза 2: digitalWrite(in1, HIGH); digitalWrite(in2, HIGH); digitalWrite(in3, LOW); digitalWrite(in4, LOW); delay(del); //фаза 3: digitalWrite(in1, LOW); digitalWrite(in2, HIGH); digitalWrite(in3, HIGH); digitalWrite(in4, LOW); delay(del); //фаза 4: digitalWrite(in1, LOW); digitalWrite(in2, LOW); digitalWrite(in3, HIGH); digitalWrite(in4, HIGH); delay(del); }
Как можно догадаться, задержка del определяет скорость вращения двигателя. Уменьшая или увеличивая её можно ускорять или замедлять двигатель.
Если загрузить этот скетч, то увидим, что шаговый двигатель вращается против часовой стрелки. Соответственно, можно вынести цикл вращения в одну сторону в отдельную функцию rotateCounterClockwise(). И сделать аналогичную функцию вращения в противоположную сторону rotateClockwise(), в которой фазы будут следовать в обратном порядке.
Скетч управления шаговым двигателем (разворачивается)
// порты для подключения модуля ULN2003 к Arduino #define in1 8 #define in2 9 #define in3 10 #define in4 11 int del = 5; // время задержки между импульсами void setup() { pinMode(in1, OUTPUT); pinMode(in2, OUTPUT); pinMode(in3, OUTPUT); pinMode(in4, OUTPUT); } void loop() { rotateClockwise(); } // Вращение шагового двигателя по часовой стрелке void rotateClockwise(){ phase4(); phase3(); phase2(); phase1(); } // Вращение шагового двигателя против часовой стрелки void rotateCounterClockwise(){ phase1(); phase2(); phase3(); phase4(); } // Фазы 1...4 шагового двигателя: void phase1(){ digitalWrite(in1, HIGH); digitalWrite(in2, LOW); digitalWrite(in3, LOW); digitalWrite(in4, HIGH); delay(del); } void phase2(){ digitalWrite(in1, HIGH); digitalWrite(in2, HIGH); digitalWrite(in3, LOW); digitalWrite(in4, LOW); delay(del); } void phase3(){ digitalWrite(in1, LOW); digitalWrite(in2, HIGH); digitalWrite(in3, HIGH); digitalWrite(in4, LOW); delay(del); } void phase4(){ digitalWrite(in1, LOW); digitalWrite(in2, LOW); digitalWrite(in3, HIGH); digitalWrite(in4, HIGH); delay(del); }
Если мы загрузим скетч и проверим, поворачивается ли ротор двигателя на целый оборот, если один раз вызвать функцию rotateClockwise(), то обнаружим, что нет. Для совершения полного оборота функцию необходимо вызвать несколько раз. Соответственно, хорошо бы добавить в качестве аргумента функции число, которое будет показывать количество раз, которые она должна выполняться.
Финальный скетч управления шаговым двигателем (разворачивается)
// порты для подключения модуля ULN2003 к Arduino #define in1 8 #define in2 9 #define in3 10 #define in4 11 int del = 5; // время задержки между импульсами => скорость вращения void setup() { pinMode(in1, OUTPUT); pinMode(in2, OUTPUT); pinMode(in3, OUTPUT); pinMode(in4, OUTPUT); } void loop() { rotateClockwise(100); delay(1000); rotateCounterClockwise(100); delay(1000); } // Вращение шагового двигателя по часовой стрелке void rotateClockwise(int n){ for (int i=0; i<n; i ) { phase4(); phase3(); phase2(); phase1(); } } // Вращение шагового двигателя против часовой стрелки void rotateCounterClockwise(int n){ for (int i=0; i<n; i ) { phase1(); phase2(); phase3(); phase4(); } } // Фазы 1...4 шагового двигателя: void phase1(){ digitalWrite(in1, HIGH); digitalWrite(in2, LOW); digitalWrite(in3, LOW); digitalWrite(in4, HIGH); delay(del); } void phase2(){ digitalWrite(in1, HIGH); digitalWrite(in2, HIGH); digitalWrite(in3, LOW); digitalWrite(in4, LOW); delay(del); } void phase3(){ digitalWrite(in1, LOW); digitalWrite(in2, HIGH); digitalWrite(in3, HIGH); digitalWrite(in4, LOW); delay(del); } void phase4(){ digitalWrite(in1, LOW); digitalWrite(in2, LOW); digitalWrite(in3, HIGH); digitalWrite(in4, HIGH); delay(del); }
Вот теперь совсем другое дело! Мы можем управлять скоростью шагового двигателя, задавая задержку после каждой фазы. Мы можем менять направление движения ротора двигателя. И, наконец, мы умеем поворачивать ротор на некоторый угол. Осталось только определить, какое число необходимо передавать в функции поворота rotateClockwise() и rotateCounterClockwise(), чтобы ротор шагового двигателя 1 раз провернулся на 360° вокруг своей оси. Собственно, дальнейшие наработки – вопрос фантазии или необходимости.