From 4551c88a505c50e1dee5e7356d6269c539fce0b9 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Sun, 1 Mar 2026 13:37:05 -0500 Subject: [PATCH] Implement BSAVE/BLOAD, TUI color, extended PEEK/POKE, update to v0.12.0 BSAVE/BLOAD: save and load virtual memory blocks with 0xFD-header binary format, operating on the current DEF SEG segment. TUI color: tui_refresh emits ANSI SGR codes from cell attributes; COLOR statement sets tui.current_attr when TUI is active. Extended PEEK/POKE: CGA graphics framebuffer (interlaced layout) via gfx_cga_peek/poke routed through virmem when gfx_active(); BIOS keyboard shift flags (offset 0x17 bit 7 = insert mode). Add bibliography to language reference. 64 tests, all passing. --- README.md | 10 ++-- docs/architecture.md | 2 +- docs/development.md | 9 ++-- docs/index.md | 2 +- docs/language-reference.md | 36 ++++++++++++- docs/roadmap.md | 7 --- include/graphics.h | 4 ++ include/gwbasic.h | 2 +- src/graphics.c | 61 +++++++++++++++++++++ src/interp.c | 83 ++++++++++++++++++++++++++++- src/tui.c | 26 +++++++++ src/virmem.c | 15 ++++-- tests/expected/bsave_bload.expected | 5 ++ tests/expected/peek_gfx.expected | 4 ++ tests/programs/bsave_bload.bas | 18 +++++++ tests/programs/color_test.bas | 8 +++ tests/programs/peek_gfx.bas | 17 ++++++ 17 files changed, 282 insertions(+), 27 deletions(-) create mode 100644 tests/expected/bsave_bload.expected create mode 100644 tests/expected/peek_gfx.expected create mode 100644 tests/programs/bsave_bload.bas create mode 100644 tests/programs/color_test.bas create mode 100644 tests/programs/peek_gfx.bas diff --git a/README.md b/README.md index 9cf12ee..1d52da7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Interactive mode launches the authentic GW-BASIC full-screen editor: ``` $ ./gwbasic -GW-BASIC 2026 0.11.0 +GW-BASIC 2026 0.12.0 (C) Eremey Valetov 2026. MIT License. Based on Microsoft GW-BASIC assembly source. Ok @@ -77,7 +77,7 @@ SPACE$, STRING$, HEX$, OCT$, INSTR, INPUT$ | File management | KILL, NAME, FILES, MKDIR, RMDIR, CHDIR, SHELL | | Date/time | DATE$, TIME$, TIMER | | Screen | LOCATE, COLOR, WIDTH, SCREEN, KEY ON/OFF/LIST | -| Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT, GET/PUT (sprites) | +| Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT, GET/PUT (sprites), BSAVE, BLOAD | | Sound | SOUND, BEEP, PLAY (MML) | | Memory | DEF SEG, PEEK, POKE | @@ -162,8 +162,8 @@ expected on a real IBM PC: | Segment | Description | |---------|-------------| -| `0040` | BIOS data area — video mode, cursor position, timer ticks (18.2 Hz) | -| `B800` | CGA text buffer — character/attribute pairs (80×25 = 4000 bytes) | +| `0040` | BIOS data area — video mode, cursor position, timer ticks (18.2 Hz), keyboard flags | +| `B800` | CGA text buffer (text mode) or CGA framebuffer (graphics mode) | ``` DEF SEG = &HB800 @@ -220,7 +220,7 @@ input, so they're not part of the automated test suite. ## Tests -61 test programs in `tests/programs/`, with golden-file regression testing +64 test programs in `tests/programs/`, with golden-file regression testing and CI via GitHub Actions: ```bash diff --git a/docs/architecture.md b/docs/architecture.md index ed69050..3ffee9e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -57,7 +57,7 @@ HAL writes straight to stdout. src/ — core interpreter (21 files) include/ — headers (13 files) platform/ — HAL backends (1 file) -tests/ — 61 automated test programs, 4 classic interactive programs, compat harness +tests/ — 64 automated test programs, 4 classic interactive programs, compat harness docs/ — Sphinx documentation ``` diff --git a/docs/development.md b/docs/development.md index dc3ee1f..6533fa4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -15,10 +15,11 @@ | 0.9.0 | `2a8f98b` | EDIT statement, ON TIMER/ON KEY event trapping, F-key escape parser fixes | | 0.10.0 | | Binary tokenized SAVE/LOAD, INKEY$ extended key sequences, golden-file regression tests, classic BASIC programs | | 0.11.0 | | DEF SEG / PEEK / POKE virtual memory, GET/PUT graphics sprites, PRINT USING fixes | +| 0.12.0 | | BSAVE/BLOAD, TUI ANSI 16-color rendering, CGA graphics framebuffer PEEK/POKE, keyboard shift flags | ## Tests -61 test programs in `tests/programs/`, plus 4 classic interactive programs in +64 test programs in `tests/programs/`, plus 4 classic interactive programs in `tests/classic/` (Hamurabi, Lunar Lander, Gunner, Diamond from David Ahl's *BASIC Computer Games*). @@ -28,9 +29,9 @@ Run the full automated suite: bash tests/run_tests.sh ``` -Each test has a 5-second timeout. 57 tests have `.expected` golden files +Each test has a 5-second timeout. 60 tests have `.expected` golden files for output regression detection. Three timing-dependent tests (datetime, -on_timer, timer_stop) and one sound test (play_scale) run without golden +on_timer, timer_stop) and one visual test (color_test) run without golden comparison. ### Compatibility Testing @@ -48,6 +49,6 @@ bash tests/run_compat.sh ## CI GitHub Actions runs on every push to `main` and on pull requests. The workflow -builds the project with PulseAudio support and runs all 61 test programs. +builds the project with PulseAudio support and runs all 64 test programs. See [`.github/workflows/ci.yml`](https://github.com/evvaletov/gw-basic-2026/blob/main/.github/workflows/ci.yml). diff --git a/docs/index.md b/docs/index.md index 8476bea..60e94f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ version is structured as modular C suitable for new feature development. - **Printer output** — `LPRINT`/`LLIST` to file or real hardware via `--lpt` - **Classic programs** — Hamurabi, Lunar Lander, Gunner, and Diamond from David Ahl's *BASIC Computer Games* (1978) run out of the box -- **58 test programs** with golden-file regression testing and DOSBox-X +- **64 test programs** with golden-file regression testing and DOSBox-X compatibility testing against real GWBASIC.EXE - **MIT License** diff --git a/docs/language-reference.md b/docs/language-reference.md index 188557b..65a2b9a 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -59,7 +59,7 @@ type suffixes (`%`, `!`, `#`) | Screen | `LOCATE`, `COLOR`, `WIDTH`, `SCREEN`, `KEY ON`/`OFF`/`LIST`, `KEY n,"string"` | | Graphics | `PSET`, `PRESET`, `LINE`, `CIRCLE`, `DRAW`, `PAINT`, `GET`, `PUT` | | Sound | `SOUND`, `BEEP`, `PLAY` (MML parser, PulseAudio backend) | -| Memory | `DEF SEG`, `PEEK`, `POKE` | +| Memory | `DEF SEG`, `PEEK`, `POKE`, `BSAVE`, `BLOAD` | | Misc | `KEY`, `TRON`/`TROFF`, `OPTION BASE`, `MID$` assignment | | System | `SYSTEM` | @@ -183,12 +183,31 @@ DEF SEG ' reset to default segment | Segment | Address Range | Description | |---------|---------------|-------------| -| `0040` | BIOS data area | Video mode (`0049`), column count (`004A`), cursor position (`0050-0051`), timer ticks (`006C-006F`), screen rows (`0084`) | +| `0040` | BIOS data area | Video mode (`0049`), column count (`004A`), cursor position (`0050-0051`), timer ticks (`006C-006F`), screen rows (`0084`), keyboard shift flags (`0017`: bit 7 = insert mode) | | `B800` | CGA text buffer | Character/attribute pairs in text mode (even byte = char, odd byte = attr, 80 columns × 25 rows = 4000 bytes) | +| `B800` | CGA graphics buffer | In SCREEN 1/2: CGA interlaced layout (even rows at offset 0, odd rows at 0x2000, 80 bytes/row) | All other segments read as 0 and writes are silently discarded. The timer tick counter at `0040:006C` tracks real time at the original 18.2 Hz rate. +## BSAVE / BLOAD + +`BSAVE` saves a block of virtual memory to a binary file with a 7-byte header. +`BLOAD` loads it back. These operate on whichever segment was last set by +`DEF SEG`. + +``` +DEF SEG = &HB800 +BSAVE "screen.bin", 0, 4000 ' save the CGA text buffer +CLS +BLOAD "screen.bin" ' restore it +BLOAD "screen.bin", 100 ' load to a different offset +``` + +The file format is compatible with the original GW-BASIC: byte 0 = `0xFD`, +bytes 1-2 = segment (LE), bytes 3-4 = offset (LE), bytes 5-6 = length (LE), +followed by the raw data bytes. + ## Sound - `SOUND frequency, duration` — play a tone (frequency in Hz, duration in clock ticks) @@ -268,3 +287,16 @@ not fire inside their own handler (re-entrant protection). `TIMER STOP` / `KEY(n) STOP` queue events while stopped; switching to `TIMER ON` / `KEY(n) ON` fires the pending event immediately. + +## References + +- Microsoft Corporation. *Microsoft GW-BASIC: User's Guide and Reference*. + Microsoft Press, 1989. ISBN 978-1-55615-260-3. +- Inman, Don and Bob Albrecht. *The GW-BASIC Reference*. Osborne McGraw-Hill, + 1990. ISBN 978-0-07-881644-4. +- Ahl, David H. *BASIC Computer Games: Microcomputer Edition*. Workman, 1978. + ISBN 978-0-89480-052-8. +- Microsoft Corporation. *GW-BASIC User's Manual*. Microsoft, 1987. + (OEM bundled; no ISBN.) +- Microsoft Corporation. + [GW-BASIC Source Code](https://github.com/microsoft/GW-BASIC). Released 2020. diff --git a/docs/roadmap.md b/docs/roadmap.md index 9ac09de..27422d9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -15,15 +15,10 @@ bus so retro programs that talk to the speaker, joystick port, or parallel interface can do *something* useful instead of silently no-oping. Think of it as a tiny museum exhibit for vintage BASIC hardware hacking. -- **BSAVE / BLOAD** — binary file save/load for screen buffers and data -- **TUI color support** — map GW-BASIC COLOR attributes to ANSI 16-color output - in the TUI screen buffer - **VIEW / WINDOW / PALETTE** — graphics viewport, coordinate mapping, and palette remapping - **MBF format support for binary LOAD** — convert Microsoft Binary Format float constants when loading files saved by the original GWBASIC.EXE -- **Extended PEEK/POKE regions** — keyboard shift flags (0040:0017), CGA - graphics mode framebuffer mapping ## IDE and Notebook Integration @@ -41,8 +36,6 @@ ## Known Limitations -- Virtual memory emulation covers BIOS data area and CGA text buffer only; - CGA graphics mode PEEK/POKE not yet mapped - Binary files use IEEE 754 floats, not MBF — files saved here are not byte-compatible with original GWBASIC.EXE binary format - String garbage collection not implemented (uses `malloc`/`free` instead) diff --git a/include/graphics.h b/include/graphics.h index 10f12b3..5cd7a1b 100644 --- a/include/graphics.h +++ b/include/graphics.h @@ -50,4 +50,8 @@ void gfx_sprite_put(int x, int y, const int16_t *buf, int bufsize, int action); /* Calculate required array element count for a sprite rectangle. */ int gfx_sprite_size(int x1, int y1, int x2, int y2); +/* CGA framebuffer PEEK/POKE (segment 0xB800 in graphics mode) */ +uint8_t gfx_cga_peek(uint16_t offset); +void gfx_cga_poke(uint16_t offset, uint8_t val); + #endif diff --git a/include/gwbasic.h b/include/gwbasic.h index 04b5a29..b8bdef2 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -10,7 +10,7 @@ #include "gw_math.h" #include "strings.h" -#define GW_VERSION "0.11.0" +#define GW_VERSION "0.12.0" #define GW_BANNER "GW-BASIC " GW_VERSION /* Tokenizer */ diff --git a/src/graphics.c b/src/graphics.c index aa15563..29d25f6 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -462,6 +462,67 @@ void gfx_flush(void) free(buf); } +/* + * CGA framebuffer PEEK/POKE. + * + * CGA interlaced layout: + * Even rows: offset 0x0000, odd rows: offset 0x2000 + * Each row = 80 bytes + * SCREEN 1: 2 bpp (4 pixels/byte, MSB first) + * SCREEN 2: 1 bpp (8 pixels/byte, MSB first) + */ + +static void cga_offset_to_pixels(uint16_t offset, int *row, int *col_byte) +{ + int bank = (offset >= 0x2000) ? 1 : 0; + uint16_t linear = offset - bank * 0x2000; + *row = (linear / 80) * 2 + bank; + *col_byte = linear % 80; +} + +uint8_t gfx_cga_peek(uint16_t offset) +{ + if (!framebuf) return 0; + + int row, col_byte; + cga_offset_to_pixels(offset, &row, &col_byte); + if (row < 0 || row >= fb_height) return 0; + + int bpp = (screen_mode == 2) ? 1 : 2; + int ppb = 8 / bpp; + int base_x = col_byte * ppb; + uint8_t val = 0; + + for (int i = 0; i < ppb; i++) { + int x = base_x + i; + if (x >= fb_width) break; + int px = get_pixel(x, row) & ((1 << bpp) - 1); + val |= px << (8 - bpp - i * bpp); + } + return val; +} + +void gfx_cga_poke(uint16_t offset, uint8_t val) +{ + if (!framebuf) return; + + int row, col_byte; + cga_offset_to_pixels(offset, &row, &col_byte); + if (row < 0 || row >= fb_height) return; + + int bpp = (screen_mode == 2) ? 1 : 2; + int ppb = 8 / bpp; + int base_x = col_byte * ppb; + int mask = (1 << bpp) - 1; + + for (int i = 0; i < ppb; i++) { + int x = base_x + i; + if (x >= fb_width) break; + int px = (val >> (8 - bpp - i * bpp)) & mask; + set_pixel(x, row, px); + } +} + /* * GET/PUT sprite support. * diff --git a/src/interp.c b/src/interp.c index 40964d2..8af219a 100644 --- a/src/interp.c +++ b/src/interp.c @@ -2436,10 +2436,15 @@ void gw_exec_stmt(void) } if (gfx_active()) { gfx_set_color(fg); + } else if (tui.active) { + /* TUI mode: update current attribute for subsequent output */ + if (fg >= 0 && fg < 16) + tui.current_attr = (tui.current_attr & 0xF0) | (fg & 0x0F); + if (bg >= 0 && bg < 8) + tui.current_attr = (tui.current_attr & 0x0F) | ((bg & 0x07) << 4); } else if (gw_hal) { - /* Text mode: emit ANSI color codes */ + /* Piped/non-TUI mode: emit ANSI color codes directly */ char ansi[32]; - /* Map GW-BASIC colors to ANSI (simplified) */ static const int ansi_fg[] = {30,34,32,36,31,35,33,37,90,94,92,96,91,95,93,97}; static const int ansi_bg[] = {40,44,42,46,41,45,43,47}; if (fg >= 0 && fg < 16) { @@ -2615,6 +2620,80 @@ void gw_exec_stmt(void) return; } + /* BSAVE "filename", offset, length */ + if (tok == TOK_BSAVE) { + gw_chrget(); + gw_value_t fname = gw_eval_str(); + char *path = gw_str_to_cstr(&fname.sval); + gw_str_free(&fname.sval); + gw_skip_spaces(); + if (gw_chrgot() != ',') { free(path); gw_error(ERR_SN); } + gw_chrget(); + uint16_t offset = gw_eval_uint16(); + gw_skip_spaces(); + if (gw_chrgot() != ',') { free(path); gw_error(ERR_SN); } + gw_chrget(); + uint16_t length = gw_eval_uint16(); + + FILE *fp = fopen(path, "wb"); + free(path); + if (!fp) gw_error(ERR_FF); + + /* 7-byte BSAVE header: 0xFD, segment(2), offset(2), length(2) LE */ + fputc(0xFD, fp); + fputc(gw.def_seg & 0xFF, fp); + fputc((gw.def_seg >> 8) & 0xFF, fp); + fputc(offset & 0xFF, fp); + fputc((offset >> 8) & 0xFF, fp); + fputc(length & 0xFF, fp); + fputc((length >> 8) & 0xFF, fp); + + for (uint16_t i = 0; i < length; i++) + fputc(virmem_peek(gw.def_seg, offset + i), fp); + fclose(fp); + return; + } + + /* BLOAD "filename" [, offset] */ + if (tok == TOK_BLOAD) { + gw_chrget(); + gw_value_t fname = gw_eval_str(); + char *path = gw_str_to_cstr(&fname.sval); + gw_str_free(&fname.sval); + + int has_offset = 0; + uint16_t user_offset = 0; + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + user_offset = gw_eval_uint16(); + has_offset = 1; + } + + FILE *fp = fopen(path, "rb"); + free(path); + if (!fp) gw_error(ERR_FF); + + /* Read and validate 7-byte header */ + int marker = fgetc(fp); + if (marker != 0xFD) { fclose(fp); gw_error(ERR_BM); } + /* Skip segment (2 bytes) — we use gw.def_seg */ + fgetc(fp); fgetc(fp); + uint16_t file_offset = (uint8_t)fgetc(fp); + file_offset |= (uint8_t)fgetc(fp) << 8; + uint16_t length = (uint8_t)fgetc(fp); + length |= (uint8_t)fgetc(fp) << 8; + + uint16_t load_offset = has_offset ? user_offset : file_offset; + for (uint16_t i = 0; i < length; i++) { + int b = fgetc(fp); + if (b == EOF) break; + virmem_poke(gw.def_seg, load_offset + i, (uint8_t)b); + } + fclose(fp); + return; + } + /* WRITE */ if (tok == TOK_WRITE) { gw_chrget(); diff --git a/src/tui.c b/src/tui.c index 3d26daf..3c9fcca 100644 --- a/src/tui.c +++ b/src/tui.c @@ -128,20 +128,39 @@ void tui_locate(int row, int col) int tui_get_cursor_row(void) { return tui.cursor_row; } int tui_get_cursor_col(void) { return tui.cursor_col; } +/* CGA-to-ANSI color mapping */ +static const int ansi_fg[16] = {30,34,32,36,31,35,33,37,90,94,92,96,91,95,93,97}; +static const int ansi_bg[8] = {40,44,42,46,41,45,43,47}; + +static void emit_attr(uint8_t attr) +{ + int fg = attr & 0x0F; + int bg = (attr >> 4) & 0x07; + printf("\033[%d;%dm", ansi_fg[fg], ansi_bg[bg]); +} + void tui_refresh(void) { printf("\033[H"); int bottom = tui.key_bar_visible ? tui.rows : tui.view_bottom + 1; + uint8_t prev_attr = 0xFF; for (int r = 0; r < bottom; r++) { printf("\033[%d;1H", r + 1); for (int c = 0; c < tui.cols; c++) { + uint8_t attr = TUI_CELL(r, c).attr; + if (attr != prev_attr) { + emit_attr(attr); + prev_attr = attr; + } uint8_t ch = TUI_CELL(r, c).ch; putchar(ch ? ch : ' '); } } + printf("\033[0m"); + if (tui.key_bar_visible) tui_refresh_row(tui.rows - 1); @@ -152,10 +171,17 @@ void tui_refresh_row(int row) { if (row < 0 || row >= tui.rows) return; printf("\033[%d;1H", row + 1); + uint8_t prev_attr = 0xFF; for (int c = 0; c < tui.cols; c++) { + uint8_t attr = TUI_CELL(row, c).attr; + if (attr != prev_attr) { + emit_attr(attr); + prev_attr = attr; + } uint8_t ch = TUI_CELL(row, c).ch; putchar(ch ? ch : ' '); } + printf("\033[0m"); fflush(stdout); } diff --git a/src/virmem.c b/src/virmem.c index 53b4bf7..10b5d40 100644 --- a/src/virmem.c +++ b/src/virmem.c @@ -19,7 +19,8 @@ static uint8_t bios_peek(uint16_t offset) { switch (offset) { case 0x17: - return 0; /* no shift keys pressed */ + /* Bit 7 = insert mode; other modifier bits not detectable on POSIX */ + return tui.insert_mode ? 0x80 : 0; case 0x18: return 0; @@ -109,8 +110,11 @@ uint8_t virmem_peek(uint16_t segment, uint16_t offset) if (segment == 0x0040) return bios_peek(offset); - if (segment == 0xB800 && !gfx_active()) + if (segment == 0xB800) { + if (gfx_active()) + return gfx_cga_peek(offset); return cga_text_peek(offset); + } /* Unhandled regions return 0 */ return 0; @@ -123,8 +127,11 @@ void virmem_poke(uint16_t segment, uint16_t offset, uint8_t value) return; } - if (segment == 0xB800 && !gfx_active()) { - cga_text_poke(offset, value); + if (segment == 0xB800) { + if (gfx_active()) + gfx_cga_poke(offset, value); + else + cga_text_poke(offset, value); return; } diff --git a/tests/expected/bsave_bload.expected b/tests/expected/bsave_bload.expected new file mode 100644 index 0000000..230e219 --- /dev/null +++ b/tests/expected/bsave_bload.expected @@ -0,0 +1,5 @@ +BSAVE OK +BLOAD OK +BLOAD OFFSET OK +KILL OK +DONE diff --git a/tests/expected/peek_gfx.expected b/tests/expected/peek_gfx.expected new file mode 100644 index 0000000..fdfad04 --- /dev/null +++ b/tests/expected/peek_gfx.expected @@ -0,0 +1,4 @@ +POKE FF PEEK= 255 +ODD ROW PEEK= 170 +CLEARED= 0 +DONE diff --git a/tests/programs/bsave_bload.bas b/tests/programs/bsave_bload.bas new file mode 100644 index 0000000..747d67e --- /dev/null +++ b/tests/programs/bsave_bload.bas @@ -0,0 +1,18 @@ +10 REM Test BSAVE and BLOAD via virtual memory +20 REM In piped mode, B800 text buffer is inactive so values are 0 +30 REM This test verifies the BSAVE/BLOAD flow and file format +40 DEF SEG = &HB800 +50 REM BSAVE 4 bytes (will be zeros in piped mode) +60 BSAVE "BSTEST.BIN", 0, 4 +70 PRINT "BSAVE OK" +80 REM BLOAD back to same offset +90 BLOAD "BSTEST.BIN" +100 PRINT "BLOAD OK" +110 REM BLOAD with explicit offset +120 BLOAD "BSTEST.BIN", 100 +130 PRINT "BLOAD OFFSET OK" +140 REM Verify file was created and can be re-read +150 DEF SEG +160 KILL "BSTEST.BIN" +170 PRINT "KILL OK" +180 PRINT "DONE" diff --git a/tests/programs/color_test.bas b/tests/programs/color_test.bas new file mode 100644 index 0000000..575815d --- /dev/null +++ b/tests/programs/color_test.bas @@ -0,0 +1,8 @@ +10 REM Test COLOR statement +20 COLOR 14, 1 +30 PRINT "YELLOW ON BLUE" +40 COLOR 15, 4 +50 PRINT "WHITE ON RED" +60 COLOR 7, 0 +70 PRINT "DEFAULT COLORS" +80 PRINT "DONE" diff --git a/tests/programs/peek_gfx.bas b/tests/programs/peek_gfx.bas new file mode 100644 index 0000000..cb627b2 --- /dev/null +++ b/tests/programs/peek_gfx.bas @@ -0,0 +1,17 @@ +10 REM Test CGA graphics framebuffer PEEK/POKE +20 SCREEN 1 +30 DEF SEG = &HB800 +40 REM POKE a byte at offset 0 (even row 0, first 4 pixels) +50 POKE 0, &HFF +60 V = PEEK(0) +70 PRINT "POKE FF PEEK="; V +80 REM POKE at odd row bank (offset 0x2000) +90 POKE &H2000, &HAA +100 V = PEEK(&H2000) +110 PRINT "ODD ROW PEEK="; V +120 REM Clear and verify +130 POKE 0, 0 +140 PRINT "CLEARED="; PEEK(0) +150 SCREEN 0 +160 DEF SEG +170 PRINT "DONE"