Как соединить несколько Arduino с RPI для управления домашним освещением и выключателями
Планируя инфраструктуру освещения (выключатели на стенах и светильники) моего нового дома (он все еще находится в стадии строительства), я решил пойти по «автоматизированному маршруту» и, в силу своего опыта (я «старый» системный/сетевой администратор с навыками программирования и большой «страстью» и «активностью» в отношении ПО с открытым исходным кодом), я серьезно пытаюсь реализовать его с помощью трех Arduino и одного RPi2.
В связи с количеством и расположением настенных выключателей и светильников я хотел бы использовать три MEGA, которые будут подключаться как к настенным выключателям, так и к светильникам в соседних комнатах. Кроме того, RPi2 (или аналогичный) будет использоваться в качестве «контроллера» для правильного программирования MEGA при необходимости и для взаимодействия с другим оборудованием (сенсорным экраном, Wi-Fi-планшетом/смартфоном, пультами дистанционного управления и т.д.) через IP-сеть.
Схема, которую я пытаюсь реализовать, похожа на эту:
где вы видите:
- 9 ламп (L1–L9), каждая из которых управляется собственным реле (R1–R9);
- 7 настенных кнопок (WPB1–WPB7) для включения/выключения одного или нескольких светильников;
- 3 MEGA, соединяющие настенные кнопки и светильники;
- 1 RPi2, действующий как «супервизор» и «шлюз Интернет/Ethernet».
Моя главная архитектурная проблема связана с взаимосвязью RPI2 и MEGA. Поскольку каждое устройство будет расположено на расстоянии нескольких десятков метров друг от друга, я остановился на двух вариантах (поправьте меня, если я ошибаюсь):
- Ethernet
- RS485
(Кстати: я категорически исключаю «беспроводные соединения», поскольку уже разместил все электрические трубы «совместимым» образом. Другими словами: я хотел бы избежать беспроводных технологий в «контролирующей сети»)
Что касается Ethernet, я выберу его как второй вариант, как из-за немного более высокой стоимости, так и из-за сложности (нужен коммутатор для включения питания, дополнительные проблемы с кабелями и т. д.).
Я тщательно изучил «шину RS485» и обнаружил, что с физической точки зрения её относительно легко реализовать с помощью двух проводов в многоточечной конфигурации.
К сожалению, с «прикладной точки зрения» всё сложнее, поскольку поддерживается только «полудуплексная» связь и, что ещё хуже, нет возможности избежать «коллизий» при обмене данными (вероятно, поэтому протокол MODBUS, обычно используемый на шине RS485, обеспечивает сценарий «один ведущий, несколько ведомых»).
Прежде чем задать вопросы, мне нужно добавить ещё одно ограничение: я хочу, чтобы инфраструктура была «максимально отказоустойчивой», особенно к проблемам с шиной и/или «контроллером» (RPI2). Другими словами:
- Каждая MEGA должна позволять включать собственное освещение, когда этого требует одна из её кнопок. Например:
- если WPB1 управляет L2 и L3, он должен работать, даже если шина неисправна или RPI2 отключен;
- если WPB3 управляет L4 и L9, то при возникновении проблем с шиной/RPI2 при нажатии WPB3 будет включен только L4;
Итак, после всего вышесказанного, вот мои вопросы:
Подойдет ли двухпроводная многоточечная шина RS485 для моего сценария?
Если нет, то какие (возможно, недорогие и простые) другие решения я могу рассмотреть?
Если да, то какую логику мне нужно реализовать на MEGA, например:
- а) они должны действовать как «ведомые» по отношению к RPI2 при включении освещения с помощью кнопки, подключенной к другим MEGA, или когда RP2 решает включить некоторые огни (например, из-за удаленного/интернет-доступа);
- б) они должны выступать в роли «главных», когда нажимается одна из их кнопок, и... это должно быть передано в RPI2, чтобы отправлять команды другим MEGA для включения «управляемых» ламп;
Что касается пункта 3b), вместо того, чтобы MEGA выступали в роли ведущего устройства при нажатии WPB, можно ли реализовать логику «частого опроса» на RPI2? Если да, то какое значение частоты опроса является разумным (1 опрос в секунду? 5 опросов в секунду? Слишком много? Слишком мало?)
Я понимаю, что это слишком обширный вопрос, но я действительно провел множество исследований, и, несмотря на огромное количество онлайн-документации, я так и не смог найти ответ на эти вопросы.
Обновление 1
- Если говорить об общем количестве, то у меня будет 31 светильник, которые будут управляться 46 настенными кнопками, более или менее равномерно распределенными по 4 отдельным панелям управления;
- Что касается выбора MEGA (вместо UNO), я выбрал MEGA из-за большего количества контактов ввода/вывода. Я просто выбрал плату с максимальным количеством контактов;
- Что касается RPI2, я решил использовать полноценный «компьютер» (а не дополнительный микроконтроллер), поскольку мне нужна своего рода «развязка» между «физической управляющей сетью» и «интерфейсом управления пользователем». Другими словами, я хочу, чтобы управление физическими кнопками/реле осуществлялось устройством, подобным ПЛК (Arduino: очень быстрое включение; работа в режиме реального времени; высокая надёжность; минимальные/отсутствие внешних «вычислительных» факторов, вызывающих задержки/проблемы). В то же время, весь пользовательский интерфейс будет обрабатываться реальным компьютером, на котором я смогу легко писать мощные HTTP-веб-сервисы и/или действительно сложную «логику», используя технологии и языки, которые плохо сочетаются с Arduino (позже — гораздо позже — я планирую разместить по всему дому несколько 10,1-дюймовых планшетов Android с Full HD-экраном за 150 долларов, которые будут работать как беспроводные «клиенты» для того, что должно быть «мощным» (с точки зрения вычислительной мощности) веб-сервером; также я планирую добавить различные датчики [температуры, влажности, контактов, измерителей мощности и т. д.], данные которых нужно будет сохранять для анализа тенденций/архивирования). Поэтому я задумался о RPi2, который при необходимости можно легко заменить чем-то более мощным.
@Damiano Verzulli, 👍10
Обсуждение4 ответа
Лучший ответ:
Я написал длинный пост о RS485.
Во-первых, использование плат Mega кажется излишеством, если только у вас их ещё нет. Платы Uno или одной из плат меньшего форм-фактора, похоже, будет вполне достаточно для управления несколькими выключателями и включения пары лампочек.
Даже RPI кажется лишним. Другой Uno легко мог бы контролировать ваши линии RS485 и подключаться через Ethernet (через Ethernet-экран) к остальной части вашего дома или к тому, чем вы занимаетесь.
... нет никаких положений, позволяющих избежать «столкновений» при общении...
Ну, вы это встраиваете. Вы присваиваете каждому Mega адрес (например, хранящийся в EEPROM), затем «адресуете» нужный вам модуль и ждете ответа. Например, в коде с моей страницы выше:
Мастер
#include "RS485_protocol.h"
#include <SoftwareSerial.h>
const byte ENABLE_PIN = 4;
const byte LED_PIN = 13;
SoftwareSerial rs485 (2, 3); // вывод приема, вывод передачи
// процедуры обратного вызова
void fWrite (const byte what)
{
rs485.write (what);
}
int fAvailable ()
{
return rs485.available ();
}
int fRead ()
{
return rs485.read ();
}
void setup()
{
rs485.begin (28800);
pinMode (ENABLE_PIN, OUTPUT); // включение выхода драйвера
pinMode (LED_PIN, OUTPUT); // встроенный светодиод
} // конец настройки
byte old_level = 0;
void loop()
{
// чтение потенциометра
byte level = analogRead (0) / 4;
// нет изменений? забудьте
if (level == old_level)
return;
// собрать сообщение
byte msg [] = {
1, // устройство 1
2, // включить свет
level // до какого уровня
};
// отправить на подчиненное устройство
digitalWrite (ENABLE_PIN, HIGH); // разрешить отправку
sendMsg (fWrite, msg, sizeof msg);
digitalWrite (ENABLE_PIN, LOW); // отключить отправку
// получить ответ
byte buf [10];
byte received = recvMsg (fAvailable, fRead, buf, sizeof buf);
digitalWrite (LED_PIN, received == 0); // включить светодиод, если ошибка
// отправлять только один раз при успешном изменении
if (received)
old_level = level;
} // конец цикла
Вы настраиваете приёмопередатчик RS485 на отправку или приём. Обычно он находится в режиме приёма, а для отправки «пакета» данных переключаетесь в режим отправки. (См. «Включить отправку» выше).
Теперь адресуемое устройство переводит свой приёмопередатчик в режим отправки и отвечает. В коде в написанной мной библиотеке есть тайм-аут, поэтому, если ведомое устройство неисправно, приём прекращается. Вы можете помнить об этом на ведущем устройстве и стараться реже связываться с ним. Или же вам может быть всё равно, что тайм-аут короткий.
Раб
#include <SoftwareSerial.h>
#include "RS485_protocol.h"
SoftwareSerial rs485 (2, 3); // вывод приема, вывод передачи
const byte ENABLE_PIN = 4;
void fWrite (const byte what)
{
rs485.write (what);
}
int fAvailable ()
{
return rs485.available ();
}
int fRead ()
{
return rs485.read ();
}
void setup()
{
rs485.begin (28800);
pinMode (ENABLE_PIN, OUTPUT); // включение выхода драйвера
}
void loop()
{
byte buf [10];
byte received = recvMsg (fAvailable, fRead, buf, sizeof (buf));
if (received)
{
if (buf [0] != 1)
return; // не мое устройство
if (buf [1] != 2)
return; // неизвестная команда
byte msg [] = {
0, // устройство 0 (главное)
3, // получена команда включить свет
};
delay (1); // дать мастеру время подготовиться к приему
digitalWrite (ENABLE_PIN, HIGH); // разрешить отправку
sendMsg (fWrite, msg, sizeof msg);
digitalWrite (ENABLE_PIN, LOW); // отключить отправку
analogWrite (11, buf [2]); // установить уровень освещенности
} // конец, если что-то получено
} // конец цикла
Примечание: пример кода на моей связанной странице не считывает адрес из EEPROM, однако это легко реализовать.
Не вижу особых причин, по которым вы не могли бы опрашивать подчиненных достаточно часто. Чем еще может заниматься главный сервер? Вы также можете настроить главный сервер как HTTP-сервер, чтобы общаться с ним с ноутбука или из любой другой части дома.
Моя проводка:

Чтобы продемонстрировать идею, я установил два устройства Uno, соединив их примерно 8-метровым кабелем для звонков (даже не витой парой и уж точно не экранированным).

Один Uno запускал стандартный скетч таблицы ASCII (со скоростью 9600 бод), его вывод Tx был подключен к LTC1480, а затем выводы A/B были подключены к проводу звонка.
Другой Uno был подключен как USB-интерфейс (сброс подключен к земле), и он просто повторял все, что приходило на контакт Tx к USB.
Насколько я мог видеть, всё работало идеально.
Мне вообще не понадобится никакой опрос, так как с вашим подходом я могу реализовать контекст «один главный/несколько подчинённых»… но с «подвижным» главным. Правильно?
В моем ответе выше предполагалось, что вероятность выхода из строя ведомых устройств выше, чем у ведущего (не могу понять, почему это произошло, но, возможно, это связано с тем, что ведомых устройств больше, чем ведущих, и ведущее устройство не управляет такими вещами, как освещение).
Я считаю свои Arduino чрезвычайно надежными при выполнении простых задач (например, отпирании двери при предъявлении RFID-карты).
Возможно, вы предусмотрите резервную позицию для подчинённых устройств. В конце концов, если они опрашиваются каждую секунду, а затем опрос не приходит, они, вероятно, могут попытаться взять на себя роль главного устройства, возможно, в порядке возрастания номера устройства, чтобы избежать конфликтов. Частью этого опроса «новым главным устройством» может быть проверка «исходного главного устройства» на готовность возобновить выполнение своих обязанностей.
Библиотека, которую я описал на странице по ссылке, имеет встроенную проверку ошибок. Идея заключается в том, что, проверяя CRC пакета, вы гарантируете, что не получите пакет в середине и не истолкуете содержащиеся в нем данные неверно.
Можно также предусмотреть случайность времени опроса, чтобы разрешить тупиковую ситуацию, если два подчинённых устройства одновременно попытаются стать ведущим. В случае неудачи подчинённое устройство может подождать случайное (и увеличивающееся) время, прежде чем повторить попытку, чтобы дать шанс другому подчинённому устройству.
Я просто хотел отметить, что вероятность коллизий пакетов довольно мала. Пакеты отправляются только при нажатии кнопки или когда нужно включить свет.
Гербен прав, но я бы опасался, что уведомление о переключении осталось незамеченным. Один из возможных вариантов решения этой проблемы — вместо того, чтобы отвечать на запрос изменениями состояния, ведомые устройства будут отвечать текущим состоянием. Вот так:
Master: Slave 3, what is your status?
Slave 3: Lights 1 and 4 on, lights 2 and 3 off.
Мне вообще не понадобится никакой опрос, так как с вашим подходом я могу реализовать контекст «один главный/несколько подчинённых»… но с «подвижным» главным. Правильно?
Я немного поразмыслил над этим и теперь думаю, что можно создать систему, практически не требующую главного устройства. Работать это может так:
Каждое устройство имеет свой собственный адрес, который оно получает из EEPROM (или DIP-переключателей). Например, 1, 2, 3, 4, 5 ...
Вы выбираете диапазон адресов, которые собираетесь использовать (например, максимум 10)
При включении устройство сначала прослушивает другие устройства, «общающиеся» на шине. Надеемся, что оно услышит хотя бы одно (если нет, см. ниже).
Мы выбираем фиксированный «пакет сообщения», скажем, из 50 байт, включая адрес, CRC и т. д. При скорости 9600 бод отправка займет 52 мс.
Каждое устройство получает «слот» времени и ждет своей очереди, чтобы связаться с шиной.
По достижении своего временного интервала устройство переходит в режим вывода и рассылает пакет, содержащий его собственный адрес. Таким образом, все остальные устройства теперь могут считывать его состояние (и при необходимости выполнять соответствующие действия). Например, устройство 1 может сообщить, что выключатель 3 замкнут, что означает, что устройство 2 должно включить свет.
В идеале вы понимаете, что ваш временной интервал наступил, потому что адрес вашего устройства на единицу больше адреса только что прослушанного вами пакета. Например, вы — устройство 3. Вы только что услышали, как устройство 2 объявило о своём статусе. Теперь ваша очередь. Конечно, вы переходите к максимальному номеру, поэтому после устройства 10 вы возвращаетесь к устройству 1.
Если устройство отсутствует и не отвечает, вы даёте ему, скажем, половину временного интервала для ответа, а затем предполагаете, что оно неисправно, и все устройства на шине теперь предполагают, что начался следующий временной интервал. (Например, вы услышали устройство 2, устройство 3 должно ответить, после 25 мс бездействия устройство 4 теперь может ответить). Это правило даёт устройству 25 мс для ответа, чего должно быть достаточно, даже если оно обслуживает прерывание или что-то подобное.
Если отсутствует несколько устройств подряд, засчитывается промежуток в 25 мс за каждое отсутствующее устройство, пока не наступит ваша очередь.
Как только вы получите хотя бы один ответ, время будет повторно синхронизировано, поэтому любой дрейф часов будет устранен.
Единственная сложность здесь заключается в том, что при первоначальном включении питания (которое может произойти одновременно, если электроснабжение здания было отключено, а затем восстановлено) в данный момент нет устройства, передающего свой статус, и, следовательно, не с чем синхронизироваться.
В этом случае:
Если после прослушивания достаточно долгого времени (например, 250 мс), чтобы были слышны все устройства, и ничего не слышно, устройство предположительно предполагает, что оно первое, и начинает трансляцию. Однако, возможно, два устройства сделают это одновременно и, таким образом, не услышат друг друга.
Если устройство не получило сигнал от другого устройства, оно случайным образом разносит время между передачами (возможно, используя генератор случайных чисел из своего номера устройства, чтобы избежать «случайного» разнесения передач всеми устройствами на одинаковую величину).
Эти случайные смещения во времени не будут иметь значения, потому что все равно никто не слушает.
Рано или поздно одно устройство получит исключительное право использования шины, а остальные смогут синхронизироваться с ним обычным способом.
Этот случайный промежуток между попытками установить связь похож на то, что происходило в Ethernet, когда несколько устройств использовали один коаксиальный кабель.
Демонстрация системы без мастера
Это была интересная задача, поэтому я собрал демо-версию, как это сделать, не имея какого-либо конкретного мастера, как описано выше.
Сначала вам нужно настроить текущий адрес устройства и количество устройств в EEPROM, поэтому запустите этот скетч, изменив myAddress для каждого Arduino:
#include <EEPROM.h>
const byte myAddress = 3;
const byte numberOfDevices = 4;
void setup ()
{
if (EEPROM.read (0) != myAddress)
EEPROM.write (0, myAddress);
if (EEPROM.read (1) != numberOfDevices)
EEPROM.write (1, numberOfDevices);
} // end of setup
void loop () { }
Теперь загрузите это на каждое устройство:
/*
Multi-drop RS485 device control demo.
Devised and written by Nick Gammon.
Date: 7 September 2015
Version: 1.0
Licence: Released for public use.
For RS485_non_blocking library see: http://www.gammon.com.au/forum/?id=11428
For JKISS32 see: http://forum.arduino.cc/index.php?topic=263849.0
*/
#include <RS485_non_blocking.h>
#include <SoftwareSerial.h>
#include <EEPROM.h>
// the data we broadcast to each other device
struct
{
byte address;
byte switches [10];
int status;
} message;
const unsigned long BAUD_RATE = 9600;
const float TIME_PER_BYTE = 1.0 / (BAUD_RATE / 10.0); // seconds per sending one byte
const unsigned long PACKET_LENGTH = ((sizeof (message) * 2) + 6); // 2 bytes per payload byte plus STX/ETC/CRC
const unsigned long PACKET_TIME = TIME_PER_BYTE * PACKET_LENGTH * 1000000; // microseconds
// software serial pins
const byte RX_PIN = 2;
const byte TX_PIN = 3;
// transmit enable
const byte XMIT_ENABLE_PIN = 4;
// debugging pins
const byte OK_PIN = 6;
const byte TIMEOUT_PIN = 7;
const byte SEND_PIN = 8;
const byte SEARCHING_PIN = 9;
const byte ERROR_PIN = 10;
// action pins (demo)
const byte LED_PIN = 13;
const byte SWITCH_PIN = A0;
// times in microseconds
const unsigned long TIME_BETWEEN_MESSAGES = 3000;
unsigned long noMessagesTimeout;
byte nextAddress;
unsigned long lastMessageTime;
unsigned long lastCommsTime;
unsigned long randomTime;
SoftwareSerial rs485 (RX_PIN, TX_PIN); // receive pin, transmit pin
// what state we are in
enum {
STATE_NO_DEVICES,
STATE_RECENT_RESPONSE,
STATE_TIMED_OUT,
} state;
// callbacks for the non-blocking RS485 library
size_t fWrite (const byte what)
{
rs485.write (what);
}
int fAvailable ()
{
return rs485.available ();
}
int fRead ()
{
lastCommsTime = micros ();
return rs485.read ();
}
// RS485 library instance
RS485 myChannel (fRead, fAvailable, fWrite, 20);
// from EEPROM
byte myAddress; // who we are
byte numberOfDevices; // maximum devices on the bus
// Initial seed for JKISS32
static unsigned long x = 123456789,
y = 234567891,
z = 345678912,
w = 456789123,
c = 0;
// Simple Random Number Generator
unsigned long JKISS32 ()
{
long t;
y ^= y << 5;
y ^= y >> 7;
y ^= y << 22;
t = z + w + c;
z = w;
c = t < 0;
w = t & 2147483647;
x += 1411392427;
return x + y + w;
} // end of JKISS32
void Seed_JKISS32 (const unsigned long newseed)
{
if (newseed != 0)
{
x = 123456789;
y = newseed;
z = 345678912;
w = 456789123;
c = 0;
}
} // end of Seed_JKISS32
void setup ()
{
// debugging prints
Serial.begin (115200);
// software serial for talking to other devices
rs485.begin (BAUD_RATE);
// initialize the RS485 library
myChannel.begin ();
// debugging prints
Serial.println ();
Serial.println (F("Commencing"));
myAddress = EEPROM.read (0);
Serial.print (F("My address is "));
Serial.println (int (myAddress));
numberOfDevices = EEPROM.read (1);
Serial.print (F("Max address is "));
Serial.println (int (numberOfDevices));
if (myAddress >= numberOfDevices)
Serial.print (F("** WARNING ** - device number is out of range, will not be detected."));
Serial.print (F("Packet length = "));
Serial.print (PACKET_LENGTH);
Serial.println (F(" bytes."));
Serial.print (F("Packet time = "));
Serial.print (PACKET_TIME);
Serial.println (F(" microseconds."));
// calculate how long to assume nothing is responding
noMessagesTimeout = (PACKET_TIME + TIME_BETWEEN_MESSAGES) * numberOfDevices * 2;
Serial.print (F("Timeout for no messages = "));
Serial.print (noMessagesTimeout);
Serial.println (F(" microseconds."));
// set up various pins
pinMode (XMIT_ENABLE_PIN, OUTPUT);
// demo action pins
pinMode (SWITCH_PIN, INPUT_PULLUP);
pinMode (LED_PIN, OUTPUT);
// debugging pins
pinMode (OK_PIN, OUTPUT);
pinMode (TIMEOUT_PIN, OUTPUT);
pinMode (SEND_PIN, OUTPUT);
pinMode (SEARCHING_PIN, OUTPUT);
pinMode (ERROR_PIN, OUTPUT);
// seed the PRNG
Seed_JKISS32 (myAddress + 1000);
state = STATE_NO_DEVICES;
nextAddress = 0;
randomTime = JKISS32 () % 500000; // microseconds
} // end of setup
// set the next expected address, wrap around at the maximum
void setNextAddress (const byte current)
{
nextAddress = current;
if (nextAddress >= numberOfDevices)
nextAddress = 0;
} // end of setNextAddress
// Here to process an incoming message
void processMessage ()
{
// we cannot receive a message from ourself
// someone must have given two devices the same address
if (message.address == myAddress)
{
digitalWrite (ERROR_PIN, HIGH);
while (true)
{ } // give up
} // can't receive our address
digitalWrite (OK_PIN, HIGH);
// handle the incoming message, depending on who it is from and the data in it
// make our LED match the switch of the previous device in sequence
if (message.address == (myAddress - 1))
digitalWrite (LED_PIN, message.switches [0]);
digitalWrite (OK_PIN, LOW);
} // end of processMessage
// Here to send our own message
void sendMessage ()
{
digitalWrite (SEND_PIN, HIGH);
memset (&message, 0, sizeof message);
message.address = myAddress;
// fill in other stuff here (eg. switch positions, analog reads, etc.)
message.switches [0] = digitalRead (SWITCH_PIN);
// now send it
digitalWrite (XMIT_ENABLE_PIN, HIGH); // enable sending
myChannel.sendMsg ((byte *) &message, sizeof message);
digitalWrite (XMIT_ENABLE_PIN, LOW); // disable sending
setNextAddress (myAddress + 1);
digitalWrite (SEND_PIN, LOW);
lastCommsTime = micros (); // we count our own send as activity
randomTime = JKISS32 () % 500000; // microseconds
} // end of sendMessage
void loop ()
{
// incoming message?
if (myChannel.update ())
{
memset (&message, 0, sizeof message);
int len = myChannel.getLength ();
if (len > sizeof message)
len = sizeof message;
memcpy (&message, myChannel.getData (), len);
lastMessageTime = micros ();
setNextAddress (message.address + 1);
processMessage ();
state = STATE_RECENT_RESPONSE;
} // end of message completely received
// switch states if too long a gap between messages
if (micros () - lastMessageTime > noMessagesTimeout)
state = STATE_NO_DEVICES;
else if (micros () - lastCommsTime > PACKET_TIME)
state = STATE_TIMED_OUT;
switch (state)
{
// nothing heard for a long time? We'll take over then
case STATE_NO_DEVICES:
if (micros () - lastCommsTime >= (noMessagesTimeout + randomTime))
{
Serial.println (F("No devices."));
digitalWrite (SEARCHING_PIN, HIGH);
sendMessage ();
digitalWrite (SEARCHING_PIN, LOW);
}
break;
// we heard from another device recently
// if it is our turn, respond
case STATE_RECENT_RESPONSE:
// we allow a small gap, and if it is our turn, we send our message
if (micros () - lastCommsTime >= TIME_BETWEEN_MESSAGES && myAddress == nextAddress)
sendMessage ();
break;
// a device did not respond in its slot time, move onto the next one
case STATE_TIMED_OUT:
digitalWrite (TIMEOUT_PIN, HIGH);
setNextAddress (nextAddress + 1);
lastCommsTime += PACKET_TIME;
digitalWrite (TIMEOUT_PIN, LOW);
state = STATE_RECENT_RESPONSE; // pretend we got the missing response
break;
} // end of switch on state
} // end of loop
В настоящее время при замыкании переключателя на A0 (замыкание на землю) светодиод (контакт 13) на следующем по порядку устройстве выключается. Это подтверждает, что устройства взаимодействуют друг с другом. Конечно, на практике в передаваемом пакете будет что-то более сложное.
В ходе тестирования я обнаружил, что светодиод мгновенно включается и выключается.
Когда все устройства отключены, первое подключенное устройство будет «охотиться» за другими. Если у вас подключены отладочные светодиоды, как у меня, вы можете увидеть, как светодиод «поиска» загорается через случайные интервалы, пока устройство передает пакеты данных со случайно меняющимися интервалами. После подключения второго устройства они успокаиваются и просто обмениваются информацией. Я тестировал с тремя подключенными устройствами одновременно.
Возможно, с HardwareSerial было бы надёжнее — я использовал SoftwareSerial для отладки. Несколько небольших изменений помогут этого добиться.
Измененная схема

Снимки экрана кода в действии
Эти изображения показывают, как работает код. Сначала, с одним подключенным устройством:

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

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

Теперь, когда три устройства подключены к сети, мы видим три блока данных и один пробел, поскольку отсутствующее устройство обойдено.
Если вы проверяете цифры, они были получены при удвоенной скорости передачи данных в качестве теста до 19200 бод.
Испытание прокладки кабеля
Для надлежащего тестирования оборудования я подключил устройства к домашнему кабелю UTP. У меня есть кабель Cat-5, проложенный из разных комнат к центральному распределительному щитку. Если соединить два конца дома (достаточно длинный), всё работает отлично. Для начала, между Arduino и розеткой есть кабель длиной 5 метров. Плюс ещё один кабель длиной 5 метров на другом конце. Затем идут два кабеля длиной примерно по 15 метров от комнат до распределительного помещения, а внутри кабеля есть короткий соединительный кабель для их соединения.
Это было при том, что платы все еще были запрограммированы на работу со скоростью 19200 бод.
1) ОГРОМНОЕ спасибо за ваш ответ и руководство по RS-485. Обязательно им воспользуюсь; 2) Что касается участия MEGA и RPI, я обновил исходный пост. Пожалуйста, обратитесь к нему за подробностями; 3) Что касается «_не вижу особой причины, по которой вы не могли опрашивать ведомые устройства достаточно часто_», является ли 10 опросов в секунду на скорости 9600 бод разумным выбором? И наконец: если я правильно понял, мне вообще **не** понадобится никакой опрос, поскольку с вашим подходом я могу реализовать контекст «один ведущий/несколько ведомых»… но с «подвижным» ведущим. Верно?, @Damiano Verzulli
См. обновленный ответ., @Nick Gammon
См. исправленный ответ, демонстрирующий концепцию движущегося мастера., @Nick Gammon
«Является ли 10 опросов в секунду на скорости 9600 бод разумным выбором?» — в моём текущем тесте (см. скриншоты) я тестирую 4 устройства многократно в течение 75 мс на скорости 19200 бод, то есть получаю 13 отчётов о состоянии в секунду. По сути, это означает, что одно устройство должно реагировать на изменение состояния другого устройства (например, замыкание переключателя) за 1/10 секунды. В вашем первоначальном вопросе упоминалось 4 устройства, поэтому вы должны получить похожие результаты., @Nick Gammon
Не могли бы вы подсказать, где вы взяли белый зажим для провода, который изображен на картинке на красном фоне (зажим прикреплен к жгуту проводов)?, @jsotola
Ebay: https://www.ebay.com/itm/400846299509 . Найдите «10x 2p пружинный соединитель, провод, без сварки, без винта»., @Nick Gammon
Помните, что RS485 — это не протокол, а определение физического транспортного уровня. С выбранным вами Mega вы можете использовать последовательные порты 1, 2 и 3 и запускать их в полнодуплексном режиме через сеть RS485, чтобы вы могли получать то, что отправляете.
У меня он работает в конфигурации с несколькими ведущими. Каждый мегакомпьютер при передаче данных также принимает только что отправленные данные и может побайтно определить, нужно ли ему отступить. Задержка перед возобновлением связи определяется адресом узла и временем, когда шина становится доступной.
Код будет использовать функцию записи вместо функции печати, что позволит проверять каждый байт. (Со временем я, вероятно, сделаю это программно и эмулирую CAN или просто использую контроллер CAN). Да, 8 бит работают отлично, девятый бит — это бит чётности или другое применение, в зависимости от того, кто его определяет.
Моя тестовая система работает с пакетами и отправляет данные в следующем формате:
|Длина|Цель|Источник|Последовательность|Команда|Флаги|Данные|Проверка
и ожидает ACK в ответ. Это относительно простой первый проход, не содержащий недопустимых кодов. Все байты представлены в шестнадцатеричном формате, но вы можете использовать любые другие.
Это даёт мне контроль несущей, множественный доступ, но с деструктивным арбитражем. Если вам нужен CSMANDA, просто используйте физический уровень CAN или побитовое разделение передаваемых данных, и тогда вы сможете проводить арбитраж побитно. Программное обеспечение не будет сильно отличаться, за исключением части передачи.
Я планирую использовать Raspberry Pi с Linux в качестве основного узла. С 8-битными данными всё будет работать отлично. Я начал работать с этой конфигурацией всего две недели назад, и пока всё идёт хорошо. Скорость передачи данных составляет 9600 бод, и свободного времени много.
Гил
Как у вас настроено обнаружение коллизий? Возвращает ли конкретный чип, который вы используете для интерфейса RS-485, измеренные данные по линии RX одновременно с отправкой данных по линии TX, и передаёт ли он их?, @cjs
Если вы хотите использовать протокол Ника Гэммона, вам может пригодиться следующее: https://github.com/Sthing/Nick-Gammon-RS485
Это моя реализация протокола на Python (для использования на RaspberryPi). Пока что я протестировал только Python-Python, следующим в моем списке дел — тестирование с использованием кода Ника Гэммона.
/Вещь
Недавно я успешно тестировал неблокируемый протокол RS485 Ника Гэммона между тремя MEGA в схеме «один ведущий, два ведомых». Я столкнулся с несколькими проблемами, в основном связанными с использованием физических последовательных портов (а не программных). К счастью, мне удалось всё исправить. Я опубликовал работающий код (с некоторыми другими подробностями) здесь: https://github.com/verzulli/arduino-smart-home — сейчас я активно работаю над этим проектом и планирую обновить его в ближайшие недели., @Damiano Verzulli
Думаю, протокол CDBUS для RS485 — это именно то, что вам нужно. Он реализует механизм арбитража, который автоматически предотвращает конфликты, как и шина CAN. Я даже могу передавать по нему видеопоток:
Полное видео:
https://youtu.be/qX5dh4wcfSk
Raspberry Pi может одновременно выводить видеопревью и подавать команды управления. Мы можем отслеживать процесс распознавания на ПК. При возникновении проблем удобно определить причину и скорректировать параметры, а отключение ПК не повлияет на работу демо-устройства.
Кроме того, Raspberry Pi может в любое время получить доступ к Интернету через ПК, а также легко обновлять программное обеспечение и осуществлять удаленное управление.
Подробности о CDBUS:
- https://github.com/dukelec/cdbus_doc (Введение)
- https://github.com/dukelec/cdbus_ip (Сведения о протоколе и IP-ядро ПЛИС)
Обновление:
Подключите контроллер CDCTL-Bx к Arduino:

Упоминание видео не имеет отношения к заданному вопросу и теме Arduino на этом сайте, и поэтому несколько отвлекает от неё. Такой объём данных может создать проблемы в системах с ограниченными ресурсами, если не организовать его элегантно. Если вы хотите предложить эту схему, объясните, как она подходит для решения поставленной задачи, и **для Arduino**... если это действительно так., @Chris Stratton
То же самое касается и Arduino, замените Raspberry Pi Zero на моей схеме на Arduino, другими словами, добавьте внешнюю плату экранирования контроллера RS485 для Arduino, взаимодействуйте через интерфейс I2c или SPI., @dukelec
- Проблема со связью по Modbus между двумя Arduino при записи более 27 регистров.
- Основная связь Arduino ModBus RTU с проблемой измерителя мощности
- Мониторинг контроллера Modbus RTU с помощью Arduino и модуля RS485
- Как заставить I2C работать на RS485?
- Arduino Ethernet Shield при использовании контактов Arduino Mega
- Удаленная загрузка кода на плату Arduino через интернет
- Когда дело доходит до связи UART-RS485, в чем разница между модулем "MAX485" и модулем "HW-0519"?
- Modbus IP с Simply Modbus TCP
Вероятно, вам понадобится подключить к Raspberry Pi устройство, способное осуществлять 9-битную передачу данных по RS-485. Linux не поддерживает 9-битные UART, поэтому без него вам придётся извращаться с битом чётности., @Ignacio Vazquez-Abrams
Что, если я буду подключать через RS485 только ARDUINO? Я имею в виду, что проблема, о которой вы говорите, возникает именно при подключении RPI к RS485, верно? Я спрашиваю, потому что, основываясь на ответе @nick-gammon ниже, я, вероятно, подключу шину RS485 к дополнительному Arduino и соединю RPI с Arduino через Ethernet (а не RS485). В таком случае RPI не потребуется общаться по RS485, и проблема должна исчезнуть, верно?, @Damiano Verzulli
Хочу лишь отметить, что вероятность коллизий пакетов довольно мала. Пакеты отправляются только при нажатии выключателя или когда нужно включить свет. Если вы не живёте на дискотеке, это происходит всего несколько раз в день на каждый выключатель/лампочку., @Gerben
@Gerben: с моими двумя маленькими сыновьями (2,5 и 5,5 лет) могу вас заверить, что такая вероятность НЕ так мала, как можно было бы ожидать в «нормальных» условиях :-). Шутки в сторону, учтите, что если всё пойдёт гладко, я планирую добавить МНОГО дополнительных датчиков/устройств, которые будут опрашиваться через «транспортную систему» (датчики температуры/влажности [по одному на комнату]; датчики открытия/закрытия дверей/окон [минимум два на каждое окно]; счётчики электроэнергии и расхода воды, вероятно, генерирующие «импульсы», которые нужно каким-то образом собрать. Итак, опять же, если произойдёт «столкновение», мои МЕГА «выживут»? Или сгорят?, @Damiano Verzulli