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 если по ней идет обмен.
Далее сформируем сигнал START, для чего используем:
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[displenth + 1]; //временный массив символов, длиной на 1 больше длинны строки дисплея, так как строку нужно закончить сиv символом NULL ASCII 0x00
strncpy(tmp_str, pnt, displenth); //копируем в нашу временную строку не более displenth символов исходной строки
for (i=0; i<displenth; i++)
{
if (tmp_str[i] == 0) break; //если обнаружили NULL символ, віходим из цикла
LCD_CHAR(tmp_str[i]); //посимвольно выводим строку на экран
}
}
void lcd_I2C_int(int pnt) //Вывод целого числа на дисплей
{
char tmp_str[8]; //временная строка
unsigned short i, j;
IntToStr(pnt,tmp_str); //преобразовываем число в строку, длинна 6 символов + NULL символ
while (tmp_str[0]==32)
{
for (i=0; i<7; i++)
{
tmp_str[i]=tmp_str[i+1]; //уберем лишние пробелы (ASCII код 32)
tmp_str[6-j]=0;
}
j++;
}
lcd_I2C_txt (tmp_str); //выведем на дисплей строку
}
void lcd_I2C_Goto(unsigned short row, unsigned short col) //переход к заданной позиции дисплея
{
col--; //адрес знакоместа в строке начинается с 0, аргумент функции на 1 больше
switch (row)
{
case 1:
lcd_Command(0x80 + col); //переводим курсор на нужное знакоместо
break;
case 2:
lcd_Command(0x80 + col + 0x40);
break;
case 3:
lcd_Command(0x80 + col + 0x14);
break;
case 4:
lcd_Command(0x80 + col + 0x54);
break;
}
}
void lcd_I2C_cls() //очищаем дисплей
{
lcd_Command(0x01);
delay_ms (5);
}
void lcd_I2C_backlight (unsigned short int state) //включаем либо отключаем подсветку дисплея
{
lcd_reg = 0;
BL_status.b0 = state.b0; //изменяем значение бита, связанного с пином к которому подключен транзистор в цепи подсветки дисплея
lcd_reg.BL = state.b0;
I2C_PCF8574_WriteReg (lcd_reg);
delay_ms (1);
}
Теперь подключим только что созданную библиотеку у файлу с нашей главной функцией:
#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.