Hamby wrote on 2024-11-21, 00:54:
Thanks but I was thinking more in terms of parallel coms, not serial coms 😀
Here is an overview. We're assuming it's a bare-bones, output only parallel port.
For LPT1: your data port (output) is 378h. Whatever you put there goes to pins 2-9 of the parallel port.
Your status port (input) is 379h. The bits in it are 0:reserved, 1: reserved, 2:IRQ, 3:ERROR#, 4:SELECT, 5:PAPER_OUT, 6:ACK#, and 7:BUSY#
For more detail, https://wiki.osdev.org/Parallel_port
Assuming your LapLink cable has standard pinout, bits 0 through 5 go to pins 15(ERROR#), 13(SELECT), 12(PAPER_OUT), 10(ACK#), and 11(BUSY#) on the other side. Now we see the reason for the pinout. So, whatever we write to the output port is shifted to the left by 3 bits at the status port at the other end. Despite a few of these status lines being marked as active-low, it is only BUSY# that appears inverted at the other side, when using a null-printer cable (https://bitsavers.org/pdf/ibm/pc/pc/6025008_P … rence_Aug81.pdf p. 2-68).
The other piece to mention here is that when set up properly, the parallel port will generate an IRQ 7 on the proper transition of the ACK line. That allows your nibble-mode parallel port code to transition between idling and intensive bit-banging polling-mode operation. The latter is needed when exchanging nibbles to make it fast. It is a per-nibble, stop-and-wait protocol between the two sides. Another general rule is to not accept the value of the status port until it doesn't change on two consecutive reads (and you might have some number of microsend delay on reading it too).
You could review the Linux plip_send for an idea: https://github.com/torvalds/linux/blob/master … lip/plip.c#L710
Basically the sending protocol there is:
- Place the low nibble on bits 0 through 3 of output and set bit 4 to zero. Then flip bit 4 from zero to one. This causes the BUSY# line to transition from one to zero on the opposite end.
- Continue reading from the status port until bit 7 (BUSY#) is zero, meaning that the other side has set bit 4 to one on their end, acknowledging the incoming nibble.
- Place the high nibble on bits 0 through 3 of output, continuing to leave bit 4 set to one.
- Flip bit 4 from one to zero. This causes the BUSY# line to transition from zero to one on the opposite end.
- Continue reading from the status port until bit 7 (BUSY#) is one, meaning that the other side has cleared bit 4 on their end, acknowledging the incoming nibble.
If you look at plip_receive, line 484, it is symmetric. Wait until status port until bit 7 clears, then grab bits 3 through 6 as the low nibble. Set output port bit 4. Wait until status port bit 7 sets, then grab bits 3 through 6 as the high nibble. Clear output port bit 4.
Aside from that, the complexity is in the state machine and all in the timeouts, checksums, and other error handling (like collisions) and in triggering the interrupt on the other end when needed.
As you can see at line 859, the implementation idles by writing zero to the output port (so status port bit 6, ACK#, rests at 1), and on line 777, wakes up the other end by writing 0x08 to the output port (causing status port 6, ACK#, to transition from 1 to 0 at the other end, generating an IRQ and waking it up).