Уже много разнообразных модулей микроконтроллеров STM32 мы рассмотрели в рамках курса по STM32CubeMx. Но вот до I2C дело как-то все не доходило… Что же, сегодня исправим это недоразумение! И для начала разберем простой обмен данными “в лоб”, а затем перейдем к работе на прерываниях.
У меня как раз есть плата на базе STM32F103, на которой помимо всего прочего установлен датчик давления BMP280, который подключен как раз по I2C. Скоро я отдельно расскажу про эту плату и про все возможности BMP280, сегодня же мы задействуем интерфейс I2C для чтения данных из его регистров.
Открываем даташит и, как и во многих других датчиках, первым в списке идет регистр “ID”. И традиционно именно этот регистр используется для отладки коммуникации, потому что его значение строго фиксировано. Если считанное значение равно тому, которое указано в документации – связь работает, если нет… в общем, все понятно 🙂

В данном случае “правильным” для нас будет значение 0x58. То есть план таков:
- конфигурируем I2C модуль
- отправляем датчику команду на чтение регистра ID
- проверяем полученное значение
В итоге мы получим минимально необходимый функционал, который уже можно использовать в своих проектах каким угодно образом!
Пара слов по электронике… На моей плате датчик подключен к I2C1:
- PB8 – I2C SCL
- PB9 – I2C SDA
По умолчанию для I2C1 в Cube активируются пины PB6 и PB7, так что в данном случае нужно будет поменять их на соседние. Да, кстати, микроконтроллер – STM32F103RET6, так что проект будет именно для него. Но, как и в любом проекте, созданном с помощью STM32CubeMx, изменить в случае необходимости контроллер будет максимально просто 🙂 И, конечно же, всегда при работе с I2C не забываем о подтягивающих резисторах, я обычно ставлю 4.7 КОм:

Кроме того, необходимо определиться с I2C адресом датчика. У BMP280 6 старших битов адреса фиксированы, а младший бит определяется сигналом на выводе SDO:

Таким образом, адрес имеет следующий вид – 0b111011x, где x = 1, если на SDO высокий уровень сигнала, и x = 0 при логическом нуле. У меня как раз этот вывод подтянут к земле, поэтому получаем адрес – 0b1110110 = 0x76.
Итак, пора переходить к созданию проекта. Здесь все проходит как обычно 🙂 (создание проекта и настройки тактирования). Только для разнообразия давайте задействуем в этом проекте внутренний источник тактирования вместо внешнего:

Включаем модуль I2C1 и настраиваем те выводы, которые у нас используются, то есть PB8 и PB9. Также выбираем Serial Wire в окне Debug для отладки:


На этом, в общем-то и все, настройки I2C пока менять нет никакой необходимости, оставляем все по умолчанию:

Генерируем проект и переходим к его редактированию! Сразу же определим адрес датчика:
/* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ #define I2C_ADDRESS 0x76 /* USER CODE END PD */
И вот тут есть один небольшой, но важный нюанс.Многие проблемы с работой I2C связаны именно с этим 🙂 При отправке данных BMP280 первый из отправляемых байтов является не просто адресом устройства, а адресом (который у нас состоит из 7 битов), дополненным еще одним битом. Этот бит RW отвечает за направление передачи данных, то есть определяет выполняем мы чтение или запись:

И поскольку бит RW является младшим, то 7 битов адреса нам нужно будет сдвигать на одну позицию влево при передаче в функции I2C HAL драйвера. Сам же бит RW HAL добавит к адресу автоматически, без нашего участия. Сейчас на примере со всем разберемся!
Итак, нам нужно прочитать значение регистра ID, адрес которого – 0xD0. Снова смотрим на схему – получается, что нам нужно отправить BMP280 команду с адресом самого датчика и адресом регистра, значение которого мы хотим получить. После этого мы снова выдаем на линию адрес и встаем на чтение. В HAL весь комплекс этих операций мы сможем осуществить всего двумя функциями:
- HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
- HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
Аргументы функций идентичны:
- hi2c – указатель на структуру, в которой хранится информация о модуле I2C. Эту структуру STM32CubeMx уже сгенерировал, она используется при настройке модуля.
- DevAddress – адрес устройства, не забываем сдвинуть на 1 бит влево.
- pData – указатель на буфер данных. При записи – данные из этого буфера будут передаваться по интерфейсу I2C. При чтении, соответственно, в этот буфер будут сохраняться полученные значения.
- Size – количество байтов данных для чтения/записи.
- Timeout – величина таймаута, по истечении которого функция выдаст ошибку и прекратит выполнение. Этот механизм необходим для того, чтобы при каком-либо сбое в коммуникации программа не зависала намертво на ожидании ответа.
С этим разобрались, идем дальше! Зададим упомянутый таймаут равным 10 мс и определим дефайном:
/* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ #define I2C_ADDRESS 0x76 #define I2C_ID_ADDRESS 0xD0 #define I2C_TIMEOUT 10 /* USER CODE END PD */
Кроме того, мы определили адрес регистра, с которым будет экспериментировать. Добавим еще парочку переменных:
/* USER CODE BEGIN PV */ uint8_t regData = 0; uint8_t regAddress = I2C_ID_ADDRESS; /* USER CODE END PV */
И, наконец-то, перейдем к отправке данных:
/* USER CODE BEGIN 2 */ HAL_I2C_Master_Transmit(&hi2c1, (I2C_ADDRESS << 1), ®Address, 1, I2C_TIMEOUT); HAL_I2C_Master_Receive(&hi2c1, (I2C_ADDRESS << 1), ®Data, 1, I2C_TIMEOUT); /* USER CODE END 2 */
В результате в переменной regData у нас окажется верное значение регистра ID:

Связь STM32 с датчиком по I2C налажена!
Здесь мы работали в так называемом Polling режиме, то есть вызов I2C функций блокировал выполнение программы до тех пор, пока функции не завершат свою работу. Иногда это вполне допустимо, иногда так даже удобнее, но довольно часто такие задержки не ведут ни к чему хорошему, поэтому нельзя обойти вниманием режим работы с I2C на прерываниях. И здесь нам на помощь придут две другие функции:
- 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)
Если говорить об аргументах, то тут отличие только одно. Отсутствует последний аргумент, который определяет величину таймаута, попросту потому что здесь нет никакого ожидания и необходимости в проверке таймаута 🙂
Кроме того, как и при работе с другой периферией на прерываниях, HAL предлагает нам использовать ряд callback-функций. Остановимся на двух из них:
- void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
- void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
Первая вызывается по окончанию передачи, а вторая, соответственно по окончанию приема. Получается, что нам нужно действовать так:
- вызываем функцию передачи по I2C
- далее контроллер может выполнять любые наши задачи и операции, мы же ждем вызова callback функции HAL_I2C_MasterTxCpltCallback()
- в теле этой функции мы запускаем процесс приема
- и ожидаем второго callback’а – по окончанию приема данных
Реализуем в проекте! Но для начала включим прерывания I2C в STM32CubeMx:

Теперь можно переходить к коду. В функции main() выполняем первый из этих этапов:
HAL_I2C_Master_Transmit_IT(&hi2c1, (I2C_ADDRESS << 1), ®Address, 1);
Теперь на очереди callback-функции:
/* USER CODE BEGIN 4 */ void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) { HAL_I2C_Master_Receive_IT(&hi2c1, (I2C_ADDRESS << 1), ®Data, 1); } void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) { // I2C data ready! } /* USER CODE END 4 */
Если мы поставим брейкпоинт в функции HAL_I2C_MasterRxCpltCallback(), то можем под отладчиком увидеть, что в переменной regData как и при работе в Polling режиме находится абсолютно верное значение!
И на этом я на сегодня заканчиваю, спасибо всем за внимание, надеюсь эти наработки будут вам полезны в ваших проектах и задачах!
Выкладываю полный проект к этой статье – ссылка.