DEMO: High Speed Multi-Prop to Prop Communication
Beau Schwabe
Posts: 6,566
I should have released this sooner... the "Saturday robotics club" that I participate in has a project going on where this is a perfect fit, and after cleaning it up a bit I thought I would pass it along.
Here is a derivative of the high speed 8.42 Meg Baud (1.05 Million Bytes per second) Prop-to-Prop communication that I wrote some time ago. Last March there were several changes to the front end of both the Receiver and the Transmitter in the way that the handshaking took place. Before you had to make sure that the Receiver was up and running before you Transmitted... this is no longer the case, now it doesn't matter making it more user friendly.
For just the average user, it's pretty straight forward... there is only one command to Send, and there is only one command to Receive.
Basic Use:
To receive, just specify the pin you want to listen on and the address of where you want the received data to go to. Remember, this transmission is designed to send large packets of data, so if your just sending a few bytes here and there, this object is probably not for you.
RX(_Pin,_DataAddress)
To Transmit, is basically the same thing with a few more parameters... you specify the pin you want to yell on, the address of where the data is coming from, How much data you want to send in longs.
TX(_Pin,_DataSamples,_DataAddress,_%0000, 0)
Note: the last two fields are not used in the Basic setup, they will be discussed in Advanced Use.
So that's it for basic use.
Advanced Use:
The Receive is just the same as before, but it can be used as a function to return additional. information from the Server.
Command := RX(_Pin,_DataAddress)
This will receive data just as before and place it in the assigned address, but Command contains the size of the transmitted packet, a destination offset, and a Packet Command. Organized as such...
%ssssssssssssss_aaaaaaaaaaaaaa_cccc
where:
s = 14-Bit Packet Size
a = 14-Bit Destination Offset
c = 4 Bit Command
The Packet Size is obviously useful for determining how much data you received and allows support for variable width packets.
The Destination Offset is unique in the sense that the Server has some control as to where the Data will end up on the receiver. Basically this value gets added to the DataAddress that you specify on the receiver so that the incoming data is written to a location starting at the DataAddress plus the Offset. This feature allows random block writes from the Server to the Client.
The 4-Bit Command is just a way for the server to pass a specific command to the receiver. It can be used for anything you want. It's up to you.
For Transmission, it's just the same as the Basic Transmission as well, except the two parameters that were Zero'd out now have some meaning.
TX(_Pin,_DataSamples,_DataAddress,_DataCommand,_Offset)
DataCommand as just mentioned is a 4-bit command you can pass directly to the Client and can be used for anything you want.
The Offset, also just mentioned, can be used to tell the receiver to write data to another location.
This is useful when you only want to update a block or section of memory on the Client.
Finally with the Demo programs supplied, shows a round-robin approach to sending data across multiple Propellers.
The Idea is that you have one buffer that every Propeller sends around the loop ... "infinitely"
To prevent collisions, ALL Propellers have access to reading the entire buffer, however, and this is what makes it work. Each Propeller can only write to a specific assigned location of that buffer. <- This isn't exactly true, but it's a good programming practice to implement. There aren't any collisions for similar reasons that you don't have collisions from COG to COG on a single Propeller.
When a Propeller reads the Buffer, he is only allowed to write to the section that he is assigned to before sending the Buffer on to the next Propeller. (Note the Demo Code has this restriction lifted and can write to any location on the Buffer... But in my description, that's how you would typically manage the data across multiple Propellers and avoid collision. It works in the Demo, because there is only one Propeller writing to the buffer)
In the Ring, you can have as many Propellers as you want, with each Propeller only having a 3-wire interface... (Ground, TX, and RX) ... I have tested up to 5 Propellers with the supplied demo code.
One Propeller must be identified as the Server to initiate the data ring, but all of the other Propellers are identified as Clients.
Within each Propeller regardless of Server or Client ALL Propellers have equal access to the Data Buffer.
I Hope this makes sense... Enjoy!!
EDIT: added a slightly newer version that addresses detection of the USB plugged into the PC. This prevents unwanted resets.
EDIT: Check this link out for a way to control switches across multiple Propellers, i.e. for lighting
Here is a derivative of the high speed 8.42 Meg Baud (1.05 Million Bytes per second) Prop-to-Prop communication that I wrote some time ago. Last March there were several changes to the front end of both the Receiver and the Transmitter in the way that the handshaking took place. Before you had to make sure that the Receiver was up and running before you Transmitted... this is no longer the case, now it doesn't matter making it more user friendly.
For just the average user, it's pretty straight forward... there is only one command to Send, and there is only one command to Receive.
Basic Use:
To receive, just specify the pin you want to listen on and the address of where you want the received data to go to. Remember, this transmission is designed to send large packets of data, so if your just sending a few bytes here and there, this object is probably not for you.
RX(_Pin,_DataAddress)
To Transmit, is basically the same thing with a few more parameters... you specify the pin you want to yell on, the address of where the data is coming from, How much data you want to send in longs.
TX(_Pin,_DataSamples,_DataAddress,_%0000, 0)
Note: the last two fields are not used in the Basic setup, they will be discussed in Advanced Use.
So that's it for basic use.
Advanced Use:
The Receive is just the same as before, but it can be used as a function to return additional. information from the Server.
Command := RX(_Pin,_DataAddress)
This will receive data just as before and place it in the assigned address, but Command contains the size of the transmitted packet, a destination offset, and a Packet Command. Organized as such...
%ssssssssssssss_aaaaaaaaaaaaaa_cccc
where:
s = 14-Bit Packet Size
a = 14-Bit Destination Offset
c = 4 Bit Command
The Packet Size is obviously useful for determining how much data you received and allows support for variable width packets.
The Destination Offset is unique in the sense that the Server has some control as to where the Data will end up on the receiver. Basically this value gets added to the DataAddress that you specify on the receiver so that the incoming data is written to a location starting at the DataAddress plus the Offset. This feature allows random block writes from the Server to the Client.
The 4-Bit Command is just a way for the server to pass a specific command to the receiver. It can be used for anything you want. It's up to you.
For Transmission, it's just the same as the Basic Transmission as well, except the two parameters that were Zero'd out now have some meaning.
TX(_Pin,_DataSamples,_DataAddress,_DataCommand,_Offset)
DataCommand as just mentioned is a 4-bit command you can pass directly to the Client and can be used for anything you want.
The Offset, also just mentioned, can be used to tell the receiver to write data to another location.
This is useful when you only want to update a block or section of memory on the Client.
Finally with the Demo programs supplied, shows a round-robin approach to sending data across multiple Propellers.
The Idea is that you have one buffer that every Propeller sends around the loop ... "infinitely"
To prevent collisions, ALL Propellers have access to reading the entire buffer, however, and this is what makes it work. Each Propeller can only write to a specific assigned location of that buffer. <- This isn't exactly true, but it's a good programming practice to implement. There aren't any collisions for similar reasons that you don't have collisions from COG to COG on a single Propeller.
When a Propeller reads the Buffer, he is only allowed to write to the section that he is assigned to before sending the Buffer on to the next Propeller. (Note the Demo Code has this restriction lifted and can write to any location on the Buffer... But in my description, that's how you would typically manage the data across multiple Propellers and avoid collision. It works in the Demo, because there is only one Propeller writing to the buffer)
In the Ring, you can have as many Propellers as you want, with each Propeller only having a 3-wire interface... (Ground, TX, and RX) ... I have tested up to 5 Propellers with the supplied demo code.
One Propeller must be identified as the Server to initiate the data ring, but all of the other Propellers are identified as Clients.
Within each Propeller regardless of Server or Client ALL Propellers have equal access to the Data Buffer.
I Hope this makes sense... Enjoy!!
EDIT: added a slightly newer version that addresses detection of the USB plugged into the PC. This prevents unwanted resets.
EDIT: Check this link out for a way to control switches across multiple Propellers, i.e. for lighting
Comments
Cool!
Any idea how far separated the props can be from adjacent Props?
And do you mean there is no upper limit as to the number of Props?
"And do you mean there is no upper limit as to the number of Props? " - within reason to the amount of speed that your willing to sacrifice. The data transmits at just over 1 Million Bytes per second. Propagation delay on per Propeller basis "IF" a Propeller needs to write to the buffer will contribute to that propagation delay.
Once I get a few of my miniature prop pcbs built I should be able to give this a workout I am sure jazzed could too.
Oh, but it is! This is going to be perfect for talking to the 256x224 full color video propeller. 8 megbaud is fast enough to run video at that resolution. Sweet. Thanks Beau!
Thanks
The 330 Ohm resistors form a transmission line between One Propeller and another Propeller and should be of equal value for proper matching on both ends. This Transmission line helps to reduce any external RF interference that could enter the line due to antenna effects.
The 100 Ohm resistor is there for protection in a situation where both the Server and Client could potentially drive the line in disagreement.
That said, you need to treat both of the 330 Ohm resistors as if they are in parallel with each other... so the circuit now becomes a voltage divider between the 100 Ohms protection resistor and 165 Ohms (1/2 of 330 Ohms). Using a 100 Ohm resistor is about as high as you can go and still have a signal on the receivers side that is above the voltage threshold of the I/O.
...So using 3.3V on the TX side, the receiving voltage level should be about 2.05V ... using 5V on the TX side, the receiving voltage level should be about 3.12V
In which case, the 3V logic should be ok ... the question is, is 2.05V enough for the 5V logic to determine a logic "1"
The biggest problem I see will be getting the timing just right with your 5V micro processor. The timing needs to be within 25ns to 50ns or it won't work correctly.
I'm confused though, I thought you were wanting to communicate using the same high speed protocol with a 5V micro?
Erm, its dr hydra working with 5v. I doing a dual-prop design.
I have another idea, what do you think?
Add an "assert" that is pulled high by an external resistor, then assert low before transmitting a message. The prop does a listen for assert, then pulls it low, then transmits a packet with a source and destination ID, then lets go of assert. This is a more rudimentary method if CSMA/CD and gives you more payload space in the packet. It also avoids the token ring effect.
I'm sure there are some clever ways of reducing the instruction count so that collisions are very unlikely.
If you get rid of the wire count limitation, do an I2C like data, clock, and select. You pull select low, then wait for the same number of clocks as it took to do the check and set for select, then check that the clock isn't low, then bring the clock low to signal the begin of transmission. Doing the select, then waiting, then bringing clock low would ensure a foolproof protocol for collision avoidance.
Then you just put multiple props on a bus and just run a simple wait loop to receive all the time, or break in to send.
I am using it to run a common memory buffer between 3 props. 24 core processing with 1K shared RAM.
Is there a way to pack it into one cog? Right now I dedicate a cog to write my process variables into out of the shared memory plus the HS_Rx and HS_Tx cog. That takes up 25% of my oompf.
If the user modifying memory code could reside in one cog along with the Tx and Rx methods that would be great. Right now my 3 props are indeed maxed out.
sm
Because of speed requirements, the code needs to have it's own cog, but the cog only gets launched when you call the command. After the request is done it should release the cog. Perhaps the duty requirements can be shared with another cog that doesn't need to continuously run as well.
Dave
Another thing you can do is define a DAT block and put your variables in there ... aligned properly. Like this:
This is just to illustrate padding when a BYTE is followed by a WORD or LONG or a WORD is followed by a LONG. If you have a LONG first, you don't need padding for WORDs or BYTEs and if you have a WORD first, you don't need padding for BYTEs. These variables can be used in Spin just like a VAR. Here an initial value has to be specified and, if you're making an object that will be defined multiple times in your program, there will be only one DAT for that object ... not one copy per object reference as with VARs.
Thanks for the info. I've been giving it a try and I'm still not able to get it to work.
On the Client side I've just changed the last line PST.str(@buffer) to PST.dec(@Buffer)
On the Server side I've made a couple of changes.
The result is that I see "2308" in the terminal window, regardless of what I set the variable "SensorData" as. Any suggestions?
Thanks!
Client:
VAR
long Buffer[1024],Stack[400]
OBJ
hsRX : "HSp2pRX" 'High Speed Receive driver
hsTX : "HSp2pTX" 'High Speed Transmit driver
PST : "Parallax Serial Terminal" 'RS232 Serial Driver
PUB start|debugLED
'
if ina[USB_Rx] == 0 '' Check to see if USB port is powered
outa[USB_Tx] := 0 '' Force Propeller Tx line LOW if USB not connected
else
cognew(SerialDisplay,@Stack) '' Initialize serial communication to the PC
'
dira[23..16]~~ '<- I/O direction for debug LEDs
'
repeat
hsRX.RX(RX_Pin,@Buffer) '<- Receive Data from External Propeller
hsTX.TX(TX_Pin,1024,@Buffer,Command,Offset) '<- Transmit Data to External Propeller
'
debugLED++ '<- debug LED section
if debugLED > 60
outa[23..16] := $FF - outa[23..16]
debugLED := 0
'
PUB SerialDisplay 'DEBUG ONLY
PST.Start(19200)
repeat
PST.HOME
PST.dec(@Buffer) '<- Display Data
Server side:
long Buffer[1024]
long SensorData
Con
OBJ
hsRX : "HSp2pRX" 'High Speed Receive driver
hsTX : "HSp2pTX" 'High Speed Transmit driver
PUB start|debugLED
SensorData := 12345
longmove(@Buffer,SensorData,1) '<- Moves TestMessage into Buffer
dira[23..16]~~
repeat
hsTX.TX(TX_Pin,1024,@Buffer,Command,Offset) '<- Transmit Data to External Propeller
hsRX.RX(RX_Pin,@Buffer) '<- Receive Data from External Propeller
debugLED++
if debugLED > 60
outa[23..16] := $FF - outa[23..16]
debugLED := 0
'Dat
'SensorData long 0
Client side:
Server side:
Server:
First of all, thanks for the code, it's great.
Now, how would one implement some sort of error detection with Round Robin? What I mean is: if e.g. the connection between the 3rd and 4th Prop is broken, the buffer can't travel around anymore and the TX/RX routine is stuck and the calling routine is stuck, too. I was thinking of ABORT traps but don't want to mess with Beau's code.
I was also thinking of starting a counter when calling the TX or RX routine, wait a few milliseconds and if it doesn't finish in that time assume that something went wrong. But how would I force the Prop to abort the routine that is stuck, give out an error message and then go on with the next commands?
My Buffer looks like this:
Buffer[0]
Buffer[4]
Buffer[8]
Buffer[12]
Buffer[16]
Buffer[20]
Buffer[24]
Buffer[28]
Buffer[32]
ID sent by Master, compared to Buffer[8]
some value to pass to clients
the client ID, increased by each client
value sent back to master from client 1
value sent back to master from client 1
value sent back to master from client 2
value sent back to master from client 2
value sent back to master from client 3
value sent back to master from client 3
So, my Buffer is not travelling around all the time but only on demand.
The bold snippet in the code above is an example where I thought I could check if the Round Robin is still working, right after the user entered his command. But how exactly?
Thanks in advance.
Akkarin
As for communication between Props: Assign a cog in each prop as controller to take/pass the block of packets from/to other props. That way, the round-robin communication is travelling constantly regardless of whether the particular prop needs it or not. And, you can assign a byte as flag where the controller will see if other cogs within its prop needs to modify any part of the packet when it arrives.
As for error checking: One way is you can do polling but frankly, I don't see the point as if the communication does break, most likely than not, its hardware related by that time...
I'm just beginning to understand how this thing is supposed to be used. I'll try to set up a constantly travelling round-robin buffer.
One thing to clarify here: I want to check for connection breaks in the wiring. My system consists of several boards in an 19" rack which are interconnectet by a backplane. If e.g. one board is taken out or for some reason is not connected to the backplane anymore, the whole round robin doesn't work anymore. I'd like to check for that and than post an appropriate message to the user so that he can check the boards for poor connections or whatever.
But the main reason is that I don't want my program to get stuck and this I can accomplish by assigning a cog on its own for the round robin, right?
Thanks
Akkarin
1. Before you transmit data to external propeller, do a ping by sending a byte, say $5E, & poll (in a cog) for an acknowledgement with a specific time (say, 5ms). It'll act like a watchdog so if you don't get an acknowledgement byte example $7F from the external propeller, you can send a message to the user that that particular prop/board is not responding. In either case, you'll cogstop the cog that's polling
last 2 days I was trying to set up the constantly travelling buffer, but I can't get it to work reliably. Some times it works, sometimes not ...
I reckon my problem is how I access the buffer from different cogs ... I have to update the buffer in main ram and read from main ram if different cogs want to use the buffer, right? I decided to use LONG[@Buffer][index] to access the buffer in main ram from different cogs, but it doesn't work 100%.
I'll go back a step, to batch mode, and try to implement what you suggested before.
EDIT: I think the round robin is working OK, but my accessing the buffer not.
Thanks
Akkarin
Please wait a minute, I'm a little confused. You mentioned that your project setup is such that it has a backplane that connects all your clients to the server, am I correct? In that case, having a continuous round-robin communication would sure to pose a problem if any of the 5 client boards were to be removed, as you have also mentioned. Therefore, I suggested you perform a check on the next client before launching hxTX.TX. E.g.:
Then, on your Client 2, you would be listening for $5E from Master. When it received it, send the acknowledgement byte ($7F) over then launch the hxRX.
And, to your latest question, if you want other cogs to update the hub ram before sending over to Client 2, you'll need to pass the hub address over to the cog. Then, for each cog, you'll need to ensure they update into their correct/assigned offset area else the cogs will be fighting to write on the same section. Also, within cog, you'll access its data as LONG[Buffer][Index] since Buffer is already the address.
Hope the above helps. I just typed out the above code just to show you the idea so I didn't test it.
sorry I didn't have the time to reply, yet.
I implemented the connection check such that I open a new cog directly after the user entered a command in the main menu. This new cog sends a message to the clients and waits for the reply. If it times out after, say, 5ms, the main routine stops the cog and I know that there is a connection problem. If not, the cog stops itself after reception of the client reply. The main routine reads the correct reply and goes on with the program. This way it's not continuously checking, but good enough for me right now.
That's a good hint! Thanks
I realize I'm a bit late to the game, but I just discovered this code for a project I'm doing. Beau, the code is great! Thanks for posting it. I have it working within my old code, which is now split between two Props, but I have one obstacle I can't seem to overcome. Anytime I run the code with the USB cables plugged into my boards (Propsticks) it runs and the debug LEDs blink on the client side letting me know the buffer is being passed around. Looking into the terminal I have good data. However I am unable to get it to work without the USB cables plugged in (to both client and server). I tried to remove the code that looks at the USB power and then starts PST but no matter what I try the LEDs never start. Then as soon as I plug them in (and have the client PST code back in place) and cycle the power, the LEDs start blinking. My question is "Does PST need to be running for this to work?" I looks like it is for debugging only and in my final setup I don't plan on having a PC hooked up.
Thanks,
-Brian
*****EDIT**********
Found the issue. Client not properly grounded. Looks like the USB provided an acceptable ground while connected but when unplugged the loop was broken.