Arduino Serial: начало работы с последовательной связью для отправки команд
В предыдущей статье я кратко рассказал о различных форматах данных и о том, как я рекомендую максимально упрощать вещи. Имея это в виду, для первого проекта давайте создадим простой мигающий светодиод. У нас будет один Arduino, управляющий светодиодом на втором Arduino.
Команды для включения или выключения светодиода будут отправляться по последовательному порту от первого Arduino ко второму. Это максимально базовый вариант. Arduino Blink по дистанционному управлению. У светодиода всего два состояния, поэтому можно использовать простые управляющие коды, и для начала я использую 1 для включения и 0 для выключения.
В этих примерах я использую Arduino Nano, но можно использовать любой тип Arduino. Для этой серии статей я использую связь Arduino с Arduino. Методы абсолютно одинаковы для любого устройства UART-UART. Например, в статье Arduino to Arduino by Bluetooth я использую точно такие же методы последовательной связи для беспроводной отправки команд через Bluetooth.
Пример #1: удалённое управление миганием
Простая схема из двух Arduino со следующими подключениями:
Пин 0 (RX) на Arduino #1 подключается к пину 1 (TX) на Arduino #2.
Пин 1 (TX) на Arduino #1 подключается к пину 0 (RX) на Arduino #2.
GND Arduino #1 к GND Arduino #2
Arduino #2: D2 к светодиоду + резистор (220 или 330 Ом)
Скетч примера #1: удалённое управление миганием
Master Arduino передаёт команды:
// Arduino Serial Example #1 Remote Control Blink - Master
// www.martyncurrey.com
void setup()
{
Serial.begin(9600);
// wait for the serial port to connect. Required for Leonardo/Micro native USB port only
while (!Serial) { ; }
}
void loop()
{
Serial.print(1);
delay(1000);
Serial.print(0);
delay(1000);
}
Slave Arduino получает команды. Если получает 1 — включает светодиод, если 0 — выключает.
// Arduino Serial Example #1 Remote Control Blink - Slave
// www.martyncurrey.com
char c = ' ';
byte LED = 2;
void setup()
{
pinMode(LED, OUTPUT);
Serial.begin(9600);
Serial.println("START");
}
void loop()
{
if(Serial.available())
{
char c = Serial.read();
if (c=='0') { digitalWrite(LED, LOW); }
if (c=='1') { digitalWrite(LED, HIGH); }
Serial.println(c);
}
}
Это довольно просто.
Arduino #1 передаёт «1», ждёт секунду, затем передаёт «0», ждёт ещё секунду и начинает заново.
Стоит упомянуть, что Arduino #1 будет продолжать передавать значения независимо от того, слушает ли его кто-то.
Помните, что при использовании Serial.print() числа преобразуются в ASCII. Это означает, что фактически передаются значения 48 для 0 и 49 для 1. Это важно знать при создании кода для принимающего Arduino.
Скетч на Arduino #2 инициализирует аппаратный последовательный канал, выводит сообщение «START» в монитор порта, а затем постоянно проверяет входящие данные. Вся магия происходит в функции loop().
if(Serial.available())
{
char c = Serial.read();
Serial.println(c);
if (c=='0') { digitalWrite(LED, LOW); }
if (c=='1') { digitalWrite(LED, HIGH); }
}
Сначала проверяется, есть ли данные в буфере Serial:
char c = Serial.read();
В этот момент у нас есть значение в «c». Нас интересуют только «0» и «1», поэтому следующая часть проверяет эти значения.
if (c=='0') { digitalWrite(LED, LOW); }
if (c=='1') { digitalWrite(LED, HIGH); }
Если c=='0', светодиод выключается с помощью digitalWrite(LED, LOW);, а если c=='1', светодиод включается с помощью digitalWrite(LED, HIGH);. Любое другое значение игнорируется.
Обратите внимание, что 0 и 1 заключены в одинарные кавычки. Это потому, что это ASCII-символы, и я использую переменную типа char. С char используйте одинарные кавычки „0“ и „1“, а не двойные «0» и «1». Поскольку я использую char, я мог бы также проверять фактическое значение, а не ASCII-символ.
Поскольку нас интересуют только «1» и «0», всё остальное игнорируется. Master-устройство может отправить любой символ, но slave реагирует только на «1» и «0».
Код ниже работает точно так же, но его чуть сложнее читать:
if (c==48) { digitalWrite(LED, LOW); }
if (c==49) { digitalWrite(LED, HIGH); }
Если вы уже собрали схему, вам нужно отключить последовательные соединения перед загрузкой скетчей. Если вы забудете отключить последовательные соединения, IDE попытается загрузить скетч, потерпит неудачу и в итоге истечёт время ожидания. Конечно, после загрузки нужно подключить обратно. Это одна из проблем при использовании аппаратного Serial — постоянное отключение/подключение быстро надоедает.
Загрузите скетчи, и у вас должно получиться удалённое мигание. Помните, что управляет светодиодом именно master Arduino.
Вы также можете проверить полученные данные, открыв монитор порта на slave Arduino.
Пример #2: удалённое управление миганием — 2 светодиода
Используя тот же метод, довольно просто добавить второй светодиод, так что давайте попробуем. Подключите второй светодиод (с резистором) к Slave Arduino на пин D3.
Скетч примера #2: удалённое управление миганием с 2 светодиодами
// Arduino Serial Example #2 Remote Control Blink With 2 LEDs -Master
// www.martyncurrey.com
void setup()
{
Serial.begin(9600);
// wait for the serial port to connect. Required for Leonardo/Micro native USB port only
while (!Serial) { ; }
}
void loop()
{
Serial.print(1);
Serial.print(3);
delay(1000);
Serial.print(0);
Serial.print(2);
delay(1000);
}
Я добавил 2 новых команды для управления вторым светодиодом:
«3» для включения светодиода
«2» для его выключения
// Arduino Serial Example #2 Remote Control Blink With 2 LEDs - Slave
// www.martyncurrey.com
char c = ' ';
byte LED1 = 2;
byte LED2 = 3;
void setup()
{
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
Serial.begin(9600);
Serial.println("START");
}
void loop()
{
if(Serial.available())
{
char c = Serial.read();
if (c=='0') { digitalWrite(LED1, LOW); }
if (c=='1') { digitalWrite(LED1, HIGH); }
if (c=='2') { digitalWrite(LED2, LOW); }
if (c=='3') { digitalWrite(LED2, HIGH); }
Serial.println(c);
}
}
Две новые команды обрабатываются следующим образом:
if (c=='2') { digitalWrite(LED2, LOW); }
if (c=='3') { digitalWrite(LED2, HIGH); }
Вы должны увидеть, что дополнительные светодиоды можно добавлять, просто используя больше команд.
Пример #3: удалённое управление кнопками
Вышеописанное работает хорошо и очень надёжно, но если всё, что вам нужно — включать и выключать что-то через равные промежутки времени, нет необходимости использовать два Arduino. Поэтому давайте усложним задачу, добавив пару кнопок для управления светодиодами.
К существующей схеме на Arduino #1 добавьте кнопки на пины A0/D14 и A1/D15. Если вы ещё не знаете, аналоговые пины можно использовать как цифровые (кроме A5 и A6, которые только аналоговые). Я выбрал эти пины только потому, что схема выглядит аккуратнее. Другой причины нет.
Я не буду подробно объяснять код работы с кнопками. Если вам интересно узнать больше, смотрите руководства по включению и выключению.
Скетч примера #3: удалённое управление кнопками
// Example #3. Sketch: Remote Control With Push Button Switches. Master
// www.martyncurrey.com
byte buttonSwitch1 = 14;
byte buttonSwitch2 = 15;
boolean buttonState1 = false;
boolean buttonState2 = false;
boolean buttonState3 = false;
boolean buttonSwitch1_State_old = false;
boolean buttonSwitch2_State_old = false;
void setup()
{
Serial.begin(9600);
// wait for the serial port to connect. Required for Leonardo native USB port only
while (!Serial) { ; }
Serial.print("Start");
}
void loop()
{
// simple debounce
buttonState1 = digitalRead(buttonSwitch1); delay(1);
buttonState2 = digitalRead(buttonSwitch1); delay(1);
buttonState3 = digitalRead(buttonSwitch1); delay(1);
if ( (buttonState1 == buttonState2) && (buttonState1 == buttonState3) )
{
// has the button switch state changed?
if (buttonState1 != buttonSwitch1_State_old)
{
buttonSwitch1_State_old = buttonState1;
if (buttonSwitch1_State_old == HIGH) { Serial.print(1);} else { Serial.print(0);}
}
}
buttonState1 = digitalRead(buttonSwitch2); delay(1);
buttonState2 = digitalRead(buttonSwitch2); delay(1);
buttonState3 = digitalRead(buttonSwitch2); delay(1);
if ( (buttonState1 == buttonState2) && (buttonState1 == buttonState3) )
{
// has the button switch state changed?
if (buttonState1 != buttonSwitch2_State_old)
{
buttonSwitch2_State_old = buttonState1;
if (buttonSwitch2_State_old == HIGH) { Serial.print(3);} else { Serial.print(2);}
}
}
}
Состояние кнопок проверяется, и при изменении состояния отправляется управляющий код. Главное — коды отправляются только при изменении положения переключателя.
Скетч slave не изменился:
// Example #3. Sketch: Remote Control With Push Button Switches. Slave
// www.martyncurrey.com
char c = ' ';
byte LED1 = 2;
byte LED2 = 3;
void setup()
{
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
Serial.begin(9600);
Serial.println("START");
}
void loop()
{
if(Serial.available())
{
char c = Serial.read();
if (c=='0') { digitalWrite(LED1, LOW); }
if (c=='1') { digitalWrite(LED1, HIGH); }
if (c=='2') { digitalWrite(LED2, LOW); }
if (c=='3') { digitalWrite(LED2, HIGH); }
Serial.println(c);
}
}
Теперь вместо таймера для отправки команд используются кнопки. Замкните переключатель — светодиод загорается. Разомкните — светодиод гаснет.
Меня немного раздражает, что кнопки включают «не те» светодиоды, но я оставлю вам возможность исправить это самостоятельно.
Пример #4: удалённое управление миганием через Software Serial
Если вы повторяли примеры, то, подозреваю, вас, как минимум немного, раздражало постоянное отключение и подключение проводов к последовательным пинам. При написании этого руководства я сам забывал это сделать как минимум пару раз.
Хотя аппаратный Serial всегда лучший выбор, когда он доступен, он может доставлять неудобства при разработке кода. В оставшихся примерах я начну использовать одну из программных библиотек Serial. Если помните из предыдущего руководства, лучший вариант — AltSoftSerial, за ним NeoSerial, а стандартная SoftwareSerial — в конце. В этом примере я перевожу пример 3 с аппаратного Serial на программный, используя библиотеку SoftwareSerial, поставляемую с Arduino IDE. Не лучший выбор, но полезный в данном сценарии.
Технически можно использовать пины 0 и 1 для программного Serial, но это сводит на нет всю идею. Поэтому на master Arduino я использую пины 2 и 3 (2 для TX и 3 для RX), а на slave Arduino — пины 11 и 12 (11 для RX и 12 для TX).
Скетч примера #4: удалённое управление через Software Serial
// Example #4 Remote Control Blink Using Software Serial - Master
// www.martyncurrey.com
#include <SoftwareSerial.h>
SoftwareSerial SoftSerial(2, 3); // RX, TX
byte buttonSwitch1 = 14;
byte buttonSwitch2 = 15;
boolean buttonState1 = false;
boolean buttonState2 = false;
boolean buttonState3 = false;
boolean buttonSwitch1_State_old = false;
boolean buttonSwitch2_State_old = false;
void setup()
{
SoftSerial.begin(9600);
}
void loop()
{
// simple debounce
buttonState1 = digitalRead(buttonSwitch1); delay(1);
buttonState2 = digitalRead(buttonSwitch1); delay(1);
buttonState3 = digitalRead(buttonSwitch1); delay(1);
if ( (buttonState1 == buttonState2) && (buttonState1 == buttonState3) )
{
// has the button switch state changed?
if (buttonState1 != buttonSwitch1_State_old)
{
buttonSwitch1_State_old = buttonState1;
if (buttonSwitch1_State_old == HIGH) { SoftSerial.print(1);} else { SoftSerial.print(0);}
}
}
buttonState1 = digitalRead(buttonSwitch2); delay(1);
buttonState2 = digitalRead(buttonSwitch2); delay(1);
buttonState3 = digitalRead(buttonSwitch2); delay(1);
if ( (buttonState1 == buttonState2) && (buttonState1 == buttonState3) )
{
// has the button switch state changed?
if (buttonState1 != buttonSwitch2_State_old)
{
buttonSwitch2_State_old = buttonState1;
if (buttonSwitch2_State_old == HIGH) { SoftSerial.print(3);} else { SoftSerial.print(2);}
}
}
}
Как видите, я заменил строку инициализации аппаратного Serial на:
SoftSerial.begin(9600);
Откуда взялось «SoftSerial»? Это имя, которое я дал экземпляру программного Serial, и это делается при объявлении библиотеки. Я мог бы использовать любое имя (в разумных пределах), например SWS или sSerial.
#include <SoftwareSerial.h>
SoftwareSerial SoftSerial(2, 3); // RX, TX
При объявлении библиотеки программного Serial нужно указать, какие пины использовать. Здесь я указываю пин D2 для приёма и D3 для передачи.
// Example #4 Remote Control Blink Using Software Serial - Slave
// www.martyncurrey.com
#include <SoftwareSerial.h>
SoftwareSerial SoftSerial(11, 12); // RX, TX
char c = ' ';
byte LED1 = 2;
byte LED2 = 3;
void setup()
{
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
SoftSerial.begin(9600);
}
void loop()
{
if(SoftSerial.available())
{
char c = SoftSerial.read();
if (c=='0') { digitalWrite(LED1, LOW); }
if (c=='1') { digitalWrite(LED1, HIGH); }
if (c=='2') { digitalWrite(LED2, LOW); }
if (c=='3') { digitalWrite(LED2, HIGH); }
}
}
В скетче slave пины D2 и D3 уже используются (плохое планирование, знаю), поэтому я использовал D11 и D12:
#include <SoftwareSerial.h>
SoftwareSerial SoftSerial(11, 12); // RX, TX
Если попробуете, то обнаружите, что всё работает точно так же, как в примере 3. Разница в том, что нам не нужно отключать последовательные соединения для загрузки нового скетча.
Это было очень мягкое введение в использование последовательной связи для отправки управляющих кодов от Arduino к Arduino. Использование одиночных символьных кодов или команд позволяет сохранять код простым. Один символ можно загрузить в переменную char или byte, которую легко сравнивать с помощью простого ==. Нет необходимости в массивах символов или строках. Если символ нам не нужен, его можно проигнорировать и прочитать следующий.
if(SoftSerial.available())
{
char c = SoftSerial.read();
if (c=='0') { digitalWrite(LED1, LOW); }
if (c=='1') { digitalWrite(LED1, HIGH); }
if (c=='2') { digitalWrite(LED2, LOW); }
if (c=='3') { digitalWrite(LED2, HIGH); }
}
Важный момент — буфер Serial проверяется на наличие данных перед попыткой чтения. Это позволяет создавать неблокирующий код, который продолжает работать, пока нет данных для обработки.