scripts: runner: Introduce gd32isp flash runner

Add GigaDevice ISP console flash runner.  This tool enable uses ROM
bootloader to flash devices using serial port.

The GD32_ISP_Console tool can be found at
  http://www.gd32mcu.com/download/down/document_id/175/path_type/1

Signed-off-by: Gerson Fernando Budke <gerson.budke@atl-electronics.com>
diff --git a/CODEOWNERS b/CODEOWNERS
index df2803d..529ab87 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -645,6 +645,8 @@
 /scripts/twister                          @nashif
 /scripts/series-push-hook.sh              @erwango
 /scripts/west_commands/                   @mbolivar-nordic
+/scripts/west_commands/runners/gd32isp.py @mbolivar-nordic @nandojve
+/scripts/west_commands/tests/test_gd32isp.py @mbolivar-nordic @nandojve
 /scripts/west-commands.yml                @mbolivar-nordic
 /scripts/zephyr_module.py                 @tejlmand
 /scripts/uf2conv.py                       @petejohanson
diff --git a/boards/common/gd32isp.board.cmake b/boards/common/gd32isp.board.cmake
new file mode 100644
index 0000000..426d0a2
--- /dev/null
+++ b/boards/common/gd32isp.board.cmake
@@ -0,0 +1,5 @@
+# Copyright (c) 2021, ATL Electronics
+# SPDX-License-Identifier: Apache-2.0
+
+board_set_flasher_ifnset(gd32isp)
+board_finalize_runner_args(gd32isp)
diff --git a/scripts/west_commands/runners/__init__.py b/scripts/west_commands/runners/__init__.py
index eb4816a..3aa8392 100644
--- a/scripts/west_commands/runners/__init__.py
+++ b/scripts/west_commands/runners/__init__.py
@@ -31,6 +31,7 @@
     'dediprog',
     'dfu',
     'esp32',
+    'gd32isp',
     'hifive1',
     'intel_s1000',
     'jlink',
diff --git a/scripts/west_commands/runners/gd32isp.py b/scripts/west_commands/runners/gd32isp.py
new file mode 100644
index 0000000..4f2a1b1
--- /dev/null
+++ b/scripts/west_commands/runners/gd32isp.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2021, ATL-Electronics
+# SPDX-License-Identifier: Apache-2.0
+
+'''GigaDevice ISP tool (gd32isp) runner for serial boot ROM'''
+
+from runners.core import ZephyrBinaryRunner, RunnerCaps
+
+DEFAULT_GD32ISP_CLI   = 'GD32_ISP_Console'
+DEFAULT_GD32ISP_PORT  = '/dev/ttyUSB0'
+DEFAULT_GD32ISP_SPEED = '57600'
+DEFAULT_GD32ISP_ADDR  = '0x08000000'
+
+class Gd32ispBinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for gd32isp.'''
+
+    def __init__(self, cfg, device,
+                 isp=DEFAULT_GD32ISP_CLI,
+                 port=DEFAULT_GD32ISP_PORT,
+                 speed=DEFAULT_GD32ISP_SPEED,
+                 addr=DEFAULT_GD32ISP_ADDR):
+        super().__init__(cfg)
+        self.device = device
+        self.isp = isp
+        self.port = port
+        self.speed = speed
+        self.addr = addr
+
+    @classmethod
+    def name(cls):
+        return 'gd32isp'
+
+    @classmethod
+    def capabilities(cls):
+        return RunnerCaps(commands={'flash'})
+
+    @classmethod
+    def do_add_parser(cls, parser):
+        # Required:
+        parser.add_argument('--device', required=True,
+                            help='device part number')
+
+        # Optional:
+        parser.add_argument('--isp', default=DEFAULT_GD32ISP_CLI,
+                            help='path to gd32 isp console program')
+        parser.add_argument('--port', default=DEFAULT_GD32ISP_PORT,
+                            help='serial port to use, default is ' +
+                            str(DEFAULT_GD32ISP_PORT))
+        parser.add_argument('--speed', default=DEFAULT_GD32ISP_SPEED,
+                            help='serial port speed to use, default is ' +
+                            DEFAULT_GD32ISP_SPEED)
+        parser.add_argument('--addr', default=DEFAULT_GD32ISP_ADDR,
+                            help='flash address, default is ' +
+                            DEFAULT_GD32ISP_ADDR)
+
+    @classmethod
+    def do_create(cls, cfg, args):
+        return Gd32ispBinaryRunner(cfg,
+                                   device=args.device,
+                                   isp=args.isp,
+                                   port=args.port,
+                                   speed=args.speed,
+                                   addr=args.addr)
+
+    def do_run(self, command, **kwargs):
+        self.require(self.isp)
+        self.ensure_output('bin')
+
+        cmd_flash = [self.isp,
+                     '-c',
+                     '--pn', self.port,
+                     '--br', self.speed,
+                     '--sb', '1',
+                     '-i', self.device,
+                     '-e',
+                     '--all',
+                     '-d',
+                     '--a', self.addr,
+                     '--fn', self.cfg.bin_file]
+
+        self.check_call(cmd_flash)
diff --git a/scripts/west_commands/tests/test_gd32isp.py b/scripts/west_commands/tests/test_gd32isp.py
new file mode 100644
index 0000000..14631af
--- /dev/null
+++ b/scripts/west_commands/tests/test_gd32isp.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2021 Gerson Fernando Budke <nandojve@gmail.com>
+# SPDX-License-Identifier: Apache-2.0
+
+import argparse
+import os
+import platform
+from unittest.mock import patch, call
+
+import pytest
+
+from runners.gd32isp import Gd32ispBinaryRunner
+from conftest import RC_KERNEL_BIN
+
+if platform.system() != 'Linux':
+    pytest.skip("skipping Linux-only gd32isp tests", allow_module_level=True)
+
+TEST_GD32ISP_CLI   = 'GD32_ISP_Console'
+TEST_GD32ISP_CLI_T = 'GD32_ISP_CLI'
+TEST_GD32ISP_DEV   = 'test-gd32test'
+TEST_GD32ISP_PORT  = 'test-gd32isp-serial'
+TEST_GD32ISP_SPEED = '2000000'
+TEST_GD32ISP_ADDR  = '0x08765430'
+
+EXPECTED_COMMANDS_DEFAULT = [
+    [TEST_GD32ISP_CLI, '-c', '--pn', '/dev/ttyUSB0', '--br', '57600',
+     '--sb', '1', '-i', TEST_GD32ISP_DEV, '-e', '--all', '-d',
+     '--a', '0x08000000', '--fn', RC_KERNEL_BIN],
+]
+
+EXPECTED_COMMANDS = [
+    [TEST_GD32ISP_CLI_T, '-c', '--pn', TEST_GD32ISP_PORT,
+     '--br', TEST_GD32ISP_SPEED,
+     '--sb', '1', '-i', TEST_GD32ISP_DEV, '-e', '--all', '-d',
+     '--a', TEST_GD32ISP_ADDR, '--fn', RC_KERNEL_BIN],
+]
+
+def require_patch(program):
+    assert program in [TEST_GD32ISP_CLI, TEST_GD32ISP_CLI_T]
+
+os_path_isfile = os.path.isfile
+
+def os_path_isfile_patch(filename):
+    if filename == RC_KERNEL_BIN:
+        return True
+    return os_path_isfile(filename)
+
+
+@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
+@patch('runners.core.ZephyrBinaryRunner.check_call')
+def test_gd32isp_init(cc, req, runner_config):
+    runner = Gd32ispBinaryRunner(runner_config, TEST_GD32ISP_DEV)
+    with patch('os.path.isfile', side_effect=os_path_isfile_patch):
+        runner.run('flash')
+    assert cc.call_args_list == [call(x) for x in EXPECTED_COMMANDS_DEFAULT]
+
+
+@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
+@patch('runners.core.ZephyrBinaryRunner.check_call')
+def test_gd32isp_create(cc, req, runner_config):
+    args = ['--device', TEST_GD32ISP_DEV,
+            '--port', TEST_GD32ISP_PORT,
+            '--speed', TEST_GD32ISP_SPEED,
+            '--addr', TEST_GD32ISP_ADDR,
+            '--isp', TEST_GD32ISP_CLI_T]
+    parser = argparse.ArgumentParser()
+    Gd32ispBinaryRunner.add_parser(parser)
+    arg_namespace = parser.parse_args(args)
+    runner = Gd32ispBinaryRunner.create(runner_config, arg_namespace)
+    with patch('os.path.isfile', side_effect=os_path_isfile_patch):
+        runner.run('flash')
+    assert cc.call_args_list == [call(x) for x in EXPECTED_COMMANDS]
diff --git a/scripts/west_commands/tests/test_imports.py b/scripts/west_commands/tests/test_imports.py
index 079f6b7..5ebec4f 100644
--- a/scripts/west_commands/tests/test_imports.py
+++ b/scripts/west_commands/tests/test_imports.py
@@ -21,6 +21,7 @@
                     'dediprog',
                     'dfu-util',
                     'esp32',
+                    'gd32isp',
                     'hifive1',
                     'intel_s1000',
                     'jlink',