Skip to content

Commit b273d14

Browse files
committed
Add authentic deck loading and cap deck concatenation
1 parent ac49c47 commit b273d14

11 files changed

Lines changed: 621 additions & 10 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ tests/tests: $(TESTS) libge.a
3737
tools:
3838
$(MAKE) -C assembler gasm
3939
$(MAKE) -C disassembler gdis
40+
$(MAKE) -C tools capcat
4041

4142
.PHONY: check
4243
.PHONY: native-reset
@@ -97,6 +98,7 @@ clean:
9798
rm -f $(TESTS) $(TESTS:%.o=%.d)
9899
$(MAKE) -C assembler clean
99100
$(MAKE) -C disassembler clean
101+
$(MAKE) -C tools clean
100102
# NB: do NOT recurse into console/wasm clean here — the wasm build's own
101103
# libge.a step calls `make -C ../.. clean`, so recursing would delete the
102104
# console/wasm/main.o it just built and break `make wasm`. Clean the wasm

cardreader.c

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ struct cardreader_ctx {
9999
* to two packed nibbles here.) `half` tracks which nibble is pending. */
100100
int pack;
101101
int half; /* 0 = high nibble pending, 1 = low nibble pending */
102+
int post_loader_pack;
102103

103104
/* the ge_peri node we allocated (kept for potential future use) */
104105
struct ge_peri peri;
@@ -215,6 +216,7 @@ static int cr_advance(struct cardreader_ctx *ctx)
215216
static int cardreader_on_clock(struct ge *ge, void *opaque)
216217
{
217218
struct cardreader_ctx *ctx = (struct cardreader_ctx *)opaque;
219+
int pack_now;
218220

219221
/* TU03N card-feed strobe (CE09), read as a one-cycle pulse: the CPU raises
220222
* it at end-of-card (state ea). We latch and clear it here at TO00 so it
@@ -320,6 +322,8 @@ static int cardreader_on_clock(struct ge *ge, void *opaque)
320322
* loader card in the registered mode and the program cards in TC_BINARY
321323
* ("by-pass"). */
322324
enum transcode_mode m;
325+
pack_now = ctx->pack || (ctx->post_loader_pack &&
326+
ctx->card_idx != ctx->loader_card);
323327
if (ctx->pack)
324328
m = ctx->mode;
325329
else if (ctx->card_idx == ctx->loader_card)
@@ -331,6 +335,13 @@ static int cardreader_on_clock(struct ge *ge, void *opaque)
331335
* must NOT override the loader card — it applies to the PROGRAM
332336
* cards the loader's Set-by-Pass selects. */
333337
m = ctx->mode;
338+
else if (ctx->post_loader_pack)
339+
/* The recovered loader program reads the following program cards in
340+
* by-pass / column-binary form. The current signal-level model does
341+
* not yet expose a distinct "non-packed by-pass" transfer state, so
342+
* we feed each decoded COLBIN byte as a hi/lo nibble pair through
343+
* the existing channel-1 packer. */
344+
m = TC_COLBIN;
334345
else if (ge->integrated_reader.active_valid)
335346
m = ge->integrated_reader.active_mode;
336347
else
@@ -355,7 +366,7 @@ static int cardreader_on_clock(struct ge *ge, void *opaque)
355366
* In legacy mode one full value is presented per column. */
356367
uint8_t present;
357368
int is_last;
358-
if (ctx->pack) {
369+
if (pack_now) {
359370
present = (ctx->half == 0) ? (uint8_t)((byte >> 4) & 0x0f)
360371
: (uint8_t)(byte & 0x0f);
361372
is_last = (ctx->half == 1) && is_last_col;
@@ -377,7 +388,7 @@ static int cardreader_on_clock(struct ge *ge, void *opaque)
377388
* mode / framing visible at the pins. */
378389
ge->integrated_reader.pom01 = (m == TC_BINARY || m == TC_COLBIN);
379390
ge->integrated_reader.picon = (ctx->col_idx == 0);
380-
ge->integrated_reader.bi20 = (ctx->pack && ctx->half == 1);
391+
ge->integrated_reader.bi20 = (pack_now && ctx->half == 1);
381392

382393
reader_setup_to_send(ge, present, is_last ? 1 : 0);
383394
ctx->end_of_card_presented = is_last;
@@ -387,14 +398,20 @@ static int cardreader_on_clock(struct ge *ge, void *opaque)
387398
* the next call (do not move the pointer); only after the low nibble do
388399
* we advance to the next column / card. For a multi-card deck cr_advance
389400
* moves to (card+1, col 0), ready for the next PER read. */
390-
if (ctx->pack && ctx->half == 0) {
401+
if (pack_now && ctx->half == 0) {
391402
ctx->half = 1;
392403
} else {
393404
ctx->half = 0;
394405
if (is_last_col) {
395-
/* Card boundary: defer the cross-to-next-card feed until the
396-
* CPU's TU03N end-of-card strobe (state ea). */
397-
ctx->feed_pending = 1;
406+
/* Only the bootstrap loader card uses the explicit TU03 feed
407+
* pulse (state ea). Once that loader has switched the reader to
408+
* by-pass and is pulling binary program cards into its buffer,
409+
* the next card becomes visible at the transfer boundary rather
410+
* than waiting for the bootstrap-only TU03 path. */
411+
if (ctx->card_idx == ctx->loader_card && !ctx->pack)
412+
ctx->feed_pending = 1;
413+
else
414+
cr_advance(ctx);
398415
} else {
399416
cr_advance(ctx);
400417
}
@@ -475,6 +492,7 @@ static int cr_register(struct ge *ge, const char *cap_path,
475492
auto_loader = cr_find_hollerith_loader_card(deck);
476493
if (auto_loader >= 0)
477494
ctx->mode = TC_HEX;
495+
ctx->post_loader_pack = !ctx->pack && ctx->mode == TC_HEX;
478496

479497
/* Find the first non-empty card at or after first_card */
480498
ctx->card_idx = auto_loader >= 0 ? auto_loader : (first_card < 0 ? 0 : first_card);

docs/peripherals.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,80 @@ the overlapped typewriter/printer transfer to finish before halting.
174174
`present` is set only by `printer_register`, so bootstrap/reader tests (which do
175175
not register a printer) leave it 0 and are completely unaffected.
176176

177+
### Example: load a small printer program
178+
179+
`assembler/examples/print.s` is the shortest end-to-end channel-2 output demo.
180+
It issues a `PER 0x80, order`, waits for `__io_status` at `0x0031` to become
181+
`0x01`, then halts.
182+
183+
Build it:
184+
185+
```sh
186+
make -C software/gemu tools ge
187+
./software/gemu/assembler/gasm \
188+
-o /tmp/print.bin \
189+
./software/gemu/assembler/examples/print.s
190+
```
191+
192+
Run it from the CLI:
193+
194+
```sh
195+
./software/gemu/ge /tmp/print.bin
196+
```
197+
198+
You should see:
199+
200+
```text
201+
PRN> HELLO
202+
exit: halted=1 ...
203+
```
204+
205+
Run it in wasm:
206+
207+
```sh
208+
make -C software/gemu wasm
209+
```
210+
211+
Then open the wasm console, choose `/tmp/print.bin` in the simulator gadget,
212+
press `Stage`, then on the panel press `CLEAR`, `LOAD`, `START`. The printer
213+
paper view should show `HELLO`, then the CPU halts.
214+
215+
### Example: load the authentic printer deck
216+
217+
The real punched deck in the dump set is
218+
`software/DUMP1/printermechanicaltest.cap`. This is a full card deck, not a
219+
direct binary image.
220+
221+
CLI, authentic reader/bootstrap path:
222+
223+
```sh
224+
./software/gemu/ge --deck ./software/DUMP1/printermechanicaltest.cap
225+
```
226+
227+
That path uses the real reader flow (`LOAD1`, `LOAD`, bootstrap card, then deck
228+
cards through the integrated reader).
229+
230+
CLI, fast scatter-decoded path:
231+
232+
```sh
233+
./software/gemu/ge ./software/DUMP1/printermechanicaltest.cap
234+
```
235+
236+
That uses the default positional `.cap` image path: the deck is scatter-decoded
237+
to memory first, then execution starts at the lowest loaded address.
238+
239+
Wasm:
240+
241+
1. Choose `software/DUMP1/printermechanicaltest.cap` in the simulator gadget.
242+
2. Press `Stage`.
243+
3. On the panel press `CLEAR`.
244+
4. Press `LOAD`.
245+
5. Press `START`.
246+
247+
In the browser, `.cap` currently follows the staged scatter-image path rather
248+
than the authentic multi-card reader chain, so use the `--deck` CLI form above
249+
when you specifically want to exercise the real bootstrap/read path.
250+
177251
### Two-way chat (wasm + interactive CLI)
178252

179253
* **Output**`printer_output()` / `printer_output_len()` expose the captured

docs/punchcards.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,28 @@ as a gemu-loadable **unified binary** (`gdis --image -o deck.bin deck.cap`; see
355355
`docs/binformat.md`), bridging a Hollerith `.cap` deck to the binary load path
356356
(`./ge deck.bin`).
357357
358+
When a base deck needs extra payload cards appended, use `tools/capcat`:
359+
360+
```sh
361+
make -C software/gemu tools
362+
363+
# append one or more unified GE12 images
364+
software/gemu/tools/capcat -o combined.cap \
365+
software/DUMP1/funktionalcpu.cap \
366+
overlay.bin
367+
368+
# raw flat overlay at an explicit address
369+
software/gemu/tools/capcat -o combined.cap \
370+
software/DUMP1/funktionalcpu.cap \
371+
0x2E00:segment2.raw
372+
```
373+
374+
`capcat` copies the base deck's parsed hex-token cards, auto-detects its dominant
375+
8-byte program-card prefix, and emits appended self-addressed COLBIN cards
376+
(`LL`, load address, payload). The result is a real `.cap` medium that can be
377+
scatter-loaded (`./ge combined.cap`) or run through the authentic reader path
378+
(`./ge --deck combined.cap`).
379+
358380
---
359381

360382
*Findings derived from the project's own reader firmware and the `gemu`

main.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ static void print_usage(const char *argv0)
9090
" e.g. --poke 0x0E00=0x80 to set a diagnostic option\n"
9191
" --deck <path> Path to a .cap card deck; loaded via the reader (connector 2)\n"
9292
" --trace <spec> Enable log types from spec string\n"
93-
" --max-cycles <N> Maximum CPU cycles before forced exit (default: 100000)\n"
93+
" --max-cycles <N> Maximum CPU cycles before forced exit (default: 100000,\n"
94+
" or 500000 for --deck unless overridden)\n"
9495
" --console Enable the console socket /tmp/gemu.console (no UI attached)\n"
9596
" --tui Implies --console and starts the ncurses console client\n"
9697
" --interactive, -i Run until killed; SIGUSR1/SIGUSR2 toggle SWITCH 1/2 at\n"
@@ -107,6 +108,7 @@ int main(int argc, char *argv[])
107108
struct ge ge;
108109
int ret = 0;
109110
long max_cycles = 100000;
111+
int max_cycles_set = 0;
110112
long cycles = 0;
111113
int use_console = 0;
112114
int use_tui = 0;
@@ -150,6 +152,7 @@ int main(int argc, char *argv[])
150152
return 1;
151153
}
152154
max_cycles = atol(argv[++i]);
155+
max_cycles_set = 1;
153156
if (max_cycles <= 0) {
154157
fprintf(stderr, "error: --max-cycles must be a positive integer\n");
155158
return 1;
@@ -220,6 +223,13 @@ int main(int argc, char *argv[])
220223
return 1;
221224
}
222225

226+
/* The cycle-faithful card-reader bootstrap is substantially slower than a
227+
* direct binary or scatter-loaded image. Give `--deck` a roomier default so
228+
* a real deck does not time out during the load unless the user explicitly
229+
* requested a tighter budget. */
230+
if (deck_path && !max_cycles_set)
231+
max_cycles = 500000;
232+
223233
ge_init(&ge);
224234

225235
if (use_console) {

reader.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ void reader_send_tu00(struct ge *ge)
5252
case 0x01: r->mode_n002 = 1; r->active_mode = TC_NORMAL; break; /* read normal ii */
5353
case 0x24: r->mode_mi01 = 1; r->active_mode = TC_NORMAL; break; /* read mixed i */
5454
case 0x04: r->mode_mi02 = 1; r->active_mode = TC_NORMAL; break; /* read mixed ii */
55-
case 0x20: r->mode_debi = 1; r->active_mode = TC_BINARY; break; /* read binary */
55+
case 0x20: r->mode_debi = 1; r->active_mode = TC_COLBIN; break; /* read by-pass / column-binary */
56+
case 0xa0: r->mode_debi = 1; r->active_mode = TC_COLBIN; break; /* channel code card: with by-pass */
5657
default: latch = 0; break; /* 0x40 etc.: keep mode */
5758
}
5859
if (latch) { r->cocon = 1; r->active_valid = 1; }

tests/cap.c

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <string.h>
24

35
#include "utest.h"
46
#include "../cap.h"
7+
#include "../binimage.h"
8+
#include "../transcode.h"
9+
10+
static uint16_t test_colbin_encode_byte(uint8_t b)
11+
{
12+
uint16_t col = 0;
13+
14+
if (b & 0x80) col |= (uint16_t)(1u << 0);
15+
if (b & 0x40) col |= (uint16_t)(1u << 1);
16+
if (b & 0x20) col |= (uint16_t)(1u << 2);
17+
if (b & 0x10) col |= (uint16_t)(1u << 3);
18+
if (b & 0x08) col |= (uint16_t)(1u << 6);
19+
if (b & 0x04) col |= (uint16_t)(1u << 7);
20+
if (b & 0x02) col |= (uint16_t)(1u << 8);
21+
if (b & 0x01) col |= (uint16_t)(1u << 9);
22+
23+
return col;
24+
}
25+
26+
static int write_single_card_cap(const char *path, const uint16_t cols[80])
27+
{
28+
FILE *f = fopen(path, "w");
29+
if (!f)
30+
return -1;
31+
fprintf(f, "Synthetic cap for capcat test\n");
32+
fprintf(f, "Card n. 1\n");
33+
for (int i = 0; i < 80; i++)
34+
fprintf(f, "%04X%c", cols[i] & 0x1FFFu, (i == 79) ? '\n' : ' ');
35+
fclose(f);
36+
return 0;
37+
}
538

639
UTEST(cap, parse_funktional)
740
{
@@ -29,3 +62,54 @@ UTEST(cap, missing_file)
2962
struct cap_deck *d = cap_load("DUMP1/does-not-exist.cap");
3063
ASSERT_TRUE(d == NULL);
3164
}
65+
66+
UTEST(cap, capcat_appends_overlay_cards)
67+
{
68+
static const char base_path[] = "/tmp/gemu_capcat_base.cap";
69+
static const char overlay_path[] = "/tmp/gemu_capcat_overlay.bin";
70+
static const char out_path[] = "/tmp/gemu_capcat_out.cap";
71+
static const char tool_path[] = "tools/capcat";
72+
static const uint8_t prefix[8] = { 0x00, 0x04, 0x40, 0x00, 0x20, 0x40, 0x40, 0x42 };
73+
uint16_t cols[80] = {0};
74+
uint8_t image[65536];
75+
unsigned lo = 0, hi = 0;
76+
char cmd[512];
77+
FILE *probe;
78+
79+
probe = fopen(tool_path, "r");
80+
if (!probe) {
81+
printf(" [SKIP] %s not built\n", tool_path);
82+
return;
83+
}
84+
fclose(probe);
85+
86+
for (int i = 0; i < 8; i++)
87+
cols[i] = test_colbin_encode_byte(prefix[i]);
88+
cols[8] = test_colbin_encode_byte(0x01); /* 2 payload bytes */
89+
cols[9] = test_colbin_encode_byte(0x01);
90+
cols[10] = test_colbin_encode_byte(0x00);
91+
cols[11] = test_colbin_encode_byte(0xAA);
92+
cols[12] = test_colbin_encode_byte(0x55);
93+
ASSERT_EQ(write_single_card_cap(base_path, cols), 0);
94+
95+
{
96+
uint8_t ov[3] = { 0x11, 0x22, 0x33 };
97+
FILE *f = fopen(overlay_path, "wb");
98+
ASSERT_TRUE(f != NULL);
99+
ASSERT_EQ(binimage_write(f, 0x0200, 0x0200, ov, (uint16_t)sizeof(ov)), BINIMAGE_OK);
100+
fclose(f);
101+
}
102+
103+
snprintf(cmd, sizeof(cmd), "%s -o %s %s %s >/tmp/gemu_capcat_cmd.log 2>&1",
104+
tool_path, out_path, base_path, overlay_path);
105+
ASSERT_EQ(system(cmd), 0);
106+
107+
memset(image, 0, sizeof(image));
108+
ASSERT_GT(cap_load_scattered(out_path, TC_COLBIN, image, &lo, &hi), 0);
109+
ASSERT_EQ(lo, 0x0100u);
110+
ASSERT_EQ(image[0x0100], 0xAA);
111+
ASSERT_EQ(image[0x0101], 0x55);
112+
ASSERT_EQ(image[0x0200], 0x11);
113+
ASSERT_EQ(image[0x0201], 0x22);
114+
ASSERT_EQ(image[0x0202], 0x33);
115+
}

0 commit comments

Comments
 (0)