Compare commits
327 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 | ||
|
209191af72 | ||
|
aa46cc48e5 | ||
|
22b15bf89b | ||
|
1f1b092663 | ||
|
8673ea3b37 | ||
|
5966be84a5 | ||
|
5847a0f8a5 | ||
|
b9f893f96b | ||
|
53e7a07a46 | ||
|
bd44cbbd07 | ||
|
88ade57b1b | ||
|
9e2fd7e824 | ||
|
5523c0429a | ||
|
e14b26f876 | ||
|
a27ab17766 | ||
|
9ad13ae483 | ||
|
212ed2a90a | ||
|
985b3d6a7a | ||
|
d45b39b180 | ||
|
e833d71fad | ||
|
0a1751cc77 | ||
|
5b8409413c | ||
|
202f7b36ce | ||
|
fd13326367 | ||
|
74e0cc4768 | ||
|
ea5ee95807 | ||
|
20d9b2ed10 | ||
|
c91e9d43f4 | ||
|
4f466826b9 | ||
|
37e6f3024f | ||
|
fce719deb8 | ||
|
4bf8531ae4 | ||
|
a8f22224a8 | ||
|
8ea3abe2ff | ||
|
3dbb90d0ca | ||
|
4d6b02e4d6 | ||
|
862b087d4e | ||
|
a28745ab83 | ||
|
09117fd7c5 | ||
|
3660e0ae98 | ||
|
3618cbb9c9 | ||
|
9f2a026fa3 | ||
|
5a3dcc60bb | ||
|
d5433ad8d9 | ||
|
61dae23726 | ||
|
3f7e3b3013 | ||
|
eeebf1bb0f | ||
|
1d0a3e0b34 | ||
|
472f8a5c51 | ||
|
f511a9d0e0 | ||
|
b89f88d879 | ||
|
6917a21f38 | ||
|
c65d8585d4 | ||
|
df41114043 | ||
|
f92bc2b08e | ||
|
b1b55f4c0b | ||
|
cb844611d9 | ||
|
37005b16e9 | ||
|
de8ee829ee | ||
|
c10a6f7e8a | ||
|
e14577d501 | ||
|
b930cf5fd6 | ||
|
00e4476e86 | ||
|
3d21387d47 | ||
|
68b949d871 | ||
|
43a5980fd3 | ||
|
4d7447f7b4 | ||
|
3d75002588 | ||
|
67a92895b3 | ||
|
f53b849a52 | ||
|
018ff935e6 | ||
|
7f5bf6b47f | ||
|
049e80acd7 | ||
|
047233f16b | ||
|
68d68eadfc | ||
|
4b655b40b8 | ||
|
d079ec3d2b | ||
|
b847cd34e6 | ||
|
ded7c10ed3 | ||
|
3d040114dc | ||
|
fc9e4011f9 |
11
.circleci/config.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- 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 ./...
|
18
.gitignore
vendored
Normal file
|
@ -0,0 +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
|
312
README.md
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
# izapple2 - Apple ][+, //e emulator
|
||||||
|
|
||||||
|
Portable emulator of an Apple II+ or //e. Written in Go.
|
||||||
|
|
||||||
|
## 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 paged ROM
|
||||||
|
- Storage
|
||||||
|
- 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 (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/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 //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):
|
||||||
|
|
||||||
|
``` 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):
|
||||||
|
|
||||||
|
``` 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 `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
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# #
|
||||||
|
# APPLE II #
|
||||||
|
# #
|
||||||
|
# DOS VERSION 3.3 SYSTEM MASTER #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# JANUARY 1, 1983 #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# COPYRIGHT APPLE COMPUTER,INC. 1980,1982 #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# ]10 PRINT "HELLO WORLD" #
|
||||||
|
# #
|
||||||
|
# ]LIST #
|
||||||
|
# #
|
||||||
|
# 10 PRINT "HELLO WORLD" #
|
||||||
|
# #
|
||||||
|
# ]RUN #
|
||||||
|
# HELLO WORLD #
|
||||||
|
# #
|
||||||
|
# ]_ #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
############################################
|
||||||
|
Line:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command line options
|
||||||
|
|
||||||
|
<!-- 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 (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
|
||||||
|
|
||||||
|
```
|
||||||
|
<!-- doc/usage.txt end -->
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
``` terminal
|
||||||
|
git clone github.com/ivanizag/izapple2
|
||||||
|
cd izapple2\frontend\a2sdl
|
||||||
|
go build .
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
``` 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")
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
255
apple2.go
|
@ -1,196 +1,101 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"sync/atomic"
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ivanizag/apple2/core6502"
|
"github.com/ivanizag/iz6502"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Apple2 represents all the components and state of the emulated machine
|
// Apple2 represents all the components and state of the emulated machine
|
||||||
type Apple2 struct {
|
type Apple2 struct {
|
||||||
cpu *core6502.State
|
Name string
|
||||||
mmu *memoryManager
|
cpu *iz6502.State
|
||||||
io *ioC0Page
|
mmu *memoryManager
|
||||||
cg *CharacterGenerator
|
io *ioC0Page
|
||||||
cards [8]card
|
cg *CharacterGenerator
|
||||||
isApple2e bool
|
cards [8]Card
|
||||||
panicSS bool
|
tracers []executionTracer
|
||||||
commandChannel chan int
|
|
||||||
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
|
softVideoSwitch *SoftVideoSwitch
|
||||||
isColor bool
|
board string
|
||||||
fastMode bool
|
isApple2e bool
|
||||||
fastRequestsCounter int
|
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 (
|
// GetCards returns the array of inserted cards
|
||||||
// CpuClockMhz is the actual Apple II clock speed
|
func (a *Apple2) GetCards() [8]Card {
|
||||||
CpuClockMhz = 14.318 / 14
|
return a.cards
|
||||||
cpuClockEuroMhz = 14.238 / 14
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxWaitDuration = 100 * time.Millisecond
|
|
||||||
|
|
||||||
// Run starts the Apple2 emulation
|
|
||||||
func (a *Apple2) Run(log bool) {
|
|
||||||
// Start the processor
|
|
||||||
a.cpu.Reset()
|
|
||||||
referenceTime := time.Now()
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Run a 6502 step
|
|
||||||
a.cpu.ExecuteInstruction(log && a.cycleDurationNs != 0)
|
|
||||||
|
|
||||||
// Execute meta commands
|
|
||||||
commandsPending := true
|
|
||||||
for commandsPending {
|
|
||||||
select {
|
|
||||||
case command := <-a.commandChannel:
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// SetKeyboardProvider attaches an external keyboard provider
|
||||||
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
|
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
|
||||||
CommandToggleSpeed = iota + 1
|
a.io.setKeyboardProvider(kb)
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
// SendCommand enqueues a command to the emulator thread
|
|
||||||
func (a *Apple2) SendCommand(command int) {
|
|
||||||
a.commandChannel <- command
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Apple2) executeCommand(command int) {
|
// SetSpeakerProvider attaches an external keyboard provider
|
||||||
switch command {
|
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
|
||||||
case CommandToggleSpeed:
|
a.io.setSpeakerProvider(s)
|
||||||
if a.cycleDurationNs == 0 {
|
|
||||||
fmt.Println("Slow")
|
|
||||||
a.cycleDurationNs = 1000.0 / CpuClockMhz
|
|
||||||
} else {
|
|
||||||
fmt.Println("Fast")
|
|
||||||
a.cycleDurationNs = 0
|
|
||||||
}
|
|
||||||
case CommandToggleColor:
|
|
||||||
a.isColor = !a.isColor
|
|
||||||
case CommandSaveState:
|
|
||||||
fmt.Println("Saving state")
|
|
||||||
a.save("apple2.state")
|
|
||||||
case CommandLoadState:
|
|
||||||
fmt.Println("Loading state")
|
|
||||||
a.load("apple2.state")
|
|
||||||
case CommandDumpDebugInfo:
|
|
||||||
a.dumpDebugInfo()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Apple2) requestFastMode() {
|
// SetJoysticksProvider attaches an external joysticks provider
|
||||||
|
func (a *Apple2) SetJoysticksProvider(j JoysticksProvider) {
|
||||||
|
a.io.setJoysticksProvider(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain.
|
||||||
if a.fastMode {
|
atomic.AddInt32(&a.fastRequestsCounter, 1)
|
||||||
a.fastRequestsCounter++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Apple2) releaseFastMode() {
|
func (a *Apple2) ReleaseFastMode() {
|
||||||
if a.fastMode {
|
atomic.AddInt32(&a.fastRequestsCounter, -1)
|
||||||
a.fastRequestsCounter--
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type persistent interface {
|
func (a *Apple2) registerRemovableMediaDrive(d drive) {
|
||||||
save(io.Writer)
|
a.removableMediaDrives = append(a.removableMediaDrives, d)
|
||||||
load(io.Reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Apple2) save(filename string) {
|
|
||||||
f, err := os.Create(filename)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
w := bufio.NewWriter(f)
|
|
||||||
defer w.Flush()
|
|
||||||
|
|
||||||
a.cpu.Save(w)
|
|
||||||
a.mmu.save(w)
|
|
||||||
a.io.save(w)
|
|
||||||
binary.Write(w, binary.BigEndian, a.isColor)
|
|
||||||
binary.Write(w, binary.BigEndian, a.fastMode)
|
|
||||||
binary.Write(w, binary.BigEndian, a.fastRequestsCounter)
|
|
||||||
|
|
||||||
for _, c := range a.cards {
|
|
||||||
if c != nil {
|
|
||||||
c.save(w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Apple2) load(filename string) {
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
// Ignore error if can't load the file
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
r := bufio.NewReader(f)
|
|
||||||
|
|
||||||
a.cpu.Load(r)
|
|
||||||
a.mmu.load(r)
|
|
||||||
a.io.load(r)
|
|
||||||
binary.Read(r, binary.BigEndian, &a.isColor)
|
|
||||||
binary.Read(r, binary.BigEndian, &a.fastMode)
|
|
||||||
binary.Read(r, binary.BigEndian, &a.fastRequestsCounter)
|
|
||||||
|
|
||||||
for _, c := range a.cards {
|
|
||||||
if c != nil {
|
|
||||||
c.load(r)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,79 +0,0 @@
|
||||||
package apple2
|
|
||||||
|
|
||||||
import "github.com/ivanizag/apple2/core6502"
|
|
||||||
|
|
||||||
// NewApple2 instantiates an apple2
|
|
||||||
func NewApple2(romFile string, charRomFile string, clockMhz float64,
|
|
||||||
isColor bool, fastMode bool, panicSS bool) *Apple2 {
|
|
||||||
var a Apple2
|
|
||||||
a.mmu = newMemoryManager(&a, romFile)
|
|
||||||
a.cpu = core6502.NewNMOS6502(a.mmu)
|
|
||||||
if charRomFile != "" {
|
|
||||||
a.cg = NewCharacterGenerator(charRomFile)
|
|
||||||
}
|
|
||||||
a.commandChannel = make(chan int, 100)
|
|
||||||
a.isColor = isColor
|
|
||||||
a.fastMode = fastMode
|
|
||||||
a.panicSS = panicSS
|
|
||||||
|
|
||||||
if clockMhz <= 0 {
|
|
||||||
// Full speed
|
|
||||||
a.cycleDurationNs = 0
|
|
||||||
} else {
|
|
||||||
a.cycleDurationNs = 1000.0 / clockMhz
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the io in 0xc000
|
|
||||||
a.io = newIoC0Page(&a)
|
|
||||||
a.mmu.setPages(0xc0, 0xc0, a.io)
|
|
||||||
|
|
||||||
return &a
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Apple2) insertCard(c card, slot int) {
|
|
||||||
c.assign(a, slot)
|
|
||||||
a.cards[slot] = c
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddDisk2 insterts a DiskII controller
|
|
||||||
func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) {
|
|
||||||
var c cardDisk2
|
|
||||||
c.loadRom(diskRomFile)
|
|
||||||
a.insertCard(&c, slot)
|
|
||||||
|
|
||||||
if diskImage != "" {
|
|
||||||
diskette := loadDisquette(diskImage)
|
|
||||||
//diskette.saveNib(diskImage + "bak")
|
|
||||||
c.drive[0].insertDiskette(diskette)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
package apple2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MainApple is a device independant main. Video, keyboard and speaker won't be defined
|
|
||||||
func MainApple() *Apple2 {
|
|
||||||
romFile := flag.String(
|
|
||||||
"rom",
|
|
||||||
"<internal>/Apple2_Plus.rom",
|
|
||||||
"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")
|
|
||||||
cpuClock := flag.Float64(
|
|
||||||
"mhz",
|
|
||||||
CpuClockMhz,
|
|
||||||
"cpu speed in Mhz, use 0 for full speed. Use F5 to toggle.")
|
|
||||||
charRomFile := flag.String(
|
|
||||||
"charRom",
|
|
||||||
"<internal>/Apple2rev7CharGen.rom",
|
|
||||||
"rom file for the disk drive controller")
|
|
||||||
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")
|
|
||||||
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")
|
|
||||||
dumpChars := flag.Bool(
|
|
||||||
"dumpChars",
|
|
||||||
false,
|
|
||||||
"shows the character map",
|
|
||||||
)
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *dumpChars {
|
|
||||||
cg := NewCharacterGenerator(*charRomFile)
|
|
||||||
cg.Dump()
|
|
||||||
os.Exit(0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
a := NewApple2(*romFile, *charRomFile, *cpuClock, !*mono, *fastDisk, *panicSS)
|
|
||||||
if *languageCardSlot >= 0 {
|
|
||||||
a.AddLanguageCard(*languageCardSlot)
|
|
||||||
}
|
|
||||||
if *saturnCardSlot >= 0 {
|
|
||||||
a.AddSaturnCard(*saturnCardSlot)
|
|
||||||
}
|
|
||||||
if *disk2Slot >= 0 {
|
|
||||||
a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage)
|
|
||||||
}
|
|
||||||
|
|
||||||
//a.AddCardInOut(2)
|
|
||||||
//a.AddCardLogger(4)
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/ivanizag/apple2"
|
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
a := apple2.MainApple()
|
|
||||||
SDLRun(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SDLRun starts the Apple2 emulator on SDL
|
|
||||||
func SDLRun(a *apple2.Apple2) {
|
|
||||||
s := newSDLSpeaker()
|
|
||||||
s.start()
|
|
||||||
|
|
||||||
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("Apple2")
|
|
||||||
|
|
||||||
kp := newSDLKeyBoard(a)
|
|
||||||
a.SetKeyboardProvider(kp)
|
|
||||||
a.SetSpeakerProvider(s)
|
|
||||||
go a.Run(false)
|
|
||||||
|
|
||||||
running := true
|
|
||||||
for running {
|
|
||||||
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
|
||||||
switch t := event.(type) {
|
|
||||||
case *sdl.QuitEvent:
|
|
||||||
running = false
|
|
||||||
case *sdl.KeyboardEvent:
|
|
||||||
//fmt.Printf("[%d ms] Keyboard\ttype:%d\tsym:%c\tmodifiers:%d\tstate:%d\trepeat:%d\n",
|
|
||||||
// t.Timestamp, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat)
|
|
||||||
kp.putKey(t)
|
|
||||||
case *sdl.TextInputEvent:
|
|
||||||
//fmt.Printf("[%d ms] TextInput\ttype:%d\texts:%s\n",
|
|
||||||
// t.Timestamp, t.Type, t.GetText())
|
|
||||||
kp.putText(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,126 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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
|
|
||||||
|
|
||||||
Missing Reset button
|
|
||||||
*/
|
|
||||||
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_DELETE:
|
|
||||||
result = 24
|
|
||||||
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
|
|
||||||
|
|
||||||
// Base64A clone particularities
|
|
||||||
case sdl.K_F2:
|
|
||||||
result = 127
|
|
||||||
case sdl.K_UP:
|
|
||||||
result = 31
|
|
||||||
case sdl.K_DOWN:
|
|
||||||
result = 10
|
|
||||||
|
|
||||||
// Control of the emulator
|
|
||||||
case sdl.K_F5:
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
97
base64a.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package izapple2
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copam BASE64A adaptation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const (
|
||||||
|
// There are 6 ROM chips. Each can have 4Kb or 8Kb. They can fill
|
||||||
|
// 2 or 4 banks with 2kb windows.
|
||||||
|
base64aRomBankSize = 12 * 1024
|
||||||
|
base64aRomBankCount = 4
|
||||||
|
base64aRomWindowSize = 2 * 1024
|
||||||
|
base64aRomChipCount = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadBase64aRom(a *Apple2) error {
|
||||||
|
// Load the 6 PROM dumps
|
||||||
|
romBanksBytes := make([][]uint8, base64aRomBankCount)
|
||||||
|
for j := range romBanksBytes {
|
||||||
|
romBanksBytes[j] = make([]uint8, 0, base64aRomBankSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < base64aRomChipCount; i++ {
|
||||||
|
filename := fmt.Sprintf("<internal>/BASE64A_%X.BIN", 0xd0+i*0x08)
|
||||||
|
data, _, err := LoadResource(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for j := range romBanksBytes {
|
||||||
|
start := (j * base64aRomWindowSize) % len(data)
|
||||||
|
romBanksBytes[j] = append(romBanksBytes[j], data[start:start+base64aRomWindowSize]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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, 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() uint8 {
|
||||||
|
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
|
||||||
|
p := rom.getPage()
|
||||||
|
rom.setPage(p & 2)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}, "ANN0OFF-ROM")
|
||||||
|
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() uint8 {
|
||||||
|
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
|
||||||
|
p := rom.getPage()
|
||||||
|
rom.setPage(p & 1)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}, "ANN1OFF-ROM")
|
||||||
|
io.addSoftSwitchRW(0x5B, func() uint8 {
|
||||||
|
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
|
||||||
|
p := rom.getPage()
|
||||||
|
rom.setPage(p | 2)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}, "ANN1ON-ROM")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func charGenColumnsMapBase64a(column int) int {
|
||||||
|
bit := column + 2
|
||||||
|
// Weird positions
|
||||||
|
if column == 6 {
|
||||||
|
bit = 2
|
||||||
|
} else if column == 0 {
|
||||||
|
bit = 1
|
||||||
|
}
|
||||||
|
return bit
|
||||||
|
}
|
196
cardBase.go
|
@ -1,49 +1,197 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type card interface {
|
// Card represents an Apple II card to be inserted in a slot
|
||||||
loadRom(filename string)
|
type Card interface {
|
||||||
|
loadRom(data []uint8, layout cardRomLayout) error
|
||||||
assign(a *Apple2, slot int)
|
assign(a *Apple2, slot int)
|
||||||
persistent
|
reset()
|
||||||
|
|
||||||
|
setName(name string)
|
||||||
|
setDebug(debug bool)
|
||||||
|
GetName() string
|
||||||
|
GetInfo() map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type cardBase struct {
|
type cardBase struct {
|
||||||
a *Apple2
|
a *Apple2
|
||||||
rom *memoryRange
|
name string
|
||||||
slot int
|
trace bool
|
||||||
ssr [16]softSwitchR
|
romCsxx *memoryRangeROM
|
||||||
ssw [16]softSwitchW
|
romC8xx memoryHandler
|
||||||
|
romCxxx memoryHandler
|
||||||
|
|
||||||
|
slot int
|
||||||
|
_ssr [16]softSwitchR
|
||||||
|
_ssw [16]softSwitchW
|
||||||
|
_ssrName [16]string
|
||||||
|
_sswName [16]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardBase) loadRom(filename string) {
|
func (c *cardBase) setName(name string) {
|
||||||
if c.a != nil {
|
c.name = name
|
||||||
panic("Rom must be loaded before inserting the card in the slot")
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
data := loadResource(filename)
|
err = c.loadRom(data, layout)
|
||||||
c.rom = newMemoryRange(0, data)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cardBase) loadRom(data []uint8, layout cardRomLayout) error {
|
||||||
|
if c.a != nil {
|
||||||
|
return fmt.Errorf("ROM must be loaded before inserting the card in the slot")
|
||||||
|
}
|
||||||
|
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) {
|
func (c *cardBase) assign(a *Apple2, slot int) {
|
||||||
c.a = a
|
c.a = a
|
||||||
c.slot = slot
|
c.slot = slot
|
||||||
if slot != 0 && c.rom != nil {
|
if slot != 0 {
|
||||||
c.rom.base = uint16(0xc000 + slot*0x100)
|
if c.romCsxx != nil {
|
||||||
a.mmu.setPagesRead(uint8(0xc0+slot), uint8(0xc0+slot), c.rom)
|
// 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++ {
|
for i := 0; i < 0x10; i++ {
|
||||||
a.io.addSoftSwitchR(uint8(0xC80+slot*0x10+i), c.ssr[i])
|
if c._ssr[i] != nil {
|
||||||
a.io.addSoftSwitchW(uint8(0xC80+slot*0x10+i), c.ssw[i])
|
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])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardBase) save(w io.Writer) {
|
func (c *cardBase) addCardSoftSwitchR(address uint8, ss softSwitchR, name string) {
|
||||||
// Empty
|
c._ssr[address] = ss
|
||||||
|
c._ssrName[address] = name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardBase) load(r io.Reader) {
|
func (c *cardBase) addCardSoftSwitchW(address uint8, ss softSwitchW, name string) {
|
||||||
// Empty
|
c._ssw[address] = ss
|
||||||
|
c._sswName[address] = name
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
299
cardDisk2.go
|
@ -1,8 +1,10 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"fmt"
|
||||||
"io"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ivanizag/izapple2/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -19,116 +21,217 @@ http://yesterbits.com/media/pubs/AppleOrchard/articles/disk-ii-part-1-1983-apr.p
|
||||||
NIB: 35 tracks 6656 bytes, 232960 bytes
|
NIB: 35 tracks 6656 bytes, 232960 bytes
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const maxHalfTrack = 68
|
|
||||||
|
|
||||||
type cardDisk2 struct {
|
// CardDisk2 is a DiskII interface card
|
||||||
|
type CardDisk2 struct {
|
||||||
cardBase
|
cardBase
|
||||||
selected int // q5, Only 0 and 1 supported
|
selected int // q5, Only 0 and 1 supported
|
||||||
|
power bool // q4
|
||||||
drive [2]cardDisk2Drive
|
drive [2]cardDisk2Drive
|
||||||
|
fastMode bool
|
||||||
|
|
||||||
dataLatch uint8
|
dataLatch uint8
|
||||||
q6 bool
|
q6 bool
|
||||||
q7 bool
|
q7 bool
|
||||||
|
|
||||||
|
trackTracer trackTracer
|
||||||
|
}
|
||||||
|
|
||||||
|
type drive interface {
|
||||||
|
insertDiskette(path string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type cardDisk2Drive struct {
|
type cardDisk2Drive struct {
|
||||||
diskette *diskette16sector
|
name string
|
||||||
currentPhase int
|
diskette storage.Diskette
|
||||||
power bool // q4
|
phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor
|
||||||
halfTrack int
|
trackStep int // Stepmotor for tracks position. 4 steps per track
|
||||||
position int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func newCardDisk2Builder() *cardBuilder {
|
||||||
diskBitCycle = 4 // There is a dataLatch bit transferred every 4 cycles
|
return &cardBuilder{
|
||||||
diskLatchReadCycles = 7 // Loaded data is available for a little more than 7ns
|
name: "Disk II",
|
||||||
diskWriteByteCycle = 32 // Load data to write every 32 cycles
|
description: "Disk II interface card",
|
||||||
diskWriteSelfSyncCycle = 40 // Save $FF every 40 cycles. Self sync is 10 bits: 1111 1111 00
|
defaultParams: &[]paramSpec{
|
||||||
diskMotorStartMs = 150 // Time with the disk spinning to get full speed
|
{"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,
|
// Q1, Q2, Q3 and Q4 phase control soft switches,
|
||||||
for i := 0; i < 4; i++ {
|
for i := uint8(0); i < 4; i++ {
|
||||||
phase := i
|
phase := i
|
||||||
c.ssr[phase<<1] = func(_ *ioC0Page) uint8 {
|
c.addCardSoftSwitchRW(phase<<1, func() uint8 {
|
||||||
return c.dataLatch // All even addresses return the last dataLatch
|
// Update magnets and position
|
||||||
}
|
|
||||||
c.ssr[(phase<<1)+1] = func(_ *ioC0Page) uint8 {
|
|
||||||
// Move the head up or down depending on the previous phase.
|
|
||||||
drive := &c.drive[c.selected]
|
drive := &c.drive[c.selected]
|
||||||
delta := (phase - drive.currentPhase + 4) % 4
|
drive.phases &^= (1 << phase)
|
||||||
switch delta {
|
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
|
||||||
case 1: // Up
|
|
||||||
drive.halfTrack++
|
if c.trackTracer != nil {
|
||||||
case 2: // Illegal, let's say up
|
c.trackTracer.traceTrack(drive.trackStep, c.slot, c.selected)
|
||||||
drive.halfTrack++
|
|
||||||
case 3: // Down
|
|
||||||
drive.halfTrack--
|
|
||||||
case 0: // No chamge
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't go over the limits
|
return c.dataLatch // All even addresses return the last dataLatch
|
||||||
if drive.halfTrack > maxHalfTrack {
|
}, fmt.Sprintf("PHASE%vOFF", phase))
|
||||||
drive.halfTrack = maxHalfTrack
|
|
||||||
} else if drive.halfTrack < 0 {
|
c.addCardSoftSwitchRW((phase<<1)+1, func() uint8 { // Update magnets and position
|
||||||
drive.halfTrack = 0
|
drive := &c.drive[c.selected]
|
||||||
|
drive.phases |= (1 << phase)
|
||||||
|
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
|
||||||
|
|
||||||
|
if c.trackTracer != nil {
|
||||||
|
c.trackTracer.traceTrack(drive.trackStep, slot, c.selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
drive.currentPhase = phase
|
|
||||||
//fmt.Printf("DISKII: Current halftrack is %v\n", drive.halfTrack)
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}, fmt.Sprintf("PHASE%vON", phase))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Q4, power switch
|
// Q4, power switch
|
||||||
c.ssr[0x8] = func(_ *ioC0Page) uint8 {
|
c.addCardSoftSwitchRW(0x8, func() uint8 {
|
||||||
if c.drive[c.selected].power {
|
c.softSwitchQ4(false)
|
||||||
c.drive[c.selected].power = false
|
|
||||||
c.a.releaseFastMode()
|
|
||||||
}
|
|
||||||
return c.dataLatch
|
return c.dataLatch
|
||||||
}
|
}, "Q4DRIVEOFF")
|
||||||
c.ssr[0x9] = func(_ *ioC0Page) uint8 {
|
c.addCardSoftSwitchRW(0x9, func() uint8 {
|
||||||
if !c.drive[c.selected].power {
|
c.softSwitchQ4(true)
|
||||||
c.drive[c.selected].power = true
|
|
||||||
c.a.requestFastMode()
|
|
||||||
}
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}, "Q4DRIVEON")
|
||||||
|
|
||||||
// Q5, drive selecion
|
// Q5, drive selecion
|
||||||
c.ssr[0xA] = func(_ *ioC0Page) uint8 {
|
c.addCardSoftSwitchRW(0xA, func() uint8 {
|
||||||
c.selected = 0
|
c.softSwitchQ5(0)
|
||||||
return c.dataLatch
|
return c.dataLatch
|
||||||
}
|
}, "Q5SELECT1")
|
||||||
c.ssr[0xB] = func(_ *ioC0Page) uint8 {
|
c.addCardSoftSwitchRW(0xB, func() uint8 {
|
||||||
c.selected = 1
|
c.softSwitchQ5(1)
|
||||||
return 0
|
return 0
|
||||||
}
|
}, "Q5SELECT2")
|
||||||
|
|
||||||
// Q6, Q7
|
// Q6, Q7
|
||||||
for i := 0xC; i <= 0xF; i++ {
|
for i := uint8(0xC); i <= 0xF; i++ {
|
||||||
iCopy := i
|
iCopy := i
|
||||||
c.ssr[iCopy] = func(_ *ioC0Page) uint8 {
|
c.addCardSoftSwitchR(iCopy, func() uint8 {
|
||||||
return c.softSwitchQ6Q7(iCopy, 0)
|
return c.softSwitchQ6Q7(iCopy, 0)
|
||||||
}
|
}, "Q6Q7")
|
||||||
c.ssw[iCopy] = func(_ *ioC0Page, value uint8) {
|
c.addCardSoftSwitchW(iCopy, func(value uint8) {
|
||||||
c.softSwitchQ6Q7(iCopy, value)
|
c.softSwitchQ6Q7(iCopy, value)
|
||||||
}
|
}, "Q6Q7")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.cardBase.assign(a, slot)
|
c.cardBase.assign(a, slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardDisk2) softSwitchQ6Q7(index int, in uint8) uint8 {
|
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 {
|
||||||
switch index {
|
switch index {
|
||||||
case 0xC: // Q6L
|
case 0xC: // Q6L
|
||||||
c.q6 = false
|
c.q6 = false
|
||||||
case 0xD: // Q6H
|
case 0xD: // Q6H
|
||||||
c.q6 = true
|
c.q6 = true
|
||||||
case 0xE: // Q/L
|
case 0xE: // Q7L
|
||||||
c.q7 = false
|
c.q7 = false
|
||||||
case 0xF: // Q7H
|
case 0xF: // Q7H
|
||||||
c.q7 = true
|
c.q7 = true
|
||||||
|
@ -138,25 +241,22 @@ func (c *cardDisk2) softSwitchQ6Q7(index int, in uint8) uint8 {
|
||||||
if index&1 == 0 {
|
if index&1 == 0 {
|
||||||
// All even addresses return the last dataLatch
|
// All even addresses return the last dataLatch
|
||||||
return c.dataLatch
|
return c.dataLatch
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardDisk2) processQ6Q7(in uint8) {
|
func (c *CardDisk2) processQ6Q7(in uint8) {
|
||||||
d := &c.drive[c.selected]
|
d := &c.drive[c.selected]
|
||||||
if d.diskette == nil {
|
if d.diskette == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !c.q6 {
|
if !c.q6 { // shift
|
||||||
if !c.q7 { // Q6L-Q7L: Read
|
if !c.q7 { // Q6L-Q7L: Read
|
||||||
track := d.halfTrack / 2
|
c.dataLatch = d.diskette.Read(d.trackStep, c.a.cpu.GetCycles())
|
||||||
c.dataLatch, d.position = d.diskette.read(track, d.position)
|
|
||||||
} else { // Q6L-Q7H: Write the dataLatch value to disk. Shift data out
|
} else { // Q6L-Q7H: Write the dataLatch value to disk. Shift data out
|
||||||
track := d.halfTrack / 2
|
d.diskette.Write(d.trackStep, c.dataLatch, c.a.cpu.GetCycles())
|
||||||
d.position = d.diskette.write(track, d.position, c.dataLatch)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else { // load
|
||||||
if !c.q7 { // Q6H-Q7L: Sense write protect / prewrite state
|
if !c.q7 { // Q6H-Q7L: Sense write protect / prewrite state
|
||||||
// Bit 7 of the control status register means write protected
|
// Bit 7 of the control status register means write protected
|
||||||
c.dataLatch = 0 // Never write protected
|
c.dataLatch = 0 // Never write protected
|
||||||
|
@ -164,40 +264,21 @@ func (c *cardDisk2) processQ6Q7(in uint8) {
|
||||||
c.dataLatch = in
|
c.dataLatch = in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if c.dataLatch >= 0x80 {
|
||||||
|
fmt.Printf("Datalach: 0x%.2x in cycle %v\n", c.dataLatch, c.a.cpu.GetCycles())
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *cardDisk2Drive) insertDiskette(dt *diskette16sector) {
|
func (d *cardDisk2Drive) insertDiskette(name string) error {
|
||||||
d.diskette = dt
|
diskette, err := LoadDiskette(name)
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *cardDisk2) save(w io.Writer) {
|
d.name = name
|
||||||
binary.Write(w, binary.BigEndian, c.selected)
|
d.diskette = diskette
|
||||||
binary.Write(w, binary.BigEndian, c.dataLatch)
|
return nil
|
||||||
binary.Write(w, binary.BigEndian, c.q6)
|
|
||||||
binary.Write(w, binary.BigEndian, c.q7)
|
|
||||||
c.drive[0].save(w)
|
|
||||||
c.drive[1].save(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardDisk2) load(r io.Reader) {
|
|
||||||
binary.Read(r, binary.BigEndian, &c.selected)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.dataLatch)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.q6)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.q7)
|
|
||||||
c.drive[0].load(r)
|
|
||||||
c.drive[1].load(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *cardDisk2Drive) save(w io.Writer) {
|
|
||||||
binary.Write(w, binary.BigEndian, d.currentPhase)
|
|
||||||
binary.Write(w, binary.BigEndian, d.power)
|
|
||||||
binary.Write(w, binary.BigEndian, d.halfTrack)
|
|
||||||
binary.Write(w, binary.BigEndian, d.position)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *cardDisk2Drive) load(r io.Reader) {
|
|
||||||
binary.Read(r, binary.BigEndian, &d.currentPhase)
|
|
||||||
binary.Read(r, binary.BigEndian, &d.power)
|
|
||||||
binary.Read(r, binary.BigEndian, &d.halfTrack)
|
|
||||||
binary.Read(r, binary.BigEndian, &d.position)
|
|
||||||
}
|
}
|
||||||
|
|
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)
|
||||||
|
}
|
100
cardFastChip.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package izapple2
|
||||||
|
|
||||||
|
/*
|
||||||
|
Simulates just what is needed to make Total Replay use fast mode. Can change
|
||||||
|
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
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CardFastChip represents a
|
||||||
|
type CardFastChip struct {
|
||||||
|
cardBase
|
||||||
|
unlocked bool
|
||||||
|
unlockCounter uint8
|
||||||
|
enabled bool
|
||||||
|
accelerated bool
|
||||||
|
configRegister uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
fastChipUnlockToken = 0x6a
|
||||||
|
fastChipUnlockRepeats = 4
|
||||||
|
fastChipNormalSpeed = uint8(9)
|
||||||
|
)
|
||||||
|
|
||||||
|
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(value uint8) {
|
||||||
|
if value == fastChipUnlockToken {
|
||||||
|
c.unlockCounter++
|
||||||
|
if c.unlockCounter >= fastChipUnlockRepeats {
|
||||||
|
c.unlocked = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.unlockCounter = 0
|
||||||
|
c.unlocked = false
|
||||||
|
c.enabled = false
|
||||||
|
}
|
||||||
|
}, "FASTCHIP-LOCK")
|
||||||
|
|
||||||
|
a.io.addSoftSwitchW(0x6b, func(uint8) {
|
||||||
|
if c.unlocked {
|
||||||
|
c.enabled = true
|
||||||
|
}
|
||||||
|
}, "FASTCHIP-ENABLE")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
218
cardInOut.go
|
@ -1,7 +1,9 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -11,137 +13,163 @@ See:
|
||||||
"Apple II Monitors peeled."
|
"Apple II Monitors peeled."
|
||||||
http://mysite.du.edu/~etuttle/math/acia.htm
|
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
|
cardBase
|
||||||
i int
|
reader *bufio.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardInOut) assign(a *Apple2, slot int) {
|
func newCardInOutBuilder() *cardBuilder {
|
||||||
for i := 0x0; i <= 0xf; i++ {
|
return &cardBuilder{
|
||||||
iCopy := i
|
name: "InOut test card",
|
||||||
c.ssr[i] = func(*ioC0Page) uint8 {
|
description: "Card to test I/O",
|
||||||
value := []uint8{0xc1, 0xc1, 0x93, 0x0}[c.i%4]
|
buildFunc: func(params map[string]string) (Card, error) {
|
||||||
c.i++
|
return &CardInOut{}, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
c.ssw[i] = func(_ *ioC0Page, value uint8) {
|
|
||||||
fmt.Printf("[cardInOut] Write access to softswith 0x%x for slot %v, value 0x%x.\n", iCopy, slot, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
in := true
|
func (c *CardInOut) assign(a *Apple2, slot int) {
|
||||||
out := false
|
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{
|
data := [256]uint8{
|
||||||
// Register
|
// Register
|
||||||
0xA9, 0xC2,
|
0x4c, 0x40, 0xc2, 0, 0, 0, 0, 0,
|
||||||
0x85, 0x37,
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0x85, 0x39,
|
|
||||||
0xA9, 0x10,
|
|
||||||
0x85, 0x36,
|
|
||||||
0xA9, 0x15,
|
|
||||||
0x85, 0x38,
|
|
||||||
0x60, 0xEA,
|
|
||||||
|
|
||||||
// Out char
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0x8D, 0xA1, 0xC0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0x60, 0xEA,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
|
||||||
// Get char
|
0x48, 0xA5, 0x38, 0xD0, 0x11, 0xA9, 0xC2, 0xC5,
|
||||||
0x91, 0x28,
|
0x39, 0xD0, 0x0B, 0xAD, 0x4F, 0x85, 0x38, 0x68,
|
||||||
0xAD, 0xA0, 0xC0,
|
|
||||||
0x60,
|
0x91, 0x28, 0xAD, 0xA0, 0xC0, 0x60, 0x68, 0x8D,
|
||||||
|
0xA1, 0xC0, 0x60,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !out {
|
// Fix slot dependant addresses
|
||||||
// NOP the CSWL,H change
|
data[0x02] = uint8(0xc0 + slot)
|
||||||
for _, v := range []uint8{2, 3, 8, 9} {
|
data[0x46] = uint8(0xc0 + slot)
|
||||||
data[v] = 0xEA
|
data[0x53] = uint8(0x80 + slot<<4)
|
||||||
}
|
data[0x58] = uint8(0x81 + slot<<4)
|
||||||
}
|
|
||||||
|
|
||||||
if !in {
|
return data[:]
|
||||||
// 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("Only slot 2 supported for the InOut card")
|
|
||||||
}
|
|
||||||
c.cardBase.assign(a, slot)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The ROM code was assembled using https://www.masswerk.at/6502/assembler.html
|
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:
|
src:
|
||||||
BASL = $28
|
BASL = $28
|
||||||
CSWL = $36
|
|
||||||
CSWH = $37
|
|
||||||
KSWL = $38
|
KSWL = $38
|
||||||
KSWH = $39
|
KSWH = $39
|
||||||
|
|
||||||
* = $C200
|
* = $C200
|
||||||
Register:
|
Entry:
|
||||||
|
JMP SkipHeader
|
||||||
|
|
||||||
|
* = $C240
|
||||||
|
SkipHeader:
|
||||||
|
PHA
|
||||||
|
LDA *KSWL
|
||||||
|
BNE PREntry
|
||||||
LDA #$C2
|
LDA #$C2
|
||||||
STA *CSWH
|
CMP *KSWH
|
||||||
STA *KSWH
|
BNE PREntry
|
||||||
LDA #$10
|
FixKSWL:
|
||||||
STA *CSWL
|
LDA #<INEntry
|
||||||
LDA #$15
|
|
||||||
STA *KSWL
|
STA *KSWL
|
||||||
RTS
|
INEntry:
|
||||||
NOP
|
PLA
|
||||||
OutChar:
|
|
||||||
STA $C0A1
|
|
||||||
RTS
|
|
||||||
NOP
|
|
||||||
GetChar:
|
|
||||||
STA (BASL),Y
|
STA (BASL),Y
|
||||||
LDA $C0A0
|
LDA $C0A0
|
||||||
RTS
|
RTS
|
||||||
|
PREntry:
|
||||||
|
PLA
|
||||||
|
STA $C0A1
|
||||||
|
RTS
|
||||||
|
|
||||||
|
|
||||||
assembled as:
|
|
||||||
|
Listing:
|
||||||
|
pass 2
|
||||||
|
|
||||||
0000 BASL = 0028
|
0000 BASL = 0028
|
||||||
0000 CSWL = 0036
|
|
||||||
0000 CSWH = 0037
|
|
||||||
0000 KSWL = 0038
|
0000 KSWL = 0038
|
||||||
0000 KSWH = 0039
|
0000 KSWH = 0039
|
||||||
|
|
||||||
* = $C200
|
* = $C200
|
||||||
C200 REGIST
|
C200 ENTRY:
|
||||||
C200 LDA #$C2 A9 C2
|
C200 JMP SKIPHE 4C 40 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
|
|
||||||
|
|
||||||
object code:
|
* = $C240
|
||||||
A9 C2 85 37 85 39 A9 10
|
C240 SKIPHE
|
||||||
85 36 A9 15 85 38 60 EA
|
C240 PHA 48
|
||||||
8D A1 C0 60 EA 91 28 AD
|
C241 LDA *KSWL A5 38
|
||||||
A0 C0 60
|
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
|
||||||
*/
|
*/
|
||||||
|
|
139
cardLanguage.go
|
@ -1,9 +1,4 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Language card with 16 extra kb for the Apple ][ and ][+
|
Language card with 16 extra kb for the Apple ][ and ][+
|
||||||
|
@ -16,62 +11,81 @@ Note also that language cards for the Apple ][ had ROM on
|
||||||
board to replace the main board F8 ROM with Autostart. That
|
board to replace the main board F8 ROM with Autostart. That
|
||||||
was not used/needed on the Apple ][+. As this emulates the
|
was not used/needed on the Apple ][+. As this emulates the
|
||||||
Apple ][+, it is not considered. For the Plus it is often
|
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
|
"When RAM is deselected, the ROM on the Language card is selected for
|
||||||
the top 2K ($F800-$FFFF), and the ROM on the main board is selected
|
the top 2K ($F800-$FFFF), and the ROM on the main board is selected
|
||||||
for $D000-$F7FF.
|
for $D000-$F7FF.
|
||||||
|
|
||||||
Power on RESET initializes ROM to read mode and RAM to write mode,
|
Power on RESET initializes ROM to read mode and RAM to write mode,
|
||||||
and selects the second 4K bank to map $D000-$DFFF."
|
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
|
cardBase
|
||||||
readState bool
|
readState bool
|
||||||
writeState int
|
writeState uint8
|
||||||
activeBank int
|
altBank bool // false is bank1, true is bank2
|
||||||
ramBankA *memoryRange // First 4kb to map in 0xD000-0xDFFF
|
}
|
||||||
ramBankB *memoryRange // Second 4kb to map in 0xD000-0xDFFF
|
|
||||||
ramUpper *memoryRange // Upper 8kb to map in 0xE000-0xFFFF
|
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 (
|
const (
|
||||||
// Write enabling requires two sofstwitch accesses
|
// Write enabling requires two softswitch accesses
|
||||||
lcWriteDisabled = 0
|
lcWriteDisabled = 0
|
||||||
lcWriteHalfEnabled = 1
|
lcWriteHalfEnabled = 1
|
||||||
lcWriteEnabled = 2
|
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.readState = false
|
||||||
c.writeState = lcWriteEnabled
|
c.writeState = lcWriteEnabled
|
||||||
c.activeBank = 1
|
c.altBank = true // Start on bank2
|
||||||
|
|
||||||
c.ramBankA = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
a.mmu.initLanguageRAM(1)
|
||||||
c.ramBankB = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
for i := uint8(0x0); i <= 0xf; i++ {
|
||||||
c.ramUpper = newMemoryRange(0xe000, make([]uint8, 0x2000))
|
|
||||||
|
|
||||||
for i := 0x0; i <= 0xf; i++ {
|
|
||||||
iCopy := i
|
iCopy := i
|
||||||
c.ssr[i] = func(*ioC0Page) uint8 {
|
c.addCardSoftSwitchR(iCopy, func() uint8 {
|
||||||
c.ssAction(iCopy)
|
c.ssAction(iCopy, false)
|
||||||
return 0
|
return 0
|
||||||
}
|
}, "LANGCARDR")
|
||||||
c.ssw[i] = func(*ioC0Page, uint8) {
|
c.addCardSoftSwitchW(iCopy, func(uint8) {
|
||||||
// Writing resets write count (from A2AUDIT)
|
c.ssAction(iCopy, true)
|
||||||
c.writeState = lcWriteDisabled
|
}, "LANGCARDW")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.cardBase.assign(a, slot)
|
c.cardBase.assign(a, slot)
|
||||||
c.applyState()
|
c.applyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardLanguage) ssAction(ss int) {
|
func (c *CardLanguage) ssAction(ss uint8, write bool) {
|
||||||
c.activeBank = (ss >> 3) & 1
|
c.altBank = ((ss >> 3) & 1) == 0
|
||||||
action := ss & 0x3
|
action := ss & 0x3
|
||||||
switch action {
|
switch action {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -81,7 +95,9 @@ func (c *cardLanguage) ssAction(ss int) {
|
||||||
case 1:
|
case 1:
|
||||||
// ROM read, RAM write
|
// ROM read, RAM write
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState++
|
if !write {
|
||||||
|
c.writeState++
|
||||||
|
}
|
||||||
case 2:
|
case 2:
|
||||||
// ROM read, no writes
|
// ROM read, no writes
|
||||||
c.readState = false
|
c.readState = false
|
||||||
|
@ -89,7 +105,15 @@ func (c *cardLanguage) ssAction(ss int) {
|
||||||
case 3:
|
case 3:
|
||||||
//RAM read, RAM write
|
//RAM read, RAM write
|
||||||
c.readState = true
|
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 {
|
if c.writeState > lcWriteEnabled {
|
||||||
|
@ -99,51 +123,6 @@ func (c *cardLanguage) ssAction(ss int) {
|
||||||
c.applyState()
|
c.applyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardLanguage) getActiveBank() *memoryRange {
|
func (c *CardLanguage) applyState() {
|
||||||
if c.activeBank == 0 {
|
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
|
||||||
return c.ramBankA
|
|
||||||
}
|
|
||||||
return c.ramBankB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardLanguage) applyState() {
|
|
||||||
mmu := c.a.mmu
|
|
||||||
|
|
||||||
if c.readState {
|
|
||||||
mmu.setPagesRead(0xd0, 0xdf, c.getActiveBank())
|
|
||||||
mmu.setPagesRead(0xe0, 0xff, c.ramUpper)
|
|
||||||
} else {
|
|
||||||
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.writeState == lcWriteEnabled {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xdf, c.getActiveBank())
|
|
||||||
mmu.setPagesWrite(0xe0, 0xff, c.ramUpper)
|
|
||||||
} else {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xff, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardLanguage) save(w io.Writer) {
|
|
||||||
binary.Write(w, binary.BigEndian, c.readState)
|
|
||||||
binary.Write(w, binary.BigEndian, c.writeState)
|
|
||||||
binary.Write(w, binary.BigEndian, c.activeBank)
|
|
||||||
c.ramBankA.save(w)
|
|
||||||
c.ramBankB.save(w)
|
|
||||||
c.ramUpper.save(w)
|
|
||||||
|
|
||||||
c.cardBase.save(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardLanguage) load(r io.Reader) {
|
|
||||||
binary.Read(r, binary.BigEndian, &c.readState)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.writeState)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.activeBank)
|
|
||||||
c.ramBankA.load(r)
|
|
||||||
c.ramBankB.load(r)
|
|
||||||
c.ramUpper.load(r)
|
|
||||||
|
|
||||||
c.applyState()
|
|
||||||
c.cardBase.load(r)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -8,36 +8,30 @@ import (
|
||||||
Logger card. It never existed, I use it to trace accesses to the card.
|
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
|
cardBase
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardLogger) assign(a *Apple2, slot int) {
|
func newCardLoggerBuilder() *cardBuilder {
|
||||||
for i := 0x0; i <= 0xf; i++ {
|
return &cardBuilder{
|
||||||
iCopy := i
|
name: "Softswitch logger card",
|
||||||
c.ssr[i] = func(*ioC0Page) uint8 {
|
description: "Card to log softswitch accesses",
|
||||||
fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", iCopy, slot)
|
buildFunc: func(params map[string]string) (Card, error) {
|
||||||
return 0
|
return &CardLogger{}, nil
|
||||||
}
|
},
|
||||||
c.ssw[i] = func(_ *ioC0Page, value uint8) {
|
|
||||||
fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%v.\n", iCopy, slot, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
if slot != 0 {
|
|
||||||
a.mmu.setPagesRead(uint8(0xc0+slot), uint8(0xc0+slot), c)
|
|
||||||
}
|
|
||||||
c.cardBase.assign(a, slot)
|
c.cardBase.assign(a, slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryHandler implementation
|
|
||||||
func (c *cardLogger) peek(address uint16) uint8 {
|
|
||||||
fmt.Printf("[cardLogger] Read in %x.\n", address)
|
|
||||||
c.a.dumpDebugInfo()
|
|
||||||
|
|
||||||
return 0xf3
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*cardLogger) poke(address uint16, value uint8) {
|
|
||||||
fmt.Printf("[cardLogger] Write %x in %x.\n", value, address)
|
|
||||||
}
|
|
||||||
|
|
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
|
||||||
|
}
|
137
cardSaturn.go
|
@ -1,85 +1,77 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
RAM card with 128Kb. It's like 8 language cards.
|
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
|
cardBase
|
||||||
readState bool
|
readState bool
|
||||||
writeState int
|
writeState uint8
|
||||||
activeBank int
|
altBank bool
|
||||||
activeBlock int
|
activeBlock uint8
|
||||||
ramBankA [saturnBlocks]*memoryRange // First 4kb to map in 0xD000-0xDFFF
|
|
||||||
ramBankB [saturnBlocks]*memoryRange // Second 4kb to map in 0xD000-0xDFFF
|
|
||||||
ramUpper [saturnBlocks]*memoryRange // Upper 8kb to map in 0xE000-0xFFFF
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func newCardSaturnBuilder() *cardBuilder {
|
||||||
// Write enabling requires two sofstwitch accesses
|
return &cardBuilder{
|
||||||
saturnWriteDisabled = 0
|
name: "Saturn 128KB Ram Card",
|
||||||
saturnWriteHalfEnabled = 1
|
description: "RAM card with 128Kb, it's like 8 language cards",
|
||||||
saturnWriteEnabled = 2
|
buildFunc: func(params map[string]string) (Card, error) {
|
||||||
)
|
return &CardSaturn{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
saturnBlocks = 8
|
saturnBlocks = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *cardSaturn) assign(a *Apple2, slot int) {
|
func (c *CardSaturn) assign(a *Apple2, slot int) {
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState = lcWriteEnabled
|
c.writeState = lcWriteEnabled
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
|
c.activeBlock = 0
|
||||||
|
a.mmu.initLanguageRAM(saturnBlocks)
|
||||||
|
|
||||||
for i := 0; i < saturnBlocks; i++ {
|
// TODO: use addCardSoftSwitches()
|
||||||
c.ramBankA[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
for i := uint8(0x0); i <= 0xf; i++ {
|
||||||
c.ramBankB[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
|
|
||||||
c.ramUpper[i] = newMemoryRange(0xe000, make([]uint8, 0x2000))
|
|
||||||
}
|
|
||||||
for i := 0x0; i <= 0xf; i++ {
|
|
||||||
iCopy := i
|
iCopy := i
|
||||||
c.ssr[i] = func(*ioC0Page) uint8 {
|
c.addCardSoftSwitchR(iCopy, func() uint8 {
|
||||||
c.ssAction(iCopy)
|
c.ssAction(iCopy)
|
||||||
return 0
|
return 0
|
||||||
}
|
}, "SATURNR")
|
||||||
c.ssw[i] = func(*ioC0Page, uint8) {
|
c.addCardSoftSwitchW(iCopy, func(uint8) {
|
||||||
// Writing resets write count (from A2AUDIT)
|
c.ssAction(iCopy)
|
||||||
c.writeState = lcWriteDisabled
|
}, "SATURNW")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
c.cardBase.assign(a, slot)
|
c.cardBase.assign(a, slot)
|
||||||
c.applyState()
|
c.applyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardSaturn) ssAction(ss int) {
|
func (c *CardSaturn) ssAction(ss uint8) {
|
||||||
switch ss {
|
switch ss {
|
||||||
case 0:
|
case 0:
|
||||||
// RAM read, no writes
|
// RAM read, no writes
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 1:
|
case 1:
|
||||||
// ROM read, RAM write
|
// ROM read, RAM write
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 2:
|
case 2:
|
||||||
// ROM read, no writes
|
// ROM read, no writes
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 3:
|
case 3:
|
||||||
//RAM read, RAM write
|
//RAM read, RAM write
|
||||||
c.activeBank = 0
|
c.altBank = false
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 4:
|
case 4:
|
||||||
|
@ -92,22 +84,22 @@ func (c *cardSaturn) ssAction(ss int) {
|
||||||
c.activeBlock = 3
|
c.activeBlock = 3
|
||||||
case 8:
|
case 8:
|
||||||
// RAM read, no writes
|
// RAM read, no writes
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 9:
|
case 9:
|
||||||
// ROM read, RAM write
|
// ROM read, RAM write
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 10:
|
case 10:
|
||||||
// ROM read, no writes
|
// ROM read, no writes
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = false
|
c.readState = false
|
||||||
c.writeState = lcWriteDisabled
|
c.writeState = lcWriteDisabled
|
||||||
case 11:
|
case 11:
|
||||||
//RAM read, RAM write
|
//RAM read, RAM write
|
||||||
c.activeBank = 1
|
c.altBank = true
|
||||||
c.readState = true
|
c.readState = true
|
||||||
c.writeState++
|
c.writeState++
|
||||||
case 12:
|
case 12:
|
||||||
|
@ -127,56 +119,7 @@ func (c *cardSaturn) ssAction(ss int) {
|
||||||
c.applyState()
|
c.applyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cardSaturn) getActiveBank() [8]*memoryRange {
|
func (c *CardSaturn) applyState() {
|
||||||
if c.activeBank == 0 {
|
c.a.mmu.setLanguageRAMActiveBlock(c.activeBlock)
|
||||||
return c.ramBankA
|
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
|
||||||
}
|
|
||||||
return c.ramBankB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardSaturn) applyState() {
|
|
||||||
mmu := c.a.mmu
|
|
||||||
block := c.activeBlock
|
|
||||||
|
|
||||||
if c.readState {
|
|
||||||
mmu.setPagesRead(0xd0, 0xdf, c.getActiveBank()[block])
|
|
||||||
mmu.setPagesRead(0xe0, 0xff, c.ramUpper[block])
|
|
||||||
} else {
|
|
||||||
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.writeState == lcWriteEnabled {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xdf, c.getActiveBank()[block])
|
|
||||||
mmu.setPagesWrite(0xe0, 0xff, c.ramUpper[block])
|
|
||||||
} else {
|
|
||||||
mmu.setPagesWrite(0xd0, 0xff, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardSaturn) save(w io.Writer) {
|
|
||||||
for i := 0; i < saturnBlocks; i++ {
|
|
||||||
binary.Write(w, binary.BigEndian, c.readState)
|
|
||||||
binary.Write(w, binary.BigEndian, c.writeState)
|
|
||||||
binary.Write(w, binary.BigEndian, c.activeBank)
|
|
||||||
binary.Write(w, binary.BigEndian, c.activeBlock)
|
|
||||||
c.ramBankA[i].save(w)
|
|
||||||
c.ramBankB[i].save(w)
|
|
||||||
c.ramUpper[i].save(w)
|
|
||||||
}
|
|
||||||
c.cardBase.save(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cardSaturn) load(r io.Reader) {
|
|
||||||
for i := 0; i < saturnBlocks; i++ {
|
|
||||||
binary.Read(r, binary.BigEndian, &c.readState)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.writeState)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.activeBank)
|
|
||||||
binary.Read(r, binary.BigEndian, &c.activeBlock)
|
|
||||||
c.ramBankA[i].load(r)
|
|
||||||
c.ramBankB[i].load(r)
|
|
||||||
c.ramUpper[i].load(r)
|
|
||||||
|
|
||||||
c.applyState()
|
|
||||||
}
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
67
cardThunderClockPlus.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package izapple2
|
||||||
|
|
||||||
|
import "github.com/ivanizag/izapple2/component"
|
||||||
|
|
||||||
|
/*
|
||||||
|
ThunderClock`, real time clock card.
|
||||||
|
|
||||||
|
See:
|
||||||
|
https://ia800706.us.archive.org/22/items/ThunderClock_Plus/ThunderClock_Plus.pdf
|
||||||
|
https://prodos8.com/docs/technote/01/
|
||||||
|
https://www.semiee.com/file/backup/NEC-D1990.pdf
|
||||||
|
|
||||||
|
|
||||||
|
uPD1990AC hookup:
|
||||||
|
bit 0 = data in
|
||||||
|
bit 1 = CLK
|
||||||
|
bit 2 = STB
|
||||||
|
bit 3 = C0
|
||||||
|
bit 4 = C1
|
||||||
|
bit 5 = C2
|
||||||
|
bit 7 = data out
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CardThunderClockPlus represents a ThunderClock+ card
|
||||||
|
type CardThunderClockPlus struct {
|
||||||
|
cardBase
|
||||||
|
upd1990 component.MicroPD1990ac
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}, "THUNDERCLOCKR")
|
||||||
|
|
||||||
|
c.addCardSoftSwitchW(0, func(value uint8) {
|
||||||
|
dataIn := (value & 0x01) == 1
|
||||||
|
clock := ((value >> 1) & 0x01) == 1
|
||||||
|
strobe := ((value >> 2) & 0x01) == 1
|
||||||
|
command := (value >> 3) & 0x07
|
||||||
|
/* fmt.Printf("[cardThunderClock] dataIn %v, clock %v, strobe %v, command %v.\n",
|
||||||
|
dataIn, clock, strobe, command) */
|
||||||
|
|
||||||
|
c.upd1990.In(clock, strobe, command, dataIn)
|
||||||
|
}, "THUNDERCLOCKW")
|
||||||
|
|
||||||
|
c.cardBase.assign(a, slot)
|
||||||
|
}
|
54
cardVidHD.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package izapple2
|
||||||
|
|
||||||
|
/*
|
||||||
|
Simulates just what is needed to make Total Replay use the GS modes if the VidHD card is found
|
||||||
|
|
||||||
|
See:
|
||||||
|
https://github.com/a2-4am/4cade/blob/master/src/hw.vidhd.a
|
||||||
|
http://www.applelogic.org/files/GSHARDWAREREF.pdf, page 89
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
data[0] = 0x24
|
||||||
|
data[1] = 0xEA
|
||||||
|
data[2] = 0x4C
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ioDataNewVideo uint8 = 0x29
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CardVidHD) assign(a *Apple2, slot int) {
|
||||||
|
// The softswitches are outside the card reserved ss
|
||||||
|
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,82 +1,107 @@
|
||||||
package apple2
|
package izapple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
See:
|
See:
|
||||||
hhttps://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Companies/Apple/Documentation/Apple%20Technical%20Information%20Library/a2til041.txt
|
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Companies/Apple/Documentation/Apple%20Technical%20Information%20Library/a2til041.txt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// CharacterGenerator represents the ROM wth the characters bitmaps
|
// CharacterGenerator represents the ROM wth the characters bitmaps
|
||||||
type CharacterGenerator struct {
|
type CharacterGenerator struct {
|
||||||
data []uint8
|
data []uint8
|
||||||
|
columnMap charColumnMap
|
||||||
|
page int
|
||||||
|
pageSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
type charColumnMap func(column int) int
|
||||||
|
|
||||||
|
func charGenColumnsMap2Plus(column int) int {
|
||||||
|
return 6 - column
|
||||||
|
}
|
||||||
|
|
||||||
|
func charGenColumnsMap2e(column int) int {
|
||||||
|
return column
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rev7CharGenSize = 2048
|
charGenPageSize2Plus = 2048
|
||||||
|
charGenPageSize2E = 2048 * 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewCharacterGenerator instantiates a new Character Generator with the rom on the file given
|
// NewCharacterGenerator instantiates a new Character Generator with the rom on the file given
|
||||||
func NewCharacterGenerator(filename string) *CharacterGenerator {
|
func newCharacterGenerator(filename string, order charColumnMap, pageSize int) (*CharacterGenerator, error) {
|
||||||
var cg CharacterGenerator
|
var cg CharacterGenerator
|
||||||
cg.load(filename)
|
cg.columnMap = order
|
||||||
return &cg
|
cg.pageSize = pageSize
|
||||||
|
err := cg.load(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CharacterGenerator) load(filename string) {
|
func (cg *CharacterGenerator) load(filename string) error {
|
||||||
bytes := loadResource(filename)
|
bytes, _, err := LoadResource(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
size := len(bytes)
|
size := len(bytes)
|
||||||
if size != rev7CharGenSize {
|
if size < cg.pageSize {
|
||||||
panic("Character ROM size not supported")
|
return errors.New("character ROM size not supported")
|
||||||
}
|
}
|
||||||
cg.data = bytes
|
cg.data = bytes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *CharacterGenerator) setPage(page int) {
|
||||||
|
// Some clones had a switch to change codepage with extra characters
|
||||||
|
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 {
|
func (cg *CharacterGenerator) getPixel(char uint8, row int, column int) bool {
|
||||||
bits := cg.data[int(char)*8+row]
|
bits := cg.data[int(char)*8+row+cg.page*cg.pageSize]
|
||||||
bit := bits >> (uint(6 - column)) & 1
|
bit := cg.columnMap(column)
|
||||||
return bit == 1
|
value := bits >> uint(bit) & 1
|
||||||
|
return value == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CharacterGenerator) dumpCharFast(char uint8) {
|
func setupCharactedGenerator(a *Apple2, board string, charRomFile string) error {
|
||||||
base := int(char) * 8
|
var charGenMap charColumnMap
|
||||||
fmt.Printf("Char: %v\n---------\n", char)
|
initialCharGenPage := 0
|
||||||
for i := 0; i < 8; i++ {
|
pageSize := charGenPageSize2Plus
|
||||||
fmt.Print("|")
|
switch board {
|
||||||
b := cg.data[base+i]
|
case "2plus":
|
||||||
for j := 6; j >= 0; j-- {
|
charGenMap = charGenColumnsMap2Plus
|
||||||
if (b>>uint(j))&1 == 1 {
|
case "2e":
|
||||||
fmt.Print("#")
|
charGenMap = charGenColumnsMap2e
|
||||||
} else {
|
pageSize = charGenPageSize2E
|
||||||
fmt.Print(" ")
|
case "base64a":
|
||||||
}
|
charGenMap = charGenColumnsMapBase64a
|
||||||
}
|
initialCharGenPage = 1
|
||||||
fmt.Println("|")
|
default:
|
||||||
|
return fmt.Errorf("board %s not supported it must be '2plus', '2e' or 'base64a'", board)
|
||||||
}
|
}
|
||||||
fmt.Println("---------")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cg *CharacterGenerator) dumpChar(char uint8) {
|
cg, err := newCharacterGenerator(charRomFile, charGenMap, pageSize)
|
||||||
fmt.Printf("Char: %v\n---------\n", char)
|
if err != nil {
|
||||||
for row := 0; row < 8; row++ {
|
return err
|
||||||
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() {
|
|
||||||
for i := 0; i < 256; i++ {
|
|
||||||
cg.dumpChar(uint8(i))
|
|
||||||
}
|
}
|
||||||
|
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++
|
||||||
|
}
|
||||||
|
}
|
120
component/microPD1990ac.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
microPD1990ac Serial I/O Calendar Clock IC
|
||||||
|
See:
|
||||||
|
https://www.semiee.com/file/backup/NEC-D1990.pdf
|
||||||
|
|
||||||
|
Used by the ThunderClock+ real time clock card.
|
||||||
|
|
||||||
|
The 40 bit register has 5 bytes (10 nibbles):
|
||||||
|
byte 4:
|
||||||
|
month, binary from 1 to 12
|
||||||
|
day of week, BCD 0 to 6
|
||||||
|
byte 3: day of month, BCD 1 to 31
|
||||||
|
byte 2: hour, BCD 0 to 23
|
||||||
|
byte 1: minute, BCD 0 to 59
|
||||||
|
byte 0: seconds, BCD 0 to 59
|
||||||
|
|
||||||
|
*/
|
||||||
|
type MicroPD1990ac struct {
|
||||||
|
clock bool // CLK state
|
||||||
|
strobe bool // STB state
|
||||||
|
command uint8 // C0, C1, C2 command. From 0 to 7
|
||||||
|
register uint64 // 40 bit shift register
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
mpd1990commandRegHold = 0
|
||||||
|
mpd1990commandRegShift = 1
|
||||||
|
mpd1990commandTimeSet = 2
|
||||||
|
mpd1990commandTimeRead = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *MicroPD1990ac) In(clock bool, strobe bool, command uint8, dataIn bool) {
|
||||||
|
// Detect signal raise
|
||||||
|
clockRaise := clock && !m.clock
|
||||||
|
strobeRaise := strobe && !m.strobe
|
||||||
|
|
||||||
|
// Update signal status
|
||||||
|
m.clock = clock
|
||||||
|
m.strobe = strobe
|
||||||
|
|
||||||
|
// On strobe raise, update command and execute if needed
|
||||||
|
if strobeRaise {
|
||||||
|
m.command = command
|
||||||
|
|
||||||
|
switch m.command {
|
||||||
|
case mpd1990commandRegShift:
|
||||||
|
// Nothing to do
|
||||||
|
case mpd1990commandTimeRead:
|
||||||
|
m.loadTime()
|
||||||
|
default:
|
||||||
|
// Ignore unknown commands (like set time)
|
||||||
|
//panic(fmt.Sprintf("PD1990ac command %v not implemented.", m.command))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On clock raise, with shift enable, shift the register
|
||||||
|
if clockRaise && m.command == mpd1990commandRegShift {
|
||||||
|
// Rotate right the 40 bits of the shift register
|
||||||
|
lsb := m.register & 1
|
||||||
|
m.register >>= 1
|
||||||
|
m.register += lsb << 39
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Return the LSB of the register shift
|
||||||
|
return (m.register & 1) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MicroPD1990ac) loadTime() {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
var register uint64
|
||||||
|
|
||||||
|
register = uint64(now.Month())
|
||||||
|
register <<= 4
|
||||||
|
register += uint64(now.Weekday())
|
||||||
|
|
||||||
|
day := uint64(now.Day())
|
||||||
|
register <<= 4
|
||||||
|
register += day / 10
|
||||||
|
register <<= 4
|
||||||
|
register += day % 10
|
||||||
|
|
||||||
|
hour := uint64(now.Hour())
|
||||||
|
register <<= 4
|
||||||
|
register += hour / 10
|
||||||
|
register <<= 4
|
||||||
|
register += hour % 10
|
||||||
|
|
||||||
|
minute := uint64(now.Minute())
|
||||||
|
register <<= 4
|
||||||
|
register += minute / 10
|
||||||
|
register <<= 4
|
||||||
|
register += minute % 10
|
||||||
|
|
||||||
|
second := uint64(now.Second())
|
||||||
|
register <<= 4
|
||||||
|
register += second / 10
|
||||||
|
register <<= 4
|
||||||
|
register += second % 10
|
||||||
|
|
||||||
|
m.register = register
|
||||||
|
}
|
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,32 +0,0 @@
|
||||||
package core6502
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFunctional(t *testing.T) {
|
|
||||||
m := new(FlatMemory)
|
|
||||||
s := NewNMOS6502(m)
|
|
||||||
|
|
||||||
// Test suite from https://github.com/Klaus2m5/6502_65C02_functional_tests
|
|
||||||
m.loadBinary("testdata/6502_functional_test.bin")
|
|
||||||
|
|
||||||
s.reg.setPC(0x0400)
|
|
||||||
for true {
|
|
||||||
testCase := s.mem.Peek(0x0200)
|
|
||||||
if testCase >= 240 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log := testCase > 43
|
|
||||||
if log {
|
|
||||||
fmt.Printf("[ %d ] ", testCase)
|
|
||||||
}
|
|
||||||
pc := s.reg.getPC()
|
|
||||||
s.ExecuteInstruction(log)
|
|
||||||
if pc == s.reg.getPC() {
|
|
||||||
t.Errorf("Failure in test %v.", testCase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
package core6502
|
|
||||||
|
|
||||||
const (
|
|
||||||
modeImplicit = iota + 1
|
|
||||||
modeImplicitX
|
|
||||||
modeImplicitY
|
|
||||||
modeAccumulator
|
|
||||||
modeImmediate
|
|
||||||
modeZeroPage
|
|
||||||
modeZeroPageX
|
|
||||||
modeZeroPageY
|
|
||||||
modeRelative
|
|
||||||
modeAbsolute
|
|
||||||
modeAbsoluteX
|
|
||||||
modeAbsoluteY
|
|
||||||
modeIndirect
|
|
||||||
modeIndexedIndirectX
|
|
||||||
modeIndirectIndexedY
|
|
||||||
)
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// Todo: assert impossible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,132 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
vectorNMI uint16 = 0xfffa
|
|
||||||
vectorReset uint16 = 0xfffc
|
|
||||||
vectorBreak uint16 = 0xfffe
|
|
||||||
)
|
|
||||||
|
|
||||||
type opcode struct {
|
|
||||||
name string
|
|
||||||
bytes uint8
|
|
||||||
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(log bool) {
|
|
||||||
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 := uint8(0); i < opcode.bytes; i++ {
|
|
||||||
line[i] = s.mem.Peek(pc)
|
|
||||||
pc++
|
|
||||||
}
|
|
||||||
s.reg.setPC(pc)
|
|
||||||
|
|
||||||
if log {
|
|
||||||
fmt.Printf("%#04x %-12s: ", pc, lineString(line, opcode))
|
|
||||||
}
|
|
||||||
opcode.action(s, line, opcode)
|
|
||||||
s.cycles += uint64(opcode.cycles)
|
|
||||||
if log {
|
|
||||||
fmt.Printf("%v, [%02x]\n", s.reg, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset resets the processor state. Moves the program counter to the vector in 0cfffc.
|
|
||||||
func (s *State) Reset() {
|
|
||||||
startAddress := getWord(s.mem, vectorReset)
|
|
||||||
s.cycles = 0
|
|
||||||
s.reg.setPC(startAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCycles returns the count of CPU cycles since last reset.
|
|
||||||
func (s *State) GetCycles() uint64 {
|
|
||||||
return s.cycles
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save saves the CPU state (registers and cycle counter)
|
|
||||||
func (s *State) Save(w io.Writer) {
|
|
||||||
binary.Write(w, binary.BigEndian, s.cycles)
|
|
||||||
binary.Write(w, binary.BigEndian, s.reg.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load loads the CPU state (registers and cycle counter)
|
|
||||||
func (s *State) Load(r io.Reader) {
|
|
||||||
binary.Read(r, binary.BigEndian, &s.cycles)
|
|
||||||
binary.Read(r, binary.BigEndian, &s.reg.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
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])
|
|
||||||
default:
|
|
||||||
t += "UNKNOWN MODE"
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
|
@ -1,487 +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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Tests for BRK, JMP, JSR, RTI, RTS
|
|
|
@ -1,43 +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) {
|
|
||||||
bytes, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, v := range bytes {
|
|
||||||
m.Poke(uint16(i), uint8(v))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,184 +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, opPHA},
|
|
||||||
0x08: opcode{"PHP", 1, 3, modeImplicit, opPHP},
|
|
||||||
0x68: opcode{"PLA", 1, 4, modeImplicit, opPLA},
|
|
||||||
0x28: opcode{"PLP", 1, 4, modeImplicit, opPLP},
|
|
||||||
|
|
||||||
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},
|
|
||||||
}
|
|
|
@ -1,242 +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, value bool) opFunc {
|
|
||||||
return func(s *State, line []uint8, opcode opcode) {
|
|
||||||
if s.reg.getFlag(flag) == value {
|
|
||||||
// This assumes that PC is already pointing to the next instruction
|
|
||||||
pc := s.reg.getPC()
|
|
||||||
pc += uint16(int8(line[1]))
|
|
||||||
s.reg.setPC(pc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func opBIT(s *State, line []uint8, opcode opcode) {
|
|
||||||
value := resolveValue(s, line, opcode)
|
|
||||||
acc := s.reg.getA()
|
|
||||||
// Future note: The immediate addressing mode (65C02 or 65816 only) does not affect V.
|
|
||||||
s.reg.updateFlag(flagZ, value&acc == 0)
|
|
||||||
s.reg.updateFlag(flagN, value&(1<<7) != 0)
|
|
||||||
s.reg.updateFlag(flagV, value&(1<<6) != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 opPLA(s *State, line []uint8, opcode opcode) {
|
|
||||||
value := pullByte(s)
|
|
||||||
s.reg.setA(value)
|
|
||||||
s.reg.updateFlagZN(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func opPLP(s *State, line []uint8, opcode opcode) {
|
|
||||||
value := pullByte(s)
|
|
||||||
s.reg.setP(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func opPHA(s *State, line []uint8, opcode opcode) {
|
|
||||||
pushByte(s, s.reg.getA())
|
|
||||||
}
|
|
||||||
|
|
||||||
func opPHP(s *State, line []uint8, opcode opcode) {
|
|
||||||
pushByte(s, s.reg.getP()|(flagB+flag5))
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
|
@ -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
|
@ -1,174 +0,0 @@
|
||||||
package apple2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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
|
|
||||||
)
|
|
||||||
|
|
||||||
type diskette16sector struct {
|
|
||||||
track [numberOfTracks][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *diskette16sector) read(track int, position int) (value uint8, newPosition int) {
|
|
||||||
value = d.track[track][position]
|
|
||||||
newPosition = (position + 1) % nibBytesPerTrack
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *diskette16sector) write(track int, position int, value uint8) int {
|
|
||||||
d.track[track][position] = value
|
|
||||||
return (position + 1) % nibBytesPerTrack
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadDisquette(filename string) *diskette16sector {
|
|
||||||
var d diskette16sector
|
|
||||||
|
|
||||||
data := loadResource(filename)
|
|
||||||
size := len(data)
|
|
||||||
|
|
||||||
if size == nibImageSize {
|
|
||||||
// Load file already in nib format
|
|
||||||
for i := 0; i < numberOfTracks; i++ {
|
|
||||||
d.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]
|
|
||||||
d.track[i] = nibEncodeTrack(trackData, defaultVolumeTag, byte(i))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic("Invalid disk size")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *diskette16sector) saveNib(filename string) {
|
|
||||||
f, err := os.Create(filename)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
for _, v := range d.track {
|
|
||||||
_, err := f.Write(v)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
BIN
doc/dos33.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
doc/karateka.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
doc/totalreplay.png
Normal file
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
|
2
dockerbuild/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
build
|
||||||
|
|
18
dockerbuild/Dockerfile
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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 unzip
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
CMD ["./buildindocker.sh"]
|
5
dockerbuild/build.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cd "$( dirname $0)"
|
||||||
|
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
|
||||||
|
|
||||||
|
|
49
dockerbuild/buildindocker.sh
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cd /tmp
|
||||||
|
git clone https://github.com/ivanizag/izapple2
|
||||||
|
|
||||||
|
# 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 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 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 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
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 |
1
frontend/a2fyne/resources/format-font.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="M17,8H20V20H21V21H17V20H18V17H14L12.5,20H14V21H10V20H11L17,8M18,9L14.5,16H18V9M5,3H10C11.11,3 12,3.89 12,5V16H9V11H6V16H3V5C3,3.89 3.89,3 5,3M6,5V9H9V5H6Z" /></svg>
|
After Width: | Height: | Size: 449 B |
1
frontend/a2fyne/resources/layers-triple.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="M12 0L3 7L4.63 8.27L12 14L19.36 8.27L21 7L12 0M19.37 10.73L12 16.47L4.62 10.74L3 12L12 19L21 12L19.37 10.73M19.37 15.73L12 21.47L4.62 15.74L3 17L12 24L21 17L19.37 15.73Z" /></svg>
|
After Width: | Height: | Size: 464 B |