The I2C bus is a two-wire synchronous serial interface for intelligent IC devices.
June 01, 1992
URL:http://www.drdobbs.com/architecture-and-design/programming-the-i2c-interface/184408784
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
Mitch is a senior strategic development engineer for Intel and can be contacted at 5000 W. Chandler Blvd., Chandler, AZ 85226 or at mkahn@sedona. intel.com.
The Inter-Integrated Circuit Bus (I"{2}C Bus" for short) is a two-wire, synchronous, serial interface designed primarily for communication between intelligent IC devices. The I2C bus offers several advantages over "traditional" serial interfaces such as Microwire and RS-232. Among the advanced features of I2C are multimaster operation, automatic baud-rate adjustment, and "plug-and-play" network extensions.
Mention the I2C bus to a group of American engineers and you'll likely get hit with an abundance of blank stares. I say American engineers because until recently the I2C bus was primarily a European phenomenon. Within the last year, however, interest in I2C in the United States has risen dramatically. Embedded systems designers are realizing the cost, space, and power savings afforded by robust serial interchip protocols.
The idea of serial interconnect between integrated circuits is not new. Many semiconductor vendors offer devices designed to "talk" via serial links with other processors. Current examples include Microwire (National Semiconductor), SPI (Motorola), and most recently Echelon's Neuron chips. In all cases, the goal is the same: to reduce the wiring and pincount necessary for a parallel data bus. It simply does not make economic sense to route a full-speed parallel bus to a slow peripheral.
Unfortunately for most serial-bus-capable devices, the choice of a bus protocol will dictate the CPU architecture. For example, only two CPU architectures implement an on-chip I2C port. If your choice of architecture precludes use of these architectures, then your only option is to implement the protocol in software.
The software implementation of the I2C protocol discussed in this article came about as a result of an implicit challenge during a staff meeting. One of our managers proposed that we hire a consultant to write a software I2C driver for the Intel 80C186EB embedded processor. Being somewhat new to the group, I took exception (although not verbally!) to his suggestion. A weekend of intense hacking later, I presented the first prototype of the driver. My reward? I got to write a generic version of the driver for general distribution.
Three distinct tasks are involved in implementing the I2C protocol: watching the bus, waiting for a specific amount of time, and driving the bus. This became apparent when I flowcharted 1 byte of a typical bus transaction; see Figure 1 . The time delays associated with creating the bus waveforms would normally have been relegated to the 80C186EB's on-chip timers. I could not, however, assume that the end users of my code would be able to spare a timer for the software I2C port. I had to forego the elegance (and to some extent accuracy) of the on-chip timers for the sledgehammer approach of software timing loops. Luckily, the I2C protocol is extremely forgiving with regard to timing accuracy. The decision to use assembly instead of a high-level language stemmed directly from the need to control program-execution time. I had neither the time nor the inclination to handtune high-level code.
Having made the decision to use assembly language, I faced my next problem: Could I make the code portable? Intel offers a plethora of CPU and embedded-controller architectures. Would it be possible to make the code somewhat portable between disparate assembly languages? I found my answer in the use of macros.
All the basic building blocks of the I2C protocol (watching, waiting, and doing) can be compartmentalized into distinct macros. The algorithms that make up the I2C driver are written with these macros as the framework. You don't need to understand the intricacies of the I2C protocol to port these routines -- you just need to know how to make your CPU watch, wait, and do.
For example, a 4.7_uS delay is a common event during a transfer. The macro %Wait_4_7_uS implements just such a delay by using the 8086 LOOP instruction with a couple of NOPs for tuning; see Example 1(a). Total execution time is readily calculated from instruction timing tables. The same macro is ported to the i960 architecture in Example 1(b). Although I am a neophyte when it comes to i960 programming, I had no problems porting the core macros.
(a) %*DEFINE(Wait_4_7_uS)( mov cx, 5 ; 4 clocks loop $ ; 4*15+5 = 65 clocks nop ; 3 clocks nop ; 3 clocks ; total = 75 clocks ; 75 * 62.5ns = 4.69uS (close enough) ) (b) define(Wait_4_7_uS,' lda 0x17, r4 # instruction may be issued in parallel # so assume no clocks. Ob: cmpdeco 0, r4 # compare and decrement counter in r4 bne.t Ob # if !=0 branch back (predict taken # branch) # # The cmpdeco and bne.t together take 3 # clocks in parallel minimum. # # 0x17 (25 decimal) * 3 = 75 clocks # at 16MHz this is 4.69uS ')
A few words about the target hardware are in order before I discuss the code. Any implementation of the I2C protocol requires two open-drain (or open-collector), bidirectional port pins for the Serial Clock (SCL) and Serial Data (SDA) lines. The code in this article was designed for the 80C186EB embedded processor, which has two open-drain ports on-chip. The two pins, P2.6 (SCL) and P2.7 (SDA), are part of a larger 8-bit port. Processors without open-drain I/O ports can easily implement I2C with the addition of an external open-collector latch.
Two special-function registers, P2PIN and P2LTCH, are used to read and write the state of the port pins. The 80C186EB allows the special-function registers to be located anywhere in either memory or I/O space. For this implementation, I chose to leave the registers in I/O space, even though this limited my choice of instructions. The 80186 architecture does not provide for read-modify-write instructions in I/O space (an AND to I/O, for example); it can only load and store (IN and OUT). So why did I limit myself? Again, I had to assume the lowest common denominator for our customers when designing my code.
Early on in development, I decided to partition my code macros according to physical processes involved in the I2C protocol. Code not directly involved in mimicking the actions of a hardware I2C port was not written as macros. For example, the code necessary to access the stack frame is not written as a macro, whereas the code needed to toggle the clock line is. This was done to isolate architecture-dependent code sequences from the more generic I2C functions. Macros were also not used for "gray areas" such as the shifting of serial data, which is both architecture dependent and physical in nature. The I2C functions that passed the litmus test fell into the three aforementioned categories of watching, waiting, and doing.
The "waiting" macros provide a fixed-minimum time delay. They are implemented using a simple LOOP $ delay. The LOOP instruction decrements the CX register, then branches to the target (in this case itself) if the result is non-zero. The delay is (n-1)*15+5 clocks, where n is the starting value in the CX register. All the delays were calculated assuming a 16-MHz clock rate (62.5 nanoseconds per clock). The code still works at lower CPU speeds because the I2C protocol only specifies minimum timings. In fact, the delay macros are only "accurate enough," providing timings as close as I could get to the specified minimum without undue tuning.
The "watching" macros are "spin-on-bit" polling loops. These pieces of code wait for a transition on the appropriate I2C line to occur before allowing execution to continue. There are two polling macros for each of the two I2C signal lines; one for high-to-low transitions and one for low-to-high transitions. The polling of the SCL line that gives rise to an important feature of I2C: automatic, bit-by-bit baud-rate adjustment. Any device on the I2C bus may hold the clock line low in order to stall the bus for more time (a serial wait state). The other devices on the bus are then forced to poll the SCL line until the slow device releases control of the clock.
The %Get_SDA_Bit macro also falls under the category of "watching." Its function is simply to return the state of the SDA line without waiting for a transition. %Get_SDA_Bit is used primarily to pull the serial data off the bus when the clock is valid.
The "doing" macros control the state of the clock and data lines. As with the polling macros, there are four types -- one for each transition of the SCL or SDA lines. The "doing" macros are named to reflect the physical operations they perform. For example, %Drive_SCL_Low always drives the SCL line to a low state. %Release_SCL_High, on the other hand, relinquishes control of the SCL line, which may then be pulled high or driven low by another device on the bus. A read-modify-write operation is used for the bit manipulation so that the other 6 bits of Port 2 are not affected by the I2C operations.
Three procedures were created using the macro framework. I'll describe only the master transmit (Listing One, page 106) and master receive functions (Listing Two, page 108), as they represent the needs of most I2C users. The slave procedure is long and intricate and will not be described here.
An I2C master transmission proceeds as follows:
The master transmit procedure performs error checking on the passed parameters before attempting to send the message. The maximum message length is set at 64 Kbytes by the segmentation of the 80186 memory space. This restriction could be removed by including code to handle segment boundaries. The transmit procedure also checks the direction bit in the slave address to ensure that a reception was not erroneously indicated. Errors are reported back to the calling procedure through the AX register. (The exact code is in Listing One.)
The first step in sending a message is getting on the I2C bus. The macro %Check_For_Bus_Free simply polls the bus to determine if any transactions are in progress. If so, the transmit procedure aborts with the appropriate error code. If the bus is free, a start condition is generated. The start condition is defined as a high-to-low transition of SDA with SCL high followed by a 4.7_uS pause. These waveforms are easily generated with the %Drive_SDA_Low and %Wait_4_7_uS macros.
All communication on the I2C bus between the stop and start conditions, including addressing and data, takes place as an 8-bit data value followed by an acknowledge bit. This lead to the natural nested loop structure for the body of the procedure; see Figure 2.
The inner loop is responsible for transmitting the 8 bits of each data byte. Each transmitted bit generates the appropriate data (SDA) and clock (SCL) waveforms while checking for both serial wait states and potential bus collisions. A bus collision occurs when two masters attempt to gain control of the bus simultaneously. The I2C protocol handles collisions with the simple rule: "He who transmits the first 0 on the SDA line wins the bus." To ensure that we (the master transmit procedure) own the bus, the SDA line is checked whenever transmitting a 1. If a 0 is present, then a collision has occurred (because another master is pulling the line low), and the transfer must be aborted.
Control is turned over to the outer loop after the 8 bits of data (or address) have been transmitted. The outer loop immediately checks for an acknowledge from the addressed slave. The transfer is aborted if an acknowledge is not received. At the end of the ACK bit the message length counter is decremented. Control is returned to the inner loop if more data remains, otherwise a stop condition is generated and the master transmit procedure terminates.
Registers are used for intermediate result storage throughout the body of the procedure. For example, the AH register is used to hold the current value (either address or data) being shifted onto the SDA line. This eliminates the need for local data storage within the procedure.
The steps involved in an I2C master receive transaction are almost identical to those in transmission:
The structure of the receive procedure differs slightly once the start condition has been generated; see Figure 3. The slave address is transmitted using one iteration of the transmit procedure's outer loop. Control is passed to the receive loop once the slave acknowledges its address.
The receive loop structure is patterned after that of the transmit procedure. The inner loop controls the clocking of the SCL line and the shifting of the serial data off the SDA line into the CPU. Eight iterations of the inner loop are performed to receive each byte. The outer loop stores the received byte in the buffer, decrements the byte count, then sends an ACK to the slave. The last data byte is signalled by not sending an ACK.
Listing Three (page 110) shows a short program that uses both the master transmit and master receive procedures. The call to procedure I2C_XMIT displays the word "bUS-" on a four-character, seven-segment display controlled by the SAA1064 I[2]C compatible display driver. The time of day is read from the PCF8583 real-time clock by the call to procedure I2C_RECV.
Please note that interrupts must be disabled during the execution of both procedures. An interruption at an inopportune time (when the master is not in control of the clock) could cause the bus to hang. If you need to service interrupts periodically, then enable them only when the clock is driven low.
These procedures have been tested on a wide array of I2C devices ranging from serial EEPROMs to voice synthesizers. No compatibility problems have been seen to date.
I've kicked around many ideas for enhancing the I2C procedures. You could, for example, replace the timing loops with timed interrupts. That way, the CPU could perform useful work during the pauses. Along the same lines, the pauses could be scheduled using a real-time kernel, again improving CPU throughput. Finally, you could add a high-level language calling structure.
The use of timed interrupts adds an order of magnitude to the complexity of the code, but would be worth it for high-performance, real-time systems.
I[2]C is not the only game in town when in comes to serial protocols. Hopefully, some of the techniques presented here will carry over into the development of other "simulated" serial protocols, such as those targeted at the home-automation market. Who knows, maybe someday a snippet of my code may find its way into a truly intelligent dishwasher. I'll be waiting....
I2C Bus Specification, Philips Corporation (undated).
_PROGRAMMING THE I2C INTERFACE_ by Mitchell Kahn[LISTING ONE]
$pagelength (30) $mod186 $debug $xref NAME i2c_transmit; $include (\include\pcp_io.inc) PUBLIC i2c_xmit ;****** EQUates ****** BUS_FREE_MIN EQU 2 ; Loop counter for free bus delay. MAXIMUM_MESSAGE_LEN EQU 255 CODE_ILLEGAL_ADDR EQU 020H CODE_MSG_LEN EQU 040H ;****** STACK FRAME STRUCTURE ****** stack_frame STRUC ret_ip DW ? ret_cs DW ? buffer_offset DW ? buffer_segment DW ? count DW ? address DW ? stack_frame ENDS %*DEFINE(Drive_SCL_Low)( mov dx, P2LTCH in al, dx and al, 10111111B ; SCL is bit 6 out dx, al ) %*DEFINE(Release_SCL_High)( mov dx, P2LTCH in al, dx or al, 01000000B out dx, al ) %*DEFINE(Drive_SDA_Low)( mov dx, P2LTCH in al, dx and al, 01111111B ; SDA is bit 6 out dx, al ) %*DEFINE(Release_SDA_High)( mov dx, P2LTCH in al, dx or al, 10000000B out dx, al ) %*DEFINE(Wait_4_7_uS)( mov cx, 5 loop $ nop nop ) %*DEFINE(Wait_Half_Bit_Time)( mov cx, 3 loop $ ) %*DEFINE(Wait_SCL_Low_Time)( mov cx, 5 loop $ nop nop ) %*DEFINE(Wait_SCL_High_Time)( mov cx, 5 loop $ nop nop ) %*DEFINE(Wait_For_SCL_To_Go_Low)LOCAL wait( mov dx, P2PIN %wait: in al, dx test al, 01000000B jne %wait ) %*DEFINE(Wait_For_SCL_To_Go_High)LOCAL wait( mov dx, P2PIN %wait: in al, dx test al, 01000000B je %wait %*DEFINE(Wait_For_SDA_To_Go_High)LOCAL wait( mov dx, P2PIN %wait: in al, dx test al, 10000000B je %wait ) ) %*DEFINE(Get_SDA_Bit)( mov dx, P2PIN in al, dx and al, 0080H ) %*DEFINE(Check_For_Bus_Free)( mov dx, P2PIN in al, dx mov bl, 0C0H ; Mask for SCL and SDA. and al, bl ; If SCL and SDA are high xor al, bl ; this sequence will leave a zero in AX. ) ;*************************************************************************** ;** Revision History: 0.0 (7/90): First frozen working verion. No slave wait ;** timeout. No arbitration turn around. Inefficient register usage. ;** 0.1 (7/16/90): 8-bit registers used (improves 80C188EB. Use STRUCT for ;** stack frame clarity. Implements slave wait timeout. Saves ES. ;*************************************************************************** ;***************************************************************** ;** Procedure I2C_XMIT ** ;** Call Type: FAR ** ;** Uses : All regs. ** ;** Saves : DS and ES only. ** ;** Stack Frame: ** ;** [bp]= ip ** ;** [bp+2]= cs ** ;** [bp+4]= message offset ** ;** [bp+6]= message segment ** ;** [bp+8]= message count ** ;** [bp+10]= slave adress ** ;** Return Codes in AX register: ** ;** XX00 = Transmisiion completed without error ** ;** XX01 = Bus unavailable ** ;** XX02 = Addressed slave not responding ** ;** nn04 = Addressed slave aborted during xfer ** ;** (nn= number of bytes transferred before ** ;** transfer aborted) ** ;** XX08 = Arbitration loss (note 1) ** ;** XX10 = Bus wait timeout ** ;** XX20 = Illegal address ** ;** XX40 = Illegal message count ** ;** note 1: Arbitration loss requires that the ** ;** I2C unit switch to slave receive ** ;** mode. This is not implemented. ** ;***************************************************************** code segment public assume cs:code i2c_xmit proc far mov bp, sp push ds push es test word ptr [bp].address,01H ; Check for illegal ; address (a READ). jz addr_ok mov ax, CODE_ILLEGAL_ADDR ; Illegal addr pop es pop ds ret 8 ; Tear down stack frame addr_ok: mov cx, [bp].count ; Get message length. cmp cx, MAXIMUM_MESSAGE_LEN jle message_len_ok ; Message is 256 or less ; characters. mov ax, CODE_MSG_LEN ; Bad length return code. pop es pop ds ret 8 message_len_ok: mov si, [bp].buffer_offset ; Get message offset. mov ax, [bp].buffer_segment ; Get message segment mov ds, ax ; and put in DS. ; Test for I2C bus free condition. ; SCL and SDA must be high at least 4.7uS mov cx, BUS_FREE_MIN ; initialize free time counter. ; The following loop takes 48 clocks while cx>1 and 33 clocks ; on the last iteration. To insure that bus is free, samples ; of bus must span at least 4.7uS. At 16Mhz: 48*(62.5ns)=3uS ; The first sample is at 0us, the second at 3us, and the ; third will be at 6. Although this exceeds the 4.7us ; spec, it is better safe than sorry. bus_free_wait: %Check_For_Bus_Free jz i2c_bus_free ; At this point the bus is not available. mov ax, 01H ; 01= return code for pop es ; a busy bus. pop ds ret 8 ; return and tear down ; stack frame. i2c_bus_free: loop bus_free_wait ; bus may be free but wait ; the 4.7uS required! ; I2C bus is available, generate a START condition %Drive_SDA_Low %Wait_4_7_uS mov ax, [bp].address xchg ah, al ; ah = address next_byte: mov di, 8 ; set up bit counter next_bit: %Drive_SCL_Low %Wait_Half_Bit_Time mov bl, ah ; get current data and bl, 080H ; strip MSB mov dx, P2LTCH in al, dx and al, 7fh or al, bl ; set bit 7 to reflect ; data bit out dx, al ; xmit data bit %Wait_Half_Bit_Time %Release_SCL_High %Wait_For_SCL_To_Go_High ; At this point SCL is high so if there is another master ; attempting to gain the bus, it's data would be valid here. ; We need only check when our data is "1"... test bl, 80H ; Is data a "1"? jz won_arbitration ; If not -> don't check arbitration. mov dx, P2PIN in al, dx test al, 80H ; Is SDA high? jnz won_arbitration jmp lost_arbitration ; If SDA != 1 then we lost ; arbitration.... won_arbitration: %Wait_SCL_High_Time shl ah, 1 ; shift current byte dec di ; tick down bit counter jne next_bit ; continue bits ; a byte has been completed. Time to get an ACKNOWLEDGE. %Drive_SCL_Low %Wait_Half_Bit_Time %Release_SDA_High %Wait_Half_Bit_Time %Release_SCL_High %Wait_For_SCL_TO_Go_High ; SCL is now high. We must loop while checking SDA for 4.7us. ; With a count of 3 we have a delay of 89 clocks (5.5uS). This ; could be find tuned with NOPs when performance is critical. mov cx, 3 check_4_ack: %Get_SDA_Bit ; Is SDA a "0" jnz abort_no_ack ; if so -> abort loop check_4_ack ; if we've gotten to here, then an acknowledge was received. mov ah, byte ptr [si] inc si ; point to next byte dec word ptr [bp].count ; dec string counter js xfer_done jmp next_byte ; END OF MESSAGE: Issue a STOP condition xfer_done: mov di, 0 ; Normal completion code. jmp i2c_bus_stop abort_no_ack: cmp si, [bp].buffer_offset ; Check if this is the je slave_did_not_respond ; first byte (the address ). mov di, 4H ; Abort during xfer code. jmp i2c_bus_stop slave_did_not_respond: mov di, 02H ; i2c_bus_stop: %Drive_SCL_Low %Wait_Half_Bit_Time %Drive_SDA_Low %Wait_4_7_uS %Release_SCL_High %Wait_For_SCL_To_Go_High %Wait_4_7_uS %Release_SDA_High %Wait_For_SDA_To_Go_High mov ax, di pop es pop ds ret 8 ; Return and tear ; down stack frame. lost_arbitration: mov dx, P2LTCH in al, dx ; Release SDA and SCL or al, 0C0H out dx, al mov ax, 08H ; Lost arbitration code. pop es pop ds ret 8 i2c_xmit endp code ends end[LISTING TWO]
$pagelength (30) $mod186 $debug $xref NAME i2c_receive; $include (/include/pcp_io.inc) PUBLIC i2c_recv ;****** EQUates ****** BUS_FREE_MIN EQU 1H ; Loop counter for free bus delay. MAXLEN EQU 255 ;****** STACK FRAME STRUCTURE ****** stack_frame STRUC ret_ip DW ? ret_cs DW ? buffer_offset DW ? buffer_segment DW ? count DW ? address DW ? stack_frame ENDS %*DEFINE(Drive_SCL_Low)( mov dx, P2LTCH in al, dx and al, 10111111B ; SCL is bit 6 out dx, al ) %*DEFINE(Release_SCL_High)( mov dx, P2LTCH in al, dx or al, 01000000B out dx, al ) %*DEFINE(Drive_SDA_Low)( mov dx, P2LTCH in al, dx and al, 01111111B ; SDA is bit 6 out dx, al ) %*DEFINE(Release_SDA_High)( mov dx, P2LTCH in al, dx or al, 10000000B out dx, al ) %*DEFINE(Wait_4_7_uS)( mov cx, 5 loop $ nop nop ) %*DEFINE(Wait_Half_Bit_Time)( mov cx, 3 loop $ ) %*DEFINE(Wait_SCL_Low_Time)( mov cx, 5 loop $ nop nop ) %*DEFINE(Wait_SCL_High_Time)( mov cx, 5 loop $ nop nop ) %*DEFINE(Wait_For_SCL_To_Go_Low)LOCAL wait( mov dx, P2PIN %wait: in al, dx test al, 01000000B jne %wait ) %*DEFINE(Wait_For_SCL_To_Go_High)LOCAL wait( mov dx, P2PIN %wait: in al, dx test al, 01000000B je %wait %*DEFINE(Wait_For_SDA_To_Go_High)LOCAL wait( mov dx, P2PIN %wait: in al, dx test al, 10000000B je %wait ) ) %*DEFINE(Get_SDA_Bit)( mov dx, P2PIN in al, dx and al, 0080H ) %*DEFINE(Check_For_Bus_Free)( mov dx, P2PIN in al, dx mov bl, 0C0H ; Mask for SCL and SDA. and al, bl ; If SCL and SDA are high xor al, bl ; this sequence will leave ) ; a zero in AX. code segment public assume cs:code i2c_recv proc far ; The LSB of the address for a READ always has a "1" in the LSB. ; The first step is to check for a legal address.... mov bp, sp push ds push es test word ptr [bp].address,01H ; Check for illegal ; address (an XMIT). jnz addr_ok ; The address passed was for a transmit (WRITE). This is ; illegal in this procedure.... mov ax, 20H ; Illegal addr pop es pop ds ret 8 ; Tear down stack frame addr_ok: cmp word ptr [bp].count, MAXLEN jg message_wrong_len cmp word ptr [bp].count, 1 ; check message length jge len_ok message_wrong_len: mov ax, 40H ; error code pop es pop ds ret 8 ; tear down frame len_ok: ; Test for I2C bus free condition. ; SCL and SDA must be high at least 4.7uS mov cx, BUS_FREE_MIN ; initialize free time counter. ; Following loop takes 48 clocks while cx>1 and 33 clocks on last iteration. ; To insure that bus is free, samples of bus must span at least 4.7uS. At 16Mhz ; 48*(62.5ns)= 3uS. First sample is at 0us, second at 3us, and third will be at ; 6. Although this exceeds 4.7us spec, it is better safe than sorry. bus_free_wait: %Check_For_Bus_Free jz i2c_bus_free ; At this point the bus is not available. mov ax, 01H ; 01= return code for pop es ; a busy bus. pop ds ret 8 ; return and tear down stack frame. i2c_bus_free: loop bus_free_wait ; bus may be free but wait 4.7uS required ; I2C bus is available, generate a START condition %Drive_SDA_Low %Wait_4_7_uS ; A receive begins with transmission of the ADDRESS mov di, 8 ; set up bit counter next_bit: %Drive_SCL_Low %Wait_Half_Bit_Time mov bx, [bp].address and bl, 080H ; strip MSB mov dx, P2LTCH in al, dx and al, 7fh or al, bl ; set bit 7 to reflect data bit out dx, al ; xmit data bit sal [bp].address,1 ; shift current byte %Wait_Half_Bit_Time %Release_SCL_High %Wait_For_SCL_To_Go_High ; At this point SCL is high so if there is another master ; attempting to gain the bus, it's data would be valid here. ; We need only check when our data is a "1"... test bl, 10000000B ; Is data a "1"? je won_arbitration ; If not -> don't check arbitration. mov dx, P2PIN in al, dx test al, 10000000B ; Is SDA high? jnz won_arbitration jmp lost_arbitration won_arbitration: %Wait_4_7_uS ; count off high time. dec di ; tick down bit counter jne next_bit ; continue bits ; The address has been completed. Time to get an ACKNOWLEDGE. %Drive_SCL_Low %Wait_Half_Bit_Time %Release_SDA_High %Wait_Half_Bit_Time %Release_SCL_High ; Here we are expecting to see an acknowledge from addressed slave receiver: %Wait_For_SCL_To_Go_High ; a wait state mov cx, 3 check_4_ack: mov dx, P2PIN in al, dx ; get SDA value and al, 10000000B ; is it high? jnz abort_no_ack ; if so -> abort nop nop nop ; NOPs for timing at 16Mhz loop check_4_ack ; if we've gotten to here, then an acknowledge was received. ; At this point in the code, slave receiver has acknowledged ; receipt of its address. SCL has just been driven low, SDA is floating. jmp start_recv abort_no_ack: %Drive_SCL_Low mov di, 02H ; Code for unresponsive slave. jmp i2c_bus_stop ; Now the master transmitter switches to master receiver.... start_recv: mov di, [bp].buffer_offset mov ax, [bp].buffer_segment mov es, ax next_byte_r: mov bx, 0 mov si, 8 next_bit_r: %Drive_SCL_Low %Wait_4_7_uS %Release_SCL_High %Wait_For_SCL_To_Go_High %Get_SDA_Bit shr al, 7 ; move SDA value to LSB or bl, al ; drop in lsb of bl %Wait_4_7_uS dec si ; tick down bit counter je byte_Recv_comp ; continue bits shl bl, 1 ; shift bl for next bit jmp next_bit_r ; The word has been completed. Time to send an ACKNOWLEDGE. byte_Recv_comp: mov al, bl stosb %Drive_SCL_Low %Wait_Half_Bit_Time ; Here we need to decide whether or not to transmit an acknowledge. If this is ; last byte required from slave, we do not send an ack; otherwise we do.... dec [bp].count ; decrement the message count cmp [bp].count, 0 jne send_ack %Release_SDA_High jmp do_ack send_ack: %Drive_SDA_Low do_ack: %Wait_Half_Bit_Time %Release_SCL_High %Wait_For_SCL_To_Go_High %Wait_4_7_uS %Drive_SCL_Low %Wait_Half_Bit_Time %Release_SDA_High cmp [bp].count, 0 je recv_done jmp next_byte_r recv_done: mov di, 00 i2c_bus_stop: %Wait_Half_Bit_Time %Drive_SDA_Low %Wait_4_7_uS %Release_SCL_High %Wait_For_SCL_To_Go_High %Wait_4_7_uS %Release_SDA_High %Wait_For_SDA_To_Go_High mov ax, di pop es pop ds ret 8 ; Return and tear down stack frame. lost_arbitration: mov dx, P2LTCH in al, dx ; Release SDA and SCL or al, 0C0H out dx, al pop es pop ds ret 8 i2c_recv endp code ends end[LISTING THREE]
$mod186 $debug $xref $include (\include\pcp_io.inc) ; a file of EQUates for 186EB register names NAME i2c_example EXTRN i2c_recv:far, i2c_xmit:far %*DEFINE(XMIT(ADDR,COUNT,MESSAGE))( push %ADDR push %COUNT push seg %MESSAGE push offset %MESSAGE call i2c_xmit ) %*DEFINE(RECV(ADDR,COUNT,BUFFER))( push %ADDR push %COUNT push seg %BUFFER push offset %BUFFER call i2c_recv ) stack segment stack DW 20 DUP (?) t_o_s DW 0 stack ends data segment para public 'RAM' bus_msg db 00h,77h,01h,02h,04h,08h ; the LED I2C message recv_buff db 255 dup(?) data ends usr_code segment para 'RAM' assume cs:usr_code start: mov ax, data ; data segment init mov ds, ax cli assume ds:data mov ax, stack ; set up stack mov ss, ax assume ss:stack mov sp, offset t_o_s mov dx, P2DIR ; set up open-drain in ax, dx ; port pins on 186EB and ax, 3FH out dx, ax mov dx, P2CON in ax, dx and ax, 03FH out dx, ax ; The I2C address of the LED driver is 70H for a transmit. %XMIT(70H,6,bus_msg) ; send "bus" message ; The address for the clock is 0xA3 for a receive. %RECV(0A3H,15,recv_buff) ; read first 15 bytes in clock chip. usr_code ends end start Example 1: (a) 80C186 implementation of 4.7uS wait macro; (b) 80960CA implementation of 4.7uS wait macro. (a) %*DEFINE(Wait_4_7_uS)( mov cx, 5 ; 4 clocks loop $ ; 4*15+5 = 65 clocks nop ; 3 clocks nop ; 3 clocks ; total = 75 clocks ; 75 * 62.5ns = 4.69uS (close enough) ) (b) define(Wait_4_7_uS,' lda 0x17, r4 # instruction may be issued in parallel # so assume no clocks. 0b: cmpdeco 0, r4 # compare and decrement counter in r4 bne.t 0b # if !=0 branch back (predict taken # branch) # # The cmpdeco and bne.t together take 3 # clocks in parallel minimum. # # 0x17 (25 decimal) * 3 = 75 clocks # at 16MHz this is 4.69
Copyright © 1992, Dr. Dobb's Journal
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.