Главная chevron_right Уроки chevron_right Связь chevron_right Протокол 1-Wire: работа с однопроводными устройствами на Arduino

Протокол 1-Wire: работа с однопроводными устройствами на Arduino

Разбираем протокол 1-Wire: подключение, питание, адресация и чтение данных с датчиков DS18B20 и других однопроводных устройств.

Статья обновлена 28.09.2022 автором Hannes Siebeneicher.

Термины «controller» и «peripheral» заменяют устаревшие «master» и «slave». Arduino официально отказалась от старой терминологии: устройство, которое управляет шиной, называется контроллером, а подключённые к нему устройства — периферийными.

1-Wire — это протокол связи, в котором контроллер и периферийные устройства обмениваются данными всего по одному проводу. В этой статье разберём основы работы с 1-Wire на Arduino с помощью библиотеки OneWire. Рассмотрим сам протокол, интерфейс, способы питания, адресацию и чтение устройств, а в конце — немного истории библиотеки.

Последняя версия библиотеки

Актуальная версия latest version of the library размещена на сайте Paul Stoffregen's.

Сейчас библиотеку поддерживает Paul Stoffregen. Если вы нашли ошибку или хотите предложить улучшение, пишите на paul@pjrc.com. Перед этим убедитесь, что используете последнюю версию OneWire.

Bus — это подкласс библиотеки OneWire. Класс Bus сканирует шину 1-Wire, подключённую к аналоговому пину, и сохраняет ROMs в массив. В классе Bus доступны методы для получения данных от различных датчиков на шине 1-Wire (DS18B20, DS2438).

Протокол 1-Wire

Dallas Semiconductor (теперь Maxim) выпускает целое семейство устройств, управляемых через фирменный протокол 1-Wire. Использование драйверов Dallas 1-Wire (торговая марка) бесплатно для разработчиков.

В сети 1-Wire, которую Dallas назвала «MicroLan» (торговая марка), один контроллер общается с одним или несколькими периферийными устройствами по единственной линии данных. Эта же линия может питать периферийные устройства — такой режим называется _parasitic power_ (паразитное питание). Подробнее об этом рассказывает руководство Tom Boyd guide to 1-Wire — возможно, там найдётся ответ на ваш вопрос.

Температурные датчики с интерфейсом 1-Wire стали особенно популярными: они дёшевы, просты в использовании и сразу выдают откалиброванное цифровое значение температуры. Ещё один плюс — устойчивость к длинным проводам между датчиком и Arduino. Пример кода ниже демонстрирует работу с 1-Wire устройствами через библиотеку OneWire Джима Штудта, на примере цифрового термометра DS18S20. Многие микросхемы 1-Wire поддерживают оба режима работы parasitic and normal power modes.

Интерфейсы 1-Wire

Выделенный контроллер шины

Dallas/Maxim и ряд других компаний выпускают специализированные контроллеры для чтения, записи и управления сетями 1-Wire. Большинство из них перечислены здесь:

http://owfs.org/index.php?page=bus-masters

Эти устройства специально спроектированы и оптимизированы для эффективной работы с 1-Wire сетями. Подобно UART/USART-контроллерам, они обрабатывают тактируемые операции аппаратно, используя буфер. Это снимает нагрузку с основного процессора (например, микроконтроллера или шлюза датчиков) и повышает точность. Внешние подтягивающие резисторы зачастую не нужны.

Многие из таких микросхем умеют обрабатывать ошибки, связанные с потерей целостности сигнала, изменением уровней, отражениями и другими проблемами шины — особенно актуальными в больших сетях. Устройства предлагаются в широком ценовом диапазоне от $1 до $30 и имеют разнообразные дополнительные функции.

Ещё одно важное преимущество — поддержка файловой системы с широким набором устройств для контроллеров 1-Wire, открывающей доступ ко многим нативным функциям для различных типов устройств 1-Wire.

UART/USART-контроллеры

Большинство UART/USARTs легко обеспечивают скорость, значительно превышающую 15,4 кбит/с, необходимые для шины 1-Wire в стандартном режиме. Важнее то, что тактирование и буферизация выполняются отдельно, снова разгружая основной процессор. Подробности этой реализации описаны здесь: http://www.maximintegrated.com/en/app-notes/index.mvp/id/214.

Программная эмуляция (bitbanging)

Если аппаратная буферизация недоступна, 1-Wire можно реализовать на обычном GPIO-пине, вручную переключая его состояние для эмуляции UART/USART. Такой подход значительно менее эффективен по загрузке процессора и напрямую влияет на другие процессы, делящие ресурсы с ним.

На Arduino и совместимых платах для этого служит библиотека OneWire.

На одноплатных компьютерах, например Raspberry Pi, чтение сети 1-Wire часто возможно через драйверы ядра с нативной поддержкой. Модули ядра w1-gpio, w1-gpio-therm и w1-gpio-custom входят в актуальные дистрибутивы Raspbian и весьма популярны — они позволяют работать с частью устройств 1-Wire без дополнительного оборудования. Однако поддержка устройств и размер шины в этом случае ограничены программно.

Питание устройств OneWire

Микросхему можно питать двумя способами. Первый — «паразитный» режим: к чипу идут всего два провода. Второй — с отдельным проводом питания; в ряде случаев он обеспечивает более стабильную работу (хотя паразитный режим тоже работает хорошо). Если датчик находится в пределах 6 метров от Arduino, паразитного питания вполне достаточно для начала работы. Приведённый ниже код работает в обоих режимах.

Паразитный режим питания

В паразитном режиме нужны только два провода: линия данных и земля. Согласно даташиту, линия питания должна быть подключена к земле. На стороне контроллера к шине 1-wire необходимо подключить подтягивающий резистор 4,7 кОм. Когда линия находится в состоянии «высокого» уровня, устройство заряжает внутренний конденсатор.

Обычно потребляемый ток невелик, но при конвертации температуры или записи в EEPROM он может достигать 1,5 mA. Во время таких операций контроллер шины обязан удерживать линию в высоком состоянии для питания устройства. Для конвертации температуры DS18S20 требуется задержка 750 мс. В это время контроллер не может выполнять другие команды или опрашивать устройства. Библиотека OneWire поддерживает удержание шины в высоком состоянии после записи данных.

Обычный режим (внешнее питание)

При внешнем питании нужны три провода: линия данных, земля и питание. Подтягивающий резистор 4,7 кОм по-прежнему обязателен на линии данных. Поскольку шина свободна для передачи данных, микроконтроллер может непрерывно опрашивать состояние устройства во время конвертации и получить результат сразу по её завершении — без фиксированной задержки.

Замечание о резисторах

Для больших сетей можно попробовать резисторы меньшего номинала. Даташит ATmega328/168 рекомендует начинать с 1,6 кОм, и многие пользователи подтверждают, что меньшие значения лучше работают в разветвлённых сетях.

Адресация устройств 1-Wire

Каждое устройство 1-Wire имеет уникальный 64-битный ROM-адрес: 8-битный код семейства, 48-битный серийный номер и 8-битный CRC. CRC используется для проверки целостности данных. Например, пример кода ниже проверяет, является ли устройство датчиком температуры DS18S20, сверяя код семейства с 0x10. Для более нового датчика DS18B20 нужно проверять код 0x28, а для DS1822 — 0x22.

Команды для одного устройства

Прежде чем отправить команду конкретному периферийному устройству, контроллер должен выбрать его по уникальному ROM-адресу. Все последующие команды будет обрабатывать только это устройство.

Команды для нескольких устройств

Команду можно адресовать сразу всем периферийным устройствам, отправив команду «Skip ROM» (0xCC). Важно понимать последствия такой широковещательной рассылки. Иногда это удобно: например, Skip ROM с последующей командой Convert T (0x44) заставит все устройства на шине одновременно начать конвертацию температуры — быстро и эффективно. С другой стороны, команда Read Scratchpad (0xBE) заставит все устройства одновременно выдать данные, что приведёт к коллизии. Также важно учитывать суммарное энергопотребление всех устройств при использовании Skip ROM.

Чтение устройств 1-Wire

Чтение устройства 1-Wire выполняется в несколько шагов, которые зависят от типа устройства. Популярный DS18B20, например, измеряет температуру, а DS2438 — напряжение, ток и температуру.

Два основных этапа чтения

Конвертация Контроллер отправляет устройству команду запуска внутренней конвертации. Для DS18B20 это команда Convert T (байт 0x44). В библиотеке OneWire она выглядит как ds.write(0x44), где ds — экземпляр класса OneWire. После этого устройство считывает внутренний ADC и по завершении копирует результат в регистры Scratchpad. Время конвертации зависит от разрешения и указано в даташите. DS18B20 тратит от 94 мс (9-битное разрешение) до 750 мс (12-битное). В процессе конвертации устройство можно опрашивать командой ds.read() в OneWire, чтобы узнать о завершении.

Чтение Scratchpad После завершения конвертации данные копируются в память Scratchpad, откуда их можно прочитать. Scratchpad можно читать в любой момент без предварительной команды конвертации — в нём хранится результат последнего измерения, а также настройки разрешения и другие параметры устройства.

Асинхронное и синхронное чтение/запись

Большинство существующего кода для устройств 1-Wire, особенно написанного для Arduino, использует простой алгоритм «Конвертировать — Подождать — Прочитать». Он создаёт ряд проблем.

Влияние на тайминги других функций Главная проблема: если в коде есть фиксированная задержка ожидания, программа полностью зависает на это время. Это неприемлемо, если в системе есть другие процессы — обработка ввода, вычисления, управление периферией. Конвертация 12-битного значения DS18B20 может занимать до 750 мс. Гораздо эффективнее запустить конвертацию и вернуться за результатом позже, когда он будет готов.

Масштабирование при работе с несколькими устройствами Метод «Конвертировать — Подождать — Прочитать» плохо масштабируется. Все команды конвертации можно запустить последовательно (или одновременно через Skip ROM), а затем прочитать результаты по очереди. Подробнее об этом: http://interfaceinnovations.org/onewireoptimization.html.

Подбор времени ожидания под реальное время конвертации Оптимальный подход — учитывать реальное время конвертации для текущего разрешения. В примере ниже используется 1000 мс, хотя даташит указывает максимум 750 мс, а типичное время — 625 мс и меньше. Для 9-битного разрешения конвертация занимает не более 94 мс, и ждать 1000 мс не имеет смысла. Самый эффективный способ — опрашивать устройство через временной слот чтения: так можно узнать точный момент готовности данных и сразу их забрать.

История библиотеки

В 2007 году Jim Studt создал оригинальную библиотеку OneWire, упрощающую работу с устройствами 1-Wire. Первая версия работала только с arduino-007 и требовала большой таблицы подстановки (256 байт) для вычисления CRC. Позже библиотека была updated для совместимости с arduino-0008 и более поздними версиями. В последней версии таблица CRC устранена; библиотека протестирована под arduino-0010.

В библиотеке OneWire была ошибка, вызывавшая бесконечный цикл при использовании функции поиска. Версия Version 2.0 включает улучшенную функцию поиска от Robin James и оптимизированные процедуры ввода/вывода от Paul Stoffregen (исправляют редкие ошибки связи), а также ряд мелких оптимизаций.

Версия 2.1 добавила совместимость с Arduino 1.0-beta, улучшенный пример работы с температурой (Paul Stoffregen), пример DS250x PROM (Guillermo Lovato), совместимость с chipKit (Jason Dangel), CRC16, вспомогательные функции и пример DS2408 (Glenn Trewitt).

Майлс Бёртон разработал свою библиотеку Dallas Temperature Control Library на её основе.

Пример кода

#include <OneWire.h>


// Ввод/вывод датчика температуры DS18S20

OneWire ds(10);  // на пине 10


void setup(void) {

  // инициализация входов/выходов

  // запуск последовательного порта

  Serial.begin(9600);

}


void loop(void) {

  byte i;

  byte present = 0;

  byte data[12];

  byte addr[8];


  ds.reset_search();

  if ( !ds.search(addr)) {

    Serial.print("No more addresses.\n");

    ds.reset_search();

    return;

  }


  Serial.print("R=");

  for( i = 0; i < 8; i++) {

    Serial.print(addr[i], HEX);

    Serial.print(" ");

  }


  if ( OneWire::crc8( addr, 7) != addr[7]) {

      Serial.print("CRC is not valid!\n");

      return;

  }


  if ( addr[0] == 0x10) {

    Serial.print("Device is a DS18S20 family device.\n");

  }

  else if ( addr[0] == 0x28) {

    Serial.print("Device is a DS18B20 family device.\n");

  }

  else {

    Serial.print("Device family is not recognized: 0x");

    Serial.println(addr[0],HEX);

    return;

  }


  ds.reset();

  ds.select(addr);

  ds.write(0x44,1); // запуск преобразования, с паразитным питанием в конце


  delay(1000);  // 750ms может быть достаточно, а может и нет

  // можно вызвать ds.depower(), но сброс сам об этом позаботится


  present = ds.reset();

  ds.select(addr);

  ds.write(0xBE); // чтение Scratchpad


  Serial.print("P=");

  Serial.print(present,HEX);

  Serial.print(" ");

  for ( i = 0; i < 9; i++) {  // нам нужно 9 байт

    data[i] = ds.read();

    Serial.print(data[i], HEX);

    Serial.print(" ");

  }

  Serial.print(" CRC=");

  Serial.print( OneWire::crc8( data, 8), HEX);

  Serial.println();

}

Перевод HEX в температуру

Чтобы преобразовать HEX-данные в значение температуры, нужно определить, какой датчик используется: DS18S20 или DS18B20. Код чтения немного отличается: DS18B20 (и DS1822) возвращает 12-битное значение (точность 0,0625°C), тогда как DS18S20 и DS1820 возвращают 9-битные значения (точность 0,5°C).

Сначала объявите переменные (разместите их прямо перед loop()):

int HighByte, LowByte, TReading, SignBit, Tc_100, Whole, Fract;

Для датчиков серии DS18B20 добавьте следующий код после строки Serial.println();:

LowByte = data[0];

  HighByte = data[1];

  TReading = (HighByte << 8) + LowByte;

  SignBit = TReading & 0x8000;  // проверка старшего бита

  if (SignBit) // отрицательное значение

  {

    TReading = (TReading ^ 0xffff) + 1; // дополнение до 2

  }

  Tc_100 = (6 * TReading) + TReading / 4; // умножить на (100 * 0.0625) или 6.25


  Whole = Tc_100 / 100; // выделить целую и дробную части

  Fract = Tc_100 % 100;


  if (SignBit) // если значение отрицательное

  {

    Serial.print("-");

  }

  Serial.print(Whole);

  Serial.print(".");

  if (Fract < 10)

  {

    Serial.print("0");

  }

  Serial.print(Fract);


  Serial.print("\n");

Этот блок кода конвертирует температуру в градусы Цельсия и выводит результат в Serial.

Пример кода для DS1820 с разрешением 0,5°C

Пример выше рассчитан только на B-версию DS1820. Ниже приведён код, совместимый с датчиком DS1820 с меньшим разрешением, а также с несколькими датчиками одновременно — с выводом значений на LCD. В примере используется пин 9 Arduino; при необходимости замените его на подходящий. Пины 1 и 3 датчика DS1820 должны быть подключены к земле. В примере резистор 5 кОм подключён от пина 2 датчика DS1820 к Vcc (+5 В). Подключение LCD к Arduino описано в документации LiquidCrystal.

#include <OneWire.h>

#include <LiquidCrystal.h>

// LCD=======================================================

//инициализация библиотеки с номерами пинов интерфейса

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

#define LCD_WIDTH 20

#define LCD_HEIGHT 2


/* Ввод/вывод датчика температуры DS18S20 */


OneWire ds(9);  // на пине 9

#define MAX_DS1820_SENSORS 2

byte addr[MAX_DS1820_SENSORS][8];

void setup(void)

{

  lcd.begin(LCD_WIDTH, LCD_HEIGHT,1);

  lcd.setCursor(0,0);

  lcd.print("DS1820 Test");

  if (!ds.search(addr[0]))

  {

    lcd.setCursor(0,0);

    lcd.print("No more addresses.");

    ds.reset_search();

    delay(250);

    return;

  }

  if ( !ds.search(addr[1]))

  {

    lcd.setCursor(0,0);

    lcd.print("No more addresses.");

    ds.reset_search();

    delay(250);

    return;

  }

}

int HighByte, LowByte, TReading, SignBit, Tc_100, Whole, Fract;

char buf[20];


void loop(void)

{

  byte i, sensor;

  byte present = 0;

  byte data[12];


  for (sensor=0;sensor<MAX_DS1820_SENSORS;sensor++)

  {

    if ( OneWire::crc8( addr[sensor], 7) != addr[sensor][7])

    {

      lcd.setCursor(0,0);

      lcd.print("CRC is not valid");

      return;

    }


    if ( addr[sensor][0] != 0x10)

    {

      lcd.setCursor(0,0);

      lcd.print("Device is not a DS18S20 family device.");

      return;

    }


    ds.reset();

    ds.select(addr[sensor]);

    ds.write(0x44,1); // запуск преобразования, с паразитным питанием в конце


    delay(1000);  // 750ms может быть достаточно, а может и нет

    // можно вызвать ds.depower(), но сброс сам об этом позаботится


    present = ds.reset();

    ds.select(addr[sensor]);

    ds.write(0xBE); // чтение Scratchpad


    for ( i = 0; i < 9; i++)

    { // нам нужно 9 байт

      data[i] = ds.read();

    }


    LowByte = data[0];

    HighByte = data[1];

    TReading = (HighByte << 8) + LowByte;

    SignBit = TReading & 0x8000;  // проверка старшего бита

    if (SignBit) // отрицательное значение

    {

      TReading = (TReading ^ 0xffff) + 1; // дополнение до 2

    }

    Tc_100 = (TReading*100/2);


    Whole = Tc_100 / 100; // выделить целую и дробную части

    Fract = Tc_100 % 100;


    sprintf(buf, "%d:%c%d.%d\337C     ",sensor,SignBit ? '-' : '+', Whole, Fract < 10 ? 0 : Fract);


    lcd.setCursor(0,sensor%LCD_HEIGHT);

    lcd.print(buf);

  }

}

Практические замечания

  • Длина линии: паразитный режим хорошо работает на расстояниях до 6–7 метров. На больших расстояниях лучше использовать внешнее питание и экранированный кабель.
  • Несколько датчиков на одной шине: 1-Wire позволяет подключить десятки устройств к одному пину. Убедитесь, что подтягивающий резистор подобран правильно — при большом числе устройств может потребоваться уменьшить его номинал.
  • Паразитный режим и конвертация: в паразитном режиме во время конвертации шина должна быть удержана в высоком состоянии. Если этого не сделать, датчик может вернуть некорректное значение (обычно 85°C — это сигнал об ошибке питания).
  • Значение 85°C: если датчик постоянно возвращает 85°C, это почти всегда означает проблему с питанием или удержанием шины, а не реальную температуру.
  • CRC-проверка: всегда проверяйте CRC при чтении данных, особенно если линия длинная или проходит в зашумлённой среде.