Top.Mail.Ru

Подключение датчика температуры DS18B20 к STM32. Модуль KY-001.

Доброго времени суток, завершаем начатое, а именно подключение, настройку и получение данных с датчика температуры DS18B20, выполненном в виде модуля KY-001, который и представлен на изображении сверху. Все перечисленное осуществим на микроконтроллере STM32, а если быть более точным, то на STM32F401CC.

Теоретическая часть.

Для разогрева пробежимся по некоторым из основных характеристик датчика:

  • Напряжение питания: 3В - 5.5 В.
  • Возможна настройка разрешения результата измерения температуры: 9 - 12 бит. Само собой, больше разрешение ⇒ больше точность ⇒ больше время измерения. Все логично.
  • Диапазон измерения температуры: -55°C - +125 °C.
  • Существует возможность подключения нескольких датчиков к одной шине (как обычно и бывает на практике).
  • Для идентификации датчиков на шине каждый из них имеет свой собственный идентификатор, доступный для считывания.
  • При максимальной точности измерений (разрешение - 12 бит) максимальное время преобразования составляет 750 мс. Мягко говоря - немало.

И сразу же выкладываю даташиты, чтобы потом не забыть:

Подключение датчика температуры DS18B20 к STM32.

Из первой части помним, что для подключения к 1-wire можно обойтись и всего двумя линиями (одна - GND, вторая - питание + данные). Но у меня сейчас есть в наличии только один модуль KY-001 и, соответственно, нет особой необходимости в использовании паразитного питания, поэтому я задействую три линии для коммутации. Если есть интерес, либо необходимость в подключении нескольких датчиков, то пишите на форуме или в комментарии, соорудим пример и для этого случая. Благо раздобыть DS18B20 можно довольно просто. Итак, вариант схемы подключения модуля KY-001 выглядит так:

Подключения DS18B20 к STM32.

То есть, поскольку шина 1-Wire является однопроводной по определению, то мы замыкаем между собой Rx и Tx использующегося USART. Но USART STM32 дает нам еще один вариант, а именно конфигурацию в режиме Single-Wire (Half-Duplex), чем мы и воспользуемся. Итоговая схема для данного проекта такова:

Подключение датчика температуры DS18B20.

Резистор, подтягивающий линию к питанию крайне важен для функционирования шины. Если используете готовый модуль с установленным DS18B20, то с большой долей вероятности он уже распаян там. В случае его отсутствия нужно будет добавить, на всякий случай, вот распиновка "голого" датчика:

Распиновка DS18B20.

Бывают прецеденты, когда маркировка, нанесенная на плате модуля (в частности KY-001), не соответствует действительности, так что может быть не лишним прозвонить ножки датчика с выходным разъемом модуля. На всякий случай, для душевного спокойствия.

Основные аспекты работы датчика DS18B20.

С аппаратно-электрической частью разобрались, так что начинаем планомерно продвигаться к достижению поставленной цели. Организация памяти DS18B20 представлена на схеме:

Память.

Здесь мы имеем оперативную память (RAM), в которой хранятся последовательно 8 байт:

  • TEMPERATURE LSB и TEMPERATURE MSB

Соответственно младший и старший байты измеренного значения температуры. То есть именно тот результат, который мы и хотим получить с этого датчика.

  • TH и TL

При измерении температуры DS18B20 может автоматически производить сравнение результата со значениями, сохраненными в этих двух регистрах/байтах RAM. И в случае, если реальная температура выходит за пределы, заданные в TH (верхний порог) и TL (нижний порог), то датчик выставит свой внутренний флаг ALARM, сигнализирующий об этом событии. Управляющий контроллер может специальной командой опрашивать датчики на предмет возникновения такой ситуации. Таким образом, датчик берет на себя функции по первичной проверке измеренной температуры, облегчая жизнь внешнему контроллеру.

Поскольку эти регистры 8-ми битные, а результат измерения 9-12 битный, то младшие биты измеренного значения отбрасываются и в сравнении с порогами участия не принимают.

В случае, если этот функционал не требуется, то TH и TL могут быть использованы для хранения неких пользовательских данных. Двигаемся дальше, и на очереди конфигурационный регистр:

  • CONFIG

Информативными в нем являются только два бита - R1 и R0:

Конфигурационный регистр.

Они задают разрешение (9 - 12 бит). Здесь же я указал максимальное время преобразования для разных настроек. Больше разрешающая способность ⇒ больше точность, но за это приходится платить большим же временем выполнения каждого отдельного измерения.

Оставшиеся три байта зарезервированы, то есть по сути не используются.

Чтение этих данных из DS18B20 осуществляется при помощи команды Read Scratchpad, о чем мы поговорим чуть ниже. Пока же обратим внимание на то, что при выполнении данной команды помимо 8-ми байт будет считан еще и 9-ый, который представляет из себя контрольную сумму, расчет и проверку которой мы также осуществим при создании проекта 👍

Как обозначено на нашей схеме памяти, помимо оперативной (RAM) датчик оснащен еще и энергонезависимой EEPROM-памятью для хранения конфигурационной информации. А эта самая конфигурационная информация представляет из себя три уже рассмотренных нами регистра:

  • TH
  • TL
  • CONFIG

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

И это еще не все, каждый датчик оснащен ROM-памятью, доступной только для чтения, основной смысл которой в том, чтобы хранить уникальный идентификатор каждого девайса. Поскольку на шине может быть (и обычно так и бывает) как минимум несколько датчиков, то для их идентификации нужен какой-то механизм. И в качестве этого механизма как раз и выступает данный ID, который может быть считан из DS18B20 и в дальнейшем использован для получения данных с каждого из имеющихся датчиков в отдельности. ROM-память представляет из себя 64 бита (8 байт):

ROM-память.

Помимо 48-ми бит серийного номера/идентификатора имеем 8-ми битный FAMILY CODE (фиксированное значение 0x28), а также 8 бит контрольной суммы для проверки целостности данных. В результате и имеем 48 + 8 + 8 = 64 бита.

Идем дальше, рассмотрим процедуру пересчета полученных с DS18B20 данных в значение температуры в градусах Цельсия. Для этого есть наглядная схема:

Пересчет температуры в градусы Цельсия.

Сразу же - старшие 5 битов дают нам информацию о знаке значения. Либо они все нулевые и температура положительна, либо все 5 битов равны "1" - температура отрицательна. Каждый из оставшихся битов вносит определенный вклад в итоговое значение. По нашей отличной традиции для понимания будем рассматривать конкретные примеры:

0x0037 \rArr 0000 \medspace 0000 \medspace 0011 \medspace 0111 \rArr \enspace 0 \cdot 2^{6} + 0 \cdot 2^{5} + 0 \cdot 2^{4} + 0 \cdot 2^{3} + 0 \cdot 2^{2} + 1 \cdot 2^{1} + 1 \cdot 2^{0} + 0 \cdot 2^{-1} + 1 \cdot 2^{-2} + 1 \cdot 2^{-3} + 1 \cdot 2^{-4} \rArr \enspace 3.4375 \degree C

По итогу, нетрудно заметить, что для перевода достаточно осуществить лишь следующие манипуляции:

T \degree C = register\_value \cdot \medspace 0.0625

Для этих же значений из примера:

0x0037 = 55 \rArr T \degree C = 55 \cdot 0.0625 = 3.4375 \degree C

Если для датчика задано разрешение, к примеру - 9 бит, то незначащие биты (0-й, 1-й, 2-й) будут равны нулю. Ко всему этому мы вернемся при создании проекта, сейчас же переходим к командам, поддерживаемым датчиком.

Команды для работы с DS18B20.

Поскольку у меня на руках только один датчик (в составе модуля KY-001) на текущий момент, то и акцент будет смещен в сторону работы с одним датчиком. В случае необходимости, пишите, организуем по мере возможности работу с несколькими. Итак, команды...

Глобально протокол обмена данными с датчиком представляет из себя последовательность из 3-х шагов:

  • Инициализация.
  • ROM-команда.
  • Функциональная команда.

В большинстве случаев следует неукоснительно придерживаться данной последовательности (кроме команд Search ROM и Alarm Search), иначе датчик впадет в ступор. Поэтому именно в такой последовательности и разберем досконально.

Инициализация заключается всего в одном действии - в отправке команды Reset на шину 1-wire (снова отсылка к предыдущей статье). То есть вызвав уже созданную нами функцию OneWire_Reset() мы успешно завершаем первый этап последовательности команд и переходим ко второму - к ROM-командам.

Список ROM-команд состоит из пяти возможных вариантов, то есть команд:

  • Read ROM (код команды 0x33)
  • Match ROM (0x55)
  • Skip ROM (0xCC)
  • Search ROM (0xF0)
  • Alarm Search (0xEC)

Read ROM позволяет прочитать из ROM датчика 64 бита, рассмотренные выше.

Match ROM используется для того, чтобы обратиться к какому-либо конкретному датчику из присутствующих на шине. Для этого после отправки команды нужно выдать в шину 64-х битный код, соответствующий нужному датчику. В результате последующая функциональная команда будет выполнена только этим датчиком, остальные же датчики ее проигнорируют.

Команда Skip ROM дает возможность обратиться ко всем датчикам, но нужно учитывать, что в случае, если за ней следом отправляется функциональная команда на чтение данных, то возникнет коллапс. Поскольку все устройства на шине начнут выдаввать информацию одновременно. В случае же одного датчика Skip ROM подходит идеально, поскольку у нас нет никакой необходимости обращаться к датчику по серийному номеру, так как он так и так всего один.

Следующая на очереди - Search ROM - позволяет обнаружить все устройства на шине, допустим, после включения питания, когда управляющее устройство не знает ни количество доступных датчиков, ни тем более их идентификационные данные.

Alarm Search идентична предыдущей команде по логике своей работы за тем лишь отличием, что ответят только те устройства, у которых выставлен флаг ALARM, сигнализирующий о выходе измеренной температуры за установленные пользователем допустимые пороги.

Следуем по обозначенному плану и, закончив с ROM-командами, переходим к списку функциональных команд:

  • Write Scratchpad (код команды 0x4E)
  • Read Scratchpad (0xBE)
  • Copy Scratchpad (0x48)
  • Convert T (0x44)
  • Recall E2 (0xB8)
  • Read Power Supply (0xB4)

Write Scratchpad позволяет обновить конфигурационную информацию, то есть регистры TH, TL и CONFIG. Соответственно, за командой должны следовать 3 байта, которые и будут сохранены в памяти датчика.

Аналогично Read Scratchpad позволяет прочитать данные из RAM, рассмотренные нами ближе к началу статьи. За этой командой следуют 8 байт из памяти и 9-й байт, представляющий из себя контрольную сумму.

Copy Scratchpad выполняет запись конфигурационных данных в энергонезависимую EEPROM-память.

Convert T - запуск измерения температуры. Есть несколько вариантов, как внешний контроллер может понять, что измерительные процессы завершены, их мы рассмотрим при создании практического примера.

Команда Recall E2 выполняет чтение данных из EEPROM и последующее копирование их в RAM. При этом данная команда выполняется автоматически при подаче питания на DS18B20, избавляя нас от необходимости заботиться об этом.

И, в завершение, команда Read Power Supply, как следует из ее красноречивого названия, позволяет определить, использует ли датчик паразитное питание или нет.

Таким образом, обменные процессы с датчиком могут быть упрощенно визуализированы следующим образом:

DS18B20 flowchart.

В примере для STM32 мы добавим не все из этих команд, а только необходимые, но механизм добавления новых команд будет максимально прост, так что это не особо и важно. Как раз к примеру и переходим.

Практическая часть.

Библиотека для работы с датчиком температуры DS18B20 на STM32. Модуль KY-001.

Ну с инициализацией периферии мы разобрались в предыдущей статье по 1-Wire, продублирую настройки USART'а:

Настройка USART в STM32CubeMx.

Аналогичным же образом добавим и отдельные файлы для работы с DS18B20 в проект с 1-Wire:

Структура проекта.

С приготовлениями на этом официально закончено. Дефайны из хэдера копировать отдельно не буду, в конце статьи будет полный код, остановимся на объявлении структуры, которая будет базовой при работе с датчиком температуры:

typedef struct DS18B20
{
  uint8_t isInitialized;
  uint8_t isConnected;
  UART_HandleTypeDef *uart;
  uint8_t serialNumber[DS18B20_SERIAL_NUMBER_LEN_BYTES];
  uint8_t temperatureLimitLow;
  uint8_t temperatureLimitHigh;
  uint8_t configRegister;
  float temperature;
} DS18B20;

Поля здесь несут следующий смысл:

  • isInitialized - единица, если датчик был проинициализирован успешно, ноль в противном случае.
  • isConnected - все аналогично, но проверяться будет наличие датчика на шине, что произведем при помощи анализа ответа на команду Reset, выданную в 1-Wire.
  • uart - здесь будет храниться указатель на хэндлер используемого модуля USART. Нужно это для того, чтобы максимально отделить низкоуровневую периферийную часть от непосредственно работы с датчиком.
  • serialNumber[] - сюда сохраним идентификационный номер датчика, это мы уже в красках и деталях обсудили.
  • temperatureLimitLow, temperatureLimitHigh, configRegister - это те три конфигурационных параметра, которые нам предоставляет DS18B20. Их будем также хранить в этой структуре.
  • temperature - наконец, то, ради чего все затевается, то есть результирующее, считанное с датчика, значение температуры.

Двигаемся дальше, еще одна структура для оформления команд датчика:

typedef struct DS18B20_Command
{
  uint8_t code;
  uint8_t rxBytesNum;
  uint8_t txBytesNum;
} DS18B20_Command;

Здесь последовательно - код команды, кол-во байт, которые необходимо вычитать из шины после отправки данной команды, кол-во байт для передачи вслед за командой. Соответственно, команды у нас либо предусматривают передачу, либо прием данных, поэтому одно из этих полей будет нулевым. На конкретных командах посмотрим примеры.

Ну что, начнем с инициализации датчика само собой:

/*----------------------------------------------------------------------------*/
void DS18B20_Init(DS18B20 *sensor, UART_HandleTypeDef *huart)
{
  sensor->isConnected = 0;
  sensor->uart = huart;
  sensor->isInitialized = 1;
}



/*----------------------------------------------------------------------------*/

И сразу же вдогонку алгоритм расчета контрольной суммы:

/*----------------------------------------------------------------------------*/
static uint8_t CalculateChecksum(uint8_t *data, uint8_t length)
{
  uint8_t checksum = 0;

  while (length--)
  {
    uint8_t currentByte = *data++;

    for (uint8_t i = 8; i; i--)
    {
      uint8_t temp = (checksum ^ currentByte) & 0x01;
      checksum >>= 1;

      if (temp)
      {
        checksum ^= 0x8C;
      }

      currentByte >>= 1;
    }
  }

  return checksum;
}



/*----------------------------------------------------------------------------*/

Далее идем по имеющемуся плану, то есть списку:

  • Команда инициализации.
  • ROM-команды.
  • Функциональные команды.

Инициализация заключается в отправке в шину команды Reset, по ответу на которую определяем, подключен ли к шине датчик:

/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_InitializationCommand(DS18B20 *sensor)
{
  if (sensor->isInitialized == 0)
  {
    return DS18B20_ERROR;
  }

  ONEWIRE_Status status = OneWire_Reset(sensor->uart);

  if (status == ONEWIRE_OK)
  {
    sensor->isConnected = 1;
    return DS18B20_OK;
  }
  else
  {
    sensor->isConnected = 0;
    return DS18B20_ERROR;
  }
}



/*----------------------------------------------------------------------------*/

Добавим пару ROM-команд, аналогично в случае необходимости можно добавить другие, которые потребуются. Первым делом объявляем:

// ROM commands
static DS18B20_Command readRom = {.code = 0x33, .rxBytesNum = 8, .txBytesNum = 0};
static DS18B20_Command skipRom = {.code = 0xCC, .rxBytesNum = 0, .txBytesNum = 0};

Давайте детально разберем принцип работы на примере команды чтения содержимого ROM-памяти. Вся работа в соответствующей функции DS18B20_ReadRom():

/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_ReadRom(DS18B20 *sensor)
{
  DS18B20_Status result;
  uint8_t rxData[DS18B20_READ_ROM_RX_BYTES_NUM];
  result = ExecuteCommand(sensor, readRom, rxData);

  if (result != DS18B20_OK)
  {
    return result;
  }

  for (uint8_t i = 0; i < DS18B20_SERIAL_NUMBER_LEN_BYTES; i++)
  {
    sensor->serialNumber[i] = rxData[DS18B20_SERIAL_NUMBER_OFFSET_BYTES + i];
  }

  return DS18B20_OK;
}



/*----------------------------------------------------------------------------*/

Все функции принимают аргументом указатель на структуру с данными датчика, в частности из этой структуры узнаем текущий использующийся модуль USART. Отправка любой команды производится вспомогательной функцией ExecuteCommand():

/*----------------------------------------------------------------------------*/
static DS18B20_Status ExecuteCommand(DS18B20 *sensor, DS18B20_Command command, uint8_t *data)
{
  if (sensor->isConnected == 0)
  {
    return DS18B20_ERROR;
  }

  OneWire_ProcessByte(sensor->uart, command.code);

  if (command.rxBytesNum != 0)
  {
    for (uint8_t i = 0; i < command.rxBytesNum; i++)
    {
      data[i] = OneWire_ProcessByte(sensor->uart, 0xFF);
    }

    uint8_t checkSum = CalculateChecksum(data, command.rxBytesNum - 1);
    if (checkSum != data[command.rxBytesNum - 1])
    {
      return DS18B20_ERROR;
    }
  }
  else
  {
    for (uint8_t i = 0; i < command.txBytesNum; i++)
    {
      OneWire_ProcessByte(sensor->uart, data[i]);
    }
  }

  return DS18B20_OK;
}



/*----------------------------------------------------------------------------*/

Разберем подробнее, что здесь имеется, и вернемся к DS18B20_ReadRom():

  1. Проверяем, что датчик обнаружен.
  2. Отправляем код команды в низкоуровневую OneWire_ProcessByte(), созданную нами ранее.
  3. Далее возможны варианты. Если для данной команды rxBytesNum не равно 0, то необходимо прочитать из 1-Wire данные. Вспоминаем, что для этого мы отправляем 0xFF и по принятым данным делаем вывод об ответе датчика. Опять же это подробно разобрали в упомянутой первой части, поэтому заострять дальше внимание не будем.
  4. Приняв данные, проверяем контрольную сумму, как без этого.
  5. Возвращаемся к пункту 2, в случае если команда требует отправки дополнительных информационных байт, то это и делаем - OneWire_ProcessByte(sensor->uart, data[i]).

Команда Read ROM дает нам возможность считать из датчика данные, которые мы после выполнения ExecuteCommand() помещаем в поле serialNumber[] датчика:

for (uint8_t i = 0; i < DS18B20_SERIAL_NUMBER_LEN_BYTES; i++)
{
	sensor->serialNumber[i] = rxData[DS18B20_SERIAL_NUMBER_OFFSET_BYTES + i];
}

Полностью аналогичный механизм используется для любых других команд, будь то ROM-команды или функциональные, в этом и смысл концепции. На примере Write Scratchpad:

/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_WriteScratchpad(DS18B20 *sensor, uint8_t *data)
{
  DS18B20_Status result;

  result = ExecuteCommand(sensor, writeScratchpad, data);

  if (result != DS18B20_OK)
  {
    return result;
  }

  sensor->temperatureLimitHigh = data[0];
  sensor->temperatureLimitLow = data[1];
  sensor->configRegister = data[2];

  return result;
}



/*----------------------------------------------------------------------------*/

Отправляем DS18B20 конфигурационные параметры и заодно записываем их в структуру, чтобы всегда знать, в каком состоянии находится датчик. Чуть объемнее для Read Scratchpad:

/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_ReadScratchpad(DS18B20 *sensor)
{
  DS18B20_Status result;
  uint8_t rxData[DS18B20_READ_SCRATCHPAD_RX_BYTES_NUM];
  result = ExecuteCommand(sensor, readScratchpad, rxData);

  if (result != DS18B20_OK)
  {
    return result;
  }

  sensor->temperatureLimitHigh = rxData[DS18B20_SCRATCHPAD_T_LIMIT_H_BYTE_IDX];
  sensor->temperatureLimitLow = rxData[DS18B20_SCRATCHPAD_T_LIMIT_L_BYTE_IDX];
  sensor->configRegister = rxData[DS18B20_SCRATCHPAD_CONFIG_BYTE_IDX];

  uint16_t tRegValue = (rxData[DS18B20_SCRATCHPAD_T_MSB_BYTE_IDX] << 8) | rxData[DS18B20_SCRATCHPAD_T_LSB_BYTE_IDX];
  uint16_t sign = tRegValue & DS18B20_SIGN_MASK;

  if (sign != 0)
  {
    tRegValue = (0xFFFF - tRegValue + 1);
  }

  switch (sensor->configRegister)
  {
    case DS18B20_9_BITS_CONFIG:
      tRegValue &= DS18B20_9_BITS_DATA_MASK;
      break;

    case DS18B20_10_BITS_CONFIG:
      tRegValue &= DS18B20_10_BITS_DATA_MASK;
      break;

    case DS18B20_11_BITS_CONFIG:
      tRegValue &= DS18B20_11_BITS_DATA_MASK;
      break;

    case DS18B20_12_BITS_CONFIG:
      tRegValue &= DS18B20_12_BITS_DATA_MASK;
      break;

    default:
      tRegValue &= DS18B20_12_BITS_DATA_MASK;
      break;
  }

  sensor->temperature = (float)tRegValue * DS18B20_T_STEP;

  if (sign != 0)
  {
    sensor->temperature *= (-1);
  }

  return DS18B20_OK;
}



/*----------------------------------------------------------------------------*/
  1. Так же как и для Read ROM вызываем ExecuteCommand() и в результате имеем принятые от датчика данные в массиве rxData[], остается только их обработать.
  2. Сохраняем считанную конфигурацию датчика в sensor->temperatureLimitHigh, sensor->temperatureLimitLow и sensor->configRegister.
  3. Далее следует кусок для пересчета температуры в градусы Цельсия. Это уже разобрали, но если возникнут вопросы, пишите 👍 Здесь учитывается значение sensor->configRegister, которое определяет разрешение DS18B20.
  4. Ну и на этом все.

Осталось чуть уделить внимания команде запуска измерения температуры Convert T:

/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_ConvertT(DS18B20 *sensor, DS18B20_WaitCondition waitCondition)
{
  DS18B20_Status result;
  uint8_t rxDummyData;

  result = ExecuteCommand(sensor, convertT, &rxDummyData);

  if (waitCondition == DS18B20_DATA)
  {
    WaitForConversionFinished(sensor);
  }

  if (waitCondition == DS18B20_DELAY)
  {
    uint32_t delayValueMs = 0;

    switch (sensor->configRegister)
    {
      case DS18B20_9_BITS_CONFIG:
        delayValueMs = DS18B20_9_BITS_DELAY_MS;
        break;

      case DS18B20_10_BITS_CONFIG:
        delayValueMs = DS18B20_10_BITS_DELAY_MS;
        break;

      case DS18B20_11_BITS_CONFIG:
        delayValueMs = DS18B20_11_BITS_DELAY_MS;
        break;

      case DS18B20_12_BITS_CONFIG:
        delayValueMs = DS18B20_12_BITS_DELAY_MS;
        break;

      default:
        break;
    }

    HAL_Delay(delayValueMs);
  }

  return result;
}



/*----------------------------------------------------------------------------*/

Здесь добавляется один нюанс в виде разных вариантов определения окончания измерения. Этот вариант задается аргументом waitCondition и может принимать значения:

typedef enum
{
  DS18B20_NONE            = 0x00,
  DS18B20_DATA            = 0x01,
  DS18B20_DELAY           = 0x02,
} DS18B20_WaitCondition;

Если waitCondition == DS18B20_NONE, то функция просто завершает выполнение после отправки команды о старте измерения. Соответственно, ответственность за проверку окончания процессов перед считыванием из датчика значений ложится на дополнительную внешнюю логику, которая должна быть реализована вне библиотеки.

Второй вариант - DS18B20_DATA - это самый оптимальный способ. Во время выполнения измерения можно продолжать считывать данные из шины, пока датчик занят работой - на шине будет логический "0", как только появится "1" - датчик завершил свои рабочие процессы. Именно это и проверяет функция WaitForConversionFinished():

/*----------------------------------------------------------------------------*/
static void WaitForConversionFinished(DS18B20 *sensor)
{
  uint8_t data = OneWire_ProcessBit(sensor->uart, 1);
  while(data != 0xFF)
  {
    data = OneWire_ProcessBit(sensor->uart, 1);
  }
}



/*----------------------------------------------------------------------------*/

Внимание! Описанный выше вариант не доступен в случае подключения датчика по схеме с паразитным питанием!

И третий вариант - DS18B20_DELAY - выжидать обозначенное в документации время в банальной функции задержки после старта измерения. То есть из даташита берем максимальное значение времени, которое требуется датчику для измерения при том или ином разрешении, и отправляем его прямиком в HAL_Delay(). Это неоптимальный способ ввиду того, что время, требуемое датчику может быть и меньше максимального значения.

Так, ну функционал разобрали, рассмотрим реальный пример применения библиотеки. Дальнейшая деятельность перемещается в main.c:

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  DS18B20_Init(&temperatureSensor, &huart1);

  DS18B20_InitializationCommand(&temperatureSensor);
  DS18B20_ReadRom(&temperatureSensor);
  DS18B20_ReadScratchpad(&temperatureSensor);

  uint8_t settings[3];
  settings[0] = temperatureSensor.temperatureLimitHigh;
  settings[1] = temperatureSensor.temperatureLimitLow;
  settings[2] = DS18B20_12_BITS_CONFIG;

  DS18B20_InitializationCommand(&temperatureSensor);
  DS18B20_SkipRom(&temperatureSensor);
  DS18B20_WriteScratchpad(&temperatureSensor, settings);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    DS18B20_InitializationCommand(&temperatureSensor);
    DS18B20_SkipRom(&temperatureSensor);
    DS18B20_ConvertT(&temperatureSensor, DS18B20_DATA);

    DS18B20_InitializationCommand(&temperatureSensor);
    DS18B20_SkipRom(&temperatureSensor);
    DS18B20_ReadScratchpad(&temperatureSensor);
  }
  /* USER CODE END 3 */
}

И объявление:

/* USER CODE BEGIN PV */
DS18B20 temperatureSensor;

/* USER CODE END PV */

Инициализируем датчик, сообщая ему что у нас тут между прочим USART1:

DS18B20_Init(&temperatureSensor, &huart1);

Считываем информацию из памяти DS18B20:

DS18B20_InitializationCommand(&temperatureSensor);
DS18B20_ReadRom(&temperatureSensor);
DS18B20_ReadScratchpad(&temperatureSensor);

И сразу же выполняем его настройку. Пороги температуры я задавать не хочу, так как не использую, поэтому передаю в датчик только что считанные значения, сохраненные в temperatureLimitHigh и temperatureLimitLow. Это связано как раз с тем, что датчик не поддерживает возможность отправки одного конфигурационного параметра, только всех трех. А вот в регистре CONFIG зададим максимальное разрешение, 12 бит:

uint8_t settings[3];
settings[0] = temperatureSensor.temperatureLimitHigh;
settings[1] = temperatureSensor.temperatureLimitLow;
settings[2] = DS18B20_12_BITS_CONFIG;

DS18B20_InitializationCommand(&temperatureSensor);
DS18B20_SkipRom(&temperatureSensor);
DS18B20_WriteScratchpad(&temperatureSensor, settings);

Далее у нас в цикле while(1) две последовательности команд. Первая запускает преобразование и ожидает его завершения. Вторая же считывает измеренное значение:

DS18B20_InitializationCommand(&temperatureSensor);
DS18B20_SkipRom(&temperatureSensor);
DS18B20_ConvertT(&temperatureSensor, DS18B20_DATA);

DS18B20_InitializationCommand(&temperatureSensor);
DS18B20_SkipRom(&temperatureSensor);
DS18B20_ReadScratchpad(&temperatureSensor);

По итогу в temperatureSensor->temperature имеем желаемое, а именно измеренное значение температуры:

Датчик температуры DS18B20 и STM32.

В целом, есть хорошие примеры для Arduino, можно для ознакомления также поизучать.

/**
  ******************************************************************************
  * @file           : ds18b20.c
  * @brief          : DS18B20 driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "ds18b20.h"
#include "onewire.h"



/* Declarations and definitions ----------------------------------------------*/

// ROM commands
static DS18B20_Command readRom = {.code = 0x33, .rxBytesNum = 8, .txBytesNum = 0};
static DS18B20_Command skipRom = {.code = 0xCC, .rxBytesNum = 0, .txBytesNum = 0};

// Function commands
static DS18B20_Command readScratchpad = {.code = 0xBE, .rxBytesNum = 9, .txBytesNum = 0};
static DS18B20_Command writeScratchpad = {.code = 0x4E, .rxBytesNum = 0, .txBytesNum = 3};
static DS18B20_Command convertT = {.code = 0x44, .rxBytesNum = 0, .txBytesNum = 0};



/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
static uint8_t CalculateChecksum(uint8_t *data, uint8_t length)
{
  uint8_t checksum = 0;

  while (length--)
  {
    uint8_t currentByte = *data++;

    for (uint8_t i = 8; i; i--)
    {
      uint8_t temp = (checksum ^ currentByte) & 0x01;
      checksum >>= 1;

      if (temp)
      {
        checksum ^= 0x8C;
      }

      currentByte >>= 1;
    }
  }

  return checksum;
}



/*----------------------------------------------------------------------------*/
static DS18B20_Status ExecuteCommand(DS18B20 *sensor, DS18B20_Command command, uint8_t *data)
{
  if (sensor->isConnected == 0)
  {
    return DS18B20_ERROR;
  }

  OneWire_ProcessByte(sensor->uart, command.code);

  if (command.rxBytesNum != 0)
  {
    for (uint8_t i = 0; i < command.rxBytesNum; i++)
    {
      data[i] = OneWire_ProcessByte(sensor->uart, 0xFF);
    }

    uint8_t checkSum = CalculateChecksum(data, command.rxBytesNum - 1);
    if (checkSum != data[command.rxBytesNum - 1])
    {
      return DS18B20_ERROR;
    }
  }
  else
  {
    for (uint8_t i = 0; i < command.txBytesNum; i++)
    {
      OneWire_ProcessByte(sensor->uart, data[i]);
    }
  }

  return DS18B20_OK;
}



/*----------------------------------------------------------------------------*/
static void WaitForConversionFinished(DS18B20 *sensor)
{
  uint8_t data = OneWire_ProcessBit(sensor->uart, 1);
  while(data != 0xFF)
  {
    data = OneWire_ProcessBit(sensor->uart, 1);
  }
}



/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_ConvertT(DS18B20 *sensor, DS18B20_WaitCondition waitCondition)
{
  DS18B20_Status result;
  uint8_t rxDummyData;

  result = ExecuteCommand(sensor, convertT, &rxDummyData);

  if (waitCondition == DS18B20_DATA)
  {
    WaitForConversionFinished(sensor);
  }

  if (waitCondition == DS18B20_DELAY)
  {
    uint32_t delayValueMs = 0;

    switch (sensor->configRegister)
    {
      case DS18B20_9_BITS_CONFIG:
        delayValueMs = DS18B20_9_BITS_DELAY_MS;
        break;

      case DS18B20_10_BITS_CONFIG:
        delayValueMs = DS18B20_10_BITS_DELAY_MS;
        break;

      case DS18B20_11_BITS_CONFIG:
        delayValueMs = DS18B20_11_BITS_DELAY_MS;
        break;

      case DS18B20_12_BITS_CONFIG:
        delayValueMs = DS18B20_12_BITS_DELAY_MS;
        break;

      default:
        break;
    }

    HAL_Delay(delayValueMs);
  }

  return result;
}



/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_ReadScratchpad(DS18B20 *sensor)
{
  DS18B20_Status result;
  uint8_t rxData[DS18B20_READ_SCRATCHPAD_RX_BYTES_NUM];
  result = ExecuteCommand(sensor, readScratchpad, rxData);

  if (result != DS18B20_OK)
  {
    return result;
  }

  sensor->temperatureLimitHigh = rxData[DS18B20_SCRATCHPAD_T_LIMIT_H_BYTE_IDX];
  sensor->temperatureLimitLow = rxData[DS18B20_SCRATCHPAD_T_LIMIT_L_BYTE_IDX];
  sensor->configRegister = rxData[DS18B20_SCRATCHPAD_CONFIG_BYTE_IDX];

  uint16_t tRegValue = (rxData[DS18B20_SCRATCHPAD_T_MSB_BYTE_IDX] << 8) | rxData[DS18B20_SCRATCHPAD_T_LSB_BYTE_IDX];
  uint16_t sign = tRegValue & DS18B20_SIGN_MASK;

  if (sign != 0)
  {
    tRegValue = (0xFFFF - tRegValue + 1);
  }

  switch (sensor->configRegister)
  {
    case DS18B20_9_BITS_CONFIG:
      tRegValue &= DS18B20_9_BITS_DATA_MASK;
      break;

    case DS18B20_10_BITS_CONFIG:
      tRegValue &= DS18B20_10_BITS_DATA_MASK;
      break;

    case DS18B20_11_BITS_CONFIG:
      tRegValue &= DS18B20_11_BITS_DATA_MASK;
      break;

    case DS18B20_12_BITS_CONFIG:
      tRegValue &= DS18B20_12_BITS_DATA_MASK;
      break;

    default:
      tRegValue &= DS18B20_12_BITS_DATA_MASK;
      break;
  }

  sensor->temperature = (float)tRegValue * DS18B20_T_STEP;

  if (sign != 0)
  {
    sensor->temperature *= (-1);
  }

  return DS18B20_OK;
}



/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_WriteScratchpad(DS18B20 *sensor, uint8_t *data)
{
  DS18B20_Status result;

  result = ExecuteCommand(sensor, writeScratchpad, data);

  if (result != DS18B20_OK)
  {
    return result;
  }

  sensor->temperatureLimitHigh = data[0];
  sensor->temperatureLimitLow = data[1];
  sensor->configRegister = data[2];

  return result;
}



/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_InitializationCommand(DS18B20 *sensor)
{
  if (sensor->isInitialized == 0)
  {
    return DS18B20_ERROR;
  }

  ONEWIRE_Status status = OneWire_Reset(sensor->uart);

  if (status == ONEWIRE_OK)
  {
    sensor->isConnected = 1;
    return DS18B20_OK;
  }
  else
  {
    sensor->isConnected = 0;
    return DS18B20_ERROR;
  }
}



/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_ReadRom(DS18B20 *sensor)
{
  DS18B20_Status result;
  uint8_t rxData[DS18B20_READ_ROM_RX_BYTES_NUM];
  result = ExecuteCommand(sensor, readRom, rxData);

  if (result != DS18B20_OK)
  {
    return result;
  }

  for (uint8_t i = 0; i < DS18B20_SERIAL_NUMBER_LEN_BYTES; i++)
  {
    sensor->serialNumber[i] = rxData[DS18B20_SERIAL_NUMBER_OFFSET_BYTES + i];
  }

  return DS18B20_OK;
}



/*----------------------------------------------------------------------------*/
DS18B20_Status DS18B20_SkipRom(DS18B20 *sensor)
{
  DS18B20_Status result;
  uint8_t rxDummyData;
  result = ExecuteCommand(sensor, skipRom, &rxDummyData);

  if (result != DS18B20_OK)
  {
    return result;
  }

  return DS18B20_OK;
}



/*----------------------------------------------------------------------------*/
void DS18B20_Init(DS18B20 *sensor, UART_HandleTypeDef *huart)
{
  sensor->isConnected = 0;
  sensor->uart = huart;
  sensor->isInitialized = 1;
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : ds18b20.h
  * @brief          : DS18B20 driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef DS18B20_H
#define DS18B20_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f4xx_hal.h"



/* Declarations and definitions ----------------------------------------------*/

#define DS18B20_SERIAL_NUMBER_LEN_BYTES                                     6
#define DS18B20_SERIAL_NUMBER_OFFSET_BYTES                                  1

#define DS18B20_SCRATCHPAD_T_LSB_BYTE_IDX                                   0
#define DS18B20_SCRATCHPAD_T_MSB_BYTE_IDX                                   1
#define DS18B20_SCRATCHPAD_T_LIMIT_H_BYTE_IDX                               2
#define DS18B20_SCRATCHPAD_T_LIMIT_L_BYTE_IDX                               3
#define DS18B20_SCRATCHPAD_CONFIG_BYTE_IDX                                  4

#define DS18B20_9_BITS_CONFIG                                               0x1F
#define DS18B20_10_BITS_CONFIG                                              0x3F
#define DS18B20_11_BITS_CONFIG                                              0x5F
#define DS18B20_12_BITS_CONFIG                                              0x7F

#define DS18B20_9_BITS_DELAY_MS                                             94
#define DS18B20_10_BITS_DELAY_MS                                            188
#define DS18B20_11_BITS_DELAY_MS                                            375
#define DS18B20_12_BITS_DELAY_MS                                            750

#define DS18B20_9_BITS_DATA_MASK                                            0x7F8
#define DS18B20_10_BITS_DATA_MASK                                           0x7FC
#define DS18B20_11_BITS_DATA_MASK                                           0x7FE
#define DS18B20_12_BITS_DATA_MASK                                           0x7FF

#define DS18B20_SIGN_MASK                                                   0xF800

#define DS18B20_T_STEP                                                      0.0625

#define DS18B20_READ_ROM_RX_BYTES_NUM                                       8
#define DS18B20_READ_SCRATCHPAD_RX_BYTES_NUM                                9



typedef struct DS18B20
{
  uint8_t isInitialized;
  uint8_t isConnected;
  UART_HandleTypeDef *uart;
  uint8_t serialNumber[DS18B20_SERIAL_NUMBER_LEN_BYTES];
  uint8_t temperatureLimitLow;
  uint8_t temperatureLimitHigh;
  uint8_t configRegister;
  float temperature;
} DS18B20;



typedef struct DS18B20_Command
{
  uint8_t code;
  uint8_t rxBytesNum;
  uint8_t txBytesNum;
} DS18B20_Command;



typedef enum
{
  DS18B20_OK              = 0x00,
  DS18B20_ERROR           = 0x01,
} DS18B20_Status;



typedef enum
{
  DS18B20_NONE            = 0x00,
  DS18B20_DATA            = 0x01,
  DS18B20_DELAY           = 0x02,
} DS18B20_WaitCondition;



/* Functions -----------------------------------------------------------------*/

extern DS18B20_Status DS18B20_ConvertT(DS18B20 *sensor, DS18B20_WaitCondition waitCondition);
extern DS18B20_Status DS18B20_ReadScratchpad(DS18B20 *sensor);
extern DS18B20_Status DS18B20_WriteScratchpad(DS18B20 *sensor, uint8_t *data);
extern DS18B20_Status DS18B20_InitializationCommand(DS18B20 *sensor);
extern DS18B20_Status DS18B20_ReadRom(DS18B20 *sensor);
extern DS18B20_Status DS18B20_SkipRom(DS18B20 *sensor);
extern void DS18B20_Init(DS18B20 *sensor, UART_HandleTypeDef *huart);



#endif // #ifndef DS18B20_H

Ссылка на проект для DS18B20 на базе модуля KY-001 - MT_DS18B20_Project_Part_2.

P. S. Да, кстати, DMA я в этом примере не стал задействовать ввиду отсутствия такой необходимости. Но если вдруг будет спрос на такой вариант, то добавлю пример с DMA.

Подписаться
Уведомить о
guest

24 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Naitalar
Naitalar
2 лет назад

Спасибо большое. Способ подключения легко переделывается под stm32L4. Всё просто объяснено (как раз для меня). Буду пробовать различные режимы измерения датчиков. Также буду пробовать использовать эти наработки для подключения микросхемы с MAC адресом (DS2502P-48+), там тоже 1-wire.

Nulik
Nulik
2 лет назад

Не получается реализовать searchRom, не могли бы растолковать как добавить эту команду. Нужно получать температуру с двух датчиков.
Заранее спасибо

Эдуард
Ответ на комментарий  Nulik
2 лет назад

У меня есть библиотека под С++. Только не помню, доделана до конца или нет.
Можно взять реализацию у ардуйнщиков. Библиотека называется OneWire

Кирилл
Кирилл
1 год назад

Добрый день! Спасибо за такое развернутое объяснение работы с этим датчиком!
Входе изучения и разбора Вашего проекта возник вопрос... В функции DS18B20_ReadScratchpad перводим полученные значения датчика в градусы цельсия:
sensor->temperature = (float)tRegValue*DS18B20_T_STEP.
Т.е. умножаем значение на 0.0625.

Только вот почему-то при комнатной температуре в 23-25 °С датчик выдает 2.9-3.3 °С. В чем может быть проблема?

Кирилл
Кирилл
Ответ на комментарий  Aveal
1 год назад

Добрый день! sensor->temperature = (float)tRegValue;
выдаёт 0x31

Кирилл
Кирилл
Ответ на комментарий  Aveal
1 год назад

Тоже самое 0х31

20230426_104921.jpg
Кирилл
Кирилл
Ответ на комментарий  Кирилл
1 год назад

Перед указанной Вами строчки
0х2000

20230426_105004.jpg
Кирилл
Кирилл
Ответ на комментарий  Aveal
1 год назад

Да, соответстсвует...

20230426_133732.jpg
Кирилл
Кирилл
Ответ на комментарий  Кирилл
1 год назад

Заметил такую вещь... в DS18B20_ReadScratchpad в конструкции switch-case выполнение скидывается сразу в default.

20230426_134327.jpg
Кирилл
Кирилл
Ответ на комментарий  Кирилл
1 год назад

И в ConvertT так же не заходит в switch-case...

20230426_134924.jpg
Аркадий Симонов
1 год назад

Еще позволю себе наглость отметить, что Ваш код не совсем универсален. В том смысле, что не все компиляторы умеют в качестве размера массива принимать переменные. На мой взгляд, это экзотический прием....
Например,

uint8_t rxData[readScratchpad.rxBytesNum];
Видимо дело в стандартах языка, IAR - 8.32 о такое спотыкается...

Аркадий Симонов
1 год назад

Нашел несоотвествие параметров функии в *.с и *.h файлах

в ds18b20.h прототип выглядит так:
extern DS18B20_Status DS18B20_ConvertT(DS18B20 *sensor, uint8_t wait);

а в ds18b20.c функция объявлена так:
DS18B20_Status DS18B20_ConvertT(DS18B20 *sensor, DS18B20_WaitCondition waitCondition)

Мой компилятор не пропускает.

Александр
Александр
1 год назад

Здравствуйте! Будет ли статья о подключении множества таких датчиков на одной линии? Очень хотелось бы видеть. Спасибо.

Юрий
Юрий
10 месяцев назад

Спасибо за материал. Использую STM32F103C8T6 и модулёк HW-477 V0.2 на вид точно такой же, как и в этом материале. Но в нём стоит микросхемка MY18E20. Из её DataSheet на первый взгляд видно, что это тоже самое, что и DS18B20. Но есть некоторые вопросы. И первое, что бросилось в глаза, так параметр Temperature Conversion Time, которого в прямом виде у DS18B20 нет. Так вот для MY18E20 этот параметр равен 500 ms. Есть другая микросхема MY18E20-15, у которой этот параметр равен 15 ms. Сначала подумал, что это как то связано с фиксированным для микросхемы разрешением АЦП, но в DataSheet по этому поводу ничего не увидел, как будто в этом плане ограничений нет. В связи с этим к Вам вопрос такой. Этот параметр сказывается на правильную работу Вашей программы?. Подключил упомянутый модулёк в соответствии с DataSheet (распиновка разная), подправил программу для USART2 и STM32F103C8T6, запустил. Вижу, что на платке светодиод подмигивает, но данных никаких не вижу. Пока рассуждал как же понять в чём же дело, пришла не очень радостная мысль.
Так понимаю, что Ваша программа зависает на время преобразования АЦП. В худшем случае это может быть 750 ms для 12 битного преобразования, а мне это неприемлемо. Тут надо работать по прерываниям. В связи с этим второй вопрос: Сложно ли в этом смысле доработать Вашу программу или придётся менять всю её концепцию.

Глеб
Глеб
10 месяцев назад

Спасибо, всё работает

24
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x