Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кармин Новиелло - Освоение STM32.pdf
Скачиваний:
2742
Добавлен:
23.09.2021
Размер:
47.68 Mб
Скачать

I2C

413

Таблица 1: Доступность периферийных устройств I²C в микроконтроллерах, оснащающих все шестнадцать плат Nucleo

Теперь мы готовы рассмотреть, как использовать API-интерфейсы CubeHAL для программирования данного периферийного устройства.

14.2. Модуль HAL_I2C

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

typedef struct {

 

 

 

I2C_TypeDef

*Instance;

/* Базовый адрес регистров I²C

*/

I2C_InitTypeDef

Init;

/* Параметры I²C-связи

*/

uint8_t

*pBuffPtr;

/* Указатель на буфер передачи I²C

*/

uint16_t

XferSize;

/* Размер передачи I²C

*/

__IO uint16_t

XferCount;

/* Счетчик передачи I²C

*/

DMA_HandleTypeDef

*hdmatx;

/* Параметры дескриптора DMA I²C Tx

*/

DMA_HandleTypeDef

*hdmarx;

/* Параметры дескриптора DMA I²C Rx

*/

HAL_LockTypeDef

Lock;

/* Блокировка объекта I²C

*/

I2C

 

 

 

414

__IO HAL_I2C_StateTypeDef

State;

/* Состояние работы I²C

*/

__IO HAL_I2C_ModeTypeDef

Mode;

/*

Режим I²C-связи

*/

__IO uint32_t

ErrorCode;

/*

Код ошибки I²C

*/

} I2C_HandleTypeDef;

 

 

 

 

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

Instance (экземпляр): это указатель на дескриптор I²C, который мы будем исполь-

зовать. Например, I2C1 является дескриптором первого периферийного устройства I²C.

Init: экземпляр структуры Си I2C_InitTypeDef, используемой для конфигурации

периферийного устройства. Мы рассмотрим ее более подробно в ближайшее время.

pBuffPtr: указатель на внутренний буфер, используемый для временного хране-

ния данных, передаваемых на периферийное устройство I²C и с него. Он используется, когда I²C работает в режиме прерываний и данный буфер не должен изменяться из пользовательского кода.

hdmatx, hdmarx: указатель на экземпляры структуры DMA_HandleTypeDef, используе-

мые, когда периферийное устройство I²C работает в режиме DMA.

Конфигурация периферийного устройства I²C выполняется с использованием экземпляра структуры Си I2C_InitTypeDef, которая определена следующим образом:

typedef struct {

 

 

uint32_t ClockSpeed;

/* Задает тактовую частоту.

*/

uint32_t DutyCycle;

/* Задает рабочий цикл I²C режима fast mode.

*/

uint32_t OwnAddress1;

/* Задает собственный адрес первого устройства.

*/

uint32_t OwnAddress2;

/* Задает собственный адрес второго устройства,

 

 

если выбран режим двойной адресации.

*/

uint32_t AddressingMode; /* Определяет, выбран 7-разрядный или 10-разрядный

 

 

режим адресации.

*/

uint32_t DualAddressMode;

/* Определяет, выбран ли режим двойной адресации.

*/

uint32_t GeneralCallMode;

/* Определяет, включен ли режим общего вызова.

*/

uint32_t NoStretchMode;

/* Определяет, отключен ли режим удержания

 

 

синхросигнала.

*/

} I2C_InitTypeDef;

 

 

Функции наиболее важных полей данной структуры Си.

ClockSpeed: в этом поле задается скорость интерфейса I²C, и она должна соответ-

ствовать скоростям шины, определенным в спецификациях I²C (режимы standard mode, fast mode и т. д.). Однако точное значение данного поля также зависит от значения DutyCycle, как мы увидим далее. Максимальное значение для этого поля для большинства микроконтроллеров STM32 составляет 400000 (400 кГц), что означает, что микроконтроллеры STM32 поддерживают режимы вплоть до fast mode. Микроконтроллеры STM32F0/F3/F7/L0/L4 составляют исключение из этого правила (см. таблицу 1) и поддерживают также режим fast mode plus (1 МГц). В этих других микроконтроллерах поле ClockSpeed заменяется другим, называемым Timing. Значение конфигурации для поля Timing вычисляется по-другому, и мы не будем его рассматривать здесь. ST предоставляет

I2C

415

специальное руководство по применению (AN423513), в котором объясняется, как вычислить точное значение для этого поля в соответствии с требуемой скоростью шины I²C. Тем не менее, CubeMX может сгенерировать для вас правильное значение конфигурации.

Таблица 2: Характеристики линий SDA и SCL для устройств шины I²C в режимах standard, fast, и fast-mode plus

DutyCycle (рабочий цикл): это поле, которое доступно только в тех микроконтрол-

лерах, которые не поддерживают режим скорости обмена данными fast mode plus, задает соотношение между tLOW и tHIGH линии SCL шины I²C. Может принимать значения I2C_DUTYCYCLE_2 и I2C_DUTYCYCLE_16_9, чтобы указать рабочий цикл, равный 2:1 и 16:9. Выбирая заданный режим синхронизации, мы можем поделить периферийный тактовый сигнал для достижения желаемой тактовой частоты I²C. Чтобы лучше понять роль данного параметра конфигурации, нам нужно рассмотреть некоторые фундаментальные концепции шины I²C. В Главе 11 мы увидели,

что рабочий цикл (или коэффициент заполнения) – это процент от одного периода времени (например, 10 мкс), в течение которого сигнал активен. Для каждой из скоростей шины I²C спецификация I²C точно определяет минимальные значения tLOW и tHIGH. Таблица 2, взятая из UM10204 от NXP14, показывает значения tLOW и tHIGH для конкретной скорости обмена данными (значения выделены желтым цветом в таблице 2). Отношение этих двух значений является рабочим циклом, который не зависит от скорости обмена данными. Например, период 100 кГц соответствует 10 мкс, но tHIGH + tLOW из таблицы 2 составляет менее 10 мкс (4 мкс + 4,7 мкс = 8,7 мкс). Таким образом, соотношение фактических значений может варьироваться, если соблюдаются минимальные значения времени tLOW и tHIGH (4,7 мкс и 4 мкс соответственно). Смысл этих соотношений состоит в том, чтобы проиллюстрировать, что тайминги I²C различны для разных режимов I²C. Это не обязательные соотношения, которые должны соблюдаться периферийными устройствами I²C STM32. Например, tHIGH = 4 мкс и tLOW = 6 мкс составят соотношение, равное 0,67, которое по-прежнему совместимо с таймингами режима standard mode (100 кГц) (поскольку tHIGH = 4 мкс и tLOW > 4.7 мкс, а их сумма равна 10 мкс). Периферийные устройства I²C в микроконтроллерах 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. Когда использовать

13 http://www.st.com/content/ccc/resource/technical/document/application_note/de/14/eb/51/75/e3/49/f8/DM00074956.pdf/files/DM00074956.pdf/jcr:content/translations/en.DM00074956.pdf

14 http://www.nxp.com/documents/user_manual/UM10204.pdf

I2C

416

соотношение 2:1 вместо 16:9 и наоборот? Это зависит от периферийного тактового сигнала (PCLK1). Соотношение 2:1 означает, что 400 МГц достигаются путем деления источника тактового сигнала на 3 (1 + 2). Это означает, что PCLK1 должен быть кратным 1,2 МГц (400 кГц * 3). Использование соотношения 16:9 означает, что мы делим PCLK1 на 25. Это означает, что мы можем получить максимальную частоту шины I²C, когда PCLK1 кратен 10 МГц (400 кГц * 25). Таким образом, правильный выбор рабочих циклов зависит от действующей частоты шины APB1 и требуемой (максимальной) частоты линии SCL I²C. Важно подчеркнуть, что несмотря на то что частота линии SCL ниже 400 кГц (например, используя отношение 16:9 при частоте PCLK1, равной 8 МГц, мы можем достичь максимальной скорости обмена данными, равной 360 кГц), мы все равно удовлетворяем требования спецификации режима I²C fast mode (верхний предел 400 кГц).

OwnAddress1, OwnAddress2: периферийное устройство I²C в микроконтроллерах

STM32 может использоваться для разработки как ведущих, так и ведомых I²C-устройств. При разработке ведомых I²C-устройств в поле OwnAddress1 можно указать адрес ведомого I²C-устройства: периферийное устройство автоматически определяет данный адрес на шине I²C и автоматически запускает все связанные события (например, оно может генерировать соответствующее прерывание, чтобы код микропрограммы мог начать новую транзакцию на шине). Периферийное устройство I²C поддерживает 7- или 10-разрядную адресацию, а также

режим 7-разрядной двойной адресации (7-bit dual addressing mode): в этом случае мы можем указать два отдельных 7-разрядных адреса ведомого устройства, чтобы устройство могло отвечать на запросы, отправленные на оба адреса.

AddressingMode: это поле может принимать значения I2C_ADDRESSINGMODE_7BIT или

I2C_ADDRESSINGMODE_10BIT для указания режима 7- или 10-разрядной адресации со-

ответственно.

DualAddressMode: это поле может принимать значения I2C_DUALADDRESS_ENABLE или I2C_DUALADDRESS_DISABLE для включения/отключения режима 7-разрядной двойной

адресации.

GeneralCallMode: Общий вызов (General Call) – это своего рода широковещательная

адресация в протоколе I²C. Специальный адрес ведомого I²C-устройства – 0x0000 000 – используется для отправки сообщения всем устройствам на одной шине. Общий вызов является необязательной функцией, и, установив в данном поле значение I2C_GENERALCALL_ENABLE, периферийное устройство I²C будет генерировать события при получении адреса общего вызова. Мы не будем рассматривать этот режим в данной книге.

NoStretchMode: это поле, которое может принимать значения I2C_NOSTRETCH_ENABLE

или I2C_NOSTRETCH_DISABLE, используется для отключения/включения необязательного режима удержания синхросигнала (обратите внимание, что, установив его в I2C_NOSTRETCH_ENABLE, вы отключите режим удержания синхросигнала). Для получения дополнительной информации об этом дополнительном режиме I²C см. UM10204 от NXP15 и справочное руководство по вашему микроконтроллеру.

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

HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *hi2c);

15 http://www.nxp.com/documents/user_manual/UM10204.pdf

I2C

417

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

14.2.1.Использование периферийного устройства I²C в

режиме ведущего

Теперь мы собираемся проанализировать основные процедуры, предоставляемые CubeHAL для использования периферийного устройства I²C в режиме ведущего. Для выполнения транзакции по шине I²C в режиме записи 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, рассмотренный ра-

нее, который идентифицирует периферийное устройство I²C;

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),

чтобы оповестить о завершении передачи в режиме чтения. За исключением семейств

I2C

418

STM32F0 и STM32L0, периферийное устройство I²C во всех микроконтроллерах STM32 использует отдельное прерывание для оповещения об ошибках (взгляните на таблицу векторов, связанную с вашим микроконтроллером). По этой причине в соответствующей ISR нам нужно вызвать HAL_I2C_ER_IRQHandler(), который, в свою очередь, вызывает

HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) в случае ошибки. Существует десять раз-

личных обратных вызовов, вызываемых CubeHAL. В таблице 3 перечислены все из них, вместе с ISR, которые вызывают обратный вызов.

Таблица 3: Доступные обратные вызовы CubeHAL при работе периферийного устройства I²C в режиме прерываний или DMA

Обратный вызов

Вызываемая 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()

Оповещает о том, что ведущее

 

 

устройство разместило адрес пери-

 

 

ферийного ведомого устройства на

 

 

шине (периферийное устройство ра-

 

 

ботает в режиме ведомого).

I2C

419

Таблица 3: Доступные обратные вызовы CubeHAL при работе периферийного устройства I²C в режиме прерываний или DMA (продолжение)

Обратный вызов

Вызываемая ISR

Описание

 

 

 

HAL_I2C_ListenCpltCallback()

I2Cx_EV_IRQHandler()

Оповещает о том, что режим про-

 

 

слушивания завершен (это происхо-

 

 

дит, когда выдается STOP-условие и

 

 

периферийное устройство работает

 

 

в режиме ведомого – подробнее об

 

 

этом позже).

HAL_I2C_ErrorCallback()

I2Cx_ER_IRQHandler()

Оповещает о возникновении

 

 

ошибки (периферийное устройство

 

 

работает как в режиме ведущего, так

 

 

и в режиме ведомого).

HAL_I2C_AbortCpltCallback()

I2Cx_ER_IRQHandler()

Оповещает о том, что сработало

 

 

STOP-условие и транзакция I²C была

 

 

прервана (периферийное устройство

 

 

работает как в режиме ведущего, так

 

 

и в режиме ведомого).

Наконец, функции:

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);

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

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

Рисунок 6: Схема выводов EEPROM 24LCxx с корпусом PDIP-8

I2C

420

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

Рисунок 7: Как формируется адрес 24LCxx на шине I²C

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

Прочитайте внимательно

Микроконтроллеры STM32F1 не предоставляют возможность подтягивания линий SDA и SCL. Их GPIO должны быть сконфигурированы как с открытым стоком (open-drain). Таким образом, вы должны добавить два дополнительных резистора для подтяжки линий I²C. Сопротивление между 4 кОм и 10 кОм является достоверным значением.

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

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

I2C

421

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

Рисунок 9: Как выполнить операцию записи в память EEPROM 24LCxx

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

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

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

16 Память EEPROM 24LCxx спроектирована таким образом, что она работает одинаково, даже если мы завершим транзакцию с помощью STOP-условия, а затем немедленно запустим новую в режиме чтения. Такая гибкость позволит нам организовать первый пример этой главы, как мы увидим через некоторое время.

I2C

422

Рисунок 10: Как выполнить операцию произвольного чтения с EEPROM 24LCxx

Наконец мы готовы организовать законченный пример. Создадим две простые функ-

ции с именами Read_From_24LCxx() и Write_To_24LCxx(), которые позволяют записы-

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

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

14int main(void) {

15const char wmsg[] = "We love STM32!";

16char rmsg[20];

17

18HAL_Init();

19Nucleo_BSP_Init();

21 MX_I2C1_Init();

22

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

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

26if(strcmp(wmsg, rmsg) == 0) {

27while(1) {

28HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);

29HAL_Delay(100);

30}

31}

32

33while(1);

34}

35

36/* Функция инициализации I2C1 */

37static void MX_I2C1_Init(void) {

38GPIO_InitTypeDef GPIO_InitStruct;

40/* Разрешение тактирования периферии */

41

__HAL_RCC_I2C1_CLK_ENABLE();

42

 

43hi2c1.Instance = I2C1;

44hi2c1.Init.ClockSpeed = 100000;

45hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;

46hi2c1.Init.OwnAddress1 = 0x0;

47hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;

48hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;

49hi2c1.Init.OwnAddress2 = 0;

50hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;

51hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;

52

53 GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;

I2C

423

54GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;

55GPIO_InitStruct.Pull = GPIO_PULLUP;

56GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;

57GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;

58HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

59

60HAL_I2C_Init(&hi2c1);

61}

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

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

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

63HAL_StatusTypeDef Read_From_24LCxx(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, \

64uint16_t MemAddress, uint8_t *pData, uint16_t len) {

65HAL_StatusTypeDef returnValue;

66uint8_t addr[2];

67

68/* Вычисляем MSB и LSB части адреса памяти */

69addr[0] = (uint8_t) ((MemAddress & 0xFF00) >> 8);

70addr[1] = (uint8_t) (MemAddress & 0xFF);

71

72/* Сначала отправляем адрес ячейки памяти, откуда начинаем считывать данные */

73returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, addr, 2, HAL_MAX_DELAY);

74if(returnValue != HAL_OK)

75return returnValue;

76

77/* Далее мы можем получить данные из EEPROM */

78returnValue = HAL_I2C_Master_Receive(hi2c, DevAddress, pData, len, HAL_MAX_DELAY);

80return returnValue;

81}

83HAL_StatusTypeDef Write_To_24LCxx(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, \

84uint16_t MemAddress, uint8_t *pData, uint16_t len) {

85HAL_StatusTypeDef returnValue;

86uint8_t *data;

I2C

424

88/* Сначала мы выделяем временный буфер для хранения адреса памяти пункта

89* назначения и данных для сохранения */

90data = (uint8_t*)malloc(sizeof(uint8_t)*(len+2));

91

92/* Вычисляем MSB и LSB части адреса памяти */

93data[0] = (uint8_t) ((MemAddress & 0xFF00) >> 8);

94data[1] = (uint8_t) (MemAddress & 0xFF);

95

96/* И копируем содержимое массива pData во временный буфер */

97memcpy(data+2, pData, len);

98

99/* Теперь мы готовы передать буфер по шине I2C */

100returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, data, len + 2, HAL_MAX_DELAY);

101if(returnValue != HAL_OK)

102return returnValue;

103

104 free(data);

105

106/* Ждем, пока EEPROM эффективно сохранит данные в памяти */

107while(HAL_I2C_Master_Transmit(hi2c, DevAddress, 0, 0, HAL_MAX_DELAY) != HAL_OK);

109return HAL_OK;

110}

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

адрес ведомого I²C-устройства памяти EEPROM (DevAddress);

адрес ячейки памяти, с которой начинается сохранение/считывание данных

(MemAddress);

указатель на буфер памяти, используемый для обмена данными с EEPROM

(pData);

объем данных для сохранения/чтения (len);

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

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