Moteino / Arduino and 1Wire : Optimize your read for speed

Hello all,

I’ve been picking apart my code to do some optimization, and an obvious target was the read of 1Wire devices, the DS18B20 in particular. Currently, I use the OneWire library, without the DallasTemperature library, which I didn’t much care for.

What all 1Wire read routines have in common is that they issue the Convert command prior to reading a measured value. For the DS18B20, this is the Convert T command, 0x44. Depending on the resolution, the conversion takes from 75-750ms, per the datasheet.

There are three main issues I had with the implementation using OneWire as in the examples:

  • There was no provision for setting the resolution. While this is available in the DallasTemperature library, it just didn’t make sense to me to rewrite all of my lean code around this library to get this one feature.
  • Wait times are hardcoded. Per the datasheet, it is possible to check with the device for status after issuing the Convert T command to know exactly when the temperature conversion is complete. In most implementations, however, this wait time is hard-coded, and typically VERY generously. For example, in a read operation at 12 bits that actually takes <600ms, the hardcoded wait time is 1000ms. This is a 40% savings, ladies and gentlemen. Futhermore, most test code does not even adjust the delay time for different resolution. This is just insane. A ready that should take 60-75ms will implement a wait of 1000ms (?!). I mean, I understand if speed is not that important to you, but this is crazy.
  • Everything is done synchronously. Nothing else can happen while we are waiting for this conversion This is very inefficient, that is, in the case that your program needs to do anything else. This is  the real reason I dug into this. I wanted to read sensors, but be able to update a display in real-time. The moral of the story: if anything else in your program depends on timing, forget using the default 1Wire read code. So to easily work out the first two items, I created the example below in code box 1. Run it after substituting your 1Wire pin, and you’ll get something like this for reading 9, 10, 11, and 12 bit resolutions:
dsaddress:2838FF400500004E,
 Conversion took: 76 ms
 Raw Scratchpad Data:
 50 1 0 0 1F FF 10 10 21
 Temp (C): 21.00

dsaddress:2838FF400500004E,
 Conversion took: 150 ms
 Raw Scratchpad Data:
 50 1 0 0 3F FF 10 10 51
 Temp (C): 21.00

dsaddress:2838FF400500004E,
 Conversion took: 298 ms
 Raw Scratchpad Data:
 50 1 0 0 5F FF 10 10 C1
 Temp (C): 21.00

dsaddress:2838FF400500004E,
 Conversion took: 596 ms
 Raw Scratchpad Data:
 4F 1 0 0 7F FF 1 10 37
 Temp (C): 20.94

A HUGE improvement over stock wait times. With the accuracy of the DS18B20, it really doesn’t make much sense to use 12 bits, so 10 bits saves me loads in timing. I left in a bunch of original code that’s been commented out so you can see how it was done previously.

Now, we can also separate conversion commands and reading the data back. If you have multiple sensors, you actually want to use the Skip ROM and Convert T commands to tell all devices on the bus to convert simultaneously, but that’s another topic. In the meantime while conversion is taking place, we can do other stuff.

So we separate our read DS18B20 routine into find, set resolution, send conversion command, and finally read temperature. Between the last two, we just continually check in our loop to see that data is ready, and when it is, we read it. Pretty simple, but SUPER EFFECTIVE. You can see we lose a little due to overhead, but still, plenty fast, and we can do other stuff at the same time.

Enjoy!
C

Temp (C): 21.50
Elapsed time (ms): 99
Temp (C): 21.50
Elapsed time (ms): 170
Temp (C): 21.62
Elapsed time (ms): 321
Temp (C): 21.56
Elapsed time (ms): 618

Code box 1:

#include <OneWire.h>

#define LED 9
#define SERIAL_BAUD   115200

void setup(void) {
Serial.begin(SERIAL_BAUD);
}

void loop(void) {
for (int i=9;i<13;i++){
handleOWIO(6,i);
Serial.println();
}

delay(1000);
Blink(LED,3);
}

void handleOWIO(byte pin, byte resolution) {
int owpin = pin;

// Device identifier
byte dsaddr[8];
OneWire myds(owpin);
getfirstdsadd(myds,dsaddr);

Serial.print(F("dsaddress:"));
int j;
for (j=0;j<8;j++) {
if (dsaddr[j] < 16) {
Serial.print('0');
}
Serial.print(dsaddr[j], HEX);
}
// Data

Serial.println(getdstemp(myds, dsaddr, resolution));

} // run OW sequence

void getfirstdsadd(OneWire myds, byte firstadd[]){
byte i;
byte present = 0;
byte addr[8];
float celsius, fahrenheit;

int length = 8;

//Serial.print("Looking for 1-Wire devices...\n\r");
while(myds.search(addr)) {
//Serial.print("\n\rFound \'1-Wire\' device with address:\n\r");
for( i = 0; i < 8; i++) {
firstadd[i]=addr[i];
//Serial.print("0x");
if (addr[i] < 16) {
//Serial.print('0');
}
//Serial.print(addr[i], HEX);
if (i < 7) {
//Serial.print(", ");
}
}
if ( OneWire::crc8( addr, 7) != addr[7]) {
//Serial.print("CRC is not valid!\n");
return;
}
// the first ROM byte indicates which chip

//Serial.print("\n\raddress:");
//Serial.print(addr[0]);

return;
}
}

float getdstemp(OneWire myds, byte addr[8], byte resolution) {
byte present = 0;
int i;
byte data[12];
byte type_s;
float celsius;
float fahrenheit;

switch (addr[0]) {
case 0x10:
//Serial.println(F("  Chip = DS18S20"));  // or old DS1820
type_s = 1;
break;
case 0x28:
//Serial.println(F("  Chip = DS18B20"));
type_s = 0;
break;
case 0x22:
//Serial.println(F("  Chip = DS1822"));
type_s = 0;
break;
default:
Serial.println(F("Device is not a DS18x20 family device."));
}

// Get byte for desired resolution
byte resbyte = 0x1F;
if (resolution == 12){
resbyte = 0x7F;
}
else if (resolution == 11) {
resbyte = 0x5F;
}
else if (resolution == 10) {
resbyte = 0x3F;
}

// Set configuration
myds.reset();
myds.select(addr);
myds.write(0x4E);         // Write scratchpad
myds.write(0);            // TL
myds.write(0);            // TH
myds.write(resbyte);         // Configuration Register

myds.write(0x48);         // Copy Scratchpad

myds.reset();
myds.select(addr);

long starttime = millis();
myds.write(0x44,1);         // start conversion, with parasite power on at the end
while (!myds.read()) {
// do nothing
}
Serial.print("Conversion took: ");
Serial.print(millis() - starttime);
Serial.println(" ms");

//delay(1000);     // maybe 750ms is enough, maybe not
// we might do a ds.depower() here, but the reset will take care of it.

present = myds.reset();
myds.select(addr);
myds.write(0xBE);         // Read Scratchpad

//Serial.print("  Data = ");
//Serial.print(present,HEX);
Serial.println("Raw Scratchpad Data: ");
for ( i = 0; i < 9; i++) {           // we need 9 bytes
data[i] = myds.read();
Serial.print(data[i], HEX);
Serial.print(" ");
}
//Serial.print(" CRC=");
//Serial.print(OneWire::crc8(data, 8), HEX);
Serial.println();

// convert the data to actual temperature

unsigned int raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// count remain gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
} else {
byte cfg = (data[4] & 0x60);
if (cfg == 0x00) raw = raw << 3;  // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw << 2; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw << 1; // 11 bit res, 375 ms
// default is 12 bit resolution, 750 ms conversion time
}
}
celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;
Serial.print("Temp (C): ");
//Serial.println(celsius);
return celsius;
}

void Blink(byte PIN, int DELAY_MS)
{
pinMode(PIN, OUTPUT);
digitalWrite(PIN,HIGH);
delay(DELAY_MS);
digitalWrite(PIN,LOW);
}

Code box 2:

#include <OneWire.h>

#define LED 9
#define SERIAL_BAUD   115200

OneWire myds(6);
byte readstage;
byte resolution;
unsigned long starttime;
unsigned long elapsedtime;
byte dsaddr[8];

void setup(void) {
Serial.begin(SERIAL_BAUD);
readstage = 0;
resolution = 12;
}

void loop(void) {

if (readstage == 0){
getfirstdsadd(myds,dsaddr);
dssetresolution(myds,dsaddr,resolution);
starttime = millis();
dsconvertcommand(myds,dsaddr);
readstage++;
}
else {
if (myds.read()) {
Serial.println(dsreadtemp(myds,dsaddr, resolution));

Serial.print("Elapsed time (ms): ");
elapsedtime = millis() - starttime;
Serial.println(elapsedtime);
readstage=0;
if (resolution == 12){
resolution = 9;
}
else {
resolution ++;
}
}
}

Blink(LED,5);
}

void getfirstdsadd(OneWire myds, byte firstadd[]){
byte i;
byte present = 0;
byte addr[8];
float celsius, fahrenheit;

int length = 8;

//Serial.print("Looking for 1-Wire devices...\n\r");
while(myds.search(addr)) {
//Serial.print("\n\rFound \'1-Wire\' device with address:\n\r");
for( i = 0; i < 8; i++) {
firstadd[i]=addr[i];
//Serial.print("0x");
if (addr[i] < 16) {
//        Serial.print('0');
}
//      Serial.print(addr[i], HEX);
if (i < 7) {
//Serial.print(", ");
}
}
if ( OneWire::crc8( addr, 7) != addr[7]) {
Serial.print("CRC is not valid!\n");
return;
}
// the first ROM byte indicates which chip

//Serial.print("\n\raddress:");
//Serial.print(addr[0]);

return;
}
}

void dssetresolution(OneWire myds, byte addr[8], byte resolution) {

// Get byte for desired resolution
byte resbyte = 0x1F;
if (resolution == 12){
resbyte = 0x7F;
}
else if (resolution == 11) {
resbyte = 0x5F;
}
else if (resolution == 10) {
resbyte = 0x3F;
}

// Set configuration
myds.reset();
myds.select(addr);
myds.write(0x4E);         // Write scratchpad
myds.write(0);            // TL
myds.write(0);            // TH
myds.write(resbyte);         // Configuration Register

myds.write(0x48);         // Copy Scratchpad
}

void dsconvertcommand(OneWire myds, byte addr[8]){
myds.reset();
myds.select(addr);
myds.write(0x44,1);         // start conversion, with parasite power on at the end

}

float dsreadtemp(OneWire myds, byte addr[8], byte resolution) {
byte present = 0;
int i;
byte data[12];
byte type_s;
float celsius;
float fahrenheit;

switch (addr[0]) {
case 0x10:
//Serial.println(F("  Chip = DS18S20"));  // or old DS1820
type_s = 1;
break;
case 0x28:
//Serial.println(F("  Chip = DS18B20"));
type_s = 0;
break;
case 0x22:
//Serial.println(F("  Chip = DS1822"));
type_s = 0;
break;
default:
Serial.println(F("Device is not a DS18x20 family device."));
}

present = myds.reset();
myds.select(addr);
myds.write(0xBE);         // Read Scratchpad

//Serial.print("  Data = ");
//Serial.print(present,HEX);
//  Serial.println("Raw Scratchpad Data: ");
for ( i = 0; i < 9; i++) {           // we need 9 bytes
data[i] = myds.read();
//    Serial.print(data[i], HEX);
//    Serial.print(" ");
}
//Serial.print(" CRC=");
//Serial.print(OneWire::crc8(data, 8), HEX);
//  Serial.println();

// convert the data to actual temperature

unsigned int raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// count remain gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
} else {
byte cfg = (data[4] & 0x60);
if (cfg == 0x00) raw = raw << 3;  // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw << 2; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw << 1; // 11 bit res, 375 ms
// default is 12 bit resolution, 750 ms conversion time
}
}
celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;
Serial.print("Temp (C): ");
//Serial.println(celsius);
return celsius;
}

void Blink(byte PIN, int DELAY_MS)
{
pinMode(PIN, OUTPUT);
digitalWrite(PIN,HIGH);
delay(DELAY_MS);
digitalWrite(PIN,LOW);
}

8 thoughts on “Moteino / Arduino and 1Wire : Optimize your read for speed”

  1. Thank you for this code, really surprising that the splitting in conversion command and read temperature is not done in the “standard” / existing lib already! It is so a huge benefit for running non blocking sketches and also for power saving scenarios.

    I tried the code #1 in the Moteino board first https://lowpowerlab.com/forum/index.php/topic,666.msg3910.html but it did not run. There is an additional line in the nun running code
    sprintf(dscharaddr,”%02x%02x%02x%02x%02x%02x%02x%02x”,dsaddr[0],dsaddr[1],dsaddr[2],dsaddr[3],dsaddr[4],dsaddr[5],dsaddr[6],dsaddr[7]);
    Serial.println(‘,’);

    that is missing in the code above.

    Second my hardware setup was a parasitic mode (see http://openenergymonitor.org/emon/buildingblocks/DS18B20-temperature-sensing). I got a reading but the reading was wrong. So I changed my setting to normal and after this modification it runs.

    Btw would be nice(er) to have the pin definition in a separate variable on top. In case someone is searching it is the first argument in the function “handleOWIO”.

    1. Thanks for the comment. I too found it incredible that all of the code I found was blocking and non-optimized, especially considering how popular these sensors are and how long they’ve been around. Not to mention that the same convert, then read sequence is common to all 1Wire sensors.

      Is there code above that isn’t working? I often truncate code for clarity and sometimes things get left out. The dscharadd array was really only there for debug/printing purposes, so I eliminated it when shaving memory.

      The reason the OWIO pin isn’t defined is that I loop over all of my available IO pins, and use the handleOWIO function for whichever pin I’m processing. I use more or less the same sketch for every application, except when I do things like use LCDs. See here for example: http://www.cupidcontrols.com/2014/09/adventures-in-moteino-modular-communication-for-cupid-remote/

      Colin

      1. I assume by ‘conversion polling’ I assume you are referring to the strong pullup on the bus required in parasitic power mode that precludes all other activity on the bus. You are correct. I always power the devices, so I do not consider this possibility in any code or write-ups here.

  2. Hi,
    For an art installation I’m planning to use an 1-Wire bus to read 25 ultrasonic sensors (Sainsmart HC-SR04 + Maxim DS2450) and 10 Motion Sensors (Seeed Studio Mini PIR + Maxim DS2413).
    As master I planned to use a “Serial Port To 1-Wire Controller”(Maxim DS2480B). The bus should run in “overdrive” mode. For the wiring (ca. 50m) I planned to use a shielded cat7 cable.

    There is just one open question:
    Does this 1-Wire setup allow to read all 35 sensors at least every 1000 ms?

    I wonder if there is any chance to get an approximate read-rate without setting up the whole system …

    salute
    Thomas

    1. I don’t see why not, although you’d be better off using the DS2438, since the DS2450 has been discontinued. Conversion time on the DS2438 is listed as 10ms. The approach of issuing Skip Rom and convert will allow you to read all devices in succession after conversion. In this case, however, you will want to check the datasheet to ensure your power supply and voltage drop will be ok for simultaneous conversion. In theory, however, this should be easily possible.

      C

  3. I tried Code in Box 2 and it works fine. It only reads temperature from one of two sensors installed. I have not studied the code yet. First have to indent everything to make it readable. I only need resolution 10 so I fixed that one. Thanks this save me a lot of headache.

Leave a Reply

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