Микроконтроллеры PIC благодаря своей универсальности и относительно невысокой цене находят широкое применение в различных электронных проектах. Но как бы не был универсален микроконтроллер, в больших системах он все равно не может выполнять все операции в одиночку – ему приходится обмениваться данными с другими устройствами с помощью какого-нибудь коммуникационного протокола. К наиболее популярным подобным протоколам (интерфейсам), находящих широкое применение в современной встраиваемой электронике, относятся протоколы USART, IIC (I2C), SPI и CAN. Использование последовательного протокола связи USART в микроконтроллерах PIC мы уже рассматривали на нашем сайте в этой статье, а в данной статье мы рассмотрим использование протокола I2C в микроконтроллере PIC16F877.
Ранее на нашем сайте мы рассматривали использование интерфейса I2C в плате Arduino и в плате STM32F103C8 (Blue Pill).
Принципы работы протокола I2C
IIC (Inter Integrated Circuits), часто обозначаемый также как I2C, представляет собой двухпроводный синхронный коммуникационный протокол. Протокол I2C имеет две линии – линию данных (Serial Data Line, SDL) и линию синхронизации (Serial Clock Line, SCL)
Первоначально протокол I2C был разработан компанией Phillips. Протокол использует модель ведущий (Master) – ведомый (Slave). Связь в данном протоколе может осуществляться только между ведущим и ведомым. Достоинством протокола I2C по сравнению с ранее существовавшими до него аналогичными протоколами является то, что в нем к ведущему может быть подключен более чем один ведомый. Всего же протокол I2C поддерживает сеть размером до 128 устройств, каждое из которых имеет в сети уникальный адрес.
Структурная схема интерфейса I2C представлена на следующем рисунке.
Обмен данными в протоколе I2C осуществляется с помощью двух линий – линии данных (SDL) и линии синхронизации (SCL).
В любой момент времени только ведущий (master) может инициировать связь (обмен данными). Поскольку в сети протокола может быть несколько ведомых (slave), то каждый из них должен иметь свой уникальный адрес, чтобы с его помощью ведущий мог указать какому ведомому предназначаются передаваемые им данные. В конкретный момент времени данные передает только тот ведомый, к которому обратился ведущий (по адресу), все остальные ведомые в это время «молчат». Таким образом, с использованием всего 2-х линий осуществляется связь между многочисленными устройствами.
Где применяется протокол I2C
Протокол I2C используется только для связи на короткие расстояния. Протокол отличается высокой надежностью работы благодаря наличию линии синхронизации. В большинстве случаев протокол I2C используется в датчиках или других устройствах, которые должны передавать данные ведущему (master). При этом очень удобно то, что микроконтроллер с помощью всего двух контактов (линий) может взаимодействовать с большим количеством ведомых устройств.
Если вам нужна связь на большие расстояния чем те, которые может обеспечить протокол I2C, то вам необходимо использовать протокол RS232 или RS485, если же вам необходима более надежная связь на короткие расстояния, тогда вам целесообразно использовать протокол SPI.
I2C в микроконтроллере PIC16F877a с использованием компилятора XC8
В данном проекте мы будем рассматривать связь по протоколу I2C в микроконтроллере PIC16F877a только с использованием компилятора XC8. Данный процесс похож на аналогичные процессы и в других микроконтроллерах, но с небольшими отличиями. При этом стоит иметь ввиду, что «продвинутые» серии микроконтроллеров PIC, например, серия PIC18F в своем компиляторе уже содержит встроенную библиотеку для работы с протоколом I2C, но для микроконтроллера PIC16F877A такой библиотеки нет, поэтому мы создадим ее самостоятельно. Код данной библиотеки доступен для скачивания в конце данной статьи.
При изучении чего то нового в микроконтроллере всегда желательно посмотреть даташит на него. В нашем случае нам необходимо в этом даташите посмотреть раздел про интерфейс I2C и выяснить какие регистры микроконтроллера используются для настройки его работы. Здесь мы не будем приводить эти регистры, их настройка представлена далее в приведенных фрагментах программы.
Далее рассмотрим функции, которые мы будем использовать в нашей библиотеке для работы с протоколом I2C в микроконтроллере PIC16F877A.
void I2C_Initialize()
Данная функция инициализации будет сообщать микроконтроллеру что мы будем использовать протокол I2C. Это осуществляется с помощью установки соответствующих битов в регистрах SSPCON и SSPCON2. Вначале нам необходимо задать режимы работы контактов, на которых мы будем использовать интерфейс IIC (у нас это контакты RC3 и RC4) на ввод данных. Затем установить необходимые биты в регистрах SSPCON и SSPCON2. Мы будем использовать режим ведущего в интерфейсе IIC с частотой синхронизации FOSC/(4 * (SSPADD + 1)). В комментариях к этим строкам программы указаны страницы в даташите на микроконтроллер, на которых подробно описаны эти настройки.
Далее нам необходимо установить частоту синхронизации (clock frequency), она может отличаться для различных приложений. Ее мы будем задавать с помощью переменной feq_k – изменяя в программе ее значение можно изменять частоту синхронизации.
1 2 3 4 5 6 7 8 9 10 |
void I2C_Initialize(const unsigned long feq_K) //Begin IIC as master { TRISC3 = 1; TRISC4 = 1; //Set SDA and SCL pins as input pins SSPCON = 0b00101000; //pg84/234 SSPCON2 = 0b00000000; //pg85/234 SSPADD = (_XTAL_FREQ/(4*feq_K*100))-1; //Setting Clock Speed pg99/234 SSPSTAT = 0b00000000; //pg83/234 } |
Void I2C_Hold()
Данная функция используется для приостановки работы устройства до тех пор пока не завершится выполнение текущей операции I2C. Мы должны проверить завершение ее выполнения прежде чем переходить к новой операции. Это осуществляется с помощью проверки регистров SSPSTAT и SSPCON2. Регистр SSPSTAT содержит информацию о состоянии шины I2C. Указанные функции выполняются с помощью следующих команд:
1 2 |
SSPSTAT & 0b00000100 SSPCON2 & 0b00011111 |
С помощью данных команд мы убеждаемся в том, что 2-й бит регистра SSPSTAT установлен в 0, а биты с 0 по 4 регистра SSPCON2 также установлены в 0. Затем мы объединяем эти две команды в одну и проверяем результат получившейся операции. Если он равен 0, то программа продолжит выполнение, если он не равен 0, то выполнение программы задерживается до тех пор пока ее результат не будет равен 0.
1 2 3 4 |
void I2C_Hold() { while ( (SSPCON2 & 0b00011111) || (SSPSTAT & 0b00000100) ) ; //check the this on registers to make sure the IIC is not in progress } |
Void I2C_Begin() и void I2C_End()
Каждый раз, когда мы будем передавать или принимать данные по шине I2C, мы должны открывать и закрывать соединение I2C. Для открытия соединения I2C нам необходимо установить бит SEN, а для закрытия соединения I2C нам необходимо установить бит PEN. Перед переключением данных битов мы должны проверить занята ли шина I2C или нет с помощью функции I2C_Hold, описанной ранее.
1 2 3 4 5 6 7 8 9 10 |
void I2C_Begin() { I2C_Hold(); //Hold the program is I2C is busy SEN = 1; //Begin IIC pg85/234 } void I2C_End() { I2C_Hold(); //Hold the program is I2C is busy PEN = 1; //End IIC pg85/234 } |
Void I2C_Write()
Данная функция используется для передачи данных от ведущего (master) к ведомому (slave). Эту функцию в большинстве случаев следует использовать после функции I2C_Begin(), а после ее выполнения вызывать функцию I2C_End(). Данные, которые будут передаваться по шине I2C, будут проходить через переменную data. Затем из этой переменной они будут перемещаться в буфер регистра SSPBUF чтобы затем передаться по шине I2C.
Но перед передачей данных нам необходимо передать адрес ведомого устройства, которому будут передаваться эти данные. Поэтому функцию I2C_Write нам необходимо будет использовать дважды: первый раз для передачи адреса, а второй – непосредственно для передачи данных.
1 2 3 4 5 |
void I2C_Write(unsigned data) { I2C_Hold(); //Hold the program is I2C is busy SSPBUF = data; //pg82/234 } |
unsigned short I2C_Read()
Данная функция будет использоваться для считывания данных, которые в данный момент находятся на шине I2C. Она используется после запроса ведомому чтобы он передал данные на шину. Принимаемые данные будут записываться в регистр SSPBUF, откуда мы их можем переместить в любую переменную.
Во время обмена данными по протоколу I2C ведомый после передачи данных, запрошенных ведущим, должен также передать бит подтверждения (acknowledgment bit). Этот бит должен быть проверен ведущим чтобы убедиться в том, что процесс обмена данными был успешным. После проверки бита подтверждения ACKDT мы будем устанавливать бит ACKEN.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
unsigned short I2C_Read(unsigned short ack) { unsigned short incoming; I2C_Hold(); RCEN = 1; I2C_Hold(); incoming = SSPBUF; //get the data saved in SSPBUF I2C_Hold(); ACKDT = (ack)?0:1; //check if ack bit received ACKEN = 1; //pg 85/234 return incoming; } |
Описанных нами функций вполне хватит для большинства операций обмена данными по интерфейсу I2C. Хотя функционал протокола I2C значительно больше чем в представленных нами функциях, мы не рассматриваем их здесь из-за ограниченного объема статьи. Для их более подробного изучения вы всегда можете обратиться к даташиту на микроконтроллер.
Полный код программы для работы с протоколом I2C в микроконтроллере PIC вы можете скачать по этой ссылке.
Начало программы для работы с протоколом I2C
Теперь, когда у нас запрограммированы функции для работы с протоколом I2C, мы можем написать небольшую программу для работы с данным протоколом. Затем мы можем смоделировать работу нашей программы.
Первым делом в программе установим биты конфигурации и установим тактовую частоту микроконтроллера равную 20 MHz.
1 2 3 4 5 6 7 8 9 10 |
#pragma config FOSC = HS // Oscillator Selection bits (HS oscillator) #pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled) #pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled) #pragma config BOREN = ON // Brown-out Reset Enable bit (BOR enabled) #pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming) #pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off) #pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control) #pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off) #define _XTAL_FREQ 20000000 |
Затем нам необходимо подключить заголовочный файл PIC16F877a_I2C.h, который можно скачать по ссылке, приведенной выше. Убедитесь в том, что заголовочный файл добавлен в заголовок вашего проекта. Структура файлов вашего проекта должна выглядеть как на следующем рисунке:
После этого подключите заголовочный файл в программе.
1 2 |
#include <xc.h> #include "PIC16F877a_I2C.h" |
Далее внутри цикла while мы будем открывать соединение I2C и передавать по шине I2C несколько случайных данных, после этого мы будем закрывать соединение I2C. В качестве случайных значений мы выбрали D0, 88 и FF. Вы можете изменить их по своему усмотрению.
1 2 3 4 5 6 7 8 9 |
while(1) { I2C_Begin(); I2C_Write(0xD0); I2C_Write(0x88); I2C_Write(0xFF); I2C_End(); __delay_ms(1000); } |
Моделирование работы проекта в Proteus
Симулятор Proteus имеет замечательный инструмент под названием I2C debugger, который можно использовать для считывания данных с шины I2C. Таким образом, с его помощью мы можем проверить передаваемые нами в шину I2C значения. Нарисованная в симуляторе Proteus схема нашего проекта приведена на следующем рисунке.
Если вы внимательно посмотрите на данные в этом окне, вы увидите что они те же самые, которые мы передавали в программе для микроконтроллера – D0, 88 и FF. Эти данные передаются каждую секунду, поэтому и время в данном окне будет обновляться соответствующим образом. Синяя стрелка показывает что данные передаются от ведущего к ведомому. Если данные будут передаваться в противоположном направлении, то стрелка будет направлена в другую сторону. Вид отдельно взятой строчки в этом окне выглядит следующим образом:
Исходный код программы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
/* * File: IIC with PIC16F * Author: Aswinth Raj * * Created on 7 May, 2018, 6:42 PM */ #pragma config FOSC = HS // Oscillator Selection bits (HS oscillator) #pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled) #pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled) #pragma config BOREN = ON // Brown-out Reset Enable bit (BOR enabled) #pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming) #pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off) #pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control) #pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off) #define _XTAL_FREQ 20000000 #include <xc.h> #include "PIC16F877a_I2C.h" int main() { I2C_Initialize(100); //Initialize I2C Master with 100KHz clock while(1) { I2C_Begin(); I2C_Write(0xD0); I2C_Write(0x88); I2C_Write(0xFF); I2C_End(); __delay_ms(1000); } |
Все файлы для данного проекта вы можете скачать по следующей ссылке.
18 ответов к “Использование интерфейса I2C в микроконтроллере PIC16F877”
Здравствуйте. Обещал рассказать, вот. С Протеусом занимаюсь только 4 дня, раньше он мне был не нужен. Теперь вот подучился, нарисовал, запрограммировал на 2 чипа pic16f887 и начал получать такую же фигню, как Alex. Уже всё перепробовал, и ошибки проверил, и с нескольких сайтов примеры позагружал. А фигня продолжается. Непонятно что отправляет, и непонятно что получает. Так что продолжаю ковырять.
Добрый день. Сочувствую что все оказалось не так просто как описано в статье. Дальнейших вам успехов
Стоило написать и один байт научился передавать-ловить. Но со вторым пока бьюсь. А мне нужно 4. I2C_debugger при этом продолжает показывать всякую чушь. Жалко.
Ну тут, как говорится, Москва не сразу строилась. Если получилось с одним байтом, значит скоро должно получиться и с другими
Здравствуйте. Короче — пока ничего не получается. Уже попробовал как минимум три варианта. Первый правильно передавал один байт. Второй почему-то стал сдвигать байт на 1 бит, но остальное передавал (байт). Третий оказался самым обещающим. Передавал 4 байта, и даже дебаггер показывал, что они передаются (помимо прочей непонятной мишуры). Но странно, передавал 4 раза 0xFF вместо того, что я отправлял. Так что вот так вот. Попробовал залить всё в реальные микроконтроллеры, но там тоже есть непонятные вещи. В общем пока я всё в тех же зарослях. Такие дела. Что-то я делаю не так.
И вам добрый вечер. Сочувствую тому, что у вас пока не получается довести данный проект до логического завершения. Пробовали передавать все нули или все единицы? Может настройки для вашего микроконтроллера нужны не совсем такие как приведены в данном проекте?
И вообще немного странно, что передача ведётся не по конкретному адресу, а куда-то просто в I2C. А кому? Кто принимать эти данные будет?
В тексте статьи указано что сначала по шине I2C должен передаваться адрес ведомого устройства, а уже потом данные. Видимо, этот момент опущен в программе из-за того что I2C_debugger имеет I2C адрес по умолчанию, который уже известен микроконтроллеру
Здравствуйте. А не могли бы показать, как из slave-а передаётся, скажем, два-три байта и как мастер их должен ловить?
Добрый день, за прием данных отвечает функция short I2C_Read(), она описана в программе. А каким образом slave будет передавать эти данные зависит от конкретного slave’а. Чтобы потренироваться в качестве ведомого устройства можете использовать второй микроконтроллер PIC или плату Arduino (если вы с ней знакомы). Тогда все процессы передачи и приема данных будут под вашим контролем
Да, спасибо, уже нашёл. И именно таким путём и иду. Только на 887-м чипе, поскольку надо, чтобы 26 бит читалось и передавалось по I2C.
Просто в статье про адрес вообще ничего не сказано, а для мастера это критично. Лучше было бы его и при записи в слейв, и при чтении из него (в мастере) указывать. Просто чтобы не терялся. Тем более, что в слайве он при передаче не указывается. Его инициирует мастер при чтении. И разница в адресах при записи и чтении была бы явно видна и понятна. В общем, как-то так. Спасибо.
Ну нет в этом мире чего то идеального, вроде когда переводил думал что хорошая статья, однако, как теперь вижу, и в ней оказались небольшие недоработки. Спасибо вам за конструктивный комментарий и буду признателен если отпишитесь потом получилось ли у вас все это сделать или нет
Хорошо. Сейчас немного занят программкой на ПК, которая всё это будет ловить. Только там основной контроллер pic18f97j60, он все собранные данные по интернету на ПК сбрасывать будет. Потом всё расскажу.
Ок, будем ждать
Добрый день!Вы случайно не знаете, почему в i2c дебаггере вместо такой картинки, как у вас, появляется нечто подобное:
324.601us 409.603us S Sr P
409.603us 525.606us ? ? S P
525.606us 645.404us ? ? ? ? S P
645.404us 738.206us ? ? S P
738.206us 1.001 s ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? S Sr P
1.001 s 1.001 s ? ? S P
1.001 s 1.001 s ? ? ? ? S P
1.001 s 1.001 s ? ? S P
1.001 s 1.001 s ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? S Sr P
и т.д.?
Добрый день, точно не знаю. Но у вас вначале процесс идет нормально, а потом зацикливается почему то на 1 секунде. У вас конфигурация регистров правильно выставлена? Нет ли случайно где опечатки?
SDA=SDL=SDI?
На схеме и в тексте всяко-разно…
Да, случаются иногда небольшие изменения в терминологии. А SDI вы где увидели? Подскажите