diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a7105b..288fcc19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md). ### Build tools - Many toolchains are now WASI builds: cc65 (pinned to 2021 6ac4aa4), DASM 2.20.17, acme, zmac, yasm and merlin32. +- SDCC 4 mos6502 backend: C64 presets (`hello-sdcc.c`, `screen_ram-sdcc.c`, `joymove-sdcc.c`, `siegegame-sdcc.c`, `sprite_collision-sdcc.c`); `load_header` platform param wraps the image as a DOS 3.3 binary (apple2) or a PRG with a `SYS` stub (c64). - Updated batari BASIC; sdcc now parses `#symbol` directives and fixes lost link errors. - New build directives replace the legacy CFGFILE-style ones; build parameters no longer leak between builds. - `#pragma compile("...")` and oscar64 symbol/listing parsing. @@ -64,7 +65,9 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md). - cc65 `a2.lo.s` (Apple II lo-res TGI) patched in `8bitworkshop-compilers/patches/cc65-apple2-lo-tgi.patch`: a stray `bpl :+` in `INSTALL` made `tgi_install(a2_lo_tgi)` crash on a non-enhanced Apple II. - Presets adjusted for the newer cc65: NES `chase` closes its zero-page `#pragma` region, C64 `side_scroller` uses an unsigned fixed-point constant, and the Atari vector presets no longer recurse into `main()`. `neslib.h` no longer redefines `NULL` if `stddef.h` already did. - SDCC: both 3.6.5 and 4.x are bundled; 3.6.5 is the default (several times faster to build) and `//#tooldef c sdcc=4` opts in to 4.x (the default is `SDCC_DEFAULT_VERSION` in `toolmeta.ts`). Programs without an `opt_code*` pragma now build with `--no-peep --nolospre --max-allocs-per-node 500`, including Game Boy, which previously always got the full optimizer. See *SDCC versions* in the build directives docs. -- SDCC 4 mos6502 backend (experimental): a `-sdcc.c` file on `devel-6502` (see `presets/devel-6502/hello-sdcc.c`) builds with sdcc, `sdas6500` and `sdld6808`. Apple II and other 6502 platforms need their own crt0 and memory map. +- SDCC 4 mos6502 backend (experimental): a `.sdcc` or `-sdcc.c` file on `devel-6502` or `apple2` builds with sdcc, `sdas6500` and `sdld6808` (see `hello-sdcc.c` in those presets). Apple II programs link as a DOS 3.3 binary at `$803`. `.sdcc` now selects SDCC on every Z80 and 6502 platform. +- `//#tooldef ld code_start=ADDR` and `data_start=ADDR` move SDCC's code and RAM data. The Apple II preset `cosmic-sdcc.c` builds the Cosmic Impalas game with SDCC by including `cosmic.c`, which now has `__SDCC` alternatives for `conio` and inline assembly. +- `devel-6502` runs headless in the `8bws` CLI, and the new `serial` run-script command prints what the program sent to its serial port. - Build is now only `esbuild` through `scripts/build.mjs`; code split into a headless `src/common/projectcore.ts` / `toolselect.ts`. - New `buildpresets` script that builds and runs every preset. diff --git a/presets/apple2/cosmic-sdcc.c b/presets/apple2/cosmic-sdcc.c new file mode 100644 index 00000000..cad60a1a --- /dev/null +++ b/presets/apple2/cosmic-sdcc.c @@ -0,0 +1,7 @@ +// Cosmic Impalas built with SDCC's mos6502 backend instead of cc65. +// The game is cosmic.c; this file picks the compiler and memory layout. + +// the hires page 1 buffer is $2000-$3FFF, so the program loads above it +//#tooldef ld code_start=0x4000 + +#include "cosmic.c" diff --git a/presets/apple2/cosmic.c b/presets/apple2/cosmic.c index ce8ffe28..9e70827e 100644 --- a/presets/apple2/cosmic.c +++ b/presets/apple2/cosmic.c @@ -1,8 +1,10 @@ // reserve space for the HGR1 screen buffer //#tooldef ld cfgfile=apple2-hgr.cfg +#ifndef __SDCC #pragma data-name(push,"HGR") #pragma data-name(pop) +#endif /* * An Apple ][ port of the Cosmic Impalas game @@ -11,9 +13,11 @@ */ #include +#ifndef __SDCC #include #include #include +#endif // type aliases for byte/signed byte/unsigned 16-bit typedef unsigned char byte; @@ -21,7 +25,18 @@ typedef signed char sbyte; typedef unsigned short word; // peeks, pokes, and strobes +#ifdef __SDCC +// (building with sdcc, see cosmic-sdcc.c: no conio, so read the keyboard directly) +#define STROBE(addr) ((void)*(volatile byte*)(addr)) +byte kbhit(void) { return *(volatile byte*)0xc000 & 0x80; } +char cgetc(void) { + char key = *(volatile byte*)0xc000 & 0x7f; + STROBE(0xc010); // clear the keyboard strobe + return key; +} +#else #define STROBE(addr) __asm__ ("sta %w", addr) +#endif // speaker click #define CLICK STROBE(0xc030) @@ -232,11 +247,24 @@ void draw_bcd_word(word bcd, byte col, byte row, byte vert) { // add two 4-digit BCD words word bcd_add(word a, word b) { +#ifdef __SDCC + // add one digit at a time, in plain C + word result = 0; + byte shift, carry = 0; + for (shift = 0; shift < 16; shift += 4) { + byte d = ((a >> shift) & 0xf) + ((b >> shift) & 0xf) + carry; + carry = d > 9; + if (carry) d -= 10; + result |= (word)d << shift; + } + return result; +#else word result; __asm__ ("sed"); // set decimal (BCD) mode result = a+b; __asm__ ("cld"); // clear BCD mode return result; +#endif } // diff --git a/presets/apple2/hello-sdcc.c b/presets/apple2/hello-sdcc.c new file mode 100644 index 00000000..f4eee7a4 --- /dev/null +++ b/presets/apple2/hello-sdcc.c @@ -0,0 +1,31 @@ +// Hello World in C for the Apple ][ using SDCC's mos6502 backend. +// (The -sdcc.c suffix selects SDCC instead of cc65.) The program is linked +// as a DOS 3.3 binary at $803; there is no C library or conio, so we poke +// the text screen directly. + +#include + +#define TEXT_PAGE1 ((volatile uint8_t *)0x400) + +const char message[] = "HELLO, SDCC 6502!"; + +uint8_t row = 10; // initialized data is copied to RAM at startup + +// text rows are interleaved: three groups of 8 rows, 40 bytes apart, +// each row of a group $80 bytes after the previous +volatile uint8_t *row_address(uint8_t r) { + return TEXT_PAGE1 + (r & 7) * 0x80 + (r >> 3) * 40; +} + +void main(void) { + uint8_t i, r; + volatile uint8_t *p; + for (r = 0; r < 24; r++) { // clear the screen + p = row_address(r); + for (i = 0; i < 40; i++) p[i] = 0xa0; + } + p = row_address(row) + 11; + for (i = 0; message[i]; i++) + p[i] = message[i] | 0x80; // normal video: high bit set + while (1); +} diff --git a/presets/c64/c64-sdcc.h b/presets/c64/c64-sdcc.h new file mode 100644 index 00000000..837bc048 --- /dev/null +++ b/presets/c64/c64-sdcc.h @@ -0,0 +1,106 @@ +// Minimal C64 definitions for SDCC's mos6502 backend, which has no c64.h, +// conio.h or joystick.h like cc65 does. Included by common.h under __SDCC. + +#ifndef _C64_SDCC_H +#define _C64_SDCC_H + +#include +#include +#include + +#define PEEK(addr) (*(volatile uint8_t*)(addr)) +#define POKE(addr,val) (*(volatile uint8_t*)(addr) = (val)) + +#define COLOR_BLACK 0 +#define COLOR_WHITE 1 +#define COLOR_RED 2 +#define COLOR_CYAN 3 +#define COLOR_PURPLE 4 +#define COLOR_GREEN 5 +#define COLOR_BLUE 6 +#define COLOR_YELLOW 7 +#define COLOR_ORANGE 8 +#define COLOR_BROWN 9 +#define COLOR_LIGHTRED 10 +#define COLOR_GRAY1 11 +#define COLOR_GRAY2 12 +#define COLOR_LIGHTGREEN 13 +#define COLOR_LIGHTBLUE 14 +#define COLOR_GRAY3 15 + +// VIC-II registers, same field names as cc65's struct __vic2 +struct __vic2 { + struct { uint8_t x, y; } spr_pos[8]; + uint8_t spr_hi_x, ctrl1, rasterline, strobe_x, strobe_y; + uint8_t spr_ena, ctrl2, spr_exp_y, addr, irr, imr; + uint8_t spr_bg_prio, spr_mcolor, spr_exp_x, spr_coll, spr_bg_coll; + uint8_t bordercolor, bgcolor0, bgcolor1, bgcolor2, bgcolor3; + uint8_t spr_mcolor0, spr_mcolor1; + uint8_t spr_color[8]; +}; + +// 6526 CIA registers +struct __6526 { + uint8_t pra, prb, ddra, ddrb, ta_lo, ta_hi, tb_lo, tb_hi; + uint8_t tod_10, tod_sec, tod_min, tod_hour, sdr, icr, cra, crb; +}; + +#define VIC (*(volatile struct __vic2*)0xd000) +#define CIA1 (*(volatile struct __6526*)0xdc00) +#define CIA2 (*(volatile struct __6526*)0xdd00) +#define COLOR_RAM ((volatile uint8_t*)0xd800) + +// joystick: bits are active low on CIA1 (port 2 = pra, port 1 = prb) +#define JOY_UP_MASK 0x01 +#define JOY_DOWN_MASK 0x02 +#define JOY_LEFT_MASK 0x04 +#define JOY_RIGHT_MASK 0x08 +#define JOY_BTN_1_MASK 0x10 +#define JOY_UP(v) ((v) & JOY_UP_MASK) +#define JOY_DOWN(v) ((v) & JOY_DOWN_MASK) +#define JOY_LEFT(v) ((v) & JOY_LEFT_MASK) +#define JOY_RIGHT(v) ((v) & JOY_RIGHT_MASK) +#define JOY_BTN_1(v) ((v) & JOY_BTN_1_MASK) +#define joy_install(drv) +#define joy_static_stddrv 0 +// (the KERNAL's keyboard scan also drives these ports, so stop it while reading) +static uint8_t joy_read(uint8_t port) { + uint8_t v; + __asm__("sei"); + if (port) { CIA1.ddra = 0; v = CIA1.pra; CIA1.ddra = 0xff; } // joystick 2 + else { CIA1.ddrb = 0; v = CIA1.prb; } // joystick 1 + __asm__("cli"); + return ~v & 0x1f; +} + +// conio-like text output straight to screen RAM (screen codes, not PETSCII) +static uint8_t _cx, _cy, _ccolor = COLOR_LIGHTBLUE, _crev; + +static void clrscr(void) { + uint16_t i; + for (i = 0; i < 1000; i++) POKE(0x400 + i, 32); + _cx = _cy = 0; +} + +static uint8_t _screencode(char c) { + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) return c & 0x1f; + if (c == '#') return 0xa3; // the uppercase character set has no plain '#' glyph; use reverse video + return c; // digits, punctuation and space match ASCII +} + +static void textcolor(uint8_t c) { _ccolor = c; } +static uint8_t revers(uint8_t on) { uint8_t old = _crev; _crev = on ? 0x80 : 0; return old; } + +static void cputcxy(uint8_t x, uint8_t y, char c) { + uint16_t ofs = y * 40 + x; + POKE(0x400 + ofs, _screencode(c) | _crev); + POKE(0xd800 + ofs, _ccolor); +} + +static void cputsxy(uint8_t x, uint8_t y, const char* s) { + while (*s) cputcxy(x++, y, *s++); +} + +static uint8_t kbhit(void) { return PEEK(0xc6); } // KERNAL keyboard buffer count + +#endif diff --git a/presets/c64/common.h b/presets/c64/common.h index 43b876a3..cbeaf115 100644 --- a/presets/c64/common.h +++ b/presets/c64/common.h @@ -1,12 +1,17 @@ #ifndef _COMMON_H #define _COMMON_H +#ifdef __SDCC +// SDCC has no cc65 headers, so use our own subset (see c64-sdcc.h) +#include "c64-sdcc.h" +#else #include #include #include #include #include #include +#endif #ifdef __CC65__ #include @@ -16,7 +21,13 @@ typedef uint8_t byte; // 8-bit unsigned typedef int8_t sbyte; // 8-bit signed typedef uint16_t word; // 16-bit unsigned +#ifdef __SDCC +typedef uint8_t bool; +#define false 0 +#define true 1 +#else typedef enum { false, true } bool; // boolean +#endif #define COLS 40 // total # of columns #define ROWS 25 // total # of rows @@ -127,7 +138,7 @@ char* get_screen_memory(); char __fastcall__ poll_keyboard(); #endif -#ifndef __CC65__ +#if !defined(__CC65__) && !defined(__SDCC) inline void clrscr() { __asm__ volatile ("jsr $E544" : : : "a","x","y"); // regs clobbered } @@ -135,6 +146,9 @@ inline void waitvsync() { raster_wait(255); } #endif +#ifdef __SDCC +static void waitvsync(void) { raster_wait(255); } +#endif // for use with set_irq() // sets up the VIC to send raster interrupts diff --git a/presets/c64/hello-sdcc.c b/presets/c64/hello-sdcc.c new file mode 100644 index 00000000..d49133a3 --- /dev/null +++ b/presets/c64/hello-sdcc.c @@ -0,0 +1,32 @@ +// Hello World in C for the Commodore 64 using SDCC's mos6502 backend. +// (The -sdcc.c suffix selects SDCC instead of cc65.) The program is a PRG +// with a BASIC "SYS 2061" stub; there is no C library or conio, so we poke +// screen RAM and color RAM directly. + +#include + +#define SCREEN ((volatile uint8_t *)0x400) +#define COLOR_RAM ((volatile uint8_t *)0xd800) +#define BORDER (*(volatile uint8_t *)0xd020) +#define BACKGROUND (*(volatile uint8_t *)0xd021) + +// screen codes (not ASCII): letters are 1-26, space is 32 +const uint8_t message[] = { 8,5,12,12,15, 32, 19,4,3,3, 32, 54,53,48,50 }; + +uint8_t color = 7; // initialized data is copied to RAM at startup + +void main(void) { + uint16_t i; + uint8_t c; + BORDER = 6; + BACKGROUND = 0; + for (i = 0; i < 1000; i++) { // clear the screen + SCREEN[i] = 32; + COLOR_RAM[i] = 1; + } + for (c = 0; c < sizeof(message); c++) { + SCREEN[12*40 + 12 + c] = message[c]; + COLOR_RAM[12*40 + 12 + c] = color; + } + while (1); +} diff --git a/presets/c64/joymove.c b/presets/c64/joymove.c index 743bee66..38e4c4f3 100644 --- a/presets/c64/joymove.c +++ b/presets/c64/joymove.c @@ -20,6 +20,9 @@ void main(void) { byte bgcoll; // sprite background collision flags byte joy; // joystick flags +#ifdef __SDCC + clrscr(); // (cc65's startup code clears the screen, SDCC's does not) +#endif // copy sprite pattern to RAM address 0x3800 memcpy((char*)0x3800, SPRITE_DATA, sizeof(SPRITE_DATA)); // set sprite #0 shape entry (224) diff --git a/presets/c64/screen_ram.c b/presets/c64/screen_ram.c index 1b3aa4aa..fce1403d 100644 --- a/presets/c64/screen_ram.c +++ b/presets/c64/screen_ram.c @@ -2,7 +2,9 @@ #include "common.h" //#link "common.c" +#ifndef __SDCC #include +#endif void main(void) { unsigned int i; diff --git a/presets/c64/sprite_collision-sdcc.c b/presets/c64/sprite_collision-sdcc.c new file mode 100644 index 00000000..f9a9e3d9 --- /dev/null +++ b/presets/c64/sprite_collision-sdcc.c @@ -0,0 +1,11 @@ +// sprite_collision.c built with SDCC's mos6502 backend instead of cc65. +// The program is sprite_collision.c; common.h supplies the C64 definitions SDCC lacks. +// (common.c and sprites.c are #included, since cc65 and SDCC objects can't be linked together.) + +#include "common.h" +#include "common.c" + +#include "sprites.h" +#include "sprites.c" + +#include "sprite_collision.c" diff --git a/presets/devel-6502/hello.dasm b/presets/devel-6502/hello.dasm new file mode 100644 index 00000000..5a85898d --- /dev/null +++ b/presets/devel-6502/hello.dasm @@ -0,0 +1,22 @@ + processor 6502 + org $8000 + +Start +; ADD YOUR CODE HERE + rts + + org $ffe0 +NMIVec +RESVec +BRKVec + sei + cld + ldx #$ff + txs + jsr Start + jmp . + + org $fffa + word NMIVec ; NMI + word RESVec ; reset + word BRKVec ; BRK diff --git a/src/common/baseplatform.ts b/src/common/baseplatform.ts index 8d0a2f17..e93d7687 100644 --- a/src/common/baseplatform.ts +++ b/src/common/baseplatform.ts @@ -523,7 +523,7 @@ export abstract class Base6502Platform extends BaseDebugPlatform { return disassemble6502(pc, read(pc), read(pc + 1), read(pc + 2)); } getToolForFilename = getToolForFilename_6502; - getDefaultExtensions() { return [".c", ".cpp", ".o64", ".acme", ".xa", ".ca65", ".dasm", ".ecs", ".wiz"]; }; + getDefaultExtensions() { return [".c", ".sdcc", ".cpp", ".o64", ".acme", ".xa", ".ca65", ".dasm", ".ecs", ".wiz"]; }; getDebugCategories() { return ['CPU', 'ZPRAM', 'Stack']; @@ -643,7 +643,7 @@ export abstract class BaseZ80Platform extends BaseDebugPlatform { } getToolForFilename = getToolForFilename_z80; - getDefaultExtensions() { return [".c", ".ns", ".s", ".scc", ".sgb", ".z", ".wiz"]; }; + getDefaultExtensions() { return [".c", ".sdcc", ".ns", ".s", ".scc", ".sgb", ".z", ".wiz"]; }; // TODO: Z80 opcode metadata //this.getOpcodeMetadata = function() { } diff --git a/src/common/emucore.ts b/src/common/emucore.ts index 9b3411ec..040e677e 100644 --- a/src/common/emucore.ts +++ b/src/common/emucore.ts @@ -421,6 +421,12 @@ export class EmuCore { get frameRate(): number { return this.video ? this.video.frameRate : 60; } saveState(): EmuState | null { return this.platform.saveState ? this.platform.saveState() : null; } + /** Text a platform's program has sent to a serial port, or null if it has none. */ + getSerialOutput(): string | null { + const p: any = this.platform; + return typeof p.getSerialOutput === 'function' ? p.getSerialOutput() : null; + } + getDebugInfo(): DebugSection[] { const state = this.saveState(); const p: any = this.platform; diff --git a/src/common/toolmeta.ts b/src/common/toolmeta.ts index 05e566f3..aae60ab3 100644 --- a/src/common/toolmeta.ts +++ b/src/common/toolmeta.ts @@ -410,7 +410,7 @@ export const TOOL_META: { [id: string]: ToolMeta } = { sdcc: { id: 'sdcc', name: 'SDCC', kind: 'compiler', arch: 'z80', - extensions: ['.c', '.h'], + extensions: ['.c', '.h', '.sdcc'], includeDirs: ['/share/sdcc/include', '/include'], editorStyle: 'text/x-csrc', helpURL: 'http://sdcc.sourceforge.net/doc/sdccman.pdf', diff --git a/src/common/toolselect.ts b/src/common/toolselect.ts index 74252ed2..4b08d40e 100644 --- a/src/common/toolselect.ts +++ b/src/common/toolselect.ts @@ -12,7 +12,7 @@ export type ToolSelector = (fn: string) => string; export function getToolForFilename_6502(fn: string): string { if (fn.endsWith("-llvm.c")) return "remote:llvm-mos"; - if (fn.endsWith("-sdcc.c")) return "sdcc"; // SDCC 4.x mos6502 backend + if (fn.endsWith("-sdcc.c") || fn.endsWith(".sdcc")) return "sdcc"; // SDCC 4.x mos6502 backend if (fn.endsWith(".c")) return "cc65"; if (fn.endsWith(".h")) return "cc65"; if (fn.endsWith(".s")) return "ca65"; @@ -30,7 +30,7 @@ export function getToolForFilename_6502(fn: string): string { } export function getToolForFilename_z80(fn: string): string { - if (fn.endsWith(".c")) return "sdcc"; + if (fn.endsWith(".c") || fn.endsWith(".sdcc")) return "sdcc"; if (fn.endsWith(".h")) return "sdcc"; if (fn.endsWith(".s")) return "sdasz80"; if (fn.endsWith(".sgb")) return "sdasgb"; @@ -61,7 +61,6 @@ export function getToolForFilename_arm32(fn: string): string { export function getToolForFilename_vcs(fn: string): string { if (fn.endsWith(".cc2600")) return "cc2600"; if (fn.endsWith("-llvm.c")) return "remote:llvm-mos"; - if (fn.endsWith("-sdcc.c")) return "sdcc"; // SDCC 4.x mos6502 backend if (fn.endsWith(".wiz")) return "wiz"; if (fn.endsWith(".bb") || fn.endsWith(".bas")) return "bataribasic"; if (fn.endsWith(".ca65")) return "ca65"; diff --git a/src/docs/build-directives.md b/src/docs/build-directives.md index 7b20b552..a5cdea07 100644 --- a/src/docs/build-directives.md +++ b/src/docs/build-directives.md @@ -135,11 +135,13 @@ The supported parameters are: | --- | --- | --- | | `ld` | `cfgfile` | linker configuration file to use (e.g. for CC65's ld65) | | `ld` | `libargs` | comma-separated list of library/symbol arguments | +| `ld` | `code_start`, `data_start` | where SDCC's linker places the code and the RAM data (e.g. `0x4000`) | | `c` | `sdcc` | SDCC version, `3` or `4` (see *SDCC versions* below) | ~~~c //#tooldef ld cfgfile=apple2-hgr2.cfg //#tooldef ld libargs=,nes.lib +//#tooldef ld code_start=0x4000 //#tooldef c sdcc=3 ~~~ diff --git a/src/platform/apple2.ts b/src/platform/apple2.ts index 89922372..264e0b8a 100644 --- a/src/platform/apple2.ts +++ b/src/platform/apple2.ts @@ -28,6 +28,8 @@ const APPLE2_PRESETS: Preset[] = [ // {id:'tb_6502.s', name:'Tom Bombem (assembler game)'}, { id: 'dos33bin.a', name: "DOS 3.3 Binary" }, { id: 'applesinglebin.a', name: "AppleSingle Binary" }, + { id: 'hello-sdcc.c', name: 'Hello World (SDCC)', category: "C (SDCC)" }, + { id: 'cosmic-sdcc.c', name: 'Cosmic Impalas (SDCC)' }, ]; /// MAME support @@ -51,7 +53,7 @@ class Apple2MAMEPlatform extends BaseMAME6502Platform implements Platform { } getOpcodeMetadata = getOpcodeMetadata_6502; - getDefaultExtensions() { return [".c", ".cpp", ".acme", ".xa", ".ca65", ".dasm", ".ecs", ".wiz"]; } + getDefaultExtensions() { return [".c", ".sdcc", ".cpp", ".acme", ".xa", ".ca65", ".dasm", ".ecs", ".wiz"]; } getToolForFilename = getToolForFilename_apple2; getPresets() { return APPLE2_PRESETS; } diff --git a/src/platform/c64.ts b/src/platform/c64.ts index aefc7adb..26a3d221 100644 --- a/src/platform/c64.ts +++ b/src/platform/c64.ts @@ -46,6 +46,8 @@ const C64_PRESETS : Preset[] = [ {id:'hello.cpp', name:'Hello Oscar64', category:'C++ (Oscar64)'}, {id:'particles.cpp', name:'Fireworks'}, {id:'lander.cpp', name:'Lunar Lander'}, + {id:'hello-sdcc.c', name:'Hello World (SDCC)', category:'C (SDCC)'}, + {id:'sprite_collision-sdcc.c', name:'Sprite Collision (SDCC)'}, ]; const C64_MEMORY_MAP = { main:[ diff --git a/src/platform/devel.ts b/src/platform/devel.ts index be27e5f4..c2117d86 100644 --- a/src/platform/devel.ts +++ b/src/platform/devel.ts @@ -12,7 +12,26 @@ var DEVEL_6502_PRESETS = [ {id:'hello-sdcc.c', name:'Hello World (SDCC C)'}, ]; -class SerialInOutViewer { +/** What the serial harness draws on: the TeleType viewer, or nothing (headless). */ +interface SerialViewer { + tty : { addtext(s: string, flags: number): void, newline(): void, clear(): void, + saveState(): any, loadState(state: any): void }; + start(): void; + reset(): void; + saveState(): any; + loadState(state: any): void; +} + +/** For the CLI and tests, which have no DOM: the output is kept by the harness. */ +class HeadlessSerialViewer implements SerialViewer { + tty = { addtext() {}, newline() {}, clear() {}, saveState() { return {}; }, loadState() {} }; + start() {} + reset() {} + saveState() { return {}; } + loadState() {} +} + +class SerialInOutViewer implements SerialViewer { div : HTMLElement; tty : TeleType; @@ -48,7 +67,7 @@ function byteToASCII(b: number) : string { export class SerialTestHarness implements SerialIOInterface { - viewer : SerialInOutViewer; + viewer : SerialViewer; bufferedRead : boolean = true; cyclesPerByte = 1000000/(57600/8); // 138.88888 cycles maxOutputBytes = 4096; @@ -117,11 +136,12 @@ export class SerialTestHarness implements SerialIOInterface { class Devel6502Platform extends Base6502MachinePlatform implements Platform { serial : SerialTestHarness; - serview : SerialInOutViewer; + serview : SerialViewer; constructor(mainElement: HTMLElement) { super(mainElement); - this.serview = new SerialInOutViewer(mainElement); + // no element: running headless (the 8bws CLI) + this.serview = mainElement ? new SerialInOutViewer(mainElement) : new HeadlessSerialViewer(); } async start() { @@ -165,6 +185,9 @@ class Devel6502Platform extends Base6502MachinePlatform implements Pl // TODO: reload tty UI } + /** Everything the program has sent to the serial port (for the CLI's `serial` command). */ + getSerialOutput() { return byteArrayToString(this.serial.outputBytes); } + newMachine() { return new Devel6502(); } getPresets() { return DEVEL_6502_PRESETS; } readAddress(a) { return this.machine.readConst(a); } diff --git a/src/tools/buildpresets.ts b/src/tools/buildpresets.ts index c2fb9953..af3b5ec1 100644 --- a/src/tools/buildpresets.ts +++ b/src/tools/buildpresets.ts @@ -103,6 +103,8 @@ const SKIP_PLATFORMS = [ 'astrocade-arcade', 'mcr', 'vectrex', + 'devel', + 'devel-6502', ]; // build these, but don't run them: the emulator needs a real browser diff --git a/src/tools/runscript.ts b/src/tools/runscript.ts index 13efb0d0..ea8e1057 100644 --- a/src/tools/runscript.ts +++ b/src/tools/runscript.ts @@ -43,6 +43,7 @@ export const RUN_SCRIPT_HELP = [ ' keydown KEY / keyup KEY - raw key down/up events', ' mem START [LEN] - hexdump memory (default 16 bytes)', ' screen [START] [COLS] [ROWS]- decode screen RAM to text (default $0400 40x25)', + ' serial - print what the program sent to its serial port (devel-6502)', ' pc [N] - print PC + disassembly of N instructions', ' info - platform/machine debug info', ' reset - reset the emulator', @@ -485,6 +486,13 @@ export class RunScript { } } + cmdSerial() { + const text = this.target.getSerialOutput(); + if (text == null) throw new Error(`'${this.target.id}' has no serial output`); + this.log(`serial output (${text.length} bytes):`); + this.out(text.endsWith('\n') ? text : text + '\n'); + } + cmdPC(tokens: string[]) { this.target.settle(); const pc = this.target.getPC(); @@ -534,6 +542,7 @@ const COMMANDS: { [name: string]: Command } = { 'keyup': RunScript.prototype.cmdKeyUp, 'mem': RunScript.prototype.cmdMem, 'screen': RunScript.prototype.cmdScreen, + 'serial': RunScript.prototype.cmdSerial, 'pc': RunScript.prototype.cmdPC, 'info': RunScript.prototype.cmdInfo, 'reset': RunScript.prototype.cmdReset, diff --git a/src/worker/builder.ts b/src/worker/builder.ts index df9a86e0..9c7b608e 100644 --- a/src/worker/builder.ts +++ b/src/worker/builder.ts @@ -466,8 +466,8 @@ export function applyAsmProjectParams(params) { * * //#symbol [] NAME[=VALUE] phase in c|as|ld (default c) * //#flag raw argv for one phase - * //#tooldef NAME=VALUE typed knob (linker: cfgfile, libargs; - * compiler: sdcc=3|4) + * //#tooldef NAME=VALUE typed knob (linker: cfgfile, libargs, + * code_start, data_start; compiler: sdcc=3|4) * * Symbols are phase-scoped because they are different things: a preprocessor * macro (`c`) takes text, while an assembler/linker global (`as`/`ld`) is an @@ -733,6 +733,11 @@ export function applyBuildDirectives(dir: SourceDirectives, params) { params.cfgfile = td.value; } else if (td.phase === 'linker' && td.name === 'libargs') { params.libargs = td.value.split(',').filter((s) => s !== ''); + } else if (td.phase === 'linker' && (td.name === 'code_start' || td.name === 'data_start')) { + // where sdld puts the code (and the DOS 3.3 header's load address) and the RAM data + const addr = Number(td.value); + if (Number.isInteger(addr) && addr >= 0 && addr < 0x10000) params[td.name] = addr; + else dir.errors.push(`#tooldef ld ${td.name}: expected an address, got '${td.value}'`); } else if (td.phase === 'compiler' && td.name === 'sdcc') { // SDCC major version; every sdcc/sdas/sdld step of the build reads it if (td.value === '3' || td.value === '4') params.sdcc_version = parseInt(td.value); diff --git a/src/worker/platforms.ts b/src/worker/platforms.ts index 887188b4..d7eaee47 100644 --- a/src/worker/platforms.ts +++ b/src/worker/platforms.ts @@ -221,6 +221,11 @@ export var PLATFORM_PARAMS = { libargs: [ '--lib-path', '/share/target/apple2/drv', 'apple2.lib'], __CODE_RUN__: 16384, code_start: 0x803, + // for SDCC (-sdcc.c): a DOS 3.3 binary at $803 with data above the program + rom_size: 0x9400 - 0x803, + data_start: 0x9400, + zp_start: 0x60, + load_header: 'dos33', acmeargs: ['-f', 'apple'], }, 'apple2-e': { @@ -301,6 +306,12 @@ export var PLATFORM_PARAMS = { cfgfile: 'c64.cfg', // SYS 2061 libargs: ['openroms-compat.o', 'c64.lib'], extra_link_files: ['openroms-compat.o'], + // for SDCC (-sdcc.c): a PRG with a BASIC SYS stub, code at $80D, data in upper RAM + code_start: 0x80d, + rom_size: 0xa000 - 0x80d, + data_start: 0xc000, + zp_start: 0x2, + load_header: 'prg', acmeargs: ['-f', 'cbm'], //extra_link_files: ['c64-cart.cfg'], }, diff --git a/src/worker/tools/sdcc.ts b/src/worker/tools/sdcc.ts index 6fb124ba..6613438a 100644 --- a/src/worker/tools/sdcc.ts +++ b/src/worker/tools/sdcc.ts @@ -209,6 +209,53 @@ export function parseIHX(ihx: string, rom_start: number, rom_size: number, error return output; } +/** End offset, within [rom_start, rom_start + rom_size), of the highest data record. */ +export function ihxExtent(ihx: string, rom_start: number, rom_size: number): number { + let end = 0, upper = 0; + for (const s of ihx.split("\n")) { + if (s[0] != ':') continue; + const arr = hexToArray(s, 1); + if (arr[3] == 0) { + const offset = upper + (arr[1] << 8) + arr[2] - rom_start; + if (offset >= 0 && offset + arr[0] <= rom_size) end = Math.max(end, offset + arr[0]); + } else if (arr[3] == 4) { + upper = ((arr[4] << 8) | arr[5]) << 16; + } + } + return end; +} + +/** + * The `load_header` platform param: how a loadable file wraps the linked + * image (trimmed to the program's end, `length` bytes, linked at `start`). + * dos33 Apple II binary: load address and length (2 bytes each, little + * endian); the loader checks length == file size - 4 + * prg Commodore PRG: load address $0801, then the BASIC line + * `10 SYS ` that runs the program, which must be linked at + * $080D (right after that stub) + */ +export function loadHeader(kind: string, image: Uint8Array, start: number, length: number): Uint8Array { + var header: number[]; + switch (kind) { + case 'dos33': + header = [start & 0xff, start >> 8, length & 0xff, length >> 8]; + break; + case 'prg': { + const sys = [...String(start)].map((c) => c.charCodeAt(0)); + const end = 0x801 + 4 + 1 + sys.length + 1; // link, line number, SYS token, digits, NUL + header = [0x01, 0x08, end & 0xff, end >> 8, 0x0a, 0x00, 0x9e, ...sys, 0, 0, 0]; + if (0x801 + header.length - 2 !== start) throw new Error(`load_header prg: code must start at $${(0x801 + header.length - 2).toString(16)}, not $${start.toString(16)}`); + break; + } + default: + throw new Error(`unknown load_header '${kind}'`); + } + const out = new Uint8Array(new ArrayBuffer(header.length + length)); + out.set(header); + out.set(image.subarray(0, length), header.length); + return out; +} + function errorMatcherSDASZ80(path: string, errors: WorkerError[]) { //?ASxxxx-Error- in line 1 of main.asm null // .org in REL area or directive / mnemonic error @@ -432,6 +479,10 @@ async function linkSDLD(step: BuildStep, ld: 'sdldz80' | 'sdld6808') { if (errors.length) { return { errors: errors }; } + if (params.load_header) { + const start = params.rom_start !== undefined ? params.rom_start : params.code_start; + binout = loadHeader(params.load_header, binout, start, ihxExtent(hexout, start, params.rom_size)); + } // parse listings var listings: CodeListingMap = {}; for (var fn of step.files) { @@ -499,13 +550,14 @@ async function linkSDLD(step: BuildStep, ld: 'sdldz80' | 'sdld6808') { * source comments, source lines. `srcprefix` is the source file name without * its extension: SDCC 3.6.5 writes `;:N:` comments, SDCC 4.x writes * `;file.c:N: ` (with mcpp's `//` path prefix), and 4.x addresses - * have 8 hex digits instead of 4. + * have 8 hex digits instead of 4. The mos6502 backend writes `;\tfile.c: N: `. */ export function parseRSTListing(rstout: string, srcprefix: string) { // 0000 21 02 00 [10] 52 ld hl, #2 var asmlines = parseListing(rstout, /^\s*([0-9A-F]{4,8})\s+([0-9A-F][0-9A-F r]*[0-9A-F])\s+\[([0-9 ]+)\]?\s+(\d+) (.*)/i, 4, 1, 2, 3); const name = srcprefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const srcre = new RegExp(`^\\s+\\d+ ;(?:|/*${name}\\.[^:\\s]+):(\\d+):`, 'i'); + // the mos6502 backend writes `;file.c: 17: source`, with a space after the colon + const srcre = new RegExp(`^\\s+\\d+ ;\\s*(?:|/*${name}\\.[^:\\s]+):\\s*(\\d+):`, 'i'); var srclines = parseSourceLines(rstout, srcre, /^\s*([0-9A-F]{4,8})/i); // TODO: you have to get rid of all source lines to get asm listing return { @@ -597,9 +649,9 @@ export async function compileSDCC(step: BuildStep): Promise { // if "#pragma opt_code" found do not disable optimziations if (!/^\s*#pragma\s+opt_code/m.exec(code)) { args.push.apply(args, [ - '--max-allocs-per-node', '500', '--no-peep', - '--nolospre' + '--nolospre', + '--max-allocs-per-node', '500', ]); } if (params.extra_compile_args) { diff --git a/test/cli/testcli.js b/test/cli/testcli.js index 72264081..dad9af08 100644 --- a/test/cli/testcli.js +++ b/test/cli/testcli.js @@ -124,6 +124,30 @@ describe('8bws CLI', function () { assert.ok(r.success, r.error); assert.equal(r.data.frames, 10); }); + it('should build cosmic.c with SDCC from a header-only wrapper', function () { + var out = path.join(os.tmpdir(), '8bws-cosmic-sdcc.bin'); + cli('build', '-p', 'apple2', 'presets/apple2/cosmic-sdcc.c', '-o', out); + var bin = fs.readFileSync(out); + assert.equal(bin[0] | bin[1] << 8, 0x4000); // //#tooldef ld code_start=0x4000 + assert.equal(bin[2] | bin[3] << 8, bin.length - 4); + assert.ok(bin.length > 4000); // the whole game, not just the wrapper + }); + ['hello', 'screen_ram', 'joymove', 'siegegame', 'sprite_collision'].forEach(function (name) { + it('should build the c64 ' + name + ' SDCC preset as a PRG', function () { + var out = path.join(os.tmpdir(), '8bws-c64-' + name + '-sdcc.prg'); + cli('build', '-p', 'c64', 'presets/c64/' + name + '-sdcc.c', '-o', out); + var bin = fs.readFileSync(out); + assert.equal(bin[0] | bin[1] << 8, 0x801); // load address, then 10 SYS 2061 + assert.equal(bin[6], 0x9e); + }); + }); + it('should print the serial output of a devel-6502 program', function () { + var out = cli('run', '-p', 'devel-6502', 'presets/devel-6502/hello-sdcc.c', '-e', 'run 5; serial'); + assert.ok(out.includes('Hello, SDCC 6502!\nagain\nagain\nagain')); + }); + it('should reject serial on a platform without a serial port', function () { + cliFails('run', '-p', 'nes', 'presets/nes/hello.c', '-e', 'run 1; serial'); + }); it('should build and run a directory', function () { var dir = fs.mkdtempSync(path.join(os.tmpdir(), '8bws-dir-')); fs.copyFileSync('presets/nes/hello.c', path.join(dir, 'hello.c')); diff --git a/test/cli/testworker.js b/test/cli/testworker.js index 5351ef04..313867ab 100644 --- a/test/cli/testworker.js +++ b/test/cli/testworker.js @@ -245,6 +245,25 @@ describe('Worker', function() { done(); }, 32768, 0, 0); }); + it('should link SDCC mos6502 as a DOS 3.3 binary on apple2', function(done) { + var csource = ab2str(fs.readFileSync('presets/apple2/hello-sdcc.c')); + compile('sdcc', csource, 'apple2', function(err, msg) { + var bin = msg.output; + assert.equal(bin[0] | bin[1] << 8, 0x803); // load address + assert.equal(bin[2] | bin[3] << 8, bin.length - 4); // length: what the loader checks + assert.ok(bin.length < 1024); // trimmed to the program + done(); + }, 377, 0, 0); + }); + it('should link SDCC mos6502 as a PRG with a BASIC stub on c64', function(done) { + var csource = ab2str(fs.readFileSync('presets/c64/hello-sdcc.c')); + compile('sdcc', csource, 'c64', function(err, msg) { + var bin = msg.output; + assert.deepEqual(Array.from(bin.slice(0, 12)), [1,8, 0x0b,8, 10,0, 0x9e, 0x32,0x30,0x36,0x31, 0]); // 10 SYS 2061 + assert.ok(bin.length < 1024); // trimmed to the program + done(); + }, 332, 0, 0); + }); it('should compile oscar64 and return listings/symbols/segments', async function() { var msgs = [{code:'#include \nint main() { printf("FOO"); return 0; }', platform:'c64', tool:'oscar64', path:'main.c', mainfile:true}]; var result = await new Promise(function(resolve, reject) { diff --git a/test/unit/testbuildparams.ts b/test/unit/testbuildparams.ts index ba879cbf..1f07be57 100644 --- a/test/unit/testbuildparams.ts +++ b/test/unit/testbuildparams.ts @@ -257,6 +257,16 @@ describe("explicit build directives (//#symbol, //#flag, //#tooldef)", function assert.strictEqual(params.sdcc_version, 3); }); + it("#tooldef ld code_start and data_start take addresses", function () { + const params: any = {}; + applyDirectives("//#tooldef ld code_start=0x4000\n//#tooldef ld data_start=36864\n", params); + assert.strictEqual(params.code_start, 0x4000); + assert.strictEqual(params.data_start, 0x9000); + const dir = applyDirectives("//#tooldef ld code_start=nowhere\n//#tooldef ld data_start=0x10000\n", params); + assert.strictEqual(dir.errors.length, 2); + assert.strictEqual(params.code_start, 0x4000); + }); + it("#tooldef rejects unknown knobs", function () { const params = c64Params(); const dir = applyDirectives("//#tooldef ld nonsense=1\n", params); diff --git a/test/unit/testsdcclisting.ts b/test/unit/testsdcclisting.ts index e44eaad9..3b93844b 100644 --- a/test/unit/testsdcclisting.ts +++ b/test/unit/testsdcclisting.ts @@ -18,8 +18,24 @@ const RST_SDCC4 = [ " 0000056F CD 40 00 [17] 1278 \tcall\t_inline_fn", ].join("\n"); +// the mos6502 backend: a tab and a space around the line number, [ 2] cycles +const RST_MOS6502 = [ + " 117 ;\thello.c: 17: void main(void) {", + " 118 ;\tgenLabel", + " 00000865 126 _main:", + " 128 ;\thello.c: 18: putstr(message);", + " 00000865 A2 08 [ 2] 136 \tldx\t#>_message", + " 00000869 20 4E 08 [ 6] 138 \tjsr\t_putstr", +].join("\n"); + describe("sdcc listings", function () { + it("maps mos6502 source comments", function () { + const l = parseRSTListing(RST_MOS6502, "hello"); + assert.deepStrictEqual(l.lines.map((x) => [x.line, x.offset]), [[17, 0x865], [18, 0x865]]); // the function line maps to its label + assert.deepStrictEqual(l.asmlines.map((x) => x.offset), [0x865, 0x869]); + }); + it("maps SDCC 3.6.5 source comments", function () { const l = parseRSTListing(RST_SDCC3, "skeleton"); assert.deepStrictEqual(l.lines.map((x) => [x.line, x.offset]), [[172, 0x56c]]); diff --git a/test/unit/testsdcctarget.ts b/test/unit/testsdcctarget.ts index 95ebb1fd..284c86b9 100644 --- a/test/unit/testsdcctarget.ts +++ b/test/unit/testsdcctarget.ts @@ -1,7 +1,7 @@ import assert from "assert"; import { describe, it } from "mocha"; -import { sdccTarget } from "../../src/worker/tools/sdcc"; -import { getToolForFilename_6502 } from "../../src/common/toolselect"; +import { sdccTarget, loadHeader } from "../../src/worker/tools/sdcc"; +import { getToolForFilename_6502, getToolForFilename_z80, getToolForFilename_apple2 } from "../../src/common/toolselect"; describe("sdcc targets", function () { @@ -25,4 +25,20 @@ describe("sdcc targets", function () { assert.strictEqual(getToolForFilename_6502('hello.c'), 'cc65'); }); + it("selects SDCC for .sdcc files", function () { + assert.strictEqual(getToolForFilename_6502('hello.sdcc'), 'sdcc'); + assert.strictEqual(getToolForFilename_apple2('hello.sdcc'), 'sdcc'); + assert.strictEqual(getToolForFilename_z80('skeleton.sdcc'), 'sdcc'); + }); + + + it("wraps linked images in a load header", function () { + const img = Uint8Array.of(1, 2, 3, 4, 5); + assert.deepStrictEqual(Array.from(loadHeader('dos33', img, 0x803, 3)), [3, 8, 3, 0, 1, 2, 3]); + // load address, link pointer, line 10, SYS 2061, then the code + assert.deepStrictEqual(Array.from(loadHeader('prg', img, 0x80d, 2)), + [1, 8, 0x0b, 8, 10, 0, 0x9e, 0x32, 0x30, 0x36, 0x31, 0, 0, 0, 1, 2]); + assert.throws(() => loadHeader('prg', img, 0x900, 2), /\$80d/); + assert.throws(() => loadHeader('bogus', img, 0, 0), /unknown load_header/); + }); }); diff --git a/test/unit/testtoolmeta.ts b/test/unit/testtoolmeta.ts index 92474f0e..95066bcb 100644 --- a/test/unit/testtoolmeta.ts +++ b/test/unit/testtoolmeta.ts @@ -4,7 +4,7 @@ import { TOOL_META, getToolMeta, getToolMetaForFilename, getPreloadFSName, getSkeletonName, getIncludePatterns, getLinkPatterns, matchDependencyPatterns, getSystemIncludePatterns, SYSTEM_INCLUDE_PATTERNS, getSharedFileSystemName, getToolHelpURL, - getPlatformToolHelpURL + getPlatformToolHelpURL, SDCC_DEFAULT_VERSION } from "../../src/common/toolmeta"; // Node-friendly worker entry point (re-exports TOOLS without Worker wiring) import { TOOLS } from "../../src/worker/workerlib"; @@ -51,12 +51,16 @@ describe('Tool metadata registry', function () { assert.strictEqual(getSharedFileSystemName('cc65', 'nes'), 'wasi:cc65-fs-nes.zip'); assert.strictEqual(getSharedFileSystemName('cc65', 'c64'), 'wasi:cc65-fs-c64.zip'); assert.strictEqual(getSharedFileSystemName('cc65', 'nonexistent'), undefined); - // SDCC 4.x (WASI zip) by default; 3.x-library platforms keep the 3.6.5 preloadFS - assert.strictEqual(getPreloadFSName('sdcc'), undefined); - assert.strictEqual(getSharedFileSystemName('sdcc', 'mw8080bw'), 'wasi:sdcc-fs.zip'); + // the 'default' config leads with whichever SDCC the project defaults to + // (getSharedFileSystemName prefers preloadFS); platforms whose libraries + // were built by 3.x always keep the 3.6.5 preloadFS. + const defaultPreload = SDCC_DEFAULT_VERSION === 3 ? 'sdcc' : undefined; + const defaultShared = SDCC_DEFAULT_VERSION === 3 ? 'sdcc' : 'wasi:sdcc-fs.zip'; + assert.strictEqual(getPreloadFSName('sdcc'), defaultPreload); + assert.strictEqual(getSharedFileSystemName('sdcc', 'mw8080bw'), defaultShared); assert.strictEqual(getPreloadFSName('sdcc', 'gb'), 'sdcc'); assert.strictEqual(getPreloadFSName('sdcc', 'msx-libcv'), 'sdcc'); - assert.strictEqual(getSharedFileSystemName('sdcc', 'msx'), 'wasi:sdcc-fs.zip'); + assert.strictEqual(getSharedFileSystemName('sdcc', 'msx'), defaultShared); assert.strictEqual(getPreloadFSName('dasm'), undefined); });