синхронизация с данными (Serial порт)

Vanovich
Offline
Зарегистрирован: 16.05.2012

 В процессе изучения программирования последовательного порта никак не могу добиться четкой синхронизации записи/чтения данных. Поясню: ардуина отправляет в порт фиксированный объем данных с определенной частотой. В реале это данные с акселерометра. Данные читаются и обрабатываются на компе в processing-е. Но вот в чем дело: квантование происходит неоднородно, разница зависит от частоты отправки в ардуине. Написал скетч для ардуины и для принимающей стороны на processing

Ардуино:
 

#include <TimerOne.h>

boolean readyToSend;

void setup()
{
  Serial.begin(115200);
  pinMode(13, OUTPUT);
  Timer1.initialize(10000); // 100Hz
  Timer1.attachInterrupt(handler);
}

void loop()
{
  if(readyToSend)
  {
    Serial.print("test message");
    Serial.print('\n');
    readyToSend = false;  
  }
}

void handler()
{
  readyToSend = true;
}

Processing:
 

import processing.serial.*;

Serial comPort;
long timestamp;
int count;

void setup()
{
  comPort = new Serial(this, "COM5", 115200);
  comPort.bufferUntil('\n');
  delay(100);
  timestamp = millis();
}

void draw()
{
  if (millis() - timestamp >= 1000)
  {
    print(count);
    println(" Hz");
    count = 0;
    timestamp = millis();
  }
}

void serialEvent(Serial comPort)
{
  count ++;
}

 В результате выполнения кода на процессинге должно выводиться значение частоты в Hz, которая задается в коде установки таймера на ардуино. По идее, значение долно быть строго постоянным, но на практике оно гуляет с определенной погрешностью (100 +-3 Гц) Причем у увеличением частоты таймера погрешность увеличивается тоже. Кто тут лажает? Ардуина? Код? Я?

leshak
Offline
Зарегистрирован: 29.09.2011

 Скорее всего - вы.

Причем не сколько в коде, сколько "концептуально". Сам Serial не является средой с гарантированным временем доставки. Так что "боротся можно", но получив удовлетворяющую стабильно это все равно будет это все равно будет "пока ветер не переменится".

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

Далее как ни крути, а отправка в serial и сам процесс "рисования" - тоже занимает время, а в этот момент могут произойти тики - следовательно "тайминги сбиваются". 

Можно попробовать:

  • Добавить Serial.flush() после принтов - выталкивание из буфера все что накопилось каждый раз. возможно буфер вам портит картину.
  • Уменьшить саму текстовую строку - чем меньше тем быстрее отправится.
  • На "принимающей" вычислять новый timestamp и обнуление каунтера не после вывода, а перед. сразу после проверки условию (тогда вывод "время вывода" не будет плюсоватся к интервалу). Или и там попытатся использовать что-то типа таймера (не использовал, если честно, процессинг но думаю должно что-то подобное быть).

Но еще раз. Все это "шаманство". Которое если и "поможет", то только "сдесь и сейчас".

Более "непробиваемым" решением, было-бы в каждом сообщении слать его временную метку (или просто сами временные метки считать сообщениями). Тогда приняв от ардуины "ее текущие время время" вы сможете определить у нее началась новая секуда или "еще старая идет". И какие-то "временные задержки канала" - перестанут играть роль.

P.S. и Serial.print("...");Serial.print('\n"); можно заменить на один  Serial.println("...")

 

Vanovich
Offline
Зарегистрирован: 16.05.2012

 Вот СПАСИБО! Скажу честно - с некоторой ревностью глядел на развивающиеся по ответам соседние ветки и тоскливо обновлял страничку - а темка моя пустовала...И тут ичерпывающий ответ. Не понял что значит "более непробиваемое" решение - идея отсылать временные метки с ардуины уже в очереди на проработку, "непробиваемое" - имеется ввиду более верное или наоборот? 
PS: про println  в курсе, в оправдание могу сказать, что отличие есть, println отсылает 2 байта в конце, а для маркера достаточно одного ('\n')  - экономия :)

leshak
Offline
Зарегистрирован: 29.09.2011

Vanovich пишет:

Не понял что значит "более непробиваемое" решение - идея отсылать временные метки с ардуины уже в очереди на проработку, "непробиваемое" - имеется ввиду более верное или наоборот? 

Верное или нет - не знаю. Все завист от поставленных целей. Это вам решать.

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

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

Vanovich пишет:

про println в курсе, в оправдание могу сказать, что отличие есть, println отсылает 2 байта в конце, а для маркера достаточно одного ('\n') - экономия :)

Вот за это мне и нравится помогать на форуме. Хочешь не хочешь - а всегда что-то новое узнаешь. Столько времени уже вожусь, а как-то проскользнули эти строки документации мимо сознания. Действительно там еще \r высылвается. И, судя по исходнику это поведение, - никак не настраивается.

ну тут уж вам решать, что важней "байт экономии" (смешно выглядит на фоне общей длины строки) или "читабильность кода".

Vanovich
Offline
Зарегистрирован: 16.05.2012

Результаты попыток синхронизации в варианте регистрации временных интервалов на стороне ардуино. Исходный скетч:

#define OUTPUT_FREQUENCY 100 // Hz

uint32_t dt, timeStamp, timeLimit;
boolean timeReady;

void setup()
{
  Serial.begin(115200);
  timeLimit = 1000000 / OUTPUT_FREQUENCY;
  delay(100);
}

void loop()
{
  timeReady = (micros() - timeStamp) >= timeLimit;
  if(timeReady)
  {
    dt = micros() - timeStamp;
    timeStamp = micros();
    Serial.println(dt);
    Serial.flush();
  }
}

Вычисляемые значения dt (по идее, вычисленные средствами ардуины) читаем в processing (впрочем, можно и просто в serial монитор заглянуть)

import processing.serial.*;

Serial comPort;
long timestamp;
int count;
String inString;

void setup()
{
  comPort = new Serial(this, "COM5", 115200);
  comPort.bufferUntil('\n');
  delay(100);
  timestamp = millis();
}

void draw()
{
  if (millis() - timestamp >= 1000) 
  {
    timestamp = millis();
    println(inString);
    count = 0;
  }
}

void serialEvent(Serial comPort)
{
  while (comPort.available () > 0)
    inString = comPort.readStringUntil('\n');
}

Для удобства чтения полученная строка выводится раз в секунду.
Что сказать...Тоже не все гладко и прозрачно. Вместо ожидаемых стабильных значений 10000 результат все же имеет небольшую погрешность (понятно, с учетом того, что значение micros() кратно 4-м:)

10008
10004
10008
10008
10008
10012
10004
10004
10004
10012
10012
10004
10008

Возможно, я предъявляю слишком уж высокие требования к стабильности частоты, ибо для 10000 погрешность в +- 12 это ни о чем...Но если перевести вычисления в millis(), то результат такой же - имеем разброс от 10 до 12.
В чем причина? Какие существуют методы стабилизации частоты?

maksim
Offline
Зарегистрирован: 12.02.2012

Читаем arduino.ru/Reference/Micros 

У меня получилось так

#define OUTPUT_FREQUENCY 100 // Hz

uint32_t dt, timeStamp, timeLimit;
boolean timeReady;

void setup()
{
  Serial.begin(115200);
  timeLimit = 1000000 / OUTPUT_FREQUENCY - 4;
  delay(100);
}

void loop()
{
 // timeReady = (micros() - timeStamp) >= timeLimit;
  if((micros() - timeStamp) >= timeLimit)
  {
    dt = micros() - timeStamp;
    timeStamp = micros();
    Serial.println(dt, DEC);
    Serial.flush();
  }
}

 

10000
10000
10004
10000
10000
10004
10000
10004

 

Vanovich
Offline
Зарегистрирован: 16.05.2012

Согласен, лучше. Правда, иногда еще едкие 10008 и 10012 проскакивают. Т.е все так и должно быть?

Jacks_d
Offline
Зарегистрирован: 31.12.2011

 я когда лепил... то сделал проще, грубо говоря внешее прерывание, т.к. были часы на DS1307, брал секунды и сравнивал их, после изменения опрашывал датчик темперауры, но отправлял эти данные только после изменения времени в следующий раз, тебе можно сразу... собственно к чему я это, в какой-то из библиотек к этим же часам, была настройка скорости изменения данных, толи переход в микросекунды толи скорость обновления, нпомню натыкался на это с 4 месяца назад, т.е. мужно было сделать как бы прерывание по таемеру. Там же брать таймстамп. Вот как-то так, может чем помог)