Coder Social home page Coder Social logo

skx / cpmulator Goto Github PK

View Code? Open in Web Editor NEW
96.0 5.0 3.0 675 KB

Golang CP/M emulator for zork, Microsoft BASIC, Turbo Pascal, Wordstar, lighthouse-of-doom, etc

License: MIT License

Go 68.18% Makefile 0.41% Assembly 31.40% DIGITAL Command Language 0.01%
cpm golang golang-application z80 zork emulation

cpmulator's Introduction

Steve Kemp

Personal details:

  • Location: Helsinki, Finland.
    • Nationality: British Citizen.
  • Occupation: Sysadmin / Devops / Cloud-person.

Overview:

  • I've been programming for over half my life, I'm comfortable creating, developing, maintaining, and improving software written in multiple languages
    • Including C, C++, Emacs Lisp, Perl, Ruby, Java, Shell, & TCL.
    • Most of my personal projects are written in Golang and Rust.
  • My interests primarily revolve around compilers, interpreters, domain-specific languages, and virtual machines.
  • I'm also interested in retro programming/projects, primarily based around the Z80 processor.

Surveys & Email Harvesting

I explicitly do not consent to receiving your "research surveys", or other contact generated via email scraping of the Github service. Such contact will always be followed up with.

Github Activity

cpmulator's People

Contributors

skx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

cpmulator's Issues

We need to pay attention to the FCB drive field

The current README for this project says:

  • Inconsistent handling of Drives in FCB entries.
    • There seems to be no suffering here, but ..

This is sadly incorrect, now I'm using more drives in the cpm-dist repository. For example:

$ cpmulator -cd ~/Repos/github.com/skx/cpm-dist -directories
A> DUMP G:ZORK1.DAT
.. failed to open ...

The issue here is that I use the current drive only, and don't set the drive to be that contained within the FCB. CCP usually calls SET_DRV before running things, so there I could run "C:FOO", "A:BAR", etc.

Running without the -directories flag all is OK, but otherwise the file open, make file, erase, and renaming of files needs to pay explicit attention to the Drive field:

  • FCB+00h == Drive
    • 0 for default
    • 1-16 for A-P.

Remove stty usage

As part of improving our input handling (specifically correctly dealing with blocking / non-blocking and the use of echo / no-echo) I added calls to stty.

This broke Windows builds, because the program isn't available there.

Add a go-lang native function for "EchoOn()" and "EchoOff()" and build for "everything".

Turbo pascal almost works!

With #59 in-play Turbo pascal launches, and compiles code correctly:

  • The editor is functional.
  • Files can be compiled to disk, or memory.

The generated executables run under other emulators, however they don't run under cpmulator - instead they launch and terminate with "out of memory" issues.

While I won't report bugs for all programs in the world if I could fix this one it would be nice, and it is obviously very close to being complete.

Continue running after JP 0x0000

The pascal-compiled binaries exit with JP 0x0000, which causes our emulation to terminate.

We should stop that, by allowing JP 0x0000 to be caught:

  • Catch warmboot and restart.
  • Catch coldboot and restart.

Global I/O helper

At the moment we have the io package for polling for characters, and lines. It turns on/off echos as necessary.

If we have "no echo input" ten times in a row it disables echoing ten times in a row.

If we instead used a consistent object for IO it could have three states:

  • Unknown
  • Echo on
  • Echo off

And it would only need to make the change when the state changed.

Overhaul our file handling

I tried to write a file-copy command this morning, in z80 assembly, however it turned out to be painful:

  1. Open the source file
  2. Count the number of blocks.
  3. Close the source file
  4. Open the source file
  5. Read a block of data
  6. Close the file
  7. Open the destination file
  8. Write the block of data
  9. Close the file
  10. Goto 4 until all blocks are copied.

The root cause of this is that we only keep one cached/open filehandle - when an OpenFile request is made we close any previously opened file.

We need to allow N files to be opened, perhaps with a lower mask. We can probably assume that the Open/Read/Write/Close operations carried out will keep the same FCB address. We can probably fill in FCB.Al with the filehandle, or some other "invisible" strucutre. Needs thought, but we should do better.

File I/O is broken

Not 100% sure what is happening, but something is wrong now with file I/O and zork won't even run

frodo ~/Repos/github.com/skx/cpmulator $ go build . && ./cpmulator ZORK1.COM 
Error running ZORK1.COM: failed to get file size of ZORK1.DAT: stat ZORK1.DAT: bad file descriptor

Need to try again to rethink the file handle handling :/

Also need to add a test-case, or three!

Tight CPU loop for AUX input is a problem.

Running zork1, etc, is fine, because that uses the nice I/O syscall, but running mbasic involves the use of the A_READ/A_WRITE functions and the reader in particular ends up busy-waiting on pending-input, which causes high CPU load (on the host).

We should look at using termbox, or similar, so that we can poll for pending input more efficiently - or alternatively we should poll STDIN in a goroutine and route input to the cpm object.

Fix C_STAT

Right now we fake the result, but what we should do is return a flag describing if input is pending.

This came up when adding https://github.com/dimitrit/tastybasic

I think we could probably "fake" this by adding read-with-timeout of 10milliseconds, or similar. Otherwise we have to switch to using one of the go terminal packages - go-termbox, or similar.

Either works but the former will be simpler to implement.

Confusion over BDOS and BIOS implementation?

I suspect I'm confused between console handling, which might explain why I'm getting such bogus stuff from WS.COM (wordstar).

Sanity-check things to make sure I'm not confusing :

I have a strong-suspicin that everything I'm handling via the SysCallIO - i.e. the RST instructions - is wrong. I'm reusing the same syscall handlers. Though it has to be said that MBASIC - which is the first program I found using the "RST 0xXX" approach does work properly.

Ensure we're running under cpmulator

We have some custom BIOS extensions which allow changing our behaviour:

  • CTRLC
  • CONSOLE
  • QUIET
  • CCP

These operate blindly, we should test that we're running under cpmulator before proceeding.

Our glob-handling is not ideal

When we receive a command such as:

  • ERA *.GO
  • DIR *.com

We don't handle that in an ideal way, the problem is that CP/M upper-cases all the command-line arguments, so we're left tryign to run a glob with a pattern like "*COM", or "*GO".

However our glob uses the golang filepath.Glob function and that doesn't correctly cope with the case differences. We probably need to run fileplath.Glob( "*") in all cases, and add a test for the match against a pattern directly.

Until then we cannot match lower-case files. A good example is if you have this repository cloned locally:

$ go build && ./cpmulator

A>DIR README.*
A: README  .MD 

A>DIR *.md  
No file

The first case works because README.md has an upper-case prefix. But the second case fails because the suffix .md is lower-cased.

Move the character output routine into its own pacakge

We have a consolein package for reading from STDIN.

We should move the output writing into consoleout, and then allow switching the console output routine at runtime.

At the moment our output routine pretends to be a ADM-32 terminal:

This works with the WS.COM (wordstar) binary in our cpm-dist repository, and for most other stuff I tried. We can switch to raw/ANSI via the environmental variable SIMPLE_CHAR. Instead we should add a custom syscall to allow switching at runtime - as per:

Add a new binary, beneath samples/, named "console.com". Expected use will be something like:

  • console
    • Show the selected output method
  • console ansi|raw
    • Use raw output
  • console adm
    • Use the ADM driver.

Add tracing

Use slog, or similar, to trace the calls made and a decent summary of their arguments and return values.

Pressing Ctrl-C at the input-prompt should reboot

This isn't happening because the Readline we're using killing the process instead..

It would be a pain to have to rewrite the readline in terms of read-character (terminating on "\n", and handling backspace, etc.) but that's an obvious solution. (With raw mode, of course).

stat.com - doesn't show file sizes

There are various versions of STAT.COM floating around, but I don't think I've ever seen source code.

Anyway most of them just show the list of filenames matching a pattern, via "F_SFIRST" and "F_SNEXT", along with sizes. Under cpmulator we don't get any file-sizes as I don't open the files found and populate the FCB entries with RC (record-count) entries.

This is an easy fix to make and once complete I should add a working STAT.COM to cpm-dist.

SUBMIT.COM is broken for >2 commands

Originally SUBMIT.COM didn't work at all, as reported in #75, because I didn't correctly truncate files on close.

I added support, tested it, etc, but only handled the case of files with two commands.

I created a file like so:

$ submit.pl "QUIET" "CTRLC 0" "QUIET" "CTRLC 1"

This should run four commands,but doesn't. Submit.pl looks like this:

#!/bin/sh
#
# Write a $$$.SUB file using the commands specified in the argument.
#
# Usage:
#    submit.pl 'CC ECHO' 'AS ECHO' 'LN ECHO.O C.LIB'
#
perl -e 'print join("\n", reverse @ARGV)' "$@" | \
    perl -ne 's/[\r\n]//g; printf "%-128s", chr(length($_)).$_.chr(0)' > '/home/skx/Repos/github.com/skx/cpm-dist/A/$$$.SUB'

The generated file appears to be correct, multiples of 128 characters, with good contents. But it only works for two commands.

Probably the truncation code is removing too much, or otherwise being weird.

Implement virtual filesystem

So I've created some custom extensions, for changing the CtrlC handler, the CCP, the console, etc, etc. These extensions require the user has the CTRLC.COM binary present, as well as CCP.COM, CONSOLE.COM, etc.

It makes no sense to bundle those binaries in cpm-dist because they're 100% specific to this emulator.

But of course saying:

  • Download cpm-dist.
    • Or have your own collection of binaries.
  • Then add my utilties there too.

What would be ideal would be if those files were always present on A:, 100% of the time, and were embedded in the interpreter. (Perhaps along with SUBMIT.COM?)

To do that I'd need to:

  • Move the binaries from samples/ into "embed/".
  • Embed the binaries into our binary.
  • Update our File-Find, FileOPen, etc, to take account of them.
  • Document this.

It shouldn't be hard, and it would users to build a single standalone binary with whatever things they wanted available on A:, or any other drive.

Perhaps we just add:

  • embed/A - embed/P
  • And include embed/A/* - embed/P/*

I guess some things, like zork, would require that we embed more than just *.COM, we'd need *.DAT too. That means I probably don't want to put the sources to the helpers in the same place.

TLDR; Allow embedding CP/M binaries/files inside our emulator. Merge them in with local contents at run-time.

When CCP reloads current drive is lost

#38 accidentally demonstrates this, but a simpler example:

$ ./cpmulator -directories -cd ~/Repos/github.com/skx/z80-playground-cpm-fat/dist/CPM/DISKS/

A>G:

G>zork1

ZORK I: The Great Underground Empire
Copyright (c) 1981, 1982, 1983 Infocom, Inc. All rights
reserved.
ZORK is a registered trademark of Infocom, Inc.
Revision 88 / Serial number 840726

West of House
You are standing in an open field west of a white house, with
a boarded front door.
There is a small mailbox here.

>quit
Your score is 0 (total of 350 points), in 0 moves.
This gives you the rank of Beginner.
Do you wish to leave the game? (Y is affirmative): >y

A>

Note at the end we're back on A:? We should be on G:, despite the fact that we save the state and restore it.

MBASIC.com binary does not work

This is something that would be awesome to have, so it should be resolved.

When the binary runs we immediately get an error:

{"time":"2024-04-24T21:25:45.43693778+03:00",
 "level":"ERROR",
 "msg":"Unimplemented SysCall",
 "syscall":66,"syscallHex":"0x42"}
Error running MBASIC.COM []: UNIMPLEMENTED

I do not believe that the syscall number there is correct, CP/M syscalls are implemented by loading hte operation-number in the C register and running call 0x0005. I suspect what is actually happening here is that control-flow has gone wrong, and we're getting a random branch there.

Now why might that be? Well I disassembled the binary and I see a bunch of rst xx instructions. Those are single-byte calls to specific addresses in the zero-page.

For example:

$ z80dasm B/MBASIC.COM| grep -i rst
	rst 10h	
	rst 8	
	rst 0	
	rst 0	
	rst 8	
	rst 8	

So I think the issue here is that we don't actually setup any traps, or handlers, for those low addresses - and we're actually executing the contents of the FCB, DMA area, etc, etc.

To resolve this we need to add JMP handlers to point to the entry-point - or otherwise fake things so that this works - via more breakpoints, for example.

Allow running commands at startup.

We might expect users to have a startup-script to run, to configure thins to their liking. For example:

QUIET 
CTRLC 1
CCP CCPZ
CONSOLE ANSI

The CCPs both process $$$.SUB if it is present, but then delete it. It seems like we could just populate that file with the contents of autoexec.sub if it exists.

The biggest issue is of course that the commands have to exist.

If a user runs the following all is okay:

cpmulator -cd cpm-dist/ -directories

But if they run the naive:

cpmulator

Then chances are QUIET.com won't exist, etc. Which means on startup the processing will fail.

It might be best to just run "SUBMIT AUTOEXEC" if there is a "SUBMIT.COM" present and an "AUTOEXEC.SUB" file. We'll assume that if submit.com is present the binaries the user tries to invoke is also present.

needs thought..?

WORDSTAR fails to launch

Wordstar fails to launch, complaining of too little free RAM.

$ cpmulator WS.COM
WordStar, CP/M Edition, Release 4.00   #21             
Copyright (C) 1979, 1987 MicroPro International Corporation
All rights reserved.


No room

Implement function 34 (F_WRITERAND)

There are two forms of file I/O in CP/M:

  • Sequential
    • function 20 - F_READ
    • function 21 - F_WRITE
  • Random
    • function 33 - F_READRAND
    • function 34 - F_WRITERAND

Of those four three are implemented and one is not. Add it :)

Add -quiet command-line flag

At the moment we have samples/QUIET.COM to disable the printing of output when CCP is (re)booted.

But we don't have a CLI flag for that behaviour, and it is an obvious omission.

Always use drives (internall)

Setup a map of drive -> paths, and use it unconditionally.

This will remove the "if cpm.Drives" which we have otherwise in our syscall handlers.

SUBMIT.COM is broken in CCP

So we have two CCPs which are installed:

  • CCP - The original one from DR.
  • CCPZ - An extended/enhanced one.

I specifically added support for CCPZ because I remembered there were reports of the SUBMIT handling being broken in the default DR CCP - which came from the z80 playground project.

We should fix this so that both CCPs have working SUBMIT, even though changing, at runtime even, is trivial.

Remove duplicate code

We have a bunch of code for getting a file-name from the FCB, etc, and we should unify it all.

SUBMIT.com doesn't work

Now I'm using the CCP which is lightly modified from the z80-playground version, and I recall that had issues with SUBMIT.com so it might be I need to make changes there.

Populate FCB paths from CLI arguments

In #1 I reported the absence of CLI arguments in the default DMA area, which was resolved in #3.

However by default the first two arguments are used to populate the FCB data, if present.

I updated samples/cli-args.z80 to show the filenames in the default FCBs, so the next step is to update them:

  • Remember that filenames are converted to CP/M pathes "foo.com" gets converted to "FOO COM".
  • Long entries will be truncated.
  • All arguments are upper-cased.
  • Only the first two arguments get used.

If we add FCB support we'll be able to implement FindFirst/FindNext which will make simple LS.COM-like utilities work.

Allow switching CCP at runtime

Now that we've added a "secret" BIOS function for interacting with the emulator we've used it a bunch of times:

  • Change the Ctrl-C requirements for rebooting.
  • Change the output driver, at runtime.

The obvious next step is to allow switching CCP at runtime too, via CCP.COM. Run that with the name of the thing you want to run and it should just work.

We'll need to overhaul the main loop which runs:

  • Forever
    • Load CCP
    • Execute it

But that's not a bad thing to do anyway. main.go is getting messy as it is and should be cleaned up in a separate issue.

TLDR:

  • Add syscall for changing CCP.
  • Add samples/CCP.{z80 COM}.
    • Add to cpm-dist too.
  • Make it work.

Reorganize code, add cmd/ driver.

  • Overhaul the code to give structure.
    • Probably add one function for each CP/M bios we implement.
  • Create cmd/go-cpm, or similar.
  • Add test-cases.
  • Add CI/CD

Aztec C compiler doesn't work

The assembler and compiler appear to produce identical output.

However the link step fails.

Sample files here:

Try running these in the CCP and initial state looks good, but then:

$ ./cpmulator -directories -cd ~/Repos/github.com/skx/z80-playground-cpm-fat/dist/CPM/DISKS/

A>c:

C>CC ECHO

C Vers. 1.06D 8080  (C) 1982 1983 1984 by Manx Software Systems

A>AS ECHO

AS?

A>C:

C>AS ECHO

8080 Assembler Vers. 1.06D

A>C:

C>LN ECHO.O T.LIB C.LIB

C Linker Vers. 1.06D
Base: 0100   Code: 008b  Data: 0000  Udata: 0000  Total: 00008e

A>C:

C>ECHO HELLO WORLD


C>

Consider a UI wrapper

Right now we've ignored the "is key pending", because there's no good way to get that in a portable program.

Look at textcell, or some other ncurses/console wrapper and see if we can use that to handle the async I/O requests.

Not a blocker, if we can't then we don't. I guess my aim here would be to run zork, and if that is necessary then we do our best. We'll need to implement basic file handling before we learn what is required.

Add exit/halt/quit commands

Not sure which is best, perhaps all?

But we can exit the CCP cleanly if we add a command that terminates the Z80 emulator with a "HALT" instruction.

Cannot save game : DRV_ALLRESET unimplemented

Running zork and attempting a save fails immediately:

You are standing in an open field west of a white house, with
a boarded front door.
There is a small mailbox here.

>save
Load SAVE disk then enter file name.
(default file name is ZORK1.SAV).
Type <ENTER> to continue  > ss

{"time":"2024-04-15T22:04:30.340694136+03:00","level":"ERROR","msg":"Unimplemented syscall","syscall":13,"syscallHex":"0x0D"}
Error running ZORK1.COM: UNIMPLEMENTED

Should be a trivial one:

  • Close any open file.
  • Reset drive to be A: - even though we don't care about drives.
  • Return 0x00 in A register.

Obviously it will immediately fail to save the game when it attempts it, but it'll be interesting to see if it tries to write by block, or sequentially.

Ergonomics ..

We recently added support for the "Write character to printer" syscall (L_WRITE).

Because writing the character given to the screen would be messy and unexpected I just decided to write to the static file "print.log":

  • Add a new CLI argument "-printer /path/to/print.log"
  • Add a new CLI flag to replace the $DEBUG usage "-debug /path/to/file.log"

These will make our ergonomics a little nicer.

Cannot restore game: F_READ unimplemented

To load ZORK, and other z-machine games, I had to implement the random-read functionality.

It seems that when save-states are used the sequential-read operations are required. To save a game we need to implement F_WRITE (logged in #21), and to restore a game we need F_READ which this issue covers.

Assuming we can successfully write a saved game we current see a failure attempting to load it:

>restore
Load SAVE disk then enter file name.
(default file name is ZORK1.SAV).
Type <ENTER> to continue  > FOO

{"time":"2024-04-15T22:15:23.074815567+03:00","level":"ERROR","msg":"Unimplemented syscall","syscall":20,"syscallHex":"0x14"}
Error running ZORK1.COM: UNIMPLEMENTED

Cannot save game: F_WRITE unimplemented

Attempting to save a game fails because the F_WRITE syscall is not implemented.

Load SAVE disk then enter file name.
(default file name is ZORK1.SAV).
Type <ENTER> to continue  > foo

{"time":"2024-04-15T22:11:34.391482519+03:00","level":"ERROR","msg":"Unimplemented syscall","syscall":21,"syscallHex":"0x15"}
Error running ZORK1.COM: UNIMPLEMENTED

The file is created, after the disks were reset (implemented in #20).

Allow reading files

Looking at ZORK as my example program it actually uses both:

  • 15 / F_OPEN
  • 33 / F_RANDOMREAD

So I guess this will be annoying. Opening seems simple enough. But random reading seems annoying given the flags for offsets and a lack of good documentation in my simple reference:

Drive handling is weird ..

CP/M supports up to 16 drives:

  • A:
  • B:
  • ..
  • P:

That is number 00 to 15. We use AND to mask the value which leads to weird choices:

A>W:   
G>X:
H>S:

We should probably do an "if > 15 : then = 15 ; fi" instead. That way selecting "S:", "W:", or "X:" would always select "P:". That's more consistent and less confusing.

Allow alternative CCPs

Related to #75, and assuming the worst, it seems like one way to rule out the CCP is to allow installing another version.

Since a CCP is 2k, at most, we can embed two+ versions in the ccp/ package, and let the user choose at runtime.

CCPZ is an obvious candidate, I'm not familiar with others off-hand. CCPZ allows:

  • Showing the User-Number in the prompt (if non-zero).
  • Setting A: as a fallback drive
    • So on C: if you type "FOO" , and C:FOO.COM doesn't exist A:FOO.COM will be loaded, if available.
  • Loading a binary at a given address without running it.
  • GO/JUMP to jump to specific areas of RAM, works nicely with the previous feature.

If we do add CCPZ I need to patch it to support EXIT/HALT/QUIT, but otherwise will leave alone.

We should implement the simple primitives

I suspect there are two kinds of BIOS calls to implement:

  • The easy ones.
  • The file ones.

The easy ones can be faked (get current drive, set current drive, get dma area, etc) pretty easily. It's just a matter of adding them.

The file ones (find-first, find-next) are more complex - if they were implemented then we'd be 99% complete. I'm not yet sure whether it is worth the effort, given that existing CP/M emulators exist and work. I guess that's a question of my motivation and energy.

Implement at least the trivial ones to regard this as complete:

  • ย - 25 (DRV_GET) - Return current drive
  • - 14 (DRV_SET) - Select disc
  • - 32 (F_USERNUM) - get/set user number

Implement a CCP

If no arguments are supplied then fall back to running a CCP - i.e. command-prompt.

I can almost certainly take the one built from my other project:

I guess after this I could consider a virtual filesystem, so that when launched with no arguments we had ZORK1, etc, available inside the binary. That will be a seperate issue, if it seems like a good idea.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.