Кодекс летописца, или Ода к телеметрии

ca7a4b34d8dda488c95122d72dc5bc40.jpg

Допустим, ко группе инженеров снизошла задача разработать систему управления чем-нибудь достаточно сложным. Теоретик заточил зубы и приступил к граниту — строит модели объекта и системы управления. Комплексники копают руду компоновки, вопросов климатики, вибрации и спецтребований, кто-то рисует платы/корпуса/кабели, кто-то пишет и тестирует уже определившиеся элементы ПО.

И здесь же, с самого старта, нужен человек, который займется телеметрией: ее формированием, передачей и сохранением. Ибо переоценить важность телеметрии для разработки практически нереально. Когда что-то пойдет не так —, а оно пойдет — только телеметрия даст шанс понять, что это, черт возьми, было. Когда все будет так — она станет объективным доказательством успеха. Больше того: иногда, когда внешне все прошло так, она заставит при анализе запуска уронить челюсть и спросить себя и окружающих: «как, черт возьми, всё обошлось?»

А потому исходное положение кодекса, пункт зеро:

0. Телеметрия нужна

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

Конечно, в идеале система должна быть предварительно отработана на модели, все мелочи вычищены там же, а реализация на железе — заработать сразу и без проблем. Но подобные мечты высокие и светлые следует отбросить, упав вместо того в объятия реальности, где не так пойдет даже то, что по природе своей к этому неспособно.

А когда хотя бы какая-то телеметрия появляется, к ней тут же просыпается аппетит, и хочется видеть еще вот то, вот это, и неплохо бы добавить пяток-другой значений отсюда. Но с ростом аппетита быстро приходит понимание:

1. Телеметрию необходимо сохранять для дальнейшего анализа

Человеку постороннему при слове «телеметрия» первым делом представляются живые графики во многих окнах. Да, следить за поведением системы в реальном времени — это чертовски увлекательно. Держать руку на пульсе, видеть, как система дышит, и прочее подобное. Но красивые кривые в реальном времени — вещь сильно второстепенная, и вот почему.

Во-первых, эти графики надо в чем-то рисовать. А если реалтайм-телеметрия не является значимой продуктовой фичей — под нее не будет отдельной строки в расходной ведомости. Но бог бы с ним, можно на коленке накидать приложуху с графиками (»— думаете вы», © один анекдот), в крайнем случае просто отображать цифры в окошках. Да можно хоть в консоли смотреть!

И это, действительно, вариант. Случается даже, что от прошлых проектов доступна полноценная реалтайм-система наблюдения. Но есть еще два «но», куда более важных:

  • вне стенда, в условиях реального применения, может вообще не быть канала связи с устройством (а знать, что происходило, хочется);

  • телеметрия реального времени практически бесполезна при отработке систем управления. Потому что человек неспособен эффективно оценивать одновременно более двух-трех параметров, особенно представленных в числовом виде. Уж тем более не удастся осознать скорость их изменения или отследить кратковременные странности.

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

А еще записанные данные суть ключ к одной редко вспоминаемой и не всем доступной (зависит от методики разработки), но также неоценимой возможности: их можно прогнать через исходную матмодель системы управления и посмотреть, как она вела бы себя в тех же внешних условиях при иных настройках параметров. Конечно, для этого первым делом потребно существование модели системы как таковой; плюс она должна уметь брать данные не только с модели объекта управления, но и из внешнего источника. И, конечно, у этого метода есть ограничения, накладываемые предметной областью. Но при всем при этом запись реального запуска есть уникальный материал для как моделирования поведения системы в «боевых» условиях, так и для уточнения матмодели объекта управления. Потому что нет такой модели, которую нельзя было бы немножечко допилить.

С необходимостью записи понятно. Встает масса практических вопросов, начиная с:

2. Выбор формата передачи и хранения (человеко-читаемая форма vs. бинарная)

image-loader.svg

У человеко-читаемого формата есть очевидные плюсы:

  • человеко-читаемость per se: возможность видеть цифры данных (и импортировать их для анализа во что-нибудь вроде Excel) без дополнительного ПО;

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

Но эти плюсы остаются плюсами только в случае, когда перечень и объем передаваемых данных крайне мал, и все потребные числа можно уместить буквально в одну строку, окидываемую взглядом. А это — вырожденный случай, потому что на практике в проектах, где телеметрия действительно нужна, условие краткости перечня не выполняется. О, нет! В таких случаях переменных много, их набор меняется от версии к версии, от режима к режиму, они нужны с разной частотой. В потоке могут присутствовать данные об однократных и случайно возникающих событиях. К телеметрии в целом, как к подсистеме, появляется определенный набор практических требований. В этом случае человеко-читаемость теряет оба преимущества: разобраться в мешанине цифри идентификаторов в режиме реального времени становится невозможно вообще, а выбор руками нужных данных из записанного потока прекращается в квест. И все это — на фоне крайне низкой эффективности использования канала связи: из всех доступных значений байта задействованы только 10 цифр и несколько букв и символов.

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

3. Конечность пропускной способности канала связи

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

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

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

  • сохранить баланс интересов всех участвующих сторон;

  • учесть, что эффективность использования канала не будет равна 100%: иногда звезды станут сходиться так, что данные в аппаратном буфере передачи закончились, а поток, заполняющий этот буфер, на паузе из-за того, что именно сейчас выполняется более приоритетная задача;

  • не забыть оставить запас под рост аппетитов участников.

Вариант расчета

На практике распределение пропускной способности канала удобно планировать на основе пакетной организации данных, поскольку однородные данные рационально группировать в пакеты фиксированного размера, снабженные заголовком. Про пакеты тоже будет отдельный разговор впереди, а пока в качестве примера — случай, когда все пакеты имеют одинаковый размер. Положим, имеем канал 1 Мбит/сек, то есть 125 кбайт/с. Размер пакета — 200 байт. Следовательно, теоретически достижимая частота передачи пакетов — 125000/200=625 Гц. Потребители хотят иметь один пакет с частотой 200 Гц, два пакета с частотой 50 Гц, и два пакета с частотой 25 Гц. Итого канал занят на 200+2×50+2×25=350 Гц, запас еще 625–350=275 Гц, 42% от теоретической пропускной способности. Или, оценивая в герцах, доступными остаются еще 4 потока по 50 Гц и плюс 3 по 25 Гц.

4. Приоритезация данных

image-loader.svg

От телеметрии хочется получить по возможности подробную и целостную историю состояния системы. Частота передачи различных данных — это столп, на котором зиждется подробность. Другой столп, не менее важный — относительная ценность различных групп данных. Вот первичные данные с датчиков: они появляются часто, записывать их надо столь же часто, но при этом мы практически ничего не потеряем, если время от времени в поток не будет попадать отсчет-другой. В то же время срабатывание датчика безопасности будет случаться лишь изредка, но нам крайне важно, чтобы запись об этом сохранилась в телеметрии, и попала в нее как можно раньше после того, как датчик сработал[1]. А между тем канал может быть доступен не всегда, или в ходе развития системы его запасы пропускной способности окажутся выбраны даже больше, чем на 100%, и возникнет конкуренция между данными.

По такому случаю имеет смысл завести в системе две (а то и больше) очереди пакетов, ожидающих передачи: обычные и высокоприоритетные/разовые. При наличии пакетов в высокоприоритетной очереди отсылаются сначала они, а уж после того — обычные.

Возможно, придется подумать и об относительной приоритезации обычных пакетов в случае деградации канала. Как жертвовать данными, если послать всё желаемое не удается по не зависящим от нас причинам? Можно сохранять соотношение между частотами передачи, пропорционально снижая их все. Либо отказаться от пропорциональности, и стараться передавать по возможности одинаково часто каждый из пакетов. Решение за разработчиком.

Вариант решения

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

  • запускается таймер с частотой срабатывания, равной наименьшему общему кратному частот передачи всех пакетов, и превышающей полосу канала: для примера выше (пакеты 200 Гц, 50 Гц, 25 Гц, полоса канала 625 Гц) это может быть 800 Гц;

  • создается пул объектов-наблюдателей, в котором каждый наблюдатель отвечает за расчет времени до следующей посылки «своего» телеметрического пакета, и а) осведомлен о необходимой частоте его передачи, и б) может получать уведомления о факте отправки подконтрольного пакета в канал. Период передачи подконтрольного пакета определяется наблюдателем в тактах задающего таймера: каждые N тактов пакет должен быть передан в телеметрию. Скажем, для 200-Гц пакета эта величина составляет N=800/200=4 такта;

  • скелет класса наблюдателя:

    #include

    class PacketPeriodWatcher
    {
    public:
         void onTimerTick ()
         {
               if (_packetSent) {
                    _ticksLeft = sendingPeriodTicks;
                    _packetSent = false;
               }
               --_ticksLeft;
         } 

         int getTimeLeft () const
         {
               switch (_degradationHandling)
               {
               case DegradationHandling: keepRelativeFrequencies:
                    // Оставшееся время до отсылки выдается в виде числа
                    // собственных периодов отсылки данного пакета, округленных
                    // до ближайшего большего целого. Таким образом, чем реже
                    // пакет шлется в нормальных условиях, тем медленнее будет
                    // возрастать и его «величина просрочки» при возникновении
                    // аковой, и тем реже он будет получать преимущество по
                    // величине просрочки перед пакетами, в норме отсылаемыми
                    // более часто
                    if (_ticksLeft > 0) {
                         return (_ticksLeft + sendingPeriodTicks — 1) / sendingPeriodTicks;
                    }
                    else {
                         return _ticksLeft / sendingPeriodTicks;
                    }

               case DegradationHandling: equalizeFrequencies:
                    // Оставшееся время до отсылки выдается в форме количества
                    // оставшихся периодов задающего таймера. При 
                    // возникновении просрочки ее величина будет расти у всех
                    // пакетов одинаково быстро, и чем дольше любой пакет
                    // задерживается, тем скорее он окажется первым в очереди
                    // на отправку
                    return _ticksLeft;

               default:
                    // добавлен какой-то новый вариант стратегии, нужен обработчик
                    assert (false);
                    return 0;
               }
         }

         void onPacketSent ()
         {
               _packetSent = true;
         }

    private:
         // варианты стратегии поведения при деградации канала
         enum class DegradationHandling {
               // пытаться сохранить соотношение частот передачи пакетов
               keepRelativeFrequencies,
               // слать все пакеты одинаково часто
               equalizeFrequencies,
         };
     
         enum: int {
               // раз во сколько тиков таймера слать пакет
               sendingPeriodTicks = 4,
         };
     
         DegradationHandling _degradationHandling = DegradationHandling: keepRelativeFrequencies;
         int                  _ticksLeft = sendingPeriodTicks;
         bool                 _packetSent = false;
    };

  • по прерыванию таймера вызывается onTimerTick () для каждого из наблюдателей;

  • cуществует также таск-диспетчер канала передачи, который по завершении передачи очередного пакета и при отсутствии запросов в очереди высокоприоритетных пакетов вызывает getTimeLeft () для каждого наблюдателя из пула. Если находятся пакеты, чье время до отправки равно или меньше 0 (последнее возможно при деградации канала и означает, что пакет уже отстал от графика), выбирается тот из пакетов, чье оставшееся время минимально. Такой пакет передается в канал, а у его наблюдателя вызывается onPacketSent (). При отсутствии пакетов с оставшимся временем 0 и менее диспетчер канала инициирует переключение таска.

У этой схемы есть очевидные ограничения: требуются удобные кратности периодов передачи всех пакетов, будет достаточно часто вызываться прерывание таймера, на обработку которого тратится время процессора. Есть и подводные камни: многопоточный доступ на запись _packetSent. Однако в целом вариант вполне рабочий.

5. Буферизация

Используемый для передачи телеметрии канал может оказаться разделяемым, полудуплексным, или иметь периоды неработоспособности иной природы. Например, при записи на SD-карточку в FAT32 необходимо периодически вызывать функцию flush (), иначе есть риск при отключении питания потерять инфу за заметный промежуток времени (точнее, физически данные на флэшке будут, но в FAT-е этот факт не отразится[2]). И пока синхронизация выполняется, запись на флэшку невозможна —, а данные-то продолжают поступать, и терять их не хочется. Примерно то же происходит, когда размер файла приближается к 4 ГБ, и нужно закрывать имеющийся и создавать новый файл.

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

6. Версионность, метаданные

image-loader.svg

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

Через это совершенно необходимо иметь в потоке телеметрии номера версий всего, что может меняться: аппаратуры, прошивки основного и периферических модулей, самой телеметрии. Если есть возможность, хорошо бы сохранять и данные о дате и времени запуска. Без, как минимум, сведений о версии прошивки будет крайне трудно потом разобраться, в каких условиях были получены данные, а иногда — даже и понять, что они вообще собой представляют. Последнее случается при пакетной организации данных, когда идентификаторы переменных в пакете не передаются, а содержимое пакета определяется его структурой, каковая мутирует: вчера в пятом слове от начала пакета была только температура, а сегодня там еще два старших бита слова признаков. Посему следует заранее выделить и навсегда (в рамках текущего проекта) закрепить за версионными данными группу уникальных идентификаторов. Тогда, как бы ни менялось содержимое остальной телеметрии, версию ее всегда можно будет определить, а оттуда уж танцевать краковяк дальнейшего разбора.

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

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

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

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

Если таки есть желание подключаться к каналу когда бог пошлёт, тут же встает вопрос обнаружения в потенциально непрерывном потоке данных начала ближайшей целостной единицы. Если канал по своей природе пакетный, то проблемы нет. Вот в протоколах CAN и МКИО (MIL-STD-1553) формирование и контроль пакетов осуществляется аппаратно, и обработка подключения-отключения абонентов встроена в них изначально. Получить половину пакета и смутиться не удастся, аппаратура такое поползновение пресечет на корню. А, скажем, RS-232 оперирует именно отдельными байтами, и в этом случае понять, включившись в канал, что мы видим и где концы, не так просто.

Логичным решением в этом случае будет периодически замешивать в поток данных какой-то уникальный идентификатор — токен. Значение его нужно взять таким, чтобы вероятность появления в данных его двоичного близнеца была достаточно мала. Размер — от 2 байт: любое значение однобайтного токена обречено на слишком частые коллизии и, как следствие, ложноположительные обнаружения. Брать токен длиннее 4 байт тоже не имеет особого смысла: труднее обрабатывать, и избыточность великовата. При пакетной организации потока токен можно помещать в начало или конец пакета, а при передаче потока отдельных переменных — слать после каждых N переменных, выбирая N так, чтобы расход трафика на токен оставался достаточно мал, но появление гарантировалось спустя недолгое время после подключения к каналу.

При пакетной организации потока дополнительным источником сведений для различения токена и данных будет известная длина пакета, будь то фиксированная или задаваемая в самом пакете. Если токен принят, а ближайший пакет оказывается несамосогласованным — значит, это был не токен, особенно если его значение снова обнаружилось в ходе приема пакета: тогда, возможно, настоящий токен — вот это второе обнаружение, а до того были данные. Для случая потока отдельных переменных — повторное обнаружение токена ранее, чем после приема N переменных может значить, что первое обнаружение ложное и нужно пробовать еще.

Поможет правильному обнаружению токена и

8. Контроль целостности данных

image-loader.svg

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

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

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

Если все-таки астрологи объявили неделю гарантий целостности и механизм придется реализовывать, то его контрольные значения удобно передавать совместно с токенами сегментирования потока (в конце пакета/после отсылки N переменных). Заодно это послужит дополнительным средством определения того, действительно ли принятое значение было признаком границы сегмента.

Собственно метод расчета контрольных значений можно выбрать любой на свой вкус, исходя из доступных вычислительных возможностей и предполагаемой степени зашумленности канала: от простого XOR-а со сдвигом до мощных хэш-функций. Тут на Хабре об этом полно информации. Можно даже развернуться и внедрить коды Рида-Соломона, гарантируя себе не только контроль целостности, но и возможность восстановления сбойных участков (как всегда, взамен на дополнительный расход трафика).

9. Идентификация и пакетирование переменных

Очевидно, нельзя слать в канал произвольный поток чисел, и надеяться потом что-то в нем понять. Поток должен быть структурирован, а переменные в нем — идентифицируемы.

В простейшем случае, когда набор параметров очень короткий, можно слать все переменные одну за одной по кругу. Известно их общее количество и размер каждой, содержимое переменной определяется местоположением в цикле отправки — сведений для парсинга потока достаточно. Но даже в этом, базовом случае нужен механизм отслеживания изменений структуры передачи: с помощью ли файлов описания, предварительной ли передачей (при старте работы системы) имен переменных в нужной последовательности, или путем жесткого прописывания последовательности передачи в зависимости от версии системы в спецПО интерпретации.

Потом переменных становится больше, возникает нужда передавать их с разной частотой. Слать вместе с каждой переменной ее имя? Адский расход трафика. Фактически оправданны два пути:

  • присвоить всем переменным числовые идентификаторы, и передавать идентификатор в паре с каждым посылаемым значением соответствующей переменной;

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

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

Выбор размера идентификатора определяется количеством переменных, которые надо передавать. Удобно группировать однородные переменные, давая им близкие номера, и не забывая о запасе на развитие. Скажем, есть три подсистемы, в одной 15 параметров, в другой 40, в третьей 8. Итого меньше 70 уникальных переменных. Значит, хватит 8-битного идентификатора; под данные первой подсистемы можно выделить номера, начинающиеся с 10, 20 и 30 (итого место под 30 параметров, на будущее), второй — с 40 по 90 (60 позиций), и третьей — 100 и 110 (20 позиций). Не забыть, разумеется, данные о версии — это будут параметры с номерами [0…9]. Остальное про запас.

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

Наконец, о пакетах. Сборка переменных в пакеты и передача их в таком виде, без указания идентификаторов отдельных переменных, дает максимально возможную эффективность использования канала. Тогда, как уже говорилось, поиск нужной переменной в потоке базируется на номере пакета и известном местоположении переменной в нем (смещении относительно начала пакета). Для определения границ пакетов и для различения их друг от друга каждому пакету нужен, как минимум, заголовок, содержащий, как минимум, идентификатор пакета. Что еще будет в заголовке — определяется желанием: токен сегментации, время формирования, длина пакета, контрольная сумма, какие-то специальные признаки. Опционально добавляется уникальный либо единый признак конца пакета (который во втором случае становится по совместительству токеном сегментации). Можно слать даже вырожденные пакеты, состоящие из одного заголовка — в этом случае пакет выполняет роль переменной, сигнализирующей о чем-либо.

Зачастую канал изначально имеет пакетную природу — как те же CAN и МКИО, причем оба они разрешают, например, посылки переменной длины, и оба содержат средства контроля целостности. Тогда для задания пакетов можно пользоваться готовой аппаратной инфраструктурой, а это снимает многие головные боли с разработчика. Однако всякое аппаратное решение имеет ограничения: например, у CAN (в базовом формате пакета) это 11-битный идентификатор и максимум 8 байт данных в пакете, а у МКИО — запрос-ответная организация канала, и всего лишь 5 бит на поле подадреса оконечного устройства, т.е. на номер пакета (зато максимум 32 16-битных слова в пакете, что достаточно много в сравнении с CAN). К чему это всё: придется сначала оценить, достаточно ли аппаратных возможностей канала для задач телеметрической подсистемы, и если нет — возводить слой абстракции поверх используемого аппаратного протокола.

10. Формат времени

image-loader.svg

Если есть какой-то параметр, важность которого трудно переоценить — так это время. Без знания моментов формирования данных вся затея с телеметрией теряет смысл. В принципе, можно фиксировать время на приемной стороне, при получении переменных/пакетов, но тут есть риск исказить сведения о последовательности возникновения событий внутри системы — наружу-то пакеты выдаются с разной частотой, поэтому пакет с реакцией может быть выдан раньше пакета с причиной. Кроме того, не всегда на приемной стороне в принципе доступно достаточно точное знание момента выдачи пакета со стороны передающей, в силу программно-аппаратных ограничений платформы и/или канала (например, из-за приоритезации пакетов в той же шине CAN). Наконец, при этом подходе невозможно знать внутреннее время системы, а без него во многих случаях никакой содержательный анализ не провести.

Поэтому возникает нужда передавать время системы в потоке данных. Спрашивается: как это лучше делать?

Прежде всего, время следует передавать в форме целочисленной переменной, дальше будет сказано, почему. Выбрать тогда остается размер переменной и цену ее младшего разряда. Оба этих параметра определяются сочетанием пары же условий: насколько точно время нужно знать, и как велика максимальная длительность непрерывной работы (при условии, конечно, что время нужно знать от момента включения; если требуется только знание относительной удаленности событий друг от друга, переменной времени можно позволить циклически переполняться, это не станет помехой).

С ценой младшего разряда все довольно просто: во многих случаях в системе присутствует какой-то базовый высокочастотный цикл, чаще которого ничего не происходит — скажем, опрос первичных датчиков с частотой 1000 Гц. В этом случае за единицу времени рационально принять период этого цикла (1 мс в примере). Все значимые события в системе так или иначе будут подчинены периоду опроса датчиков, и моменты их возникновения будут кратны периоду опроса. Если же такого цикла нет, придется пытать аналитиков относительно того, какая точность знания времени их устроит.

Выбор размера переменной времени зависит от ожидаемой длительности непрерывной работы. Формально нижняя граница количества бит определятся как log2(максимум времени/цена младшего разряда), округленный до ближайшего большего целого. А прикинуть, хватит разрядности или нет, можно и на глаз, зная предельное целое число, умещающееся в выбранное слово. Например, 16-битная переменная переполнится в нашем примере через 65,536 секунды, а 32-битная — через без малого 4,3 миллиона секунд. Для большинства задач достаточно.

В принципе, если запись данных ведется непрерывно с самого начала работы, переменной тоже можно позволить свободно переполняться: истинное время будет восстановлено при обработке. Но если планируется подключение к каналу в произвольные моменты времени, придется выделить под время побольше места. В качестве альтернативного решения можно разделить переменную времени на старшую и младшую части, и передавать младшую часть настолько часто, насколько это необходимо (в каждом пакете/каждые N переменных), а старшую — лишь время от времени, но не реже периода переполнения младшей части. Лучше, конечно, еще чаще: возможны сбои, или отключение от канала до момента передачи старшего слова времени. Таким способом можно записывать совершенно гигантские времена с огромной точностью.

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

Так в чем все-таки плюс целочисленного времени? Точность. Точность в таком представлении никогда не теряется, независимо от того, сколь долго продолжается запись. В этом его кардинальное отличие от формата с плавающей точкой: можно, конечно, передавать время как переменную типа float (32-битное одинарной точности по IEEE-754), и потом иметь при обработке сразу готовое бинарное значение, но у float-а точность — лишь 7 с хвостиком десятичных разрядов. В нашем примере это значит, что, начиная с примерно десяти тысяч секунд записи, позиции точек данных станут дерганными, и тогда о качестве и удобстве анализа можно будет забыть. При 100-мкс точности проблемы полезут уже где-то с тысячи секунд. Можно, конечно, подключить всю мощь double-а с его практически 16-ю десятичными разрядами, но, камон, это 64 бита данных. Передавая время в 64-битной целочисленной переменной с 

© Habrahabr.ru