Stm32 софтовый i2c для работы с eeprom. STM32, последовательный интерфейс I2С. Регистр управления IICC2

Сегодня на нашем операционном столе новый гость, это продукт компании Microchip расширитель портов MCP23008-E. Предназначена эта штуковина (как понятно из названия) для увеличения числа I/O ног микроконтроллера, если их вдруг стало не хватать. Конечно если нам нужные ноги-выходы то можно взять и не париться. Если нужны ноги-входы то и тут есть решение на жесткой логике. Если же нам нужны одновременно входы и выходы да еще и управляемая подтяжка для входов, то расширитель портов это пожалуй самое нормальное решение. Что касаемо цены девайса то она весьма скромная — примерно бакс. В данной статье я попробую детально описать как рулить данной микросхемой при помощи микроконтроллера AVR.

Для начала немного о характеристиках:

  • 8 независимых пинов порта
  • Интерфейс для связи с внешним миром — I2C (частота до 1.7 МГц)
  • Настраиваемая подтяжка для входов
  • Может дёрнуть ногой когда состояние определённых входов изменится
  • Три входа для задания адреса микросхемы (можно повесить 8 устройств на одну шину)
  • Рабочее напряжение от 1.8 до 5.5 вольт
  • Малый ток потребления
  • Большой выбор корпусов (PDIP/SOIC/SSOP/QFN)

Интерфейс я предпочитаю использовать I2C, он привлекает меня малым количеством проводков:-) однако если нужна очень быстрая скорость (до 10 МГц), то использовать нужно SPI интерфейс который присутствует в MCP23S08. Различие между MCP23S08 и MCP23008 как я понял только в интерфейсе и в количестве ног для задания адреса микросхемы. Кому что по душе короче. Распиновка микрухи есть в даташите, она ни чем не интересна и рассматриваться тут не будет. Поэтому сразу перейдем к тому как начать работать с данным девайсом. Все работа сводится к тому чтоб записывать и считывать определенные данные из регистров микросхемы. Регистров на радость мне оказалось совсем не много — всего лишь одиннадцать штук. Писать и читать данные из регистров очень просто, пост про это . Речь там правда не о регистрах, но принцип тот же. Теперь осталось выяснить из каких регистров что считывать и в какие регистры что записывать. В этом нам разумеется поможет даташит. Из даташита мы узнаем, что у микросхемы есть следующие регистры:

Регистр IODIR
Задаёт направление в котором идут данные. Если бит соответствующий определённой ножке установлен в единицу, то ножка вход. Если сброшен в ноль, то выход. Короче этот регистр аналог DDRx в AVR (только в AVR 1- это выход а 0 — вход).

Регистр IPOL
Если бит регистра установлен,то для соответствующей ноги включена инверсия входа. Это означает, что если на ножку подать лог. ноль то из регистра GPIO считается единица и наоборот. Полезность данной фичи весьма сомнительна.

Регистр GPINTEN
Каждый бит этого регистра соответствует определённому пину порта. Если бит установлен, то соответствующий пин порта настроенный на вход может вызывать прерывание. Если бит сброшен, то что бы не делали с ногой порта - прерывания не будет. Условия возникновения прерывания задаются двумя следующими регистрами.

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

Регистр INTCON
Каждый бит этого регистра соответствует определённому пину порта. Если бит сброшен, то любая смена логического уровня на противоположный вызывает прерывание. В случае если бит установлен, то на возникновение прерывания оказывает влияние регистр DEFVAL (в противоположном случае он вообще игнорируется).

Регистр IOCON
Это регистр настроек. Состоит он из четырёх бит:
SEQOP — бит управляет автоувеличением адреса. Если он установлен то автоувеличение отключено, в противном случае включено. Если мы выключим его, то можем очень быстро считывать значение одного и того же регистра за счёт того, что нам не придётся передавать его адрес каждый раз. Если же нужно быстро прочитать все 11 регистров по очереди то автоувеличение нужно включить. При каждом считывании байта из регистра адрес будет сам увеличиваться на единицу и передавать его не придётся.
DISSLW — фиг знает чё за бит. Как его не крутил всё равно работает всё. Буду рад если кто объяснит.
HAEN — Бит настройки вывода INT. Если он установлен то вывод сконфигурирован как открытый сток, если бит сброшен, то активный уровень на ноге INT определяет бит INTPOL
INTPOL — определяет активный уровень на ноге INT. Если он установлен то активный уровень единица, в противном случае ноль.

Есть еще бит HAEN но он не используется в данной микросхеме (включает/выключает пины аппаратной адресации в MCP23S08)

Регистр GPPU
Управляет подтяжкой входов. Если бит установлен то на соответствующем ему пине появится подтяжка к питанию через резистор 100 кОм.

Регистр INTF
Регистр флагов прерываний. Если бит установлен то это означает, что соответствующая ему нога порта вызывала прерывание. Разумеется прерывания для нужных ног должны быть включены в регистре GPINTEN

Регистр INTCAP
В момент возникновения прерывания в этот регистр считывается весь порт. Если потом после этого будут еще прерывания, то содержимое этого регистра не затрется новым значением до тех пор пока мы не считаем его или GPIO.

Регистр GPIO
Считывая данные из регистра — считываем логические уровни на ножках порта. Записывая данные — устанавливаем логические уровни. При записи в этот регистр автоматически происходит запись этих же самых данных в регистр OLAT

Регистр OLAT
Записывая данные в этот регистр - выводим данные в порт. Если прочитать оттуда данные, то прочитается то что записали, а не то что что фактически есть на входах порта. Для чтения входов используем только GPIO.

Описание регистров на мой взгляд вполне нормальное и мозг ни кому взорвать не должно. Теперь ради интереса напишем небольшой демонстрационный пример который будет опрашивать 4 кнопки подключенные к расширителю порта, и в зависимости от их состояния зажигать или гасить 4 соответствующих светодиода подключенных к тому же расширителю. Пример будет максимально простым, без всяких прерываний. Просто постоянно считываем состояние кнопок и выводим это состояние на светодиоды. Схема нашего устройства будет выглядеть так:

Вывод прерывания можно не подключать, он тут нам не нужен, а вот подтяжка для ноги сброса обязательна! Я потратил много времени пока понял, что расширитель портов у меня периодически сбрасывался из-за отсутствия подтяжки. Теперь возьмёмся за код. Писать будем конечно же на . Код простецкий:

Program rashiritel; const AdrR=%01000001; //Адрес микросхемы с битом чтение const AdrW=%01000000; //Адрес микросхемы с битом запись var r:byte; ///Процедура записывает данные из переменной Dat в регистр по адресу Adr Procedure WriteReg(Dat,Adr:byte); Begin TWI_Start(); TWI_Write(AdrW); TWI_Write(Adr); TWI_Write(Dat); TWI_Stop(); End; ///Функция возвращает значение регистра по адресу Adr Function ReadReg(Adr:byte):byte; var a:byte; Begin TWI_Start(); TWI_Write(AdrW); TWI_Write(Adr); TWI_Start(); TWI_Write(AdrR); a:=TWI_Read(0); TWI_Stop(); result:=a; End; begin TWI_INIT(200000); ///Инициализация i2c WriteReg(%00001111,0x00); //Младшие 4 бита входы а остальные 4 выходы WriteReg(%00001111,0x06); //Вкл. подтяжку для 4-х входов While TRUE do Begin r:=ReadReg(0x09); //Считали состояние входов r:= NOT r; //Надо инвертировать биты иначе при отпущеной кнопке светодиоды будут гореть r:= r shl 4; //Сдвигаем на 4 бита влево... WriteReg(r,0x0A); //Выводим состояние кнопок end; end.

Для использования в дальнейшем понадобилось связать, используя I2C микроконтроллер STM32 с экраном 2004. Не найдя аналогичного решения в сети, публикую здесь. Данный рецепт подойдёт также для экранов 1602. Далее под катом. (Осторожно, картинки).

Игрушечная касса, купленная сыну, оказалось с дефектом, и работала через раз. Появилась идея переделать её внутренности, и момент выбора микроконтроллера совпал с публикацией статьи про STM32 . Немного прикинув и сравнив цены: STM32+LCD2004+I2C = ArduinoMega (причина была в том, что нужно было реализовать клавиатуру, динамик, устройство ввода штрих-кода и экран, поэтому каждый вывод микроконтроллера на счету) я выбрал первый набор.

Были сделаны покупки, и наступило время ожидания. Для прошивки купил ещё USB-USART переходник.

Что и где покупалось.

  1. 2004 LCD HD44780 . Оказался без кириллицы. Обращайте внимание на данную особенность при поиске, если нужен русский язык на экране.
  2. IIC/I2C/TWI/SP​​I Serial Interface Board Module Port For Arduino 1602LCD Display По описанию совместим с 2004. Но думаю подойдёт любой аналогичный.
  3. USB to UART TTL CP2012 для прошивки и отладки. Можно воспользоваться и другими поддерживаемыми способами прошивки и отладки, но этот вариант самый дешевый.

Средства для программирования, прошивки и отладки, используемые мною:
  1. Прошивальщик с оригинального сайта: STM32 and STM8 Flash loader demonstrator .
  2. Терминал для чтения сигналов от MK через USB2UART: Terminal v1.91b . Но подойдёт и Putty (Connection->Serial).
После получения микроконтроллера попробовал поиграть с светодиодами, получилось. А потом были несколько часов попыток связать экран с МК. Всё это описывать скучно, попробую вспомнить грабли, на которые напоролся.

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


Данное подключение актуально для STM32F103C8. Для других плат МК проверьте пины подключения I2C1 по даташиту.
USART переходник в USB. Тут понятно. Далее - USART подключаем к STM32 к выведенному около разъема miniUSB USART1. TX к RX и соответственно RX к TX. У меня на USART есть вывод 3v3, я от него и запитал МК. Землю я подключил отдельно, для удобного её отключения во время переключения режимов прошивки и работы. К экрану я припаял I2C (так же на ebay есть экраны с припаянными I2C). Питание для I2C и экрана берётся от 3v3 МК или 5В от USART. Ниже написал про настройку контраста при различном напряжении питании. Далее: SCL от I2C подключается к PB6, SDA от I2C к PB7. Притягивать SCL и SDA к питанию при использовании одного данного устройства нет необходимости.

Первыми граблями был USART. Его я использовал для отладки, в приведённом здесь коде строки работы с ним закомментированы. Но с ним проблему так и не решил. Такое впечатление, что нет синхронизации между компьютером и микроконтроллером до посылки первого символа. Причем если использовать код из примера - то МК прекрасно дублирует получаемый текст, а сам писать не может. Я добился наиболее приемлемого для отладки вывода строк, добавив Delay(500) после каждого символа.

Потом попытался реализовать работу с I2C. Взял код из примера , обратил внимание на комментарии про подвисание МК, проанализировав исходники, увидел что как и автору комментариев, мне необходим сдвиг адреса устройства влево:

//http://microtechnics.ru/stm32-ispolzovanie-i2c/#comment-8109 I2C_Send7bitAddress(I2Cx, slaveAddress<<1, transmissionDirection);
Вставил код и попробовал запустить. Программа повисала на моменте ожидания освобождения шины:

While(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
Тут грабли в адресе I2C устройства. Судя из описания продавца, у меня был адрес 0x20. Вот тут я и потерял 15 минут впустую, но вчитавшись в описание разных моделей I2C переходников, ссылку на которое привёл в своей статье , обратил внимание на последнюю модель и попробовал поменять адрес на 0x27. Всё заработало. Вывод такой: если у Вас на переходнике запаяны A0 A1 A2 - адрес 0x20, не запаяны - 0x27.
Сравните:




Далее - экран. Оказалось, что он прекрасно работает и от 3.3 Вольт, как и переходник I2C (в даташите микросхемы переходника - от 2.5 до 6 В). Но сначала я его проверял от 5В. И контраст был выкручен на максимум. В итоге в результате запуска программы экран был полностью заполнен. Я расстроился и продолжил ковырять код. Но спустя полчаса проснулся и подбежал виновник разработки, я ему показал экран и случайно увидел под углом сбоку, что там что-то написано. Причиной этому является неправильная регулировка контраста. (Извините, если описал тут очевидные вещи, может найдутся такие же, кто этого не знал.)

Ничего не видно

То же самое, но под углом

При 5В питания контраст нужно немного уменьшить. А при 3.3В поставить на максимум, на настройке от 5В ничего не видно. Результат представлен на первой картинке в посте. Мой оказался без русского языка, я это увидел, пролистав символы. Попробовал нарисовать кляксу, не зная, что максимум можно определить 8 своих символов, написал для кляксы 12. Подобрал похожие из китайских, вроде получилось.


Код представлен на гихабе, так как для достижения результата переписал библиотеку от Ардуины.

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

Интерфейс 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.

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

В статье приведено описание последовательного интерфейса I2С 32-разрядных ARM-микроконтроллеров серии STM32 от компании STMicroelectronics. Рассмотрены архитектура, состав и назначение регистров конфигурирования интерфейса, а также приведены примеры программ его использования.

Введение

Интерфейс I2С, или IIC получил свою аббревиатуру от английских слов Inter-Integrated Circuit и представляет собой последовательную шину, состоящую из двух двунаправленных линий связи с названием SDA и SCL, как сокращение от слов Serial Data Address и Serial Clock. Он обеспечивает обмен данными между микроконтроллером и различными периферийными устройствами, такими как АЦП, ЦАП, микросхемы памяти, другие микроконтроллеры и микросхемы. Схема подключения устройств по интерфейсу I2C показана на рисунке 1.

Рис. 1. Схема подключения устройств по интерфейсу I 2 C

Стандарт на интерфейс I2С был разработан фирмой Philips в начале 1980-х годов. Согласно этому стандарту, интерфейс имел 7-разрядный адрес. Он позволял обращаться к 127 устройствам на скорости до 100 кбит/с. В дальнейшем интерфейс получил своё развитие и стал 10-разрядным, позволяющим обращаться к 1023 устройствам на скорости до 400 кбит/с. Максимальное
допустимое количество микросхем, подсоединённых к одной шине, ограничивается максимальной ёмкостью шины в 400 пФ. Версия стандарта 2.0, выпущенная в 1998 году, представила
высокоскоростной режим работы со скоростью до 3,4 Мбит/с с пониженным энергопотреблением. Версия 2.1 2001 года включает в себя лишь незначительные доработки.

Описание интерфейса I 2 C

Микроконтроллер STM32 включает в свой состав интерфейс I2С, который отличается своей развитостью. Он допускает несколько ведущих устройств на шине и поддерживает высокоскоростной режим. Кроме того, в микроконтроллере STM32 интерфейс I2C можно использовать для широкого спектра приложений, включая генерацию и верификацию контрольной суммы. С ним также можно работать по протоколам SMBus (System Management Bus) и PMBus (Power Management Bus). Большинство моделей STM32 включают в свой состав два интерфейса I2С с именами I2С1 и I2С2. Интерфейс может работать в одном из следующих четырёх режимов:

  • Slave transmitter (ведомый передатчик);
  • Slave receiver (ведомый приёмник);
  • Master transmitter (ведущий передатчик);
  • Master receiver (ведущий приёмник).

По умолчанию интерфейс работает в режиме «Ведомый» и автоматически переключается на «Ведущий» после генерирования старт-условия. Переключение с «Ведущего» на «Ведомый» происходит при потере арбитража или после генерирования стоп-условия, что позволяет работать нескольким «Ведущим» микроконтроллерам в одной системе поочередно. В режиме «Ведущий» I2C инициирует обмен данными и генерирует тактовый сигнал. Передаче последовательных данных всегда предшествует старт-условие, а завершается обмен всегда стоп-условием. Оба этих условия генерируются в режиме «Ведущий» программно. В режиме «Ведомый» I2C способен распознать свой собственный адрес (7 или 10 бит) и адрес общего вызова. Определение наличия адреса общего вызова можно включить или отключить программно. Адрес и данные передаются 8-битными посылками, старшим битом вперёд. Первый байт, следующий за стартусловием, содержит адрес (один байт в 7-битном режиме и два байта в 10-битном режиме). Адрес всегда передаётся в режиме «Ведущий».

За 8 тактами передачи байта данных следует 9-й такт, в течение которого приёмник должен послать бит уведомления ACK, получивший своё название от слова ACKnowledge. На рисунке 2 приведена временна′я диаграмма одной посылки интерфейса I2C. Наличие уведомления в ответе можно программно включить или отключить. Размерность адреса интерфейса I2C (7 бит или 10 бит и адрес
общего вызова) можно выбрать программно.


Рис. 2. Временная диаграмма одной посылки интерфейса I

Архитектура блока интерфейса I 2 С

Функциональная схема блока интерфейса I2C для микроконтроллера STM32 приведена на рисунке 3.


Рис. 3. Функциональная схема блока интерфейса I 2 C

Регистр сдвига на этой схеме представляет собой основной регистр, через который передаются и принимаются данные. Передаваемые данные предварительно записываются в регистр данных, после чего через регистр сдвига последовательно транслируются в линию связи SDA. Принимаемые по этой же линии связи данные накапливаются в регистре сдвига, а затем перемещаются в регистр данных. Таким образом, интерфейс может передавать и принимать данные только поочередно. Кроме того, регистр сдвига аппаратно подключён к компаратору, который позволяет сравнивать принятый адрес
с адресными регистрами и, таким образом, определять, для кого предназначен очередной блок данных. Узел управления частотой позволяет формировать сигнал синхронизации SCL в роли ведущего и синхронизироваться от этого сигнала в качестве ведомого устройства. Регистр CCR обеспечивает программную настройку данного узла. Блок интерфейса подключён к выходу PCLK1 шины APB1 через
два предварительных делителя. Микроконтроллер поддерживает два режима обмена: стандартный (Standard Speed) – до 100 кГц, и быстрый (Fast Speed) – до 400 кГц. В зависимости от режима обмена частота тактирования модуля должна быть не менее 2 МГц в стандартном режиме и не менее 4 МГц в быстром режиме. Блок вычисления позволяет аппаратно вычислять контрольную сумму блока данных и сохранять её в регистре PEC. Управление блоком интерфейса I2C, а также формирование флагов событий и прерываний выполняется узлом логики управления. Он же позволяет обслуживать запросы ПДП и формировать сигнал ACK. Связь этого блока с микроконтроллером осуществляется программно с помощью регистров управления CR1, CR2 и регистров состояния SR1, SR2.

Прерывания от I 2 C

Интерфейс I2C имеет аппаратную организацию, способную формировать запросы на прерывание в зависимости от режима работы и текущих событий. В таблице 1 приведены запросы на прерывание от интерфейса I2C.

Таблица 1. Запросы на прерывание от интерфейса I 2 C

Описание регистров

Для работы с интерфейсом I2C в микроконтроллере STM32 имеются специальные регистры. Карта этих регистров с названием входящих в них разрядов представлена в таблице 2. Рассмотрим регистры, необходимые для работы интерфейса I2С. К ним относятся:

  • I 2 C_CR1 – управляющий регистр 1;
  • I 2 C_CR2 – управляющий регистр 2;
  • I 2 C_OAR1 – регистр собственного адреса 1;
  • I 2 C_OAR2 – регистр собственного адреса 2;
  • I 2 C_DR – регистр данных;
  • I 2 C_SR1 – статусный регистр 1;
  • I 2 C_SR2 – статусный регистр 2;
  • I 2 C_CCR – регистр управления тактовым сигналом;
  • I 2 C_TRISE – регистр параметра TRISE.

Некоторые разряды этих регистров используются для работы в режиме SMBus. Регистр I2C_CR1 является первым управляющим регистром интерфейса I2C. Он имеет следующие управляющие разряды:

  • разряд 15 SWRST – обеспечивает программный сброс шины I 2 C;
  • разряд 14 – зарезервирован;
  • разряд 13 SMBus – формирует сигнал тревоги в режиме SMBus;
  • разряд 12 PEC – служит для функции проверки ошибки пакета (Packet Error Checking);
  • разряд 11 POS – служит для анализа сигналов ACK или PEC при приёме;
  • разряд 10 ACK – возвращает бит уведомления ACK после приёма корректного байта адреса или данных;
  • разряд 9 STOP – служит для формирования и анализа стоп-условия;
  • разряд 8 START – служит для формирования и анализа старт-условия;
  • разряд 7 NOSCTETCH – отключает растяжку такта в режиме ведомого;
  • разряд 6 ENGC – разрешает общий вызов;
  • разряд 5 ENPEC – разрешает сигнал PEC;
  • разряд 4 ENARP – разрешает сигнал ARP;
  • разряд 3 SMBTYPE – назначает тип интерфейса в качестве ведущего или ведомого для режима SMBus;
  • разряд 2 – зарезервирован;
  • разряд 1 SMBUS – переключает режимы I 2 C и SMBus;
  • разряд 0 PE – разрешает работу интерфейса.

Регистр I2C_CR2 является вторым управляющим регистром интерфейса I2C и имеет следующие управляющие разряды:

  • разряды 15…13 – зарезервированы;
  • разряд 12 LAST – используется в режиме ведущего приёмника, чтобы позволить генерацию сигнала NACK по последнему принятому байту;
  • разряд 11 DMAEN – разрешает запрос DMA;
  • разряд 10 ITBUFEN – разрешает прерывания от буфера;
  • разряд 9 ITEVTEN – разрешает прерывания от события;
  • разряд 8 ITERREN – разрешает прерывания от ошибки;
  • разряды 7 и 6 – зарезервированы;
  • разряды 5…0 FREQ – задают частоту работы шины.

Регистр I2C_OAR1 – первый регистр собственного адреса, включает в себя следующие разряды:

  • разряд 15 ADDMODE – задаёт 7- или 10-разрядный режим адресации в качестве ведомого;
  • разряды 14…10 – зарезервированы;
  • разряды 9 и 8 ADD – назначают 9 и 8 биты адреса при 10-битной адресации интерфейса;
  • разряды 7…1 ADD – назначают 7…1 биты адреса;
  • разряд 0 ADD0 – назначает бит 0 адреса при 10-битной адресации интерфейса.

Регистр I2C_OAR2 – второй регистр собственного адреса, включает в себя следующие разряды:

  • разряды 15…8 – зарезервированы;
  • разряды 7…1 ADD – назначают 7…1 биты адреса в режиме двойной адресации;
  • разряд 0 ENDUAL – разрешает режим двойной адресации.

Регистр данных I2C_DR имеет 8 разрядов DR для приёма и передачи данных на шину I2C. В этот регистр данные записываются для передачи и читаются из него при приёме. Разряды 15…9 – зарезервированы. Регистр I2C_SR1 – первый статусный регистр, и включает в себя следующие разряды:

  • разряд 15 SMBALERT – сигнализирует о тревоге шины SMBus;
  • разряд 13 – зарезервирован;
  • разряд 14 TIMEOUT – оповещает об ошибке превышения времени для сигнала SCL;
  • разряд 12 PECERR – свидетельствует об ошибке PEC при приёме;
  • разряд 11 OVR – формируется при ошибке переполнения данных;
  • разряд 10 AF – возникает в случае ошибки уведомления;
  • разряд 9 ARLO – указывает на ошибку потери прав на шину;
  • разряд 8 BERR – устанавливается при ошибке шины;
  • разряд 7 TxE – оповещает, что регистр данных пуст;
  • разряд 5 – зарезервирован;
  • разряд 6 RxNE – информирует, что регистр данных не пуст;
  • разряд 4 STOPF – детектирует стоп-условие в режиме ведомого;
  • разряд 3 ADD10 – устанавливается, когда ведущий послал первый байт адреса при 10-битной адресации;
  • разряд 2 BTF – оповещает о завершении передачи байта;
  • разряд 1 ADDR – устанавливается если послан адрес в режиме ведущего или принят адрес в режиме ведомого;
  • разряд 0 SB – устанавливается при генерации старт-условия в режиме ведущего.

Регистр I2C_SR2 – второй статусный регистр, включает в себя следующие разряды:

  • разряды 15…8 PEC – содержат контрольную сумму кадра;
  • разряд 7 DUALF – является флагом двойной адресации в режиме ведо-мого;
  • разряд 6 SMBHOST – устанавливается, когда принят заголовок SMBus Host в режиме ведомого;
  • разряд 5 SMBDEFAULT – возникает, если принят адрес по умолчанию для SMBus-устройства в режиме ведомого;­
  • разряд 4 GENCALL – указывает, что принятадрес общего вызова в режиме ведомого;
  • разряд 3 – зарезервирован;
  • разряд 2 TRA – оповещает о режиме передачи/приёма;
  • разряд 1 BUSY – информирует, что шина занята;
  • разряд 0 MSL – детектирует режим «Ведущий»/«Ведомый».

Регистр I2C_CCR – регистр управления тактовым сигналом, который включает в себя разряды:

  • разряд 15 F/S – задаёт стандартную или быструю скорость для режима ведущего;
  • разряд 14 DUTY – назначает скважность 2 или 16/9 в быстром режиме;
  • разряды 13 и 12 – зарезервированы;
  • разряды 11…0 CCR – управляют тактовым сигналом для быстрой и стандартной скорости в режиме ведущего.

Регистр I2C_TRISE – регистр параметра TRISE, который включает в себя:

  • разряды 15…6 – зарезервированы;
  • разряды 5…0 TRISE – определяют максимальное время фронта для быстрой и стандартной скорости в режиме ведущего. Данный пара-метр задаёт момент времени, по которому производятся выборка состояния линий.

Более подробное описание назначения всех регистров I2C и их разрядов можно найти на сайте www.st.com .

П рограммирование интерфейса I 2 С

Рассмотрим практическую реализацию по использованию интерфейса I2С. Для этого можно воспользоваться стандартной библиотекой периферии микроконтроллера STM32. Для интерфейса I2C настройки режима, скорости и всего остального находятся в заголовочном файле и объявлены в виде структуры:

I2C_InitTypeDef:typedef struct{ uint32_t I2C_ClockSpeed; uint16_t I2C_Mode; uint16_t I2C_DutyCycle; uint16_t I2C_OwnAddress1; uint16_t I2C_Ack; uint16_t I2C_ AcknowledgedAddress; }I2C_InitTypeDef;

В этой структуре её элементы имеют следующее назначение:

  • uint32_t I 2 C_ClockSpeed – частота тактового сигнала, максимум – 400 КГц;
  • uint16_t I 2 C_Mode – режим работы;
  • uint16_t I 2 C_DutyCycle – настройки для работы в быстром режиме;
  • uint16_t I 2 C_OwnAddress – собственный адрес устройства;
  • uint16_t I 2 C_Ack – включено или нет использование бита подтверждения ACK;
  • uint16_t I 2 C_AcknowledgedAddress – выбор формата адреса: 7 бит или 10 бит.

Рассмотрим процедуры инициализации и работы с интерфейсом I2C. Для настройки интерфейса I2C в качестве ведущего устройства и передачи данных через него необходимо выполнить следующие действия:

  1. разрешить тактирование портов;
  2. инициализировать I 2 C, задав его скорость, адрес и формат адреса;
  3. назначить выводы микроконтроллера;
  4. разрешить работу интерфейса;
  5. сформировать стартовое условие;
  6. послать адрес адресуемого устройства и данные;
  7. сформировать стоповое условие.

Для облегчения процесса программирования желательно создать набор основных функций для работы с I2C. В листинге 1 приведена функция инициализации интерфейса I2C в соответствии с описанным выше алго­ритмом.

Листинг 1 GPIO_InitTypeDef gpio; // Создание структуры для портов ввода-вывода I2C_InitTypeDef i2c; // Создание структуры для интерфейса I2C void init_I2C1(void) { // Включить тактирование RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // Инициализировать I2C i2c.I2C_ClockSpeed = 100000; i2c.I2C_Mode = I2C_Mode_I2C; i2c.I2C_DutyCycle = I2C_DutyCycle_2; // Задать адрес=0x12 i2c.I2C_OwnAddress1 = 0x12; i2c.I2C_Ack = I2C_Ack_Disable; i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &i2c); // Назначить выводы интерфейса gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;

Теперь рассмотрим функцию для общения по I2C. Для расширения возможностей эта функция имеет три параметра: номер используемого блока I2C, направление передачи данных и адрес подчинённого устройства. Код данной функции приведён в лис­тинге 2.

Листинг 2 void I2C_StartTransmission(I2C_TypeDef* I2Cx, uint8_t transmissionDirection, uint8_t slaveAddress) { // Ждать освобождения шины while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY)); // Сформировать старт-условие I2C_GenerateSTART(I2Cx, ENABLE); // Ждать установки бита while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT)); // Отправить адрес ведомому устройству I2C_Send7bitAddress(I2Cx, slaveAddress, transmissionDirection); // Если передача данных if(transmissionDirection== I2C_Direction_Transmitter) {while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));} // Если прием данных if(transmissionDirection== I2C_Direction_Receiver) {while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));} }

Приведённая функция использует простые функции передачи и приёма данных, приведённые в листинге 3.

Листинг 3 // Функция передачи данных void I2C_WriteData(I2C_TypeDef* I2Cx, uint8_t data) { // Вызвать библиотечную функцию передачи данных I2C_SendData(I2Cx, data); // Ждать окончания передачи данных while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); } // Функция приема данных uint8_t I2C_ReadData(I2C_TypeDef* I2Cx) { // Ждать поступления данных while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)); data = I2C_ReceiveData(I2Cx); // Считать данные из регистра return data; // Возвратить данные в вызывающую функцию

Закончив обмен данными по I2C, необходимо вызвать функцию формирования стоп-условия
I2C_Generate STOP(I2Cx, ENABLE).
На основе приведённых функций можно создавать программы для работы с множеством разнообразных периферийных устройств.

Заключение

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

Похожие публикации