# MIT License # # Copyright (c) 2023 Neil Webber # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from types import SimpleNamespace import breakpoints as BKP from machine import PDP1170 from branches import BRANCH_CODES from pdptraps import PDPTraps import unittest import random import os import hashlib from pdpasmhelper import InstructionBlock class TestMethods(unittest.TestCase): PDPLOGLEVEL = 'WARNING' # used to create various instances, collects all the options # detail into this one place... mostly this is about loglevel @classmethod def make_pdp(cls): return PDP1170(loglevel=cls.PDPLOGLEVEL) @staticmethod def ioaddr(p, offs): """Given a within-IO-page IO offset, return an IO addr.""" return (offs + p.mmu.iopage_base) & 0o177777 # convenience routine to load word values into physical memory @staticmethod def loadphysmem(p, words, addr): for a, w in enumerate(words, start=(addr >> 1)): p.physmem[a] = w # some of these can't be computed at class definition time, so... @classmethod def usefulconstants(cls): p = cls.make_pdp() # meh, need this for some constants ns = SimpleNamespace() # Kernel instruction space PDR registers ns.KISD0 = cls.ioaddr(p, p.mmu.APR_KERNEL_OFFS) ns.KISD7 = ns.KISD0 + 0o16 # Kernel data space PDR registers ns.KDSD0 = ns.KISD0 + 0o20 ns.KDSD7 = ns.KDSD0 + 0o16 # Kernel instruction space PAR registers ns.KISA0 = ns.KDSD0 + 0o20 ns.KISA7 = ns.KISA0 + 0o16 # Kernel data space PAR registers ns.KDSA0 = ns.KISA0 + 0o20 ns.KDSA7 = ns.KDSA0 + 0o16 # User mode similar ns.UISD0 = cls.ioaddr(p, p.mmu.APR_USER_OFFS) ns.UDSD0 = ns.UISD0 + 0o20 ns.UISA0 = ns.UDSD0 + 0o20 ns.UDSA0 = ns.UISA0 + 0o20 ns.MMR0 = cls.ioaddr(p, p.mmu.MMR0_OFFS) ns.MMR3 = cls.ioaddr(p, p.mmu.MMR3_OFFS) return ns # # Create and return a test machine with a simple memory mapping: # Kernel Instruction space seg 0 points to physical 0 # Kernel Data space segment 0 also points to physical 0 # User instruction space seg 0 points to physical 0o20000 # User Data space seg 0 points to physical 0o40000 # and turns on the MMU # # premmu is an optional list of instructions to execute # before turning on the MMU # # postmmu is an optional list of instructions to execute # after turning on the MMU # def simplemapped_pdp(self, p=None, *, premmu=[], postmmu=[]): if p is None: p = self.make_pdp() cn = self.usefulconstants() # this is a table of instructions that ... # Puts the system stack at 0o20000 (8K) # Puts 0o22222 into physical location 0o20000 # Puts 0o33333 into physical location 0o20002 # Puts 0o44444 into physical location 0o40000 # Sets Kernel Instruction space A0 to point to physical 0 # Sets Kernel Data space A0 to point to physical 0 # Sets Kernel Data space A7 to point to the IO page # Sets User Instruction space A0 to point to physical 0o20000 # sets User Data space D0 to point to physical 0o40000 # and turns on the MMU with I/D sep # # These instructions will be placed at 2K in memory # a = InstructionBlock() a.mov(0o20000, 'sp') # start system stack at 8k # write the constants as described above a.mov(0o22222, a.ptr(0o20000)) a.mov(0o33333, a.ptr(0o20002)) a.mov(0o44444, a.ptr(0o40000)) # point both kernel seg 0 PARs to physical zero a.clr(a.ptr(cn.KISA0)) a.clr(a.ptr(cn.KDSA0)) # kernel seg 7 D space PAR to I/O page (at 22-bit location) a.mov(0o017760000 >> 6, a.ptr(cn.KDSA0 + (7 * 2))) # user I seg 0 to 0o20000, user D seg 0 to 0o40000 a.mov(0o20000 >> 6, a.ptr(cn.UISA0)) a.mov(0o40000 >> 6, a.ptr(cn.UDSA0)) # set the PDRs for segment zero a.mov(0o077406, 'r3') # 77406 = PDR<2:0> = ACF = 0o110 = read/write # PLF<14:8> =0o0774 = full length (128*64 bytes = 8K) a.mov('r3', a.ptr(cn.KISD0)) a.mov('r3', a.ptr(cn.KDSD0)) a.mov('r3', a.ptr(cn.UISD0)) a.mov('r3', a.ptr(cn.UDSD0)) # PDR for segment 7 a.mov('r3', a.ptr(cn.KDSD0 + (7 * 2))) # set previous mode to USER, keeping current mode KERNEL, pri 7 a.mov((p.KERNEL << 14) | (p.USER << 12) | (7 << 5), a.ptr(self.ioaddr(p, p.PS_OFFS))) # turn on 22-bit mode, unibus mapping, and I/D sep for k & u a.mov(0o000065, a.ptr(cn.MMR3)) # Instructions supplied by caller, to be executed before # enabling the MMU. They are "literals" since they have # already been assembled. for w in premmu: a.literal(w) # turn on relocation mode ... a.inc(a.ptr(cn.MMR0)) # and the post-MMU instructions for w in postmmu: a.literal(w) a.halt() instloc = 0o4000 # 2K self.loadphysmem(p, a, instloc) return p, instloc # test a typical sequence to set up a stack # This is really a test of label support in pdpasmhelper def test_fwdlabrel(self): a = InstructionBlock() a.clr('r0') # just to have something here # typical sequence to put stack at end of code a.mov('pc', 'r0') a.add(a.getlabel('stack', idxrel=True), 'r0') a.mov('r0', 'sp') a.halt() a.literal(0o111111) a.literal(0o777777) a.label('stack') p = self.make_pdp() self.loadphysmem(p, a, 0o10000) p.run(pc=0o10000) self.assertEqual(p.mmu.wordRW(p.r[6] - 2), 0o777777) self.assertEqual(p.mmu.wordRW(p.r[6] - 4), 0o111111) # these tests end up testing other stuff too of course, including MMU def test_mfpi(self): tvecs = [] for result, r1tval in ((0o33333, 2), (0o22222, 0)): # r1=r1tval, mfpi (r1) -> r0; expect r0 = result a = InstructionBlock() a.mov(r1tval, 'r1') a.mfpi('(r1)') a.mov('(sp)+', 'r0') tvecs.append((result, list(a)),) for result, insts in tvecs: with self.subTest(result=result, insts=insts): p, pc = self.simplemapped_pdp(postmmu=insts) p.run(pc=pc) self.assertEqual(p.r[0], result) def test_mfpxsp(self): cn = self.usefulconstants() u = InstructionBlock() u.mov('r2', 'r6') u.trap(0) premmu = InstructionBlock() premmu.mov(0o14000, premmu.ptr(0o34)) # set vector 034 to 14000 premmu.clr(premmu.ptr(0o36)) # PSW for trap - zero work premmu.mov(0o20000, 'r0') for uinst in u: premmu.mov(uinst, '(r0)+') premmu.mov(0o123456, 'r2') premmu.mov(0o140340, '-(sp)') # push user-ish PSW to K stack premmu.clr('-(sp)') # new user PC = 0 postmmu = InstructionBlock() postmmu.rtt() # RTT - goes to user mode, addr 0 p, pc = self.simplemapped_pdp(premmu=premmu, postmmu=postmmu) # put the trap handler at 14000 as expected th = InstructionBlock() th.mfpd('sp') th.mov('(sp)+', 'r3') th.halt() self.loadphysmem(p, th, 0o14000) p.run(pc=pc) self.assertEqual(p.r[2], p.r[3]) def test_mtpi(self): cn = self.usefulconstants() ts = InstructionBlock() ts.mov(0o1717, '-(sp)') # pushing 0o1717 ts.mtpi(ts.ptr(0o02)) # and MTPI it to user location 2 ts.clr(ts.ptr(cn.MMR0)) # turn MMU back off ts.mov(ts.ptr(0o20002), 'r0') # r0 = (020002) tvecs = ((0o1717, ts),) for r0result, insts in tvecs: with self.subTest(r0result=r0result, insts=insts): p, pc = self.simplemapped_pdp(postmmu=insts) p.run(pc=pc) self.assertEqual(p.r[0], r0result) def test_add_sub(self): p = self.make_pdp() testvecs = ( # (op0, op1, expected op0 + op1, nzvc, expected op0 - op1, nzvc) # None for nzvc means dont test that (yet/for-now/need to verify) (1, 1, 2, 0, 0, 4), # 1 + 1 = 2(_); 1 - 1 = 0(Z) (1, 32767, 32768, 0o12, 32766, 0), (0, 0, 0, 0o04, 0, 0o04), (32768, 1, 32769, 0o10, 32769, 0o13), (65535, 1, 0, 0o05, 2, 1), ) testloc = 0o10000 add_loc = testloc sub_loc = testloc + 4 for addsub, loc in (('add', add_loc), ('sub', sub_loc)): a = InstructionBlock() getattr(a, addsub)('r0', 'r1') a.halt() self.loadphysmem(p, a, loc) for r0, r1, added, a_nzvc, subbed, s_nzvc in testvecs: with self.subTest(r0=r0, r1=r1, op="add"): p.r[0] = r0 p.r[1] = r1 p.run(pc=add_loc) self.assertEqual(p.r[1], added) if a_nzvc is not None: self.assertEqual(p.psw & 0o17, a_nzvc) with self.subTest(r0=r0, r1=r1, op="sub"): p.r[0] = r0 p.r[1] = r1 p.run(pc=sub_loc) self.assertEqual(p.r[1], subbed) if s_nzvc is not None: self.assertEqual(p.psw & 0o17, s_nzvc) # test BNE (and, implicitly, INC/DEC) def test_bne(self): p = self.make_pdp() loopcount = 0o1000 a = InstructionBlock() a.mov(loopcount, 'r1') a.clr('r0') a.label('LOOP') a.inc('r0') a.dec('r1') a.bne('LOOP') a.halt() instloc = 0o4000 self.loadphysmem(p, a, instloc) p.run(pc=instloc) self.assertEqual(p.r[0], loopcount) self.assertEqual(p.r[1], 0) # test BEQ and BNE (BNE was also tested in test_bne) def test_eqne(self): p = self.make_pdp() goodval = 0o4321 # arbitrary, not zero a = InstructionBlock() a.clr('r1') # if successful r1 will become goodval a.clr('r0') a.beq('good') a.halt() # stop here if BEQ fails a.label('good') a.literal(0o000257) # 1f: CCC .. clear all the condition codes a.bne('good2') a.halt() # stop here if BNE fails a.label('good2') a.mov(goodval, 'r1') # indicate success a.halt() instloc = 0o4000 self.loadphysmem(p, a, instloc) p.run(pc=instloc) self.assertEqual(p.r[1], goodval) # create the instruction sequence shared by test_cc and test_ucc def _cc_unscc(self, br1, br2): a = InstructionBlock() # program is: # CLR R0 # MOV @#05000,R1 ; see discussion below # MOV @#05002,R2 ; see discussion below # CMP R1,R2 # br1 1f ; see discussion # HALT # 1: DEC R0 # CMP R2,R1 # br2 1f ; see discussion # HALT # 1: DEC R0 # HALT # # The test_cc and test_unscc tests will poke various test # cases into locations 5000 and 5002, knowing the order of # the operands in the two CMP instructions and choosing # test cases and br1/br2 accordingly. # # If the program makes it to the end R0 will be 65554 (-2) a.clr('r0') a.mov(a.ptr(0o5000), 'r1') a.mov(a.ptr(0o5002), 'r2') a.cmp('r1', 'r2') a.literal((br1 & 0o177400) | 1) # br1 1f a.halt() a.dec('r0') a.cmp('r2', 'r1') a.literal((br2 & 0o177400) | 1) # br2 1f a.halt() a.dec('r0') a.halt() return a def test_cc(self): # various condition code tests p = self.make_pdp() insts = self._cc_unscc(BRANCH_CODES['blt'], BRANCH_CODES['bgt']) instloc = 0o4000 self.loadphysmem(p, insts, instloc) # just a convenience so the test data can use neg numbers def s2c(x): return x & 0o177777 for lower, higher in ((0, 1), (s2c(-1), 0), (s2c(-1), 1), (s2c(-32768), 32767), (s2c(-32768), 0), (s2c(-32768), 32767), (17, 42), (s2c(-42), s2c(-17))): p.physmem[0o5000 >> 1] = lower p.physmem[0o5002 >> 1] = higher with self.subTest(lower=lower, higher=higher): p.run(pc=instloc) self.assertEqual(p.r[0], 65534) # probably never a good idea, but ... do some random values for randoms in range(1000): a = random.randint(-32768, 32767) b = random.randint(-32768, 32767) while a == b: b = random.randint(-32768, 32767) if a > b: a, b = b, a p.physmem[0o5000 >> 1] = s2c(a) p.physmem[0o5002 >> 1] = s2c(b) with self.subTest(lower=a, higher=b): p.run(pc=instloc) self.assertEqual(p.r[0], 65534) def test_mode3(self): # @(Rn)+ mode instloc = 0o4000 ptrloc = 0o020000 dataloc = 0o030000 bval = 0o42 a = InstructionBlock() a.mov(ptrloc, 'r0') a.mov(dataloc, a.ptr(ptrloc)) a.movb(bval, '@(r0)+') a.halt() p = self.make_pdp() self.loadphysmem(p, a, instloc) p.run(pc=instloc) self.assertEqual(p.physmem[dataloc >> 1], bval) self.assertEqual(p.r[0], ptrloc+2) def test_mode5(self): # @-(Rn) mode instloc = 0o4000 ptrloc = 0o020000 dataloc = 0o030000 bval = 0o42 a = InstructionBlock() a.mov(ptrloc+2, 'r0') # +2 because pre-decrement a.mov(dataloc, a.ptr(ptrloc)) a.movb(bval, '@-(r0)') a.halt() p = self.make_pdp() self.loadphysmem(p, a, instloc) p.run(pc=instloc) self.assertEqual(p.physmem[dataloc >> 1], bval) self.assertEqual(p.r[0], ptrloc) def test_swab(self): # mostly about testing the N bit which behaves... this way instloc = 0o4000 a = InstructionBlock() a.mov(0o377, 'r0') a.swab('r0') a.halt() a.swab('r0') a.halt() p = self.make_pdp() self.loadphysmem(p, a, instloc) p.run(pc=instloc) self.assertEqual(p.r[0], 0o377 << 8) self.assertFalse(p.psw_n) p.run() # resume after first halt self.assertEqual(p.r[0], 0o377) self.assertTrue(p.psw_n) def test_unscc(self): # more stuff like test_cc but specifically testing unsigned Bxx codes p = self.make_pdp() insts = self._cc_unscc(BRANCH_CODES['blo'], BRANCH_CODES['bhi']) instloc = 0o4000 self.loadphysmem(p, insts, instloc) for lower, higher in ((0, 1), (0, 65535), (32768, 65535), (65534, 65535), (32767, 32768), (17, 42)): p.physmem[0o5000 >> 1] = lower p.physmem[0o5002 >> 1] = higher with self.subTest(lower=lower, higher=higher): p.run(pc=instloc) self.assertEqual(p.r[0], 65534) # probably never a good idea, but ... do some random values for randoms in range(1000): a = random.randint(0, 65535) b = random.randint(0, 65535) while a == b: b = random.randint(0, 65535) if a > b: a, b = b, a p.physmem[0o5000 >> 1] = a p.physmem[0o5002 >> 1] = b with self.subTest(lower=a, higher=b): p.run(pc=instloc) self.assertEqual(p.r[0], 65534) def test_ash1(self): # this code sequence taken from Unix startup, it's not really # much of a test. a = InstructionBlock() a.mov(0o0122451, 'r2') neg6 = -6 & 0xFFFF a.ash(neg6, 'r2') a.bic(0o0176000, 'r2') a.halt() p = self.make_pdp() instloc = 0o4000 self.loadphysmem(p, a, instloc) p.run(pc=instloc) self.assertEqual(p.r[2], 0o1224) def test_shiftb(self): # test correct operation of byte operations on registers # r2 counts test progress a = InstructionBlock() a.clr('r2') a.mov(0o177401, 'r0') a.literal(0o106300) # ASLB R0 a.cmp(0o177402, 'r0') a.bne('fail') a.inc('r2') a.halt() a.label('fail') a.halt() p = self.make_pdp() instloc = 0o4000 self.loadphysmem(p, a, instloc) p.run(pc=instloc) self.assertEqual(p.r[2], 1) def test_br(self): # though the bug has been fixed, this is a test of whether # all branch offset values work correctly. Barn door shut... p = self.make_pdp() # the idea is a block of INC R0 instructions # followed by a halt, then a spot for a branch # then a block of INC R1 instructions followed by a halt # # By tweaking the BR instruction (different forward/back offsets) # and starting execution at the BR, the result on R0 and R1 # will show if the correct branch offset was effected. # # NOTE: 0o477 (branch offset -1) is a tight-loop branch to self # and that case is tested separately. # insts = [0o5200] * 300 # 300 INC R0 instructions insts += [0] # 1 HALT instruction insts += [0o477] # BR instruction .. see below # want to know where in memory this br will is brspot = len(insts) - 1 insts += [0o5201] * 300 # 300 INC R1 instructions insts += [0] # 1 HALT instruction # put that mess into memory at an arbitrary spot baseloc = 0o10000 self.loadphysmem(p, insts, baseloc) # test the negative offsets: # Set R0 to 65535 (-1) # Set R1 to 17 # -1 is a special case, that's the tight loop and not tested here # -2 reaches the HALT instruction only, R0 will remain 65535 # -3 reaches back to one INC R0, R0 will be 0 # -4 reaches back two INC R0's, R0 will be 1 # and so on # 0o400 | offset starting at 0o376 will be the BR -2 case expected_R0 = 65535 for offset in range(0o376, 0o200, -1): p.physmem[(baseloc >> 1) + brspot] = (0o400 | offset) p.r[0] = 65535 p.r[1] = 17 # note the 2* because PC is an addr vs physmem word index p.run(pc=baseloc + (2*brspot)) with self.subTest(offset=offset): self.assertEqual(p.r[0], expected_R0) self.assertEqual(p.r[1], 17) expected_R0 = (expected_R0 + 1) & 0o177777 # and the same sort of test but with forward branching expected_R1 = 42 + 300 for offset in range(0, 0o200): p.physmem[(baseloc >> 1) + brspot] = (0o400 | offset) p.r[0] = 17 p.r[1] = 42 # note the 2* because PC is an addr vs physmem word index p.run(pc=baseloc + (2*brspot)) with self.subTest(offset=offset): self.assertEqual(p.r[0], 17) self.assertEqual(p.r[1], expected_R1) expected_R1 = (expected_R1 - 1) & 0o177777 def test_div(self): # test the div instruction # The 32-bit int in R and R|1 is divided by the src operand p = self.make_pdp() a = InstructionBlock() # The test cases will be X / Y: # X : 1, 255, 4096, 10017, 32767, 32768, 32769 # and then those same values with 690000 added to them # Y : -50 .. 50 but skipping 0 and using a large number # # The code is written this way so that the resulting block # is completely self-contained (does not rely on python to # drive it). This made it easier to cross-verify w/SIMH # As described above: X test values. # *** SHA256 NOTE: DO NOT CHANGE THESE TEST VALUES. # THE TEST RELIES ON A PRECOMPUTED SHA256 HASH BASED # ON RESULTS FROM THESE VALUES xvals = (1, 255, 4096, 10017, 32767, 32768, 32769) xtable = 0o20000 # address for storing the above results = 0o30000 # address for storing results list # instead of div by zero, div by this randomish large number # *** DO NOT CHANGE; see sha256 note above largedivisor = 10017 # has to be 16 bits or less # The divisor will run from -this to this. # *** DO NOT CHANGE; see sha256 note above divisorrange = 50 a.clr(a.ptr(0o177776)) a.mov(xtable, 'r0') for x in xvals: # same data but negated xneg = ((p.MASK32 + 1) - x) & p.MASK32 # same data but 690000 arbitrarily added (to make it 32bit) xplus = x + 690000 # ...and negated xplusneg = ((p.MASK32 + 1) - xplus) & p.MASK32 # put all of those into the dividend table for v in (x, xneg, xplus, xplusneg): a.mov((v >> 16) & p.MASK16, '(r0)+') a.mov(v & p.MASK16, '(r0)+') a.clr('(r0)+') # sentinel a.clr('(r0)') # sentinel # test loop. Divisor in r4. Dividend in r2/r3 # xval pointer in r0. Results pointer in r1 a.mov(results, 'r1') a.mov(-divisorrange, 'r4') a.label('outer') a.mov(xtable, 'r0') a.label('inner') a.mov('(r0)+', 'r2') a.mov('(r0)+', 'r3') a.tst('r2') a.bne('divide') a.tst('r3') a.bne('divide') # hit the sentinel, bump the divisor # look for the large divisor and forge a zero to get to 1 a.cmp('r4', largedivisor) a.bne('bump') a.clr('r4') a.label('bump') a.inc('r4') a.bne('nz') a.mov(largedivisor, 'r4') a.br('outer') a.label('nz') a.cmp('r4', divisorrange) a.ble('outer') a.mov(69, 'r0') # this indicates success a.halt() a.label('divide') # divide instruction hand-assembled a.literal(0o071204) # first save the PSW a.mov(a.ptr(0o177776), '(r1)+') a.mov('r2', '(r1)+') a.mov('r3', '(r1)+') a.br('inner') self.loadphysmem(p, a, 0o10000) p.run(pc=0o10000) # --- this is how the above was exported for use in SIMH --- # with open('div.ini', 'w') as f: # for s in a.simh(startaddr=0o10000): # f.write(s + '\n') # # Then that .ini file was loaded into SIMH, the program was run, # and the output data was downloaded from SIMH. The results are # huge, so that was sha256 hashed into this one "good" hash value. good = ('f5e525b90728cb6fc4eecd97ad4b36c' '995d2e5b8890f7c0531284615ee9958d4') # so see if the live results hash to the same value def bytify(): for a in range(0o30000, p.r[1], 2): m = p.mmu.wordRW(a) yield m & 0xFF yield (m >> 8) & 0xFF h = hashlib.sha256() h.update(bytes(bytify())) self.assertEqual(good, h.hexdigest()) def test_trap(self): # test some traps p = self.make_pdp() # put a handlers for different traps into memory # starting at location 0o10000 (4K). This just knows # that each handler is 3 words long, the code being: # MOV something,R4 # RTT # # where the "something" changes with each handler. handlers_addr = 0o10000 handlers = ( 0o012704, 0o4444, 0o000006, # for vector 0o004 0o012704, 0o1010, 0o000006, # for vector 0o010 0o012704, 0o3030, 0o000006, # for vector 0o030 0o012704, 0o3434, 0o000006 # for vector 0o034 ) self.loadphysmem(p, handlers, handlers_addr) # and just jam the vectors in place p.physmem[2] = handlers_addr # vector 0o004 p.physmem[3] = 0 # new PSW, stay in kernel mode p.physmem[4] = handlers_addr + 6 # each handler above was 6 bytes p.physmem[5] = 0 p.physmem[12] = handlers_addr + 12 # vector 0o30 (EMT) p.physmem[13] = 0 p.physmem[14] = handlers_addr + 18 # vector 0o34 (TRAP) p.physmem[15] = 0 # (tnum, insts) testvectors = ( # this will reference an odd address, trap 4 (0o4444, ( # establish reasonable stack pointer (at 8K) 0o012706, 0o20000, # CLR R3 and R4 so will know if they get set to something 0o005003, 0o005004, # put 0o1001 into R0 0o012700, 0o1001, # and reference it ... boom! 0o011001, # show that the RTT got to here by putting magic into R3 0o012703, 0o123456)), # this will execute a reserved instruction trap 10 (0o1010, ( # establish reasonable stack pointer (at 8K) 0o012706, 0o20000, # CLR R3 and R4 so will know if they get set to something 0o005003, 0o005004, # 0o007777 is a reserved instruction ... boom! 0o007777, # show that the RTT got to here by putting magic into R3 0o012703, 0o123456)), # this will execute an EMT instruction (0o3030, ( # establish reasonable stack pointer (at 8K) 0o012706, 0o20000, # CLR R3 and R4 so will know if they get set to something 0o005003, 0o005004, # EMT #42 0o104042, # show that the RTT got to here by putting magic into R3 0o012703, 0o123456)), # this will execute an actual TRAP instruction (0o3434, ( # establish reasonable stack pointer (at 8K) 0o012706, 0o20000, # CLR R3 and R4 so will know if they get set to something 0o005003, 0o005004, # TRAP #17 0o104417, # show that the RTT got to here by putting magic into R3 0o012703, 0o123456)), ) for R4, insts in testvectors: self.loadphysmem(p, insts, 0o3000) p.run(pc=0o3000) self.assertEqual(p.r[3], 0o123456) self.assertEqual(p.r[4], R4) def test_trapcodes(self): # a more ambitious testing of TRAP which verifies all # available TRAP instruction codes work p = self.make_pdp() # poke the TRAP vector info directly in p.physmem[14] = 0o10000 # vector 0o34 (TRAP) --> 0o10000 p.physmem[15] = 0 # this trap handler puts the trap # into R3 handler = InstructionBlock() # the saved PC is at the top of the stack ... get it handler.mov('(sp)', 'r0') # get the low byte of the instruction which is the trap code # note that the PC points after the TRAP instruction so: handler.movb('-2(r0)', 'r3') handler.rtt() self.loadphysmem(p, handler, 0o10000) # just bash a stack pointer directly in p.r[6] = 0o20000 # 8K and working down for i in range(256): a = InstructionBlock() a.trap(i) # TRAP #i a.mov('r3', 'r1') # MOV R3,R1 just to show RTT worked a.halt() self.loadphysmem(p, a, 0o30000) p.run(pc=0o30000) self.assertEqual(p.r[3], p.r[1]) # because the machine code did MOVB, values over 127 get # sign extended, so take that into consideration if i > 127: trapexpected = 0xFF00 | i else: trapexpected = i self.assertEqual(p.r[1], trapexpected) def _make_updown(self, taddr, uaddr, kaddr, uphysdata=0o200000): # Makes the instruction blocks required for the mmu_updown tests. # This is separated out so (as described below) it becomes possible # to execute this in isolation just to generate the instructions # and use them in other simulators (e.g., SIMH) # # Returns three tuples: # (taddr, t), (uaddr, u), (k addr, k) # where the t/u/k elements are the instruction blocks. # the kernel stack will start at 8K and work its way down. # This is fixed/required in the code. The taddr and kaddr must also # be in that first 8K and MUST leave space for the kernel stack. # The kernel stack is arbitrarily given at least 256 bytes # (in reality it doesn't come close to using that) APR0_end = 0o20000 if APR0_end - max(taddr, kaddr) < 256: raise ValueError("not enough room for kernel stack") kernel_stack = APR0_end # there are some variable locations needed in the code, they # are allocated from the "stack", like this: kernel_stack -= 2 saved_r5 = kernel_stack cn = self.usefulconstants() # Two tests - up and down. # # In both tests, KERNEL I space page 0 is mapped to physical 0 # and KERNEL I space page 7 is mapped to the I/O page. # I/D separation is NOT enabled for KERNEL. # taddr and kaddr MUST be in this first 8K of memory (if only # because the mapping setup doesn't map anything else) # # USER I space is mapped to uaddr which can be any 64-byte # (i.e., mappable to user 0) boundary 0o20000 and beyond. # # All 64K of USER D space is mapped to 64K of physical memory # from uphysdata to uphysdata + 64K, but with a bizarre page # length scheme according to UP or DOWN phase of the test as # below. I/D separation is (obviously) enabled for USER. # All 64K of that memory is filled with sequential words such # that (vaddr) + vaddr = 0o123456 (where vaddr is a user D space # virtual address 0 .. 65534). This gives the test two ways to verify # the MMU map is working correctly: by where the accessibility of a # segment ends and by the value at the location where it ends. # # Segment pages in the PDP-11 are broken down into 32-word (64-byte) # units called "blocks" in the manual. There are 128 blocks in # each 8KB page. # # For UP direction test: # # using ED=0 (segments grow upwards), create a user DSPACE mapping # where page zero has length ("PLF") 0, page 1 has # length 16, page 2 has length 32 (all measured in blocks) etc... # and then check that valid addresses map correctly and # invalid ones fault correctly. Note a subtle semantic of the PDP # page length field: to be invalid (in an upward growing segment) # the address has to be GREATER than the computed block number. # Thus a length of "zero" still has 1 valid block of words. # # For DOWN: # using ED=1 ("dirbit" = 0o10) segments grow downwards, with the # same 0, 16, 32 .. progression (of valid "blocks") but they # are at the end of the segments. # # This test can be rightly criticized as unnecessarily complex; # once the math has been shown correct for a few important length # cases to consider, the possibility of further bugs is remote. # Nevertheless, here it is. # this code will go at taddr tr = InstructionBlock() # The trap handler for MMU faults and the trap 0 / trap 1 from # the user code (when there is no MMU fault). It is integrated # into one routine. # # The user code gets to use r0 and r1, the trap handler # gets to use r2-r5: # r2: expected good flag (initialized elsewhere) # r3: determined good flag (by trap entry) # r5: test table data pointer (initialized elsewhere) tr.label('UTrap') # first determine if trap0 (bad) or trap1 (good) tr.mov('(sp)', 'r3') # get user PC from trap frame tr.mfpi('-2(r3)') # get the trap instruction tr.mov('(sp)+', 'r3') # r3 is now the trap instruction tr.bic(0o177400, 'r3') tr.cmp(1, 'r3') tr.beq('common') # skip the HALT and the MMU entry point # this was not a "good" trap, the user code failed tr.halt() tr.label('TrapMMU') tr.clr('r3') # indicate MMU fault case # both Utrap and TrapMMU join in common here on out # see if the access was good/bad as expected tr.label('common') tr.cmp('r2', 'r3') tr.beq('bump') # jump over the HALT tr.halt() # NOPE, something wrong! # the user mode code specifically avoids '(r0)+' # to avoid ambiguity in machine types for when the # autoincrement happens in the face of MMU aborts. # Bump r0 for the user code here accordingly: tr.label('bump') tr.add(2, 'r0') # see if it is time to switch to next table entry tr.cmp('2(r5)', 'r0') tr.bne('rtu') # skip over the "time to switch" stanza # it is time to switch tr.add(4, 'r5') tr.mov('(r5)', 'r2') tr.cmp(0o666, 'r2') tr.bne('rtu') tr.halt() # test done; success if r2 = 0o666 tr.label('rtu') # next iteration of the user code loop tr.clr('(sp)') # put user PC back to zero tr.rtt() # this trap handler is only used during the startup phase # See where the kernel code invokes the user setup code tr.label('trap_usersetup') # the kernel put a resume address onto the stack, just go there tr.add(4, 'sp') # get rid of user trap frame, don't care tr.mov('(sp)+', 'pc') # user mode program: there are two parts to this # Starting at (user) location ZERO: the test program: # read the given address: mov (r0),r1 # If this causes an MMU fault, it goes to the MMU trap handler # If it succeeds, it then hits the trap(1) and goes to that # handler. The kernel trap handler, using the test table, # can then validate that the right thing happened. # The code "loops" only because the kernel trap handler resets # the PC to zero and bumps r0 then returns to user mode for the # next iteration. (Yes, all this could have been done with mfpd # but that feels like a different test than this) # # Start at (user) location labelled 'setup' # user-mode code executed before the test begins to put # the test pattern into memory u = InstructionBlock() # this subtract combines the access check with part1 of chksum u.mov(0o123456, 'r1') u.sub('(r0)', 'r1') u.cmp('r0', 'r1') u.beq('good') u.trap(0o77) # trap 77 indicates miscompare u.label('good') u.trap(1) # indicate good status # the kernel puts the PC back to zero after the good trap # and also bumps r0. This is how the loop loops. u.halt() # never get here, this is illegal # this code is executed one time at startup (see kernel code) u.label('setup') # Initialize the user D space pattern that will be checked # by the user code. In python this was: # # checksum = 0o123456 # arbitrary # user_phys_DSPACEbase = 0o200000 # words = (checksum - (user_phys_DSPACEbase + o) & 0o177777 # for o in range(0, 65536, 2)) # self.loadphysmem(p, words, user_phys_DSPACEbase) u.clr('r0') u.mov(0o123456, 'r1') u.label('pattern') u.mov('r1', '(r0)+') u.sub(2, 'r1') u.tst('r0') u.bne('pattern') # the kernel code looks for this in r1 as success flag u.mov(0o3333, 'r1') u.trap(0) # The kernel-mode code that drives the whole test # this is certainly one way to allocate data variables, # given the limitations of the non-assembler ASM() methodology... # # The stack will start at kstackstart k = InstructionBlock() k.mov(kernel_stack, 'sp') # KERNEL I SPACE # PAR 0 to physical 0 # PAR 7 to physical 760000 and 22bit not turned on # # PDR 77406 = read/write, full length k.clr(k.ptr(cn.KISA0)) k.mov(0o760000 >> 6, k.ptr(cn.KISA7)) k.mov(0o077406, k.ptr(cn.KISD0)) k.mov(0o077406, k.ptr(cn.KISD7)) # USER I SPACE k.mov(uaddr >> 6, k.ptr(cn.UISA0)) k.mov(0o077406, k.ptr(cn.UISD0)) # USER D SPACE... first set it up to be simply/fully # accessible at its physical home, so that the pattern # can be set. Then come back and limit the page lengths later. k.mov(cn.UDSD0, 'r3') # will walk through D0 .. D7 # NOTE: A0 .. A7 is 040(r3) k.mov(uphysdata >> 6, 'r4') # phys addr base k.mov(8, 'r0') k.label('utmp') k.mov('r4', '040(r3)') # set U PAR; don't bump r3 yet k.add(0o200, 'r4') # 0o200 = 8192>>6 k.mov(0o77406, '(r3)+') # set U PDR and bump to next k.sob(0, 'utmp') k.bis(1, k.ptr(cn.MMR3)) # enable I/D sep just for USER k.mov(1, k.ptr(cn.MMR0)) # turn on MMU # certainly could have used mtpd to set up the user mode # pattern but this is just another excuse to test more things # jump to the user 'setup' code, but first establish a handler # for the trap it will execute when done. k.mov(taddr + tr.getlabel('trap_usersetup'), '*$34') k.mov(0o340, '*$36') k.mov('pc', '-(sp)') # add the offset to (forward ref) back_from_u. k.add(k.getlabel('back_from_u', idxrel=True), '(sp)') k.mov(0o140340, '-(sp)') # push user-ish PSW to K stack k.mov(u.getlabel('setup'), '-(sp)') # PC for setup code k.rtt() k.label('back_from_u') # user code dropped this magic value into r1 on success k.cmp(0o3333, 'r1') k.beq('ok') k.halt() k.label('ok') # and now set the length limits on the user D space k.mov(cn.UDSD0, 'r3') # will walk through D0 .. D7 k.clr('r0') # r0: segno*2 = (0, 2, 4, .., 14) k.label('PDRloop') k.mov('r0', 'r2') # r2 = segno*2 k.ash(3, 'r2') # r2 = segno*16 k.swab('r2') # really (segno*16)<<8 k.add(0o06, 'r2') # ACF r/w segment k.mov('r2', '(r3)+') # set U PDR k.inc('r0') # bump r0 by two k.inc('r0') k.cmp('r0', 16) # and loop until done all 8 segments k.blt('PDRloop') # create the test table, just push it onto the stack (yeehah!) k.mov(0, '-(sp)') # this is a PAD (not really needed) k.mov(0o666, '-(sp)') # this is a sentinel k.mov(0, '-(sp)') k.mov(0, '-(sp)') k.mov(0o176100, '-(sp)') k.mov(1, '-(sp)') k.mov(0o160000, '-(sp)') k.mov(0, '-(sp)') k.mov(0o154100, '-(sp)') k.mov(1, '-(sp)') k.mov(0o140000, '-(sp)') k.mov(0, '-(sp)') k.mov(0o132100, '-(sp)') k.mov(1, '-(sp)') k.mov(0o120000, '-(sp)') k.mov(0, '-(sp)') k.mov(0o110100, '-(sp)') k.mov(1, '-(sp)') k.mov(0o100000, '-(sp)') k.mov(0, '-(sp)') k.mov(0o66100, '-(sp)') k.mov(1, '-(sp)') k.mov(0o60000, '-(sp)') k.mov(0, '-(sp)') k.mov(0o44100, '-(sp)') k.mov(1, '-(sp)') k.mov(0o40000, '-(sp)') k.mov(0, '-(sp)') k.mov(0o22100, '-(sp)') k.mov(1, '-(sp)') k.mov(0o20000, '-(sp)') k.mov(0, '-(sp)') k.mov(0o100, '-(sp)') k.mov(1, '-(sp)') # the test table for the trap handler is now here: k.mov('sp', 'r5') k.mov('sp', k.ptr(saved_r5)) # so it can be recovered later # test starts in the region at the start of the table k.mov('(r5)', 'r2') # poke the MMU trap handler vector (250) k.mov(taddr + tr.getlabel('TrapMMU'), '*$250') k.mov(0o340, '*$252') # same for the "trap N" handler k.mov(taddr + tr.getlabel('UTrap'), '*$34') k.mov(0o340, '*$36') # ok, now ready to start the user program k.mov(0o140340, '-(sp)') # push user-ish PSW to K stack k.clr('-(sp)') # new user PC = 0 k.clr('r0') # user test expects r0 to start zero k.rtt() # this is where the DOWN test starts. k.label('DOWNTEST') # re-establish initial kernel stack k.mov(kernel_stack, 'sp') # Redo the entire test table for the down address cases # these were precomputed from the algorithm for setting PDRs k.mov(0, '-(sp)') k.mov(0o666, '-(sp)') k.mov(0, '-(sp)') k.mov(1, '-(sp)') k.mov(0o161700, '-(sp)') k.mov(0, '-(sp)') k.mov(0o160000, '-(sp)') k.mov(1, '-(sp)') k.mov(0o143700, '-(sp)') k.mov(0, '-(sp)') k.mov(0o140000, '-(sp)') k.mov(1, '-(sp)') k.mov(0o125700, '-(sp)') k.mov(0, '-(sp)') k.mov(0o120000, '-(sp)') k.mov(1, '-(sp)') k.mov(0o107700, '-(sp)') k.mov(0, '-(sp)') k.mov(0o100000, '-(sp)') k.mov(1, '-(sp)') k.mov(0o71700, '-(sp)') k.mov(0, '-(sp)') k.mov(0o60000, '-(sp)') k.mov(1, '-(sp)') k.mov(0o53700, '-(sp)') k.mov(0, '-(sp)') k.mov(0o40000, '-(sp)') k.mov(1, '-(sp)') k.mov(0o35700, '-(sp)') k.mov(0, '-(sp)') k.mov(0o20000, '-(sp)') k.mov(1, '-(sp)') k.mov(0o17700, '-(sp)') k.mov(1, '-(sp)') k.mov('sp', 'r5') # r5 is where the table starts k.mov('sp', k.ptr(saved_r5)) # store location for re-use # fiddle the PDRs (PARs stay the same) for the down configuration k.mov(cn.UDSD0, 'r3') k.clr('r0') k.label('PARloopDOWN') # compute segno * 8 in r2 (r0 starts as segno*2) k.mov('r0', 'r2') k.ash(3, 'r2') # pln = 0o177 - (segno * 16) k.mov(0o177, 'r1') k.sub('r2', 'r1') k.mov('r1', 'r2') k.swab('r2') k.add(0o16, 'r2') # the downward growing case k.mov('r2', '(r3)+') # set U PDR k.inc('r0') k.inc('r0') k.cmp('r0', 16) k.blt('PARloopDOWN') k.clr('r2') # the down test starts in 'bad' zone # ok, now ready to start the user program k.mov(0o140340, '-(sp)') # push user-ish PSW to K stack k.clr('-(sp)') # new user PC = 0 k.clr('r0') # user test expects r0 to start zero k.rtt() # Now for something extra frosty... relocate just segment 4 # (arbitrarily chosen) of the user memory to a different # physical page and run the test again to ensure it still works. # This will make use of KERNEL A1 and A2 segments to map the # relocation (note: I space because no sep I/D for kernel here) k.label('BONUS') # recover the r5 stack table beginning k.mov(k.ptr(saved_r5), 'r5') # copy UDSA4 into KISA1 - mapping old segment into kernel space k.mov(k.ptr(cn.UDSA0 + 4*2), k.ptr(cn.KISA0 + 2)) # i.e., A1 # the new location for this data will be physical 0o600000 # (not a typo) which becomes 0o6000 in the PAR k.mov(0o6000, k.ptr(cn.KISA0 + 4)) # i.e., A2 # the standard PDR access/full-length/etc bits k.mov(0o077406, k.ptr(cn.KISD0 + 2)) k.mov(0o077406, k.ptr(cn.KISD0 + 4)) # count r0, source address r1, destination r2 k.mov(4096, 'r0') k.mov(8192, 'r1') k.mov(8192*2, 'r2') k.mov('(r1)+', '(r2)+') k.literal(0o077002) # SOB to the copy # switch the user page to the new mapping k.mov(0o6000, k.ptr(cn.UDSA0 + 4*2)) # and the standard initialization dance k.clr('r2') # the down test starts in 'bad' zone k.clr('r0') k.clr('(sp)') # just knows the user loop starts at zero k.rtt() return (taddr, tr), (uaddr, u), (kaddr, k) def test_mmu_updown(self): # test the page length field support in both up and down directions # This somewhat-irresponsible magnum opus of assembly code would # have been much easier to write, understand, and maintain if it # used the PDP1170/mmu/etc methods directly and was mostly # written at the python level rather than elaborate machine code. # For example, it could have looped over calls to mmu.v2p() # instead of performing an elaborate dance of user mode code # and kernel trap handlers to analyze the same thing. # # HOWEVER, doing it as machine code allowed the same instructions # to be run through SIMH for cross-verification of the test itself # and the machine behavior. Was the juice worth the squeeze? # Someone else will have to decide; the deed is done. # # Note that the assembly of three code segments # (the trap handlers, the user code, and the "kernel") are # in a separate method, which is helpful for getting at the # instructions in the "import to SIMH" usage scenario. # cn = self.usefulconstants() p = self.make_pdp() # On these addresses: the code isn't fully general (mostly in # how the MMU is set up). The kernel stack will start at 8K # (0o20000 physical) and work downwards. The traps and kernel # code can be "anywhere" so long as it is in that first 8K # of memory and leaves room for trap vectors at the bottom and # stack at the top. # # The user code can be on any 64-byte (!) physical boundary. # The kernel knows the virtual address for the start will be zero. # # this are all PHYSICAL addresses. The code is not fully general, # there are constraints: taddr and kaddr must be in the first # 8K of physical memory (if only because that's how the trivial # kernel mapping is set up). The kernel stack, which also # requires room for the test tables, must start at the tail end # of the first 8K. # # The uaddr must be on an 8K boundary taddr = 0o4000 kaddr = 0o6000 # make sure enough room for the traps code uaddr = 0o20000 for addr, b in self._make_updown(taddr, uaddr, kaddr): if addr == kaddr: # need to know DOWNTEST and BONUS downtest = kaddr + b.getlabel('DOWNTEST') bonus = kaddr + b.getlabel('BONUS') self.loadphysmem(p, b, addr) with self.subTest(phase="UP"): # finally ready to run the whole shebang! p.run(pc=kaddr) # a halt was encountered, verify r2 is the end sentinel self.assertEqual(p.r[2], 0o666) with self.subTest(phase="DOWN"): # run the down test p.r[2] = 0 # superfluous but makes sure p.run(pc=downtest) self.assertEqual(p.r[2], 0o666) with self.subTest(phase="BONUS"): # and the bonus test p.r[2] = 0 # superfluous but makes sure p.run(pc=bonus) self.assertEqual(p.r[2], 0o666) def test_mmu_AWbits(self): cn = self.usefulconstants() p = self.make_pdp() base_address = 0o10000 k = InstructionBlock() # this is silly but easy, just put the trap handler here and # jump over it k.br('L1') k.label('traphandler') k.rtt() k.label('L1') k.mov(base_address, 'sp') # system stack just below this code # # No I/D separation turned on, so everything is mapped via I SPACE # PAR 0 to physical 0 # PAR 1 to physical 8K # PAR 2 to physical 16K # ... etc ... 1:1 virtual:physical mapping up to ... # PAR 7 to phys 760000 and 22bit not turned on (i.e., I/O page) # k.clr('r2') # r2 will step by 0o200 for 8K PAR incrs k.mov(cn.KISA0, 'r0') # r0 will chug through the PARs k.mov(7, 'r1') # count of PARs to set k.label('parloop') k.mov('r2', '(r0)+') k.add(0o200, 'r2') k.sob('r1', 'parloop') # set the PAR7 to I/O page k.mov(0o7600, '(r0)') # now the PDRs k.mov(cn.KISD0, 'r4') k.mov(0o77406, '(r4)+') # read/write, full length for PDR0 k.mov(0o77404, 'r2') # r/w, full length, trap on any r/w k.mov(6, 'r1') # setting PDR1 .. PDR6 k.label('pdrloop') k.mov('r2', '(r4)+') k.sob('r1', 'pdrloop') k.mov(0o77406, '(r4)+') # r/w, full length for PDR7 / IO page # NOTE: at this point '-(r4)' will be PDR7 ... that is used below # set up the trap handler k.mov(base_address + k.getlabel('traphandler'), '*$250') k.mov(0o340, '*$252') k.mov(1, k.ptr(cn.MMR0)) # turn on MMU # this test code just "knows" the code is in APR0 (8K and below) # and makes three accesses: # a READ at 12K (APR1) # a WRITE at 20K (APR2) # a READ-then-WRITE at 28K (APR3) # # then it dumps the 8 kernel PDRS onto the stack in reverse # (so that at the end (sp) -> PDR0 # k.mov(0o20000, 'r0') # 8K will will be the base for: k.mov('010000(r0)', 'r1') # read from 12K k.mov(1234, '030000(r0)') # write to 20K k.inc('050000(r0)') # read-then-write to 28K # push (the dumb way) PDRs onto the stack for examination k.mov('-(r4)', '-(sp)') k.mov('-(r4)', '-(sp)') k.mov('-(r4)', '-(sp)') k.mov('-(r4)', '-(sp)') k.mov('-(r4)', '-(sp)') k.mov('-(r4)', '-(sp)') k.mov('-(r4)', '-(sp)') k.mov('-(r4)', '-(sp)') # expected: # * PDR0: W only, mgmt traps not set here # * PDR1 to be only A bit # * PDR2 to be A and W # * PDR3 to be A and W # * PDR4-6 to be neither # * PDR7 (not really part of the test but will be neither) # These expected_PDRs were obtained by running the machine # code in this test under SIMH. expected_PDRs = [0o077506, 0o077604, 0o077704, 0o077704, 0o077404, 0o077404, 0o077404, 0o077406] k.clr('r0') k.mov('sp', 'r1') for i, xpdr in enumerate(expected_PDRs): k.cmp(xpdr, '(r1)+') k.beq(f"LXX{i}") k.mov(i | 0o100000, 'r0') k.mov('-2(r1)', 'r3') k.br('_done') k.label(f"LXX{i}") k.label('_done') k.halt() self.loadphysmem(p, k, base_address) p.run(pc=base_address) self.assertEqual(p.r[0], 0) def test_stacklim0(self): # verify that simply *having* an illegal SP doesn't trap p = self.make_pdp() a = InstructionBlock() a.clr('r0') # will be used to verify progress # none of these bad stack pointers should cause # a YELLOW trap as they are never used as stacks... a.clr('sp') # really it's already zero... a.inc('r0') # show made it to here a.mov(1, 'sp') # odd SP, very bad idea a.inc('r0') # show made it to here a.mov(0o100, 'sp') # still too low a.inc('r0') # show made it to here a.halt() aa = 0o4000 self.loadphysmem(p, a, aa) p.run(pc=aa) self.assertEqual(p.r[0], 3) # confirm made it all the way through def _stacklimcode(self, go_red=False): # memory usage: # 0o4000.. is the test code # 0o6000.. is the trap handler # 0o7000.. is the log of various values collected in the trap handler # r5 is used to walk through the 0o7000+ storage, it is initialized # in the test code and used in the trap handler p = self.make_pdp() tr = InstructionBlock() # record... tr.mov('r2', '(r5)+') # ...separator/entry number tr.mov('sp', '(r5)+') # ...the sp tr.mov('(sp)', '(r5)+') # ...the trap-saved pc tr.mov(tr.ptr(0o177766), 'r1') # (will be used later) tr.mov('r1', '(r5)+') # ...cpu error register tr.mov('r2', '(r5)+') # ...separator/entry number # indicate successfully completed the above, bump entry number tr.inc('r2') # but if RED trap, stop here. tr.bit(p.CPUERR_BITS.REDZONE, 'r1') tr.beq('rtt') tr.halt() tr.label('rtt') tr.rtt() tra = 0o6000 self.loadphysmem(p, tr, tra) recordmagic = 0o66000 a = InstructionBlock() a.mov(0o400, 'sp') a.mov(0o7000, 'r5') a.mov(recordmagic, 'r2') # dirty up the (to be pushed to) stack to verify writes happened a.mov(0o370, 'r0') a.mov('r0', '(r0)+') a.mov('r0', '(r0)+') a.mov('r0', '(r0)+') a.mov('r0', '(r0)+') # install the trap handler a.mov(tra, a.ptr(0o4)) a.mov(0o340, a.ptr(0o6)) loopcount = 3 if not go_red else 30 # will never get to 30 a.mov(loopcount, 'r0') a.label('push') a.clr('-(sp)') a.sob('r0', 'push') a.halt() aa = 0o4000 self.loadphysmem(p, a, aa) p.r[p.PC] = aa return p def test_stacklim1(self): # Set the stack at the top of the yellow zone and then use it. # This test should cause loopcount (3) YELLOW synchronous traps. # Behavior and expected results verified by running identical # machine code in SIMH # r5 is used to walk through the 0o7000+ storage, it is initialized # in the test code and used in the trap handler p = self._stacklimcode() p.run() # obtained by running machine code in SIMH expected_7000 = [ # MARKER SP PC CPUERR MARKER 0o066000, 0o000372, 0o004052, 0o000010, 0o066000, 0o066001, 0o000370, 0o004052, 0o000010, 0o066001, 0o066002, 0o000366, 0o004052, 0o000010, 0o066002, 0] recbase = 0o7000//2 # word address in phys mem for i, val in enumerate(expected_7000): with self.subTest(i=i, val=val): self.assertEqual(val, p.physmem[recbase + i]) def test_stacklim_red(self): p = self._stacklimcode(go_red=True) p.run() # Behavior/results verified by running machine code on SIMH; # however, SIMH halts the simulation on the red stack trap and # requires intervention to continue into the actual trap handler. # Doing that (i.e., "CONTINUE") leads to these same results. self.assertEqual(p.r[1], 0o14) # RED|YELLOW self.assertEqual(p.r[2], 0o66021) # known magic iteration marker self.assertEqual(p.r[6], 0) # stack should be at zero # these results obtained from SIMH (running same machine code) expected_7000 = [ # MARKER SP PC CPUERR MARKER 0o066000, 0o000372, 0o004052, 0o000010, 0o066000, 0o066001, 0o000370, 0o004052, 0o000010, 0o066001, 0o066002, 0o000366, 0o004052, 0o000010, 0o066002, 0o066003, 0o000364, 0o004052, 0o000010, 0o066003, 0o066004, 0o000362, 0o004052, 0o000010, 0o066004, 0o066005, 0o000360, 0o004052, 0o000010, 0o066005, 0o066006, 0o000356, 0o004052, 0o000010, 0o066006, 0o066007, 0o000354, 0o004052, 0o000010, 0o066007, 0o066010, 0o000352, 0o004052, 0o000010, 0o066010, 0o066011, 0o000350, 0o004052, 0o000010, 0o066011, 0o066012, 0o000346, 0o004052, 0o000010, 0o066012, 0o066013, 0o000344, 0o004052, 0o000010, 0o066013, 0o066014, 0o000342, 0o004052, 0o000010, 0o066014, 0o066015, 0o000340, 0o004052, 0o000010, 0o066015, 0o066016, 0o000336, 0o004052, 0o000010, 0o066016, 0o066017, 0o000334, 0o004052, 0o000010, 0o066017, 0o066020, 0o000000, 0o004052, 0o000014, 0o066020, 0] recbase = 0o7000//2 # word address in phys mem for i, val in enumerate(expected_7000): with self.subTest(i=i, val=val): self.assertEqual(val, p.physmem[recbase + i]) def test_breakpoints1(self): # test the steps=N breakpoint capability p = self.make_pdp() maxtest = 100 a = InstructionBlock() for i in range(maxtest): a.mov(i, 'r0') a.clr('r0') a.halt() startaddr = 0o4000 self.loadphysmem(p, a, startaddr) for i in range(maxtest): with self.subTest(i=i): p.run_steps(pc=startaddr, steps=i+1) self.assertEqual(p.r[0], i) def test_breakpoints2(self): # test the PCBreakpoint ('run_until') breakpoint capability p = self.make_pdp() maxtest = 100 a = InstructionBlock() for i in range(maxtest): a.mov(i, 'r0') a.label(f"L{i}") a.clr('r0') startaddr = 0o4000 self.loadphysmem(p, a, startaddr) for i in range(maxtest): with self.subTest(i=i): p.run_until(pc=startaddr, stoppc=startaddr+a.getlabel(f"L{i}")) self.assertEqual(p.r[0], i) def test_breakpoints3(self): # test multiple breakpoints p = self.make_pdp() maxtest = 100 a = InstructionBlock() for i in range(maxtest): a.mov(i+1, 'r0') a.clr('r0') a.halt() startaddr = 0o4000 self.loadphysmem(p, a, startaddr) # create one MultiBreakpoint with four different Steps bkpts, # in this order in the MultiBreakpoint: # one at 300 steps (should not fire at all) # one at 75 steps # one at 1 step # one at 50 steps # each of these should fire at its "absolute" step, because they # are being re-used... # create one MultiBreakpoint of PCBreakpoints at each label s300 = BKP.StepsBreakpoint(steps=300) s75 = BKP.StepsBreakpoint(steps=75) s1 = BKP.StepsBreakpoint(steps=1) s50 = BKP.StepsBreakpoint(steps=50) mbp = BKP.MultiBreakpoint(s300, s75, s1, s50) # this test just knows there are four of them, code the easy/dumb way: p.r[p.PC] = startaddr p.run(breakpoint=mbp) # this should have fired after 1 instruction... self.assertEqual(p.r[0], 1) # the next two similar p.run(breakpoint=mbp) self.assertEqual(p.r[0], 50) p.run(breakpoint=mbp) self.assertEqual(p.r[0], 75) # the last one will complete because of the HALT p.run(breakpoint=mbp) self.assertEqual(p.r[0], 0) # this is really an internal detail, but test it as a way # to make sure s1, etc all were continuing to be called self.assertTrue(s1.togo < 0) self.assertTrue(s50.togo < 0) self.assertTrue(s75.togo < 0) def test_bkplog(self): # test the instruction logger "breakpoint" fnamebase = f"pdptestlog-{hex(id(object()))}" fname = fnamebase + ".log" try: os.remove(fname) except FileNotFoundError: pass p = PDP1170(logger=fnamebase, loglevel='DEBUG') # the point of this program is just to create N log # entries (when executed) that can be verified a = InstructionBlock() a.mov('r0', 'r0') a.mov('r0', 'r1') a.mov('r0', 'r2') a.mov('r0', 'r3') a.mov('r0', 'r4') a.mov('r0', 'r5') a.mov('r1', 'r0') a.mov('r1', 'r1') a.mov('r1', 'r2') a.mov('r1', 'r3') a.mov('r1', 'r4') a.mov('r1', 'r5') a.halt() instloc = 0o4000 self.loadphysmem(p, a, instloc) p.run(pc=instloc, breakpoint=BKP.Logger()) # This is a probably-too-fragile attempt to see if each of the above # instructions made it into the logfile, in order. While trying to # accommodate possibility of other logging lines getting in there. # There was, of course, a better way, but this worked... with open(fname, 'r') as logf: raninto_EOF = False for inst in a: try: while f":: {oct(inst)}" not in next(logf): pass except StopIteration: raninto_EOF = True self.assertFalse(raninto_EOF) os.remove(fname) def test_lookbackbp(self): p = self.make_pdp() # dynamically determine (within reason!) # of default lookbacks maxguess = 5000 # if it's more than this... meh curguess = 1 startaddr = 0o4000 while curguess < maxguess: a = InstructionBlock() for i in range(curguess): a.mov(i, 'r0') a.halt() self.loadphysmem(p, a, startaddr) bp = BKP.Lookback() p.run(pc=startaddr, breakpoint=bp) # if current == first, there was 1 lookback, that's 1 # But also the halt instruction takes up on; hence +2 n = (p.r[0] - bp.states[0][1]['R0']) + 2 if n < curguess: default_lookbacks = n break curguess += 1 maxtest = default_lookbacks + 1 a = InstructionBlock() for i in range(maxtest): a.mov(i, 'r0') a.clr('r0') a.halt() self.loadphysmem(p, a, startaddr) for i in range(maxtest): bp = BKP.Lookback(BKP.StepsBreakpoint(steps=i+1)) bp7 = BKP.Lookback(BKP.StepsBreakpoint(steps=i+1), lookbacks=7) with self.subTest(i=i): p.run(pc=startaddr, breakpoint=bp) p.run(pc=startaddr, breakpoint=bp7) self.assertEqual(p.r[0], i) if i+1 <= default_lookbacks: self.assertEqual(len(bp.states), i+1) else: self.assertEqual(len(bp.states), default_lookbacks) self.assertEqual(len(bp7.states), min(i+1, 7)) def test_jmp10(self): """Test of JMP (R0) instruction (mode 0o10)""" p = self.make_pdp() instloc = 0o10000 a = InstructionBlock() a.clr('r2') a.jmp('(r0)') # test driver code will set R0 to ... a.inc('r2') # various a.inc('r2') # ... different a.inc('r2') # ...... locations a.inc('r2') # ........ among these a.halt() self.loadphysmem(p, a, instloc) for offs, r2 in ((4, 4), (6, 3), (8, 2), (10, 1), (12, 0)): p.r[0] = instloc + offs p.run(pc=instloc) with self.subTest(offs=offs): self.assertEqual(p.r[2], r2) def test_jmp67(self): """Test of JMP offs(PC) instruction (mode 0o67)""" # this is really more of a test of InstructionBLock jmp offset # calculations than it is a test of jmp itself p = self.make_pdp() a = InstructionBlock() a.clr('r0') a.clr('r1') a.clr('r2') a.jmp('X2') a.label('X0') a.inc('r0') a.add(2, 'r1') a.add(3, 'r2') a.jmp('X1') # never executed a.clr('r0') a.clr('r1') a.clr('r2') a.label('X1') a.inc('r1') a.add('r1', 'r2') a.add('r2', 'r0') a.halt() a.label('X2') a.inc('r2') a.jmp('X0') a.halt() instloc = 0o4000 self.loadphysmem(p, a, instloc) p.run(pc=instloc) # results by hand-computation but also cross verified in SIMH self.assertEqual(p.r[0], 8) self.assertEqual(p.r[1], 3) self.assertEqual(p.r[2], 7) def test_jsrco(self): """Another special case of the JSR instruction is JSR PC, @(SP) + which exchanges the top element of the processor stack and the contents of the program counter. Use of this instruction allows two routines to swap program control and resume operation when recalled where they left off. Such routines are called 'co-routines.' """ p = self.make_pdp() a = InstructionBlock() # XXX TBD XXX def test_ubmap(self): p = self.make_pdp() ubmaps = self.ioaddr(p, p.ub.UBMAP_OFFS) # code paraphrased from UNIX startup, creates a mapping pattern # that the rest of the code expects (and fiddles upper bits) # So ... test that. for i in range(0, 62, 2): p.mmu.wordRW(ubmaps + (2 * i), i << 12 & 0o1777777) p.mmu.wordRW(ubmaps + (2 * (i + 1)), 0) # XXX there is no real test yet because the UBMAPs # are all just dummied up right now # this is not a unit test, invoke it using timeit etc def speed_test_setup(self, *, loopcount=200, mmu=True, inst=None): """Set up a test run of 50*loopcount inst instructions. Returns tuple: p, pc """ p, pc = self.simplemapped_pdp() # the returned pdp is loaded with instructions for setting up # the mmu; only do them if that's what is wanted # # NOTE: the test code is run in USER mode Because Reasons # (was experimenting with virtual caches and this was helpful). # The test code will run at (virtual/user) 0 when the MMU is # enabled, or its physical location (0o20000) when off. user_physloc = 0o20000 if mmu: p.run(pc=pc) # set up all those mappings usermode_base = 0 # physical 0o20000 maps here in USER mode else: usermode_base = user_physloc # by default the instruction being timed will be MOV R1,R0 # but other instructions could be used. MUST ONLY BE ONE WORD if inst is None: inst = 0o010100 # this is the tiny kernel code used to set up and start # each iteration of the user mode timing code. It slightly # distorts the per-instruction overhead of course. C'est la vie. k = InstructionBlock() k.mov(0o20000, 'sp') # establish proper kernel stack k.mov(0o140340, '-(sp)') # USER mode, no interrupts k.mov(usermode_base, '-(sp)') # pc start for loop/USER code k.rtt() # off to the races! kloc = 0o4000 for a2, w in enumerate(k): p.mmu.wordRW(kloc + (2 * a2), w) # The test timing loop... 49 "inst" instructions # and an SOB for looping (so 50 overall instructions per loop) a = InstructionBlock() a.mov(loopcount, 'r4') a.label('LOOP') for i in range(49): a.literal(inst) a.sob('r4', 'LOOP') a.halt() for a2, w in enumerate(a): p.physRW(user_physloc + (2 * a2), w) return p, kloc def speed_test_run(self, p, instloc): """See speed_test_setup""" p.run(pc=instloc) if __name__ == "__main__": import argparse import timeit movr1r0 = 0o010100 parser = argparse.ArgumentParser() parser.add_argument('-p', '--performance', action="store_true") parser.add_argument('-i', '--instruction', default=movr1r0, type=int) parser.add_argument('--nommu', action="store_true") parser.add_argument('--clr', action="store_true") args = parser.parse_args() if args.performance: # the goal is to execute inst 1M times. The loop executes 49 inst # instructions and 1 sob (which taken together are considered as 50). # Want to drive "number=" up more than loopcount, so use # loopcount=20 ... means "1000" inst instructions # number=1000 ... do that 1000 times, for 1M instructions # simple way to test CLR instruction vs default MOV. # The CLR instruction is not optimized the way MOV is so # this shows the difference. if args.clr: args.instruction = 0o005000 t = TestMethods() mmu = not args.nommu inst = args.instruction p, pc = t.speed_test_setup(loopcount=20, inst=inst, mmu=mmu) ta = timeit.repeat(stmt='t.speed_test_run(p, pc)', number=1000, globals=globals(), repeat=50) tnsec = round(1000 * min(*ta), 1) if args.instruction == movr1r0: instr = 'MOV R1,R0' elif (args.instruction & 0o177770) == 0o005000: instr = f'CLR R{args.instruction & 7}' else: instr = oct(args.instruction) print(f"Instruction {instr} took {tnsec} nsecs") else: unittest.main()