Compare commits

...

170 Commits
v1.2 ... master

Author SHA1 Message Date
Iván Izaguirre ad720f0527 Explicit cards ROM layout 2024-03-24 20:22:36 +01:00
Iván Izaguirre 9e89f0a23f Explicit cards ROM layout 2024-03-24 20:15:16 +01:00
Iván Izaguirre cb355a17cd Improve the option to panic on undefined softswitches 2024-03-09 20:09:12 +01:00
Iván Izaguirre 30e3a7503d Tilde expansion for path parameters is slot configuration strings 2024-03-09 19:22:04 +01:00
Iván Izaguirre a4ad599056 Ralle Palaveev's ProDOS-Romcard3 2024-03-08 21:36:27 +01:00
Iván Izaguirre e20e355f9f Remove setBase from memeryHandler, it is not needed 2024-03-04 21:36:45 +01:00
Iván Izaguirre 9611993c85 ProDOS ROM Drive Card 2024-03-04 20:50:27 +01:00
Iván Izaguirre cedb810400 MacOS x64 builds 2024-03-04 20:47:59 +01:00
Ivan Izaguirre 4b061a76ff Help info with F1 2024-02-13 23:39:25 +01:00
Iván Izaguirre 94c85a460a Build multiplatform from MacOS 2024-02-13 22:51:05 +01:00
Iván Izaguirre 5fe1dc4fdf uint8 params 2024-02-13 22:51:05 +01:00
Ivan Izaguirre 9035db5d81 Fixes on the Mouse card 2024-02-12 23:24:57 +01:00
Ivan Izaguirre 0b3b90a198 Disable the mouse card by default to run A2OSX 2024-02-12 23:21:16 +01:00
Ivan Izaguirre c77041195e Fixes on the Mouse card 2024-02-12 23:20:31 +01:00
Ivan Izaguirre 51a7f17e5b Improve usage message and automate the inclusion in README.md 2024-02-12 20:45:52 +01:00
Ivan Izaguirre 9178372942 Better usage info 2024-02-08 22:17:14 +01:00
Ivan Izaguirre 3a6b8648a6 Apple II rev 0 model 2024-02-08 20:34:49 +01:00
Ivan Izaguirre 5cf351f05c Reverse 6 colors mod 2024-02-08 20:24:37 +01:00
Ivan Izaguirre 7cd5ce02ec Emulation of the improved Dan ][ card 2024-01-31 23:17:47 +01:00
Ivan Izaguirre 18c0779064 Dan ][ Controller card 2024-01-30 00:33:53 +01:00
Ivan Izaguirre f0f8d6448e Original BrainBoard card 2024-01-27 17:28:54 +01:00
Ivan Izaguirre e17033329d Original BrainBoard card 2024-01-27 17:21:54 +01:00
Ivan Izaguirre 36faa6e906 Original BrainBoard card 2024-01-27 17:21:54 +01:00
Ivan Izaguirre b2cf890957 Trace sofswitches per card 2024-01-27 17:21:54 +01:00
Iván Izaguirre 0c615fc96c Some traces for CP/M 65, test for CP/M 65 boot and some other changes 2024-01-25 22:20:22 +01:00
Iván Izaguirre 624374f344 Video 7 RGB doesn't have NTSC artifacts on the mixed mode text 2024-01-25 18:42:59 +01:00
Iván Izaguirre ddfc462927 Card Disk II softswitches are active also on writes. CP/M 85 relies on that. 2024-01-25 18:42:59 +01:00
Iván Izaguirre 4b2a0d836f Improve rendering 2024-01-25 18:42:59 +01:00
Ivan Izaguirre 9c27175f86 ROM can be paged, refactor the Base64A ROM 2024-01-22 23:09:35 +01:00
Ivan Izaguirre 63c982d976 Update dependencies to get more undocumented opcodes from iz6502 2024-01-22 21:02:57 +01:00
Ivan Izaguirre 98e6dd0cb5 Add an 80 col card by default for the ][+ 2024-01-22 21:02:00 +01:00
Ivan Izaguirre a66dc18226 Fix Base64a boot test 2024-01-15 23:54:15 +01:00
Iván Izaguirre 677e1113cb MultiROM card by davidm in applefritter 2024-01-15 21:09:02 +01:00
Iván Izaguirre ea492f118e Card refactor bits 2024-01-15 21:08:18 +01:00
Iván Izaguirre e966b57a0c Tests of card presence with Card Cat 2024-01-06 23:41:38 +01:00
Iván Izaguirre b1a643f748 A2AUDIT tests 2024-01-06 23:40:54 +01:00
Iván Izaguirre c24eeef200 Update depdendencies 2024-01-06 23:40:22 +01:00
Iván Izaguirre 0a1a619586 Configuration redone 2024-01-06 21:48:23 +01:00
Ivan Izaguirre ba4c7437b4 Emulate the Brain Board II 2023-12-18 23:16:41 +01:00
dependabot[bot] ca49f0f535 Bump golang.org/x/net from 0.7.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.7.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.7.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 09:34:31 +02:00
Chris Green e770dbc058 Make device panel scrollable
Previous panel caused main window to resize when device panel was enabled so if the vertical resolution wasn't big enough you'd lose visibility of the toolbar and the text would be noticeably distorted.
2023-10-25 09:27:18 +02:00
Chris Green 4e116182e7 replace deprecated Fyne call 2023-10-25 09:27:18 +02:00
Iván Izaguirre 8ea72357d5 Support for 4 disks on the command line. Support tyo trace track changes. 2023-09-25 20:55:06 +02:00
Iván Izaguirre 63e3e8b80b Fix on the Apple Pascal traces 2023-09-25 20:47:57 +02:00
Chris Green 996772a023
fix disk b index for sequencer drive (#21)
Disk B for sequencer drive was using index 0 instead of 1, so both -disk and -diskb were setting the first disk.
2023-09-22 09:00:37 +02:00
Ivan Izaguirre eb34906fea Using sonarqube 2023-09-15 17:39:21 +02:00
dependabot[bot] fa17d4baa4
Bump gopkg.in/yaml.v3 from 3.0.0-20210107192922-496545a6307b to 3.0.0 (#20)
Bumps gopkg.in/yaml.v3 from 3.0.0-20210107192922-496545a6307b to 3.0.0.

---
updated-dependencies:
- dependency-name: gopkg.in/yaml.v3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 16:45:04 +02:00
Iván Izaguirre a89cf24fe7 Change diskettes with drag and drop 2023-04-04 18:35:13 +02:00
Iván Izaguirre e38c4cf57b Fix for the same issue on RGB card as mamedev 10975 2023-03-10 21:10:20 +01:00
Iván Izaguirre 0a76765b8e Woz test for Command is unstable 2023-03-10 16:23:27 +01:00
Iván Izaguirre 02b6fd0fea Pass tests on Mac 2023-03-10 16:13:55 +01:00
dependabot[bot] 259d7e2cfe
Bump golang.org/x/net from 0.0.0-20220225172249-27dd8689420f to 0.7.0 (#18)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220225172249-27dd8689420f to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-27 16:47:02 +01:00
dependabot[bot] 39a2c7325c
Bump golang.org/x/image from 0.0.0-20220321031419-a8550c1d254a to 0.5.0 (#17)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20220321031419-a8550c1d254a to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/commits/v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-27 10:41:49 +01:00
dependabot[bot] a4f459cab4
Bump golang.org/x/text from 0.3.7 to 0.3.8 (#15)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-27 10:35:59 +01:00
Iván Izaguirre a5e8c200c7 Parallel Printer Card works with PrintShopPro 2022-11-26 22:24:15 +01:00
Ivan Izaguirre e7ba568149 Imaginary Fujinet Clock device with prodos datetime format 2022-11-14 18:47:14 +01:00
Ivan Izaguirre 192166435b Imaginary Fujinet Clock device 2022-11-01 16:57:45 +01:00
Ivan Izaguirre 39e0c9ef7a Rename SmartPort constants 2022-10-31 22:57:18 +01:00
Ivan Izaguirre 33ea1f6e7a Basic Fujinet network support, iis-tracker works 2022-10-31 22:47:53 +01:00
Ivan Izaguirre 9f36507fa5 Support multiple devices on the Smartport card 2022-10-24 23:15:20 +02:00
Ivan Izaguirre 718f0b60b3 Fujinet network device 2022-10-24 23:09:06 +02:00
Ivan Izaguirre 5d857dda4b Separation of the Smartport card and the Smartport hard disk 2022-10-23 18:07:00 +02:00
Ivan Izaguirre d631ee7973 Updates and lint warning 2022-10-23 18:02:47 +02:00
Iván Izaguirre 9feded23f8 Simplify softswitches 2022-08-05 19:43:17 +02:00
Iván Izaguirre f2bc9f21aa Support the Apple II Parallel Printer Interface card 2022-08-05 19:08:34 +02:00
Ivan Izaguirre 1777954a54 Check key range 2022-07-04 20:29:47 +02:00
Ivan Izaguirre d85e22cc0a Fix github action 2022-05-11 22:49:37 +02:00
Iván Izaguirre 5b71a131f9 More headless commands 2022-05-10 13:40:17 +02:00
Iván Izaguirre 695eaa603b Save GIFs on headless mode 2022-05-09 14:34:47 +02:00
Ivan Izaguirre dcc1597cb2 Headless experiment 2022-05-08 18:00:25 +02:00
Iván Izaguirre 6590626b02 Use go 1.18 2022-04-18 16:17:42 +02:00
Iván Izaguirre 862a3c22dc Use go 1.18 2022-04-18 16:10:25 +02:00
Iván Izaguirre 80977143de Sound improvements 2022-04-18 13:23:04 +02:00
Iván Izaguirre 15c38b091d Udate README for Swyftcard 2022-04-18 13:02:28 +02:00
Iván Izaguirre 1e32421c86
Swyftcard support (#13) 2022-03-08 20:11:26 +01:00
Ivan Izaguirre fe15ce8c93 Woz protections automatic tests 2022-02-26 00:05:09 +01:00
Ivan Izaguirre cde673a552 Test boot process for all models 2022-02-21 23:05:53 +01:00
Ivan Izaguirre 9d6393f078 Update modules 2022-01-28 19:31:30 +01:00
Ivan Izaguirre f28dbdaa1c Replace vfsgen with go embed 2022-01-28 19:25:52 +01:00
Ivan Izaguirre cc7f4678a8 Update dependencies 2022-01-28 18:36:18 +01:00
Ivan Izaguirre 41431fc0e3 Install SDL2 for build 2022-01-28 18:22:36 +01:00
Ivan Izaguirre b8769578dc Run all tests in the Github action 2022-01-28 18:14:30 +01:00
Ivan Izaguirre 2ae91f8585 Extract the 6502 emulation to a separate module 2022-01-28 18:09:59 +01:00
Ivan Izaguirre 765b1fba28 Use go 1.17 and the new unsafe.Slice() 2022-01-28 17:50:42 +01:00
Ivan Izaguirre da45f0468f Improved Applecorn traces 2021-10-25 23:45:05 +02:00
Ivan Izaguirre 4a2c094198 ROMXe font emulation 2021-10-12 12:26:40 +02:00
Ivan Izaguirre a93a32f63e Comment on Rockwell65c02 difference 2021-09-27 20:46:43 +02:00
Ivan Izaguirre 71fd530e26 Fix V flag on ADC for BCD 2021-09-26 23:29:11 +02:00
Ivan Izaguirre 76f9ae514e Some undocumented NOPs for 6502 2021-09-26 18:25:11 +02:00
Ivan Izaguirre 2a7cf020ae Fixes for ADC and SBC on BCD mode 2021-09-26 17:32:15 +02:00
Ivan Izaguirre 7143cdc2de Make it work pre go 1.16 2021-09-26 01:43:21 +02:00
Ivan Izaguirre 4f963b0bd6 Cycle count accurate per the Harte test suite 2021-09-26 01:22:08 +02:00
Ivan Izaguirre 1a150d0f96 Tom Harte tests for wdc6502 2021-09-25 21:11:15 +02:00
Ivan Izaguirre 358c4fabc1 Pass the 6502s test from Harte without breaking the 65c02 tests from Klaus 2021-09-25 20:58:48 +02:00
Ivan Izaguirre 3f916a60fa First run with https://github.com/TomHarte/ProcessorTests 2021-09-25 20:33:45 +02:00
Ivan Izaguirre fe4afa1de9 Remove new warnings 2021-09-25 20:16:40 +02:00
Iván Izaguirre 146e2cdad6 trace Acorn ROM entrypoints 2021-08-17 12:50:04 +02:00
Iván Izaguirre 5d710b36ae traceBBC fixes 2021-08-15 20:45:39 +02:00
Iván Izaguirre 3c9c3b7625 traceBBC log improvements 2021-08-11 19:15:47 +02:00
Iván Izaguirre 3de2c9dc1a Fixing lint warnings 2021-08-05 21:12:52 +02:00
Ivan Izaguirre faf97dcdc9 Show the star command used in MOS OSCLI 2021-07-25 23:26:03 +02:00
Ivan Izaguirre 4884a8861f Show the filenames of MOS saves log 2021-07-25 23:16:32 +02:00
Ivan Izaguirre e47730e131 Change PC externally 2021-07-25 11:50:35 +02:00
Ivan Izaguirre 3b56312d9c Simpler approach for Applecorn trace, no reentrancy 2021-07-24 19:41:53 +02:00
Ivan Izaguirre 612adbc11f Change A, X, Y and P externally 2021-07-24 19:37:35 +02:00
Ivan Izaguirre b1d4d53869 Change A, X and Y externally 2021-07-24 17:30:09 +02:00
Iván Izaguirre 49a425b727 Page 2 vectors 2021-07-24 14:02:50 +02:00
Ivan Izaguirre 950d99c59f Better display of reentrant calls 2021-07-23 18:11:30 +02:00
Ivan Izaguirre b270c1c55b MOS return values 2021-07-23 17:53:00 +02:00
Iván Izaguirre 95b9d3ef05 Log level for Applecorn 2021-07-23 17:23:24 +02:00
Iván Izaguirre 620d496807 Trace calls to the BBC MOS when using Applecorn 2021-07-22 19:33:34 +02:00
Ivan Izaguirre c1a8bf7ee4 More WOZ verification 2021-07-01 19:59:15 +02:00
Ivan Izaguirre 511f1bc8c7 Experiments with WOZ writing 2021-07-01 18:15:49 +02:00
Ivan Izaguirre a765ac8033 Use go 1.13 in CircleCI 2021-05-10 23:36:24 +02:00
Ivan Izaguirre fc03b5533d Add command line option to use the sequencer based Disk II card 2021-05-10 22:24:41 +02:00
Ivan Izaguirre cab42e9a94 The disk continues turning for 1 second after the turn off command. Bouncing Kamungas now works. 2021-05-10 22:13:55 +02:00
Ivan Izaguirre fa7604800b Cleanup 2021-05-09 20:09:54 +02:00
Ivan Izaguirre 0daf1b9fd9 Disk II state machine 2021-05-09 19:48:54 +02:00
Ivan Izaguirre 902437419e Trace Apple Pascal BIOS calls 2021-04-02 20:39:59 +02:00
Iván Izaguirre c3c1a262fc #11 Improve the build instructions 2021-03-30 21:27:47 +02:00
Ivan Izaguirre 8e1bbdef0e Move microPD1990ac.go 2021-03-19 17:18:00 +01:00
Iván Izaguirre f9ba9256b5 #12 Restore F6 and F12. F6 to switch mono green/NTSC modes 2021-03-18 20:11:23 +01:00
Ivan Izaguirre c390be1ced MC6845 Cursor modes 2021-03-14 22:38:41 +01:00
Ivan Izaguirre f15a7bb9d2 #11 Update paths in the build instructions 2021-03-14 20:06:19 +01:00
Ivan Izaguirre dfb8b1ffb2 Videx Videoterm 80 columns card support. Videx Soft Video Switch support 2021-03-14 18:46:52 +01:00
Ivan Izaguirre f34b3da510 Shared power for the two drives. Fix fast mode on disk b 2021-03-14 18:24:29 +01:00
Ivan Izaguirre d531a26b2e Upgrade to fyne.io v2 (experimental) 2021-03-06 20:23:17 +01:00
Iván Izaguirre 9d040fa58b Show alt text character map 2021-03-02 00:19:18 +01:00
Ivan Izaguirre e49bef397b Ctrl-F10 to show the current character map 2021-02-14 23:53:29 +01:00
Iván Izaguirre 69ec17e541
Update Total Replay version 2021-02-13 16:15:05 +01:00
Ivan Izaguirre 0c150c3ecf Mouse access with IN# and PR# 2021-01-25 19:01:53 +01:00
Ivan Izaguirre 55ca1df967 Mouse support with Apple Mouse Card 2021-01-24 23:25:52 +01:00
Ivan Izaguirre 8b50ae1500 Card to bridge PR# and IN# to the host terminal 2021-01-24 16:56:06 +01:00
Iván Izaguirre 89881a8466
Update README.md
Wrong CircleCI badge
2020-12-15 17:23:10 +01:00
Ivan Izaguirre ffe644caf4 WOZ cross track sync for tracks with different size. Now Hard Hat Mack and Blazing Paddles work. 2020-12-07 21:34:51 +01:00
Ivan Izaguirre 95998ba37d Add proper extra cycle when adding an index cross boundaries on memory reads 2020-12-06 21:18:20 +01:00
Ivan Izaguirre 9c89ad4a61 Extra cycle when brancing on conditional jumps. Fixes Sammy Lighfoot woz loadind. 2020-12-06 17:24:24 +01:00
Ivan Izaguirre c7f643623d Extra cycle when brancing on conditional jumps. Fixes Sammy Lighfoot woz loadind. 2020-12-06 17:16:57 +01:00
Ivan Izaguirre 512988655d Trace ProDOS driver calls 2020-11-20 19:28:22 +01:00
Ivan Izaguirre bce5c4b92e Experiments with prodos device drivers 2020-11-20 16:19:06 +01:00
Ivan Izaguirre c9bb9f2208 Update README.md 2020-11-03 18:41:10 +01:00
Rob Greene 313733b728 now detecting and setting writeable flag based on how a resource was opened 2020-11-03 18:14:56 +01:00
Rob Greene cadbd70b53 adding ability to read from gzip or zip files 2020-11-03 18:14:56 +01:00
Rob Greene f31773b51c Adding UI elements into the Fyne UI. 2020-11-03 15:01:28 +01:00
Rob Greene 541d8be170 Adding a --forceCaps flag to ensure all keypresses are in caps. Particularly useful if you are multitasking! 2020-11-03 15:01:28 +01:00
Ivan Izaguirre d8293e2dda MMU and IOU reset on the Apple IIe 2020-10-28 00:43:33 +01:00
Ivan Izaguirre 2eb16b4c3d Update go-sdl2 to v0.4.4 2020-10-27 23:15:54 +01:00
Ivan Izaguirre f8360ea4b7 Update fyne.io to v1.4.0-rc1 2020-10-27 23:08:56 +01:00
Ivan Izaguirre 3e5e10b8dd Experimental RomX 2020-10-25 00:22:52 +02:00
Ivan Izaguirre 17edb28d38 Docker build for Fyne 2020-10-18 00:29:45 +02:00
Ivan Izaguirre 6c4a4df730 Move frontends 2020-10-17 23:59:52 +02:00
Ivan Izaguirre 18e0363d84 Move disk related code to a separate package 2020-10-17 20:10:48 +02:00
Ivan Izaguirre 7329d4f042 Snapshot testing for screen rendering 2020-10-17 19:34:06 +02:00
Ivan Izaguirre 1a4a1507a5 Remove warnings 2020-10-17 13:42:31 +02:00
Ivan Izaguirre de7a1af593 Separate package for screen rendering 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 083c19d2c2 Separate package for screen rendering 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 9d4df5b643 Screenmode in debug mode 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 4ec3bff858 UI to select screen modes 2020-10-17 13:31:42 +02:00
Ivan Izaguirre a9b8fe76df UI to select screen modes 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 1539102ad9 Restore fyne version 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 9e63e82e9d Card information 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 758e4f79bb Card refactor and card panels 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 91e6eb6f6d Sidebar 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 9395c2ffb3 Joystick support in Fyne with glfw 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 17f1121980 Toolbar commands 2020-10-17 13:31:42 +02:00
Ivan Izaguirre adcf87c29a Use temporaly a fyne fork 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 10f915c90f Keyboard bindings with fyne 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 97acee9f9e Start fyne.io UI 2020-10-17 13:31:42 +02:00
Ivan Izaguirre 0bf4a05606 Fix warnings 2020-10-07 09:37:13 +02:00
Ivan Izaguirre e18b44c88d Use all pair and even buttons of the joysticks as buttons 0 and 1 (one of the buttons of my controller broke) 2020-10-05 22:17:19 +02:00
317 changed files with 11457 additions and 30762 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: circleci/golang:1.12
- image: cimg/go:1.18
steps:
- checkout
- run: sudo apt update

View File

@ -13,10 +13,15 @@ jobs:
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.13
go-version: ^1.18
id: go
- name: Check out code into the Go module directory
@ -34,4 +39,4 @@ jobs:
run: go build -v .
- name: Test
run: go test -v .
run: go test -v ./...

17
.gitignore vendored
View File

@ -1 +1,18 @@
.vscode
SDL2.dll
a2sdl.exe
frontend/a2sdl/a2sdl
frontend/*/*.woz
frontend/*/*.dsk
frontend/*/*.po
frontend/*/*.2mg
frontend/*/*.hdv
frontend/a2fyne/a2fyne
frontend/headless/headless
frontend/*/snapshot.gif
frontend/*/snapshot.png
printer.out
.DS_STORE
.scannerwork
coverage.out
test.out

255
README.md
View File

@ -2,35 +2,52 @@
Portable emulator of an Apple II+ or //e. Written in Go.
[![CircleCI](https://circleci.com/gh/ivanizag/izapple2/tree/master.svg?style=svg)](https://circleci.com/gh/ivanizag/izapple2/tree/master)
## Features
- Models:
- Apple ][+ with 48Kb of base RAM
- Apple //e with 128Kb of RAM
- Apple //e enhanced with 128Kb of RAM
- Base64A clone with 48Kb of base RAM and paginated ROM
- Base64A clone with 48Kb of base RAM and paged ROM
- Storage
- 16 Sector 5 1/4 diskettes in NIB, DSK or PO format
- 16 Sector 5 1/4 diskettes in WOZ 1.0 or 2.0 format (read only)
- 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
- 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
- Bootable Smartport / ProDOS card
- Apple //e 80 columns with 64Kb extra RAM and optional RGB modes
- 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
- No Slot Clock based on the DS1216
- 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 only)
- Text 80 columns (Apple //e and Videx VideoTerm)
- Low-Resolution graphics
- Double-Width Low-Resolution graphics (Apple //e only)
- High-Resolution graphics
@ -48,24 +65,32 @@ Portable emulator of an Apple II+ or //e. Written in Go.
- 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)
- ProDOS MLI calls tracing
- 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 is aux slot
- Memory Expansion card with 1Gb in slot 1
- 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
- ThunderClock Plus card in slot 4
- 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)
@ -100,7 +125,7 @@ Download the excellent [Total Replay](https://archive.org/details/TotalReplay) c
[a2-4am](https://github.com/a2-4am/4cade):
``` terminal
casa@servidor:~$ ./izapple2sdl Total\ Replay\ v4.0-alpha.3.hdv
casa@servidor:~$ ./izapple2sdl Total\ Replay\ v4.0.hdv
```
Displays super hi-res box art as seen with the VidHD card.
@ -144,112 +169,138 @@ Line:
```
### Keys
- Ctrl-F1: Reset button
- F5: Toggle speed between real and fastest
- Ctrl-F5: Show current speed in Mhz
- F6: Toggle between NTSC color TV and green phosphor monochrome monitor
- F7: Show the video mode and a split screen with the views for NTSC color TV, page 1, page 2 and extra info.
- F10: Cycle character generator code pages. Only if the character generator ROM has more than one 2Kb page.
- F11: Toggle on and off the trace to console of the CPU execution
- F12: Save a screen snapshot to a file `snapshot.png`
- Pause: Pause the emulation
Only valid on SDL mode
### Command line options
<!-- doc/usage.txt start -->
```terminal
-charRom string
rom file for the character generator (default "<default>")
-disk string
file to load on the first disk drive (default "<internal>/dos33.dsk")
-disk2Slot int
slot for the disk driver. -1 for none. (default 6)
-disk35 string
file to load on the SmartPort disk (slot 5)
-diskRom string
rom file for the disk drive controller (default "<internal>/DISK2.rom")
-diskb string
file to load on the second disk drive
-dumpChars
shows the character map
-fastChipSlot int
slot for the FASTChip accelerator card, -1 for none (default 3)
-fastDisk
set fast mode when the disks are spinning (default true)
-hd string
file to load on the boot hard disk
-hdSlot int
slot for the hard drive if present. -1 for none. (default -1)
-languageCardSlot int
slot for the 16kb language card. -1 for none
-memoryExpSlot int
slot for the Memory Expansion card with 1GB. -1 for none (default 1)
-mhz float
cpu speed in Mhz, use 0 for full speed. Use F5 to toggle. (default 1.0227142857142857)
Usage: izapple2 [file]
file
path to image to use on the boot device
-charrom string
rom file for the character generator (default "<internal>/Apple IIe Video Enhanced.bin")
-cpu string
cpu type, can be '6502' or '65c02' (default "65c02")
-forceCaps
force all letters to be uppercased (no need for caps lock!)
-model string
set base model. Models available 2plus, 2e, 2enh, base64a (default "2enh")
-mono
emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle.
-nsc int
add a DS1216 No-Slot-Clock on the main ROM (use 0) or a slot ROM. -1 for none (default -1)
-panicSS
panic if a not implemented softswitch is used
set base model (default "2enh")
-mods string
comma separated list of mods applied to the board, available mods are 'shift', 'four-colors
-nsc string
add a DS1216 No-Slot-Clock on the main ROM (use 'main') or a slot ROM (default "main")
-profile
generate profile trace to analyse with pprof
-ramworks int
memory to use with RAMWorks card, 0 for no card, max is 16384 (default 8192)
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 (default true)
emulate the RGB modes of the 80col RGB card for DHGR
-rom string
main rom file (default "<default>")
-saturnCardSlot int
slot for the 256kb Saturn card. -1 for none (default -1)
-thunderClockCardSlot int
slot for the ThunderClock Plus card. -1 for none (default 4)
-traceCpu
dump to the console the CPU execution. Use F11 to toggle.
-traceHD
dump to the console the hd/smartport commands
-traceMLI
dump to the console the calls to ProDOS machine language interface calls to $BF00
-traceSS
dump to the console the sofswitches calls
-traceSSReg
dump to the console the sofswitch registrations
-vidHDSlot int
slot for the VidHD card, only for //e models. -1 for none (default 2)
-woz string
show WOZ file information
main rom file (default "<internal>/Apple2e_Enhanced.rom")
-romx
emulate a RomX
-s0 string
slot 0 configuration. (default "language")
-s1 string
slot 1 configuration. (default "empty")
-s2 string
slot 2 configuration. (default "vidhd")
-s3 string
slot 3 configuration. (default "fastchip")
-s4 string
slot 4 configuration. (default "empty")
-s5 string
slot 5 configuration. (default "empty")
-s6 string
slot 6 configuration. (default "diskii,disk1=<internal>/dos33.dsk")
-s7 string
slot 7 configuration. (default "empty")
-speed string
cpu speed in Mhz, can be 'ntsc', 'pal', 'full' or a decimal nunmber (default "ntsc")
-trace string
trace CPU execution with one or more comma separated tracers (default "none")
The available pre-configured models are:
2: Apple ][
2e: Apple IIe
2enh: Apple //e
2plus: Apple ][+
base64a: Base 64A
swyft: swyft
The available cards are:
brainboard: Firmware card. It has two ROM banks
brainboard2: Firmware card. It has up to four ROM banks
dan2sd: Apple II Peripheral Card that Interfaces to a ATMEGA328P for SD card storage
diskii: Disk II interface card
diskiiseq: Disk II interface card emulating the Woz state machine
fastchip: Accelerator card for Apple IIe (limited support)
fujinet: SmartPort interface card hosting the Fujinet
inout: Card to test I/O
language: Language card with 16 extra KB for the Apple ][ and ][+
memexp: Memory expansion card
mouse: Mouse card implementation, does not emulate a real card, only the firmware behaviour
multirom: Multiple Image ROM card
parallel: Card to dump to a file what would be printed to a parallel printer
prodosromcard3: A bootable 4 MB ROM card by Ralle Palaveev
prodosromdrive: A bootable 1 MB solid state disk by Terence Boldt
saturn: RAM card with 128Kb, it's like 8 language cards
smartport: SmartPort interface card
softswitchlogger: Card to log softswitch accesses
swyftcard: Card with the ROM needed to run the Swyftcard word processing system
thunderclock: Clock card
videx: Videx compatible 80 columns card
vidhd: Firmware signature of the VidHD card to trick Total Replay to use the SHR mode
The available tracers are:
cpm65: Trace CPM65 BDOS calls
cpu: Trace CPU execution
mli: Trace ProDOS MLI calls
mos: Trace MOS calls with Applecorn skipping terminal IO
mosfull: Trace MOS calls with Applecorn
panicss: Panic on unimplemented softswitches
ss: Trace sotfswiches calls
ssreg: Trace sotfswiches registrations
ucsd: Trace UCSD system calls
```
<!-- doc/usage.txt end -->
## Building from source
### izapple2console
### Linux
The only dependency is having a working Go installation on any platform.
Besides having a working Go installation, install the SDL2 developer files. Run:
``` terminal
git clone github.com/ivanizag/izapple2
cd izapple2/frontend/a2sdl
go build .
```
### MacOS
With a working Go installation, run:
``` terminal
brew install SDL2
git clone github.com/ivanizag/izapple2
cd izapple2/frontend/a2sdl
go build .
```
### Windows
On Windows, CGO needs a gcc compiler. Install [mingw-w64](http://mingw-w64.org/doku.php/download/mingw-builds) and the [SDL2 developer files](https://www.libsdl.org/release/) for mingw-64.
Run:
``` terminal
go get github.com/ivanizag/izapple2/izapple2console
go build github.com/ivanizag/izapple2/izapple2console
git clone github.com/ivanizag/izapple2
cd izapple2\frontend\a2sdl
go build .
```
### izapple2sdl
Besides having a working Go installation, install the SDL2 developer files. Valid for any platform
Run:
``` terminal
go get github.com/ivanizag/izapple2/izapple2sdl
go build github.com/ivanizag/izapple2/izapple2sdl
```
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
@ -259,5 +310,3 @@ To create executables for Linux and Windows without installing Go, SDL2 or the W
cd docker
./build.sh
```
To run in Windows, copy the file `SDL2.dll` on the same folder as `izapple2sdl.exe`. The latest `SDL2.dll` can be found in the [Runtime binary for Windows 64-bit](https://www.libsdl.org/download-2.0.php).

96
a2audit_test.go Normal file
View 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")
})
*/
}

238
apple2.go
View File

@ -1,110 +1,61 @@
package izapple2
import (
"fmt"
"time"
"sync/atomic"
"github.com/ivanizag/izapple2/core6502"
"github.com/ivanizag/iz6502"
)
// Apple2 represents all the components and state of the emulated machine
type Apple2 struct {
Name string
cpu *core6502.State
mmu *memoryManager
io *ioC0Page
cg *CharacterGenerator
cards [8]card
isApple2e bool
commandChannel chan int
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
isColor bool
fastMode bool
fastRequestsCounter int
profile bool
showSpeed bool
paused bool
traceMLI *traceProDOS
Name string
cpu *iz6502.State
mmu *memoryManager
io *ioC0Page
cg *CharacterGenerator
cards [8]Card
tracers []executionTracer
softVideoSwitch *SoftVideoSwitch
board string
isApple2e bool
isFourColors bool // An Apple II without the 6 color mod
commandChannel chan command
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
fastRequestsCounter int32
cycleBreakpoint uint64
breakPoint bool
profile bool
showSpeed bool
paused bool
forceCaps bool
removableMediaDrives []drive
}
const (
// CPUClockMhz is the actual Apple II clock speed
CPUClockMhz = 14.318 / 14
cpuClockEuroMhz = 14.238 / 14
)
// GetCards returns the array of inserted cards
func (a *Apple2) GetCards() [8]Card {
return a.cards
}
const (
maxWaitDuration = 100 * time.Millisecond
cpuSpinLoops = 100
)
// SetKeyboardProvider attaches an external keyboard provider
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
a.io.setKeyboardProvider(kb)
}
// Run starts the Apple2 emulation
func (a *Apple2) Run() {
// SetSpeakerProvider attaches an external keyboard provider
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
a.io.setSpeakerProvider(s)
}
// Start the processor
a.cpu.Reset()
referenceTime := time.Now()
speedReferenceTime := referenceTime
speedReferenceCycles := uint64(0)
// SetJoysticksProvider attaches an external joysticks provider
func (a *Apple2) SetJoysticksProvider(j JoysticksProvider) {
a.io.setJoysticksProvider(j)
}
for {
// Run a 6502 step
if !a.paused {
for i := 0; i < cpuSpinLoops; i++ {
a.cpu.ExecuteInstruction()
a.executionTrace()
}
} else {
time.Sleep(200 * time.Millisecond)
}
// Execute meta commands
commandsPending := true
for commandsPending {
select {
case command := <-a.commandChannel:
switch command {
case CommandKill:
return
case CommandPauseUnpauseEmulator:
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
}
}
// SetMouseProvider attaches an external joysticks provider
func (a *Apple2) SetMouseProvider(m MouseProvider) {
a.io.setMouseProvider(m)
}
// IsPaused returns true when emulator is paused
@ -112,8 +63,18 @@ func (a *Apple2) IsPaused() bool {
return a.paused
}
func (a *Apple2) setProfiling(value bool) {
a.profile = value
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
@ -121,89 +82,20 @@ func (a *Apple2) IsProfiling() bool {
return a.profile
}
const (
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
CommandToggleSpeed = iota + 1
// CommandShowSpeed toggles printinf the current freq in Mhz
CommandShowSpeed
// CommandToggleColor toggles between NTSC color TV and Green phospor monitor
CommandToggleColor
// CommandDumpDebugInfo dumps usefull info
CommandDumpDebugInfo
// CommandNextCharGenPage cycles the CharGen page if several
CommandNextCharGenPage
// CommandToggleCPUTrace toggle tracing of CPU execution
CommandToggleCPUTrace
// CommandKill stops the cpu execution loop
CommandKill
// CommandReset executes a 6502 reset
CommandReset
// CommandPauseUnpauseEmulator allows the Pause button to freeze the emulator for a coffee break
CommandPauseUnpauseEmulator
)
// SendCommand enqueues a command to the emulator thread
func (a *Apple2) SendCommand(command int) {
a.commandChannel <- command
// IsForceCaps returns true when all letters are forced to upper case
func (a *Apple2) IsForceCaps() bool {
return a.forceCaps
}
func (a *Apple2) executeCommand(command int) {
switch command {
case CommandToggleSpeed:
if a.cycleDurationNs == 0 {
fmt.Println("Slow")
a.cycleDurationNs = 1000.0 / CPUClockMhz
} else {
fmt.Println("Fast")
a.cycleDurationNs = 0
}
case CommandShowSpeed:
a.showSpeed = !a.showSpeed
case CommandToggleColor:
a.isColor = !a.isColor
case CommandDumpDebugInfo:
a.dumpDebugInfo()
case CommandNextCharGenPage:
a.cg.nextPage()
fmt.Printf("Chargen page %v\n", a.cg.page)
case CommandToggleCPUTrace:
a.cpu.SetTrace(!a.cpu.GetTrace())
case CommandReset:
a.cpu.Reset()
}
}
func (a *Apple2) requestFastMode() {
func (a *Apple2) RequestFastMode() {
// Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain.
if a.fastMode {
a.fastRequestsCounter++
}
atomic.AddInt32(&a.fastRequestsCounter, 1)
}
func (a *Apple2) releaseFastMode() {
if a.fastMode {
a.fastRequestsCounter--
}
func (a *Apple2) ReleaseFastMode() {
atomic.AddInt32(&a.fastRequestsCounter, -1)
}
func (a *Apple2) executionTrace() {
if a.traceMLI != nil {
a.traceMLI.inspect()
}
}
func (a *Apple2) dumpDebugInfo() {
// See "Apple II Monitors Peeled"
pageZeroSymbols := map[int]string{
0x36: "CSWL",
0x37: "CSWH",
0x38: "KSWL",
0x39: "KSWH",
}
fmt.Printf("Page zero values:\n")
for _, k := range []int{0x36, 0x37, 0x38, 0x39} {
d := a.mmu.physicalMainRAM.data[k]
fmt.Printf(" %v(0x%x): 0x%02x\n", pageZeroSymbols[k], k, d)
}
func (a *Apple2) registerRemovableMediaDrive(d drive) {
a.removableMediaDrives = append(a.removableMediaDrives, d)
}

159
apple2Run.go Normal file
View 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
}
}

View File

@ -1,227 +0,0 @@
package izapple2
import (
"errors"
"github.com/ivanizag/izapple2/core6502"
)
func newApple2() *Apple2 {
var a Apple2
a.Name = "Pending"
a.mmu = newMemoryManager(&a)
a.io = newIoC0Page(&a)
return &a
}
func (a *Apple2) setup(isColor bool, clockMhz float64, fastMode bool, traceMLI bool) {
a.commandChannel = make(chan int, 100)
a.isColor = isColor
a.fastMode = fastMode
if traceMLI {
a.traceMLI = newTraceProDOS(a)
}
if clockMhz <= 0 {
// Full speed
a.cycleDurationNs = 0
} else {
a.cycleDurationNs = 1000.0 / clockMhz
}
}
func setApple2plus(a *Apple2) {
a.Name = "Apple ][+"
a.cpu = core6502.NewNMOS6502(a.mmu)
addApple2SoftSwitches(a.io)
}
func setApple2e(a *Apple2) {
a.Name = "Apple IIe"
a.isApple2e = true
a.cpu = core6502.NewNMOS6502(a.mmu)
a.mmu.initExtendedRAM(1)
addApple2SoftSwitches(a.io)
addApple2ESoftSwitches(a.io)
}
func setApple2eEnhanced(a *Apple2) {
a.Name = "Apple //e"
a.isApple2e = true
a.cpu = core6502.NewCMOS65c02(a.mmu)
a.mmu.initExtendedRAM(1)
addApple2SoftSwitches(a.io)
addApple2ESoftSwitches(a.io)
}
func (a *Apple2) insertCard(c card, slot int) {
c.assign(a, slot)
a.cards[slot] = c
}
const (
apple2RomSize = 12 * 1024
apple2eRomSize = 16 * 1024
)
// LoadRom loads a standard Apple2+ or 2e ROM
func (a *Apple2) LoadRom(filename string) error {
data, err := loadResource(filename)
if err != nil {
return err
}
size := len(data)
if size != apple2RomSize && size != apple2eRomSize {
return errors.New("Rom size not supported")
}
romBase := 0x10000 - size
a.mmu.physicalROM[0] = newMemoryRangeROM(uint16(romBase), data, "Main ROM")
return nil
}
// AddDisk2 inserts a DiskII controller
func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage, diskBImage string) error {
var c cardDisk2
data, err := loadResource(diskRomFile)
if err != nil {
return err
}
c.loadRom(data)
a.insertCard(&c, slot)
if diskImage != "" {
diskette, err := loadDiskette(diskImage)
if err != nil {
return err
}
c.drive[0].insertDiskette(diskette)
}
if diskBImage != "" {
diskette, err := loadDiskette(diskBImage)
if err != nil {
return err
}
c.drive[1].insertDiskette(diskette)
}
return nil
}
// AddSmartPortDisk adds a smart port card and image
func (a *Apple2) AddSmartPortDisk(slot int, hdImage string, trace bool) error {
var c cardHardDisk
c.setTrace(trace)
c.loadRom(buildHardDiskRom(slot))
a.insertCard(&c, slot)
hd, err := openBlockDisk(hdImage)
if err != nil {
return err
}
c.addDisk(hd)
return nil
}
// AddVidHD adds a card with the signature of VidHD
func (a *Apple2) AddVidHD(slot int) {
var c cardVidHD
c.loadRom(buildVidHDRom())
a.insertCard(&c, slot)
}
// AddFastChip adds a card with the signature of VidHD
func (a *Apple2) AddFastChip(slot int) {
var c cardFastChip
c.loadRom(buildFastChipRom())
a.insertCard(&c, slot)
}
// AddLanguageCard inserts a 16Kb card
func (a *Apple2) AddLanguageCard(slot int) {
a.insertCard(&cardLanguage{}, slot)
}
// AddSaturnCard inserts a 128Kb card
func (a *Apple2) AddSaturnCard(slot int) {
a.insertCard(&cardSaturn{}, slot)
}
// AddMemoryExpansionCard inserts an Apple II Memory Expansion card with 1GB
func (a *Apple2) AddMemoryExpansionCard(slot int, romFile string) error {
var c cardMemoryExpansion
data, err := loadResource(romFile)
if err != nil {
return err
}
c.loadRom(data)
a.insertCard(&c, slot)
return nil
}
// AddThunderClockPlusCard inserts a ThunderClock Plus clock card
func (a *Apple2) AddThunderClockPlusCard(slot int, romFile string) error {
var c cardThunderClockPlus
data, err := loadResource(romFile)
if err != nil {
return err
}
c.loadRom(data)
a.insertCard(&c, slot)
return nil
}
// AddRGBCard inserts an RBG option to the Apple IIe 80 col 64KB card
func (a *Apple2) AddRGBCard() {
setupRGBCard(a)
}
// AddRAMWorks inserts adds RAMWorks style RAM to the Apple IIe 80 col 64KB card
func (a *Apple2) AddRAMWorks(banks int) {
setupRAMWorksCard(a, banks)
}
// AddNoSlotClock inserts a DS1215 no slot clock under the main ROM
func (a *Apple2) AddNoSlotClock() {
nsc := newNoSlotClockDS1216(a, a.mmu.physicalROM[0])
a.mmu.physicalROM[0] = nsc
}
// AddNoSlotClockInCard inserts a DS1215 no slot clock under a card ROM
func (a *Apple2) AddNoSlotClockInCard(slot int) error {
cardRom := a.mmu.cardsROM[slot]
if cardRom == nil {
return errors.New("No ROM available on the slot to add a no slot clock")
}
nsc := newNoSlotClockDS1216(a, cardRom)
a.mmu.cardsROM[slot] = nsc
return nil
}
// AddCardLogger inserts a fake card that logs accesses
func (a *Apple2) AddCardLogger(slot int) {
a.insertCard(&cardLogger{}, slot)
}
// AddCardInOut inserts a fake card that interfaces with the emulator host
func (a *Apple2) AddCardInOut(slot int) {
a.insertCard(&cardInOut{}, slot)
}
// SetKeyboardProvider attaches an external keyboard provider
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
a.io.setKeyboardProvider(kb)
}
// SetSpeakerProvider attaches an external keyboard provider
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
a.io.setSpeakerProvider(s)
}
// SetJoysticksProvider attaches an external joysticks provider
func (a *Apple2) SetJoysticksProvider(j JoysticksProvider) {
a.io.setJoysticksProvider(j)
}

99
apple2Tester.go Normal file
View 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
}
}

View File

@ -1,325 +0,0 @@
package izapple2
import (
"flag"
"os"
)
const defaultInternal = "<default>"
// MainApple is a device independant main. Video, keyboard and speaker won't be defined
func MainApple() *Apple2 {
romFile := flag.String(
"rom",
defaultInternal,
"main rom file")
disk2RomFile := flag.String(
"diskRom",
"<internal>/DISK2.rom",
"rom file for the disk drive controller")
disk2Slot := flag.Int(
"disk2Slot",
6,
"slot for the disk driver. -1 for none.")
diskImage := flag.String(
"disk",
"<internal>/dos33.dsk",
"file to load on the first disk drive")
diskBImage := flag.String(
"diskb",
"",
"file to load on the second disk drive")
wozImage := flag.String(
"woz",
"",
"show WOZ file information")
hardDiskImage := flag.String(
"hd",
"",
"file to load on the boot hard disk (slot 7)")
hardDiskSlot := flag.Int(
"hdSlot",
-1,
"slot for the hard drive if present. -1 for none.")
smartPortImage := flag.String(
"disk35",
"",
"file to load on the SmartPort disk (slot 5)")
cpuClock := flag.Float64(
"mhz",
CPUClockMhz,
"cpu speed in Mhz, use 0 for full speed. Use F5 to toggle.")
charRomFile := flag.String(
"charRom",
defaultInternal,
"rom file for the character generator")
languageCardSlot := flag.Int(
"languageCardSlot",
0,
"slot for the 16kb language card. -1 for none")
saturnCardSlot := flag.Int(
"saturnCardSlot",
-1,
"slot for the 256kb Saturn card. -1 for none")
vidHDCardSlot := flag.Int(
"vidHDSlot",
2,
"slot for the VidHD card, only for //e models. -1 for none")
fastChipCardSlot := flag.Int(
"fastChipSlot",
3,
"slot for the FASTChip accelerator card, -1 for none")
memoryExpansionCardSlot := flag.Int(
"memoryExpSlot",
1,
"slot for the Memory Expansion card with 1GB. -1 for none")
ramWorksKb := flag.Int(
"ramworks",
8192,
"memory to use with RAMWorks card, 0 for no card, max is 16384")
thunderClockCardSlot := flag.Int(
"thunderClockCardSlot",
4,
"slot for the ThunderClock Plus card. -1 for none")
nsc := flag.Int(
"nsc",
-1,
"add a DS1216 No-Slot-Clock on the main ROM (use 0) or a slot ROM. -1 for none")
mono := flag.Bool(
"mono",
false,
"emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle.")
rgbCard := flag.Bool(
"rgb",
true,
"emulate the RGB modes of the 80col RGB card for DHGR")
fastDisk := flag.Bool(
"fastDisk",
true,
"set fast mode when the disks are spinning")
panicSS := flag.Bool(
"panicSS",
false,
"panic if a not implemented softswitch is used")
traceCPU := flag.Bool(
"traceCpu",
false,
"dump to the console the CPU execution operations")
traceSS := flag.Bool(
"traceSS",
false,
"dump to the console the sofswitches calls")
traceSSReg := flag.Bool(
"traceSSReg",
false,
"dump to the console the sofswitch registrations")
traceHD := flag.Bool(
"traceHD",
false,
"dump to the console the hd/smarport commands")
dumpChars := flag.Bool(
"dumpChars",
false,
"shows the character map")
model := flag.String(
"model",
"2enh",
"set base model. Models available 2plus, 2e, 2enh, base64a")
profile := flag.Bool(
"profile",
false,
"generate profile trace to analyse with pprof")
traceMLI := flag.Bool(
"traceMLI",
false,
"dump to the console the calls to ProDOS machine language interface calls to $BF00")
flag.Parse()
// Process a filename with autodetection
filename := flag.Arg(0)
diskImageFinal := *diskImage
hardDiskImageFinal := *hardDiskImage
if filename != "" {
if isDiskette(filename) {
diskImageFinal = filename
} else {
hardDiskImageFinal = filename
}
}
if *wozImage != "" {
f, err := loadFileWoz(*wozImage)
if err != nil {
panic(err)
}
f.dump()
return nil
}
a := newApple2()
a.setup(!*mono, *cpuClock, *fastDisk, *traceMLI)
a.io.setTrace(*traceSS)
a.io.setTraceRegistrations(*traceSSReg)
a.io.setPanicNotImplemented(*panicSS)
a.setProfiling(*profile)
var charGenMap charColumnMap
initialCharGenPage := 0
switch *model {
case "2plus":
setApple2plus(a)
if *romFile == defaultInternal {
*romFile = "<internal>/Apple2_Plus.rom"
}
if *charRomFile == defaultInternal {
*charRomFile = "<internal>/Apple2rev7CharGen.rom"
}
charGenMap = charGenColumnsMap2Plus
*vidHDCardSlot = -1
case "2e":
setApple2e(a)
if *romFile == defaultInternal {
*romFile = "<internal>/Apple2e.rom"
}
if *charRomFile == defaultInternal {
*charRomFile = "<internal>/Apple IIe Video Unenhanced - 342-0133-A - 2732.bin"
}
a.isApple2e = true
charGenMap = charGenColumnsMap2e
case "2enh":
setApple2eEnhanced(a)
if *romFile == defaultInternal {
*romFile = "<internal>/Apple2e_Enhanced.rom"
}
if *charRomFile == defaultInternal {
*charRomFile = "<internal>/Apple IIe Video Enhanced - 342-0265-A - 2732.bin"
}
a.isApple2e = true
charGenMap = charGenColumnsMap2e
case "base64a":
setBase64a(a)
if *romFile == defaultInternal {
err := loadBase64aRom(a)
if err != nil {
panic(err)
}
*romFile = ""
}
if *charRomFile == defaultInternal {
*charRomFile = "<internal>/BASE64A_ROM7_CharGen.BIN"
initialCharGenPage = 1
}
charGenMap = charGenColumnsMapBase64a
*vidHDCardSlot = -1
default:
panic("Model not supported")
}
a.cpu.SetTrace(*traceCPU)
// Load ROM if not loaded already
if *romFile != "" {
err := a.LoadRom(*romFile)
if err != nil {
panic(err)
}
}
// Load character generator if it loaded already
cg, err := newCharacterGenerator(*charRomFile, charGenMap)
if err != nil {
panic(err)
}
cg.setPage(initialCharGenPage)
a.cg = cg
// Externsion cards
if *languageCardSlot >= 0 {
a.AddLanguageCard(*languageCardSlot)
}
if *saturnCardSlot >= 0 {
a.AddSaturnCard(*saturnCardSlot)
}
if *memoryExpansionCardSlot >= 0 {
err := a.AddMemoryExpansionCard(*memoryExpansionCardSlot,
"<internal>/MemoryExpansionCard-341-0344a.bin")
if err != nil {
panic(err)
}
}
if *thunderClockCardSlot > 0 {
err := a.AddThunderClockPlusCard(*thunderClockCardSlot,
"<internal>/ThunderclockPlusROM.bin")
if err != nil {
panic(err)
}
}
if *vidHDCardSlot >= 0 {
a.AddVidHD(*vidHDCardSlot)
}
if *smartPortImage != "" {
err := a.AddSmartPortDisk(5, *smartPortImage, *traceHD)
if err != nil {
panic(err)
}
}
if *fastChipCardSlot >= 0 {
a.AddFastChip(*fastChipCardSlot)
}
if *disk2Slot > 0 {
err := a.AddDisk2(*disk2Slot, *disk2RomFile, diskImageFinal, *diskBImage)
if err != nil {
panic(err)
}
}
if hardDiskImageFinal != "" {
if *hardDiskSlot <= 0 {
// If there is a hard disk image, but no slot assigned, use slot 7.
*hardDiskSlot = 7
}
err := a.AddSmartPortDisk(*hardDiskSlot, hardDiskImageFinal, *traceHD)
if err != nil {
panic(err)
}
}
if *ramWorksKb != 0 {
if *ramWorksKb%64 != 0 {
panic("Ramworks size must be a multiple of 64")
}
a.AddRAMWorks(*ramWorksKb / 64)
}
if *rgbCard {
a.AddRGBCard()
}
if *nsc == 0 {
a.AddNoSlotClock()
} else if *nsc > 0 {
err := a.AddNoSlotClockInCard(*nsc)
if err != nil {
panic(err)
}
}
//a.AddCardInOut(2)
//a.AddCardLogger(4)
if *dumpChars {
a.cg.Dump()
os.Exit(0)
return nil
}
return a
}

View File

@ -1,22 +1,11 @@
package izapple2
import (
"fmt"
"github.com/ivanizag/izapple2/core6502"
)
import "fmt"
/*
Copam BASE64A adaptation.
*/
func setBase64a(a *Apple2) {
a.Name = "Base 64A"
a.cpu = core6502.NewNMOS6502(a.mmu)
addApple2SoftSwitches(a.io)
addBase64aSoftSwitches(a.io)
}
const (
// There are 6 ROM chips. Each can have 4Kb or 8Kb. They can fill
// 2 or 4 banks with 2kb windows.
@ -35,7 +24,7 @@ func loadBase64aRom(a *Apple2) error {
for i := 0; i < base64aRomChipCount; i++ {
filename := fmt.Sprintf("<internal>/BASE64A_%X.BIN", 0xd0+i*0x08)
data, err := loadResource(filename)
data, _, err := LoadResource(filename)
if err != nil {
return err
}
@ -45,48 +34,52 @@ func loadBase64aRom(a *Apple2) error {
}
}
// Create banks
for j := range romBanksBytes {
a.mmu.physicalROM[j] = newMemoryRange(0xd000, romBanksBytes[j], fmt.Sprintf("Base64 ROM page %v", j))
// Create paged ROM
romData := make([]uint8, 0, base64aRomBankSize*base64aRomBankCount)
for _, bank := range romBanksBytes {
romData = append(romData, bank...)
}
rom := newMemoryRangePagedROM(0xd000, romData, "Base64 ROM", base64aRomBankCount)
// Start with first bank active
a.mmu.setActiveROMPage(0)
rom.setPage(0)
a.mmu.physicalROM = rom
return nil
}
func addBase64aSoftSwitches(io *ioC0Page) {
// Other softswitches, not implemented but called from the ROM
io.addSoftSwitchW(0x0C, notImplementedSoftSwitchW, "80COLOFF")
io.addSoftSwitchW(0x0E, notImplementedSoftSwitchW, "ALTCHARSETOFF")
// Write on the speaker. That is a double access and should do nothing
// but works somehow on the BASE64A
io.addSoftSwitchW(0x30, func(io *ioC0Page, value uint8) {
speakerSoftSwitch(io)
}, "SPEAKER")
io.addSoftSwitchW(0x0C, buildNotImplementedSoftSwitchW(io), "80COLOFF")
io.addSoftSwitchW(0x0E, buildNotImplementedSoftSwitchW(io), "ALTCHARSETOFF")
// ROM pagination softswitches. They use the annunciator 0 and 1
mmu := io.apple2.mmu
io.addSoftSwitchRW(0x58, func(*ioC0Page) uint8 {
p := mmu.getActiveROMPage()
mmu.setActiveROMPage(p & 2)
io.addSoftSwitchRW(0x58, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p & 2)
}
return 0
}, "ANN0OFF-ROM")
io.addSoftSwitchRW(0x59, func(*ioC0Page) uint8 {
p := mmu.getActiveROMPage()
mmu.setActiveROMPage(p | 1)
io.addSoftSwitchRW(0x59, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p | 1)
}
return 0
}, "ANN0ON-ROM")
io.addSoftSwitchRW(0x5A, func(*ioC0Page) uint8 {
p := mmu.getActiveROMPage()
mmu.setActiveROMPage(p & 1)
io.addSoftSwitchRW(0x5A, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p & 1)
}
return 0
}, "ANN1OFF-ROM")
io.addSoftSwitchRW(0x5B, func(*ioC0Page) uint8 {
p := mmu.getActiveROMPage()
mmu.setActiveROMPage(p | 2)
io.addSoftSwitchRW(0x5B, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p | 2)
}
return 0
}, "ANN1ON-ROM")

View File

@ -1,15 +1,29 @@
package izapple2
type card interface {
loadRom(data []uint8)
import (
"fmt"
)
// Card represents an Apple II card to be inserted in a slot
type Card interface {
loadRom(data []uint8, layout cardRomLayout) error
assign(a *Apple2, slot int)
reset()
setName(name string)
setDebug(debug bool)
GetName() string
GetInfo() map[string]string
}
type cardBase struct {
a *Apple2
romCsxx *memoryRangeROM
romC8xx *memoryRangeROM
romCxxx *memoryRangeROM
a *Apple2
name string
trace bool
romCsxx *memoryRangeROM
romC8xx memoryHandler
romCxxx memoryHandler
slot int
_ssr [16]softSwitchR
_ssw [16]softSwitchW
@ -17,25 +31,99 @@ type cardBase struct {
_sswName [16]string
}
func (c *cardBase) loadRom(data []uint8) {
func (c *cardBase) setName(name string) {
c.name = name
}
func (c *cardBase) GetName() string {
return c.name
}
func (c *cardBase) GetInfo() map[string]string {
return nil
}
func (c *cardBase) setDebug(debug bool) {
c.trace = debug
}
func (c *cardBase) reset() {
// nothing
}
type cardRomLayout int
const (
cardRomSimple cardRomLayout = iota // The ROM is on the slot area, there can be more than one page
cardRomUpper // The ROM is on the full C800 area. The slot area copies C8xx
cardRomUpperHalfEnd // The ROM is on half of the C800 areas. The slot area copies CBxx
cardRomFull // The ROM is on the full Cxxx area, with pages for each slot position
)
func (c *cardBase) loadRomFromResource(resource string, layout cardRomLayout) error {
data, _, err := LoadResource(resource)
if err != nil {
// The resource should be internal and never fail
return err
}
err = c.loadRom(data, layout)
if err != nil {
return err
}
return nil
}
func (c *cardBase) loadRom(data []uint8, layout cardRomLayout) error {
if c.a != nil {
panic("Assert failed. Rom must be loaded before inserting the card in the slot")
return fmt.Errorf("ROM must be loaded before inserting the card in the slot")
}
if len(data) == 0x100 {
// Just 256 bytes in Cs00
c.romCsxx = newMemoryRangeROM(0, data, "Slot ROM")
} else if len(data) == 0x800 {
// The file has C800 to C8FF
// The 256 bytes in Cx00 are copied from the first page in C800
c.romCsxx = newMemoryRangeROM(0, data, "Slor ROM")
c.romC8xx = newMemoryRangeROM(0xc800, data, "Slot C8 ROM")
} else 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 {
panic("Invalid ROM size")
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) {
@ -75,3 +163,35 @@ func (c *cardBase) addCardSoftSwitchW(address uint8, ss softSwitchW, name string
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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,
}

View 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)
}
}

View File

@ -2,6 +2,9 @@ package izapple2
import (
"fmt"
"strconv"
"github.com/ivanizag/izapple2/storage"
)
/*
@ -18,98 +21,162 @@ http://yesterbits.com/media/pubs/AppleOrchard/articles/disk-ii-part-1-1983-apr.p
NIB: 35 tracks 6656 bytes, 232960 bytes
*/
const maxHalfTrack = 68
type cardDisk2 struct {
// CardDisk2 is a DiskII interface card
type CardDisk2 struct {
cardBase
selected int // q5, Only 0 and 1 supported
selected int // q5, Only 0 and 1 supported
power bool // q4
drive [2]cardDisk2Drive
fastMode bool
dataLatch uint8
q6 bool
q7 bool
trackTracer trackTracer
}
type drive interface {
insertDiskette(path string) error
}
type cardDisk2Drive struct {
diskette diskette
power bool // q4
phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor
tracksStep int // Stepmotor for tracks position. 4 steps per track
name string
diskette storage.Diskette
phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor
trackStep int // Stepmotor for tracks position. 4 steps per track
}
const (
diskBitCycle = 4 // There is a dataLatch bit transferred every 4 cycles
diskLatchReadCycles = 7 // Loaded data is available for a little more than 7ns
diskWriteByteCycle = 32 // Load data to write every 32 cycles
diskWriteSelfSyncCycle = 40 // Save $FF every 40 cycles. Self sync is 10 bits: 1111 1111 00
diskMotorStartMs = 150 // Time with the disk spinning to get full speed
func newCardDisk2Builder() *cardBuilder {
return &cardBuilder{
name: "Disk II",
description: "Disk II interface card",
defaultParams: &[]paramSpec{
{"disk1", "Diskette image for drive 1", ""},
{"disk2", "Diskette image for drive 2", ""},
{"tracktracer", "Trace how the disk head moves between tracks", "false"},
{"fast", "Enable CPU burst when accessing the disk", "true"},
},
buildFunc: func(params map[string]string) (Card, error) {
var c CardDisk2
err := c.loadRomFromResource("<internal>/DISK2.rom", cardRomSimple)
if err != nil {
return nil, err
}
)
disk1 := paramsGetPath(params, "disk1")
if disk1 != "" {
err := c.drive[0].insertDiskette(disk1)
if err != nil {
return nil, err
}
}
disk2 := paramsGetPath(params, "disk2")
if disk2 != "" {
err := c.drive[1].insertDiskette(disk2)
if err != nil {
return nil, err
}
}
trackTracer := paramsGetBool(params, "tracktracer")
if trackTracer {
c.trackTracer = makeTrackTracerLogger()
}
c.fastMode = paramsGetBool(params, "fast")
return &c, nil
},
}
}
// GetInfo returns smartPort info
func (c *CardDisk2) GetInfo() map[string]string {
info := make(map[string]string)
info["rom"] = "16 sector"
info["power"] = strconv.FormatBool(c.power)
info["D1 name"] = c.drive[0].name
info["D1 track"] = strconv.FormatFloat(float64(c.drive[0].trackStep)/4, 'f', 2, 64)
info["D2 name"] = c.drive[1].name
info["D2 track"] = strconv.FormatFloat(float64(c.drive[1].trackStep)/4, 'f', 2, 64)
return info
}
func (c *CardDisk2) reset() {
// UtA2e 9-12, all switches forced to off
drive := &c.drive[c.selected]
drive.phases = 0 // q0, q1, q2, q3
c.softSwitchQ4(false) // q4
c.selected = 0 // q5
c.q6 = false
c.q7 = false
}
func (c *CardDisk2) setTrackTracer(tt trackTracer) {
c.trackTracer = tt
}
func (c *CardDisk2) assign(a *Apple2, slot int) {
a.registerRemovableMediaDrive(&c.drive[0])
a.registerRemovableMediaDrive(&c.drive[1])
func (c *cardDisk2) assign(a *Apple2, slot int) {
// Q1, Q2, Q3 and Q4 phase control soft switches,
for i := uint8(0); i < 4; i++ {
phase := i
c.addCardSoftSwitchR(phase<<1, func(_ *ioC0Page) uint8 {
c.addCardSoftSwitchRW(phase<<1, func() uint8 {
// Update magnets and position
drive := &c.drive[c.selected]
drive.phases &^= (1 << phase)
drive.tracksStep = moveStep(drive.phases, drive.tracksStep)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep, c.slot, c.selected)
}
return c.dataLatch // All even addresses return the last dataLatch
}, fmt.Sprintf("PHASE%vOFF", phase))
c.addCardSoftSwitchR((phase<<1)+1, func(_ *ioC0Page) uint8 {
// Update magnets and position
c.addCardSoftSwitchRW((phase<<1)+1, func() uint8 { // Update magnets and position
drive := &c.drive[c.selected]
drive.phases |= (1 << phase)
drive.tracksStep = moveStep(drive.phases, drive.tracksStep)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep, slot, c.selected)
}
return 0
}, fmt.Sprintf("PHASE%vON", phase))
}
// Q4, power switch
c.addCardSoftSwitchR(0x8, func(_ *ioC0Page) uint8 {
drive := &c.drive[c.selected]
if drive.power {
drive.power = false
c.a.releaseFastMode()
if drive.diskette != nil {
drive.diskette.powerOff(c.a.cpu.GetCycles())
}
}
c.addCardSoftSwitchRW(0x8, func() uint8 {
c.softSwitchQ4(false)
return c.dataLatch
}, "Q4DRIVEOFF")
c.addCardSoftSwitchR(0x9, func(_ *ioC0Page) uint8 {
drive := &c.drive[c.selected]
if !drive.power {
drive.power = true
c.a.requestFastMode()
if drive.diskette != nil {
drive.diskette.powerOn(c.a.cpu.GetCycles())
}
}
c.addCardSoftSwitchRW(0x9, func() uint8 {
c.softSwitchQ4(true)
return 0
}, "Q4DRIVEON")
// Q5, drive selecion
c.addCardSoftSwitchR(0xA, func(_ *ioC0Page) uint8 {
c.selected = 0
c.addCardSoftSwitchRW(0xA, func() uint8 {
c.softSwitchQ5(0)
return c.dataLatch
}, "Q5SELECT1")
c.addCardSoftSwitchR(0xB, func(_ *ioC0Page) uint8 {
c.selected = 1
c.addCardSoftSwitchRW(0xB, func() uint8 {
c.softSwitchQ5(1)
return 0
}, "Q5SELECT2")
// Q6, Q7
for i := uint8(0xC); i <= 0xF; i++ {
iCopy := i
c.addCardSoftSwitchR(iCopy, func(_ *ioC0Page) uint8 {
c.addCardSoftSwitchR(iCopy, func() uint8 {
return c.softSwitchQ6Q7(iCopy, 0)
}, "Q6Q7")
c.addCardSoftSwitchW(iCopy, func(_ *ioC0Page, value uint8) {
c.addCardSoftSwitchW(iCopy, func(value uint8) {
c.softSwitchQ6Q7(iCopy, value)
}, "Q6Q7")
}
@ -117,10 +184,48 @@ func (c *cardDisk2) assign(a *Apple2, slot int) {
c.cardBase.assign(a, slot)
}
func (c *CardDisk2) softSwitchQ4(value bool) {
if !value && c.power {
// Turn off
c.power = false
if c.fastMode {
c.a.ReleaseFastMode()
}
drive := &c.drive[c.selected]
if drive.diskette != nil {
drive.diskette.PowerOff(c.a.cpu.GetCycles())
}
} else if value && !c.power {
// Turn on
c.power = true
if c.fastMode {
c.a.RequestFastMode()
}
drive := &c.drive[c.selected]
if drive.diskette != nil {
drive.diskette.PowerOn(c.a.cpu.GetCycles())
}
}
}
func (c *CardDisk2) softSwitchQ5(selected int) {
if c.power && c.selected != selected {
// Selected changed with power on, power goes to the other disk
if c.drive[c.selected].diskette != nil {
c.drive[c.selected].diskette.PowerOff(c.a.cpu.GetCycles())
}
if c.drive[selected].diskette != nil {
c.drive[selected].diskette.PowerOn(c.a.cpu.GetCycles())
}
}
c.selected = selected
}
// Q6: shift/load
// Q7: read/write
func (c *cardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
func (c *CardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
switch index {
case 0xC: // Q6L
c.q6 = false
@ -140,16 +245,16 @@ func (c *cardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
return 0
}
func (c *cardDisk2) processQ6Q7(in uint8) {
func (c *CardDisk2) processQ6Q7(in uint8) {
d := &c.drive[c.selected]
if d.diskette == nil {
return
}
if !c.q6 { // shift
if !c.q7 { // Q6L-Q7L: Read
c.dataLatch = d.diskette.read(d.tracksStep, c.a.cpu.GetCycles())
c.dataLatch = d.diskette.Read(d.trackStep, c.a.cpu.GetCycles())
} else { // Q6L-Q7H: Write the dataLatch value to disk. Shift data out
d.diskette.write(d.tracksStep, c.dataLatch, c.a.cpu.GetCycles())
d.diskette.Write(d.trackStep, c.dataLatch, c.a.cpu.GetCycles())
}
} else { // load
if !c.q7 { // Q6H-Q7L: Sense write protect / prewrite state
@ -160,89 +265,20 @@ func (c *cardDisk2) processQ6Q7(in uint8) {
}
}
if c.dataLatch >= 0x80 {
//fmt.Printf("Datalacth: 0x%.2x in cycle %v\n", c.dataLatch, c.a.cpu.GetCycles())
}
}
/*
Stepper motor to position the track.
There are a number of group of four magnets. The stepper motor can be thought as a long
line of groups of magnets, each group on the same configuration. We call phase each of those
magnets. The cog is attracted to the enabled magnets, and can stay aligned to a magnet or
between two.
Phases (magents): 3 2 1 0 3 2 1 0 3 2 1 0
Cog direction (step withn a group): 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
We will consider that the cog would go to the prefferred postion if there is one. Independenly
of the previous position. The previous position is only used to know if it goes up or down
a full group.
*/
const (
undefinedPosition = -1
maxStep = 68 * 2 // What is the maximum quarter tracks a DiskII can go?
stepsPerGroup = 8
stepsPerTrack = 4
)
var cogPositions = []int{
undefinedPosition, // 0000, phases active
0, // 0001
2, // 0010
1, // 0011
4, // 0100
undefinedPosition, // 0101
3, // 0110
2, // 0111
6, // 1000
7, // 1001
undefinedPosition, // 1010
0, // 1011
5, // 1100
6, // 1101
4, // 1110
undefinedPosition, // 1111
}
func moveStep(phases uint8, prevStep int) int {
//fmt.Printf("magnets: 0x%x\n", phases)
cogPosition := cogPositions[phases]
if cogPosition == undefinedPosition {
// Don't move if magnets don't push on a defined direction.
return prevStep
}
prevPosition := prevStep % stepsPerGroup // Direction, step in the current group of magnets.
delta := cogPosition - prevPosition
if delta < 0 {
delta = delta + stepsPerGroup
}
var nextStep int
if delta < 4 {
// Steps up
nextStep = prevStep + delta
if nextStep > maxStep {
nextStep = maxStep
/*
if c.dataLatch >= 0x80 {
fmt.Printf("Datalach: 0x%.2x in cycle %v\n", c.dataLatch, c.a.cpu.GetCycles())
}
} else if delta == 4 {
// Don't move if magnets push on the oposite direction
nextStep = prevStep
} else { // delta > 4
// Steps down
nextStep = prevStep + delta - stepsPerGroup
if nextStep < 0 {
nextStep = 0
}
}
return nextStep
*/
}
func (d *cardDisk2Drive) insertDiskette(dt diskette) {
d.diskette = dt
func (d *cardDisk2Drive) insertDiskette(name string) error {
diskette, err := LoadDiskette(name)
if err != nil {
return err
}
d.name = name
d.diskette = diskette
return nil
}

83
cardDisk2DriveStepper.go Normal file
View 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
View 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
View 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)
}

View File

@ -6,13 +6,18 @@ from controlled speed to max speed the emulator can do.
Note: It ends up not being useful for Total Replay as loading from HD is already
very fast. HD blocks are loaded directly on the emulated RAM.
Note that it doesn't intefere with the Apple IIe 80 columns in slot 3. It doesn't
have ROM or slot specific sofswitches.
See:
https://github.com/a2-4am/4cade/blob/master/src/hw.accel.a
http://www.a2heaven.com/webshop/resources/pdf_document/18/82/c.pdf
*/
type cardFastChip struct {
// CardFastChip represents a
type CardFastChip struct {
cardBase
unlocked bool
unlockCounter uint8
@ -21,9 +26,14 @@ type cardFastChip struct {
configRegister uint8
}
func buildFastChipRom() []uint8 {
data := make([]uint8, 256)
return data
func newCardFastChipBuilder() *cardBuilder {
return &cardBuilder{
name: "FASTChip IIe Card - limited",
description: "Accelerator card for Apple IIe (limited support)",
buildFunc: func(params map[string]string) (Card, error) {
return &CardFastChip{}, nil
},
}
}
const (
@ -32,10 +42,10 @@ const (
fastChipNormalSpeed = uint8(9)
)
func (c *cardFastChip) assign(a *Apple2, slot int) {
func (c *CardFastChip) assign(a *Apple2, slot int) {
// The softswitches are outside the card reserved ss
// Only writes are implemented to avoid conflicts with the joysticks
a.io.addSoftSwitchW(0x6a, func(_ *ioC0Page, value uint8) {
a.io.addSoftSwitchW(0x6a, func(value uint8) {
if value == fastChipUnlockToken {
c.unlockCounter++
if c.unlockCounter >= fastChipUnlockRepeats {
@ -48,25 +58,25 @@ func (c *cardFastChip) assign(a *Apple2, slot int) {
}
}, "FASTCHIP-LOCK")
a.io.addSoftSwitchW(0x6b, func(_ *ioC0Page, _ uint8) {
a.io.addSoftSwitchW(0x6b, func(uint8) {
if c.unlocked {
c.enabled = true
}
}, "FASTCHIP-ENABLE")
a.io.addSoftSwitchW(0x6d, func(_ *ioC0Page, value uint8) {
a.io.addSoftSwitchW(0x6d, func(value uint8) {
if c.enabled {
c.setSpeed(a, value)
}
}, "FASTCHIP-SPEED")
a.io.addSoftSwitchW(0x6e, func(_ *ioC0Page, value uint8) {
a.io.addSoftSwitchW(0x6e, func(value uint8) {
if c.enabled {
c.configRegister = value
}
}, "FASTCHIP-CONFIG")
a.io.addSoftSwitchW(0x6f, func(_ *ioC0Page, value uint8) {
a.io.addSoftSwitchW(0x6f, func(value uint8) {
if c.enabled && c.configRegister == 0 {
c.setSpeed(a, value)
}
@ -75,16 +85,16 @@ func (c *cardFastChip) assign(a *Apple2, slot int) {
c.cardBase.assign(a, slot)
}
func (c *cardFastChip) setSpeed(a *Apple2, value uint8) {
func (c *CardFastChip) setSpeed(a *Apple2, value uint8) {
newAccelerated := (value > fastChipNormalSpeed)
if newAccelerated == c.accelerated {
// No change requested
return
}
if newAccelerated {
a.requestFastMode()
a.RequestFastMode()
} else {
a.releaseFastMode()
a.ReleaseFastMode()
}
c.accelerated = newAccelerated
}

View File

@ -1,251 +0,0 @@
package izapple2
import "fmt"
/*
To implement a hard drive we just have to support boot from #PR7 and the PRODOS expextations.
See:
Beneath Prodos, section 6-6, 7-13 and 5-8. (http://www.apple-iigs.info/doc/fichiers/beneathprodos.pdf)
Apple IIc Technical Reference, 2nd Edition. Chapter 8. https://ia800207.us.archive.org/19/items/AppleIIcTechnicalReference2ndEd/Apple%20IIc%20Technical%20Reference%202nd%20ed.pdf
https://prodos8.com/docs/technote/21/
https://prodos8.com/docs/technote/20/
*/
type cardHardDisk struct {
cardBase
disk *blockDisk
mliParams uint16
trace bool
}
func buildHardDiskRom(slot int) []uint8 {
data := make([]uint8, 256)
ssBase := 0x80 + uint8(slot<<4)
copy(data, []uint8{
// Preamble bytes to comply with the expectation in $Cn01, 3, 5 and 7
0xa9, 0x20, // LDA #$20
0xa9, 0x00, // LDA #$00
0xa9, 0x03, // LDA #$03
0xa9, 0x00, // LDA #$00
// 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
})
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
}
// Entrypoints and Smartport body
copy(data[0x40:], []uint8{
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
})
// Prodos entrypoint body
copy(data[0x80:], []uint8{
0xad, ssBase + 0, 0xc0, // LDA $C0n0 ; Softswitch 0, execute command. Error code in reg A.
0x48, // PHA
0xae, ssBase + 1, 0xc0, // LDX $C0n1 ; Softswitch 1, LO(Blocks), STATUS needs that in reg X.
0xac, ssBase + 2, 0xc0, // LDY $C0n2 ; Softswitch 2, HI(Blocks). STATUS needs that in reg Y.
0x18, // CLC ; Clear carry for no errors.
0x68, // PLA ; Sets Z if no error
0xF0, 0x01, // BEQ $01 ; Skips the SEC if reg A is zero
0x38, // SEC ; Set carry on errors
0x60, // RTS
})
data[0xfc] = 0
data[0xfd] = 0
data[0xfe] = 3 // Status and Read. No write, no format. Single volume
data[0xff] = 0x40 // Driver entry point
return data
}
const (
proDosDeviceCommandStatus = 0
proDosDeviceCommandRead = 1
proDosDeviceCommandWrite = 2
proDosDeviceCommandFormat = 3
)
const (
proDosDeviceNoError = uint8(0)
proDosDeviceErrorIO = uint8(0x27)
proDosDeviceErrorNoDevice = uint8(0x28)
proDosDeviceErrorWriteProtected = uint8(0x2b)
)
func (c *cardHardDisk) assign(a *Apple2, slot int) {
c.addCardSoftSwitchR(0, func(*ioC0Page) uint8 {
// Prodos entry point
command := a.mmu.Peek(0x42)
unit := a.mmu.Peek(0x43)
address := uint16(a.mmu.Peek(0x44)) + uint16(a.mmu.Peek(0x45))<<8
block := uint16(a.mmu.Peek(0x46)) + uint16(a.mmu.Peek(0x47))<<8
if c.trace {
fmt.Printf("[CardHardDisk] Prodos command %v on slot %v, unit $%x, block %v to $%x.\n", command, slot, unit, block, address)
}
switch command {
case proDosDeviceCommandStatus:
return c.status(unit, address)
case proDosDeviceCommandRead:
return c.readBlock(block, address)
case proDosDeviceCommandWrite:
return c.writeBlock(block, address)
default:
// Prodos device command not supported
return proDosDeviceErrorIO
}
}, "HDCOMMAND")
c.addCardSoftSwitchR(1, func(*ioC0Page) uint8 {
// Blocks available, low byte
return uint8(c.disk.blocks)
}, "HDBLOCKSLO")
c.addCardSoftSwitchR(2, func(*ioC0Page) uint8 {
// Blocks available, high byte
return uint8(c.disk.blocks >> 8)
}, "HDBLOCKHI")
c.addCardSoftSwitchR(3, func(*ioC0Page) 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
unit := a.mmu.Peek(paramsAddress + 1)
address := uint16(a.mmu.Peek(paramsAddress+2)) + uint16(a.mmu.Peek(paramsAddress+3))<<8
block := uint16(a.mmu.Peek(paramsAddress+4)) + uint16(a.mmu.Peek(paramsAddress+5))<<8
if c.trace {
fmt.Printf("[CardHardDisk] Smart port command %v on slot %v, unit $%x, block %v to $%x.\n", command, slot, unit, block, address)
}
switch command {
case proDosDeviceCommandStatus:
return c.status(unit, address)
case proDosDeviceCommandRead:
return c.readBlock(block, address)
case proDosDeviceCommandWrite:
return c.writeBlock(block, address)
default:
// Smartport device command not supported
return proDosDeviceErrorIO
}
}, "HDSMARTPORT")
c.addCardSoftSwitchW(4, func(_ *ioC0Page, value uint8) {
c.mliParams = (c.mliParams & 0xff00) + uint16(value)
if c.trace {
fmt.Printf("[CardHardDisk] Smart port LO: 0x%x.\n", c.mliParams)
}
}, "HDSMARTPORTLO")
c.addCardSoftSwitchW(5, func(_ *ioC0Page, value uint8) {
c.mliParams = (c.mliParams & 0x00ff) + (uint16(value) << 8)
if c.trace {
fmt.Printf("[CardHardDisk] Smart port HI: 0x%x.\n", c.mliParams)
}
}, "HDSMARTPORTHI")
c.cardBase.assign(a, slot)
}
func (c *cardHardDisk) readBlock(block uint16, dest uint16) uint8 {
if c.trace {
fmt.Printf("[CardHardDisk] Read block %v into $%x.\n", block, dest)
}
data, err := c.disk.read(uint32(block))
if err != nil {
return proDosDeviceErrorIO
}
// Byte by byte transfer to memory using the full Poke code path
for i := uint16(0); i < uint16(proDosBlockSize); i++ {
c.a.mmu.Poke(dest+i, data[i])
}
return proDosDeviceNoError
}
func (c *cardHardDisk) writeBlock(block uint16, source uint16) uint8 {
if c.trace {
fmt.Printf("[CardHardDisk] Write block %v from $%x.\n", block, source)
}
if c.disk.readOnly {
return proDosDeviceErrorWriteProtected
}
// Byte by byte transfer from memory using the full Peek code path
buf := make([]uint8, proDosBlockSize)
for i := uint16(0); i < uint16(proDosBlockSize); i++ {
buf[i] = c.a.mmu.Peek(source + i)
}
err := c.disk.write(uint32(block), buf)
if err != nil {
return proDosDeviceErrorIO
}
return proDosDeviceNoError
}
func (c *cardHardDisk) status(unit uint8, dest uint16) uint8 {
if c.trace {
fmt.Printf("[CardHardDisk] Status for %v into $%x.\n", unit, dest)
}
// See http://www.1000bit.it/support/manuali/apple/technotes/smpt/tn.smpt.2.html
c.a.mmu.Poke(dest+0, 0x02) // One device
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) // Versión 1.0 final
c.a.mmu.Poke(dest+6, 0x00)
c.a.mmu.Poke(dest+7, 0x00) // Reserved
return proDosDeviceNoError
}
func (c *cardHardDisk) addDisk(disk *blockDisk) {
c.disk = disk
}
func (c *cardHardDisk) setTrace(trace bool) {
c.trace = trace
}

View File

@ -1,7 +1,9 @@
package izapple2
import (
"bufio"
"fmt"
"os"
)
/*
@ -11,137 +13,163 @@ See:
"Apple II Monitors peeled."
http://mysite.du.edu/~etuttle/math/acia.htm
PR#n stores Cn00 in CSWL and CSWH
IN#n stores Cn00 in KSWL and KSWH
*/
type cardInOut struct {
// CardInOut is an experimental card to bridge with the host console
type CardInOut struct {
cardBase
i int
reader *bufio.Reader
}
func (c *cardInOut) assign(a *Apple2, slot int) {
for i := uint8(0x0); i <= 0xf; i++ {
iCopy := i
c.addCardSoftSwitchR(i, func(*ioC0Page) uint8 {
value := []uint8{0xc1, 0xc1, 0x93, 0x0}[c.i%4]
c.i++
fmt.Printf("[cardInOut] Read access to softswith 0x%x for slot %v, value %x.\n", iCopy, slot, value)
//return 0x41 + 0x80
return []uint8{0x41, 0x41, 0x13}[i%3] + 0x80
}, "INOUTR")
c.addCardSoftSwitchW(i, func(_ *ioC0Page, value uint8) {
fmt.Printf("[cardInOut] Write access to softswith 0x%x for slot %v, value 0x%x.\n", iCopy, slot, value)
}, "INOUTW")
func newCardInOutBuilder() *cardBuilder {
return &cardBuilder{
name: "InOut test card",
description: "Card to test I/O",
buildFunc: func(params map[string]string) (Card, error) {
return &CardInOut{}, nil
},
}
}
in := true
out := false
func (c *CardInOut) assign(a *Apple2, slot int) {
c.addCardSoftSwitchR(0, func() uint8 {
if c.reader == nil {
c.reader = bufio.NewReader(os.Stdin)
}
value, err := c.reader.ReadByte()
if err != nil {
panic(err)
}
value += 0x80
if value&0x7f == 10 {
value = 13 + 0x80
}
//fmt.Printf("[cardInOut] Read access to softswith 0x%x for slot %v, value %x.\n", 0, slot, value)
return value
}, "INOUTR")
c.addCardSoftSwitchW(1, func(value uint8) {
//fmt.Printf("[cardInOut] Write access to softswith 0x%x for slot %v, value 0x%x: %v, %v.\n", 1, slot, value, value&0x7f, string(value&0x7f))
if value&0x7f == 13 {
fmt.Printf("\n")
} else {
fmt.Printf("%v", string(value&0x7f))
}
}, "INOUTW")
data := buildBaseInOutRom(slot)
c.romCsxx = newMemoryRangeROM(0xC200, data[:], "InOUt card")
c.cardBase.assign(a, slot)
}
func buildBaseInOutRom(slot int) []uint8 {
data := [256]uint8{
// Register
0xA9, 0xC2,
0x85, 0x37,
0x85, 0x39,
0xA9, 0x10,
0x85, 0x36,
0xA9, 0x15,
0x85, 0x38,
0x60, 0xEA,
0x4c, 0x40, 0xc2, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
// Out char
0x8D, 0xA1, 0xC0,
0x60, 0xEA,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// Get char
0x91, 0x28,
0xAD, 0xA0, 0xC0,
0x60,
0x48, 0xA5, 0x38, 0xD0, 0x11, 0xA9, 0xC2, 0xC5,
0x39, 0xD0, 0x0B, 0xAD, 0x4F, 0x85, 0x38, 0x68,
0x91, 0x28, 0xAD, 0xA0, 0xC0, 0x60, 0x68, 0x8D,
0xA1, 0xC0, 0x60,
}
if !out {
// NOP the CSWL,H change
for _, v := range []uint8{2, 3, 8, 9} {
data[v] = 0xEA
}
}
// Fix slot dependant addresses
data[0x02] = uint8(0xc0 + slot)
data[0x46] = uint8(0xc0 + slot)
data[0x53] = uint8(0x80 + slot<<4)
data[0x58] = uint8(0x81 + slot<<4)
if !in {
// NOP the KSWL,H change
for _, v := range []uint8{4, 5, 12, 13} {
data[v] = 0xEA
}
}
c.romCsxx = newMemoryRangeROM(0xC200, data[0:255], "InOUt card")
if slot != 2 {
// To make it work on other slots, patch C2, A0 and A1
panic("Assert failed. Only slot 2 supported for the InOut card")
}
c.cardBase.assign(a, slot)
return data[:]
}
/*
The ROM code was assembled using https://www.masswerk.at/6502/assembler.html
We will have $Cn00 as the entry point for CSWL/H. But before doing
anything we have to check that we are not in $Cn00 because of an IN#.
To da that we check id KSWL/H is $Cn00, it is it we wif it to INEntry.
src:
BASL = $28
CSWL = $36
CSWH = $37
KSWL = $38
KSWH = $39
* = $C200
Register:
Entry:
JMP SkipHeader
* = $C240
SkipHeader:
PHA
LDA *KSWL
BNE PREntry
LDA #$C2
STA *CSWH
STA *KSWH
LDA #$10
STA *CSWL
LDA #$15
CMP *KSWH
BNE PREntry
FixKSWL:
LDA #<INEntry
STA *KSWL
RTS
NOP
OutChar:
STA $C0A1
RTS
NOP
GetChar:
INEntry:
PLA
STA (BASL),Y
LDA $C0A0
RTS
PREntry:
PLA
STA $C0A1
RTS
assembled as:
Listing:
pass 2
0000 BASL = 0028
0000 CSWL = 0036
0000 CSWH = 0037
0000 KSWL = 0038
0000 KSWH = 0039
* = $C200
C200 REGIST
C200 LDA #$C2 A9 C2
C202 STA *CSWH 85 37
C204 STA *KSWH 85 39
C206 LDA #$10 A9 10
C208 STA *CSWL 85 36
C20A LDA #$15 A9 15
C20C STA *KSWL 85 38
C20E RTS 60
C20F NOP EA
C210 OUTCHA
C210 STA $C0A1 8D A1 C0
C213 RTS 60
C214 NOP EA
C215 GETCHA
C215 STA (BASL),Y 91 28
C217 LDA $C0A0 AD A0 C0
C21A RTS
* = $C200
C200 ENTRY:
C200 JMP SKIPHE 4C 40 C2
object code:
A9 C2 85 37 85 39 A9 10
85 36 A9 15 85 38 60 EA
8D A1 C0 60 EA 91 28 AD
A0 C0 60
* = $C240
C240 SKIPHE
C240 PHA 48
C241 LDA *KSWL A5 38
C243 BNE PRENTR D0 11
C245 LDA #$C2 A9 C2
C247 CMP *KSWH C5 39
C249 BNE PRENTR D0 0B
C24B LDA #<INENTR A9 4F
C24D STA *KSWL 85 38
C24F INENTR
C24F PLA 68
C250 STA (BASL),Y 91 28
C252 LDA $C0A0 AD A0 C0
C255 RTS 60
C256 PRENTR
C256 PLA 68
C257 STA $C0A1 8D A1 C0
C25A RTS 60
done.
Object Code:
c200:
4C 40 C2
c240:
48 A5 38 D0 11
A9 C2 C5 39 D0 0B A9 4F
85 38 68 91 28 AD A0 C0
60 68 8D A1 C0 60
*/

View File

@ -11,7 +11,7 @@ Note also that language cards for the Apple ][ had ROM on
board to replace the main board F8 ROM with Autostart. That
was not used/needed on the Apple ][+. As this emulates the
Apple ][+, it is not considered. For the Plus it is often
refered as Language card but it is really a 16 KB Ram card,
referred as Language card but it is really a 16 KB Ram card,
"When RAM is deselected, the ROM on the Language card is selected for
@ -21,19 +21,30 @@ for $D000-$F7FF.
Power on RESET initializes ROM to read mode and RAM to write mode,
and selects the second 4K bank to map $D000-$DFFF."
Writing to the softswtich disables writing in LC? Saw that
Writing to the softswitch disables writing in LC? Saw that
somewhere but doing so fails IIe self check.
*/
type cardLanguage struct {
// CardLanguage is an Language Card
type CardLanguage struct {
cardBase
readState bool
writeState uint8
altBank bool // false is bank1, true is bank2
}
func newCardLanguageBuilder() *cardBuilder {
return &cardBuilder{
name: "16 KB Language Card",
description: "Language card with 16 extra KB for the Apple ][ and ][+",
buildFunc: func(params map[string]string) (Card, error) {
return &CardLanguage{}, nil
},
}
}
const (
// Write enabling requires two softswitch accesses
lcWriteDisabled = 0
@ -41,7 +52,18 @@ const (
lcWriteEnabled = 2
)
func (c *cardLanguage) assign(a *Apple2, slot int) {
func (c *CardLanguage) reset() {
if c.a.isApple2e {
// UtA2e 1-3, 5-23
c.readState = false
c.writeState = lcWriteEnabled
c.altBank = true // Start on bank2
c.applyState()
}
}
func (c *CardLanguage) assign(a *Apple2, slot int) {
c.readState = false
c.writeState = lcWriteEnabled
c.altBank = true // Start on bank2
@ -49,11 +71,11 @@ func (c *cardLanguage) assign(a *Apple2, slot int) {
a.mmu.initLanguageRAM(1)
for i := uint8(0x0); i <= 0xf; i++ {
iCopy := i
c.addCardSoftSwitchR(iCopy, func(*ioC0Page) uint8 {
c.addCardSoftSwitchR(iCopy, func() uint8 {
c.ssAction(iCopy, false)
return 0
}, "LANGCARDR")
c.addCardSoftSwitchW(iCopy, func(*ioC0Page, uint8) {
c.addCardSoftSwitchW(iCopy, func(uint8) {
c.ssAction(iCopy, true)
}, "LANGCARDW")
}
@ -62,7 +84,7 @@ func (c *cardLanguage) assign(a *Apple2, slot int) {
c.applyState()
}
func (c *cardLanguage) ssAction(ss uint8, write bool) {
func (c *CardLanguage) ssAction(ss uint8, write bool) {
c.altBank = ((ss >> 3) & 1) == 0
action := ss & 0x3
switch action {
@ -101,6 +123,6 @@ func (c *cardLanguage) ssAction(ss uint8, write bool) {
c.applyState()
}
func (c *cardLanguage) applyState() {
func (c *CardLanguage) applyState() {
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
}

View File

@ -8,21 +8,30 @@ import (
Logger card. It never existed, I use it to trace accesses to the card.
*/
type cardLogger struct {
// CardLogger is a fake card to log soft switch invocations
type CardLogger struct {
cardBase
}
func (c *cardLogger) assign(a *Apple2, slot int) {
for i := uint8(0x0); i <= 0xf; i++ {
iCopy := i
c.addCardSoftSwitchR(i, func(*ioC0Page) uint8 {
fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", iCopy, slot)
return 0
}, "LOGGERR")
c.addCardSoftSwitchW(i, func(_ *ioC0Page, value uint8) {
fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%v.\n", iCopy, slot, value)
}, "LOGGERW")
func newCardLoggerBuilder() *cardBuilder {
return &cardBuilder{
name: "Softswitch logger card",
description: "Card to log softswitch accesses",
buildFunc: func(params map[string]string) (Card, error) {
return &CardLogger{}, nil
},
}
}
func (c *CardLogger) assign(a *Apple2, slot int) {
c.addCardSoftSwitches(func(address uint8, data uint8, write bool) uint8 {
if write {
fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%02x.\n", address, slot, data)
} else {
fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", address, slot)
}
return 0
}, "LOGGER")
c.cardBase.assign(a, slot)
}

View File

@ -1,10 +1,12 @@
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
@ -12,65 +14,107 @@ See:
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 (
memoryExpansionSize256 = 256 * 1024
memoryExpansionSize512 = 512 * 1024
memoryExpansionSize768 = 768 * 1024
memoryExpansionSize1024 = 1024 * 1024
memoryExpansionMask = 0x000fffff // 10 bits, 1MB
memoryExpansionMask = 0x000fffff // 10 bits, 1MB
)
type cardMemoryExpansion struct {
ram [memoryExpansionSize1024]uint8
index int
// CardMemoryExpansion is a Memory Expansion card
type CardMemoryExpansion struct {
cardBase
ram []uint8
index int
}
func (c *cardMemoryExpansion) assign(a *Apple2, slot 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(*ioC0Page) uint8 {
c.addCardSoftSwitchR(0, func() uint8 {
return uint8(c.index)
}, "MEMORYEXLOR")
c.addCardSoftSwitchR(1, func(*ioC0Page) uint8 {
c.addCardSoftSwitchR(1, func() uint8 {
return uint8(c.index >> 8)
}, "MEMORYEXMIR")
c.addCardSoftSwitchR(2, func(*ioC0Page) uint8 {
c.addCardSoftSwitchR(2, func() uint8 {
// Top nibble returned is 0xf
return uint8(c.index>>16) | 0xf0
}, "MEMORYEXHIR")
// Set pointer position
c.addCardSoftSwitchW(0, func(_ *ioC0Page, value uint8) {
c.addCardSoftSwitchW(0, func(value uint8) {
c.index = (c.index &^ 0xff) + int(value)
}, "MEMORYEXLOW")
c.addCardSoftSwitchW(1, func(_ *ioC0Page, value uint8) {
c.addCardSoftSwitchW(1, func(value uint8) {
c.index = (c.index &^ 0xff00) + int(value)<<8
}, "MEMORYEXMIW")
c.addCardSoftSwitchW(2, func(_ *ioC0Page, value uint8) {
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(*ioC0Page) uint8 {
c.addCardSoftSwitchR(3, func() uint8 {
var value uint8
if c.index < len(c.ram) {
value = c.ram[c.index]
@ -82,7 +126,7 @@ func (c *cardMemoryExpansion) assign(a *Apple2, slot int) {
}, "MEMORYEXR")
// Write data
c.addCardSoftSwitchW(3, func(_ *ioC0Page, value uint8) {
c.addCardSoftSwitchW(3, func(value uint8) {
if c.index < len(c.ram) {
c.ram[c.index] = value
}
@ -91,7 +135,7 @@ func (c *cardMemoryExpansion) assign(a *Apple2, slot int) {
// The rest of the softswitches return 255, at least on //e and //c
for i := uint8(4); i < 16; i++ {
c.addCardSoftSwitchR(i, func(*ioC0Page) uint8 {
c.addCardSoftSwitchR(i, func() uint8 {
return 255
}, "MEMORYEXUNUSEDR")
}

271
cardMouse.go Normal file
View 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
View 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
View 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
View 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
View 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)
}

View File

@ -38,35 +38,37 @@ func setupRGBCard(a *Apple2) *cardRGB {
a.io.softSwitchesData[ioFlagRGBCardActive] = ssOn
// Does not have ROM or private softswitches. It spies on the softswitches
a.io.addSoftSwitchRW(0x50, func(io *ioC0Page) uint8 {
io.softSwitchesData[ioFlagText] = ssOff
a.io.addSoftSwitchRW(0x50, func() uint8 {
a.io.softSwitchesData[ioFlagText] = ssOff
// Reset RGB modes when entering graphics mode
c.step = 0
io.softSwitchesData[ioFlag1RGBCard] = ssOn
io.softSwitchesData[ioFlag2RGBCard] = ssOn
a.io.softSwitchesData[ioFlag1RGBCard] = ssOn
a.io.softSwitchesData[ioFlag2RGBCard] = ssOn
return 0
}, "TEXTOFF")
}, "TEXTOFF-RGB")
a.io.addSoftSwitchRW(0x5e, func(io *ioC0Page) uint8 {
io.softSwitchesData[ioFlagAnnunciator3] = ssOff
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(io *ioC0Page) uint8 {
io.softSwitchesData[ioFlagAnnunciator3] = ssOn
a.io.addSoftSwitchRW(0x5f, func() uint8 {
a.io.softSwitchesData[ioFlagAnnunciator3] = ssOn
switch c.step {
case 1:
io.softSwitchesData[ioFlag1RGBCard] = io.softSwitchesData[ioFlag80Col]
a.io.softSwitchesData[ioFlag1RGBCard] = a.io.softSwitchesData[ioFlag80Col]
c.step++
case 3:
io.softSwitchesData[ioFlag2RGBCard] = io.softSwitchesData[ioFlag80Col]
a.io.softSwitchesData[ioFlag2RGBCard] = a.io.softSwitchesData[ioFlag80Col]
c.step++
}

View File

@ -1,5 +1,10 @@
package izapple2
import (
"fmt"
"strconv"
)
/*
RAMWorks style card on the Apple IIe aus slot.
https://patents.google.com/patent/US4601018
@ -11,14 +16,22 @@ Diagnostics disks:
It's is like the extra 64kb on an Apple IIe 80col 64kb card, but with up to 256 banks
*/
func setupRAMWorksCard(a *Apple2, banks int) {
a.mmu.initExtendedRAM(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)
}
ssr := func(_ *ioC0Page) uint8 {
a.mmu.initExtendedRAM(size / 64)
ssr := func() uint8 {
return a.mmu.extendedRAMBlock
}
ssw := func(_ *ioC0Page, value uint8) {
ssw := func(value uint8) {
a.mmu.setExtendedRAMActiveBlock(value)
}
@ -31,4 +44,6 @@ func setupRAMWorksCard(a *Apple2, banks int) {
a.io.addSoftSwitchW(0x73, ssw, "RAMWORKSW")
a.io.addSoftSwitchW(0x75, ssw, "RAMWORKSW")
a.io.addSoftSwitchW(0x77, ssw, "RAMWORKSW")
return nil
}

View File

@ -7,7 +7,8 @@ See:
http://www.applelogic.org/files/SATURN128MAN.pdf
*/
type cardSaturn struct {
// CardSaturn is a Saturn128 card
type CardSaturn struct {
cardBase
readState bool
writeState uint8
@ -15,24 +16,35 @@ type cardSaturn struct {
activeBlock uint8
}
func newCardSaturnBuilder() *cardBuilder {
return &cardBuilder{
name: "Saturn 128KB Ram Card",
description: "RAM card with 128Kb, it's like 8 language cards",
buildFunc: func(params map[string]string) (Card, error) {
return &CardSaturn{}, nil
},
}
}
const (
saturnBlocks = 8
)
func (c *cardSaturn) assign(a *Apple2, slot int) {
func (c *CardSaturn) assign(a *Apple2, slot int) {
c.readState = false
c.writeState = lcWriteEnabled
c.altBank = true
c.activeBlock = 0
a.mmu.initLanguageRAM(saturnBlocks)
// TODO: use addCardSoftSwitches()
for i := uint8(0x0); i <= 0xf; i++ {
iCopy := i
c.addCardSoftSwitchR(iCopy, func(*ioC0Page) uint8 {
c.addCardSoftSwitchR(iCopy, func() uint8 {
c.ssAction(iCopy)
return 0
}, "SATURNR")
c.addCardSoftSwitchW(iCopy, func(*ioC0Page, uint8) {
c.addCardSoftSwitchW(iCopy, func(uint8) {
c.ssAction(iCopy)
}, "SATURNW")
}
@ -40,7 +52,7 @@ func (c *cardSaturn) assign(a *Apple2, slot int) {
c.applyState()
}
func (c *cardSaturn) ssAction(ss uint8) {
func (c *CardSaturn) ssAction(ss uint8) {
switch ss {
case 0:
// RAM read, no writes
@ -107,7 +119,7 @@ func (c *cardSaturn) ssAction(ss uint8) {
c.applyState()
}
func (c *cardSaturn) applyState() {
func (c *CardSaturn) applyState() {
c.a.mmu.setLanguageRAMActiveBlock(c.activeBlock)
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
}

301
cardSmartport.go Normal file
View 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
View 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
View 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)
}
}

View File

@ -1,5 +1,7 @@
package izapple2
import "github.com/ivanizag/izapple2/component"
/*
ThunderClock`, real time clock card.
@ -19,14 +21,30 @@ uPD1990AC hookup:
bit 7 = data out
*/
type cardThunderClockPlus struct {
microPD1990ac
// CardThunderClockPlus represents a ThunderClock+ card
type CardThunderClockPlus struct {
cardBase
upd1990 component.MicroPD1990ac
}
func (c *cardThunderClockPlus) assign(a *Apple2, slot int) {
c.addCardSoftSwitchR(0, func(*ioC0Page) uint8 {
bit := c.microPD1990ac.out()
func newCardThunderClockPlusBuilder() *cardBuilder {
return &cardBuilder{
name: "ThunderClock+ Card",
description: "Clock card",
buildFunc: func(params map[string]string) (Card, error) {
var c CardThunderClockPlus
err := c.loadRomFromResource("<internal>/ThunderclockPlusROM.bin", cardRomUpper)
if err != nil {
return nil, err
}
return &c, nil
},
}
}
func (c *CardThunderClockPlus) assign(a *Apple2, slot int) {
c.addCardSoftSwitchR(0, func() uint8 {
bit := c.upd1990.Out()
// Get the next data bit from uPD1990AC on the MSB
if bit {
return 0x80
@ -34,7 +52,7 @@ func (c *cardThunderClockPlus) assign(a *Apple2, slot int) {
return 0
}, "THUNDERCLOCKR")
c.addCardSoftSwitchW(0, func(_ *ioC0Page, value uint8) {
c.addCardSoftSwitchW(0, func(value uint8) {
dataIn := (value & 0x01) == 1
clock := ((value >> 1) & 0x01) == 1
strobe := ((value >> 2) & 0x01) == 1
@ -42,7 +60,7 @@ func (c *cardThunderClockPlus) assign(a *Apple2, slot int) {
/* fmt.Printf("[cardThunderClock] dataIn %v, clock %v, strobe %v, command %v.\n",
dataIn, clock, strobe, command) */
c.microPD1990ac.in(clock, strobe, command, dataIn)
c.upd1990.In(clock, strobe, command, dataIn)
}, "THUNDERCLOCKW")
c.cardBase.assign(a, slot)

View File

@ -8,10 +8,23 @@ See:
http://www.applelogic.org/files/GSHARDWAREREF.pdf, page 89
*/
type cardVidHD struct {
// CardVidHD represents a VidHD card
type CardVidHD struct {
cardBase
}
func newCardVidHDBuilder() *cardBuilder {
return &cardBuilder{
name: "VidHD Card - limited",
description: "Firmware signature of the VidHD card to trick Total Replay to use the SHR mode",
buildFunc: func(params map[string]string) (Card, error) {
var c CardVidHD
c.loadRom(buildVidHDRom(), cardRomSimple)
return &c, nil
},
}
}
func buildVidHDRom() []uint8 {
data := make([]uint8, 256)
@ -26,16 +39,16 @@ const (
ioDataNewVideo uint8 = 0x29
)
func (c *cardVidHD) assign(a *Apple2, slot int) {
func (c *CardVidHD) assign(a *Apple2, slot int) {
// The softswitches are outside the card reserved ss
a.io.addSoftSwitchR(0x22, notImplementedSoftSwitchR, "VIDHD-TBCOLOR")
a.io.addSoftSwitchW(0x22, notImplementedSoftSwitchW, "VIDHD-TBCOLOR")
a.io.addSoftSwitchR(0x29, getStatusSoftSwitch(ioDataNewVideo), "VIDHD-NEWVIDEO")
a.io.addSoftSwitchW(0x29, setStatusSoftSwitch(ioDataNewVideo), "VIDHD-NEWVIDEO")
a.io.addSoftSwitchR(0x34, notImplementedSoftSwitchR, "VIDHD-CLOCKCTL")
a.io.addSoftSwitchW(0x34, notImplementedSoftSwitchW, "VIDHD-CLOCKCTL")
a.io.addSoftSwitchR(0x35, notImplementedSoftSwitchR, "VIDHD-SHADOW")
a.io.addSoftSwitchW(0x35, notImplementedSoftSwitchW, "VIDHD-SHADOW")
a.io.addSoftSwitchR(0x22, buildNotImplementedSoftSwitchR(a.io), "VIDHD-TBCOLOR")
a.io.addSoftSwitchW(0x22, buildNotImplementedSoftSwitchW(a.io), "VIDHD-TBCOLOR")
a.io.addSoftSwitchR(0x29, getStatusSoftSwitch(a.io, ioDataNewVideo), "VIDHD-NEWVIDEO")
a.io.addSoftSwitchW(0x29, setStatusSoftSwitch(a.io, ioDataNewVideo), "VIDHD-NEWVIDEO")
a.io.addSoftSwitchR(0x34, buildNotImplementedSoftSwitchR(a.io), "VIDHD-CLOCKCTL")
a.io.addSoftSwitchW(0x34, buildNotImplementedSoftSwitchW(a.io), "VIDHD-CLOCKCTL")
a.io.addSoftSwitchR(0x35, buildNotImplementedSoftSwitchR(a.io), "VIDHD-SHADOW")
a.io.addSoftSwitchW(0x35, buildNotImplementedSoftSwitchW(a.io), "VIDHD-SHADOW")
c.cardBase.assign(a, slot)
}

190
cardVidex.go Normal file
View 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
}

View File

@ -15,6 +15,7 @@ type CharacterGenerator struct {
data []uint8
columnMap charColumnMap
page int
pageSize int
}
type charColumnMap func(column int) int
@ -28,28 +29,31 @@ func charGenColumnsMap2e(column int) int {
}
const (
charGenPageSize = 2048
charGenPageSize2Plus = 2048
charGenPageSize2E = 2048 * 2
)
// NewCharacterGenerator instantiates a new Character Generator with the rom on the file given
func newCharacterGenerator(filename string, order charColumnMap) (*CharacterGenerator, error) {
func newCharacterGenerator(filename string, order charColumnMap, pageSize int) (*CharacterGenerator, error) {
var cg CharacterGenerator
cg.columnMap = order
cg.pageSize = pageSize
err := cg.load(filename)
if err != nil {
return nil, err
}
cg.columnMap = order
return &cg, nil
}
func (cg *CharacterGenerator) load(filename string) error {
bytes, err := loadResource(filename)
bytes, _, err := LoadResource(filename)
if err != nil {
return err
}
size := len(bytes)
if size < charGenPageSize {
return errors.New("Character ROM size not supported")
if size < cg.pageSize {
return errors.New("character ROM size not supported")
}
cg.data = bytes
return nil
@ -57,63 +61,47 @@ func (cg *CharacterGenerator) load(filename string) error {
func (cg *CharacterGenerator) setPage(page int) {
// Some clones had a switch to change codepage with extra characters
pages := len(cg.data) / charGenPageSize
pages := len(cg.data) / cg.pageSize
cg.page = page % pages
}
func (cg *CharacterGenerator) getPage() int {
return cg.page
}
func (cg *CharacterGenerator) nextPage() {
cg.setPage(cg.page + 1)
}
func (cg *CharacterGenerator) getPixel(char uint8, row int, column int) bool {
bits := cg.data[int(char)*8+row+cg.page*charGenPageSize]
bits := cg.data[int(char)*8+row+cg.page*cg.pageSize]
bit := cg.columnMap(column)
value := bits >> uint(bit) & 1
return value == 1
}
func (cg *CharacterGenerator) dumpCharRaw(char int) {
base := int(char) * 8
fmt.Printf("Char: %v\n---------\n", char)
for i := 0; i < 8; i++ {
fmt.Print("|")
b := cg.data[base+i]
for j := 0; j < 8; j++ {
if (b>>uint(j))&1 == 1 {
fmt.Print("#")
} else {
fmt.Print(" ")
}
}
fmt.Println("|")
func setupCharactedGenerator(a *Apple2, board string, charRomFile string) error {
var charGenMap charColumnMap
initialCharGenPage := 0
pageSize := charGenPageSize2Plus
switch board {
case "2plus":
charGenMap = charGenColumnsMap2Plus
case "2e":
charGenMap = charGenColumnsMap2e
pageSize = charGenPageSize2E
case "base64a":
charGenMap = charGenColumnsMapBase64a
initialCharGenPage = 1
default:
return fmt.Errorf("board %s not supported it must be '2plus', '2e' or 'base64a'", board)
}
fmt.Println("---------")
}
func (cg *CharacterGenerator) dumpChar(char uint8) {
fmt.Printf("Char: %v\n---------\n", char)
for row := 0; row < 8; row++ {
fmt.Print("|")
for col := 0; col < 7; col++ {
if cg.getPixel(char, row, col) {
fmt.Print("#")
} else {
fmt.Print(" ")
}
}
fmt.Println("|")
}
fmt.Println("---------")
}
// Dump to sdtout all the character maps
func (cg *CharacterGenerator) Dump() {
pages := len(cg.data) / charGenPageSize
for p := 0; p < pages; p++ {
cg.setPage(p)
for i := 0; i < 256; i++ {
cg.dumpChar(uint8(i))
//cg.dumpCharRaw(int(i))
}
cg, err := newCharacterGenerator(charRomFile, charGenMap, pageSize)
if err != nil {
return err
}
cg.setPage(initialCharGenPage)
a.cg = cg
return nil
}

106
command.go Normal file
View 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
View 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++
}
}

View File

@ -1,4 +1,4 @@
package izapple2
package component
import (
"time"
@ -21,8 +21,7 @@ import (
byte 0: seconds, BCD 0 to 59
*/
type microPD1990ac struct {
type MicroPD1990ac struct {
clock bool // CLK state
strobe bool // STB state
command uint8 // C0, C1, C2 command. From 0 to 7
@ -36,7 +35,7 @@ const (
mpd1990commandTimeRead = 3
)
func (m *microPD1990ac) in(clock bool, strobe bool, command uint8, dataIn bool) {
func (m *MicroPD1990ac) In(clock bool, strobe bool, command uint8, dataIn bool) {
// Detect signal raise
clockRaise := clock && !m.clock
strobeRaise := strobe && !m.strobe
@ -69,20 +68,22 @@ func (m *microPD1990ac) in(clock bool, strobe bool, command uint8, dataIn bool)
}
}
func (m *microPD1990ac) out() bool {
if m.command == mpd1990commandRegHold {
//panic("Output on RegHold should be a 1Hz signal. Not implemented.")
}
func (m *MicroPD1990ac) Out() bool {
/*
if m.command == mpd1990commandRegHold {
panic("Output on RegHold should be a 1Hz signal. Not implemented.")
}
if m.command == mpd1990commandTimeRead {
//panic("Output on RegHold should be a 512Hz signal with LSB. Not implemented.")
}
if m.command == mpd1990commandTimeRead {
panic("Output on RegHold should be a 512Hz signal with LSB. Not implemented.")
}
*/
// Return the LSB of the register shift
return (m.register & 1) == 1
}
func (m *microPD1990ac) loadTime() {
func (m *MicroPD1990ac) loadTime() {
now := time.Now()
var register uint64

35
component/pins.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
name: swyft
parent: 2enh
s3: swyftcard
s6: diskii,disk1=<internal>/SwyftWare_-_SwyftCard_Tutorial.woz

277
configuration.go Normal file
View 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
View 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.`)
}
})
}

View File

@ -1,152 +0,0 @@
package core6502
import "fmt"
const (
modeImplicit = iota + 1
modeImplicitX
modeImplicitY
modeAccumulator
modeImmediate
modeZeroPage
modeZeroPageX
modeZeroPageY
modeRelative
modeAbsolute
modeAbsoluteX
modeAbsoluteY
modeIndirect
modeIndexedIndirectX
modeIndirectIndexedY
// Added on the 65c02
modeIndirectZeroPage
modeAbsoluteIndexedIndirectX
modeZeroPageAndRelative
)
func getWordInLine(line []uint8) uint16 {
return uint16(line[1]) + 0x100*uint16(line[2])
}
func resolveValue(s *State, line []uint8, opcode opcode) uint8 {
switch opcode.addressMode {
case modeAccumulator:
return s.reg.getA()
case modeImplicitX:
return s.reg.getX()
case modeImplicitY:
return s.reg.getY()
case modeImmediate:
return line[1]
}
// The value is in memory
address := resolveAddress(s, line, opcode)
return s.mem.Peek(address)
}
func resolveSetValue(s *State, line []uint8, opcode opcode, value uint8) {
switch opcode.addressMode {
case modeAccumulator:
s.reg.setA(value)
return
case modeImplicitX:
s.reg.setX(value)
return
case modeImplicitY:
s.reg.setY(value)
return
}
// The value is in memory
address := resolveAddress(s, line, opcode)
s.mem.Poke(address, value)
}
func resolveAddress(s *State, line []uint8, opcode opcode) uint16 {
var address uint16
switch opcode.addressMode {
case modeZeroPage:
address = uint16(line[1])
case modeZeroPageX:
address = uint16(line[1] + s.reg.getX())
case modeZeroPageY:
address = uint16(line[1] + s.reg.getY())
case modeAbsolute:
address = getWordInLine(line)
case modeAbsoluteX:
address = getWordInLine(line) + uint16(s.reg.getX())
case modeAbsoluteY:
address = getWordInLine(line) + uint16(s.reg.getY())
case modeIndexedIndirectX:
addressAddress := uint8(line[1] + s.reg.getX())
address = getZeroPageWord(s.mem, addressAddress)
case modeIndirect:
addressAddress := getWordInLine(line)
address = getWord(s.mem, addressAddress)
case modeIndirectIndexedY:
address = getZeroPageWord(s.mem, line[1]) +
uint16(s.reg.getY())
// 65c02 additions
case modeIndirectZeroPage:
address = getZeroPageWord(s.mem, line[1])
case modeAbsoluteIndexedIndirectX:
addressAddress := getWordInLine(line) + uint16(s.reg.getX())
address = getWord(s.mem, addressAddress)
case modeRelative:
// This assumes that PC is already pointing to the next instruction
address = s.reg.getPC() + uint16(int8(line[1])) // Note: line[1] is signed
case modeZeroPageAndRelative:
// Two addressing modes combined. We refer to the second one, relative,
// placed one byte after the zeropage reference
address = s.reg.getPC() + uint16(int8(line[2])) // Note: line[2] is signed
default:
panic("Assert failed. Missing addressing mode")
}
return address
}
func lineString(line []uint8, opcode opcode) string {
t := opcode.name
switch opcode.addressMode {
case modeImplicit:
case modeImplicitX:
case modeImplicitY:
//Nothing
case modeAccumulator:
t += fmt.Sprintf(" A")
case modeImmediate:
t += fmt.Sprintf(" #$%02x", line[1])
case modeZeroPage:
t += fmt.Sprintf(" $%02x", line[1])
case modeZeroPageX:
t += fmt.Sprintf(" $%02x,X", line[1])
case modeZeroPageY:
t += fmt.Sprintf(" $%02x,Y", line[1])
case modeRelative:
t += fmt.Sprintf(" *%+x", int8(line[1]))
case modeAbsolute:
t += fmt.Sprintf(" $%04x", getWordInLine(line))
case modeAbsoluteX:
t += fmt.Sprintf(" $%04x,X", getWordInLine(line))
case modeAbsoluteY:
t += fmt.Sprintf(" $%04x,Y", getWordInLine(line))
case modeIndirect:
t += fmt.Sprintf(" ($%04x)", getWordInLine(line))
case modeIndexedIndirectX:
t += fmt.Sprintf(" ($%02x,X)", line[1])
case modeIndirectIndexedY:
t += fmt.Sprintf(" ($%02x),Y", line[1])
// 65c02 additions:
case modeIndirectZeroPage:
t += fmt.Sprintf(" ($%02x)", line[1])
case modeAbsoluteIndexedIndirectX:
t += fmt.Sprintf(" ($%04x,X)", getWordInLine(line))
case modeZeroPageAndRelative:
t += fmt.Sprintf(" $%02x %+x", line[1], int8(line[2]))
default:
t += "UNKNOWN MODE"
}
return t
}

View File

@ -1,168 +0,0 @@
package core6502
/*
For the diffrences with NMOS6502 see:
http://6502.org/tutorials/65c02opcodes.html
http://wilsonminesco.com/NMOS-CMOSdif/
http://www.obelisk.me.uk/65C02/reference.html
http://www.obelisk.me.uk/65C02/addressing.html
http://anyplatform.net/media/guides/cpus/65xx%20Processor%20Data.txt
*/
// NewCMOS65c02 returns an initialized 65c02
func NewCMOS65c02(m Memory) *State {
var s State
s.mem = m
var opcodes [256]opcode
for i := 0; i < 256; i++ {
opcodes[i] = opcodesNMOS6502[i]
if opcodes65c02Delta[i].cycles != 0 {
opcodes[i] = opcodes65c02Delta[i]
}
}
add65c02NOPs(&opcodes)
s.opcodes = &opcodes
return &s
}
func add65c02NOPs(opcodes *[256]opcode) {
nop11 := opcode{"NOP", 1, 1, modeImplicit, opNOP}
nop22 := opcode{"NOP", 2, 2, modeImmediate, opNOP}
nop23 := opcode{"NOP", 2, 3, modeImmediate, opNOP}
nop24 := opcode{"NOP", 2, 4, modeImmediate, opNOP}
nop34 := opcode{"NOP", 3, 4, modeAbsolute, opNOP}
nop38 := opcode{"NOP", 3, 8, modeAbsolute, opNOP}
opcodes[0x02] = nop22
opcodes[0x22] = nop22
opcodes[0x42] = nop22
opcodes[0x62] = nop22
opcodes[0x82] = nop22
opcodes[0xc2] = nop22
opcodes[0xe2] = nop22
opcodes[0x44] = nop23
opcodes[0x54] = nop24
opcodes[0xD4] = nop24
opcodes[0xF4] = nop24
opcodes[0x5c] = nop38
opcodes[0xdc] = nop34
opcodes[0xfc] = nop34
for i := 0; i < 0x100; i = i + 0x10 {
opcodes[i+0x03] = nop11
// RMB and SMB; opcodes[i+0x07] = nop11
opcodes[i+0x0b] = nop11
// BBR and BBS: opcodes[i+0x0f] = nop11
}
// Detection of 65c816
opcodes[0xbf].name = "XCE"
}
var opcodes65c02Delta = [256]opcode{
// Functional difference
0x00: opcode{"BRK", 1, 7, modeImplicit, opBRKAlt},
0x24: opcode{"BIT", 2, 3, modeZeroPage, opBIT},
0x2C: opcode{"BIT", 3, 3, modeAbsolute, opBIT},
// Fixed BCD arithmetic flags
0x69: opcode{"ADC", 2, 2, modeImmediate, opADCAlt},
0x65: opcode{"ADC", 2, 3, modeZeroPage, opADCAlt},
0x75: opcode{"ADC", 2, 4, modeZeroPageX, opADCAlt},
0x6D: opcode{"ADC", 3, 4, modeAbsolute, opADCAlt},
0x7D: opcode{"ADC", 3, 4, modeAbsoluteX, opADCAlt},
0x79: opcode{"ADC", 3, 4, modeAbsoluteY, opADCAlt},
0x61: opcode{"ADC", 2, 6, modeIndexedIndirectX, opADCAlt},
0x71: opcode{"ADC", 2, 5, modeIndirectIndexedY, opADCAlt},
0xE9: opcode{"SBC", 2, 2, modeImmediate, opSBCAlt},
0xE5: opcode{"SBC", 2, 3, modeZeroPage, opSBCAlt},
0xF5: opcode{"SBC", 2, 4, modeZeroPageX, opSBCAlt},
0xED: opcode{"SBC", 3, 4, modeAbsolute, opSBCAlt},
0xFD: opcode{"SBC", 3, 4, modeAbsoluteX, opSBCAlt},
0xF9: opcode{"SBC", 3, 4, modeAbsoluteY, opSBCAlt},
0xE1: opcode{"SBC", 2, 6, modeIndexedIndirectX, opSBCAlt},
0xF1: opcode{"SBC", 2, 5, modeIndirectIndexedY, opSBCAlt},
// Different cycle count
0x1e: opcode{"ASL", 3, 6, modeAbsoluteX, buildOpShift(true, false)},
0x3e: opcode{"ROL", 3, 6, modeAbsoluteX, buildOpShift(true, true)},
0x5e: opcode{"LSR", 3, 6, modeAbsoluteX, buildOpShift(false, false)},
0x7e: opcode{"ROR", 3, 6, modeAbsoluteX, buildOpShift(false, true)},
// New indirect zero page addresssing mode
0x12: opcode{"ORA", 2, 5, modeIndirectZeroPage, buildOpLogic(operationOr)},
0x32: opcode{"AND", 2, 5, modeIndirectZeroPage, buildOpLogic(operationAnd)},
0x52: opcode{"EOR", 2, 5, modeIndirectZeroPage, buildOpLogic(operationXor)},
0x72: opcode{"ADC", 2, 5, modeIndirectZeroPage, opADCAlt},
0x92: opcode{"STA", 2, 5, modeIndirectZeroPage, buildOpStore(regA)},
0xb2: opcode{"LDA", 2, 5, modeIndirectZeroPage, buildOpLoad(regA)},
0xd2: opcode{"CMP", 2, 5, modeIndirectZeroPage, buildOpCompare(regA)},
0xf2: opcode{"SBC", 2, 5, modeIndirectZeroPage, opSBCAlt},
// New addressing options
0x89: opcode{"BIT", 2, 2, modeImmediate, opBIT},
0x34: opcode{"BIT", 2, 4, modeZeroPageX, opBIT},
0x3c: opcode{"BIT", 3, 4, modeAbsoluteX, opBIT},
0x1a: opcode{"INC", 1, 2, modeAccumulator, buildOpIncDec(true)},
0x3a: opcode{"DEC", 1, 2, modeAccumulator, buildOpIncDec(false)},
0x7c: opcode{"JMP", 3, 6, modeAbsoluteIndexedIndirectX, opJMP},
// Additional instructions: BRA, PHX, PHY, PLX, PLY, STZ, TRB, TSB
0xda: opcode{"PHX", 1, 3, modeImplicit, buildOpPush(regX)},
0x5a: opcode{"PHY", 1, 3, modeImplicit, buildOpPush(regY)},
0xfa: opcode{"PLX", 1, 4, modeImplicit, buildOpPull(regX)},
0x7a: opcode{"PLY", 1, 4, modeImplicit, buildOpPull(regY)},
0x80: opcode{"BRA", 2, 4, modeRelative, opJMP},
0x64: opcode{"STZ", 2, 3, modeZeroPage, opSTZ},
0x74: opcode{"STZ", 2, 4, modeZeroPageX, opSTZ},
0x9c: opcode{"STZ", 3, 4, modeAbsolute, opSTZ},
0x9e: opcode{"STZ", 3, 5, modeAbsoluteX, opSTZ},
0x14: opcode{"TRB", 2, 5, modeZeroPage, opTRB},
0x1c: opcode{"TRB", 3, 6, modeAbsolute, opTRB},
0x04: opcode{"TSB", 2, 5, modeZeroPage, opTSB},
0x0c: opcode{"TSB", 3, 6, modeAbsolute, opTSB},
// Additional in Rockwell 65c02 and WDC 65c02?
// They have a double addressing mode: zeropage and relative.
0x0f: opcode{"BBR0", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(0, false)},
0x1f: opcode{"BBR1", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(1, false)},
0x2f: opcode{"BBR2", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(2, false)},
0x3f: opcode{"BBR3", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(3, false)},
0x4f: opcode{"BBR4", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(4, false)},
0x5f: opcode{"BBR5", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(5, false)},
0x6f: opcode{"BBR6", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(6, false)},
0x7f: opcode{"BBR7", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(7, false)},
0x8f: opcode{"BBS0", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(0, true)},
0x9f: opcode{"BBS1", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(1, true)},
0xaf: opcode{"BBS2", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(2, true)},
0xbf: opcode{"BBS3", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(3, true)},
0xcf: opcode{"BBS4", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(4, true)},
0xdf: opcode{"BBS5", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(5, true)},
0xef: opcode{"BBS6", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(6, true)},
0xff: opcode{"BBS7", 3, 2, modeZeroPageAndRelative, buildOpBranchOnBit(7, true)},
0x07: opcode{"RMB0", 2, 5, modeZeroPage, buildOpSetBit(0, false)},
0x17: opcode{"RMB1", 2, 5, modeZeroPage, buildOpSetBit(1, false)},
0x27: opcode{"RMB2", 2, 5, modeZeroPage, buildOpSetBit(2, false)},
0x37: opcode{"RMB3", 2, 5, modeZeroPage, buildOpSetBit(3, false)},
0x47: opcode{"RMB4", 2, 5, modeZeroPage, buildOpSetBit(4, false)},
0x57: opcode{"RMB5", 2, 5, modeZeroPage, buildOpSetBit(5, false)},
0x67: opcode{"RMB6", 2, 5, modeZeroPage, buildOpSetBit(6, false)},
0x77: opcode{"RMB7", 2, 5, modeZeroPage, buildOpSetBit(7, false)},
0x87: opcode{"SMB0", 2, 5, modeZeroPage, buildOpSetBit(0, true)},
0x97: opcode{"SMB1", 2, 5, modeZeroPage, buildOpSetBit(1, true)},
0xa7: opcode{"SMB2", 2, 5, modeZeroPage, buildOpSetBit(2, true)},
0xb7: opcode{"SMB3", 2, 5, modeZeroPage, buildOpSetBit(3, true)},
0xc7: opcode{"SMB4", 2, 5, modeZeroPage, buildOpSetBit(4, true)},
0xd7: opcode{"SMB5", 2, 5, modeZeroPage, buildOpSetBit(5, true)},
0xe7: opcode{"SMB6", 2, 5, modeZeroPage, buildOpSetBit(6, true)},
0xf7: opcode{"SMB7", 2, 5, modeZeroPage, buildOpSetBit(7, true)},
// Maybe additional Rockwell: STP, WAI
}

View File

@ -1,32 +0,0 @@
package core6502
import (
"testing"
)
func TestCMOs65c02NoUndocumented(t *testing.T) {
m := new(FlatMemory)
s := NewCMOS65c02(m)
for i := 0; i < 256; i++ {
if s.opcodes[i].cycles == 0 {
t.Errorf("Opcode missing for $%02x.", i)
}
}
}
func TestCMOS65c02asNMOS(t *testing.T) {
m := new(FlatMemory)
s := NewCMOS65c02(m)
m.loadBinary("testdata/6502_functional_test.bin")
executeSuite(t, s, 0x200, 240, false, 255)
}
func TestCMOS65c02(t *testing.T) {
m := new(FlatMemory)
s := NewCMOS65c02(m)
m.loadBinary("testdata/65C02_extended_opcodes_test.bin")
executeSuite(t, s, 0x202, 240, false, 255)
}

View File

@ -1,140 +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
const (
maxInstructionSize = 3
)
// State represents the state of the simulated device
type State struct {
reg registers
mem Memory
cycles uint64
opcodes *[256]opcode
trace bool
lineCache []uint8
// We cache the allocation of a line to avoid a malloc per instruction. To be used only
// by ExecuteInstruction(). 2x speedup on the emulation!!
}
const (
vectorNMI uint16 = 0xfffa
vectorReset uint16 = 0xfffc
vectorBreak uint16 = 0xfffe
)
type opcode struct {
name string
bytes uint16
cycles int
addressMode int
action opFunc
}
type opFunc func(s *State, line []uint8, opcode opcode)
func (s *State) executeLine(line []uint8) {
opcode := s.opcodes[line[0]]
if opcode.cycles == 0 {
panic(fmt.Sprintf("Unknown opcode 0x%02x\n", line[0]))
}
opcode.action(s, line, opcode)
}
// ExecuteInstruction transforms the state given after a single instruction is executed.
func (s *State) ExecuteInstruction() {
pc := s.reg.getPC()
opcodeID := s.mem.PeekCode(pc)
opcode := s.opcodes[opcodeID]
if opcode.cycles == 0 {
panic(fmt.Sprintf("Unknown opcode 0x%02x\n", opcodeID))
}
if s.lineCache == nil {
s.lineCache = make([]uint8, maxInstructionSize)
}
for i := uint16(0); i < opcode.bytes; i++ {
s.lineCache[i] = s.mem.PeekCode(pc)
pc++
}
s.reg.setPC(pc)
if s.trace {
//fmt.Printf("%#04x %#02x\n", pc-opcode.bytes, opcodeID)
fmt.Printf("%#04x %-13s: ", pc-opcode.bytes, lineString(s.lineCache, opcode))
}
opcode.action(s, s.lineCache, opcode)
s.cycles += uint64(opcode.cycles)
if s.trace {
fmt.Printf("%v, [%02x]\n", s.reg, s.lineCache[0:opcode.bytes])
}
}
// Reset resets the processor. Moves the program counter to the vector in 0cfffc.
func (s *State) Reset() {
startAddress := getWord(s.mem, vectorReset)
s.cycles += 6
s.reg.setPC(startAddress)
}
// GetCycles returns the count of CPU cycles since last reset.
func (s *State) GetCycles() uint64 {
return s.cycles
}
// SetTrace activates tracing of the cpu execution
func (s *State) SetTrace(trace bool) {
s.trace = trace
}
// GetTrace gets trhe tracing state of the cpu execution
func (s *State) GetTrace() bool {
return s.trace
}
// GetPCAndSP returns the current program counter and stack pointer. Used to trace MLI calls
func (s *State) GetPCAndSP() (uint16, uint8) {
return s.reg.getPC(), s.reg.getSP()
}
// GetCarryAndAcc returns the value of te carry flag and the accumulator. Used to trace MLI calls
func (s *State) GetCarryAndAcc() (bool, uint8) {
return s.reg.getFlag(flagC), s.reg.getA()
}
// Save saves the CPU state (registers and cycle counter)
func (s *State) Save(w io.Writer) error {
err := binary.Write(w, binary.BigEndian, s.cycles)
if err != nil {
return err
}
binary.Write(w, binary.BigEndian, s.reg.data)
if err != nil {
return err
}
return nil
}
// Load loads the CPU state (registers and cycle counter)
func (s *State) Load(r io.Reader) error {
err := binary.Read(r, binary.BigEndian, &s.cycles)
if err != nil {
return err
}
err = binary.Read(r, binary.BigEndian, &s.reg.data)
if err != nil {
return err
}
return nil
}

View File

@ -1,485 +0,0 @@
package core6502
import (
"testing"
)
func TestLoad(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.executeLine([]uint8{0xA9, 0x42})
if s.reg.getA() != 0x42 {
t.Error("Error in LDA #")
}
s.executeLine([]uint8{0xA9, 0x00})
if s.reg.getP() != flagZ {
t.Error("Error in flags for LDA $0")
}
s.executeLine([]uint8{0xA9, 0xF0})
if s.reg.getP() != flagN {
t.Error("Error in flags for LDA $F0")
}
s.executeLine([]uint8{0xA0, 0xFE})
if s.reg.getY() != 0xFE {
t.Error("Error in LDY #")
}
s.mem.Poke(0x38, 0x87)
s.executeLine([]uint8{0xA5, 0x38})
if s.reg.getA() != 0x87 {
t.Error("Error in LDA zpg")
}
s.mem.Poke(0x57, 0x90)
s.reg.setX(0x10)
s.executeLine([]uint8{0xB5, 0x47})
if s.reg.getA() != 0x90 {
t.Error("Error in LDA zpg, X")
}
s.mem.Poke(0x38, 0x12)
s.reg.setX(0x89)
s.executeLine([]uint8{0xB5, 0xAF})
if s.reg.getA() != 0x12 {
t.Error("Error in LDA zpgX with sero page overflow")
}
s.mem.Poke(0x1234, 0x67)
s.executeLine([]uint8{0xAD, 0x34, 0x12})
if s.reg.getA() != 0x67 {
t.Error("Error in LDA abs")
}
s.mem.Poke(0xC057, 0x7E)
s.reg.setX(0x57)
s.executeLine([]uint8{0xBD, 0x00, 0xC0})
if s.reg.getA() != 0x7E {
t.Error("Error in LDA abs, X")
}
s.mem.Poke(0xD059, 0x7A)
s.reg.setY(0x59)
s.executeLine([]uint8{0xB9, 0x00, 0xD0})
if s.reg.getA() != 0x7A {
t.Error("Error in LDA abs, Y")
}
s.mem.Poke(0x24, 0x74)
s.mem.Poke(0x25, 0x20)
s.reg.setX(0x04)
s.mem.Poke(0x2074, 0x66)
s.executeLine([]uint8{0xA1, 0x20})
if s.reg.getA() != 0x66 {
t.Error("Error in LDA (oper,X)")
}
s.mem.Poke(0x86, 0x28)
s.mem.Poke(0x87, 0x40)
s.reg.setY(0x10)
s.mem.Poke(0x4038, 0x99)
s.executeLine([]uint8{0xB1, 0x86})
if s.reg.getA() != 0x99 {
t.Error("Error in LDA (oper),Y")
}
}
func TestStore(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0x10)
s.reg.setX(0x40)
s.reg.setY(0x80)
s.executeLine([]uint8{0x85, 0x50})
if s.mem.Peek(0x0050) != 0x10 {
t.Error("Error in STA zpg")
}
s.executeLine([]uint8{0x86, 0x51})
if s.mem.Peek(0x0051) != 0x40 {
t.Error("Error in STX zpg")
}
s.executeLine([]uint8{0x84, 0x52})
if s.mem.Peek(0x0052) != 0x80 {
t.Error("Error in STY zpg")
}
s.executeLine([]uint8{0x8D, 0x20, 0xC0})
if s.mem.Peek(0xC020) != 0x10 {
t.Error("Error in STA abs")
}
s.executeLine([]uint8{0x9D, 0x08, 0x10})
if s.mem.Peek(0x1048) != 0x10 {
t.Error("Error in STA abs, X")
}
}
func TestTransfer(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0xB0)
s.executeLine([]uint8{0xAA})
if s.reg.getX() != 0xB0 {
t.Error("Error in TAX")
}
if s.reg.getP() != flagN {
t.Error("Error in TAX flags")
}
s.reg.setA(0xB1)
s.executeLine([]uint8{0xA8})
if s.reg.getY() != 0xB1 {
t.Error("Error in TAY")
}
s.reg.setSP(0xB2)
s.executeLine([]uint8{0xBA})
if s.reg.getX() != 0xB2 {
t.Error("Error in TSX")
}
s.reg.setX(0xB3)
s.executeLine([]uint8{0x8A})
if s.reg.getA() != 0xB3 {
t.Error("Error in TXA")
}
s.reg.setX(0xB4)
s.executeLine([]uint8{0x9A})
if s.reg.getSP() != 0xB4 {
t.Error("Error in TXS")
}
s.reg.setY(0xB5)
s.executeLine([]uint8{0x98})
if s.reg.getA() != 0xB5 {
t.Error("Error in TYA")
}
}
func TestIncDec(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setX(0x7E)
s.executeLine([]uint8{0xE8})
if s.reg.getX() != 0x7F {
t.Errorf("Error in INX")
}
s.reg.setY(0xFC)
s.executeLine([]uint8{0x88})
if s.reg.getY() != 0xFB {
t.Error("Error in DEY")
}
if s.reg.getP() != flagN {
t.Error("Error in DEY flags")
}
}
func TestShiftRotate(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0xF0)
s.executeLine([]uint8{0x2A})
if s.reg.getA() != 0xE0 {
t.Errorf("Error in ROL")
}
if !s.reg.getFlag(flagC) {
t.Errorf("Error in ROL carry. %v", s.reg)
}
s.reg.setFlag(flagC)
s.reg.setA(0x0F)
s.executeLine([]uint8{0x6A})
if s.reg.getA() != 0x87 {
t.Errorf("Error in ROR. %v", s.reg)
}
if !s.reg.getFlag(flagC) {
t.Errorf("Error in ROR carry")
}
s.reg.setFlag(flagC)
s.reg.setA(0x81)
s.executeLine([]uint8{0x0A})
if s.reg.getA() != 0x02 {
t.Errorf("Error in ASL. %v", s.reg)
}
if !s.reg.getFlag(flagC) {
t.Errorf("Error in ASL carry")
}
s.reg.setFlag(flagC)
s.reg.setA(0x02)
s.executeLine([]uint8{0x4A})
if s.reg.getA() != 0x01 {
t.Errorf("Error in LSR. %v", s.reg)
}
if s.reg.getFlag(flagC) {
t.Errorf("Error in LSR carry")
}
}
func TestClearSetFlag(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setP(0x00)
s.executeLine([]uint8{0xF8})
if !s.reg.getFlag(flagD) {
t.Errorf("Error in SED. %v", s.reg)
}
s.executeLine([]uint8{0xD8})
if s.reg.getFlag(flagD) {
t.Errorf("Error in CLD. %v", s.reg)
}
}
func TestLogic(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0xF0)
s.executeLine([]uint8{0x29, 0x1C})
if s.reg.getA() != 0x10 {
t.Errorf("Error in AND <. %v", s.reg)
}
s.reg.setA(0xF0)
s.executeLine([]uint8{0x49, 0x1C})
if s.reg.getA() != 0xEC {
t.Errorf("Error in EOR <. %v", s.reg)
}
s.reg.setA(0xF0)
s.executeLine([]uint8{0x09, 0x0C})
if s.reg.getA() != 0xFC {
t.Errorf("Error in ORA <. %v", s.reg)
}
}
func TestAdd(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0xA0)
s.reg.clearFlag(flagC)
s.executeLine([]uint8{0x69, 0x0B})
if s.reg.getA() != 0xAB {
t.Errorf("Error in ADC. %v", s.reg)
}
if s.reg.getFlag(flagC) {
t.Errorf("Error in carry ADC. %v", s.reg)
}
s.reg.setA(0xFF)
s.reg.clearFlag(flagC)
s.executeLine([]uint8{0x69, 0x02})
if s.reg.getA() != 0x01 {
t.Errorf("Error in ADC with carry. %v", s.reg)
}
if !s.reg.getFlag(flagC) {
t.Errorf("Error in carry ADC with carry. %v", s.reg)
}
s.reg.setA(0xA0)
s.reg.setFlag(flagC)
s.executeLine([]uint8{0x69, 0x01})
if s.reg.getA() != 0xA2 {
t.Errorf("Error in carried ADC with carry. %v", s.reg)
}
if s.reg.getFlag(flagC) {
t.Errorf("Error in carry in carried ADC with carry. %v", s.reg)
}
}
func TestAddDecimal(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setFlag(flagD)
s.reg.setA(0x12)
s.reg.clearFlag(flagC)
s.executeLine([]uint8{0x69, 0x013})
if s.reg.getA() != 0x25 {
t.Errorf("Error in ADC decimal. %v", s.reg)
}
if s.reg.getFlag(flagC) {
t.Errorf("Error in carry ADC. %v", s.reg)
}
s.reg.setA(0x44)
s.reg.clearFlag(flagC)
s.executeLine([]uint8{0x69, 0x68})
if s.reg.getA() != 0x12 {
t.Errorf("Error in ADC decimal with carry. %v", s.reg)
}
if !s.reg.getFlag(flagC) {
t.Errorf("Error in carry ADC decimal with carry. %v", s.reg)
}
s.reg.setA(0x44)
s.reg.setFlag(flagC)
s.executeLine([]uint8{0x69, 0x23})
if s.reg.getA() != 0x68 {
t.Errorf("Error in carried ADC decimal with carry. %v", s.reg)
}
if s.reg.getFlag(flagC) {
t.Errorf("Error in carry in carried ADC decimal with carry. %v", s.reg)
}
}
func TestSub(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0x09)
s.reg.clearFlag(flagC)
s.executeLine([]uint8{0xE9, 0x05})
if s.reg.getA() != 0x03 {
t.Errorf("Error in SBC. %v", s.reg)
}
if !s.reg.getFlag(flagC) {
t.Errorf("Error in carry SBC. %v", s.reg)
}
s.reg.setA(0x01)
s.reg.clearFlag(flagC)
s.executeLine([]uint8{0xE9, 0x02})
if s.reg.getA() != 0xFE {
t.Errorf("Error in SBC with carry. %v", s.reg)
}
if s.reg.getFlag(flagC) {
t.Errorf("Error in carry SBC with carry. %v", s.reg)
}
s.reg.setA(0x08)
s.reg.setFlag(flagC)
s.executeLine([]uint8{0xE9, 0x02})
if s.reg.getA() != 0x06 {
t.Errorf("Error in carried SBC with carry. %v", s.reg)
}
if !s.reg.getFlag(flagC) {
t.Errorf("Error in carry in carried SBC with carry. %v", s.reg)
}
}
func TestCompare(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0x02)
s.executeLine([]uint8{0xC9, 0x01})
if s.reg.getP() != 0x01 {
t.Errorf("Error in CMP <. %v", s.reg)
}
s.executeLine([]uint8{0xC9, 0x02})
if s.reg.getP() != 0x03 {
t.Errorf("Error in CMP =. %v", s.reg)
}
s.executeLine([]uint8{0xC9, 0x03})
if s.reg.getP() != 0x80 {
t.Errorf("Error in CMP >. %v", s.reg)
}
s.reg.setX(0x04)
s.executeLine([]uint8{0xE0, 0x05})
if s.reg.getP() != 0x80 {
t.Errorf("Error in CPX >. %v", s.reg)
}
s.reg.setY(0x08)
s.executeLine([]uint8{0xC0, 0x09})
if s.reg.getP() != 0x80 {
t.Errorf("Error in CPY >. %v", s.reg)
}
}
func TestBit(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setA(0x0F)
s.mem.Poke(0x0040, 0xF0)
s.executeLine([]uint8{0x24, 0x40})
if s.reg.getP() != 0xC2 {
t.Errorf("Error in BIT. %v", s.reg)
}
s.reg.setA(0xF0)
s.mem.Poke(0x0040, 0xF0)
s.executeLine([]uint8{0x24, 0x40})
if s.reg.getP() != 0xC0 {
t.Errorf("Error in BIT, 2. %v", s.reg)
}
s.reg.setA(0xF0)
s.mem.Poke(0x01240, 0x80)
s.executeLine([]uint8{0x2C, 0x40, 0x12})
if s.reg.getP() != 0x80 {
t.Errorf("Error in BIT, 2. %v", s.reg)
}
}
func TestBranch(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setPC(0xC600)
s.reg.setFlag(flagV)
s.executeLine([]uint8{0x50, 0x20})
if s.reg.getPC() != 0xC600 {
t.Errorf("Error in BVC, %v", s.reg)
}
s.executeLine([]uint8{0x70, 0x20})
if s.reg.getPC() != 0xC620 {
t.Errorf("Error in BVS, %v", s.reg)
}
s.reg.setPC(0xD600)
s.reg.clearFlag(flagC)
s.executeLine([]uint8{0x90, 0xA0})
if s.reg.getPC() != 0xD5A0 {
t.Errorf("Error in BCC, %v", s.reg)
}
}
func TestStack(t *testing.T) {
s := NewNMOS6502(new(FlatMemory))
s.reg.setSP(0xF0)
s.reg.setA(0xA0)
s.reg.setP(0x0A)
s.executeLine([]uint8{0x48})
if s.reg.getSP() != 0xEF {
t.Errorf("Error in PHA stack pointer, %v", s.reg)
}
if s.mem.Peek(0x01F0) != 0xA0 {
t.Errorf("Error in PHA, %v", s.reg)
}
s.executeLine([]uint8{0x08})
if s.reg.getSP() != 0xEE {
t.Errorf("Error in PHP stack pointer, %v", s.reg)
}
if s.mem.Peek(0x01EF) != 0x3A {
t.Errorf("Error in PHP, %v", s.reg)
}
s.executeLine([]uint8{0x68})
if s.reg.getSP() != 0xEF {
t.Errorf("Error in PLA stack pointer, %v", s.reg)
}
if s.reg.getA() != 0x3A {
t.Errorf("Error in PLA, %v", s.reg)
}
s.executeLine([]uint8{0x28})
if s.reg.getSP() != 0xF0 {
t.Errorf("Error in PLP stack pointer, %v", s.reg)
}
if s.reg.getP() != 0xA0 {
t.Errorf("Error in PLP, %v", s.reg)
}
}

View File

@ -1,54 +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)
// PeekCode can bu used to optimize the memory manager to requests with more
// locality. It must return the same as a call to Peek()
PeekCode(address uint16) 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]
}
// PeekCode returns the data on the given address
func (m *FlatMemory) PeekCode(address uint16) uint8 {
return m.data[address]
}
// Poke sets the data at the given address
func (m *FlatMemory) Poke(address uint16, value uint8) {
m.data[address] = value
}
func (m *FlatMemory) loadBinary(filename string) error {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
for i, v := range bytes {
m.Poke(uint16(i), uint8(v))
}
return nil
}

View File

@ -1,189 +0,0 @@
package core6502
// NewNMOS6502 returns an initialized NMOS6502
func NewNMOS6502(m Memory) *State {
var s State
s.mem = m
s.opcodes = &opcodesNMOS6502
return &s
}
var opcodesNMOS6502 = [256]opcode{
0x00: opcode{"BRK", 1, 7, modeImplicit, opBRK},
0x4C: opcode{"JMP", 3, 3, modeAbsolute, opJMP},
0x6C: opcode{"JMP", 3, 3, modeIndirect, opJMP},
0x20: opcode{"JSR", 3, 6, modeAbsolute, opJSR},
0x40: opcode{"RTI", 1, 6, modeImplicit, opRTI},
0x60: opcode{"RTS", 1, 6, modeImplicit, opRTS},
0x48: opcode{"PHA", 1, 3, modeImplicit, buildOpPush(regA)},
0x08: opcode{"PHP", 1, 3, modeImplicit, buildOpPush(regP)},
0x68: opcode{"PLA", 1, 4, modeImplicit, buildOpPull(regA)},
0x28: opcode{"PLP", 1, 4, modeImplicit, buildOpPull(regP)},
0x09: opcode{"ORA", 2, 2, modeImmediate, buildOpLogic(operationOr)},
0x05: opcode{"ORA", 2, 3, modeZeroPage, buildOpLogic(operationOr)},
0x15: opcode{"ORA", 2, 4, modeZeroPageX, buildOpLogic(operationOr)},
0x0D: opcode{"ORA", 3, 4, modeAbsolute, buildOpLogic(operationOr)},
0x1D: opcode{"ORA", 3, 4, modeAbsoluteX, buildOpLogic(operationOr)}, // Extra cycles
0x19: opcode{"ORA", 3, 4, modeAbsoluteY, buildOpLogic(operationOr)}, // Extra cycles
0x01: opcode{"ORA", 2, 6, modeIndexedIndirectX, buildOpLogic(operationOr)},
0x11: opcode{"ORA", 2, 5, modeIndirectIndexedY, buildOpLogic(operationOr)}, // Extra cycles
0x29: opcode{"AND", 2, 2, modeImmediate, buildOpLogic(operationAnd)},
0x25: opcode{"AND", 2, 3, modeZeroPage, buildOpLogic(operationAnd)},
0x35: opcode{"AND", 2, 4, modeZeroPageX, buildOpLogic(operationAnd)},
0x2D: opcode{"AND", 3, 4, modeAbsolute, buildOpLogic(operationAnd)},
0x3D: opcode{"AND", 3, 4, modeAbsoluteX, buildOpLogic(operationAnd)}, // Extra cycles
0x39: opcode{"AND", 3, 4, modeAbsoluteY, buildOpLogic(operationAnd)}, // Extra cycles
0x21: opcode{"AND", 2, 6, modeIndexedIndirectX, buildOpLogic(operationAnd)},
0x31: opcode{"AND", 2, 5, modeIndirectIndexedY, buildOpLogic(operationAnd)}, // Extra cycles
0x49: opcode{"EOR", 2, 2, modeImmediate, buildOpLogic(operationXor)},
0x45: opcode{"EOR", 2, 3, modeZeroPage, buildOpLogic(operationXor)},
0x55: opcode{"EOR", 2, 4, modeZeroPageX, buildOpLogic(operationXor)},
0x4D: opcode{"EOR", 3, 4, modeAbsolute, buildOpLogic(operationXor)},
0x5D: opcode{"EOR", 3, 4, modeAbsoluteX, buildOpLogic(operationXor)}, // Extra cycles
0x59: opcode{"EOR", 3, 4, modeAbsoluteY, buildOpLogic(operationXor)}, // Extra cycles
0x41: opcode{"EOR", 2, 6, modeIndexedIndirectX, buildOpLogic(operationXor)},
0x51: opcode{"EOR", 2, 5, modeIndirectIndexedY, buildOpLogic(operationXor)}, // Extra cycles
0x69: opcode{"ADC", 2, 2, modeImmediate, opADC},
0x65: opcode{"ADC", 2, 3, modeZeroPage, opADC},
0x75: opcode{"ADC", 2, 4, modeZeroPageX, opADC},
0x6D: opcode{"ADC", 3, 4, modeAbsolute, opADC},
0x7D: opcode{"ADC", 3, 4, modeAbsoluteX, opADC}, // Extra cycles
0x79: opcode{"ADC", 3, 4, modeAbsoluteY, opADC}, // Extra cycles
0x61: opcode{"ADC", 2, 6, modeIndexedIndirectX, opADC},
0x71: opcode{"ADC", 2, 5, modeIndirectIndexedY, opADC}, // Extra cycles
0xE9: opcode{"SBC", 2, 2, modeImmediate, opSBC},
0xE5: opcode{"SBC", 2, 3, modeZeroPage, opSBC},
0xF5: opcode{"SBC", 2, 4, modeZeroPageX, opSBC},
0xED: opcode{"SBC", 3, 4, modeAbsolute, opSBC},
0xFD: opcode{"SBC", 3, 4, modeAbsoluteX, opSBC}, // Extra cycles
0xF9: opcode{"SBC", 3, 4, modeAbsoluteY, opSBC}, // Extra cycles
0xE1: opcode{"SBC", 2, 6, modeIndexedIndirectX, opSBC},
0xF1: opcode{"SBC", 2, 5, modeIndirectIndexedY, opSBC}, // Extra cycles
0x24: opcode{"BIT", 2, 3, modeZeroPage, opBIT},
0x2C: opcode{"BIT", 3, 3, modeAbsolute, opBIT},
0xC9: opcode{"CMP", 2, 2, modeImmediate, buildOpCompare(regA)},
0xC5: opcode{"CMP", 2, 3, modeZeroPage, buildOpCompare(regA)},
0xD5: opcode{"CMP", 2, 4, modeZeroPageX, buildOpCompare(regA)},
0xCD: opcode{"CMP", 3, 4, modeAbsolute, buildOpCompare(regA)},
0xDD: opcode{"CMP", 3, 4, modeAbsoluteX, buildOpCompare(regA)}, // Extra cycles
0xD9: opcode{"CMP", 3, 4, modeAbsoluteY, buildOpCompare(regA)}, // Extra cycles
0xC1: opcode{"CMP", 2, 6, modeIndexedIndirectX, buildOpCompare(regA)},
0xD1: opcode{"CMP", 2, 5, modeIndirectIndexedY, buildOpCompare(regA)}, // Extra cycles
0xE0: opcode{"CPX", 2, 2, modeImmediate, buildOpCompare(regX)},
0xE4: opcode{"CPX", 2, 3, modeZeroPage, buildOpCompare(regX)},
0xEC: opcode{"CPX", 3, 4, modeAbsolute, buildOpCompare(regX)},
0xC0: opcode{"CPY", 2, 2, modeImmediate, buildOpCompare(regY)},
0xC4: opcode{"CPY", 2, 3, modeZeroPage, buildOpCompare(regY)},
0xCC: opcode{"CPY", 3, 4, modeAbsolute, buildOpCompare(regY)},
0x2A: opcode{"ROL", 1, 2, modeAccumulator, buildOpShift(true, true)},
0x26: opcode{"ROL", 2, 5, modeZeroPage, buildOpShift(true, true)},
0x36: opcode{"ROL", 2, 6, modeZeroPageX, buildOpShift(true, true)},
0x2E: opcode{"ROL", 3, 6, modeAbsolute, buildOpShift(true, true)},
0x3E: opcode{"ROL", 3, 7, modeAbsoluteX, buildOpShift(true, true)},
0x6A: opcode{"ROR", 1, 2, modeAccumulator, buildOpShift(false, true)},
0x66: opcode{"ROR", 2, 5, modeZeroPage, buildOpShift(false, true)},
0x76: opcode{"ROR", 2, 6, modeZeroPageX, buildOpShift(false, true)},
0x6E: opcode{"ROR", 3, 6, modeAbsolute, buildOpShift(false, true)},
0x7E: opcode{"ROR", 3, 7, modeAbsoluteX, buildOpShift(false, true)},
0x0A: opcode{"ASL", 1, 2, modeAccumulator, buildOpShift(true, false)},
0x06: opcode{"ASL", 2, 5, modeZeroPage, buildOpShift(true, false)},
0x16: opcode{"ASL", 2, 6, modeZeroPageX, buildOpShift(true, false)},
0x0E: opcode{"ASL", 3, 6, modeAbsolute, buildOpShift(true, false)},
0x1E: opcode{"ASL", 3, 7, modeAbsoluteX, buildOpShift(true, false)},
0x4A: opcode{"LSR", 1, 2, modeAccumulator, buildOpShift(false, false)},
0x46: opcode{"LSR", 2, 5, modeZeroPage, buildOpShift(false, false)},
0x56: opcode{"LSR", 2, 6, modeZeroPageX, buildOpShift(false, false)},
0x4E: opcode{"LSR", 3, 6, modeAbsolute, buildOpShift(false, false)},
0x5E: opcode{"LSR", 3, 7, modeAbsoluteX, buildOpShift(false, false)},
0x38: opcode{"SEC", 1, 2, modeImplicit, buildOpUpdateFlag(flagC, true)},
0xF8: opcode{"SED", 1, 2, modeImplicit, buildOpUpdateFlag(flagD, true)},
0x78: opcode{"SEI", 1, 2, modeImplicit, buildOpUpdateFlag(flagI, true)},
0x18: opcode{"CLC", 1, 2, modeImplicit, buildOpUpdateFlag(flagC, false)},
0xD8: opcode{"CLD", 1, 2, modeImplicit, buildOpUpdateFlag(flagD, false)},
0x58: opcode{"CLI", 1, 2, modeImplicit, buildOpUpdateFlag(flagI, false)},
0xB8: opcode{"CLV", 1, 2, modeImplicit, buildOpUpdateFlag(flagV, false)},
0xE6: opcode{"INC", 2, 5, modeZeroPage, buildOpIncDec(true)},
0xF6: opcode{"INC", 2, 6, modeZeroPageX, buildOpIncDec(true)},
0xEE: opcode{"INC", 3, 6, modeAbsolute, buildOpIncDec(true)},
0xFE: opcode{"INC", 3, 7, modeAbsoluteX, buildOpIncDec(true)},
0xC6: opcode{"DEC", 2, 5, modeZeroPage, buildOpIncDec(false)},
0xD6: opcode{"DEC", 2, 6, modeZeroPageX, buildOpIncDec(false)},
0xCE: opcode{"DEC", 3, 6, modeAbsolute, buildOpIncDec(false)},
0xDE: opcode{"DEC", 3, 7, modeAbsoluteX, buildOpIncDec(false)},
0xE8: opcode{"INX", 1, 2, modeImplicitX, buildOpIncDec(true)},
0xC8: opcode{"INY", 1, 2, modeImplicitY, buildOpIncDec(true)},
0xCA: opcode{"DEX", 1, 2, modeImplicitX, buildOpIncDec(false)},
0x88: opcode{"DEY", 1, 2, modeImplicitY, buildOpIncDec(false)},
0xAA: opcode{"TAX", 1, 2, modeImplicit, buildOpTransfer(regA, regX)},
0xA8: opcode{"TAY", 1, 2, modeImplicit, buildOpTransfer(regA, regY)},
0x8A: opcode{"TXA", 1, 2, modeImplicit, buildOpTransfer(regX, regA)},
0x98: opcode{"TYA", 1, 2, modeImplicit, buildOpTransfer(regY, regA)},
0x9A: opcode{"TXS", 1, 2, modeImplicit, buildOpTransfer(regX, regSP)},
0xBA: opcode{"TSX", 1, 2, modeImplicit, buildOpTransfer(regSP, regX)},
0xA9: opcode{"LDA", 2, 2, modeImmediate, buildOpLoad(regA)},
0xA5: opcode{"LDA", 2, 3, modeZeroPage, buildOpLoad(regA)},
0xB5: opcode{"LDA", 2, 4, modeZeroPageX, buildOpLoad(regA)},
0xAD: opcode{"LDA", 3, 4, modeAbsolute, buildOpLoad(regA)},
0xBD: opcode{"LDA", 3, 4, modeAbsoluteX, buildOpLoad(regA)}, // Extra cycles
0xB9: opcode{"LDA", 3, 4, modeAbsoluteY, buildOpLoad(regA)}, // Extra cycles
0xA1: opcode{"LDA", 2, 6, modeIndexedIndirectX, buildOpLoad(regA)},
0xB1: opcode{"LDA", 2, 5, modeIndirectIndexedY, buildOpLoad(regA)}, // Extra cycles
0xA2: opcode{"LDX", 2, 2, modeImmediate, buildOpLoad(regX)},
0xA6: opcode{"LDX", 2, 3, modeZeroPage, buildOpLoad(regX)},
0xB6: opcode{"LDX", 2, 4, modeZeroPageY, buildOpLoad(regX)},
0xAE: opcode{"LDX", 3, 4, modeAbsolute, buildOpLoad(regX)},
0xBE: opcode{"LDX", 3, 4, modeAbsoluteY, buildOpLoad(regX)}, // Extra cycles
0xA0: opcode{"LDY", 2, 2, modeImmediate, buildOpLoad(regY)},
0xA4: opcode{"LDY", 2, 3, modeZeroPage, buildOpLoad(regY)},
0xB4: opcode{"LDY", 2, 4, modeZeroPageX, buildOpLoad(regY)},
0xAC: opcode{"LDY", 3, 4, modeAbsolute, buildOpLoad(regY)},
0xBC: opcode{"LDY", 3, 4, modeAbsoluteX, buildOpLoad(regY)}, // Extra cycles
0x85: opcode{"STA", 2, 3, modeZeroPage, buildOpStore(regA)},
0x95: opcode{"STA", 2, 4, modeZeroPageX, buildOpStore(regA)},
0x8D: opcode{"STA", 3, 4, modeAbsolute, buildOpStore(regA)},
0x9D: opcode{"STA", 3, 5, modeAbsoluteX, buildOpStore(regA)},
0x99: opcode{"STA", 3, 5, modeAbsoluteY, buildOpStore(regA)},
0x81: opcode{"STA", 2, 6, modeIndexedIndirectX, buildOpStore(regA)},
0x91: opcode{"STA", 2, 6, modeIndirectIndexedY, buildOpStore(regA)},
0x86: opcode{"STX", 2, 3, modeZeroPage, buildOpStore(regX)},
0x96: opcode{"STX", 2, 4, modeZeroPageY, buildOpStore(regX)},
0x8E: opcode{"STX", 3, 4, modeAbsolute, buildOpStore(regX)},
0x84: opcode{"STY", 2, 3, modeZeroPage, buildOpStore(regY)},
0x94: opcode{"STY", 2, 4, modeZeroPageX, buildOpStore(regY)},
0x8C: opcode{"STY", 3, 4, modeAbsolute, buildOpStore(regY)},
0x90: opcode{"BCC", 2, 2, modeRelative, buildOpBranch(flagC, false)}, // Extra cycles
0xB0: opcode{"BCS", 2, 2, modeRelative, buildOpBranch(flagC, true)}, // Extra cycles
0xD0: opcode{"BNE", 2, 2, modeRelative, buildOpBranch(flagZ, false)}, // Extra cycles
0xF0: opcode{"BEQ", 2, 2, modeRelative, buildOpBranch(flagZ, true)}, // Extra cycles
0x10: opcode{"BPL", 2, 2, modeRelative, buildOpBranch(flagN, false)}, // Extra cycles
0x30: opcode{"BMI", 2, 2, modeRelative, buildOpBranch(flagN, true)}, // Extra cycles
0x50: opcode{"BVC", 2, 2, modeRelative, buildOpBranch(flagV, false)}, // Extra cycles
0x70: opcode{"BVS", 2, 2, modeRelative, buildOpBranch(flagV, true)}, // Extra cycles
0xEA: opcode{"NOP", 1, 2, modeImplicit, opNOP},
// Undocumented opcodes, see http://bbc.nvg.org/doc/6502OpList.txt
0x1A: opcode{"NOP", 1, 2, modeImplicit, opNOP}, // INC A in the 65c02
0xC2: opcode{"NOP", 1, 2, modeImplicit, opNOP}, // Should be HALT?
0x02: opcode{"NOP", 1, 2, modeImplicit, opNOP}, // Should be HALT?
}

View File

@ -1,38 +0,0 @@
package core6502
import (
"fmt"
"testing"
)
func TestNMOS6502(t *testing.T) {
m := new(FlatMemory)
s := NewNMOS6502(m)
m.loadBinary("testdata/6502_functional_test.bin")
executeSuite(t, s, 0x200, 240, false, 255)
}
// To execute test suites from https://github.com/Klaus2m5/6502_65C02_functional_tests
func executeSuite(t *testing.T, s *State, stepAddress uint16, steps uint8, showStep bool, traceCPUStep uint8) {
s.reg.setPC(0x0400)
currentStep := uint8(255)
for true {
testCase := s.mem.Peek(stepAddress)
if testCase != currentStep {
currentStep = testCase
if showStep {
fmt.Printf("[ Step %d ]\n", testCase)
}
s.SetTrace(testCase == traceCPUStep)
}
if testCase >= steps {
break
}
pc := s.reg.getPC()
s.ExecuteInstruction()
if pc == s.reg.getPC() {
t.Fatalf("Failure in test %v.", testCase)
}
}
}

View File

@ -1,313 +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 := resolveValue(s, line, opcode)
if opcode.addressMode == modeAbsoluteX || opcode.addressMode == modeAbsoluteY {
// Double read, needed to pass A2Audit for the Language Card
value = resolveValue(s, line, opcode)
}
if inc {
value++
} else {
value--
}
s.reg.updateFlagZN(value)
resolveSetValue(s, line, opcode, value)
}
}
func buildOpShift(isLeft bool, isRotate bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := resolveValue(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)
resolveSetValue(s, line, opcode, 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) {
value := s.reg.getRegister(regSrc)
resolveSetValue(s, line, opcode, value)
}
}
func buildOpUpdateFlag(flag uint8, value bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
s.reg.updateFlag(flag, value)
}
}
func buildOpBranch(flag uint8, test bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
if s.reg.getFlag(flag) == test {
address := resolveAddress(s, line, opcode)
s.reg.setPC(address)
}
}
}
func buildOpBranchOnBit(bit uint8, test bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
// Note that those operations have two addressing modes:
// one for the zero page value, another for the relative jump.
// We will have to resolve the first one here.
value := s.mem.Peek(uint16(line[1]))
bitValue := ((value >> bit) & 1) == 1
if bitValue == test {
address := resolveAddress(s, line, opcode)
s.reg.setPC(address)
}
}
}
func buildOpSetBit(bit uint8, set bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
if set {
value = value | (1 << bit)
} else {
value = value &^ (1 << bit)
}
resolveSetValue(s, line, opcode, value)
}
}
func opBIT(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
acc := s.reg.getA()
s.reg.updateFlag(flagZ, value&acc == 0)
// The immediate addressing mode (65C02 or 65816 only) does not affect N & V.
if opcode.addressMode != modeImmediate {
s.reg.updateFlag(flagN, value&(1<<7) != 0)
s.reg.updateFlag(flagV, value&(1<<6) != 0)
}
}
func opTRB(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
a := s.reg.getA()
s.reg.updateFlag(flagZ, (value&a) == 0)
resolveSetValue(s, line, opcode, value&^a)
}
func opTSB(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
a := s.reg.getA()
s.reg.updateFlag(flagZ, (value&a) == 0)
resolveSetValue(s, line, opcode, value|a)
}
func buildOpCompare(reg int) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
reference := s.reg.getRegister(reg)
s.reg.updateFlagZN(reference - value)
s.reg.updateFlag(flagC, reference >= value)
}
}
func operationAnd(a uint8, b uint8) uint8 { return a & b }
func operationOr(a uint8, b uint8) uint8 { return a | b }
func operationXor(a uint8, b uint8) uint8 { return a ^ b }
func buildOpLogic(operation func(uint8, uint8) uint8) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
result := operation(value, s.reg.getA())
s.reg.setA(result)
s.reg.updateFlagZN(result)
}
}
func opADC(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
aValue := s.reg.getA()
carry := s.reg.getFlagBit(flagC)
total := uint16(aValue) + uint16(value) + uint16(carry)
signedTotal := int16(int8(aValue)) + int16(int8(value)) + int16(carry)
truncated := uint8(total)
if s.reg.getFlag(flagD) {
totalBcdLo := int(aValue&0x0f) + int(value&0x0f) + int(carry)
totalBcdHi := int(aValue>>4) + int(value>>4)
if totalBcdLo >= 10 {
totalBcdHi++
}
totalBcd := (totalBcdHi%10)<<4 + (totalBcdLo % 10)
s.reg.setA(uint8(totalBcd))
s.reg.updateFlag(flagC, totalBcdHi > 9)
} else {
s.reg.setA(truncated)
s.reg.updateFlag(flagC, total > 0xFF)
}
// ZNV flags behave for BCD as if the operation was binary?
s.reg.updateFlagZN(truncated)
s.reg.updateFlag(flagV, signedTotal < -128 || signedTotal > 127)
}
func opADCAlt(s *State, line []uint8, opcode opcode) {
opADC(s, line, opcode)
// The Z and N flags on BCD are fixed in 65c02.
s.reg.updateFlagZN(s.reg.getA())
}
func opSBC(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
aValue := s.reg.getA()
carry := s.reg.getFlagBit(flagC)
total := 0x100 + uint16(aValue) - uint16(value) + uint16(carry) - 1
signedTotal := int16(int8(aValue)) - int16(int8(value)) + int16(carry) - 1
truncated := uint8(total)
if s.reg.getFlag(flagD) {
totalBcdLo := 10 + int(aValue&0x0f) - int(value&0x0f) + int(carry) - 1
totalBcdHi := 10 + int(aValue>>4) - int(value>>4)
if totalBcdLo < 10 {
totalBcdHi--
}
totalBcd := (totalBcdHi%10)<<4 + (totalBcdLo % 10)
s.reg.setA(uint8(totalBcd))
s.reg.updateFlag(flagC, totalBcdHi >= 10)
} else {
s.reg.setA(truncated)
s.reg.updateFlag(flagC, total > 0xFF)
}
// ZNV flags behave for SBC as if the operation was binary
s.reg.updateFlagZN(truncated)
s.reg.updateFlag(flagV, signedTotal < -128 || signedTotal > 127)
}
func opSBCAlt(s *State, line []uint8, opcode opcode) {
opSBC(s, line, opcode)
// The Z and N flags on BCD are fixed in 65c02.
s.reg.updateFlagZN(s.reg.getA())
}
const stackAddress uint16 = 0x0100
func pushByte(s *State, value uint8) {
adresss := stackAddress + uint16(s.reg.getSP())
s.mem.Poke(adresss, value)
s.reg.setSP(s.reg.getSP() - 1)
}
func pullByte(s *State) uint8 {
s.reg.setSP(s.reg.getSP() + 1)
adresss := stackAddress + uint16(s.reg.getSP())
return s.mem.Peek(adresss)
}
func pushWord(s *State, value uint16) {
pushByte(s, uint8(value>>8))
pushByte(s, uint8(value))
}
func pullWord(s *State) uint16 {
return uint16(pullByte(s)) +
(uint16(pullByte(s)) << 8)
}
func buildOpPull(regDst int) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := pullByte(s)
s.reg.setRegister(regDst, value)
if regDst != regP {
s.reg.updateFlagZN(value)
}
}
}
func buildOpPush(regSrc int) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := s.reg.getRegister(regSrc)
if regSrc == regP {
value |= flagB + flag5
}
pushByte(s, value)
}
}
func opJMP(s *State, line []uint8, opcode opcode) {
address := resolveAddress(s, line, opcode)
s.reg.setPC(address)
}
func opNOP(s *State, line []uint8, opcode opcode) {}
func opJSR(s *State, line []uint8, opcode opcode) {
pushWord(s, s.reg.getPC()-1)
address := resolveAddress(s, line, opcode)
s.reg.setPC(address)
}
func opRTI(s *State, line []uint8, opcode opcode) {
s.reg.setP(pullByte(s))
s.reg.setPC(pullWord(s))
}
func opRTS(s *State, line []uint8, opcode opcode) {
s.reg.setPC(pullWord(s) + 1)
}
func opBRK(s *State, line []uint8, opcode opcode) {
pushWord(s, s.reg.getPC()+1)
pushByte(s, s.reg.getP()|(flagB+flag5))
s.reg.setFlag(flagI)
s.reg.setPC(getWord(s.mem, vectorBreak))
}
func opBRKAlt(s *State, line []uint8, opcode opcode) {
opBRK(s, line, opcode)
/*
The only difference in the BRK instruction on the 65C02 and the 6502
is that the 65C02 clears the D (decimal) flag on the 65C02, whereas
the D flag is not affected on the 6502.
*/
s.reg.clearFlag(flagD)
}
func opSTZ(s *State, line []uint8, opcode opcode) {
resolveSetValue(s, line, opcode, 0)
}

View File

@ -1,93 +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 {
ch := (r.getA() & 0x3F) + 0x40
return fmt.Sprintf("A: %#02x(%v), X: %#02x, Y: %#02x, SP: %#02x, PC: %#04x, P: %#02x, (NV-BDIZC): %08b",
r.getA(), string(ch), r.getX(), r.getY(), r.getSP(), r.getPC(), r.getP(), r.getP())
}

View File

@ -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")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +0,0 @@
package izapple2
import (
"errors"
)
type diskette interface {
powerOn(cycle uint64)
powerOff(cycle uint64)
read(quarterTrack int, cycle uint64) uint8
write(quarterTrack int, value uint8, cycle uint64)
}
func isDiskette(filename string) bool {
data, err := loadResource(filename)
if err != nil {
return false
}
return isFileNib(data) || isFileDsk(data) || isFileWoz(data)
}
func loadDiskette(filename string) (diskette, error) {
data, err := loadResource(filename)
if err != nil {
return nil, err
}
if isFileNib(data) {
var d diskette16sector
d.nib = newFileNib(data)
return &d, nil
}
if isFileDsk(data) {
var d diskette16sectorWritable
d.nib = newFileDsk(data, filename)
return &d, nil
}
if isFileWoz(data) {
f, err := newFileWoz(data)
if err != nil {
return nil, err
}
return newDisquetteWoz(f)
}
return nil, errors.New("Diskette format not supported")
}

56
doc/update_readme.go Normal file
View 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
View 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

View File

@ -1,4 +1,4 @@
FROM golang:1.15.2
FROM golang:1.18.0
LABEL MAINTAINER="Ivan Izaguirre <ivanizag@gmail.com>"

View File

@ -1,5 +1,5 @@
#!/bin/bash
cd "$( dirname $0)"
docker build . -t apple2builder
docker build . -t apple2builder --platform linux/amd64
mkdir -p ${PWD}/build
docker run --rm -it -v ${PWD}/build:/build apple2builder

40
dockerbuild/build_all.sh Executable file
View 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

View File

@ -3,28 +3,47 @@ cd /tmp
git clone https://github.com/ivanizag/izapple2
# Build izapple2console for Linux
cd /tmp/izapple2/izapple2console
go build .
chown --reference /build izapple2console
cp izapple2console /build
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
cd /tmp/izapple2/izapple2console
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -o izapple2console.exe .
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
cp izapple2console.exe /build/izapple2console_windows_amd64.exe
# Build izapple2sdl for Linux
cd /tmp/izapple2/izapple2sdl
go build .
chown --reference /build izapple2sdl
cp izapple2sdl /build
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
cd /tmp/izapple2/izapple2sdl
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib -lSDL2" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -o izapple2sdl.exe .
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
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
View 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
View 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)
})
}
}
}

View 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
}

View 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
View 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
View 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

View 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
}

View 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
}

View 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)
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View 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

View 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

View 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

View 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

View 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

View 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="M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z" /></svg>

After

Width:  |  Height:  |  Size: 416 B

View 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="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M14,8V16H18V8H14Z" /></svg>

After

Width:  |  Height:  |  Size: 386 B

View 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.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z" /></svg>

After

Width:  |  Height:  |  Size: 851 B

View 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="M14,19H18V5H14M6,19H10V5H6V19Z" /></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -0,0 +1,21 @@
# Resources
## Sources
- From https://materialdesignicons.com/icon{icon-name}:
- restart
- fast-forward
- pause
- palette
- layers-triple
- format-font
- camera
- page-layout-sidebar-right
- https://www.iconfinder.com/icons/281710/rainbow_apple_icon
## Building resources.go
```
~/go/bin/fyne bundle resources > resources.go
```

Some files were not shown because too many files have changed in this diff Show More