Русский
Русский
English
Статистика
Реклама

Stm32

Как раскирпичить STM32

25.03.2021 16:19:55 | Автор: admin

Здравствуйте! Меня зовут Дмитрий Руднев. В этой публикации я поделюсь своим горьким опытом.

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

В процессе отладки бывает всякое. Не беда, если STM32 после прошивки ведёт себя неадекватно. Обидно, если при этом к нему не удаётся подключиться.

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

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

Connect Under Reset


Если на входе BOOT0 обнаружен низкий уровень, системный загрузчик передаёт управление пользовательской программе, находящейся в FLASH-памяти. Если при этом к интерфейсу SWD подключен в режиме Connect Under Reset внутрисхемный отладчик, ему удаётся управление перехватить.

Рассмотрим, как это сделать с помощью программы STM32 ST-LINK Utility и программатора ST-LINK/V2-1. Программа была получена с официального сайта ST. Программатор пришёл в составе платы NUCLEO-F446ZE.

Запускаем программу, входим в Settings:


В окне Settings выбираем режим Connect Under Reset:


Подключаемся к нашему кирпичику:


Производим очистку памяти программ:



Подключение по UART1


Очень часто для прошивки STM32 применяются недорогие китайские клоны ST-LINK/V2. Без аппаратной переделки режим Connect Under Reset они не поддерживают. В этом случае стоит попытаться очистить память программ, подключившись к микроконтроллеру по UART.

Если подать на вход BOOT0 высокий уровень, то можно подключиться к микроконтроллеру через интерфейс UART1 с использованием программы Flash Loader Demonstrator. Программу можно получить с официального сайта ST. Преобразователь USBUART подойдёт любой.

Запускаем программу. Выбираем COM-порт, к которому подключен преобразователь USBUART:


Убеждаемся, что соединение установлено:


На следующем экране программа показывает области памяти микроконтроллера:


На следующем экране мы должны выбрать действие. Выбираем Erase All:


Очистка памяти программ успешно завершена:


На этом месте надо вернуть на вход BOOT0 низкий уровень.

От автора


Любое несчастье, которое происходит с Вами, с кем-то другим уже происходило. Всё, что описано в публикации, происходило со мной и моим оборудованием.

Первая часть публикации повествует о том, как я в самом начале самоизоляции закирпичил новенькую оригинальную NUCLEO-F446ZE.

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

Предыдущий опыт был более трагичным. Я использовал совсем бюджетную плату в связке с очень недорогим клоном ST-LINK/V2. В один прекрасный миг, связь с платой по SWD пропала.

Результаты поиска в сети убедили меня использовать режим Connect Under Reset. Ничтоже сумняшеся, я подключил вывод NRST микроконтроллера к выводу Reset программатора. Не знал я тогда, что этот вывод используется только при работе с STM8.

Сигнал сброса не проходил. Связь по интерфейсу SWD не восстанавливалась. Игры с кнопкой Reset на плате результата не давали. В самый раз было начинать читать мануалы.

И метод RTFM сработал! Из раздела 2.3.10 Boot modes datasheet DS5792 rev13 я узнал про загрузку через UART1. Затем я нашёл информацию о Flash Loader Demonstrator. Восстановить работоспособность STM32F103RET6 с этими инструментами было уже несложно, что и вылилось в 113 слов и пять картинок второй части публикации

Буду рад, если мой опыт будет кому-то полезен!

Подробнее..

CAT-интерфейс для трансивера Радио-76

13.05.2021 12:17:12 | Автор: admin
В предыдущей публикации о трансивере Радио-76 упоминалось о синтезаторе частоты с CAT-интерфейсом. В этой статье тема CAT-интерфейса будет раскрыта подробней.

CAT-интерфейс (Computer Aided Transceiver) предназначен для управления частотой, видами модуляции и другими функциями радиостанции с помощью компьютера.

В сети есть множество описаний радиолюбительских синтезаторов на базе Si5351, но в массе своей CAT они не поддерживают. Данная публикация этот пробел должна восполнить.

Аппаратное решение


Аппаратное решение не несёт никакой новизны. Синтезатор создавался из того, что было под рукой: плата Black Pill c микроконтроллером STM32F411CEU6, дисплей SSD1306 и микросхема синтезатора частоты Si5351A-B-GT.

Схема подключения Si5351A-B-GT приведена ниже.


Подтягивающие резисторы R1, R2 устанавливаются на выводы дальнего от микроконтроллера устройства на шине I2C. Сигнал с выхода CLK0 подаётся на первый смеситель основной платы трансивера Радио-76. Сигнал с выхода CLK2 подаётся на второй смеситель трансивера. Делители напряжения на резисторах R3, R6 и R4, R5 препятствуют перегрузке смесителей.

Вся схема собрана на печатной плате переходника SSOP-DIP:


Из имеющихся у меня в наличии кварцевых резонаторов на частоту 25MHz и 27MHz ни один на этих частотах не запустился. Параллельно включенные резисторы и конденсаторы ситуацию не спасали. На фотографии кварц, который запустился на частоте 24MHz, когда параллельно ему был включен резистор номиналом 1МОм.

Конфигурация микроконтроллера


Проект создан на основе платы Black Pill c микроконтроллером STM32F411CEU6 в среде разработки STM32CubeIDE:


Шина I2C подключена к выводам PB9, PB10 микроконтроллера. К выводам PB0, PB1, PB2 подключены тангента (PTT, Push-To-Talk) и контакты телеграфного ключа (KEY_DIT, KEY_DAH). Вывод PC13 служит для аппаратного переключения режима приём/передача (RX/TX). Режиму TX соответствует сигнал низкого логического уровня, при этом светится индикатор на плате.

Виртуальный COM-порт создан на основе IP Commucation Device Class. Максимальное количество интерфейсов равно двум. Размер буферов задан равным 64 Bytes.

Программная реализация CAT


Ссылка на репозиторий: https://github.com/dmitrii-rudnev/radio-76-cat

За основу решения была принята система команд для управления популярным трансивером Yaesu FT-817. Описание работы CAT-интерфейса этой радиостанции занимает в руководстве пользователя всего четыре страницы.

Управляющие программы сторонних производителей обычно используют для связи с радиостанцией драйвер OmniRig, созданный канадским радиолюбителем Alex Shovkoplyas (VE3NEA). Описанная реализация CAT-интерфейса использует ограниченный набор команд Yaesu FT-817, поддерживаемый этим драйвером.

При настройке OmniRig для работы с публикуемым решением нужно выбрать в конфигураторе OmniRig тип трансивера FT-817, COM-порт, к которому подключен CAT, и установить скорость порта 9600 бит/с.

Для управления радиостанцией один раз в 200 мс через COM-порт посылается пять байт данных. Пятый байт содержит команду управления, первые четыре байта могут содержать данные или быть пустыми. Радиостанция распознает команду, выполняет её и посылает отклик.

Описанная реализация CAT-интерфейса поддерживает работу в составе радиостанции двух генераторов плавного диапазона VFO A и VFO B. Наличие двух VFO позволяет работать на разнесённых частотах (режим Split), когда приём осуществляется на частоте одного VFO, а передача на частоте другого.

Описанное решение позволяет осуществлять переключение между VFO, перестройку частоты активного VFO управляющей программой, а также производить переключение режимов приём/передача.

Помимо этого, хотя для Радио-76 это и не актуально, оставлена возможность переключения вида модуляции.

Собственно трансивер реализован переменной trx со структурой, приведённой ниже:
typedef enum{  MODE_LSB = 0x00,  MODE_USB = 0x01,  MODE_CW  = 0x02,  //CW-USB  MODE_CWR = 0x03,  //CW-LSB  MODE_AM  = 0x04,  MODE_FM  = 0x08,  MODE_DIG = 0x0A,  //DIG-U  MODE_PKT = 0x0C   //DIG-L} Mode;typedef struct{  Mode mode;     //используемая модуляция из списка  uint64_t vfoa; //частота VFO A в герцах  uint64_t vfob; //частота VFO A в герцах  uint8_t vfo;   //активный VFO: 0, если активен VFO A; 1, если VFO B  uint8_t split; //режим работы на разнесенных частотах: 1, если включен   uint8_t is_tx; //режим передачи: 1, если включен  uint32_t sysclock; //системное время  uint8_t systicks;  //счётчик прерываний SysTick до десяти} TRX_TypeDef;

Системное время используется для отслеживания тайм-аутов. Инкремент trx.sysclock происходит по каждому десятому прерыванию SysTick.

Переключение режима RX/TX осуществляет программный модуль ptt_if.c.

Переключение в режим передача (TX) и возврат в режим приём (RX) производится двумя разными способами:
1. По нажатию (TX) тангенты и её отпусканию (RX) (низкий/высокий уровень на входе PTT).
2. При получении по CAT команды FT817_PTT_ON (0x08) (TX) и получении по CAT команды FT817_PTT_OFF (0x88) (RX).

Следует учесть, что возврат в режим приём возможен только при условии отсутствия всех сигналов переключения в режим передачи.

Обработка состояния телеграфного ключа, а также линий RTS и DTR виртуального COM-порта в публикуемой реализации CAT-интерфейса не предусмотрена.

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

Модуль cat_if.c содержит драйвер CAT-интерфейса.

Обработчик состояния CAT-интерфейса запускается в бесконечном цикле main.c. Обработчик проверяет наличие подключения по виртуальному COM-порту, и если оно есть, то проверяется наличие данных в буфере CAT-интерфейса.

Если данные в буфере есть, обработчик извлекает из буфера пять байт данных и распознаёт команду по списку поддерживаемых. Если команда распознана, запускается обработчик команды, который обращается или к ptt_if.c, или к vfo_if.c. По результатам обработки формируется отклик, который передаётся в компьютер через виртуальный COM-порт.

Наиболее часто трансивер получает две команды: FT817_GET_FREQ (код команды 0x03) и FT817_READ_TX_STATE (0xF7). По ним он возвращает частоту настройки, вид модуляции, текущий режим приём/передача и режим работы на разнесённых частотах.

Виртуальный COM-порт


Виртуальный COM-порт создан на основе IP Commucation Device Class.

Команды CAT передаются из приёмного буфера CDC в приёмный буфер CAT функцией CDC_Receive_FS из состава файла usbd_cdc_if.c, расположенного в папке USB_DEVICE\App.

Отклик на команды передаётся из обработчика команд CAT-интерфейса запуском функции CDC_Transmit_FS.

Для корректной работы COM-порта в его буфер необходимо прописать параметры подключения:
static int8_t CDC_Init_FS(void){  /* USER CODE BEGIN 3 */  USBD_CDC_HandleTypeDef   *hcdc;  USBD_CDC_LineCodingTypeDef line_coding =  {    /* 9600 8n1 */    .bitrate    = 9600U, /* Data terminal rate, in bits per second */    .format     = 0U,    /* Stop bits: 0 - 1 Stop bit */    .paritytype = 0U,    /* Parity:    0 - None */    .datatype   = 8U,    /* Data bits */  };  hcdc = (USBD_CDC_HandleTypeDef*) hUsbDeviceFS.pClassData;  memcpy ((uint8_t*) hcdc, &line_coding, 7U);  /* Set Application Buffers */  USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0);  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS);  return (USBD_OK);  /* USER CODE END 3 */} 

Без этой записи в буфере OmniRig к COM-порту может и не подключиться.

От автора



Данное решение CAT-интерфейса может работать с любыми доступными радиолюбителям синтезаторами частоты и контроллерами дисплеев с минимальными переделками main.c и vfo_if.c.

Мне будет очень приятно, если эта публикация поможет кому-нибудь реализовать управление по CAT своим радиоприёмником или радиостанцией.

Подробнее..

.NET nanoFramework платформа для разработки приложений на C для микроконтроллеров

25.03.2021 22:22:00 | Автор: admin
nanoframework

.NET nanoFramework это бесплатная платформа с открытым исходным кодом, основанная на .NET и предназначена для малых встраиваемых устройств, микроконтроллеров. С ее помощью можно разрабатывать различные устройства для Интернета вещей, носимые устройства, научные приборы, робототехнические устройства, можно создавать прототипы и даже использовать на промышленном оборудование.

.NET nanoFramework является малой версией большого .NET Framework предназначенного для настольных систем. Разработка приложений ведется на языке C# в среде разработки Visual Studio. Сама платформа является исполнительной средой .NET кода, это позволяет абстрагироваться от аппаратного обеспечения и дает возможность переносить программный код с одного микроконтроллера на другой, который тоже поддерживает .NET nanoFramework. Программный код на C# для настольных систем, без изменений или с небольшой адаптацией (необходимо помнить про малый объем оперативной памяти) исполнится на микроконтроллере. Благодаря этому, разработчики на .NET с минимальными знаниями в области микроэлектроники смогут разрабатывать различные устройства на .NET nanoFramework.

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

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

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

Особенности .NET nanoFramework:

  • Может работать на 32- и 64-разрядных микроконтроллерах ARM, с наличием всего 256 КБ флэш-памяти и 64 КБ ОЗУ.
  • Работает нативно на чипе, в настоящее время поддерживаются устройства ARM Cortex-M и ESP32.
  • Поддерживает самые распространенные интерфейсы такие как :GPIO, UART, SPI, I2C, USB, networking.
  • Обеспечивает встроенную поддержку многопоточности.
  • Включает функции управления электропитанием для обеспечения энергоэффективности, например, устройств работающих от аккумуляторных батарей.
  • Поддерживает совмещение управляемого кода на C# и неуправляемого кода на C/C++ в одном проекте.
  • Автоматическая сборка мусора благодаря сборщику мусора.

В сравнение с другими платформами:

  • Доступен интерактивный отладчик при запуске кода на самом устройстве с точками останова.
  • Есть развитая и бесплатная среда программирования с Microsoft Visual Studio.
  • Поддержка большого количества недорогих плат от различных производителей, включая: платы Discovery и Nucleo от ST Microelectronics, Quail от Mikrobus, Netduino от Wilderness Labs, ESP32 DevKit C, Texas Instruments CC3220 Launchpad, CC1352 Launchpad и NXP MIMXRT1060-EVK.
  • Легко переносится на другие аппаратные платформы и устройства на ОС RTOS .В настоящее время совместимость обеспечивается в соответствие с CMSIS и ESP32 FreeRTOS.
  • Полностью бесплатное решение с открытым исходным кодом, никаких скрытых лицензионных отчислений. От основных компонентов до утилит, используемых для создания, развертывания, отладки и компонентов IDE.


Предыстория


Вначале было Слово и было это Слово .NET Micro Framework от компании Micrsoft. До появления .NET nanoFramework, в Microsoft любительский проект перерос в серьезную платформу .NET Micro Framework, которая быстро завоевала популярность на американском рынке. Такая компания GHI Electronics с 2008 года, построила весь свой бизнес на разработке микроконтроллеров и решений на базе .NET Micro Framework. В портфолио GHI Electronics были небольшие микроконтроллеры в стиле Arduino FEZ Domino и весьма производительные с несколькими мегабайтами ОЗУ (для микроконтроллеров это весьма круто).

Микроконтроллеры компании GHI Electronics

GHI Electronics Modules

Устройства могли работать практически с любой периферией, была поддержка стека TCP/IP, WiFI, обновления по воздуху. Была даже ограниченная реализация СУБД SQLite, поддержка USB Host и USB Client. Не трудно понять что компания быстро смогла себе сделать имя, стать основным разработчикам решений на .NET Micro Framework, продукцию которой постановляют во все страны мира.

В 2014 г. в проекте Школьный звонок на .NET Micro Framework с удаленным управлением мною использовалась плата FEZ Domino от GHI Electronics. Так же было и множество других проектов таких как Netduino.

FEZ Domino

FEZ Domino

В октябре 2015 года на GitHub был опубликован релиз .NET Micro Framework v4.4, этот релиз оказался последним. Компании Micrsoft отказалась дальше развивать платформу, с этого момента начинает свою историю проект nanoFramework (с 2017 года), в основе которого кодовая база .NET Micro Framework. Многие основные библиотеки были полностью переписаны, некоторые перенесены как есть, было проведено множество чисток и улучшений кода. Энтузиасты встраиваемых систем, гики увлеченные программированием, возродили платформу!

Работа над платформой ведется в основном в свободное время. Некоторые из основных участников работают в компаниях, которые активно спонсируют .NET nanoFramework и предлагают свои рабочие часы для продвижения проекта. Если вы используете .NET nanoFramework как часть своего продукта или просто хотите его поддержать, можете сделать пожертвование. Это позволяет оплачивать затраты на инфраструктуру и уделять больше времени на реализацию проекта.


Архитектура платформы


Платформа включает в себя уменьшенную версию .NET Common Language Runtime (CLR) и подмножество библиотек базовых классов .NET вместе с наиболее распространенными API-интерфейсами, включенными в универсальную платформу Windows (UWP). В текущей реализации, .NET nanoFramework работает поверх ChibiOS которая поддерживается, некоторыми платами ST Microelectronics, Espressif ESP32, Texas InstrumentsCC3220 Launchpad,CC1352 Launchpad и NXP MIMXRT1060-EVK. Разработка ведется в Microsoft Visual Studio или Visual Studio Code, отладка производится непосредственно на устройстве в интерактивном режиме.

Общая архитектура


Для того чтобы эффективно писать код на C # на микроконтроллере, необходимо нужно понимать (на высоком уровне), как это работает. Весь код в микроконтроллере делится на 4 логических компонента, как показано ниже:

nanoframework architecture

На самом высоком уровне находится ваше приложение на C #, которое необходимо запускать на MCU. Ниже располагается уровень CLR, исполнительная среда для нашей программы. Загрузчик базовый компонент любого микроконтроллера, запускает среду CLR при включении MCU. Наконец, на самом низком уровне у нас есть MCU.

Для использования .NET nanoFramework необходимо загрузить среду nanoCLR на микроконтроллер. После этого приложение на C#, скомпилированное в виде бинарного файла, можно загружать на микроконтроллер.

Схема архитектуры .NET nanoFramework


nanoframework architecture

nanoCLR базируется на слое аппаратной абстракции (HAL). HAL предоставляет абстракцию устройств и стандартизует методы и функции работы с устройствами. Это позволяет использовать наборы функций которые одинаковы доступны на уровне абстракции платформы (PAL) и конкретных драйверов. Среда nanoCLR построена на PAL и содержит некоторые ключевые библиотеки, такие как mscorlib (System и несколько других пространств имен), которые всегда используются. Модульность .NET nanoFramework позволяет добавлять пространства имен (namespaces) и классы, связанные с nanoCLR.


ChibiOS


ChibiOS/RT компактная многозадачная операционная система реального времени (ОСРВ). Предназначена для встраиваемых приложений, работающих в реальном времени. Эта ОСРВ отличается высокой мобильностью, компактными размерами и, главным образом имеет свою собственную уникальную архитектуру, которая как никак подходит для быстрого и эффективного переключения контекста.

Основные характеристики:

  • Эффективное и портативное ядро.
  • Лучшая в своем классе реализация переключения контекста.
  • Множество поддерживаемых платформ.
  • Статичная архитектура все статически выделяется во время компиляции.
  • Динамические расширения динамические объекты поддерживаются как дополнительный слой надстройки статичного ядра.
  • Богатый набор примитивов для ОСРВ: потоки (threads), виртуальные таймера (virtual timers), семафоры (semaphores), мьютексы (mutexes), переменные условия/синхронизации (condition variables), сообщения (messages), очереди (queues), флаги событий (event flags) и почтовый ящик (mailboxes).
  • Поддержка алгоритма наследования для мьютексов.
  • HAL-компонент поддержки различных абстрактных драйверов устройств: порт, последовательный порт, ADC, CAN, I2C, MAC, MMC, PWM, SPI, UART, USB, USB-CDC.
  • Поддержка внешних компонентов uIP, lwIP, FatFs.
  • Инструментарий для отладки ОСРВ

Поддерживаемые платформы:

  • ARM7, ARM9
  • Cortex-M0, -M0+, -M3, -M4, -M7
  • PPC e200zX
  • STM8
  • MSP430
  • AVR
  • x86
  • PIC32

Области применения ChibiOs/RT:

  • Автомобильная электроника.
  • Робототехника и промышленная автоматика.
  • Бытовая электроника.
  • Системы управления электроэнергией.
  • DIY.

ChibiOS/RT также был портирована на Raspberry Pi, и были реализованы следующие драйверы устройств: порт (GPIO), Seral, GPT (универсальный таймер), I2C, SPI и PWM.


Поддерживаемые устройства


Поддерживаемые устройства делятся на две категории: основные платы и поддерживаемые сообществом.

Основные платы:

  • ESP32 WROOM-32, ESP32 WROOM-32D, ESP32 WROOM-32U, ESP32 SOLO-1
  • ESP-WROVER-KIT, ESP32 WROVER-B, ESP32 WROVER-IB
  • STM32NUCLEO-F091RC
  • STM32F429IDISCOVERY
  • STM32F769IDISCOVERY
  • OrgPal PalThree
  • CC1352R1_LAUNCHXL
  • CC3220SF_LAUNCHXL
  • MIMXRT1060 Evalboard
  • Netduino N3 WiFi

Платы поддерживаемые сообществом:

  • ESP32 ULX3S
  • STM32NUCLEO144-F746ZG
  • STM32F4DISCOVERY
  • TI CC1352P1 LAUNCHXL
  • GHI FEZ cerb40 nf
  • I2M Electron nf
  • I2M Oxygen
  • ST Nucleo 144 f439zi
  • ST Nucleo 64 f401re/f411re nf
  • STM NUCLEO144 F439ZI board
  • QUAIL


Пример программы


Примеры кода представлены в разделе nanoframework/Samples. Рассмотрим базовый пример, Blinky пример программы позволяющей мигать встроенным светодиодом на плате ESP32-WROOM:

using System.Device.Gpio;using System;using System.Threading;namespace Blinky{public class Program    {        private static GpioController s_GpioController;        public static void Main()        {            s_GpioController = new GpioController();            // ESP32 DevKit: 4 is a valid GPIO pin in, some boards like Xiuxin ESP32 may require GPIO Pin 2 instead.            GpioPin led = s_GpioController.OpenPin(4,PinMode.Output);            led.Write(PinValue.Low);            while (true)            {                led.Toggle();                Thread.Sleep(125);                led.Toggle();                Thread.Sleep(125);                led.Toggle();                Thread.Sleep(125);                led.Toggle();                Thread.Sleep(525);            }        }            }}

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


Что сейчас доступно из коробки?


Аппаратные интерфейсы:

  • Windows.Devices.WiFi работа с Wi-Fi сетью.
  • nanoFramework.Devices.Can работа с CAN шиной. CAN (Controller Area Network) стандарт промышленной сети, ориентированный, прежде всего, на объединение в единую сеть различных исполнительных устройств и датчиков, заменяет устаревшую шину RS 485. Используется в АСУ ТП, на заводах и фабриках.
  • 1-Wire 1-Wire интерфейс, используется для подключения одного/нескольких температурных датчиков DS18B20.
  • Windows.Devices.I2c, System.Device.I2c I2C шина для подключения нескольких устройств.
  • Windows.Devices.Spi SPI шина.
  • Windows.Devices.Adc аналого-цифровой преобразователь (АЦП).
  • Windows.Devices.Pwm широтно-импульсная модуляция (ШИМ).
  • System.Devices.Dac - цифро-аналоговый преобразователь (ЦАП).

Классы:

  • Windows.Devices.SerialCommunication работа с последовательным интерфейсом.
  • MQTT MQTT клиент, порт популярной библиотеки M2Mqtt. Библиотека предназначена для отправки коротких сообщений, используется для Интернета вещей и M2M взаимодействия.
  • System.Net.Http.Server и System.Net.Http.Client готовые классы Web-сервера и Web-клиента с поддержкой TLS/SSL.
  • Json работа с данными в формате Json.
  • nanoFramework.Graphics библиотека работы с отображения графических примитивов на LCD-TFT дисплеях.
  • System.Collections коллекции объектов.
  • Discord bot реализация Discord бота.
  • Json Serializer and Deserializer Json сериализацияя/десериализация.

Сетевые протоколы:

  • AMQP.Net Lite Облегченная версия открытого протокола AMQP 1.0 для передачи сообщений между компонентами системы. Основная идея заключается в том, что отдельные подсистемы (или независимые приложения) могут обмениваться произвольным образом сообщениями через AMQP-брокера, который осуществляет маршрутизацию, гарантирует доставку, распределяет потоки данных, предоставляет подписку на нужные типы сообщений. Используется в инфраструктуре Azure, поддерживает шифрование TLS.
  • SNTP протокол синхронизации времени по компьютерной сети.


Библиотеки классов


В таблице представлена общая организация библиотек классов .NET nanoFramework. Приведенные ниже примеры относятся к ChibiOS (которая в настоящее время является эталонной реализацией .NET nanoFramework):

Библиотека класса Название Nuget пакета
Base Class Library (also know as mscorlib) nanoFramework.CoreLibrary
nanoFramework.Hardware.Esp32 nanoFramework.Hardware.Esp32
nanoFramework.Runtime.Events nanoFramework.Runtime.Events
nanoFramework.Runtime.Native nanoFramework.Runtime.Native
nanoFramework.Runtime.Sntp nanoFramework.Runtime.Sntp
Windows.Devices.Adc nanoFramework.Windows.Devices.Adc
Windows.Devices.I2c nanoFramework.Windows.Devices.I2c
Windows.Device.Gpio nanoFramework.Windows.Devices.Gpio
Windows.Devices.Pwm nanoFramework.Windows.Devices.Pwm
Windows.Devices.SerialCommunication nanoFramework.Windows.Devices.SerialCommunication
Windows.Devices.Spi nanoFramework.Windows.Devices.Spi
Windows.Devices.WiFi nanoFramework.Windows.Devices.WiFi
Windows.Networking.Sockets nanoFramework.Windows.Networking.Sockets
Windows.Storage nanoFramework.Windows.Storage
Windows.Storage.Streams nanoFramework.Windows.Storage.Streams
System.Net nanoFramework.Windows.System.Net

Все дополнительные пакеты добавляются с помощью системы Nuget, как это принято в .NET Core.


Unit-тестирование


nanoframework unit test architecture

Тестирование настольного приложения на рабочей станции не вызывает никаких проблем, но все обстоит иначе, если необходимо тестировать приложение для микроконтроллера. Исполнительная среда должна быть эквивалентна по характеристикам микроконтроллеру. В Unit тестирование кода на C#, используется концепция Адаптера (Adapter). Для тестовой платформы Visual Studio (vstest) был разработан специальный компонент nanoFramework.TestAdapter. В нем реализовано, два интерфейса для детального описания конфигурации и третий для описания специфических параметров, таких как time out, в зависимости от целевой среды исполнения, на реальном оборудование или в Win32 nanoCLR. Механизм проведения тестов на Win32 nanoCLR и реальном оборудовании, одинаков. Единственное отличие это консоль вывода, которая в случае реального оборудования отправляет данные в порт отладки.

Один из интерфейсов называется ITestDiscoverer, который используется Visual Studio для сбора возможных тестов. vstest вызовет адаптер для любой запущенной сборки и передаст бинарный файл dll или exe, соответствующую определенному условию сборки (пока у нас нет TFM, и мы используем небольшой хак и основной .NET Framework 4.0 ). Затем nanoFramework TestAdapter анализирует каталоги, чтобы найти nfproj, анализируя файлы cs, глядя на определенные атрибуты Test, определенные для nanoFramework. На основе этого составляется список, который передается обратно.

Этот хак выполняется с помощью файла с расширением .runsettings с необходимым минимумов элементов ( для запуска приложения в Win32 nanoCLR, параметр IsRealHardware необходимо выставить в false, в случае реального устройства true):

nanoframework unit test

Когда выполняется сборка проекта отрабатывает триггер интерфейс ITestExecutor. В случае, если вы работаете в контексте Visual Studio, передается список тестов (индивидуальный или полный список), и именно здесь запускается nanoFramework.nanoCLR.Win32.exe как процесс, передающий nanoFramework.UnitTestLauncher.pe, mscorlib.pe, nanoFramework.TestFramework.pe и, конечно же, тестовую библиотеку для исполняемого файла.

nanoFramework Unit Test загрузит тестовую сборку и выполнит тесты с начиная с первого Setup, затем TestMethod и, наконец, тесты Cleanup.

Выходные данные этого процесса перенаправляются в TestAdapter, которые затем разбираются. Важное примечание: UnitTestLauncher необходимо собрать и развернуть при отладке! В противном случае он никогда не сможет ничего вывести в консоль. Чтобы запустить тест на реальном оборудование, нужно просто добавить строку ввода в файл runsettings, как указано выше.

По окончании выполнения тестов, возвращается статусы. Простой строковый вывод со статусом теста, имени метода и времени его выполнения и/или исключение. Тест пройден: MethodName, 1234 или Test failed: MethodName, подробное исключение. Это передается обратно в vstest, а затем отображается в Visual Studio.

Для Unit-тестирования необходимо в проект добавить NuGet пакет nanoFramework.TestFramework. Или использовать готовый проект для Unit-тестирования в Visual Studio, это самый простой способ! Он автоматически добавит в проект NuGet и .runsettings.


Лицензирование


Весь исходный код .NET nanoFramework распространяется по лицензией MIT, включая nf-interpreter, классы библиотек, расширение Visual Studio и все сопутствующие утилиты.

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

Для коммерческого использования ChibiOS, необходимо приобретать лицензию. Так же можно лицензировать отдельные компоненты.

Но если количество выпущенных коммерческих устройств с ChibiOS не превышает 500 ядер, то оплачивать лицензию не требуется. Свыше этого объема приобретаются пакеты лицензий на 500, 1000, 5000 ядер или неограниченно. При бесплатном коммерческом использование некоторые функции недоступны и в своем продукте необходимо реализовать рекламу ChibiOS.

Управляемые приложения managed apps (C#) запущенные на .NET nanoFramework не компилируются и не собираются ChibiOS, а интерпретируются на лету. Поэтому рассматриваются как отдельный компонент от встроенного ПО. Таким образом, схема лицензирования ChibiOS не распространяется на приложения C#, и не зависит от условий лицензирования ChibiOS.


Использование в промышленном сфере


Американская компания OrgPal.Iot специализируется на аппаратных и программных решениях для Интернета вещей (IOT), которые осуществляют сбор данных телеметрии для отдаленнейшего анализа, управление инфраструктурой через частное облако. Компания предоставляет конечные устройства и шлюзы для передачи данных на рабочие станции, сервера и мобильные устройства. Решения совместимы с Azure IoT.

Основные направление компании это мониторинг инфраструктуры:

  • Промышленного производства
  • Нефтяных месторождений
  • Панелей солнечных электростанций
  • Систем управления электропитанием

Одно из разработанных устройств компании это PalThree. PalThree сертифицированное устройство Azure IoT Ready Gateway & Edge Point для сбора данных и телеметрии с последующей передачей в облако Azure IoT. На борту большой набор входных интерфейсов для получения данных о технологических процессах. Устройство основано на STM32 ARM 4 и ARM 7, поставляется в двух вариантах с микроконтроллерами STM32F469x и STM32F769x, с 1 МБ SDRAM на плате, флэш-памятью SPI и QSPI. Программное обеспечение основано на .NET nanoFramework, с ChibiOS для STM32.

Как это работает

nanoframework palthree sensors cloud

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

Цистерны для хранения нефтегазовых продуктов
nanoframework oil tank palthree

Шкаф с PalThree
nanoframework palthree close up


Web-сервер с поддержкой: REST api, многопоточности, параметров в URL запросе, статических файлов.


Специально для .NET nanoFramework, Laurent Ellerbach разработал библиотеку nanoFramework.WebServer, которая по своей сути является упрощенной версией ASP.NET.

Возможности Web-сервера:

  • Обработка многопоточных запросов
  • Хранение статических файлов на любом носителе
  • Обработка параметров в URL запросе
  • Возможность одновременной работы нескольких веб-серверов
  • Поддержка команд GET/PUT и любых другие
  • Поддержка любого типа заголовка http-запроса
  • Поддерживает контент в POST запросе
  • Доступно использование контроллеров и маршрутизации
  • Хелперы для возврата кода ошибок, для REST API
  • Поддержка HTTPS
  • Декодирование/кодирование URL

Ограничение: не поддерживает компрессию ZIP в запросе и ответе.

Для использования Web-сервера необходимо просто указать порт и добавить обработчик запросов:

using (WebServer server = new WebServer(80, HttpProtocol.Http){    // Add a handler for commands that are received by the server.    server.CommandReceived += ServerCommandReceived;    // Start the server.    server.Start();    Thread.Sleep(Timeout.Infinite);}

Так же, можно передать контроллер, например ControllerTest, и будет использоваться декоратор для маршрутизации и методов:

using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest) })){    // Start the server.    server.Start();    Thread.Sleep(Timeout.Infinite);}

В следующем примере, определяется маршрут test и т.д., в котором определяется метод GET, и test/any:

public class ControllerTest{    [Route("test"), Route("Test2"), Route("tEst42"), Route("TEST")]    [CaseSensitive]    [Method("GET")]    public void RoutePostTest(WebServerEventArgs e)    {        string route = $"The route asked is {e.Context.Request.RawUrl.TrimStart('/').Split('/')[0]}";        e.Context.Response.ContentType = "text/plain";        WebServer.OutPutStream(e.Context.Response, route);    }    [Route("test/any")]    public void RouteAnyTest(WebServerEventArgs e)    {        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);    }}

Функция RoutePostTest будет вызываться каждый раз, когда вызываемый URL-адрес будет test, Test2, tEst42 или TEST, URL-адрес может быть с параметрами и методом GET. RouteAnyTest вызывается всякий раз, когда URL-адрес является test/any, независимо от метода. По умолчанию маршруты не чувствительны к регистру, и атрибут должен быть в нижнем регистре.

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

Cooming Soon


В продолжение к публикации, будет практическая работа с загрузкой CLR на ESP32 и Nucleo, с написанием первой программы на C#.

Страница проекта .NET nanoFramework
Подробнее..

Как мы верифицированный полетный контроллер для квадрокоптера написали. На Ada

30.03.2021 12:10:29 | Автор: admin

Однажды на новогодних каникулах, лениво листая интернет, бракоделы в нашем* R&D офисе заметили видео с испытаний прототипа роботакси. Комментатор отзывался восторженным тоном революция, как-никак. Здорово, да, но время сейчас такое кругом революции, и ИТ их возглавляет.

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

Перед глазами побежали флешбеки, где-то из глубин подсознания всплыла забытая уже информация о прошивках для Тойоты на миллионы тысяч строк Си и 2 тысячи глобальных переменных (Toyota: 81564 ошибки в коде).

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

Но ведь можно иначе? Можно, решили мы!

И решили это доказать. На Avito был куплен акробатический FPV-квадрик на базе STM32F405, для отладки - Discovery-плата для этого же контроллера, а дальше все как в тумане..

Так как же быть иначе?

После быстрого совещания возникли вот такие мысли:

  • нам нужен другой подход

  • язык и подход должны друг друга дополнять

  • академический подход не подойдет, нужны практические применения.

В качестве нового подхода решили, что лучше всего опираться на возможность возможность верификации ПО - до необходимого уровня, без злоупотреблений. Но для языка типа С доступных промышленных зрелых решений не существует, только прототипы [FC] и рекомендации.

При выборе языка мы поставили себе вот какими требования:

  • это должно быть что-то близкое к embedded

  • Нам нужен хороший богатый runtime с возможностями RTOS, но при этом брать и интегрировать RTOS не хочется

  • Он не должен заметно уступать в производительности тому, что используется сейчас.

Оказалось, что из практических инструментов в эти требования хорошо подходит один очень старый, незаслуженно забытый инструмент. Да, это Ada. А точнее, его модерновое, регулярно обновляемое ядро SPARK. В [SRM] описаны основные отличия SPARK от Ada, их не так много.

Что такое SPARK, будет ясно дальше, мы покажем, как именно оно было применено, почему Ада понравилась больше, чем С, как работает прувер, и почему мы при этом ничего не потеряли, а только приобрели. И почему мы не взяли Rust :)

Иной подход

Иной подход это верификация ПО. Обычно при этих словах люди начинают думать об абстрактных монадах, академический манускриптах, докладах на конференциях и пыльных трудах института системного программирования РАН, которые как будто бы от жизни отстают лет на 30, а то и 50. Но оказалось, что не все так плохо.

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

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

  • отсутствия переполнения массивов и переменных

  • отсутствия выхода за границы в типах и диапазонах

  • отсутствия разыменования null-указателей

  • отсутствие выброса исключений.

  • гарантию неприменения инструментов, проверку которых выполнить нельзя.

  • гарантию выполнения всех инвариантов, которые мы опишем. А опишем мы много!

    Круто, да?

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

SPARK также учитывает ограничения на типы, которые описаны в Ada. В случае обычного исполнения ошибка несоответствия типов упадет в Runtime; SPARK же позволяет статически доказать, что ограничения на типы не могут быть нарушены никаким потоком исполнения.

Например:

Или другой пример:

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

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

Сам SPARK делит верификацию на уровни: от "каменного" (Stone level) через "Бронзовый" и "Серебряный" уровни до "Золотого" (Gold) и "Платинового". Каждый из уровней усиливает гарантии:

Stone

Мы в принципе знаем, что есть SPARK

Bronze

Stone + верификация потоков исполнения и детерминизм/отсутствие неинициализированных переменных

Silver

Bronze + доказательное отсутствие runtime-ошибок

Gold

Silver + гарантии целостности - не-нарушения инвариантов локальных и глобальных состояний

Platinum

Gold + гарантия функциональной целостности

Мы остановились на уровне Gold, потому что наш квадрокоптер все-таки не Boing 777 MAX.

Как работает верификация в SPARK: прувер собирает описание контрактов и типов, на их основе генерирует правила и ограничения, и далее передает их в солвер (SMT - Z3), который проверяет выполнимость ограничений. Результат решения прувер привязывает к конкретным строкам, в которых возникает невыполнимость.

Более подробно можно почитать в [SUG]

Иной язык

Несмотря на то, что сейчас "рулят" си-подобные ECMA-языки, мы нормально отнеслись к тому, что от этого придется отказаться. Более того, кажется, что чем больше программа, тем больше вредит укорочение ключевых слов и конструкций. Что касается Rust, то он - субъективно - в отношении минимализма издалека сильно напоминает Perl, к сожалению.

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

Профили

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

Мы используем профиль Ravenscar, специально для embedded-разработки. Он включает пару дюжин ограничений, которые делают вашу разработку для микроконтроллеров более удобной и верифицируемой: нельзя на ходу переназначать приоритеты задач-потоков, переключать обработчики прерываний, сложные объекты из stdlib-ы и такое.

Вот ограничения профиля Ravescar, для примера

Runtime

Когда в embedded необходимо создать более-менее сложное приложение, там всегда заводится RTOS, и ее выбор и интеграция это отдельная песня. В случае с Ada с этим больше нет сложностей - сама Ada включает в себя минимальную исполняемую среду с вытесняющим планировщиком потоков (в Ada это tasks, задачи), с интегрированными в сам язык средствами синхронизации (семафоры, рандеву, называются entries) и даже средствами избегания дедлоков и инверсии приоритетов. Это оказалось очень удобно для квадрокоптера, как станет понятно ниже.

Для embedded-разработчика доступны на выбор также разные рантаймы:

  • zero-footprint - с минимальными ограничениями и даже без многопоточности; зато минимальная программа не превышает пары килобайт, влезает даже в TO MSP430

  • small footprint - доступна большая часть функций Ada, но и требования побольше, несколько десятков килобайт RAM

  • full ravenscar - доступны все функции в рамках профиля Ravenscar/Extended Ravenscar

Вот пример описания пустой задачи

Хочется обратить внимание, что эти задачи - это по сути легковесные green threads, так как под ними нет никаких настоящих потоков не существует. Поэтому мы не страдаем от отсутствия корутин, ведь задачи не тяжелее них, зато встроены в язык.

Кроме этого, в Ada есть достаточно мощная stdlib для ядра STM32, включая полную реализацию рантайма. Возможно, и для вашей архитектуры она тоже есть.

Почему не rustRustRUST!

Когда мы говорим, про гарантии в языках программирования, сразу вспоминается Rust и его гарантии относительно указателей. Почему тут не он? Нам кажется, что Spark мощнее.

Ada не очень любит указатели - там они называются access types, и в большинстве случаев там они не нужны, но если нужны, то - в Spark также есть проверки владения, как в Rust. Если вы аллоцировали объект по указателю, то простое копирование указателя означает передачу владения (которую проконтролирует компилятор/верификатор), а передачу во временное владение (или доступ на чтение) верификатор также понимает и контролирует.

В общем, концепция владения объектом по указателю, и уровень доступа через этот указатель - есть не только в Rust, и его преимуществами можно пользоваться и в других инструментах, в частности, в Ada/SPARK. Подробно можно почитать в [UPS]

Вот пример кода с владением

Почему мы пишем, что в Ada/SPARK не нужны указатели? В отличие от Си, где базовым инструментом является указатель (хочешь ссылку - вот указатель, хочешь адрес в памяти - вот указатель, хочешь массив - вот указатель - ну вы поняли?), в Ada для всего этого есть строгий тип. Если не хватает встроенных операций, их допустимо переопределять (например, реализовать инлайновый автоинкремент), аналогично можно создать placement constructor, используя т.н. limited-типы - типы, которые компилятор запрещает копировать.

Если уже и этого мало, есть интероп с СИ то есть код можно компилировать совместно, и слинковать на этапе сборки. Но в этом случае гарантии поведения модуля как черного ящика остаются на разработчике. Для интеропа используются атрибуты - вот так, например, мы оборачиваем функцию на Си в доступ для Ada.

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

IDE

Для работы доступна вполне приятная и удобная IDE, но всегда можно использовать и VSCode с плагинами, и другие текстовые редакторы.

О производительности и надежности

Вполне валидным аргументом может быть вопрос с эффективностью ПО. Что касается эффективности, то в интернете доступно свежее исследование [EFF], из которого хочется привести табличку, показывающую, что старичок Ada еще огого:

Если говорить о надежности, то SPARK/Ada известен как один из языков с наименьшим количеством ошибок. В планируемом на 21 запуске кубсатов [LIC] полетное ПО планируется реализовывать на Ada, предыдущий спутник BasiLEO тоже на Ada был единственным среди 12, кому удалось приступить к планируемой миссии.

А теперь - о самом полетном контроллере

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

Структурная схема управляющего ПО показана на рисунке

Как видно из рисунка, ПО состоит из двух частей:

  • Veriflight - собственно, верифицированный полетный контроллер с алгоритмами.

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

Так как тратить много времени не хотелось, то драйвер для USB в STM32 был взят прямо нативный и при помощи Interop был слинкован с оберткой на Ada.

Плата оказалась оснащена минимальным количеством периферийных устройств:

  • STM32F405 микроконтроллер на 168 МГц (192кб RAM, 1Mб flash)

  • трансивером S.BUS на USART1

  • 6-осевым гиро-акселерометром без магнитного компаса

  • токовыми усилителями PWM

  • USB-интерфейсом, PHY-часть которого реализована на самом микроконтроллере платы.

Полетный контроллер реализован по простой схеме и крутит 2 цикла:

  1. внешний

  2. внутренний

Внешний цикл это цикл опроса периферии (CMD task на рисунке) в ожидании команд с радиопередатчика. Если команды нет, он передает признаки сохраняем высоту, держим горизонт. Если команда с пульта есть, передаем ее - целевой угол наклона, целевую мощность на пропеллеры. Частота внешнего цикла 20 Гц.

Внутренний цикл - цикл опроса гиро-акселерометра и распределения мощности на двигатели. Цикл оборудован 3 PID-регуляторами, и математикой Махони для расчета текущего положения по сигналам с гироскопов. В расчетах внутри используем кватернионы, для генерации управляющего сигнала - углы Эйлера. Частота размыкания внутреннего цикла - 200 Гц. Да, Ада без проблем успевает диспетчеризировать с такой скоростью.

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

Внутренний цикл реализует опрос PID и стабилизацию аппарата:

  • Считали затребованные пилотом углы

  • Запросили у математики расчетные углы положения

  • Нашли расхождение между желаемыми и настоящими

  • Пересчитали текущее положение на основании сигналов с гиро-акселерометров

  • Зарядили PID-регуляторы на новую коррекцию, если пришли новые затребованные углы

  • Запросили у PID-пакетов текущие импульсы коррекции

  • На основании них, а также запрошенной пилотом мощности на двигатели, сформировали необходимое распределение скоростей вращения на двигателях

Забавно, что большинство опен-сорсных реализаций Махони (для Arduino и не только) - на Cи и Wiring оказались содержащими разнообразные баги. Это мешало системе заработать. После того, как было выпито пол-ящика лимонада и съедена корзина круассанов, алгоритм воссоздали с нуля по описанию из [MHN], и система тут же заработала.

Данная схема, как и любая упрощенная модель, испытывает сложности при приближениях параметров к предельным. Здесь это 90 по крену и тангажу - при их превышении для безопасности реализовано отключение двигателей (disarm).

Кроме этого, управление по углу рыскания выполнено сквозным, а PID там используется только для контроля сильных отклонений между выданным и расчетным углами. Это связано с тем, что пилот по скорости, крену и тангажу ожидает реакцию коптера в виде наклона, пропорционального отклонению стиков, а по рысканию -- в виде скорости вращения, пропорциональной отклонению.

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

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

Итог на текущий момент

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

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

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

Для себя мы сделали вывод, что для embedded будем стараться писать только на Ada.

Если вы также считаете, что современная robotics и automotive это слишком важные вещи, чтобы позволить себе переполнения буфера и разыменование нуля и ой программисты не написали тесты, пишите, комментируйте, присоединяйтесь: пора сделать ПО надёжнее, потому что оно вокруг нас везде.

Литература для дальнейшего изучения

[SUG] SPARK user guide https://docs.adacore.com/spark2014-docs/html/ug/index.html

[SRM] SPARK reference manual (https://docs.adacore.com/live/wave/spark2014/html/spark2014_rm/index.html)

[FC] Frama-C - платформа для модульного анализа кода С https://frama-c.com/

[UPS] https://blog.adacore.com/using-pointers-in-spark

[MHN] https://nitinjsanket.github.io/tutorials/attitudeest/mahony

[EFF] https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf

[LIC] https://en.wikipedia.org/wiki/Lunar_IceCube

Подробнее..

100500-ая автоматика полива для растений

04.04.2021 16:09:41 | Автор: admin

Вступление с отступлениями. Задача первой итерации

Долго ли, коротко ли, решено было сделать шайтан-машину для полива растений, которая будет сама выращивать представителей флоры. Кавычки тут подразумеваются уместными в силу, на первый (а может и более) взгляд, необъятности задачи автономности подобных устройств (впрочем, любых роботов, начиная от пылесосов, заканчивая андроидами, которые, как известно, неизвестно, думают ли об электроовцах). Вобщем, для первой итерации было задумано дать растниям воду по расписанию, да не из бака куда её предусмотрительно налил пользователь, а прямо из водопровода (следует заметить, это требование, само по себе оказалось, по сложности реализации, сопоставимым с остальными функциями. Но об этом позже). Чтобы присматривать и корректировать поведение машины, был задуман интерфейс. Сначала локальный (дисплей 16х2, да кнопки), а затем удалённый, в браузере (интернет, локальная сеть).

В этой статье рассмотрена история развития материальной части комплекса - исполнительные устройства, аппаратное обеспечение.

За дело

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

Первый результат

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

пластиковый шаровый клапан для водыпластиковый шаровый клапан для воды

Отложим ардуину

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

STM32

Была приобретена STM32VL-Discovery и уже на ней был запущен тот шаровый клапан. Это был ещё один момент ликования. Работает мотор, щёлкает микрик, мотор останавливается, вода идёт. После годов работы в офисе с монитором, мышкой, да клавиатурой, эти новые звуки мотора, воды были настоящей музыкой.

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

Плата

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

Удалённый интерфейс

Для реализации удалённого управления было решено использовать Raspberry Pi. А связать Pi с STM32 через UART. Писать простенькие сайтики к тому моменту я уже умел, опыт PHP и JS небольшой был.

Задачей посложней оказалось работать с последовательным портом как под Linux, так и на STM32. Под Linux для начала были использованы какие-то стандартные средства (типа, cat /dev/tty > dumpfile и echo -e "data" > /dev/tty), плюс на PHP написан парсер самодельных пакетов, идущих с STM32. Так появился первый протокол устройства. Одновременно с этим я узнал, что PHP годится не только для разовой отрисовки сайтов, но и для работы в бесконечном цикле, в стиле демона. Позже для решения этой задачи был написан демон на C. Разумеется, последний работает на порядки быстрей, прямей и весит меньше.

Поскольку опыт написания сайтов уже какой-то был, смастерить простейший интерфейс для управления железкой на STM32 теперь было не сложней версии с дисплейчиком 16x2. В этом интерфейсе появились кнопки ВКЛ/ВКЛ для ряда твердотельных реле, кнопки Открыть/Закрыть для четырёх клапанов и формочки для четырёх дозаторов удобрений, с длительностью работы оных и кнопкой запуска дозирования. Для визуального контроля результатов работы контроллера была использована веб-камера, подключенная в USB порт Raspberry Pi. Картинки выводились в тот же интерфейс

Дозаторы

Первыми были опробованы самые дешевые перильстатические насосы из Китая. Два вида.

один из типов недорогих дозатороводин из типов недорогих дозаторов

В ходе испытаний они показали себя с не лучшей стороны. Их клинило, дозирование было неравномерным. Следом за ними были опробованы дозаторы японского производителя Welco. Эти устройства оказались значительно более качественными. Настолько, что используются и по сегодняшний день. Меняются только трубки, как расходный материал.

Клапаны

Тот шаровый клапан с мотором-актуатором со временем достаточно плотно сросся с контроллером STM32 и в итоге, вкупе с дальномером HC-SR04 получилось устройство, которое стало исправно поставлять чистую воду в систему полива. Клапан открывал подачу воды из водопровода в фильтр обратного осмоса, наполняя буферный (накопительный) бак. Затем, система полива брала воду уже из накопительного бака.

Тем временем, в деле были опробованы клапаны соленоидного типа. Самые дешёвые, китайские. Такие, которые повсеместно используются в стиральных машинах.

пластиковый соленоидный клапан, белыйпластиковый соленоидный клапан, белый

Главными отличительными особенностями клапанов-соленоидов от шаровых стали более высокое потребление электричества (около 0.4А при 12В, пока открыт) и необходимость создавать на входе повышенное давление воды, чтобы происходило их открывание. Вместе с этими клапанами были найдены более дорогие (тоже соленоидные, тоже пластик, нейлон), не менее китайские, так называемые чёрные клапаны.

черный пластиковый клапанчерный пластиковый клапан

Они потребляли ещё больше тока (до 2А, 12В), но открывались уже без внешнего давления. Чёрные клапаны ставились, к примеру, на входе из накопительного бака в систему полива.

Mixtank. Бак для смешивания растворов

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

Магистраль смешивания

Первые прототипы включали в себя гибкие шланги, выполняющие функции проводников воды. С появлением дозаторов шланги были заменены на жесткие трубки из ПВХ. Так появилась первая зафиксированная сначала на бумаге, а затем и в первом корпусе система трубопроводов. В этой системе трубопроводов одной из частей оказалась магистраль смешивания удобрений. Это был кусок ПВХ трубки диаметром 20мм, в котором были насверлены отверстия около 5мм и с усилием вставлены соединители для ирригационных трубочек 4/6мм. В месте стыка трубы и переходника была применена эпоксидная смола для герметизации стыка.

первая версия корпуса системы поливапервая версия корпуса системы полива

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

Системный насос

Насос, который использовался на начальных стадиях развития системы полива был самый простой, дешёвый, обязательным было только условие вход и выход имеют резьбу в пол-дюйма. Он выдавал в лучшем случае 0.3 атмосферы давления на выходе.

В ходе экспериментов с поливами выяснилось, что подводить трубочки из системы полива и поливать растения таким образом, без обратной связи по влажности почвы получается не очень качественно. Малейшее засорение, изгиб или различная длина трубок от системы полива к растению приводили к неравномерности полива. Один горшок получал больше воды, чем другой, за равное количество времени работы полива. Было найдено решение в виде компенсированных капельниц, которые используются в системах капельного полива. Это такие устройства, которые пропускают через себя фиксированный объём воды, при условии подачи последней под давлением в заданном диапазоне (типичный диапазон рабочих давлений составляет от 1 до 4 атмосфер). Для обеспечения такого давления используются принципиально иные насосы, нежели тот, который имелся. Потому был приобретён мембранный насос высокого давления. Сначала, тоже самый дешёвый, китайский. Он работал от 12В и потреблял до 5 ампер и при работе шумел и вибрировал так, что невольно, система полива, в числе прочих тестов, стала проходить вибрационные тесты, а соседи - тесты терпимости. Тем не менее, тот насос дал требуемое давление и даже с лихвой до 5 атмосфер до срабатывания механической отсечки. Отсечка, к слову, регулируемая винтом, но на тот момент я дополнительно взял датчик давления воды и сделал отсечку программную.

Датчики давления воды

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

датчик давления из нержавейкидатчик давления из нержавейки


После стального был хромированный, с резьбой на четверть дюйма.

хромированный датчик давленияхромированный датчик давления


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

Опять насос

Самый дешевый мембранный насос в плане производительности был слабоват. В виду этого, входы и выходы доступных его вариантов имели максимальный размер резьбы 3/8 дюйма. Из которых, полезное сечение для прохода воды и того меньше. А трубки ПВХ и детали к ним имели диаметр 20мм и резьбы в пол-дюйма. Насос был однозначно слабоват. В результате были найдены варианты насосов покрупней и среди них выбран с минимальными шумами, тоже мембранный. Стоил он уже в несколько раз дороже. Такие используются, к примеру, в водопроводе домиков на колёсах, или в лодках для откачивания морской воды. Сам насос покрупней, потяжелей и, в соответствии с ценой, выглядит тоже внушительно.

мембранный насосмембранный насос


Прокачиваемый поток воды с прежних 4-5 поднялся до 10-12 литров в минуту. И насос, действительно, стал работать гораздо тише, с меньшими вибрациями.

Логика и силовая часть. Разделение

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

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

Покумекав над требованиями, набросал первую версию силового модуля и новый контроллер.
В первой версии силового модуля для управления нагрузками была неудачно выбрана микросхема L293. Неудачной она оказалась потому, что в её составе использованы биполярные ключи. Это даёт немалое собственное потребление (и, соответственно, тепловыделение) микросхемы в моменты работы нагрузок. Радиаторы, установленные на микросхемах работали на пределе. В следующем варианте схемы были выбраны драйверы L6205PD. Они выполнены на полевых транзисторах и грелись уже существенно меньше. При этом, позволяли нагружать на каждый канал значительно больше тока. Кроме того, корпус микросхем с окончанием PD в названии микросхемы имеет хорошее теплоотводящее основание, которое позволяет отводить тепло прямо в плату. В результате, в дизайн платы были заложены приличные площади меди как раз для этой функции. Испытания показали удовлетворительные результаты, без использования дополнительных радиаторов, в условиях пассивного охлаждения. Следует заметить, что крепилась плата управления нагрузками внутри пластикового короба, вместе с основным контроллером и Raspberry Pi. Последняя, справедливости ради, в таких условиях, в жаркие дни, в теплице, перегревалась до состояния зависания, в отличие от остальной электроники.

Поскольку разделение силовой и логической частей делалось ради снижения влияния помех от мощных нагрузок на логику, то здесь была применена гальваническая развязка. Выполнена она была на ADUM1250. Соответственно, на плате силового драйвера был поставлен I2C-декодер (экспандер) MCP23017. Рядом с ADUM разместилась сдвоенная оптопара, которая одним каналом делала декодеру сброс и вторым каналом включала/выключала питание на микросхемы драйверов через мощный полевой транзистор. Для питания MCP23017 изначально использовался MINI360, который впоследствии был заменен на LM317. Схема драйвера может работать начиная с около 10 вольт и выше. Потолок не проверял, но оценочно можно смело утверждать 24В, может 36В (теоретически, это разумный предел для LM317). Для L6205 заявлены вообще 50В. На практике вся система проверялась в работе на 12В.

На 4 микросхемы L6205, установленных на одной плате, получается 16 каналов управления для исполнительны устройств. Модульность позволяет подключать несколько плат. Для этого необходимо задать разные I2C адреса для MCP23017 при помощи трёх резисторов, предусмотренных на плате. Одиночные L6205 каналы можно сдваивать (согласно аппноту), чтобы получить больше пропускной способности. Именно так и были запитаны чёрные клапаны (наиболее прожорливые), на минимальной конфигурации системы полива, где одной платы управления нагрузками хватает в самый раз.

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

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

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

Измерительная техника

Поскольку аппарат сам готовит растворы, то одним из самых важных (после датчика уровня воды) приборов для измерений стал кондуктометр (он же, EC-метр, он же TDS-метр). В список измерительной техники так же попал измеритель кислотности (он же pH-метр), датчик давления воды и термометр. Таким был первоначальный и основной список сенсоров, поддерживаемых логикой основного контроллера.

EC-метр

Не мудрствуя лукаво, был опробован ряд схем, найденных в поисковике. Самая первая страдала недостаточной повторяемостью (половина собранных модулей почему-то не работала). Вторая схема не заработала в железе вообще. Третья заработала, четвертая была дорогая. Выбор пал на третий вариант, которым стал модуль на таймере 555. Суть проста измеряем частоту меандра, которая задаётся парой конденсатор плюс сопротивление. Сопротивление раствор воды с удобрениями. Датчики электропроводности поначалу были сделаны самопальные. Чтобы снизить коррозию электродов, последние были сделаны из позолоченных пинов. Весь датчик был сделан из эпоксидной смолы, что позволило залить сразу в него термодатчик. В роли последнего был выбран DS18B20.

самодельный датчик электропроводностисамодельный датчик электропроводности

Впоследствии эта конструкция заливалась в деталь из ПВХ - переход с трубы 20мм на резьбу пол-дюйма.


Это позволяет вкручивать его в остальную гидросистему аппарата.

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

pH-метр

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

В итоге, была найдена микросхема LMP91200. Этот кандидат требовал минимальной обвязки, в виде того же АЦП и развязки, что и в варианте выше. В качестве ADC был поставлен ADS1110, для развязки - ADUM1250 и всё сразу заработало. Показания с модуля, при свежем сенсоре кислотности обладали завидной стабильностью.

Питание модуля гальванически развязал недорогими (около доллара за штуку) DC-DC преобразователями, типа 0505, на 1 ватт.

Опять EC

Отсутствие развязки по EC модулю на таймере 555 не давали спать спокойно. Кроме того, вода проникала в под эпоксидку и иногда достигала встроенного датчика DS18B20. Это приводило к печальным последствиям в виде ржавчины и почернения проводов датчика температуры. Иногда металл позолоченных пинов съедался вовсе. Помогала их лакировка.

Тем не менее, к тому времени в загашниках уже имелся модуль EVAL-0349.

EVAL-0349 от AnalogEVAL-0349 от Analog

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

В очередной итерации схемы и платы контроллера был заменен блок измерения EC со старого (с таймером 555) на примерно тот, который предлагался в EVAL-0349. Добавлена та же ADUM1250 для изоляции сигнала, 0505 по питанию и показания электропроводности воды вместе с её температурой стали электрически отделены от контроллера. Вместе с этим были испробованы относительно дешевые сенсоры EC из Китая. За два цикла испытаний нареканий не обнаружено.

По ходу дела был также обнаружен весьма экзотический способ измерения солей в воде - индуктивный. Это когда наматывается катушка, изолированная от воды, а в роли сердечника выступает измеряемая вода. Индуктивность получившейся системы, очевидно, зависит от электропроводности воды-сердечника. Таким образом осуществляется измерение. Такие сенсоры обладают повышенной (в сравнении с традиционными, контактными) долговечностью. На али был обнаружен карманный экземпляр за 70 долларов, однако пределы измерений и точность годятся для морской воды, нежели слабых растворов, применяемых в растениеводстве. Мои мимолётные эксперименты намотать катушку на трубу с водой пока не дали положительных результатов.

Влажность субстрата

Скорость, с которой растения потребляют залитый под корень объём жидкости, кроме прочего, зависит от погодных условий (температура, влажность, количество света на листьях). Влажность почвы (или иного субстрата) влияет на состояние корней и, следовательно, также влияет на скорость потребления раствора растением. Перелив плохо, недолив тоже не очень. Чтобы учитывать этот фактор, было решено добавить датчиков. Чтобы минимизировать переделки платы контроллера и оставить её размеры в разумных пределах, было решено использовать беспроводные датчики влажности. Поначалу были интегрированы Bluetooth датчики Xiaomi. Спустя некоторое время по почте пришла ещё пара этих датчиков, с иной прошивкой. Шаманства с версиями прошивок не оставили равнодушным было решено сделать самодельные датчики. В очередной версии платы контроллера был добавлен беспроводной трансивер NRF24.

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

Беспроводные датчики влажности почвы

Подглядев, как китайцы делают за доллар датчики, в которых сенсором является часть платы (Capacitive soil moisture sensor на алиэкспресс), покрытой лаком, сделал аналогичный сенсор. В качестве контроллера был взят уже знакомый STM32, на 20 пинов, только серия уже F0. В качестве измерителя был взят уже знакомый таймер 555. И теперь сенсор стал не сопротивлением (как в EC измерителе), а ёмкостью. На практике изучая вопрос скорости опустошения получившимся датчиком батарейки CR2032, узнал, что есть версия таймера 555, построенная на полевых транзисторах, что означает меньшее энергопотребление (привет L293 и L6205). Называется LMC555.

Помимо этого на плате датчика был добавлен TMP100 I2C термодатчик. Он был запланирован, чтобы узнавать температуру воздуха вокруг датчика. По правде, в STM32 уже встроен термодатчик. Но я решил, что на испытательных образцах, второй датчик лишним не будет. Также добавлен светодиод, для индикации работы и, возможно, определения освещённости датчика.

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

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

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

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

Подробнее..

100500-ая автоматика полива для растений. Часть 2 Сенсоры и электроника

08.04.2021 20:08:42 | Автор: admin

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

Уровень воды

Сначала был дискретный сенсор, сделанный из двух пластин нержавейки.

две пластинки нержавейки ждут касания водыдве пластинки нержавейки ждут касания воды

Через некую (давно было, не сыскать уже исходников) схему сигнал поступал на контроллер. Дискретность его заключалась в том, что он показывал лишь наличие или отсутствие погружения сенсора в воду. Планировалось также мониторить уровень линейно в зависимости от степени погружения пластин в раствор, меняется сопротивление на таком сенсоре. Но данный метод был быстро забракован ввиду накопления осадка на пластинах (даже если не брать в расчёт налёт возникающий на пластинах вследствие электрического осаждения элементов раствора, оставался налёт, возникающий из-за высыхания раствора на воздухе) и, со временем, изменяющегося оттого сопротивления. Калибровка сбивалась.

Далее были опробованы различные ультразвуковые датчики: HY-SRF05, HC-SR04, JSN-SR04T, US-025. Наиболее приемлемыми (по измеряемому диапазону) и стабильными (с точки зрения показаний и выносливости) были были выбраны HC-SR04.

HC-SR04 смотрит на воду в бакеHC-SR04 смотрит на воду в баке

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

поплавок с герконом. Такой без проблем тянет 12В, 0.4Апоплавок с герконом. Такой без проблем тянет 12В, 0.4А


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

Слышал про использование лазерного датчика уровня. В комментариях подсказывают ставится в бак трубка с поплавком и на поплавок наводим лазер. С ним я пока не имел дел. Будет датчик попробую.

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

датчик давления крови. Достаточно чувствительный прибордатчик давления крови. Достаточно чувствительный прибор


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

Третья проблема датчик залип на некотором значении внутри диапазона верх-низ бака. Такое, как правило, происходит по причине попадания воды на сенсор уровня. Для подобного рода ситуаций предусмотрен ещё один таймаут TO2. Это время, подбираемое опытным путём, устанавливает пользователь во время первичной настройки. Подбирается обычно так, чтобы быть чуть больше времени, затрачиваемого системой на заполнение бака с нуля до самого верха. Если этот таймаут превышен, то отключам автоматику долива, поднимая флаг ошибки. Вернуться к работоспособности автоматики можно после сброса пользователем данной ошибки.
Кто-то может удивиться, мол, зачем такие сложности? Если у вас теплица на земле, действительно, может статься так, что часть мер излишняя. Но если под вами есть соседи, то я бы порекомендовал проверить в тестовом прогоне все варианты аварий, приводящих к потопу.

Температура

Тут можно измерять температуры воды, воздуха. Изначально опыты проводились на обычных резистивных датчиках, типа, PT100. Далее были освоены датчики DHT11/22. И ещё позже DS18B20. Резистивным датчикам необходим аналоговый вход на контроллере. По входу на каждый датчик. DHT тоже хотят по входу на каждый, но уже можно использовать цифровые входы (коих, обычно, больше). В DHT плюсом идёт измерение влажности. DS18B20 хорош тем, что на один пин контроллера можно подключить несколько датчиков разом. По-итогу, на сегодня, в самом устройстве ставится один DS18B20, измеряющий температуру воды в районе магистрали смешивания удобрений. Это становится излишним, когда в сенсор EC уже встроен резистивный термодатчик, измерения с которого снимаются AD5934, входящим в состав блока для измерения EC.

DS18B20 подключается с резистором 10к между питанием и линией данных.

EC (ака TDS, ака солемер)

Для измерения количества солей были также перепробованы несколько способов:

1. По этой ссылке была найдена следующая схема

Измеритель EC/TDS на TL074. http://www.octiva.net/projects/ppm/Измеритель EC/TDS на TL074. http://www.octiva.net/projects/ppm/

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

2. Была ещё одна схема на TL074, но сейчас, к сожалению, не вспомню откуда и какая. Учитывая только память о том, что была пробная сборка на макетке, видимо она не заработала.

3. Самой простой и очевидной оказалась схема измерения влажности на таймере 555.

измеритель влажности на таймере 555. http://www.emesystems.com/OLDSITE/OL2mhos.htmизмеритель влажности на таймере 555. http://www.emesystems.com/OLDSITE/OL2mhos.htm

Это натолкнуло меня на изучение данного таймера и в итоге я собрал нечто похожее, что даже заработало. Погрешность была, мягко говоря, так себе. Однако воспроизводимость была 100%. Достаточно долго этот вариант меня вполне устраивал, пока не появилась необходимость сделать более точный. Так появился следующий вариант с применением...

4. AD5934 который водит в состав демонстрационной платы EVAL-0349, от Analog.

демо-плата EVAL-0349 от Analogдемо-плата EVAL-0349 от Analog


Тут уже всё стало более чем серьёзно (на самом деле есть варианты ещё более точные, с более широким диапазоном измерений и более кусачей ценой, но наврядли они пригодятся в растениеводстве). Шутка ли два диапазона измерений (первый: от 25S до 2500S, второй: от 0.2mS до 200mS) с относительной погрешностью измерений 0.5% для первого диапазона и 1% (после программной коррекции. Если без неё, то 3%) для второго. Вообще, AD5934, насколько я сегодня понимаю, был придуман больше для измерения качества проводной сети (типа, проверять затухание сигнала в витой паре). Но CN0349 рассказывает удивительные вещи и про растворы солей. Рекомендую изучить сей circuit note, для общего образования.

схемы EVAL-0349 с сайта analog.comсхемы EVAL-0349 с сайта analog.com


Если вкратце, то работает эта штука следующим образом: есть микросема ADG715, которая делает выбор между одним из двух пределов измерений EC или термодатчиком. Есть сам AD5934, который измеряет сопротивление с сенсора EC или резистивного термодатчика через операционный усилитель AD8606. Всё это дело заведено через изолятор питания ADuM5000 и изолятор данных ADuM1250. Чтобы всё это собрать и запустить на своей плате, пришлось изрядно покурить даташиты по всем этим компонентам. В итоге, когда всё заработало, измерения электропроводности воды стали максимально достоверными за всю историю проекта. Сами сенсоры, после самодельных, были взяты с aliexpress, с сантехнической резьбой пол-дюйма, со встроенным резистивным термодатчиком. Константа сенсора 1.0.

EC сенсор с удлиннителем примеряется в гидросистему аппарата полива растенийEC сенсор с удлиннителем примеряется в гидросистему аппарата полива растений

Для внедрения этого, достаточно длинного сенсора, в уже имеющуюся гидросистему, пришлось сделать небольшой удлинитель.

Сенсоры давления воды

Калибровка стального датчика давленияКалибровка стального датчика давления

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

Все эти сенсоры расчитаны на 5В питание. Выдают, соответственно, напряжение, линейно зависящее от давления, в диапазоне от 0.5В до 4.5В. АЦП контроллера STM32 запитан от 3.3В, следовательно в схему подключения привносится самый простой делитель напряжения резистивный.

Измерение кислотности раствора

Измерение кислотности раствора происходит обычным сенсором pH, которые также продают на aliexpress.

Как уже упоминалось, первый, достаточно добротный вариант удалось собрать на операционном усилителе CA3240. Схема была взята тут.

измеритель pH. http://www.emesystems.com/OLDSITE/OL2ph.htmизмеритель pH. http://www.emesystems.com/OLDSITE/OL2ph.htm

Первая версия, собранная своими руками выглядела как-то так

как ни странно, это заработалокак ни странно, это заработалоэтот вариант стал ещё лучше, уменьшившись в размерах и получив экранэтот вариант стал ещё лучше, уменьшившись в размерах и получив экран

Следующим стал модуль на специализированном решении (LMP91200 от Texas Instruments), заточенном под измерение кислотности стандартными сенсорами pH.

Типичная схема из даташита незамысловата, её и взял.

схема типичного применения LMP91200 из даташита. ti.comсхема типичного применения LMP91200 из даташита. ti.com

там же в даташите нарисовано как надо разводить плату.

Рекомендуемый в даташите на LMP91200 дизайн платыРекомендуемый в даташите на LMP91200 дизайн платы

По схеме из даташита, как видим, выход с LMP91200 подаётся на вход АЦП контроллера, я же направил этот выход на вход отдельного АЦП ADS1110. Этот АЦП уже передаёт данные на STM32 по I2C, через гальваническую развязку в лице ADuM1250 (данные) и дешевого изолятора питания на 1 ватт B0505S-1W (питание).

Изолятор питания B0505S-1WИзолятор питания B0505S-1W


Можно, конечно, как в EVAL-0349 использовать ADuM5000, но этот товарищ имеет одно неприятное свойство достаточно сильные помехи (про это можно почитать в даташите на данный изолятор и сопутствующие аппноты про EMI considerations), требующие разводить плату соответствующим образом.

RTC

Хоть это и не сенсор, но тоже важная периферия, поскольку в системе активно используются различные таймеры. Дело в том, что многочисленные проблемы с RTC, идущим вместе с STM32 прилично утомили. Среди этих проблем были некачественные blue-pill (да, именно на них была разведена основная масса версий плат), криво припаянные кварцы, невидимые сопли на контактах кварца или пинах (PC14, PC15), самого чипа контроллера, куда подключаются кварцы. Однажды я заметил, что часы идут, пока blue-pill не вставлена в разъёмы платы контроллера. Как только вставляешь не идут. Достаёшь опять идут. Отрезал от пилюли пины (PC14, PC15), которые вонзались в разъёмы часы пошли в любом положении. Не любят эти выводы лишних емкостей. Можно было бы как следует чистить платы, разъёмы, отрезать пины, покрывать лаком... да вот один случай совсем расстроил в один из аппаратов забрался таракан и прилёг погреться на чип STM32. Что за дискотеку он там устроил не очень понятно, да вот только напачкал прилично, прям на чипе и вокруг. Встроенный RTC встал. Чистка помогла, но было принято решение дублировать часы при помощи DS3221.

Что было опробовано, но пока не вошло в обиход в силу тех или иных причин

Счётчик воды

Счётчик воды для шлангаСчётчик воды для шланга


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

Счётчик воды с резьбойСчётчик воды с резьбой


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

СО2

Сенсор концентрации углекислого газа MG811Сенсор концентрации углекислого газа MG811


Эксперимента ради был также испробован сенсор концентрации углекислого газа MG811. Сенсор представляет из себя устройство, типа батарейки, с электролитом внутри. Судя по даташиту, ЭДС такой батарейки связан через уравнение Нернста с концентрацией углекислого газа в измеряемой атмосфере. Генерируемые токи такого элемента в районе единиц пикоампер, поэтому часто можно в продаже встретить MG811 в составе шилда, на котором расположен операционный усилитель для поднятия столь мизерных токов. Испольуется такой сенсор не сразу, типа, включил и измеряешь, а в условиях прогретого сенсора. Для этого в MG811 встроен нагревательный элемент, который запитывается по двум (из шести всего) отдельным пинам и потребляет пару сотен миллиампер. И эти миллиамперы, зачастую, несмотря на шилды со встроенными усилителями, требуется подавать с напряжением 6В. Не сказать, чтобы большая проблема поставить дополнительный повышающий преобразователь с 3.3 или 5 вольт, но учитывать этот момент всё же приходится. Вероятно, есть в природе шилды со встроенным повышающим преобразователем напряжения, но мне попадались без него. Времени на разогрев требуется около минуты, отзывчивость тоже не такая резкая, что подышал и увидел броски показаний. За неимением газового обрудования и возможности его применять в полной мере, отложил сей сенсор пока на полку. Кстати, для полноценной работы с ним будет нелишним обзавестись смесями калибровочных газов, что иногда затруднительно и доставляет хлопот поболее, чем с теми же калибровочными жидкостями для pH сенсоров.

Из известных мне, есть ещё инфракрасные сенсоры углекислоты, так называемые NDIR. Их есть несколько моделей. Отличаются скоростью отклика, диапазонами измерений, погрешностью и, разумеется, ценой. В качестве примера, на который в своё время посматривал, могу назвать MH-Z14A. Рекомендовать или отговаривать не могу, ибо не имею в наличии и не проверял. На Хабре есть те, кто держал подобные в руках (раз и два).

Датчик освещённости

Хороший пример описан тут

Системы, способы контроля и предотвращения нештатных ситуаций. Электроника

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

Watchdog

В контроллерах STM32 имеется встроенный независимый вочдог таймер именуемый IWDG (Independent WatchDoG). Он хорошо справляется с подавляющим числом зависаний контроллера, на большинстве контроллеров позволяет даже просыпаться из глубокого сна. Однако, как показала практика (особенно на сырых версиях электроники, да простят меня олдовые), в реальных условиях, даже он не всегда справляется. Поэтому был установлен дополнительный, аппаратный сторожевой таймер. С усовершенствованием схемотехники и прошивок, толку от него становится меньше. Тем не менее, для страховки он остался по сей день. Я использовал MAX6369. Для тех кто не в курсе, поясню вкратце у аппаратного сторожевого таймера выход WDO подключается на вход RESET контроллера, а вход WDI заводится на одну из ног контроллера. Когда всё работает, прошивка периодически генерирует импульс на WDI, чтобы дать понять сторожевому таймеру, что всё впорядке. Если в течение продолжительного времени на WDI такого импульса не приходит, сторожевой таймер даёт импульс на WDO, что приводит к аппаратному (как от кнопки) перезапуску контроллера. Можно такой сторожевик собрать и на таймере 555 (чем я тоже баловался в качестве эксперимента), но он занимает прилично места, по сравнению со специализированными решениями.

Неопредённые состояния во время старта

Неопредённые состояния во время старта больше всего досаждают на исполнительных устройствах. Пока контроллер не загрузился, на выходах творится непонятное, что приводит к нежелательным срабатываниям всего, что управляется этими выводами (управление питанием измерительной техники, клапана, дозаторы...). Чтобы устранить эти явления, на контроллере можно железно подтянуть управляющие выходы к земле (или питанию. В зависимости от полярности управляющего сигнала) резистором. В данном случае так сделано, например, для полевых транзисторов, которые управляют питанием измерительных модулей и Raspberry Pi.

разные варианты драйвера нагрузок, до и после испытанийразные варианты драйвера нагрузок, до и после испытаний

Поскольку у меня исполнительными устройствами усправляет отдельный драйвер, где управляющие сигналы создаёт экспандер MCP23017, то подтянуть все 16 линий, вероятно (следует заглянуть в даташит на L6205 и/или протестировать в железе, чтобы уточнить однозначно), можно на нём. Однако, чтобы получить результат наверняка и с меньшим числом точек запайки я выбрал установку достаточно мощного полевого P-канального транзистора на питание всех L6205. В результате, этим полевиком управляет основной контроллер STM32 через оптопару. Логика прошивки сначала инициализирует все входы/выходы на выход, пишет в регистры выходов нули (всё выключено), зачитвает обратно содержимое этих регистров, сверяет с тем, что только что было записано и, если все биты совпадают, то старт считается успешным. Если старт определился как неуспешный, поднимается соответствующий флаг ошибки и работа всей силовой части блокируется (основной контроллер не станет открывать полевик до устранения проблемы). Если же старт засчитан успешным, то полевик всё ещё будет закрыт. До тех пор, пока не будет выдана команда на запуск одной из нагрузок, которая (команда) отправит на экспандер драйвера последовательность бит (где как минимум один из битов будет отличаться от нуля) и не прочитает обратно такую же последовательность. Когда все эти условия выполнятся, основной контроллер открывает MOSFET и только тогда драйвер начинает питать нагрузку. По завершении работы нагрузки/нагрузок, основной контроллер вновь отключает питание силовой части драйвера, закрывая полевик до следующих мероприятий. Если происходит физический обрыв проводов (скажем, отгрызла внезапная мышь), то линия управления полевым транзистором ложится в ноль (для P-канального, на самом деле, в 1) и нагрузки не сдвинутся с места (как минимум, до тех пор, пока не будет отгрызен подтягивающий резистор).

Кстати, о силовом драйвере. Однажды, пришёл ко мне один молодой человек и попросил дать ему попрактиковаться в сборке. Было ему выделено место, даны компоненты, паяльник, платы, схемы, тестовый аппарат. Собрал он пару таких драйверов и стал их тестировать. Не получается. Звонит мне, рассказывает невероятные вещи. Приехать на место и проверить лично в чём там дело непросто - я в другой стране. В драйвере этом, в роли экспандера, использовалась 74HC595. Он тычет в неё и говорит, мол, то ли микросема отстой, то ли прошивка твоя не важнецкая. Дело дрянь, вобщем. Я проверяю прошивку на своём девайсе, перепроверяю все подключения у себя, шлю ему видео. И у него всё-равно не работает. Ну, думаю, что с микросхемами-то может быть. Я же не DI HALT, который всё-всё видывал в силу огроменного опыта и может писать про подделки, в которых нет кристаллов или ещё что похуже. Мне чисто по теории вероятности левак достанется крайне наврядли - думал я. А вариант с прошивкой не подтверждался в моих тестах.
Микрухи, действительно, оказались леваком. Об этом я узнал, когда прибыл на место событий. Чувак к тому моменту пропал за горизонтом. Так что, если ты вдруг это читаешь, дядька, знай - ты был прав, микрухи оказались шляпой. А я ошибся.

Малина

Иногда Raspberry Pi может перестать подавать признаки жизни. Как сказано выше, основной контроллер может управлять её питанием. И в таких случаях тишины он именно это и делает. Причин тишины от малины может быть несколько. Наиболее частые из них: перегрев малины или износ карты памяти. Для нивелирования зависаний по перегреву основной контроллер выжидает заданный промежуток времени, после чего перезапускает питание Raspberry Pi. То же самое происходит и в случае с износом карты памяти, только здесь, как несложно догадаться, рестарт уже не помогает. Специальный счётчик даёт несколько попыток рестарта. Если они не помогли, то, как это уже стало традицией, поднимается соответствующий флаг ошибки и малина оставляется в покое.

Чтобы износ карт памяти происходил как можно реже, используется как минимум две карты. Одна из них монтируется в режиме только для чтения. С неё происходит загрузка системы. Логи, данные, графики пишутся на отдельную карту, смонтированную для чтения/записи. Кроме этого, в ходе испытаний были испробованы различные карты памяти и из них выбраны наиболее долговечные. Разница в жизнеспособности разных представителей карт памяти измеряется порядками. Некоторые карты перестают записываться, а некоторые даже читаться. Чтобы не просрать все полимеры логи, написана функция, запускающая телеграм-бота, который периодически шлёт пользователю графики логов.

Питание

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

Кстати, о клеммах

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

клемма поплылаклемма поплыла

Они были использованы в первых версиях аппаратов на проводах основного насоса. Сначала ставились рядом с мотором, чтобы удобно было поставить обратный диод. Когда принесли аппарат с погоревшими клеммами, диод я стал запаивать, а клеммы вынес подальше, сантиметров на 10-15. Это не изменило ровным счётом ничего. Больше я их там не использую вообще, только запайка. А вот разём GX16 справляется и не чернеет даже (хотя, лет через 10, кто его знает).

Пара историй про химические фэйлы

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

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

Ещё один случай связан с кислотой. Там была азотная кислота, около 35% концентрации, подключенная в дозатор. На КДПВ из первой части видно, как штатные светло-кремовые шланги дозаторов переходят через черные соединители в коричневые (бывают черные) ПВХ. Это был тот самый случай. Как выяснилось (да простят меня химики), так делать категорически не следует. ПВХ за считанные дни задубел, соединители превратились в сопли-порошок, система потекла (хорошо, что аварийный дренаж был в наличии). Теперь только шланги, рекомендованные производителем под эти кислоты, с регламентом снятия/замены. И кислоты разводить в воде.

светлые трубочки идущие в канистры - расходный материалсветлые трубочки идущие в канистры - расходный материал
Подробнее..

Транслируем искусство через робототехнику

08.04.2021 22:07:23 | Автор: admin

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

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

Big Fucking Printer

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

  • Разработать систему разметки поля

  • Придумать маркер для нанесения краски на огромную площадь

  • Переделать всю электронику робота, не кататься же на бееедуине, верно? Никто ведь так не делает, правда?

  • Разработать стек навигации для робота

  • Написать пару*строк кода

Ну что, строим?

Конечно же нет, ведь нужно выбрать из чего, как и сколько это будет стоить. Для решения вопроса с навигацией мы потратили не мало времени и ни одно копьё было сломано.SLAMотвергли наши программисты-питонисты-(дата сатанисты). Инерциальный датчик отвергли программисты-электронщики. А вот езда по тросу понравилась всем. Еще были такие варианты: расставить лазеры, расставить метки, использовать энкодеры на колёсах, лазерная рулетка,GPSи много всего другого. Размер нашего холста: 40x50 метров. Для определения текущего положения робота с точность до 1 см использовалась связка энкодера и инфракрасного датчика расстояния. При помощи ввёртышей и талрепов натягивается трос каждые 5 метров. На роботе устанавливается каретка с энкодером, ее отклонение фиксирует датчик расстояния. Трос проходит через энкодер и при смещении робота относительно троса, смещается и каретка. Конструкция хоть и простая, но на таких расстояниях крайне эффективная.

Допустим мы знаем наше положение на поле, но чем и как мы будем рисовать? А почему бы не использовать готовые решения? Итак, мы обзавелись обычным аэрографом, компрессором с ресивером и огромной направляющей из алюминия длинной в 5 метров. Каждые 10 сантиметров робот останавливается, аэрограф совершает поступательное движение по рельсе и наносит часть изображения. Для приведения аэрографа в движение к нему прикрутили мотор с шестерней, а на рельсе установили зубчатую рейку, бережно отфрезерованную из обрезков фанеры.

Маркер в действии

Теперь-то строим?

Почти. Моделируем. СвязкаSolidWorksиFusion360, запихнутая вGazebo это отдельный способ извращений, но команда состоит из творческих людей с различными взглядами на жизнь. Так или иначе, лучший файлообменник телеграмм соединяет сердца инженеров. Модель робота была кропотливо разработана вCADсистемах (а с нашим бюджетом мы не могли позволить купить лишних запчастей) и протестирована в симуляторе, ведь пруды чистые, а не черновые.

Итого, детали в пути, и мы приступаем к проектированию плат и написанию кода. Робототехников хлебом не корми, дай на питончике, да на плюсах покодить,а вот держи. Собственно мозгами робота сталиRaspberryPi,STM32-Discoveryи несколько плат с камушками серииSTM32F1. Их связь осуществляется черезUART. Для питания сего изобретения были выбраны литиевые аккумуляторы в сборке 12s8p, переваренные из батареиTeslaи два 12-вольтовых гелиевых аккумулятора. В качестве движителей общим решением были избраны моторыNema-23 с планетарными редукторами 1:4. Дешёвые драйверы двигателейTB6600 расплавились при первом включении, пришлось срочно искать замену DM860H.

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

Полевые испытания

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

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

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

Фото с первых тестов

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

Финальный эксперимент?

Чтобы управа района одобрила покраску Чистых Прудов, нам было необходимо продемонстрировать работоспособность робота. Итак, в последний раз мы отправляемся в хоккейную коробку. Что по изменениям? К краскопульту добавили шторки, чтобы краска не летела во все стороны, а также не намерзала на винтовой передаче. Добавили систему подачи краски в ресивер краскопульта. В качестве краски использовали колер, разбавленный спиртом. Как вы понимаете, спирта было потрачено много. Гелиевые аккумуляторы дополнились сборкой 4s8pиз тех же аккумуляторовTesla, которыеPanasonic.

Рисовать решили логотипYouTube-канала Техникум, они ездили с нами на испытания, поэтому мы захотели их отблагодарить.Окраска квадрата 5x5м заняла около 30 минут. На удивление, первый видимый результат получился удовлетворительным.

Результат

Ало, пустите на пруд?

Работа над проектом затянулась на 3 месяца. Последние испытания мы провели 18 марта, когда на улице температура стала колебаться около нуля. Нам вежливо предложили отказаться от этой идеи. Да, мы не успели довести дело до финальной точки в этом году. В следующем году мы постараемся все же выехать в центр города и провести нашу акцию. А пока, можете посмотреть результат работы в нашем инстаграм-аккаунте:artbot.moscow

Мы, робот и Грант Гастин

P.S.

Сейчас мы продолжаем совершенствовать робота. Первоочередная задача перенос стека навигации на aruco метки, чтобы робота можно было использовать в помещениях. 17 апреля в университете науки и технологий НИТУ "МИСиС" пройдёт день открытых дверей. Мы будем рисовать логотип университета в центральном лобби, фотоотчёт будет в инсте.

Подробнее..

MIDI браслет для управления синтезаторами (в основном для органично звучащего вибрато)

15.04.2021 00:12:17 | Автор: admin

Для нетерпеливых - ссылка на видео с демонстрацией и полным контентом поста в конце

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

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

Не удивительно, что на рынке полно устройств, которые преобразовывают это усилие в язык музыки. Первый это Roli Seaboard, мультиполифоническая MIDI-клавиатура которая позволяет сопоставлять жесты со звуковыми параметрами. Другая схожая интерпретация этой идеи это Хакен Континуум. Оба эти варианта поставляются с премиальным ценником, и по моему мнению - заслужено.

Следующий пример чуть мене комплексный. Genki Waves, насколько я понимаю, состоит в основном из кольца, которое может управлять программным обеспечением. Но сайт позволяет приобрести модуль eurorack и midi-адаптер 5din, что делает этот сетап вполне универсальным.

Другой вариант, если существует необходимость управлять железными синтезаторами Enhancia Neova ring. Полный комплект состоит из кольца, станции с современными 3,5 - мм MIDI входом и выходом и программного обеспечения для точной интерпретации жестов.

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

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

План состоит в использовании двух плат esp32. Один-как наручный сервер, собирающий данные с акселерометра и преобразующий их в сообщения об изменении высоты тона и модуляции. Я привяжу тангаж к бендам и крен к модуляции. Кроме того, поскольку на борту модуля MPU6050 есть встроенный гироскоп, для бендов я также буду собирать информацию о горизонтальном смещении, поскольку оно также отображает это интуитивное движение. Три потенциометра будут контролировать чувствительность каждой оси к соответствующему параметру. Литиевая батарея будет поддерживать беспроводную работу устройства с помощью модуля управления зарядкой. Этот конкретный модуль поддерживает все средства защиты и не имеет никаких ограничений по минимальному току, что, наконец, отправило платы на базе tp4056 для меня в отставку. В таком случае у меня будет возможность подключить свой компьютер к наручному блоку для управления программными синтезаторами с, я надеюсь, низкой задержкой. Для управления любыми железными блоками я также построю стационарный хаб, который сможет конвертировать беспроводные BLEMIDI сообщения в аппаратные MIDI сообщения для взаимодействия с аппаратными синтезаторами. Для этого хаба я выбрал контроллер с OLED-экраном и с помощью поворотного энкодера смогу переназначить бенды и модуляцию как любую другую MIDI CC команду для управления громкостью, панорамированием, позиции уэйвтейбла и всем тем, что конкретный синтезатор сможет изменить на лету. Я не могу собрать простую сквозную схему, она неизбежно повредит данные, это должна быть полноценная смешивающая схема. Я также добавлю 2 CV-выхода для взаимодействия с модульным и полумодульным оборудованием.

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

Со стороны станции я припаял аппаратные MIDI входы и выходы и операционные усилители для усиления сигналов от ЦАПОВ до уровней CV, идущих на выходы 3,5 мм. Входящие BT-сообщения могут быть реорганизованы как любое MIDI СС-сообщение идущее через любой канал чтобы управлять несколькими синтезаторами одновременно, или переключаться между ними.

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

Есть небольшая задержка, но так как выразительность-главная цель, на это стоит обратить внимание. Кроме того, устройство постоянно отключалось и подключение никогда не следовало одной и той же процедуре и требовало несколько перезагрузок и перезапусков DAW. Это очень неудобно и выбивает из колеи.

После отключения всех остальных беспроводных соединений на моем компьютере, включая Wi-Fi, задержка сократилась, и проблема отключения стала менее частой. Это привело меня к выводу, что слабым звеном в этой цепочке являются беспроводные возможности моего компьютера. Поскольку мой конкретный аудиоинтерфейс не имеет MIDI-портов, я схватил Arduino Due и быстренько собрал USB-MIDI-интерфейс, чтобы иметь возможность сопряжать устройство со станцией, подключать станцию к интерфейсу миди-кабелем, подключать интерфейс к ПК и получать MIDI-сообщения таким образом. И проблема отваливания от блютуз исчезла.

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

Поскольку прямое BT подключение к моему компьютеру немного медленное, и я все равно скован использованием MIDI-USB интерфейса, вместо того чтобы читать значения, преобразовывать их в BT midi и отправлять медленные BT MIDI сообщения для отправки обычных MIDI, я решил полностью лишить сообщения универсального протокола и отправлять необработанные значения и преобразовывать их на принимающем конце в MIDI, уменьшая количество шагов и объем отправляемых данных. Для этого я использовал протокол ESPNOW, который без каких-либо махинаций с рукопожатиями при наличии только mac-адреса может отправлять только 3 байта данных на устройство, которое ожидает только 3 байта данных. Я удалил все куски кода с подтверждением передачи, чтобы уменьшить задержку. И все заработало безупречно.

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

Во-первых, это конструкция из 3 контроллеров, где на самом деле требуется только 2. Прием данных, создание MIDI и преобразование в USB MIDI должны происходить в одном устройстве. И это, как минимум в теории, довольно легко реализовать, потому что более поздняя версия ESP32 под названием S2 способна быть нативным USB устройством, что делает адаптацию кода довольно легкой. Но мало того, что этой штуки у меня нет, существуют и другие проблемы.

В основном энергопотребление. Текущая установка убивает относительно жирную батарейку через полчаса. Именно по этой причине на всех кадрах был шнур, это не связь, это просто для питания. И проблема даже не в емкости, а в возможностях токоотдачи этой батареи примерно на 3,5 В она просто перестает быть способной питать прожорливый BT/WIFI чип. Это заставляет меня поменять аппаратное обеспечение и перепроектировать все с нуля.

Поскольку лучшая прошивка с точки зрения задержки использовала прямую передачу пакетов, не будучи завернутой в очень специфический протокол, я решил использовать модули nrf24l01+, которые очень похожи в этом отношении. Это означает, что я могу соединить этот модуль с DUE, которая раньше был просто MIDI USB интерфейсом, и использовать его в качестве хаба, который и делает все - прием данных, аппаратное midi, USB midi, CV-напряжение, реорганизацию сообщений и т. Д.

В качестве передатчика у меня было 2 варианта Pro Micro и STM32 blue pill. Второй вариант не только менее энергозатратен, но и имеет гораздо большее разрешение на входах АЦП, что позволит избежать потенциальных резких рывков при изменении уровня эффекта. Не говоря уже о том, насколько большими вычислительными способностями он обладает.

Таким образом, сетап меняется от сложного чтения данных об ускорении, обертывания этих данных в протокол BLE MIDI, отправки BLE MIDI, преобразования в MIDI и преобразования из MIDI в USB MIDI к гораздо более простому чтения данных об ускорении, отправки этих необработанных данных, а затем простого преобразования их в USB MIDI. И как всем известно, простое решение это надежное решение.

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

Я могу использовать встроенные в синтезатор бенды и модуляции. Я могу перенаправить разные CC на любой параметр VST на любом канале. Я могу использовать устройство для управления несколькими параметрами одного синтезатора. Я могу использовать устройство для управления параметрами нескольких VST одновременно. Можно во время или после записи прописать автоматизацию трека по параметру.

Так что в конце дня у меня было 3 версии одного и того же устройства. BT версия полезна в том случае, когда требуется только подключение к ПК. Будучи по своей сути BT сервером при переключении между ПК и хабом, я должен вручную прерывать соединение и каждый раз устанавливать новое. Поэтому, поскольку я не использую свой компьютер в качестве центра управления железными синтами (не то, чтобы мой аудиоинтерфейс это поддерживает) мне не хочется продолжать реализацию конкретно этой версии. Однако существует открытый проект умных часов, в которые встроен акселерометр. Сенсорный экран должен быть способен вместить в себя все меню, переключать каналы, номера CC сообщений и чувствительность и позволять хранить множество пресетов. Будучи проектом на базе ESP32, перенос проекта не должен вызвать много проблем, если когда-нибудь я заполучу его в свои руки. Я чувствую себя обязанным подчеркнуть, что я протестировал более одного модуля ESP32, и задержка колеблется от юзабельной до невыносимой, и я понятия не имею, как будет вести себя этот конкретный блок. Задержка также коррелировала с энергопотреблением более медленный модуль был способен жить от батареи гораздо дольше, разряжая ее должным образом.

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

Тем более, что текущая рабочая версия, основанная на DUE и STM32, обеспечивает наилучший баланс между задержкой (она в принципе существует только в контексте версии ESPNOW) и простотой. Энергопотребление очень низкое и я не смог разрядить батарею во время тестирования. Станция поддерживает как USB, так и аппаратный MIDI одновременно, без необходимости ручного подключения, мне просто нужно щелкнуть переключатель и подать питание. Эту версию все еще можно сделать намного меньше, но в данный момент это не принципиально. Что можно поменять, так это железо. Либо поставить более мощную версию NRF24 в хаб, либо перенести весь проект на LORA.

Cсылка на видео (с демонстрацией игры)

Код

Подробнее..

OpenRPNCalc делаем бескомпромиссный калькулятор

18.06.2021 02:21:22 | Автор: admin
Калькулятор как он есть.Калькулятор как он есть.

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

Но зачем?

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

Калькуляторов у меня не было с окончания универа (последний был Citizen SRP-75). Как оказалось, дизайн их интерфейса с тех пор изменился неузнаваемо и топовые модели теперь скорее напоминают какую-нибудь Wolfram Mathematica. Ничего не имею против, но если мне надо посчитать действительно что-то сложное, гораздо удобнее это сделать на компьютере. В калькуляторе же мне хотелось бы иметь минимальный набор функций, которые мне нужны, без необходимости путешествовать по многоуровневым меню. И не иметь тех, которые точно не нужны, т. к. место на клавиатуре не резиновое.

Как оказалось, есть небольшая фирма SwissMicros, которая выпускает неплохие копии старых программируемых калькуляторов Hewlett Packard (HP) на основе современных ARM-процессоров и симулятора Free42 с открытым кодом. Но опять же, это не идеал есть некоторые функции (об этом ниже), которые мне пришлось бы программировать, а запускать программы это совсем не то же самое, что нажать на кнопку.

Вот примерно такая мотивация привела меня к решению сделать свой калькулятор (с трудом удерживаюсь от цитирования персонажа Футурамы). Хотя, честно, полностью рациональным такое решение назвать нельзя, и по большей части оно объясняется диагнозом руки чесались.

Для разнообразия я решил в кои-то веки сделать проект, который не выглядит слишком колхозно, которым реально можно пользоваться, и может быть даже не слишком прячась от коллег по работе. Хотя это и не первый раз, когда я делаю какую-то электронику, до сих пор я в основном возился с DIP-корпусами, макетками и синей изолентой, а тут сам бог велел сделать что-то посовременнее. Соответственно, я получил море новых впечатлений, разбираясь с многими вещами с нуля (программирование для ARM, пайка SMD, разработка в KiCAD и OpenSCAD, 3D-печать). Готовьтесь, сейчас я ими здесь поделюсь. Вдруг кому-то поможет, или кто из более опытных посоветует что-нибудь дельное.

Код, как и вся, с позволения сказать, документация выложены на GitHub. Да, код ужасен. Да, постараюсь исправиться :)

Концепция

Итак, будем делать научный, непрограммируемый, калькулятор, в который при желании можно добавлять новые функции. Как бывший член экипажа лунолёта Кон-Тики, я, конечно, обязан был сделать калькулятор с обратной бесскобочной (она же польская, она же RPN) логикой. Благо, её и программировать легче.Ещё одним преимуществом RPN поделился со мной пользователь с сайта Hackaday: такой калькулятор у вас вряд ли кто попросит попользоваться на время.

Итак, что хотелось мне иметь в идеале в своей машинке:

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

  • Из того, что редко попадается в коммерческих калькуляторах некоторые функции для работы со случайными распределениями: erf(x) (бывает частенько, но обычно с доступом через меню) и обратная к ней erf-1(x) (никогда не встречал), интеграл от распределения хи-квадрат для данного числа степеней свободы, распределение Пуассона. При этом мне не нужен статистический режим как таковой с вводом больших массивов данных для тяжёлой обработки данных у меня всё равно есть компьютер.

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

  • Некоторые очень специфические функции, в основном связанные с релятивистской кинематикой.

  • Стандартные режимы отображения SCI (с мантиссой и порядком) и ENG (с порядком, кратным трём) и изменяемым количеством значащих цифр мантиссы (3-10). В режиме ENG, к тому же, можно для удобства сделать показ префиксов единиц СИ (m, k, M и т. д.).Диапазона double будет более чем достаточно. SwissMicros делает калькуляторы c quarduple precision (что ещё ждать от швейцарской-то фирмы?), но в нашей немудрёной науке, если в вычислении используется больше шести-семи значащих цифр с вычислением что-то не так.

  • Обратная бесскобочная логика со стеком из 4 элементов (X,Y,Z,T) плюс регистр предыдущего результата (LASTx или X1) как у HP или Б3-34. Есть ещё вариант сделать бесконечный стек, как у старших моделей HP, но пока я ограничился более простым вариантом.

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

Электроника

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

С экраном вопросов нет это будет монохромный ЖКИ дисплей Sharp Memory LCD, как у SwissMicros DM42. Судя по многим отзывам, это практически идеальный дисплей с хорошей контрастностью, очень малой потребляемой мощностью, и управляется по последовательной шине SPI. В нашем случае это будет модуль LS027B7DH01размером 2.7 (размер изображения 60x40 мм) и разрешением 400x240 точек. С таким разрешением можно показывать все 4 регистра стека одновременно, да и для режима вычислений с ошибками это будет полезно.Модуль потребляет всего около 20 мкА от 5В в режиме показываю, но ничего не делаю.

Процессор, недолго думая, я тоже взял из DM42: STM32L476, правда, в корпусе LQFP64 (модификация STM32L476RG). В DM42 стоит тот же процессор в корпусе LQFP100 (100 пинов), но нам не нужны ни внешний Flash, ни SD-карта, так что 64 пина хватит за глаза. Процессор может работать на частоте до 80 МГц, есть 128 кБ оперативки и 1 МБ программного флеша ought to be enough for anybody. Ну и ещё много всяческого добра, которым мы по большей части не будем пользоваться.

С клавиатурой вопросов больше. Многие обозреватели жалуются, что у SwissMicros слишком жёсткая клавиатура, быстро кнопки нажимать неудобно, и вообще ничто не может сравниться с классикой HP. Попробуем найти что-то получше, чем купольные кнопки на DM42. Первые попавшиеся тактовые кнопки с AliExpress мне показались слишком тугими. Порывшись по каталогам, я нашёл самые мягкие и достаточно плоские из тех, которые можно заказать, не особо напрягаясь Panasonic EVQQ2B01W с усилием нажатия 50 г (при том, что обычные кнопки, которые продаются на каждом углу, обычно требуют усилие в 150-200 г).

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

Схема всего девайса элементарная и показана на рисунке выше. Собственно, кроме STM32 в стандартном подключении, клавиатурной матрицы и пары разъемов (один для ЖКИ, другой для программатора) там есть только преобразователь напряжения 3В в 5В для питания ЖКИ на очень экономном чипе TPS61222. STM запитан непосредственно от литиевой батарейки. Не знаю, хорошая ли это идея, или лучше было поставить стабилизированный преобразователь. Кварц для тактирования процессора решил не ставить (можно и встроенным RC генератором обойтись), но на всякий случай поставил часовой кварц.

Кстати, по поводу питания ЖКИ. То, что нарисовано сейчас на схеме, хоть и работает, но не совсем правильно. Как оказалось уже после того, как я развел и заказал плату, преобразователь TPS61222 не полностью отключает выходную цепь от питания при низком уровне сигнала 5V_EN, а только выключает сам преобразователь, оставляя на выходе 3В вместо пяти. Надо внимательнее читать даташиты! Попутно оказалось, что и от трех вольт ЖКИ прекрасно работает, и даже контрастность не страдает. Может быть, в следующей версии платы преобразователь можно просто выкинуть?

Рисовал схему и разводил плату в KiCAD. Почти все элементы там нашлись в стандартной библиотеке, кроме 10-пинового разъёма Molex с шагом 0.5 мм для ЖКИ, его пришлось нарисовать самому по образцу какого-то другого с другим шагом.

С лазерным утюгом я не дружу, поэтому плату заказал на одном из специализированных сайтов (в плате нет никаких тонкостей, так что любой дешёвый PCB-сервис должен с ней справиться). Дисплей Sharp и разная мелочь продаётся на AliExpress, а вот с покупкой процессора STM я, похоже, пал жертвой дефицита чипов. Три китайских продавца меня кинули (причём один сделал вид, что всё выслал, тянул две недели, после чего уверял, что посылку задержала таможня, а сам поднял цену на тот же чип раза в три). К счастью, в один прекрасный момент несколько сотен нужных чипов выбросили на сайте Mouser, из которых я и отхватил несколько штук. На том же Маузере я заказал и кнопки Panasonic, т.к. на Али практически все кнопки noname с непонятно какими характеристиками.

Несмотря на мой изначальный страх, пайка SMD пошла на удивление легко, даже разъём LCD и сам STM32 с ножками с шагом в 0.5 мм паяются без проблем. Оказалось, пора уже было забыть про натуральную сосновую канифоль и перейти на современную бездушную паяльную пасту. Немного больше тренировки потребовала пайка разной мелочи типоразмера 0603 (резисторы, конденсаторы).

Собранная плата с обратной стороны. К плюсовому контакту батарейки припаян не предусмотренный в проекте штырь для того, чтобы запитывать плату от программатора STLink и измерять потребляемый ток. Собранная плата с обратной стороны. К плюсовому контакту батарейки припаян не предусмотренный в проекте штырь для того, чтобы запитывать плату от программатора STLink и измерять потребляемый ток. Собранная плата с уже прошитым STM32, вид спереди. Дисплей прилеплен на двусторонний скотч. Собранная плата с уже прошитым STM32, вид спереди. Дисплей прилеплен на двусторонний скотч.

Прошивка STM32

Как оказалось, найти информацию для того, чтобы начать программирование на STM32 с нуля, не так-то просто, похоже, из-за того, что альтернативных инструментов очень много, они быстро появляются и устаревают. Наверное, в конце концов лучше учиться писать на голом gcc, но для начала я хотел взять какой-нибудь IDE в стиле для чайников с визуальным конфигурированием процессора. В результате я использовал STM32Cube IDE. Я так и не смог добиться, чтобы он работал в Ubuntu, поэтому пришлось ставить ради него целую виртуальную машину с Windows 10.

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

Функции для работы с дисплеем Sharp я писал сам по даташиту, и там всё оказалось очень просто. Система команд дисплея состоит, практически, из 2-х команд. Первая это очистка экрана. Вторая передача массива информации, который состоит из номера строки и 50 байт данных строки.Одна тонкость работы с дисплеем когда он включен, ему нужен постоянный внешний сигнал около 1 Гц для периодического изменения полярности электрического поля на ЖК-матрице. Этот сигнал генерируется по прерыванию от внутреннего таймера STM. При выключенном ЖКИ этот сигнал надо также выключать.

Собственно саму реализацию алгоритма работы калькулятора я сперва отладил на большом компьютере, написав заглушки для функций работы с клавиатурой и дисплеем. STM32L476 поддерживает полную математическую библиотеку gcc, более того, вычисления с плавающей точкой там реализованы в железе, так что всё работает очень быстро. Я понизил частоту работы процессора до 8 МГц, чтобы ограничить максимальный потребляемый ток (который тогда получается около 4 мА при полной нагрузке), при этом никаких видимых задержек при вычислениях не появляется. При меньшей частоте начинает заметно тормозить вывод на экран.

Для прошивки я купил один из китайских клонов программатора/отладчика ST-Link v2, которые продают где угодно за копейки. С ним вышла небольшая проблема: судя по всему, мой экземпляр не умеет делать connect under reset, из-за чего STM в состоянии спячки я программировать не могу. Пришлось предусмотреть в прошивке волшебное сочетание кнопок (Shift+RESET), при котором контроллер не уходит в STOP, а ждёт соединения с программатором. Неприятно, но не смертельно.

Вся прошивка занимает примерно 120 кБ программной памяти. При этом большую часть объёма составляют растровые экранные шрифты (размером от 6x8 до 24x40).

Корпус и клавиатура

Первоначальная наивная задумка была сделать корпус, фрезерованный из алюминия, так, чтоб на века. Но фрезерного станка у меня нет, а после изучения расценок на штучные детали на сайтах типа 3DHubs и Xometry планы пришлось подкорректировать и смотреть в сторону 3D-печати из пластика.Поскольку до этого я ничего, кроме кошечек по готовым чертежам, на своём 3D-принтере не печатал, заодно пришлось разобраться с софтом для 3D-дизайна.

Промаявшись несколько вечеров с глючным FreeCADом (не покупать же программы Autodesk за бешеные деньги), я понял, что гораздо легче написать программу, которая описывает геометрию детали, чем ползать мышкой по меню, поэтому перешёл на OpenSCAD. Хотя у него есть свои ограничения: например, в отличие от FreeCAD, в нём сложно делать фаски и скругления граней.На OpenSCAD дело пошло гораздо веселее.

Корпус и клавиатура, нарисованные в OpenSCAD.Корпус и клавиатура, нарисованные в OpenSCAD.

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

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

Корпус получился не очень тонким (полная толщина 12.5 мм), хотя внутри он по большей части пустой. Всё из-за того, что гнездо для батарейки CR2032 довольно высокое (5.5 мм) и с этим сложно что-либо поделать. Для следующей версии надо будет изучить вопрос, существуют ли более тонкие держатели для CR2032, которые к тому же можно установить в вырез на печатной плате. Таким образом можно было бы сэкономить как минимум пару-тройку миллиметров толщины.

Не буду здесь долго описывать сагу про печать корпуса скажу только, что он получился лишь с четвёртой или пятой попытки, при этом пришлось тонко настраивать геометрию моего Ender 3 (перпендикулярность осей X и Y), иначе корпус вело винтом после соединения двух половин. Печатал пластиком PETG.

Надписи на кнопках и на корпусе я напечатал на тонкой матовой клеящейся плёнке для лазерных принтеров (чего только не существует в мире канцелярских товаров!). Кажется, эту идею я нашёл на каком-то форуме любителей самостоятельно делать наклейки для русификации клавиатур (да, такое тоже бывает). Сам PDF-файл с надписями и линиями отреза генерируется питоновским скриптом с использованием библиотеки matplotlib. А что ещё можно использовать для графики с обилием математических символов?Напечатанный текст держится на стикерах намертво, их можно даже мыть. Тот стикер, который на корпусе, за счёт большой площади прилип надёжно. К сожалению, стикеры на кнопках не очень прочно прилипают и неосторожным движением их можно отодрать. Впоследствии, возможно, их лучше будет покрыть сверху лаком, но пока надписи на клавиатуре не устаканились, и так сойдет. Минус такой технологии наклейки будут хорошо видны только на белом корпусе.

Результат

Вот что в результате получилось. Дисплей в режиме отображения ENG с 6 значащими цифрами и активированным режимом вычислений с неопределенностями.Вот что в результате получилось. Дисплей в режиме отображения ENG с 6 значащими цифрами и активированным режимом вычислений с неопределенностями.

Калькулятор в его сегодняшнем состоянии показан на картинке. Большая часть кнопок пояснений не требует. Вот список тех, которые могут быть неочевидными:

  • F, G клавиши shift. Пока в основном задействована только F. G нужна только для обратного направления преобразований (), (), а дальше будет использоваться для новых функций, если таковые появятся.

  • Mode изменение режима отображения чисел (FIX, SCI, ENG).

  • Uncr включение/выключение режима вычислений с неопределённостями (UNCERT).

  • Prec переключение количества значащих цифр мантиссы (от 10 до 3 циклически, с F в обратную сторону).

  • Drop, X<>Y, Rot работа со стеком. LASTx вызов результата предыдущей операции.

  • DR переключение измерения углов между градусами и радианами, D<>R то же с преобразованием значения угла в регистре X.

  • RP, PR перевод между декартовыми и полярными координатами.

  • N(x), N(x-y) работают только в режиме неопределённостей, и выполняют, соответственно, вычисление стат. значимости значения в регистре X (значение, деленное на неопредёленность) и значимости разности значений в регистрах X и Y.

  • (), (), p(zxy) те самые специфические функции, которые мало кому нужны: вычисление псевдобыстроты (pseudorapidity), релятивистского гамма-фактора, вычисление импульса в центре масс двухчастичного распада.

  • /-/ обычное изменение знака числа в регистре X, а переключение между вводом значения и ошибки в режиме UNCERT.

Планы на будущее

В принципе, машинка уже сейчас вполне функциональная, но всегда можно что-то улучшить:

  • Есть ещё резервы в плане уменьшения энергопотребления. Сейчас калькулятор расходует около 50 мкА в режиме со включенным дисплеем, и 40 мкА с выключенным. Как я уже говорил, полностью питание с дисплея сейчас не снимается, хотя надо бы это пофиксить. Кроме того, можно улучшить алгоритм опроса клавиатуры: сейчас, когда калькулятор включен и работает дисплей, процессор не засыпает, пока нажатая кнопка не отпущена, и потребляет при этом около 4 мА. Надо бы здесь тоже задействовать внешние прерывания и режим STOP.

  • Функция сканирования клавиатуры понимает только одну нажатую кнопку за раз. Хотелось бы сделать режим two-key rollover, когда регистрируется кнопка, нажатая до того, как отпущена предыдущая, чтобы кнопки надёжнее срабатывали при быстром наборе.

  • Как я уже писал выше, можно было бы сделать более тонкий корпус за счёт другого держателя батареи.

  • Клавиатуре всё ещё далеко до HP и даже до Citizen. Кнопки нажимаются легко, но глубина нажатия всего 0.2 мм это не очень комфортно. Не знаю, можно ли сделать что-то сильно лучше в домашних условиях, не заказывая кастомную мембранную клавиатуру.

  • Что хотелось бы из нового функционала. Более удобное отображение значений с неопределённостью, когда есть ненулевой порядок, в виде (10.1)e-10 вместо теперешнего 1e-10 1e-11. Больше регистров памяти (пока только один). Целочисленный режим с булевыми функциями и переводом между двоичной, десятичной и шестнадцатеричной системами. Новые функции по мере надобности (вычисление распределений Пуассона и хи-квадрат пока не сделал, но это дело техники).

По мере работы над проектом, я выкладываю новости в блог Hackaday.io.

P.S. Спасибо @Boomburumза приглашение и советы.

Подробнее..

Запуск QT на STM32. Часть 2. Теперь с псевдо 3d и тачскрином

05.04.2021 20:15:29 | Автор: admin
Мы в проекте Embox некоторое время назад запустили Qt на платформе STM32. Примером было приложение moveblocks анимация с четырьмя синими квадратами, которые перемещаются по экрану. Нам захотелось большего, например, добавить интерактивность, ведь на плате доступен тачскрин. Мы выбрали приложение animatedtiles просто потому, что оно и на компьютере круто смотрится. По нажатию виртуальных кнопок множество иконок плавно перемещаются по экрану, собираясь в различные фигуры. Причем выглядит это вполне как 3d анимация и у нас даже были сомнения, справится ли микроконтроллер с подобной задачей.

Сборка


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

Первый запуск на плате


Размер экрана у STM32F746G-Discovery 480x272, при запуске приложение нарисовалось только в верхнюю часть экрана. Нам естественно захотелось выяснить в чем дело. Конечно можно уйти в отладку прямо на плате, но есть более простое решение. запустить приложение на Линукс с теми же самыми размерами 480x272 с виртуальным фреймбуфером QVFB.

Запускаем на Линукс


Для запуска на Linux нам потребуется три части QVFB, библиотека Qt, и само приложение.

QVFB это обычное приложение, которое предоставит нам виртуальный экран для работы Qt. Собираем его как написано в официальной документации.

Запускаем с нужным размером экрана:
./qvfb -width 480 -height 272 -nocursor


Далее, собираем библиотеку Qt как embedded, т.е. С указанием опции -embedded. Я еще отключил разные модули для ускорения сборки, в итоге конфигурация выглядела вот так:
./configure -opensource -confirm-license -debug \    -embedded -qt-gfx-qvfb -qvfb \    -no-javascript-jit -no-script -no-scripttools \    -no-qt3support -no-webkit -nomake demos -nomake examples


Далее собираем приложение animatedtiles (qmake + make). И запускаем скомпилированное приложение, указав ему наш QVFB:
./examples/animation/animatedtiles/animatedtiles -qws -display QVFb:0


После запуска я увидел, что на Линуксе также рисуется только в часть экрана. Я немного доработал animatedtiles, добавив опцию -fullscreen, при указании которой приложение стартует в полноэкранном режиме.

Запуск на Embox


Модифицированный исходный код приложения будем использовать в Embox. Пересобираем и запускаем. Приложение не запустилось, при этом появились сообщения о нехватке памяти в Qt. Смотрим в конфигурацию Embox и находим что размер кучи установлен 2Мб и его явно не хватает. Опять же, можно попробовать выяснить этот момент с прямо на плате, но давайте сделаем это с удобствами в Линукс.

Для этого запускаем приложение следующим образом:
$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


В файле animatedtiles.out видим максимальное значение заполненности кучи порядка 2.7 Мб. Отлично, теперь можно не гадать, а вернуться в Embox и поставить размер кучи 3Мб.

Animatedtiles запустилось.

Запуск на STM32F769I-Discovery.


Давайте попробуем еще усложнить задачу, и запустим тот же пример на подобном микроконтроллере, но только с большим разрешением экрана STM32F769I-Discovery (800x480). То есть теперь под фреймбуфер потребуется в 1.7 раз больше памяти (напомню, что у STM32F746G экран 480x272), но это компенсируется в два раза большим размером SDRAM (16 Мб против 8Мб доступной памяти SDRAM у STM32F746G).

Для оценки размера кучи, как и выше, сначала запускаем Qvfb и наше приложение на Линуксе:
$ ./qvfb -width 800 -height 480 -nocursor &$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


Смотрим расход памяти в куче около 6 МБ (почти в два раза больше, чем на STM32F746G).

Осталось выставить нужный размер кучи в mods.conf и пересобрать. Приложение запустилось сразу и без проблем, что и продемонстрировано этом коротком видео


Традиционно можно воспроизвести результаты самостоятельно. Как это сделать описано у нас на wiki.

Данная статья впервые была нами опубликована на английском языке на embedded.com.
Подробнее..

Работа с параметрами в EEPROM

02.06.2021 22:14:13 | Автор: admin

Введение

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

Смотря на то, как студенты делают свои курсовые, я стараюсь замечать моменты, которые вызывают у них затруднения. Одним из таких моментов является работа с внешним EEPROM. Это то место, где хранятся пользовательские настройки и другая полезная информация, которая не должна быть уничтожена после выключения питания. Самый простой пример - изменение единиц измерения. Пользователь жмет на кнопку и меняет единицы измерения. Ну или записывает коэффициенты калибровки через какой-нибудь внешний протокол, типа Модбаса.

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

int address = 0;float val1 = 123.456f;byte val2 = 64;char name[10] = "Arduino";EEPROM.put(address, val1);address += sizeof(val1); //+4EEPROM.put(address, val2);address += sizeof(val2); //+1EEPROM.put(address, name);address += sizeof(name); //+10

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

Кроме того, обычно студенты используют РТОС, а потому нужно понимать, что обращение к EEPROM из разных потоков может привести либо к фейлам, либо ко всяким там дедлокам. Поэтому если студент использует EEPROM, я вначале прошу нарисовать дизайн, чтобы показать как он собирается работать этой подсистемой.

Обычно все сводится к двум вариантам:

  • Доступ к EEPROM только из одного места. Типа такой EepromManager , который запускается в отдельной задаче и проходится по списку кешеруемых EEPROM параметров и смотрит, было ли в них изменение, и если да, то пишет его в EEPROM.

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

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

  • Второй способ - пишем всегда сразу по месту.

    Плюс в том, что пользователь всегда получает достоверный ответ. Мы не задумываясь пишем параметр в EEPROM там где надо, и это выглядит просто.

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

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

Но в обоих этих способах, мы хотим, чтобы доступ к параметрам не был похож, на тот код с Ардуино, что я привел, а был простым и понятным, в идеале, чтобы было вообще так:

//Записываем 10.0F в EEPROM по адресу, где лежит myEEPROMData параметр myEEPROMData = 10.0F;

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

//Записываем в EEPROM строку из 5 символов по адресу параметра myStrDataauto returnStatus = myStrData.Set(tStr6{"Hello"}); if (!returnStatus){std::cout << "Ok"}//Записываем в EEPROM float параметр по адресу параметра myFloatDatareturnStatus = myFloatData.Set(37.2F); 

Ну что же приступим

Анализ требований и дизайн

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

Давайте поймем, что мы вообще хотим. Сформируем требования более детально:

  • Каждая наша переменная(параметр) должна иметь уникальный адрес в EEPROM

    • Мы не хотим руками задавать этот адрес, он должен высчитываться сам, на этапе компиляции, потому что мы не хотим, чтобы студент нечаянно задал неверный адрес и сбил все настройки

  • Мы не хотим постоянно лазить в EEPROM, когда пользователь хочет прочитать параметр

    • Обычно EEPROM подключается через I2C или SPI. Передача данных по этим интерфейсам тоже отнимает время, поэтому лучше кэшировать параметры в ОЗУ, и возвращать сразу копию из кеша.

  • При инициализации параметра, если не удалось прочитать данные с EEPROM, мы должны вернуть какое-то значение по умолчанию.

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

  • Все должно быть дружелюбным простым и понятным :)

Давайте прикинем дизайн класса, который будет описывать такой параметр и удовлетворять нашим требованиям: Назовем класс CaсhedNvData

CachedNvData

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

При вызове метода Init() мы должны полезть в EEPROM и считать оттуда нужный параметр с нужного адреса.

Адрес будет высчитываться на этапе компиляции, пока эту магию пропустим. Прочитанное значение хранится в data, и как только кому-то понадобится, оно возвращается немедленно из копии в ОЗУ с помощью метода Get().

А при записи, мы уже будем работать с EEPROM через nvDriver. Можно подсунуть любой nvDriver, главное, чтобы у него были методы Set() и Get(). Вот например, такой драйвер подойдет.

NvDriver

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

Например, если у нас есть 3 параметра:

//Длина параметра 6 байтconstexpr CachedNvData<NvVarList, tString6, myStrDefaultValue,  nvDriver> myStrData;//Длина параметра 4 байтаconstexpr CachedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData;//Длина параметра 4 байтconstexpr CachedNvData<NvVarList, std::uint32_t, myUint32DefaultValue,  nvDriver> myUint32Data; 

То когда мы сделаем какой-то такой список:

NvVarList<100U, myStrData, myFloatData, myUint32Data>

У нас бы у myStrData был бы адрес 100, у myFloatData - 106, а у myUint32Data - 110. Ну и соответственно список мог бы его вернуть для каждого из параметра.

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

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

Сделаем такой базовый класс, назовем его NvVarListBase:

NvVarListBase

В прицнипе то и все.

Код

А теперь самая простая часть - пишем код. Комментировать не буду, вроде бы и так понятно

CaсhedNvData

template<typename NvList, typename T, const T& defaultValue, const auto& nvDriver>class CaсhedNvData{  public:    ReturnCode Set(T value) const    {      //Ищем адрес EEPROM параметра в списке       constexpr auto address =                 NvList::template GetAddress<NvList,T,defaultValue,nvDriver>();      //Записываем новое значение в EEPROM      ReturnCode returnCode = nvDriver.Set(                                address,                                reinterpret_cast<const tNvData*>(&value), sizeof(T));      //Если значение записалось успешно, обновляем копию в ОЗУ      if (!returnCode)      {        memcpy((void*)&data, (void*)&value, sizeof(T));      }      return returnCode;    }    ReturnCode Init() const    {      constexpr auto address =                 NvList::template GetAddress<NvList,T,defaultValue,nvDriver>();      //Читаем значение из EEPROM      ReturnCode returnCode = nvDriver.Get(                                address,                                 reinterpret_cast<tNvData*>(&data), sizeof(T));      //Tесли значение не прочиталось из EEPROM, устанавливаем значение по умолчанию      if (returnCode)      {        data = defaultValue;      }      return returnCode;    }    T Get() const    {      return data;    }        using Type = T;  private:    inline static T data = defaultValue;};
template<const tNvAddress startAddress, const auto& ...nvVars>struct NvVarListBase{        template<typename NvList, typename T, const T& defaultValue, const auto& nvDriver>    constexpr static size_t GetAddress()    {       //Ищем EEPROM адрес параметра с типом       //CaсhedNvData<NvList, T, defaultValue, nvDriver>      using tQueriedType = CaсhedNvData<NvList, T, defaultValue, nvDriver>;                  return startAddress +             GetAddressOffset<tQueriedType>(NvVarListBase<startAddress,nvVars...>());    }      private:       template <typename QueriedType, const auto& arg, const auto&... args>       constexpr static size_t GetAddressOffset(NvVarListBase<startAddress, arg, args...>)   {    //Чтобы узнать тип первого аргумента в списке,     //создаем объект такого же типа как и первый аргумент    auto test = arg;    //если тип созданного объекта такой же как и искомый, то заканчиваем итерации    if constexpr (std::is_same<decltype(test), QueriedType>::value)    {        return  0U;    } else    {      //Иначе увеличиваем адрес на размер типа параметра и переходим к       //следующему параметру в списке.        return sizeof(typename decltype(test)::Type) +                 GetAddressOffset<QueriedType>(NvVarListBase<startAddress, args...>());    }  }    };

Использование

А теперь встанем не место студента и попробуем это все дело использовать.

Задаем начальные значения параметров:

using tString6 = std::array<char, 6U>;inline constexpr float myFloatDataDefaultValue = 10.0f;inline constexpr tString6 myStrDefaultValue = {"Habr "};inline constexpr std::uint32_t myUint32DefaultValue = 0x30313233;

Зададем сами параметры:

//поскольку список ссылается на параметры, а параметры на список. //Используем forward declarationstruct NvVarList;   constexpr NvDriver nvDriver;//Теперь можем использовать NvVarList в шаблоне EEPROM параметровconstexpr CaсhedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData;constexpr CaсhedNvData<NvVarList, tString6, myStrDefaultValue,  nvDriver> myStrData;constexpr CaсhedNvData<NvVarList, uint32_t, myUint32DefaultValue,  nvDriver> myUint32Data;

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

struct NvVarList : public NvVarListBase<0, myStrData, myFloatData, myUint32Data>{};

А теперь можем использовать наши параметры хоть где, очень просто и элементарно:

struct NvVarList;constexpr NvDriver nvDriver;using tString6 = std::array<char, 6U>;inline constexpr float myFloatDataDefaultValue = 10.0f;inline constexpr tString6 myStrDefaultValue = {"Habr "};inline constexpr uint32_t myUint32DefaultValue = 0x30313233;constexpr CaсhedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData;constexpr CaсhedNvData<NvVarList, tString6, myStrDefaultValue,  nvDriver> myStrData;constexpr CaсhedNvData<NvVarList, uint32_t, myUint32DefaultValue,  nvDriver> myUint32Data;struct NvVarList : public NvVarListBase<0, myStrData, myFloatData, myUint32Data>{};int main(){        myStrData.Init();    myFloatData.Init();    myUint32Data.Init()        myStrData.Get();    returnCode = myStrData.Set(tString6{"Hello"});    if (!returnCode)    {        std::cout << "Hello has been written" << std::endl;    }    myStrData.Get();    myFloatData.Set(37.2F);        myUint32Data.Set(0x30313233);        return 1;}

Можно передавать ссылку на них в любой класс, через конструктор или шаблон.

template<const auto& param>struct SuperSubsystem{  void SomeMethod()  {    std::cout << "SuperSubsystem read param" << param.Get() << std::endl;   }};int main(){    SuperSubsystem<myFloatData> superSystem;  superSystem.SomeMethod();}

Собственно и все. Теперь студенты могут работать с EEPROM более юзерфрендли и допускать меньше ошибок, ведь часть проверок за них сделает компилятор.

Ссылка на пример кода тут: https://godbolt.org/z/W5fPjh6ae

P.S Хотел еще рассказать про то, как можно реализовать драйвер работы с EEPROM через QSPI (студенты слишком долго понимали как он работает), но слишком разношерстный получался контекст, поэтому думаю описать это в другой статье, если конечно будет интересно.

Подробнее..

Power-line communication. Часть 3 Основные блоки устройства

25.05.2021 00:13:35 | Автор: admin

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

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

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

Zero cross детектор

Как говорилось ранее, передающие и принимающие устройства синхронизируются между собой с помощью отдельного блока zero cross детектора.

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

В электросетях с частотой 50 Гц, синусоида напряжения пересекает ноль 100 раз в секунду.

Есть несколько вариантов исполнения ZC детектора. Ниже я покажу пример реализации на оптопаре.

Начнём с конца схемы сначала представим, как сигнал с ZC детектора попадает на контроллер.

На картинке схема с подтягивающим pull-up резистором и ключом. При замыкании ключа, на вход МК будет подаваться логический 0, а при размыкании ключа, pull-up резистор будет подтягивать напряжение на входе МК до логической единицы.

На место ключа ставим оптрон. Оптрон (оптопара) это простой элемент, в котором с одной стороны светодиод, а с другой фототранзистор.

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

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

Для выпрямления можно использовать smd мостовой выпрямитель.

После выпрямления на фотодиод пойдёт пульсирующее с удвоенной частотой напряжение от 0 до 310 В. Разумеется диод не выдержит такого размаха напряжения, поэтому после мостового выпрямителя поставим сопротивление.

Номинал сопротивления можно вычислить исходя из характеристик фотодиода в оптопаре

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

Из datasheet на PLC817Из datasheet на PLC817

На примере PC817 видно, что максимальный ток, который выдержит светодиод - 50 мА. Максимальный коэффициент передачи при 20 мА. И "замыкать ключ" он будет уже и при >1 мА.

SMD резисторы типоразмера 1210 выдерживают рассеивание до 0.5 Вт мощности. Максимальный постоянный ток, который мы может пропускать при 310 вольт равен 0.5/310 = 0.00161 А. С учетом, того что у нас пульсирующее напряжение, округлим до 0.002 А (2 мА). Этого тока достаточно, чтобы "ключ замыкался". Номинал сопротивления при этом равен 310/0.002 = 155000 Ом. Итог: ставим последовательно три SMD резистора, типоразмером 1210, номиналом 51 кОм каждый.

В итоге, схема ZC детектора выглядит примерно так.

Теперь микроконтроллеры PLC устройств, подключенных к одной фазе могут синхронизироваться между собой с помощью сигнала на ножке "ZC input" из такого ZC детектора.

Схема согласования сигнальных цепей с линией 220 В

Схема согласования закрывает собой компоненты входной и выходной цепей. Входная и выходная сигнальные цепи обычно выполнены на микросхемах усилителях, которые питаются небольшим постоянным напряжением (3-12 В). Подключить их напрямую к 220 В не получится.

Из электросети должны проходить только высокочастотные сигналы. Основная гармоника 50 Гц, на которой передаётся электроэнергия, не должна попасть в сигнальные цепи устройства. Также в этой схеме обычно располагается защита от скачков напряжения и перегрузок.

Эта часть схемы принимает различный вид в разных datasheet на готовые PLC микросхемы. Опишем минимально работоспособный вариант.

Для первых опытов

Можно взять ферритовое кольцо типа 17,5x8,2x5 М2000Н, есть в любом магазине электроники. Провод МГТФ наматываем сразу 3 обмотки в 20 витков.

Конденсатор плёночный из серии MKP или любой аналогичный, который выдерживает от 220 В переменки (с запасом).

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

Для защиты усилителей от импульсных перенапряжений можно поставить защитные диоды (супрессоры) и/или варисторы с предохранителем. Тема защиты устройства от электрических неприятностей довольно обширная, в этой статье не рассматривается. Но забывать про это не стоит.

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

Входная цепь измерение полезного сигнала

Входная цепь должна выполнить как минимум две задачи:

  • отфильтровать грубый входящий сигнал, срезав все лишнее;

  • после этого усилить сигнал до приемлемого уровня, подходящего для измерения и оцифровки с помощью ЦАП микроконтроллера.

Фильтрация

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

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

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

Усиление

У АЦП есть такая характеристика как разрешение. Оцифровка сигнала происходит с некоторой дискретностью. А после пассивной фильтрации входной сигнал заметно похудеет в амплитуде. И если подать на АЦП очень слабый сигнал, то его форма сильно исказится и потеряется большая часть информации о сигнале.

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

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

Бывает, что одного ОУ не хватает для усиления сигнала до приемлемого уровня. В этом случае можно сразу после первого ОУ поставить второй по аналогичной схеме. В продаже есть микросхемы сразу с двумя ОУ в одном корпусе.

Ссылки на статьи про операционные усилители и их про каскадное подключение оставил в конце статьи.

Выходная цепь генерация полезного сигнала

Задача выходной цепи фильтровать и усиливать сигнал из ЦАП микроконтроллера.

Микроконтроллер по специальному алгоритму генерирует полезный сигнал, нужной длительности и частоты, соответствующей передаваемому символу. На выходе из ЦАП у нас получается просто болванка полезного сигнала, угловатая, примерно похожая на синусоиду, но (самое главное!) нужной нам частоты.

Далее сигнал сглаживается фильтром и отправляется в аналоговую часть схемы (усилитель и схема согласования с 220 В).

Можно подумать, что форма сигнала не особо важна при кодировании, так как преобразование Фурье всё равно может вычленить основную гармонику полезного сигнала, отбросив всё лишнее. Но чем сигнал ближе по форме к синусоиде, тем меньше энергии мы будем тратить в пустоту, просто добавляя высокочастотный шум в сеть. И выходной усилитель будет работать стабильнее. Как уже говорилось на входе важна лишь основная гармоника сигнала. Остальные гармоники это шум.

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

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

Гуглить их можно по фразе audio amplifier btl 1w. Но тут нужно учесть, что они обычно рассчитаны на аудио сигналы до 20 кГц, и производитель не рассчитывал, что их будут использовать в PLC модеме. Есть модели, которые хорошо усиливают частоты до 100-150 кГц, и обычно в datasheet об этом не пишут.

Плюсы:

  • они очень удобны тем что там встроенная стабилизация сигнала;

  • есть режим mute - мизерное потребление в режиме простоя;

  • хватает однополярного питания не надо париться с блоком питания.

Минусы:

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

  • большой минус это их незащищённость от импульсных помех в электросети. Сгорают мгновенно. Но от этого можно спастись, поставив на выходе усилителя супрессоры, что-то наподобие P4SMAJ5.0A или аналогичный.

Примерно так выглядит усиление с однополярным питанием.

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

Итого

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

В следующей части статьи планировал на примерах показать, как можно программно генерировать синус нужной частоты для ЦАП в STM32. И заодно как обработать приходящий на АЦП сигнал и выяснить наличие в нём нужных гармоник (частот) полезного сигнала.


Полезные ссылки

Общее:

Фильтры:

Операционные усилители:

ZC детекторы:

Схемы согласования с 220 В в доках на PLC микросхемы:

Подробнее..

Stm32 USB на шаблонах C. Продолжение. Делаем HID

27.03.2021 20:12:03 | Автор: admin

В прошлый раз показал один из способов распределение ресурсов между конечными точками, а именно регистров EPnR, памяти под дескрипторы буферов и под сами буферы. Предлагаю продолжить начатое и рассмотреть написанную библиотеку на примере создания простого HID-устройства, позволяющего управлять светодиодом.

Разделение прерывания

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

using EpRequestHandler = std::add_pointer_t<void()>;template<typename...>class EndpointHandlersBase;template<typename... Endpoints, int8_t... Indexes>class EndpointHandlersBase<TypeList<Endpoints...>, Int8_tArray<Indexes...>>{public:  // Массив указателей на обработчики  static constexpr EpRequestHandler _handlers[] = {Endpoints::Handler...};  // Индексы обработчиков  static constexpr int8_t _handlersIndexes[] = {Indexes...};public:  inline static void Handle(uint8_t number, EndpointDirection direction)  {    _handlers[_handlersIndexes[2 * number + (direction == EndpointDirection::Out ? 1 : 0)]]();  }};

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

template<int8_t Index, typename Endpoints>class EndpointHandlersIndexes{  // Предикат для поиска очередной конечной точки.  using Predicate = Select<Index % 2 == 0, IsTxOrBidirectionalEndpointWithNumber<Index / 2>, IsRxOrBidirectionalEndpointWithNumber<Index / 2>>::value;  static const int8_t EndpointIndex = Search<Predicate::template type, Endpoints>::value;public:  // В конец массива индекса вставляется номер соответствующей конечной точки или -1 в случае пропуска.  using type = typename Int8_tArray_InsertBack<typename EndpointHandlersIndexes<Index - 1, Endpoints>::type, EndpointIndex>::type;};template<typename Endpoints>class EndpointHandlersIndexes<-1, Endpoints>{public:  using type = Int8_tArray<>;};

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

Класс конечной точки

Из-за необходимости распределить ресурсы между конечными точками пришлось разделить код на два класса: базовый, который и нужно инстанцировать:

template <uint8_t _Number, EndpointDirection _Direction, EndpointType _Type, uint16_t _MaxPacketSize, uint8_t _Interval>class EndpointBase...

А также расширенный, с адресами буферов и регистра (которые назначаются менеджером, описанным в прошлой статье). Из-за различий между конечными точками разных типов/направлений (например, у однонаправленной конечной точки типа Interrupt будет один буфер, а у двунаправленной или Bulk с двойной буферизацией - два) для каждого типа объявлен свой класс:

template <typename _Base, typename _Reg>class Endpoint : public _Base...template<typename _Base, typename _Reg, uint32_t _TxBufferAddress, uint32_t _TxCountRegAddress, uint32_t _RxBufferAddress, uint32_t _RxCountRegAddress>class BidirectionalEndpoint : public Endpoint<_Base, _Reg>...template<typename _Base, typename _Reg, uint32_t _Buffer0Address, uint32_t _Count0RegAddress, uint32_t _Buffer1Address, uint32_t _Count1RegAddress>class BulkDoubleBufferedEndpoint : public Endpoint<_Base, _Reg>

Конечная точка на текущий момент реализована простой: экспортирует метод инициализации (в котором заполняется регистр EPnR), метод заполнения дескриптора, методы управления битами регистра (Очистка битов CTR_TX/RX, установка битов TX/RX_STATUS), а также отправку данных.

Класс интерфейса

Следующей сущностью в иерархии является интерфейс, который, по сути (как я понимаю) есть просто контейнер для конечных точек, поэтому реализующий его класс очень простой (прикладываю код полностью, потому что здесь применяется мощь variadic-шаблонов, который позволил исключить лишние зависимости):

template <uint8_t _Number, uint8_t _AlternateSetting = 0, uint8_t _Class = 0, uint8_t _SubClass = 0, uint8_t _Protocol = 0, typename... _Endpoints>class Interface{public:  using Endpoints = Zhele::TemplateUtils::TypeList<_Endpoints...>;  static const uint8_t EndpointsCount = ((_Endpoints::Direction == EndpointDirection::Bidirectional ? 2 : 1) + ...);  static void Reset()  {    (_Endpoints::Reset(), ...);  }  static uint16_t FillDescriptor(InterfaceDescriptor* descriptor)  {    uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {      .Number = _Number,      .AlternateSetting = _AlternateSetting,      .EndpointsCount = EndpointsCount,      .Class = _Class,      .SubClass = _SubClass,      .Protocol = _Protocol    };        EndpointDescriptor* endpointsDescriptors = reinterpret_cast<EndpointDescriptor*>(++descriptor);    totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + ...);    return totalLength;  }};

Класс конфигурации

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

template <uint8_t _Number, uint8_t _MaxPower, bool _RemoteWakeup = false, bool _SelfPowered = false, typename... _Interfaces>class Configuration{public:  using Endpoints = Zhele::TemplateUtils::Append_t<typename _Interfaces::Endpoints...>;  static void Reset()  {    (_Interfaces::Reset(), ...);  }...

Класс устройства

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

template<  typename _Regs,  IRQn_Type _IRQNumber,  typename _ClockCtrl,   uint16_t _UsbVersion,  DeviceClass _Class,  uint8_t _SubClass,  uint8_t _Protocol,  uint16_t _VendorId,  uint16_t _ProductId,  uint16_t _DeviceReleaseNumber,  typename _Ep0,  typename... _Configurations>class DeviceBase : public _Ep0{  using This = DeviceBase<_Regs, _IRQNumber, _ClockCtrl, _UsbVersion, _Class, _SubClass, _Protocol, _VendorId, _ProductId, _DeviceReleaseNumber, _Ep0, _Configurations...>;  using Endpoints = Append_t<typename _Configurations::Endpoints...>;  using Configurations = TypeList<_Configurations...>;  // Replace Ep0 with this for correct handler register.  using EpBufferManager = EndpointsManager<Append_t<_Ep0, Endpoints>>;  using EpHandlers = EndpointHandlers<Append_t<This, Endpoints>>;...

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

static void CommonHandler(){  if(_Regs()->ISTR & USB_ISTR_RESET)  {    Reset();  }  if (_Regs()->ISTR & USB_ISTR_CTR)  {    uint8_t endpoint = _Regs()->ISTR & USB_ISTR_EP_ID;    EpHandlers::Handle(endpoint, ((_Regs()->ISTR & USB_ISTR_DIR) != 0 ? EndpointDirection::Out : EndpointDirection::In));  }  NVIC_ClearPendingIRQ(_IRQNumber);}

Являясь одновременно и нулевой конечной точкой, класс Device содержит обработчик обращений к нулевой точке, реализуя, на данный момент, процесс нумерации:

Обработчик прерывания нулевой конечной точки
static void Handler(){  if(_Ep0::Reg::Get() & USB_EP_CTR_RX)  {    _Ep0::ClearCtrRx();    if(_Ep0::Reg::Get() & USB_EP_SETUP)    {      SetupPacket* setup = reinterpret_cast<SetupPacket*>(_Ep0::RxBuffer);      switch (setup->Request) {      case StandartRequestCode::GetStatus: {        uint16_t status = 0;        _Ep0::Writer::SendData(&status, sizeof(status));        break;      }      case StandartRequestCode::SetAddress: {        TempAddressStorage = setup->Value;        _Ep0::Writer::SendData(0);        break;      }      case StandartRequestCode::GetDescriptor: {        switch (static_cast<GetDescriptorParameter>(setup->Value)) {        case GetDescriptorParameter::DeviceDescriptor: {          DeviceDescriptor tempDeviceDescriptor;          FillDescriptor(reinterpret_cast<DeviceDescriptor*>(&tempDeviceDescriptor));          _Ep0::Writer::SendData(&tempDeviceDescriptor, setup->Length < sizeof(DeviceDescriptor) ? setup->Length : sizeof(DeviceDescriptor));          break;        }        case GetDescriptorParameter::ConfigurationDescriptor: {          uint8_t temp[64];          uint16_t size = GetType<0, Configurations>::type::FillDescriptor(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]));          _Ep0::Writer::SendData(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]), setup->Length < size ? setup->Length : size);          break;        }        case GetDescriptorParameter::HidReportDescriptor: {          uint16_t size = sizeof(GetType_t<0, Configurations>::HidReport::Data);          _Ep0::Writer::SendData(GetType_t<0, Configurations>::HidReport::Data, setup->Length < size ? setup->Length : size);          break;        }        default:          _Ep0::SetTxStatus(EndpointStatus::Stall);          break;        }        break;      }      case StandartRequestCode::GetConfiguration: {        uint16_t configuration = 0;        _Ep0::Writer::SendData(&configuration, 1);        break;      }      case StandartRequestCode::SetConfiguration: {        _Ep0::Writer::SendData(0);        break;      }      default:        _Ep0::SetTxStatus(EndpointStatus::Stall);        break;      }    }    _Ep0::SetRxStatus(EndpointStatus::Valid);  }  if(_Ep0::Reg::Get() & USB_EP_CTR_TX)  {    _Ep0::ClearCtrTx();    if(TempAddressStorage != 0)    {      _Regs()->DADDR = USB_DADDR_EF | (TempAddressStorage & USB_DADDR_ADD);      TempAddressStorage = 0;    }    _Ep0::SetRxStatus(EndpointStatus::Valid);  }}

Интерфейс HID

HID-устройство - это устройство как минимум с одним интерфейсом типа HID, поэтому в библиотеке класс HID - это производный от интерфейса:

Класс интерфейса hid
template <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Hid, typename... _Endpoints>class HidInterface : public Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>{  using Base = Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>;public:  using Endpoints = Base::Endpoints;  static uint16_t FillDescriptor(InterfaceDescriptor* descriptor)  {    uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {      .Number = _Number,      .AlternateSetting = _AlternateSetting,      .EndpointsCount = Base::EndpointsCount,      .Class = 0x03,      .SubClass = _SubClass,      .Protocol = _Protocol    };    _Hid* hidDescriptor = reinterpret_cast<_Hid*>(++descriptor);    *hidDescriptor = _Hid {    };    uint8_t* reportsPart = reinterpret_cast<uint8_t*>(++hidDescriptor);    uint16_t bytesWritten = _Hid::FillReports(reportsPart);    totalLength += sizeof(_Hid) + bytesWritten;    EndpointDescriptor* endpointsDescriptors = reinterpret_cast<EndpointDescriptor*>(&reportsPart[bytesWritten]);    totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + ...);    return totalLength;  }private:};

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

HID-устройство

Теперь давайте из всего этого сделаем устройство, которое будет содержать один светодиод (потому что так удобно, он есть на плате BluePill) и поддерживать возможность управления этим светодиодом с компьютера (через USB HID Demonstrator).

Основной любого HID-устройства является Report, определяющий порядок взаимодействия. В нашем случае он будет достаточно простым:

using Report = HidReport<  0x06, 0x00, 0xff,    // USAGE_PAGE (Generic Desktop)  0x09, 0x01,          // USAGE (Vendor Usage 1)  0xa1, 0x01,          // COLLECTION (Application)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x15, 0x00,          //   LOGICAL_MINIMUM (0)  0x25, 0x01,          //   LOGICAL_MAXIMUM (1)  0x75, 0x08,          //   REPORT_SIZE (8)  0x95, 0x01,          //   REPORT_COUNT (1)  0xb1, 0x82,          //   FEATURE (Data,Var,Abs,Vol)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x91, 0x82,          //   OUTPUT (Data,Var,Abs,Vol)  0xc0                 // END_COLLECTION>;

Далее нужно определить всю ирерахию: конечные точки, интерфейс, конфигурацию и устройство:

using HidDesc = HidDescriptor<0x1001, Report>;using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;using Config = HidConfiguration<0, 250, false, false, Report, Hid>;using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

В общем-то всё, осталось написать обработчик для конечной точки управления светодиодом:

using Led = IO::Pc13Inv; // Inv - инвертированный.template<>void LedsControlEp::Handler(){  LedsControlEp::ClearCtrRx();  uint8_t* buffer = reinterpret_cast<uint8_t*>(LedsControlEp::Buffer);  bool needSet = buffer[1] != 0;  // Код почти целиком позаимствован из поста "STM32 и USB-HID  это просто".  // Не стал изменять его для удобной навигации.  switch(buffer[0])  {  case 1:    needSet ? Led::Set() : Led::Clear();    break;  }  LedsControlEp::SetRxStatus(EndpointStatus::Valid);}

Целиком файл main.c для Stm32f103 выглядит так (по-моему, достаточно компактно):

Полный код программы
#include <clock.h>#include <iopins.h>#include <usb.h>using namespace Zhele;using namespace Zhele::Clock;using namespace Zhele::IO;using namespace Zhele::Usb;using Report = HidReport<  0x06, 0x00, 0xff,        // USAGE_PAGE (Generic Desktop)  0x09, 0x01,          // USAGE (Vendor Usage 1)  0xa1, 0x01,          // COLLECTION (Application)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x15, 0x00,          //   LOGICAL_MINIMUM (0)  0x25, 0x01,          //   LOGICAL_MAXIMUM (1)  0x75, 0x08,          //   REPORT_SIZE (8)  0x95, 0x01,          //   REPORT_COUNT (1)  0xb1, 0x82,          //   FEATURE (Data,Var,Abs,Vol)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x91, 0x82,          //   OUTPUT (Data,Var,Abs,Vol)  0xc0               // END_COLLECTION>;using HidDesc = HidDescriptor<0x1001, Report>;using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;using Config = HidConfiguration<0, 250, false, false, Report, Hid>;using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;using Led = IO::Pc13Inv;void ConfigureClock();void ConfigureLeds();int main(){  ConfigureClock();  ConfigureLeds();  Zhele::IO::Porta::Enable();  MyDevice::Enable();  for(;;)  {  }}void ConfigureClock(){  PllClock::SelectClockSource(PllClock::ClockSource::External);  PllClock::SetMultiplier(9);  Apb1Clock::SetPrescaler(Apb1Clock::Div2);  SysClock::SelectClockSource(SysClock::Pll);  MyDevice::SelectClockSource(Zhele::Usb::ClockSource::PllDividedOneAndHalf);}void ConfigureLeds(){  Led::Port::Enable();  Led::SetConfiguration<Led::Configuration::Out>();  Led::SetDriverType<Led::DriverType::PushPull>();  Led::Set();}template<>void LedsControlEp::Handler(){  LedsControlEp::ClearCtrRx();  uint8_t* buffer = reinterpret_cast<uint8_t*>(LedsControlEp::Buffer);  bool needSet = buffer[1] != 0;  switch(buffer[0])  {  case 1:    needSet ? Led::Set() : Led::Clear();    break;  }  LedsControlEp::SetRxStatus(EndpointStatus::Valid);}extern "C" void USB_LP_IRQHandler(){  MyDevice::CommonHandler();}

Заключение

Не совсем очевидная реализация библиотечного кода (в прошлой статье получил заслуженные комментарии в стиле "Не хотел бы увидеть такой код в продакшне", "Как это поддерживать" и т.п.) позволила максимально упростить непосредственно реализацию устройства, не нужно даже вручную объявлять дескрипторы: все генерируется из подставленных в шаблоны аргументов. Использование variadic-шаблонов помогло избавиться от лишних зависимостей. Прошивка тоже получается компактной, код из примера выше с оптимизацией Og вышел в 2360 байтов Flash и 36 байтов RAM (с оптимизацией Os прошивка весит 1712 байтов, но не работает. Пока не разобрался, почему именно), что я считаю неплохим результатом.

Благодарности

За замечательный пост про HID благодарен @RaJa. Также менее, чем за неделю до написания этого поста вышел еще крутой материал по HID от @COKPOWEHEU. Без этих постов я бы ничего не осилил. Еще большую помощь оказали пользователи с форума radiokot (COKPOWEHEU и VladislavS), был приятно удивлен оперативностью ответов и желанием помочь.

Подробнее..

Разработка firmware на С словно игра в бисер. Как перестать динамически выделять память и начать жить

07.04.2021 10:06:45 | Автор: admin

C++ is a horrible language. It's made more horrible by the fact that a lotof substandard programmers use it, to the point where it's much mucheasier to generate total and utter crap with it.

Linus Benedict Torvalds

Собеседование шло уже второй час. Мы наконец-то закончили тягучее и вязкое обсуждение моей скромной персоны, и фокус внимания плавно переполз на предлагаемый мне проект. Самый бойкий из трех моих собеседников со знанием дела и без лишних деталей принялся за его описание. Говорил он быстро и уверенно явно повторяет весь этот рассказ уже не первый раз. По его словам, работа велась над неким чрезвычайно малым, но очень важным устройством на базе STM32L4. Потребление энергии должно быть сведено к минимуму... USART... SPI... ничего необычного, уже неоднократно слышал подобное. После нескольких убаюкивающих фраз собеседник внезапно подался чуть вперед и, перехватив мой сонный взгляд, не без гордости произнес:

А firmware мы пишем на C++! мой будущий коллега заулыбался и откинулся в кресле, ожидая моей реакции на свою провокативную эскападу.

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

У вас есть какие-то опасения? поспешил спросить он с искренней озабоченностью в голосе.

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

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

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

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

Да, я согласился участвовать в проекте. Но кто бы смел отказаться, придавленный столь весомыми аргументами?

IAR

Так уж получилось, что мы впервые встретились на этом проекте. "Ну, это же специальный компилятор для железок", наивно думал я, "сработаемся". Не скажу, что я жестоко ошибся и проклял тот день, но использование именно этого компилятора доставляет определенный дискомфорт. Дело в том, что в проекте уже начали внедрение относительно нового стандарта С++17. Я уже потирал потные ладошки, представляя, как перепишу вон то и вот это, как станет невероятно красиво, но IAR может охладить пыл не хуже, чем вид нововоронежской Аленушки.

Новый стандарт реализован для нашего любимого коммерческого компилятора лишь частично, несмотря на все заверения о поддержке всех возможностей новейших стандартов. Например, structured binding declaration совсем не работает, сколько не уговаривай упрямца. Еще IAR весьма нежен и хрупок, какая-нибудь относительно сложная конструкция может довести его до истерики: компиляция рухнет из-за некой внутренней ошибки. Это самое неприятное, поскольку нет никаких подсказок, по какой причине все так неприятно обернулось. Такие провалы огорчают даже сильнее финала Игры престолов.

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

SIL

Для некоторых классов устройств существует такое понятие, как стандарты SIL. Safety integrity level уровень полноты безопасности, способность системы обеспечивать функциональную безопасность.

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

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

std::exception

Несмотря на утверждение Герба Саттера, что без исключений C++ уже перестает быть тем языком, который мы знаем и любим, они были беспощадно выпилены. Немудрено, ведь обычный механизм исключений использует динамическое выделение памяти, что недопустимо в нашем проекте.

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

__cxa_allocate_exception

Название у нее уже какое-то нехорошее, и действительно, выделяет память для объекта исключения и делает это весьма неприятным образом прямо в куче. Вполне возможно эту функцию подменить на собственную реализацию и работать со статическим буфером. Если не ошибаюсь, то в руководстве для разработчиков autosar для с++14 так и предлагают делать. Но есть нюансы. Для разных компиляторов реализация может отличаться, нужно точно знать, что делает оригинальная функция, прежде чем грубо вмешиваться в механизм обработки. Проще и безопаснее от исключений отказаться вовсе.Что и было сделано, и соответствующий флаг гордо реет теперь над компилятором! Только вот стандартную библиотеку нужно будет использовать вдвойне осторожнее, поскольку пересобрать ее с нужными опциями под IAR возможности нет.

std::vector

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

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

template <class T, std::size_t Size>class StaticArray { using ssize_t = int;public: using value_type = T; template <class U> struct rebind {   using other = StaticArray<U, Size>; }; StaticArray() = default; ~StaticArray() = default; template <class U, std::size_t S> StaticArray(const StaticArray<U, S>&); auto allocate(std::size_t n) -> value_type*; auto deallocate(value_type* p, std::size_t n) -> void; auto max_size() const -> std::size_t;};

Ключевые функции, конечно, allocate и deallocate. Передаваемый им параметр n это не размер в байтах, а размер в попугаях, которые хранятся в векторе. Функция max_size используется при проверке вместимости аллокатора и возвращает максимально возможное теоретически число, которое можно передать в функцию allocate.

Тут очевиднейший пример использования аллокатора
std::vector<int, StaticArray<int, 100>> v;    v.push_back(1000);std::cout<<"check size "<<v.size()<<std::endl;    v.push_back(2000);std::cout<<"check size "<<v.size()<<std::endl;

Результат выполнения такой программы (скомпилировано GCC) будет следующий:

max_size() -> 100

max_size() -> 100

allocate(1)

check size 1

max_size() -> 100

max_size() -> 100

allocate(2)

deallocate(1)

check size 2

deallocate(2)

std::shared_ptr

Умные указатели, безусловно, хорошая вещь, но нужная ли в bare metal? Требование безопасности, запрещающее динамическую аллокацию памяти, делает использование умных указателей в этой области крайне сомнительным мероприятием.

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

template <class Element,           std::size_t Size,           class SharedWrapper = Element>class StaticSharedAllocator {  public:  static constexpr std::size_t kSize = Size;  using value_type = SharedWrapper;  using pool_type = StaticPool<Element, kSize>;  pool_type &pool_;  using ElementPlaceHolder = pool_type::value_type;  template <class U>  struct rebind {    using other = StaticSharedAllocator<Element, kSize, U>;  };  StaticSharedAllocator(pool_type &pool) : pool_{pool} {}  ~StaticSharedAllocator() = default;  template <class Other, std::size_t OtherSize>  StaticSharedAllocator(const StaticSharedAllocator<Other, OtherSize> &other)     : pool_{other.pool_} {}  auto allocate(std::size_t n) -> value_type * {    static_assert(sizeof(value_type) <= sizeof(ElementPlaceHolder));    static_assert(alignof(value_type) <= alignof(ElementPlaceHolder));    static_assert((alignof(ElementPlaceHolder) % alignof(value_type)) == 0u);      return reinterpret_cast<value_type *>(pool_.allocate(n));  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(reinterpret_cast<value_type *>(p), n);  }};

Очевидно, Element тип целевого объекта, который и должен храниться как разделяемый объект. Size максимальное число объектов данного типа, которое можно создать через аллокатор. SharedWrapper это тип объектов, которые будут храниться в контейнере на самом деле!

Конечно, вы знаете, что для работы shared_ptr необходима некоторая дополнительная информация, которую нужно где-то хранить, лучше прямо с целевым объектом вместе. Поэтому для этого аллокатора очень важна структура rebuild. Она используется в недрах стандартной библиотеки, где-то в районе alloc_traits.h, чтобы привести аллокатор к виду, который необходим для работы разделяемого указателя:

using type = typename _Tp::template rebind<_Up>::other;

где _Tp это StaticSharedAllocator<Element, Size>,

_Up это std::_Sp_counted_ptr_inplace<Object, StaticSharedAllocator<Element, Size>, __gnu_cxx::_S_atomic>

К сожалению, это верно только для GCC, в IAR тип будет немного другой, но общий принцип неизменен: нам нужно сохранить немного больше информации, чем содержится в Element. Для простоты тип целевого объекта и расширенный тип должны быть сохранены в шаблонных параметрах. Как вы уже догадались, SharedWrapper и будет расширенным типом, с которым непосредственно работает shared_ptr.

Лично я не нашел ничего лучше, чем просто резервировать немного больше места в хранилище или пулле объектов. Размер дополнительного пространства подобран эмпирически, ибо разным компиляторам нужен разный объем дополнительной памяти.

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

Еще немного кода для иллюстрации

Сам пул объектов основан на StaticArray аллокаторе. А чего добру пропадать?

template <class Type, size_t Size>struct StaticPool {  static constexpr size_t kSize = Size;  static constexpr size_t kSizeOverhead = 48;  using value_type = std::aligned_storage_t<sizeof(Type)+kSizeOverhead,                                             alignof(std::max_align_t)>;  StaticArray<value_type, Size> pool_;    auto allocate(std::size_t n) -> value_type * {    return pool_.allocate(n);  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(p, n);  }};

А теперь небольшой пример, как это все работает вместе:

struct Object {  int index;};constexpr size_t kMaxObjectNumber = 10u;StaticPool<Object, kMaxObjectNumber> object_pool {};StaticSharedAllocator<Object, kMaxObjectNumber> object_alloc_ {object_pool};std::shared_ptr<Object> MakeObject() {  return std::allocate_shared<Object>(object_alloc_);}

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

std::function

Универсальная полиморфная обертка над функциями или функциональными объектами. Очень удобная штука. Точно была бы полезна в embedded проекте, хотя бы для каких-нибудь функций обратного вызова (callbacks).

Чем мы платим за универсальность?

Во-первых, std::function может использовать динамическую аллокацию памяти.

Небольшой и несколько искусственный пример:

int x[] = {1, 2, 3, 4, 5};    auto sum = [=] () -> int {      int sum = x[0];      for (size_t i = 1u; i < sizeof(x) / sizeof(int); i++) {        sum += x[i];      }      return sum;    };        std::function<int()> callback = sum; 

Когда элементов массива 5, то размер функции 20 байт. В этом случае, когда мы присваиваем переменной callback экземпляр нашей лямбда-функции, будет использована динамическая аллокация.

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

Любая функция в С++ может быть определена с помощью двух указателей максимум. Для свободных функций или функторов достаточно одного указателя, если нужно вызвать метод класса, то нужен указатель на объект и смещение внутри класса. Собственно, у нас есть небольшое укромное местечко для пары указателей. Конечно, небольшие функциональные объекты можно хранить прямо на месте этих указателей! Если размер лямбды, например, не позволяет целиком запихать ее туда, то на помощь снова придет динамическая аллокация.

Для GCC

Опции -specs=nano.specs уже не будет хватать для std::function.

Сразу появится сообщения подобного вида:

abort.c:(.text.abort+0xa): undefined reference to _exit

signalr.c:(.text.killr+0xe): undefined reference to _kill

signalr.c:(.text.getpidr+0x0): undefined reference to _getpid

Правильно, ведь пустая функция должна бросать исключение.

Нужна другая опция -specs=nosys.specs, где включены все необходимые заглушки для всяких системных функций.

Соберем небольшую прошивку, чтоб проверить как повлияет включение std::function на потребление памяти различных видов. Прошивка стандартный пример от ST для подмигивающего светодиода. Изменения в размере секций файла-прошивки в таблице:

text

data

bss

67880

2496

144

Невооруженным взглядом видно, что секция .text выросла просто фантастически (на 67Кб!). Как одна функция могла сделать такое?

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

Если заглянуть в получившийся elf-файл, то можно увидеть много новых символов. Отсортируем их по размеру и посмотрим на самые жирные.

00000440cplus_demangle_operators0000049e__gxx_personality_v0000004c4 d_encoding000004fed_exprlist00000574_malloc_r0000060cd_print_mod000007f0d_type00000eec_dtoa_r00001b36_svfprintf_r0000306cd_print_comp

Много функций с префиксом d_* функции из файла cp-demangle.c библиотеки libiberty, которая, как я понимаю, встроена в gcc, и не так просто выставить ее за дверь.

Также имеются функции для обработки исключений (bad_function_call, std::unexpected, std::terminate)

_sbrk, malloc, free функции для работы с динамическим выделением памяти.

Результат ожидаемый флаги -fno-exceptions и -fno-rtti не спасают.

Внедрим второй подобный функциональный объект в другой единице трансляции:

text

data

bss

67992

2504

144

Вторая std::function обошлась не так уж и дорого.

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

Для случая без std::function список короткий
libc_nano.alibg_nano.alibg_nano.a(lib_a-exit.o)libg_nano.a(lib_a-exit.o) (_global_impure_ptr)libg_nano.a(lib_a-impure.o)libg_nano.a(lib_a-init.o)libg_nano.a(lib_a-memcpy-stub.o)libg_nano.a(lib_a-memset.o)libgcc.alibm.alibstdc++_nano.a
Для случая с std::function список гораздо длиннее
libc.alibg.alibg.a(lib_a-__atexit.o)libg.a(lib_a-__call_atexit.o)libg.a(lib_a-__call_atexit.o) (__libc_fini_array)libg.a(lib_a-__call_atexit.o) (atexit)libg.a(lib_a-abort.o)libg.a(lib_a-abort.o) (_exit)libg.a(lib_a-abort.o) (raise)libg.a(lib_a-atexit.o)libg.a(lib_a-callocr.o)libg.a(lib_a-closer.o)libg.a(lib_a-closer.o) (_close)libg.a(lib_a-ctype_.o)libg.a(lib_a-cxa_atexit.o)libg.a(lib_a-cxa_atexit.o) (__register_exitproc)libg.a(lib_a-dtoa.o)libg.a(lib_a-dtoa.o) (_Balloc)libg.a(lib_a-dtoa.o) (__aeabi_ddiv)libg.a(lib_a-exit.o)libg.a(lib_a-exit.o) (__call_exitprocs)libg.a(lib_a-exit.o) (_global_impure_ptr)libg.a(lib_a-fclose.o)libg.a(lib_a-fflush.o)libg.a(lib_a-findfp.o)libg.a(lib_a-findfp.o) (__sread)libg.a(lib_a-findfp.o) (_fclose_r)libg.a(lib_a-findfp.o) (_fwalk)libg.a(lib_a-fini.o)libg.a(lib_a-fputc.o)libg.a(lib_a-fputc.o) (__retarget_lock_acquire_recursive)libg.a(lib_a-fputc.o) (__sinit)libg.a(lib_a-fputc.o) (_putc_r)libg.a(lib_a-fputs.o)libg.a(lib_a-fputs.o) (__sfvwrite_r)libg.a(lib_a-freer.o)libg.a(lib_a-fstatr.o)libg.a(lib_a-fstatr.o) (_fstat)libg.a(lib_a-fvwrite.o)libg.a(lib_a-fvwrite.o) (__swsetup_r)libg.a(lib_a-fvwrite.o) (_fflush_r)libg.a(lib_a-fvwrite.o) (_free_r)libg.a(lib_a-fvwrite.o) (_malloc_r)libg.a(lib_a-fvwrite.o) (_realloc_r)libg.a(lib_a-fvwrite.o) (memchr)libg.a(lib_a-fvwrite.o) (memmove)libg.a(lib_a-fwalk.o)libg.a(lib_a-fwrite.o)libg.a(lib_a-impure.o)libg.a(lib_a-init.o)libg.a(lib_a-isattyr.o)libg.a(lib_a-isattyr.o) (_isatty)libg.a(lib_a-locale.o)libg.a(lib_a-locale.o) (__ascii_mbtowc)libg.a(lib_a-locale.o) (__ascii_wctomb)libg.a(lib_a-locale.o) (_ctype_)libg.a(lib_a-localeconv.o)libg.a(lib_a-localeconv.o) (__global_locale)libg.a(lib_a-lock.o)libg.a(lib_a-lseekr.o)libg.a(lib_a-lseekr.o) (_lseek)libg.a(lib_a-makebuf.o)libg.a(lib_a-makebuf.o) (_fstat_r)libg.a(lib_a-makebuf.o) (_isatty_r)libg.a(lib_a-malloc.o)libg.a(lib_a-mallocr.o)libg.a(lib_a-mallocr.o) (__malloc_lock)libg.a(lib_a-mallocr.o) (_sbrk_r)libg.a(lib_a-mbtowc_r.o)libg.a(lib_a-memchr.o)libg.a(lib_a-memcmp.o)libg.a(lib_a-memcpy.o)libg.a(lib_a-memmove.o)libg.a(lib_a-memset.o)libg.a(lib_a-mlock.o)libg.a(lib_a-mprec.o)libg.a(lib_a-mprec.o) (_calloc_r)libg.a(lib_a-putc.o)libg.a(lib_a-putc.o) (__swbuf_r)libg.a(lib_a-readr.o)libg.a(lib_a-readr.o) (_read)libg.a(lib_a-realloc.o)libg.a(lib_a-reallocr.o)libg.a(lib_a-reent.o)libg.a(lib_a-s_frexp.o)libg.a(lib_a-sbrkr.o)libg.a(lib_a-sbrkr.o) (_sbrk)libg.a(lib_a-sbrkr.o) (errno)libg.a(lib_a-signal.o)libg.a(lib_a-signal.o) (_kill_r)libg.a(lib_a-signalr.o)libg.a(lib_a-signalr.o) (_getpid)libg.a(lib_a-signalr.o) (_kill)libg.a(lib_a-sprintf.o)libg.a(lib_a-sprintf.o) (_svfprintf_r)libg.a(lib_a-stdio.o)libg.a(lib_a-stdio.o) (_close_r)libg.a(lib_a-stdio.o) (_lseek_r)libg.a(lib_a-stdio.o) (_read_r)libg.a(lib_a-strcmp.o)libg.a(lib_a-strlen.o)libg.a(lib_a-strncmp.o)libg.a(lib_a-strncpy.o)libg.a(lib_a-svfiprintf.o)libg.a(lib_a-svfprintf.o)libg.a(lib_a-svfprintf.o) (__aeabi_d2iz)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpeq)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpun)libg.a(lib_a-svfprintf.o) (__aeabi_dmul)libg.a(lib_a-svfprintf.o) (__aeabi_dsub)libg.a(lib_a-svfprintf.o) (__aeabi_uldivmod)libg.a(lib_a-svfprintf.o) (__ssprint_r)libg.a(lib_a-svfprintf.o) (_dtoa_r)libg.a(lib_a-svfprintf.o) (_localeconv_r)libg.a(lib_a-svfprintf.o) (frexp)libg.a(lib_a-svfprintf.o) (strncpy)libg.a(lib_a-syswrite.o)libg.a(lib_a-syswrite.o) (_write_r)libg.a(lib_a-wbuf.o)libg.a(lib_a-wctomb_r.o)libg.a(lib_a-writer.o)libg.a(lib_a-writer.o) (_write)libg.a(lib_a-wsetup.o)libg.a(lib_a-wsetup.o) (__smakebuf_r)libgcc.alibgcc.a(_aeabi_uldivmod.o)libgcc.a(_aeabi_uldivmod.o) (__aeabi_ldiv0)libgcc.a(_aeabi_uldivmod.o) (__udivmoddi4)libgcc.a(_arm_addsubdf3.o)libgcc.a(_arm_cmpdf2.o)libgcc.a(_arm_fixdfsi.o)libgcc.a(_arm_muldf3.o)libgcc.a(_arm_muldivdf3.o)libgcc.a(_arm_unorddf2.o)libgcc.a(_dvmd_tls.o)libgcc.a(_udivmoddi4.o)libgcc.a(libunwind.o)libgcc.a(pr-support.o)libgcc.a(unwind-arm.o)libgcc.a(unwind-arm.o) (__gnu_unwind_execute)libgcc.a(unwind-arm.o) (restore_core_regs)libm.alibnosys.alibnosys.a(_exit.o)libnosys.a(close.o)libnosys.a(fstat.o)libnosys.a(getpid.o)libnosys.a(isatty.o)libnosys.a(kill.o)libnosys.a(lseek.o)libnosys.a(read.o)libnosys.a(sbrk.o)libnosys.a(write.o)libstdc++.alibstdc++.a(atexit_arm.o)libstdc++.a(atexit_arm.o) (__cxa_atexit)libstdc++.a(class_type_info.o)libstdc++.a(cp-demangle.o)libstdc++.a(cp-demangle.o) (memcmp)libstdc++.a(cp-demangle.o) (realloc)libstdc++.a(cp-demangle.o) (sprintf)libstdc++.a(cp-demangle.o) (strlen)libstdc++.a(cp-demangle.o) (strncmp)libstdc++.a(del_op.o)libstdc++.a(del_ops.o)libstdc++.a(eh_alloc.o)libstdc++.a(eh_alloc.o) (std::terminate())libstdc++.a(eh_alloc.o) (malloc)libstdc++.a(eh_arm.o)libstdc++.a(eh_call.o)libstdc++.a(eh_call.o) (__cxa_get_globals_fast)libstdc++.a(eh_catch.o)libstdc++.a(eh_exception.o)libstdc++.a(eh_exception.o) (operator delete(void*, unsigned int))libstdc++.a(eh_exception.o) (__cxa_pure_virtual)libstdc++.a(eh_globals.o)libstdc++.a(eh_personality.o)libstdc++.a(eh_term_handler.o)libstdc++.a(eh_terminate.o)libstdc++.a(eh_terminate.o) (__cxxabiv1::__terminate_handler)libstdc++.a(eh_terminate.o) (__cxxabiv1::__unexpected_handler)libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())libstdc++.a(eh_terminate.o) (__cxa_begin_catch)libstdc++.a(eh_terminate.o) (__cxa_call_unexpected)libstdc++.a(eh_terminate.o) (__cxa_end_cleanup)libstdc++.a(eh_terminate.o) (__gxx_personality_v0)libstdc++.a(eh_terminate.o) (abort)libstdc++.a(eh_throw.o)libstdc++.a(eh_type.o)libstdc++.a(eh_unex_handler.o)libstdc++.a(functional.o)libstdc++.a(functional.o) (std::exception::~exception())libstdc++.a(functional.o) (vtable for __cxxabiv1::__si_class_type_info)libstdc++.a(functional.o) (operator delete(void*))libstdc++.a(functional.o) (__cxa_allocate_exception)libstdc++.a(functional.o) (__cxa_throw)libstdc++.a(pure.o)libstdc++.a(pure.o) (write)libstdc++.a(si_class_type_info.o)libstdc++.a(si_class_type_info.o) (__cxxabiv1::__class_type_info::__do_upcast(__cxxabiv1::__class_type_info const*, void**) const)libstdc++.a(si_class_type_info.o) (std::type_info::__is_pointer_p() const)libstdc++.a(tinfo.o)libstdc++.a(tinfo.o) (strcmp)libstdc++.a(vterminate.o)libstdc++.a(vterminate.o) (__cxa_current_exception_type)libstdc++.a(vterminate.o) (__cxa_demangle)libstdc++.a(vterminate.o) (fputc)libstdc++.a(vterminate.o) (fputs)libstdc++.a(vterminate.o) (fwrite)

А что IAR?

Все устроено немного иначе. Он не требует явного указания спецификации nano или nosys, ему не нужны никакие заглушки. Этот компилятор все знает и сделает все в лучшем виде, не нужно ему мешать.

text

ro data

rw data

2958

38

548

О, добавилось всего-то каких-то жалких 3Кб кода! Это успех. Фанат GCC во мне заволновался, почему так мало? Смотрим, что же добавил нам IAR.

Добавились символы из двух новых объектных файлов:

dlmalloc.o 1'404 496

heaptramp0.o 4

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

Естественно, никаких выделений в куче нет, но IAR приготовился: видно, что он создал структуру gm (global malloc: a malloc_state holds all of the bookkeeping for a space) и некоторые функции для работы с этой структурой.

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

до

main.cpp.obj 3'218 412 36'924

после

main.cpp.obj 4'746 451 36'964

Файл прибавил более 1Кб. Появилась std::function, ее сопряжение с лямбдой, аллокаторы.

Добавление второго такого функционального объекта в другую единицу трансляции дает нам очередной прирост:

text

ro data

rw data

3 998

82

600

Прибавили более 1Кб. Т.е. каждая новая функция добавляет нам по килобайту кода в каждой единице трансляции. Это не слишком помогает экономить: в проекте не один и не два колбэка, больше десятка наберется. Хорошо, что большинство таких функций имеют сигнатуру void(*)(void) или void(*)(uint8_t *, int), мы можем быстро накидать свою реализацию std::function без особых проблем. Что я и сделал.

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

Дома меня поджидало письмо от коллеги, преисполненное благодарности. Он писал, что благодаря отказу от богомерзких std::function проект сильно схуднул, мы все молодцы! Сочившееся из меня самодовольство брызнуло во все стороны. Прилагался также классический рекламно-наглядный график до-после, вопивший об уменьшении размера отладочной версии прошивки аж на 30 процентов. В абсолютных величинах цифра была еще страшнее, это, на минуточку, целых 150 килобайт! Что-о-о-о? Улыбка довольного кота медленно отделилась от лица и стремительным домкратом полетела вниз, пробивая перекрытия. В коде просто нет столько колбэков, чтоб хоть как-то можно было оправдать этот странный феномен. В чем дело?

Смотря на сонное спокойствие темной улицы, раскинувшейся внизу, я твердо решил, что не сомкну глаз, пока не отыщу ответ. Проснувшись утром, в первую очередь сравнил два разных elf-файла: до и после замены std::function. Тут все стало очевидно!

В одном забытом богом и кем-то из разработчиков заголовочном файле были такие строчки:

using Handler = std::function<void()>;static auto global_handlers = std::pair<Handler, Handler> {};

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

Понятно, чего хотел добиться неизвестный мне автор, и это вполне могло получиться. Начиная с 17-го стандарта, в заголовочном файле можно разместить некие глобальные объекты, которые будут видны и в других единицах трансляции. Достаточно вместо static написать inline. Это работает даже для IAR. Впрочем, я не стал изменять себе и просто все убрал.

Вот тут я все же не удержатся от объяснения очевидных вещей

Если у вас несколько единиц трансляции и создание глобального объекта вынесено в заголовочный файл, то при сборке проекта вы неизбежно получите ошибку multiple definition. Ежели добавить static, как сделал неизвестный мне разработчик, то все пройдет гладко, но в итоге будет занято несколько участков памяти и от глобальности ничего не останется.

Давайте же наглядно продемонстрируем как можно получить несколько одинаковых символов в финальном файле. Ну, если кто-то еще сомневается, конечно.

// a.h

#pragma once

int a();

// a.cpp

#include "a.h"

#include "c.hpp"

int a() { return cglob * 2; }

// b.h

#pragma once

int b();

// b.cpp

#include "b.h"

#include "c.hpp"

int b() { return cglob * 4; }

// main.cpp

#include "a.h"

#include "b.h"

int main() { return a() + b(); }

// c.hpp

#pragma once

int c_glob = 0;

Пробуем собрать наш небольшой и бесполезный проект.

$ g++ a.cpp b.cpp main.cpp -o test

/usr/lib/gcc/x8664-pc-cygwin/10/../../../../x8664-pc-cygwin/bin/ld: /tmp/cccXOcPm.o:b.cpp:(.bss+0x0): повторное определение cglob; /tmp/ccjo1M9W.o:a.cpp:(.bss+0x0): здесь первое определение

collect2: ошибка: выполнение ld завершилось с кодом возврата 1

Неожиданно получаем ошибку. Так, теперь меняем содержимое файла c.hpp:

static int c_glob = 0;

Вот теперь все собирается! Полюбуемся на символы:

$ objdump.exe -t test.exe | grep glob | c++filt.exe

[ 48](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000000 c_glob

[ 65](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000010 c_glob

Вот и второй лишний символ, что и требовалось доказать.

А ежели изменить c.hpp таким образом:

inline int c_glob = 0;

Объект c_glob будет единственным, все единицы трансляции будут ссылаться на один и тот же объект.

Вывод будет весьма банален: нужно понимать, что делаешь... и соответствовать стандартам SIL!

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

Всем спасибо, всем удачи!

Подробнее..

Отладочный вывод на микроконтроллерах как Concepts и Ranges отправили мой printf на покой

09.05.2021 22:19:11 | Автор: admin

Здравствуйте! Меня зовут Александр и я работаю программистом микроконтроллеров.

Начиная на работе новый проект, я привычно набрасывал в project tree исходники всяческих полезных утилит. И на хедере app_debug.h несколько подзавис.

Дело в том, что в декабре прошлого года у GNU Arm Embedded Toolchain вышел релиз 10-2020-q4-major, включающий все GCC 10.2 features, а значит и поддержку Concepts, Ranges, Coroutines вкупе с другими, менее "громкими" новинками С++20.

Воодушевленное новым стандартом воображение рисовало мой будущий С++ код ультрасовременным и лаконично-поэтичным. И старый, добрый printf("Debug message\n") в это благостное видение не очень-то вписывался.

Хотелось бескомпромиссной плюсовой функциональности и стандартных удобств!

float raw[] = {3.1416, 2.7183, 1.618};array<int, 3> arr{123, 456, 789};cout << int{2021}       << '\n'     << float{9.806}    << '\n'     << raw             << '\n'     << arr             << '\n'     << "Hello, Habr!"  << '\n'     << ("esreveR me!" | views::take(7) | views::reverse ) << '\n';

Ну а если хочется хорошего, зачем же себе отказывать?

Реализуем на С++20 интерфейс потока для отладочного вывода МК, поддерживающий любой подходящий протокол, предусмотренный вендром камня. Легковесный и быстрый, без бойлерплейта. Поддерживающий как блокирующий посимвольный вывод - для нечувствительных к времени выполнения участков кода, так и неблокирующий, для быстрых функций.

Зададим для комфортного чтения кода несколько удобных алиасов:

using base_t = std::uint32_t;using fast_t = std::uint_fast32_t;using index_t = std::size_t;

Как известно, в микроконтроллерах неблокирующие алгоритмы передачи данных реализуются на прерываниях и DMA. Для идентификации режимов вывода заведем enum:

enum class BusMode{BLOCKING,IT,DMA,};

Опишем базовый класс, реализующий логику протоколов, ответственных за отладочный вывод:

class BusInterface
template<typename T>class BusInterface{public:using derived_ptr = T*;    static constexpr BusMode mode = T::mode;void send (const char arr[], index_t num) noexcept {if constexpr (BusMode::BLOCKING == mode){derived()->send_block(arr, num);} else if (BusMode::IT == mode){derived()->send_it(arr, num);} else if (BusMode::DMA == mode){derived()->send_dma(arr, num);}}private:derived_ptr derived(void) noexcept{return static_cast<derived_ptr>(this);}void send_block (const char arr[], const index_t num) noexcept {}void send_it (const char arr[], const index_t num) noexcept {}void send_dma (const char arr[], const index_t num) noexcept {}};

Класс реализован по паттерну CRTP, что дает нам преимущества полиморфизма времени компиляции. Класс содержит единственный публичный метод send(), в котором на этапе компиляции, в зависимости от режима вывода, выбирается нужный метод. В качестве аргументов метод принимает указатель на буфер с данными и его полезный размер. На моей практике это самый распространенный формат аргументов в HAL-функциях вендоров МК.

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

class Uart
template<BusMode Mode>class Uart final : public BusInterface<Uart<Mode>> {private:static constexpr BusMode mode = Mode;void send_block (const char arr[], const index_t num) noexcept{HAL_UART_Transmit(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num),base_t{5000});}    void send_it (const char arr[], const index_t num) noexcept {HAL_UART_Transmit_IT(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num));}void send_dma (const char arr[], const index_t num) noexcept {HAL_UART_Transmit_DMA(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num));}friend class BusInterface<Uart<BusMode::BLOCKING>>;friend class BusInterface<Uart<BusMode::IT>>;friend class BusInterface<Uart<BusMode::DMA>>;};

По аналогии можно реализовать классs и других протоколов, поддерживаемых микроконтроллером, заменив в методах send_block(), send_it() и send_dma() соответствующие функции HAL. Если протокол передачи данных поддерживает не все режимы, тогда соответствующий метод просто не определяем.

И в завершении этой части заведем короткие алиасы итогового класса Uart:

using UartBlocking = BusInterface<Uart<BusMode::BLOCKING>>;using UartIt = BusInterface<Uart<BusMode::IT>>;using UartDma = BusInterface<Uart<BusMode::DMA>>;

Отлично, теперь разработаем класс потока вывода:

class StreamBase
template <class Bus, char Delim>class StreamBase final: public StreamStorage{public:using bus_t = Bus;  using stream_t = StreamBase<Bus, Delim>;static constexpr BusMode mode = bus_t::mode;StreamBase() = default;~StreamBase(){ if constexpr (BusMode::BLOCKING != mode) flush(); }  StreamBase(const StreamBase&) = delete;StreamBase& operator= (const StreamBase&) = delete;stream_t& operator << (const char_type auto c){if constexpr (BusMode::BLOCKING == mode){bus.send(&c, 1);} else {*it = c;it = std::next(it);}return *this;}stream_t& operator << (const std::floating_point auto f){if constexpr (BusMode::BLOCKING == mode){auto [ptr, cnt] = NumConvert::to_string_float(f, buffer.data());bus.send(ptr, cnt);} else {auto [ptr, cnt] = NumConvert::to_string_float(f, buffer.data() + std::distance(buffer.begin(), it));it = std::next(it, cnt);}return *this;}stream_t& operator << (const num_type auto n){auto [ptr, cnt] = NumConvert::to_string_integer( n, &buffer.back() );if constexpr (BusMode::BLOCKING == mode){bus.send(ptr, cnt);} else {auto src = std::prev(buffer.end(), cnt + 1);it = std::copy(src, buffer.end(), it);}return *this;}stream_t& operator << (const std::ranges::range auto& r){        std::ranges::for_each(r, [this](const auto val) {                        if constexpr (char_type<decltype(val)>){                            *this << val;            } else if (num_type<decltype(val)> || std::floating_point<decltype(val)>){                *this << val << Delim;            }        });return *this;}private:void flush (void) {bus.send(buffer.data(), std::distance(buffer.begin(), it));it = buffer.begin();}std::span<char> buffer{storage};std::span<char>::iterator it{buffer.begin()};bus_t bus;}; 

Рассмотрим подробнее его значимые части.

Шаблон класса параметризуется классом протокола, значением Delim типа char и наследуется от класса StreamStorage. Единственная задача последнего - предоставить доступ к массиву char, в котором будут формироваться строки вывода в неблокирующем режиме. Имплементацию здесь не привожу, она вторична к рассматриваемой теме; оставляю на ваше усмотрение или утяните из моего примера в конце статьи. Для удобной и безопасной работы с этим массивом (в примере - storage) мы заведем два приватных члена класса:

std::span<char> buffer{storage};std::span<char>::iterator it{buffer.begin()};

Delim - разделитель между значениями чисел при выводе содержимого массивов/контейнеров.

Публичные методы класса - это четыре перегрузки operator<<. Три из них - для вывода базовых типов, с которыми наш интерфейс будет работать (char, float и integral type), а четвертая - для вывода содержимого массивов и стандартных контейнеров.

Вот здесь начинается самая вкуснота.

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

template <typename T>concept char_type = std::same_as<T, char>;template <typename T>concept num_type = std::integral<T> && !char_type<T>;

... и концепты из стандартной библиотеки - std::floating_point и std::ranges::range.

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

Логика внутри каждого оператора вывода базового типа проста. В зависимости от режима вывода (блокирующий / не блокирующий) мы или сразу отправляем символ на печать, либо формируем в буфере потока строку. И в момент выхода из функции объект нашего потока разрушается, вызывается деструктор, где приватный метод flush() отправляет заготовленную строку на печать в режиме IT или DMA.

При конвертации числового значения в массив char-ов я отказался от известной идиомы с snprintf() в пользу наработок neiver. Автор в своих публикациях показывает заметное превосходство предложенных им алгоритмов конвертации чисел в строку как в размере бинарника, так и в скорости преобразования. Позаимствованный у него код я инкапсулировал в классе NumConvert, содержащем методы to_string_integer() и to_string_float().

В перегрузке оператора вывода данных массива/контейнера мы с помощью стандартного алгоритма std::ranges::for_each() пробегаемся по содержимому рэйнджа и если элемент удовлетворяет концепту char_type, выводим строку слитно. Если же удовлетворяет концептам num_type или std::floating_point, разделяем значения с помощью заданного значения Delim.

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

int main() {    using StreamUartBlocking = StreamBase<UartBlocking, ' '>;    StreamUartBlocking cout;    cout << 'A'; // 1  cout << ("esreveR me!" | std::views::take(7) | std::views::reverse); // 2    return 0;}

Выставим флаги компилятора: -std=gnu++20 -Os -fno-exceptions -fno-rtti. Тогда на первом примере мы получим следующий ассемблерный листинг:

main:        push    {r3, lr}        movs    r0, #65        bl      putchar        movs    r0, #0        pop     {r3, pc}

На втором:

.LC0:        .ascii  "esreveR me!\000"main:        push    {r3, r4, r5, lr}        ldr     r5, .L4        movs    r4, #5.L3:        subs    r4, r4, #1        bcc     .L2        ldrb    r0, [r5, r4]    @ zero_extendqisi2        bl      putchar        b       .L3.L2:        movs    r0, #0        pop     {r3, r4, r5, pc}.L4:        .word   .LC0

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

Конечно же, при выводе числовых значений, добавится еще код конвертации числа в строку.

Потестировать онлайн можно здесь (hardware dependent код заменил для наглядности на putchar() ).

Рабочий код проекта смотрите/забирайте отсюда. Там реализован пример из начала статьи.

Это стартовый вариант, для уверенного использования еще требуются некоторые доработки и тесты. Например, нужно предусмотреть механизм синхронизации при неблокирующем выводе - когда, скажем, вывод данных предыдущей функции еще не завершен, а мы в следующей функции уже переписываем буфер новой информацией. Также нужно еще внимательно поэкспериментровать с алгоритмами std::views. Например std::views::drop() при применении ее к строковому литералу или массиву char-ов, взрывается ошибкой "inconsistent directions for distance and bound". Ну что ж, стандарт новый, со временем освоим.

Как это работает можно посмотреть здесь. Проект поднят на двухядерном STM32H745; с одного ядра (480МГц) вывод идет в блокирующем режиме через отладочный интерфейс SWO, код примера выстреливается за 9,2 мкс, со второго(240МГц) - через Uart в режиме DMA, примерно за 20 мкс.

Как-то так.

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

Подробнее..

Stm32 USB на шаблонах C. Продолжение. Делаем CDC

30.05.2021 20:12:29 | Автор: admin

Продолжаю разработку полностью шаблонной библиотеки под микроконтроллеры Stm32, в прошлой статье рассказал об успешной (почти) реализации HID устройства. Еще одним популярным классом USB является виртуальный COM-порт (VCP) из класса CDC. Популярность объясняется тем, что обмен данными осуществляется аналогично привычному и простому последовательному протоколу UART, однако снимает необходимость установки в устройство отдельного преобразователя.

Интерфейсы

Устройство класса CDC должно поддерживать два интерфейса: интерфейс для управления параметрами соединения и интерфейс обмена данными.

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

template <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Ep0, typename _Endpoint, typename... _Functionals>class CdcCommInterface : public Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::Comm, _SubClass, _Protocol, _Ep0, _Endpoint>{  using Base = Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::Comm, _SubClass, _Protocol, _Ep0, _Endpoint>;  static LineCoding _lineCoding;  ...

В базовом случае интерфейс должен поддерживать три управляющих (setup) пакета:

  • SET_LINE_CODING: установка параметров линии: Baudrate, Stop Bits, Parity, Data bits. Некоторые проекты, на которые я ориентировался (основным источников вдохновения стал этот проект), игнорируют данный пакет, однако в этом случае некоторые терминалы (например, Putty), отказываются работать.

  • GET_LINE_CODING: обратная операция, в ответ на эту команду устройство должно вернуть текущие параметры.

  • SET_CONTROL_LINE_STATE: установка состояния линии (RTS, DTR и т.д.).

Код обработчика setup-пакетов:

switch (static_cast<CdcRequest>(setup->Request)){case CdcRequest::SetLineCoding:  if(setup->Length == 7)  {    // Wait line coding    _Ep0::SetOutDataTransferCallback([]{      memcpy(&_lineCoding, reinterpret_cast<const void*>(_Ep0::RxBuffer), 7);      _Ep0::ResetOutDataTransferCallback();      _Ep0::SendZLP();    });    _Ep0::SetRxStatus(EndpointStatus::Valid);  }  break;case CdcRequest::GetLineCoding:  _Ep0::SendData(&_lineCoding, sizeof(LineCoding));  break;case CdcRequest::SetControlLineState:  _Ep0::SendZLP();  break;default:  break;}

Ключевой момент нумерации, а именно формирование дескрипторов, выполнен по уже привычной схеме раскрытия variadic-ов, что позволяет избавиться от зависимости классов в иерархии:

static uint16_t FillDescriptor(InterfaceDescriptor* descriptor){  uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {    .Number = _Number,    .AlternateSetting = _AlternateSetting,    .EndpointsCount = Base::EndpointsCount,    .Class = DeviceAndInterfaceClass::Comm,    .SubClass = _SubClass,    .Protocol = _Protocol  };  uint8_t* functionalDescriptors = reinterpret_cast<uint8_t*>(descriptor);  ((totalLength += _Functionals::FillDescriptor(&functionalDescriptors[totalLength])), ...);  EndpointDescriptor* endpointDescriptors = reinterpret_cast<EndpointDescriptor*>(&functionalDescriptors[totalLength]);  totalLength += _Endpoint::FillDescriptor(endpointDescriptors);  return totalLength;}

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

template <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Ep0, typename _Endpoint>class CdcDataInterface : public Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::CdcData, _SubClass, _Protocol, _Ep0, _Endpoint>{  using Base = Interface<_Number, _AlternateSetting, DeviceAndInterfaceClass::CdcData, _SubClass, _Protocol, _Ep0, _Endpoint>;  ...

Поскольку мои познания в CDC-устройствах весьма небольшие, из просмотренных примеров я сделал вывод, что управляющий интерфейс почти всегда одинаковый и содержит 4 функциональности: Header, CallManagement, ACM, Union, поэтому добавил упрощенный шаблон интерфейса:

template<uint8_t _Number, typename _Ep0, typename _Endpoint>using DefaultCdcCommInterface = CdcCommInterface<_Number, 0, 0x02, 0x01, _Ep0, _Endpoint, HeaderFunctional, CallManagementFunctional, AcmFunctional, UnionFunctional>;

Применение разработанных классов

Для использования разработанных классов достаточно объявить две конечные точки (Interrupt для первого интерфейса и двунаправленную Bulk для второго), объявить оба интерфейса, конфигурацию с ними и, наконец, инстанцировать класс устройства:

using CdcCommEndpointBase = InEndpointBase<1, EndpointType::Interrupt, 8, 0xff>;using CdcDataEndpointBase = BidirectionalEndpointBase<2, EndpointType::Bulk, 32, 0>;using EpInitializer = EndpointsInitializer<DefaultEp0, CdcCommEndpointBase, CdcDataEndpointBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using CdcCommEndpoint = EpInitializer::ExtendEndpoint<CdcCommEndpointBase>;using CdcDataEndpoint = EpInitializer::ExtendEndpoint<CdcDataEndpointBase>;using CdcComm = DefaultCdcCommInterface<0, Ep0, CdcCommEndpoint>;using CdcData = CdcDataInterface<1, 0, 0, 0, Ep0, CdcDataEndpoint>;using Config = Configuration<0, 250, false, false, CdcComm, CdcData>;using MyDevice = Device<0x0200, DeviceAndInterfaceClass::Comm, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

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

template<>void CdcDataEndpoint::HandleRx(){  uint8_t* data = reinterpret_cast<uint8_t*>(CdcDataEndpoint::RxBuffer);  uint8_t size = CdcDataEndpoint::RxBufferCount::Get();  if(size > 0)  {    if(data[0] == '0')    {      Led::Clear();      CdcDataEndpoint::SendData("LED is turn off\r\n", 17);    }    if(data[0] == '1')    {      Led::Set();      CdcDataEndpoint::SendData("LED is turn on\r\n", 16);    }  }  CdcDataEndpoint::SetRxStatus(EndpointStatus::Valid);}

Отладка и тестирование

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

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

WireShark с установленным UsbPcap оказался весьма удобным, он нормально парсит все данные, так что поиск ошибок значительно упрощается. Главное, что нужно сделать - правильно установить фильтры. Не нашел ничего лучше, кроме выполнить следующие две операции:

Сначала отфильтровать по заведомо известному значению. Например, по значению PID, которое присутствует в ответе устройства на запрос GET_DEVICE_DESCRIPTOR. Фильтр: "usb.idProduct == 0x5711". Это позволит быстро определить адрес устройства.

Далее отфильтровать по адресу устройства с помощью оператора contains. Дело в том, что отображаемый адрес состоит из трех частей, последняя из которых является номером конечной точки (можно, конечно, перечислить все адреса). Фильтр: "usb.addr contains "1.19"".

Однако стоит заметить, что UsbPcap может доставить некоторые трудности, под катом опишу ситуацию, в которую недавно попал и потратил кучу времени и нервов.

Проблема с usbpcap

Для большей мобильности завел себе внешний SSD, на котором установлена Windows 10 To Go (Windows, предназначенная для установки на внешние носители). Хотя Microsoft вроде отказалась от поддержки этой технологии, в целом все работает. Прихожу с диском в новое место, гружусь с него, система подтягивает драйвера и все нормально (и быстро) работает.

Однажды Windows просто не загрузилась с синим экраном "inaccessible boot device". Потратил целые выходные, восстановить так и не смог, пришлось все переустановить. Через некоторое время та же проблема и снова потраченные на переустановку выходные. Спустя пару дней система опять не грузится, начал вспоминать и анализировать, что я такого делал. Выяснил, что проблема возникала после установки как раз WireShark с usbpcap. На одном из форумов наткнулся на сообщение от пользователя, который жаловался на проблему с мышкой/клавиатурой после установки usbpcap. Снес через LiveCD драйвер и Windows запустилась. Не уверен на 100%, но предположение такое: при запуске компьютера Windows начинается загружаться, подгружает драйвера usbpcap, тот блокирует USB, система дальше грузиться не может и падает в BSOD. Очень неочевидное поведение, жаль потраченного времени.

Тестировал написанный код в программе Terminal v1.9b, на скриншоте приведен результат отправки на устройство сообщений "0" и "1".

Полный код примера можно посмотреть в репозитории. Пример протестирован на STM32F072B-DISCO. Как и в случае с HID, громоздкая библиотека (особенно менеджер конечных точек) сильно облегчили реализацию поддержки CDC, на все ушел примерно полный день. Далее планирую добавить еще класс Mass Storage Device, и на этом, наверно, можно остановиться. Приветствую вопросы и замечания.

Подробнее..

Предельная скорость USB на STM32F103, чем она обусловлена?

24.05.2021 14:14:07 | Автор: admin
У данной статьи тяжёлая история. Мне надо было сделать USB-устройства, не выполняющие никакой функции, но работающие на максимальной скорости. Это были бы эталоны для проверки некоторых вещей. HS-устройство я сделал на базе ПЛИС и ULPI, загрузив туда прошивку на базе проекта Daisho. Для FS-устройства, разумеется, была взята голубая пилюля. Скорость получалась смешная. Прямо скажем, черепашья скорость.



Но я же крутой, у меня теперь есть USB-анализатор (его разработка была описана в блоке из нескольких предыдущих статей). Посмотрев логи, я понял, что похоже, знаю, почему скорость невозможно поднять, даже если STM32F103 обслуживает исключительно USB и ничего больше. Даже начал писать о результатах Но потом решил, что кому-то это будет не интересно, а кто-то скажет, что и так это знал.

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

Итак, давайте выясним, почему именно STM32F103C8T6 не может прокачать по шине USB данные на скорости 12 мегабит, заняв всю ширину предоставленного канала.

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

Подготовка проекта STM32


Создаём проект


Итак. Чтобы все могли повторить мои действия, я скачал самую свежую на момент написания статьи версию 6.2. Правда, они выходят с такой частотой, что на момент, когда всё будет выложено на Хабр, всё может уже измениться. Но так или иначе. Скачиваем, устанавливаем.

Создаём новый проект для STM32F103C8Tx. Добавляем туда USB-Device.



Теперь, когда есть USB, добавляем CDC-устройство. При этом заменим ему VID и PID. Дело в том, что у меня есть inf Файл, который ставит драйвер winusb именно на эту пару идентификаторов. При работе через драйвер usbser скорость была ещё ниже. Я решил исключить всё, что может влиять. Буду замерять скорость без каких-либо прослоек.



Теперь добавляем RCC для работы с тактовыми сигналами:



После всего этого (добавили USB и добавили RCC) можно и тактовые частоты настроить, но сначала спасём себя от самоотключающегося блока отладки. Он спрятан надёжно! Вот так сейчас всё выглядит по умолчанию:



А вот так надо



Прекрасно! Теперь можно настроить тактовые частоты. Я всегда это делаю опытным путём. Системная частота должна стать 72 МГц, а частота USB 48 МГц. Это я в состоянии запомнить. Остальное каждый раз заново вывожу.



Ну всё. Для тестового проекта настроек, вроде, достаточно. Заполняем свойства проекта и сохраняем. Лично я в формате MDK ARM. Он же Кейл. Мне так проще.

Надеюсь, я ничего не забыл сделать. Я специально показываю все шаги, чтобы внимательная общественность проверила меня.

Донастраиваем проект в среде разработки


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



Дописываем код проекта


Наш проект должен просто принимать данные из USB и И всё! Принимать, принимать, принимать! Не будем тратить время на какую-то обработку этих данных. Просто приняли и забыли, приняли и забыли. Обработчик события данные приняты в типовом CDC проекте живёт здесь:



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

Подготовка проекта под Windows


Вариант честной работы с UART мы опустим. Дело в том, что совсем скоро мы будем искать причины тормозов. А вдруг они вызваны драйвером usbser.sys? Нет. Мы возьмём проверенный временем драйвер winusb и будем работать с ним через библиотеку libusb. Кому нравится Linux сможет работать через эту же библиотеку там. Мы тренировались работать с нею в этой статье. А в этой учились работать с нею в асинхронном режиме.

Сначала я вёл работу через блокирующие функции, так как их написать было проще. Мало того, черновые замеры, которые я делал ещё до начала работы над текстом, были вполне красивые. Это ещё не всё, первая метрика, снятая для статьи, тоже была прекрасна и полностью отражала черновые результаты! Потом что-то случилось. График стал каким-то удивительным, правда, чуть ниже я эту удивительность объясню. При работе с блоками меньше чем 64 байта программа съедала 25% процессорного времени, а именно на блоке 64 байта был излом. Мне казалось, что кто-то обязательно напишет в комментариях, что сделай я всё на асинхронных функциях, всё станет намного лучше. В итоге, я взял и всё переписал на асинхронный вариант. Процент потребления процессорного времени на малых блоках действительно изменился. Теперь программа потребляет 28% вместо двадцати пяти Цифры скоростей же не изменились Но асинхронная работа более правильная сама по себе, так что я покажу именно её. Вся теория уже рассматривалась мною в тех статьях про libusb.

Я завожу всё ту же вспомогательную структуру:
    struct asyncParams    {        uint8_t* pData;        uint32_t dataOffset;        uint32_t dataSizeInBytes;        uint32_t transferLen;        uint32_t actualTranfered;        QElapsedTimer timer;        quint64       timerAfter;    };    asyncParams m_asyncParams;

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

Ну, и указатели на объекты передача имеются, куда же без них:
    static const int m_nTransfers = 32;    libusb_transfer* m_transfers [m_nTransfers]; 

Функция обратного вызова отличается от описанной в предыдущих статьях как раз тем, что она считывает показание таймера, если передавать больше нечего. Это произойдёт не единожды, а для каждой из передач (тридцати двух в случае мелких блоков, если блоки крупные их будет меньше, но всё равно не одна). Но на самом деле, это не страшно. Мы это значение будем анализировать только после последнего вызова этой функции. В остальном там всё то же, что и раньше, так что просто покажу код, не объясняя его. Объяснения все были в предыдущих статьях.
void MainWindow::WriteDataTranfserCallback(libusb_transfer *transfer){    MainWindow* pClass = (MainWindow*) transfer->user_data;    switch (transfer->status )    {    case LIBUSB_TRANSFER_COMPLETED:        pClass->m_asyncParams.actualTranfered += transfer->length;        // Still need transfer data        if (pClass->m_asyncParams.dataOffset < pClass->m_asyncParams.dataSizeInBytes)        {            transfer->buffer = pClass->m_asyncParams.pData+pClass->m_asyncParams.dataOffset;            pClass->m_asyncParams.dataOffset += pClass->m_asyncParams.transferLen;            libusb_submit_transfer(transfer);        } else        {            pClass->m_asyncParams.timerAfter = pClass->m_asyncParams.timer.nsecsElapsed();        }        break;/*    case LIBUSB_TRANSFER_CANCELLED:    {        pClass->m_cancelCnt -= 1;    }*/    default:        break;    }}

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

Ну, а параметр blockSize у функции это я в своих статьях уже набил оскомину высказыванием, что при работе с USB скорость зависит от размера блока. До определённого значения она ниже нормальной. Это связано с тем, что хост посылает пакеты медленнее, чем их может обработать устройство. Поэтому я всегда строю графики и смотрю, где они входят в насыщение. Сегодня я буду делать то же самое. Правда, сегодня график в дополнение к банальному росту, имеет непривычную для меня форму, что и сподвигло меня на переделку программы с блокирующего на асинхронный режим. Итак, функция, измеряющая скорость, выглядит так:
Её текст я скрыл под катом.
quint64 MainWindow::MeasureSpeed2(uint32_t totalSize, uint32_t blockSize, uint32_t avgCnt){    std::vector<qint64> gist;    gist.resize(avgCnt);    QByteArray data;    data.resize(totalSize);    m_asyncParams.dataSizeInBytes = totalSize;    m_asyncParams.transferLen = blockSize;    uint32_t nTranfers = m_nTransfers;    if (totalSize/blockSize < nTranfers)    {        nTranfers = totalSize/blockSize;    }    for (uint32_t i=0;i<avgCnt;i++)    {        m_asyncParams.dataOffset = 0;        m_asyncParams.actualTranfered = 0;        m_asyncParams.pData = (uint8_t*)data.constData();        // Готовим структуры для передач        for (uint32_t i=0;i<nTranfers;i++)        {            m_transfers[i] = libusb_alloc_transfer(0);            libusb_fill_bulk_transfer (m_transfers[i],m_usb.m_hUsb,0x02,//,0x01,                                       m_asyncParams.pData+m_asyncParams.dataOffset,m_asyncParams.transferLen,WriteDataTranfserCallback,                                            // No need use timeout! Let it be as more as possibly                                       this,0x7fffffff);            m_asyncParams.dataOffset += m_asyncParams.transferLen;        }        m_asyncParams.timerAfter = 0;        m_asyncParams.timer.start();        for (uint32_t i=0;i<nTranfers;i++)        {            int res = libusb_submit_transfer(m_transfers[i]);            if (res != 0)            {                qDebug() << libusb_error_name(res);            }        }        timeval tv;        tv.tv_sec = 0;        tv.tv_usec = 500000;        while (m_asyncParams.actualTranfered < totalSize)        {            libusb_handle_events_timeout (m_usb.m_ctx,&tv);        }        quint64 size = totalSize;        size *= 1000000000;        gist [i] = size/m_asyncParams.timerAfter;    }    for (uint32_t i = 0;i<nTranfers;i++)    {        libusb_free_transfer(m_transfers[i]);        m_transfers[i] = 0;    }    qint64 avgSpeed = 0;    for (uint32_t i=0;i<avgCnt;i++)    {        avgSpeed += gist [i];    }    avgSpeed /= avgCnt;    if (avgCnt < 4)    {        return avgSpeed;    }    for (uint32_t i=0;i<avgCnt;i++)    {        if (gist [i] < (avgSpeed * 3)/4)        {            gist [i] = 0;        }        if (gist [i] > (avgSpeed * 5)/4)        {            gist [i] = 0;        }    }    avgSpeed = 0;    int realAvgCnt = 0;    for (uint32_t i=0;i<avgCnt;i++)    {        if (gist[i]!= 0)        {            avgSpeed += gist [i];            realAvgCnt += 1;        }    }    if (realAvgCnt == 0)    {        return 0;    }    return avgSpeed/realAvgCnt;}


Сборку статистики в файл csv я делаю так:
void MainWindow::on_m_btnWriteStatistics_clicked(){    QFile file ("speedMEasure.csv");    if (!file.open(QIODevice::WriteOnly))    {        QMessageBox::critical(this,"Error","Cannot create csv file");        return;    }    QTextStream out (&file);    QApplication::setOverrideCursor(Qt::WaitCursor);    for (int blockSize=0x8;blockSize<=0x20000;blockSize *= 2)    {        quint64 speed = MeasureSpeed(0x100000,blockSize,10);        out << blockSize << "," << speed << Qt::endl;    }    out.flush();    file.close();    QApplication::restoreOverrideCursor();}


Первый результат


Итак. У нас есть прошитое устройство, у нас есть программа для его тестирования. Строим статистику для разных размеров блока, чтобы быть уверенными, что проблемы не зависят от этого значения.



Где-то после размера блока 4 килобайта, скорость упирается в 560 килобайт в секунду. Давайте я грубо умножу это на 8. Получаю условные 4.5 мегабита в секунду. Условность состоит в том, что на самом деле, там ещё бывают вставные биты, да и на пакеты оверхед имеется. Но всё равно, это отстоит очень далеко от 12 мегабит в секунду, положенных на скорости Full Speed (кстати, именно поэтому на вступительном рисунке стоит знак 120, он символизирует данный теоретический предел).

Почему результат именно такой


Будучи человеком вооружённым (не зря же я всех полгода пытал статьями про изготовление анализатора), я взял и рассмотрел детали трафика. И вот что у меня получилось (данные я чуть обрезал, там реально всегда уходит по 64 байта):


То же самое текстом.
*** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA0C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD *** +45 usACKD2 *** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA14B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD *** +45 usNAK5A *** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA14B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD*** +45 usACKD2 *** +5 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA0C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD*** +45 usNAK5A


И так до бесконечности на всех участках, что я смог осмотреть глазами, возвращается то ACK, то NAK. Причём в режиме FS каждая пачка данных передаётся целиком. Принялась она или нет, а всё равно передаётся целиком. Хорошо, что это не роняет всю шину USB, так как до последнего хаба данные бегут на скорости HS в виде SPLIT транзакции. А дальше уже хаб мелко шинкует её на пакеты по 64 байта и пытается отослать на скорости FS.

Подобные чередования ACK-NAK я нашёл и в сети, когда пытался проверить, у одного ли меня такой результат. Нет, не у одного.

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



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

А почему при блоке 64 байта скорость выше? Я долго думал, и нашёл следующее объяснение: 64 байта это тот размер блока, после которого при работе с FS-устройствами через HS-хабы начинается использование SPLIT-транзакций. Что это такое можно посмотреть в стандарте, там этому посвящён не один десяток страниц. Но если коротко: до хаба запрос идёт на скорости HS, а уже хаб обеспечивает снижение скорости и нарезание данных на FS-блоки. Основная шина при этом не тормозит.

Выше мы видели, что уже через 5 микросекунд после прихода примитива ACK, пошёл следующий пакет, который не был обработан контроллером. А что будет, если мы будем работать блоками по 64 байта? Я начну с примитива SOF.
Смотреть код.
*** +1000 usSOF 42.0 (0x2a)A5 2A 50 *** +3 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA0C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 *** +45 usACKD2 *** +37 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA14B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*** +45 usACKD2 *** +43 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA0C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*** +45 usACKD2


Вот и разгадка! Как видим, быстрее не всегда лучше. Время между пакетами больше. Особенность работы хоста такая.

По этой же причине, если мы воткнём между материнской платой и устройством дешёвый USB2-хаб, марку которого я не скажу, так как сильно поругался с магазином, владеющим именем бренда, но судя по ID, чип там VID_05E3&PID_0608, то статистика окажется намного лучше, чем при прямом подключении к материнке:



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

Пробуем двухбуферную систему


Будучи опытным программистом для микроконтроллеров, я знаю, что обычно такая проблема решается путём применения двухбуферной схемы. Пока обрабатывается один буфер, данные передаются во второй. А какая схема используется здесь? Ответ на мой вопрос мы получим из следующего кода:
USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev){  /* USER CODE BEGIN EndPoint_Configuration */  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);  /* USER CODE END EndPoint_Configuration */  /* USER CODE BEGIN EndPoint_Configuration_CDC */  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xC0);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0x110);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x82 , PCD_SNG_BUF, 0x100);

Тут, везде написано PCD_SNG_BUF. Вообще, в статье про DMA я уже рассуждал, что если разработчики этой библиотеки что-то не используют, значит и не стоит этого использовать. Но всё же, я попробовал заменить SNG_BUF на DBL_BUF. Результат остался прежним. Тогда я нашёл в сети следующее утверждение:

The USB peripheral's HAL driver (PCD) has a known limitation: it directly maps EPnR register numbers with endpoint addresses. This works if all OUT and IN endpoints with the same number (e.g. 0x01 and 0x81) are the same type, and not double buffered. However, isochronous endpoints have to be double buffered, therefore you have to use an endpoint number that's unused in the other direction. E.g. in your case, set AUDIO_OUT_EP1 to 0x01, and AUDIO_IN_EP1 to 0x82. (Or you can drop this USB stack altogether as I did.)


Попробовал разнести точки результат тот же. В общем, анализ показал, что для точек типа BULK это если и возможно, то только путём переписывания MiddleWare. Короче, не зря там везде одиночные буферы выбраны. Разработчики знали, что делают.

Тормоза в обработчике прерывания


Теперь подойдём к проблеме с уже имеющимися данными, но с другой стороны. Выше в комментариях мы видели, что пока мы находимся в обработчике прерывания, система будет слать NAK. А как велик этот обработчик?

Немного потрассируем код. Давайте я поставлю точку останова в функции CDC_Receive_FS, в которую мы попадаем ради каждого блока в 64 байта или меньше. Ну, потому что транзакции, больше чем на 64 байта, будут разрезаны на пакеты такого размера. Вот такой у нас получается стек возвратов в обработчик прерываний. А это ещё часть функций заинлайнилась!



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

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

Но и это ещё не всё! Ради всё тех же каждых 64 байт, мы вызываем функцию USBD_CDC_SetRxBuffer(), а затем USBD_CDC_ReceivePacket(), которая также расслоится на кучку вызовов, каждый из которых всего лишь чуть-чуть перетасовывает данные. Оцените стек возвратов, когда мы находимся в самом глубоком слое!



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

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

Проверяем теорию осциллографом


Имея стек возвратов, а также известные времянки посылки транзакций, мы можем проверить всё с другой стороны, чтобы быть уверенными, что нигде не ошиблись. Давайте я на входе в прерывание буду взводить какой-либо порт, а на выходе сбрасывать. Как всегда, я буду делать это через библиотеку mcucpp Константина Чижова.

Объявим, скажем, ножку Pa0 для этой цели:
#include <iopins.h>typedef Mcucpp::IO::Pa0 oscOut;//PC13

За что люблю эту библиотеку, так за её простоту. Инициализация ножки выглядит так:
oscOut::ConfigPort::Enable();oscOut::SetDirWrite();

Включили тактирование аппаратных блоков, назначили ножку на выход. Собственно, всё.

И добавим пару строк в функцию обработки прерывания (первая взведёт ножку в единицу, вторая сбросит в ноль):


То же самое текстом.
void USB_LP_CAN1_RX0_IRQHandler(void){  /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 0 */oscOut::Set();  /* USER CODE END USB_LP_CAN1_RX0_IRQn 0 */  HAL_PCD_IRQHandler(&hpcd_USB_FS);  /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 1 */oscOut::Clear();  /* USER CODE END USB_LP_CAN1_RX0_IRQn 1 */}


Типичный период следования прерываний от 70 до 100 микросекунд. При этом в прерывании мы находимся чуть меньше чем 19 микросекунд. На первом рисунке показан случай, когда данные идут помедленнее (расстояния велики), на втором побыстрее (расстояния меньше). Обе осциллограммы сняты при подключении через хаб.





А вот хорошая и не очень хорошая ситуации, снятые при прямом подключении к материнке. Собственно, на плохом варианте видно, что расстояния удваиваются. Один из пакетов уходит с NAKом, в прерывание мы не попадаем.





Осталось понять, как эти прерывания располагаются относительно USB-примитивов. В этом нам поможет Reference Manual. Момент прихода прерывания я выделил.



Вот теперь всё сходится. После формирования ACKа мы не готовы принимать новые пакеты на протяжении 18 микросекунд. Когда они приходят через 3-5 микросекунд (а именно это мы видим в текстовом логе анализатора выше для плохого случая), контроллер их просто игнорирует, посылая NAK. Когда через 30-40 (что мы наблюдаем в текстовом логе для случая хорошего, хотя, точно подойдёт любое значение, больше чем 19) обрабатывает.

Плюс из текстовых логов мы видим, что влево от прерывания около пятидесяти микросекунд занимает сама OUT-транзакция на аппаратном уровне. Кстати. У нас же скорость FS. Такие сигналы можно ловить обычным осциллографом. Давайте я добавлю активность на шине USB в виде голубого луча. Что получим?

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



Вот такая картинка была, когда я получил производительность 814037 байт в секунду. Извините, но быстрее никак. Либо по шине идут данные, либо мы обрабатываем прерывание. Простоев нет!




Причём 64 байта, с учётом, что я передаю все нули, а значит там может быть вставленный бит это примерно 576 бит. При частоте 12 МГц их передача займёт 48 миллисекунд. То есть, когда между пакетами примерно по 50 миллисекунд, мы имеем дело с пределом скорости. Тут даже NAKов нет.




NAKи вот они, когда хост пытается что-то передавать, пока мы заняты, что совпадает с выводами, сделанными при анализе текстовых логов. Вот тут прямо видны два пакета, первый из которых начался, когда аппаратура ещё не готова его принимать.




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

Мне показалось, что там они решили проблему, полностью переделав логику работы Middleware USB. Они там рассуждают, что дико сократили время пребывания внутри прерывания. Но если честно, у меня не стояло задачи выжать из этого контроллера всё. Так что я даже сильно не вчитывался в доводы авторов. Правда, при работе через хаб всё попроще. Короче, результаты у китайских специалистов если и есть, они также будут работать только на определённой конфигурации, ведь на паузы от хаба повлиять не сможет никто.

Неожиданное следствие из снятой осциллограммы


И раз уж я снял осциллограммы, предлагаю посмотреть на процесс, когда пакеты идут по шине, с точки зрения не оптимизатора, а простого программиста. Вот осциллограмма. Единичное значение контроллер находится в обработчике прерывания. Все остальные задачи в это время не могут работать. Каково?



А при коротких пакетах, бывает и такое:



Правда, это только во время передачи данных. Если передача не идёт, мы видим только SOFы (на предыдущей осциллограмме короткая иголка слева это как раз SOF). Но когда идёт обмен данными, процентов 20 времени контроллер не готов нас обслуживать. Он находится в контексте прерывания. Так что если и обслужит, то только прерывания с бОльшим приоритетом.

Выводы


Ну что, пришла пора делать выводы. Как видим, контроллер STM32F103C8T6 не может выжать всю производительность даже из шины USB 2.0 FS.

Хорошо это или плохо? Ни то, ни другое. Есть тысяча и одна задача, где не надо гнаться за производительностью USB, и этот копеечный контроллер прекрасно с ними справляется. Вот там его и надо использовать. (Дополнение: пока статья дежала в столе, на Хабре появилась статья, что уже не копеечный. Цена у местных поставщиков, согласно той статье, выросла в 10 раз. Надеюсь, это временное явление.)

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

А для производительных вещей я в ближайшее время собираюсь изучить контроллер, у которого USB обрабатывается по стандарту EHCI. Там всё на дескрипторах. Заполнил адрес, длину Когда пришло прерывание данные уже готовы Надеюсь Если будет что-то интересное сделаю статью. А здесь сам подход к обработке приходящих данных (они помещаются в выделенную память, а затем программно изымаются оттуда, причём в контексте прерывания) не даёт развить высоких скоростей. По крайней мере, на кристалле F103.

Следующий вывод: добавленный в систему дешёвый USB-хаб даёт неожиданный прирост производительности. Это связано с тем, что он шлёт пакеты с паузами 20-25 микросекунд (в статье подтверждающий лог не приводится для экономии места, но его можно скачать здесь для самостоятельного изучения). Получаем грубо 20 микросекунд задержки, 50 микросекунд передачи. Итого 5/7 от полной производительности. Как раз 700-800 килобайт в секунду при теоретическом максимуме 1000-1100. Так что любой FS-контроллер, включённый через этот хаб, не сможет выдать больше.

Дальше: видно, что, когда по USB передаются данные, контроллер довольно большой процент времени находится в обработчике прерывания USB. Это также надо иметь в виду, проектируя систему. Прерываниям UART, SPI и прочим, где данные ждать невозможно, а обработка будет быстрой, стоит задавать приоритет выше, чем у прерывания USB. Ну, или использовать DMA.

И, наконец, мы выяснили, что для FS устройств на базе STM32 нет чёткого критерия оптимальной работы со стороны прикладной программы. Одна и та же система, в которую то добавляли, то исключали внешний USB-хаб, работала с максимальной производительностью либо при длине блока 64 байта (без хаба), либо более четырёх килобайт (с хабом). При разработке прикладных программ для PC, требующих высокой производительности, следует учитывать и этот аспект. Вплоть до калибровки параметров под конкретную конфигурацию оборудования.

Послесловие. А что там в режиме HS?


Ради интереса приведу результаты, снятые для HS-устройства на базе ПЛИС. Второй столбец скорость при прямом подключении к материнской плате, третий через тот самый дешёвый хаб. Мне уже интересно, почему там хаб даёт такую просадку, но с этим я разберусь как-нибудь в другой раз.



А пока статья лежала в столе, я взял типовой пример CDC-прошивки от NXP, немного доработал его (без доработки он зависнет при односторонней передаче), залил в плату Teensy 4.1 и снял метрики там. У него контроллер EHCI и скорость HS.



Причина та же самая. Аппаратура, вроде, позволяет поставить в очередь несколько запросов (как для асинхронной работы), но увы, программная часть это явно запрещает:
usb_status_t USB_DeviceCdcAcmRecv(class_handle_t handle, uint8_t ep, uint8_t *buffer, uint32_t length){    if (1U == cdcAcmHandle->bulkOut.isBusy)    {        return kStatus_USB_Busy;    }    cdcAcmHandle->bulkOut.isBusy = 1U;

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



Но это уже тема для другой статьи.
Подробнее..

STM32 LTDC и 7-дюймовый дисплей часть 1

09.04.2021 22:19:33 | Автор: admin

Доброго времени суток.

Речь пойдёт о подключении дисплея AT070TN94 с параллельным интерфейсом к контроллеру STM32H743. И хотя в интернете достаточно много информации по данной теме, при создании своего устройства у меня периодически возникали те или иные вопросы, ответов на которых найти не удавалось. Пишу в первую очередь для новичков, а профи приглашаю почитать ради советов и аргументированной критики (первая статья как-никак).

В первой части статьи затрону частично выбор ЭКБ и настройку LTDC. Если сообщество написанное одобрит, выйдет вторая часть по интеграции с небезызвестным TouchGFX, значительно упрощающим создание графического интерфейса.

У нас было:

  • Задача по созданию некоторого устройства, содержащего среди прочего цветной дисплей достаточно большой диагонали, построенное на базе STM32.

  • Собственно, сам дисплей AT070TN90/92/94. Лично мне при беглом сравнении документации не удалось выявить различий между этими тремя моделями. Дисплей был выбран исходя из доступности к приобретению в известном китайском магазине (по цене около 1000-1500 рублей за штуку).

  • Для работы с данным типом экранов, не содержащим собственной видеопамяти, необходимо где-то хранить отображаемую картинку. ОЗУ микроконтроллера слишком мала для такой задачи, поэтому приходится использовать внешнюю SDRAM память, у нас это была W9812G6KH.

  • Драйвер питания LCD панели. Исходя из документации на дисплей, становится жутко понятно, что ему требуются 4 нестандартных напряжения, это не считая всем знакомых 3.3 В и напряжения питания подсветки. Выбрана микросхема TPS65100.

  • Драйвер подсветки дисплея. Преобразовывает 5В -> 9В для питания подсветки, задает ток светодиодов. Поддерживает регулировку яркости путем подачи ШИМ-сигнала. Использовали TPS61040DBVR.

  • Разъем для подключения шлейфа дисплея к печатной плате. Имеет 50 контактов с шагом 0.5 мм. Ищется по запросу "FFC FPC 50 pin" .

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

Так выглядит сам экран и разъем для подключения к плате

Немного теории про дисплей

Как я озвучил ранее, дисплей данного типа не имеет на борту собственной видеопамяти. Данные о цвете пикселей непрерывно переносятся непосредственно из параллельного интерфейса, используя в качестве синхронизации сигналы HSYNC, VSYNC, CLK и DE. В параллельную же шину они передаются микроконтроллером прямиком из видеобуфера. Давайте взглянем на картинку ниже, взятого из замечательного Application note AN4861 :

Что нам тут пытаются показать?

Получив сигнал VSYNC, дисплей понимает, что начат новый кадр, и готовится принять первый пиксель. Далее следует задержка в несколько тактов CLK, называемая VBP. После чего, получив сигнал HSYNC, выждав опять же некоторую задержку, экран начинает попиксельно заполнять первую строку (1 такт CLK = 1 пиксель). Заполнив строку и выждав задержку (её можно мыслить как некоторая невидимая часть ширины дисплея), снова приходит сигнал HSYNC, начинается заполнение новой строки, и так до конца дисплея + некоторой задержки VFP, которую также можно мыслить как невидимую область под экраном. Заполнив таким образом кадр, следует сигнал VSYNC, и история повторяется.

А что DE? Он сигнализирует о том, что на шине сейчас валидные RGB данные о пикселе (как видно на картинке выше). Вообще, некоторые дисплеи, в том числе и использованный мной, могут работать только от двух сигналов, DE и CLK, вычисляя сигналы горизонтальной и вертикальной синхронизаций самостоятельно. Соответствующий режим выбирается подачей 1 или 0 на вход MODE экрана. Для конфигурирования LTDC нам в принципе будет все равно, какой режим выбрать: контроллер будет формировать и сигналы H-Vsync, и DE.

Организация схемы питания дисплея

Все необходимые дисплею напряжения будут формировать специально обученные микросхемы TPS61040DBVR и TPS65100, драйверы подсветки и питания LCD матрицы соответственно. В их даташитах хорошо описаны схемы включения и даже даны рекомендации по трассировке печатной платы, поэтому на этих моментах я останавливаться не буду. Все, что необходимо будет сделать - это рассчитать обвязку из резисторов под требуемые напряжения питания по формулам. Ещё у каждой из микросхем есть вход ENABLE, который можно подключить прямо к МК и получить возможно программно управлять питанием экрана. Кроме того, на вход ENABLE драйвера подсветки можно подать ШИМ сигнал и таким образам управлять яркостью. Удобно, не правда ли?

Настраиваем LTDC

Тут мы будем пользоваться CubeMX. Для желающих настроить на регистрах была вот эта статейка, + в Application note AN4861 все подробно расписано и сложностей возникнуть не должно. Сначала тайминги сигналов. Разберу подробно на примере HSYNC.

Для начала сравним 2 картинки.

Таблица с числовыми значениями таймингов экрана

Обратим внимание на H blanking (картинка 1) и HBP (картинка 2). Видим, что тайминги в понимании ST и производителя дисплея немного разные, соответственно, просто переписать значения таблицы в куб не выйдет, придется открывать калькулятор.

Включим LTDC, выберем режим (у нас это RGB888), и заглянем в настройки.

Начнём. Ширину импульса синхронизации (HS pulse width, таблица 1) даташит нам разрешает выбрать самостоятельно из некоторого диапазона. Пусть будет 18. Далее следите за картинками. HBP = H blanking - HS pulse width. Active width = ширина нашего дисплея. HFP = H front porch (таблица 1).

Аналогично поступаем и с вертикальными таймингами. Что ещё? Полярности сигналов синхронизации либо берем из даташита, либо подбираем экспериментально.

Background color можно установить любой, кроме черного. Этим цветом дисплей радостно встретит нас, если в итоге заработает.

Устанавливаем частоту тактирования блока LTDC равной 33,3 МГц (как указано в таблице 1) путем настройки соответствующей PLL из окна тактирования куба.

Идём на вкладку Layer settings.

Ставим 1 слой (пока делаем все на минималках, лишь бы работало). Horizontal start = HSYNC width + HBP из предыдущих настроек куба. Это расположение нашего слоя в пространстве дисплея. Если поставить в данном месте 0, начало слоя окажется в невидимой области. С Vertical start аналогично. Формат цвета выберем попроще (RGB88). Frame Buffer Start Address - адрес расположения нашего видеобуфера. В моем (и в вашем, скорее всего, тоже) случае это адрес FMC, в котором живёт внешняя ОЗУ. Либо адрес внешней SPI flash, куда прошита какая-то картинка. Ну и в очередной раз укажем размер активной области дисплея в настройках буфера ниже.

Зайдем на вкладку GPIO settings и вручную укажем максимально возможную скорость использующимся пинам. Это важно, но куб не делает это автоматически.

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

И что теперь?

А теперь всё должно заработать. Если нет - диагностику начинать как всегда с питания. Важно, хотя микросхемы-драйверы и имеют встроенную защиту от КЗ, при случайном замыкании выходов питания на землю сгорают моментально. Также возможно, что следует поменять полярность сигналов синхронизации. А может, вы забыли инициализировать микросхему ОЗУ какой-нибудь SDRAM_Initialization_Sequence? И вообще, она у вас точно работает?

Снят ли с дисплея RESET? Посмотреть осциллографом, что там действительно творится на выходах LTDC, посчитать периоды и частоту. Если плата самодельная - проверьте целостность линий.

Если заработало, можно поиграться с Background color, или же залить экран каким-нибудь цветом с помощью memset:

memset(FRAMEBUFFER_ADDR, 0x00FFFFFF, FRAMEBUFFER_SIZE);

Либо включить DMA2D (в настройках выбрать формат цвета такой же, как в LTDC)

Настройки DMA2D

И попробовать залить экран цветом при помощи него:

void FillScreen(uint32_t color){  hdma2d.Init.Mode = DMA2D_R2M;  hdma2d.Init.OutputOffset = 0;  if(HAL_DMA2D_Init(&hdma2d) == HAL_OK)  {    if (HAL_DMA2D_Start(&hdma2d, color, FRAMEBUFFER_ADDR,    hltdc.LayerCfg[0].ImageWidth, hltdc.LayerCfg[0].ImageHeight) == HAL_OK)    {      HAL_DMA2D_PollForTransfer(&hdma2d, 10);    }  }}

Заключительное слово автора

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

Репозиторий с минимальным проектом тут.

Подробнее..

USB на регистрах interrupt endpoint на примере HID

10.04.2021 12:12:31 | Автор: admin

Еще более низкий уровень (avr-vusb)
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: isochronous endpoint на примере Audio device

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

Первым делом напомню, что протокол HID (Human Interface Device) не предназначен для обмена большими массивами данных. Весь обмен строится на двух понятиях: событие и состояние. Событие это разовая посылка, возникающая в ответ на внешнее или внутреннее воздействие. Например, пользователь кнопочку нажал или мышь передвинул. Или на одной клавиатуре отключил NumLock, после чего хост вынужден и второй послать соответствующую команду, чтобы она это исправила, также послав сигнал нажатия NumLock и включила его обратно отобразила это на индикаторе. Для оповещения о событиях и используются interrupt точки. Состояние же это какая-то характеристика, которая не меняется просто так. Ну, скажем, температура. Или настройка уровня громкости. То есть что-то, посредством чего хост управляет поведением устройства. Необходимость в этом возникает редко, поэтому и взаимодействие самое примитивное через ep0.

Таким образом назначение у interrupt точки такое же как у прерывания в контроллере быстро сообщить о редком событии. Вот только USB штука хост-центричная, так что устройство не имеет права начинать передачу самостоятельно. Чтобы это обойти, разработчики USB придумали костыль: хост периодически посылает запросы на чтение всех interrupt точек. Периодичность запроса настраивается последним параметром в EndpointDescriptor'е (это часть ConfigurationDescriptor'а). В прошлых частях мы уже видели там поле bInterval, но его значение игнорировалось. Теперь ему наконец-то нашлось применение. Значение имеет размер 1 байт и задается в миллисекундах, так что опрашивать нас будут с интервалом от 1 мс до 2,55 секунд. Для низкоскоростных устройств минимальный интервал составляет 10 мс. Наличие костыля с опросом interrupt точек для нас означает, что даже в отсутствие обмена они будут впустую тратить полосу пропускания шины.

Логичный вывод: interrupt точки предназначены только для IN транзакций. В частности, они используются для передачи событий от клавиатуры или мыши, для оповещения об изменении служебных линий COM-порта, для синхронизации аудиопотока и тому подобных вещей. Но для всего этого придется добавлять другие типы точек. Поэтому, чтобы не усложнять пример, ограничимся реализацией HID-устройства. Вообще-то, такое устройство мы уже делали в первой части, но там дополнительные точки не использовались вовсе, да и структура HID-протокола рассмотрена не была.

ConfigurationDescriptor


static const uint8_t USB_ConfigDescriptor[] = {  ARRLEN34(  ARRLEN1(    bLENGTH, // bLength: Configuration Descriptor size    USB_DESCR_CONFIG,    //bDescriptorType: Configuration    wTOTALLENGTH, //wTotalLength    1, // bNumInterfaces    1, // bConfigurationValue: Configuration value    0, // iConfiguration: Index of string descriptor describing the configuration    0x80, // bmAttributes: bus powered    0x32, // MaxPower 100 mA  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_INTERFACE, //bDescriptorType    0, //bInterfaceNumber    0, // bAlternateSetting    2, // bNumEndpoints    HIDCLASS_HID, // bInterfaceClass:     HIDSUBCLASS_BOOT, // bInterfaceSubClass:     HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol:     0x00, // iInterface  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_HID, //bDescriptorType    USB_U16(0x0110), //bcdHID    0, //bCountryCode    1, //bNumDescriptors    USB_DESCR_HID_REPORT, //bDescriptorType    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_ENDPOINT, //bDescriptorType    INTR_NUM, //bEdnpointAddress    USB_ENDP_INTR, //bmAttributes    USB_U16( INTR_SIZE ), //MaxPacketSize    10, //bInterval  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_ENDPOINT, //bDescriptorType    INTR_NUM | 0x80, //bEdnpointAddress    USB_ENDP_INTR, //bmAttributes    USB_U16( INTR_SIZE ), //MaxPacketSize    10, //bInterval  )  )};


Внимательный читатель тут же может обратить внимание на описания конечных точек. Со второй все в порядке IN точка (раз произведено сложение с 0x80) типа interrupt, заданы размер и интервал. А вот первая вроде бы объявлена как OUT, но в то же время interrupt, что противоречит сказанному ранее. Да и здравому смыслу тоже: хост не нуждается в костылях чтобы передать в устройство что угодно и когда угодно. Но таким способом обходятся другие грабли: тип конечной точки в STM32 устанавливается не для одной точки, а только для пары IN/OUT, так что не получится задать 0x81-й точке тип interrupt, а 0x01-й control. Впрочем, для хоста это проблемой не является, он бы, наверное, и в bulk точку те же данные посылал что, впрочем, я проверять не стану.

HID descriptor


Структура HID descriptor'а больше всего похожа на конфигурационных файл имя=значение, но в отличие от него, имя представляет собой числовую константу из списка USB-специфичных, а значение либо тоже константу, либо переменную размером от 0 до 3 байт.
Важно: для некоторых имен длина значения задается в 2 младших битах поля имени. Например, возьмем LOGICAL_MINIMUM (минимальное значение, которое данная переменная может принимать в штатном режиме). Код этой константы равен 0x14. Соответственно, если значения нет (вроде бы такого не бывает, но утверждать не буду зачем-то же этот случай ввели), то в дескрипторе будет единственное число 0x14. Если значение равно 1 (один байт) то записано будет 0x15, 0x01. Для двухбайтного значения 0x1234 будет записано 0x16, 0x34, 0x12 значение записывается от младшего к старшему. Ну и до кучи число 0x123456 будет 0x17, 0x56, 0x34, 0x12.

Естественно, запоминать все эти числовые константы мне лень, поэтому воспользуемся макросами. К сожалению, я так и не нашел способа заставить их самостоятельно определять размер переданного значения и разворачиваться в 1, 2, 3 или 4 байта. Поэтому пришлось сделать костыль: макрос без суффикса отвечает за самые распространенные 8-битные значения, с суффиксом 16 за 16-битные, а с 24 за 24-битные. Также были написаны макросы для составных значений вроде диапазона LOGICAL_MINMAX24(min, max), которые разворачиваются в 4, 6 или 8 байтов.

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

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

CA Collection(application) Служебная информация, никакой переменной не соответствующая
CL Collection(logical) -/-
CP Collection(phisical) -/-
DV Dynamic Value входное или выходное значение (переменная)
MC Momentary Control флаг состояния (1-флаг взведен, 0-сброшен)
OSC One Shot Control однократное событие. Обрабатывается только переход 0->1


Есть, разумеется, и другие, но в моем примере они не используются. Если, например, поле X помечено как DV, то оно считается переменной ненулевой длины и будет включено в структуру репорта. Поля MC или OSC также включаются в репорт, но имеют размер 1 бит.

Один репорт (пакет данных, посылаемый или принимаемый устройством) содержит значения всех описанных в нем переменных. Описание кнопки говорит о всего одном занимаемом бите, но для относительных координат (насколько передвинулась мышка, например) требуется как минимум байт, а для абсолютных (как для тачскрина) уже нужно минимум 2 байта. Плюс к этому, многие элементы управления имеют еще свои физические ограничения. Например, АЦП того же тачскрина может иметь разрешение всего 10 бит, то есть выдавать значения от 0 до 1023, которое хосту придется масштабировать к полному разрешению экрана. Поэтому в дескрипторе помимо предназначения каждого поля задается еще диапазон его допустимых значений (LOGICAL_MINMAX), плюс иногда диапазон физических значений (в миллиматрах там, или в градусах) и обязательно представление в репорте. Представление задается двумя числами: размер одной переменной (а битах) и их количество. Например, координаты касания тачскрина в создаваемом нами устройстве задаются так:
USAGE( USAGE_X ), // 0x09, 0x30,USAGE( USAGE_Y ), // 0x09, 0x31,LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00,   0x26, 0x10, 0x27,REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,

Здесь видно, что объявлены две переменные, изменяющиеся в диапазоне от 0 до 10000 и занимающие в репорте два участка по 16 бит.

Последнее поле говорит, что вышеописанные переменные будут хостом читаться (IN) и поясняется как именно. Описывать его флаги подробно я не буду, остановлюсь только на нескольких. Флаг HID_ABS показывает, что значение абсолютное, то есть никакая предыстория на него не влияет. Альтернативное ему значение HID_REL показывает что значение является смещением относительно предыдущего. Флаг HID_VAR говорит, что каждое поле отвечает за свою переменную. Альтернативное значение HID_ARR говорит, что передаваться будут не состояния всех кнопок из списка, а только номера активных. Этот флаг применим только к однобитным полям. Вместо того, чтобы передавать 101/102 состояния всех кнопок клавиатуры можно ограничиться несколькими байтами со списком нажатых клавиш. Тогда первый параметр REPORT_FMT будет отвечать за размер номера, а второй за количество.

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

Теперь мы можем если не написать дескриптор с нуля, то хотя бы попытаться его читать, то есть определить, каким битам соответствует то или иное поле. Достаточно посчитать INPUT_HID'ы и соответствующие им REPORT_FMT'ы. Только учтите, что именно такие макросы придумал я, больше их никто не использует. В чужих дескрипторах придется искать input, report_size, report_count, а то и вовсе числовые константы.

Вот теперь можно привести дескриптор целиком:
static const uint8_t USB_HIDDescriptor[] = {  //keyboard  USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,  USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,  COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,    REPORT_ID( 1 ), // 0x85, 0x01,    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,    USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,        LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,    REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08         INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,     //reserved    REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,    INPUT_HID(HID_CONST), // 0x81, 0x01,                  REPORT_FMT(1, 5),  // 0x75, 0x01, 0x95, 0x05,    USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,    USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,      OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,    //выравнивание до 1 байта    REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,    OUTPUT_HID( HID_CONST ), // 0x91, 0x01,    REPORT_FMT(8, 6),  // 0x75, 0x08, 0x95, 0x06,    LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,             USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,    USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,    INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,             )  //touchscreen  USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,  USAGE( USAGE_PEN ), // 0x09, 0x02,  COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,    REPORT_ID( 2 ), //0x85, 0x02,    USAGE( USAGE_FINGER ), // 0x09, 0x22,    COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,      USAGE( USAGE_TOUCH ), // 0x09, 0x42,      USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,      LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,      REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,      INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,      REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,      INPUT_HID( HID_CONST ), // 0x81, 0x01,                      USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,      USAGE( USAGE_POINTER ), // 0x09, 0x01,      COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,                 USAGE( USAGE_X ), // 0x09, 0x30,        USAGE( USAGE_Y ), // 0x09, 0x31,        LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,        REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,        INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,      )    )  )};

Помимо рассмотренных ранее полей тут есть и такое интересное поле, как REPORT_ID. Поскольку, как понятно из комментариев, устройство у нас составное, хосту надо как-то определить, чьи данные он принимает. Для этого данное поле и нужно.

И еще одно поле, на которое хотелось бы обратить внимание OUTPUT_HID. Как видно из названия, оно отвечает не за прием репорта (IN), а за передачу (OUT). Расположено оно в разделе клавиатуры и описывает индикаторы CapsLock, NumLock, ScrollLock а также два экзотических Compose (флаг ввода некоторых символов, для которых нет собственных кнопок вроде , или ) и Kana (ввод иероглифов). Собственно, ради этого поля мы и заводили OUT точку. В ее обработчике будем проверять не надо ли зажечь индикаторы CapsLock и NumLock: на плате как раз два диодика и разведено.

Существует и третье поле, связанное с обменом данными FEATURE_HID, мы его использовали в первом примере. Если INPUT и OUTPUT предназначены для передачи событий, то FEATURE состояния, которое можно как читать, так и писать. Правда, делается это не через выделенные endpoint'ы, а через обычную ep0 путем соответствующих запросов.

Если внимательно рассмотреть дескриптор, можно восстановить структуру репорта. Точнее, двух репортов:
struct{  uint8_t report_id; //1  union{    uint8_t modifiers;    struct{      uint8_t lctrl:1; //left control      uint8_t lshift:1;//left shift      uint8_t lalt:1;  //left alt      uint8_t lgui:1;  //left gui. Он же hyper, он же winkey      uint8_t rctrl:1; //right control      uint8_t rshift:1;//right shift      uint8_t ralt:1;  //right alt      uint8_t rgui:1;  //right gui    };  };  uint8_t reserved; //я не знаю зачем в официальной документации это поле  uint8_t keys[6]; //список номеров нажатых клавиш}__attribute__((packed)) report_kbd;struct{  uint8_t report_id; //2  union{    uint8_t buttons;    struct{      uint8_t touch:1;   //фактнажатия на тачскрин      uint8_t inrange:1; //нажатие в рабочей области      uint8_t reserved:6;//выравнивание до 1 байта    };  };  uint16_t x;  uint16_t y;}__attribute__((packed)) report_tablet;


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

В процессе тестирования наткнулся на забавный побочный эффект: в Windows7 при нажатии на тачскрин вылезает окошко рукописного ввода. Я об этой фиче не знал.

Если к вам попало готовое устройство


и хочется посмотреть на него изнутри. Первым делом, естественно, смотрим, можно даже от обычного пользователя, ConfigurationDescriptor:
lsusb -v -d <VID:PID>

Для HID-дескриптора же я не нашел (да и не искал) способа лучше, чем от рута:
cat /sys/kernel/debug/hid/<address>/rdes

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

Заключение


Вот, собственно, и все, что я нарыл по HID. План-минимум научиться читать готовые дескрипторы, эмулировать несколько устройств одновременно и реализовать планшетный ввод выполнен. Ну и философию interrupt точек рассмотрели заодно.

Как и в плошлый раз, немножко документации оставил в репозитории на случай если дизайнеры USB-IF снова решат испортить сайт.
Подробнее..

USB на регистрах isochronous endpoint на примере Audio device

23.05.2021 14:09:40 | Автор: admin
image<картинка с платой и наушниками>
Еще более низкий уровень (avr-vusb): habr.com/ru/post/460815
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: interrupt endpoint на примере HID

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

Как ни странно, этот тип конечной точки оказался самым мозговыносящим (и это после всего, что я успел повидать с stm'ками!). Тем не менее, сегодня мы сделаем аудиоустройство и заодно чуть-чуть допилим ядро библиотеки USB. Как обычно, исходные коды доступны:
github.com/COKPOWEHEU/usb/tree/main/4.Audio_L1
github.com/COKPOWEHEU/usb/tree/main/4.Audio_F1

Доработка ядра


Допиливать ядро нужно потому, что у STM изохронные точки могут быть только с двойной буферизацией, то есть, грубо говоря, нельзя сделать 0x01 изохронной, а 0x81 управляющей. То есть в дескрипторе USB это прописать, конечно, можно, но внутренности контроллера это не изменит, и реальный адрес точки просто будет отличаться от видимого снаружи. Что, естественно, повысит риск ошибок, поэтому в эту сторону извращаться не будем.

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

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

Прием и передача пакетов отличается не так сильно, хотя и отняла гораздо больше времени сначала на попытки понять как же она должна работать по логике ST, потом на подгонку заклинания из интернета чтобы все-таки заработало. Как говорилось раньше, если для обычной точки два буфера независимы и отличаются направлением обмена, то для буферизованной они одинаковы и отличаются только смещением. Так что немножко изменим функции usb_ep_write и usb_ep_read чтобы они принимали не номер точки, а номер смещения. То есть если раньше эти функции предполагали существование восьми сдвоенных точек, то теперь 16 одинарных. Соответственно, номер новой полуточки на запись равен всего лишь номеру обычной, умноженному на два, а для usb_ep_read надо еще добавить единицу (см. распределение буферов в PMA). Собственно, это и делается инлайн-функциями usb_ep_write и usb_ep_read для обычных точек. А вот логику буферизованных рассмотрим чуть подробнее.

Согласно документации, один буфер такой точки доступен для железа, второй для софта. Потом они переключаются и снова не мешают друг другу. Для OUT точки флагом со стороны железа является бит USB_EP_DTOG_RX, который нужно прочитать чтобы понять в какой из буферов только что закончилась запись и откуда соответственно софт может читать. Когда он прочитал свой буфер, нужно дернуть бит USB_EP_DTOG_TX, что собственно переключит буферы. Не уверен, что подразумевалось именно это, но оно, по крайней мере, работает.

Симметричная ситуация должны была быть с IN точками. Но на практике оказалось, что и проверять, и дергать надо USB_EP_DTOG_RX. Почему не TX я так и не понял Спасибо пользователю kuzulis за ссылку на github.com/dmitrystu/libusb_stm32/edit/master/src/usbd_stm32f103_devfs.c

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

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

Для пользователя разница невелика: вместо usb_ep_init использовать usb_ep_init_double, а вместо usb_ep_write и usb_ep_read соответственно usb_ep_write_double и usb_ep_read_double.

Устройство AudioDevice


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

Согласно стандарту USB аудиоустройство представляет собой набор сущностей (entity), соединенных друг с другом в некую топологию, по которой и проходит аудиосигнал. Каждая сущность имеет свой уникальный номер (bTerminalID, он же UnitID), по которому к ней могут подключаться другие сущности или конечные точки, по нему же обращается хост, если хочет изменить какие-то параметры. И он же считается единственным выходом данной сущности. А вот входов может вообще не быть (если это входной терминал), а может быть и больше одного (bSourceID). Собственно записью в массив bSourceID номеров сущностей, от которых текущая получает аудиосигнал, мы и описываем всю топологию, которая в результате может получиться весьма резвесистой. Для примера приведу топологию покупной USB-звуковой карты (цифрами показаны bTerminalID / UnitID):

lsusb и его расшифровка
Bus 001 Device 014: ID 0d8c:013c C-Media Electronics, Inc. CM108 Audio Controller#Тут пока ничего интересногоDevice Descriptor:  bLength                18  bDescriptorType         1  bcdUSB               1.10  bDeviceClass            0   bDeviceSubClass         0   bDeviceProtocol         0   bMaxPacketSize0         8  idVendor           0x0d8c C-Media Electronics, Inc.  idProduct          0x013c CM108 Audio Controller  bcdDevice            1.00  iManufacturer           1   iProduct                2   iSerial                 0   bNumConfigurations      1  #интересное начинается тут  Configuration Descriptor:    bLength                 9    bDescriptorType         2    wTotalLength       0x00fd    bNumInterfaces          4  # общее количество интерфейсов    bConfigurationValue     1    iConfiguration          0     bmAttributes         0x80      (Bus Powered)    MaxPower              100mA    #интерфейс 0 - описание топологии    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        0      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      1 Control Device      bInterfaceProtocol      0       iInterface              0       AudioControl Interface Descriptor:        bLength                10        bDescriptorType        36        bDescriptorSubtype      1 (HEADER)        bcdADC               1.00        wTotalLength       0x0064        bInCollection           2  # ВАЖНО! количество интерфейсов данных (2)        baInterfaceNr(0)        1  #номер перовго из них        baInterfaceNr(1)        2  #номер второго ##### Топологоия ###### 1 InputTerminal (USB, на динамик)       AudioControl Interface Descriptor:        bLength                12        bDescriptorType        36        bDescriptorSubtype      2 (INPUT_TERMINAL)        bTerminalID             1  # Вот номер данной сущности        wTerminalType      0x0101 USB Streaming        bAssocTerminal          0        bNrChannels             2  # Здесь задается количество каналов        wChannelConfig     0x0003  # А здесь - их расположение в пространстве          Left Front (L)          Right Front (R)        iChannelNames           0         iTerminal               0         # 2 InputTerminal (микрофон)      AudioControl Interface Descriptor:        bLength                12        bDescriptorType        36        bDescriptorSubtype      2 (INPUT_TERMINAL)        bTerminalID             2        wTerminalType      0x0201 Microphone        bAssocTerminal          0        bNrChannels             1        wChannelConfig     0x0001          Left Front (L)        iChannelNames           0         iTerminal               0         # 6 OutputTerminal (динамик), вход соединен с сущностью 9      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      3 (OUTPUT_TERMINAL)        bTerminalID             6        wTerminalType      0x0301 Speaker        bAssocTerminal          0        bSourceID               9  # Номера входов указываются здесь        iTerminal               0         # 7 OutputTerminal (USB), вход соединен с сущностью 8      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      3 (OUTPUT_TERMINAL)        bTerminalID             7        wTerminalType      0x0101 USB Streaming        bAssocTerminal          0        bSourceID               8        iTerminal               0         # 8 Selector, входы соединены только с сущностью 10      AudioControl Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      5 (SELECTOR_UNIT)        bUnitID                 8        bNrInPins               1  # У сущностей с несколькими входами указывается их количество        baSourceID(0)          10  # а потом номера        iSelector               0         # 9 Feature, вход соединен с сущностью 15      AudioControl Interface Descriptor:        bLength                10        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                 9        bSourceID              15        bControlSize            1        bmaControls(0)       0x01          Mute Control        bmaControls(1)       0x02          Volume Control        bmaControls(2)       0x02          Volume Control        iFeature                0         # 10 Feature, вход соединен с сущностью 2      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                10        bSourceID               2        bControlSize            1        bmaControls(0)       0x43          Mute Control          Volume Control          Automatic Gain Control        bmaControls(1)       0x00        iFeature                0         # 13 Feature, вход соединен с сущностью 2      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                13        bSourceID               2        bControlSize            1        bmaControls(0)       0x03          Mute Control          Volume Control        bmaControls(1)       0x00        iFeature                0         # 15 Mixer, входы соединены с сущностями 1 и 13      AudioControl Interface Descriptor:        bLength                13        bDescriptorType        36        bDescriptorSubtype      4 (MIXER_UNIT)        bUnitID                15        bNrInPins               2  # Снова массив входов        baSourceID(0)           1  # и их номера        baSourceID(1)          13        bNrChannels             2        wChannelConfig     0x0003          Left Front (L)          Right Front (R)        iChannelNames           0         bmControls(0)        0x00        iMixer                  0 ##### конец топологии ###### Интерфейс 1 (основной) - заглушка без конечных точек    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        1      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       # Интерфейс 1 (альтернативный) - рабочий с одной конечной точкой    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        1      bAlternateSetting       1      bNumEndpoints           1      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       AudioStreaming Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      1 (AS_GENERAL)        bTerminalLink           1        bDelay                  1 frames        wFormatTag         0x0001 PCM      AudioStreaming Interface Descriptor:        bLength                14        bDescriptorType        36        bDescriptorSubtype      2 (FORMAT_TYPE)        bFormatType             1 (FORMAT_TYPE_I)        bNrChannels             2        bSubframeSize           2        bBitResolution         16        bSamFreqType            2 Discrete        tSamFreq[ 0]        48000        tSamFreq[ 1]        44100      Endpoint Descriptor:        bLength                 9        bDescriptorType         5        bEndpointAddress     0x01  EP 1 OUT        bmAttributes            9          Transfer Type            Isochronous          Synch Type               Adaptive          Usage Type               Data        wMaxPacketSize     0x00c8  1x 200 bytes        bInterval               1        bRefresh                0        bSynchAddress           0        AudioStreaming Endpoint Descriptor:          bLength                 7          bDescriptorType        37          bDescriptorSubtype      1 (EP_GENERAL)          bmAttributes         0x01            Sampling Frequency          bLockDelayUnits         1 Milliseconds          wLockDelay         0x0001          # Интерфейс 2 (основной) - заглушка    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        2      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       # Интерфейс 2 (альтернативный)    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        2      bAlternateSetting       1      bNumEndpoints           1      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       AudioStreaming Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      1 (AS_GENERAL)        bTerminalLink           7        bDelay                  1 frames        wFormatTag         0x0001 PCM      AudioStreaming Interface Descriptor:        bLength                14        bDescriptorType        36        bDescriptorSubtype      2 (FORMAT_TYPE)        bFormatType             1 (FORMAT_TYPE_I)        bNrChannels             1        bSubframeSize           2        bBitResolution         16        bSamFreqType            2 Discrete        tSamFreq[ 0]        48000        tSamFreq[ 1]        44100      Endpoint Descriptor:        bLength                 9        bDescriptorType         5        bEndpointAddress     0x82  EP 2 IN        bmAttributes            9          Transfer Type            Isochronous          Synch Type               Adaptive          Usage Type               Data        wMaxPacketSize     0x0064  1x 100 bytes        bInterval               1        bRefresh                0        bSynchAddress           0        AudioStreaming Endpoint Descriptor:          bLength                 7          bDescriptorType        37          bDescriptorSubtype      1 (EP_GENERAL)          bmAttributes         0x01            Sampling Frequency          bLockDelayUnits         0 Undefined          wLockDelay         0x0000##### Конец описания аудиоинтерфейсов ###### Интерфейс 3 "Клавиши громкости и всего остального" (не интересно)    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        3      bAlternateSetting       0      bNumEndpoints           1      bInterfaceClass         3 Human Interface Device      bInterfaceSubClass      0       bInterfaceProtocol      0       iInterface              0         HID Device Descriptor:          bLength                 9          bDescriptorType        33          bcdHID               1.00          bCountryCode            0 Not supported          bNumDescriptors         1          bDescriptorType        34 Report          wDescriptorLength      60         Report Descriptors:            ** UNAVAILABLE **      Endpoint Descriptor:        bLength                 7        bDescriptorType         5        bEndpointAddress     0x87  EP 7 IN        bmAttributes            3          Transfer Type            Interrupt          Synch Type               None          Usage Type               Data        wMaxPacketSize     0x0004  1x 4 bytes        bInterval               2



image

Мы же будем делать нечто более простое (заготовку брал отсюда):

image

Здесь видно две независимых ветки распространения сигнала: либо от USB через фичу к динамику, либо из микрофона через другую фичу к USB. Микрофон и динамик не просто так взяты в кавычки: на моей отладочной плате их нет, поэтому вместо собственно звука будем пользоваться кнопками и светодиодами. Впрочем, ничего нового. Фичи в моем случае ничего не делают и добавлены скорее для красоты.

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

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

1. Входной терминал (Input Terminal)
Как следует из названия, именно через него в аудиоустройство попадает звуковой сигнал. Это может быть USB, может быть микрофон обыкновенный, микрофон гарнитурный, даже микрофонный массив.

2. Выходной терминал (Output Terminal)
Тоже вполне очевидно то, через что звук покидает наше устройство. Это может быть все тот же USB, может быть динамик, гарнитура, динамик в мониторе, динамики различных частот и куча других устройств.

3. Микшер (Mixer Unit)
Берет несколько входных сигналов, усиливает каждый на заданную величину и складывает то, что получилось, в выходной канал. При желании можно задать усиление в ноль раз, что сведет его к следующей сущности.

4. Селектор (Selector Unit)
Берет несколько входных сигналов и перенаправляет один из них на выход.

5. Фильтр (Feature Unit)
Берет единственный входной сигнал, меняет параметры звука (громкость, тембр и т.п.) и выдает на выход. Естественно, все эти параметры одинаковым способом прикладываются ко всему сигналу, без взаимодействия логических каналов внутри него

6. Processing Unit
А вот эта штука уже позволяет проводить манипуляции над отдельными логическими каналами внутри каждого входного. Более того, позволяет сделать количество логических каналов в выходном не равным количеству во входных.

7. Extension Unit
Весь набор нестандартных сущностей, чтобы больной фантазии производителей оборудования было раздолье. Соответственно, и поведение, и настройки будут зависеть от этой самой фантазии.

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

Грабли в дескрипторе


В отличие от предыдущих USB-устройств, здесь дескриптор сложный, многоуровневый и склонный пугать виндоусы до BSOD'а. Как мы видели выше, топология у аутиоустройства может быть весьма сложной и развесистой. Под ее описание выделяется целый интерфейс. Очевидно, endpoint'ов он содержать не будет, зато будет содержать список дескрипторов сущностей и описаний к чему подключены их входы. Тут особо описывать смысла не вижу, проще посмотреть в коде и документации. Отмечу только главные грабли: здесь описывается какие интерфейсы с соответствующими конечными точками относятся именно к данному устройству. Скажем, если вы захотите изменить мою конфигурацию и убрать оттуда динамик, придется не просто удалить половину сущностей (слава макросам, хотя бы с подсчетом длины дескриптора проблемы не будет), но и уменьшить поле bInCollection до 1, после чего из следующего за ним массива bInterfaceNr убрать номер лишнего интерфейса.

Дальше находятся интерфейсы, отвечающие за обмен данными. В моем случае 1-й интерфейс отвечает за микрофон, а 2-й за динамик. Здесь стоит обратить внимание в первую очередь на два варианта каждого из этих интерфейсов. Один с bAlternateSetting равным 0, второй с 1. Они отличаются наличием конечной точки. То есть если наше устройство в данный момент не используется, хост просто переключается на тот альтернативный интерфейс, который конечной точкой не оборудован, и уже не тратит на нее пропускную способность шины.

Вторая особенность интерфейсов данных это формат аудиосигнала. В соответствующем дескрипторе задается тип кодирования, количество каналов, разрешение и частота дискретизации (которая задается 24-битным числом). Вариантов кодирования предусмотрено довольно много, но мы будем использовать самый простой PCM. По сути это просто последовательность значений мгновенной величины сигнала без какого-либо кодирования, причем величина считается целым числом со знаком. Разрешение сигнала задается в двух местах (зачем непонятно): в поле bSubFrameSize указывается количество байтов, а в bBitResolution количество битов. Вероятно, можно указать, что диапазон нашей звуковой карты не доходит до полного диапазона типа данных, скажем, int16_t и составляет всего 10 бит.

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

Ах да, чуть не забыл упомянуть очередную пачку BSOD'ов при тестировании неправильных дескрипторов. Еще раз напоминаю: количество интерфейсов данных должно соответствовать числу bInCollection, а их номера следующему за ним массиву!
Скрытый текст
Как представлю отладку подобного кода под виндами, с этими постоянными вылетами, да еще без нормальной консоли. бр-р-р.


Логика работы устройства


Как я уже говорил, для тестов не имеет смысла городить на отладочную плату навесные компоненты, поэтому все тестирование будет осуществляться тем, что уже установлено кнопки да светодиоды. Впрочем, в данном случае это проблемы не составляет: микрофон может просто генерировать синусоиду частотой, скажем, 1 кГц, а динамик включать светодиод при превышении порогового значения звука (скажем, выше числа 10000: при указанных 16 битах разрешения, что соответствует диапазону -32768 +32767, это примерно треть).

А вот с тестированием возникла небольшая проблема: я не нашел простого способа перенаправить сигнал с микрофона на stdin какой-нибудь программы. Вроде бы раньше это делалось просто чтением /dev/dsp, но сейчас что-то поломалось. Впрочем, ничего критичного, ведь есть всякие библиотеки взаимодействия с мультимедией SDL, SFLM и другие. Собственно на SFML я и написал простенькую утилиту для чтения с микрофона и, если надо, визуализации сигнала.

Особое внимание уделю ограничениям нашего аудиоустройства: насколько я понял, изохронный запрос IN отправляется один раз в миллисекунду (а вот OUT'ов может быть много), что ограничивает частоту дискретизации. Допустим, размер конечной точки у нас 64 байта (учитывая буферизацию, в памяти она занимает 128 байт, но хост об этом не знает), разрешение 16 бит, то есть за раз можно отправить 32 отсчета. Учитывая интервал в 1 мс получаем теоретический предел 32 кГц для одного канала. Самый простой способ это обойти увеличить размер конечной точки. Но тут надо помнить, что размер общего буфера PMA у нас всего 512 байт. Минус таблица распределения точек, минус ep0, получаем максимум 440 байт, то есть 220 байт на единственную точку с учетом буферизации. И это теоретический предел.

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

Заключение (общее для цикла)


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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru