Уважаемые посетители, коллеги и друзья, традиционно снова рад приветствовать вас на нашем сайте 🤝 Давеча разговорились мы с пользователем TQFP на тему разрастания и продвижения нашего сообщества, в результате чего он выразил желание оформить небольшой курс по подключению простейших, но что более важно, наиболее популярных датчиков к STM32. Основные цели в этом действе двояки - это как облегчение старта и понижение порога вхождения для тех, кто только начинает ступать на тропу работы с микроконтроллерами, так и потенциальная оптимизация/доработка для опытных профессионалов, так или иначе уже использующих то, о чем пойдет речь. Собственно, TQFP решил взять за базу готовый набор датчиков, который у него есть в наличии под рукой - а именно этот (набор датчиков 37 в 1):
Многие входящие в состав набора компоненты слишком просты в использовании для того, чтобы посвящать им отдельный материал, поэтому мы договорились, чтобы в таком случае уделять больше внимания каким-либо скрытым на первый взгляд деталям, либо интересным практическим применениям. Кроме того, я на фоне данной беседы вспомнил, что и у меня где-то пылится аналогичный набор, так что я тоже сделаю пару статей на эту тему. Но в данном случае несколько проявлю наглость и на себя возьму только то, что мне более-менее интересно самому )
Ну и, в общем-то, одна из ближайших по плану статей у меня о реализации ПИД-регулятора на 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 для отладки:
Тактовые частоты зададим так:
Но опять же, в данном конкретном проекте это все не слишком существенно. Больше внимания уделим непосредственно USART'у. Я взял USART1, соответственно, обмен данными будет осуществляться через PA9. Настраиваем следующим образом:
Касаемо скорости передачи данных еще поговорим, по умолчанию ставим 115200 Бит/с. На этом заканчиваем с частью, посвященной периферии, и перемещаемся к программной реализации.
Принцип работы 1-Wire.
Сразу же структурируем проект подобающий образом, создав файлы для работы с шиной (onewire.c/onewire.h). В них инкапсулируем всю низкоуровневую часть для обмена данными, на базе которой впоследствии надстроим работу конкретно с датчиком DS18B20:
И начнем с разбора того, как мы собственно будем взаимодействовать с 1-Wire. И все процессы будут заключены всего лишь в 4-х возможных операциях:
- Команда сброса
- Передача бита 1.
- Передача бита 0.
- Чтение бита.
Так что планомерно и систематично добавим поддержку перечисленного, и на этом можно будет переходить к следующему этапу. Итак, команда сброса... Официальная документация говорит нам следующее:
То есть команда сброса представляет из себя не что иное, как низкий уровень на линии на протяжении обозначенного времени. Вспоминаем, что мы решили использовать USART и производим необходимые расчеты. Классический метод заключается в том, что при отправке команды сброса, USART переконфигурируется на скорость передачи данных 9600 бит/с. При данной скорости время передачи одного бита составляет:
T_{bit} = \frac{1000000 \medspace мкс}{9600 } = 104 \medspace мкс
Итого - для генерации Reset'а отправим в USART байт 0xF0 (0b11110000). Что это нам даст? Тут все просто - биты будут передаваться, начиная с младшего, значит первой будет выдана последовательность из четырех нулей. Приплюсовываем к этому нулевой старт-бит USART'а и получаем 5 бит, что эквивалентно низкому уровню на шине на протяжении 5 * 104 мкс = 520 мкс:
Это в свою очередь полностью соответствует внешнему виду команды Reset 👍 После отправки 0xF0 встаем на прием, и в том случае, если на шине присутствуют другие устройства, мы примем значение, не равное тому, что мы отправили. Если же устройств на шине нет, то примем ровно то, что отправили, а именно 0xF0, поскольку фактически Tx и Rx USART'а у нас замкнуты (одна линия для передачи данных по шине). Таким вот способом будет осуществляться обнаружение подключенных датчиков.
На этом с первым пунктом интерфейсной части успешно прощаемся. Переходим к передачи информационных битов. Передача единицы:
Как видите, механизм при работе с 1-Wire един и неизменен - по умолчанию линия подтянута к питанию, устройства же взаимодействуют выдачей на линию нуля. Вся разница только в длительностях. В данном случае для передачи единицы необходимо подать ноль на время, соответствующее интервалу 1-15 мкс. Да, кстати, скорость 9600 бит/с будет использоваться только и исключительно для команды Reset. Для оставшихся трех пунктов переконфигурируем снова на 115200 бит/с. С этим наглядно разберемся на практическом примере.
На скорости 115200 бит/с передача одного бита это:
T_{bit} = \frac{1000000 \medspace мс}{115200} = 8.7 \medspace мс
Поэтому передавать будем байт 0xFF, что вкупе со старт-битом даст нам требуемое:
Сразу же рассмотрим и передачу нуля:
Все то же самое, разница, как мы уже обсудили, только во временных интервалах. Передаем в USART 0x00, имеем:
Все четко! Остается только один пункт - чтение информационного бита. И снова все завязано на физическом устройстве шины, при котором линия подтянута вверх, а устройства при возникновении такого желания опускают ее вниз принудительно. Для чтения осуществляем выдачу 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.
Спасибо большое за статью. Написано всё очень просто и разложено по полочкам.
Благодарю)
Добрый день, спасибо за материал, мкс поправьте в формулах, а то я ступор мозговины поймал: время передачи одного бита делю 1/9600 = 1,04*10^-4 секунд = 104мкс.
Благодарю )
Еще бы статью с bit-bang тоже бы сделать. Через прерывания. Было бы интересно почитать.
Есть еще способ через таймер шим
файлы onewire.c or onewire.h зеркало. у кого как и что могло получиться , хз
А, ну вставил одно и то же под спойлер случайно, сейчас обновлю. В архиве полный проект со всеми файлами - так и работает.
Подскажите, пожалуйста, я пробовал перенести на STM32F7xx и столкнулся с ошибкой, что функция UART_BRR_SAMPLING8 объявлена неявно и недействительна на С99. Пробовал искать эту функцию и нашёл только в HAL_StatusTypeDef UART_SetConfig(UART_HandleTypeDef *huart) нечто похожее - UART_DIV_SAMPLING8, однако, если использовать её, то в отладчике ничего не показывает. Пробовал посмотреть на осциллографе и вижу только какую-то команду на 30мкс. В чём может быть проблема?
На самом деле проще всего - посмотреть в HAL, как для этого семейства они baudrate настраивают, и кусок этот оттуда скопировать )
Подскажите, нафига в начале функции сброса отправка байта 0x43?
Прекрасный вопрос, но идей нет )
Единственное объяснение пока вижу, что я для отладки добавил, а потом забыл убрать... Сейчас еще подумаю на предмет тайного смысла, потом удалю и обновлю проекты/код.
Спасибо за замечание )
Бывает)
В файле onewire.c в строке 51 ругается на UART_OVERSAMPLING_8. Если заменить на UART_OVERSAMPLING_16, то компилятор проглатывает. Пока не стал разбираться, может, Вам будет проще увидеть в чём дело и существенно ли это.
Это для какого контроллера?
STM32F103C8T6, STM32CubeIDE
Да, функцию SetBaudrate() надо адаптировать вручную под использующийся контроллер тогда.
И ещё вопрос. Под 1-wire хочу использовать USART2. Так понимаю, что для этого в onewire.c все USART1 надо заменить на USART2. Правильно ли это?
В onewire.c UART_HandleTypeDef *huart идет аргументом в функциях, поэтому нужный UART просто указывается в момент вызова функции, то есть допустим:
или
В ds18b20.c использующийся модуль в момент инициализации однократно задаем.