GE-115 Emulator
An Emulator of the General Electrics GE-115 computer
main.c
Go to the documentation of this file.
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <stdint.h>
5#include <unistd.h>
6#include <signal.h>
7#include <sys/types.h>
8#include <sys/wait.h>
9#include "ge.h"
10#include "console_socket.h"
11#include "cardreader.h"
12#include "printer.h"
13#include "disk.h"
14#include "tape.h"
15#include "transcode.h"
16#include "log.h"
17#include "sat_batches.h"
18#include <fcntl.h>
19
20/*
21 * Forward declaration for ge_log_set_active_types_from_spec, which is being
22 * added concurrently in log.c/log.h by another agent. Once that lands the
23 * prototype in log.h this extern becomes redundant but harmless.
24 */
25extern void ge_log_set_active_types_from_spec(const char *spec);
26
27/*
28 * Launch the ncurses console client (console/curses/console.py) as a child
29 * process for --tui. The client connects to the /tmp/gemu.console socket that
30 * --console registers and draws the operator/diagnostic panel; the emulator
31 * keeps running in this (parent) process. Returns the child pid, or -1 if the
32 * client could not be found / launched.
33 *
34 * The script is looked for next to the ge executable first (so it works from
35 * any cwd), then relative to the current directory.
36 */
37/* Interactive console switches driven by signals: SIGUSR1 toggles SWITCH 1
38 * (JS1), SIGUSR2 toggles SWITCH 2 (JS2). The handler only sets a flag; the
39 * run loop applies it between cycles (so we never touch ge state from a
40 * handler). Lets a human (or an automated harness) flip the diagnostic
41 * switches mid-run: e.g. start the funktionalcpu test with SWITCH 2 on, then
42 * `kill -USR2 <pid>` to release it and watch where the deck goes. */
43static volatile sig_atomic_t g_toggle_js1 = 0;
44static volatile sig_atomic_t g_toggle_js2 = 0;
45static void on_sigusr1(int sig) { (void)sig; g_toggle_js1 = 1; }
46static void on_sigusr2(int sig) { (void)sig; g_toggle_js2 = 1; }
47
48static pid_t spawn_tui(const char *argv0)
49{
50 char path[4096];
51 const char *slash = strrchr(argv0, '/');
52
53 if (slash) {
54 int dlen = (int)(slash - argv0);
55 snprintf(path, sizeof(path), "%.*s/console/curses/console.py", dlen, argv0);
56 } else {
57 snprintf(path, sizeof(path), "console/curses/console.py");
58 }
59 if (access(path, R_OK) != 0)
60 snprintf(path, sizeof(path), "console/curses/console.py");
61 if (access(path, R_OK) != 0) {
62 fprintf(stderr, "error: --tui: cannot find console/curses/console.py\n");
63 return -1;
64 }
65
66 pid_t pid = fork();
67 if (pid < 0) {
68 perror("fork");
69 return -1;
70 }
71 if (pid == 0) {
72 execlp("python3", "python3", path, (char *)NULL);
73 perror("error: --tui: cannot exec python3");
74 _exit(127);
75 }
76 return pid;
77}
78
79static void print_usage(const char *argv0)
80{
81 fprintf(stderr,
82 "Usage: %s [OPTIONS] [deck.cap]\n"
83 "\n"
84 " deck.cap A card deck. It is placed in the reader's hopper and\n"
85 " pulled in by the machine's own bootstrap: CLEAR, LOAD1,\n"
86 " LOAD, START. The IPL reads exactly ONE card (80 columns\n"
87 " nibble-packed to 40 bytes at 0x0000) and executes it;\n"
88 " that card's code pulls the rest of the deck.\n"
89 "\n"
90 " A deck is the only way to get a program into the machine, here as on the\n"
91 " iron. Build one with `gasm -o prog.cap prog.s` or `gec -o prog.cap prog.c`.\n"
92 "\n"
93 "Options:\n"
94 " --deck <path> Explicit alias for the positional .cap argument\n"
95 " --sat <id> Use a built-in Site Acceptance Test batch\n"
96 " --list-sat List the built-in SAT batches and exit\n"
97 " --trace <spec> Enable log types from spec string\n"
98 " --max-cycles <N> Maximum CPU cycles before forced exit (default: 100000,\n"
99 " or 500000 for --deck unless overridden)\n"
100 " --console Enable the console socket /tmp/gemu.console (no UI attached)\n"
101 " --tui Implies --console and starts the ncurses console client\n"
102 " --interactive, -i Run until killed; SIGUSR1/SIGUSR2 toggle SWITCH 1/2 at\n"
103 " runtime (prints the pid + step/halt progress)\n"
104 " --switch1 Start with SWITCH 1 (JS1) on\n"
105 " --switch2 Start with SWITCH 2 (JS2) on\n"
106 " (console/curses/console.py); runs until you quit the TUI\n"
107 " --help, -h Print this help and exit\n",
108 argv0);
109}
110
111int main(int argc, char *argv[])
112{
113 struct ge ge;
114 int ret = 0;
115 long max_cycles = 100000;
116 int max_cycles_set = 0;
117 long cycles = 0;
118 int use_console = 0;
119 int use_tui = 0;
120 int trace_set = 0;
121 const char *deck_path = NULL; /* --deck: cycle-faithful card-reader bootstrap */
122 const char *disk_path = NULL; /* --disk: DSS pack image on connector 3 unit 0 */
123 const char *tape_path = NULL; /* --tape: MTC reel image on connector 4 unit 0 */
124 const char *sat_batch = NULL; /* --sat: built-in SAT batch */
125 int interactive = 0; /* --interactive: run until killed, switches via signals */
126 int sw1_init = 0; /* --switch1: start with SWITCH 1 (JS1) on */
127 int sw2_init = 0; /* --switch2: start with SWITCH 2 (JS2) on */
128
129 /* --- argument parsing: --opt value style --- */
130 for (int i = 1; i < argc; i++) {
131 if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
132 print_usage(argv[0]);
133 return 0;
134 } else if (strcmp(argv[i], "--console") == 0) {
135 use_console = 1;
136 } else if (strcmp(argv[i], "--tui") == 0) {
137 use_tui = 1;
138 use_console = 1; /* --tui implies --console */
139 } else if (strcmp(argv[i], "--deck") == 0) {
140 if (i + 1 >= argc) {
141 fprintf(stderr, "error: --deck requires an argument\n");
142 return 1;
143 }
144 deck_path = argv[++i];
145 } else if (strcmp(argv[i], "--disk") == 0) {
146 if (i + 1 >= argc) {
147 fprintf(stderr, "error: --disk requires an argument\n");
148 return 1;
149 }
150 disk_path = argv[++i]; /* DSS pack image; connector 3, unit 0 */
151 } else if (strcmp(argv[i], "--tape") == 0) {
152 if (i + 1 >= argc) {
153 fprintf(stderr, "error: --tape requires an argument\n");
154 return 1;
155 }
156 tape_path = argv[++i]; /* MTC reel image; connector 4, unit 0 */
157 } else if (strcmp(argv[i], "--sat") == 0) {
158 if (i + 1 >= argc) {
159 fprintf(stderr, "error: --sat requires an argument\n");
160 return 1;
161 }
162 sat_batch = argv[++i];
163 } else if (strcmp(argv[i], "--list-sat") == 0) {
164 for (int j = 0; j < sat_batch_count(); j++) {
165 const struct sat_batch_info *info = sat_batch_info_at(j);
166 printf("%-20s %s\n", info->id, info->title);
167 printf(" %s\n", info->summary);
168 }
169 return 0;
170 } else if (strcmp(argv[i], "--trace") == 0) {
171 if (i + 1 >= argc) {
172 fprintf(stderr, "error: --trace requires an argument\n");
173 return 1;
174 }
176 trace_set = 1;
177 } else if (strcmp(argv[i], "--max-cycles") == 0) {
178 if (i + 1 >= argc) {
179 fprintf(stderr, "error: --max-cycles requires an argument\n");
180 return 1;
181 }
182 max_cycles = atol(argv[++i]);
183 max_cycles_set = 1;
184 if (max_cycles <= 0) {
185 fprintf(stderr, "error: --max-cycles must be a positive integer\n");
186 return 1;
187 }
188 } else if (strcmp(argv[i], "--interactive") == 0 || strcmp(argv[i], "-i") == 0) {
189 interactive = 1;
190 } else if (strcmp(argv[i], "--switch1") == 0) {
191 sw1_init = 1;
192 } else if (strcmp(argv[i], "--switch2") == 0) {
193 sw2_init = 1;
194 } else if (argv[i][0] == '-') {
195 fprintf(stderr, "error: unknown option '%s'\n", argv[i]);
196 print_usage(argv[0]);
197 return 1;
198 } else if (!deck_path && !sat_batch) {
199 /* Positional input: a card deck, and nothing else. There is no
200 * direct-to-memory load — the machine has no such door. */
201 const char *p = argv[i];
202 size_t n = strlen(p);
203 if (n < 4 || strcmp(p + n - 4, ".cap") != 0) {
204 fprintf(stderr,
205 "error: '%s' is not a .cap card deck.\n"
206 " A program reaches the machine only on cards; build a deck with\n"
207 " `gasm -o prog.cap prog.s` or `gec -o prog.cap prog.c`.\n", p);
208 return 1;
209 }
210 deck_path = p;
211 } else {
212 fprintf(stderr, "error: unexpected argument '%s'\n", argv[i]);
213 print_usage(argv[0]);
214 return 1;
215 }
216 }
217
218 if (deck_path && sat_batch) {
219 fprintf(stderr, "error: give only one of a .cap deck, --deck, or --sat\n");
220 return 1;
221 }
222
223 /* Reading a deck through the reader costs real machine cycles. Give a deck
224 * run a roomier default so the load does not time out unless the user
225 * explicitly requested a tighter budget. */
226 if ((deck_path || sat_batch) && !max_cycles_set)
227 max_cycles = 500000;
228
229 ge_init(&ge);
230
231 if (use_console) {
233 if (ret != 0) {
234 ge_deinit(&ge);
235 return ret;
236 }
237 }
238
239 ge_clear(&ge);
240
241 /* The only load path there is: put the deck in the hopper, select the load
242 * unit, arm the bootstrap, then START. LOAD itself does nothing but set
243 * AINI — the read happens when the machine is released. */
244 if (sat_batch) {
245 char note[256];
246 static const char sat_cap_path[] = "/tmp/gemu_sat_batch.cap";
247 const struct sat_batch_info *info = sat_batch_find(sat_batch);
248 if (!info) {
249 fprintf(stderr, "error: unknown SAT batch '%s' (use --list-sat)\n", sat_batch);
250 ge_deinit(&ge);
251 return 1;
252 }
253
254 if (sat_batch_prepare_deck("Site_Acceptance_Test", sat_batch,
255 sat_cap_path, note, sizeof(note)) != 0) {
256 fprintf(stderr, "error: failed to compose SAT batch '%s'\n", sat_batch);
257 ge_deinit(&ge);
258 return 1;
259 }
260 ge_load_1(&ge);
261 ge_load(&ge);
262 ret = cardreader_register(&ge, sat_cap_path, TC_NORMAL);
263 if (ret != 0) {
264 fprintf(stderr, "error: failed to load SAT batch '%s'\n", sat_batch);
265 ge_deinit(&ge);
266 return ret;
267 }
268 fprintf(stderr, "SAT batch %s: %s\n", sat_batch, note);
269 } else if (deck_path) {
270 ge_load_1(&ge); /* select connector 2 (LOAD1) */
271 ge_load(&ge); /* set AINI: state 80 -> c8 starts the load sequence */
272 ret = cardreader_register(&ge, deck_path, TC_NORMAL);
273 if (ret != 0) {
274 fprintf(stderr, "error: failed to load deck '%s'\n", deck_path);
275 ge_deinit(&ge);
276 return ret;
277 }
278 }
279
280 ge_start(&ge);
281
282 /* Console switch initial state (after ge_start, which clears them). */
283 ge.JS1 = sw1_init;
284 ge.JS2 = sw2_init;
285
286 /* Attach a DSS disk pack on connector 3 (standard GE-100), if requested. */
287 if (disk_path) {
288 if (disk_register(&ge, disk_path, 3, 0) != 0)
289 fprintf(stderr, "warning: failed to attach disk '%s'\n", disk_path);
290 else
291 fprintf(stderr, "disk: attached '%s' on connector 3 unit 0\n", disk_path);
292 }
293
294 /* Attach an MTC tape reel on connector 4 (standard GE-100), if requested. */
295 if (tape_path) {
296 if (tape_register(&ge, tape_path, 4, 0) != 0)
297 fprintf(stderr, "warning: failed to attach tape '%s'\n", tape_path);
298 else
299 fprintf(stderr, "tape: attached '%s' on connector 4 unit 0\n", tape_path);
300 }
301
302 int printer_enabled = 0;
303 int printed = 0;
304 int kbd_fl = -1;
305 /* The integrated printer/typewriter on channel 2 is part of the machine,
306 * not an option: attach it for every non-TUI run, so a deck that prints is
307 * not left parked on an unanswered PER.
308 *
309 * It used to swallow the card load instead. State b8 is shared between the
310 * channel-1 reader input-wait and the channel-2 print-wait, and printer.c
311 * answered both, so the IPL fell through to alpha at address 0 having read
312 * nothing. It now checks PC121 -- the machine's own decode of "connector 2
313 * on channel 1", the card reader -- and keeps out of an order that is not
314 * its own. See printer.c. */
315 if (!use_tui) {
317 printer_enabled = 1;
318 kbd_fl = fcntl(0, F_GETFL, 0);
319 if (kbd_fl != -1)
320 fcntl(0, F_SETFL, kbd_fl | O_NONBLOCK);
321 }
322
323 if (interactive) {
324 /* Signal-driven interactive run: flip the diagnostic switches with
325 * `kill -USR1/-USR2 <pid>` and watch the deck. Run until killed.
326 * Freeze PC on HLT (the GE-120 sequencer is frozen by ALTO when
327 * halted) so the stop address stays readable; signals are still
328 * serviced so you can record a switch change before restarting. */
329 signal(SIGUSR1, on_sigusr1);
330 signal(SIGUSR2, on_sigusr2);
331 if (!trace_set)
333 /* Integrated printer/typewriter on channel 2: completes print PERs (so
334 * the machine does not hang waiting for a device gemu does not drive at
335 * signal level) and captures output. Two-way: bytes typed on stdin are
336 * fed to the operator keyboard queue (non-blocking). */
337 if (!printer_enabled) {
339 printer_enabled = 1;
340 }
341 kbd_fl = fcntl(0, F_GETFL, 0);
342 if (kbd_fl != -1)
343 fcntl(0, F_SETFL, kbd_fl | O_NONBLOCK);
344 printed = 0; /* bytes of printer output already echoed to stdout */
345 long pid = (long)getpid();
346 printf("interactive: pid=%ld SWITCH1=%d SWITCH2=%d\n", pid, ge.JS1, ge.JS2);
347 printf(" kill -USR1 %ld # toggle SWITCH 1 (JS1)\n", pid);
348 printf(" kill -USR2 %ld # toggle SWITCH 2 (JS2)\n", pid);
349 printf(" type to feed the operator keyboard; printer output appears as 'PRN> ...'\n");
350 fflush(stdout);
351 uint8_t last_step = ge.mem[0x0010];
352 int was_halted = -1;
353 for (;;) {
354 /* Drain newly-printed characters to the terminal. */
355 int olen = printer_output_len(&ge);
356 if (olen > printed) {
357 const char *o = printer_output(&ge);
358 printf("PRN> %.*s", olen - printed, o + printed);
359 printed = olen;
360 fflush(stdout);
361 }
362 /* Feed any typed bytes to the operator keyboard queue. */
363 {
364 unsigned char kb[64];
365 ssize_t r = read(0, kb, sizeof kb);
366 for (ssize_t k = 0; k < r; k++)
367 printer_feed_key(&ge, kb[k]);
368 }
369 if (g_toggle_js1) {
370 g_toggle_js1 = 0; ge.JS1 = !ge.JS1;
371 printf("[cyc %ld] SWITCH 1 -> %d PO=%04x step=0x%02x%s\n",
372 cycles, ge.JS1, ge.rPO, ge.mem[0x0010],
373 ge_halted(&ge) ? " (halted)" : "");
374 fflush(stdout);
375 }
376 if (g_toggle_js2) {
377 g_toggle_js2 = 0; ge.JS2 = !ge.JS2;
378 printf("[cyc %ld] SWITCH 2 -> %d PO=%04x step=0x%02x%s\n",
379 cycles, ge.JS2, ge.rPO, ge.mem[0x0010],
380 ge_halted(&ge) ? " (halted)" : "");
381 fflush(stdout);
382 }
383 if (ge_halted(&ge)) {
384 if (was_halted != 1) {
385 printf("[cyc %ld] HALT PO=%04x step=0x%02x\n",
386 cycles, ge.rPO, ge.mem[0x0010]);
387 fflush(stdout);
388 was_halted = 1;
389 }
390 usleep(5000); /* frozen; still responsive to signals */
391 continue;
392 }
393 was_halted = 0;
394 ret = ge_run_cycle(&ge);
395 cycles++;
396 if (ret != 0)
397 break;
398 uint8_t st = ge.mem[0x0010];
399 if (st != last_step) {
400 printf("[cyc %ld] step -> 0x%02x PO=%04x\n", cycles, st, ge.rPO);
401 fflush(stdout);
402 last_step = st;
403 }
404 }
405 } else if (use_tui) {
406 /* Interactive session: launch the ncurses client and run the emulator
407 * until the user quits the TUI. Ignore max-cycles, and keep cycling
408 * even after a HLT so the console socket stays serviced and the panel
409 * stays live (a halted GE-120 just spins on HLT;JU self). Throttle when
410 * halted so an idle session doesn't peg a core. */
411 /* The TUI owns the terminal; silence the (all-on by default) log so it
412 * doesn't scribble over the panel — unless the user explicitly asked
413 * for a --trace. */
414 if (!trace_set)
416 pid_t tui_pid = spawn_tui(argv[0]);
417 if (tui_pid < 0) {
418 ge_deinit(&ge);
419 return 1;
420 }
421 while (waitpid(tui_pid, NULL, WNOHANG) == 0) {
422 ret = ge_run_cycle(&ge);
423 cycles++;
424 if (ret != 0)
425 break;
426 if (ge_halted(&ge))
427 usleep(2000);
428 }
429 /* The TUI restores the terminal (curses.endwin) on quit; make sure the
430 * child is gone before we print and exit. */
431 kill(tui_pid, SIGTERM);
432 waitpid(tui_pid, NULL, 0);
433 } else {
434 while (!ge_halted(&ge) && cycles < max_cycles) {
435 if (printer_enabled) {
436 int olen = printer_output_len(&ge);
437 if (olen > printed) {
438 const char *o = printer_output(&ge);
439 fwrite(o + printed, 1, (size_t)(olen - printed), stdout);
440 fflush(stdout);
441 printed = olen;
442 }
443 unsigned char kb[64];
444 ssize_t r = read(0, kb, sizeof kb);
445 for (ssize_t k = 0; k < r; k++)
446 printer_feed_key(&ge, kb[k]);
447 }
448 ret = ge_run_cycle(&ge);
449 cycles++;
450 if (ret != 0)
451 break;
452 }
453 }
454
455 printf("exit: halted=%d cycles=%ld max=%ld error=%d state=%02x PO=%04x\n",
456 ge_halted(&ge), cycles, max_cycles, ret, ge.rSO, ge.rPO);
457
458 ge_deinit(&ge);
459 return ret;
460}
int cardreader_register(struct ge *ge, const char *cap_path, enum transcode_mode mode)
Definition cardreader.c:632
int console_socket_register(struct ge *ge)
int disk_register(struct ge *ge, const char *image_path, uint8_t connector, uint8_t unit)
Definition disk.c:105
int ge_deinit(struct ge *ge)
Deinitialize the emulator.
Definition ge.c:560
void ge_load_1(struct ge *ge)
Emulate the press of the "load 1" button in the console.
Definition ge.c:372
void ge_clear(struct ge *ge)
Emulate the press of the "clear" button in the console.
Definition ge.c:137
int ge_run_cycle(struct ge *ge)
Run all GE "mastri" clock periods until next clock cycle.
Definition ge.c:549
void ge_init(struct ge *ge)
Initialize the emulator.
Definition ge.c:14
void ge_load(struct ge *ge)
Emulate the press of the "load" button in the console.
Definition ge.c:362
void ge_start(struct ge *ge)
Emulate the press of the "start" button in the console.
Definition ge.c:394
static uint8_t ge_halted(const struct ge *ge)
Is the CPU stopped?
Definition ge.h:929
int main(int argc, char *argv[])
Definition main.c:111
static void on_sigusr2(int sig)
Definition main.c:46
void ge_log_set_active_types_from_spec(const char *spec)
Set active log types from a comma-separated name specification.
Definition log.c:47
static void print_usage(const char *argv0)
Definition main.c:79
static void on_sigusr1(int sig)
Definition main.c:45
static volatile sig_atomic_t g_toggle_js1
Definition main.c:43
static pid_t spawn_tui(const char *argv0)
Definition main.c:48
static volatile sig_atomic_t g_toggle_js2
Definition main.c:44
int printer_output_len(struct ge *ge)
Definition printer.c:480
int printer_register(struct ge *ge)
Definition printer.c:436
const char * printer_output(struct ge *ge)
Definition printer.c:485
void printer_feed_key(struct ge *ge, uint8_t c)
Definition printer.c:470
const struct sat_batch_info * sat_batch_find(const char *id)
int sat_batch_count(void)
const struct sat_batch_info * sat_batch_info_at(int idx)
int sat_batch_prepare_deck(const char *root, const char *id, const char *out_path, char *note, size_t note_sz)
The entire state of the emulated system, including registers, memory, peripherals and timings.
Definition ge.h:172
uint8_t JS2
Console jump condition 2.
Definition ge.h:496
uint16_t rPO
Program addresser.
Definition ge.h:191
uint8_t rSO
Main sequencer.
Definition ge.h:309
uint8_t mem[MEM_SIZE]
The memory of the emulated system.
Definition ge.h:695
uint8_t JS1
Console jump condition 1.
Definition ge.h:495
const char * summary
Definition sat_batches.h:12
const char * title
Definition sat_batches.h:11
const char * id
Definition sat_batches.h:10
int tape_register(struct ge *ge, const char *image_path, uint8_t connector, uint8_t unit)
Definition tape.c:138
@ TC_NORMAL
Definition transcode.h:19