Компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе. Поэтому я создал новую рубрику, посвященную работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Также вот глобальная рубрика по STM32 - ссылка.
Довольно часто возникает необходимость связать микроконтроллер STM32 c другим микроконтроллером или с другим девайсом, например, внешней памятью. И тут на помощь приходит шина I2C, о которой до сих пор не было написано статьи на нашем сайте. Пора исправлять это недоразумение )
И снова для начала кратко обсудим теоретические аспекты. Итак, I2C – последовательная шина данных, разработанная Philips около тридцати лет назад. Интерфейс I2С очень часто применяется в различных девайсах для реализации внутренней связи между устройствами. Возможные применения I2С включают, например:
- Обмен информацией между микроконтроллерами
- Доступ к модулям памяти
- Обмен данными с модулями АЦП
- Обмен данными с ЦАП
И это далеко не полный список. I2С гарантирует неплохую скорость работы при относительной простоте разработки и низкой себестоимости. К слову о скоростях:
- Обычный режим – до 100КГц
- Быстрый режим – до 400КГц
I2С использует две линии, которые подтянуты к напряжению питания. Одна линия – линия данных, другая – линия тактирования. Я не буду расписывать тут как работает I2С, потому что это уже много-много раз описано. Без труда можно найти в интернете материал на свой вкус. А мы сразу перейдем к особенностям реализации I2С в STM32.
Основные характеристики шины:
- Каждое устройство может выступать как в роли Master, так и Slave.
- В режиме Master реализуется:
- Генерация тактового сигнала.
- Генерация старт-сигнала и стоп-сигнала.
- В режиме Slave реализуется:
- Механизм подтверждения адреса.
- Использование двух Slave адресов.
- Обнаружение стоп-бита, выданного ведущим на линию.
- Генерация и определение 7/10 битных адресов.
- Поддержка разных скоростей передачи данных.
- Наличие множества флагов, сигнализирующих о событиях, а также об ошибках на линии.
Возможна работа в одном из следующих режимов:
- Slave приемник
- Slave передатчик
- Master приемник
- Master передатчик
По умолчанию установлен режим Slave, но как только устройство генерирует старт-бит оно сразу же превращается из подчиненного в ведущего.
Блок-схема модуля I2С:
Само собой на каждое событие I2С можно повесить прерывание, смотрите сами:
Для работы с I2С в STM32 выделено 9 регистров. Не будем на этом останавливаться сейчас подробно, но в даташит обязательно загляните.
А мы двинемся дальше и перейдем к практическому использованию интерфейса. Посмотрим, как реализована работа с I2С в SPL. Как и для другой периферии, все настройки режима, скорости и всего остального находятся в заголовочном файле. Давайте смотреть. Находим там объявление структуры I2C_InitTypeDef:
typedef struct { uint32_t I2C_ClockSpeed; uint16_t I2C_Mode; uint16_t I2C_DutyCycle; uint16_t I2C_OwnAddress1; uint16_t I2C_Ack; uint16_t I2C_AcknowledgedAddress; }I2C_InitTypeDef;
Вот с этими полями нам и придется работать, чтобы настроить интерфейс I2C так, как мы хотим:
- uint32_t I2C_ClockSpeed – частота тактового сигнала, максимум – 400 КГц
- uint16_t I2C_Mode – тут все понятно, режим работы
- uint16_t I2C_DutyCycle – настройки для работы в быстром режиме
- uint16_t I2C_OwnAddress – собственный адрес устройства
- uint16_t I2C_Ack – включено или нет использование бита подтверждения Ack
- uint16_t I2C_AcknowledgedAddress – выбор формата адреса, 7 бит или 10 бит
С этим вроде бы все понятно.
Не будем мудрить и придумывать хитрую задачу, просто напишем основные функции для работы с I2C, а там уже каждый сам найдет им применение. И для начала, конечно же, инициализация. Чуть не забыл сказать - я буду использовать STM32F4 для этого проекта, но в принципе это не так уж и важно. Если же вы используете библиотеку HAL Driver, то вот ссылка на пример работы с шиной в STM32CubeMx.
/***************************************************************************************/ GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; /***************************************************************************************/ void init_I2C1(void) { // Включаем тактирование нужных модулей RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // I2C использует две ноги микроконтроллера, их тоже нужно настроить gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_OType = GPIO_OType_OD; gpio.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOB, &gpio); GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_I2C1); // А вот и настройка I2C i2c.I2C_ClockSpeed = 100000; i2c.I2C_Mode = I2C_Mode_I2C; i2c.I2C_DutyCycle = I2C_DutyCycle_2; // Адрес я тут взял первый пришедший в голову i2c.I2C_OwnAddress1 = 0x15; i2c.I2C_Ack = I2C_Ack_Disable; i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &i2c); // Ну и включаем, собственно, модуль I2C1 I2C_Cmd(I2C1, ENABLE); } /***************************************************************************************/
С инициализацией разобрались, что же дальше? Давайте напишем функцию для начала общения по I2C. Хотелось бы сделать что-нибудь универсальное, поэтому в эту функцию мы будем передавать три параметра - номер используемого модуля I2C, направление передачи данных и адрес подчиненного устройства.
/***************************************************************************************/ void I2C_StartTransmission(I2C_TypeDef* I2Cx, uint8_t transmissionDirection, uint8_t slaveAddress) { // На всякий слуыай ждем, пока шина осовободится while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY)); // Генерируем старт - тут все понятно ) I2C_GenerateSTART(I2Cx, ENABLE); // Ждем пока взлетит нужный флаг while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT)); // Посылаем адрес подчиненному I2C_Send7bitAddress(I2Cx, slaveAddress, transmissionDirection); // А теперь у нас два варианта развития событий - в зависимости от выбранного направления обмена данными if(transmissionDirection== I2C_Direction_Transmitter) { while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); } if(transmissionDirection== I2C_Direction_Receiver) { while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); } } /***************************************************************************************/
С этим разобрались, осталось написать главные функции - для приема и передачи данных!
/***************************************************************************************/ void I2C_WriteData(I2C_TypeDef* I2Cx, uint8_t data) { // Просто вызываем готовую функцию из SPL и ждем, пока данные улетят I2C_SendData(I2Cx, data); while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); } /***************************************************************************************/ uint8_t I2C_ReadData(I2C_TypeDef* I2Cx) { uint8_t data; // Тут картина похожа, как только данные пришли быстренько считываем их и возвращаем while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)); data = I2C_ReceiveData(I2Cx); return data; } /***************************************************************************************/
Чуть не забыли важную часть! Закончив обмен данными по I2C, необходимо вызвать функцию I2C_GenerateSTOP(I2Cx, ENABLE). Не забываем про этот момент.
Вот так вот получилось. При использовании I2C для передачи данных, например, последовательность действий должна быть такой:
- инициализируем модуль I2C, нужные ножки контроллера, ну и все остальное
- посылаем "старт"
- шлем данные
- генерируем "стоп"
Как видите для всех этих пунктов мы уже подготовили специальные функции. Поэтому на этом и остановимся на сегодня, до скорых встреч на сайте!