Скачать готовый код можно по ссылке https://cloud.as.life/s/mtCccJ5HXP4iLCL
Я к этому уроку написал простую библиотеку, которая позволит работать как с одним, так и с несколькими датчиками температуры на шине 1-Wire. И поскольку ссылку на код я привел выше, вы можете скачать, открыть, смотреть. А лучше всего подключите эту библиотеку к своему проекту, разведка боем будет. Я же детально опишу что там к чему.
Для начала настройте макросы конкретно под ваш проект.
#define DS_TIM htim3 // Хэндлер таймера общего назначения. Таймер должен быть включен в кубе
#define DS_RXTX_PORT OWIRE_GPIO_Port // Порт пина RX/TX 1-Wire (должен быть настроен как GPIO Output Open Drain)
#define DS_RXTX_PIN OWIRE_Pin // Пин RX/TX 1-Wire PB10
#define DS_TIM_FREQ 32000000U // Частота на которой работает таймер, с учетом всех делителей
Этот пример написан для одноногого управления. Настройте линию порта как Open-Drain Output High Speed.
Для задержки я использовал таймер общего назначения. На мой взгляд, опираться на счетчик таймера гораздо надежнее, нежели считать такты процессора. Если бы мы считали такты и во время отсчета задержки произошло бы прерывание, то мы бы уже ошиблись в подсчете тактов. А таймер не прервется. Вот так выглядит непосредственно задержка.
// Ждать определенное число отсчетов таймера
void DS_WAIT(uint16_t cnt) {
DS_TIM_START();
while (DS_TIM.Instance->CNT < cnt) __NOP();
DS_TIM_STOP();
}
Зная кол-во микросекунд для задержки, мы можем вычислить кол-во тиков
#define DS_CNT(n) (DS_TIM_FREQ / 1000000UL) * (n - 3)
От указанного кол-ва микросекунд мы вычитаем 3 мкс. Это время, затрачиваемое на запуск таймера.
А теперь, когда мы можем выполнить точную задержку в микросекундах, нетрудно и послать датчику любой бит
// запись лог 1
void DS_WriteBit1(void) {
DS_TX(DS_OFF);
DS_WAIT(DS_CNT(12));
DS_TX(DS_ON);
DS_WAIT(DS_CNT(244));
}
// Запись лог 0
void DS_WriteBit0(void) {
DS_TX(DS_OFF);
DS_WAIT(DS_CNT(64));
DS_TX(DS_ON);
DS_WAIT(DS_CNT(180));
}
Логические нолики и единички мы записываем группой - это уже байт.
// Запись байта (команда или данные)
void DS_WriteByte(uint8_t data) {
// Запись ведется младшим битом вперед
for (uint8_t i = 0; i < 8; i++) {
if ((data >> i) & 1) DS_WriteBit1();
else DS_WriteBit0();
}
}
Окей, посылать байты мы умеем. А читать их тоже нужно:
// Читает один бит на шине 1-Wire
uint8_t DS_ReadBit(void) {
uint16_t res;
DS_TX(DS_OFF);
DS_WAIT(DS_CNT(12));
DS_TX(DS_ON);
//for (res = 0; res < 34; res++); // Примитивная задержка для очень быстрых процессоров
if (!DS_RX()) res = 0; // Если шина провалена, значит лог 0
else res = 1; // Если шина поднялась, значит лог 1
DS_WAIT(DS_CNT(180));
return res;
}
Один бит прочесть - это уже большое достижение. Обратите внимание на закомментированную строчку с примитивной задержкой. Я должен объяснить, для чего там она нужна. Мы просаживаем шину на 12 мкс и отпускаем, если она остается просаженной - значит датчик ее просадил, передаёт лог. 0. Если датчик не просадил шину - то она будет поднята, на ней будет напряжение. Провод шины 1-Wire, линия порта, сам датчик - обладают какой-то мизерной ёмкостью, а это значит, что напряжение поднимется не мгновенно, а в течение какого-то времени, может 500 нс, может несколько единиц мкс. Если емкость шины велика, и/или процессор слишком быстрый, то мы будем читать всегда лог. 0 с невосстановленной шины. Для этого предусмотрена крошечная задержка.
Умея читать бит - читаем байт.
// Читает 1 байт
uint8_t DS_ReadByte(void) {
uint8_t temp = 0;
// Чтение ведется младшим битом вперед
for (uint8_t i = 0; i < 8; i++) {
temp |= (DS_ReadBit() << i);
}
return temp;
}
Вот такой механизм чтения и передачи цифровых данных.
Теперь разберемся в том, как пользоваться библиотекой.
Работа с датчиком начинается с функции инициализации.
// Инициализация термометра
DS_ERROR DS_Init(DS_DEPTH depth) {
DS_TX(DS_ON); // Запитать шину
HAL_Delay(10);
if (!DS_RX()) return ds_error_short; // Если шина в кз, выйти с ошибкой
uint8_t config_reg = 0x1F; // 0b11111
config_reg |= ((uint8_t)depth << 5);
DS_TIM.Instance->ARR = 65535;
// запись настроек в датчики
if (!DS_Reset()) return ds_error_empty; // Если на шине нет устройств, выйти с ошибкой
DS_WriteByte(DS_CMD_SKIP_ROM); // Пропуск ROM
DS_WriteByte(DS_CMD_WRITE_RAM); // Запись памяти (регистров)
DS_WriteByte(0); // TH
DS_WriteByte(0); // TL
DS_WriteByte(config_reg); // Настройка разрядности датчиков
return ds_error_none;
}
В эту функцию мы передаем 1 параметр - разрешение датчика. DS18B20 можно запустить 4 разрешениях:
9 бит. Точность 0.5 градуса, время измерения ~100 мс;
10 бит. Точность 0.25 градусов, время измерения ~200 мс;
11 бит. Точность 0.125 градусов, время измерения ~400 мс;
12 бит. Точность 0.0625 градусов, время измерения ~800 мс.
Перечисление режимов описано в заголовочном файле и выглядит так:
typedef enum {
ds_depth_9bit = 0,
ds_depth_10bit,
ds_depth_11bit,
ds_depth_12bit
} DS_DEPTH;
Если шина 1-Wire не в коротком замыкании и если на ней есть хотя бы 1 датчик, то инициализация пройдет без ошибок.
Любая операция с датчиком всегда начинается с импульса сброса.
// Сброс датчика. Вернет true, если датчик отвечает
bool DS_Reset(void) {
bool res = false;
// Подать импульс сброса
DS_TX(DS_OFF);
DS_WAIT(DS_CNT(485));
DS_TX(DS_ON);
// Ждать импульс присутствия
DS_TIM_START();
while(DS_TIM_CNT < DS_CNT(500)) { // В течение 480 мкс датчик должен ответить импульсом присутствия
if (!DS_RX()) { // Если возник импульс присутствия
res = true;
break; // Покинуть цикл
}
}
DS_TIM_STOP();
if (res) {
DS_WAIT(DS_CNT(444));
}
else {
DS_TX(DS_ON);
DS_WAIT(DS_CNT(180));
}
return res;
}
Мы просаживаем шину на 480+ микросекунд, а затем отпускаем. Так вот, в течение 480 мкс после отпускания, датчик должен просадить шину в ответ. Если этого не произошло, то образуется ошибка - нет датчиков на шине. Если датчик ответил импульсом присутствия, то он готов слушать нашу команду.
Практическое использование библиотеки
Пользователю, для работы с представленной библиотекой, в заголовочном файле доступны всего 3 функции.
DS_ERROR DS_Init(DS_DEPTH depth); // Инициализация термометра
// Далее data - это 8 байт ROM искомого датчика. Если ROM == 0, то без ROM
DS_ERROR DS_ReadROM(uint8_t * data); // Прочитать уникальный идентификатор датчика.
DS_ERROR DS_GetTemperature(uint8_t * data, int8_t * temper); // Забрать температуру из RAM и запустить очередное конвертирование
1. DS_Init - инициализация датчика(-ов)
Инициализация делается одним обращением ко всем датчикам. Мы передаем по шине 5 байт.
DS_WriteByte(DS_CMD_SKIP_ROM); // Пропуск ROM
DS_WriteByte(DS_CMD_WRITE_RAM); // Запись памяти (регистров)
DS_WriteByte(0); // TH
DS_WriteByte(0); // TL
DS_WriteByte(config_reg); // Настройка разрядности датчиков
> Пропуск ROM - передать всем на шине, не обращаясь к кому-то конкретно.
> Запись памяти - конфигурирование, грубо говоря.
> Запись регистра TH - уставка порогового значения для аварии перегрева
> Запись регистра TL - уставка порогового значения для аварии переохлаждения
> Настройка разрешения (9, 10, 11 или 12 бит)
2. DS_ReadROM(uint8_t * data) - чтение уникального ID датчика на шине
Для того чтобы забрать ID, нужно чтобы датчик на шине был всего один! Прочитанный ID запишется в массив data[8].
3. DS_GetTemperature(uint8_t * data, int8_t * temper)
Так мы читаем значение температуры из датчика с уникальным ID data[8], а затем стартуем новый замер температуры для этого же датчика.
Здесь результат измеренной температуры запишется в *temper и это будет целое число. Конкретно в этой библиотеке не рассматриваются дробные значения температур. Не было задачи получать доли градусов.
temper_raw = (uint16_t)DS_ROM[0];
temper_raw |= (uint16_t)DS_ROM[1] << 8;
temper_raw = temper_raw >> 4;
DS_Temper = (int8_t)(temper_raw & 0xFF);
Так я просто убираю (не использую) 4 младших бита результата (сдвигаю на 4 бита вправо - значит делю на 16 пренебрегая остатком) и получаю целочисленное значение температуры. Если же вам интересна дробная часть значения, то делите результат на 16. И результат, и операнд будут вещественного типа
temper_raw = (uint16_t)DS_ROM[0];
temper_raw |= (uint16_t)DS_ROM[1] << 8;
DS_Temper = (float)temper_raw / 16.0f;
На этом всё.
Обсудить в нашем Telegram канале (клик)