Введение
Протоколы связи — основа встраиваемых систем. Читаете ли вы датчик, общаетесь с SD-картой или выводите отладочные сообщения — вы используете один из трех основных протоколов: Serial (UART), I2C (TWI) или SPI. В этом учебнике мы запустим все три одновременно на Arduino Uno с помощью браузерного симулятора Velxio. Вы увидите, как сканировать шину I2C, читать/записывать EEPROM, выполнять передачи SPI и выводить всё через Serial.

Обзор схемы
В этом примере используется только сама плата Arduino Uno — внешние компоненты не требуются. Встроенные периферийные устройства Arduino предоставляют всё необходимое:
- Serial (UART): использует пины 0 (RX) и 1 (TX) для связи с монитором порта.
- I2C (TWI): использует пины A4 (SDA) и A5 (SCL). Внутренние подтягивающие резисторы включаются библиотекой Wire.
- SPI: использует пины 10 (SS), 11 (MOSI), 12 (MISO) и 13 (SCK). Пин 10 настроен как выход выбора ведомого.
Подключение не требуется, так как все соединения внутренние для Arduino. Холст Velxio показывает Uno с подписями пинов, и вы можете просмотреть виртуальные компоненты, открыв панель выбора компонентов.

Разбор кода
Давайте разберем скетч шаг за шагом.
Библиотеки и определения
#include <Wire.h>
#include <SPI.h>
#define DS1307_ADDR 0x68
#define EEPROM_ADDR 0x50
#define SS_PIN 10
Мы подключаем библиотеку Wire для I2C и библиотеку SPI для SPI. Определены два адреса I2C: 0x68 для RTC DS1307 (хотя физически он отсутствует, мы будем его искать) и 0x50 для EEPROM 24Cxx. Пин выбора ведомого SPI установлен на цифровой пин 10.
Вспомогательные функции
byte bcdToDec(byte val) {
return ((val >> 4) * 10) + (val & 0x0F);
}
void readRTC(byte &hr, byte &min, byte &sec) {
Wire.beginTransmission(DS1307_ADDR);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDR, 3);
sec = bcdToDec(Wire.read() & 0x7F);
min = bcdToDec(Wire.read());
hr = bcdToDec(Wire.read() & 0x3F);
}
Функция bcdToDec преобразует двоично-десятичный код (BCD) в десятичный. readRTC считывает секунды, минуты и часы из RTC DS1307. Маски & 0x7F и & 0x3F очищают неиспользуемые биты.
void writeEEPROM(byte reg, byte value) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
delay(5);
}
byte readEEPROM(byte reg) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, 1);
return Wire.available() ? Wire.read() : 0xFF;
}
Эти функции записывают и читают один байт из EEPROM I2C по адресу 0x50. delay(5) гарантирует, что EEPROM успеет завершить цикл записи.
byte spiTransfer(byte data) {
digitalWrite(SS_PIN, LOW);
byte result = SPI.transfer(data);
digitalWrite(SS_PIN, HIGH);
return result;
}
Эта функция выполняет полнодуплексную передачу SPI: опускает SS, отправляет байт, получает ответ и поднимает SS.
Setup
void setup() {
Serial.begin(9600);
Wire.begin();
pinMode(SS_PIN, OUTPUT);
digitalWrite(SS_PIN, HIGH);
SPI.begin();
Serial.println("===================================");
Serial.println(" Multi-Protocol Demo");
Serial.println(" Serial (USART) + I2C (TWI) + SPI");
Serial.println("===================================");
Serial.println();
// ── I2C: Scan bus ──
Serial.println("[I2C] Scanning bus...");
int found = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print(" Found device at 0x");
if (addr < 16) Serial.print("0");
Serial.println(addr, HEX);
found++;
}
}
Serial.print(" ");
Serial.print(found);
Serial.println(" device(s) on I2C bus.");
Serial.println();
// ── I2C: Write/read EEPROM ──
Serial.println("[I2C] EEPROM write/read test:");
writeEEPROM(0, 42);
writeEEPROM(1, 99);
byte v0 = readEEPROM(0);
byte v1 = readEEPROM(1);
Serial.print(" Wrote 42, read ");
Serial.print(v0);
Serial.println(v0 == 42 ? " [OK]" : " [FAIL]");
Serial.print(" Wrote 99, read ");
Serial.print(v1);
Serial.println(v1 == 99 ? " [OK]" : " [FAIL]");
Serial.println();
// ── SPI: Transfer test ──
Serial.println("[SPI] Transfer test:");
byte spiData[] = {0xAA, 0x55, 0x42};
for (int i = 0; i < 3; i++) {
byte rx = spiTransfer(spiData[i]);
Serial.print(" TX=0x");
if (spiData[i] < 16) Serial.print("0");
Serial.print(spiData[i], HEX);
Serial.print(" RX=0x");
if (rx < 16) Serial.print("0");
Serial.println(rx, HEX);
}
Serial.println();
Serial.println("Setup complete. Reading RTC...");
Serial.println();
}
В setup() мы инициализируем все три протокола. Затем мы:
- Сканируем шину I2C на наличие устройств с адресами от 1 до 126.
- Тестируем EEPROM, записывая значения 42 и 99 по адресам 0 и 1, затем читая их обратно и проверяя.
- Тестируем SPI, отправляя три байта (
0xAA,0x55,0x42) и выводя полученные байты.
Loop
void loop() {
// ── Serial: Print RTC time every 2 seconds ──
byte hr, min, sec;
readRTC(hr, min, sec);
Serial.print("[RTC] ");
if (hr < 10) Serial.print("0");
Serial.print(hr);
Serial.print(":");
if (min < 10) Serial.print("0");
Serial.print(min);
Serial.print(":");
if (sec < 10) Serial.print("0");
Serial.print(sec);
Serial.print(" | Uptime: ");
Serial.print(millis() / 1000);
Serial.println("s");
delay(2000);
}
Каждые две секунды цикл считывает время RTC и выводит его вместе с временем работы системы. Это демонстрирует непрерывный вывод через Serial, пока I2C и SPI простаивают.
Ключевые концепции
- Serial (UART): Асинхронный, точка-точка. Используется для отладочного вывода и связи с ПК или модулями, такими как GPS.
- I2C: Синхронный, мультимастерный, использует два провода (SDA, SCL). Каждое устройство имеет уникальный адрес. Отлично подходит для датчиков и EEPROM.
- SPI: Синхронный, полнодуплексный, использует четыре провода (MOSI, MISO, SCK, SS). Быстрее I2C, но требует больше пинов.
- Сканирование шины: Перебор адресов I2C для обнаружения подключенных устройств.
- Чтение/запись EEPROM: Энергонезависимая память, сохраняющая данные после отключения питания.
- Передача SPI: Одновременная отправка и получение данных.
Частые ошибки и советы по отладке
- Подтягивающие резисторы I2C: Библиотека Wire включает внутренние подтяжки, но для длинных шин рекомендуются внешние резисторы (4.7 кОм).
- Пин SS SPI: Всегда настраивайте SS как выход и держите его высоким, когда не используется, чтобы случайно не войти в ведомый режим.
- Время записи EEPROM: После записи подождите не менее 5 мс перед следующей операцией.
- Скорость передачи монитора порта: Убедитесь, что монитор порта настроен на 9600 бод.
- Отсутствие внешних устройств: В этой симуляции сканирование I2C не найдет устройств (кроме, возможно, внутреннего TWI Arduino). Тест EEPROM использует виртуальную EEPROM, встроенную в симулятор.
Предлагаемые расширения
- Добавьте реальные датчики: Подключите RTC DS1307 и EEPROM 24Cxx к шине I2C, а модуль SD-карты через SPI.
- Записывайте данные в EEPROM: Сохраняйте показания датчиков и извлекайте их после сброса.
- Используйте прерывания: Запускайте передачу SPI по нажатию кнопки.
- Реализуйте командный интерфейс: Разбирайте команды Serial для управления операциями I2C и SPI.
Попробуйте сами
Готовы увидеть все три протокола в действии? Откройте живой пример в Velxio, скомпилируйте и запустите. Наблюдайте за выводом монитора порта, пока Arduino сканирует шину I2C, тестирует EEPROM, выполняет передачи SPI, а затем непрерывно выводит время RTC.
Запустить мультипротокольную демонстрацию на Velxio

Удачного программирования!