Перейти к основному содержимому

Урок 5. Интерфейсы передачи данных

Введение

У всех интерфейсов передачи данных, как легко догадаться из названия, одна цель - передача данных. Данными с микроконтроллером могут обмениваться датчики, исполнительные устройства, индикаторы, дисплеи, компьютеры, смартфоны и даже другие микроконтроллеры. Причем под обменом данных имеются в виду не только двухстороннюю связь, но и одностороннюю, когда, например, датчик температуры передает информацию на контроллер и ничего не ждет от него в ответ.

Если говорить применительно к STM32, то в аппаратном виде у всех МК присутствуют четыре интерфейса: UART, SPI, I2C и CAN. У каждого из перечисленных интерфейсов есть свои преимущества и недостатки, а также свои клиенты (устройства), использующие только тот или иной интерфейс для связи с внешним миром. В этом уроке мы подробно разберем интерфейсы UART, SPI и I2C.

UART

Общая информация

Мы уже научились с помощью библиотеки Serial пользоваться UART. Так как он прост в работе, его очень удобно использовать при отладке программ или если возникает необходимость связи компьютера и МК. В этом уроке мы узнаем, как устроен этот интерфейс.

UART был одним из первых последовательных протоколов. Когда-то повсеместно распространенные последовательные порты почти всегда работали по протоколу UART: принтеры, мыши, модемы и т. д. В последние годы популярность UART снизилась: такие протоколы, как SPI и I2C, заменяют UART на уровне микросхем и компонентов. Вместо обмена данными через последовательный порт в большинстве современных компьютеров и периферийных устройств теперь используются такие интерфейсы, как Ethernet и USB. Для обмена данными между различными узлами внутри самолетов, автомобилей и роботов применяются интерфейсы МКИО, CAN и FireWire. Однако UART по-прежнему используется для приложений с более низкой скоростью и пропускной способностью, поскольку он очень прост, дешев и легок в реализации.

На аппаратном уровне UART использует две ножки микроконтроллера – Rx и Tx, где первые буквы означают Receive и Transmit соответственно. Символ «x» в эти аббревиатурах – это телеграфно-телефонная традиция. Во времена аналоговой связи сокращения обознались символом «x» на конце. Соединять выводы Rx и Tx следует крест-накрест: Tx передающего контроллера подключается к Rx принимающего и наоборот.

sch

Рис. 1. Схема подключения устройств по UART.

Напомним, что UART расшифровывается как Universal Asynchronous Receiver-Transmitter. Асинхронность означает, что передатчик и приемник не используют общий тактовый сигнал. Хотя это значительно упрощает протокол, данное свойство предъявляет определенные требования к передатчику и приемнику. Поскольку у них нет общего тактового сигнала, оба конца должны передавать данные с одинаковой заранее заданной скоростью, чтобы иметь одинаковую синхронизацию битов. Наиболее распространенные скорости передачи данных UART, используемые сегодня: 4800, 9600, 19.2 кбит/с, 57.6 кбит/с и 115.2 кбит/с. Помимо одинаковой скорости передачи данных, обе стороны UART-соединения также должны использовать одинаковую структуру и параметры кадра. Лучший способ получить представление о протоколе — посмотреть на кадр UART.

sch

Рис. 2. Временная диаграмма одного UART-кадра.

Как и в большинстве цифровых систем, высокий уровень напряжения используется для обозначения логической 1, а низкий уровень напряжения используется для обозначения логического 0. Обратите внимание, что в состоянии ожидания (когда данные не передаются) в линии поддерживается высокий уровень. Это позволяет легко обнаружить поврежденную линию или передатчик.

Стартовые и стоповые биты

Поскольку UART является асинхронным протоколом, передатчик должен сигнализировать о поступлении битов данных. Это делается с помощью стартового бита. Стартовый бит — это переход из состояния ожидания высокого уровня в состояние низкого уровня, за которым сразу же следуют пользовательские биты данных. После того, как биты данных закончились, стоповый бит указывает на окончание пользовательских данных. Стоповый бит — это либо переход обратно в состояние высокого уровня или состояние ожидания, либо сохранение этого состояния в течение дополнительного битового интервала. Второй (необязательный) стоповый бит может быть настроен, как правило, на то, чтобы дать приемнику время подготовиться к следующему кадру, но на практике это используется редко.

sch

Рис. 3. Временная диаграмма одного UART-кадра с двумя стоп-битами.

Биты данных

Биты данных являются пользовательскими данными или «полезными» битами и идут сразу после стартового бита. Может быть, от 5 до 9 битов пользовательских данных, хотя чаще всего используется 7 или 8 битов. Эти биты данных обычно передаются в формате с первым младшим значащим битом. Когда данные передаются таким образом, на английском это часто называют Least Significant Bit (LSB). Если наоборот, то Most Significant Bit (MSB).

sch

Рис. 4. Временная диаграмма одного UART-кадра.

к сведению

Если мы хотим передать заглавную букву «S» в 7-битном коде ASCII, битовая последовательность будет выглядеть как 1 0 1 0 0 1 1. Сначала мы меняем порядок битов, чтобы организовать перед передачей формат с первым младшим значащим битом, то есть 1 1 0 0 1 0 1. После передачи последнего бита данных для завершения кадра используется стоповый бит, и линия возвращается в состояние ожидания.

7-битный код ASCII ‘S(0x52) = 1 0 1 0 0 1 1

Порядок LSB = 1 1 0 0 1 0 1

Бит четности

Кадр UART может также содержать дополнительный бит четности, который можно использовать для обнаружения ошибок. Этот бит вставляется между последним битом данных и стоповым битом. Значение бита четности зависит от типа используемого контроля четности (на четность или нечетность):

При контроле на четность этот бит устанавливается таким образом, чтобы общее количество единиц в кадре было четным.

При контроле на нечетность этот бит устанавливается таким образом, чтобы общее количество единиц в кадре было нечетным.

к сведению

Заглавная S (1 0 1 0 0 1 1) содержит три нуля и 4 единицы. При использовании контроля четности бит четности равен нулю, потому что кадр уже содержит четное количество единиц. При использовании контроля нечетности бит четности должен быть равен единице, чтобы кадр имел нечетное количество единиц.

Бит четности способен обнаруживать только один измененный (инвертированный) бит. Если инвертируется более одного бита, невозможно надежно обнаружить их с помощью единственного бита четности.

Реализация

В библиотеке Serial можно задавать количество битов данных, тип бита четности и вид стоп-бита. Подробнее об установке этих параметрах можно посмотреть в документации. По умолчанию стоят следующие параметры:

  • Кол-во битов данных: 8;
  • Бит четности: отсутствует;
  • Стоп бит: 1.

Давайте теперь напишем простой скетч, который передаёт на компьютер данные по UART и посмотрим осциллографом, как выглядят передаваемые кадры.

#include <VBCoreG4_arduino_system.h>

int cnt = 0;

void setup() {
Serial.begin(115200);
}

void loop() {
if (cnt < 255)
cnt++;
else
cnt = 0;
Serial.write(cnt);

delay(10);
}

Обратите внимание, что для передачи данных мы воспользовались функцией write() вместо print() или println(). Функция write() передаёт только один байт данных в каждой посылке. Так нам проще будет отслеживать передаваемые данные на осциллографе. Прошейте МК и подключитесь щупом осциллографа к контакту PA2, являющийся выводом Rx (проверьте это на схеме). Установите на осциллографе следующие параметры:

  • Vertical Volts/Div: 1 V
  • Horizontal Sec/Div: 10 us
  • Trigger Level: 1.5 V
  • Trig Menu -> Slope -> Falling

Вы должны увидеть постоянно бегущие данные. На изображении ниже показан пример одного UART-кадра с расшифровкой переданных данных (показано зеленым).

sch

Рис. 5. Осциллограмма одного UART-кадра.

Как вы помните, данные в UART передаются в формате LSB, а это значит, что первыми идут младшие биты данных. Если мы изменим порядок битов в человеческий формат, то получим 00110001 или 0x31 в шестнадцатеричной системе счисления. В ASCII-коде это соответствует символу 1.

примечание

Чтобы самостоятельно потренироваться в определении передаваемых данных, нажмите RUN/STOP на своём осциллографе и попытайтесь понять, какое именно число передаётся в вашем случае и узнайте, какому ASCII-коду соответствует это число.

SPI

Описание интерфейса

Последовательный периферийный интерфейс (SPI) — это последовательный синхронный протокол передачи данных, используемый микроконтроллерами для обмена данными с одним или несколькими периферийными устройствами на небольших расстояниях. Был разработан компанией Motorola в 1980-х годах для простого и недорогого сопряжения устройств, расположенных на одной печатной плате. Его главное назначение – это связь одного ведущего устройства с одним или нескольким ведомыми устройствами. Ведущее устройство в терминологии SPI называют Master, а ведомое – Slave. В качестве ведомых устройств могут выступать датчики, дисплеи, микросхемы ЦАП и АЦП, RFID-ридеры, модули беспроводной связи, включая приемо-передатчики WiFi и Bluetooth, GPRS-адаптеры и так далее.

Организации соединения SPI осуществляется тремя общими линиями и одной линией выбора ведомого устройства:

  • MISO (Master In Slave Out) – используется для передачи данных от ведомого к ведущему;
  • MOSI (Master Out Slave In) – выход ведущего - вход ведомого, для передачи данных от ведущего к периферийным устройствам;
  • SCK (Serial Clock) — синхронизирующая линия, синхросигнал генерируется ведущим устройством;
  • CS (Chip Select) / SS (Slave Select) – вход на ведомых устройствах с помощью которого ведущий может инициировать обмен данными с периферийным устройством. Если на этом входе LOW, то ведомый взаимодействует с ведущим, если HIGH, то ведомый игнорирует сигналы от ведущего.

Если в сети есть только одно ведомое устройство, подключение его к ведущему устройству довольно простое. Контакты ведомого устройства подключаются к тем же контактам ведущего устройства. На приведенной ниже схеме показан пример подключения ведомого устройства к ведущему для связи по протоколу SPI:

sch

Рис. 6. Схема подключения двух SPI-устройств.

Для сети SPI с несколькими ведомыми устройствами существует два варианта подключения: независимый и цепочкой. Как видно на рисунке 7, при независимом подключении у мастера задействуется несколько выводов CS/SS. Каждая линия CS/SS подключена к отдельному ведомому устройству.

sch

Рис. 7. Схема независимого подключения SPI-устройств.

При подключении цепочкой ведущему устройству требуется всего один контакт SS/CS для связи со всеми ведомыми устройствами. Тут есть очевидный плюс: требуется гораздо меньше пинов МК, но далеко не все периферийные устройства поддерживают такой режим.

sch

Рис. 8. Схема подключения SPI-устройств цепочкой.

Теперь рассмотрим взаимодействие между устройствами на примере временной диаграммы (рис. 9). Когда ведущему устройству необходимо связаться с конкретным ведомым устройством, оно переводит вывод CS ведомого устройства в низкое состояние. Это сообщает ведомому, что ведущее будет отправлять ему данные. Ведущее устройство поддерживает вывод CS в низком состоянии на протяжении всего обмена данными между устройствами.

После того, как вывод CS установлен в низкое состояние, ведущее устройство отправляет данные по линии MOSI. Одновременно оно также отправляет тактовые импульсы по линии SCK. Ведомое в это же время может передавать данные по линии MISO. Если ведущее устройство ожидает ответа от ведомого устройства после отправки команды по MOSI, то оно будет продолжать отправлять тактовые импульсы до тех пор, пока данные не будут получены на выводе MISO.

sch

Рис. 9. Временная диаграмма обмена по SPI.

Вспомните предыдущий урок. Не напоминает ли вам описываемый протокол работу со сдвиговым регистром? Это действительно так, и SPI как раз построен на базе сдвиговых регистров. Получается, что вы уже сами того не осознавая, знали, как работает SPI на базовом уровне.

Но у самого протокола еще есть несколько нюансов. Давайте разберем их:

  1. Порядок передаваемых битов: LSB или MSB
  2. Скорость передачи данных
  3. Активный уровень линии SCK
  4. Синхронизация с SCK по переднему или заднему фронту

Существует четыре стандартных режима работы SPI, определяющие пункты 3 4, которые описаны в таблице ниже. Нужно добавить, что именно мастер должен настраиваться на тот режим, на котором работает ведомое устройство.

РежимАктивный уровень SCKСинхронизацияВременная диаграмма
SPI0ВысокийПередний фронт
SPI1ВысокийЗадний фронт
SPI2НизкийЗадний фронт
SPI3НизкийПередний фронт

Библиотека SPI

При работе с SPI на МК можно задавать сигналы, как мы это делали в предыдущем уроке, но также можно использовать специальную библиотеку для работы с SPI.

Работая с библиотекой, сначала необходимо объявить объект класса SPIClass:

SPIClass myspi(MOSI_PIN, MISO_PIN, SCK_PIN); – в качестве аргументов указываются соответствующие линиям пины МК.

Основные методы класса:

  • .begin(); - запускает аппаратную шину SPI на контроллере, устанавливает режимы пинов и их уровни, дополнительно, при помощи pinMode() этого делать не надо;

  • .end(); - отключает шину SPI, инициализация пинов при этом остается;

  • .setBitOrder(order); - назначает порядок вывода и ввода битов в байте, где order - LSBFIRST - младший бит первый, MSBFIRST - старший бит первый;

  • .setClockDivider(); - делитель частоты контроллера для тактовой частоты шины. Чем больше, тем шина медленней. Аргумент SPI_CLOCK_DIVx, где x может принимать значения: 2, 4, 8, 16, 32, 64 и 128. Например, команда SPI.

  • setClockDivider(SPI_CLOCK_DIV4); заставит работать шину с частотой 1/4 от частоты контроллера;

  • .setDataMode(mode); - устанавливает режим работы шины. Аргумент: SPI_MODEx, где x - номер режима от 0 до 3 из таблицы выше;

  • .transfer(); - собственно сам прием-передача байта данных. Прием и передача осуществляется одновременно: in = SPI.transfer(out); где in - принятый байт, out – переданный.

Наш микроконтроллер STM32G474 имеет три аппаратных реализации интерфейса SPI. К каким пинам на МК они подключены, вы можете ознакомиться на схеме.

Реализация

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

Для работы с SPI нужно будет немного пересобрать проект. Мы будем использовать SPI1. На общей схеме модуля VBCores видно, что SPI1 подключен к следующим пинам МК: PB3, PB4 и PB5.

sch

Рис. 10. Фрагмент функциональной схемы модуля, показывающий пины соответствующие SPI1.

Соответственно, нам необходимо переподключить перемычки к этим пинам МК, как показано на сборочной схеме:

sch

Рис. 11. Сборочная схема проекта со сдвиговым регистром, управляемым по SPI1.

Текст программы, приведен ниже:

#include <VBCoreG4_arduino_system.h>
#include <SPI.h>

#define CS_PIN PA4 //Пин подключен к ST_CP входу 74HC595
#define SCK_PIN PB3 //Пин подключен к SH_CP входу 74HC595
#define MISO_PIN PB4 // в данном проекте использоваться не будет
#define MOSI_PIN PB5 //Пин подключен к DS входу 74HC595

uint8_t data = 0;

SPIClass myspi(MOSI_PIN, MISO_PIN, SCK_PIN);

void setup() {
// определить CS_PIN в качестве выхода
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);
// инициализировать SPI
myspi.begin();
myspi.setBitOrder(LSBFIRST);
myspi.setDataMode(SPI_MODE0);
myspi.setClockDivider(SPI_CLOCK_DIV128);
}

void loop() {
digitalWrite(CS_PIN, LOW); // прижимаем CS к нулю
delayMicroseconds(5); // ожидание необходимо, так как если одновременно сделать CS=0 и послать синхроимпульсы, то протокол нарушится
myspi.transfer(data); // передача данных
delayMicroseconds(5); // снова ожидание
digitalWrite(CS_PIN, HIGH); // перевод режим ожидания

data++; // инкремент счетчика
delay(500);
}

Ознакомьтесь с программой и её комментариями. Скомпилируйте и загрузите её на микроконтроллер. Убедитесь, что всё работает, как задумывалось.

I2C

I2C (также известная как Inter-Integrated Circuit, IIC, I2C) – разработанная компанией Philips последовательная шина для низкоскоростной (до 100 кбит/с в стандартном режиме и 400 кбит/с в «быстром» режиме) передачи 8-битных данных между микроконтроллерами (или процессорами) и периферийными компонентами, такими как различные датчики, драйверы, ОЗУ, АЦП и т.д.

Физический уровень

Для передачи информации используется всего две линии:

  • SDA — линия данных
  • SCL — линия синхронизации.
sch

Рис. 12. Схема подключения устройств по I2C.

Каждое устройство определяется как ведущее или ведомое, а также обладает уникальным (в пределах шины) адресом. На одной шине может находиться до 127 устройств. К шине должны быть подключены подтягивающие резисторы. Это вызвано тем, что когда обмен данными отсутствует на обеих линиях должен быть высокий уровень, т.е. логическая единица. Когда устройствам нужно выдать что-то в шину, то они прижимают линию к земле. Стандартная величина сопротивления резисторов составляет 4.7 кОм.

МК STM32 питаются от 3.3В, поэтому к этому же уровню необходимо подтягивать обе линии. При организации сети I2C нужно принимать во внимание факт максимального уровня напряжения питания устройств.

Начало и завершение обмена данными

Обмен данными инициируется ведущим устройством. Оно генерирует тактирующие сигналы на линии SCL, которые имеют определенную периодичность, но могут быть задержаны приемником, если он еще не готов принят следующий байт данных. В исходном состоянии обе линии SDA и SCL находятся в высоком состоянии.

Процедура обмена данными начинается с формирования ведущим устройством состояния СТАРТ: переход состояния линии SDA с высокого на низкое при высоком состоянии на линии SCL.

Процедура обмена данными заканчивается, когда ведущий формирует состояние СТОП: переход состояния линии SDA с низкого на высокое при высоком состоянии на линии SCL.

sch

Рис. 13. Временная диаграмма обмена по I2C.

Обмен данными

Данные передаются изменением уровня сигнала на линии SDA в определенной последовательности. Обмен данными происходит байтами, каждый из которых состоит из 8-ми бит. За один сеанс может быть передано неограниченное количество байт.

После формирования состояния СТАРТ и до появления состояния СТОП изменять уровень на линии SDA можно только при низком состоянии на тактирующей линии SCL. Бит информации читается приёмником только когда на линии SCL установлен высокий уровень сигнала, поэтому в этот момент на линии SDA сигнал должен быть неизменным и стабильным. Принятые данные считаются действительными, только если соблюдается это правило.

sch

Рис. 14. Временная диаграмма обмена по I2C.

Подтверждение приема данных

Для корректной передачи данных, приемник должен подтверждать прием каждого байта от передатчика. Для этого в спецификации протокола обмена по шине I2C вводится специальный бит подтверждения, выставляемый приёмником на шину SDA после приема каждого 8-го бита данных. Передача каждых 8-ми бит данных считается успешной, если приемник отправляет бит подтверждения с низким уровнем сигнала по линии SDA. Импульс синхронизации для подтверждающего бита генерируется ведущим устройством сразу после переданных 8-ми бит данных.

sch

Рис. 15. Временная диаграмма обмена по I2C.

Адресация

Логично, что перед обменом данными ведущее устройство должно определить с каким ведомым устройством планируется работа в этом сеансе. Т.е. ведущий должен адресовать необходимого ведомого, «позвать его по имени». Для этого ведущее устройство, сразу после установки состояния СТАРТ, отправляет по шине специальный байт информации, первые 7 бит которого содержат адрес ведомого (в обычном режиме), а 8-й бит (бит направления) — информацию о направлении передачи.

Как уже отмечалось выше, каждое устройство в шине I2C должно иметь уникальный адрес, но также в системе предусмотрен адрес общего вызова для обращения ко всем ведомым сразу. Адрес ведомого может состоять из фиксированной и программируемой частей. После отправки адресного байта по шине, каждое устройство в системе сравнивает первые семь бит после сигнала СТАРТ со своим адресом. Если адреса совпали, устройство считает себя выбранным как ведомый-приёмник или как ведомый-передатчик, в зависимости от состояния бита направления: низкий уровень означает, что ведущий будет отправлять информацию ведомому высокий означает, что ведущий будет получать информацию от ведомого.

Из спецификации шины следует, что допускаются не только простые форматы обмена, но и комбинированные, когда в промежутке от состояния СТАРТ до состояния СТОП ведущий и ведомый могут выступать и как приемник и как передатчик данных. Комбинированные форматы могут быть использованы, например, для управления последовательной памятью. Типичный сеанс обмена данными по шине I2C проиллюстрирован на следующем изображении:

sch

Рис. 16.

sch

Рис. 17.

Библиотека Wire

У микроконтроллера STM32 имеются пины, на аппаратном уровне поддерживающие интерфейс I2C. Мы будем использовать I2C4. Исходя из функциональной схемы модуля, мы будем использовать пины PB7 и PC6.

Для удобной работы с интерфейсов в среде Arduino IDE имеется стандартная библиотека Wire, создающая одноименный класс. Рассмотрим некоторые его функции.

Общие:

.begin(address) - запуск класса, подключение к шине. Если адрес не указан, значит мы на мастере, если указан, значит это адрес ведомого.

.write() - передача байта или последовательности байт, в зависимости от параметров.

.read() - возвращает очередной принятый байт.

.available() - возвращает количество принятых байтов, доступных для приема.

Функции только для ведущего устройства (мастера):

.beginTransmission(address) - начало передачи данных ведомому устройству с заданным адресом.

.endTransmission() - прекращение передачи данных ведомому устройству.

Далее две функции только для ведомого устройства:

.onReceive(foo) - в качестве параметра указывается функция, вызываемая при получении данных от ведущего.

.onRequest(foo) - в качестве параметра указывается функция, которая вызывается когда требуется отправить данные на ведущий.

Как видно из одного списка функций, работа протокола на ведущем и работа на ведомом устройстве сильно различаются. У ведущего все действия совершаются директивным образом, команда подается тогда, когда это нужно. У ведомого прием и отправка осуществляются автоматически по факту обращения от ведущего. Для приема требуется написать функцию, реагирующую на команды, для отправки необходимо всегда иметь заранее готовые данные.

Реализация

Как говорилось выше, чаще всего протокол I2C используется для общения контроллера (в качестве мастера) с датчиками и исполнительными устройствами. Это удобно, когда требуется высокая скорость и надежность обмена данными. Зачастую под каждое устройство написана собственная библиотека, которая незаметно включает в себя описанные выше функции протокола, но бывает и так, что приходится разбираться с устройством самостоятельно. Тогда берем в руки даташит и реализуем на его основе команды для передачи и приема данных.

Не всегда адрес устройства известен заранее, его просто забывают указать производители. В таком случае можно использовать программу i2c_scanner, которая простым перебором вычисляет адреса подключенных и откликающихся устройств.

Однако, протокол вполне успешно можно применять для обмена данными между микроконтроллерами. Разумеется, один из них, согласно стандарту, будет ведущим, а другой (или другие) - ведомым. Рассмотрим пример такого взаимодействия. Возьмем две платы VBCores и соединим их по схеме из рисунка 18. Суть работы программы такая: ведущий передает данные, ведомый принимает. Ниже представлены две программы: одна для ведущего устройства, а другая – для ведомого.

sch

Рис. 18. Сборочная схема проекта с I2C.

Ведущее устройство:

#include <VBCoreG4_arduino_system.h>
#include <Wire.h> // подключаем библиотеку

#define SDA_PIN PB_7_ALT1
#define SCL_PIN PC6

int address = 8;
byte x = 0;

void setup() {
pinMode(PA5, OUTPUT);

Wire.setSDA(SDA_PIN);
Wire.setSCL(SCL_PIN);
Wire.begin(); // запускаем шину i2c без адреса, т.к. это Мастер
}

void loop() {
digitalWrite(PA5, HIGH); // включаем светодиод
Wire.beginTransmission(address); // начало передачи на устройство номер 8
Wire.write("x is "); // отправляем цепочку текстовых байт
Wire.write(x); // отправляем байт из переменной
Wire.endTransmission(); // останавливаем передачу
digitalWrite(PA5, LOW); // выключаем светодиод

x++; // увеличиваем значение переменной на 1
delay(500); // ждем полсекунды
}

Ведомое устройство:

#include <VBCoreG4_arduino_system.h>
#include <Wire.h> // подключаем библиотеку

#define SDA_PIN PB_7_ALT1
#define SCL_PIN PC6

int address = 8;

void receiveEvent(int arg) { // функция, автоматически вызывается при получении данных
digitalWrite(PA5, HIGH); // включаем светодиод

while (1 < Wire.available())
{ // если принятых данных более 1 байта
char c = Wire.read(); // значит это текстовые байты
Serial.print(c); // выводим их в монитор
}

int x = Wire.read(); // принимаем последний байт в виде int-числа, это данные счетчика
Serial.println(x); // выводим в монитор

digitalWrite(PA5, LOW); // выключаем светодиод
}

void setup() {
pinMode(PA5, OUTPUT);

Wire.setSDA(SDA_PIN);
Wire.setSCL(SCL_PIN);
Wire.begin(address); // запускаем шину с адресом 8, это номер нашего устройства
Wire.onReceive(receiveEvent); // привязываем функцию прерывания

Serial.begin(115200); // запускаем сериал-порт для наблюдения за результатом в мониторе
}

void loop() { // главный цикл пуст
}

Сначала загрузите код на ведущее устройство (левая плата). Затем отключите программатор от него и вставьте в плату справа. Загрузите на неё прошивку ведомого. После этого включите Serial монитор в Arduino IDE и удостоверьтесь в том, что передача осуществляется верно.