Merge pull request #69 from riscv/multicore
authorTim Newsome <tim@sifive.com>
Tue, 12 Sep 2017 16:36:34 +0000 (09:36 -0700)
committerGitHub <noreply@github.com>
Tue, 12 Sep 2017 16:36:34 +0000 (09:36 -0700)
Proper multicore support for debug tests

24 files changed:
debug/Makefile
debug/gdbserver.py
debug/programs/entry.S
debug/programs/init.c
debug/programs/multicore.c [new file with mode: 0644]
debug/programs/start.S [deleted file]
debug/targets.py
debug/targets/RISC-V/spike.cfg [new file with mode: 0644]
debug/targets/RISC-V/spike32-2.py [new file with mode: 0644]
debug/targets/RISC-V/spike32.cfg [deleted file]
debug/targets/RISC-V/spike32.lds
debug/targets/RISC-V/spike32.py
debug/targets/RISC-V/spike64-2.py [new file with mode: 0644]
debug/targets/RISC-V/spike64.cfg [deleted file]
debug/targets/RISC-V/spike64.lds
debug/targets/RISC-V/spike64.py
debug/targets/SiFive/Freedom/E300.py
debug/targets/SiFive/Freedom/E300Sim.py
debug/targets/SiFive/Freedom/Freedom.lds
debug/targets/SiFive/Freedom/U500.py
debug/targets/SiFive/Freedom/U500Sim.py
debug/targets/SiFive/HiFive1.lds
debug/targets/SiFive/HiFive1.py
debug/testlib.py

index 2d8d36759d0aac813935b88d88771d9caca8ad32..33988dd02615fae0a40517bedc85f80f7b4a29cf 100644 (file)
@@ -4,9 +4,9 @@ XLEN ?= 64
 src_dir ?= .
 GDBSERVER_PY = $(src_dir)/gdbserver.py
 
-default: spike$(XLEN)
+default: spike$(XLEN)-2
 
-all:   pylint spike32 spike64
+all:   pylint spike32 spike64 spike32-2 spike64-2
 
 pylint:
        pylint --rcfile=pylint.rc `git ls-files '*.py'`
index cbb1299ad9ecafc009df6c486a8e656f49fde011..21eea4ed521b2da90c18e301b06838784eb4c20d 100755 (executable)
@@ -12,7 +12,7 @@ import targets
 import testlib
 from testlib import assertEqual, assertNotEqual, assertIn, assertNotIn
 from testlib import assertGreater, assertRegexpMatches, assertLess
-from testlib import GdbTest
+from testlib import GdbTest, GdbSingleHartTest, TestFailed
 
 MSTATUS_UIE = 0x00000001
 MSTATUS_SIE = 0x00000002
@@ -66,8 +66,8 @@ def readable_binary_string(s):
 
 class SimpleRegisterTest(GdbTest):
     def check_reg(self, name):
-        a = random.randrange(1<<self.target.xlen)
-        b = random.randrange(1<<self.target.xlen)
+        a = random.randrange(1<<self.hart.xlen)
+        b = random.randrange(1<<self.hart.xlen)
         self.gdb.p("$%s=0x%x" % (name, a))
         self.gdb.stepi()
         assertEqual(self.gdb.p("$%s" % name), a)
@@ -77,12 +77,12 @@ class SimpleRegisterTest(GdbTest):
 
     def setup(self):
         # 0x13 is nop
-        self.gdb.command("p *((int*) 0x%x)=0x13" % self.target.ram)
-        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.target.ram + 4))
-        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.target.ram + 8))
-        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.target.ram + 12))
-        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.target.ram + 16))
-        self.gdb.p("$pc=0x%x" % self.target.ram)
+        self.gdb.command("p *((int*) 0x%x)=0x13" % self.hart.ram)
+        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.hart.ram + 4))
+        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.hart.ram + 8))
+        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.hart.ram + 12))
+        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.hart.ram + 16))
+        self.gdb.p("$pc=0x%x" % self.hart.ram)
 
 class SimpleS0Test(SimpleRegisterTest):
     def test(self):
@@ -114,7 +114,7 @@ class SimpleF18Test(SimpleRegisterTest):
         assertLess(abs(float(self.gdb.p_raw("$%s" % name)) - b), .001)
 
     def early_applicable(self):
-        return self.target.extensionSupported('F')
+        return self.hart.extensionSupported('F')
 
     def test(self):
         self.check_reg("f18")
@@ -124,8 +124,8 @@ class SimpleMemoryTest(GdbTest):
         assertEqual(self.gdb.p("sizeof(%s)" % data_type), size)
         a = 0x86753095555aaaa & ((1<<(size*8))-1)
         b = 0xdeadbeef12345678 & ((1<<(size*8))-1)
-        addrA = self.target.ram
-        addrB = self.target.ram + self.target.ram_size - size
+        addrA = self.hart.ram
+        addrB = self.hart.ram + self.hart.ram_size - size
         self.gdb.p("*((%s*)0x%x) = 0x%x" % (data_type, addrA, a))
         self.gdb.p("*((%s*)0x%x) = 0x%x" % (data_type, addrB, b))
         assertEqual(self.gdb.p("*((%s*)0x%x)" % (data_type, addrA)), a)
@@ -157,7 +157,7 @@ class MemTest64(SimpleMemoryTest):
 #            assert False, "Read should have failed."
 #        except testlib.CannotAccess as e:
 #            assertEqual(e.address, 0xdeadbeef)
-#        self.gdb.p("*((int*)0x%x)" % self.target.ram)
+#        self.gdb.p("*((int*)0x%x)" % self.hart.ram)
 #
 #class MemTestWriteInvalid(SimpleMemoryTest):
 #    def test(self):
@@ -168,24 +168,25 @@ class MemTest64(SimpleMemoryTest):
 #            assert False, "Write should have failed."
 #        except testlib.CannotAccess as e:
 #            assertEqual(e.address, 0xdeadbeef)
-#        self.gdb.p("*((int*)0x%x)=6874742" % self.target.ram)
+#        self.gdb.p("*((int*)0x%x)=6874742" % self.hart.ram)
 
 class MemTestBlock(GdbTest):
+    length = 1024
+    line_length = 16
+
     def test(self):
-        length = 1024
-        line_length = 16
         a = tempfile.NamedTemporaryFile(suffix=".ihex")
         data = ""
-        for i in range(length / line_length):
+        for i in range(self.length / self.line_length):
             line_data = "".join(["%c" % random.randrange(256)
-                for _ in range(line_length)])
+                for _ in range(self.line_length)])
             data += line_data
-            a.write(ihex_line(i * line_length, 0, line_data))
+            a.write(ihex_line(i * self.line_length, 0, line_data))
         a.flush()
 
-        self.gdb.command("restore %s 0x%x" % (a.name, self.target.ram))
-        for offset in range(0, length, 19*4) + [length-4]:
-            value = self.gdb.p("*((int*)0x%x)" % (self.target.ram + offset))
+        self.gdb.command("restore %s 0x%x" % (a.name, self.hart.ram))
+        for offset in range(0, self.length, 19*4) + [self.length-4]:
+            value = self.gdb.p("*((int*)0x%x)" % (self.hart.ram + offset))
             written = ord(data[offset]) | \
                     (ord(data[offset+1]) << 8) | \
                     (ord(data[offset+2]) << 16) | \
@@ -194,13 +195,16 @@ class MemTestBlock(GdbTest):
 
         b = tempfile.NamedTemporaryFile(suffix=".ihex")
         self.gdb.command("dump ihex memory %s 0x%x 0x%x" % (b.name,
-            self.target.ram, self.target.ram + length))
+            self.hart.ram, self.hart.ram + self.length))
         for line in b:
             record_type, address, line_data = ihex_parse(line)
             if record_type == 0:
-                assertEqual(readable_binary_string(line_data),
-                        readable_binary_string(
-                            data[address:address+len(line_data)]))
+                written_data = data[address:address+len(line_data)]
+                if line_data != written_data:
+                    raise TestFailed(
+                            "Data mismatch at 0x%x; wrote %s but read %s" % (
+                                address, readable_binary_string(written_data),
+                                readable_binary_string(line_data)))
 
 class InstantHaltTest(GdbTest):
     def test(self):
@@ -213,7 +217,7 @@ class InstantHaltTest(GdbTest):
             self.gdb.thread(t)
             pcs.append(self.gdb.p("$pc"))
         for pc in pcs:
-            assertEqual(self.target.reset_vector, pc)
+            assertEqual(self.hart.reset_vector, pc)
         # mcycle and minstret have no defined reset value.
         mstatus = self.gdb.p("$mstatus")
         assertEqual(mstatus & (MSTATUS_MIE | MSTATUS_MPRV |
@@ -225,16 +229,16 @@ class InstantChangePc(GdbTest):
         # 0x13 is nop
         self.gdb.command("monitor reset halt")
         self.gdb.command("flushregs")
-        self.gdb.command("p *((int*) 0x%x)=0x13" % self.target.ram)
-        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.target.ram + 4))
-        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.target.ram + 8))
-        self.gdb.p("$pc=0x%x" % self.target.ram)
+        self.gdb.command("p *((int*) 0x%x)=0x13" % self.hart.ram)
+        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.hart.ram + 4))
+        self.gdb.command("p *((int*) 0x%x)=0x13" % (self.hart.ram + 8))
+        self.gdb.p("$pc=0x%x" % self.hart.ram)
         self.gdb.stepi()
-        assertEqual((self.target.ram + 4), self.gdb.p("$pc"))
+        assertEqual((self.hart.ram + 4), self.gdb.p("$pc"))
         self.gdb.stepi()
-        assertEqual((self.target.ram + 8), self.gdb.p("$pc"))
+        assertEqual((self.hart.ram + 8), self.gdb.p("$pc"))
 
-class DebugTest(GdbTest):
+class DebugTest(GdbSingleHartTest):
     # Include malloc so that gdb can make function calls. I suspect this malloc
     # will silently blow through the memory set aside for it, so be careful.
     compile_args = ("programs/debug.c", "programs/checksum.c",
@@ -325,10 +329,10 @@ class DebugBreakpoint(DebugTest):
 
 class Hwbp1(DebugTest):
     def test(self):
-        if self.target.instruction_hardware_breakpoint_count < 1:
+        if self.hart.instruction_hardware_breakpoint_count < 1:
             return 'not_applicable'
 
-        if not self.target.honors_tdata1_hmode:
+        if not self.hart.honors_tdata1_hmode:
             # Run to main before setting the breakpoint, because startup code
             # will otherwise clear the trigger that we set.
             self.gdb.b("main")
@@ -345,7 +349,7 @@ class Hwbp1(DebugTest):
 
 class Hwbp2(DebugTest):
     def test(self):
-        if self.target.instruction_hardware_breakpoint_count < 2:
+        if self.hart.instruction_hardware_breakpoint_count < 2:
             return 'not_applicable'
 
         self.gdb.hbreak("main")
@@ -415,21 +419,19 @@ class UserInterrupt(DebugTest):
         self.gdb.p("i=0")
         self.exit()
 
-class MulticoreTest(GdbTest):
-    compile_args = ("programs/infinite_loop.S", )
+class MulticoreRegTest(GdbTest):
+    compile_args = ("programs/infinite_loop.S", "-DMULTICORE")
+
+    def early_applicable(self):
+        return len(self.target.harts) > 1
 
     def setup(self):
         self.gdb.load()
-
-    def test(self):
-        threads = self.gdb.threads()
-        if len(threads) < 2:
-            return 'not_applicable'
-
-        for t in threads:
-            self.gdb.thread(t)
+        for hart in self.target.harts:
+            self.gdb.select_hart(hart)
             self.gdb.p("$pc=_start")
 
+    def test(self):
         # Run to main
         self.gdb.b("main")
         self.gdb.c()
@@ -456,18 +458,47 @@ class MulticoreTest(GdbTest):
         # Confirmed that we read different register values for different harts.
         # Write a new value to x1, and run through the add sequence again.
 
-        for t in threads:
-            self.gdb.thread(t)
-            self.gdb.p("$x1=0x%x" % (int(t.id) * 0x800))
+        for hart in self.target.harts:
+            self.gdb.select_hart(hart)
+            self.gdb.p("$x1=0x%x" % (hart.index * 0x800))
             self.gdb.p("$pc=main_post_csrr")
         self.gdb.c()
         for t in self.gdb.threads():
             assertIn("main_end", t.frame)
+        for hart in self.target.harts:
             # Check register values.
-            self.gdb.thread(t)
+            self.gdb.select_hart(hart)
             for n in range(1, 32):
                 value = self.gdb.p("$x%d" % n)
-                assertEqual(value, int(t.id) * 0x800 + n - 1)
+                assertEqual(value, hart.index * 0x800 + n - 1)
+
+class MulticoreRunHaltStepiTest(GdbTest):
+    compile_args = ("programs/multicore.c", "-DMULTICORE")
+
+    def early_applicable(self):
+        return len(self.target.harts) > 1
+
+    def setup(self):
+        self.gdb.load()
+        for hart in self.target.harts:
+            self.gdb.select_hart(hart)
+            self.gdb.p("$pc=_start")
+
+    def test(self):
+        previous_hart_count = [0 for h in self.target.harts]
+        for _ in range(10):
+            self.gdb.c(wait=False)
+            time.sleep(1)
+            self.gdb.interrupt()
+            self.gdb.p("buf", fmt="")
+            hart_count = self.gdb.p("hart_count")
+            for i, h in enumerate(self.target.harts):
+                assertGreater(hart_count[i], previous_hart_count[i])
+                self.gdb.select_hart(h)
+                pc = self.gdb.p("$pc")
+                self.gdb.stepi()
+                stepped_pc = self.gdb.p("$pc")
+                assertNotEqual(pc, stepped_pc)
 
 class StepTest(GdbTest):
     compile_args = ("programs/step.S", )
@@ -479,7 +510,7 @@ class StepTest(GdbTest):
 
     def test(self):
         main_address = self.gdb.p("$pc")
-        if self.target.extensionSupported("c"):
+        if self.hart.extensionSupported("c"):
             sequence = (4, 8, 0xc, 0xe, 0x14, 0x18, 0x22, 0x1c, 0x24, 0x24)
         else:
             sequence = (4, 8, 0xc, 0x10, 0x18, 0x1c, 0x28, 0x20, 0x2c, 0x2c)
@@ -558,16 +589,16 @@ class TriggerStoreAddressInstant(TriggerTest):
 
 class TriggerDmode(TriggerTest):
     def early_applicable(self):
-        return self.target.honors_tdata1_hmode
+        return self.hart.honors_tdata1_hmode
 
     def check_triggers(self, tdata1_lsbs, tdata2):
-        dmode = 1 << (self.target.xlen-5)
+        dmode = 1 << (self.hart.xlen-5)
 
         triggers = []
 
-        if self.target.xlen == 32:
+        if self.hart.xlen == 32:
             xlen_type = 'int'
-        elif self.target.xlen == 64:
+        elif self.hart.xlen == 64:
             xlen_type = 'long long'
         else:
             raise NotImplementedError
@@ -627,7 +658,7 @@ class WriteGprs(RegsTest):
         self.gdb.command("info registers")
         for n in range(len(regs)):
             assertEqual(self.gdb.x("data+%d" % (8*n), 'g'),
-                    ((0xdeadbeef<<n)+17) & ((1<<self.target.xlen)-1))
+                    ((0xdeadbeef<<n)+17) & ((1<<self.hart.xlen)-1))
 
 class WriteCsrs(RegsTest):
     def test(self):
@@ -651,7 +682,7 @@ class WriteCsrs(RegsTest):
 class DownloadTest(GdbTest):
     def setup(self):
         # pylint: disable=attribute-defined-outside-init
-        length = min(2**10, self.target.ram_size - 2048)
+        length = min(2**10, self.hart.ram_size - 2048)
         self.download_c = tempfile.NamedTemporaryFile(prefix="download_",
                 suffix=".c", delete=False)
         self.download_c.write("#include <stdint.h>\n")
@@ -677,7 +708,7 @@ class DownloadTest(GdbTest):
         if self.crc < 0:
             self.crc += 2**32
 
-        self.binary = self.target.compile(self.download_c.name,
+        self.binary = self.target.compile(self.hart, self.download_c.name,
                 "programs/checksum.c")
         self.gdb.command("file %s" % self.binary)
 
@@ -708,7 +739,7 @@ class DownloadTest(GdbTest):
 #        # pylint: disable=attribute-defined-outside-init
 #        self.gdb.load()
 #
-#        misa = self.target.misa
+#        misa = self.hart.misa
 #        self.supported = set()
 #        if misa & (1<<20):
 #            self.supported.add(0)
index ff8ae3092a517ad20ce2860b7b56ac2d1cd05b5a..a2ea955a2707ed93d1283b066d801fc46ac2d398 100755 (executable)
@@ -1,9 +1,8 @@
-#ifndef ENTRY_S
-#define ENTRY_S
-
 #include "encoding.h"
 
-#define STACK_SIZE 512
+// Enough stack to store every register in case a trap handler is executed,
+// plus 33 more values.
+#define STACK_SIZE (64 * XLEN / 8)
 
 #if XLEN == 64
 # define LREG ld
@@ -61,8 +60,15 @@ handle_reset:
   la gp, __global_pointer$
 .option pop
 
-  # initialize stack pointer
-  la sp, stack_top
+  # Initialize stack pointer.
+  # Give each hart STACK_SIZE of stack.
+  # Assume hart IDs are contiguous and start at 0.
+  csrr  t0, CSR_MHARTID
+  addi  t0, t0, 1
+  li    t1, STACK_SIZE
+  mul   t0, t0, t1
+  la    sp, stack_bottom
+  add   sp, sp, t0
 
   # Clear all hardware triggers
   li    t0, ~0
@@ -73,8 +79,33 @@ handle_reset:
   csrr  t1, CSR_TSELECT
   beq   t0, t1, 1b
 
+#ifdef MULTICORE
+  csrr  t0, CSR_MHARTID
+  bnez  t0, wait_until_initialized
+#endif
+
+  la    t0, __bss_start
+  la    t1, __bss_end
+1:
+  bge   t0, t1, 2f
+  sb    zero, 0(t0)
+  addi  t0, t0, 1
+  j     1b
+2:
+#ifdef MULTICORE
+  la    t0, initialized
+  li    t1, 1
+  sw    t1, 0(t0)
+
+wait_until_initialized:      # Wait for hart 0 to perform initialization.
+  la    t0, initialized
+1:
+  lw    t1, 0(t0)
+  beqz  t1, 1b
+#endif
+
   # perform the rest of initialization in C
-  j _init
+  j     _init
 
 
 trap_entry:
@@ -157,9 +188,13 @@ trap_entry:
   addi sp, sp, 32*REGBYTES
   mret
 
+loop_forever:
+  j loop_forever
+
   // Fill the stack with data so we can see if it was overrun.
   .align 4
 stack_bottom:
-  .fill STACK_SIZE/4, 4, 0x22446688
+  .fill NHARTS * STACK_SIZE/4, 4, 0x22446688
 stack_top:
-#endif
+initialized:
+  .word 0
index a2b41b02d60e2eb51af3d9322dcedb2f72460c67..9933c23a72705be76deef9a670ff053b853384a8 100644 (file)
@@ -1,7 +1,30 @@
+#include "init.h"
+#include "encoding.h"
+
 int main(void);
 
+trap_handler_t trap_handler[NHARTS] = {0};
+
+void set_trap_handler(trap_handler_t handler)
+{
+    unsigned hartid = csr_read(mhartid);
+    trap_handler[hartid] = handler;
+}
+
+void enable_timer_interrupts()
+{
+    set_csr(mie, MIP_MTIP);
+    set_csr(mstatus, MSTATUS_MIE);
+}
+
 void handle_trap(unsigned int mcause, unsigned int mepc, unsigned int sp)
 {
+    unsigned hartid = csr_read(mhartid);
+    if (trap_handler[hartid]) {
+        trap_handler[hartid](hartid, mcause, mepc, sp);
+        return;
+    }
+
     while (1)
         ;
 }
diff --git a/debug/programs/multicore.c b/debug/programs/multicore.c
new file mode 100644 (file)
index 0000000..d7dd845
--- /dev/null
@@ -0,0 +1,81 @@
+#include <stdint.h>
+
+typedef struct {
+    int counter;
+} atomic_t;
+
+static inline int atomic_xchg(atomic_t *v, int n)
+{
+    register int c;
+
+    __asm__ __volatile__ (
+            "amoswap.w.aqrl %0, %2, %1"
+            : "=r" (c), "+A" (v->counter)
+            : "r" (n));
+    return c;
+}
+
+#define csr_read(csr)                                   \
+({                                                      \
+    register unsigned long __v;                         \
+    __asm__ __volatile__ ("csrr %0, " #csr              \
+                  : "=r" (__v));                        \
+    __v;                                                \
+})
+
+static inline void mb(void)
+{
+    __asm__ __volatile__ ("fence");
+}
+
+void get_lock(atomic_t *lock)
+{
+    while (atomic_xchg(lock, 1) == 1)
+        ;
+    mb();
+}
+
+void put_lock(atomic_t *lock)
+{
+    mb();
+    atomic_xchg(lock, 0);
+}
+
+static atomic_t buf_lock = { .counter = 0 };
+static char buf[32];
+static int buf_initialized;
+static unsigned hart_count[2];
+
+static const char case_bit = 'a' - 'A';
+volatile int initialized;
+
+int main()
+{
+    uint32_t hartid = csr_read(mhartid);
+    hart_count[hartid] = 0;
+
+    while (1) {
+        get_lock(&buf_lock);
+        hart_count[hartid]++;
+
+        if (!buf_initialized) {
+            for (unsigned i = 0; i < sizeof(buf); i++) {
+                buf[i] = 'A' + (i % 26);
+            }
+            buf_initialized = 1;
+        }
+
+        char first = buf[0];
+        int offset = (first & ~0x20) - 'A';
+        for (unsigned i = 0; i < sizeof(buf); i++) {
+            while (buf[i] != (first - offset + ((offset + i) % 26)))
+                ;
+
+            if (hartid & 1)
+                buf[i] = 'A' + ((i + hartid + hart_count[hartid]) % 26);
+            else
+                buf[i] = 'a' + ((i + hartid + hart_count[hartid]) % 26);
+        }
+        put_lock(&buf_lock);
+    }
+}
diff --git a/debug/programs/start.S b/debug/programs/start.S
deleted file mode 100644 (file)
index 76c37bb..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-        .global         _start
-
-_start:
-        la      sp, stack_end
-        jal     main
-done:
-        j       done
-
-        .data
-stack:
-        .fill   4096, 1, 0
-stack_end:
index 7183a38fe8011fc68f77d7e63be04abcbc4c7b84..db8d917c7b9f2a2132acd4c026f4edecf53d2500 100644 (file)
@@ -5,51 +5,71 @@ import tempfile
 
 import testlib
 
+class Hart(object):
+    # XLEN of the hart. May be overridden with --32 or --64 command line
+    # options.
+    xlen = 0
+
+    # Will be autodetected (by running ExamineTarget) if left unset. Set to
+    # save a little time.
+    misa = None
+
+    # Path to linker script relative to the .py file where the target is
+    # defined. Defaults to <name>.lds.
+    link_script_path = None
+
+    # Implements dmode in tdata1 as described in the spec. Harts that need
+    # this value set to False are not compliant with the spec (but still usable
+    # as long as running code doesn't try to mess with triggers set by an
+    # external debugger).
+    honors_tdata1_hmode = True
+
+    # Address where a r/w/x block of RAM starts, together with its size.
+    ram = None
+    ram_size = None
+
+    # Number of instruction triggers the hart supports.
+    instruction_hardware_breakpoint_count = 0
+
+    # Defaults to target-<index>
+    name = None
+
+    def extensionSupported(self, letter):
+        # target.misa is set by testlib.ExamineTarget
+        if self.misa:
+            return self.misa & (1 << (ord(letter.upper()) - ord('A')))
+        else:
+            return False
+
 class Target(object):
     # pylint: disable=too-many-instance-attributes
 
+    # List of Hart object instances, one for each hart in the target.
+    harts = []
+
     # Name of the target. Defaults to the name of the class.
     name = None
 
-    # XLEN of the target. May be overridden with --32 or --64 command line
-    # options.
-    xlen = 0
-
     # GDB remotetimeout setting.
     timeout_sec = 2
 
-    # Path to OpenOCD configuration file relative to the .py file where the
-    # target is defined. Defaults to <name>.cfg.
-    openocd_config_path = None
-
     # Timeout waiting for the server to start up. This is different than the
     # GDB timeout, which is how long GDB waits for commands to execute.
     # The server_timeout is how long this script waits for the Server to be
     # ready for GDB connections.
     server_timeout_sec = 60
 
-    # Path to linker script relative to the .py file where the target is
-    # defined. Defaults to <name>.lds.
-    link_script_path = None
-
-    # Will be autodetected (by running ExamineTarget) if left unset. Set to
-    # save a little time.
-    misa = None
+    # Path to OpenOCD configuration file relative to the .py file where the
+    # target is defined. Defaults to <name>.cfg.
+    openocd_config_path = None
 
     # List of commands that should be executed in gdb after connecting but
     # before starting the test.
     gdb_setup = []
 
-    # Implements dmode in tdata1 as described in the spec. Targets that need
-    # this value set to False are not compliant with the spec (but still usable
-    # as long as running code doesn't try to mess with triggers set by an
-    # external debugger).
-    honors_tdata1_hmode = True
-
     # Internal variables:
     directory = None
     temporary_files = []
-    temporary_binary = None
 
     def __init__(self, path, parsed):
         # Path to module.
@@ -57,7 +77,8 @@ class Target(object):
         self.directory = os.path.dirname(path)
         self.server_cmd = parsed.server_cmd
         self.sim_cmd = parsed.sim_cmd
-        self.isolate = parsed.isolate
+        self.temporary_binary = None
+        Target.isolate = parsed.isolate
         if not self.name:
             self.name = type(self).__name__
         # Default OpenOCD config file to <name>.cfg
@@ -65,11 +86,15 @@ class Target(object):
             self.openocd_config_path = "%s.cfg" % self.name
         self.openocd_config_path = os.path.join(self.directory,
                 self.openocd_config_path)
-        # Default link script to <name>.lds
-        if not self.link_script_path:
-            self.link_script_path = "%s.lds" % self.name
-        self.link_script_path = os.path.join(self.directory,
-                self.link_script_path)
+        for i, hart in enumerate(self.harts):
+            hart.index = i
+            if not hart.name:
+                hart.name = "%s-%d" % (self.name, i)
+            # Default link script to <name>.lds
+            if not hart.link_script_path:
+                hart.link_script_path = "%s.lds" % self.name
+            hart.link_script_path = os.path.join(self.directory,
+                    hart.link_script_path)
 
     def create(self):
         """Create the target out of thin air, eg. start a simulator."""
@@ -78,42 +103,35 @@ class Target(object):
     def server(self):
         """Start the debug server that gdb connects to, eg. OpenOCD."""
         return testlib.Openocd(server_cmd=self.server_cmd,
-                               config=self.openocd_config_path,
-                               timeout=self.server_timeout_sec)
+                config=self.openocd_config_path)
 
-    def compile(self, *sources):
+    def compile(self, hart, *sources):
         binary_name = "%s_%s-%d" % (
                 self.name,
                 os.path.basename(os.path.splitext(sources[0])[0]),
-                self.xlen)
-        if self.isolate:
+                hart.xlen)
+        if Target.isolate:
             self.temporary_binary = tempfile.NamedTemporaryFile(
                     prefix=binary_name + "_")
             binary_name = self.temporary_binary.name
             Target.temporary_files.append(self.temporary_binary)
-        march = "rv%dima" % self.xlen
+        march = "rv%dima" % hart.xlen
         for letter in "fdc":
-            if self.extensionSupported(letter):
+            if hart.extensionSupported(letter):
                 march += letter
         testlib.compile(sources +
                 ("programs/entry.S", "programs/init.c",
+                    "-DNHARTS=%d" % len(self.harts),
                     "-I", "../env",
                     "-march=%s" % march,
-                    "-T", self.link_script_path,
+                    "-T", hart.link_script_path,
                     "-nostartfiles",
                     "-mcmodel=medany",
-                    "-DXLEN=%d" % self.xlen,
+                    "-DXLEN=%d" % hart.xlen,
                     "-o", binary_name),
-                xlen=self.xlen)
+                xlen=hart.xlen)
         return binary_name
 
-    def extensionSupported(self, letter):
-        # target.misa is set by testlib.ExamineTarget
-        if self.misa:
-            return self.misa & (1 << (ord(letter.upper()) - ord('A')))
-        else:
-            return False
-
 def add_target_options(parser):
     parser.add_argument("target", help=".py file that contains definition for "
             "the target to test with.")
@@ -149,4 +167,7 @@ def target(parsed):
     assert len(found) == 1, "%s does not define exactly one subclass of " \
             "targets.Target" % parsed.target
 
-    return found[0](parsed.target, parsed)
+    t = found[0](parsed.target, parsed)
+    assert t.harts, "%s doesn't have any harts defined!" % t.name
+
+    return t
diff --git a/debug/targets/RISC-V/spike.cfg b/debug/targets/RISC-V/spike.cfg
new file mode 100644 (file)
index 0000000..9b1841c
--- /dev/null
@@ -0,0 +1,16 @@
+adapter_khz     10000
+
+interface remote_bitbang
+remote_bitbang_host $::env(REMOTE_BITBANG_HOST)
+remote_bitbang_port $::env(REMOTE_BITBANG_PORT)
+
+set _CHIPNAME riscv
+jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x10e31913
+
+set _TARGETNAME $_CHIPNAME.cpu
+target create $_TARGETNAME riscv -chain-position $_TARGETNAME -rtos riscv
+
+gdb_report_data_abort enable
+
+init
+reset halt
diff --git a/debug/targets/RISC-V/spike32-2.py b/debug/targets/RISC-V/spike32-2.py
new file mode 100644 (file)
index 0000000..6cf558d
--- /dev/null
@@ -0,0 +1,12 @@
+import targets
+import testlib
+
+import spike32  # pylint: disable=import-error
+
+class spike32_2(targets.Target):
+    harts = [spike32.spike32_hart(), spike32.spike32_hart()]
+    openocd_config_path = "spike.cfg"
+    timeout_sec = 30
+
+    def create(self):
+        return testlib.Spike(self)
diff --git a/debug/targets/RISC-V/spike32.cfg b/debug/targets/RISC-V/spike32.cfg
deleted file mode 100644 (file)
index 2742335..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-adapter_khz     10000
-
-interface remote_bitbang
-remote_bitbang_host $::env(REMOTE_BITBANG_HOST)
-remote_bitbang_port $::env(REMOTE_BITBANG_PORT)
-
-set _CHIPNAME riscv
-jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x10e31913
-
-set _TARGETNAME $_CHIPNAME.cpu
-#target create $_TARGETNAME riscv -chain-position $_TARGETNAME -rtos riscv
-target create $_TARGETNAME riscv -chain-position $_TARGETNAME
-
-gdb_report_data_abort enable
-
-init
-reset halt
-
-echo "Ready for Remote Connections"
index 01d0e3d0f7861ac5d6766cf4af4754630072ceca..84216dbac808ad27520bb1a16fbbb93306abf721 100755 (executable)
@@ -22,11 +22,13 @@ SECTIONS
   }
 
   /* bss segment */
+  __bss_start = .;
   .sbss : {
     *(.sbss .sbss.* .gnu.linkonce.sb.*)
     *(.scommon)
   }
   .bss : { *(.bss) }
+  __bss_end = .;
 
   __malloc_start = .;
   . = . + 512;
index 3bf8b4783c4578bb5f671b9036c5a08de37c7ab4..665d7e99a56c6ae73e7364a60ef11a33f80b14f5 100644 (file)
@@ -1,12 +1,18 @@
 import targets
 import testlib
 
-class spike32(targets.Target):
+class spike32_hart(targets.Hart):
     xlen = 32
     ram = 0x10000000
     ram_size = 0x10000000
     instruction_hardware_breakpoint_count = 4
     reset_vector = 0x1000
+    link_script_path = "spike32.lds"
+
+class spike32(targets.Target):
+    harts = [spike32_hart()]
+    openocd_config_path = "spike.cfg"
+    timeout_sec = 30
 
     def create(self):
         return testlib.Spike(self)
diff --git a/debug/targets/RISC-V/spike64-2.py b/debug/targets/RISC-V/spike64-2.py
new file mode 100644 (file)
index 0000000..c6321dc
--- /dev/null
@@ -0,0 +1,12 @@
+import targets
+import testlib
+
+import spike64  # pylint: disable=import-error
+
+class spike64_2(targets.Target):
+    harts = [spike64.spike64_hart(), spike64.spike64_hart()]
+    openocd_config_path = "spike.cfg"
+    timeout_sec = 30
+
+    def create(self):
+        return testlib.Spike(self)
diff --git a/debug/targets/RISC-V/spike64.cfg b/debug/targets/RISC-V/spike64.cfg
deleted file mode 100644 (file)
index 2742335..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-adapter_khz     10000
-
-interface remote_bitbang
-remote_bitbang_host $::env(REMOTE_BITBANG_HOST)
-remote_bitbang_port $::env(REMOTE_BITBANG_PORT)
-
-set _CHIPNAME riscv
-jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x10e31913
-
-set _TARGETNAME $_CHIPNAME.cpu
-#target create $_TARGETNAME riscv -chain-position $_TARGETNAME -rtos riscv
-target create $_TARGETNAME riscv -chain-position $_TARGETNAME
-
-gdb_report_data_abort enable
-
-init
-reset halt
-
-echo "Ready for Remote Connections"
index dc7cb63a86f9ddce9a662f0a6d09033bdfe046d3..2e7d65d177f8ef470a4b4da54769f5b3b3f777eb 100755 (executable)
@@ -20,11 +20,13 @@ SECTIONS
   }
 
   /* bss segment */
+  __bss_start = .;
   .sbss : {
     *(.sbss .sbss.* .gnu.linkonce.sb.*)
     *(.scommon)
   }
   .bss : { *(.bss) }
+  __bss_end = .;
 
   __malloc_start = .;
   . = . + 512;
index c70585782622610b4513cbaedbff5fb1fc49af1d..6e3da896ef01b79a454893fe964def2da1c441ab 100644 (file)
@@ -1,12 +1,18 @@
 import targets
 import testlib
 
-class spike64(targets.Target):
+class spike64_hart(targets.Hart):
     xlen = 64
     ram = 0x1212340000
     ram_size = 0x10000000
     instruction_hardware_breakpoint_count = 4
     reset_vector = 0x1000
+    link_script_path = "spike64.lds"
+
+class spike64(targets.Target):
+    harts = [spike64_hart()]
+    openocd_config_path = "spike.cfg"
+    timeout_sec = 30
 
     def create(self):
         return testlib.Spike(self)
index 95ddcfd6273a598a6aecf52e0000b0aac8e8a514..170de400df202221b7834678bcb300d89f2ba10d 100644 (file)
@@ -1,9 +1,12 @@
 import targets
 
-class E300(targets.Target):
+class E300Hart(targets.Hart):
     xlen = 32
     ram = 0x80000000
-    ram_size = 16 * 1024
+    ram_size = 256 * 1024 * 1024
     instruction_hardware_breakpoint_count = 2
-    openocd_config_path = "Freedom.cfg"
     link_script_path = "Freedom.lds"
+
+class E300(targets.Target):
+    openocd_config_path = "Freedom.cfg"
+    harts = [E300Hart()]
index 91be2e8901778cb3c6ea65d0b7fd3623d67a8391..f9428d077650d62c3483dcfe987dd7810ea24216 100644 (file)
@@ -1,14 +1,17 @@
 import targets
 import testlib
 
-class E300Sim(targets.Target):
+class E300Hart(targets.Hart):
     xlen = 32
-    timeout_sec = 6000
     ram = 0x80000000
     ram_size = 256 * 1024 * 1024
     instruction_hardware_breakpoint_count = 2
-    openocd_config_path = "Freedom.cfg"
     link_script_path = "Freedom.lds"
 
+class E300Sim(targets.Target):
+    timeout_sec = 6000
+    openocd_config_path = "Freedom.cfg"
+    harts = [E300Hart()]
+
     def create(self):
         return testlib.VcsSim(sim_cmd=self.sim_cmd, debug=False)
index 1e0645a34ef8ac6f1c6f0fd23b0bab9d4ee52ed4..9354d3fa49f5e3df67cb0c284f0ebb39b160cbfa 100644 (file)
@@ -20,11 +20,13 @@ SECTIONS
   }
 
   /* bss segment */
+  __bss_start = .;
   .sbss : {
     *(.sbss .sbss.* .gnu.linkonce.sb.*)
     *(.scommon)
   }
   .bss : { *(.bss) }
+  __bss_end = .;
 
   __malloc_start = .;
   . = . + 512;
index c22aa4c6907fd3b280550e812f558dc05b627812..6da3ac509c5b53c7d75034983404b19159d6a945 100644 (file)
@@ -1,9 +1,12 @@
 import targets
 
-class U500(targets.Target):
+class U500Hart(targets.Hart):
     xlen = 64
     ram = 0x80000000
     ram_size = 16 * 1024
     instruction_hardware_breakpoint_count = 2
-    openocd_config_path = "Freedom.cfg"
     link_script_path = "Freedom.lds"
+
+class U500(targets.Target):
+    openocd_config_path = "Freedom.cfg"
+    harts = [U500Hart()]
index 62bc8278ab6efd20f30e9ce8c324883aab89d5a1..065ab08ae446b4fb76dba6b9a4968acec34b464c 100644 (file)
@@ -1,14 +1,17 @@
 import targets
 import testlib
 
-class U500Sim(targets.Target):
+class U500Hart(targets.Hart):
     xlen = 64
-    timeout_sec = 6000
     ram = 0x80000000
     ram_size = 256 * 1024 * 1024
     instruction_hardware_breakpoint_count = 2
-    openocd_config_path = "Freedom.cfg"
     link_script_path = "Freedom.lds"
 
-    def create(self):
+class U500Sim(targets.Target):
+    timeout_sec = 6000
+    openocd_config_path = "Freedom.cfg"
+    harts = [U500Hart()]
+
+    def target(self):
         return testlib.VcsSim(sim_cmd=self.sim_cmd, debug=False)
index 1e0645a34ef8ac6f1c6f0fd23b0bab9d4ee52ed4..9354d3fa49f5e3df67cb0c284f0ebb39b160cbfa 100755 (executable)
@@ -20,11 +20,13 @@ SECTIONS
   }
 
   /* bss segment */
+  __bss_start = .;
   .sbss : {
     *(.sbss .sbss.* .gnu.linkonce.sb.*)
     *(.scommon)
   }
   .bss : { *(.bss) }
+  __bss_end = .;
 
   __malloc_start = .;
   . = . + 512;
index 813829e415116ec15a76c463924f168c14f83c87..3cb508cd0e1e358797ce7f836ffc6fdcc5046ad7 100644 (file)
@@ -1,8 +1,11 @@
 import targets
 
-class HiFive1(targets.Target):
+class HiFive1Hart(targets.Hart):
     xlen = 32
     ram = 0x80000000
     ram_size = 16 * 1024
     instruction_hardware_breakpoint_count = 2
     misa = 0x40001105
+
+class HiFive1(targets.Target):
+    harts = [HiFive1Hart()]
index b76f320a7071cc4fb00c02cd0aaacade8066c9e2..8ac616e7dd19b24d02bb401537858cb2fdc368a1 100644 (file)
@@ -17,8 +17,11 @@ import pexpect
 def find_file(path):
     for directory in (os.getcwd(), os.path.dirname(__file__)):
         fullpath = os.path.join(directory, path)
-        if os.path.exists(fullpath):
-            return fullpath
+        relpath = os.path.relpath(fullpath)
+        if len(relpath) >= len(fullpath):
+            relpath = fullpath
+        if os.path.exists(relpath):
+            return relpath
     return None
 
 def compile(args, xlen=32): # pylint: disable=redefined-builtin
@@ -36,13 +39,12 @@ def compile(args, xlen=32): # pylint: disable=redefined-builtin
             cmd.append(found)
         else:
             cmd.append(arg)
+    header("Compile")
+    print "+", " ".join(cmd)
     process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
     stdout, stderr = process.communicate()
     if process.returncode:
-        print
-        header("Compile failed")
-        print "+", " ".join(cmd)
         print stdout,
         print stderr,
         header("")
@@ -63,16 +65,34 @@ class Spike(object):
     def __init__(self, target, halted=False, timeout=None, with_jtag_gdb=True):
         """Launch spike. Return tuple of its process and the port it's running
         on."""
+        self.process = None
+
+        if target.harts:
+            harts = target.harts
+        else:
+            harts = [target]
+
         if target.sim_cmd:
             cmd = shlex.split(target.sim_cmd)
         else:
             spike = os.path.expandvars("$RISCV/bin/spike")
             cmd = [spike]
-        if target.xlen == 32:
+
+        cmd += ["-p%d" % len(harts)]
+
+        assert len(set(t.xlen for t in harts)) == 1, \
+                "All spike harts must have the same XLEN"
+
+        if harts[0].xlen == 32:
             cmd += ["--isa", "RV32G"]
         else:
             cmd += ["--isa", "RV64G"]
-        cmd += ["-m0x%x:0x%x" % (target.ram, target.ram_size)]
+
+        assert len(set(t.ram for t in harts)) == 1, \
+                "All spike harts must have the same RAM layout"
+        assert len(set(t.ram_size for t in harts)) == 1, \
+                "All spike harts must have the same RAM layout"
+        cmd += ["-m0x%x:0x%x" % (harts[0].ram, harts[0].ram_size)]
 
         if timeout:
             cmd = ["timeout", str(timeout)] + cmd
@@ -82,7 +102,7 @@ class Spike(object):
         if with_jtag_gdb:
             cmd += ['--rbb-port', '0']
             os.environ['REMOTE_BITBANG_HOST'] = 'localhost'
-        self.infinite_loop = target.compile(
+        self.infinite_loop = target.compile(harts[0],
                 "programs/checksum.c", "programs/tiny-malloc.c",
                 "programs/infinite_loop.S", "-DDEFINE_MALLOC", "-DDEFINE_FREE")
         cmd.append(self.infinite_loop)
@@ -106,11 +126,12 @@ class Spike(object):
                     "connection"
 
     def __del__(self):
-        try:
-            self.process.kill()
-            self.process.wait()
-        except OSError:
-            pass
+        if self.process:
+            try:
+                self.process.kill()
+                self.process.wait()
+            except OSError:
+                pass
 
     def wait(self, *args, **kwargs):
         return self.process.wait(*args, **kwargs)
@@ -164,6 +185,8 @@ class Openocd(object):
     print "OpenOCD Temporary Log File: %s" % logname
 
     def __init__(self, server_cmd=None, config=None, debug=False, timeout=60):
+        self.timeout = timeout
+
         if server_cmd:
             cmd = shlex.split(server_cmd)
         else:
@@ -200,7 +223,13 @@ class Openocd(object):
         logfile = open(Openocd.logname, "w")
         logfile.write("+ %s\n" % " ".join(cmd))
         logfile.flush()
-        self.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+
+        self.ports = []
+        self.port = None
+        self.process = self.start(cmd, logfile)
+
+    def start(self, cmd, logfile):
+        process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
                 stdout=logfile, stderr=logfile)
 
         try:
@@ -215,19 +244,23 @@ class Openocd(object):
                 m = re.search(r"Listening on port (\d+) for gdb connections",
                         log)
                 if m:
-                    self.port = int(m.group(1))
+                    if not self.ports:
+                        self.port = int(m.group(1))
+                    self.ports.append(int(m.group(1)))
+
+                if "telnet server disabled" in log:
                     break
 
-                if not self.process.poll() is None:
+                if not process.poll() is None:
                     raise Exception(
                             "OpenOCD exited before completing riscv_examine()")
                 if not messaged and time.time() - start > 1:
                     messaged = True
                     print "Waiting for OpenOCD to start..."
-                if (time.time() - start) > timeout:
+                if (time.time() - start) > self.timeout:
                     raise Exception("ERROR: Timed out waiting for OpenOCD to "
                             "listen for gdb")
-
+            return process
         except Exception:
             header("OpenOCD log")
             sys.stdout.write(log)
@@ -291,6 +324,10 @@ class Gdb(object):
         # Force consistency.
         self.command("set print entry-values no")
 
+    def select_hart(self, hart):
+        output = self.command("thread %d" % (hart.index + 1))
+        assert "Unknown" not in output
+
     def wait(self):
         """Wait for prompt."""
         self.child.expect(r"\(gdb\)")
@@ -331,13 +368,23 @@ class Gdb(object):
             raise CannotAccess(int(m.group(1), 0))
         return output.split('=')[-1].strip()
 
-    def p(self, obj):
-        output = self.command("p/x %s" % obj)
+    def parse_string(self, text):
+        text = text.strip()
+        if text.startswith("{") and text.endswith("}"):
+            inner = text[1:-1]
+            return [self.parse_string(t) for t in inner.split(", ")]
+        elif text.startswith('"') and text.endswith('"'):
+            return text[1:-1]
+        else:
+            return int(text, 0)
+
+    def p(self, obj, fmt="/x"):
+        output = self.command("p%s %s" % (fmt, obj))
         m = re.search("Cannot access memory at address (0x[0-9a-f]+)", output)
         if m:
             raise CannotAccess(int(m.group(1), 0))
-        value = int(output.split('=')[-1].strip(), 0)
-        return value
+        rhs = output.split('=')[-1]
+        return self.parse_string(rhs)
 
     def p_string(self, obj):
         output = self.command("p %s" % obj)
@@ -391,19 +438,20 @@ def run_all_tests(module, target, parsed):
     gdb_cmd = parsed.gdb
 
     todo = []
-    if parsed.misaval:
-        target.misa = int(parsed.misaval, 16)
-        print "Using $misa from command line: 0x%x" % target.misa
-    elif target.misa:
-        print "Using $misa from target definition: 0x%x" % target.misa
-    else:
-        todo.append(("ExamineTarget", ExamineTarget))
+    for hart in target.harts:
+        if parsed.misaval:
+            hart.misa = int(parsed.misaval, 16)
+            print "Using $misa from command line: 0x%x" % hart.misa
+        elif hart.misa:
+            print "Using $misa from hart definition: 0x%x" % hart.misa
+        else:
+            todo.append(("ExamineTarget", ExamineTarget, hart))
 
     for name in dir(module):
         definition = getattr(module, name)
         if type(definition) == type and hasattr(definition, 'test') and \
                 (not parsed.test or any(test in name for test in parsed.test)):
-            todo.append((name, definition))
+            todo.append((name, definition, None))
 
     results, count = run_tests(parsed, target, todo)
 
@@ -417,12 +465,12 @@ def run_tests(parsed, target, todo):
     results = {}
     count = 0
 
-    for name, definition in todo:
-        instance = definition(target)
+    for name, definition, hart in todo:
         log_name = os.path.join(parsed.logs, "%s-%s-%s.log" %
                 (time.strftime("%Y%m%d-%H%M%S"), type(target).__name__, name))
         log_fd = open(log_name, 'w')
         print "Running %s > %s ..." % (name, log_name),
+        instance = definition(target, hart)
         sys.stdout.flush()
         log_fd.write("Test: %s\n" % name)
         log_fd.write("Target: %s\n" % type(target).__name__)
@@ -491,8 +539,13 @@ def print_log(path):
 class BaseTest(object):
     compiled = {}
 
-    def __init__(self, target):
+    def __init__(self, target, hart=None):
         self.target = target
+        if hart:
+            self.hart = hart
+        else:
+            self.hart = random.choice(target.harts)
+            self.hart = target.harts[-1]    #<<<
         self.server = None
         self.target_process = None
         self.binary = None
@@ -514,7 +567,7 @@ class BaseTest(object):
             if compile_args not in BaseTest.compiled:
                 # pylint: disable=star-args
                 BaseTest.compiled[compile_args] = \
-                        self.target.compile(*compile_args)
+                        self.target.compile(self.hart, *compile_args)
         self.binary = BaseTest.compiled.get(compile_args)
 
     def classSetup(self):
@@ -581,8 +634,8 @@ class BaseTest(object):
 
 gdb_cmd = None
 class GdbTest(BaseTest):
-    def __init__(self, target):
-        BaseTest.__init__(self, target)
+    def __init__(self, target, hart=None):
+        BaseTest.__init__(self, target, hart=hart)
         self.gdb = None
 
     def classSetup(self):
@@ -598,15 +651,12 @@ class GdbTest(BaseTest):
         if self.binary:
             self.gdb.command("file %s" % self.binary)
         if self.target:
-            self.gdb.command("set arch riscv:rv%d" % self.target.xlen)
+            self.gdb.command("set arch riscv:rv%d" % self.hart.xlen)
             self.gdb.command("set remotetimeout %d" % self.target.timeout_sec)
         if self.server.port:
             self.gdb.command(
                     "target extended-remote localhost:%d" % self.server.port)
-            # Select a random thread.
-            # TODO: Allow a command line option to force a specific thread.
-            thread = random.choice(self.gdb.threads())
-            self.gdb.thread(thread)
+            self.gdb.select_hart(self.hart)
 
         for cmd in self.target.gdb_setup:
             self.gdb.command(cmd)
@@ -618,6 +668,17 @@ class GdbTest(BaseTest):
         del self.gdb
         BaseTest.classTeardown(self)
 
+class GdbSingleHartTest(GdbTest):
+    def classSetup(self):
+        GdbTest.classSetup(self)
+
+        for hart in self.target.harts:
+            # Park all harts that we're not using in a safe place.
+            if hart != self.hart:
+                self.gdb.select_hart(hart)
+                self.gdb.p("$pc=loop_forever")
+        self.gdb.select_hart(self.hart)
+
 class ExamineTarget(GdbTest):
     def test(self):
         self.target.misa = self.gdb.p("$misa")