Кратко про протоколы взаимодействия в микроконтроллерах: SPI, IDC, UART, CAN

904df0fbc30e99f3ae1bb24175a961ce.png

Привет, Хабр!

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

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

В этой статье рассмотрим такие протоколы взаимодействия как SPI, IDC, UART, CAN

SPI

Протокол SPI — это синхронный серийный интерфейс, используемый для обмена данными между микроконтроллерами и различными периферийными устройствами, например, сенсорами, SD-картами, модулями памяти и так далее. Разработан он был компанией Motorola в 1980-х годах, чтобы обеспечить высокоскоростную, прямую и эффективную передачу данных. С тех пор SPI стал широко распространённым и популярным благодаря своей простоте.

Сама суть SPI — это мастер-слейв архитектура, где одно устройство выступает в роли мастера, управляя обменом, а одно или несколько устройств работают как слейвы. Обмен данными происходит по четырем основным линиям:

  1. MOSI (Master Out Slave In) — линия для передачи данных от мастера к слейву.

  2. MISO (Master In Slave Out) — линия для передачи данных от слейва к мастеру.

  3. SCK (Serial Clock) — линия синхронизации, управляемая мастером.

  4. SS (Slave Select) — линия выбора слейва, также управляется мастером.

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

Технические параметры

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

Режимы работы определяются четырьмя комбинациями фазы и полярности тактового сигнала (CPOL и CPHA), что позволяет точно настроить протокол под требования конкретного устройства.

Мультиплексирование SS линий позволяет одному мастеру управлять несколькими слейвами, используя индивидуальные линии выбора слейва или декодеры адреса для экономии выводов микроконтроллера.

Пример реализации

Рссмотрим классический пример SPI, исходя из ситуации, когда нужно подключить и работать с SD-картой с использованием микроконтроллера, где SPI находит своё применение благодаря необходимости высокоскоростного обмена данными.

Цель: интеграция SD-карты с микроконтроллером для чтения и записи данных. Для этого используется SPI-интерфейс.

Компоненты:

  • Микроконтроллер (например, Arduino Uno)

  • SD-карта и модуль чтения SD-карт

  • Провода для подключения

Шаг 1: подключение модуля SD-карты к микроконтроллеру

  1. MOSI микроконтроллера подключается к MOSI модуля SD-карты.

  2. MISO микроконтроллера подключается к MISO модуля SD-карты.

  3. SCK микроконтроллера подключается к SCK модуля SD-карты.

  4. SS или иногда обозначаемый как CS микроконтроллера подключается к CS модуля SD-карты.

  5. Питание и земля подключаются соответственно.

Шаг 2: настройка и инициализация SPI в коде

Для Arduino это может выглядеть примерно так:

#include 
#include 

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // ждем подключения серийного порта
  }
  
  Serial.print("Инициализация SD-карты...");
  
  if (!SD.begin(10)) { // пин CS подключен к 10 пину
    Serial.println("инициализация не удалась!");
    return;
  }
  Serial.println("инициализация успешна.");
}

void loop() {
  //  код для работы с SD-картой
}

Код инициализирует SPI-интерфейс и SD-карту, используя библиотеки SPI и SD, предоставляемые Arduino. В setup() происходит проверка успешности подключения SD-карты. В loop() можно реализовать чтение или запись данных на SD-карту.

Шаг 3: чтение и запись данных

Для работы с файлами на SD-карте используются функции SD.open(), file.read(), file.write() и другие, предоставляемые библиотекой SD.

I2C

I2C, которого часто произносят как ай-ту-си, был разработан Philips (ныне NXP Semiconductors) в начале 1980-х годов как средство для облегчения коммуникации между чипами внутри телевизоров.

I2C — это двухпроводной, синхронный серийный протокол. Он использует две линии для связи:

I2C поддерживает мульти-мастер конфигурации, что позволяет нескольким мастерам управлять шиной, не мешая друг другу.

Каждое слейв устройство на шине I2C имеет уникальный адрес, который используется мастером для инициации коммуникации. Адреса могут быть 7-битными или 10-битными.

Процесс обмена данными:

  1. Старт: мастер инициирует передачу, отправляя условие СТАРТ, затем передает адрес слейва с битом чтения/записи.

  2. Адресация: Если слейв с соответствующим адресом присутствует на шине, он отвечает сигналом подтверждения (ACK).

  3. Передача данных: после получения ACK, мастер или слейв (в зависимости от операции) начинает передачу данных, с подтверждением после каждого байта.

  4. Стоп: мастер завершает передачу, отправляя условие СТОП.

Тех параметры:

I2C поддерживает несколько скоростей: стандартная (100 кбит/с), быстрая (400 кбит/с), и быстрая+ (1 Мбит/с). Есть и экспериментальные реализации с ещё более высокими скоростями, но они как правило редко используемые.

В I2C используется техника подтягивания линий к питанию через резисторы.

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

Ну и как уже было сказано, устройства могут иметь 7-битные или 10-битные адреса.

Пример реализации

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

Компоненты:

  • Микроконтроллер (Arduino Uno, Nano, или любой другой с поддержкой I2C)

  • Датчик температуры на I2C (например, TMP102)

  • Подключающие провода

Подключение датчика к микроконтроллеру:

  1. Подключите VCC датчика к 5V на Arduino (или к 3.3V, если датчик на 3.3V).

  2. Подключите GND датчика к GND на Arduino.

  3. Подключите SDA (данные) датчика к A4 на Arduino Uno (или соответствующему SDA-пину на других платах).

  4. Подключите SCL (тактовый сигнал) датчика к A5 на Arduino Uno (или соответствующему SCL-пину на других платах).

Программирование микроконтроллера:

#include 

// адрес датчика TMP102
const int tempSensorAddress = 0x48; 

void setup() {
  Wire.begin(); // инициализация I2C
  Serial.begin(9600); // начало серийной связи для вывода данных
}

void loop() {
  // запрос на чтение 2 байтов из датчика температуры
  Wire.requestFrom(tempSensorAddress, 2); 
  if(Wire.available() == 2) {
    byte MSB = Wire.read(); // старший байт
    byte LSB = Wire.read(); // младший байт

    // преобразование в температуру в соответствии с документацией датчика
    int temp = ((MSB << 8) | LSB) >> 4; 

    // преобразование в градусы Цельсия
    float celsius = temp * 0.0625;
    Serial.print("Температура: ");
    Serial.print(celsius);
    Serial.println(" C");
  }
  delay(1000); // задержка для читаемости
}

После загрузки программы на микроконтроллер, открываем монитор порта в Arduino IDE, чтобы увидеть текущую температуру, считываемую датчиком. Эти данные будут обновляться раз в секунду.

UART

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

UART работает, передавая и принимая данные через два провода: TX (передача) и RX (прием).

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

Скорость передачи данных в UART обычно измеряется в бодах (битах в секунду). Скорость передачи обеих сторон должна быть согласована для корректной работы. Распространенные скорости включают 9600, 19200, 38400, 57600, 115200 бод и выше.

Стандартная конфигурация включает 8 бит данных, хотя возможны и другие настройки (например, 7 бит данных). Бит четности (мжет быть нечетным, четным или отсутствовать) используется для обнаружения ошибок в передаваемых данных.

В отличие от интерфейсов I2C и SPI, UART не имеет строгих требований к физическому уровню.

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

Многие UART-интерфейсы имеют встроенные буферы для передачи и приема.

Реализация

Например, нужно организовать обмен строкой «Привет, UART!» между двумя микроконтроллерами.

Оборудование:

  • 2 микроконтроллера (например, Arduino Uno, STM32, ESP32 и т.п.).

  • Провода для соединения TX/RX пинов и земли между микроконтроллерами.

Подключение:

  1. Микроконтроллер 1:

    • TX → RX Микроконтроллера 2

    • RX <- TX Микроконтроллера 2

    • GND соединяется с GND Микроконтроллера 2

  2. Микроконтроллер 2:

    • RX соединен с TX Микроконтроллера 1

    • TX соединен с RX Микроконтроллера 1

    • GND соединяется с GND Микроконтроллера 1

Прогаем первый передатчик:

void setup() {
  // инициализация Serial порта со скоростью 9600 бод.
  Serial.begin(9600);
}

void loop() {
  // отправка сообщения на Микроконтроллер 2
  Serial.println("Привет, UART!");
  // задержка, чтобы не перегружать канал сообщениями.
  delay(2000); // задержка 2 секунды
}

Приемник:

void setup() {
  // инициализация Serial порта со скоростью 9600 бод.
  Serial.begin(9600);
}

void loop() {
  if (Serial.available() > 0) {
    // чтение полученной строки и вывод в Serial Monitor.
    String received = Serial.readStringUntil('\n');
    Serial.print("Получено сообщение: ");
    Serial.println(received);
  }
}

Работа будет выглядеть так:

  1. Передатчик инициализирует свой Serial порт и в цикле отправляет строку «Привет, UART!» на Микроконтроллер 2 каждые две секунды.

  2. Приемник также инициализирует свой Serial порт и ожидает входящие данные. Как только данные поступают, он считывает их до символа новой строки ('\n'), после чего выводит полученное сообщение в свой Serial Monitor.

CAN

CAN был разработан в начале 1980-х годов компанией Bosch для нужд автомобильной индустрии. CAN — это многоузловой, серийный коммуникационный протокол, который позволяет множеству устройств (нод) на автобусе общаться друг с другом без необходимости главного управляющего устройства. Это децентрализованный протокол, который предназначен для работы в условиях высоких электромагнитных помех.

CAN использует два провода для передачи данных (CAN_H и CAN_L). Разница напряжений между CAN_H и CAN_L используется для определения логических уровней. Именно это позволяет CAN работать в условиях сильных электромагнитных помех.

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

Форматы кадров:

CAN поддерживает различные типы кадров:

  • Кадр данных: переносит данные от отправителя к получателю.

  • Кадр запроса: запрос на отправку данных.

  • Кадр ошибки: передается нодой, обнаружившей ошибку.

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

CAN поддерживает различные скорости передачи данных, обычно от 125 кбит/с до 1 Мбит/с. Скорость влияет на максимально возможную длину шины: чем выше скорость, тем короче шина. Например, при скорости 1 Мбит/с максимальная длина шины составляет около 40 метров, в то время как при 125 кбит/с она может достигать более 500 метров.

Каждое сообщение в сети CAN имеет ID, который определяет его приоритет во время арбитража. Существует два формата идентификаторов: стандартный (11 бит) и расширенный (29 бит).

Пример реализации

Нужно организовать обмен сообщениями между двумя нодами (Node A и Node B) через CAN-интерфейс.

Оборудование:

  • 2 микроконтроллера с поддержкой CAN (например, STM32F103C8T6).

  • CAN-трансиверы (например, SN65HVD230), по одному на каждый микроконтроллер.

  • Цепь питания для микроконтроллеров и трансиверов.

  • Провода для соединения микроконтроллеров с трансиверами и соединения CAN_H и CAN_L линий между трансиверами.

Подключение:

  1. Подключите CAN_H и CAN_L выходы первого трансивера к CAN_H и CAN_L входам второго трансивера соответственно.

  2. Подключите TX и RX микроконтроллера к CAN TX и CAN RX трансивера.

  3. Подключите питание и землю к микроконтроллерам и трансиверам.

Для Node A и Node B напишем простой код, который будет отправлять и принимать CAN-сообщения. Предположим, что используется HAL библиотека для STM32.

Node A:

#include "stm32f1xx_hal.h"
#include "main.h"
#include "can.h"

CAN_HandleTypeDef hcan;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_CAN_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_CAN_Init();

  CAN_TxHeaderTypeDef TxHeader;
  uint8_t message[5] = {'H', 'e', 'l', 'l', 'o'};
  uint32_t TxMailbox;

  TxHeader.DLC = 5; // Длина сообщения
  TxHeader.IDE = CAN_ID_STD;
  TxHeader.RTR = CAN_RTR_DATA;
  TxHeader.StdId = 0x123; // Идентификатор сообщения

  while (1)
  {
    if(HAL_CAN_AddTxMessage(&hcan, &TxHeader, message, &TxMailbox) == HAL_OK) {
      // Сообщение успешно отправлено
    }
    HAL_Delay(1000); // Задержка 1 секунда
  }
}

Node B:

#include "stm32f1xx_hal.h"
#include "main.h"
#include "can.h"

CAN_HandleTypeDef hcan;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_CAN_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_CAN_Init();

  CAN_RxHeaderTypeDef RxHeader;
  uint8_t receivedMessage[5];

  while (1)
  {
    if(HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &RxHeader, receivedMessage) == HAL_OK) {
      // Сообщение успешно принято
      // Можно добавить код для обработки сообщения
    }
  }
}

Сравним эти протоколы в таблице для наглядности и закрепления:

Характеристика

SPI

I2C

UART

CAN

Тип соединения

Синхронное

Синхронное

Асинхронное

Синхронное

Количество проводов

4 и более (MOSI, MISO, SCK, SS для каждого слейва)

2 (SDA и SCL)

2 (TX и RX)

2 (CAN_H и CAN_L)

Мульти-мастер

Нет (обычно один мастер)

Да

Не применимо

Да

Скорость передачи

До нескольких десятков Мбит/с

До 5 Мбит/с (в режиме High-Speed)

Обычно до 1 Мбит/с

До 1 Мбит/с

Дальность

Несколько метров

До нескольких метров (до 1 м для High-Speed)

Десятки метров (зависит от скорости)

До 1 км (при низких скоростях)

Адресация

По линиям SS

По адресу на шине

Не требуется

По идентификатору сообщения

Комплексность

Средняя (требует управления несколькими линиями для мульти-слейв конфигураций)

Средняя (сложность возрастает с количеством устройств на шине)

Низкая (прост в реализации)

Высокая (требует точной настройки параметров)

Устойчивость к помехам

Низкая до средней (зависит от реализации)

Средняя

Средняя

Высокая

Поддержка арбитража

Нет

Да (через механизм столкновений и повторов)

Не применимо

Да (арбитраж без потерь)

Применение

Высокоскоростная передача в закрытых системах

Подключение периферии, датчиков

Простая серийная коммуникация

Автомобильные сети, промышленные сети

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

Напоследок хочу порекомендовать вам бесплатный вебинар, на котором коллеги из OTUS расскажут про важные аспекты обеспечения питания для стационарных и носимых устройств. Вы узнаете о различных типичных уровнях напряжений, используемых в современной электронике, и методах их реализации. Принципы батарейного менеджмента, необходимые для оптимизации срока службы и эффективности и многое другое.

© Habrahabr.ru