SNAPComms

From RepRapWiki
Jump to: navigation, search

Note: this is a description for an older attempt to communicate between various electronic RepRap components. While there is currently no known device using this, it might be worthwhile in some future arrangement.

Contents

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.