Привет Хабр!

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

Итак, для базы прототипа был выбран 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 кб. памяти
Ну и четвертый, как по мне - самый несущественный недостаток - сама логика работы. Множество циклов, цикл в цикле, рекурсия.... По моему мнению - данный код дает неслабую нагрузку на ардуинку, хотя это все относительно и вообще я могу ошибаться. Вы еще помните что я мухожук? Уже не актуально. Избавились от рекурсии, что заметно улучшило логику работы.

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

Комментарии (18)


  1. max_dark
    19.06.2024 02:33
    +1

    void detectUartSpeed(&uart)
    {
      do
      {
        uart.begin(...);
        bool success = checkData(uart);
        if (success) return;
        uart.end();
        switchToNextBaudRate;
      }
      while(true);
    }

    И рекурсия не нужна.


    1. WoWSab Автор
      19.06.2024 02:33

      Спасибо, подтолкнули меня в правильную сторону. Действительно работает стабильнее. Поправил статью


  1. CitizenOfDreams
    19.06.2024 02:33
    +1

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

    В этой вашей рекурсии вы стек не переполняете? Микроконтроллеры надо программировать немножко не так, как большие компьютеры, где мегабайтом больше, мегабайтом меньше...


    1. WoWSab Автор
      19.06.2024 02:33

      Вполне вероятно. У меня есть предположение, что возможно сам вызов рекурсии переполняет стек... Мол, каждый вызов функции добавляет запись в стек, и когда функция выполняется множество раз, то стек переполняется. Буду рад если проконсультируете, насколько реально такое поведение?


      1. CitizenOfDreams
        19.06.2024 02:33

        Ну да, каждый вызов любой подпрограммы записывает в стек как минимум адрес возврата из этой подпрограммы. То есть если рекурсировать слишком глубоко, стек рано или поздно переполнится и затрет полезные данные.

        В некоторых мелких микроконтроллерах стек вообще не находится в общей памяти, а имеет фиксированный и очень маленький размер - например, 16 или даже 8 уровней. Там за вложенностью вызовов нужно следить особенно тщательно.


    1. WoWSab Автор
      19.06.2024 02:33

      UPD: Спасибо за подсказку, да, действительно было переполнение стека. Исправил на цикл while, дополнил статью.


  1. randomsimplenumber
    19.06.2024 02:33
    +4

    <данные удалены>.

    Вот что мне в микроконтроллерах нравится - никто никогда не узнает, какой г-код обеспечивает мигание лампочки:)


    1. CitizenOfDreams
      19.06.2024 02:33

      Вот что мне в микроконтроллерах нравится - никто никогда не узнает, какой г-код обеспечивает мигание лампочки:)

      Ну, в микроконтроллерах есть встроенная защита от г-кода. Плохой код просто не поместится в память. Или моментально упадет, потому что в контроллере нет гигабайтов памяти, которая может часами и сутками незаметно утекать. Или взорвет мощные транзисторы (10 долларов за штуку, доставка через 5-8 недель).


      1. randomsimplenumber
        19.06.2024 02:33

        Г-код не обязательно неработающий;) Лампочка мигает же, а если что утечет - есть watchdog. ;)


        1. CitizenOfDreams
          19.06.2024 02:33

          Лампочка мигает же, а если что утечет - есть watchdog. ;)

          Если watchdog срабатывает от утечек в коде, а не от внешних помех - то это все-таки неработающий код. По-моему так.

          Вспомнил, кстати, я два года назад сделал себе прототип фонарика на PIC10. Watchdog ему не включил, отложил на "когда-нибудь потом". Так два года и таскаю его в кармане, пока что не зависал. :-)


          1. randomsimplenumber
            19.06.2024 02:33

            По всякому бывает ;) Читал где-то, что у Боинга какой то из контроллеров питания перегружался на 49 сутки. Счётчик миллисекунд, ага :). Летают с этим.


  1. AndronNSK
    19.06.2024 02:33
    +2

    Я думал, тут будет замерять минимальное время импульса, а тут.......


    1. CitizenOfDreams
      19.06.2024 02:33

      Надо будет попробовать. При этом единственное требование к потоку данных - чтобы в нем присутствовала последовательность битов "1-0-1" или "1-0-stop".


  1. GarryC
    19.06.2024 02:33
    +1

    В модемах была такая функция, но работала совсем по-другому, я смотрел код прошивок и делал так же.

    Там вначале МК просто смотрит на ножку, ловит старт-бит, измеряет его длительность, далее в режиме программного UART с вычисленной скоростью принимает первый символ, затем включает аппаратный порт на нужной скорости и принимает остаток команды. При этом команда начинается с гарантированного префикса "AT" или "at" и все получается хорошо.

    Забавно, что эта процедура повторяется для каждой ожидаемой команды и Вы можете передавать их на разной скорости, хотя не очень понятно, зачем.

    А передаваемые/принимаемые данные дополнительно могут передаваться на скорости установленного сеанса связи.


  1. melodictsk
    19.06.2024 02:33

    Мне попадалось несколько готовых библиотек на эту тему. Зачем было изобретать велосипед?


    1. randomsimplenumber
      19.06.2024 02:33

      Значит автору они не попались.Ну и совершенно не факт что в тех либах сильно лучше.


  1. ptr128
    19.06.2024 02:33

    Не проще ли было в режиме GPIO замерять минимальную длительность импульса, а уже на основании её вычислять скорость?


  1. enjoyneering
    19.06.2024 02:33

    На арудуино под esp8266 и edp32 точно стандартные методы автоматическое определение скорости UART.