[Home] [< Prev: Измерение температуры. Датчики температуры. Цифровой датчик DS18B20] [Next: Переходник RS-232 (COM-порт) - 1-Wire. SEARCH ROM >]

Пример использования цифрового датчика температуры DS18B20

Описываемый здесь цифровой термометр имеет диапазон измеряемых температур от -55ºC до +125ºC и выводит температуру на индикатор с разрешением 0.1ºC.

Используемые средства
Схема
Программная реализация протокола шины 1-Wire
Пример программы



Используемые средства

В этой статье рассмотрим программу для микроконтроллера, которая измеряет температуру с помощью цифрового датчика температуры DS18B20 и выводит результат измерения на 4-х разрядный семисегментный светодиодный индикатор FYQ-3641A (общий катод, предназначен для динамической индикации). Используется установленный на оценочной плате STM32VLDISCOVERY микроконтроллер STM32F100RBT6B.

О датчике DS18B20 рассказывалось в предыдущей статье. О семисегментных индикаторах и динамической индикации речь шла здесь.

Используемый микроконтроллер не имеет аппаратной поддержки интерфейса 1-Wire, который используется для взаимодействия с цифровым датчиком температуры DS18B20. Поэтому потребуется программная реализация интерфейса. Это не так сложно, потому что используемый протокол довольно прост. Всё что требуется - настроить используемый для обмена данными вывод микроконтроллера для работы в качестве выхода порта общего назначения с открытым стоком. Затем нужно лишь, аккуратно следуя требованиям протокола шины 1-Wire и с учётом набора команд датчика, формировать требуемые уровни сигнала на выходе микроконтроллера на заданные промежутки времени и считывать состояние шины в определённые моменты времени. Как управлять портами ввода-вывода микроконтроллера обсуждалось статье "GPIO: порты ввода/вывода общего назначения", а вопросы измерения интервалов времени обсуждались в статье о системном таймере микроконтроллера.

Схема

Индикатор подключён к микроконтроллеру в точности также, как в примере к статье "Семисегментный индикатор. Динамическая индикация".

Схема подключения DS18B20 к микроконтроллеру в цифровом термометре

Цифровой датчик температуры подключён к выводам PB0 и PB1. PB1 используется для питания датчика. Питание от вывода микроконтроллера используется для того, чтобы можно было в целях эксперимента программно переключаться из режима питания от внешнего источника в режим паразитного питания и обратно. PB0 используется для обмена данными. Резистор R14 является подтягивающим к высокому уровню для шины данных. Резисторы R13 и R15 защищают микроконтроллер во время аварийных режимов работы, например при коротком замыкании в линии с помощью которой датчик подключается к микроконтроллеру.

Программная реализация протокола шины 1-Wire

Для низкоуровневого взаимодействия с шиной будем использовать следующие 3 функции:

// Низкоуровневые функции для взаимодействия с датчиком.
// Реализованы для случая использования вывода PB0.

// Установить на шине 1.
inline void set_dq1()
{
    GPIOB->BSRR=GPIO_BSRR_BS0;
}

// Установить на шине 0.
inline void set_dq0()
{
    GPIOB->BRR=GPIO_BRR_BR0;
}

// Считывает и возвращает состояние шины (0 - false или 1-true).
inline bool get_dq()
{
    return GPIOB->IDR&GPIO_IDR_IDR0;
}

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

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

Выберем длительность интервалов равной 500 мкс - с небольшим запасом. После завершения импульса сброса, ведомые устройства ждут 15..60 мкс и отвечают импульсом присутствия, устанавливая на шине 0 на время 60..240 мкс. Так что импульс присутствия может начаться, самое позднее, через 60 мкс от завершения импульса сброса и может закончится (при каких-то условиях), самое раннее, через 15+60=75 мкс от завершения импульса сброса. Для считывания состояния шины выбираем значение в районе среднего между крайними значениями 60 и 75 мкс, например, 68 мкс.

Надо сказать, что для установления в шине высокого уровня под действием подтягивающего резистора после освобождения шины также требуется время, определяемое переходными процессами в RC-цепи. Например, если шина с подключёнными устройствами имеет ёмкость порядка 200 пФ, сопротивление подтягивающего резистора около 5 кОм, тогда время нарастания сигнала будет иметь порядок 1 мкс. В данном случае этим временем можно пренебречь.

Инициализация будет выполняться с помощью функции reset. Функция инициализации использует определённые ранее функции низкоуровневого доступа к 1-Wire шине. Также используются средства для формирования задержек с высокой точностью: функция ndelay и макрос USTON. Подробнее о формировании задержек говорится в статье о системном таймере.

// Инициализация: сброс и проверка присутствия устройств на шине.
// Возвращает true, если на шине есть ведомые устройства.
bool reset()
{
	const uint32_t RESET_TIME_LOW=500;
	const uint32_t RESET_TIME_HIGH=500;
	const uint32_t PRESENCE_DETECT_POINT=68;

	// Запрещаем исключения для точного измерения времени.
	bool primask=__get_PRIMASK();
	__disable_irq();

	set_dq0();        // Импульс сброса.
	ndelay(USTON(RESET_TIME_LOW));
	set_dq1();        // Освободить шину.
	ndelay(USTON(PRESENCE_DETECT_POINT));
	bool res=!get_dq();

	// Восстанавливаем состояние регистра запрета
	// исключений: не требуется высокая точность
	// при формировании следующего интервала времени.
	__set_PRIMASK(primask);

	ndelay(USTON(RESET_TIME_HIGH-PRESENCE_DETECT_POINT));
	return res;
}

Далее, нам потребуется реализация функций, которые формируют тайм-слоты для записи и для чтения.

Длительность тайм-слотов записи составляет минимум 60 мкс и максимум 120 мкс; выбираем среднее значение - 90 мкс. При формировании слота записи 1 важно успеть освободить шину, чтобы к моменту времени 15 мкс от начала слота, на шине установился уровень логической 1, но при этом в начале слота требуется удерживать на шине логический 0 не менее 1 мкс; выберем величину 7 мкс, оставив на переход шины к высокому уровню 8 мкс.

Для тайм-слотов чтения минимальная длительность определена в спецификации равной 60 мкс. Но можно выделить и больше, лишнее время просто увеличит время восстановления между соседними слотами и только повысит надёжность обмена данными. Гарантируется, что данные, которые устанавливает ведомое устройство на шине, действительны до момента времени 15 мкс от начала тайм-слота чтения. К этому моменту ведущее устройство должно успеть освободить шину, дождаться завершения переходных процессов (в случае, если ведущее устройство передаёт 1, в шине должен установиться высокий уровень под действием подтягивающего резистора) и определить состояние шины. Можно, например, установить 0 на шине на 6 мкс, освободить шину на 6 мкс, после чего прочитать состояние шины (это будет момент 12 мкс от начала тайм-слота).

Между тайм-слотами как в случае чтения, так и случае записи должны быть интервалы, когда шина остаётся свободной - время восстановления. В спецификации определена минимальная величина времени восстановления, равная 1 мкс. Максимальная длительность не ограничена. Для большей надёжности передачи, лучше сделать интервалы немного больше минимально допустимой величины. А ещё лучше - много больше, например, 10 мкс.

// Слот записи бита 0.
inline void slot_w0()
{
    set_dq0();
    ndelay(USTON(90));
    set_dq1();
}

// Слот записи бита 1.
inline void slot_w1()
{
    set_dq0();
    ndelay(USTON(7));
    set_dq1();
    ndelay(USTON(83));
}

// Запись бита + формирование интервала восстановления.
void slot_w(bool bit)
{
    bool primask=__get_PRIMASK();
    __disable_irq();

    if(bit)
        slot_w1();
    else
        slot_w0();

    __set_PRIMASK(primask);

    ndelay(USTON(10));
}

// Слот чтения бита от устройства.
bool slot_r()
{
    bool primask=__get_PRIMASK();
    __disable_irq();

    set_dq0();
    ndelay(USTON(6));
    set_dq1();
    ndelay(USTON(6));
    bool res=get_dq();

    __set_PRIMASK(primask);

    ndelay(USTON(90));
    return res;
}

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

// Передача данных осуществляется, начиная с младшего бита.

// Отправить байт устройству.
void send_byte(uint8_t b)
{
    for(uint32_t i=0; i<8; i++)
    {
        slot_w(b&1);
        b>>=1;
    }
}

// Получить байт от устройства.
uint8_t receive_byte()
{
    uint8_t res=0;
    for(uint32_t i=0; i<8; i++)
    {
        res>>=1;
        res|=slot_r()<<7;
    }
    return res;
}

Пример программы

Исходный код всего проекта

Теперь приведём пример функции измерения температуры для простейшего случая: используется внешнее питание датчика DS18B20, к 1-Wire шине подключён только один датчик. Будем считать, что DS18B20 сконфигурирован для работы в 12-битном режиме. Функция блокирующая, т.е. возвращает управление после завершения процесса измерения температуры, что требует до 750 мс времени, но во время выполнения функции разрешена обработка исключений. Функция возвращает двухбайтовое целое число со знаком, равное измеренной температуре в ºC, умноженной на 16. Такая форма возвращаемых данных выбрана для того, чтобы не связываться с числами с плавающей точкой, которые существенно "утяжеляют" программу. В случае ошибки функция возвращает значение 0x8000.

int16_t get_themp_x16()
{
    int16_t t=0x8000;
    if(reset())
    {
        send_byte(0xCC);    // Skip ROM.
        send_byte(0x44);    // Convert T.
        while(slot_r()==0)  // Ждём готовности.
            delay(75);      // Пауза, если ещё не готовы.
        if(reset())
        {
            send_byte(0xCC);      // Skip ROM.
            send_byte(0xBE);      // Read scratchpad.
            t=receive_byte();     // Читаем младший байт.
            t|=receive_byte()<<8; // Читаем старший байт.
        }
    }
    return t;
}

Далее приводится пример программы, в которой периодически с датчика считывается температура и полученные данные выводятся на индикатор.

int main()
{
    // Конфигурируем выводы для подключения индикатора.
    display_config();

    // Конфигурируем выводы для подключения датчика.
    // PB1 - out (power); PB0 - in/out (open drain).
    RCC->APB2ENR|=RCC_APB2ENR_IOPBEN;

    const uint32_t mask=GPIO_CRL_CNF0|GPIO_CRL_MODE0|
            GPIO_CRL_CNF1|GPIO_CRL_MODE1;
    GPIOB->CRL=GPIOB->CRL&~mask|
            (GPIO_CRL_MODE0_1|GPIO_CRL_CNF0_0|
            GPIO_CRL_MODE1_1);
    // DS18B20 Power on:
    GPIOB->BSRR=GPIO_BSRR_BS1;

    SysTick_Config(SystemCoreClock/1000);

    while(true)
    {
        int tx16=get_themp_x16();
        int D=tx16*100/16;
        if(D<0)
            D-=5;
        else
            D+=5;
        D/=10;
        display(D, 1);
        delay(500);
    }
}

Ничего сложного здесь нет. Единственное о чём стоит упомянуть, это то, что в программе операции выполняются только с целыми, что не мешает отображать дробные числа на индикаторе: датчик имеет разрешение 0.0625ºC, поэтому используется отображение результата на индикаторе с точностью 0.1ºC.

Если процессор не имеет аппаратной поддержки чисел с плавающей точкой (а используемый STM32F100RB не имеет, как и другие Cortex-M3), то операции с ними реализуются программно на основе целочисленной арифметики. При этом компилятор подключает к программе соответствующие функции. Размер программы существенно увеличивается, операции над вещественными числами программно выполняются медленно. Поэтому если нет острой необходимости, лучше обходиться без использования чисел с плавающей точкой на устройствах без их аппаратной поддержки.

Один из вариантов - использовать числа с фиксированной точкой. При этом сложение, вычитание таких чисел, умножение и деление их на целое (имеется в виду, что числа в операциях имеют одинаковое расположение точки) производится также, как если бы они были целыми, а при умножении или делении двух чисел с фиксированной точкой потребуется нормирование результата. Числа с фиксированной точкой, например, использует тот же самый датчик DS18B20, который в такой форме возвращает измеренную температуру.

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

В нашем случае мы хотим вывести на индикатор температуру T с точностью до 0.1ºC. Это то же самое, что вывести на индикатор целое число 10*T и отобразить на индикаторе точку перед младшим разрядом. Так как датчик даёт число T16=16*T (если рассматривать это число T16 как целое), то нам нужно отобразить на индикаторе целое число D=(10*T16)/16 и не забыть показать точку в нужном месте.

При целочисленном делении дробная часть отбрасывается. В программе используется чуть более сложный вариант с округлением. Для округления вычисляем результат как целое с точностью до сотых T100=(100*T16)/16, корректируем на ±5 в зависимости от знака и делим нацело на 10. Полученное D будет значением с округлением до десятых и масштабированное до целых.

Для вывода числа на индикатор и отображения точки используется функция display, определённая в файле dynamic_display.cpp проекта.

// Функция выводит на индикатор число n/(10**p).
// Иначе говоря, выводит целое число n (возможно,
// с необходимым числом ведущих нулей) и отображает
// точку после разряда p (p=0 соответствует точке
// после младшего разряда - в конце числа; если p=1,
// отображаемое число имеет один разряд после точки
// и т.д.).
// Если p<0, точка не отображается.
// Если p не меньше количества разрядов индикатора или
// число n не помещается в индикаторе, отображается
// сообщение об ошибке: "-E" для отрицательных чисел и
// "E" для неотрицательных.
// Возвращает true в случае успеха и false в случае ошибки.

bool display(int n, int p);

Исходный код всего проекта

author: hamper; date: 2016-02-04; modified: 2016-02-08
  @Mail.ru