Введение

На сегодняшний день существует много способов организовать обмен данными между Desktop-приложением и устройствами на микроконтроллерах: Wi-Fi, Bluetooth, RF, USB, преобразователи интерфейсов и т.д.

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

В случае использования интерфейсов RS-232, RS-485, RS-422 или чистого UART организация пакетного обмена данными ложится на программиста.

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

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

Нужно отправить из Qt-приложения пакет данных на устройство с микроконтроллером STM8. К компьютеру присоединён USB-RS485 преобразователь, а на устройстве соответственно преобразователь RS485-UART. Нужно учесть, что на линии связи возможен высокий уровень помех. В этой ситуации было принято решение написать быструю и легкую библиотеку для гарантии целостности принятых данных. *Гарантию доставки данных брала на себе бизнес-логика. Библиотеку решил назвать Sheller.

Требования к разрабатываемой библиотеки были следующие:

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

  • Минимальное количество внутренних буферов;

  • Наличие надежной, быстровычисляемой контрольной суммы;

  • Побайтного занесения данных;

  • Возможность работы в условиях поврежденных и потерянных байт в пакете;

Выполнение задачи

Принцип работы: определимся с понятием пакета. Пакет представляет собой стартовый байт, данные пользователя и два байта контрольной суммы CRC-16:

В начале использовался алгоритм контрольной суммы CRC8, однако в ходе тестов было выяснено, что при длине пакета в 8 байт количество коллизий было слишком высоким. Тест заключался в следующем: некоторые пакеты специально отправлялись битыми (менялись значения или вовсе удалялся байт из пакета). В итоге на каждый 50000й пакет происходила коллизия CRC и неправильный пакет передавался в бизнес-логику.

Первый случай коллизии происходил, когда значения байтов в пакете были изменены таким образом, что их контрольная сумма осталась прежней:

А во втором случае часть побитого пакета с частью целого пакета соединялись в один пакет, который проходил по контрольной сумме:

*Красной чертой обозначен пакет, последние байты которого не дошли до приемника. Зеленой чертой показан нормальный пакет. Синей чертой изображен пакет, который был отправлен в бизнес-логику.
*Красной чертой обозначен пакет, последние байты которого не дошли до приемника. Зеленой чертой показан нормальный пакет. Синей чертой изображен пакет, который был отправлен в бизнес-логику.

Поэтому было принято решение перейти на CRC16. При запуске тоже теста было пройдено более 2млн пакетов без коллизий и тест был завершен удачно. Вычисление контрольной суммы происходит по табличному методу для ускорения работы.

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

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

Работу всей библиотеки можно описать следующей схемой:

Данная библиотека реализована на Си и на C++. C++ версия предназначена в основном для Arduino.

Для проверки работы Sheller на микроконтроллере был написан ShellerTerminal:

*представляет собой обычный COM-port терминал с использованием алгоритма Sheller.
*представляет собой обычный COM-port терминал с использованием алгоритма Sheller.

Информацию по использованию библиотеку вы можете найти на GitHub.

P.S. Библиотека разрабатывалась для работы на ATMEL и STM8.

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


  1. Leopoldius
    15.09.2021 18:23
    +1

    Достаточно красивая имплементация, теперь предлагаю ответить на вопрос, что будет, если подряд встретятся несколько байтов 0x23, который выбран как стартовый байт. Для упрощения жизни, много лет назад придумали такую штуку, как ESC последовательности и byte stuffing

    Очень рекомендую посмотреть имплементацию протокола wake и slip, как источник вдохновения.


    1. vladyslavNovytskyi Автор
      15.09.2021 18:27

      Если отправить несколько стартовых байтов, то на стороне приемника ничего не произойдет, так как количество байт в пакете по умолчанию 8. Если отправить 8 стартовых байтов, то приемник попытается прочесть пакет, но не пройдет проверка CRC.


      1. Asmcavr
        16.09.2021 10:23

        автор в очередной раз изобретает велосипед)

        хотя это полезно для мозга и бесполезно практически)


  1. Sap_ru
    15.09.2021 18:35

    А чего бы сразу byte-stuffing не сделать и не мучаться больше? У вас при плотном обмене неизвестное время восстановления получается, вплоть до бесконечного.
    Например, сделайте началом пакета 0xFA+<байт с установленным старшим битом>, а все встретившиеся 0xFA заменяйте на 0xFA+0x05. И будет вам праздник. Можете даже в этом первом байте в семи оставшихся битах что-то полезное кодировать — код команды, длину пакеты или ещё что-то, а сочетания 0xFA+<байт с нулевым старшим битом> использовать для какой-то дополнительной сигнализации (конец пакета, например, чтобы длину не передавать) И всё — протокол гарантированно восстанавливается после сбоя на следующем же пакете.


    1. vladyslavNovytskyi Автор
      15.09.2021 18:46

      Интересная идея. Возможно попробую так сделать в следующей версии


      1. edo1h
        16.09.2021 06:16
        +1

        заметьте, когда вам в первом комментарии предложили то же самое, вы даже не поняли о чём речь (и поленились погуглить!)
        нет, неплохо изобретать что-то своё. но прежде чем поделиться своим изобретением с другими неплохо бы изучить то, что уже было придумано до вас.


    1. Polarisru
      16.09.2021 11:57

      Заголовочный байт + контрольная сумма + кольцевой буфер так же гарантированно восстанавливают следующий же пакет.


      1. Sap_ru
        20.09.2021 22:06

        много лишних телодвижений и крайне не эффективно. Представдье, что у вас заголовочный байт 0xFF, «битый» пакет с полем данных длинной 255 байт и в поле данных заполнено 0xff. Сколько раз вы посчитаете контрольную сумму до восстановления синхронизации и где будете искать новую работу если такое пойдёт в продакшн на контроллерах?


        1. Polarisru
          20.09.2021 22:21

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


          1. Sap_ru
            20.09.2021 22:28

            Вы случаем не Web-программист, чтобы такие заявления делать? Тогда посчитайте мне CRC на JS «аппаратно» — я посмеюсь.
            У вас устройство может представлять из себя микроконтроллер с частотой 35 МГц и без всяких аппаратных подсчётов CRC. И чтобы что-то кто-то «аппаратно» посчитатл, то кто-то эти данные в этот блок расчёта должен передать, а потом дождаться результата. Ваш алгоритм плох абсолютно со всех сторон. Даже на «больших» процессорах это будет гемор и куча лишней работы.


            1. Polarisru
              20.09.2021 23:06

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

              Кстати, судя по всему, вы вообще не в курсе, как считается контрольная сумма аппаратными средствами в современных контроллерах, даже тех, у которых всего-то 48МГц в максимуме.


              1. Sap_ru
                21.09.2021 02:53

                255 сигнатурных байт, это пример. Если их будет 16, то всё равно, будет неприятно. Я очень дажде в курсе, но чтобы пересчитать контрольную сумму, вам всё равно нужно передать все данные блока. И даже DMA к CRC тут не сильно поможет при реальных последовательных интерфейсов. Т.е. при потере синхронизации, вы предлагаете на каждый сигнатурный байт анализировать и разбирать заголовок и считать CRC. Да ещё и хранить сырые принятые уже «обработанные» данные на случай потери синхронизации. Совершенно ужасный алгоритм для встраиваемых систем. У вас и память лишняя расходуется, и какие-то кольцевые буфера городить нужно (ну, или резервировать более двойного размер пакета под буфер приёма), и худшее время обработки пакета получается огромным относительно нормального. Т.е. если вы обычный пакет, например, обрабатываете 0.1 мс, то худший случай у вас запросто получается в сотни раз дольше. А резервировать время обработки пакета нужно исходя из худшего случая.
                И всё зачем? Чтобы не делать простейшего байт-стафинга, где ни данных от уже посчитанной контрольной суммы хранить не нужно, и время обработки пакета получается предсказуемое. Можно даже на лету пакет или какие-то его части разбирать, что сильно упрощает программу при использовании для приёма чередующихся DMA-буферов. Можно например по 32 байта буферами принимать и иметь только один буфер для корректно-принятого пакета.


  1. ktod
    15.09.2021 19:04
    +2

    Мой опыт подсказывает, что самый надежный и удобный способ определить конец/начало пакета - это пауза на линии. Тем более, что современные микроконтроллеры поддерживают детектирование и обработку паузы на аппаратном уровне.


    1. vladyslavNovytskyi Автор
      15.09.2021 19:58

      В таком случае разве не снижается пропускная способность канала ?


      1. checkpoint
        15.09.2021 23:04

        Снижается и еще как.

        Кстати, в UART предусмотрен специальный сигнал "break condition" который часто используют для того, что бы оповестить приемную сторону о начале новой последовательности данных или об окончании предидущей. Например, протокол DMX512.


      1. Sun-ami
        15.09.2021 23:10

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


        1. Sap_ru
          20.09.2021 22:10

          Если у вас запрос-ответ или есть естественные паузы в обмене, то не нужна. Ну, и стартовый маркер лучше выбирать 0xF0 и правильно использовать контроль чётности.


          1. Sun-ami
            20.09.2021 23:06

            В случае обмена запрос-ответ с несколькими ведомыми пауза не нужна перед ответом, но нужна перед запросом. Стартовый маркер 0xF0 не поможет аппаратному асинхронному приёмнику восстановить битовую синхронизацию в непрерывном потоке данных — лучше заменить стартовый маркер на паузу, если стоит задача максимально использовать пропускную способность канала. Естественные паузы в обмене обычно имеют случайную длительность, поэтому понадеявшись на них можно получить большое максимальное время доставки даже в малошумном канале.


            1. Sap_ru
              21.09.2021 03:01

              Это кажется, что синхронизация не восстанавливается ;)
              А если внимательно посмотреть, то на на самом деле, так как ошибка стартового бита приводит к потере синхронизации, то синхронизация гарантированно восстанавливается на определённой последовательности байт в непрерывном потоке ;) Конкретная последовательность зависит от параметров обмена. Во многих случаях это просто одиночный байт 0xFF, который гарантированно восстаналивает синхронизацию на стартовом бите следующего байта.
              А если копать ещё глубже, то есть байты, и сочетания байт, которые повышают вероятность корректного восстановления синхронизации (т.е.можно доказать что при случайной ошибке синхронизация восстановится, например, в 80% случаев, а в 20% придётся ждать паузы в обмене данными).
              Паузы это плохо по той причине, что при быстром обмене их сложно реализовывать, а при обмене с PC, так, и вообще небольшие паузы нельзя гарантировать. Если возможен длительный непрерывный поток данных и нужно гарантированное время восстановления, то проще определить последовательность байт, которая гарантированно восстанавливает синхронизацию и слать эти байты в начале/конце пакета. И программы упрощаются, и гарантия восстановления, и средняя скорость обмена получается выше.


      1. ktod
        16.09.2021 04:29

        А еще мой опыт показывает, что разработка на самом пределе технических возможностей сущности - это bad practices.


      1. dernuss
        16.09.2021 11:46
        +1

        Для stm8 конечно скорость очень важна;(


    1. Sap_ru
      20.09.2021 22:08

      А как гарантировать небольшую паузу под Windows/Linux? Или пауза будет 100мс и пропускная способность максимум 10 пакетов в секунду на мегабитном канале?
      Это очень плохое решение. Лучший способ — маркеры и байт-стафинг.


      1. Sun-ami
        20.09.2021 23:15

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


        1. Sap_ru
          21.09.2021 03:18
          -1

          В каком драйвере? Вы будете под своё устройство драйвера под Windows/Linux писать? Ха-ха.
          А контроллерах, измерение интервалов ломает работу с DMA, так как вы не знаете времени окончания приёма, например. Да, и вообще, лего сказать «таймеры». В сложной программе это выливается в лишние танцы с бубном, особенно на высоких скоростях.
          У байт-стафинга вообще нет недостатков при обмене запрос-ответ, либо при выделенном мастере. А при полностью асинхронном обмене, без чётности, либо с чётной чётностью байт 0xFF восстанавливает битовую синхронизацию.
          Вопрос — городить огород с задержками и таймерами, если есть более простые и гарантированно работающие решения?


  1. rsashka
    15.09.2021 19:11
    +4

    А еще проще взять уже готовую библиотеку Modbus и не париться.


    1. checkpoint
      15.09.2021 22:55

      Modbus - транзакционный протокол: один запрос, один ответ. На протокол пакетной передачи не тянет, так как не позволяет пересылать запросы независимо от ответов. Помимо этого в Modbus-е конец передачи определяется паузой в передаче, что для интенсивного обмена совсем "не айс".

      Но похоже, что для задачи решаемой автором статьи даже Modbus-а более чем позаглаза. :)


      1. rsashka
        16.09.2021 08:42
        +1

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

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


      1. Asmcavr
        19.09.2021 09:59

        Капитан Ясен Куй, ведь приемопередатчиков на линии может быть ого-го, а автор чпокается с двумя приемопередатчиками . Но если добавить в линию ещё кого нить то мега "протокол" автора превратится в генератор белого шума).


        1. SpiderEkb
          19.09.2021 12:31

          Когда в линии несколько абонентов (типичная ситуация для RS485), надо или разрабатывать протокол разрешения коллизий, или, что проще в реализации, переходить на опросный режим master-slave. Т.е. 1 узел всегда master, сеанс связи с любым другим узлом может инициировать только он. Все остальные - slave. Даже если им "есть что сказать", сидят и молчат в тряпочку пока их не спросят.


    1. Sap_ru
      20.09.2021 22:14
      -1

      Modbus ужасен со всех сторон. Там конец пакета паузой передаётся. Это снижает пропускную способность, сильно усложняет логику работы (правильно выдерживать и измерять паузы на больших скоростях обмена) и очень плохо работает если один из абонентов это компьютер под Windows/Linux, где гарантия временных интервалов на асинхронных интерфейсах работает очень плохо.
      Либо используйте MODBUS ASCII, но это очень медленно.
      А ещё обратите внимание, что в модбасе есть проблема с тем, чтоб различить запрос и ответ — MODBUS на полудуплексной линии (RS422) это боль и страдания.
      По возможности избегайте использовать Mobdus.


      1. Sun-ami
        20.09.2021 23:44
        +1

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


  1. checkpoint
    15.09.2021 22:49
    +5

    Протоколы SLIP, PPP (HDLC) придумали 30 (если не 40) лет назад, они отработаны и опробированы в миллионах телекоммуникационных изделий, написаны и опубликованы сотни библиотек разной степени сложности. Зачем Вы изобретаете велосипед ? Тем более, что проблему Вы таки не решили, а поверхностно закопали (подложили мину себе и своим пользователям).


    1. belav
      16.09.2021 07:24

      На старших курсах в универе ему расскажут про теорию передачи данных, про вероятность ошибки и тд.


    1. Sap_ru
      20.09.2021 22:23

      А Вы сами их применяли?
      SLIP — не имеет контроля ошибок. Это чистый байт-стафинг и при этом далеко не лучший.
      HDLC — мимо кассы, так как полагается на сигнализацию физического уровня и очень ресурсоёмкий при программной реализации.
      PPP -108 служебных байт на пакет, из которых 4 имеют неизменной значение? И при этом он для синхронизации полагается на протоколы более низкого уровня. Чем оно тут поможет?


      1. edo1h
        21.09.2021 20:27

        А Вы сами их применяли?

        так речь о том «где можно подсмотреть идеи», а не о том, чтобы взять готовое.


        SLIP — не имеет контроля ошибок. Это чистый байт-стафинг и при этом далеко не лучший.
        HDLC — мимо кассы, так как полагается на сигнализацию физического уровня и очень ресурсоёмкий при программной реализации.
        PPP -108 служебных байт на пакет, из которых 4 имеют неизменной значение? И при этом он для синхронизации полагается на протоколы более низкого уровня. Чем оно тут поможет?

        статья про написанное вами была бы в 1000 раз полезнее этой статьи об очередном велосипеде.


        1. vladyslavNovytskyi Автор
          21.09.2021 20:55

          Какой протокол тогда лучше использовать для связи STM8 и приложения?


          1. edo1h
            22.09.2021 03:22

            сначала о том как не стоит делать по моему мнению:


            • разумеется, ничего такого тяжеловесного как hdlc и ppp использовать не стоит;
            • о протоколах, использующих задержку для определения старта передачи, были уже написаны вполне обоснованные критические комментарии;
            • иногда для маркировки начала передачи используют девятый бит (aka parity), но это опять же потенциальный источник проблем с переносимостью.

            экранирование (byte stuffing) не нравится тем, что из сообщения фиксированной длины получается сообщение переменной.


            Побайтного занесения данных;

            я бы отказался от этого требования, слал по 7 бит на символ, восьмой бит же использовал как признак начала пакета. реализация очень простая, требуется только сдвиг и логическое «или».


            вы не привели достаточно сведений: ожидаемый уровень ошибок, какие «повреждения» характерны для реальной линии, насколько нежелательна потеря пакетов; поэтому «лайтовый» вариант:
            исходим из того, что в пакете 64 бита данных + минимум 8 бит crc (я очень люблю crc за его способность гарантированно обнаруживать некоторые виды ошибок), что даёт нам 72 бита нагрузки; мы договорились о 7 битах на символ — итого будем использовать 11 символов/77 бит.
            5 бит остаются свободными, в них можно запихать какие-то магические числа, например, так:


            1010dddd
            0ddddddd
            ...
            0ddddccc
            110ccccc

            (d — полезная нагрузка, c — crc8)


            тогда первый байт всегда будет 0xA_, последний 0xC_ или 0xD_, а все остальные будут начинаться на цифру от 0 до 7. это достаточно удобно при отладке (сразу видны границы пакета), да и парсить просто.


            код crc8 можно выбрать из тех, которые на сообщениях нужного размера гарантировано детектируют до 4 битовых ошибок (hd=4).


            для относительно незашумлённых линий это будет неплохо работать.
            не нужно никакой чёрной магии с таймингами при кодировании/раскодировании, а значит без проблем можно использовать usb-контроллеры или передавать по сети.
            ну и простота реализации, конечно; самая сложная часть тут — кастомный crc8.


            P. S. ещё встречал совет отправлять 0xFF перед посылкой, на первый взгляд выглядит разумно, если нет жёстких требований по утилизации канала, то я бы именно так и поступил (со стороны приёмника это никакой специальной обработки не требует).


  1. SpiderEkb
    16.09.2021 13:27

    А из каких соображений используется фиксированный размер буфера?

    Просто долго занимался разработкой системы мониторинга инженерного оборудования зданий, начинали с достаточно простой конфигурации:

    ПК -> RS232 -> Головной Контроллер (ГК) -> RS485 -> несколько Домовых Контроллеров (ДК), от каждого ДК -> RS485 -> несколько Устройств Сопряжения с Объектом (УСО).

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

    Несколько интерфейсных клиентов (разного назначения) -> TCP -> Микроядро системы (вместо бывшего ГК) -> UDP -> несколько IP-шлюзов (вместо бывших ДК), от каждого шлюза -> RS485 -> несколько УСО

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

    Структура датаграммы:

    <Фиксированый заголовок <Общая длина датаграммы><Адрес (идентификатор) отправителя><Адрес (идентификатор) получателя><Идентификатор датаграммы (уникален для отправителя)><Ключ (команда)>><Блок данных переменной длины, содержимое определяется ключом (командой)><CRC16>

    Чтение в три приема - сначала фиксированый заголовок, по нему понятно сколько данных надо прочитать. Дальше чтение данных и потом чтение CRC16.

    Дальше - сравнение CRC16.

    Дальше, если идентификатор датаграммы равен 0xFFFF - значит датаграмма не требует подтверждения приема - отдаем в обработку. Если не равен - требуется подтверждение - отсылаем ответную датаграмму с кодом (дай бог памяти, по-моему) 0xE1, идентификатором 0xFFFF и идентификатором принятой датаграммы в блоке данных.

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

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

    Датаграммы могут инкапсулироваться одна в другую при необходимости. Все это маршрутизируется. Например, интерфйесный клиент посылает команду какому-то устройству. Команда уходит на микроядро. То по адресу устройства определяет на какой из шлюзов нужно послать эту датаграмму и отдает ему. Тот опять определяет на каком из УСО подключено это устройство и отсылает туда.

    Начинали ту разработку мы в 93-м, ушел оттуда я в 17-м (сначала я все что на ПК работает писал, потом это разрослось и занимался главным образом разработкой микроядра, клиентов делал другой человек).


    1. Sap_ru
      20.09.2021 22:24

      Реализуйте это под Windows, например, со всеми таймаутами :)


      1. SpiderEkb
        21.09.2021 08:37

        Не поверите, но именно под Win все это и работало. И действительно работало и все еще работает.

        Правда, в полной версии ушли на архитектуру ПК - IP шлюзы по UDP, IP-шлюз - УСО по RS-485. Но это требования заказчика - им нужна была распределенная система чтобы из одной точки контролировать много объектов очень сильно разнесенных в пространстве.

        "Размах" полной системы - порядка 20-ти IP шлюзов, на каждом от 5-ти до 30-ти УСО (зависиит от конфигурации каждого конкретного "куста"). Общее количество конечных устройств в системе порядка 1000 (точно затруднюсь зазвать цифру)

        А лайтовая версия для локальных объектов по прежнему работает вся на 485. Там нет IP шлюзов, несколько УСО по 485 (через преобразователь RS-485-USB) подключаются непосредственно к ПК.

        В лайтовой версии, конечно, поменьше - на то она и лайтовая. Там 2-3 УСО и устройств пара десятков всего.


  1. ModestONE
    16.09.2021 15:03
    +2

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

    ЗЫ: Для каких железок, вы это реализуете?


    1. vladyslavNovytskyi Автор
      16.09.2021 15:09

      В основном для STM8


      1. ModestONE
        16.09.2021 15:11

        В основном для STM8

        Понятно. Что создаете, если не секрет?


        1. vladyslavNovytskyi Автор
          16.09.2021 15:21

          Девайс, который будет выступать переходником USB-NRF24L01 + приложение на QML