Шина I2C. Подробности программной реализации
Устройства, связанные через I2C, должны поддерживать определенную последовательность событий. Каждое событие соответствует определенному способу управления линиями тактовой синхронизации (SCK) и данных (SDA); как обсуждалось в статьях, приведенных в списке «Вспомогательная информация», эти два сигнала являются единственным средством, с помощью которого устройства на шине могут обмениваться информацией. Мы будем рассматривать одну информационную последовательность как «транзакцию»; это слово более уместно, чем «передача», поскольку каждая транзакция включает в себя как переданные данные, так и полученные данные, хотя в некоторых случаях единственными полученными данными являются бит подтверждения (ACK) или не-подтверждения (NACK), детектируемые ведущим устройством. Следующая временная диаграмма показывает типовую транзакцию I2C.

Обратите внимание на следующее:
- Пунктирная линия, соответствующая длительности логической единицы в тактовом сигнале, напоминает нам, что логическая единица (и для SCL, и для SDA) является «рецессивным» состоянием – другими словам, сигнал доходит до высокого логического уровня с помощью подтягивающего резистора. «Доминантное» состояние – это логический ноль, потому что сигнал будет на низком логическом уровне только тогда, когда устройство действительно приводит его к состоянию логического нуля.
- Транзакция начинается со «стартового бита». Каждая I2C транзакция должна начинаться со стартового бита, который определятся как спадающий фронт на линии SDA, в то время как линия SCL находится в состоянии логической единицы.
- Транзакция заканчивается «стоповым битом», определяемым как нарастающий фронт на линии SDA, в то время как линия SCL находится в состоянии логической единицы. Транзакции I2C должны заканчиваться стоповым битом; однако, как будет рассказано позже, на шине могут появиться несколько стартовых битов до того, как будет сгенерирован стоповый бит.
- Данные действительны, когда на линии синхронизации установлена логическая единица, и изменяют состояние, когда на линии синхронизации установлен логический ноль; цифровые системы связи обычно ориентируются на изменения состояния на линиях, поэтому на практике данные считываются по нарастающему фрону на линии синхронизации и обновляются по спадающему фронту на линии синхронизации.
- Обмен информацией происходит по одному байту за раз, начиная со старшего значащего бита; и за каждым байтом следует ACK или NACK.
- Вы можете ожидать, что ACK будет обозначаться логической единицей, а NACK – логическим нулем, но в данном случае это не так. ACK соответствует логическому нулю, а NACK – логической единице. Это необходимо, потому что логическая единица является рецессивным состоянием – если ведомое устройство не работает, то сигнал, соответственно, будет поднят до NACK. Аналогично, ACK (указывается доминантным логическим нулем) может быть передан только в том случае, если устройство работает и готово продолжить транзакцию.
Следующий список описывает последовательность событий в вышеуказанной транзакции:
- Ведущее устройство генерирует стартовый бит, чтобы начать транзакцию.
- Ведущее устройство передает 7-битный адрес, соответствующий ведомому устройству, с которым оно хочет установить соединение.
- Последним битом в первом однобайтовом сегменте является индикатор чтения/записи. Мастер устанавливает этот бит в логическую единицу, если он хочет считывать данные с ведомого устройства, или в логический ноль, если хочет записать данные в ведомое устройство.
- Следующий байт – это первый байт данных. Он приходит либо от ведущего, либо от ведомого устройства, в зависимости от состояния бита чтения/записи. Как обычно, у нас есть 8 бит данных, начинающихся со старшего значащего бита.
- За байтом данных следует ACK или NACK, сгенерированный ведущим устройством, если это транзакция чтения, или ведомым устройством, если это транзакция записи. ACK и NACK могут означать разные вещи в зависимости от прошивки или низкоуровневой аппаратной схемы взаимодействующих устройств. Например, мастер может использовать NACK, чтобы сказать: «это последний байт данных», или если ведомое устройство знает, сколько данных должно быть отправлено, оно может использовать ACK для подтверждения того, что данные были успешно получены.
- Транзакция завершается стоповым битом, сгенерированным ведущим устройством.
Сколько байт?
Каждая транзакция начинается одинаково: стартовый бит, адрес, чтение/запись, ACK/NACK. После этого любое количество байт данных может быть отправлено от мастера к ведомому устройству или от ведомого к мастеру, причем после каждого байта следует ACK или NACK. NACK может быть полезен как способ сказать: «прекрати отправку данных!». Например, мастер может захотеть получать непрерывный поток данных от ведомого устройства (например, датчик температуры); за каждым байтом следует ACK, и если мастеру необходимо обратить внимание на что-то еще, он может послать ведомому устройству NACK и начать новую транзакцию, когда будет снова готов.

Старт без стопа
Протокол I2C допускает нечто, называемое условием «повторного старта». Это происходит, когда мастер инициирует транзакцию со стартовым битом, а затем инициирует новую транзакцию через другой стартовый бит без промежуточного стопового бита следующим образом:

Эта функция может использоваться всякий раз, когда одному ведущему устройству необходимо выполнить две или более отдельных транзакции. Однако есть ситуация, когда условие повторного старта особенно удобно.
Допусти, у вас есть ведомое устройство, которое хранит информацию в банке регистров. Вы хотите запросить данные из регистра с адресом 160, 0xA0 в шестнадцатеричном формате. Протокол I2C не позволяет мастеру отправлять данные и получать данные в одной транзакции. Следовательно, вы должны выполнить транзакцию записи, чтобы указать адрес регистра, а затем отдельную транзакцию чтения для извлечения данных. Хотя этот подход может привести к проблемам, поскольку мастер освобождает шину в конце первой транзакции, и, таким образом, другой мастер может занять шину и не дать первому мастеру получить нужные ему данных. Кроме того, второй мастер может взаимодействовать с тем же ведомым устройством и задать другой адрес регистра. Если первый мастер затем займет шину и прочитает данные без повторного указания адреса регистра, он будет считывать неправильные данные! Если второй мастер затем попытается выполнить транзакцию чтения в своей процедуре «запись и затем чтение», то это также закончится чтением неверных данных! Этого системного сбоя стоит ожидать, но, к счастью, условие повторного старта может предотвратить этот беспорядок, инициировав вторую транзакцию (чтение) без освобождения шины.

Когда ведущие устройства не могут уживаться вместе
Часть того, что делает I2C настолько универсальной, – это поддержка нескольких ведущих устройств. Но, как показывает предыдущий раздел, ведущие устройства не всегда хорошо работают вместе. Логика I2C устройства должна быть в состоянии определить, свободна ли шина; если шину занял другой мастер, то устройство до запуска своей собственной транзакции ждет, пока не завершится текущая транзакция. Но что происходит, когда два (или более) мастера пытаются инициировать транзакцию одновременно? I2C обеспечивает эффективное и удивительно простое решение этой неприятной, если бы она случилась, проблемы. Этот процесс называется «арбитраж», и он полагается на гибкость схемы шины I2C с открытым стоком: если один мастер пытается привести сигнал к логической единице, а другой мастер пытаются привести сигнал к логическому нулю, то «выиграет» мастер с логическим нулем, и, кроме того, «проигравший» может обнаружить, что фактическое состояние на выходе отличается от состояния, которое он хотел установить:

Эта схема показывает основу арбитража I2C; процесс происходит следующим образом:
- Оба мастера генерируют стартовые биты и осуществляют свои передачи.
- Если мастера выбирают на линии одни и те же логические уровни, ничего не происходит.
- Как только мастера пытаются установить на линии разные логические уровни, мастер, установивший на линии логический ноль, объявляется победителем; а проигравший обнаруживает несоответствие логических уровней и отказывается от своей передачи.
Потратьте минутку, чтобы оценить простоту и эффективность этого механизма:
- Победитель продолжает свою передачу без перерыва – нет поврежденных данных, нет конфликта устройств, нет необходимости в перезапуске транзакции.
- Теоретически проигравший мог контролировать адрес ведомого устройства в ходе процесса арбитража и фактически принимать правильные данные, если так оказалось, что он и является этим ведомым устройством.
- Если конкурирующие мастера запрашивают данные от одного и того же ведомого устройства, процесс арбитража не требует необязательного прерывания транзакции – не будет обнаружено ошибки, и ведомое устройство выведет свои данные на шину, чтобы их могли получить несколько мастеров.
Заключение
В данной статье рассмотрены важные детали I2C, которые влияют на разработку программного или низкоуровнего аппаратного обеспечения. Если ваш микроконтроллер включает аппаратные модули I2C или SMBus, то некоторые детали реализации будут обрабатываться автоматически. Хотя это и удобно, но не оправдывает безграмотность потому, что вам всё равно нужно знать хотя бы немного (и, возможно, немного больше) о том, как на самом деле работает I2C. Кроме того, если вы когда-нибудь окажетесь на необитаемом острове без аппаратных модулей I2C, представленная здесь информация поможет вам на пути к разработке чисто программных I2C функций (с так называемой побитовой обработкой).
Где эти ACK, NAK, STALL
Главное это сразу понять , что в коде , сгенерированном CubeMX явного вызова функций посылки ACK , NAK, .. вы НЕ найдете.
Эти пакеты формируются автоматически контроллером USB встроенным в STM32. В механизме прерываний и в коллбек функциях вы тоже не найдете эти команды посылки ACK , NAK, .. и т.д.
Также скорее всего вы НЕ увидите эти пакеты в программах типа USB анализаторов и это вас собъет с толку.
Наверное лучше эти пакеты воспринимать как состояние контроллера USB.
Т.е. если контроллер принял команду и долго ее выполняет , то он шлет на запрос (ну когда же ты девайс ответишь?) пакет NAK , означающий (я еще не подготовил данные). Это может продолжаться и секунду и дольше.
Cостояние NAK выставляется прямой записью в регистры USB определенных значений , примерно таким образом :
USBx_INEP(ep->num)->DIEPCTL |= (USB_OTG_DIEPCTL_CNAK | USB_OTG_DIEPCTL_EPENA);
То , что ниже в принципе можно не читать.
Universal Serial BusMass Storage ClassBulk-Only
Наконец-то находим даташит по протоколу и многое проясняется :
Command Block Wrapper
31 байтный стандартный пакет — так называемый Command Block Wrapper (CBW):
0-3: 55 53 42 43 — это dCBWSignature
4-7: E0 A9 2D EC — dCBWTag — The device shall echo the contents of this field back to the host
8-11: 24 00 00 00 = (24) — dCBWDataTransferLength — The number of bytes of data that the host expects to transfer on the Bulk-In or Bulk-Out endpoint
12: bmCBWFlags — 1 байт — Bit 7 =0 (Data-Out from host to the device 0x80) =1(Data-In from the device to the host)
13: — bCBWLUN — The device Logical Unit Number (LUN) to which the command block is being sent
14: bCBWCBLength — The valid length of the CBWCB in bytes
15-30: CBWCB — The command block to be executed by the device

Command Status Wrapper
Это 13 байтная короткая команда ответа :

0-3: 55 53 42 53 всегда сначала dCSWSignature
4-7: — dCBWTag — The device shall echo the contents of this field back to the host — Девайс указываем некие 4 байта , и смотрит , чтобы хост в следующей посылке их повторил.
8-11: dCSWDataResidue — (For Data-Out the device shall report in the dCSWDataResidue the difference between the amount of
data expected as stated in the dCBWDataTransferLength, and the actual amount of data processed by
the device. For Data-In the device shall report in the dCSWDataResidue the difference between the
amount of data expected as stated in the dCBWDataTransferLength and the actual amount of relevant
data sent by the device. The dCSWDataResidue shall not exceed the value sent in the
dCBWDataTransferLength)
можно указать оставшееся количество не переданных байт, т.е. запросили dCBWDataTransferLength , а мы минус сколько считали.
12: bCSWStatus 00 good status, 01 Command Failed, 02 Phase Error
Тут в ответе хосту состояние занято в принципе как передать?
bCSWStatus либо good status либо Failed и это явно не для этого.
Если вы захотите использовать поле dCSWDataResidue как сигнал , что данные еще не готовы , то у вас ничего не получится. Для этого используется только NAK пакет.
Этот кусок не показывает истинного протокола обмена.
Вот маленький кусочек обмена по USB , снятого USBLyzer-ом. Когда инициализации прошла , ПК узнал , что перед ним флэшка и что ПК делает дальше .
Тут между показанными пакетами, еще очень много NAK, ACK пакетов.

Вот маленький кусок огромного лога анализатора LA1010
Тут как раз показан один из многократно повторяющихся пакетов NAK :

Ну и для еще большего понимания выгруженный протокол обмена в файл:
Настройка Kafka для работы в режиме подтверждения о принятии сообщения (ack/nack)
На новом проекте, на котором я работаю в качестве PHP Tech Lead. Команда столкнулась с вопросом наведения порядков, один из которых — унификация:
- единый тип реляционной базы данных
- единый брокер сообщений
- единый фреймворк в разрезе языка программирования
- etc.
В этой статье пойдет речь о выборе брокера сообщений.
Kafka на данный момент уже используется в компании и эта технология является более масштабируемой чем тот же RabbitMQ, поэтому мы и смотрим в ее сторону. Перед тем как начать внедрять Kafka в наши проекты, мы протестируем закрывает ли эта технология наши потребности.
Предъявляемые требования к брокеру сообщений:
- Совместимость с PHP
- Стабильная работа, отказоустойчивость, высокий SLA(>99,95%)
- Масштабируемость
- Быстрая скорость отправки сообщения в очередь
- Уверенность в доставке сообщения
- Уверенность в получении и обработке сообщения(ack/nack)
По сути это все все и так есть и отлично работает(на малых и средних объемах) у RabbitMQ.
Но как я уже писал выше — мы не хотим плодить зоопарк технологий.
1. Выбор клиента для PHP
На официальном сайте Kafka есть несколько ссылок на репозиториев интеграции.
- https://github.com/EVODelavega/phpkafka
- https://github.com/arnaud-lb/php-rdkafka
- https://github.com/quipo/kafka-php
- https://github.com/michal-harish/kafka-php
Только 1н репозиторий «живой» на текущий момент и имеет большое количество звезд на GitHub — arnaud-lb/php-rdkafka.
2. Сетап окружения
В 21м году Docker для разработчика это как кнут для Индианы Джонс.
- Я зашел на hub.docker.com и нашел самый популярный официальный образ kafka.
- Дальше поиск в гугл готового примера docker-compose файла с PHP, возможно даже с kafka. И о сюрприз — первая строка выдачи поискового результата — Phillaf/php-kafka-demo. Тут уже присутствует образ из первого пункта.
git clone
- После клонирования репозитория, запускаем:
docker-compose up -d
С первого раза конечно же проект не собрался(как минимум у меня на MacOs BigSur). Для того чтобы испраить ошибку было достаточно просто убрать чётко заданую версию Kafka.

- Все готово для работы приожения, но а как же без доп. бонуса? Для того чтобы лучше понимать что происходит с неизвестным мне инструментом, я решил сразу же поискать визуальный менеджер. Было несколько платных версий которые нужно устанавливать на ПК. Но это же не трушно. Поэтому я установил opensource admin panel Для этого добавляем несколько строк в наш docker-compose.yml
kafka-ui: image: provectuslabs/kafka-ui container_name: kafka-ui ports: - "8080:8080" restart: always environment: - KAFKA_CLUSTERS_0_NAME=local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 - KAFKA_CLUSTERS_0_ZOOKEEPER=zookeeper:2181 - KAFKA_CLUSTERS_0_READONLY=false
- На старт, внимание, пуск. docker-compose up -d
- Go to http://localhost — Добавляет сообщения в нашу очередь(топик).
- Go to http://localhost:8080 — откроет нашу admin panel — где мы увидим наш топик и первые сообщения в нем.
- заходим в контейнер (лично я предпочитаю пользоватьтся docker в phpStorm), но можно и черз GUI docker так и с помощью консоли(не буду тут на этом останавливаться, думаю что тем кому уже интересна Kafka — точно умеют смогут зайти в контейнер Docker) и запускаем consumer
php ./public/consumer_low.php
В консоли мы увидим наши сообщения.
Поздравляю вас теперь вы адепты Kafka.
3. Настраиваем конфигурацию под наши требования
Как я писал выше, нас интересует полный контроль над сообщениями, наше приложение хочет получать сообщение ровно 1н раз, не больше и не меньше. При этом мы хотим быть уверенными что если в процессе обработки сообщения консюмер внезапно завершит свою работу(в следствии деплоя, возникновения ошибки или падения сервера, . ) то после перезапуска — это сообщение будет обработано, а не канет в небытие.
- Для этого нам потребуется отключить автоматическую синхронизацию кафки о сохраненном оффсете.
$conf->set('enable.auto.commit', 'false');
- Для того чтобы севрвер мог сохранять offset мы должны назначить consumer’у уникальный идентификатор чтобы при переподключении кафка знала на каком месте мы остановлись.
$conf->set('group.id', 'group_1');
- Начинаем прослушивать очередь:
$topic->consumeStart($partition, RD_KAFKA_OFFSET_STORED);
1й параметр — номер партиции для чтения(по дефолту топик имеет 1у партицию и значение переменной будет = 0). 2й параметр — число(int) — offset c которого стоит начать читать сообщения. Существует 3 предустановленных режима:
- RD_KAFKA_OFFSET_BEGINNING — начать чтение с первого сообщения в партиции
- RD_KAFKA_OFFSET_END — читать только новые сообщения, которые появятся в очереди только после подключения консюмера
- RD_KAFKA_OFFSET_STORED — читать с того места где остановились в прошлый раз(этот флаг возможно использовать только в случае указания — ‘group.id’)
- Получение сообщения из очереди
$msg = $topic->consume($partition, 1000);
- После успешной обработки сообщения(сохранения в БД или тп) помечаем сообщение как прочитанное(инкрементируем оффсет)
$topic->offsetStore($partition, $msg->offset);
Вуаля, мы реализовали консистентную работу c Apache Kafka.
Буду рад вашим комментариям.
Перевод «Nack» на русский
The transactional queue must be local to the queue manager that will move the negative acknowledgment (NACK) message to the queue.
Эта транзакционная очередь должна быть локальной для диспетчера очереди, который будет перемещать в очередь сообщение отрицательного подтверждения (NACK).
There are five conditions that lead to the generation of a NACK
Есть пять условий, которые могут привести к генерации NACK
If the DHCP server somehow decides that the computer cannot have an IP address, it will send a command called NACK.
Если сервер решит, что устройство не может иметь IP-адрес, он отправит NACK.
If some of the frames were unsuccessfully decoded by the receive processor, the controller/processor 740 may also use an acknowledgement (ACK) and/or negative acknowledgement (NACK) protocol to support retransmission requests for those frames.
Если некоторые из кадров не были успешно декодированы процессором приема, то контроллер/процессор 240 также может использовать протокол положительного подтверждения (ACK) и/или отрицательного подтверждения (NACK) для поддержки запросов повторной передачи для этих кадров.
In the event an original data transmission for HARQ interlace 1 during subframe 1 is unsuccessful, a negative acknowledgement («NACK«) signal may be sent on a complementary link (e.g., an uplink in the case of a downlink HARQ transmission).
В случае если первоначальная передача данных для чередования 1 HARQ в подкадре 1 оказалась неуспешной, можно отправить сигнал отрицательного подтверждения («NACK«) на дополнительной линии связи (например, по восходящей, в случае передачи HARQ по нисходящей линии связи).
By requesting negative acknowledgment (NACK) messages and checking their class. (This technique is applicable to the quotas of destination queues, not to computer quotas.)
с помощью запроса сообщений с отрицательным подтверждением (NACK) и проверки класса этих сообщений (данная процедура подходит лишь для проверки квот очередей назначения, а не квот компьютеров);
In this regard, if the gateway transmits an NACK message back to the mobile station, the mobile station can be configured to reattempt to establish communication with the gateway, and resend the call session ID and shared secret.
В этом отношении, если шлюз передает сообщение NACK обратно в мобильную станцию, то мобильная станция может конфигурироваться для повторной попытки установления связи со шлюзом и повторной посылки идентификатора сеанса вызова и совместно используемого секретного кода.