The protocol is easy to implement in logic gates but also equally so in software. This could be life-saving if you must let two CPU or MPU communicate without dedicated interface and with bidirectional messages. This is possible with RS232/asynch serial for example but this is limited to 8-bit streams in practice, so framing is not inherent. MPUs usually implement SPI master features that are exploited. I²C is also rarely a slave device. And asynchronous communications usually require hard real-time constraints.
SPI4C uses more pins but none of them require special hardware, and no hard real-time constraint is required. A CPU or MPU can bit-bang the protocol as a part of an event loop, thus with some jitter but this is not critical.
The protocol is symmetrical so a single code/algorithm works on both peers. The needed resources are :
- A timer that can last about 1s (plus or minus a few potatoes)
- GPI : General Purpose Input bits : GPIa and GPIb are 2s contiguous bits ideally (to ease coding but not necessary)
- GPO : 2 General Purpose Output bits GPOa and GPOb
The algorithm also needs some variables per link :
- state_out : 2 bits, copy of GPO
- state_in: 2 bits, copy of GPI
- input_buffer : bytes received
- output_buffer : bytes to send
- data_to_send : number of bits to be sent from the output buffer
- bits_received : number of bit already received
- polarity : 1 or 0, if GPIa and GPIb are swapped
- link_ok : status flag (1 when communication is deemed working)
The status link_ok changes under these conditions :
- goes to 0 during initialisation or when the timer expires
- goes to 1 when the protocol receives a new trit
From there we have 3 entry points :
- Init() :
read state_in = GPI read state_out = GPO (if applicable) Send ACK : GPO = ( State_out ^= 3 ) trigger_timer( 1 second nominally ) data_to_send=0; polarity=0 link_ok=0
Nothing incredible here but we see that a copy of the GPO and GPI are kept, for faster operation and to compare with incoming data. We also see how to send a trit : XOR the state_out with either 1, 2 (data) or 3 (ACK).
- Timeout() :
link_ok = 0 (yeah it's a timeout so the link is now down) flush_bit_buffer() (flush any remaining pack that was not framed by ACK yet) bits_received=0 if (data_to_send) (just in case : close the current frame, and tell the peer that we're alive) data_to_send = 0 send ACK : GPO = ( State_out ^= 3 ) (beware if peer missed the last data bit and sees the ACK anyway, it would be interpreted as another bit, so ACK must be sent after closing/init here) trigger_timer(1s)
Here there is a little issue to shield from : if something was missed from either peer, both may want to end mostly at the same time, but they could have unfinished frames and they eventually could miss the ACK.
- new_trit() :
read GPI if GPI != state_in : link_ok = 1 re-read GPI (confirm the value !) trit = GPI ^ state_in state_in = GPI (to compare later) if trit == 3 { (handle ACK) if (bits_received) (end of frame ?) flush_bit_buffer() (call user function/hook) bits_received=0 } else { data=(trit & 1)^polarity input_buffer[bits_received >> 3] |= data << (bits_received & 7) bits_received++ } if (data_to_send) bit_to_send = output_buffer[data_to_send >> 3] send data : GPO = ( State_out ^= 1 << (bit_to_send & 1) ) (if bit=0 then send 01, if bit=1 then send 10) data_to_send-- else envoyer ACK : GPO = ( State_out ^= 3 ) trigger_timer(1s)
Here goes all the meat of the algorithm. This function is called periodically, thus polling the input pins if no "interrupt on change" is available.
Polling could happen every 10ms for example, then every 100us if activity is detected, or even directly under no-load condition with high traffic.
The higher levels of the interface will then manage simple buffers containing one frame. I didn't handle buffer overflows, by the way. I must even have messed a counter or two but it's pseudocode, you see the intent and you'll have to adapt to your own system.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.