Сегодня мы попробуем с помощью библиотеки CMSIS поработать с шиной I2C. В качестве подопытного устройства мы возьмём микросхему EEPROM — AT24C32, которая установлена в модуле с часовой микросхемой DS3231 и также в часовом модуле с микросхемой DS1307. С данной микросхемой мы уже работали, когда изучали программирование шины I2C для контроллеров AVR. Там эта шина называлась TWI. Причём микросхему мы использовали с часовым модулем на микросхеме DS1307. Данный модуль мы будем использовать и сегодня. Также в уроке 150 мы работали с данной микросхемой, причём мы подробно изучили аппаратную организацию модуля I2C в контроллере STM32F1, все регистры, биты и битовые поля. Также узнали, как вычисляются коэффициенты для настройки различных скоростей работы шины. Поэтому сегодня нам будет проще, так как ещё раз изучать нам это не придётся. Поэтому кто не видел урок 150, обязательно посмотрите.
Единственная трудность будет в том, что всю инициализацию модуля I2C взял на себя проектогерератор, а в данном уроке нам придётся это проделать самим. Но, так как мы всё равно просматривали, что нам сгенерировал Cube MX, то нам тоже проинициализировать I2C много труда не составит.
Схема осталась прежняя, также подключен к ней логический анализатор для того, чтобы мониторить, что происходит на шине
Проект за основу мы возьмём из урока 170 по динамической индикации с именем CMSIS_LED_DYN и назовём его CMSIS_I2C_EEPROM.
Откроем проект в Keil и из тела бесконечного цикла функции main() файла main.c удалим весь код.
Выше функции main() создадим функцию инициализации I2C. Сделаем её статической, так как видеть её из других модулей нам не требуется
1 2 3 4 5 |
//---------------------------------------------------------- static void I2C_Init(void) { } //---------------------------------------------------------- |
Вызовем её в main()
1 2 |
GPIO_Init(); I2C_Init(); |
И начнём потихоньку наполнять кодом её тело. Инициализируем сначала ножки GPIO PB6 и PB7 для шины I2c, включив им альтернативный режим и максимальную скорость, а затем включим порту B тактирование
1 2 3 4 5 6 |
static void I2C_Init(void) { //I2C1 GPIO SET_BIT(GPIOB->CRL, GPIO_CRL_CNF7_1 | GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_0 | GPIO_CRL_CNF6_0 |\ GPIO_CRL_MODE7_1 | GPIO_CRL_MODE6_1 | GPIO_CRL_MODE7_0 | GPIO_CRL_MODE6_0); SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN); |
Хотя у нас уже включено тактирование данного порта, так как мы его другие ножки используем для индикатора, но сделаем это для универсальности.
Включим тактирование модуля I2C
1 2 3 4 5 |
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN); //RCC peripheral clock enabling SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN); //Delay after an RCC peripheral clock enabling tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN); |
Отключим режим двойной адресации
1 2 3 |
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN); //Disable acknowledge on Own Address2 match address CLEAR_BIT(I2C1->OAR2, I2C_OAR2_ENDUAL); |
Работу с широковещательными адресами мы не планируем, поэтому отключим также и эту возможность
1 2 3 |
CLEAR_BIT(I2C1->OAR2, I2C_OAR2_ENDUAL); //Disable General Call CLEAR_BIT(I2C1->CR1, I2C_CR1_ENGC); |
Задействуем отложенный отклик в случае невозможности SLAVE откликнуться мгновенно
1 2 3 |
CLEAR_BIT(I2C1->CR1, I2C_CR1_ENGC); //Enable Clock stretching CLEAR_BIT(I2C1->CR1, I2C_CR1_NOSTRETCH); |
Отключим модуль для его безболезненной дальнейшей настройки
1 2 3 |
CLEAR_BIT(I2C1->CR1, I2C_CR1_NOSTRETCH); //Disable I2C peripheral CLEAR_BIT(I2C1->CR1, I2C_CR1_PE); |
Настроим скорость шины 100 кГц, как настраивается скорость, мы также знаем. Причём настроим ещё и на 400 кГц, но данные настройки пока закомментируем
1 2 3 4 5 6 7 |
CLEAR_BIT(I2C1->CR1, I2C_CR1_PE); //ClockSpeed MODIFY_REG(I2C1->CR2, I2C_CR2_FREQ, 36); MODIFY_REG(I2C1->TRISE, I2C_TRISE_TRISE, 36 + 1); //100 //MODIFY_REG(I2C1->TRISE, I2C_TRISE_TRISE, 11); //400 MODIFY_REG(I2C1->CCR, (I2C_CCR_FS | I2C_CCR_DUTY | I2C_CCR_CCR), 180); //100 //MODIFY_REG(I2C1->CCR, (I2C_CCR_FS | I2C_CCR_DUTY | I2C_CCR_CCR), 0x0000801E); //400 |
Добавим макросы — один для настройки модуля на 7-битный режим адресации, а другой — для режима I2C (как мы помним, бывает ещё SMBus)
1 2 3 |
#define TIM_DisableCounter(TIMx) CLEAR_BIT(TIMx->CR1, TIM_CR1_CEN) #define I2C_OWNADDRESS1_7BIT 0x00004000U #define I2C_MODE_I2C 0x00000000U |
В функции I2C_Init настроим данные режимы путём инициализации определённых регистров и их битов
1 2 3 4 5 |
//MODIFY_REG(I2C1->CCR, (I2C_CCR_FS | I2C_CCR_DUTY | I2C_CCR_CCR), 0x0000801E); //400 //Set the Own Address1 MODIFY_REG(I2C1->OAR1, I2C_OAR1_ADD0 | I2C_OAR1_ADD1_7 | I2C_OAR1_ADD8_9 | I2C_OAR1_ADDMODE, I2C_OWNADDRESS1_7BIT); //Configure I2C1 peripheral mode with parameter : I2C MODIFY_REG(I2C1->CR1, I2C_CR1_SMBUS | I2C_CR1_SMBTYPE | I2C_CR1_ENARP, I2C_MODE_I2C); |
Включим модуль
1 2 3 |
MODIFY_REG(I2C1->CR1, I2C_CR1_SMBUS | I2C_CR1_SMBTYPE | I2C_CR1_ENARP, I2C_MODE_I2C); //Enable I2C peripheral SET_BIT(I2C1->CR1, I2C_CR1_PE); |
Разрешим генерацию условия ACK после приёма байта
1 2 3 |
SET_BIT(I2C1->CR1, I2C_CR1_PE); //TypeAcknowledge MODIFY_REG(I2C1->CR1, I2C_CR1_ACK, I2C_CR1_ACK); |
Установим значение второго адреса в 0
1 2 3 |
MODIFY_REG(I2C1->CR1, I2C_CR1_ACK, I2C_CR1_ACK); //Set the 7bits Own Address2 MODIFY_REG(I2C1->OAR2, I2C_OAR2_ADD2, 0); |
Вот и вся инициализация.
Осталось научить наш модуль отправлять и принимать данные. Это мы тоже делать умеем.
Поэтому выше функции main() создадим сначала функцию для передачи определённого количества байтов из буфера в шину
1 2 3 4 5 6 |
//---------------------------------------------------------- void AT24C_WriteBytes (uint16_t addr,uint8_t *buf, uint16_t bytes_count) { uint16_t i; } //---------------------------------------------------------- |
Для того, чтобы что-то передавать по шине I2C, нужно знать адрес устройства, который мы знаем, поэтому создадим соответствующий макрос для адреса. Так как адрес 7-битный, то в качестве 8-го бита будет либо 0 в случае записи, либо 1 в случае чтения, поэтому кроме адреса добавим ещё 2 макроса
1 2 3 4 |
#define I2C_MODE_I2C 0x00000000U #define SLAVE_OWN_ADDRESS 0xA0 #define I2C_REQUEST_WRITE 0x00 #define I2C_REQUEST_READ 0x01 |
Передавать мы будем 20 байт подряд, читать тоже, поэтому создадим два буфера, один сразу инициализированный числами, а другой — пока нулями
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}; |
Вызовем функцию передачи байтов в main()
1 2 |
TIM_EnableCounter(TIM2); AT24C_WriteBytes (0x008A, wr_value, 20); |
Начнём писать код тела данной функции.
Сбросим бит POS в регистре CR1
1 2 3 |
uint16_t i; //Disable Pos CLEAR_BIT(I2C1->CR1, I2C_CR1_POS); |
Включим генерирование условия ACK
1 2 3 4 |
CLEAR_BIT(I2C1->CR1, I2C_CR1_POS); //Prepare the generation of a ACKnowledge condition //after the address receive match code or next received byte MODIFY_REG(I2C1->CR1, I2C_CR1_ACK, I2C_CR1_ACK); |
Сгенерируем условие START
1 2 3 |
MODIFY_REG(I2C1->CR1, I2C_CR1_ACK, I2C_CR1_ACK); //Generate a START condition SET_BIT(I2C1->CR1, I2C_CR1_START); |
Дождёмся установки бита SB в регистре SR1. Данный бит устанавливается, как мы уже знаем, в случае наличия на шине условия START
1 2 3 4 5 |
SET_BIT(I2C1->CR1, I2C_CR1_START); //Indicate the status of Start Bit (master mode) while (!READ_BIT(I2C1->SR1, I2C_SR1_SB)){}; //read state (void) I2C1->SR1; |
Передадим адрес SLAVE микросхеме
1 2 3 4 5 6 |
(void) I2C1->SR1; //Transmit Address SLAVE MODIFY_REG(I2C1->DR, I2C_DR_DR, SLAVE_OWN_ADDRESS | I2C_REQUEST_WRITE); while (!READ_BIT(I2C1->SR1, I2C_SR1_ADDR)){}; (void) I2C1->SR1; (void) I2C1->SR2; |
Передадим 16-битный адрес памяти EEPROM, с которого мы будем писать байты в неё путём дальнейшей их последовательной передачи
1 2 3 4 5 6 |
(void) I2C1->SR2; //Transmit Address begin EEPROM MODIFY_REG(I2C1->DR, I2C_DR_DR, (uint8_t) (addr>>8)); while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; MODIFY_REG(I2C1->DR, I2C_DR_DR, (uint8_t) addr); while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; |
Затем последовательно передадим байты, которые запишутся с данного адреса в память микросхемы
1 2 3 4 5 6 |
while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; for(i=0;i<bytes_count;i++) { MODIFY_REG(I2C1->DR, I2C_DR_DR, buf[i]); while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; } |
Сгенерируем условие STOP для окончания передачи
1 2 3 |
while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; } SET_BIT(I2C1->CR1, I2C_CR1_STOP); |
Вот и вся функция передачи.
Добавим код в функцию main(), который покажет нам значения байтов, которые мы записали в микросхему
1 2 3 4 5 6 |
AT24C_WriteBytes (0x008A, wr_value, 20); for(i=0;i<20;i++) { ledprint(wr_value[i]); delay_ms(1000); } |
Соберём код, прошьём контроллер и посмотрим результат в логической программе
Все байты успешно переданы.
Также дисплей у нас всё отображает
Добавим теперь функцию для чтения значений из микросхемы. Механизм чтения байтов из микросхемы немного сложнее, чем их записи. Последний байт (даже если он единственный) принимается без подтверждения (условие NACK). Начало функции аналогичное
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//---------------------------------------------------------- void AT24C_ReadBytes (uint16_t addr, uint8_t *buf, uint16_t bytes_count) { uint16_t i; //Disable Pos CLEAR_BIT(I2C1->CR1, I2C_CR1_POS); //Prepare the generation of a ACKnowledge condition //after the address receive match code or next received byte MODIFY_REG(I2C1->CR1, I2C_CR1_ACK, I2C_CR1_ACK); //Generate a START condition SET_BIT(I2C1->CR1, I2C_CR1_START); //Indicate the status of Start Bit (master mode) while (!READ_BIT(I2C1->SR1, I2C_SR1_SB)){}; //read state (void) I2C1->SR1; //Transmit Address MODIFY_REG(I2C1->DR, I2C_DR_DR, SLAVE_OWN_ADDRESS | I2C_REQUEST_WRITE); while (!READ_BIT(I2C1->SR1, I2C_SR1_ADDR)){}; (void) I2C1->SR1; (void) I2C1->SR2; //Transmit Address begin EEPROM MODIFY_REG(I2C1->DR, I2C_DR_DR, (uint8_t) (addr>>8)); while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; MODIFY_REG(I2C1->DR, I2C_DR_DR, (uint8_t) addr); while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; } //---------------------------------------------------------- |
В функции main() закомментируем вызов функции записи и вызовем функцию чтения
1 2 |
//AT24C_WriteBytes (0x008A, wr_value, 20); AT24C_ReadBytes (0x008A, rd_value, 20); |
В цикле также закомментируем вывод элементов массива данных для записи и выведем элементы массива со считанными данными из микросхемы
1 2 |
//ledprint(wr_value[i]); ledprint(rd_value[i]); |
Вернёмся в функцию AT24C_ReadBytes, сформируем ещё одно условие START, а затем передадим адрес устройства уже с установленным флагом чтения, такой порядок работы с шиной
1 2 3 4 5 6 7 8 9 |
while (!READ_BIT(I2C1->SR1, I2C_SR1_TXE)){}; SET_BIT(I2C1->CR1, I2C_CR1_START); while (!READ_BIT(I2C1->SR1, I2C_SR1_SB)){}; (void) I2C1->SR1; //Transmit Address MODIFY_REG(I2C1->DR, I2C_DR_DR, SLAVE_OWN_ADDRESS | I2C_REQUEST_READ); while (!READ_BIT(I2C1->SR1, I2C_SR1_ADDR)){}; (void) I2C1->SR1; (void) I2C1->SR2; |
В цикле примем все наши байты, причём последний с установленным флагом NACK
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(void) I2C1->SR2; for(i=0;i<bytes_count;i++) { if(i<(bytes_count-1)) { while (!READ_BIT(I2C1->SR1, I2C_SR1_RXNE)){}; buf[i] = READ_BIT(I2C1->DR, I2C_DR_DR); } else { CLEAR_BIT(I2C1->CR1, I2C_CR1_ACK); SET_BIT(I2C1->CR1, I2C_CR1_STOP); while (!READ_BIT(I2C1->SR1, I2C_SR1_RXNE)){}; buf[i] = READ_BIT(I2C1->DR, I2C_DR_DR); } } |
Функция готова, так как мы вызов уже добавили, то можем проверить чтение, собрав код и прошив контроллер.
Посмотрим результат в программе логического анализа
Мы видим, что все наши байты успешно принялись и последний пришёл без подтверждения. На дисплее тоже всё отобразились (думаю, показывать это не обязательно, поверьте на слово).
Теперь давайте попробуем настроить шину на максимальную скорость, так как производитель микросхемы нам обещает работу на огромных скоростях. Правда больше 400 кбпс наша шина на STM не разгоняется, но и это неплохо.
Да и у нас уже всё настроено на такую скорость, осталось нам найти данный фрагмент настройки скорости в функции инициализации модуля I2C_Init и закомментировать там настройку на 100 кГц в двух местах, а на 400 — раскомментировать
1 2 3 4 |
//MODIFY_REG(I2C1->TRISE, I2C_TRISE_TRISE, 36 + 1); //100 MODIFY_REG(I2C1->TRISE, I2C_TRISE_TRISE, 11); //400 //MODIFY_REG(I2C1->CCR, (I2C_CCR_FS | I2C_CCR_DUTY | I2C_CCR_CCR), 180); //100 MODIFY_REG(I2C1->CCR, (I2C_CCR_FS | I2C_CCR_DUTY | I2C_CCR_CCR), 0x0000801E); //400 |
Собираем код, прошиваем контроллер, смотрим результат в логическом анализе
Процесс теперь длится окло 550 микросекунд, что вчетверо быстрее, чем в режиме Standard. Оно так и есть скорость вместо 100 кбпс у нас теперь 400.
Закоментируем теперь вызов функции чтения, раскомментируем запись, таким же образом поступим и с массивом в цикле
1 2 |
AT24C_WriteBytes (0x008A, wr_value, 20); //AT24C_ReadBytes (0x008A, rd_value, 20); |
1 2 |
ledprint(wr_value[i]); //ledprint(rd_value[i]); |
Соберём код, прошьём контроллер и убедимся в том, что процесс записи у нас также остался в работоспособном состоянии
Всё нормально работает, времени уходит даже меньше, чем на чтение. Ну у нас и байтов меньше в обмене, мы адрес дважды не передаём.
Таким образом, на данном уроке мы научились работать с шиной I2C с использованием возможностей библиотеки CMSIS, что позволило нам записать и считать данные по данной шине в микросхему памяти EEPROM AT24C32. Также мы научились самостоятельно писать инициализацию модуля I2C без помощи автогенерации кода.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Модуль RTC DS3231 с микросхемой памяти (2-5 шт)
Модуль RTC DS3231 с микросхемой памяти (1 шт) — так дороже
Семисегментный чертырехразрядный индикатор красный (с общим анодом или катодом на выбор) 10 шт
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК (нажмите на картинку)
Не удаётся пройти регистрацию — не приходят подтверждения
Странно, а как же Вы сюда пишете, вроде всё зарегистрировано.
Сам удивлён. Некто похозяйничал в моём ящике, жалко только полезные ссылки. Пусть это остаётся на совести авторов майл.ру.
Ваши уроки вдохновили на старости лет заняться полезным делом — изучать языки, контроллеры. Всё очень доходчиво, понятно с первого раза. Чувствуется старая школа. Снимаю шляпу.
Доброго времени суток. Такой вопрос: Вы не использовали еще новую среду разработки от ST CubeIDE на базе Eclipse (она ведь бесплатная)? Если да, то можете подсказать как правильно собрать проект с нуля где нужно вручную добавлять все файлы нужные для используемого камня, а то, насколько я понял, Вам Keil помогает с этим.
С данной средой я ещё до конца не разобрался. Заметил очень немало глюков особенно в части построения проекта до генерации.
Здравствуйте! Никак не соображу как переделать на 24с02. Подтолкните пожалуйста.
Не увидел упоминаний о глючности I2C в stm32f1(03). Надежная инициализация предполагает некоторые танцы с бубном, по errdata?
Чтение из I2C также в правильном исполнении выглядит гораздо сложней, с учетом всех особенностей.
Печально, что это снова тропка по болоту, дальше без проводника снова не будет хода. А хотелось бы системных знаний. Ок, ок, читаем документацию.
Нормально. Но, странно как-то в 2019_м году видеть примеры для F1xx. Хотя бы F303 какой-нибудь взяли.
На новых версиях шина лучше настраивается.