Язык Go благодаря своей простоте, возможности компиляции в выполняемый образ и встроенной поддержке многозадачности стал, в некотором смысле, "серебряной пулей" для создания высокопроизводительных инструментов и, совместно с Rust, сформировал современный технологический ландшафт для DevOps. Но в действительности, благодаря поддержке набора инструментов LLVM, стало возможным использовать Go и для встраиваемых систем, например при создании мобильных приложений для Android/iOS  (например, проекты android-go или gomobile) или микроконтроллеров. В этой статье мы поговорим о возможностях проекта TinyGo, его преимуществах по сравнению с C++ для Arduino и других микроконтроллеров, рассмотрим несколько примеров по работе с оборудованием (на примере реализации драйвера шины SPI для светодиодной ленты WS2812). Несмотря на то, что в LLVM + clang "из коробки" поддерживаются различные целевые платформы (включая архитектуру AVR, которая используется в микроконтроллерах Arduino) результат выполнения компиляции у Golang Compiler весьма значителен по размеру, что в условиях ограниченной памяти у микроконтроллеров становится блокирующим фактором. Для решения этой проблемы был создан проект TinyGo - альтернативный компилятор, поддерживающий многие функции из стандартной библиотеки, но при этом создающий оптимизированный по размеру код, который может быть загружен даже на микроконтроллеры с очень незначительным объемом памяти (например, у наиболее бюджетного Arduino Uno доступно чуть меньше 8кб для загрузки программы). Кроме того, особенностью микроконтроллеров архитектуры AVR является отсутствие операционной системы, а также отличная от принятой на Intel-архитектуре модель прерываний и многозадачности, что накладывает ограничения как на реализацию библиотек (которые должны быть уникальны для каждого семейства микроконтроллеров), так и на поддержку возможностей языка Go и использование структур данных в памяти (goroutines, динамическое управление памятью, сборка мусора). При этом, благодаря возможностям оптимизации компилятора Go, результирующий размер образа для идентичных программ в среднем получается меньше, чем после компиляции с языка C (например, классическая "мигалка" на C создает образ в 924 байта, тогда как TinyGo создает образ 560 байт). Проект TinyGo включает в себя компилятор (создающий IR-код для компиляции в LLVM с использованием значительного количества оптимизаций), набор драйверов для периферийного оборудования (подключаемого через шины микроконтроллера), а также инструменты для создания выполняемого образа и его загрузки на микроконтроллер. Целевой платформой может быть один из микроконтроллеров на архитектурах AVR (все Arduino), семейства Xtensa (ESP32/ESP8266), ARM и частично RISC-V. Также компилятор может создавать выполняемые файлы для Linux и WebAssembly (в среднем размер образа становится меньше, чем при компиляции в целевую платформу wasm32 через обычный компилятор Go). Компилятор может быть собран из исходных текстов, установлен для Windows / Linux или MacOS, а также запущен с использованием Docker. При установке из пакетов для сборки кода для микроконтроллеров на архитектуре AVR нужно дополнительно настроить avr-gcc, avr-libc и avrdude (для взаимодействия с загрузчиком).  После установки становится доступной утилита командной строки tinygo, которая может быть использована в следующих сценариях: tinygo build -o <name> -target <platform> path - сборка двоичного образа для указанной платформы (например, arduino или nintendoswitch. Тип образа определяется платформой и расширением файла, например. ll - создание текстового IR-представления; bc - создание биткода LLVM; hex - создание файла в формате Intel HEX для прошивки на микроконтроллер; bin - создание двоичного файла для прошивки на микроконтроллер; wasm - создание webassembly-файла для запуска в браузере. tinygo flash -target <platform> path - сборка образа и загрузка прошивки на микроконтроллер; tinygo gdb - создание отлаживаемого образа кода (с возможной загрузкой на микроконтроллер) и запуск отладчика; tinygo run - загрузка и запуск выполняемого кода на целевом устройстве; tinygo clean - удаление промежуточных артефактов. При выполнении компиляции можно указать уровень оптимизации (-opt=0 - отсутствие оптимизаций, 1 - минимальная оптимизация (полезна при необходимости изучения IR), 2 - хорошая оптимизация, s - оптимизация по размеру кода, z - агрессивная оптимизация). Кроме того, поскольку сборка мусора интегрируется в скомпилированный код (и увеличивает объем образа), может быть задана модель управления памятью (-gc=none - автоматическое управление памятью отсутствует, leaking - разрешено выделение памяти, автоматически не освобождается, conservative - сборка мусора на основе алгоритма mark-sweep). Также может быть задана реализация планировщика, например для AVR можно его отключить -scheduler= none. Параметр -size также может быть полезна для получения информации о размере образа. Для доступа к оборудованию (с учетом специфики архитектуры и/или конкретной платы) используется пакет machine, который представляет набор констант для идентификации интерфейсов gpio (например, цифровых и аналоговых пинов) и методов для взаимодействия с ними (например, Configure для настройки режима, Low/High для переключения состояния цифрового пина). Также, в зависимости от возможностей микроконтроллера, предоставляются структуры для взаимодействия через шины I2C, SPI, UART, при этом поддерживаются как аппаратные реализации, так и (для некоторых драйверов) программная эмуляция протоколов шины. При необходимости создания кода с точным контролем временных параметров (например, при программной эмуляции протоколов через GPIO) для реализации функций может использоваться код на ассемблере (зависит от типа микроконтроллера).  Рассмотрим несколько примеров кода на Go для запуска на микроконтроллере.  Аналогом Hello World для языков программирования является приложение-мигалка, которая использует интегрированный (или подключенный через GPIO-пины) светодиод и циклически переключает состояние его включения. Для реализации такого приложения будет необходимо подключить два пакета: timer (для выполнения задержки между переключениями состояния) и machine (для доступа к GPIO, к которому подключен светодиод). При запуске программы прежде всего выполним настройку GPIO в режим вывода (будем использовать константу LED для определения идентификатора GPIO, к которому на конкретном устройстве подключен встроенный светодиод): Далее в цикле будем переключать состояние светодиода и разделять переключение временной задержкой Полный код приложения может выглядеть так: Запустим наше приложение на подключенном микроконтроллере, например для Arduino Uno: Рассмотрим теперь более сложный сценарий с использованием данных акселерометра ADXL335. Для доступа к подключенному устройству будет необходимо установить пакет драйвера из соответствующего репозитория.  Поскольку устройство взаимодействует с микроконтроллером по шине I2C, перед инициализацией драйвера необходимо выполнить настройку I2C (доступна для Arduino через machine.I2C0), после которой выполняется конфигурация оборудования и становятся доступными координаты для получения. В завершении работы с акселерометром должна быть вызвана функция Halt, которая отключает устройство и обновление координат.  Подробная информация по использованию функций adxl345 может быть найдена в документации. Теперь посмотрим на использование возможностей ассемблерных вставок для реализации точной настройки тайминга. В качестве примера посмотрим на возможную реализацию протокола WS2812B для AVR (в официальном драйвере поддерживается только вариант протокола WS2812), которая используется во множестве светодиодных панелей и лент. Особенностью протокола является использование скважности для кодирования значения бит. В приведенном примере подстройка временных характеристик используется вставка пустых операций (nop). Хочется обратить внимание на использование регистров. Поскольку в AVR системах использование оперативной памяти весьма ограничено, для хранения значений локальных переменных (при оптимизациях --opt=2 и выше) используются регистры процессора и это может привести к проблемам при применении регистров в ассемблерном коде (например на этапе компиляции с --opt=z возникает ошибка из-за невозможности зарезервировать регистры для хранения всех необходимых локальных значений функции. Для низкоуровневого доступа к аппаратным возможностям микроконтроллера доступны пакеты device/<arch> (например, device/avr) и runtime/volatile и runtime/interrupt. С помощью volatile может анонсироваться явное хранение переменной в регистре (тип   *volatile.Register8). Через пакет device/avr может быть получен доступ к линиям прерываний, связанным с аппаратными компонентами микроконтроллера (например,  avr.IRQ_SPI_STC для регистрации обработчика прерывания при получении данных через аппаратный контроллер SPI с использованием interrupt.New(avr.IRQ_SPI_STC, handler), где обработчик получает структуру interrupt.Interrupt. Аналогично могут быть использованы аппаратные возможности для других микроконтроллеров, например при использовании Xtensa (ESP32/ESP8266) можно задействовать AT-команды для Wi-Fi адаптера с использованием драйвера и пакета tinygo.org/x/drivers/net. Поскольку получение данных выполняется асинхронно важно использовать goroutines и каналы для организации отправки-получения данных через сеть. Пример использования goroutines для обработки данных можно посмотреть в драйвере mqtt-router. При разработке кода можно использовать все языковые возможности Go, создавать структуры с функциями для описания бизнес-логики и интерфейсов устройств ввода-вывода. В зависимости от доступного объема памяти и типа процессора можно использовать механизмы многозадачности (goroutines, channels), динамическое выделение памяти и сборку мусора, а также готовые алгоритмы, реализованные в доступных пакетах на Go (например, можно запустить веб-сервер на микроконтроллере). Также доступны многие стандартные пакеты Go, но особое внимание надо уделять динамическому выделению памяти, особенно на микроконтроллерах с небольшим объемом оперативной памяти. Использование динамических массивов (с выделением памяти через make) в этом случае будет ограничено типом []byte, который является эквивалентом строки. Для работы со строками в TinyGo доступны пакеты fmt, strconv, strings, unicode и text. Также есть возможность использовать пакеты encoding при работе с различными формами кодирования данных (кроме xml), crypto для шифрования и хэширования, archive и compress для работы со сжатыми данными. image для поддержки изображений, math для математических действий, net для обмена данными с использованием сетевых протоколов (например, net/http) и другие. Из практики использования математических пакетов хочется отметить, что добавление поддержки чисел с плавающей точкой существенно увеличение размера скомпилированного образа и, например, для генерации случайных чисел может использоваться шум, получаемый с неподключенными аналогового входа или иной генератор случайных чисел (если он предусмотрен в микроконтроллере). В TinyGo недоступны os/exec, os/signal и os/user, поскольку на микроконтроллерах отсутствует операционная система или механизмы взаимодействия с ней отличаются от принятых в POSIX. Пример использования TinyGo можно посмотреть в исходном коде прошивки для микроконтроллера прототипа устройства AirCube на Github. А всех, кто дочитал до конца, хочу пригласить на бесплатное занятие от моих коллег из OTUS по теме: "Структуры языка Golang". На занятии будут рассмотрены такие понятия как: процесс определения структур, инкапсуляция полей структуры, определение методов структуры. Побеседуем про вложенные и анонимные структуры. Также постараемся успеть поговорить и про структурные тэги и их использование в контексте JSON, XML и СУБД. После занятия вы сможете создавать пользовательские типы данных и комбинировать их между собой.   Зарегистрироваться на вебинар.  Flutter Developer@YandexPro, DevOps, IT Architect