As it came up recently in a similar thread, but without the use of VIAs, I thought about how I'd do this with a VIA. This is untested but I think it should work.
I searched around, and other VIA-based methods I've seen mostly revolve around bitbanging - but I think it should be possible and quite comfortable to use the shift register as Garth suggested higher in this thread, to minimise CPU overheads, maybe requiring only one interrupt per character received from the keyboard.
This circuit is quite appealing as it only requires a few small external components:
Attachment:
File comment: VIA-based PS/2 circuit - with SR bug
ps2keyboard_via_bad.png [ 17.56 KiB | Viewed 3459 times ]
However, it suffers from the shift register bug, so we do actually need to add another IC, following
Garth's advice to work around that:
Attachment:
File comment: VIA-based PS/2 circuit with workaround
ps2keyboard_via_fixed.png [ 17.12 KiB | Viewed 3459 times ]
This should allow bidirectional communication with the PS/2 device. The general principle is to use PB6 pulse counting via Timer 2 to get interrupts after certain bits are transmitted. You could also use CB1 interrupts or the SR interrupt, but using PB6 pulse counting alone means that for the common case of reading data we only need to spend one interrupt per frame, exactly when the data is available.
So initially we set PB6 as an input, enable shift register input with an external clock source (mode 011), and enable Timer 2's pulse counting mode. The PS/2 clock falling causes CB1 to rise on the next PHI2 leading edge, shifting data into the shift register. First we count 9 pulses, to cover the start bit and eight data bits:
Code:
start D0 D1 D2 D3 D4 D5 D6 D7
CA2 """\___/"""V"""\___/"""\___/"""V"""V""
PB6 """"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_
T2 9 8 7 6 5 4 3 2 1 0
T2 IRQ ---^
When the interrupt arrives, we read data out of the shift register and reset Timer 2 to count another 11 pulses - that covers the parity bit, stop bit, next start bit, and 8 data bits - so this will give another interrupt after the next character arrives:
Code:
start D0 D1 D2 D3 D4 D5 D6 D7 par stop start D0 D1 D2 D3 D4 D5 D6 D7 par stop
CA2 """\___/"""V"""\___/"""\___/"""V"""V"""V"""V"""""""""""\___/"""V"""\___/"""\___/"""V"""V"""V"""V""""
PB6 """"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"""""""""\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"
T2 9 8 7 6 5 4 3 2 1 0 B A 9 8 7 6 5 4 3 2 1 0 B A 9
T2 IRQ ---^ T2 IRQ ---^
This is not doing any checking of start/parity/stop bits, so could be vulnerable to going out of sync. It may be desirable to accept two interrupts per frame, reading the first 8 bits from the shift register in the first one and the last three bits in the second one, so we can check the start/stop/parity bits are correct and resynchronise if not:
Code:
start D0 D1 D2 D3 D4 D5 D6 D7 par stop start D0 D1 D2 D3 D4 D5 D6 D7 par stop
CA2 """\___/"""V"""\___/"""\___/"""V"""V"""V"""V"""""""""""\___/"""V"""\___/"""\___/"""V"""V"""V"""V""""
PB6 """"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"""""""""\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"\_/"
T2 8 7 6 5 4 3 2 1 0 3 2 1 0 8 7 6 5 4 3 2 1 0 3 2 1 0 8
T2 IRQ ---^-----------^ T2 IRQ ---^-----------^
If we need to pause data transmission or interrupt a character, we can briefly set PB6 as a low output to pull the PS/2 clock line low. This interrupts the device and we can set T2 to its initial value again, get an interrupt where we want it during the next character. This could be an appropriate response to detecting a framing error.
To send a character to the device, we need to pull the PS/2 clock (PB6) low as above, and then pull the PS/2 data line low by setting CB2 as a low output, then let the clock float again (by setting PB6 as an input). There are some timing constraints here. Then we need to send data in sync with the clock coming from the device. That can be done either by bitbanging CB2 based on CB1 rising-edge interrupts, or using the shift register, but note that we have more than 8 bits to send, so if we are using the shift register then we need to catch the interrupt on shift register completion (or use PB6 pulse counting again) and send a second byte at that point, in order to fill out the full frame. The second byte won't complete (the device won't send enough clock pulses) so we also then need to count the pulses and, after the frame is over, turn off the shift register output so that we're ready to receive data again.
It's also possible to poll timer 2 to determine progress during a transmit or receive, or to determine whether we are between frames - I'm not sure if that's useful for anything though.
Overall this should allow receiving data with only one interrupt per character, or two if you want to be a bit safer and check for framing errors, and allows for interrupting the device, and sending data to the device - so I think it should cover all the bases. Sending may be less efficient (requiring maybe two interrupts to manage the sending operation) but as that's rare, I don't see that as a big problem. It seems quite flexible, and while it's a shame it requires an extra IC in addition to the VIA, that's not bad overall!
Edit: I've built this now (the one with the 74HCT74 to guard against the shift register bug), and it works pretty well. The shift register is an awkward little thing to get working properly though - it doesn't act exactly how any of the datasheets say (comparing the WDC ones to the Rockwell ones, and to the old typewritten MOS one). The upshot is, we do need to use two interrupts per input character - but it's not wasted, as it allows doing all the framing and parity checks. I've attached the code for reference in case it's useful to anyone - this uses interrupts for reads, but bit-banging for writes (as they are much more rare) - though it should be possible, with care, to use the shift register for writes as well.
Edit 2 - I looked into using the shift register for writes as well as reads, but it's not a good fit because it changes the data on the wrong clock edge. To make that work you'd need some external circuit to uninvert the clock during writes, which is not really practical. The shift register would also drive CB2 high as well as low, when it's meant to be an open-collector signal, so it's not a great fit really.