I2c stm32 обработка ошибок

В этом руководстве (перевод статьи [1]) мы рассмотрим встроенное периферийное устройство I2C микроконтроллера (MCU) STM32. Начнем с краткого введения в принцип работы шины Inter-Integrated Circuit (I2C), и подробно рассмотрим аппаратный модуль STM32 I2C, его функционал, режимы работы, опции и конфигурации. Будут рассмотрены сигналы прерывания, которые может генерировать аппаратура STM32 I2C hardware. В качестве примеров рассмотрим различные режимы приема и передачи данных I2C — polling (с опросом флагов), interrupt (получение статуса по прерываниям) — как устройства I2C master, так и устройства I2C slave. В заключение мы проверим доступные варианты примеров конфигураций I2C, которые можно получить в генераторе кода STM32CubeMX [2], рассмотрим способы работы с периферией I2C через библиотеки HAL API, предоставляемые компанией STMicroelectronics.

[Введение в коммуникации по шине I2C]

Шина I2C изначально была разработана компанией Philips Semiconductors (теперь NXP) в 1982 году. В документации для некоторых MCU наподобие Atmel AVR интерфейс I2C часто называют как TWI (аббревиатура от Two Wire Interface). I2C является синхронной, двунаправленной, полудуплексной шиной. Она позволяет работать в конфигурациях с несколькими главными устройствами на шине и одним или несколькими подчиненными устройствами (multi-master), несколькими подчиненными устройствами на шине и одним главным устройством (multi-slave). Однако чаще всего встречается конфигурация multi-slave, когда одно главное устройство (master) управляет одним или несколькими подчиненными (slave) устройствами. I2C широко используется в промышленной и бытовой аппаратуре для обмена данными между микросхемами на небольших расстояниях, когда они расположены на одной печатной плате или в одном корпусе электронного прибора.

Режимы и скорости I2C. Поначалу скорость I2C ограничивалась 100 килобит/сек (частота тактов SCL 100 кГц). Со временем в спецификацию было внесено несколько дополнений, так что теперь существует 5 категорий рабочих скоростей. Категории устройств Standard-mode, Fast-mode (Fm), Fast-mode Plus (Fm+) и High-speed mode (Hs-mode) обратно совместимы другом, т. е. более высокоскоростное устройство master имеет возможность переключиться в режим пониженной скорости для обеспечения совместимости с менее скоростными устройствами на шине. Устройства Ultra Fast-mode несовместимы с устройствами предыдущих версий, поскольку такая шина работает только в одном направлении передачи данных.

Шина I2C с двунаправленной передачей данных может работать в следующих режимах (полудуплекс):

Standard-Mode (Sm), скорость передачи до 100 килобит/сек
Fast-Mode (Fm), скорость передачи до 400 килобит/сек
Fast-Mode Plus (Fm+), скорость передачи до 1 Мегабит/сек
High-speed Mode (Hs-mode), скорость передачи до 3.4 Мегабит/сек

Однонаправленная шина I2C:

Ultra Fast-Mode (UFm), скорость передачи до 5 Мегабит/сек

Аппаратура STM32 штатно поддерживает режимы скоростей Sm и Fm. Программированием регистра делителя тактовой частоты можно добиться поддержки режима Fm+.

Физический слой I2C. Шина I2C использует драйвер с открытым стоком (открытый коллектор) для своих сигналов SDA и SCL. Это означает, что сигналы шины SDA и SCL должны быть подтянуты к + питания (Vcc) через внешние подтягивающие резисторы (pull-up). Типичный номинал для pull-up от 4.7 кОм до 10 кОм, но это может меняться в зависимости от скорости передачи и длины линий сигналов SDA и SCL. По этой причине состоянием ожидания шины (IDLE, никакие данные не передаются) считается лог. 1 на обоих сигналах SDA и SCL, когда все транзисторы драйвера закрыты. Если какой-либо из транзисторов драйвера откроется, то на соответствующем сигнале шины появится лог. 0.

I2C physical layer fig01

Рис. 1. Физическая организация шины I2С.

Чтобы записать на шину сигнал лог. 0, мы включаем выходной драйвер, переводя тем самым сигнальную линию в низкий уровень (LOW). Для записи лог. 1 выключаем выходной драйвер, и линия будет поднята на высокий уровень (HIGH) под действием внешних резисторов. Открытый сток шины I2C делает её двунаправленной, и протокол I2C обеспечивает разрешение коллизий устройств, когда несколько устройств пытаются получить одновременно доступ к шине. Любое из master-устройств на шине, которое первым откроет свой транзистор, выведя на шину лог. 0, выиграет тем самым арбитраж, и другое master-устройство приостановит свою работу в ожидании освобождения шины.

SDA и SCL, достоверность данных. Физически обе линии сигналов SDA и SCL являются двунаправленными. Когда шина свободна, на обоих этих сигналах присутствует уровень HIGH. Выходные каскады всех параллельно подключенных к шине устройств должны иметь выход с общим стоком, реализуя тем самым логическую функцию «проводное И» (wired-AND).

Примечание: если не используется опциональное растягивание импульса тактов slave-устройством (clock stretching) в конфигурации с одним master, то на шине I2C сигнал SCL не обязательно должен быть с открытым стоком, и все сигналы SCL у slave-устройств работают только как вход. В этом случае выход SCL устройства master может быть двухтактным, потому что его уровнем всегда управляет одно и только одно устройство — master. Сигнал SDA по-прежнему должен быть с открытым стоком как у устройства master, так и у всех устройств slave.

Из-за того, что к шине I2C могут быть подключены микросхемы, изготовленные по разным технологиям (CMOS, NMOS, TTL), уровни напряжений для лог. 0 (LOW) и лог. 1 (HIGH) не жестко фиксированы, однако привязаны к уровню напряжения питания VDD (все микросхемы, подключенные к шине I2C, должны иметь одинаковое напряжение питания). Пороговые уровни установлены на 30% и 70% от VDD. Таким образом, лог. 0 (LOW) должен обеспечиваться уровнем VIL = 0.3VDD, и лог. 1 (HIGH) уровнем VIH = 0.7VDD. Данные на SDA должны оставаться стабильными во время периода HIGH на сигнале тактов SCL. Изменение уровня HIGH или LOW сигнала данных должно происходить только тогда, когда сигнал SCL находится на уровне LOW. Для каждого передаваемого по шине бита SDA генерируется один период тактов SCL.

I2C send one bit fig02

Рис. 2. Передача бита по шине I2C.

Элементы транзакций I2C. Типовое сообщение I2C состоит из некоторого количества базовых элементов (сигналов), которые последовательно генерируются сигналами шины. Первый базовый элемент, который появляется на шине — сигнал старта (start condition, далее сокращенно START, или S). За сигналом START идет другой базовый элемент — адрес устройства (обычно 7-разрядный, но иногда 10 разрядный), затем идет один R/W-бит, который обозначает тип операции, которую master заказал на шине. Если бит R/W равен 0, то это означает операцию записи (write), а если равен 1, то операцию чтения (read). После этого, если slave-устройство с переданным адресом присутствует на шине и работает нормально, то оно подтверждает свое присутствие сигналом в бите ACK (Acknowledge), подтягивая шину SDA к лог. 0. Если же устройства с указанным адресом нет на шине, или оно не готово к обмену, шина SDA остается в лог. 1, что означает отрицательное подтверждение NACK (Negative Acknowledge).

Если была заказана операция записи, после этого master передает байт данных, за которым идет сигнал подтверждения от slave-устройства. Далее может быть передано несколько байт. По завершении передачи master может прервать обмен отправкой еще одного базового элемента — сигнала остановки (Stop Condition, далее сокращенно STOP, или P).

Master для инициации новой транзакции может выдать повторно сигнал старта — REPEATED START (далее по тексту сокращенно Sr), без выдачи сигнала STOP (P).

I2C protocol base elements typical sequence fig03

Рис. 3. Типовая последовательность базовых элементов протокола I2C.

Таким образом, различают следующие базовые элементы протокола I2C:

• Сигнал запуска транзакции, Start Condition (START, S)
• Сигнал завершения транзакции, Stop Condition (STOP, P)
• Сигнал перезапуска транзакции, Repeated Start (Restart) Condition (REPEATED START, Sr)
• Положительное подтверждение, Acknowledge (ACK, A)
• Отсутствие подтверждения, Not Acknowledge (NACK, ~A)
• Адрес и бит R/W
• Байт данных

Более подробно про работу шины I2C и её протокола можно почитать в различных публикациях, например в Википедии или статье [3].

[Аппаратура STM32 I2C]

Основные возможности STM32 I2C:

• Совместимость с конфигурацией Multimaster: один и тот же интерфейс может работать в режиме Master или Slave.
• Функции I2C Master: аппаратная генерация тактов, сигналов START и STOP.
• Функции I2C Slave: программируемый адрес I2C с одновременной поддержкой 2 адресов (с возможностью подтверждения двух slave-адресов), детектирования сигнала STOP.
• Генерация и детектирование 7-битной/10-битной адресации и общего вызова по шине (General Call).
• Поддержка скоростей обмена:
   – Standard Speed (до 100 кГц)
   – Fast Speed (до 400 кГц)
• Аналоговый фильтр помех (Analog noise filter).
• 2 вектора прерываний:
   – 1 прерывание для успешного обмена адресом / данными.
   – 1 прерывание для ситуации ошибки.
• Опциональное растягивание импульса тактов slave-устройством (clock stretching) для управления потоком.
• 1-байтный буфер с поддержкой DMA.
• Конфигурируемая функция PEC (packet error checking) для генерации или проверки.
• Совместимость с шиной SMBus 2.0.
• Совместимость с шиной PMBus.

I2C peripheral block diagram fig04

Рис. 4. Блок-схема аппаратуры STM32 I2C.

Как можно увидеть из схемы на рис. 4, здесь присутствует главный регистр сдвига данных, регистр буфера и некоторая логика управления, обслуживающая поведение машины состояний шагов транзакции I2C. Логика обслуживает детектирование совпадения адреса, генерацию сигнала SCL, фильтрацию сигналов от помех, проверку ошибок и т. д.

Выбор режима I2C. Интерфейс STM32 I2C может работать в одном из 4 режимов:

• Slave transmitter (передача данных подчиненным устройством).
• Slave receiver (прием данных подчиненным устройством).
• Master transmitter (передача данных главным устройством).
• Master receiver (прием данных главным устройством).

По умолчанию периферия работает в режиме slave. Интерфейс автоматически переключится из режима slave в режим master после генерации сигнала START, и из режима master в режим slave, если произойдет потеря арбитража или будет сгенерирован сигнал STOP. Такое поведение аппаратуры обеспечивает совместимость multi-master. Далее мы создадим 4 рабочих примера, демонстрирующих работу периферии I2C в этих режимах.

STM32 I2C в режиме Slave. По умолчанию аппаратура I2C находится в режиме slave. Для переключения в режим master необходима генерация сигнала START. Для обеспечения корректных таймингов шины тактовая частота периферии должна быть запрограммирована в регистр I2C_CR2. Исходная входная частота периферии должна быть как минимум:

• 2 МГц для режима скорости Sm (режим стандартной скорости 100 кГц)
• 4 МГц для режима скорости Fm (режим повышенной скорости 400 кГц)

При обнаружении сигнала START от внешнего master принимаемый адрес с сигнала SDA последовательно поступает в регистр сдвига. Затем он сравнивается с запрограммированными (в регистрах (I2C_OAR1 и (I2C_OAR2) адресами. После получения адреса интерфейс slave-устройства с помощью регистра сдвига принимает байты через сигнал SDA в регистр I2C_DR. После каждого байта интерфейс генерирует импульс подтверждения, если установлен бит ACK.

Если установлен флаг RxNE, и данные не были прочитаны из регистра I2C_DR до окончания приема следующего байта, установится бит BTF и интерфейс будет ожидать, пока не будет очищен бит BTF путем чтения регистра I2C_SR1, за которым идет чтение регистра I2C_DR. Пока не будет очищен бит BTF, интерфейс slave будет растягивать сигнал SCL (подтягивая его к уровню LOW), давая тем самым устройству master сигнал неготовности к получению дальнейших данных. Дальнейшая передача данных будет невозможна, пока slave не освободит линию сигнала SCL. По этой причине следует быть осторожным с функцией clock stretching в устройствах slave.

После того, как передан последний байт данных, master генерирует сигнал STOP. Интерфейс определит этот сигнал, установит бит STOPF и сгенерирует прерывание, если установлен бит ITEVFEN. Бит STOPF очищается путем чтения регистра SR1, за которым идет запись регистра CR1.

STM32 I2C в режиме Master. В этом режиме интерфейс инициирует передачу данных и генерирует сигнал тактов SCL. Передача данных всегда начинается с сигнала START, и заканчивается сигналом STOP. Режим Master выбирается сразу, как только на шине генерируется сигнал START. Дальнейшая генерация последовательности необходимых базовых элементов протокола I2C происходит в режиме master.

Типовая последовательность действий программы для организации работы в режиме master:

• Программируется входная тактовая частота периферийного устройства I2C в регистре I2C_CR2, чтобы обеспечить корректные тайминги шины I2C.
• Конфигурируются регистры управления тактами.
• Конфигурируется регистр времени нарастания уровня.
• Программируется регистр I2C_CR1, чтобы разрешить работу периферийного устройства I2C.
• Устанавливается бит START в регистре I2C_CR1, чтобы сгенерировать сигнал START.

Исходная входная частота периферии должна быть как минимум (требования такие же, как и для режима slave):

• 2 МГц для режима скорости Sm (режим стандартной скорости 100 кГц)
• 4 МГц для режима скорости Fm (режим повышенной скорости 400 кГц)

STM32 I2C PEC. Аббревиатура PEC расшифровывается как Packet Error Checking, т. е. это функция проверки ошибок при передаче данных. В периферийном устройстве I2C реализован калькулятор PEC, чтобы улучшить надежность коммуникаций по шине I2C. PEC вычисляется с использованием полинома C(x) = x8 + x2 + x + 1 CRC-8 , последовательно на каждом бите. Если разрешить работу PEC, то появляется возможность автоматически проверять наличие ошибок в больших транзакциях данных, без каких-либо дополнительных накладных расходов по процессорному времени MCU (вычисление и проверка контрольной суммы PEC происходит аппаратно).

Однако следует учитывать, что если master в любой момент проиграет арбитраж, то это приведет к повреждению PEC. И в результате устройство вернется обратно в режим slave, будет ожидать, пока выигравший арбитраж master не завершит свое сообщение. Только после этого можно будет заново начать процесс передачи своих данных и заново начать подсчет PEC для них.

[Обработка ошибок STM32 I2C]

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

Bus Error (BERR). Эта ошибка произойдет, когда интерфейс I2C определить внешний (неожиданный в данный момент) сигнал STOP или START, когда происходит передача адреса или данных.

Acknowledge Failure (AF). Эта ошибка произойдет, когда интерфейс определит бит отсутствия подтверждения (non-acknowledge, NACK).

Arbitration Lost (ARLO). Эта ошибка произойдет, когда интерфейс определит потерю арбитража.

Overrun/Underrun Error (OVR). Событие переполнения на приеме (Overrun) может произойти в режиме slave, если запрещено растягивание тактов (clock stretching). В этом случае интерфейс принял данные, и данные в регистре DR не были прочитаны до приема интерфейсом следующего байта. Событие недогрузки (Underrun) может произойти в режиме slave, когда также запрещено растягивание тактов, и интерфейс I2C передает данные. В этом случае программа не обновила регистр DR следующим байтом до момента, когда поступили такты уже для следующего байта.

[Прерывания STM32 I2C]

Различные события прерывания I2C привязаны к одному и тому же вектору, так что обработчике прерывания (ISR) для I2C необходимо проанализировать флаги, чтобы определить событие (причину) прерывания. Эти события генерируют прерывание, если установлен соответствующий бит разрешения.

Таблица 1. Запросы на прерывание I2C.

Событие прерывания Флаг события Бит, разрешающий прерывание
Отправлен сигнал START (режим master) SB ITEVFEN
Отправлен адрес (режим master), или обнаружено совпадение адреса (режим slave) ADDR
Отправлен 10-битный заголовок (режим master) ADD10
Принят сигнал STOP (режим slave) STOPF
Завершена передача байта данных BTF
Буфер приема не пуст RxNE ITEVFEN и ITBUFEN
Буфер передачи пуст TxE

[Передача и прием STM32 I2C]

В этой секции мы рассмотрим возможные варианты обработки транзакций I2C в приложении firmware для STM32.

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

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

I2C Interrupt. Для более эффективного использования процессорного времени MCU мы можем разрешить прерывания I2C. Тогда программа будет получать сигналы от нужных событий, и запускать ISR для их обработки. Это может происходить, когда данные были переданы или были приняты. Что экономит много ресурсов процессора, потому что программа в фоновом режиме может заниматься другими действиями, не тратя время на циклы опроса аппаратуры.

Однако и с прерываниями следует быть осторожными, когда необходимо реализовать приложение жесткого реального времени, когда необходимо быстро, в строго заданный интервал времени реагировать на критические внешние события — например, в нужное время обработать сигнал датчика столкновения и вовремя раскрыть подушку безопасности автомобиля. Мы не можем знать заранее, когда произойдет прерывание, во время выполнения какой задачи может начать выполняться код его ISR. При неправильном назначении приоритетов прерываний и небрежного планирования выполнения задач приложения может произойти некорректное поведение программы в реальном времени.

I2C с использованием DMA. Чтобы избежать событий недогрузки и переполнения для работы на максимальной скорости во время передачи для I2C необходимо вовремя предоставлять данные, и во время приема нужно вовремя считывать поступившие данные. Для упрощения перемещений данных аппаратура I2C поддерживает функцию DMA (Direct Memory Access, прямой доступ к памяти), реализующую простой протокол запрос/подтверждение.

Запросы DMA генерируются, когда при передаче регистр данных опустошается, и когда при приеме он заполняется. Использование DMA также освобождает процессорное время MCU, потому что перемещения данных между периферийным устройством и памятью происходят аппаратно. DMA считается самым эффективным методом обработки потока данных.

[Чтение/запись микросхемы памяти I2C]

В этой секции мы рассмотрим полезные функции HAL API для библиотеки драйвера I2C, которые позволяют читать и записывать микросхему памяти. Функции HAL_I2C_Mem_Write() и HAL_I2C_Mem_Read() реализуют чтение и запись микросхем памяти с блокированием выполнения.

Существуют и другие аналогичные версии функций, которые выполняют ту же самую работу без блокировки (с использованием прерываний или прерываний + DMA). Но для начала давайте разберем, как происходит процесс чтения/записи памяти устройства.

Нам, как разработчикам, обычно не нужно что-то делать на низком аппаратном уровне, когда необходимо реализовать какие-либо действия по обмену данными с внешними устройствами. Нужно просто иметь общее представление о том, как работает шина, и помнить о том, что низкоуровневые операции по реализацию протокола I2C может автоматически обрабатывать аппаратура MCU, нам всего лишь нужно её правильно запрограммировать путем записи и чтения необходимых регистров и флагов I2C. Для этой цели существуют готовые функции HAL (Hardware Abstraction Layer), которые упрощают манипуляции с регистрами I2C.

Базовый функционал по передаче данных через I2C в качестве устройства Master обрабатывает следующая HAL-функция HAL_I2C_Master_Transmit(). Она передает какие-либо данные через I2C в указанный адрес slave-устройство (если это устройство присутствует на шине) в блокирующем режиме.

В чем же разница между функциями I2C_Transmit и I2C_Mem_Write? Для ответа на этот вопрос рассмотрим пример взаимодействия с устройством MPU6050 IMU (расшифровывается как Inertial Measurement Unit, т. е. это микросхема инерциального гироскопа, или датчика положения в пространстве).

У IMU есть внутренний адрес устройства I2C, чтобы любой master шины мог по нему обратиться к модулю сенсора микросхемы MPU6050. Внутри MPU6050 есть регистры с внутренними уникальными адресами. Таким образом, для работы с сенсором IMU нам нужно установить (записать) некоторые значения в определенные регистры микросхемы MPU6050 (чтобы её сконфигурировать), и затем считывать значения из других регистров MPU6050, в которых содержатся данные IMU.

Итак, если у вас есть master STM32, и хотите получить показания MPU6050 IMU, то нужно будет выполнить следующие указанные в таблице действия.

Таблица 2. Последовательность базовых элементов протокола I2C для чтения одного байта.

Master S AD+W   RA   S AD+R     NACK P
Slave     ACK   ACK     ACK DATA    

Как можно видеть, устройство master (STM32 MCU) должно запустить транзакцию отправкой сигнала START (S). Затем master должен отправить адрес с признаком операции записи (AD+W, адрес здесь это адрес I2C самого модуля MPU6050). Затем master записывает внутренний регистр адреса микросхемы MPU6050 (RA), это тот регистр, который необходимо прочитать. Устройство slave (микросхема MPU6050) подтвердит эту команду (ACK), и в ответ на операцию чтения (повторная отправка S и адреса с признаком операции чтения AD+R) отправит данные, которые находятся в этом указанном регистре. В завершении транзакции master прервет коммуникацию отправкой сигналов NACK и STOP (P).

Аналогичная операция чтения происходит со многими другими микросхемами с интерфейсом I2C, отличаются только адрес I2C и адреса внутренних регистров или ячеек памяти.

Вы можете выполнить операции записи и чтения с помощью вызова базовых низкоуровневых функций HAL API (I2C_Transmit, I2C_Receive), или с помощью более высокоуровневых функций (Mem_Write, Mem_Read), разных версий, поддерживающих все режимы аппаратуры STM32 (blocking, interrupt, DMA).

Разберем пример конфигурирования микроконтроллера STM32F429ZITx (плата 32F429IDISCOVERY, или STM32F429I-DISC1 [4]) для подключения к микросхеме контроллера клавиатуры TCA8418 [5]. Будем использовать I2C1 и его выводы PB8 SCL, PB9 SDA, работу с I2C по прерываниям (без DMA).

Шаг 1. Запустите генератор кода STM32CubeMX, выберите микроконтроллер (кнопка ACCESS TO MCU SELECTOR).

I2C1 master STM32CubeMX example fig01

I2C1 master STM32CubeMX example fig02

Шаг 2. На закладке Pinout & Configuration разверните выпадающий список Connectivity, и выберите I2C1. В выпадающем списке I2C поменяйте выбор с Disable на I2C.

I2C1 master STM32CubeMX example fig03

Шаг 3. Теперь нужно выбрать ножки портов для аппаратуры интерфейса I2C1. Для ножек портов GPIO микроконтроллера STM32F429 можно выбрать альтернативные функции, соответствующие различным внутренним периферийным устройствам (подробнее см. [6]). Для выбора ножек мы воспользуемся удобным интерфейсом генератора кода STM32CubeMX.

По умолчанию генератор кода назначил для I2C1 ножки портов PB6 (сигнал SCL) и PB7 (сигнал SDA). Нам же нужны ножки PB8 для SCL и PB9 для SDA. Для этого в правой части окна STM32CubeMX, где представлен вид на корпус LQFP144 микроконтроллера, переведите вид на выводы PB7 и PB6 (приближать можно колесиком мыши с удержанием клавиши Ctrl, а перемещать картинку можно удерживая правую кнопку мыши). Кликните на вывод PB7, и выберите Reset_State. Конфигурация по умолчанию сбросится, и можно выбрать другие ножки порта.

Кликните на вывод порта PB8, и выберите для него альтернативную функцию I2C1_SCL. 

I2C1 master STM32CubeMX example fig04

Таким же способом выберите для PB9 альтернативную функцию I2C1_SDA. После этого на закладке настроек GPIO Settings появятся новые выбранные ножки для сигналов SCL и SDA.

I2C1 master STM32CubeMX example fig05

Шаг 4. Перейдите в раздел настроек контроллера прерываний (NVIC Settings). Поставьте галочки I2C1 event interrupt (обработка основных событий приема и передачи) и I2C1 error interrupt (обработка событий ошибки).

I2C1 master STM32CubeMX example fig06

Если Вы планируете использовать DMA, то на закладке DMA Settings можно добавить соответствующие запросы DMA.

Шаг 5. Теперь можно будет установить вариант скорости I2C (Standard Mode или Fast Mode) и тактовую частоту для режима master. Выберите закладку настроек Parameter Settings, установите режим Fast Mode, автоматически установится частота тактов I2C 400 кГц. Здесь можно сконфигурировать и другие параметры, такие как скважность тактов, коэффициент цифрового фильтра, запрет аналогового фильтра.

I2C1 master STM32CubeMX example fig07

Если для интерфейса I2C STM32 используется режим slave, то необходимо также установить Primary slave address и его длину. Также для slave-режима можно разрешить растягивание тактов и другие параметры.

Шаг 6. Теперь нужно настроить общую систему тактирования STM32. Перейдите в раздел настроек System Core, выберите RCC, и в выпадающем списке High Speed Clock (HSE) выберите Crystal/Ceramic Resonator.

I2C1 master STM32CubeMX example fig08

Перейдите на закладку Clock Configuration, переключите мультиплексор тактов в положение HSE, и выберите частоту кварцевого резонатора 8 МГц (такой кварц установлен на плате STM32F429 DISCOVERY). Остальные опции здесь можно оставить по умолчанию.

I2C1 master STM32CubeMX example fig09

Шаг 7. Перейдите на закладку Project Manager. В разделе настроек проекта (кнопка Project) выберите имя для проекта (поле ввода Project Name), место расположения папки проекта (Project Location), используемую среду разработки (Toolchain / IDE) и его минимальную версию (Min Version). Остальные параметры можно оставить без изменения.

I2C1 master STM32CubeMX example fig10

Сохраните проект выбором в меню File -> Save Project (Ctrl+S).

I2C1 master STM32CubeMX example fig11

Шаг 8. Кликните на кнопку GENERATE CODE (находится справа вверху). Если Вы раньше не генерировали проекты для этого семейства микроконтроллеров, то будет выведен запрос на подтверждение загрузки необходимого пакета библиотек. Подтвердите, начнется процесс загрузки и установки.

I2C1 master STM32CubeMX example fig12

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

I2C1 master STM32CubeMX example fig13

Примечание: во время установки может произойти ошибка «Target directory allready exists». Это старая болячка STM32CubeMX, решить эту проблему довольно легко, см. [8].

[Пример работы master I2C]

Пример инициализации I2C:

I2C_HandleTypeDef hi2c1;
 
void MX_I2C1_Init(void)
{
   hi2c1.Instance = I2C1;
   hi2c1.Init.ClockSpeed = 100000;
   hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
   hi2c1.Init.OwnAddress1 = 0;
   hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
   hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
   hi2c1.Init.OwnAddress2 = 0;
   hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
   hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
   if (HAL_I2C_Init(&hi2c1) != HAL_OK)
      HALerrorHandler(__FILE__, __LINE__);
   if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK)
      HALerrorHandler(__FILE__, __LINE__);
   if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK)
      HALerrorHandler(__FILE__, __LINE__);
}

Теперь предстоит запрограммировать регистры микросхемы контроллера TCA8418, чтобы она была сконфигурирована в соответствии с используемой матрицей клавиатуры.

Предположим, что необходимо сконфигурировать матрицу клавиатуры 5×8 (ROW0-ROW4, COL0-COL7):

TCA8418 schematic example

В этом случае для инициализации TCA8418 необходимо отправить следующий массив данных TCA8418cfg:

// Адреса регистров TCA8418:
#define TCA8418_REG_CFG          0x01
#define TCA8418_REG_INT_STAT     0x02
#define TCA8418_REG_KEY_EVENT_A  0x04
#define TCA8418_REG_KP_GPIO1     0x1D
#define TCA8418_REG_KP_GPIO2     0x1E
#define TCA8418_REG_KP_GPIO3     0x1F
 
static const uint8_t TCA8418cfg [] =
{
   TCA8418_REG_KP_GPIO1, 0x1F,   // Режим сканирования для ROW0-ROW4
   TCA8418_REG_KP_GPIO2, 0xFF,   // Режим сканирования для COL0-COL7
   // Основная конфигурация TCA8418:
   TCA8418_REG_CFG,      0xF1,   // 11110001
   // Расшифровка 0xF1 = 11110001b:
   //   AI=1: автоинкремент адреса разрешен
   //   GPI_E_CFG=1: события GPI не отслеживаются, когда клавиатура заблокирована
   //   OVR_FLOW_M=1: старые данные при переполнении удаляются
   //   INT_CFG=1: сигнал ~INT снимается через 50 мкс
   //   OVR_FLOW_IEN=0: ~INT при переполнении не генерируется
   //   K_LCK_IEN=0: ~INT при разблокировке клавиатуры не генерируется
   //   GPI_IEN=0: ~INT для сигналов GPI не генерируется
   //   KE_IEN: разрешение генерации ~INT для событий матрицы клавиатуры
   // Очистка флагов прерывания OVR_FLOW_INT, K_LCK_INT, GPI_INT, K_INT:
   TCA8418_REG_INT_STAT, 0x0F
};

Отправка данных конфигурации:

#define TCA8418_DEV_ADDR   (0x34 << 1)
 
HAL_StatusTypeDef halstatus;
 
for(uint8_t i = 0; i < 9; i += 2)
{
   // Передача пар байт "адрес регистра, значение регистра":
   halstatus = HAL_I2C_Master_Transmit(&hi2c1,
                                       TCA8418_DEV_ADDR,
                                       (uint8_t*)TCA8418cfg+i,
                                       2,
                                       I2Cx_TIMEOUT_MAX_KB);
   if (HAL_OK != halstatus)
   {
      // Тут нужно вставить обработку ошибки:
      ..
   }
}

Пример чтения кода нажатой клавиши, которое должно запускаться по сигналу ~INT:

uint8_t keycode = 0;
uint8_t data8;
 
// запрос 1 байта из FIFO:
data8 = TCA8418_REG_KEY_EVENT_A;
halstatus = HAL_I2C_Master_Transmit(&hi2c1,
                                    TCA8418_DEV_ADDR,
                                    &data8,
                                    1,
                                    I2Cx_TIMEOUT_MAX_KB);
if (HAL_OK != halstatus)
{
   // Обработка ошибки
   ..
}
halstatus = HAL_I2C_Master_Receive(&hi2c1,
                                   TCA8418_DEV_ADDR,
                                   &data8,
                                   1,
                                   I2Cx_TIMEOUT_MAX_KB);
if (HAL_OK != halstatus)
{
   // Обработка ошибки
   ..
}
 
// В переменной keycode находится код нажатой клавиши,
// в старшем бите keycode единица означает нажатие,
// ноль отпускание:
keycode = data8;

Ниже рассмотрено использование STM32 I2C1 в режиме подчиненного устройства, с обработкой событий I2C по прерываниям.

Настройка параметров аппаратуры I2C:

static void MX_I2C1_Init(void)
{
   hi2c1.Instance = I2C1;
   hi2c1.Init.ClockSpeed = 100000;
   hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
   // Адрес подчиненного устройства I2C:
   hi2c1.Init.OwnAddress1 = 16;
   hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
   hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
   hi2c1.Init.OwnAddress2 = 0;
   hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
   hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
   if (HAL_I2C_Init(&hi2c1) != HAL_OK)
      HALerrorHandler(__FILE__, __LINE__);
   if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK)
      HALerrorHandler(__FILE__, __LINE__);
   if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK)
      HALerrorHandler(__FILE__, __LINE__);
}

Инициализация аппаратуры I2C и прерывания:

void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
   GPIO_InitTypeDef GPIO_InitStruct = {0};
 
   // В качестве мастера I2C у нас будет работать ATmega328.
   __HAL_RCC_GPIOB_CLK_ENABLE();
 
   /** Конфигурация ножек I2C1:
   PB8     ------> I2C1_SCL
   PB9     ------> I2C1_SDA
    */
   GPIO_InitStruct.Pin = I2C1_SCL_Pin|I2C1_SDA_Pin;
   GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
   GPIO_InitStruct.Pull = GPIO_PULLUP;
   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
   GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
 
   /* Разрешение тактирования I2C: */
   __HAL_RCC_I2C1_CLK_ENABLE();
 
   /* Инициализация обработчика прерывания */
   HAL_NVIC_SetPriority(I2C1_EV_IRQn, KEYB_EV_INTERRUPT_PRIORITY, 0);
   HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
   HAL_NVIC_SetPriority(I2C1_ER_IRQn, KEYB_ER_INTERRUPT_PRIORITY, 0);
   HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
}

Обработчик прерывания событий I2C:

void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) 
{
   static BaseType_t xHigherPriorityTaskWoken;
   // По семафору keybsem разблокируется поток, который
   // осуществляет обмен с устройством master:
   xSemaphoreGiveFromISR(keybsem, &xHigherPriorityTaskWoken);
}

Код, который получает байт данных от устройства master:

halstatus = HAL_I2C_Slave_Receive_IT(&hi2c1, &data8, 1);

[Блокирующие HAL-функции для STM32 I2C]

«Блокирующими» называют такие функции, которые не возвращают управление из своего тела до тех пор, пока не будет выполнена их задача (например, пока не будет выполнена передача заказанного блока, или пока в процессе передачи не произойдет ошибка). Т. е. эти функции блокируют выполнение кода, из которого они были вызваны. Использование таких функций вполне допустимо в простых приложениях, когда не нужно параллельно выполнять другие задачи, либо в многопоточных приложениях с вытеснением (на основе RTOS), когда в случае блокировки потока на функции другие потоки могут нормально выполняться. Ниже приведен список блокирующих функций.

Передача master:

HAL_StatusTypeDef HAL_I2C_Master_Transmit (I2C_HandleTypeDef* hi2c,
                                           uint16_t DevAddress,
                                           uint8_t* pData,
                                           uint16_t Size,
                                           uint32_t Timeout);

Прием master:

HAL_StatusTypeDef HAL_I2C_Master_Receive (I2C_HandleTypeDef* hi2c,
                                          uint16_t DevAddress,
                                          uint8_t* pData,
                                          uint16_t Size,
                                          uint32_t Timeout);

Передача slave:

HAL_StatusTypeDef HAL_I2C_Slave_Transmit (I2C_HandleTypeDef* hi2c,
                                          uint8_t* pData,
                                          uint16_t Size,
                                          uint32_t Timeout);

Прием slave:

HAL_StatusTypeDef HAL_I2C_Slave_Receive (I2C_HandleTypeDef* hi2c,
                                         uint8_t* pData,
                                         uint16_t Size,
                                         uint32_t Timeout);

Запись в устройство памяти:

HAL_StatusTypeDef HAL_I2C_Mem_Write (I2C_HandleTypeDef* hi2c,
                                     uint16_t DevAddress,
                                     uint16_t MemAddress,
                                     uint16_t MemAddSize,
                                     uint8_t* pData,
                                     uint16_t Size,
                                     uint32_t Timeout);

Чтение из устройства памяти:

HAL_StatusTypeDef HAL_I2C_Mem_Read (I2C_HandleTypeDef* hi2c,
                                    uint16_t DevAddress,
                                    uint16_t MemAddress,
                                    uint16_t MemAddSize,
                                    uint8_t* pData,
                                    uint16_t Size,
                                    uint32_t Timeout);

Все эти функции возвращают значение из перечисления HAL_StatusTypeDef (в случае успеха будет возвращено HAL_OK, иначе один из кодов ошибки). В таблице ниже показано назначение параметров функций.

Таблица 3. Параметры HAL-функций STM32 I2C.

Параметр Назначение
I2C_HandleTypeDef* hi2c Указатель на дескриптор выбранного аппаратного устройства I2C.
uint16_t DevAddress Адрес I2C подчиненного устройства на шине.
uint8_t* pData Указатель на буфер данных в памяти.
uint16_t Size Размер буфера данных (количество байт).
uint32_t Timeout Таймаут ожидания завершения операции в миллисекундах.
uint16_t MemAddress Адрес внутри устройства памяти.
uint16_t MemAddSize Размер адреса устройства памяти.
uint32_t Trials Количество проверок готовности.

[HAL-функции прерываний для STM32 I2C]

Следующие функции могут работать в неблокирующем режиме — они возвращают управление сразу после вызова, и выполняют всю работу по обслуживанию транзакции I2C, используя для этого обработчик прерывания (ISR). Достоинство такого принципа работы в том, что во время передачи данных I2C процессор не простаивает на циклах ожидания и может выполнять другую работу.

Передача master:

HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT (I2C_HandleTypeDef* hi2c,
                                              uint16_t DevAddress,
                                              uint8_t* pData,
                                              uint16_t Size);

После вызова функции HAL_I2C_Master_Transmit_IT периферийное устройство I2C начнет передавать все байты данных в буфере pData один за другим, пока не будет передано Size байт из буфера. Когда передача завершится, будет запущена функция обратного вызова HAL_I2C_MasterTxCpltCallback (см. ниже). Если нужно выполнить какие-либо действия по завершению транзакции, то для этого можно вставить в тело функции HAL_I2C_MasterTxCpltCallback любой необходимый код. Функция HAL_I2C_MasterTxCpltCallback определена в модуле stm32f4xx_hal_i2c.c библиотеки с модификатором __weak [7], и поэтому должна быть при необходимости переопределена в любом из модулей пользователя (например в main.c):

void HAL_I2C_MasterTxCpltCallback (I2C_HandleTypeDef * hi2c)
{
   // Передача I2C завершена. Сюда можно вставить какой-нибудь код,
   // например обновление данных буфера и повторный запуск транзакции.
   ..
}

Прием master:

HAL_StatusTypeDef HAL_I2C_Master_Receive_IT (I2C_HandleTypeDef* hi2c,
                                             uint16_t DevAddress,
                                             uint8_t* pData,
                                             uint16_t Size);

После вызова функции HAL_I2C_Master_Receive_IT периферийное устройство I2C начнет принимать поступающие от slave-устройство байты данных, и записывать их один за другим в буфер pData, пока не будет принято Size байт. Когда все данные были приняты, будет вызвана callback-функция HAL_I2C_MasterRxCpltCallback. Если нужно выполнить какие-либо действия по завершению транзакции, то для этого можно вставить в тело функции HAL_I2C_MasterRxCpltCallback любой необходимый код. Функция HAL_I2C_MasterRxCpltCallback определена в модуле stm32f4xx_hal_i2c.c библиотеки с модификатором __weak [7], и поэтому должна быть при необходимости переопределена в любом из модулей пользователя (например в main.c):

void HAL_I2C_MasterRxCpltCallback (I2C_HandleTypeDef * hi2c)
{
   // Прием I2C завершен. Сюда можно вставить какой-нибудь код,
   // например обработку принятых данных.
   ..
}

Подобные API-функции реализованы также и для режима slave-устройства I2C.

[HAL-функции для STM32 I2C с использованием DMA]

Следующие функции также не блокирующие, и кроме прерываний они используют функцию прямого доступа к памяти (Direct Memory Access, DMA), дополнительно разгружающую процессор от операций перемещения данных из периферийного устройства в память (или в обратном направлении).

Передача master:

HAL_StatusTypeDef HAL_I2C_Master_Transmit_DMA (I2C_HandleTypeDef* hi2c,
                                               uint16_t DevAddress,
                                               uint8_t* pData,
                                               uint16_t Size);

Прием master:

HAL_StatusTypeDef HAL_I2C_Master_Receive_DMA (I2C_HandleTypeDef* hi2c,
                                              uint16_t DevAddress,
                                              uint8_t* pData,
                                              uint16_t Size);

Эти функции работают аналогично своим *_IT аналогам, описанным выше. После вызова функции *_DMA периферийное устройство I2C начнет работу по выполнению заказанной транзакции. Перемещение данных между буфером и периферийным устройством I2C будет выполняться аппаратно, с помощью DMA. Когда транзакция завершится, будет запущена функция обратного вызова HAL_I2C_MasterTxCpltCallback (для передачи) или HAL_I2C_MasterRxCpltCallback (для приема), см. описание выше.

Подобные API-функции DMA реализованы также и для режима slave-устройства I2C.

[Проверка устройства STM32 I2C]

Эта функция может использоваться для проверки, присутствует ли на шине slave-устройство с адресом DevAddress, и находится ли оно в работоспособном для обмена состоянии:

HAL_StatusTypeDef HAL_I2C_IsDeviceReady (I2C_HandleTypeDef* hi2c,
                                         uint16_t DevAddress,
                                         uint32_t Trials,
                                         uint32_t Timeout);

[Ссылки]

1. STM32 I2C HAL Code Examples Slave & Master DMA Interrupt site:deepbluembedded.com.
2. STM32CubeMX site:st.com.
3. AT24C64: Serial EEPROM с интерфейсом I2C (TWI).
4. STM32F429 Discovery.
5. TCA8418: контроллер матрицы клавиатуры.
6. STM32F429: GPIO и альтернативные функции.
7. Что такое weak-функция?
8. Ошибка установки пакета в STM32 CubeMX.

Пытаюсь реализовать коммуникацию по i2c и вьехать в философию прерываний.

Есть два обработчика прерываний

void I2C2_EV_IRQHandler(void)

и 

void I2C2_ER_IRQHandler(void)

Первый обработчик может быть вызван несколько раз (т.к. есть разные «этапы» взаимодействия по i2c, в документации их называют EVx, он вызывается после каждой успешной i2c операции, то есть на каждый успешный вызов функции, например I2C_GenerateSTART или I2C_SendData, будет вызвано прерывание). А если произошла ошибка, будет вызван I2C2_ER_IRQHandler.

Правильно ли я понимаю, что если был вызван void I2C2_ER_IRQHandler(void) — что значит, одна из i2c функций не выполнилась, то операция отменена, и ни I2C2_EV_IRQHandler, ни I2C2_ER_IRQHandler  вызываться больше не будут? И что обычно делают в обработчике прерывания ошибки? Еще раз попытаться отправить запрос?

И второй вопрос, можно ли отменить начатую, но еще не законченную (для которой еще не вызвалось прерывание), i2c операцию? То есть, чтоб для нее больше не вызывались прерывания, и можно было ее начать сначала.


Изменено пользователем bobbjenkins

In this article, let’s learn about the implementation of I2C_ER_IRQHandling.

Exercise:

1. Download the file (Figure 1) attached to the resource section.

I2C error IRQ handler implementation

Figure 1. Implementation of I2C_ER_IRQHandling.

2. In the downloaded file, the code is partially written, and it contains some comments (Figure 2), according to which you have to implement the code.

I2C error IRQ handler implementation

Figure 2. Comments for implementing I2C_ER_IRQHandling.

3. Steps for implementation:

  • When the I2C error IRQ handling (I2C_ER_IRQHandling()) is called first, you need to check what kind of error has happened.
  • Check for the bus error by checking the bus error flag in the status register 1 (Figure 3). If it is set, then clear the bus error flag, then notify the application about the error by calling the I2C_ApplicationEventCallback(), and the event I2C_ERROR_BERR is passed as a parameter of I2C_ApplicationEventCallback(), as shown in Figure 3. BERR is nothing but a bus error.

I2C error IRQ handler implementation

Figure 3. Check for the bus error.
  • The procedure to clear the bus error flag is given in the document itself. Check the BERR flag of the status register 1 shown in Figure 4.

If the BERR flag is set, then it means that misplaced start or stop condition. This flag is set by the hardware when the interface detects an SDA’s rising or falling edge while SCL is high, occurring in a non-valid position during a byte transfer, and the hardware will not clear it automatically. The software clears it. But this flag will get cleared automatically when you disable the peripheral (PE=0).

I2C error IRQ handler implementation

Figure 4. BERR flag of the status register 1.
  • The bus error flag is cleared, as shown in Figure 5.

I2C error IRQ handler implementation

Figure 5. Code to clear the bus error flag.
  • Similar things you have to do for other errors.
  • Copy the definition of all the event flags which will be sent to the application (Figure 6) and paste it into the header file driver.h, as shown in Figure 7.

Definition of the event flags

Figure 6. Definition of the event flags.
Pasting the event flag definitions into the header file driver.h
Figure 7. Pasting the event flag definitions into the header file driver.h.

In the following article, let’s do the exercise :Testing I2C interrupt APIs.

FastBit Embedded Brain Academy Courses

Click here: https://fastbitlab.com/course1

Post author avatar

FastBitLab

The FastBit Embedded Brain Academy uses the power of internet to bring the online courses related to the field of embedded system programming, Real time operating system, Embedded Linux systems, etc at your finger tip with very low cost. Backed with strong experience of industry, we have produced lots of courses with the customer enrolment over 3000+ across 100+ countries.

Волею судеб мне пришлось разрабатывать прошивку для одного устройства на основе микроконтроллера STM32F103. Функций у устройства много, в том числе и общение с EEPROM подключенным посредством протокола I2C. Кто не знает, микроконтроллеры STM32 во многих своих версиях поддерживают работу по данному протоколу на аппаратном уровне. Это значит, что у микросхемы микроконтроллера присутствуют специальные выводы, которые можно использовать в том числе и для работы по протоколу I2C, а все издержки по этому протоколу выполняются «железом» микроконтроллера.

микросхема, eeprom

Вообще, I2C — штука популярная. Реализуется не так сложно, для его работы требуется всего два сигнальных провода. По одному подаются тактовые импульсы, по второму происходит передача данных, привязанная к тактам первого провода. К шире или выводам I2C можно подключить несколько устройств, они не будут мешать друг-другу, т.к. при обращении к конкретному устройству указывается его уникальный адрес.

Шина I2C не высокоскоростная и предназначена в первую очередь для обмена данными с различными датчиками, модулями и внешними системами. Через шину прокачать много информации не выйдет, но этого и не требуется. Главное, что она проста, дешева и универсальна. Ну много ли данных передает в секунду датчик температуры или давления? Сущие байты. Этого вполне достаточно.

Как правило в шине I2C применяется система с одним ведущим устройством и подключаемыми к нему ведомыми. В качестве ведущего устройства, разумеется, используется микроконтроллер. И он опрашивает подключенные устройства, в надежде получить с них данные. Каким же образом передаются данные по-фактически одному проводу? Конечно, передача данных по одному проводу в I2C не является полнодуплексной. Нет возможности в стандарте по одному проводу передавать и принимать данные одновременно. Поэтому, команды на передачу дает ведущее устройство, а все остальные слушают и отвечают, когда это им позволяется.

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

Для реализации микропрограммы был выбран фреймворк STM32Arduino, так как требовалось использовать некоторые библиотеки, которые легче взять готовые, нежели разрабатывать их заново. К чипу же STM32 подключена обычная микросхема EEPROM на несколько килобит. EEPROM используется для частых записей, для чего не предназначена Flash-память на чипе STM32. Все аппаратные подключения проведены в строгом соответствии с документацией как производителя микроконтроллера, так и микросхемы EEPROM. И именно проверка того, как сделаны аппаратные подключения, надежно ли питание, есть ли все необходимые подтяжки и прочее, должна происходить в самую первую очередь, если возникла проблема. Иначе можно потратить годы на то, чтобы найти программную причину ошибки, особенно там, где ее нет.

В моем случае проблема заключалась в выдаче недостоверных результатов с EEPROM и невозможность записи. Точнее запись проходила, но на конечный осмысленный результат они никак не влияла. Причем неполадка появлялась только после аппаратного перезапуска устройства и примерно один раз из десяти. Присутствие какой-либо адекватной реакции от всех программах слоев, на которые опирается STM32Duino ожидать не стоит. I2C протокол простой и он либо работает, либо нет. И он работал, выдавал данные, причем даже если данные с EEPROM приходили откровенно левые, то никакие ошибки обращения с библиотекой Wire не возникало. Пришлось начать копать интернет в поисках похожих ошибок и методов их решения.

Как оказалось, проблема при работе с I2C на чипах STM32, особенно семейства F103, возникает чуть ли не у каждого второго пользователя чипов. Причем независимо от того, на чем он пишет свой код: HAL, Arduino, Mbed или еще чего. Проблем возникает много, у кого-то ничего не работает сразу, что несколько легче, так как искать ошибку проще, а у других все работает из коробки, но не постоянно. Основные проблемы, на которые натыкаются пользователи кроются в некоторых, назовем их так, особенностях структуры чипов STM32F10x, да ошибках, которые присутствуют в HAL.

Приведу основные причины возникновения неполадок с I2C, полученные после изучения «всего интернета»:

  • Ненадежное аппаратное подключение, ненадежное неверное питание, несоблюдение рекомендаций по подключению.
  • Блокировка шины на стороне микроконтроллера со статусом Busy.
  • Перепутанные выходы, перепутанная инициализация при добавлении второго канала I2C на многоканальных чипах. Ошибка из серии «Я скопировал оттуда, где работало, а тут не работает».

Кстати, последняя ошибка встречается не столько при простом копировании кода, завязанного на работу через I2C, а сколько на его инициализацию. STM32 штука сложная и если невнимательно работать с Cube или писать инициализацию своими руками, то наверняка куда-то может затесаться мизерная ошибочка, которую замыленный глаз уже не в состоянии разглядеть. Вторая же ошибка по большей части связана с неверной (а зачастую с бездумной) инициализацией микроконтроллера, но присутствуют и особенности реализации (читай ошибки) в самом чипе. С ними (обнаруженными и признанными) и объясняется что делать в замечательном документе Errata sheet (ссылка внизу статьи).

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

/* USER CODE BEGIN SysInit */
HAL_RCC_I2C1_CLK_ENABLE();
HAL_Delay(100); HAL_RCC_I2C1_FORCE_RESET();
HAL_Delay(100);
__HAL_RCC_I2C1_RELEASE_RESET();
HAL_Delay(100);
/* USER CODE END SysInit */

В Arduino на STM32 данный блок так же можно применить, но он не помогает, по крайней мере, в моем случае. Пришлось еще немного пораскинуть мозгами и попытаться докопаться до причины проблемы, а потом попытаться ее решить. В моем случае обмен данными с EEPROM по I2C идет без каких-либо проблем. Данные читаются, записываются, никаких ошибок не возникает. Только вот в одном разе из 10 после аппаратной перезагрузки всей системы, EEPROM начинает выдавать совершенно левые данные, при этом никаких ошибок не возникает. С записью тоже в такие моменты не все гладко, проверить-то никак.

Как известно, чипы STM32 многофункциональны и многие из выводов микросхем могут быть использованы под различные функции. У многих микроконтроллеров, не только у STM32, после перезагрузки, некоторые выводы могут переключиться в так называемые неинициализированные состояния. Обычный софтверный разработчик, как правило не задумывается над тем, какой у него уровень на выводах микроконтроллера после его перезагрузки. Высокий? Низкий? Серединный? При использовании фирменного конфигуратора STM32Cube есть возможность настроить инициализацию выводов и некоторых других функций микроконтроллера путем относительно простого конфигурирования. Но данная процедура может быть опущена, а инициализацию можно провести позже, например, при процедуре вызова той или иной функции. Именно последним путем и пошли разработчики STM32Duino. При загрузке микроконтроллера происходит так называемая базовая инициализация функций микроконтроллера, ножки выводов принимают значения по умолчанию. А вот если с данной конкретной ножки требуется другая функция, то ее инициализация происходит при первом вызове соответствующей функции.

В чипах STM32 инициализация происходит очень быстро, ну сами чипы скоростные, это, во-первых, а во-вторых, загрузчик не тормозит загрузку пользовательского кода, так как вызывается при соответствующей аппаратной комбинации. Значит, проблема неинициализированных «ног», когда на них болтается неизвестно что, встает не в полный рост. А вот на других чипах микроконтроллеров, где загрузчик некоторое время ожидает подачу ему сигнала и только потом переходит на пользовательский код, проблема может существенно попортить жизнь. Представьте, что на такой «ноге», которая еще не определилась с уровнем своего сигнала, «висит» управляющий контакт реле. И вот на реле идет жуткая последовательность непонятных сигналов. Что ему делать? Дергаться туда-сюда, пока микроконтроллер не определиться со своим выводом?

Опытный читатель или электронщик, уже догадался, в чем изюминка порылась. Микросхема EEPROM возвращает неверные данные, а библиотека, работающая с I2C, говорит, что все нормально, ошибок нет. Суть нестабильного поведения кроется в следующем. На универсальных чипах STM32F103 многие из выводов многофункциональны. При неверной инициализации или отсутствии инициализации, на «ногах», подключенных к микросхеме EEPROM, может появиться произвольный сигнал, который «сведет с ума» саму микросхему EEPROM (команды на обмен данными с EEPROM та еще китайская азбука, куча условностей, задержек и прочего). Да, она будет как-то реагировать на команды, но вот выдавать данные может совсем не те, что должна. Повторная инициализация I2C в микроконтроллере ничего не даст, так как ведомое устройство уже не в себе и вывести его из себя можно только аппаратной перезагрузкой микросхемы EEPROM (перезагрузка микроконтроллера тут не помогает, по той же причине, что и переинициализация через HAL).

Именно такая ситуация возникла в моем случае. Проблема возникала случайным образом, но статистически она присутствовала. Если код инициализации Wire поместить ближе к началу программного кода, то вероятность возникновения ошибки уменьшается, если отодвинуть его куда-то подальше, то ошибка будет возникать чаще. И спастись от проблемы можно только аппаратным сбросом всего оборудования (передергиванием питания).

Так как же можно избавиться от проблемы «сумасшествия» ведомой микросхемы EEPROM? Очевидно, что нужно максимально быстро проинициализировать соответствующие терминалы ввода-вывода, дабы успеть в тот момент времени, пока EEPROM не начнет жить по своим собственным законам, повинуясь непонятным сигналам с микроконтроллера. Другими словами, подвинуть код инициализации Wire в самое начало программы. Но… Данный трюк не решает полностью описанное поведение EEPROM. Все еще остается вероятность отказа EEPROM (и опыты это подтверждают). Почему? Потому, что выполнение кода инициализации Wire занимает какое-то время, бесценные микросекунды, которых хватает на то, чтоб EEPROM удалились в мир грез и фантазий. Что в этом случае можно сделать?

pinMode(I2C_SCL, OUTPUT);
pinMode(I2C_SDA, OUTPUT);

Оказалось, что достаточно только проинициализировать порты микроконтроллера, ответственные за I2C как выходные цифровые выводы, как можно быстрее. В этом случае цифровое, а скорее аналоговое, шатание отменяется и невменяемость EEPROM тоже. В STM32Duino при инициализации пина как выходящего, он автоматически включается на низкий уровень. Если этого не происходит, например, поменялась идеология разработчиков фреймворка или вышла новая плата, на которой все не так, то дополнительно можно принудительно установить низкий уровень, что должно обеспечить нормальную работоспособность всей связки.

А что же до любителей HAL и особенно STM32Cube? Если работать только на HAL и не прибегать к Cube, как к средству конфигурирования, то проблема будет ровно такой же. Если не применить четкую инициализацию «ног» I2C как можно быстрее, то нормально с EEPROM не поработаешь. Впрочем, с Cube тоже не все так гладко как хотелось бы. Да, утилита помогает провести инициализацию микроконтроллера, которая сама по себе не отличается простотой. Но и тут могут быть нюансы. Во-первых, код инициализации I2C из Cube может быть выполнен в самую последнюю очередь, когда уже поздно, во-вторых, могут наступить и прочие конфликты, связанные с неверной инициализацией (Cube только выглядит просто, на самом деле без понимания туда лезть не стоит). Более подробно о потенциальных проблемах можно почитать в ссылках ниже.

Полезные ссылки:

  1. STM32 WRITE AND READ EEPROM OVER I2C BUS — статья детально разжевывающая методы обращения с EEPROM подключенным посредством I2C.
  2. STM32F10xx8 STM32F10xxB Errata sheet (medium-density device limitations) — бюллетень от STMicroelectronics описывающий возможные затруднения и способы борьбы с ними по различным блокам своих микроконтроллеров. Проблем работы с I2C в документе указано аж 7.
  3. STM32 — I2C — HAL_BUSY — статья что делать, если возникает Busy.


Вторая и заключительная часть моего перевода главы по модулю I2C в STM32. Первая часть по ссылке. В данной части будут описаны аспекты практического использования модуля HAL I2C и его структура.

14.2 Модуль HAL I2C

Для работы с периферийным устройством I2C CubeHAL определяет структуру I2C_HandleTypeDef, которая объявлена следующим образом:

typedef struct {
    I2C_TypeDef                 *Instance;  // I2C registers base address
    I2C_InitTypeDef             Init;       // I2C communication parameters
    uint8_t                     *pBuffPtr;  // Pointer to I2C transfer buffer
    uint16_t                    XferSize;   // I2C transfer size
    __IO uint16_t               XferCount;  // I2C transfer counter
    DMA_HandleTypeDef           *hdmatx;    // I2C Tx DMA handle parameters
    DMA_HandleTypeDef           *hdmarx;    // I2C Rx DMA handle parameters
    HAL_LockTypeDef             Lock;       // I2C locking object
    __IO HAL_I2C_StateTypeDef   State;      // I2C communication state
    __IO HAL_I2C_ModeTypeDef    Mode;       // I2C communication mode
    __IO uint32_t               ErrorCode;  // I2C Error code
} I2C_HandleTypeDef;

Давайте проанализируем наиболее важные поля этой структуры С:

  • Instance: указатель на дескриптор I2C интерфейса, который мы будем использовать. Например, I2C1 является дескриптором первого I2C периферийного устройства.
  • Init: экземпляр структуры I2C_InitTypeDef, используемой для настройки периферийного устройства. Более подробно об этой структуре будет рассказано ниже.
  • pBuffPtr: указатель на внутренний буфер, используемый для временного хранения данных, получаемых и передаваемых в интерфейс и из него. Используется, когда I2C работает в режиме прерывания и данный буфер не должен изменяться из приложения пользователя.
  • hdmatx, hdmarx: указатель на экземпляры структуры DMA_HandleTypeDef, используемые, когда периферийное устройство I2C работает в режиме DMA.

Настройка I2C выполняется с использованием экземпляра структуры I2C_InitTypeDef, которая объявлена следующим образом:

typedef struct {
    uint32_t ClockSpeed;       // Specifies the clock frequency.
    uint32_t DutyCycle;        // Specifies the I2C fast mode duty cycle.
    uint32_t OwnAddress1;      // Specifies the first device own address.
    uint32_t OwnAddress2;      // Specifies the second device own address if dual addressing mode is selected.
    uint32_t AddressingMode;   // Specifies if 7-bit or 10-bit addressing mode is selected.
    uint32_t DualAddressMode;  // Specifies if dual addressing mode is selected.
    uint32_t GeneralCallMode;  // Specifies if general call mode is selected.
    uint32_t NoStretchMode;    // Specifies if nostretch mode is selected.
} I2C_InitTypeDef;

Рассмотрим наиболее важные поля этой структуры:

  • ClockSpeed: в этом поле указывается скорость интерфейса I2C и она должна соответствовать спецификации шины (standard mode, fast mode и т.д.). Однако, установка значения этого поля возможна также через поле DutyCycle как мы увидим далее. Максимальное значение этого поля для большинства микроконтроллеров STM32 составляет 400 кГц, что означает, что микроконтроллеры STM32 поддерживают режимы вплоть до fast mode. Микроконтроллеры STM32F0/F3/F7/L0/L4 составляют исключение из этого правила (см. Таблицу 1) и поддерживают также режим fast mode plus (1 МГц). В этих микроконтроллерах поле ClockSpeed заменено другим, называемым Timing. Значение конфигурации для поля Timing вычисляется по-другому и здесь оно не будет рассматриваться. У ST имеется специальный апноут AN4235, в котором объясняется, как вычислить точное значение для этого поля в соответствии с требуемой скоростью шины. Тем не менее, CubeMX может сгенерировать правильное значение конфигурации за вас.
Таблица 2. Характеристики линий шины I2C для standard, fast, и fast-mode plus режимов
  • DutyCycle: это поле, которое доступно только в тех микроконтроллерах, которые не поддерживают режим fast mode plus, и задает соотношение между tLOW и tHIGH линии SCL. Может принимать значения I2C_DUTYCYCLE_2 и I2C_DUTYCYCLE_16_9 для указания рабочих циклов 2:1 и 16:9 соответственно. Выбирая заданный режим синхронизации, мы можем поделить частоту тактирования периферии, чтобы достичь желаемой тактовой частоты I2C. Чтобы лучше понять роль этого параметра, нам необходимо рассмотреть некоторые фундаментальные концепции шины I2C. В главе 11 мы увидели, что рабочий цикл представляет собой процентное соотношение от одного периода тактовой частоты (к примеру 10 мкс), в течение которого сигнал активен. Для каждой из скоростей шины I2C спецификация точно определяет минимальные значения tLOW и tHIGH. Таблица 2, извлеченная из UM10204 от NXP, показывает значения tLOW и tHIGH для конкретной скорости связи (значения выделены желтым цветом). Соотношение этих двух значений и является рабочим циклом, который не зависит от скорости связи. Например, период 100 кГц соответствует значению 10 мкс, но tLOW + tHIGH из таблицы 2 составляет менее 10 мкс (4 мкс + 4.7 мкс = 8.7 мкс). Таким образом, соотношение фактических значений может изменяться, если соблюдаются минимальные значения времени tLOW и tHIGH (4.7 мкс и 4 мкс соответственно). Смысл этих соотношений состоит в том, чтобы проиллюстрировать, что тайминги I2C различны для разных режимов I2C. Это не обязательные соотношения, которые должны соблюдаться периферийными устройствами STM32. Например, tHIGH = 4 мкс и tLOW = 6 мкс составят соотношение равное 0.67, которое по прежнему совместимо с таймингами стандартного режима (100 кГц) (поскольку tHIGH = 4 мкс и tLOW > 4.7 мкс, а их сумма по равна 10 мкс). I2C в микроконтроллерах STM32 определяет следующие рабочие циклы (отношения). Для standard mode это соотношение составляет 1:1. Это означает, что tLOW = tHIGH = 5 мкс. Для fast mode можно использовать два соотношения: 2:1 или 16:9. Отношение 2:1 означает, что 4 мкс (= 400 кГц) получаются при tLOW = 2.66 мкс tHIGH = 1.33 мкс, оба значения выше, указанных в таблице 2 (0.6 мкс и 1.3 мкс). Соотношение 16:9 означает, что 4 мкс получаются при tLOW = 2.56 мкс tHIGH = 1.44 мкс, оба значение также выше, указанных в таблице 2. Когда использовать соотношение 2:1 вместо 16:9 и наоборот? Это зависит от тактовой частоты периферии (PCLK1). Отношение 2:1 означает, что 400 кГц достигаются путем деления источника тактовой частоты на 3 (2 + 1). Это означает, что PCLK1 должен быть кратным 1.2 МГц (400 кГц * 3). Использование соотношения 16:9 означает, что мы делим PCLK1 на 25. Т.е. максимальную частоту шины I2C можно получить при PCLK1 кратной 10 МГц (400 кГц * 25). Таким образом, правильный выбор рабочих циклов зависит от эффективной скорости шины APB1 и требуемой частоты SCL I2C. Важно подчеркнуть, что даже если частота SCL ниже, чем 400 кГц (например, используя соотношение 16:9 при частоте PCLK1 8 МГц, мы можем достигнуть максимальной скорости связи, равной 360 кГц), мы все равно удовлетворяем требованиям спецификации для режима fast mode I2C (400 кГц это верхний лимит скорости).
  • OwnAddress1 , OwnAddress2: I2C в микроконтроллерах STM32 может использоваться для разработки как ведущих, так и ведомых устройств I2C. При разработке ведомых устройства I2C поле OwnAddress1 позволяет указать адрес ведомого устройства I2C: периферийное устройство автоматически определяет данный адрес в шине I2C и автоматически запускает все связанные события (например, оно может генерировать соответствующее прерывание, чтобы приложение могло начать новую транзакцию на шине). I2C поддерживает 7- или 10-битную адресацию, а также 7-битный режим двойной адресации: в этом случае мы можем указать два отдельных 7-битных адреса, чтобы ведомое устройство могло отвечать на запросы, отправленные на оба адреса.
  • AddressingMode: это поле может принимать значения I2C_ADDRESSINGMODE_7BIT или I2C_ADDRESSINGMODE_10BIT для указания 7- или 10-битного режима адресации соответственно.
  • DualAddressMode: это поле может принимать значения I2C_DUALADDRESS_ENABLE или I2C_DUALADDRESS_DISABLE для включения/отключения 7-битного режима двойной адресации.
  • GeneralCallMode: общий вызов это своего рода широковещательная адресация в протоколе I2C. Специальный адрес 0x0000 000, который используется для отправки сообщения все устройствам на одной шине. Общий вызов является необязательной функцией, и, установив в этом поле значение I2C_GENERALCALL_ENABLE, I2C будет генерировать события при получении адреса общего вызова. В этой книге не будет рассматриваться данный режим.
  • NoStretchMode: это поле может принимать значения I2C_NOSTRETCH_ENABLE или I2C_NOSTRETCH_DISABLE и используется для отключения/включения необязательного режима удержания тактовых импульсов (обратите внимание, что, установив это поле в значение I2C_NOSTRETCH_ENABLE, вы отключите данный режим). Для получения дополнительной информации об этом дополнительном режиме смотрите UM10204 от NXP и референс-мануал на ваш микроконтроллер.

Как обычно для настройки периферийного устройства I2C мы используем функцию:

HAL_StatusTypeDef HAL_I2C_Init (I2C_HandleTypeDef *hi2c);

которая принимает в качестве параметра указатель на экземпляр структуры I2C_HandleTypeDef, рассматриваемую ранее.

14.2.1 Использование периферийного устройства I2C в режиме Master

Теперь проанализируем основные функции CubeHAL для использования I2C в режима ведущего или Master. Для выполнения транзакции по шине I2C в режиме записи CubeHAL предоставляет функцию:

HAL_StatusTypeDef HAL_I2C_Master_Transmit (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

где:

  • hi2c: это указатель на экземпляр структуры I2C_HandleTypeDef, которая идентифицирует периферию I2C;
  • DevAddress: это адрес ведомого устройства, длина которого может быть 7 или 10 бит в зависимости от конкретной микросхемы;
  • pData: указатель на массив длинной, равной параметру Size, содержащий последовательность байт, которую мы хотим передать;
  • Timeout: представляет собой максимальное время, выраженное в миллисекундах, которое мы готовы отвести для полного завершения передачи данного объема данных. Если передача не завершена в указанный временной промежуток, функция прерывается и возвращает значение HAL_TIMEOUT, в противном случае возвращается значение HAL_OK, если не произошло никаких других ошибок. Более того, мы можем передать функции параметр, равный HAL_MAX_DELAY (0xFFFF FFFF), чтобы неопределенно долго ждать завершения транзакции.

Для выполнения транзакции в режиме чтения мы можем использовать следующую функцию:

HAL_StatusTypeDef HAL_I2C_Master_Receive (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

Обе предыдущие функции выполняют транзакции в режиме опроса. Для транзакций на основе прерываний мы можем использовать функции:

HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);

HAL_StatusTypeDef HAL_I2C_Master_Receive_IT (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);

Эти функции работают так же, как и другие процедуры, описанные в предыдущих главах (например, те, которые касаются передачи UART в режиме прерывания). Чтобы использовать их правильно нам нужно включить соответствующий вектор прерывания ISR и выполнить вызов функции HAL_I2C_EV_IRQHandler(), которая, в свою очередь, вызывает HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c), чтобы сигнализировать о завершении передачи в режиме записи, или HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c), чтобы сигнализировать об окончании передачи в режиме чтения. За исключением семейств STM32F0 и STM32L0 I2C во всех микроконтроллерах STM32 использует отдельное прерывание для сигнализации об ошибках (см. Таблицу векторов прерываний для вашего микроконтроллера). По этой причине в соответствующем ISR нам нужно вызвать HAL_I2C_ER_IRQHandler(), который вызовет HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) в случае ошибки. Существует десять различных функций обратного вызова, используемых в CubeHAL. В Таблице 3 перечислены они все вместе с ISR, который вызывает обратный вызов.

Таблица 3. Доступные обратные вызовы CubeHAL I2C в режиме прерывания или DMA.

Callback Функция ISR Описание
HAL_I2C_MasterTxCpltCallback() I2Cx_EV_IRQHandler() Сигнализирует о том, что передача от ведущего к ведомому завершена (периферийное устройство работает в режиме ведущего).
HAL_I2C_MasterRxCpltCallback() I2Cx_EV_IRQHandler() Сигнализирует о том, что передача от ведомого к ведущему завершена (периферийное устройство работает в режиме ведущего).
HAL_I2C_SlaveTxCpltCallback() I2Cx_EV_IRQHandler() Сигнализирует о том, что передача от ведомого к ведущему завершена (периферийное устройство работает в режиме ведомого).
HAL_I2C_SlaveRxCpltCallback() I2Cx_EV_IRQHandler() Сигнализирует о том, что передача от ведущего к ведомому завершена (периферийное устройство работает в режиме ведомого).
HAL_I2C_MemTxCpltCallback() I2Cx_EV_IRQHandler() Сигнализирует о том, что передача от ведущего к внешней памяти завершена (вызывается при использовании функции HAL_I2C_Mem_xxx() и периферийное устройство работает в режиме ведущего).
HAL_I2C_MemRxCpltCallback() I2Cx_EV_IRQHandler() Сигнализирует о завершении передачи из внешней памяти в ведущее устройство (вызывается при использовании функции HAL_I2C_Mem_xxx() и периферийное устройство работает в режиме ведущего)
HAL_I2C_AddrCallback() I2Cx_EV_IRQHandler() Сигнализирует о том, что ведущее устройство разместило адрес ведомого в шине (периферийное устройство работает в режиме ведомого)
HAL_I2C_ListenCpltCallback() I2Cx_EV_IRQHandler() Сигнализирует о том, что режим прослушивания завершен (это происходит, когда выдается условие STOP и периферийное устройство работает в режиме ведомого — подробнее об этом позже).
HAL_I2C_ErrorCallback() I2Cx_ER_IRQHandler() Сигнализирует о возникновении ошибки (периферийное устройство работает как в режиме ведущего, так и в режиме ведомого).
HAL_I2C_AbortCpltCallback() I2Cx_ER_IRQHandler() Сигнализирует о том, что условие STOP активно и транзакция I2C была прервана (периферийное устройство работает как в режиме ведущего, так и в режиме ведомого).

И наконец функции:

HAL_StatusTypeDef HAL_I2C_Master_Transmit_DMA (I2C_HandleTypeDef *hi2c,uint16_t DevAddress, uint8_t *pData, uint16_t Size);

HAL_StatusTypeDef HAL_I2C_Master_Receive_DMA (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);

позволяют выполнять транзакции I2C с использованием DMA.

Для создания законченных и полностью работоспособных примеров нам необходимо внешнее устройство, способное взаимодействовать через I2C интерфейс, поскольку платы Nucleo не имеют подобной периферии. По этой причине мы будем использовать внешнюю EEPROM память 24LCxx. Это действительно популярное семейство последовательных EEPROM, которые стали своего рода стандартом в электронной промышленности. Они очень дешевы (обычно из цена составляет несколько десятков центов), выпускаются в нескольких вариантах корпусов (от «старых» P-DIP до современных и компактных WLCP), обеспечивают хранение данных более 200 лет и отдельные страницы памяти могут быть перезаписаны более миллиона раз. Более того, многие производители интегральных микросхем имеют собственные совместимые версии этой серии памяти (ST также предоставляет собственный набор EEPROM, совместимых с 24LCxx). Эта память также популярна как и 555 таймер и я уверен, что она еще будет актуальна весьма долгое время.

Рисунок 6. Распиновка EEPROM памяти 24LCxx в корпусе PDIP-8

Наши примеры будут основаны на модели 24LC64, которая является EEPROM памятью на 64 кбита (это означает, что память может хранить 8 кБ или 8192 байта). Распиновка версии в корпусе PDIP-8 представлена на рисунке 6. A0, A1 и A2 используются для установки LSB битов адреса I2C, как показано на рисунке 7: если один из этих контактов привязан к земле, то соответствующий бит установлен в 0, если же он подтянут к питанию, то бит устанавливается в 1. Если все три контакта подключены к земле, то адрес I2C соответствует 0xA0.

Рисунок 7. Как формируется I2C адрес в 24LCxx.

Вывод WP это вывод защиты от записи: если он подключен к земле, мы можем писать в отдельные ячейки памяти. При подключении к питанию операции записи не имеют никакого эффекта. Поскольку I2C1 имеется на одних и тех же контактах на всех платах Nucleo, на рисунке 8 показан правильный способ подключения 24LCxx EEPROM к Arduino-совместимому разъему всех шестнадцати плат Nucleo.

Как было сказано ранее, EEPROM на 64 кбит имеет 8192 адреса в диапазоне от 0x000 до 0x1FFF. Запись байта выполняется путем отправки по шине I2C адреса EEPROM, старшей половины адреса ячейки памяти, за которой следует младшая часть, и значения, которое нужно сохранить в этой ячейке, закрывая транзакцию условием STOP.

Рисунок 8. Как подключить 24LCxx EEPROM память к Nucleo.

Предполагая, что мы хотим сохранить значение 0x4C в ячейке памяти 0x320, на Рисунке 9 показана правильная последовательность транзакций. Адрес 0x320 разделен на две части: первая часть, равная 0x3, передается в первую очередь, а младший байт 0x20 сразу следующим. Затем отправляются данные для сохранения в ячейку памяти. Также можно отправить несколько байт для хранения: внутренний счетчик адреса инкрементируется с каждым новым байтом. Это позволяет сократить время транзакции и увеличить общую пропускную способность.

Бит ACK, устанавливаемый EEPROM после последнего отправленного байта, не означает, что данные были эффективно сохранены в памяти. Отправленные данные хранятся во временном буфере, поскольку EEPROM стирается постранично, а не индивидуально по ячейкам. Страница (состоит из 32 байт) обновляется при каждой операции записи и переданные байты сохраняются только в конце данной операции. В течение времени стирания каждая команда, отправленная EEPROM, будет игнорироваться. Чтобы определить, когда операция записи полностью завершена, нужно использовать запрос подтверждения. Запрос состоит из, отправляемого ведущим, условия START, за которым следует адрес ведомого устройства и управляющий байт для команды записи (бит R/W установлен в 0). Если устройство все еще занято циклом записи, ACK не будет возвращен в ответ на запрос. Если ACK не возвращается, то запрос подтверждения должен быть послан повторно. По завершению цикла записи устройство возвратит ACK на запрос подтверждения и ведущий может продолжить отправку следующей команды записи или чтения.

Рисунок 9. Выполнение операции записи в EEPROM 24LCxx.

Операции чтения инициируются точно так же, как и операции записи, за исключением того, что бит R/W устанавливается в 1. Существует три основных типа операций чтения: чтение текущего адреса, произвольное чтение и чтение последовательности. В этой главе мы сосредоточим наше внимание только на режиме произвольного чтения, оставляя читателю возможность изучить другие режимы самостоятельно.

Операции произвольного чтения позволяют ведущему устройству получать доступ к любой ячейки памяти случайным образом. Чтобы выполнить этот тип операции чтения, адрес микросхемы памяти должен быть отправлен в первую очередь. Адрес 24LCxx отправляется как часть операции записи, т.е. бит R/W устанавливается в 0. После отправки адреса ведущий генерирует условие RESTART после получения подтверждения ACK (Память 24LCxx спроектирована таким образом, что она работает одинаково, даже если мы завершаем транзакцию с помощью условия STOP, а затем немедленно запускаем новую в режиме чтения. Эта гибкость позволит нам создать первый пример этой главы, как мы увидим далее. Примечание автора). Это завершает операцию записи, но не раньше, чем будет установлен внутренний счетчик адресов. Затем ведущий снова отправляет адрес ведомого, но уже с битом R/W установленным в 0. Затем 24LCxx выдает ACK и передает 8-битное слово данных. Ведущий не подтверждает передачу и генерирует условие STOP, которое заставляет EEPROM прекратить передачу (см. Рисунок 10). После случайной команды чтения внутренний счетчик адресов будет указывать на местоположение адреса ячейки памяти, следующей сразу за той, что была прочитана ранее.

Рисунок 10. Выполнение операции чтения произвольной ячейки памяти EEPROM 24LCxx.

Наконец мы готовы полностью описать данный пример. Создадим две простые функции с именами Read_From_24LCxx() и Write_To_24LCxx(), которые позволяют записывать и читать данные из памяти 24LCxx, используя CubeHAL. Затем проверим работу этих функций, сохранив строку внутри EEPROM и прочитав ее обратно: если исходная строка равна той, что считана из EEPROM, светодиод LD2 на плате Nucleo начнет мигать.

Имя файла: src/main-ex1.c

int main(void) 
{
    const char wmsg[] = "We love STM32!";
    char rmsg[20];

    HAL_Init();
    Nucleo_BSP_Init();

    MX_I2C1_Init();

    Write_To_24LCxx(&hi2c1, 0xA0, 0x1AAA, (uint8_t*)wmsg, strlen(wmsg)+1);
    Read_From_24LCxx(&hi2c1, 0xA0, 0x1AAA, (uint8_t*)rmsg, strlen(wmsg)+1);

    if(strcmp(wmsg, rmsg) == 0) 
    {
        while(1) 
        {
            HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
            HAL_Delay(100);
        }
    }
    
    while(1);
}

/* I2C1 init function */
static void MX_I2C1_Init(void) 
{
    GPIO_InitTypeDef GPIO_InitStruct;

    /* Peripheral clock enable */
    __HAL_RCC_I2C1_CLK_ENABLE();

    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000;
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = 0x0;
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.OwnAddress2 = 0;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;

    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    HAL_I2C_Init(&hi2c1);
}

Давайте проанализируем приведенный выше фрагмент кода, начиная с функции MX_I2C1_Init(). Функция начинается с включения тактирования I2C1, чтобы можно было работать с регистрами периферийного устройства. Затем устанавливается скорость шины (в нашем случае 100 кГц и в этом случае настройка рабочего цикла игнорируется, потому что рабочий цикл зафиксирован на соотношении 1:1, когда шина работает на скоростях ниже и равной 100 кГц). Далее происходит настройка выводов PB8 и PB9 так, чтобы они функционировали как линии SCL и SDA соответственно.

Процедура main() очень проста: она сохраняет строку «We love STM32!» в ячейку памяти по адресу 0x1AAA, затем строка считывается из EEPROM и сравнивается с исходной. Здесь требуется пояснить почему сохранение и чтение в буфер производится с длиной строки, равной strlen(wmsg)+1. Это потому, что процедура C strlen() возвращает длину строки без учета символа конца строки ‘\0’. Без сохранения этого символа и последующего чтения из EEPROM, процедура strcmp() не сможет вычислить точную длину строки.

HAL_StatusTypeDef Read_From_24LCxx(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) 
{
    HAL_StatusTypeDef returnValue;
    uint8_t addr[2];

    /* We compute the MSB and LSB parts of the memory address */
    addr[0] = (uint8_t) ((MemAddress & 0xFF00) >> 8);
    addr[1] = (uint8_t) (MemAddress & 0xFF);

    /* First we send the memory location address where start reading data */
    returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, addr, 2, HAL_MAX_DELAY);
    if(returnValue != HAL_OK)
        return returnValue;

    /* Next we can retrieve the data from EEPROM */
    returnValue = HAL_I2C_Master_Receive(hi2c, DevAddress, pData, len, HAL_MAX_DELAY);

    return returnValue;
}

HAL_StatusTypeDef Write_To_24LCxx(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) 
{
    HAL_StatusTypeDef returnValue;
    uint8_t *data;

    /* First we allocate a temporary buffer to store the destination memory
    * address and the data to store */
    data = (uint8_t*)malloc(sizeof(uint8_t)*(len+2));

    /* We compute the MSB and LSB parts of the memory address */
    data[0] = (uint8_t) ((MemAddress & 0xFF00) >> 8);
    data[1] = (uint8_t) (MemAddress & 0xFF);

    /* And copy the content of the pData array in the temporary buffer */
    memcpy(data+2, pData, len);

    /* We are now ready to transfer the buffer over the I2C bus */
    returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, data, len + 2, HAL_MAX_DELAY);
    if(returnValue != HAL_OK)
        return returnValue;

    free(data);

    /* We wait until the EEPROM effectively stores data in memory */
    while(HAL_I2C_Master_Transmit(hi2c, DevAddress, 0, 0, HAL_MAX_DELAY) != HAL_OK);

    return HAL_OK;
}

Теперь мы можем сфокусировать наше внимание на двух процедурах для использования 24LCxx EEPROM. Обе принимают одни и те же параметры:

  • адрес ведомого устройства I2C памяти EEPROM (DevAddress);
  • адрес памяти, с которого начинается запись / чтение данных (MemAddress);
  • указатель на буфер памяти, используемый для обмена данными с EEPROM (pData);
  • длина буфера данных для записи / чтения (len);

Функция Read_From_24LCxx() начинается с вычисления двух половин адреса памяти (MSB и LSB). Затем обе части отправляются в шину с использованием функции HAL_I2C_Master_Transmit(). Как было сказано ранее, память 24LCxx спроектирована так, что она автоматически устанавливает внутренний счетчик адресов в переданный адрес памяти. Мы можем запустить новую транзакцию в режиме чтения, чтобы получить данные из EEPROM из переданного адреса ячейки памяти.

Функция Write_To_24LCxx() делает практически то же самое, но несколько иным способом. Она должна соответствовать протоколу 24LCxx, описанному на Рисунке 9, который немного отличается от того, что на Рисунке 8. Это означает, что мы не можем использовать две отдельные транзакции для адреса ячейки памяти и данных для записи, оба этих действия должны быть объединены в одну транзакцию I2C. По этой причине мы используем временный динамический буфер, который содержит обе части адреса ячейки памяти и данные, которые необходимо записать в EEPROM. Теперь можно выполнить транзакцию по шине I2C, а затем подождать, пока EEPROM завершит передачу данных в ячейку памяти.

14.2.1.1 Операции I/O MEM

Протокол, используемый 24LCxx EEPROM в действительности является общим для большей части устройств I2C, которые имеют адресуемые в памяти регистры для записи/чтения. Например, многие датчики I2C, такие как HTS221 от ST, используют один и тот же протокол. По этой причине инженеры ST уже внедрили определенные процедуры в CubeHAL, которые выполняют ту же работу, что и Read_From_24LCxx() и Write_To_24LCxx() только быстрее и лучше. Функции:

HAL_StatusTypeDef HAL_I2C_Mem_Write (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

HAL_StatusTypeDef HAL_I2C_Mem_Read (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

позволяют записывать и читать данные из устройств I2C с адресуемой памятью с одним заметным отличием: функция HAL_I2C_Mem_Write() не предназначена для ожидания завершения цикла записи, как мы делали в предыдущем примере. Но и для этой операции HAL предоставляет специальную и более переносимую процедуру:

HAL_StatusTypeDef HAL_I2C_IsDeviceReady (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout);

Эта функция принимает в качестве параметра максимальное количество попыток проверки доступности устройства в шине перед тем как возвратить ошибку, но если передать HAL_MAX_DELAY в качестве значения Timeout, то мы сможем передать 1 в параметре Trials. Когда устройство доступно, функция возвращает HAL_OK. В противном случае она возвращает значение HAL_BUSY.

Итак, функция main, написанная ранее может быть переписана следующим образом:

int main(void) 
{
    char wmsg[] ="We love STM32!";
    char rmsg[20];

    HAL_Init();
    Nucleo_BSP_Init();

    MX_I2C1_Init();

    HAL_I2C_Mem_Write(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)wmsg, strlen(wmsg)+1, HAL_MAX_DELAY);
    while(HAL_I2C_IsDeviceReady(&hi2c1, 0xA0, 1, HAL_MAX_DELAY) != HAL_OK);

    HAL_I2C_Mem_Read(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)rmsg, strlen(wmsg)+1, HAL_MAX_DELAY);

    if(strcmp(wmsg, rmsg) == 0) 
    {
        while(1) 
        {
            HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
            HAL_Delay(100);
        }
    }

    while(1);
}

Вышеуказанные API работают в режиме опроса, но CubeHAL также предоставляет соответствующие подпрограммы для выполнения транзакций в режиме прерывания и DMA. Как обычно, эти другие API имеют аналогичную сигнатуру функции, с одним только отличием: функциями обратного вызова, используемыми для оповещения об окончании передачи, являются HAL_I2C_MemTxCpltCallback() и HAL_I2C_MemRxCpltCallback(), как показано в таблице 3.

14.2.1.2 Комбинированные транзакции

Последовательность транзакций при операции чтения памяти 24LCxx EEPROM относится к категории комбинированных транзакций. Перед инвертированием направления передачи от записи к чтению используется условие RESTART. В первом примере мы смогли использовать две отдельные транзакции внутри Read_From_24LCxx(), потому что 24LCxx EEPROM спроектирована для подобного подхода. Это возможно благодаря внутреннему счетчику адресов: первая транзакция устанавливает счетчик адресов в требуемое местоположение; вторая, выполняемая в режиме чтения, извлекает данные из EEPROM, начиная с этого места. Однако, это не только уменьшает максимальную пропускную способность, но, что более важно, часто приводит к невозможности портировать код: существует некоторые категории устройств I2C, которые строго придерживаются протокола I2C и реализуют комбинированные транзакции в соответствии со спецификацией, используя условие RESTART (поэтому они не совместимы с использованием условия STOP в середине транзакции).

CubeHAL предоставляет две выделенные функции для обработки комбинированных транзакций или, как их называют в CubeHAL, последовательных передач:

HAL_I2C_Master_Sequential_Transmit_IT (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size,uint32_t XferOptions);

HAL_I2C_Master_Sequential_Receive_IT (I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t XferOptions);

По сравнению с другими функциями, которые мы видели ранее, единственный релевантный параметр, который здесь можно выделить, это XferOptions. Он может принимать одно из значений, указанных в Таблице 4 и используется для управления генерацией условий START / RESTART / STOP в одной транзакции. Обе функции работают таким образом. Давайте предположим, что мы хотим прочитать n-байт из 24LCxx EEPROM. Согласно протоколу I2C, мы должны выполнить следующие операции (см. Рисунок 10):

  1. Мы должны начать новую транзакцию в режиме записи, выдав условие START, за которым следует адрес ведомого устройства;
  2. Затем передаем два байта, содержащие MSB и LSB части адреса ячейки памяти;
  3. После выдаем условие RESTART и передаем адрес ведомого устройства с последним битом, установленным в 1, чтобы начать транзакцию чтения;
  4. Ведомое устройство начинает передавать байты данных побайтно, пока мы не завершим транзакцию, выдав условие NACK или STOP.

Таблица 4. Значения параметра XferOptions для генерации условий STAR / RESTART / STOP.

Вариант передачи Описание
I2C_FIRST_FRAME Эта опция позволяет генерировать только условие START, не генерируя окончательное условие STOP в конце передачи.
I2C_NEXT_FRAME Эта опция позволяет генерировать RESTART перед передачей данных при изменении направления передачи (т.е. мы вызываем HAL_I2C_Master_Sequential_Transmit_IT() после
HAL_I2C_Master_Sequential_Receive_IT() или наоборот), или это позволяет управлять только новыми данными для передачи без изменения направления и без окончательного условия STOP в обоих случаях.
I2C_LAST_FRAME Эта опция позволяет генерировать RESTART перед передачей данных при изменении направления передачи (т.е. мы вызываем HAL_I2C_Master_Sequential_Transmit_IT() после
HAL_I2C_Master_Sequential_Receive_IT() или наоборот), или это позволяет управлять только новыми данными для передачи без изменения направления и с окончательным условием STOP в обоих случаях.
I2C_FIRST_AND_LAST_FRAME Последовательная передача не используется. Обе процедуры работают одинаково для функций HAL_I2C_Master_Transmit_IT() и HAL_I2C_Master_Receive_IT().

Используя процедуры последовательной передачи, мы можем действовать следующим образом:

  1. Вызываем процедуру HAL_I2C_Master_Sequential_Transmit_IT(), передавая ей адрес ведомого устройства и два байта адреса ячейки памяти, в качестве параметра передаем значение I2C_FIRST_FRAME, чтобы функция генерировала условие START без выдачи условия STOP после отправки двух байт адреса;
  2. Далее вызываем HAL_I2C_Master_Sequential_Receive_IT(), передавая в качестве параметров адрес ведомого устройства, указатель на буфер, используемый для чтения данных из памяти, количество байт данных для чтения из EEPROM и значение I2C_LAST_FRAME, так что функция сгенерирует условие RESTART и завершает транзакцию в конце передачи, выдав условие STOP.

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

На момент написания этой главы в последних релизах HAL для семейств F1 и L0 не были предусмотрены процедуры последовательной передачи. Я думаю, что ST активно работает над этим вопросом и в следующих релизах мы их увидим.

По той же причине владельцы плат Nucleo-F103RB и Nucleo-L0XX не смогут выполнить примеры, связанные с использованием интерфейса I2C в режиме ведомого.

14.2.1.3 Замечание о конфигурации тактирования в семействах STM32F0/L0/L4

В семействах STM32F0/L0 можно выбрать разные источники тактирования для синхронизации I2C1. Это связано с тем, что в этих семействах интерфейс I2C1 способен работать даже в некоторых режимах с пониженным энергопотреблением, что позволяет активировать микроконтроллер, когда I2C работает в режиме ведомого и сконфигурированный адрес ведомого устройства попадает в шину. Для более подробной информации обратитесь к настройке тактирования CubeMX.

В микроконтроллерах STM32L4 можно выбрать источник тактирования для всех интерфейсов I2C.

14.2.2 Использование I2C периферии в режиме ведомого (Slave mode)

В настоящее время можно приобрести большое количество модулей типа System-on-Board (SoB). Обычно это небольшие печатные платы, на которых есть одна или несколько микросхем, специализирующиеся на выполнении какой-либо актуальной задачи. Модули GPRS и GPS или мультисенсорные платы являются примерами SoB модулей. Эти модули припаиваются к основной плате, благодаря тому, что на их сторонах имеются контакты для пайки, также известные как «зубчатые отверстия». На рисунке 11 показан модуль INEMO-M1 от ST, который представляет собой интегрированная и программируемый модуль с STM32F103 и двумя высокоинтегрированными MEMS датчиками (6-осевой цифровой электронный компас и 3-осевой цифровой гироскоп).

Рисунок 11. Модуль INEMO-M1 от ST.

Микроконтроллер на подобных платах обычно поставляется с предварительно запрограммированной прошивкой, которая специализируется на выполнении хорошо поставленной задачи. Плата хоста также может содержать еще одну программируемую микросхему, может быть другой микроконтроллер или что-то подобное. Основная плата взаимодействует с SoB, используя хорошо известный протокол связи, которым обычно являются UART, шина CAN, SPI или шина I2C. По этой причине довольно часто микроконтроллеры STM32 программируются так, чтобы они работали в режиме ведомого.

CubeHAL предоставляет весь необходимый инструментарий для простой разработки приложений с I2C. Процедуры в режиме ведомого идентичны тем, которые используются для программирования в режиме ведущего или master mode. Например, следующие процедуры используются для передачи/приема данных в режиме прерывания:

HAL_StatusTypeDef HAL_I2C_Slave_Transmit_IT (I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size);

HAL_StatusTypeDef HAL_I2C_Slave_Receive_IT (I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size);

Точно так же процедуры обратного вызова, вызываемые после окончания передачи/приема данных выглядят следующим образом:

void HAL_I2C_SlaveTxCpltCallback (I2C_HandleTypeDef *hi2c);

void HAL_I2C_SlaveRxCpltCallback (I2C_HandleTypeDef *hi2c);

Теперь рассмотрим полный пример, который показывает, как разрабатывать приложения с ведомым контроллером I2C с использованием CubeHAL. Реализуем своего рода цифровой датчик температуры с интерфейсом I2C, похожий на большинство цифровых датчиков температуры (например, популярный TMP275 от TI или HT221 от ST). Этот «датчик» будет работать с использованием трех регистров:

  • Регистр WHO_AM_I, используемый для проверки правильности работы I2C интерфейса, этот регистр возвращает фиксированное значение 0xBC.
  • Два, связанных с температурой, регистра, называемые TEMP_OUT_INT и TEMP_OUT_FRAC, которые содержат целую и дробную часть полученной температуры, например, если измеренное значение температуры равно 27.34°C, то регистр TEMP_OUT_INT будет содержать значение 27, а регистр TEMP_OUT_FRAC — значение 34.
Рисунок 12. Протокол I2C, используемый для чтения внутреннего регистра нашего ведомого устройства.

Наш датчик будет спроектирован для ответа на действительно простой протокол, основанный на комбинированных транзакциях, который показан на рисунке 12. Как можно заметить, единственное заметное отличие от протокола, используемого с EEPROM 24LCxx, в том, что в режиме чтение произвольного участка памяти, размер регистра памяти равен одному байту.

В примере представлены реализации как для ведомого, так и для ведущего устройства: макрос SLAVE_BOARD, определенный на уровне проекта, управляет компиляцией двух участков кода. В примере требуется две платы Nucleo (К сожалению, когда я начал разрабатывать данный пример, я подумал, что было бы неплохо использовать одну плату, подключив I2C1, например, к I2C3. Но, после многих трудностей, я пришел к выводу, что периферийные устройства I2C в STM32 не являются «действительно асинхронными», и невозможно использовать два периферийных устройства I2C одновременно. Таким образом, для запуска этих примеров вам нужны две платы. Примечание автора).

volatile uint8_t transferDirection, transferRequested;

#define TEMP_OUT_INT_REGISTER   0x0
#define TEMP_OUT_FRAC_REGISTER  0x1
#define WHO_AM_I_REGISTER       0xF
#define WHO_AM_I_VALUE          0xBC
#define TRANSFER_DIR_WRITE      0x1
#define TRANSFER_DIR_READ       0x0
#define I2C_SLAVE_ADDR          0x33

int main(void) 
{
    char uartBuf[20];
    uint8_t i2cBuf[2];
    float ftemp;
    int8_t t_frac, t_int;

    HAL_Init();
    Nucleo_BSP_Init();

    MX_I2C1_Init();
#ifdef SLAVE_BOARD
    uint16_t rawValue;
    uint32_t lastConversion;
    
    MX_ADC1_Init();
    HAL_ADC_Start(&hadc1);

    while(1) 
    {
        HAL_I2C_EnableListen_IT(&hi2c1);
        while(!transferRequested) 
        {
            if(HAL_GetTick() - lastConversion > 1000L) 
            {
                HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
                
                rawValue = HAL_ADC_GetValue(&hadc1);
                ftemp = ((float)rawValue) / 4095 * 3300;
                ftemp = ((ftemp - 760.0) / 2.5) + 25;

                t_int = ftemp;
                t_frac = (ftemp - t_int)*100;

                sprintf(uartBuf, "Temperature: %f\r\n", ftemp);
                HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);

                sprintf(uartBuf, "t_int: %d - t_frac: %d\r\n", t_frac, t_int);
                HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);
                
                lastConversion = HAL_GetTick();
            }
        }

        transferRequested = 0;
        
        if(transferDirection == TRANSFER_DIR_WRITE) 
        {
            /* Master is sending register address */
            HAL_I2C_Slave_Sequential_Receive_IT(&hi2c1, i2cBuf, 1, I2C_FIRST_FRAME);
            while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_LISTEN);

            switch(i2cBuf[0]) 
            {
                case WHO_AM_I_REGISTER:
                    i2cBuf[0] = WHO_AM_I_VALUE;
                    break;
                case TEMP_OUT_INT_REGISTER:
                    i2cBuf[0] = t_int;
                    break;
                case TEMP_OUT_FRAC_REGISTER:
                    i2cBuf[0] = t_frac;
                    break;
                default:
                    i2cBuf[0] = 0xFF;
                    break;
            }
            
            HAL_I2C_Slave_Sequential_Transmit_IT(&hi2c1, i2cBuf, 1, I2C_LAST_FRAME);
            while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
        }
    }

Наиболее существенная часть функции main() начинается со строки 31. Процедура HAL_I2C_EnableListen_IT() включает все прерывания, связанные с периферией I2C. Это означает, что новое прерывание сработает, когда ведущий установит адрес ведомого устройства (который определяется макросом I2C_SLAVE_ADDR). Процедура HAL_I2C_EV_IRQHandler() автоматически вызывает функцию HAL_I2C_AddrCallback(), которую мы проанализируем позже.

Затем начинается выполнение аналого-цифрового преобразования датчика температуры каждую секунду с разделением полученного значения температуры (в переменной ftemp) на два целых числа:  t_int и t_frac: они представляют собой целую и дробную части температуры. Выполнение аналого-цифрового преобразования прерывается, как только переменная transferRequested становится равной 1: эта глобальная переменная устанавливается функцией HAL_I2C_AddrCallback() вместе с переменной transferDirection, которая содержит значение направления передачи данных (чтение/запись).

Если ведущее устройство запускает транзакцию в режиме записи, это означает, что он передает регистр адреса. Затем в строке 60 вызывается функция HAL_I2C_Slave_Sequential_Receive_IT(): данная функция принимает адрес регистра от ведущего. Поскольку функция работает в режиме прерывания, нам нужен способ дождаться завершения передачи. HAL_I2C_GetState() возвращает внутренний статус HAL, который равен HAL_I2C_STATE_BUSY_RX_LISTEN до завершения передачи. Когда это происходит, статус изменяется на HAL_I2C_STATE_LISTEN и мы можем продолжить, передав ведущему содержимое запрашиваемого регистра.

Данное действие происходит в строке 79, где вызывается функция HAL_I2C_Slave_Sequential_Transmit_IT(): функция инвертирует направление передачи и отправляет ведущему содержимое требуемого регистра. Сложная конструкция у нас в строке 80. Здесь у нас цикл, который не будет прерван до тех пор, пока состояние I2C не установится в HAL_I2C_STATE_READY. Почему не проверяется статус периферийного устройства на соответствие состоянию HAL_I2C_STATE_LISTEN как в строке 61? Чтобы понять этот аспект, нам нужно запомнить важную особенность комбинированных транзакций. Когда транзакция инвертирует направление передачи, ведущий начинает подтверждать каждый отправленный байт данных. Помните, что только ведущий знает как долго продлится транзакция и он решает когда ее прервать. В комбинированных транзакциях ведущий завершает передачу от ведомого, выдавая NACK, что заставляет ведомое устройство выполнить условие STOP. С точки зрения периферии I2C условие STOP заставляет периферийное устройство выйти из режима прослушивания (технически говоря, оно генерирует условие прерывания — если вы реализуете функцию обратного вызова HAL_I2C_AbortCpltCallback(), то сможете отслеживать, когда это происходит), и это причина по которой нам необходимо проверять состояние HAL_I2C_STATE_READY и снова переводить периферийное устройство в режим прослушивания в строке 31.

#else //Master board
    i2cBuf[0] = WHO_AM_I_REGISTER;
    HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_FIRST_FRAME);
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

    HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_LAST_FRAME);
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

    sprintf(uartBuf, "WHO AM I: %x\r\n", i2cBuf[0]);
    HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);

    i2cBuf[0] = TEMP_OUT_INT_REGISTER;
    HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_FIRST_FRAME);
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

    HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_int, 1, I2C_LAST_FRAME);
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

    i2cBuf[0] = TEMP_OUT_FRAC_REGISTER;
    HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_FIRST_FRAME);
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

    HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_frac, 1, I2C_LAST_FRAME);
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

    ftemp = ((float)t_frac)/100.0;
    ftemp += (float)t_int;
    
    sprintf(uartBuf, "Temperature: %f\r\n", ftemp);
    HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);
#endif

    while (1);
}

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

Часть ведущего контроллера начинается со строки 84. Код действительно прост. Здесь используется функция HAL_I2C_Master_Sequential_Transmit_IT() для запуска комбинированной транзакции и HAL_I2C_Master_Sequential_Receive_IT() для получения содержимого требуемого регистра от ведомого устройства. Затем целая и дробная части температуры снова объединяются в число с плавающей точкой и полученное значение температуры отправляется в UART2.

void I2C1_EV_IRQHandler(void) 
{
HAL_I2C_EV_IRQHandler(&hi2c1);
}

void I2C1_ER_IRQHandler(void) 
{
HAL_I2C_ER_IRQHandler(&hi2c1);
}

void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) 
{
    UNUSED(AddrMatchCode);
    
    if(hi2c->Instance == I2C1) 
    {
        transferRequested = 1;
        transferDirection = TransferDirection;
    }
}

Последняя часть, которую необходимо рассмотреть, представлена обработчиками прерываний ISR. I2C1_EV_IRQHandler() вызывает функцию HAL_I2C_EV_IRQHandler(), как сказано было выше. Это приводит к тому, что функция HAL_I2C_AddrCallback() вызывается каждый раз, когда ведущий передает адрес ведомого на шину. При вызове функция обратного вызова получает указатель на I2C_HandleTypeDef, представляющий конкретный дескриптор I2C, направление передачи TransferDirection и соответствующий адрес I2C AddrMatchCode: это необходимо, поскольку периферийное устройство I2C STM32, работающее в режиме ведомого, может ответить на два разных адреса и у нас есть возможность написать условный код в зависимости от адреса I2C, выданного ведущим в шину.

14.3 Использование CubeMX для настройки I2C

Как обычно, CubeMX сводит к минимуму усилия, необходимые для настройки периферийного устройства I2C. После активации периферии в панели IP (из представления Pinout view) мы можем настроить все параметры в представлении Configuration как показано на рисунке 13.

Рисунок 13. Окно конфигурации CubeMX для настройки I2C.

По умолчанию при включении I2C1 в микроконтроллерах STM32 в корпусах LQFP-64 CubeMX включает выводы PB7 и PB6 (SDA и SCL соответственно). Это не те контакты, что выведены на Arduino-совместимый разъем платы Nucleo, поэтому необходимо выбрать два альтернативных вывода PB9 и PB8, щелкнув по ним, а затем выбрав соответствующую функцию в раскрывающемся меню, как показано на следующем рисунке.

Рисунок 14. Как выбрать правильные выводы I2C1 для платы Nucleo-64.

Понравилась статья? Подпишись на мой канал в telegram, чтобы не пропустить новые статьи.

Понравилась статья? Поделить с друзьями:
  • I20 ошибка на посудомойке электролюкс форум
  • Hx tsr exe ошибка приложения
  • I want nothing nice to eat исправьте ошибки
  • Hxtsr exe ошибка приложения
  • I o operation on closed file python ошибка