Ich habe einen langen Beitrag über RS485 geschrieben .
Erstens scheint Ihre Verwendung von Megas ein Overkill zu sein, es sei denn, Sie haben sie bereits zur Hand. Ein Uno oder eine der kleineren Formfaktor-Karten scheint vollkommen ausreichend zu sein, um einige Schalter zu überwachen und ein paar Lichter einzuschalten.
Sogar das RPI scheint unnötig. Ein anderer Uno kann problemlos Ihre RS485-Leitungen überwachen und über Ethernet (eine Ethernet-Abschirmung) eine Verbindung zum Rest Ihres Hauses herstellen oder was auch immer Sie tun.
... es gibt keine Bestimmung, um "Kollisionen" bei der Kommunikation zu vermeiden ...
Nun, Sie bauen das ein. Sie geben jedem Mega eine Adresse (z. B. im EEPROM gespeichert) und "adressieren" dann die gewünschte Adresse und warten dann auf eine Antwort. Zum Beispiel im Code von meiner obigen Seite:
Meister
#include "RS485_protocol.h"
#include <SoftwareSerial.h>
const byte ENABLE_PIN = 4;
const byte LED_PIN = 13;
SoftwareSerial rs485 (2, 3); // receive pin, transmit pin
// callback routines
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); // driver output enable
pinMode (LED_PIN, OUTPUT); // built-in LED
} // end of setup
byte old_level = 0;
void loop()
{
// read potentiometer
byte level = analogRead (0) / 4;
// no change? forget it
if (level == old_level)
return;
// assemble message
byte msg [] = {
1, // device 1
2, // turn light on
level // to what level
};
// send to slave
digitalWrite (ENABLE_PIN, HIGH); // enable sending
sendMsg (fWrite, msg, sizeof msg);
digitalWrite (ENABLE_PIN, LOW); // disable sending
// receive response
byte buf [10];
byte received = recvMsg (fAvailable, fRead, buf, sizeof buf);
digitalWrite (LED_PIN, received == 0); // turn on LED if error
// only send once per successful change
if (received)
old_level = level;
} // end of loop
Sie konfigurieren den RS485-Transceiver so, dass er entweder sendet oder empfängt. Normalerweise befindet es sich im Empfangsmodus und Sie wechseln in den Sendemodus, um ein "Datenpaket" zu senden. (Siehe "Senden aktivieren" oben).
Jetzt schaltet das adressierte Gerät seinen Transceiver in den "Send" -Modus und antwortet. Der Code in der Bibliothek, den ich geschrieben habe, hat eine Zeitüberschreitung. Wenn also dieser bestimmte Slave tot ist, läuft der Empfang ab. Vielleicht erinnern Sie sich am Master-Ende daran und versuchen, seltener damit zu kommunizieren. Oder es ist Ihnen egal, ob die Auszeit kurz ist.
Sklave
#include <SoftwareSerial.h>
#include "RS485_protocol.h"
SoftwareSerial rs485 (2, 3); // receive pin, transmit pin
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); // driver output enable
}
void loop()
{
byte buf [10];
byte received = recvMsg (fAvailable, fRead, buf, sizeof (buf));
if (received)
{
if (buf [0] != 1)
return; // not my device
if (buf [1] != 2)
return; // unknown command
byte msg [] = {
0, // device 0 (master)
3, // turn light on command received
};
delay (1); // give the master a moment to prepare to receive
digitalWrite (ENABLE_PIN, HIGH); // enable sending
sendMsg (fWrite, msg, sizeof msg);
digitalWrite (ENABLE_PIN, LOW); // disable sending
analogWrite (11, buf [2]); // set light level
} // end if something received
} // end of loop
Hinweis : Der Beispielcode von meiner verlinkten Seite liest die Adresse nicht aus dem EEPROM, dies ist jedoch trivial zu implementieren.
Ich kann keinen bestimmten Grund erkennen, warum Sie die Sklaven nicht häufig befragen konnten. Was würde der Meister sonst noch tun? Sie können den Master auch als HTTP-Server einrichten, um von Ihrem Laptop oder einem anderen Teil des Hauses aus mit ihm zu kommunizieren.
Meine Verkabelung:
Um die Idee zu demonstrieren, habe ich zwei Unos aufgestellt, die über ca. 8 m Glockendraht verbunden sind (nicht einmal Twisted Pair und schon gar nicht abgeschirmt).
Ein Uno führte die Standard-ASCII-Tabellenskizze (bei 9600 Baud) aus, sein Tx-Pin ging in den LTC1480 und dann gingen die A / B-Pins zum Glockendraht.
Das andere Uno war als USB-Schnittstelle angeschlossen (Reset mit Masse verbunden) und gab nur das wieder, was auf dem Tx-Pin des USB-Anschlusses angekommen war.
Soweit ich sehen konnte, hat es perfekt funktioniert.
Ich brauche überhaupt keine Umfrage, da ich mit Ihrem Ansatz einen "Single-Master / Multiple-Slave" -Kontext implementieren kann ... aber mit einem "Moving" -Master. Ist das richtig?
Meine obige Antwort ging davon aus, dass die Slaves eher versagen als der Master (ich kann mir nicht vorstellen, warum dies passieren würde, aber vielleicht aufgrund der Tatsache, dass es mehr Slaves als Master gibt und der Master Dinge wie Lichter nicht kontrolliert ).
Ich finde meine Arduinos äußerst zuverlässig, wenn ich einfache Dinge tue (wie das Aufschließen einer Tür, wenn eine RFID-Karte vorgelegt wird).
Es ist denkbar, dass Sie eine Rückfallposition in die Sklaven einbauen. Wenn sie jede Sekunde abgefragt werden und dann keine Umfrage eintrifft, könnten sie möglicherweise versuchen, als Master zu fungieren, möglicherweise in aufsteigender Reihenfolge der Gerätenummer, um Konflikte zu vermeiden. Ein Teil dieser Abfrage durch den "neuen Master" könnte darin bestehen, den "ursprünglichen Master" zu überprüfen, um festzustellen, ob er bereit ist, seine Aufgaben wieder aufzunehmen.
In die Bibliothek, die ich auf meiner verlinkten Seite beschrieben habe, ist eine Fehlerprüfung integriert. Die Idee ist, dass Sie durch Überprüfen einer CRC für das Paket sicherstellen, dass Sie nicht auf halbem Weg durch ein Paket kommen und die darin enthaltenen Daten falsch interpretieren.
Sie können auch zufällige Zufallszeiten einbauen, um einen Deadlock zu beheben, wenn zwei Slaves gleichzeitig versuchen, Master zu werden. Wenn ein Slave ausfällt, kann er eine zufällige (und zunehmende) Zeit warten, bevor er erneut versucht, einem anderen Slave die Möglichkeit zu geben, dies zu tun.
Ich wollte nur beachten, dass die Wahrscheinlichkeit von Paketkollisionen eher gering ist. Sie senden Pakete nur, wenn ein Schalter gedrückt wird oder wenn ein Licht eingeschaltet werden muss.
Gerben hat recht, aber ich würde mir Sorgen machen, dass eine Benachrichtigung über einen Wechsel unbemerkt blieb. Eine mögliche Problemumgehung besteht darin, dass die Slaves nicht mit Statusänderungen auf eine Abfrage antworten, sondern mit dem aktuellen Status antworten. So könnte es gehen:
Master: Slave 3, what is your status?
Slave 3: Lights 1 and 4 on, lights 2 and 3 off.
Ich brauche überhaupt keine Umfrage, da ich mit Ihrem Ansatz einen "Single-Master / Multiple-Slave" -Kontext implementieren kann ... aber mit einem "Moving" -Master. Ist das richtig?
Ich habe ein bisschen darüber nachgedacht, und ich denke, jetzt könnten Sie ein System erstellen, das im Grunde genommen masterfrei ist. Es könnte so funktionieren:
Jedes Gerät hat eine eigene Adresse, die es vom EEPROM (oder den DIP-Schaltern) erhält. z.B. 1, 2, 3, 4, 5 ...
Sie wählen einen Adressbereich aus, den Sie verwenden möchten (z. B. maximal 10).
Wenn das Gerät eingeschaltet wird, lauscht es zuerst auf andere Geräte, die auf dem Bus "sprechen". Hoffentlich hört es mindestens einen anderen (wenn nicht, siehe unten).
Wir entscheiden uns für ein festes "Nachrichtenpaket", beispielsweise von 50 Bytes, einschließlich Adresse, CRC usw. Bei 9600 Baud würde das Senden 52 ms dauern.
Jedes Gerät erhält einen "Zeitschlitz" und wartet, bis es an der Reihe ist, mit dem Bus zu sprechen.
Wenn sein Zeitschlitz eintrifft, wechselt er in den Ausgabemodus und sendet sein Paket, das seine eigene Adresse enthält. Daher können jetzt alle anderen Geräte ihren Status lesen (und gegebenenfalls darauf reagieren). Z.B. Gerät 1 meldet möglicherweise, dass Schalter 3 geschlossen ist, was bedeutet, dass Gerät 2 ein Licht einschalten muss.
Im Idealfall wissen Sie, dass Ihr Zeitfenster angekommen ist, da Ihre Geräteadresse um eins größer ist als das Paket, das Sie gerade abgehört haben. Z.B. Sie sind Gerät 3. Sie haben gerade gehört, dass Gerät 2 seinen Status ankündigt. Jetzt bist du dran. Natürlich wickeln Sie sich mit der maximalen Anzahl um, also kehren Sie nach Gerät 10 zu Gerät 1 zurück.
Wenn ein Gerät fehlt und nicht antwortet, geben Sie ihm (sagen wir) einen halben Zeitschlitz, um zu antworten, und nehmen dann an, dass es tot ist, und jedes Gerät im Bus geht jetzt davon aus, dass der nächste Zeitschlitz gestartet wurde. (z. B. Sie haben gehört, dass Gerät 2, Gerät 3 antworten sollte. Nach 25 ms Inaktivität kann Gerät 4 jetzt antworten.) Diese Regel gibt einem Gerät 25 ms Zeit, um zu antworten. Dies sollte ausreichend sein, selbst wenn es einen Interrupt oder ähnliches bedient.
Wenn mehrere Geräte nacheinander fehlen, zählen Sie für jedes fehlende Gerät eine Lücke von 25 ms, bis Sie an der Reihe sind.
Sobald Sie mindestens eine Antwort erhalten, wird das Timing neu synchronisiert, sodass jede Drift in den Uhren aufgehoben wird.
Die einzige Schwierigkeit besteht darin, dass beim ersten Einschalten (was gleichzeitig passieren kann, wenn die Stromversorgung des Gebäudes unterbrochen und dann wiederhergestellt wird) derzeit kein Gerät seinen Status sendet und daher nichts synchronisiert werden kann.
In diesem Fall:
Wenn das Gerät lange genug gehört hat, um alle Geräte zu hören (z. B. 250 ms) und nichts zu hören, geht es vorläufig davon aus, dass es das erste ist, und sendet eine Sendung. Möglicherweise tun dies jedoch zwei Geräte gleichzeitig und hören sich daher nie.
Wenn ein Gerät nichts von einem anderen Gerät gehört hat, wird die Zeit zwischen den Sendungen zufällig verschoben (möglicherweise wird der Zufallszahlengenerator aus seiner Gerätenummer entfernt, um zu vermeiden, dass alle Geräte die Sendungen "zufällig" um denselben Betrag versetzen).
Diese zufällige Staffelung um zusätzliche Zeit spielt keine Rolle, da sowieso niemand zuhört.
Früher oder später wird ein Gerät den Bus exklusiv nutzen und die anderen können dann auf die übliche Weise mit ihm synchronisieren.
Diese zufällige Lücke zwischen Kommunikationsversuchen ähnelt der von Ethernet, wenn mehrere Geräte ein Koaxialkabel gemeinsam nutzen.
Demo des masterfreien Systems
Dies war eine interessante Herausforderung, daher habe ich eine Demo zusammengestellt, in der dies ohne einen bestimmten Master durchgeführt wurde, wie oben beschrieben.
Zuerst müssen Sie die aktuelle Geräteadresse und die Anzahl der Geräte im EEPROM einrichten. Führen Sie diese Skizze aus und ändern Sie sie myAddress
für jedes 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 () { }
Laden Sie dies nun auf jedes Gerät hoch:
/*
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
Wenn Sie derzeit einen Schalter an A0 (Kurzschluss nach Masse) schließen, wird nacheinander eine LED (Pin 13) am nächsthöheren Gerät ausgeschaltet. Dies beweist, dass die Geräte miteinander sprechen. In der Praxis hätten Sie natürlich etwas Anspruchsvolleres in dem Paket, das gesendet wird.
Beim Testen stellte ich fest, dass die LED sofort ein- und ausgeschaltet zu sein schien.
Wenn alle Geräte getrennt sind, "sucht" das erste angeschlossene nach anderen Geräten. Wenn Sie Debugging-LEDs wie ich angeschlossen haben, können Sie sehen, dass die "Such" -LED in zufälligen Intervallen aufleuchtet, wenn sie ihr Paket mit zufällig variierenden Lücken sendet. Sobald Sie eine zweite angeschlossen haben, beruhigen sie sich und tauschen einfach Informationen aus. Ich habe mit drei gleichzeitig verbundenen getestet.
Mit HardwareSerial wäre es wahrscheinlich zuverlässiger - ich habe SoftwareSerial verwendet, um beim Debuggen zu helfen. Ein paar kleine Änderungen würden das erreichen.
Geänderter Schaltplan
Screenshots des Codes in Aktion
Diese Bilder zeigen, wie der Code funktioniert. Erstens, wenn nur ein Gerät angeschlossen ist:
An den dortigen Impulsen können Sie erkennen, dass das Gerät seine Daten in zufällig variierenden Intervallen sendet, um zu vermeiden, dass es weiterhin zu Konflikten mit einem anderen Gerät kommt, das im selben Moment eingeschaltet wurde.
Jetzt sehen wir Datenblöcke von zwei Geräten mit ungefähr gleich großen Lücken in der Mitte. Ich habe es für vier Geräte konfiguriert, aber nur zwei sind vorhanden, sodass wir zwei Datenblöcke und zwei Lücken sehen.
Jetzt, da drei Geräte online sind, sehen wir drei Datenblöcke und eine Lücke, da das fehlende Gerät umgangen wird.
Wenn Sie die Zahlen überprüfen, wurden diese mit der Baudrate genommen, die als Test auf 19200 Baud verdoppelt wurde.
Kabelführungstest
Für einen ordnungsgemäßen Hardwaretest habe ich die Geräte an meine interne UTP-Verkabelung angeschlossen. Ich habe ein Cat-5-Kabel, das von verschiedenen Räumen zu einer zentralen Steckdose führt. Wenn Sie von einem Ende des Hauses zum anderen gehen (eine angemessene Länge), funktioniert es immer noch einwandfrei. Zunächst befindet sich zwischen dem Arduino und der Wandsteckdose ein 5 m langes Kabel. Plus ein weiteres 5 m Kabel am anderen Ende. Dann gibt es ungefähr 2 x 15 m Läufe von den Räumen zum Schaltraum, und darin befindet sich ein kurzes Überbrückungskabel, um sie miteinander zu verbinden.
Dies war mit den Boards noch programmiert, um mit 19200 Baud zu laufen.