sdcc: added apple2 and c64 support for sdcc 4; serial command for devel-6502; fixed test

This commit is contained in:
Steven Hugg committed 2026-10-03 15:01:22 +02:00
1 parent 17baa12308
commit 55709205c1
30 files changed
+487 -26

No files matched your search

+4 -1
View File
@@ -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.
+7
View File
@@ -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"
+28
View File
@@ -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 <string.h>
#ifndef __SDCC
#include <conio.h>
#include <apple2.h>
#include <peekpoke.h>
#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
}
//
+31
View File
@@ -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 <stdint.h>
#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);
}
+106
View File
@@ -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 <stdint.h>
#include <stdlib.h>
#include <string.h>
#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
+15 -1
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <peekpoke.h>
#include <string.h>
#include <c64.h>
#endif
#ifdef __CC65__
#include <conio.h>
@@ -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
+32
View File
@@ -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 <stdint.h>
#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);
}
+3
View File
@@ -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)
+2
View File
@@ -2,7 +2,9 @@
#include "common.h"
//#link "common.c"
#ifndef __SDCC
#include <cbm_screen_charmap.h>
#endif
void main(void) {
unsigned int i;
+11
View File
@@ -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"
+22
View File
@@ -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
+2 -2
View File
@@ -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() { }
+6
View File
@@ -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;
+1 -1
View File
@@ -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',
+2 -3
View File
@@ -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";
+2
View File
@@ -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
~~~
+3 -1
View File
@@ -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; }
+2
View File
@@ -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:[
+27 -4
View File
@@ -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<Devel6502> 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<Devel6502> 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); }
+2
View File
@@ -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
+9
View File
@@ -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,
+7 -2
View File
@@ -466,8 +466,8 @@ export function applyAsmProjectParams(params) {
*
* //#symbol [<phase>] NAME[=VALUE] phase in c|as|ld (default c)
* //#flag <phase> <args...> raw argv for one phase
* //#tooldef <phase> NAME=VALUE typed knob (linker: cfgfile, libargs;
* compiler: sdcc=3|4)
* //#tooldef <phase> 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);
+11
View File
@@ -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'],
},
+56 -4
View File
@@ -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 <start>` 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<ArrayBuffer> {
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-<o> in line 1 of main.asm null
// <o> .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 `;<stdin>:N:` comments, SDCC 4.x writes
* `;file.c:N: <source>` (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: <source>`.
*/
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+ ;(?:<stdin>|/*${name}\\.[^:\\s]+):(\\d+):`, 'i');
// the mos6502 backend writes `;<tab>file.c: 17: source`, with a space after the colon
const srcre = new RegExp(`^\\s+\\d+ ;\\s*(?:<stdin>|/*${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<BuildStepResult> {
// 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) {
+24
View File
@@ -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'));
+19
View File
@@ -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 <stdio.h>\nint main() { printf("FOO"); return 0; }', platform:'c64', tool:'oscar64', path:'main.c', mainfile:true}];
var result = await new Promise(function(resolve, reject) {
+10
View File
@@ -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);
+16
View File
@@ -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 <stdin> source comments", function () {
const l = parseRSTListing(RST_SDCC3, "skeleton");
assert.deepStrictEqual(l.lines.map((x) => [x.line, x.offset]), [[172, 0x56c]]);
+18 -2
View File
@@ -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/);
});
});
+9 -5
View File
@@ -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);
});