Documented the new simpler transport layer
This commit is contained in:
parent
cf9c945fb6
commit
0be8c0258c
128
README.md
128
README.md
|
@ -6,18 +6,18 @@ SSS7 is the **S**eidenstrasse **S**ignaling **S**ystem 7.
|
||||||
|
|
||||||
This repository contains the schematics its the hardware,
|
This repository contains the schematics its the hardware,
|
||||||
along with a description of the physical layer and the transport layer.
|
along with a description of the physical layer and the transport layer.
|
||||||
The actual application layer will be developed independent of this specification.
|
The actual application layer is developed independently of this project.
|
||||||
|
|
||||||
|
|
||||||
Physical Layer
|
Physical Layer
|
||||||
--------------
|
--------------
|
||||||
The physical layer is based on a RS485-like two wire bus
|
The physical layer is based on a RS485-like two wire bus
|
||||||
(normal RS485 has different levels when idle),
|
(normal RS485 has different levels when idle),
|
||||||
using the *MCP2551* is used as transceiver chip.
|
using the *MCP2551* is used as transceiver chip.
|
||||||
Since this chip is actually a CAN-transceiver,
|
Since this chip is actually a CAN-transceiver,
|
||||||
it has a well defined behaviour in case of collisions (no short circuits possible)
|
it has a well defined behaviour in case of collisions
|
||||||
and it allows to read back the bits which were actually on the bus while sending.
|
and it allows to read back the bits which were actually on the bus while sending.
|
||||||
Additionally for most regular RS485 transceivers a collision on the bus results
|
For regular RS485 transceivers a collision on the bus results
|
||||||
in one transceiver pulling the bus while the other one pulls it high,
|
in one transceiver pulling the bus while the other one pulls it high,
|
||||||
effectively creating a temporary short circuit.
|
effectively creating a temporary short circuit.
|
||||||
Therefore using a CAN-transceiver is preferable for our bus configuration.
|
Therefore using a CAN-transceiver is preferable for our bus configuration.
|
||||||
|
@ -25,70 +25,101 @@ Therefore using a CAN-transceiver is preferable for our bus configuration.
|
||||||
The MCP2551 requires that the levels of the differential data lines are within +-7V
|
The MCP2551 requires that the levels of the differential data lines are within +-7V
|
||||||
of its ground potential.
|
of its ground potential.
|
||||||
This a bit of a problem, since it is not really practical to run an additional wire
|
This a bit of a problem, since it is not really practical to run an additional wire
|
||||||
along the bus to provide a common.
|
along the bus to provide a common ground.
|
||||||
Additionally routers may use power supplies which do not have a floating potential
|
Additionally routers may use power supplies which do not have a floating ground potential
|
||||||
(e.g. a computer power supply).
|
(e.g. a computer power supply).
|
||||||
Connecting the ground rails of two non-floating power supplies can lead to a nasty
|
Connecting the ground rails of two non-floating power supplies will lead to a problems.
|
||||||
short circuit in the worst case.
|
Therefore an isolated DC/DC converter is used to provide a floating supply voltage
|
||||||
Therefore an isolated DC/DC converter is used to provide a floating supply voltage
|
|
||||||
for the transceiver chip and the data lines are isolated using optocouplers.
|
for the transceiver chip and the data lines are isolated using optocouplers.
|
||||||
Still a common ground potential for all modes is required for the transceiver chips to work.
|
Still a common ground potential for all nodes is required for the transceiver chips to work.
|
||||||
This is solved by adding diodes in inverse direction between the floating 5v rail and both data lines
|
This is solved by adding diodes in blocking direction between the floating 5v rail and both data lines
|
||||||
and between the chips ground and bot datalines.
|
and between the chips ground and both data lines.
|
||||||
If the potential of the high data line is more than 0.7v above the floating 5v rail,
|
If the potential of the high data line is more than 0.7v above the floating 5v rail,
|
||||||
on of diodes will become conducting effectively shifting the floating grounds potential up.
|
on of diodes will become conducting effectively shifting the floating grounds potential up.
|
||||||
Likewise if the potential of the low data line is more than 0.7v below the floating ground rail,
|
Likewise if the potential of the low data line is more than 0.7v below the floating ground rail,
|
||||||
the ground rail will be shifted down.
|
the ground rail will be shifted down.
|
||||||
Hence the floating grounds rails of all modems connected to bus will be within +-0.7v relative to each other
|
Hence the floating grounds rails of all modems connected to bus will be within +-0.7v
|
||||||
(not considering the voltage drop over the long wires).
|
relative to each other(not considering the voltage drop over the long wires).
|
||||||
|
|
||||||
Additionally these diodes suppress voltages spikes on the bus,
|
Additionally these diodes suppress voltages spikes on the bus,
|
||||||
protecting the transceiver.
|
protecting the transceiver.
|
||||||
|
|
||||||
Using a sss7modem any device with a decent UART (microcontroller, raspi ...)
|
Using a sss7modem any device with a decent UART (microcontroller, raspi ...)
|
||||||
can communicate over long distances (1000m+).
|
can communicate over long distances (1000m+).
|
||||||
The data is send at 9600 Baud (aka. 9,6kbit/s) with a maximum baudrate error of +-5%,
|
The data is send at 9600 Baud (aka. 9,6kbit/s) with a maximum baudrate error of +-5%,
|
||||||
to ensure that even the slowest microcontroller can participate in the communication.
|
to ensure that even the slowest microcontroller can participate in the communication.
|
||||||
A slow bitrate also necessary to keep communication and collision detection reliable over longer
|
A slow bitrate also necessary to keep communication and collision detection reliable over longer
|
||||||
distances.
|
distances and allows to use slower (and cheaper) optocouplers.
|
||||||
|
|
||||||
|
|
||||||
Transport Layer
|
Transport Layer
|
||||||
---------------
|
---------------
|
||||||
A device is allowed to send data after the bus has been idle for at least *48 bit times* (aka. 5ms).
|
|
||||||
After this period the sender is allowed to send a single frame.
|
|
||||||
The frame format is simple: The first two bytes are a always 0xAAFE.
|
|
||||||
This preamble allows the uart to synchronise to the incoming data.
|
|
||||||
The bit pattern also increases the chances,
|
|
||||||
that a collision of two preambles results in frame error.
|
|
||||||
The next byte is the length of the payload in bytes,
|
|
||||||
followed by the actual payload.
|
|
||||||
The remaining 2 bytes are a 16bit CRC checksum over the frames payload.
|
|
||||||
If a received frame has timed out or if its CRC sum does not match its payload,
|
|
||||||
any receiving devices should assume a collision and drop the frame.
|
|
||||||
A frame can be assumed as timed out if it was not fully received yet and no
|
|
||||||
additional data has been received for at least *24 bit times* (aka. 2.5ms).
|
|
||||||
Furthermore each frame is associated to a priority between 0 (highest) and 5 (lowest),
|
|
||||||
which is not part of the frame send over the bus.
|
|
||||||
Instead the priority is used to calculate the necessary back off in case of a collision.
|
|
||||||
|
|
||||||
The sender has to detect collisions by reading back each byte written to the bus immediately after writing it.
|
|
||||||
If the read byte does not match the written byte, a collision occurred.
|
|
||||||
In this case the sender has to wait for random back off interval until attempting a retransmit.
|
|
||||||
The back off interval is calculated as `(timeout * bit_time) * (priority + rand(1,5))`,
|
|
||||||
where `bit_time = 1 / 9600 s` and priority is the frames priority.
|
|
||||||
After the backoff interval the bus has to be idle for *48 bit times* before a retransmit.
|
|
||||||
|
|
||||||
Devices receiving on the bus are not necessarily able to detect collisions,
|
### Frame format:
|
||||||
unless they show up as timing error at their UART.
|
``` 0xAA 0xFE <payload 16 bytes> <payload crc 1byte> ```
|
||||||
Therefore receiving devices should rely only on the length and the CRC sum of frame to detect collisions.
|
|
||||||
|
16 bytes total
|
||||||
|
|
||||||
|
**Header: 0xAA 0xFE**
|
||||||
|
- Choosen emperically to help the Uarts to synchronize
|
||||||
|
- Improves detection of two headers colliding
|
||||||
|
|
||||||
|
**CRC-Type: Maxim iButton 8-bit**
|
||||||
|
Polynomial: x^8 + x^5 + x^4 + 1 (0x8C)
|
||||||
|
Initial value: 0x0
|
||||||
|
|
||||||
|
### Communication Protocol
|
||||||
|
|
||||||
|
- **Not final, use with caution**
|
||||||
|
- Frames are sent using standard RS232 Uarts connected to sss7 modems
|
||||||
|
- Uarts should be configured to: 9600baud, 8bits, 1 stop bit, no flow-control, no parity
|
||||||
|
- Every node can send a frame at an arbitrary point in time,
|
||||||
|
unless it is currently receiving a frame.
|
||||||
|
- Collision detection on the sender side is done by reading back each sent byte.
|
||||||
|
- Collision checking on receiver side, is done by checking the CRC and frame header.
|
||||||
|
- If a frame has been started and there are no new bytes received for 20ms,
|
||||||
|
the frame is considered timed out and all received data can be dropped.
|
||||||
|
- Incoming messages are stored in a fifo until the application retrieves them.
|
||||||
|
- The receive fifo has a size of at least 2 messages.
|
||||||
|
- If the fifo is full new messages will be dropped, until the application retrieves a message.
|
||||||
|
- Even if sending was successful, there is still a chance that the receiver could not
|
||||||
|
receive the frame due to missing buffer space or not enough processing time to react. **Important messages should utilize a ack-mechanism.**
|
||||||
|
- It is up to the application to resend messages in case of a collision/missing ack.
|
||||||
|
|
||||||
|
### Planned API
|
||||||
|
- **Not final, use with caution**
|
||||||
|
- All calls are non-blocking,
|
||||||
|
any real work should be done by ISRs or an eventloop, which is called peridically.
|
||||||
|
|
||||||
|
- **sss7_can_send()**
|
||||||
|
|
||||||
|
Checks wether we see the bus as idle.
|
||||||
|
Return true iff. we neither are currently receiving a frame
|
||||||
|
nor are sending a frame of our own.
|
||||||
|
|
||||||
|
- **sss7_send_failed()**
|
||||||
|
|
||||||
|
Checks the send error flag.
|
||||||
|
Returns true if the last attempt at sending a frame failed.
|
||||||
|
An attempt is considered failed if the Uart reads back a different byte than the byte send out.
|
||||||
|
This indicates either a collision or a hardware failure.
|
||||||
|
|
||||||
|
- **sss7_send(payload)**
|
||||||
|
|
||||||
|
Constructs a frame around the payload and prepares for sending.
|
||||||
|
The actual sending is either done in the ISR or by the eventloop, depending on the platform.
|
||||||
|
The frame is completely sent iff. ```sss7_can_send() && !sss7_send_failed()``` holds.
|
||||||
|
|
||||||
|
- **sss7_has_received()**
|
||||||
|
|
||||||
|
Returns True iff. the receive fifo contains at least one message.
|
||||||
|
|
||||||
|
- **sss7_get_received(payload)**
|
||||||
|
|
||||||
|
Retrieves the oldest message from the receive fifo.
|
||||||
|
It is up the application to provide sufficient space in payload.
|
||||||
|
Does nothing sss7_has_received() is false.
|
||||||
|
|
||||||
Even though the collision detection theoretically ensures that there are no damaged frames due to collisions,
|
|
||||||
there might still be frames with a broken crc sum or wrong length due to electrical interference on the bus.
|
|
||||||
For autorouters this should not be a problem since they will need mechanism for acknowledging the successful
|
|
||||||
execution of actions anyway.
|
|
||||||
Therefore a lost frame will either result in a command not being executed or in command being executed but not acked.
|
|
||||||
Both cases can be handled efficiently by the resending the command of no ack was received.
|
|
||||||
|
|
||||||
Licenses
|
Licenses
|
||||||
--------
|
--------
|
||||||
|
@ -103,4 +134,3 @@ Therefore you should ask for permission before using the seidenstrasse logo in d
|
||||||
Also please remove my personal logo if I am not involved in your project.
|
Also please remove my personal logo if I am not involved in your project.
|
||||||
|
|
||||||
The schematics and pcb layout files in hardware are released under the *CERN Open Hardware Licence v1.2*.
|
The schematics and pcb layout files in hardware are released under the *CERN Open Hardware Licence v1.2*.
|
||||||
|
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
#!/usr/bin/env python2
|
|
||||||
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
|
|
||||||
from sss7bus import SSS7Bus
|
|
||||||
|
|
||||||
|
|
||||||
bus = SSS7Bus('/dev/ttyUSB0')
|
|
||||||
|
|
||||||
framecount = 0
|
|
||||||
lost = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
bus.send_message("Hello World!")
|
|
||||||
|
|
||||||
more = True
|
|
||||||
while more:
|
|
||||||
msg = bus.read_message()
|
|
||||||
print msg
|
|
||||||
print framecount
|
|
||||||
frame = int(msg.split(':')[1])
|
|
||||||
delta = frame - framecount
|
|
||||||
if delta > 1 and framecount <> 0:
|
|
||||||
lost += delta
|
|
||||||
|
|
||||||
framecount = frame
|
|
||||||
|
|
||||||
if frame % 100 == 0 and frame > 0:
|
|
||||||
print "Lost Frames: %d %d %f" % (frame, lost, 100.0 * lost / frame)
|
|
||||||
|
|
||||||
more = bus.has_message()
|
|
||||||
|
|
||||||
time.sleep((500.0 + random.randint(0,500)) / 1000.0)
|
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/env python2
|
|
||||||
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
|
|
||||||
from sss7bus import SSS7Bus
|
|
||||||
|
|
||||||
|
|
||||||
bus = SSS7Bus('/dev/ttyUSB1')
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
bus.send_message("Timestamp:%d:%d" % (counter, time.time()))
|
|
||||||
while bus.has_message():
|
|
||||||
bus.read_message()
|
|
||||||
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
time.sleep((500.0 + random.randint(0,500)) / 1000.0)
|
|
|
@ -1,3 +0,0 @@
|
||||||
crcmod==1.7
|
|
||||||
pyserial==2.7
|
|
||||||
wheel==0.24.0
|
|
|
@ -1,160 +0,0 @@
|
||||||
#!/usr/bin/env python2
|
|
||||||
|
|
||||||
import serial
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
from crcmod.predefined import PredefinedCrc
|
|
||||||
|
|
||||||
|
|
||||||
# Timings in bit times
|
|
||||||
TIMEOUT = 48
|
|
||||||
CLEARCHANNEL = 2 * TIMEOUT
|
|
||||||
|
|
||||||
|
|
||||||
class SSS7Bus(object):
|
|
||||||
|
|
||||||
def __init__(self, port, baudrate=9600):
|
|
||||||
random.seed()
|
|
||||||
|
|
||||||
self._bit_time = 1.0 / baudrate
|
|
||||||
|
|
||||||
self._serial = serial.Serial(port, baudrate, timeout=self._bit_time * TIMEOUT)
|
|
||||||
|
|
||||||
self._buffer = []
|
|
||||||
|
|
||||||
|
|
||||||
def _debug(self, msg):
|
|
||||||
print "[SSS7Bus] %s" % msg
|
|
||||||
|
|
||||||
|
|
||||||
def _flush_input_buffer(self):
|
|
||||||
while self._serial.inWaiting() > 0:
|
|
||||||
self._debug("Flushing input buffer")
|
|
||||||
self._read_frame()
|
|
||||||
|
|
||||||
|
|
||||||
def _can_send(self):
|
|
||||||
self._flush_input_buffer()
|
|
||||||
|
|
||||||
self._serial.timeout = self._bit_time * CLEARCHANNEL
|
|
||||||
self._debug("Checking if bus is idle for %f seconds" % (self._serial.timeout))
|
|
||||||
|
|
||||||
first_byte = self._serial.read(1)
|
|
||||||
if len(first_byte) == 0:
|
|
||||||
self._debug("Bus seems idle")
|
|
||||||
return True
|
|
||||||
|
|
||||||
self._debug("Bus is not idle, reading frames")
|
|
||||||
self._read_frame_rest(first_byte)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _read_frame(self):
|
|
||||||
self._debug("Trying to read a frame from the bus")
|
|
||||||
self._serial.timeout = timeout=self._bit_time * TIMEOUT
|
|
||||||
|
|
||||||
first_byte = self._serial.read(1)
|
|
||||||
if len(first_byte) == 0:
|
|
||||||
self._debug("Timeout reading first byte")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self._read_frame_rest(first_byte)
|
|
||||||
|
|
||||||
|
|
||||||
def _read_frame_rest(self, first_byte):
|
|
||||||
if ord(first_byte) <> 0xAA:
|
|
||||||
self._debug("Wrong first byte: %s" % hex(ord(first_byte)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
second_byte = self._serial.read(1)
|
|
||||||
if len(second_byte) == 0:
|
|
||||||
self._debug("Timeout reading second byte")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if ord(second_byte) <> 0xFE:
|
|
||||||
self._debug("Wrong second byte")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
length_byte = self._serial.read(1)
|
|
||||||
if len(length_byte) == 0:
|
|
||||||
self._debug("Timeout reading length byte")
|
|
||||||
return False
|
|
||||||
|
|
||||||
length = ord(length_byte) + 2 # Payload length + 2 byte CRC16
|
|
||||||
|
|
||||||
self._serial.timeout = timeout=self._bit_time * TIMEOUT
|
|
||||||
|
|
||||||
self._debug("Trying to read remaing %d bytes of frame" % length)
|
|
||||||
data = self._serial.read(length)
|
|
||||||
|
|
||||||
# if read returns less then length, no new byte has been received for timeout seconds
|
|
||||||
if len(data) != length:
|
|
||||||
self._debug("Timeout reading frame payload")
|
|
||||||
return False
|
|
||||||
|
|
||||||
crc = data[-2:]
|
|
||||||
payload = data[:-2]
|
|
||||||
|
|
||||||
crc16 = PredefinedCrc('crc16')
|
|
||||||
crc16.update(payload)
|
|
||||||
real_crc = crc16.digest()
|
|
||||||
|
|
||||||
if real_crc != crc:
|
|
||||||
self._debug("Wrong crc for frame payload")
|
|
||||||
return True
|
|
||||||
|
|
||||||
self._debug("Sucessfully read rest of frame")
|
|
||||||
self._buffer.insert(0,payload)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _send_byte(self, byte):
|
|
||||||
self._serial.write(byte)
|
|
||||||
return byte == self._serial.read(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _send_frame(self, frame):
|
|
||||||
while not self._can_send():
|
|
||||||
pass
|
|
||||||
|
|
||||||
for byte in frame:
|
|
||||||
result = self._send_byte(byte)
|
|
||||||
if not result:
|
|
||||||
self._debug("Sending frame failed with collision")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._debug("Successfully send frame")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def send_message(self, msg, priority=5):
|
|
||||||
if len(msg) > 253:
|
|
||||||
raise ValueError("Message length can not exceed 253 byte")
|
|
||||||
|
|
||||||
crc16 = PredefinedCrc('crc16')
|
|
||||||
crc16.update(msg)
|
|
||||||
msg_crc = crc16.digest()
|
|
||||||
|
|
||||||
frame = chr(0xAA) + chr(0xFE) + chr(len(msg)) + msg + msg_crc
|
|
||||||
|
|
||||||
result = self._send_frame(frame)
|
|
||||||
|
|
||||||
while not result:
|
|
||||||
backoff = TIMEOUT * self._bit_time * (4 + priority + random.randint(1,5))
|
|
||||||
self._debug("Collision occured backing off %f" % backoff)
|
|
||||||
time.sleep(backoff)
|
|
||||||
result = self._send_frame(frame)
|
|
||||||
|
|
||||||
|
|
||||||
def has_message(self):
|
|
||||||
return len(self._buffer) > 0
|
|
||||||
|
|
||||||
def read_message(self):
|
|
||||||
while not self.has_message():
|
|
||||||
self._read_frame()
|
|
||||||
|
|
||||||
return self._buffer.pop()
|
|
Loading…
Reference in New Issue