Но недавно я прочитал статью на Датагоре «Несколько функций для програмной реализации протокола I2C на AVR», в которой автор выложил свой пример программной реализации протокола под IAR. Я в тот же миг переписал всё в CodeVision, но, к сожалению, у меня опять не заработало как надо.
Решил набраться терпения и разобраться сам.
Содержание статьи / Table Of Contents
↑ Разбираемся с протоколом i2c
Любой протокол можно разобрать по кирпичикам. Так вот, в данном протоколе таким «кирпичиком» является передача/приём одного бита. Схематично передачу одного бита я изобразил на рисунке.На этом рисунке я изобразил поведение линии SCL при передаче одного бита и длиной в один такт, а также пояснил некоторые понятия. С SCL вроде бы всё ясно, надо разобраться с SDA.
Так вот, при записи бита, надо на линии SDA выставлять значение, в соответствии с содержанием этого бита. Причём делать это лучше всего в середине паузы. Сразу появился вот такой код:
// пишем бит в шину
void i2c_wr_bit(char b) {
delay_us(i2c_time/2); // формируем первую половину паузы
if (b) wrsda; else rdsda; // пишем бит...
delay_us(i2c_time/2); // завершаем паузу
rdscl; // выдаем фронт в линию SCL
delay_us(i2c_time); // формируем сам импульс
wrscl; // выдаем спад в линию SCL
}
Далее рассмотрим чтение бита из шины. Нужно точно также протактировать линию SCL и прочитать значение линии SDA сразу после фронта импульса SCL. Вот код:
// читаем бит из шины
char i2c_rd_bit() {
char ret=1; // временая переменная
rdsda; // SDA - на чтение
delay_us(i2c_time); // формируем паузу между импульсами
rdscl; // выдаем фронт SCL
if (i2c_read) ret=0; // читаем SDA
delay_us(i2c_time); // формируем фронт
wrscl; // формируем спад SCL
return ret; // вернули, что прочитали...
}
Теперь пора рассмотреть все дефайны, которых, как известно, мало не бывает:
#include <mega328p.h> // нужно для Атмегушки
#include <delay.h> // нужно для задержек
// задержка в микросекундах (2 задержки на такт передачи)
#define i2c_time 10
//i2c порт
#define sdaprt PORTB.0 // порт для SDA
#define sdaddr DDRB.0 // направлене для SDA
#define sdapin PINB.0 // для чтения SDA
#define sclprt PORTB.1 // порт для SCL
#define sclddr DDRB.1 // направление для SCL
// управление битами шины
#define clrsda sdaprt=0; // переводим в 0 линию SDA
//#define setsda sdaprt=1; // оставил на всякий случай
#define clrscl sclprt=0; // переводим в 0 линию SCL
//#define setscl sclprt=1; // оставил на всякий случай
#define rdscl sclddr=0; // переводим SCL линию на вход
#define wrscl sclddr=1; // переводим SCL линию на выход
#define rdsda sdaddr=0; // переводим SDA линию на вход
#define wrsda sdaddr=1; // переводим SDA линию на выход
#define i2c_read sdapin // для чтения SDA
Далее нужно научиться передавать байт. Тут ничего сложного:
// передача байта на шину
char i2c_wr(char data) {
//Функция возвращает 1, если есть ответ ACK
char i; // счетчик для цикла. Если объявлять глобально, могут быть глюки
for (i=0; i<8; i++) { // выдаем в шину 8 бит
i2c_wr_bit((data&0x80)==0); // отдаем бит
data<<=1; } // двигаем байт
// читаем «акнолидж» (подтверждение приёма)
return i2c_rd_bit(); // вернули, что прочитали
}
Хотелось бы пояснить, что шина i2c устроена так, что при передаче байта мы должны отправить восемь бит данных и потом прочитать бит — так называемый «акнолидж» (ack). Это такой бит подтверждения, посылаемый устройством на шине, который сигнализирует нам, что устройство благополучно «проглотило» наш байт.
При работе с устройствами этот бит, как правило, говорит устройству о конце/продолжении передачи при чтении нескольких байт из него. Так же иногда этот «акнолидж» надо иногда передавать устройству, особенно при чтении данных с него.
С записью байта понятно, разберемся с чтением. Тут ситуация в точности наоборот — мы сначала читаем восемь бит байта, а потом выдаем этот «акнолидж».
Реализация приема байта:
// прием байта с шины
char i2c_rd(char a) {
//если ack=1, то выдается подтверждение ack в шину
char i, data=0; // счетчик и принимаемый байт
for (i=0; i<8; i++) { // цикл приема бит
if (!i2c_rd_bit()) data++; // читаем бит
if(i!=7) data<<=1; } // двигаем байт
i2c_wr_bit(a); // выдаем «акнолидж» (подтверждение приема)
return data; // вернули, что прочли
}
Теперь пропишем пресловутые функции старт/стоп, как требует протокол:
// старт i2c
void i2c_go(void) {
delay_us(i2c_time); // ждем, пока предидущие сигналы устаканятся
rdsda; rdscl; // отпускаем шину
delay_us(i2c_time); // формируем паузу:)
wrsda; clrsda; // давим SDA и ставим 0
delay_us(i2c_time); // задержка...
wrscl; clrscl; // давим SCL и ставим 0
delay_us(i2c_time); // задержка...
}
// стоп i2c
void i2c_end(void) {
wrsda; // давим SDA
delay_us(i2c_time); // ждем..
rdscl; // давим SCL
delay_us(i2c_time); // ждем..
rdsda; // отдаем SDA
}
↑ Проверим код с помощью записи/считывания AT24C08
Для проверки я решил использовать Ардуино Nano и микросхему памяти AT24C08. Обратите внимание, аналоги, например чип ST24C08, имеют немного другую распиновку, сверяйтесь с даташитами.Для контроля работы сначала хотел использовать последовательный порт, но потом решил, что это лишнее и решил обойтись обычным светодиодом, ведь он уже распаян на Ардуинке. Работу с микросхемой AT24C08 расписывать не буду, всё есть в документации к ней.
Идея проверки состоит в том, что мы сначала записываем в микросхему два байта кода, а потом читаем их и сравниваем с оригиналом. Если записанные и считанные байты совпадают, то мы зажигаем светодиод. Это и будет говорить о полной работоспособности протокола.
Теперь код:
// главная функция
void main(void) {
DDRB.5=1;
delay_ms(100); // ждем, пока питание устаканится
while (1) {
char t=0;
delay_ms(100); // ждем немного
PORTB.5=0; // гасим тестовый светодиод
// пишем байт 0xF7 в память микросхемы по адресу 05
i2c_go();
i2c_wr(0xA0); // i2c адрес микросхемы
i2c_wr(0x05); // адрес в памяти
i2c_wr(0xF7); // данные
i2c_end();
// пишем байт 0x3B в память микросхемы по адресу 06
i2c_go();
i2c_wr(0xA0); // i2c адрес микросхемы
i2c_wr(0x06); // адрес в памяти
i2c_wr(0x3B); // данные
i2c_end();
// читаем память..
i2c_go();
i2c_wr(0xA0); // i2c адрес микросхемы
i2c_wr(0x05); // адрес для чтения
i2c_go(); // так надо........
i2c_wr(0xA1); // запуск чтения
if (i2c_rd(1)==0xF7) t++; // принимаем данные с линии
if (i2c_rd(0)==0x3B) t++; // аск передаем 0, т.к. это последний байт для чтения
i2c_end();
// если все данные прочитаны верно
if (t==2) PORTB.5=1; // включаем тестовый светодиод
}
}
Написанный код я сначала тестирую в Протеусе (ISIS 7 Proteus). Привожу осциллограмму работы:
На осциллограмме видно, что синхросигнал линии SCL получен ровный, данные на линии SDA выставляются верно. Но есть и минус: всплески сигнала на линии SDA. Я долго выяснял, откуда они взялись. Оказалось, что этот всплеск возникает сразу после передачи/приёма бита «акнолидж» (ACK). Устранить этот недостаток мне так и не удалось, оставил, как есть. На работу данный всплеск не должен влиять.
Может на досуге ещё подумаю над этим или кто-нибудь из опытных читателей подскажет в комментариях.
↑ Тестируем протокол в железе
Переходим к тесту в железе. Воткнул в беспаечную макетную плату (breadboard) платку ARDUINO NANO, микросхему AT24C08 и соединил всё проводками. Я для прошивки Ардуинки использую стандартный Ардуиновский бутлоадер (загрузчик) и программу XLoader, а вы можете использовать привычный вам способ.При первом запуске у меня пошел дым. Что-то не так! Оказалось, что я перепутал распиновку микросхемы памяти AT24C08. Обидно. В Ардуино у меня сгорел диод по питанию и микросхема памяти вышла из строя. Я заменил и то и другое.
Прошил, включаю: светодиод «молчит». Потыкал тестером и понял, что совершенно забыл про подтягивающие резисторы. Воткнул их. Молчок. Немного подумав и покурив даташит на AT24C08, подал на вывод памяти WP (Write Protect — защита записи, pin 7) не ноль, а единицу.
И, вуаля, всё заработало:
↑ Итого
На фото видно, что тестовый светодиод горит. Это означает, что всё сработало! Наш код можно сохранить и поместить в папку с примерами.Всем удачи, а меня с почином. Это первая моя статья.
В скором времени планирую написать статью о протоколе SPI.
Камрад, рассмотри датагорские рекомендации
🌼 Полезные и проверенные железяки, можно брать
Опробовано в лаборатории редакции или читателями.