P2 Multiport x16 Serial Driver working (based on JonnyMac's FullDuplexSerial)
Cluso99
Posts: 18,069
Rather than take over @DiverBob's thread I thought I should start a new one.
forums.parallax.com/discussion/172485/creating-a-multi-port-serial-object#latest
Thanks to Ken, there is now a Quick Bytes here
https://www.parallax.com/multiple-serial-port-16-object/
Update 29-Jan-2021: V0.40 code posted with working demos for 1, 2 & 8 fullduplexports (2, 4 and 16 ports/pins). Tested with flexspin and pnut.
https://forums.parallax.com/discussion/comment/1515662/#Comment_1515662
Update 27-Jan-2021: V0.31 code posted with working demos for 1, 2 & 8 fullduplexports (2, 4 and 16 ports/pins). Tested with flexspin and pnut.
http://forums.parallax.com/discussion/comment/1515415/#Comment_1515415
Update 26-Jan-2021: V0.30 code posted showing two full duplex ports working.
http://forums.parallax.com/discussion/comment/1515306/#Comment_1515306
Update 22Jan2012: posted working code below. This will support up to 16 total uni-directional ports (ie up to 16 pins that may be any mix of transmit or receive).
Just for fun, here is a 64-Port version. For serious use I would need to tweek the driver.
https://forums.parallax.com/discussion/172821/p2-multiport-x64-serial-driver-working-just-for-fun/p1
Please note the following implementation detail has changed in the final versions.
JonnyMac made an amazing object (need a link) that includes formatting strings with binary/decimal/hex/etc. I've added a few personal bits like dumping hub memory with the usual 16 bytes per line in hex plus ASCII. I've starting breaking down the PASM driver into a separate object so that it can be loaded separately from the spin code. The reason for this is to be able to make the PASM driver able to stay resident when loading other objects, as is required when making a P2 OS (operating system). There are other benefits too, such as being able to use the spin code to format strings, etc, while changing the PASM driver underneath to say VGA and Keyboard, and still retain the hub buffer interface (ie hardware virtualisation).
The first part is to define the hub interface. This works well in Jon's code but I would like to take the time to expand on a couple of things before adding the multiple buffer sets required for each port. Here is the current interface for a single port...
The real part of the interface is this section...
Since we have a reasonable 512KB, providing a few extra longs to convey some additional info would be nice. What might these be?
* tx and rx pin
* baud
I would also like to add in
* rxsize
* txsize
This would make the buffer sizes flexible tho we have to account for this.
The padded string(s) would be defined in the spin section so it would not be part of the buffer interface (or mailbox). And the driver cog number is probably not required here.
Question(s)...
* Would a word (16 bits) be good enough for the head, tail and size? This is 64KB after all !!!
Why? Well it depends on whether we want to pack the heads/tails/sizes for all ports into one contiguous group and the buffers separate to these. There are some advantages to using PTR[offset] for getting these values.
So here are two alternatives for packing the buffers and pointers (I've just used longs for now)...
Repeat for each port
or
If you want to be able to start and stop ports at will, then the interface will likely need an extra interface for port number, txpin, rxpin, baud, txbuf and rxbuf addresses and txsize and rxsize for sizes, and a flag indicating if it is running. txpin, rxpin and flag can be bytes within a long, baud is a long, and the txbuf, rxbuf need to be a hub address so 20 bits minimum so a long each.
Ideas???
forums.parallax.com/discussion/172485/creating-a-multi-port-serial-object#latest
Thanks to Ken, there is now a Quick Bytes here
https://www.parallax.com/multiple-serial-port-16-object/
Update 29-Jan-2021: V0.40 code posted with working demos for 1, 2 & 8 fullduplexports (2, 4 and 16 ports/pins). Tested with flexspin and pnut.
https://forums.parallax.com/discussion/comment/1515662/#Comment_1515662
Update 27-Jan-2021: V0.31 code posted with working demos for 1, 2 & 8 fullduplexports (2, 4 and 16 ports/pins). Tested with flexspin and pnut.
http://forums.parallax.com/discussion/comment/1515415/#Comment_1515415
Update 26-Jan-2021: V0.30 code posted showing two full duplex ports working.
http://forums.parallax.com/discussion/comment/1515306/#Comment_1515306
Update 22Jan2012: posted working code below. This will support up to 16 total uni-directional ports (ie up to 16 pins that may be any mix of transmit or receive).
Just for fun, here is a 64-Port version. For serious use I would need to tweek the driver.
https://forums.parallax.com/discussion/172821/p2-multiport-x64-serial-driver-working-just-for-fun/p1
Please note the following implementation detail has changed in the final versions.
JonnyMac made an amazing object (need a link) that includes formatting strings with binary/decimal/hex/etc. I've added a few personal bits like dumping hub memory with the usual 16 bytes per line in hex plus ASCII. I've starting breaking down the PASM driver into a separate object so that it can be loaded separately from the spin code. The reason for this is to be able to make the PASM driver able to stay resident when loading other objects, as is required when making a P2 OS (operating system). There are other benefits too, such as being able to use the spin code to format strings, etc, while changing the PASM driver underneath to say VGA and Keyboard, and still retain the hub buffer interface (ie hardware virtualisation).
The first part is to define the hub interface. This works well in Jon's code but I would like to take the time to expand on a couple of things before adding the multiple buffer sets required for each port. Here is the current interface for a single port...
var long cog ' cog flag/id long rxp ' rx smart pin long txp ' tx smart pin long rxhub ' hub address of rxbuf long txhub ' hub address of txbuf long rxhead ' rx head index long rxtail ' rx tail index long txhead ' tx head index long txtail ' tx tail index long txdelay ' ticks to transmit one byte byte rxbuf[BUF_SIZE] ' buffers byte txbuf[BUF_SIZE] byte pbuf[80] ' padded strings
The real part of the interface is this section...
long rxhead ' rx head index long rxtail ' rx tail index long txhead ' tx head index long txtail ' tx tail index
Since we have a reasonable 512KB, providing a few extra longs to convey some additional info would be nice. What might these be?
* tx and rx pin
* baud
I would also like to add in
* rxsize
* txsize
This would make the buffer sizes flexible tho we have to account for this.
The padded string(s) would be defined in the spin section so it would not be part of the buffer interface (or mailbox). And the driver cog number is probably not required here.
Question(s)...
* Would a word (16 bits) be good enough for the head, tail and size? This is 64KB after all !!!
Why? Well it depends on whether we want to pack the heads/tails/sizes for all ports into one contiguous group and the buffers separate to these. There are some advantages to using PTR[offset] for getting these values.
So here are two alternatives for packing the buffers and pointers (I've just used longs for now)...
Repeat for each port
long rxhead ' rx head index long rxtail ' rx tail index long txhead ' tx head index long txtail ' tx tail index long rxsize ' rx buffer size long txsize ' tx buffer size byte rxbuf[BUF_SIZE] ' buffers byte txbuf[BUF_SIZE]
or
long rxhead1 ' rx head index long rxtail1 ' rx tail index long txhead1 ' tx head index long txtail1 ' tx tail index long rxsize1 ' rx buffer size long txsize1 ' tx buffer size long rxhead2 ' rx head index long rxtail2 ' rx tail index long txhead2 ' tx head index long txtail2 ' tx tail index long rxsize2 ' rx buffer size long txsize2 ' tx buffer size long rxhead3 ' rx head index long rxtail3 ' rx tail index long txhead3 ' tx head index long txtail3 ' tx tail index long rxsize3 ' rx buffer size long txsize3 ' tx buffer size long rxhead4 ' rx head index long rxtail4 ' rx tail index long txhead4 ' tx head index long txtail4 ' tx tail index long rxsize4 ' rx buffer size long txsize4 ' tx buffer size byte rxbuf1[BUF_SIZE1] ' buffers byte txbuf1[BUF_SIZE1] byte rxbuf2[BUF_SIZE2] ' buffers byte txbuf2[BUF_SIZE2] byte rxbuf3[BUF_SIZE3] ' buffers byte txbuf3[BUF_SIZE3] byte rxbuf4[BUF_SIZE4] ' buffers byte txbuf4[BUF_SIZE4]
If you want to be able to start and stop ports at will, then the interface will likely need an extra interface for port number, txpin, rxpin, baud, txbuf and rxbuf addresses and txsize and rxsize for sizes, and a flag indicating if it is running. txpin, rxpin and flag can be bytes within a long, baud is a long, and the txbuf, rxbuf need to be a hub address so 20 bits minimum so a long each.
Ideas???
Comments
The UART manager cog could check a mailbox that would have a long broken into a word and two bytes
word[1] is the hub address of the port structure
byte[1] is add or remove
byte[0] is the port (0 to 7) to add or remove
If there are not port changes a value of 0 in the mailbox would indicate there is nothing to do but check on the buffers.
If a buffer is added, a bit flag is added to available ports and the cog reads the configuration data it needs (mostly pointers) from the hub. If a port is removed the bit is cleared so that those pins are no longer checked on scans.
The foreground Spin can configure the smart pins for serial to take that burden off of the cog -- this keeps things simple (I think).
Thanks for commenting so quick Jon.
I think we would need a table of bits for the currently in-use ports so a byte for 8 ports. I do this for mapping used stay-resident cogs in my P1 OS and it woks nicely.
I think the hub address of the port structure should be at least 20 bits but we could combine the port number and add/remove bit within the long, so your mailbox would work.
BUT, the problem comes with multiple cogs trying to get a port concurrently. We would need to use a lock or we have to have separate longs for each port and continuously read them all by the driver cog.
Now if we used 2 longs (ie 8 bytes) as just a flag for each port, the driver could read these 2 longs using a setq #2-1 & rdlong and compare with zero, OR the driver could read each long using the wz flag. Neither of these would take too long to interfere with the rx/tx scanning. A further set of 8 longs would follow with one dedicated to each port. This would be quicker than using locks.
Smartpins may as well be set/cleared by the driver cog as there is plenty of cog space and it's only a couple of instructions. Might just need to be interleaved within the port scanning. Thinking more, yes, could be better for the calling cog to set the smartpins as the bitper needs to be calculated from baud and clkfreq .
Do you see that the data buffers for each port should be in a contiguous area in hub? Or do you think separate areas for each port would be better?
IMHO it would be nice to have separate hub areas but it's more complex and might be even more confusing to newcomers.
And if they are contiguous then there is no need for the hub pointers for each port.
And INCMOD allows sizes to not be a binary multiple.
Yes, I stole it from you. It just needs to be a variable
If rx and tx buffers are all contiguous...
Or if we allowed tx/rx buffer pairs to be anywhere in hub, then this...
I've not looked at this code/design in detail but one (probably helpful) reason to separate port control into a byte per port is that it allows the independent port control settings to be setup/changed independently from different COG contexts without extra code needed to preserve any other port state during the change. i.e. it potentially avoids the read-modify-write cycle needed to change the value for the port control byte when the byte containing it in hub memory spans two actual port control nibbles, and you can then just do a single byte write atomically instead and simply clobber its prior value as you change it. There's no additional nibble shifting needed either. In this case sure it might burn an extra long of hub memory, but with 512kB I'd say it would be well worth it.
In my embedded/middleware work over the years I've found it's useful to do little things like that for assisting multi-threaded code, particularly when state can change dynamically after initialization. If you try to pack things a bit too tight you can come unstuck in some areas so it's a tradeoff as usual.
Maybe make two versions, one supporting eight ports with a lower top speed, and the other with four ports for higher speeds.
This has led me to the following thoughts and I'd like to know if this makes sense...
* Sixteen uni-directional ports each with its' own control byte in an array (solves read-modify-write or lock problems)
* Each port is for 1 pin only and can be either a receive or a transmit pin (ie you can have 16 receivers if you want)
* Each port will have a p_head and p_tail which are pointers to hub addresses (ie not offsets)
* At initialisation the p_head and p_tail will point to the first and last+1 hub address of the buffer
* The calling code will setup and disable the smart pin (ie the driver will not program the smart pin associated with the port)
' Main hub interface (allows up to 16 uni-directional ports)
port_control 'byte 0[16] '\ control (16 ports) %atpppppp where...
'| a: 1 = port active, t: 1 = tx, 0 = rx, pppppp: = port pin
byte 1<<7 | 0<<6 | 63 '| port[0]: active, rx, P63
byte 1<<7 | 1<<6 | 62 '| port[1]: active, tx, P62
byte 0[14] '| port[2]-[15]: = inactive/idle
port_params 'long 0[2][16] '| max (16 ports) of 2 longs... (note: only need to reserve as many ports as max required)
long @xbuf_0 '| port[0]: p_head (initially set to start of buffer)
long @xbuf_0_end '| p_tail (initially set to end of buffer)
long @xbuf_1 '| port[1]: p_head (initially set to start of buffer)
long @xbuf_1_end '| p_tail (initially set to end of buffer)
' long 0[2][14] '/ port[2]-[15]: (no need to fill/reserve unused ports)
' buffers (no need to follow port_params)
xbuf_0 long 0[128] ' port[0]: buffer
xbuf_0_end
xbuf_1 long 0[128] ' port[1]: buffer
xbuf_1_end
This minimises the hub when only a small number of uni-directional ports will be used. Just leave space for the maximum number of supported pins (ie uni-directional ports).
This simplifies the number of instructions needed to be executed in the pasm driver, and therefore maximises the number of ports and speed that can be supported.
This has led me to the following thoughts and I'd like to know if this makes sense...
* Sixteen uni-directional ports each with its' own control byte in an array (solves read-modify-write or lock problems)
* Each port is for 1 pin only and can be either a receive or a transmit pin (ie you can have 16 receivers if you want)
* Each port will have 4 long parameters...
- p_head pointer to current head hub address of the buffer (ie not an offset)
- p_tail pointer to current tail hub address of the buffer (ie not an offset)
- p_first pointer to first hub address of the buffer (ie not an offset)
- p_end pointer to last+1 hub address of the buffer (ie not an offset)
* The calling code will setup and disable the smart pin (ie the driver will not program the smart pin associated with the port)
This minimises the hub when only a small number of uni-directional ports will be used. Just leave space for the maximum number of supported pins (ie uni-directional ports).
This simplifies the number of instructions needed to be executed in the pasm driver, and therefore maximises the number of ports and speed that can be supported.
By having p_first and p_last available it reduces the number of instructions necessary to calculate the start and end and can be accessed easily by both spin and pasm objects.
It’s probably advisable for the setup code to walk the table to look for pin conflicts before adding new entries. That wouldn’t be particularly difficult to achieve.
Same applies to configure the smart pins. The caller setting up the smartpins also lets the caller configure baud and number of bits.
I’m almost ready to test it out but i will not have time tomorrow due to day job. Mondays are always hectic.
While it’s unusual, having half-duplex (ie single pin uni-directional) ports seems like it will work nicely. You can have any mix of receive and transmit pins (ports) to a maximum of 16 pins. So you can have 1 transmit and 15 receive, or any other mix to a total of 16.
Using SETQ and rdlong minimise the number of clocks when getting parameter blocks from hub. P2 has some really neat instructions.
Yes, it does.
Yes, it's easy enough for the caller to walk the table and check the pin's not in use. Of course it cannot know if the pin is being used by a different driver eg I2C.
True.
Code looks ready for testing to me; Seems to be striking a good balance between clarity and speed.
Yes. Just ran out of time to test it with a new front-end. I will build at least two front-ends. One will just be a full duplex (ie 2 port pins) object that will be pre-configured for the masses. The other will be an extendable version and it will probably undergo a few revisions to get it easy to use.
Only tested with FlexProp v5.0.7
pnut and PropTool testing to come soon. pnut working.
mpx_multiportserialdriver.spin2
This is the pasm driver that is loaded into it's own cog. The code is working and AFAIK with no bugs. It will support up to 16 half duplex ports - any mix of receive ports (pins) and transmit ports (pins) up to a total of 16. Smartpins are setup in the calling program, as are the port control and parameters.
mpx.demo.spin2
This is a demo program and just calls mpx_fullduplexserial.spin2. This is a simple demo program that uses a few methods to display and receive some serial data. Nothing major here.
mpx_fullduplexserial.spin2
This program is based on @JonnyMac 's jm_fullduplexserial.spin2 code. This is great code which includes methods to display strings with embedded hex/decimal/binary etc. A really great object.
I have added some additional methods to display such things as hub dumps which includes both hex and ascii representation.
Also, this is now using my new mpx_multiportserialdriver.spin2 driver.
Warning: I know there are some problems in the receive section because I haven't completed the process to use the new style receive buffers. I have just released it so you can get a preview. AFAIK the transmit methods should work correctly.
If you find any bugs please let me know here, aside from the receive methods (rxcheck, rxavailable, etc) which haven't been converted yet.
See next post for latest code
txflush, rxcheck, available, etc should now be working
Please report bugs here.
Glad to hear you have this running. I do hope that diverbob is following this thread as I know he needs at least 6 serial ports for his robot.
Jim