aboutsummaryrefslogtreecommitdiffhomepage
path: root/docs/microcontrollers.rst
blob: 4ddc5c7f8fa3ff30015ba86a89c8f52b6abb11c2 (plain)
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
.. _microcontrollers:

.. highlight:: go


Go on microcontrollers
======================

TinyGo was designed to run on microcontrollers, but the Go language wasn't.
This means there are a few challenges to writing Go code for microcontrollers.

Microcontrollers have very little RAM and execute code directly from flash.
Also, constant globals are generally put in flash whenever possible. The Go
language itself heavily relies on garbage collection so care must be taken to
avoid dynamic memory allocation.


Heap allocation
---------------

Many operations in Go rely on heap allocation. Some of these heap allocations
are optimized away, but not all of them. Also, TinyGo does not yet contain a
garbage collector so heap allocation must be avoided whenever possible outside
of initialization code.

These operations currently do heap allocations:

  * Taking the pointer of a local variable. This will result in a heap
    allocation, unless the compiler can see the resulting pointer never
    escapes. This causes a heap allocation::

        var global *int

        func foo() {
            i := 3
            global = &i
        }

    This does not cause a heap allocation::

        func foo() {
            i := 3
            bar(&i)
        }

        func bar(i *int) {
            println(*i)
        }

  * Converting between ``string`` and ``[]byte``. In general, this causes a
    heap allocation because one is constant while the other is not: for
    example, a ``[]byte`` is not allowed to write to the underlying buffer of a
    ``string``. However, there is an optimization that avoids a heap allocation
    when converting a string to a ``[]byte`` when the compiler can see the
    slice is never written to. For example, this ``WriteString`` function does
    not cause a heap allocation::

        func WriteString(s string) {
            Write([]byte(s))
        }

        func Write(buf []byte) {
            for _, c := range buf {
                WriteByte(c)
            }
        }

  * Converting a ``byte`` or ``rune`` into a ``string``. This operation is
    actually a conversion from a Unicode code point into a single-character
    string so is similar to the previous point.

  * Concatenating strings, unless one of them is zero length.

  * Creating an interface with a value larger than a pointer. Interfaces in Go
    are not a zero-cost abstraction and should be used carefully on
    microcontrollers.

  * Closures where the collection of shared variables between the closure and
    the main function is larger than a pointer.

  * Creating and modifying maps. Maps have *very* little support at the moment
    and should not yet be used. They exist mostly for compatibility with some
    standard library packages.

  * Starting goroutines. There is limited support for goroutines and currently
    they are not at all efficient. Also, there is no support for channels yet
    so their usefulness is limited.


The ``volatile`` keyword
------------------------

Go does not have the ``volatile`` keyword like C/C++. This keyword is
unnecessary in most desktop use cases but is required for memory mapped I/O on
microcontrollers and interrupt handlers. As a workaround, any variable of a
type annotated with the ``//go:volatile`` pragma will be marked volatile. For
example::

    //go:volatile
    type volatileBool bool

    var isrFlag volatileBool

This is a workaround for a limitation in the Go language and should at some
point be replaced with something else.


Inline assembly
---------------

The device-specific packages like ``device/avr`` and ``device/arm`` provide
``Asm`` functions which you can use to write inline assembly::

    arm.Asm("wfi")

You can also pass parameters to the inline assembly::

    var result int32
    arm.AsmFull(`
        lsls  {value}, #1
        str   {value}, {result}
    `, map[string]interface{}{
        "value":  42,
        "result": &result,
    })
    println("result:", result)

In general, types are autodetected. That is, integer types are passed as raw
registers and pointer types are passed as memory locations. This means you can't
easily do pointer arithmetic. To do that, convert a pointer value to a
``uintptr``.

Inline assembly support is expected to change in the future and may change in a
backwards-incompatible manner.


Harvard architectures (AVR)
---------------------------

The AVR architecture is a modified Harvard architecture, which means that flash
and RAM live in different address spaces. In practice, this means that any
given pointer may either point to RAM or flash, but this is not visible from
the pointer itself.

To get TinyGo to work on the Arduino, which uses the AVR architecutre, all
global variables (which include string constants!) are marked non-constant and
thus are stored in RAM and all pointer dereferences assume that pointers point
to RAM. At some point this should be optimized so that obviously constant data
is kept in read-only memory but this optimization has not yet been implemented.