Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

12KB

Building a vi/vim clone for the C64 with Power C — hard-won notes

Everything below was verified on this machine (GTK3VICE 3.10, reference/POWERc.d81), mostly by first getting it wrong. Read this before touching the build or the keyboard code.

The build: one session, start to finish

Compile every unit, link, then bye out of the Power C shell, then load and run from BASIC — all typed into one emulator session from one -keybuf string:

cc vim.c        <RETURN><RETURN><RETURN>  ... padding
cc vscreen.c    <RETURN><RETURN><RETURN>  ... padding
link -s
vim.o
vscreen.o
^                                         up-arrow: stdlib.l + syslib.l
                                          empty RETURN: done
vim                                       output name
bye
load"vim",8,1
run
  • Never run two emulators against the same .d81. Both hold the whole image in memory and write it back on exit, so the second one's exit silently erases what the first wrote. Symptoms: a compile that “produced nothing”, a linker that “found no object”, a directory that changes between two c1541 -list calls.
  • Use Start-Process -Wait (or .NET Process + WaitForExit) so a step cannot overlap the next.
  • -keybuf-delay is wall-clock milliseconds per keystroke while the machine runs at warp, so 900 ms per key is plenty for the machine.
  • cc clears the keyboard buffer when it starts, so its three disk-prompt answers have to arrive after the prompts appear, not before. That is why a 300 ms burst of RETURNs fails: the answers are eaten and cc sits at “insert object disk” forever. 900 ms plus ~20 spare RETURNs after each cc works. Spare RETURNs at the shell prompt are harmless.
  • The linker's > prompts are line-based and it loads each object before asking again, so no padding RETURNs may sit between the object names — an empty line means “end of object list”. ^ is the up-arrow key and pulls in the libraries; forget it and you get “unresolved external references” (Main, SYS, MALLOC, ...).
  • -exitscreenshot will not overwrite an existing file, and -limitcycles is what ends the session (VICE exits with status 1 and prints “cycle limit reached” — that is normal).
  • Keep the disk image a bare filename and run from its folder: VICE splits its image:file autostart argument on a colon, so a full path is ambiguous.

PowerShell traps

  • Start-Process -ArgumentList joins the array with spaces and quotes nothing. "-keybuf", "cc vim.c\n" therefore arrived as two arguments and VICE silently never started (no window, no screenshot). Build the command line yourself and hand it to System.Diagnostics.ProcessStartInfo.Arguments, escaping inner " as \" (see build.ps1).
  • PowerShell does not interpret \n in a double-quoted string, so "cc $u.c\n" really is backslash-n — which is what VICE wants.

Power C specifics

  • cc name.c → name.o; it prompts for source disk / compiler disk / object disk. The Shell prompt is $; bye returns to BASIC.
  • The linker refuses to overwrite — “63, file exists” — and then gives up quietly. Build on a fresh copy of the system disk instead.
  • link -s with no address links at the start of BASIC: the file begins 01 08, holds 10 SYS 2062, and $080E is a JMP to the C runtime, so load"name",8,1 + run works.
  • Source files on disk are SEQ PETSCII with CR line endings: a-z → $41-$5A, A-Z → $C1-$DA, CRLF → CR, { | } ~ → $DB $DC $DD $DE (ASCII $7B-$7F plus $60).
  • Do not write ||. It needs | = LOGO+* = PETSCII $DC, and cc dies with “*** illegal character” part-way through the file. Use nested ifs. (&& is fine.)
  • EOF is not declared unless you #include <stdio.h>; test if (ch < 0) break; instead.
  • K&R only: no prototypes, no void, no // comments, identifiers significant to 8 characters, int is 16 bits, chars are unsigned and not sign-extended. Functions returning int need no declaration; declare char *malloc();.
  • peek()/poke() are C128-only — use pointer casts.
  • malloc() (declared char *malloc();) happily gives 19000 bytes. Compiled programs and the heap end at $D000 by default.
  • Keep the readable .c authoritative and .pet derived. In one bad session the converted PETSCII copy ended up written over the host .c file (every letter uppercased, CR line endings). It still compiled — the conversion round-trips — but the source was gone.

Calling machine language: sys() and the keyboard

  • On the C64, sys takes no bank: sys(address, &a, &x, &y). The library reference page documents the C128 form sys(bank, address, ...) (with “bank 15” in its example); only the C64 tutorial section (“add3” at $C000) shows the four-argument call. Passing the bank makes 15 the jump address, so the CPU runs zero page and the C64 warm-starts: blank screen and READY., no other evidence. That was the real cause of the earlier “GETIN through sys() crashes” and of the first ML row-painter crash.
  • Do not return from main() to BASIC. Power C reserves zero page 43-74 ($2B-$4A), which is BASIC's program/variable pointer area, so after the program ends BASIC runs on garbage: no READY., or a mangled message such as UNCTIO?VER. The editor quits with sys(0xfce2, &a, &x, &y) (KERNAL reset) instead, which comes up with the banner and READY..
  • The library's getchar() is line buffered: it echoes through the KERNAL's screen editor and only hands the characters over after RETURN. Useless for an editor.
  • The answer: read the KERNAL keyboard buffer directly. $C6 holds how many characters are queued and they sit from $0277. The KERNAL's IRQ handler scans the matrix and fills that buffer, so this gets every real press as it happens, with no line buffering and no echo. Pop by shifting the rest down and decrementing $C6.
  • Clear $91 at startup. Bit 7 is the RUN/STOP flag and it can already be set when the program starts; treating that as “Esc” made the editor's first keypress a phantom and swallowed the filename.
  • VICE's -keybuf feeds that same buffer, so a program reading $C6 can be driven from the command line — but the keys race the program's start: type a filename and it can be eaten by BASIC before the program runs.

Machine language helpers (vml.s) - what bit us

  • sys() is using zero page $22-$2A while your routine runs. The manual lists 34-42 as “temporary storage” for ML, but sys() keeps where to store A/X/Y back in there: our routine left a screen pointer in $25, so sys() wrote X/Y into screen RAM ($0404) and then jumped to $0002. Every entry in vml.s saves $22-$2A, $4B-$60, $FB-$FC on entry and restores them before returning (zsave/zrest). The old single-row painter had the same bug; it only showed as one wrong cell at the start of the status row.
  • Do not put anything at $CF00. Power C's C stack starts just under $D000, and it is set up before main() runs, so highmem(0xc000) cannot move it. The code goes at $C000 (copied there by mlinit() after highmem(0xc000), before malloc), the parameter block at $C400, the zero page save area at $C410.
  • Every Power C function call is expensive. Filling the parameter block with one helper call per field made a full repaint cost 7 jiffies; writing through one pointer inline brought it down. Keep C out of per-row and per-byte loops: one sys() paints all 24 rows (scrn), memmove (bmove) and line-index fix-ups (ladj) are ML too.
  • Key repeat. The Power C shell leaves $028A set to repeat every key, and VICE -keybuf holds each key for wall-clock time while the machine runs at warp, so one j became seven. sinit() puts $028A back to 0 (C64 default: only cursor keys, space and DEL repeat).
  • Popping the KERNAL keyboard buffer is done in ML with SEI, so the IRQ cannot append a key half way through the shift.

Measured with the jiffy clock at $a2 and a temporary d<draw> e<op> readout on the status row (since removed):

before after
j (repaint) 17 jiffies 0-3
insert a char near the top of a 9 KB file 103 jiffies 14

C64 screen facts the editor relies on

  • Screen matrix $0400, colour RAM $D800 (both directly writable).
  • $D018 = $17 → screen at $0400, character set at $1800 (lower/upper case), so PETSCII $41-$5A draws as real lowercase and $C1-$DA as uppercase. Default is $15 (uppercase/graphics at $1000) — restore it on exit.
  • PETSCII → screen code: $20-$3F unchanged, $40-$5F minus $40, $60-$7F minus $20, $A0-$BF minus $40, $C0-$FF minus $80.
  • Reverse video is the high bit: screen code + $80. That is the cursor block and the status row.
  • $00CC non-zero stops the KERNAL cursor blinking (0 turns it on); with it left at 0 the KERNAL blinked one cell of the status row.
  • Closing the command channel (15) closes every file on the drive. Open 15 first, open() the file, read the status from 15 (a missing file still opens - only the status says 62), read the file, close it, close 15 last.
  • Arrow keys are PETSCII $11 down, $91 up, $1D right, $9D left; RETURN $0D, DEL $14.

Debugging technique that paid off

  • -exitscreenshot + -limitcycles gives one png per run; read it directly.
  • A crash warm-starts the machine: screen cleared, READY., evidence gone. So probe in steps — write a marker to $0400 (p[0] = 2 shows as B) and end with for (;;) { } instead of going on. Whatever is on screen when it hangs is the last step that worked.
  • Prove the toolchain before debugging your own code: a six-line program that pokes the top row and spins.
  • Keep a screenshot of every build step (shots\cc-*.png, link.png) and make the script fail per step (“cc vim.c produced no object”) instead of at the end, or a compile failure looks like a linker failure.

Verified in VICE

The editing commands were driven from build.ps1 -Keys against a scratch file (ALPHA/BRAVO/CHARLIE/DELTA on work.d81), then read back with c1541 and compared:

  • dd removes the current line and p puts it below the next one.
  • A, I, O and o insert where they should; RUN/STOP leaves insert mode.
  • :q on a dirty buffer refuses with unsaved - :q! quits; :q! quits and leaves the file alone.
  • A missing file prompts not found - create it? (y/n): y starts an empty buffer that :wq creates, n goes back to the file: prompt.
  • \x03 in -keybuf arrives as RUN/STOP (PETSCII $03), so a script can leave insert mode.

Power C limits met while adding :dir

  • Keep each unit small. With :dir added to vim.c (9 KB of code) the compiler emitted return; in main as a jump to garbage (jmp $a657, an address outside the object), so the editor crashed on :q. Moving dirlist() into its own unit (vdir.c) fixed the size, and main no longer has a return after sdone() (which never returns anyway). Even at 7.5 KB the bad jump came back until that return was removed - treat return; in a big main with suspicion.
  • build.ps1 lists the units: vim, vscreen, vview, vedit, vdir, vml.
  • The directory is read like a file: open(5, 8, 0, "$"), skip the load address, then per line link word (0,0 = end), block count word, text up to a 0 byte.

Improved build

build-improved.bat builds all six units with the improved toolchain from reference/powerc-re (peephole optimizer + faster runtime objects; the original Power C disk is untouched) and writes vim-improved.d81; run-improved.bat starts it. The plain build.bat / run.bat path (original toolchain, work.d81) still works.

Powered by TurnKey Linux.