Stm32 интерфейс i2c описание продолжение. Шина I2C и применение её в МК STM32. Что и где покупалось

В одном из своих проектов использую микроконтроллеры STM32F030. Недавно возникла необходимость подключения внешней EEPROM памяти по шине I 2 C. Сначала я хотел взять готовый пример с инета, но в итоге пришлось изобретать свой велосипед писать свой код. В статье рассказываю о типичных граблях при работе с шиной I 2 C STM32F030, и предлагаю свой велосипед вариант работы с шиной.

Итак, для критики возьму один из примеров кода, взятый с интернета:

/** Описание Записывает байт данных в I2C EEPROM. * Параметр data: переменная для записи в EEPROM. * Параметр WriteAddr: Внутренний адрес EEPROM для записи. * Возвращаемое значение нет */ uint32_t EEPROM_I2C_Write(uint8_t data, uint16_t WriteAddr) { //uint32_t DataNum = 0; Address = Address + (WriteAddr / 256); /* Конфигурирование адреса ведомого; количество байтов, которые будут запрограммированы (переданы); перезагрузки и генерировать старт */ I2C_TransferHandling(I2C1, Address, 1, I2C_Reload_Mode, I2C_Generate_Start_Write); /* Подождите, пока TXIS флаг не будет установлен */ while(I2C_GetFlagStatus(I2C1, I2C_ISR_TXIS) == RESET); /* Отправить адрес памяти */ I2C_SendData(I2C1, (uint8_t)WriteAddr); /* Подождите, пока TCR флаг не будет установлен */ while(I2C_GetFlagStatus(I2C1, I2C_ISR_TCR) == RESET); /* Обновить CR2: установить Адрес ведомого, установить запрос на запись, генерировать Пуск и заданного конечного режим */ I2C_TransferHandling(I2C1, Address, 1, I2C_AutoEnd_Mode, I2C_No_StartStop); /* Подождите, пока TXIS флаг не будет установлен */ while(I2C_GetFlagStatus(I2C1, I2C_ISR_TXIS) == RESET); /* Запись данных в TXDR */ I2C_SendData(I2C1, data); /* Подождите, пока STOPF флаг не будет установлен */ while(I2C_GetFlagStatus(I2C1, I2C_ISR_STOPF) == RESET); /* Очистить флаг STOPF */ I2C_ClearFlag(I2C1, I2C_ICR_STOPCF); }

Вставляю этот код в свой проект, проверяю, шина работает. Дальше начинаю проверять код на наличие граблей. Для начала отключаю микросхему памяти, чтобы при обращении на шине отсутствовал ACK. Проверяю работу кода, и тут же натыкаюсь на грабли. Давайте разбираться где подвох.

На шину выдан старт, отправлен адрес устройства. Так как мы отключили микросхему памяти, установился флаг NACKF (Not Acknowledge Flag). Смотрим код дальше.

А вот здесь микроконтроллер зависает, так как ждёт запрос на передачу байта (установку флага TXIS, Transmit Interrupt Status). Запрос никогда не поступит, так как ведомое устройство на шине не отвечает. Это первые грабли. Соответственно, пока никаких сбоев на шине нет - наше устройство работает нормально. Как только произошёл малейший сбой - микроконтроллер наглухо виснет. Смотрю код дальше.

Здесь тоже имеется ошибка. Если микросхема не отвечает, устанавливается флаг NACKF, а флаг TCR (Transfer Complete Reload) никогда не будет возведён. Микроконтроллер зависнет.

Последняя строчка ожидает возведения флага STOPF (Stop detection Flag), но мы замкнули ножки и заблокировали обмен данными. Шина замечает подвох, и взлетает флаг ARLO (Arbitration Lost). Флаг STOPF не устанавливается, микроконтроллер зависает. Более того, появляются ещё одни грабли.

Так как возведён флаг ARLO, обмен данными по шине невозможен, микроконтроллер не будет выдавать даже старт на шину.

Таким образом, применив вышеописанный код, мы закладываем в проект пачку хаотично появляющихся граблей. В общем я решил изобрести велосипед написать свой вариант кода. Весь код расписывать не буду (можно скачать в конце статьи), лишь поясню ключевые моменты.

Отправка данных.

/* Выполняет транзакцию записи Size байт в регистр Register по адресу Adress. Параметры: Adress - адрес ведомого устройства Register - регистр, в который хотим передать данные Data - указывает откуда брать данные для передачи Size - сколько байт хотим передать (от 1 до 254) Возвращает: 1 - если данные успешно переданы 0 - если произошла ошибка */ u8 I2C_Write_Transaction (u8 Adress, u8 Register, u8 *Data, u8 Size) { u8 Count=0; // Счётчик успешно переданных байт // Старт I2C_Start_Direction_Adress_Size (I2C_Transmitter, Adress, 1+Size); // Сейчас либо I2C запросит первый байт для отправки, // Либо взлетит NACK-флаг, говорящий о том, что микросхема не отвечает. // Если взлетит NACK-флаг, отправку прекращаем. while ((((I2C_BUS->ISR & I2C_ISR_TXIS)==0) && ((I2C_BUS->ISR & I2C_ISR_NACKF)==0)) && (I2C_BUS->ISR & I2C_ISR_BUSY)) {}; if (I2C_BUS->ISR & I2C_ISR_TXIS) I2C_BUS->TXDR=Register; // Отправляю адрес регистра // Отправляем байты до тех пор, пока не взлетит TC-флаг. // Если взлетит NACK-флаг, отправку прекращаем. while ((((I2C_BUS->ISR & I2C_ISR_TC)==0) && ((I2C_BUS->ISR & I2C_ISR_NACKF)==0)) && (I2C_BUS->ISR & I2C_ISR_BUSY)) { if (I2C_BUS->ISR & I2C_ISR_TXIS) I2C_BUS->TXDR=*(Data+Count++); // Отправляю данные } I2C_Stop(); if (Count == Size) return 1; return 0; }

После старта шины и отправки адреса микросхемы, есть 3 варианта исхода событий:

  • байт успешно отправлен, если в очереди есть ещё один байт - возводится TXIS, если все байты переданы - возводится TC (Transfer Complete)
  • микросхема не отвечает - возводится NACKF
  • прочие ошибки на шине - возводится ARLO или BERR (Bus Error), опускается BUSY (Bus Busy)

Следует обратить внимание на циклы while - они реализованы с учётом всех вышеописанных вариантов. Идём дальше.

Приём данных.

/* Выполняет транзакцию чтения Size байт из регистра Register по адресу Adress. Параметры: Adress - адрес ведомого устройства Register - регистр, из которого хотим принять данные Data - указывает куда складывать принятые данные Size - сколько байт хотим принять (от 1 до 255) Возвращает: 1 - если данные успешно приняты 0 - если произошла ошибка */ u8 I2C_Read_Transaction (u8 Adress, u8 Register, u8 *Data, u8 Size) { u8 Count=0; // Счётчик успешно принятых байт // Старт I2C_Start_Direction_Adress_Size (I2C_Transmitter, Adress, 1); // Сейчас либо I2C запросит первый байт для отправки, // Либо взлетит NACK-флаг, говорящий о том, что микросхема не отвечает. // Если взлетит NACK-флаг, отправку прекращаем. while ((((I2C_BUS->ISR & I2C_ISR_TC)==0) && ((I2C_BUS->ISR & I2C_ISR_NACKF)==0)) && (I2C_BUS->ISR & I2C_ISR_BUSY)) { if (I2C_BUS->ISR & I2C_ISR_TXIS) I2C_BUS->TXDR = Register; // Отправляю адрес регистра } // Повторный старт I2C_Start_Direction_Adress_Size (I2C_Receiver, Adress, Size); // Принимаем байты до тех пор, пока не взлетит TC-флаг. // Если взлетит NACK-флаг, приём прекращаем. while ((((I2C_BUS->ISR & I2C_ISR_TC)==0) && ((I2C_BUS->ISR & I2C_ISR_NACKF)==0)) && (I2C_BUS->ISR & I2C_ISR_BUSY)) { if (I2C_BUS->ISR & I2C_ISR_RXNE) *(Data+Count++) = I2C_BUS->RXDR; // Принимаю данные } I2C_Stop(); if (Count == Size) return 1; return 0; }

Здесь всё почти так же, как и при передаче. Подробно описывать не буду, идём дальше.

Шина I2C существует уже достаточно давно: ее в 1980х создала компания Philips для низкоскоростных устройств. В настоящий момент она достаточно широко применяется, и, скорей всего, дома у вас есть хоть одно устройство с данной шиной. Название шины расшифровывается как Inter-Integrated Circuit. Хардварным модулем I2C в настоящее время обладает большинство микроконтроллеров, в некоторых их и вовсе несколько, как у тех же STM32 (в серии F4 есть целых три модуля I2C).

Шина представляет собой 2 линии, одна из которых данные (SDA), другая - синхросигнал (SCL), обе линии изначально притянуты к питанию. Следует отметить, что четкого указания какое именно должно быть напряжение нет, но чаще всего используется +5В и +3.3В. Устройства в линии не одноранговые, как в CAN, поэтому всегда должно быть Master-устройство. Допускается наличие нескольких Master-устройств, но это все же гораздо реже, чем один Master и ворох Slave устройств.

Передача данных инициируется мастером, который отправляет в шину адрес необходимого устройства, тактирование осуществляется так же мастером. Но, при этом, Slave-устройство имеет возможность «придержать» линию тактирования, как бы сообщая Master-устройству, что не успевает принять или отправить данные, что порой бывает очень полезно. Наибольшее распространение получили в текущий вариант реализации I2C с частотой шины 100 kHz (Standard mode) и 400 kHz (Fast mode).

Существует реализация I2C версии 2.0, которая позволяет достичь гораздо больших скоростей, в 2-3 Мбит/с, но они пока что весьма редкие. Так же у линии есть ограничение по емкости в 400 пФ. Обычно в даташитах для датчиков и прочих I2C устройств указывается их емкость, так что приблизительно можно вычислить «влезет» ли еще один датчик или нет.

В микроконтроллерах очень часто есть внутренняя подтяжка на выводах, что в свободном состоянии даст необходимые +3.3В (или +5В) на линии, но этой подтяжки абсолютно не хватит на нормальную линию. Поэтому всегда стоит делать внешнюю подтяжку и SCL и SDA к питанию резисторами в 4.7кОм..2кОм.

Отдельно стоит отметить то, что обычно линию I2C не рекомендуют делать длинной, да и чаще всего она встречается на печатных платах для обмена между некими цифровыми устройствами, гораздо реже I2C пускают по проводам (но не стоит думать, что это редкость, и то и другое вполне нормально). Если у вас возникла надобность сделать длинную линию I2C, да еще на 400 кГц, то стоит уменьшить сопротивление резисторов подтяжки. 1 кОм - вполне приемлемое значение для линии длиной чуть более метра и с несколькими устройствами на ней. Только не забывайте, что уменьшая сопротивление резисторов, вы увеличиваете ток в линии, что при переизбытке может привести к повреждению устройств.

С программной точки зрения обмен по шине I2C выглядит следующим образом: Master отправляет стартовую последовательность START (при высоком уровне SCL к нулю притягивается SDA), затем отправляет адрес с бит-флагом, указывающим режим чтения или записи, причем в следующим формате:

Если бит режима равен нулю, то это значит, что Master будет записывать информацию в Slave устройство, единица - чтение из Slave. Если взглянуть на это с другой стороны, то каждое I2C устройство предоставляет два «виртуальных» устройства, исходя из чего получается, что если весь байт адреса (т.е. исконные 7 бит + бит режима) четный, то это адрес записи, если нечетный - адрес чтения. Исходя из этого появляется ограничение на количество устройств в шине: 127.

После получения адреса Slave устройство должно сообщить мастеру о принятии адреса, что подтвердит сам факт существования Slave устройства с таким адресом на линии. Подтверждение - это специальный 9й бит, который равен нулю, если адрес совпал и готовы работать, и единице, если не совпал. Это сигналы ACK и NACK соответственно. Так же, ACK используется при последующим приеме и передаче данных. Если мастер записывает в слейв, то слейв должен каждый байт подтверждать сигналом ACK. Если слейв отправляет данные мастеру, то мастер должен на все байты отвечать ACK, кроме последнего - это будет сигналом, что больше отправлять данные не требуется.

В конце всей передачи Master должен отправить завершающую последовательность STOP, которая заключается в поднятии линии SDA до высокого уровня при поднятой линии SCL.

Таким образом, стандартный «пакет» выглядит следующим образом:

Теперь можно перейти к рассмотрению работы с данной шиной на микроконтроллере STM32. Сразу стоит заметить, что во всех сериях данный модуль приблизительно одинаковый, за исключением регистра фильтра в старших сериях (например, STM32F407), поэтому единожды написанный код сможет работать и далее.

Для начала следует включить тактирование модуля I2C, что, впрочем, необходимо и для всей периферии. Так же необходимо включить и настроить пины в режим альтернативной функции. Чтобы посмотреть на какой шине что находится, необходимо обратиться к даташиту, в раздел Device Overview (для F4, это, например, страница 18) (рис.1). Из изображения видно, что I2C находятся на шине APB1. Следующий шаг - включение и настройка GPIO, все, что необходимо: режим альтернативной функции (по даташиту I2C относится к AF4), тип OpenDrain, а подтяжка должна быть внешняя. «Скорость» пинов для 100кГц можно выбрать Low (2 MHz), а для 400 кГц ST рекомендуют выбирать уже Medium или Fast (от 10 MHz). И, наконец, можно настроить I2C. Показывать регистры не имеет смысла, они есть все в reference manual, все, что нужно для стандартного случая будет ниже. До включения непосредственно модуля I2C следует в регистр CR2 записать текущее значение частоты той шины, на которой сидит модуль I2C, в данном случае это частота шины APB1. В рамках даташита это значение называется PCLK.

В разных контроллерах количество и именование шин разное. Так, в серии F4xx есть и APB1 и APB2, и переменная PCLK будет соответственно нумероваться - PCLK1 и PCLK2. Чтобы посмотреть или высчитать конкретную частоту тактирования шины можно воспользоваться приложением CubeMX, которое загружается с официального сайта ST Microelectronics.

Рис. 1 - Схема периферии контроллера STM32F407.

В регистре CR2 так же включаются прерывания от данного модуля. Под этим понимается то, что будет ли модуль сообщать в NVIC о том, что что-то произошло, либо же просто поставит нужные флаги в статусном регистре. Стоит заметить, что в статусном регистре всегда будут ставиться событийные флаги, что логично. В первую очередь интересны прерывания ITEVTEN и ITERREN, прерывания событий и ошибок соответственно. Можно обойтись вполне и только событиями, как наиболее общим.

I2C1->CR2 |= 48; // Peripheral frequency 24MHz I2C1->CR2 |= I2C_CR2_ITEVTEN; // Enable events

Регистр CCR отвечает за тактирование самой шины наружу, поэтому сюда необходимо внести значение, которое рассчитывается по формуле PCLK/I2C_SPEED. Например, мы хотим шину на 400 кГц завести, внутренняя шина APB1 тактируется 48 МГц, соответственно в CCR запишем значение, равное 48*106/4*105 = 120. Так же в данном регистре необходимо указать режим работы Slow/Fast, это последний, 16й бит.

I2C1->CCR &= ~I2C_CCR_CCR; I2C1->CCR |= 120; I2C1->CCR |= I2C_CCR_FS; // FastMode, 400 kHz

Регистр TRISE отвечает за фронты сигналов на SDA и SCL, сюда необходимо внести значение с небольшим запасом. Можно и без запаса, главное не меньше - ничего не заработает. Вносимое значение рассчитывается так: TRISE = RISE/tPCLK. tPCLK = 1/PCLK. Константа RISE - это максимальное время нарастания сигнала, по спецификации это 1000 нс для Slow Mode и 300 нс для Fast mode. tPCLK - это просто период, получается стандартно по формуле 1/F. Так как у нас Fast Mode, то значение в TRISE необходимо следующее: 3.000*10-7/2.083*10-8 = 14.4, и т.к. необходим запас, то округляем в большую сторону, т.е. 15.

Данный показатель важен, но не настолько, как сбившаяся частота тактирования. Я по ошибке посчитал константу TRISE в Fast Mode по формуле для Slow Mode и все работает. Но все же лучше делать правильно, по спецификации шины. Найти ее можно по поисковой фразе “i2c specification”. Да-да, она на английском языке.

I2C1->TRISE = 24;

После того, как данные действия будут выполнены, можно включать модуль и прерывания в модуле NVIC (если нужны).

I2C1->CR1 |= I2C_CR1_PE; // Enable I2C block NVIC_EnableIRQ(I2C1_EV_IRQn); NVIC_SetPriority(I2C1_EV_IRQn, 1);

После настройки модуля, есть два варианта как вы будете с ним работать. Вариант первый - поллингом, т.е. вы будете ждать появления флага в цикле while (и контроллер будет занят только этим, что в большинстве случаев плохо), либо вариант второй - на прерываниях. Я рассмотрю второй вариант, так как он предпочтительный, а если понять логику состояний по прерываниям, то перейти на поллинг не является проблемой.

Итак, первое что необходимо сделать - добавить в код обработчик прерываний событий от модуля I2C. Функция должна называться определенным образом, и ее название можно взять из startup-файла. Для модуля I2C1 функция называется I2C1_EV_IRQHandler. Поэтому в необходимый.c файл добавляем такую функцию:

Void I2C1_EV_IRQHandler(void) { }

Именование данного метода, понятное дело, можно изменять. В startup-файле в ассемблерном коде просто создаются метки, которые не требуют обязательного наличия данной функции. Компилятор языка Си найдя в исходном файле функцию, например ту же I2C1_EV_IRQHandler, выделить под нее точно такое же имя. Когда уже будет происходить окончательная сборка, все это сведется воедино и вместо имени будет присутствовать переход в нужную позицию в коде. Поэтому можно изменять название метки как заблагорассудится (по правилам именования функций, конечно), хотя и не рекомендуется - другие разработчики могут просто не понять, обработчик прерываний это или нет.

Если вы пишете на языке С++, не забудьте «обернуть» обработчик прерываний в блок extern “C” { … }, так как компилятор С++ изменяет имя функции по своим правилам во время компиляции (туда вносится информация о параметрах и возвращаемом значении, например), поэтому сборщик потом не свяжет написанный обработчик и метку в startup файле.

Для написания обработчика прерываний можно обратиться напрямую к документации, reference manual, там есть достаточно подробные схемы для разных режимов работы. Для начала возьмем отправку slave-устройству данных:

Например, мы хотим просто отправить 1 байт данных устройству и прекратить передачу. Для этого нам потребуется только состояния EV5, EV6 и EV8. Где-то в коде программы у нас была глобальная переменная data типа uint8_t, которую мы проинициализировали каким-то значением и хотим передать slave устройству. Инициацию передачи, как мы уже знаем, делает последовательность START:

I2C1->CR1 |= I2C_CR1_START;

Данную строку можно поставить по ходу программы там, когда нужно начинать передачу. Например, после того, как инициализировали переменную data. Дальше уже будет код внутри функции-обработчика прерываний. Для начала необходимо в отдельные переменные сохранить значение статусов:

Volatile uint32_t sr1 = I2C1->SR1, sr2 = I2C1->SR2;

После отправки стартовой последовательности произойдет прерывание с событием EV5. В данном случае в статусном регистре должен выставиться бит SB. Если данный бит выставлен, нам необходимо отправить адрес с битом режима чтения или записи. Для упрощения можно сделать так:

<<1) | mode)

Теперь можно написать обработчик состояния EV5:

If(sr1 & I2C_SR1_SB) { I2C1->DR = I2C_ADDRESS(0x14,I2C_MODE_READ); }

Когда адрес отправится и slave-устройство ответит последовательностью ACK, то произойдет событие EV6 и одновременно EV8: установится флаг ADDR и TXE. А рамках Master-режима, ADDR означает, что адрес отправлен и воспринят slave-устройством, а TXE означает, что буфер свободен для внесения данных для последующей передачи. Флаг ADDR сбросится сам, как только мы прочитаем SR1 и SR2 (необходимо их оба прочитать), а флаг TXE обработаем отдельным блоком кода. Так что, по факту, обрабатывать необходимо только EV5 и EV8, EV6 только информирует о наличии нужного slave на линии. В обработчике TXE все, что нужно - это передавать данные. Так как передавать мы хотим только 1 байт, то сразу же отправим и последовательность STOP:

If(sr1 & I2C_SR1_TXE) { I2C1->DR = data; I2C1->CR1 |= I2C_CR1_STOP; }

Таким образом, заполнив переменную data и дав команду формирования последовательности START, вся работа будет идти в прерываниях, а контроллер тем временем будет занят другой полезной работой большую часть времени (т.е. другая работа кроме, собственно, обработчика прерываний).

Если данных требуется отправить больше 1 байта, то изменения в коде минимальны. Теперь вместо uint8_t data создадим такие глобальные переменные:

Uint8_t iter; uint8_t data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Так как глобальные переменные целого типа по умолчанию равны нулю, то явно инициализировать переменную iter не требуется. Изменения в обработчике же вообще минимальные - требуется переписать блок обработки события EV8:

If(sr1 & I2C_SR1_TXE) { if(iter < 10) { I2C1->DR = data; } else { I2C1->CR1 |= I2C_CR1_STOP; } }

Таким образом, мы получили вот такую функцию-обработчик:

#define I2C_MODE_READ 1 #define I2C_MODE_WRITE 0 #define I2C_ADDRESS(addr, mode) ((addr<<1) | mode) uint8_t iter; uint8_t data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; void I2C1_EV_IRQHandler(void) { volatile uint32_t sr1 = module ->SR1, sr2 = module ->SR2; if(sr1 & I2C_SR1_SB) { module ->DR = I2C_ADDRESS(0x14,I2C_MODE_READ); } if(sr1 & I2C_SR1_TXE) { if(iter < 10) { I2C1->DR = data; } else { I2C1->CR1 |= I2C_CR1_STOP; } } }

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

Void I2C_handler(I2C_TypeDef* module, uint8_t addr, uint8_t data) { volatile uint32_t sr1 = module ->SR1, sr2 = module ->SR2; if(sr1 & I2C_SR1_SB) { module ->DR = I2C_ADDRESS(addr,I2C_MODE_READ); } if(sr1 & I2C_SR1_TXE) { module ->DR = data; module ->CR1 |= I2C_CR1_STOP; } }

Это позволит одну и ту же функцию использовать сразу в двух модулях. Например, мы можем ее вставить вот так:

Void I2C1_EV_IRQHandler(void) { I2C_handler(I2C1, 0x14, 0x10); } void I2C1_EV_IRQHandler(void) { I2C_handler(I2C1, 0x27, 0xFF); }

Еще раз напомню, что адрес устройства, который мы видим в документации - это биты из того байта, который передается модулем, а бит 0 - это режим. Так, указав выше в аргументах адрес 0x14 и режим передачи данных, я получу на передачу байт 0x29. Так как в макросе проверки нет, стоит не забывать, что передавать в него можно максимум адрес 0x7F, иначе получите чехарду.

Для режима чтения все похоже, как можно видеть из диаграммы:

Для обработки нам нужны состояния EV5, EV6, EV7, EV7_1. Статус EV6 по-прежнему сбросится сам после чтения регистров SR1 и SR2, а статус EV7_1 соответствует последнему необходимому байту. Т.е. когда мы приняли предпоследний байт, мы должны отключить отправку сообщения ACK слейву, чтобы следующий байт уже был последним. Итак, возьмем наш предыдущий код и просто внесем в него дополнительный обработчик такого вида, чтобы принять 10 байт данных:

If(sr1 & I2C_SR1_RXNE) { if(rx_iter == 8) { I2C1->CR1 &= ~I2C_CR1_ACK; } else if (rx_iter == 9) { I2C1-> < 10) { rx_data = I2C1->DR; } }

При этом должны быть глобальные переменные:

Uint8_t rx_iter; uint8_t rx_data;

Таким образом, получили вот такой обработчик прерываний для модуля I2C1:

#define I2C_MODE_READ 1 #define I2C_MODE_WRITE 0 #define I2C_ADDRESS(addr, mode) ((addr<<1) | mode) uint8_t iter; uint8_t data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; uint8_t rx_iter; uint8_t rx_data; uint8_t i2c_mode; void I2C1_EV_IRQHandler(void) { volatile uint32_t sr1 = I2C1->SR1, sr2 = I2C1->SR2; if(sr1 & I2C_SR1_SB) { I2C1->DR = I2C_ADDRESS(0x14,i2c_mode); } if(sr1 & I2C_SR1_TXE) { if(iter < 10) { I2C1->DR = data; } else { I2C1->CR1 |= I2C_CR1_STOP; } } if(sr1 & I2C_SR1_RXNE) { if(rx_iter == 8) { I2C1->CR1 &= ~I2C_CR1_ACK; } else if (rx_iter == 9) { I2C1->CR1 |= I2C_CR1_ACK; } if(rx_iter < 10) { rx_data = I2C1->DR; } }

Данных выше хватает, в общем-то, для очень многих случаев, и, поняв логику написания кода выше, можно по документации будет расширить код необходимым. И еще одно - часто для того, чтобы что-то прочитать из slave-устройства, необходимо в него что-то записать.

Post Views: 318

В последнее время все чаще натыкаюсь на негативные отзывы о шине I2C у STM32 , мол работа с ней это танцы с бубном и тд.
За последний месяц мне удалось запустить две микросхемы, работающие по I2C и ни каких танцев, только вдумчивое чтение даташита.

Модуль I2C у STM32 обладает следующими особенностями:

  • может работать в двух режимах Fm (fast mode) и Sm (standart mode), первый работает на частотах до 400KHz, второй до 100KHz
  • буфер размером 1 байт с поддержкой DMA
  • поддерживает аппаратный подсчет контрольной суммы
  • на его основе возможна реализуется SMBus (System Management Bus) и PMBus (Power Management Bus)
  • два вектора прерывания, генерируются при успешной передаче и при возникновении ошибки
  • фильтр для борьбы с шумами
  • может работать в режиме Master или Slave
В режиме Master:
  • генерирует тактирующий сигнал
  • генерирует START и STOP
В режиме Slave:
  • можно программировать основной и альтернативный адрес на который он будет отзываться
  • определяет STOP

По умолчанию модуль находится в режиме Slave , но он автоматически переключается в режим Master после генерации состояния START .
Принципиальное отличие между Master и Slave, в том, что Master генерирует тактовый сигнал и всегда инициирует передачу данных и заканчивает её . Slave же, откликается на свой адрес и широковещательный , при чем отклик на широковещательный адрес можно отключить. Также Slave генерирует состояние ACK , но его тоже можно отключить.

Такое подробное разъяснение необходимо потому, что в обоих режимах устройство может выступать как передатчиком, так и приемником.

  • Slave transmitter
  • Slave receiver
  • Master transmitter
  • Master receiver

Ниже показана структура модуля I2C.

Регистр управления I2C_CR1:

SWRST (Software reset) - единица в этом бите сбрасывает значение всех регистров модуля в дефолтное стояние, может использоваться для сброса при возникновении ошибки.

ALERT (SMBus alert) - установка единицы в этот бит разрешает генерировать сигнал alert в режиме SMBus .

PEC (Packet error checking) - управление этим битом производится программно, но он может быть сброшен аппаратно когда передается PEC, START, STOP или PE=0. Единица в этом бите разрешает передачу CRC .

POS (Acknowledge/PEC Position (for data reception)) - состояние этого бита определяет положение ACK /PEC в двух байтовой конфигурации в режиме Master.

ACK (Acknowledge enable) - единица в этом бите разрешает отправлять ACK /NACK после приема байта адреса или данных.

STOP (Stop generation) - установка единицы в этот бит генерирует сигнал STOP в режиме Master.

START (Start generation) - установка единицы в этот бит генерирует состояние START в режиме Master,

NOSTRETCH (Clock stretching disable (Slave mode)) - если на обработку данных требуется время Slave может остановить передачу мастера, прижав линию SCL к земле, Master будет ждать и не будет ни чего слать, пока линия не будет отпущена. Ноль в этом бите прижимает SCL к земле.

ENGC (General call enable) - если в этом бите установлена единица, модуль отвечает ACK ом на широковещательный адрес 0х00.

ENPEC (PEC enable) - установка единицы в этот бит включает аппаратный подсчет CRC .

ENARP (ARP enable) - установка единицы в этот бит включает ARP .

SMBTYPE (SMBus type) - если в этом бите установлен ноль модуль работает в режиме Slave, если единица в режиме Master.

SMBUS (SMBus mode) - если в этом бите установлен ноль модуль работает в режиме I2C , если единица SMBus .

PE (Peripheral enable) - единица в этом бите включает модуль.

Регистр управления I2C_CR2:

LAST (DMA last transfer) - единица в этом бите разрешает DMA генерировать сигнал окончания передачи EOT (End of Transfer).

DMAEN (DMA requests enable) - единица в этом бите разрешает делать запрос к DMA при установке флагов TxE или RxNE .

ITBUFEN (Buffer interrupt enable) - если этот бит сброшен, разрешены все прерывания, кроме прерываний по приему и передаче.

ITEVTEN (Event interrupt enable) - единица в этом бите разрешает прерывания по событию.

ITERREN (Error interrupt enable) - единица в этом бите разрешает прерывания при возникновении ошибок.

FREQ (Peripheral clock frequency) - в это битовое битовое поле необходимо записать частоту тактирования модуля, она может принимать значение от 2 до 50.

Регистр I2C_OAR1:

ADDMODE (Addressing mode) - этот бит определяет размер адреса Slave, ноль соответствует размеру адреса 7 бит, единица - 10 бит.

ADD (Interface address) - старшие биты адреса, в случае если адрес 10-битный.

ADD (Interface address) - адрес устройства.

ADD0 (Interface address) - младший бит адреса, в случае если адрес 10-битный..

Регистр I2C_OAR2:

ADD2 - альтернативный адрес на который будет отзываться Slave.

ENDUAL (Dual addressing mode enable) - единица в этом бите разрешает Slave отзываться на альтернативный адрес в 7-битном режиме.

I2C_DR - регистр данных, для отправки данных пишем в регистр DR , для приёма читаем его же.

Регистр статуса I2C_SR1:

SMBALERT (SMBus alert) - возникает в случае alert в шине SMBus .

TIMEOUT (Timeout or Tlow error) - возникает если линия SCL прижата к земле. Для master 10mS, для slave 25mS.

PECERR (PEC Error in reception) - возникает при ошибке PEC при приеме.

OVR (Overrun/Underrun) - возникает при переполнении данных.

AF (Acknowledge failure) - устанавливается при получении сигнала NACK . Для сброса нужно записать 0.

ARLO (Arbitration lost (master mode)) - устанавливается при потере арбитража. Для сброса нужно записать 0.

BERR (Bus error) - ошибка шины. Устанавливается в случае возникновения сигнала START или STOP в неправильный момент.

TxE (Data register empty (transmitters)) - устанавливается при опустошении регистра DR, а точнее когда данные из него были перемещены в сдвиговый регистр.

RxNE (Data register not empty (receivers)) - устанавливается при приеме байта данных, кроме адреса.

STOPF (Stop detection (slave mode)) - при работе в режиме slave устанавливается при обнаружении сигнала STOP , если перед этим был сигнал ACK. Для сброса необходимо прочитать SR1 и произвести запись в CR1 .

ADD10 (10-bit header sent (Master mode)) - устанавливается при отправке первого байта 10-битного адреса.

BTF (Byte transfer finished) - флаг устанавливается по окончании приема/передачи байта, работает только при NOSTRETCH равном нулю.

ADDR (Address sent (master mode)/matched (slave mode)) - в режиме master устанавливается после передачи адреса, в режиме slave устанавливается при совпадении адреса. Для сброса нужно прочитать регистр SR1, а затем SR2.

SB (Start bit (Master mode)) - устанавливается при возникновении сигнала START. Для сброса флага необходимо прочитать SR1 и записать данные в регистр DR .

Регистр статуса I2C_SR2:

PEC (Packet error checking register) - в это битовое поле записывается контрольная сумма кадра.

DUALF (Dual flag (Slave mode)) - ноль в этом бите говорит о том, что адрес который принял Slave соответствует OAR1 , иначе OAR2 .

SMBHOST (SMBus host header (Slave mode)) - устанавливается, когда принят заголовок SMBus Host .

SMBDEFAULT (SMBus device default address (Slave mode)) - устанавливается, если принят адрес по умолчанию
для SMBus -устройства.

GENCALL (General call address (Slave mode)) - устанавливается, если принят широковещательный адрес в режиме ведомого.

TRA (Transmitter/receiver) - единица в этом бите говорит о том, что модуль работает как передатчик, иначе приемник.

BUSY (Bus busy) - флаг занятости.

MSL (Master/slave) - единица в этом бите говорит о том, что модуль работает в режиме Master, иначе Slave.

Регистр управления частотой I2C_CCR:

F/S (I2C master mode selection) - при установке единицы в этот бит модуль работает в режиме FAST , иначе STANDART .

DUTY (Fm mode duty cycle) - этот бит задает скважность сигнала SCL в режиме FAST . Если установлен ноль tlow/thigh = 2, иначе tlow/thigh = 16/9.

CCR (Clock control register in Fm/Sm mode (Master mode)) - при работе в режиме Master задает тактовую частоту линии SCL.

Sm mode or SMBus :
Thigh = CCR * TPCLK1
Tlow = CCR * TPCLK1

Fm mode :
If DUTY = 0:
Thigh = CCR * TPCLK1
Tlow = 2 * CCR * TPCLK1

If DUTY = 1: (to reach 400 kHz)
Thigh = 9 * CCR * TPCLK1
Tlow = 16 * CCR * TPCLK1

Получаем для режима SM следующее:
CCR * TPCLK1 + CCR * TPCLK1 = 10 000ns
CCR = 10 000/(2* TPCLK1)

Регистр I2C_TRISE:

TRISE - определяет время нарастания фронта. Рассчитывается по формуле (Tr max/TPCLK1)+1 ,
где Tr max для SM составляет 1000nS , а для FM 300nS ,
а TPCLK1 - период который рассчитывается как 1/F (APB1).

Регистр управления фильтрами I2C_FLTR:

ANOFF (Analog noise filter OFF) - ноль в этом бите включает аналоговый фильтр.

DNF (Digital noise filter) - битовое поле для настройки цифрового фильтра. За подробностями нужно обратиться к документации.

Инициализация модуля из рабочего проекта.
void I2C2_Init(void) { /* SDL -> PB10 SDA -> PB11 RST -> PE15 */ //включаем тактирование портов и модуля I2C RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOEEN; RCC->APB1ENR |= RCC_APB1ENR_I2C2EN; //альтернативная ф-ция, выход с открытым стоком, 2 MHz GPIOB->AFR |= (0x04<<2*4); GPIOB->AFR |= (0x04<<3*4); GPIOB->MODER |= GPIO_MODER_MODER10_1; GPIOB->OTYPER |= GPIO_OTYPER_OT_10; GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR10; GPIOB->MODER |= GPIO_MODER_MODER11_1; GPIOB->OTYPER |= GPIO_OTYPER_OT_11; GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR11; //PE15 двухтактный выход 50MHz GPIOE->MODER |= GPIO_MODER_MODER15_0; GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR15; AU_RST_HIGH //настраиваем модуль в режим I2C I2C2->CR1 &= ~2C_CR1_SMBUS; //указываем частоту тактирования модуля I2C2->CR2 &= ~I2C_CR2_FREQ; I2C2->CR2 |= 42; // Fclk1=168/4=42MHz //конфигурируем I2C, standart mode, 100 KHz duty cycle 1/2 I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_DUTY); //задаем частоту работы модуля SCL по формуле 10 000nS/(2* TPCLK1) I2C2->CCR |= 208; //10 000ns/48ns = 208 //Standart_Mode = 1000nS, Fast_Mode = 300nS, 1/42MHz = 24nS I2C2->TRISE = 42; //(1000nS/24nS)+1 //включаем модуль I2C2->CR1 |= I2C_CR1_PE; } void I2C_Write(uint8_t reg_addr, uint8_t data) { //стартуем I2C2->CR1 |= I2C_CR1_START; while(!(I2C2->SR1 & I2C_SR1_SB)){}; (void) I2C2->SR1; //передаем адрес устройства I2C2->DR = I2C_ADDRESS(ADDR,I2C_MODE_WRITE); while(!(I2C2->SR1 & I2C_SR1_ADDR)){}; (void) I2C2->SR1; (void) I2C2->SR2; //передаем адрес регистра I2C2->DR = reg_addr; while(!(I2C2->SR1 & I2C_SR1_TXE)){}; //пишем данные I2C2->DR = data; while(!(I2C2->SR1 & I2C_SR1_BTF)){}; I2C2->CR1 |= I2C_CR1_STOP; } uint8_t I2C_Read(uint8_t reg_addr) { uint8_t data; //стартуем I2C2->CR1 |= I2C_CR1_START; while(!(I2C2->SR1 & I2C_SR1_SB)){}; (void) I2C2->SR1; //передаем адрес устройства I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_WRITE); while(!(I2C2->SR1 & I2C_SR1_ADDR)){}; (void) I2C2->SR1; (void) I2C2->SR2; //передаем адрес регистра I2C2->DR = reg_addr; while(!(I2C2->SR1 & I2C_SR1_TXE)){}; I2C2->CR1 |= I2C_CR1_STOP; //рестарт!!! I2C2->CR1 |= I2C_CR1_START; while(!(I2C2->SR1 & I2C_SR1_SB)){}; (void) I2C2->SR1; //передаем адрес устройства, но теперь для чтения I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_READ); while(!(I2C2->SR1 & I2C_SR1_ADDR)){}; (void) I2C2->SR1; (void) I2C2->SR2; //читаем I2C2->CR1 &= ~I2C_CR1_ACK; while(!(I2C2->SR1 & I2C_SR1_RXNE)){}; data = I2C2->DR; I2C2->CR1 |= I2C_CR1_STOP; return data; }

Кто-то любит пирожки, а кто-то - нет.

Интерфейс i2c широко распространён и используется. В stm32f4 модулей, реализующих данный протокол, аж целых три штуки.
Естественно, с полной поддержкой всего этого дела.

Работа с модулем, в целом, такая же, как и в других контроллерах: даёшь ему команды, он их выполняет и отчитывается о результате:
Я> Шли START.
S> Ок, послал.
Я> Круто, шли адрес теперь. Вот такой: 0xXX.
S> Ок, послал. Мне сказали, что ACK. Давай дальше.
Я> Жив ещё, хорошо. Вот тебе номер регистра: 0xYY, - шли.
S> Послал, получил ACK.
Я> Шли ему теперь данные, вот тебе байт: 0xZZ.
S> Послал, он согласен на большее: ACK.
Я> Фиг ему, а не ещё. Шли STOP.
S> Okay.

И всё примерно в таком духе.

В данном контроллере выводы i2c раскиданы по портам таким образом:
PB6: I2C1_SCL
PB7: I2C1_SDA

PB8: I2C1_SCL
PB9: I2C1_SDA

PB10: I2C2_SCL
PB11: I2C2_SDA

PA8: I2C3_SCL
PC9: I2C3_SDA
Вообще, распиновку периферии удобно смотреть в на 59 странице.

Что удивительно, но для работы с i2c нужны все его регистры, благо их немного:
I2C_CR1 - команды модулю для отправки команд/состояний и выбор режимов работы;
I2C_CR2 - настройка DMA и указание рабочей частоты модуля (2-42 МГц);
I2C_OAR1 - настройка адреса устройства (для slave), размер адреса (7 или 10 бит);
I2C_OAR2 - настройка адреса устройства (если адресов два);
I2C_DR - регистр данных;
I2C_SR1 - регистр состояния модуля;
I2C_SR2 - регистр статуса (slave, должен читаться, если установлен флаги ADDR или STOPF в SR1);
I2C_CCR - настройка скорости интерфейса;
I2C_TRISE - настройка таймингов фронтов.

Впрочем, половина из них типа «записать и забыть».

На плате STM32F4-Discovery уже есть I2C устройство, с коим можно попрактиковаться: CS43L22 , аудиоЦАП. Он подключён к выводам PB6/PB9. Главное, не забыть подать высокий уровень на вывод PD4 (там сидит ~RESET), иначе ЦАП не станет отвечать.

Порядок настройки примерно таков:
1 . Разрешить тактирование портов и самого модуля.
Нам нужны выводы PB6/PB9, потому надо установить бит 1 (GPIOBEN) в регистре RCC_AHB1ENR, чтоб порт завёлся.
И установить бит 21 (I2C1EN) в регистре RCC_APB1ENR, чтоб включить модуль I2C. Для второго и третьего модуля номера битов 22 и 23 соответственно.
2 . Дальше настраиваются выводы: выход Oped Drain (GPIO->OTYPER), режим альтернативной функции (GPIO->MODER), и номер альтренативной функции (GPIO->AFR).
По желанию можно настроить подтяжку (GPIO->PUPDR), если её нет на плате (а подтяжка к питанию обеих линий необходима в любом виде). Номер для I2C всегда один и тот же: 4. Приятно, что для каждого типа периферии заведён отдельный номер.
3 . Указывается текущая частота тактирования периферии Fpclk1 (выраженная в МГц) в регистре CR2. Я так понял, это нужно для расчёта разных таймингов протокола.
Кстати, она должна быть не менее двух для обычного режима и не менее четырёх для быстрого. А если нужна полная скорость в 400 кГц, то она ещё и должна делиться на 10 (10, 20, 30, 40 МГц).
Максимально разрешённая частота тактирования: 42 МГц.
4 . Настраивается скорость интерфейса в регистре CCR, выбирается режим (обычный/быстрый).
Cмысл таков: Tsck = CCR * 2 * Tpckl1, т.е. период SCK пропорционален CCR (для быстрого режима всё несколько хитрее, но в RM расписано).
5 . Настраивается максимальное время нарастания фронта в регистре TRISE. Для стандартного режима это время 1 мкс. В регистр надо записать количество тактов шины, укладывающихся в это время, плюс один:
если такт Tpclk1 длится 125 нс, то записываем (1000 нс / 125 нс) + 1 = 8 + 1 = 9.
6 . По желанию разрешается генерация сигналов прерывания (ошибки, состояние и данных);
7 . Модуль включается: флаг PE в регистре CR1 переводится в 1.

Дальше модуль работает уже как надо. Надо только реализовать правильный порядок команд и проверки результатов. Например, запись регистра:
1 . Сначала нужно отправить START, установив флаг с таким именем в регистре CR1. Если всё ок, то спустя некоторое время выставится флаг SB в регистре SR1.
Хочу заметить один момент, - если нет подтяжки на линии (и они в 0), то этот флаг можно не дождаться вовсе.
2 . Если флаг-таки дождались, то отправляем адрес. Для семибитного адреса просто записываем его в DR прям в таком виде, как он будет на линии (7 бит адреса + бит направления). Для десятибитного более сложный алгоритм.
Если устройство ответит на адрес ACK"ом, то в регистре SR1 появится флаг ADDR. Если нет, то флаг AF (Acknowledge failure).
Если ADDR появился, надо прочитать регистр SR2. Можно ничего там и не смотреть, просто последовательное чтение SR1 и SR2 сбрасывает этот флаг. А пока флаг установлен, SCL удерживается мастером в низком состоянии, что полезно, если надо попросить удалённое устройство подождать с отправкой данных.
Если всё ок, то дальше модуль перейдёт в режим приёма или передачи данных в зависимости от младшего бита отправленного адреса. Для записи он должен быть нулём, для чтения - единицей.
но мы рассматриваем запись, потому примем, что там был ноль.
3 . Дальше отправляем адрес регистра, который нас интересует. Точно так же, записав его в DR. После передачи выставится флаг TXE (буфер передачи пуст) и BTF (передача завершена).
4 . Дальше идут данные, которые можно отправлять, пока устройство отвечает ACK. Если ответом будет NACK, то эти флаги не установятся.
5 . По завершении передачи (или в случае непредвиденного состояния) отправляем STOP: устанавливается одноимённый флаг в регистре CR1.

При чтении всё то же самое. Меняется только после записи адреса регистра.
Вместо записи данных идёт повторная отправка START (повторный старт) и отправка адреса с установленным младшим битом (признак чтения).
Модуль будет ждать данных от устройства. Чтобы поощрать его к отправке следующих байт, надо перед приёмом установить флаг ACK в CR1 (чтобы после приёма модуль посылал этот самый ACK).
Как надоест, флаг снимаем, устройство увидит NACK и замолчит. После чего шлём STOP обычным порядком и радуемся принятым данным.

Вот то же самое в виде кода:
// Инициализация модуля void i2c_Init(void) { uint32_t Clock = 16000000UL; // Частота тактирования модуля (system_stm32f4xx.c не используется) uint32_t Speed = 100000UL; // 100 кГц // Включить тактирование порта GPIOB RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; // Настроим выводы PB6, PB9 // Open drain! GPIOB->OTYPER |= GPIO_OTYPER_OT_6 | GPIO_OTYPER_OT_9; // Подтяжка внешняя, потому тут не настраивается! // если надо, см. регистр GPIOB->PUPDR // Номер альтернативной функции GPIOB->AFR &= ~(0x0FUL << (6 * 4)); // 6 очистим GPIOB->AFR |= (0x04UL << (6 * 4)); // В 6 запишем 4 GPIOB->AFR &= ~(0x0FUL << ((9 - 8) * 4)); // 9 очистим GPIOB->AFR |= (0x04UL << ((9 - 8) * 4)); // В 9 запишем 4 // Режим: альтернативная функция GPIOB->MODER &= ~((0x03UL << (6 * 2)) | (0x03UL << (9 * 2))); // 6, 9 очистим GPIOB->MODER |= ((0x02UL << (6 * 2)) | (0x02UL << (9 * 2))); // В 6, 9 запишем 2 // Включить тактирование модуля I2C1 RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; // На данный момент I2C должен быть выключен // Сбросим всё (SWRST == 1, сброс) I2C1->CR1 = I2C_CR1_SWRST; // PE == 0, это главное I2C1->CR1 = 0; // Считаем, что запущены от RC (16 МГц) // Предделителей в системе тактирования нет (все 1) // По-хорошему, надо бы вычислять это вс из // реальной частоты тактирования модуля I2C1->CR2 = Clock / 1000000UL; // 16 МГц // Настраиваем частоту { // Tclk = (1 / Fperiph); // Thigh = Tclk * CCR; // Tlow = Thigh; // Fi2c = 1 / CCR * 2; // CCR = Fperiph / (Fi2c * 2); uint16_t Value = (uint16_t)(Clock / (Speed * 2)); // Минимальное значение: 4 if(Value < 4) Value = 4; I2C1->CCR = Value; } // Задаём предельное время фронта // В стандартном режиме это время 1000 нс // Просто прибавляем к частоте, выраженной в МГц единицу (см. RM стр. 604). I2C1->TRISE = (Clock / 1000000UL) + 1; // Включим модуль I2C1->CR1 |= (I2C_CR1_PE); // Теперь можно что-нибудь делать } // Отправить байт bool i2c_SendByte(uint8_t Address, uint8_t Register, uint8_t Data) { if(!i2c_SendStart()) return false; // Адрес микросхемы if(!i2c_SendAddress(Address)) return i2c_SendStop(); // Адрес регистра if(!i2c_SendData(Register)) return i2c_SendStop(); // Данные if(!i2c_SendData(Data)) return i2c_SendStop(); // Стоп! i2c_SendStop(); return true; } // Получить байт bool i2c_ReceiveByte(uint8_t Address, uint8_t Register, uint8_t * Data) { if(!i2c_SendStart()) return false; // Адрес микросхемы if(!i2c_SendAddress(Address)) return i2c_SendStop(); // Адрес регистра if(!i2c_SendData(Register)) return i2c_SendStop(); // Повторный старт if(!i2c_SendStart()) return false; // Адрес микросхемы (чтение) if(!i2c_SendAddress(Address | 1)) return i2c_SendStop(); // Получим байт if(!i2c_ReceiveData(Data)) return i2c_SendStop(); // Стоп! i2c_SendStop(); return true; } Использование: { uint8_t ID = 0; i2c_Init(); // Считаем, что PD4 выставлен в высокий уровень и ЦАП работает (это надо сделать как-нибудь) // Отправка байта в устройство с адресом 0x94, в регистр 0x00 со значением 0x00. i2c_SendByte(0x94, 0x00, 0x00); // Приём байта из устройства с адресом 0x94 из регистра 0x01 (ID) в переменную buffer i2c_ReceiveByte(0x94, 0x01, &ID); }
Конечно, кроме как в учебном примере так делать нельзя. Ожидание окончания действия слишком уж долгое для такого быстрого контроллера.

Опубліковано 26.10.2016

В предыдущей статье мы рассмотрели работу STM32 с шиной I 2 C в качестве Мастера. То есть, он был ведущий и опрашивал датчик. Теперь сделаем так, чтобы STM32 был Slave-ом и отвечал на запросы, то есть сам работал как датчик. Мы выделим 255 байт памяти под регистры с адресами от 0 до 0xFF, и позволим Мастеру в них писать/читать. А чтобы пример был не таким простым, сделаем из нашего STM32, еще и аналого-цифровой преобразователь с интерфейсом I 2 C. ADC будет обрабатывать 8 каналов. Результаты преобразований контроллер будет отдавать Мастеру при чтении из регистров. Поскольку результат преобразования ADC занимает 12 бит, нам потребуется 2 регистра (2 байта) на каждый канал ADC.

i2c_slave.h содержит настройки:

I2CSLAVE_ADDR – адрес нашего устройства;

ADC_ADDR_START – начальный адрес регистров, которые отвечают за результаты преобразований ADC.

В файле i2c_slave.c нас больше всего интересуют функции get_i2c1_ram и set_i2c1_ram . Функция get_i2c1_ram отвечает за считывание данных из регистров. Она возвращает данные с указанного адреса, которые отдаются Мастеру. В нашем случае данные считываются из массива i2c1_ram , но, если Мастер спрашивает адреса регистров из диапазона отведенного для результатов ADC, то отправляются данные преобразований ADC.

get_i2c1_ram :

Uint8_t get_i2c1_ram(uint8_t adr) { //ADC data if ((ADC_ADDR_START <= adr) & (adr < ADC_ADDR_START + ADC_CHANNELS*2)) { return ADCBuffer; } else { // Other addresses return i2c1_ram; } }

Функция set_i2c1_ram – записывает данные принятые от Мастера в регистры с указанным адресом. В нашем случае данные просто записываются в массив i2c1_ram . Но это не обязательно. Вы можете, например, добавить проверку, и, когда на определенный адрес приходит определенное число, выполнить какие-то действия. Таким образом, Вы сможете подавать микроконтроллеру разные команды.

set_i2c1_ram :

Void set_i2c1_ram(uint8_t adr, uint8_t val) { i2c1_ram = val; return; }

Инициализация достаточно проста:

Int main(void) { SetSysClockTo72(); ADC_DMA_init(); I2C1_Slave_init(); while(1) { } }

Сначала мы устанавливаем максимальную частоту работы контроллера. Максимальная скорость необходима, когда нужно избежать любых задержек на шине I 2 C. Затем запускаем работу ADC с использованием DMA. О . О . И, наконец, выполняем инициализацию шины I 2 C как Slave . Как видите, ничего сложного.

Теперь подключим наш модуль STM32 к Raspberry Pi. К каналам ADC подключим потенциометры. И будем считывать с нашего контроллера показатели ADC. Не забываем, что для работы шины I 2 C нужно на каждую линию шины установить подтягивающие резисторы.

В консоли Raspberry проверим видно ли наше устройство на шине I 2 C (о том, ):

I2cdetect -y 1

Как видите, адрес устройства 0x27 , хотя мы указали 0x4E. Когда будет время, подумайте – почему так произошло.

Для считывания из регистров I 2 C-Slave устройства выполняем команду:

I2cget -y 1 0x27 0x00

Где:
0x27 – адрес устройства,
0x00 – адрес регистра (0x00…0xFF).

Для записи в регистры I 2 C-Slave устройства выполняем команду:

I2cset -y 1 0x27 0xA0 0xDD

Де:
0x27 – адрес устройства,
0xA0 – адрес регистра
0xDD -8-bit данные (0x00…0xFF)

Предыдущая команда записала число 0xDD в регистр 0xA0 (писать в первые 16 регистров можно, и смысла нет, по они отведены под ADC). Теперь прочитаем:

I2cget -y 1 0x27 0xA0

Чтобы упростить процесс считывания данных ADC-каналов я написал скрипт:

#!/usr/bin/env python import smbus import time bus = smbus.SMBus(1) address = 0x27 while (1): ADC = {}; for i in range(0, 8): LBS = bus.read_byte_data(address, 0x00+i*2) MBS = bus.read_byte_data(address, 0x00+i*2+1) ADC[i] = MBS*256 + LBS print ADC time.sleep(0.2)

Он опрашивает и выводит в консоль результаты всех 8-ми ADC-каналов.

Аналогичным образом можно объединить несколько микроконтроллеров. Один из них должен быть Master (), другие Slave.

Желаю успехов!



Просмотров