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

Arm

Перевод Как преобразовать текст в речь с использованием Google Tesseract и Arm NN на Raspberry Pi

17.02.2021 16:08:10 | Автор: admin

Привет, Хабр! Сегодня специально к старту нового потока курса по Maсhine Learning делимся с вами постом, автор которого создаёт устройство преобразования текста в речь. Такой механизм преобразования текста в речь (TTS) ключевой элемент систем, которые стремятся сформировать естественное взаимодействие между людьми и машинами на основе встроенных устройств. Встроенные устройства могут, например, помочь людям с нарушениями зрения читать знаки, буквы и документы. В частности, устройство может, используя оптическое распознавание символов, дать понять пользователю, что видно на изображении. Впрочем, приступим к крафту



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

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

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

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

В этой статье я покажу, как это сделать с помощью TensorFlow, OpenCV, Festival и Raspberry Pi. Для оптического распознавания текста я буду использовать платформу машинного обучения TensorFlow вместе с предварительно обученной моделью Keras-OCR. Библиотека OpenCV будет использоваться для захвата изображений с веб-камеры. Наконец, в качестве TTS-модуля будет выступать система синтеза речи Festival. Затем всё соединим, чтобы создать приложение на Python для Raspberry Pi.

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

Начало работы


Во-первых, чтобы создать устройство и приложение для этого туториала, понадобится Raspberry Pi. Для этого примера подойдут версии 2, 3 или 4. Вы также можете использовать собственный компьютер для разработки (мы тестировали код для Python 3.7).

Необходимо установить два пакета: tensorflow (2.1.0) и keras_ocr (0.7.1). Вот несколько полезных ссылок:


OCR с помощью рекуррентных нейронных сетей


Здесь для распознавания текста на изображениях я использую пакет keras_ocr. Этот пакет основан на платформе TensorFlow и свёрточной нейронной сети, которая первоначально была опубликована в качестве примера OCR на веб-сайте Keras.

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

Рекуррентные нейронные сети (РНС) обычно состоят из слоёв долгой краткосрочной памяти (LTSM). Долгая краткосрочная память произвела революцию во многих применениях ИИ, включая распознавание речи, создание субтитров к изображениям и анализ временных рядов. OCR-модели используют РНС для создания так называемой матрицы вероятностей символов. Эта матрица определяет степень уверенности в том, что заданный символ находится в конкретной части входного изображения.

Таким образом, на последнем этапе эта матрица используется для декодирования текста на изображении. Обычно люди используют алгоритм классификации по рейтингу (Connectionist Temporal Classification, CTC). CTC стремится преобразовать матрицу в осмысленное слово или последовательность таких слов. Такое преобразование не тривиальная задача, так как в соседних частях изображения могут быть найдены одинаковые символы. Кроме того, некоторые входные части могут не содержать символов.

Хотя OCR-системы на основе РНС эффективны, пытаясь внедрить их в свои проекты, можно столкнуться с множеством проблем. В идеале необходимо выполнить обучение преобразованию, чтобы настроить модель в соответствие со своими данными. Затем модель преобразуется в формат TensorFlow Lite, чтобы оптимизировать для вывода на оконечное устройство. Такой подход оказался успешным в мобильных приложениях компьютерного зрения. Например, многие предварительно обученные сети MobileNet эффективно классифицируют изображения на мобильных устройствах и устройствах Интернета вещей.

Однако TensorFlow Lite представляет собой подмножество TensorFlow, и поэтому в настоящее время поддерживается не каждая операция. Эта несовместимость становится проблемой, когда необходимо выполнить оптическое распознавание символов, подобное тому, что включено в пакет keras-ocr на устройстве Интернета вещей. Список возможных решений предоставлен на официальном сайте TensorFlow.

В этой статье я покажу, как использовать модель TensorFlow, поскольку двунаправленные слои LSTM (используемые в keras-ocr) еще не поддерживаются в TensorFlow Lite.

Предварительно обученная OCR-модель


Для начала я написал тестовый скрипт (ocr.py), который показывает, как использовать модель нейронной сети из keras-ocr:

# Importsimport keras_ocrimport helpers # Prepare OCR recognizerrecognizer = keras_ocr.recognition.Recognizer() # Load images and their labelsdataset_folder = 'Dataset'image_file_filter = '*.jpg' images_with_labels = helpers.load_images_from_folder(dataset_folder, image_file_filter) # Perform OCR recognition on the input imagespredicted_labels = []for image_with_label in images_with_labels:predicted_labels.append(recognizer.recognize(image_with_label[0])) # Display resultsrows = 4cols = 2font_size = 14helpers.plot_results(images_with_labels, predicted_labels, rows, cols, font_size)

Этот скрипт создаёт экземпляр объекта Recognizer на основе модуля keras_ocr.recognition. Затем скрипт загружает изображения и их метки из прикреплённого набора тестовых данных (папка Dataset). Этот набор данных содержит восемь случайно выбранных изображений из набора синтетических слов (Synth90k). Затем скрипт запускает оптическое распознавание символов на каждом изображении этого набора данных, а затем отображает результаты прогнозирования.



Для загрузки изображений и их меток я использую функцию load_images_from_folder, которую я реализовал в модуле helpers. Этот метод предполагает два параметра: путь к папке с изображениями и фильтр. Здесь я предполагаю, что изображения находятся в подпапке Dataset, и я читаю все изображения в формате JPEG (с расширением имени файла .jpg).

В наборе данных Synth90k каждое имя файла изображения содержит метку изображения между символами подчёркивания. Например: 199_pulpiest_61190.jpg. Таким образом, чтобы получить метку изображения, функция load_images_from_folder разделяет имя файла по символу подчёркивания, а затем берёт первый элемент полученной коллекции строк. Также обратите внимание, что функция load_images_from_folder возвращает массив кортежей. Каждый элемент такого массива содержит изображение и соответствующую метку. По этой причине я передаю обработчику OCR только первый элемент этого кортежа.

Для распознавания я использую метод распознавания объекта Recognizer. Этот метод возвращает прогнозируемую метку, которую я сохраняю в коллекции predicted_labels.

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

Камера


После тестирования OCR-модели я реализовал класс camera. В этом классе используется библиотека OpenCV, которая была установлена вместе с модулем keras-ocr. OpenCV предоставляет собой удобный программный интерфейс для доступа к камере. В явном виде вы сначала инициализируете объект VideoCapture, а затем вызываете его метод чтения (read), чтобы получить изображение с камеры.

import cv2 as opencv class camera(object):def __init__(self):# Initialize the camera captureself.camera_capture = opencv.VideoCapture(0)def capture_frame(self, ignore_first_frame):# Get frame, ignore the first one if neededif(ignore_first_frame):self.camera_capture.read()(capture_status, current_camera_frame) = self.camera_capture.read() # Verify capture statusif(capture_status):return current_camera_frame else:# Print error to the consoleprint('Capture error')

В этом коде я создал объект VideoCapture в инициализаторе класса camera. Я передаю объекту VideoCapture значение 0, чтобы указать на камеру системы по умолчанию. Затем я сохраняю полученный объект в поле camera_capture класса camera.

Чтобы получать изображения с камеры, я реализовал метод capture_frame. У него есть дополнительный параметр, ignore_first_frame. Когда значение этого параметра равно True, я дважды вызываю метод caper_capture.read, но игнорирую результат первого вызова. Смысл этой операции заключается в том, что первый кадр, возвращаемый моей камерой, обычно пуст.

Второй вызов метода read дает статус захвата и кадр. Если сбор данных был успешным (capture_status = True), я возвращаю кадр камеры. В противном случае я печатаю строку Ошибка захвата.

Преобразование текста в речь


Последний элемент данного приложения TTS-модуль. Было решено использовать здесь систему Festival, потому что она может работать в автономном режиме. Другие возможные подходы к TTS хорошо описаны в статье Adafruit Speech Synthesis on the Raspberry Pi (Синтез речи на Raspberry Pi).
Чтобы установить Festival на Raspberry Pi, выполните следующую команду:

sudo apt-get install festival -y

Убедиться в том, что всё работает правильно, можно, введя следующую команду:

echo "Hello, Arm" | Festival tts

Ваш Raspberry Pi должен произнести: Hello, Arm.
Festival предоставляет API-интерфейс. Однако для простоты было решено взаимодействовать с Festival посредством командной строки. С этой целью модуль helpers был дополнен ещё одним методом:

def say_text(text):os.system('echo ' + text + ' | festival --tts')

Собираем всё вместе


Наконец, мы можем собрать всё вместе. Я сделал это в скрипте main.py:

import keras_ocrimport camera as camimport helpers if __name__ == "__main__":# Prepare recognizerrecognizer = keras_ocr.recognition.Recognizer() # Get image from the cameracamera = cam.camera() # Ignore the first frame, which is typically blank on my machineimage = camera.capture_frame(True) # Perform recognitionlabel = recognizer.recognize(image) # Perform TTS (speak label)helpers.say_text('The recognition result is: ' + label)

Сначала я создаю OCR-распознаватель. Затем я создаю объект Camera и считываю кадр с веб-камеры по умолчанию. Изображение передаётся распознавателю, а полученная в результате метка произносится вспомогательным TTS-модулем.

Заключение


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

В более сложном сценарии распознаванию текста может предшествовать обнаружение текста. Сначала на изображении обнаруживаются строки текста, а затем распознаётся каждая из них. Для этого потребуются только возможности пакета keras-ocr по обнаружению текста. Это было показано в данной версии реализации Keras CRNN и опубликованной модели обнаружения текста CRAFT Фаусто Моралесом.

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

Хочется завершить этот материал цитатой третьего закона Артура Кларка:

Любая достаточно развитая технология неотличима от магии.

Если следовать ему то можно спокойно сказать, что у нас в SkillFactory мы обучаем людей настоящей магии, просто она называется data science и machine learning.



image
Подробнее..

Перевод Как распознать рукописный текст с помощью ИИ на микроконтроллерах

18.02.2021 18:15:13 | Автор: admin


Распознавание рукописных цифр с помощью TensorFlow и MNIST стало довольно распространённым введением в искусственный интеллект (ИИ) и ML. MNIST это база данных, которая содержит 70 000 примеров рукописных цифр. Она широко используется как источник изображений для обучения систем обработки изображений и программного обеспечения для машинного обучения.

Хотя учебные пособия по ML с использованием TensorFlow и MNIST стали привычными, до недавнего времени они обычно демонстрировались в полнофункциональных средах обработки с архитектурой x86 и графическими процессорами класса рабочих станций. Однако сегодня можно создать полнофункциональное приложение для распознавания рукописного ввода MNIST даже на 8-разрядном микроконтроллере. Чтобы продемонстрировать это, мы собираемся создать полнофункциональное приложение для распознавания рукописного ввода MNIST, используя TensorFlow Lite для получения результатов ИИ на маломощном микроконтроллере STMicroelectronics на базе процессора ARM Cortex M7.



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



Для создания этого проекта потребуется несколько компонентов:


Код этого проекта можно найти на GitHub.

Краткий обзор


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

  1. Обучить прогнозирующую модель на основе набора данных (рукописные цифры MNIST).
  2. Преобразовать модель в формат TensorFlow Lite.
  3. Создать встроенное приложение.
  4. Создать образцы данных.
  5. Развернуть и протестировать приложение.

Чтобы ускорить и упростить этот процесс, я создал записную книжку Jupyter в Google Colab, чтобы сделать первые два шага за вас из вашего браузера, не устанавливая и не настраивая Python на вашем компьютере. Она также может служить справочным материалом для других проектов, поскольку содержит весь код, необходимый для обучения и оценки модели MNIST с помощью TensorFlow, а также для преобразования модели в целях автономного использования в TensorFlow Lite для микроконтроллеров и создания версии кода массива Си модели для простой компиляции в любую программу на C++.

Чтобы перейти к встроенному приложению на шаге 3, сначала в меню записной книжки нажмите Runtime Run All (Время выполнения > Выполнить всё), чтобы создать файл model.h. Загрузите его из списка файлов на левой стороне. Также можно загрузить предварительно созданную модель из репозитория GitHub, чтобы включить её в проект.

Чтобы выполнить эти действия локально на своём компьютере, убедитесь, что используете платформу TensorFlow версии 2.0 или более поздней и дистрибутив Anaconda для установки и использования Python. Если вы используете упомянутую ранее записную книжку Jupyter, о которой говорилось выше, вам не придётся беспокоиться об установке TensorFlow 2.0, так как эта версия входит в состав этой записной книжки.

Обучение модели TensorFlow с использованием MNIST


Keras это высокоуровневая библиотека Python для нейронных сетей, часто используемая для создания прототипов ИИ-решений. Она интегрирована с TensorFlow, а также содержит встроенный набор данных MNIST из 60 000 изображений и 10 000 тестовых образцов, доступных прямо в TensorFlow.

Чтобы прогнозировать рукописные цифры, этот набор данных использовался для обучения относительно простой модели, в которой изображение 2828 принимается в качестве входной формы и выводятся до 10 категорий результатов с помощью функции активации Softmax с одним скрытым слоем между входным и выходным слоями. Этого было достаточно для достижения точности 96,6 %, но при желании можно добавить больше скрытых слоёв или тензоров.

За более глубоким обсуждением работы с набором данных MNIST в TensorFlow я рекомендую обратиться к некоторым (из многих) замечательным учебным пособиям по TensorFlow в Интернете, таким как Not another MNIST tutorial with TensorFlow, автор О'Рейли (O'Reilly). Вы также можете обратиться к примеру синусоидальной модели TensorFlow в этой записной книжке, чтобы ознакомиться с обучением и оценкой моделей TensorFlow и преобразованием модели в формат TensorFlow Lite для микроконтроллеров.



Преобразование модели в формат TensorFlow Lite


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

В этом случае платформа TensorFlow Lite нужна, чтобы приложение занимало как можно меньше места во флеш-памяти и ОЗУ, оставаясь при этом быстрым, чтобы можно было немного понизить точность, не жертвуя слишком многим.

Чтобы ещё больше уменьшить размер, преобразователь TensorFlow Lite поддерживает дискретизацию модели, чтобы перейти в вычислениях от 32-разрядных значений с плавающей запятой к 8-разрядным целым числам, так как часто высокая точность значений с плавающей запятой не требуется. В результате также значительно уменьшается размер модели и повышается производительность.

Мне не удалось получить дискретизированную модель для правильного и согласованного использования функции Softmax. На моём устройстве STM32F7 Discovery возникает ошибка не удалось вызвать. Преобразователь TensorFlow Lite постоянно развивается, и некоторые конструкции моделей ещё не поддерживаются. Например, этот инструмент преобразует некоторые веса в значения типа int8 вместо uint8, а тип int8 не поддерживается. По крайней мере пока.

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

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

Я добавил скрипт Python в конец своей записной книжки, чтобы обработать эту часть и превратить её в файл model.h. При желании в Linux с помощью команды оболочки xxd -i созданный tflite-файл также можно преобразовать в массив Си. Загрузите этот файл из меню слева и приготовьтесь добавить его в проект встроенного приложения на следующем шаге.

import binasciidef convert_to_c_array(bytes) -> str:  hexstr = binascii.hexlify(bytes).decode("UTF-8")  hexstr = hexstr.upper()  array = ["0x" + hexstr[i:i + 2] for i in range(0, len(hexstr), 2)]  array = [array[i:i+10] for i in range(0, len(array), 10)]  return ",\n  ".join([", ".join(e) for e in array])tflite_binary = open("model.tflite", 'rb').read()ascii_bytes = convert_to_c_array(tflite_binary)c_file = "const unsigned char tf_model[] = {\n  " + ascii_bytes +   "\n};\nunsigned int tf_model_len = " + str(len(tflite_binary)) + ";"# print(c_file)open("model.h", "w").write(c_file)

Создание встроенного приложения


Теперь мы готовы взять нашу обученную модель MNIST и реализовать её на реальном маломощном микроконтроллере. Ваши конкретные действия могут зависеть от используемого набора инструментов, но с моими интегрированной средой разработки PlatformIO и устройством STM32F746G Discovery мною были предприняты следующие действия.

Сначала создан новый проект приложения с настройками для соответствующего устройства на базе ARM Cortex-M и подготовлены основные функции setup и loop. Я выбрал структуру Stm32Cube, чтобы выводить результаты на экран. Если вы используете Stm32Cube, вы можете загрузить файлы stm32_app.h и stm32_app.c из репозитория и создать файл main.cpp с функциями setup и loop, например, как здесь:

#include "stm32_app.h"void setup() {}void loop() {}



Добавьте или загрузите библиотеку TensorFlow Lite Micro. Я предварительно настроил библиотеку для интегрированной среды разработки PlateformIO, чтобы вы могли загрузить папку tfmicro отсюда в папку lib проекта и добавить её в качестве зависимости библиотеки в файл platformio.ini:

[env:disco_f746ng]platform = ststm32board = disco_f746ngframework = stm32cubelib_deps = tfmicro

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

#include "stm32_app.h"#include "tensorflow/lite/experimental/micro/kernels/all_ops_resolver.h"#include "tensorflow/lite/experimental/micro/micro_error_reporter.h"#include "tensorflow/lite/experimental/micro/micro_interpreter.h"#include "tensorflow/lite/schema/schema_generated.h"#include "tensorflow/lite/version.h"void setup() {}void loop() {}

Включите преобразованный ранее файл model.h в этот проект в папку Include и добавьте его под заголовками TensorFlow. Затем сохраните результат и выполните сборку, чтобы убедиться, что всё в порядке, ошибок нет.

#include "model.h"



Определите для TensorFlow следующие глобальные переменные, которые будут использоваться в вашем коде:

// Globalsconst tflite::Model* model = nullptr;tflite::MicroInterpreter* interpreter = nullptr;tflite::ErrorReporter* reporter = nullptr;TfLiteTensor* input = nullptr;TfLiteTensor* output = nullptr;constexpr int kTensorArenaSize = 5000; // Just pick a big enough numberuint8_t tensor_arena[ kTensorArenaSize ] = { 0 };float* input_buffer = nullptr;

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

void setup() {  // Load Model  static tflite::MicroErrorReporter error_reporter;  reporter = &error_reporter;  reporter->Report( "Let's use AI to recognize some numbers!" );  model = tflite::GetModel( tf_model );  if( model->version() != TFLITE_SCHEMA_VERSION ) {reporter->Report(   "Model is schema version: %d\nSupported schema version is: %d",   model->version(), TFLITE_SCHEMA_VERSION );return;  }   // Set up our TF runner  static tflite::ops::micro::AllOpsResolver resolver;  static tflite::MicroInterpreter static_interpreter(  model, resolver, tensor_arena, kTensorArenaSize, reporter );  interpreter = &static_interpreter;   // Allocate memory from the tensor_arena for the model's tensors.  TfLiteStatus allocate_status = interpreter->AllocateTensors();  if( allocate_status != kTfLiteOk ) {reporter->Report( "AllocateTensors() failed" );return;  }  // Obtain pointers to the model's input and output tensors.  input = interpreter->input(0);  output = interpreter->output(0);  // Save the input buffer to put our MNIST images into  input_buffer = input->data.f;}

Подготовьте TensorFlow к выполнению на устройстве ARM Cortex-M при каждом вызове функции loop с короткой задержкой (одна секунда) между обновлениями, например, как здесь:

void loop() {  // Run our model  TfLiteStatus invoke_status = interpreter->Invoke();  if( invoke_status != kTfLiteOk ) {reporter->Report( "Invoke failed" );return;  }   float* result = output->data.f;  char resultText[ 256 ];  sprintf( resultText, "It looks like the number: %d", std::distance( result, std::max_element( result, result + 10 ) ) );  draw_text( resultText, 0xFF0000FF );  // Wait 1-sec til before running again  delay( 1000 );}

Приложение готово к работе. Оно просто ждёт, когда мы скормим ему несколько забавных тестовых изображений MNIST для обработки!

Создание образца данных MNIST для встраивания


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

Чтобы добавить эти изображения в программу независимо от внешнего хранилища, мы можем заранее преобразовать 100 изображений MNIST из формата JPEG в чёрно-белые изображения, сохранённые в виде массивов, так же как и наша модель TensorFlow. Для этого я использовал веб-инструмент с открытым исходным кодом под названием image2cpp, который выполняет большую часть этой работы за нас в одном пакете. Если вы хотите сгенерировать их самостоятельно, проанализируйте пиксели и закодируйте по восемь в каждый байт и запишите их в формате массива Си, как показано ниже.

ПРИМЕЧАНИЕ. Веб-инструмент генерирует код для интегрированной среды разработки Arduino, поэтому в коде найдите и удалите все экземпляры PROGMEM, а затем компилируйте код в среде PlatformIO.

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



// 'mnist_0_1', 28x28pxconst unsigned char mnist_1 [] PROGMEM = {  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x00,  0x00, 0x3f, 0xe0, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x7e, 0x30, 0x00, 0x00, 0xfc, 0x38, 0x00,  0x00, 0xf0, 0x1c, 0x00, 0x00, 0xe0, 0x1c, 0x00, 0x00, 0xc0, 0x1e, 0x00, 0x00, 0xc0, 0x1c, 0x00,  0x01, 0xc0, 0x3c, 0x00, 0x01, 0xc0, 0xf8, 0x00, 0x01, 0xc1, 0xf8, 0x00, 0x01, 0xcf, 0xf0, 0x00,  0x00, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

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

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

const unsigned char* test_images[] = {  mnist_1, mnist_2, mnist_3, mnist_4, mnist_5,   mnist_6, mnist_7, mnist_8, mnist_9, mnist_10,  mnist_11, mnist_12, mnist_13, mnist_14, mnist_15,   mnist_16, mnist_17, mnist_18, mnist_19, mnist_20,  mnist_21, mnist_22, mnist_23, mnist_24, mnist_25,   mnist_26, mnist_27, mnist_28, mnist_29, mnist_30,  mnist_31, mnist_32, mnist_33, mnist_34, mnist_35,   mnist_36, mnist_37, mnist_38, mnist_39, mnist_40,  mnist_41, mnist_42, mnist_43, mnist_44, mnist_45,   mnist_46, mnist_47, mnist_48, mnist_49, mnist_50,  mnist_51, mnist_52, mnist_53, mnist_54, mnist_55,   mnist_56, mnist_57, mnist_58, mnist_59, mnist_60,  mnist_61, mnist_62, mnist_63, mnist_64, mnist_65,   mnist_66, mnist_67, mnist_68, mnist_69, mnist_70,  mnist_71, mnist_72, mnist_73, mnist_74, mnist_75,   mnist_76, mnist_77, mnist_78, mnist_79, mnist_80,  mnist_81, mnist_82, mnist_83, mnist_84, mnist_85,   mnist_86, mnist_87, mnist_88, mnist_89, mnist_90,  mnist_91, mnist_92, mnist_93, mnist_94, mnist_95,   mnist_96, mnist_97, mnist_98, mnist_99, mnist_100,};

Не забудьте включить в верхнюю часть кода заголовок нового изображения:

#include "mnist.h"

Тестирование изображений MNIST


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

void bitmap_to_float_array( float* dest, const unsigned char* bitmap ) { // Populate input_vec with the monochrome 1bpp bitmap  int pixel = 0;  for( int y = 0; y < 28; y++ ) {for( int x = 0; x < 28; x++ ) {  int B = x / 8; // the Byte # of the row  int b = x % 8; // the Bit # of the Byte  dest[ pixel ] = ( bitmap[ y * 4 + B ] >> ( 7 - b ) ) & 0x1 ? 1.0f : 0.0f;  pixel++;}  }}void draw_input_buffer() {  clear_display();  for( int y = 0; y < 28; y++ ) {for( int x = 0; x < 28; x++ ) {  draw_pixel( x + 16, y + 3, input_buffer[ y * 28 + x ] > 0 ? 0xFFFFFFFF : 0xFF000000 );}  }}

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

void loop() {  // Pick a random test image for input  const int num_test_images = ( sizeof( test_images ) / sizeof( *test_images ) );  bitmap_to_float_array( input_buffer,  test_images[ rand() % num_test_images ] );  draw_input_buffer();   // Run our model  ...}

Если всё в порядке, ваш проект будет скомпонован и развёрнут, и вы увидите, как ваш микроконтроллер распознаёт все рукописные цифры и выдаёт отличные результаты! Верите?




Что дальше?


Теперь, когда вы узнали о возможностях маломощных микроконтроллеров ARM Cortex-M, позволяющих использовать возможности глубокого обучения с помощью TensorFlow, вы готовы сделать гораздо больше! От обнаружения животных и предметов различных типов до обучения устройства понимать речь или отвечать на вопросы вы со своим устройством можете открыть новые горизонты, которые ранее считались возможными только при использовании мощных компьютеров и устройств.

На GitHub доступны несколько потрясающих примеров TensorFlow Lite для микроконтроллеров, разработанных командой TensorFlow. Ознакомьтесь с этимирекомендациями, чтобы убедиться, что вы максимально эффективно используете свой проект ИИ, работающий на устройстве Arm Cortex-M. А если хотите прокачать себя в Machine Learning, Data Science или поднять уровень уже имеющихся знаний приходите учиться, будет сложно, но интересно.



image
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR:

Подробнее..

Перевод Как классифицировать мусор с помощью Raspberry Pi и машинного обучения Arm NN

18.02.2021 20:14:00 | Автор: admin


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

Производительность таких решений сильно зависит от пропускной способности сети и задержки. Кроме того, отправка данных внешнему сервису может привести к проблемам с конфиденциальностью. В этой статье демонстрируется возможность переноса ИИ из облачной среды на периферию. Чтобы продемонстрировать ML с использованием периферийных ресурсов, мы будем использовать API-интерфейсы Arm NN для классификации изображений мусора с веб-камеры, подключённой к компьютеру Raspberry Pi, который покажет результаты классификации.



Arm NN и Arm Compute Library это библиотеки с открытым исходным кодом для оптимизации машинного обучения на процессорах Arm и оконечных устройствах Интернета вещей. Рабочие вычисления машинного обучения могут выполняться целиком на оконечных устройствах, что позволяет сложному программному обеспечению с поддержкой ИИ работать практически где угодно, даже без доступа к сети.

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



Что нам нужно для создания устройства и приложения?



  • Устройство Raspberry Pi. Урок проверен на устройствах Pi 2, Pi 3 и Pi 4 модели B.
  • Карту MicroSD.
  • Модуль камеры USB или MIPI для Raspberry Pi.
  • Чтобы создать собственную библиотеку Arm NN, также потребуется хост-компьютер Linux или компьютер с установленной виртуальной средой Linux.
  • Стекло, бумагу, картон, пластик, металл или любой другой мусор, который Raspberry Pi поможет вам классифицировать.



Конфигурация устройства


Я использовал Raspberry Pi 4 модель B с четырёхъядерным процессором ARM Cortex A72, встроенной памятью объёмом 1 ГБ, картой MicroSD объёмом 8 ГБ, аппаратным ключом WiFi и USB-камерой (Microsoft HD-3000).

Перед включением устройства необходимо установить ОС Raspbian на карту MicroSD, как описано в руководстве по настройке Raspberry Pi. Чтобы облегчить установку, я использовал установщик NOOBS.

Затем я загрузил Raspberry Pi, сконфигурировал Raspbian и настроил систему удалённого доступа к рабочему столу VNC для удалённого доступа к моему компьютеру Raspberry Pi. Подробные инструкции по настройке VNC можно найти на странице VNC (Virtual Network Computing) сайта RaspberryPi.org.

После настройки оборудования я занялся программным обеспечением. Данное решение состоит из трёх компонентов:
  • camera.hpp реализует вспомогательные методы захвата изображений с веб-камеры;
  • ml.hpp содержит методы загрузки модели машинного обучения и классификации изображений мусора на основе входных данных с камеры;
  • main.cpp содержит метод main, который объединяет указанные выше компоненты. Это точка входа в приложение.

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

Настройка камеры


Для получения изображений с веб-камеры я использовал библиотеку OpenCV с открытым исходным кодом для компьютерного зрения. Эта библиотека предоставляет удобный интерфейс для захвата и обработки изображений. Один и тот же API-интерфейс легко использовать для различных приложений и устройств, от Интернета вещей до мобильных устройств и настольных ПК.
Самый простой способ включить OpenCV в свои приложения Raspbian для Интернета вещей установить пакет libopencv-dev с помощью программы apt-get:

sudo apt-get updatesudo apt-get upgradesudo apt-get install libopencv-dev

После загрузки и установки пакетов можно приступать к захвату изображений с веб-камеры. Я начал с реализации двух методов: grabFrame и showFrame (см. camera.hpp в сопутствующем коде):

Mat grabFrame(){  // Open default camera  VideoCapture cap(0);  // If camera was open, get the frame  Mat frame;  if (cap.isOpened())  {cap >> frame;imwrite("image.jpg", frame);   }  else  {printf("No valid camera\n");  }  return frame;} void showFrame(Mat frame){  if (!frame.empty())  {imshow("Image", frame);waitKey(0);  }}

Первый метод, grabFrame, открывает веб-камеру по умолчанию (индекс 0) и захватывает один кадр. Обратите внимание, что интерфейс C++ OpenCV для представления изображений использует класс Mat, поэтому grabFrame возвращает объекты этого типа. Доступ к необработанным данным изображения можно получить, считав элемент данных класса Mat.

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

Для тестирования указанных выше методов я вызвал их в методе main (main.cpp):

int main(){  // Grab image  Mat frame = grabFrame();   // Display image  showFrame(frame);   return 0;}

Для создания приложения я использовал команду g++ и связал библиотеки OpenCV посредством pkg-config:

g++ main.cpp -o trashClassifier 'pkg-config --cflags --libs opencv'

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



Набор данных о мусоре и обучение модели


Модель классификации TensorFlow была обучена на основе набора данных, созданного Гэри Тунгом (Gary Thung) и доступного в его репозитории Github trashnet.

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

# Building the modelmodel = Sequential()# 3 convolutional layersmodel.add(Conv2D(32, (3, 3), input_shape = (IMG_HEIGHT, IMG_WIDTH, 3)))model.add(Activation("relu"))model.add(MaxPooling2D(pool_size=(2,2)))model.add(Conv2D(64, (3, 3)))model.add(Activation("relu"))model.add(MaxPooling2D(pool_size=(2,2)))model.add(Conv2D(64, (3, 3)))model.add(Activation("relu"))model.add(MaxPooling2D(pool_size=(2,2)))model.add(Dropout(0.25))# 2 hidden layersmodel.add(Flatten())model.add(Dense(128))model.add(Activation("relu"))model.add(Dense(128))model.add(Activation("relu"))# The output layer with 6 neurons, for 6 classesmodel.add(Dense(6))model.add(Activation("softmax"))

Модель достигла точности около 83 %. С помощью преобразователя tf.lite.TFLiteConverter мы преобразовали её в формат TensorFlow Lite trash_model.tflite.

converter = tf.lite.TFLiteConverter.from_keras_model_file('model.h5') model = converter.convert()file = open('model.tflite' , 'wb') file.write(model)

Настройка пакета средств разработки Arm NN


Следующий шаг подготовка пакета средств разработки (SDK) Arm NN. При создании библиотеки Arm NN для Raspberry Pi можно последовать учебному руководству Cross-compile Arm NN and Tensorflow for the Raspberry Pi компании Arm или выполнить автоматический сценарий из репозитория Github Tool-Solutions компании Arm для кросс-компиляции пакета средств разработки. Двоичный tar-файл Arm NN 19.08 для Raspberry Pi можно найти на GitHub.

Независимо от выбранного подхода скопируйте полученный tar-файл (armnn-dist) в Raspberry Pi. В этом случае я использую VNC для передачи файлов между моим ПК для разработки и Raspberry Pi.

Затем задайте переменную среды LD_LIBRARY_PATH. Она должна указывать на подпапку armnn/lib в armnn-dist:

export LD_LIBRARY_PATH=/home/pi/armnn-dist/armnn/lib

Здесь я предполагаю, что armnn-dist находится в папке home/pi.

Использование Arm NN для получения логических выводов на основе машинного обучения на устройстве


Загрузка меток вывода модели


Для интерпретации выходных данных модели необходимо использовать метки вывода модели. В нашем коде мы создаём строковый вектор для хранения 6 классов.

const std::vector<std::string> modelOutputLabels = {"cardboard", "glass", "metal", "paper", "plastic", "trash"};

Загрузка и предварительная обработка входного изображения


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

На входе нашей модели находится слой Conversion 2D (преобразование 2D) с идентификатором conv2d_input. Её выход слой активации функции Softmax с идентификатором activation_5/Softmax. Свойства модели извлекаются с помощью Tensorboard, инструмента визуализации, предоставленного в TensorFlow для проверки модели.

const std::string inputName = "conv2d_input";const std::string outputName = "activation_5/Softmax";const unsigned int inputTensorWidth = 256;const unsigned int inputTensorHeight = 192;const unsigned int inputTensorBatchSize = 32;const armnn::DataLayout inputTensorDataLayout = armnn::DataLayout::NHWC;

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

// Load and preprocess input imageconst std::vector<TContainer> inputDataContainers ={ PrepareImageTensor<uint8_t>("image.jpg" , inputTensorWidth, inputTensorHeight, normParams, inputTensorBatchSize, inputTensorDataLayout) } ;

Вся логика, связанная с загрузкой модели машинного обучения и выполнением прогнозов, содержится в файле ml.hpp.

Создание синтаксического анализатора и загрузка сети


Следующий шаг при работе с Armn NN создание объекта синтаксического анализатора, который будет использоваться для загрузки файла сети. В Arm NN есть синтаксические анализаторы для файлов моделей различных типов, включая TFLite, ONNX, Caffe и т. д. Синтаксические анализаторы обрабатывают создание базового графа Arm NN, поэтому вам не нужно создавать граф модели вручную.

В этом примере используется модель TFLite, поэтому мы создаём синтаксический анализатор TfLite для загрузки модели, используя указанный путь.

Наиболее важный метод в ml.hpp это loadModelAndPredict. Сначала он создаёт синтаксический анализатор модели TensorFlow:

// Import the TensorFlow model. // Note: use CreateNetworkFromBinaryFile for .tflite files.armnnTfLiteParser::ITfLiteParserPtr parser =   armnnTfLiteParser::ITfLiteParser::Create();armnn::INetworkPtr network =   parser->CreateNetworkFromBinaryFile("trash_model.tflite");

Затем вызывается метод armnnTfLiteParser::ITfLiteParser::Create, синтаксический анализатор используется для загрузки файла trash_model.tflite.

После анализа модели создаются привязки к слоям с помощью метода GetNetworkInputBindingInfo/GetNetworkOutputBindingInfo:

// Find the binding points for the input and output nodesconst size_t subgraphId = 0;armnnTfParser::BindingPointInfo inputBindingInfo = parser->GetNetworkInputBindingInfo(subgraphId, inputName);armnnTfParser::BindingPointInfo outputBindingInfo = parser->GetNetworkOutputBindingInfo(subgraphId, outputName);

Для получения выходных данных модели необходимо подготовить контейнер. Размер выходного тензора равен количеству меток вывода модели. Это реализовано следующим образом:

// Output tensor size is equal to the number of model output labelsconst unsigned int outputNumElements = modelOutputLabels.size();std::vector<TContainer> outputDataContainers = { std::vector<uint8_t>(outputNumElements)};

Выбор внутренних интерфейсов, создание среды выполнения и оптимизация модели


Необходимо оптимизировать сеть и загрузить её на вычислительное устройство. Пакет средств разработки Arm NN поддерживает внутренние интерфейсы оптимизированного выполнения на центральных процессорах Arm, графических процессорах Mali и устройствах DSP. Внутренние интерфейсы идентифицируются строкой, которая должна быть уникальной для всех выходных интерфейсов. Можно указать один или несколько внутренних интерфейсов в порядке предпочтения.

В нашем коде Arm NN определяет, какие уровни поддерживаются внутренним интерфейсом. Сначала проверяется центральный процессор. Если один или несколько уровней невозможно выполнить на центральном процессоре, сначала осуществляется возврат к эталонной реализации.

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

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

// Optimize the network for a specific runtime compute // device, e.g. CpuAcc, GpuAccarmnn::IRuntime::CreationOptions options;armnn::IRuntimePtr runtime = armnn::IRuntime::Create(options);armnn::IOptimizedNetworkPtr optNet = armnn::Optimize(*network,   {armnn::Compute::CpuAcc, armnn::Compute::CpuRef},     runtime->GetDeviceSpec());

Механизм логического вывода в пакете Arm NN SDK предоставляет мост между существующими платформами нейронных сетей и центральными процессорами Arm Cortex-A, графическими процессорами Arm Mali и устройствами DSP. При получении логических выводов на основе машинного обучения с помощью Arm NN SDK алгоритмы машинного обучения оптимизируются для используемого оборудования.

После оптимизации сеть загружается в среду выполнения:

// Load the optimized network onto the runtime devicearmnn::NetworkId networkIdentifier;runtime->LoadNetwork(networkIdentifier, std::move(optNet));

Затем выполните прогнозы с помощью метода EnqueueWorkload:

// Predictarmnn::Status ret = runtime->EnqueueWorkload(networkIdentifier,  armnnUtils::MakeInputTensors(inputBindings, inputDataContainers),  armnnUtils::MakeOutputTensors(outputBindings, outputDataContainers));

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

std::vector<uint8_t> output = boost::get<std::vector<uint8_t>>(outputDataContainers[0]);size_t labelInd = std::distance(output.begin(), std::max_element(output.begin(), output.end()));std::cout << "Prediction: ";std::cout << modelOutputLabels[labelInd] << std::endl;

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

Объединение всех компонентов и создание приложения


Наконец, я собрал все компоненты вместе в методе main (main.cpp):

#include "camera.hpp"#include "ml.hpp" int main(){  // Grab frame from the camera  grabFrame(true);   // Load ML model and predict  loadModelAndPredict();   return 0;}

Обратите внимание, что у метода grabFrame есть дополнительный параметр. Если этот параметр имеет значение true, изображение камеры преобразуется в оттенки серого с изменением размеров до 256192 пикселей в соответствии с входным форматом модели машинного обучения, а затем преобразованное изображение передаётся методу loadModelAndPredict.
Для создания приложения требуется использовать команду g++ и связать библиотеку OpenCV и пакет Arm NN SDK:

g++ main.cpp -o trashClassifier 'pkg-config --cflags --libs opencv' -I/home/pi/armnn-dist/armnn/include -I/home/pi/armnn-dist/boost/include -L/home/pi/armnn-dist/armnn/lib -larmnn -lpthread -linferenceTest -lboost_system -lboost_filesystem -lboost_program_options -larmnnTfLiteParser -lprotobuf

Опять же, я предполагаю, что пакет Arm NN SDK находится в папке home/pi/armnn-dist. Запустите приложение и сделайте снимок какого-нибудь картона.

pi@raspberrypi:~/ $ ./trashClassifierArmNN v20190800Running network...Prediction: cardboard

Если во время выполнения приложения отображается сообщение error while loading shared libraries: libarmnn.so: cannot open shared object file: No such file or directory (ошибка при загрузке общих библиотек: libarmnn.so, не удаётся открыть общий объектный файл: нет такого файла или каталога), убедитесь, что ваша переменная среды LD_LIBRARY_PATH задана правильно.

Данное приложение также можно улучшить, реализовав запуск захвата и распознавания изображений по внешнему сигналу. Для этого требуется изменить метод loadAndPredict в модуле ml.hpp и отделить загрузку модели от прогнозирования (логического вывода). А если хотите прокачать себя в Data Science, Machine Learning или поднять уровень уже имеющихся знаний приходите учиться, будет сложно, но интересно.

image
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение:

Подробнее..

.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
Подробнее..

Управляем контактами GPIO из C .NET 5 в Linux на одноплатном компьютере Banana Pi M64 (ARM64) и Cubietruck (ARM32)

10.05.2021 12:10:00 | Автор: admin
dotnet libgpiod

Когда заходит речь про программирование на C# .NET для одноплатных компьютеров, то разговоры крутятся только в основном вокруг Raspberry Pi на Windows IoT. А как же Banana/Orange/Rock/Nano Pi, Odroid, Pine64 и другие китайские одноплатные компьютеры работающие на Linux? Так давайте это исправим, установим .NET 5 на Banana Pi BPI-M64 (ARM64) и Cubietruck (ARM32), и будем управлять контактами GPIO из C# в Linux. В первой части серии постов, подключим светодиод и кнопку для отработки прерываний и рассмотрим библиотеку Libgpiod (спойлер, библиотеку так же можно использовать в C++, Python) для доступа к контактам GPIO.

Предисловие


Управление светодиодом и получение событий от кнопки будет реализовано через библиотеку Libgpiod, которая не является частью платформы .NET. Данная библиотека предоставляет доступ к GPIO из любого языка программирования, требуется лишь написание класса обертки.

Данный пост применим не только к платам Banana Pi BPI-M64 и Cubietruck, но и другим, основанных на процессоре ARM архитектуры armv71(32-bit) и aarch64 (64-bit). На Banana Pi BPI-M64 (ARM64) и Cubietruck (ARM32) установлена ОС Armbian версии 21.02.1, основанная на Ubuntu 18.04.5 LTS (Bionic Beaver), ядро Linux 5.10.12. uname: Linux bananapim64 5.10.12-sunxi64 #21.02.1 SMP Wed Feb 3 20:42:58 CET 2021 aarch64 aarch64 aarch64 GNU/Linux

Armbian это самый популярный дистрибутив Linux, предназначенный для одноплатных компьютеров построенных на ARM процессоре, список поддерживаемых плат огромен: Orange Pi, Banana Pi, Odroid, Olimex, Cubietruck, Roseapple Pi, Pine64, NanoPi и др. Дистрибутив Armbain основан на Debian и Ubuntu. Из большого перечня поддерживаемых одноплатных компьютеров можно выбрать то решение, которое лучше всего походит для вашего IoT проекта, от максимально энергоэффективных до высокопроизводительных плат с NPU. И на базе всех этих одноплатных компьютеров, вы сможете реализовать свое решения на платформе .NET и работать с периферийными устройствами из кода на C#.

Что такое GPIO


GPIO(general-purpose input/output) интерфейс ввода/вывода общего назначения. GPIOподключены напрямую к процессоруSoC (System-on-a-Chip Система на кристалле), и неправильное использование может вывести его из строя. Большинство одноплатных компьютеров, кроме обычных двунаправленных Input/Output портов, имеют один или более интерфейсов: UART,SPI,IC/TWI,PWM (ШИМ), но не имеютADC (АЦП). GPIO- порты обычно могут быть сконфигурированны на ввод или вывод (Input/Output), состояние по умолчанию обычноINPUT.

Некоторые GPIO-порты являются просто питающими портами 3.3V, 5V и GND, они не связаны сSoCи не могут использоваться как либо еще.

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

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

Работа с контактами GPIOосуществляется через виртуальную файловую систему sysfs. стандартный интерфейс для работы с контактами sysfs впервые появился с версии ядра 2.6.26, в Linux. Работа с GPIO проходит через каталог /sys/class/gpio путём обращения к файлам-устройствам.

К портам GPIO подключаются:

  • светодиоды;
  • кнопки;
  • реле;
  • температурные и другие датчики;
  • различные периферийные устройства.

Для программирования GPIO существует несколько способов обращения:

  • Посредством файл-устройства GPIO;
  • Используя языки программирования:
    • Через прямое обращение к регистрам чипа;
    • Используя уже готовые библиотеки (libgpiod).


Одноплатный компьютер Banana Pi BPI-M64


Banana Pi BPI-M64 это 64-битный четырехъядерный мини-одноплатный компьютер, поставляемый как решение с открытым исходном кодом. Ядром системы является процессор Allwinner A64 с 4-мя ядрами Cortex-A53 с частотой 1.2 ГГц. На плате размещено 2 ГБ DDR3 SDRAM 733МГц оперативной памяти и 8 ГБ eMMC.

На плате размещен 40-контактный совместимый с Raspberry Pi разъем, который содержит: GPIO (x28), Power (+5V, +3.3V and GND), UART, I2C, SPI. И 40-контактный интерфейс MIPI DSI.

dotnet libgpiod
Banana Pi BPI-M64 и 40-контактный разъем типа Raspberry Pi 3

Наличие 40-контактного разъема типа Raspberry Pi 3 GPIO, существенно облегчает подключение датчиков из-за совпадение назначение контактов с Raspberry Pi 3. Не приходится гадать к какому контакту подключать тот или иной датчик. Указанные в посте датчики (светодиод и кнопка) подключенные к Banana Pi BPI-M64, можно подключать на те же самые контакты другого одноплатного компьютера, на котором тоже есть 40-контактный разъем, типа Raspberry Pi 3 (или к самой Raspberry Pi 3, разницы нет никакой). Единственное, необходимо изменить номера контактов (линий, ножка процессора) в программном коде, т.к. они зависят от используемого процессора. Но легко определяются но названию контакта. Плата Cubietruck (ARM32) приведена для проверки совместимости и работы кода на 32-разрядных ARM процессорах.

Banana Pi BPI-M64 GPIO Header Position
Позиция [1] 3V3 power соответствует позиции на плате со стрелочкой

Формула для вычисления номера GPIOXX
Для обращение к контактам из C# кода необходимо знать порядковый номер (линия, порт) физической ножки процессора SoC(для Allwinner). Эти данные в спецификациях отсутствую, т.к. порядковый номер получаем путем простого расчета. Например, из схемы возьмем 32-контакт на разъеме типа Raspberry Pi. Название контакта PB7, для получения номера контакта на процессоре произведем расчет по формуле:
(позиция буквы в алфавите 1) * 32 + позиция вывода.Первая буква не учитывается т.к. P PORT, позиция буквы B в алфавите = 2, получаем (2-1) * 32 + 7 = 39. Физический номер контакта PB7является номер 39. У каждого разработчика SoC может быть свой алгоритм расчета номера контактов, должен быть описан в Datasheet к процессору.

Banana Pi BPI-M64 GPIOXX
Контакт PB7 на процессоре Allwiner A64, номер ножки 39

Библиотеки .NET IoT


До того как напишем первую программу на C# по управления GPIO, необходимо рассмотреть пространство имен входящих в dotnet/iot. Все используемые библиотеки добавляются через Nuget пакеты. Подробно рассмотрим драйвера для получения доступа к контактам GPIO одноплатного компьютера. Код на C# взаимодействует с GPIO через специальный драйвер, который является абстракцией доступа к GPIO и позволяет переносить исходный код от одного одноплатного компьютера к другому, без изменений.

Пространства имен .NET IoT:

  • System.Device.Gpio. Пакет System.Device.Gpio поддерживает множество протоколов для взаимодействия с низкоуровневыми аппаратными интерфейсами:
    • General-purpose I/O (GPIO);
    • Inter-Integrated Circuit (I2C);
    • Serial Peripheral Interface (SPI);
    • Pulse Width Modulation (PWM);
    • Serial port.


  • Iot.Device.Bindings. Пакет Iot.Device.Bindings содержит:
    • Драйвера и обертки над System.Device.Gpio для различных устройств которые упрощают разработку приложений;
    • Дополнительные драйвера поддерживаемые сообществом (community-supported).


dotnet IoT Library
Стек библиотек .NET IoT

Рассмотрим первую программу типа Hello World, мигание светодиода (Blink an LED):

using System;using System.Device.Gpio;using System.Threading;Console.WriteLine("Blinking LED. Press Ctrl+C to end.");int pin = 18;using var controller = new GpioController();controller.OpenPin(pin, PinMode.Output);bool ledOn = true;while (true){    controller.Write(pin, ((ledOn) ? PinValue.High : PinValue.Low));    Thread.Sleep(1000);    ledOn = !ledOn;}

Разбор кода:

  • using System.Device.Gpio пространство имен для использования контроллера GpioController доступа к аппаратным ресурсам;
  • using var controller = new GpioController() создает экземпляр контроллера для управления контактами GPIO;
  • controller.OpenPin(pin, PinMode.Output) инициализирует контакт pin = 18 на вывод, к 18 контакту подключен светодиод;
  • controller.Write(pin, ((ledOn)? PinValue.High: PinValue.Low)) если ledOn принимает значение True, то PinValue.High присваивает высокое значение 18 контакту и светодиод загорается. На 18 контакт подается напряжение в 3.3V. Если ledOn принимает значение False, то PinValue.Low присваивает низкое значение контакту 18 и светодиод гаснет. На 18 контакт подается напряжение в 0V (или минимальное пороговое для значения 0, может быть немного выше 0V).

Далее остается компиляция под ARM архитектуру: dotnet publish -r linux-arm или dotnet publish -r linux-arm64. Но так работает просто только для Raspberry Pi. При использование одноплатных компьютерах отличных от Raspberry Pi необходимо при инициализации GpioController выбирать драйвер доступа к GPIO.

Драйвера доступа к GPIO из .NET


Классы драйверов доступа к GPIO находятся в пространстве имен System.Device.Gpio.Drivers. Доступны следующие драйвера-классы:

  • HummingBoardDriver GPIO драйвер для платы HummingBoard на процессоре NXP i.MX 6 Arm Cortex A9;
  • LibGpiodDriver этот драйвер использует библиотеку Libgpiod для получения доступа к портам GPIO, заменяет драйвер SysFsDriver. Библиотека Libgpiod может быть установлена на Linux и Armbian, не является аппаратно-зависимой, что позволяет ее использовать для различных одноплатных компьютерах ARM32 и ARM64;
  • RaspberryPi3Driver GPIO драйвер для одноплатных компьютеров Raspberry Pi 3 или 4;
  • SysFsDriver GPIO драйвер работающий поверх интерфейса SysFs для Linux и Unux систем, предоставляет существенно меньше возможностей, чем драйвер LibGpiodDriver, но не требует установки библиотеки Libgpiod. Тот случай, когда хочется просто попробовать помигать светодиодом из C# без дополнительных действий;
  • UnixDriver базовый стандартный класс доступа к GPIO для Unix систем;
  • Windows10Driver GPIO драйвер для ОС Windows 10 IoT. Из поддерживаемых плат только Raspberry Pi, весьма ограниченное применение.

В данном посте будет рассматриваться доступ к GPIO через драйвер LibGpiodDriver. Драйвер SysFsDriver базируется на устаревшем методе работы с GPIO через виртуальную файловую систему SysFs. Для решений IoT, SysFs не подходит по трем серьезным причинам:

  • Низкая скорость работы I/O;
  • Есть проблемы с безопасной работой с GPIO при совместном доступе;
  • При контейнеризации приложения на C# в контейнер придется пробрасывать много путей из файловой системы Linux, что создается дополнительные сложности. При использование библиотеки Libgpiod этого не требуется.

Библиотека Libgpiod предназначена для работы с GPIO не только из .NET кода, но и из Python, C++, и т.д. Поэтому ниже изложенная инструкция по установке библиотеки Libgpiod позволит разработчикам на Python реализовывать подобную функциональность, как и на C#. В состав пакета Libgpiod входят утилиты для работы с GPIO. До создание программы на C#, поработаем с датчиками через эти утилиты.

Схема подключения светодиода (LED) и кнопки


Подключать светодиод и кнопку будем на 40-контактный разъем совместимый с Raspberry Pi 3. Светодиод будет подключен на 33 контакт разъема, название контакта PB4, номер линии 36. Кнопка будет подключен на 35 контакт разъема, название контакта PB6, номер линии 38. Необходимо обратить внимание на поддержку прерывания на контакте PB6 для кнопки. Поддержка прерывания необходима для исключения постоянного опроса линии с помощью CPU. На контакте PB6 доступно прерывание PB_EINT6, поэтому кнопку к этому контакту и подключим. Например, соседний контакт PL12 не имеет прерывание, поэтому подключать кнопку к нему кнопку не будем. Если вы подключаете кнопку и резистор напрямую, то не забывайте в цепь добавить резистор для сопротивления для избежания выгорания порта!

libgpiod Armbian
Схема подключения светодиода (LED) и кнопки к 40-контактному разъему совместимый с Raspberry Pi 3

libgpiod Armbian
Схема назначения контактов к которым подключается светодиод (LED) и кнопка

Интерфейс GPIO ядра Linux


GPIO (General-Purpose Input/Output) является одним из наиболее часто используемых периферийных устройств во встраиваемых системах (embedded system) Linux.

Во внутренней архитектуре ядро Linux реализует доступ к GPIO через модель производитель/потребитель. Существуют драйверы, которые предоставляют доступ к линиям GPIO (драйверы контроллеров GPIO) и драйверы, которые используют линии GPIO (клавиатура, сенсорный экран, датчики и т. д.).

В ядре Linux система gpiolib занимается регистрацией и распределением GPIO. Эта структура доступна через API как для драйверов устройств, работающих в пространстве ядра (kernel space), так и для приложений пользовательского пространства (user space).

libgpiod Armbian
Схема работы gpiolib

Старый путь: использование виртуальной файловой системы sysfs для доступа к GPIO


До версии ядра Linux 4.7 для управления GPIO в пользовательском пространстве использовался интерфейс sysfs. Линии GPIO были доступны при экспорте по пути /sys/class/gpio. Так, например, для подачи сигнала 0 или 1 на линию GPIO, необходимо:

  1. Определить номер линии (или номер ножки процессора) GPIO;
  2. Экспортировать номер GPIO, записав его номер в /sys/class/gpio/export;
  3. Конфигурировать линию GPIO как вывод, указав это в /sys/class/gpio/gpioX/direction;
  4. Установить значение 1 или 0 для линии GPIO /sys/class/gpio/gpioX/value;

Для наглядности установим для линии GPIO 36 (подключен светодиод) из пользовательского пространства, значение 1. Для этого необходимо выполнить команды:

# echo 36 > /sys/class/gpio/export# echo out > /sys/class/gpio/gpio36/direction# echo 1 > /sys/class/gpio/gpio36/value

Этот подход очень простой как и интерфейс sysfs, он неплохо работает, но имеет некоторые недостатки:

  1. Экспорт линии GPIO не связан с процессом, поэтому если процесс использующий линию GPIO аварийно завершит свою работу, то эта линия GPIO так и останется экспортированной;
  2. Учитываю первый пункт возможен совместный доступ к одной и той же линии GPIO, что приведет к проблеме совместного доступа. Процесс не может узнать у ОС используется ли та или иная линия GPIO в настоящий момент;
  3. Для каждой линии GPIO приходится выполнять множество операций open()/read()/write()/close(), а так же указывать параметры (export, direction, value, и т.д.) используя методы работы с файлами. Это усложняет программный код;
  4. Невозможно включить/выключить сразу несколько линий GPIO одним вызовом;
  5. Процесс опроса для перехвата событий (прерываний от линий GPIO) ненадежен;
  6. Нет единого интерфейса (API) для конфигурирования линий GPIO;
  7. Номера, присвоенные линиям GPIO непостоянны, их приходится каждый раз экспортировать;
  8. Низкая скорость работы с линиями GPIO;

Новый путь: интерфейс chardev


Начиная с ядра Linux версии 4.8 интерфейс GPIO sysfs объявлен как deprecated и не рекомендуется к использованию. На замену sysfs появился новый API, основанный на символьных устройствах для доступа к линиям GPIO из пользовательского пространства.

Каждый контроллер GPIO (gpiochip) будет иметь символьное устройство в разделе /dev, и мы можем использовать файловые операции (open(), read(), write(), ioctl(), poll(), close()) для управления и взаимодействия с линиями GPIO. контроллеры GPIO доступны по путям /dev/gpiochipN или /sys/bus/gpiochipN, где N порядковый номер чипа. Просмотр доступных контроллеров GPIO (gpiochip) на Banana Pi BPI-M64:

root@bananapim64:~# ls /dev/gpiochip*/dev/gpiochip0  /dev/gpiochip1  /dev/gpiochip2


libgpiod Armbian
Стек работы библиотеки libgpiod

Несмотря на то, что новый API предотвращает управление линиями GPIO с помощью стандартных инструментов командной строки, таких как echo и cat, он обладает весомыми преимуществами по сравнению с интерфейсом sysfs, а именно:

  • Выделение линий GPIO связано с процессом, который он его использует. При завершение процесса, так же в случае аварийного завершения, линии GPIO используемые процессом освобождаются автоматически;
  • Дополнительно, можно всегда определить какой процесс в данное время использует определенную линию GPIO;
  • Можно одновременно читать и писать в несколько линий GPIO одновременно;
  • Контроллеры GPIO и линии GPIO можно найти по названию;
  • Можно настроить состояние вывода контакта (open-source, open-drain и т. д.);
  • Процесс опроса для перехвата событий (прерывания от линий GPIO) надежен.

Библиотека libgpiod и инструменты управления GPIO


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

Libgpiod(LibraryGeneralPurposeInput/Outputdevice) предоставляет набор API для вызова из своих программ и несколько утилит для управления линиями GPIO из пользовательского режима.

В состав libgpiod входят следующие утилиты:

  • gpiodetect выведет список всех чипов GPIO, их метки и количество линий;
  • gpioinfo выведет информацию о линиях GPIO конкретного контроллера GPIO. В таблице вывода по колонкам будет указано: номер линии, название контакта, направление ввода/вывода, текущее состояние;
  • gpioget считает текущее состояние линии GPIO;
  • gpioset установит значение для линии GPIO;
  • gpiofind выполняет поиск контроллера GPIO и линии по имени;
  • gpiomon осуществляет мониторинг состояния линии GPIO и выводит значение при изменение состояния.

Например, следующая программа написанная на C использует libgpiod для чтения строки GPIO:

void main() {struct gpiod_chip *chip;struct gpiod_line *line;int req, value;chip = gpiod_chip_open("/dev/gpiochip0");if (!chip)return -1;line = gpiod_chip_get_line(chip, 3);if (!line) {gpiod_chip_close(chip);return -1;}req = gpiod_line_request_input(line, "gpio_state");if (req) {gpiod_chip_close(chip);return -1;}value = gpiod_line_get_value(line);printf("GPIO value is: %d\n", value);gpiod_chip_close(chip);}

Библиотеку можно вызывать так же и из кода на C++, Python, C#, и т.д.

Для управления линиями GPIO из терминала необходимо использовать инструменты командной строки, предоставляемые libgpiod. Библиотеку libgpiod и инструменты управления GPIO можно установить скомпилировать из исходного текста и установить.

Установка библиотеки libgpiod и инструментов управления GPIO


Репозитарий библиотеки libgpiod доступ по адресу libgpiod/libgpiod.git. В разделе Download опубликованы релизы библиотеки. На 28.04.2021 последний релиз: v1.6.3.

Библиотеку libgpiod можно установить из репозитария дистрибутива, но скорее всего будет доступна старая версия. Установка libgpiod:

$ sudo apt-get update$ sudo apt-get install -y libgpiod-dev gpiod

Для установки последней актуальной версии необходимо выполнить скрипт установки, который возьмет последнюю версию библиотеки из исходного репозитария. В строке вызова скрипта установки setup-libgpiod-arm64.sh, в качестве первого параметра указать номер версии библиотеки (например: 1.6.3), второй параметр (необязательный) папка установки скрипта. По умолчанию библиотека установится по пути: /usr/share/libgpiod.

Скрипт установки из исходного текста библиотеки libgpiod и утилит для ARM32/ARM64:

$ cd ~/$ sudo apt-get update$ sudo apt-get install -y curl $ curl -SL --output setup-libgpiod-armv7-and-arm64.sh https://raw.githubusercontent.com/devdotnetorg/dotnet-libgpiod-linux/master/setup-libgpiod-armv7-and-arm64.sh$ chmod +x setup-libgpiod-armv7-and-arm64.sh$ sudo ./setup-libgpiod-armv7-and-arm64.sh 1.6.3

Для удаления библиотеки выполнить скрипт: remove-libgpiod-armv7-and-arm64.sh

Если по итогу выполнения скрипта появится надпись Successfully, то значит библиотека и утилиты успешно установлены. Дополнительно для проверки, можно вызвать команду с выводом номера версии библиотеки:

root@bananapim64:~# gpiodetect -vgpiodetect (libgpiod) v1.6.3Copyright (C) 2017-2018 Bartosz GolaszewskiLicense: LGPLv2.1This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.

Инструменты библиотеки libgpiod


Команда gpiodetect выведет список всех чипов GPIO, их метки и количество линий. Результат выполнения команды:

root@bananapim64:~# gpiodetectgpiochip0 [1f02c00.pinctrl] (32 lines)gpiochip1 [1c20800.pinctrl] (256 lines)gpiochip2 [axp20x-gpio] (2 lines)

gpiochip0 и gpiochip1, это чипы входящие в состав SoC Allwinner A64. gpiochip1 имеет выход на 40-контактный разъем совместимый с Raspberry Pi. Чип gpiochip2 отдельная микросхема управления электропитанием axp209 подключенная по интерфейсу I2C.

Для вывод справки к вызываемой команде необходимо добавлять параметр "--help". Вызов справки для команды gpiodetect. Результат выполнения команды:

root@bananapim64:~# gpiodetect --helpUsage: gpiodetect [OPTIONS]List all GPIO chips, print their labels and number of GPIO lines.Options:  -h, --help:           display this message and exit  -v, --version:        display the version and exit

Команда gpioinfo выведет информацию о линиях GPIO конкретного контроллера GPIO (или всех контроллеров GPIO, если они не указаны).Результат выполнения команды:

root@bananapim64:~# gpioinfo 1gpiochip1 - 256 lines:        line   0:      unnamed       unused   input  active-high...        line  64:      unnamed         "dc"  output  active-high [used]...        line  68:      unnamed "backlightlcdtft" output active-high [used]...        line  96:      unnamed   "spi0 CS0"  output   active-low [used]        line  97:      unnamed       unused   input  active-high        line  98:      unnamed       unused   input  active-high        line  99:      unnamed       unused   input  active-high        line 100:      unnamed      "reset"  output   active-low [used]...        line 120:      unnamed "bananapi-m64:red:pwr" output active-high [used]...        line 254:      unnamed       unused   input  active-high        line 255:      unnamed       unused   input  active-high

В таблице по колонкам указано: номер линии, название контакта, направление ввода/вывода, текущее состояние. Сейчас к Banana Pi BPI-M64 подключен LCD экран ILI9341 на SPI интерфейсе, для подключения используется вариант с управляемой подсветкой, файл DTS sun50i-a64-spi-ili9341-backlight-on-off.dts. В DTS файле контакт PC4 GPIO68 обозначен для управления подсветкой, название backlightlcdtft. Соответственно в выводе команды, указан номер линии 68, название backlightlcdtft, направление вывод, текущее состояние active-high (включено).

Команда gpioset установит значение для линии GPIO. Например, следующая команда попытается выключить подсветку на LCD ILI9341. Команда: gpioset 1 68=0, где 1 gpiochip1, 68 номер линии(контакта), 0 логическое значение, может быть 0 или 1. Результат выполнения команды:

root@bananapim64:~# gpioset 1 68=0gpioset: error setting the GPIO line values: Device or resource busyroot@bananapim64:~#

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

Попробуем включить светодиод на линии 36, название PB4, номер контакта на 40-контактном разъеме (совместимый с Raspberry Pi) 33. Результат выполнения команды:

root@bananapim64:~# gpioset 1 36=1

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

Команда gpioget считывает текущее состояние линии GPIO. Результат выполнения команды:

root@bananapim64:~# gpioget 1 361

Получили значение 1, т.к. до этого включили светодиод командой gpioset.

Команда gpiomon будет осуществлять мониторинг состояния линии GPIO и выводить значение при изменение состояния. Будем мониторить состояние кнопки, которая подключена на линию 38, название PB4, номер контакта на 40-контактном разъеме (совместимый с Raspberry Pi) 35. Команда: gpiomon 1 38, где 1 gpiochip1, 38 номер линии (контакта). Результат выполнения команды:

root@bananapim64:~# gpiomon 1 38event:  RISING EDGE offset: 38 timestamp: [     122.943878429]event: FALLING EDGE offset: 38 timestamp: [     132.286218099]event:  RISING EDGE offset: 38 timestamp: [     137.639045559]event: FALLING EDGE offset: 38 timestamp: [     138.917400584]

Кнопка несколько раз нажималась. RISING повышение, изменение напряжения с 0V до 3.3V, кнопка нажата и удерживается состояние. FALLING понижение, изменение напряжения с 3.3V до 0V, происходит отпускание кнопки, и кнопка переходит в состояние не нажата.

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

Установка .NET 5.0 для ARM


Одно из лучших нововведений в .NET 5.0 стало увеличение производительности для архитектуры ARM64. Поэтому переход на новую версию не только увеличит производительность решения на базе ARM64, но и увеличит время автономной работы в случае использования аккумуляторной батареи.

Определение архитектуры ARM32 и ARM64 для SoC


.NET 5 устанавливается на одноплатный компьютер в соответствие с архитектурой SoC:

  • ARM32, ARMv7, aarch32, armhf 32-разрядная архитектура ARM. Первые процессоры ARM для встраиваемых систем разрабатывались именно на этой архитектуре. По заявлению компании ARM Holding, в 2022 поддержка 32-битных платформ прекратится, и будет поддерживаться только 64-битная архитектура. Это означает, что компания не будет поддерживать разработку ПО для 32-битных систем. Если конечный производитель устройства пожелает установить 32-битную ОС, то ему придется самостоятельно заняться портированием драйверов с 64-битной архитектуры на 32-битную.
  • ARM64, ARMv8, aarch64 64-разрядная архитектура ARM. Ядра Cortex-A53 и Cortex-A57, поддерживающие ARMv8, были представлены компанией ARM Holding 30 октября 2012 года.

Плата Banana Pi BPI-M64 построена на основе процессора Allwinner A64, содержит в себе 64-битные ядра Cortex-A53, поэтому поддерживает 64-разрядные приложения. Для платы Banana Pi BPI-M64 используется 64-разрядный образ ОС Armbian, поэтому на плату будем устанавливать .NET для 64-разрядных систем ARM.

Плата Cubietruck построена на основе процессора Allwinner A20 содержит в себе 32-битные ядра Cortex-A7, поэтому поддерживает только 32-разрядные приложения. Соответственно на плату устанавливается .NET для 32-разрядных систем.

Если вы не знаете какую версию .NET установить на одноплатный компьютер, то необходимо выполнить команду для получения информации об архитектуре системы: uname -m.

Выполним команду на Banana Pi BPI-M64:

root@bananapim64:~# uname -maarch64

Строка aarch64 говорит о 64-разрядной архитектуре ARM64, ARMv8, aarch64, поэтому установка .NET для 64-х разрядных ARM систем.

Выполним команду на Cubietruck:

root@cubietruck:~# uname -marmv7l

Строка armv7l говорит о 32-разрядной архитектуре ARM32, ARMv7, aarch32, armhf, поэтому установка .NET для 32-разрядных ARM систем.

Редакции .NET 5.0 на ARM


.NET 5.0 можно устанавливать в трех редакциях:

  • .NET Runtime содержит только компоненты, необходимые для запуска консольного приложения.
  • ASP.NET Core Runtime предназначен для запуска ASP.NET Core приложений, так же включает в себя .NET Runtime для запуска консольных приложений.
  • SDK включает в себя .NET Runtime, ASP.NET Core Runtime и .NET Desktop Runtime. Позволяет кроме запуска приложений, компилировать исходный код на языках C# 9.0, F# 5.0, Visual Basic 15.9.

Для запуска .NET программ достаточно установки редакции .NET Runtime, т.к. компиляция проекта будет на компьютере x86.

Загрузить .NET с сайта Microsoft можно по ссылке Download .NET 5.0.

Установка .NET Runtime


На странице Download .NET 5.0. можно узнать текущую актуальную версию .NET. В первой колонке Release information будет указана версия: v5.0.5 Released 2021-04-06. Версия номер: 5.0.5. В случае выхода более новый версии .NET, ниже в скрипте в строке export DOTNET_VERSION=5.0.5, нужно будет заменить номер версии на последний. Выполним скрипт установки, в зависимости от разрядности системы ARM32 (Cubietruck) или ARM64(Banana Pi BPI-M64):

ARM64

$ cd ~/$ apt-get update && apt-get install -y curl$ export DOTNET_VERSION=5.0.5$ curl -SL --output dotnet.tar.gz https://dotnetcli.azureedge.net/dotnet/Runtime/$DOTNET_VERSION/dotnet-runtime-$DOTNET_VERSION-linux-arm64.tar.gz \&& mkdir -p /usr/share/dotnet \&& tar -ozxf dotnet.tar.gz -C /usr/share/dotnet \&& rm dotnet.tar.gz$ ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

ARM32
$ cd ~/$ apt-get update && apt-get install -y curl$ export DOTNET_VERSION=5.0.5$ curl -SL --output dotnet.tar.gz https://dotnetcli.azureedge.net/dotnet/Runtime/$DOTNET_VERSION/dotnet-runtime-$DOTNET_VERSION-linux-arm.tar.gz \&& mkdir -p /usr/share/dotnet \&& tar -ozxf dotnet.tar.gz -C /usr/share/dotnet \&& rm dotnet.tar.gz$ ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet


Проверим запуск .NET, командой (результат одинаков для Banana Pi BPI-M64 и Cubietruck): dotnet --info

root@bananapim64:~# dotnet --infoHost (useful for support):  Version: 5.0.5  Commit:  2f740adc14.NET SDKs installed:  No SDKs were found..NET runtimes installed:  Microsoft.NETCore.App 5.0.5 [/usr/share/dotnet/shared/Microsoft.NETCore.App]To install additional .NET runtimes or SDKs:  https://aka.ms/dotnet-download

.NET установлен в системе, для запуска приложений в Linux необходимо воспользоваться командой: dotnet ConsoleApp1.dll

Обновление .NET 5.0


При выходе новых версий .NET необходимо сделать следующее:

  1. Удалить папку /usr/share/dotnet/
  2. Выполнить скрипт установки, указав новую версию .NET в строке export: DOTNET_VERSION=5.0.5. Номер последней версии .NET можно посмотреть на странице Download .NET 5.0. Строку скрипта создания символической ссылки выполнять не надо: ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet


Удаленная отладка приложения на .NET 5.0 в Visual Studio Code для ARM


Удаленная отладка в Visual Studio Code позволяет в интерактивном режиме видеть ошибки и просматривать состояние переменных, без необходимости постоянного ручного переноса приложения на одноплатный компьютер, что существенно облегчает разработку. Бинарные файлы копируются в автоматическом режиме с помощью утилиты Rsync. Для работы с GPIO, настройка удаленной отладки не является обязательной задачей. Более подробно можно почитать в публикации Удаленная отладка приложения на .NET 5.0 в Visual Studio Code для ARM на примере Banana Pi BPI-M64 и Cubietruck (Armbian, Linux).

Создание первого приложения для управления (вкл/выкл светодиода) GPIO на C#, аналог утилиты gpioset


Поздравляю тебя %habrauser%! Мы уже подходим к финалу, осталось буквально чуть-чуть. Разрабатывать и компилировать приложение будем на x86 компьютере в в Visual Studio Code. Находясь в этой точке, подразумевается, что на одноплатном компьютере уже установлена платформа .NET 5 и библиотека Libgpiod, а на компьютере x86 .NET 5 и Visual Studio Code. Итак приступаем:

Шаг 1 Создание приложения dotnet-gpioset


Действия выполняются на x86 компьютере. В командной строке создаем проект с названием dotnet-gpioset: dotnet new console -o dotnet-gpioset, где dotnet-gpioset название нового проекта. Результат выполнения команды:

D:\Anton\Projects>dotnet new console -o dotnet-gpiosetGetting ready...The template "Console Application" was created successfully.Processing post-creation actions...Running 'dotnet restore' on dotnet-gpioset\dotnet-gpioset.csproj...  Определение проектов для восстановления...  Восстановлен D:\Anton\Projects\dotnet-gpioset\dotnet-gpioset.csproj (за 68 ms).Restore succeeded.

После выполнения команды будет создана папка \Projects\dotnet-gpioset\, в этой папке будет расположен наш проект: папка obj, файл программы Program.cs и файл проекта dotnet-gpioset.csproj.

Шаг 2 Установка расширения C# for Visual Studio Code (powered by OmniSharp) для Visual Studio Code


Запустим Visual Studio Code и установим расширение C# for Visual Studio Code (powered by OmniSharp), для возможности работы с кодом на C#. Для этого нажмем на закладке: 1. Extensions, затем 2. в поле ввода напишем название расширения C# for Visual Studio Code, выберем пункт 3. C# for Visual Studio Code (powered by OmniSharp). 4. Перейдем на страницу описание расширения и нажмем на кнопку Install.

.NET Visual Studio Code ARM
C# for Visual Studio Code (powered by OmniSharp)

После установки можно выполнить настройку расширения.

.NET Visual Studio Code ARM
Настройка расширения C# for Visual Studio Code

После установки расширения, перезапустим Visual Studio Code.

Шаг 3 Открытие проекта в Visual Studio Code и добавление NuGet пакетов


Откроем проект в Visual Studio Code. Меню: File =>Open Folder, и выберем папку с проектом \Projects\dotnet-gpioset\

dotnet libgpiod
Проект в Visual Studio Code

Откроем файл dotnet-gpioset.csproj, убедимся что версия .NET выставлена верно, должно быть следующее содержание:

dotnet libgpiod
Содержание файла dotnet-gpioset.csproj

NuGet пакеты можно добавить через командную строку или расширение NuGet Package Manager. Установим данное расширение, и добавим пакеты: Iot.Device.Bindings и System.Device.Gpio. Для этого нажмем комбинацию Ctrl+Shift+P, затем в поле введем: Nuget, выберем Nuget Packet Managet: Add Package.

dotnet libgpiod
Запуск расширения NuGet Package Manager

В поле ввода укажем название пакета Iot.Device.Bindings, нажмем Enter, затем выберем версию 1.4.0 и нажмем Enter. Так же сделать и для пакета System.Device.Gpio. В результате добавление пакетов, содержимое файла dotnet-gpioset.csproj должно быть следующим:

dotnet libgpiod
Содержание файла dotnet-gpioset.csproj

Шаг 4 Добавление обработки аргументов в код


Утилита dotnet-gpioset как и оригинальная gpioset будет принимать на вход точно такие же аргументы. Вызов: dotnet-gpioset 1 36=1, включит светодиод на gpiochipX 1, номер линии 36, значение 1. В режиме отладки будут заданы значения по умолчанию int_gpiochip=1, int_pin=36, pin_value = PinValue.High. Подключим пространство имен System.Device.Gpio для использование структуры PinValue.

Обработка входящих аргументов:

static void Main(string[] args){  //run: dotnet-gpioset 1 36=1  //-----------------------------------------------                          int? int_gpiochip=null,int_pin=null;  PinValue? pin_value=null;    #if DEBUG    Console.WriteLine("Debug version");    int_gpiochip=1;    int_pin=36;    pin_value = PinValue.High;  #endif  if (args.Length==2)    {      //Read args      if (int.TryParse(args[0], out int output)) int_gpiochip = output;      Regex r = new Regex(@"\d+=\d+");//36=1      if (r.IsMatch(args[1])) //check: 36=1        {          var i = args[1].Split("=");          if (int.TryParse(i[0], out output)) int_pin = output;          if (int.TryParse(i[1], out output))            {              pin_value=(output != 0) ? PinValue.High : PinValue.Low;                                         }        }      }  Console.WriteLine($"Args gpiochip={int_gpiochip}, pin={int_pin}, value={pin_value}");  //next code  Console.WriteLine("Hello World!");}

Запускаем выполнение кода для проверки, меню Run => Start Debugging, все работает отлично!

Загружено "C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.5\System.Text.Encoding.Extensions.dll". Загрузка символов пропущена. Модуль оптимизирован, включен параметр отладчика "Только мой код".Debug versionArgs gpiochip=1, pin=36, value=HighHello World!Программа "[8528] dotnet-gpioset.dll" завершилась с кодом 0 (0x0).

Шаг 5 Добавление контроллера управления GPIO c драйвером LibGpiodDriver


Для управления GPIO необходимо создать объект GpioController и указать драйвер LibGpiodDriver, для этого добавим пространство имен System.Device.Gpio.Drivers.

Добавление контроллера:

//next codeGpioController controller;var drvGpio = new LibGpiodDriver(int_gpiochip.Value);            controller = new GpioController(PinNumberingScheme.Logical, drvGpio);

Описание кода:

  • GpioController класс контроллера для управления контактами GPIO;
  • LibGpiodDriver(int_gpiochip.Value) драйвер обертки библиотеки Libgpiod, в качестве аргумента указываем номер gpiochip;
  • GpioController(PinNumberingScheme.Logical, drvGpio) инициализация контроллера, PinNumberingScheme.Logical формат указания контактов. Есть два варианта, по названию контакта или по его номеру. Но т.к. названия контактов не заданы, то обращение будет только по номеру.

Шаг 6 Управление контактом GPIO


Добавление кода для задания значения контакту:

//set value            if(!controller.IsPinOpen(int_pin.Value))  {    controller.OpenPin(int_pin.Value,PinMode.Output);    controller.Write(int_pin.Value,pin_value.Value);                      } 

Описание кода:

  • controller.IsPinOpen проверка открытия контакта, может быть занят или недоступен;
  • controller.OpenPin открытие контакта и задание ему режима работы, PinMode.Output на вывод;
  • controller.Write(int_pin.Value,pin_value.Value) выставление контакту int_pin значение pin_value.

Шаг 7 Публикация для архитектуры ARM


Открыть командную строку, и перейти в папку \Projects\dotnet-gpioset\.

Для ARM32 выполнить команду:

  • параметр --runtime задает архитектуру выполнения программы (берется из списка Runtime Identifiers (RIDs));
  • параметр --self-contained указывает на необходимость добавление в каталог всех зависимых сборок .NET, при выставление значение в False, копируются только дополнительные сборки не входящие в .NET Runtime (в данном случае будут скопированы сборки из дополнительных NuGet пакетов).

dotnet publish dotnet-gpioset.csproj --configuration Release --runtime linux-arm --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-gpioset\bin\Release\net5.0\linux-arm\publish\.

Для ARM64 выполнить команду:

dotnet publish dotnet-gpioset.csproj --configuration Release --runtime linux-arm64 --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-gpioset\bin\Release\net5.0\linux-arm64\publish\.

Шаг 8 Перенос папки \publish\


Содержимое папки \publish\ необходимо перенести в домашний каталог Linux пользователя на одноплатном компьютере. Это можно сделать используя терминал MobaXterm.

Шаг 9 Запуск dotnet-gpioset на одноплатном компьютере


Содержимое папки \publish\ было скопировано в папку /root/publish-dotnet-gpioset. Исполняемым файлом будет файл с расширением *.dll. В самом начале, светодиод был подключен на контакт 33, 40-контактного разъема совместимого с Raspberry P, название контакта PB4, номер линии 36. Поэтому в качестве аргумента номера контакта указываем 36. Для запуска программы необходимо выполнить команду:

dotnet dotnet-gpioset.dll 1 36=1

Результат выполнения команды:

root@bananapim64:~# cd /root/publish-dotnet-gpiosetroot@bananapim64:~/publish-dotnet-gpioset# dotnet dotnet-gpioset.dll 1 36=1Args gpiochip=1, pin=36, value=HighOK

Светодиод включился!



Проект доступен на GitHub dotnet-gpioset.

Создание приложения обработки прерывания от кнопки


Теперь реализуем программу обработки прерываний от GPIO. Задача будет заключаться в переключение светодиода по нажатию кнопки. Первое нажатие кнопки включит светодиод, последующее, выключит светодиод, и так до бесконечности. Программа основана на примере Push button.

Светодиод подключен контакту с номером 36. Кнопка подключена на контакт с номером 38. Итак приступаем:

Шаг 1 Создание приложения dotnet-led-button


Действия выполняются на x86 компьютере. В командной строке создаем проект с названием dotnet-led-button: dotnet new console -o dotnet-led-button, где dotnet-led-button название нового проекта.

D:\Anton\Projects>dotnet new console -o dotnet-led-buttonGetting ready...The template "Console Application" was created successfully.Processing post-creation actions...Running 'dotnet restore' on dotnet-led-button\dotnet-led-button.csproj...  Определение проектов для восстановления...  Восстановлен D:\Anton\Projects\dotnet-led-button\dotnet-led-button.csproj (за76 ms).Restore succeeded.

После выполнения команды будет создана папка с файлами проекта \Projects\dotnet-led-button\.

Шаг 2 Открытие проекта в Visual Studio Code и добавление NuGet пакетов


Точно так же, как и в предыдущем проекте добавим Nuget пакеты: Iot.Device.Bindings и System.Device.Gpio.

Шаг 3 Добавление контроллера управления GPIO c драйвером LibGpiodDriver


Добавим контроллер для управления GPIO, и выставим режим работы контактов:

private const int GPIOCHIP = 1;private const int LED_PIN = 36;private const int BUTTON_PIN = 38;       private static PinValue ledPinValue = PinValue.Low;     static void Main(string[] args){                          GpioController controller;  var drvGpio = new LibGpiodDriver(GPIOCHIP);  controller = new GpioController(PinNumberingScheme.Logical, drvGpio);  //set value  if(!controller.IsPinOpen(LED_PIN)&&!controller.IsPinOpen(BUTTON_PIN))    {      controller.OpenPin(LED_PIN,PinMode.Output);      controller.OpenPin(BUTTON_PIN,PinMode.Input);    }  controller.Write(LED_PIN,ledPinValue); //LED OFF

Описание кода:

  • controller.OpenPin(LED_PIN,PinMode.Output) - открывает контакт светодиода, и выставляет режим работы на вывод;
  • controller.OpenPin(BUTTON_PIN,PinMode.Input) - открывает контакт кнопки, и выставляет режим работы на ввод (сигнал поступает от кнопки.

Шаг 4 Добавление обработки прерывания кнопки


Обработка прерывания реализуется путем добавление Callback на изменение состояние контакта. Callback регистрируется в контроллере GPIO:

controller.RegisterCallbackForPinValueChangedEvent(BUTTON_PIN,PinEventTypes.Rising,(o, e) =>  {    ledPinValue=!ledPinValue;    controller.Write(LED_PIN,ledPinValue);    Console.WriteLine($"Press button, LED={ledPinValue}");          });

Описание кода:

  • RegisterCallbackForPinValueChangedEvent регистрация Callback на контакт BUTTON_PIN, будет срабатывать при нажатие на кнопку Rising. Так же доступно срабатывание на событие отпускание кнопки.

Шаг 5 Публикация для архитектуры ARM


Открыть командную строку, и перейти в папку \Projects\dotnet-led-button\.

Для ARM32 выполнить команду:

dotnet publish dotnet-led-button.csproj --configuration Release --runtime linux-arm --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-led-button\bin\Release\net5.0\linux-arm\publish\.

Для ARM64 выполнить команду:

dotnet publish dotnet-led-button.csproj --configuration Release --runtime linux-arm64 --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-led-button\bin\Release\net5.0\linux-arm64\publish\.

Шаг 6 Перенос папки \publish\


Содержимое папки \publish\ необходимо перенести в домашний каталог Linux пользователя на одноплатном компьютере.

Шаг 7 Запуск dotnet-led-button на одноплатном компьютере


Содержимое папки \publish\ было скопировано в папку /root/publish-dotnet-led-button. Для запуска программы необходимо выполнить команду:

dotnet dotnet-led-button.dll

Результат выполнения команды:

root@bananapim64:~/publish-dotnet-led-button# dotnet dotnet-led-button.dllCTRL+C to interrupt the read operation:Press any key, or 'X' to quit, or CTRL+C to interrupt the read operation:Press button, LED=LowPress button, LED=HighPress button, LED=LowPress button, LED=HighPress button, LED=Low

Кнопка работает!

Проект доступен на GitHub dotnet-led-button.

Теперь поговорим о скорости


Замеры скорости управления GPIO на Banana Pi BPI-M64 не проводились из-за отсутствия осциллографа. Но не так давно, пользователь ZhangGaoxing опубликовал результаты замеров скорости на Orange Pi Zero: ОС Armbian buster, ядро Linux 5.10.16, .NET 5.0.3. Тест заключался в быстром переключение контакта GPIO с 0 на 1 и наоборот, по сути осуществлялась генерация сигнала ШИМ (в Arduino аналог SoftPWM). Чем больше частота, тем быстрее переключатся контакт. Для замера был разработан проект SunxiGpioDriver.GpioSpeed. ZhangGaoxing для доступа к контактам разработал драйвер SunxiDriver, который напрямую обращается к регистрам памяти для управления GPIO. Код этого драйвера так же можно адаптировать к любой плате, путем изменения адресов регистров памяти из datasheet к процессору. Минус такого подхода заключается в отсутствие контроля к GPIO со стороны ОС, можно влезть в контакт используемой ОС и вызвать сбой работы.

Таблица замеров:
Драйвер Язык Версия библиотеки Средняя частота
SunxiDriver C# - 185 KHz
SysFsDriver C# System.Device.Gpio 1.3.0 692 Hz
LibGpiodDriver C# System.Device.Gpio 1.3.0
libgpiod 1.2-3
81 KHz
wiringOP C 35de015 1.10 MHz

Результаты подтвердили, что самым медленным интерфейсом является SysFs, и его не стоит использовать для серьезных проектов. wiringOP является С оберткой доступа к GPIO. Непосредственно управление GPIO из C кода существенно быстрее, чем из приложения на .NET, разница скорости в ~13 раз. Это и есть плата за Runtime.

Итог


Управлять контактами GPIO в C# оказалось не сложнее чем на Arduino. В отличие от Arduino в нашем распоряжение Linux с поддержкой полноценной графики, звуком, и большими возможностями подключения различной периферии. В далеком 2014 году с хабровчанином prostosergik был спор о целесообразности использовании Raspberry Pi в качестве школьного звонка. Мною был реализован подобный функционал на C# .NET Micro Framework, отладочная плата FEZ Domino. С того времени многое что изменилось. Сейчас вариант использования для подобных индивидуальных задач, одноплатных компьютеров на Linux более оправдан, чем использование микроконтроллера. Первое существенное изменение это .NET теперь работает на Linux нативно. Второе появились библиотеки которые упрощают и скрывают под капотом все сложную работу. Третье цена, сейчас одноплатный компьютер с 256 Мб ОЗУ, Ethernet и Wi-Fi в известном китайском магазине можно приобрести за 18$. За такие деньги МК, с поддержкой полноценного Web-интерфейса и шифрования сетевого трафика, вряд ли найдешь. Платформа .NET IoT позволяет работать с GPIO на достаточно высоком уровне абстракции, что существенно снижает порог вхождения. В результате любой разработчик .NET платформы, может с легкостью реализовать свое решение для IoT не вдаваясь в детали как это работает внутри. Установка платформы .NET и библиотеки Libgpiod было приведено для понимания, как это работает, но такой подход не является самым удобным. Гораздо удобнее все разворачивать в Docker контейнере, тем более это mainstream для Linux. В продолжении посмотрим как упаковывать приложение на C# вместе с .NET 5 и Libgpiod в один контейнер, для дальнейшей удобной дистрибьюции нашего решения потенциальному клиенту, задействуем LCD для вывода информации из .NET кода.



На правах рекламы


Прямо сейчас вы можете заказать мощные серверы, которые используют новейшие процессоры AMD Epyc. Гибкие тарифы от 1 ядра CPU до безумных 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe.

Подписывайтесь на наш чат в Telegram.

Подробнее..

Превращаем одноплатник Cubietruck в Wi-Fi Hotspot с Captive portal, VPN-шлюзом и Ad block

25.05.2021 10:11:06 | Автор: admin
raspap

Для построения Wi-Fi сети обычно используют готовые маршрутизаторы, функционал которых всегда ограничен прошивкой. А если необходимо добавить блокировщик рекламы, VPN шлюз и красивый Captive portal, покупать новую железку? Стоимость устройства с таким функционалом будет уже весьма высока. Можно взять Linux с Hostapd и сделать точку доступа с Wi-Fi, но в отличие от готовых маршрутизаторов не будет наглядного Web-интерфейса. И для решения этой задачи был создан проект RaspAP, который на базе устройств с ОС Debian создает Wi-Fi Hotspot с Captive portal, VPN-шлюзом, Ad block. Для RaspAP в отличие от OpenWrt не требуется непосредственная поддержка устройства, достаточно поддержки последней версии Debian. RaspAP работает поверх уже установленных ОС: Raspberry Pi OS, Armbian, Debian, Ubuntu. Как сделать Wi-Fi Hotspot на RaspAP прошу под кат.

RaspAP open-source проект создания беспроводного маршрутизатора из многих популярных устройств работающих на ОС Debian, включая Raspberry Pi. Содержит удобный Web-интерфейс для настройки, блокировщик рекламы, осуществляет шлюзование сетевого трафика через OpenVPN или WireGuard.

Используя RaspAP можно быстро развернуть Hotspot с доступом в сеть Интернет, где угодно: в магазине или торговом центре, заправке, кафе и ресторане, библиотеке, больнице, аэропорте и вокзале, а также в совершенно непривычных, уединенных местах, например на вершине горы. Благодаря наличию Captive portal, посетители подключаясь к Сети, обязательно увидят информацию, которую владелец Wi-Fi желает довести до пользователей. Это может быть информация о соглашение использования публичного hotspot, и т.д.

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


Для устройств на ARM-архитектуре заявлена официальная поддержка, устройства на x86 процессорах в настоящее время находится в стадии Beta.

raspap

Поддерживаемые ОС и архитектуры RaspAP

Базовой платформой работы RaspAP является устройство Raspberry Pi. Но благодаря поддержки Armbian на основе Debian, список поддерживаемых устройств становится весьма широким.

Wi-Fi Hotspot на RaspAP будет развернут на одноплатном компьютере Cubietruck, процессор AllWinner A20 (ARM32), с ОС Armbian (на базе Debian). Для задач маршрутизации сетевого трафика для нескольких клиентов процессора AllWinner A20 с двумя ядрами Cortex-A7 будет недостаточно, но вы можете взять гораздо более мощное устройства из каталога поддерживаемых проектом Armbian.

RaspAP


raspap

Под капотом RaspAP использует hostapd, dnsmasq, iptables, веб-интерфейс работает на lighttpd с php-скриптами. С точки зрения использования новых функций применяется политика спонсорства. Если оформить ежемесячное спонсорство, то ваш аккаунт на GitHub будет добавлен в группу Insiders, которые первыми получают возможность протестировать новые функции. Функции доступные на данный момент только спонсорам будут помечены Insiders Edition.

raspap

Веб-интерфейс RaspAP

Возможности RaspAP:

  • Графический интерфейс для настройки и отображения графиков активности клиентских устройств;
  • Поддержка сертификатов SSL;
  • Интеграция с Captive portal;
  • Управление DHCP-сервером;
  • Поддержка адаптеров 802.11ac 5 ГГц;
  • Автоопределение внешних беспроводных адаптеров.

Пройдемся коротко по основным функциям RaspAP.

Точка доступа


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

  • Interface: wlan0
  • SSID: raspi-webgui
  • Wireless Mode: 802.11n 2.4GHz
  • Channel: 1
  • Security Type: WPA2
  • Encryption Type: CCMP
  • Passphrase: ChangeMe

К AP можно подключаться по ключевой паре SSID + пароль или по QR-коду. В случае бездействия клиента, AP может его отключить (требуется поддержка в драйверах). В Insiders Edition доступна возможность изменять мощность в dBm. Для обеспечения гарантированной работы можно задать ограниченное количество подключаемых клиентов.

Для Raspberry Pi Zero W доступен режим виртуализации беспроводного устройства. Единственное на борту Wi-Fi устройство будет работать в режиме клиента и точки доступа. Режим виртуализации сетевых интерфейсов работает и на других адаптерах USB Wi-Fi таких как RTL8188.

Блокировщик рекламы (Ad blocking)


Блокирует ads, трекеры и узлы из черного списка. В качестве источника черного списка выступает проект notracking, список обновляется автоматически. Блокируются следующие типы узлов: tracking, поставщики рекламы, сбор аналитики, фишинговые и мошенические сайты, содержащие вредоносные программы, веб-майнеры.

Captive portal


raspap

Captive portal

Из коробки интегрирован nodogsplash. nodogsplash легкое и простое решения создания кастомизируемых порталов. Поддерживает различные политики работы клиентов.

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


Статистическую работу можно выводить на TFT-экран Adafruit Mini PiTFT контроллер ST7789. Скрипт вывода информации написан на Python, поэтому программный код можно легко адаптировать и для другого дисплея, например для ILI9341.

raspap

Вывод информации о работе AP

Поддержка различных сетевых устройств в качестве WAN-интерфейса (Insiders Edition)


В качестве доступа к сети Интернет, RaspAP поддерживает несколько различных типов сетевых устройств, такие как:

  • Ethernet interface (eth);
  • Wireless adapter (wlan);
  • Mobile data modem (ppp);
  • Mobile data adapter with built-in router;
  • USB connected smartphone (USB tethering);

Это особенно удобно когда вы путешествуете или работает в полевых условиях.

OpenVPN


raspap

Сетевой трафик можно туннелировать используя клиента OpenVPN. В Insiders Edition доступна возможность хранения нескольких профилей OpenVPN с функцией быстрого переключения между ними.

WireGuard (Insiders Edition)


raspap

WireGuard быстрый и современный VPN, в котором используется самая современная криптография. Он более производителен, чем OpenVPN, и обычно считается наиболее безопасным, простым в использовании и самым простым решением VPN для современных дистрибутивов Linux. Благодаря низкому overhead, если устройство работает от батареи, то время работы при использование WireGuard будет больше, чем при использование OpenVPN.

Доступ к Web-интересу настроек через SSL


raspap

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

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


Установка RaspAP будет произведена из публичного репозитория, на Cubietruck установлена последняя версия Armbian (на основе Debian): Armbian 21.02.3 Buster, Linux 5.10.21-sunxi. На борту имеется встроенный адаптер wlan0, будет выступать в качестве клиентского доступа к сети Интернет (WAN-интерфейс). Для Hotspot подключим RTL8188 USB WiFi dongle wlan1.

  • IP конфигурация для wlan0: address 192.168.43.12 netmask 255.255.255.0 gateway 92.168.43.1.
  • IP конфигурация для wlan1: address 10.3.141.1 netmask 255.255.255.0 gateway 10.3.141.1.

Конфигурация DHCP-сервера:

  • Диапазон выдаваемых IP-адресов 10.3.141.50 10.3.141.254;
  • Шлюз/DNS-сервер: 10.3.141.1.

Для шлюзования сетевого трафика через OpenVPN установим на VPS сервер SoftEther VPN Server. SoftEther VPN Server мультипротокольный VPN-сервер, который может поднимать L2TP/IPsec, OpenVPN, MS-SSTP, L2TPv3, EtherIP-серверы, а также имеет свой собственный протокол SSL-VPN, который неотличим от обычного HTTPS-трафика (чего не скажешь про OpenVPN handshake, например), может работать не только через TCP/UDP, но и через ICMP (подобно pingtunnel, hanstunnel) и DNS (подобно iodine), работает быстрее (по заверению разработчиков) текущих имплементаций, строит L2 и L3 туннели, имеет встроенный DHCP-сервер, поддерживает как kernel-mode, так и user-mode NAT, IPv6, шейпинг, QoS, кластеризацию, load balancing и fault tolerance, может быть запущен под Windows, Linux, Mac OS, FreeBSD и Solaris и является Open-Source проектом под GPLv2.

Для VPS сервера выберем тариф на vdsina.ru за 330 р./месяц, в который включена квота на 32 ТБ трафика, чего более чем достаточно. SoftEther VPN Server будет развернут в Docker контейнере, поэтому выбор ОС CentOS/Debian/Ubuntu не принципиально важен.

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

raspap

VPS сервер на vdsina.ru

Сервер был развернут в Московской локации, IP-адрес 94.103.85.152, dns-имя: v636096.hosted-by-vdsina.ru. Подключение к серверу будет по DNS имени.

raspap

Итоговая схема сети

Как будет выглядеть Web-интерфейс RaspAP и подключение к Hotspot


Подключение к AP SSID: raspi-webgui


Подключение к AP raspi-webgui

Конфигурационные файлы RaspAP


Для установки RaspAP есть Quick installer, но он выполняется без задания параметров и wlan0 настроен как Hotspot, что нам не подходит. Поэтому воспользуемся Manual installation, с некоторыми изменениями т.к. руководство содержит некоторые ошибки и сам RaspAP работает с некоторыми некритичными багами, из-за этого пришлось немного больше потратить время на установку. О багах будет в ходе установки.

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

Список конфигурационных файлов (GitHub):

  • hostapd.conf служба hostapd
  • default_hostapd служба hostapd
  • 090_raspap.conf служба dnsmasq.d
  • 090_wlan1.conf служба dnsmasq.d
  • defaults.json служба raspap
  • dhcpcd.conf служба raspap
  • config.php портал конфигурации RaspAP

hostapd.conf служба hostapd

Содержит настройки AP по умолчанию такие как: ssid, channel, password и т.д.

hostapd.conf
driver=nl80211ctrl_interface=/var/run/hostapdctrl_interface_group=0beacon_int=100auth_algs=1wpa_key_mgmt=WPA-PSKssid=raspi-webguichannel=1hw_mode=gwpa_passphrase=ChangeMeinterface=wlan1wpa=2wpa_pairwise=CCMPcountry_code=RU## Rapberry Pi 3 specific to on board WLAN/WiFi#ieee80211n=1 # 802.11n support (Raspberry Pi 3)#wmm_enabled=1 # QoS support (Raspberry Pi 3)#ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40] # (Raspberry Pi 3)## RaspAP wireless client AP mode#interface=uap0## RaspAP bridge AP mode (disabled by default)#bridge=br0



default_hostapd служба hostapd

Настройка службы hostapd, параметр DAEMON_CONF определяет путь к настройкам.

default_hostapd
# Location of hostapd configuration fileDAEMON_CONF="/etc/hostapd/hostapd.conf"



090_raspap.conf служба dnsmasq.d

Настройка службы dnsmasq, параметр conf-dir определяет путь к настройкам.

090_raspap.conf
# RaspAP default configlog-facility=/tmp/dnsmasq.logconf-dir=/etc/dnsmasq.d


090_wlan1.conf служба dnsmasq.d

Настройка dnsmasq для сетевого интерфейса wlan1. Содержит диапазон выдаваемых IP-адресов, и другие сетевые настройки. Необходимо обратить внимание на название файла по маске 090_[ИДЕНТИФИКАТОР_ИНТЕРФЕЙСА_HOTSPOT].conf. Если у вас сетевой интерфейс для hostspot будет назваться например wlan2, то следует задать название файла 090_wlan2.conf.

090_wlan1.conf
# RaspAP wlan0 configuration for wired (ethernet) AP modeinterface=wlan1domain-neededdhcp-range=10.3.141.50,10.3.141.255,255.255.255.0,12hdhcp-option=6,10.3.141.1


defaults.json служба raspap

Настройка DHCP серверов для интерфейсов wlan0 и wlan1.

defaults.json
{  "dhcp": {    "wlan1": {       "static ip_address": [ "10.3.141.1/24" ],      "static routers": [ "10.3.141.1" ],      "static domain_name_server": [ "10.3.141.1" ],      "subnetmask": [ "255.255.255.0" ]    },    "wlan0": {      "static ip_address": [ "192.168.43.12/24" ],      "static routers": [ "192.168.43.1" ],      "static domain_name_server": [ "1.1.1.1 8.8.8.8" ],      "subnetmask": [ "255.255.255.0" ]    },    "options": {      "# RaspAP default configuration": null,      "hostname": null,      "clientid": null,      "persistent": null,      "option rapid_commit": null,      "option domain_name_servers, domain_name, domain_search, host_name": null,      "option classless_static_routes": null,      "option ntp_servers": null,      "require dhcp_server_identifier": null,      "slaac private": null,      "nohook lookup-hostname": null    }  },  "dnsmasq": {    "wlan1": {      "dhcp-range": [ "10.3.141.50,10.3.141.255,255.255.255.0,12h" ]    },    "wlan0": {      "dhcp-range": [ "192.168.43.50,192.168.50.150,12h" ]    }  }}


dhcpcd.conf служба raspap

Настройка для сетевого интерфейса wlan0, который выходит в сеть Интернет.

dhcpcd.conf
# RaspAP default configurationhostnameclientidpersistentoption rapid_commitoption domain_name_servers, domain_name, domain_search, host_nameoption classless_static_routesoption ntp_serversrequire dhcp_server_identifierslaac privatenohook lookup-hostname# RaspAP wlan0 configurationinterface wlan0static ip_address=192.168.43.12/24static routers=192.168.43.1static domain_name_server=1.1.1.1 8.8.8.8


config.php портал конфигурации RaspAP

Файл настроек графического Web-интерфейса. Содержит переменные влияющие на отображение настроек. Самый главный параметр define('RASPI_WIFI_AP_INTERFACE', 'wlan1');. В качестве значения указать сетевой интерфейс hotspot wlan1.

config.php
...define('RASPI_WIFI_AP_INTERFACE', 'wlan1');...define('RASPI_ADBLOCK_ENABLED', true);define('RASPI_OPENVPN_ENABLED', false);...


Пошаговая установкаRaspAP


Руководство установки доступно в разделе Manual installation.

Шаг 1 Подключение адаптера USB WiFi RTL8188


Подключаем адаптер в любой доступный USB порт. В Armbian драйвера уже есть, поэтому проверим подключение командой lsusb:

root@bananapim64:~# lsusbBus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hubBus 003 Device 004: ID 0bda:c811 Realtek Semiconductor Corp.Bus 003 Device 002: ID 1a40:0101 Terminus Technology Inc. HubBus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hubBus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hubBus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hubBus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hubroot@bananapim64:~#

В списке присутствует Realtek Semiconductor Corp., значит адаптер успешно распознался. Если вывести название интерфейса для подключенного адаптера, то его имя будет wlxe81e0584796d, что несколько далеко от привычного именования вида wlanX. Для задания названия для адаптера wlan1, необходимо выполнить следующие действия (более подробнее почитать про именование сетевых интерфейсов по ссылке1,ссылке2):

$ sudo ln -s /dev/null /etc/udev/rules.d/80-net-setup-link.rules$ sudo reboot

После перезагрузки в системе будет два беспроводных адаптера: wlan0 и wlan1.

Шаг 2 Настройка сетевых интерфейсов


Настроим сетевые интерфейсы в конфигурационном файле: /etc/network/interfaces.

# Network is managed by Network managerauto loiface lo inet loopback# WANauto wlan0allow-hotplug wlan0iface wlan0 inet dhcp# Wi-Fi APauto wlan1iface wlan1 inet static    address 10.3.141.1    netmask 255.255.255.0    gateway 10.3.141.1

Шаг 3 Установка RaspAP


Теперь приступаем к установке RaspAP.
Обновление системы:

sudo apt-get updatesudo apt-get full-upgrade

Установка зависимостей для не RPi OS:

sudo apt-get install software-properties-common sudo add-apt-repository ppa:ondrej/phpsudo apt-get install dhcpcd5

Установка пакетов:

sudo apt-get install -y lighttpd git hostapd dnsmasq iptables-persistent vnstat qrencode php7.3-cgi

PHP:

sudo lighttpd-enable-mod fastcgi-php    sudo service lighttpd force-reloadsudo systemctl restart lighttpd.service

Создание Web-портала:

sudo rm -rf /var/www/htmlsudo git clone https://github.com/RaspAP/raspap-webgui /var/www/htmlWEBROOT="/var/www/html"CONFSRC="$WEBROOT/config/50-raspap-router.conf"LTROOT=$(grep "server.document-root" /etc/lighttpd/lighttpd.conf | awk -F '=' '{print $2}' | tr -d " \"")HTROOT=${WEBROOT/$LTROOT}HTROOT=$(echo "$HTROOT" | sed -e 's/\/$//')awk "{gsub(\"/REPLACE_ME\",\"$HTROOT\")}1" $CONFSRC > /tmp/50-raspap-router.confsudo cp /tmp/50-raspap-router.conf /etc/lighttpd/conf-available/sudo ln -s /etc/lighttpd/conf-available/50-raspap-router.conf /etc/lighttpd/conf-enabled/50-raspap-router.confsudo systemctl restart lighttpd.servicecd /var/www/htmlsudo cp installers/raspap.sudoers /etc/sudoers.d/090_raspap

Создание конфигурации:

sudo mkdir /etc/raspap/sudo mkdir /etc/raspap/backupssudo mkdir /etc/raspap/networkingsudo mkdir /etc/raspap/hostapdsudo mkdir /etc/raspap/lighttpdsudo cp raspap.php /etc/raspap 

Установка разрешения:

sudo chown -R www-data:www-data /var/www/htmlsudo chown -R www-data:www-data /etc/raspap

Настройка контролирующих скриптов:

sudo mv installers/*log.sh /etc/raspap/hostapd sudo mv installers/service*.sh /etc/raspap/hostapdsudo chown -c root:www-data /etc/raspap/hostapd/*.sh sudo chmod 750 /etc/raspap/hostapd/*.sh sudo cp installers/configport.sh /etc/raspap/lighttpdsudo chown -c root:www-data /etc/raspap/lighttpd/*.shsudo mv installers/raspapd.service /lib/systemd/systemsudo systemctl daemon-reloadsudo systemctl enable raspapd.service

Установка стартовых настроек, настройки в каталоге ~/temp, при необходимости заменить на свои:

sudo apt-get install -y curl unzipmkdir -p ~/tempcurl -SL --output ~/temp/config_ct.zip https://github.com/devdotnetorg/Site/raw/master/Uploads/files/config_ct.zipunzip ~/temp/config_ct.zip -d ~/temprm ~/temp/config_ct.zipесли есть: sudo mv /etc/default/hostapd ~/default_hostapd.oldесли есть: sudo cp /etc/hostapd/hostapd.conf ~/hostapd.conf.oldsudo cp ~/temp/default_hostapd /etc/default/hostapdsudo cp ~/temp/hostapd.conf /etc/hostapd/hostapd.confsudo cp config/090_raspap.conf /etc/dnsmasq.d/090_raspap.confsudo cp ~/temp/090_wlan1.conf /etc/dnsmasq.d/090_wlan1.confsudo cp ~/temp/dhcpcd.conf /etc/dhcpcd.confsudo cp ~/temp/config.php /var/www/html/includes/sudo cp ~/temp/defaults.json /etc/raspap/networking/sudo systemctl stop systemd-networkdsudo systemctl disable systemd-networkdsudo cp config/raspap-bridge-br0.netdev /etc/systemd/network/raspap-bridge-br0.netdevsudo cp config/raspap-br0-member-eth0.network /etc/systemd/network/raspap-br0-member-eth0.network 

Оптимизация PHP:

sudo sed -i -E 's/^session\.cookie_httponly\s*=\s*(0|([O|o]ff)|([F|f]alse)|([N|n]o))\s*$/session.cookie_httponly = 1/' /etc/php/7.3/cgi/php.inisudo sed -i -E 's/^;?opcache\.enable\s*=\s*(0|([O|o]ff)|([F|f]alse)|([N|n]o))\s*$/opcache.enable = 1/' /etc/php/7.3/cgi/php.inisudo phpenmod opcache

Настройка маршрутизации:

echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/90_raspap.conf > /dev/nullsudo sysctl -p /etc/sysctl.d/90_raspap.confsudo /etc/init.d/procps restartsudo iptables -t nat -A POSTROUTING -j MASQUERADEsudo iptables -t nat -A POSTROUTING -s 192.168.43.0/24 ! -d 192.168.43.0/24 -j MASQUERADEsudo iptables-save | sudo tee /etc/iptables/rules.v4

Включение hostapd:

sudo systemctl unmask hostapd.servicesudo systemctl enable hostapd.service

OpenVPN:

sudo apt-get install openvpnsudo sed -i "s/\('RASPI_OPENVPN_ENABLED', \)false/\1true/g" /var/www/html/includes/config.phpsudo systemctl enable openvpn-client@clientsudo mkdir /etc/raspap/openvpn/sudo cp installers/configauth.sh /etc/raspap/openvpn/sudo chown -c root:www-data /etc/raspap/openvpn/*.sh sudo chmod 750 /etc/raspap/openvpn/*.sh

Ad blocking:

sudo mkdir /etc/raspap/adblockwget https://raw.githubusercontent.com/notracking/hosts-blocklists/master/hostnames.txt -O /tmp/hostnames.txtwget https://raw.githubusercontent.com/notracking/hosts-blocklists/master/domains.txt -O /tmp/domains.txtsudo cp /tmp/hostnames.txt /etc/raspap/adblocksudo cp /tmp/domains.txt /etc/raspap/adblock sudo cp installers/update_blocklist.sh /etc/raspap/adblock/sudo chown -c root:www-data /etc/raspap/adblock/*.*sudo chmod 750 /etc/raspap/adblock/*.shsudo touch /etc/dnsmasq.d/090_adblock.confecho "conf-file=/etc/raspap/adblock/domains.txt" | sudo tee -a /etc/dnsmasq.d/090_adblock.conf > /dev/null echo "addn-hosts=/etc/raspap/adblock/hostnames.txt" | sudo tee -a /etc/dnsmasq.d/090_adblock.conf > /dev/nullsudo sed -i '/dhcp-option=6/d' /etc/dnsmasq.d/090_raspap.confsudo sed -i "s/\('RASPI_ADBLOCK_ENABLED', \)false/\1true/g" includes/config.php

При конфигурирование через Web-интерфейс столкнулся с багом, который при изменение настроек DHCP сервера на интерфейсе wlan1 удаляет файл конфигурации 090_wlan1.conf и не создает его заново. В результате DHCP сервер не выдает IP-конфигурацию новым клиентам. Временное решение этой проблемы заключается в блокировке файла на удаление, необходимо выполнить следующую команду (по блокировке файлов почитать по ссылке):

sudo chattr +i /etc/dnsmasq.d/090_wlan1.conf

После установки необходимо перезагрузить систему:

sudo reboot now

После перезагрузке появится Wi-Fi точка доступа с SSID raspi-webgui и паролем ChangeMe. Портал будет доступен по адресу: http://10.3.141.1.

Установка SoftEther VPN Server на VPS сервер


На сервер v636096.hosted-by-vdsina.ru установим Docker по официальному руководству Install Docker Engine on Ubuntu.

Создание сети для Docker контейнеров


Для подсети в которой будет контейнер с SoftEther VPN Server определим следующие параметры:

  • Название сети: vpnnetwork;
  • Subnet: 172.22.0.0/24;
  • Driver: bridge;
  • Range: 172.22.0.0/25;
  • gateway: 172.22.0.127;
  • HostMin: 172.22.0.1;
  • HostMax: 172.22.0.126;
  • Hosts/Net: 126.

Для создание внутренней сети Docker выполним команду:

$ docker network create --driver bridge --subnet 172.22.0.0/24 --ip-range=172.22.0.0/25 --gateway 172.22.0.127 vpnnetwork

Для проверки доступности сети выполнить команду: ping 172.22.0.127.

Создание контейнера с SoftEther VPN Server


Для создание контейнера будем использовать образ siomiz/softethervpn. До запуска основного контейнера необходимо создать конфигурацию, в которой указать пароль для управления сервером параметр SPW и пароль для управления хабом параметр HPW. Файл конфигурации будет располагаться по пути /usr/vpnserver/vpn_server.config. Выполнить следующие команды:

$ mkdir -p /usr/vpnserver$ docker run --name vpnconf -e "SPW={PASSWORD}" -e "HPW={PASSWORD}" siomiz/softethervpn echo$ docker cp vpnconf:/usr/vpnserver/vpn_server.config /usr/vpnserver/vpn_server.config$ docker rm vpnconf

Для уменьшения размера контейнера возьмем образ на основе Alpine, все журналы log в null. Выполнить следующие команды для создание контейнера:

$ docker run --name vps-server-softethervpn -d --cap-add NET_ADMIN --restart always --net vpnnetwork --ip 172.22.0.2 -p 443:443/tcp -p 992:992/tcp \-p 1194:1194/udp -p 5555:5555/tcp -v /usr/vpnserver/vpn_server.config:/usr/vpnserver/vpn_server.config \-v /dev/null:/usr/vpnserver/server_log -v /dev/null:/usr/vpnserver/packet_log -v /dev/null:/usr/vpnserver/security_log siomiz/softethervpn:alpine

Если контейнер запустился, то переходим к следующему шагу.

Настройка SoftEther VPN Server


Для настройки SoftEther VPN Server лучше использовать графическую утилиту для ОС Windows. Для загрузки необходимо перейти на страницу SoftEther Download Center. В списке Select Component, выбрать SoftEther VPN Server Manager for Windows, далее Select Platform windows. Можно выбрать пакет .zip без необходимости установки. Пакет softether-vpn_admin_tools-v4.34-9745-rtm-2020.04.05-win32.zip распаковать и запустить vpnsmgr.exe.

Создаем новый профиль кнопка New Setting, указываем следующие настройка:

  • Setting Name: VDSina_ru_main_server
  • Host Name: v636096.hosted-by-vdsina.ru
  • Port Number: 443
  • Password: пароль который был указан в переменной SPW при создание конфигурационного файла vpn_server.config

Затем подключаемся к серверу кнопка Connect.

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

Для настройки алгоритма шифрования нажать на кнопку Encryption and Network. По умолчанию включен алгоритм DHE-RSA-AES256-SHA. Из списка выбрать другие более стойкие комбинации шифрования, но нужно помнить чем сильнее алгоритм, тем больше нагрузка на CPU сервера и на конечное маршрутизирующее устройство.

По умолчанию будет доступен хаб DEFAULT, удаляем его.

Создаем новый хаб кнопка Create a Virtual Hub. Укажем Virtual Hub Name: VPNROOT. Открываем настройки хаба кнопка Manage Virtual Hub.

Создадим пользователя подключения, кнопка Manage Users, затем кнопка New. Аутентификация будет по паре логин/пароль, укажем имя: officeuser1.

Для отделение подсети клиентов VPN сервера и подсети Docker контейнеров включим NAT, кнопка Virtual NAT and Virtual DHCP Server (SecureNAT), далее кнопка Enable SecureNAT. Изменим подсеть VPN клиентов на: 192.168.30.x, закроем окно, кнопка Exit.

На этом настройка сервера закончена.


Последовательность действий по настройке SoftEther VPN Server

Получение файлов конфигурации *.ovpn


Для подключения OpenVPN клиента необходимо получить файлы конфигурации *.ovpn, для этого переходим на главный экран настроек SoftEther VPN Server и нажимаем на кнопку OpenVPN / MS-SSTP Settings. Далее, в следующем окне генерируем файлы конфигурации, кнопка Generate a Sample Configuration File for OpenVPN Clients. Сохраняем архив OpenVPN_Sample_Config_v636096.hosted-by-vdsina.ru_20210519_150311.zip, для дальнейшего подключения потребуется файл f1167ecd086e_openvpn_remote_access_l3.ovpn.


Последовательность действий по получению файлов конфигурации *.ovpn

Настройка OpenVPN на RaspAP


Создание маршрутов
Теперь переходим в консоль Cubietruck и добавляем маршруты:

sudo iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADEsudo iptables -A FORWARD -i tun0 -o wlan1 -m state --state RELATED,ESTABLISHED -j ACCEPTsudo iptables -A FORWARD -i wlan1 -o tun0 -j ACCEPT

Делаем копию существующих маршрутов и сохраняем новые

cp /etc/iptables/rules.v4 /etc/iptables/rules.v4.baksudo iptables-save | sudo tee /etc/iptables/rules.v4

Настройка профиля OpenVPN

Переходим на портал по адресу 192.168.43.12/openvpn_conf и указываем данные для подключения:

  • Username: officeuser1
  • Password: указанный для officeuser1 в SoftEther VPN Server
  • Для конфигурационного файла выбираем файл f1167ecd086e_openvpn_remote_access_l3.ovpn.

Сохраняем настройки и перезапускаем OpenVPN. Если подключение удалось, но в поле IPV4 ADDRESS будет публичный IP-адрес VPN сервера: 94.103.85.152 (v636096.hosted-by-vdsina.ru).

raspap
Страница настроек OpenVPN в RaspAP

Настройка Captive portal


Установка Captive portal в руководстве Captive portal setup.

Для установки выполним следующие действия:

sudo apt-get updatesudo apt-get install -y libmicrohttpd-devcd ~/git clone https://github.com/nodogsplash/nodogsplash.gitcd nodogsplashmakesudo make install

Далее необходимо внести изменения в конфигурационный файл /etc/nodogsplash/nodogsplash.conf. Указать следующие параметры:

...GatewayInterface wlan1...GatewayAddress 10.3.141.1...

Регистрация службы и запуск:

sudo cp ~/nodogsplash/debian/nodogsplash.service /lib/systemd/system/sudo systemctl enable nodogsplash.servicesudo systemctl start nodogsplash.service sudo systemctl status nodogsplash.service

Страницы html для изменение дизайна страниц располагаются по пути: /etc/nodogsplash/htdocs/. Теперь выполним подключение к AP SSID: raspi-webgui.

Устранение проблем


Первым делом необходимо проверить IP-конфигурацию сетевых интерфейсов, командами: ifconfig или ip a.

Проверить занятость портов, командами:

netstat -ntlp | grep LISTENlsof -i | grep LISTENlsof -nP -i | grep LISTEN

Если установка RaspAP выполняется на Ubuntu, то вы можете столкнуться с конфликтом использования 53 порта, который занят службой systemd-resolved. Для отключения данной службы, воспользоваться материалом How to disable systemd-resolved in Ubuntu.

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

sudo systemctl status hostapd.servicesudo systemctl status dnsmasq.servicesudo systemctl status lighttpd.servicesudo systemctl status openvpn-client@clientsudo systemctl status nodogsplash.servicesudo systemctl status raspapd.service


Что дальше?


Для проверки совместимости необходимо RaspAP развернуть на Banana Pi BPI-M64 (Armbian 21.02.1 на основе Ubuntu 18.04.5 LTS). Далее, развернуть на x86 с другим более новым адаптером, например USB Realtek 8811CU Wireless LAN 802.11ac. На GitHub размещен репозиторий raspap-docker, который оборачивает RaspAP в контейнер, но по факту запускает скрипт автоматической установки, что несколько неудобно и неправильно. Поэтому для более широкого распространения RaspAP необходимо его правильно обернуть в Docker контейнер для ARM и x86 архитектур.

Итог


Проект RaspAP безусловно заслуживает внимания, основные функции работают отлично. Это единственный проект связанный с Wi-Fi сетями, работающий поверх существующей ОС, у которого работает Web-интерфейс (пока есть небольшие баги). Для личного использования, теста, стоит попробовать. Но для продакшен в бизнесе пока лучше не использовать, необходимо более детально просмотреть исходный код и конфигурацию на предмет безопасности. В любом случае, проект добавил себе в закладки, надеюсь баги исправят в скором времени.



На правах рекламы


VDSina предлагает виртуальные серверы на Linux и Windows выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

Archlinuxarm просто

19.03.2021 18:12:20 | Автор: admin

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

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

sudo pacman -Syu manjaro-arm-installer

По завершении установки подключить носитель предназначенный для одноплатного ПК и запустить

sudo bash manjaro-arm-installer

Дальше заполнить несколько скринов настроек и подождать, пока пройдёт установка. Дальше размонтировать и извлечь накопитель и готовиться к чуду воткнуть в одноплатный пк. Если всё прошло успешно, у вас за ~ 30 минут будет в строю ваша платка, и, что немаловажно, под управлением archlinuxarm.

Линк на сам установщик. Там написано какие одноплатные пк потдерживаються (Raspberry Pi 4 точно есть), ровно как и то, каким образом запустить этот установщик на произвольном дистрибутиве linux.

Подробнее..
Категории: *nix , Arm , Install , Operating systems , Archlinux

Обзор инструкций ARM NEON для тех, кто знаком с MMXSSEAVX

31.03.2021 10:06:06 | Автор: admin

Мир изменился. Я чувствую это в воде, чувствую это в земле, ощущаю в воздухе.

Властелин колец, Джон Рональд Руэл Толкин

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

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

Мой опыт написания высокопроизводительного кода в основном связан с обработкой изображений в библиотеке Pillow-SIMD. Там я использовал интринсики в коде на Си чтобы добиться 6-8-кратного ускорения наиболее частых операций.

Под что вообще пишем?

Честно говоря, это самый высокий порог для вхождения в ARM архитектуру из тех, что будут. А x86 есть базовый набор команд, есть расширения (разные версии SSE, AVX, криптография или виртуализация) и есть разрядность (32 или 64 бита). В ARM же, загибайте пальцы:

  • Есть архитектуры, коих более 20. Называются они примерно так: ARMv7-M, ARMv8-R, ARMv8.3-A.

  • Есть микроархитектура. Например: Cortex-R4, Cortex-A76.

  • Есть профайл: Classic, Microcontroller, Real-time, Application.

  • Есть разные наборы команд! A32, A64, Thumb, Thumb2.

  • Наконец, расширения набора команд: SIMD, NEON, SVE.

  • Ну и никуда не делась разрядность: AArch32 и AArch64.

Не претендуя на полноту описания, я подсвечу основные моменты и укажу, что сейчас можно опустить. Все архитектуры соответствуют одному из четырех профайлов. Classic это прям совсем классик, такое вы вряд ли встретите. Из трёх остальных самое ходовое это Application. Все телефоны, сервера и рабочие станции это Application. Профайл всегда отражён в названии архитектуры в виде постфикса (A, M, R). Актуальных архитектур всего две ARMv7 и ARMv8, зато у ARMv8 вышло уже 6 минорных версий, которые тоже называются архитектурами (например, ARMv8.2-A). Причём 64-битная разрядность появилась только в ARMv8. Однако ARMv8-A не гарантирует наличие 64-битного режима у процессора, а вот ARMv8.1-A уже гарантирует.

Если знание архитектуры чипа нужно нам, разработчикам софта, чтобы знать, какой минимальный набор функциональности возможно использовать, то микроархитектура уже нужна для разработчиков чипов, чтобы знать, сколько кеша нужно насыпать, сколько вычислительных блоков должно быть и какие опциональные технологи нужно включить в чип. Причем, бывает как микроархитектура от самой компании ARM (она обычно называется Cortex и следом снова постфикс профайла), так и кастомная, которая может называться Apple Firestorm, Neoverse N1 или никак не называться.

Набор команд A32 используется в 32-битном режиме AArch32, а A64 в 64-битном режиме AArch64. И казалось бы, зачем выделять такие очевидные вещи. Но дело в том, что A32 не единственный набор команд, который может быть в 32-битном режиме. A32 и A64 всегда используют 32 бита для кодирования любой инструкции, а AArch32 вышел очень давно и многим казалось, что это расточительство и тогда появились альтернативные способы кодирования Thumb и Thumb2. В них часто используемые инструкции занимали 16 бит. Для AArch64 уже ничего такого не завезли, в нём любая инструкция занимает 32 бита.

Ну и наконец, расширения набора команд. Вообще, есть ещё расширения VFPv1-VFPv5 для работы с плавающей точкой, разницу между которыми я так и не смог понять. Как и в x86, в ARM плавающую точку завезли не сразу. В ARMv6 было добавлено расширение SIMD (так и называется), а в ARMv7 появился опциональный 128-битный advanced SIMD, он же ASIMD, он же NEON, по сути прямой аналог SSE последних версий. О нём я буду рассказывать больше всего. А вот аналога AVX в ARM нет, там пошли другим путём. Вместо того, чтобы каждые пять лет представлять новое расширение, под которые нужно будет всё переписывать, было разработано расширение Scalable Vector Extension (SVE), которое позволяет выполнять один и тот же код на чипах, реализующих разный размер векторов. Но на практике, как я понял, SVE реализован только в Fugaku supercomputer.

Это же ужас?

Ну, вообще, да, если вы собрались писать приложение, которое может быть выполнено на любом ARM процессоре, как это бывает с x86. Теоретически на нем может не оказаться не только NEON, но даже 64-битной арифметики с плавающей точкой. Вот только, к счастью, у ARM нет того наследия работающих систем, на которых могли бы запустить ваш код. Это в любом случае будет свежий процессор. И ещё, с максимальной вероятностью это всё же будет AArch64 система. А теперь следите за руками.

AArch64 появился только в ARMv8. ARMv8-A уже гарантирует наличие VFPv4 (64-битный FPU), NEON и криптографии. А SVE можно даже не проверять ещё пару лет. У NEON никаких версий нет. Так же остается только один набор инструкций: A64. А микроархитектура просто ни на что не влияет.

Получается, несмотря на огромное количество вариантов, в реальности писать код под ARM (точнее под AArch64) даже проще, чем под x86. Никакие проверки в рантайме не нужны, просто ставите `#ifdef __aarch64__` и пользуетесь всем, чем хотите.

Знакомство с NEON

Принципиальное устройство x86 и ARM мало чем отличаются. И там и там есть общие регистры и регистры для вычислений с плавающей точкой и SIMD. Для общей картины достаточно прочитать раздел General-purpose registers в AArch64 Instruction Set Architecture. И сразу после этого можно переходить к самому полному вводному гайду Coding for Neon.

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

LD3 { V0.16B, V1.16B, V2.16B }, [x0]

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

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

В SSE, например, такого нет. Похожая функциональность появилась только в AVX2 с инструкциями vpsrlv[dq], vpsllv[dq], vpsravd. Но, во-первых, в NEON инструкции сдвигают в обе стороны, в зависимости от знака. Во-вторых, в AVX2 можно сдвинуть только 32-битные и 64-битные значения. На этом вкусности NEON не заканчиваются, из других вариантов сдвига есть:

  • Сдвиг и сложение с аккумулятором

  • Сдвиг вправо с округлением

  • Сдвиг с сатурацией

  • Сдвиг с уменьшением или увеличением разрядности

Причем некоторые варианты из списка можно комбинировать, и всё это работает с любым типом данных. Поправьте, но вроде SSE ничего из этого не может предложить.

Что касается интринсиков, в отличие от SSE/AVX, где типизированны только регистры для float и double (__m128и __m128d), в NEON есть типы для всех целых типов и названия придерживаются конвенции stdint.h.

uint8x16_t Sx4 = vld1q_u8(&Srgba[i]);uint8x16_t Dx4 = vld1q_u8(&Drgba[i]);uint32x4_t Sax4 = vshrq_n_u32((uint32x4_t) Sx4, 24);uint32x4_t Dax4 = vshrq_n_u32((uint32x4_t) Dx4, 24);

Тут первые две переменны имеют тип 16 беззнаковых int8, вторые 4 беззнаковых int32. Но, так как это одни и те же регистры, их можно приводить друг к другу. Интересно, что есть типы вроде uint8x16x3_t это три регистра подряд. В основном такие типы используются для загрузки и сохранения в оперативную память.

Справочник интринсиков

Если вы за последнее десятилетие писали SIMD-код для x86, вы наверняка пользовались Intel Intrinsics Guide. Это прекрасный справочник с интерактивным поиском и фильтром, понятным описанием и псевдокодом для каждой инструкции. И даже есть таблицы задержек и пропускной способности по разным поколениям процессоров Intel.

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

У ARM аналогом этого гайда служит Neon Intrinsics Reference. И это просто боль и унижение.

  • Нет никаких фильтров

  • Поиск работает с перезагрузкой страницы

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

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

  • По запросу "mul" находятся 50 страниц функций! То есть 1500 штук. Знаете, почему в результатах оказалась функция, показанная на скриншоте? Потому что в описании есть слово accumulate!

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

  • Вы вообще видели этот псевдокод? Он сам по себе очень избыточен и запутан.

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

Подходящая задача

Давайте попробуем NEON в деле. В качестве примера кода, на котором можно поэкспериментировать, я выбрал альфа-композитинг с premultiplied alpha. Его можно описать такой формулой:

R_{rgba} = S_{rgba} + D_{rgba} (1 S_a)

Алгоритм идеально ложится на SIMD:

#include <stdint.h>#include <stddef.h>#define SHIFTFORDIV255(a)\    ((((a) >> 8) + a) >> 8)#define DIV255(a)\    SHIFTFORDIV255(a + 0x80)static voidopSourceOver_premul(uint8_t* restrict Rrgba,                    const uint8_t* restrict Srgba,                    const uint8_t* restrict Drgba, size_t len){    size_t i = 0;    for (; i < len*4; i += 4) {        uint8_t Sa = Srgba[i + 3];        Rrgba[i + 0] = DIV255(Srgba[i + 0] * 255 + Drgba[i + 0] * (255 - Sa));        Rrgba[i + 1] = DIV255(Srgba[i + 1] * 255 + Drgba[i + 1] * (255 - Sa));        Rrgba[i + 2] = DIV255(Srgba[i + 2] * 255 + Drgba[i + 2] * (255 - Sa));        Rrgba[i + 3] = DIV255(Srgba[i + 3] * 255 + Drgba[i + 3] * (255 - Sa));    }}

Все операции выполняются над целочисленными значениями. 1 - Sa превращается в 255 - Sa. Так как правую часть мы умножаем на байт, то и левую нужно умножить. А после сложения нужно поделить на 255, это делает макрос DIV255путём нескольких сдвигов.

Запускать я буду на Raspberry Pi 4, естественно под AArch64. Чем богаты, тем и рады, как говорится. Причем в данном случае мне интересно посмотреть именно пиковую производительность, без влияния памяти. Для этого я буду тестировать на строке длиной 1000 пикселей, то есть всего будет задействовано 12 Кб данных за один вызов функции.

Я буду пользоваться компилятором Clang-9, т.к. он в большинстве случаев выдает более быстрый код, чем GCC. Для начала интересно, как быстро работает чистый код, без векторизации.

$ clang-9 -Wall -O2 -o run.64 main.c -fno-tree-vectorize && ./run.64Time elapsed: 0.189449Time elapsed: 0.189280Time elapsed: 0.189272Time elapsed: 0.189272

Время указано в секундах для 20 тысяч прогонов функции с длиной строк 1000 пикселей. То есть можно сказать, что скорость работы примерно 105 МПх/с. И вообще-то это очень мало, даже для Raspberry Pi. Если включить автоматическую векторизацию, результат будет чуть лучше.

$ clang-9 -Wall -O2 -o run.64 main.c -ftree-vectorize && ./run.64Time elapsed: 0.082168Time elapsed: 0.082341Time elapsed: 0.082135Time elapsed: 0.082147

Ускорение в 2.3 раза существенно, но это не всё, на что можно было бы рассчитывать. Посмотрим, что можно сделать вручную.

NEON-версия

Первое, что нужно сделать, чтобы начать программировать на NEON подключить заголовочный файл arm_neon.h. Согласитесь, это приятнее, чем каждый раз гуглить ничего не значащие имена заголовочных файлов, вроде smmintrin.h.

1. Загрузка/сохранение. Хотя видно, что код хорошо векторизуется внутри цикла, можно сразу пойти чуть дальше и читать из памяти 128-битный вектор целиком и работать с четырьмя пикселями.

#include <stdint.h>#include <stddef.h>#include <arm_neon.h>static voidopSourceOver_premul(uint8_t* restrict Rrgba,                    const uint8_t* restrict Srgba,                    const uint8_t* restrict Drgba, size_t len){    size_t i = 0;    for (; i < len*4 - 12; i += 16) {        uint8x16_t Sx4 = vld1q_u8(&Srgba[i]);        uint8x16_t Dx4 = vld1q_u8(&Drgba[i]);        uint8x16_t Rx4 = vaddq_u8(Sx4, Dx4);  // Temporary stub        vst1q_u8(&Rrgba[i], Rx4);    }    for (; i < len*4; i += 4) {        uint8_t Sa = Srgba[i + 3];        Rrgba[i + 0] = DIV255(Srgba[i + 0] * 255 + Drgba[i + 0] * (255 - Sa));        Rrgba[i + 1] = DIV255(Srgba[i + 1] * 255 + Drgba[i + 1] * (255 - Sa));        Rrgba[i + 2] = DIV255(Srgba[i + 2] * 255 + Drgba[i + 2] * (255 - Sa));        Rrgba[i + 3] = DIV255(Srgba[i + 3] * 255 + Drgba[i + 3] * (255 - Sa));    }}

Тут пока что Rx4считается намеренно неправильно. Но зато код запускается и уже можно прикинуть, сколько работает код на NEON, если он ничего не делает:

$ clang-9 -Wall -O2 -o run.64 main.c && ./run.64Time elapsed: 0.008030Time elapsed: 0.007872Time elapsed: 0.007859Time elapsed: 0.008629

8 мс или 2500 МПх/с! Вот мы и ускорили код с помощью NEON в 25 раз.

2. Выделение альфа-канала. Следующая задача нужно из вектора Sx4отдельно вытащить все компоненты альфа-канала. Они находятся на 3, 7, 11 и 15 позиции. Причем желательно их сразу размножить на все соседние байты этого же пикселя. Несмотря на множество команд перемешивания байтов, я не нашел ничего специального и сделал через векторный поиск в таблице. Дальше нужно вычесть полученное значение из 255.

uint8x16_t vsubq_u8 (uint8x16_t a, uint8x16_t b);uint8x16_t vdupq_n_u8 (uint8_t value);uint8x16_t vqtbl1q_u8 (uint8x16_t t, uint8x16_t idx);uint8x16_t Sax4 = vsubq_u8(    vdupq_n_u8(255),    vqtbl1q_u8(Sx4, (uint8x16_t){3,3,3,3, 7,7,7,7, 11,11,11,11, 15,15,15,15}));

Интересно, что все компиляторы при оптимизации заменяют операцию вычитания из 255 на побитовое отрицание, что логично.

3. Умножение. Дальше нужно все элементы Sx4умножить на 255, а элементы Dx4на соответствующие элементы альфы из Sax4. Все значения 8-битные.

В NEON есть два вида умножения: либо это обычные функции vmulq_*, которые не меняют разрядность и отдают нижнюю часть результата, либо это vmull_* и vmull_high_*, которые делают операцию только над половиной вектора, но зато увеличивают разрядность и отдают результат целиком.

uint16x8_t vmull_u8 (uint8x8_t a, uint8x8_t b);uint8x8_t vget_low_u8 (uint8x16_t a);uint8x8_t vdup_n_u8 (uint8_t value);uint16x8_t vmull_high_u8 (uint8x16_t a, uint8x16_t b);uint16x8_t Rx2lo = vmull_u8(vget_low_u8(Sx4), vdup_n_u8(255));uint16x8_t Rx2hi = vmull_high_u8(Sx4, vdupq_n_u8(255));

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

uint16x8_t vmlal_u8 (uint16x8_t a, uint8x8_t b, uint8x8_t c);uint16x8_t vmlal_high_u8 (uint16x8_t a, uint8x16_t b, uint8x16_t c);Rx2lo = vmlal_u8(Rx2lo, vget_low_u8(Dx4), vget_low_u8(Sax4));Rx2hi = vmlal_high_u8(Rx2hi, Dx4, Sax4);

4. Деление на 255. Ну что, пришла пора опробовать в деле крутые сдвиги. В Си-версии это происходит так:

#define DIV255(a)\    ((((a + 0x80) >> 8) + a + 0x80) >> 8)

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

#define ROUND_SHR(a, n)\    ((a + (1<<(n-1))) >> n)#define DIV255(a)\    ROUND_SHR(ROUND_SHR(a, 8) + a, 8)

Дальше следует обратить внимание на конструкцию ROUND_SHR(a, 8) + a. Это же сдвиг с аккумулятором vrsraq_n_u16, помните? Ну а последний сдвиг можно сделать так, чтобы он заодно уменьшал разрядность результата, ведь тот не должен превышать 255. Кроме того, можно уменьшить разрядность не только в нижнюю половину вектора, но и в верхнюю (vqrshrn_high_n_u16).

uint8x16_t vqrshrn_high_n_u16 (uint8x8_t r, uint16x8_t a, const int n);uint8x8_t vqrshrn_n_u16 (uint16x8_t a, const int n);uint16x8_t vrsraq_n_u16 (uint16x8_t a, uint16x8_t b, const int n);uint8x16_t Rx4 = vqrshrn_high_n_u16(    vqrshrn_n_u16(vrsraq_n_u16(Rx2lo, Rx2lo, 8), 8),    vrsraq_n_u16(Rx2hi, Rx2hi, 8), 8);

Всё вместе:

static voidopSourceOver_premul(uint8_t* restrict Rrgba,                    const uint8_t* restrict Srgba,                    const uint8_t* restrict Drgba, size_t len){    size_t i = 0;    for (; i < len*4 - 12; i += 16) {        uint8x16_t Sx4 = vld1q_u8(&Srgba[i]);        uint8x16_t Dx4 = vld1q_u8(&Drgba[i]);        uint8x16_t Sax4 = vsubq_u8(            vdupq_n_u8(255),            vqtbl1q_u8(Sx4, (uint8x16_t){3,3,3,3, 7,7,7,7, 11,11,11,11, 15,15,15,15})        );        uint16x8_t Rx2lo = vmull_u8(vget_low_u8(Sx4), vdup_n_u8(255));        uint16x8_t Rx2hi = vmull_high_u8(Sx4, vdupq_n_u8(255));        Rx2lo = vmlal_u8(Rx2lo, vget_low_u8(Dx4), vget_low_u8(Sax4));        Rx2hi = vmlal_high_u8(Rx2hi, Dx4, Sax4);        uint8x16_t Rx4 = vqrshrn_high_n_u16(            vqrshrn_n_u16(vrsraq_n_u16(Rx2lo, Rx2lo, 8), 8),            vrsraq_n_u16(Rx2hi, Rx2hi, 8), 8);        vst1q_u8(&Rrgba[i], Rx4);    }    for (; i < len*4; i += 4) {        uint8_t Sa = Srgba[i + 3];        Rrgba[i + 0] = DIV255(Srgba[i + 0] * 255 + Drgba[i + 0] * (255 - Sa));        Rrgba[i + 1] = DIV255(Srgba[i + 1] * 255 + Drgba[i + 1] * (255 - Sa));        Rrgba[i + 2] = DIV255(Srgba[i + 2] * 255 + Drgba[i + 2] * (255 - Sa));        Rrgba[i + 3] = DIV255(Srgba[i + 3] * 255 + Drgba[i + 3] * (255 - Sa));    }}

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

$ clang-9 -Wall -O2 -o run.64 main.c && ./run.64Time elapsed: 0.047613Time elapsed: 0.047455Time elapsed: 0.047452Time elapsed: 0.047448

Это в 1,75 раз быстрее, чем автовекторизованная версия и в 4 раза быстрее, чем версия совсем без векторизации (кстати, для GCC ускорение получается 5,5 раза).

Оптимизация чтения

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

Код выполнялся на процессоре Broadcom BCM2835, который хоть и относительно современный, но супербюджетный. Можно ожидать, что на таком процессоре могут быть существенные задержки даже для доступа к кешу L1. При этом никакие инструкции предвыборки не помогут, т.к. данные уже лежат в самом близком кеше. Зато может помочь предварительное чтение. Для этого нужно на каждом шаге класть в карман данные, которые понадобятся на следующем шаге. А из кармана доставать то, что было выбрано на предыдущем.

    uint8x16_t Sx4_next = vld1q_u8(&Srgba[0]);    uint8x16_t Dx4_next = vld1q_u8(&Drgba[0]);    // for (; i < len*4 - 12; i += 16) {    for (; i < len*4 - 12 - 16; i += 16) {        // uint8x16_t Sx4 = vld1q_u8(&Srgba[i]);        // uint8x16_t Dx4 = vld1q_u8(&Drgba[i]);        uint8x16_t Sx4 = Sx4_next;        uint8x16_t Dx4 = Dx4_next;        Sx4_next = vld1q_u8(&Srgba[i + 16]);        Dx4_next = vld1q_u8(&Drgba[i + 16]);        ...

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

$ clang-9 -Wall -O2 -o run.64 main.c && ./run.64Time elapsed: 0.038070Time elapsed: 0.037855Time elapsed: 0.037834Time elapsed: 0.037831

Гипотеза оказалась верной, это дало прирост ещё 25%. Итого NEON работает ровно в 5 раз быстрее, чем код без векторизации.

Разбор сгенерированного кода

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

$ clang-9 -Wall -O2 -o main.s main.c -S

Это вывод уже отформатированный, с некоторыми переименованными регистрами и с комментариями оригинального кода:

    ldr     q0, [x19]                   // Sx4 = vld1q_u8(&Srgba[0])    ldr     q1, [x20]                   // Dx4 = vld1q_u8(&Drgba[0])    movi    v17.2d, #0xffffffffffffffff    mov     x9, xzr.LBB0_3:    tbl     v5.16b, { v0.16b }, v16.16b // vqtbl1q_u8(Sx4, v16)    mvn     v5.16b, v5.16b              // Sax4 = vsubq_u8(vdupq_n_u8(255), v5)    ext     v6.16b, v0.16b, v0.16b, #8    umull   v3.8h, v1.8b, v5.8b         // Rx2lo = vmull_u8(Dx4, Sax4);    add     x10, x19, x9    add     x11, x20, x9    umull   v4.8h, v6.8b, v17.8b        // Rx2hi = vmull_high_u8(Sx4, 0xff)    umlal   v3.8h, v0.8b, v17.8b        // vmlal_u8(Rx2lo, Sx4, 0xff)    umlal2  v4.8h, v1.16b, v5.16b       // vmlal_high_u8(Rx2hi, Dx4, Sax4)    ldr     q0, [x10, #16]              // Sx4 = vld1q_u8(&Srgba[i + 16])    ldr     q1, [x11, #16]              // Dx4 = vld1q_u8(&Drgba[i + 16])    ursra   v3.8h, v3.8h, #8            // vrsraq_n_u16(Rx2lo, Rx2lo, 8)    ursra   v4.8h, v4.8h, #8            // vrsraq_n_u16(Rx2hi, Rx2hi, 8)    uqrshrn v2.8b, v3.8h, #8            // Rx4 = vqrshrn_n_u16(Rx2lo, 8)    add     x10, x9, #16                uqrshrn2 v2.16b, v4.8h, #8          // vqrshrn_high_n_u16(Rx4, Rx2hi, 8)    cmp     x10, #3972                      str     q2, [x21, x9]               // vst1q_u8(&Rrgba[i], Rx4)    mov     x9, x10    b.lo    .LBB0_3

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

Далее стоит обратить внимание на последовательности umullи umlal. Тут произошло странное, зачем-то компилятор заменил umull2 на ещё один umull. Для этого ему понадобилось в строчке 8 сделать лишнее извлечение верхней части v0 во временный регистр v6. Формально мы использовали функцию vget_low_u8, которая это и подразумевает. Однако это было сделано только для того, чтобы привести переменную к нужному типу. Если посмотреть, что генерируют компиляторы для такого кода, то видно, что они не очень понимают, что нижняя часть регистра это и есть сам регистр. Ну а GCC вообще творит какую-то дичь: создает два разных регистра с константами, делает три копирования.

На этом странности не заканчиваются. Для Rx2loпереставлены местами vmull_u8и vmlal_u8. Формально это ни на что не влияет, но все равно не понятно, зачем.

Ну и последнее, на что можно обратить внимание это странная работа с индексами. Если для команды str q2, [x21, x9]в качестве смещения используется регистр, то для ldrсмещение вычисляется заранее, причем два раза, хотя очевидно, что можно было вычислить x9 + 16и использовать это значение в обеих загрузках.

Пробуем всё это исправить:

    ldr     q0, [x19]                   // Sx4 = vld1q_u8(&Srgba[0])    ldr     q1, [x20]                   // Dx4 = vld1q_u8(&Drgba[0])    movi    v17.2d, #0xffffffffffffffff    mov     x9, xzr                     // i = 0.LBB0_3:    tbl     v5.16b, { v0.16b }, v16.16b // vqtbl1q_u8(Sx4, v16)    mvn     v5.16b, v5.16b              // Sax4 = vsubq_u8(vdupq_n_u8(255), v5)    add     x10, x9, #16                // x10 = i + 16    umull   v3.8h, v0.8b, v17.8b        // Rx2lo = vmull_u8(Sx4, 0xff);    umull2  v4.8h, v0.16b, v17.16b      // Rx2hi = vmull_high_u8(Sx4, 0xff)    ldr     q0, [x19, x10]              // Sx4 = vld1q_u8(&Srgba[i + 16])    umlal   v3.8h, v1.8b, v5.8b         // vmlal_u8(Rx2lo, Dx4, Sax4)    umlal2  v4.8h, v1.16b, v5.16b       // vmlal_high_u8(Rx2hi, Dx4, Sax4)    ldr     q1, [x20, x10]              // Dx4 = vld1q_u8(&Drgba[i + 16])    ursra   v3.8h, v3.8h, #8            // vrsraq_n_u16(Rx2lo, Rx2lo, 8)    ursra   v4.8h, v4.8h, #8            // vrsraq_n_u16(Rx2hi, Rx2hi, 8)    uqrshrn v2.8b, v3.8h, #8            // Rx4 = vqrshrn_n_u16(Rx2lo, 8)    uqrshrn2 v2.16b, v4.8h, #8          // vqrshrn_high_n_u16(Rx4, Rx2hi, 8)    cmp     x10, #3972                      str     q2, [x21, x9]               // vst1q_u8(&Rrgba[i], Rx4)    mov     x9, x10    b.lo    .LBB0_3

Запускаем:

$ clang-9 -Wall -O2 -o main.o -c main.s && gcc ./main.o -o run.64 && ./run.64 Time elapsed: 0.033388Time elapsed: 0.033204Time elapsed: 0.033223Time elapsed: 0.033190

Есть ещё 14% прироста. Итого ускорение 5,7 раз.

Было бы интересно также посчитать, сколько тактов уходит на этот цикл. Имеем 1800МГц (тактов/с), 0.033190 с/запуск и 250 * 20 * 1000 циклов/запуск. Итого: 1800000000 *0.033190 / (250 * 20 * 1000) 12 тактов/цикл. Учитывая, что в цикле 17 инструкций, это прекрасный результат. Я не думал, что такой простой процессор может работать так эффективно.

Решение на SSE

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

__m128i Sax4 = _mm_sub_epi8(    _mm_set1_epi8((char) 255),    _mm_shuffle_epi8(Sx4, _mm_set_epi8(        15,15,15,15, 11,11,11,11, 7,7,7,7, 3,3,3,3)));

Существенное отличие только в том, что порядок байтов у _mm_set_epi8инвертирован.

Дальше нужно 8-битное умножение. В SSE есть такое, это интринсик _mm_maddubs_epi16. И кстати, он же увеличивает разрядность результата и даже делает сложение соседних элементов. Можно было бы подумать, что дело в шляпе.

// Это неправильный код!__m128i Rx2lo = _mm_maddubs_epi16(    _mm_unpacklo_epi8(_mm_set1_epi8((char) 255), Sax4),    _mm_unpacklo_epi8(Sx4, Dx4));__m128i Rx2hi = _mm_maddubs_epi16(    _mm_unpackhi_epi8(_mm_set1_epi8((char) 255), Sax4),    _mm_unpackhi_epi8(Sx4, Dx4));

Количество инструкций умножения уменьшилось вдвое по сравнению с NEON-версией. Но, к сожалению, _mm_maddubs_epi16 принимаеттолько первый 8-битный аргумент как целое без знака, а второй аргумент считается со знаком, поэтому результат будет неверным. Функции, в которой оба аргумента были бы без знака, нет. А значит, нужно использовать 16-битное умножение и распаковывать каждый аргумент с пустым регистром, что существенно увеличивает и запутывает код.

__m128i Rx2lo = _mm_add_epi16(    _mm_mullo_epi16(_mm_unpacklo_epi8(Sx4, _mm_setzero_si128()),                    _mm_set1_epi16(255)),    _mm_mullo_epi16(_mm_unpacklo_epi8(Dx4, _mm_setzero_si128()),                    _mm_unpacklo_epi8(Sax4, _mm_setzero_si128())));__m128i Rx2hi = _mm_add_epi16(    _mm_mullo_epi16(_mm_unpackhi_epi8(Sx4, _mm_setzero_si128()),                    _mm_set1_epi16(255)),    _mm_mullo_epi16(_mm_unpackhi_epi8(Dx4, _mm_setzero_si128()),                    _mm_unpackhi_epi8(Sax4, _mm_setzero_si128())));

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

Rx2lo = _mm_add_epi16(Rx2lo, _mm_set1_epi16(0x80));Rx2lo = _mm_srli_epi16(_mm_add_epi16(_mm_srli_epi16(Rx2lo, 8), Rx2lo), 8);Rx2hi = _mm_add_epi16(Rx2hi, _mm_set1_epi16(0x80));Rx2hi = _mm_srli_epi16(_mm_add_epi16(_mm_srli_epi16(Rx2hi, 8), Rx2hi), 8);__m128i Rx4 = _mm_packus_epi16(Rx2lo, Rx2hi);

За вычетом загрузок/сохранений, констант и приведений типов, я насчитал 23 интринсика в SSE версии против 10 в NEON. Предложения по улучшению преветствуются.

Моё впечатление

NEON произвел впечатление очень продуманной и эффективной системы команд. Я нашел для себя такие плюсы:

  • В отличие от SSE, есть консистентность типов данных, с которыми работают разные инструкции.

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

  • Можно встраивать NEON-код в любое место приложения без проверок рантайм.

  • Использование NEON дает ощутимый прирост производительности, примерно равный такому от использования SSE.

  • Очень понятный ассемблер с типизированными аргументами.

  • Были опасения, что будет сильно не хватать инструкции _mm_madd_epi16, делающей 8 умножений и 4 сложения. Однако её функциональность покрывается парой vmull_*/vmlal_*, не требующих подготовки данных.

Минусы я бы отметил следующие:

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

  • Производительность сильно зависит от компилятора, возможно придется залочиться на clang.

  • Имена некоторых интринсиков напоминают читы в играх: vqrshrn_n_u16, vqdmulh_s16

Бенчмарки

Я собрал все варианты из этой статьи в один репозиторий с make-файлом, чтобы быстро запускать на разных системах или с разным окружением. Помимо Raspberry Pi 4 я смог запустить код ещё на c6g.largeинстансе в AWS, которые работают на процессорах AWS Graviton2. Вот что я намерил:

Raspberry Pi 4

c6g.large

GCC 8.3.0

Clang 9.0.1

GCC 9.3.0

Clang 9.0.1

Без векторизации

267,4 мс
7.94x

185,6 мс
5.51x

140,2 мс
10.01x

103,8 мс
7.41x

Авто векторизация

116,8
3.47x

82,55
2.45x

140,2
10.01x

46,54
3.32x

Ручная векторизация

46,36
1.38x

47,56
1.41x

22,85
1.63x

17,59
1.26x

Оптимизация чтения

46
1.37x

36,8
1.09x

24,01
1.71x

16,86
1.2x

Ассемблер

33,66 мс
1.0x

14 мс
1.0x

Коэффициентами я обозначил замедление относительно варианта на ассемблере. Таблица получилась очень интересная. Выводов можно сделать много:

  • Поведение очень сильно зависит от компилятора. Протестированные версии GCC практически везде медленнее Clang.

  • Автоматическая векторизация в целом работает, но не дает такого же эффекта, как ручная.

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

  • GCC также не оценил оптимизацию чтения. Я не смотрел код, но выглядит так, будто он её просто выкинул.

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

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

Кроме этого я все же решил измерить неизмеримое и сравнить несравнимое. Запустил тесты для ARM на Apple M1, а для x86 на Intel(R) Xeon. Выбор M1 понятен кроме него пока нет десктопных процессоров от Apple. А вот на чем запускать x86 было вопросом. На ноутбуке у меня процессор может работать в диапазоне от 2,4 до 4,1 Ггц при разных сценариях. Поэтому я решил запустить на серверном процессоре, у которого стабильная частота.

Ещё момент, в результатах на M1 был сильнейший разброс, вплоть до двух раз. Я запускал не у себя и у меня не было возможности проверить, почему так выходит. Моё предположение, что какое-то время работали слабые ядра. В любом случае, я брал минимальное время.

Apple M1

c5.large

Clang 12.0.0

GCC 9.3.0

Clang 9.0.1

Без векторизации

43,13 мс
4.74x

74,56 мс
5.09x

80,26 мс
6.16x

Авто векторизация

16,71
1.84x

74,54
5.09x

80,28
6.16x

Ручная 128-битная векторизация

9,09
1.0x

14,65
1.0x

13,03
1.0x

Ручная 256-битная векторизация

7,76
0.53x

6,52
0.5x

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

  • Скорость M1 без векторизации впечатляет. Это при том, что частота обоих чипов примерно одинаковая.

  • Упс, авто векторизация на x86 не сработала на обоих компиляторах. А случай всё ещё простейший.

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

  • Хоть 128-битная версия на M1 всё еще выполняется быстрее, чем на x86, против AVX ему нечего противопоставить.


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

Подробнее..

Перевод ARMv9 в чем преимущество?

06.04.2021 18:11:06 | Автор: admin

Что такое масштабируемые векторные расширения (Scalable Vector Extension)? Что они значат для индустрии и пользователей?

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

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

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

Есть много вещей, о которых стоит поговорить, но наиболее важной темой является стандартизация того, что мы называем масштабируемым векторным расширением (Scalable Vector Extension 2, SVE2). Вы наверняка слышали про наборы инструкций SIMD (Single Instruction Multiple Data, одиночный поток инструкций, множественный поток данных), такие как MMX, SSE, AVX, AVX-512 от Intel или Neon от ARM. Однако вы можете не знать, для чего они нужны. Я постараюсь объяснить, что отличает SVE/SVE2 от более старых наборов инструкций SIMD.

Знали ли вы, что Fujitsu сыграла важную роль во всем этом? Мы наблюдаем своего рода возвращение к супервычислениям старой школы, которые встречались в суперкомпьютерах Cray-1 несколько десятилетий назад. Фактически компания Cray не умерла и сейчас занимается созданием суперкомпьютеров на базе ARM: LRZ to Deploy HPEs Cray CS500 System with Arm Fujitsu A64FX Processors.

Суперкомпьютер Cray-1, 1976 год. Высота примерно 1.8м, диаметр 2.1м.

ARMv9 это процессор, который я могу купить в магазине?


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

Позвольте мне объяснить, как это работает. Множество компаний по всему миру, таких как Qualcomm, Apple, Fujitsu Ampere Computing, Amazon, проектируют собственные микропроцессоры. Это многоступенчатый процесс. Например, ни Apple, ни AMD не производят собственные чипы. Вместо этого они разрабатывают дизайн микросхемы, а затем отправляют его на заводы, например, Global Foundries или TSMC. Там дизайн травят на кремниевых пластинах, которые затем разрезают на отдельные микрочипы и упаковывают.

Компания ARM Ltd. не похожа на Qualcomm или Ampere Computing. Они не производят готовые чертежи, которые можно передавать на завод. Вместо этого они продают чертежи интеллектуальных блоков. Компании вроде Apple могут купить эти блоки и объединить их в итоговый чертеж, который отправится на фабрики.

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

ARMv9, как и предыдущий ARMv8, не законченный чертеж, который регламентирует соединение транзисторов. Это то, как вы размещаете транзисторы для достижения высокопроизводительной архитектуры. Мы называем это микроархитектурой.


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

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

Пример инструкций для загрузки чисел из памяти по адресам 14 и 23 в регистры x1 и x2 соответственно, сложения содержимого регистров и записи результата в x3.

load x1, 14       ; x1  memory[14]load x2, 24       ; x2  memory[24]add  x3, x1, x2   ; x3  x1 + x2

Чтобы узнать больше, читайте: How Does a Modern Microprocessor Work?

Архитектура процессора важна для разработчиков инструментов. Программное обеспечение, такое как компиляторы и линковщики, работает с заданной архитектурой. Это значит, что выпуск ARMv9 равнозначен выпуску нового стандарта для разработчиков программного и аппаратного обеспечения. Пока Apple, Ampere и Qualcomm производят оборудование, которое понимает инструкции, указанные в спецификации ARMv9, программное обеспечение, производящее код для этой спецификации, будет работать.

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

Что нового в ARMv9?


Чтобы показать, насколько большой вклад производит выпуск новой архитектуры, мы вспомним про ARMv8. ARMv8 была выпущена восемь лет назад и стала первой 64-битной архитектурой компании ARM Ltd. Микропроцессоры на базе ARMv7 были 32-битными. Это означало, что регистры внутри ЦП могли работать только с числами, которые содержали не более 32 двоичных цифр.

Появление ARMv8 было важным событием для Apple, так как это позволило им неожиданно рано для индустрии перейти на 64-битную архитектуру. Это дало iPhone и iPad фору. Несомненно, Apple хотела заниматься высокопроизводительными архитектурами как можно быстрее, поэтому они стремились создать процессоры ARM для настольных компьютеров.

Если упростить, то можно сказать, что ARMv8 это архитектура для настольных компьютеров. ARM сделала шаг от телефонов в мир настольных компьютеров и серверов.

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

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

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

Именно это приходит в ARMv9 благодаря добавлению целого ряда новых инструкций, которые называются SVE2, или Scalable Vector Extension 2. Другими словами, процессоры ARM становятся все более похожими на старые суперкомпьютеры.

Для дополнительного чтения: ARM, x86 and RISC-V Microprocessors Compared.

Суперкомпьютер в кармане


Почему ARM сегодня выбирает архитектуру как у суперкомпьютеров? Что происходит?

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

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

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

Читать далее: RISC-V Vector Instructions vs ARM and x86 SIMD.

ARM и высокопроизводительные вычисления


Следующим шагом после настольных компьютеров и серверов, естественно, являются высокопроизводительные вычисления (High Performance Computing, HPC). Настоящие суперкомпьютеры. В далеком прошлом в этом сегменте доминировали специальные аппаратные решения, такие как ARM, а затем появились крупные центры обработки данных с серийным оборудованием x86 и мощными видеокартами.

Intel и AMD хорошо зарабатывают на этом рынке, поскольку машинное обучение и анализ данных стали гораздо более распространенными и важными. Естественно, ARM хочет получить кусок этого рынка. Первым серьезным шагом стал микропроцессор Fujitsu A64FX на базе ARM.

У Fujitsu есть опыт построения Cray-подобных компьютеров с векторной обработкой. Они объединились с ARM, чтобы расширить процессоры ARM набором инструкций Scalable Vector Extension. Таким образом, это не изобретение ARM, а адаптация уже существующего набора инструкций для высокопроизводительных вычислений к процессорам ARM.

Эта комбинация, используемая для A64FX, стала основной для создания самого мощного суперкомпьютера в мире: Japans Fugaku gains title as worlds fastest supercomputer.

Процессор A64FX на архитектуре ARMv8, разработанный Fujitsu для высокопроизводительных вычислений. Это первый процессор с Scalable Vector Extension.

Но следует помнить об одном важном факте: это было расширением и не является частью спецификации ARMv8. То есть ваши iPhone или iPad с процессором на ARMv8 не может запускать код, созданный для суперкомпьютера Fugaku, потому что у них нет поддержки инструкций SVE.

Для сравнения, в ARMv9 такие инструкции стали частью стандарта. Именно поэтому я говорю, что ARM кладут суперкомпьютер в карман.

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

Они делают это таким образом, что прирост производительности обычного кода может превышать определенные ранее пределы. Подобные вещи невозможны с решениями, которые Intel и AMD использовали с инструкциями AVX для своих микропроцессоров x86.

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


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

Что такое вектор?


Вектор это просто причудливый математический термин для списка чисел. Например:

[3, 5, 9]

Удобно работать со списками чисел, обращаясь к ним по имени или иному идентификатору. Например, два вектора с именами v1 и v2.

v1  [3, 2, 1]      v2  [1, 2, 2]

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

v3  v1 + v2  ; v3 должен быть [4, 4, 3]

Почему я использую стрелки ()? Это полезно при объяснении того, что происходит внутри компьютера, потому что обычно мы храним числа в некоторой области памяти. Эта память может быть пронумерована или названа. Внутри микропроцессора есть небольшая область памяти, которая разделена на фрагменты, называемые регистрами. В микропроцессорах ARM эти регистры имеют имена, такие как x0, x1, , x31 или v0, v1, , v31.

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


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

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

x1  3        ; записать 3 в x1x2  4        ; записать 4 в x2x2  x1 + x2  ; сложение x1 и x2 дает 7

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

Представьте, что столбцы Amount и Unit Cost это векторы. Столбец Total Cost это результат поэлементного математического вычисления двух векторов.

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

Хотя современные компьютеры обычно не работают с матрицами, вычисления на матрицах можно ускорить с помощью векторов. Вот почему векторные инструкции важны для ускорения машинного обучения, распознавания изображений и речи. Математика векторов и матриц называется линейной алгеброй. У меня есть вступление для любопытных: The Core Idea of Linear Algebra.

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

SIMD против векторных инструкций


Технически ARM Neon и SVE являются формой SIMD (Single Instruction Multiple Data). Под этими инструкциями мы подразумеваем такие вещи, как сложение, вычитание и умножение. Таким образом, основная идея SIMD заключается в том, что вы отправляете одну инструкцию для процессора, а он выполняет одну и ту же операцию с несколькими значениями одновременно.

Один поток инструкций, один поток данных (SISD) и один поток инструкций множественные потоки данных (SIMD)
Подобные наборы инструкций существуют уже некоторое время. Вы, наверное, слышали про наборы инструкций MMX, SSE, а теперь и AVX на микропроцессорах x86 Intel и AMD. Они были созданы, чтобы выполнять обработку мультимедиа, такую как кодирование и декодирование видео. Инструкции ARM Neon наиболее похожи на них. Эти инструкции выглядят так:

LDR v0, [x4]    ; v0  memory[x4]LDR v1, [x6]    ; v1  memory[x6]ADD v4.16B, v0.16B, v1.16B STR v4, [x8]    ; v4  memory[x8]

В этом примере скалярные регистры x4, x6 и x8 содержат адреса в памяти, по которым располагаются числа в памяти.

Инструкция LDR загружает числа из памяти в регистр. Инструкция STR делает обратное: записывает числа из регистра в память.

Инструкция ADD выглядит странно. Почему там есть суффикс .16B после имени каждого регистра?

Дорожки в векторной обработке


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

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

128/8 = 16

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

ADD v4.2D, v0.2D, v1.2D

В терминах микропроцессора мы называем 32-битное число машинным словом (word), а 64-битное число двойным машинным словом (double-word). Таким образом, .2D означает два двойных слова, а .4S четыре одинарных.

128/32 = 4

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

Количество элементов, на которые мы разбиваем регистр во время вычисления, определяет, сколько дорожек будет настроено для вычислений. Подумайте о дороге, где числа, словно машины, идут параллельно по нескольким полосам. Ниже приведен пример этого. У нас есть регистры v1 и v2, которые используют для вычислений, а результат сохраняется в регистр v3. Таким образом, мы разбиваем на два элемента (.2D), и у нас есть две дорожки для вычислений. Каждая дорожка получает одно арифметико-логическое устройство (АЛУ).

Сколько АЛУ используется в SIMD-вычислениях. У нас есть две дорожки вычислений. Каждая дорожка обслуживается одним АЛУ.

Если вам не нравятся мои иллюстрации, то вот иллюстрация ARM, которая демонстрирует задумку на четырех дорожках.

Сложение регистров v8 и v9 с четырьмя дорожками

Проблемы с инструкциями SIMD


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

ADD v4.2D, v0.2D, v1.2D ADD v4.4S, v0.4S, v1.4S

Однако они кодируются как отдельные инструкции. Это быстро выходит из-под контроля, что хорошо видно на примере x86. Intel начала с MMX, затем появились SSE, SSE2, AVX, AVX2 и наконец AVX-512. MMX, например, имел 64-битные векторные регистры, поэтому вы могли выполнять параллельную работу над двумя 32-битными регистрами или восемью 8-битными.

Со временем, когда транзисторов становилось все больше, было принято решение сделать новые векторные регистры большего размера. Например, SSE2 имеет 128-битные регистры. В конце концов этого оказалось недостаточно, и мы получили AVX, а AVX2 предоставил нам 256-битные регистры. Теперь, наконец, AVX-512 представил нам невероятные 512-битные регистры. Итак, теперь мы можем вычислять шестьдесят четыре 8-битных значения цвета параллельно.

Каждый раз, когда Intel делала доступными регистры большего размера, им приходилось добавлять множество новых инструкций. Почему? Потому что длина векторного регистра прописана в инструкции SIMD. Например, инструкция ADD потребуется для:

  1. Каждого регистра длиной 64, 128, 256 или 512 бит.
  2. Для каждого из регистров нужен отдельный вариант с нужным числом дорожек.

Таким образом, добавление инструкций SIMD привело к резкому увеличению числа инструкций, особенно для x86. И конечно же, не каждый процессор поддерживает эти инструкции. Только новые будут поддерживать AVX-512.

Почему ARM не следует стратегии AMD и Intel


Эта стратегия не работает для ARM. У Intel и AMD простая миссия. Они просто пытаются сделать самые мощные узкоспециализированные процессоры, которые они могут выпустить в любое время в магазин.

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

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

SVE и SVE2 позволяют ARM задавать разную физическую длину векторных регистров для каждого типа микросхем. В SVE/SVE2 векторный регистр должен иметь длину от 128 до 2048 бит. Для смартфоном с низким энергопотреблением они могут продавать дизайны с 128-битными векторными регистрами, а для суперкомпьютеров с 2048-битными.

Красота SVE в том, что один код будет работать как на суперкомпьютере, так и на дешевом телефоне. Это невозможно с инструкциями SIMD x86. Хотя я не являюсь экспертом по асемблерному коду ARM, судя по тому, как выглядит код при использовании Neon и SVE, мне кажется, что последний будет более эффективным даже при равной длине векторного регистра.

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

SVE в действии


Если мы посмотрим на инструкции Neon, то они кодируют количество дорожек так же, как указано в предыдущем примере.

ADD v4.2D, v0.2D, v1.2D ADD v4.4S, v0.4S, v1.4S

Но если мы переведем это в инструкции SVE, то мы увидим что-то подобное.

ADD v4.D, v0.D, v1.D ADD v4.S, v0.S, v1.S

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

Предикация


Вместо этого в SVE используется то, что мы называем предикацией. Есть набор специальных регистров p0, p1, , p15, которые работают как маски для вычислительных дорожек. Их можно использовать для включения или выключения дорожек. Таким образом, использовавшаяся ранее инструкция сложения выглядела бы так:

ADD v4.D, p0/M, v0.D, v1.D

Теперь у нас есть дополнительный аргумент p0/M, который позволяет процессору сохранять результаты сложения v0 и v1 в v4 только когда соответствующий элемент p0 равен логической единице (истина). В псевдокоде это выглядит следующим образом.

while i < N   if p0[i] == 1      v4[i] = v0[i] + v1[i]   else      v4[i] = v0[i]   end   i += 1end

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

LD1D z1.D, p0/Z, [x1, x3, LSL #3]

Здесь происходит несколько процессов, поэтому необходимо некоторое объяснение. [x1, x3, LSL #3] это типичный для ARM способ указания адреса памяти. Это можно прочесть так:

base_address = x1 + x3*2^3z1  memory[base_address]

Но поскольку мы используем предикат, то это не совсем правильно. Нам нужно отфильтровать то, что загружается. Более точно это выглядит так:

base = x1 + x3*2^3while i < N   if p0[i] == 1      v1[i] = memory[base + i]   else      v1[i] = 0   end   i += 1end

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

julia> mask = [false, true, true, false];julia> A = [2, 4, 8, 10];julia> B = [1, 3, 7, 9];julia> A[mask]2-element Vector{Int64}: 4 8 julia> B[[true, false, false, true]]2-element Vector{Int64}: 1 9 julia> A[mask] + B[mask]2-element Vector{Int64}:  7 15

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

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

Как работать с вектором неизвестной длины


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

Это помогает нам значительно упростить код векторной обработки и избежать необходимости знать точную длину вектора. Допустим, нам нужно обработать шесть 32-битных значений. То есть N = 6, и это единственное, что вы знаете во время компиляции. Инструкции Neon будут выглядеть так:

ADD v4.4S, v0.4S, v1.4S  ; v4  v0 + v1

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

С SVE этого делать не придется. Вместо этого нам приходит на помощь волшебная инструкция WHILELT. Вот пример:

WHILELT p3.s, x1, x4

Но что она делает? Я объясню на примере псевдокода. Допустим, есть M дорожек для векторной обработки. Вы не знаете значение M до начала выполнения, но, допустим, что M = 4. Тем не менее, мы знаем количество элементов, которые хотим обработать, то есть N = x4 = 6. Инструкция WHILELT (WHILE Less Than, пока меньше чем) работает так:

i = 0while i < M   if x1 < x4      p3[i] = 1   else      p3[i] = 0  end  i += 1  x1 += 1end

Таким образом, если мы представим выполнение этих векторных операций в цикле, то на первой итерации p3 будет выглядеть так:

x1 = 0p3 = [1, 1, 1, 1]

На второй итерации в какой-то момент x1 станет больше, чем x4, поэтому получаем следующее:

p3 = [1, 1, 0, 0]

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

Так работает вся обработка SIMD. Вы обрабатываете партии чисел. Так, например, если вам нужно обработать 20 элементов, а ваш векторный регистр вмещает 4 дорожки, то вы можете сделать всю работу за 5 итераций (54 = 20). Но что если у вас 22 элемента?

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

Операции загрузки и сохранения


Другой важной особенностью SVE-инструкций является поддержка того, что мы называем операциями сборки-разборки (gather-scatter). Это означает, что вы можете заполнить векторный регистр данными, которые распределены по нескольким ячейкам памяти, всего за одну операцию. Точно так же вы можете записывать результаты из вектора в несколько местоположений. Принцип аналогичен тому, что мы обсуждали с предикатами.

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

struct Sale {    int unit_price;    int sold_units;    int tax;}Sale sales[1000];

Допустим, у нас тысячи таких объектов. Обычно мы хотим проводить расчеты между связанными полями. Например, умножить цену unit_price на количество проданных единиц sold_units. Это значит, что вы хотите, чтобы в одном векторном регистра были цены за единицу, а в другом количество проданных единиц. Однако значения этих полей не лежат в памяти последовательно. Они чередуются.

Есть много таких деталей, которые позволяют применять SVE-инструкции к гораздо большее разнообразному коду. Можно векторизовать больше циклов for, что дает больше возможностей для повышения производительности.

Что предлагает SVE2?


Здесь, вы, естественно, задаетесь вопросом, а что добавляет SVE2, чего еще нет в SVE?

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

Помните, SVE создавался только для суперкомпьютерных вещей, а для мультимедийных рабочих нагрузок, для которых создавался Neon? Мультимедийным материалам обычно не нужны длинные регистры. Рассмотрим цветной пиксель, закодированный как RGBA. Это четыре 8-битных значения, которые помещаются в 32-битный регистр.


Каждый пиксель состоит из четырех компонент: красный, зеленый, синий цвета и прозрачность. Каждый байт должен вычисляться независимо. Мы можем сделать это, используя 32-битный векторный регистр с 4 дорожками.

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

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

Им не нужно каждые несколько лет добавлять множество новых SIMD-инструкций. SVE2 дает фундамент с большой стабильностью и хорошим пространством для роста.

Последствия для пользователей, разработчиков и отрасли


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

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

ARM также будет все больше вытеснять Intel и AMD из прибыльного бизнеса в центрах обработки данных. Я не являюсь экспертом по дизайну микросхем, но, видя, как RISC-V использует этот набор инструкций и понимает все преимущества, мне кажется, что Intel и AMD совершили ошибку, когда отказались от ARM. Их стратегия с SIMD не кажется мне разумной. Подозреваю, что эта ошибка будет их преследовать.

Подробнее..

Как сделать кластерный сервер на ARM процессоре и тестирование VPS на AWS Graviton2

28.04.2021 12:07:29 | Автор: admin
Cluster Server ARM

В предыдущей публикации рассматривались преимущества использование ARM серверов для хостинг провайдеров. В этом посте рассмотрим практические варианты создания кластерного сервера на ARM процессоре и протестируем инстанс Amazon EC2 T4g работающий на процессоре ARM AWS Graviton2, посмотрим на что он способен.

Создание серверного кластера на Raspberry/Banana/Orange Pi


На данный момент существует множество различных вариантов сборки кластера на базе различных одноплатных компьютеров. Можно собрать самостоятельно или купить готовую коробку (CASE) для наполнения модулями CoM Raspberry Pi.

Cluster Server ARM
Кластер на Raspberry/Banana/Orange Pi

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

  1. Малая производительность используемых ARM-процессоров.
  2. В большинстве случаем хранение данных только на eMMC или microSD.
  3. Большой набор лишней периферии, такой как модуль связи Wi-Fi и Bluetooth, порты видеовыхода HMDI и т.д., что приводит к лишней стоимости платы и увеличению энергопотребления.
  4. Невозможна плотная компоновка модулей.
  5. Большое количество лишних проводов для подключения линий электропитания и Ethernet.

Для решения выше перечисленных недостатков необходимо перейти на концепцию Компьютер-на-Модуле (Computer on Module, CoM).

Компьютер на модуле (CoM)


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

Cluster Server ARM
Компьютер на модуле (CoM)

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

Cluster Server ARM
Подключение модуля CoM к несущей плате

Стартап miniNodes разрабатывает недорогие ARM сервера, предназначенные для небольших облачных сервисов, веб-сайтов и приложений Интернета вещей. Они разработали микро-сервер состоящий из несущей платы и 5 модулей Raspberry Pi 3 CoM. Несущая плата Carrier Board содержит встроенный гигабитный коммутатор, который обеспечивает подключение ко всем 5 модулям. С противоположной стороны платы размещен модуль питания для обеспечивания питание CoM. Каждый из вычислительных модулей также имеет отдельный переключатель включения/выключения питания. Модули Raspberry Pi CM3 + поставляются с 8 ГБ, 16 ГБ или 32 ГБ eMMC на борту, поэтому нет необходимости использовать SD-карты, как на обычных платах Raspberry Pi. Таким образом, при полной загрузке есть 5 узлов, состоящих из 4 ядер, 1 ГБ ОЗУ и до 32 ГБ eMMC каждый, всего 20 ядер, 5 ГБ ОЗУ и до 160 ГБ хранилища.

Cluster Server ARM
5 Node Raspberry Pi 3 CoM Carrier Board

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

Блейд-сервер от Firefly на процессорах Rockchip


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

Cluster Server ARM
Модули COM от компании Firefly

Компания разработала сервер Cluster Server R1 в 1U форм-факторе который может содержать до 11 модулей CoM.

Cluster Server ARM
Сервер Cluster Server R1

Сервер предназначен для запуска приложений на Linux, облачных игр, виртуальных рабочих столов, тестирования мобильных приложений (до 110 виртуальных телефонов на Android). Возможен запуск ОС: Linux и Android.

Для комплектации доступны модули CoM:

  • RK3399(AI) Core Board (Core-3399-JD4): два ядра A72 + четыре ядра A53, частота до 1.8GHz
  • RK3328 Core Board (Core-3328-JD4): четы ядра A53, частота до 1.5GHz
  • RK1808(AI) Core Board (Core-1808-JD4): два ядра с A35, частота до 1.6GHz

На лицевой панели блейд-сервера размещено: 4 порта Gigabit Ethernet, HDMI, два порта USB2.0, OTG, дополнительно для модулей доступен 3.5-дюймовый жесткий диск SATA/SSD с горячей заменой, слот для SIM карт модуля 4G-LTE.

Cluster Server ARM
Лицевая панель сервера Cluster Server R1

Для управления узлами используется BMC (Baseboard Management Controller) с помощью которой можно управлять узлами: включать/выключать, удаленный доступ, мониторинг состояния, управление аппаратной конфигурацией.

В начале этого года была представлена вторая версия кластерного сервера Cluster Server R2. Cluster Server R2 поставляется в 2U форм-факторе и содержит:

  • 9 блейд-узлов (каждый узел содержит 8 модулей CoM).
  • Два 3.5-дюймовых жестких дисков SATA/SSD.
  • 4 порта Gigabit Ethernet.
  • два порта USB 3.0, USB 2.0, порт HDMI.

Cluster Server ARM
Cluster Server Cluster Server R2

Кластерный север так же работает под управлением ОС: Android, Ubuntu или некоторых других дистрибутивов Linux. Варианты использования сервера: облачный телефон, виртуальный рабочий стол, облачные игры, облачное хранилище, блокчейн, декодирование многоканального видео, и т. Д. Наличие AI (NPU-нейронный процессор) делает кластер похожим на Solidrun Janux GS31 Edge AI. Сервер, предназначенный для вывода в режиме реальном времени нескольких видеопотоков для мониторинга умных городов и инфраструктуры, интеллектуального корпоративного/промышленного видеонаблюдения, обнаружения, распознавания и классификации объектов, интеллектуального визуального анализа и т. д.

Что можно запустить на ARM процессоре?


Запуск приложений с помощью Docker и Kubernetes давно стал де-факто стандартом для Linux. Поэтому рассмотрим какие наиболее популярные контейнеры можно запустить под ARM:

  1. Portainer.io управление и мониторинг контейнерами с помощью web-интерфейса.
  2. OpenVPN самый популярный бесплатный VPN сервер
  3. SoftEther VPN мультипротокольный VPN-сервер с графическим интересом под Windows.
  4. Базы данных все официальные Docker-образы так же собраны для ARM архитектуры: PostgreSQL, Mariadb, MongoDB.
  5. Nginx-proxy Nginx прокси-сервер, образ на базе Alpine
  6. Traefik обратный прокси-сервер, альтернатива Nginx
  7. Wordpress популярная CMS-система
  8. Elasticsearch поисковая система на Java
  9. Asterisk PBX компьютерная телефония (в том числе, VoIP) с открытым исходным кодом от компании Digium
  10. Zabbix система мониторинга и отслеживания статусов различных сервисов компьютерной сети, серверов и сетевого оборудования

Отдельно необходимо отметить проект linuxserver.io, которые собирает контейнеры на базе наиболее популярных приложения для Linux, сборки готовятся и для архитектуры ARM. Наиболее популярные приложений для Linux представлены в виде контейнеров для ARM-систем, поэтому можно уже начинать тестировать.

Тестирование VPS на AWS Graviton2


Amazon для тестирования предоставляет инстансы на базе процессора ARM AWS Graviton2. Это отличная возможность бесплатно протестировать ПО на совместимость с ARM архитектурой и просто получить опыт работы, эксплуатации системы на ARM процессоре. Бесплатно предоставляется инстанс t4g.micro до 30 июня 2021 года в режиме 24x7. Для тестирования достаточно зарегистрироваться и развернуть инстанс на Ubuntu Server 20.04 LTS.
Конфигурация инстанса t4g.micro:

  • 2 vCPUs 2.5 GHz
  • 1 GiB memory
  • 8 GB SSD

Инстанс t4g.micro доступен для развертывания на различных площадках. Ближайшая к нам площадка Europe (Frankfurt) eu-central-1, пинг от Питера в среднем составляет 78 ms.

Команда lscpu выдает следующую информацию:

ubuntu@host:~$ lscpuArchitecture:                    aarch64CPU op-mode(s):                  32-bit, 64-bitByte Order:                      Little EndianCPU(s):                          2...Vendor ID:                       ARMModel:                           1Model name:                      Neoverse-N1Stepping:                        r3p1BogoMIPS:                        243.75L1d cache:                       128 KiBL1i cache:                       128 KiBL2 cache:                        2 MiBL3 cache:                        32 MiBNUMA node0 CPU(s):               0,1...Flags:                           fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimdd                                 p ssbs

Железо:

root@host:/home/ubuntu# inxi -bSystem:    Host: host Kernel: 5.4.0-1038-aws aarch64 bits: 64 Console: tty 1           Distro: Ubuntu 20.04.2 LTS (Focal Fossa)Machine:   Type: Other-vm? System: Amazon EC2 product: t4g.micro v: N/A serial: ec21ba8c-f1b0-3f47-74e0-c648a84383c4           Mobo: Amazon EC2 model: N/A serial: N/A UEFI: Amazon EC2 v: 1.0 date: 11/1/2018CPU:       Dual Core: Model N/A type: MCP speed: 0Graphics:  Message: No Device data found.           Display: server: No display server data found. Headless machine? tty: 130x42           Message: Advanced graphics data unavailable for root.Network:   Device-1: Amazon.com Elastic Network Adapter driver: enaDrives:    Local Storage: total: 8.00 GiB used: 2.52 GiB (31.5%)Info:      Processes: 145 Uptime: 2d 23h 46m Memory: 952.5 MiB used: 336.2 MiB (35.3%) Shell: bash inxi: 3.0.38

Тест CPU

Тестируем процессор sysbenchем:

root@host:/home/ubuntu# sysbench --test=cpu --cpu-max-prime=20000 --num-threads=1 runsysbench 1.0.18 (using system LuaJIT 2.1.0-beta3)Running the test with following options:Number of threads: 1Initializing random number generator from current timePrime numbers limit: 20000...CPU speed:    events per second:  1097.02General statistics:    total time:                          10.0002s    total number of events:              10972Latency (ms):         min:                                    0.91         avg:                                    0.91         max:                                    0.95         95th percentile:                        0.92         sum:                                 9998.11Threads fairness:    events (avg/stddev):           10972.0000/0.00    execution time (avg/stddev):   9.9981/0.00

Тест ОЗУ:

root@host:/home/ubuntu# sysbench --test=memory --num-threads=4 --memory-total-size=512MB runsysbench 1.0.18 (using system LuaJIT 2.1.0-beta3)...Total operations: 524288 (3814836.42 per second)512.00 MiB transferred (3725.43 MiB/sec)General statistics:    total time:                          0.1360s    total number of events:              524288Latency (ms):         min:                                    0.00         avg:                                    0.00         max:                                    8.00         95th percentile:                        0.00         sum:                                  315.89

Тест диска

Посмотрим что покажет dd. Скорость до полноценного SSD недотягивает, но уже полноценный SATA:

root@home:/home/ubuntu# dd if=/dev/zero of=test bs=64k count=16k conv=fdatasync16384+0 records in16384+0 records out1073741824 bytes (1.1 GB, 1.0 GiB) copied, 7.03664 s, 153 MB/s

Результаты теста бенчмарка 7-zip:
root@host:/home/ubuntu# 7za b7-Zip (a) [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs LE)LECPU Freq: - - - - - - 512000000 - -RAM size:     952 MB,  # CPU hardware threads:   2RAM usage:    441 MB,  # Benchmark threads:      2                       Compressing  |                  DecompressingDict     Speed Usage    R/U Rating  |      Speed Usage    R/U Rating         KiB/s     %   MIPS   MIPS  |      KiB/s     %   MIPS   MIPS22:       6957   167   4054   6768  |       8353   199    358    71323:        757   172    448    772  |      22509   200    975   194824:       7073   185   4118   7606  |      80370   200   3535   705625:       6831   185   4227   7800  |      77906   199   3480   6934----------------------------------  | ------------------------------Avr:             177   3212   5736  |              199   2087   4163Tot:             188   2649   4950


Сравнение стоимости инстанса t4g.micro с VPS на x86


Воспользуемся калькулятором calculator.aws и рассчитаем на сколько ARM дешевле x86 VPS на AWS. Расчет будем производить для площадки Europe (Frankfurt) eu-central-1. Ближайшей аналог x86 по характеристикам это инстанс t2.micro.

Конфигурация инстанса t2.micro:
  • 1 vCPUs
  • 1 GiB memory
  • 8 GB SSD

Допустим инстанс будет работать 365 дней * 24 часа * 1 год = 8760.0000 часов.

Ежемесячный платеж за аренду инстанса по требованию без учета трафика составит:
  • t4g.micro (ARM): 5.33 USD ~ 400 р. (по курсу 1$ = 75 р.);
  • t2.micro (x86): 7.96 USD ~ 597 р. (по курсу 1$ = 75 р.).

Получает что сервер на ARM стоит на 33% дешевле аналога на x86. Отдельно необходимо рассчитывать объем сетевого трафика, стоимость не зависит от типа архитектуры инстанса. Первый гигабайт трафика в месяц будет бесплатным, далее объем до 10 ТБ оплачивается по цене $0.09 за каждый Гб. Входящий трафик не тарифицируется. Если будем исходить из средней сетевой нагрузки в 150 Гб исходящего трафика в месяц, то стоимость за трафик будет:

  • 150 Гб * $0.09 = $13.5 ~ 1000 р. (по курсу 1$ = 75 р.);

В итоге VPS инстанс t4g.micro (ARM) с объемом трафика в 150 Гб в месяц будет стоить 1 400 р./месяц.

Если для сравнения взять VPS от VDSina.ru за 330 р./месяц (1 ядро, 30 ГБ NVMe, 32 ТБ трафика), то для конкурентного преимущества серверам на ARM еще расти и расти.

Вывод: Отсутствие нативного ПО под ARM архитектуру еще долго будет останавливающим фактором массового перехода. Но так или иначе частичный переход на ARM сервера это уже тренд. Основными двигателями перехода выступают три мощных фактора. Первый фактор независимость от основных поставщиков процессоров x86. Можно выбрать решение максимально подходящее под себя. Второй фактор возможность максимальной оптимизации под себя, исключение всего лишнего и добавление специализированных блоков, таких как NPU, FPGA, и т.д. Третий фактор открытость и доступность Linux. Если сравнивать ARM сервера для массового потребительского сектора, то в этом сегменте еще очень долго будет господствовать x86 архитектура. Скорее всего мы увидим создание нового сегмент рынка для специальных задач ориентированных на преимущества ARM архитектуры, например сервера с FPGA модулями или NPU.



На правах рекламы


Наша компания предлагает в аренду серверы с современными процессорами от Intel и AMD под самые разнообразные задачи. Эпичные серверы это VDS с AMD EPYC, частота ядра CPU до 3.4 GHz. Максимальная конфигурация 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe. Создайте свой собственный тариф самостоятельно в пару кликов!

Подробнее..

Полноценная GDB отладка через USB на плате BluePill (STM32F103С8T6)

09.03.2021 18:08:33 | Автор: admin

В данной статье речь пойдет о программировании и полноценной отладке микроконтроллера STM32F103C8T6 через USB.

Однажды, от коллег поступило предложение о участии в IoT проекте. Система предусматривала однопоточный запуск скриптов. Отладка производилась с помощью логов. И тут мне в голову пришла мысль о полноценной удаленной отладке проектов под микроконтроллеры.

Для начала нужно было опробовать все на прототипе. В качестве отладочной платы была выбрана почти всем знакомая BluePill на микроконтроллере STM32F103. Поскольку на данной отладке имеется интерфейс MicroUSB, было принято решение в прототипе использовать именно этот интерфейс. В будущем предполагался переход на UART подключенный к GSM модулю.

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

  1. Драйвер интерфейса USB со стороны микроконтроллера.

  2. Код обновления прошивки микроконтроллера с помощью GDB.

  3. GDB сервер.

  4. Вывод отладочных логов.

Обо все по порядку. Для прототипирования был реализован загрузчик (bootloader).

1. Первым был реализован протокол отладочного интерфейса. Т.е. USB. В качестве класса USB-устройств был выбран WinUSB. Для его реализации я использовал воспользовался исходным кодом библиотеки libopencm3. Для этого необходимо описать дескриптор устройства, дескрипторы конфигурации, интерфейса, конечных точек, а так же дескрипторы содержащие стоки "MSFT100" и "WINUSB". Последние два дескриптора требуются для определения устройства как WinUSB. Конфигурация конечных точек (USB-Endpoint) выбрана следующим образом control endpoint 0, bulk out endpoint 1, bulk in endpoint 81, bulk in endpoint 82. Конечная точка с номером ноль присутствует во всех устройствах USB, endpoint 1- применяется для передачи команд в загрузчик микроконтроллера, endpoint 81 - для передачи ответов на команды на компьютер, а 82 - для передачи текстовой информации (логов). Подробнее о USB можно прочитать в публикациях из разряда USB in a NutShell.

2. Требовался код работы с флеш памятью. Вы можете подумать что тут все просто. Это так и не так одновременно. Первая проблема, которая возникла,- невозможность стереть флеш память в обработчике прерывания. Дело в том, что архитектура Cortex M предусматривает два режима работы процессора. Thread и Handler. В первом режиме процессор находится после старта, а так же когда нет активных прерываний. В Handler mode исполняются все обработчики исключений и прерываний. К сожалению, стирание flash-памяти на STM32F103C8T6 в Handler режиме приводит к корректному статусу стирания памяти, но сама память не стирается.

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

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

3. Требовалось реализовать GDB-сервер. Я воспользовался исходным кодом проекта BlackMagic, для обработки команд приходящих из среды разработки. На самом деле приходящих от приложения arm-none-eabi-gdb. Далее команды транслировались в команды бинарного протокола, который используется для п процессе взаимодействия с микроконтроллером. Нижний уровень GDB-сервера выполнен с использованием библиотеки WinUSB.

4. После того как прототип заработал, я пришел к решению добавить вывод отладочной информации с использованием printf. Для передачи отладочных сообщений использовал endpoint 82. На самом деле 8 - это единица в старшем разряде, указывающая направление передачи данных по шине USB в сторону компьютера (Host-а).

Но таким образом функцией printf можно было пользоваться только в bootloader-е. А как же быть с отлаживаемым приложением? Обычно для взаимодействия с операционной системой используются прерывания/системные вызовы. Так BIOS использова int13, ms-dos int21. Мы же на микроконтроллере воспользуемся системным вызовом, т.е. командой svc. При выполнении данной команды в прошивке, будет вызван обработчик прерывания SVC, находящийся в bootloader-е. Что нам и требовалось сделать.

Bootloader использует 10Kb flash памяти, но зарезервировано 16Kb с целью расширения функционала. Так же используется 4K оперативной памяти. Оперативная память применяется для хранения буферов USB, контекста прерванного процесса, а так же как память стека обработчиков прерываний. Итого. Остается 16Kb из 20Kb оперативной памяти и 48Kb flash памяти. Хотя на самом деле flash-память в контроллере STM32F103C8T6 не 64Kb а 128Kb,- соответственно остается 112Kb.

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

И наконец, - что поддерживается:

  1. Загрузка прошивки на плату с использованием GDB. Т.е. непосредственно из среды программирования/отладки. В моем случае это STM32CubeIDE. Адрес вектора прерываний должен находится по адресу 0x8004000.

  2. Просмотр и изменение памяти.

  3. Просмотр и изменение регистров периферии.

  4. Восемь точек останова.

  5. Режим пошаговой отладки.

  6. Принудительная остановка.

В отлаживаемой прошивке нельзя изменять адрес вектора обработчика прерываний. Нельзя изменять приоритеты прерываний. Приоритет должен быть выше или равен 0x40 по значению. Нельзя запрещать прерывания systick, прерывание usb, и прерывания DebugMon, SvcHandler, а так же всех FaultHandler-s.

Код прототипа проекта доступен по ссылке

Видео работы с платой в среде STM32CubeIDE

Подробнее..

Перевод Яблочная ARMия

26.03.2021 20:17:43 | Автор: admin

Apple отказывается от процессоров Intel в пользу собственных на базе ARM. Последуют ли её примеру другие производители ПК?

Apple отказывается от процессоров Intel в пользу своих собственных. Почему это произошло и каковы возможные последствия для всего рынка ПК? Как случилось, что микроархитектура CPU, изначально появившаяся в безвестных британских домашних компьютерах в 1980-х годах, бросает вызов империи Intel? В этой статье мы рассмотрим специфику проектирования процессоров ARM, проследим за тем, как они совершенствовались с годами и как достигнутый прогресс отразился на тестах производительности, а также сопоставим полученные результаты с результатами тестов железа от Intel. Ещё порассуждаем о конкуренции на рынке ПО и как она сказывается на нас, потребителях. И хорошо ли для пользователей ПК иметь микроархитектуру, построенную на совершенно отличном от привычного набора инструкций.
Что представляет собой процессор от Intel? Что такое ПК? В те дни, когда компоненты для IBM поставляли различные производители, у неё (IBM) имелись собственные процессоры 801 RISC. Однако она отказалась от них в пользу более экономичных Intel 8088, и так повелось, что в любой совестимый ПК можно было поставить процессор с архитектурой x86.
В теории проектировать и производить x86-совместимые процессоры мог любой, однако по закону Intel обладала патентом на наборы команд CPU.Это означало, что всем желающим их приобрести, пришлось бы покупать лицензию. Если сторонняя фирма и занималась разработкой или производством процессоров x86, то только потому, что Intel или суд дали на это разрешение. С AMD дела обстоят иначе, поскольку она заключила соглашение о патентной лицензии с Intel, чтобы потом не судиться друг с другом до беспамятства.
Долгое время производством CPU на архитектуре x86 занималось несколько компаний: IBM с её линейкой 386-х и 486-х процессоров, AMD, Cytrix, VIA, NEC, Transmeta и др. Дизайн их CPU оставлял желать лучшего. Intel всегда была победителем, в то время как другие (за исключением AMD и IBM) были лишь рядовыми спортсменами. Вы, конечно, могли бы возразить, что на рынке была конкуренция. Но считалась ли конкуренцией битва за отбросы? Суть в том, что у Intel не было достойного соперника даже сегодня, учитывая, что дела у AMD идут хорошо, ей (AMD) принадлежит всего лишь 18% рынка. Сам производитель заявлял в 2020-м году о том, что стремится заполучить 10% рынка серверов и снова достигнуть высот 2006 года, когда на долю Opteron приходилось 25% рынка.
Можно, конечно, сетовать на отсутствие конкуренции, но что можно изменить, чтобы положить конец создавшемуся положению вещей? Недавно Apple сделала громкое заявление о том, что она отказывается от процессоров на базе Intel и переводит всё своё железо на CPU собственной разработки. Речь идёт не только о лэптопах и низкопроизводительных iMacs, но даже о высокопроизводительных рабочих станциях на базе Intel Xeon. Планы, конечно, грандиозные, но как она собирается воплощать их в жизнь?

Старые добрые времена

Итак, Apple не собирается строить будущие процессоры на базе архитектуры x86. Ни для кого не секрет, что iPhone очень популярны. Кроме того, их причисляют к самым быстрым смартфонам на рынке мобильных устройств. При желании компания могла бы задействовать любую микроархитектуру своих мобильных CPU для разработки настольных систем. Опять же, кто проектировал эти процессоры? Сама Apple, используя архитектуру набора команд (ISA) по лицензии ARM.

Для справки: микроархитектура ARM использует сокращённый набор команд (RISC), в то время как микроархитектура x86 использует полный набор команд (CISC).

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

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

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

Вступайте в ARMию

Далее мы проследим за тем, как архитектура ARM развивались с течением времени.

ARM это компания, которая разрабатывает спецификации АСК (архитектуры системы команд) процессоров ARM и улучшает их с помощью новых технологий. В их число входит особый дизайн ядер big.LITTLE, набор инструкций NEON SIMD и усовершенствованные математические сопроцессоры. Как правило, каждому новому семейству CPU производитель присваивает своё имя. Так у Apple оно впервые появилось с выходом линейки процессоров ARM11 с 32-битной архитектурой. Впоследствии разработчики Яблока создали собственную микроархитектуру на базе обновлений 64-битной ARMv8.

Результаты тестов в Geekbench мобильных процессоров ARMРезультаты тестов в Geekbench мобильных процессоров ARM

Первые iPhone работали на SoC от Samsung. Эта система на кристалле была построена на базе процессоров ARM11 с архитектурой ARMv6 2002-го года выпуска. Разработка и реализация iPhone осуществлялась в соответствии с нуждами, бытовавшими на заре появления смартфонов. В них была представлена SIMD (вычислительная система с одиночным потоком команд и множественным потоком данных) для считывания MPEG-файлов, увеличенный кэш (всего лишь 32К) и восьмиступенчатый конвейер. Так как функции изменения очерёдности команд и предсказания ветвлений были ограничены, то производительность первых iPhone не иначе как слабой не назовёшь.
iPhone 3GS стал первым удобным для пользования смартфоном от Apple (с точки зрения программных функций). Он всё ещё работал на SoC от Samsung, но теперь имел в составе улучшенное ядро Cortex-A8. Результаты испытаний показали увеличение скорости на 107% запишите это на счёт суперскалярного 13-ступенчатого конвейера и 10-ступенчатого конвейера NEON SIMD для ускорения медиаприложений. Помимо удвоенного кэша L1 в 3GS был впервые представлен кэш L2 на 256K, а также встроенный сопроцессор. Двигаясь по пути наименьшего сопротивления, ARM и Apple без труда оптимизировали CPU на ранних стадиях, что привело к увеличению скорости процессоров работы в два раза.
Apple A4 стала первой SoC собственной разработки Apple. Она дебютировала на оригинальном iPad с частотой 1 ГГц, однако позже использовалась в iPhone 4 при частоте 800 МГц. Если бы у Яблока была своя модель развития микропроцессоров Тик-так, как у Intel, то это была бы стадия так. Построенная на базе прежней архитектуры Cortex-A8 и того же 45-нм техпроцесса от Samsung она предлагала значительные улучшения за счёт увеличения частоты и удвоения кэша L2 до 512К и шины памяти до 64 бит.
Вместе с iPad 2 компания Apple представила свою принципиально новую однокристальную систему Apple A5 с частотой 1 ГГц. Позже та же SoC была заявлена в iPhone 4S, только работала она при 800 МГц. Выпуск Apple A5 стал знаковым событием для Яблока: теперь его ядра имели обновлённый дизайн Cortex-A9, а сам процессор стал двухъядерным. 45-нм техпроцесс от Samsung и тактовая частота остались прежними, зато быстродействие памяти выросло до 400 МГц, а кэш L2 снова удвоился до 1 Мб. В Cortex-A9 также были представлены ключевые улучшения: 8-ступенчатый конвейер с упреждающим считыванием, способный выполнять команды с изменением их последовательности, улучшенный NEON SIMD и математический сопроцессор с увеличенной вдвое скоростью.

Релиз A6 случился тогда, когда Apple начала брать под контроль разработку своих смартфонов и внедрять собственные дизайнерские идеи в ARMv7. Apple A6 были последними процессорами от Apple, построенными на 32-битной архитектуре. И хотя кэши L1 и L2 были те же, что и у А5, техпроцесс уменьшился до 32 нм, а тактовая частота выросла до 1,3 ГГц. Благодаря грамотным решениям в архитектуре производительность значительно увеличилась, потребление энергии же сократилось.

Судя по всему, A6 построен на ядре Cortex-A9, однако в нём использованы компоненты улучшенного чипа Cortex-A15, включая тогда ещё новые v4 FPU и Advanced SIMD v2. Анализ показывает, что в него было включено 5 функциональных модулей (2 арифметико-логических устройства (АЛУ), 2 математических сопроцессора/набора инструкций NEON и 1 модуль загрузки/сохранения). И вот этот значительно улучшенный FPU, оптимизированный кэш, специально выделенный модуль для загрузки/сохранения всё это привело к тому, что производительность памяти увеличилась втрое, а быстродействие вдвое.

С этого времени дела у производителя пошли в гору, а его А7 и вовсе совершил прорыв, став первым 64-битным процессором, в то время как остальные производители отстали с его выпуском на год. Благодаря архитектуре ARMv8-А на базе 28-нм техпроцесса от Samsung Apple добавила кэш L3 на 4 Мб, удвоила кэш L2 до 1 Мб и L1 до 128 Кб. Фактически Apple удвоила разрядность за счёт 4-х АЛУ, 2-х модулей для загрузки/сохранения, 2-х блоков передачи управления, 3-х модулей FPU/NEON. А7 достиг отметки 1млрд. транзисторов, а производительность его увеличилась на 33% по сравнению с А6. В то время Geekbench 2, изначально предназначенный только для замера производительности 32-битных систем, начал устаревать. Результаты же тестов в Geekbench 3 показали, что ядра А7 Cyclone превзошли своих конкурентов в два раза!

64-битное ядро Cyclone64-битное ядро Cyclone

Apple А8 остаётся под вопросом. Похоже, в то время Apple уделяла больше внимания графическому ускорителю. Тогда же она разработала собственный пользовательский шейдер и, видимо, тогда же производитель начал переходить на новый 20-нм техпроцесс от TSMC. Схожая ситуация и с выпуском Apple А9, однако благодаря внедрению 20-нм техпроцесса от TSMC и 14-нм техпроцесса от Samsung тактовая частота процессора выросла до 1,8 ГГц, а L2 увеличился втрое до 3 Мб.
Появлению Apple А10 предшествовали два больших сдвига: внедрение технологии big.LITTLE от ARM, использующей высоко- и маломощные ядра для сбалансированного энергопотребления, и переход на уменьшенный до 16-нм техпроцесс от TSMC. Лёгкой победой стало увеличение частоты до 2,3 ГГц, которого удалось добиться посредством двух маломощных ядер Zephyr. Они работали на частоте 1 ГГц и тем самым использовали лишь 20% мощности больших ядер. Тогда же состоялся переход на новую микроархитектуру ARMv8.1-A, по сути являвшейся корректировочной версией прежней микроархитектуры. Эта система на кристалле от Apple была последней, проходившей тестирование в Geekbench 2. Результаты испытаний показывали лишь увеличение тактовой частоты, в то время как в новых версиях Geekbench появились замеры производительности графического ускорителя. И, согласно этим результатам, скорость работы элементов GPU от Apple стабильно росла.

В Apple A11 были представлены 2 крупных (Monsoon) и 4 малых (Mistral) вычислительных ядра, причём последние были построены на базе ядер Apple A6 Swift. В отличие от A10, малые ядра могли работать независимо от крупных ядер. Крупные ядра значительно улучшились: теперь они могли декодировать до 7 инструкций за такт вместо прежних 6. В то же время число блоков ALU увеличилось на две единицы, и теперь общее их количество достигло 6.

Спроектированные в 2012-м году Apple Swift стали большим шагом вперёдСпроектированные в 2012-м году Apple Swift стали большим шагом вперёд

A12 стал ещё одним шагом вперёд для Apple он был первым доступным широкому кругу пользователей 7-нанометровым чипом. A12 сильно изменился в плане организации кэша, что в свою очередь способствовало уменьшению времени отклика и увеличению пропускной способности. Кэш L3 был изъят в пользу системного кэша L2 на 8 Мб, а L1 был удвоен до 256 К. Чип содержал 2 крупных высокопроизводительных ядра Vortex и 4 малых энергоэффективных ядра Tempest на базе Apple A6 Swift. Крупные ядра имели однопоточный быстрый режим до 2,5 ГГц. Микроархитектуры A11 и A12 были очень мощными, даже для десктопных процессоров.

Разработчики текущей модели A13 продолжили делать ставку на систему кэша. System Level Cache получила аж 16 Мб на обслуживание SoC. У малых ядер (Thunder) имеется 4Мб кэша L2, у крупных (Lightning) 8Мб. В целом дизайн A13 похож на коммуникационный процессор с шириной декодирования 7 и улучшенным множителем.
Apple, несомненно, поборется с Intel за рынок настольных ПК. Дизайн CPU, как и всего ПК, у Яблока в целом хорошо проработан. Однако важно помнить, что положение Apple отлично от остальных лицензиатов ARM. Apple проектирует чипы с тем расчётом, чтобы продавать их в продуктах по премиальной цене. Наверняка в её гаджетах будут мощные батареи и прочие свистелки-дуделки, а владелец будет знать, что его вложения окупятся.

Однако для сторонних производителей такая модель ведения бизнеса просто невозможна. Взять хотя бы AMD: ранее она не могла конкурировать с Intel, и с трудом делает это сейчас. Так собирается ли производитель процессоров на базе ARM отнять рынок ПК (или даже лэптопов) у Intel и АMD? Нет. Цены на рынке десктопов демократичны, а потребление электроэнергии не представляет проблемы. И потому зацепиться на нём тому, кто производит продукцию на базе ARM, будет непросто.
Вот где системы ARM могут действительно составить конкуренцию x86-й архитектуре, так это в мобильном секторе. Взять хотя бы Lenovo Flex 5G, который работает на однокристальной системе Snapdragon 8cх. Мы не будем приводить всех характеристик самого SoC, лишь упомянем, что он построен на микроархитектуре Cortex-A76, имеет 3 АЛУ, 2 модуля FPU/SIMD, 2 модуля загрузки/хранения, блок передачи управления. Безусловно, такие характеристики указывают на высокую производительность чипа, однако это лишь часть того, что Apple вкладывает в свой CPU следующего поколения. Четырёхъядерный Snapdragon набрал 716 баллов в однопоточных тестах. Это меньше половины того, что показал Apple A13.

И пока Intel снова косячит со своим технологическим процессом, Apple по крайней мере удаётся грамотно проектировать чипы и за счёт этого увеличивать их производительность: лицензированные ядра ARM намереваются бросить вызов Intel Core i5. В то время как AMD выжимает все соки из рабочих станций, ARM завоёвывает позиции на прибыльном HPC (вычислениях на суперкомпьютерах) и на серверном поприще. Очевидно, Intel вытесняют по всем фронтам.



Подробнее..
Категории: Apple , Процессоры , Arm , Смартфоны , Cpu

Сравнение криптографической производительности популярных ARM-процессоров для DYI и Edge-устройств, плюс Xeon E-2224

13.04.2021 10:07:06 | Автор: admin

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

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

На тесте были:

  • Nvidia Jetson Nano - 4 core ARM A57 @ 1.43 GHz

  • Raspberry Pi 4, Model B - Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz

  • Raspberry Pi 3, Model B+ - Broadcom BCM2837B0, Cortex-A53 (ARMv8) 64-bit SoC @ 1.4GHz

  • Orange Pi Zero LTS - AllWinner H2 Quad-coreCortex-A7

и, чтобы им не скучно было, к тестированию подключил Intel Xeon E-2224, дабы возникло понимание в сравнительных возможностях ARM vs Intel.

На Jetson Nano установлено активное охлаждение с помощью вентилятора, на других платформах просто радиатор.

Все процессоры 4х-ядерные, SMT нигде нет. Сравнение производилось в рамках однопоточного теста с помощью стандартных возможностей OpenSSL (OpenSSL 1.1.1 11 Sep 2018).

Тест алгоритмов семейства SHA

openssl speed sha

Победителем теста среди ARM оказался Nvidia Jetson Nano, причем для sha-256/16KB он обогнал даже Xeon E-2224 - я повторно провел данный бенчмарк для Xeon E-2224, результат остался тем же.

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

Nvidia Jetson Nano

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1             73350.09k   200777.04k   416181.08k   573274.72k   645373.43k   653034.35ksha256           68908.76k   188685.90k   412290.48k   568202.87k   644962.46k   651681.85ksha512           19732.45k    78505.91k   122326.65k   175421.47k   201366.51k   202794.47k

Raspberry Pi 4

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1             40358.89k   103684.42k   199123.37k   258472.96k   283866.45k   285665.96ksha256           27360.34k    65673.69k   120294.66k   151455.74k   164413.44k   165281.79ksha512           10255.33k    40882.35k    60587.95k    83416.41k    94066.01k    94874.28k

Raspberry Pi 3

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1             19338.74k    52534.42k   105558.02k   140777.13k   156311.55k   157537.62ksha256           12821.65k    31949.78k    59951.62k    77581.99k    84858.20k    85415.25ksha512            7444.83k    29450.71k    47035.65k    66549.76k    75893.42k    76660.74k

Orange Pi Zero

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1              9313.16k    23691.09k    45304.83k    58655.40k    64249.86k    64684.03ksha256            6051.17k    14204.69k    25856.60k    32542.38k    35198.29k    35400.36ksha512            3319.25k    13320.17k    19863.55k    27670.87k    31290.71k    31582.89k

Вне конкурса: Intel Xeon E-2224

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1            171568.52k   420538.79k   843694.68k  1124105.90k  1259615.57k  1257401.00ksha256          101953.18k   231621.03k   427492.44k   534554.28k   575944.02k   582303.74ksha512           69861.21k   279030.78k   493514.41k   732609.88k   855792.76k   864578.22k

Тест алгоритмов семейства AES

openssl speed aes

Победителем теста среди ARM оказался Raspberry Pi 4, отставание от Xeon E-2224 более чем в 2 раза, следом расположился Nvidia Jetson Nano.

Raspberry Pi 4

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      74232.10k    80724.61k    84387.02k    85057.54k    85314.22k    85196.80kaes-192 cbc      66069.32k    70589.59k    72967.00k    73584.64k    73766.23k    73667.93kaes-256 cbc      58926.68k    62458.30k    64351.40k    64619.16k    64976.21k    64913.41k

Nvidia Jetson Nano

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      64590.62k    68711.06k    71231.36k    71509.33k    71963.57k    71401.47kaes-192 cbc      55971.60k    59210.12k    60951.72k    61140.65k    61300.74k    61289.31kaes-256 cbc      49413.88k    51999.08k    53115.39k    53581.57k    53518.34k    53513.76k

Raspberry Pi 3

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      37401.48k    45102.98k    47455.83k    48064.51k    48130.73k    48119.81kaes-192 cbc      33444.87k    38794.88k    40544.51k    40930.30k    41047.38k    41036.46kaes-256 cbc      30299.70k    34635.65k    36142.58k    36331.52k    36424.36k    36427.09k

Orange Pi Zero

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      22108.15k    24956.95k    25791.06k    26007.89k    26069.67k    26072.41kaes-192 cbc      19264.22k    21327.66k    21971.29k    22138.88k    22186.67k    22189.40kaes-256 cbc      17211.09k    18887.40k    19399.34k    19532.12k    19570.69k    19573.42k

Вне конкурса: Intel Xeon E-2224

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc     173352.23k   196197.33k   201970.86k   203862.70k   204595.20k   203975.34kaes-192 cbc     149237.38k   164843.65k   167562.58k   168944.98k   169667.24k   169056.58kaes-256 cbc     130430.20k   141325.91k   143808.17k   144901.46k   145601.88k   145424.38k

Выводы

Делать выводы о том, насколько хорошо, согласно результатам бенчмарка OpenSSL, будет работать тот или иной CPU на реальной невычислительной задаче, конечно, нельзя. Однако, с точки зрения производительности в рамках задач, завязанных на TLS, можно сказать, что чипы, использованные в Raspberry Pi 4 и Jetson Nano, обладая низкой стоимостью, позволяют обеспечить достойную производительность: в расчете на 1 рубль, вероятно, непринужденно побеждают Xeon E-2224.

Надеюсь, что было полезно.

Подробнее..

Переворот на инфраструктурном рынке ARM против Intel

19.04.2021 12:12:03 | Автор: admin

99,9 % всех серверов на рынке построены на базе Intel. Поэтому, говоря про серверное железо, мы невольно подразумеваем технику на базе Intel, как в свое время под ксероксом подразумевали копировальный аппарат от единственного производителя этих устройств.

С годами на рынке устройств печати монополия была разрушена появились сильные игроки, потеснившие Xerox. А что же с рынком серверного оборудования? До недавнего времени многие считали, что Intel продержится у руля еще 10лет, и предпосылок к переменам не было.

Тем не менее начались изменения: ARM-процессоры, всегда применявшиеся для смартфонов и потребительской техники, вторглись на территорию Intel, и довольно успешно. Облачные провайдеры начали вкладывать деньги, чтобы создать свои решения на этой платформе и на их основе предлагать услуги и вычислительные ресурсы в аренду. А компания Apple, которая много лет производила ноутбуки и ПК исключительно на Intel, внезапно выпустила новое поколение устройств на ARM-платформе, добившись потрясающей производительности.

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

Темы круглого стола:

  • Почему процессоры ARM внезапно стали такими быстрыми?

  • Какой софт нужен новым процессорам, и где его взять?

  • Зачем компаниям нужна закупка подобного железа?

  • Как прошли первые тесты ARM? Как процессоры показывают себя в продуктивной эксплуатации?

Кому будет интересно?

  • руководителям направления ИТ-инфраструктуры;

  • CIO компаний различных отраслей;

  • старшему техническому составу ИТ-подразделений.

Модератор: Илья Воронин, руководитель Центра проектирования вычислительных комплексов Инфосистемы Джет

Спикеры:

  • Павел Романченко, технический директор Центра инноваций Инфосистемы Джет;

  • Александр Голуб, технический директор Т-Платформы;

  • Василий Гладышев, руководитель департаментаИТ эксплуатации QIWI.

Регистрация по ссылке

Подробнее..

Assembler Editor Plus Установка

17.02.2021 20:19:33 | Автор: admin

Продолжение цикла статей.

Предыдущая статья: Редактор ассемблера для ARM микроконтроллеров для компилятора gnu as. Старт

Итак, вы все таки решились скачать и попробовать, что для этого нужно сделать:

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

Для тех же кому нужен лайт вариант уже есть варианты:

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

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

    - в группе ВК

  1. ну или кому то может быть проще скачать с того же Телеграмма (вот только узнал что например на/в Украине такие сервисы как Яндекс и ВК не доступны)

Выбирайте кому что больше нравится

во всех вышеуказанных случаях у вас должны быть установлены драйвера на ваш программатор, в редакторе сделана реализация для ST-Link (у меня китаец V2, c SWD)

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

Вариант Easy - скачать с Яндекс.Диск полный пакет, с различными допами в виде драйверов, программы редактирования шрифтов, доками на некоторые микроконтроллеры и отладочные платы. (внимание размер 130 мб)

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

Описание папок и файлов:

bin - папка программ компиляции, у меня это gnu as из пакета arm-none-eabi

inf - файлы настроек для микроконтроллеров, меню редактора и т.д.

openocd - сервер отладки

tmp - папка временных файлов редактора

AsmEdit.exe - запускаемый файл

asmedit.ini - базовые настройки редактора

new - да удалите его, затесался, и является лишним

Для Easy варианта, действия те же самые, скачать и распаковать куда вам удобно, но путь должен быть без пробелов, содержимое будет чуть больше

О папке AsmEdit сказано выше

В папке Add находятся:

  • в install: инсталляторы для ST-Link, и программа установки dll для J-Link (если кто использует именно его, не спешите с его установкой!)

  • в MCUDoc: различные справочные файлы, какие то книги скаченные с интернета, описания плат разработки, даташиты на некоторые MCU которые находятся в работе и т.д.

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

Запуск редактора

Ну экзешник один, так что запускаем

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

Нажатием кнопки "Задать" найдите и укажите файл ST-Link_CLI.exe на своем компьютере, на скриншоте настройки расположения файла по умолчанию при установке драйвера ST-Link из папки Add\Install\ST-Link в Easy варианте редактора

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

Настройки OpenOCD

Если кто внимательно читал, то сервер openOCD идет вместе с редактором (каталог openocd в папке редактора), так что вы можете использовать его, или же указать расположение уже установленного у вас сервера

  • Если желаете использовать сервер установленный с редактором, то можно выбрать версию для 32ух или 64ех битных систем

  • при использовании J-Link настройки нужно указать как это указано на скриншоте выше

  • при использовании ST-Link V2, нужно модифицировать настройки следующим образом

если поставить чек бокс "Использовать OpenOCD для записи прошивки в устройство", то прошивка устройства будет происходить так же силами OpenOCD, это было сделано для J-Link, но будет работать и с ST-Link, однако прошивка при помощи программы ST-Link_CLI будет происходить быстрее (см предыдущий шаг настройки), поэтому я рекомендую при наличии программатора ST-Link этот чекбокс не устанавливать

Настройки редактора

Старался их группировать по смыслу, получилось пока не очень, поэтому опишу немного

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

Параметры визуализации текста в редакторе

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

Настройки компилятора осуществляются в разрезе проекта, поэтому о них расскажу позже

В меню Справка есть некоторые дополнительные инфобоксы

По идее будет дополнятся по мере расширения редактора.

Подробнее..

Assembler Editor Plus Первый проект

18.02.2021 16:06:37 | Автор: admin

Продолжение цикла статей про редактор ассемблера для ARM микроконтроллеров под компилятор GNU AS

Предыдущая статья Assembler Editor Plus: Установка

Картинки под катом !

Итак, мы дошли до создания первого проекта.

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

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

Сейчас активно описываются микроконтроллеры семейства STM32F4x, основным для тестирования является STM32F407, поэтому первый пример будет именно под этот микроконтроллер, он задействован в отладочной плате STM32F4 Discovery от ST, у меня этот микроконтроллер задействован на отладочной плате Open407I-C (документация есть в папке Add\MCUDoc в Easy варианте редактора для скачивания, см. предыдущую статью), или STM32F4VE (китайская платка, так же с алиэкспресс)

Итак, запускаем редактор, и выбираем "Проект" - "Новый" и после указания папки и имени для хранения файлов проекта будет показано окно настроек проекта

В этом окне нужно задать используемый в проекте микроконтроллер, нажимаем "Задать", и выбираем STM32F407

Настройки микроконтроллера будут прочитаны и поля настроек проекта будут заполнены:

Поля .syntax .cpu .thumb .fpu - это параметры компиляции для выбранного микроконтроллера,

Адрес прошивки - куда будет записываться прошивка

на вкладке Конфигурация, настройки для самого редактора, вам вряд ли придется из изменять самостоятельно даже в будущем

Далее переходим во вкладку Каталоги

Здесь указываются папки которые будут созданы в папке проекта, и если приведенные в окне настроек значения по умолчанию вас устраивают то необходимо просто нажать кнопку "Создать каталоги проекта" и так же можно скопировать исполняемые файлы компилятора с .bat файлом для создания возможности само-компиляции проекта не используя возможности редактора.

После этого нажимаем кнопку "Принять". Настройки проекта применятся и вы увидите следующее состояние редактора

По интерфейсу:

[ 1 ] - список открытых в редакторе файлов

[ 2 ] - выбор режима отображения файлов проекта

[ 3 ] - окно показа файлов в: Каталоге проекта, Каталоге исходных файлов, Каталоге компиляции

[ 4 ] - поле редактора

[ 5 ] - кнопки быстрого доступа к дополнительным функциям

Теперь не углубляясь дальше в подробности создадим тестовую программу

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

и видим все доступные шаблоны кода, нам интересен раздел "Стартовые файлы (main.asm)"

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

Теперь чтобы откомпилировать проект идем в меню "Запуск" - "Настройки компиляции"

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

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

теперь вы можете откомпилировать проект "Запуск" - "Компиляция и сборка"

Теперь в каталоге компиляции проекта \compile можно посмотреть результирующие файлы, например текст прошивки находится в файле sys.sasm, прошивка соответственно в sys.bin или sys.hex (выбирайте в зависимости от своего программатора, ну а в редакторе вам об этом думать особо не придется)

Теперь самое время подключить ваше устройство через программатор и записать прошивку в устройство, выбираем "Запуск" - "Запись в устройство через..." если у вас ST-Link - то ST-Link [direct] - это прошивка при помощи утилит программатора от ST (смотрите прошлую статью), ну а если у вас другой программатор (J-Link) то запись делаем через OpenOCD (но там я еще эксперементирую)

Поскольку в нашей программе визуальных эффектов никаких не предусмотрено, то просто запустим отладку и посмотрим как это все происходит

Для этого выбираем "Запуск" - "Исполнение и отладка [OpenOCD]"

В появившемся окне нажимаем "Старт" и через несколько секунд видим что наш микроконтроллер работает (Running), поэтому нет значений регистров и состояние не известно

Давайте сбросим микроконтроллер и перейдем в режим пошагового исполнения кода, нажмите кнопки: "HALT" (принудительный останов микроконтроллера), "RESET HALT" (сброс микроконтроллера и переход в режим пошагового исполнения программы) в окне отладчика

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

Теперь вы можете исполнять программу по шагам, для этого есть две кнопки "STEP IN" (выполнение текущей команды с заходом в подпрограммы) и "STEP OVER" исполнение подпрограмм за один шаг.

Поскольку у нас программа фактически и состоит из одной подпрограммы то нажмем "STEP IN" и получим переход на адрес 0х08000048, подпрограмму с меткой SYSCLK168_START

И вот так можно шагать по программе и смотреть как меняются значения регистров и флагов.

Дополнительно в окне отладчика вы можете просмотреть текущее состояние регистров настройки периферии микроконтроллера

При двойном клике мышкой на поле регистра его значение будет показано в различных форматах, порядок BIN -> HEX -> DEC

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

Для того чтобы продолжить исполнение программы нажимаем кнопку "RESUME"

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

Подробнее..

Assembler Editor Plus Добавление нового микроконтроллера

21.02.2021 12:18:25 | Автор: admin

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

Перечень микроконтроллеров находится в файле inf\mculist.ini

Открыть его для редактирования можно из самого редактора

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

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

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

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

Теперь нужно описать конкретный микроконтроллер из нашего списка, по запросу одного из участников проекта (Сергей привет!), будем описывать 1921ВК035

Сначала создадим файловую структуру для хранения настроек новых микроконтроллеров в каталоге inf/ редактора, у меня получилось так

Дополнительно нам понадобятся следующие файлы

  • Для OpenOCD нужны target файлы под выбранные микроконтроллера, их можно "утащить" из настроек OpenOCD сервера сред предлагаемых производителем/разработчиком микроконтроллера и используемых для отладочного сервера, эти файлы нужно скопировать в папку openocd\scripts\target

  • Так же для интерфейса отладчика нужен .svd файл микроконтроллера, по которому редактор будет показывать состояние регистров настройки микроконтроллера, эти файлы так же можно найти в ПО от производителя/разработчика микроконтроллера или достать из из другой среды разработки, эти файлы я помещаю в папке микроконтроллера

Теперь опять вернемся к нашему настроечному файлу списка микроконтроллеров mculist.ini и пропишем и разберем настройки микроконтроллера

  • type=config указатель на то что описывается микроконтроллер, если этой строчки нет, то содержимое секции ini файла будет рассматриваться как названия субкатегорий микроконтроллеров

  • file=inf\niiet\1921VKx\1921VK035\k1921vk035.ini - файл описания модулей микроконтроллера, пока мы его не создали, сделаем это ниже

  • openocd=openocd\scripts\target\k1921vk035.cfg расположение target файла для openOCD

  • deviceinfo=inf\NIIET\1921VKx\1921VK035\K1921VK035.svd описатель микроконтроллера для отладчика редактора

  • targetadr=0x00000000 - адрес для размещения прошивки микроконтроллера, это значение нужно искать в документации по микроконтроллеру

  • syntax=unified - формат написания команд ассемблера, это опция компилятора которая указывается в каждом .asm файле проекта

  • cpu=cortex-m4 указание на ядро микроконтроллера, так же смотрим в документации по микроконтроллеру, спрашиваем на форумах и так далее, эта опция нужна и для компилятора, и для редактора, так на основании этого значения выбираются правила подсветки команд ассемблера в редакторе

  • thumb=.thumb указание компилятору на размер используемых команд

  • fpu= - указание на сопроцессор для операций с плавающей точкой (эту информацию я еще не нашел, оставим не заполненным)

Теперь можно переходить к созданию файла описания модулей микроконтроллера

Общий шаблон выглядит следующим образом

Сначала идет имя секции с именем микроконтроллера [К1921ВК035] и дальше идут пары параметров:

  • textX - текст показываемый в дереве списка модулей

  • linkX - ссылка на поддерево

Нумерация пар textX / linkX должна быть последовательна ! то есть сначала описываем нулевые элементы, потом первые, вторые и так далее... Если описать нулевой элемент, а потом сразу второй - обработки не произойдет!!

При необходимости запуска скрипта установки модуля в проект прописываются следующие параметры

  • textX - текст показываемый в дереве списка модулей

  • scriptX - указатель на файл скрипта модуля

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

Подробнее..

USB на регистрах STM32L1 STM32F1

21.03.2021 16:08:22 | Автор: admin

Еще более низкий уровень (avr-vusb)
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: interrupt endpoint на примере HID
USB на регистрах: isochronous endpoint на примере Audio device

С программным USB на примере AVR мы уже познакомились, пришла пора взяться за более тяжелые камни stm32. Подопытными у нас будут классический STM32F103C8T6 а также представитель малопотребляющей серии STM32L151RCT6. Как и раньше, пользоваться покупными отладочными платами и HAL'ом не будем, отдав предпочтение велосипеду.

Раз уж в заглавии указано два контроллера, стоит рассказать об основных отличиях. В первую очередь это резистор подтяжки, говорящий usb-хосту, что в него что-то воткнули. В L151 он встроен и управляется битом SYSCFG_PMC_USB_PU, а в F103 нет, придется впаивать на плату снаружи и соединять либо с VCC, либо с ножкой контроллера. В моем случае под руку попалась ножка PA10. На которой висит UART1 А другой вывод UART1 конфликтует с кнопкой замечательную плату я развел, не находите? Второе отличие это объем флеш-памяти: в F103 ее 64 кБ, а в L151 целых 256 кБ, чем мы когда-нибудь и воспользуемся при изучении конечных точек типа Bulk. Также у них немного отличаются настройки тактирования, да и лампочками с кнопочками могут на разных ногах висеть, но это уже совсем мелочи. Пример для F103 доступен в репозитории, так что адаптировать под него остальные эксперименты с L151 будет несложно. Исходные коды доступны тут: github.com/COKPOWEHEU/usb

Общий принцип работы с USB


Работа с USB в данном контроллере предполагается с использованием аппаратного модуля. То есть мы ему говорим что делать, он делает и в конце дергает прерывание я готовое!. Соответственно, из основного main'а нам вызывать почти ничего не надо (хотя функцию usb_class_poll я на всякий случай предусмотрел). Обычный цикл работы ограничен единственным событием обмен данными. Остальные сброс, сон и прочие события исключительные, разовые.

В низкоуровневые подробности обмена я на этот раз углубляться не буду. Кому интересно, может почитать про vusb. Но напомню, что обмен обычными данными идет не по одному байту, а по пакету, причем направление передачи задает хост. И названия этих направлений диктует тоже он: IN передача означает что хост принимает данные (а устройство передает), а OUT что хост передает данные (а мы принимаем). Более того, каждый пакет имеет свой адрес номер конечной точки, с которой хост хочет общаться. Пока что у нас будет единственная конечная точка 0, отвечающая за устройство в целом (для краткости я еще буду называть ее ep0). Для чего нужны остальные я расскажу в других статьях. Согласно стандарту, размер ep0 составляет строго 8 байт для низкоскоростных устройств (к каковым относится все тот же vusb) и на выбор 8, 16, 32, 64 байта для полноскоростных вроде нашего.

А если данных слишком мало и они не заполняют буфер полностью? Тут все просто: помимо данных в пакете передается и их размер (это может быть поле wLength либо низкоуровневая комбинация сигналов SE0, обозначающая конец передачи), так что даже если нам надо через ep0 размером 64 байта передать три байта, то переданы будут именно три байта. В результате расходовать пропускную способность, гоняя ненужные нули, мы не будем. Так что не стоит мельчить: если можем себе позволить потратить 64 байта, тратим не раздумывая. Помимо прочего это несколько уменьшит загруженность шины, ведь передать за раз кусок в 64 байта (плюс все заголовки и хвосты) проще, чем 8 раз по 8 байт (к каждому из которых опять-таки заголовки и хвосты).

А если данных напротив слишком много? Тут сложнее. Данные приходится разбивать по размеру конечной точки и передавать порциями. Скажем, размер ep0 у нас 8 байт, а передать хост пытается 20 байт. При первом прерывании к нам придут байты 0-7, во втором 8-15, в третьем 16-20. То есть чтобы собрать посылку целиком нужно получить целых три прерывания. Для этого в том же HAL придуман хитрый буфер, с которым я попытался разобраться, но после четвертого уровня пересылки одного и того же между функциями, плюнул. Как результат, в моей реализации буферизация ложится на плечи программиста.

Но хост хотя бы всегда говорит сколько данных пытается передать. Когда же данные передаем мы, надо как-то хитро дернуть низкоуровневые состояния ножек чтобы дать понять что данные закончились. Точнее, дать понять модулю usb, что данные закончились и что надо дернуть ножки. Делается это вполне очевидным способом записью только части буфера. Скажем, если буфер у нас 8 байт, а мы записали 4, то очевидно, данных у нас всего 4 байта, после которых модуль пошлет волшебную комбинацию SE0 и все будут довольны. А если мы записали 8 байт, это значит что у нас всего 8 байт или что это только часть данных, которая влезла в буфер? Модуль usb считает что часть. Поэтому если мы хотим остановить передачу, то после записи 8-байтного буфера должны записать следом 0-байтный. Это называется ZLP, Zero Length Packet. Про то, как это выглядит в коде, я расскажу чуть позже.

Организация памяти


Согласно стандарту, размер конечной точки 0 может достигать 64 байт. Размер любой другой аж 1024 байт. Количество точек также может отличаться от устройства к устройству. Те же STM32L1 поддерживают до 7 точек на вход и 7 на выход (не считая ep0), то есть до 14 кБ одних только буферов. Которые в таком объеме скорее всего никому никогда не понадобятся. Непозволительный расход памяти! Вместо этого модуль usb отгрызает себе кусок общей памяти ядра и пользуется ей. Эта область называется PMA (packet memory area) и начинается с USB_PMAADDR. А чтобы указать где внутри нее располагаются буферы каждой конечной точки, в начале выделен массив на 8 элементов каждый со следующей структурой, и только потом собственно область для данных:
typedef struct{    volatile uint32_t usb_tx_addr;    volatile uint32_t usb_tx_count;    volatile uint32_t usb_rx_addr;    volatile union{      uint32_t usb_rx_count;      struct{        uint32_t rx_count:10;        uint32_t rx_num_blocks:5;        uint32_t rx_blocksize:1;      };    };}usb_epdata_t;


Здесь задаются начало буфера передачи, его размер, потом начало буфера приема и его размер. Обратите внимание во-первых, что usb_tx_count задает не собственно размер буфера, а количество данных для передачи. То есть наш код должен записать по адресу usb_tx_addr данные, потом записать в usb_tx_count их размер и только потом дернуть регистр модуля usb что данные мол записаны, передавай. Еще большее внимание обратите внимание на странный формат размера буфера приема: он представляет собой структуру, в которой 10 бит rx_count отвечают за реальное количество прочитанных данных, а вот остальные уже действительно за размер буфера. Надо же железке знать докуда писать можно, а где начинаются чужие данные. Формат этой настройки тоже довольно интересный: флаг rx_block_size говорит в каких единицах задается размер. Если он сброшен в 0, то в 2-байтных словах, тогда размер буфера равен 2*rx_num_blocks, то есть от 0 до 62. А если выставлен в 1, то в 32-байтных блоках, соответственно размер буфера тогда оказывается 32*rx_num_blocks и лежит в диапазоне от 32 до 512 (да, не до 1024, такое вот ограничение контроллера).

Для размещения буферов в этой области будем использовать полудинамический подход. То есть выделять память по запросу, но не освобождать ее (еще не хватало malloc / free изобретать!). На начало неразмеченного пространства будет указывать переменная lastaddr, изначально указывающая на начало области PMA за вычетом таблицы структур, рассмотренной выше. Ну а при каждом вызове функции настройки очередной конечной точки usb_ep_init() она будет сдвигаться на указанный там размер буфера. И в соответствующую ячейку таблицы будет вносится нужное значение, естественно. Значение этой переменной сбрасывается по событию ресета, после чего тут же следует вызов usb_class_init(), в котором точки настраиваются заново в соответствии с юзерской задачей.

Работа с регистрами приема-передачи


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

Первые грабли начинаются при собственно работе с буфером: он организован не по 32 бита, как весь остальной контроллер, и не по 8 бит, как можно было ожидать. А по 16 бит! В результате запись и чтение в него осуществляются по 2 байта, выровненные по 4 байта. Спасибо, ST, что сделали такое извращение! Как бы скучно без этого жилось! Теперь обычным memcpy не обойтись, придется городить специальные функции. Кстати, если кто любит DMA, то оно такое преобразование делать вроде умеет самостоятельно, хотя я это не проверял.

И тут же вторые грабли с записью в регистры модуля. Дело в том, что за настройку каждой конечной точки за ее тип (control, bulk и т.д.) и состояние отвечает один регистр USB_EPnR, то есть просто так в нем бит не поменяешь, надо следить чтобы не попортить остальные. А во-вторых, в этом регистре присутствуют биты аж четырех типов! Одни доступны только на чтение (это замечательно), другие на чтение и запись (тоже нормально), третьи игнорируют запись 0, но при записи 1 меняют состояние на противоположное (начинается веселье), а четвертые напротив игнорируют запись 1, но запись 0 сбрасывает их в 0. Скажите мне, какой наркоман додумался в одном регистре сделать биты, игнорирующие 0 и игнорирующие 1?! Нет, я готов предположить что это сделано ради сохранения целостности регистра, когда к нему обращаются и из кода, и из железа. Но вам что, лень было поставить инвертор чтобы биты сбрасывались записью 1? Или в другом месте инвертор чтобы другие биты инвертировались записью 0? В результате выставление двух битов регистра выглядит так (еще раз спасибо ST за такое извращение):
#define ENDP_STAT_RX(num, stat) do{USB_EPx(num) = ((USB_EPx(num) & ~(USB_EP_DTOG_RX | USB_EP_DTOG_TX | USB_EPTX_STAT)) | USB_EP_CTR_RX | USB_EP_CTR_TX) ^ stat; }while(0)


Ах да, чуть не забыл: доступа к регистру по номеру у них тоже нет. То есть макросы USB_EP0R, USB_EP1R и т.д. у них есть, но вот если номер пришел в переменной, то увы. Пришлось изобретать свой USB_EPx() а что поделать.

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

Обработка IN и OUT запросов


Возникновение прерывания USB может сигнализировать о разных вещах, но сейчас мы сосредоточимся на запросах обмена данными. Флагом такого события будет бит USB_ISTR_CTR. Если увидели его, можем разбираться с какой точкой хочет общаться хост. Номер точки скрывается под битовой маской USB_ISTR_EP_ID, а направление IN или OUT под битами USB_EP_CTR_TX и USB_EP_CTR_RX соответственно.

Поскольку точек у нас может быть много, и каждая со своим алгоритмом обработки, заведем им всем callback-функции, которые бы вызывались по соответствующим событиям. Например, послал хост данные в endpoint3, мы прочитали USB->ISTR, вытащили оттуда что запрос у нас OUT и что номер точки равен 3. Вот и вызываем epfunc_out[3](3). Номер точки в скобках передается если вдруг юзерский код захочет повесить один обработчик на несколько точек. Ах да, еще в стандарте USB принято входные точки IN помечать взведенным 7-м битом. То есть endpoint3 на выход будет иметь номер 0x03, а на вход 0x83. Причем это разные точки, их можно использовать одновременно, друг другу они не мешают. Ну почти: в stm32 у них настройка типа (bulk, interrupt, ...) общая и на прием, и на передачу. Так что та же 0x83-я точка IN будет соответствовать callback'у epfunc_in[3](3 | 0x80).

Тот же принцип используется и для ep0. Разница только в том, что ее обработка происходит внутри библиотеки, а не внутри юзерского кода. Но что делать если нужно обрабатывать специфичные запросы вроде какого-нибудь HID не лезть же ковырять код библиотеки? Для этого предусмотрены специальные callback'и usb_class_ep0_out и usb_class_ep0_in, которые вызываются в специальных местах и имеют специальный формат, рассказывать про который я буду ближе к концу.

Стоит упомянуть еще про один не очень очевидный момент, связанный с возникновением прерываний обработки пакетов. С OUT запросами все просто: данные пришли, вот они. А вот IN прерывание генерируется не тогда, когда хост послал IN запрос, а когда в буфере передачи пусто. То есть по принципу действия это прерывание аналогично прерыванию по опустошению буфера UART. Следовательно, когда мы хотим что-то передать хосту, мы это просто записываем данные в буфер передачи, ждем прерывания IN и дописываем что не поместилось (не забываем про ZLP). И ладно еще с обычными endpoint'ами, ими программист управляет, можно пока не обращать внимание. Но вот через ep0 обмен идет всегда. Поэтому и работа с ней должна быть встроена в библиотеку.

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

Ну а сам обработчик устроен довольно просто: записывает очередной кусок данных в буфер передачи, сдвигает адрес начала буфера и уменьшает количество оставшихся для передачи байтов. Отдельный костыль связан с тем самым ZLP и необходимостью на некоторые запросы отвечать пустым пакетом. В данном случае конец передачи обозначается тем, что адрес данных стал NULL. А пустой пакет что он равен константе ZLPP. И то и другое происходит при равенстве размера нулю, так что реальной записи не происходит.

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

Логика общения по USB


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

Обработка SETUP запросов: DeviceDescriptor


Человек, хоть немного ковырявший USB, уже давно должен был насторожиться: COKPOWEHEU, ты говоришь про запросы IN и OUT, но ведь в стандарте прописан еще и SETUP. Да, так и есть, но это скорее разновидность OUT запроса, специально структурированная и предназначенная исключительно для конечной точки 0. Об ее структуре и особенностях работы и поговорим. Сама структура выглядит следующим образом:
typedef struct{  uint8_t bmRequestType;  uint8_t bRequest;  uint16_t wValue;  uint16_t wIndex;  uint16_t wLength;}config_pack_t;


Поля этой структуры рассмотрены во множестве источников, но все же напомню.
bmRequestType битовая маска, биты в которой означают следующее:
7: направление передачи. 0 от хоста к устройству, 1 от устройства к хосту. Фактически, это тип следующей передачи, OUT или IN.
6-5: класс запроса
0x00 (USB_REQ_STANDARD) стандартный (обрабатывать пока будем только их)
0x20 (USB_REQ_CLASS) специфичные для класса (до них дойдем в следующих статьях)
0x40 (USB_REQ_VENDOR) специфичные для производителя (надеюсь, не придется их трогать)
4-0: собеседник
0x00 (USB_REQ_DEVICE) устройство в целом
0x01 (USB_REQ_INTERFACE) отдельный интерфейс
0x02 (USB_REQ_ENDPOINT) конечная точка

bRequest собственно запрос
wValue небольшое 16-битное поле данных. На случай простых запросов, чтобы не гонять полноценные пересылки.
wIndex номер получателя. Например, интерфейса, с которым хост хочет пообщаться.
wLength размер дополнительных данных, если 16 бит wValue недостаточно.

Первым делом при подключении устройства хост пытается узнать что же именно в него воткнули. Для этого он посылает запрос со следующими данными:
bmRequestType = 0x80 (запрос на чтение) + USB_REQ_STANDARD (стандартный) + USB_REQ_DEVICE (к устройству в целом)
bRequest = 0x06 (GET_DESCRIPTOR) запрос дескриптора
wValue = 0x0100 (DEVICE_DESCRIPTOR) дескриптор устройства в целом
wIndex = 0 не используется
wLength = 0 дополнительных данных нет
После чего шлет запрос IN, куда устройство должно положить ответ. Как мы помним, запрос IN от хоста и прерывание контроллера слабо связаны, так что записывать ответ будем сразу в буфер передатчика ep0. Теоретически, данные из этого, да и всех прочих, дескрипторов привязаны к конкретному устройству, поэтому помещать их в ядро библиотеки бессмысленно. Соответствующие запросы передаются функции usb_class_get_std_descr, которая возвращает ядру указатель на начало данных и их размер. Дело в том, что некоторые дескрипторы могут быть переменного размера. Но DEVICE_DESCRIPTOR к ним не относится. Его размер и структура стандартизованы и выглядят так:
uint8_t bLength; //размер дескриптораuint8_t bDescriptorType; //тип дескриптора. В данном случае USB_DESCR_DEVICE (0x01)uint16_t bcdUSB; //число 0x0110 для usb-1.1, либо 0x0200 для 2.0. Других значений я не встречалuint8_t bDeviceClass; //класс устройстваuint8_t bDeviceSubClass; //подклассuint8_t bDeviceProtocol; //протоколuint8_t bMaxPacketSize0; //размер ep0uint16_t idVendor; // VIDuint16_t idProduct; // PIDuint16_t bcdDevice_Ver; //версия в BCD-форматеuint8_t iManufacturer; //номер строки названия производителяuint8_t iProduct; //номер строки названия продуктаuint8_t iSerialNumber; //номер строки версииuint8_t bNumConfigurations; //количество конфигураций (почти всегда равно 1)

В первую очередь обратите внимание на первые два поля размер дескриптора и его тип. Они характерны почти для всех дескрипторов USB (кроме HID, пожалуй). Причем если bDescriptorType это константа, то bLength приходится чуть ли не считать вручную для каждого дескриптора. В какой-то момент мне это надоело и был написан макрос
#define ARRLEN1(ign, x...) (1+sizeof((uint8_t[]){x})), x

Он считает размер переданных ему аргументов и подставляет вместо первого. Дело в том, что иногда дескрипторы бывают вложенными, так что один, скажем, требует размер в первом байте, другой в 3 и 4 (16-битное число), а третий в 6 и 7 (снова 16-битное число). Точные значения аргументов макросам безразличны, но хотя бы количество совпадать должно. Собственно, макросы для подстановки в 1, в 3 и 4, а также в 6 и 7 байты там тоже есть, но их применение я покажу на более характерном примере.
Пока же рассмотрим 16-битные поля вроде VID и PID. Понятное дело что в одном массиве смешать 8-битные и 16-битные константы не выйдет, да плюс endiannes в общем, на выручку снова приходят макросы: USB_U16( x ).

В плане выбора VID:PID вопрос сложный. Если планируется выпускать продукцию серийно, все же стоит купить персональную пару. Для личного же пользования можно подобрать чужую от похожего устройства. Скажем, у меня в примерах будут пары от AVR LUFA и STM. Все равно хост определяет по этой паре скорее специфичные баги реализации, чем назначение. Потому что назначение устройства подробно расписывается в специальном дескрипторе.

Внимание, грабли! Как оказалось, Windows привязывает к этой паре драйвера, то есть вы, например, собрали устройство HID, показали системе и установили драйвера. А потом перепрошили устройство под MSD (флешку), не меняя VID:PID, то драйвера останутся старые и, естественно, работать устройство не будет. Придется лезть в управление оборудованием, удалять драйвера и заставлять систему найти новые. Я думаю, ни для кого не станет неожиданностью, что в Linux такой проблемы нет: устройства просто подключаются и работают.

StringDescriptor


Еще одной интересной особенностью дескрипторов USB является любовь к строкам. В шаблоне дескриптора они обозначаются префиксом i, как например iSerialNumber или iPhone. Эти строки входят во многие дескрипторы и, честно говоря, я не знаю, зачем их так много. Тем более что при подключении устройства видны будут только iManufacturer, iProduct и iSerialNumber. Как бы то ни было, строки представляют собой те же дескрипторы (то есть поля bLength и bDescriptorType в наличии), но вместо дальнейшей структуры идет поток 16-битных символов, похожих на юникод. Смысл данного извращения мне опять непонятен, ведь подобные названия даются все равно обычно на английском, где и 8-битного ASCII хватило бы. Ну хорошо, хотите расширенный набор символов, так UTF-8 бы взяли. Странные люди Для удобного формирования строк удобно применять угадайте что правильно, макросы. Но на этот раз не моей разработки, а подсмотренные у EddyEm. Поскольку строки являются дескрипторами, то и запрашивать их хост будет как обычные дескрипторы, только в поле wValue подставит 0x0300 (STRING_DESCRIPTOR). А вместо младшего байта будет собственно индекс строки. Скажем, запрос 0x0300 это строка с индексом 0 (она зарезервирована под язык устройства и почти всегда равна u"\x0409"), а запрос 0x0302 строка с индексом 2.

Внимание, грабли! Сколь бы ни был велик соблазн засунуть в iSerialNumber просто строку, даже строку с честной версией вида u''1.2.3'' не делайте этого! Некоторые операционные системы считают, что там должны быть только шестнадцатеричные цифры, то есть '0'-'9', 'A'-'Z' и все. Даже точек нельзя. Наверное, они как-то считают от этого числа хэш чтобы идентифицировать при повторном подключении, не знаю. Но проблему такую заметил при тестировании на виртуальной машине с Windows 7, она считала устройство бракованным. Что интересно, Windows XP и 10 проблему не заметили.

ConfigurationDescriptor


С точки зрения хоста устройство представляет набор отдельных интерфейсов, каждый из которых предназначен для решения какой-то задачи. В дескрипторе интерфейса описывается его устройство и привязанные конечные точки. Да, конечные точки описываются не сами по себе, а только как часть интерфейса. Обычно интерфейсы со сложной архитектурой управляются SETUP запросами (то есть через ep0), в которых поле wIndex номеру интерфейса и соответствует. Максимум позволяется прикарманить конечную точку для прерываний. А от интерфейсов данных хосту нужны только описания конечных точек и обмен будет идти через них.

Интерфейсов в одном устройстве может быть много, причем очень разных. Поэтому чтобы не путаться где заканчивается один интерфейс и начинается другой, в дескрипторе указывается не только размер заголовка, но и отдельно (обычно 3-4 байтами) полный размер интерфейса. Таким образом интерфейс складывается подобно матрешке: внутри общего контейнера (который хранит размер заголовка, bDescriptorType и полный размер содержимого, включая заголовок) может находиться еще парочка контейнеров поменьше, но устроенных точно так же. А внутри еще и еще. Приведу пример дескриптора примитивного HID-устройства:
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    0, // bNumEndpoints    HIDCLASS_HID, // bInterfaceClass:     HIDSUBCLASS_NONE, // bInterfaceSubClass:     HIDPROTOCOL_NONE, // bInterfaceProtocol:     0x00, // iInterface  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_HID, //bDescriptorType    USB_U16(0x0101), //bcdHID    0, //bCountryCode    1, //bNumDescriptors    USB_DESCR_HID_REPORT, //bDescriptorType    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength  )  )};


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

Как можно видеть, данный дескриптор состоит из заголовка USB_DESCR_CONFIG (хранящего полный размер содержимого включая себя!), интерфейса USB_DESCR_INTERFACE (описывающего подробности устройства) и USB_DESCR_HID, в общих чертах говорящего что же именно за HID мы изображаем. Причем именно что в общих чертах: конкретная структура HID описывается в специальном дескрипторе HID_REPORT_DESCRIPTOR, рассматривать который я здесь не буду, просто потому что слишком плохо его знаю. Так что ограничимся копипастом из какого-нибудь примера.

Вернемся к интерфейсам. Учитывая, что у них есть номера, логично предположить, что в одном устройстве интерфейсов может быть много. Причем они могут отвечать как за одну общую задачу (скажем, интерфейс управления USB-CDC и интерфейс данных), так и за принципиально несвязанные. Скажем, ничто не мешает нам (кроме отсутствия знаний пока) на одном контроллере реализовать два переходника USB-CDC плюс флешку плюс, скажем, клавиатуру. Очевидно, что интерфейс флешки знать не знает про COM-порт. Впрочем, тут есть свои подводные камни, которые, надеюсь, когда-нибудь рассмотрим. Еще стоит отметить, что один интерфейс может иметь несколько альтернативных конфигураций (bAlternateSetting), отличающихся, скажем, количеством конечных точек или частотой их опроса. Собственно, для того и сделано: если хост считает, что лучше пропускную способность поберечь, он может переключить интерфейс в какой-нибудь альтернативный режим, который ему больше понравился.

Обмен данными с HID


Вообще говоря, HID-устройства имитируют объекты реального мира, у которых есть не столько данные, сколько набор неких параметров, которые можно измерять или задавать (запросы SET_REPORT / GET_REPORT) и которые могут уведомлять хост о внезапном внешнем событии (INTERRUPT). Таким образом, собственно для обмена данными данные устройства не предназначены но кого это когда останавливало!

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

Начнем с более простого чтения по запросу HIDREQ_GET_REPORT. По сути это такой же запрос, как и всякие DEVICE_DESCRIPTOR, только специфичный именно для HID. Плюс этот запрос адресован не устройству в целом, а интерфейсу. То есть если мы реализовали в одном устройстве несколько независимых HID-устройств, их можно различить по полю wIndex запроса. Правда, именно для HID это не лучший подход: проще сам дескриптор сделать составным. В любом случае до таких извращений нам далеко, так что даже не будем анализировать что и куда хост пытался послать: на любой запрос к интерфейсу и с полем bRequest равным HIDREQ_GET_REPORT будем возвращать собственно данные. По идее, такой подход предназначен чтобы возвращать дескрипторы (со всеми bLength и bDescriptorType), но в случае HID разработчики решили все упростить и обмениваться только данными. Вот и возвращаем указатель на нашу структуру и ее размер. Ну и небольшая дополнительная логика вроде обработки кнопок и счетчика запросов.

Более сложный случай запрос на запись. Это первый раз, когда мы сталкиваемся с наличием дополнительных данных в SETUP запросе. То есть ядро нашей библиотеки должно сначала прочитать сам запрос, и только потом данные. И передать их юзерской функции. А буфера у нас, напоминаю, нет. В результате некоторой низкоуровневой магии был разработан следующий алгоритм. Callback вызывать будем всегда, но укажем ему с какого по счету байта данные сейчас лежат в буфере приема конечной точки (offset) а также размер этих данных (size). То есть при приеме самого запроса значения offset и size равны нулю (данных-то нет). При приеме первого пакета offset все еще равен нулю, а size размеру принятых данных. Для второго offset будет равен размеру ep0 (потому что если данные пришлось разбивать, делают это по размеру конечной точки), а size размеру принятых данных. И так далее. Важно! Если данные приняты, их надо считать. Это может сделать либо обработчик вызовом usb_ep_read() и возвратом 1 (мол я там сам считал, не утруждайся), либо просто вернув 0 (мне эти данные не нужны) без чтения тогда очисткой займется ядро библиотеки. По этому принципу и построена функция: она проверяет в наличии ли данные и если да, то читает их и зажигает светодиоды.

Софт для обмена данными


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

Заключение


Вот, собственно, и все. Основы работы с USB при помощи аппаратного модуля в STM32 я рассказал, некоторый грабли тоже пощупал. Учитывая значительно меньший объем кода, чем тот ужас, что генерирует STMCube, разобраться в нем будет проще. Собственно говоря, в Cube'ической лапше я так и не разобрался, уж больно много там вызовов одного и того же в разных комбинациях. Гораздо лучше для понимания вариант от EddyEm, от которого я отталкивался. Конечно, и там не без косяков, но хотя бы пригодно для понимания. Также похвастаюсь, что размер моего варианта едва ли не в 5 раз меньше ST'шного (~2.7 кБ против 14) при том, что оптимизацией я не занимался и наверняка можно еще ужать.

Отдельно хочу отметить разницу поведения различных операционных систем при подключении сомнительного оборудования. Linux просто работает, даже если в дескрипторах ошибки. Windows XP, 7, 10 при малейших ошибках ругаются что устройство поломанное, я с ним работать отказываюсь. Причем XP иногда даже в BSOD падала от негодования. Ах да, еще они постоянно выводят устройство может работать быстрее, что с этим делать я не знаю. В общем, как бы хорош Linux не был для разработки, он прощает слишком многое, тестировать надо и на менее юзер-френдли системах.

Дальнейшие планы: рассмотреть остальные типы конечных точек (пока что был пример только с Control); рассмотреть другие контроллеры (скажем, у меня еще at90usb162 (AVR) и gd32vf103 (RISC_V) валяются), но это совсем далекие планы. Также хорошо бы поподробнее разобраться с отдельными USB-устройствами вроде тех же HID, но тоже не приоритетная задача.
Подробнее..

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