Датчики температуры DS18B20. Часть 3, Библиотека на Си

Скачать готовый код можно по ссылке 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 канале (клик)