Миф, который надо убить первым
Откройте любой поиск по запросу «Arduino на Go» — и вы упрётесь в статьи, где автор радостно показывает, как написать func loop() и прошить это в Uno. Почти всегда это либо устаревшая информация, либо подмена понятий, либо откровенная выдумка. Поэтому начнём не с того, «как», а с того, «почему нет», потому что понимание границы спасает больше времени, чем любой пример кода.
Короткий ответ: на классических Arduino на AVR (Uno, Nano, Mega) скетч на Go написать нельзя. Не «сложно», не «нужна библиотека» — а физически невозможно по двум независимым причинам, и обе проверяются за минуту.
Первая причина — архитектура процессора. Стандартный компилятор Go генерирует нативный код для ограниченного набора архитектур, заданных парами GOOS/GOARCH (операционная система и набор команд). AVR — это отдельный набор команд, и в списке поддерживаемых Go-целевых платформ его нет. Компилятор Go попросту не умеет производить машинный код для AVR. Это не баг и не «пока не добавили» — это фундаментальное свойство инструмента, и ждать, что оно появится в обозримом будущем, не приходится.
Вторая причина — память, и она даже нагляднее. Возьмём самую популярную плату, Uno, на микроконтроллере ATmega328P. Согласно даташиту на этот чип, у него 32 килобайта флеш-памяти программ (из которых около полкилобайта занято загрузчиком, то есть для скетча остаётся чуть меньше 32 килобайт), 2 килобайта оперативной памяти и 1 килобайт энергонезависимой памяти данных. Теперь посмотрим, что такое «hello world» на Go, собранный стандартным компилятором. Даже с агрессивным удалением отладочной информации и без использования библиотеки Си, минимальный бинарник Go измеряется сотнями килобайт, а типично — мегабайтами, потому что в каждый бинарник Go вкладывается среда выполнения: планировщик лёгких потоков, сборщик мусора, диспетчер, таблицы типов. Этих сотен килобайт и мегабайт в 32 килобайта флеш и 2 килобайта оперативной памяти не помещается ни при каких условиях. Точка. Никакая «оптимизация» не сожмёт среду выполнения Go до размеров, совместимых с AVR Uno.
Именно поэтому связка «Go и Arduino» в реальности распадается на два разных сценария, и путать их — главная ошибка новичка. Первый сценарий: Go живёт на обычном компьютере и управляет Arduino как периферийным устройством. Второй сценарий: на микроконтроллере работает не стандартный Go, а отдельный компилятор подмножества Go, который умеет генерировать компактный код под конкретные архитектуры. Разберём оба честно.
Сценарий первый: Go как хост, Arduino как периферия
Это самый надёжный, самый старый и самый «проверенный» путь, и он работает на любой Arduino-плате — хоть на AVR Uno, хоть на ESP32, хоть на ARM-плате. Идея простая: скетч на плате по-прежнему пишется на языке Arduino (по сути Си-плюс-плюс с обвязкой), а Go-программа на компьютере выступает «мозгом», который общается с платой по последовательному порту и при необходимости прошивает её.
Здесь Go не лезет в микроконтроллер вообще. Он решает задачи, для которых он и создан: сетевой ввод-вывод, обработка данных, логику, базы данных, веб-сервер, очередь, агрегацию показаний. А плата делает то, для чего она хороша: читает ножки, меряет датчики, крутит моторы, зажигает светодиоды. Это честное разделение труда, и оно не требует чудес.
Как устроено общение по последовательному порту
Классический Arduino подключается к компьютеру по USB, и на компьютере это видится как последовательный порт (на Linux обычно /dev/ttyACM0 или /dev/ttyUSB0, на Windows — COM3, COM4 и так далее). Скетч на плате вызывает Serial.begin, Serial.read, Serial.println — и всё это течёт по этому порту. Go-хост открывает тот же порт и читает/пишет в него байты.
Чтобы общение было предсказуемым, используют построчный протокол: каждая команда и каждый ответ заканчиваются переводом строки, и обе стороны читают до перевода строки. Это не какая-то магия — это ровно то, как устроены текстовые протоколы вроде HTTP по сути: разделение на строки, парсинг, ответ. Ниже — типовая карта такого протокола для проекта-термометра.
Хост (Go) -> Плата (скетч Arduino)
"READ\n" -> запрос текущего показания
"SET 25\n" -> установить порог, например 25 градусов
"PING\n" -> проверка живости
Плата (скетч Arduino) -> Хост (Go)
"T=24.6,H=51.2\n" -> температура и влажность
"OK\n" -> подтверждение установки порога
"ERR sensor\n" -> ошибка чтения датчикаСкетч на стороне Arduino читает строку из Serial, смотрит первое слово-команду, выполняет и печатает ответ строкой. Go-хост пишет команду с переводом строки и читает ответ до перевода строки. Всё. Никаких двоичных фреймов, никаких CRC, никаких таймеров — для настольного проекта по проводам этого более чем достаточно, а усложнять протокол имеет смысл только при шуме линии или больших скоростях.
Прошивка Arduino из Go через avrdude
Второе, что умеет и часто нужно в этом сценарии, — прошивать плату программно, без открытия Arduino IDE. Здесь Go снова не изобретает протокол загрузчика, а делегирует это проверенному инструменту. Для AVR-плат канонический программатор — avrdude, утилита, которая существует десятилетиями и знает все протоколы загрузчиков (Optiboot по последовательному порту, ISP через программатор, STK500 и так далее). Go вызывает avrdude как внешний процесс через стандартную библиотеку os/exec, и вот здесь я могу дать конкретный код без оговорок, потому что os/exec — это стабильная часть Go, API которой не менялся годами.
package main
import (
"context"
"fmt"
"os/exec"
"time"
)
// flashUno прошивает скомпилированный скетч в Arduino Uno через avrdude.
// sketchHex — путь к .hex-файлу, полученному из Arduino IDE или из
// командной сборки (arduino-cli compile ... --output-dir ...).
// port — последовательный порт платы: на Linux /dev/ttyACM0, на Windows COM3.
func flashUno(ctx context.Context, port, sketchHex string) error {
args := []string{
"-p", "m328p", // процессор ATmega328P (Uno/Nano)
"-c", "arduino", // программатор = загрузчик Arduino по serial
"-P", port, // порт платы
"-b", "115200", // скорость загрузчика Optiboot на Uno
"-D", // не стирать флеш перед записью
"-U", "flash:w:" + sketchHex + ":i", // записать .hex во флеш
}
cmd := exec.CommandContext(ctx, "avrdude", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("avrdude завершился с ошибкой: %w\nвывод:\n%s", err, out)
}
fmt.Printf("прошивка успешна:\n%s\n", out)
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := flashUno(ctx, "/dev/ttyACM0", "build/sketch.ino.hex"); err != nil {
fmt.Println(err)
}
}Этот код я даю уверенно: флаги avrdude для Uno канонические, а os/exec.CommandContext/CombinedOutput — стандартная библиотека. Единственные внешние допущения, которые надо проверить под свою машину: имя порта (на Linux это /dev/ttyACM0 для плат с原生 USB-интерфейсом вроде Uno R3/SMD и /dev/ttyUSB0 для плат с мостом CH340/FTDI), скорость загрузчика (для Uno с Optiboot это 115200, но на старых загрузчиках и других платах бывает 57600 — это берётся из даташита/настроек платы, см. статью про чтение даташита) и права доступа к порту. На Linux пользователю, запускающему прошивку, обычно нужна группа dialout (или uucp на некоторых дистрибутивах), иначе avrdude не откроет порт. Это не особенность Go, а особенность прав операционной системы на последовательные устройства.
Для ESP32 и ARM-плат avrdude не подходит — у них другие загрузчики. ESP32 прошивается по своему протоколу загрузчика Espressif (по UART или по Wi-Fi), ARM-платформы вроде Zero/MKR — через bossac/протокол SAM-BA или через Mass Storage (плата видится как флешка). В этих случаях Go так же вызывает внешний инструмент (esptool.py для ESP32, bossac для SAMD), но конкретные флаги этих утилит я в этом ответе не перечитывал и дословно не привожу — сверяйте с их официальной документацией, по тому же принципу, что и в статье про даташит: не угадывать флаг, а открыть справочник инструмента.
Чтение данных с платы в Go
Здесь начинается зона, где я обязан поставить рамку. Для открытия последовательного порта из Go нужен пакет, потому что стандартная библиотека Go не имеет удобного переносимого API для последовательных портов (низкоуровнево это syscall/termios под Linux и свои вызовы под Windows, писать это руками — боль и источник ошибок). Де-факто стандартный пакет для этой задачи в Go-сообществе — go.bug.st/serial. Я уверен, что он существует и является основным выбором, но точные сигнатуры его функций (Open, SetMode, Read, Write и структура режима с baud-рейтом) я в этом ответе не перечитывал по документации пакета, поэтому не буду выдавать их за проверенные дословно. Ниже — каркас, показывающий архитектуру, с явной пометкой, где свериться с docs.
// Каркас чтения построчного протокола с Arduino.
// ТОЧНЫЙ API пакета go.bug.st/serial сверь с его официальной документацией:
// имена функций, структура Mode и поля (BaudRate и т.п.) могут отличаться
// от версии к версии. Я не перечитывал docs пакета в рамках этой статьи,
// поэтому привожу структуру логики, а не гарантированно компилируемый код.
// 1. Открыть порт по имени (/dev/ttyACM0 на Linux, COM3 на Windows).
// 2. Задать скорость, совпадающую с Serial.begin в скетче (например 115200).
// 3. В цикле: записать команду "READ\n", прочитать ответ до "\n",
// распарсить "T=24.6,H=51.2" в числа, передать дальше (в лог, в HTTP, в БД).
// 4. Обрабатывать таймауты чтения и ошибки порта как штатные события,
// а не как падения: плата может быть выдернута, датчик может вернуть ERR.Архитектурно всё тривиально и проверяемо: порт открыт, скорость согласована с обеих сторон, построчное чтение-запись. Конкретные вызовы — в документацию go.bug.st/serial. Это честнее, чем вставить «красивый» код, который не соберётся из-за угаданного поля структуры.
Скелет скетча на стороне Arduino
Чтобы картина была полной, вот каркас скетча, который соответствует построчному протоколу выше. Язык здесь — Arduino (Си-плюс-плюс с обвязкой), и это правильно: скетч остаётся в родной для платы экосистеме, а Go работает снаружи.
// Каркас скетча для Uno/Nano: построчный протокол с хостом на Go.
// Точные имена функций библиотеки датчика сверяйте с её документацией
// (см. статью про поделку с DHT и OLED про библиотеки Adafruit).
void setup() {
Serial.begin(115200); // скорость совпадает с -b 115200 у avrdude
// инициализация датчика, например dht.begin();
}
void loop() {
if (Serial.available() > 0) {
String line = Serial.readStringUntil('\n');
line.trim();
if (line == "PING") {
Serial.println("PONG");
} else if (line == "READ") {
// float t = ...; float h = ...; // чтение датчика
// Serial.printf("T=%.1f,H=%.1f\n", t, h);
Serial.println("T=0.0,H=0.0"); // заглушка, пока датчик не подключён
} else if (line.startsWith("SET ")) {
// парсинг числа после "SET ", установка порога
Serial.println("OK");
} else {
Serial.println("ERR unknown");
}
}
}Обратите внимание на деталь, которая связывает эту статью со статьёй про питание: Serial.begin(115200) и -b 115200 у avrdude — это не совпадение по вкусу, а требование согласования скорости на обеих сторонах, ровно как в статье про питание согласовывались напряжения и тайминги. Рассогласование здесь даст «мусор» в порту, и новичок будет искать баг в парсере Go, хотя дело в скорости.
Сценарий второй: TinyGo — компилятор подмножества Go на микроконтроллере
Теперь про тот самый «скетч на Go», который существует, но не там, где все ждут. TinyGo — это отдельный компилятор, который принимает подмножество языка Go и генерирует компактный код под встраиваемые архитектуры. Он не является стандартным Go-компилятором и не обещает совместимости со всем языком; это инструмент со своей целью — дать Go-синтаксис там, где полный Go не влезает.
Ключевое, что надо понять про TinyGo, прежде чем радоваться: он не спасает сценарий «прошу Go в Uno». TinyGo тоже не поддерживает AVR как целевую архитектуру, и даже если бы поддерживал, 2 килобайта оперативной памяти Uno слишком малы для среды выполнения TinyGo (у него тоже есть сборщик мусора и планировщик, просто заметно легче, чем у полного Go). То есть на Uno/Nano/Mega TinyGo не работает. Это важно сказать прямо, потому что многие статьи про «Go на Arduino» молча подразумевают именно TinyGo, но рисуют при этом картинку с Uno.
Где TinyGo реально работает — это платы на архитектурах, для которых у него есть бэкенды и достаточно памяти:
- ESP32 и ESP8266 (наборы команд Xtensa) — и это естественная точка стыковки со статьями про ESP32-S3 и про новинки процессоров: та же плата, которую мы прошивали через Arduino IDE и через
esptool, умеет принимать и TinyGo-код. - ARM-платформы на Cortex-M: например, Arduino Nano 33 BLE (на чипе nRF52840), Arduino Zero/MKR (на SAMD), Raspberry Pi Pico (на RP2040), micro:bit.
- Ряд других встраиваемых целей, полный список которых меняется от релиза к релизу.
Точный перечень поддерживаемых плат и имена таргетов для флага -target при сборке я в этом ответе не перечитывал по документации TinyGo, поэтому не привожу их как гарантированный список. Канонический способ узнать, что доступно в вашей версии TinyGo, — команда tinygo targets, которая печатает именно те имена, что понимает установленный компилятор. Это ровно тот принцип «иди в первоисточник, не угадывай», который мы разбирали в статье про даташит: список таргетов живёт в самом инструменте, а не в моей памяти.
Что даёт TinyGo и чем он ограничен
TinyGo позволяет писать логику на знакомом синтаксисе Go и получать бинарник, который влезает в сотни килобайт флеш и десятки-сотни килобайт оперативной памяти — то есть в ESP32 и в ARM-платы, но не в AVR. Это делает его удобным для проектов, где хочется Go-стиль (статическая типизация, простые структуры, каналы и лёгкие потоки там, где они поддержаны) без перехода на Си-плюс-плюс.
Но это подмножество, и ограничения надо знать заранее, иначе проект упрётся в стену на середине:
- Не все возможности стандартной библиотеки Go доступны; что именно поддержано для вашего таргета — смотрите в документации TinyGo по стандартной библиотеке.
- Часть возможностей языка работает иначе или ограничена (например, отражение типов, небезопасные указатели, некоторые аспекты дженериков и интерфейсов) — точный список зависит от версии и сверяется с docs.
- Поведение лёгких потоков и сборщика мусора на микроконтроллере отличается от десктопа: есть задержки, есть стоимость памяти, и «бесплатные» горутинки на десктопе здесь не бесплатны.
- Отладка и экосистема библиотек беднее, чем у Arduino IDE или у полного Go: многие драйверы датчиков придётся писать самим или брать из репозиториев драйверов TinyGo, а не из мира Arduino-библиотек.
Конкретные флаги сборки (-target, -opt, -size) и точные имена таргетов я сознательно не привожу дословно как «рабочую команду», потому что не перечитывал их в этом ответе; направление такое: сборка идёт через tinygo build/tinygo flash с таргетом из tinygo targets, а прошивка — либо встроенным механизмом tinygo flash для поддерживаемой платы, либо внешним инструментом (esptool для ESP32, bossac/Mass Storage для SAMD), как и в первом сценарии. Точные параметры — в официальную документацию TinyGo.
Когда выбирать TinyGo, а когда — хост-сценарий
Это не вопрос «что круче», а вопрос соответствия задаче, и рамка тут простая.
Нужна логика на компьютере: сеть, база, веб, агрегация, много данных
-> сценарий первый: Go-хост + скетч на Arduino по последовательному порту
-> работает на ЛЮБОЙ плате, включая Uno
Нужен компактный код прямо на микроконтроллере, и плата не AVR
(ESP32, ESP8266, ARM-плата с достаточной памятью)
-> сценарий второй: TinyGo
Нужен код на микроконтроллере, но плата — AVR Uno/Nano/Mega
-> TinyGo не подходит, полный Go не подходит
-> пишите скетч на Arduino (Си-плюс-плюс), а Go используйте снаружи
Нужна максимальная экосистема библиотек датчиков и «всё из коробки»
-> сценарий первый со скетчем на Arduino, потому что Arduino IDE
имеет огромную базу готовых библиотек, которой у TinyGo нетОбратите внимание на последний пункт: даже когда TinyGo технически возможен, экосистема Arduino-библиотек (те самые Adafruit-библиотеки из статьи про поделку) часто перевешивает, и тогда разумнее писать скетч на Arduino, а Go держать на хосте. TinyGo выигрывает там, где вам критичен именно Go-синтаксис и где вы готовы платить меньшим набором готовых драйверов.
Практический мини-проект, связывающий обе статьи
Соберём то, что уже разбирали, в один контур. Берём поделку из статьи про термогигрометр: Arduino Uno читает DHT22 и показывает на OLED. Теперь добавляем Go-хост, который не трогает дисплей, а забирает показания по последовательному порту и пишет их в файл или отдаёт по HTTP — то есть превращает настольную станцию в сетевое устройство телеметрии, не меняя скетч радикально.
Архитектура получается такая: скетч на Uno по-прежнему рисует температуру на OLED (это его локальная задача, и он с ней справляется), и параллельно по запросу хоста печатает строку T=...,H=... в последовательный порт. Go-хост открывает порт, периодически шлёт READ\n, читает ответ, парсит числа и складывает в лог или в HTTP-ответ. Прошивка скетча — через avrdude из Go, как в примере выше. Ни один из двух процессов не лезет в чужую память: плата делает своё, хост делает своё, связь — построчный протокол по проводам.
Это и есть взрослый ответ на «Go и Arduino»: не «пишем скетч на Go», а «Go оркеструет Arduino». И если завтра вы захотите уйти от Uno к ESP32-S3 ради Wi-Fi (тема статей про ESP32-S3 и про новинки процессоров), сценарий первый останется валидным без изменений — поменяется только инструмент прошивки (avrdude на esptool) и транспорт (последовательный порт на сокет по Wi-Fi), а идея «хост на Go управляет платой» не изменится. А сценарий второй с TinyGo на ESP32-S3 тоже станет возможен, потому что это уже не AVR.
Чек-лист перед стартом
- Поняли, какая у вас плата: AVR (Uno/Nano/Mega) или не-AVR (ESP32/ESP8266/ARM). От этого зависит, доступен ли TinyGo вообще.
- Для AVR приняли как факт: скетч пишется на Arduino, Go работает снаружи. Не тратьте время на попытки «прошить Go в Uno».
- Согласовали скорость последовательного порта на обеих сторонах (
Serial.beginв скетче и скорость открытия порта в хосте; для прошивки Uno —-b 115200уavrdude). - Проверили права на порт в операционной системе (на Linux — группа
dialout/uarp-аналог, см. примечание выше; на Windows — что порт не занят Arduino IDE). - Для прошивки не-AVR-плат выбрали правильный внешний инструмент (
esptoolдля ESP32,bossac/Mass Storage для SAMD) и сверили его флаги с документацией инструмента, а не по памяти. - Для TinyGo уточнили список таргетов командой
tinygo targetsв своей установленной версии и прочитали раздел об ограничениях подмножества в официальной документации, прежде чем переносить туда существующий Go-код. - Для API пакета последовательного порта (
go.bug.st/serial) сверили сигнатуры с его официальной документацией, потому что каркас в этой статье намеренно не является гарантированно компилируемым кодом. - Держали в голове питание и тайминги: построчный протокол по проводам прощает многое, но просадки и рассогласование скоростей ломают чтение так же, как в статье про питание ломают радио.
Итог
«Go и Arduino» — это не одна технология, а два разных вопроса, и смешивать их значит гарантированно нарваться на миф. Первый вопрос: «можно ли писать скетчи на Go для Uno?» — нет, и это проверяется картой памяти ATmega328P из даташита и отсутствием AVR в целевых архитектурах Go; среда выполнения полного Go физически не влезает в 32 килобайта флеш и 2 килобайта оперативной памяти. Второй вопрос: «можно ли использовать Go в проекте с Arduino?» — да, и даже двумя способами: Go как хост, управляющий платой по последовательному порту и прошивающий её через проверенные внешние инструменты вроде avrdude (работает на любой плате), либо TinyGo как компилятор подмножества Go прямо на микроконтроллере (работает на ESP32, ESP8266 и ARM-платах, но не на AVR). Выбор между ними — это выбор между «логика на компьютере» и «компактный код на плате», и он определяется задачей, а не модой.
Эта статья естественно встраивается в цепочку блога: железо и выбор чипа разбирались в ESP32-S3 и в Новинках процессоров, практическая поделка с датчиком и дисплеем — в Термогигрометре с OLED на Arduino, почему «плавают» показания и как это связано с питанием и таймингами — в Питании ESP32 и ESP32-S3, а принцип «не угадывай цифру, иди в первоисточник», который я применял к флагам avrdude, к таргетам TinyGo и к API пакета последовательного порта, — в Как читать даташит. Маркировка резисторов в обвязке датчика — в Маркировке SMD-резисторов, а место локальной обработки сигналов в общей картине — в Искусственном интеллекте для радиолюбителей.
