1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
.. faq:
Frequently Asked Questions
==========================
What is TinyGo exactly?
-----------------------
A new compiler and a new runtime implementation.
Specifically:
* A new compiler using (mostly) the standard library to parse Go programs and
using LLVM to optimize the code and generate machine code for the target
architecture.
* A new runtime library that implements some compiler intrinsics, like a
memory allocator, a scheduler, and operations on strings. Also, some
packages that are strongly connected to the runtime like the ``sync``
package and the ``reflect`` package have been or will be re-implemented for
use with this new compiler.
Why a new compiler?
-------------------
Why not modify the existing compiler to produce binaries for microcontrollers?
There are several reasons for this:
* The standard Go compiler (``gc``) does not support instruction sets as used
on microcontrollers:
* The Thumb instruction set is unsupported, but it should be possible to
add support for it as it already has an ARM backend.
* The AVR instruction set (as used in the Arduino Uno) is unsupported and
unlikely to be ever supported.
Of course, it is possible to use ``gccgo``, but that has different problems
(see below).
* The runtime is really big. A standard 'hello world' on a desktop PC produces
a binary of about 1MB, even when using the builtin ``println`` function and
nothing else. All this overhead is due to the runtime. Of course, it may be
possible to use a different runtime with the same compiler but that will be
kind of painful as the exact ABI as used by the compiler has to be matched,
limiting optimization opportunities (see below).
* The compiler is optimized for speed, not for code size or memory
consumption (which are usually far more important on MCUs). This results in
design choices like allocating memory on every value → interface conversion
while TinyGo sacrifices some performance for reduced GC pressure.
* With the existing Go libraries for parsing Go code and the pretty awesome
LLVM optimizer/backend it is relatively easy to get simple Go programs
working with a very small binary size. Extra features can be added where
needed in a pay-as-you-go manner similar to C++ avoiding their cost when
unused. Most programs on microcontrollers are relatively small so a
not-complete compiler is still useful.
* The standard Go compilers do not allocate global variables as static data,
but as zero-initialized data that is initialized during program startup.
This is not a big deal on desktop computers but prevents allocating these
values in flash on microcontrollers. Part of this is due to how the
`language specification defines package initialization
<https://golang.org/ref/spec#Package_initialization>`_, but this can be
worked around to a large extent.
* The standard Go compilers do a few special things for CGo calls. This is
necessary because only Go code can use the (small) Go stack while C code
will need a much bigger stack. A new compiler can avoid this limitation if
it ensures stacks are big enough for C, greatly reducing the C ↔ Go calling
overhead.
`At one point <https://github.com/aykevl/tinygo-gccgo>`_, a real Go compiler
had been used to produce binaries for various platforms, and the result was
painful enough to start writing a new compiler:
* The ABI was fixed, so could not be optimized for speed. Also, the ABI
didn't seem to be documented anywhere.
* Working arount limitations in the ``go`` toolchain was rather burdensome
and quite a big hack.
* The binaries produced were quite bloated, for various reasons:
* The Go calling convention places all arguments on the stack. Due to
this, stack usage was really bad and code size was bigger than it
needed to be.
* Global initialization was very inefficient, see above.
* There seemed to be no way to optimize across packages.
Why Go instead of Rust?
-----------------------
Rust is another "new" and safer language that is now made ready for embedded
processors. There is `a fairly active community around it
<https://rust-embedded.github.io/blog/>`_.
However, apart from personal language preference, Go has a few advantages:
* Subjective, but in general Go is `easier to learn
<https://matthias-endler.de/2017/go-vs-rust/>`_. Rust is in general far more
complicated than Go, with difficult-to-grasp ownership rules, traits,
generics, etc. Go prides itself on being a simple and slightly dumb
language, sacrificing some expressiveness for readability.
* Built-in support for concurrency with goroutines and channels that do not
rely on a particular implementation threads. This avoids the need for a
custom `RTOS-like framework <https://blog.japaric.io/rtfm-v2/>`_ or a
`full-blown RTOS <https://github.com/rust-embedded/wg/issues/45>`_ with the
associated API one has to learn. In Go, everything is handled by goroutines
which are built into the language itself.
* A batteries-included standard library that consists of loosely-coupled
packages. Rust uses a monolithic standard library that is currently unusable
on bare-metal, while the Go standard library is much more loosely coupled so
is more likely to be (partially) supported. Also, non-standard packages in
Go do not have to be marked with something like ``#![no_std]`` to be usable
on bare metal. Note: most standard library packages cannot yet be compiled,
but this situation will hopefully improve in the future.
At the same time, Rust has other advantages:
* Unlike Go, Rust does not have a garbage collector by default and carefully
written Rust code can avoid most or all uses of the heap. Go relies heavily
on garbage collection and often implicitly allocates memory on the heap.
* Rust has stronger memory-safety guarantees.
* In general, Rust is more low-level and easier to support on a
microcontroller. Of course, this doesn't mean one shouldn't try to run Go on
a microcontroller, just that it is more difficult. When even dynamic
languages like `Python <https://micropython.org/>`_, `Lua
<https://nodemcu.readthedocs.io/en/master/>`_ and `JavaScript
<https://www.espruino.com/>`_ can run on a microcontroller, then certainly
Go can.
.. _faq-esp:
What about the ESP8266/ESP32?
-----------------------------
These chips use the rather obscure Xtensa instruction set. While a port of GCC
exists and Espressif provides precompiled GNU toolchains, there is no support
yet in LLVM (although there have been `multiple attempts
<http://lists.llvm.org/pipermail/llvm-dev/2018-July/124789.html>`_).
There are two ways these chips might be supported in the future, and both will
take a considerable amount of work:
* The compiled LLVM IR can be converted into (ugly) C and then be compiled
with a supported C compiler (like GCC for Xtensa). This has been `done
before <https://github.com/JuliaComputing/llvm-cbe>`_ so should be doable.
* One of the work-in-progress LLVM backends can be worked on to get it in a
usable state. If this is finished, a true TinyGo port is possible.
|