Go and Hardware: Why It's No Joke?
\nWhen you hear the phrase "embedded development," you usually imagine C, assembly, and painful debugging via an LED. But the world doesn't stand still. Today, Go (or Golang) is increasingly knocking on the door of the microcontroller world. And it's not knocking for nothing; it comes with goroutines, a garbage collector, and built-in interfaces.
\nIn this article, we'll explore what embedded development in Go looks like, write real code for a hypothetical microcontroller, and figure out if it's worth the effort. Spoiler: yes, if you're tired of memory leaks and love readable code.
\n\nWhy Go, Not C? Three Pillars
\nClassic embedded is C. But Go has three killer features that make engineers reconsider their habits:
\n- \n
- Memory safety: no wild pointers that can fly into space. The Go compiler is stricter than the C compiler. \n
- Goroutines: lightweight threads that fit perfectly into the event-driven architecture of microcontrollers. \n
- Cross-platform: you write code on a Mac and compile it for ARM Cortex-M. No jumping through hoops. \n
Of course, there's a nuance: native Go (gc) cannot compile for small MCUs. But there is the TinyGo project — a compiler that adapts Go for hardware with kilobytes of memory. That's what we'll focus on.
\n\nEnvironment Setup: TinyGo and Your Board
\nBefore writing code, let's prepare the workspace. For embedded development in Go, you'll need:
\n- \n
- Install TinyGo:
brew install tinygo(or download from the official website). \n - Install the toolchain for your board (e.g., ARM GCC for STM32). \n
- Connect the board via USB. \n
Let's check that everything works:
\ntinygo version\ntinygo: 0.31.0 (linux/amd64)\n\nNow you can compile code for any supported board: tinygo build -target=arduino-nano -o firmware.hex main.go.
First Blink: LED in Go
\nA classic. Connect to a pin, toggle it. Here's what microcontroller programming in Go looks like:
\npackage main\n\nimport (\n "machine"\n "time"\n)\n\nfunc main() {\n // The built-in LED is usually on pin 13 (or LED_BUILTIN)\n led := machine.LED\n led.Configure(machine.PinConfig{Mode: machine.PinOutput})\n\n for {\n led.High()\n time.Sleep(500 * time.Millisecond)\n led.Low()\n time.Sleep(500 * time.Millisecond)\n }\n}\n\nCompile and flash to the board:
\ntinygo flash -target=arduino-nano -port=/dev/ttyUSB0 main.go\n\nVoila! The LED blinks. Note: we use the machine package — this is TinyGo's hardware abstraction. It hides register manipulation behind the scenes but provides a clean Go interface.
Working with GPIO and Interrupts
\nIn real projects, just blinking isn't enough. You need to read buttons, sensors, and react to events. In Go for embedded systems, interrupts are implemented via channels and goroutines. Example: attach a button press handler.
\npackage main\n\nimport (\n "machine"\n "fmt"\n)\n\nfunc main() {\n button := machine.D2\n button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})\n\n // Wait for a press (pin == LOW, since it's pulled up to VCC)\n for {\n if !button.Get() {\n fmt.Println("Button pressed!")\n }\n }\n}\n\nBut this is a busy loop. Want something more efficient? Use an interrupt via a channel:
\nfunc main() {\n button := machine.D2\n button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})\n\n // Channel for events\n ch := make(chan bool, 1)\n button.SetInterrupt(machine.PinFalling,