scripts: runner: add debug support to nios.py

Signed-off-by: Marti Bolivar <marti.bolivar@linaro.org>
diff --git a/scripts/support/runner/nios2.py b/scripts/support/runner/nios2.py
index b18145a..480e1ed 100644
--- a/scripts/support/runner/nios2.py
+++ b/scripts/support/runner/nios2.py
@@ -2,11 +2,12 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
-'''Runner for NIOS II.'''
+'''Runner for NIOS II, based on quartus-flash.py and GDB.'''
 
 from os import path
+import os
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner, NetworkPortHelper
 
 
 class Nios2BinaryRunner(ZephyrBinaryRunner):
@@ -18,31 +19,62 @@
     #      over the JTAG and the CPU directly boots from __start. CONFIG_XIP
     #      and CONFIG_INCLUDE_RESET_VECTOR must be disabled."
 
-    def __init__(self, hex_, cpu_sof, zephyr_base, debug=False):
+    def __init__(self, hex_name=None, elf_name=None, cpu_sof=None,
+                 zephyr_base=None, gdb=None, tui=None, debug=False):
         super(Nios2BinaryRunner, self).__init__(debug=debug)
-        self.hex_ = hex_
+        self.hex_name = hex_name
+        self.elf_name = elf_name
         self.cpu_sof = cpu_sof
         self.zephyr_base = zephyr_base
+        self.gdb_cmd = [gdb] if gdb is not None else None
+        self.tui_arg = [tui] if tui is not None else []
 
     def replaces_shell_script(shell_script, command):
-        return command == 'flash' and shell_script == 'nios2.sh'
+        return (command in {'flash', 'debug', 'debugserver'} and
+                shell_script == 'nios2.sh')
 
     def create_from_env(command, debug):
         '''Create runner from environment.
 
-        Required:
+        Required for 'flash', 'debug':
 
         - O: build output directory
+
+        Required for 'flash':
+
         - KERNEL_HEX_NAME: name of kernel binary in HEX format
         - NIOS2_CPU_SOF: location of the CPU .sof data
         - ZEPHYR_BASE: zephyr Git repository base directory
-        '''
-        hex_ = path.join(get_env_or_bail('O'),
-                         get_env_or_bail('KERNEL_HEX_NAME'))
-        cpu_sof = get_env_or_bail('NIOS2_CPU_SOF')
-        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
 
-        return Nios2BinaryRunner(hex_, cpu_sof, zephyr_base, debug=debug)
+        Required for 'debug':
+
+        - KERNEL_ELF_NAME: name of kernel binary in ELF format
+        - GDB: GDB executable
+
+        Optional for 'debug':
+
+        - TUI: one additional argument to GDB (e.g. -tui)
+        '''
+        cpu_sof = os.environ.get('NIOS2_CPU_SOF', None)
+        zephyr_base = os.environ.get('ZEPHYR_BASE', None)
+
+        o = os.environ.get('O', None)
+        hex_ = os.environ.get('KERNEL_HEX_NAME', None)
+        elf = os.environ.get('KERNEL_ELF_NAME', None)
+        hex_name = None
+        elf_name = None
+        if o is not None:
+            if hex_ is not None:
+                hex_name = path.join(o, hex_)
+            if elf is not None:
+                elf_name = path.join(o, elf)
+
+        gdb = os.environ.get('GDB', None)
+        tui = os.environ.get('TUI', None)
+
+        return Nios2BinaryRunner(hex_name=hex_name, elf_name=elf_name,
+                                 cpu_sof=cpu_sof, zephyr_base=zephyr_base,
+                                 gdb=gdb, tui=tui, debug=debug)
 
     def run(self, command, **kwargs):
         if command not in {'flash', 'debug', 'debugserver'}:
@@ -54,12 +86,54 @@
             self.debug_debugserver(command, **kwargs)
 
     def flash(self, **kwargs):
+        sof_msg = (
+            'Cannot flash; '
+            'Please set NIOS2_CPU_SOF variable to location of CPU .sof data')
+
+        if self.zephyr_base is None:
+            raise ValueError('Cannot flash; ZEPHYR_BASE is missing.')
+        if self.cpu_sof is None:
+            raise ValueError(sof_msg)
+        if self.hex_name is None:
+            raise ValueError('Cannot flash; .hex binary name is missing')
+
         cmd = [path.join(self.zephyr_base, 'scripts', 'support',
                          'quartus-flash.py'),
                '--sof', self.cpu_sof,
-               '--kernel', self.hex_]
+               '--kernel', self.hex_name]
 
         self.check_call(cmd)
 
-    def debug_debugserver(command, **kwargs):
-        raise NotImplementedError()
+    def print_gdbserver_message(self, gdb_port):
+        print('Nios II GDB server running on port {}'.format(gdb_port))
+
+    def debug_debugserver(self, command, **kwargs):
+        # Per comments in the shell script, the NIOSII GDB server
+        # doesn't exit gracefully, so it's better to explicitly search
+        # for an unused port. The script picks a random value in
+        # between 1024 and 49151, but we'll start with the
+        # "traditional" 3333 choice.
+        gdb_start = 3333
+        nh = NetworkPortHelper()
+        gdb_port = nh.get_unused_ports([gdb_start])[0]
+
+        server_cmd = (['nios2-gdb-server',
+                       '--tcpport', str(gdb_port),
+                       '--stop', '--reset-target'])
+
+        if command == 'debugserver':
+            self.print_gdbserver_message(gdb_port)
+            self.check_call(server_cmd)
+        else:
+            if self.elf_name is None:
+                raise ValueError('Cannot debug; elf is missing')
+            if self.gdb_cmd is None:
+                raise ValueError('Cannot debug; no gdb specified')
+
+            gdb_cmd = (self.gdb_cmd +
+                       self.tui_arg +
+                       [self.elf_name,
+                        '-ex', 'target remote :{}'.format(gdb_port)])
+
+            self.print_gdbserver_message(gdb_port)
+            self.run_server_and_client(server_cmd, gdb_cmd)