Overview
TODO: Write introduction. Goal is to run baremetal PDP-11 programs in SIMH, a PDP-11 simulator.
TODO: What kind of joint header do I want across all the articles in a set, linking them together?
SIMH Installation
We first need to install SIMH, a program that simulates many computer architectures including the DEC PDP-11.
FreeBSD: Either install package via
pkg install simh
or build from ports underemulators/simh
.Debian: Install as package via
sudo apt install simh
.Windows: Download pre-built snapshot binaries.
Other: Read the SIMH README section “Building Simulators Yourself”.
After installation, launch the PDP-11 simulator with the command pdp11
. It
should display a prompt sim>
where you can type quit
followed by ENTER
to
exit the simulator.
% pdp11
PDP-11 simulator V3.9-0
sim> quit
Goodbye
%
SIMH Basics
Now that SIMH is installed, go ahead and launch it again with the command
pdp11
. This should result in the prompt sim>
. All commands entered at this
sim>
prompt are executed by the SIMH simulator itself, not by the simulated
PDP-11. For example, we could display the configuration of the simulated
PDP-11 defined in SIMH.
sim> show configuration
PDP-11 simulator configuration
CPU, 11/73, NOCIS, idle disabled, autoconfiguration enabled, 256KB SYSTEM
[...]
We can change the simulated PDP-11, for example by changing to a different model.
sim> set cpu 11/40
Disabling XQ
sim> show configuration
PDP-11 simulator configuration
CPU, 11/40, NOFIS, idle disabled, autoconfiguration enabled, 256KB SYSTEM
[...]
SIMH also allows us to deposit values directly into memory and read them back. Note that addresses in SIMH refer to the physical address rather than the virtual address, and thus may be up to 22-bits long on certain models, as shown below.
sim> examine 0140000
140000: 000000
sim> deposit 0140000 042
sim> examine 0140000
140000: 000042
sim>
Once a program is loaded into memory, the simulated PDP-11 may be commanded to
execute it via the command go <address>
. For example, if the program was
loaded starting at address 01000
, then begin execution with go 01000
.
Interrupting Simulation
At any time, the simulation may be paused by pressing Ctrl-e
. This returns to
the sim>
prompt where memory may be examined or other extra-sim capabilities
executed. For example, if we have the PDP-11 CPU execute a tight infinite loop,
the simulation never naturally ends, but we can pause it, allowing us to exit.
sim> go
<Simulation runs until user presses Ctrl-e.>
Simulation stopped, PC: 004166 (BR 4166)
sim> quit
Goodbye
%
Note that, instead of quit
, typing go
would have resumed the simulation
exactly where it paused, shown in the status message where the PC
register is
set to address 04166
.
Saving Configuration
Any command that may be entered at the sim>
prompt, may also be entered in a
configuration file. For example, consider the following file named simh.conf
.
deposit 01000 0777
echo Just set address 01000 to the instruction 'BRANCH 01000'.
go 01000
We can tell SIMH to load this file and execute each line as though it were
typed at the sim>
prompt by including the config file name. Even though the
commands are not displayed, we can see from the echo
command that they were
executed.
% pdp11 simh.conf
PDP-11 simulator V3.9-0
Just set address 01000 to the instruction 'BRANCH 01000'.
<Simulation runs until user presses Ctrl-e.>
Simulation stopped, PC: 001000 (BR 1000)
sim> quit
Goodbye
Because this program is an infinite loop, we pressed Ctrl-e
to pause the
simulation and allow us to quit.
SIMH Loader
Since we’re trying to run bare-metal code on this simulated PDP-11, we don’t
need to bother with disk images; we will load a binary directly into the
PDP-11’s memory using SIMH’s load
command.
Loader Format
The loader included with SIMH doesn’t accept a raw binary file or the a.out executable generated by our cross compiler. Instead, it expects a paper tape image file in the following format.
Loader format consists of blocks, optionally preceded, separated, and
followed by zeroes. Each block consists of the following entries. Note
that all entries are one byte.
0001
0000
Low byte of block length (data byte count + 6 for header, excludes checksum)
High byte of block length
Low byte of load address
High byte of load address
Data byte 0
...
Data byte N
Checksum
The 8-bit checksum for a block is the twos-complement of the lower eight
sum bits for all six header bytes and all data bytes.
If the block length is exactly six bytes (i.e. only header, no data),
then the block marks the end-of-tape. The checksum should be zero. If
the load address of this final block is not 000001, then it is used as
the starting PC.
If you don’t want to generate this format yourself, use the utility
bin2load which converts a
binary image into a SIMH compatible loader format. See the README.md
file for
installation instructions. For example:
$ git clone git://git.subgeniuskitty.com/pdp11-bin2load
$ cd pdp11-bin2load
$ make install
$ export PATH=$HOME/bin:$PATH
$ bin2load -h
bin2load v2 (www.subgeniuskitty.com)
Usage: bin2load -i <file> -o <file> [-a <address>]
-i <file> Raw binary file to be written to tape.
For example, output from 'pdp11-aout-objdump' (see README.md).
-o <file> Output file created by bin2load containing tape image for use with SIMH.
-a <address> (optional) Address on PDP-11 at which to load tape contents.
Load and Execute
We can extract a binary from the a.out file generated by our cross compiler
using the pdp11-aout-objcopy
tool built at the same time as the cross
compiler. This binary can then be converted by bin2load
and loaded into SIMH.
% pdp11-aout-objcopy --only-section=.text --output-target binary program.out program.bin
% bin2load -i program.bin -o program.pdp11 -a 01000
% pdp11
PDP-11 simulator V3.9-0
sim> load program.pdp11
sim> go
If we pass a starting address to bin2load
with the -a
flag (as shown
above), then SIMH configures the simulation to begin execution at the passed
address.
The load <file>
and go
command may also be included in the SIMH
configuration file, automatically loading and executing whenever the simulator
is started.
Execution
Let’s bring all these steps together. Assume we’ve built our cross compiler and
created a Hello, World!
program like the one shown in the four files below.
program.c
:
#include <stdint.h>
#define XCSR (*((volatile uint16_t *)0177564))
#define XBUF (*((volatile uint16_t *)0177566))
void
putch(uint16_t c)
{
while((XCSR && 0200) == 0) continue;
XBUF = c;
}
void
print_string(const char * string)
{
while (*string != '\0') {
putch(*string);
string++;
}
}
void
cstart(void)
{
volatile uint16_t * test_word = (volatile uint16_t *) 0140000;
*test_word = 0123456;
print_string("Hello, World!\r\n");
}
bootstrap.s
:
.globl _start
_start:
mov $01000,sp
jsr pc,_cstart
halt
pdp11.ld
:
OUTPUT_FORMAT("a.out-pdp11")
ENTRY(start)
phys = 00001000;
SECTIONS
{
.text phys : AT(phys) {
code = .;
*(.text)
*(.rodata)
. = ALIGN(0100);
}
.data : AT(phys + (data - code))
{
data = .;
*(.data)
. = ALIGN(0100);
}
.bss : AT(phys + (bss - code))
{
bss = .;
*(.bss)
. = ALIGN(0100);
}
end = .;
}
simh.conf
:
load program.pdp11
go 1000
Note that the program writes the value 0123456
to address 0140000
and then
prints the string Hello, World!
to the console SLU before halting.
We can compile and execute this program with the following sequence of
commands. After the program halts, we are able to examine address 0140000
and
see the value 0123456
written by the program.
% pdp11-aout-as -o bootstrap.o bootstrap.s
% pdp11-aout-gcc -c -Wall -Wno-unused-function -O0 -ffreestanding \
-fomit-frame-pointer -fno-builtin-alloca -std=c99 -o program.o program.c
% pdp11-aout-ld -T pdp11.ld --entry _start bootstrap.o program.o -o program.out
% pdp11-aout-objcopy --only-section=.text --output-target binary program.out program.bin
% bin2load -i program.bin -o program.pdp11 -a 01000
% pdp11 simh.conf
Paper tape will load at address 01000.
PDP-11 simulator V3.9-0
Hello, World!
HALT instruction, PC: 001012 (BR 1016)
sim> examine 0140000
140000: 123456
sim> quit
Goodbye
TADA! Now you can test your programs on the simulator as you write them.
Beyond Basics
SIMH includes many features beyond the basics shown in this document. The
curious user may enter help
at the sim>
prompt for more information.
However, before leaving the topic of testing code on simulated PDP-11s, I want to mention two simulated pieces of hardware provided by SIMH that can be of use in this task.
Line Printer
The DEC LP11 line printer is simulated by SIMH and its output saved to a text
file during the simulation. To save output as line_printer.txt
, execute the
following command in SIMH.
sim> attach lpt line_printer.txt
The DEC LP11 is controlled by two registers, the Line Printer Status register
(LPS
) located at 0177514
and the Line Printer Data Buffer register (LPDB
)
at 0177516
. To use the LP11, simply test bit 7 of LPS
for printer readiness
and then write a 7-bit character into the low bits of LPDB
.
For example, you could use something like the following C code to print from the simulation to the printer text file.
#define LPS (*((volatile uint16_t *)0177514))
#define LPDB (*((volatile uint16_t *)0177516))
void
putch(uint16_t c)
{
/* Test bit 7 (aka: 0200) of LPS for readiness. */
while((LPS && 0200) == 0) continue;
/* Transfer a byte when ready. */
LPDB = c;
}
This provides an easy method for your PDP-11 program to output data to the host which is automatically saved, all while requiring minimal code be added to the PDP-11’s program.
Serial via Telnet
The DEC DC11 asynchronous line interface is used between the PDP-11 and a
serial asynch line. Via SIMH, these lines can be redirected to TCP ports which
are accessible via telnet
. For example, to enable eight separate lines in
SIMH and listen on port 1170
, type the following at the sim>
prompt or add
to your SIMH configuration file.
sim> set dci en
sim> set dci lines=8
sim> set dci 1170
Now you can telnet
to port 1170
and SIMH will redirect your connection to
the first available line of the simulated DC11.
Inside the simulation, interacting with a DC11 is fairly simple.
Four registers are associated with each serial line. The first is at addresses
0177400
-0177406
, the next at 0177410
-0177416
, etc. Within a block of
four words, the registers are assigned as follows.
1774xx0
: Receiver Status (RCSR
)1774xx2
: Receiver Buffer (RBUF
)1774xx4
: Transmitter Status (XCSR
)1774xx6
: Transmitter Buffer (XBUF
)
Your program should test bit 7 of the RCSR
and XCSR
registers to determine
when the serial line has received a byte and is ready for it to be read, or the
serial line is ready to transmit a byte. When ready, transfer a byte to the
lower half of XBUF
or from the lower half of RBUF
.
The code for these operations should look just like the code given above for the LP11 line printer.
For a full description of the programming interface of the DC11, see the PDP-11 Peripherals Handbook starting on page 109.
Using these simulated serial lines, you can easily interact with your PDP-11
program via TCP ports, either manually using a telnet
program, or
programmatically. This can be a powerful debugging tool.