STM32, последовательный интерфейс I2С. Шина I2C и применение её в МК STM32 Stm32 софтовый i2c для работы с eeprom

Первые шаги с STM32 и компилятором mikroC для ARM архитектуры - Часть 4 - I2C, pcf8574 и подключение LCD на базе HD4478

Следующую статью я хочу посвятить работе с распространенным интерфейсом i2c, достаточно часто используемом в разнообразных микросхемах, подключаемых к микроконтроллеру.

I2C представляет собой шину работающую по двум физическим соединениям (помимо общего провода). Достаточно много о ней расписано в Интернете, неплохие статьи есть в Википедии . Кроме того алгоритм работы шины очень понятно описан . В вкратце, шина представят собой двухпроводную синхронную шину. На шине может одновременно находится до 127 устройств (адрес устройства 7-битный, к этому вернемся далее). Ниже приведена типичная схема подключения устройств к i2c шине, с МК в качестве ведущего устройства.


Для i2c все устройства (как мастер так и слейвы) используют open-drain выходы. Проще говоря они могут притягивать шину ТОЛЬКО К ЗЕМЛЕ. Высокий уровень ша шине обеспечивается подтягивающими резисторами. Номинал этих резисторов обычно выбирается в диапазоне от 4,7 до 10 кОм. i2c достаточна чувствительна к физическим линиям, соединяющим устройства, поэто если используется соединение с большой емкостью (например длинный тонкий или экранированный кабель), влияние этой емкости может «размыть» фронты сигналов и помешать нормальной работе шины. Чем меньше подтягивающий резистор, тем меньше влияет эта емкость на характеристику фронтов сигнала, но ТЕМ БОЛЬШЕ НАГРУЗКА на выходные транзисторы на интерфейсах i2c. Значение этих резисторов подбирается для каждой конкретной реализации, но они не должны быть меньше 2,2 кОмов, иначе можно просто спалить выходные транзисторы в устройствах, работающих с шиной.

Шина состоит из двух линий: SDA (линии данных) и SCL (тактирующего сигнала). Тактирует шину Мастер устройство , обычно наш МК. Когда на SCL высокий уровень информация считывается с шины данных. Изменять состояние SDA можно только при низком уровне тактирующего сигнала . При высоком уровне SCL сигнал на SDAизменяется при формировании сигналов START (при высоком уровне SCL сигнал на SDA изменяется высокого на низкий) и STOP - при высоком уровне SCL сигнал на SDA изменяется с низкого на высокий).

Отдельно следует сказать, что в i2c адрес задается 7-битным числом. 8 - младший бит указывает направление передачи данных 0 - означает что слейв будет передавать данные, 1 - принимать. . Вкратце алгоритм работы с i2c такой:

  • Высокий уроень на SDA и SCL - шина свободна, можно начинать работу
  • Мастер поднимает SCL в 1, и изменяет состояние SDA c 1 на 0 - притягивает его к земле - формируется сигнал START
  • Мастер передает 7-битный адрес слейва с битом направления (данные на SDA выставляются когда SCL притянут к земле, и читаются слейвом когда он отпущен). Если слейв не успевает «схавать» предыдущий бит, он притягивает SCL к земле, давая понять мастеру что состаяние шинны данных не нужно менять: «еще читаю предыдущий». После того как мастер отпустил шину он проверяет, отпустил ли ее слейв .
  • После передачи 8 бит адреса мастер генерирует 9-й такт и отпускает шину данных. Если слейв услышал и свой адрес и принял его то он прижмет SDA к земле . Так формируется сигнал ASK - принял, все ОК. Если слейв ничего не понял, или его просто там нет то некому будет прижать шину. мастер подождет таймаут и поймет что его не поняли.
  • После передачи адреса, если у нас выставлено направление от мастера к слейву (8 бит адреса равен 1), то мастер передает данные в слейв, не забывая после передачи каждого байта проверять наличие ASK от слейва, ожидая обработки поступившей информации ведомым устройством.
  • При приеме мастером данных от слейва, мастер сам формирует сигнал ASK после приема каждого байта, а слейв контролирует его наличие. Мастер может специально не послать ASK перед отправкой команды STOP , обычно, так давая понять ведомому, что больше предавать данные не нужно.
  • Если после отправки данных мастером (режим записи) необходимо прочитать данные со слейва, то мастер формирует снова сигнал START , отправляя адрес слейва с флагом чтения. (еcли перед командой START не был передан STOP то формируется команда RESTART ). Это используется для смены направления общения мастре-слейв. Например мы передаем слейву адрес регистра, а потом читаем из него данные.)
  • По окончанию работы со слейвом мастер формирует сигнал STOP - при высоком уровне тактирующего сигнала формирует переход шины данных с 0 в 1.
В STM 32 есть аппаратно реализованные приемопередатчики i2c шины. Таких модулей в МК может быть 2 или 3. Для их конфигурации используются специальные регистры, описанные в референсе к используемому МК.

В MicroC перед использованием i2c (как впрочем и любой периферии) ее необходимо должным образом проинициализировать. Для этого используем такую функцию (Иннициализация в качестве мастера):

I2Cn_Init_Advanced(unsigned long: I2C_ClockSpeed, const Module_Struct *module);

  • n - номер используемого модуля, например I2C1 или I2C2 .
  • I2C_ClockSpeed - скорость работы шины, 100000 (100 kbs, стандартный режим) или 400000 (400 kbs, быстрый режим). Второй в 4 раза быстрее, но его поддерживают не все устройства
  • *module - указатель на периферийный модуль, например &_GPIO_MODULE_I2C1_PB67 , здесь не забываем что Code Assistant (ctrl-пробел ) очень помогает.
Для начала проверим свободность шины, для этого существует функция I2Cn_Is_Idle(); возвращающая 1 если шина свободна, и 0 если по ней идет обмен.

I2Cn_Start();
где n - номер используемого модуля i2c нашего микроконтроллера. Функция вернет 0 если на шине возникла ошибка и 1 если все ОК.

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

I2Cn_Write(unsigned char slave_address, unsigned char *buf, unsigned long count, unsigned long END_mode);

  • n - номер используемого модуля
  • slave_address - 7-битный адрес слейва.
  • *buf - указатель на наши данные - байт или массив байтов.
  • count - количество передаваемых байт данных.
  • END_mode - что делать после передачи данных слейву, END_MODE_STOP - передать сигнал STOP , либо END_MODE_RESTART снова отправить START , сформировав сигнал RESTART и дав понять ведомству, что сеанс работы с ним не окончен и с него сейчас будут читать данные.
Для чтения данных со слейва используется функция:

I2Cn_Read(char slave_address, char *ptrdata, unsigned long count, unsigned long END_mode);

  • n - номер используемого модуля
  • slave_address - 7-битный адрес слейва.
  • *buf - указатель на переменную или массив в который мы принимаем данные, тип char или short int
  • count - количество принимаемых байт данных.
  • END_mode - что делать после приема данных от слейва - END_MODE_STOP - передать сигнал STOP , либо END_MODE_RESTART отправить сигнал RESTART .
Давайте попробуем что-то подключить к нашему МК. Для начала: распостраненную микросхему PCF8574(A) представляющего собой расширитель портов ввода вывода с управлением по шине i2c. Данная микросхема содержит всего один внутренний регистр, являющийся ее физическим портом ввода-вывода. Тоесть если ей передать байт, он тут-же выставится на ее выводы. Если считать с нее байт (Передать START адрес с флагом чтения, сигнал RESTERT, прочитать данные и в конце сформировать сигнал STOP ) то он отразит логические состояния на ее выводах. Подключим нашу микросхему в соответствии с даташитом:


Адрес микросхемы формируется из состояния выводов A0, А1, А2 . Для микросхемы PCF8574 адрес будет: 0100A0A1A2 . (Например у нас A0, А1, А2 имеют высокий уровень, соответственно адрес нашей микросхемы будет 0b0100111 = 0x27). Для PCF8574A - 0111A0A1A2 , что с нашей схемой подключения даст адрес 0b0111111 = 0x3F . Если, допустим A2 соединить с землей, то адрес для PCF8574A будет 0x3B . Итого на одну шину i2c можно одновременно повесить 16 микросхем, по 8 PCF8574A и PCF8574.

Давайте попробуем что-то передать иннициализировать i2c шину и что-то передать нашей PCF8574.

#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574 void I2C_PCF8574_WriteReg(unsigned char wData) { I2C1_Start(); // Формируем сигнал START I2C1_Write(PCF8574A_ADDR,&wData, 1, END_MODE_STOP); // Передаем 1 байт данных и формируем сигнал STOP } char PCF8574A_reg; // переменная которую мы пишем в PCF8574 void main () { I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); // Запускаем I2C delay_ms(25); // Немного подождем PCF8574A_reg.b0 = 0; //зажжем первый светодиод PCF8574A_reg.b1 = 1; // погасим второй светодиод while (1) { delay_ms(500); PCF8574A_reg.b0 = ~PCF8574A_reg.b0; PCF8574A_reg.b1 = ~PCF8574A_reg.b1; //инвертируем состояние светодиодов I2C_PCF8574_WriteReg (PCF8574A_reg); //передадим нашей PCF8574 данные } }
Компилируем и запускаем нашу программу и видим что наши светодиоды попеременно моргают.
Я не просто так подключил светодиоды катодом к нашей PCF8574. Все дело в том, что микросхема при подачи на выход логического 0 честно притягивает свой вывод к земле, а вот при подаче логической 1 подключает его к + питания через источник тока в 100 мкА. Тоесть «честной» логической 1 на выходе не получить. И светодиод от 100 мкА не зажечь. Сделано это для того, чтобы без дополнительных регистров настраивать вывод PCF8574 на вход. Мы просто пишем в выходной регистр 1 (фактически устанавливаем состояния ножки в Vdd) и можем просто коротить его на землю. Источник тока не даст «сгореть» выходному каскаду нашего расширителя ввода/вывода. Если ножка притянута к земле, то на ней потенциал земли, и читается логический 0. Если ножка притянута к +, то читается логическая 1. С одной стороны просто, но с другой, про это всегда нужно помнить, работая с данными микросхемами.


Давайте попробуем прочитать состояние выводов нашей микросхемы-расширителя.

#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574 void I2C_PCF8574_WriteReg(unsigned char wData) { I2C1_Start(); // Формируем сигнал START I2C1_Write(PCF8574A_ADDR, &wData, 1, END_MODE_STOP); // Передаем 1 байт данных и формируем сигнал STOP } void I2C_PCF8574_ReadReg(unsigned char rData) { I2C1_Start(); // Формируем сигнал START I2C1_Read(PCF8574A_ADDR, &rData, 1, END_MODE_STOP); // Читаем 1 байт данных и формируем сигнал STOP } char PCF8574A_reg; //переменная которую мы пишем в PCF8574 char PCF8574A_out; // переменная в которую мы читаем и PCF8574 char lad_state; //включен либо выключен наш светодиод void main () { I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); // Запускаем I2C delay_ms(25); // Немного подождем PCF8574A_reg.b0 = 0; // зажжем первый светодиод PCF8574A_reg.b1 = 1; // погасим второй светодиод PCF8574A_reg.b6 = 1; // Притяним выводы 6 и 7 к питанию. PCF8574A_reg.b7 = 1; while (1) { delay_ms(100); I2C_PCF8574_WriteReg (PCF8574A_reg); // пишем данные в РCF8574 I2C_PCF8574_ReadReg (PCF8574A_out); // читаем из РCF8574 if (~PCF8574A_out.b6) PCF8574A_reg.b0 = ~PCF8574A_reg.b0; // Если нажата 1 кнопка (6 бит прочитанного байта из РCF8574 равен 0, то включим/выключим наш светодиод) if (~PCF8574A_out.b7) PCF8574A_reg.b1 = ~PCF8574A_reg.b1; // аналогично для 2 кнопки и 2 светодиода } }
Теперь нажимая на кнопочки мы включаем или отключаем наш светодиод. У микросхемы есть еще вывод INT . На нем формируется импульс каждый раз, когда меняется состояние выводов нашего расширителя ввода/вывода. Подключив его в входу внешнего прерывания нашего МК (как настроить внешние прерывания и как с ними работать я расскажу в одной из следующих статей).

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


Даташит на него и контроллер HD44780 можно с легкостью найти в Интернете. Подключим наш дисплей к РCF8574, а ее, соответственно к нашему STM32.

HD44780 использует параллельный стробируемый интерфейс. Данные передаются по 8 (за один такт) либо 4 (за 2 такта) стробирующего импульса на выводе E . (читаются контроллером дисплея по нисходящему фронту, переходу с 1 в 0). Вывод RS указывает передаем ли мы нашему дисплею данные (RS = 1 ) (символы которые он должен отобразить, фактически из ASCII коды) либо команды (RS = 0 ). RW указывает направление передачи данных, запись либо чтение. Обычно мы пишем данные в дисплей, поэтому (RW = 0 ). Резистор R6 управляет контрастностью дисплея. Просто подключать вход регулировке контрастности к земле или питанию нельзя, иначе ничего не увидите. . VT1 служит для включения и выключения подсветки дисплея по командам МК. В MicroC есть библиотека для работе с такими дисплеями по параллельному интерфейсу, но обычно, тратить на дисплей 8 ног накладно, поэтому я практически всегда использую РCF8574 для работы с такими экранчиками. (Если кому-то будет интересно, то напишу статью про работу с дисплеями на базе HD44780 встроенными в MicroC по параллельному интерфейсу.) Протокол обмена не особо сложный (мы будем использовать 4 линии данных и передавать информацию за 2 такта), его наглядно показывает следующая временная диаграмма:


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

  • 0x28 - связь с индикатором по 4 линиям
  • 0x0C - включаем вывод изображения, отключаем отображение курсора
  • 0x0E - включаем вывод изображения, включаем отображение курсора
  • 0x01 - очищаем индикатор
  • 0x08 - отключаем вывод изображения
  • 0x06 - после вывода символа курсор сдвигается на 1 знакоместо
Так как нам будет нужно достаточно часто работать с данным индикатором то создадим подключаемую библиотеку «i2c_lcd.h» . Для этого в Project Maneger Header Files и выберем Add New File . Создадим наш заголовочный файл.

#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574 #define DB4 b4 // Соответствие выводов PCF8574 и индикатора #define DB5 b5 #define DB6 b6 #define DB7 b7 #define EN b3 #define RW b2 #define RS b1 #define BL b0 //управление подсветкой #define displenth 20 // количество символов в строке нашего дисплея static unsigned char BL_status; // переменная хранящая состояние подсветки (вкл/выкл) void lcd_I2C_Init(void); // Функция иннициализации дисплея и PCF8574 void lcd_I2C_txt(char *pnt); // Выводит на экран строку текста, параметр - указатель на эту строку void lcd_I2C_int(int pnt); // Выводит на экран значение целочисленной переменной, параметр - выводимое значение void lcd_I2C_Goto(unsigned short row, unsigned short col); // перемещает курсор на указанную позицию, параметры row - строка (от 1 до 2 или 4 в зависимости от дисплея) и col - (от 1 до displenth)) void lcd_I2C_cls(); // Очищает экран void lcd_I2C_backlight (unsigned short int state); // Включает (при передаче 1 и отключает - при передаче 0 подсветку дисплея)
Теперь опишем наши фунции, снова идем в Project Maneger клацнем правой кнопкой по папке Sources и выберем Add New File . Создаем файл «i2c_lcd.с» .

#include "i2c_lcd.h" //инклудим наш хедер-файл char lcd_reg; //регистр временного хранения данных отправляемых в PCF8574 void I2C_PCF8574_WriteReg(unsigned char wData) //функция отпарвки данных по i2c в чип PCF8574 { I2C1_Start(); I2C1_Write(PCF8574A_ADDR,&wData, 1, END_MODE_STOP); } void LCD_COMMAND (char com) //функция отправки команды нашему дисплею { lcd_reg = 0; //пишем 0 во временный регистр lcd_reg.BL = BL_status.b0; //пин подсветки выставляем в соответстви со значением переменной, хранящей состояние подсветки lcd_reg.DB4 = com.b4; //выставляем на шину данных индикатора 4 старших бита нащей команды lcd_reg.DB5 = com.b5; lcd_reg.DB6 = com.b6; lcd_reg.DB7 = com.b7; lcd_reg.EN = 1; //ставим строб. вывод в 1 I2C_PCF8574_WriteReg (lcd_reg); //пишем в регистр PCF8574, фактически отправив данные на индикатор delay_us (300); //ждем тайммаут lcd_reg.EN = 0; //сбрасываем строб импульс в 0, индикатор читает данные I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg = 0; lcd_reg.BL = BL_status.b0; lcd_reg.DB4 = com.b0; //то же самое для 4 младших бит lcd_reg.DB5 = com.b1; lcd_reg.DB6 = com.b2; lcd_reg.DB7 = com.b3; lcd_reg.EN = 1; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg.EN = 0; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); } void LCD_CHAR (unsigned char com) //отправка индикатору данных (ASCII кода символа) { lcd_reg = 0; lcd_reg.BL = BL_status.b0; lcd_reg.EN = 1; lcd_reg.RS = 1; //отправка символа отличается от отправки команды установкой в 1 бита RS lcd_reg.DB4 = com.b4; //выставляем на входах 4 старших бита lcd_reg.DB5 = com.b5; lcd_reg.DB6 = com.b6; lcd_reg.DB7 = com.b7; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg.EN = 0; //сбрасываем строб. импульс в 0, индикатор читает данные I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg = 0; lcd_reg.BL = BL_status.b0; lcd_reg.EN = 1; lcd_reg.RS = 1; lcd_reg.DB4 = com.b0; //выставляем на входах 4 младших бита lcd_reg.DB5 = com.b1; lcd_reg.DB6 = com.b2; lcd_reg.DB7 = com.b3; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg.EN = 0; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); } void lcd_I2C_Init(void) { I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); //иннициализируем наш I2c модуль у МК delay_ms(200); lcd_Command(0x28); // Дисплей в режиме 4 бита за такт delay_ms (5); lcd_Command(0x08); //Отключаем вывод данных на дисплей delay_ms (5); lcd_Command(0x01); //Очищаем дисплей delay_ms (5); lcd_Command(0x06); //Включаем автоматический сдвиг курсора после вывода символа delay_ms (5); lcd_Command(0x0C); //Включаем отображение информации без отображения курсора delay_ms (25); } void lcd_I2C_txt(char *pnt) //Вывод строки символов на дисплей { unsigned short int i; //временная переменная индекса масисва символов char tmp_str; //временный массив символов, длиной на 1 больше длинны строки дисплея, так как строку нужно закончить сиv символом NULL ASCII 0x00 strncpy(tmp_str, pnt, displenth); //копируем в нашу временную строку не более displenth символов исходной строки for (i=0; i Теперь подключим только что созданную библиотеку у файлу с нашей главной функцией:

#include "i2c_lcd.h" //инклудим наш хедер-файл unsigned int i; //временная переменная счетчик void main() { lcd_I2C_Init(); //иннициализируем дисплей lcd_I2C_backlight (1); //включим подсветку lcd_I2C_txt ("Hellow habrahabr"); //выведем на дисплей стрроку while (1) { delay_ms(1000); lcd_I2C_Goto (2,1); //перейдем к 1 символу 2 строки lcd_i2c_int (i); //выведем значение на дисплей i++; // инкриментируем счетчик } }

Если все правильно собрано то мы должны увидеть на индикаторе текст и инкриметирующийся каждую секунду счетчик. В общем, ничего сложного:)

В следующей статье мы продолжем разбиратся с i2c протоколом и устройствами работающем с ним. Рассмотрим работу с EEPROM 24XX памятью и акселерометром/гироскопом MPU6050.

Шина 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

В статье приведено описание последовательного интерфейса 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 является простота подключения устройств с помощью всего лишь двух линий связи и общего провода, благодаря чему данный интерфейс надёжно закрепился в технике и по-прежнему широко применяется в современной аппаратуре.

В последнее время все чаще натыкаюсь на негативные отзывы о шине 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; }

kselltrum 20 февраля 2017 в 01:17

Первые шаги с STM32 и компилятором mikroC для ARM архитектуры - Часть 4 - I2C, pcf8574 и подключение LCD на базе HD4478

  • Программирование микроконтроллеров

Следующую статью я хочу посвятить работе с распространенным интерфейсом i2c, достаточно часто используемом в разнообразных микросхемах, подключаемых к микроконтроллеру.

I2C представляет собой шину работающую по двум физическим соединениям (помимо общего провода). Достаточно много о ней расписано в Интернете, неплохие статьи есть в Википедии . Кроме того алгоритм работы шины очень понятно описан . В вкратце, шина представят собой двухпроводную синхронную шину. На шине может одновременно находится до 127 устройств (адрес устройства 7-битный, к этому вернемся далее). Ниже приведена типичная схема подключения устройств к i2c шине, с МК в качестве ведущего устройства.


Для i2c все устройства (как мастер так и слейвы) используют open-drain выходы. Проще говоря они могут притягивать шину ТОЛЬКО К ЗЕМЛЕ. Высокий уровень ша шине обеспечивается подтягивающими резисторами. Номинал этих резисторов обычно выбирается в диапазоне от 4,7 до 10 кОм. i2c достаточна чувствительна к физическим линиям, соединяющим устройства, поэто если используется соединение с большой емкостью (например длинный тонкий или экранированный кабель), влияние этой емкости может «размыть» фронты сигналов и помешать нормальной работе шины. Чем меньше подтягивающий резистор, тем меньше влияет эта емкость на характеристику фронтов сигнала, но ТЕМ БОЛЬШЕ НАГРУЗКА на выходные транзисторы на интерфейсах i2c. Значение этих резисторов подбирается для каждой конкретной реализации, но они не должны быть меньше 2,2 кОмов, иначе можно просто спалить выходные транзисторы в устройствах, работающих с шиной.

Шина состоит из двух линий: SDA (линии данных) и SCL (тактирующего сигнала). Тактирует шину Мастер устройство , обычно наш МК. Когда на SCL высокий уровень информация считывается с шины данных. Изменять состояние SDA можно только при низком уровне тактирующего сигнала . При высоком уровне SCL сигнал на SDAизменяется при формировании сигналов START (при высоком уровне SCL сигнал на SDA изменяется высокого на низкий) и STOP - при высоком уровне SCL сигнал на SDA изменяется с низкого на высокий).

Отдельно следует сказать, что в i2c адрес задается 7-битным числом. 8 - младший бит указывает направление передачи данных 0 - означает что слейв будет передавать данные, 1 - принимать. . Вкратце алгоритм работы с i2c такой:

  • Высокий уроень на SDA и SCL - шина свободна, можно начинать работу
  • Мастер поднимает SCL в 1, и изменяет состояние SDA c 1 на 0 - притягивает его к земле - формируется сигнал START
  • Мастер передает 7-битный адрес слейва с битом направления (данные на SDA выставляются когда SCL притянут к земле, и читаются слейвом когда он отпущен). Если слейв не успевает «схавать» предыдущий бит, он притягивает SCL к земле, давая понять мастеру что состаяние шинны данных не нужно менять: «еще читаю предыдущий». После того как мастер отпустил шину он проверяет, отпустил ли ее слейв .
  • После передачи 8 бит адреса мастер генерирует 9-й такт и отпускает шину данных. Если слейв услышал и свой адрес и принял его то он прижмет SDA к земле . Так формируется сигнал ASK - принял, все ОК. Если слейв ничего не понял, или его просто там нет то некому будет прижать шину. мастер подождет таймаут и поймет что его не поняли.
  • После передачи адреса, если у нас выставлено направление от мастера к слейву (8 бит адреса равен 1), то мастер передает данные в слейв, не забывая после передачи каждого байта проверять наличие ASK от слейва, ожидая обработки поступившей информации ведомым устройством.
  • При приеме мастером данных от слейва, мастер сам формирует сигнал ASK после приема каждого байта, а слейв контролирует его наличие. Мастер может специально не послать ASK перед отправкой команды STOP , обычно, так давая понять ведомому, что больше предавать данные не нужно.
  • Если после отправки данных мастером (режим записи) необходимо прочитать данные со слейва, то мастер формирует снова сигнал START , отправляя адрес слейва с флагом чтения. (еcли перед командой START не был передан STOP то формируется команда RESTART ). Это используется для смены направления общения мастре-слейв. Например мы передаем слейву адрес регистра, а потом читаем из него данные.)
  • По окончанию работы со слейвом мастер формирует сигнал STOP - при высоком уровне тактирующего сигнала формирует переход шины данных с 0 в 1.
В STM 32 есть аппаратно реализованные приемопередатчики i2c шины. Таких модулей в МК может быть 2 или 3. Для их конфигурации используются специальные регистры, описанные в референсе к используемому МК.

В MicroC перед использованием i2c (как впрочем и любой периферии) ее необходимо должным образом проинициализировать. Для этого используем такую функцию (Иннициализация в качестве мастера):

I2Cn_Init_Advanced(unsigned long: I2C_ClockSpeed, const Module_Struct *module);

  • n - номер используемого модуля, например I2C1 или I2C2 .
  • I2C_ClockSpeed - скорость работы шины, 100000 (100 kbs, стандартный режим) или 400000 (400 kbs, быстрый режим). Второй в 4 раза быстрее, но его поддерживают не все устройства
  • *module - указатель на периферийный модуль, например &_GPIO_MODULE_I2C1_PB67 , здесь не забываем что Code Assistant (ctrl-пробел ) очень помогает.
Для начала проверим свободность шины, для этого существует функция I2Cn_Is_Idle(); возвращающая 1 если шина свободна, и 0 если по ней идет обмен.

I2Cn_Start();
где n - номер используемого модуля i2c нашего микроконтроллера. Функция вернет 0 если на шине возникла ошибка и 1 если все ОК.

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

I2Cn_Write(unsigned char slave_address, unsigned char *buf, unsigned long count, unsigned long END_mode);

  • n - номер используемого модуля
  • slave_address - 7-битный адрес слейва.
  • *buf - указатель на наши данные - байт или массив байтов.
  • count - количество передаваемых байт данных.
  • END_mode - что делать после передачи данных слейву, END_MODE_STOP - передать сигнал STOP , либо END_MODE_RESTART снова отправить START , сформировав сигнал RESTART и дав понять ведомству, что сеанс работы с ним не окончен и с него сейчас будут читать данные.
Для чтения данных со слейва используется функция:

I2Cn_Read(char slave_address, char *ptrdata, unsigned long count, unsigned long END_mode);

  • n - номер используемого модуля
  • slave_address - 7-битный адрес слейва.
  • *buf - указатель на переменную или массив в который мы принимаем данные, тип char или short int
  • count - количество принимаемых байт данных.
  • END_mode - что делать после приема данных от слейва - END_MODE_STOP - передать сигнал STOP , либо END_MODE_RESTART отправить сигнал RESTART .
Давайте попробуем что-то подключить к нашему МК. Для начала: распостраненную микросхему PCF8574(A) представляющего собой расширитель портов ввода вывода с управлением по шине i2c. Данная микросхема содержит всего один внутренний регистр, являющийся ее физическим портом ввода-вывода. Тоесть если ей передать байт, он тут-же выставится на ее выводы. Если считать с нее байт (Передать START адрес с флагом чтения, сигнал RESTERT, прочитать данные и в конце сформировать сигнал STOP ) то он отразит логические состояния на ее выводах. Подключим нашу микросхему в соответствии с даташитом:


Адрес микросхемы формируется из состояния выводов A0, А1, А2 . Для микросхемы PCF8574 адрес будет: 0100A0A1A2 . (Например у нас A0, А1, А2 имеют высокий уровень, соответственно адрес нашей микросхемы будет 0b0100111 = 0x27). Для PCF8574A - 0111A0A1A2 , что с нашей схемой подключения даст адрес 0b0111111 = 0x3F . Если, допустим A2 соединить с землей, то адрес для PCF8574A будет 0x3B . Итого на одну шину i2c можно одновременно повесить 16 микросхем, по 8 PCF8574A и PCF8574.

Давайте попробуем что-то передать иннициализировать i2c шину и что-то передать нашей PCF8574.

#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574 void I2C_PCF8574_WriteReg(unsigned char wData) { I2C1_Start(); // Формируем сигнал START I2C1_Write(PCF8574A_ADDR,&wData, 1, END_MODE_STOP); // Передаем 1 байт данных и формируем сигнал STOP } char PCF8574A_reg; // переменная которую мы пишем в PCF8574 void main () { I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); // Запускаем I2C delay_ms(25); // Немного подождем PCF8574A_reg.b0 = 0; //зажжем первый светодиод PCF8574A_reg.b1 = 1; // погасим второй светодиод while (1) { delay_ms(500); PCF8574A_reg.b0 = ~PCF8574A_reg.b0; PCF8574A_reg.b1 = ~PCF8574A_reg.b1; //инвертируем состояние светодиодов I2C_PCF8574_WriteReg (PCF8574A_reg); //передадим нашей PCF8574 данные } }
Компилируем и запускаем нашу программу и видим что наши светодиоды попеременно моргают.
Я не просто так подключил светодиоды катодом к нашей PCF8574. Все дело в том, что микросхема при подачи на выход логического 0 честно притягивает свой вывод к земле, а вот при подаче логической 1 подключает его к + питания через источник тока в 100 мкА. Тоесть «честной» логической 1 на выходе не получить. И светодиод от 100 мкА не зажечь. Сделано это для того, чтобы без дополнительных регистров настраивать вывод PCF8574 на вход. Мы просто пишем в выходной регистр 1 (фактически устанавливаем состояния ножки в Vdd) и можем просто коротить его на землю. Источник тока не даст «сгореть» выходному каскаду нашего расширителя ввода/вывода. Если ножка притянута к земле, то на ней потенциал земли, и читается логический 0. Если ножка притянута к +, то читается логическая 1. С одной стороны просто, но с другой, про это всегда нужно помнить, работая с данными микросхемами.


Давайте попробуем прочитать состояние выводов нашей микросхемы-расширителя.

#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574 void I2C_PCF8574_WriteReg(unsigned char wData) { I2C1_Start(); // Формируем сигнал START I2C1_Write(PCF8574A_ADDR, &wData, 1, END_MODE_STOP); // Передаем 1 байт данных и формируем сигнал STOP } void I2C_PCF8574_ReadReg(unsigned char rData) { I2C1_Start(); // Формируем сигнал START I2C1_Read(PCF8574A_ADDR, &rData, 1, END_MODE_STOP); // Читаем 1 байт данных и формируем сигнал STOP } char PCF8574A_reg; //переменная которую мы пишем в PCF8574 char PCF8574A_out; // переменная в которую мы читаем и PCF8574 char lad_state; //включен либо выключен наш светодиод void main () { I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); // Запускаем I2C delay_ms(25); // Немного подождем PCF8574A_reg.b0 = 0; // зажжем первый светодиод PCF8574A_reg.b1 = 1; // погасим второй светодиод PCF8574A_reg.b6 = 1; // Притяним выводы 6 и 7 к питанию. PCF8574A_reg.b7 = 1; while (1) { delay_ms(100); I2C_PCF8574_WriteReg (PCF8574A_reg); // пишем данные в РCF8574 I2C_PCF8574_ReadReg (PCF8574A_out); // читаем из РCF8574 if (~PCF8574A_out.b6) PCF8574A_reg.b0 = ~PCF8574A_reg.b0; // Если нажата 1 кнопка (6 бит прочитанного байта из РCF8574 равен 0, то включим/выключим наш светодиод) if (~PCF8574A_out.b7) PCF8574A_reg.b1 = ~PCF8574A_reg.b1; // аналогично для 2 кнопки и 2 светодиода } }
Теперь нажимая на кнопочки мы включаем или отключаем наш светодиод. У микросхемы есть еще вывод INT . На нем формируется импульс каждый раз, когда меняется состояние выводов нашего расширителя ввода/вывода. Подключив его в входу внешнего прерывания нашего МК (как настроить внешние прерывания и как с ними работать я расскажу в одной из следующих статей).

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


Даташит на него и контроллер HD44780 можно с легкостью найти в Интернете. Подключим наш дисплей к РCF8574, а ее, соответственно к нашему STM32.

HD44780 использует параллельный стробируемый интерфейс. Данные передаются по 8 (за один такт) либо 4 (за 2 такта) стробирующего импульса на выводе E . (читаются контроллером дисплея по нисходящему фронту, переходу с 1 в 0). Вывод RS указывает передаем ли мы нашему дисплею данные (RS = 1 ) (символы которые он должен отобразить, фактически из ASCII коды) либо команды (RS = 0 ). RW указывает направление передачи данных, запись либо чтение. Обычно мы пишем данные в дисплей, поэтому (RW = 0 ). Резистор R6 управляет контрастностью дисплея. Просто подключать вход регулировке контрастности к земле или питанию нельзя, иначе ничего не увидите. . VT1 служит для включения и выключения подсветки дисплея по командам МК. В MicroC есть библиотека для работе с такими дисплеями по параллельному интерфейсу, но обычно, тратить на дисплей 8 ног накладно, поэтому я практически всегда использую РCF8574 для работы с такими экранчиками. (Если кому-то будет интересно, то напишу статью про работу с дисплеями на базе HD44780 встроенными в MicroC по параллельному интерфейсу.) Протокол обмена не особо сложный (мы будем использовать 4 линии данных и передавать информацию за 2 такта), его наглядно показывает следующая временная диаграмма:


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

  • 0x28 - связь с индикатором по 4 линиям
  • 0x0C - включаем вывод изображения, отключаем отображение курсора
  • 0x0E - включаем вывод изображения, включаем отображение курсора
  • 0x01 - очищаем индикатор
  • 0x08 - отключаем вывод изображения
  • 0x06 - после вывода символа курсор сдвигается на 1 знакоместо
Так как нам будет нужно достаточно часто работать с данным индикатором то создадим подключаемую библиотеку «i2c_lcd.h» . Для этого в Project Maneger Header Files и выберем Add New File . Создадим наш заголовочный файл.

#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574 #define DB4 b4 // Соответствие выводов PCF8574 и индикатора #define DB5 b5 #define DB6 b6 #define DB7 b7 #define EN b3 #define RW b2 #define RS b1 #define BL b0 //управление подсветкой #define displenth 20 // количество символов в строке нашего дисплея static unsigned char BL_status; // переменная хранящая состояние подсветки (вкл/выкл) void lcd_I2C_Init(void); // Функция иннициализации дисплея и PCF8574 void lcd_I2C_txt(char *pnt); // Выводит на экран строку текста, параметр - указатель на эту строку void lcd_I2C_int(int pnt); // Выводит на экран значение целочисленной переменной, параметр - выводимое значение void lcd_I2C_Goto(unsigned short row, unsigned short col); // перемещает курсор на указанную позицию, параметры row - строка (от 1 до 2 или 4 в зависимости от дисплея) и col - (от 1 до displenth)) void lcd_I2C_cls(); // Очищает экран void lcd_I2C_backlight (unsigned short int state); // Включает (при передаче 1 и отключает - при передаче 0 подсветку дисплея)
Теперь опишем наши фунции, снова идем в Project Maneger клацнем правой кнопкой по папке Sources и выберем Add New File . Создаем файл «i2c_lcd.с» .

#include "i2c_lcd.h" //инклудим наш хедер-файл char lcd_reg; //регистр временного хранения данных отправляемых в PCF8574 void I2C_PCF8574_WriteReg(unsigned char wData) //функция отпарвки данных по i2c в чип PCF8574 { I2C1_Start(); I2C1_Write(PCF8574A_ADDR,&wData, 1, END_MODE_STOP); } void LCD_COMMAND (char com) //функция отправки команды нашему дисплею { lcd_reg = 0; //пишем 0 во временный регистр lcd_reg.BL = BL_status.b0; //пин подсветки выставляем в соответстви со значением переменной, хранящей состояние подсветки lcd_reg.DB4 = com.b4; //выставляем на шину данных индикатора 4 старших бита нащей команды lcd_reg.DB5 = com.b5; lcd_reg.DB6 = com.b6; lcd_reg.DB7 = com.b7; lcd_reg.EN = 1; //ставим строб. вывод в 1 I2C_PCF8574_WriteReg (lcd_reg); //пишем в регистр PCF8574, фактически отправив данные на индикатор delay_us (300); //ждем тайммаут lcd_reg.EN = 0; //сбрасываем строб импульс в 0, индикатор читает данные I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg = 0; lcd_reg.BL = BL_status.b0; lcd_reg.DB4 = com.b0; //то же самое для 4 младших бит lcd_reg.DB5 = com.b1; lcd_reg.DB6 = com.b2; lcd_reg.DB7 = com.b3; lcd_reg.EN = 1; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg.EN = 0; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); } void LCD_CHAR (unsigned char com) //отправка индикатору данных (ASCII кода символа) { lcd_reg = 0; lcd_reg.BL = BL_status.b0; lcd_reg.EN = 1; lcd_reg.RS = 1; //отправка символа отличается от отправки команды установкой в 1 бита RS lcd_reg.DB4 = com.b4; //выставляем на входах 4 старших бита lcd_reg.DB5 = com.b5; lcd_reg.DB6 = com.b6; lcd_reg.DB7 = com.b7; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg.EN = 0; //сбрасываем строб. импульс в 0, индикатор читает данные I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg = 0; lcd_reg.BL = BL_status.b0; lcd_reg.EN = 1; lcd_reg.RS = 1; lcd_reg.DB4 = com.b0; //выставляем на входах 4 младших бита lcd_reg.DB5 = com.b1; lcd_reg.DB6 = com.b2; lcd_reg.DB7 = com.b3; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); lcd_reg.EN = 0; I2C_PCF8574_WriteReg (lcd_reg); delay_us (300); } void lcd_I2C_Init(void) { I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); //иннициализируем наш I2c модуль у МК delay_ms(200); lcd_Command(0x28); // Дисплей в режиме 4 бита за такт delay_ms (5); lcd_Command(0x08); //Отключаем вывод данных на дисплей delay_ms (5); lcd_Command(0x01); //Очищаем дисплей delay_ms (5); lcd_Command(0x06); //Включаем автоматический сдвиг курсора после вывода символа delay_ms (5); lcd_Command(0x0C); //Включаем отображение информации без отображения курсора delay_ms (25); } void lcd_I2C_txt(char *pnt) //Вывод строки символов на дисплей { unsigned short int i; //временная переменная индекса масисва символов char tmp_str; //временный массив символов, длиной на 1 больше длинны строки дисплея, так как строку нужно закончить сиv символом NULL ASCII 0x00 strncpy(tmp_str, pnt, displenth); //копируем в нашу временную строку не более displenth символов исходной строки for (i=0; i Теперь подключим только что созданную библиотеку у файлу с нашей главной функцией:

#include "i2c_lcd.h" //инклудим наш хедер-файл unsigned int i; //временная переменная счетчик void main() { lcd_I2C_Init(); //иннициализируем дисплей lcd_I2C_backlight (1); //включим подсветку lcd_I2C_txt ("Hellow habrahabr"); //выведем на дисплей стрроку while (1) { delay_ms(1000); lcd_I2C_Goto (2,1); //перейдем к 1 символу 2 строки lcd_i2c_int (i); //выведем значение на дисплей i++; // инкриментируем счетчик } }

Если все правильно собрано то мы должны увидеть на индикаторе текст и инкриметирующийся каждую секунду счетчик. В общем, ничего сложного:)

В следующей статье мы продолжем разбиратся с i2c протоколом и устройствами работающем с ним. Рассмотрим работу с EEPROM 24XX памятью и акселерометром/гироскопом MPU6050.