Arduino монитор порта кракозябры

Проблемы с несколькими аргументами в Serial.print

Проблема при объединении текста и чисел и выводе в print в одной строке

Часто для отладки программы требуется вывести несколько значений, снабдив их каким-то комментарием. Например, такой текст:  «Sensor’s value is:  15». Если вы просто используете такой код:

Serial.print(“Sensor’s value is:  15”);

,то все отобразится правильно. Но если вы попытаетесь вместо подстроки «15» вставить реальное показание датчика, объединив строку и числовое значение, то увидите, что строка выводится некорректно.

Serial.print(“Sensor’s value is:  ” + analogRead (A0));

Этот код даст непредсказуемый результат, в мониторе порта вы не увидите или пустоту или случайный набор символов. Причина ошибки в механизме конвертации типов данных. При объединении строк  «на лету», как в нашем примере, ардуино не знает, как интерпретировать типы данных для аргументов при операторе суммирования. В процессе такой непредсказуемой конвертации и результат может быть непредсказуемым.

Для решения этой проблемы вы можете использовать два способа:

Первый вариант. Объявить предварительно переменную типа String, инициализировать ее константой-строкой и использовать в аргументе print. Такой вот пример будет работать:

String str = “Sensor’s value is:  “;

Serial.println(str + analogRead(A0));

Второй, более предпочтительный и удобный вариант: использование функции print несколько раз:

Serial.print(“Sensor’s value is:  “);

Serial.println(analogRead(A0));

Более подробно прочитать об особенностях  работы со строками при выводе информации в монитор порта можно на официальной странице Ардуино.

Проблема объединения нескольких строк в аргументе функции print

Попробуйте загрузить скетч с таким фрагментом:

Serial.print(“Sensor’s value is:  ” + analogRead (A0) + “15 cm”);

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

Создание String с помощью энкодера


Схема подключения энкодера и дисплея к Ардуино Уно

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

Скетч. Создание переменной String энкодером

#include <Wire.h>                             // библиотека для протокола I2C
#include <LiquidCrystal_I2C.h>       // библиотека для LCD 1602 
LiquidCrystal_I2C LCD(0x27,20,2);  // присваиваем имя дисплею

#include "RotaryEncoder.h"      // библиотека для энкодера
RotaryEncoder encoder(4, 2);  // пины подключение энкодера (DT, CLK)
#define SW 6                              // пин подключения порты SW энкодера

byte scale = 5;  // указываем сколько символов должно быть в строке

// создаем массив из 39 символов - его можно увеличивать и уменьшать
char massiv = {
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M',
  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
  ' ', '-', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};

String simvol;
String stroka;
byte w;
int pos;
int newPos;
boolean buttonWasUp = true;

void setup() {
   LCD.init();                                       // инициализация дисплея
   LCD.backlight();                             // включение подсветки
   pinMode(SW, INPUT_PULLUP);  // подключаем пин SW

   // выводим первый символ в массиве на дисплей
   simvol = massiv;
   LCD.setCursor(w, 0);
   LCD.print(simvol);
}

void loop() {

   while (w < scale) {
     // проверяем положение ручки энкодера
     encoder.tick(); newPos = encoder.getPosition();

     // указываем максимальный и минимальный диапазон энкодера
     if (newPos > 38) { encoder.setPosition(0); }
     if (newPos < 0) { encoder.setPosition(38); }

     // если положение энкодера изменилось  - выводим на монитор символ
     if (pos != newPos && newPos <= 38 && newPos >= 0) {
        pos = newPos;
        simvol = massiv;
        LCD.setCursor(w, 0);
        LCD.print(simvol);
     }

     // узнаем, отпущена ли кнопка энкодера сейчас
     boolean buttonIsUp = digitalRead(SW);
     // если кнопка была отпущена и не отпущена сейчас
     if (buttonWasUp && !buttonIsUp) {
        // исключаем дребезг контактов кнопки энкодера
        delay(10);
        // узнаем состояние кнопки энкодера снова
        buttonIsUp = digitalRead(SW);
        // если кнопка была нажата, то сохраняем символ в строку
        if (!buttonIsUp) {
           w = w + 1;
           stroka = stroka + simvol;
           encoder.setPosition(0);
           LCD.setCursor(w, 0);
           LCD.print(simvol);
      }
    }
    // запоминаем состояние кнопки энкодера
    buttonWasUp = buttonIsUp;
    }

   // если было введено 5 символов - выходим из цикла while
   LCD.clear();
   LCD.setCursor(0, 0);
   LCD.print("ITOG:");
   LCD.setCursor(0, 1);
   LCD.print(stroka);
   delay(1000);
}

Пояснения к коду:

  1. переменная в данной программе отвечает не только за выход из цикла while, но и положение курсора в строке на дисплее 1602;
  2. при нажатии на кнопку энкодера происходит увеличение переменной , сохранение символа в строку и обнуление позиции энкодера.

Serial Port Monitor overview

Serial Port Monitor is a highly-functional and friendly utility that works with RS232/RS422/RS485 ports and allows you to save a lot of your time while developing and testing serial applications and hardware. It offers the full set of advanced port monitoring features, including a built-in terminal, unique data filtering options, convenient data viewing modes, session playback capability, and more.

  • If a COM port you want to monitor is already opened by a third-party application, Serial Port Analyzer will still be able to connect to it and record its activity. It will capture the port’s data and display it to you. Serial Analyzer software will let you select whether to show serial data in the Table, Line, Dump or Terminal mode. Also, you can enable all available visualizers at a time.

  • With the Table view, you get the data in the form of a table consisting of the recorded IRPs. When you select the Line view, you will see more details about each request passing in and out of a monitored COM port. The Dump view will display all incoming and outgoing data in hexadecimal and string formats as a data dump. In the Terminal mode, the monitoring data will be shown as ASCII characters.

  • All captured data, including serial input/output control codes (IOCTLs) with full details, can be recorded to a selected file. Also, you can save your monitoring session and load it next time you need it. Serial Port Monitor software allows reading serial port data in real time, which means you’ll be able to solve any problem immediately after it occurs.

  • Serial Port Sniffer lets you test an unlimited number of COM ports within one RS232 monitoring session. Just select “Start session” in the main menu of the app and get the ability to track the communications of any serial program with several devices at a time. All data will be recorded using the first-in, first-out method, which makes it more convenient for analysis.

  • With RS232 monitor software, you can not only log serial traffic but also emulate sending data to a particular serial port as though it were sent from the monitored app. This enables you to easily check how a device connected to this port reacts to specific data.

  • The Modbus Sniffer makes it possible to perform monitoring and analysis of Modbus communications. It allows intercepting Modbus RTU and Modbus ASCII frames exchanged between Modbus-enabled devices or apps.

Форматирование и конвертация строк

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

Варианты значений констант для форматирования:

  • DEC – обычное число в десятичной системе исчисления
  • BIN – преобразует в двоичный код и выведет строку, содержащую только символы 0 и 1
  • OCT – преобразует в восьмеричную систему исчисления
  • HEX – преобразует в шестнадцатеричную систему
  • Цифра от 0 до 9 – используется, если первый аргумент – вещественное число с плавающей запятой. Форма указывает количество знаков после запятой, которые останутся при выводе. Само число при этом будет округлено.

Примеры:

В старых версиях ардуино можно было использовать еще один параметр BYTE. Начиная с версии 1.0 эта константа не поддерживается и компилятор выдаст вам ошибку «Ключевое слово ‘BYTE’ больше не поддерживается». Для вывода ASCII символа по его коду нужно использовать метод write().

Управление светодиодом с клавиатуры

Напишем пример управления встроенным светодиодом с клавиатуры. Если нажата клавиша 1, то светодиод должен загореться, при нажатии клавиши выключим светодиод.

Часть кода нам уже знакома — мы используем встроенный светодиод под номером 13.

Сигнал от компьютера поступает в виде байта. Создаём новую переменную incomingByte для этих целей.

Последовательный порт включается командой begin() с указанием скорости.

Если с компьютера поступает сигнал, то функция available() вернёт количество байт, доступное для чтения. Таким образом, мы просто убеждаемся, что какой-то сигнал пришёл (больше нуля).

После первой проверки мы проверяем введённый символ, который может быть представлен и как байт. Если символ равен единице, то включаем светодиод, как мы делали раньше. Если символ равен 0, то выключаем.

Как это выглядит на практике. Заливаем скетч и запускаем Serial Monitor (Ctrl+Shift+M). В окне Serial Monitor наверху есть текстовое поле. Вводим в него числа 1 или 0 и нажимаем кнопку Send. Можно также нажать клавишу Enter для быстрого ввода.

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

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

Функция Serial.end() закрывает последовательное соединение, порты RX и TX освобождаются и могут быть использованы для ввода/вывода.

В различных уроках вы будете принимать сигналы от платы Arduino. Это полезно, например, для отладки приложения, когда вы выводите сообщения и по ним ориентируетесь, какая часть программа работает, а какая — нет. Способность общения между Arduino и компьютером очень важна. Вы можете принимать сигналы не только в Arduino IDE, но и в других приложениях на компьютере. Например, в связке с Arduino часто используют приложение Processing, в котором рисуют графики поступаемых сигналов.

Если вы больше не нуждаетесь в получении данных, то закрывайте окно Serial Monitor.

Также существует библиотека SoftwareSerial. Она позволяет осуществить последовательную передачу
данных через другие цифровые контакты Arduino.

Отправка данных

Мы можем не только получать данные с платы, но и отправлять данные на плату, заставляя выполнять команды с компьютера.

Допустим, мы будем посылать символ «1» из Processing. Когда плата обнаружит присланный символ, включим светодиод на порту 13 (встроенный).

Скетч будет похож на предыдущий. Для примера создадим небольшое окно. При щелчке в области окна будем отсылать «1» и дублировать в консоли для проверки. Если щелчков не будет, то посылается команда «0».

Теперь напишем скетч для Arduino.

Запускаем оба скетча. Щёлкаем внутри окна и замечаем, что светодиод загорается. Можно даже не щёлкать, а удерживать кнопку мыши нажатой — светодиод будет гореть постоянно.

Внутри ATmega328

В основе данного подраздела лежит техническое описание на ATmega328 версии Rev. 8271C – 08/10. Приводимые мной страницы могут немного отличаться от текущей версии.

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

Просмотр технического описания на микроконтроллер показывает нам, как работает двухпроводная (Two Wire) система. Стоит отметить:

Микроконтроллер включает в себя аппаратный модуль TWI, который обрабатывает связь через шину I2C. … Интересно, что это означает, что связь не обрабатывается библиотекой исключительно программно, как вы могли бы подумать! Другими словами, библиотека сама в программе не создает битовый поток. Библиотека взаимодействует с аппаратным компонентом, который выполняет тяжелую работу. Смотрите страницу 222 технического описания.
«AVR TWI работает с байтами и основывается на прерываниях…» (раздел 21.6 на странице 224)

Это ключевой момент; это означает, что
вы настраиваете регистры;
вы позволяете TWI модулю осуществлять связь;
вы можете делать в это время что-то еще; ваш микроконтроллер с тактовой частотой 16 МГц не занят управлением последовательной связью на 100 кГц;
TWI модуль вызывает прерывание, когда заканчивает работу, чтобы уведомить процессор об изменениях состояния (включая успешность операций и/или ошибки).

Однако обратите внимание, что библиотека блокирует ввод/вывод. Это означает, что он переходит в цикл ожидания и ждет завершения связи I2C

Ваше приложение не может ничего делать, пока модуль TWI общается по шине I2C. Обратите внимание, что это может быть не то, чего бы вы хотели: если ваша программа критична ко времени, то ваш 16-мегагерцовый процессор, застрявший в цикле ожидания и ждущий 100-килогерцового потока связи, будет не эффективен. Возможно, вам лучше написать собственный код I2C. В исходном коде avr-libc есть пример в ./doc/examples/twitest/twitest.c (смотрите http://www.nongnu.org/avr-libc/). Вы можете найти версию avr-libc, используемую вашей конкретной IDE Arduino, посмотрев файл versions.txt в каталоге установки Arduino IDE. Это будет где-то в Java/hardware/tools/avr. На Mac полный путь будет следующим /Applications/Arduino.app/Contents/Resources/Java/hardware/tools/avr/versions.txt; путь у вас будет другим, но схожим.

Проблемы с несколькими аргументами в Serial.print

Проблема при объединении текста и чисел и выводе в print в одной строке

Часто для отладки программы требуется вывести несколько значений, снабдив их каким-то комментарием. Например, такой текст:  «Sensor’s value is:  15». Если вы просто используете такой код:

Serial.print(“Sensor’s value is:  15”);

,то все отобразится правильно. Но если вы попытаетесь вместо подстроки «15» вставить реальное показание датчика, объединив строку и числовое значение, то увидите, что строка выводится некорректно.

Serial.print(“Sensor’s value is:  ” + analogRead (A0));

Этот код даст непредсказуемый результат, в мониторе порта вы не увидите или пустоту или случайный набор символов. Причина ошибки в механизме конвертации типов данных. При объединении строк  «на лету», как в нашем примере, ардуино не знает, как интерпретировать типы данных для аргументов при операторе суммирования. В процессе такой непредсказуемой конвертации и результат может быть непредсказуемым.

Для решения этой проблемы вы можете использовать два способа:

Первый вариант. Объявить предварительно переменную типа String, инициализировать ее константой-строкой и использовать в аргументе print. Такой вот пример будет работать:

String str = “Sensor’s value is:  “;

Serial.println(str + analogRead(A0));

Второй, более предпочтительный и удобный вариант: использование функции print несколько раз:

Serial.print(“Sensor’s value is:  “);

Serial.println(analogRead(A0));

Более подробно прочитать об особенностях  работы со строками при выводе информации в монитор порта можно на официальной странице Ардуино.

Проблема объединения нескольких строк в аргументе функции print

Попробуйте загрузить скетч с таким фрагментом:

Serial.print(“Sensor’s value is:  ” + analogRead (A0) + “15 cm”);

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

Функция println и отличия от print

Если вы попробовали использовать функцию print(), то уже обратили внимание, что вся информация в мониторе порта выводится в одной строке. Если же мы хотим вывести текст в новых строках, то должны использовать близкого родственника функции – println()

Метод println () класса Serial выполняет ту же функцию, что и print() – он выводит в последовательный порт ASCII-текст.  Аргументы у методов тоже совпадают – мы передаем текст или число с возможным вторым аргументом, определяющим формат. Отличие же println заключается в принудительном добавлении в конце передающейся строки символа новой строки “r” (ASCII код 13). Суффикс ln обозначает сокращенное слово line (строка). Используя println, мы можем быть уверены, что следующая (но не текущая) строка будет выведена с новой строки.

Например, следующие команды выведут три строки текста, каждое предложение в новой строке.

При формировании строки мы также можем использовать следующие специальные символы: “”, “r”, “t” (символ табуляции). Табулирование позволяет печатать на экране что-то типа таблицы значений:

  Serial.print("Column1tt");  Serial.println("Column2");  Serial.print("Cell 11tt");  Serial.println("Cel l2");  Serial.print("Cell 21tt");  Serial.println("Cel 22");  

Обмен данными

Теперь попытаемся объединить оба подхода и обмениваться сообщениями между платой и приложением в двух направлениях.

Для максимальной эффективности добавим булеву переменную. В результате у нас отпадает необходимость постоянно отсылать 1 или 0 от Processing и последовательный порт разгружается и не передает лишнюю информацию.

Когда плата обнаружит присланную единицу, то меняем булевое значение на противоположное относительно текущего состояния (LOW на HIGH и наоборот). В else используем строку «Hello Kity», которую будем отправлять только в случае, когда не обнаружим ‘1’.

Функция establishContact() отсылает строку, которую мы ожидаем получить в Processing. Если ответ приходит, значит Processing может получить данные.

Переходим к скетчу Processing. Мы будем использовать метод serialEvent(), который будет вызываться каждый раз, когда обнаруживается определенный символ в буфере.

Добавим новую булеву переменную firstContact, которая позволяет определить, есть ли соединение с Arduino.

В методе setup() добавляем строку serial.bufferUntil(‘\n’);. Это позволяет хранить поступающие данные в буфере, пока мы не обнаружим определённый символ. В этом случае возвращаем (\n), так как мы отправляем Serial.println() от Arduino. ‘\n’ в конце значит, что мы активируем новую строку, то есть это будут последние данные, которые мы увидим.

Так как мы постоянно отсылаем данные, метод serialEvent() выполняет задачи цикла draw(), то можно его оставить пустым.

Теперь рассмотрим основной метод serialEvent(). Каждый раз, когда мы выходим на новую строку (\n), вызывается этот метод. И каждый раз проводится следующая последовательность действий:

  • Считываются поступающие данные;
  • Проверяется, содержат ли они какие-то значения (то есть, не передался ли нам пустой массив данных или «нуль»);
  • Удаляем пробелы;
  • Если мы первый раз получили необходимые данные, изменяем значение булевой переменной firstContact и сообщаем Arduino, что мы готовы принимать новые данные;
  • Если это не первый приём необходимого типа данных, отображаем их в консоли и отсылаем микроконтроллеру данные о клике, который совершался;
  • Собщаем Arduino, что мы готовы принимать новый пакет данных.

При подключении и запуске в консоли должна появится фраза ‘Hello Kitty’. Когда вы будете щёлкать мышкой в окне Processing, светодиод на пине 13 будет включаться и выключаться.

Кроме Processing, вы можете использовать программы PuTTy или написать свою программу на C# использованием готовых классов для работы с портами.

Протокол I2C в Arduino

На следующем рисунке показаны контакты платы Arduino UNO, которые используются для связи по протоколу I2C.

Линия протокола I2C Контакт платы Arduino UNO
SDA A4
SCL A5

Для осуществления связи по протоколу I2C в плате Arduino используется библиотека <Wire.h>. В ней содержатся следующие функции для связи по протоколу I2C.

1. Wire.begin(address).

Эта команда производит инициализацию библиотеки Wire и осуществляет подключение к шине I2C в качестве ведущего (master) или ведомого (slave). 7-битный адрес ведомого в данной команде является опциональным и если он не указан , то устройство (плата Arduino) подключается к шине I2C в качестве ведущего (master).

2. Wire.read().

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

3. Wire.write().

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

От ведомого ведущему (Slave to Master): ведомый записывает (передает) данные ведущему когда в ведущем работает функция Wire.RequestFrom().

От ведущему ведомому (Master to Slave): в этом случае функция Wire.write() должна использоваться между вызовами функций Wire.beginTransmission() и Wire.endTransmission().

Функцию Wire.write() можно использовать в следующих вариантах:

  • Wire.write(value); value — значение передаваемого одиночного байта;
  • Wire.write(string) – для передачи последовательности байт;
  • Wire.write(data, length); data – массив данных для передачи в виде байт, length – число байт для передачи.

4. Wire.beginTransmission(address).

Эта функция используется для начали передачи по протоколу I2C устройству с заданным адресом ведомого (slave address). После этого вызывается функция Wire.write() с заданной последовательностью байт для передачи, а после нее функция endTransmission() для завершения процесса передачи.

5. Wire.endTransmission().

Эта функция используется для завершения процесса передачи ведомому устройству, который до этого был инициирован функциями beginTransmission() и Wire.write().

6. Wire.onRequest().

Эта функция вызывается когда ведущий запрашивает данные с помощью функции Wire.requestFrom() от ведомого устройства. В этом случае мы можем использовать функцию Wire.write() для передачи данных ведущему.

7. Wire.onReceive().

Эта функция вызывается когда ведомое устройство получает данные от ведущего. В этом случае мы можем использовать функцию Wire.read() для считывания данных передаваемых ведущим.

8. Wire.requestFrom(address,quantity).

Эта функция используется в ведущем устройстве чтобы запросить байты (данные) с ведомого устройства. После этого используется функция Wire.read() чтобы принять данные переданные ведомым устройством.
address: 7-битный адрес устройства, с которого запрашиваются байты (данные).
quantity: число запрашиваемых байт.

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

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

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

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