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

I2C

425

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

Протокол, используемый EEPROM 24LCxx в действительности является общим для всех устройств I²C, которые имеют адресуемые в памяти регистры чтения/записи. Например, многие датчики I²C, такие как 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);

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

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

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

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

14int main(void) {

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

16char rmsg[20];

17

18HAL_Init();

19Nucleo_BSP_Init();

21

MX_I2C1_Init();

22

 

23

HAL_I2C_Mem_Write(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)wmsg,

24

strlen(wmsg)+1, HAL_MAX_DELAY);

25

while(HAL_I2C_IsDeviceReady(&hi2c1, 0xA0, 1, HAL_MAX_DELAY) != HAL_OK);

26

 

27

HAL_I2C_Mem_Read(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)rmsg,

28

strlen(wmsg)+1, HAL_MAX_DELAY);

HAL_I2C_MemTxCpltCallback()

I2C

426

29

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

31while(1) {

32HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);

33HAL_Delay(100);

34}

35}

36

37while(1);

38}

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

HAL_I2C_MemRxCpltCallback(), как показано в таблице 3.

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

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

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

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-байт из EEPROM 24LCxx. Согласно протоколу I²C, мы должны выполнить следующие операции

(см. рисунок 10):

I2C

427

1.мы должны начать новую транзакцию в режиме записи, выдав START-условие, за которым следует адрес ведомого устройства;

2.затем мы передаем два байта, содержащие MSB и LSB части адреса ячейки памяти;

3.после мы выдаем RESTART-условие и передаем адрес ведомого устройства с последним битом, установленным в 1, чтобы начать транзакцию чтения.

4.ведомое устройство начинает посылать побайтно данные до тех пор, пока мы не завершим транзакцию, выдав NACK или STOP-условие.

Таблица 4: Значения параметра XferOptions для управления генерацией START-/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-условие.

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

I2C

428

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

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

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

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

STM32F0/L0/L4

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

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

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

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

В настоящее время на рынке продается множество модулей типа System-on-Board (SoB или одноплатные системы). Обычно это небольшие печатные платы с уже установленными несколькими ИС, специализирующиеся на выполнении какой-либо конкретной задачи. Модули GPRS и GPS или многодатчиковые платы являются примерами модулей SoB. Эти модули затем припаиваются к основной плате благодаря тому, что на их боковых сторонах открытые паяемые контакты, также известные как «зубчатые отверстия

(castellated vias)» или «зубцы (castellations)». На рисунке 11 показан модуль INEMO-M1

от ST, который представляет собой интегрированный и программируемый модуль с STM32F103 и двумя высокоинтегрированными MEMS датчиками (6-осевой электронный цифровой компас и 3-осевой цифровой гироскоп).

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

I2C

429

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

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

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

Теперь рассмотрим полный пример, который показывает, как разрабатывать приложения с ведомыми I²C-устройствами с использованием CubeHAL. Мы реализуем своего рода цифровой датчик температуры с интерфейсом I²C, похожий на большинство цифровых датчиков температуры, представленных на рынке (например, популярный TMP275 от TI и HT221 от ST). Этот «датчик» будет предоставлять только три регистра:

регистр WHO_AM_I, используемый кодом ведущего устройства для проверки правильности работы интерфейса I²C; этот регистр возвращает фиксированное значение 0xBC.

два связанных с температурой регистра, называемые TEMP_OUT_INT и TEMP_OUT_FRAC, которые содержат целую и дробную часть полученной температуры; например, если измеренное значение температуры равно 27,34°C, то регистр TEMP_OUT_INT будет содержать значение 27, а TEMP_OUT_FRAC – значение 34.

Рисунок 12: Протокол I²C, используемый для чтения внутреннего регистра нашего ведомого устройства

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

I2C

430

В примере представлены реализации как «ведомого», так и «ведущего»: макрос SLAVE_BOARD, определенный на уровне проекта, управляет компиляцией двух частей. Пример требует двух плат Nucleo17.

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

15 volatile uint8_t transferDirection, transferRequested;

16

17#define TEMP_OUT_INT_REGISTER 0x0

18#define TEMP_OUT_FRAC_REGISTER 0x1

19

define WHO_AM_I_REGISTER

0xF

20

define WHO_AM_I_VALUE

0xBC

21

define TRANSFER_DIR_WRITE

0x1

22

define TRANSFER_DIR_READ

0x0

23

define I2C_SLAVE_ADDR

0x33

24

 

 

25int main(void) {

26char uartBuf[20];

27uint8_t i2cBuf[2];

28float ftemp;

29int8_t t_frac, t_int;

31HAL_Init();

32Nucleo_BSP_Init();

34

MX_I2C1_Init();

35

 

36#ifdef SLAVE_BOARD

37uint16_t rawValue;

38uint32_t lastConversion;

40MX_ADC1_Init();

41HAL_ADC_Start(&hadc1);

43while(1) {

44HAL_I2C_EnableListen_IT(&hi2c1);

45while(!transferRequested) {

46if(HAL_GetTick() - lastConversion > 1000L) {

47HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);

49rawValue = HAL_ADC_GetValue(&hadc1);

50ftemp = ((float)rawValue) / 4095 * 3300;

51ftemp = ((ftemp - 760.0) / 2.5) + 25;

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

I2C

431

53t_int = ftemp;

54t_frac = (ftemp - t_int)*100;

56sprintf(uartBuf, "Temperature: %f\r\n", ftemp);

57HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);

59sprintf(uartBuf, "t_int: %d - t_frac: %d\r\n", t_frac, t_int);

60HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);

62lastConversion = HAL_GetTick();

63}

64}

66transferRequested = 0;

68if(transferDirection == TRANSFER_DIR_WRITE) {

69/* Ведущее устройство отправляет адрес регистра */

70HAL_I2C_Slave_Sequential_Receive_IT(&hi2c1, i2cBuf, 1, I2C_FIRST_FRAME);

71while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_LISTEN);

73switch(i2cBuf[0]) {

74case WHO_AM_I_REGISTER:

75i2cBuf[0] = WHO_AM_I_VALUE;

76break;

77case TEMP_OUT_INT_REGISTER:

78i2cBuf[0] = t_int;

79break;

80case TEMP_OUT_FRAC_REGISTER:

81i2cBuf[0] = t_frac;

82break;

83default:

84i2cBuf[0] = 0xFF;

85}

86

87HAL_I2C_Slave_Sequential_Transmit_IT(&hi2c1, i2cBuf, 1, I2C_LAST_FRAME);

88while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

89}

90}

Наиболее значимая часть функции main() начинается со строки 44. Процедура HAL_I2C_EnableListen_IT() разрешает все прерывания, связанные с периферийным устройством I²C. Это означает, что новое прерывание сработает, когда ведущее устройство установит адрес ведомого устройства (который определяется макросом

I2C_SLAVE_ADDR). Процедуры HAL_I2C_EV_IRQHandler() будут автоматически вызывать

функцию HAL_I2C_AddrCallback(), которую мы проанализируем позже.

Затем функция main() начинает выполнять аналого-цифровое преобразование внутреннего датчика температуры каждую секунду с разделением полученной температуры (сохраненной в переменную ftemp) на два 8-разрядных целых числа: t_int и t_frac. Они представляют собой целую и дробную части температуры. Функция main() временно

I2C

432

останавливает аналого-цифровое преобразование, как только переменная transferRequested становится равной 1: эта глобальная переменная устанавливается

функцией HAL_I2C_AddrCallback() вместе с переменной transferDirection, которая содер-

жит направление передачи (чтение/запись) транзакции I²C.

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

Данное действие выполняется в строке 87, где вызывается функция HAL_I2C_Slave_Sequential_Transmit_IT(): функция инвертирует направление передачи и отправляет ведущему устройству содержимое требуемого регистра. Сложная конструкция представлена

встроке 88. Здесь мы простаиваем до тех пор, пока состояние периферийного устройства I²C не станет равным HAL_I2C_STATE_READY. Почему мы не проверяем состояние периферийного устройства на соответствие состоянию HAL_I2C_STATE_LISTEN, как мы это делали

встроке 71? Чтобы понять этот аспект, нам нужно запомнить важную особенность комбинированных транзакций. Когда транзакция инвертирует направление передачи, ведущее устройство начинает подтверждать каждый отправленный байт. Помните, что только ведущее устройство знает, как долго длится транзакция, и только оно решает, когда остановить транзакцию. В комбинированных транзакциях ведущее устройство завершает передачу от ведомого к ведущему, выполняя NACK, что заставляет ведомое устройство выполнить STOP-условие. С точки зрения периферийного устройства I²C STOP-условие заставляет периферийное устройство выйти из режима прослушивания (технически говоря, оно генерирует условие прерывания, и если вы реализуете обратный вызов HAL_I2C_AbortCpltCallback(), то сможете отслеживать, когда это происходит), и по этой причине нам нужно проверять состояние HAL_I2C_STATE_READY и снова переводить периферийное устройство в режим прослушивания (listen mode) в строке 44.

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

92#else // Плата ведущего устройства

93i2cBuf[0] = WHO_AM_I_REGISTER;

94HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf,

95

1, I2C_FIRST_FRAME);

96

while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

97

 

98

HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf,

99

1, I2C_LAST_FRAME);

100

while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

101

 

102sprintf(uartBuf, "WHO AM I: %x\r\n", i2cBuf[0]);

103HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);

105i2cBuf[0] = TEMP_OUT_INT_REGISTER;

106HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf,

107

1, I2C_FIRST_FRAME);

108

while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

 

I2C

433

109

 

 

110

HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_int,

 

111

1, I2C_LAST_FRAME);

 

112

while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

 

113

 

 

114i2cBuf[0] = TEMP_OUT_FRAC_REGISTER;

115HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf,

116

1, I2C_FIRST_FRAME);

117

while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

118

 

119

HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_frac,

120

1, I2C_LAST_FRAME);

121

while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

122

 

123ftemp = ((float)t_frac)/100.0;

124ftemp += (float)t_int;

125

126sprintf(uartBuf, "Temperature: %f\r\n", ftemp);

127HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);

129#endif

131while (1);

132}

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

«Часть ведущего устройства» примера начинается со строки 92. Код довольно прост.

Здесь мы используем функцию HAL_I2C_Master_Sequential_Transmit_IT() для запуска

комбинированной транзакции и HAL_I2C_Master_Sequential_Receive_IT() для получения содержимого требуемого регистра от ведомого устройства. Затем целая и дробная части температуры снова объединяются в число типа float, и полученная температура отправляется по UART2.

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

134void I2C1_EV_IRQHandler(void) {

135HAL_I2C_EV_IRQHandler(&hi2c1);

136}

137

138void I2C1_ER_IRQHandler(void) {

139HAL_I2C_ER_IRQHandler(&hi2c1);

140}

141

142void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t \

143AddrMatchCode) {

144UNUSED(AddrMatchCode);

145