PIC. Урок 19. MSSP. I2C. Подключаем внешний EEPROM. Часть 2



В предыдущей части урока мы познакомились с шиной I2C и с её организацией в контроллере PIC16.

 

Сегодня мы поработаем с шиной I2C в режиме ведущего устройства. И будем мы как принимать байты, та их и передавать.

В качестве подопытного устройства мы возьмём микросхему EEPROM — AT24C32D, которая установлена в модуле с часовой микросхемой DS3231. С данной микросхемой мы уже работали, когда изучали программирование шины I2C для контроллеров AVR. Там эта шина называлась TWI. Причём микросхему мы использовали с часовым модулем на микросхеме DS1307. Думаю, с такой устаревшей микросхемой мы уже работать не будем, поэтому модуль был взят другой — с микросхемой DS3231. С данным модулем мы также работали на контроллере STM32, только с микросхемой EEPROM мы там не работали.

Вообщем, вот такой вот модуль

 

 

С другой стороны, как водится в таких модулях, установлена литиевая батарейка для обеспечения энергонезависимости

 

 

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

Посмотрим блок-схему микросхемы EEPROM

 

 

 

Как мы видим, здесь имеется само собой шина I2C, контакт WPWrite Protect, при соединении которого с общим проводом мы работаем с данной микросхемой в обычном режиме, а если мы его подтянем к шине питания, то, соответственно, память микросхемы будет только читаться.

Также мы видим здесь адресную шину. состоящую из трёх проводов, соединение каждого из которых с землёй сбрасывает соответствующий бит в адресе. Это сделано с учётом того, чтобы на одной шине можно было использовать несколько подобных микросхем. Вот так вот выглядит адрес устройства при условии, если все эти ножки будут соединены с общим проводом. Соединение с питанием каждой из данных ножек соответствующий бит переведёт в логическую 1

 

 

Данная микросхема также умеет работать очень быстро. При питании 1,7 вольт она может работать до 400 кГц, а при 2,5-5 в — до 1 мегагерца.

Подключим нашу схему. Присоединим к специальному разъёму модуль, также LED-индикатор. Логический анализатор подключим к тем же ножкам, но на разъёме SPI, ибо там они те же самые

 

 

Ну и наконец — проект.

Создадим мы его из проекта урока 17 MCP3201 и назовём проект по наименованию микросхемы — AT24C.

Откроем проект в MPLAB.X, сделаем его главным, откроем файл main.c и удалим из него объявление глобальной переменной и макросов

 

//CS = RC6

#define CS_ON() PORTC &= ~0x40

#define CS_OFF() PORTC |= 0x40

 

Добавим макрос с адресом микросхемы

 

#include "led.h"

//------------------------------------------------

#define DEV_ADDR 0xAE

 

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

Функцию SPI_Receive_byte удалим вместе с телом.

Из функции main() удалим объявление локальных переменных

 

unsigned long dt1=0, dt2=0, res=0;

 

Вместо этого добавим переменную для счёта

 

void main()

{

  int i;

 

 

Удалим проверку индикатора, мы уже знаем, что он нормально работает

 

ledprint(1234);

 

Немного изменим настройку направления ножек порта C

 

TRISC=0x18;////RC3 and RC4 Input (SDC and SDA)

 

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

Далее идёт настройка шины. Для этого необходимо настроить три регистра, которые мы очень подробно изучили.

Тем не менее перед настройкой напишем подробный комментарий, какие биты мы и для чего включаем

 

TRISC=0x18;////RC3 and RC4 Input (SDC and SDA)

// SSPM3:SSPM0 = 1000 (I2C Master mode, clock = FOSC/(4 * (SSPADD + 1))

// = 4000000/(4 * (9 + 1)) = 100000 Hz)

// SKE=1(Disable SMBus specific inputs)

// SKP=1(Release clock)

// SMP=1 (Slew rate control disabled for standard speed mode (100 kHz and 1 MHz))

// SSPEN= 1 (Enables the serial port and configures the SDA and SCL pins as the serial port pins)

SSPADD=0X09;

SSPSTAT=0X80;

SSPCON=0X38;

 

Теперь напишем несколько функций для работы с шиной.

Над функцией main() сначала добавим функцию ожидания освобождения шины

 

//------------------------------------------------

void I2C_Idle()

{

  while(READ_WRITE);

  ZERO=0;

  while(ZERO)

  {

    SSPCON2&0x1f;

  }

}

//------------------------------------------------

 

В данной функции мы сначала ждём обнуления бита R/W регистра STATUS.

Затем мы ждём обнуления пяти младших битов регистра SSPCON2 (ACKEN, RCEN, PEN, RSEN и SEN).

Напишем теперь ниже функцию ожидания установки флага прерывания шины, в которой мы его также сразу и сбросим

 

//------------------------------------------------

void I2C_IntWait(void)

{

  while(!SSPIF);

  SSPIF=0;

}

//------------------------------------------------

 

Далее добавим функцию передачи байта ведомому устройству

 

//------------------------------------------------

void I2C_SendByte(unsigned char data)

{

  SSPBUF = data;

  I2C_IntWait();

}

//------------------------------------------------

 

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

Следующая функция — функция приёма байта от ведомого устройства с подтверждением

 

//------------------------------------------------

unsigned char I2C_ReceiveByte_Ack(void)

{

  unsigned char data = 0;

  RCEN=1;

  while(!SSPIF);

  data=SSPBUF;

  I2C_IntWait();

  ACKDT=0;

  ACKEN=1;

  I2C_IntWait();

  return data;

}

//------------------------------------------------

 

Здесь немного побольше операций.

Сначала мы включаем режим приёма с помощью определённого бита. Затем ждём установки флага прерывания, переписываем значение буфера в переменную, опять ждём прерывания, обнуляем флаг, устанавливаем флаги с учетом ожидания подтверждения от ведомого, опять ждём прерывания и сброса флага и возвращаем результат из функции.

 

 

Далее добавим функцию приёма байта без ожидания подтверждения от ведомого

 

//------------------------------------------------

unsigned char I2C_ReceiveByte_Nack(void)

{

  unsigned char data = 0;

  RCEN=1;

  while(!SSPIF);

  data=SSPBUF;

  while(!SSPIF);

  SSPIF=0;

  ACKDT=1;

  ACKEN=1;

  while(!SSPIF);

  SSPIF=0;

  return data;

}

//------------------------------------------------

 

Здесь практически то же самое, только здесь мы выставляем флаг ACKDT в 1. Поэтому здесь ведущий не будет ждать подтверждения от ведомого.

Ещё добавим функцию формирования на шине условия START

 

//------------------------------------------------

void I2C_StartCondition(void)

{

  I2C_Idle();

  SEN=1;

  I2C_IntWait();

}

//------------------------------------------------

 

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

Сначала мы ждём, пока наша шина станет свободной, затем выставляем флаг SEN, который заставит шину сформировать условие START, а затем по возникновению прерывания сбрасываем флаг.

Далее аналогичная функция, которая будет уже формировать условие STOP на шине I2C

 

//------------------------------------------------

void I2C_StopCondition(void)

{

  PEN=1;

  NOP();

}

//------------------------------------------------

 

Здесь мы уже устанавливаем флаг PEN, который обяжет шину сформировать условие STOP, и просто подождём один операционный цикл.

Только по одному байту в микросхему нам писать неинтересно, так как она поддерживает запись сразу непрерывными участками.

Для этого зайдём в техническую документацию и посмотрим следующую картинку

 

 

Здесь рассказано, как передать один байт для записи в определённый адрес памяти микросхемы. Сначала мы передаём адрес и бит записи, затем подтверждение, затем старший байт адреса памяти, подтверждение, младший бит адреса памяти. подтверждение, байт данных для записи, подтверждение и СТОП.

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

 

 

 

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

Добавим функцию записи серии байтов в микросхему

 

//------------------------------------------------

void AT24C_WriteBytes (unsigned int addr,unsigned char *buf, unsigned int bytes_count)

{

  unsigned int i;

  I2C_SendByte(DEV_ADDR);

  I2C_SendByte(addr<<8);

  I2C_SendByte((unsigned char)addr);

  while(BF);

  for(i=0;i<bytes_count;i++)

  {

    I2C_SendByte(buf[i]);

  }

}

//------------------------------------------------

 

В функции мы всё делаем по диаграмме, которую мы только что посмотрели.

Мы предаём адрес микросхеме, чтобы она знала, что мы обращаемся именно к ней, с битом 0, что означает запись, затем передаём адрес памяти, с которого мы начнём запись в микросхему, сначала его старшую, а затем младшую часть, с помощью состояния бита BF узнаем, закончилась ли передача, и затем начнём передавать байты нашей секвенции (серии байтов). Условия START и STOP мы будем формировать отдельно, не занося их в тело функции.

Теперь займёмся приёмом данных из микросхемы. Так как без этого мы не проверим, правильно ли записались наши байты в микросхему.

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

Откроем следующую таблицу в технической документации на микросхему (нажмите на картинку для увеличения изображения)

 

 

Это диаграмма считывания из определённой ячейки памяти одного байта.

Она чем-то похожа на процедуру записи, но не совсем это так.

Сначала мы также создаём условие СТАРТ на шине, затем передаём адрес с битом записи, именно записи. Тут вопрос, почему записи. Потому, что мы должны отправить адрес устройства и адрес ячейки памяти. из которой мы потом и заберём байт. Затем подтверждение, затем старшая часть адреса ячейки памяти, подтверждение, младшая часть ячейки памяти, подтверждение.

Затем, как ни странно, опять условие СТАРТ. Именно так. Затем уже передаём адрес устройства с битом чтения (1), подтверждение, затем после подтверждения читаем данные из регистра данных, а затем уже подтверждения не ждём, то есть генерируем бит без подтверждения, то есть шина данных становится в логический 1, а затем условие СТОП. Вот так.

Так что, как видите, здесь не совсем всё так просто.

И это всё при условии, что надо нам считать только один байт. Это удобно, если мы тестируем память на случайное чтение, постоянно читая какой-то случайный бит, поэтому и диаграмма названа Random Read.

Но в большинстве случаев нам требуется сразу считать несколько байт, расположенных последовательно в ячейках памяти. Пример из жизни: данный тип микросхем очень широко применяется в спутниковых рессиверах и T2-приёмниках и у нас, например глюканула эта микросхема, что происходит отнюдь не редко. А в памяти данной микросхемы находятся все практически настройки. Мы берем живую микросхему из другого такого же устройства и считываем оттуда прошивку. Вот такая процедура тут уже и пригодится. Затем уже мы эту считанную прошивку либо загружаем в микросхему со сбойными данными, либо в новую, на которую сбойную потом меняем.

Для такой процедуры существует другая диаграмма в даташите (нажмите на картинку для увеличения размера)

 

 

Данная диаграмма нам показывает, что считывается серия байтов почти так же, как и один байт, только после считывания последнего байта мы не ждём подтверждения от ведущего, поэтому на шине мы формируем условие NO ASK.

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

 

//------------------------------------------------

void AT24C_ReadBytes (unsigned int addr,unsigned char *buf, unsigned int bytes_count)

{

  unsigned int i;

  I2C_SendByte(DEV_ADDR);

  I2C_SendByte(addr<<8);

  I2C_SendByte((unsigned char)addr);

  while(BF);

  I2C_Idle();

  SSPIF=0;

  RSEN=1;

  I2C_IntWait();

  I2C_SendByte(DEV_ADDR|0x01);

  for(i=0;i<bytes_count;i++)

  {

    if(i>bytes_count)

    {

      buf[i] = I2C_ReceiveByte_Ack();

    }

    else

    {

      buf[i] = I2C_ReceiveByte_Nack();

    }

  }

}

//------------------------------------------------

 

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

 

В следующей части урока мы проверим запись и чтение байтов по шине I2C.

 

 

Предыдущая часть Программирование МК PIC Следующая часть

 

 

Купить программатор (неоригинальный) можно здесь: PICKit3

Купить программатор (оригинальный) можно здесь: PICKit3 original

Отладочную плату PIC Open18F4520-16F877A можно приобрести здесь: PIC Open18F4520-16F877A

Модуль RTC DS3231 с микросхемой памяти (3 шт)

Модуль RTC DS3231 с микросхемой памяти (1 шт) — так дороже

Индикатор 4-разрядный LED WaveShare можно приобрести здесь: LED WaveShare

Логический анализатор 16 каналов можно приобрести здесь

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

PIC MSSP. I2C. Подключаем внешний EEPROM

8 комментариев на “PIC. Урок 19. MSSP. I2C. Подключаем внешний EEPROM. Часть 2
  1. Роман:

    Отличная защита от дурака! Я целый день потратил на разбирание что к чему!
    Хорошая работа. Спасибо!

  2. Николай:

    if(i>bytes_count)
    {
    buf[i] = I2C_ReceiveByte_Ack();

    не должно ли условие быть «меньше»?

  3. Игорь:

    Добрый день!
    Поясните код:
    ZERO=0;
    while(ZERO)
    {
    SSPCON2&0x1f;
    }
    Если ZERO первоначально False, как программа войдет в while(ZERO){}. Спасибо.

    • Алексей:

      Этот код не правильный,взят из примеров к данной отладочной плате.Должно быть так:
      НИКАКАЯ переменная ZERO не нужна, а вся функция должна занимать одну строку
      while((SSPCON2 & 0x1F)||(SSPSTAT & 0x04));
      Дело в том что биты SEN,RSEN,ACKEN,PEN,RCEN в ПИКах сбрасываются аппаратно и для того чтобы
      отследить освобождение шины нужно дождаться их обнуления а также сброшен ли бит RW(SSPSTAT).
      По этой же причине если модуль не использует прерываний,флагом SSPIF можно вообще не пользоваться,
      а отслеживать сброс соответствующих битов в регистре SSPCON2.При наличии на шине только одного устройства
      такой подход более оправдан — кода получается меньше.
      Еще желательно сделать защиту от зависания внутри цикла,для этого внутри цикла можно инкрементировать
      дополнительную переменную например от 0 до FF.Если дошли до FF,то выходим из цикла и возвращаем результат
      ошибки.

  4. Алексей:

    Этот код не правильный,взят из примеров к данной отладочной плате.Должно быть так:
    НИКАКАЯ переменная ZERO не нужна, а вся функция должна занимать одну строку
    while((SSPCON2 & 0x1F)||(SSPSTAT & 0x04));
    Дело в том что биты SEN,RSEN,ACKEN,PEN,RCEN в ПИКах сбрасываются аппаратно и для того чтобы
    отследить освобождение шины нужно дождаться их обнуления а также сброшен ли бит RW(SSPSTAT).
    По этой же причине если модуль не использует прерываний,флагом SSPIF можно вообще не пользоваться,
    а отслеживать сброс соответствующих битов в регистре SSPCON2.При наличии на шине только одного устройства
    такой подход более оправдан — кода получается меньше.
    Еще желательно сделать защиту от зависания внутри цикла,для этого внутри цикла можно инкрементировать
    дополнительную переменную например от 0 до FF.Если дошли до FF,то выходим из цикла и возвращаем результат
    ошибки.

  5. Павел:

    Добрый день
    Я очень внимательно изучал Ваш материал, спасибо, но у меня остались
    неясные моменты.
    1 Вы не настроили регистр SSPCON1 в части четырех младших бит 1000 для
    получения ведущего режима работы и не включили модуль MSSP бит SSPEN.
    2 При записи байта вы не анализируете бит подтверждения ASKSTAT.
    Или я что-то не правильно понял.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*