Creating a multi port serial object
DiverBob
Posts: 1,108
in Propeller 2
I have been working on a large hexapod robot for several years that currently uses 7 P1 processors (6 leg controllers and one master). After a while it become evident that the P1master was not fast enough to perform floating point trig calculations so I started to look for other options. Luckily the P2 is available so my quest has been to replicate or find replacements for the P1 Objects used in my code. The robot relies on serial communications between the master and the individual leg controllers for leg movements. At this point its using 2 copies of the P1 4 port serial communications object for a total of 8 separate serial lines (1 line for output from master to legs, 6 separate receive only lines from the legs to the master, and 1 line to the master via the prop plug for troubleshooting).
Looking over the serial options I thought JonnyMac's jm_serial.spin2 looked easiest to modify to allow multiple ports with its use of Smart Pins. I did a quick mod to allow 2 ports for testing. The code compiles with no errors but in testing I'm not getting any response at the PST terminal. As best I can tell the Smart Pins aren't being configured correctly in the addport routine but I'm not figuring out what I did wrong. I archived the demo (I used JonnyMac's serial demo also, commented out most of the code to reduce complexity until it works) and attached it along with a portion of the serial code.
Looking over the serial options I thought JonnyMac's jm_serial.spin2 looked easiest to modify to allow multiple ports with its use of Smart Pins. I did a quick mod to allow 2 ports for testing. The code compiles with no errors but in testing I'm not getting any response at the PST terminal. As best I can tell the Smart Pins aren't being configured correctly in the addport routine but I'm not figuring out what I did wrong. I archived the demo (I used JonnyMac's serial demo also, commented out most of the code to reduce complexity until it works) and attached it along with a portion of the serial code.
con { fixed io pins } RX1 = 63 { I } ' programming / debug TX1 = 62 { O } FS_CS = 61 { O } ' flash storage FS_CLK = 60 { O } FS_MOSI = 59 { O } FS_MISO = 58 { I } con { pst formatting } HOME = 1 CRSR_XY = 2 CRSR_LF = 3 CRSR_RT = 4 CRSR_UP = 5 CRSR_DN = 6 BELL = 7 BKSP = 8 TAB = 9 LF = 10 CLR_EOL = 11 CLR_DN = 12 CR = 13 CRSR_X = 14 CRSR_Y = 15 CLS = 16 obj nstr : "jm_nstr" ' number-to-string var long rxp0 long txp0 long rxp1 long txp1 byte pbuf[80] ' padded strings pub null() '' This is not a top level object pub addport(port1, rxpin, txpin, baud) | bitmode '' Start simple serial coms on rxpin and txpin at baud, can use different rx/tx based on selected port case port1 0: longmove(@rxp0, @rxpin, 2) ' save port0 pins 1: longmove(@rxp1, @rxpin, 2) ' save port1 pins bitmode := muldiv64(clkfreq, $1_0000, baud) & $FFFFFC00 ' set bit timing bitmode |= 7 ' set bits (8) org fltl rxpin ' configure rx smart pin wrpin ##P_ASYNC_RX, rxpin wxpin bitmode, rxpin drvl rxpin fltl txpin ' configure tx smart pin wrpin ##(P_ASYNC_TX | P_OE), txpin wxpin bitmode, txpin drvl txpin end pub rxflush(port1) '' Clear serial input repeat while (rxcheck(port1) >= 0) pub rxcheck(port1) : rxbyte | check '' Check for serial input '' -- returns -1 if nothing available rxbyte := -1 case port1 0: check := pinr(rxp0) if (check) rxbyte := rdpin(rxp0) >> 24 1: check := pinr(rxp1) if (check) rxbyte := rdpin(rxp1) >> 24 pub rxtime(port1, ms) : b | mstix, t '' Wait ms milliseconds for a byte to be received '' -- returns -1 if no byte received, $00..$FF if byte mstix := clkfreq / 1000 t := getct() repeat until ((b := rxcheck(port1)) >= 0) or (((getct() - t) / mstix) > ms) pub rx(port1) : rxbyte '' Wait for serial input '' -- blocks! repeat rxbyte := rxcheck(port1) until (rxbyte >= 0) pub tx(port1, b) '' Emit byte case port1 0: wypin(txp0, b) txflush(port1) 1: wypin(txp1, b) txflush(port1) pub txn(port1, b, n) '' Emit byte n times repeat n tx(port1, b) pub txflush(port1) | check '' Wait until last byte has finished case port1 0: repeat check := pinr(txp0) while (check == 0) 1: repeat check := pinr(txp1) while (check == 0) pub str(port1, p_str) '' Emit z-string at p_str repeat (strsize(p_str)) tx(port1, byte[p_str++])
Comments
The SETQ works with the RDLONG to block transfer 4 parameters into rdx, tdx, p_rxbuf and p_txbuf. Does this same mechanism work for transferring in the array of pin values stored in rxp[8] from the addport routine? Another question is how to determine how many ports have actually been assigned via the addports routine.
I haven't found any examples of moving arrays into pasm so I don't know if its possible or just no one has needed to do it yet. I'll run some tests and see what happens also.
I still haven't completely sorted out how I'm going to do multi-port, but these are my thoughts:
-- true mode only
-- additional ports will use external buffers
-- keep a byte flag of ports that are opened
-- have a command mail box that can tell the pasm cog to add a port
I'm hoping that the current code will allow for global cog variables to be used so that there's not too much redundancy.
When I started on the P1 the object exchange was pretty good and the code objects I needed so there wasn't any need to learn pasm. Now the P2 is still pretty young and the extensions to allow in-line pasm make learning it a whole lot more attractive. My biggest stumbling block is the lack of in-depth documentation at this point. I tend to learn best from written examples vs studying someone else's code and figuring out their thought process. Knowledge is coming along but slowly.
rdlong is used to do a block copy of the 4 variables from hub memory over to cog memory. Since the reserved variables listed in the PASM section are in the same order as the VAR values, the copy keeps everything going to the right places. I'm not sure I fully understand testb. The description for testb is 'Test bit S[4:0] of D, write to C/Z. C/Z = D[S[4:0]].', I know this is a Verilog description but I haven't been able to decipher what that actually means. Rxd would have the output from the smartpin, is the smartpin output located in the 24-31 positions of the output long value. If that is the case then testb is checking for a stop bit from an incoming value? Here is testp that tests the smartpin. The description is 'Test IN bit of pin D[5:0], write to C/Z. C/Z = IN[D[5:0]].', not sure what it means by the IN bit reference. If C is empty then the code returns to the last location on the stack which should be rx_serial.
Thanks for any help.
When a smart pin is ready it will give you a 1 with testp. When this is the case you use rdpin to get the value. With an RX UART you have to shift that value right by 24 bits to realign for the user.
Thanks for any help!
* it only reserves space
* Does not set it to zero
* Beware of any following code - it is mostly at the end of a piece of pasm code so should be followed only by a separate piece of code - a new DAT, ORG, PUB, PRI, VAR
IIRC there is a P1 write up in the manual describing its use.
PTRA points to the hub memory location that is set when using COGINIT. In the case below it points to the memory location of global variable RXP. In the assembly code the SETQ and RDLONG then block copy the next 4 long values starting at global variable RXP into the memory location reserved for RXD and the next 3 sequential memory locations reserved in assembly. RXD is single unassigned long reserved long value set at the end of the assembly code. So the effect of the process copies the values in the global variables into the 4 variables used by assembly.
As Cluso99 states the RES command reserves sequential long spaces but doesn’t assign a value (this is done by the rdlong command) and it needs to be at the end of the pasm commands otherwise bad things can happen.
Could the following sequence have been used instead of RES? Going further I saw this syntax used in the array rotation assembly code Does this create an array of 8 elements all assigned a 0 value that are accessed like rxd[0] through rxd[7]?
This leads me to the problem I am trying to figure out. If I have a global variable that is an array, how do get the array values into assembly? For instance I declare these global variables. When the cog is setup the same code is used as it just points to the start of the global variable address In assembly the code to get the 16 values into assembly variables would be like this This is the simplified version of the code I have been using for testing but so far it doesn’t appear to work. If the syntax is correct then my problem is most likely my method I’m using to test the code. I asked a lot of questions here but I want to make sure I understand this data transfer process as it hasn’t always worked the way I thought it should.
It is safer to use long 0 rather than res 1 unless you’re short on hub space.
Please can you put up a link to that old video of Jeff Martin.
It sounds like good value!
They implied there was going to be an advanced video that I haven't found yet. I didn't see this on the Parallax youtube channel, if there is another advanced video I haven't found it yet.
I will try out your code and see how it goes, it looks pretty good. I’m still curious about the ##INDEX20 operation mode, haven’t run across any examples of it in my searches except for in the P2 silicone documentation I posted. This assembly coding exercise has been good in making me study the documents much more closely and search for examples of each code element, learning a lot more about P2 assembly this way even if it is a slow process!
This should work providing that index is a constant. Which may not be what you need. If index is a variable, you'll need to do something else, for example adding some appropriate multiple of index to ptra before using ptra (and then subtracting it back to restore ptra after you're done).
Yes, it's assuming that you want to read an array of longs, so the indexes refer to longs instead of bytes. That's why index has to be multiplied.
You'll also need to offset ptra by the appropriate amount for the variables you're using. It looks like rxd has offset 0 and 8 longs, so the txd array will start at offset 32 (8*4), p_rxbuf at offset 64, and so on. so you'd do something like:
This could be made a bit faster if you "inverted" things and used an array of structures rather than a set of arrays. That is, instead of declaring and so on, for 9 different variables (or however many you need for each serial port) you did:
If you're reading many variables all together the "add ptra, t1" / "sub ptra, t1" pairs can be combined, i.e. you just have to do one "fixup ptra" before reading the variables and one "restore ptra" at the end once all the variables are read.
Your last code example is great in that the array structure uses a lot of memory setting up for 8 ports even if you are only using 1 or 2 ports. This reduces the amount of space needed to only what is required for the actual number of ports.
For single instances, the large offset will be more efficient in both code space and execution time, but for accessing blocks using a single ptra manipulation you quickly reverse that.
Another advantage of using the structure approach could be to perform a single ptra manipulation around a burst read of only the parameter block for the port to be operated on.
@DiverBob, As you have only buffer address, head, and tail pointers per buffer you can only have fixed size buffers, which might introduce risks of frame loss on fast ports, or wasting hubram for slower ports.
The structure could grow to two more longs per port to give a buffer max address for each buffer, allowing per port buffer sizes and even different sized buffers for rx and tx functions.
If the head and tail longs contain the absolute hubram addresses then there's no pointer maths required to access the buffer, and the wrapping tests simply compare the head and tail pointers against the buffer max address, wrapping to the buffer start address. All of this can be performed against data in cogram with the buffer pointer section of the parameter block being written back to hubram before switching to a different port.
Advantages:
No pointer maths
Parameters sit at fixed locations in cogram, so code remains static
Variable buffer sizes enabled by clean code
Only 10 long parameter block loaded per port, so constant parameter loading time per port and smaller cogram footprint.
Simpler code (no indexing) meaning smaller, faster code in cogram.
Disadvantage:
Parameters must be reloaded every time you need to switch to support a different port; although this is likely the case anyway.