Top.Mail.Ru

Библиотека для работы с шиной 1-Wire на STM32.

Уважаемые посетители, коллеги и друзья, традиционно снова рад приветствовать вас на нашем сайте 🤝 Давеча разговорились мы с пользователем TQFP на тему разрастания и продвижения нашего сообщества, в результате чего он выразил желание оформить небольшой курс по подключению простейших, но что более важно, наиболее популярных датчиков к STM32. Основные цели в этом действе двояки - это как облегчение старта и понижение порога вхождения для тех, кто только начинает ступать на тропу работы с микроконтроллерами, так и потенциальная оптимизация/доработка для опытных профессионалов, так или иначе уже использующих то, о чем пойдет речь. Собственно, TQFP решил взять за базу готовый набор датчиков, который у него есть в наличии под рукой - а именно этот (набор датчиков 37 в 1):

Набор датчиков STM32

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

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

Начнем, опять же традиционно, с краткого экскурса в некоторые историко-теоретические аспекты. Для работы с датчиком используется уже упомянутая шина 1-Wire, которая является двунаправленной шиной для обмена данными с устройствами на относительно невысокой скорости (не более 125 Кбит/с). Разработана шина была компанией Dallas Semiconductor, которая является и производителем DS18B20. Отличительной особенностью можно считать то, что для полноценного подключения устройств нужны лишь две линии:

  • общая (GND)
  • одна линия как для передачи данных, так и для питания. То есть подача питания осуществляется той же линией, что и служит для передачи полезной информации.

Для вводного слова думаю достаточно, переходим к активной деятельности 👍 Итак, что у нас по аппаратной части. Пункт первый - непосредственно датчик DS18B20 в виде модуля KY-001. Пункт второй, завершающий - плата с микроконтроллером STM32F401CC. Какой именно контроллер использовать особой роли не играет, библиотеку для работы с 1-Wire будем, конечно же, делать универсальной по максимуму. Ввиду растущей популярности проекты для сайта и сейчас делаю в STM32CubeIDE, что также до неприличия облегчает процесс потенциального переноса ПО на другой контроллер.

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

  • Bit-banging - то есть датчик подключается к любому порту GPIO и обмен данными происходит путем дергания этой ногой, а также считывания ее состояния вручную.
  • С использованием USART. Именно этот способ я задействую, как более интересный и оптимальный.

Настройка и инициализация периферии.

С электрической частью покончили, переходим к созданию проекта и настройке необходимой периферии в STM32CubeMx. Активируем базово-необходимые вещи, такие как внешний кварцевый резонатор и интерфейс SWD для отладки:

STM32CubeMx базовые настройки

Тактовые частоты зададим так:

STM32CubeMx настройки тактирования

Но опять же, в данном конкретном проекте это все не слишком существенно. Больше внимания уделим непосредственно USART'у. Я взял USART1, соответственно, обмен данными будет осуществляться через PA9. Настраиваем следующим образом:

STM32CubeMx настройки USART

Касаемо скорости передачи данных еще поговорим, по умолчанию ставим 115200 Бит/с. На этом заканчиваем с частью, посвященной периферии, и перемещаемся к программной реализации.

Принцип работы 1-Wire.

Сразу же структурируем проект подобающий образом, создав файлы для работы с шиной (onewire.c/onewire.h). В них инкапсулируем всю низкоуровневую часть для обмена данными, на базе которой впоследствии надстроим работу конкретно с датчиком DS18B20:

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

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

  1. Команда сброса
  2. Передача бита 1.
  3. Передача бита 0.
  4. Чтение бита.

Так что планомерно и систематично добавим поддержку перечисленного, и на этом можно будет переходить к следующему этапу. Итак, команда сброса... Официальная документация говорит нам следующее:

1-Wire reset command

То есть команда сброса представляет из себя не что иное, как низкий уровень на линии на протяжении обозначенного времени. Вспоминаем, что мы решили использовать USART и производим необходимые расчеты. Классический метод заключается в том, что при отправке команды сброса, USART переконфигурируется на скорость передачи данных 9600 бит/с. При данной скорости время передачи одного бита составляет:

T_{bit} = \frac{1000000 \medspace мкс}{9600 } = 104 \medspace мкс

Итого - для генерации Reset'а отправим в USART байт 0xF0 (0b11110000). Что это нам даст? Тут все просто - биты будут передаваться, начиная с младшего, значит первой будет выдана последовательность из четырех нулей. Приплюсовываем к этому нулевой старт-бит USART'а и получаем 5 бит, что эквивалентно низкому уровню на шине на протяжении 5 * 104 мкс = 520 мкс:

1-Wire reset на USART STM32

Это в свою очередь полностью соответствует внешнему виду команды Reset 👍 После отправки 0xF0 встаем на прием, и в том случае, если на шине присутствуют другие устройства, мы примем значение, не равное тому, что мы отправили. Если же устройств на шине нет, то примем ровно то, что отправили, а именно 0xF0, поскольку фактически Tx и Rx USART'а у нас замкнуты (одна линия для передачи данных по шине). Таким вот способом будет осуществляться обнаружение подключенных датчиков.

На этом с первым пунктом интерфейсной части успешно прощаемся. Переходим к передачи информационных битов. Передача единицы:

1-Wire write bit 1

Как видите, механизм при работе с 1-Wire един и неизменен - по умолчанию линия подтянута к питанию, устройства же взаимодействуют выдачей на линию нуля. Вся разница только в длительностях. В данном случае для передачи единицы необходимо подать ноль на время, соответствующее интервалу 1-15 мкс. Да, кстати, скорость 9600 бит/с будет использоваться только и исключительно для команды Reset. Для оставшихся трех пунктов переконфигурируем снова на 115200 бит/с. С этим наглядно разберемся на практическом примере.

На скорости 115200 бит/с передача одного бита это:

T_{bit} = \frac{1000000 \medspace мс}{115200} = 8.7 \medspace мс

Поэтому передавать будем байт 0xFF, что вкупе со старт-битом даст нам требуемое:

1-Wire бит 1 на USART STM32

Сразу же рассмотрим и передачу нуля:

1-Wire write bit 0

Все то же самое, разница, как мы уже обсудили, только во временных интервалах. Передаем в USART 0x00, имеем:

1-Wire бит 0 на USART STM32

Все четко! Остается только один пункт - чтение информационного бита. И снова все завязано на физическом устройстве шины, при котором линия подтянута вверх, а устройства при возникновении такого желания опускают ее вниз принудительно. Для чтения осуществляем выдачу 0xFF в USART (по-прежнему на 115200) и встаем на прием данных. При приеме в ответ того же байта 0xFF делаем вывод, что приняли бит "1", при любом другом значении - приняли "0". Собираем все вышеобозначенное воедино в графической инсталляции )

1-Wire USART baudrate USART data
Reset 9600 Кбит/с 0xF0
Bit 1 115200 Кбит/с 0xFF
Bit 0 115200 Кбит/с 0x00

То есть при обмене данными одному биту на шине 1-Wire соответствует один байт (8 бит), передаваемый/принимаемый по USART. С общей концепцией разобрались, если и остались какие-либо неявные моменты, то их мы без проблем развеем при помощи пресловутого практического примера, к которому и переходим.

Библиотека для работы с шиной 1-Wire на STM32.

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

/*----------------------------------------------------------------------------*/
static void SetBaudrate(UART_HandleTypeDef *huart, uint32_t baudrate)
{
  uint32_t pclk = 0;
  huart->Init.BaudRate = baudrate;

#if defined(USART6) && defined(UART9) && defined(UART10)
    if ((huart->Instance == USART1) || (huart->Instance == USART6) ||
        (huart->Instance == UART9)  || (huart->Instance == UART10))
    {
      pclk = HAL_RCC_GetPCLK2Freq();
    }
#elif defined(USART6)
    if ((huart->Instance == USART1) || (huart->Instance == USART6))
    {
      pclk = HAL_RCC_GetPCLK2Freq();
    }
#else
    if (huart->Instance == USART1)
    {
      pclk = HAL_RCC_GetPCLK2Freq();
    }
#endif /* USART6 */
    else
    {
      pclk = HAL_RCC_GetPCLK1Freq();
    }

  if (huart->Init.OverSampling == UART_OVERSAMPLING_8)
  {
    huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
  }
  else
  {
    huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
  }
}



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

У меня здесь использован контроллер STM32F4xx, поэтому при переходе на другое семейство потребуется внести изменения. В общем-то, похожую часть кода можно найти в HAL'овской static void UART_SetConfig(UART_HandleTypeDef *huart). Поэтому в случае необходимости нужно без лишних сомнений и сложностей позаимствовать оттуда 👍 Все готово для осуществления деятельности по выполнению команды сброса:

/*----------------------------------------------------------------------------*/
ONEWIRE_Status OneWire_Reset(UART_HandleTypeDef *huart)
{
  OneWire_ProcessByte(huart, 0x43);

  ONEWIRE_Status status = ONEWIRE_OK;
  uint8_t txByte = ONEWIRE_RESET_BYTE;
  uint8_t rxByte = 0x00;

  SetBaudrate(huart, ONEWIRE_RESET_BAUDRATE);

  HAL_UART_Transmit(huart, &txByte, 1, ONEWIRE_UART_TIMEOUT);
  HAL_UART_Receive(huart, &rxByte, 1, ONEWIRE_UART_TIMEOUT);

  SetBaudrate(huart, ONEWIRE_BAUDRATE);

  if (rxByte == txByte)
  {
    status = ONEWIRE_ERROR;
  }

  return status;
}



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

Здесь все в точности по тем теоретическим принципам, которые мы уже обсудили. Отправляем 0xF0 (ONEWIRE_RESET_BYTE) и по ответу делаем вывод о наличии устройств на шине. Все определения в onewire.h:

#define ONEWIRE_BAUDRATE                                              115200
#define ONEWIRE_RESET_BAUDRATE                                        9600

#define ONEWIRE_RESET_BYTE                                            0xF0
#define ONEWIRE_UART_TIMEOUT                                          10
#define ONEWIRE_BITS_NUM                                              8



typedef enum
{
  ONEWIRE_OK              = 0x00,
  ONEWIRE_ERROR           = 0x01,
} ONEWIRE_Status;

Для отправки и получения полезных данных у нас будут функции:

  • uint8_t OneWire_ProcessByte(UART_HandleTypeDef *huart, uint8_t byte)
  • uint8_t OneWire_ProcessBit(UART_HandleTypeDef *huart, uint8_t bit)

В первую мы передаем байт, которой необходимо выдать на шину, а также указатель на структуру, соответствующую используемому USART'у, определение которой для конкретного случая можно найти в main.c:

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart1;

Структура HAL подразумевает генерацию кода такого вида для любой выбранной периферии. Итак, возвращаемся к передаче информации.

OneWire_ProcessByte() принимает аргументом байт данных. И здесь имеется ввиду именно один байт в контексте 1-Wire. А как мы уже выяснили, выдача одного байта в шину 1-Wire представляет из себя выдачу 8-ми байт в USART (1 бит 1-wire = 1 байт в USART, 8 бит 1-wire = 8 байт в USART). Поэтому байт, переданный в OneWire_ProcessByte(), мы разбиваем на биты и передаем в функцию OneWire_ProcessBit():

/*----------------------------------------------------------------------------*/
uint8_t OneWire_ProcessByte(UART_HandleTypeDef *huart, uint8_t byte)
{
  uint8_t rxByte = 0x00;

  for (uint8_t i = 0; i < ONEWIRE_BITS_NUM; i++)
  {
    uint8_t txBit = (byte >> i) & 0x01;
    uint8_t rxBit = 0;

    uint8_t tempRxData = OneWire_ProcessBit(huart, txBit);

    if (tempRxData == 0xFF)
    {
      rxBit = 1;
    }

    rxByte |= (rxBit << i);
  }

  return rxByte;
}



/*----------------------------------------------------------------------------*/
uint8_t OneWire_ProcessBit(UART_HandleTypeDef *huart, uint8_t bit)
{
  uint8_t txData = 0xFF;
  uint8_t rxData = 0x00;

  if (bit == 0)
  {
    txData = 0x00;
  }

  HAL_UART_Transmit(huart, &txData, 1, ONEWIRE_UART_TIMEOUT);
  HAL_UART_Receive(huart, &rxData, 1, ONEWIRE_UART_TIMEOUT);

  return rxData;
}



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

Если посмотреть на табличку, которую мы организовали, то все четко по ней здесь и происходит. Вычленяем txBit из byte и запихиваем его в OneWire_ProcessBit(). Если бит "1", то в USART улетает 0xFF, иначе - 0x00. Сразу же встаем на прием и, если приняли 0xFF (if (tempRxData == 0xFF)), то это сигнализирует о том, что с 1-Wire принят бит "1" - rxBit = 1 - в противном случае rxBit будет равен 0. Остается поместить бит в rxByte на нужную позицию - rxByte |= (rxBit << i). И в итоге на выходе из функции OneWire_ProcessByte() мы имеем принятый байт rxByte.

Полный код созданных файлов получаем такой:

/**
  ******************************************************************************
  * @file           : onewire.c
  * @brief          : 1-Wire driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



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

#include "onewire.h"



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



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

/*----------------------------------------------------------------------------*/
static void SetBaudrate(UART_HandleTypeDef *huart, uint32_t baudrate)
{
  uint32_t pclk = 0;
  huart->Init.BaudRate = baudrate;

#if defined(USART6) && defined(UART9) && defined(UART10)
    if ((huart->Instance == USART1) || (huart->Instance == USART6) ||
        (huart->Instance == UART9)  || (huart->Instance == UART10))
    {
      pclk = HAL_RCC_GetPCLK2Freq();
    }
#elif defined(USART6)
    if ((huart->Instance == USART1) || (huart->Instance == USART6))
    {
      pclk = HAL_RCC_GetPCLK2Freq();
    }
#else
    if (huart->Instance == USART1)
    {
      pclk = HAL_RCC_GetPCLK2Freq();
    }
#endif /* USART6 */
    else
    {
      pclk = HAL_RCC_GetPCLK1Freq();
    }

  if (huart->Init.OverSampling == UART_OVERSAMPLING_8)
  {
    huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
  }
  else
  {
    huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
  }
}



/*----------------------------------------------------------------------------*/
uint8_t OneWire_ProcessBit(UART_HandleTypeDef *huart, uint8_t bit)
{
  uint8_t txData = 0xFF;
  uint8_t rxData = 0x00;

  if (bit == 0)
  {
    txData = 0x00;
  }

  HAL_UART_Transmit(huart, &txData, 1, ONEWIRE_UART_TIMEOUT);
  HAL_UART_Receive(huart, &rxData, 1, ONEWIRE_UART_TIMEOUT);

  return rxData;
}



/*----------------------------------------------------------------------------*/
uint8_t OneWire_ProcessByte(UART_HandleTypeDef *huart, uint8_t byte)
{
  uint8_t rxByte = 0x00;

  for (uint8_t i = 0; i < ONEWIRE_BITS_NUM; i++)
  {
    uint8_t txBit = (byte >> i) & 0x01;
    uint8_t rxBit = 0;

    uint8_t tempRxData = OneWire_ProcessBit(huart, txBit);

    if (tempRxData == 0xFF)
    {
      rxBit = 1;
    }

    rxByte |= (rxBit << i);
  }

  return rxByte;
}



/*----------------------------------------------------------------------------*/
ONEWIRE_Status OneWire_Reset(UART_HandleTypeDef *huart)
{
  ONEWIRE_Status status = ONEWIRE_OK;
  uint8_t txByte = ONEWIRE_RESET_BYTE;
  uint8_t rxByte = 0x00;

  SetBaudrate(huart, ONEWIRE_RESET_BAUDRATE);

  HAL_UART_Transmit(huart, &txByte, 1, ONEWIRE_UART_TIMEOUT);
  HAL_UART_Receive(huart, &rxByte, 1, ONEWIRE_UART_TIMEOUT);

  SetBaudrate(huart, ONEWIRE_BAUDRATE);

  if (rxByte == txByte)
  {
    status = ONEWIRE_ERROR;
  }

  return status;
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : onewire.h
  * @brief          : 1-Wire driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef ONEWIRE_H
#define ONEWIRE_H



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

#include "stm32f4xx_hal.h"



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

#define ONEWIRE_BAUDRATE                                              115200
#define ONEWIRE_RESET_BAUDRATE                                        9600

#define ONEWIRE_RESET_BYTE                                            0xF0
#define ONEWIRE_UART_TIMEOUT                                          10
#define ONEWIRE_BITS_NUM                                              8



typedef enum
{
  ONEWIRE_OK              = 0x00,
  ONEWIRE_ERROR           = 0x01,
} ONEWIRE_Status;



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

extern ONEWIRE_Status OneWire_Reset(UART_HandleTypeDef *huart);
extern uint8_t OneWire_ProcessByte(UART_HandleTypeDef *huart, uint8_t byte);
extern uint8_t OneWire_ProcessBit(UART_HandleTypeDef *huart, uint8_t bit);



#endif // #ifndef ONEWIRE_H

И вот на этом месте сделаем паузу... Поскольку статья получилась объемнее, чем я планировал, а я не очень люблю чересчур затянутые материалы, да и поисковики их не так чтобы сильно любят, поэтому разобьем работу с 1-Wire на две части. В ближайшее время выйдет вторая, в которой будем использовать созданное сегодня, и сразу перейдем непосредственно к получению данных с DS18B20. Так что до скорого🤝

Ссылка на проект (здесь уже содержится и часть для работы с датчиком из следующей статьи) - MT_DS18B20_Project.

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

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

Спасибо большое за статью. Написано всё очень просто и разложено по полочкам.

Олег
Олег
2 лет назад

Добрый день, спасибо за материал, мкс поправьте в формулах, а то я ступор мозговины поймал: время передачи одного бита делю 1/9600 = 1,04*10^-4 секунд = 104мкс.

Алексей
Алексей
2 лет назад

Еще бы статью с bit-bang тоже бы сделать. Через прерывания. Было бы интересно почитать.

Ден
Ден
2 лет назад

Есть еще способ через таймер шим

aleks
aleks
1 год назад

файлы onewire.c or onewire.h зеркало. у кого как и что могло получиться , хз

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

Подскажите, пожалуйста, я пробовал перенести на STM32F7xx и столкнулся с ошибкой, что функция UART_BRR_SAMPLING8 объявлена неявно и недействительна на С99. Пробовал искать эту функцию и нашёл только в HAL_StatusTypeDef UART_SetConfig(UART_HandleTypeDef *huart) нечто похожее - UART_DIV_SAMPLING8, однако, если использовать её, то в отладчике ничего не показывает. Пробовал посмотреть на осциллографе и вижу только какую-то команду на 30мкс. В чём может быть проблема?

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

Подскажите, нафига в начале функции сброса отправка байта 0x43?

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

Бывает)

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

В файле onewire.c в строке 51 ругается на UART_OVERSAMPLING_8. Если заменить на UART_OVERSAMPLING_16, то компилятор проглатывает. Пока не стал разбираться, может, Вам будет проще увидеть в чём дело и существенно ли это.

Юрий
Юрий
Ответ на комментарий  Aveal
10 месяцев назад

STM32F103C8T6, STM32CubeIDE

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

И ещё вопрос. Под 1-wire хочу использовать USART2. Так понимаю, что для этого в onewire.c все USART1 надо заменить на USART2. Правильно ли это?

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