В предыдущей части нашего урока мы познакомились с аппаратной организацией шины I2C в микроконтроллере STM32F1, в том числе подробно изучили регистры модуля I2C.
Теперь, прежде чем приступить к созданию и настройке проекта нашего занятия, давайте познакомимся со схемой.
Основная схема была взята из урока 148 по динамической индикации. Только ещё мы к данной схеме подключим модуль на часовой микросхеме DS1307 с необходимой нам микросхемой AT24C32, подведя от нашей макетной платы к нему общий провод, провод питания и провода контактов SDA и SCL, которые, забегая немного вперёд, будут соответствовать ножкам портов PB7 и PB6
Для лучшего мониторинга ситуации подключим также логический анализатор к ножкам шины
Проект мы создадим из проекта урока 148 с именем LL_LED_DYN и назовём его LL_I2C_EEPROM.
Откроем наш проект в Cube MX и включим модуль I2C1
Настройки оставляем все по умолчанию (режим Standard и частота 100000).
Ножки также оставляем по умолчанию
И, самое главное, не забываем нашу шину настроить к работе именно с библиотекой LL
Сгенерируем проект, откроем его в Keil, настроим программатор на autoreset, отключим оптимизацию, подключим к проекту файл библиотеки led.c.
Попробуем собрать проект, подключить схему и прошить контроллер. Если индикатор нормально отсчитывает наши циклы, то продолжим дальше.
Займёмся изучением того, как проходит инициализация шины I2C. Для этого перейдём в тело функции и увидим, что там сначала объявляется переменная типа структуры LL_I2C_InitTypeDef. Также создаётся и переменная типа структуры LL_GPIO_InitTypeDef для настройки ножек портов, задействованных в шине.
Далее ножки портов настраиваются для работы в альтернативном режиме и включается их тактирование.
Затем с помощью функции LL_I2C_DisableOwnAddress2 отключается режим двойной адресации путём сброса соответствующего бита в регистре OAR2
1 |
CLEAR_BIT(I2Cx->OAR2, I2C_OAR2_ENDUAL); |
Потом с помощью функции LL_I2C_DisableGeneralCall отключается работа с широковещательными запросами
1 |
CLEAR_BIT(I2Cx->CR1, I2C_CR1_ENGC); |
Далее при помощи функции LL_I2C_EnableClockStretching отключается увеличение времени в режиме SLAVE
1 |
CLEAR_BIT(I2Cx->CR1, I2C_CR1_NOSTRETCH); |
Затем заполняются поля структуры I2C_InitStruct и вызывается функция LL_I2C_Init, в тело которой мы теперь и перейдём.
Там сначала проверяется заполненность структуры, а потом с помощью функции LL_I2C_Disable отключается модуль I2C
1 |
CLEAR_BIT(I2Cx->CR1, I2C_CR1_PE); |
Далее с помощью функции LL_RCC_GetSystemClocksFreq заполняется структура rcc_clocks значениями различных частот тактирования
1 |
LL_RCC_GetSystemClocksFreq(&rcc_clocks); |
Потом при помощи функции LL_I2C_ConfigSpeed происходит настройка скорости работы шины. Во втором входном параметре данной функции используется поле вышезаполненной структуры, несущее в себе значение частоты PCLK1, которое является частотой тактирования шины APB1, на которой и находится наш модуль I2C1.
Перейдём в данную функцию и увидим, что здесь сначала при помощи функции __LL_I2C_FREQ_HZ_TO_MHZ частота PCLK1 переводится из герц в мегагерцы
1 |
freqrange = __LL_I2C_FREQ_HZ_TO_MHZ(PeriphClock); |
Затем в битовое поле I2C_CR2_FREQ регистра CR2 заносится значение данной частоты
1 |
MODIFY_REG(I2Cx->CR2, I2C_CR2_FREQ, freqrange); |
Далее в битовое поле TRISE одноименного регистра TRISE заносится значение времени нарастания фронта, которое сначала вычисляется при помощи макроса __LL_I2C_RISE_TIME, в параметрах которого передаются наша частота тактирования в мегагерцах, а также частота работы шины
1 |
MODIFY_REG(I2Cx->TRISE, I2C_TRISE_TRISE, __LL_I2C_RISE_TIME(freqrange, ClockSpeed)); |
Затем, если скорость работы шины установлена в значение больше 100 килогерц, то переменная clockconfig, впоследствии используемая для занесения значений в определенные биты регистра CCR, инициализируется следующим образом
1 2 3 |
clockconfig = LL_I2C_CLOCK_SPEED_FAST_MODE | __LL_I2C_SPEED_FAST_TO_CCR(PeriphClock, ClockSpeed, DutyCycle) | DutyCycle; |
А если будет использована стандартная скорость работы шины или менее, то данная переменная инициализируется уже вот так
1 2 |
clockconfig = LL_I2C_CLOCK_SPEED_STANDARD_MODE | __LL_I2C_SPEED_STANDARD_TO_CCR(PeriphClock, ClockSpeed); |
И далее значениями данной переменной заполняются соответствующие биты и поля регистра CCR
1 |
MODIFY_REG(I2Cx->CCR, (I2C_CCR_FS | I2C_CCR_DUTY | I2C_CCR_CCR), clockconfig); |
Возвращаемся в функцию LL_I2C_Init, где затем у нас происходит заполнение адреса при помощи функции LL_I2C_SetOwnAddress1
1 |
MODIFY_REG(I2Cx->OAR1, I2C_OAR1_ADD0 | I2C_OAR1_ADD1_7 | I2C_OAR1_ADD8_9 | I2C_OAR1_ADDMODE, OwnAddress1 | OwnAddrSize); |
Так как у нас шина работает в режиме MASTER, то адрес у нас равен 0, то есть вообще может быть любой, от него ничего не зависит, ибо он используется только в режиме SLAVE.
Затем с помощью функции LL_I2C_SetMode устанавливается режим работы шины (I2C или SMBus) посредством занесения значений в соответствующие биты регистра CR1 (в нашем случае они все обнуляются)
1 |
MODIFY_REG(I2Cx->CR1, I2C_CR1_SMBUS | I2C_CR1_SMBTYPE | I2C_CR1_ENARP, PeripheralMode); |
Далее при помощи функции LL_I2C_Enable наш модуль включается посредством установки бита PE в регистре CR1
1 |
SET_BIT(I2Cx->CR1, I2C_CR1_PE); |
Затем с помощью функции LL_I2C_AcknowledgeNextData устанавливается бит ACK в регистре CR1, который разрешает модулю генерировать условие ACK после приёма байтов
1 |
MODIFY_REG(I2Cx->CR1, I2C_CR1_ACK, TypeAcknowledge); |
Возвращаемся в функцию MX_I2C1_Init, где затем мы при помощи LL_I2C_SetOwnAddress2 устанавливаем значение второго адреса в 0 путём заполнения в регистре OAR2 соответствующего поля нулями
1 |
MODIFY_REG(I2Cx->OAR2, I2C_OAR2_ADD2, OwnAddress2); |
Вот и вся инициализация.
Теперь наша дальнейшая задача – написать код, который сможет общаться с микросхемой внешней памяти EEPROM по шине I2C, а именно записывать и читать данные по определённым адресам памяти.
Для этого мы объявим в файле main.c два глобальных массива. Один из них будет для хранения считанных из внешнего EEPROM значений, а другой будет состоять из заранее заданных 8-битных чисел для записи в микросхему
1 2 3 4 5 6 |
extern uint16_t num_gl; uint8_t rd_value[20] = {0}; uint8_t wr_value[20] = {0x14,0x13,0x12,0x11,0x10, 0x0F,0x0E,0x0D,0x0C,0x0B, 0x0A,0x09,0x08,0x07,0x06, 0x05,0x04,0x03,0x02,0x01}; |
Добавим также некоторые макросы для работы с шиной, а именно адрес SLAVE микросхемы, и переменные для инициализации бита записи/чтения в адресе
1 2 3 4 |
#define ALL_SEG_OFF() LL_GPIO_SetOutputPin(SEG_PORT,SH|SG|SF|SE|SD|SC|SB|SA); #define I2C_REQUEST_WRITE 0x00 #define I2C_REQUEST_READ 0x01 #define SLAVE_OWN_ADDRESS 0xA0 |
Почему именно A0, а не AE, как мы задавали в случае использования модуля DS3231? А потому что адресные ножки A0:A2 у нас в модуле, который мы используем сейчас, притянуты к земле, а следовательно биты в адресе будут равны 0
Поэтому смотрите внимательно, какие у вас уровни на ножках 1-3 микросхемы. Лучше всего померить вольтметром.
Далее добавим функцию, которая будет записывать определённое количество байтов в память микросхемы из массива, начиная с определённого адреса памяти
1 2 3 4 5 6 |
/* USER CODE BEGIN 0 */ //------------------------------------------------ void AT24C_WriteBytes (uint16_t addr,uint8_t *buf, uint16_t bytes_count) { } //------------------------------------------------ |
Объявим в нашей функции локальную переменную
1 2 3 |
void AT24C_WriteBytes (uint16_t addr,uint8_t *buf, uint16_t bytes_count) { uint16_t i; |
Сбросим бит POS в регистре CR1 с помощью специально-обученной этому фукнции
1 2 3 |
uint16_t i; //Disable Pos LL_I2C_DisableBitPOS(I2C1); |
Включим генерирование условия ACK
1 2 |
LL_I2C_DisableBitPOS(I2C1); LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_ACK); |
Сгенерируем условие START
1 2 |
LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_ACK); LL_I2C_GenerateStartCondition(I2C1); |
Дождёмся установки бита SB в регистре SR1 также при помощью специальной функции. Данный бит устанавливается, как мы уже знаем, в случае наличие на шине условия START
1 2 |
LL_I2C_GenerateStartCondition(I2C1); while(!LL_I2C_IsActiveFlag_SB(I2C1)){}; |
Считаем регистр SR1
1 2 3 |
while(!LL_I2C_IsActiveFlag_SB(I2C1)){}; //read state (void) I2C1->SR1; |
Давайте пока убедимся, что мы вообще как-то общаемся с нашей шиной I2C. Для этого сразу отправим условие STOP в шину
1 2 |
(void) I2C1->SR1; LL_I2C_GenerateStopCondition(I2C1); |
Вызовем нашу функцию в функции main()
1 2 |
LL_TIM_EnableCounter(TIM2); AT24C_WriteBytes (0x004A, wr_value, 20); |
Писать и читать мы будем 20 байтов с адреса 4A.
Правда, пока мы ничего ещё не пишем и не читаем.
Соберём код, прошьём контроллер и посмотрим результат работы кода в программе логического анализа, настроенной на работу с шиной I2C
Мы видим, что у нас прекрасно сформировались на шине наши условия START и STOP.
В следующей части нашего урока мы напишем функции записи и чтения данных по шине I2C и проверим наши знания на практике, также попробуем увеличить скорость обмена данными по I2C.
Предыдущая часть Программирование МК STM32 Следующая часть
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Модуль RTC DS3231 с микросхемой памяти (3 шт)
Модуль RTC DS3231 с микросхемой памяти (1 шт) – так дороже
Семисегментный чертырехразрядный индикатор красный с общим анодом 10 шт
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК (нажмите на картинку)
Привет!
Отличный разбор библиотек! Спасибо Вам!
Добавьте способ обхода ошибки аналоговых фильтров из Errata на сотую серию п.2.13.7