Compare commits
246 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ad720f0527 | ||
|
9e89f0a23f | ||
|
cb355a17cd | ||
|
30e3a7503d | ||
|
a4ad599056 | ||
|
e20e355f9f | ||
|
9611993c85 | ||
|
cedb810400 | ||
|
4b061a76ff | ||
|
94c85a460a | ||
|
5fe1dc4fdf | ||
|
9035db5d81 | ||
|
0b3b90a198 | ||
|
c77041195e | ||
|
51a7f17e5b | ||
|
9178372942 | ||
|
3a6b8648a6 | ||
|
5cf351f05c | ||
|
7cd5ce02ec | ||
|
18c0779064 | ||
|
f0f8d6448e | ||
|
e17033329d | ||
|
36faa6e906 | ||
|
b2cf890957 | ||
|
0c615fc96c | ||
|
624374f344 | ||
|
ddfc462927 | ||
|
4b2a0d836f | ||
|
9c27175f86 | ||
|
63c982d976 | ||
|
98e6dd0cb5 | ||
|
a66dc18226 | ||
|
677e1113cb | ||
|
ea492f118e | ||
|
e966b57a0c | ||
|
b1a643f748 | ||
|
c24eeef200 | ||
|
0a1a619586 | ||
|
ba4c7437b4 | ||
|
ca49f0f535 | ||
|
e770dbc058 | ||
|
4e116182e7 | ||
|
8ea72357d5 | ||
|
63e3e8b80b | ||
|
996772a023 | ||
|
eb34906fea | ||
|
fa17d4baa4 | ||
|
a89cf24fe7 | ||
|
e38c4cf57b | ||
|
0a76765b8e | ||
|
02b6fd0fea | ||
|
259d7e2cfe | ||
|
39a2c7325c | ||
|
a4f459cab4 | ||
|
a5e8c200c7 | ||
|
e7ba568149 | ||
|
192166435b | ||
|
39e0c9ef7a | ||
|
33ea1f6e7a | ||
|
9f36507fa5 | ||
|
718f0b60b3 | ||
|
5d857dda4b | ||
|
d631ee7973 | ||
|
9feded23f8 | ||
|
f2bc9f21aa | ||
|
1777954a54 | ||
|
d85e22cc0a | ||
|
5b71a131f9 | ||
|
695eaa603b | ||
|
dcc1597cb2 | ||
|
6590626b02 | ||
|
862a3c22dc | ||
|
80977143de | ||
|
15c38b091d | ||
|
1e32421c86 | ||
|
fe15ce8c93 | ||
|
cde673a552 | ||
|
9d6393f078 | ||
|
f28dbdaa1c | ||
|
cc7f4678a8 | ||
|
41431fc0e3 | ||
|
b8769578dc | ||
|
2ae91f8585 | ||
|
765b1fba28 | ||
|
da45f0468f | ||
|
4a2c094198 | ||
|
a93a32f63e | ||
|
71fd530e26 | ||
|
76f9ae514e | ||
|
2a7cf020ae | ||
|
7143cdc2de | ||
|
4f963b0bd6 | ||
|
1a150d0f96 | ||
|
358c4fabc1 | ||
|
3f916a60fa | ||
|
fe4afa1de9 | ||
|
146e2cdad6 | ||
|
5d710b36ae | ||
|
3c9c3b7625 | ||
|
3de2c9dc1a | ||
|
faf97dcdc9 | ||
|
4884a8861f | ||
|
e47730e131 | ||
|
3b56312d9c | ||
|
612adbc11f | ||
|
b1d4d53869 | ||
|
49a425b727 | ||
|
950d99c59f | ||
|
b270c1c55b | ||
|
95b9d3ef05 | ||
|
620d496807 | ||
|
c1a8bf7ee4 | ||
|
511f1bc8c7 | ||
|
a765ac8033 | ||
|
fc03b5533d | ||
|
cab42e9a94 | ||
|
fa7604800b | ||
|
0daf1b9fd9 | ||
|
902437419e | ||
|
c3c1a262fc | ||
|
8e1bbdef0e | ||
|
f9ba9256b5 | ||
|
c390be1ced | ||
|
f15a7bb9d2 | ||
|
dfb8b1ffb2 | ||
|
f34b3da510 | ||
|
d531a26b2e | ||
|
9d040fa58b | ||
|
e49bef397b | ||
|
69ec17e541 | ||
|
0c150c3ecf | ||
|
55ca1df967 | ||
|
8b50ae1500 | ||
|
89881a8466 | ||
|
ffe644caf4 | ||
|
95998ba37d | ||
|
9c89ad4a61 | ||
|
c7f643623d | ||
|
512988655d | ||
|
bce5c4b92e | ||
|
c9bb9f2208 | ||
|
313733b728 | ||
|
cadbd70b53 | ||
|
f31773b51c | ||
|
541d8be170 | ||
|
d8293e2dda | ||
|
2eb16b4c3d | ||
|
f8360ea4b7 | ||
|
3e5e10b8dd | ||
|
17edb28d38 | ||
|
6c4a4df730 | ||
|
18e0363d84 | ||
|
7329d4f042 | ||
|
1a4a1507a5 | ||
|
de7a1af593 | ||
|
083c19d2c2 | ||
|
9d4df5b643 | ||
|
4ec3bff858 | ||
|
a9b8fe76df | ||
|
1539102ad9 | ||
|
9e63e82e9d | ||
|
758e4f79bb | ||
|
91e6eb6f6d | ||
|
9395c2ffb3 | ||
|
17f1121980 | ||
|
adcf87c29a | ||
|
10f915c90f | ||
|
97acee9f9e | ||
|
0bf4a05606 | ||
|
e18b44c88d | ||
|
4fca7dc828 | ||
|
442daca829 | ||
|
fddc9e2f6d | ||
|
3c59e9f3e4 | ||
|
b0a1282b0a | ||
|
acf47f3fc0 | ||
|
fdd17624ab | ||
|
1cd2939dca | ||
|
ce0ea6c68f | ||
|
b061b12ad8 | ||
|
41f39f7b54 | ||
|
66e94574a0 | ||
|
e07754d1d0 | ||
|
1d014b8a4f | ||
|
3f7c0bdf57 | ||
|
9ce9d1f96d | ||
|
8ebd745a63 | ||
|
634a079857 | ||
|
38f3a1e53c | ||
|
cd9c4e2b41 | ||
|
696038fa30 | ||
|
10d2551b36 | ||
|
8e109720e6 | ||
|
8f2b12d6ef | ||
|
fe72071245 | ||
|
ccd100677e | ||
|
3a3c350748 | ||
|
20808d78c8 | ||
|
8cfd448f95 | ||
|
9935510445 | ||
|
06704ed1e3 | ||
|
0c7e4b7db2 | ||
|
c82e57d895 | ||
|
81c77c7700 | ||
|
a3f69bc01d | ||
|
20c9a842d5 | ||
|
53842b2b4e | ||
|
79a584fb95 | ||
|
986172516f | ||
|
c2b620ec01 | ||
|
caa312c12a | ||
|
65f013e9b3 | ||
|
ede3b65257 | ||
|
d1bf13ce7f | ||
|
62d0a35b3e | ||
|
d47b0a5e28 | ||
|
0ef160a56e | ||
|
aa198877ec | ||
|
caabdf4781 | ||
|
6bfa1d2986 | ||
|
dad2a67321 | ||
|
b375869923 | ||
|
636895a7e4 | ||
|
48afaa471b | ||
|
6e25111224 | ||
|
f4cc6521cb | ||
|
f6c99d93a8 | ||
|
1406012c2a | ||
|
810c1a7d89 | ||
|
b9dbcc43a8 | ||
|
38a187ee5b | ||
|
fb29ae8ee7 | ||
|
b1d7fe6571 | ||
|
01099a8ce3 | ||
|
da7bb3ebf7 | ||
|
e57a4b21e6 | ||
|
54f8e48521 | ||
|
b8367a3804 | ||
|
35b8bb032b | ||
|
4d3d199d33 | ||
|
9b4ab62b08 | ||
|
4c227f3399 | ||
|
08edc95834 | ||
|
c7cc5c15b4 | ||
|
6387e93f06 | ||
|
b2b009037e |
|
@ -2,9 +2,10 @@ version: 2
|
|||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
- image: cimg/go:1.18
|
||||
steps:
|
||||
- checkout
|
||||
- run: sudo apt update
|
||||
- run: sudo apt install libsdl2-dev
|
||||
- run: go get -d ./...
|
||||
- run: go test ./...
|
||||
|
|
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 2 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['go']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
42
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Install SDL2
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsdl2-dev
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.18
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
if [ -f Gopkg.toml ]; then
|
||||
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
dep ensure
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
17
.gitignore
vendored
|
@ -1 +1,18 @@
|
|||
.vscode
|
||||
SDL2.dll
|
||||
a2sdl.exe
|
||||
frontend/a2sdl/a2sdl
|
||||
frontend/*/*.woz
|
||||
frontend/*/*.dsk
|
||||
frontend/*/*.po
|
||||
frontend/*/*.2mg
|
||||
frontend/*/*.hdv
|
||||
frontend/a2fyne/a2fyne
|
||||
frontend/headless/headless
|
||||
frontend/*/snapshot.gif
|
||||
frontend/*/snapshot.png
|
||||
printer.out
|
||||
.DS_STORE
|
||||
.scannerwork
|
||||
coverage.out
|
||||
test.out
|
||||
|
|
356
README.md
|
@ -1,86 +1,143 @@
|
|||
# Apple ][+, //e emulator
|
||||
# izapple2 - Apple ][+, //e emulator
|
||||
|
||||
Portable emulator of an Apple II+ or //e. Written in Go.
|
||||
|
||||
[![CircleCI](https://circleci.com/gh/ivanizag/apple2/tree/master.svg?style=svg)](https://circleci.com/gh/ivanizag/apple2/tree/master)
|
||||
|
||||
## Features
|
||||
|
||||
- Models:
|
||||
- Apple ][+ with 48Kb of base RAM
|
||||
- Apple //e with 128Kb of RAM
|
||||
- Apple //e enhanced with 128Kb of RAM
|
||||
- Base64A clone with 48Kb of base RAM and paginated ROM
|
||||
- Apple ][+ with 48Kb of base RAM
|
||||
- Apple //e with 128Kb of RAM
|
||||
- Apple //e enhanced with 128Kb of RAM
|
||||
- Base64A clone with 48Kb of base RAM and paged ROM
|
||||
- Storage
|
||||
- 16 Sector diskettes in NIB format
|
||||
- 16 Sector diskettes in DSK format
|
||||
- 16 Sector diskettes in WOZ 1.0 or 2.0 format (read only)
|
||||
- ProDos hard disk
|
||||
- 16 Sector 5 1/4 diskettes. Uncompressed or compressed witth gzip or zip. Supported formats:
|
||||
- NIB (read only)
|
||||
- DSK
|
||||
- PO
|
||||
- [WOZ 1.0 or 2.0](storage/WozSupportStatus.md) (read only)
|
||||
- 3.5 disks in PO or 2MG format
|
||||
- Hard disk in HDV or 2MG format with ProDOS and SmartPort support
|
||||
- Emulated extension cards:
|
||||
- DiskII controller
|
||||
- 16Kb Language Card
|
||||
- 256Kb Saturn RAM
|
||||
- ThunderClock Plus real time clock
|
||||
- Bootable hard disk card
|
||||
- Apple //e 80 columns with 64Kb extra RAM
|
||||
- VidHd, limited to the ROM signature and SHR as used by Total Replay, only for //e models with 128Kb
|
||||
- FASTChip, limited to what Total Replay needs to set and clear fast mode
|
||||
- Graphic modes:
|
||||
- Text 40 columns
|
||||
- Text 80 columns (Apple //e only)
|
||||
- Low-Resolution graphics
|
||||
- Double-Width Low-Resolution graphics (Apple //e only)
|
||||
- High-Resolution graphics
|
||||
- Double-Width High-Resolution graphics (Apple //e only)
|
||||
- Super High Resolution (VidHD only)
|
||||
- Mixed mode
|
||||
- Displays:
|
||||
- Green monochrome monitor with half width pixel support
|
||||
- NTSC Color TV (extracting the phase from the mono signal)
|
||||
- RGB for Super High Resolution
|
||||
- ANSI Console, avoiding the SDL2 dependency
|
||||
- Other features:
|
||||
- Sound
|
||||
- Joystick support. Up to two joysticks or four paddles.
|
||||
- Adjustable speed.
|
||||
- Fast disk mode to set max speed while using the disks.
|
||||
- Single file executable with embedded ROMs and DOS 3.3
|
||||
- DiskII controller (state machine based for WOZ files)
|
||||
- 16Kb Language Card
|
||||
- 256Kb Saturn RAM
|
||||
- Parallel Printer Interface card
|
||||
- 1Mb Memory Expansion Card (slinky)
|
||||
- RAMWorks style expansion Card (up to 16MB additional) (Apple //e only)
|
||||
- ThunderClock Plus real time clock
|
||||
- Apple //e 80 columns card with 64Kb extra RAM and optional RGB modes
|
||||
- No Slot Clock based on the DS1216
|
||||
- Videx Videoterm 80 column card with the Videx Soft Video Switch (Apple ][+ only)
|
||||
- SwyftCard (Apple //e only)
|
||||
- Brain Board
|
||||
- Brain Board II
|
||||
- MultiROM card
|
||||
- Dan ][ Controller card
|
||||
- ProDOS ROM card
|
||||
- Useful cards not emulating a real card
|
||||
- Bootable SmartPort / ProDOS card with the following smartport devices:
|
||||
- Block device (hard disks)
|
||||
- Fujinet network device (supports only http(s) with GET and JSON)
|
||||
- Fujinet clock (not in Fujinet upstream)
|
||||
- VidHd, limited to the ROM signature and SHR as used by Total Replay, only for //e models with 128Kb
|
||||
- FASTChip, limited to what Total Replay needs to set and clear fast mode
|
||||
- Mouse Card, emulates the entry points, not the softswitches.
|
||||
- Host console card. Maps the host STDIN and STDOUT to PR# and IN#
|
||||
- ROMXe, limited to font switching
|
||||
|
||||
- Graphic modes:
|
||||
- Text 40 columns
|
||||
- Text 80 columns (Apple //e and Videx VideoTerm)
|
||||
- Low-Resolution graphics
|
||||
- Double-Width Low-Resolution graphics (Apple //e only)
|
||||
- High-Resolution graphics
|
||||
- Double-Width High-Resolution graphics (Apple //e only)
|
||||
- Super High Resolution (VidHD only)
|
||||
- Mixed mode
|
||||
- RGB card text 40 columns with 16 colors for foreground and background (mixable)
|
||||
- RGB card mode 11, mono 560x192
|
||||
- RGB card mode 12, ntsc 160*192
|
||||
- RGB card mode 13, ntsc 140*192 (regular DHGR)
|
||||
- RGB card mode 14, mix of modes 11 and 13 on the fly
|
||||
- Displays:
|
||||
- Green monochrome monitor with half width pixel support
|
||||
- NTSC Color TV (extracting the phase from the mono signal)
|
||||
- RGB for Super High Resolution and RGB card
|
||||
- ANSI Console, avoiding the SDL2 dependency
|
||||
- Debug mode: shows four panels with actual screen, page1, page2 and extra info dependant of the video mode
|
||||
- Tracing capabilities:
|
||||
- CPU execution disassembled
|
||||
- Softswitch reads and writes
|
||||
- ProDOS MLI calls
|
||||
- Apple Pascal BIOS calls
|
||||
- SmartPort commands
|
||||
- BBC MOS calls when using [Applecorn](https://github.com/bobbimanners/)
|
||||
- Other features:
|
||||
- Sound
|
||||
- Joystick support. Up to two joysticks or four paddles
|
||||
- Mouse support. No mouse capture needed
|
||||
- Adjustable speed
|
||||
- Fast disk mode to set max speed while using the disks
|
||||
- Single file executable with embedded ROMs and DOS 3.3
|
||||
- Pause (thanks a2geek)
|
||||
- Passes the [A2AUDIT 1.06](https://github.com/zellyn/a2audit) tests as II+, //e, and //e Enhanced.
|
||||
- Partial pass ot the [ProcessorTests](https://github.com/TomHarte/ProcessorTests) for 6502 and 65c02. Failing test 6502/v1/20_55_13; flags N anv V issues with ADC; and missing some undocumented 6502 opcodes.
|
||||
|
||||
By default the following configuration is launched:
|
||||
|
||||
- Enhanced Apple //e with 65c02 processor
|
||||
- RAMWorks card with 80 column, RGB (with Video7 modes) and 8Gb RAM in aux slot
|
||||
- Parallel print inteface in slot 1
|
||||
- VidHD card (SHR support) in slot 2
|
||||
- FASTChip Accelerator card in slot 3
|
||||
- Mouse card in slot 4
|
||||
- SmartPort card with 1 device in slot 5 (if an image is provided with -disk35)
|
||||
- DiskII controller card in slot 6
|
||||
- SmartPort card with 1 device in slot 7 (if an image is provided with -hd)
|
||||
|
||||
## Running the emulator
|
||||
|
||||
No installation required. [Download](https://github.com/ivanizag/apple2/releases) the single file executable `apple2xxx_xxx` for linux or Mac, SDL2 graphics or console. Build from source to get the latest features.
|
||||
No installation required. [Download](https://github.com/ivanizag/izapple2/releases) the single file executable `izapple2xxx_xxx` for linux or Mac, SDL2 graphics or console. Build from source to get the latest features.
|
||||
|
||||
### Default mode
|
||||
|
||||
Execute without parameters to have an emulated Apple II+ with 64kb booting DOS 3.3 ready to run Applesoft:
|
||||
```
|
||||
casa@servidor:~$ ./apple2sdl
|
||||
Execute without parameters to have an emulated Apple //e Enhanced with 128kb booting DOS 3.3 ready to run Applesoft:
|
||||
|
||||
``` terminal
|
||||
casa@servidor:~$ ./izapple2sdl
|
||||
```
|
||||
|
||||
![DOS 3.3 started](doc/dos33.png)
|
||||
|
||||
### Play games
|
||||
Download a DSK or WOZ file or use an URL ([Asimov](https://www.apple.asimov.net/images/) is an excellent source) with the `-disk` parameter:
|
||||
```
|
||||
casa@servidor:~$ ./apple2sdl -disk "https://www.apple.asimov.net/images/games/action/karateka/karateka (includes intro).dsk"
|
||||
|
||||
Download a DSK or WOZ file or use an URL ([Asimov](https://www.apple.asimov.net/images/) is an excellent source):
|
||||
|
||||
``` terminal
|
||||
casa@servidor:~$ ./izapple2sdl "https://www.apple.asimov.net/images/games/action/karateka/karateka (includes intro).dsk"
|
||||
```
|
||||
|
||||
![Karateka](doc/karateka.png)
|
||||
|
||||
### Play the Total Replay collection
|
||||
|
||||
Download the excellent [Total Replay](https://archive.org/details/TotalReplay) compilation by
|
||||
[a2-4am](https://github.com/a2-4am/4cade). Run it with the `-hd` parameter:
|
||||
```
|
||||
casa@servidor:~$ ./apple2sdl -hd "Total Replay v2.0.2mg"
|
||||
[a2-4am](https://github.com/a2-4am/4cade):
|
||||
|
||||
``` terminal
|
||||
casa@servidor:~$ ./izapple2sdl Total\ Replay\ v4.0.hdv
|
||||
```
|
||||
|
||||
Displays super hi-res box art as seen with the VidHD card.
|
||||
|
||||
![Total Replay](doc/totalreplay.png)
|
||||
|
||||
### Terminal mode
|
||||
To run text mode right on the terminal without the SDL2 dependency, use `apple2console`. It runs on the console using ANSI escape codes. Input is sent to the emulated Apple II one line at a time:
|
||||
```
|
||||
casa@servidor:~$ ./apple2console -model 2plus
|
||||
|
||||
To run text mode right on the terminal without the SDL2 dependency, use `izapple2console`. It runs on the console using ANSI escape codes. Input is sent to the emulated Apple II one line at a time:
|
||||
|
||||
``` terminal
|
||||
casa@servidor:~$ ./izapple2console -model 2plus
|
||||
|
||||
############################################
|
||||
# #
|
||||
|
@ -112,99 +169,144 @@ Line:
|
|||
|
||||
```
|
||||
|
||||
### Keys
|
||||
|
||||
- Ctrl-F1: Reset button
|
||||
- F5: Toggle speed between real and fastest
|
||||
- Ctrl-F5: Show current speed in Mhz
|
||||
- F6: Toggle between NTSC color TV and green phosphor monochrome monitor
|
||||
- F7: Save current state to disk
|
||||
- F8: Restore state from disk
|
||||
- F10: Cycle character generator codepages. Only if the character generator ROM has more than one 2Kb page.
|
||||
- F11: Toggle on and off the trace to console of the CPU execution
|
||||
- F12: Save a screen snapshot to a file `snapshot.png`
|
||||
|
||||
Only valid on SDL mode
|
||||
|
||||
### Command line options
|
||||
|
||||
```
|
||||
-charRom string
|
||||
rom file for the character generator (default "<internal>/Apple2rev7CharGen.rom")
|
||||
-disk string
|
||||
file to load on the first disk drive (default "<internal>/dos33.dsk")
|
||||
-disk2Slot int
|
||||
slot for the disk driver. -1 for none. (default 6)
|
||||
-diskRom string
|
||||
rom file for the disk drive controller (default "<internal>/DISK2.rom")
|
||||
-dumpChars
|
||||
shows the character map
|
||||
-fastDisk
|
||||
set fast mode when the disks are spinning (default true)
|
||||
-hd string
|
||||
file to load on the hard disk
|
||||
-hdSlot int
|
||||
slot for the hard drive if present. -1 for none. (default -1)
|
||||
-languageCardSlot int
|
||||
slot for the 16kb language card. -1 for none
|
||||
-mhz float
|
||||
cpu speed in Mhz, use 0 for full speed. Use F5 to toggle. (default 1.0227142857142857)
|
||||
<!-- doc/usage.txt start -->
|
||||
```terminal
|
||||
Usage: izapple2 [file]
|
||||
file
|
||||
path to image to use on the boot device
|
||||
-charrom string
|
||||
rom file for the character generator (default "<internal>/Apple IIe Video Enhanced.bin")
|
||||
-cpu string
|
||||
cpu type, can be '6502' or '65c02' (default "65c02")
|
||||
-forceCaps
|
||||
force all letters to be uppercased (no need for caps lock!)
|
||||
-model string
|
||||
set base model. Models available 2plus, 2e, 2enh, base64a (default "2enh")
|
||||
-mono
|
||||
emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle.
|
||||
-panicSS
|
||||
panic if a not implemented softswitch is used
|
||||
set base model (default "2enh")
|
||||
-mods string
|
||||
comma separated list of mods applied to the board, available mods are 'shift', 'four-colors
|
||||
-nsc string
|
||||
add a DS1216 No-Slot-Clock on the main ROM (use 'main') or a slot ROM (default "main")
|
||||
-profile
|
||||
generate profile trace to analyse with pprof
|
||||
generate profile trace to analyse with pprof
|
||||
-ramworks string
|
||||
memory to use with RAMWorks card, max is 16384 (default "8192")
|
||||
-rgb
|
||||
emulate the RGB modes of the 80col RGB card for DHGR
|
||||
-rom string
|
||||
main rom file (default "<internal>/Apple2_Plus.rom")
|
||||
-saturnCardSlot int
|
||||
slot for the 256kb Saturn card. -1 for none (default -1)
|
||||
-thunderClockCardSlot int
|
||||
slot for the ThunderClock Plus card. -1 for none (default 4)
|
||||
-traceCpu
|
||||
dump to the console the CPU execution. Use F11 to toggle.
|
||||
-traceHD
|
||||
dump to the console the hd commands
|
||||
-traceSS
|
||||
dump to the console the sofswitches calls
|
||||
-vidHDSlot int
|
||||
slot for the VidHD card, only for //e models. -1 for none (default 2)
|
||||
-woz string
|
||||
show WOZ file information
|
||||
main rom file (default "<internal>/Apple2e_Enhanced.rom")
|
||||
-romx
|
||||
emulate a RomX
|
||||
-s0 string
|
||||
slot 0 configuration. (default "language")
|
||||
-s1 string
|
||||
slot 1 configuration. (default "empty")
|
||||
-s2 string
|
||||
slot 2 configuration. (default "vidhd")
|
||||
-s3 string
|
||||
slot 3 configuration. (default "fastchip")
|
||||
-s4 string
|
||||
slot 4 configuration. (default "empty")
|
||||
-s5 string
|
||||
slot 5 configuration. (default "empty")
|
||||
-s6 string
|
||||
slot 6 configuration. (default "diskii,disk1=<internal>/dos33.dsk")
|
||||
-s7 string
|
||||
slot 7 configuration. (default "empty")
|
||||
-speed string
|
||||
cpu speed in Mhz, can be 'ntsc', 'pal', 'full' or a decimal nunmber (default "ntsc")
|
||||
-trace string
|
||||
trace CPU execution with one or more comma separated tracers (default "none")
|
||||
|
||||
The available pre-configured models are:
|
||||
2: Apple ][
|
||||
2e: Apple IIe
|
||||
2enh: Apple //e
|
||||
2plus: Apple ][+
|
||||
base64a: Base 64A
|
||||
swyft: swyft
|
||||
|
||||
The available cards are:
|
||||
brainboard: Firmware card. It has two ROM banks
|
||||
brainboard2: Firmware card. It has up to four ROM banks
|
||||
dan2sd: Apple II Peripheral Card that Interfaces to a ATMEGA328P for SD card storage
|
||||
diskii: Disk II interface card
|
||||
diskiiseq: Disk II interface card emulating the Woz state machine
|
||||
fastchip: Accelerator card for Apple IIe (limited support)
|
||||
fujinet: SmartPort interface card hosting the Fujinet
|
||||
inout: Card to test I/O
|
||||
language: Language card with 16 extra KB for the Apple ][ and ][+
|
||||
memexp: Memory expansion card
|
||||
mouse: Mouse card implementation, does not emulate a real card, only the firmware behaviour
|
||||
multirom: Multiple Image ROM card
|
||||
parallel: Card to dump to a file what would be printed to a parallel printer
|
||||
prodosromcard3: A bootable 4 MB ROM card by Ralle Palaveev
|
||||
prodosromdrive: A bootable 1 MB solid state disk by Terence Boldt
|
||||
saturn: RAM card with 128Kb, it's like 8 language cards
|
||||
smartport: SmartPort interface card
|
||||
softswitchlogger: Card to log softswitch accesses
|
||||
swyftcard: Card with the ROM needed to run the Swyftcard word processing system
|
||||
thunderclock: Clock card
|
||||
videx: Videx compatible 80 columns card
|
||||
vidhd: Firmware signature of the VidHD card to trick Total Replay to use the SHR mode
|
||||
|
||||
The available tracers are:
|
||||
cpm65: Trace CPM65 BDOS calls
|
||||
cpu: Trace CPU execution
|
||||
mli: Trace ProDOS MLI calls
|
||||
mos: Trace MOS calls with Applecorn skipping terminal IO
|
||||
mosfull: Trace MOS calls with Applecorn
|
||||
panicss: Panic on unimplemented softswitches
|
||||
ss: Trace sotfswiches calls
|
||||
ssreg: Trace sotfswiches registrations
|
||||
ucsd: Trace UCSD system calls
|
||||
|
||||
```
|
||||
<!-- doc/usage.txt end -->
|
||||
|
||||
## Building from source
|
||||
|
||||
### apple2console
|
||||
### Linux
|
||||
|
||||
The only dependency is having a working Go installation on any platform.
|
||||
Besides having a working Go installation, install the SDL2 developer files. Run:
|
||||
|
||||
``` terminal
|
||||
git clone github.com/ivanizag/izapple2
|
||||
cd izapple2/frontend/a2sdl
|
||||
go build .
|
||||
```
|
||||
|
||||
### MacOS
|
||||
|
||||
With a working Go installation, run:
|
||||
|
||||
``` terminal
|
||||
brew install SDL2
|
||||
git clone github.com/ivanizag/izapple2
|
||||
cd izapple2/frontend/a2sdl
|
||||
go build .
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
On Windows, CGO needs a gcc compiler. Install [mingw-w64](http://mingw-w64.org/doku.php/download/mingw-builds) and the [SDL2 developer files](https://www.libsdl.org/release/) for mingw-64.
|
||||
|
||||
Run:
|
||||
```
|
||||
$ go get github.com/ivanizag/apple2/apple2console
|
||||
$ go build github.com/ivanizag/apple2/apple2console
|
||||
|
||||
``` terminal
|
||||
git clone github.com/ivanizag/izapple2
|
||||
cd izapple2\frontend\a2sdl
|
||||
go build .
|
||||
```
|
||||
|
||||
### apple2sdl
|
||||
|
||||
Besides having a working Go installation, install the SDL2 developer files. Valid for any platform
|
||||
|
||||
Run:
|
||||
```
|
||||
$ go get github.com/ivanizag/apple2/apple2sdl
|
||||
$ go build github.com/ivanizag/apple2/apple2sdl
|
||||
```
|
||||
To run in Windows, copy the file `SDL2.dll` on the same folder as `a2sdl.exe`. The latest `SDL2.dll` can be found in the [Runtime binary for Windows 64-bit](https://www.libsdl.org/download-2.0.php).
|
||||
|
||||
### Use docker to cross compile for Linux and Windows
|
||||
|
||||
To create executables for Linux and Windows without installing Go, SDL2 or the Windows cross compilation toosl, run:
|
||||
```
|
||||
$ cd docker
|
||||
$ ./build.sh
|
||||
```
|
||||
|
||||
To run in Windows, copy the file `SDL2.dll` on the same folder as `apple2sdl.exe`. The latest `SDL2.dll` can be found in the [Runtime binary for Windows 64-bit](https://www.libsdl.org/download-2.0.php).
|
||||
``` terminal
|
||||
cd docker
|
||||
./build.sh
|
||||
```
|
||||
|
|
96
a2audit_test.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testA2AuditInternal(t *testing.T, model string, removeLangCard bool, cycles uint64, messages []string) {
|
||||
overrides := newConfiguration()
|
||||
if removeLangCard {
|
||||
overrides.set(confS0, "empty")
|
||||
}
|
||||
overrides.set(confS1, "empty")
|
||||
overrides.set(confS2, "empty")
|
||||
overrides.set(confS3, "empty")
|
||||
overrides.set(confS4, "empty")
|
||||
overrides.set(confS5, "empty")
|
||||
overrides.set(confS6, "diskii,disk1=\"<internal>/audit.dsk\"")
|
||||
overrides.set(confS7, "empty")
|
||||
|
||||
at, err := makeApple2Tester(model, overrides)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
at.terminateCondition = buildTerminateConditionTexts(at, messages, false, cycles)
|
||||
at.run()
|
||||
|
||||
text := at.getText()
|
||||
for _, message := range messages {
|
||||
if !strings.Contains(text, message) {
|
||||
t.Errorf("Expected '%s', got '%s'", message, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestA2Audit(t *testing.T) {
|
||||
|
||||
t.Run("test a2audit on Apple IIe enhanced", func(t *testing.T) {
|
||||
testA2AuditInternal(t, "2enh", false, 4_000_000, []string{
|
||||
"MEMORY:128K",
|
||||
"APPLE IIE (ENHANCED)",
|
||||
"LANGUAGE CARD TESTS SUCCEEDED",
|
||||
"AUXMEM TESTS SUCCEEDED",
|
||||
"SOFTSWITCH TESTS SUCCEEDED",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("test a2audit on Apple IIe", func(t *testing.T) {
|
||||
testA2AuditInternal(t, "2e", false, 4_000_000, []string{
|
||||
"MEMORY:128K",
|
||||
"APPLE IIE",
|
||||
"LANGUAGE CARD TESTS SUCCEEDED",
|
||||
"AUXMEM TESTS SUCCEEDED",
|
||||
"SOFTSWITCH TESTS SUCCEEDED",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("test a2audit on Apple II plus", func(t *testing.T) {
|
||||
testA2AuditInternal(t, "2plus", false, 4_000_000, []string{
|
||||
"MEMORY:64K",
|
||||
"APPLE II PLUS",
|
||||
"LANGUAGE CARD TESTS SUCCEEDED",
|
||||
"64K OR LESS:SKIPPING AUXMEM TEST",
|
||||
"NOT IIE OR IIC:SKIPPING SOFTSWITCH TEST",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("test a2audit on Apple II plus without lang card", func(t *testing.T) {
|
||||
testA2AuditInternal(t, "2plus", true, 4_000_000, []string{
|
||||
"MEMORY:48K",
|
||||
"APPLE II PLUS",
|
||||
"48K:SKIPPING LANGUAGE CARD TEST",
|
||||
"64K OR LESS:SKIPPING AUXMEM TEST",
|
||||
"NOT IIE OR IIC:SKIPPING SOFTSWITCH TEST",
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
t.Run("test Mouse card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "mouse", 50_000_000, "2 38-18-01-20 Apple II Mouse Card")
|
||||
})
|
||||
|
||||
t.Run("test Parallel printer card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "parallel", 50_000_000, "2 48-48-58-FF Apple Parallel Interface Card")
|
||||
})
|
||||
|
||||
// Saturn not detected
|
||||
// VidHD not detected
|
||||
// Swyftcard not compatible with Card Cat
|
||||
// Pending to try Saturn, 80col with 2plus.
|
||||
|
||||
t.Run("test ThunderClock Plus card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "thunderclock", 50_000_000, "2 FF-05-18-B8 ThunderClock Plus Card")
|
||||
})
|
||||
*/
|
||||
}
|
349
apple2.go
|
@ -1,294 +1,101 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ivanizag/apple2/core6502"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/ivanizag/iz6502"
|
||||
)
|
||||
|
||||
// Apple2 represents all the components and state of the emulated machine
|
||||
type Apple2 struct {
|
||||
Name string
|
||||
cpu *core6502.State
|
||||
mmu *memoryManager
|
||||
io *ioC0Page
|
||||
cg *CharacterGenerator
|
||||
cards [8]card
|
||||
isApple2e bool
|
||||
commandChannel chan int
|
||||
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
|
||||
isColor bool
|
||||
fastMode bool
|
||||
fastRequestsCounter int
|
||||
profile bool
|
||||
showSpeed bool
|
||||
Name string
|
||||
cpu *iz6502.State
|
||||
mmu *memoryManager
|
||||
io *ioC0Page
|
||||
cg *CharacterGenerator
|
||||
cards [8]Card
|
||||
tracers []executionTracer
|
||||
|
||||
softVideoSwitch *SoftVideoSwitch
|
||||
board string
|
||||
isApple2e bool
|
||||
isFourColors bool // An Apple II without the 6 color mod
|
||||
commandChannel chan command
|
||||
|
||||
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
|
||||
fastRequestsCounter int32
|
||||
cycleBreakpoint uint64
|
||||
breakPoint bool
|
||||
profile bool
|
||||
showSpeed bool
|
||||
paused bool
|
||||
forceCaps bool
|
||||
removableMediaDrives []drive
|
||||
}
|
||||
|
||||
const (
|
||||
// CPUClockMhz is the actual Apple II clock speed
|
||||
CPUClockMhz = 14.318 / 14
|
||||
cpuClockEuroMhz = 14.238 / 14
|
||||
)
|
||||
|
||||
const maxWaitDuration = 100 * time.Millisecond
|
||||
|
||||
// Run starts the Apple2 emulation
|
||||
func (a *Apple2) Run() {
|
||||
if a.profile {
|
||||
// See the log with:
|
||||
// go tool pprof --pdf ~/go/bin/apple2sdl /tmp/profile329536248/cpu.pprof > profile.pdf
|
||||
defer profile.Start().Stop()
|
||||
}
|
||||
|
||||
// Start the processor
|
||||
a.cpu.Reset()
|
||||
referenceTime := time.Now()
|
||||
speedReferenceTime := referenceTime
|
||||
speedReferenceCycles := uint64(0)
|
||||
|
||||
for {
|
||||
// Run a 6502 step
|
||||
a.cpu.ExecuteInstruction()
|
||||
|
||||
// Execute meta commands
|
||||
commandsPending := true
|
||||
for commandsPending {
|
||||
select {
|
||||
case command := <-a.commandChannel:
|
||||
if command == CommandKill {
|
||||
return
|
||||
}
|
||||
a.executeCommand(command)
|
||||
default:
|
||||
commandsPending = false
|
||||
}
|
||||
}
|
||||
|
||||
if a.cycleDurationNs != 0 && a.fastRequestsCounter <= 0 {
|
||||
// Wait until next 6502 step has to run
|
||||
clockDuration := time.Since(referenceTime)
|
||||
simulatedDuration := time.Duration(float64(a.cpu.GetCycles()) * a.cycleDurationNs)
|
||||
waitDuration := simulatedDuration - clockDuration
|
||||
if waitDuration > maxWaitDuration || -waitDuration > maxWaitDuration {
|
||||
// We have to wait too long or are too much behind. Let's fast forward
|
||||
referenceTime = referenceTime.Add(-waitDuration)
|
||||
waitDuration = 0
|
||||
}
|
||||
if waitDuration > 0 {
|
||||
time.Sleep(waitDuration)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showSpeed && a.cpu.GetCycles()-speedReferenceCycles > 1000000 {
|
||||
// Calculate speed in MHz every million cycles
|
||||
newTime := time.Now()
|
||||
newCycles := a.cpu.GetCycles()
|
||||
elapsedCycles := float64(newCycles - speedReferenceCycles)
|
||||
freq := 1000.0 * elapsedCycles / float64(newTime.Sub(speedReferenceTime).Nanoseconds())
|
||||
fmt.Printf("Freq: %f Mhz\n", freq)
|
||||
speedReferenceTime = newTime
|
||||
speedReferenceCycles = newCycles
|
||||
}
|
||||
}
|
||||
// GetCards returns the array of inserted cards
|
||||
func (a *Apple2) GetCards() [8]Card {
|
||||
return a.cards
|
||||
}
|
||||
|
||||
func (a *Apple2) setProfile(value bool) {
|
||||
a.profile = value
|
||||
// SetKeyboardProvider attaches an external keyboard provider
|
||||
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
|
||||
a.io.setKeyboardProvider(kb)
|
||||
}
|
||||
|
||||
const (
|
||||
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
|
||||
CommandToggleSpeed = iota + 1
|
||||
// CommandShowSpeed toggles printinf the current freq in Mhz
|
||||
CommandShowSpeed
|
||||
// CommandToggleColor toggles between NTSC color TV and Green phospor monitor
|
||||
CommandToggleColor
|
||||
// CommandSaveState stores the state to file
|
||||
CommandSaveState
|
||||
// CommandLoadState reload the last state
|
||||
CommandLoadState
|
||||
// CommandDumpDebugInfo dumps usefull info
|
||||
CommandDumpDebugInfo
|
||||
// CommandNextCharGenPage cycles the CharGen page if several
|
||||
CommandNextCharGenPage
|
||||
// CommandToggleCPUTrace toggle tracing of CPU execution
|
||||
CommandToggleCPUTrace
|
||||
// CommandKill stops the cpu execution loop
|
||||
CommandKill
|
||||
// CommandReset executes a 6502 reset
|
||||
CommandReset
|
||||
)
|
||||
|
||||
// SendCommand enqueues a command to the emulator thread
|
||||
func (a *Apple2) SendCommand(command int) {
|
||||
a.commandChannel <- command
|
||||
// SetSpeakerProvider attaches an external keyboard provider
|
||||
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
|
||||
a.io.setSpeakerProvider(s)
|
||||
}
|
||||
|
||||
func (a *Apple2) executeCommand(command int) {
|
||||
switch command {
|
||||
case CommandToggleSpeed:
|
||||
if a.cycleDurationNs == 0 {
|
||||
fmt.Println("Slow")
|
||||
a.cycleDurationNs = 1000.0 / CPUClockMhz
|
||||
} else {
|
||||
fmt.Println("Fast")
|
||||
a.cycleDurationNs = 0
|
||||
}
|
||||
case CommandShowSpeed:
|
||||
a.showSpeed = !a.showSpeed
|
||||
case CommandToggleColor:
|
||||
a.isColor = !a.isColor
|
||||
case CommandSaveState:
|
||||
fmt.Println("Saving state")
|
||||
err := a.save("apple2.state")
|
||||
if err != nil {
|
||||
fmt.Printf("Error loadind state: %v.", err)
|
||||
}
|
||||
case CommandLoadState:
|
||||
fmt.Println("Loading state")
|
||||
err := a.load("apple2.state")
|
||||
if err != nil {
|
||||
fmt.Printf("Error loadind state: %v.", err)
|
||||
}
|
||||
case CommandDumpDebugInfo:
|
||||
a.dumpDebugInfo()
|
||||
case CommandNextCharGenPage:
|
||||
a.cg.nextPage()
|
||||
fmt.Printf("Chargen page %v\n", a.cg.page)
|
||||
case CommandToggleCPUTrace:
|
||||
a.cpu.SetTrace(!a.cpu.GetTrace())
|
||||
case CommandReset:
|
||||
a.cpu.Reset()
|
||||
}
|
||||
// SetJoysticksProvider attaches an external joysticks provider
|
||||
func (a *Apple2) SetJoysticksProvider(j JoysticksProvider) {
|
||||
a.io.setJoysticksProvider(j)
|
||||
}
|
||||
|
||||
func (a *Apple2) requestFastMode() {
|
||||
// SetMouseProvider attaches an external joysticks provider
|
||||
func (a *Apple2) SetMouseProvider(m MouseProvider) {
|
||||
a.io.setMouseProvider(m)
|
||||
}
|
||||
|
||||
// IsPaused returns true when emulator is paused
|
||||
func (a *Apple2) IsPaused() bool {
|
||||
return a.paused
|
||||
}
|
||||
|
||||
func (a *Apple2) GetCycles() uint64 {
|
||||
return a.cpu.GetCycles()
|
||||
}
|
||||
|
||||
// SetCycleBreakpoint sets a cycle number to pause the emulator. 0 to disable
|
||||
func (a *Apple2) SetCycleBreakpoint(cycle uint64) {
|
||||
a.cycleBreakpoint = cycle
|
||||
a.breakPoint = false
|
||||
}
|
||||
|
||||
func (a *Apple2) BreakPoint() bool {
|
||||
return a.breakPoint
|
||||
}
|
||||
|
||||
// IsProfiling returns true when profiling
|
||||
func (a *Apple2) IsProfiling() bool {
|
||||
return a.profile
|
||||
}
|
||||
|
||||
// IsForceCaps returns true when all letters are forced to upper case
|
||||
func (a *Apple2) IsForceCaps() bool {
|
||||
return a.forceCaps
|
||||
}
|
||||
|
||||
func (a *Apple2) RequestFastMode() {
|
||||
// Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain.
|
||||
if a.fastMode {
|
||||
a.fastRequestsCounter++
|
||||
}
|
||||
atomic.AddInt32(&a.fastRequestsCounter, 1)
|
||||
}
|
||||
|
||||
func (a *Apple2) releaseFastMode() {
|
||||
if a.fastMode {
|
||||
a.fastRequestsCounter--
|
||||
}
|
||||
func (a *Apple2) ReleaseFastMode() {
|
||||
atomic.AddInt32(&a.fastRequestsCounter, -1)
|
||||
}
|
||||
|
||||
type persistent interface {
|
||||
save(io.Writer) error
|
||||
load(io.Reader) error
|
||||
}
|
||||
|
||||
func (a *Apple2) save(filename string) error {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w := bufio.NewWriter(f)
|
||||
defer w.Flush()
|
||||
|
||||
err = a.cpu.Save(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = a.mmu.save(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = a.io.save(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, a.isColor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, a.fastMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, a.fastRequestsCounter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range a.cards {
|
||||
if c != nil {
|
||||
err = c.save(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apple2) load(filename string) error {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
r := bufio.NewReader(f)
|
||||
|
||||
err = a.cpu.Load(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = a.mmu.load(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = a.io.load(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &a.isColor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &a.fastMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &a.fastRequestsCounter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range a.cards {
|
||||
if c != nil {
|
||||
err = c.load(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apple2) dumpDebugInfo() {
|
||||
// See "Apple II Monitors Peeled"
|
||||
pageZeroSymbols := map[int]string{
|
||||
0x36: "CSWL",
|
||||
0x37: "CSWH",
|
||||
0x38: "KSWL",
|
||||
0x39: "KSWH",
|
||||
}
|
||||
|
||||
fmt.Printf("Page zero values:\n")
|
||||
for _, k := range []int{0x36, 0x37, 0x38, 0x39} {
|
||||
d := a.mmu.physicalMainRAM.data[k]
|
||||
fmt.Printf(" %v(0x%x): 0x%02x\n", pageZeroSymbols[k], k, d)
|
||||
}
|
||||
func (a *Apple2) registerRemovableMediaDrive(d drive) {
|
||||
a.removableMediaDrives = append(a.removableMediaDrives, d)
|
||||
}
|
||||
|
|
159
apple2Run.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// CPUClockMhz is the actual Apple II clock speed
|
||||
CPUClockMhz = 14.318 / 14
|
||||
cpuClockEuroMhz = 14.238 / 14
|
||||
)
|
||||
|
||||
const (
|
||||
maxWaitDuration = 100 * time.Millisecond
|
||||
cpuSpinLoops = 100
|
||||
)
|
||||
|
||||
// Run starts the Apple2 emulation
|
||||
func (a *Apple2) Run() {
|
||||
a.Start(false)
|
||||
}
|
||||
|
||||
// Start the Apple2 emulation, can start paused
|
||||
func (a *Apple2) Start(paused bool) {
|
||||
// Start the processor
|
||||
a.cpu.Reset()
|
||||
referenceTime := time.Now()
|
||||
speedReferenceTime := referenceTime
|
||||
speedReferenceCycles := uint64(0)
|
||||
|
||||
a.paused = paused
|
||||
|
||||
for {
|
||||
// Run 6502 steps
|
||||
if !a.paused {
|
||||
for i := 0; i < cpuSpinLoops; i++ {
|
||||
// Conditional tracing
|
||||
//pc, _ := a.cpu.GetPCAndSP()
|
||||
//a.cpu.SetTrace(pc >= 0xc700 && pc < 0xc800)
|
||||
|
||||
// Execution
|
||||
a.cpu.ExecuteInstruction()
|
||||
|
||||
// Special tracing
|
||||
a.executionTrace()
|
||||
}
|
||||
|
||||
if a.cycleBreakpoint != 0 && a.cpu.GetCycles() >= a.cycleBreakpoint {
|
||||
a.breakPoint = true
|
||||
a.cycleBreakpoint = 0
|
||||
a.paused = true
|
||||
}
|
||||
} else {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Execute meta commands
|
||||
commandsPending := true
|
||||
for commandsPending {
|
||||
select {
|
||||
case command := <-a.commandChannel:
|
||||
switch command.getId() {
|
||||
case CommandKill:
|
||||
return
|
||||
case CommandPause:
|
||||
if !a.paused {
|
||||
a.paused = true
|
||||
}
|
||||
case CommandStart:
|
||||
if a.paused {
|
||||
a.paused = false
|
||||
referenceTime = time.Now()
|
||||
speedReferenceTime = referenceTime
|
||||
}
|
||||
case CommandPauseUnpause:
|
||||
a.paused = !a.paused
|
||||
referenceTime = time.Now()
|
||||
speedReferenceTime = referenceTime
|
||||
default:
|
||||
// Execute the other commands
|
||||
a.executeCommand(command)
|
||||
}
|
||||
default:
|
||||
commandsPending = false
|
||||
}
|
||||
}
|
||||
|
||||
if a.cycleDurationNs != 0 && a.fastRequestsCounter <= 0 {
|
||||
// Wait until next 6502 step has to run
|
||||
clockDuration := time.Since(referenceTime)
|
||||
simulatedDuration := time.Duration(float64(a.cpu.GetCycles()) * a.cycleDurationNs)
|
||||
waitDuration := simulatedDuration - clockDuration
|
||||
if waitDuration > maxWaitDuration || -waitDuration > maxWaitDuration {
|
||||
// We have to wait too long or are too much behind. Let's fast forward
|
||||
referenceTime = referenceTime.Add(-waitDuration)
|
||||
waitDuration = 0
|
||||
}
|
||||
if waitDuration > 0 {
|
||||
time.Sleep(waitDuration)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showSpeed && a.cpu.GetCycles()-speedReferenceCycles > 1000000 {
|
||||
// Calculate speed in MHz every million cycles
|
||||
newTime := time.Now()
|
||||
newCycles := a.cpu.GetCycles()
|
||||
elapsedCycles := float64(newCycles - speedReferenceCycles)
|
||||
freq := 1000.0 * elapsedCycles / float64(newTime.Sub(speedReferenceTime).Nanoseconds())
|
||||
fmt.Printf("Freq: %f Mhz\n", freq)
|
||||
speedReferenceTime = newTime
|
||||
speedReferenceCycles = newCycles
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Apple2) reset() {
|
||||
a.cpu.Reset()
|
||||
a.mmu.reset()
|
||||
for _, c := range a.cards {
|
||||
if c != nil {
|
||||
c.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Apple2) executionTrace() {
|
||||
for _, v := range a.tracers {
|
||||
v.inspect()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Apple2) dumpDebugInfo() {
|
||||
// See "Apple II Monitors Peeled"
|
||||
pageZeroSymbols := map[int]string{
|
||||
0x36: "CSWL",
|
||||
0x37: "CSWH",
|
||||
0x38: "KSWL",
|
||||
0x39: "KSWH",
|
||||
0xe2: "ACJVAFLDL", // Apple Pascal
|
||||
0xe3: "ACJVAFLDH", // Apple Pascal
|
||||
0xec: "JVBFOLDL", // Apple Pascal
|
||||
0xed: "JVBFOLDH", // Apple Pascal
|
||||
0xee: "JVAFOLDL", // Apple Pascal
|
||||
0xef: "JVAFOLDH", // Apple Pascal
|
||||
}
|
||||
fmt.Printf("Page zero values:\n")
|
||||
for _, k := range []int{0x36, 0x37, 0x38, 0x39, 0xe2, 0xe3, 0xec, 0xed, 0xee, 0xef} {
|
||||
d := a.mmu.physicalMainRAM.data[k]
|
||||
fmt.Printf(" %v(0x%x): 0x%02x\n", pageZeroSymbols[k], k, d)
|
||||
}
|
||||
|
||||
pc := uint16(0xc700)
|
||||
for pc < 0xc800 {
|
||||
line, newPc := a.cpu.DisasmInstruction(pc)
|
||||
fmt.Println(line)
|
||||
pc = newPc
|
||||
}
|
||||
}
|
192
apple2Setup.go
|
@ -1,192 +0,0 @@
|
|||
package apple2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ivanizag/apple2/core6502"
|
||||
)
|
||||
|
||||
// newApple2 instantiates an apple2
|
||||
func newApple2plus() *Apple2 {
|
||||
var a Apple2
|
||||
a.Name = "Apple ][+"
|
||||
a.mmu = newMemoryManager(&a)
|
||||
a.cpu = core6502.NewNMOS6502(a.mmu)
|
||||
a.io = newIoC0Page(&a)
|
||||
addApple2SoftSwitches(a.io)
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
func newApple2e() *Apple2 {
|
||||
var a Apple2
|
||||
a.Name = "Apple IIe"
|
||||
a.mmu = newMemoryManager(&a)
|
||||
a.cpu = core6502.NewNMOS6502(a.mmu)
|
||||
a.io = newIoC0Page(&a)
|
||||
a.mmu.InitRAMalt()
|
||||
addApple2SoftSwitches(a.io)
|
||||
addApple2ESoftSwitches(a.io)
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
func newApple2eEnhanced() *Apple2 {
|
||||
var a Apple2
|
||||
a.Name = "Apple //e"
|
||||
a.mmu = newMemoryManager(&a)
|
||||
a.cpu = core6502.NewCMOS65c02(a.mmu)
|
||||
a.io = newIoC0Page(&a)
|
||||
a.mmu.InitRAMalt()
|
||||
addApple2SoftSwitches(a.io)
|
||||
addApple2ESoftSwitches(a.io)
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a *Apple2) setup(isColor bool, clockMhz float64, fastMode bool) {
|
||||
a.commandChannel = make(chan int, 100)
|
||||
a.isColor = isColor
|
||||
a.fastMode = fastMode
|
||||
|
||||
if clockMhz <= 0 {
|
||||
// Full speed
|
||||
a.cycleDurationNs = 0
|
||||
} else {
|
||||
a.cycleDurationNs = 1000.0 / clockMhz
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Apple2) insertCard(c card, slot int) {
|
||||
c.assign(a, slot)
|
||||
a.cards[slot] = c
|
||||
}
|
||||
|
||||
const (
|
||||
apple2RomSize = 12 * 1024
|
||||
apple2eRomSize = 16 * 1024
|
||||
)
|
||||
|
||||
// LoadRom loads a standard Apple2+ or 2e ROM
|
||||
func (a *Apple2) LoadRom(filename string) error {
|
||||
data, err := loadResource(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
size := len(data)
|
||||
if size != apple2RomSize && size != apple2eRomSize {
|
||||
return errors.New("Rom size not supported")
|
||||
}
|
||||
|
||||
romStart := 0
|
||||
mmu := a.mmu
|
||||
if size == apple2eRomSize {
|
||||
// The extra 4kb ROM is first in the rom file.
|
||||
// It starts with 256 unused bytes not mapped to 0xc000.
|
||||
a.isApple2e = true
|
||||
extraRomSize := apple2eRomSize - apple2RomSize
|
||||
mmu.physicalROMe = newMemoryRange(0xc000, data[0:extraRomSize])
|
||||
romStart = extraRomSize
|
||||
}
|
||||
|
||||
mmu.physicalROM[0] = newMemoryRange(0xd000, data[romStart:])
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDisk2 inserts a DiskII controller
|
||||
func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) error {
|
||||
var c cardDisk2
|
||||
data, err := loadResource(diskRomFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.loadRom(data)
|
||||
a.insertCard(&c, slot)
|
||||
|
||||
if diskImage != "" {
|
||||
diskette, err := loadDisquette(diskImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.drive[0].insertDiskette(diskette)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddHardDisk adds a ProDos hard dirve with a 2MG image
|
||||
func (a *Apple2) AddHardDisk(slot int, hdImage string, trace bool) error {
|
||||
var c cardHardDisk
|
||||
c.setTrace(trace)
|
||||
c.loadRom(buildHardDiskRom(slot))
|
||||
a.insertCard(&c, slot)
|
||||
|
||||
hd, err := openHardDisk2mg(hdImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.addDisk(hd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddVidHD adds a card with the signature of VidHD
|
||||
func (a *Apple2) AddVidHD(slot int) {
|
||||
var c cardVidHD
|
||||
c.loadRom(buildVidHDRom())
|
||||
a.insertCard(&c, slot)
|
||||
}
|
||||
|
||||
// AddFastChip adds a card with the signature of VidHD
|
||||
func (a *Apple2) AddFastChip(slot int) {
|
||||
var c cardFastChip
|
||||
c.loadRom(buildFastChipRom())
|
||||
a.insertCard(&c, slot)
|
||||
}
|
||||
|
||||
// AddLanguageCard inserts a 16Kb card
|
||||
func (a *Apple2) AddLanguageCard(slot int) {
|
||||
a.insertCard(&cardLanguage{}, slot)
|
||||
}
|
||||
|
||||
// AddSaturnCard inserts a 128Kb card
|
||||
func (a *Apple2) AddSaturnCard(slot int) {
|
||||
a.insertCard(&cardSaturn{}, slot)
|
||||
}
|
||||
|
||||
// AddThunderClockPlusCard inserts a ThunderClock Plus clock card
|
||||
func (a *Apple2) AddThunderClockPlusCard(slot int, romFile string) error {
|
||||
var c cardThunderClockPlus
|
||||
data, err := loadResource(romFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.loadRom(data)
|
||||
a.insertCard(&c, slot)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddCardLogger inserts a fake card that logs accesses
|
||||
func (a *Apple2) AddCardLogger(slot int) {
|
||||
a.insertCard(&cardLogger{}, slot)
|
||||
}
|
||||
|
||||
// AddCardInOut inserts a fake card that interfaces with the emulator host
|
||||
func (a *Apple2) AddCardInOut(slot int) {
|
||||
a.insertCard(&cardInOut{}, slot)
|
||||
}
|
||||
|
||||
// SetKeyboardProvider attaches an external keyboard provider
|
||||
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
|
||||
a.io.setKeyboardProvider(kb)
|
||||
}
|
||||
|
||||
// SetSpeakerProvider attaches an external keyboard provider
|
||||
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
|
||||
a.io.setSpeakerProvider(s)
|
||||
}
|
||||
|
||||
// SetJoysticksProvider attaches an external joysticks provider
|
||||
func (a *Apple2) SetJoysticksProvider(j JoysticksProvider) {
|
||||
a.io.setJoysticksProvider(j)
|
||||
}
|
99
apple2Tester.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
)
|
||||
|
||||
type terminateConditionFunc func(a *Apple2) bool
|
||||
|
||||
type apple2Tester struct {
|
||||
a *Apple2
|
||||
terminateCondition terminateConditionFunc
|
||||
}
|
||||
|
||||
func makeApple2Tester(model string, overrides *configuration) (*apple2Tester, error) {
|
||||
models, _, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := models.getWithOverrides(model, overrides)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.set(confSpeed, "full")
|
||||
a, err := configure(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var at apple2Tester
|
||||
a.addTracer(&at)
|
||||
return &at, nil
|
||||
}
|
||||
|
||||
func (at *apple2Tester) connect(a *Apple2) {
|
||||
at.a = a
|
||||
}
|
||||
|
||||
func (at *apple2Tester) inspect() {
|
||||
if at.terminateCondition(at.a) {
|
||||
at.a.SendCommand(CommandKill)
|
||||
}
|
||||
}
|
||||
|
||||
func (at *apple2Tester) run() {
|
||||
at.a.Run()
|
||||
}
|
||||
|
||||
func (at *apple2Tester) getText() string {
|
||||
return screen.RenderTextModeString(at.a, false, false, false, at.a.isApple2e)
|
||||
}
|
||||
|
||||
func (at *apple2Tester) getText80() string {
|
||||
return screen.RenderTextModeString(at.a, true, false, false, at.a.isApple2e)
|
||||
}
|
||||
|
||||
/*
|
||||
func buildTerminateConditionCycles(cycles uint64) terminateConditionFunc {
|
||||
return func(a *Apple2) bool {
|
||||
return a.cpu.GetCycles() > cycles
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const textCheckInterval = uint64(100_000)
|
||||
|
||||
func buildTerminateConditionText(at *apple2Tester, needle string, col80 bool, timeoutCycles uint64) terminateConditionFunc {
|
||||
needles := []string{needle}
|
||||
return buildTerminateConditionTexts(at, needles, col80, timeoutCycles)
|
||||
}
|
||||
|
||||
func buildTerminateConditionTexts(at *apple2Tester, needles []string, col80 bool, timeoutCycles uint64) terminateConditionFunc {
|
||||
lastCheck := uint64(0)
|
||||
found := false
|
||||
return func(a *Apple2) bool {
|
||||
cycles := a.cpu.GetCycles()
|
||||
if cycles > timeoutCycles {
|
||||
return true
|
||||
}
|
||||
if cycles-lastCheck > textCheckInterval {
|
||||
lastCheck = cycles
|
||||
var text string
|
||||
if col80 {
|
||||
text = at.getText80()
|
||||
} else {
|
||||
text = at.getText()
|
||||
}
|
||||
for _, needle := range needles {
|
||||
if !strings.Contains(text, needle) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
found = true
|
||||
}
|
||||
return found
|
||||
}
|
||||
}
|
241
apple2main.go
|
@ -1,241 +0,0 @@
|
|||
package apple2
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
)
|
||||
|
||||
const defaultInternal = "<default>"
|
||||
|
||||
// MainApple is a device independant main. Video, keyboard and speaker won't be defined
|
||||
func MainApple() *Apple2 {
|
||||
romFile := flag.String(
|
||||
"rom",
|
||||
defaultInternal,
|
||||
"main rom file")
|
||||
disk2RomFile := flag.String(
|
||||
"diskRom",
|
||||
"<internal>/DISK2.rom",
|
||||
"rom file for the disk drive controller")
|
||||
disk2Slot := flag.Int(
|
||||
"disk2Slot",
|
||||
6,
|
||||
"slot for the disk driver. -1 for none.")
|
||||
diskImage := flag.String(
|
||||
"disk",
|
||||
"<internal>/dos33.dsk",
|
||||
"file to load on the first disk drive")
|
||||
wozImage := flag.String(
|
||||
"woz",
|
||||
"",
|
||||
"show WOZ file information")
|
||||
hardDiskImage := flag.String(
|
||||
"hd",
|
||||
"",
|
||||
"file to load on the hard disk")
|
||||
hardDiskSlot := flag.Int(
|
||||
"hdSlot",
|
||||
-1,
|
||||
"slot for the hard drive if present. -1 for none.")
|
||||
cpuClock := flag.Float64(
|
||||
"mhz",
|
||||
CPUClockMhz,
|
||||
"cpu speed in Mhz, use 0 for full speed. Use F5 to toggle.")
|
||||
charRomFile := flag.String(
|
||||
"charRom",
|
||||
defaultInternal,
|
||||
"rom file for the character generator")
|
||||
languageCardSlot := flag.Int(
|
||||
"languageCardSlot",
|
||||
0,
|
||||
"slot for the 16kb language card. -1 for none")
|
||||
saturnCardSlot := flag.Int(
|
||||
"saturnCardSlot",
|
||||
-1,
|
||||
"slot for the 256kb Saturn card. -1 for none")
|
||||
vidHDCardSlot := flag.Int(
|
||||
"vidHDSlot",
|
||||
2,
|
||||
"slot for the VidHD card, only for //e models. -1 for none")
|
||||
fastChipCardSlot := flag.Int(
|
||||
"fastChipSlot",
|
||||
3,
|
||||
"slot for the FASTChip accelerator card, -1 for none")
|
||||
thunderClockCardSlot := flag.Int(
|
||||
"thunderClockCardSlot",
|
||||
4,
|
||||
"slot for the ThunderClock Plus card. -1 for none")
|
||||
mono := flag.Bool(
|
||||
"mono",
|
||||
false,
|
||||
"emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle.")
|
||||
fastDisk := flag.Bool(
|
||||
"fastDisk",
|
||||
true,
|
||||
"set fast mode when the disks are spinning")
|
||||
panicSS := flag.Bool(
|
||||
"panicSS",
|
||||
false,
|
||||
"panic if a not implemented softswitch is used")
|
||||
traceCPU := flag.Bool(
|
||||
"traceCpu",
|
||||
false,
|
||||
"dump to the console the CPU execution operations")
|
||||
traceSS := flag.Bool(
|
||||
"traceSS",
|
||||
false,
|
||||
"dump to the console the sofswitches calls")
|
||||
traceHD := flag.Bool(
|
||||
"traceHD",
|
||||
false,
|
||||
"dump to the console the hd commands")
|
||||
dumpChars := flag.Bool(
|
||||
"dumpChars",
|
||||
false,
|
||||
"shows the character map")
|
||||
model := flag.String(
|
||||
"model",
|
||||
"2enh",
|
||||
"set base model. Models available 2plus, 2e, 2enh, base64a")
|
||||
profile := flag.Bool(
|
||||
"profile",
|
||||
false,
|
||||
"generate profile trace to analyse with pprof")
|
||||
flag.Parse()
|
||||
|
||||
if *wozImage != "" {
|
||||
f, err := loadFileWoz(*wozImage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.dump()
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
var a *Apple2
|
||||
var charGenMap charColumnMap
|
||||
initialCharGenPage := 0
|
||||
switch *model {
|
||||
case "2plus":
|
||||
a = newApple2plus()
|
||||
if *romFile == defaultInternal {
|
||||
*romFile = "<internal>/Apple2_Plus.rom"
|
||||
}
|
||||
if *charRomFile == defaultInternal {
|
||||
*charRomFile = "<internal>/Apple2rev7CharGen.rom"
|
||||
}
|
||||
charGenMap = charGenColumnsMap2Plus
|
||||
*vidHDCardSlot = -1
|
||||
|
||||
case "2e":
|
||||
a = newApple2e()
|
||||
if *romFile == defaultInternal {
|
||||
*romFile = "<internal>/Apple2e.rom"
|
||||
}
|
||||
if *charRomFile == defaultInternal {
|
||||
*charRomFile = "<internal>/Apple IIe Video Unenhanced - 342-0133-A - 2732.bin"
|
||||
}
|
||||
a.isApple2e = true
|
||||
charGenMap = charGenColumnsMap2e
|
||||
|
||||
case "2enh":
|
||||
a = newApple2eEnhanced()
|
||||
if *romFile == defaultInternal {
|
||||
*romFile = "<internal>/Apple2e_Enhanced.rom"
|
||||
}
|
||||
if *charRomFile == defaultInternal {
|
||||
*charRomFile = "<internal>/Apple IIe Video Enhanced - 342-0265-A - 2732.bin"
|
||||
}
|
||||
a.isApple2e = true
|
||||
charGenMap = charGenColumnsMap2e
|
||||
|
||||
case "base64a":
|
||||
a = newBase64a()
|
||||
if *romFile == defaultInternal {
|
||||
err := loadBase64aRom(a)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*romFile = ""
|
||||
}
|
||||
if *charRomFile == defaultInternal {
|
||||
*charRomFile = "<internal>/BASE64A_ROM7_CharGen.BIN"
|
||||
initialCharGenPage = 1
|
||||
}
|
||||
charGenMap = charGenColumnsMapBase64a
|
||||
*vidHDCardSlot = -1
|
||||
|
||||
default:
|
||||
panic("Model not supported")
|
||||
}
|
||||
|
||||
a.setup(!*mono, *cpuClock, *fastDisk)
|
||||
a.cpu.SetTrace(*traceCPU)
|
||||
a.io.setTrace(*traceSS)
|
||||
a.io.setPanicNotImplemented(*panicSS)
|
||||
a.setProfile(*profile)
|
||||
|
||||
// Load ROM if not loaded already
|
||||
if *romFile != "" {
|
||||
err := a.LoadRom(*romFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load character generator if it loaded already
|
||||
cg, err := newCharacterGenerator(*charRomFile, charGenMap)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cg.setPage(initialCharGenPage)
|
||||
a.cg = cg
|
||||
|
||||
// Externsion cards
|
||||
if *languageCardSlot >= 0 {
|
||||
a.AddLanguageCard(*languageCardSlot)
|
||||
}
|
||||
if *saturnCardSlot >= 0 {
|
||||
a.AddSaturnCard(*saturnCardSlot)
|
||||
}
|
||||
if *thunderClockCardSlot > 0 {
|
||||
err := a.AddThunderClockPlusCard(*thunderClockCardSlot, "<internal>/ThunderclockPlusROM.bin")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if *vidHDCardSlot >= 0 {
|
||||
a.AddVidHD(*vidHDCardSlot)
|
||||
}
|
||||
if *fastChipCardSlot >= 0 {
|
||||
a.AddFastChip(*fastChipCardSlot)
|
||||
}
|
||||
if *disk2Slot > 0 {
|
||||
err := a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if *hardDiskImage != "" {
|
||||
if *hardDiskSlot <= 0 {
|
||||
// If there is a hard disk image, but no slot assigned, use slot 7.
|
||||
*hardDiskSlot = 7
|
||||
}
|
||||
err := a.AddHardDisk(*hardDiskSlot, *hardDiskImage, *traceHD)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//a.AddCardInOut(2)
|
||||
//a.AddCardLogger(4)
|
||||
|
||||
if *dumpChars {
|
||||
a.cg.Dump()
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/apple2"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := apple2.MainApple()
|
||||
if a != nil {
|
||||
SDLRun(a)
|
||||
}
|
||||
}
|
||||
|
||||
// SDLRun starts the Apple2 emulator on SDL
|
||||
func SDLRun(a *apple2.Apple2) {
|
||||
|
||||
window, renderer, err := sdl.CreateWindowAndRenderer(4*40*7, 4*24*8,
|
||||
sdl.WINDOW_SHOWN)
|
||||
if err != nil {
|
||||
panic("Failed to create window")
|
||||
}
|
||||
window.SetResizable(true)
|
||||
|
||||
defer window.Destroy()
|
||||
defer renderer.Destroy()
|
||||
window.SetTitle(a.Name)
|
||||
|
||||
kp := newSDLKeyBoard(a)
|
||||
a.SetKeyboardProvider(kp)
|
||||
|
||||
s := newSDLSpeaker()
|
||||
s.start()
|
||||
a.SetSpeakerProvider(s)
|
||||
|
||||
j := newSDLJoysticks()
|
||||
a.SetJoysticksProvider(j)
|
||||
|
||||
go a.Run()
|
||||
|
||||
running := true
|
||||
for running {
|
||||
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
||||
switch t := event.(type) {
|
||||
case *sdl.QuitEvent:
|
||||
a.SendCommand(apple2.CommandKill)
|
||||
running = false
|
||||
case *sdl.KeyboardEvent:
|
||||
kp.putKey(t)
|
||||
j.putKey(t)
|
||||
case *sdl.TextInputEvent:
|
||||
kp.putText(t)
|
||||
case *sdl.JoyAxisEvent:
|
||||
j.putAxisEvent(t)
|
||||
case *sdl.JoyButtonEvent:
|
||||
j.putButtonEvent(t)
|
||||
}
|
||||
}
|
||||
|
||||
img := apple2.Snapshot(a)
|
||||
if img != nil {
|
||||
surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]),
|
||||
int32(img.Bounds().Dx()), int32(img.Bounds().Dy()),
|
||||
32, 4*img.Bounds().Dx(),
|
||||
0x0000ff, 0x0000ff00, 0x00ff0000, 0xff000000)
|
||||
// Valid for little endian. Should we reverse for big endian?
|
||||
// 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
texture, err := renderer.CreateTextureFromSurface(surface)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
renderer.Clear()
|
||||
w, h := window.GetSize()
|
||||
renderer.Copy(texture, nil, &sdl.Rect{X: 0, Y: 0, W: w, H: h})
|
||||
renderer.Present()
|
||||
|
||||
surface.Free()
|
||||
texture.Destroy()
|
||||
}
|
||||
sdl.Delay(1000 / 30)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/ivanizag/apple2"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
type sdlKeyboard struct {
|
||||
keyChannel chan uint8
|
||||
a *apple2.Apple2
|
||||
}
|
||||
|
||||
func newSDLKeyBoard(a *apple2.Apple2) *sdlKeyboard {
|
||||
var k sdlKeyboard
|
||||
k.keyChannel = make(chan uint8, 100)
|
||||
k.a = a
|
||||
return &k
|
||||
}
|
||||
|
||||
func (k *sdlKeyboard) putText(textEvent *sdl.TextInputEvent) {
|
||||
text := textEvent.GetText()
|
||||
|
||||
for _, ch := range text {
|
||||
// We will use computed text only for printable ASCII chars
|
||||
if ch < ' ' || ch > '~' {
|
||||
continue
|
||||
}
|
||||
|
||||
buf := make([]uint8, 1)
|
||||
utf8.EncodeRune(buf, ch)
|
||||
|
||||
k.putChar(buf[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
/*
|
||||
See "Apple II reference manual", page 5
|
||||
|
||||
To get keys as understood by the Apple2 hardware run:
|
||||
10 A=PEEK(49152)
|
||||
20 PRINT A, A - 128
|
||||
30 GOTO 10
|
||||
*/
|
||||
if keyEvent.Type != sdl.KEYDOWN {
|
||||
// Process only key pushes
|
||||
return
|
||||
}
|
||||
|
||||
key := keyEvent.Keysym
|
||||
ctrl := key.Mod&sdl.KMOD_CTRL != 0
|
||||
|
||||
if ctrl {
|
||||
if key.Sym >= 'a' && key.Sym <= 'z' {
|
||||
k.putChar(uint8(key.Sym) - 97 + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := uint8(0)
|
||||
|
||||
switch key.Sym {
|
||||
case sdl.K_ESCAPE:
|
||||
result = 27
|
||||
case sdl.K_BACKSPACE:
|
||||
result = 8
|
||||
case sdl.K_RETURN:
|
||||
result = 13
|
||||
case sdl.K_RETURN2:
|
||||
result = 13
|
||||
case sdl.K_LEFT:
|
||||
if ctrl {
|
||||
result = 31 // Base64A
|
||||
}
|
||||
result = 8
|
||||
case sdl.K_RIGHT:
|
||||
result = 21
|
||||
|
||||
// Apple //e
|
||||
case sdl.K_UP:
|
||||
result = 11 // 31 in the Base64A
|
||||
case sdl.K_DOWN:
|
||||
result = 10
|
||||
case sdl.K_TAB:
|
||||
result = 9
|
||||
case sdl.K_DELETE:
|
||||
result = 127 // 24 in the Base64A
|
||||
|
||||
// Base64A clone particularities
|
||||
case sdl.K_F2:
|
||||
result = 127 // Base64A
|
||||
|
||||
// Control of the emulator
|
||||
case sdl.K_F1:
|
||||
if ctrl {
|
||||
k.a.SendCommand(apple2.CommandReset)
|
||||
}
|
||||
case sdl.K_F5:
|
||||
if ctrl {
|
||||
k.a.SendCommand(apple2.CommandShowSpeed)
|
||||
} else {
|
||||
k.a.SendCommand(apple2.CommandToggleSpeed)
|
||||
}
|
||||
case sdl.K_F6:
|
||||
k.a.SendCommand(apple2.CommandToggleColor)
|
||||
case sdl.K_F7:
|
||||
k.a.SendCommand(apple2.CommandSaveState)
|
||||
case sdl.K_F8:
|
||||
k.a.SendCommand(apple2.CommandLoadState)
|
||||
case sdl.K_F9:
|
||||
k.a.SendCommand(apple2.CommandDumpDebugInfo)
|
||||
case sdl.K_F10:
|
||||
k.a.SendCommand(apple2.CommandNextCharGenPage)
|
||||
case sdl.K_F11:
|
||||
k.a.SendCommand(apple2.CommandToggleCPUTrace)
|
||||
case sdl.K_F12:
|
||||
err := apple2.SaveSnapshot(k.a, "snapshot.png")
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving snapshoot: %v.\n.", err)
|
||||
} else {
|
||||
fmt.Println("Saving snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
// Missing values 91 to 95. Usually control for [\]^_
|
||||
// On the Base64A it's control for \]./
|
||||
|
||||
if result != 0 {
|
||||
k.putChar(result)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *sdlKeyboard) putChar(ch uint8) {
|
||||
k.keyChannel <- ch
|
||||
}
|
||||
|
||||
func (k *sdlKeyboard) GetKey(_ bool) (key uint8, ok bool) {
|
||||
select {
|
||||
case key = <-k.keyChannel:
|
||||
ok = true
|
||||
default:
|
||||
ok = false
|
||||
}
|
||||
return
|
||||
}
|
78
base64a.go
|
@ -1,29 +1,11 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ivanizag/apple2/core6502"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
Copam BASE64A adaptation.
|
||||
*/
|
||||
|
||||
// newBase64a instantiates an apple2
|
||||
func newBase64a() *Apple2 {
|
||||
var a Apple2
|
||||
|
||||
a.Name = "Base 64A"
|
||||
a.mmu = newMemoryManager(&a)
|
||||
a.cpu = core6502.NewNMOS6502(a.mmu)
|
||||
a.io = newIoC0Page(&a)
|
||||
addApple2SoftSwitches(a.io)
|
||||
addBase64aSoftSwitches(a.io)
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
const (
|
||||
// There are 6 ROM chips. Each can have 4Kb or 8Kb. They can fill
|
||||
// 2 or 4 banks with 2kb windows.
|
||||
|
@ -42,7 +24,7 @@ func loadBase64aRom(a *Apple2) error {
|
|||
|
||||
for i := 0; i < base64aRomChipCount; i++ {
|
||||
filename := fmt.Sprintf("<internal>/BASE64A_%X.BIN", 0xd0+i*0x08)
|
||||
data, err := loadResource(filename)
|
||||
data, _, err := LoadResource(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -52,48 +34,52 @@ func loadBase64aRom(a *Apple2) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Create banks
|
||||
for j := range romBanksBytes {
|
||||
a.mmu.physicalROM[j] = newMemoryRange(0xd000, romBanksBytes[j])
|
||||
// Create paged ROM
|
||||
romData := make([]uint8, 0, base64aRomBankSize*base64aRomBankCount)
|
||||
for _, bank := range romBanksBytes {
|
||||
romData = append(romData, bank...)
|
||||
}
|
||||
rom := newMemoryRangePagedROM(0xd000, romData, "Base64 ROM", base64aRomBankCount)
|
||||
|
||||
// Start with first bank active
|
||||
a.mmu.setActiveROMPage(0)
|
||||
|
||||
rom.setPage(0)
|
||||
a.mmu.physicalROM = rom
|
||||
return nil
|
||||
}
|
||||
|
||||
func addBase64aSoftSwitches(io *ioC0Page) {
|
||||
// Other softswitches, not implemented but called from the ROM
|
||||
io.addSoftSwitchW(0x0C, notImplementedSoftSwitchW, "80COLOFF")
|
||||
io.addSoftSwitchW(0x0E, notImplementedSoftSwitchW, "ALTCHARSETOFF")
|
||||
|
||||
// Write on the speaker. That is a double access and should do nothing
|
||||
// but works somehow on the BASE64A
|
||||
io.addSoftSwitchW(0x30, func(io *ioC0Page, value uint8) {
|
||||
speakerSoftSwitch(io)
|
||||
}, "SPEAKER")
|
||||
io.addSoftSwitchW(0x0C, buildNotImplementedSoftSwitchW(io), "80COLOFF")
|
||||
io.addSoftSwitchW(0x0E, buildNotImplementedSoftSwitchW(io), "ALTCHARSETOFF")
|
||||
|
||||
// ROM pagination softswitches. They use the annunciator 0 and 1
|
||||
mmu := io.apple2.mmu
|
||||
io.addSoftSwitchRW(0x58, func(*ioC0Page) uint8 {
|
||||
p := mmu.getActiveROMPage()
|
||||
mmu.setActiveROMPage(p & 2)
|
||||
io.addSoftSwitchRW(0x58, func() uint8 {
|
||||
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
|
||||
p := rom.getPage()
|
||||
rom.setPage(p & 2)
|
||||
}
|
||||
return 0
|
||||
}, "ANN0OFF-ROM")
|
||||
io.addSoftSwitchRW(0x59, func(*ioC0Page) uint8 {
|
||||
p := mmu.getActiveROMPage()
|
||||
mmu.setActiveROMPage(p | 1)
|
||||
io.addSoftSwitchRW(0x59, func() uint8 {
|
||||
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
|
||||
p := rom.getPage()
|
||||
rom.setPage(p | 1)
|
||||
}
|
||||
return 0
|
||||
}, "ANN0ON-ROM")
|
||||
io.addSoftSwitchRW(0x5A, func(*ioC0Page) uint8 {
|
||||
p := mmu.getActiveROMPage()
|
||||
mmu.setActiveROMPage(p & 1)
|
||||
io.addSoftSwitchRW(0x5A, func() uint8 {
|
||||
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
|
||||
p := rom.getPage()
|
||||
rom.setPage(p & 1)
|
||||
}
|
||||
return 0
|
||||
}, "ANN1OFF-ROM")
|
||||
io.addSoftSwitchRW(0x5B, func(*ioC0Page) uint8 {
|
||||
p := mmu.getActiveROMPage()
|
||||
mmu.setActiveROMPage(p | 2)
|
||||
io.addSoftSwitchRW(0x5B, func() uint8 {
|
||||
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
|
||||
p := rom.getPage()
|
||||
rom.setPage(p | 2)
|
||||
}
|
||||
return 0
|
||||
}, "ANN1ON-ROM")
|
||||
|
||||
|
|
186
cardBase.go
|
@ -1,19 +1,29 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type card interface {
|
||||
loadRom(data []uint8)
|
||||
// Card represents an Apple II card to be inserted in a slot
|
||||
type Card interface {
|
||||
loadRom(data []uint8, layout cardRomLayout) error
|
||||
assign(a *Apple2, slot int)
|
||||
persistent
|
||||
reset()
|
||||
|
||||
setName(name string)
|
||||
setDebug(debug bool)
|
||||
GetName() string
|
||||
GetInfo() map[string]string
|
||||
}
|
||||
|
||||
type cardBase struct {
|
||||
a *Apple2
|
||||
rom *memoryRange
|
||||
romExtra *memoryRange
|
||||
a *Apple2
|
||||
name string
|
||||
trace bool
|
||||
romCsxx *memoryRangeROM
|
||||
romC8xx memoryHandler
|
||||
romCxxx memoryHandler
|
||||
|
||||
slot int
|
||||
_ssr [16]softSwitchR
|
||||
_ssw [16]softSwitchW
|
||||
|
@ -21,34 +31,126 @@ type cardBase struct {
|
|||
_sswName [16]string
|
||||
}
|
||||
|
||||
func (c *cardBase) loadRom(data []uint8) {
|
||||
func (c *cardBase) setName(name string) {
|
||||
c.name = name
|
||||
}
|
||||
|
||||
func (c *cardBase) GetName() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *cardBase) GetInfo() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cardBase) setDebug(debug bool) {
|
||||
c.trace = debug
|
||||
}
|
||||
|
||||
func (c *cardBase) reset() {
|
||||
// nothing
|
||||
}
|
||||
|
||||
type cardRomLayout int
|
||||
|
||||
const (
|
||||
cardRomSimple cardRomLayout = iota // The ROM is on the slot area, there can be more than one page
|
||||
cardRomUpper // The ROM is on the full C800 area. The slot area copies C8xx
|
||||
cardRomUpperHalfEnd // The ROM is on half of the C800 areas. The slot area copies CBxx
|
||||
cardRomFull // The ROM is on the full Cxxx area, with pages for each slot position
|
||||
)
|
||||
|
||||
func (c *cardBase) loadRomFromResource(resource string, layout cardRomLayout) error {
|
||||
data, _, err := LoadResource(resource)
|
||||
if err != nil {
|
||||
// The resource should be internal and never fail
|
||||
return err
|
||||
}
|
||||
err = c.loadRom(data, layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cardBase) loadRom(data []uint8, layout cardRomLayout) error {
|
||||
if c.a != nil {
|
||||
panic("Assert failed. Rom must be loaded before inserting the card in the slot")
|
||||
return fmt.Errorf("ROM must be loaded before inserting the card in the slot")
|
||||
}
|
||||
if len(data) >= 0x100 {
|
||||
c.rom = newMemoryRange(0, data)
|
||||
}
|
||||
if len(data) >= 0x800 {
|
||||
c.romExtra = newMemoryRange(0, data)
|
||||
switch layout {
|
||||
case cardRomSimple:
|
||||
if len(data) == 0x100 {
|
||||
// Just 256 bytes in Cs00
|
||||
c.romCsxx = newMemoryRangeROM(0, data, "Slot ROM")
|
||||
} else if len(data)%0x100 == 0 {
|
||||
// The ROM covers many 256 bytes pages of Csxx
|
||||
// Used on the Dan 2 controller card
|
||||
c.romCsxx = newMemoryRangePagedROM(0, data, "Slot paged ROM", uint8(len(data)/0x100))
|
||||
} else {
|
||||
return fmt.Errorf("invalid ROM size for simple layout")
|
||||
}
|
||||
case cardRomUpper:
|
||||
if len(data) == 0x800 {
|
||||
// The file has C800 to CFFF
|
||||
// The 256 bytes in Cx00 are copied from the first page in C800
|
||||
c.romCsxx = newMemoryRangeROM(0, data, "Slot ROM")
|
||||
c.romC8xx = newMemoryRangeROM(0xc800, data, "Slot C8 ROM")
|
||||
} else {
|
||||
return fmt.Errorf("invalid ROM size for upper layout")
|
||||
}
|
||||
case cardRomUpperHalfEnd:
|
||||
if len(data) == 0x400 {
|
||||
// The file has C800 to CBFF for ROM
|
||||
// The 256 bytes in Cx00 are copied from the last page in C800-CBFF
|
||||
// Used on the Videx 80 columns card
|
||||
c.romCsxx = newMemoryRangeROM(0, data[0x300:], "Slot ROM")
|
||||
c.romC8xx = newMemoryRangeROM(0xc800, data, "Slot C8 ROM")
|
||||
} else {
|
||||
return fmt.Errorf("invalid ROM size for upper half end layout")
|
||||
}
|
||||
case cardRomFull:
|
||||
if len(data) == 0x1000 {
|
||||
// The file covers the full Cxxx range. Only showing the page
|
||||
// corresponding to the slot used.
|
||||
c.romCxxx = newMemoryRangeROM(0xc000, data, "Slot ROM")
|
||||
} else if len(data)%0x1000 == 0 {
|
||||
// The ROM covers the full Cxxx range with several pages
|
||||
c.romCxxx = newMemoryRangePagedROM(0xc000, data, "Slot paged ROM", uint8(len(data)/0x1000))
|
||||
} else {
|
||||
return fmt.Errorf("invalid ROM size for full layout")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid card ROM layout")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cardBase) assign(a *Apple2, slot int) {
|
||||
c.a = a
|
||||
c.slot = slot
|
||||
if slot != 0 && c.rom != nil {
|
||||
c.rom.base = uint16(0xc000 + slot*0x100)
|
||||
a.mmu.setCardROM(slot, c.rom)
|
||||
}
|
||||
|
||||
if slot != 0 && c.romExtra != nil {
|
||||
c.romExtra.base = uint16(0xc800)
|
||||
a.mmu.setCardROMExtra(slot, c.romExtra)
|
||||
if slot != 0 {
|
||||
if c.romCsxx != nil {
|
||||
// Relocate to the assigned slot
|
||||
c.romCsxx.setBase(uint16(0xc000 + slot*0x100))
|
||||
a.mmu.setCardROM(slot, c.romCsxx)
|
||||
}
|
||||
if c.romC8xx != nil {
|
||||
a.mmu.setCardROMExtra(slot, c.romC8xx)
|
||||
}
|
||||
if c.romCxxx != nil {
|
||||
a.mmu.setCardROM(slot, c.romCxxx)
|
||||
a.mmu.setCardROMExtra(slot, c.romCxxx)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 0x10; i++ {
|
||||
a.io.addSoftSwitchR(uint8(0xC80+slot*0x10+i), c._ssr[i], c._ssrName[i])
|
||||
a.io.addSoftSwitchW(uint8(0xC80+slot*0x10+i), c._ssw[i], c._sswName[i])
|
||||
if c._ssr[i] != nil {
|
||||
a.io.addSoftSwitchR(uint8(0xC80+slot*0x10+i), c._ssr[i], c._ssrName[i])
|
||||
}
|
||||
if c._ssw[i] != nil {
|
||||
a.io.addSoftSwitchW(uint8(0xC80+slot*0x10+i), c._ssw[i], c._sswName[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,12 +164,34 @@ func (c *cardBase) addCardSoftSwitchW(address uint8, ss softSwitchW, name string
|
|||
c._sswName[address] = name
|
||||
}
|
||||
|
||||
func (c *cardBase) save(w io.Writer) error {
|
||||
// Empty
|
||||
return nil
|
||||
func (c *cardBase) addCardSoftSwitchRW(address uint8, ss softSwitchR, name string) {
|
||||
c._ssr[address] = ss
|
||||
c._ssrName[address] = name
|
||||
|
||||
c._ssw[address] = func(uint8) {
|
||||
ss()
|
||||
}
|
||||
c._sswName[address] = name
|
||||
}
|
||||
|
||||
func (c *cardBase) load(r io.Reader) error {
|
||||
// Empty
|
||||
return nil
|
||||
type softSwitches func(address uint8, data uint8, write bool) uint8
|
||||
|
||||
func (c *cardBase) addCardSoftSwitches(sss softSwitches, name string) {
|
||||
|
||||
for i := uint8(0x0); i <= 0xf; i++ {
|
||||
address := i
|
||||
c.addCardSoftSwitchR(address, func() uint8 {
|
||||
return sss(address, 0, false)
|
||||
}, fmt.Sprintf("%v%XR", name, address))
|
||||
c.addCardSoftSwitchW(address, func(value uint8) {
|
||||
sss(address, value, true)
|
||||
}, fmt.Sprintf("%v%XW", name, address))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cardBase) tracef(format string, args ...interface{}) {
|
||||
if c.trace {
|
||||
prefixedFormat := fmt.Sprintf("[%s] %v", c.name, format)
|
||||
fmt.Printf(prefixedFormat, args...)
|
||||
}
|
||||
}
|
||||
|
|
159
cardBrainBoard.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package izapple2
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
Brain board card for Apple II
|
||||
|
||||
See:
|
||||
http://www.willegal.net/appleii/brainboard.htm
|
||||
http://www.willegal.net/appleii/bb-v5_3.1.pdf
|
||||
|
||||
The Brain Board card has 2 banks of ROM to replace the main ROM
|
||||
|
||||
A 27c256 ROM (32k) is used, with the following mapping:
|
||||
0x0000-0x00ff: lower card rom maps to $Csxx
|
||||
0x1000-0x37ff: lower bank rom maps to $D000 to $F7FF
|
||||
0x3800-0x3fff: F8 lower bank rom maps to $F800 to $FFFF
|
||||
0x4000-0x40ff: upper card rom maps to $Csxx
|
||||
0x5000-0x77ff: upper bank rom maps to $D000 to $F7FF
|
||||
0x7800-0x7fff: F8 upper bank rom maps to $F800 to $FFFF
|
||||
|
||||
DIP SWitches_
|
||||
1-ON : The range F8 can be replaced
|
||||
1-OFF: The range F8 is not replaced
|
||||
|
||||
3-ON ,4-OFF: The motherboard ROM can be used
|
||||
3-OFF,4-ON : The motherboard ROM is always replaced
|
||||
|
||||
5-ON ,6-OFF: The lower bank is used and mapped to Bank A
|
||||
5-OFF,6-ON : The lower bank is not used (A can be motherboards or uppers)
|
||||
|
||||
7-ON ,8-OFF: The upper bank is used and mapper to bank B
|
||||
7-OFF,8-ON : The upper bank is not used (B can be motherboards or lowers)
|
||||
|
||||
|
||||
Switches and softswitches:
|
||||
Up, $COsO - SS clear: A bank selected
|
||||
Down, $COs1 - SS set: B bank selected
|
||||
|
||||
*/
|
||||
|
||||
// CardBrainBoard represents a Brain Board card
|
||||
type CardBrainBoard struct {
|
||||
cardBase
|
||||
isBankB bool
|
||||
isMotherboardRomEnabled bool
|
||||
|
||||
dip1_replaceF8 bool
|
||||
dip34_useMotherboardRom bool
|
||||
dip56_lowerInA bool
|
||||
dip78_upperInB bool
|
||||
|
||||
rom []uint8
|
||||
}
|
||||
|
||||
func newCardBrainBoardBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Brain Board",
|
||||
description: "Firmware card. It has two ROM banks",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"rom", "ROM file to load", "<internal>/wozaniam_integer.rom"},
|
||||
{"dips", "DIP switches, leftmost is DIP 1", "1-011010"},
|
||||
{"switch", "Bank selected at boot, 'up' or 'down'", "up"},
|
||||
},
|
||||
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardBrainBoard
|
||||
var err error
|
||||
|
||||
bank := paramsGetString(params, "switch")
|
||||
if bank == "up" {
|
||||
c.isBankB = false
|
||||
} else if bank == "down" {
|
||||
c.isBankB = true
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid bank '%s', must be up or down", bank)
|
||||
}
|
||||
|
||||
dips, err := paramsGetDIPs(params, "dips", 8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.dip1_replaceF8 = dips[1]
|
||||
if dips[3] == dips[4] {
|
||||
return nil, fmt.Errorf("DIP switches 3 and 4 must be different")
|
||||
}
|
||||
if dips[5] == dips[6] {
|
||||
return nil, fmt.Errorf("DIP switches 5 and 6 must be different")
|
||||
}
|
||||
if dips[7] == dips[8] {
|
||||
return nil, fmt.Errorf("DIP switches 7 and 8 must be different")
|
||||
}
|
||||
|
||||
c.dip34_useMotherboardRom = dips[3]
|
||||
c.dip56_lowerInA = dips[5]
|
||||
c.dip78_upperInB = dips[7]
|
||||
|
||||
romFile := paramsGetPath(params, "rom")
|
||||
data, _, err := LoadResource(romFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) != 0x8000 {
|
||||
return nil, fmt.Errorf("the ROM file for the Brainboard must be 32k")
|
||||
}
|
||||
|
||||
c.isMotherboardRomEnabled = true
|
||||
c.rom = data
|
||||
c.romCxxx = &c
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardBrainBoard) updateState() {
|
||||
isMotherboardRomEnabled := c.dip34_useMotherboardRom &&
|
||||
((!c.dip56_lowerInA && !c.isBankB) || (!c.dip78_upperInB && c.isBankB))
|
||||
|
||||
if isMotherboardRomEnabled && !c.isMotherboardRomEnabled {
|
||||
c.a.mmu.inhibitROM(nil)
|
||||
} else if !isMotherboardRomEnabled && c.isMotherboardRomEnabled {
|
||||
c.a.mmu.inhibitROM(c)
|
||||
}
|
||||
|
||||
c.isMotherboardRomEnabled = isMotherboardRomEnabled
|
||||
}
|
||||
|
||||
func (c *CardBrainBoard) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchRW(0, func() uint8 {
|
||||
c.isBankB = false
|
||||
c.updateState()
|
||||
return 0x55
|
||||
}, "BRAINCLEAR")
|
||||
|
||||
c.addCardSoftSwitchRW(1, func() uint8 {
|
||||
c.isBankB = true
|
||||
c.updateState()
|
||||
return 0x55
|
||||
}, "BRAINSET")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
c.updateState()
|
||||
}
|
||||
|
||||
func (c *CardBrainBoard) translateAddress(address uint16) uint16 {
|
||||
if c.isBankB {
|
||||
return address - 0xc000 + 0x4000
|
||||
} else {
|
||||
return address - 0xc000
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardBrainBoard) peek(address uint16) uint8 {
|
||||
return c.rom[c.translateAddress(address)]
|
||||
}
|
||||
|
||||
func (c *CardBrainBoard) poke(address uint16, value uint8) {
|
||||
// Nothing
|
||||
}
|
122
cardBrainBoard2.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package izapple2
|
||||
|
||||
/*
|
||||
Brain board II card for Apple II
|
||||
|
||||
See:
|
||||
http://www.willegal.net/appleii/brainboard.htm
|
||||
http://www.willegal.net/appleii/bb-v5_3.1.pdf
|
||||
https://github.com/Alex-Kw/Brain-Board-II
|
||||
|
||||
The Brain Board II card has 4 banks instead of the 2 of the
|
||||
original Brain Board:
|
||||
Bank 0: SS clear, DIP2 OFF (wozaniam)
|
||||
Bank 1: SS set, DIP2 OFF (applesoft)
|
||||
Bank 2: SS clear, DIP2 ON (wozaniam)
|
||||
Bank 3: SS set, DIP2 ON (integer)
|
||||
|
||||
The card is emulated as having the DIP switches as follows:
|
||||
1 - ON: The range F8 can be replaced
|
||||
2 - ON: Select the two top banks
|
||||
3 - OFF: The motherboard ROM is always replaced
|
||||
4 - ON: The softswitch selects low or high bank
|
||||
|
||||
Softswitches:
|
||||
$COsO - SS clear: Low bank selected
|
||||
$COs1 - SS set: High bank selected
|
||||
|
||||
Operation:
|
||||
The card boots on wozaniam. Use CAPS LOCK for the commands
|
||||
to work. Starts with left arrow.
|
||||
To siwtch to Integer BASIC, type:
|
||||
1000:AD 91 C0 6C FC FF
|
||||
R
|
||||
To return to wozaniam, type:
|
||||
CALL -151
|
||||
1000:AD 90 C0 6C FD FF
|
||||
1000G
|
||||
*/
|
||||
|
||||
// CardBrainBoardII represents a Brain Board II card
|
||||
type CardBrainBoardII struct {
|
||||
cardBase
|
||||
highBank bool
|
||||
dip2 bool
|
||||
rom []uint8
|
||||
pages int
|
||||
forceBank int
|
||||
}
|
||||
|
||||
const noForceBank = -1
|
||||
|
||||
func newCardBrainBoardIIBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Brain Board II",
|
||||
description: "Firmware card. It has up to four ROM banks",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"rom", "ROM file to load", "<internal>/ApplesoftInteger.BIN"},
|
||||
{"dip2", "Use the upper half of the ROM", "true"},
|
||||
{"bank", "Force a ROM bank, 'no' or bank number", "no"}},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardBrainBoardII
|
||||
var err error
|
||||
c.highBank = false // Start with wozaniam by default
|
||||
c.dip2 = paramsGetBool(params, "dip2")
|
||||
c.forceBank, err = paramsGetInt(params, "bank")
|
||||
if err != nil {
|
||||
c.forceBank = noForceBank
|
||||
}
|
||||
|
||||
// The ROM has:xaa-wozaniam xab-applesoft xac-wozaniam xad-integer
|
||||
romFile := paramsGetPath(params, "rom")
|
||||
data, _, err := LoadResource(romFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.pages = len(data) / 0x4000
|
||||
c.rom = data
|
||||
c.romCxxx = &c
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardBrainBoardII) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchRW(0, func() uint8 {
|
||||
c.highBank = false
|
||||
return 0x55
|
||||
}, "BRAINCLEAR")
|
||||
|
||||
c.addCardSoftSwitchRW(1, func() uint8 {
|
||||
c.highBank = true
|
||||
return 0x55
|
||||
}, "BRAINSET")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
a.mmu.inhibitROM(c)
|
||||
}
|
||||
|
||||
func (c *CardBrainBoardII) translateAddress(address uint16) uint16 {
|
||||
var bank = 0
|
||||
if c.forceBank != noForceBank {
|
||||
bank = c.forceBank
|
||||
} else {
|
||||
bank = 0
|
||||
if c.highBank {
|
||||
bank += 1
|
||||
}
|
||||
if c.dip2 {
|
||||
bank += 2
|
||||
}
|
||||
}
|
||||
return address - 0xc000 + uint16(bank*0x4000)
|
||||
}
|
||||
|
||||
func (c *CardBrainBoardII) peek(address uint16) uint8 {
|
||||
return c.rom[c.translateAddress(address)]
|
||||
}
|
||||
|
||||
func (c *CardBrainBoardII) poke(address uint16, value uint8) {
|
||||
// Nothing
|
||||
}
|
50
cardBrainBoard_test.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func buildBrainBoardTester(t *testing.T, conf string) *apple2Tester {
|
||||
overrides := newConfiguration()
|
||||
overrides.set(confS2, conf)
|
||||
overrides.set(confS3, "empty")
|
||||
overrides.set(confS4, "empty")
|
||||
overrides.set(confS5, "empty")
|
||||
overrides.set(confS6, "empty")
|
||||
overrides.set(confS7, "empty")
|
||||
|
||||
at, err := makeApple2Tester("2plus", overrides)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return at
|
||||
}
|
||||
|
||||
func TestBrainBoardCardWozaniam(t *testing.T) {
|
||||
at := buildBrainBoardTester(t, "brainboard,switch=up")
|
||||
|
||||
at.terminateCondition = func(a *Apple2) bool {
|
||||
return a.cpu.GetCycles() > 10_000_000
|
||||
}
|
||||
at.run()
|
||||
|
||||
at.terminateCondition = buildTerminateConditionText(at, "_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@", false, 100_000)
|
||||
|
||||
text := at.getText()
|
||||
if !strings.Contains(text, "_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@") {
|
||||
t.Errorf("Expected screen filled with _@_@', got '%s'", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrainBoardCardIntegerBasic(t *testing.T) {
|
||||
at := buildBrainBoardTester(t, "brainboard,switch=down")
|
||||
|
||||
at.terminateCondition = buildTerminateConditionText(at, "APPLE ][\n>", false, 1_000_000)
|
||||
at.run()
|
||||
|
||||
text := at.getText()
|
||||
if !strings.Contains(text, "APPLE ][\n>") {
|
||||
t.Errorf("Expected APPLE ][' and '>', got '%s'", text)
|
||||
}
|
||||
}
|
223
cardBuilder.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type paramSpec struct {
|
||||
name string
|
||||
description string
|
||||
defaultValue string
|
||||
}
|
||||
|
||||
type cardBuilder struct {
|
||||
name string
|
||||
description string
|
||||
defaultParams *[]paramSpec
|
||||
requiresIIe bool
|
||||
buildFunc func(params map[string]string) (Card, error)
|
||||
}
|
||||
|
||||
const noCardName = "empty"
|
||||
|
||||
var commonParams = []paramSpec{
|
||||
{"trace", "Enable debug messages", "false"},
|
||||
{"tracess", "Trace softswitches", "false"},
|
||||
{"panicss", "Panic on unimplemented softswitches", "false"},
|
||||
}
|
||||
|
||||
var cardFactory map[string]*cardBuilder
|
||||
|
||||
func getCardFactory() map[string]*cardBuilder {
|
||||
if cardFactory != nil {
|
||||
return cardFactory
|
||||
}
|
||||
cardFactory = make(map[string]*cardBuilder)
|
||||
cardFactory["brainboard"] = newCardBrainBoardBuilder()
|
||||
cardFactory["brainboard2"] = newCardBrainBoardIIBuilder()
|
||||
cardFactory["dan2sd"] = newCardDan2ControllerBuilder()
|
||||
cardFactory["diskii"] = newCardDisk2Builder()
|
||||
cardFactory["diskiiseq"] = newCardDisk2SequencerBuilder()
|
||||
cardFactory["fastchip"] = newCardFastChipBuilder()
|
||||
cardFactory["fujinet"] = newCardSmartPortFujinetBuilder()
|
||||
cardFactory["inout"] = newCardInOutBuilder()
|
||||
cardFactory["language"] = newCardLanguageBuilder()
|
||||
cardFactory["softswitchlogger"] = newCardLoggerBuilder()
|
||||
cardFactory["memexp"] = newCardMemoryExpansionBuilder()
|
||||
cardFactory["mouse"] = newCardMouseBuilder()
|
||||
cardFactory["multirom"] = newMultiRomCardBuilder()
|
||||
cardFactory["parallel"] = newCardParallelPrinterBuilder()
|
||||
cardFactory["prodosromdrive"] = newCardProDOSRomDriveBuilder()
|
||||
cardFactory["prodosromcard3"] = newCardProDOSRomCard3Builder()
|
||||
//cardFactory["prodosnvramdrive"] = newCardProDOSNVRAMDriveBuilder()
|
||||
cardFactory["saturn"] = newCardSaturnBuilder()
|
||||
cardFactory["smartport"] = newCardSmartPortStorageBuilder()
|
||||
cardFactory["swyftcard"] = newCardSwyftBuilder()
|
||||
cardFactory["thunderclock"] = newCardThunderClockPlusBuilder()
|
||||
cardFactory["videx"] = newCardVidexBuilder()
|
||||
cardFactory["vidhd"] = newCardVidHDBuilder()
|
||||
return cardFactory
|
||||
}
|
||||
|
||||
func availableCards() []string {
|
||||
names := maps.Keys(getCardFactory())
|
||||
slices.Sort(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func (cb *cardBuilder) fullDefaultParams() map[string]string {
|
||||
finalParams := make(map[string]string)
|
||||
for _, commonParam := range commonParams {
|
||||
finalParams[commonParam.name] = commonParam.defaultValue
|
||||
}
|
||||
if cb.defaultParams != nil {
|
||||
for _, defaultParam := range *cb.defaultParams {
|
||||
finalParams[defaultParam.name] = defaultParam.defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
return finalParams
|
||||
}
|
||||
|
||||
func setupCard(a *Apple2, slot int, paramString string) (Card, error) {
|
||||
actualArgs := splitConfigurationString(paramString, ',')
|
||||
|
||||
cardName := actualArgs[0]
|
||||
if cardName == "" || cardName == noCardName {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
builder, ok := getCardFactory()[cardName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown card %s", cardName)
|
||||
}
|
||||
|
||||
if builder.requiresIIe && !a.isApple2e {
|
||||
return nil, fmt.Errorf("card %s requires an Apple IIe", builder.name)
|
||||
}
|
||||
|
||||
finalParams := builder.fullDefaultParams()
|
||||
for i := 1; i < len(actualArgs); i++ {
|
||||
actualArgSides := splitConfigurationString(actualArgs[i], '=')
|
||||
actualArgName := strings.ToLower(actualArgSides[0])
|
||||
|
||||
if _, ok := finalParams[actualArgName]; !ok {
|
||||
return nil, fmt.Errorf("unknown parameter %s", actualArgSides[0])
|
||||
}
|
||||
if len(actualArgSides) > 2 {
|
||||
return nil, fmt.Errorf("invalid parameter value for %s", actualArgSides[0])
|
||||
}
|
||||
if len(actualArgSides) == 1 {
|
||||
finalParams[actualArgName] = "true"
|
||||
} else {
|
||||
finalParams[actualArgName] = actualArgSides[1]
|
||||
}
|
||||
}
|
||||
|
||||
card, err := builder.buildFunc(finalParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Common parameters
|
||||
if paramsGetBool(finalParams, "tracess") {
|
||||
a.io.traceSlot(slot)
|
||||
}
|
||||
|
||||
if paramsGetBool(finalParams, "panicss") {
|
||||
a.io.panicNotImplementedSlot(slot)
|
||||
}
|
||||
|
||||
debug := paramsGetBool(finalParams, "trace")
|
||||
|
||||
card.setName(builder.name)
|
||||
card.setDebug(debug)
|
||||
card.assign(a, slot)
|
||||
a.cards[slot] = card
|
||||
return card, err
|
||||
}
|
||||
|
||||
func paramsGetBool(params map[string]string, name string) bool {
|
||||
value, ok := params[name]
|
||||
if !ok {
|
||||
value = "false"
|
||||
}
|
||||
return value == "true"
|
||||
}
|
||||
|
||||
func paramsGetString(params map[string]string, name string) string {
|
||||
value, ok := params[name]
|
||||
if !ok {
|
||||
value = ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func paramsGetPath(params map[string]string, name string) string {
|
||||
value := paramsGetString(params, name)
|
||||
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func paramsGetInt(params map[string]string, name string) (int, error) {
|
||||
value, ok := params[name]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("missing parameter %s", name)
|
||||
}
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
func paramsGetUInt8(params map[string]string, name string) (uint8, error) {
|
||||
value, ok := params[name]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("missing parameter %s", name)
|
||||
}
|
||||
result, err := strconv.ParseUint(value, 10, 8)
|
||||
return uint8(result), err
|
||||
}
|
||||
|
||||
// Returns a 1 based array of bools
|
||||
func paramsGetDIPs(params map[string]string, name string, size int) ([]bool, error) {
|
||||
value, ok := params[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing parameter %s", name)
|
||||
}
|
||||
if len(value) != 8 {
|
||||
return nil, fmt.Errorf("DIP switches must be 8 characters long")
|
||||
}
|
||||
result := make([]bool, size+1)
|
||||
for i := 0; i < 8; i++ {
|
||||
result[i+1] = value[i] == '1'
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func splitConfigurationString(s string, separator rune) []string {
|
||||
// Split by comma, but not inside quotes
|
||||
var result []string
|
||||
var current string
|
||||
inQuote := false
|
||||
for _, c := range s {
|
||||
if c == '"' {
|
||||
inQuote = !inQuote
|
||||
}
|
||||
if c == separator && !inQuote {
|
||||
result = append(result, current)
|
||||
current = ""
|
||||
} else {
|
||||
current += string(c)
|
||||
}
|
||||
}
|
||||
if current != "" {
|
||||
result = append(result, current)
|
||||
}
|
||||
return result
|
||||
}
|
17
cardBuilder_test.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package izapple2
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCardBuilder(t *testing.T) {
|
||||
cardFactory := getCardFactory()
|
||||
for name, builder := range cardFactory {
|
||||
if name != "prodosromdrive" && name != "prodosromcard3" && name != "prodosnvramdrive" {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := builder.buildFunc(builder.fullDefaultParams())
|
||||
if err != nil {
|
||||
t.Errorf("Exception building card '%s': %s", name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
54
cardCat_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testCardDetectedInternal(t *testing.T, model string, card string, cycles uint64, banner string) {
|
||||
overrides := newConfiguration()
|
||||
overrides.set(confS3, "empty")
|
||||
overrides.set(confS4, "empty")
|
||||
overrides.set(confS5, "empty")
|
||||
overrides.set(confS7, "empty")
|
||||
|
||||
overrides.set(confS2, card)
|
||||
overrides.set(confS6, "diskii,disk1=\"<internal>/Card Cat 1.0b9.dsk\"")
|
||||
|
||||
at, err := makeApple2Tester(model, overrides)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
at.terminateCondition = buildTerminateConditionText(at, banner, true, cycles)
|
||||
at.run()
|
||||
|
||||
text := at.getText80()
|
||||
if !strings.Contains(text, banner) {
|
||||
t.Errorf("Expected '%s', got '%s'", banner, text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardsDetected(t *testing.T) {
|
||||
|
||||
t.Run("test Memory Expansion card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "memexp", 50_000_000, "2 03-00-05-D0 Apple II Memory Expansion Card (SP)")
|
||||
})
|
||||
|
||||
t.Run("test Mouse card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "mouse", 50_000_000, "2 38-18-01-20 Apple II Mouse Card")
|
||||
})
|
||||
|
||||
t.Run("test Parallel printer card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "parallel", 50_000_000, "2 48-48-58-FF Apple Parallel Interface Card")
|
||||
})
|
||||
|
||||
t.Run("test ThunderClock Plus card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "thunderclock", 50_000_000, "2 FF-05-18-B8 ThunderClock Plus Card")
|
||||
})
|
||||
|
||||
// Saturn not detected
|
||||
// VidHD not detected
|
||||
// Swyftcard not compatible with Card Cat
|
||||
// Pending to try Saturn, 80col with 2plus but fails with an illegal opcode
|
||||
|
||||
}
|
604
cardDan2Controller.go
Normal file
|
@ -0,0 +1,604 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
/*
|
||||
Apple II DAN ][ CONTROLLER CARD.]
|
||||
|
||||
See:
|
||||
https://github.com/profdc9/Apple2Card
|
||||
https://github.com/ThorstenBr/Apple2Card
|
||||
https://www.applefritter.com/content/dan-sd-card-disk-controller
|
||||
|
||||
*/
|
||||
|
||||
// CardDan2Controller represents a Dan ][ controller card
|
||||
type CardDan2Controller struct {
|
||||
cardBase
|
||||
|
||||
commandBuffer []uint8
|
||||
responseBuffer []uint8
|
||||
|
||||
receivingWiteBuffer bool
|
||||
writeBuffer []uint8
|
||||
commitWrite func([]uint8) error
|
||||
|
||||
portB uint8
|
||||
portC uint8
|
||||
|
||||
slotA *cardDan2ControllerSlot
|
||||
slotB *cardDan2ControllerSlot
|
||||
|
||||
improved bool
|
||||
a2slot uint8
|
||||
}
|
||||
|
||||
type cardDan2ControllerSlot struct {
|
||||
card *CardDan2Controller
|
||||
path string
|
||||
fileNo uint8
|
||||
fileName string
|
||||
fileNameAlt string
|
||||
}
|
||||
|
||||
func newCardDan2ControllerBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Dan ][ Controller card",
|
||||
description: "Apple II Peripheral Card that Interfaces to a ATMEGA328P for SD card storage",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"improved", "Emulate improved firmware from ThorstenBr", "true"},
|
||||
{"slot1", "Image in slot 1. File for raw device, folder for fs mode using files as BLKDEV0x.PO", ""},
|
||||
{"slot1file", "Device selected in slot 1: 0 for raw device, 1 to 9 for file number", "0"},
|
||||
{"slot2", "Image in slot 2. File for raw device, folder for fs mode using files as BLKDEV0x.PO", ""},
|
||||
{"slot2file", "Device selected in slot 2: 0 for raw device, 1 to 9 for file number", "0"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardDan2Controller
|
||||
c.responseBuffer = make([]uint8, 0, 1000)
|
||||
|
||||
c.improved = paramsGetBool(params, "improved")
|
||||
|
||||
c.slotA = &cardDan2ControllerSlot{}
|
||||
c.slotA.card = &c
|
||||
c.slotA.path = params["slot1"]
|
||||
num, _ := paramsGetUInt8(params, "slot1file")
|
||||
c.slotA.fileNo = uint8(num)
|
||||
c.slotA.initializeDrive()
|
||||
|
||||
c.slotB = &cardDan2ControllerSlot{}
|
||||
c.slotB.card = &c
|
||||
c.slotB.path = params["slot2"]
|
||||
num, _ = paramsGetUInt8(params, "slot2file")
|
||||
c.slotB.fileNo = uint8(num)
|
||||
c.slotB.initializeDrive()
|
||||
|
||||
romFilename := "<internal>/Apple2CardFirmware.bin"
|
||||
if c.improved {
|
||||
romFilename = "<internal>/Apple2CardFirmwareImproved.bin"
|
||||
}
|
||||
err := c.loadRomFromResource(romFilename, cardRomSimple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitches(func(address uint8, data uint8, write bool) uint8 {
|
||||
address &= 0x03 // only A0 and A1 are connected
|
||||
if write {
|
||||
c.writeSoftSwitch(address, data)
|
||||
return 0
|
||||
} else {
|
||||
return c.readSoftSwitch(address)
|
||||
}
|
||||
}, "DAN2CONTROLLER")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) writeSoftSwitch(address uint8, data uint8) {
|
||||
switch address {
|
||||
case 0: // Port A
|
||||
if c.receivingWiteBuffer {
|
||||
c.writeBuffer = append(c.writeBuffer, data)
|
||||
if len(c.writeBuffer) == 512 {
|
||||
c.commitWrite(c.writeBuffer)
|
||||
}
|
||||
} else if c.commandBuffer == nil {
|
||||
if data == 0xac {
|
||||
c.commandBuffer = make([]uint8, 0)
|
||||
} else if data == 0x30 || data == 0x20 {
|
||||
c.tracef("Sync started to upload Atmega firmware, not supported\n")
|
||||
} else {
|
||||
c.tracef("Not supported command prefix $%02x\n", data)
|
||||
}
|
||||
} else {
|
||||
c.commandBuffer = append(c.commandBuffer, data)
|
||||
c.processCommand()
|
||||
}
|
||||
case 3: // Control
|
||||
if data&0x80 == 0 {
|
||||
bit := (data >> 1) & 0x08
|
||||
if data&1 == 0 {
|
||||
// Reset bit
|
||||
c.portC &^= uint8(1) << bit
|
||||
} else {
|
||||
// Set bit
|
||||
c.portC |= uint8(1) << bit
|
||||
}
|
||||
c.romCsxx.setPage((c.portC & 0x07) | ((c.portB << 4) & 0xf0))
|
||||
} else {
|
||||
if data != 0xfa {
|
||||
c.tracef("Not supported status %v, it must be 0xfa\n", data)
|
||||
}
|
||||
/* Sets the 8255 with status 0xfa, 1111_1010:
|
||||
1: set mode
|
||||
11: port A mode 2
|
||||
1: port A input
|
||||
1: port C(upper) input
|
||||
0: port B mode 0
|
||||
1: port B input
|
||||
0: port C(lower) output
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) readSoftSwitch(address uint8) uint8 {
|
||||
switch address {
|
||||
case 0: // Port A
|
||||
if len(c.responseBuffer) > 0 {
|
||||
value := c.responseBuffer[0]
|
||||
c.responseBuffer = c.responseBuffer[1:]
|
||||
return value
|
||||
}
|
||||
return 0
|
||||
case 2: // Port C
|
||||
portC := uint8(0x80) // bit 7-nOBF is always 1, the output buffer is never full
|
||||
if len(c.responseBuffer) > 0 {
|
||||
portC |= 0x20 // bit 5-niBF is 1 if the input buffer has data
|
||||
}
|
||||
return portC
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *cardDan2ControllerSlot) blockPosition(unit uint8, block uint16) int64 {
|
||||
if s.fileNo == 0 {
|
||||
// Raw device
|
||||
return 512 * (int64(block) + (int64(unit&0x0f) << 12))
|
||||
} else {
|
||||
// File device
|
||||
return 512 * int64(block)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cardDan2ControllerSlot) openFile() (*os.File, error) {
|
||||
file, err := os.OpenFile(s.fileName, os.O_RDWR, 0)
|
||||
if err == nil {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
if s.card.improved && s.fileNameAlt != s.fileName {
|
||||
return os.OpenFile(s.fileNameAlt, os.O_RDWR, 0)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *cardDan2ControllerSlot) status(unit uint8) error {
|
||||
file, err := s.openFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *cardDan2ControllerSlot) readBlock(unit uint8, block uint16) ([]uint8, error) {
|
||||
file, err := s.openFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
position := s.blockPosition(unit, block)
|
||||
buffer := make([]uint8, 512)
|
||||
_, err = file.ReadAt(buffer, position)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) readBootBlock(block uint16) []uint8 {
|
||||
position := 512 * int64(block)
|
||||
program := PROGMEM[:]
|
||||
if c.improved {
|
||||
// If a file VOLA2.po is present, it is used. Not emulated.
|
||||
program = PROGMEM_v7[:]
|
||||
}
|
||||
if position+512 > int64(len(program)) {
|
||||
return []uint8{}
|
||||
}
|
||||
return program[position : position+512]
|
||||
}
|
||||
|
||||
func (s *cardDan2ControllerSlot) writeBlock(unit uint8, block uint16, data []uint8) error {
|
||||
file, err := s.openFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
position := s.blockPosition(unit, block)
|
||||
_, err = file.WriteAt(data, position)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Version info
|
||||
func (c *CardDan2Controller) versionInfo() []uint8 {
|
||||
return []uint8{
|
||||
3, 4, 2, // Version 3.4.2
|
||||
3, // DAN ][ CONTROLLER CARD with Atmega328P
|
||||
0x80 | // Support raw sd
|
||||
0x40, // Support fat
|
||||
// 0x20 | // Support Ethernet
|
||||
// 0x10 | // Support FTP
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cardDan2ControllerSlot) initializeDrive() {
|
||||
if s.fileNo == 255 {
|
||||
s.fileNo = 0 // Wide raw not supported, changed to raw
|
||||
}
|
||||
if s.fileNo == 0 {
|
||||
// Raw device
|
||||
s.fileName = s.path
|
||||
s.fileNameAlt = s.path
|
||||
} else {
|
||||
s.fileName = filepath.Join(s.path, fmt.Sprintf("BLKDEV%02X.PO", s.fileNo))
|
||||
s.fileNameAlt = filepath.Join(s.path, fmt.Sprintf("VOL%02X.PO", s.fileNo))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) selectSlot(unit uint8) *cardDan2ControllerSlot {
|
||||
if unit&0x80 == 0 {
|
||||
return c.slotA
|
||||
} else {
|
||||
return c.slotB
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) processCommand() {
|
||||
// See : Apple2Arduino.ino::do_command()
|
||||
command := c.commandBuffer[0]
|
||||
|
||||
if !c.improved && command >= 8 && command < 128 {
|
||||
// Command not supported in the original firmware
|
||||
c.tracef("Command %v not supported in the improved version\n", command)
|
||||
c.sendResponseCode(0x27)
|
||||
c.commandBuffer = nil
|
||||
return
|
||||
}
|
||||
|
||||
switch command {
|
||||
case 0, 3: // Status and format
|
||||
if len(c.commandBuffer) == 6 {
|
||||
unit, _, _ := c.getUnitBufBlk()
|
||||
slot := c.selectSlot(unit)
|
||||
err := slot.status(unit)
|
||||
if err != nil {
|
||||
c.tracef("Error status : %v\n", err)
|
||||
c.sendResponseCode(0x28)
|
||||
} else {
|
||||
c.sendResponseCode(0x00)
|
||||
}
|
||||
|
||||
if command == 0 {
|
||||
c.tracef("0-Status unit $%02x\n", unit)
|
||||
} else {
|
||||
c.tracef("3-Format unit $%02x\n", unit)
|
||||
}
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
|
||||
case 1: // Read block
|
||||
if len(c.commandBuffer) == 6 {
|
||||
unit, buffer, block := c.getUnitBufBlk()
|
||||
c.tracef("1-Read unit $%02x, buffer $%x, block %v\n", unit, buffer, block)
|
||||
|
||||
slot := c.selectSlot(unit)
|
||||
data, err := slot.readBlock(unit, block)
|
||||
if err != nil {
|
||||
c.tracef("Error reading block : %v\n", err)
|
||||
c.sendResponseCode(0x28)
|
||||
} else {
|
||||
c.sendResponse(data...)
|
||||
}
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
|
||||
case 2: // Write block
|
||||
if len(c.commandBuffer) == 6 {
|
||||
unit, buffer, block := c.getUnitBufBlk()
|
||||
c.tracef("2-Write unit $%02x, buffer $%x, block %v\n", unit, buffer, block)
|
||||
|
||||
c.receivingWiteBuffer = true
|
||||
c.writeBuffer = make([]uint8, 0, 512)
|
||||
c.commitWrite = func(data []uint8) error {
|
||||
slot := c.selectSlot(unit)
|
||||
err := slot.writeBlock(unit, block, c.writeBuffer)
|
||||
if err != nil {
|
||||
c.tracef("Error writing block : %v\n", err)
|
||||
}
|
||||
c.receivingWiteBuffer = false
|
||||
c.writeBuffer = nil
|
||||
c.commitWrite = nil
|
||||
return nil
|
||||
}
|
||||
c.sendResponseCode(0x00)
|
||||
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
|
||||
case 5, 9: // Get volume
|
||||
// cmd=5: return eeprom volume configuration. (Not emulated: returns current)
|
||||
// cmd=9: return current volume selection
|
||||
if len(c.commandBuffer) == 6 {
|
||||
c.tracef("%v-Get Volume\n", command)
|
||||
|
||||
c.sendResponse(c.slotA.fileNo, c.slotB.fileNo)
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
|
||||
case 4, 6, 7, 8: // Set volume
|
||||
// cmd=4: permanently selects drives 0+1, single byte response (used by eprom+bootpg for volume selection)
|
||||
// cmd=6: temporarily selects drives 0+1, 512byte response (used by bootpg for volume preview)
|
||||
// cmd=7: permanently selects drives 0+1, 512byte response (used by bootpg for volume selection)
|
||||
// cmd=8: permanently selects drive 0 only, single byte response (used by eprom for quick access keys)
|
||||
if len(c.commandBuffer) == 6 {
|
||||
unit, _, block := c.getUnitBufBlk()
|
||||
c.a2slot = (unit >> 4) & 0x3
|
||||
c.slotA.fileNo = uint8(block & 0xff)
|
||||
c.slotA.initializeDrive()
|
||||
if command != 8 {
|
||||
c.slotB.fileNo = uint8((block >> 8) & 0xff)
|
||||
c.slotB.initializeDrive()
|
||||
}
|
||||
|
||||
c.tracef("%v-Set Volume %v and %v\n",
|
||||
command, c.slotA.fileNo, c.slotB.fileNo)
|
||||
|
||||
if command == 4 || command == 8 {
|
||||
c.sendResponseCode(0x00) // Success code
|
||||
} else {
|
||||
c.sendResponse()
|
||||
// command 6 does not write eeprom, not emulated
|
||||
}
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
|
||||
case 0xa: // Read failsafe
|
||||
if len(c.commandBuffer) == 6 {
|
||||
unit, buffer, block := c.getUnitBufBlk()
|
||||
c.tracef("1-Read unit $%02x, buffer $%x, block %v\n", unit, buffer, block)
|
||||
|
||||
slot := c.selectSlot(unit)
|
||||
data, err := slot.readBlock(unit, block)
|
||||
if err != nil {
|
||||
c.sendResponse(c.readBootBlock(block)...)
|
||||
} else {
|
||||
c.sendResponse(data...)
|
||||
}
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
|
||||
case 0x0b: // Version info
|
||||
c.tracef("0b-Version info\n")
|
||||
c.sendResponse(c.versionInfo()...)
|
||||
c.commandBuffer = nil
|
||||
|
||||
case 13 + 128, 32 + 128: // Read bootblock
|
||||
if len(c.commandBuffer) == 6 {
|
||||
_, _, block := c.getUnitBufBlk()
|
||||
|
||||
c.tracef("ac-Read bootblock\n")
|
||||
c.sendResponse(c.readBootBlock(block)...)
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
|
||||
default: // Unknown command
|
||||
c.tracef("Unknown command %v\n", command)
|
||||
c.sendResponseCode(0x27)
|
||||
c.commandBuffer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) sendResponseCode(code uint8) {
|
||||
c.responseBuffer = append(c.responseBuffer, code)
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) sendResponse(response ...uint8) {
|
||||
c.responseBuffer = append(c.responseBuffer, 0x00) // Success code
|
||||
c.responseBuffer = append(c.responseBuffer, response...)
|
||||
rest := 512 - len(response)
|
||||
if rest > 0 {
|
||||
c.responseBuffer = append(c.responseBuffer, make([]uint8, rest)...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDan2Controller) getUnitBufBlk() (uint8, uint16, uint16) {
|
||||
return c.commandBuffer[1],
|
||||
uint16(c.commandBuffer[2]) + uint16(c.commandBuffer[3])<<8,
|
||||
uint16(c.commandBuffer[4]) + uint16(c.commandBuffer[5])<<8
|
||||
}
|
||||
|
||||
var PROGMEM = [512]uint8{
|
||||
0xea, 0xa9, 0x20, 0x85, 0xf0, 0xa9, 0x60, 0x85, 0xf3, 0xa5, 0x43, 0x4a,
|
||||
0x4a, 0x4a, 0x4a, 0x29, 0x07, 0x09, 0xc0, 0x85, 0xf2, 0xa0, 0x00, 0x84,
|
||||
0xf1, 0x88, 0xb1, 0xf1, 0x85, 0xf1, 0x20, 0x93, 0xfe, 0x20, 0x89, 0xfe,
|
||||
0x20, 0x58, 0xfc, 0x20, 0xa2, 0x09, 0xa9, 0x00, 0x85, 0x25, 0x20, 0x22,
|
||||
0xfc, 0xa5, 0x25, 0x85, 0xf5, 0x85, 0xf6, 0x20, 0x90, 0x09, 0xa9, 0x00,
|
||||
0x85, 0x24, 0xa5, 0x25, 0x20, 0xe3, 0xfd, 0xe6, 0x24, 0x20, 0x7a, 0x09,
|
||||
0x20, 0x04, 0x09, 0xa9, 0x14, 0x85, 0x24, 0xa5, 0x25, 0x20, 0xe3, 0xfd,
|
||||
0xe6, 0x24, 0xa5, 0x43, 0x09, 0x80, 0x85, 0x43, 0x20, 0x7a, 0x09, 0x20,
|
||||
0x04, 0x09, 0xa5, 0x43, 0x29, 0x7f, 0x85, 0x43, 0xe6, 0x25, 0xa5, 0x25,
|
||||
0xc9, 0x10, 0x90, 0xbe, 0xa9, 0x00, 0x85, 0x24, 0xa9, 0x12, 0x85, 0x25,
|
||||
0x20, 0x22, 0xfc, 0xa2, 0x14, 0x20, 0x66, 0x09, 0x20, 0x61, 0x09, 0xa9,
|
||||
0x0a, 0x85, 0x24, 0xa5, 0xf7, 0x20, 0xf8, 0x08, 0xa9, 0x14, 0x85, 0x24,
|
||||
0x20, 0x5c, 0x09, 0xa9, 0x1e, 0x85, 0x24, 0xa5, 0xf8, 0x20, 0xf8, 0x08,
|
||||
0xa9, 0x0a, 0x85, 0x24, 0x20, 0xca, 0x08, 0x85, 0xf5, 0x20, 0xf8, 0x08,
|
||||
0xa9, 0x1e, 0x85, 0x24, 0x20, 0xca, 0x08, 0x85, 0xf6, 0x20, 0xf8, 0x08,
|
||||
0x20, 0x8c, 0x09, 0x4c, 0xb7, 0x09, 0xa5, 0xf7, 0x85, 0xf5, 0xa5, 0xf8,
|
||||
0x85, 0xf6, 0x20, 0x90, 0x09, 0x68, 0x68, 0x4c, 0xb7, 0x09, 0x20, 0x0c,
|
||||
0xfd, 0xc9, 0x9b, 0xf0, 0xe9, 0xc9, 0xa1, 0xf0, 0x20, 0xc9, 0xe1, 0x90,
|
||||
0x03, 0x38, 0xe9, 0x20, 0xc9, 0xc1, 0x90, 0x04, 0xc9, 0xc7, 0x90, 0x0b,
|
||||
0xc9, 0xb0, 0x90, 0xe2, 0xc9, 0xba, 0xb0, 0xde, 0x29, 0x0f, 0x60, 0x38,
|
||||
0xe9, 0x07, 0x29, 0x0f, 0x60, 0xa9, 0xff, 0x60, 0xc9, 0xff, 0xf0, 0x03,
|
||||
0x4c, 0xe3, 0xfd, 0xa9, 0xa1, 0x4c, 0xed, 0xfd, 0xa2, 0x00, 0xb0, 0x25,
|
||||
0xad, 0x05, 0x10, 0x30, 0x20, 0xad, 0x04, 0x10, 0x29, 0xf0, 0xc9, 0xf0,
|
||||
0xd0, 0x17, 0xad, 0x04, 0x10, 0x29, 0x0f, 0xf0, 0x10, 0x85, 0xf9, 0xbd,
|
||||
0x05, 0x10, 0x09, 0x80, 0x20, 0xed, 0xfd, 0xe8, 0xe4, 0xf9, 0xd0, 0xf3,
|
||||
0x60, 0x4c, 0x66, 0x09, 0xbc, 0xce, 0xcf, 0xa0, 0xd6, 0xcf, 0xcc, 0xd5,
|
||||
0xcd, 0xc5, 0xbe, 0x00, 0xc3, 0xc1, 0xd2, 0xc4, 0xa0, 0xb1, 0xba, 0x00,
|
||||
0xc4, 0xc1, 0xce, 0xa0, 0xdd, 0xdb, 0xa0, 0xd6, 0xcf, 0xcc, 0xd5, 0xcd,
|
||||
0xc5, 0xa0, 0xd3, 0xc5, 0xcc, 0xc5, 0xc3, 0xd4, 0xcf, 0xd2, 0x8d, 0x00,
|
||||
0xa9, 0xb2, 0x8d, 0x41, 0x09, 0xa2, 0x0c, 0x4c, 0x66, 0x09, 0xbd, 0x30,
|
||||
0x09, 0xf0, 0x0e, 0x20, 0xed, 0xfd, 0xe8, 0xd0, 0xf5, 0xa9, 0x00, 0x85,
|
||||
0x44, 0xa9, 0x10, 0x85, 0x45, 0x60, 0xa9, 0x01, 0x85, 0x42, 0x20, 0x71,
|
||||
0x09, 0xa9, 0x02, 0x85, 0x46, 0xa9, 0x00, 0x85, 0x47, 0x4c, 0xf0, 0x00,
|
||||
0xa9, 0x07, 0xd0, 0x02, 0xa9, 0x06, 0x85, 0x42, 0x20, 0x71, 0x09, 0xa5,
|
||||
0xf5, 0x85, 0x46, 0xa5, 0xf6, 0x85, 0x47, 0x4c, 0xf0, 0x00, 0xa9, 0x05,
|
||||
0x85, 0x42, 0x20, 0x71, 0x09, 0x20, 0xf0, 0x00, 0xad, 0x00, 0x10, 0x85,
|
||||
0xf7, 0xad, 0x01, 0x10, 0x85, 0xf8, 0x60, 0xa9, 0x00, 0x85, 0xf1, 0x6c,
|
||||
0xf1, 0x00,
|
||||
}
|
||||
|
||||
var PROGMEM_v7 = [3 * 512]uint8{
|
||||
0xea, 0x8d, 0x0e, 0xc0, 0xa9, 0x20, 0x85, 0xf0, 0xa9, 0x60, 0x85, 0xf3,
|
||||
0xa5, 0x43, 0x4a, 0x4a, 0x4a, 0x4a, 0x29, 0x07, 0x09, 0xc0, 0x85, 0xf2,
|
||||
0xa0, 0x00, 0x84, 0xf1, 0x88, 0xb1, 0xf1, 0x85, 0xf1, 0xa9, 0x02, 0x85,
|
||||
0xf9, 0xe6, 0x46, 0xe6, 0x45, 0xe6, 0x45, 0x20, 0xf0, 0x00, 0x90, 0x06,
|
||||
0x20, 0xe2, 0xfb, 0x4c, 0xba, 0xfa, 0xc6, 0xf9, 0xd0, 0xeb, 0xa9, 0x00,
|
||||
0x85, 0xfc, 0x20, 0x93, 0xfe, 0x20, 0x89, 0xfe, 0x20, 0xc4, 0x0b, 0x20,
|
||||
0xb6, 0x08, 0x20, 0x01, 0x09, 0xa5, 0xf5, 0x85, 0x46, 0xa5, 0xf6, 0x85,
|
||||
0x47, 0x20, 0xab, 0x0b, 0x4c, 0xe6, 0x0c, 0x20, 0xd4, 0x08, 0x20, 0x6c,
|
||||
0x08, 0xa5, 0xf7, 0x85, 0x46, 0xa5, 0xf8, 0x85, 0x47, 0x4c, 0xaf, 0x0b,
|
||||
0xa9, 0x00, 0x85, 0xfb, 0xa9, 0x04, 0x85, 0x25, 0x20, 0x22, 0xfc, 0xa5,
|
||||
0xfb, 0x05, 0xfc, 0x85, 0x46, 0x09, 0x80, 0x85, 0x47, 0x20, 0xaf, 0x0b,
|
||||
0xa9, 0x00, 0x20, 0x42, 0x0a, 0xa9, 0xff, 0x85, 0x32, 0xa5, 0x43, 0x09,
|
||||
0x80, 0x85, 0x43, 0xa5, 0xfb, 0x09, 0x80, 0x85, 0xfb, 0xa9, 0x14, 0x20,
|
||||
0x42, 0x0a, 0xa9, 0xff, 0x85, 0x32, 0x4a, 0x25, 0x43, 0x85, 0x43, 0xe6,
|
||||
0x25, 0xe6, 0xfb, 0xa5, 0xfb, 0x29, 0x7f, 0x85, 0xfb, 0xc9, 0x10, 0x90,
|
||||
0xbf, 0x60, 0xa5, 0xf7, 0x85, 0xf5, 0xa5, 0xf8, 0x85, 0xf6, 0x60, 0xa0,
|
||||
0x00, 0x84, 0x24, 0xa0, 0x03, 0x84, 0x25, 0x20, 0x22, 0xfc, 0x20, 0x83,
|
||||
0x0b, 0xa0, 0x14, 0x84, 0x24, 0x4c, 0x7e, 0x0b, 0xa5, 0xfa, 0x48, 0xa9,
|
||||
0x15, 0x20, 0x62, 0x0b, 0x20, 0x8b, 0x0b, 0xa9, 0x14, 0x85, 0x24, 0x20,
|
||||
0x87, 0x0b, 0xa9, 0x01, 0x85, 0xfa, 0x20, 0x14, 0x0a, 0xc6, 0xfa, 0x10,
|
||||
0xf9, 0x68, 0x85, 0xfa, 0x60, 0x20, 0xc0, 0x0c, 0x20, 0xb6, 0x08, 0x68,
|
||||
0x68, 0x18, 0x4c, 0xe6, 0x0c, 0x20, 0x02, 0x0a, 0x20, 0xbf, 0x08, 0xa5,
|
||||
0xf5, 0x29, 0x70, 0x85, 0xfc, 0xa9, 0x00, 0x85, 0xfa, 0x20, 0x5b, 0x08,
|
||||
0x20, 0xda, 0x0b, 0xa9, 0x40, 0x20, 0x9e, 0x0a, 0x20, 0x14, 0x0a, 0x20,
|
||||
0x0c, 0xfd, 0x48, 0xa9, 0x01, 0x20, 0x9e, 0x0a, 0xa6, 0xfa, 0xb5, 0xf5,
|
||||
0x29, 0x8f, 0xa8, 0x68, 0xc9, 0x8d, 0xd0, 0x1f, 0xa9, 0x00, 0x20, 0x9e,
|
||||
0x0a, 0xa5, 0xfa, 0x49, 0x01, 0x85, 0xfa, 0xd0, 0x01, 0x60, 0xaa, 0xb5,
|
||||
0xf5, 0x29, 0x70, 0xc5, 0xfc, 0xf0, 0xcc, 0x85, 0xfc, 0x20, 0x5b, 0x08,
|
||||
0x4c, 0x17, 0x09, 0xc9, 0x9b, 0xf0, 0x9e, 0xc9, 0xa1, 0xd0, 0x05, 0xa9,
|
||||
0xff, 0x85, 0xf5, 0x60, 0xc9, 0xac, 0xf0, 0x04, 0xc9, 0x8b, 0xd0, 0x13,
|
||||
0x98, 0x0a, 0x08, 0x38, 0xe9, 0x02, 0x29, 0x1e, 0x28, 0x6a, 0x29, 0x8f,
|
||||
0x05, 0xfc, 0x95, 0xf5, 0x4c, 0x17, 0x09, 0xc9, 0xa0, 0xf0, 0x04, 0xc9,
|
||||
0x8a, 0xd0, 0x04, 0xc8, 0x98, 0xd0, 0xeb, 0xc9, 0x88, 0xd0, 0x1e, 0x98,
|
||||
0x49, 0x80, 0xa8, 0x05, 0xfc, 0x95, 0xf5, 0x10, 0xe3, 0xa9, 0xf0, 0x18,
|
||||
0x65, 0xfc, 0x29, 0x70, 0x85, 0xfc, 0x98, 0x48, 0x20, 0x5b, 0x08, 0xa6,
|
||||
0xfa, 0x68, 0x4c, 0x72, 0x09, 0xc9, 0x95, 0xd0, 0x0e, 0x98, 0x49, 0x80,
|
||||
0xa8, 0x05, 0xfc, 0x95, 0xf5, 0x30, 0xc1, 0xa9, 0x10, 0xd0, 0xdc, 0xc9,
|
||||
0xe1, 0x90, 0x03, 0x38, 0xe9, 0x20, 0xc9, 0xc1, 0x90, 0x09, 0xc9, 0xc7,
|
||||
0xb0, 0x05, 0x38, 0xe9, 0x07, 0xd0, 0x08, 0xc9, 0xb0, 0x90, 0x11, 0xc9,
|
||||
0xba, 0xb0, 0x0d, 0x29, 0x0f, 0x85, 0xf9, 0x98, 0x29, 0x80, 0x05, 0xf9,
|
||||
0xa8, 0x4c, 0x72, 0x09, 0xc9, 0xc9, 0xf0, 0x02, 0xd0, 0x8e, 0x20, 0x02,
|
||||
0x0a, 0x20, 0xda, 0x0b, 0x20, 0x0c, 0x0c, 0x4c, 0x01, 0x09, 0xc9, 0xff,
|
||||
0xf0, 0x03, 0x4c, 0xda, 0xfd, 0xa9, 0xa1, 0x4c, 0xed, 0xfd, 0x20, 0x58,
|
||||
0xfc, 0xa9, 0x3f, 0x20, 0x6b, 0x0b, 0xa9, 0x17, 0x20, 0x62, 0x0b, 0xa9,
|
||||
0x23, 0x4c, 0x6b, 0x0b, 0xa2, 0x0b, 0xa5, 0xfa, 0xf0, 0x02, 0xa2, 0x1f,
|
||||
0x86, 0x24, 0xa0, 0x15, 0x84, 0x25, 0x20, 0x22, 0xfc, 0xa4, 0xfa, 0xb9,
|
||||
0xf5, 0x00, 0x48, 0x30, 0x04, 0xa0, 0x01, 0xd0, 0x02, 0xa0, 0x02, 0x98,
|
||||
0x20, 0xe3, 0xfd, 0xe6, 0x24, 0x68, 0x29, 0x7f, 0x20, 0xf6, 0x09, 0xc6,
|
||||
0x24, 0x60, 0x85, 0x24, 0xa5, 0xfb, 0x05, 0xfc, 0x48, 0x29, 0x7f, 0x20,
|
||||
0xda, 0xfd, 0xa9, 0xba, 0x20, 0xed, 0xfd, 0xe6, 0x24, 0x68, 0xc5, 0xf5,
|
||||
0xf0, 0x04, 0xc5, 0xf6, 0xd0, 0x04, 0xa9, 0x3f, 0x85, 0x32, 0x20, 0x9e,
|
||||
0x0b, 0xb0, 0x32, 0xad, 0x05, 0x10, 0x30, 0x2d, 0xad, 0x04, 0x10, 0xaa,
|
||||
0x29, 0xf0, 0xc9, 0xf0, 0xd0, 0x23, 0x8a, 0x29, 0x0f, 0xf0, 0x1e, 0x85,
|
||||
0xf9, 0xa2, 0x00, 0xbd, 0x05, 0x10, 0x09, 0x80, 0x20, 0xed, 0xfd, 0xe8,
|
||||
0xe4, 0xf9, 0xd0, 0xf3, 0xe0, 0x0f, 0xd0, 0x01, 0x60, 0xa9, 0xa0, 0x20,
|
||||
0xed, 0xfd, 0xe8, 0xd0, 0xf3, 0xa2, 0x00, 0x4c, 0x92, 0x0b, 0xa6, 0xfa,
|
||||
0xc9, 0x01, 0xd0, 0x0a, 0xa9, 0x80, 0xa4, 0xf5, 0xc4, 0xf6, 0xd0, 0x02,
|
||||
0xa9, 0x00, 0x85, 0xf9, 0xb5, 0xf5, 0x30, 0x04, 0xa0, 0x04, 0xd0, 0x02,
|
||||
0xa0, 0x18, 0x84, 0x24, 0x29, 0x7f, 0x45, 0xfc, 0xc9, 0x10, 0xb0, 0x17,
|
||||
0x69, 0x04, 0x85, 0x25, 0x20, 0x22, 0xfc, 0xa4, 0x24, 0xa2, 0x0f, 0xb1,
|
||||
0x28, 0x29, 0x3f, 0x05, 0xf9, 0x91, 0x28, 0xc8, 0xca, 0xd0, 0xf4, 0x60,
|
||||
0xad, 0xad, 0xad, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
|
||||
0xa0, 0xa0, 0xa0, 0x00, 0xc4, 0xd2, 0xc9, 0xd6, 0xc5, 0xa0, 0xb1, 0xba,
|
||||
0xa0, 0xd3, 0xc4, 0xbf, 0xaf, 0x00, 0xd3, 0xc4, 0xb1, 0xba, 0x00, 0x20,
|
||||
0x20, 0x01, 0x10, 0x10, 0x0c, 0x05, 0x20, 0x09, 0x09, 0x20, 0x06, 0x0f,
|
||||
0x12, 0x05, 0x16, 0x05, 0x12, 0x21, 0x20, 0x20, 0x16, 0x33, 0x2e, 0x34,
|
||||
0x2e, 0x32, 0x00, 0x04, 0x01, 0x0e, 0x20, 0x09, 0x09, 0x20, 0x16, 0x0f,
|
||||
0x0c, 0x15, 0x0d, 0x05, 0x20, 0x13, 0x05, 0x0c, 0x05, 0x03, 0x14, 0x0f,
|
||||
0x12, 0x00, 0xa0, 0xcf, 0xcb, 0xa1, 0x00, 0xc5, 0xd2, 0xd2, 0xcf, 0xd2,
|
||||
0xa1, 0x87, 0x87, 0x87, 0x00, 0xc6, 0xd4, 0xd0, 0xaf, 0xc9, 0xd0, 0xba,
|
||||
0xa0, 0x00, 0xce, 0xc5, 0xd7, 0xa0, 0xc9, 0xd0, 0xba, 0xa0, 0xa0, 0xa0,
|
||||
0xa0, 0xae, 0xa0, 0xa0, 0xa0, 0xae, 0xa0, 0xa0, 0xa0, 0xae, 0xa0, 0xa0,
|
||||
0xa0, 0x00, 0x85, 0x25, 0xa9, 0x00, 0x85, 0x24, 0x4c, 0x22, 0xfc, 0x48,
|
||||
0xa2, 0x26, 0xa9, 0x20, 0x20, 0xed, 0xfd, 0xca, 0x10, 0xf8, 0x68, 0xaa,
|
||||
0xa9, 0x08, 0x85, 0x24, 0xd0, 0x14, 0xa9, 0xb2, 0x8d, 0xfc, 0x0a, 0xa2,
|
||||
0x1e, 0xd0, 0x0b, 0xa9, 0xb2, 0xd0, 0x02, 0xa9, 0xb1, 0x8d, 0xf2, 0x0a,
|
||||
0xa2, 0x10, 0xbd, 0xdc, 0x0a, 0xf0, 0x06, 0x20, 0xed, 0xfd, 0xe8, 0xd0,
|
||||
0xf5, 0x60, 0xa0, 0x00, 0x84, 0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46,
|
||||
0x4c, 0xb9, 0x0b, 0xa9, 0x07, 0xd0, 0x02, 0xa9, 0x06, 0x85, 0x42, 0xa5,
|
||||
0x47, 0x49, 0x80, 0x85, 0x47, 0xa9, 0x00, 0x85, 0x44, 0xa9, 0x10, 0x85,
|
||||
0x45, 0x4c, 0xf0, 0x00, 0xa9, 0x05, 0x85, 0x42, 0x20, 0xb9, 0x0b, 0xad,
|
||||
0x00, 0x10, 0x85, 0xf7, 0xad, 0x01, 0x10, 0x49, 0x80, 0x85, 0xf8, 0x4c,
|
||||
0xb6, 0x08, 0xa9, 0x21, 0x20, 0xb1, 0x0b, 0xb0, 0xbc, 0xad, 0x10, 0x10,
|
||||
0xf0, 0xb7, 0xa9, 0x01, 0x20, 0x62, 0x0b, 0xa2, 0x09, 0x86, 0x24, 0xa2,
|
||||
0x65, 0x20, 0x92, 0x0b, 0xad, 0x06, 0x10, 0x20, 0xb4, 0x0c, 0xad, 0x07,
|
||||
0x10, 0x20, 0xb4, 0x0c, 0xad, 0x08, 0x10, 0x20, 0xb4, 0x0c, 0xad, 0x09,
|
||||
0x10, 0x4c, 0x13, 0x0d, 0xa5, 0x43, 0x48, 0xa9, 0x0a, 0x20, 0x62, 0x0b,
|
||||
0xa2, 0x6e, 0x20, 0x92, 0x0b, 0xa9, 0x08, 0x85, 0x24, 0x20, 0x54, 0x0c,
|
||||
0xb0, 0xed, 0x85, 0x44, 0xe6, 0x24, 0x20, 0x54, 0x0c, 0xb0, 0xe4, 0x85,
|
||||
0x45, 0xe6, 0x24, 0x20, 0x54, 0x0c, 0xb0, 0xdb, 0x85, 0x46, 0xe6, 0x24,
|
||||
0x20, 0x54, 0x0c, 0xb0, 0xd2, 0x85, 0x47, 0x85, 0x43, 0xa9, 0x20, 0x85,
|
||||
0x42, 0x20, 0xf0, 0x00, 0x18, 0xaa, 0x68, 0x85, 0x43, 0xca, 0xf0, 0x01,
|
||||
0x38, 0x4c, 0xd0, 0x0c, 0xa9, 0x00, 0x85, 0xf9, 0xa2, 0x03, 0x8a, 0x48,
|
||||
0x20, 0x93, 0x0c, 0xb0, 0x17, 0xc9, 0xff, 0xf0, 0x17, 0xa8, 0x20, 0x84,
|
||||
0x0c, 0xb0, 0x0d, 0x98, 0x65, 0xf9, 0x85, 0xf9, 0xb0, 0x06, 0x68, 0xaa,
|
||||
0xca, 0xd0, 0xe3, 0x48, 0x68, 0xa5, 0xf9, 0x60, 0x68, 0x18, 0x65, 0x24,
|
||||
0x85, 0x24, 0x90, 0xf5, 0xa2, 0x0a, 0xa9, 0x00, 0x18, 0x65, 0xf9, 0xb0,
|
||||
0x05, 0xca, 0xd0, 0xf9, 0x85, 0xf9, 0x60, 0x20, 0x0c, 0xfd, 0xc9, 0xa0,
|
||||
0xd0, 0x04, 0xa9, 0xff, 0x18, 0x60, 0xc9, 0x8d, 0xf0, 0xf8, 0xc9, 0xae,
|
||||
0xf0, 0xf4, 0xc9, 0xb0, 0x30, 0xe9, 0xc9, 0xba, 0xb0, 0xe5, 0x20, 0xed,
|
||||
0xfd, 0x29, 0x0f, 0x60, 0x20, 0x13, 0x0d, 0xa9, 0xae, 0x4c, 0xed, 0xfd,
|
||||
0xa9, 0x00, 0xf0, 0x02, 0xa9, 0x80, 0xa2, 0x01, 0x86, 0xfa, 0x48, 0x20,
|
||||
0x9e, 0x0a, 0x68, 0xc6, 0xfa, 0x10, 0xf7, 0x60, 0xa2, 0x56, 0x90, 0x02,
|
||||
0xa2, 0x5b, 0x20, 0x92, 0x0b, 0xa2, 0x06, 0xa9, 0xff, 0x20, 0xa8, 0xfc,
|
||||
0xca, 0xd0, 0xf8, 0x4c, 0x58, 0xfc, 0x08, 0x20, 0xd4, 0x08, 0x20, 0xbc,
|
||||
0x0c, 0xa9, 0x16, 0x20, 0x62, 0x0b, 0xa9, 0x11, 0x85, 0x24, 0x28, 0x20,
|
||||
0xd0, 0x0c, 0xa9, 0x08, 0x48, 0x85, 0x45, 0xa9, 0x00, 0x48, 0x85, 0x44,
|
||||
0x85, 0x46, 0x85, 0x47, 0xa2, 0x01, 0x81, 0x44, 0xa9, 0x0a, 0x85, 0x42,
|
||||
0x6c, 0xf1, 0x00, 0xc9, 0x0a, 0x90, 0x1e, 0xa2, 0x64, 0x86, 0xf9, 0x20,
|
||||
0x38, 0x0d, 0xe0, 0x00, 0xf0, 0x06, 0x48, 0x8a, 0x20, 0xe3, 0xfd, 0x68,
|
||||
0xa2, 0x0a, 0x86, 0xf9, 0x20, 0x38, 0x0d, 0x48, 0x8a, 0x20, 0xe3, 0xfd,
|
||||
0x68, 0x4c, 0xe3, 0xfd, 0xa2, 0x00, 0xc5, 0xf9, 0x90, 0x06, 0xe8, 0x38,
|
||||
0xe5, 0xf9, 0xb0, 0xf6, 0x60,
|
||||
}
|
26
cardDan2Controller_test.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDan2Controller(t *testing.T) {
|
||||
overrides := newConfiguration()
|
||||
overrides.set(confS7, "dan2sd,slot1=resources/ProDOS_2_4_3.po")
|
||||
|
||||
at, err := makeApple2Tester("2enh", overrides)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
at.terminateCondition = buildTerminateConditionText(at, "NEW VOL", true, 10_000_000)
|
||||
|
||||
at.run()
|
||||
|
||||
text := at.getText()
|
||||
if !strings.Contains(text, "NEW VOL") {
|
||||
t.Errorf("Expected Bitsy Bye screen, got '%s'", text)
|
||||
}
|
||||
|
||||
}
|
386
cardDisk2.go
|
@ -1,9 +1,10 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/ivanizag/izapple2/storage"
|
||||
)
|
||||
|
||||
/*
|
||||
|
@ -20,98 +21,162 @@ http://yesterbits.com/media/pubs/AppleOrchard/articles/disk-ii-part-1-1983-apr.p
|
|||
NIB: 35 tracks 6656 bytes, 232960 bytes
|
||||
|
||||
*/
|
||||
const maxHalfTrack = 68
|
||||
|
||||
type cardDisk2 struct {
|
||||
// CardDisk2 is a DiskII interface card
|
||||
type CardDisk2 struct {
|
||||
cardBase
|
||||
selected int // q5, Only 0 and 1 supported
|
||||
selected int // q5, Only 0 and 1 supported
|
||||
power bool // q4
|
||||
drive [2]cardDisk2Drive
|
||||
fastMode bool
|
||||
|
||||
dataLatch uint8
|
||||
q6 bool
|
||||
q7 bool
|
||||
|
||||
trackTracer trackTracer
|
||||
}
|
||||
|
||||
type drive interface {
|
||||
insertDiskette(path string) error
|
||||
}
|
||||
|
||||
type cardDisk2Drive struct {
|
||||
diskette diskette
|
||||
power bool // q4
|
||||
phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor
|
||||
tracksStep int // Stepmotor for tracks position. 4 steps per track
|
||||
name string
|
||||
diskette storage.Diskette
|
||||
phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor
|
||||
trackStep int // Stepmotor for tracks position. 4 steps per track
|
||||
}
|
||||
|
||||
const (
|
||||
diskBitCycle = 4 // There is a dataLatch bit transferred every 4 cycles
|
||||
diskLatchReadCycles = 7 // Loaded data is available for a little more than 7ns
|
||||
diskWriteByteCycle = 32 // Load data to write every 32 cycles
|
||||
diskWriteSelfSyncCycle = 40 // Save $FF every 40 cycles. Self sync is 10 bits: 1111 1111 00
|
||||
diskMotorStartMs = 150 // Time with the disk spinning to get full speed
|
||||
func newCardDisk2Builder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Disk II",
|
||||
description: "Disk II interface card",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"disk1", "Diskette image for drive 1", ""},
|
||||
{"disk2", "Diskette image for drive 2", ""},
|
||||
{"tracktracer", "Trace how the disk head moves between tracks", "false"},
|
||||
{"fast", "Enable CPU burst when accessing the disk", "true"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardDisk2
|
||||
err := c.loadRomFromResource("<internal>/DISK2.rom", cardRomSimple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
)
|
||||
disk1 := paramsGetPath(params, "disk1")
|
||||
if disk1 != "" {
|
||||
err := c.drive[0].insertDiskette(disk1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
disk2 := paramsGetPath(params, "disk2")
|
||||
if disk2 != "" {
|
||||
err := c.drive[1].insertDiskette(disk2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
trackTracer := paramsGetBool(params, "tracktracer")
|
||||
if trackTracer {
|
||||
c.trackTracer = makeTrackTracerLogger()
|
||||
}
|
||||
c.fastMode = paramsGetBool(params, "fast")
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo returns smartPort info
|
||||
func (c *CardDisk2) GetInfo() map[string]string {
|
||||
info := make(map[string]string)
|
||||
info["rom"] = "16 sector"
|
||||
info["power"] = strconv.FormatBool(c.power)
|
||||
|
||||
info["D1 name"] = c.drive[0].name
|
||||
info["D1 track"] = strconv.FormatFloat(float64(c.drive[0].trackStep)/4, 'f', 2, 64)
|
||||
|
||||
info["D2 name"] = c.drive[1].name
|
||||
info["D2 track"] = strconv.FormatFloat(float64(c.drive[1].trackStep)/4, 'f', 2, 64)
|
||||
return info
|
||||
}
|
||||
|
||||
func (c *CardDisk2) reset() {
|
||||
// UtA2e 9-12, all switches forced to off
|
||||
drive := &c.drive[c.selected]
|
||||
drive.phases = 0 // q0, q1, q2, q3
|
||||
c.softSwitchQ4(false) // q4
|
||||
c.selected = 0 // q5
|
||||
c.q6 = false
|
||||
c.q7 = false
|
||||
}
|
||||
|
||||
func (c *CardDisk2) setTrackTracer(tt trackTracer) {
|
||||
c.trackTracer = tt
|
||||
}
|
||||
|
||||
func (c *CardDisk2) assign(a *Apple2, slot int) {
|
||||
a.registerRemovableMediaDrive(&c.drive[0])
|
||||
a.registerRemovableMediaDrive(&c.drive[1])
|
||||
|
||||
func (c *cardDisk2) assign(a *Apple2, slot int) {
|
||||
// Q1, Q2, Q3 and Q4 phase control soft switches,
|
||||
for i := uint8(0); i < 4; i++ {
|
||||
phase := i
|
||||
c.addCardSoftSwitchR(phase<<1, func(_ *ioC0Page) uint8 {
|
||||
c.addCardSoftSwitchRW(phase<<1, func() uint8 {
|
||||
// Update magnets and position
|
||||
drive := &c.drive[c.selected]
|
||||
drive.phases &^= (1 << phase)
|
||||
drive.tracksStep = moveStep(drive.phases, drive.tracksStep)
|
||||
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
|
||||
|
||||
if c.trackTracer != nil {
|
||||
c.trackTracer.traceTrack(drive.trackStep, c.slot, c.selected)
|
||||
}
|
||||
|
||||
return c.dataLatch // All even addresses return the last dataLatch
|
||||
}, fmt.Sprintf("PHASE%vOFF", phase))
|
||||
|
||||
c.addCardSoftSwitchR((phase<<1)+1, func(_ *ioC0Page) uint8 {
|
||||
// Update magnets and position
|
||||
c.addCardSoftSwitchRW((phase<<1)+1, func() uint8 { // Update magnets and position
|
||||
drive := &c.drive[c.selected]
|
||||
drive.phases |= (1 << phase)
|
||||
drive.tracksStep = moveStep(drive.phases, drive.tracksStep)
|
||||
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
|
||||
|
||||
if c.trackTracer != nil {
|
||||
c.trackTracer.traceTrack(drive.trackStep, slot, c.selected)
|
||||
}
|
||||
|
||||
return 0
|
||||
}, fmt.Sprintf("PHASE%vOFF", phase))
|
||||
}, fmt.Sprintf("PHASE%vON", phase))
|
||||
}
|
||||
|
||||
// Q4, power switch
|
||||
c.addCardSoftSwitchR(0x8, func(_ *ioC0Page) uint8 {
|
||||
drive := &c.drive[c.selected]
|
||||
if drive.power {
|
||||
drive.power = false
|
||||
c.a.releaseFastMode()
|
||||
if drive.diskette != nil {
|
||||
drive.diskette.powerOff(c.a.cpu.GetCycles())
|
||||
}
|
||||
}
|
||||
c.addCardSoftSwitchRW(0x8, func() uint8 {
|
||||
c.softSwitchQ4(false)
|
||||
return c.dataLatch
|
||||
}, "Q4DRIVEOFF")
|
||||
c.addCardSoftSwitchR(0x9, func(_ *ioC0Page) uint8 {
|
||||
drive := &c.drive[c.selected]
|
||||
if !drive.power {
|
||||
drive.power = true
|
||||
c.a.requestFastMode()
|
||||
if drive.diskette != nil {
|
||||
drive.diskette.powerOn(c.a.cpu.GetCycles())
|
||||
}
|
||||
}
|
||||
c.addCardSoftSwitchRW(0x9, func() uint8 {
|
||||
c.softSwitchQ4(true)
|
||||
return 0
|
||||
}, "Q4DRIVEON")
|
||||
|
||||
// Q5, drive selecion
|
||||
c.addCardSoftSwitchR(0xA, func(_ *ioC0Page) uint8 {
|
||||
c.selected = 0
|
||||
c.addCardSoftSwitchRW(0xA, func() uint8 {
|
||||
c.softSwitchQ5(0)
|
||||
return c.dataLatch
|
||||
}, "Q5SELECT1")
|
||||
c.addCardSoftSwitchR(0xB, func(_ *ioC0Page) uint8 {
|
||||
c.selected = 1
|
||||
c.addCardSoftSwitchRW(0xB, func() uint8 {
|
||||
c.softSwitchQ5(1)
|
||||
return 0
|
||||
}, "Q5SELECT2")
|
||||
|
||||
// Q6, Q7
|
||||
for i := uint8(0xC); i <= 0xF; i++ {
|
||||
iCopy := i
|
||||
c.addCardSoftSwitchR(iCopy, func(_ *ioC0Page) uint8 {
|
||||
c.addCardSoftSwitchR(iCopy, func() uint8 {
|
||||
return c.softSwitchQ6Q7(iCopy, 0)
|
||||
}, "Q6Q7")
|
||||
c.addCardSoftSwitchW(iCopy, func(_ *ioC0Page, value uint8) {
|
||||
c.addCardSoftSwitchW(iCopy, func(value uint8) {
|
||||
c.softSwitchQ6Q7(iCopy, value)
|
||||
}, "Q6Q7")
|
||||
}
|
||||
|
@ -119,10 +184,48 @@ func (c *cardDisk2) assign(a *Apple2, slot int) {
|
|||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *CardDisk2) softSwitchQ4(value bool) {
|
||||
if !value && c.power {
|
||||
// Turn off
|
||||
c.power = false
|
||||
if c.fastMode {
|
||||
c.a.ReleaseFastMode()
|
||||
}
|
||||
drive := &c.drive[c.selected]
|
||||
if drive.diskette != nil {
|
||||
drive.diskette.PowerOff(c.a.cpu.GetCycles())
|
||||
}
|
||||
} else if value && !c.power {
|
||||
// Turn on
|
||||
c.power = true
|
||||
if c.fastMode {
|
||||
c.a.RequestFastMode()
|
||||
}
|
||||
drive := &c.drive[c.selected]
|
||||
if drive.diskette != nil {
|
||||
drive.diskette.PowerOn(c.a.cpu.GetCycles())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDisk2) softSwitchQ5(selected int) {
|
||||
if c.power && c.selected != selected {
|
||||
// Selected changed with power on, power goes to the other disk
|
||||
if c.drive[c.selected].diskette != nil {
|
||||
c.drive[c.selected].diskette.PowerOff(c.a.cpu.GetCycles())
|
||||
}
|
||||
if c.drive[selected].diskette != nil {
|
||||
c.drive[selected].diskette.PowerOn(c.a.cpu.GetCycles())
|
||||
}
|
||||
}
|
||||
|
||||
c.selected = selected
|
||||
}
|
||||
|
||||
// Q6: shift/load
|
||||
// Q7: read/write
|
||||
|
||||
func (c *cardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
|
||||
func (c *CardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
|
||||
switch index {
|
||||
case 0xC: // Q6L
|
||||
c.q6 = false
|
||||
|
@ -142,16 +245,16 @@ func (c *cardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (c *cardDisk2) processQ6Q7(in uint8) {
|
||||
func (c *CardDisk2) processQ6Q7(in uint8) {
|
||||
d := &c.drive[c.selected]
|
||||
if d.diskette == nil {
|
||||
return
|
||||
}
|
||||
if !c.q6 { // shift
|
||||
if !c.q7 { // Q6L-Q7L: Read
|
||||
c.dataLatch = d.diskette.read(d.tracksStep, c.a.cpu.GetCycles())
|
||||
c.dataLatch = d.diskette.Read(d.trackStep, c.a.cpu.GetCycles())
|
||||
} else { // Q6L-Q7H: Write the dataLatch value to disk. Shift data out
|
||||
d.diskette.write(d.tracksStep, c.dataLatch, c.a.cpu.GetCycles())
|
||||
d.diskette.Write(d.trackStep, c.dataLatch, c.a.cpu.GetCycles())
|
||||
}
|
||||
} else { // load
|
||||
if !c.q7 { // Q6H-Q7L: Sense write protect / prewrite state
|
||||
|
@ -162,177 +265,20 @@ func (c *cardDisk2) processQ6Q7(in uint8) {
|
|||
}
|
||||
}
|
||||
|
||||
if c.dataLatch >= 0x80 {
|
||||
//fmt.Printf("Datalacth: 0x%.2x in cycle %v\n", c.dataLatch, c.a.cpu.GetCycles())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Stepper motor to position the track.
|
||||
|
||||
There are a number of group of four magnets. The stepper motor can be thought as a long
|
||||
line of groups of magnets, each group on the same configuration. We call phase each of those
|
||||
magnets. The cog is attracted to the enabled magnets, and can stay aligned to a magnet or
|
||||
between two.
|
||||
|
||||
Phases (magents): 3 2 1 0 3 2 1 0 3 2 1 0
|
||||
Cog direction (step withn a group): 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
|
||||
We will consider that the cog would go to the prefferred postion if there is one. Independenly
|
||||
of the previous position. The previous position is only used to know if it goes up or down
|
||||
a full group.
|
||||
*/
|
||||
|
||||
const (
|
||||
undefinedPosition = -1
|
||||
maxStep = 68 * 2 // What is the maximum quarter tracks a DiskII can go?
|
||||
stepsPerGroup = 8
|
||||
stepsPerTrack = 4
|
||||
)
|
||||
|
||||
var cogPositions = []int{
|
||||
undefinedPosition, // 0000, phases active
|
||||
0, // 0001
|
||||
2, // 0010
|
||||
1, // 0011
|
||||
4, // 0100
|
||||
undefinedPosition, // 0101
|
||||
3, // 0110
|
||||
2, // 0111
|
||||
6, // 1000
|
||||
7, // 1001
|
||||
undefinedPosition, // 1010
|
||||
0, // 1011
|
||||
5, // 1100
|
||||
6, // 1101
|
||||
4, // 1110
|
||||
undefinedPosition, // 1111
|
||||
}
|
||||
|
||||
func moveStep(phases uint8, prevStep int) int {
|
||||
|
||||
//fmt.Printf("magnets: 0x%x\n", phases)
|
||||
|
||||
cogPosition := cogPositions[phases]
|
||||
if cogPosition == undefinedPosition {
|
||||
// Don't move if magnets don't push on a defined direction.
|
||||
return prevStep
|
||||
}
|
||||
|
||||
prevPosition := prevStep % stepsPerGroup // Direction, step in the current group of magnets.
|
||||
delta := cogPosition - prevPosition
|
||||
if delta < 0 {
|
||||
delta = delta + stepsPerGroup
|
||||
}
|
||||
|
||||
var nextStep int
|
||||
if delta < 4 {
|
||||
// Steps up
|
||||
nextStep = prevStep + delta
|
||||
if nextStep > maxStep {
|
||||
nextStep = maxStep
|
||||
/*
|
||||
if c.dataLatch >= 0x80 {
|
||||
fmt.Printf("Datalach: 0x%.2x in cycle %v\n", c.dataLatch, c.a.cpu.GetCycles())
|
||||
}
|
||||
} else if delta == 4 {
|
||||
// Don't move if magnets push on the oposite direction
|
||||
nextStep = prevStep
|
||||
} else { // delta > 4
|
||||
// Steps down
|
||||
nextStep = prevStep + delta - stepsPerGroup
|
||||
if nextStep < 0 {
|
||||
nextStep = 0
|
||||
}
|
||||
}
|
||||
return nextStep
|
||||
*/
|
||||
}
|
||||
|
||||
func (d *cardDisk2Drive) insertDiskette(dt diskette) {
|
||||
d.diskette = dt
|
||||
}
|
||||
func (d *cardDisk2Drive) insertDiskette(name string) error {
|
||||
diskette, err := LoadDiskette(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *cardDisk2) save(w io.Writer) error {
|
||||
err := binary.Write(w, binary.BigEndian, c.selected)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.dataLatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.q6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.q7)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.drive[0].save(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.drive[1].save(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.cardBase.save(w)
|
||||
}
|
||||
|
||||
func (c *cardDisk2) load(r io.Reader) error {
|
||||
err := binary.Read(r, binary.BigEndian, &c.selected)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.dataLatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.q6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.q7)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.drive[0].load(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.drive[1].load(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.cardBase.load(r)
|
||||
}
|
||||
|
||||
func (d *cardDisk2Drive) save(w io.Writer) error {
|
||||
err := binary.Write(w, binary.BigEndian, d.power)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, d.phases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, d.tracksStep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *cardDisk2Drive) load(r io.Reader) error {
|
||||
err := binary.Read(r, binary.BigEndian, &d.power)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &d.phases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &d.tracksStep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.name = name
|
||||
d.diskette = diskette
|
||||
return nil
|
||||
}
|
||||
|
|
83
cardDisk2DriveStepper.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package izapple2
|
||||
|
||||
/*
|
||||
Stepper motor to position the track.
|
||||
|
||||
There are a number of group of four magnets. The stepper motor can be thought as a long
|
||||
line of groups of magnets, each group on the same configuration. We call phase each of those
|
||||
magnets. The cog is attracted to the enabled magnets, and can stay aligned to a magnet or
|
||||
between two.
|
||||
|
||||
Phases (magnets): 3 2 1 0 3 2 1 0 3 2 1 0
|
||||
Cog direction (step within a group): 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
|
||||
We will consider that the cog would go to the prefferred position if there is one. Independently
|
||||
of the previous position. The previous position is only used to know if it goes up or down
|
||||
a full group.
|
||||
|
||||
Phases are coded in 4 bits in an uint8. Q3, q2, q1 and q0 with q0 on the LSB.
|
||||
*/
|
||||
|
||||
const (
|
||||
undefinedPosition = -1
|
||||
maxStep = 68 * 2 // What is the maximum quarter tracks a DiskII can go?
|
||||
stepsPerGroup = 8
|
||||
stepsPerTrack = 4
|
||||
)
|
||||
|
||||
var cogPositions = []int{
|
||||
undefinedPosition, // 0000, phases active
|
||||
0, // 0001
|
||||
2, // 0010
|
||||
1, // 0011
|
||||
4, // 0100
|
||||
undefinedPosition, // 0101
|
||||
3, // 0110
|
||||
2, // 0111
|
||||
6, // 1000
|
||||
7, // 1001
|
||||
undefinedPosition, // 1010
|
||||
0, // 1011
|
||||
5, // 1100
|
||||
6, // 1101
|
||||
4, // 1110
|
||||
undefinedPosition, // 1111
|
||||
}
|
||||
|
||||
func moveDriveStepper(phases uint8, prevStep int) int {
|
||||
|
||||
//fmt.Printf("magnets: 0x%x\n", phases)
|
||||
|
||||
cogPosition := cogPositions[phases]
|
||||
if cogPosition == undefinedPosition {
|
||||
// Don't move if magnets don't push on a defined direction.
|
||||
return prevStep
|
||||
}
|
||||
|
||||
prevPosition := prevStep % stepsPerGroup // Direction, step in the current group of magnets.
|
||||
delta := cogPosition - prevPosition
|
||||
if delta < 0 {
|
||||
delta = delta + stepsPerGroup
|
||||
}
|
||||
|
||||
var nextStep int
|
||||
if delta < 4 {
|
||||
// Steps up
|
||||
nextStep = prevStep + delta
|
||||
if nextStep > maxStep {
|
||||
nextStep = maxStep
|
||||
}
|
||||
} else if delta == 4 {
|
||||
// Don't move if magnets push on the opposite direction
|
||||
nextStep = prevStep
|
||||
} else { // delta > 4
|
||||
// Steps down
|
||||
nextStep = prevStep + delta - stepsPerGroup
|
||||
if nextStep < 0 {
|
||||
nextStep = 0
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Printf("[DiskII] 1/4 track: %03d %vO\n", nextStep, strings.Repeat(" ", nextStep))
|
||||
return nextStep
|
||||
}
|
317
cardDisk2Sequencer.go
Normal file
|
@ -0,0 +1,317 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"github.com/ivanizag/izapple2/component"
|
||||
)
|
||||
|
||||
// TODO: fast mode
|
||||
|
||||
/*
|
||||
See:
|
||||
"Understanding the Apple II, chapter 9"
|
||||
Beneath Apple ProDOS, Appendix
|
||||
Phases: http://yesterbits.com/media/pubs/AppleOrchard/articles/disk-ii-part-1-1983-apr.pdf
|
||||
IMW Floppy Disk I/O Controller info: (https://www.brutaldeluxe.fr/documentation/iwm/apple2_IWM_INFO_19840510.pdf)
|
||||
Woz https://applesaucefdc.com/woz/reference2/
|
||||
Schematic: https://mirrors.apple2.org.za/ftp.apple.asimov.net/documentation/hardware/schematics/APPLE_DiskII_SCH.pdf
|
||||
|
||||
|
||||
*/
|
||||
|
||||
// CardDisk2Sequencer is a DiskII interface card with the Woz state machine
|
||||
type CardDisk2Sequencer struct {
|
||||
cardBase
|
||||
|
||||
p6ROM []uint8
|
||||
q [8]bool // 8-bit latch SN74LS259
|
||||
register uint8 // 8-bit shift/storage register SN74LS323
|
||||
sequence uint8 // 4 bits stored in an hex flip-flop SN74LS174
|
||||
motorDelay uint64 // NE556 timer, used to delay motor off
|
||||
drive [2]cardDisk2SequencerDrive
|
||||
|
||||
lastWriteValue bool // We write transitions to the WOZ file. We store the last value to send a pulse on change.
|
||||
lastPulseCycles uint8 // There is a new pulse every 4ms, that's 8 cycles of 2Mhz
|
||||
|
||||
lastCycle uint64 // 2 Mhz cycles
|
||||
|
||||
trackTracer trackTracer
|
||||
}
|
||||
|
||||
// Shared methods between both versions on the Disk II card
|
||||
type cardDisk2Shared interface {
|
||||
//insertDiskette(drive int, ...)
|
||||
setTrackTracer(tt trackTracer)
|
||||
}
|
||||
|
||||
const (
|
||||
disk2MotorOffDelay = uint64(2 * 1000 * 1000) // 2 Mhz cycles. Total 1 second.
|
||||
disk2PulseCyles = uint8(8) // 8 cycles = 4ms * 2Mhz
|
||||
|
||||
/*
|
||||
We skip register calculations for long periods with the motor
|
||||
on but not reading bytes. It's an optimizations, 10000 is too
|
||||
short for cross track sync copy protections.
|
||||
*/
|
||||
disk2CyclestoLoseSsync = 100000
|
||||
)
|
||||
|
||||
func newCardDisk2SequencerBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Disk II Sequencer",
|
||||
description: "Disk II interface card emulating the Woz state machine",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"disk1", "Diskette image for drive 1", ""},
|
||||
{"disk2", "Diskette image for drive 2", ""},
|
||||
{"tracktracer", "Trace how the disk head moves between tracks", "false"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardDisk2Sequencer
|
||||
err := c.loadRomFromResource("<internal>/DISK2.rom", cardRomSimple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, _, err := LoadResource("<internal>/DISK2P6.rom")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.p6ROM = data
|
||||
|
||||
disk1 := paramsGetString(params, "disk1")
|
||||
if disk1 != "" {
|
||||
err := c.drive[0].insertDiskette(disk1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
disk2 := paramsGetString(params, "disk2")
|
||||
if disk2 != "" {
|
||||
err := c.drive[1].insertDiskette(disk2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
trackTracer := paramsGetBool(params, "tracktracer")
|
||||
if trackTracer {
|
||||
c.trackTracer = makeTrackTracerLogger()
|
||||
}
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo returns card info
|
||||
func (c *CardDisk2Sequencer) GetInfo() map[string]string {
|
||||
info := make(map[string]string)
|
||||
info["rom"] = "16 sector"
|
||||
// TODO: add drives info
|
||||
return info
|
||||
}
|
||||
|
||||
func (c *CardDisk2Sequencer) reset() {
|
||||
// UtA2e 9-12, all switches forced to off
|
||||
c.q = [8]bool{}
|
||||
}
|
||||
|
||||
func (c *CardDisk2Sequencer) setTrackTracer(tt trackTracer) {
|
||||
c.trackTracer = tt
|
||||
}
|
||||
|
||||
func (c *CardDisk2Sequencer) assign(a *Apple2, slot int) {
|
||||
a.registerRemovableMediaDrive(&c.drive[0])
|
||||
a.registerRemovableMediaDrive(&c.drive[1])
|
||||
|
||||
c.addCardSoftSwitches(func(address uint8, data uint8, _ bool) uint8 {
|
||||
/*
|
||||
Slot card pins to SN74LS259 latch mapping:
|
||||
slot_address[3,2,1] => latch_address[2,1,0]
|
||||
slot_address[0] => latch_data
|
||||
slot_dev_selct => latch_write_enable ;It will be true
|
||||
*/
|
||||
c.q[address>>1] = (address & 1) != 0
|
||||
|
||||
// Advance the Disk2 state machine since the last call to softswitches
|
||||
c.catchUp(data)
|
||||
|
||||
/*
|
||||
Slot card pins to SN74LS259 mapping:
|
||||
slot_address[0] => latch_oe2_n
|
||||
*/
|
||||
registerOutputEnableNeg := (address & 1) != 0
|
||||
if !registerOutputEnableNeg {
|
||||
return c.register
|
||||
} else {
|
||||
return 33 // Floating
|
||||
}
|
||||
}, "DISK2SEQ")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *CardDisk2Sequencer) catchUp(data uint8) {
|
||||
currentCycle := c.a.cpu.GetCycles() << 1 // Disk2 cycles are x2 cpu cycle
|
||||
|
||||
motorOn := c.step(data, true)
|
||||
if motorOn && currentCycle > c.lastCycle+disk2CyclestoLoseSsync {
|
||||
// We have lost sync. We start the count.
|
||||
// We do at least a couple 2 Mhz cycles
|
||||
c.lastCycle = currentCycle - 2
|
||||
}
|
||||
c.lastCycle++
|
||||
|
||||
for motorOn && c.lastCycle <= currentCycle {
|
||||
motorOn = c.step(data, false)
|
||||
c.lastCycle++
|
||||
}
|
||||
|
||||
if !motorOn {
|
||||
c.lastCycle = 0 // Sync lost
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardDisk2Sequencer) step(data uint8, firstStep bool) bool {
|
||||
/*
|
||||
Q4 and Q6 set on the sofswitches is stored on the
|
||||
latch.
|
||||
*/
|
||||
q5 := c.q[5] // Drive selection
|
||||
q4 := c.q[4] // Motor on (before delay)
|
||||
|
||||
/*
|
||||
Motor On comes from the latched q4 via the 556 to
|
||||
provide a delay. The delay is reset while q4 is on.
|
||||
*/
|
||||
if q4 {
|
||||
c.motorDelay = disk2MotorOffDelay
|
||||
}
|
||||
motorOn := c.motorDelay > 0
|
||||
|
||||
/*
|
||||
The pins for the cable drives ENBL1 and ENBL2 are
|
||||
connected to q5 and motor using half of the 74LS132
|
||||
NAND to combine them.
|
||||
*/
|
||||
c.drive[0].enable(!q5 && motorOn)
|
||||
c.drive[1].enable(q5 && motorOn)
|
||||
|
||||
/*
|
||||
Motor on AND the 2 Mhz clock (Q3 pin 37 of the slot)
|
||||
are connected to the clok pulse of the shift register
|
||||
if off, the sequences does not advance. The and uses
|
||||
another quarter of the 74LS132 NAND.
|
||||
*/
|
||||
if !motorOn {
|
||||
c.sequence = 0
|
||||
return false
|
||||
}
|
||||
c.motorDelay--
|
||||
|
||||
/*
|
||||
Head movements. We assume it's instantaneous on Q0-Q3 change. We
|
||||
will place it on the first step.
|
||||
Q0 to Q3 are connected directly to the drives.
|
||||
*/
|
||||
if firstStep {
|
||||
q0 := c.q[0]
|
||||
q1 := c.q[1]
|
||||
q2 := c.q[2]
|
||||
q3 := c.q[3]
|
||||
c.drive[0].moveHead(q0, q1, q2, q3, c.trackTracer, c.slot, 0)
|
||||
c.drive[1].moveHead(q0, q1, q2, q3, c.trackTracer, c.slot, 1)
|
||||
}
|
||||
|
||||
/*
|
||||
The reading from the drive is converted to a pulse detecting
|
||||
changes using Q3 and Q4 of the flip flop, combined with
|
||||
the last quarter of the 74LS132 NAND.∫
|
||||
The woz format provides the pulse directly and we won't emulate
|
||||
this detection.
|
||||
*/
|
||||
pulse := false
|
||||
c.lastPulseCycles++
|
||||
if c.lastPulseCycles == disk2PulseCyles {
|
||||
// Read
|
||||
pulse = c.drive[0].readPulse() ||
|
||||
c.drive[1].readPulse()
|
||||
c.lastPulseCycles = 0
|
||||
}
|
||||
|
||||
/*
|
||||
The write protected signal comes directly from any of the
|
||||
drives being enabled (motor on) and write protected.
|
||||
*/
|
||||
wProt := (c.drive[0].enabled && c.drive[0].writeProtected) ||
|
||||
(c.drive[1].enabled && c.drive[1].writeProtected)
|
||||
|
||||
/*
|
||||
The next instruction for the sequencer is retrieved from
|
||||
the ROM P6 using the address:
|
||||
A0, A5, A6, A7 <= sequence from 74LS174
|
||||
A1 =< high, MSB of register (pin Q7)
|
||||
A2 <= Q6 from 9334
|
||||
A3 <= Q7 from 9334
|
||||
A4 <= pulse transition
|
||||
*/
|
||||
high := c.register >= 0x80
|
||||
seqBits := component.ByteToPins(c.sequence)
|
||||
romAddress := component.PinsToByte([8]bool{
|
||||
seqBits[1], // seq1
|
||||
high,
|
||||
c.q[6],
|
||||
c.q[7],
|
||||
!pulse,
|
||||
seqBits[0], // seq0
|
||||
seqBits[2], // seq2
|
||||
seqBits[3], // seq3
|
||||
})
|
||||
|
||||
romData := c.p6ROM[romAddress]
|
||||
inst := romData & 0xf
|
||||
next := romData >> 4
|
||||
|
||||
/*
|
||||
The pins for the register shifter update are:
|
||||
SR(CLR) <- ROM D3
|
||||
S1 <- ROM D0
|
||||
S0 <- ROM D1
|
||||
DS0(SR) <- WPROT pin of the selected drive
|
||||
DS7(SL) <- ROM D2
|
||||
IO[7.0] <-> D[0-7] slot data bus (the order is reversed)
|
||||
|
||||
*/
|
||||
if inst < 8 {
|
||||
c.register = 0 // Bit 4 clear to reset
|
||||
} else {
|
||||
switch inst & 0x3 { // Bit 0 and 1 are the operation
|
||||
case 0:
|
||||
// Nothing
|
||||
case 1:
|
||||
// Shift left bringing bit 1
|
||||
c.register = (c.register << 1) | ((inst >> 2) & 1)
|
||||
case 2:
|
||||
// Shift right bringing wProt
|
||||
c.register = c.register >> 1
|
||||
if wProt {
|
||||
c.register |= 0x80
|
||||
}
|
||||
case 3:
|
||||
// Load
|
||||
c.register = data
|
||||
}
|
||||
|
||||
if c.q[7] && (inst&0x3) != 0 {
|
||||
currentWriteValue := next >= 0x8
|
||||
writePulse := currentWriteValue != c.lastWriteValue
|
||||
c.drive[0].writePulse(writePulse)
|
||||
c.drive[1].writePulse(writePulse)
|
||||
c.lastWriteValue = currentWriteValue
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Printf("[D2SEQ] Step. seq:%x inst:%x next:%x reg:%02x\n",
|
||||
// c.sequence, inst, next, c.register)
|
||||
|
||||
c.sequence = next
|
||||
return true
|
||||
}
|
102
cardDisk2SequencerDrive.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
|
||||
"github.com/ivanizag/izapple2/component"
|
||||
"github.com/ivanizag/izapple2/storage"
|
||||
)
|
||||
|
||||
type cardDisk2SequencerDrive struct {
|
||||
data *storage.FileWoz
|
||||
enabled bool
|
||||
writeProtected bool
|
||||
currentQuarterTrack int
|
||||
|
||||
position uint32 // Current position on the track
|
||||
positionMax uint32 // As tracks may have different lengths position is related of positionMax of the las track
|
||||
|
||||
mc3470Buffer uint8 // Four bit buffer to detect weak bits and to add latency
|
||||
}
|
||||
|
||||
func (d *cardDisk2SequencerDrive) insertDiskette(filename string) error {
|
||||
data, writeable, err := LoadResource(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := storage.NewFileWoz(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Discard not supported features
|
||||
if f.Info.DiskType != 1 {
|
||||
return errors.New("only 5.25 disks are supported")
|
||||
}
|
||||
if f.Info.BootSectorFormat == 2 { // Info not available in WOZ 1.0
|
||||
return errors.New("woz 13 sector disks are not supported")
|
||||
}
|
||||
|
||||
d.data = f
|
||||
d.writeProtected = !writeable
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *cardDisk2SequencerDrive) enable(enabled bool) {
|
||||
d.enabled = enabled
|
||||
}
|
||||
|
||||
func (d *cardDisk2SequencerDrive) moveHead(q0, q1, q2, q3 bool, trackTracer trackTracer, slot int, driveNumber int) {
|
||||
if !d.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
phases := component.PinsToByte([8]bool{
|
||||
q0, q1, q2, q3,
|
||||
false, false, false, false,
|
||||
})
|
||||
d.currentQuarterTrack = moveDriveStepper(phases, d.currentQuarterTrack)
|
||||
|
||||
if trackTracer != nil {
|
||||
trackTracer.traceTrack(d.currentQuarterTrack, slot, driveNumber)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *cardDisk2SequencerDrive) readPulse() bool {
|
||||
if !d.enabled || d.data == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get next bit taking into account the MC3470 latency and weak bits
|
||||
var fluxBit bool
|
||||
fluxBit, d.position, d.positionMax = d.data.GetNextBitAndPosition(
|
||||
d.position,
|
||||
d.positionMax,
|
||||
d.currentQuarterTrack)
|
||||
d.mc3470Buffer = (d.mc3470Buffer << 1) & 0x0f
|
||||
if fluxBit {
|
||||
d.mc3470Buffer++
|
||||
}
|
||||
bit := ((d.mc3470Buffer >> 1) & 0x1) != 0 // Use the previous to last bit to add latency
|
||||
if d.mc3470Buffer == 0 && rand.Intn(100) < 30 {
|
||||
// Four consecutive zeros. It'a a fake bit.
|
||||
// Output a random value. 70% zero, 30% one
|
||||
bit = true
|
||||
}
|
||||
|
||||
return bit
|
||||
}
|
||||
|
||||
func (d *cardDisk2SequencerDrive) writePulse(value bool) {
|
||||
if d.writeProtected || !d.enabled || d.data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
d.data.SetBit(
|
||||
value,
|
||||
d.position,
|
||||
d.positionMax,
|
||||
d.currentQuarterTrack)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
/*
|
||||
Simulates just what is needed to make Total Replay use fast mode. Can change
|
||||
|
@ -6,23 +6,34 @@ from controlled speed to max speed the emulator can do.
|
|||
Note: It ends up not being useful for Total Replay as loading from HD is already
|
||||
very fast. HD blocks are loaded directly on the emulated RAM.
|
||||
|
||||
Note that it doesn't intefere with the Apple IIe 80 columns in slot 3. It doesn't
|
||||
have ROM or slot specific sofswitches.
|
||||
|
||||
|
||||
See:
|
||||
https://github.com/a2-4am/4cade/blob/master/src/hw.accel.a
|
||||
http://www.a2heaven.com/webshop/resources/pdf_document/18/82/c.pdf
|
||||
|
||||
*/
|
||||
|
||||
type cardFastChip struct {
|
||||
// CardFastChip represents a
|
||||
type CardFastChip struct {
|
||||
cardBase
|
||||
unlocked bool
|
||||
unlockCounter uint8
|
||||
enabled bool
|
||||
accelerated bool
|
||||
unlocked bool
|
||||
unlockCounter uint8
|
||||
enabled bool
|
||||
accelerated bool
|
||||
configRegister uint8
|
||||
}
|
||||
|
||||
func buildFastChipRom() []uint8 {
|
||||
data := make([]uint8, 256)
|
||||
return data
|
||||
func newCardFastChipBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "FASTChip IIe Card - limited",
|
||||
description: "Accelerator card for Apple IIe (limited support)",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
return &CardFastChip{}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -31,10 +42,10 @@ const (
|
|||
fastChipNormalSpeed = uint8(9)
|
||||
)
|
||||
|
||||
func (c *cardFastChip) assign(a *Apple2, slot int) {
|
||||
func (c *CardFastChip) assign(a *Apple2, slot int) {
|
||||
// The softswitches are outside the card reserved ss
|
||||
// Only writes are implemented to avoid conflicts with the joysticks
|
||||
a.io.addSoftSwitchW(0x6a, func(_ *ioC0Page, value uint8) {
|
||||
a.io.addSoftSwitchW(0x6a, func(value uint8) {
|
||||
if value == fastChipUnlockToken {
|
||||
c.unlockCounter++
|
||||
if c.unlockCounter >= fastChipUnlockRepeats {
|
||||
|
@ -43,30 +54,47 @@ func (c *cardFastChip) assign(a *Apple2, slot int) {
|
|||
} else {
|
||||
c.unlockCounter = 0
|
||||
c.unlocked = false
|
||||
c.enabled = false
|
||||
}
|
||||
}, "FASTCHIP-LOCK")
|
||||
|
||||
a.io.addSoftSwitchW(0x6b, func(_ *ioC0Page, _ uint8) {
|
||||
a.io.addSoftSwitchW(0x6b, func(uint8) {
|
||||
if c.unlocked {
|
||||
c.enabled = true
|
||||
}
|
||||
}, "FASTCHIP-ENABLE")
|
||||
|
||||
a.io.addSoftSwitchW(0x6d, func(_ *ioC0Page, value uint8) {
|
||||
if c.unlocked && c.enabled {
|
||||
newAccelerated := (value > fastChipNormalSpeed)
|
||||
if newAccelerated == c.accelerated {
|
||||
// No change requested
|
||||
return
|
||||
}
|
||||
if newAccelerated {
|
||||
a.requestFastMode()
|
||||
} else {
|
||||
a.releaseFastMode()
|
||||
}
|
||||
c.accelerated = newAccelerated
|
||||
a.io.addSoftSwitchW(0x6d, func(value uint8) {
|
||||
if c.enabled {
|
||||
c.setSpeed(a, value)
|
||||
}
|
||||
}, "FASTCHIP-SPEED")
|
||||
|
||||
a.io.addSoftSwitchW(0x6e, func(value uint8) {
|
||||
if c.enabled {
|
||||
c.configRegister = value
|
||||
}
|
||||
}, "FASTCHIP-CONFIG")
|
||||
|
||||
a.io.addSoftSwitchW(0x6f, func(value uint8) {
|
||||
if c.enabled && c.configRegister == 0 {
|
||||
c.setSpeed(a, value)
|
||||
}
|
||||
}, "FASTCHIP-CONFIG")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *CardFastChip) setSpeed(a *Apple2, value uint8) {
|
||||
newAccelerated := (value > fastChipNormalSpeed)
|
||||
if newAccelerated == c.accelerated {
|
||||
// No change requested
|
||||
return
|
||||
}
|
||||
if newAccelerated {
|
||||
a.RequestFastMode()
|
||||
} else {
|
||||
a.ReleaseFastMode()
|
||||
}
|
||||
c.accelerated = newAccelerated
|
||||
}
|
||||
|
|
203
cardHardDisk.go
|
@ -1,203 +0,0 @@
|
|||
package apple2
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
To implement a hard drive we just have to support boot from #PR7 and the PRODOS expextations.
|
||||
|
||||
See:
|
||||
Beneath Prodos, section 6-6, 7-13 and 5-8. (http://www.apple-iigs.info/doc/fichiers/beneathprodos.pdf)
|
||||
Apple IIc Technical Reference, 2nd Edition. Chapter 8. https://ia800207.us.archive.org/19/items/AppleIIcTechnicalReference2ndEd/Apple%20IIc%20Technical%20Reference%202nd%20ed.pdf
|
||||
https://prodos8.com/docs/technote/21/
|
||||
|
||||
|
||||
*/
|
||||
|
||||
type cardHardDisk struct {
|
||||
cardBase
|
||||
disk *hardDisk
|
||||
trace bool
|
||||
}
|
||||
|
||||
func buildHardDiskRom(slot int) []uint8 {
|
||||
data := make([]uint8, 256)
|
||||
ssBase := 0x80 + uint8(slot<<4)
|
||||
|
||||
copy(data, []uint8{
|
||||
// Preamble bytes to comply with the expectation in $Cn01, 3, 5 and 7
|
||||
0xa9, 0x20, // LDA #$20
|
||||
0xa9, 0x00, // LDA #$00
|
||||
0xa9, 0x03, // LDA #$03
|
||||
0xa9, 0x3c, // LDA #$3c
|
||||
// Alternate: 0xa9, 0x00, // LDA #$00 ; Not a Smartport device, but won't boot on ii+ ROM
|
||||
|
||||
// Boot code: SS will load block 0 in address $0800. The jump there.
|
||||
// Note: after execution the first block expects $42 to $47 to have
|
||||
// valid values to read block 0. At least Total Replay expects that.
|
||||
0xa9, 0x01, // LDA·#$01
|
||||
0x85, 0x42, // STA $42 ; Command READ(1)
|
||||
0xa9, 0x00, // LDA·#$00
|
||||
0x85, 0x43, // STA $43 ; Unit 0
|
||||
0x85, 0x44, // STA $44 ; Dest LO($0800)
|
||||
0x85, 0x46, // STA $46 ; Block LO(0)
|
||||
0x85, 0x47, // STA $47 ; Block HI(0)
|
||||
0xa9, 0x08, // LDA·#$08
|
||||
0x85, 0x45, // STA $45 ; Dest HI($0800)
|
||||
|
||||
0xad, ssBase, 0xc0, // LDA $C0n1 ;Call to softswitch 0.
|
||||
0xa2, uint8(slot << 4), // LDX $s7 ; Slot on hign nibble of X
|
||||
0x4c, 0x01, 0x08, // JMP $801 ; Jump to loaded boot sector
|
||||
})
|
||||
|
||||
// Entrypoints and Smartport body
|
||||
copy(data[0x40:], []uint8{
|
||||
0x4c, 0x80, 0xc0 + uint8(slot), // JMP $cs80 ; Prodos Entrypoint
|
||||
|
||||
// 3 btes later, smartport entrypoint. Uses the ProDos MLI calling convention
|
||||
0x68, // PLA
|
||||
0x8d, ssBase + 4, 0xc0, // STA $c0n4 ; Softswitch 4, store LO(cmdblock)
|
||||
0xa8, // TAY ; We will need it later
|
||||
0x68, // PLA
|
||||
0x8d, ssBase + 5, 0xc0, // STA $c0n5 ; Softswitch 5, store HI(cmdblock)
|
||||
0x48, // PHA
|
||||
0x98, // TYA
|
||||
0x69, 0x03, // ADC #$03 ; Fix return address past the cmdblock
|
||||
0x48, // PHA
|
||||
0xad, ssBase + 3, 0xc0, // LDA $C0n3 ; Softswitch 3, execute command. Error code in reg A.
|
||||
0x18, // CLC ; Clear carry for no errors.
|
||||
0xF0, 0x01, // BEQ $01 ; Skips the SEC if reg A is zero
|
||||
0x38, // SEC ; Set carry on errors
|
||||
0x60, // RTS
|
||||
})
|
||||
|
||||
// Prodos entrypoint body
|
||||
copy(data[0x80:], []uint8{
|
||||
0xad, ssBase + 0, 0xc0, // LDA $C0n0 ; Softswitch 0, execute command. Error code in reg A.
|
||||
0x48, // PHA
|
||||
0xae, ssBase + 1, 0xc0, // LDX $C0n1 ; Softswitch 1, LO(Blocks), STATUS needs that in reg X.
|
||||
0xac, ssBase + 2, 0xc0, // LDY $C0n2 ; Softswitch 2, HI(Blocks). STATUS needs that in reg Y.
|
||||
0x18, // CLC ; Clear carry for no errors.
|
||||
0x68, // PLA ; Sets Z if no error
|
||||
0xF0, 0x01, // BEQ $01 ; Skips the SEC if reg A is zero
|
||||
0x38, // SEC ; Set carry on errors
|
||||
0x60, // RTS
|
||||
})
|
||||
|
||||
data[0xfc] = 0
|
||||
data[0xfd] = 0
|
||||
data[0xfe] = 3 // Status and Read. No write, no format. Single volume
|
||||
data[0xff] = 0x40 // Driver entry point
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const (
|
||||
proDosDeviceCommandStatus = 0
|
||||
proDosDeviceCommandRead = 1
|
||||
proDosDeviceCommandWrite = 2
|
||||
proDosDeviceCommandFormat = 3
|
||||
)
|
||||
|
||||
const (
|
||||
proDosDeviceNoError = uint8(0)
|
||||
proDosDeviceErrorIO = uint8(0x27)
|
||||
proDosDeviceErrorNoDevice = uint8(0x28)
|
||||
proDosDeviceErrorWriteProtected = uint8(0x2b)
|
||||
)
|
||||
|
||||
func (c *cardHardDisk) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchR(0, func(*ioC0Page) uint8 {
|
||||
|
||||
// Prodos entry point
|
||||
command := a.mmu.Peek(0x42)
|
||||
unit := a.mmu.Peek(0x43)
|
||||
address := uint16(a.mmu.Peek(0x44)) + uint16(a.mmu.Peek(0x45))<<8
|
||||
block := uint16(a.mmu.Peek(0x46)) + uint16(a.mmu.Peek(0x47))<<8
|
||||
if c.trace {
|
||||
fmt.Printf("[CardHardDisk] Command %v on unit $%x, block %v to $%x.\n", command, unit, block, address)
|
||||
}
|
||||
|
||||
switch command {
|
||||
case proDosDeviceCommandStatus:
|
||||
return proDosDeviceNoError
|
||||
case proDosDeviceCommandRead:
|
||||
return c.readBlock(block, address)
|
||||
case proDosDeviceCommandWrite:
|
||||
return c.writeBlock(block, address)
|
||||
default:
|
||||
// Prodos device command not supported
|
||||
return proDosDeviceErrorIO
|
||||
}
|
||||
}, "HDCOMMAND")
|
||||
c.addCardSoftSwitchR(1, func(*ioC0Page) uint8 {
|
||||
// Blocks available, low byte
|
||||
return uint8(c.disk.header.Blocks)
|
||||
}, "HDBLOCKSLO")
|
||||
c.addCardSoftSwitchR(2, func(*ioC0Page) uint8 {
|
||||
// Blocks available, high byte
|
||||
return uint8(c.disk.header.Blocks >> 8)
|
||||
}, "HDBLOCKHI")
|
||||
|
||||
c.addCardSoftSwitchR(3, func(*ioC0Page) uint8 {
|
||||
if c.trace {
|
||||
fmt.Printf("[CardHardDisk] Smart port command. Not implemented.\n")
|
||||
}
|
||||
return proDosDeviceErrorIO
|
||||
}, "HDSMARTPORT")
|
||||
c.addCardSoftSwitchW(4, func(*ioC0Page, uint8) {
|
||||
// Not implemented
|
||||
}, "HDSMARTPORTLO")
|
||||
c.addCardSoftSwitchW(5, func(*ioC0Page, uint8) {
|
||||
// Not implemented
|
||||
}, "HDSMARTPORTHI")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *cardHardDisk) readBlock(block uint16, dest uint16) uint8 {
|
||||
if c.trace {
|
||||
fmt.Printf("[CardHardDisk] Read block %v into $%x.\n", block, dest)
|
||||
}
|
||||
|
||||
data, err := c.disk.read(uint32(block))
|
||||
if err != nil {
|
||||
return proDosDeviceErrorIO
|
||||
}
|
||||
// Byte by byte transfer to memory using the full Poke code path
|
||||
for i := uint16(0); i < uint16(proDosBlockSize); i++ {
|
||||
c.a.mmu.Poke(dest+i, data[i])
|
||||
}
|
||||
|
||||
return proDosDeviceNoError
|
||||
}
|
||||
|
||||
func (c *cardHardDisk) writeBlock(block uint16, source uint16) uint8 {
|
||||
if c.trace {
|
||||
fmt.Printf("[CardHardDisk] Write block %v from $%x.\n", block, source)
|
||||
}
|
||||
|
||||
if c.disk.readOnly {
|
||||
return proDosDeviceErrorWriteProtected
|
||||
}
|
||||
|
||||
// Byte by byte transfer from memory using the full Peek code path
|
||||
buf := make([]uint8, proDosBlockSize)
|
||||
for i := uint16(0); i < uint16(proDosBlockSize); i++ {
|
||||
buf[i] = c.a.mmu.Peek(source + i)
|
||||
}
|
||||
|
||||
err := c.disk.write(uint32(block), buf)
|
||||
if err != nil {
|
||||
return proDosDeviceErrorIO
|
||||
}
|
||||
|
||||
return proDosDeviceNoError
|
||||
}
|
||||
|
||||
func (c *cardHardDisk) addDisk(disk *hardDisk) {
|
||||
c.disk = disk
|
||||
}
|
||||
|
||||
func (c *cardHardDisk) setTrace(trace bool) {
|
||||
c.trace = trace
|
||||
}
|
218
cardInOut.go
|
@ -1,7 +1,9 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
/*
|
||||
|
@ -11,137 +13,163 @@ See:
|
|||
"Apple II Monitors peeled."
|
||||
http://mysite.du.edu/~etuttle/math/acia.htm
|
||||
|
||||
|
||||
PR#n stores Cn00 in CSWL and CSWH
|
||||
IN#n stores Cn00 in KSWL and KSWH
|
||||
*/
|
||||
|
||||
type cardInOut struct {
|
||||
// CardInOut is an experimental card to bridge with the host console
|
||||
type CardInOut struct {
|
||||
cardBase
|
||||
i int
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
func (c *cardInOut) assign(a *Apple2, slot int) {
|
||||
for i := uint8(0x0); i <= 0xf; i++ {
|
||||
iCopy := i
|
||||
c.addCardSoftSwitchR(i, func(*ioC0Page) uint8 {
|
||||
value := []uint8{0xc1, 0xc1, 0x93, 0x0}[c.i%4]
|
||||
c.i++
|
||||
fmt.Printf("[cardInOut] Read access to softswith 0x%x for slot %v, value %x.\n", iCopy, slot, value)
|
||||
//return 0x41 + 0x80
|
||||
return []uint8{0x41, 0x41, 0x13}[i%3] + 0x80
|
||||
}, "INOUTR")
|
||||
c.addCardSoftSwitchW(i, func(_ *ioC0Page, value uint8) {
|
||||
fmt.Printf("[cardInOut] Write access to softswith 0x%x for slot %v, value 0x%x.\n", iCopy, slot, value)
|
||||
}, "INOUTW")
|
||||
func newCardInOutBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "InOut test card",
|
||||
description: "Card to test I/O",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
return &CardInOut{}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
in := true
|
||||
out := false
|
||||
func (c *CardInOut) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchR(0, func() uint8 {
|
||||
if c.reader == nil {
|
||||
c.reader = bufio.NewReader(os.Stdin)
|
||||
}
|
||||
value, err := c.reader.ReadByte()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
value += 0x80
|
||||
if value&0x7f == 10 {
|
||||
value = 13 + 0x80
|
||||
}
|
||||
//fmt.Printf("[cardInOut] Read access to softswith 0x%x for slot %v, value %x.\n", 0, slot, value)
|
||||
return value
|
||||
}, "INOUTR")
|
||||
c.addCardSoftSwitchW(1, func(value uint8) {
|
||||
//fmt.Printf("[cardInOut] Write access to softswith 0x%x for slot %v, value 0x%x: %v, %v.\n", 1, slot, value, value&0x7f, string(value&0x7f))
|
||||
if value&0x7f == 13 {
|
||||
fmt.Printf("\n")
|
||||
} else {
|
||||
fmt.Printf("%v", string(value&0x7f))
|
||||
}
|
||||
|
||||
}, "INOUTW")
|
||||
|
||||
data := buildBaseInOutRom(slot)
|
||||
c.romCsxx = newMemoryRangeROM(0xC200, data[:], "InOUt card")
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func buildBaseInOutRom(slot int) []uint8 {
|
||||
data := [256]uint8{
|
||||
// Register
|
||||
0xA9, 0xC2,
|
||||
0x85, 0x37,
|
||||
0x85, 0x39,
|
||||
0xA9, 0x10,
|
||||
0x85, 0x36,
|
||||
0xA9, 0x15,
|
||||
0x85, 0x38,
|
||||
0x60, 0xEA,
|
||||
0x4c, 0x40, 0xc2, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
|
||||
// Out char
|
||||
0x8D, 0xA1, 0xC0,
|
||||
0x60, 0xEA,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
|
||||
// Get char
|
||||
0x91, 0x28,
|
||||
0xAD, 0xA0, 0xC0,
|
||||
0x60,
|
||||
0x48, 0xA5, 0x38, 0xD0, 0x11, 0xA9, 0xC2, 0xC5,
|
||||
0x39, 0xD0, 0x0B, 0xAD, 0x4F, 0x85, 0x38, 0x68,
|
||||
|
||||
0x91, 0x28, 0xAD, 0xA0, 0xC0, 0x60, 0x68, 0x8D,
|
||||
0xA1, 0xC0, 0x60,
|
||||
}
|
||||
|
||||
if !out {
|
||||
// NOP the CSWL,H change
|
||||
for _, v := range []uint8{2, 3, 8, 9} {
|
||||
data[v] = 0xEA
|
||||
}
|
||||
}
|
||||
// Fix slot dependant addresses
|
||||
data[0x02] = uint8(0xc0 + slot)
|
||||
data[0x46] = uint8(0xc0 + slot)
|
||||
data[0x53] = uint8(0x80 + slot<<4)
|
||||
data[0x58] = uint8(0x81 + slot<<4)
|
||||
|
||||
if !in {
|
||||
// NOP the KSWL,H change
|
||||
for _, v := range []uint8{4, 5, 12, 13} {
|
||||
data[v] = 0xEA
|
||||
}
|
||||
}
|
||||
|
||||
c.rom = newMemoryRange(0xC200, data[0:255])
|
||||
|
||||
if slot != 2 {
|
||||
// To make ifwork on other slots, patch C2, A0 and A1
|
||||
panic("Assert failed. Only slot 2 supported for the InOut card")
|
||||
}
|
||||
c.cardBase.assign(a, slot)
|
||||
return data[:]
|
||||
}
|
||||
|
||||
/*
|
||||
The ROM code was assembled using https://www.masswerk.at/6502/assembler.html
|
||||
|
||||
We will have $Cn00 as the entry point for CSWL/H. But before doing
|
||||
anything we have to check that we are not in $Cn00 because of an IN#.
|
||||
To da that we check id KSWL/H is $Cn00, it is it we wif it to INEntry.
|
||||
|
||||
src:
|
||||
BASL = $28
|
||||
CSWL = $36
|
||||
CSWH = $37
|
||||
KSWL = $38
|
||||
KSWH = $39
|
||||
|
||||
* = $C200
|
||||
Register:
|
||||
Entry:
|
||||
JMP SkipHeader
|
||||
|
||||
* = $C240
|
||||
SkipHeader:
|
||||
PHA
|
||||
LDA *KSWL
|
||||
BNE PREntry
|
||||
LDA #$C2
|
||||
STA *CSWH
|
||||
STA *KSWH
|
||||
LDA #$10
|
||||
STA *CSWL
|
||||
LDA #$15
|
||||
CMP *KSWH
|
||||
BNE PREntry
|
||||
FixKSWL:
|
||||
LDA #<INEntry
|
||||
STA *KSWL
|
||||
RTS
|
||||
NOP
|
||||
OutChar:
|
||||
STA $C0A1
|
||||
RTS
|
||||
NOP
|
||||
GetChar:
|
||||
INEntry:
|
||||
PLA
|
||||
STA (BASL),Y
|
||||
LDA $C0A0
|
||||
RTS
|
||||
PREntry:
|
||||
PLA
|
||||
STA $C0A1
|
||||
RTS
|
||||
|
||||
|
||||
assembled as:
|
||||
|
||||
Listing:
|
||||
pass 2
|
||||
|
||||
0000 BASL = 0028
|
||||
0000 CSWL = 0036
|
||||
0000 CSWH = 0037
|
||||
0000 KSWL = 0038
|
||||
0000 KSWH = 0039
|
||||
|
||||
* = $C200
|
||||
C200 REGIST
|
||||
C200 LDA #$C2 A9 C2
|
||||
C202 STA *CSWH 85 37
|
||||
C204 STA *KSWH 85 39
|
||||
C206 LDA #$10 A9 10
|
||||
C208 STA *CSWL 85 36
|
||||
C20A LDA #$15 A9 15
|
||||
C20C STA *KSWL 85 38
|
||||
C20E RTS 60
|
||||
C20F NOP EA
|
||||
C210 OUTCHA
|
||||
C210 STA $C0A1 8D A1 C0
|
||||
C213 RTS 60
|
||||
C214 NOP EA
|
||||
C215 GETCHA
|
||||
C215 STA (BASL),Y 91 28
|
||||
C217 LDA $C0A0 AD A0 C0
|
||||
C21A RTS
|
||||
* = $C200
|
||||
C200 ENTRY:
|
||||
C200 JMP SKIPHE 4C 40 C2
|
||||
|
||||
object code:
|
||||
A9 C2 85 37 85 39 A9 10
|
||||
85 36 A9 15 85 38 60 EA
|
||||
8D A1 C0 60 EA 91 28 AD
|
||||
A0 C0 60
|
||||
* = $C240
|
||||
C240 SKIPHE
|
||||
C240 PHA 48
|
||||
C241 LDA *KSWL A5 38
|
||||
C243 BNE PRENTR D0 11
|
||||
C245 LDA #$C2 A9 C2
|
||||
C247 CMP *KSWH C5 39
|
||||
C249 BNE PRENTR D0 0B
|
||||
C24B LDA #<INENTR A9 4F
|
||||
C24D STA *KSWL 85 38
|
||||
C24F INENTR
|
||||
C24F PLA 68
|
||||
C250 STA (BASL),Y 91 28
|
||||
C252 LDA $C0A0 AD A0 C0
|
||||
C255 RTS 60
|
||||
C256 PRENTR
|
||||
C256 PLA 68
|
||||
C257 STA $C0A1 8D A1 C0
|
||||
C25A RTS 60
|
||||
|
||||
done.
|
||||
|
||||
Object Code:
|
||||
c200:
|
||||
4C 40 C2
|
||||
c240:
|
||||
48 A5 38 D0 11
|
||||
A9 C2 C5 39 D0 0B A9 4F
|
||||
85 38 68 91 28 AD A0 C0
|
||||
60 68 8D A1 C0 60
|
||||
*/
|
||||
|
|
106
cardLanguage.go
|
@ -1,9 +1,4 @@
|
|||
package apple2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
package izapple2
|
||||
|
||||
/*
|
||||
Language card with 16 extra kb for the Apple ][ and ][+
|
||||
|
@ -16,7 +11,7 @@ Note also that language cards for the Apple ][ had ROM on
|
|||
board to replace the main board F8 ROM with Autostart. That
|
||||
was not used/needed on the Apple ][+. As this emulates the
|
||||
Apple ][+, it is not considered. For the Plus it is often
|
||||
refered as Language card but it is really a 16 KB Ram card,
|
||||
referred as Language card but it is really a 16 KB Ram card,
|
||||
|
||||
|
||||
"When RAM is deselected, the ROM on the Language card is selected for
|
||||
|
@ -26,15 +21,30 @@ for $D000-$F7FF.
|
|||
Power on RESET initializes ROM to read mode and RAM to write mode,
|
||||
and selects the second 4K bank to map $D000-$DFFF."
|
||||
|
||||
Writing to the softswitch disables writing in LC? Saw that
|
||||
somewhere but doing so fails IIe self check.
|
||||
|
||||
|
||||
*/
|
||||
|
||||
type cardLanguage struct {
|
||||
// CardLanguage is an Language Card
|
||||
type CardLanguage struct {
|
||||
cardBase
|
||||
readState bool
|
||||
writeState uint8
|
||||
altBank bool // false is bank1, true is bank2
|
||||
}
|
||||
|
||||
func newCardLanguageBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "16 KB Language Card",
|
||||
description: "Language card with 16 extra KB for the Apple ][ and ][+",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
return &CardLanguage{}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// Write enabling requires two softswitch accesses
|
||||
lcWriteDisabled = 0
|
||||
|
@ -42,24 +52,30 @@ const (
|
|||
lcWriteEnabled = 2
|
||||
)
|
||||
|
||||
func (c *cardLanguage) assign(a *Apple2, slot int) {
|
||||
func (c *CardLanguage) reset() {
|
||||
if c.a.isApple2e {
|
||||
// UtA2e 1-3, 5-23
|
||||
c.readState = false
|
||||
c.writeState = lcWriteEnabled
|
||||
c.altBank = true // Start on bank2
|
||||
c.applyState()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *CardLanguage) assign(a *Apple2, slot int) {
|
||||
c.readState = false
|
||||
c.writeState = lcWriteEnabled
|
||||
c.altBank = true // Start on bank2
|
||||
|
||||
if a.isApple2e {
|
||||
// The Apple //e with 128kb has two blocks of language upper RAM
|
||||
a.mmu.initLanguageRAM(2)
|
||||
} else {
|
||||
a.mmu.initLanguageRAM(1)
|
||||
}
|
||||
a.mmu.initLanguageRAM(1)
|
||||
for i := uint8(0x0); i <= 0xf; i++ {
|
||||
iCopy := i
|
||||
c.addCardSoftSwitchR(iCopy, func(*ioC0Page) uint8 {
|
||||
c.addCardSoftSwitchR(iCopy, func() uint8 {
|
||||
c.ssAction(iCopy, false)
|
||||
return 0
|
||||
}, "LANGCARDR")
|
||||
c.addCardSoftSwitchW(iCopy, func(*ioC0Page, uint8) {
|
||||
c.addCardSoftSwitchW(iCopy, func(uint8) {
|
||||
c.ssAction(iCopy, true)
|
||||
}, "LANGCARDW")
|
||||
}
|
||||
|
@ -68,7 +84,7 @@ func (c *cardLanguage) assign(a *Apple2, slot int) {
|
|||
c.applyState()
|
||||
}
|
||||
|
||||
func (c *cardLanguage) ssAction(ss uint8, write bool) {
|
||||
func (c *CardLanguage) ssAction(ss uint8, write bool) {
|
||||
c.altBank = ((ss >> 3) & 1) == 0
|
||||
action := ss & 0x3
|
||||
switch action {
|
||||
|
@ -79,7 +95,9 @@ func (c *cardLanguage) ssAction(ss uint8, write bool) {
|
|||
case 1:
|
||||
// ROM read, RAM write
|
||||
c.readState = false
|
||||
c.writeState++
|
||||
if !write {
|
||||
c.writeState++
|
||||
}
|
||||
case 2:
|
||||
// ROM read, no writes
|
||||
c.readState = false
|
||||
|
@ -87,54 +105,24 @@ func (c *cardLanguage) ssAction(ss uint8, write bool) {
|
|||
case 3:
|
||||
//RAM read, RAM write
|
||||
c.readState = true
|
||||
c.writeState++
|
||||
if !write {
|
||||
c.writeState++
|
||||
}
|
||||
}
|
||||
|
||||
if write && c.writeState == lcWriteHalfEnabled {
|
||||
// UtA2e, 5-23. It is reset by even read access or any write acccess in the $C08x range
|
||||
// And https://github.com/zellyn/a2audit/issues/3
|
||||
c.writeState = lcWriteDisabled
|
||||
}
|
||||
|
||||
if c.writeState > lcWriteEnabled {
|
||||
c.writeState = lcWriteEnabled
|
||||
}
|
||||
|
||||
// Writing to the softswtich disables writes.
|
||||
if write {
|
||||
c.writeState = lcWriteDisabled
|
||||
}
|
||||
|
||||
c.applyState()
|
||||
}
|
||||
|
||||
func (c *cardLanguage) applyState() {
|
||||
func (c *CardLanguage) applyState() {
|
||||
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
|
||||
}
|
||||
|
||||
func (c *cardLanguage) save(w io.Writer) error {
|
||||
err := binary.Write(w, binary.BigEndian, c.readState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.writeState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.altBank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.cardBase.save(w)
|
||||
}
|
||||
|
||||
func (c *cardLanguage) load(r io.Reader) error {
|
||||
err := binary.Read(r, binary.BigEndian, &c.readState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.writeState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.altBank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.applyState()
|
||||
return c.cardBase.load(r)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -8,21 +8,30 @@ import (
|
|||
Logger card. It never existed, I use it to trace accesses to the card.
|
||||
*/
|
||||
|
||||
type cardLogger struct {
|
||||
// CardLogger is a fake card to log soft switch invocations
|
||||
type CardLogger struct {
|
||||
cardBase
|
||||
}
|
||||
|
||||
func (c *cardLogger) assign(a *Apple2, slot int) {
|
||||
for i := uint8(0x0); i <= 0xf; i++ {
|
||||
iCopy := i
|
||||
c.addCardSoftSwitchR(i, func(*ioC0Page) uint8 {
|
||||
fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", iCopy, slot)
|
||||
return 0
|
||||
}, "LOGGERR")
|
||||
c.addCardSoftSwitchW(i, func(_ *ioC0Page, value uint8) {
|
||||
fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%v.\n", iCopy, slot, value)
|
||||
}, "LOGGERW")
|
||||
func newCardLoggerBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Softswitch logger card",
|
||||
description: "Card to log softswitch accesses",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
return &CardLogger{}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardLogger) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitches(func(address uint8, data uint8, write bool) uint8 {
|
||||
if write {
|
||||
fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%02x.\n", address, slot, data)
|
||||
} else {
|
||||
fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", address, slot)
|
||||
}
|
||||
return 0
|
||||
}, "LOGGER")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
|
144
cardMemoryExpansion.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package izapple2
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
Apple II Memory Expansion Card
|
||||
|
||||
See:
|
||||
|
||||
http://www.apple-iigs.info/doc/fichiers/a2me.pdf
|
||||
http://ae.applearchives.com/files/RamFactor_Manual_1.5.pdf
|
||||
http://www.1000bit.it/support/manuali/apple/technotes/memx/tn.memx.1.html
|
||||
|
||||
There is a self test in ROM, address Cs0A.
|
||||
|
||||
From the RamFactor docs:
|
||||
|
||||
The RamFactor card has five addressable registers, which are addressed
|
||||
|
||||
according to the slot number the card is in:
|
||||
|
||||
$C080+slot * 16low byte of RAM address
|
||||
$C081+slot * 16middle byte of RAM address
|
||||
$C082+slot * 16high byte of RAM address
|
||||
$C083+slot * 16data at addressed location
|
||||
$C08F+slot * 16Firmware Bank Select
|
||||
After power up or Control-Reset, the registers on the card are all in a
|
||||
|
||||
disabled state. They will be enabled by addressing any address in the firmware
|
||||
page $Cs00-CsFF.
|
||||
|
||||
The three address bytes can be both written into and read from. If the card
|
||||
|
||||
has one Megabyte or less, reading the high address byte will always return a
|
||||
value in the range $F0-FF. The top nybble can be any value when you write it,
|
||||
but it will always be “F” when you read it. If the card has more than one
|
||||
Megabyte of RAM, the top nybble will be a meaningful part of the address.
|
||||
|
||||
Notes for RAMFactor:
|
||||
- https://github.com/mamedev/mame/blob/master/src/devices/bus/a2bus/a2memexp.cpp
|
||||
- ss 5 is for the ROM page, there are two.
|
||||
- https://ae.applearchives.com/all_apple_iis/ramfactor/
|
||||
*/
|
||||
const (
|
||||
memoryExpansionMask = 0x000fffff // 10 bits, 1MB
|
||||
)
|
||||
|
||||
// CardMemoryExpansion is a Memory Expansion card
|
||||
type CardMemoryExpansion struct {
|
||||
cardBase
|
||||
ram []uint8
|
||||
index int
|
||||
}
|
||||
|
||||
func newCardMemoryExpansionBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Memory Expansion Card",
|
||||
description: "Memory expansion card",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"size", "RAM of the card, can be 256, 512, 768 or 1024", "1024"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
size, err := paramsGetInt(params, "size")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if size != 256 && size != 512 && size != 768 && size != 1024 {
|
||||
return nil, fmt.Errorf("invalid RAM size %v. It must be 256, 512, 768 or 1024", size)
|
||||
}
|
||||
|
||||
var c CardMemoryExpansion
|
||||
c.ram = make([]uint8, size*1024)
|
||||
err = c.loadRomFromResource("<internal>/MemoryExpansionCard-341-0344a.bin", cardRomFull)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo returns card info
|
||||
func (c *CardMemoryExpansion) GetInfo() map[string]string {
|
||||
info := make(map[string]string)
|
||||
info["size"] = fmt.Sprintf("%vKB", len(c.ram)/1024)
|
||||
return info
|
||||
}
|
||||
|
||||
func (c *CardMemoryExpansion) assign(a *Apple2, slot int) {
|
||||
|
||||
// Read pointer position
|
||||
c.addCardSoftSwitchR(0, func() uint8 {
|
||||
return uint8(c.index)
|
||||
}, "MEMORYEXLOR")
|
||||
c.addCardSoftSwitchR(1, func() uint8 {
|
||||
return uint8(c.index >> 8)
|
||||
}, "MEMORYEXMIR")
|
||||
c.addCardSoftSwitchR(2, func() uint8 {
|
||||
// Top nibble returned is 0xf
|
||||
return uint8(c.index>>16) | 0xf0
|
||||
}, "MEMORYEXHIR")
|
||||
|
||||
// Set pointer position
|
||||
c.addCardSoftSwitchW(0, func(value uint8) {
|
||||
c.index = (c.index &^ 0xff) + int(value)
|
||||
}, "MEMORYEXLOW")
|
||||
c.addCardSoftSwitchW(1, func(value uint8) {
|
||||
c.index = (c.index &^ 0xff00) + int(value)<<8
|
||||
}, "MEMORYEXMIW")
|
||||
c.addCardSoftSwitchW(2, func(value uint8) {
|
||||
// Only lo nibble is used
|
||||
c.index = (c.index &^ 0xff0000) + int(value&0x0f)<<16
|
||||
}, "MEMORYEXHIW")
|
||||
|
||||
// Read data
|
||||
c.addCardSoftSwitchR(3, func() uint8 {
|
||||
var value uint8
|
||||
if c.index < len(c.ram) {
|
||||
value = c.ram[c.index]
|
||||
} else {
|
||||
value = 0xde // Ram socket not populated
|
||||
}
|
||||
c.index = (c.index + 1) & memoryExpansionMask
|
||||
return value
|
||||
}, "MEMORYEXR")
|
||||
|
||||
// Write data
|
||||
c.addCardSoftSwitchW(3, func(value uint8) {
|
||||
if c.index < len(c.ram) {
|
||||
c.ram[c.index] = value
|
||||
}
|
||||
c.index = (c.index + 1) & memoryExpansionMask
|
||||
}, "MEMORYEXW")
|
||||
|
||||
// The rest of the softswitches return 255, at least on //e and //c
|
||||
for i := uint8(4); i < 16; i++ {
|
||||
c.addCardSoftSwitchR(i, func() uint8 {
|
||||
return 255
|
||||
}, "MEMORYEXUNUSEDR")
|
||||
}
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
271
cardMouse.go
Normal file
|
@ -0,0 +1,271 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
Mouse card implementation. Does not emulate a real card, only the behaviour. Idea taken
|
||||
from aiie (https://hackaday.io/project/19925-aiie-an-embedded-apple-e-emulator/log/188017-entry-23-here-mousie-mousie-mousie)
|
||||
|
||||
See:
|
||||
https://www.apple.asimov.net/documentation/hardware/io/AppleMouse%20II%20User%27s%20Manual.pdf
|
||||
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/Digitizers/Apple%20Mouse%20Interface%20Card/Documentation/Apple%20II%20Mouse%20Technical%20Notes.pdf
|
||||
http://www.1000bit.it/support/manuali/apple/technotes/mous/tn.mous.2.html
|
||||
|
||||
The management of IN# and PR# is copied from cardInOut
|
||||
|
||||
Not compatible with A2OSX that needs interrupts on VBL
|
||||
|
||||
*/
|
||||
|
||||
// CardMouse represents a SmartPort card
|
||||
type CardMouse struct {
|
||||
cardBase
|
||||
|
||||
lastX, lastY uint16
|
||||
lastPressed bool
|
||||
|
||||
minX, minY, maxX, maxY uint16
|
||||
mode uint8
|
||||
|
||||
response string
|
||||
iOut int
|
||||
iIn int
|
||||
}
|
||||
|
||||
func newCardMouseBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Mouse Card",
|
||||
description: "Mouse card implementation, does not emulate a real card, only the firmware behaviour",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
return &CardMouse{
|
||||
maxX: 0x3ff,
|
||||
maxY: 0x3ff,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
mouseXLo = uint16(0x478)
|
||||
mouseYLo = uint16(0x4f8)
|
||||
mouseXHi = uint16(0x578)
|
||||
mouseYHi = uint16(0x5f8)
|
||||
mouseStatus = uint16(0x778)
|
||||
mouseMode = uint16(0x7f8)
|
||||
)
|
||||
|
||||
const (
|
||||
mouseModeEnabled = uint8(1)
|
||||
mouseModeIntMoveEnabled = uint8(2)
|
||||
mouseModeIntButtonEnabled = uint8(4)
|
||||
mouseModeIntVBlankEnabled = uint8(8)
|
||||
)
|
||||
|
||||
func (c *CardMouse) set(field uint16, value uint8) {
|
||||
// Update the card screen-holes
|
||||
c.a.mmu.Poke(field+uint16(c.slot), value)
|
||||
}
|
||||
|
||||
func (c *CardMouse) get(field uint16) uint8 {
|
||||
// Read from the card screen-holes
|
||||
return c.a.mmu.Peek(field /*+ uint16(c.slot)*/)
|
||||
}
|
||||
|
||||
func (c *CardMouse) setMode(mode uint8) {
|
||||
c.mode = mode
|
||||
enabled := mode&mouseModeEnabled == 1
|
||||
moveInts := mode&mouseModeIntMoveEnabled == 1
|
||||
buttonInts := mode&mouseModeIntButtonEnabled == 1
|
||||
vBlankInts := mode&mouseModeIntVBlankEnabled == 1
|
||||
|
||||
c.tracef("Mode set to 0x%02x. Enabled %v. Interrups: move=%v, button=%v, vblank=%v.\n",
|
||||
mode, enabled, moveInts, buttonInts, vBlankInts)
|
||||
|
||||
if moveInts || buttonInts || vBlankInts {
|
||||
panic("Mouse interrupts not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardMouse) checkFromFirmware() {
|
||||
pc, _ := c.a.cpu.GetPCAndSP()
|
||||
if (pc >> 8) != 0xc0+uint16(c.slot) {
|
||||
c.tracef("Softswitch access from outside the firmware. It will not work.\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardMouse) readMouse() (uint16, uint16, bool) {
|
||||
x, y, pressed := c.a.io.mouse.ReadMouse()
|
||||
xTrans := uint16(uint64(c.maxX-c.minX) * uint64(x) / 65536)
|
||||
yTrans := uint16(uint64(c.maxY-c.minY) * uint64(y) / 65536)
|
||||
return xTrans, yTrans, pressed
|
||||
}
|
||||
|
||||
func (c *CardMouse) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchR(0, func() uint8 {
|
||||
c.checkFromFirmware()
|
||||
if c.iOut == 0 {
|
||||
// Create a new response
|
||||
x, y, pressed := c.readMouse()
|
||||
|
||||
button := 1
|
||||
if !pressed {
|
||||
button += 2
|
||||
}
|
||||
if !c.lastPressed {
|
||||
button++
|
||||
}
|
||||
|
||||
keyboard := "+"
|
||||
strobed := (c.a.io.softSwitchesData[ioDataKeyboard] & (1 << 7)) == 0
|
||||
if !strobed {
|
||||
keyboard = "-"
|
||||
}
|
||||
|
||||
c.response = fmt.Sprintf("%v,%v,%v%v\r", x, y, keyboard, button)
|
||||
}
|
||||
value := uint8(c.response[c.iOut])
|
||||
c.iOut++
|
||||
if c.iOut == len(c.response) {
|
||||
c.iOut = 0
|
||||
}
|
||||
|
||||
value += 0x80
|
||||
c.tracef("IN#%v -> %02x.\n", slot, value)
|
||||
return value
|
||||
}, "MOUSEOUT")
|
||||
|
||||
c.addCardSoftSwitchW(1, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("PR#%v <- %02x\n", slot, value)
|
||||
if c.iIn == 0 {
|
||||
// We care only about the first byte
|
||||
c.setMode(value & 0x0f)
|
||||
}
|
||||
c.iIn++
|
||||
if value == 13 {
|
||||
c.iIn = 0 // Ready for the next command
|
||||
}
|
||||
}, "MOUSEIN")
|
||||
|
||||
c.addCardSoftSwitchW(2, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("SetMouse(0x%02v)\n", value)
|
||||
c.setMode(value & 0x0f)
|
||||
}, "SETMOUSE")
|
||||
|
||||
c.addCardSoftSwitchW(3, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("ServeMouse() NOT IMPLEMENTED\n")
|
||||
panic("Mouse interrupts not implemented")
|
||||
}, "SERVEMOUSE")
|
||||
|
||||
c.addCardSoftSwitchW(4, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
if c.mode&mouseModeEnabled == 1 {
|
||||
x, y, pressed := c.readMouse()
|
||||
|
||||
status := uint8(0)
|
||||
if pressed {
|
||||
status |= 1 << 7
|
||||
}
|
||||
if c.lastPressed {
|
||||
status |= 1 << 6
|
||||
}
|
||||
if (x != c.lastX) || (y != c.lastY) {
|
||||
status |= 1 << 5
|
||||
}
|
||||
|
||||
c.set(mouseXHi, uint8(x>>8))
|
||||
c.set(mouseYHi, uint8(y>>8))
|
||||
c.set(mouseXLo, uint8(x))
|
||||
c.set(mouseYLo, uint8(y))
|
||||
c.set(mouseStatus, status)
|
||||
c.set(mouseMode, c.mode)
|
||||
if (status&(1<<5) != 0) || (pressed != c.lastPressed) {
|
||||
c.tracef("ReadMouse(): x: %v, y: %v, pressed: %v\n",
|
||||
x, y, pressed)
|
||||
}
|
||||
|
||||
c.lastX = x
|
||||
c.lastY = y
|
||||
c.lastPressed = pressed
|
||||
}
|
||||
}, "READMOUSE")
|
||||
|
||||
c.addCardSoftSwitchW(5, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("ClearMouse() NOT IMPLEMENTED\n")
|
||||
c.set(mouseXHi, 0)
|
||||
c.set(mouseYHi, 0)
|
||||
c.set(mouseXLo, 0)
|
||||
c.set(mouseYLo, 0)
|
||||
}, "CLEARMOUSE")
|
||||
c.addCardSoftSwitchW(6, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("PosMouse() NOT IMPLEMENTED\n")
|
||||
}, "POSMOUSE")
|
||||
|
||||
c.addCardSoftSwitchW(7, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("ClampMouse(%v)\n", value)
|
||||
|
||||
if value == 0 {
|
||||
c.minX = uint16(c.get(mouseXLo)) + uint16(c.get(mouseXHi))<<8
|
||||
c.maxX = uint16(c.get(mouseYLo)) + uint16(c.get(mouseYHi))<<8
|
||||
} else if value == 1 {
|
||||
c.minY = uint16(c.get(mouseXLo)) + uint16(c.get(mouseXHi))<<8
|
||||
c.maxY = uint16(c.get(mouseYLo)) + uint16(c.get(mouseYHi))<<8
|
||||
}
|
||||
|
||||
c.tracef("Current bounds: X[%v-%v], Y[%v-%v],\n", c.minX, c.maxX, c.minY, c.maxY)
|
||||
}, "CLAMPMOUSE")
|
||||
|
||||
c.addCardSoftSwitchW(8, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("HomeMouse() NOT IMPLEMENTED\n")
|
||||
}, "HOMEMOUSE")
|
||||
|
||||
c.addCardSoftSwitchW(9, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
c.tracef("InitMouse()\n")
|
||||
c.minX = 0
|
||||
c.minY = 0
|
||||
c.maxX = 0x3ff
|
||||
c.maxY = 0x3ff
|
||||
c.mode = 0
|
||||
}, "INITMOUSE")
|
||||
|
||||
c.addCardSoftSwitchW(0xc, func(value uint8) {
|
||||
c.checkFromFirmware()
|
||||
// See http://www.1000bit.it/support/manuali/apple/technotes/mous/tn.mous.2.html
|
||||
c.tracef("TimeData(%v) NOT IMPLEMENTED\n", value)
|
||||
}, "TIMEDATEMOUSE")
|
||||
|
||||
data := buildBaseInOutRom(slot)
|
||||
c.romCsxx = newMemoryRangeROM(0xC200, data[:], "Mouse card")
|
||||
|
||||
// Identification as a mouse card
|
||||
// From Technical Note Misc #8, "Pascal 1.1 Firmware Protocol ID Bytes":
|
||||
data[0x05] = 0x38
|
||||
data[0x07] = 0x18
|
||||
data[0x0b] = 0x01
|
||||
data[0x0c] = 0x20
|
||||
// From "AppleMouse // User's Manual", Appendix B:
|
||||
//data[0x0c] = 0x20
|
||||
data[0xfb] = 0xd6
|
||||
|
||||
// Set 8 entrypoints to sofstwitches 2 to 1f
|
||||
for i := uint8(0); i < 14; i++ {
|
||||
base := 0x60 + 0x05*i
|
||||
data[0x12+i] = base
|
||||
data[base+0] = 0x8D // STA $C0x2
|
||||
data[base+1] = 0x82 + i + uint8(slot<<4)
|
||||
data[base+2] = 0xC0
|
||||
data[base+3] = 0x18 // CLC ;no error
|
||||
data[base+4] = 0x60 // RTS
|
||||
}
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
107
cardMultiRom.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package izapple2
|
||||
|
||||
/*
|
||||
MultiROM card for Apple II
|
||||
|
||||
See: https://www.applefritter.com/content/multiple-image-rom-card
|
||||
|
||||
|
||||
-------
|
||||
28C256 (Also for 27256 on Rev 1.1 cards)
|
||||
-------
|
||||
0000-07FF - CPM boot (User 2) Bank 3
|
||||
0800-0FFF - Freeze (User 3) Bank 2
|
||||
1000-37FF - IntBasic including programmers aid BankBasic 0
|
||||
3800-3FFF - Monitor Bank 6
|
||||
4000-47FF - Lockbuster (User 1) Bank 4
|
||||
4800-4FFF - Dead boot diagnostic Bank 5
|
||||
5000-77FF - Applesoft BankBasic 1
|
||||
7800-7FFF - Autostart Bank 7
|
||||
|
||||
|
||||
*/
|
||||
|
||||
// MultiRomCard represents a Multiple Image ROM Card
|
||||
type MultiRomCard struct {
|
||||
cardBase
|
||||
rom []uint8
|
||||
basicBank int
|
||||
f8Bank int
|
||||
}
|
||||
|
||||
func newMultiRomCardBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "MultiROM",
|
||||
description: "Multiple Image ROM card",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"rom", "ROM file to load", "<internal>/MultiRom(SP boot)-Prog aid-28C256.BIN"},
|
||||
{"basic", "Bank for D000 to F7FF", "1"},
|
||||
{"bank", "Bank for F8", "7"}},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c MultiRomCard
|
||||
var err error
|
||||
c.basicBank, err = paramsGetInt(params, "basic")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.f8Bank, err = paramsGetInt(params, "bank")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
romFile := paramsGetPath(params, "rom")
|
||||
data, _, err := LoadResource(romFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.rom = data
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MultiRomCard) assign(a *Apple2, slot int) {
|
||||
c.cardBase.assign(a, slot)
|
||||
a.mmu.inhibitROM(c)
|
||||
}
|
||||
|
||||
func (c *MultiRomCard) translateAddress(address uint16) uint16 {
|
||||
var baseAddress uint16
|
||||
// Basic part
|
||||
if address < 0xf800 {
|
||||
switch c.basicBank {
|
||||
case 0:
|
||||
baseAddress = 0x1000
|
||||
default:
|
||||
baseAddress = 0x5000
|
||||
}
|
||||
return address - 0xd000 + baseAddress
|
||||
}
|
||||
|
||||
// F8 part
|
||||
|
||||
switch c.f8Bank {
|
||||
case 2:
|
||||
baseAddress = 0x0800
|
||||
case 3:
|
||||
baseAddress = 0x0000
|
||||
case 4:
|
||||
baseAddress = 0x4000
|
||||
case 5:
|
||||
baseAddress = 0x4800
|
||||
case 6:
|
||||
baseAddress = 0x3800
|
||||
default:
|
||||
baseAddress = 0x7800
|
||||
}
|
||||
return address - 0xf800 + baseAddress
|
||||
}
|
||||
|
||||
func (c *MultiRomCard) peek(address uint16) uint8 {
|
||||
return c.rom[c.translateAddress(address)]
|
||||
}
|
||||
|
||||
func (c *MultiRomCard) poke(address uint16, value uint8) {
|
||||
// Nothing
|
||||
}
|
66
cardParallelPrinter.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
/*
|
||||
Apple II Parallel Printer Interface card.
|
||||
|
||||
See:
|
||||
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/Parallel/Apple%20II%20Parallel%20Printer%20Interface%20Card/
|
||||
|
||||
*/
|
||||
|
||||
// CardParallelPrinter represents a Parallel Printer Interface card
|
||||
type CardParallelPrinter struct {
|
||||
cardBase
|
||||
file *os.File
|
||||
ascii bool
|
||||
}
|
||||
|
||||
func newCardParallelPrinterBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Parallel Printer Interface",
|
||||
description: "Card to dump to a file what would be printed to a parallel printer",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"file", "File to store the printed code", "printer.out"},
|
||||
{"ascii", "Remove the 7 bit. Useful for normal text printing, but breaks graphics printing ", "false"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardParallelPrinter
|
||||
c.ascii = paramsGetBool(params, "ascii")
|
||||
filepath := paramsGetPath(params, "file")
|
||||
f, err := os.OpenFile(filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.file = f
|
||||
err = c.loadRomFromResource("<internal>/Apple II Parallel Printer Interface Card ROM fixed.bin", cardRomSimple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardParallelPrinter) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchW(0, func(value uint8) {
|
||||
c.printByte(value)
|
||||
}, "PARALLELDEVW")
|
||||
|
||||
c.addCardSoftSwitchR(4, func() uint8 {
|
||||
return 0xff // TODO: What are the bit values?
|
||||
}, "PARALLELSTATUSR")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *CardParallelPrinter) printByte(value uint8) {
|
||||
if c.ascii {
|
||||
// As text the MSB has to be removed, but if done, graphics modes won't work
|
||||
value = value & 0x7f // Remove the MSB bit
|
||||
}
|
||||
c.file.Write([]byte{value})
|
||||
}
|
133
cardProDOSRomCard3.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package izapple2
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
Ralle Palaveev's ProDOS-Romcard3
|
||||
|
||||
See:
|
||||
https://github.com/rallepaqlaveev/ProDOS-Romcard3
|
||||
|
||||
Note that this card disables the C800-CFFF range only on writes to CFFF, not as most other cards that disable on reads and writes.
|
||||
|
||||
*/
|
||||
|
||||
// CardProDOSRomCard3 is a Memory Expansion card
|
||||
type CardProDOSRomCard3 struct {
|
||||
cardBase
|
||||
bank uint16
|
||||
data []uint8
|
||||
nvram bool
|
||||
secondROMPage bool
|
||||
}
|
||||
|
||||
func newCardProDOSRomCard3Builder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "ProDOS ROM Card 3",
|
||||
description: "A bootable 4 MB ROM card by Ralle Palaveev",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"image", "ROM image with the ProDOS volume", "https://github.com/rallepalaveev/ProDOS-Romcard3/raw/main/ProDOS-ROMCARD3_4MB_A2D.v1.4_v37.po"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
image := paramsGetPath(params, "image")
|
||||
if image == "" {
|
||||
return nil, fmt.Errorf("image required for the ProDOS ROM drive")
|
||||
}
|
||||
|
||||
data, _, err := LoadResource(image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) != 4*1024*1024 {
|
||||
return nil, fmt.Errorf("NVRAM image must be 4MB")
|
||||
}
|
||||
|
||||
var c CardProDOSRomCard3
|
||||
c.data = data
|
||||
c.loadRom(data[0x200:0x300], cardRomSimple)
|
||||
c.romC8xx = &c
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newCardProDOSNVRAMDriveBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "ProDOS 4MB NVRAM DRive",
|
||||
description: "A bootable 4 MB NVRAM card by Ralle Palaveev",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"image", "ROM image with the ProDOS volume", ""},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
image := paramsGetPath(params, "image")
|
||||
if image == "" {
|
||||
return nil, fmt.Errorf("image required for the ProDOS ROM drive")
|
||||
}
|
||||
|
||||
data, _, err := LoadResource(image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) != 4*1024*1024 && len(data) != 512*1024 {
|
||||
return nil, fmt.Errorf("NVRAM image must be 512KB or 4MB")
|
||||
}
|
||||
|
||||
var c CardProDOSRomCard3
|
||||
c.data = data
|
||||
c.loadRom(data[0x200:0x400], cardRomSimple)
|
||||
c.romC8xx = &c
|
||||
c.nvram = true
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardProDOSRomCard3) assign(a *Apple2, slot int) {
|
||||
|
||||
// Set pointer position
|
||||
c.addCardSoftSwitchW(0, func(value uint8) {
|
||||
c.bank = uint16(value) | c.bank&0xff00
|
||||
}, "BANKLO")
|
||||
c.addCardSoftSwitchW(1, func(value uint8) {
|
||||
c.bank = uint16(value)<<8 | c.bank&0xff
|
||||
}, "BANKHI")
|
||||
|
||||
if c.nvram {
|
||||
c.addCardSoftSwitchW(2, func(value uint8) {
|
||||
if c.secondROMPage {
|
||||
c.romCsxx.setPage(0)
|
||||
} else {
|
||||
c.romCsxx.setPage(1)
|
||||
}
|
||||
}, "?????")
|
||||
}
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *CardProDOSRomCard3) translateAddress(address uint16) int {
|
||||
// An address from 0xC800 to 0xCFFF is mapped to the corresponding bank of the ROM
|
||||
// There are 0x800 (2048) banks with 0x0800 (2048) bytes each
|
||||
offset := address - 0xC800
|
||||
pageAddress := int(c.bank&0x7FF) * 0x0800
|
||||
|
||||
//fmt.Printf("CardProDOSRomCard3.translateAddress: address=%04X, bank=%04X, offset=%04X, pageAddress=%08X\n", address, c.bank, offset, pageAddress)
|
||||
|
||||
return pageAddress + int(offset)
|
||||
}
|
||||
|
||||
func (c *CardProDOSRomCard3) peek(address uint16) uint8 {
|
||||
if address&0xff == 0 {
|
||||
fmt.Printf("CardProDOSRomCard3.peek: address=%04X\n", address)
|
||||
}
|
||||
return c.data[c.translateAddress(address)]
|
||||
}
|
||||
|
||||
func (c *CardProDOSRomCard3) poke(address uint16, value uint8) {
|
||||
fmt.Printf("CardProDOSRomCard3.poke: address=%04X, value=%02X\n", address, value)
|
||||
if c.nvram && address != 0xcfff {
|
||||
c.data[c.translateAddress(address)] = value
|
||||
}
|
||||
}
|
73
cardProDOSRomDrive.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package izapple2
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
Terence Boldt's ProDOS-ROM-Drive: A bootable 1 MB solid state disk for Apple ][ computers
|
||||
|
||||
Emulates version 4.0+
|
||||
|
||||
See:
|
||||
https://github.com/tjboldt/ProDOS-ROM-Drive
|
||||
https://github.com/Alex-Kw/ProDOS-ROM-Drive-Images
|
||||
|
||||
*/
|
||||
|
||||
// CardMemoryExpansion is a Memory Expansion card
|
||||
type CardProDOSRomDrive struct {
|
||||
cardBase
|
||||
address uint16
|
||||
data []uint8
|
||||
}
|
||||
|
||||
const proDOSRomDriveMask = 0xf_ffff // 1 MB mask
|
||||
|
||||
func newCardProDOSRomDriveBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "ProDOS ROM Drive",
|
||||
description: "A bootable 1 MB solid state disk by Terence Boldt",
|
||||
defaultParams: &[]paramSpec{
|
||||
//{"image", "ROM image with the ProDOS volume", "https://github.com/tjboldt/ProDOS-ROM-Drive/raw/v4.0/Firmware/GamesWithFirmware.po"},
|
||||
{"image", "ROM image with the ProDOS volume", "https://github.com/Alex-Kw/ProDOS-ROM-Drive-Images/raw/main/ProDOS_2.4.3_TJ.po"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
image := paramsGetPath(params, "image")
|
||||
if image == "" {
|
||||
return nil, fmt.Errorf("image required for the ProDOS ROM drive")
|
||||
}
|
||||
|
||||
data, _, err := LoadResource(image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c CardProDOSRomDrive
|
||||
c.data = data
|
||||
c.loadRom(data[0x300:0x400], cardRomSimple)
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardProDOSRomDrive) assign(a *Apple2, slot int) {
|
||||
|
||||
// Set pointer position
|
||||
c.addCardSoftSwitchW(0, func(value uint8) {
|
||||
c.address = uint16(value) | c.address&0xff00
|
||||
}, "LATCHLO")
|
||||
c.addCardSoftSwitchW(1, func(value uint8) {
|
||||
c.address = uint16(value)<<8 | c.address&0xff
|
||||
}, "LATCHHI")
|
||||
|
||||
// Read data
|
||||
for i := uint8(0x0); i <= 0xf; i++ {
|
||||
iCopy := i
|
||||
c.addCardSoftSwitchR(iCopy, func() uint8 {
|
||||
offset := uint32(c.address)<<4 + uint32(iCopy)
|
||||
offset &= proDOSRomDriveMask
|
||||
return c.data[offset]
|
||||
}, fmt.Sprintf("READ%X", iCopy))
|
||||
}
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
79
cardRGB.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package izapple2
|
||||
|
||||
/*
|
||||
Extended 80-Column Text AppleColor Card or Video7 RGB-SL7 card
|
||||
See:
|
||||
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/Apple%20IIe/Apple%20IIe%20Extended%2080%20Column%20RGB%20Card/Manuals/Apple%20Ext80ColumnAppleColorCardHR%20Manual.pdf
|
||||
https://apple2online.com/web_documents/Video-7%20Manual%20KB.pdf
|
||||
https://mirrors.apple2.org.za/ftp.apple.asimov.net/documentation/hardware/video/DIGICARD%2064K%20Extended%2080%20Column%20RGB%20Card%20for%20Apple%20IIe%20Instruction%20Manual.pdf
|
||||
|
||||
Diagnostics disk:
|
||||
https://mirrors.apple2.org.za/ftp.apple.asimov.net/images/hardware/video/Video-7%20Apple%20II%20RGB%20Demo%20%28Video-7%2C%20Inc.%29%281984%29.dsk
|
||||
|
||||
It goes to the 80 column slot.
|
||||
|
||||
To set the state it AN3 in graphics mode has to go off-on-off-on. Each pair off-on record the state of 80col:
|
||||
on step 0, an ANN3OFF moves to step 1
|
||||
on step 1, an ANN3ON moves to step 2, and the value of 80COL is copied to RGB flag 1
|
||||
on step 2, an ANN3OFF moves to step 3
|
||||
on step 3, an ANN3ON moves to step 4, and the value of 80COL is copied to RGB flag 2
|
||||
|
||||
Modes by RGB flags 1 and 2:
|
||||
0-0: 560*192 mono
|
||||
1-1: 140*192 ntsc
|
||||
0-1: Mixed mode
|
||||
1-0: 160*192 ntsc
|
||||
|
||||
*/
|
||||
|
||||
type cardRGB struct {
|
||||
// cardBase, not a regular card
|
||||
step uint8
|
||||
}
|
||||
|
||||
func setupRGBCard(a *Apple2) *cardRGB {
|
||||
var c cardRGB
|
||||
c.step = 0
|
||||
|
||||
a.io.softSwitchesData[ioFlagRGBCardActive] = ssOn
|
||||
|
||||
// Does not have ROM or private softswitches. It spies on the softswitches
|
||||
a.io.addSoftSwitchRW(0x50, func() uint8 {
|
||||
a.io.softSwitchesData[ioFlagText] = ssOff
|
||||
// Reset RGB modes when entering graphics mode
|
||||
c.step = 0
|
||||
a.io.softSwitchesData[ioFlag1RGBCard] = ssOn
|
||||
a.io.softSwitchesData[ioFlag2RGBCard] = ssOn
|
||||
return 0
|
||||
}, "TEXTOFF-RGB")
|
||||
|
||||
a.io.addSoftSwitchRW(0x5e, func() uint8 {
|
||||
a.io.softSwitchesData[ioFlagAnnunciator3] = ssOff
|
||||
switch c.step {
|
||||
case 0:
|
||||
c.step++
|
||||
case 2:
|
||||
c.step++
|
||||
case 4:
|
||||
c.step = 0
|
||||
}
|
||||
|
||||
return 0
|
||||
}, "ANN3OFF-RGB")
|
||||
|
||||
a.io.addSoftSwitchRW(0x5f, func() uint8 {
|
||||
a.io.softSwitchesData[ioFlagAnnunciator3] = ssOn
|
||||
switch c.step {
|
||||
case 1:
|
||||
a.io.softSwitchesData[ioFlag1RGBCard] = a.io.softSwitchesData[ioFlag80Col]
|
||||
c.step++
|
||||
case 3:
|
||||
a.io.softSwitchesData[ioFlag2RGBCard] = a.io.softSwitchesData[ioFlag80Col]
|
||||
c.step++
|
||||
}
|
||||
|
||||
return 0
|
||||
}, "ANN3ON-RGB")
|
||||
|
||||
return &c
|
||||
}
|
49
cardRamWorks.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
RAMWorks style card on the Apple IIe aus slot.
|
||||
https://patents.google.com/patent/US4601018
|
||||
https://ae.applearchives.com/apple_e/ramworks_iii/ramworks_iii_basic_manual_1.pdf
|
||||
|
||||
Diagnostics disks:
|
||||
https://ae.applearchives.com/apple_e/ramworks_iii/ramworks_diagnostics.zip
|
||||
|
||||
It's is like the extra 64kb on an Apple IIe 80col 64kb card, but with up to 256 banks
|
||||
*/
|
||||
|
||||
func setupRAMWorksCard(a *Apple2, sizeArg string) error {
|
||||
size, err := strconv.Atoi(sizeArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid RamWorks card RAM size: %s", sizeArg)
|
||||
}
|
||||
if size%64 != 0 {
|
||||
return fmt.Errorf("the Ramworks size must be a multiple of 64, %v is not", size)
|
||||
}
|
||||
|
||||
a.mmu.initExtendedRAM(size / 64)
|
||||
|
||||
ssr := func() uint8 {
|
||||
return a.mmu.extendedRAMBlock
|
||||
}
|
||||
|
||||
ssw := func(value uint8) {
|
||||
a.mmu.setExtendedRAMActiveBlock(value)
|
||||
}
|
||||
|
||||
// Does not have a slot assigned
|
||||
a.io.addSoftSwitchR(0x71, ssr, "RAMWORKSR")
|
||||
a.io.addSoftSwitchR(0x73, ssr, "RAMWORKSR")
|
||||
a.io.addSoftSwitchR(0x75, ssr, "RAMWORKSR")
|
||||
a.io.addSoftSwitchR(0x77, ssr, "RAMWORKSR")
|
||||
a.io.addSoftSwitchW(0x71, ssw, "RAMWORKSW")
|
||||
a.io.addSoftSwitchW(0x73, ssw, "RAMWORKSW")
|
||||
a.io.addSoftSwitchW(0x75, ssw, "RAMWORKSW")
|
||||
a.io.addSoftSwitchW(0x77, ssw, "RAMWORKSW")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,19 +1,14 @@
|
|||
package apple2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
package izapple2
|
||||
|
||||
/*
|
||||
|
||||
RAM card with 128Kb. It's like 8 language cards.
|
||||
|
||||
http://www.applelogic.org/files/SATURN128MAN.pdf
|
||||
|
||||
See:
|
||||
http://www.applelogic.org/files/SATURN128MAN.pdf
|
||||
*/
|
||||
|
||||
type cardSaturn struct {
|
||||
// CardSaturn is a Saturn128 card
|
||||
type CardSaturn struct {
|
||||
cardBase
|
||||
readState bool
|
||||
writeState uint8
|
||||
|
@ -21,32 +16,43 @@ type cardSaturn struct {
|
|||
activeBlock uint8
|
||||
}
|
||||
|
||||
func newCardSaturnBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Saturn 128KB Ram Card",
|
||||
description: "RAM card with 128Kb, it's like 8 language cards",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
return &CardSaturn{}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
saturnBlocks = 8
|
||||
)
|
||||
|
||||
func (c *cardSaturn) assign(a *Apple2, slot int) {
|
||||
func (c *CardSaturn) assign(a *Apple2, slot int) {
|
||||
c.readState = false
|
||||
c.writeState = lcWriteEnabled
|
||||
c.altBank = true
|
||||
c.activeBlock = 0
|
||||
a.mmu.initLanguageRAM(saturnBlocks)
|
||||
|
||||
// TODO: use addCardSoftSwitches()
|
||||
for i := uint8(0x0); i <= 0xf; i++ {
|
||||
iCopy := i
|
||||
c.addCardSoftSwitchR(iCopy, func(*ioC0Page) uint8 {
|
||||
c.ssAction(iCopy, false)
|
||||
c.addCardSoftSwitchR(iCopy, func() uint8 {
|
||||
c.ssAction(iCopy)
|
||||
return 0
|
||||
}, "SATURNR")
|
||||
c.addCardSoftSwitchW(iCopy, func(*ioC0Page, uint8) {
|
||||
c.ssAction(iCopy, true)
|
||||
c.addCardSoftSwitchW(iCopy, func(uint8) {
|
||||
c.ssAction(iCopy)
|
||||
}, "SATURNW")
|
||||
}
|
||||
c.cardBase.assign(a, slot)
|
||||
c.applyState()
|
||||
}
|
||||
|
||||
func (c *cardSaturn) ssAction(ss uint8, write bool) {
|
||||
func (c *CardSaturn) ssAction(ss uint8) {
|
||||
switch ss {
|
||||
case 0:
|
||||
// RAM read, no writes
|
||||
|
@ -110,60 +116,10 @@ func (c *cardSaturn) ssAction(ss uint8, write bool) {
|
|||
c.writeState = lcWriteEnabled
|
||||
}
|
||||
|
||||
// Writing to the softswtich disables writes.
|
||||
if write {
|
||||
c.writeState = lcWriteDisabled
|
||||
}
|
||||
|
||||
c.applyState()
|
||||
}
|
||||
|
||||
func (c *cardSaturn) applyState() {
|
||||
c.a.mmu.setLanguageRAMBlock(c.activeBlock)
|
||||
func (c *CardSaturn) applyState() {
|
||||
c.a.mmu.setLanguageRAMActiveBlock(c.activeBlock)
|
||||
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
|
||||
}
|
||||
|
||||
func (c *cardSaturn) save(w io.Writer) error {
|
||||
for i := 0; i < saturnBlocks; i++ {
|
||||
err := binary.Write(w, binary.BigEndian, c.readState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.writeState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.altBank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(w, binary.BigEndian, c.activeBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.cardBase.save(w)
|
||||
}
|
||||
|
||||
func (c *cardSaturn) load(r io.Reader) error {
|
||||
for i := 0; i < saturnBlocks; i++ {
|
||||
err := binary.Read(r, binary.BigEndian, &c.readState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.writeState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.altBank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &c.activeBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.applyState()
|
||||
}
|
||||
return c.cardBase.load(r)
|
||||
}
|
||||
|
|
301
cardSmartport.go
Normal file
|
@ -0,0 +1,301 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
To implement a hard drive we just have to support boot from #PR7 and the PRODOS expectations.
|
||||
|
||||
See:
|
||||
Beneath Prodos, section 6-6, 7-13 and 5-8. (http://www.apple-iigs.info/doc/fichiers/beneathprodos.pdf)
|
||||
Apple IIc Technical Reference, 2nd Edition. Chapter 8. https://ia800207.us.archive.org/19/items/AppleIIcTechnicalReference2ndEd/Apple%20IIc%20Technical%20Reference%202nd%20ed.pdf
|
||||
https://prodos8.com/docs/technote/21/
|
||||
https://prodos8.com/docs/technote/20/
|
||||
|
||||
|
||||
*/
|
||||
|
||||
// CardSmartPort represents a SmartPort card
|
||||
type CardSmartPort struct {
|
||||
cardBase
|
||||
devices []smartPortDevice
|
||||
hardDiskBlocks uint32
|
||||
|
||||
mliParams uint16
|
||||
trace bool
|
||||
}
|
||||
|
||||
func newCardSmartPortStorageBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "SmartPort",
|
||||
description: "SmartPort interface card",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"image1", "Disk image for unit 1", ""},
|
||||
{"image2", "Disk image for unit 2", ""},
|
||||
{"image3", "Disk image for unit 3", ""},
|
||||
{"image4", "Disk image for unit 4", ""},
|
||||
{"image5", "Disk image for unit 5", ""},
|
||||
{"image6", "Disk image for unit 6", ""},
|
||||
{"image7", "Disk image for unit 7", ""},
|
||||
{"image8", "Disk image for unit 8", ""},
|
||||
{"tracesp", "Trace SmartPort calls", "false"},
|
||||
{"tracehd", "Trace image accesses", "false"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardSmartPort
|
||||
c.trace = paramsGetBool(params, "tracesp")
|
||||
traceHD := paramsGetBool(params, "tracehd")
|
||||
for i := 1; i <= 8; i++ {
|
||||
image := paramsGetPath(params, "image"+strconv.Itoa(i))
|
||||
if image != "" {
|
||||
err := c.LoadImage(image, traceHD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newCardSmartPortFujinetBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Fujinet",
|
||||
description: "SmartPort interface card hosting the Fujinet",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"tracesp", "Trace SmartPort calls", "false"},
|
||||
{"tracenet", "Trace on the network device", "false"},
|
||||
{"traceclock", "Trace on the clock device", "false"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardSmartPort
|
||||
c.trace = paramsGetBool(params, "tracesp")
|
||||
|
||||
net := NewSmartPortFujinetNetwork(&c)
|
||||
net.trace = paramsGetBool(params, "tracenet")
|
||||
c.AddDevice(net)
|
||||
|
||||
clock := NewSmartPortFujinetClock(&c)
|
||||
clock.trace = paramsGetBool(params, "traceclock")
|
||||
c.AddDevice(clock)
|
||||
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo returns smartPort info
|
||||
func (c *CardSmartPort) GetInfo() map[string]string {
|
||||
info := make(map[string]string)
|
||||
info["trace"] = strconv.FormatBool(c.trace)
|
||||
return info
|
||||
}
|
||||
|
||||
// LoadImage loads a disk image
|
||||
func (c *CardSmartPort) LoadImage(filename string, trace bool) error {
|
||||
device, err := NewSmartPortHardDisk(c, filename)
|
||||
if err == nil {
|
||||
device.trace = trace
|
||||
c.devices = append(c.devices, device)
|
||||
c.hardDiskBlocks = device.disk.GetSizeInBlocks() // Needed for the PRODOS status
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadImage loads a disk image
|
||||
func (c *CardSmartPort) AddDevice(device smartPortDevice) {
|
||||
c.devices = append(c.devices, device)
|
||||
c.hardDiskBlocks = 0 // Needed for the PRODOS status
|
||||
}
|
||||
|
||||
func (c *CardSmartPort) assign(a *Apple2, slot int) {
|
||||
c.loadRom(buildHardDiskRom(slot), cardRomSimple)
|
||||
|
||||
c.addCardSoftSwitchR(0, func() uint8 {
|
||||
// Prodos entry point
|
||||
command := a.mmu.Peek(0x42)
|
||||
unit := a.mmu.Peek(0x43) & 0x0f
|
||||
|
||||
// Generate Smarport compatible params
|
||||
var call *smartPortCall
|
||||
if command == smartPortCommandStatus {
|
||||
call = newSmartPortCallSynthetic(c, command, []uint8{
|
||||
3, // 3 args
|
||||
unit,
|
||||
a.mmu.Peek(0x44), a.mmu.Peek(0x45), // data address
|
||||
0,
|
||||
})
|
||||
} else if command == smartPortCommandReadBlock || command == smartPortCommandWriteBlock {
|
||||
call = newSmartPortCallSynthetic(c, command, []uint8{
|
||||
3, // 3args
|
||||
unit,
|
||||
a.mmu.Peek(0x44), a.mmu.Peek(0x45), // data address
|
||||
a.mmu.Peek(0x46), a.mmu.Peek(0x47), 0, // block number
|
||||
})
|
||||
} else {
|
||||
return smartPortBadCommand
|
||||
}
|
||||
|
||||
return c.exec(call)
|
||||
}, "SMARTPORTPRODOSCOMMAND")
|
||||
|
||||
c.addCardSoftSwitchR(1, func() uint8 {
|
||||
// Blocks available, low byte
|
||||
return uint8(c.hardDiskBlocks)
|
||||
}, "HDBLOCKSLO")
|
||||
c.addCardSoftSwitchR(2, func() uint8 {
|
||||
// Blocks available, high byte
|
||||
return uint8(c.hardDiskBlocks >> 8)
|
||||
}, "HDBLOCKHI")
|
||||
|
||||
c.addCardSoftSwitchR(3, func() uint8 {
|
||||
// Smart port entry point
|
||||
command := c.a.mmu.Peek(c.mliParams + 1)
|
||||
paramsAddress := uint16(c.a.mmu.Peek(c.mliParams+2)) + uint16(c.a.mmu.Peek(c.mliParams+3))<<8
|
||||
|
||||
call := newSmartPortCall(c, command, paramsAddress)
|
||||
return c.exec(call)
|
||||
}, "SMARTPORTEXEC")
|
||||
|
||||
c.addCardSoftSwitchW(4, func(value uint8) {
|
||||
c.mliParams = (c.mliParams & 0xff00) + uint16(value)
|
||||
}, "HDSMARTPORTLO")
|
||||
c.addCardSoftSwitchW(5, func(value uint8) {
|
||||
c.mliParams = (c.mliParams & 0x00ff) + (uint16(value) << 8)
|
||||
}, "HDSMARTPORTHI")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
||||
func (c *CardSmartPort) exec(call *smartPortCall) uint8 {
|
||||
var result uint8
|
||||
unit := int(call.unit())
|
||||
|
||||
if call.command == smartPortCommandStatus &&
|
||||
// Call to the host
|
||||
call.statusCode() == smartPortStatusCodeDevice {
|
||||
|
||||
result = c.hostStatus(call)
|
||||
} else if unit > len(c.devices) {
|
||||
result = smartPortErrorNoDevice
|
||||
} else {
|
||||
if unit == 0 {
|
||||
unit = 1 // For unit 0(host) use the first device
|
||||
}
|
||||
|
||||
if unit > len(c.devices) {
|
||||
result = smartPortErrorNoDevice
|
||||
} else {
|
||||
result = c.devices[unit-1].exec(call)
|
||||
}
|
||||
}
|
||||
|
||||
if c.trace {
|
||||
fmt.Printf("[CardSmartPort] Command %v on slot %v => result %s.\n",
|
||||
call, c.slot, smartPortErrorMessage(result))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *CardSmartPort) hostStatus(call *smartPortCall) uint8 {
|
||||
dest := call.param16(2)
|
||||
if c.trace {
|
||||
fmt.Printf("[CardSmartPort] Host status into $%x.\n", dest)
|
||||
}
|
||||
|
||||
// See http://www.1000bit.it/support/manuali/apple/technotes/smpt/tn.smpt.2.html
|
||||
c.a.mmu.Poke(dest+0, uint8(len(c.devices)))
|
||||
c.a.mmu.Poke(dest+1, 0xff) // No interrupt
|
||||
c.a.mmu.Poke(dest+2, 0x00)
|
||||
c.a.mmu.Poke(dest+3, 0x00) // Unknown manufacturer
|
||||
c.a.mmu.Poke(dest+4, 0x01)
|
||||
c.a.mmu.Poke(dest+5, 0x00) // Version 1.0 final
|
||||
c.a.mmu.Poke(dest+6, 0x00)
|
||||
c.a.mmu.Poke(dest+7, 0x00) // Reserved
|
||||
|
||||
return smartPortNoError
|
||||
}
|
||||
|
||||
func buildHardDiskRom(slot int) []uint8 {
|
||||
data := make([]uint8, 256)
|
||||
ssBase := 0x80 + uint8(slot<<4)
|
||||
|
||||
copy(data, []uint8{
|
||||
// Preamble bytes to comply with the expectation in $Cn01, 3, 5 and 7
|
||||
0xa9, 0x20, // LDA #$20
|
||||
0xa9, 0x00, // LDA #$00
|
||||
0xa9, 0x03, // LDA #$03
|
||||
0xa9, 0x00, // LDA #$00
|
||||
0xd0, 0x36, // BNE bootcode, there is no space for a jmp
|
||||
})
|
||||
|
||||
if slot == 7 {
|
||||
// It should be 0 for SmartPort, but with 0 it's not bootable with the II+ ROM
|
||||
// See http://www.1000bit.it/support/manuali/apple/technotes/udsk/tn.udsk.2.html
|
||||
data[0x07] = 0x3c
|
||||
}
|
||||
|
||||
copy(data[0x0a:], []uint8{
|
||||
// Entrypoints and SmartPort body it has to be in $Cx0a
|
||||
0x4c, 0x80, 0xc0 + uint8(slot), // JMP $cs80 ; Prodos Entrypoint
|
||||
|
||||
// 3 bytes later, smartPort entrypoint. Uses the ProDos MLI calling convention
|
||||
0x68, // PLA
|
||||
0x8d, ssBase + 4, 0xc0, // STA $c0n4 ; Softswitch 4, store LO(cmdBlock)
|
||||
0xa8, // TAY ; We will need it later
|
||||
0x68, // PLA
|
||||
0x8d, ssBase + 5, 0xc0, // STA $c0n5 ; Softswitch 5, store HI(cmdBlock)
|
||||
0x48, // PHA
|
||||
0x98, // TYA
|
||||
0x18, // CLC
|
||||
0x69, 0x03, // ADC #$03 ; Fix return address past the cmdblock
|
||||
0x48, // PHA
|
||||
0xad, ssBase + 3, 0xc0, // LDA $C0n3 ; Softswitch 3, execute command. Error code in reg A.
|
||||
0x18, // CLC ; Clear carry for no errors.
|
||||
0xF0, 0x01, // BEQ $01 ; Skips the SEC if reg A is zero
|
||||
0x38, // SEC ; Set carry on errors
|
||||
0x60, // RTS
|
||||
})
|
||||
|
||||
copy(data[0x40:], []uint8{
|
||||
// Boot code: SS will load block 0 in address $0800. The jump there.
|
||||
// Note: after execution the first block expects $42 to $47 to have
|
||||
// valid values to read block 0. At least Total Replay expects that.
|
||||
0xa9, 0x01, // LDA·#$01
|
||||
0x85, 0x42, // STA $42 ; Command READ(1)
|
||||
0xa9, 0x00, // LDA·#$00
|
||||
0x85, 0x43, // STA $43 ; Unit 0
|
||||
0x85, 0x44, // STA $44 ; Dest LO($0800)
|
||||
0x85, 0x46, // STA $46 ; Block LO(0)
|
||||
0x85, 0x47, // STA $47 ; Block HI(0)
|
||||
0xa9, 0x08, // LDA·#$08
|
||||
0x85, 0x45, // STA $45 ; Dest HI($0800)
|
||||
|
||||
0xad, ssBase, 0xc0, // LDA $C0n1 ;Call to softswitch 0.
|
||||
0xa2, uint8(slot << 4), // LDX $s7 ; Slot on hign nibble of X
|
||||
0x4c, 0x01, 0x08, // JMP $801 ; Jump to loaded boot sector
|
||||
})
|
||||
|
||||
// Prodos entrypoint body
|
||||
copy(data[0x80:], []uint8{
|
||||
0xad, ssBase + 0, 0xc0, // LDA $C0n0 ; Softswitch 0, execute command. Error code in reg A.
|
||||
0x48, // PHA
|
||||
0xae, ssBase + 1, 0xc0, // LDX $C0n1 ; Softswitch 1, LO(Blocks), STATUS needs that in reg X.
|
||||
0xac, ssBase + 2, 0xc0, // LDY $C0n2 ; Softswitch 2, HI(Blocks). STATUS needs that in reg Y.
|
||||
0x18, // CLC ; Clear carry for no errors.
|
||||
0x68, // PLA ; Sets Z if no error
|
||||
0xF0, 0x01, // BEQ $01 ; Skips the SEC if reg A is zero
|
||||
0x38, // SEC ; Set carry on errors
|
||||
0x60, // RTS
|
||||
})
|
||||
|
||||
data[0xfc] = 0
|
||||
data[0xfd] = 0
|
||||
data[0xfe] = 3 // Status and Read. No write, no format. Single volume
|
||||
data[0xff] = 0x0a // Driver entry point // Must be $0a
|
||||
|
||||
return data
|
||||
}
|
137
cardSwyft.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package izapple2
|
||||
|
||||
/*
|
||||
Swyft card for Apple IIe
|
||||
|
||||
See:
|
||||
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/Other/IAI%20SwyftCard/
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
"SwyftCard Hardware Theory of Operation". SwyftCard manual, page 98:
|
||||
|
||||
The SwyftCard is a plug-in card for the Apple /Ie that operates in
|
||||
slot 3. The card contains three integrated circuits which provide a
|
||||
power-on reset circuit, storage for the SwyftCard program, and control
|
||||
signals for the card. The card operates by asserting the Apple IIe bus
|
||||
signal INH' which disables the built-in ROM and enables the SwyftCard
|
||||
ROM. This permits the SwyftCard program to take over the system at
|
||||
power-on and run the SwyftCard program. (Please refer to the
|
||||
schematic.)
|
||||
|
||||
The LM311 voltage comparator is connected to provide the power-on
|
||||
reset function. When the Apple lIe is first turned on, the power-on
|
||||
reset circuit resets the PAL, turning on the SwyftCard and disabling
|
||||
the Apple IIe internal ROM. The power-on reset circuit must be
|
||||
provided because the existing Apple IIe reset function is used by
|
||||
many Apple lie programs for a "warm start": if Apple lie reset always
|
||||
started the SwyftCard, other programs could not use the "warm start."
|
||||
|
||||
The 27128 PROM is used to store the SwyftCard program. The PROM
|
||||
contains 16384 bytes which are mapped into the address space
|
||||
$DOOO - $FFFF. Since the address space is only 12 Kbytes, there are
|
||||
two 4 Kbyte sections of the PROM mapped into the address space
|
||||
$DOOO-$DFFF.
|
||||
|
||||
The card is controlled by the PAL. When the SwyftCard is active, the
|
||||
PAL asserts the INH' signal, enables the PROM, and bank switches
|
||||
the $DOOO-$DFFF address space. The card is controlled by two soft
|
||||
switches. The soft switches are controlled by accessing the following
|
||||
memory locations with either a read or a write operation.
|
||||
|
||||
$COBO - SwyftCard active, Bank 1
|
||||
$COB1 - SwyftCard inactive, Bank 1
|
||||
$COB2 - SwyftCard active, Bank 2
|
||||
|
||||
When the power-on reset circuit asserts the RES signal on Pin 3 of the
|
||||
PAL, the SwyftCard is made active in Bank 1. Accessing location
|
||||
$COB1 deactivates the SwyftCard for normal Apple IIe operation.
|
||||
|
||||
The INH' line is driven by a tri-state driver, so if another card in the
|
||||
Apple /Ie asserts the IINH' signal there will not be a bus contention.
|
||||
However, there will be a bus contention on the data bus if another card
|
||||
attempts to control the bus while the SwyftCard is active.
|
||||
|
||||
The Cx00 rom is not used. The card is expected to be installed in
|
||||
slot 3 of an Apple IIe with the 80 column firmware already present.
|
||||
|
||||
*/
|
||||
|
||||
// CardSwyft represents a Swyft card
|
||||
type CardSwyft struct {
|
||||
cardBase
|
||||
bank2 bool
|
||||
rom []uint8
|
||||
}
|
||||
|
||||
func newCardSwyftBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "SwyftCard",
|
||||
description: "Card with the ROM needed to run the Swyftcard word processing system",
|
||||
requiresIIe: true,
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardSwyft
|
||||
|
||||
// Load main ROM replacement
|
||||
data, _, err := LoadResource("<internal>/SwyftCard ROM.bin")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.rom = data
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardSwyft) assign(a *Apple2, slot int) {
|
||||
if slot != 3 {
|
||||
panic("SwyftCard must be installed in slot 3")
|
||||
}
|
||||
|
||||
c.addCardSoftSwitchRW(0, func() uint8 {
|
||||
a.mmu.inhibitROM(c)
|
||||
c.bank2 = false
|
||||
return 0x55
|
||||
}, "SWYFTONBANK1")
|
||||
|
||||
c.addCardSoftSwitchRW(1, func() uint8 {
|
||||
a.mmu.inhibitROM(nil)
|
||||
c.bank2 = false
|
||||
return 0x55
|
||||
}, "SWYFTOFFBANK1")
|
||||
|
||||
c.addCardSoftSwitchRW(2, func() uint8 {
|
||||
a.mmu.inhibitROM(c)
|
||||
c.bank2 = true
|
||||
return 0x55
|
||||
}, "SWYFTONBANK2")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
a.mmu.inhibitROM(c)
|
||||
}
|
||||
|
||||
func (c *CardSwyft) translateAddress(address uint16) uint16 {
|
||||
/*
|
||||
The four 4k sections of the 16k ROM image are mapped:
|
||||
D000-DFFF (page 1)
|
||||
D000-DFFF (page 2)
|
||||
E000-EFFF
|
||||
F000-FFFF
|
||||
*/
|
||||
if address >= 0xE000 {
|
||||
return address - 0xE000 + 0x2000
|
||||
}
|
||||
if !c.bank2 {
|
||||
return address - 0xD000
|
||||
}
|
||||
return address - 0xD000 + 0x1000
|
||||
}
|
||||
|
||||
func (c *CardSwyft) peek(address uint16) uint8 {
|
||||
return c.rom[c.translateAddress(address)]
|
||||
}
|
||||
|
||||
func (c *CardSwyft) poke(address uint16, value uint8) {
|
||||
// Nothing
|
||||
}
|
23
cardSwyft_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSwyftTutorial(t *testing.T) {
|
||||
at, err := makeApple2Tester("swyft", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
at.terminateCondition = buildTerminateConditionText(at, "HOW TO USE SWYFTCARD", true, 10_000_000)
|
||||
|
||||
at.run()
|
||||
|
||||
text := at.getText80()
|
||||
if !strings.Contains(text, "HOW TO USE SWYFTCARD") {
|
||||
t.Errorf("Expected 'HOW TO USE SWYFTCARD', got '%s'", text)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import "github.com/ivanizag/izapple2/component"
|
||||
|
||||
/*
|
||||
ThunderClock`, real time clock card.
|
||||
|
@ -19,14 +21,30 @@ uPD1990AC hookup:
|
|||
bit 7 = data out
|
||||
*/
|
||||
|
||||
type cardThunderClockPlus struct {
|
||||
microPD1990ac
|
||||
// CardThunderClockPlus represents a ThunderClock+ card
|
||||
type CardThunderClockPlus struct {
|
||||
cardBase
|
||||
upd1990 component.MicroPD1990ac
|
||||
}
|
||||
|
||||
func (c *cardThunderClockPlus) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchR(0, func(*ioC0Page) uint8 {
|
||||
bit := c.microPD1990ac.out()
|
||||
func newCardThunderClockPlusBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "ThunderClock+ Card",
|
||||
description: "Clock card",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardThunderClockPlus
|
||||
err := c.loadRomFromResource("<internal>/ThunderclockPlusROM.bin", cardRomUpper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardThunderClockPlus) assign(a *Apple2, slot int) {
|
||||
c.addCardSoftSwitchR(0, func() uint8 {
|
||||
bit := c.upd1990.Out()
|
||||
// Get the next data bit from uPD1990AC on the MSB
|
||||
if bit {
|
||||
return 0x80
|
||||
|
@ -34,7 +52,7 @@ func (c *cardThunderClockPlus) assign(a *Apple2, slot int) {
|
|||
return 0
|
||||
}, "THUNDERCLOCKR")
|
||||
|
||||
c.addCardSoftSwitchW(0, func(_ *ioC0Page, value uint8) {
|
||||
c.addCardSoftSwitchW(0, func(value uint8) {
|
||||
dataIn := (value & 0x01) == 1
|
||||
clock := ((value >> 1) & 0x01) == 1
|
||||
strobe := ((value >> 2) & 0x01) == 1
|
||||
|
@ -42,7 +60,7 @@ func (c *cardThunderClockPlus) assign(a *Apple2, slot int) {
|
|||
/* fmt.Printf("[cardThunderClock] dataIn %v, clock %v, strobe %v, command %v.\n",
|
||||
dataIn, clock, strobe, command) */
|
||||
|
||||
c.microPD1990ac.in(clock, strobe, command, dataIn)
|
||||
c.upd1990.In(clock, strobe, command, dataIn)
|
||||
}, "THUNDERCLOCKW")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
|
|
35
cardVidHD.go
|
@ -1,4 +1,4 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
/*
|
||||
Simulates just what is needed to make Total Replay use the GS modes if the VidHD card is found
|
||||
|
@ -8,10 +8,23 @@ See:
|
|||
http://www.applelogic.org/files/GSHARDWAREREF.pdf, page 89
|
||||
*/
|
||||
|
||||
type cardVidHD struct {
|
||||
// CardVidHD represents a VidHD card
|
||||
type CardVidHD struct {
|
||||
cardBase
|
||||
}
|
||||
|
||||
func newCardVidHDBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "VidHD Card - limited",
|
||||
description: "Firmware signature of the VidHD card to trick Total Replay to use the SHR mode",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardVidHD
|
||||
c.loadRom(buildVidHDRom(), cardRomSimple)
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildVidHDRom() []uint8 {
|
||||
data := make([]uint8, 256)
|
||||
|
||||
|
@ -26,16 +39,16 @@ const (
|
|||
ioDataNewVideo uint8 = 0x29
|
||||
)
|
||||
|
||||
func (c *cardVidHD) assign(a *Apple2, slot int) {
|
||||
func (c *CardVidHD) assign(a *Apple2, slot int) {
|
||||
// The softswitches are outside the card reserved ss
|
||||
a.io.addSoftSwitchR(0x22, notImplementedSoftSwitchR, "VIDHD-TBCOLOR")
|
||||
a.io.addSoftSwitchW(0x22, notImplementedSoftSwitchW, "VIDHD-TBCOLOR")
|
||||
a.io.addSoftSwitchR(0x29, getStatusSoftSwitch(ioDataNewVideo), "VIDHD-NEWVIDEO")
|
||||
a.io.addSoftSwitchW(0x29, setStatusSoftSwitch(ioDataNewVideo), "VIDHD-NEWVIDEO")
|
||||
a.io.addSoftSwitchR(0x34, notImplementedSoftSwitchR, "VIDHD-CLOCKCTL")
|
||||
a.io.addSoftSwitchW(0x34, notImplementedSoftSwitchW, "VIDHD-CLOCKCTL")
|
||||
a.io.addSoftSwitchR(0x35, notImplementedSoftSwitchR, "VIDHD-SHADOW")
|
||||
a.io.addSoftSwitchW(0x35, notImplementedSoftSwitchW, "VIDHD-SHADOW")
|
||||
a.io.addSoftSwitchR(0x22, buildNotImplementedSoftSwitchR(a.io), "VIDHD-TBCOLOR")
|
||||
a.io.addSoftSwitchW(0x22, buildNotImplementedSoftSwitchW(a.io), "VIDHD-TBCOLOR")
|
||||
a.io.addSoftSwitchR(0x29, getStatusSoftSwitch(a.io, ioDataNewVideo), "VIDHD-NEWVIDEO")
|
||||
a.io.addSoftSwitchW(0x29, setStatusSoftSwitch(a.io, ioDataNewVideo), "VIDHD-NEWVIDEO")
|
||||
a.io.addSoftSwitchR(0x34, buildNotImplementedSoftSwitchR(a.io), "VIDHD-CLOCKCTL")
|
||||
a.io.addSoftSwitchW(0x34, buildNotImplementedSoftSwitchW(a.io), "VIDHD-CLOCKCTL")
|
||||
a.io.addSoftSwitchR(0x35, buildNotImplementedSoftSwitchR(a.io), "VIDHD-SHADOW")
|
||||
a.io.addSoftSwitchW(0x35, buildNotImplementedSoftSwitchW(a.io), "VIDHD-SHADOW")
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
}
|
||||
|
|
190
cardVidex.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"github.com/ivanizag/izapple2/component"
|
||||
)
|
||||
|
||||
/*
|
||||
Videx 80 columns card for the Apple II+
|
||||
|
||||
See:
|
||||
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/80%20Column%20Cards/Videx%20Videoterm/Manuals/Videx%20Videoterm%20-%20Installation%20and%20Operation%20Manual.pdf
|
||||
http://bitsavers.trailing-edge.com/components/motorola/_dataSheets/6845.pdf
|
||||
https://glasstty.com/?p=660
|
||||
|
||||
*/
|
||||
|
||||
// CardVidex represents a Videx compatible 80 column card
|
||||
type CardVidex struct {
|
||||
cardBase
|
||||
mc6845 component.MC6845
|
||||
sramPage uint8
|
||||
sram [0x800]uint8
|
||||
upperROM memoryHandler
|
||||
charGen []uint8
|
||||
}
|
||||
|
||||
func newCardVidexBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Videx 80 columns Card",
|
||||
description: "Videx compatible 80 columns card",
|
||||
defaultParams: &[]paramSpec{
|
||||
{"rom", "ROM file to load", "<internal>/Videx Videoterm ROM 2.4.bin"},
|
||||
{"charmap", "Character map file to load", "<internal>/80ColumnP110.BIN"},
|
||||
},
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardVidex
|
||||
|
||||
// The C800 area has ROM and RAM
|
||||
err := c.loadRomFromResource("<internal>/Videx Videoterm ROM 2.4.bin", cardRomUpperHalfEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.upperROM = c.romC8xx
|
||||
c.romC8xx = &c
|
||||
|
||||
err = c.loadCharacterMap(paramsGetPath(params, "charmap"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardVidex) loadCharacterMap(filename string) error {
|
||||
bytes, _, err := LoadResource(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size := len(bytes)
|
||||
if size < 0x800 {
|
||||
return errors.New("character ROM size not supported for Videx")
|
||||
}
|
||||
c.charGen = bytes
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CardVidex) assign(a *Apple2, slot int) {
|
||||
|
||||
// TODO: use addCardSoftSwitches()
|
||||
for i := uint8(0x0); i <= 0xf; i++ {
|
||||
// Bit 0 goes to the RS pin of the MC6548. It controls
|
||||
// whether a register is being accesed or the contents
|
||||
// of the register is being accessed
|
||||
rsPin := (i & 1) == 1
|
||||
|
||||
// Bits 2 and 3 determine which page will be selected
|
||||
sramPage := i >> 2
|
||||
|
||||
ssName := fmt.Sprintf("VIDEXPAGE%v", sramPage)
|
||||
if rsPin {
|
||||
ssName += "REG"
|
||||
} else {
|
||||
ssName += "ADDRESS"
|
||||
}
|
||||
|
||||
c.addCardSoftSwitchR(i, func() uint8 {
|
||||
c.sramPage = sramPage
|
||||
return c.mc6845.Read(rsPin)
|
||||
}, ssName+"R")
|
||||
c.addCardSoftSwitchW(i, func(value uint8) {
|
||||
c.sramPage = sramPage
|
||||
c.mc6845.Write(rsPin, value)
|
||||
}, ssName+"W")
|
||||
}
|
||||
|
||||
c.cardBase.assign(a, slot)
|
||||
a.softVideoSwitch = NewSoftVideoSwitch(c)
|
||||
}
|
||||
|
||||
const videxRomLimit = uint16(0xcc00)
|
||||
const videxSramLimit = uint16(0xce00)
|
||||
const videxSramMask = uint16(0x01ff)
|
||||
|
||||
func (c *CardVidex) peek(address uint16) uint8 {
|
||||
if address < videxRomLimit {
|
||||
return c.upperROM.peek(address)
|
||||
} else if address < videxSramLimit {
|
||||
return c.sram[address&videxSramMask+uint16(c.sramPage)*0x200]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *CardVidex) poke(address uint16, value uint8) {
|
||||
if address >= videxRomLimit && address < videxSramLimit {
|
||||
c.sram[address&videxSramMask+uint16(c.sramPage)*0x200] = value
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
videxCharWidth = uint8(8)
|
||||
)
|
||||
|
||||
func (c *CardVidex) buildImage(light color.Color) *image.RGBA {
|
||||
params := c.mc6845.ImageData()
|
||||
width, height := params.DisplayedWidthHeight(videxCharWidth)
|
||||
if (width == 0) || (height == 0) {
|
||||
// No image available
|
||||
size := image.Rect(0, 0, 3, 3)
|
||||
img := image.NewRGBA(size)
|
||||
img.Set(1, 1, color.White)
|
||||
return img
|
||||
}
|
||||
ms := time.Now().Nanosecond() / (1000 * 1000) // Host time, used for the cursoR blink
|
||||
|
||||
size := image.Rect(0, 0, width, height)
|
||||
img := image.NewRGBA(size)
|
||||
|
||||
params.IterateScreen(func(address uint16, charLine uint8,
|
||||
cursorMode uint8, displayEnable bool,
|
||||
column uint8, y int) {
|
||||
|
||||
bits := uint8(0)
|
||||
if displayEnable {
|
||||
char := c.sram[address&0x7ff]
|
||||
bits = c.charGen[(uint16(char&0x7f)<<4)+uint16(charLine)]
|
||||
isCursor := false
|
||||
switch cursorMode {
|
||||
case component.MC6845CursorFixed:
|
||||
isCursor = true
|
||||
case component.MC6845CursorSlow:
|
||||
// It should be 533ms (32/60, 32 screen refreshes)
|
||||
// Let's make a 2 blinks per second
|
||||
isCursor = ms/2 > 1000/4
|
||||
case component.MC6845CursorFast:
|
||||
// It should be 266ms (32/60, 16 screen refreshes)
|
||||
// Let's make a 4 blinks per second
|
||||
isCursor = ms/4 > 1000/8
|
||||
}
|
||||
if isCursor {
|
||||
bits = ^bits
|
||||
}
|
||||
if char >= 128 {
|
||||
// Inverse
|
||||
bits = ^bits
|
||||
}
|
||||
}
|
||||
|
||||
x := int(column) * int(videxCharWidth)
|
||||
|
||||
for i := 0; i < int(videxCharWidth); i++ {
|
||||
pixel := (bits & 0x80) != 0
|
||||
if pixel {
|
||||
img.Set(x, y, light)
|
||||
} else {
|
||||
img.Set(x, y, color.Black)
|
||||
}
|
||||
bits <<= 1
|
||||
x++
|
||||
}
|
||||
})
|
||||
|
||||
return img
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package apple2
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -15,6 +15,7 @@ type CharacterGenerator struct {
|
|||
data []uint8
|
||||
columnMap charColumnMap
|
||||
page int
|
||||
pageSize int
|
||||
}
|
||||
|
||||
type charColumnMap func(column int) int
|
||||
|
@ -28,28 +29,31 @@ func charGenColumnsMap2e(column int) int {
|
|||
}
|
||||
|
||||
const (
|
||||
charGenPageSize = 2048
|
||||
charGenPageSize2Plus = 2048
|
||||
charGenPageSize2E = 2048 * 2
|
||||
)
|
||||
|
||||
// NewCharacterGenerator instantiates a new Character Generator with the rom on the file given
|
||||
func newCharacterGenerator(filename string, order charColumnMap) (*CharacterGenerator, error) {
|
||||
func newCharacterGenerator(filename string, order charColumnMap, pageSize int) (*CharacterGenerator, error) {
|
||||
var cg CharacterGenerator
|
||||
cg.columnMap = order
|
||||
cg.pageSize = pageSize
|
||||
err := cg.load(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cg.columnMap = order
|
||||
|
||||
return &cg, nil
|
||||
}
|
||||
|
||||
func (cg *CharacterGenerator) load(filename string) error {
|
||||
bytes, err := loadResource(filename)
|
||||
bytes, _, err := LoadResource(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size := len(bytes)
|
||||
if size < charGenPageSize {
|
||||
return errors.New("Character ROM size not supported")
|
||||
if size < cg.pageSize {
|
||||
return errors.New("character ROM size not supported")
|
||||
}
|
||||
cg.data = bytes
|
||||
return nil
|
||||
|
@ -57,63 +61,47 @@ func (cg *CharacterGenerator) load(filename string) error {
|
|||
|
||||
func (cg *CharacterGenerator) setPage(page int) {
|
||||
// Some clones had a switch to change codepage with extra characters
|
||||
pages := len(cg.data) / charGenPageSize
|
||||
pages := len(cg.data) / cg.pageSize
|
||||
cg.page = page % pages
|
||||
}
|
||||
|
||||
func (cg *CharacterGenerator) getPage() int {
|
||||
return cg.page
|
||||
}
|
||||
|
||||
func (cg *CharacterGenerator) nextPage() {
|
||||
cg.setPage(cg.page + 1)
|
||||
}
|
||||
|
||||
func (cg *CharacterGenerator) getPixel(char uint8, row int, column int) bool {
|
||||
bits := cg.data[int(char)*8+row+cg.page*charGenPageSize]
|
||||
bits := cg.data[int(char)*8+row+cg.page*cg.pageSize]
|
||||
bit := cg.columnMap(column)
|
||||
value := bits >> uint(bit) & 1
|
||||
return value == 1
|
||||
}
|
||||
|
||||
func (cg *CharacterGenerator) dumpCharRaw(char int) {
|
||||
base := int(char) * 8
|
||||
fmt.Printf("Char: %v\n---------\n", char)
|
||||
for i := 0; i < 8; i++ {
|
||||
fmt.Print("|")
|
||||
b := cg.data[base+i]
|
||||
for j := 0; j < 8; j++ {
|
||||
if (b>>uint(j))&1 == 1 {
|
||||
fmt.Print("#")
|
||||
} else {
|
||||
fmt.Print(" ")
|
||||
}
|
||||
}
|
||||
fmt.Println("|")
|
||||
func setupCharactedGenerator(a *Apple2, board string, charRomFile string) error {
|
||||
var charGenMap charColumnMap
|
||||
initialCharGenPage := 0
|
||||
pageSize := charGenPageSize2Plus
|
||||
switch board {
|
||||
case "2plus":
|
||||
charGenMap = charGenColumnsMap2Plus
|
||||
case "2e":
|
||||
charGenMap = charGenColumnsMap2e
|
||||
pageSize = charGenPageSize2E
|
||||
case "base64a":
|
||||
charGenMap = charGenColumnsMapBase64a
|
||||
initialCharGenPage = 1
|
||||
default:
|
||||
return fmt.Errorf("board %s not supported it must be '2plus', '2e' or 'base64a'", board)
|
||||
}
|
||||
fmt.Println("---------")
|
||||
}
|
||||
|
||||
func (cg *CharacterGenerator) dumpChar(char uint8) {
|
||||
fmt.Printf("Char: %v\n---------\n", char)
|
||||
for row := 0; row < 8; row++ {
|
||||
fmt.Print("|")
|
||||
for col := 0; col < 7; col++ {
|
||||
if cg.getPixel(char, row, col) {
|
||||
fmt.Print("#")
|
||||
} else {
|
||||
fmt.Print(" ")
|
||||
}
|
||||
}
|
||||
fmt.Println("|")
|
||||
}
|
||||
fmt.Println("---------")
|
||||
}
|
||||
|
||||
// Dump to sdtout all the character maps
|
||||
func (cg *CharacterGenerator) Dump() {
|
||||
pages := len(cg.data) / charGenPageSize
|
||||
for p := 0; p < pages; p++ {
|
||||
cg.setPage(p)
|
||||
for i := 0; i < 256; i++ {
|
||||
cg.dumpChar(uint8(i))
|
||||
//cg.dumpCharRaw(int(i))
|
||||
}
|
||||
cg, err := newCharacterGenerator(charRomFile, charGenMap, pageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cg.setPage(initialCharGenPage)
|
||||
a.cg = cg
|
||||
return nil
|
||||
}
|
||||
|
|
106
command.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package izapple2
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
|
||||
CommandToggleSpeed = iota + 1
|
||||
// CommandShowSpeed toggles printinf the current freq in Mhz
|
||||
CommandShowSpeed
|
||||
// CommandDumpDebugInfo dumps useful info
|
||||
CommandDumpDebugInfo
|
||||
// CommandNextCharGenPage cycles the CharGen page if several
|
||||
CommandNextCharGenPage
|
||||
// CommandToggleCPUTrace toggle tracing of CPU execution
|
||||
CommandToggleCPUTrace
|
||||
// CommandKill stops the cpu execution loop
|
||||
CommandKill
|
||||
// CommandReset executes a 6502 reset
|
||||
CommandReset
|
||||
// CommandPauseUnpause allows the Pause button to freeze the emulator for a coffee break
|
||||
CommandPauseUnpause
|
||||
// CommandPause pauses the emulator
|
||||
CommandPause
|
||||
// CommandStart restarts the emulator
|
||||
CommandStart
|
||||
// CommandComplex for commands that use a struct with parameters
|
||||
CommandComplex
|
||||
)
|
||||
|
||||
type command interface {
|
||||
getId() int
|
||||
}
|
||||
|
||||
type commandSimple struct {
|
||||
id int
|
||||
}
|
||||
|
||||
type commandLoadDisk struct {
|
||||
drive int
|
||||
path string
|
||||
}
|
||||
|
||||
func (c *commandSimple) getId() int {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c *commandLoadDisk) getId() int {
|
||||
return CommandComplex
|
||||
}
|
||||
|
||||
func (a *Apple2) queueCommand(c command) {
|
||||
a.commandChannel <- c
|
||||
}
|
||||
|
||||
// SendCommand enqueues a command to the emulator thread
|
||||
func (a *Apple2) SendCommand(commandId int) {
|
||||
var c commandSimple
|
||||
c.id = commandId
|
||||
a.queueCommand(&c)
|
||||
}
|
||||
|
||||
func (a *Apple2) SendLoadDisk(drive int, path string) {
|
||||
var c commandLoadDisk
|
||||
c.drive = drive
|
||||
c.path = path
|
||||
a.queueCommand(&c)
|
||||
}
|
||||
|
||||
func (a *Apple2) executeCommand(command command) {
|
||||
switch command.getId() {
|
||||
case CommandToggleSpeed:
|
||||
if a.cycleDurationNs == 0 {
|
||||
//fmt.Println("Slow")
|
||||
a.cycleDurationNs = 1000.0 / CPUClockMhz
|
||||
} else {
|
||||
//fmt.Println("Fast")
|
||||
a.cycleDurationNs = 0
|
||||
}
|
||||
case CommandShowSpeed:
|
||||
a.showSpeed = !a.showSpeed
|
||||
case CommandDumpDebugInfo:
|
||||
a.dumpDebugInfo()
|
||||
case CommandNextCharGenPage:
|
||||
a.cg.nextPage()
|
||||
fmt.Printf("Chargen page %v\n", a.cg.page)
|
||||
case CommandToggleCPUTrace:
|
||||
a.cpu.SetTrace(!a.cpu.GetTrace())
|
||||
case CommandReset:
|
||||
a.reset()
|
||||
case CommandComplex:
|
||||
switch t := command.(type) {
|
||||
case *commandLoadDisk:
|
||||
err := a.changeDisk(t.drive, t.path)
|
||||
if err != nil {
|
||||
fmt.Printf("Could no load file %v\n%v\n", t.path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Apple2) changeDisk(unit int, path string) error {
|
||||
if unit < len(a.removableMediaDrives) {
|
||||
return a.removableMediaDrives[unit].insertDiskette(path)
|
||||
}
|
||||
return fmt.Errorf("unit %v not defined", unit)
|
||||
}
|
117
component/mc6845.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package component
|
||||
|
||||
/*
|
||||
MC6845 CRT Controller
|
||||
See:
|
||||
Motorola MC6845 datasheet
|
||||
|
||||
Pins:
|
||||
RW, RS, D0-D7: Read() and Write()
|
||||
MA0-13, RA04, CURSOR, DE: MC6845RasterCallBack()
|
||||
*/
|
||||
|
||||
type MC6845 struct {
|
||||
reg [18]uint8 // Internal registers R0 to R17
|
||||
sel uint8 // Selected address register AR
|
||||
}
|
||||
|
||||
func (m *MC6845) Read(rs bool) uint8 {
|
||||
if !rs {
|
||||
// AR is not readable
|
||||
return 0x00
|
||||
} else if m.sel >= 14 && m.sel <= 17 {
|
||||
// Only R14 to R17 are readable
|
||||
// Should we mask R14 and R16?
|
||||
return m.reg[m.sel]
|
||||
}
|
||||
return 0x00
|
||||
}
|
||||
|
||||
func (m *MC6845) Write(rs bool, value uint8) {
|
||||
if !rs {
|
||||
// AR is 5 bits
|
||||
// What happens if AR > 17 ?
|
||||
m.sel = value & 0x1f
|
||||
} else if m.sel <= 15 {
|
||||
// R0 to R15 are writable
|
||||
m.reg[m.sel] = value
|
||||
//fmt.Printf("Set %v to %v\n", m.sel, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MC6845) ImageData() MC6845ImageData {
|
||||
var data MC6845ImageData
|
||||
|
||||
data.firstChar = uint16(m.reg[12]&0x3f)<<8 + uint16(m.reg[13])
|
||||
data.charLines = (m.reg[9] + 1) & 0x1f
|
||||
data.columns = m.reg[1]
|
||||
data.lines = m.reg[6] & 0x7f
|
||||
data.adjustLines = m.reg[5] & 0x1f
|
||||
|
||||
data.cursorPos = uint16(m.reg[14]&0x3f)<<8 + uint16(m.reg[15])
|
||||
data.cursorStart = m.reg[10] & 0x1f
|
||||
data.cursorEnd = m.reg[11] & 0x1f
|
||||
data.cursorMode = (m.reg[10] >> 5) & 0x03 // Bit 6 and 5
|
||||
return data
|
||||
}
|
||||
|
||||
const (
|
||||
MC6845CursorFixed = uint8(0)
|
||||
MC6845CursorNone = uint8(1)
|
||||
MC6845CursorFast = uint8(2)
|
||||
MC6845CursorSlow = uint8(3)
|
||||
)
|
||||
|
||||
type MC6845ImageData struct {
|
||||
firstChar uint16 // 14 bits, address of the firt char on the first line
|
||||
charLines uint8 // 5 bits, lines par character
|
||||
columns uint8 // 8 bits, chars per line
|
||||
lines uint8 // 7 bits, char lines per screen
|
||||
adjustLines uint8 // 5 bits, extra blank lines
|
||||
|
||||
cursorPos uint16 // 14 bits, address? of the cursor position
|
||||
cursorStart uint8 // 5 bits, cursor starting char row
|
||||
cursorEnd uint8 // 5 bits, cursor ending char row
|
||||
cursorMode uint8 // 2 bits, cursor mode
|
||||
|
||||
}
|
||||
|
||||
func (data *MC6845ImageData) DisplayedWidthHeight(charWidth uint8) (int, int) {
|
||||
return int(data.columns) * int(charWidth),
|
||||
int(data.lines)*int(data.charLines) + int(data.adjustLines)
|
||||
}
|
||||
|
||||
type MC6845RasterCallBack func(address uint16, charLine uint8, // Lookup in char ROM
|
||||
cursorMode uint8, displayEnable bool, // Modifiers
|
||||
column uint8, y int) // Position in screen
|
||||
|
||||
func (data *MC6845ImageData) IterateScreen(callBack MC6845RasterCallBack) {
|
||||
lineAddress := data.firstChar
|
||||
y := 0
|
||||
var address uint16
|
||||
for line := uint8(0); line < data.lines; line++ {
|
||||
for charLine := uint8(0); charLine < data.charLines; charLine++ {
|
||||
address = lineAddress // Back to the first char of the line
|
||||
for column := uint8(0); column < data.columns; column++ {
|
||||
cursorMode := MC6845CursorNone
|
||||
isCursor := (address == data.cursorPos) &&
|
||||
(charLine >= data.cursorStart) &&
|
||||
(charLine <= data.cursorEnd)
|
||||
if isCursor {
|
||||
cursorMode = data.cursorMode
|
||||
}
|
||||
|
||||
callBack(address, charLine, cursorMode, true, column, y)
|
||||
address = (address + 1) & 0x3fff // 14 bits
|
||||
}
|
||||
y++
|
||||
}
|
||||
lineAddress = address
|
||||
}
|
||||
for adjust := uint8(0); adjust <= data.adjustLines; adjust++ {
|
||||
for column := uint8(0); column < data.columns; column++ {
|
||||
callBack(0, 0, MC6845CursorNone, false, column, y) // lines with display not enabled
|
||||
}
|
||||
y++
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package apple2
|
||||
package component
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
@ -21,8 +21,7 @@ import (
|
|||
byte 0: seconds, BCD 0 to 59
|
||||
|
||||
*/
|
||||
|
||||
type microPD1990ac struct {
|
||||
type MicroPD1990ac struct {
|
||||
clock bool // CLK state
|
||||
strobe bool // STB state
|
||||
command uint8 // C0, C1, C2 command. From 0 to 7
|
||||
|
@ -36,7 +35,7 @@ const (
|
|||
mpd1990commandTimeRead = 3
|
||||
)
|
||||
|
||||
func (m *microPD1990ac) in(clock bool, strobe bool, command uint8, dataIn bool) {
|
||||
func (m *MicroPD1990ac) In(clock bool, strobe bool, command uint8, dataIn bool) {
|
||||
// Detect signal raise
|
||||
clockRaise := clock && !m.clock
|
||||
strobeRaise := strobe && !m.strobe
|
||||
|
@ -69,20 +68,22 @@ func (m *microPD1990ac) in(clock bool, strobe bool, command uint8, dataIn bool)
|
|||
}
|
||||
}
|
||||
|
||||
func (m *microPD1990ac) out() bool {
|
||||
if m.command == mpd1990commandRegHold {
|
||||
//panic("Output on RegHold should be a 1Hz signal. Not implemented.")
|
||||
}
|
||||
func (m *MicroPD1990ac) Out() bool {
|
||||
/*
|
||||
if m.command == mpd1990commandRegHold {
|
||||
panic("Output on RegHold should be a 1Hz signal. Not implemented.")
|
||||
}
|
||||
|
||||
if m.command == mpd1990commandTimeRead {
|
||||
//panic("Output on RegHold should be a 512Hz signal with LSB. Not implemented.")
|
||||
}
|
||||
if m.command == mpd1990commandTimeRead {
|
||||
panic("Output on RegHold should be a 512Hz signal with LSB. Not implemented.")
|
||||
}
|
||||
*/
|
||||
|
||||
// Return the LSB of the register shift
|
||||
return (m.register & 1) == 1
|
||||
}
|
||||
|
||||
func (m *microPD1990ac) loadTime() {
|
||||
func (m *MicroPD1990ac) loadTime() {
|
||||
now := time.Now()
|
||||
|
||||
var register uint64
|
35
component/pins.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package component
|
||||
|
||||
func ByteToPins(v uint8) [8]bool {
|
||||
var pins [8]bool
|
||||
for i := 0; i < 8; i++ {
|
||||
pins[i] = (v & 1) != 0
|
||||
v >>= 1
|
||||
}
|
||||
return pins
|
||||
}
|
||||
|
||||
func PinsToByte(pins [8]bool) uint8 {
|
||||
v := uint8(0)
|
||||
for i := 7; i >= 0; i-- {
|
||||
v <<= 1
|
||||
if pins[i] {
|
||||
v++
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func ReversePins(data uint8) uint8 {
|
||||
pins := ByteToPins(data)
|
||||
return PinsToByte([8]bool{
|
||||
pins[7],
|
||||
pins[6],
|
||||
pins[5],
|
||||
pins[4],
|
||||
pins[3],
|
||||
pins[2],
|
||||
pins[1],
|
||||
pins[0],
|
||||
})
|
||||
}
|
29
component/pins_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package component
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestByteToPins(t *testing.T) {
|
||||
v := uint8(0b10010110)
|
||||
a := [8]bool{
|
||||
false, true, true, false,
|
||||
true, false, false, true,
|
||||
}
|
||||
|
||||
if a != ByteToPins(v) {
|
||||
t.Error("Error on byte to pins")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinsToByte(t *testing.T) {
|
||||
v := uint8(0b10010110)
|
||||
a := [8]bool{
|
||||
false, true, true, false,
|
||||
true, false, false, true,
|
||||
}
|
||||
|
||||
if v != PinsToByte(a) {
|
||||
t.Error("Error on pins to byte")
|
||||
}
|
||||
}
|
7
configs/2.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
name: Apple ][
|
||||
parent: _base
|
||||
board: 2plus
|
||||
mods: four-colors
|
||||
rom: <internal>/341-000x_integer.rom
|
||||
charrom: <internal>/Apple2rev7CharGen.rom
|
||||
forceCaps: true
|
7
configs/2e.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
name: Apple IIe
|
||||
parent: _base
|
||||
board: 2e
|
||||
rom: <internal>/Apple2e.rom
|
||||
charrom: <internal>/Apple IIe Video Unenhanced.bin
|
||||
s0: language
|
||||
s6: diskii,disk1=<internal>/dos33.dsk
|
12
configs/2enh.cfg
Normal file
|
@ -0,0 +1,12 @@
|
|||
name: Apple //e
|
||||
parent: _base
|
||||
board: 2e
|
||||
cpu: 65c02
|
||||
rom: <internal>/Apple2e_Enhanced.rom
|
||||
charrom: <internal>/Apple IIe Video Enhanced.bin
|
||||
ramworks: 8192
|
||||
nsc: main
|
||||
s0: language
|
||||
s2: vidhd
|
||||
s3: fastchip
|
||||
s6: diskii,disk1=<internal>/dos33.dsk
|
9
configs/2plus.cfg
Normal file
|
@ -0,0 +1,9 @@
|
|||
name: Apple ][+
|
||||
parent: _base
|
||||
board: 2plus
|
||||
rom: <internal>/Apple2_Plus.rom
|
||||
charrom: <internal>/Apple2rev7CharGen.rom
|
||||
forceCaps: true
|
||||
s0: language
|
||||
s3: videx
|
||||
s6: diskii,disk1=<internal>/dos33.dsk
|
20
configs/_base.cfg
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: No name
|
||||
cpu: 6502
|
||||
speed: ntsc
|
||||
profile: false
|
||||
forceCaps: false
|
||||
ramworks: none
|
||||
nsc: none
|
||||
mods:
|
||||
rgb: false
|
||||
romx: false
|
||||
chargenmap: 2e
|
||||
trace: none
|
||||
s0: empty
|
||||
s1: empty
|
||||
s2: empty
|
||||
s3: empty
|
||||
s4: empty
|
||||
s5: empty
|
||||
s6: empty
|
||||
s7: empty
|
8
configs/base64a.cfg
Normal file
|
@ -0,0 +1,8 @@
|
|||
name: Base 64A
|
||||
parent: _base
|
||||
board: base64a
|
||||
rom: <custom>
|
||||
charrom: <internal>/BASE64A_ROM7_CharGen.BIN
|
||||
s0: language
|
||||
s1: parallel
|
||||
s6: diskii,disk1=<internal>/dos33.dsk
|
4
configs/swyft.cfg
Normal file
|
@ -0,0 +1,4 @@
|
|||
name: swyft
|
||||
parent: 2enh
|
||||
s3: swyftcard
|
||||
s6: diskii,disk1=<internal>/SwyftWare_-_SwyftCard_Tutorial.woz
|
277
configuration.go
Normal file
|
@ -0,0 +1,277 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const configSuffix = ".cfg"
|
||||
const defaultConfiguration = "2enh"
|
||||
|
||||
const (
|
||||
confParent = "parent"
|
||||
confModel = "model"
|
||||
confName = "name"
|
||||
confBoard = "board"
|
||||
|
||||
confRom = "rom"
|
||||
confCharRom = "charrom"
|
||||
confCpu = "cpu"
|
||||
confSpeed = "speed"
|
||||
confRamworks = "ramworks"
|
||||
confNsc = "nsc"
|
||||
confTrace = "trace"
|
||||
confProfile = "profile"
|
||||
confForceCaps = "forceCaps"
|
||||
confRgb = "rgb"
|
||||
confRomx = "romx"
|
||||
confMods = "mods"
|
||||
confS0 = "s0"
|
||||
confS1 = "s1"
|
||||
confS2 = "s2"
|
||||
confS3 = "s3"
|
||||
confS4 = "s4"
|
||||
confS5 = "s5"
|
||||
confS6 = "s6"
|
||||
confS7 = "s7"
|
||||
)
|
||||
|
||||
//go:embed configs/*.cfg
|
||||
var configurationFiles embed.FS
|
||||
|
||||
type configurationModels struct {
|
||||
preconfiguredConfigs map[string]*configuration
|
||||
}
|
||||
|
||||
type configuration struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func newConfiguration() *configuration {
|
||||
c := configuration{}
|
||||
c.data = make(map[string]string)
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *configuration) getHas(key string) (string, bool) {
|
||||
key = strings.ToLower(key)
|
||||
value, ok := c.data[key]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (c *configuration) get(key string) string {
|
||||
key = strings.ToLower(key)
|
||||
value, ok := c.data[key]
|
||||
if !ok {
|
||||
// Should not happen
|
||||
panic(fmt.Errorf("key %s not found", key))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *configuration) getFlag(key string) bool {
|
||||
return c.get(key) == "true"
|
||||
}
|
||||
|
||||
func (c *configuration) set(key string, value string) {
|
||||
key = strings.ToLower(key)
|
||||
c.data[key] = value
|
||||
}
|
||||
|
||||
func loadConfigurationModelsAndDefault() (*configurationModels, *configuration, error) {
|
||||
models := &configurationModels{}
|
||||
dir, err := configurationFiles.ReadDir("configs")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
models.preconfiguredConfigs = make(map[string]*configuration)
|
||||
for _, file := range dir {
|
||||
if file.Type().IsRegular() && strings.HasSuffix(strings.ToLower(file.Name()), configSuffix) {
|
||||
content, err := configurationFiles.ReadFile("configs/" + file.Name())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
config := newConfiguration()
|
||||
for iLine, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
colonPos := strings.Index(line, ":")
|
||||
if colonPos < 0 {
|
||||
return nil, nil, fmt.Errorf("invalid configuration in %s:%d", file.Name(), iLine)
|
||||
}
|
||||
key := strings.TrimSpace(line[:colonPos])
|
||||
value := strings.TrimSpace(line[colonPos+1:])
|
||||
config.data[key] = value
|
||||
}
|
||||
name_no_ext := file.Name()[:len(file.Name())-len(configSuffix)]
|
||||
models.preconfiguredConfigs[name_no_ext] = config
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig, err := models.get(defaultConfiguration)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defaultConfig.set(confModel, defaultConfiguration)
|
||||
|
||||
return models, defaultConfig, nil
|
||||
}
|
||||
|
||||
func mergeConfigs(base *configuration, addition *configuration) *configuration {
|
||||
result := newConfiguration()
|
||||
for k, v := range base.data {
|
||||
result.set(k, v)
|
||||
}
|
||||
for k, v := range addition.data {
|
||||
result.set(k, v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *configurationModels) get(name string) (*configuration, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
config, ok := c.preconfiguredConfigs[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("configuration %s.cfg not found", name)
|
||||
}
|
||||
|
||||
parentName, hasParent := config.getHas(confParent)
|
||||
if !hasParent {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
parent, err := c.get(parentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := mergeConfigs(parent, config)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *configurationModels) availableModels() []string {
|
||||
models := make([]string, 0, len(c.preconfiguredConfigs)-1)
|
||||
for name := range c.preconfiguredConfigs {
|
||||
if !strings.HasPrefix(name, "_") {
|
||||
models = append(models, name)
|
||||
}
|
||||
}
|
||||
slices.Sort(models)
|
||||
return models
|
||||
}
|
||||
|
||||
func (c *configurationModels) getWithOverrides(model string, overrides *configuration) (*configuration, error) {
|
||||
configValues, err := c.get(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if overrides != nil {
|
||||
configValues = mergeConfigs(configValues, overrides)
|
||||
}
|
||||
return configValues, nil
|
||||
}
|
||||
|
||||
func setupFlags(models *configurationModels, configuration *configuration) error {
|
||||
paramDescription := map[string]string{
|
||||
confModel: "set base model",
|
||||
confRom: "main rom file",
|
||||
confCharRom: "rom file for the character generator",
|
||||
confCpu: "cpu type, can be '6502' or '65c02'",
|
||||
confSpeed: "cpu speed in Mhz, can be 'ntsc', 'pal', 'full' or a decimal nunmber",
|
||||
confMods: "comma separated list of mods applied to the board, available mods are 'shift', 'four-colors",
|
||||
confRamworks: "memory to use with RAMWorks card, max is 16384",
|
||||
confNsc: "add a DS1216 No-Slot-Clock on the main ROM (use 'main') or a slot ROM",
|
||||
confTrace: "trace CPU execution with one or more comma separated tracers",
|
||||
confProfile: "generate profile trace to analyse with pprof",
|
||||
confForceCaps: "force all letters to be uppercased (no need for caps lock!)",
|
||||
confRgb: "emulate the RGB modes of the 80col RGB card for DHGR",
|
||||
confRomx: "emulate a RomX",
|
||||
confS0: "slot 0 configuration.",
|
||||
confS1: "slot 1 configuration.",
|
||||
confS2: "slot 2 configuration.",
|
||||
confS3: "slot 3 configuration.",
|
||||
confS4: "slot 4 configuration.",
|
||||
confS5: "slot 5 configuration.",
|
||||
confS6: "slot 6 configuration.",
|
||||
confS7: "slot 7 configuration.",
|
||||
}
|
||||
|
||||
boolParams := []string{confProfile, confForceCaps, confRgb, confRomx}
|
||||
|
||||
for name, description := range paramDescription {
|
||||
defaultValue, ok := configuration.getHas(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("default value not found for %s", name)
|
||||
}
|
||||
if slices.Contains(boolParams, name) {
|
||||
flag.Bool(name, defaultValue == "true", description)
|
||||
} else {
|
||||
flag.String(name, defaultValue, description)
|
||||
}
|
||||
}
|
||||
|
||||
flag.Usage = func() {
|
||||
out := flag.CommandLine.Output()
|
||||
fmt.Fprintf(out, "Usage: %s [file]\n", flag.CommandLine.Name())
|
||||
fmt.Fprintf(out, " file\n")
|
||||
fmt.Fprintf(out, " path to image to use on the boot device\n")
|
||||
flag.PrintDefaults()
|
||||
|
||||
fmt.Fprintf(out, "\nThe available pre-configured models are:\n")
|
||||
for _, model := range models.availableModels() {
|
||||
config, _ := models.get(model)
|
||||
fmt.Fprintf(out, " %s: %s\n", model, config.get(confName))
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "\nThe available cards are:\n")
|
||||
for _, card := range availableCards() {
|
||||
builder := getCardFactory()[card]
|
||||
fmt.Fprintf(out, " %s: %s\n", card, builder.description)
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "\nThe available tracers are:\n")
|
||||
for _, tracer := range availableTracers() {
|
||||
builder := getTracerFactory()[tracer]
|
||||
fmt.Fprintf(out, " %s: %s\n", tracer, builder.description)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigurationFromCommandLine() (*configuration, string, error) {
|
||||
models, configuration, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
setupFlags(models, configuration)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
modelFlag := flag.Lookup(confModel)
|
||||
if modelFlag != nil && strings.TrimSpace(modelFlag.Value.String()) != defaultConfiguration {
|
||||
// Replace the model
|
||||
configuration, err = models.get(modelFlag.Value.String())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
configuration.set(f.Name, f.Value.String())
|
||||
})
|
||||
|
||||
filename := flag.Arg(0)
|
||||
|
||||
return configuration, filename, nil
|
||||
}
|
76
configuration_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigurationModel(t *testing.T) {
|
||||
|
||||
t.Run("test that the default model exists", func(t *testing.T) {
|
||||
_, _, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test preconfigured models are complete", func(t *testing.T) {
|
||||
models, _, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
requiredFields := []string{
|
||||
confRom, confCharRom, confCpu, confSpeed, confRamworks, confNsc,
|
||||
confTrace, confProfile, confForceCaps, confRgb, confRomx,
|
||||
confS0, confS1, confS2, confS3, confS4, confS5, confS6, confS7,
|
||||
}
|
||||
availabledModels := models.availableModels()
|
||||
for _, modelName := range availabledModels {
|
||||
model, err := models.get(modelName)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for _, field := range requiredFields {
|
||||
if _, ok := model.getHas(field); !ok {
|
||||
t.Errorf("missing field '%s' in the preconfigured model '%s'", field, modelName)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommandLineHelp(t *testing.T) {
|
||||
t.Run("test command line help", func(t *testing.T) {
|
||||
models, configuration, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
prevFlags := flag.CommandLine
|
||||
flag.CommandLine = flag.NewFlagSet("izapple2", flag.ExitOnError)
|
||||
|
||||
setupFlags(models, configuration)
|
||||
|
||||
buffer := strings.Builder{}
|
||||
flag.CommandLine.SetOutput(&buffer)
|
||||
flag.Usage()
|
||||
usage := buffer.String()
|
||||
|
||||
flag.CommandLine = prevFlags
|
||||
|
||||
prevous, err := os.ReadFile("doc/usage.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if usage != string(prevous) {
|
||||
os.WriteFile("doc/usage_new.txt", []byte(usage), 0644)
|
||||
t.Errorf(`Usage has changed, check doc/usage_new.txt for the new version.
|
||||
If it is correct, execute \"go run update_readme.go\" in the doc folder.`)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
modeImplicit = iota + 1
|
||||
modeImplicitX
|
||||
modeImplicitY
|
||||
modeAccumulator
|
||||
modeImmediate
|
||||
modeZeroPage
|
||||
modeZeroPageX
|
||||
modeZeroPageY
|
||||
modeRelative
|
||||
modeAbsolute
|
||||
modeAbsoluteX
|
||||
modeAbsoluteY
|
||||
modeIndirect
|
||||
modeIndexedIndirectX
|
||||
modeIndirectIndexedY
|
||||
// Added on the 65c02
|
||||
modeIndirectZeroPage
|
||||
modeAbsoluteIndexedIndirectX
|
||||
modeZeroPageAndRelative
|
||||
)
|
||||
|
||||
func getWordInLine(line []uint8) uint16 {
|
||||
return uint16(line[1]) + 0x100*uint16(line[2])
|
||||
}
|
||||
|
||||
func resolveValue(s *State, line []uint8, opcode opcode) uint8 {
|
||||
getValue, _, _ := resolve(s, line, opcode)
|
||||
return getValue()
|
||||
}
|
||||
|
||||
func resolveGetSetValue(s *State, line []uint8, opcode opcode) (value uint8, setValue func(uint8)) {
|
||||
getValue, setValue, _ := resolve(s, line, opcode)
|
||||
value = getValue()
|
||||
return
|
||||
}
|
||||
|
||||
func resolveSetValue(s *State, line []uint8, opcode opcode) func(uint8) {
|
||||
_, setValue, _ := resolve(s, line, opcode)
|
||||
return setValue
|
||||
}
|
||||
|
||||
func resolveAddress(s *State, line []uint8, opcode opcode) uint16 {
|
||||
_, _, address := resolve(s, line, opcode)
|
||||
return address
|
||||
}
|
||||
|
||||
func resolve(s *State, line []uint8, opcode opcode) (getValue func() uint8, setValue func(uint8), address uint16) {
|
||||
hasAddress := true
|
||||
register := regNone
|
||||
|
||||
switch opcode.addressMode {
|
||||
case modeAccumulator:
|
||||
getValue = func() uint8 { return s.reg.getA() }
|
||||
hasAddress = false
|
||||
register = regA
|
||||
case modeImplicitX:
|
||||
getValue = func() uint8 { return s.reg.getX() }
|
||||
hasAddress = false
|
||||
register = regX
|
||||
case modeImplicitY:
|
||||
getValue = func() uint8 { return s.reg.getY() }
|
||||
hasAddress = false
|
||||
register = regY
|
||||
case modeImmediate:
|
||||
getValue = func() uint8 { return line[1] }
|
||||
hasAddress = false
|
||||
case modeZeroPage:
|
||||
address = uint16(line[1])
|
||||
case modeZeroPageX:
|
||||
address = uint16(line[1] + s.reg.getX())
|
||||
case modeZeroPageY:
|
||||
address = uint16(line[1] + s.reg.getY())
|
||||
case modeAbsolute:
|
||||
address = getWordInLine(line)
|
||||
case modeAbsoluteX:
|
||||
address = getWordInLine(line) + uint16(s.reg.getX())
|
||||
case modeAbsoluteY:
|
||||
address = getWordInLine(line) + uint16(s.reg.getY())
|
||||
case modeIndexedIndirectX:
|
||||
addressAddress := uint8(line[1] + s.reg.getX())
|
||||
address = getZeroPageWord(s.mem, addressAddress)
|
||||
case modeIndirect:
|
||||
addressAddress := getWordInLine(line)
|
||||
address = getWord(s.mem, addressAddress)
|
||||
case modeIndirectIndexedY:
|
||||
address = getZeroPageWord(s.mem, line[1]) +
|
||||
uint16(s.reg.getY())
|
||||
// 65c02 additions
|
||||
case modeIndirectZeroPage:
|
||||
address = getZeroPageWord(s.mem, line[1])
|
||||
case modeAbsoluteIndexedIndirectX:
|
||||
addressAddress := getWordInLine(line) + uint16(s.reg.getX())
|
||||
address = getWord(s.mem, addressAddress)
|
||||
case modeRelative:
|
||||
// This assumes that PC is already pointing to the next instruction
|
||||
address = s.reg.getPC() + uint16(int8(line[1])) // Note: line[1] is signed
|
||||
case modeZeroPageAndRelative:
|
||||
// Two addressing modes combined. We refer to the second one, relative,
|
||||
// placed one byte after the zeropage reference
|
||||
address = s.reg.getPC() + uint16(int8(line[2])) // Note: line[2] is signed
|
||||
|
||||
default:
|
||||
panic("Assert failed. Missing addressing mode")
|
||||
}
|
||||
|
||||
if hasAddress {
|
||||
getValue = func() uint8 { return s.mem.Peek(address) }
|
||||
}
|
||||
|
||||
setValue = func(value uint8) {
|
||||
if hasAddress {
|
||||
s.mem.Poke(address, value)
|
||||
} else if register != regNone {
|
||||
s.reg.setRegister(register, value)
|
||||
} else {
|
||||
panic("Assert failed. Should never happen")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lineString(line []uint8, opcode opcode) string {
|
||||
t := opcode.name
|
||||
switch opcode.addressMode {
|
||||
case modeImplicit:
|
||||
case modeImplicitX:
|
||||
case modeImplicitY:
|
||||
//Nothing
|
||||
case modeAccumulator:
|
||||
t += fmt.Sprintf(" A")
|
||||
case modeImmediate:
|
||||
t += fmt.Sprintf(" #%02x", line[1])
|
||||
case modeZeroPage:
|
||||
t += fmt.Sprintf(" $%02x", line[1])
|
||||
case modeZeroPageX:
|
||||
t += fmt.Sprintf(" $%02x,X", line[1])
|
||||
case modeZeroPageY:
|
||||
t += fmt.Sprintf(" $%02x,Y", line[1])
|
||||
case modeRelative:
|
||||
t += fmt.Sprintf(" *%+x", int8(line[1]))
|
||||
case modeAbsolute:
|
||||
t += fmt.Sprintf(" $%04x", getWordInLine(line))
|
||||
case modeAbsoluteX:
|
||||
t += fmt.Sprintf(" $%04x,X", getWordInLine(line))
|
||||
case modeAbsoluteY:
|
||||
t += fmt.Sprintf(" $%04x,Y", getWordInLine(line))
|
||||
case modeIndirect:
|
||||
t += fmt.Sprintf(" ($%04x)", getWordInLine(line))
|
||||
case modeIndexedIndirectX:
|
||||
t += fmt.Sprintf(" ($%02x,X)", line[1])
|
||||
case modeIndirectIndexedY:
|
||||
t += fmt.Sprintf(" ($%02x),Y", line[1])
|
||||
// 65c02 additions:
|
||||
case modeIndirectZeroPage:
|
||||
t += fmt.Sprintf(" ($%02x)", line[1])
|
||||
case modeAbsoluteIndexedIndirectX:
|
||||
t += fmt.Sprintf(" ($%04x,X)", getWordInLine(line))
|
||||
case modeZeroPageAndRelative:
|
||||
t += fmt.Sprintf(" $%02x *%+x", line[1], int8(line[2]))
|
||||
default:
|
||||
t += "UNKNOWN MODE"
|
||||
}
|
||||
return t
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
package core6502
|
||||
|
||||
/*
|
||||
For the diffrences with NMOS6502 see:
|
||||
http://6502.org/tutorials/65c02opcodes.html
|
||||
http://wilsonminesco.com/NMOS-CMOSdif/
|
||||
http://www.obelisk.me.uk/65C02/reference.html
|
||||
http://www.obelisk.me.uk/65C02/addressing.html
|
||||
http://anyplatform.net/media/guides/cpus/65xx%20Processor%20Data.txt
|
||||
*/
|
||||
|
||||
// NewCMOS65c02 returns an initialized 65c02
|
||||
func NewCMOS65c02(m Memory) *State {
|
||||
var s State
|
||||
s.mem = m
|
||||
|
||||
var opcodes [256]opcode
|
||||
for i := 0; i < 256; i++ {
|
||||
opcodes[i] = opcodesNMOS6502[i]
|
||||
if opcodes65c02Delta[i].cycles != 0 {
|
||||
opcodes[i] = opcodes65c02Delta[i]
|
||||
}
|
||||
}
|
||||
add65c02NOPs(&opcodes)
|
||||
s.opcodes = &opcodes
|
||||
return &s
|
||||
}
|
||||
|
||||
func add65c02NOPs(opcodes *[256]opcode) {
|
||||
nop11 := opcode{"NOP", 1, 1, modeImplicit, opNOP}
|
||||
nop22 := opcode{"NOP", 2, 2, modeImmediate, opNOP}
|
||||
nop23 := opcode{"NOP", 2, 3, modeImmediate, opNOP}
|
||||
nop24 := opcode{"NOP", 2, 4, modeImmediate, opNOP}
|
||||
nop34 := opcode{"NOP", 3, 4, modeAbsolute, opNOP}
|
||||
nop38 := opcode{"NOP", 3, 8, modeAbsolute, opNOP}
|
||||
|
||||
opcodes[0x02] = nop22
|
||||
opcodes[0x22] = nop22
|
||||
opcodes[0x42] = nop22
|
||||
opcodes[0x62] = nop22
|
||||
opcodes[0x82] = nop22
|
||||
opcodes[0xc2] = nop22
|
||||
opcodes[0xe2] = nop22
|
||||
|
||||
opcodes[0x44] = nop23
|
||||
opcodes[0x54] = nop24
|
||||
opcodes[0xD4] = nop24
|
||||
opcodes[0xF4] = nop24
|
||||
|
||||
opcodes[0x5c] = nop38
|
||||
opcodes[0xdc] = nop34
|
||||
opcodes[0xfc] = nop34
|
||||
|
||||
for i := 0; i < 0x100; i = i + 0x10 {
|
||||
opcodes[i+0x03] = nop11
|
||||
// RMB and SMB; opcodes[i+0x07] = nop11
|
||||
opcodes[i+0x0b] = nop11
|
||||
// BBR and BBS: opcodes[i+0x0f] = nop11
|
||||
}
|
||||
}
|
||||
|
||||
var opcodes65c02Delta = [256]opcode{
|
||||
// Functional difference
|
||||
0x00: opcode{"BRK", 1, 7, modeImplicit, opBRKAlt},
|
||||
0x24: opcode{"BIT", 2, 3, modeZeroPage, opBIT},
|
||||
0x2C: opcode{"BIT", 3, 3, modeAbsolute, opBIT},
|
||||
|
||||
// Fixed BCD arithmetic flags
|
||||
0x69: opcode{"ADC", 2, 2, modeImmediate, opADCAlt},
|
||||
0x65: opcode{"ADC", 2, 3, modeZeroPage, opADCAlt},
|
||||
0x75: opcode{"ADC", 2, 4, modeZeroPageX, opADCAlt},
|
||||
0x6D: opcode{"ADC", 3, 4, modeAbsolute, opADCAlt},
|
||||
0x7D: opcode{"ADC", 3, 4, modeAbsoluteX, opADCAlt},
|
||||
0x79: opcode{"ADC", 3, 4, modeAbsoluteY, opADCAlt},
|
||||
0x61: opcode{"ADC", 2, 6, modeIndexedIndirectX, opADCAlt},
|
||||
0x71: opcode{"ADC", 2, 5, modeIndirectIndexedY, opADCAlt},
|
||||
0xE9: opcode{"SBC", 2, 2, modeImmediate, opSBCAlt},
|
||||
0xE5: opcode{"SBC", 2, 3, modeZeroPage, opSBCAlt},
|
||||
0xF5: opcode{"SBC", 2, 4, modeZeroPageX, opSBCAlt},
|
||||
0xED: opcode{"SBC", 3, 4, modeAbsolute, opSBCAlt},
|
||||
0xFD: opcode{"SBC", 3, 4, modeAbsoluteX, opSBCAlt},
|
||||
0xF9: opcode{"SBC", 3, 4, modeAbsoluteY, opSBCAlt},
|
||||
0xE1: opcode{"SBC", 2, 6, modeIndexedIndirectX, opSBCAlt},
|
||||
0xF1: opcode{"SBC", 2, 5, modeIndirectIndexedY, opSBCAlt},
|
||||
|
||||
// Different cycle count
|
||||
0x1e: opcode{"ASL", 3, 6, modeAbsoluteX, buildOpShift(true, false)},
|
||||
0x3e: opcode{"ROL", 3, 6, modeAbsoluteX, buildOpShift(true, true)},
|
||||
0x5e: opcode{"LSR", 3, 6, modeAbsoluteX, buildOpShift(false, false)},
|
||||
0x7e: opcode{"ROR", 3, 6, modeAbsoluteX, buildOpShift(false, true)},
|
||||
|
||||
// New indirect zero page addresssing mode
|
||||
0x12: opcode{"ORA", 2, 5, modeIndirectZeroPage, buildOpLogic(operationOr)},
|
||||
0x32: opcode{"AND", 2, 5, modeIndirectZeroPage, buildOpLogic(operationAnd)},
|
||||
0x52: opcode{"EOR", 2, 5, modeIndirectZeroPage, buildOpLogic(operationXor)},
|
||||
0x72: opcode{"ADC", 2, 5, modeIndirectZeroPage, opADCAlt},
|
||||
0x92: opcode{"STA", 2, 5, modeIndirectZeroPage, buildOpStore(regA)},
|
||||
0xb2: opcode{"LDA", 2, 5, modeIndirectZeroPage, buildOpLoad(regA)},
|
||||
0xd2: opcode{"CMP", 2, 5, modeIndirectZeroPage, buildOpCompare(regA)},
|
||||
0xf2: opcode{"SBC", 2, 5, modeIndirectZeroPage, opSBCAlt},
|
||||
|
||||
// New addressing options
|
||||
0x89: opcode{"BIT", 2, 2, modeImmediate, opBIT},
|
||||
0x34: opcode{"BIT", 2, 4, modeZeroPageX, opBIT},
|
||||
0x3c: opcode{"BIT", 3, 4, modeAbsoluteX, opBIT},
|
||||
0x1a: opcode{"INC", 1, 2, modeAccumulator, buildOpIncDec(true)},
|
||||
0x3a: opcode{"DEC", 1, 2, modeAccumulator, buildOpIncDec(false)},
|
||||
0x7c: opcode{"JMP", 3, 6, modeAbsoluteIndexedIndirectX, opJMP},
|
||||
|
||||
// Additional instructions: BRA, PHX, PHY, PLX, PLY, STZ, TRB, TSB
|
||||
0xda: opcode{"PHX", 1, 3, modeImplicit, buildOpPush(regX)},
|
||||
0x5a: opcode{"PHY", 1, 3, modeImplicit, buildOpPush(regY)},
|
||||
0xfa: opcode{"PLX", 1, 4, modeImplicit, buildOpPull(regX)},
|
||||
0x7a: opcode{"PLY", 1, 4, modeImplicit, buildOpPull(regY)},
|
||||
0x80: opcode{"BRA", 2, 4, modeRelative, opJMP},
|
||||
|
||||
0x64: opcode{"STZ", 2, 3, modeZeroPage, opSTZ},
|
||||
0x74: opcode{"STZ", 2, 4, modeZeroPageX, opSTZ},
|
||||
0x9c: opcode{"STZ", 3, 4, modeAbsolute, opSTZ},
|
||||
0x9e: opcode{"STZ", 3, 5, modeAbsoluteX, opSTZ},
|
||||
|
||||
0x14: opcode{"TRB", 2, 5, modeZeroPage, opTRB},
|
||||
0x1c: opcode{"TRB", 3, 6, modeAbsolute, opTRB},
|
||||
|
||||
0x04: opcode{"TSB", 2, 5, modeZeroPage, opTSB},
|
||||
0x0c: opcode{"TSB", 3, 6, modeAbsolute, opTSB},
|
||||
|
||||
// Additional in Rockwell 65c02 and WDC 65c02?
|
||||
// They have a double addressing mode: zeropage and relative.
|
||||
0x0f: opcode{"BBR0", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(0, false)},
|
||||
0x1f: opcode{"BBR1", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(1, false)},
|
||||
0x2f: opcode{"BBR2", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(2, false)},
|
||||
0x3f: opcode{"BBR3", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(3, false)},
|
||||
0x4f: opcode{"BBR4", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(4, false)},
|
||||
0x5f: opcode{"BBR5", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(5, false)},
|
||||
0x6f: opcode{"BBR6", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(6, false)},
|
||||
0x7f: opcode{"BBR7", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(7, false)},
|
||||
0x8f: opcode{"BBS0", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(0, true)},
|
||||
0x9f: opcode{"BBS1", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(1, true)},
|
||||
0xaf: opcode{"BBS2", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(2, true)},
|
||||
0xbf: opcode{"BBS3", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(3, true)},
|
||||
0xcf: opcode{"BBS4", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(4, true)},
|
||||
0xdf: opcode{"BBS5", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(5, true)},
|
||||
0xef: opcode{"BBS6", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(6, true)},
|
||||
0xff: opcode{"BBS7", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(7, true)},
|
||||
|
||||
0x07: opcode{"RMB0", 2, 5, modeZeroPage, buildOpSetBit(0, false)},
|
||||
0x17: opcode{"RMB1", 2, 5, modeZeroPage, buildOpSetBit(1, false)},
|
||||
0x27: opcode{"RMB2", 2, 5, modeZeroPage, buildOpSetBit(2, false)},
|
||||
0x37: opcode{"RMB3", 2, 5, modeZeroPage, buildOpSetBit(3, false)},
|
||||
0x47: opcode{"RMB4", 2, 5, modeZeroPage, buildOpSetBit(4, false)},
|
||||
0x57: opcode{"RMB5", 2, 5, modeZeroPage, buildOpSetBit(5, false)},
|
||||
0x67: opcode{"RMB6", 2, 5, modeZeroPage, buildOpSetBit(6, false)},
|
||||
0x77: opcode{"RMB7", 2, 5, modeZeroPage, buildOpSetBit(7, false)},
|
||||
0x87: opcode{"SMB0", 2, 5, modeZeroPage, buildOpSetBit(0, true)},
|
||||
0x97: opcode{"SMB1", 2, 5, modeZeroPage, buildOpSetBit(1, true)},
|
||||
0xa7: opcode{"SMB2", 2, 5, modeZeroPage, buildOpSetBit(2, true)},
|
||||
0xb7: opcode{"SMB3", 2, 5, modeZeroPage, buildOpSetBit(3, true)},
|
||||
0xc7: opcode{"SMB4", 2, 5, modeZeroPage, buildOpSetBit(4, true)},
|
||||
0xd7: opcode{"SMB5", 2, 5, modeZeroPage, buildOpSetBit(5, true)},
|
||||
0xe7: opcode{"SMB6", 2, 5, modeZeroPage, buildOpSetBit(6, true)},
|
||||
0xf7: opcode{"SMB7", 2, 5, modeZeroPage, buildOpSetBit(7, true)},
|
||||
|
||||
// Maybe additional Rockwell: STP, WAI
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCMOs65c02NoUndocumented(t *testing.T) {
|
||||
m := new(FlatMemory)
|
||||
s := NewCMOS65c02(m)
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
if s.opcodes[i].cycles == 0 {
|
||||
t.Errorf("Opcode missing for $%02x.", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCMOS65c02asNMOS(t *testing.T) {
|
||||
m := new(FlatMemory)
|
||||
s := NewCMOS65c02(m)
|
||||
|
||||
m.loadBinary("testdata/6502_functional_test.bin")
|
||||
executeSuite(t, s, 0x200, 240, false, 255)
|
||||
}
|
||||
|
||||
func TestCMOS65c02(t *testing.T) {
|
||||
m := new(FlatMemory)
|
||||
s := NewCMOS65c02(m)
|
||||
|
||||
m.loadBinary("testdata/65C02_extended_opcodes_test.bin")
|
||||
executeSuite(t, s, 0x202, 240, false, 255)
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// https://www.masswerk.at/6502/6502_instruction_set.html
|
||||
// http://www.emulator101.com/reference/6502-reference.html
|
||||
// https://www.csh.rit.edu/~moffitt/docs/6502.html#FLAGS
|
||||
// https://ia800509.us.archive.org/18/items/Programming_the_6502/Programming_the_6502.pdf
|
||||
|
||||
// State represents the state of the simulated device
|
||||
type State struct {
|
||||
reg registers
|
||||
mem Memory
|
||||
cycles uint64
|
||||
opcodes *[256]opcode
|
||||
trace bool
|
||||
}
|
||||
|
||||
const (
|
||||
vectorNMI uint16 = 0xfffa
|
||||
vectorReset uint16 = 0xfffc
|
||||
vectorBreak uint16 = 0xfffe
|
||||
)
|
||||
|
||||
type opcode struct {
|
||||
name string
|
||||
bytes uint16
|
||||
cycles int
|
||||
addressMode int
|
||||
action opFunc
|
||||
}
|
||||
|
||||
type opFunc func(s *State, line []uint8, opcode opcode)
|
||||
|
||||
func (s *State) executeLine(line []uint8) {
|
||||
opcode := s.opcodes[line[0]]
|
||||
if opcode.cycles == 0 {
|
||||
panic(fmt.Sprintf("Unknown opcode 0x%02x\n", line[0]))
|
||||
}
|
||||
opcode.action(s, line, opcode)
|
||||
}
|
||||
|
||||
// ExecuteInstruction transforms the state given after a single instruction is executed.
|
||||
func (s *State) ExecuteInstruction() {
|
||||
pc := s.reg.getPC()
|
||||
opcodeID := s.mem.Peek(pc)
|
||||
opcode := s.opcodes[opcodeID]
|
||||
|
||||
if opcode.cycles == 0 {
|
||||
panic(fmt.Sprintf("Unknown opcode 0x%02x\n", opcodeID))
|
||||
}
|
||||
|
||||
line := make([]uint8, opcode.bytes)
|
||||
for i := uint16(0); i < opcode.bytes; i++ {
|
||||
line[i] = s.mem.Peek(pc)
|
||||
pc++
|
||||
}
|
||||
s.reg.setPC(pc)
|
||||
|
||||
if s.trace {
|
||||
//fmt.Printf("%#04x %#02x\n", pc-opcode.bytes, opcodeID)
|
||||
fmt.Printf("%#04x %-13s: ", pc-opcode.bytes, lineString(line, opcode))
|
||||
}
|
||||
opcode.action(s, line, opcode)
|
||||
s.cycles += uint64(opcode.cycles)
|
||||
if s.trace {
|
||||
fmt.Printf("%v, [%02x]\n", s.reg, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the processor. Moves the program counter to the vector in 0cfffc.
|
||||
func (s *State) Reset() {
|
||||
startAddress := getWord(s.mem, vectorReset)
|
||||
s.cycles += 6
|
||||
s.reg.setPC(startAddress)
|
||||
}
|
||||
|
||||
// GetCycles returns the count of CPU cycles since last reset.
|
||||
func (s *State) GetCycles() uint64 {
|
||||
return s.cycles
|
||||
}
|
||||
|
||||
// SetTrace activates tracing of the cpu execution
|
||||
func (s *State) SetTrace(trace bool) {
|
||||
s.trace = trace
|
||||
}
|
||||
|
||||
// GetTrace gets trhe tracing state of the cpu execution
|
||||
func (s *State) GetTrace() bool {
|
||||
return s.trace
|
||||
}
|
||||
|
||||
// Save saves the CPU state (registers and cycle counter)
|
||||
func (s *State) Save(w io.Writer) error {
|
||||
err := binary.Write(w, binary.BigEndian, s.cycles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
binary.Write(w, binary.BigEndian, s.reg.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load loads the CPU state (registers and cycle counter)
|
||||
func (s *State) Load(r io.Reader) error {
|
||||
err := binary.Read(r, binary.BigEndian, &s.cycles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &s.reg.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,485 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.executeLine([]uint8{0xA9, 0x42})
|
||||
if s.reg.getA() != 0x42 {
|
||||
t.Error("Error in LDA #")
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0xA9, 0x00})
|
||||
if s.reg.getP() != flagZ {
|
||||
t.Error("Error in flags for LDA $0")
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0xA9, 0xF0})
|
||||
if s.reg.getP() != flagN {
|
||||
t.Error("Error in flags for LDA $F0")
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0xA0, 0xFE})
|
||||
if s.reg.getY() != 0xFE {
|
||||
t.Error("Error in LDY #")
|
||||
}
|
||||
|
||||
s.mem.Poke(0x38, 0x87)
|
||||
s.executeLine([]uint8{0xA5, 0x38})
|
||||
if s.reg.getA() != 0x87 {
|
||||
t.Error("Error in LDA zpg")
|
||||
}
|
||||
|
||||
s.mem.Poke(0x57, 0x90)
|
||||
s.reg.setX(0x10)
|
||||
s.executeLine([]uint8{0xB5, 0x47})
|
||||
if s.reg.getA() != 0x90 {
|
||||
t.Error("Error in LDA zpg, X")
|
||||
}
|
||||
|
||||
s.mem.Poke(0x38, 0x12)
|
||||
s.reg.setX(0x89)
|
||||
s.executeLine([]uint8{0xB5, 0xAF})
|
||||
if s.reg.getA() != 0x12 {
|
||||
t.Error("Error in LDA zpgX with sero page overflow")
|
||||
}
|
||||
|
||||
s.mem.Poke(0x1234, 0x67)
|
||||
s.executeLine([]uint8{0xAD, 0x34, 0x12})
|
||||
if s.reg.getA() != 0x67 {
|
||||
t.Error("Error in LDA abs")
|
||||
}
|
||||
|
||||
s.mem.Poke(0xC057, 0x7E)
|
||||
s.reg.setX(0x57)
|
||||
s.executeLine([]uint8{0xBD, 0x00, 0xC0})
|
||||
if s.reg.getA() != 0x7E {
|
||||
t.Error("Error in LDA abs, X")
|
||||
}
|
||||
|
||||
s.mem.Poke(0xD059, 0x7A)
|
||||
s.reg.setY(0x59)
|
||||
s.executeLine([]uint8{0xB9, 0x00, 0xD0})
|
||||
if s.reg.getA() != 0x7A {
|
||||
t.Error("Error in LDA abs, Y")
|
||||
}
|
||||
|
||||
s.mem.Poke(0x24, 0x74)
|
||||
s.mem.Poke(0x25, 0x20)
|
||||
s.reg.setX(0x04)
|
||||
s.mem.Poke(0x2074, 0x66)
|
||||
s.executeLine([]uint8{0xA1, 0x20})
|
||||
if s.reg.getA() != 0x66 {
|
||||
t.Error("Error in LDA (oper,X)")
|
||||
}
|
||||
|
||||
s.mem.Poke(0x86, 0x28)
|
||||
s.mem.Poke(0x87, 0x40)
|
||||
s.reg.setY(0x10)
|
||||
s.mem.Poke(0x4038, 0x99)
|
||||
s.executeLine([]uint8{0xB1, 0x86})
|
||||
if s.reg.getA() != 0x99 {
|
||||
t.Error("Error in LDA (oper),Y")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
s.reg.setA(0x10)
|
||||
s.reg.setX(0x40)
|
||||
s.reg.setY(0x80)
|
||||
|
||||
s.executeLine([]uint8{0x85, 0x50})
|
||||
if s.mem.Peek(0x0050) != 0x10 {
|
||||
t.Error("Error in STA zpg")
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x86, 0x51})
|
||||
if s.mem.Peek(0x0051) != 0x40 {
|
||||
t.Error("Error in STX zpg")
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x84, 0x52})
|
||||
if s.mem.Peek(0x0052) != 0x80 {
|
||||
t.Error("Error in STY zpg")
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x8D, 0x20, 0xC0})
|
||||
if s.mem.Peek(0xC020) != 0x10 {
|
||||
t.Error("Error in STA abs")
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x9D, 0x08, 0x10})
|
||||
if s.mem.Peek(0x1048) != 0x10 {
|
||||
t.Error("Error in STA abs, X")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransfer(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setA(0xB0)
|
||||
s.executeLine([]uint8{0xAA})
|
||||
if s.reg.getX() != 0xB0 {
|
||||
t.Error("Error in TAX")
|
||||
}
|
||||
if s.reg.getP() != flagN {
|
||||
t.Error("Error in TAX flags")
|
||||
}
|
||||
|
||||
s.reg.setA(0xB1)
|
||||
s.executeLine([]uint8{0xA8})
|
||||
if s.reg.getY() != 0xB1 {
|
||||
t.Error("Error in TAY")
|
||||
}
|
||||
|
||||
s.reg.setSP(0xB2)
|
||||
s.executeLine([]uint8{0xBA})
|
||||
if s.reg.getX() != 0xB2 {
|
||||
t.Error("Error in TSX")
|
||||
}
|
||||
|
||||
s.reg.setX(0xB3)
|
||||
s.executeLine([]uint8{0x8A})
|
||||
if s.reg.getA() != 0xB3 {
|
||||
t.Error("Error in TXA")
|
||||
}
|
||||
|
||||
s.reg.setX(0xB4)
|
||||
s.executeLine([]uint8{0x9A})
|
||||
if s.reg.getSP() != 0xB4 {
|
||||
t.Error("Error in TXS")
|
||||
}
|
||||
|
||||
s.reg.setY(0xB5)
|
||||
s.executeLine([]uint8{0x98})
|
||||
if s.reg.getA() != 0xB5 {
|
||||
t.Error("Error in TYA")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncDec(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setX(0x7E)
|
||||
s.executeLine([]uint8{0xE8})
|
||||
if s.reg.getX() != 0x7F {
|
||||
t.Errorf("Error in INX")
|
||||
}
|
||||
|
||||
s.reg.setY(0xFC)
|
||||
s.executeLine([]uint8{0x88})
|
||||
if s.reg.getY() != 0xFB {
|
||||
t.Error("Error in DEY")
|
||||
}
|
||||
if s.reg.getP() != flagN {
|
||||
t.Error("Error in DEY flags")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftRotate(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setA(0xF0)
|
||||
s.executeLine([]uint8{0x2A})
|
||||
if s.reg.getA() != 0xE0 {
|
||||
t.Errorf("Error in ROL")
|
||||
}
|
||||
if !s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in ROL carry. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setFlag(flagC)
|
||||
s.reg.setA(0x0F)
|
||||
s.executeLine([]uint8{0x6A})
|
||||
if s.reg.getA() != 0x87 {
|
||||
t.Errorf("Error in ROR. %v", s.reg)
|
||||
}
|
||||
if !s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in ROR carry")
|
||||
}
|
||||
|
||||
s.reg.setFlag(flagC)
|
||||
s.reg.setA(0x81)
|
||||
s.executeLine([]uint8{0x0A})
|
||||
if s.reg.getA() != 0x02 {
|
||||
t.Errorf("Error in ASL. %v", s.reg)
|
||||
}
|
||||
if !s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in ASL carry")
|
||||
}
|
||||
|
||||
s.reg.setFlag(flagC)
|
||||
s.reg.setA(0x02)
|
||||
s.executeLine([]uint8{0x4A})
|
||||
if s.reg.getA() != 0x01 {
|
||||
t.Errorf("Error in LSR. %v", s.reg)
|
||||
}
|
||||
if s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in LSR carry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearSetFlag(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
s.reg.setP(0x00)
|
||||
|
||||
s.executeLine([]uint8{0xF8})
|
||||
if !s.reg.getFlag(flagD) {
|
||||
t.Errorf("Error in SED. %v", s.reg)
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0xD8})
|
||||
if s.reg.getFlag(flagD) {
|
||||
t.Errorf("Error in CLD. %v", s.reg)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLogic(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setA(0xF0)
|
||||
s.executeLine([]uint8{0x29, 0x1C})
|
||||
if s.reg.getA() != 0x10 {
|
||||
t.Errorf("Error in AND <. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0xF0)
|
||||
s.executeLine([]uint8{0x49, 0x1C})
|
||||
if s.reg.getA() != 0xEC {
|
||||
t.Errorf("Error in EOR <. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0xF0)
|
||||
s.executeLine([]uint8{0x09, 0x0C})
|
||||
if s.reg.getA() != 0xFC {
|
||||
t.Errorf("Error in ORA <. %v", s.reg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setA(0xA0)
|
||||
s.reg.clearFlag(flagC)
|
||||
s.executeLine([]uint8{0x69, 0x0B})
|
||||
if s.reg.getA() != 0xAB {
|
||||
t.Errorf("Error in ADC. %v", s.reg)
|
||||
}
|
||||
if s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry ADC. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0xFF)
|
||||
s.reg.clearFlag(flagC)
|
||||
s.executeLine([]uint8{0x69, 0x02})
|
||||
if s.reg.getA() != 0x01 {
|
||||
t.Errorf("Error in ADC with carry. %v", s.reg)
|
||||
}
|
||||
if !s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry ADC with carry. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0xA0)
|
||||
s.reg.setFlag(flagC)
|
||||
s.executeLine([]uint8{0x69, 0x01})
|
||||
if s.reg.getA() != 0xA2 {
|
||||
t.Errorf("Error in carried ADC with carry. %v", s.reg)
|
||||
}
|
||||
if s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry in carried ADC with carry. %v", s.reg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddDecimal(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
s.reg.setFlag(flagD)
|
||||
|
||||
s.reg.setA(0x12)
|
||||
s.reg.clearFlag(flagC)
|
||||
s.executeLine([]uint8{0x69, 0x013})
|
||||
if s.reg.getA() != 0x25 {
|
||||
t.Errorf("Error in ADC decimal. %v", s.reg)
|
||||
}
|
||||
if s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry ADC. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0x44)
|
||||
s.reg.clearFlag(flagC)
|
||||
s.executeLine([]uint8{0x69, 0x68})
|
||||
if s.reg.getA() != 0x12 {
|
||||
t.Errorf("Error in ADC decimal with carry. %v", s.reg)
|
||||
}
|
||||
if !s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry ADC decimal with carry. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0x44)
|
||||
s.reg.setFlag(flagC)
|
||||
s.executeLine([]uint8{0x69, 0x23})
|
||||
if s.reg.getA() != 0x68 {
|
||||
t.Errorf("Error in carried ADC decimal with carry. %v", s.reg)
|
||||
}
|
||||
if s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry in carried ADC decimal with carry. %v", s.reg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setA(0x09)
|
||||
s.reg.clearFlag(flagC)
|
||||
s.executeLine([]uint8{0xE9, 0x05})
|
||||
if s.reg.getA() != 0x03 {
|
||||
t.Errorf("Error in SBC. %v", s.reg)
|
||||
}
|
||||
if !s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry SBC. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0x01)
|
||||
s.reg.clearFlag(flagC)
|
||||
s.executeLine([]uint8{0xE9, 0x02})
|
||||
if s.reg.getA() != 0xFE {
|
||||
t.Errorf("Error in SBC with carry. %v", s.reg)
|
||||
}
|
||||
if s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry SBC with carry. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0x08)
|
||||
s.reg.setFlag(flagC)
|
||||
s.executeLine([]uint8{0xE9, 0x02})
|
||||
if s.reg.getA() != 0x06 {
|
||||
t.Errorf("Error in carried SBC with carry. %v", s.reg)
|
||||
}
|
||||
if !s.reg.getFlag(flagC) {
|
||||
t.Errorf("Error in carry in carried SBC with carry. %v", s.reg)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setA(0x02)
|
||||
s.executeLine([]uint8{0xC9, 0x01})
|
||||
if s.reg.getP() != 0x01 {
|
||||
t.Errorf("Error in CMP <. %v", s.reg)
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0xC9, 0x02})
|
||||
if s.reg.getP() != 0x03 {
|
||||
t.Errorf("Error in CMP =. %v", s.reg)
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0xC9, 0x03})
|
||||
if s.reg.getP() != 0x80 {
|
||||
t.Errorf("Error in CMP >. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setX(0x04)
|
||||
s.executeLine([]uint8{0xE0, 0x05})
|
||||
if s.reg.getP() != 0x80 {
|
||||
t.Errorf("Error in CPX >. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setY(0x08)
|
||||
s.executeLine([]uint8{0xC0, 0x09})
|
||||
if s.reg.getP() != 0x80 {
|
||||
t.Errorf("Error in CPY >. %v", s.reg)
|
||||
}
|
||||
|
||||
}
|
||||
func TestBit(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setA(0x0F)
|
||||
s.mem.Poke(0x0040, 0xF0)
|
||||
s.executeLine([]uint8{0x24, 0x40})
|
||||
if s.reg.getP() != 0xC2 {
|
||||
t.Errorf("Error in BIT. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0xF0)
|
||||
s.mem.Poke(0x0040, 0xF0)
|
||||
s.executeLine([]uint8{0x24, 0x40})
|
||||
if s.reg.getP() != 0xC0 {
|
||||
t.Errorf("Error in BIT, 2. %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setA(0xF0)
|
||||
s.mem.Poke(0x01240, 0x80)
|
||||
s.executeLine([]uint8{0x2C, 0x40, 0x12})
|
||||
if s.reg.getP() != 0x80 {
|
||||
t.Errorf("Error in BIT, 2. %v", s.reg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranch(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setPC(0xC600)
|
||||
s.reg.setFlag(flagV)
|
||||
s.executeLine([]uint8{0x50, 0x20})
|
||||
if s.reg.getPC() != 0xC600 {
|
||||
t.Errorf("Error in BVC, %v", s.reg)
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x70, 0x20})
|
||||
if s.reg.getPC() != 0xC620 {
|
||||
t.Errorf("Error in BVS, %v", s.reg)
|
||||
}
|
||||
|
||||
s.reg.setPC(0xD600)
|
||||
s.reg.clearFlag(flagC)
|
||||
s.executeLine([]uint8{0x90, 0xA0})
|
||||
if s.reg.getPC() != 0xD5A0 {
|
||||
t.Errorf("Error in BCC, %v", s.reg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
s := NewNMOS6502(new(FlatMemory))
|
||||
|
||||
s.reg.setSP(0xF0)
|
||||
s.reg.setA(0xA0)
|
||||
s.reg.setP(0x0A)
|
||||
s.executeLine([]uint8{0x48})
|
||||
if s.reg.getSP() != 0xEF {
|
||||
t.Errorf("Error in PHA stack pointer, %v", s.reg)
|
||||
}
|
||||
if s.mem.Peek(0x01F0) != 0xA0 {
|
||||
t.Errorf("Error in PHA, %v", s.reg)
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x08})
|
||||
if s.reg.getSP() != 0xEE {
|
||||
t.Errorf("Error in PHP stack pointer, %v", s.reg)
|
||||
}
|
||||
if s.mem.Peek(0x01EF) != 0x3A {
|
||||
t.Errorf("Error in PHP, %v", s.reg)
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x68})
|
||||
if s.reg.getSP() != 0xEF {
|
||||
t.Errorf("Error in PLA stack pointer, %v", s.reg)
|
||||
}
|
||||
if s.reg.getA() != 0x3A {
|
||||
t.Errorf("Error in PLA, %v", s.reg)
|
||||
}
|
||||
|
||||
s.executeLine([]uint8{0x28})
|
||||
if s.reg.getSP() != 0xF0 {
|
||||
t.Errorf("Error in PLP stack pointer, %v", s.reg)
|
||||
}
|
||||
if s.reg.getP() != 0xA0 {
|
||||
t.Errorf("Error in PLP, %v", s.reg)
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import "io/ioutil"
|
||||
|
||||
// Memory represents the addressable space of the processor
|
||||
type Memory interface {
|
||||
Peek(address uint16) uint8
|
||||
Poke(address uint16, value uint8)
|
||||
}
|
||||
|
||||
func getWord(m Memory, address uint16) uint16 {
|
||||
return uint16(m.Peek(address)) + 0x100*uint16(m.Peek(address+1))
|
||||
}
|
||||
|
||||
func getZeroPageWord(m Memory, address uint8) uint16 {
|
||||
return uint16(m.Peek(uint16(address))) + 0x100*uint16(m.Peek(uint16(address+1)))
|
||||
}
|
||||
|
||||
// FlatMemory puts RAM on the 64Kb addressable by the processor
|
||||
type FlatMemory struct {
|
||||
data [65536]uint8
|
||||
}
|
||||
|
||||
// Peek returns the data on the given address
|
||||
func (m *FlatMemory) Peek(address uint16) uint8 {
|
||||
return m.data[address]
|
||||
}
|
||||
|
||||
// Poke sets the data at the given address
|
||||
func (m *FlatMemory) Poke(address uint16, value uint8) {
|
||||
m.data[address] = value
|
||||
}
|
||||
|
||||
func (m *FlatMemory) loadBinary(filename string) error {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, v := range bytes {
|
||||
m.Poke(uint16(i), uint8(v))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,189 +0,0 @@
|
|||
package core6502
|
||||
|
||||
// NewNMOS6502 returns an initialized NMOS6502
|
||||
func NewNMOS6502(m Memory) *State {
|
||||
var s State
|
||||
s.mem = m
|
||||
s.opcodes = &opcodesNMOS6502
|
||||
return &s
|
||||
}
|
||||
|
||||
var opcodesNMOS6502 = [256]opcode{
|
||||
0x00: opcode{"BRK", 1, 7, modeImplicit, opBRK},
|
||||
0x4C: opcode{"JMP", 3, 3, modeAbsolute, opJMP},
|
||||
0x6C: opcode{"JMP", 3, 3, modeIndirect, opJMP},
|
||||
0x20: opcode{"JSR", 3, 6, modeAbsolute, opJSR},
|
||||
0x40: opcode{"RTI", 1, 6, modeImplicit, opRTI},
|
||||
0x60: opcode{"RTS", 1, 6, modeImplicit, opRTS},
|
||||
|
||||
0x48: opcode{"PHA", 1, 3, modeImplicit, buildOpPush(regA)},
|
||||
0x08: opcode{"PHP", 1, 3, modeImplicit, buildOpPush(regP)},
|
||||
0x68: opcode{"PLA", 1, 4, modeImplicit, buildOpPull(regA)},
|
||||
0x28: opcode{"PLP", 1, 4, modeImplicit, buildOpPull(regP)},
|
||||
|
||||
0x09: opcode{"ORA", 2, 2, modeImmediate, buildOpLogic(operationOr)},
|
||||
0x05: opcode{"ORA", 2, 3, modeZeroPage, buildOpLogic(operationOr)},
|
||||
0x15: opcode{"ORA", 2, 4, modeZeroPageX, buildOpLogic(operationOr)},
|
||||
0x0D: opcode{"ORA", 3, 4, modeAbsolute, buildOpLogic(operationOr)},
|
||||
0x1D: opcode{"ORA", 3, 4, modeAbsoluteX, buildOpLogic(operationOr)}, // Extra cycles
|
||||
0x19: opcode{"ORA", 3, 4, modeAbsoluteY, buildOpLogic(operationOr)}, // Extra cycles
|
||||
0x01: opcode{"ORA", 2, 6, modeIndexedIndirectX, buildOpLogic(operationOr)},
|
||||
0x11: opcode{"ORA", 2, 5, modeIndirectIndexedY, buildOpLogic(operationOr)}, // Extra cycles
|
||||
|
||||
0x29: opcode{"AND", 2, 2, modeImmediate, buildOpLogic(operationAnd)},
|
||||
0x25: opcode{"AND", 2, 3, modeZeroPage, buildOpLogic(operationAnd)},
|
||||
0x35: opcode{"AND", 2, 4, modeZeroPageX, buildOpLogic(operationAnd)},
|
||||
0x2D: opcode{"AND", 3, 4, modeAbsolute, buildOpLogic(operationAnd)},
|
||||
0x3D: opcode{"AND", 3, 4, modeAbsoluteX, buildOpLogic(operationAnd)}, // Extra cycles
|
||||
0x39: opcode{"AND", 3, 4, modeAbsoluteY, buildOpLogic(operationAnd)}, // Extra cycles
|
||||
0x21: opcode{"AND", 2, 6, modeIndexedIndirectX, buildOpLogic(operationAnd)},
|
||||
0x31: opcode{"AND", 2, 5, modeIndirectIndexedY, buildOpLogic(operationAnd)}, // Extra cycles
|
||||
|
||||
0x49: opcode{"EOR", 2, 2, modeImmediate, buildOpLogic(operationXor)},
|
||||
0x45: opcode{"EOR", 2, 3, modeZeroPage, buildOpLogic(operationXor)},
|
||||
0x55: opcode{"EOR", 2, 4, modeZeroPageX, buildOpLogic(operationXor)},
|
||||
0x4D: opcode{"EOR", 3, 4, modeAbsolute, buildOpLogic(operationXor)},
|
||||
0x5D: opcode{"EOR", 3, 4, modeAbsoluteX, buildOpLogic(operationXor)}, // Extra cycles
|
||||
0x59: opcode{"EOR", 3, 4, modeAbsoluteY, buildOpLogic(operationXor)}, // Extra cycles
|
||||
0x41: opcode{"EOR", 2, 6, modeIndexedIndirectX, buildOpLogic(operationXor)},
|
||||
0x51: opcode{"EOR", 2, 5, modeIndirectIndexedY, buildOpLogic(operationXor)}, // Extra cycles
|
||||
|
||||
0x69: opcode{"ADC", 2, 2, modeImmediate, opADC},
|
||||
0x65: opcode{"ADC", 2, 3, modeZeroPage, opADC},
|
||||
0x75: opcode{"ADC", 2, 4, modeZeroPageX, opADC},
|
||||
0x6D: opcode{"ADC", 3, 4, modeAbsolute, opADC},
|
||||
0x7D: opcode{"ADC", 3, 4, modeAbsoluteX, opADC}, // Extra cycles
|
||||
0x79: opcode{"ADC", 3, 4, modeAbsoluteY, opADC}, // Extra cycles
|
||||
0x61: opcode{"ADC", 2, 6, modeIndexedIndirectX, opADC},
|
||||
0x71: opcode{"ADC", 2, 5, modeIndirectIndexedY, opADC}, // Extra cycles
|
||||
|
||||
0xE9: opcode{"SBC", 2, 2, modeImmediate, opSBC},
|
||||
0xE5: opcode{"SBC", 2, 3, modeZeroPage, opSBC},
|
||||
0xF5: opcode{"SBC", 2, 4, modeZeroPageX, opSBC},
|
||||
0xED: opcode{"SBC", 3, 4, modeAbsolute, opSBC},
|
||||
0xFD: opcode{"SBC", 3, 4, modeAbsoluteX, opSBC}, // Extra cycles
|
||||
0xF9: opcode{"SBC", 3, 4, modeAbsoluteY, opSBC}, // Extra cycles
|
||||
0xE1: opcode{"SBC", 2, 6, modeIndexedIndirectX, opSBC},
|
||||
0xF1: opcode{"SBC", 2, 5, modeIndirectIndexedY, opSBC}, // Extra cycles
|
||||
|
||||
0x24: opcode{"BIT", 2, 3, modeZeroPage, opBIT},
|
||||
0x2C: opcode{"BIT", 3, 3, modeAbsolute, opBIT},
|
||||
|
||||
0xC9: opcode{"CMP", 2, 2, modeImmediate, buildOpCompare(regA)},
|
||||
0xC5: opcode{"CMP", 2, 3, modeZeroPage, buildOpCompare(regA)},
|
||||
0xD5: opcode{"CMP", 2, 4, modeZeroPageX, buildOpCompare(regA)},
|
||||
0xCD: opcode{"CMP", 3, 4, modeAbsolute, buildOpCompare(regA)},
|
||||
0xDD: opcode{"CMP", 3, 4, modeAbsoluteX, buildOpCompare(regA)}, // Extra cycles
|
||||
0xD9: opcode{"CMP", 3, 4, modeAbsoluteY, buildOpCompare(regA)}, // Extra cycles
|
||||
0xC1: opcode{"CMP", 2, 6, modeIndexedIndirectX, buildOpCompare(regA)},
|
||||
0xD1: opcode{"CMP", 2, 5, modeIndirectIndexedY, buildOpCompare(regA)}, // Extra cycles
|
||||
|
||||
0xE0: opcode{"CPX", 2, 2, modeImmediate, buildOpCompare(regX)},
|
||||
0xE4: opcode{"CPX", 2, 3, modeZeroPage, buildOpCompare(regX)},
|
||||
0xEC: opcode{"CPX", 3, 4, modeAbsolute, buildOpCompare(regX)},
|
||||
|
||||
0xC0: opcode{"CPY", 2, 2, modeImmediate, buildOpCompare(regY)},
|
||||
0xC4: opcode{"CPY", 2, 3, modeZeroPage, buildOpCompare(regY)},
|
||||
0xCC: opcode{"CPY", 3, 4, modeAbsolute, buildOpCompare(regY)},
|
||||
|
||||
0x2A: opcode{"ROL", 1, 2, modeAccumulator, buildOpShift(true, true)},
|
||||
0x26: opcode{"ROL", 2, 5, modeZeroPage, buildOpShift(true, true)},
|
||||
0x36: opcode{"ROL", 2, 6, modeZeroPageX, buildOpShift(true, true)},
|
||||
0x2E: opcode{"ROL", 3, 6, modeAbsolute, buildOpShift(true, true)},
|
||||
0x3E: opcode{"ROL", 3, 7, modeAbsoluteX, buildOpShift(true, true)},
|
||||
|
||||
0x6A: opcode{"ROR", 1, 2, modeAccumulator, buildOpShift(false, true)},
|
||||
0x66: opcode{"ROR", 2, 5, modeZeroPage, buildOpShift(false, true)},
|
||||
0x76: opcode{"ROR", 2, 6, modeZeroPageX, buildOpShift(false, true)},
|
||||
0x6E: opcode{"ROR", 3, 6, modeAbsolute, buildOpShift(false, true)},
|
||||
0x7E: opcode{"ROR", 3, 7, modeAbsoluteX, buildOpShift(false, true)},
|
||||
|
||||
0x0A: opcode{"ASL", 1, 2, modeAccumulator, buildOpShift(true, false)},
|
||||
0x06: opcode{"ASL", 2, 5, modeZeroPage, buildOpShift(true, false)},
|
||||
0x16: opcode{"ASL", 2, 6, modeZeroPageX, buildOpShift(true, false)},
|
||||
0x0E: opcode{"ASL", 3, 6, modeAbsolute, buildOpShift(true, false)},
|
||||
0x1E: opcode{"ASL", 3, 7, modeAbsoluteX, buildOpShift(true, false)},
|
||||
|
||||
0x4A: opcode{"LSR", 1, 2, modeAccumulator, buildOpShift(false, false)},
|
||||
0x46: opcode{"LSR", 2, 5, modeZeroPage, buildOpShift(false, false)},
|
||||
0x56: opcode{"LSR", 2, 6, modeZeroPageX, buildOpShift(false, false)},
|
||||
0x4E: opcode{"LSR", 3, 6, modeAbsolute, buildOpShift(false, false)},
|
||||
0x5E: opcode{"LSR", 3, 7, modeAbsoluteX, buildOpShift(false, false)},
|
||||
|
||||
0x38: opcode{"SEC", 1, 2, modeImplicit, buildOpUpdateFlag(flagC, true)},
|
||||
0xF8: opcode{"SED", 1, 2, modeImplicit, buildOpUpdateFlag(flagD, true)},
|
||||
0x78: opcode{"SEI", 1, 2, modeImplicit, buildOpUpdateFlag(flagI, true)},
|
||||
0x18: opcode{"CLC", 1, 2, modeImplicit, buildOpUpdateFlag(flagC, false)},
|
||||
0xD8: opcode{"CLD", 1, 2, modeImplicit, buildOpUpdateFlag(flagD, false)},
|
||||
0x58: opcode{"CLI", 1, 2, modeImplicit, buildOpUpdateFlag(flagI, false)},
|
||||
0xB8: opcode{"CLV", 1, 2, modeImplicit, buildOpUpdateFlag(flagV, false)},
|
||||
|
||||
0xE6: opcode{"INC", 2, 5, modeZeroPage, buildOpIncDec(true)},
|
||||
0xF6: opcode{"INC", 2, 6, modeZeroPageX, buildOpIncDec(true)},
|
||||
0xEE: opcode{"INC", 3, 6, modeAbsolute, buildOpIncDec(true)},
|
||||
0xFE: opcode{"INC", 3, 7, modeAbsoluteX, buildOpIncDec(true)},
|
||||
0xC6: opcode{"DEC", 2, 5, modeZeroPage, buildOpIncDec(false)},
|
||||
0xD6: opcode{"DEC", 2, 6, modeZeroPageX, buildOpIncDec(false)},
|
||||
0xCE: opcode{"DEC", 3, 6, modeAbsolute, buildOpIncDec(false)},
|
||||
0xDE: opcode{"DEC", 3, 7, modeAbsoluteX, buildOpIncDec(false)},
|
||||
0xE8: opcode{"INX", 1, 2, modeImplicitX, buildOpIncDec(true)},
|
||||
0xC8: opcode{"INY", 1, 2, modeImplicitY, buildOpIncDec(true)},
|
||||
0xCA: opcode{"DEX", 1, 2, modeImplicitX, buildOpIncDec(false)},
|
||||
0x88: opcode{"DEY", 1, 2, modeImplicitY, buildOpIncDec(false)},
|
||||
|
||||
0xAA: opcode{"TAX", 1, 2, modeImplicit, buildOpTransfer(regA, regX)},
|
||||
0xA8: opcode{"TAY", 1, 2, modeImplicit, buildOpTransfer(regA, regY)},
|
||||
0x8A: opcode{"TXA", 1, 2, modeImplicit, buildOpTransfer(regX, regA)},
|
||||
0x98: opcode{"TYA", 1, 2, modeImplicit, buildOpTransfer(regY, regA)},
|
||||
0x9A: opcode{"TXS", 1, 2, modeImplicit, buildOpTransfer(regX, regSP)},
|
||||
0xBA: opcode{"TSX", 1, 2, modeImplicit, buildOpTransfer(regSP, regX)},
|
||||
|
||||
0xA9: opcode{"LDA", 2, 2, modeImmediate, buildOpLoad(regA)},
|
||||
0xA5: opcode{"LDA", 2, 3, modeZeroPage, buildOpLoad(regA)},
|
||||
0xB5: opcode{"LDA", 2, 4, modeZeroPageX, buildOpLoad(regA)},
|
||||
0xAD: opcode{"LDA", 3, 4, modeAbsolute, buildOpLoad(regA)},
|
||||
0xBD: opcode{"LDA", 3, 4, modeAbsoluteX, buildOpLoad(regA)}, // Extra cycles
|
||||
0xB9: opcode{"LDA", 3, 4, modeAbsoluteY, buildOpLoad(regA)}, // Extra cycles
|
||||
0xA1: opcode{"LDA", 2, 6, modeIndexedIndirectX, buildOpLoad(regA)},
|
||||
0xB1: opcode{"LDA", 2, 5, modeIndirectIndexedY, buildOpLoad(regA)}, // Extra cycles
|
||||
0xA2: opcode{"LDX", 2, 2, modeImmediate, buildOpLoad(regX)},
|
||||
0xA6: opcode{"LDX", 2, 3, modeZeroPage, buildOpLoad(regX)},
|
||||
0xB6: opcode{"LDX", 2, 4, modeZeroPageY, buildOpLoad(regX)},
|
||||
0xAE: opcode{"LDX", 3, 4, modeAbsolute, buildOpLoad(regX)},
|
||||
0xBE: opcode{"LDX", 3, 4, modeAbsoluteY, buildOpLoad(regX)}, // Extra cycles
|
||||
0xA0: opcode{"LDY", 2, 2, modeImmediate, buildOpLoad(regY)},
|
||||
0xA4: opcode{"LDY", 2, 3, modeZeroPage, buildOpLoad(regY)},
|
||||
0xB4: opcode{"LDY", 2, 4, modeZeroPageX, buildOpLoad(regY)},
|
||||
0xAC: opcode{"LDY", 3, 4, modeAbsolute, buildOpLoad(regY)},
|
||||
0xBC: opcode{"LDY", 3, 4, modeAbsoluteX, buildOpLoad(regY)}, // Extra cycles
|
||||
|
||||
0x85: opcode{"STA", 2, 3, modeZeroPage, buildOpStore(regA)},
|
||||
0x95: opcode{"STA", 2, 4, modeZeroPageX, buildOpStore(regA)},
|
||||
0x8D: opcode{"STA", 3, 4, modeAbsolute, buildOpStore(regA)},
|
||||
0x9D: opcode{"STA", 3, 5, modeAbsoluteX, buildOpStore(regA)},
|
||||
0x99: opcode{"STA", 3, 5, modeAbsoluteY, buildOpStore(regA)},
|
||||
0x81: opcode{"STA", 2, 6, modeIndexedIndirectX, buildOpStore(regA)},
|
||||
0x91: opcode{"STA", 2, 6, modeIndirectIndexedY, buildOpStore(regA)},
|
||||
0x86: opcode{"STX", 2, 3, modeZeroPage, buildOpStore(regX)},
|
||||
0x96: opcode{"STX", 2, 4, modeZeroPageY, buildOpStore(regX)},
|
||||
0x8E: opcode{"STX", 3, 4, modeAbsolute, buildOpStore(regX)},
|
||||
0x84: opcode{"STY", 2, 3, modeZeroPage, buildOpStore(regY)},
|
||||
0x94: opcode{"STY", 2, 4, modeZeroPageX, buildOpStore(regY)},
|
||||
0x8C: opcode{"STY", 3, 4, modeAbsolute, buildOpStore(regY)},
|
||||
|
||||
0x90: opcode{"BCC", 2, 2, modeRelative, buildOpBranch(flagC, false)}, // Extra cycles
|
||||
0xB0: opcode{"BCS", 2, 2, modeRelative, buildOpBranch(flagC, true)}, // Extra cycles
|
||||
0xD0: opcode{"BNE", 2, 2, modeRelative, buildOpBranch(flagZ, false)}, // Extra cycles
|
||||
0xF0: opcode{"BEQ", 2, 2, modeRelative, buildOpBranch(flagZ, true)}, // Extra cycles
|
||||
0x10: opcode{"BPL", 2, 2, modeRelative, buildOpBranch(flagN, false)}, // Extra cycles
|
||||
0x30: opcode{"BMI", 2, 2, modeRelative, buildOpBranch(flagN, true)}, // Extra cycles
|
||||
0x50: opcode{"BVC", 2, 2, modeRelative, buildOpBranch(flagV, false)}, // Extra cycles
|
||||
0x70: opcode{"BVS", 2, 2, modeRelative, buildOpBranch(flagV, true)}, // Extra cycles
|
||||
|
||||
0xEA: opcode{"NOP", 1, 2, modeImplicit, opNOP},
|
||||
|
||||
// Undocumented opcodes, see http://bbc.nvg.org/doc/6502OpList.txt
|
||||
0x1A: opcode{"NOP", 1, 2, modeImplicit, opNOP}, // INC A in the 65c02
|
||||
0xC2: opcode{"NOP", 1, 2, modeImplicit, opNOP}, // Should be HALT?
|
||||
0x02: opcode{"NOP", 1, 2, modeImplicit, opNOP}, // Should be HALT?
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNMOS6502(t *testing.T) {
|
||||
m := new(FlatMemory)
|
||||
s := NewNMOS6502(m)
|
||||
|
||||
m.loadBinary("testdata/6502_functional_test.bin")
|
||||
executeSuite(t, s, 0x200, 240, false, 255)
|
||||
}
|
||||
|
||||
// To execute test suites from https://github.com/Klaus2m5/6502_65C02_functional_tests
|
||||
func executeSuite(t *testing.T, s *State, stepAddress uint16, steps uint8, showStep bool, traceCPUStep uint8) {
|
||||
s.reg.setPC(0x0400)
|
||||
currentStep := uint8(255)
|
||||
for true {
|
||||
testCase := s.mem.Peek(stepAddress)
|
||||
if testCase != currentStep {
|
||||
currentStep = testCase
|
||||
if showStep {
|
||||
fmt.Printf("[ Step %d ]\n", testCase)
|
||||
}
|
||||
s.SetTrace(testCase == traceCPUStep)
|
||||
}
|
||||
if testCase >= steps {
|
||||
break
|
||||
}
|
||||
pc := s.reg.getPC()
|
||||
s.ExecuteInstruction()
|
||||
if pc == s.reg.getPC() {
|
||||
t.Fatalf("Failure in test %v.", testCase)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,310 +0,0 @@
|
|||
package core6502
|
||||
|
||||
func buildOpTransfer(regSrc int, regDst int) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value := s.reg.getRegister(regSrc)
|
||||
s.reg.setRegister(regDst, value)
|
||||
if regDst != regSP {
|
||||
s.reg.updateFlagZN(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpIncDec(inc bool) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value, setValue := resolveGetSetValue(s, line, opcode)
|
||||
if inc {
|
||||
value++
|
||||
} else {
|
||||
value--
|
||||
}
|
||||
s.reg.updateFlagZN(value)
|
||||
setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpShift(isLeft bool, isRotate bool) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value, setValue := resolveGetSetValue(s, line, opcode)
|
||||
|
||||
oldCarry := s.reg.getFlagBit(flagC)
|
||||
var carry bool
|
||||
if isLeft {
|
||||
carry = (value & 0x80) != 0
|
||||
value <<= 1
|
||||
if isRotate {
|
||||
value += oldCarry
|
||||
}
|
||||
} else {
|
||||
carry = (value & 0x01) != 0
|
||||
value >>= 1
|
||||
if isRotate {
|
||||
value += oldCarry << 7
|
||||
}
|
||||
}
|
||||
s.reg.updateFlag(flagC, carry)
|
||||
s.reg.updateFlagZN(value)
|
||||
setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpLoad(regDst int) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value := resolveValue(s, line, opcode)
|
||||
s.reg.setRegister(regDst, value)
|
||||
s.reg.updateFlagZN(value)
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpStore(regSrc int) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
setValue := resolveSetValue(s, line, opcode)
|
||||
value := s.reg.getRegister(regSrc)
|
||||
setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpUpdateFlag(flag uint8, value bool) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
s.reg.updateFlag(flag, value)
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpBranch(flag uint8, test bool) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
if s.reg.getFlag(flag) == test {
|
||||
address := resolveAddress(s, line, opcode)
|
||||
s.reg.setPC(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpBranchOnBit(bit uint8, test bool) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
// Note that those operations have two addressing modes:
|
||||
// one for the zero page value, another for the relative jump.
|
||||
// We will have to resolve the first one here.
|
||||
value := s.mem.Peek(uint16(line[1]))
|
||||
bitValue := ((value >> bit) & 1) == 1
|
||||
|
||||
if bitValue == test {
|
||||
address := resolveAddress(s, line, opcode)
|
||||
s.reg.setPC(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpSetBit(bit uint8, set bool) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value, setValue := resolveGetSetValue(s, line, opcode)
|
||||
if set {
|
||||
setValue(value | (1 << bit))
|
||||
} else {
|
||||
setValue(value &^ (1 << bit))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func opBIT(s *State, line []uint8, opcode opcode) {
|
||||
value := resolveValue(s, line, opcode)
|
||||
acc := s.reg.getA()
|
||||
s.reg.updateFlag(flagZ, value&acc == 0)
|
||||
// The immediate addressing mode (65C02 or 65816 only) does not affect N & V.
|
||||
if opcode.addressMode != modeImmediate {
|
||||
s.reg.updateFlag(flagN, value&(1<<7) != 0)
|
||||
s.reg.updateFlag(flagV, value&(1<<6) != 0)
|
||||
}
|
||||
}
|
||||
|
||||
func opTRB(s *State, line []uint8, opcode opcode) {
|
||||
value, setValue := resolveGetSetValue(s, line, opcode)
|
||||
a := s.reg.getA()
|
||||
s.reg.updateFlag(flagZ, (value&a) == 0)
|
||||
setValue(value &^ a)
|
||||
}
|
||||
|
||||
func opTSB(s *State, line []uint8, opcode opcode) {
|
||||
value, setValue := resolveGetSetValue(s, line, opcode)
|
||||
a := s.reg.getA()
|
||||
s.reg.updateFlag(flagZ, (value&a) == 0)
|
||||
setValue(value | a)
|
||||
}
|
||||
|
||||
func buildOpCompare(reg int) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value := resolveValue(s, line, opcode)
|
||||
reference := s.reg.getRegister(reg)
|
||||
s.reg.updateFlagZN(reference - value)
|
||||
s.reg.updateFlag(flagC, reference >= value)
|
||||
}
|
||||
}
|
||||
|
||||
func operationAnd(a uint8, b uint8) uint8 { return a & b }
|
||||
func operationOr(a uint8, b uint8) uint8 { return a | b }
|
||||
func operationXor(a uint8, b uint8) uint8 { return a ^ b }
|
||||
|
||||
func buildOpLogic(operation func(uint8, uint8) uint8) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value := resolveValue(s, line, opcode)
|
||||
result := operation(value, s.reg.getA())
|
||||
s.reg.setA(result)
|
||||
s.reg.updateFlagZN(result)
|
||||
}
|
||||
}
|
||||
|
||||
func opADC(s *State, line []uint8, opcode opcode) {
|
||||
value := resolveValue(s, line, opcode)
|
||||
aValue := s.reg.getA()
|
||||
carry := s.reg.getFlagBit(flagC)
|
||||
|
||||
total := uint16(aValue) + uint16(value) + uint16(carry)
|
||||
signedTotal := int16(int8(aValue)) + int16(int8(value)) + int16(carry)
|
||||
truncated := uint8(total)
|
||||
|
||||
if s.reg.getFlag(flagD) {
|
||||
totalBcdLo := int(aValue&0x0f) + int(value&0x0f) + int(carry)
|
||||
totalBcdHi := int(aValue>>4) + int(value>>4)
|
||||
if totalBcdLo >= 10 {
|
||||
totalBcdHi++
|
||||
}
|
||||
totalBcd := (totalBcdHi%10)<<4 + (totalBcdLo % 10)
|
||||
s.reg.setA(uint8(totalBcd))
|
||||
s.reg.updateFlag(flagC, totalBcdHi > 9)
|
||||
} else {
|
||||
s.reg.setA(truncated)
|
||||
s.reg.updateFlag(flagC, total > 0xFF)
|
||||
}
|
||||
|
||||
// ZNV flags behave for BCD as if the operation was binary?
|
||||
s.reg.updateFlagZN(truncated)
|
||||
s.reg.updateFlag(flagV, signedTotal < -128 || signedTotal > 127)
|
||||
}
|
||||
|
||||
func opADCAlt(s *State, line []uint8, opcode opcode) {
|
||||
opADC(s, line, opcode)
|
||||
// The Z and N flags on BCD are fixed in 65c02.
|
||||
s.reg.updateFlagZN(s.reg.getA())
|
||||
}
|
||||
|
||||
func opSBC(s *State, line []uint8, opcode opcode) {
|
||||
value := resolveValue(s, line, opcode)
|
||||
aValue := s.reg.getA()
|
||||
carry := s.reg.getFlagBit(flagC)
|
||||
|
||||
total := 0x100 + uint16(aValue) - uint16(value) + uint16(carry) - 1
|
||||
signedTotal := int16(int8(aValue)) - int16(int8(value)) + int16(carry) - 1
|
||||
truncated := uint8(total)
|
||||
|
||||
if s.reg.getFlag(flagD) {
|
||||
totalBcdLo := 10 + int(aValue&0x0f) - int(value&0x0f) + int(carry) - 1
|
||||
totalBcdHi := 10 + int(aValue>>4) - int(value>>4)
|
||||
if totalBcdLo < 10 {
|
||||
totalBcdHi--
|
||||
}
|
||||
totalBcd := (totalBcdHi%10)<<4 + (totalBcdLo % 10)
|
||||
s.reg.setA(uint8(totalBcd))
|
||||
s.reg.updateFlag(flagC, totalBcdHi >= 10)
|
||||
} else {
|
||||
s.reg.setA(truncated)
|
||||
s.reg.updateFlag(flagC, total > 0xFF)
|
||||
}
|
||||
|
||||
// ZNV flags behave for SBC as if the operation was binary
|
||||
s.reg.updateFlagZN(truncated)
|
||||
s.reg.updateFlag(flagV, signedTotal < -128 || signedTotal > 127)
|
||||
}
|
||||
|
||||
func opSBCAlt(s *State, line []uint8, opcode opcode) {
|
||||
opSBC(s, line, opcode)
|
||||
// The Z and N flags on BCD are fixed in 65c02.
|
||||
s.reg.updateFlagZN(s.reg.getA())
|
||||
}
|
||||
|
||||
const stackAddress uint16 = 0x0100
|
||||
|
||||
func pushByte(s *State, value uint8) {
|
||||
adresss := stackAddress + uint16(s.reg.getSP())
|
||||
s.mem.Poke(adresss, value)
|
||||
s.reg.setSP(s.reg.getSP() - 1)
|
||||
}
|
||||
|
||||
func pullByte(s *State) uint8 {
|
||||
s.reg.setSP(s.reg.getSP() + 1)
|
||||
adresss := stackAddress + uint16(s.reg.getSP())
|
||||
return s.mem.Peek(adresss)
|
||||
}
|
||||
|
||||
func pushWord(s *State, value uint16) {
|
||||
pushByte(s, uint8(value>>8))
|
||||
pushByte(s, uint8(value))
|
||||
}
|
||||
|
||||
func pullWord(s *State) uint16 {
|
||||
return uint16(pullByte(s)) +
|
||||
(uint16(pullByte(s)) << 8)
|
||||
|
||||
}
|
||||
|
||||
func buildOpPull(regDst int) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value := pullByte(s)
|
||||
s.reg.setRegister(regDst, value)
|
||||
if regDst != regP {
|
||||
s.reg.updateFlagZN(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpPush(regSrc int) opFunc {
|
||||
return func(s *State, line []uint8, opcode opcode) {
|
||||
value := s.reg.getRegister(regSrc)
|
||||
if regSrc == regP {
|
||||
value |= flagB + flag5
|
||||
}
|
||||
pushByte(s, value)
|
||||
}
|
||||
}
|
||||
|
||||
func opJMP(s *State, line []uint8, opcode opcode) {
|
||||
address := resolveAddress(s, line, opcode)
|
||||
s.reg.setPC(address)
|
||||
}
|
||||
|
||||
func opNOP(s *State, line []uint8, opcode opcode) {}
|
||||
|
||||
func opJSR(s *State, line []uint8, opcode opcode) {
|
||||
pushWord(s, s.reg.getPC()-1)
|
||||
address := resolveAddress(s, line, opcode)
|
||||
s.reg.setPC(address)
|
||||
}
|
||||
|
||||
func opRTI(s *State, line []uint8, opcode opcode) {
|
||||
s.reg.setP(pullByte(s))
|
||||
s.reg.setPC(pullWord(s))
|
||||
}
|
||||
|
||||
func opRTS(s *State, line []uint8, opcode opcode) {
|
||||
s.reg.setPC(pullWord(s) + 1)
|
||||
}
|
||||
|
||||
func opBRK(s *State, line []uint8, opcode opcode) {
|
||||
pushWord(s, s.reg.getPC()+1)
|
||||
pushByte(s, s.reg.getP()|(flagB+flag5))
|
||||
s.reg.setFlag(flagI)
|
||||
s.reg.setPC(getWord(s.mem, vectorBreak))
|
||||
}
|
||||
|
||||
func opBRKAlt(s *State, line []uint8, opcode opcode) {
|
||||
opBRK(s, line, opcode)
|
||||
/*
|
||||
The only difference in the BRK instruction on the 65C02 and the 6502
|
||||
is that the 65C02 clears the D (decimal) flag on the 65C02, whereas
|
||||
the D flag is not affected on the 6502.
|
||||
*/
|
||||
s.reg.clearFlag(flagD)
|
||||
}
|
||||
|
||||
func opSTZ(s *State, line []uint8, opcode opcode) {
|
||||
setValue := resolveSetValue(s, line, opcode)
|
||||
setValue(0)
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
regA = 0
|
||||
regX = 1
|
||||
regY = 2
|
||||
regP = 3
|
||||
regSP = 4
|
||||
regPC = 5 // 2 bytes
|
||||
regPC2 = 6
|
||||
regNone = -1
|
||||
)
|
||||
|
||||
const (
|
||||
flagN uint8 = 1 << 7
|
||||
flagV uint8 = 1 << 6
|
||||
flag5 uint8 = 1 << 5
|
||||
flagB uint8 = 1 << 4
|
||||
flagD uint8 = 1 << 3
|
||||
flagI uint8 = 1 << 2
|
||||
flagZ uint8 = 1 << 1
|
||||
flagC uint8 = 1 << 0
|
||||
)
|
||||
|
||||
type registers struct {
|
||||
data [7]uint8
|
||||
}
|
||||
|
||||
func (r *registers) getRegister(i int) uint8 { return r.data[i] }
|
||||
|
||||
func (r *registers) getA() uint8 { return r.data[regA] }
|
||||
func (r *registers) getX() uint8 { return r.data[regX] }
|
||||
func (r *registers) getY() uint8 { return r.data[regY] }
|
||||
func (r *registers) getP() uint8 { return r.data[regP] }
|
||||
func (r *registers) getSP() uint8 { return r.data[regSP] }
|
||||
|
||||
func (r *registers) setRegister(i int, v uint8) {
|
||||
r.data[i] = v
|
||||
}
|
||||
func (r *registers) setA(v uint8) { r.setRegister(regA, v) }
|
||||
func (r *registers) setX(v uint8) { r.setRegister(regX, v) }
|
||||
func (r *registers) setY(v uint8) { r.setRegister(regY, v) }
|
||||
func (r *registers) setP(v uint8) { r.setRegister(regP, v) }
|
||||
func (r *registers) setSP(v uint8) { r.setRegister(regSP, v) }
|
||||
|
||||
func (r *registers) getPC() uint16 {
|
||||
return uint16(r.data[regPC])*256 + uint16(r.data[regPC2])
|
||||
}
|
||||
|
||||
func (r *registers) setPC(v uint16) {
|
||||
r.data[regPC] = uint8(v >> 8)
|
||||
r.data[regPC2] = uint8(v)
|
||||
}
|
||||
|
||||
func (r *registers) getFlagBit(i uint8) uint8 {
|
||||
if r.getFlag(i) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *registers) getFlag(i uint8) bool {
|
||||
return (r.data[regP] & i) != 0
|
||||
}
|
||||
|
||||
func (r *registers) setFlag(i uint8) {
|
||||
r.data[regP] |= i
|
||||
}
|
||||
|
||||
func (r *registers) clearFlag(i uint8) {
|
||||
r.data[regP] &^= i
|
||||
}
|
||||
|
||||
func (r *registers) updateFlag(i uint8, v bool) {
|
||||
if v {
|
||||
r.setFlag(i)
|
||||
} else {
|
||||
r.clearFlag(i)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registers) updateFlagZN(t uint8) {
|
||||
r.updateFlag(flagZ, t == 0)
|
||||
r.updateFlag(flagN, t >= (1<<7))
|
||||
}
|
||||
|
||||
func (r registers) String() string {
|
||||
return fmt.Sprintf("A: %#02x, X: %#02x, Y: %#02x, SP: %#02x, PC: %#04x, P: %#02x, (NV-BDIZC): %08b",
|
||||
r.getA(), r.getX(), r.getY(), r.getSP(), r.getPC(), r.getP(), r.getP())
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package core6502
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRegA(t *testing.T) {
|
||||
var r registers
|
||||
var data uint8
|
||||
data = 200
|
||||
r.setA(data)
|
||||
if r.getA() != data {
|
||||
t.Error("Error storing and loading A")
|
||||
}
|
||||
}
|
||||
func TestRegPC(t *testing.T) {
|
||||
var r registers
|
||||
var data uint16
|
||||
data = 0xc600
|
||||
r.setPC(data)
|
||||
if r.getPC() != data {
|
||||
t.Error("Error storing and loading PC")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlags(t *testing.T) {
|
||||
var r registers
|
||||
r.setP(0x23)
|
||||
if r.getP() != 0x23 {
|
||||
t.Error("Error storing and loading P")
|
||||
}
|
||||
|
||||
r.setP(0)
|
||||
r.setFlag(flagD)
|
||||
if !r.getFlag(flagD) {
|
||||
t.Error("Error setting and getting flag")
|
||||
}
|
||||
|
||||
r.clearFlag(flagD)
|
||||
if r.getFlag(flagD) {
|
||||
t.Error("Error clearing flag")
|
||||
}
|
||||
|
||||
r.updateFlag(flagD, true)
|
||||
if !r.getFlag(flagD) {
|
||||
t.Error("Error update flag to true")
|
||||
}
|
||||
|
||||
r.updateFlag(flagD, false)
|
||||
if r.getFlag(flagD) {
|
||||
t.Error("Error updating flag to false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFlagZN(t *testing.T) {
|
||||
var r registers
|
||||
r.updateFlagZN(0)
|
||||
if r.getP() != flagZ {
|
||||
t.Error("Error update flags ZN with 0")
|
||||
}
|
||||
|
||||
r.updateFlagZN(0x10)
|
||||
if r.getP() != 0 {
|
||||
t.Error("Error update flags ZN with 0x10")
|
||||
}
|
||||
|
||||
r.updateFlagZN(0xF2)
|
||||
if r.getP() != flagN {
|
||||
t.Error("Error update flags ZN with 0xF2")
|
||||
}
|
||||
}
|
14360
core6502/testdata/6502_functional_test.lst
vendored
11510
core6502/testdata/65C02_extended_opcodes_test.lst
vendored
38
diskette.go
|
@ -1,38 +0,0 @@
|
|||
package apple2
|
||||
|
||||
import "errors"
|
||||
|
||||
type diskette interface {
|
||||
powerOn(cycle uint64)
|
||||
powerOff(cycle uint64)
|
||||
read(quarterTrack int, cycle uint64) uint8
|
||||
write(quarterTrack int, value uint8, cycle uint64)
|
||||
}
|
||||
|
||||
func loadDisquette(filename string) (diskette, error) {
|
||||
data, err := loadResource(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isFileNibOrDsk(data) {
|
||||
f, err := newFileNibOrDsk(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d diskette16sector
|
||||
d.nib = f
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
if isFileWoz(data) {
|
||||
f, err := newFileWoz(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newDisquetteWoz(f)
|
||||
}
|
||||
|
||||
return nil, errors.New("Diskette format not supported")
|
||||
}
|
BIN
doc/karateka.png
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
56
doc/update_readme.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define the file paths
|
||||
readmePath := "../README.md"
|
||||
usagePath := "usage.txt"
|
||||
newUsagePath := "usage_new.txt"
|
||||
|
||||
err := os.Rename(newUsagePath, usagePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Read the contents of the usage file
|
||||
usageBytes, err := ioutil.ReadFile(usagePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Convert the usage bytes to string
|
||||
usage := string(usageBytes)
|
||||
|
||||
// Read the contents of the readme file
|
||||
readmeBytes, err := ioutil.ReadFile(readmePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Convert the readme bytes to string
|
||||
readme := string(readmeBytes)
|
||||
|
||||
// Find the start and end markers
|
||||
startMarker := "<!-- doc/usage.txt start -->"
|
||||
endMarker := "<!-- doc/usage.txt end -->"
|
||||
startIndex := strings.Index(readme, startMarker)
|
||||
endIndex := strings.Index(readme, endMarker)
|
||||
|
||||
// Replace the lines between start and end markers with the usage
|
||||
newReadme := readme[:startIndex+len(startMarker)] + "\n```terminal\n" + usage + "\n```\n" + readme[endIndex:]
|
||||
|
||||
// Write the updated readme back to the file
|
||||
err = ioutil.WriteFile(readmePath, []byte(newReadme), os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("README.md updated successfully!")
|
||||
}
|
88
doc/usage.txt
Normal file
|
@ -0,0 +1,88 @@
|
|||
Usage: izapple2 [file]
|
||||
file
|
||||
path to image to use on the boot device
|
||||
-charrom string
|
||||
rom file for the character generator (default "<internal>/Apple IIe Video Enhanced.bin")
|
||||
-cpu string
|
||||
cpu type, can be '6502' or '65c02' (default "65c02")
|
||||
-forceCaps
|
||||
force all letters to be uppercased (no need for caps lock!)
|
||||
-model string
|
||||
set base model (default "2enh")
|
||||
-mods string
|
||||
comma separated list of mods applied to the board, available mods are 'shift', 'four-colors
|
||||
-nsc string
|
||||
add a DS1216 No-Slot-Clock on the main ROM (use 'main') or a slot ROM (default "main")
|
||||
-profile
|
||||
generate profile trace to analyse with pprof
|
||||
-ramworks string
|
||||
memory to use with RAMWorks card, max is 16384 (default "8192")
|
||||
-rgb
|
||||
emulate the RGB modes of the 80col RGB card for DHGR
|
||||
-rom string
|
||||
main rom file (default "<internal>/Apple2e_Enhanced.rom")
|
||||
-romx
|
||||
emulate a RomX
|
||||
-s0 string
|
||||
slot 0 configuration. (default "language")
|
||||
-s1 string
|
||||
slot 1 configuration. (default "empty")
|
||||
-s2 string
|
||||
slot 2 configuration. (default "vidhd")
|
||||
-s3 string
|
||||
slot 3 configuration. (default "fastchip")
|
||||
-s4 string
|
||||
slot 4 configuration. (default "empty")
|
||||
-s5 string
|
||||
slot 5 configuration. (default "empty")
|
||||
-s6 string
|
||||
slot 6 configuration. (default "diskii,disk1=<internal>/dos33.dsk")
|
||||
-s7 string
|
||||
slot 7 configuration. (default "empty")
|
||||
-speed string
|
||||
cpu speed in Mhz, can be 'ntsc', 'pal', 'full' or a decimal nunmber (default "ntsc")
|
||||
-trace string
|
||||
trace CPU execution with one or more comma separated tracers (default "none")
|
||||
|
||||
The available pre-configured models are:
|
||||
2: Apple ][
|
||||
2e: Apple IIe
|
||||
2enh: Apple //e
|
||||
2plus: Apple ][+
|
||||
base64a: Base 64A
|
||||
swyft: swyft
|
||||
|
||||
The available cards are:
|
||||
brainboard: Firmware card. It has two ROM banks
|
||||
brainboard2: Firmware card. It has up to four ROM banks
|
||||
dan2sd: Apple II Peripheral Card that Interfaces to a ATMEGA328P for SD card storage
|
||||
diskii: Disk II interface card
|
||||
diskiiseq: Disk II interface card emulating the Woz state machine
|
||||
fastchip: Accelerator card for Apple IIe (limited support)
|
||||
fujinet: SmartPort interface card hosting the Fujinet
|
||||
inout: Card to test I/O
|
||||
language: Language card with 16 extra KB for the Apple ][ and ][+
|
||||
memexp: Memory expansion card
|
||||
mouse: Mouse card implementation, does not emulate a real card, only the firmware behaviour
|
||||
multirom: Multiple Image ROM card
|
||||
parallel: Card to dump to a file what would be printed to a parallel printer
|
||||
prodosromcard3: A bootable 4 MB ROM card by Ralle Palaveev
|
||||
prodosromdrive: A bootable 1 MB solid state disk by Terence Boldt
|
||||
saturn: RAM card with 128Kb, it's like 8 language cards
|
||||
smartport: SmartPort interface card
|
||||
softswitchlogger: Card to log softswitch accesses
|
||||
swyftcard: Card with the ROM needed to run the Swyftcard word processing system
|
||||
thunderclock: Clock card
|
||||
videx: Videx compatible 80 columns card
|
||||
vidhd: Firmware signature of the VidHD card to trick Total Replay to use the SHR mode
|
||||
|
||||
The available tracers are:
|
||||
cpm65: Trace CPM65 BDOS calls
|
||||
cpu: Trace CPU execution
|
||||
mli: Trace ProDOS MLI calls
|
||||
mos: Trace MOS calls with Applecorn skipping terminal IO
|
||||
mosfull: Trace MOS calls with Applecorn
|
||||
panicss: Panic on unimplemented softswitches
|
||||
ss: Trace sotfswiches calls
|
||||
ssreg: Trace sotfswiches registrations
|
||||
ucsd: Trace UCSD system calls
|
|
@ -1,13 +1,16 @@
|
|||
FROM golang:1.12
|
||||
FROM golang:1.18.0
|
||||
|
||||
LABEL MAINTAINER="Ivan Izaguirre <ivanizag@gmail.com>"
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y libsdl2-dev mingw-w64
|
||||
RUN apt-get install -y libsdl2-dev mingw-w64 unzip
|
||||
|
||||
RUN wget https://www.libsdl.org/release/SDL2-devel-2.0.9-mingw.tar.gz
|
||||
RUN tar -xzf SDL2-devel-2.0.9-mingw.tar.gz
|
||||
RUN cp -r SDL2-2.0.9/x86_64-w64-mingw32 /usr
|
||||
RUN wget https://www.libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz
|
||||
RUN tar -xzf SDL2-devel-2.0.12-mingw.tar.gz
|
||||
RUN cp -r SDL2-2.0.12/x86_64-w64-mingw32 /usr
|
||||
|
||||
RUN wget https://www.libsdl.org/release/SDL2-2.0.12-win32-x64.zip
|
||||
RUN unzip SDL2-2.0.12-win32-x64.zip -d /sdl2runtime
|
||||
|
||||
COPY buildindocker.sh .
|
||||
RUN chmod +x buildindocker.sh
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
cd "$( dirname $0)"
|
||||
docker build . -t apple2builder
|
||||
docker build . -t apple2builder --platform linux/amd64
|
||||
mkdir -p ${PWD}/build
|
||||
docker run --rm -it -v ${PWD}/build:/build apple2builder
|
||||
|
|
40
dockerbuild/build_all.sh
Executable file
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
cd "$( dirname $0)"
|
||||
mkdir -p ${PWD}/build
|
||||
|
||||
# MacOS ARM builds
|
||||
echo "Building MacOS console frontend"
|
||||
CGO_ENABLED=1 go build -tags static -ldflags "-s -w" ../frontend/console
|
||||
mv console build/izapple2console_mac_arm64
|
||||
|
||||
echo "Building MacOS SDL frontend"
|
||||
CGO_ENABLED=1 go build -tags static -ldflags "-s -w" ../frontend/a2sdl
|
||||
mv a2sdl build/izapple2sdl_mac_arm64
|
||||
|
||||
echo "Building MacOS Fyne frontend"
|
||||
CGO_ENABLED=1 go build -tags static -ldflags "-s -w" ../frontend/a2fyne
|
||||
mv a2fyne build/izapple2fyne_mac_arm64
|
||||
|
||||
# MacOS x64 builds
|
||||
echo "Building MacOS console frontend"
|
||||
GOARCH=amd64 CGO_ENABLED=1 go build -tags static -ldflags "-s -w" ../frontend/console
|
||||
mv console build/izapple2console_mac_amd64
|
||||
|
||||
echo "Building MacOS SDL frontend"
|
||||
GOARCH=amd64 CGO_ENABLED=1 go build -tags static -ldflags "-s -w" ../frontend/a2sdl
|
||||
mv a2sdl build/izapple2sdl_mac_amd64
|
||||
|
||||
echo "Building MacOS Fyne frontend"
|
||||
GOARCH=amd64 CGO_ENABLED=1 go build -tags static -ldflags "-s -w" ../frontend/a2fyne
|
||||
mv a2fyne build/izapple2fyne_mac_amd64∫
|
||||
|
||||
# Linux and Windows dockerized builds
|
||||
echo "Building docker container for the Linux and Windows builds"
|
||||
docker build . -t apple2builder --platform linux/amd64
|
||||
docker run --rm -it -v ${PWD}/build:/build apple2builder
|
||||
|
||||
cd build
|
||||
cp ../../README.md .
|
||||
zip izapple2sdl_windows_amd64.zip izapple2sdl_windows_amd64.exe README-SDL.txt README.md SDL2.dll
|
||||
|
||||
|
|
@ -1,27 +1,49 @@
|
|||
#!/bin/bash
|
||||
cd /tmp
|
||||
git clone https://github.com/ivanizag/apple2
|
||||
git clone https://github.com/ivanizag/izapple2
|
||||
|
||||
# Build apple2console for Linux
|
||||
cd /tmp/apple2/apple2console
|
||||
go build .
|
||||
chown --reference /build apple2console
|
||||
cp apple2console /build
|
||||
# Build izapple2console for Linux
|
||||
echo "Building Linux console frontend"
|
||||
cd /tmp/izapple2/frontend/console
|
||||
env CGO_ENABLED=1 go build -tags static -ldflags "-s -w" .
|
||||
chown --reference /build console
|
||||
cp console /build/izapple2console_linux_amd64
|
||||
|
||||
# Build apple2console.exe for Windows
|
||||
cd /tmp/apple2/apple2console
|
||||
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -o apple2console.exe .
|
||||
chown --reference /build apple2console.exe
|
||||
cp apple2console.exe /build
|
||||
# Build izapple2console.exe for Windows
|
||||
echo "Building Windows console frontend"
|
||||
cd /tmp/izapple2/frontend/console
|
||||
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -tags static -ldflags "-s -w" -o izapple2console.exe .
|
||||
chown --reference /build izapple2console.exe
|
||||
cp izapple2console.exe /build/izapple2console_windows_amd64.exe
|
||||
|
||||
# Build apple2sdl for Linux
|
||||
cd /tmp/apple2/apple2sdl
|
||||
go build .
|
||||
chown --reference /build apple2sdl
|
||||
cp apple2sdl /build
|
||||
# Build izapple2sdl for Linux
|
||||
echo "Building Linux SDL frontend"
|
||||
cd /tmp/izapple2/frontend/a2sdl
|
||||
env CGO_ENABLED=1 go build -tags static -ldflags "-s -w" .
|
||||
chown --reference /build a2sdl
|
||||
cp a2sdl /build/izapple2sdl_linux_amd64
|
||||
|
||||
# Build apple2sdl.exe for Windows
|
||||
cd /tmp/apple2/apple2sdl
|
||||
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib -lSDL2" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -o apple2sdl.exe .
|
||||
chown --reference /build apple2sdl.exe
|
||||
cp apple2sdl.exe /build
|
||||
# Build izapple2sdl.exe for Windows
|
||||
echo "Building Windows SDL frontend"
|
||||
cd /tmp/izapple2/frontend/a2sdl
|
||||
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib -lSDL2" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -tags static -ldflags "-s -w" -o izapple2sdl.exe .
|
||||
chown --reference /build izapple2sdl.exe
|
||||
cp izapple2sdl.exe /build/izapple2sdl_windows_amd64.exe
|
||||
|
||||
# Build izapple2fyne for Linux
|
||||
echo "Building Linux Fyne frontend"
|
||||
cd /tmp/izapple2/frontend/a2fyne
|
||||
env CGO_ENABLED=1 go build -tags static -ldflags "-s -w" .
|
||||
chown --reference /build a2fyne
|
||||
cp a2fyne /build/izapple2fyne_linux_amd64
|
||||
|
||||
# Build izapple2fyne.exe for Windows
|
||||
echo "Building Windows Fyne frontend"
|
||||
cd /tmp/izapple2/frontend/a2fyne
|
||||
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib " CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -tags static -ldflags "-s -w" -o izapple2fyne.exe .
|
||||
chown --reference /build izapple2fyne.exe
|
||||
cp izapple2fyne.exe /build/izapple2fyne_windows_amd64.exe
|
||||
|
||||
|
||||
# Copy SDL2 Runtime
|
||||
cp /sdl2runtime/* /build
|
||||
|
|
64
e2e_boot_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testBoots(t *testing.T, model string, disk string, cycles uint64, banner string, prompt string, col80 bool) {
|
||||
overrides := newConfiguration()
|
||||
if disk != "" {
|
||||
overrides.set(confS6, "diskii,disk1=\""+disk+"\"")
|
||||
} else {
|
||||
overrides.set(confS6, "empty")
|
||||
}
|
||||
|
||||
at, err := makeApple2Tester(model, overrides)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
at.terminateCondition = buildTerminateConditionTexts(at, []string{banner, prompt}, col80, cycles)
|
||||
at.run()
|
||||
|
||||
var text string
|
||||
if col80 {
|
||||
text = at.getText80()
|
||||
} else {
|
||||
text = at.getText()
|
||||
}
|
||||
if !strings.Contains(text, banner) {
|
||||
t.Errorf("Expected '%s', got '%s'", banner, text)
|
||||
}
|
||||
if !strings.Contains(text, prompt) {
|
||||
t.Errorf("Expected prompt '%s', got '%s'", prompt, text)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPlusBoots(t *testing.T) {
|
||||
testBoots(t, "2plus", "", 200_000, "APPLE ][", "\n]", false)
|
||||
}
|
||||
|
||||
func Test2EBoots(t *testing.T) {
|
||||
testBoots(t, "2e", "", 200_000, "Apple ][", "\n]", false)
|
||||
}
|
||||
|
||||
func Test2EnhancedBoots(t *testing.T) {
|
||||
testBoots(t, "2enh", "", 200_000, "Apple //e", "\n]", false)
|
||||
}
|
||||
|
||||
func TestBase64Boots(t *testing.T) {
|
||||
testBoots(t, "base64a", "", 1_000_000, "BASE 64A", "\n]", false)
|
||||
}
|
||||
|
||||
func TestPlusDOS33Boots(t *testing.T) {
|
||||
testBoots(t, "2plus", "<internal>/dos33.dsk", 100_000_000, "DOS VERSION 3.3", "\n]", false)
|
||||
}
|
||||
|
||||
func TestProdDOSBoots(t *testing.T) {
|
||||
testBoots(t, "2enh", "<internal>/ProDOS_2_4_3.po", 100_000_000, "BITSY BYE", "NEW VOL", false)
|
||||
}
|
||||
|
||||
func TestCPM65Boots(t *testing.T) {
|
||||
testBoots(t, "2enh", "<internal>/cpm65.po", 5_000_000, "CP/M-65 for the Apple II", "\nA>", true)
|
||||
}
|
122
e2e_woz_test.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package izapple2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testWoz(t *testing.T, sequencer bool, file string, expectedTracks []int, cycleLimit uint64) {
|
||||
|
||||
overrides := newConfiguration()
|
||||
if sequencer {
|
||||
overrides.set(confS6, "diskiiseq,disk1=\"woz_test_images/"+file+"\"")
|
||||
} else {
|
||||
overrides.set(confS6, "diskii,disk1=\"woz_test_images/"+file+"\"")
|
||||
}
|
||||
at, err := makeApple2Tester("2enh", overrides)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
diskIIcard, ok := at.a.cards[6].(cardDisk2Shared)
|
||||
if !ok {
|
||||
t.Fatal("Not a disk II card")
|
||||
}
|
||||
tt := makeTrackTracerSummary()
|
||||
diskIIcard.setTrackTracer(tt)
|
||||
|
||||
expectedLen := len(expectedTracks)
|
||||
|
||||
at.terminateCondition = func(a *Apple2) bool {
|
||||
tracksMayMatch := len(tt.quarterTracks) >= expectedLen &&
|
||||
tt.quarterTracks[expectedLen-1] == expectedTracks[expectedLen-1]
|
||||
|
||||
return tracksMayMatch || a.cpu.GetCycles() > cycleLimit
|
||||
}
|
||||
at.run()
|
||||
|
||||
if !tt.isTraceAsExpected(expectedTracks) {
|
||||
t.Errorf("Quarter tracks, expected %#v, got %#v", expectedTracks, tt.quarterTracks)
|
||||
}
|
||||
|
||||
//t.Errorf("Cycles: %d vs %d", at.a.cpu.GetCycles(), cycleLimit)
|
||||
}
|
||||
|
||||
const (
|
||||
all = 0
|
||||
seq = 1 // Passes only with the sequencer implementation
|
||||
none = 2 // Fails also with the sequencer implementation
|
||||
)
|
||||
|
||||
func TestWoz(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
skip int
|
||||
disk string
|
||||
cycleLimit uint64
|
||||
expectedTracks []int
|
||||
}{
|
||||
// How to being
|
||||
// DOS 3.2, requires 13 sector disks
|
||||
{"DOS3.3", all, "DOS 3.3 System Master.woz", 11_000_000, []int{0, 8, 0, 76, 68, 84, 68, 84, 68, 92, 16, 24}},
|
||||
|
||||
// Next choices
|
||||
{"Bouncing Kamungas", all, "Bouncing Kamungas - Disk 1, Side A.woz", 30_000_000, []int{0, 32, 0, 40, 0}},
|
||||
// Runs but the test is unstable {"Commando", seq, "Commando - Disk 1, Side A.woz", 15_000_000, []int{0, 136, 68, 128, 68, 128, 68, 124, 12, 116, 108}},
|
||||
{"Planetfall", all, "Planetfall - Disk 1, Side A.woz", 4_000_000, []int{0, 8}},
|
||||
{"Rescue Raiders", all, "Rescue Raiders - Disk 1, Side B.woz", 80_000_000, []int{
|
||||
0, 84, 44, 46,
|
||||
0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0,
|
||||
84, 44, 116, 4, 8, 4, 12, 8, 84, 44, 132, 0, 120, 44, 84, 44, 124, 0, 120, 44}},
|
||||
{"Sammy Lightfoot", all, "Sammy Lightfoot - Disk 1, Side A.woz", 80_000_000, []int{0, 64, 8, 20}},
|
||||
{"Stargate", all, "Stargate - Disk 1, Side A.woz", 50_000_000, []int{0, 8, 0, 72, 68, 80, 68, 80, 12, 44}},
|
||||
|
||||
// Cross track sync
|
||||
{"Blazing Paddles", all, "Blazing Paddles (Baudville).woz", 6_000_000, []int{0, 28, 0, 16, 12, 56, 52, 80}},
|
||||
{"Take 1", all, "Take 1 (Baudville).woz", 8_000_000, []int{0, 28, 0, 4, 0, 72, 0, 20, 0}},
|
||||
{"Hard Hat Mack", all, "Hard Hat Mack - Disk 1, Side A.woz", 10_000_000, []int{0, 134, 132}},
|
||||
|
||||
// Half tracks
|
||||
{"The Bilestoad", all, "The Bilestoad - Disk 1, Side A.woz", 6_000_000, []int{0, 24}},
|
||||
|
||||
// Even more bit fiddling
|
||||
{"Dino Eggs", all, "Dino Eggs - Disk 1, Side A.woz", 9_000_000, []int{0, 78, 60, 108, 32}},
|
||||
{"Crisis Mountain", all, "Crisis Mountain - Disk 1, Side A.woz", 20_000_000, []int{0, 32, 8, 32, 20, 76, 20, 36, 32, 84, 52, 64}},
|
||||
{"Miner 2049er II", all, "Miner 2049er II - Disk 1, Side A.woz", 11_000_000, []int{0, 12, 8, 32, 12, 136, 132}},
|
||||
|
||||
// When bits aren't really bits
|
||||
{"The Print Shop Companion", all, "The Print Shop Companion - Disk 1, Side A.woz", 14_000_000, []int{0, 68, 44, 68, 40, 68, 40, 136, 60}},
|
||||
|
||||
// What is the lifepsan of the data latch?
|
||||
{"First Math Adventures", seq, "First Math Adventures - Understanding Word Problems.woz", 6_000_000, []int{0, 8, 0, 68, 12, 20}},
|
||||
|
||||
// Reading Offset Data Streams
|
||||
{"Wings of Fury", seq, "Wings of Fury - Disk 1, Side A.woz", 410_000_000, []int{0, 4, 0, 136, 124, 128, 24, 136, 124, 128, 24, 136, 124, 128, 24, 136, 124, 128, 24, 104}},
|
||||
{"Stickybear Town Builder", all, "Stickybear Town Builder - Disk 1, Side A.woz", 8_000_000, []int{0, 16, 12, 112, 80, 100, 8}},
|
||||
|
||||
// Optimal bit timing
|
||||
// Requires disk change {"Border Zone", "Border Zone - Disk 1, Side A.woz", 500_000_000, []int{1,1,1,1,1,1}},
|
||||
|
||||
// Extra
|
||||
{"Mr. Do", seq, "Mr. Do.woz", 95_000_000, []int{0, 108, 48, 104, 72, 84, 0, 4}},
|
||||
{"Wavy Navy", all, "Wavy Navy.woz", 9_000_000, []int{0, 136}},
|
||||
// SAGA6 requires disk change,
|
||||
// Note that Congo Bongo works with the non sequencer implementation but the test is unstable
|
||||
{"Congo Bongo", seq, "Congo Bongo.woz", 8_000_000, []int{0, 4, 2, 40, 20, 40, 16, 124, 116}},
|
||||
// Wizardry III requires disk change,
|
||||
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if tc.skip == all {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
testWoz(t, false, tc.disk, tc.expectedTracks, tc.cycleLimit)
|
||||
})
|
||||
}
|
||||
if tc.skip == all || tc.skip == seq {
|
||||
t.Run(tc.name+" SEQ", func(t *testing.T) {
|
||||
testWoz(t, true, tc.disk, tc.expectedTracks, tc.cycleLimit)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
180
fileNib.go
|
@ -1,180 +0,0 @@
|
|||
package apple2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
/*
|
||||
See:
|
||||
"Beneath Apple DOS" https://fabiensanglard.net/fd_proxy/prince_of_persia/Beneath%20Apple%20DOS.pdf
|
||||
https://github.com/TomHarte/CLK/wiki/Apple-GCR-disk-encoding
|
||||
*/
|
||||
|
||||
const (
|
||||
numberOfTracks = 35
|
||||
numberOfSectors = 16
|
||||
bytesPerSector = 256
|
||||
bytesPerTrack = numberOfSectors * bytesPerSector
|
||||
nibBytesPerTrack = 6656
|
||||
nibImageSize = numberOfTracks * nibBytesPerTrack
|
||||
dskImageSize = numberOfTracks * numberOfSectors * bytesPerSector
|
||||
defaultVolumeTag = 254
|
||||
cyclesPerBit = 4
|
||||
)
|
||||
|
||||
type fileNib struct {
|
||||
track [numberOfTracks][]byte
|
||||
}
|
||||
|
||||
func loadFileNibOrDsk(filename string) (*fileNib, error) {
|
||||
data, err := loadResource(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newFileNibOrDsk(data)
|
||||
}
|
||||
|
||||
func isFileNibOrDsk(data []uint8) bool {
|
||||
size := len(data)
|
||||
return size == nibImageSize || size == dskImageSize
|
||||
}
|
||||
|
||||
func newFileNibOrDsk(data []uint8) (*fileNib, error) {
|
||||
var f fileNib
|
||||
|
||||
size := len(data)
|
||||
|
||||
if size == nibImageSize {
|
||||
// Load file already in nib format
|
||||
for i := 0; i < numberOfTracks; i++ {
|
||||
f.track[i] = data[nibBytesPerTrack*i : nibBytesPerTrack*(i+1)]
|
||||
}
|
||||
} else if size == dskImageSize {
|
||||
// Convert to nib
|
||||
for i := 0; i < numberOfTracks; i++ {
|
||||
trackData := data[i*bytesPerTrack : (i+1)*bytesPerTrack]
|
||||
f.track[i] = nibEncodeTrack(trackData, defaultVolumeTag, byte(i))
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("Invalid disk size")
|
||||
}
|
||||
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (f *fileNib) saveNib(filename string) error {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, v := range f.track {
|
||||
_, err := file.Write(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var dos33SectorsLogicOrder = [16]int{
|
||||
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
|
||||
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF,
|
||||
}
|
||||
|
||||
var sixAndTwoTranslateTable = [0x40]byte{
|
||||
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
|
||||
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
|
||||
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
|
||||
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
|
||||
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
|
||||
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
|
||||
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
|
||||
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
|
||||
}
|
||||
|
||||
const (
|
||||
gap1Len = 48
|
||||
gap2Len = 5
|
||||
primaryBufferSize = bytesPerSector
|
||||
secondaryBufferSize = bytesPerSector/3 + 1
|
||||
)
|
||||
|
||||
func oddEvenEncodeByte(b byte) []byte {
|
||||
/*
|
||||
A byte is encoded in two bytes to make sure the bytes start with 1 and
|
||||
does not have two consecutive zeros.
|
||||
Data byte: D7-D6-D5-D4-D3-D2-D1-D0
|
||||
resutl[0]: 1-D7- 1-D5- 1-D3-1 -D1
|
||||
resutl[1]: 1-D6- 1-D4- 1-D2-1 -D0
|
||||
*/
|
||||
e := make([]byte, 2)
|
||||
e[0] = ((b >> 1) & 0x55) | 0xaa
|
||||
e[1] = (b & 0x55) | 0xaa
|
||||
return e
|
||||
}
|
||||
|
||||
func nibEncodeTrack(data []byte, volume byte, track byte) []byte {
|
||||
b := make([]byte, 0, nibBytesPerTrack) // Buffer slice with enough capacity
|
||||
// Initialize gaps to be copied for each sector
|
||||
gap1 := make([]byte, gap1Len)
|
||||
for i := range gap1 {
|
||||
gap1[i] = 0xff
|
||||
}
|
||||
gap2 := make([]byte, gap2Len)
|
||||
for i := range gap2 {
|
||||
gap2[i] = 0xff
|
||||
}
|
||||
for physicalSector := byte(0); physicalSector < numberOfSectors; physicalSector++ {
|
||||
/* On the DSK file the sectors are in DOS3.3 logical order
|
||||
but on the physical encoded track as well as in the nib
|
||||
files they are in phisical order.
|
||||
*/
|
||||
logicalSector := dos33SectorsLogicOrder[physicalSector]
|
||||
sectorData := data[logicalSector*bytesPerSector : (logicalSector+1)*bytesPerSector]
|
||||
|
||||
// 6and2 prenibbilizing.
|
||||
primaryBuffer := make([]byte, primaryBufferSize)
|
||||
secondaryBuffer := make([]byte, secondaryBufferSize)
|
||||
for i, v := range sectorData {
|
||||
// Primary buffer is easy: the 6 MSB
|
||||
primaryBuffer[i] = v >> 2
|
||||
// Secondary buffer: the 2 LSB reversed, shifted and in their place
|
||||
shift := uint((i / secondaryBufferSize) * 2)
|
||||
bit0 := ((v & 0x01) << 1) << shift
|
||||
bit1 := ((v & 0x02) >> 1) << shift
|
||||
position := i % secondaryBufferSize
|
||||
secondaryBuffer[position] |= bit0 | bit1
|
||||
}
|
||||
|
||||
// Render sector
|
||||
// Address field
|
||||
b = append(b, gap1...)
|
||||
b = append(b, 0xd5, 0xaa, 0x96) // Address prolog
|
||||
b = append(b, oddEvenEncodeByte(volume)...) // 4-4 encoded volume
|
||||
b = append(b, oddEvenEncodeByte(track)...) // 4-4 encoded track
|
||||
b = append(b, oddEvenEncodeByte(physicalSector)...) // 4-4 encoded sector
|
||||
b = append(b, oddEvenEncodeByte(volume^track^physicalSector)...) // Checksum
|
||||
b = append(b, 0xde, 0xaa, 0xeb) // Epilog
|
||||
// Data field
|
||||
b = append(b, gap2...)
|
||||
b = append(b, 0xd5, 0xaa, 0xad) // Data prolog
|
||||
prevV := byte(0)
|
||||
for _, v := range secondaryBuffer {
|
||||
b = append(b, sixAndTwoTranslateTable[v^prevV])
|
||||
prevV = v
|
||||
}
|
||||
for _, v := range primaryBuffer {
|
||||
b = append(b, sixAndTwoTranslateTable[v^prevV])
|
||||
prevV = v
|
||||
}
|
||||
b = append(b, sixAndTwoTranslateTable[prevV]) // Checksum
|
||||
b = append(b, 0xde, 0xaa, 0xeb) // Data epilog
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
112
frontend/a2fyne/fyneJoysticks.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
)
|
||||
|
||||
/*
|
||||
Apple 2 supports four paddles and 3 pushbuttons. The first two paddles are
|
||||
the X, Y axis of the first joystick. The second two correspond the the second
|
||||
joystick.
|
||||
Button 0 is the primary button of joystick 0.
|
||||
Button 1 is the secondary button of joystick 0 but also the primary button of
|
||||
joystick 1.
|
||||
Button 2 is the secondary button of Joystick 1.
|
||||
*/
|
||||
|
||||
// TODO: key as buttons as on the IIe and mouse as joystick
|
||||
|
||||
type joystickInfo struct {
|
||||
present bool
|
||||
name string
|
||||
paddles [2]uint8
|
||||
buttons [2]bool
|
||||
}
|
||||
|
||||
type joysticks struct {
|
||||
s *state
|
||||
info [2]*joystickInfo
|
||||
}
|
||||
|
||||
const unplugged = uint8(255) // Max resistance when unplugged
|
||||
|
||||
func newJoysticks(s *state) *joysticks {
|
||||
var j joysticks
|
||||
j.s = s
|
||||
return &j
|
||||
}
|
||||
|
||||
func (j *joysticks) start() {
|
||||
pool := time.NewTicker(time.Second / 50)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pool.C:
|
||||
j.info[0] = j.queryJoystick(glfw.Joystick1)
|
||||
j.info[1] = j.queryJoystick(glfw.Joystick2)
|
||||
|
||||
j.s.devices.joystick.updateJoy1(j.info[0])
|
||||
j.s.devices.joystick.updateJoy2(j.info[1])
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func (j *joysticks) queryJoystick(joy glfw.Joystick) *joystickInfo {
|
||||
if !joy.Present() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var info joystickInfo
|
||||
info.name = joy.GetName()
|
||||
buttons := joy.GetButtons()
|
||||
for i, b := range buttons {
|
||||
if b == glfw.Press {
|
||||
info.buttons[i%2] = true
|
||||
}
|
||||
}
|
||||
axes := joy.GetAxes()
|
||||
for i := 0; i < len(info.paddles); i++ {
|
||||
info.paddles[i] = unplugged
|
||||
if i < len(axes) {
|
||||
v := uint16((axes[i] + 1.0) / 2.0 * 256.0)
|
||||
if v > 255 {
|
||||
v = 255
|
||||
}
|
||||
info.paddles[i] = uint8(v)
|
||||
}
|
||||
}
|
||||
return &info
|
||||
}
|
||||
|
||||
func (j *joysticks) ReadButton(i int) bool {
|
||||
var value bool
|
||||
i0 := j.info[0]
|
||||
i1 := j.info[1]
|
||||
switch i {
|
||||
case 0:
|
||||
value = (i0 != nil) && i0.buttons[0]
|
||||
case 1:
|
||||
// It can be secondary of first or primary of second
|
||||
value = ((i0 != nil) && i0.buttons[1]) ||
|
||||
(i1 != nil) && i1.buttons[0]
|
||||
case 2:
|
||||
value = (i1 != nil) && i1.buttons[0]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (j *joysticks) ReadPaddle(i int) (uint8, bool) {
|
||||
var value = unplugged
|
||||
info := j.info[i/2]
|
||||
if info != nil {
|
||||
value = info.paddles[i%2]
|
||||
}
|
||||
return value, value != unplugged
|
||||
}
|
144
frontend/a2fyne/fyneKeyboard.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
)
|
||||
|
||||
type keyboard struct {
|
||||
s *state
|
||||
keyChannel *izapple2.KeyboardChannel
|
||||
|
||||
controlLeft bool
|
||||
controlRight bool
|
||||
}
|
||||
|
||||
func newKeyboard(s *state) *keyboard {
|
||||
var k keyboard
|
||||
k.s = s
|
||||
k.keyChannel = izapple2.NewKeyboardChannel(s.a)
|
||||
return &k
|
||||
}
|
||||
|
||||
func (k *keyboard) putRune(ch rune) {
|
||||
k.keyChannel.PutRune(ch)
|
||||
}
|
||||
|
||||
// PutChar sends a character to the emulator
|
||||
func (k *keyboard) PutChar(ch uint8) {
|
||||
k.keyChannel.PutChar(ch)
|
||||
}
|
||||
|
||||
func (k *keyboard) putKeyAction(keyEvent *fyne.KeyEvent, press bool) {
|
||||
|
||||
ctrl := k.controlLeft || k.controlRight
|
||||
if press && ctrl && len(keyEvent.Name) == 1 {
|
||||
// Hacky. We relay on the letter values to be a single uppercase char.
|
||||
ch := keyEvent.Name[0]
|
||||
if ch >= 'A' && ch <= 'Z' {
|
||||
k.keyChannel.PutChar(uint8(ch) - 65 + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if press && ctrl {
|
||||
// F keys with ctrl do not generate events in putKey()
|
||||
switch keyEvent.Name {
|
||||
case fyne.KeyF1:
|
||||
k.s.a.SendCommand(izapple2.CommandReset)
|
||||
case fyne.KeyF12:
|
||||
screen.AddScenario(k.s.a, "../../screen/test_resources/")
|
||||
}
|
||||
}
|
||||
|
||||
switch keyEvent.Name {
|
||||
case desktop.KeyControlLeft:
|
||||
k.controlLeft = press
|
||||
case desktop.KeyControlRight:
|
||||
k.controlRight = press
|
||||
}
|
||||
}
|
||||
|
||||
func (k *keyboard) putKey(keyEvent *fyne.KeyEvent) {
|
||||
/*
|
||||
See "Apple II reference manual", page 5
|
||||
|
||||
To get keys as understood by the Apple2 hardware run:
|
||||
10 A=PEEK(49152)
|
||||
20 PRINT A, A - 128
|
||||
30 GOTO 10
|
||||
*/
|
||||
|
||||
// Keys with control are not generating events in putKey()
|
||||
//ctrl := k.controlLeft || k.controlRight
|
||||
|
||||
result := uint8(0)
|
||||
switch keyEvent.Name {
|
||||
case fyne.KeyEscape:
|
||||
result = 27
|
||||
case fyne.KeyBackspace:
|
||||
result = 8
|
||||
case fyne.KeyReturn:
|
||||
result = 13
|
||||
case fyne.KeyEnter:
|
||||
result = 13
|
||||
case fyne.KeyLeft:
|
||||
result = 8
|
||||
case fyne.KeyRight:
|
||||
result = 21
|
||||
|
||||
// Apple //e
|
||||
case fyne.KeyUp:
|
||||
result = 11 // 31 in the Base64A
|
||||
case fyne.KeyDown:
|
||||
result = 10
|
||||
case fyne.KeyTab: // The Tab is not reaching here
|
||||
result = 9
|
||||
case fyne.KeyDelete:
|
||||
result = 127 // 24 in the Base64A
|
||||
|
||||
// Base64A clone particularities
|
||||
case fyne.KeyF2:
|
||||
result = 127 // Base64A
|
||||
|
||||
// Control of the emulator
|
||||
case fyne.KeyF1:
|
||||
/*if ctrl {
|
||||
k.s.a.SendCommand(izapple2.CommandReset)
|
||||
}*/
|
||||
case fyne.KeyF5:
|
||||
k.s.a.SendCommand(izapple2.CommandShowSpeed)
|
||||
case fyne.KeyF6:
|
||||
if k.s.screenMode != screen.ScreenModeGreen {
|
||||
k.s.screenMode = screen.ScreenModeGreen
|
||||
} else {
|
||||
k.s.screenMode = screen.ScreenModeNTSC
|
||||
}
|
||||
case fyne.KeyF7:
|
||||
k.s.showPages = !k.s.showPages
|
||||
case fyne.KeyF9:
|
||||
k.s.a.SendCommand(izapple2.CommandDumpDebugInfo)
|
||||
case fyne.KeyF10:
|
||||
k.s.a.SendCommand(izapple2.CommandNextCharGenPage)
|
||||
case fyne.KeyF11:
|
||||
k.s.a.SendCommand(izapple2.CommandToggleCPUTrace)
|
||||
case fyne.KeyF12:
|
||||
//case fyne.KeyPrintScreen:
|
||||
err := screen.SaveSnapshot(k.s.a, k.s.screenMode, "snapshot.png")
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving snapshoot: %v.\n.", err)
|
||||
} else {
|
||||
fmt.Println("Saving snapshot")
|
||||
}
|
||||
//case fyne.KeyPause:
|
||||
// k.s.a.SendCommand(izapple2.CommandPauseUnpause)
|
||||
}
|
||||
|
||||
if result != 0 {
|
||||
k.keyChannel.PutChar(result)
|
||||
}
|
||||
}
|
142
frontend/a2fyne/main.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
a *izapple2.Apple2
|
||||
app fyne.App
|
||||
win fyne.Window
|
||||
|
||||
devices *panelDevices
|
||||
|
||||
showPages bool
|
||||
screenMode int
|
||||
}
|
||||
|
||||
func (s *state) DefaultTitle() string {
|
||||
title := "iz-" + s.a.Name
|
||||
if s.a.IsForceCaps() {
|
||||
title = strings.ToUpper(title)
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s state
|
||||
var err error
|
||||
s.a, err = izapple2.CreateConfiguredApple()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
}
|
||||
if s.a != nil {
|
||||
if s.a.IsProfiling() {
|
||||
// See the log with:
|
||||
// go tool pprof --pdf ~/go/bin/izapple2sdl /tmp/profile329536248/cpu.pprof > profile.pdf
|
||||
defer profile.Start().Stop()
|
||||
}
|
||||
|
||||
fyneRun(&s)
|
||||
}
|
||||
}
|
||||
|
||||
func fyneRun(s *state) {
|
||||
s.screenMode = screen.ScreenModeNTSC
|
||||
|
||||
s.app = app.New()
|
||||
s.app.SetIcon(resourceApple2Png)
|
||||
s.win = s.app.NewWindow(s.DefaultTitle())
|
||||
s.win.SetIcon(resourceApple2Png)
|
||||
|
||||
s.devices = newPanelDevices(s)
|
||||
s.devices.w.Hide()
|
||||
toolbar := buildToolbar(s)
|
||||
display := canvas.NewImageFromImage(nil)
|
||||
display.ScaleMode = canvas.ImageScalePixels // Looks worst but loads less.
|
||||
display.SetMinSize(fyne.NewSize(280, 192))
|
||||
|
||||
container := container.New(
|
||||
layout.NewBorderLayout(nil, toolbar, nil, s.devices.w),
|
||||
display, toolbar, s.devices.w,
|
||||
)
|
||||
s.win.SetContent(container)
|
||||
s.win.SetPadded(false)
|
||||
|
||||
registerKeyboardEvents(s)
|
||||
j := newJoysticks(s)
|
||||
j.start()
|
||||
s.a.SetJoysticksProvider(j)
|
||||
|
||||
go s.a.Run()
|
||||
|
||||
ticker := time.NewTicker(60 * time.Millisecond)
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if !s.a.IsPaused() {
|
||||
var img *image.RGBA
|
||||
if s.showPages {
|
||||
img = screen.SnapshotParts(s.a, s.screenMode)
|
||||
s.win.SetTitle(fmt.Sprintf("%v %v %vx%v", s.a.Name, screen.VideoModeName(s.a), img.Rect.Dx()/2, img.Rect.Dy()/2))
|
||||
} else {
|
||||
img = screen.Snapshot(s.a, s.screenMode)
|
||||
}
|
||||
display.Image = img
|
||||
canvas.Refresh(display)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s.win.SetOnClosed(func() {
|
||||
done <- true
|
||||
})
|
||||
|
||||
s.win.Show()
|
||||
s.app.Run()
|
||||
}
|
||||
|
||||
func registerKeyboardEvents(s *state) {
|
||||
kp := newKeyboard(s)
|
||||
canvas := s.win.Canvas()
|
||||
|
||||
// Events
|
||||
canvas.SetOnTypedKey(func(ke *fyne.KeyEvent) {
|
||||
//fmt.Printf("Event: %v\n", ke.Name)
|
||||
kp.putKey(ke)
|
||||
})
|
||||
canvas.SetOnTypedRune(func(ch rune) {
|
||||
//fmt.Printf("Rune: %v\n", ch)
|
||||
kp.putRune(ch)
|
||||
})
|
||||
if deskCanvas, ok := canvas.(desktop.Canvas); ok {
|
||||
deskCanvas.SetOnKeyDown(func(ke *fyne.KeyEvent) {
|
||||
kp.putKeyAction(ke, true)
|
||||
//fmt.Printf("Event down: %v\n", ke.Name)
|
||||
})
|
||||
deskCanvas.SetOnKeyUp(func(ke *fyne.KeyEvent) {
|
||||
kp.putKeyAction(ke, false)
|
||||
//fmt.Printf("Event up: %v\n", ke.Name)
|
||||
})
|
||||
}
|
||||
}
|
13
frontend/a2fyne/notes.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Fyne
|
||||
|
||||
- Tooltip on widgets. As there is a Hoverable, it would be nice to have Tooltipable¿? and check on mouse move.
|
||||
```
|
||||
type Tooltipable interface {
|
||||
Tooltip() string
|
||||
}
|
||||
```
|
||||
|
||||
- Text on the toolabar items, at least tooltips
|
||||
- Missing keys for Pause and for PrintScreen.
|
||||
- RunOnMain()
|
||||
- Buttons with state: pressed, disabled
|
30
frontend/a2fyne/panelCard.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/ivanizag/izapple2"
|
||||
)
|
||||
|
||||
type panelCard struct {
|
||||
w fyne.Widget
|
||||
}
|
||||
|
||||
func newPanelCard(slot int, card izapple2.Card) *panelCard {
|
||||
var pc panelCard
|
||||
|
||||
form := widget.NewForm()
|
||||
form.Append("slot", widget.NewLabel(strconv.Itoa(slot)))
|
||||
|
||||
info := card.GetInfo()
|
||||
if info != nil {
|
||||
for k, v := range info {
|
||||
form.Append(k, widget.NewLabel(v))
|
||||
}
|
||||
}
|
||||
|
||||
pc.w = widget.NewCard(card.GetName(), "", form)
|
||||
return &pc
|
||||
}
|
33
frontend/a2fyne/panelDevices.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
)
|
||||
|
||||
type panelDevices struct {
|
||||
s *state
|
||||
w *fyne.Container
|
||||
joystick *panelJoystick
|
||||
}
|
||||
|
||||
func newPanelDevices(s *state) *panelDevices {
|
||||
var pd panelDevices
|
||||
pd.s = s
|
||||
pd.w = container.NewMax()
|
||||
c := container.NewVBox()
|
||||
|
||||
pd.joystick = newPanelJoystick()
|
||||
c.Add(pd.joystick.w)
|
||||
|
||||
var cards = s.a.GetCards()
|
||||
for i, card := range cards {
|
||||
if card != nil && card.GetName() != "" {
|
||||
c.Add(newPanelCard(i, card).w)
|
||||
}
|
||||
}
|
||||
|
||||
pd.w.Add(container.NewVScroll(c))
|
||||
|
||||
return &pd
|
||||
}
|
51
frontend/a2fyne/panelJoystick.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type panelJoystick struct {
|
||||
w fyne.Widget
|
||||
labelJoy1 *widget.Label
|
||||
labelJoy2 *widget.Label
|
||||
}
|
||||
|
||||
const textJoystickNotAvailable = "unplugged"
|
||||
|
||||
func newPanelJoystick() *panelJoystick {
|
||||
var pj panelJoystick
|
||||
|
||||
pj.labelJoy1 = widget.NewLabel("")
|
||||
pj.labelJoy2 = widget.NewLabel("")
|
||||
pj.w = widget.NewCard(
|
||||
"Joysticks",
|
||||
"",
|
||||
widget.NewForm(
|
||||
widget.NewFormItem("Joystick 1", pj.labelJoy1),
|
||||
widget.NewFormItem("Joystick 2", pj.labelJoy2),
|
||||
),
|
||||
)
|
||||
|
||||
return &pj
|
||||
}
|
||||
|
||||
func (pj *panelJoystick) updateJoy1(info *joystickInfo) {
|
||||
newName := textJoystickNotAvailable
|
||||
if info != nil {
|
||||
newName = info.name
|
||||
}
|
||||
if newName != pj.labelJoy1.Text {
|
||||
pj.labelJoy1.SetText(newName)
|
||||
}
|
||||
}
|
||||
|
||||
func (pj *panelJoystick) updateJoy2(info *joystickInfo) {
|
||||
newName := textJoystickNotAvailable
|
||||
if info != nil {
|
||||
newName = info.name
|
||||
}
|
||||
if newName != pj.labelJoy2.Text {
|
||||
pj.labelJoy2.SetText(newName)
|
||||
}
|
||||
}
|
75
frontend/a2fyne/resources.go
Normal file
BIN
frontend/a2fyne/resources/apple2.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
1
frontend/a2fyne/resources/camera.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" /></svg>
|
After Width: | Height: | Size: 518 B |
1
frontend/a2fyne/resources/caps-lock.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M9.96,13.71L12,8.29L14.03,13.72M11.14,6L6.43,18H8.36L9.32,15.43H14.68L15.64,18H17.57L12.86,6H11.14M20,2H4C2.89,2 2,2.89 2,4V20C2,21.11 2.9,22 4,22H20C21.11,22 22,21.11 22,20V4C22,2.89 21.1,2 20,2M20,20H4V4H20V20Z" /></svg>
|
After Width: | Height: | Size: 507 B |
1
frontend/a2fyne/resources/fast-forward.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,6V18L21.5,12M4,18L12.5,12L4,6V18Z" /></svg>
|
After Width: | Height: | Size: 332 B |