STM32 и I2C. Настройка и использование в STM32CubeMx.

Уже много разнообразных модулей микроконтроллеров STM32 мы рассмотрели в рамках курса по STM32CubeMx. Но вот до I2C дело как-то все не доходило… Что же, сегодня исправим это недоразумение! И для начала разберем простой обмен данными “в лоб”, а затем перейдем к работе на прерываниях.

У меня как раз есть плата на базе STM32F103, на которой помимо всего прочего установлен датчик давления BMP280, который подключен как раз по I2C. Скоро я отдельно расскажу про эту плату и про все возможности BMP280, сегодня же мы задействуем интерфейс I2C для чтения данных из его регистров.

Открываем даташит и, как и во многих других датчиках, первым в списке идет регистр “ID”. И традиционно именно этот регистр используется для отладки коммуникации, потому что его значение строго фиксировано. Если считанное значение равно тому, которое указано в документации – связь работает, если нет… в общем, все понятно 🙂

BMP280 I2C registers.

В данном случае “правильным” для нас будет значение 0x58. То есть план таков:

  • конфигурируем I2C модуль
  • отправляем датчику команду на чтение регистра ID
  • проверяем полученное значение

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

Пара слов по электронике… На моей плате датчик подключен к I2C1:

  • PB8 – I2C SCL
  • PB9 – I2C SDA

По умолчанию для I2C1 в Cube активируются пины PB6 и PB7, так что в данном случае нужно будет поменять их на соседние. Да, кстати, микроконтроллер – STM32F103RET6, так что проект будет именно для него. Но, как и в любом проекте, созданном с помощью STM32CubeMx, изменить в случае необходимости контроллер будет максимально просто 🙂 И, конечно же, всегда при работе с I2C не забываем о подтягивающих резисторах, я обычно ставлю 4.7 КОм:

STM32 I2C подтягивающие резисторы.

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

BMP280 pinout.

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

Итак, пора переходить к созданию проекта. Здесь все проходит как обычно 🙂 (создание проекта и настройки тактирования). Только для разнообразия давайте задействуем в этом проекте внутренний источник тактирования вместо внешнего:

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

Настройка SWD.
STM32 I2C pinout.

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

STM32 I2C STM32CubeMx settings

Генерируем проект и переходим к его редактированию! Сразу же определим адрес датчика:

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define I2C_ADDRESS                                              0x76

/* USER CODE END PD */

И вот тут есть один небольшой, но важный нюанс.Многие проблемы с работой I2C связаны именно с этим 🙂 При отправке данных BMP280 первый из отправляемых байтов является не просто адресом устройства, а адресом (который у нас состоит из 7 битов), дополненным еще одним битом. Этот бит RW отвечает за направление передачи данных, то есть определяет выполняем мы чтение или запись:

Команда на чтение по I2C.

И поскольку бит 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 readData = 0;
uint8_t regAddress = I2C_ID_ADDRESS;

/* USER CODE END PV */

И, наконец-то, перейдем к отправке данных:

/* USER CODE BEGIN 2 */
  HAL_I2C_Master_Transmit(&hi2c1, (I2C_ADDRESS << 1), &regAddress, 1,  I2C_TIMEOUT);
  HAL_I2C_Master_Receive(&hi2c1, (I2C_ADDRESS << 1), &regData, 1,  I2C_TIMEOUT);

  /* USER CODE END 2 */

В результате в переменной regData у нас окажется верное значение регистра ID:

Значение регистра 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:

Прерывания I2C.

Теперь можно переходить к коду. В функции main() выполняем первый из этих этапов:

HAL_I2C_Master_Transmit_IT(&hi2c1, (I2C_ADDRESS << 1), &regAddress, 1);

Теперь на очереди callback-функции:

/* USER CODE BEGIN 4 */
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
  HAL_I2C_Master_Receive_IT(&hi2c1, (I2C_ADDRESS << 1), &regData, 1);
}



void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
  // I2C data ready!
}


/* USER CODE END 4 */

Если мы поставим брейкпоинт в функции HAL_I2C_MasterRxCpltCallback(), то можем под отладчиком увидеть, что в переменной regData как и при работе в Polling режиме находится абсолютно верное значение!

И на этом я на сегодня заканчиваю, спасибо всем за внимание, надеюсь эти наработки будут вам полезны в ваших проектах и задачах!

Выкладываю полный проект к этой статье – ссылка.

Поделиться!

Подписаться
Уведомление о
guest
1 Комментарий
старее
новее большинство голосов
Inline Feedbacks
View all comments
mitek
mitek
2 месяцев назад

Советую прочитать документ от NXP по корретной инициализации I2C шины. Это позволит избежать труднообъяснимых ошибок.

Присоединяйтесь!

Profile Profile Profile Profile Profile
Vkontakte
Twitter

Язык сайта

Август 2020
Пн Вт Ср Чт Пт Сб Вс
 12
3456789
10111213141516
17181920212223
24252627282930
31  

© 2013-2020 MicroTechnics.ru