Как реализовать протокол OneWire без внешних библиотек

Однажды мне нужно было подключить несколько датчиков DS18B20 к дешёвому 8-битному микроконтроллеру, на котором не было свободного места под тяжёлую библиотеку. Стандартные решения вроде OneWire.h или DallasTemperature.h занимали слишком много памяти программы для моего проекта. Пришлось разбираться в протоколе и писать всё вручную. Оказалось, что это не так сложно, как кажется, когда видишь тайминги в даташите впервые.

В этой статье я покажу, как работает OneWire на уровне сигналов, и как написать минимальную реализацию с нуля — для микроконтроллера с любым我家的 кодом, без внешних библиотек. Привожу код на C, который можно адаптировать почти под любую платформу.

Зачем вообще писать OneWire вручную

Библиотеки — это удобно. Но есть ситуации, когда без собственной реализации не обойтись:

  • мало флеш-памяти на микроконтроллере и каждый байт на счету;
  • нестандартный тайминг или нужно совместить OneWire с другой критичной по времени задачей;
  • вы переносите код на экзотический микроконтроллер, под которого готовой библиотеки нет;
  • хочется понимать, как работает протокол, а не просто вызывать готовые функции.

Понимание «железной» части протокола даёт и практический бонус: вы сразу видите, почему где-то не работает линейка датчиков, почему падают таймауты и как отладить проблему без логического анализатора.

Как устроен протокол OneWire — коротко и по делу

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

Протокол строится из трёх базовых операций:

  1. Reset Pulse — мастер тянет линию низко на 480–960 мкс, затем отпускает и слушает ответ от слейва (Presence Pulse), который длится 60–240 мкс.
  2. Write Bit — для передачи нуля мастер держит линию низко 60+ мкс, для единицы — короткий импульс 6–15 мкс низкого уровня с подтяжкой.
  3. Read Bit — мастер делает короткий низкий импульс, отпускает линию и читает её состояние в окне ~15 мкс после спада импульса.

Биты группируются в байты (младший бит первый), байты — в команды. Команда — это просто последовательность байтов, начинающаяся с кода операции.

Что вам нужно знать про тайминги

Вот минимально необходимые временные интервалы для стандартной скорости (16.3 кбит/с). Берите с запасом, микроконтроллеры не так быстры, как хотелось бы:

  • Reset: низкий уровень не менее 480 мкс.
  • После reset ожидание Presence: около 60–240 мкс, слейв держит линию низко.
  • Write «0»: низкий уровень 60–120 мкс.
  • Write «1»: низкий уровень 6–15 мкс, затем высокий уровень не менее 60 мкс для восстановления.
  • Read bit: низкий импульс 1–15 мкс, затем ожидание ~15 мкс до чтения, после чего пауза не менее 60 мкс.
  • Время восстановления (Recovery) между битами: не менее 1 мкс.

Если вы используете прерывания или DMA, убедитесь, что тайминг не «плывёт». Самый надёжный способ — написать функции напрямую в регистрах и использовать цикл задержки или таймер.

Минимальный код на C без библиотек

Ниже рабочий пример для AVR (ATmega328P), но его лко переписать на любую архитектуру — суть только в управлении пином и задержках.


// Настройки
#define OW_PIN 2
#define OW_PORT PORTB
#define OW_DDR DDRB
#define OW_PIN_REG PINB

// Заделка для задержки на 1 мкс (зависит от частоты процессора)
void delay_1us() {
    // для 16 МГц: несколько тактов
    __asm__ __volatile__("nop\n\t""nop\n\t");
}

void delay_us(uint16_t us) {
    while(us--) delay_1us();
}

void delay_ms(uint16_t ms) {
    while(ms--) delay_us(1000);
}

// Перевод пина в выход (с открытым коллектором / open-drain)
void ow_low() {
    OW_DDR |= (1 << OW_PIN);   // выход
    OW_PORT &= ~(1 << OW_PIN); // низкий уровень
}

// Перевод пина в вход с подтяжкой (отпускаем линию)
void ow_release() {
    OW_DDR &= ~(1 << OW_PIN);  // вход
    OW_PORT |= (1 << OW_PIN);  // внутренняя подтяжка (опционально)
}

// Чтение состояния пина
uint8_t ow_read() {
    return (OW_PIN_REG & (1 << OW_PIN)) ? 1 : 0;
}

// Сброс и ожидание ответа от слейва
uint8_t ow_reset() {
    ow_low();
    delay_us(480);
    ow_release();
    delay_us(60);
    uint8_t presence = ow_read(); // 0 = слейв присутствует
    delay_us(420); // дожидаемся окончания слейва presence pulse
    return presence;
}

// Отправка одного бита
void ow_write_bit(uint8_t bit) {
    ow_low();
    if(bit) {
        delay_us(6);
        ow_release();
        delay_us(64);
    } else {
        delay_us(60);
        ow_release();
        delay_us(2);
    }
}

// Чтение одного бита
uint8_t ow_read_bit() {
    ow_low();
    delay_us(1);
    ow_release();
    delay_us(14);
    uint8_t bit = ow_read();
    delay_us(45);
    return bit;
}

// Отправка байта (младший бит первый)
void ow_write_byte(uint8_t byte) {
    for(uint8_t i = 0; i < 8; i++) {
        ow_write_bit(byte & 0x01);
        byte >>= 1;
    }
}

// Чтение байта
uint8_t ow_read_byte() {
    uint8_t byte = 0;
    for(uint8_t i = 0; i < 8; i++) {
        byte >>= 1;
        if(ow_read_bit()) byte |= 0x80;
    }
    return byte;
}

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

Как читать данные с DS18B20 вручную

DS18B20 — это самый популярный датчик температуры на 1-Wire. Чтобы получить от него показания, нужно отправить ему строго определённую последовательность команд:

  1. Reset.
  2. Пропустить адресацию (Skip ROM — команда 0xCC), если датчик один.
  3. Запустить преобразование температуры (Convert T — команда 0x44).
  4. Подождать: для 12-битного разрешения это до 750 мс.
  5. Снова Reset.
  6. Skip ROM (0xCC).
  7. Читать scratchedpad (Read Scratchpad — команда 0xBE).
  8. Прочитать 9 байт данных (первые два — температура).

Температура в 16-битном формате: старшие 5 бит — знак, остальное — значение с шагом 0.0625°C. Полное преобразование выглядит так:


int16_t ds18b20_read_temp() {
    ow_reset();
    ow_write_byte(0xCC); // Skip ROM
    ow_write_byte(0x44); // Convert T
    delay_ms(750);       // ожидание при 12 бит
    ow_reset();
    ow_write_byte(0xCC); // Skip ROM
    ow_write_byte(0xBE); // Read Scratchpad
    uint8_t tl = ow_read_byte();
    uint8_t th = ow_read_byte();
    return (th << 8) | tl;
}

Если подключено несколько датчиков, вместо 0xCC используйте Match ROM (0x55) и дальше передаёте 8-байтный адрес конкретного датчика.

Реализация для разных платформ: отличия

Принцип везде одинаков, разница только в способах управления пином и задержках:

Платформа Особенности реализации
AVR (ATmega, ATtiny) Работа напрямую с регистрами DDR/PORT/PIN. Задержки через встроенную _delay_us() из <util/delay.h> или встроенный цикл.
ARM (STM32, nRF5x) Настройка GPIO через HAL или напрямую. Можно использовать DWT cycle counter для точных задержек без таймеров.
ESP32 / ESP8266 Нужно отключать прерывания на время обмена, иначе тайминг «поплывёт» из-за Wi-Fi стека. Используйте portENTER_CRITICAL / portEXIT_CRITICAL.
PIC, MSP430 Аналогично AVR, но следите за оптимизацией компилятора — он может вырезать пустые циклы задержки. Используйте volatile.

Что делать, если линия «не тянет» или данные приходят с ошибками

Вот реальные проблемы, с которыми я сталкивался, и способы их решения:

  • Нет ответа от датчика (presence = 1). Проверьте подтяжку 4.7 кОм к питанию. Без неё слейв не сможет оттянуть линию. Также проверьте, не закорочен ли датчик.
  • Все нули в ответе. Скорее всего, линия постоянно прижата к земле. Проверьте питание датчика и целостность соединений.
  • Температура всегда 85°C. Это значение по умолчанию после сброса. Значит, преобразование не запустилось — проверьте задержку после команды Convert T.
  • Показания «плывут» или случайные значения. Слишком длинный провод без экранирования, помехи на линии. Попробуйте удлину до 20–30 см или добавить параллельно конденсатор 100 нФ между питанием и землёй у датчика.
  • Не работает на скорости 115200 бод и выше. OneWire чувствителен к задержкам. Если вы используете RTOS или прерывания, отключайте их на время обмена или используйте аппаратный UART для генерации импульсов (есть трюки с отправкой 0xFF через UART для эмуляции reset).

Когда можно обойтись без собственной реализации

Не всегда писать всё с нуля — лучший путь. Вот когда лучше взять готовую библиотеку:

  • У вас достаточно памяти и нет жёстких ограничений по таймингам.
  • Нужна поддержка множества устройств на шине (поиск адресов, работа с разными семействами).
  • Вы делаете быстрый прототип и не хотите тратить время на отладку таймингов.
  • Библиотека уже проверена и работает стабильно в вашем окружении.

Но если вы попали в ситуацию, когда стандартные решения не подходили — вы теперь знаете, как сделать это самостоятельно.

Частые ошибки при самостоятельной реализации

Не забывайте: OneWire — это протокол с жёсткими временными ограничениями. Даже одна лишняя инструкция в цикле может сбить тайминг и привести к молчаливому сбою — датчик просто не ответит, и вы не сразу поймёте почему.

Вот список ошибок, которые чаще всего допускают при ручной реализации:

  1. Не отключают прерывания. На платформах с RTOS или частыми прерываниями (ESP32, STM32 с активным DMA) это приводит к сдвигу таймингов. Решение: обрамить критичный по времени код отключением прерываний.
  2. Используют _delay_us() с малыми значениями. Встроенные функции задержки часто округляют значение в большую сторону или не работают на 1–2 мкс. Лучше писать цикл вручную с учетом тактовой частоты.
  3. Забывают про подтяжку. Без резистора 4.7 кОм к питанию датчик физически не сможет вернуть линию в высокий уровень после того, как мастер её отпустил.
  4. Неправильно интерпретируют биты. OneWire передаёт младший бит первым. Если вы читаете байт в обратном порядке, получите бессмысленные данные.
  5. Не ждут завершения преобразования. После команды Convert T нужно ждать до 750 мс для 12-битного режима. Если прочитать данные раньше, вы получите старое значение или 0x85 (reset value).

Как проверить, что всё работает

Вот простой алгоритм отладки:

  1. Подключите светодиод к линии данных через транзистор или резистор и посмотрите, есть ли активность.
  2. Используйте осциллограф или логический анализатор (даже дешёвый за $10 с CH340) — вы увидите тайминги и сможете сравнить с даташитом.
  3. Начните с одного датчика на коротком проводе (10–20 см). Если работает — увеличивайте длину и количество устройств.
  4. Проверьте крайние температуры: на морозе и при нагреве — ёмкость линии меняется, и тайминги могут «поплыть».

Итог: что делать дальше

Если вы дошли до этого места, значит, у вас уже достаточно информации, чтобы написать свою реализацию OneWire. Вот краткий план действий:

  • Выберите пин и настройте его как открытый коллектор (open-drain) с подтяжкой.
  • Напишите функции reset, write_bit, read_bit — это база.
  • Добавьте write_byte и read_byte на основе битовых операций.
  • Для DS18B20 реализуйте последовательность Convert T → Wait → Read Scratchpad.
  • Протестируйте с одним датчиком, затем добавьте поддержку нескольких устройств.

Собственная реализация OneWire — это не просто экономия памяти. Это понимание того, как работает протокол на физическом уровне, которое поможет вам отладить любую проблему с 1-Wire шиной, даже если потом вы вернётесь к готовой библиотеке.

Radio-Blog.ru