Статья про I2С должна была появиться уже давно. Не смотря на кажущуюся простоту работы интерфейса, охватить все оказалось довольно непросто, поэтому подходящей фазы луны так и не было чтобы собрать все в кучу 🙂 Ситуацию решил исправить iEugene0x7CA и внести свет науки в массы, я лишь поправил стиль изложения.
Вначале нужно познакомить читателя с тем, о чем дальше пойдет речь. Для обмена данными с периферией существуют различные интерфейсы, например мы уже ранее рассматривали SPI. Его недостаток — для обмена данными используются четыре ножки, если задействовано несколько устройств, то ножку CS — chip select нужно тянуть для каждого устройства отдельную. Таким образом, если используются три внешних устройства, то у микроконтроллера будет задействовано шесть ножек.
Ножки это важный ресурс и высокая скорость, которой обладает SPI не всегда нужна, поэтому альтернативой является интерфейс I2C, который работает со скоростью до 400кГц и обладает 7 битным адресным пространством и теоретически позволяет подключать до 128 ведомых устройств. Ну и самое главное, для передачи данных используется всего две ножки микроконтроллера и не важно сколько у вас сидит устройств на шине данных 1, 2 или 128.
Сами ножки называются SCL и SDA, по первой подаются тактовые импульсы, по второй передаются данные. Особенностью является то, что обе линии должны быть подтянуты к плюсу питания через резисторы, абсолютно всегда, иначе ничего работать не будет.
Теперь о самом главном, как микроконтроллер узнает с каким устройством он общается? Для этого у каждого устройства есть свой адрес. Адрес можно узнать в даташите на устройство. Кроме того, обычно несколько бит адреса микросхемы позволено выставлять пользователю, дабы тот мог повесить несколько микросхем одного типа на одну I2C шину, например если одной внешней 64 кБ EEPROM не хватило и разработчик решил повесить 8 таких. После отсылки адреса чипа на шину, если микросхема с таковым там была и все подключено верно — именно она будет воспринимать следующие байты, остальные информацию на шине проигнорируют.
В atmega8 есть два варианта передачи данных по протоколу I2c — использование аппаратного модуля TWI(two wire interface) или программного ногодрыга. Программная реализация достаточно проста в использовании, однако имеет недостаток — после отсылки данных микроконтроллером, нужно тупить и ждать пока внешнее устройство ответит, в случае с использования аппаратной шины прием можно организовать в прерывании. Однако, для микроконтроллеров не имеющих на своем борту аппаратного модуля это единственное решение. Кроме того, шину можно повесить на любой порт, что может быть удобно при разводке плат.
Для начального ознакомления мы будем использовать программную библиотеку, в Codevision вкладка визарда называется I2C, а библиотека i2c.h. Функций в библиотеке присутствует не так много: i2c_init() — необходима для инициализации; i2c_start() — инициализирует начало передачи данных, также возвращает 1, если шина свободна и 0, если шина занята; i2c_stop() — окончание передачи; i2c_read() — принять данные; i2c_write() — передать данные.
В функциях приема и передачи есть маленькая хитрость, называется ack — работает очень просто: после завершения приема/передачи 8-ми бит, следующий такт периферия сама подтягивает линию данных к земле, контроллер при этом только «слушает» линию, и если там действительно 0 (т.е. периферия удачно приняла/отдала байт и сигнализирует об этом мастеру), то процесс продолжается. Если нет, то случился какой-то сбой. Чтобы при чтении получать подтверждение приема, отправляем i2c_read(1), если не хотим i2c_read(0).
Одной теорией сыт не будешь, хорошо бы еще научиться конкретно опрашивать периферию! Какой она бывает? Компасы-магнитометры, гироскопы-акселерометры, термометры, датчики влажности, освещенности, атмосферного давления, внешние АЦП, внешнюю EEPROM, аудиопроцессоры, эквалайзеры, экранчики, ИК-дальномеры, и еще много всякого вкусного. В общем понятно думаю, что не умея работать с I2C — вы многое теряете.
Наконец обещанный экшн, протоколы связи из даташита на 24LC512.
Подробно рассмотрим 2 основных фичи, к остальному при желании функции написать будет не проблема. Основные в данном случае- это запись произвольного байта и чтение, тоже произвольного(byte write и random read). Адресация в 24LC512 не страничная, а побайтовая, работать с такой системой очень удобно. Проще всего записывать данные, смотрим сейчас одним глазом на диаграмму из даташита и пытаемся разобраться, что же там происходит:
1. Посылаем на шину команду «старт». На практике это просто подтягивание шины на 1 такт, после этого все подключенные к ней устройства начинают «слушать» следующий байт, указывающий адрес микросхемы, с которой мы хотим работать. Старт- отдельная функция их i2c.h либы;
2. Посылаем байт с адресом.
Как видим, первый бит адресного байта(читается справа-налево) указывает, будем мы работать с 24LC512 в режиме чтения или записи. Запись — это 0. Далее идут 3 адресных бита, которые может выставить пользователь, подтянув ножки 1, 2, и 3 нашей 24LC512 к земле или питанию. Для простоты подтягиваем все к земле. Нужно это, дабы можно было подключить на I2C шину не одну EEPROM этого типа. Следующие 4 бита- это уже заводской адерс конкретно этой микросхемы. Выходит, для обращения к микрухе в режиме записи нужно кинуть на шину число «0b10100000».
3. и 4. Теперь бросаем 2 байта с адерсом того байта, который мы хотим записать. 24LC512 имеет 64 кБ памяти и для адресации каждого из них нам нужно в аккурат 16-битное число(2^16=65536, что с поправкой на тот факт, что килобайт- это 1024 байта дает нам в аккурат 64 кБ).
5. Бросаем собственно то, что хотели записать.
6. Ну, и команда «стоп»!
Все, мы записали один байт в энергонезависимую память! Если посмотрите на функцию записи из исходника тестовой прошивки — вы найдете там эти 6 простых шагов, взятых из даташитной картинки. Аналогично диаграмме из даташита действуем и для чтения данных. По сути все так же, строго идем по даташиту, используя стандартные функции i2c.h. Единственный момент, могущий показаться не очень понятным- это строка «i2c_write(Ext_EEPROM_Adr | 1)», представляющая собой банально добавление к первому биту адреса единички, т.е. обращение к 24LC512 в режиме чтения.
#include <mega8.h> #include <delay.h> // I2C Bus functions #asm .equ __i2c_port=0x15 //Адрес порта C .equ __sda_bit=4 .equ __scl_bit=5 #endasm #include <i2c.h> #include <stdio.h> #define Ext_EEPROM_Adr 0b10100000 // Адрес 24LC512 на I2C шине unsigned char high_byte, low_byte, message[23]={'H','E','L','L','O',',',' ','e','x','t','e','r','n','a','l',' ','E','E','P','R','O','M','!'}; unsigned char eeprom_read(unsigned char address2, unsigned char address1) //Функция чтения из внешней EEPROM { unsigned char data; i2c_start(); //Кидаем команду "Cтарт" на шину I2C i2c_write(Ext_EEPROM_Adr); //Кидаем на шину адрес 24LC512 i2c_write(address2); //Старший байт адресного пространства 24LC512 i2c_write(address1); //Младший байт i2c_start(); //Снова посылаем "старт" в шину i2c_write(Ext_EEPROM_Adr | 1); //Обращаемся к 24LC512 в режиме чтения, т.е. по адресу 101000001 data=i2c_read(0); //Принимаем данные с шины и сохраняем в переменную i2c_stop(); //Посылаем команду "Cтоп" return data; //Возвращаем значение прочитанного } void eeprom_write(unsigned char address2, unsigned char address1, unsigned char data) //Функция записи во внешнюю EEPROM { i2c_start(); //Кидаем команду "Cтарт" на шину I2C i2c_write(Ext_EEPROM_Adr); //Кидаем на шину адрес 24LC512 i2c_write(address2); //Старший байт адресного пространства 24LC512 i2c_write(address1); //Младший байт i2c_write(data); //Посылаем байт для записи i2c_stop(); //Посылаем команду "Стоп" delay_ms(5); //Даем микросхеме время записать данные, EEPROM довольно медлительна } void main(void) { DDRD=0xFF; // I2C Bus initialization i2c_init(); // USART initialization // Communication Parameters: 8 Data, 1 Stop, No Parity // USART Receiver: Off // USART Transmitter: On // USART Mode: Asynchronous // USART Baud Rate: 9600 UCSRA=0x00; UCSRB=0x08; UCSRC=0x86; UBRRH=0x00; UBRRL=0x33; //Записываем для теста короткую строку "HELLO, external EEPROM!" в первые 23 байта внешней EEPROM for(low_byte=0; low_byte<23; low_byte++) { eeprom_write(high_byte, low_byte, message[low_byte]); } //Кидаем в UART содержимое первых 23-х байтов адресного пространства 24LC512 for(low_byte=0; low_byte<23; low_byte++) { putchar(eeprom_read(0x00,low_byte)); delay_ms(100); } while (1); } |
Поздравляшки! Вы только что прошли краткий курс работы с периферией на I2C шине, теперь можете покупать любую цацку с I2C интерфейсом и строить свои функции для игры с таковой!
http://avr-start.ru/?p=5014
Честно говоря мне это несильно помогло 😥
у меня вопрос как учителю .Этот проэкт можно перенести в Atmel Studio7 я имею ввиду основное чтение и запись везде одинаково?
codevision использует свою библиотеку i2c, но честно я не слежу за развитием codevision и atmel studio, насколько я помню codevision стал частью atmel studio
у меня вопрос ? Как вычислить адрес старшого байта и младшего EEPROM
вычислять ничего не нужно, это ячейки куда вы пишите/читаете
тоисть нужно писать адрес ячеики старшего байта и младшего.?Куда мы будем писать информацию?
Хорошо .А где информация об 24LC512 и об ее ячеек?
так называемую таблицу?
24LC512 это просто внешняя память, вы можете в ней хранить что угодно. вся ее фишка в том, что после выключения данные не теряются. чтобы вам охватить 512 байт памяти вам нужно 2 байта для адреса.
Вы извените меня я не совсем русский.
«Хорошо .А где информация об 24LC512 и об ее ячеек?»
В даташите на эту микросхему.
У меня вопрос? если у меня есть число например 1200 как его сохранить ?а библиотека расчитана на 1 байт 255 что делать?
раскладываете на 2 байта и сохраняете отдельно.
а запись делать тоже по байту?
тоже по байту
Добрый день помогите пожалуйста соединить два модуля по шине I2C.
Если их вместе соединить всё работает только секунды не идут застывшие надо как-то опрашивать по очереди и по адресу?
BMP-180
чтения — 0b11101111
DS3231
чтения — 0b11010001
В CodeVision библиотеки то написаны как правильно сделать не пойму .
Очень прошу совета.
в ds3231 есть бит установки старт/стоп
Значит в BMP-180 есть стартовый бит и в ds3231 если их по очереди опрашивать старт и стоп битами то они друг другу мешать не будут.
Вы путаете протокольную часть и транспортную. Устройства опрашиваются по очереди. В ds3231 есть регистр, в нем нужно установить бит. Это не относится к I2C напрямую. У вас мог бы быть и SPI, и UART и любой другой интерфейс, но в любом случае вам нужно было бы через интерфейс достучаться до регистра и установить этот бит. Т.е. вам нужно заюзать протокольную часть. В I2C обычно это делается так: 1. посылаете адрес устройства, 2. посылаете адрес регистра, 3. Посылаете значение, которое нужно записать/прочитать.
Я все в толк не возьму: зачем инициализируется USART?
в коде комментарии есть, содержимое еепром выводится в uart для визуализации
У меня вопрос .Я не программист а экспериментатор.Я написал ваши функции они не работают в железе.Микрочип студия 7 эта среда.А вот написал напрямую всё работает и протеус тоже самое показывает.У меня это вопрос волнует и другой .мне нужно запустить таймер в начале программы в прерывании.Таймер при EEPROM виснет или прерывание не происходит в протеусе так.Помигать светодидом почему.Один раз моргнул и всё.Вот рабочий код EEPROM
// }
if(menu_lcd==0)
{
if (nastroyki.flag_eeprom==1)
{
nastroyki.flag_eeprom=0;
asm(«cli»);
I2C_START();
I2C_SendByte(SLA_WR_EEPROM_ADDR);
I2C_SendByte(0);
I2C_SendByte(1);
I2C_START();
I2C_SendByte(SLA_R_EEPROM_ADDR);
a1= I2C_Readbyte_EEPROM();
a2=I2C_Readlastbyte_EEPROM();
I2C_STOP();
I2C_START();
I2C_SendByte(SLA_WR_EEPROM_ADDR);
I2C_SendByte(2);
I2C_SendByte(3);
I2C_START();
I2C_SendByte(SLA_R_EEPROM_ADDR);
b1=I2C_Readbyte_EEPROM();
b2=I2C_Readlastbyte_EEPROM();
I2C_STOP();
I2C_START();
I2C_SendByte(SLA_WR_EEPROM_ADDR);
I2C_SendByte(4);
I2C_SendByte(5);
I2C_START();
I2C_SendByte(SLA_R_EEPROM_ADDR);
c1=I2C_Readbyte_EEPROM();
c2=I2C_Readlastbyte_EEPROM();
I2C_STOP();
I2C_START();
I2C_SendByte(SLA_WR_EEPROM_ADDR);
I2C_SendByte(6);
I2C_SendByte(7);
I2C_START();
I2C_SendByte(SLA_R_EEPROM_ADDR);
d1=I2C_Readbyte_EEPROM();
d2=I2C_Readlastbyte_EEPROM();
I2C_STOP();
a=((unsigned int )a1<<8)|a2;
b=((unsigned int )b1<<8)|b2;
c=((unsigned int )c1<<8)|c2;
d=((unsigned int )d1<<8)|d2;
}
asm("sei");
не знаю правильно написал или нет но работало в железе.Спазибо за всё за инфу.
Вот так прерывание по таймеру работает Когда отключаю EEPRom потом включаю почему?
Да в железе я не пробовал.Такой трюк.А с EEPROM тоже ведёт себя так тормозит программу?Если так то нужно переписывать код.Ну в AVR есть внутреняя память таких проблем не было.А в STM32 нет её.