• I






      
           

Научно-популярный образовательный ресурс для юных и начинающих радиолюбителей - Popular science educational resource for young and novice hams

Основы электричества, учебные материалы и опыт профессионалов - Basics of electricity, educational materials and professional experience

КОНКУРС
language
 
Поиск junradio

Радиодетали
ОК
Сервисы

Stock Images
Покупка - продажа
Фото и изображений


 
Выгодный обмен
электронных валют

Друзья JR



JUNIOR RADIO

 

Последовательные интерфейсы в Arduino



 

Сам по себе микроконтроллер AVR обладает сравнительно скудными возможностями. Всего 16МГц, около двух десятков пинов (мега328), и несколько килобайт памяти. Для решения каких-то локальных задач этого вполне достаточно. Но часто приходится решать задачи контроля и управления гораздо более серьезные - системы радиоуправления моделями, системы аля "Умный дом", различные погодные станции и т.д. Такие проекты предполагают распределение управляющих и исполнительных устройств на площади значительно большей, чем несколько квадратных сантиметров вокруг платы Arduino. Для их решения одним микроконтроллером уже не обойтись. Поэтому такие проекты дробят на более мелкие составные задачи, связанные между собой потоками данных. Каждая такая подзадача решается при помощи своего микроконтроллера. Возьмем к примеру систему радиоуправления самолетом. В ней будут участвовать пульт управления, радиомодуль, приемник, регулятор оборотов двигателя и 4 сервопривода. Пульт управления решает задачу получения команд с органов управления. Полученные данные он обрабатывает и передает радиомодулю. Основной задачей последнего является передача команд управления с пульта на модель посредством радиоканала. На стороне модели этот радиосигнал надо принять, расшифровать и отдать соответствующие команды исполнительным механизмам. Эту задачу решает приемник. Исполнительными механизмами являются сервоприводы и регулятор оборотов. Их основная задача сводится к управлению электродвигателями. Делают они это в соответствии с полученными с приемника командами. Но самое интересное заключается в том, что в каждом из выше перечисленных устройств стоит свой микроконтроллер, который и решает задачи устройства. Для того чтобы эта система работала как единое целое, микроконтроллеры должны иметь способ обмениваться между собой информацией. Собственно обмен информацией между двумя микроконтроллерами предполагает передачу байтов между ними. Т.к. в одном байте 8 бит, да еще нужен управляющий сигнал, получаем что в простейшем случае для организации обмена данными между двумя микроконтроллерами от каждого из них надо задействовать минимум по 9 выводов. Такой способ передачи называется параллельным. Т.е. все биты одного байта передаются одновременно параллельно друг другу. При условии, что у нас их всего около двух десятков, это непозволительное расточительство. Но данные передавать все равно надо, а пины сохранить тоже хочется. Поэтому пришли к последовательной передаче данных. В этом случае биты передаются друг за другом по единственному проводу. Но в этом случае встает вопрос правильного понимания принимающим МК того набора импульсов, который формирует МК передающий. Для решения этой проблемы возникает понятие протокола передачи данных. Протокол по своей сути - это алгоритм кодирования байтов импульсами таким образом, чтобы принимающий МК, зная этот протокол, мог однозначно преобразовать импульсы обратно в байты. Существует несколько основных протоколов, которые в микроконтроллерах AVR реализованы на уровне "железа". Т.е. передачу данных посредством такого протокола осуществляет специальный модуль внутри МК. Такая "железячная" реализация хороша тем, что основное ядро МК не тратит свое время на кодирование и передачу данных. Протокол в совокупности с модулем его реализующим можно образно себе представить  в виде некоего штекера, к которому можно подключать другие устройства и/или МК. Такой "штекер" называется интерфейсом.

Асинхронный последовательный порт UART

Самым простым интерфейсом в 328-ой меге (Arduino Uno, Nano, Mini) является UART - асинхронный последовательный порт. В больших ПК так же есть его аналог - COM-порт или RS232. Хоть протоколы их и полностью совпадают, реализация "железа" имеет принципиальные отличия. UART предназначен для передачи информации на расстояние в несколько сантиметров и потому не имеет защиты от помех. В этой реализации передаются TTL-уровни (0-5В). RS232 в свою очередь предполагает простейшую связи между двумя ПК на расстоянии в несколько метров. На таком расстоянии влияние помех становится уже ощутимым. Но поскольку интерфейс простейший, то и способ борьбы с помехами так же простейший - посредством изменения уровней сигнала. RS232 предполагает уровни в пределах от -15 до +15 вольт. Сразу ясно, что прямое соединение UART с RS232 однозначно спалит UART. Но необходимость такого соединения частенько возникает... В этом случае используют специальные преобразователи. Последние не изменяя протокол передачи данных изменяют физические величины сигналов таким образом, чтоб соединение было безопасным и ничего не сгорело. В нашем случае таким преобразователем(RS232<->TTL) является микросхема MAX232. Теперь немного подробнее о протоколе UART. Протокол предполагает двусторонний обмен данными между двумя устройствами. При этом оба устройства являются равноправными, т.е. нет явно выраженного деления на главного и подчиненного. Для организации обмена данными используется всего две линии. По одной линии данные передаются от первого устройства ко второму, а по другой - в обратном направлении. У МК при этом будут задействованы два пина. Обозначаются они соответственно Tx - передающий, и Rx - принимающий. Линии передачи соединяют Tx устройства 1 с Rx устройства 2 и Rx устройства 1 с Tx устройства 2. Они как бы перекрещиваются. Рассмотрим передачу данных устройства 1 на устройство 2. Обратное направление будет работать идентично этому.
Для начала надо вспомнить, что порт асинхронный. Это значит, что обе стороны не синхронизированы друг с другом. Принимающая сторона ничего не знает о частоте импульсов, с которой будут передаваться данные, и в какой момент эта передача будет начата. Не зная отправной точки (начало отсчета) невозможно правильно разделить данные на биты и составить из них правильные байты. Значит надо синхронизировать обе стороны. Для этого вначале передатчик выдает на линию несколько байт синхронизации вида 01010101 или 10101010. Принимая их, приемник настраивается на частоту импульсов передатчика. После такой синхронизации частоты передатчик начинает последовательно загонять в линию биты из буфера. А приемник в свою очередь с ранее полученной частотой считывает с линии значения.

Рассматривая Arduino Uno (Nano/Mini) соответствующие выводы мы найдем под номерами D0(Rx) и D1(Tx). Но при их использовании надо учитывать, что для загрузки программы в микроконтроллер наша плата Arduino как раз и использует последовательный порт. Поэтому при прошивке микроконтроллера требуется отключать все периферийные устройства от этих выводов.
Использовать последовательный порт на Arduino очень просто. Для этого в языке предусмотрен специальный класс Serial. Этот класс обладает следующими методами:

  • begin(speed) – открывает последовательный порт и устанавливает скорость передачи данных. Обычно используются стандартизированные скорости (9600, 14400, 19200, 28800, 38400, 57600 или 115200), но можно указать и любую другую. Важно, чтобы порт на обоих сторонах был открыт с одинаковой скоростью.
  • available() – функция возвращает количество байт, принятых микроконтроллером в специальный буфер и доступных для считывания в программу. Буфер может хранить до 64 байт.
  • read() – считывает один байт из буфера последовательного соединения. При этом считанный байт из буфера уходит. Байты из буфера считываются в той последовательности, в которой они поступили в порт.
  • write(val) – записывает один (или несколько) байт в последовательный порт.
  • print(val) – записывает в порт последовательность байтов в виде строки. Так же есть функция println(val). Она отличается от данной тем, что в конце строки передает байт указания перехода на новую строку.

Последовательный порт SPI

Связать два устройства между собой - это хорошо. Но есть и несколько проблем: низкая скорость и малое число связываемых устройств. Аппаратный UART у Arduino Uno всего один, а нам бы неплохо иметь возможность связи с несколькими устройствами. Можно конечно сделать UART программный. Но в этом случае неминуемо придется задействовать ресурсы основного ядра микроконтроллера. Как итог - скорость выполнения программы заметно снизится. Плюс ко всему каждое подключенное устройство затребует по 2 пина микроконтроллера. Чтобы решить все эти проблемы изобрели другой последовательный интерфейс - SPI. От части он похож на UART, но обладает существенно лучшими показателями скорости и позволяет подключать большое количество устройств, выделяя на каждое устройство всего по одному пину. Не обошлось тут и без жертв. В первую очередь на алтарь легло равноправие. В SPI есть четкое разделение на ведущее устройство (master) и ведомые (slave). Управляет передачей данных всегда только master, slave лишь отвечает. Следующей жертвой в угоду скорости стала асинхронность. В интерфейсе предусмотрена специальная линия синхронизации, которой управляет естественно master. И вот что получилось по пинам/линиям связи:

  • MOSI - master out slave in - линия передачи от ведущего к ведомому
  • MISO - master in slave out - линия передачи от ведомого к ведущему
  • SCK - по этой линии передаются импульсы синхронизации
  • SS - slave select - линия выбора ведомого

Как видно, задействуется в двое больше пинов МК, нежели в UART. Но давайте вспомним о изначальной задаче - присоединение большого числа устройств к нашему МК для организации обмена данными с ними на большой скорости. В этом свете рассмотрим использование выше перечисленных линий. Подчиненные устройства нанизываются на первые 3 как гирлянда и только SS требуется для каждого устройства свой. Если для подключения 10 устройств по UART нам потребовалось бы 20 пинов, то в случае SPI будет задействовано всего 13. И что еще немаловажно 9 из 10 устройств, подключенных по UART будут "сидеть" на программной реализации интерфейса, а с SPI все 10 будут использовать аппаратный интерфейс, что позволит снизить нагрузку на основное ядро МК.

Протокол SPI устроен следующим образом:

  1. Master при помощи линии SS выбирает с каким устройством он будет работать.
  2. Master на линию SCK выдает тактовые импульсы.
  3. С каждым тактовым импульсом master пишет в линию MOSI следующий бит передаваемых данных, а slave в свою очередь его считывает
  4. Одновременно с п.3 slave на каждый тактовый импульс пишет в линию MISO следующий бит своих данных. Master соответственно считывает этот бит.
  5. Master отключает SS, закрывая тем самым обмен данными с устройством.

Как видим, в этом алгоритме по сравнению с UART есть двойной выигрыш в скорости - синхронизация производится по отдельной линии и данные передаются одновременно в двух направления. Из-за его простоты и высокой скорости именно этот интерфейс чаще всего используют для подключения периферийных устройств к МК. Для программного использования SPI на Arduino есть одноименная библиотека. Здесь я не буду описывать всю гамму подключаемых устройств по этому протоколу. Для примера остановлюсь лишь на двух простейших: регистр ввода позволяет считать цифровые данные по 8-ми входам, и регистр вывода позволяет вывести данные на 8 цифровых выходов.

Входной регистр на SPI

Реализация входного регистра производится на микросхеме 74HC165. Назначение ее выводов следующее:

  • Vcc — питание
  • GND — земля
  • SH/L͞D — защёлка, SS (SPI)
  • CLK — тактовый вход, SCLK (SPI)
  • A-H — входы, состояние которых считывается в регистр
  • QH — последовательный вывод, MISO (SPI)
  • Q͞H — инверсный вывод, на нём идут биты с QH, но инвертированные
  • SER — последовательный ввод; к нему можно подсоединить вывод QH второго регистра, получив каскадное подключение
  • CLK INH — Clock Inhibit, или инвертированный Clock Enable; когда на нём 1, тактирование выключено

Подключим ее по следующей схеме:

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

Код на Arduino будет выглядеть следующим образом:

// подключаем библиотеку для работы с SPI
#include <SPI.h>
#define BUTTONS_SPI 8 // SS для вхлдного регистра

// Инициализация SPI
void buttonsInit()
{
  SPI.begin();
  pinMode(BUTTONS_SPI, OUTPUT);
  digitalWrite(BUTTONS_SPI, HIGH);
}

// Чтение кнопок с SPI
byte buttonsRead()
{
  /* Выставим на защёлке сначала низкий, потом - высокий уровни.
   * Сдвиговый регистр запомнит уровни сигналов на входах и сможет
   * их нам потом отдать бит за битом.
   */
  digitalWrite(BUTTONS_SPI, LOW);
  digitalWrite(BUTTONS_SPI, HIGH);
  /* Читаем запомненные состояния входов. Ноль шлём просто потому,
   * что transfer() одновременно и шлёт, и принимает.
   */
  byte dState = SPI.transfer(0);
 
  return dState;
}

void setup() {
  buttonsInit();
  Serial.begin(9600);
}

void loop() {
  byte val = buttonsRead();
  Serial.print("val = "); Serial.println(val);
  delay(100);
}

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

Выходной регистр на SPI

Реализация входного регистра производится на микросхеме 74HC595. Назначение ее выводов следующее:

  • Vcc — питание, от 2 до 6 В
  • GND — земля
  • QA-QH — эти выводы соответствуют битам, записанными по SPI
  • SI — вход ведомого, MOSI (SPI)
  • G — Output Enabe; когда на этом выводе низкий уровень, выводы включены (подключены к «защёлкам»), когда высокий — выводы переходят в состояние Hi-Z
  • RCK — защёлка, SS (SPI); при установке низкого уровня выводы регистра защёлкиваются
  • SCK — тактовый вход, SCLK (SPI)
  • SCLR — Shift Register Clear Input; если на этом выводе низкий уровень, очищает все триггеры по фронту тактового сигнала на SCLK. С нашей точки зрения это банальный RESET: прижал к земле — сбросил все биты регистра
  • QH' — на этом выводе будет появляться старший переданный бит

Подключим ее по следующей схеме:

В этой схеме по понятным причинам не используется линия MISO.

Напишем код, который выведет на светодиоды "бегущий огонек":

#include <SPI.h>

#define LIGHTS_SS 8

byte ls = 1;

void writeLights()
{
  digitalWrite(LIGHTS_SS, LOW); // выбор ведомого - нашего регистра
  SPI.transfer(ls);
  digitalWrite(LIGHTS_SS, HIGH);
}

void setup()
{

  SPI.begin();
  pinMode(LIGHTS_SS, OUTPUT);
  writeLights();
}


/* Эта функция сдвигает биты влево на одну позицию, перемещая старший бит
 * на место младшего. Другими словами, она "вращает" биты по кругу.
 */
void shiftLight()
{
  ls = ls<<1;
  if (ls==0) ls=1;
}


void loop()
{
  shiftLight();

  /* Записываем значение в сдвиговый регистр */
  writeLights();

  delay(1000 / 8); // пробегаем все 8 светодиодов за 1 секунду
}

Интерфейс I2C

Еще один достаточно распространенный интерфейс, заслуживающий упоминания - I2C (или по другому IIC). Так же как и SPI он призван обеспечивать обмен данными между МК и несколькими периферийными устройствами. Его отличительной особенностью является использование всего двух линий на все устройства. Здесь жертвой стала простота. Итак, в интерфейсе предусмотрено всего две линии SDA - двунаправленная линия данных и SDL - так же двунаправленная линия синхронизации. Любое устройство на шине I2C может быть одного из двух типов: Master (ведущий) или Slave (ведомый). Обмен данными происходит сеансами. "Мастер"-устройство полностью управляет сеансом: инициирует сеанс обмена данными, управляет передачей, подавая тактовые импульсы на линию SDL, и завершает сеанс. Кроме этого, в зависимости от направления передачи данных и "Мастер" и "Слэйв"-устройства могут быть "Приёмниками" или "Передатчиками". Когда "Мастер" принимает данные от "Слэйва" - он является "Приёмником", а "Слэйв" - "Передатчиком". Когда же "Слэйв" принимает данные от "Мастера", то он уже является "Приёмником", а "Мастер" в этом случае является "Передатчиком".

Не надо путать тип устройства "Мастер" со статусом "Передатчика". Несмотря на то, что при чтении "Мастером" информации из "Слэйва", последний выставляет данные на шину SDA, делает он это только тогда, когда "Мастер" ему это разрешит, установкой соответствующего уровня на линии SDL. Так что, хотя "Слэйв" в этом случае и управляет шиной SDA, - самим обменом всё равно управляет "Мастер".

В режиме ожидания (когда не идёт сеанс обмена данными) обе сигнальные линии (SDA и SDL) находятся в состоянии высокого уровня (притянуты к питанию). Каждый сеанс обмена начинается с подачи "Мастером" так называемого Start-условия. "Старт-условие" - это изменение уровня на линии SDA с высокого на низкий при наличии высокого уровня на линии SDL. После подачи "Старт-условия" первым делом "Мастер" должен сказать с кем он хочет пообщаться и указать, что именно он хочет - передавать данные в устройство или читать их из него. Для этого он выдаёт на шину 7-ми битный адрес "Слэйв" устройства (по другому говорят: "адресует "Слэйв" устройство"), с которым хочет общаться, и один бит, указывающий направление передачи данных (0 - если от "Мастера" к "Слэйву" и 1 - если от "Слэйва" к "Мастеру"). Первый байт после подачи "Старт"-условия всегда всеми "Слэйвами" воспринимается как адресация. Поскольку направление передачи данных указывается при открытии сеанса вместе с адресацией устройства, то для того, чтобы изменить это направление, необходимо открывать ещё один сеанс (снова подавать "Старт"-условие, адресовать это же устройство и указывать новое направление передачи). После того, как "Мастер" скажет, к кому именно он обращается и укажет направление передачи данных, - начинается собственно передача: "Мастер" выдаёт на шину данные для "Слэйва" или получает их от него. Эта часть обмена (какие именно данные и в каком порядке "Мастер" должен выдавать на шину, чтобы устройство его поняло и сделало то, что ему нужно) уже определяется каждым конкретным устройством. Заканчивается каждый сеанс обмена подачей "Мастером" так называемого Stop-условия, которое заключается в изменении уровня на линии SDA с низкого на высокий, опять же при наличии высокого уровня на линии SDL. Если на шине сформировано Stop-условие, то закрываются все открытые сеансы обмена. Внутри сеанса любые изменения на линии SDA при наличии высокого уровня на линии SDL запрещены, поскольку в это время происходит считывание данных "Приёмником". Если такие изменения произойдут, то они в любом случае будут восприняты либо как "Старт"-условие (что вызовет прекращение обмена данными), либо как "Стоп"-условие (что будет означать окончание текущего сеанса обмена). Соответственно, во время сеанса обмена установка данных "Передатчиком" (выставление нужного уровня на линии SDA) может происходить только при низком уровне на линии SDL.

Несколько слов по поводу того, в чём в данном случае разница между "прекращением обмена данными" и "окончанием сеанса обмена". В принципе "Мастеру" разрешается, не закрыв первый сеанс обмена, открыть ещё один или несколько сеансов обмена с этим же (например, как было сказано выше, для изменения направления передачи данных) или даже с другими "Слэйвами", подав новое "Старт"-условие без подачи "Стоп"-условия для закрытия предыдущего сеанса. Управлять линией SDA, для того, чтобы отвечать "Мастеру", в этом случае будет разрешено тому устройству, к которому "Мастер" обратился последним, однако старый сеанс при этом нельзя считать законченным. И вот почему. Многие устройства (например те же eeprom-ки 24Схх) для ускорения работы складывают данные, полученные от "Мастера" в буфер, а разбираться с этими полученными данными начинают только после получения сигнала об окончании сеанса обмена (то есть "Стоп-условия"). То есть, например, если на шине висит 2 микросхемы eeprom 24Cxx и вы открыли сеанс записи в одну микросхему и передали ей данные для записи, а потом, не закрывая этот первый сеанс, открыли новый сеанс для записи в другую микросхему, то реальная запись и в первую и во вторую микросхему произойдёт только после формирования на шине "Стоп-условия", которое закроет оба сеанса. После получения данных от "Мастера" eeprom-ка складывает их во внутренний буфер и ждёт окончания сеанса, для того, чтобы начать собственно процесс записи из своего внутреннего буфера непосредственно в eeprom. То есть, если вы после после передачи данных для записи в первую микруху не закрыли этот сеанс, открыли второй сеанс и отправили данные для записи во вторую микруху, а потом, не сформировав "Стоп-условие", выключили питание, то реально данные не запишутся ни в первую микросхему, ни во вторую. Или, например, если вы пишете данные попеременно в две микрухи, то в принципе вы можете открыть один сеанс для записи в первую, потом другой сеанс для записи во вторую, потом третий сеанс для записи опять в первую и т.д., но если вы не будете закрывать эти сеансы, то в конце концов это приведёт к переполнению внутренних буферов и в итоге к потере данных. Здесь можно привести такую аналогию: ученики в классе ("слэйвы") и учитель ("мастер"). Допустим учитель вызвал какого-то ученика (пусть будет Вася) к доске и попросил его решить какой-то пример. После того как Вася этот пример решил, учитель вызвал к доске Петю и начал спрашивать у него домашнее задание, но Васю на место не отпустил. Вот в этом случае вроде бы разговор с Васей закончен, - учитель разговаривает с Петей, но Вася стоит у доски и не может спокойно заниматься своими делами (сеанс общения с ним не закрыт).

В случае, если "Слэйв" во время сеанса обмена не успевает обрабатывать данные, - он может растягивать процесс обмена, удерживая линию SDL в состоянии низкого уровня, поэтому "Мастер" должен проверять возврат линии SDL к высокому уровню после того, как он её отпустит.

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

Внутри сеанса передача состоит из пакетов по девять бит, передаваемых в обычной положительной логике (то есть высокий уровень - это 1, а низкий уровень - это 0). Из них 8 бит передаёт "Передатчик" "Приёмнику", а последний девятый бит передаёт "Приёмник" "Передатчику". Биты в пакете передаются старшим битом вперёд. Последний, девятый бит называется битом подтверждения ACK (от английского слова acknowledge - подтверждение). Он передаётся в инвертированном виде, то есть 0 на линии соответствует наличию бита подтверждения, а 1 - его отсутствию. Бит подтверждения может сигнализировать как об отсутствии или занятости устройства (если он не установился при адресации), так и о том, что "Приёмник" хочет закончить передачу или о том, что команда, посланная "Мастером", не выполнена. Каждый бит передаётся за один такт. Та половина такта, во время которой на линии SDL установлен низкий уровень, используется для установки бита данных на шину передающим абонентом (если предыдущий бит передавал другой абонент, то он в это время должен отпустить шину данных). Та половина такта, во время которой на линии SDL установлен высокий уровень, используется принимающим абонентом для считывания установленного значения бита с шины данных.

 

По материалам rc-master,  автор Зайчиков Александр

 

В начало обзора



Купить радиодетали для ремонта




Необходимо добавить материалы...
Результат опроса Результаты Все опросы нашего сайта Архив опросов
Всего голосовало: 379



          

Радио для всех© 2024