Как стать автором
Обновить

Протокол потоковой передачи пакетов для микроконтроллеров PSP1N

Время на прочтение4 мин
Количество просмотров11K

Постановка задачи


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


  1. ExampleColsolePSP1N/ пример использования библиотеки на C#
  2. PSP1N_CPP/ содержит библиотеку PSP для работы с протоколом и пример использования ее на Arduino
  3. PSP1N_CSHARP/ библиотека протокола для .NET

Для демонстрации работы протокола можно прошить скетч в Ардуину и в примере ExampleSerialPortRead на компьютере получать данные с микроконтроллера через COM port. Там эти данные дешифруются и отображаются в консольном приложении. Про библиотеку написанную на C# для приемной стороны я расскажу в другой раз.


TestingConsole:



UPDATE (31.03.19): Изменен алгоритм кодирования и декодирования
Теги:
Хабы:
+8
Комментарии30

Публикации

Изменить настройки темы

Истории

Работа

.NET разработчик
72 вакансии
Программист C++
123 вакансии
QT разработчик
6 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн