Лёгкая библиотека для термометров Dallas DS18b20, обновлённая и более удобная версия библиотеки microDS18B20:
- Удобное асинхронное чтение температуры
- Чтение адреса как
uint64_t, удобная работа с адресами - Обработка ошибок чтения температуры и подключения датчика
- Поддержка паразитного питания
- Полный доступ к памяти датчика (чтение/запись своих данных итд)
- Протестировано на AVR/ESP8266/ESP32
Совместима со всеми Arduino платформами (используются Arduino-функции)
P.S. Вместо резистора на 4.7к можно использовать параллельно два по 10к
Один датчик на пине:
GyverDS18Single();
GyverDS18Single(uint8_t pin, bool parasite = true);
// установить пин
void setPin(uint8_t pin);
// сброс шины
bool reset();
// включить режим паразитного питания (умолч. вкл)
void setParasite(bool parasite);
// установить разрешение (9.. 12 бит)
bool setResolution(uint8_t res);
// прочитать разрешение
uint8_t readResolution();
// прочитать адрес датчика. 0 - ошибка
uint64_t readAddress();
// прочитать питание: DS18_PARASITE - паразитное, DS18_EXTERNAL - обычное, 0 - ошибка
uint8_t readPower();
// получить текущее время измерения температуры, мс
uint16_t getConversionTime();
// ===================== TEMP =====================
// автоматический опрос по таймеру, вызывать в loop
// DS18_READY - чтение успешно, DS18_ERROR - ошибка
uint8_t tick();
// установить период работы тикера. Будет сброшен на минимальный при вызове setResolution
void setPeriod(uint16_t prd);
// запросить температуру
bool requestTemp();
// true - температура готова (асинхронно)
bool ready();
// true - температура готова (ждать)
bool waitReady();
// true - идёт ожидание конвертации
bool isWaiting();
// прочитать температуру
bool readTemp();
// получить "сырую" температуру (умножена на 16)
int16_t getTempRaw();
// получить int температуру
int16_t getTempInt();
// получить float температуру, NAN пока буфер не инициализирован
float getTemp();
// в буфере есть валидная температура
bool initialized();
// ===================== MANUAL =====================
// прочитать содержимое оперативной памяти в буфер 5 байт
bool readRAM(gds::RAM* ram);
// записать данные в оперативную память (th, tl)
bool writeRAM(uint8_t b0, uint8_t b1);
// записать данные и разрешение в оперативную память (th, tl, res)
bool writeRAM(uint8_t b0, uint8_t b1, uint8_t res);
// записать содержимое оперативной памяти в EEPROM
bool copyRAM();
// записать содержимое EEPROM в оперативную память
bool recallRAM();
// применить разрешение к таймеру библиотеки
uint8_t applyResolution(uint8_t res);Прямая адресация - нужно передавать адрес датчика в функции:
GyverDS18();
GyverDS18(uint8_t pin, bool parasite = true);
// автоматический запрос по таймеру для всех, вызывать в loop
// DS18_READY - запрос отправлен, DS18_ERROR - ошибка запроса
uint8_t tick();
bool setResolution(uint8_t res); // установить у всех
bool setResolution(uint8_t res, uint64_t addr);
uint8_t readResolution(uint64_t addr);
uint8_t readPower(uint64_t addr);
bool requestTemp(); // запросить у всех
bool requestTemp(uint64_t addr);
bool readTemp(uint64_t addr);
bool readRAM(gds::RAM* ram, uint64_t addr);
bool writeRAM(uint8_t b0, uint8_t b1, uint64_t addr);
bool writeRAM(uint8_t b0, uint8_t b1, uint8_t res, uint64_t addr);
bool copyRAM(uint64_t addr);
bool recallRAM(uint64_t addr);Функции общего буфера getTemp(), getTempInt(), getTempRaw() и initialized(), а также таймерные ready(), waitReady(), isWaiting() и setPeriod() доступны из GyverDS18Single. Важно: температурный буфер здесь один на весь объект, а не отдельный для каждого адреса.
Работает с массивом адресов - нужно передавать индекс датчика в массиве. Массив адресов не копируется: класс хранит переданный указатель, поэтому массив должен существовать всё время работы с объектом.
GyverDS18Array();
GyverDS18Array(uint8_t pin, uint64_t* addr = nullptr, uint8_t amount = 0, bool parasite = true);
// автоматический запрос по таймеру для всех, вызывать в loop
// DS18_READY - запрос отправлен, DS18_ERROR - ошибка запроса
uint8_t tick();
// подключить массив адресов формата uint64_t[]
void setAddress(uint64_t* addr, uint8_t amount);
// получить количество адресов в массиве
uint8_t amount();
// прочитать в массив. Ошибочные чтения записываются как NAN,
// false если ошибка чтения хотя бы одного датчика
bool readTemps(float* arr);
bool setResolution(uint8_t res); // установить у всех
bool setResolution(uint8_t res, uint8_t index);
uint8_t readResolution(uint8_t index);
uint8_t readPower(uint8_t index);
bool requestTemp(); // запросить у всех
bool requestTemp(uint8_t index);
bool readTemp(uint8_t index);
bool readRAM(gds::RAM* ram, uint8_t index);
bool writeRAM(uint8_t b0, uint8_t b1, uint8_t index);
bool copyRAM(uint8_t index);
bool recallRAM(uint8_t index);Функции getTemp*() и initialized() относятся к одному общему внутреннему буферу, а не к отдельному элементу массива.
Работа с памятью:
uint8_t t_lsb;
uint8_t t_msb;
uint8_t th;
uint8_t tl;
uint8_t cfg;
int16_t getTemp();
uint8_t getRes();Хранение и вывод адреса:
Addr();
Addr(const uint64_t addr);
// адрес
uint64_t addr = 0;
// валидность адреса
operator bool();
// доступ по индексу (до 8)
uint8_t& operator[](uint8_t i);
// скопировать в массив (8 байт)
void copyTo(uint8_t* buf);
// напечатать в Print
void printTo(Print& pr, bool newline = true);
// вывести в строку
String toString();Пример:
uint64_t addr; // получен как-либо
gds::Addr(addr).printTo(Serial); // печать
gds::Addr a(addr);
String s = a.toString(); // в строку
uint64_t addr2 = a; // конвертируется в uint64_t
a[0]; // доступ к байтам адресаПоиск адресов на шине
Search(uint8_t pin);
// наследует gds::Addr
// найти адреса и записать в массив
uint8_t scan(uint64_t* addrs, uint8_t len);
// найти по индексу
gds::Addr find(uint8_t n);
// поиск, вызывать в while(), забирать из addr
bool search();
// остановить поиск
void stop();Датчик может подключаться по двум проводам (GND и DATA) - нужно замкнуть у него VCC и GND. DATA-пин так же должен быть подтянут к питанию, а в программе нужно активировать паразитный режим.
Можно подключить несколько датчиков на один пин - в этом случае нужно будет обращаться к ним по адресам. Подробнее об этом - ниже.
Датчик имеет настройку разрешения (точности) измерения температуры 9.. 12 бит. Чем выше разрешение - тем меньше шаг изменения температуры, но тем больше времени требуется датчику для измерения:
| Разрешение, бит | Шаг, °C | Время измерения, мс |
|---|---|---|
| 9 | 0.5 | 95 |
| 10 | 0.25 | 190 |
| 11 | 0.125 | 380 |
| 12 | 0.0625 | 760 |
Для работы с одним датчиком на пине используется класс GyverDS18Single. Самый простой вариант опроса - автоматически через тикер: библиотека сама опрашивает датчик по таймеру и обрабатывает ошибки, а по успешному чтению достаточно просто получить температуру:
#include <GyverDS18.h>
GyverDS18Single ds(2); // пин
void setup() {
Serial.begin(115200);
// ds.setResolution(12); // установка разрешения. Влияет на период опроса
// ds.setPeriod(2000); // ручная установка периода опроса
}
void loop() {
if (!ds.tick()) {
// по готовности и успешному чтению
Serial.println(ds.getTemp());
}
}При изменении разрешения библиотека устанавливает минимальный период опроса для текущего разрешения, т.е. в авто режиме датчик будет выдавать результат с максимально возможной частотой. Период опроса можно увеличить вручную, если это нужно.
ds.getTemp() возвращает значение из внутреннего буфера. До первого валидного чтения он вернёт NAN, состояние буфера можно проверить через ds.initialized(). После успешного чтения значение можно получать сколько угодно раз. При ошибке чтения буфер не сбрасывается и хранит последнее валидное значение.
В этом же режиме можно отловить ошибки - библиотека обрабатывает статус подключения датчика и ошибки передачи данных:
#include <GyverDS18.h>
GyverDS18Single ds(2); // пин
void setup() {
Serial.begin(115200);
}
void loop() {
switch (ds.tick()) {
case DS18_READY:
Serial.println(ds.getTemp());
break;
case DS18_ERROR:
Serial.println("Error");
break;
}
}Можно работать с датчиком на более низком уровне. Температура получается в четыре этапа:
- Запросить температуру
requestTemp() - Подождать время, равное
getConversionTime()- зависит от установленного разрешения - Прочитать температуру
readTemp() - Получить температуру
getTemp()илиgetTempInt()
Note
Сразу после включения DS18B20 имеет в памяти значение 85 градусов, оно также может появиться после замены датчика "на горячую". В GyverDS18Single такое значение не записывается в температурный буфер. Если успешного чтения ещё не было - readTemp() на 85 градусах вернёт ошибку. Если в буфере уже есть валидная температура - readTemp() вернёт true, а getTemp() вернёт предыдущее значение. Другими словами, одиночный датчик сохраняет последнее достоверное значение и не выдаёт внезапные 85 градусов в программу и не кидает ошибку чтения
Note
readTemp() именно читает датчик и при нормальном значении обновляет внутренний буфер, а getTemp() возвращает содержимое этого буфера. Поэтому getTemp() можно вызывать несколько раз подряд без нового чтения
Можно самостоятельно запрашивать измерение и получать ответ по встроенному таймеру - вот пример циклической работы (примечание: если датчик "отвалится", то цикл прервётся, т.к. если запрос завершится ошибкой - таймер не будет перезапущен):
#include <GyverDS18.h>
GyverDS18Single ds(2); // пин
void setup() {
Serial.begin(115200);
ds.requestTemp(); // первый запрос на измерение
}
void loop() {
if (ds.ready()) { // измерения готовы по таймеру
if (ds.readTemp()) { // если чтение успешно
Serial.print("temp: ");
Serial.println(ds.getTemp());
} else {
Serial.println("error");
}
ds.requestTemp(); // запрос следующего измерения
}
}Для запуска первой конвертации также можно использовать статус isWaiting() (примечание: если датчик "отвалится", то запрос будет происходить постоянно со скоростью loop):
void loop() {
// запросить конвертацию
if (!ds.isWaiting()) ds.requestTemp();
// получить и вывести
if (ds.ready()) {
if (ds.readTemp()) Serial.println(ds.getTemp());
// здесь статус waiting будет сброшен
}
}Для синхронной работы (с ожиданием) можно использовать такую конструкцию:
void setup() {
Serial.begin(115200);
// запросить, подождать, прочитать
if (ds.requestTemp() && ds.waitReady() && ds.readTemp()) {
Serial.print("temp: ");
Serial.println(ds.getTemp());
} else {
Serial.println("error");
}
}Можно подключить несколько датчиков на один пин. Для обращения к датчикам нужно знать их уникальные адреса, поэтому сначала нужно получить адрес. Для этого нужно подключить один датчик к пину и вызвать readAddress(). Также можно воспользоваться сканером шины, о нём ниже. Примечание: корректный адрес никогда не может быть равен 0, это можно использовать для проверки корректности.
В данной библиотеке, в отличие от других, адрес представлен типом uint64_t - более удобном для записи и хранения. Для более удобного вывода можно использовать встроенный инструмент Addr, пример с выводом в порт:
#include <GyverDS18.h>
GyverDS18Single ds(2); // пин
void setup() {
Serial.begin(115200);
// readAddress вернёт uint64_t
// uint64_t addr = ds.readAddress();
// для вывода
gds::Addr addr = ds.readAddress();
// если корректный
if (addr) {
Serial.print("address: ");
addr.printTo(Serial);
} else {
Serial.println("error");
}
}
void loop() {
}Для обращения к датчикам по адресам используется класс GyverDS18:
#include <GyverDS18.h>
GyverDS18 ds(2); // пин
uint64_t addr = 0xCF0417505B78FF28;
void setup() {
Serial.begin(115200);
// первый запрос на измерение. Запрос ВСЕМ датчикам на линии
ds.requestTemp();
}
void loop() {
if (ds.ready()) { // измерения готовы по таймеру
// читаем КОНКРЕТНЫЙ датчик по адресу
if (ds.readTemp(addr)) { // если чтение успешно
Serial.print("temp: ");
Serial.println(ds.getTemp());
} else {
Serial.println("error");
}
ds.requestTemp(); // запрос следующего измерения ДЛЯ ВСЕХ
}
}Примечания:
- Можно запросить и получить температуру с любого конкретного датчика по его адресу, ровно как и остальные команды
- Запрос измерения и установку разрешения можно отправлять всем датчикам сразу, не указывая адрес
- В
GyverDS18используется один общий температурный буфер.readTemp(addr)при успешном чтении записывает в него температуру указанного датчика, аgetTemp()возвращает последнее успешно записанное значение. Поэтому послеreadTemp(addr) == falseнельзя считатьgetTemp()результатом этого адреса: в буфере может оставаться предыдущее значение, в том числе от другого датчика - Для адресного чтения значение 85 градусов всегда считается ошибкой:
readTemp(addr)вернётfalse, общий буфер не изменится. Это отличается от поведенияGyverDS18Single initialized()относится к общему буферу класса, а не к конкретному адресуready()работает по общему таймеру.setResolution(res)без адреса меняет разрешение у всех датчиков и настраивает этот таймер;setResolution(res, addr)меняет только датчик и таймер не перенастраивает. Если разрешения на линии разные, установитеsetPeriod()не меньше времени самого медленного датчика. Для этого можно использоватьDS18_TCONV_9,DS18_TCONV_10,DS18_TCONV_11,DS18_TCONV_12tick()у этого класса не читает температуру: по таймеру он отправляет новый запрос всем датчикам.DS18_READYозначает, что запрос успешно отправлен,DS18_ERROR- что запрос отправить не удалось
Для этого класса тоже работает авторежим, но в этом случае он просто запрашивает температуру по таймеру, не читая её. Такой вариант удобен при внешнем питании датчиков:
void loop() {
// это таймер с периодом к setResolution, он сам делает request
if (!ds.tick()) {
if (ds.readTemp(0x4D0417508099FF28)) {
Serial.print("temp 1: ");
Serial.println(ds.getTemp());
} else {
Serial.println("error 1");
}
if (ds.readTemp(0xC3041750E553FF28)) {
Serial.print("temp 2: ");
Serial.println(ds.getTemp());
} else {
Serial.println("error 2");
}
}
}Note
При паразитном питании после requestTemp() линия должна оставаться на активной подтяжке до завершения конвертации. Поэтому для нескольких датчиков в паразитном режиме используйте ручную последовательность requestTemp() → ready()/waitReady() → чтение нужных адресов → следующий requestTemp().
Для более удобного взаимодействия с несколькими датчиками есть класс GyverDS18Array. Пример чтения "связки" датчиков в авто режиме сразу в массив температур:
#include <GyverDS18Array.h>
uint64_t addr[] = {
0x4D0417508099FF28,
0xFE04175159CDFF28,
0xC3041750E553FF28,
0xCF0417505B78FF28,
};
GyverDS18Array ds(2, addr, 4, false); // внешнее питание
void setup() {
Serial.begin(115200);
ds.setResolution(12); // для всех
}
void loop() {
// это таймер с периодом к setResolution, он сам делает request
if (!ds.tick()) {
float temps[4];
ds.readTemps(temps);
for (float t : temps) {
Serial.println(t);
}
Serial.println();
}
}readTemps(arr) читает все адреса по очереди. Для каждого неуспешного чтения (в том числе при значении 85 градусов) в соответствующий элемент записывается NAN. Функция вернёт false, если ошибка была хотя бы у одного датчика.
Как и в GyverDS18, readTemp(index) использует общий внутренний буфер: после успешного чтения getTemp() относится к последнему прочитанному индексу, а после ошибки в буфере остаётся предыдущее успешное значение. Поэтому для пакетного чтения предпочтительнее readTemps() - он сам раскладывает значения и NAN по индексам.
Для паразитного питания используйте ручную последовательность запроса/ожидания/чтения, как в примере ниже.
Или в "ручном" режиме:
#include <GyverDS18Array.h>
uint64_t addr[] = {
0xD20417515A42FF28,
0x4D0417508099FF28,
0xFE04175159CDFF28,
0xCF0417505B78FF28,
};
GyverDS18Array ds(2, addr, 4); // пин, массив, длина
void setup() {
Serial.begin(115200);
// Запрос ВСЕМ датчикам на линии
ds.requestTemp();
}
void loop() {
if (ds.ready()) { // измерения готовы по таймеру
// проходим по массиву
for (int i = 0; i < ds.amount(); i++) {
Serial.print("#");
Serial.print(i);
Serial.print(": ");
// читаем по индексу
if (ds.readTemp(i)) Serial.print(ds.getTemp());
else Serial.print("error");
Serial.print(", ");
}
Serial.println();
ds.requestTemp(); // запрос следующего измерения ДЛЯ ВСЕХ
}
}Также в библиотеке есть сканер шины, который позволяет определить адреса всех подключенных датчиков. Он имеет три режима работы:
#define DS_PIN 2
void setup() {
Serial.begin(115200);
// 1. поиск в массив
{
uint64_t addr[5] = {};
uint8_t res = gds::Search(DS_PIN).scan(addr, 5);
Serial.println(res); // кол-во найденных
// выведем в порт
for (uint64_t& a : addr) {
gds::Addr(a).printTo(Serial);
}
}
// 2. цикличный поиск
{
gds::Search s(DS_PIN);
while (s.search()) {
s.printTo(Serial);
// s.addr; // это uint64_t
}
}
// 3. поиск по индексу
{
gds::Search(DS_PIN).find(0).printTo(Serial); // печать [индекс 0]
gds::Search s(DS_PIN);
s.find(1).printTo(Serial); // печать [индекс 1]
uint64_t adr = s.find(2); // как uint64_t [индекс 2]
}
}
void loop() {
}Возможные сценарии:
- Заполнить массив для GyverDS18Array
- Обращаться через GyverDS18 по индексу (напр.
ds.readTemp(gds::Search(DS_PIN).find(0)))
Примечание: поиск по индексу будет выдавать каждый раз тот же адрес среди одного набора адресов, другими словами - при одних и тех же подключенных датчиках индексация будет сохраняться. Но если отключить один датчик - индексация сместится, т.к. по сути привязана к адресам и их сортировке. Если поменять один датчик в связке - могут измениться все индексы. Поэтому лучше пользоваться абсолютной адресацией.
Байты th и tl в памяти датчика можно использовать в своих сценариях, например хранить калибровку. Важный момент: их запись пересекается с установкой разрешения. setResolution(res) ДЛЯ ВСЕХ в GyverDS18 и GyverDS18Array записывает th = 0 и tl = 0, поэтому эти данные сбрасываются. GyverDS18Single::setResolution() и адресный setResolution(res, addr/index) сначала читают текущие th/tl и сохраняют их.
Для записи используется функция writeRAM(). Сценарий записи своих данных и желаемого разрешения:
// запись данных в RAM (оперативную память)
// здесь мы пишем два своих байта данных
ds.writeRAM(0xAB, 0xCD);
// можно сохранить данные в EEPROM датчика при помощи отдельной функции
ds.copyRAM();
// и прочитать из EEPROM обратно в RAM
// при запуске датчика они читаются автоматически!
ds.recallRAM();Чтение данных из памяти производится в переменную типа gds::RAM, в этом буфере будет температура, данные и разрешение. За одно обращение к датчику можно вытащить все нужные данные, особенно полезно если там хранится калибровка - можно сразу её применить:
gds::RAM ram;
ds.readRAM(&ram);
Serial.println(ram.th, HEX);
Serial.println(ram.tl, HEX);
Serial.println(ram.getTemp() / 16.0);
Serial.println(ram.getRes());- v1.0
- v1.0.3 - поправлены тайминги для китайских датчиков
- v1.1.0 - оптимизация работы с uint64, добавлены фичи
- v1.2.0
- Для работы требуется библиотека GyverIO
- Библиотеку можно найти по названию GyverDS18 и установить через менеджер библиотек в:
- Arduino IDE
- Arduino IDE v2
- PlatformIO
- Скачать библиотеку .zip архивом для ручной установки:
- Распаковать и положить в C:\Program Files (x86)\Arduino\libraries (Windows x64)
- Распаковать и положить в C:\Program Files\Arduino\libraries (Windows x32)
- Распаковать и положить в Документы/Arduino/libraries/
- (Arduino IDE) автоматическая установка из .zip: Скетч/Подключить библиотеку/Добавить .ZIP библиотеку… и указать скачанный архив
- Читай более подробную инструкцию по установке библиотек здесь
- Рекомендую всегда обновлять библиотеку: в новых версиях исправляются ошибки и баги, а также проводится оптимизация и добавляются новые фичи
- Через менеджер библиотек IDE: найти библиотеку как при установке и нажать "Обновить"
- Вручную: удалить папку со старой версией, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!
При нахождении багов создавайте Issue, а лучше сразу пишите на почту alex@alexgyver.ru
Библиотека открыта для доработки и ваших Pull Request'ов!
При сообщении о багах или некорректной работе библиотеки нужно обязательно указывать:
- Версия библиотеки
- Какой используется МК
- Версия SDK (для ESP)
- Версия Arduino IDE
- Корректно ли работают ли встроенные примеры, в которых используются функции и конструкции, приводящие к багу в вашем коде
- Какой код загружался, какая работа от него ожидалась и как он работает в реальности
- В идеале приложить минимальный код, в котором наблюдается баг. Не полотно из тысячи строк, а минимальный код