logo
background
 Home and Links
 Your PC and Security
 Server NAS
 Wargames
 Astronomy
 PhotoStory
 DVD making
 Raspberry Pi
 PIC projects
 Other projects
 Next >>

I2C serial comms by bit banging

PIC serial o/p
 
What is “Bit Banging” ?
'Bit Banging' is PIC code written to send data one bit at a time using an i/o pin for serial transmission. The code is typically highly optimised in order to maintain the maximum possible speed. This means the code is usually written in 'assembler' and 'byte in line' (i.e. the code is written as a 'stack' that sends 8 bits one after the other, rather than  a 'loop' or subroutine that sends 1 bit (and has to be called 8 times to send a byte)).
 
Plainly, such code is required only for PIC devices that do not have any sort of built-in serial transmission hardware (i.e. the low end PIC16C5x and 16F5x devices) and is made even harder as these devices support a limited instruction set. All mid range and above PIC devices have serial 'shift register' I2C circuit built in, so it's ONLY the low end devices that need to 'bit bang'.
 
How does I2C work ?
I2C is a 2 wire bi-directional 'bus' (SCL = clock, SDA = data) that multiple devices can attach to using a 'Master / Slave' control scheme.
 
I2C data and clock diagram
Both wires are fitted with 'passive pull up' resistors (and must never be driven with an 'active' Hi - which causes particular problems for a low-end PIC 'emulating' I2C with 'bit banging' because you are forced to use the single 'TRIS (Acc to Port Tri-state Latch)' command to switch SCL/SDA i/o pins from 'active Lo' (0v) to 'tri-state' (off) and back again - and this runs you straight into the limitations of the instructions that 'work' with the Accumulator (i.e. the W 'register')).  

Since multiple 'Master' devices can exist, the first step is to 'take control' of the bus with a 'start sequence'. Once that has been achieved, the 'Master' then has to 'select' a 'Slave' before it can actually send data. Having selected the Slave, the PIC then sends the 'destination register/address' (within the Slave) and finally the data i.e. :-   1. Send a start sequence 2. Send the I2C address of the slave with the R/W bit low (even address) 3. Send the Slave register number / address you want to write to 4. Send (or receive) the actual data byte(s) (if the destination (address) is a 'buffer', you can send multiple data bytes to 'fill the buffer' **) 5. Send the stop sequence.   **Note that, when sending to the Slave, the Slave must 'respond' with an 'ACK' after each byte before you can send another one.   Whilst I2C only needs 2 wires, the data transmission protocol is rather complex. Sending each data bit 'costs' at least 3 instructions (set data bit, set Clk Hi, set Clk Lo) - where-as RS232 (a 'straight' serial protocol) requires only 1 instruction per bit (set data). I2C however, is a lot more 'robust' (the use of a Clk means the inter-bit timing can be whatever you choose - indeed, it can even vary from bit to bit , whilst RS232 has to comply with a strict 'baud rate' timing)   Gaining control of the I2C bus In a multi-Master system, 'Bus Contention' can occur when two Masters try to gain control of the bus at the same time. The I2C bus is 'idle' when both lines SCL & SDA are 'Hi'. Further, during the transmission sequence, SCL (clock) is 'normally Lo' (i.e. it is 'pulsed' hi to transmit a bit). So, if a Master 'spots' both lines are Hi and notes SCL is Hi for 'a significant' period, it can assume the Bus is 'free' and take control.   To 'take control' (start a transfer) a Master pulls SDA Lo BEFORE pulling SCL Lo. To 'give up' (end a transfer) the bus, the Master ensures that SDA is Lo, then lets SCL go Hi BEFORE allowing SDA to go Hi again. Other Masters that are waiting for control of the Bus can look for the 'end transfer' sequence and 'jump in'.   It is, of course, possible for a 'race condition' to occur when two Masters both 'spot' the Bus is idle and both issue a 'start' at the same time. This will lead to garbled transmission - however by 'checking' the state of the SCL and SDA lines during transmission a Master can quickly detect when their 'Hi' is being corrupted by another Masters 'Lo'   Optimised 'bit banging' code Many 'tutorials' offer you a 'C code' approach, which, of course, is never going to be 'as fast as possible'. Another common mistake is to offer 'general purpose' code more suitable for PIC 18Fxxx devices with many kb's of instruction space (and is a big joke on a 16F54 PIC that has space for only 512 instructions :-) ), however most 18F devices have built in I2C hardware !   Finally, some authors go out of their way to create code capable of dealing with 'any' bit counts ! Since I'm a great believer in the KISS principle, I assume all I2C transmissions are 'byte based' (if your project calls for something else, feel free to modify my code :-) )   The code I present here is aimed at the 16F54, and fits well within the 256 subroutine space (so no nonsense with 'paging', and no need for 'Call tables')     TRIS restrictions   The PORT X Tri-state latch is set using a 'COPY Acc,rXtristate' instruction (setting a bit 1 = tristate, 0 = active). For serial transmission, we used the PORT output latches and the Rotate command to 'shift' bits down into PORT b0 (or up into b7) - we can't do that with the Tristate latch because the current settings can not be Read (which means none of the shift (RotL, RotR) or 'bit' (Btst, Bset / Bclr) instructions will work).   To output a data bit, we start with the PORT pins tri-state. Then we pull the Clk Lo, set the data bit (pull Lo or leave Hi), then, whilst maintaining the state of the data bit, we tristate the Clk again. If the PORT data register is set to 'all 0' (0x00), we only need to use the Tristate latch, however plainly multiple instructions are going to be needed 'per bit'.   I2C pseudo code
Clear PORT A o/p latch
Read PORT A, b0 (SDA) - if Lo, jump to BusBusy:
Read PORT A, b1 (SDC) - if Hi jump to StartSeq:
 
BusBsy:	;here another Master has control, wait for end sequence
(loop waiting)
 
StartSeq:  ;OK, Bus idel, lets go
Send 0x0E to TRIS PORTA		; enable b0 (only), = set SDA Lo (SCL is Hi)
Send 0x0C to TRIS PORTA		; enable bo and b1, = set both SDA and SCL Lo
; OK, we have control ... now we send the Slave address (note, b0 indicates if we are reading (hi) or writing (lo) the Slave)
Copy SlaveID to Acc
Call SendByte:
; We have the Slave, now send the Control code (Slave register address)
Copy SlaveReg to Acc
Call SendByte:
; OK, now read or write a byte
Test SlaveID, b0 - if Hi jump to I2CRead:
; it's write, so do it
Copy SlaveData to Acc
Call SendByte:
; OK all done, let the I2C bus go
Send 0x0C to TRIS PORTA		; enable b0 and b1, = set both SDA and SCL Lo
Send 0x0E to TRIS PORTA		; enable b0 (only), = keep DA Lo whilst allowing SCL Hi
Send 0x0F to TRIS PORTA		; disable both bits, SDA and SCL both Hi
; and that's it !
RETURN
I2C 'bit banging' code The fasteset way to 'get the next data biot;' is to use the Rotate (via Carry) instruction 'RotL rData' = the next bit (b7 first) is copied into Cy and the register updated (ready for the next shift) The fastest way to get the Data bit (in Cy) into Acc b0 (or b7) is to do another Rotate. The 'problem' is the I2C Clk. It has to be kept 'Lo' when setting the data bit, then a Hi 'pulse' has to be issued whilst holding the data bit steady. NOTE. The whole of the 4bit PORT A will be used for 'big banging' (i.e. the state of the 2 'unused' i/o pins is lost). This 'saves' a considerable number of instructions, however it does prevent the other 2 i/o pins being used as outputs at the same time as I2C operations (they could be used to drive 'status' LED's with zero effect on the I2C code so long as it's OK for the status to be 'invalid' during I2C operation). PORT bit0 must be used as SDA (serial data), however any of the other 3 bits could be used as SCL (serial clock) = I chose b1. ;power-on CLR rPortA ;set all data o/p's Lo, only need to do this once Now the data byte output code ;output 1 data byte ; byte is in rData and will be lost CLR rZero ;need a register full of zero's ; On entry, Clk is Lo, data is Hi ; start by pulling data bit Lo CLR rPortAtristate ; enable all outputs (data latch is 0, so now both Clk and Dat are Lo) ; do a bit (7 instructions) RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; OK, do next data bit 6 RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; OK, do next data bit 5 RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; OK, do next data bit 4 RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; OK, do next data bit 3 RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; OK, do next data bit 2 RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; OK, do next data bit 1 RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; OK, do next data bit 0 RotL rData ;get data b7 to Cy, leaving Acc unchanged RotL rZero,Acc ;get b7 to Acc b0, leaving rZero unchanged COPY Acc,rPORTAtristate ;set the data bit Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;set the Clk Lo ; all bits done ; now need to 'clock in the Slave ACK Bset Acc,b0 ;set b0 (data) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;Clk Lo, data released Bset Acc,b1 ;set b1 (Clk) of the Acc (OR Acc,0x01) COPY Acc,rPORTAtristate ;set the Clk Hi Bclr Acc,b1 ;clear b1 (AND Acc,0xFD) COPY Acc,rPORTAtristate ;Clk Lo again, data released (hi) ; ; done, total 2 + 49 + 6 = 57 instructions per data byte ; NOTE - this is without any VCALL / RETURN or the 'slave adressing' prefix overhead

Next page :- PIC serial parallel converter

[top]