Однажды мне нужно было подключить несколько датчиков DS18B20 к дешёвому 8-битному микроконтроллеру, на котором не было свободного места под тяжёлую библиотеку. Стандартные решения вроде OneWire.h или DallasTemperature.h занимали слишком много памяти программы для моего проекта. Пришлось разбираться в протоколе и писать всё вручную. Оказалось, что это не так сложно, как кажется, когда видишь тайминги в даташите впервые.
В этой статье я покажу, как работает OneWire на уровне сигналов, и как написать минимальную реализацию с нуля — для микроконтроллера с любым我家的 кодом, без внешних библиотек. Привожу код на C, который можно адаптировать почти под любую платформу.
Зачем вообще писать OneWire вручную
Библиотеки — это удобно. Но есть ситуации, когда без собственной реализации не обойтись:
- мало флеш-памяти на микроконтроллере и каждый байт на счету;
- нестандартный тайминг или нужно совместить OneWire с другой критичной по времени задачей;
- вы переносите код на экзотический микроконтроллер, под которого готовой библиотеки нет;
- хочется понимать, как работает протокол, а не просто вызывать готовые функции.
Понимание «железной» части протокола даёт и практический бонус: вы сразу видите, почему где-то не работает линейка датчиков, почему падают таймауты и как отладить проблему без логического анализатора.
Как устроен протокол OneWire — коротко и по делу
Однопроводный интерфейс (1-Wire) использует одну линию данных, по которой так же может подаваться питание (паразитное питание). Все обмены инициируются мастером, который может либо читать, либо писать в линию.
Протокол строится из трёх базовых операций:
- Reset Pulse — мастер тянет линию низко на 480–960 мкс, затем отпускает и слушает ответ от слейва (Presence Pulse), который длится 60–240 мкс.
- Write Bit — для передачи нуля мастер держит линию низко 60+ мкс, для единицы — короткий импульс 6–15 мкс низкого уровня с подтяжкой.
- 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. Чтобы получить от него показания, нужно отправить ему строго определённую последовательность команд:
- Reset.
- Пропустить адресацию (Skip ROM — команда 0xCC), если датчик один.
- Запустить преобразование температуры (Convert T — команда 0x44).
- Подождать: для 12-битного разрешения это до 750 мс.
- Снова Reset.
- Skip ROM (0xCC).
- Читать scratchedpad (Read Scratchpad — команда 0xBE).
- Прочитать 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 — это протокол с жёсткими временными ограничениями. Даже одна лишняя инструкция в цикле может сбить тайминг и привести к молчаливому сбою — датчик просто не ответит, и вы не сразу поймёте почему.
Вот список ошибок, которые чаще всего допускают при ручной реализации:
- Не отключают прерывания. На платформах с RTOS или частыми прерываниями (ESP32, STM32 с активным DMA) это приводит к сдвигу таймингов. Решение: обрамить критичный по времени код отключением прерываний.
- Используют _delay_us() с малыми значениями. Встроенные функции задержки часто округляют значение в большую сторону или не работают на 1–2 мкс. Лучше писать цикл вручную с учетом тактовой частоты.
- Забывают про подтяжку. Без резистора 4.7 кОм к питанию датчик физически не сможет вернуть линию в высокий уровень после того, как мастер её отпустил.
- Неправильно интерпретируют биты. OneWire передаёт младший бит первым. Если вы читаете байт в обратном порядке, получите бессмысленные данные.
- Не ждут завершения преобразования. После команды Convert T нужно ждать до 750 мс для 12-битного режима. Если прочитать данные раньше, вы получите старое значение или 0x85 (reset value).
Как проверить, что всё работает
Вот простой алгоритм отладки:
- Подключите светодиод к линии данных через транзистор или резистор и посмотрите, есть ли активность.
- Используйте осциллограф или логический анализатор (даже дешёвый за $10 с CH340) — вы увидите тайминги и сможете сравнить с даташитом.
- Начните с одного датчика на коротком проводе (10–20 см). Если работает — увеличивайте длину и количество устройств.
- Проверьте крайние температуры: на морозе и при нагреве — ёмкость линии меняется, и тайминги могут «поплыть».
Итог: что делать дальше
Если вы дошли до этого места, значит, у вас уже достаточно информации, чтобы написать свою реализацию OneWire. Вот краткий план действий:
- Выберите пин и настройте его как открытый коллектор (open-drain) с подтяжкой.
- Напишите функции reset, write_bit, read_bit — это база.
- Добавьте write_byte и read_byte на основе битовых операций.
- Для DS18B20 реализуйте последовательность Convert T → Wait → Read Scratchpad.
- Протестируйте с одним датчиком, затем добавьте поддержку нескольких устройств.
Собственная реализация OneWire — это не просто экономия памяти. Это понимание того, как работает протокол на физическом уровне, которое поможет вам отладить любую проблему с 1-Wire шиной, даже если потом вы вернётесь к готовой библиотеке.
