От Arduino к Arduino с использованием последовательного интерфейса. Трушин А.Н., Арутюнян М.Г. Организация соединения и обмен данными по bluetooth между Arduino и iOS-приложением Передача данных между двумя arduino

Для связи микроконтроллера с компьютером чаще всего применяют COM-порт. В этой статье мы покажем, как передать команды управления из компьютера и передать данные с контроллера.

Подготовка к работе

Большинство микроконтроллеров обладают множеством портов ввода-вывода. Для связи с ПК наиболее пригоден из них протокол UART. Это протокол последовательной асинхронной передачи данных. Для его преобразования в интерфейс USB на плате есть конвертор USB-RS232 – FT232RL.
Для выполнения примеров их этой статьи вам будет достаточно только Arduino-совместимая плата. Мы используем . Убедитесь, что на вашей плате установлен светодиод, подключенный к 13му выводу и есть кнопка для перезагрузки.

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

int symbol = 33 ; void setup() { Serial. begin(9600 ) ; Serial. println(" ASCII Table ~ Character Map " ) ; } void loop() { Serial. write(symbol) ; Serial. print(" , dec: " ) ; Serial. print(symbol) ; Serial. print(" , hex: " ) ; Serial. print(symbol, HEX) ; Serial. print(" , oct: " ) ; Serial. print(symbol, OCT) ; Serial. print(" , bin: " ) ; Serial. println(symbol, BIN) ; if (symbol = = 126 ) { while (true) { continue ; } } symbol+ + ; }

Переменная symbol хранит код символа. Таблица начинается со значения 33 и заканчивается на 126, поэтому изначально переменной symbol присваивается значение 33.
Для запуска работа порта UART служит функция Serial.begin() . Единственный ее параметр – это скорость. О скорости необходимо договариваться на передающей и приемной стороне заранее, так как протокол передачи асинхронный. В рассматриваемом примере скорость 9600бит/с.
Для записи значения в порт используются три функции:

  1. Serial.write() – записывает в порт данные в двоичном виде.
  2. Serial.print() может иметь много значений, но все они служат для вывода информации в удобной для человека форме. Например, если информация, указанная как параметр для передачи, выделена кавычками – терминальная программа выведет ее без изменения. Если вы хотите вывести какое-либо значение в определенной системе исчисления, то необходимо добавить служебное слово: BIN-двоичная, OCT – восьмеричная, DEC – десятичная, HEX – шестнадцатеричная. Например, Serial.print(25,HEX) .
  3. Serial.println() делает то же, что и Serial.print() , но еще переводит строку после вывода информации.

Для проверки работы программы необходимо, чтобы на компьютере была терминальная программа, принимающая данные из COM-порта. В Arduino IDE уже встроена такая. Для ее вызова выберите в меню Сервис->Монитор порта. Окно этой утилиты очень просто:

Теперь нажмите кнопку перезагрузки. МК перезагрузится и выведет таблицу ASCII:

Обратите внимание на вот эту часть кода:

if (symbol = = 126 ) { while (true) { continue ; } }

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

Отправка команд с ПК

Прежде чем этим заниматься, необходимо получить представление относительного того, как работает COM-порт.
В первую очередь весь обмен происходит через буфер памяти. То есть когда вы отправляете что-то с ПК устройству, данные помещаются в некоторый специальный раздел памяти. Как только устройство готово – оно вычитывает данные из буфера. Проверить состояние буфера позволяет функция Serial.avaliable() . Эта функция возвращает количество байт в буфере. Чтобы вычитать эти байты необходимо воспользоваться функцией Serial.read() . Рассмотрим работу этих функций на примере:

int val = 0 ; void setup() { Serial. begin(9600 ) ; } void loop() { if (Serial. available() > 0 ) { val = Serial. read() ; Serial. print(" I received: " ) ; Serial. write(val) ; Serial. println() ; } }

После того, как код будет загружен в память микроконтроллера, откройте монитор COM-порта. Введите один символ и нажмите Enter. В поле полученных данных вы увидите: “I received: X” , где вместо X будет введенный вами символ.
Программа бесконечно крутится в основном цикле. В тот момент, когда в порт записывается байт функция Serial.available() принимает значение 1, то есть выполняется условие Serial.available() > 0 . Далее функция Serial.read() вычитывает этот байт, тем самым очищая буфер. После чего при помощи уже известных вам функций происходит вывод.
Использование встроенного в Arduino IDE монитора COM-порта имеет некоторые ограничения. При отправке данных из платы в COM-порт вывод можно организовать в произвольном формате. А при отправке из ПК к плате передача символов происходит в соответствии с таблицей ASCII. Это означает, что когда вы вводите, например символ “1”, через COM-порт отправляется в двоичном виде “00110001” (то есть “49” в десятичном виде).
Немного изменим код и проверим это утверждение:

int val = 0 ; void setup() { Serial. begin(9600 ) ; } void loop() { if (Serial. available() > 0 ) { val = Serial. read() ; Serial. print(" I received: " ) ; Serial. println(val, BIN) ; } }

После загрузки, в мониторе порта при отправке “1” вы увидите в ответ: “I received: 110001”. Можете изменить формат вывода и просмотреть, что принимает плата при других символах.

Управление устройством через COM-порт

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

int val = 0 ; void setup() { Serial. begin(9600 ) ; } void loop() { if (Serial. available() > 0 ) { val = Serial. read() ; if (val= = "H" ) digitalWrite(13 , HIGH) ; if (val= = "L" ) digitalWrite(13 , LOW) ; } }

При отправке в COM-порт символа “H” происходит зажигание светодиода на 13ом выводе, а при отправке “L” светодиод будет гаснуть.
Если по результатам приема данных из COM-порта вы хотите, чтобы программа в основном цикле выполняла разные действия, можно выполнять проверку условий в основном цикле. Например.

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

Но давайте ближе к делу. Обмениваться данными с Arduino не так сложно, но вся загвоздка в том, что данные передаются посимвольно, а это очень плохо. В поисках этой проблемы пришлось провести достаточно долгое время, пока на хабрахабре не наткнулся на одну замечательную библиотеку. Автор реализовал в ней функцию приемки чисел, т.е. вы можете отправлять контроллеру числа, состоящие более, чем из одной цифры, и он все отработает корректно. Качайте библиотеку (ссылка), распаковывайте ее в hardwarelibraries, и переходим к практике.

Первым делом напишем скетч, и загрузим его в Arduino (Freeduino)

#include void setup() {

Serial.begin(9600); // устанавливаем скорость порта

PinMode(9, OUTPUT); // устанавливаем 9 ногу как выход для динамика

} void loop()

Long int Number; Serial.print(«Enter number: «);

Number = SerialInput.InputNumber(); // ВВодим число Serial.print(«Result = «);

Serial.println(Number * Number, DEC);

Beep(500);

} void beep(unsigned char delayms){

AnalogWrite(9, 20); // значение должно находится между 0 и 255

// поэкспериментируйте для получения хорошего тона

AnalogWrite(9, 0); // 0 — выключаем пьезо

Delay(delayms); // пауза delayms мс

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

И вот, самое интересное — пришло время пробовать. Для коммутации с контроллером я рекомендую использовать бесплатную программу putty . В настройках Connection type выберите Serial и вместо COM1 впишите корректный номер порта (можно подглядеть в среде программирования Arduino меню Tools->Serial Port). Нажимаем Open, и видим в консоли надпись Enter number, вводим любое число (в рамках разумного), жмем Enter, и видим результат.

Все, можно радоваться и прыгать от радости. Естественно все это можно улучшить, например сперва вывести отправить с контроллера в консоль менюшку, в которой подробно расписать команды. Например вводите число 0 — включается светодиодная подсветка, нажимаете 1 — гаснет. Таким образом можете хоть 100500 команд засунуть, лишь бы хватило памяти микроконтроллера (которой так мало). А о том, как расширить доступную память поговорим в следующий раз.

UPD: часть кода порезал парсер движка, поэтому вот исходник

У меня есть устройство, построенное с помощью Arduino uno:

    Программное обеспечение Arduino, установленное на Arduino uno

    можно управлять с помощью последовательных команд

    можно управлять с помощью физических кнопок и датчиков

    при изменении любой кнопки/датчика он записывает текущее состояние в последовательный

    Если в течение 5 секунд не было отправлено ни одного сообщения, оно отправляет серийное сообщение без изменений

Что нужно:

    Используйте ESP8266 для обеспечения моста между текущим программным обеспечением Arduino и MQTT/web

    Я могу запрограммировать ESP8266 как веб-сервер, клиент MQTT и т. д. с помощью Arduino IDE или Lua (но я предпочитаю IDE Arduino, так как я могу повторно использовать части кода для генерации/интерпретации связи).

    ESP8266 будет обрабатывать все, что требуется для wifi/web/MQTT; без модуля MQTT часть Arduino будет работать автономно, только пульт дистанционного управления будет отсутствовать.

    Я хотел бы внести минимальные изменения в код Arduino (или, если возможно, ни один из них). Любые изменения потребуют обширного повторного тестирования, которого я стараюсь избегать.

    ESP8266 может отсутствовать в некоторых установках.

Какие варианты я нашел:

    <�Литий> Последовательный

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

Одним из преимуществ является отсутствие изменений/испытаний кода, необходимых для части Arduino.

Сделайте Arduino мастером I2C и ESP8266 подчиненным (или наоборот) и реализуйте двунаправленную связь. Получил эту идею, прочитав эту тему .

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

Пакет данных (описание команды или состояния) состоит из 1-20 символов с возможным пиком 20 пакетов за 5 секунд и в среднем по одному пакету каждые 3 секунды. Если необходимо, я могу заставить это отправить 5 целых чисел без знака вместо буквенно-цифровых символов.

Если требуется больше, чем I2C/последовательные контакты, я могу перейти на Arduino Mega (так что количество бесплатных контактов не является проблемой).

Есть ли другие варианты для этого? (протоколы, готовые библиотеки для последовательной связи и т. д.). Я пытаюсь не изобретать велосипед.

Спасибо за ваше время!

3

1 ответы

Большинство учебников I2C сделали каждого Arduino рабом и мастером, но this лучше, потому что каждый Arduino является либо ведущим, либо ведомым (не оба), и переключение не требуется. Это облегчает ситуацию.

I2C лучше, чем серийный, потому что вы можете добавить еще Arduinos в ту же шину.

Я реализовал I2C между двумя Arduinos, и это не сложнее, что чтение/запись на последовательный порт (который вы уже сделали). И я уверен, вы можете обобщить свой код последовательного порта для работы как с последовательным, так и с I2C-сообщением.

Это мой пример (просто доказательство концепции). Ведомый Arduino управляет некоторыми контактами, темп. датчик и сторожевой таймер по приказу мастера Ардуино. Если ведомый не получает бит во времени, он сбрасывает мастер Arduino.

Мастер-код

#include #define CMD_SENSOR 1 #define CMD_PIN_ON 2 #define CMD_PIN_OFF 3 #define CMD_LUMEN 4 #define CMD_BEAT 5 const byte SLAVE_ADDRESS = 42; const byte LED = 13; char buffer; void setup () { Serial.begin(9600); Serial.println("Master"); Wire.begin (); pinMode (LED, OUTPUT); digitalWrite(LED, HIGH); delay(1000); digitalWrite(LED, LOW); Wire.beginTransmission (SLAVE_ADDRESS); Serial.println("Send LED on"); Wire.write (CMD_PIN_ON); Wire.write (2); Wire.write (10); Wire.endTransmission(); int x = Wire.requestFrom(SLAVE_ADDRESS, 1); Serial.print("status="); Serial.println(x); } //end of setup void loop () { Serial.println("."); Wire.beginTransmission (SLAVE_ADDRESS); Wire.write (CMD_SENSOR); Wire.endTransmission(); int x = Wire.requestFrom(SLAVE_ADDRESS, 1); Serial.print("Disponibles = "); Serial.println(x); int temp = (int) Wire.read(); Serial.println(temp); Wire.beginTransmission (SLAVE_ADDRESS); Wire.write (CMD_LUMEN); Wire.endTransmission(); Wire.requestFrom(SLAVE_ADDRESS, 2); int light = Wire.read() << 8 | Wire.read(); Serial.print("Light="); Serial.println(light); Wire.beginTransmission (SLAVE_ADDRESS); Wire.write (CMD_BEAT); Wire.endTransmission(); Wire.requestFrom(SLAVE_ADDRESS, 1); delay (5000); } //end of loop

Ведомый код

/* Esclavo I2C Recibe los siguientes comandos <- 1° byte -> <- 2° byte -> <- 3° byte -> CMD_SENSOR CMD_PIN_ON n° de pin duracion en segundos CMD_PIN_OFF n° de pin CMD_LUMEN CMD_BEAT Cada comando recibe una respuesta, ya sea el valor pedido o un status. */ #include #include typedef struct { int pin; unsigned long off; } PIN_PGMA; /* Lista de pines que se pueden activar via CMD_PIN_ON. */ #define PIN_LUMEN A0 #define PIN_LED 2 #define PIN_RESET 3 PIN_PGMA pgma = { {PIN_LED, 0}, {PIN_RESET, 0} }; const int pgmaSize = sizeof(pgma)/sizeof(PIN_PGMA); #define CMD_SENSOR 1 #define CMD_PIN_ON 2 #define CMD_PIN_OFF 3 #define CMD_LUMEN 4 #define CMD_BEAT 5 #define ST_OK 0 #define ST_BAD_PIN 1 #define ST_TIME_0 2 #define ST_BAD_LEN 3 #define MY_ADDRESS 42 // Maximo tiempo de espera entre comandos CMD_BEAT. Pasado // ese tiempo, se activa el PIN_RESET. // En milisegundos. #define BEAT_INTERVAL 10000 unsigned long lastBeat; // Largo del reset en milisegundos. #define RESET_LENGTH 250 byte cmd = 0; byte status = 0; int thermoDO = 11; int thermoCS = 12; int thermoCLK = 13; MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO); void setup () { Serial.begin(9600); pinMode(PIN_LUMEN, INPUT); analogRead(PIN_LUMEN); for (int i = 0; i < pgmaSize; i++) { pinMode(pgma[i].pin, OUTPUT); digitalWrite(pgma[i].pin, LOW); } lastBeat = millis(); Wire.begin (MY_ADDRESS); Wire.onReceive (receiveCommand); Wire.onRequest (sendAnswer); } void loop() { unsigned long now = millis(); // Baja la linea de RESET si no ha recibido un beat ultimamente. unsigned long diff = now - lastBeat; if (diff > BEAT_INTERVAL) { resetPin(); } // Recorre la lista de pines y apaga aquellos cuyo tiempo termino. for (int i = 0; i < pgmaSize; i++) { if (pgma[i].off > 0 && pgma[i].off <= now) { Serial.print("off pin="); Serial.println(pgma[i].pin); pgma[i].off = 0; digitalWrite(pgma[i].pin, LOW); } } } // called by interrupt service routine when outgoing data is requested void sendAnswer() { byte temp; int lightReading; switch (cmd) { case CMD_SENSOR: temp = thermocouple.readCelsius(); Wire.write(temp); break; case CMD_LUMEN: lightReading = analogRead(PIN_LUMEN); Wire.write(lightReading >> 8); Wire.write(lightReading % 0xFF); break; case CMD_PIN_ON: case CMD_PIN_OFF: case CMD_BEAT: Wire.write(status); status = ST_OK; break; } cmd = 0; } // called by interrupt service routine when incoming data arrives void receiveCommand (int howMany) { cmd = Wire.read (); status = ST_OK; switch (cmd) { case CMD_PIN_ON: cmdPinOn();; break; case CMD_PIN_OFF: cmdPinOff(); break; case CMD_BEAT: lastBeat = millis(); break; } } //end of receiveEvent void cmdPinOff() { if (Wire.available() != 1) { status = ST_BAD_LEN; } else { int pin = Wire.read(); int i = searchPin(pin); if (i < 0) { status = ST_BAD_PIN; } else { pgma[i].off = 0; digitalWrite(pin, LOW); } } } int searchPin(int pin) { int i = pgmaSize - 1; while (i >= 0 && pgma[i].pin != pin) { i--; } return i; } /* * Programa el encendido y duracion del RESET. */ void resetPin() { if (digitalRead(PIN_RESET) == LOW) { unsigned long now = millis(); int i = searchPin(PIN_RESET); pgma[i].off = now + RESET_LENGTH; lastBeat = now; digitalWrite(PIN_RESET, HIGH); } } void cmdPinOn() { if (Wire.available() != 2) { status = ST_BAD_LEN; } else { int pin = Wire.read(); int len = Wire.read(); int i = searchPin(pin); Serial.print("pin="); Serial.print(pin); Serial.print(",index="); Serial.println(i); if (i < 0) { status = ST_BAD_PIN; Serial.println("bad pin"); } else { if (len == 0) { status = ST_TIME_0; Serial.println("ban len"); } else { pgma[i].off = millis() + len * 1000; digitalWrite(pin, HIGH); Serial.println("ok"); } } } }

Загрузим стандартный пример «Physical Pixel» через меню File\Examples\4.Communication\PhysicalPixel. Эта программа ждет данные от компьютера. При получении символа ‘H’ тестовый индикатор загорается, при получении символа ‘L’ – гаснет. Разберем ее исходный код:

int outputPin = 13 ; //здесь храним номер контакта
int val; //здесь будет храниться принятый символ

void setup()
{
Serial.begin (9600 ) ; //установка порта на скорость 9600 бит/сек
pinMode(outputPin, OUTPUT) ; //устанавливаем 13 контакт в режим вывода
}

void loop()
{
if (Serial.available () ) { //если есть принятый символ,
val = Serial.read () ; // то читаем его и сохраняем в val
if (val == "H" ) { // если принят симовол "H",...
digitalWrite(outputPin, HIGH) ; // то включаем светодиод
}
if (val == "L" ) { // если принят симовол "L",
digitalWrite(outputPin, LOW) ; // то выключаем светодиод
}
}
}

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

Как проверить работу этой программы после того, как Вы загрузите ее в микроконтроллер? Нужно найти способ отправлять символы на COM-порт компьютера, чтобы микроконтроллер принимал и обрабатывал их. Существует множество вариантов решения этой задачи.

Используем встроенный в среду разработки Arduino монитор COM-порта

Это наиболее простой, и понятный начинающим метод.

Монитор COM-порта запускается через меню Tools\Serial Monitor, либо через панель инструментов. В старых версиях ПО монитор был доступен только через панель инструментов: . Вызвав монитор убедитесь, что выбрана та же самая скорость обмена, что и в программе микроконтроллера. Теперь можно вводить любые символы в поле ввода справа, и нажимать кнопку «Send» – введенные символы будут отправлены в порт, и там их примет Ваша программа. Введите там латинскую букву «H», нажмите «Send» – тестовый светодиод загорится. Если послать «L» – погаснет. Кстати, все данные, которые Ваша программа будет посылать на COM-порт будут выводиться в окне снизу.

Используем программу эмуляции терминала HyperTerminal

Это немного более сложный в реализации вариант обмена.

В состав Windows обычно включена программа эмуляции терминала HyperTerminal. В Windows XP ее можно найти в меню Пуск \ Все программы \ Программы \ Стандартные \ Связь \ HyperTerminal. При запуске нужно отказаться от создания подключения, выбрать меню Файл \ Свойства. В появившемся диалоге выбрать свой COM-порт, нажать «Настроить», и настроить параметры связи в соответствии с рисунком:

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

Нажмите «OK» в обоих окнах, и попав в основное окно программы, любую клавишу на клавиатуре – HyperTerminal подключится к COM-порту. Теперь все набираемые на клавиатуре символы попадают через COM-порт в микроконтроллер, а все, что отправляет микроконтроллер, попадает на экран. Понажимайте клавиши «H» и «L» (следите за выбранным языком, и регистром) – тестовый светодиод должен загораться и гаснуть.

Напишем собственную программу для ПК!

Этот вариант для настоящих энтузиастов, желающих программировать не только Freeduino, но и ПК. А почему бы и нет? Нам не потребуется изучать детали программирования последовательного порта под Windows, или еще какие-то сложные вещи. Специально для решения подобных простых задач существует язык Processing (http://processing.org), очень похожий синтаксисом и даже средой разработки на программное обеспечение Arduino.

Установите и запустите Processing – Вы увидите среду разработки, похожую на Arduino.

Исходный код программы для языка Processing есть в комментариях ниже основного текста примера Physical Pixel. Здесь он приведен с минимальными изменениями – мы исправили открытие порта, чтобы можно было легко заменить его номер:

import processing.serial.* ;
Serial port;
void setup()
{
size(200 , 200 ) ;
noStroke() ;
frameRate(10 ) ;
port = new Serial(this , "COM5" , 9600 ) ; // !!! Здесь прописать свой COM-порт!!!
}
boolean mouseOverRect() //Возвращает истину, если курсор внутри квадрата
{
return ((mouseX >= 50 ) && (mouseX <= 150 ) && (mouseY >= 50 ) & (mouseY <= 150 ) ) ;
}
void draw()
{
background(#222222 ) ;
if (mouseOverRect() ) // Если курсор внутри квадрата….
{
fill(#BBBBB0) ; // сменить цвет на поярче
port.write ("H" ) ; // послать "H" в микроконтроллер
} else { // если не внутри...
fill(#666660 ) ; // сменить цвет на потемнее
port.write ("L" ) ; // послать "L" в микроконтроллер
}
rect(50 , 50 , 100 , 100 ) ; // нарисовать квадрат
}

Запустите программу (через меню Sketch \ Run) – появится окно с квадратом, при помещении в который курсора мыши, будет загораться светодиод на Freeduino.

Описание языка Processing и его возможностей выходит за рамки этого простого повествования, но во многих примерах для Arduino в комментариях ниже основного текста программы представлен код Processing для ПК, взаимодействующий с Freeduino.