Arduino — автоматическое определение скорости на входящем SoftwareSerial порте

от автора

Привет Хабр!

На днях мне необходимо было создать прототип некого простого устройства, выполняющего функционал преобразования <данные удалены>. В целом ничего особенного, однако была одна особенность, без которой данной статьи бы не было. Но обо всем по порядку, а для начала дисклеймер:
Я знаю что мой код далеко не идеален и я не претендую на звание идеального решения проблемы. Я просто делюсь своим вариантом решения данного вопроса. И вообще я МухоЖук. Вы сами во всем виноваты. Хой!

Итак, для базы прототипа был выбран Arduino Uno R3. Быстро, достаточно мощно для задачи, а главное дешево.

Суть задачи — имеется некоторое устройство, которое отправляет по UART некоторые данные, которые необходимо принять на прототипируемом устройстве и далее <данные удалены>.
Но есть одна загвоздка — Baud Rate отправляемого устройства может быть отконфигурирован по разному, а нам нужен более менее универсальный вариант. А значит необходимо определить подходящий Baud Rate.

К сожалению, Ардуино не умеет автоматически определять скорость порта, и более того, его нужно изначально задавать при инициализации интерфейса в секции setup(). Быстрое гугление данного вопроса не дало результатов, что и стало причиной написания данной статьи. Будем решать проблему самостоятельно. Для этого напишем рекурсивную функцию (ошибка приводящая к переполнению стека найдена, код исправлен на цикл, спасибо @CitizenOfDreams и @max_dark за подсказки) которая будет инициализировать интерфейс с разной скоростью и проверять на ней входящие данные.

В моем случае входящие данные — это всегда печатаемые символы. По этому для определения «правильности» скорости я буду использовать функцию isPrintable(); которая возвращает нам true, символ является печатаемым. Вы же можете использовать другую функцию или написать свою =)

Начнем. Для начала, произведем необходимые настройки и инициализации:

#include <SoftwareSerial.h> //Настройки //Пины comIn const int RX1 = 4; const int TX1 = 5; //Возможные скорости для ComIn порта long baudRates[] = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200}; int numBaudRates = sizeof(baudRates) / sizeof(long); //Количество символов для проверки скорости ComIn порта const int numCharsForSetup = 5; //Конец настроек //Обьявляем софтпорт SoftwareSerial comIn(RX1, TX1);
Старый вариант от версии с рекурсией
#include <SoftwareSerial.h> //Настройки //Пины comIn const int RX1 = 4; const int TX1 = 5; //Возможные скорости для ComIn порта long baudRates[] = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200}; int numBaudRates = sizeof(baudRates) / sizeof(long); //Количество символов для проверки скорости ComIn порта const int numCharsForSetup = 5; //Количество циклов проверки скорости перед перезагрузкой контроллера const int numAttemptsBeforeReset = 10; //Конец настроек //Счетчик попыток int attempts = 0; //Обьявляем софтпорт SoftwareSerial comIn(RX1, TX1);

На этом предварительные настройки закончены, переходим в секцию setup:

void setup() { Serial.begin(9600); //запуск "железного" порта для дебага delay(100); //Задержка для стабилизации интерфейса Serial.println("Init start"); //Пишем в дебаг консоль delay(500); //Задержка для удобства дебага, в принципе её можно удалить //инициализация пинов pinMode(RX1, INPUT); pinMode(TX1, OUTPUT); initComInIface(); //наша функция запуска входящего софтпорта Serial.println("COM IN STARTED"); delay(500); //задержка для стабилизации интерфейса и удобства дебага Serial.println("Init end. Starting main program."); } 

А теперь ныряем вглубь.

Старый вариант от версии с рекурсией

Забегу немного вперед — нам понадобится функция софтварного перезапуска Ардуинки, так как в процессе тестирования оказалось, что Ардуинка имеет свойство «зависать» в процессе рекурсии. Не знаю с чем это связано (кто знает почему?), возможно с тем что у нее начинает переполняться (или наоборот, течь) память, но в любом случае софтварный перезапуск контроллера решает эту проблему:

// сброс микроконтроллера путем рестарта прошивки void reset() {   asm volatile("jmp 0x00"); }

И вот она рекурсивная функция. Суть ее довольно проста. Мы поочередно перебираем предполагаемые скорости, инициализируем интерфейс и вызываем функцию checkPrintable(); которая проверяет данные на интерфейсе и возвращает true, если данные соответствуют условию. Далее идет счетчик попыток — если мы сделали рекурсию большее число раз, чем указано в переменной numAttemptsBeforeReset — то мы перезагружаем ардуинку. Ну а дальше сама рекурсия в зависимости от флага looping. Больше не нужно. По окончанию данной функции порт с найденной скоростью будет инициализирован.

//функция ожидающая определение скорости сходящего софтпорта void initComInIface() {   bool looping = true; //флаг цикла    while (looping) {     for (int i = 0; i < numBaudRates; i++) { //цикл перебора скоростей       comIn.begin(baudRates[i]); //запуск порта на заданной скорости       delay(500); // задержка для стабилизации порта       Serial.println("Trying speed " + String(baudRates[i])); //пишем в дебаг        if (checkPrintable()) { //проверка на печатаемые символы         Serial.println("Printable characters found at baud rate: " + String(baudRates[i])); //Если найдены печатаемые символы - пишем в дебаг         looping = false; //ставим флаг остановки цикла while         break; //выходим из цикла перебора скоростей       } else {         Serial.println("No printable characters found at " + String(baudRates[i])); //Просто пишем в дебаг       }       comIn.end(); //закрываем интерфейс       delay(500); //задержка для стабилизации порта и удобства отслеживания     }   } }
Старый вариант от версии с рекурсией
//рекурсия в ожидании определения скорости входящего софтпорта void initComInIface() {   bool looping = true; //флаг рекурсии   for (int i = 0; i < numBaudRates; i++) { //цикл перебора скоростей     comIn.begin(baudRates[i]); //запуск порта на заданной скорости     delay(500); // задержка для стабилизации порта     Serial.println("Trying speed " + String(baudRates[i])); //пишем в дебаг     if (checkPrintable()) { //проверка на печатаемые символы       Serial.println("Printable characters found at baud rate: " + String(baudRates[i])); //Если найдены печатаемые символы - пишем в дебаг       looping = false; //останавливаем рекурсию       break; //выходим из цикла     } else {       Serial.println("No printable characters found at " + String(baudRates[i])); //Просто пишем в дебаг     }   }   attempts++; //увеличиваем счетчик количества попыток   if (attempts >= numAttemptsBeforeReset && lopping == true) { //если количество попыток превысило максимальное значение и мы все еще не нашли ничего     Serial.println("Num of attempts is more than requered. Restarting controller at 5 sec.");     delay(5000); //здесь тоже для удобства     reset(); //софтварная перезагрузка контроллера   }   if (looping) { //если флаг рекурсии все еще активен     comIn.end(); //закрываем интерфейс     delay(500); //задержка для стабилизации порта и удобства отслеживания     initComInIface(); //рекурсия!   } }

И наконец, гвоздь программы — функция checkPrintable(); Логика в том, что мы читаем пришедшие байты несколько раз в цикле. Количество циклов определяется переменной numCharsForSetup. Если оно равно 5 — это значит что все прочитанные 5 байт должны быть печатаемыми символами. Это сделано для того, что некоторые символы могут правильно определяться на неправильных скоростях. Тестирование показало, что буква «j» одинаково верно определяется на скоростях 4800 и 9600, а символ «$» периодически одинаково определяется на скоростях 38400 и 115200. Чтение байт подряд решает данную проблему, при условие что пришедшие байты хоть немного отличаются друг от друга.

//функция проверяет, что символы приходящие на интерфейс - печаемые boolean checkPrintable() {    int j = 0; //счетчик прочитаных символов   if (comIn.available()) { //если интерфейс доступен     for (int i = 0; i < numCharsForSetup; i++) { //цикл, проверяющий символы       delay(100); //задержка для стабилизации. Без нее иногда происходит пропуск символа       char c = comIn.read(); //читаем пришедший байт       Serial.println(String(c)); //пишем его в дебаг        if (isPrintable(c)) { //если данный байт является печатаемым символом         j++; //добавляем в счетчик символов         Serial.println("Found char \"" + String(c) + "\" at comIn interface. Setup of ring " + String(j) );//Снова пишем в дебаг         delay(100); //Небольшая задержка для стабилизации. Без нее стабильно не работает       }       c = NULL; //Обнуляем значение переменной - данная строка повышает стабильность работы.      }     if (j == numCharsForSetup) { //Если пройдены все циклы проверок       return true;     }   }   return false; }

Да. У этого кода есть недостатки. И самый главный — мы в любом случае должны предположить используемые скорости и прописать их в программе. По этой причине мы не сможем определить не предполагаемые скорости. Но я не считаю это существенным недостатком — список скоростей стандартен, по этому мы можем их смело предполагать.
Второй недостаток — необходимость использовать множество delay. Вероятно можно обойтись и без них, но у меня не получилось.
Третий недостаток — это размер скетча. Скомпилированный код занимает чуть меньше 8 килобайт, что довольно много для ардуино уно с его 32 кб. памяти
Ну и четвертый, как по мне — самый несущественный недостаток — сама логика работы. Множество циклов, цикл в цикле, рекурсия…. По моему мнению — данный код дает неслабую нагрузку на ардуинку, хотя это все относительно и вообще я могу ошибаться. Вы еще помните что я мухожук? Уже не актуально. Избавились от рекурсии, что заметно улучшило логику работы.

В целом, на этом все, всем спасибо!

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

Сталкивались ли вы с необходимостью автоопределения скорости?

40% Да4
60% Нет6

Проголосовали 10 пользователей. Воздержались 3 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/822819/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *