Controlling Temperature Sensor Ds18b20 - part 1

Sam LeeSam Lee
9 min read

The second device we want to control is a temperature sensor.

The product is called Ds18B20, and it only has three wires: data, VCC, and ground.

So, how does it communicate with just one wire?

  • Unique 1-Wire® interface requires only one port pin for communication

According to the datasheet, it uses a unique communication protocol called 1-wire.

  • ±0.5°C accuracy from –10°C to +85°C

The accuracy range is suitable for measuring the ideal temperature we want.

After reading the first page of the datasheet and getting a rough idea, I found some example codes.

I found two examples: https://github.com/feelfreelinux/ds18b20/blob/master/ds18b20.c and https://github.com/nimaltd/ds18b20.

The datasheet: https://cdn.sparkfun.com/datasheets/Sensors/Temp/DS18B20.pdf

The first one is written for ESP32, and the second one is for STM32, so I will use the second one.

It can be used with or without RTOS, and we will use it without RTOS.

There are a total of five files: ds18b20.c/h, ds18b20Config.h, and onewire.c/h.

First, let's look at the ds18b20.c file.

Since we are not using RTOS, the black-filled sections are visible.

I wanted to share all the files to see what functions are available, but it was too long, so let's look at the function names in the ds18b20.h file to get a simple idea of what they do.

There is an Init function, and start() and start_all() functions.

The datasheet states that multiple ds18b20 devices can be connected at once. So, the difference between the start() and start_all() functions is whether you want to start a specific device or all devices.

There are also functions related to resolution. According to the datasheet, it can be set to 9, 10, 11, or 12 bits, and it describes how accurately the data will be retrieved.

As the resolution increases, the conversion time also increases. We will use 11 bits because we want to display up to the first decimal place.

There are functions related to alarms, and a function called All_done that tells us when the conversion is complete.

There also seems to be a Manual_convert function for manually performing the conversion.

First, looking at the Init function, it is wrapped in a do-while loop and attempts to find the Ds18b20 device five times. If it fails, it returns false.

bool    Ds18b20_Init(void)
{
    uint8_t    Ds18b20TryToFind=5;
    do
    {
        OneWire_Init(&OneWire,_DS18B20_GPIO ,_DS18B20_PIN);
        TempSensorCount = 0;    
        while(HAL_GetTick() < 3000)
            Ds18b20Delay(100);
        OneWireDevices = OneWire_First(&OneWire);
        while (OneWireDevices)
        {
            Ds18b20Delay(100);
            TempSensorCount++;
            OneWire_GetFullROM(&OneWire, ds18b20[TempSensorCount-1].Address);
            OneWireDevices = OneWire_Next(&OneWire);
        }
        if(TempSensorCount>0)
            break;
        Ds18b20TryToFind--;
    }while(Ds18b20TryToFind>0);
    if(Ds18b20TryToFind==0)
        return false;
    for (uint8_t i = 0; i < TempSensorCount; i++)
    {
        Ds18b20Delay(50);
    DS18B20_SetResolution(&OneWire, ds18b20[i].Address, DS18B20_Resolution_12bits);
        Ds18b20Delay(50);
    DS18B20_DisableAlarmTemperature(&OneWire,  ds18b20[i].Address);
  }
    return true;
}

Inside the while loop, it initializes OneWire, sets up the GPIO, counts the temperature sensors, registers all devices on the OneWire bus, sets the resolution, and configures the alarm, completing the initialization process.

We will look at OneWire later and move on for now.

Looking at the start() function,

it first checks the ds18b20 device, initializes the OneWire bus, determines which device to control, and sends the convert command.

uint8_t DS18B20_Start(OneWire_t* OneWire, uint8_t *ROM)
{
    /* Check if device is DS18B20 */
    if (!DS18B20_Is(ROM)) {
        return 0;
    }

    /* Reset line */
    OneWire_Reset(OneWire);
    /* Select ROM number */
    OneWire_SelectWithPointer(OneWire, ROM);
    /* Start temperature conversion */
    OneWire_WriteByte(OneWire, DS18B20_CMD_CONVERTTEMP);

    return 1;
}

The startAll() function sends the skiprom command instead of selecting a specific device. What does this mean?


void DS18B20_StartAll(OneWire_t* OneWire)
{
    /* Reset pulse */
    OneWire_Reset(OneWire);
    /* Skip rom */
    OneWire_WriteByte(OneWire, ONEWIRE_CMD_SKIPROM);
    /* Start conversion on all connected devices */
    OneWire_WriteByte(OneWire, DS18B20_CMD_CONVERTTEMP);
}

The datasheet explains the commands used here. Since multiple devices can be connected, you need to select which device to send which command to.

Device address - command

This is the format for sending commands.

In the case of skiprom, it sends commands to all devices instead of a specific one. If there is only one slave device, you can just send skiprom - command.

Finally, let's look at the Manual Convert function.

First, it sets a timeout, uses the OneWire startAll() function, gives a short delay, and waits until AllDone.

bool    Ds18b20_ManualConvert(void)
{
    #if (_DS18B20_USE_FREERTOS==1)
    Ds18b20StartConvert=1;
    while(Ds18b20StartConvert==1)
        Ds18b20Delay(10);
    if(Ds18b20Timeout==0)
        return false;
    else
        return true;    
    #else    
    Ds18b20Timeout=_DS18B20_CONVERT_TIMEOUT_MS/10;
    DS18B20_StartAll(&OneWire);
    Ds18b20Delay(100);
    while (!DS18B20_AllDone(&OneWire))
    {
        Ds18b20Delay(10);  
        Ds18b20Timeout-=1;
        if(Ds18b20Timeout==0)
            break;
    }    
    if(Ds18b20Timeout>0)
    {
        for (uint8_t i = 0; i < TempSensorCount; i++)
        {
            Ds18b20Delay(100);
            ds18b20[i].DataIsValid = DS18B20_Read(&OneWire, ds18b20[i].Address, &ds18b20[i].Temperature);
        }
    }
    else
    {
        for (uint8_t i = 0; i < TempSensorCount; i++)
            ds18b20[i].DataIsValid = false;
    }
    if(Ds18b20Timeout==0)
        return false;
    else
        return true;
}

It exits the while loop when the timeout ends or the AllDone signal is received.

Then it checks if the data is valid.

uint8_t DS18B20_AllDone(OneWire_t* OneWire) { 
/* If read bit is low, then device is not finished yet with calculation temperature */ 
return OneWire_ReadBit(OneWire); }

AllDone is a function from the onewire library.

Now let's look at OneWire.

In OneWire.h, let's briefly look at the functions.

//#######################################################################################################
void OneWire_Init(OneWire_t* OneWireStruct, GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint8_t OneWire_Reset(OneWire_t* OneWireStruct);
uint8_t OneWire_ReadByte(OneWire_t* OneWireStruct);
void OneWire_WriteByte(OneWire_t* OneWireStruct, uint8_t byte);
void OneWire_WriteBit(OneWire_t* OneWireStruct, uint8_t bit);
uint8_t OneWire_ReadBit(OneWire_t* OneWireStruct);
uint8_t OneWire_Search(OneWire_t* OneWireStruct, uint8_t command);
void OneWire_ResetSearch(OneWire_t* OneWireStruct);
uint8_t OneWire_First(OneWire_t* OneWireStruct);
uint8_t OneWire_Next(OneWire_t* OneWireStruct);
void OneWire_GetFullROM(OneWire_t* OneWireStruct, uint8_t *firstIndex);
void OneWire_Select(OneWire_t* OneWireStruct, uint8_t* addr);
void OneWire_SelectWithPointer(OneWire_t* OneWireStruct, uint8_t* ROM);
uint8_t OneWire_CRC8(uint8_t* addr, uint8_t len);
//#######################################################################################################

In summary, there are parts related to Init and Reset,

and functions like First, Next, Select, GetFullROM, which seem to be about selecting a device.

There are also functions for Read, Write, and Search.


typedef struct {
    GPIO_TypeDef* GPIOx;           /*!< GPIOx port to be used for I/O functions */
    uint16_t GPIO_Pin;             /*!< GPIO Pin to be used for I/O functions */
    uint8_t LastDiscrepancy;       /*!< Search private */
    uint8_t LastFamilyDiscrepancy; /*!< Search private */
    uint8_t LastDeviceFlag;        /*!< Search private */
    uint8_t ROM_NO[8];             /*!< 8-bytes address of last search device */
} OneWire_t;

/* OneWire delay */
void ONEWIRE_DELAY(uint16_t time_us);

/* Pin settings */
void ONEWIRE_LOW(OneWire_t *gp);            
void ONEWIRE_HIGH(OneWire_t *gp);        
void ONEWIRE_INPUT(OneWire_t *gp);        
void ONEWIRE_OUTPUT(OneWire_t *gp);        

/* OneWire commands */
#define ONEWIRE_CMD_RSCRATCHPAD            0xBE
#define ONEWIRE_CMD_WSCRATCHPAD            0x4E
#define ONEWIRE_CMD_CPYSCRATCHPAD        0x48
#define ONEWIRE_CMD_RECEEPROM            0xB8
#define ONEWIRE_CMD_RPWRSUPPLY            0xB4
#define ONEWIRE_CMD_SEARCHROM            0xF0
#define ONEWIRE_CMD_READROM                0x33
#define ONEWIRE_CMD_MATCHROM            0x55
#define ONEWIRE_CMD_SKIPROM                0xCC

It defines a structure and the oneWire Delay_us function.

There are functions for setting up GPIO, and the OneWire Commands are the commands we saw in the datasheet.

Looking at the OneWire WriteBit command, it sends either 0 or 1, switches to output, gives a delay, switches to input, gives another delay, and switches back to input. The only difference between sending 0 and 1 is the length of the delay.

inline void OneWire_WriteBit(OneWire_t* OneWireStruct, uint8_t bit)
{
    if (bit) 
    {
        /* Set line low */
        ONEWIRE_LOW(OneWireStruct);
        ONEWIRE_OUTPUT(OneWireStruct);
        ONEWIRE_DELAY(10);

        /* Bit high */
        ONEWIRE_INPUT(OneWireStruct);

        /* Wait for 55 us and release the line */
        ONEWIRE_DELAY(55);
        ONEWIRE_INPUT(OneWireStruct);
    } 
    else 
    {
        /* Set line low */
        ONEWIRE_LOW(OneWireStruct);
        ONEWIRE_OUTPUT(OneWireStruct);
        ONEWIRE_DELAY(65);

        /* Bit high */
        ONEWIRE_INPUT(OneWireStruct);

        /* Wait for 5 us and release the line */
        ONEWIRE_DELAY(5);
        ONEWIRE_INPUT(OneWireStruct);
    }

}

1-wire communication uniquely switches a single wire between GPIO input and output to transmit and receive data.

The Read function works similarly, with different delays.

inline uint8_t OneWire_ReadBit(OneWire_t* OneWireStruct) 
{
    uint8_t bit = 0;

    /* Line low */
    ONEWIRE_LOW(OneWireStruct);
    ONEWIRE_OUTPUT(OneWireStruct);
    ONEWIRE_DELAY(2);

    /* Release line */
    ONEWIRE_INPUT(OneWireStruct);
    ONEWIRE_DELAY(10);

    /* Read line value */
    if (HAL_GPIO_ReadPin(OneWireStruct->GPIOx, OneWireStruct->GPIO_Pin)) {
        /* Bit is HIGH */
        bit = 1;
    }

    /* Wait 50us to complete 60us period */
    ONEWIRE_DELAY(50);

    /* Return bit value */
    return bit;
}

It assumes the bit is 0 and changes it to 1 if it enters the if statement.

As expected, read/writeByte loops 8 times since 8 bits make up 1 byte.

void OneWire_WriteByte(OneWire_t* OneWireStruct, uint8_t byte) {
    uint8_t i = 8;
    /* Write 8 bits */
    while (i--) {
        /* LSB bit is first */
        OneWire_WriteBit(OneWireStruct, byte & 0x01);
        byte >>= 1;
    }
}

uint8_t OneWire_ReadByte(OneWire_t* OneWireStruct) {
    uint8_t i = 8, byte = 0;
    while (i--) {
        byte >>= 1;
        byte |= (OneWire_ReadBit(OneWireStruct) << 7);
    }

    return byte;
}

OneWire Search was thought to search for data, but it actually searches for devices. It's a very long code, and since we will use only one device and skiprom, it doesn't seem necessary.

uint8_t OneWire_Search(OneWire_t* OneWireStruct, uint8_t command) {
    uint8_t id_bit_number;
    uint8_t last_zero, rom_byte_number, search_result;
    uint8_t id_bit, cmp_id_bit;
    uint8_t rom_byte_mask, search_direction;

    /* Initialize for search */
    id_bit_number = 1;
    last_zero = 0;
    rom_byte_number = 0;
    rom_byte_mask = 1;
    search_result = 0;

    // if the last call was not the last one
    if (!OneWireStruct->LastDeviceFlag)
    {
        // 1-Wire reset
        if (OneWire_Reset(OneWireStruct)) 
        {
            /* Reset the search */
            OneWireStruct->LastDiscrepancy = 0;
            OneWireStruct->LastDeviceFlag = 0;
            OneWireStruct->LastFamilyDiscrepancy = 0;
            return 0;
        }

        // issue the search command 
        OneWire_WriteByte(OneWireStruct, command);  

        // loop to do the search
        do {
            // read a bit and its complement
            id_bit = OneWire_ReadBit(OneWireStruct);
            cmp_id_bit = OneWire_ReadBit(OneWireStruct);

            // check for no devices on 1-wire
            if ((id_bit == 1) && (cmp_id_bit == 1)) {
                break;
            } else {
                // all devices coupled have 0 or 1
                if (id_bit != cmp_id_bit) {
                    search_direction = id_bit;  // bit write value for search
                } else {
                    // if this discrepancy if before the Last Discrepancy
                    // on a previous next then pick the same as last time
                    if (id_bit_number < OneWireStruct->LastDiscrepancy) {
                        search_direction = ((OneWireStruct->ROM_NO[rom_byte_number] & rom_byte_mask) > 0);
                    } else {
                        // if equal to last pick 1, if not then pick 0
                        search_direction = (id_bit_number == OneWireStruct->LastDiscrepancy);
                    }

                    // if 0 was picked then record its position in LastZero
                    if (search_direction == 0) {
                        last_zero = id_bit_number;

                        // check for Last discrepancy in family
                        if (last_zero < 9) {
                            OneWireStruct->LastFamilyDiscrepancy = last_zero;
                        }
                    }
                }

                // set or clear the bit in the ROM byte rom_byte_number
                // with mask rom_byte_mask
                if (search_direction == 1) {
                    OneWireStruct->ROM_NO[rom_byte_number] |= rom_byte_mask;
                } else {
                    OneWireStruct->ROM_NO[rom_byte_number] &= ~rom_byte_mask;
                }

                // serial number search direction write bit
                OneWire_WriteBit(OneWireStruct, search_direction);

                // increment the byte counter id_bit_number
                // and shift the mask rom_byte_mask
                id_bit_number++;
                rom_byte_mask <<= 1;

                // if the mask is 0 then go to new SerialNum byte rom_byte_number and reset mask
                if (rom_byte_mask == 0) {
                    //docrc8(ROM_NO[rom_byte_number]);  // accumulate the CRC
                    rom_byte_number++;
                    rom_byte_mask = 1;
                }
            }
        } while (rom_byte_number < 8);  // loop until through all ROM bytes 0-7

        // if the search was successful then
        if (!(id_bit_number < 65)) {
            // search successful so set LastDiscrepancy,LastDeviceFlag,search_result
            OneWireStruct->LastDiscrepancy = last_zero;

            // check for last device
            if (OneWireStruct->LastDiscrepancy == 0) {
                OneWireStruct->LastDeviceFlag = 1;
            }

            search_result = 1;
        }
    }

In this post, we have looked at the basic usage and related code for the Ds18b20 temperature sensor.

I didn't realize I could include code in posts. It's great to use this new feature, and I'm excited to keep learning and trying new features to make better posts.

In the next blog post, we will configure the clock settings, as clock configuration is essential for using the DS18B20 sensor.

0
Subscribe to my newsletter

Read articles from Sam Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sam Lee
Sam Lee