Thursday 17 September 2015

I2C bit-banging between two PIC microcontrollers

After soldering up a few new style PCBs and trying out a couple of board game panel sections, we came across a fatal flaw in the design. Currently, we're using a three-pin audio stereo cable to join our board game sections.


In a perfect test environment, these work really well. But when we put four or five board panels onto a table and tried connecting them, things started to go a bit awry.

Firstly, we started getting spurious data at the controller end. If the plug was slightly tight fitting into the audio socket (as some were, the first time the socket was used) we received peculiar characters over serial at the controller. Not a major problem - we simply add a checksum to the end of each message and if the contents of the message don't match the checksum, we just ignore the entire message.

But something else started happening too - some of the previously connected boards started to reset. After placing pieces on them and tracking where they are, a board reset is a really bad thing. It makes the board section forget everything it knows about the pieces placed above it, so when you pick a piece up, it registers that something has changed (one of the hall sensors has changed state) but not that you've removed a piece (because the board has "forgotten" the piece above, it thinks the change is placing not removing a piece). Again, this is something that could be factored out using firmware changes, but it's a bit alarming that some boards are resetting.

So what's going on?
Well, it seems that putting the power on the tip of our stereo plug isn't great. The idea was that the power pins would be the last pins to connect as the plug is pushed into the socket, but in truth, it doesn't matter which (tip, centre or ring) carries the power and/or ground lines - if the plug isn't inserted absolutely square to the socket (so any part of the plug touches any part of the socket during insertion) we can get power shorts and reset conditions on the power line.

So now we're back to looking at keeping our pins a row, rather than "line them up" behind each other. Thanks to the popularity of RGB LED strips, four-way plugs and sockets can be found really cheaply.


And if we're going to use these, we'll have four (rather than three) wires in total. The shape of the plug and socket mean you can't plug them in the wrong way around (without really making an effort) and there are two cables, rather than one, available for sending data.

Now, with two wires, rather than a single one, and a topology consisting of a master controller and a whole load of clients/slaves on a common bus, and immediately we're thinking about I2C as a communications protocol.

I2C is a great protocol for common bus communications. Single wire is ok, but it does rather depend on each device only trying to talk one-at-a-time. I2C actually enforces this rule, using a clock and data line. To send data, you simply check the state of the clock line and if it's in use, wait for it to become free. But the best thing of all about I2C (when implemented properly) is that there's no danger of "power clashes".

I2C basically consists of a clock line and a data line. The sending device raises or lowers the data line, then toggles the clock line. When the listening device sees the clock line go high, it checks the state of the data line. If it is high, that's a binary one. If it's low, it's binary zero. Simple huh?

But it gets even better than that. Let's say two devices - for whatever reason - try to use the clock and data lines. One device sets the data line high (sending a binary value one) but the other device tries to send it low (sending a binary value zero). If we're actively driving the line(s) high and low, we've got a "power clash" - we're effectively shorting our power and our ground lines together!

To avoid this, I2C doesn't actually drive the lines high and low. It uses external pull-up resistors to let the lines float high, and actively pulls them down to ground. So instead of driving the line "up" and "down" we either drive the line low/down or simply let go of it (and it "rises" high of it's own accord).

This is the crucial idea behind the I2C bus. It allows loads of devices to share the lines, but if two or more devices try to talk at the same time, we don't get power shorts. Of course, the data coming off the lines would be garbage, but we've not actually damaged anything. Consider two devices now trying to talk at the same time. The first device drives the data line low (sending binary zero) but the second device - instead of driving the line high (which would create a short) - simply lets the line go (to send a binary one). The result is that binary zero gets sent: not what device two wanted, but it's not trying to force the line high against device one pulling it low.




To make the lines work properly, we need open drain collector transistors to pull the data and clock lines to ground. Rather than using external components, however, we're going to try to use our PIC i/o pins.

Now, PIC microcontrollers don't all have the ability to make their i/o pins "open collector" but they do have a tri-state: input, output, hi-z (high impedence). If we disable the internal pull-up resistors on an i/o pin and make it an input, it's treated as a hi-z pin (high impedence means we can treat it as if disconnected from the rest of the circuit). So rather than turn an output pin on and off to create our high and low signals, we're going to toggle the pins between output/low (drive the line to ground) and input/hi-z (disconnect and allow the line to  "float high").

With all this in mind, we threw together a few routines to generate an I2C output signal. One last thing of note: in the I2C protocol, the data line should only be changed while the clock line is being held low. There are only two scenarios when we should allow the data line state to change while the clock line is high - these are to generate the "start" and "stop" conditions on the line(s).




Here's some code to send a message out over two I2C wires:

Define CONFIG1 = 0x0984
Define CONFIG2 = 0x1dff
Define CLOCK_FREQUENCY = 32
AllDigital

declarations:
     Symbol i2c_clock = PORTA.0
     Symbol i2c_data = PORTA.1
     Dim msg_out As String
     
initialise:
     WaitMs 1000
     msg_out = "Let's go"
     Gosub send_msg
     
loop:

Goto loop
End



send_msg:
     
     Gosub wait_for_free_clock
     
     'now we've got the clock line, wrench it low to tell everyone else we're talking
     'to pull the line low, we need to set the pin to an output then set it low
     'to raise the line, however, we set the pin to input (hi-z) and let it float high

     Gosub clock_low
     WaitMs 2 'give it a few milliseconds for everyone to start listening
     
     j = InStr(msg_out, 0x0d)
     If j = 0 Then msg_out = msg_out + CrLf
     j = Len(msg_out)
     j = j - 1
     
     For i = 0 To j
          k = msg_out(i)
          'send the data out, MSB-first
          For t = 0 To 7
               h = k And 10000000b
               If h = 0 Then
                    Gosub data_low
               Else
                    Gosub data_high
               Endif
                         
               Gosub clock_high
               WaitUs 2
               Gosub clock_low
                    
               k = ShiftLeft(k, 1)
          Next t
     Next i

     'release the clock line to let it float high for the next message
     '(from whichever device on the bus wants to use it)
     Gosub clock_high
     
     'the stop command in i2c is SDA goes high while SCL is high
     Gosub data_high
               
Return



wait_for_free_clock:
     WaitMs 1
     While i2c_clock = 0
          'do nothing
     Wend
Return



clock_low:
     ConfigPin i2c_clock = Output
     Low i2c_clock
Return



clock_high:
     ConfigPin i2c_clock = Input
Return



data_low:
     ConfigPin i2c_data = Output
     Low i2c_data
Return



data_high:
     ConfigPin i2c_data = Input
Return


As it turns out, our PicKit2 programmer (clone) has a really handy feature- a simple three channel logic analyser. So all we needed to do was add the pull-up resistors between our clock and data lines and the power supply, set the clock line as the trigger condition (rising edge) and set our code running.

The results looked something like this:

Amazingly, we had a first-time success. The chart above shows the clock and data lines - the data line leads the clock line by a fraction of a millisecond (on the graphic they sometimes appear to be changing at exactly the same time). But it's important to note that the data line only ever changes when the clock line is  low (with the exception of the start and end bits).

We've also decoded the data line at each rising clock edge, and written out the ASCII characters for each byte received. And we can see that the output on the data line exactly matches the message we sent out!

No comments:

Post a Comment