C Programming on a bare metal PDP-11
Have you ever wanted to try programming a bare metal machine without an operating system? This post will show you how to get started programming in C on a real PDP-11 or a PDP-11 simulator such as simh. If you want the whole front panel blinking lights experience (and you really do!) but don’t have a real PDP-11, I recommend getting Oscar Vermeulen’s excellent PiDP-11 kit which is a 60% scaled down PDP-11/70 replica with a Raspberry Pi running simh.
I have both the PiDP-11 kit and a real PDP 11/05 with 4KW (8KB) of core memory, a 20ma current loop console port connected to a DECwriter II and a DL11W serial card that provides a second serial port with RS232 connectivity. No disk or tape. I configured the DL11W card for the address and vector normally used by the high speed paper tape reader/punch because the register layout is close enough for the bootstrap programs to read in programs. A laptop running a terminal program that can send binary files pretends to be the high speed reader and allows me to load programs from “paper tape”.
Building/Installing the Compiler
To get started with our bare metal C programming adventure, we’ll need a cross compiler that can build PDP-11 binaries (we will use GCC and binutils) and a machine to run this compiler on. If you’re using a Raspberry Pi with or without the PiDP-11, you’ve already got a machine to the compiling on. You can either compile GCC and binutils yourself or save yourself a couple of hours and download this precompiled package.
GCC-PDP11-RaspberryPi.tgz
To install the downloaded package on the Raspberry Pi, log in as ‘pi’ and execute the following command in the home directory.
tar zxf GCC-PDP11-RaspberryPi.tgz
It will create a ‘bin’ and ‘xgcc’ directory in your home directory with the GCC compiler, assembler, linker and a tool for converting a.out format files to ones suitable for use with the absolute loader.
If you are not using a Raspberry Pi or want to compile your own tools, follow these instructions after installing the development tools for your platform. Additionally, you may need to install the texinfo
package if it isn’t already installed. These instructions are based on those found at https://xw.is/wiki/Bare_metal_PDP-11_GCC_9.3.0_cross_compiler_instructions
cd $HOME
mkdir -p src obj/binutils-build obj/gcc-build
# Fetch sources
curl https://ftp.gnu.org/gnu/binutils/binutils-2.34.tar.gz | tar -C $HOME/src -zxf -
curl https://ftp.gnu.org/gnu/gcc/gcc-9.3.0/gcc-9.3.0.tar.gz | tar -C $HOME/src -zxf -
# Download prereqisites
cd $HOME/src/gcc-9.3.0
./contrib/download_prerequisites
# Build binutils
cd $HOME/obj/binutils-build
../../src/binutils-2.34/configure --prefix $HOME/xgcc \
--bindir $HOME/bin \
--target pdp11-aout
make && make install
# Build gcc
cd $HOME/obj/gcc-build
../../src/gcc-9.3.0/configure --prefix $HOME/xgcc \
--bindir $HOME/bin \
--target pdp11-aout \
--enable-languages=c \
--with-gnu-as --with-gnu-ld \
--without-headers --disable-libssp
make && make install
If you build your own package, you’ll need to install the aout2lda converter program into your $HOME/bin directory. You can download it below (it is already included in the Raspberry Pi package above). You will need Python 3 installed on your machine to run it.
Our First Program
Now that we have the compiler and tools installed, we can start writing our first C program. Lets start with the traditional “Hello World!” program with a small twist to print it 10 times.
#include "console.h"
int main()
{
int i;
for (i=0; i<10; i++) {
cons_puts("Hello World!\r\n");
}
}
Because we don’t have an operating system, we need to write all our I/O routines ourselves, and the first routine we’ll need is a way to print and read characters on the console. The include file console.h
has declarations for these functions:
#ifndef CONSOLE_H
#define CONSOLE_H
void cons_putc(char c);
char cons_getc();
void cons_gets(char *buffer, int size);
void cons_puts(char *s);
#endif
and console.c
includes the implementation.
#ifndef CONSOLE_H
#include "console.h"
#endif
#define DL11_RCSR 0177560
#define DL11_RCSR_DONE 0x80
#define DL11_RBUF 0177562
#define DL11_XCSR 0177564
#define DL11_XCSR_READY 0x80
#define DL11_XBUF 0177566
void cons_putc(char c)
{
volatile unsigned int *xcsr = (unsigned int *)DL11_XCSR;
unsigned char *xbuf = (unsigned char *)DL11_XBUF;
while (!(*xcsr & DL11_XCSR_READY)) ;
*xbuf = c;
}
char cons_getc()
{
volatile unsigned int *rcsr = (unsigned int *)DL11_RCSR;
unsigned char *rbuf = (unsigned char *)DL11_RBUF;
while (! (*rcsr & DL11_RCSR_DONE)) ;
return *rbuf & 0x7F;
}
void cons_gets(char *buffer, int size)
{
char c, *p = buffer;
while (1) {
c = cons_getc();
if ((c == '\b') || (c == 0x7F)) {
if (p > buffer) {
cons_putc('#');
p--;
} else {
cons_putc(7); // Ring Bell
}
} else if (c >= ' ') {
if (p < buffer + size - 2) {
*(p++) = c;
cons_putc(c);
}
} else if (c == '\r') {
cons_putc(c);
cons_putc('\n');
return;
}
*p = 0;
}
}
void cons_puts(char *s)
{
for (;*s;s++) cons_putc(*s);
}
We need a small amount of assembly code to initialize the stack and set things up so that we can begin executing our main()
function. These are in the crt0.s
file:
.text
.even
.globl _main
.globl ___main
.globl _start
#############################################################################*
##### _start: initialize stack pointer,
##### clear vector memory area,
##### save program entry in vector 0
##### call C main() function
#############################################################################*
_start:
mov $00776,sp
clr r0
L_0:
clr (r0)+
cmp r0, $400
bne L_0
mov $000137,*$0 # Store JMP _start in vector 0
mov $_start,*$2
jsr pc,_main
halt
br _start
#############################################################################*
##### ___main: called by C main() function. Currently does nothing
#############################################################################*
___main:
rts pc
On the PDP-11 memory locations 0 to 377 octal are used for vectors. Locations 400-777 octal are for floating vectors for communication and some other devices. Since memory is very limited on my real PDP-11 and it doesn’t contain such devices I’ve used locations 400-776 for the stack and load the executable binary at location 1000 octal. The crt0.s
file initializes address 0 with a JMP _start instruction so that you can restart the program to starting execution at location 0.
Compiling Hello World!
To compile these files we can execute the following command:
pdp11-aout-gcc -nostdlib -Ttext 0x200 -m10 -Os \
-N -e _start crt0.s console.c hello.c -o hello
(if you get an error that it can’t find pdp11-aout-gcc
, make sure that $HOME/bin
is on your PATH)
The -nostdlib
option tells the compiler not to try linking with any system libraries. -Ttext 0x200
tells the linker to put the text or code section at hexadecimal address 0x200 (1000 octal). -m10
tells the compiler to limit the assembly instructions it generates to those a PDP 11/10 (or 11/05) understands. This means no multiply, divide or even XOR instructions. -Os
means optimize for space. -N
tells the linker put the data section immediately after the code section and not on its own page. In binutils 2.34, the page size was changed to 8192 bytes, which would mean that the data section would end up at a memory address greater than the total amount of memory in my PDP 11/05 (only 8K bytes). -e _start
tells the linker to start execution at the _start
function in crt0.s
. This may or may not be at location 1000 octal depending on the order you link your object files. The compiled binary is placed in the a.out format file called hello
.
The next step is to convert this a.out format binary to something we can feed the absolute loader. The absolute loader format is comprised of a series of records, each with a header containing the bytes: 1, 0, LSB length, MSB length, LSB address, MSB address, the data itself, and a checksum. The length is 6 + the data length, not including the checksum. If a record with a length of 6 is found, the loader will jump to the address include header to begin executing the program.
The aout2lda program reads the a.out format header to find the sizes and locations of the text, data and bus sections and the program entry point. To convert the hello a.out format file to a form suitable for the absolute loader, we can use the command:
aout2lda --aout hello --lda hello.ptap --data-align 2 \
--text 0x200 --vector0
The options are: --aout hello
means use the a.out format file called hello
. --lda hello.ptap
means create the file hello.ptap
(paper tape or absolute loader format). --data-align 2
means align the data section on a 2 byte boundry (i.e. put it directly after the last text or code address). If you didn’t compile/link with the -N
option before, then you would use --data-align 8192
. --text 0x200
means begin the text or code section at hexadecimal address 0x200 (1000 octal). --vector0
adds a record to the absolute loader output that stores a JMP entry at address 0 so we can begin execution there.
Executing Hello World!
Now comes the exciting part – running our first bare metal program. If you have a real PDP 11, you would toggle in the bootstrap, execute it to load the absolute loader DEC-11-L2PC-PO.ptap
and then use that to load hello.ptap
. If all goes well, your console will start printing 10 lines of “Hello World!”.
If you are using simh, you can use the following boot.ini
file:
ECHO Preparing to boot and run Hello World
; Set CPU parameters - PDP-11/70 with 8kW (16kB) of core memory
SET CPU 11/70
SET CPU 16K
SET NOTHROTTLE
SET NOIDLE
; Disable devices that we don't need
SET HK DISABLE
SET RHA DISABLE
SET DZ DISABLE
SET RL DISABLE
SET RX DISABLE
SET RP DISABLE
SET RQ DISABLE
SET TM DISABLE
SET TQ DISABLE
SET RK DISABLE
; COMMENT REALCONS LINES OUT IF NOT USING PIDP-11
set realcons host=localhost
set realcons panel=11/70
set realcons interval=20
set realcons connected
load hello.ptap
go 0
Execute the boot.ini
file by running pdp11 boot.ini
If you are using a PiDP-11, you’ll need to do a new more steps:
mkdir -p /opt/pidp11/systems/hello
cp hello.ptap boot.ini /opt/pidp11/systems/hello
Add a line 0010 hello
to the end of /opt/pidp11/systems/selections
using vi, nano or whatever your favourite editor is. You can then set the PiDP-11 console switches to 0010 octal and press the address knob to reboot. Once the PiDP-11 reboots it should print “Hello World!” 10 times on the console.
A Banner Printing Program
Printing Hello World is exciting, but it would be even more exciting if we would print something bigger and more interesting. I challenged myself to write a program that prints banners while fitting into the 8K of memory my PDP 11/05 has. It has the following features:
- compile time choice of TrueType font for the banner characters
- maximum of 80 character long messages
- centers text along the length of pages
- compile time selectable 80 or 132 column wide banners. (132 columns requires more than 8K of memory for the font data but a double up flag
--double
in mkfont can fit in 8K although with lower quality)
The source and executables are included in the package below. If you are on a PiDP-11, untar the file in /opt/pidp11/systems
to create a banner directory. You’ll need to edit /opt/pidp11/systems/selections
to add a line in for banner
with whatever code you want to assign it.
To run it on a real PDP-11, toggle in the bootstrap, load the absolute loader and then load banner11.ptap
. It comes pre-built with a serif font, but you can edit the Makefile
and replace the BANNER_FONT
line with whatever font you like. Rebuild with make
.
PDP11-bare-metal-banner.tgz
A Few Notes
The PDP 11/05 doesn’t have hardware multiple, divide or XOR instructions, so these need to be performed in software. The existing GCC distribution does not include these functions, so be aware that you will get linker errors because the compiler will try to call __divhi3
and related functions which aren’t implemented at this point.