Programming the PIC16F84

[Also available in an offline version including supporting files]


When studying the PIC series of microcontrollers, the first thing to realize is that the architecture is completely different from anything you are probably used to. This makes understanding the PIC quite confusing at first. You are probably familiar with the spinal cord type of computer with memory, cpu and perpherial chips hooked in parallel to the same data and address bus. The PIC chips have two separate 'data' busses, one for instructions and one for everything else. Instructions are essentially in ROM and dedicate the microcontroller to doing one task. There is very little RAM, a few dozen bytes, and this is reserved for variables operated on by the program. There is also very little 'data' storage, again a few dozen bytes, and this is in EEPROM which is slow and clumsy to change.


There are a dozen or so I/O pins on the PIC which can be configured as inputs or outputs. When outputs, they have to strength to drive LED's directly. A couple of the I/O pins are used to program the internal ROM serially with an external programmer. These are the OTP, (One Time Programmable), chips. There are also similar chips with UV erasable EPROM's used for prototyping at about three times the price. Then there is one series that is of special interest to the hobbyist, the 16F84, (C84,83), chips which have electrically reprogramable EEPROM memory for instructions. These can be reprogrammed hundreds of times. There have been many programmers designed for this series, one of the simplist appeared in the Sept. '98 issue of 'Electronic Now'.


In the 16F84 instructions are 14 bits wide and stored in EEPROM. There is a maximum of 1024 of these. It is impossible to modify these instructions except through external programming. You can't have self modifying code. When the chip is reset, a program counter is set to zero and instructions are executed from there. The program is retained when power is removed from the chip.


Besides the 14 bit program bus there is another 8 bit data bus in the PIC connected to registers, ports, timer etc. There are 80 RAM locations in the 16F84. RAM is where you put your variables. The only way to change these RAM locations is through instructions. You don't load RAM from outside as in a 'regular' computer. The information in RAM disappears when power is removed. The first 12 RAM locations, ($00 - $0B), have internal registers mapped to them. Changing these locations with instructions changes the corresponding registers. Microchip calls RAM locations 'files' or 'registers' and uses the symbol 'f' when referring to them. The remaining 68 locations can be used for your variables. Microchip calls the first 12 locations special function registers and the remaining 68 general purpose registers.


Five special function registers are not among the first twelve addresses, not even among the 80 . Because of something called 'banking' you have to set a bit in the byte at RAM location 3 to reach them. This location is called STATUS and the bit, (bit 5), is called RP0. If RP0 is zero you are in bank 0, if it is 1 you are in bank 1. For your own variables it doesn't matter which bank is in use because they are mapped to both banks. For some of the first 12 locations it does matter. Seven of the 12 are mapped to both banks but five are not; so location 5 for example has two meanings depending on RP0. If RP0 is clear, ( bank 0 ), location 5 refers to the data on PORT A. IF RP0 is set, ( bank 1 ), location 5 refers to the direction register TRISA that tells which bits of PORTA are inputs and which are outputs.

Much of this complication can be avoided by using two instructions that Microchip indicates it might not support in future products. The TRIS instruction can be used to set the port direction registers and OPTION can be used to set the OPTION register which deals mainly with timer operations. If you port your code to future Microchip processors that don't support these instructions, you will probably want to rewrite the code for some other reason anyway.


There is a third type of memory in the 16F84, 64 bytes of electrically reprogrammable memory, ( 8 bit ). This could be used to hold values you would like to remember when the power is turned off. There are a couple of difficulties. First, the memory is not directly addressable; you have to work indirectly through four of the special function registers. Secondly, it takes a few hundredths of a second to 'burn' the information in so it isn't real fast memory like RAM. This memory can also be burned in when you burn in program memory.


The small instruction set, (37 instructions), and the 14 bit size of instructions lead to a number of compromises. For one thing you can't have two registers specified in a single instruction. Each register takes 7 bits to specify its address, but you also have to specify the instruction number and what to do. The solution is to run almost everything through 'W' or working register which is internal to the processor and doesn't have an address. A register to register transfer would take two instructions. Suppose you had a pattern of LED segments to be lit in the variable PATTERN and want to move it to PORTB to light the segments:
     MOVF  PATTERN, W       ; copy the contents of PATTERN into the working
                            ; register
     MOVWF PORTB            ; copy the contents of W into Port B
The first instruction is of the form MOV f,d which moves the register 'f' to the destination 'd', ('W' in this case). The second instruction simply moves whatever is in 'W' into the register 'f', ( MOVWF, f ). PATTERN remains unchanged in the first instruction and W remains unchanged in the second. Maybe it is more like a copy than a move.

You might think you could get away with one instruction with literals. Literals ,(k), are 8 bits (0-255). Instructions with literals have no room to specify a register, you must use 'W'. Loading a register with a literal also takes two instructions:

      MOVLW $AA             ; put the pattern 10101010 into W
      MOVWF PATTERN         ; put W into the register PATTERN
The same applies when literals are used in addition, subtraction and the logical functions AND, IOR ,(inclusive OR) and XOR ,(exclusive OR); all involve two instructions:
      MOVLW k               ; move literal into 'W'
      SUBWF f,d             ; put the result of subtracting  W from f into d   
                            ; d could be either W or f
                            ; if it's W then f is not changed 
                            ; if it's f then W is unaffected
Suppose we wanted to zero out the lower nibble of PATTERN:
      MOVLW $F0             ; set up mask
      ANDWF PATTERN, f      ; result of PATTERN AND $F0 is placed in PATTERN
                            ; note that the destination could be W if we 
                            ; want the changed pattern to end up there
IF the value to be changed is already in W and the destination is W, a single instruction will work: ADDLW k, SUBLW k, ANDLW k, IORLW k and XORLW k.

Single operand instructions are easy to understand:

      CLRF f   ; set all bits in register f to zero, (clear register f)
      CLRW     ; set all bits in register W to zero, (clear working register)
      BCF f,b  ; set bit b in register f to zero, (bit clear bit b in f)
      BSF f,b  ; set bit b in register f to one, (bit set bit b in f)


Small errors are easy to make and can cause hours of wasted time. Here are some that can cause problems:

Many instructions in a program are MOV instructions and involve 'W'. It is very easy to confuse loading a register with W and loading W with a register.

      MOVWF f               ; W IS MOVED TO THE REGISTER f, ( f is changed )
      MOVF f, W             ; THE REGISTER f IS MOVED TO W, ( W is changed )
      MOVF f, f             ; THE REGISTER f IS MOVED TO ITSELF
                            ;  f is not changed but flags may be set
Note that MOVWF is the only 'WF' instruction that doesn't have a choice of destination. It's always 'f'. The other 'WF' instructions are ADDWF, SUBWF, ANDWF, SWAPWF, IORWF & XORWF. In all of these cases one of 'W' or 'f' will be changed and the other not according to the destination. Also remember in SUBWF that 'W' is subtracted from 'f'. Other instructions where the destination is changed include:
     INC f,d    ; put the value of register f + 1 in either W or f
     DEC f,d    ; put the value of register f - 1 in either W or f
     COMP f,d   ; put the result of toggling all bits of f in destination d
     SWAP f,d   ; put the result of swapping nibbles in f into d
     RLF f,d    ; the result of rotating f left thru carry goes into d
     RRF f,d    ; the result of rotating f right thru carry goes into d
If the destination is 'W' then only 'W' is affected; the original register remains the same.

IN SUBLW k, 'W' is subtracted from the literal 'k'.

It is easy to code GOTO when you meant to code CALL and vice-versa. You might think that this would cause your program to lock up but many times it just makes it act strangely instead.

Beware of using the same registers in two different routines, especially if one calls the other. For example if you use TEMP in a timing loop and then use TEMP in a subroutine that calls the timing loop, you might overlook the fact that the timing loop is changing TEMP.

The rotate instructions, ( RLF,RRF ), are rotates through carry so carry has to be set up prior to the instruction and rotates into the low or high order bit. Likewise the hi or low order bit rotates into carry.


Normally a program will start with instruction 0 and skip to the next as each is executed. Instructions which change this involve a program counter. 'GOTO 666' would set the program counter to 666 and the instruction at location 666 would be executed next. 'CALL 666' on the other hand would first push the next location on the stack and then set the program counter to 666. Instructions after 666 would be executed until a RETURN ,RETLW k or RETIE instruction is encountered:
      RETURN - return from call, location of next instruction is 
              popped from the stack and put into the program counter
      RETLW k - as RETURN but literal k is also placed into W
      RETIE - as RETURN but interrupts are also enabled
The stack consists of eight words and is circular, after the eighth word it rolls over to the first. Calls can be nested only eight deep. There is no push or pop instruction to access the stack directly.

There are four other flow instructions ,( beside CALL and GOTO ), which might be called 'skip it' instructions:

      INCFSZ f,d - put f + 1 into either W or f. skips over the next
                         instruction if the result of the increment is zero
      DECFSZ f,d - put f - 1 into either W or f. skips over the next
                         instruction if the result of the decrement is zero
      BTFSC f,b - tests bit b of register f, skip next instruction if
                        the bit is zero (bit test skip clear). doesn't 
                        change bit
      BTFSS f,b - tests bit b of register f, skip next instruction if
                        the bit is one (bit test skip set). doesn't change bit


The 16F84 has 13 pins that can be individually configured as either inputs or outputs. They a divided into PORTA, (5 bits), and PORTB, (8 bits). The direction of each bit is determined by the bits in the corresponding direction registers TRISA and TRISB. A zero means the bit will be an output, a 1 means input. To set up PORTB with alternating inputs and outputs:
      MOVLW $AA             ; port pattern '10101010'
      TRIS TRISB            ; W is placed into register TRISB
Certain port pins are also hooked to other functions of the processor. The high 4 bits of PORTB can be used as interrupt pins when they are programmed as inputs. The high bit of PORTA is also used as an external clock input for the counter/timer. Bit 0 of PORTB (RB0/INT) can be used for an external interrupt.


Often you wish to simply sit in a loop and wait for a specified period of time. Each instruction takes four clock cycles or 1 microsecond for a 4 Mhz crystal unless the program counter has to be changed, (flow control instruction). 2 microseconds are required for program branches. Here is a delay subroutine which will give a 1 millisecond delay for a 4 Mhz clock:
   MSEC1         MOVLW $F9           ; allow for 4 microsec overhead.. 
                 NOP                 ; (2 for CALL) 
   MICRO4        ADDLW $FF           ; subtract 1 from W
                 BTFSS STATUS,Z      ; skip when you reach zero
                 GOTO MICRO4         ; more loops
Some comments about the code: For longer time periods, you are going to have to use a register. The following routine is entered with the number of milliseconds delay in 'W'. Up to a quarter of a second delays are possible (1 - 255 msec):
   NMSEC         MOVWF CNTMSEC       ; W to msec count register
   MSECLOOP      MOVLW  $F8          ; allow for 8 microsec overhead
                 CALL MICRO4         ; 248 * 4 + 2 = 994 here
                 NOP                 ; make rest of loop ... 
                 NOP                 ; add up to 6 microseconds
                 DECFSZ CNTMSEC, f   ; decrement count, skip when zero
                 GOTO MSECLOOP       ; more loops


There is a internal 8 bit counter/timer that sets a flag when it rolls over from 255 to zero. This can be used as a counter or timer. As a timer, it is connected to the internal clock and increments at the clock frequency divided by four. A flag can be polled to tell when time is up. The timer can also be set up to generate an interrupt when this happens. It wouldn't take long to count all the way up at 1 Mhz so a programmable 'prescaler' can be used. The prescaler can be set to give output pulses at ratios of 1:2,1:4,1:8 etc. up to 1:256, extending the timeout up to the tens of milliseconds range for a 4 Mhz clock.

In counter mode, input comes from pulses applied externally at the high bit of Port A. The prescaler can be inserted to count every second, forth, eighth etc. pulse. The input pin can also be set to count on either rising or falling transitions.

Various bits in the OPTION register are used to set up the counter/timer. The low three bits, (0-2), set the prescaler ratio. Bit 3 determines whether the prescaler is assigned to timer 0 or the watchdog timer. Only one can use the prescaler at any one time. Bit 5 decides if TMR0 register is used as a timer or is used as a counter of pulses from Port A bit 4, (RA4).


Sometimes you can't afford to just sit and wait for a flag to go high. The solution is to set up the timer to generate an interrupt. When the timer rolls over a flag is set, the address of the next operation is pushed on the stack and the program goes to location 4 and continues from there. This is usually a GOTO to the interrupt routine.

The interrupt routine does whatever you want to happen each time the interrupt occurs. Further interrupts are disabled when the interrupt starts. You are responsible for clearing the flag and re-enabling interrupts. RETIE pulls the saved instruction address off the stack and enables interrupts.

Three other situations can be set up to cause interrupts:

  1. PORTB, bit 0 ,(RB0/INT), can be used as an external interrupt pin. A rising or falling edge can be used.
  2. A change of state of any of the high 4 bits of PORTB.
  3. A write to data EEPROM completion.
The enable and flag bits are the key to interrupts on the PIC. Each of these four situations has an associated flag bit and an enable bit. The flag bit set means the situation has happened. You have to reset these. The enable bit set means the setting of this particular flag can cause an interrupt. If in addition you want the interrupt to cause a jump to location 4, a Global Interrupt Enable, (GIE), flag must be set before the individual flag bit goes high. GIE is cleared at the interrupt condition and prevents further interrupts. RETIE resets GIE.


Sleep mode is a low current mode used to save battery life. The pic can draw as little as 50 microamps in sleep mode. The mode is started with the SLEEP instruction and can be ended by one of the following:
  1. external reset on MCLR pin.
  2. Watchdog timeout ( if enabled ).
  3. Interrupt from:
    1. register B port change.
    2. RB0/INT pin.
    3. EEPROM write completion.
While in sleep mode instruction execution is suspended. In particular, timer 0 is not incrementing. Upon wakeup from sleep instruction exection continues from the stopped point or if GIE is set, instruction continues from location 4. > 

Transfer interrupted!

The watchdog timer is independent of the PIC's internal clock. It times out from a CLRWDT ,(Clear Watchdog Timer), instruction in roughly 18 milliseconds. It is not very accurate. The prescaler can be assigned to the WDT, giving time out periods of up to a couple of seconds. The purpose of the WDT in normal use is to keep the PIC from going off into never-never land without the ability to recover. At timeout, a flag is cleared, (TO), the program counter is reset to 0000 and the program starts again. To prevent the reset, you to build a CLRWDT, (Clear Watch Dog Timer), instructions into your program before the timeout occurs.


Reading or writing to data memory, (EEPROM) requires using magic series of instructions involving the registers EEADR, EEDATA, EECON1 and EECON2. EECON1 and EECO2 are in bank 1 so you have to do some bank switching.


Suppose you had the message "Hello World!" and wanted send it out as ASCII characters to a PC COM port. Data memory is scarce and hard to use so we put the message in program memory. How do we access it? We use a table. You load 'W' with the offset of the character you want and call MSGTXT. A 0 will return 'H', a 1 will return 'e' etc:
     MSGTXT     ADDWF PCL, f               ; offset added to PCL
                RETLW $48                  ; 'H'
                RETLW $65                  ; 'e'
                RETLW $6C                  ; 'l'
                RETLW $6C                  ; 'l'
                RETLW $6F                  ; 'o'
                RETLW $20                  ; ' '
                RETLW $57                  ; 'W'
                RETLW $6F                  ; 'o'
                RETLW $72                  ; 'r'
                RETLW $6C                  ; 'l'
                RETLW $64                  ; 'd'
                RETLW $21                  ; '!'
                RETLW $0D                  ; carriage return
                RETLW $0A                  ; line feed
                RETLW $00                  ; indicates end
To output a character string you set up a register to point to the initial character, (MSGPTR), and repeatedly call MSGTXT, incrementing the pointer each time. We have reached the end of the string when a zero is returned. The routine is entered with the offset of the first character in 'W':
      OUTMSG      MOVWF MSGPTR    ; put 'W' into message pointer
      MSGLOOP     MOVF MSGPTR, W  ; put the offset in 'W'
                  CALL MSGTXT     ; returns ASCII character in 'W'
                  ADDLW 0         ; sets the zero flag if W = 0
                  BTFSC STATUS, Z ; skip if zero bit not set
                  RETURN          ; finished if W = 0
                  CALL OUTCH      ; output the character
                  INCF MSGPTR, f  ; point at next
                  GOTO MSGLOOP    ; more characters


Serial output of characters is just a matter of making an output go high or low at the proper times. It is normally high, and going low signals a start bit. At 4800 baud the bit time would be 1/4800 = 208 microseconds. Eight data bits, each one bit period, follow the start bit. A high level for longer than one bit period signifies stop bit/s. The bits are sent Least Significant Bit first. Sampling occurs midway in the bit period to determine if the bit is a 1 or 0.

RS232 high is -3V or lower, low is +3V or greater. You can actually get away with +5V for the low and 0V for the high if you keep the line short. Notice that this is flipped from what you might expect. We can use MICRO4 and 52 X 4 microsec loops for 1 bit time at 4800 baud. Actually 12 microseconds are used in overhead so we use 49 as the count. The subroutine is entered with the character to be output in 'W'. Port A bit 2, (pin 1), is use as output:

        OUTCH      MOVWF TXREG       ; put W into transmit register
                   MOVLW 8           ; eight bits of data
                   MOVWF BITS        ; a counter for bits
                   BSF PORTA, 2      ; start bit (flipped remember), RA2
        TXLOOP     MOVLW $31         ; 49 decimal, delay time
                   CALL MICRO4       ; wait 49 x 4 = 196 microseconds
                   RRF TXREG, f      ; roll rightmost bit into carry
                   BTFSC STATUS, C   ; if carry 0 want to set bit, ( a low )
                   GOTO CLRBIT       ; else clear bit, ( a high )
                   BSF PORTA, 2      ; +5V on pin 1 ( RA2 )
                   GOTO TESTDONE     ; are we finished?
        CLRBIT     BCF PORTA, 2      ; 0V on pin 1 ( RA2 )
                   NOP               ; to make both options 12 micosec
        TESTDONE   DECFSZ BITS, f    ; 1 less data bit, skip when zero
                   GOTO TXLOOP       ; more bits left, delay for this one
                   MOVLW $34         ; full 208 microsec this time
                   CALL MICRO4       ; delay for last data bit
                   BCF PORTA, 2      ; 0V, ( a high ) for stop bits
                   MOVLW $68         ; decimal 104 delay for 2 stop bits
                   CALL MICRO4       


We now have almost all the code necessary to program a PIC to transmit the message 'Hello Word'. The MAIN routine might look like this:
        MAIN        MOVLW 0            ; all port bit outputs
                    TRIS TRISA         ; on port A
                    TRIS TRISB         ; and port B
                    CLRF PORTA         ; RA2 is 0 ( RS232 high )
                    MOVLW $32          ; delay for 50 msec
                    CALL NMSEC         ; so no glitches interfere
                    MOVLW 0            ; this is offset of message
                    CALL OUTMSG        ; output the message
        ENDLESS     GOTO ENDLESS       ; go into an endless loop
Some comments about the code:


The actual connections to a PIC16F84 are few:


Your programmer probably inputs information to be programmed into the PIC in Intel Hex Format (INHX8S). Files in this format have a '.HEX' extension. If you look at one of these files you will see lines that start with ':'. The first 9 characters of the line tell the number of bytes of data and where to put it in memory. Various different addresses are assigned to the program memory, the EEPOM memory or what is called the configuration word.

Various bits in the configuration word tell things like what type of oscillator is being used and whether the watchdog timer is enabled. Some programming software allows you to input this information as command line switches when you run the program. Others expect the information to already be in the .HEX file. In the case the PIC transmitter program, the oscillator type should be set to XT.


I use either the NOPPP programmer by Michael Covington from 'Electronics Now': or the 'TOPIC' programmer designed by David Tait: To produce the '.HEX' files, I use an assembler I wrote called 'Picbuild84': (local copy). Since Picbuild doesn't put configuration data in the HEX files and NOPPP software doesn't allow command line switches for this, I use the TOPIC software which does and works for both programmers. I've included the files 'HELLO.HEX' and 'HELLO.F84' for use with PicBuild84. Use the -x switch with TOPIC.


Fire up your favorite terminal program. Set it for 4800 baud, 8 bits, no parity and plug the connector from the PIC into the COM port. When you power the PIC, 'Hello World!' should be printed on the screen. Touching the wire from MCLR to ground briefly should print it again. If you don't have a terminal program and are running DOS you could enter the following QuickBasic program and run it instead:
     OPEN "COM2:4800,N,8,2,CD0,CS0,DS0,OP0" FOR INPUT AS #1
        PRINT INPUT$(1, #1);
A program that prints out a single message over and over may be instructive but it isn't really very useful. There are many input devices that could be connected to the PIC. Serial devices are especially convenient. One such device is the Dallas Semiconductor DS1820 1 wire Digital Thermometer It is available from Newark for about $6.


The DS1820 is a computer, temperature sensor and serial port in a three pin package. It looks like a transistor and is only a little larger. There is a power pin, a ground and a single data I/O pin. The I/O pin is connected to a single line bus which can support multiple DS1820's. Only the simple situation with a single unit connected to a PIC will be discussed.

The bus is pulled high to +5V through a 4.7K resistor. Either the PIC or the 1820 can pull this line low. The PIC I/O pin is set to output and the port bit is set to 0 to pull the bus low. Changing the pin to input lets the resistor pull the line high. All timing originates with the PIC. Data bits are transferred in time slots initiated when the PIC pulls the bus down. If the bus is released,(I/O pin made input), and allowed to go high for the rest of the time slot, a 1 bit is sent. If the bus remains low it is a zero bit transfer, PIC to 1820. In either case, the bus has to be high at the end of the time slot and remain high for at least 1 microsecond before the next low. The slot must be at least 60 microseconds long. Bit timing is not critical as it is with RS232. Time slots can be any reasonable length over the minimum.

What about transfers the other way, 1820 to PIC? First, you have to send the 1820 a command to read it's scratchpad memory. The PIC must then generate read time slots to get the data bits out. The PIC pulls the bus low as before to initiate the time slot and quickly releases it. Data from the 1820 will be valid only for the first 15 microseconds. The read should take place toward the end of this interval. If the 1820 is pulling the bus low you receive a 0 bit, if not it is a 1 bit.

Any sequence of commands given to the 1820 by the PIC has to be preceded by a reset pulse and the address protocol, (64 bits). The reset pulse is a low from the PIC lasting about 600 microseconds followed by roughly a 400 microsecond period where the PIC has released the bus and can read a response 'presence pulse' from the 1820, (low), indicating everything is O.K. We will ignore this return pulse an assume everything is O.K.

Commands given the 1820 include:

The temperature value is held in the first two bytes of the scratchpad memory. First to come out is the magnitude, the second is either $00 or $FF and represents the sign, ($FF is negative). The top 7 bits of the magnitude hold an integer number in binary, (0-125 degrees C). The lowest bit indicates if an additional 0.5 C is to be tacked on, (1), or not, (0).

An additional complication is negative numbers. The unit goes to -55 degrees C, but negative numbers are expressed in twos compliment form. You have to change these by taking the compliment, (flipping all the bits), and adding one to get the magnitude in normal form.


An outline of a program to read and send temperature would look like this:
  1. Send reset pulse, $CC & $44 to start a conversion.
  2. Wait 500 milliseconds for conversion to take place.
  3. Send reset pulse, $CC & $BE to begin memory read.
  4. Receive value and sign bytes and save them.
  5. Send reset pulse to terminate read.
  6. If sign is $FF then send '-' out RS232.
  7. Convert integer magnitude to decimal and send out RS232.
  8. Send a '.' out RS232.
  9. If LSB of value is 0 send a '0' else send '5' out RS232.
  10. Send carriage return out RS232.
  11. Delay until next reading.
  12. Go to first step and repeat measurement.
The I/O bit used is Port A bit 3. The lowest level 1 wire routines look like this:
        M1HI        BSF  STATUS, RP0       ; go to page 1
                    BSF  TRISA, 3          ; make RA3 an input
                    BCF  STATUS, RP0       ; back to page 0
                    RETURN                 ; master 1 wire HI
        M1LO        BCF  PORTA, 3          ; port A, bit 3 low
                    BSF  STATUS, RP0       ; go to page 1
                    BCF  TRISA, 3          ; make port A, bit 3 an output
                    BCF  STATUS, RP0       ; back to page 0
                    RETURN                 ; master 1 wire LO

        SLOTLO     CALL M1LO         ; begin by taking RA3 low
                   GOTO DLY80        ; and remain low
        SLOTHI     CALL M1LO         ; a quick low on RA3
                   CALL M1HI         ; return high right away
        DLY80      MOVLW  $14        ; decimal 20 x 4 = 80 microsec
                   CALL MICRO4       ; delay
                   CALL M1HI         ; always end on high
The routine for transmission of characters looks much like the routine used for RS232. In fact, we can use the same registers:
        TXBYTE1    MOVWF TXREG       ; put W into transmit register
                   MOVLW 8           ; eight bits of data
                   MOVWF BITS        ; a counter for bits
        TX1LOOP    RRF TXREG, f      ; roll rightmost bit into carry
                   BTFSC STATUS, C   ; if carry 0 want to send 0
                   GOTO HIBIT1       ; else send 1 bit
                   CALL SLOTLO       ; output low bit ( RA3 )
                   GOTO DONE1        ; are we finished?
        HIBIT1     CALL SLOTHI       ; output a high bit ( RA3 )
        DONE1      DECFSZ BITS, f    ; 1 less data bit, skip when zero
                   GOTO TXBYTE1      ; more bits left
The receive routine includes the read time slot generated by the PIC:
        RXBYTE1    MOVLW 8           ; eight bits of data
                   MOVWF BITS        ; a counter for bits
        RX1LOOP    CALL  M1LO        ; a quick low pulse
                   CALL  M1HI        ; and back high
                   NOP               ; waiting for about ...
                   NOP               ; 14 microseconds ...
                   NOP               ; to elapse ...
                   NOP               ; before reading RA3
                   BSF STATUS, C     ; make carry a 1
                   BTFSS PORTA, 3    ; read RA3 skip if received a 1
                   BCF STATUS, C     ; make carry a 0
                   RLF  RXREG, f     ; roll carry into RXREG LSB
                   DECFSZ BITS, f    ; 1 less data bit, skip when zero
                   GOTO RXLOOP1      ; more bits left
The reset pulse is straightforward:
        RESET      CALL  M1LO        ; make I/O line low
                   MOVLW $96         ; 150 x 4 = 600 microseconds
                   CALL MICRO4       ; delay
                   CALL  M1HI        ; and back high
                   MOVLW $64         ; 100 x 4 = 400 microseconds
                   CALL MICRO4       ; delay
Except for the binary to decimal conversion routine, we now have almost all the code necessary to write a program to use the DS1820 to read temperature and send it to the PC COM port. The files 'DEGREES.HEX' and 'DEGREES.F86' can be entered into PicBuild. A LED on RB1 is use to give a quick blink at each reading indicating the unit is working. A normally open push button is connected between RB2 and ground to set up the delay between readings.


Numbers which represent the interval between readings are kept in EEPROM data memory locations 1-13. They are the total delay in seconds - 1. The measurement and transmit cycle is set to take 1 second by itself. I usually program in the numbers 0,1,2,3,4,9,19,29,44,59,119,179 & 239. EEPROM location 0 holds a number 1 to 13 which points to one of these locations. To change this number you hold the button down while turning the power on. This causes a GOTO to a routine that puts a 1 in a location 0 and then increments it each second. The LED blinks once for each number. You release the button when the desired number is reached. The program then burns the pointer number into EEPROM location 0 and goes into an endless loop. You power down and when you power up again collection uses the delay interval pointed to. The push button is connected to RB2. Port B can have weak pullups activated by clearing bit 7 of the OPTION register. This saves having to add an external pullup resistor for the pin. All bits in the OPTION register ar 1's at startup.


The center pin of the DS1820 is the I/O pin and is connected by a wire to RA3, (pin 2), of the PIC. A 4.7K resistor should be connected from pin 2 to +5 Volts. Looking down on the flat side of the DS1820, the ground pin on the left is wired to ground. The pin on the right is connected to +5 volts. The cathode of a LED is connected to ground and the anode is connected through a 1k resistor to RB1, (pin 7), of the PIC. Pin 8 of the PIC, (RB2), is connected to a normally open push button, the other side is connected to ground.


Connect the 9 pin socket to the COM port on the PC. Run a terminal program setting 4800 baud, 8 bits, no parity and 2 stop bits. When the PIC is powered the temperature should appear, being updated according to the delay time set in EEPROM. Put the DS1820 between your fingers and the temperature should go up.

If the time interval between points is not correct, you turn the unit off and hold the button down when you power up. Hold the button until the proper number of flashes has occurred for the desired interval. Release the button, turn the unit off and power up again to start collecting data.

Here is a QuickBasic program to store readings in a file. The file is a text file with one reading per line. You should be able to read the file directly into a spreadsheet and graph the data: The program 'GrafData' will give you a quick graph of the data on the screen.

    ' *** Take N readings, change to Deg F, and store in file ***
    INPUT "How many readings? ", readings%
    INPUT "Filename for readings? ", filename$
    OPEN "COM2:4800,N,8,2,CD0,CS0,DS0,OP0" FOR INPUT AS #1
    IF filename$ <> "" THEN OPEN filename$ FOR OUTPUT AS #2
    count% = 1
       INPUT #1, temp!
       GOSUB ShowTemp
    LOOP UNTIL count% > readings%
    IF filename$ <> "" THEN CLOSE #2

       temp! = 9 * temp! / 5 + 32
       PRINT count%; " - ";
       PRINT USING "###.# Deg F"; temp!
       IF filename$ <> "" THEN PRINT #2, USING "###.#"; temp!
       count% = count% + 1
I've also included a Pascal program to take data, (NTEMP.EXE) if you don't want to go to the trouble of loading QBasic. Notice how much extra code is necessary just to set up the COM port. COM2 is used by the program but can be changed to COM1 by making the changes noted and recompiling.

Now we have a useful device, but not as useful as it could be. After all, you don't want to lug around a computer just to measure temperature. We could use the EEPROM data memory in the PIC to collect data and carry the unit to the PC and dump them, but data memory is pretty small. The answer lies in adding more memory to the PIC and serial memory is definately the way to go. Adding a 8 pin 24C65 serial EEPROM from Microchip gives us 8K bytes of additional memory. These cost about $3 from Digi-Key. Using serial memory we can construct a reasonable temperature data logger.


The 24C65 memory communicates with the PIC over a 2 wire I2C bus. The two lines are SDA, (serial data), and SCL, (serial clock). The PIC will act as the master and the memory a slave. The master provides the clock signal and begins and terminates all transfers with start and stop signals. PIC and memory will both transmit and recieve information over SDA. SDA has to be pulled high with a resistor and the PIC pin has to be set to input to allow slaves to pull the line low, (like the DS1820 I/O line). The clock line would normally have to be the same but in this case there is only one master and one source of clock signal so it isn't necessary.

Information transfer occurs only when the clock is high:

Special care should be taken that SCL is low before any changes are made to SDA. Otherwise, it would be interpreted as a start or stop condition. Assume SDA is connected to RA1 and SCL is connected to RA0. The four most basic routines you need are: Now routines using these four can be written. A start condition would look like this:
         START       CALL LOW_SCL    ; bring SCL low before a change to SDA
                     CALL HIGH_SDA   ; SDA will start high
                     CALL HIGH_SCL   ; now set up for information transfer
                     CALL LOW_SDA    ; start: SDA goes low while SCL high
                     CALL LOW_SCL    ; clock back to where SDA can change
Notice that the clock is left in a condition where changes to SDA won't be seen as start or stop conditions. The code for a stop condition would be exactly the same except with the calls that change SDA reversed. The stop condition leaves SDA high and the bus released.

Once the slave sees the start condition it expects data bits to come from the PIC. The PIC transfers data by raising the clock for 5 microseconds or more and lowering it again. If SDA is high during this period a 1 is transferred if it is low, a 0. The PIC must also put out this clock pulse to receive data. It releases the bus (HIGH_SDA), brings SCL high and examines SDA. A high SDA means a 1 bit from memory, a low means a 0 bit. Clock lows should also last 5 microseconds or longer. This gives a maximum bit transfer rate of 100Kbits/s. Bit timing isn't important as long as it isn't too fast. The critical factor is what takes place on the SDA line when the SCL line is high .

The basic unit of transfer is the byte with an acknowledgement required after each byte is transferred. The PIC sends 8 bits, releases the bus and sends out a 9th clock pulse. If memory responds by pulling SDA low it means everything is O.K. The acknowledgement doesn't have to be acted upon but the clock pulse must be sent. We call this response from memory NACK. Likewise when the PIC is receiving bytes from memory, memory expects an acknowledgement pulse, (SDA high & clock pulse), back after each byte. We call this pulse from the PIC ACK.

Unlike the case with the DS1820, there is no way to get around sending out a slave address after each start condition. The device address is contained in the high 7 bits of a control byte. The lowest bit of the byte tells if the operation is to be a read, (1), of the slave or a write to the slave, (0).

Transmission and reception of bytes of data over the 2 wire I2C is done by the routines 'TXBYTE2' and 'RXBYTE2' in the program 'TLOGGER'. Notice that RLF is used to transfer bytes into carry because the most significant bit comes first in this case.

Writing a byte to a random address in memory requires sending a control byte and address before the actual byte to be saved. It goes like this:

We use this procedure to send both the value byte and sign byte from the DS1820 to memory. We also send the 7th and 8th byte ouput by the 1820 which will be used to break down the tenths of a degree of the reading. Four bytes are saved for each temperature reading. We start at memory location zero and increment the address after each is sent.

Reading a byte from memory also requires sending a start condition, control byte and address bytes to memory. The first 7 steps would be the same as for a write. Then a new start condition is created and a control byte with LSB =1 is sent. Only then can the PIC receive a byte by sending out clock pulses and testing SDA while the clock is high. The PIC must then switch into output mode and send an acknowledge pulse indicating the byte has been received, (ACK). Fortunately a sequential read can follow by continuing to read another byte after each ACK. When you are finished you produce a NACK instead of ACK and then produce a stop condition.

We either have to transfer a fixed amount of data or somehow remember how much data was saved. The latter approach is taken by requiring that a button be pushed before the power is turned off. At the button press, the program jumps to the code at FINISH which saves the current memory address to data EEPROM locations $0E and $0F. These two locations provide the first two bytes sent to the PC COM port when the 24C65 memory is dumped.


An eight pin DIP socket is need with pins connected as follows: In addition, some provision has to be made to signal a dump of 24C65 memory to the PC. A simple way is to add a PC boardjumper to ground on RB3, (pin 9). If the jumper is in place, control on start-up is transferred to 'MEMDUMP'. Note that if the jumper is not in place, collection of data will begin on power-up and memory will be overwritten. You may prefer a slide switch rather than a jumper.


To use the datalogger you simply turn it on and press the button when you want to start collecting data. The LED will blink as each four bytes for a data point is collected. If the time interval between readings is not correct, turn the unit off and reset it as described in the 'DEGREES' program.

After collecting data and before you turn the unit off, be sure to press and hold the button to save the stopping address. Hold the button down for at least a second or two. Put the jumper on to prevent loss of data if the unit is accidently turned on again.

While the jumper is on, connect to a PC COM2 port. Run the program 'READUMP' and turn the power on. Press the button to start data transfer. The datalogger doesn't convert the binary data to decimal like the 'DEGREES' program. This is done in the 'READUMP' program. Two other collected bytes are used to break the reading down to tenths of a degree. This is done more easily in the higher level language. It makes the graphs less 'blocky'. The file created by 'READUMP' can be loaded into a spreadsheet program or graphed with the program 'GRAFDATA'.


The ceramic resonator seems to be accurate to only a percent or so. A quartz crystal would be more accurate but much larger. If you use a quartz crystal consider trimming the overall main loop to make it closer to one second. I've added coarse and fine loops just before getting the EEPROM delay. I set these roughly by seeing how much the blinking lagged over long periods of time and inserting appropriate numbers.

One thing I've thought of adding but haven't worked out yet is multiple collections of data. The idea would be to hold starting locations in data EEPROM and cycle through them, indicating which collection you want to dump. It would probably mean adding a line to transmit data to the PIC from the PC. A 22k resistor would be used to limit current from the RS232 TX line.

It would be nice to reduce the amount of current drawn by the unit to extend battery life. One way to do this would be to use SLEEP mode, but you would have to supply a low current external clock to do this because the PIC clock shuts down during SLEEP. A simpler way to save current is to put a switch in series with the LED to take it out of the circuit when not needed. The PIC itself seems to draw only a milliamp or so. It is also possible to run the circuit at 3 volts which reduces the current. A lower clock rate would help too but you might have to reduce the RS232 transmit baud rate.

A readout of temperature on the unit itself would be nice. The problem is that we only have 6 port bits left, (7 if you count RB1). It would probably mean quite a bit of extra hardware for miminimal advantage.

The conversion to tenths of a degree and decimal output as in 'DEGREES' could have been done in the PIC. I just thought it would be easier in a higher level language.


I would like to thank all people that have posted PIC information on the Internet. After all, that is where I got all the material I learned from. I would especially like to thank:

Written by Stan Ockers.