Block I Apollo Guidance Computer (AGC) : How To Build One in Your Basement
Block I Apollo Guidance Computer (AGC) : How To Build One in Your Basement
Block I Apollo Guidance Computer (AGC) : How To Build One in Your Basement
Part 6: Assembler
John Pultorak
December, 2004
Abstract
This report describes my successful project to build a working reproduction of the 1964
prototype for the Block I Apollo Guidance Computer. The AGC is the flight computer for the
Ap ollo m oon lan din gs, and is the wo rld’s first in tegra ted c ircuit com pu ter.
If you like, you can build one too. It will take you less time, and yours will be better than
mine.
Part 3 PROC M odule: Design and construction of the processing (CPU) modu le.
Part 4 MEM Module: Design and construction of the mem ory module.
Part 6 Assem bler: A cross-a ssem bler for AG C softw are dev elopm ent.
Part 7 C+ + S imu lator: A low-level simulator that runs assemb led AGC code.
Part 9 Test & Che ckou t: A suite of test programs in AG C assembly langu age.
Overview
This A GC cross-asse m bler, code d in C ++ , produ ces AG C ob ject code in M otorola S -form at.
The code can be burned into EPROM for use by the hardware AGC, or executed by the C++
AG C sim ula tor.
Operation
The assembler reads an AGC source code text file (having a .asm extension). It generates
an assembly listing text file (.lst extension) and an two object code text files (_H.hex and
_L.hex suffixes). The object code files are readable by the AGC simulator or an EPROM
prog ram m er.
Syntax
Sou rce code files are text files con tainin g m ultiple lines of sou rce code . Each lin e is
terminated by a newline character. Source code files can be produced by any editor, as long
as it doesn't insert any hidden characters or formatting information.
The assembler ignores blank lines and anything that occurs after a semicolon on any given
line.
The comp onents, if present, mu st appear in the order given. Each component is separated
from th e nex t by on e or m ore wh ite spac es or tabs . The on ly cons traint is th at the la bel, if
present, must start in the 1st colum n of the line. If no label is present, the op code or
assembler directive must not start in the 1st column, but may appear in any subsequent
column.
CADR Define a word of storage; set the value to the operand value (assumed
to be a 14-b it address; this is an alias for CAD R).
ADRES Define a word of storage; set the value to the operand value (operand
is treated as a 12-bit ad dress).
Addressing
The location counter and symbol table always contain the full 14-bit address. AGC
instructions require a 12-bit address as an operand. Addressing is implemented in 1K banks
(bits 10 throu gh 1). B its 12 and 11 select banks 0 (era sab le m em ory), 1 (fixed -fixed ), or 2
(fixed-fixed). These banks are directly addressable in 12-bits. Banks above 2 are called
'fixed-switchable' and are addressed using the BANK register: bits 12 and 11 of the operand
are set to '1' and the bank register provides the high-order 4 bits (bits 14 through 11) of the
address. The lower 10 bits of the operand provide the address within the bank.
Errata
a) The assembler ignores the last line of the source (.asm) file. If the last line of your
source file contains code, you must add an additional blank line to the end of your
source file to ensure that your last line of code is assembled.
b) The symbol table could be sorted before the second pass. The linear search through
the sym bol table could be replaced by a mo re efficient m ethod. (Bu t, assembly is so
fast, it doesn’t m atter).
c) The assembler directives and syntax don't match those of the original block I AGC.
e) Gen erates em pty .lst an d .obj files if the so urce cod e file does n ot exist.
f) Incorrectly a ssign s a zero va lue to la bels th at equ ate to a forw ard referen ced lab el.
i.e.:
LABEL1 EQU LABEL2 ; assem bler in correc tly sets LA BEL 1 to z ero
LABEL2 EQU *
Assembler listing
The assembler produces a list file (.lst) that shows the assembled source code. The format
of the file w as des igne d to m im ic (som ewh at) the forma t of the origin al assem bler. You will
find a fragment of the original assembler listing reproduced in part 8.
There ’s lots of docum enta tion on S-R ecord form ats on the inte rnet.
The files are pretty big. If you open one up, you’ll discover they contain many short records
that look like these:
This is a fragment of the source code for the lower 8 bits of the TECO1 test and checkout
program (described in pa rt 9).
The assembler contains C++ code for generating these files; the C++ simu lator has code for
rea din g th em .
Assembler v1.6
/*
****************************************************************
*
* Cross Assembler for Block I Apollo Guidance Computer (AGC4)
* THIS VERSION IS MODIFIED TO OUTPUT IN S-RECORD FORMAT SUITABLE FOR
* LOADING IN EPROM (s2f format).
* Author: John Pultorak
* 6/1/2002
*
*****************************************************************
Versions:
1.0 - First version of the AGC assembler.
1.1 - Added ability to handle simple arithmetic expressions for the operand.
1.2 - Changed label fields to 14 char. Printed symbol table in 3 columns.
Corrected wrong implementation of OVSK.
1.3 - Added support for bank addressing.
1.4 - Added capability to inline code with nested includes.
1.5 - Added CADR, ADRES assembler directives. Swapped addresses for
TIME1 and TIME2 for compatibility with Block II PINBALL routines.
1.6 - Fixed the parser so it doesn't mangle comments anymore. Added
FCADR and ECADR to the assembler directives. Added a check for
assembled code that overwrites already used locations.
1.6 - EPROM Assembler outputs to low and high EPROM files.
Operation:
The assembler reads an AGC source code file (having a .asm extension).
It generates an assembly listing text file (.lst extension) and an
object code text file (.obj extension). The object code file is readable
by the AGC simulator.
Syntax:
Source code files are text files containing multiple lines of source code.
Each line is terminated by a newline character. Source code files can be
produced by any editor, as long as it doesn't insert any hidden characters
or formatting information.
The assembler ignores blank lines and anything that occurs after a semicolon
on any given line.
The components, if present, must appear in the order given. Each component is
separated from the next by one or more white spaces or tabs. The only constraint
is that the label, if present, must start in the 1st column of the line. If no
label is present, the op code or assembler directive must not start in the 1st
column, but may appear in any subsequent column.
Addressing:
The location counter and symbol table always contain the full 14-bit address.
AGC instructions require a 12-bit address as an operand. Addressing is implemented
in 1K banks (bits 10 through 1). Bits 12 and 11 select banks 0 (erasable memory),
1 (fixed-fixed), or 2 (fixed-fixed). These banks are directly addressable in
12-bits. Banks above 2 are called 'fixed-switchable' and are addressed using
the BANK register: bits 12 and 11 of the operand are set to '1' and the bank
register provides the high-order 4 bits (bits 14 through 11) of the address.
The lower 10 bits of the operand provide the address within the bank.
Errata:
The assembler ignores the last line of the source (.asm) file. If the last
line of your source file contains code, you must add an additional blank
line to the end of your source file to ensure that your last line of code
is assembled.
The symbol table should be sorted before the second pass. The linear
search through the symbol table should be replaced by a more efficient
method.
The assembler directives and syntax don't match those of the original block
I AGC.
Generates empty .lst and .obj files if the source code file does not exist.
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <iostream.h>
#include <stdio.h>
//***********************************************************************
// MODIFIED FROM THE ORIGINAL ASSEMBLER HERE.
// This array represents the fixed memory PROM (14-bit address)
int EPROM_H [agcMemSize];
int EPROM_L [agcMemSize];
//***********************************************************************
unsigned pass = 0;
unsigned locCntr = 0; // address in 14-bit format
unsigned errorCount = 0;
struct ocode
{
char* name;
unsigned code;
bool isOpCode; // true=it is an op code
bool addrOpnd; // true=convert operand to 12-bit address format
unsigned len; // words
};
ocode allcodes[] =
{
// Block I op codes.
{ "TC", 0000000, true, true, 1 },
{ "CCS", 0010000, true, true, 1 },
{ "INDEX", 0020000, true, true, 1 },
{ "XCH", 0030000, true, true, 1 },
{ "CS", 0040000, true, true, 1 },
{ "TS", 0050000, true, true, 1 },
{ "AD", 0060000, true, true, 1 },
{ "MASK", 0070000, true, true, 1 },
{ "MP", 0040000, true, true, 1 },
{ "DV", 0050000, true, true, 1 },
{ "SU", 0060000, true, true, 1 },
// Assembler directives
{ "DS", 0, false, false, 1 }, // define storage; reserves 1 word of memory
{ "CADR", 0, false, false, 1 }, // define 14-bit addr; reserves 1 word of memory
{ "FCADR", 0, false, false, 1 }, // define 14-bit addr; reserves 1 word of memory
{ "ECADR", 0, false, false, 1 }, // define 14-bit addr; reserves 1 word of memory
{ "ADRES", 0, false, true, 1 }, // define 12-bit addr; reserves 1 word of memory
{ "ORG", 0, false, false, 0 }, // origin; sets location counter to operand value
{ "EQU", 0, false, false, 0 }, // equate; assigns a value to a label
{ "INCL", 0, false, false, 0 }, // include; inline include source code
{ "", 0, false, false, 99 } // end-of-list flag
};
void parse(char* buf, char* labl, char* opcd, char* opnd, char* cmnt)
{
// strip off newline char.
buf[strlen(buf) - 1] = '\0';
strcpy(labl,"");
strcpy(opcd,"");
strcpy(opnd,"");
strcpy(cmnt,"");
char* sp = buf;
char* s = 0;
}
switch(mode)
{
case _labl: strcat(labl, s); mode = _opcd; break;
case _opcd: strcat(opcd, s); mode = _opnd; break;
case _opnd: strcat(opnd, s); mode = _cmnt; break;
}
}
} while(s);
}
struct symbl
{
char name[20];
unsigned val;
};
symbl symTab[5000];
unsigned nSym = 0;
if(pass == 1)
{
fprintf(fpList,"*** ERROR: %s undefined.\n", opnd);
errorCount++;
}
}
return 0;
}
*ostr = '\0';
return istr;
}
char op[20];
sp = getToken(sp, op);
char vstr[20];
int val = 0;
sp = getToken(sp,vstr);
if(*vstr =='-') // unary minus
{
sp = getToken(sp, vstr);
val = -(_getopnd(vstr));
}
else
val = _getopnd(vstr);
switch(*op)
{
case '+': tot += val; break;
case '-': tot -= val; break;
case '@': tot *= val; break;
case '/': tot /= val; break;
}
return _eval(sp,tot);
}
unsigned retval = 0;
if(strcmp(opnd,"-0") == 0 || strcmp(opnd,"-%0") == 0 || strcmp(opnd,"-$0") == 0)
retval = 077777; // -0
else
{ // return the int value of the operand
int opndVal = eval(opnd);
return 0;
}
if(strcmp(opcd,"ORG") == 0)
{
locCntr = getopnd(opnd);
}
}
unsigned genOddParity(unsigned r)
{
//check the lower 15 bits of 'r' and return the odd parity
unsigned evenParity =
(1&(r>>0)) ^ (1&(r>>1)) ^ (1&(r>>2)) ^ (1&(r>>3)) ^
(1&(r>>4)) ^ (1&(r>>5)) ^ (1&(r>>6)) ^ (1&(r>>7)) ^
(1&(r>>8)) ^ (1&(r>>9)) ^ (1&(r>>10)) ^ (1&(r>>11)) ^
(1&(r>>12)) ^ (1&(r>>13)) ^ (1&(r>>14));
return ~evenParity & 1; // odd parity
}
if(strcmp(opcd,"INCL")==0)
readSourceForPass1(opnd);
if(strlen(labl)>0)
{
if(strcmp(opcd,"EQU")==0)
add(labl, getopnd(opnd));
else
add(labl, locCntr);
}
updateLocCntr(opcd, opnd);
}
fclose(fp);
}
while(fgets(buf,256,fp))
{
parse(buf, labl, opcd, opnd, cmnt);
if(strcmp(opcd,"INCL")==0)
{
// Include directive (INCL).
fprintf(fpList, " %-14s %-8s %-14s %s\n",
labl, opcd, opnd, cmnt);
readSourceForPass2(opnd);
}
else if(strcmp(opcd,"")==0)
{
// Comment.
fprintf(fpList, " %s\n", cmnt);
}
else if(getoplen(opcd) == 0)
{
// Must be ORG or EQU assembler directive.
fprintf(fpList, " %-14s %-8s %-14s %s\n",
labl, opcd, opnd, cmnt);
if(!isDefined(opcd))
{
fprintf(fpList,"*** ERROR: %s undefined.\n", opcd);
errorCount++;
}
}
else
{
// Since we got this far, we know the assembly line contains a
// valid 'opcd' that reserves some storage space. It must be an
// instruction or a DS.
// Generate a string containing the data info for the list file.
char dataString[20];
if(isOpCode(opcd))
sprintf(dataString, "%01o %2o,%04o %1o",
(getopcode(opcd) & 070000) >> 12, operBank, operValue,
genOddParity(data));
else
sprintf(dataString, " %05o %1o", operValue, genOddParity(data));
if(memoryUsed[locCntr])
{
fprintf(fpList,"*** ERROR: %06o address already in use.\n",
locCntr);
errorCount++;
}
memoryUsed[locCntr] = true;
//***********************************************************************
// MODIFIED FROM THE ORIGINAL ASSEMBLER HERE.
// Insert the assembled code into an array, indexed by the address.
// This has the effect of sorting the data by address, so we can walk
// through the addresses and output the code to EPROM later.
unsigned dataLow = 0x00ff & data;
unsigned dataHigh = (0xff00 & data) >> 8;
updateLocCntr(opcd, opnd);
}
fclose(fp);
}
//***********************************************************************
// MODIFIED FROM THE ORIGINAL ASSEMBLER HERE.
// Some parameters that control file format. You can change maxBytes
// without affecting anything else. 'addressBytes' is determined by
// the choosen S-Record format.
const int maxBytes = 20; // set limit on record length
const int addressBytes = 3; // 16-bit address range
const int sumCheckBytes = 1;
}
// terminate record by adding the checksum and a newline.
fprintf(fpObj, "%02X\n", (~sumCheck) & 0xff);
i += dataByteCount;
}
// write an end-of-file record here
i=0; // set address zero for last record
sumCheck = 0x04; // byte count
sumCheck = (sumCheck + ((i & 0xff0000) >> 16)) % 256;
sumCheck = (sumCheck + ((i & 0x00ff00) >> 8)) % 256;
sumCheck = (sumCheck + ((i & 0x0000ff) )) % 256;
fprintf(fpObj, "S804%06X%02X", i, (~sumCheck) & 0xff);
//***********************************************************************
fp = fopen(argv[1], "r");
#endif
char sourcefile[80];
cout << "Enter source file: ";
cin >> sourcefile;
char* p = prefix;
while(*p != '\0') { p++; if(*p == '.') break; }
if(strcmp(p,".asm") != 0)
{
cerr << "*** ERROR: Source file not *.asm" << endl;
exit(-1);
}
*p = '\0';
//***********************************************************************
// MODIFIED FROM THE ORIGINAL ASSEMBLER HERE.
// Open a two text files for the object code. The filenames
// will have a .hex extension
char objfile[80];
// INITIALIZE EPROM
for(int k=0; k< agcMemSize; k++)
{
EPROM_H [k] = 0;
EPROM_L [k] = 0;
}
//***********************************************************************
locCntr = 0;
pass++;
//***********************************************************************
// MODIFIED FROM THE ORIGINAL ASSEMBLER HERE.
// Write the EPROM data to file
writeEPROM(fpObj_H, EPROM_H);
writeEPROM(fpObj_L, EPROM_L);
fclose(fpObj_H);
fclose(fpObj_L);
//***********************************************************************
fprintf(fpList,"\nSymbol table:\n");
unsigned j=0;
for(unsigned i=0; i<nSym; i++)
{
fprintf(fpList,"%-14s %06o ", symTab[i].name, symTab[i].val);
j = (j+1) % 3;
if(j==0) fprintf(fpList,"\n");
}
fclose(fpList);
}