PIC "Bit Banging", I2C serial comms What is “Bit Banging” ? 'Bit Banging' is PIC code written to send data one bit at a time using a minimal number of 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 written in 'assembler', sometimes 'byte in line' (i.e. as a 'stack' of 8 'send bit' fragments, rather than 'looping' 8 times sending 1 bit at a time). 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 PIC16 C/F 5x devices. The drawback of these devices is their rather limited instruction set. Coding the 16C/F5x devices Some of the instruction limitations are just 'annoyances' (such as the fact that a subroutine CALL destination is limited to the first 256 bytes of each memory page), which can be 'worked around' (eg by using 'indirect Call' tables - at the cost of at least 2 extra instruction cycles per 'Call'). Others - such as the fact that there is no 'increment Accumulator' (aka W "register") and the 'Inc/Dec register skip on Zero' instructions does not set the Z flag - enforce fundamental restrictions on your coding approach (whilst waiting to 'trip you up' should you forget which instructions effect the Status bits (and which do not)). If you do ANY assembler coding, I suggest you do it with a print-out of 'Table 9-2: Instruction Set Summary' from the PIC16F5X pdf manual in front of you ! Using the Microchip MPLAB IDE If you think web page CMS ('Content Management Systems') are a real pain, you should try Microchip's IDE ('Integrated Development Environment'). Written for DOS users, it can be almost impenetrable ! HOWEVER it does has one VITAL advantage - it offers full MACRO support, which allows you to write your own Macro 'library' and thus 'replace' Microchip's total confusing and non-intuitive 'PIC-speak' instruction 'names' with your own ! 'Blink that LED' - the PIC in action. Most PIC programs are written to 'blink a LED' when they start running in order to provide some 'instant feedback' to the user (and show that the code is, in fact, 'working'). Since most PIC projects quickly find a use for every i/o pins you will soon run out and start looking to 'double up' the function of some pins - and the 'serial output' pin is the obvious one to 'double up' as the 'blinking LED' drive - with the advantage that (if you run the 'bit banging' code slow enough) it's even possible to send visual messages :-) PIC power limits When driving multiple LED's you need to watch out for the PIC current limits = you may have 20mA 'per pin' to play with, however you are limited to 50mA 'per PORT' and 100mA for the whole device. In effect this means you can only use cheap 15-20mA LED's if you only require one or two - if you need to drive half a dozen or more LED's you will have to stick to the more expensive high efficiency 3-5mA LED's. Note that you can 'pulse drive' a LED with higher 'instantaneous' currents so long as the 'average' is within both the LED and PIC power limits (a LED driven with double the current for half the time appears a lot brighter to the human eye than one driven with the normal current all the time). NB. When choosing current limiting resistors, don't forget that the PIC16F5x only runs at 20MHz if you supply it with '5v' (the more LED friendly '3.3v' supply limits you to 10MHz). Why serial ? If you want to communicate with a PC, using it's Serial Port is a lot easier than using the Parallel (Printer) Port, especially as so few modern PC's actually have Serial or Parallel Ports (so, in practice, your PIC serial data will actually be wired to a Serial <> USB converter) Standard (UART implemented) serial comms is 'asynchronous' in that that both the transmitter and receiver have to be aware of (and maintain) the same fixed data rate. SPI and I2C are synchronous protocols - they have separate data and clock lines - which allows the transmitter to vary the bit rate 'as much as it likes'. For 'bit banging' this means the 'bit send' program code can be written as 'tight' as possible i.e. there is no need to 'pad out' a 'fast bit' to match the same CPU cycle count as a 'slow bit'. The disadvantage is that it takes twice as long to send 1 bit using a synchronous protocol (since you have to 'toggle' the clock before changing the data bit) I2C is similar to SPI, it that it is a two wire (data and clock) communication system, but differs in that it is 2 wire only (i.e. drops the two (or more) 'master select' / 'slave select' wires needed to implement SPI). More to the point, the industry standard 'I2C' serial comms protocol is directly supported by many peripheral chips such as RAM / EEPROM (storage) and analogue processing chips (amplifiers, DAC's etc) which are likely to find their way into your projects. Since I2C uses only 2 wires, it is about the 'least expensive' (in terms of hardware) choice, since the absolute minimum '1-wire' protocol is designed for low speed and is rather more complex to implement (it uses 64 bit 'ID codes' to identify devices). The '1-wire' protocol is used by Dell laptops to identify Dell Power Supplies (via the 'center' pin of the power plug), so may be of interest to those with Dell laptops that refuse to charge due to failure of the '1-wire' hardware (the pin is extremely fragile, as is the laptop socket) How does I2C work ? I2C is a 2 wire 'bus' (SCL = clock, SDA = data) that multiple devices can attach to using a 'Master / Slave' control scheme. Initially specified at Vdd=5v for data rates up to 100kHz, modern implementations typically support 3v3 and data rates in 'Fast mode' (400kHz) and 'Fast mode+' (1Mbps). These days, many devices also support 'High-speed' (3.4 Mbit/s) and 'Ultra Fast-mode' at up to 5 Mbit/s (see, for example, the UM10204 specification). Both wires are fitted with 'passive pull up' resistors (and must never be driven with an 'active' Hi). This causes particular problems for PIC '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. This means using 'clever tricks' to manipulate bits in the Accumulator (W 'register'). Most systems define 0.3Vdd as 'Lo' and 0.7Vdd as Hi. On a 3v3 I2C bus, 'Lo' will be 0.99v, on a 5v system Lo is 1.5v (or less). Since only 'Lo' drive is wanted (or permissible) it's possible to 'cheat' by using in-line low forward voltage (Schottky) diodes to 'eliminate' the Hi drive. This would add about 0.3v to the PIC i/o pin 'Lo' of 0.6v = 0.9v. This is well within the 1.5v limit for a 5v I2C system but rather near the max. for a 3v3 system Sticking with 'tri-state' approach (see below) requires about 7 CPU instructions to send each bit. On a 20MHz PIC, that's 28 OSC cycles and means a potential data rate of just over 710 kbps (i.e. requires a receiver that supports Fast mode+ (1 mbps)). Since the PIC is the 'Master', it can control data receive rate, so should have no problems clicking data out of a Fast mode+ (1 mbps) chip. Data has to be 'set' before it is 'clocked'. You are not allowed to change the data whilst the clock is Hi (except during the start sequence). Data Transmission starts with the MSBit and is a 3 step process :- Set the next data bit Set the clock Hi Set the clock Lo (.. repeat) 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 flag (bit0 = low is Master sends data, Hi is Slave sends data) 3. Send the Slave register number / address you want to write to / read from 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. 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' (and is a big joke on a 16F54 PIC that has space for only 512 instructions :-) ). Another common mistake is to write 'pseudo code' that is only really suitable for PIC 18Fxxx devices with larger instruction sets (and many kb's of instruction space) - however such devices typically have built in I2C hardware, so have no need for 'big banging' code :-). Finally, some authors go out of their way to create code capable of dealing with 'any' bit counts, which (of course) leads to 'count and test' loops sending single bits which adds multiple additional instructions to every bit transmitted. By assuming all I2C transmission is 'byte based', the 'loop' can be eliminated (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') NOTE. The TRIS latch controls all bits of a PORT and is not 'readable' = so there is no way to 'preserve' the existing i/o pin mode HOWEVER I will assume a 'copy' will be held in a Register (lets call it 'PortA-trisReg') PORT bit0 must be used as SDA (serial data). SDC could be any bit, however I'm going to use choose b1. NOTE that on entry to the I2C code it is assumed that b0,b1 are already 'tri-state', however the state of PORT A o/p data latch is not assumed. The big problem is the need to manipulate the TRIS bits in the Acc before using the 'Copy Acc to TRIS' command (this being the only possible way to change the TRIS latch, although at least the 'polarity' is correct (1 on TRIS = tri-state, 0 = enable o/p), so if the PORT Data latch contains a '0', the data 'bit' only has to be copied to ACC (and thence to TRIS) To extract the next data bit (without using Bit TEST instructions) we can ROTL the dataReg - this puts bit7 into Cy (and the Rotated Reg back to itself). The quickest way to get that bit into the ACC is to perform a ROTL with a 'dummy' Reg as the 'source' and Acc as the 'destination'. Whilst 'dummy' is not changed, the ACC (and thus TRIS) bits 1-7 will be taken from 'dummy' 0-6 and ACC (TRIS) b0 from Cy. So 'dummy' is, in fact, a copy of 'PortA-trisReg' that has been pre-shifted right 1 bit. Since I2C CLK has be be kept 'Lo' whilst data is being changed, b1 of the ACC must also be kept Lo. This means 'dummyReg' b0 must be a 0 (since, after ROTL, ACC b1=dummy b1, ACC b0=Cy). NB. Bit Set uses 'OR Immediate with ACC' and BClear uses AND Bit output is thus :- ROTL dataReg ;rotate the dataReg with top bit to Cy ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ... and loop for the next bit (there's no need to reset b0 data bit, it will be overwritten on the next ROTL from Cy) In the above 7 instruction sequence, the data bit is valid from 2 CPU CLK before CLK Hi and remains valid until changed. However the I2C CLK is 'Hi' for only 2 CPU Clk periods (which is only a problem if we are 'talking' to another PIC = see Data receive below) 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 of the Bus, so we have to wait for an 'end sequence' (loop waiting) StartSeq: ;OK, Bus idle, lets go with the start sequence Send 0x0E to TRIS PORTA ; enable b0 (only), = set SDA Lo (SCL is Hi) Send 0x0C to TRIS PORTA ; enable b0 and b1, = set both SDA and SCL Lo ; OK, we have control ... set up dummyReg ROTR PortA-trisReg,Acc ;get current TRIS, shifted Right BCLR b0,ACC COPY ACC,dmyReg ; OK, 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 SendByte: ; 7 Instructions per bit (plus some start and end overhead) ; On entry both Data and Clock pins are Lo, so Acc is 0x00 ; The byte to be sent is in dataReg (b7 is sent first) ; PORT A (b0-3) is used for o/p, b0 = data, b1 = clk ; Set the data pin, set Clk Hi, Clk Lo, then set next data bit ; ROTL dataReg ;rotate the dataReg with top bit to Cy ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ; ROTL dataReg ;next data bit data to CY ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ; ROTL dataReg ;next data bit data to CY ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ; ROTL dataReg ;next data bit data to CY ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ; ROTL dataReg ;next data bit data to CY ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ;ROTL dataReg ;next data bit data to CY ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ; ROTL dataReg ;next data bit data to CY ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ; ROTL dataReg ;next data bit data to CY ROTL dmyReg,Acc ;ACC b1=Lo (from dummy b0), ACC b0 comes from Cy = data top TRIS ;SDC stays Lo, enable b0 (if data was Lo) - data changes now BSET b1,Acc ;prepare SDC bit Hi TRIS ; o/p CLK Hi BCLR b1,Acc ;prepare SDC bit Lo TRIS ; o/p CLK Lo ; ; all 8 bits done, now need to clock in the Slave 'ACK' BSET b0,Acc ;tri state the data pin TRIS BSET b1,Acc ; clk Hi TRIS BCLR b1,Acc ; clk Lo TRIS RETURN ; exit (Slave ACK is ignored) 7 CPU instructions per bit, with a 20MHz OSC thats 28 cycles or 714 kbps. I2C data receive The PIC has to be the 'Master', the device a 'slave' (typical device would be a I2C RAM chip). The goal is to collect data from the slave at 1mbps (why else pay for a 'Fast mode+' (1Mbps) capable device ?). With OSC 20MHz max. CPU CLK is OSC/4 and 1 Mbps means we have 5 CPU CLK's to get each bit. To get the data bit we send the SDC (click) Hi, sample the data then pull the Clk Lo again (the slave is only permitted to change the data when Clk is Lo) The data must be PORT b0 (since we use ROTR to get it into Cy). PORT b1 will be used as the Clk. Don't forget that MSB is sent first (so has to be ROTR into the dataReg) NOTE. The TRIS latch controls all bits of a PORT and is not 'readable' = so there is no way to 'preserve' the existing i/o pin mode HOWEVER I will assume a 'copy' will be held in a Register (let's call it 'PortA-trisReg'). ; prepare ACC for TRIS control COPY PortA-trisReg,ACC BSET b0,Acc ;set data pin = input only (should already be the case) TRIS BCLR b1,PortA ;set o/p i/o b0 value 0 BCLR b1,Acc ;set TRIS b0 to o/p mode = clk Lo TRIS COPY ACC,dummyReg ;dummy contains TRIS settings for Clk Lo ; OK here Clk is Lo, ready to go with first bit (MSB) ; BSET b1,Acc ;prepare clk hi TRIS ;do it ROTR PortA,ACC ;get data b0 to Cy (without effecting other bits on the PORT o/p latch) ROTL dataReg ;load data bit into LSB to reg COPY dummyReg,ACC ;reset Acc to clk Lo TRIS ;and Lo ; ... If all 8 bits are coded 'in-line', this is 6 CPU instructions per bit, so 833 kbps (not bad). To speed this up we have to eliminate one of the instructions and the obvious one is 'COPY dummyReg,ACC'. That means the previous instruction, ROTR PortA,ACC has to leave the ACC in the 'correct' state for a TRIS to send clk Lo (and leave data b0 =1 i.e. as an input). This is possible if b2 is used as an input and is wired Lo (b1, Clk will be Hi at this time, so '1' will be rotated into b0). Since b3 will be rotated into b2 (and b2 is wired Lo), b3 has to be Hi (otherwise a TRIS would enable b2 as output !). This means all 4 bits of PORT A have to be dedicated to I2C use