RepRap SNAP Protocol Documentation
Introduction
This is an implementation of the
SNAP Protocol in a ring network.
In a
RepRap, all of the electronics are comprised of fairly independent
modules. In order for the modules to co-operate and communicate there
is a network connecting each module and also connecting to the host PC.
The SNAP protocol is a simple, general purpose network protocol to provide a simple high
level interface to the network communications. The idea is that the
interface is abstract enough so that if we change the underlying network
architecture at some point in the future, the library can be swapped and
the rest of the software in each module will remain unchanged. More
specifically, it currently implements a ring topology packet based
network and is used to send simple small packets between the devices on
the network. There are plans in the future to possibly change this to a
more efficient bus topology, but that's probably some time away.
If you are looking for information about the specific commands, see the
SNAP Command Documentation.
Protocol Description
This protocol is based upon the idea of a token ring network. This is not really a true token ring in that
there is no token frame and the procedure for ring insertion etc.
is trivial -- it is however a network with ring topology. This network can be as simple as a 2 node network with one device and one 'master' or 'host' sending commands.
Overview
A receive buffer accepts payload data as it arrives. Upon
completion, a global flag is set that acts as a lock to prevent
further receives occurring until the lock is removed. If a receive
does occur, but it is for somebody else, it is passed onto the next
node in the loop. If the receive is for ourself then we fail and
NAK the packet so that it will be re-sent at a time we can
hopefully act on it.
The lock flag also indicates to the main loop that data is awaiting
and the main loop is responsible for calling any processing on the
data. It is not called directly by the interrupt handler to prevent re-entrancy
problems. The act of receiving the byte will wake the CPU and
allow it to check for the present of the lock. After processing is
complete it may sleep if it wishes. It will be woken after every
byte is received but can just repeatedly sleep again if it likes.
The main loop that is acting on the lock flag must process the
command and send any necessary data. It may also wait for an ACK
or NAK before finally removing the lock and allowing further
receives.
When sending any packet (including ACK or NAK packets), a timeout
is started. If the timeout expires, the response is considered to
be a NAK and the ACK/NAK is resent. The timeout should be generous
enough to allow for full ring propagation with worst-case delays.
If the packets comes back to the sender, it is also treated as a
NAK. An error counter should limit the number of re-sends before
dropping the packet and returning an error.
When data is received, only the payload is available to the main
loop. A copy of the source address is also saved until the lock is
released. This allows replies to be sent regardless of other
packets received or forwarded during processing.
For the moment, data packets will not have ACK/NAK piggybacked
with them and each will be sent separately. This is because
ACKs are automatically and immediately sent by the packet receiving routines
before a response is even computed.
TODO: NAKing a packet while busy should ideally send a special NAK
that indicates busy as opposed to failed CRC, etc. This would allow
a small pause before re-sending, rather than resending immediately
and probably causing the same problem again.
When we get a packet not destined for us or with headers we don't
understand, we just pass them on. In theory, a corrupt packet
could therefore just be passed on by everybody, forever. To get
around this we could buffer the packet and check the CRC, then only
send it on if all is well. However in doing so we greatly increase
the latency. To prevent the possible long-term buildup of rogue
packets it is assumed that there is a node in the ring (such as a
more powerful PC) that will check things more thoroughly and mop up
any problem packets that are cycling the network. By only having
one such node in the network, the latency effects are minimised.
TODO: An enhancement that may be needed is something to deal with
too much data arriving. eg. if fully occupied with incoming data
and a local transmit is occasionally needed, eventually
transmitting will block while waiting for the TSR to become free
(it won't be able to contain all the outgoing data). This will
mean received data is lost and the packet will become corrupted.
However at least the next packet will also become corrupted. This
situation should be detected and if anything arrives during
blocking transmits, they should be cleanly dropped up until the
packet ending. This improvement just decreases the number of lost
packets, but is a little complex so it may or may not be worth
doing. Also in most cases for a local transmit to be needed, there
would also be a command received, which would be consumed leaving
more buffer space. Also responses from slave devices are not
expected to overwhelm the network so badly.
Problems with SNAP:
Error correction is optional. That means the flag itself could be
corrupted and no error correction will take place. It should
be mandatory and cover the header.
The destination address should occur sooner so packets can be
passed on in the network as soon as possible to decrease latency (only
relevant in a token ring situation).
The lengths are not continuous up to the sizes we want.
A lot of the other stuff is superfluous.
An ARP protocol like SMBus has might be nice.
Byte Level Descriptions
All of these are discussed in greater detail in
this PDF document.
Byte 0: Synchronization Byte (SYNC)
This byte is simply the start of our packet. It is a special value that means its the start of a SNAP packet. The first byte received is compared to this, and if it matches the protocol knows to process the next bytes as heater bytes. The values in different formats are listed below.
| Value | Format |
| 0x54 | Hexadecimal |
| 84 | Decimal |
| 01010100 | Binary |
Byte 1: Header Definition Byte #2 (HDB2)
This byte is used to describe the protocol specific information of the packet being sent. Currently not much of this is implemented by our SNAP library, but you do need to send the proper value. The tables below will tell the name of each bit and what each of the bits means.:
| 7 | 6 | 5 | 4 | 3  | 2 | 1 | 0 |
| DAB | DAB | SAB | SAB | PFB | PFB | ACK | ACK |
| Bit(s) | Meaning |
| DAB | Length of the Destination Address Bytes, in Binary. RepRap currently only accepts destinations of 1 byte length |
| SAB | Length of the Source Address Bytes, in Binary. RepRap currently only accepts source addresses of 1 byte length |
| PFB | Length of Protocol Flag Bytes. RepRap does not accept any protocol flag bytes, so this must be set to 00 |
| ACK | ACK / NAK flags. See the protocol documentation pdf for more information. RepRap fully supports these flags |
Byte 2: Header Definition Byte #2 (HDB1)
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| CMD | EDM | EDM | EDM | NDB | NDB | NDB | NDB |
| Bit(s) | Meaning |
| CMD | Command Mode Bit. Not implemented by RepRap and should be set to 0 |
| EDM | Error detection mode. See PDF for a full description. Currently RepRap only implements 8-bit CRC. this should be set to 011 |
| NDB | Number of Data Bytes. See PDF for a full description. This is a non-linear value, but we use it that way. Currently RepRap only accepts a maximum of 8 bytes (hopefully 16 soon), and this number is the length in binary. |
Byte 3: Destination Address Byte (DAB)
This byte contains the address of the intended recipient of the packet. It is a binary number from 0-255.
Byte 4: Source Address Byte (SAB)
This byte contains the address of the sender of the packet. It is a binary number from 0-255.
Byte 5-?: Data Bytes
These bytes are the actual payload and data of the packet. The number of bytes is contained in the NDB in the HDB1 packet and must match exactly.
Last Byte: Checksum (CRC)
This byte contains the checksum as calculated by the mode specified in EDM.
Protocol Implementation
PIC Implementation
This may be out of date... someone familiar with the PIC info want to update it?
Subversion Location: /reprap/firmware
Status: Working
Work to be done:
- Timeouts and re-sends are not quite there.
Most of the functionality is encapsulated in serial_inc.c. Only a few lines of code are necessary to make use of the serial routines. A simple example is below (most of the code is just processor initialisation etc).
General API
- Main loop inspects processingLock flag. If set, it actions the data
in buffer.
- A reply is optionally constructed by calling sendReply. This uses
the saved source address to send appropriate header bytes. Nothing
much happens here because the header can't be constructed until
the packet is complete (length is unknown).
- Packet payload is sent by repeatedly calling sendDataByte
- The sending is completed by calling endMessage, which will
send the actual packet by constructing a header, length, body and
CRC for the message.
- awaitDelivery is called to wait for a response. A duplicate of
the entire packet is kept in an additional buffer so that if a NAK
arrives the same data can be re-sent without bothering the client.
This method should do very little as the handling of this is
interrupt driven. If called, it will block until the delivery is
complete and return fail/success.
- deliveryStatus returns the same information as awaitDelivery
(except tristate values indicating still sending, success,
failure). This does not block however.
- When sending a new message rather than a reply, the sendMessage
function is called with the destination address.
- Call releaseLock to indicate processing is complete amd allow
any necessary cleanups. If no ACK is received yet, this
will block until it arrives. If endMessage is not called,
the packet is dropped.
In order for the routines to work the ISR must call the interrupt
handler serialInterruptHandler()
Receiving a message:
Example
if (processingLock) {
printf("Received command %d", buffer[0]);
releaseLock();
}
Receiving is typically performed in the main idle loop. The reception itself is completely automatic and happens in the background, driven by the RS-232 interrupts. When a valid packet for the device is received it is stored in a packet buffer and the buffer is locked. When this happens, the global boolean value
processingLock will be set. The packet payload is available in the global byte array
buffer. The buffer contains only the user message, not any protocol or packet information.
When any processing of the payload is complete, the buffer can be unlocked by calling the
releaseLock() function. Another packet cannot be received until the buffer is released.
Sending messages:
Example
sendReply();
sendDataByte(0);
sendDataByte(1);
endMessage();
A new message is started by calling one of two functions:
sendMessage(destAddress);
or
sendReply();
sendMessage(byte) requires a single parameter which is the destination address to send the packet to. sendReply() requires no parameters and starts a new packet destined for the sender of the most recently received packet.
Payload is transmitted by calling the
sendDataByte(byte) function with each byte of content. The
endMessage() function indicates that there is no more content and the packet will be transmitted.
Transmission occurs in the background, controlled by interrupts. This means the foreground application is not held up while the packet is delivered (and possibly NAKd, re-delivered, etc).
Other requirements
In the main interrupt service routine, it is important to call the
serialInterruptHandler() function, otherwise no reception or transmission will occur.
API details
Global variables
extern byte processingLock
Contains the value 0 when no packet data is avaiting processing or 1 when complete and valid packet data is available.
extern byte buffer[16];
Contains the actual user payload portion of the packet. This is only valid when processingLock is 1.
Functions (alphabetical order)
void awaitDelivery();
Not yet implemented
byte deliveryStatus();
Not yet implemented
void endMessage();
Indicates that all data queued for a message is now complete and the packet details can be finalised and transmitted.
void releaseLock();
Indicates that processing of a received packet is complete and a new packet can be received.
void sendDataByte(byte byteToSend);
Queues a single byte into the current message. Prior to calling this function,
sendMessage or
sendReply must have first been called.
void sendMessage(byte destinationAddress);
Starts a new message to the given node
void sendReply();
Starts a new message to the sender of the most recent message
void serialInterruptHandler();
Whenever an interrupt occurs, this should be called so the serial routines can do any necessary work.
Minimal application:
THE FOLLOWING IS NOT COMPLETE YET,
DON'T TRY IT BECAUSE IT PROBABLY WON'T
WORK. It will be cleaned up and finished soon.
// Select device
#define __16f627
#include <pic/pic16f627.h>
#include "pic14.h"
typedef unsigned int config;
config at 0x2007 __CONFIG = _CP_OFF &
_WDT_OFF &
_BODEN_OFF &
_PWRTE_ON &
_INTRC_OSC_CLKOUT &
_MCLRE_OFF &
_LVP_OFF;
// This is the address that will messages will be accepted for
byte deviceAddress = 2;
// Support routines for bank 1
#include "serial-inc.c"
static void isr() interrupt 0
{
serialInterruptHandler();
}
void processCommand()
{
switch(buffer[0]) {
case 0: // Command 0 is a standard "get version" message that all devices implement
sendReply(); // Start a reply to the current packet
sendDataByte(0); // Return the version number in bigendian format as major-minor
sendDataByte(1);
endMessage(); // Complete and send the message
break;
}
}
void main()
{
OPTION_REG = BIN(11011111); // Disable TMR0 on RA4, 1:128 WDT
CMCON = 0xff; // Comparator module defaults
TRISA = BIN(00110000); // Port A outputs (except 4/5)
// RA4 is used for clock out (debugging)
// RA5 can only be used as an input
TRISB = BIN(00000110); // Port B outputs (except 1/2 for serial)
PIE1 = BIN(00000000); // All peripheral interrupts initially disabled
INTCON = BIN(00000000); // Interrupts disabled
PIR1 = 0; // Clear peripheral interrupt flags
SPBRG = 25; // 25 = 2400 baud @ 4MHz
TXSTA = BIN(00000000); // 8 bit low speed
RCSTA = BIN(10000000); // Enable port for 8 bit receive
TXEN = 1; // Enable transmit
RCIE = 1; // Enable receive interrupts
CREN = 1; // Start reception
PEIE = 1; // Peripheral interrupts on
GIE = 1; // Now turn on interrupts
PORTB = 0;
PORTA = 0;
T1CON = BIN(00000000); // Timer 1 in clock mode with 1:1 scale
TMR1IE = 1; // Enable timer interrupt
init();
// Clear up any boot noise from the TSR
uartTransmit(0);
for(;;) {
// This is the main processing loop.
// You would normally put your main application
// in here.
// In this case, there's nothing to do so we just
// loop endlessly (this is not normally a cool thing
// to do, but this is the world of microcontrollers and
// it's okay, except for the fact we don't do any
// power saving. [We could perhaps extend this to
// wake up on serial interrupt?]
// If there is a message waiting, we should process it
if (processingLock) { // A message is waiting
processCommand(); // Process command
releaseLock(); // Release buffer
}
}
}
In the examples, why are there two C files for each device?:
This is related to an sdcc restriction on register allocation. See the sdcc documentation for further explanation.
to top