Термодатчики DS18x20, продолжение…
»1 л.с. — это сила, которую развивает 1 лошадь диаметром 1 м и массой 1 кг, в вакууме».
Не первый раз встречается, что люди, видя какую-то техническую задачу, пытаются решить ее сначала с использованием каких-нибудь эмуляторов, причем за основу берутся допущения и упрощения. В итоге приходят к закономерному выводу — «так оно работать не будет! Программа посчитала…»
Если бы мы жили в мире сферических лошадей диаметром 1 м — наверное да, реальность всегда и точно соответствовала бы теоретическим предположениям.
Но, к счастью или к сожалению, оно немножко не так.
И вот пример, снова наши термодатчики (+ немного занудства):
В предыдущей статье написал про работающую у меня («у меня всё работает!» © (тм)) схемку подключения:
пояснять, как работает транзистор, надо?
Транзистор, если не вникать в детали, работает очень просто: когда вон на том выводе, посередине слева, затворе,»+» относительно нижнего вывода — он ток проводит, когда »0» или »-» — не проводит. Такая у него базовая фича.
Когда на ножке GPIO логический 0 (0В) — получается, что напряжение на затворе = 3.3В, транзистор открыт, и линия 1-wire закорочена на 0, через транзистор и ножку процессора.
Когда на ножке логическая 1 (3.3В или около того) — получается что напряжение на затворе около 0В, он закрыт, считаем что его вообще нет, и на шине тогда 5В со стабилитрона через резистор R2.
Когда ножка в режиме чтения — напряжение на ней само поднимется до 3.3, логической 1, потому что если оно меньше — транзистор откроется и добавит, а если дошло до 3.3 — закроется. На этой точке оно и будет балансировать.
Если при этом датчик притянет линию к 0 — напряжению будет неоткуда браться, оно упадет до 0.
Поэтому на линии импульсы будут 5В, а на ножке GPIO — не выше 3.3. Всё просто.
и еще одну, тоже реально работающую штуку:
Эти схемы — как основа для реального применения, точка опоры, от которой можно отталкиваться. А вот суровая реальность:
Куплена партия датчиков, DS18B20, Китай. Подключение точно по первой схеме, в частности обращу внимание на то что ножки Vcc и Gnd самих датчиков закорочены.
Всё работает идеально.
Если не закоротить — начинаются помехи в линии, отваливаются отдельные датчики.
Куплена партия датчиков, DS18B20, Китай. Подключение точно по первой схеме — глухо, ничего не работает. Берем осциллограф в руки — просадка на линии. Просаживается она как раз при подключении ножки Vcc к Gnd, причем сразу.
Отключаем ножку Vcc — всё работает.
Еще одна линия на датчиках «валялись в коробке», т.е. были куплены непонятно когда, где, с какой партией, вперемешку.
Одни из них начинают работать, когда ножку закоротишь, другие — когда не закоротишь, третьи требуют конденсатора как на второй схеме, иначе не обнаруживаются если провод длиннее метра.
Причем с конденсаторами все работают идеально, а без них — одни да, другие нет.
То есть, при одном и том же названии микросхемы, поведение разных датчиков существенно отличается одно от другого, наверняка и внутренние схемы разные, а значит и токи потребления.
Ну и как это в принципе можно однозначно рассчитать, чтобы с уверенностью потом говорить «будет так!» ?
Да никак. Можно только оценочно прикидывать, и проверять на практике.
Но и для оценочных прикидок надо для начала разобраться, как оно работает в конкретном случае.
Например, допустим, что 1 датчик в момент измерения температуры потребляет 10 мА тока в течении 100 мс (все цифры оценочные, с потолка, т.к. реальных мы не знаем, датчики разные).
Вопрос, какой ток будут потреблять 4 датчика?
Думаете, арифметика вам поможет: 10 мА * 4 = 40 мА?
А кто сказал что они будут потреблять ток одновременно? Или сразу один за другим без пауз?
Это такое допущение, которое на самом деле еще больше с потолка, чем ток потребления неизвестной схемы. В реальности оно будет работать так, как задано процессом опроса датчиков.
И этим мы можем управлять. Например, часто в инструкциях к Адруино приводятся примеры с использованием библиотеки DallasTemperature:
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
void setup(void)
{
....
sensors.begin();
}
void loop(void)
{
....
sensors.requestTemperatures(); // Send the command to get temperatures
float tempC = sensors.getTempCByIndex(0);
....
}
Действительно, в этом случае в шину 1-wire отправляется команда запуска измерений (0×44) сразу для всех датчиков, после чего запрашивается результат с датчика, который обнаружился и попал в список на этапе инициализации, функция sensors.begin ()
Какие тут минусы?
— измерение производится сразу всеми датчиками (уточню: — получившими команду, а это не обязательно все на линии!)
— в момент измерения датчики потребляют больше, чем во всё оставшееся время. Именно тот случай когда несколько датчиков способны разрядить всю линию.
— этот процесс занимает некоторое время, ну пусть всё-таки 100 мс. А потом идет запрос к конкретному датчику, чтобы он передал температуру — это еще время. То есть, на этом участке исполнения программы МК задерживается, ждет пока там отработают датчики.
— мы не можем сказать какой именно из »287d9244060000c7»,»285bf044d4e13cab»,»28f4b844d4e13c04» будет в списке первый, какой второй и т.д., нужно работать с конкретными адресами, а для этого надо их сначала узнать.
— главный минус: всё это работает с теми датчиками, которые определились сразу, в первый раз. То есть если какой-то не определился, или добавили-удалили потом — мы этого уже не узнаем, до перезагрузки.
Этого всего можно избежать, если снимать показания немного по-другому.
Во-первых, можно измерять температуру не всеми датчиками одновременно, а по очереди. Для этого надо выбирать конкретный датчик, и отправлять команду ему.
Во-вторых, можно не ждать пока он наизмеряет — отправили команду, вернулись с вопросами через некоторое время.
Для этого нужно иметь список устройств, а вот обнаруживать устройства можно в процессе заново, т.е. появляется возможность добавлять-удалять датчики на лету, не перезагружая контроллер и не меняя прошивку.
И наконец, можно сделать так, чтобы обработка результатов измерения происходила тогда, когда они получены, и если получены.
За основу взята библиотека OneWire, которая содержит низкоуровневые команды управления 1-wire, поверх нее был написан класс:
....
#define DS_INIT 0
#define DS_SEARCH 1
#define DS_CONVERT 2
#define DS_RESULT 3
#define DS_REPORT 4
....
class owds : public OneWire {
int ds_count; // device count
unsigned long ds_timer; // loop timer
unsigned long ds_period; // loop period
int ds_mode; // current mode
int ds_sel; // selected device
owds_Addr ds_list[DS_COUNT];
owds_Name names[DS_COUNT];
int fill_mac(char * mac, int i);
public:
owds(int pin); // init
void loop(); // main procedure at system loop
const char * find_name(const char * mac); // search name for MAC
void set_name(const char * mac, const char * name); // set name for MAC
inline void clear_names() { // clear names
memset(names,0,sizeof(names));
}
inline String get_names(){ // get all names
String ret = "{";
for(int i=0; i< DS_COUNT; i++){
if(strcmp(names[i].name,"") != 0){
if(i > 0) ret += ",";
ret += "\"";
ret += names[i].mac;
ret += "\":\"";
ret += names[i].name;
ret += "\"";
}
}
ret += "}";
return ret;
}
void save_config(File & file); // save names to config file (JSON)
int load_config(File & file); // read names from config file (JSON)
virtual void report(const char * str) {}; // when reporting for all devices (JSON)
virtual void processing(const char * id, float t) {}; // when processing one device (MAC, TEMP)
virtual void debug(const char * str) {}; // hook for debug
};
.....
Принцип заключается в том, что выделены 5 режимов работы:
1 — инициализация переменных, «начинаем с начала»
2 — поиск устройств на шине
3 — для каждого устройства запрашивается измерение
4 — для каждого устройства выводятся данные
5 — отправляется итоговый отчет
И всё повторяется через некоторое время.
Переход между режимами полностью автоматический, происходит последовательно при каждой итерации.
Для вызова пользовательских функций использованы виртуальные методы класса, т.е. их можно использовать или не использовать.
В программе достаточно просто подключить библиотеку и создать свой класс, примерно так:
#define PIN_OW 5
// OneWire settings =======================================
#include
// Create own class based on owds, define user functions and play
class myowds : public owds {
public:
myowds(int pin) : owds(pin){};
// report all found devices at once, JSON string
void report(const char * str){
Serial.println("report:");
Serial.println(str);
}
// report for each found device
void processing(const char * mac, float t){
Serial.println("processing:");
Serial.print(mac);
Serial.print(" = ");
Serial.println(t);
}
// just debug message
void debug(const char * str){
Serial.print("debug: ");
Serial.println(str);
}
};
myowds oneWire(PIN_OW);
void setup() {
Serial.begin(19200);
}
void loop() {
oneWire.loop();
delay(100);
}
Всё, теперь при удачном обнаружении датчиков будут вызываться соответствующие функции. Можно просто сбрасывать данные в MQTT, можно обрабатывать на месте, если заранее известен MAC датчика (manufacturer address code, он же ROM, он же ID) и его назначение.
Именно с ее помощью и реализован у меня контроль зон. Там действительно получается всё просто: что определилось — то будет работать, а если не работает — надо лезть с осциллографом и смотреть почему
Библиотеку полностью, если кому надо — закинул на GitHub.