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.

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.

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.