В предыдущей части нашего урока мы познакомились с аппаратной организацией шины 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 с микросхемой памяти (2-5 шт)
Модуль RTC DS3231 с микросхемой памяти (1 шт) — так дороже
Семисегментный чертырехразрядный индикатор красный (с общим анодом или катодом на выбор) 10 шт
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК (нажмите на картинку)
Привет!
Отличный разбор библиотек! Спасибо Вам!
Добавьте способ обхода ошибки аналоговых фильтров из Errata на сотую серию п.2.13.7
Приветствую! Огромное спасибо за ваши уроки! Очень помогают.
Касательно ошибок аналогового фильтра есть решение. Собрал несколько проектов для тестирования ноутбучных батарей. Сперва на ардуино, потом на STM32 + HAL, сейчас заканчиваю на STM32 + LL.
STM32 + HAL тоже глючит i2c. Там я этот вопрос решил просто. Если HAL_ERROR, то MX_I2C2_Init() и помогало.
Решил следующий проект сделать на LL и те же грабли. Но!
Во-первых, нужно было избавится от блокирующего while, чтобы отловить проблему. По быстрому сделал таймер на HAL_GetTick (), задавал лимит времени от 100мс до 500мс и отловил где именно происходит глюк. Шина в какой то момент не может отправить старт. Нога SCL все время прижата к нулю. Глубококопание привело к Errata. Сделал все по рекомендации, но шина отвисала не сразу, а через минуту-две. Уже хотел сделать принудительно в цикле повторять процедуру перезапуска, и проверять не отвисла ли.
Потом на каком то форуме прочитал, что якобы регистры не любят когда бесконечно проверяют их состояние, не знаю факт или нет, но режил вместо таймера сделать тупо счетчик. И получилось! Шина больше не виснет. Двое суток непрерывного чтения ноутбучной батареи и не одного зависания!
Таперь решение:
Вместо например while(!LL_I2C_IsActiveFlag_SB(I2C2)){};
пишем
number_of_read_attempts = 1000;
do {
status = LL_I2C_IsActiveFlag_SB(I2C2);
if( ! number_of_read_attempts )
{
return I2C_START_ERROR;
}
number_of_read_attempts —;
} while (!status);
и так в каждой проверке статуса какого либо флага. Из экспериментов выяснил, что некоторым флагам достаточно number_of_read_attempts = 100, а некоторым number_of_read_attempts = 300. Так что number_of_read_attempts=1000 это чтоб уж наверняка.
Возможно, когда процессор занят другим делом, например перебирает счетчик, флаг спокойно устанавливается.
Вобщем как то так.
При глубококопании нарыл вот еще что.
При использовании библиотеки HAL, в процедуре HAL_I2C_Init происходит вот что:
/*Reset I2C*/
hi2c->Instance->CR1 |= I2C_CR1_SWRST;
hi2c->Instance->CR1 &= ~I2C_CR1_SWRST;
А LL так не делает. Поэтому я добавил в MX_I2C2_Init следующий код:
LL_I2C_EnableReset(I2C2);
LL_I2C_DisableReset(I2C2);
или можно так:
LL_I2C_EnableReset(I2C2);
while(!LL_I2C_IsResetEnabled(I2C2)){};
LL_I2C_DisableReset(I2C2);
while(LL_I2C_IsResetEnabled(I2C2)){};
Или можно завернуть это в функцию, скажем LL_I2C2_Prepare().
Вобщем после этого у меня начали читаться батареи Lenovo, которые иногда не читались