I2C часть 2 (перевод из книги Mastering STM32)

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

14.2 Модуль HAL I2C

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

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

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

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

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

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

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

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

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

HAL_StatusTypeDef HAL_I2C_Init (I2C_HandleTypeDef *hi2c);

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

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

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

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

где:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    HAL_Init();
    Nucleo_BSP_Init();

    MX_I2C1_Init();

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

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

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

    /* Peripheral clock enable */
    __HAL_RCC_I2C1_CLK_ENABLE();

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

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

    HAL_I2C_Init(&hi2c1);
}

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

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

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

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

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

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

    return returnValue;
}

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

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

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

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

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

    free(data);

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

    return HAL_OK;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

    HAL_Init();
    Nucleo_BSP_Init();

    MX_I2C1_Init();

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

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

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

    while(1);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

void HAL_I2C_SlaveTxCpltCallback (I2C_HandleTypeDef *hi2c);

void HAL_I2C_SlaveRxCpltCallback (I2C_HandleTypeDef *hi2c);

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

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

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

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

volatile uint8_t transferDirection, transferRequested;

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

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

    HAL_Init();
    Nucleo_BSP_Init();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    while (1);
}

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

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

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

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

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

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

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

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

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

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

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *