Disclaimer:
I can search stackoverflow posts well enough to cobble stuff together, but software is definitley not my specialty. This is not necessarily a good way of doing this, just a way that I got it to work on my setup.
Requirements/Specifications:
Non-blocking - Must allow other code to run while waiting for the next command
Relatively low latency - Unclear exactly how important this is, shoot for <10 us between end of command and start of response
Commands are expected to be 200kHz with ~1.25us lows indicating 1 and ~3.75us lows indicating 0
Responses will be ~250kHz with ~1us lows indicating 1 and ~3us lows indicating 0
See simples website for the the 'joybus' protocol: https://simplecontrollers.bigcartel.com/gamecube-protocol
Implementation:
Current implementation uses one of the teensys hardware serial ports to allow for asynchronous reading and writing of data
The joybus protocol is approximated by the serial protocol in a 2 joybus bit per serial byte style, see: http://www.qwertymodo.com/hardware-projects/n64/n64-controller
While the serial port can read and write the data well, it is not good at doing so with low latency, because of this interrupts and timers are used to keep track of how many bits have been read or written in order to respond with low latency
Details:
two main functions involved:
bitcounter()
function called by the RX pin interrupt which will activate on the falling edge of every pulse on the data line
increments the bit count
if only one bit has been read (indicating the start of a new command) start a timer which will call the communicate() function and set the commStatus to Read (because we are reading a command)
communicate()
function that gets called using timers to read data out of the hardware serial port buffers, parse it, determine the appropriate response, and write the data back to the hardware serial port buffers to be sent over the data line
Process:
comm status is _commIdle
data line goes low causing interrupt, bitcounter runs:
start timer that will call communicate when the first 8 bits of the command have arrived
set the comm status to _commRead
40us later
communicate() gets called by the timer
if the comm status is commRead:
checks that we have 4 bytes of serial data (first 8 bits of the command)
read those 4 bytes and parse into a single byte of joybus data
set the serial baud rate to fast to be ready to send a response
decide if the byte is a probe, origin, or poll command, or something else (error)
if its a probe command:
set the timer to trigger again at the end of the response, send the preset probe response and set the write Queue to the probe response length + the command length
set the comm status to _commWrite
if its an origin command:
set the timer to trigger again at the end of the response, sent the preset origin response and set the write queue to the origin response length + the command length
set the comm status to _commWrite
if its poll command:
set the timer to trigger again a bit before the end of the poll command
set the comm status to _commPoll
if its something else:
got garbage data, reset everything, clear the serial port and try waiting for a stop bit to syncronize
some time later
communicate() gets called again by the timer
if comm status is _commPoll:
wait for _bitCount to reach 25, indicating that the poll command has finished
send the data in pollResponse (done immediately to minimize latency)
set the timer to trigger again at the thend of the poll response
set _writeQueue to the length of the poll response + the length of the poll command
set the comm status to _commWrite
if the comm status is _commWrite:
wait for _bitCount to equal _writeQueue, indicating that the response has finished
turn the serial baud rate back to slow
set the comm status to _commIdle
set _bitCount to 0
set _writeQueue to 0
clear the serial port in case there's any garbage data left to be ready for the next command
Phobos' Suggestions:
General cleanup and commenting (obviously)
Improved parsing of serial bytes to joybus bits, should be able to just look at bits 2 and 5 (6?) of the serial data to decide what the joybus bits are, rather than matching the whole byte
Try running in half duplex mode, this is a recent addition to the teensy serial library and worked with the teensy 4.0, should work here too. Will make things cleaner and will remove the need for the diode
Consider setting up the hardware serial directly rather than using the teensy serial libraries, may make things cleaner and lower latency
See if the response can be sent to the hardware serial buffers as an array rather than one byte at a time, I tried this but wasn't able to make it work without increasing latency significantly
See if we can interrupt directly from the hardware serial port on data available (not serialevent(), I think this can be done but was too complicated for me to implement quickly