Documented the new simpler transport layer

This commit is contained in:
Sebastian 2016-10-26 17:52:29 +02:00
parent cf9c945fb6
commit 0be8c0258c
5 changed files with 79 additions and 266 deletions

128
README.md
View File

@ -6,18 +6,18 @@ SSS7 is the **S**eidenstrasse **S**ignaling **S**ystem 7.
This repository contains the schematics its the hardware,
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
--------------
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),
using the *MCP2551* is used as transceiver chip.
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.
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,
effectively creating a temporary short circuit.
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
of its ground potential.
This a bit of a problem, since it is not really practical to run an additional wire
along the bus to provide a common.
Additionally routers may use power supplies which do not have a floating potential
along the bus to provide a common ground.
Additionally routers may use power supplies which do not have a floating ground potential
(e.g. a computer power supply).
Connecting the ground rails of two non-floating power supplies can lead to a nasty
short circuit in the worst case.
Therefore an isolated DC/DC converter is used to provide a floating supply voltage
Connecting the ground rails of two non-floating power supplies will lead to a problems.
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.
Still a common ground potential for all modes 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
and between the chips ground and bot datalines.
Still a common ground potential for all nodes is required for the transceiver chips to work.
This is solved by adding diodes in blocking direction between the floating 5v rail and both data lines
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,
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,
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
(not considering the voltage drop over the long wires).
Hence the floating grounds rails of all modems connected to bus will be within +-0.7v
relative to each other(not considering the voltage drop over the long wires).
Additionally these diodes suppress voltages spikes on the bus,
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+).
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.
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
---------------
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,
unless they show up as timing error at their UART.
Therefore receiving devices should rely only on the length and the CRC sum of frame to detect collisions.
### Frame format:
``` 0xAA 0xFE <payload 16 bytes> <payload crc 1byte> ```
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
--------
@ -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.
The schematics and pcb layout files in hardware are released under the *CERN Open Hardware Licence v1.2*.

View File

@ -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)

View File

@ -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)

View File

@ -1,3 +0,0 @@
crcmod==1.7
pyserial==2.7
wheel==0.24.0

View File

@ -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()