Постановка задачи
При разработке очередного устройства на микроконтроллере, столкнулся с ситуацией, где требовалась непрерывная регистрации большого количества данных. Устройство должно было сохранять набор данных, состоящий из метки времени и результатов измерений шести АЦП от 100 раз в секунду на SD карту (назовем этот набор данных — пакетом), а затем эти данные анализировать на компьютере в виде графиков. Также было необходимо параллельно с записью данных на SD карту, передавать их по UART. Эти данные должны были занимать как можно меньше места, так как данных ну уж очень много. При этом нужно было как-то разделять эти пакеты, потому что данные шли непрерывным потоком. Порывшись в интернете ничего хорошего, не нашел, поэтому было принято решении о создании собственного протокола и библиотек под него.
И тут появился он – Packet streaming protocol (PSP1N)
В результате некоторых раздумий было решено следующие: в протоколе данные будут передаваться пакетами состоящими из N байт, где первый бит каждого байта отводится на признак стартового бита для синхронизации пакетов (отсюда и название протокола), остальные семь бит отводятся под данные. Последовательность и размеры данных известны заранее.
Пример:
Под метку времени выделим 32 бита, под измерения АЦП 60 бит (6 каналов по 10 бит), итого требуется 92 бита. Так как у нас в байте можно передать 7 бит полезных данных, то пакет будет состоять из 14 байт (92 бита / 7бит = 13,14… округляем в большую сторону получается 14). В 14 байтах 112 бит информации, из которых 14 бит занято признаком стартового бита 92 бита полезных данных, остается 6 неиспользуемых бит (в которые можно записать еще какую-нибудь информацию, но для простоты примера не будем их использовать).

Где 7-ой бит – признак стартового бита (обозначает начало пакета), 6,5,4,3,2,1,0 – биты данных.
Приемная сторона также знает, что получает пакет из 14 байт в которых первый бит первого байта — стартовый бит (1) (в остальных байтах стартовые биты 0), далее в битах данных по порядку идут 32 бита метки времени, затем 10 бит измерения АЦП№1, затем 10 бит АЦП№2 и так далее…
Аналогично проходит запись на SD карту и чтение с нее согласно протоколу. Итого за одни сутки записи на SD карту у нас получается 115,4 Мб информации (14 байт х 100 измерений в секунду х 3600 сек х 24 часа).
Такая структура данных еще удобна тем, что в последующем мы можем с любого места файла выбрать блоки данных и вывести их в виде графиков, тем самым, не загружая весь файл в оперативную память (который может достигать нескольких десятков гигабайт). А также сможем реализовать удобный скроллинг этих графиков подгружая нужные пакеты.

Пора приступить к программной реализации для микроконтроллера
Библиотеку под микроконтроллер пишем на С++.
Для удобства создадим класс:
class PSP { public: /* Функция инициализации init: startBit - стартовый бит 0 или 1 *arrayByte - указатель на массив байт пакета sizeArrayByte - количество байт в пакете */ void init(byte startBit, byte* arrayByte, byte sizeArrayByte); /* Функция добавления данных в пакет pushData: sizeBit - размер данных в битах value - передаваемое значение (должно умещаться в указанный ранее размер в битах) */ void pushData(byte sizeBit, uint32_t value); /* Функция получения данных в сформированном пакете popData: return пакет данных в массиве байт. */ byte* popData(); protected: byte startBit; //стартовый бит byte* arrayByte; //массив байт пакета byte sizeArrayByte; //размер массива пакета byte position = 0; //текущая позиция в пакете bool clearFlag = false; //флаг инициализации массива void setStartBit(byte &value); //установка стартового бита в байте void clearStartBit(byte &value); //сброс стартового бита в байте };
С методом инициализации думаю все понятно:
void PSP::init(byte startBit, byte* arrayByte, byte sizeArrayByte) { this->startBit = startBit; this->arrayByte = arrayByte; this->sizeArrayByte = sizeArrayByte; }
С методом добавления данных посложнее, здесь путем хитрых поразрядных манипуляций размещаем данные в нашем массиве байт.
void PSP::pushData(byte sizeBit, uint32_t value) { byte free; byte y; int remBit = 0; //для хранения остатка не добавленных бит //Если добавляем данные впервые, то инициализируем массив нулями if (!clearFlag) { for (byte i = 0; i < sizeArrayByte; i++) { arrayByte[i] = 0; } clearFlag = true; } //Начинается цикл разбиения входящего значения на куски по 7 бит и записи в массив while (remBit > -1) { free = 7 - (position) % 7; //количество свободных бит в байте на текущей позиции y = (position) / 7; //текущий номер байта в массиве //Определяем помещается ли значение в текущей позиции remBit = sizeBit - free; //если значение помещается в текущий байт if (remBit < 0) { arrayByte[y] |= value << ~remBit + 1; //помещаем значение в байт, со смещением влево position += sizeBit; //увеличиваем текущую позицию на размер добавленных бит данных remBit = -1; //указываем что все биты значения поместились } //если значение больше чем свободных бит else if (remBit > 0) { arrayByte[y] |= value >> remBit; //записываем в текущий байт столько бит, сколько свободного места position += sizeBit - remBit; sizeBit = remBit; //изменяем размер бит на оставшиеся не поместившиеся биты } //если свободных бит столько же сколько и бит значения else if (remBit == 0) { arrayByte[y] |= value; //добавляем значение в байт position += sizeBit; remBit = -1; //указываем что все биты значения поместились } clearStartBit(arrayByte[y]); //очищаем стартовый бит } setStartBit(arrayByte[0]); //устанавливаем стартовый бит }
Метод получения массива байт пакета:
byte* PSP::popData() { position = 0; //сбрасываем текущую позицию clearFlag = false; //сбрасываем флаг инициализации массива return arrayByte; //возвращаем массив байт }
И напоследок пару вспомогательных функций:
//Функция установки стартового бита в байте void PSP::setStartBit(byte &value) { if (startBit == 0) value &= ~(1 << 7); else value |= 1 << 7; } //Функция сброса стартового бита в байте void PSP::clearStartBit(byte &value) { if (startBit == 1) value &= ~(1 << 7); else value |= 1 << 7; }
Подведем итоги
В результате проделанной работы «родился» компактный протокол потоковой передачи данных PSP1N и готовые библиотеки, которые можно скачать с GitHub тут. В данном репозитории вы найдете:
- ExampleColsolePSP1N/ пример использования библиотеки на C#
- PSP1N_CPP/ содержит библиотеку PSP для работы с протоколом и пример использования ее на Arduino
- PSP1N_CSHARP/ библиотека протокола для .NET
Для демонстрации работы протокола можно прошить скетч в Ардуину и в примере ExampleSerialPortRead на компьютере получать данные с микроконтроллера через COM port. Там эти данные дешифруются и отображаются в консольном приложении. Про библиотеку написанную на C# для приемной стороны я расскажу в другой раз.
TestingConsole:

UPDATE (31.03.19): Изменен алгоритм кодирования и декодирования
