Serial Monitor
When you run a Go program on a desktop computer, you can use the fmt.Print()
,
fmt.Println()
, and fmt.Printf()
functions from the Go standard fmt
package to print strings and numbers to the terminal
program on the desktop computer. The Go language also supports the low-level
print() and println() built-in
functions to print to the terminal.
A TinyGo program running on a microcontroller can use those same functions to
print strings and numbers to its serial monitor port and have them appear on the
terminal program on the host computer. By default, the fmt
functions and the
print()/println()
functions are configured to send to the machine.Serial
object of the microcontroller.
On some microcontrollers, the machine.Serial
object is configured to send to
the
UART
chip, which is often wired to a USB-to-serial
adapter chip on the dev
board. The adapter chip converts the serial bits into USB packets to the host
computer. On other microcontrollers, the USB controller is built directly into
the microcontroller SoC. The
machine.Serial
object on these microcontrollers is configured to send to the
USB bus directly, instead of going through a USB-to-serial adapter.
In the context of this tutorial, it does not matter whether the microcontroller uses a UART controller or a USB controller. In both cases, the microcontroller will appear as a serial device on the host computer which can communicate via applications that read from and write to the serial port on the host computer.
Serial Output
Using fmt.Print()
and fmt.Println()
Here is a sample program that writes a line every second to the machine.Serial
port:
package main
import (
"fmt"
"time"
)
func main() {
count := 0
for {
fmt.Println(count, ": Hello, World")
time.Sleep(time.Millisecond * 1000)
count++
}
}
This can be flashed to the microcontroller using the tinygo flash
command
described in the Blinky tutorial.
Using print()
and println()
One problem with the above program is that fmt
is a large package that
consumes substantial amount of flash memory on a microcontroller. The built-in
functions print()
and println()
consume far less resources. The above
program can be written like this:
package main
import (
"time"
)
func main() {
count := 0
for {
println(count, ": Hello, World")
time.Sleep(time.Millisecond * 1000)
count++
}
}
An estimate of the flash memory consumption can be printed by the TinyGo compiler using the -size flag. Here is a table that shows the 2 versions of the program above for some microcontrollers that I have readily available:
+-----------------+---------------+-----------+
| Board Type | fmt.Println() | println() |
+-----------------+---------------+-----------+
| Arduino Zero | 43532 | 7328 |
| Seeeduino Xiao | 43532 | 7388 |
| STM32 BluePill | 41756 | 6296 |
| ESP8266 D1 Mini | 44961 | 3588 |
| ESP32 | 42410 | 3335 |
+-----------------+---------------+-----------+
The fmt
package increases flash memory consumption by 35 kB to 40 kB. On some
microcontrollers with limited amount of flash memory, (e.g. the STM32 Blue Pill
with 64 kB of flash, the Arduino Zero or Seeeduino Xiao both with 256 kB of
flash), it may be worth avoiding the overhead of the fmt
package by using the
built-in print()
and println()
instead.
Serial Monitor on Host Computer
To see the output of the serial port from the microcontroller, we need to run a
serial monitor application on the host computer. There are many ways to do this,
but the easiest is probably the tinygo monitor
subcommand which is built
directly into the tinygo
program itself.
monitor
subcommand
After flashing the program above, run the tinygo monitor
program to see the
output every second from the microcontroller:
$ tinygo monitor
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
4 : Hello, World
5 : Hello, World
[...]
In this example, the serial monitor missed the first 4 lines of “Hello, World” (0 to 3) because the program started to print those lines immediately after flashing, but before the serial monitor was connected.
-monitor
flag
It is often useful to automatically start the monitor immediately after flashing
your program to the microcontroller. The tinygo flash
command takes an
optional -monitor
flag to accomplish this:
$ tinygo flash -target=xiao -monitor
On some microcontrollers, the -monitor
flag fails with the following error
message because the monitor starts too quickly:
$ tinygo flash -target=arduino-zero -monitor
[...]
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
error: read error: Port has been closed
If this happens, you can chain the flash
and monitor
subcommands manually,
with a 1 or 2-second delay between the two commands. On Linux or MacOS, the
command invocation looks like this:
$ tinygo flash -target=arduino-zero && sleep 1 && tinygo monitor
(The &&
separator runs the next command only if the previous command completed
without errors. This is safer than using the semicolon ;
separator because the
semicolon continues to execute commands even if the previous command returned an
error code.)
Baud Rate
The default baud rate of the
serial port for almost all microcontrollers supported by TinyGo is 115200. The
exceptions are boards using the AVR processors (Arduino Nano, Arduino Mega 1280, Arduino Mega 2560). On these, the serial port
is set to 9600, so you need to override the baud rate of tinygo monitor
like
this:
$ tinygo monitor -baudrate=9600
You can combine the flash
subcommand, the -monitor
flag, and the -baudrate
flag into a single invocation like this:
$ tinygo flash -target arduino-nano -monitor -baudrate 9600
(Notice that the =
after each flag has been replaced with a space. It’s an
alternative syntax that some people prefer because a space is easier to type
than an equal sign =
.)
Serial Port on Host
The microcontroller will be assigned a serial port on the host computer. If you
have only a single microcontroller attached, you will normally not need to worry
about what these serial ports are called. The tinygo monitor
will
automatically figure out which serial port to use.
On Linux machines, the serial port will have a USB
prefix or an ACM
prefix
like this:
/dev/ttyUSB0
/dev/ttyACM0
On MacOS machines, the serial port will look like this:
/dev/cu.usbserial-1420
/dev/cu.usbmodem6D8733AC53571
On Windows machines, the serial port looks something like:
COM1
COM31
Multiple Microcontrollers
If you have more than one microcontroller attached to the host computer, the
tinygo flash
and tinygo monitor
subcommands can sometimes figure out which
port it is using, but they will sometimes print out an error message, like this:
$ tinygo flash -target arduino-nano
error: multiple serial ports available - use -port flag,
available ports are /dev/ttyACM0, /dev/ttyUSB0
You then need to supply the -port
flag to identify the microcontroller that
you want to flash and monitor:
$ tinygo flash -target=arduino-nano -port=/dev/ttyUSB0
$ tinygo monitor -port=/dev/ttyUSB0 -baudrate=9600
Sometimes it is possible to combine the two commands into a single command even in the presence of multiple microcontrollers:
$ tinygo flash -target xiao -monitor
But sometimes, combining flash
and monitor
into a single command does not
work. In that case, you can issue the flash
and monitor
commands separately.
But it is often easier to just pull out the extra microcontroller(s) so that
only a single board is connected to the host computer.
$ tinygo flash -target=arduino-nano -monitor
error: multiple serial ports available - use -port flag,
available ports are /dev/ttyACM0, /dev/ttyUSB0
$ tinygo flash -target=arduino-nano -monitor -port=/dev/ttyUSB0 -baudrate=9600
[...]
avrdude: 4238 bytes of flash verified
avrdude done. Thank you.
[...]
error: multiple serial ports available - use -port flag,
available ports are /dev/ttyACM0, /dev/ttyUSB0
Serial Input
Occasionally it is useful to send characters from the host computer to the
microcontroller. The following program reads a single byte from the
machine.Serial
object and prints the character back to the host computer.
The caveat is that the Serial.ReadByte()
feature is not currently
implemented on every microcontroller supported by TinyGo. For example, the
following program does not work on the ESP32 or the ESP8266.
package main
import (
"machine"
"time"
)
func main() {
time.Sleep(time.Millisecond * 2000)
println("Reading from the serial port...")
for {
c, err := machine.Serial.ReadByte()
if err == nil {
if c < 32 {
// Convert nonprintable control characters to
// ^A, ^B, etc.
machine.Serial.WriteByte('^')
machine.Serial.WriteByte(c + '@')
} else if c >= 127 {
// Anything equal or above ASCII 127, print ^?.
machine.Serial.WriteByte('^')
machine.Serial.WriteByte('?')
} else {
// Echo the printable character back to the
// host computer.
machine.Serial.WriteByte(c)
}
}
// This assumes that the input is coming from a keyboard
// so checking 120 times per second is sufficient. But if
// the data comes from another processor, the port can
// theoretically receive as much as 11000 bytes/second
// (115200 baud). This delay can be removed and the
// Serial.Read() method can be used to retrieve
// multiple bytes from the receive buffer for each
// iteration.
time.Sleep(time.Millisecond * 8)
}
}
You can flash this program to the microcontroller (in this example, a SAMD21 M0+ clone that emulates an Arduino Zero), and fire up the monitor like this:
$ tinygo flash -target=arduino-zero
$ tinygo monitor
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
Reading from the serial port...
abcdef^A^B^D^E^F^G^H^I^J^K^L^M^N^O^P^R^T^U^V^W^X^Y^^^[^]^_^?
Type a few characters in the tinygo monitor
, for example “abcdef”. You should
see the characters echoed back by the microcontroller, as shown above. If you
type a nonprintable control
characters, these are
echoed back as 2 characters: the caret character ^
and a letter representing
the control character. For example, typing Control-P prints ^P
.
Of the 32 possible control characters, some of them are intercepted by the
tinygo monitor
itself instead of being sent to the microcontroller:
- Control-C: terminates the
tinygo monitor
- Control-Z: suspends the
tinygo monitor
and drops back into shell - Control-\: terminates the
tinygo monitor
with a stack trace - Control-S: flow control, suspends output to the console
- Control-Q: flow control, resumes output to the console
- Control-@: thrown away by
tinygo monitor
Alternative Serial Monitors
There are many alternative serial monitor programs that can be used instead of
tinygo monitor
. The setup is slightly more complicated because you will need
to supply the serial port and baud rate of the microcontroller as described in
the Serial Port on Host and Baud Rate
subsections above.
Arduino IDE
The Arduino IDE contains its own serial
monitor. You may choose to use that instead. You need to set the serial port
(something like /dev/ttyUSB0
on Linux, or /dev/cu.usbserial-1420
on MacOS),
and set the baud rate to 115200 (or 9600 on AVR processors).
pyserial
The pyserial is a
Python library that comes with its own serial monitor. Setting up a python3
environment is a complex topic that is beyond the scope of this document. But if
you are able to install python3
and pip3
, you can install pyserial
and use
its built-in miniterm
tool roughly like this:
$ python3 -m pip install --user pyserial
$ python3 -m serial.tools.miniterm /dev/ttyUSB0 115200
Another useful feature of pyserial
is the list_ports
command:
$ python3 -m serial.tools.list_ports
/dev/ttyACM0
/dev/ttyS0
/dev/ttyUSB0
3 ports found
This is useful when you plug in a random microcontroller to the USB port, and you cannot remember which serial port it is mapped to.
picocom
The picocom terminal emulator can be installed on both Linux and MacOS. If you are using an Ubuntu flavored Linux, the installation is something like:
$ sudo apt install picocom
On MacOS, most people use Homebrew, and it can be installed like this:
$ brew install picocom
It can be invoked like this:
$ picocom -b 115200 /dev/ttyACM0
port is : /dev/ttyACM0
picocom v3.1
[...]
Type [C-a] [C-h] to see available commands
Terminal ready