python-pdp1170/boot.py

394 lines
13 KiB
Python

import time
from machine import PDP1170
from kw11 import KW11
from kl11 import KL11
from dc11 import DC11
from rp import RPRM
from rk11 import RK11
import breakpoints
STDMSG = """\
Starting PDP11; this window is NOT THE EMULATED PDP-11 CONSOLE.
*** In another window, telnet/nc to localhost:1170 to connect.
Terminal should be in raw mode. On a mac, this is a good way:
(stty raw; nc localhost 1170; stty sane)
"""
def boot_hp(p, /, *, addr=0o10000, deposit_only=False, switches=0):
"""Deposit, then run, instructions to read first 1KB of drive 0 --> addr.
RETURN VALUE: addr if deposit_only else None
If no 'addr' given, it defaults to something out of the way.
If not deposit_only (default):
* The instructions are loaded to 'addr'
* They are executed.
* Return value is None.
* NOTE: The next start address depends on what those instructions do.
* TYPICALLY, the next start address will be zero.
If deposit_only:
* The instructions are loaded to 'addr'
* 'addr' is returned.
"""
# this is the sort of thing that would be keyed in from
# the console switches (if the machine was not equipped
# with a boot rom option to hold it instead)
#
# It is a minimalist program, with lots of assumptions, to read 1K
# from block zero of drive 0 into location 0. The execution start
# at zero is done elsewhere.
#
# NOTE WELL: THIS ASSUMES THE MACHINE IS IN RESET CONDITION WHICH
# MEANS MANY OF THE DEVICE REGISTERS ARE ASSUMED TO BE ZERO
#
# MOV #176704,R0 -- note how used
# MOV #177000,-(R0) -- word count - read 1K though boot really 512
# MOV #071,-(R0) -- go!
program_insts = (
0o012700, # MOV #0176704,R0
0o176704,
0o012740, # MOV #177000,-(R0)
0o177000,
0o012740, # MOV #071, -(R0)
0o000071,
0o0, # HALT
)
for o, w in enumerate(program_insts):
p.physRW(addr + o + o, w)
p.r[p.PC] = addr
if not deposit_only:
p.run()
return addr if deposit_only else None
def boot_rk(p, /, *, addr=0o10000, deposit_only=False, switches=0):
"""Deposit, then run, instructions to read first 1KB of drive 0 --> addr.
RETURN VALUE: addr if deposit_only else None
If no 'addr' given, it defaults to something out of the way.
If not deposit_only (default):
* The instructions are loaded to 'addr'
* They are executed.
* Return value is None.
* NOTE: The next start address depends on what those instructions do.
* TYPICALLY, the next start address will be zero.
If deposit_only:
* The instructions are loaded to 'addr'
* 'addr' is returned.
"""
# this is the sort of thing that would be keyed in from
# the console switches (if the machine was not equipped
# with a boot rom option to hold it instead)
#
# It is a minimalist program, with lots of assumptions, to read 1K
# from block zero of drive 0 into location 0. The execution start
# at zero is done elsewhere.
#
# NOTE WELL: THIS ASSUMES THE MACHINE IS IN RESET CONDITION WHICH
# MEANS MANY OF THE DEVICE REGISTERS ARE ASSUMED TO BE ZERO
#
# MOV #177406,R0
# MOV #177400,(R0)
# MOV #5,-(R0)
program_insts = (
0o012700, # MOV #177406,R0
0o177406,
0o012710, # MOV #177400,(R0)
0o177400,
0o012740, # MOV #5,-(R0)
0o000005,
0o0 # HALT
)
for o, w in enumerate(program_insts):
p.physRW(addr + o + o, w)
p.r[p.PC] = addr
if not deposit_only:
p.run()
return addr if deposit_only else None
def boot_bin(p, fname, /, *, addr=0, deposit_only=False,
little_endian=True, skipwords=8):
"""Read a binary file 'fname' into location 'addr' and execute it.
RETURN VALUE: addr if deposit_only else None
NOTE: fname is in the host system, not on an emulated drive.
If no 'addr' given, it defaults to ZERO.
If deposit_only=True, the instructions are not executed.
little_endian (default True) dictates the fname byte order.
skipwords (default 8 -- a.out header) will seek that many 16-bit
words into the file before beginning to load.
"""
with open(fname, 'rb') as f:
bb = f.read()
# Two data format cases:
# 1) little_endian (the default)
#
# The file is truly a binary image of pdp11 format data
# and the words (pairs of bytes) in bb are in little endian
# order. They will be assembled accordingly and the names
# "low" and "hi" make sense.
#
# 2) not little_endian
#
# Presumably the file has been byte-swapped already and
# the words (pairs of bytes) in it are in big endian order.
# They will be assembled accordingly, but the names "low"
# and "hi" are backwards.
#
xi = iter(bb)
words = []
for low in xi:
hi = next(xi)
if little_endian:
words.append((hi << 8) | low)
else:
words.append((low << 8) | hi) # see case 2) above
for a, w in enumerate(words[skipwords:]):
p.physmem[a] = w
p.r[p.PC] = addr
if not deposit_only:
p.run()
return addr if deposit_only else None
def _must_read_n(f, n, zeroskip=False):
"""read exactly n (>0) bytes from f; raise exception if can't.
If zeroskip is True (default: False), zero bytes will be discarded.
"""
if n == 0:
raise ValueError("n == 0 is not allowed")
b = bytes()
while zeroskip and (b := f.read(1)) == b'\00':
pass
# b has one byte or none in it, depending on zeroskip
b += f.read(n - len(b))
if len(b) != n:
raise ValueError(f"needed {n} bytes; got {len(b)}")
return b
def _byte_phys_write(p, b, addr):
"""write b (bytes()) to addr. b may be odd length. addr may be odd."""
# addr can be odd, length can be odd. Pick up the bookend bytes
# as needed so physRW_N can do it as words.
if addr & 1:
addr -= 1
b = bytes([p.physRW(addr) & 255]) + b
if len(b) & 1:
b += bytes([(p.physRW(addr+len(b)-1) >> 8) & 255])
words = [(b[i+1] << 8) | b[i] for i in range(0, len(b), 2)]
p.physRW_N(addr, len(words), words)
def load_lda_f(p, f):
"""Read and load open file f as an 'absolute loader' file
(.LDA, sometimes .BIC file). This is the same format that simh
defines for its load (binary) command.
Returns: the address specified in the END block (per LDA docs)
Any file format errors or I/O errors will raise an exception.
"""
# The file is (should be) a sequence of individual blocks (see
# comments in get_lda_block()). The last block must have no data.
while True:
addr, b = get_lda_block(f)
if len(b) == 0:
return addr
_byte_phys_write(p, b, addr)
def get_lda_block(f):
"""Read next block from LDA file. Return tuple: addr, b
If the block was an END block, len(b) will be zero.
"""
# Archived DEC documentation says an LDA/absolute loader file may start
# with an arbitrary number of zero bytes. SIMH says that ANY block
# (not just the first) may have arbitrary zeros in front of it, and
# testing reveals that SIMH indeed allows (ignores) such zeros.
#
# Such zeros were probably more common in the (real) paper tape days.
# They are supported here simply because SIMH does too.
#
# The file is a sequence of blocks in header/data/checksum format:
# [HEADER] -- 6 bytes
# [DATA] -- size determined by header, can be zero length
# [CHECKSUM] -- 1 byte
#
# The header is six individual bytes, as follows:
# 1 -- literally, a byte with value 1
# 0 -- "MUST" be zero (this code ignores this byte)
# len-lsb -- lower 8 bits of block length
# len-msb -- upper 8 bits of block length
# addr-lsb -- lower 8 bits of address
# addr-msb -- upper 8 bits of address
#
# As mentioned, runs of zeros in prior to such a header (which starts
# with a 1) are ignored.
#
# The 'block length' includes the header bytes but not the checksum.
# Thus the lengthof [DATA] is six less than the block length given.
# Note that the [DATA] length is allowed to be odd, or zero.
header = _must_read_n(f, 6, zeroskip=True)
if header[0] != 1:
raise ValueError(f"header starts with {header[0]} not 1")
count = (header[3] << 8) | header[2]
addr = (header[5] << 8) | header[4]
if count < 6:
raise ValueError(f"header error, {count=}")
elif count == 6:
# an "END" block, which has no checksum.
return addr, bytes()
# count > 6
b = _must_read_n(f, count-6)
chksum = _must_read_n(f, 1)
if (sum(header) + sum(b) + sum(chksum)) & 0xFF:
raise ValueError(f"checksum mismatch, {header=}")
return addr, b
def boot_lda(p, fname, /, *, force_run=True, msg=None):
"""Load and boot an LDA/BIC/absolute-loader file.
By default, the loaded code is started even if the start address
given in the LDA file is odd (which is normally a flag to not start it).
The start address is rounded down 1 to even in such cases.
To override that behavior, specify force_run=False. The file will only
be run if the start address is even.
In all cases the raw start address is returned; however, if the loaded
code is successfully started there will never be a return unless that
code eventually halts.
"""
with open(fname, 'rb') as f:
addr = rawaddr = load_lda_f(p, f)
if rawaddr & 1:
if not force_run:
return rawaddr
addr = rawaddr - 1
if msg:
print(msg.format(STDMSG))
p.run(pc=addr)
return rawaddr
def make_unix_machine(*, loglevel='INFO', drivenames=[], rk=False):
p = PDP1170(loglevel=loglevel)
p.associate_device(KW11(p.ub), 'KW') # line clock
p.associate_device(KL11(p.ub), 'KL') # console
if rk:
p.associate_device(RK11(p.ub, *drivenames), 'RK') # disk drive
else:
p.associate_device(RPRM(p.ub, *drivenames), 'RP') # disk drive
p.associate_device(DC11(p.ub), 'DC') # additional serial poirts
return p
def boot_unix(p, /, *, runoptions={}, diskboot=boot_hp, msg="{}\n"):
# load, and execute, the key-in bootstrap
diskboot(p)
if msg:
print(msg.format(STDMSG))
p.run(pc=0, **runoptions)
# USE:
# python3 boot.py
#
# to start up unix (or whatever system is on the drive)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true')
parser.add_argument('--drive', action='append', default=[], dest='drives')
parser.add_argument('--rk', action='store_true')
parser.add_argument('--instlog', action='store_true')
parser.add_argument('--lda', action='store', default=None)
parser.add_argument('--bootmsg', type=str)
args = parser.parse_args()
pdpoptions = {'drivenames': args.drives}
runoptions = {}
bkpts = []
if args.debug:
pdpoptions['loglevel'] = 'DEBUG'
bkpts.append(breakpoints._MemChecker(1000000))
if args.instlog:
bkpts.append(breakpoints.Logger())
if bkpts:
# if there is just one, use it directly, else combine 'em
if len(bkpts) == 1:
runoptions['breakpoint'] = bkpts[0]
else:
runoptions['breakpoint'] = breakpoints.MultiBreakpoint(*bkpts)
p = make_unix_machine(**pdpoptions, rk=args.rk)
unixboot_options = {}
if args.bootmsg:
unixboot_options['msg'] = args.bootmsg
# the default boot messages are a bit hokey, in that they sort of
# know that booting an rk is the older bootstrap and booting hp is
# the newer unix bootstrap, but so be it. Specify --bootmsg to override.
if args.rk:
unixboot_options['diskboot'] = boot_rk
if not args.bootmsg:
unixboot_options['msg'] = "{}\n" + \
"At '@' prompt in that OTHER window, " + \
"(typically) type: unix\n" + \
"********* EVERYTHING TYPED HERE IS IGNORED *********"
else:
if not args.bootmsg:
unixboot_options['msg'] = "{}\n" + \
"There will be no prompt; type 'boot' in OTHER window\n" + \
"Then, at the ':' prompt, typically type: hp(0,0)unix\n" + \
"********* EVERYTHING TYPED HERE IS IGNORED *********"
if args.lda:
boot_lda(p, args.lda)
else:
boot_unix(p, runoptions=runoptions, **unixboot_options)