Перейти к содержанию

Почему Arduino зависает на Wire.endTransmission()?

Обновлено Редакция GAW

Обмен по I²C в ядре Arduino для AVR идёт через автомат TWI микроконтроллера, а библиотека ждёт его в циклах while. До версии ядра с таймаутами эти циклы были бесконечными, и в 1.8.8 таймаут всё ещё выключен по умолчанию: twi_timeout_us = 0. Если шина не освобождается, скетч висит внутри endTransmission() или requestFrom(), и даже сторожевой таймер не всегда спасает: он перезагрузит плату, но после старта всё повторится.

Почему шина не освобождается

ПричинаЧто видно на линиях
Только внутренние подтяжки (их включает twi_init) или слишком высокоомные внешние для ёмкости шинылинии в единице, но фронты медленные, на длинной шине биты искажаются
Ведомый держит SDA после сброса Arduino посреди чтенияSDA постоянно в нуле, SCL в единице
Помеха на длинных проводахавтомат TWI считает, что шину занял другой ведущий
Ведомый растягивает такт дольше обычногоSCL в нуле десятки миллисекунд

Включить таймаут

#include <Wire.h>

void setup() {
  Wire.begin();
  Wire.setWireTimeout(25000, true);    // 25 ms, reset TWI on timeout
}

void loop() {
  Wire.beginTransmission(0x76);
  Wire.write(0xD0);
  uint8_t err = Wire.endTransmission();
  if (err == 5 || Wire.getWireTimeoutFlag()) {
    Wire.clearWireTimeoutFlag();       // bus stuck: recover, then retry later
  }
}

Коды endTransmission() по исходнику twi.c: 0 — успех, 1 — данные длиннее 32-байтного буфера, 2 — нет ответа на адрес, 3 — нет ответа на данные, 4 — прочая ошибка шины, 5 — таймаут. Что значит код 2 и как устроены регистры TWI, разобрано в уроке «Wire и SPI против регистров».

Таймаут не лечит причину, а только не даёт скетчу зависнуть. Подтяжки подбирают по ёмкости шины в калькуляторе подтяжек I²C, а если SDA держит ведомый, нужна процедура освобождения шины из ответа «I²C зависает после сброса микроконтроллера». Почему ведомый вправе держать SCL, объясняет термин «Растяжка такта».

Похожие вопросы

Какой таймаут выбрать?

Комментарий в Wire.cpp называет типичным 25 мс — наибольшая растяжка такта по SMBus — и замечает, что обычно годятся и меньшие значения. Если на шине есть ведомый, который подолгу растягивает такт, таймаут должен быть больше его наибольшей паузы, иначе Wire оборвёт исправную передачу.

Таймаут сработал, а следующие обмены тоже не проходят. Что делать?

Значит, линию держит ведомый. Сброс модуля TWI в Arduino это не исправит: нужно освободить шину, например девятью тактами SCL, как описано в спецификации I²C, или снять питание с датчика. Порядок действий — в ответе о зависании I²C после сброса микроконтроллера.

Есть ли таймаут у requestFrom()?

Тот же самый: setWireTimeout() действует на все ожидания в twi.c. При таймауте requestFrom() вернёт 0 прочитанных байт, а флаг getWireTimeoutFlag() покажет, что причина — таймаут, а не NACK.

Источники

  1. Arduino. ArduinoCore-avr 1.8.8: libraries/Wire/src/Wire.h, Wire.cpp, utility/twi.c — twi.c: twi_init() включает внутренние подтяжки SDA и SCL (digitalWrite(SDA, 1), digitalWrite(SCL, 1)), twi_disable() выключает; twi_timeout_us = 0 — проверка таймаута выключена (прежнее поведение), в комментарии — в будущем по умолчанию может стать 25 000 мкс со сбросом; циклы ожидания прерываются, только если twi_timeout_us > 0 и micros() − start больше таймаута; twi_writeTo возвращает 0 — успех, 1 — данные длиннее буфера, 2 — NACK на адрес, 3 — NACK на данные, 4 — другая ошибка (потеря арбитража, ошибка шины), 5 — таймаут; Wire.h: setWireTimeout(timeout = 25000, reset_with_timeout = false), BUFFER_LENGTH 32; Wire.cpp: при таймауте выставляется флаг getWireTimeoutFlag(), сбрасываемый clearWireTimeoutFlag(); таймаут может сработать и при растяжке такта, типичное значение 25 мс — наибольшая растяжка по SMBus
  2. NXP. UM10204 I2C-bus specification and user manual (Rev. 7.0, 1 October 2021) — разд. 3.1.16 Bus clear: если SDA залипла в низком уровне, ведущий выдаёт девять тактов SCL, и устройство должно отпустить линию, иначе — аппаратный сброс или выключение питания; если залипла SCL — аппаратный сброс устройств, а без него — выключение питания, чтобы сработал внутренний сброс по включению

Нашли ошибку?