TinyGo 是专为裸机嵌入式设计的 Go 编译器,替代原生 go build,支持静态内存分配、无 OS/GC 运行;需用 tinygo build 指定 target,依赖 machine 包操作硬件,无抢占式 goroutine 调度。

用 TinyGo 编译裸机固件,不是 go build
Go 原生 go build 无法生成裸机二进制,它依赖操作系统和 runtime;嵌入式需要的是无 OS、无 GC、栈可静态分配的代码。TinyGo 是专为此设计的替代编译器,底层用 LLVM,能内联调度、裁剪未用函数、把 make 和 new 编译成静态内存分配。
实操建议:
- 安装必须用
tinygo命令,不是go:比如tinygo build -target=arduino -o firmware.hex ./main.go -
-target参数决定链接脚本、启动代码、外设寄存器定义——选错就根本烧不进芯片,常见目标有arduino、feather-m0、raspberry-pi-pico - 不支持全部 Go 标准库:
net、os、fmt(部分函数可用但会增大体积)、reflect完全不可用
操作硬件寄存器时,别直接写 0x40001000
TinyGo 提供了按芯片型号组织的 machine 包,比如 machine.GPIO{Pin: machine.PA05}.Configure(),它背后自动映射到正确的地址和位宽。手写地址不仅易错,而且跨芯片无法复用。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- LED 不亮但编译烧录都成功 → 寄存器偏移或时钟使能没配对
- 串口输出乱码 →
UART0在 SAMD21 是SERCOM0,但寄存器基址和波特率计算方式与 ESP32 不同
实操建议:
- 始终从
machine.开始查引脚常量,如machine.LED或machine.UART0 - 配置外设前必须调用
Configure(),它会自动处理时钟使能、复位释放、引脚复用(MUX)设置 - 读写寄存器优先用
machine提供的方法,比如led.High()而非手动改PORT.OUTSET
没有 goroutine 调度器,time.Sleep 和 channel 用法受限
TinyGo 的 runtime 不含抢占式调度器,goroutine 只在明确让出时才切换(比如 time.Sleep、channel 操作),且所有 goroutine 共享一个栈空间。这意味着不能靠起一堆 goroutine 来“并发”,而要靠状态机或中断驱动。
性能与兼容性影响:
-
time.Sleep(1 * time.Second)实际依赖 SysTick 或其他定时器中断,若中断被禁用或频率不对,就会卡死 - 无缓冲
channel只能用于同步,不能存数据;带缓冲 channel 的容量必须在编译期确定,且占用 RAM —— 一块 ATSAMD21 只有 32KB RAM,放不下几十个 int -
select语句只支持case 和 <code>default,不支持case ch 发送分支(除非通道已知非阻塞)
调试时 printf 是奢望,得靠 LED + UART + OpenOCD
没有标准输出,fmt.Println 默认编译失败;即使重定向到 UART,也容易因缓冲或波特率错导致丢字、卡死。真实调试靠的是组合手段。
实操建议:
- 初始化 UART 后立刻发一个固定字符串(如
"boot ok"),确认时钟和引脚正确——这是最廉价的“hello world”验证 - 用 LED 快闪表示进入中断、慢闪表示主循环运行,比加断点更可靠
- 烧录时用
tinygo flash -target=pico -ldflags="-X=main.debug=1"配合条件编译,避免调试代码污染生产固件 - 真正查寄存器值或变量状态,得接 SWD/JTAG,用
openocd+gdb单步,TinyGo 生成的 ELF 支持 DWARF 符号
最容易被忽略的是:不同芯片的 Flash 页擦除大小、写保护机制、复位向量位置都不一样,哪怕同一行 tinygo flash 命令,在 RP2040 和 nRF52840 上触发的底层流程完全不同。别迷信一键命令,得看它实际调用了哪个 OpenOCD config 和 flash algo。


















