https://github.com/virtualagc/virtualagc
Tip revision: 7d6cec3857985b92ed11312dcdeac0a49edb0ab8 authored by Mike Stewart on 13 March 2018, 04:58:16 UTC
yaAGC: Integrated the LR and RR into the new PulseInput/PulseOutput interface
yaAGC: Integrated the LR and RR into the new PulseInput/PulseOutput interface
Tip revision: 7d6cec3
AGCmain.cpp
/****************************************************************************
* AGC4 (Apollo Guidance Computer) BLOCK I Simulator
*
* AUTHOR: John Pultorak
* DATE: 07/29/02
* FILE: AGCmain.cpp
*
* VERSIONS:
* 1.0 - initial version.
* 1.1 - fixed minor bugs; passed automated test and checkout programs:
* teco1.asm, teco2.asm, and teco3.asm to test basic instructions,
* extended instructions, and editing registers.
* 1.2 - decomposed architecture into subsystems; fixed minor bug in DSKY
* keyboard logic (not tested in current teco*.asm suite).
* Implemented scaler pulses F17, F13, F10. Tied scaler output to
* involuntary counters and interrupts. Implemented counter overflow
* logic and tied it to interrupts and other counters. Added simple
* set/clear breakpoint. Fixed a bug in bank addressing.
* 1.3 - fixed bugs in the DSKY. Added 14-bit effective address (CADR) to the
* simulator display output. Inhibited interrupts when the operator
* single-steps the AGC.
* 1.4 - performance enhancements. Recoded the control pulse execution code
* for better simulator performance. Also changed the main loop so it
* polls the keyboard and system clock less often for better performance.
* 1.5 - reversed the addresses of TIME1 and TIME2 so TIME2 occurs first.
* This is the way its done in Block II so that a common routine (READLO)
* can be used to read the double word for AGC time.
* 1.6 - added indicators for 'CHECK FAIL' and 'KEY RELS'. Mapped them to OUT1,
* bits 5 and 7. Added a function to display the current location in
* the source code list file using the current CADR.
* 1.7 - increased length of 'examine' function display. Any changes in DSKY now
* force the simulator to update the display immediately. Added a 'watch'
* function that looks for changes in a memory location and halts the
* AGC. Added the 'UPTL', 'COMP', and "PROG ALM" lights to the DSKY.
* 1.8 - started reorganizing the simulator in preparation for H/W logic design.
* Eliminated slow (1Hz) clock capability. Removed BUS REQUEST feature.
* Eliminated SWRST switch.
* 1.9 - eliminated the inclusive 'OR' of the output for all registers onto the
* R/W bus. The real AGC OR'ed all register output onto the bus; normally
* only one register was enabled at a time, but for some functions several
* were simultaneously enabled to take advantage of the 'OR' function (i.e.:
* for the MASK instruction). The updated logic will use tristate outputs
* to the bus except for the few places where the 'OR' function is actually
* needed. Moved the parity bit out of the G register into a 1-bit G15
* register. This was done for convenience because the parity bit in G
* is set independently from the rest of the register.
* 1.10 - moved the G15 parity register from MBF to the PAR subsystem. Merged SBFWG
* and SBEWG pulses into a single SBWG pulse. Deleted the CLG pulse for MBF
* (not needed). Separated the ALU read pulses from all others so they can
* be executed last to implement the ALU inclusive OR functions. Implemented
* separate read and write busses, linked through the ALU. Implemented test
* parity (TP) signal in PAR; added parity alarm (PALM) FF to latch PARITY
* ALARM indicator in PAR.
* 1.11 - consolidated address testing signals and moved them to ADR. Moved memory
* read/write functions from MBF to MEM. Merged EMM and FMM subsystems into
* MEM. Fixed a bad logic bug in writeMemory() that was causing the load of
* the fixed memory to overwrite array boundaries and clobber the CPM table.
* Added a memory bus (MEM_DATA_BUS, MEM_PARITY_BUS).
* 1.12 - reduced the number of involuntary counters (CTR) from 20 to 8. Eliminated
* the SHINC subsequence. Changed the (CTR) sequence and priority registers into
* a single synchronization register clocked by WPCTR. Eliminated the fifth
* interrupt (UPRUPT; INT). Eliminated (OUT) the signal to read from output
* register 0 (the DSKY register), since it was not used and did not provide
* any useful function, anyway. Deleted register OUT0 (OUT) which shadowed
* the addressed DSKY register and did not provide any useful function.
* Eliminated the unused logic that sets the parity bit in OUT2 for downlink
* telemetry.
* 1.13 - reorganized the CPM control pulses into CPM-A, CPM-B, and CPM-C groups.
* Added the SDV1, SMP1, and SRSM3 control pulses to CPM-A to indicate when
* those subsequences are active; these signals are input to CPM-C. Moved the
* ISD function into CPM-A. Fixed a minor bug causing subsequence RSM3 to be
* displayed as RSM0. Added GENRST to clear most registers during STBY.
* 1.14 - Moved CLISQ to TP1 to fix a problem in the hardware AGC. CLISQ was clearing
* SNI on CLK2 at TP12, but the TPG was advancing on CLK1 which occurs after
* CLK2, so the TPG state machine was not seeing SNI and was not moving to
* the correct state. In this software simulation, everything advances on
* the same pulse, so it wasn't a problem to clear SNI on TP12. Added a
* switch to enable/disable the scaler.
* 1.15 - Reenabled interrupts during stepping (by removing MON::RUN) signals from
* CPM-A and CPM-C logic). Interrupts can be prevented by disabling the scaler.
* Fixed a problem with INHINT1; it is supposed to prevent an interrupt
* between instructions if there's an overflow. It was supposed to be cleared
* on TP12 after SNI (after a new instruction), but was being cleared on TP12
* after every subsequence.
* 1.16 - Changed CPM-A to load and use EPROM tables for the control pulse matrix. The
* EPROM tables are negative logic (0=asserted), but this simulator expects
* positive logic, so each word is bit-flipped when the EPROM tables load
* during simulator initialization.
* SOURCES:
* Mostly based on information from "Logical Description for the Apollo Guidance
* Computer (AGC4)", Albert Hopkins, Ramon Alonso, and Hugh Blair-Smith, R-393,
* MIT Instrumentation Laboratory, 1963.
*
* PORTABILITY:
* Compiled with Microsoft Visual C++ 6.0 standard edition. Should be fairly
* portable, except for some Microsoft-specific I/O and timer calls in this file.
*
* NOTE: set tabs to 4 spaces to keep columns formatted correctly.
*
*****************************************************************************
*/
/*
* Additional mods:
* 2016-09-02 RSB Lots of adaptations related to using g++, fixing some of
* the features, and so on.
* 2016-09-04 RSB On some computers (mine at home but not some others
* I've tried, there was a lockup after the first command
* was read. Fixed it in nbfgets.cpp.
* Also fixed that fact that single-stepping ('t') was stopping
* at the beginning of the 2nd MCT of a CCS, rather than after
* the CCS was finished. Added a primitive logging function
* to trace the AGC program's execution, so as to compare on
* a cycle-by-cycle basis against yaAGCb1.
* 2017-08-24 RSB Got rid of some clang warnings.
*/
// NCURSES vs PTHREADS. As John originally designed this program, it
// depended on the Windows functionality conio.h, which of course *only*
// existed in Windows. He needed this basically to be able to asynchronously
// detect a keystroke. I at first changed the implementation to use the
// non-blocking capabilities from NCURSES. However, the attendant difficulties
// (such as no backspacing over typos and the inability to run the program
// in Eclipse) were just too great. Instead, I pulled in the simple implementation
// (nbfgets) that I originally used in the original yaAGC, which involves a
// pthread that reads the keyboard in a blocking fashion, but still appears
// non-blocking to the main thread. This has the inconvenience of having to
// use the Enter key to terminate output, but I think that's a small price to
// pay. Of course, none of that stuff would persist into a mature design, since
// it's all related to a console-based interface for debugging and controlling
// the DSKY that's only appropriate to a very primitive program.
#ifdef USE_NCURSES
#include <ncurses.h>
#define endl "\n\r"
#define _getch() wgetch(stdscr)
static int
_kbhit()
{
int c;
c = wgetch(stdscr);
if (c != EOF)
{
ungetch(c);
return (1);
}
return (0);
}
#else
// Use nbfgets.c.
#define printw printf
#define endwin()
char *
nbfgets(char *Buffer, int Length);
char userInput[256];
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <ctype.h>
#include <stdint.h>
#include "reg.h"
#include "TPG.h"
#include "MON.h"
#include "SCL.h"
#include "SEQ.h"
#include "INP.h"
#include "OUT.h"
#include "BUS.h"
#include "DSP.h"
#include "ADR.h"
#include "PAR.h"
#include "MBF.h"
#include "MEM.h"
#include "CTR.h"
#include "INT.h"
#include "KBD.h"
#include "CRG.h"
#include "ALU.h"
#include "CPM.h"
#include "ISD.h"
#include "CLK.h"
extern int
loadPads(char *filename);
#define ROPE_SIZE (02000 * (NUMFBANK + 1))
FILE *logFile = NULL;
#define MAX_LOG_EXTRAS 10
int numLogExtras = 0;
uint16_t logExtras[MAX_LOG_EXTRAS];
bool zeroErasable = true;
using namespace std;
extern bool dskyChanged;
//-----------------------------------------------------------------------
// CONTROL LOGIC
void
genAGCStates()
{
// 1) Decode the current instruction subsequence (glbl_subseq).
// SEQ::glbl_subseq = CPM::instructionSubsequenceDecoder();
// 2) Build a list of control pulses for this state.
CPM::controlPulseMatrix();
// 3) Execute the control pulses for this state. In the real AGC, these occur
// simultaneously. Since we can't achieve that here, we break it down into the
// following steps:
// Most operations involve data transfers--usually reading data from
// a register onto a bus and then writing that data into another register. To
// approximate this, we first iterate through all registers to perform
// the 'read' operation--this transfers data from register to bus.
// Then we again iterate through the registers to do 'write' operations,
// which move data from the bus back into the register.
BUS::glbl_READ_BUS = 0; // clear bus; necessary because words are logical
// OR'ed onto the bus.
MEM::MEM_DATA_BUS = 0; // clear data lines: memory bits 15-1
MEM::MEM_PARITY_BUS = 0; // parity line: memory bit 16
// Now start executing the pulses:
// First, read register outputs onto the bus or anywhere else.
int i;
for (i = 0; i < MAXPULSES && SEQ::glbl_cp[i] != NO_PULSE; i++)
{
CLK::doexecR(SEQ::glbl_cp[i]);
}
// Next, execute ALU read pulses. See comments in ALU .C file
ALU::glbl_BUS = 0;
for (i = 0; i < MAXPULSES && SEQ::glbl_cp[i] != NO_PULSE; i++)
{
CLK::doexecR_ALU(SEQ::glbl_cp[i]);
}
BUS::glbl_WRITE_BUS = BUS::glbl_READ_BUS; // in case nothing is logically OR'ed below;
for (i = 0; i < MAXPULSES && SEQ::glbl_cp[i] != NO_PULSE; i++)
{
CLK::doexecR_ALU_OR(SEQ::glbl_cp[i]);
}
// Now, write the bus and any other signals into the register inputs.
for (i = 0; i < MAXPULSES && SEQ::glbl_cp[i] != NO_PULSE; i++)
{
CLK::doexecW(SEQ::glbl_cp[i]);
}
// Always execute these pulses.
SCL::doexecWP_SCL();
SCL::doexecWP_F17();
SCL::doexecWP_F13();
SCL::doexecWP_F10();
TPG::doexecWP_TPG();
}
//-----------------------------------------------------------------------
// SIMULATION LOGIC
// contains prefix for source filename; i.e.: the portion
// of the filename before .obj or .lst
char filename[80];
char*
getCommand(const char* prompt)
{
#ifdef USE_NCURSES
static char s[80];
char* sp = s;
printw("%s", prompt);
char key;
while ((key = _getch()) != '\n')
{
if (isprint(key))
{
printw("%c", key);
*sp = key;
sp++;
}
else if (key == 8 && sp != s)
{
printw("%c %0X", key, key);
sp--;
}
}
*sp = '\0';
printw("%s", "\n");
return s;
#else
return (&userInput[1]);
#endif
}
bool breakpointEnab = false;
int breakpoint = 0;
void
toggleBreakpoint()
{
if (!breakpointEnab)
{
char b[80];
unsigned bank, offset, flat;
int mct;
strcpy(b,
getCommand(
"Set breakpoint: -- enter banked address or 14-bit CADR (octal) or cMCT: "));
printw("%s\n", b);
if (1 == sscanf(b, "c%d", &mct) && mct > 0)
{
breakpoint = -mct;
breakpointEnab = true;
}
else if (2 == sscanf(b, "%o,%o", &bank, &offset))
{
breakpoint = bank * 02000 + offset % 02000;
breakpointEnab = true;
}
else if (1 == sscanf(b, "%o", &flat))
{
breakpoint = flat;
breakpointEnab = true;
}
else
{
printw("Unrecognized address, breakpoints cleared.\n");
breakpointEnab = false;
}
if (breakpoint >= ROPE_SIZE)
{
printw("Address is out of range, breakpoints cleared.\n");
breakpointEnab = false;
}
}
else
{
printw("%s\n", "Clearing breakpoint.");
breakpointEnab = false;
}
}
bool watchEnab = false;
unsigned watchAddr = 0;
unsigned oldWatchValue = 0;
void
toggleWatch()
{
if (!watchEnab)
{
char b[80];
strcpy(b, getCommand("Set watch: -- enter 14-bit CADR (octal): "));
printw("%s", "\n");
watchAddr = strtol(b, 0, 8);
watchEnab = true;
oldWatchValue = MEM::readMemory(watchAddr);
printw("%06o: %06o\n", watchAddr, oldWatchValue);
}
else
{
printw("%s\n", "Clearing watch.");
watchEnab = false;
}
}
void
incrCntr()
{
char cntrname[80];
strcpy(cntrname, getCommand("Increment counter: -- enter pcell (0-19): "));
printw("%s", "\n");
int pc = atoi(cntrname);
CTR::pcUp[pc] = 1;
}
void
decrCntr()
{
char cntrname[80];
strcpy(cntrname, getCommand("Decrement counter: -- enter pcell (0-19): "));
printw("%s", "\n");
int pc = atoi(cntrname);
CTR::pcDn[pc] = 1;
}
void
interrupt()
{
char iname[80];
strcpy(iname, getCommand("Interrupt: -- enter priority (1-5): "));
printw("%s", "\n");
int i = atoi(iname) - 1;
INT::rupt[i] = 1;
}
// Load AGC memory from a .bin file created by yaYUL or oct2bin.
int
loadYul(char *filename)
{
// Add the .bin extension.
char fname[1024];
uint8_t word[2];
strcpy(fname, filename);
strcat(fname, ".bin");
FILE* fp = fopen(fname, "rb");
if (!fp)
return (1);
unsigned addr = 02000;
unsigned data;
unsigned parity;
while (1 == fread(word, 2, 1, fp))
{
// The file-data is big-endian and aligned to the most-significant
// bit, with a "parity" bit that's always 0 at the least-significant
// position. We need it aligned to the least-significant bit, with
// an odd-parity bit at the most-significant position.
data = parity = (word[0] << 7) | (word[1] >> 1);
/*
parity = (parity ^ (parity << 8));
parity = (parity ^ (parity << 4));
parity = (parity ^ (parity << 2));
parity = (parity ^ (parity << 1));
data |= (parity & 0x8000) ^ 0x8000;
*/
MEM::writeMemory(addr, data);
addr++;
}
fclose(fp);
printw("Memory loaded, %s.\n", fname);
return (0);
}
// Load AGC memory from the specified file object file
int
loadObj(char *filename)
{
// Add the .obj extension.
char fname[80];
strcpy(fname, filename);
strcat(fname, ".obj");
FILE* fp = fopen(fname, "r");
if (!fp)
return (1);
unsigned addr;
unsigned data;
while (fscanf(fp, "%o %o", &addr, &data) != EOF)
{
MEM::writeMemory(addr, data);
}
fclose(fp);
printw("Memory loaded, %s.\n", fname);
return (0);
}
static uint16_t loadBuf[ROPE_SIZE + 1]; // temporary buffer for assembling H,L memory data
int
loadEPROM(char* fileName, bool highBytes)
{
printw("Reading EPROM: %s\n", fileName);
// Open the EPROM file.
FILE* ifp = fopen(fileName, "r");
if (!ifp)
{
printw("fopen failed for source file %s\n", fileName);
return (1);
}
const int addressBytes = 3; // 24-bit address range
const int sumCheckBytes = 1;
char buf[4096]; // buffer holds a single S-Record
while (fgets(buf, 4096, ifp))
{
// process a record
if (buf[0] != 'S')
{
printw("Error reading start of EPROM record for: %s\n", fileName);
return (1);
}
char tmp[256];
strncpy(tmp, &buf[2], 2);
tmp[2] = '\0';
int totalByteCount = strtol(tmp, 0, 16);
int mySumCheck = totalByteCount & 0xff;
strncpy(tmp, &buf[4], 6);
tmp[addressBytes * 2] = '\0';
int address = strtol(tmp, 0, 16);
mySumCheck = (mySumCheck + ((address & 0xff0000) >> 16)) % 256;
mySumCheck = (mySumCheck + ((address & 0x00ff00) >> 8)) % 256;
mySumCheck = (mySumCheck + ((address & 0x0000ff))) % 256;
int dataBytes = totalByteCount - addressBytes - sumCheckBytes;
int i = (addressBytes + 2) * 2; // index to 1st databyte char.
for (int j = 0; j < dataBytes; j++)
{
// get a data byte
strncpy(tmp, &buf[i], 2);
tmp[2] = '\0';
int data = strtol(tmp, 0, 16);
mySumCheck = (mySumCheck + data) % 256;
if (highBytes)
{
loadBuf[address] = loadBuf[address] | ((data << 8) & 0xff00);
}
else
{
loadBuf[address] = loadBuf[address] | (data & 0xff);
}
address++;
i += 2; // bump to next databyte char
}
strncpy(tmp, &buf[i], 2);
tmp[2] = '\0';
int sumCheck = strtol(tmp, 0, 16);
if (sumCheck != ((~mySumCheck) & 0xff))
{
printw(
"sumCheck failed; file: %s, address: %0X, sumCheck: %0X, mySumCheck: %0X\n",
fileName, address, sumCheck, mySumCheck);
return (1);
}
}
fclose(ifp);
printw("%s\n", "Memory loaded.");
return (0);
}
// Load AGC memory from the specified EPROM files
void
loadMemory(char *forceFilename)
{
// Initialize memory. The lowest bit, where the registers are,
// is simply zeroed out, but the remainder of erasable and all
// of fixed is loaded with "illegal" values having the 16th-bit
// set, since the sim has no other way of writing a non-zero
// 16th-bit in these areas.
int i;
for (i = 0; i < 060; i++)
{
MEM::register_EMEM[i].write(0);
MEM::register_EMEM[i].clk();
}
for (i = 060; i < 02000; i++)
{
MEM::register_EMEM[i].write((zeroErasable) ? 0 : 0166666);
MEM::register_EMEM[i].clk();
}
for (i = 02000; i < 041 * 02000; i++)
{
MEM::register_FMEM[i].write(0100000);
MEM::register_FMEM[i].clk();
}
strcpy(filename, forceFilename);
printw("%s\n", filename);
// We first attempt to load from filename.bin (a yaYUL output file).
// Failing that, we attempt to load from filename.obj (from John's
// assembler). Failing that, we fall back filename_H.hex and
// filename_L.hex (Motorola hex files similar to filename.obj).
if (loadYul(filename))
{
printw("Could not load %s.bin ... falling back on %s.obj.\n", filename,
filename);
if (loadObj(filename))
{
printw(
"Could not load %s.obj ... trying %s_H.hex and %s_L.hex instead.\n",
filename, filename, filename);
char fname[80];
// Add the _H.hex extension.
strcpy(fname, filename);
strcat(fname, "_H.hex");
loadEPROM(fname, true);
// Add the _L.hex extension.
strcpy(fname, filename);
strcat(fname, "_L.hex");
loadEPROM(fname, false);
//*******************************************************************
// EPROM is now in loadBuf; move it to AGC memory.
// AGC fixed memory only uses NUMFBANK banks.
for (int address = 02000; address < ROPE_SIZE; address++)
{
// Don't load address region 0-1023; that region is allocated
// to eraseable memory.
MEM::writeMemory(address, loadBuf[address]);
}
//*******************************************************************
}
}
}
// Write the entire contents of fixed and
// eraseable memory to the specified file.
// Does not write the registers
void
saveMemory(const char* filename)
{
FILE* fp = fopen(filename, "w");
if (!fp)
{
printw("*** ERROR: fopen failed: %s\n", filename);
return;
}
char buf[100];
for (unsigned addr = 020; addr < ROPE_SIZE; addr++)
{
sprintf(buf, "%06o %06o\n", addr, MEM::readMemory(addr));
fputs(buf, fp);
}
fclose(fp);
}
void
examineMemory()
{
char theAddress[20], *s;
strcpy(theAddress,
getCommand("Examine Memory -- enter octal address or address=value: "));
printw("%s\n", theAddress);
unsigned address, bank, offset, flat;
if (2 == sscanf(theAddress, "%o,%o", &bank, &offset))
{
address = bank * 02000 + offset % 02000;
}
else if (1 == sscanf(theAddress, "%o", &flat))
{
address = flat;
}
else
{
printw("Unrecognized address.\n");
return;
}
if (/* address < 0 || */ address >= ROPE_SIZE)
{
printw("Address is out of range.\n");
}
s = strstr(theAddress, "=");
if (s != NULL)
{
unsigned Value;
if (1 == sscanf(s, "=%o", &Value) && /* Value >= 0 && */ Value <= 0177777)
{
// I haven't caught all the special cases here, but right now I'm only interested
// in address range 04-14.
if (address == 4)
{
KBD::keypress((keyInType) Value);
MEM::writeMemory(04, 040 | Value);
}
else if (address == 5)
INP::register_IN1.write(Value);
else if (address == 6)
INP::register_IN2.write(Value);
else if (address == 7)
INP::register_IN3.write(Value);
else if (address == 011)
OUT::register_OUT1.write(Value);
else if (address == 012)
OUT::register_OUT2.write(Value);
else if (address == 013)
OUT::register_OUT3.write(Value);
else if (address == 014)
OUT::register_OUT4.write(Value);
else
MEM::writeMemory(address, Value);
}
}
else
{
for (unsigned i = address; i < address + 24 && i < ROPE_SIZE; i++)
{
int data = MEM::readMemory(i);
printw("%06o: %06o\n", i, data & 077777);
}
}
}
// Returns true if time (s) elapsed since last time it returned true; does not block
// search for "Time Management"
bool
checkElapsedTime(time_t s)
{
if (!s)
return true;
static clock_t start = clock();
clock_t finish = clock();
double duration = (double) (finish - start) / CLOCKS_PER_SEC;
if (duration >= s)
{
start = finish;
return true;
}
return false;
}
// Blocks until time (s) has elapsed.
void
delay(time_t s)
{
if (!s)
return;
clock_t start = clock();
clock_t finish = 0;
double duration = 0;
do
{
finish = clock();
}
while ((duration = (double) (finish - start) / CLOCKS_PER_SEC) < s);
}
void
updateAGCDisplay()
{
static bool displayTimeout = false;
static int clockCounter = 0;
if (checkElapsedTime(2))
displayTimeout = true;
if (MON::FCLK)
{
if (MON::RUN)
{
// update every 2 seconds at the start of a new instruction
if (displayTimeout || dskyChanged)
{
clockCounter++;
if ((TPG::register_SG.read() == TP12
&& SEQ::register_SNI.read() == 1)
|| (TPG::register_SG.read() == STBY) || clockCounter > 500
|| dskyChanged)
{
MON::displayAGC();
displayTimeout = false;
clockCounter = 0;
dskyChanged = false;
}
}
}
else
{
static bool displayOnce = false;
if (TPG::register_SG.read() == WAIT)
{
if (displayOnce == false)
{
MON::displayAGC();
displayOnce = true;
clockCounter = 0;
}
}
else
{
displayOnce = false;
}
}
}
else
MON::displayAGC(); // When the clock is manual or slow, always update.
}
void
showMenu()
{
printw("BLOCK 1 EMULATOR MENU:\n");
printw(" 'a' = STANDBY ALLOWED\n");
printw(" 'b' or 'bc' = TOGGLE BREAKPOINT at address or MCT count\n");
printw(" 'c' = TOGGLE SCALER: (for automatically generating F13 and F17.\n");
printw(" 'd' = DISPLAY: refreshes current register display.\n");
printw(" 'e' = EXAMINE: examine or change contents of memory.\n");
printw(" 'f' = DEBUG: automatically display source code.\n");
printw(" 'h' = RESET.\n");
printw(" 'i' = INTERRUPT: generates an AGC interrupt, 1-5.\n");
if (logFile != NULL)
printw(" 'l' = LOGGING: toggle.\n");
printw(" 'm' = MENU: show this menu of commands.\n");
printw(" 'n' = INST: toggle stepping by MCT vs pulse-sequence\n");
printw(" 'p' = POWER UP RESET\n");
printw(" 'q' = QUIT: quit the program.\n");
printw(" 'r' = RUN: toggle RUN/HALT switch upward to the RUN position.\n");
printw(" 's' = STEP: not sure if this does anything.\n");
printw(
" 't' = SINGLE CLOCK: Basically, step one clock pulse or one instruction.\n");
printw(" 'u' = MANUAL CLOCK.\n");
printw(" 'v' = FAST CLOCK.\n");
printw(" 'w' = WHERE: set program counter.\n");
printw(" 'x' = F13: manually generate F13 scaler pulse.\n");
printw(" 'y' = TOGGLE WATCHPOINT\n");
printw(" 'z' = F17: manually generate F17 scaler pulse.\n");
printw(" ']' = +CNTR: give a plus input to a priority counter cell.\n");
printw(" '[' = -CNTR: give a minus input to a priority counter cell.\n");
printw(" ';' = CLEAR PARITY ALARM.\n");
printw(" DSKY:\n");
printw(" '0-9' = NUMBERS.\n");
printw(" '+' = PLUS KEY.\n");
printw(" '-' = MINUS KEY.\n");
printw(" '.' = CLEAR KEY.\n");
printw(" '*' = NOUN KEY.\n");
printw(" '/' = VERB KEY.\n");
printw(" 'g' = KEY RELEASE.\n");
printw(" 'j' = ENTER KEY.\n");
}
#define MAX_LINE_LENGTH 110
const int startCol = 0; // columns are numbered 0-n
const int colLen = 5; // number of chars in column
const int maxLines = 24; // # of total lines to display
const int noffset = 10; // # of lines prior to, and including, selected line
// If/when I finish this code, what it will be is a buffer in memory of the
// entire assembly listing along with an array that gives an index into the
// line array for each possible address on the rope. In other words, given
// a rope address, the associated line of code and the nearby lines can be
// instantly found.
#define MAX_LISTING_LINES 65536 // Empirically, about 30,000 needed.
static char bufferedListing[MAX_LISTING_LINES][MAX_LINE_LENGTH];
static int listingAddresses[035 * 02000];
static int numListingLines = 0;
void
bufferTheListing(char *filename)
{
// Fill in the listingAddresses[] array with default values interpreted
// as "unused".
int i;
for (i = 0; i < ROPE_SIZE; i++)
listingAddresses[i] = -1;
// Add the .lst extension and open the file.
char fname[80], inputLine[1024];
strcpy(fname, filename);
strcat(fname, ".lst");
// Open the file containing the source code listing.
FILE* fp = fopen(fname, "r");
if (!fp)
{
printw("Can't load source list file: %s\n", fname);
return;
}
// Buffer the file in memory.
while (NULL != fgets(inputLine, sizeof(inputLine), fp))
{
char *s;
if (numListingLines >= MAX_LISTING_LINES)
{
printw("Too many lines in source list.\n");
break;
}
s = strstr(inputLine, "\n");
if (*s)
*s = 0;
if (strlen(inputLine) >= MAX_LINE_LENGTH)
{
inputLine[MAX_LINE_LENGTH - 1] = 0;
inputLine[MAX_LINE_LENGTH - 2] = '.';
inputLine[MAX_LINE_LENGTH - 3] = '.';
inputLine[MAX_LINE_LENGTH - 4] = '.';
}
strcpy(bufferedListing[numListingLines], inputLine);
numListingLines++;
}
fclose(fp);
// Fill in the listingAddresses[] array with the flat addresses
// corresponding to the buffered lines. Or more accurately, given
// a flat address in the rope, give the index into the lined buffer
// corresponding to that address. -1 means unused.
for (i = 0; i < numListingLines; i++)
{
char c, *line;
bool found = false;
unsigned totalLine, fileLine, fileBank, fileOffset, fileFlat,
effectiveAddress;
line = bufferedListing[i];
// We don't know if the listing is in John Pultorak's format
// or yaYUL's so we check both.
if (line[0] >= '0' && line[0] <= '7' && line[1] >= '0' && line[1] <= '7'
&& line[2] >= '0' && line[2] <= '7' && line[3] >= '0'
&& line[3] <= '7' && line[4] >= '0' && line[4] <= '7'
&& isspace(line[5]))
found = true;
else if (4
== sscanf(line, "%u,%u:%o%c", &totalLine, &fileLine, &fileFlat, &c)
&& isspace(c))
{
effectiveAddress = fileFlat;
addressOkay: ;
if (effectiveAddress < ROPE_SIZE)
{
// Okay, we have an address corresponding to the line, and
// we know that it is within the rope. However, we don't want
// to use it if the assembler has generated it for various
// pseudo-ops rather than for an actual instruction ... which
// would be legal, but just not something we want to display
// in a program listing.
char fields[5][MAX_LINE_LENGTH];
int j;
j = sscanf(line, "%s%s%s%s%s", fields[0], fields[1], fields[2],
fields[3], fields[4]);
if (j >= 3
&& (!strcmp(fields[2], "BANK") || !strcmp(fields[2], "SETLOC")
|| !strcmp(fields[2], "EQUALS")
|| !strcmp(fields[2], "ERASE")))
{
}
else if (j >= 4
&& (!strcmp(fields[3], "EQUALS")
|| !strcmp(fields[3], "ERASE")))
{
}
else if (j >= 5 && (!strcmp(fields[4], "=")))
{
}
else
found = true;
}
}
else if (5
== sscanf(line, "%u,%u:%o,%o%c", &totalLine, &fileLine, &fileBank,
&fileOffset, &c) && isspace(c))
{
effectiveAddress = fileBank * 02000 + (fileOffset % 02000);
goto addressOkay;
}
// So at this point, if found is true, then effectiveAddress holds the
// address we need to use.
if (found)
listingAddresses[effectiveAddress] = i;
}
}
void
showSourceCode()
{
unsigned effectiveAddress = MON::getPC();
int index = listingAddresses[effectiveAddress];
if (index < 0)
return;
int start = index - 5;
if (start < 0)
start = 0;
int end = index + 5;
if (end >= ROPE_SIZE)
end = ROPE_SIZE - 1;
for (int i = start; i <= end; i++)
{
if (i == index)
printw(" --------------------------------------------------"
"--------------------------------------------------------\n");
printw(" %c %s\n", (i == index) ? '>' : ' ', bufferedListing[i]);
if (i == index)
printw(" --------------------------------------------------"
"--------------------------------------------------------\n");
}
}
int
main(int argc, char* argv[])
{
int power = 0;
char Solarium[] = "Solarium055", *initialRope = Solarium;
bool autoShowSourceCode = true, loggingOn = true;
int stepCount = 0;
// Parse command line.
{
int i, j;
unsigned u;
for (i = 1; i < argc; i++)
{
if (1 == sscanf(argv[i], "--go=%o", &u))
whereGo = u;
else if (!strncmp(argv[i], "--rope=", 7))
initialRope = &argv[i][7];
else if (!strncmp(argv[i], "--log=", 6))
{
logFile = fopen(&argv[i][6], "w");
if (logFile == NULL)
printw("Cannot open log file %s\n", &argv[i][6]);
}
else if (!strcmp(argv[i], "--power"))
power = 3;
else if (1 == sscanf(argv[i], "--extra=%o", &j))
{
if (numLogExtras < MAX_LOG_EXTRAS && j >= 0 && j < ROPE_SIZE)
logExtras[numLogExtras++] = j;
else
printf("Illegal %s\n", argv[i]);
}
else if (!strcmp(argv[i], "--zero"))
zeroErasable = true;
else if (!strcmp(argv[i], "--uninit"))
zeroErasable = false;
else
{
printf("Usage:\n");
printf("\tyaAGC-Block1 [OPTIONS]\n");
printf("Possible OPTIONS:\n");
printf("--go=O Starting address (octal), default 2030.\n");
printf("--rope=F Specify a rope, default Solarium055.\n");
printf("--log=F Specify a log file (default none).\n");
printf(
"--extra=OCT In the --log file, add the value at the address OCT\n");
printf(
" to the log. There can be up to %d of these.\n",
MAX_LOG_EXTRAS);
printf(
"--power Automatically give 'p', 'r', 't' commands\n");
printf(" upon entry.\n");
printf(
"--zero At power-up, initialize erasable 060-01777 to 0,\n");
printf(" This is the default.\n");
printf(
"--uninit At power-up, initialize erasable 060-01777 to 0166666,\n");
printf(" rather than the default 0.\n");
return (1);
}
}
}
#ifdef USE_NCURSES
// Make ncurses getch() non-blocking.
initscr();
keypad(stdscr, TRUE);
cbreak();
noecho();
nodelay(stdscr, TRUE);
scrollok(stdscr, TRUE);
#endif
setvbuf(stdout, NULL, _IONBF, 0);
CPM::readEPROM("CPM1_8.hex", CPM::EPROM1_8);
CPM::readEPROM("CPM9_16.hex", CPM::EPROM9_16);
CPM::readEPROM("CPM17_24.hex", CPM::EPROM17_24);
CPM::readEPROM("CPM25_32.hex", CPM::EPROM25_32);
CPM::readEPROM("CPM33_40.hex", CPM::EPROM33_40);
CPM::readEPROM("CPM41_48.hex", CPM::EPROM41_48);
CPM::readEPROM("CPM49_56.hex", CPM::EPROM49_56);
loadMemory(initialRope);
loadPads(initialRope);
bufferTheListing(initialRope);
bool singleClock = false;
bool anyWZ = false;
genAGCStates();
MON::displayAGC();
while (1)
{
// NOTE: assumes that the display is always pointing to the start of
// a new line at the top of this loop!
// Clock the AGC, but between clocks, poll the keyboard
// for front-panel input by the user. This uses a Microsoft function;
// substitute some other non-blocking function to access the keyboard
// if you're porting this to a different platform.
if (!singleClock && !MON::FCLK)
printw("%s", "> ");
#ifdef USE_NCURSES
while (!_kbhit())
#else
while (NULL == nbfgets(userInput, sizeof(userInput)))
#endif
{
if (MON::RUN && (MON::FCLK || singleClock))
{
// This is a performance enhancement. If the AGC is running,
// don't check the keyboard or simulator display every
// simulation cycle, because that slows the simulator
// down too much.
int genStateCntr = 100;
do
{
CLK::clkAGC();
genAGCStates();
anyWZ = anyWZ || SEQ::anyWZ();
if (!MON::INST
|| (MON::INST && TPG::register_SG.read() == TP1 && anyWZ
&& SEQ::glbl_subseq != CCS1
&& SEQ::glbl_subseq != STD2))
{
singleClock = false;
}
genStateCntr--;
// Needs more work. It doesn't always stop at the
// right location and sometimes stops at the
// instruction afterwards, too.
if (breakpointEnab)
{
int countMCT = (SCL::register_SCL.read() >= 016) ?
((SCL::register_SCL.read() - 016) / 014) : 0;
if (breakpoint == ADR::getEffectiveAddress())
{
//MON::RUN = 0;
MON::FCLK = 0;
stepCount = 0;
singleClock = 0;
}
else if (breakpoint < 0 && countMCT >= -breakpoint)
{
MON::FCLK = 0;
stepCount = 0;
singleClock = 0;
}
}
// Halt right after instr that changes a watched
// memory location.
if (watchEnab)
{
unsigned newWatchValue = MEM::readMemory(watchAddr);
if (newWatchValue != oldWatchValue)
{
MON::RUN = 0;
}
oldWatchValue = newWatchValue;
}
}
while ((MON::FCLK || singleClock) && MON::RUN && genStateCntr > 0);
if (stepCount >= 0 && stepCount <= 1 && !MON::FCLK)
{
updateAGCDisplay();
if (autoShowSourceCode)
showSourceCode();
if (logFile != NULL && loggingOn)
MON::logAGC(logFile);
if (!MON::FCLK)
printw("%s", "> ");
}
else
{
if (logFile != NULL && loggingOn)
MON::logAGC(logFile);
}
}
// for convenience, clear the single step switch on TP1; in the
// hardware AGC, this happens when the switch is released
if (MON::STEP && TPG::register_SG.read() == TP1)
{
MON::STEP = 0;
}
if (stepCount > 0)
{
stepCount--;
if (stepCount > 0)
{
singleClock = true;
anyWZ = false;
}
}
else if (stepCount < 0)
{
singleClock = true;
anyWZ = false;
}
if (power)
break;
}
stepCount = 0;
char key;
if (power == 3)
{
key = 'p';
power--;
}
else if (power == 2)
{
key = 'r';
power--;
}
else if (power == 1)
{
key = 't';
power--;
}
else
{
#ifdef USE_NCURSES
key = _getch();
#else
key = userInput[0];
#endif
}
int newAddress = 02030;
// Keyboard controls for front-panel:
switch (key)
{
case ';':
// Clear ALARM indicators
PAR::CLR_PALM(); // Asynchronously clear PARITY FAIL
MON::displayAGC();
break;
case '+':
KBD::keypress(KEYIN_PLUS);
break;
case '-':
KBD::keypress(KEYIN_MINUS);
break;
case '.':
KBD::keypress(KEYIN_CLEAR);
break;
case '/':
KBD::keypress(KEYIN_VERB);
break;
case '*':
KBD::keypress(KEYIN_NOUN);
break;
case '0':
KBD::keypress(KEYIN_0);
break;
case '1':
KBD::keypress(KEYIN_1);
break;
case '2':
KBD::keypress(KEYIN_2);
break;
case '3':
KBD::keypress(KEYIN_3);
break;
case '4':
KBD::keypress(KEYIN_4);
break;
case '5':
KBD::keypress(KEYIN_5);
break;
case '6':
KBD::keypress(KEYIN_6);
break;
case '7':
KBD::keypress(KEYIN_7);
break;
case '8':
KBD::keypress(KEYIN_8);
break;
case '9':
KBD::keypress(KEYIN_9);
break;
case 'a':
MON::SA = (MON::SA + 1) % 2;
genAGCStates();
MON::displayAGC();
break;
case 'b':
toggleBreakpoint();
break;
case 'c':
MON::SCL_ENAB = (MON::SCL_ENAB + 1) % 2;
genAGCStates();
MON::displayAGC();
break;
case 'd':
genAGCStates();
MON::displayAGC();
break; // update display
case 'e':
examineMemory();
break;
case 'f':
autoShowSourceCode = !autoShowSourceCode;
if (autoShowSourceCode)
showSourceCode();
break;
case 'g':
KBD::keypress(KEYIN_KEY_RELEASE);
break;
case 'h':
KBD::keypress(KEYIN_ERROR_RESET);
break;
case 'i':
interrupt();
//genAGCStates();
//displayAGC(EVERY_CYCLE);
break;
case 'j':
KBD::keypress(KEYIN_ENTER);
break;
case 'l':
loggingOn = !loggingOn;
if (loggingOn)
printw("Logging toggled on.\n");
else
printw("Logging toggled off.\n");
break;
case 'm':
showMenu();
break;
case 'n':
MON::INST = (MON::INST + 1) % 2;
genAGCStates();
MON::displayAGC();
break;
case 'p':
MON::PURST = (MON::PURST + 1) % 2;
genAGCStates();
MON::displayAGC();
break;
case 'q':
printw("%s\n", "QUIT...");
endwin();
if (logFile != NULL)
fclose(logFile);
exit(0);
case 'r':
MON::RUN = (MON::RUN + 1) % 2;
genAGCStates();
if (!MON::FCLK)
MON::displayAGC();
break;
case 's':
MON::STEP = (MON::STEP + 1) % 2;
genAGCStates();
if (!MON::FCLK)
MON::displayAGC();
break;
case 't': // single clock pulse (when system clock off)
if (!MON::PURST && MON::RUN)
{
stepCount = atoi(&userInput[1]);
if (stepCount == 0)
printw("%s\n", "Single clock");
else
printf("%d clocks\n", stepCount);
singleClock = true;
anyWZ = false;
}
else
{
printw("%s\n", "Must be powered-up and in a running state.");
}
break;
case 'u': // manual clock (FCLK=0)
printw("%s\n", "Manual clock");
MON::FCLK = 0;
genAGCStates();
MON::displayAGC();
break;
case 'v': // fast clock (FCLK=1)
printw("%s\n", "Fast clock");
MON::FCLK = 1;
genAGCStates();
MON::displayAGC();
break;
case 'w':
{
char b[80];
strcpy(b,
getCommand("New address (octal, flat address space) [2000]: "));
newAddress = strtol(b, 0, 8);
if (newAddress == 0)
newAddress = 02030;
}
if (newAddress < 06000)
{
ADR::register_S.write(newAddress);
ADR::register_S.clk();
printw("Program counter -> %04o\n", ADR::register_S.read());
}
else
{
ADR::register_S.write(newAddress & 01777);
ADR::register_S.clk();
ADR::register_BNK.write(newAddress >> 10);
ADR::register_BNK.clk();
printw("Program counter -> %02o,%04o\n", ADR::register_BNK.read(),
06000 + ADR::register_S.read());
}
genAGCStates();
MON::displayAGC();
break;
case 'x':
//SCL::F13 = (SCL::F13 + 1) % 2;
genAGCStates();
MON::displayAGC();
break;
case 'y':
toggleWatch();
break;
case 'z':
//SCL::F17 = (SCL::F17 + 1) % 2;
genAGCStates();
MON::displayAGC();
break;
case ']':
incrCntr();
//genAGCStates();
//displayAGC(EVERY_CYCLE);
break;
case '[':
decrCntr();
//genAGCStates();
//displayAGC(EVERY_CYCLE);
break;
default:
printw("%c=%0X\n", key, key);
}
}
return (0);
}