Temp Sensor: 1-wire DS18B20

Years ago, Dallas Semiconductor created an unusual protocol that used the device’s input power line for communication. Using an embedded capacitor, the device can stand for power to be absent long enough to communicate to an MCU. This allows a sensor to be powered and communicate using one wire for positive voltage, and one wire for ground. Since most designs use a common ground, it was called the one wire protocol. Maxim acquired the rights to the 1-wire® devices, and manufactures them today. (®–Maximum Integrated owns the Registered Trademark for 1-wire interface.) Today’s devices typically keep power and ground always connected, and communicate on a third connector using the protocol.

Sources of Information about 1-wire® and PSOC

There have been several forum posts and blogs written about communicating with those devices. Maxim has a page on these devices here: https://www.maximintegrated.com/en/design/technical-documents/app-notes/3/3989.html. An old blog post on using this protocol can be found here: https://wphost.spider-e.com/?p=234. Cypress community forum posts exist several places. One is here: https://www.cypress.com/comment/202076

GPIO and 1-wire®

The PSOC 5 is ideally suited to communicate to 1-wire devices. If you initially look at the specs, you would normally assume that you need at least an external resistor. This is not the case for the PSOC 5, with its configurable GPIO pins.

What is not obvious on the PSOC, (unless you read *all* of their documentation) is if you configure the output parameters on a GPIO pin, it also configures the input parameters on that pin. So, if you need a pull-up resistor on a pin’s input, configure the output to be a resistive pull-up with a default power-on output level of 1. Doing this provides you with a resistor that is usually around 5,000 ohms (5 K ohms), but can be as low as 3.5K ohms, or as high as 8.5K ohms.

A value of 5,000 ohms typically provides about 1 milliamp of current into a pin shorted to ground when resistive pull up is used and VDD is 5 volts. This is enough to power a DS18B20 device parasitically. The data sheet specifies a 4.7K resistor to power for parasitic power, but there is always some leeway.

Device Configuration

First, put a pin on your schematic. Select Digital Input from the Components on the right hand side, under the “Cypress” tab. Also click to the “Off-Chip” tab and select an SCR. Put it on the page and wire it up using the “External Connection” option on the Pin Configuration Editor. Here is what mine looks like, (and it took some time to find all the parts):

The Pin connection to the IC pin is configured in the “Design Wide Resources” in the left side panel. Click the “+” and click on pins. Use the Port column drop-down to set the pin to Port P3 [5].

The PSOC 5 Pin that talks to the 1 wire bus now needs to be configured for our task. To do this , double click on the pin on the schematic, or right-click on it and choose configure from the pop-up menu.

I have set it up as neither an input nor an output. See the following image:

Note that the Pin has Neither Analog nor Digital input checked. (However the Digital Input and Digital Outputs have the HW connection checked.) I first checked these boxes, made changes, then unchecked them. The External Terminal is checked so I could get a blue line to connect to it from the external device for my schematic. The pin is configured as Initial Drive state High (1).

The name of the Pin is P3_5_OneWire. I have started following this naming pattern in self defense; my memory is not what it used to be. PSOC Creator allows you to rename the pins to be anything. I now name them P (for port) 3 (for port 3) _ 5 (for bit 5) _OneWire (what I use it for). I then assign the pin number for the pin in the .cydwr (System wide design resources) pins screen, as P3[5], using the drop-down selection under the Ports column.

With this naming system, I always know what the pin is. I know where to find it on the PSOC. I also know where to look on the schematic as I debug my code. It saves a lot of time during late hour debug sessions.

The Code

Here is the code for reading the temperature device. First, the Header file. Note that the ideas behind some of this code was compiled from many sources on the Internet. So feel free to use this to create your own should you not wish to copy directly. The file is ds18b20.h:

#ifndef _DS18B20_H_
#define _DS18B20_H_
/* ========================================
 * Copyright Wade Maxfield, 2020
 * All Rights Reserved
 * LICENSED SOFTWARE.
 *  Under the GPL v3 license
 * This license does not override previous licenses
 * Some information may be Proprietary to 
 * Cypress (http://www.cypress.com) for their
 * PSoC 5LP®--Cypress Semiconductor and
 * only usable on their devices.
 * PROPERTY OF Wade Maxfield.
 * Commercial license available
 * ========================================
*/

#include <project.h>

    // commands to device
#define Skip_ROM 		             0xCC
#define BeginTempConvertion 		 0x44
#define Read_scratchpad              0xBE

#define TIMEOUT_READ_WRITE_0_BIT    50           //Data bit 0 read,write 15-60us
#define TIMEOUT_READ_WRITE_1_BIT    10          //Data bit 1 read,write 1-15us
#define WAIT_TIME_US                480         //in us.(minimum 480)

/***************************************
*        Function Prototypes 
***************************************/

float getTemperature();// return temperature Celcius
float getTemperatureF(int16 * readingAvailable); // return temp Fareignheight

void initialize_1_wire_device(); // initialize system

uint8 OneWireResetResults;

uint8 Read1WireBit();                
uint8 Read1WireByte();             

uint8 Reset1WireBus();            

void Write1WireBit(uint8 payload);     
void Write1WireByte(uint8 payload);   

/*    Here are the "family" definitions for 1 wire devices
uint32 Family;
		case 0x00:             Family=0;    // unknown
        case 0x01:             Family = 0x1990;
        case 0x02:             Family = 0x1991;
        case 0x04:             Family = 0x1994;
        case 0x05:             Family = 0x2405;
        case 0x06:             Family = 0x1993;
        case 0x08:             Family = 0x1992; 
        case 0x09:             Family = 0x1982;
        case 0x10:             Family = 0x1820; // temperature 
        case 0x28:             Family = 0x18B20;// temperature
*/

#endif

/* End Of File */

Next is the C file. I will cover the non-traditional parts of it later. The file is ds18b20.c:

/* ========================================
 * Copyright Wade Maxfield, 2020
 * All Rights Reserved
 * LICENSED SOFTWARE.
 *  Under the GPL v3 license
 * This license does not override previous licenses
 * Some information may be Proprietary to 
 * Cypress (http://www.cypress.com) for their
 * PSoC 5LP®--Cypress Semiconductor and
 * only usable on their devices.
 * PROPERTY OF Wade Maxfield.
 * Commercial license available
 * ========================================
*/

#include <ds18b20.h>
#include <stdio.h>

//--------------------------------------------
// These defines are used to make it easier
// to change pin names.
//--------------------------------------------
#define OneWire_Read    P3_5_OneWire_Read
#define OneWire_Write   P3_5_OneWire_Write

uint8   OneWireResetResults;
char BUFFER[16] = " " ;  // buffer for 6 bytes SERIAL NUMBER
uint16  OneWireError;


//--------------------------------------------
// initialize the one wire subsystem
//--------------------------------------------
void initialize_1_wire_device()
{
    uint16 ReadHEX ;             
     
    OneWireResetResults = Reset1WireBus();
    Write1WireByte(0x33);             
    ReadHEX = Read1WireByte();
    // flag an error condition if it occurred
    if (ReadHEX != 0x28 && ReadHEX != 0x10)
        OneWireError=1; 
        
}
//--------------------------------------------
// read a bit from the device
//--------------------------------------------
uint8 Read1WireBit() 
{
    CyDelayUs(20);  
    OneWire_Write(0);    // signal want to read
    CyDelayUs(TIMEOUT_READ_WRITE_1_BIT); 
    OneWire_Write(1);    // send pin high so can read device response on line 
    CyDelayUs(5);       
 	return(OneWire_Read());    

}
//--------------------------------------------
// Write a single bit to the  bus. 
//--------------------------------------------
void Write1WireBit(uint8 payload) 
{
    CyDelayUs(20);  //Inactive period
    OneWire_Write(0);       
    
    if(payload==0)
        CyDelayUs(TIMEOUT_READ_WRITE_0_BIT); //0 write
    else     		    
        CyDelayUs(TIMEOUT_READ_WRITE_1_BIT); //1 write
        
    OneWire_Write(1);       //Send High
}
//--------------------------------------------
// reset the device bus
//--------------------------------------------
uint8 Reset1WireBus() 
{
    uint8 BusPin=0 ;      
    
    OneWire_Write(0);  
    
    CyDelayUs(WAIT_TIME_US); 
    
    OneWire_Write(1);     
    CyDelayUs(5);      

    if (OneWire_Read()==0)  	
        return(0xFF);   //the line is shorted, exit with error indication.
        
    CyDelayUs(55);      
    
    // read the value on the pin
    BusPin = OneWire_Read();   
    CyDelayUs(200);     // wait for temperature sensor to be done

    return(BusPin);       
} 
//--------------------------------------------------------------------
// Write a byte to the serial bus
//-------------------------------------------------------------------
void Write1WireByte(uint8 byteToWrite) 
{
    uint8  shiftcount;
    
    for (shiftcount=0; shiftcount<=7;shiftcount++) {
        Write1WireBit((byteToWrite >> shiftcount) & 0x01);
    }
}      
//--------------------------------------------------------------------
// Return a byte read from the serial bus
//--------------------------------------------------------------------
uint8 Read1WireByte() 
{
	uint8 IncomingByte=0, shiftcount=0 ;
	for (shiftcount=0; shiftcount<=7; shiftcount++) 
    {
       IncomingByte |= (Read1WireBit())<<shiftcount;//let's start with LSB
    CyDelayUs(6);
    }
    return(IncomingByte);
}
//--------------------------------------------------------------------
// Temperature reading.
//--------------------------------------------------------------------
float celcius=25.0;

//--------------------------------------------------------------------
// we use a state machine to direct the process of reading the temperature.
//--------------------------------------------------------------------
enum TemperatureReadState {
    nothing=0
    ,start
    ,waitForRBit
    ,readTemperature
    ,readStaticRamFromTempSensor
    ,tempAvailable
};
enum TemperatureReadState TemperatureState=nothing;

//--------------------------------------------------------------------
// Return the temperature in celcius
//--------------------------------------------------------------------
float getTemperature()
{
    static int16 i;
    static uint8 staticRamBytes[8];
    int16_t a2dReading;
    
    switch (TemperatureState){
        case nothing:
            TemperatureState = start;
        
        case start:         
            Reset1WireBus();
            Write1WireByte(Skip_ROM);		
       	    Write1WireByte(BeginTempConvertion);
            TemperatureState = waitForRBit;
        break;

            // NOTE: we don't have to wait for any time 
            // to read this, since we are always powering the device
        case waitForRBit:
            if (!Read1WireBit())
                break; 
            TemperatureState = readTemperature;
            
        break;
            
        case readTemperature:
        	Reset1WireBus();
        	Write1WireByte(Skip_ROM);		
        	Write1WireByte(Read_scratchpad);	
	
            CyDelayUs(1);
            TemperatureState = readStaticRamFromTempSensor;
            i=0;
        break;
            
        case readStaticRamFromTempSensor:
            staticRamBytes[i]=Read1WireByte();
            i++;
            if (i<8)
                break;
				
           
            a2dReading = (staticRamBytes[1] << 8) | staticRamBytes[0];
 
            a2dReading  <<= 3; // 9 bit resolution default
            
            //if sensor is ds1820 or ds18s20 only 9 bit 
            // calculations. See datasheet!
            if (staticRamBytes[7] == 0x10){ 
              // remaining count gives full 12 bit resolution
              a2dReading = (a2dReading & 0xFFF0) + 12 - staticRamBytes[6];
            } else { 
                //if sensor is Ds18B20 9 through 12 bits are available
                int8 configuration = (staticRamBytes[4] & 0x60);
                // if lower res, the low bits are undefined
                switch (configuration) {
                    case 0:
                        a2dReading &= ~7;  // 9 bits,takes 93.75 ms
                    break;
                    
                    case 0x20:
                       a2dReading &= ~3; // 10 bits, takes 187.5 ms
                    break;
                    
                    case 0x40:
                        a2dReading &= ~1;  // 11 bits, takes  375 ms
                    break;
                    
                    default: 
                        // default is 12 bit resolution, 
                        // takes a full 750 ms conversion time
                        // so we need to time-slice readings so as 
                        // to not freeze the rest of the system
                    break;
                }// switch
            }//else

        
            celcius=a2dReading/160.0f;
            TemperatureState = tempAvailable;
        break;
            
        case tempAvailable:
            // hang in this state until we are restarted
        break;
    }
    
    // return old values until new value exists
    return celcius;  
         
}
float TempF=70.0;

//--------------------------------------------------------------------
// return the temperature in degrees F
//--------------------------------------------------------------------
float getTemperatureF(int16 *readingAvailable){
    
    // if a non-zero passed in, assume we can write to it
    if (readingAvailable)
        *readingAvailable=0;// indicate no reading is available
    
    getTemperature();//run the state machine, ignore return
    
    if (TemperatureState == tempAvailable){
        TemperatureState = nothing; // restart reading next time    
        TempF = celcius *1.8f+32.0f;// we are using the global, could use return
        // indicate we have a reading
        if (readingAvailable)
           *readingAvailable = 1;
    }
    return TempF;
    
}

/* [] END OF FILE */

Explanation of Code

The temperature() code uses a state machine to read the temperature from the device. This is critical, since the PSOC needs to be able to do other things for our project while reading the temperature from the device.

This state machine is implemented in C as a switch. You stay in a “state” until the code in that state “transitions” you to the next state, based on decisions made on external data. This machine most closely resembles a Mealy State Machine. (see https://en.wikipedia.org/wiki/Mealy_machine.)

The transitioning of the state is done through the switch statement variable. Changing that variable changes the state. I use an enum to define the states and the variable that holds the state. This allows the C compiler to warn me if I try to assign an invalid state, or if I forget one.

The temperature() routine runs the state machine each time it is called. When a new reading is available, it sets the reading available flag. By watching that flag, you know when a new reading is available.

With a state machine, your code can revisit slowly operating external devices, and only spend uninterrupted time on them when data is forthcoming. In this case, waiting for the temperature reading can take almost a millisecond. If you were to read temperature as often as possible (like we are doing in this example), then there would be almost no time left to do other work, and your MCU would appear to be very sluggish.

Typical Usage

To use this code module, you first call initialize_1_wire_device(). After that, call getTemperatureF(), until a reading is available. The globals celcius, and TempF will hold the degrees C and degrees F. (I got lazy during naming of variables. I am only using the degrees F in my project).

Here is what the code in main() would look like:

#include "project.h"
#include "USBSerial.h"
#include "parseCommands.h"
#include "ds18b20.h"

int16_t TemperatureReadingAvailable;
float temperature;
main() 
{
 // the following two variables are for USBSerial/Command Parser
    int16 bufNum;
    USBSerialMessage *USBRxPtr;

    initialize_1_wire_device();

    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    initUSBSerial();// setup the USB serial interface.
    init_parse();

    for(;;)
    {

        //======================
        // usb serial port
        //======================
        if ((bufNum=handleUSBSerial())>=0){
            USBRxPtr=&USBRxBuffer[bufNum];
           parse((char*)(USBRxPtr->msg),&(USBRxPtr->size)); // handle possible command   
          
          temperature=getTemperatureF(&TemperatureReadingAvailable); 
          if (TemperatureReadingAvailable) {
             // write your code here to do something
          }
       }

    }

You should be able to step through the code with the debugger and see how it works. You can’t step through the Read/Write 1 bit and Read/Write 1 byte functions, but you can examine their actions on an oscilloscope.

It is instructive to do a few temperature readings using breakpoints in the debugger, and then hold your finger on the ds18b20. You should see about a 5 degree rise in just a few seconds.

Next time, if life permits, we will add reading temperature to the USB terminal.

Enjoy!

6 Comments

Add a Comment

Your email address will not be published. Required fields are marked *