EDITED:
This sketch can be setup to run (1 to n) DS18B20 devices on a particle.io device. I used five. It is flexible in number of devices, sample times (90ms-whatever, or as fast as can be done), bit resolution (9-12bits), publishing max times, publishing min times (based on a defined temperature differential).
It is fast and does not depend on any delay functions except those built into the OneWire protocol. If you have 10 devices hooked up, you could get about 80 temperature conversions/second in 9-bit mode. This program has some built-in monitoring of CRC failures, that were used to find the source of the CRC errors. But with the fixed OneWire.cpp file, there are no errors for the Photon.
Through this project we discovered a bug in the particle.io implementation of OneWire. If you include the fixed “OneWire.cpp” file as part of your compile, and you have one of the “fixable devices”, you will get ZERO CRC errors once you setup your experiment to run correctly (hookups, pullup resistor, etc). The OneWire bus is actually extremely robust when not interrupted in the middle of bit transfers. Hopefully, particle.io will have some suggestions to fix this for ALL its devices in the near future. See this thread for more information:
This is my first real attempt at writing and publishing a sketch that can be used by others. Excuse my coding style, formatting style, and commenting style, this is all new to me. Any feedback on better techniques would be appreciated.
If you find this useful, let me know…I am kind of proud of my first real program that interacts with the environment (temp sensors), haha.
See my final code in about Post 17
=================
ORIGINAL POST STARTS HERE
I’ve been working on some code to include DS18B20 devices in my Photon project to read non-critical temperatures of my pool system (water, ambient air, chlorine tank, etc). I’ve struggled over the last couple days to get a snippet of code working successfully that I would then include into my project.
Not being a programmer (I did assembly and a hardware coding language over 20 years ago), when I have problems, I REALLY have problems. In any case, by delving into the code, I finally got something that works the way that I want. I thought I would post it here, since there are a number of other posts where persons have issues bringing up multiple DS18B20 devices on a single OneWire bus.
Essentially, I have extracted/modified the code to control/start/read temperature conversions from the DS18B20. By doing so, I have eliminated the abstraction level that the DS18B20 library provided and directly interface to the DS18B20s via functions of OneWire.
Why would I do this? Well, there are a few issues I had with the DS18B20 library. First, I believe the code for changing the resolution of DS18B20s is wrong and doesn’t work. It’s possible I was not using it in the correct manner. Second, the DS18B20 library seems to rely on DELAYs (up to .75 seconds) for each conversion requested…and each request must be made individually, one for each device on the OneWire. I really don’t like the use of DELAY (even though I realize the OneWire implementation uses it extensively in much shorter time “bursts”). Third, there seems to be quite a bit of overhead associated with the OneWire and the DS18B20 libraries that I don’t need for my project. I am unclear (since I have really never worked with higher level languages) how unneeded/unused portions of libraries are incorporated into the compiled code…at this point I don’t know how much code I will have in my completed project. Fourth, I had no clue what was going on with the libraries and, since it wasn't working, the only recourse for me is to start taking out things until I got to a bare minimum of functionality that I understand completely.
My son-in-law tells me that this is not how coding is done these days (for modularity, for testing, etc), but I couldn’t help it, haha. So here it is:
Some restrictions:
- There is NO support for parasitic power on the OneWire bus
- There is NO support for CRC error checking with this implementation although it could be added back. I won’t need it on my project but will put in some minimal checks to use only “valid” data that is received from the DS18B20 devices. I do see an occasional temperature read failure with this basic implementation. Remember, my temperature readings are NOT critical, I’ll just use the next “good one”. This coding example has absolutely NO error checking that I will eventually include in my project.
- This implementation assumes that only DS18B20 devices are attached to the OneWire. There is NO support for its variations.
- In order to simplify things, all UNIQUE 8-byte DS18B20 Addresses must be extracted from the devices and put directly into the code. I will not be changing out sensors (unless there is a failure), so it wasn’t worth it for my project to search out the addresses of the DS18B20 devices when my implementation is essentially static. I must add, that this seems to have introduced a LOT of stability to my solution. When I was working with the library, I had multiple, multiple failures to detect all devices and addresses on the OneWire bus. In fact, I don’t think I ever was successful to have it recognize all five of my devices (I bought a five-pack on Amazon) at the same time.
There are only 3 functions of the DS18B20 that I needed and have implemented.
- Set the conversion resolution of all DS18B20s (9,10, 11, or 12 bit). This is a function that sets the resolution of all devices simultaneously to the same value.
- Start a temperature conversion on the DS18B20s. This is a function that starts a temperature conversion on all devices simultaneously.
- Read the temperature recorded by any DS18B20. This addresses and extracts the recorded temperature from a single DS18B20.
This code snippet works well for my setup. I have attached five DS18B20s (each with a 3-meter wire wire) to it. This is also how my final solution will be deployed. Not sure how it would apply to those projects which have their sensors spread out over a larger area. In general, I would say it is a good thing to minimize traffic on the OneWire, that is where most problems/issues seem to occur.
I do observe an occasional failure in the temperature reading, maybe one out of every few hundred (edit: i was wrong about this number). Keep in mind that I will be adding some basic “error checking”/”error ignoring” in my final solution that is not shown in this snippet.
In any case, I would like to hear any feedback good or bad in the process I have gone through. Remember, I really am a neophyte when it comes to all this.
#define ONEWIRE_SEARCH 0 // OneWire option: ignore the search code for devices/device addresses
#define ONEWIRE_CRC 0 // OneWire option: ignore the CRC code
#define ONEWIRE_CRC16 0 // OneWire option: ignore 16-bit CRC code (redundant since CRC is eliminated on prior line)
#include "OneWire.h"
#include <tgmath.h>
//ds18b20 resolution is determined by the byte written to it's configuration register
enum DS18B20_RESOLUTION : uint8_t {
DS18B20_9BIT = 0x1f, // 9 bit 93.75 ms conversion time
DS18B20_10BIT = 0x3f, // 10 bit 187.50 ms conversion time
DS18B20_11BIT = 0x5F, // 11 bit 375.00 ms conversion time
DS18B20_12BIT = 0x7F, // 12 bit 750.00 ms conversion time
};
//if ds18b20 resolution is less than full 12-bit, the low bits of the data should be masked...
enum DS18B20_RES_MASK : uint8_t {
DS18B20_9BIT_MASK = 0xf8,
DS18B20_10BIT_MASK = 0xfc,
DS18B20_11BIT_MASK = 0xfe,
DS18B20_12BIT_MASK = 0xff,
};
//ds18b20 conversion time is ALSO determined by the byte written to it's configuration register
enum DS18B20_CONVERSION_TIME : uint16_t {
DS18B20_9BIT_TIME = 100, // 9 bit 93.75 ms conversion time w/pad
DS18B20_10BIT_TIME = 200, // 10 bit 187.50 ms conversion time w/pad
DS18B20_11BIT_TIME = 400, // 11 bit 375.00 ms conversion time w/pad
DS18B20_12BIT_TIME = 800, // 12 bit 750.00 ms conversion time w/pad
};
#define DS18B20_PIN_ONEWIRE D2 // my system implements OneWire on Photon pin D2
#define NUM_DS18B20_DEVICES 5 // my system has FIVE DS18B20 devices attached to OneWire (on pin D2)
#define DS18B20_RESOLUTION DS18B20_11BIT // my system uses 11-bit: select appropriate enumerated selection above
#define DS18B20_RES_MASK DS18B20_11BIT_MASK // my system uses 11-bit: select appropriate enumerated selection above
#define DS18B20_CONVERSION_TIME DS18B20_11BIT_TIME // my system uses 11-bit: select appropriate enumerated selection above
#define DS18B20_SAMPLE_INTERVAL 500 // defines the DS18B20 Sampling Interval, which determines how often to sample the
// DS18B20 devices...this interval rescheduled automatically
// .....should be greater than: DS18B20_CONVERSION_TIME * 5 (# of Sensors) + pad(tbd)
// .....but an "interval check" in the rescheduling should handle a time "violation"
//Publishing Definitions and variables
#define PUBLISH_MAX_INTERVAL 60000 // every 10 seconds ...(these values change continuously as I am testing my system...this is 60 seconds)
#define PUBLISH_MIN_INTERVAL 1000 // every 1 second
unsigned long currentMillis;
unsigned long prior_publish_time = 0; // initialize to a value out in time, will be re-initialized later, TODO: need a better way to do this
bool publishNOW = false; // a particular function may request an immediate status publish by making this "true"
// OneWire DS18B20 8-byte unique addresses that must be obtained and entered into the table below
// Use the OneWire example code called Address_Scanner to gather these...
// ...then input them into the array below
//
// ********* REPLACE THESE ADDRESSES WITH THE UNIQUE ADDRESSES OF YOUR DS18B20 DEVICES **************
//
const uint8_t DS18B20_OneWire_ADDRESSES[NUM_DS18B20_DEVICES][8] =
{0x28, 0xAA, 0x8D, 0x68, 0x3F, 0x14, 0x01, 0x2E, // address of first DS18B20 device
0x28, 0xAA, 0x49, 0x88, 0x3F, 0x14, 0x01, 0x5A, // address of 2nd DS18B20 device
0x28, 0xAA, 0x49, 0x67, 0x3F, 0x14, 0x01, 0x89, // ..
0x28, 0xAA, 0xB3, 0x6E, 0x3F, 0x14, 0x01, 0x01, // ..
0x28, 0xAA, 0xF6, 0x6F, 0x3C, 0x14, 0x01, 0x51}; // address of last DS18B20 device
unsigned long prior_DS18B20_conversion_start = 0, prior_DS18B20_interval_start = 0, current_DS18B20_interval_start = 0;
uint16_t current_temps_raw[NUM_DS18B20_DEVICES] = {70,70,70,70,70}; // current raw readings of temperature sensors
float f_current_temps[NUM_DS18B20_DEVICES]; // current temperature readings from sensors
float f_current_temps_pub[NUM_DS18B20_DEVICES]; // last published temperatures of the sensor
// temporary (probably) variables for testing and characterizing: error counts and total conversions counter
unsigned long error_count_00 = 0, error_count_ff = 0, error_count_20, error_count_OR, error_count_5;
unsigned long conversion_count = 0;
bool DS18B20_read_conversions = false;
uint8_t DS18B20_ptr = 0;
// Function declarations
void start_DS18B20_Conversions();
void set_DS18B20_Resolutions(uint8_t resolution);
int16_t read_DS18B20_Conversion(const uint8_t addr[8]);
void doTemperatureCalculations();
bool DS18B20_SamplingComplete();
OneWire ds18b20_onewire(DS18B20_PIN_ONEWIRE); // instantiate the OneWire bus
void setup() {
set_DS18B20_Resolutions(DS18B20_RESOLUTION); //Set resolution of ALL DS18B20s attached to OneWire to the same value and at the same time
}
void loop()
{
currentMillis = millis();
// Publish the status if conditions are met
if (((currentMillis - prior_publish_time >= PUBLISH_MIN_INTERVAL) && publishNOW) ||
(currentMillis - prior_publish_time >= PUBLISH_MAX_INTERVAL)) {
if (publishAllStatus()) { // function attempts to publish the status
publishNOW = false; // ...if successful then get ready for next publish
for (uint8_t i = 0; i < NUM_DS18B20_DEVICES; i++) {
f_current_temps_pub[i] = f_current_temps[i];
publishNOW = false;
}
prior_publish_time = currentMillis; // setup for the next publish time
}
}
if (DS18B20_SamplingComplete()) doTemperatureCalculations();
}
// The sampling code for the DS18B20 sensors simply starts a conversion on all devices, and then reads the result from each device
// Only one sample is taken from each..I want to get a feeling for IF averaging of successive readings is necessary, etc.
bool DS18B20_SamplingComplete() {
// Enter this code body if within a valid DS18B20 sampling interval window AND prior DS18B20 temperature conversions have haad time to complete
if (((currentMillis - prior_DS18B20_conversion_start) >= DS18B20_CONVERSION_TIME) &&
((currentMillis - prior_DS18B20_interval_start) >= DS18B20_SAMPLE_INTERVAL)) {
// 1) start DS18B20 temperature conversions 2) read one sampled DS18B20 conversion one by one 3) sampling for the interval is complete
if (!DS18B20_read_conversions && (DS18B20_ptr == 0)) {
// starts temperature conversions on all DS18B20 devices attached to the OneWire bus
start_DS18B20_Conversions();
prior_DS18B20_conversion_start = millis(); // capture the start conversion time for determination of finish time
current_DS18B20_interval_start = prior_DS18B20_conversion_start; // capture the start time to schedule next interval
DS18B20_read_conversions = true;
}
else if (DS18B20_read_conversions) {
// no rush, read only ONE of the DS18B20 results. Cycling through them one at a time avoids all OneWire bus traffic within one loop() execution
current_temps_raw[DS18B20_ptr] = read_DS18B20_Conversion(DS18B20_OneWire_ADDRESSES[DS18B20_ptr]);
if (++DS18B20_ptr >= NUM_DS18B20_DEVICES) { // advance pointer to next analog channel of ADS1115, check if all DS18B20s have been sampled
DS18B20_read_conversions = false;
}
}
else { // here, all samples have been completed, so setup for the next DS18B20 sample interval and return "true" for compeltion
DS18B20_ptr = 0; // ...reset to 0 if 4+, there are only four channels on the DS18B20, AD0:AD3
prior_DS18B20_interval_start = // just in case the sample interval was extended or held up (for some reason) and exceeded the DS18B20_SAMPLE_INTERVAL
((currentMillis - DS18B20_SAMPLE_INTERVAL) > current_DS18B20_interval_start) ? currentMillis : current_DS18B20_interval_start;
return(true);
}
}
return(false);
}
// right now this routine does calculate the temps...but
// most of this is temporary code to see what the error rate is and what kind of errors I get when reading from the OneBus
// Eventually, there will be some kind of algorithm in here to determine if any given sensor reading is valid or not. There will also
// possibly be some error logging to see if in my final "real world" deployment at my pool equipment, is significantly
// different from the bench tests.
void doTemperatureCalculations() {
float temperature;
for (uint8_t i = 0; i < NUM_DS18B20_DEVICES; i++) {
conversion_count++; //keep track of # of conversions
if (current_temps_raw[i] > 2048) error_count_ff++; // reading all 1's check (kinda)
else if ((current_temps_raw[i] == 0) && !(f_current_temps_pub == 0)) error_count_00++; // reading ALL 0's check
else if ((current_temps_raw[i] > 880 /* corresponds to 55 celsius, 130 farenheit */) || (current_temps_raw[i] < 1 /* 0.1 farenheit */)) error_count_OR++; //outside range check
else {
temperature = current_temps_raw[i] / 16.0 * 1.8 + 32; // this is the Farenheit calculation read from the ds18b20
//temperature = current_temps_raw[i] / 16.0; // this is the Celsius calculation read from the ds18b20
if (fabs(f_current_temps_pub[i] - temperature) > 5) { // check if current reading is within 5 degrees of last reading
error_count_5++; // probably an error occurred
publishNOW = true;
}
else if (fabs(f_current_temps_pub[i] - temperature) > 1) publishNOW = true; // force a publish if temperature has changed by more than 1 degree since last published
f_current_temps[i] = temperature;
}
}
}
// this function sets the resolution for ALL ds18b20s on an instantiated OneWire
void set_DS18B20_Resolutions(uint8_t resolution)
{
ds18b20_onewire.reset(); // onewire intialization sequence, to be followed by other commands
ds18b20_onewire.write(0xcc); // onewire "SKIP ROM" command, selects ALL ds18b20s on bus
ds18b20_onewire.write(0x4e); // onewire "WRITE SCRATCHPAD" command (requires write to 3 registers: 2 hi-lo regs, 1 config reg)
ds18b20_onewire.write(100); // 1) write dummy value (100) to temp hi register
ds18b20_onewire.write(0); // 2) write dummy value (0)to temp lo register
ds18b20_onewire.write(resolution); // 3) write the selected resolution to configuration registers of all ds18b20s on the bus
}
// this function intitalizes simultaneous temperature conversions for ALL ds18b20s on an instantiated OneWire
void start_DS18B20_Conversions()
{
ds18b20_onewire.reset(); // onewire intitialization sequence, to be followed by other commands
ds18b20_onewire.write(0xcc); // onewire "SKIP ROM" command, addresses ALL ds18b20s on bus
ds18b20_onewire.write(0x44); // onewire wire "CONVERT T" command, starts temperature conversion on ALL ds18b20s
}
// this function returns the RAW temperature conversion result of a SINGLE selected DS18B20 device (via it's address)
int16_t read_DS18B20_Conversion(const uint8_t addr[8])
{
byte datal, datah;
ds18b20_onewire.reset(); // onewire intitialization sequence, to be followed by other commands
ds18b20_onewire.select(addr); // issues onewire "MATCH ROM" address which selects a SPECIFIC (only one) ds18b20 device
ds18b20_onewire.write(0xBE); // onewire "READ SCRATCHPAD" command, to access selected ds18b20's scratchpad
// reading TWO bytes (of 9 available) of the selected ds18b20's scratchpad which contain the temperature conversion value
datal = ds18b20_onewire.read() & DS18B20_RES_MASK; // low byte should be masked by the resolution of the conversion
datah = ds18b20_onewire.read();
ds18b20_onewire.reset(); // ds18b20 requirement: since only a subeset of the scratchpad is read, a OneWire reset MUST be issued
return (int16_t) ((datah << 8) | datal);
}
// Publishes the status, in my case: specifically sends it to a Google spreadsheet and the PoolController Android app
bool publishAllStatus() {
return publishNonBlocking(
"temp", // identifies the specific Google spreadsheet SHEET to record this data
"{\"T0\":" + String(f_current_temps[0], 1) + // "Header name" of identified Google SHEET column where this data is recorded
",\"T1\":" + String(f_current_temps[1], 1) +
",\"T2\":" + String(f_current_temps[2], 1) +
",\"T3\":" + String(f_current_temps[3], 1) +
",\"T4\":" + String(f_current_temps[4], 1) +
",\"CC\":" + String(conversion_count) +
",\"Eff\":" + String(error_count_ff) +
",\"E00\":" + String(error_count_00) +
",\"EOR\":" + String(error_count_OR) +
",\"E5\":" + String(error_count_5) +
"}");
}
// A wrapper around Partical.publish() to check connection first to prevent
// blocking. The prefix "pool-" is added to all names to make subscription easy.
bool publishNonBlocking(String name, String message) {
// TODO replace with a failure queue?
if (Particle.connected()) {
bool success = Particle.publish("pool-" + name, message, PUBLIC); // TODO, need to understand ramifications of making this PRIVATE
// Serial.printlnf("Published \"%s\" : \"%s\" with success=%d",
// name.c_str(), message.c_str(), success);
return success;
} else {
// Serial.printlnf("Published \"%s\" : \"%s\" with success=0 no internet",
// name.c_str(), message.c_str());
}
return false;
}
For latest sample output...see a few posts down