diff --git a/GNUmakefile b/GNUmakefile index f3801988..730bd99f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -777,6 +777,8 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=pca10040 -serial=none examples/echo @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=pca10040 -serial=rtt examples/echo + @$(MD5SUM) test.hex $(TINYGO) build -o test.nro -target=nintendoswitch examples/serial @$(MD5SUM) test.nro $(TINYGO) build -size short -o test.hex -target=pca10040 -opt=0 ./testdata/stdlib.go diff --git a/compileopts/options.go b/compileopts/options.go index 4440f4cf..bd99179c 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -10,7 +10,7 @@ import ( var ( validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"} validSchedulerOptions = []string{"none", "tasks", "asyncify"} - validSerialOptions = []string{"none", "uart", "usb"} + validSerialOptions = []string{"none", "uart", "usb", "rtt"} validPrintSizeOptions = []string{"none", "short", "full"} validPanicStrategyOptions = []string{"print", "trap"} validOptOptions = []string{"none", "0", "1", "2", "s", "z"} diff --git a/main.go b/main.go index f9eb4eff..1a9e624e 100644 --- a/main.go +++ b/main.go @@ -532,7 +532,7 @@ func Flash(pkgName, port string, options *compileopts.Options) error { return fmt.Errorf("unknown flash method: %s", flashMethod) } if options.Monitor { - return Monitor(result.Executable, "", options) + return Monitor(result.Executable, "", config) } return nil } @@ -1740,7 +1740,9 @@ func main() { fmt.Printf("%s %4s %4s %s\n", s.Name, s.VID, s.PID, s.Target) } } else { - err := Monitor("", *port, options) + config, err := builder.NewConfig(options) + handleCompilerError(err) + err = Monitor("", *port, config) handleCompilerError(err) } case "targets": diff --git a/monitor.go b/monitor.go index 571ff2c6..7b9c8964 100644 --- a/monitor.go +++ b/monitor.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "debug/dwarf" "debug/elf" "debug/macho" @@ -9,6 +10,7 @@ import ( "fmt" "go/token" "io" + "net" "os" "os/signal" "regexp" @@ -17,7 +19,6 @@ import ( "time" "github.com/mattn/go-tty" - "github.com/tinygo-org/tinygo/builder" "github.com/tinygo-org/tinygo/compileopts" "go.bug.st/serial" @@ -25,44 +26,151 @@ import ( ) // Monitor connects to the given port and reads/writes the serial port. -func Monitor(executable, port string, options *compileopts.Options) error { - config, err := builder.NewConfig(options) - if err != nil { - return err - } +func Monitor(executable, port string, config *compileopts.Config) error { + const timeout = time.Second * 3 + var exit func() // function to be called before exiting + var serialConn io.ReadWriter - wait := 300 - for i := 0; i <= wait; i++ { - port, err = getDefaultPort(port, config.Target.SerialPort) + if config.Options.Serial == "rtt" { + // Use the RTT interface, which is documented (in part) here: + // https://wiki.segger.com/RTT + + // Try to find the "machine.rttSerialInstance" symbol, which is the RTT + // control block. + file, err := elf.Open(executable) if err != nil { - if i < wait { - time.Sleep(10 * time.Millisecond) - continue + return fmt.Errorf("could not open ELF file to determine RTT control block: %w", err) + } + defer file.Close() + symbols, err := file.Symbols() + if err != nil { + return fmt.Errorf("could not read ELF symbol table to determine RTT control block: %w", err) + } + var address uint64 + for _, symbol := range symbols { + if symbol.Name == "machine.rttSerialInstance" { + address = symbol.Value + break } + } + if address == 0 { + return fmt.Errorf("could not find RTT control block in ELF file") + } + + // Start an openocd process in the background. + args, err := config.OpenOCDConfiguration() + if err != nil { return err } - break - } - - br := options.BaudRate - if br <= 0 { - br = 115200 - } - - wait = 300 - var p serial.Port - for i := 0; i <= wait; i++ { - p, err = serial.Open(port, &serial.Mode{BaudRate: br}) + args = append(args, + "-c", fmt.Sprintf("rtt setup 0x%x 16 \"SEGGER RTT\"", address), + "-c", "init", + "-c", "rtt server start 0 0") + cmd := executeCommand(config.Options, "openocd", args...) + stderr, err := cmd.StderrPipe() if err != nil { - if i < wait { - time.Sleep(10 * time.Millisecond) - continue - } return err } - break + cmd.Stdout = os.Stdout + err = cmd.Start() + if err != nil { + return err + } + defer cmd.Process.Kill() + exit = func() { + // Make sure the openocd process is terminated at exit. + // This does not happen through the defer above when exiting through + // os.Exit. + cmd.Process.Kill() + } + + // Read the stderr, which logs various important messages we need. + r := bufio.NewReader(stderr) + var telnet net.Conn + var timeoutAt time.Time + for { + // Read the next line from the openocd process. + lineBytes, err := r.ReadBytes('\n') + if err != nil { + return err + } + line := string(lineBytes) + + if line == "Info : rtt: No control block found\n" { + // Message that is sent back when OpenOCD can't find the control + // block after a 'rtt start' message. + if time.Now().After(timeoutAt) { + return fmt.Errorf("RTT timeout (could not locate RTT control block at 0x%08x)", address) + } + time.Sleep(time.Millisecond * 100) + telnet.Write([]byte("rtt start\r\n")) + } else if strings.HasPrefix(line, "Info : Listening on port") { + // We need two different ports for controlling OpenOCD + // (typically port 4444) and the RTT channel 0 socket (arbitrary + // port). + var port int + var protocol string + fmt.Sscanf(line, "Info : Listening on port %d for %s connections\n", &port, &protocol) + if protocol == "telnet" && telnet == nil { + // Connect to the "telnet" command line interface. + telnet, err = net.Dial("tcp4", fmt.Sprintf("localhost:%d", port)) + if err != nil { + return err + } + // Tell OpenOCD to start scanning for the RTT control block. + telnet.Write([]byte("rtt start\r\n")) + // Also make sure we will time out if the control block just + // can't be found. + timeoutAt = time.Now().Add(timeout) + } else if protocol == "rtt" { + // Connect to the RTT channel, for both stdin and stdout. + conn, err := net.Dial("tcp4", fmt.Sprintf("localhost:%d", port)) + if err != nil { + return err + } + serialConn = conn + } + } else if strings.HasPrefix(line, "Info : rtt: Control block found at") { + // Connection established! + break + } + } + } else { // -serial=uart or -serial=usb + var err error + wait := 300 + for i := 0; i <= wait; i++ { + port, err = getDefaultPort(port, config.Target.SerialPort) + if err != nil { + if i < wait { + time.Sleep(10 * time.Millisecond) + continue + } + return err + } + break + } + + br := config.Options.BaudRate + if br <= 0 { + br = 115200 + } + + wait = 300 + var p serial.Port + for i := 0; i <= wait; i++ { + p, err = serial.Open(port, &serial.Mode{BaudRate: br}) + if err != nil { + if i < wait { + time.Sleep(10 * time.Millisecond) + continue + } + return err + } + serialConn = p + break + } + defer p.Close() } - defer p.Close() tty, err := tty.Open() if err != nil { @@ -77,6 +185,9 @@ func Monitor(executable, port string, options *compileopts.Options) error { go func() { <-sig tty.Close() + if exit != nil { + exit() + } os.Exit(0) }() @@ -88,7 +199,7 @@ func Monitor(executable, port string, options *compileopts.Options) error { buf := make([]byte, 100*1024) var line []byte for { - n, err := p.Read(buf) + n, err := serialConn.Read(buf) if err != nil { errCh <- fmt.Errorf("read error: %w", err) return @@ -124,7 +235,7 @@ func Monitor(executable, port string, options *compileopts.Options) error { if r == 0 { continue } - p.Write([]byte(string(r))) + serialConn.Write([]byte(string(r))) } }() diff --git a/src/machine/serial-rtt.go b/src/machine/serial-rtt.go new file mode 100644 index 00000000..62c3eb9e --- /dev/null +++ b/src/machine/serial-rtt.go @@ -0,0 +1,144 @@ +//go:build baremetal && serial.rtt + +// Implement Segger RTT support. +// This is mostly useful for targets that only have a debug connection +// available, and no serial output (or input). It is somewhat like semihosting, +// but not unusably slow. +// It was originally specified by Segger, but support is available in OpenOCD +// for at least the DAPLink debuggers so I assume it works on any SWD debugger. + +package machine + +import ( + "runtime/interrupt" + "runtime/volatile" + "unsafe" +) + +// This symbol name is known by the compiler, see monitor.go. +var rttSerialInstance rttSerial + +var Serial = &rttSerialInstance + +func InitSerial() { + Serial.Configure(UARTConfig{}) +} + +const ( + // Some constants, see: + // https://github.com/SEGGERMicro/RTT/blob/master/RTT/SEGGER_RTT.h + + rttMaxNumUpBuffers = 1 + rttMaxNumDownBuffers = 1 + rttBufferSizeUp = 1024 + rttBufferSizeDown = 16 + + rttModeNoBlockSkip = 0 + rttModeNoBlockTrim = 1 + rttModeBlockIfFifoFull = 2 +) + +// The debugger knows about the layout of this struct, so it must not change. +// This is SEGGER_RTT_CB. +type rttControlBlock struct { + id [16]volatile.Register8 + maxNumUpBuffers int32 + maxNumDownBuffers int32 + buffersUp [rttMaxNumUpBuffers]rttBuffer + buffersDown [rttMaxNumDownBuffers]rttBuffer +} + +// Up or down buffer. +// This is SEGGER_RTT_BUFFER_UP and SEGGER_RTT_BUFFER_DOWN. +type rttBuffer struct { + name *byte + buffer *volatile.Register8 + bufferSize uint32 + writeOffset volatile.Register32 + readOffset volatile.Register32 + flags uint32 +} + +// Static buffers, for the default up and down buffer. +var ( + rttBufferUpData [rttBufferSizeUp]volatile.Register8 + rttBufferDownData [rttBufferSizeDown]volatile.Register8 +) + +type rttSerial struct { + rttControlBlock +} + +func (s *rttSerial) Configure(config UARTConfig) error { + s.maxNumUpBuffers = rttMaxNumUpBuffers + s.maxNumDownBuffers = rttMaxNumDownBuffers + + s.buffersUp[0].name = &[]byte("Terminal\x00")[0] + s.buffersUp[0].buffer = &rttBufferUpData[0] + s.buffersUp[0].bufferSize = rttBufferSizeUp + s.buffersUp[0].flags = rttModeNoBlockSkip + + s.buffersDown[0].name = &[]byte("Terminal\x00")[0] + s.buffersDown[0].buffer = &rttBufferDownData[0] + s.buffersDown[0].bufferSize = rttBufferSizeDown + s.buffersDown[0].flags = rttModeNoBlockSkip + + id := "SEGGER RTT" + for i := 0; i < len(id); i++ { + s.id[i].Set(id[i]) + } + + return nil +} + +func (b *rttBuffer) writeByte(c byte) { + state := interrupt.Disable() + readOffset := b.readOffset.Get() + writeOffset := b.writeOffset.Get() + newWriteOffset := writeOffset + 1 + if newWriteOffset == b.bufferSize { + newWriteOffset = 0 + } + if newWriteOffset != readOffset { + unsafe.Slice(b.buffer, b.bufferSize)[writeOffset].Set(c) + b.writeOffset.Set(newWriteOffset) + } + interrupt.Restore(state) +} + +func (b *rttBuffer) readByte() byte { + readOffset := b.readOffset.Get() + writeOffset := b.writeOffset.Get() + for readOffset == writeOffset { + readOffset = b.readOffset.Get() + } + c := unsafe.Slice(b.buffer, b.bufferSize)[readOffset].Get() + b.readOffset.Set(readOffset + 1) + return c +} + +func (b *rttBuffer) buffered() int { + readOffset := b.readOffset.Get() + writeOffset := b.writeOffset.Get() + return int((writeOffset - readOffset) % rttBufferSizeDown) +} + +func (s *rttSerial) WriteByte(b byte) error { + s.buffersUp[0].writeByte(b) + return nil +} + +func (s *rttSerial) ReadByte() (byte, error) { + return s.buffersDown[0].readByte(), errNoByte +} + +func (s *rttSerial) Buffered() int { + return s.buffersDown[0].buffered() +} + +func (s *rttSerial) Write(data []byte) (n int, err error) { + for _, v := range data { + s.WriteByte(v) + } + return len(data), nil +}