В этом руководстве (перевод статьи [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.

Рис. 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.

Рис. 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).

Рис. 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.

Рис. 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).


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

Шаг 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.

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

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

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

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

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

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

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

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

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

Примечание: во время установки может произойти ошибка «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 необходимо отправить следующий массив данных 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 операцию? То есть, чтоб для нее больше не вызывались прерывания, и можно было ее начать сначала.
Изменено 24 января, 2017 пользователем bobbjenkins
Вторая и заключительная часть моего перевода главы по модулю 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 может сгенерировать правильное значение конфигурации за вас.

- 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 таймер и я уверен, что она еще будет актуальна весьма долгое время.

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

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

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

Операции чтения инициируются точно так же, как и операции записи, за исключением того, что бит R/W устанавливается в 1. Существует три основных типа операций чтения: чтение текущего адреса, произвольное чтение и чтение последовательности. В этой главе мы сосредоточим наше внимание только на режиме произвольного чтения, оставляя читателю возможность изучить другие режимы самостоятельно.
Операции произвольного чтения позволяют ведущему устройству получать доступ к любой ячейки памяти случайным образом. Чтобы выполнить этот тип операции чтения, адрес микросхемы памяти должен быть отправлен в первую очередь. Адрес 24LCxx отправляется как часть операции записи, т.е. бит R/W устанавливается в 0. После отправки адреса ведущий генерирует условие RESTART после получения подтверждения ACK (Память 24LCxx спроектирована таким образом, что она работает одинаково, даже если мы завершаем транзакцию с помощью условия STOP, а затем немедленно запускаем новую в режиме чтения. Эта гибкость позволит нам создать первый пример этой главы, как мы увидим далее. Примечание автора). Это завершает операцию записи, но не раньше, чем будет установлен внутренний счетчик адресов. Затем ведущий снова отправляет адрес ведомого, но уже с битом R/W установленным в 0. Затем 24LCxx выдает ACK и передает 8-битное слово данных. Ведущий не подтверждает передачу и генерирует условие STOP, которое заставляет EEPROM прекратить передачу (см. Рисунок 10). После случайной команды чтения внутренний счетчик адресов будет указывать на местоположение адреса ячейки памяти, следующей сразу за той, что была прочитана ранее.
Наконец мы готовы полностью описать данный пример. Создадим две простые функции с именами 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() возвращает длину строки без учета символа конца строки ‘’. Без сохранения этого символа и последующего чтения из 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):
- Мы должны начать новую транзакцию в режиме записи, выдав условие START, за которым следует адрес ведомого устройства;
- Затем передаем два байта, содержащие MSB и LSB части адреса ячейки памяти;
- После выдаем условие RESTART и передаем адрес ведомого устройства с последним битом, установленным в 1, чтобы начать транзакцию чтения;
- Ведомое устройство начинает передавать байты данных побайтно, пока мы не завершим транзакцию, выдав условие 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(). |
Используя процедуры последовательной передачи, мы можем действовать следующим образом:
- Вызываем процедуру
HAL_I2C_Master_Sequential_Transmit_IT(), передавая ей адрес ведомого устройства и два байта адреса ячейки памяти, в качестве параметра передаем значение I2C_FIRST_FRAME, чтобы функция генерировала условие START без выдачи условия STOP после отправки двух байт адреса; - Далее вызываем
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-осевой цифровой гироскоп).

Микроконтроллер на подобных платах обычно поставляется с предварительно запрограммированной прошивкой, которая специализируется на выполнении хорошо поставленной задачи. Плата хоста также может содержать еще одну программируемую микросхему, может быть другой микроконтроллер или что-то подобное. Основная плата взаимодействует с 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. Как можно заметить, единственное заметное отличие от протокола, используемого с 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: %frn", ftemp);
HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);
sprintf(uartBuf, "t_int: %d - t_frac: %drn", 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: %xrn", 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: %frn", 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.

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

Понравилась статья? Подпишись на мой канал в telegram, чтобы не пропустить новые статьи.
| Previous Tutorial | Tutorial 44 | Next Tutorial | ||
| STM32 I2C Communication Tutorial – HAL Examples | ||||
| STM32 Course Home Page 🏠 |
In this tutorial, we’ll be discussing the I2C hardware in STM32 microcontrollers. Starting with an introduction to the Inter-Integrated Circuit (I2C) communication. And we’ll get a closer look at the STM32 I2C hardware module and its internal functionalities, modes of operation, options, and configurations. In conclusion, we’ll take a look at the possible interrupt signals that can be triggered by the I2C hardware. And the different modes to perform I2C transmit & receive operations like (polling – interrupt – DMA) both as an I2C master and as a slave device as well.
Finally, we’ll check the available I2C configuration inside of CubeMX and how to configure & operate the peripheral using the provided HAL APIs. And that’s it for this theoretical tutorial. Next, we’ll do a handful of LABs to practice using I2C in different projects for communication and modules interfacing with STM32 microcontrollers.

Tutorial Contents
- 1 1. Introduction To I2C Communication
- 1.1 I2C Modes & Bus Speeds
- 1.2 I2C Physical Layer (Hardware)
- 1.3 SDA & SCL, Data Validity
- 1.4 Elements of I2C Transactions
- 2 2. I2C Hardware In STM32
- 3 3. STM32 I2C Hardware Functionalities
- 4 4. STM32 I2C Error Conditions
- 5 5. STM32 I2C Interrupts
- 6 6. STM32 I2C Master – Slave Modes TX & RX
- 6.1 I2C With Polling
- 6.2 I2C With Interrupts
- 6.3 I2C With DMA
- 7 7. STM32 I2C Device Memory Read / Write
- 8 8. SPI Configuration In CubeMX
- 9 9. STM32 I2C HAL Functions APIs
- 9.1 1. STM32 I2C “Blocking” HAL Functions (Blocking Mode)
- 9.2 2. STM32 I2C Interrupt Mode HAL Functions (Non-Blocking Mode)
- 9.3 3. STM32 I2C DMA Mode HAL Functions (Non-Blocking Mode)
- 9.4 4. STM32 I2C Device Check
- 10 Share this:
- 11 Related
1. Introduction To I2C Communication
I2C (i-square-c) is an acronym for “Inter-Integrated-Circuit” which was originally created by Philips Semiconductors (now NXP) back in 1982. I2CTM is a registered trademark for its respective owner and maybe it was the reason they call it “Two Wire Interface (TWI)” in some microcontrollers like Atmel AVR. The I2C is a multi-master, multi-slave, synchronous, bidirectional, half-duplex serial communication bus. It’s widely used for attaching lower-speed peripheral ICs to processors and microcontrollers in short-distance, intra-board communication.
I2C Modes & Bus Speeds
Originally, the I2C-bus was limited to 100 kbit/s operations. Over time there have been several additions to the specification so that there are now five operating speed categories. Standard-mode, Fast-mode (Fm), Fast-mode Plus (Fm+), and High-speed mode (Hs-mode) devices are downward-compatible. This means any device may be operated at a lower bus speed. Ultra Fast-mode devices are not compatible with previous versions since the bus is unidirectional.
Bidirectional bus:
- Standard-Mode (Sm), with a bit rate up to 100 kbit/s
- Fast-Mode (Fm), with a bit rate up to 400 kbit/s
- Fast-Mode Plus (Fm+), with a bit rate up to 1 Mbit/s
- High-speed Mode (Hs-mode), with a bit rate up to 3.4 Mbit/s.
Unidirectional bus:
- Ultra Fast-Mode (UFm), with a bit rate up to 5 Mbit/s
Note: You have to refer to the specific device datasheet to check the typical details for the i2c hardware specifications that have actually been implemented on-chip.
I2C Physical Layer (Hardware)
The I2C bus uses what’s known as an open-drain (open-collector) output driver for both SDA and SCL lines. Which as the name suggests is having each IO pin connected to the collector of the output driver transistor internally, while having it pulled up to Vcc with a resistor eternally. That’s why the default (IDLE) state for each line is HIGH when the open-drain driver is turned OFF. However, if we turn ON the output driver, the IO pin is driven LOW to the ground by the output driver transistor as you can see in the diagram below.

The I2C bus lines being “open-drain” bidirectional pins makes it perfect for multi-master multi-slave sort of communication without any risk of having collisions. And that’s due to having what’s called “Bus Arbitration” in case of multiple masters did initiate a transaction at the exact same time.
Anyone will write a 0 first while the other is writing a 1, will win the arbitration and continue its message and the other master will stop and wait till the end. That’s because of the nature of “Open-drain” output. To write a 0, we turn ON the output driver to pull the signal line to LOW. To write a 1, we turn OFF the output driver and the line will be pulled up to HIGH by the effect of the external resistors. That’s why bus arbitration is a very powerful feature for I2C communication.
SDA & SCL, Data Validity
Both SDA and SCL are bidirectional lines, connected to a positive supply voltage via a current-source or pull-up resistor. When the bus is free, both lines are HIGH. The output stages of devices connected to the bus must have an open-drain or open-collector to perform the wired-AND function.
Due to the variety of different technology devices (CMOS, NMOS, bipolar), that can be connected to the I2C-bus, the levels of the logical ‘0’ (LOW) and ‘1’ (HIGH) are not fixed and depend on the associated level of VDD. Input reference levels are set as 30 % and 70 % of VDD; VIL is 0.3VDD and VIH is 0.7VDD. The data on the SDA line must be stable during the HIGH period of the clock. The HIGH or LOW state of the data line can only change when the clock signal on the SCL line is LOW. One clock pulse is generated for each data bit transferred.

Elements of I2C Transactions
A typical I2C message consists of some basic elements (conditions) that take place on the I2C bus sequentially and it always starts with a start condition (s). Followed by the desired slave device address (7-Bits or 10-Bits), then a R/W bit to determine whether the master (who initiated the S condition for communication) wants to read or write to this slave having that address. Then if the slave exists and works OK, it’ll acknowledge back to the master by sending an Acknowledge bit ACK otherwise, it’s considered a Negative Acknowledge NACK. Afterward, the byte of Data is sent, followed by an acknowledge from the slave. And finally, the master can terminate the communication by sending the Stop Condition (P) sequence.

We can summarize these conditions (elements) of I2C bus signaling as follows:
- Start Condition (S)
- Stop Condition (P)
- Repeated Start (Restart) Condition (Sr)
- Acknowledge ACK (A)
- Not Acknowledge NACK (~A)
- Address + R/W
- Data Byte
Other topics and details of the I2C mechanics of its operation are discussed in detail in the article down below. Including I2C clock synchronization, stretching, I2C bus arbitration, addressing, I2C bus conditions, and more.
The linked I2C tutorial above is a full guide (+12k words!) that has all the information you may need to know if you’re just starting to learn about the topic. Take the time to check it out if you need to and come back to resume this tutorial and to see the I2C hardware peripheral implemented in STM32 microcontrollers and the extra features it does have. Then, we can proceed to build embedded software applications with an I2C interface to read sensors, external memories, etc.
2. I2C Hardware In STM32
2.1 STM32 I2C Hardware Overview
I2C (inter-integrated circuit) bus Interface serves as an interface between the microcontroller and the serial I2C bus. It provides multi-master capability and controls all I2C bus-specific sequencing, protocol, arbitration, and timing. It supports the standard mode (Sm, up to 100 kHz) and Fm mode (Fm, up to 400 kHz).
It may be used for a variety of purposes, including CRC generation and verification, SMBus (system management bus), and PMBus (power management bus). Depending on specific device implementation DMA capability can be available for reduced CPU overload.
2.2 STM32 I2C Main Features
- Multimaster capability: the same interface can act as Master or Slave
- I2C Master features: [ Clock generation – Start and Stop generation]
- I2C Slave features: [Programmable I2C Address detection – Dual Addressing Capability to acknowledge 2 slave addresses – Stop bit detection]
- Generation and detection of 7-bit/10-bit addressing and General Call
- Supports different communication speeds:
– Standard Speed (up to 100 kHz)
– Fast Speed (up to 400 kHz) - Analog noise filter
- 2 Interrupt vectors:
– 1 Interrupt for successful address/ data communication
– 1 Interrupt for error condition - Optional clock stretching
- 1-byte buffer with DMA capability
- Configurable PEC (packet error checking) generation or verification:
- SMBus 2.0 Compatibility
- PMBus Compatibility
3. STM32 I2C Hardware Functionalities
In this section, we’ll get a deep insight into the STM32 I2C module hardware, its block diagram, functionalities, modes of operations, and data reception/transmission.
3.1 STM32 I2C Block Diagram

As you can see in the I2C block diagram above, there is the main shift register, a buffer register, and some control logic units to handle all I2C transaction steps. Just like address match checking, generating the clock signal, filtering, error checking, and so on.
3.2 STM32 I2C Mode Selection
The interface can operate in one of the four following modes:
- Slave transmitter
- Slave receiver
- Master transmitter
- Master receiver
By default, it operates in slave mode. The interface automatically switches from slave to master, after it generates a START condition and from master to slave, if an arbitration loss or a Stop generation occurs, allowing multi-master capability. We’ll be creating a handful of example projects to operate the I2C peripheral in each one of all the 4 modes mentioned above.
3.3 STM32 I2C In Slave Mode

By default, the I2C interface operates in Slave mode. To switch from default Slave mode to Master mode a Start condition generation is needed. The peripheral input clock must be programmed in the I2C_CR2 register in order to generate correct timings. The peripheral input clock frequency must be at least:
- 2 MHz in Sm mode
- 4 MHz in Fm mode
As soon as a start condition is detected, the address is received from the SDA line and sent to the shift register. Then it is compared with the address of the interface. Following the address reception and after clearing ADDR, the slave receives bytes from the SDA line into the DR register via the internal shift register. After each byte, the interface generates an acknowledge pulse if the ACK bit is set.
If RxNE is set and the data in the DR register is not read before the end of the next data reception, the BTF bit is set and the interface waits until BTF is cleared by a read from I2C_SR1 followed by a read from the I2C_DR register, stretching SCL low. Clock stretching is essentially holding the clock line SCL LOW by the slave device which prevents any master device on the I2C bus from initiating any new transaction until that slave releases the SCL back again. That’s why you should be careful when using clock stretching in slave devices.
After the last data byte is transferred a Stop Condition is generated by the master. The interface detects this condition and sets the STOPF bit and generates an interrupt if the ITEVFEN bit is set. The STOPF bit is cleared by a read of the SR1 register followed by a write to the CR1 register.
3.4 STM32 I2C In Master Mode
In Master mode, the I2C interface initiates a data transfer and generates the clock signal. A serial data transfer always begins with a Start condition and ends with a Stop condition.
Master mode is selected as soon as the Start condition is generated on the bus with a START bit. The following is the required sequence in master mode.
- Program the peripheral input clock in I2C_CR2 Register in order to generate correct timings
- Configure the clock control registers
- Configure the rise time register
- Program the I2C_CR1 register to enable the peripheral
- Set the START bit in the I2C_CR1 register to generate a Start condition
The peripheral input clock frequency must be at least:
- 2 MHz in Sm mode
- 4 MHz in Fm mode
3.5 STM32 I2C PEC (Packet Error Checking)
A PEC calculator has been implemented by STMicroelectronics in I2C hardware to improve the reliability of communication. The PEC is calculated by using the C(x) = x8 + x2 + x + 1 CRC-8 polynomial serially on each bit. By enabling PEC, you can have automatic error checking for large data packet transactions all done by hardware without adding any overhead on the software side.
However, you should also know that if the master device did lose the “arbitration” at any instance, this will corrupt the PEC. And the device will be set back to slave mode and waits until the arbitration “winner” master device finishes its message. Only then, you can start the process all over again.
4. STM32 I2C Error Conditions
There are some error conditions that could be detected by the I2C interface hardware to indicate some issues on the hardware level. The software can easily detect those error conditions by reading the corresponding flag bits for each error signal. The error conditions include:
Bus Error (BERR) – This error occurs when the I2C interface detects an external Stop or Start condition during an address or a data transfer.
Acknowledge Failure (AF) – This error occurs when the interface detects a non-acknowledge bit.
Arbitration Lost (ARLO) – This error occurs when the I2C interface detects an arbitration lost condition.
Overrun/Underrun Error (OVR) – An overrun error can occur in slave mode when clock stretching is disabled and the I2C interface is receiving data. The interface has received a byte and the data in DR has not been read before the next byte is received by the interface. Underrun error can occur in slave mode when clock stretching is disabled and the I2C interface is transmitting data. The interface has not updated the DR with the next byte before the clock comes for the next byte.
5. STM32 I2C Interrupts
The I2C interrupt events are connected to the same interrupt vector. So the I2C fires a single interrupt signal regardless of the source of it. The software will have to detect it. These events generate an interrupt if the corresponding Enable Control Bit is set.

6. STM32 I2C Master – Slave Modes TX & RX
In this section, I’ll list the possible ways that you can handle I2C transactions in your firmware applications. For code example LABs and testing, just click on the next tutorial button and keep going through this series of tutorials. There will be lots of examples and libraries that we’ll build based on I2C communication (e.g I2C_LCD, OLED, MPU6050 IMU, etc…).
I2C With Polling
The first and the easiest way to do anything in embedded software is just to poll for the hardware resource until it’s ready to move on to the next step in your program instructions. However, it’s the least efficient way to do things and the CPU will end up wasting so much time in a “busy-waiting” state.
It’s the same thing for both transmission and reception. You just wait until the current byte of data to be transmitted so you can start the next one and so on.
I2C With Interrupts
We can, however, enable the I2C interrupts and have a signal when it’s done and ready for servicing by CPU. Either for data that has been sent or received. Which saves a lot of time and has been always the best way to handle events like that.
However, in some “Time Critical” applications we need everything to be as deterministic, in time, as possible. And a major problem with interrupts is that we can’t expect when it’d arrive or during which task. That can potentially screw up the timing behavior of the system.
I2C With DMA
To operate at its maximum speed, the I2C needs to be fed with the data for transmission and the data received on the Rx buffer should be read to avoid overrun. To facilitate the transfers, the I2C features a DMA capability implementing a simple request/acknowledge protocol.
DMA requests are generated by Data Register becoming empty in transmission and Data Register becoming full in reception. Using the DMA will also free the CPU from doing the data transfers “peripheral <-> memory”. This will end up saving a lot of time and is considered to be the most efficient way to handle this peripheral to memory data transfer and vice versa.
7. STM32 I2C Device Memory Read / Write
In this section, I’ll explain a useful feature that has been implemented in HAL APIs for the I2C driver firmware library which is the device memory read/write. The following are the (Blocking) version for both of the 2 functions.
|
HAL_I2C_Mem_Write(); HAL_I2C_Mem_Read(); |
There are other versions for other modes of operation like interrupt and interrupt+DMA. But let’s first explain what is the device memory read/write operation.
We, the developers, basically don’t need to do anything on the hardware level for the I2C interface to get it to work. The details of I2C operation mentioned earlier in this tutorial are mostly to give you an understanding of what’s happening at the hardware level. Despite the fact that most of those operations are done automatically by hardware when we set/clear some control bits.
Therefore, we only need to call some basic functions that represent a wrapper layer for the control registers set/clear bits sort of configuration. Just to get the I2C interface properly configured and also initiate data transmission/reception operations.
The basic functionality for transmitting data over I2C as a Master device is handled by the following HAL function.
|
HAL_I2C_Master_Transmit(); |
It sends some data over I2C to a specific slave device address on the bus (if found) in a blocking mode. So, what’s the difference between the normal I2C_Transmit and I2C_Mem_Write? That’s what we’re trying to answer in this section. For that, let’s consider as an example the MPU6050 IMU device.
The IMU has an internal I2C device address so that any master on the bus can easily address this sensor module. Internally, the MPU6050 itself has some registers with a unique address for each of them. Generally speaking, we need to set some values for the internal registers of the IMU in order to configure it as per our application, and also read some registers in which the IMU data is located.
So, if you’ve got a master STM32 uC and would like to get the readings of the MPU6050 IMU, you’d need to do the following as stated in the datasheet.

As you can see, the Master (STM32 uC) has to start the transaction. Then send the AD+W (the address of the MPU6050 module itself) for writing operation. The master then writes out the internal register address (RA) inside the MPU6050 that we’d like to read. The slave will therefore acknowledge the command and send out the data located in that specific register. Finally, the master will terminate the communication.
This is how to read a register at a specific address inside a module that has its unique address as well on the I2C bus!
You can do all of that manually with the provided basic HAL APIs or use the Mem_Write / Mem_Read collection of functions that support all modes (blocking – interrupt – DMA). We’ll be putting all of those functions under testing in the upcoming tutorials and LABs to get more familiar with them.
8. SPI Configuration In CubeMX
In the next few tutorials, we’ll be doing some practical LABs to implement I2C Master/Slave (TX & RX) code examples. In which we’ll be using the CubeMX software tool to configure the I2C hardware. Therefore, in this section, I’ll introduce to you the features and options that can be configured within CubeMX GUI for I2C peripheral.
Here is the configuration tab for the I2C peripheral in CubeMX. And those are the possible modes for I2C.

Let’s pick the “I2C” mode. Now, you’ll find that we’re able to set the clock rate for master mode. And if you’re going to operate in slave mode, you can set the I2C device address length & value, and enable/disable the clock stretching feature that we’ve discussed earlier.
We can also enable/disable interrupts for I2C in the NVIC tab if you’re willing to use interrupt mode instead of polling the peripheral. And the same goes for the DMA, you can click the DMA tab to “Add” a DMA channel dedicated to I2C transfer and configure its parameters.

9. STM32 I2C HAL Functions APIs
1. STM32 I2C “Blocking” HAL Functions (Blocking Mode)
Master Transmission
|
HAL_I2C_Master_Transmit (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t* pData, uint16_t Size, uint32_t Timeout); |
Master Reception
|
HAL_I2C_Master_Receive (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t* pData, uint16_t Size, uint32_t Timeout); |
Slave Transmission
|
HAL_I2C_Slave_Transmit (I2C_HandleTypeDef * hi2c, uint8_t * pData, uint16_t Size, uint32_t Timeout); |
Slave Reception
|
HAL_I2C_Slave_Receive (I2C_HandleTypeDef * hi2c, uint8_t * pData, uint16_t Size, uint32_t Timeout); |
Device Memory Write
|
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); |
Device Memory Read
|
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); |
2. STM32 I2C Interrupt Mode HAL Functions (Non-Blocking Mode)
Master Transmission
|
HAL_I2C_Master_Transmit_IT (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size); |
After calling the above function, the I2C peripheral will start sending all the data bytes in the buffer one by one until it’s done. When it’s done this function below will be called and executed, if you’d like to do something upon data transmission completion, then add that to your code in the application source file (main.c).
|
void HAL_I2C_MasterTxCpltCallback (I2C_HandleTypeDef * hi2c) { // TX Done .. Do Something! } |
Master Reception
|
HAL_I2C_Master_Receive_IT (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size); |
After calling the above function, the I2C peripheral will start receiving all the incoming data bytes in the buffer one by one until it’s done. When it’s done this function below will be called and executed, if you’d like to do something upon data reception completion, then add that to your code in the application source file (main.c).
|
void HAL_I2C_MasterRxCpltCallback (I2C_HandleTypeDef * hi2c) { // RX Done .. Do Something! } |
Similar APIs are also available for slave mode of operation.
3. STM32 I2C DMA Mode HAL Functions (Non-Blocking Mode)
Master Transmission
|
HAL_I2C_Master_Transmit_DMA (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size); |
After calling the above function, the I2C peripheral will start sending all the data bytes in the buffer one by one until it’s done (in DMA Mode). When it’s done this function below will be called and executed, if you’d like to do something upon data transmission completion, then add that to your code in the application source file (main.c).
|
void HAL_I2C_MasterTxCpltCallback (I2C_HandleTypeDef * hi2c) { // TX Done .. Do Something! } |
Master Reception
|
HAL_I2C_Master_Receive_DMA (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size); |
After calling the above function, the I2C peripheral will start receiving all the incoming data bytes in the buffer one by one until it’s done (in DMA Mode). When it’s done this function below will be called and executed, if you’d like to do something upon data reception completion, then add that to your code in the application source file (main.c).
|
void HAL_I2C_MasterRxCpltCallback (I2C_HandleTypeDef * hi2c) { // RX Done .. Do Something! } |
Similar APIs are also available for slave mode of operation.
4. STM32 I2C Device Check
This is a useful utility to check if a slave device on the I2C is actually existing and working or not.
|
AL_I2C_IsDeviceReady (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout); |
The I2C Communication Bus Demonstrated in This Tutorial Will Be Used In The Following Next Tutorials:
- MCU – MCU Communication Over I2C
- External Serial I2C EEPROM
- OLED Display
- I2C LED Interface
- and more…
That’s it for this tutorial. If you’d like to see the practical LAB examples for I2C and other interfacing projects and libraries we’ll build using this peripheral, just keep progressing through this series of tutorials and click to the next tutorial link down below.
Did you find this helpful? If yes, please consider supporting this work and sharing these tutorials!
Stay tuned for the upcoming tutorials and don’t forget to SHARE these tutorials. And consider SUPPORTING this work to keep publishing free content just like this!
| STM32 Course Home Page 🏠 |
||||
| Previous Tutorial | Tutorial 44 | Next Tutorial |
Волею судеб мне пришлось разрабатывать прошивку для одного устройства на основе микроконтроллера STM32F103. Функций у устройства много, в том числе и общение с EEPROM подключенным посредством протокола I2C. Кто не знает, микроконтроллеры STM32 во многих своих версиях поддерживают работу по данному протоколу на аппаратном уровне. Это значит, что у микросхемы микроконтроллера присутствуют специальные выводы, которые можно использовать в том числе и для работы по протоколу I2C, а все издержки по этому протоколу выполняются «железом» микроконтроллера.

Вообще, 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 только выглядит просто, на самом деле без понимания туда лезть не стоит). Более подробно о потенциальных проблемах можно почитать в ссылках ниже.
Полезные ссылки:
- STM32 WRITE AND READ EEPROM OVER I2C BUS — статья детально разжевывающая методы обращения с EEPROM подключенным посредством I2C.
- STM32F10xx8 STM32F10xxB Errata sheet (medium-density device limitations) — бюллетень от STMicroelectronics описывающий возможные затруднения и способы борьбы с ними по различным блокам своих микроконтроллеров. Проблем работы с I2C в документе указано аж 7.
- STM32 — I2C — HAL_BUSY — статья что делать, если возникает Busy.

Про проблему с i2c у микроконтроллеров stm32, в частности F103 (а может и ещё в каких-то).
Суть проблемы: при старте МК, аналоговый фильтр
(есть такая штуковина не имеющая в данном случае отношения к аналоговому входу)
блокирует флаг I2C_FLAG_BUSY, и тот не может сброситься. В итоге I2C всегда занят. То есть, любая HALовская функция, работающая с i2c, возвращает HAL_BUSY. Эта проблема описана в Errata (п 2.13.7).
Это касается функций без прерывания.
Если же использовать функции с прерыванием — _IT, то ситуация совсем скверная. Допустим вы отправили данные с помощью HAL_I2C_Master_Transmit_IT() и пошли дальше заниматься своими делами думая что всё хорошо. Однако на самом деле функция вместо того чтоб отправить данные, ждёт пока сброситься флаг I2C_FLAG_BUSY, а он и не думает сбрасываться. Таймаут у функции 10 сек, в течении которых она ждёт когда сброситься флаг, естественно не дожидается этого, и в итоге вместо отправки данных возвращает HAL_TIMEOUT.
Решения этой проблемы для функций без прерывания
Первая рекомендация:
В файле stm32f1xx_hal_msp.c, в функции…
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hi2c->Instance==I2C1)
{
/* USER CODE BEGIN I2C1_MspInit 0 */
/* USER CODE END I2C1_MspInit 0 */
__HAL_RCC_GPIOB_CLK_ENABLE();
/**I2C1 GPIO Configuration
PB8 ------> I2C1_SCL
PB9 ------> I2C1_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
__HAL_AFIO_REMAP_I2C1_ENABLE();
/* Peripheral clock enable */
__HAL_RCC_I2C1_CLK_ENABLE();
/* USER CODE BEGIN I2C1_MspInit 1 */
/* USER CODE END I2C1_MspInit 1 */
}
}
Включение клоков интерфейса (__HAL_RCC_I2C1_CLK_ENABLE()) сделано после включения клоков GPIO, хотя в других функциях, типа
void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
или
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
сделано наоборот, сначала клоки интерфейса, а потом GPIO.
Например SPI…
void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hspi->Instance==SPI1)
{
/* USER CODE BEGIN SPI1_MspInit 0 */
/* USER CODE END SPI1_MspInit 0 */
/* Peripheral clock enable */
__HAL_RCC_SPI1_CLK_ENABLE(); //////// сначала интерфейс
__HAL_RCC_GPIOA_CLK_ENABLE(); //////// затем GPIO
/**SPI1 GPIO Configuration
PA5 ------> SPI1_SCK
PA6 ------> SPI1_MISO
PA7 ------> SPI1_MOSI
*/
GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN SPI1_MspInit 1 */
/* USER CODE END SPI1_MspInit 1 */
}
}
Поэтому первой попыткой исправить проблему, будет сделать…
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hi2c->Instance==I2C1)
{
/* USER CODE BEGIN I2C1_MspInit 0 */
__HAL_RCC_I2C1_CLK_ENABLE(); ///////////// ЭТО !!! ////////////////
/* USER CODE END I2C1_MspInit 0 */
__HAL_RCC_GPIOB_CLK_ENABLE();
/**I2C1 GPIO Configuration
PB8 ------> I2C1_SCL
PB9 ------> I2C1_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
__HAL_AFIO_REMAP_I2C1_ENABLE();
/* Peripheral clock enable */
__HAL_RCC_I2C1_CLK_ENABLE(); //////// это можно закоментировать
/* USER CODE BEGIN I2C1_MspInit 1 */
/* USER CODE END I2C1_MspInit 1 */
}
}
Однако как показывает практика, это не очень то помогает. Всё равно шина виснет и даже не помогает ресет платы.
Поэтому применим второй метод:
Перед инициализацией всего и вся, нужно добавить ресет всего чего только можно у I2C…
__HAL_RCC_I2C1_CLK_ENABLE();
HAL_Delay(100);
__HAL_RCC_I2C1_FORCE_RESET();
HAL_Delay(100);
__HAL_RCC_I2C1_RELEASE_RESET();
HAL_Delay(100);
То есть выглядеть код будет так…
/* Configure the system clock */
SystemClock_Config();
/* 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 */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_ADC1_Init();
MX_SPI1_Init();
MX_I2C1_Init();
MX_TIM4_Init();
/* USER CODE BEGIN 2 */
После этого всё становиться почти хорошо, зависания шины если и происходят, то редко, и при нажатии ресета шина тут же приходит в себя. Тем не менее в процессе работы случаются зависоны.
Поэтому третий метод:
Запуская функцию, не важно, отправка, приём, или ещё что-то, надо проверять что она возвращает…
uint32_t status = HAL_I2C_Master_Transmit(&hi2c1, (uint16_t)LCD_ADDR, &bt, 1, 1000);
Если статус не HAL_OK, тогда используем метод описанный в STM32F10xx8 STM32F10xxB Errata sheet, глава 2.13.7 — I2C analog filter may provide wrong value, locking BUSY flag and preventing master mode entry
При вызове функции проверяем статус…
uint32_t status = HAL_I2C_Master_Transmit(&hi2c1, (uint16_t)LCD_ADDR, &bt, 1, 1000);
if(status != HAL_OK)
{
I2C_ClearBusyFlagErratum(&hi2c1, 1000);
}
Если он плохой, то вызываем самописную функцию I2C_ClearBusyFlagErratum(&hi2c1, 1000), представляющую из себя 15 пунктов описанных в еррата.
На HAL она выглядит так…
static void I2C_ClearBusyFlagErratum(I2C_HandleTypeDef *hi2c, uint32_t timeout)
{
// 2.13.7 I2C analog filter may provide wrong value, locking BUSY. STM32F10xx8 STM32F10xxB Errata sheet
GPIO_InitTypeDef GPIO_InitStructure = {0};
// 1. Clear PE bit.
CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_PE);
// 2. Configure the SCL and SDA I/Os as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
HAL_I2C_DeInit(hi2c);
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Pin = GPIO_PIN_8; // SCL // если пин другой, то укажите нужный
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure); // если порт другой, то укажите нужную букву GPIOх, и ниже там все порты и пины поменяйте на своё
GPIO_InitStructure.Pin = GPIO_PIN_9; // SDA
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
// 3. Check SCL and SDA High level in GPIOx_IDR.
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
wait_for_gpio_state_timeout(GPIOB, GPIO_PIN_8, GPIO_PIN_SET, timeout);
wait_for_gpio_state_timeout(GPIOB, GPIO_PIN_9, GPIO_PIN_SET, timeout);
// 4. Configure the SDA I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
// 5. Check SDA Low level in GPIOx_IDR.
wait_for_gpio_state_timeout(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET, timeout);
// 6. Configure the SCL I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
// 7. Check SCL Low level in GPIOx_IDR.
wait_for_gpio_state_timeout(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET, timeout);
// 8. Configure the SCL I/O as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
// 9. Check SCL High level in GPIOx_IDR.
wait_for_gpio_state_timeout(GPIOB, GPIO_PIN_8, GPIO_PIN_SET, timeout);
// 10. Configure the SDA I/O as General Purpose Output Open-Drain , High level (Write 1 to GPIOx_ODR).
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
// 11. Check SDA High level in GPIOx_IDR.
wait_for_gpio_state_timeout(GPIOB, GPIO_PIN_9, GPIO_PIN_SET, timeout);
// 12. Configure the SCL and SDA I/Os as Alternate function Open-Drain.
GPIO_InitStructure.Mode = GPIO_MODE_AF_OD;
//GPIO_InitStructure.Alternate = GPIO_AF4_I2C2; // F4
GPIO_InitStructure.Pin = GPIO_PIN_8;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_9;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
// 13. Set SWRST bit in I2Cx_CR1 register.
SET_BIT(hi2c->Instance->CR1, I2C_CR1_SWRST);
asm("nop");
/* 14. Clear SWRST bit in I2Cx_CR1 register. */
CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_SWRST);
asm("nop");
/* 15. Enable the I2C peripheral by setting the PE bit in I2Cx_CR1 register */
SET_BIT(hi2c->Instance->CR1, I2C_CR1_PE);
asm("nop");
// Call initialization function.
HAL_I2C_Init(hi2c);
}
Эта функция вызывает ещё одну функцию проверки состояния ножек после их включения/отключения…
static uint8_t wait_for_gpio_state_timeout(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState state, uint32_t timeout)
{
uint32_t Tickstart = HAL_GetTick();
uint8_t ret = 1;
for(;(state != HAL_GPIO_ReadPin(port, pin)) && (1 == ret);) // Wait until flag is set
{
if(timeout != HAL_MAX_DELAY) // Check for the timeout
{
if((timeout == 0U) || ((HAL_GetTick() - Tickstart) > timeout)) ret = 0;
}
asm("nop");
}
return ret;
}
При желании можно проверять что она возвращает, 0 или 1, но это уже излишество, и ретурны можно выпилить.
Тут можно взять это, оформленное в виде хедера. Добавить файл
i2c_er.h
в проект, и там где вызываются функции с проверкой статуса добавить инклюд…
#include "i2c_er.h"
Всё, теперь можно проверять что возвращает любая HALовская функция I2C, и в случае неудачи I2C будет переинициализироваться.
Все эти методы можно использовать вместе.
Что же касается функций с прерываниями, то первые два метода могут решить проблему во время старта, но не во время работы. Как тут поступить я не знаю. Придётся корректировать HALовский код, и как-то приделывать туда описанную в последнем методе проверку статуса.
Это всё, всем спасибо 
Примеры взяты из сети и немного изменены.
Телеграм-чат istarik
Телеграм-чат STM32

В последнее время все чаще натыкаюсь на негативные отзывы о шине I2C у STM32, мол работа с ней это танцы с бубном и тд.
За последний месяц мне удалось запустить две микросхемы, работающие по I2C и ни каких танцев, только вдумчивое чтение даташита.
Модуль I2C у STM32 обладает следующими особенностями:
- может работать в двух режимах Fm(fast mode) и Sm(standart mode), первый работает на частотах до 400KHz, второй до 100KHz
- буфер размером 1 байт с поддержкой DMA
- поддерживает аппаратный подсчет контрольной суммы
- на его основе возможна реализуется SMBus(System Management Bus) и PMBus(Power Management Bus)
- два вектора прерывания, генерируются при успешной передаче и при возникновении ошибки
- фильтр для борьбы с шумами
- может работать в режиме Master или Slave
В режиме Master:
- генерирует тактирующий сигнал
- генерирует START и STOP
В режиме Slave:
- можно программировать основной и альтернативный адрес на который он будет отзываться
- определяет STOP
По умолчанию модуль находится в режиме Slave, но он автоматически переключается в режим Master после генерации состояния START.
Принципиальное отличие между Master и Slave, в том, что Master генерирует тактовый сигнал и всегда инициирует передачу данных и заканчивает её. Slave же, откликается на свой адрес и широковещательный, при чем отклик на широковещательный адрес можно отключить. Также Slave генерирует состояние ACK, но его тоже можно отключить.
Такое подробное разъяснение необходимо потому, что в обоих режимах устройство может выступать как передатчиком, так и приемником.
- Slave transmitter
- Slave receiver
- Master transmitter
- Master receiver
Ниже показана структура модуля I2C.

Регистр управления I2C_CR1:
SWRST(Software reset) — единица в этом бите сбрасывает значение всех регистров модуля в дефолтное стояние, может использоваться для сброса при возникновении ошибки.
ALERT(SMBus alert) — установка единицы в этот бит разрешает генерировать сигнал alert в режиме SMBus.
PEC(Packet error checking) — управление этим битом производится программно, но он может быть сброшен аппаратно когда передается PEC, START, STOP или PE=0. Единица в этом бите разрешает передачу CRC.
POS(Acknowledge/PEC Position (for data reception)) — состояние этого бита определяет положение ACK/PEC в двух байтовой конфигурации в режиме Master.
ACK(Acknowledge enable) — единица в этом бите разрешает отправлять ACK/NACK после приема байта адреса или данных.
STOP(Stop generation) — установка единицы в этот бит генерирует сигнал STOP в режиме Master.
START(Start generation) — установка единицы в этот бит генерирует состояние START в режиме Master,
NOSTRETCH(Clock stretching disable (Slave mode)) — если на обработку данных требуется время Slave может остановить передачу мастера, прижав линию SCL к земле, Master будет ждать и не будет ни чего слать, пока линия не будет отпущена. Ноль в этом бите прижимает SCL к земле.
ENGC(General call enable) — если в этом бите установлена единица, модуль отвечает ACKом на широковещательный адрес 0х00.
ENPEC(PEC enable) — установка единицы в этот бит включает аппаратный подсчет CRC.
ENARP(ARP enable) — установка единицы в этот бит включает ARP.
SMBTYPE(SMBus type) — если в этом бите установлен ноль модуль работает в режиме Slave, если единица в режиме Master.
SMBUS(SMBus mode) — если в этом бите установлен ноль модуль работает в режиме I2C, если единица SMBus.
PE(Peripheral enable) — единица в этом бите включает модуль.
Регистр управления I2C_CR2:
LAST(DMA last transfer) — единица в этом бите разрешает DMA генерировать сигнал окончания передачи EOT(End of Transfer).
DMAEN(DMA requests enable) — единица в этом бите разрешает делать запрос к DMA при установке флагов TxE или RxNE.
ITBUFEN(Buffer interrupt enable) — если этот бит сброшен, разрешены все прерывания, кроме прерываний по приему и передаче.
ITEVTEN(Event interrupt enable) — единица в этом бите разрешает прерывания по событию.
ITERREN(Error interrupt enable) — единица в этом бите разрешает прерывания при возникновении ошибок.
FREQ[5:0](Peripheral clock frequency) — в это битовое битовое поле необходимо записать частоту тактирования модуля, она может принимать значение от 2 до 50.
Регистр I2C_OAR1:
ADDMODE(Addressing mode) — этот бит определяет размер адреса Slave, ноль соответствует размеру адреса 7 бит, единица — 10 бит.
ADD[9:8](Interface address) — старшие биты адреса, в случае если адрес 10-битный.
ADD[1:7](Interface address) — адрес устройства.
ADD0(Interface address) — младший бит адреса, в случае если адрес 10-битный..
Регистр I2C_OAR2:
ADD2[7:1] — альтернативный адрес на который будет отзываться Slave.
ENDUAL(Dual addressing mode enable) — единица в этом бите разрешает Slave отзываться на альтернативный адрес в 7-битном режиме.
I2C_DR
— регистр данных, для отправки данных пишем в регистр DR, для приёма читаем его же.
Регистр статуса I2C_SR1:
SMBALERT(SMBus alert) — возникает в случае alert в шине SMBus.
TIMEOUT (Timeout or Tlow error) — возникает если линия SCL прижата к земле. Для master 10mS, для slave 25mS.
PECERR (PEC Error in reception) — возникает при ошибке PEC при приеме.
OVR (Overrun/Underrun) — возникает при переполнении данных.
AF (Acknowledge failure) — устанавливается при получении сигнала NACK. Для сброса нужно записать 0.
ARLO (Arbitration lost (master mode) ) — устанавливается при потере арбитража. Для сброса нужно записать 0.
BERR (Bus error) — ошибка шины. Устанавливается в случае возникновения сигнала START или STOP в неправильный момент.
TxE (Data register empty (transmitters)) — устанавливается при опустошении регистра DR, а точнее когда данные из него были перемещены в сдвиговый регистр.
RxNE (Data register not empty (receivers)) — устанавливается при приеме байта данных, кроме адреса.
STOPF(Stop detection (slave mode)) — при работе в режиме slave устанавливается при обнаружении сигнала STOP, если перед этим был сигнал ACK. Для сброса необходимо прочитать SR1 и произвести запись в CR1.
ADD10 (10-bit header sent (Master mode)) — устанавливается при отправке первого байта 10-битного адреса.
BTF (Byte transfer finished) — флаг устанавливается по окончании приема/передачи байта, работает только при NOSTRETCH равном нулю.
ADDR(Address sent (master mode)/matched (slave mode)) — в режиме master устанавливается после передачи адреса, в режиме slave устанавливается при совпадении адреса. Для сброса нужно прочитать регистр SR1, а затем SR2.
SB(Start bit (Master mode)) — устанавливается при возникновении сигнала START. Для сброса флага необходимо прочитать SR1 и записать данные в регистр DR .
Регистр статуса I2C_SR2:
PEC[7:0](Packet error checking register) — в это битовое поле записывается контрольная сумма кадра.
DUALF(Dual flag (Slave mode)) — ноль в этом бите говорит о том, что адрес который принял Slave соответствует OAR1, иначе OAR2.
SMBHOST(SMBus host header (Slave mode)) — устанавливается, когда принят заголовок SMBus Host.
SMBDEFAULT(SMBus device default address (Slave mode)) — устанавливается, если принят адрес по умолчанию
для SMBus-устройства.
GENCALL(General call address (Slave mode)) — устанавливается, если принят широковещательный адрес в режиме ведомого.
TRA(Transmitter/receiver) — единица в этом бите говорит о том, что модуль работает как передатчик, иначе приемник.
BUSY(Bus busy) — флаг занятости.
MSL(Master/slave) — единица в этом бите говорит о том, что модуль работает в режиме Master, иначе Slave.
Регистр управления частотой I2C_CCR:
F/S(I2C master mode selection) — при установке единицы в этот бит модуль работает в режиме FAST, иначе STANDART.
DUTY (Fm mode duty cycle) — этот бит задает скважность сигнала SCL в режиме FAST. Если установлен ноль tlow/thigh = 2, иначе tlow/thigh = 16/9.
CCR[11:0](Clock control register in Fm/Sm mode (Master mode)) — при работе в режиме Master задает тактовую частоту линии SCL.
Sm mode or SMBus:
Thigh = CCR * TPCLK1
Tlow = CCR * TPCLK1
Fm mode:
If DUTY = 0:
Thigh = CCR * TPCLK1
Tlow = 2 * CCR * TPCLK1
If DUTY = 1: (to reach 400 kHz)
Thigh = 9 * CCR * TPCLK1
Tlow = 16 * CCR * TPCLK1
Далее надо подставить значения Thigh и Tlow в след формулу
Tscl = Thigh + Tlow
где Tscl для SM 10uS, а для FM 2,5 uS.
Получаем для режима SM следующее:
CCR * TPCLK1 + CCR * TPCLK1 = 10 000ns
CCR = 10 000/(2* TPCLK1)
Регистр I2C_TRISE:
TRISE[5:0] — определяет время нарастания фронта. Рассчитывается по формуле (Tr max/TPCLK1)+1,
где Tr max для SM составляет 1000nS, а для FM 300nS,
а TPCLK1 — период который рассчитывается как 1/F(APB1).
Регистр управления фильтрами I2C_FLTR:
ANOFF(Analog noise filter OFF) — ноль в этом бите включает аналоговый фильтр.
DNF[3:0](Digital noise filter) — битовое поле для настройки цифрового фильтра. За подробностями нужно обратиться к документации.
Инициализация модуля из рабочего проекта.
void I2C2_Init(void)
{
/*
SDL -> PB10
SDA -> PB11
RST -> PE15
*/
//включаем тактирование портов и модуля I2C
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOEEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
//альтернативная ф-ция, выход с открытым стоком, 2 MHz
GPIOB->AFR[1] |= (0x04<<2*4);
GPIOB->AFR[1] |= (0x04<<3*4);
GPIOB->MODER |= GPIO_MODER_MODER10_1;
GPIOB->OTYPER |= GPIO_OTYPER_OT_10;
GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR10;
GPIOB->MODER |= GPIO_MODER_MODER11_1;
GPIOB->OTYPER |= GPIO_OTYPER_OT_11;
GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR11;
//PE15 двухтактный выход 50MHz
GPIOE->MODER |= GPIO_MODER_MODER15_0;
GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR15;
AU_RST_HIGH
//настраиваем модуль в режим I2C
I2C2->CR1 &= ~2C_CR1_SMBUS;
//указываем частоту тактирования модуля
I2C2->CR2 &= ~I2C_CR2_FREQ;
I2C2->CR2 |= 42; // Fclk1=168/4=42MHz
//конфигурируем I2C, standart mode, 100 KHz duty cycle 1/2
I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_DUTY);
//задаем частоту работы модуля SCL по формуле 10 000nS/(2* TPCLK1)
I2C2->CCR |= 208; //10 000ns/48ns = 208
//Standart_Mode = 1000nS, Fast_Mode = 300nS, 1/42MHz = 24nS
I2C2->TRISE = 42; //(1000nS/24nS)+1
//включаем модуль
I2C2->CR1 |= I2C_CR1_PE;
}
void I2C_Write(uint8_t reg_addr, uint8_t data)
{
//стартуем
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства
I2C2->DR = I2C_ADDRESS(ADDR,I2C_MODE_WRITE);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//передаем адрес регистра
I2C2->DR = reg_addr;
while(!(I2C2->SR1 & I2C_SR1_TXE)){};
//пишем данные
I2C2->DR = data;
while(!(I2C2->SR1 & I2C_SR1_BTF)){};
I2C2->CR1 |= I2C_CR1_STOP;
}
uint8_t I2C_Read(uint8_t reg_addr)
{
uint8_t data;
//стартуем
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства
I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_WRITE);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//передаем адрес регистра
I2C2->DR = reg_addr;
while(!(I2C2->SR1 & I2C_SR1_TXE)){};
I2C2->CR1 |= I2C_CR1_STOP;
//рестарт!!!
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства, но теперь для чтения
I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_READ);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//читаем
I2C2->CR1 &= ~I2C_CR1_ACK;
while(!(I2C2->SR1 & I2C_SR1_RXNE)){};
data = I2C2->DR;
I2C2->CR1 |= I2C_CR1_STOP;
return data;
}
In this tutorial, we will discuss about STM32 I2C communication modes, hardware overview and functionalities, I2C interrupts, handling I2C transactions for both master and slave including HAL APIs for I2C for different I2C modes.
STM32 I2C Introduction
In this section, let us now focus on the I2C hardware module of STM32 including its features, modes of operations and data transactions.
The I2C bus acts as an interface between the STM32 board and the I2C serial bus. It is responsible for controlling all the I2C bus timing and sequencing and multi master capability. The STM32 on-chip IC supports the standard and the fast mode for the I2C bus.

The diagram below shows the block diagram of the I2C module in STM32.

The module consists of a shift and data register, along with control logic for the DMA requests and ACK and interrupts. The I2C transaction steps are all handled which include address match check, clock control, noise filter, error check etc.
Key Features
Lets list some of the key features of the STM32 I2C protocol:
- Has multi-master capability meaning that it can act both as a master and as a slave
- The I2C master features clock, start and stop generation.
- The I2C slave features stop bit detection, I2C address detection which is programmable in nature and dual address capability.
- Is operable on two bus speeds: standard (maximum 100 kHz) and fast (maximum 400 kHz).
- Contains an analog noise filter
- Generates and detects 7 bit and 10 bit addressing
- Has compatibility of SMBus 2.0 and PMBus
- Packet Error Checking (PEC) generation/verification can also be configured
- Has a buffer of 1 byte for DMA
- Features one interrupt for data communication and another for the error condition.
STM32 I2C Modes
The I2C interface for STM32 can be configured in four modes. The STM32 boards can either act as a slave transmitter, slave receiver, master transmitter or master receiver. By default, the slave mode is being used. Multi master capability is also a feature of this module, where the board automatically changes from being a slave to a master and vice versa according to the start, stop conditions or occurrence of arbitration loss.
Lets look at each of these four modes closely.
I2C Slave Mode (receiver and transmitter)
As mentioned previously, the I2C mode by default is set in slave mode. The peripheral input clock is of high importance as it governs the correct timings according to the values set in the I2C_CR2 register. Moreover, when using in standard mode, the input clock frequency should have a minimum value of 2MHz. Likewise, when using in fast mode, 4MHz minimum input clock frequency is required.
When the mode switches from slave to master, a Start condition is generated. When the detection of this condition, the shift register receives the address from the SDA line. This address is then compared to the address of the interface. The internal shift register is responsible for sending the received bytes from the SDA line to the DR register. This happens after the address is received and the ADDR is wiped off. The interface then produces an acknowledge signal when the ACK bit is set, for every byte.
Before the end of the next data reception, if the data in the DR register is not read considering the RxNE flag is also configured, then the BTF is set and the I2C interface stands by until the BTF is cleared. The I2C_SR1 reads the BTF which clears it. After that, the I2C_DR register reads it and sets the SCL line to a LOW state. A stop condition is produced by the master when the last data byte is sent. If an interrupt was set (ITEVFEN bit), then an interrupt will be generated when the I2C interface catches the stop condition. At this moment, the STOPF bit gets cleared when the SR1 register reads it and writes it to the CR1 register.
An important concept here is ‘clock stretching’. In this process, the slave holds the state of the SCL line is LOW. This ensures that the master device does not start a new data transaction until the slave is ready to do so, after it releases the SCL line again.
I2C Master Mode (receiver and transmitter)
Now lets look at the I2C master mode which is responsible for generating the clock signal and starting the data transfer. Each I²C command initiated by a master device starts with a START condition and ends with a STOP condition. For both conditions, SCL has to be high. After the Start condition, the bus is considered busy and can be used by another master only after a Stop condition is detected.
The master device pulls SDA (serial data) low and leaves SCL (serial clock) high in order to start the address frame. It alerts all the slave devices that a transmission is going to get started. If there are two or more master devices, then whichever master device brings the SDA low first will take ownership of the bus.
- Start condition: A high to low transition of SDA.
- Stop condition: A low to high transition of SDA.

Lets look the sequence of steps that are followed by the master device for initiating data transaction.
- Firstly, the I2C_CR2 register is programmed in such a manner for the peripheral input clock to produce correct timings.
- Next, the clock control registers and the rise time register is configured.
- The peripheral is enabled by programming the I2C_CR1 register.
- Finally, the START condition is generated by setting the START bit in the I2C_CR1 register.
Moreover, when using in standard mode, the peripheral input clock frequency should have a minimum value of 2MHz. Likewise, when using in fast mode, 4MHz minimum peripheral input clock frequency is required.
STM32 I2C Packet Error Checking
One of the great features of STM32 I2C module is the implementation of PEC which is packet error checking mechanism. This is to enhance the reliability of the I2C communication. This packet error checking mechanism helps to automatically detect errors without involving software by inspecting large data packet transactions. One important thing to note here is that if the master loses an arbitration at any point, then the device goes back to slave mode as the packet error checking got corrupted. When the master completes its data transaction, then the process starts again and the slave device has to wait all this time.
To calculate the PEC, the following CRC-8 polynomial formula is used serially on every bit:
C(x) = x8 + x2 + x + 1
STM32 I2C Error Conditions
The STM32 I2C module also offers detection of error conditions that occur on the hardware level. The table below shows the error conditions which can be monitored and checked through their respective flag bits associated with each error signal.
| BERR (Bus Error) | The bus error occurs if an external start/stop condition is detected in the course of data transfer/address. |
| AF (Acknowledge Error) | If a non-acknowledge bit is detected, then this error occurs. |
| ARLO (Arbitration Lost Error) | This error occurs whenever an arbitration lost condition happens. |
| OVR (Overrun/Underrun Error) | The overrun error occurs when the device which is working as a slave with disabled clock stretching, receives data. Such a situation exists when the interface receives a byte of data when it has not been read in DR before the next byte is received by the interface.
However, the underrun error occurs when the device which is working as a slave with disabled clock stretching, transmits data. Such a situation exists when the interface has not updated the DR with the next byte before the clock comes for the next byte. |
STM32 I2C Interrupts
The STM32 I2C module also features interrupt events which when detected cause an interrupt to be triggered which the software detects. Each interrupt event has its own specific enable control bit which is set whenever an interrupt is generated.
The table below lists the I2C interrupt requests along with their event flags and the corresponding enable control bits:
| Interrupt Event | Event Flag | Enable Control Bit |
| Start Bit Sent (Master) | SB | ITEVFEN |
| Address Sent (Master) or Address Matched (Slave) | ADDR | ITEVFEN |
| 10-bit Header Sent (Master) | ADD10 | ITEVFEN |
| Stop Received (Slave) | STOPF | ITEVFEN |
| Data Byte Transfer Finished | BTF | ITEVFEN |
| Receive Buffer Not Empty | RxNE | ITEVFEN & ITBUFEN |
| Transmit Buffer Empty | TxE | ITEVFEN & ITBUFEN |
STM32 I2C Master Transmission and Reception (Tx & RX)
STM32 I2C data transmission and reception can be handled in three different ways blocking/n-blocking which means that the I2C module can be configured differently for each case.
I2C with Polling Mode
The I2C module when configured in polling mode works in blocking mode. It is one of the simplest configurations where the CPU waits until the current byte of data is transmitted and then sends the next one and so on. After the data transmission completes, the CPU starts again and continues with the execution of the main code. Both data transmission and reception works in the same way, where there is a wait state between each data transaction. This causes this method to be a bit inefficient as the CPU spends a lot of precious time waiting.
I2C with Interrupt Mode
The I2C module when configured in polling mode works in non-blocking mode. Unlike the polling method, the CPU does not stop but continues the execution of the main code. The completion of the data transaction marks the triggering of the interrupt. The signal is generated when it is time for servicing by the CPU. This works for both data transmission and data reception. This method reduces a lot of time that was wasted before in the polling mode. However, using the interrupt method with high rate of transactions can however overload the CPU and they may also potentially harm the timing of the system.
I2C with DMA Mode
In this scenario, the I2C module is configured in non-blocking mode where the DMA sends the data transactions from the peripheral to the memory directly. The CPU is not involved as was the case in polling and interrupt. It frees the CPU from all the data transfers thus making the system very time efficient. It uses a basic request/acknowledge protocol. The data register generates the DMA request by emptying the register during data transmission and filling it completely during data reception.
STM32 I2C Device Memory Read / Write
Most of the functions that we already discussed related to the I2C interface occur automatically by the hardware through the setting and clearing of the control bits. To set the configuration of the set/clear bits of the control registers, some simple functions are used.
Master Data TX
One of the most commonly used HAL function to initiate data transfer over I2C protocol for a master is as follows:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef * hi2c,
uint16_t DevAddress,
uint8_t * pData,
uint16_t Size,
uint32_t Timeout
)
This function transmits an amount of data in master mode in blocking mode. This function takes in several parameters which include:
- Pointer to the I2C_HandleTypeDef structure that holds the configuration parameters for the specified I2C.
- Target device address
- Pointer to the data buffer
- Size of the data to be sent
- Duration of the timeout
The Master_Transmit collection of functions are available for all three modes: blocking, interrupt and DMA.
Device Memory Write
However, we have HAL APIs for the I2C driver firmware library which is the device memory read/write for all three operable modes as well. In blocking mode, the API for memory write is shown below:
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
)
This function is used to write an amount of data in blocking mode to a particular memory address. This function takes in several parameters which include:
- Pointer to the I2C_HandleTypeDef structure that holds the configuration parameters for the specified I2C.
- Target device address
- Internal memory address
- Size of the internal memory address
- Pointer to the data buffer
- Size of the data to be sent
- Duration of the timeout
Lets understand the basic difference between I2C_Transmit() and I2C_Mem_Write() through an example of an I2C sensor for example MPU6050 which acts as an I2C slave. This device has its own unique internal I2C device address which is used by the master to identify the sensor module and address it. This address is held inside the registers present inside the sensor. In order to configure the sensor according to our needs, we have to set/read the value of these internal registers accordingly.
So what happens when the master STM32 microcontroller wants to access the readings of the slave I2C sensor. The table below shows the single byte read sequence that follows between the master and the slave.
| Master | S | AD+W | RA | S | AD+R | NACK | P | ||||
| Slave | ACK | ACK | ACK | DATA |
As you can see in the table above, the master initiates the transaction and then sends AD+W, address of the slave along with write operation. This will cause the master to write the register address RA inside the slave that it wants to read. The slave acknowledges (ACK) the command and sends the data present in the particular register. The master will then end the I2C communication. This is how a register is read from a particular address inside the slave which has its own address on the I2C bus.
Using CubeMX IDE to Configure I2C Mode
To program STM32 microcontroller in I2C mode for data transmission and reception we use STMCube IDE to configure all the parameters. First of all head over to Connectivity and select the I2C channel. STM32 Blue Pill consists of two I2C channels, I2C1 and I2C2. After selecting the channel, you will have to select the I2C mode from the list shown below.

After selecting the mode as ‘I2C’, you will be able to view the parameter settings. This includes the I2C speed mode and clock speed for the master. For the I2C slave, we can set the I2C device address length, value and enable/disable the clock in no stretch mode etc.

Moreover, the user can head over to NVIC Settings to enable the event and error interrupt, if using the interrupt mode.

Similarly, the user can configure the DMA mode, by heading over to DMA Settings and adding the DMA request for I2C TX and I2C RX according to the I2C channel selected.
In this section, we will discuss some important HAL APIs related to STM32 I2C module which includes all the three modes.
STM32 I2C Data Transmission and Reception in Blocking Mode
The following function is used to transmit an amount of data in master mode in blocking mode.
HAL_I2C_Master_Transmit (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t* pData, uint16_t Size, uint32_t Timeout);
Likewise, this function is used to receive an amount of data in master mode in blocking mode.
HAL_I2C_Master_Receive (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t* pData, uint16_t Size, uint32_t Timeout);
For the slave in blocking mode, the following function is used to transmit an amount of data.
HAL_I2C_Slave_Transmit (I2C_HandleTypeDef * hi2c, uint8_t * pData, uint16_t Size, uint32_t Timeout);
Similarly, the following function is used to receive an amount of data in slave mode in blocking mode.
HAL_I2C_Slave_Receive (I2C_HandleTypeDef * hi2c, uint8_t * pData, uint16_t Size, uint32_t Timeout);
STM32 I2C Data Transmission and Reception in Interrupt Mode
When using the I2C module in interrupt mode, the following function will transmit an amount of data in master mode, in non-blocking mode with interrupt.
HAL_I2C_Master_Transmit_IT (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size);
Similarly, the following function will be called to receive an amount of data in master mode, in non-blocking mode with interrupt.
HAL_I2C_Master_Receive_IT (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size);
STM32 IC Data Transmission and Reception in DMA Mode
This function is used to transmit an amount of data in master mode, in non-blocking mode with DMA.
HAL_I2C_Master_Transmit_DMA (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size);
The following function will receive an amount of data in master mode, in non-blocking mode with DMA.
HAL_I2C_Master_Receive_DMA (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint8_t * pData, uint16_t Size);
Device Memory Write/Read
This function is used to write an amount of data in blocking mode to a particular memory address.
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);
This function is used to read an amount of data in blocking mode to a particular memory address.
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);
You can read following tutorials where we used I2C channels of STM32 Blue Pill to interface different display and get readings from different sensors:
- SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE
- STM32 Blue Pill BME280 Data Logger using STM32CubeIDE
- BME280 Sensor with STM32 Blue Pill using STM32CubeIDE
- I2C LCD with STM32 Blue Pill using STM32CubeIDE
-
-
October 29 2015, 06:41
- Техника
- Cancel
Ставлю рекорд, третий пост за сегодня, а точнее за ночь.
Я тут допилил чтения I2C, возложил все на прерывания в место тупых цыклов. Разрешил общее прерывания от I2C, и когда вылетает какой то флаг, то попадаем в это прерывания, где проверяем что у нас там случилось. Запили я это все пока что для старта чтения пакета байт, после чего пинаю DMA, который продолжает эту грязную работенку.
Инцилизация прерывания для I2C:

Для старта всей этой хитрой замуты нам надо всего лишь пнуть в I2C сигнал — START, и все, все остальное сделает переферия.
Теперь когда вылетает флаг SB- формирования старта закончена, попадаем в общее прерывания, где проверяем какой флаг потревожил нас,
Если это был SB, то суем в i2c адрес MPU6050, и сваливаем нафиг от сюда. Пришлось завести глобальную переменную STEP_I2C, которая показывает этап подготовки интерфейса i2c для чтения. Если STEP_I2C=1; то это означает что отправлен второй СТАРТ бит, и теперь нам надо отправить адрес MPU6050 с младшим битом =1, что означает режим Read.

Теперь когда датчик MPU6050 подготовит нам пачку данных, он дёрнет за ногу микроконтроллера, и в программе попадаем в обработчик внешнего прерывания :

Тут ребутим DMA, и включаем его, после чего он принимает все байти от сенсора и складывает их аккуратненько в масив.
До этого прерывания EXIT2 занимало примерно 1000мкс, а вот теперь оно стало 7мкс !!!!

Вот так вот! В 140 раз уменьшили бесполезное трату процессорного времени, убрав тупые циклы. Теперь реально читаем сенсор полностью аппаратно, и у нас просто в нашей глобальной переменно обновляются данные со скоростью 1000 раз в секунду.
Теперь можно приступать к обработке данных с полным запасом процессорного временем.
Завтра я напишу обработку ошибок с i2c, так же через прерывания, а теперь можно пойти поспать.
Работая по шине i2c в режиме ожидания флагов контроллер получает данные. Решил сделать работу на прерываниях но после генерации условия START прерывания не происходит программа выполняется дальше. Может неправильная инициализация? Подскажите кто знает.
Код:
void I2C_init(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);//тактируем пины для i2c
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);//тактируем сам i2c
//--------------------------------------------
GPIO_InitTypeDef busi2c;//создаем структуры
I2C_InitTypeDef i2c;//для инициализации
//------------------------------------
i2c.I2C_ClockSpeed = 100000;//100kHz
i2c.I2C_Mode = I2C_Mode_I2C;
i2c.I2C_DutyCycle = I2C_DutyCycle_2;
i2c.I2C_OwnAddress1 = OWNADRESS;
i2c.I2C_Ack = I2C_Ack_Enable;
i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &i2c);
//---------------------------------------
busi2c.GPIO_Pin = BUS_SCL|BUS_SDA;
busi2c.GPIO_Mode = GPIO_Mode_AF;
busi2c.GPIO_Speed = GPIO_Speed_2MHz;
busi2c.GPIO_OType = GPIO_OType_OD;
busi2c.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &busi2c);
//---------------------------------------------
GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1);//включаем альтернативные
GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_I2C1);//функции ножек
//----------------------------------------------
I2C_Cmd(I2C1, ENABLE);//включаем модуль i2c
}
void init_IT_i2c(void)
{
NVIC_InitTypeDef IT_i2c;
IT_i2c.NVIC_IRQChannel = I2C1_EV_IRQn;//прерывания по событию
IT_i2c.NVIC_IRQChannelPreemptionPriority = 0;//приоритет прерывания чем ниже значение тем выше приоритет
IT_i2c.NVIC_IRQChannelSubPriority = 0;//Sub приоритет прерывания чем ниже значение тем выше приоритет
IT_i2c.NVIC_IRQChannelCmd = ENABLE;//включение параметра канала прерывания
NVIC_Init(&IT_i2c);//инициализируем прерывание
IT_i2c.NVIC_IRQChannel = I2C1_ER_IRQn;//прерывания по ошибке
IT_i2c.NVIC_IRQChannelPreemptionPriority = 0;//приоритет прерывания чем ниже значение тем выше приоритет
IT_i2c.NVIC_IRQChannelSubPriority = 0;//Sub приоритет прерывания чем ниже значение тем выше приоритет
IT_i2c.NVIC_IRQChannelCmd = ENABLE;//включение параметра канала прерывания
NVIC_Init(&IT_i2c);
I2C_ITConfig(I2C1, I2C_IT_EVT, ENABLE);//включили прерывания по событию
I2C_ITConfig(I2C1, I2C_IT_ERR, ENABLE);//включили прерывания по ошибке
}
void I2C_start_IT(void)
{
// ждем освобождения шины
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_ITConfig(I2C1, I2C_IT_BUF, ENABLE);//включаем прерывания для буфера приема и передачи
I2C_GenerateSTART(I2C1, ENABLE);//генерируем старт
}
После генерации START зажигается флаг SB но прерывания не срабатывают