|
This project shows how to communicate with I2C
Devices. The example shown below uses the DS1624 Digital Thermometer and
Memory IC available from Dallas Semiconductor http://www.dalsemi.com.
The DS1624 is a good choice in temperature sensors because
it requires no external components and has a wide temperature range of
+125°C to –55°C. The DS1624 also has 256 bytes of available E2prom memory
for general storage (although that will not be covered in this article).


DS1624 Pinout
|
SDA
|
Data Pin
|
VDD
|
Vsupply (+5V)
|
|
SCL
|
Clock Pin
|
A0
|
I2C Address bit 0
|
|
NC
|
No Connect
|
A1
|
I2C Address bit 1
|
|
GND
|
Ground (0V)
|
A2
|
I2C Address bit 2
|

For This Experiment:
 |
SDA is connected
to PortC.1
|
 |
SCL is connected to
PortC.2
|
 |
A0, A1, A2 are all
connected to GND
|
 |
VDD is
connected to +5V
|
 |
GND is connected to
Ground
|
The command protocol for the DS1624 is shown in the
following diagram:


Before
each read or write, a 4 bit device code along with a 3 bit address code and a
read or write bit must be sent. This allows up to 8 devices of a particular type
to be added on 2 I/O lines, a great advantage, but can be difficult to control
in software. The 4 bit device code for the DS1624 is %1001. In this experiment,
all address bits are logic low, therefore the address of this Temperature sensor
is %000. If you wish to add multiple temperature sensors to the same bus, then
just tie each address pin high or low and change the address accordingly.
Depending upon which operation is desired to be performed, bit 0 of the control
byte will either be 0 for a “write” command or 1 for a “read” command.
On to the code….
' DS1624 Temperature sensor code example:
' A pic with a hardware USART was used to send serial data to a
' terminal @19200,N,1 but an LCD could easily be added instead.
' If using the USART, a level translator such as the MAX232 is
' required since inversion can't be used with the hardware serial port.
' PIC16F876 used @ 20Mhz, see annotation for changes necessary
DEFINE OSC 20 '20Mhz Oscillator was used
DEFINE HSER_RCSTA 90h 'Enable Asynchronous Serial Receive
DEFINE HSER_TXSTA 20h 'Enable Asynchronous Serial Transmit
DEFINE HSER_BAUD 19200 'Baud = 19200
DEFINE HSER_SPBRG 15 'Must be changed depending on Osc. used, see PIC datasheet
DEFINE HSER_CLROERR 1 'Errors automatically cleared when encountered
SDA VAR PORTC.1 'Alias “SDA” to pin C.1
SCL VAR PORTC.2 'Alias “SDA” to pin C.1
i2c_read CON 1 'R/W configuration bit (1 = read)
i2c_write CON 0 'R/W configuration bit (0 = write)
i2c_out VAR BYTE 'data to sent over I2C bus
i2c_in VAR BYTE[2] 'data received over I2C bus
i2c_ack VAR BIT 'acknowledgement bit
temp VAR WORD
GOSUB Config_Register 'Set Configuration
GOSUB Start_Convert 'Start continuous conversion
TOP:
PAUSE 5000
GOSUB Read_Temp 'Read the current temperature
i2c_in[2] = i2c_in[1] >> 3 'Shift 5 decimal bits to LS position
temp = (i2c_in[1]*1000) 'PIC Doesn’t like decimals, but there is a way to work around this
HSEROUT [DEC i2c_in[0],".",DEC2 (temp ** 2048)/100,13,10]'Outputs temperature to terminal
GOTO top 'Loops forever
Config_Register: 'Set continuous conversion
GOSUB I2C_START 'Start Condition
i2c_out = %10010000 'Send Address, (device= %1001, A0= 0, A1= 0, A2= 0, R/W= 0)
GOSUB I2C_TX 'Send data in “i2c_out”
i2c_out = $AC 'Send “Access Configuration” command
GOSUB I2C_TX 'Send data in “i2c_out”
i2c_out = $00 'Send “Continuous Conversion” command
GOSUB I2C_TX 'Send data in “i2c_out”
GOSUB I2C_STOP 'Stop Condition
RETURN
Start_Convert: 'Start Conversion
GOSUB I2C_START
i2c_out = %10010000 'Send Address, (device= %1001, A0= 0, A1= 0, A2= 0, R/W= 0)
GOSUB I2C_TX
i2c_out = $EE 'Send “Start to Convert” command
GOSUB I2C_TX
GOSUB I2C_STOP
RETURN
Read_Temp: 'Read temperature
GOSUB I2C_START
i2c_out = %10010000 'You must “write” the command to read the temperature before
GOSUB I2C_TX 'reading it, therefore R/W still is 0
i2c_out = $AA 'Send “Read Temperature” command
GOSUB I2C_TX
GOSUB I2C_START '* Reissue Start Condition *
i2c_out = %10010001 'Send Address, (device= %1001, A0= 0, A1= 0, A2= 0, R/W= *1*)
GOSUB I2C_TX 'Transmit address with R/W bit as 1 (“read”)
GOSUB I2C_RX 'Start getting data coming in
GOSUB I2C_STOP 'Issue stop condition
RETURN
I2C_START: 'I2C start (start communication on I2C bus)
HIGH SDA
HIGH SCL
LOW SDA
LOW SCL
RETURN
I2C_STOP: 'I2C stop (terminate communication on I2C bus)
LOW SDA
HIGH SCL
HIGH SDA
PAUSE 1
RETURN
I2C_RX: 'I2C receive -> receive data from slave
SHIFTIN SDA,SCL,0,[i2c_in[0]] 'Shift in first byte MSBpre
SHIFTOUT SDA,SCL,1,[%0\1] 'Send acknowledge (ACK) = 0
SHIFTIN SDA,SCL,0,[i2c_in[1]] 'Shift in second byte MSBpre
SHIFTOUT SDA,SCL,1,[%1\1] 'Send not acknowledge (NACK) = 1
RETURN
I2C_TX: 'I2C transmit -> send data to the slave
SHIFTOUT SDA,SCL,1,[i2c_out] 'Shift out “i2c_out” MSBfirst
SHIFTIN SDA,SCL,0,[i2c_ack\1] 'Receive ACK bit
RETURN

The only part of this code that
gets really tricky is under the TOP: label where the data coming in is
converted to a serial transfer. To understand what is happening, one must
first know how data from the DS1624 comes in. 13 bits are shifted into 2
bytes of an array, but they must first be manipulated to be of any use. Data
in "i2c_in[0]" gets received first.
Alone this data could be used as-is for 1°C resolution measurements. What
about negative values? Well we know the data can only go as high as +125°C,
but yet some values come in as high then this. Take 255 (the maximum value of
a byte) and subtract only those numbers > 125, and you will get a bunch of
negative values; isn't that convenient! (Ex. 250 > 255 therefore 255 - 250
= -5°C). This conversion doesn't deal with negative numbers, because they are
easy enough to implement and there is enough to deal with already.
That takes care of the first byte, but now what about i2c_in[1]? Well this is
a 5 bit number that represents .03125°C decimal precision, but it is in the 5
Most Significant Bits. In order to work with this number, it must be shifted
over 3 places to the right ( >> 3). The value is then multiplied by
1000 because it is hard to work with decimals, and we would prefer to have
00.000 decimal precision (although with integer math, only 00.00 decimal
precision will be accurately attainable).
The number is then modified with the operation (" ** 2048") which
essentially is like multiplying by 2048/65536 = 0.03125 with no rounding
until the end. The value is then divided by 100 and displayed with dec2
because we want a result of 00.00°C at the end.
For
Example:
The DS1624 gives a value of $1980 Shifted in
$19 = 25 so we know there is a measured
temperature of 25.X°C
$80 = %10000000 and we shift over 3
giving %10000 as the 5 bit decimal value
%10000 = 16 * 10000 = 16000
* this will never overflow because (1/0.03125) * 10000 = 32000)*
16000 * (2048/65536) = 5000 in integer math
5000 / 100 = 50
Because of the format we are using to
send serial data to the terminal, the end result
is “25”, “.”, “50” ending up being
“25.50” degrees Celsius
Any conversion to
Fahrenheit must be done using a lookup table or conversion factor, and is not
a part of this code.
The code itself is quite simple,
and easy to understand, but it is not very efficient. First of all, only one
byte can be sent out at a time. Take for instance when you want to set the
DS1624 for continuous conversion, you must first send the device code, then
$AC then $00. There are many places where the “device code” is sent out with
either a 1 or a 0 Read/Write bit, but the way this code stands, each device
needs its own subroutine. This code could be improved to allow you to send
and receive any number of bytes, and would let you specify which device code
to perform these operations on.
The following code does
the same as the subroutines above, only now things are a lot more condensed.
Because you may wish to use this in different projects, it has been created
as 2 “include” files. The reason that 2 files are need is that some variables
must be declared before subroutines are run, and it is undesirable to put all
the subroutines before the main body of the code because the PIC will “flow”
into these instructions and the program will not work. Therefore it is easier
to just type 2 includes into the code rather then remember the exact
variables that need to be added for the subroutines to work the first part is
included below as well as a brief explanation.

' File: "I2C_A.bas"
' Subroutines for using custom I2C
' Any Questions, jonz@shaw.ca
' To send data, put the number of bytes to send in var
"FLAG" (IE: flag = 2) will send 2 bytes
' After declaring number of bytes, set the data to send into the array
I2C_OUT[$??]
' Then run the subroutine "I2CTX"
' To receive data set the FLAG variable again, data will be placed
in the string I2C_IN MSBFIRST
' in string position [0] the [1] ect. Run the subroutine
"I2CRX" After setting the flag variable
' Include the following into your code that needs I2C Routines:
' SDA Var PortC.1 ' The
port your SDA line is on
' SCL Var PortC.2 ' The
port your SCL line is on
' ADDR Var
Byte ' The address of the I2C device you wish
to access
' I2C_OUT Var Byte[2] '[2] is max bytes sent
out, has to be >= max strings being sent out
' I2C_IN Var Byte[2] '[2] is max bytes
received, has to be >= max strings being sent in at once
' Include "I2C_A.bas
' The Rest of your program goes below. All variables must be
declared before "include" statement
' To get around the fact that some variables must be declared before
the subroutines ect, a second
' include file is needed. Add it below your program as follows:
' Include "I2C_B.bas"
' ***** Start of “I2C_A.bas” content ***** ‘
I2C_ACK VAR BIT 'ACK bit
receive
N
VAR BYTE '”FOR” loop
Variable
FLAG
VAR BYTE 'FLAG Variable
for Bytes to send/receive
RW
VAR BIT 'Read/Write bit
variable used by subroutines
' ***** End of “I2C_A.bas” content ***** ‘
The Second file
of the I2C Routines contains subroutines. “Include “I2C_B.bas” after the main
body of your program.
' File: "I2C_B.bas"
' Subroutines for using custom I2C
' Any Questions, jonz@shaw.ca
' ***** Start of “I2C_B.bas” content ***** ‘
I2CTX:
' Transmit I2C Routine
GOSUB I2C_Start
' Issue Start Condition
RW =
0
' “Write” command
GOSUB Set_ADDR
' Send Address from variable ADDR
FOR N = 1 TO FLAG
' Shifts out byte[0]..[1]… for number in FLAG times
SHIFTOUT SDA, SCL, 1,
[I2C_OUT[N-1]\8]
SHIFTIN SDA,
SCL, 0, [I2C_ACK\1]
NEXT N
GOSUB I2C_Stop
' Issue Stop Command
RETURN
I2CRX:
' Receive I2C Routine
GOSUB I2C_Start
RW =
1
' “Read” command
GOSUB Set_ADDR
IF FLAG >= 2 THEN ' If there is
more then 1 byte, send all but last, then ACK
FOR N = 1 TO (FLAG - 1)
SHIFTIN
SDA, SCL, 0, [I2C_IN[N-1]\8]
SHIFTOUT SDA,
SCL, 1, [%0\1]
NEXT N
ENDIF
SHIFTIN SDA, SCL, 0,
[I2C_IN[FLAG-1]\8]' *** Send last byte with NACK ***
SHIFTOUT SDA, SCL, 1, [%1\1]
GOSUB I2C_Stop
' Issue Stop command
RETURN
Set_ADDR:
' Send Address of device from Var ADDR
SHIFTOUT SDA, SCL, 1, [ADDR >> 1\7,RW\1]' Shifts ADDR
bits over, adds on R/W bit
SHIFTIN SDA, SCL, 0,
[I2C_ACK\1] ' Receives ACK
RETURN
I2C_Start: ' Start
Condition
HIGH SDA
HIGH SCL
LOW SDA
LOW SCL
RETURN
I2C_Stop:
' Stop Condition
LOW SDA
HIGH SCL
HIGH SDA
PAUSE 1
RETURN
' ***** End of “I2C_B.bas” content ***** ‘

To see the result of the
new subroutines, the following file can be used and compiled as long at the
above 2 include files are in the same folder and named accordingly.
' File: DS1624.bas
' Uses I2C Subs in I2C_A.bas and I2C_B.bas
DEFINE LOADER_USED 1
DEFINE OSC 20
DEFINE HSER_RCSTA 90h
DEFINE HSER_TXSTA 20h
DEFINE HSER_BAUD 19200
DEFINE HSER_SPBRG 15
DEFINE HSER_CLROERR 1
SDA VAR PortC.1 ' The port your
SDA line is on
SCL
VAR PortC.2 ' The port your SCL line is on
ADDR
VAR BYTE ' The address
of the I2C device you wish to access
I2C_OUT
VAR BYTE[2] '[2] is maximum bytes being sent
out, can change to send more
I2C_IN
VAR BYTE[2] '[2] is maximum bytes being sent
in, can chage to receive more
TEMP
VAR WORD
INCLUDE "I2C_A.bas" ' Include
Subroutine variables
ADDR = %10010000
' Address’ can be set by a variable
FLAG = 2 : I2C_OUT[0] = $AC : I2C_OUT[1] = $00 ' Send 2 bytes,
Address taken care of
GOSUB I2CTX
FLAG = 1 : I2C_OUT[0] = $EE ' Send out 1
byte
GOSUB I2CTX
TOP:
PAUSE 500
GOSUB Read_Temp
I2C_IN[2] = I2C_IN[1] >> 3 ' Shift data so
that 5 bits are in the LS position
TEMP = (I2C_IN[1]*1000)
' Takes the decimal data and prepares it for * by .03125
HSEROUT [DEC I2C_IN[0],".",DEC1 (TEMP **
2048)/100,13,10]
GOTO TOP
Read_Temp:
FLAG = 1 : I2C_OUT[0] = $AA ' 1 Byte out,
R/W = 0 because TX subroutine used
GOSUB I2CTX
FLAG =
2
' 2 bytes in
GOSUB I2CRX
' Receive subroutine, R/W = 1 because RX subroutine used
RETURN
INCLUDE "I2C_B.bas"
' Include Subroutines

When you compile the program
you’ll notice the second version will use more memory. While it may seem at
first this is a disadvantage to the first approach, you must take into
consideration the first was used to just access a certain address using a
specific byte out, byte in pattern. The second approach allows as many bytes
in or out as the PIC’s RAM allows, and the address can be specified by a
variable. It is then more desirable to use the subroutine approach if
different and/or multiple devices on the same bus were to be used.
Connection Diagram for
Experiment:



Contact the author: You can contact
Jonathan Zacharko by e-mail at:
jonz@shaw.ca
This article is available for download in .pdf
format HERE. |