Compare commits

...

327 Commits

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
Ivan Izaguirre 4fca7dc828 Update snapshots 2020-10-04 19:41:07 +02:00
Ivan Izaguirre 442daca829 Fix mix mode 40 columns text 2020-10-04 19:21:49 +02:00
Ivan Izaguirre fddc9e2f6d Remove trace 2020-10-04 16:59:00 +02:00
Ivan Izaguirre 3c59e9f3e4 Respect a bit more the luminance in NTSC 2020-10-04 16:10:34 +02:00
Ivan Izaguirre b0a1282b0a Fix Mix mode 2020-10-04 16:09:59 +02:00
Ivan Izaguirre acf47f3fc0 Add SDL2 runtime for windows 2020-10-04 15:45:23 +02:00
Ivan Izaguirre fdd17624ab Use go-sdl 4.0 2020-10-04 14:14:44 +02:00
Ivan Izaguirre 1cd2939dca Use go-sdl 4.0 2020-10-04 14:09:09 +02:00
Ivan Izaguirre ce0ea6c68f Fix docker build 2020-10-04 00:05:45 +02:00
Iván Izaguirre b061b12ad8
Create go.yml 2020-10-03 23:44:34 +02:00
Iván Izaguirre 41f39f7b54
Create codeql-analysis.yml 2020-10-03 23:43:31 +02:00
Ivan Izaguirre 66e94574a0 Let's give this thing a name 2020-10-03 23:38:26 +02:00
Ivan Izaguirre e07754d1d0 File format autodetection 2020-10-03 23:00:04 +02:00
Ivan Izaguirre 1d014b8a4f Passes a2audit 1.06 2020-09-23 18:31:43 +02:00
Ivan Izaguirre 3f7c0bdf57 Implement the INTC8ROM softswitch 2020-09-23 18:23:57 +02:00
Ivan Izaguirre 9ce9d1f96d Fix a2audit regression on aux mem 2020-09-23 18:21:45 +02:00
Ivan Izaguirre 8ebd745a63 Name all the memory segments 2020-09-23 18:19:15 +02:00
Ivan Izaguirre 634a079857 Fix softswitch name 2020-09-23 18:14:45 +02:00
Ivan Izaguirre 38f3a1e53c RGB video modes available only with RGB card 2020-09-23 18:12:28 +02:00
Ivan Izaguirre cd9c4e2b41 Double read on INC to pass a2audit tests 2020-09-23 18:11:47 +02:00
Ivan Izaguirre 696038fa30 Reset LC activation count on writes. To pass a2audit tests 2020-09-23 18:10:46 +02:00
Ivan Izaguirre 10d2551b36 Don't panic 2020-09-23 18:09:18 +02:00
Ivan Izaguirre 8e109720e6 RGB video modes available only with RGB card 2020-09-23 18:08:19 +02:00
Ivan Izaguirre 8f2b12d6ef Use the mouse as joystick if none available 2020-09-04 20:25:03 +02:00
Ivan Izaguirre fe72071245 Support no slot clock installed on any card ROM 2020-09-01 17:46:30 +02:00
Ivan Izaguirre ccd100677e Trace softswitch registrations 2020-08-30 21:11:43 +02:00
Ivan Izaguirre 3a3c350748 Fix II+ with no DiskII card 2020-08-30 18:59:00 +02:00
Ivan Izaguirre 20808d78c8 Fix signature in test 2020-08-29 21:53:02 +02:00
Ivan Izaguirre 8cfd448f95 Support write on dsk files 2020-08-29 21:48:09 +02:00
Ivan Izaguirre 9935510445 NIB to DSK 2020-08-29 18:45:51 +02:00
Ivan Izaguirre 06704ed1e3 Fix tests with the new PeekCode() in Memory 2020-08-16 15:41:21 +02:00
Ivan Izaguirre 0c7e4b7db2 No Slot Clock support, enable with -nsc 2020-08-16 15:30:44 +02:00
Ivan Izaguirre c82e57d895 ROM memory changes and some speed improvements 2020-08-14 17:19:24 +02:00
Ivan Izaguirre 81c77c7700 CPU execution with bursts 2020-08-13 20:34:37 +02:00
Ivan Izaguirre a3f69bc01d Delete partial save state implementation 2020-08-13 16:50:48 +02:00
Ivan Izaguirre 20c9a842d5 Fix serpentine hang waiting for noise on the cassette port 2020-08-13 16:43:40 +02:00
Ivan Izaguirre 53842b2b4e Set the name of the 65c816 XCE opcode to detect it on traces 2020-08-13 16:41:55 +02:00
Ivan Izaguirre 79a584fb95 Move slots and Status SmartPort call 2020-08-12 22:50:40 +02:00
Ivan Izaguirre 986172516f Use diskette instead of disquette consistently 2020-08-11 23:56:08 +02:00
Ivan Izaguirre c2b620ec01 Support 3.5 disks and HDV format. Generalize the hard disk support to be used for any SmartPort device. 2020-08-11 23:53:05 +02:00
Ivan Izaguirre caa312c12a Fix text mix 2020-08-11 23:13:12 +02:00
Ivan Izaguirre 65f013e9b3 RAMWorks card support for up to 16MB RAM 2020-08-10 20:52:17 +02:00
Ivan Izaguirre ede3b65257 Video7, mix with RGB color text 40 2020-08-09 17:42:47 +02:00
Ivan Izaguirre d1bf13ce7f Fix typo 2020-08-09 17:06:02 +02:00
Ivan Izaguirre 62d0a35b3e Support for video7 RGB 160*192 video mode 2020-08-09 16:42:16 +02:00
Ivan Izaguirre d47b0a5e28 reverse ntsc paterns to align the colormaps 2020-08-09 15:39:41 +02:00
Ivan Izaguirre 0ef160a56e RGB card 40 columns text with 16 colors video mode 2020-08-08 19:23:35 +02:00
Ivan Izaguirre aa198877ec Screen video modes refactor. Video debug mode 2020-08-08 13:44:45 +02:00
Ivan Izaguirre caabdf4781 Fix markdown warnings 2020-08-08 13:40:48 +02:00
Ivan Izaguirre 6bfa1d2986 Support RGB mode 14. Mix if mono with no NTSC artifacts and color 2020-08-06 18:35:34 +02:00
Ivan Izaguirre dad2a67321 Ctrl-F6: Show a split screen with the views for NTSC color TV, monochrome monitor, HGR page 1 and HGR page 2 2020-07-31 09:46:53 +02:00
Ivan Izaguirre b375869923 Fix CircleCI image setup 2020-06-07 18:59:25 +02:00
Ivan Izaguirre 636895a7e4 Trace ProDOS MLI calls 2020-06-07 18:23:39 +02:00
Ivan Izaguirre 48afaa471b Support PO ProDOS ordered disk files 2020-06-06 14:10:27 +02:00
Rob Greene 6e25111224 Adding '-diskb' to README. 2020-04-17 22:59:50 +02:00
Rob Greene f4cc6521cb Allowing two disks to be specified. 2020-04-17 22:59:50 +02:00
Ivan Izaguirre f6c99d93a8 Fixed double lo res. Bejeweled works 2020-04-05 17:02:11 +02:00
Ivan Izaguirre 1406012c2a LC mem fix. Fixes IIe self check and The Games: Summer Edition 2020-04-05 17:01:04 +02:00
Ivan Izaguirre 810c1a7d89 LC mem fix. Fixes IIe self check and The Games: Summer Edition 2020-04-04 20:15:32 +02:00
Ivan Izaguirre b9dbcc43a8 Improve trace 2020-04-04 20:13:53 +02:00
Ivan Izaguirre 38a187ee5b Opcode dump with the proper size 2020-04-02 23:52:45 +02:00
Iván Izaguirre fb29ae8ee7
Add PAUSE in doc 2020-04-02 23:05:21 +02:00
Ivan Izaguirre b1d7fe6571 Fastchip alternate mode 2020-04-02 21:50:01 +02:00
Rob Greene 01099a8ce3 Placing screen rendering on pause as well. 2020-03-15 18:02:42 +01:00
Rob Greene da7bb3ebf7 Adding pause capability. 2020-03-15 18:02:42 +01:00
Ivan Izaguirre e57a4b21e6 Unused softswitches on the Slinky return 255 2020-03-09 23:22:21 +01:00
Iván Izaguirre 54f8e48521
Update defaults 2020-03-08 23:46:16 +01:00
Iván Izaguirre b8367a3804
Update default machine 2020-03-08 23:42:39 +01:00
Ivan Izaguirre 35b8bb032b Apple II Memory Expansion Card with up to 1024KB 2020-03-08 23:39:25 +01:00
Ivan Izaguirre 4d3d199d33 Total Replay v3 2020-01-21 23:42:44 +01:00
Ivan Izaguirre 9b4ab62b08 Smartport support on hard drives 2020-01-12 17:14:20 +01:00
Ivan Izaguirre 4c227f3399 Non-blocking speaker clicks even when the queue is full 2020-01-11 20:29:12 +01:00
Ivan Izaguirre 08edc95834 Fix regression on the command line parsing 2020-01-11 17:23:34 +01:00
Ivan Izaguirre c7cc5c15b4 Avoid memory allocations, reuse an array from the state 2020-01-11 17:17:39 +01:00
Ivan Izaguirre 6387e93f06 Much faster resolution avoiding a single getSet with callbacks. 2020-01-11 17:15:42 +01:00
Ivan Izaguirre b2b009037e Better profiling 2020-01-11 17:13:29 +01:00
Ivan Izaguirre 209191af72 Use golang 1.12 docker official image for builds 2019-12-27 15:31:34 +01:00
Ivan Izaguirre aa46cc48e5 Use docker to build 2019-12-26 23:11:32 +01:00
Ivan Izaguirre 22b15bf89b Add SDL2 build dependency in circle CI 2019-12-22 17:11:50 +01:00
Ivan Izaguirre 1f1b092663 Use go 1.12 in circleci 2019-12-22 16:55:40 +01:00
Ivan Izaguirre 8673ea3b37 Use go 1.12 modules 2019-12-22 16:41:15 +01:00
Ivan Izaguirre 5966be84a5 WOZ format support documentation 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 5847a0f8a5 Autodetect diskette file format 2019-12-22 14:37:19 +01:00
Ivan Izaguirre b9f893f96b Support fake bits in WOZ 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 53e7a07a46 List working status for the referecen woz files 2019-12-22 14:37:19 +01:00
Ivan Izaguirre bd44cbbd07 Discard unsupported 3.5 and 13 sector woz disks 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 88ade57b1b Fix fast disk mode 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 9e2fd7e824 Support WOZ files read only 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 5523c0429a Parse WOZ files 2019-12-22 14:37:19 +01:00
Ivan Izaguirre e14b26f876 Time based disk reads 2019-12-22 14:37:19 +01:00
Ivan Izaguirre a27ab17766 Use the diskette abstraction for no cycle dependant DSK, NIB disks 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 9ad13ae483 Better comments on the stepper motor code 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 212ed2a90a Use quarter tracks instead of half tracks 2019-12-22 14:37:19 +01:00
Ivan Izaguirre 985b3d6a7a Incomplete Smartport implementation. Not really needed for Total Replay 2019-11-23 19:07:12 +01:00
Ivan Izaguirre d45b39b180 Do not reset cycle counter on cpu reset 2019-11-23 18:29:56 +01:00
Ivan Izaguirre e833d71fad Enable vidHD only on 128kb models 2019-11-22 23:27:25 +01:00
Ivan Izaguirre 0a1751cc77 Fix width of Double high res 2019-11-22 23:25:37 +01:00
Ivan Izaguirre 5b8409413c Map the RESET button to CTRL-F1 2019-11-21 00:13:53 +01:00
Ivan Izaguirre 202f7b36ce Reduce the sound delay 2019-11-20 23:57:24 +01:00
Ivan Izaguirre fd13326367 Properly emulate no joysticks available 2019-11-20 23:28:51 +01:00
Ivan Izaguirre 74e0cc4768 Properly emulate no joysticks available 2019-11-20 23:27:11 +01:00
Ivan Izaguirre ea5ee95807 Fix mmu for double hires failing on Tetris 2019-11-20 22:43:37 +01:00
Ivan Izaguirre 20d9b2ed10 Partial FASTChip emulation. 2019-11-12 23:47:48 +01:00
Ivan Izaguirre c91e9d43f4 Fix the SHR pallette 2019-11-11 23:53:50 +01:00
Ivan Izaguirre 4f466826b9 Update README.md 2019-11-11 23:53:50 +01:00
Ivan Izaguirre 37e6f3024f SuperHiRes video mode 2019-11-11 23:53:50 +01:00
Ivan Izaguirre fce719deb8 VidHD card signature. Total Replay thinks it's there 2019-11-11 23:53:50 +01:00
Ivan Izaguirre 4bf8531ae4 Support double width high resolution 2019-11-09 18:13:19 +01:00
Ivan Izaguirre a8f22224a8 Support double width low resolution 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 8ea3abe2ff Extra Apple //e keys 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 3dbb90d0ca 80 columns support 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 4d6b02e4d6 Show current speed in Mhz with Ctrl-F5 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 862b087d4e Unenhanced //e does not have a 65c02 2019-11-09 18:13:19 +01:00
Ivan Izaguirre a28745ab83 Fix shadow slot 3 ROM in Apple //e 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 09117fd7c5 More Apple //e softswitches 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 3660e0ae98 Memory manager rewritten. Logic calculated on every access. 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 3618cbb9c9 Trace HD commands 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 9f2a026fa3 Info con Smartport Disk 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 5a3dcc60bb Adding unenhanced Apple IIe 2019-11-09 18:13:19 +01:00
Ivan Izaguirre d5433ad8d9 Nanmed sotfswitches for easier traces. 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 61dae23726 Language cards, disable write on softswitches writes. 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 3f7e3b3013 Apple IIe char gen ROM management 2019-11-09 18:13:19 +01:00
Ivan Izaguirre eeebf1bb0f Initial Apple 2e enhanced support 2019-11-09 18:13:19 +01:00
Ivan Izaguirre 1d0a3e0b34 Simulate joystick buttons with Alt and AltGr 2019-10-20 00:19:00 +02:00
Ivan Izaguirre 472f8a5c51 Remove Lint warning 2019-10-19 21:01:09 +02:00
Ivan Izaguirre f511a9d0e0 Decay beeper level to avoid clicks on startup and shutdown 2019-10-19 20:33:50 +02:00
Ivan Izaguirre b89f88d879 Add the -profile option 2019-10-19 20:31:18 +02:00
Ivan Izaguirre 6917a21f38 Add the -profile option 2019-10-19 19:56:51 +02:00
Ivan Izaguirre c65d8585d4 Fix performance regression. Avoid array copy. 2019-10-19 18:41:10 +02:00
Ivan Izaguirre df41114043 Emulation of the 65c02 processor variant. 2019-10-11 00:06:42 +02:00
Ivan Izaguirre f92bc2b08e Proper Go error management 2019-10-06 01:26:00 +02:00
Ivan Izaguirre b1b55f4c0b Write support for ProDOS hard disks. 2019-10-05 15:30:13 +02:00
Ivan Izaguirre cb844611d9 Change default slot of ThunderClock to 4. 2019-10-05 15:26:24 +02:00
Ivan Izaguirre 37005b16e9 Read only ProDOS hard disk support. 2019-10-02 23:39:39 +02:00
Ivan Izaguirre de8ee829ee Ignore not supported calls to the ThunderClock+ card. 2019-10-02 23:03:00 +02:00
Ivan Izaguirre c10a6f7e8a Press F11 to toggle CPU execution trace to standard output 2019-10-02 23:00:02 +02:00
Ivan Izaguirre e14577d501 Notes on how to implement prodos hard disk support. 2019-09-28 19:17:47 +02:00
Ivan Izaguirre b930cf5fd6 Ignore not implemented set time. 2019-09-28 18:58:28 +02:00
Ivan Izaguirre 00e4476e86 Support the ThunderClock Plus card. Partial mulation of the microPD1990AC integrated circuit. 2019-09-28 13:37:42 +02:00
Ivan Izaguirre 3d21387d47 Fix language card. ProDos 2.4.2 boots now. 2019-09-24 23:43:58 +02:00
Ivan Izaguirre 68b949d871 Added the -traceCpu and -traceSS command line switches. 2019-09-24 23:32:03 +02:00
Ivan Izaguirre 43a5980fd3 Added the traceCpu command line switch 2019-09-23 23:35:39 +02:00
Ivan Izaguirre 4d7447f7b4 Support for joysticks. 2019-08-06 00:38:36 +02:00
Ivan Izaguirre 3d75002588 Support for joysticks 2019-08-06 00:37:27 +02:00
Ivan Izaguirre 67a92895b3 Use non-compressed DSK images with http links. Valid also with ROMs 2019-07-07 17:40:29 +02:00
Ivan Izaguirre f53b849a52 How to cross compile for Windows 2019-06-15 14:02:22 +02:00
Ivan Izaguirre 018ff935e6 How to cross compile for Windows 2019-06-15 14:01:10 +02:00
Iván Izaguirre 7f5bf6b47f
Add CircleCI badge 2019-06-11 00:13:56 +02:00
Ivan Izaguirre 049e80acd7 Use CircleCI 2019-06-11 00:04:23 +02:00
Ivan Izaguirre 047233f16b Support for character generator pages. F10 to change page. 2019-06-09 23:54:27 +02:00
Ivan Izaguirre 68d68eadfc Initial support for the Base64A clone. Support for ROM banks 2019-06-09 17:36:29 +02:00
Ivan Izaguirre 4b655b40b8 Base64A roms 2019-06-09 17:35:32 +02:00
Ivan Izaguirre d079ec3d2b Refactor ROM loading. Preparation for alt roms. 2019-06-07 20:01:20 +02:00
Ivan Izaguirre b847cd34e6 Fix Saturn card to pass Saturn test 2019-06-07 17:42:18 +02:00
Ivan Izaguirre ded7c10ed3 Fix texts 2019-06-02 23:18:35 +02:00
Ivan Izaguirre 3d040114dc Readme file 2019-06-02 23:00:31 +02:00
Ivan Izaguirre fc9e4011f9 Press F12 to save a screen capture 2019-06-02 22:59:51 +02:00
322 changed files with 14962 additions and 18181 deletions

11
.circleci/config.yml Normal file
View File

@ -0,0 +1,11 @@
version: 2
jobs:
build:
docker:
- image: cimg/go:1.18
steps:
- checkout
- run: sudo apt update
- run: sudo apt install libsdl2-dev
- run: go get -d ./...
- run: go test ./...

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 2 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

42
.github/workflows/go.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Install SDL2
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Build
run: go build -v .
- name: Test
run: go test -v ./...

18
.gitignore vendored Normal file
View File

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

312
README.md Normal file
View File

@ -0,0 +1,312 @@
# izapple2 - Apple ][+, //e emulator
Portable emulator of an Apple II+ or //e. Written in Go.
## Features
- Models:
- Apple ][+ with 48Kb of base RAM
- Apple //e with 128Kb of RAM
- Apple //e enhanced with 128Kb of RAM
- Base64A clone with 48Kb of base RAM and paged ROM
- Storage
- 16 Sector 5 1/4 diskettes. Uncompressed or compressed witth gzip or zip. Supported formats:
- NIB (read only)
- DSK
- PO
- [WOZ 1.0 or 2.0](storage/WozSupportStatus.md) (read only)
- 3.5 disks in PO or 2MG format
- Hard disk in HDV or 2MG format with ProDOS and SmartPort support
- Emulated extension cards:
- DiskII controller (state machine based for WOZ files)
- 16Kb Language Card
- 256Kb Saturn RAM
- Parallel Printer Interface card
- 1Mb Memory Expansion Card (slinky)
- RAMWorks style expansion Card (up to 16MB additional) (Apple //e only)
- ThunderClock Plus real time clock
- Apple //e 80 columns card with 64Kb extra RAM and optional RGB modes
- No Slot Clock based on the DS1216
- Videx Videoterm 80 column card with the Videx Soft Video Switch (Apple ][+ only)
- SwyftCard (Apple //e only)
- Brain Board
- Brain Board II
- MultiROM card
- Dan ][ Controller card
- ProDOS ROM card
- Useful cards not emulating a real card
- Bootable SmartPort / ProDOS card with the following smartport devices:
- Block device (hard disks)
- Fujinet network device (supports only http(s) with GET and JSON)
- Fujinet clock (not in Fujinet upstream)
- VidHd, limited to the ROM signature and SHR as used by Total Replay, only for //e models with 128Kb
- FASTChip, limited to what Total Replay needs to set and clear fast mode
- Mouse Card, emulates the entry points, not the softswitches.
- Host console card. Maps the host STDIN and STDOUT to PR# and IN#
- ROMXe, limited to font switching
- Graphic modes:
- Text 40 columns
- Text 80 columns (Apple //e and Videx VideoTerm)
- Low-Resolution graphics
- Double-Width Low-Resolution graphics (Apple //e only)
- High-Resolution graphics
- Double-Width High-Resolution graphics (Apple //e only)
- Super High Resolution (VidHD only)
- Mixed mode
- RGB card text 40 columns with 16 colors for foreground and background (mixable)
- RGB card mode 11, mono 560x192
- RGB card mode 12, ntsc 160*192
- RGB card mode 13, ntsc 140*192 (regular DHGR)
- RGB card mode 14, mix of modes 11 and 13 on the fly
- Displays:
- Green monochrome monitor with half width pixel support
- NTSC Color TV (extracting the phase from the mono signal)
- RGB for Super High Resolution and RGB card
- ANSI Console, avoiding the SDL2 dependency
- Debug mode: shows four panels with actual screen, page1, page2 and extra info dependant of the video mode
- Tracing capabilities:
- CPU execution disassembled
- Softswitch reads and writes
- ProDOS MLI calls
- Apple Pascal BIOS calls
- SmartPort commands
- BBC MOS calls when using [Applecorn](https://github.com/bobbimanners/)
- Other features:
- Sound
- Joystick support. Up to two joysticks or four paddles
- Mouse support. No mouse capture needed
- Adjustable speed
- Fast disk mode to set max speed while using the disks
- Single file executable with embedded ROMs and DOS 3.3
- Pause (thanks a2geek)
- Passes the [A2AUDIT 1.06](https://github.com/zellyn/a2audit) tests as II+, //e, and //e Enhanced.
- Partial pass ot the [ProcessorTests](https://github.com/TomHarte/ProcessorTests) for 6502 and 65c02. Failing test 6502/v1/20_55_13; flags N anv V issues with ADC; and missing some undocumented 6502 opcodes.
By default the following configuration is launched:
- Enhanced Apple //e with 65c02 processor
- RAMWorks card with 80 column, RGB (with Video7 modes) and 8Gb RAM in aux slot
- Parallel print inteface in slot 1
- VidHD card (SHR support) in slot 2
- FASTChip Accelerator card in slot 3
- Mouse card in slot 4
- SmartPort card with 1 device in slot 5 (if an image is provided with -disk35)
- DiskII controller card in slot 6
- SmartPort card with 1 device in slot 7 (if an image is provided with -hd)
## Running the emulator
No installation required. [Download](https://github.com/ivanizag/izapple2/releases) the single file executable `izapple2xxx_xxx` for linux or Mac, SDL2 graphics or console. Build from source to get the latest features.
### Default mode
Execute without parameters to have an emulated Apple //e Enhanced with 128kb booting DOS 3.3 ready to run Applesoft:
``` terminal
casa@servidor:~$ ./izapple2sdl
```
![DOS 3.3 started](doc/dos33.png)
### Play games
Download a DSK or WOZ file or use an URL ([Asimov](https://www.apple.asimov.net/images/) is an excellent source):
``` terminal
casa@servidor:~$ ./izapple2sdl "https://www.apple.asimov.net/images/games/action/karateka/karateka (includes intro).dsk"
```
![Karateka](doc/karateka.png)
### Play the Total Replay collection
Download the excellent [Total Replay](https://archive.org/details/TotalReplay) compilation by
[a2-4am](https://github.com/a2-4am/4cade):
``` terminal
casa@servidor:~$ ./izapple2sdl Total\ Replay\ v4.0.hdv
```
Displays super hi-res box art as seen with the VidHD card.
![Total Replay](doc/totalreplay.png)
### Terminal mode
To run text mode right on the terminal without the SDL2 dependency, use `izapple2console`. It runs on the console using ANSI escape codes. Input is sent to the emulated Apple II one line at a time:
``` terminal
casa@servidor:~$ ./izapple2console -model 2plus
############################################
# #
# APPLE II #
# #
# DOS VERSION 3.3 SYSTEM MASTER #
# #
# #
# JANUARY 1, 1983 #
# #
# #
# COPYRIGHT APPLE COMPUTER,INC. 1980,1982 #
# #
# #
# ]10 PRINT "HELLO WORLD" #
# #
# ]LIST #
# #
# 10 PRINT "HELLO WORLD" #
# #
# ]RUN #
# HELLO WORLD #
# #
# ]_ #
# #
# #
############################################
Line:
```
### Command line options
<!-- doc/usage.txt start -->
```terminal
Usage: izapple2 [file]
file
path to image to use on the boot device
-charrom string
rom file for the character generator (default "<internal>/Apple IIe Video Enhanced.bin")
-cpu string
cpu type, can be '6502' or '65c02' (default "65c02")
-forceCaps
force all letters to be uppercased (no need for caps lock!)
-model string
set base model (default "2enh")
-mods string
comma separated list of mods applied to the board, available mods are 'shift', 'four-colors
-nsc string
add a DS1216 No-Slot-Clock on the main ROM (use 'main') or a slot ROM (default "main")
-profile
generate profile trace to analyse with pprof
-ramworks string
memory to use with RAMWorks card, max is 16384 (default "8192")
-rgb
emulate the RGB modes of the 80col RGB card for DHGR
-rom string
main rom file (default "<internal>/Apple2e_Enhanced.rom")
-romx
emulate a RomX
-s0 string
slot 0 configuration. (default "language")
-s1 string
slot 1 configuration. (default "empty")
-s2 string
slot 2 configuration. (default "vidhd")
-s3 string
slot 3 configuration. (default "fastchip")
-s4 string
slot 4 configuration. (default "empty")
-s5 string
slot 5 configuration. (default "empty")
-s6 string
slot 6 configuration. (default "diskii,disk1=<internal>/dos33.dsk")
-s7 string
slot 7 configuration. (default "empty")
-speed string
cpu speed in Mhz, can be 'ntsc', 'pal', 'full' or a decimal nunmber (default "ntsc")
-trace string
trace CPU execution with one or more comma separated tracers (default "none")
The available pre-configured models are:
2: Apple ][
2e: Apple IIe
2enh: Apple //e
2plus: Apple ][+
base64a: Base 64A
swyft: swyft
The available cards are:
brainboard: Firmware card. It has two ROM banks
brainboard2: Firmware card. It has up to four ROM banks
dan2sd: Apple II Peripheral Card that Interfaces to a ATMEGA328P for SD card storage
diskii: Disk II interface card
diskiiseq: Disk II interface card emulating the Woz state machine
fastchip: Accelerator card for Apple IIe (limited support)
fujinet: SmartPort interface card hosting the Fujinet
inout: Card to test I/O
language: Language card with 16 extra KB for the Apple ][ and ][+
memexp: Memory expansion card
mouse: Mouse card implementation, does not emulate a real card, only the firmware behaviour
multirom: Multiple Image ROM card
parallel: Card to dump to a file what would be printed to a parallel printer
prodosromcard3: A bootable 4 MB ROM card by Ralle Palaveev
prodosromdrive: A bootable 1 MB solid state disk by Terence Boldt
saturn: RAM card with 128Kb, it's like 8 language cards
smartport: SmartPort interface card
softswitchlogger: Card to log softswitch accesses
swyftcard: Card with the ROM needed to run the Swyftcard word processing system
thunderclock: Clock card
videx: Videx compatible 80 columns card
vidhd: Firmware signature of the VidHD card to trick Total Replay to use the SHR mode
The available tracers are:
cpm65: Trace CPM65 BDOS calls
cpu: Trace CPU execution
mli: Trace ProDOS MLI calls
mos: Trace MOS calls with Applecorn skipping terminal IO
mosfull: Trace MOS calls with Applecorn
panicss: Panic on unimplemented softswitches
ss: Trace sotfswiches calls
ssreg: Trace sotfswiches registrations
ucsd: Trace UCSD system calls
```
<!-- doc/usage.txt end -->
## Building from source
### Linux
Besides having a working Go installation, install the SDL2 developer files. Run:
``` terminal
git clone github.com/ivanizag/izapple2
cd izapple2/frontend/a2sdl
go build .
```
### MacOS
With a working Go installation, run:
``` terminal
brew install SDL2
git clone github.com/ivanizag/izapple2
cd izapple2/frontend/a2sdl
go build .
```
### Windows
On Windows, CGO needs a gcc compiler. Install [mingw-w64](http://mingw-w64.org/doku.php/download/mingw-builds) and the [SDL2 developer files](https://www.libsdl.org/release/) for mingw-64.
Run:
``` terminal
git clone github.com/ivanizag/izapple2
cd izapple2\frontend\a2sdl
go build .
```
To run in Windows, copy the file `SDL2.dll` on the same folder as `a2sdl.exe`. The latest `SDL2.dll` can be found in the [Runtime binary for Windows 64-bit](https://www.libsdl.org/download-2.0.php).
### Use docker to cross compile for Linux and Windows
To create executables for Linux and Windows without installing Go, SDL2 or the Windows cross compilation toosl, run:
``` terminal
cd docker
./build.sh
```

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

255
apple2.go
View File

@ -1,196 +1,101 @@
package apple2
package izapple2
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"os"
"time"
"sync/atomic"
"github.com/ivanizag/apple2/core6502"
"github.com/ivanizag/iz6502"
)
// Apple2 represents all the components and state of the emulated machine
type Apple2 struct {
cpu *core6502.State
mmu *memoryManager
io *ioC0Page
cg *CharacterGenerator
cards [8]card
isApple2e bool
panicSS bool
commandChannel chan int
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
isColor bool
fastMode bool
fastRequestsCounter int
Name string
cpu *iz6502.State
mmu *memoryManager
io *ioC0Page
cg *CharacterGenerator
cards [8]Card
tracers []executionTracer
softVideoSwitch *SoftVideoSwitch
board string
isApple2e bool
isFourColors bool // An Apple II without the 6 color mod
commandChannel chan command
cycleDurationNs float64 // Current speed. Inverse of the cpu clock in Ghz
fastRequestsCounter int32
cycleBreakpoint uint64
breakPoint bool
profile bool
showSpeed bool
paused bool
forceCaps bool
removableMediaDrives []drive
}
const (
// CpuClockMhz is the actual Apple II clock speed
CpuClockMhz = 14.318 / 14
cpuClockEuroMhz = 14.238 / 14
)
const maxWaitDuration = 100 * time.Millisecond
// Run starts the Apple2 emulation
func (a *Apple2) Run(log bool) {
// Start the processor
a.cpu.Reset()
referenceTime := time.Now()
for {
// Run a 6502 step
a.cpu.ExecuteInstruction(log && a.cycleDurationNs != 0)
// Execute meta commands
commandsPending := true
for commandsPending {
select {
case command := <-a.commandChannel:
a.executeCommand(command)
default:
commandsPending = false
}
}
if a.cycleDurationNs != 0 && a.fastRequestsCounter <= 0 {
// Wait until next 6502 step has to run
clockDuration := time.Since(referenceTime)
simulatedDuration := time.Duration(float64(a.cpu.GetCycles()) * a.cycleDurationNs)
waitDuration := simulatedDuration - clockDuration
if waitDuration > maxWaitDuration || -waitDuration > maxWaitDuration {
// We have to wait too long or are too much behind. Let's fast forward
referenceTime = referenceTime.Add(-waitDuration)
waitDuration = 0
}
if waitDuration > 0 {
time.Sleep(waitDuration)
}
}
}
// GetCards returns the array of inserted cards
func (a *Apple2) GetCards() [8]Card {
return a.cards
}
const (
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
CommandToggleSpeed = iota + 1
// CommandToggleColor toggles between NTSC color TV and Green phospor monitor
CommandToggleColor
// CommandSaveState stores the state to file
CommandSaveState
// CommandLoadState reload the last state
CommandLoadState
// CommandDumpDebugInfo dumps usefull info
CommandDumpDebugInfo
)
// SendCommand enqueues a command to the emulator thread
func (a *Apple2) SendCommand(command int) {
a.commandChannel <- command
// SetKeyboardProvider attaches an external keyboard provider
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
a.io.setKeyboardProvider(kb)
}
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 CommandToggleColor:
a.isColor = !a.isColor
case CommandSaveState:
fmt.Println("Saving state")
a.save("apple2.state")
case CommandLoadState:
fmt.Println("Loading state")
a.load("apple2.state")
case CommandDumpDebugInfo:
a.dumpDebugInfo()
}
// SetSpeakerProvider attaches an external keyboard provider
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
a.io.setSpeakerProvider(s)
}
func (a *Apple2) requestFastMode() {
// SetJoysticksProvider attaches an external joysticks provider
func (a *Apple2) SetJoysticksProvider(j JoysticksProvider) {
a.io.setJoysticksProvider(j)
}
// SetMouseProvider attaches an external joysticks provider
func (a *Apple2) SetMouseProvider(m MouseProvider) {
a.io.setMouseProvider(m)
}
// IsPaused returns true when emulator is paused
func (a *Apple2) IsPaused() bool {
return a.paused
}
func (a *Apple2) GetCycles() uint64 {
return a.cpu.GetCycles()
}
// SetCycleBreakpoint sets a cycle number to pause the emulator. 0 to disable
func (a *Apple2) SetCycleBreakpoint(cycle uint64) {
a.cycleBreakpoint = cycle
a.breakPoint = false
}
func (a *Apple2) BreakPoint() bool {
return a.breakPoint
}
// IsProfiling returns true when profiling
func (a *Apple2) IsProfiling() bool {
return a.profile
}
// IsForceCaps returns true when all letters are forced to upper case
func (a *Apple2) IsForceCaps() bool {
return a.forceCaps
}
func (a *Apple2) RequestFastMode() {
// Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain.
if a.fastMode {
a.fastRequestsCounter++
}
atomic.AddInt32(&a.fastRequestsCounter, 1)
}
func (a *Apple2) releaseFastMode() {
if a.fastMode {
a.fastRequestsCounter--
}
func (a *Apple2) ReleaseFastMode() {
atomic.AddInt32(&a.fastRequestsCounter, -1)
}
type persistent interface {
save(io.Writer)
load(io.Reader)
}
func (a *Apple2) save(filename string) {
f, err := os.Create(filename)
if err != nil {
panic(err)
}
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
a.cpu.Save(w)
a.mmu.save(w)
a.io.save(w)
binary.Write(w, binary.BigEndian, a.isColor)
binary.Write(w, binary.BigEndian, a.fastMode)
binary.Write(w, binary.BigEndian, a.fastRequestsCounter)
for _, c := range a.cards {
if c != nil {
c.save(w)
}
}
}
func (a *Apple2) load(filename string) {
f, err := os.Open(filename)
if err != nil {
// Ignore error if can't load the file
return
}
defer f.Close()
r := bufio.NewReader(f)
a.cpu.Load(r)
a.mmu.load(r)
a.io.load(r)
binary.Read(r, binary.BigEndian, &a.isColor)
binary.Read(r, binary.BigEndian, &a.fastMode)
binary.Read(r, binary.BigEndian, &a.fastRequestsCounter)
for _, c := range a.cards {
if c != nil {
c.load(r)
}
}
}
func (a *Apple2) dumpDebugInfo() {
// See "Apple II Monitors Peeled"
pageZeroSymbols := map[int]string{
0x36: "CSWL",
0x37: "CSWH",
0x38: "KSWL",
0x39: "KSWH",
}
fmt.Printf("Page zero values:\n")
for _, k := range []int{0x36, 0x37, 0x38, 0x39} {
d := a.mmu.physicalMainRAM.data[k]
fmt.Printf(" %v(0x%x): 0x%02x\n", pageZeroSymbols[k], k, d)
}
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,79 +0,0 @@
package apple2
import "github.com/ivanizag/apple2/core6502"
// NewApple2 instantiates an apple2
func NewApple2(romFile string, charRomFile string, clockMhz float64,
isColor bool, fastMode bool, panicSS bool) *Apple2 {
var a Apple2
a.mmu = newMemoryManager(&a, romFile)
a.cpu = core6502.NewNMOS6502(a.mmu)
if charRomFile != "" {
a.cg = NewCharacterGenerator(charRomFile)
}
a.commandChannel = make(chan int, 100)
a.isColor = isColor
a.fastMode = fastMode
a.panicSS = panicSS
if clockMhz <= 0 {
// Full speed
a.cycleDurationNs = 0
} else {
a.cycleDurationNs = 1000.0 / clockMhz
}
// Set the io in 0xc000
a.io = newIoC0Page(&a)
a.mmu.setPages(0xc0, 0xc0, a.io)
return &a
}
func (a *Apple2) insertCard(c card, slot int) {
c.assign(a, slot)
a.cards[slot] = c
}
// AddDisk2 insterts a DiskII controller
func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) {
var c cardDisk2
c.loadRom(diskRomFile)
a.insertCard(&c, slot)
if diskImage != "" {
diskette := loadDisquette(diskImage)
//diskette.saveNib(diskImage + "bak")
c.drive[0].insertDiskette(diskette)
}
}
// AddLanguageCard inserts a 16Kb card
func (a *Apple2) AddLanguageCard(slot int) {
a.insertCard(&cardLanguage{}, slot)
}
// AddSaturnCard inserts a 128Kb card
func (a *Apple2) AddSaturnCard(slot int) {
a.insertCard(&cardSaturn{}, slot)
}
// AddCardLogger inserts a fake card that logs accesses
func (a *Apple2) AddCardLogger(slot int) {
a.insertCard(&cardLogger{}, slot)
}
// AddCardInOut inserts a fake card that interfaces with the emulator host
func (a *Apple2) AddCardInOut(slot int) {
a.insertCard(&cardInOut{}, slot)
}
// SetKeyboardProvider attaches an external keyboard provider
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
a.io.setKeyboardProvider(kb)
}
// SetSpeakerProvider attaches an external keyboard provider
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
a.io.setSpeakerProvider(s)
}

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

Binary file not shown.

View File

@ -1,86 +0,0 @@
package apple2
import (
"flag"
"os"
)
// MainApple is a device independant main. Video, keyboard and speaker won't be defined
func MainApple() *Apple2 {
romFile := flag.String(
"rom",
"<internal>/Apple2_Plus.rom",
"main rom file")
disk2RomFile := flag.String(
"diskRom",
"<internal>/DISK2.rom",
"rom file for the disk drive controller")
disk2Slot := flag.Int(
"disk2Slot",
6,
"slot for the disk driver. -1 for none.")
diskImage := flag.String(
"disk",
"<internal>/dos33.dsk",
"file to load on the first disk drive")
cpuClock := flag.Float64(
"mhz",
CpuClockMhz,
"cpu speed in Mhz, use 0 for full speed. Use F5 to toggle.")
charRomFile := flag.String(
"charRom",
"<internal>/Apple2rev7CharGen.rom",
"rom file for the disk drive controller")
languageCardSlot := flag.Int(
"languageCardSlot",
0,
"slot for the 16kb language card. -1 for none")
saturnCardSlot := flag.Int(
"saturnCardSlot",
-1,
"slot for the 256kb Saturn card. -1 for none")
mono := flag.Bool(
"mono",
false,
"emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle.",
)
fastDisk := flag.Bool(
"fastDisk",
true,
"set fast mode when the disks are spinning",
)
panicSS := flag.Bool(
"panicss",
false,
"panic if a not implemented softswitch is used")
dumpChars := flag.Bool(
"dumpChars",
false,
"shows the character map",
)
flag.Parse()
if *dumpChars {
cg := NewCharacterGenerator(*charRomFile)
cg.Dump()
os.Exit(0)
return nil
}
a := NewApple2(*romFile, *charRomFile, *cpuClock, !*mono, *fastDisk, *panicSS)
if *languageCardSlot >= 0 {
a.AddLanguageCard(*languageCardSlot)
}
if *saturnCardSlot >= 0 {
a.AddSaturnCard(*saturnCardSlot)
}
if *disk2Slot >= 0 {
a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage)
}
//a.AddCardInOut(2)
//a.AddCardLogger(4)
return a
}

Binary file not shown.

View File

@ -1,82 +0,0 @@
package main
import (
"unsafe"
"github.com/ivanizag/apple2"
"github.com/veandco/go-sdl2/sdl"
)
func main() {
a := apple2.MainApple()
SDLRun(a)
}
// SDLRun starts the Apple2 emulator on SDL
func SDLRun(a *apple2.Apple2) {
s := newSDLSpeaker()
s.start()
window, renderer, err := sdl.CreateWindowAndRenderer(4*40*7, 4*24*8,
sdl.WINDOW_SHOWN)
if err != nil {
panic("Failed to create window")
}
window.SetResizable(true)
defer window.Destroy()
defer renderer.Destroy()
window.SetTitle("Apple2")
kp := newSDLKeyBoard(a)
a.SetKeyboardProvider(kp)
a.SetSpeakerProvider(s)
go a.Run(false)
running := true
for running {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch t := event.(type) {
case *sdl.QuitEvent:
running = false
case *sdl.KeyboardEvent:
//fmt.Printf("[%d ms] Keyboard\ttype:%d\tsym:%c\tmodifiers:%d\tstate:%d\trepeat:%d\n",
// t.Timestamp, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat)
kp.putKey(t)
case *sdl.TextInputEvent:
//fmt.Printf("[%d ms] TextInput\ttype:%d\texts:%s\n",
// t.Timestamp, t.Type, t.GetText())
kp.putText(t)
}
}
img := apple2.Snapshot(a)
if img != nil {
surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]),
int32(img.Bounds().Dx()), int32(img.Bounds().Dy()),
32, 4*img.Bounds().Dx(),
0x0000ff, 0x0000ff00, 0x00ff0000, 0xff000000)
// Valid for little endian. Should we reverse for big endian?
// 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff)
if err != nil {
panic(err)
}
texture, err := renderer.CreateTextureFromSurface(surface)
if err != nil {
panic(err)
}
renderer.Clear()
w, h := window.GetSize()
renderer.Copy(texture, nil, &sdl.Rect{X: 0, Y: 0, W: w, H: h})
renderer.Present()
surface.Free()
texture.Destroy()
}
sdl.Delay(1000 / 30)
}
}

View File

@ -1,126 +0,0 @@
package main
import (
"unicode/utf8"
"github.com/ivanizag/apple2"
"github.com/veandco/go-sdl2/sdl"
)
type sdlKeyboard struct {
keyChannel chan uint8
a *apple2.Apple2
}
func newSDLKeyBoard(a *apple2.Apple2) *sdlKeyboard {
var k sdlKeyboard
k.keyChannel = make(chan uint8, 100)
k.a = a
return &k
}
func (k *sdlKeyboard) putText(textEvent *sdl.TextInputEvent) {
text := textEvent.GetText()
for _, ch := range text {
// We will use computed text only for printable ASCII chars
if ch < ' ' || ch > '~' {
continue
}
buf := make([]uint8, 1)
utf8.EncodeRune(buf, ch)
k.putChar(buf[0])
}
}
func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
/*
See "Apple II reference manual", page 5
To get keys as understood by the Apple2 hardware run:
10 A=PEEK(49152)
20 PRINT A, A - 128
30 GOTO 10
Missing Reset button
*/
if keyEvent.Type != sdl.KEYDOWN {
// Process only key pushes
return
}
key := keyEvent.Keysym
ctrl := key.Mod&sdl.KMOD_CTRL != 0
if ctrl {
if key.Sym >= 'a' && key.Sym <= 'z' {
k.putChar(uint8(key.Sym) - 97 + 1)
return
}
}
result := uint8(0)
switch key.Sym {
case sdl.K_ESCAPE:
result = 27
case sdl.K_DELETE:
result = 24
case sdl.K_BACKSPACE:
result = 8
case sdl.K_RETURN:
result = 13
case sdl.K_RETURN2:
result = 13
case sdl.K_LEFT:
if ctrl {
result = 31 // Base64A
}
result = 8
case sdl.K_RIGHT:
result = 21
// Base64A clone particularities
case sdl.K_F2:
result = 127
case sdl.K_UP:
result = 31
case sdl.K_DOWN:
result = 10
// Control of the emulator
case sdl.K_F5:
k.a.SendCommand(apple2.CommandToggleSpeed)
case sdl.K_F6:
k.a.SendCommand(apple2.CommandToggleColor)
case sdl.K_F7:
k.a.SendCommand(apple2.CommandSaveState)
case sdl.K_F8:
k.a.SendCommand(apple2.CommandLoadState)
case sdl.K_F9:
k.a.SendCommand(apple2.CommandDumpDebugInfo)
}
// Missing values 91 to 95. Usually control for [\]^_
// On the Base64A it's control for \]./
if result != 0 {
k.putChar(result)
}
}
func (k *sdlKeyboard) putChar(ch uint8) {
k.keyChannel <- ch
}
func (k *sdlKeyboard) GetKey(_ bool) (key uint8, ok bool) {
select {
case key = <-k.keyChannel:
ok = true
default:
ok = false
}
return
}

97
base64a.go Normal file
View File

@ -0,0 +1,97 @@
package izapple2
import "fmt"
/*
Copam BASE64A adaptation.
*/
const (
// There are 6 ROM chips. Each can have 4Kb or 8Kb. They can fill
// 2 or 4 banks with 2kb windows.
base64aRomBankSize = 12 * 1024
base64aRomBankCount = 4
base64aRomWindowSize = 2 * 1024
base64aRomChipCount = 6
)
func loadBase64aRom(a *Apple2) error {
// Load the 6 PROM dumps
romBanksBytes := make([][]uint8, base64aRomBankCount)
for j := range romBanksBytes {
romBanksBytes[j] = make([]uint8, 0, base64aRomBankSize)
}
for i := 0; i < base64aRomChipCount; i++ {
filename := fmt.Sprintf("<internal>/BASE64A_%X.BIN", 0xd0+i*0x08)
data, _, err := LoadResource(filename)
if err != nil {
return err
}
for j := range romBanksBytes {
start := (j * base64aRomWindowSize) % len(data)
romBanksBytes[j] = append(romBanksBytes[j], data[start:start+base64aRomWindowSize]...)
}
}
// Create paged ROM
romData := make([]uint8, 0, base64aRomBankSize*base64aRomBankCount)
for _, bank := range romBanksBytes {
romData = append(romData, bank...)
}
rom := newMemoryRangePagedROM(0xd000, romData, "Base64 ROM", base64aRomBankCount)
// Start with first bank active
rom.setPage(0)
a.mmu.physicalROM = rom
return nil
}
func addBase64aSoftSwitches(io *ioC0Page) {
// Other softswitches, not implemented but called from the ROM
io.addSoftSwitchW(0x0C, buildNotImplementedSoftSwitchW(io), "80COLOFF")
io.addSoftSwitchW(0x0E, buildNotImplementedSoftSwitchW(io), "ALTCHARSETOFF")
// ROM pagination softswitches. They use the annunciator 0 and 1
mmu := io.apple2.mmu
io.addSoftSwitchRW(0x58, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p & 2)
}
return 0
}, "ANN0OFF-ROM")
io.addSoftSwitchRW(0x59, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p | 1)
}
return 0
}, "ANN0ON-ROM")
io.addSoftSwitchRW(0x5A, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p & 1)
}
return 0
}, "ANN1OFF-ROM")
io.addSoftSwitchRW(0x5B, func() uint8 {
if rom, ok := mmu.physicalROM.(*memoryRangeROM); ok {
p := rom.getPage()
rom.setPage(p | 2)
}
return 0
}, "ANN1ON-ROM")
}
func charGenColumnsMapBase64a(column int) int {
bit := column + 2
// Weird positions
if column == 6 {
bit = 2
} else if column == 0 {
bit = 1
}
return bit
}

View File

@ -1,49 +1,197 @@
package apple2
package izapple2
import (
"io"
"fmt"
)
type card interface {
loadRom(filename string)
// Card represents an Apple II card to be inserted in a slot
type Card interface {
loadRom(data []uint8, layout cardRomLayout) error
assign(a *Apple2, slot int)
persistent
reset()
setName(name string)
setDebug(debug bool)
GetName() string
GetInfo() map[string]string
}
type cardBase struct {
a *Apple2
rom *memoryRange
slot int
ssr [16]softSwitchR
ssw [16]softSwitchW
a *Apple2
name string
trace bool
romCsxx *memoryRangeROM
romC8xx memoryHandler
romCxxx memoryHandler
slot int
_ssr [16]softSwitchR
_ssw [16]softSwitchW
_ssrName [16]string
_sswName [16]string
}
func (c *cardBase) loadRom(filename string) {
if c.a != nil {
panic("Rom must be loaded before inserting the card in the slot")
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
}
data := loadResource(filename)
c.rom = newMemoryRange(0, data)
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 {
return fmt.Errorf("ROM must be loaded before inserting the card in the slot")
}
switch layout {
case cardRomSimple:
if len(data) == 0x100 {
// Just 256 bytes in Cs00
c.romCsxx = newMemoryRangeROM(0, data, "Slot ROM")
} else if len(data)%0x100 == 0 {
// The ROM covers many 256 bytes pages of Csxx
// Used on the Dan 2 controller card
c.romCsxx = newMemoryRangePagedROM(0, data, "Slot paged ROM", uint8(len(data)/0x100))
} else {
return fmt.Errorf("invalid ROM size for simple layout")
}
case cardRomUpper:
if len(data) == 0x800 {
// The file has C800 to CFFF
// The 256 bytes in Cx00 are copied from the first page in C800
c.romCsxx = newMemoryRangeROM(0, data, "Slot ROM")
c.romC8xx = newMemoryRangeROM(0xc800, data, "Slot C8 ROM")
} else {
return fmt.Errorf("invalid ROM size for upper layout")
}
case cardRomUpperHalfEnd:
if len(data) == 0x400 {
// The file has C800 to CBFF for ROM
// The 256 bytes in Cx00 are copied from the last page in C800-CBFF
// Used on the Videx 80 columns card
c.romCsxx = newMemoryRangeROM(0, data[0x300:], "Slot ROM")
c.romC8xx = newMemoryRangeROM(0xc800, data, "Slot C8 ROM")
} else {
return fmt.Errorf("invalid ROM size for upper half end layout")
}
case cardRomFull:
if len(data) == 0x1000 {
// The file covers the full Cxxx range. Only showing the page
// corresponding to the slot used.
c.romCxxx = newMemoryRangeROM(0xc000, data, "Slot ROM")
} else if len(data)%0x1000 == 0 {
// The ROM covers the full Cxxx range with several pages
c.romCxxx = newMemoryRangePagedROM(0xc000, data, "Slot paged ROM", uint8(len(data)/0x1000))
} else {
return fmt.Errorf("invalid ROM size for full layout")
}
default:
return fmt.Errorf("invalid card ROM layout")
}
return nil
}
func (c *cardBase) assign(a *Apple2, slot int) {
c.a = a
c.slot = slot
if slot != 0 && c.rom != nil {
c.rom.base = uint16(0xc000 + slot*0x100)
a.mmu.setPagesRead(uint8(0xc0+slot), uint8(0xc0+slot), c.rom)
if slot != 0 {
if c.romCsxx != nil {
// Relocate to the assigned slot
c.romCsxx.setBase(uint16(0xc000 + slot*0x100))
a.mmu.setCardROM(slot, c.romCsxx)
}
if c.romC8xx != nil {
a.mmu.setCardROMExtra(slot, c.romC8xx)
}
if c.romCxxx != nil {
a.mmu.setCardROM(slot, c.romCxxx)
a.mmu.setCardROMExtra(slot, c.romCxxx)
}
}
for i := 0; i < 0x10; i++ {
a.io.addSoftSwitchR(uint8(0xC80+slot*0x10+i), c.ssr[i])
a.io.addSoftSwitchW(uint8(0xC80+slot*0x10+i), c.ssw[i])
if c._ssr[i] != nil {
a.io.addSoftSwitchR(uint8(0xC80+slot*0x10+i), c._ssr[i], c._ssrName[i])
}
if c._ssw[i] != nil {
a.io.addSoftSwitchW(uint8(0xC80+slot*0x10+i), c._ssw[i], c._sswName[i])
}
}
}
func (c *cardBase) save(w io.Writer) {
// Empty
func (c *cardBase) addCardSoftSwitchR(address uint8, ss softSwitchR, name string) {
c._ssr[address] = ss
c._ssrName[address] = name
}
func (c *cardBase) load(r io.Reader) {
// Empty
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

@ -1,8 +1,10 @@
package apple2
package izapple2
import (
"encoding/binary"
"io"
"fmt"
"strconv"
"github.com/ivanizag/izapple2/storage"
)
/*
@ -19,116 +21,217 @@ http://yesterbits.com/media/pubs/AppleOrchard/articles/disk-ii-part-1-1983-apr.p
NIB: 35 tracks 6656 bytes, 232960 bytes
*/
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 *diskette16sector
currentPhase int
power bool // q4
halfTrack int
position int
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 := 0; i < 4; i++ {
for i := uint8(0); i < 4; i++ {
phase := i
c.ssr[phase<<1] = func(_ *ioC0Page) uint8 {
return c.dataLatch // All even addresses return the last dataLatch
}
c.ssr[(phase<<1)+1] = func(_ *ioC0Page) uint8 {
// Move the head up or down depending on the previous phase.
c.addCardSoftSwitchRW(phase<<1, func() uint8 {
// Update magnets and position
drive := &c.drive[c.selected]
delta := (phase - drive.currentPhase + 4) % 4
switch delta {
case 1: // Up
drive.halfTrack++
case 2: // Illegal, let's say up
drive.halfTrack++
case 3: // Down
drive.halfTrack--
case 0: // No chamge
drive.phases &^= (1 << phase)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep, c.slot, c.selected)
}
// Don't go over the limits
if drive.halfTrack > maxHalfTrack {
drive.halfTrack = maxHalfTrack
} else if drive.halfTrack < 0 {
drive.halfTrack = 0
return c.dataLatch // All even addresses return the last dataLatch
}, fmt.Sprintf("PHASE%vOFF", phase))
c.addCardSoftSwitchRW((phase<<1)+1, func() uint8 { // Update magnets and position
drive := &c.drive[c.selected]
drive.phases |= (1 << phase)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep, slot, c.selected)
}
drive.currentPhase = phase
//fmt.Printf("DISKII: Current halftrack is %v\n", drive.halfTrack)
return 0
}
}, fmt.Sprintf("PHASE%vON", phase))
}
// Q4, power switch
c.ssr[0x8] = func(_ *ioC0Page) uint8 {
if c.drive[c.selected].power {
c.drive[c.selected].power = false
c.a.releaseFastMode()
}
c.addCardSoftSwitchRW(0x8, func() uint8 {
c.softSwitchQ4(false)
return c.dataLatch
}
c.ssr[0x9] = func(_ *ioC0Page) uint8 {
if !c.drive[c.selected].power {
c.drive[c.selected].power = true
c.a.requestFastMode()
}
}, "Q4DRIVEOFF")
c.addCardSoftSwitchRW(0x9, func() uint8 {
c.softSwitchQ4(true)
return 0
}
}, "Q4DRIVEON")
// Q5, drive selecion
c.ssr[0xA] = func(_ *ioC0Page) uint8 {
c.selected = 0
c.addCardSoftSwitchRW(0xA, func() uint8 {
c.softSwitchQ5(0)
return c.dataLatch
}
c.ssr[0xB] = func(_ *ioC0Page) uint8 {
c.selected = 1
}, "Q5SELECT1")
c.addCardSoftSwitchRW(0xB, func() uint8 {
c.softSwitchQ5(1)
return 0
}
}, "Q5SELECT2")
// Q6, Q7
for i := 0xC; i <= 0xF; i++ {
for i := uint8(0xC); i <= 0xF; i++ {
iCopy := i
c.ssr[iCopy] = func(_ *ioC0Page) uint8 {
c.addCardSoftSwitchR(iCopy, func() uint8 {
return c.softSwitchQ6Q7(iCopy, 0)
}
c.ssw[iCopy] = func(_ *ioC0Page, value uint8) {
}, "Q6Q7")
c.addCardSoftSwitchW(iCopy, func(value uint8) {
c.softSwitchQ6Q7(iCopy, value)
}
}, "Q6Q7")
}
c.cardBase.assign(a, slot)
}
func (c *cardDisk2) softSwitchQ6Q7(index int, in uint8) uint8 {
func (c *CardDisk2) softSwitchQ4(value bool) {
if !value && c.power {
// Turn off
c.power = false
if c.fastMode {
c.a.ReleaseFastMode()
}
drive := &c.drive[c.selected]
if drive.diskette != nil {
drive.diskette.PowerOff(c.a.cpu.GetCycles())
}
} else if value && !c.power {
// Turn on
c.power = true
if c.fastMode {
c.a.RequestFastMode()
}
drive := &c.drive[c.selected]
if drive.diskette != nil {
drive.diskette.PowerOn(c.a.cpu.GetCycles())
}
}
}
func (c *CardDisk2) softSwitchQ5(selected int) {
if c.power && c.selected != selected {
// Selected changed with power on, power goes to the other disk
if c.drive[c.selected].diskette != nil {
c.drive[c.selected].diskette.PowerOff(c.a.cpu.GetCycles())
}
if c.drive[selected].diskette != nil {
c.drive[selected].diskette.PowerOn(c.a.cpu.GetCycles())
}
}
c.selected = selected
}
// Q6: shift/load
// Q7: read/write
func (c *CardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
switch index {
case 0xC: // Q6L
c.q6 = false
case 0xD: // Q6H
c.q6 = true
case 0xE: // Q/L
case 0xE: // Q7L
c.q7 = false
case 0xF: // Q7H
c.q7 = true
@ -138,25 +241,22 @@ func (c *cardDisk2) softSwitchQ6Q7(index int, in uint8) uint8 {
if index&1 == 0 {
// All even addresses return the last dataLatch
return c.dataLatch
} else {
return 0
}
return 0
}
func (c *cardDisk2) processQ6Q7(in uint8) {
func (c *CardDisk2) processQ6Q7(in uint8) {
d := &c.drive[c.selected]
if d.diskette == nil {
return
}
if !c.q6 {
if !c.q6 { // shift
if !c.q7 { // Q6L-Q7L: Read
track := d.halfTrack / 2
c.dataLatch, d.position = d.diskette.read(track, d.position)
c.dataLatch = d.diskette.Read(d.trackStep, c.a.cpu.GetCycles())
} else { // Q6L-Q7H: Write the dataLatch value to disk. Shift data out
track := d.halfTrack / 2
d.position = d.diskette.write(track, d.position, c.dataLatch)
d.diskette.Write(d.trackStep, c.dataLatch, c.a.cpu.GetCycles())
}
} else {
} else { // load
if !c.q7 { // Q6H-Q7L: Sense write protect / prewrite state
// Bit 7 of the control status register means write protected
c.dataLatch = 0 // Never write protected
@ -164,40 +264,21 @@ func (c *cardDisk2) processQ6Q7(in uint8) {
c.dataLatch = in
}
}
/*
if c.dataLatch >= 0x80 {
fmt.Printf("Datalach: 0x%.2x in cycle %v\n", c.dataLatch, c.a.cpu.GetCycles())
}
*/
}
func (d *cardDisk2Drive) insertDiskette(dt *diskette16sector) {
d.diskette = dt
}
func (d *cardDisk2Drive) insertDiskette(name string) error {
diskette, err := LoadDiskette(name)
if err != nil {
return err
}
func (c *cardDisk2) save(w io.Writer) {
binary.Write(w, binary.BigEndian, c.selected)
binary.Write(w, binary.BigEndian, c.dataLatch)
binary.Write(w, binary.BigEndian, c.q6)
binary.Write(w, binary.BigEndian, c.q7)
c.drive[0].save(w)
c.drive[1].save(w)
}
func (c *cardDisk2) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &c.selected)
binary.Read(r, binary.BigEndian, &c.dataLatch)
binary.Read(r, binary.BigEndian, &c.q6)
binary.Read(r, binary.BigEndian, &c.q7)
c.drive[0].load(r)
c.drive[1].load(r)
}
func (d *cardDisk2Drive) save(w io.Writer) {
binary.Write(w, binary.BigEndian, d.currentPhase)
binary.Write(w, binary.BigEndian, d.power)
binary.Write(w, binary.BigEndian, d.halfTrack)
binary.Write(w, binary.BigEndian, d.position)
}
func (d *cardDisk2Drive) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &d.currentPhase)
binary.Read(r, binary.BigEndian, &d.power)
binary.Read(r, binary.BigEndian, &d.halfTrack)
binary.Read(r, binary.BigEndian, &d.position)
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)
}

100
cardFastChip.go Normal file
View File

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

View File

@ -1,7 +1,9 @@
package apple2
package izapple2
import (
"bufio"
"fmt"
"os"
)
/*
@ -11,137 +13,163 @@ See:
"Apple II Monitors peeled."
http://mysite.du.edu/~etuttle/math/acia.htm
PR#n stores Cn00 in CSWL and CSWH
IN#n stores Cn00 in KSWL and KSWH
*/
type cardInOut struct {
// CardInOut is an experimental card to bridge with the host console
type CardInOut struct {
cardBase
i int
reader *bufio.Reader
}
func (c *cardInOut) assign(a *Apple2, slot int) {
for i := 0x0; i <= 0xf; i++ {
iCopy := i
c.ssr[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
}
c.ssw[i] = func(_ *ioC0Page, value uint8) {
fmt.Printf("[cardInOut] Write access to softswith 0x%x for slot %v, value 0x%x.\n", iCopy, slot, value)
}
func newCardInOutBuilder() *cardBuilder {
return &cardBuilder{
name: "InOut test card",
description: "Card to test I/O",
buildFunc: func(params map[string]string) (Card, error) {
return &CardInOut{}, nil
},
}
}
in := true
out := false
func (c *CardInOut) assign(a *Apple2, slot int) {
c.addCardSoftSwitchR(0, func() uint8 {
if c.reader == nil {
c.reader = bufio.NewReader(os.Stdin)
}
value, err := c.reader.ReadByte()
if err != nil {
panic(err)
}
value += 0x80
if value&0x7f == 10 {
value = 13 + 0x80
}
//fmt.Printf("[cardInOut] Read access to softswith 0x%x for slot %v, value %x.\n", 0, slot, value)
return value
}, "INOUTR")
c.addCardSoftSwitchW(1, func(value uint8) {
//fmt.Printf("[cardInOut] Write access to softswith 0x%x for slot %v, value 0x%x: %v, %v.\n", 1, slot, value, value&0x7f, string(value&0x7f))
if value&0x7f == 13 {
fmt.Printf("\n")
} else {
fmt.Printf("%v", string(value&0x7f))
}
}, "INOUTW")
data := buildBaseInOutRom(slot)
c.romCsxx = newMemoryRangeROM(0xC200, data[:], "InOUt card")
c.cardBase.assign(a, slot)
}
func buildBaseInOutRom(slot int) []uint8 {
data := [256]uint8{
// Register
0xA9, 0xC2,
0x85, 0x37,
0x85, 0x39,
0xA9, 0x10,
0x85, 0x36,
0xA9, 0x15,
0x85, 0x38,
0x60, 0xEA,
0x4c, 0x40, 0xc2, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
// Out char
0x8D, 0xA1, 0xC0,
0x60, 0xEA,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// Get char
0x91, 0x28,
0xAD, 0xA0, 0xC0,
0x60,
0x48, 0xA5, 0x38, 0xD0, 0x11, 0xA9, 0xC2, 0xC5,
0x39, 0xD0, 0x0B, 0xAD, 0x4F, 0x85, 0x38, 0x68,
0x91, 0x28, 0xAD, 0xA0, 0xC0, 0x60, 0x68, 0x8D,
0xA1, 0xC0, 0x60,
}
if !out {
// NOP the CSWL,H change
for _, v := range []uint8{2, 3, 8, 9} {
data[v] = 0xEA
}
}
// Fix slot dependant addresses
data[0x02] = uint8(0xc0 + slot)
data[0x46] = uint8(0xc0 + slot)
data[0x53] = uint8(0x80 + slot<<4)
data[0x58] = uint8(0x81 + slot<<4)
if !in {
// NOP the KSWL,H change
for _, v := range []uint8{4, 5, 12, 13} {
data[v] = 0xEA
}
}
c.rom = newMemoryRange(0xC200, data[0:255])
if slot != 2 {
// To make ifwork on other slots, patch C2, A0 and A1
panic("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

@ -1,9 +1,4 @@
package apple2
import (
"encoding/binary"
"io"
)
package izapple2
/*
Language card with 16 extra kb for the Apple ][ and ][+
@ -16,62 +11,81 @@ Note also that language cards for the Apple ][ had ROM on
board to replace the main board F8 ROM with Autostart. That
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
"When RAM is deselected, the ROM on the Language card is selected for
the top 2K ($F800-$FFFF), and the ROM on the main board is selected
for $D000-$F7FF.
Power on RESET initializes ROM to read mode and RAM to write mode,
and selects the second 4K bank to map $D000-$DFFF."
Writing to the softswitch disables writing in LC? Saw that
somewhere but doing so fails IIe self check.
*/
type cardLanguage struct {
// CardLanguage is an Language Card
type CardLanguage struct {
cardBase
readState bool
writeState int
activeBank int
ramBankA *memoryRange // First 4kb to map in 0xD000-0xDFFF
ramBankB *memoryRange // Second 4kb to map in 0xD000-0xDFFF
ramUpper *memoryRange // Upper 8kb to map in 0xE000-0xFFFF
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 sofstwitch accesses
// Write enabling requires two softswitch accesses
lcWriteDisabled = 0
lcWriteHalfEnabled = 1
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.activeBank = 1
c.altBank = true // Start on bank2
c.ramBankA = newMemoryRange(0xd000, make([]uint8, 0x1000))
c.ramBankB = newMemoryRange(0xd000, make([]uint8, 0x1000))
c.ramUpper = newMemoryRange(0xe000, make([]uint8, 0x2000))
for i := 0x0; i <= 0xf; i++ {
a.mmu.initLanguageRAM(1)
for i := uint8(0x0); i <= 0xf; i++ {
iCopy := i
c.ssr[i] = func(*ioC0Page) uint8 {
c.ssAction(iCopy)
c.addCardSoftSwitchR(iCopy, func() uint8 {
c.ssAction(iCopy, false)
return 0
}
c.ssw[i] = func(*ioC0Page, uint8) {
// Writing resets write count (from A2AUDIT)
c.writeState = lcWriteDisabled
}
}, "LANGCARDR")
c.addCardSoftSwitchW(iCopy, func(uint8) {
c.ssAction(iCopy, true)
}, "LANGCARDW")
}
c.cardBase.assign(a, slot)
c.applyState()
}
func (c *cardLanguage) ssAction(ss int) {
c.activeBank = (ss >> 3) & 1
func (c *CardLanguage) ssAction(ss uint8, write bool) {
c.altBank = ((ss >> 3) & 1) == 0
action := ss & 0x3
switch action {
case 0:
@ -81,7 +95,9 @@ func (c *cardLanguage) ssAction(ss int) {
case 1:
// ROM read, RAM write
c.readState = false
c.writeState++
if !write {
c.writeState++
}
case 2:
// ROM read, no writes
c.readState = false
@ -89,7 +105,15 @@ func (c *cardLanguage) ssAction(ss int) {
case 3:
//RAM read, RAM write
c.readState = true
c.writeState++
if !write {
c.writeState++
}
}
if write && c.writeState == lcWriteHalfEnabled {
// UtA2e, 5-23. It is reset by even read access or any write acccess in the $C08x range
// And https://github.com/zellyn/a2audit/issues/3
c.writeState = lcWriteDisabled
}
if c.writeState > lcWriteEnabled {
@ -99,51 +123,6 @@ func (c *cardLanguage) ssAction(ss int) {
c.applyState()
}
func (c *cardLanguage) getActiveBank() *memoryRange {
if c.activeBank == 0 {
return c.ramBankA
}
return c.ramBankB
}
func (c *cardLanguage) applyState() {
mmu := c.a.mmu
if c.readState {
mmu.setPagesRead(0xd0, 0xdf, c.getActiveBank())
mmu.setPagesRead(0xe0, 0xff, c.ramUpper)
} else {
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
}
if c.writeState == lcWriteEnabled {
mmu.setPagesWrite(0xd0, 0xdf, c.getActiveBank())
mmu.setPagesWrite(0xe0, 0xff, c.ramUpper)
} else {
mmu.setPagesWrite(0xd0, 0xff, nil)
}
}
func (c *cardLanguage) save(w io.Writer) {
binary.Write(w, binary.BigEndian, c.readState)
binary.Write(w, binary.BigEndian, c.writeState)
binary.Write(w, binary.BigEndian, c.activeBank)
c.ramBankA.save(w)
c.ramBankB.save(w)
c.ramUpper.save(w)
c.cardBase.save(w)
}
func (c *cardLanguage) load(r io.Reader) {
binary.Read(r, binary.BigEndian, &c.readState)
binary.Read(r, binary.BigEndian, &c.writeState)
binary.Read(r, binary.BigEndian, &c.activeBank)
c.ramBankA.load(r)
c.ramBankB.load(r)
c.ramUpper.load(r)
c.applyState()
c.cardBase.load(r)
func (c *CardLanguage) applyState() {
c.a.mmu.setLanguageRAM(c.readState, c.writeState == lcWriteEnabled, c.altBank)
}

View File

@ -1,4 +1,4 @@
package apple2
package izapple2
import (
"fmt"
@ -8,36 +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 := 0x0; i <= 0xf; i++ {
iCopy := i
c.ssr[i] = func(*ioC0Page) uint8 {
fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", iCopy, slot)
return 0
}
c.ssw[i] = func(_ *ioC0Page, value uint8) {
fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%v.\n", iCopy, slot, value)
}
func 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")
if slot != 0 {
a.mmu.setPagesRead(uint8(0xc0+slot), uint8(0xc0+slot), c)
}
c.cardBase.assign(a, slot)
}
// MemoryHandler implementation
func (c *cardLogger) peek(address uint16) uint8 {
fmt.Printf("[cardLogger] Read in %x.\n", address)
c.a.dumpDebugInfo()
return 0xf3
}
func (*cardLogger) poke(address uint16, value uint8) {
fmt.Printf("[cardLogger] Write %x in %x.\n", value, address)
}

144
cardMemoryExpansion.go Normal file
View File

@ -0,0 +1,144 @@
package izapple2
import "fmt"
/*
Apple II Memory Expansion Card
See:
http://www.apple-iigs.info/doc/fichiers/a2me.pdf
http://ae.applearchives.com/files/RamFactor_Manual_1.5.pdf
http://www.1000bit.it/support/manuali/apple/technotes/memx/tn.memx.1.html
There is a self test in ROM, address Cs0A.
From the RamFactor docs:
The RamFactor card has five addressable registers, which are addressed
according to the slot number the card is in:
$C080+slot * 16low byte of RAM address
$C081+slot * 16middle byte of RAM address
$C082+slot * 16high byte of RAM address
$C083+slot * 16data at addressed location
$C08F+slot * 16Firmware Bank Select
After power up or Control-Reset, the registers on the card are all in a
disabled state. They will be enabled by addressing any address in the firmware
page $Cs00-CsFF.
The three address bytes can be both written into and read from. If the card
has one Megabyte or less, reading the high address byte will always return a
value in the range $F0-FF. The top nybble can be any value when you write it,
but it will always be F when you read it. If the card has more than one
Megabyte of RAM, the top nybble will be a meaningful part of the address.
Notes for RAMFactor:
- https://github.com/mamedev/mame/blob/master/src/devices/bus/a2bus/a2memexp.cpp
- ss 5 is for the ROM page, there are two.
- https://ae.applearchives.com/all_apple_iis/ramfactor/
*/
const (
memoryExpansionMask = 0x000fffff // 10 bits, 1MB
)
// CardMemoryExpansion is a Memory Expansion card
type CardMemoryExpansion struct {
cardBase
ram []uint8
index int
}
func newCardMemoryExpansionBuilder() *cardBuilder {
return &cardBuilder{
name: "Memory Expansion Card",
description: "Memory expansion card",
defaultParams: &[]paramSpec{
{"size", "RAM of the card, can be 256, 512, 768 or 1024", "1024"},
},
buildFunc: func(params map[string]string) (Card, error) {
size, err := paramsGetInt(params, "size")
if err != nil {
return nil, err
}
if size != 256 && size != 512 && size != 768 && size != 1024 {
return nil, fmt.Errorf("invalid RAM size %v. It must be 256, 512, 768 or 1024", size)
}
var c CardMemoryExpansion
c.ram = make([]uint8, size*1024)
err = c.loadRomFromResource("<internal>/MemoryExpansionCard-341-0344a.bin", cardRomFull)
if err != nil {
return nil, err
}
return &c, nil
},
}
}
// GetInfo returns card info
func (c *CardMemoryExpansion) GetInfo() map[string]string {
info := make(map[string]string)
info["size"] = fmt.Sprintf("%vKB", len(c.ram)/1024)
return info
}
func (c *CardMemoryExpansion) assign(a *Apple2, slot int) {
// Read pointer position
c.addCardSoftSwitchR(0, func() uint8 {
return uint8(c.index)
}, "MEMORYEXLOR")
c.addCardSoftSwitchR(1, func() uint8 {
return uint8(c.index >> 8)
}, "MEMORYEXMIR")
c.addCardSoftSwitchR(2, func() uint8 {
// Top nibble returned is 0xf
return uint8(c.index>>16) | 0xf0
}, "MEMORYEXHIR")
// Set pointer position
c.addCardSoftSwitchW(0, func(value uint8) {
c.index = (c.index &^ 0xff) + int(value)
}, "MEMORYEXLOW")
c.addCardSoftSwitchW(1, func(value uint8) {
c.index = (c.index &^ 0xff00) + int(value)<<8
}, "MEMORYEXMIW")
c.addCardSoftSwitchW(2, func(value uint8) {
// Only lo nibble is used
c.index = (c.index &^ 0xff0000) + int(value&0x0f)<<16
}, "MEMORYEXHIW")
// Read data
c.addCardSoftSwitchR(3, func() uint8 {
var value uint8
if c.index < len(c.ram) {
value = c.ram[c.index]
} else {
value = 0xde // Ram socket not populated
}
c.index = (c.index + 1) & memoryExpansionMask
return value
}, "MEMORYEXR")
// Write data
c.addCardSoftSwitchW(3, func(value uint8) {
if c.index < len(c.ram) {
c.ram[c.index] = value
}
c.index = (c.index + 1) & memoryExpansionMask
}, "MEMORYEXW")
// The rest of the softswitches return 255, at least on //e and //c
for i := uint8(4); i < 16; i++ {
c.addCardSoftSwitchR(i, func() uint8 {
return 255
}, "MEMORYEXUNUSEDR")
}
c.cardBase.assign(a, slot)
}

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

79
cardRGB.go Normal file
View File

@ -0,0 +1,79 @@
package izapple2
/*
Extended 80-Column Text AppleColor Card or Video7 RGB-SL7 card
See:
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/Apple%20IIe/Apple%20IIe%20Extended%2080%20Column%20RGB%20Card/Manuals/Apple%20Ext80ColumnAppleColorCardHR%20Manual.pdf
https://apple2online.com/web_documents/Video-7%20Manual%20KB.pdf
https://mirrors.apple2.org.za/ftp.apple.asimov.net/documentation/hardware/video/DIGICARD%2064K%20Extended%2080%20Column%20RGB%20Card%20for%20Apple%20IIe%20Instruction%20Manual.pdf
Diagnostics disk:
https://mirrors.apple2.org.za/ftp.apple.asimov.net/images/hardware/video/Video-7%20Apple%20II%20RGB%20Demo%20%28Video-7%2C%20Inc.%29%281984%29.dsk
It goes to the 80 column slot.
To set the state it AN3 in graphics mode has to go off-on-off-on. Each pair off-on record the state of 80col:
on step 0, an ANN3OFF moves to step 1
on step 1, an ANN3ON moves to step 2, and the value of 80COL is copied to RGB flag 1
on step 2, an ANN3OFF moves to step 3
on step 3, an ANN3ON moves to step 4, and the value of 80COL is copied to RGB flag 2
Modes by RGB flags 1 and 2:
0-0: 560*192 mono
1-1: 140*192 ntsc
0-1: Mixed mode
1-0: 160*192 ntsc
*/
type cardRGB struct {
// cardBase, not a regular card
step uint8
}
func setupRGBCard(a *Apple2) *cardRGB {
var c cardRGB
c.step = 0
a.io.softSwitchesData[ioFlagRGBCardActive] = ssOn
// Does not have ROM or private softswitches. It spies on the softswitches
a.io.addSoftSwitchRW(0x50, func() uint8 {
a.io.softSwitchesData[ioFlagText] = ssOff
// Reset RGB modes when entering graphics mode
c.step = 0
a.io.softSwitchesData[ioFlag1RGBCard] = ssOn
a.io.softSwitchesData[ioFlag2RGBCard] = ssOn
return 0
}, "TEXTOFF-RGB")
a.io.addSoftSwitchRW(0x5e, func() uint8 {
a.io.softSwitchesData[ioFlagAnnunciator3] = ssOff
switch c.step {
case 0:
c.step++
case 2:
c.step++
case 4:
c.step = 0
}
return 0
}, "ANN3OFF-RGB")
a.io.addSoftSwitchRW(0x5f, func() uint8 {
a.io.softSwitchesData[ioFlagAnnunciator3] = ssOn
switch c.step {
case 1:
a.io.softSwitchesData[ioFlag1RGBCard] = a.io.softSwitchesData[ioFlag80Col]
c.step++
case 3:
a.io.softSwitchesData[ioFlag2RGBCard] = a.io.softSwitchesData[ioFlag80Col]
c.step++
}
return 0
}, "ANN3ON-RGB")
return &c
}

49
cardRamWorks.go Normal file
View File

@ -0,0 +1,49 @@
package izapple2
import (
"fmt"
"strconv"
)
/*
RAMWorks style card on the Apple IIe aus slot.
https://patents.google.com/patent/US4601018
https://ae.applearchives.com/apple_e/ramworks_iii/ramworks_iii_basic_manual_1.pdf
Diagnostics disks:
https://ae.applearchives.com/apple_e/ramworks_iii/ramworks_diagnostics.zip
It's is like the extra 64kb on an Apple IIe 80col 64kb card, but with up to 256 banks
*/
func setupRAMWorksCard(a *Apple2, sizeArg string) error {
size, err := strconv.Atoi(sizeArg)
if err != nil {
return fmt.Errorf("invalid RamWorks card RAM size: %s", sizeArg)
}
if size%64 != 0 {
return fmt.Errorf("the Ramworks size must be a multiple of 64, %v is not", size)
}
a.mmu.initExtendedRAM(size / 64)
ssr := func() uint8 {
return a.mmu.extendedRAMBlock
}
ssw := func(value uint8) {
a.mmu.setExtendedRAMActiveBlock(value)
}
// Does not have a slot assigned
a.io.addSoftSwitchR(0x71, ssr, "RAMWORKSR")
a.io.addSoftSwitchR(0x73, ssr, "RAMWORKSR")
a.io.addSoftSwitchR(0x75, ssr, "RAMWORKSR")
a.io.addSoftSwitchR(0x77, ssr, "RAMWORKSR")
a.io.addSoftSwitchW(0x71, ssw, "RAMWORKSW")
a.io.addSoftSwitchW(0x73, ssw, "RAMWORKSW")
a.io.addSoftSwitchW(0x75, ssw, "RAMWORKSW")
a.io.addSoftSwitchW(0x77, ssw, "RAMWORKSW")
return nil
}

View File

@ -1,85 +1,77 @@
package apple2
import (
"encoding/binary"
"io"
)
package izapple2
/*
RAM card with 128Kb. It's like 8 language cards.
http://www.applelogic.org/files/SATURN128MAN.pdf
See:
http://www.applelogic.org/files/SATURN128MAN.pdf
*/
type cardSaturn struct {
// CardSaturn is a Saturn128 card
type CardSaturn struct {
cardBase
readState bool
writeState int
activeBank int
activeBlock int
ramBankA [saturnBlocks]*memoryRange // First 4kb to map in 0xD000-0xDFFF
ramBankB [saturnBlocks]*memoryRange // Second 4kb to map in 0xD000-0xDFFF
ramUpper [saturnBlocks]*memoryRange // Upper 8kb to map in 0xE000-0xFFFF
writeState uint8
altBank bool
activeBlock uint8
}
const (
// Write enabling requires two sofstwitch accesses
saturnWriteDisabled = 0
saturnWriteHalfEnabled = 1
saturnWriteEnabled = 2
)
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.activeBank = 1
c.altBank = true
c.activeBlock = 0
a.mmu.initLanguageRAM(saturnBlocks)
for i := 0; i < saturnBlocks; i++ {
c.ramBankA[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
c.ramBankB[i] = newMemoryRange(0xd000, make([]uint8, 0x1000))
c.ramUpper[i] = newMemoryRange(0xe000, make([]uint8, 0x2000))
}
for i := 0x0; i <= 0xf; i++ {
// TODO: use addCardSoftSwitches()
for i := uint8(0x0); i <= 0xf; i++ {
iCopy := i
c.ssr[i] = func(*ioC0Page) uint8 {
c.addCardSoftSwitchR(iCopy, func() uint8 {
c.ssAction(iCopy)
return 0
}
c.ssw[i] = func(*ioC0Page, uint8) {
// Writing resets write count (from A2AUDIT)
c.writeState = lcWriteDisabled
}
}, "SATURNR")
c.addCardSoftSwitchW(iCopy, func(uint8) {
c.ssAction(iCopy)
}, "SATURNW")
}
c.cardBase.assign(a, slot)
c.applyState()
}
func (c *cardSaturn) ssAction(ss int) {
func (c *CardSaturn) ssAction(ss uint8) {
switch ss {
case 0:
// RAM read, no writes
c.activeBank = 0
c.altBank = false
c.readState = true
c.writeState = lcWriteDisabled
case 1:
// ROM read, RAM write
c.activeBank = 0
c.altBank = false
c.readState = false
c.writeState++
case 2:
// ROM read, no writes
c.activeBank = 0
c.altBank = false
c.readState = false
c.writeState = lcWriteDisabled
case 3:
//RAM read, RAM write
c.activeBank = 0
c.altBank = false
c.readState = true
c.writeState++
case 4:
@ -92,22 +84,22 @@ func (c *cardSaturn) ssAction(ss int) {
c.activeBlock = 3
case 8:
// RAM read, no writes
c.activeBank = 1
c.altBank = true
c.readState = true
c.writeState = lcWriteDisabled
case 9:
// ROM read, RAM write
c.activeBank = 1
c.altBank = true
c.readState = false
c.writeState++
case 10:
// ROM read, no writes
c.activeBank = 1
c.altBank = true
c.readState = false
c.writeState = lcWriteDisabled
case 11:
//RAM read, RAM write
c.activeBank = 1
c.altBank = true
c.readState = true
c.writeState++
case 12:
@ -127,56 +119,7 @@ func (c *cardSaturn) ssAction(ss int) {
c.applyState()
}
func (c *cardSaturn) getActiveBank() [8]*memoryRange {
if c.activeBank == 0 {
return c.ramBankA
}
return c.ramBankB
}
func (c *cardSaturn) applyState() {
mmu := c.a.mmu
block := c.activeBlock
if c.readState {
mmu.setPagesRead(0xd0, 0xdf, c.getActiveBank()[block])
mmu.setPagesRead(0xe0, 0xff, c.ramUpper[block])
} else {
mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM)
}
if c.writeState == lcWriteEnabled {
mmu.setPagesWrite(0xd0, 0xdf, c.getActiveBank()[block])
mmu.setPagesWrite(0xe0, 0xff, c.ramUpper[block])
} else {
mmu.setPagesWrite(0xd0, 0xff, nil)
}
}
func (c *cardSaturn) save(w io.Writer) {
for i := 0; i < saturnBlocks; i++ {
binary.Write(w, binary.BigEndian, c.readState)
binary.Write(w, binary.BigEndian, c.writeState)
binary.Write(w, binary.BigEndian, c.activeBank)
binary.Write(w, binary.BigEndian, c.activeBlock)
c.ramBankA[i].save(w)
c.ramBankB[i].save(w)
c.ramUpper[i].save(w)
}
c.cardBase.save(w)
}
func (c *cardSaturn) load(r io.Reader) {
for i := 0; i < saturnBlocks; i++ {
binary.Read(r, binary.BigEndian, &c.readState)
binary.Read(r, binary.BigEndian, &c.writeState)
binary.Read(r, binary.BigEndian, &c.activeBank)
binary.Read(r, binary.BigEndian, &c.activeBlock)
c.ramBankA[i].load(r)
c.ramBankB[i].load(r)
c.ramUpper[i].load(r)
c.applyState()
}
c.cardBase.load(r)
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)
}
}

67
cardThunderClockPlus.go Normal file
View File

@ -0,0 +1,67 @@
package izapple2
import "github.com/ivanizag/izapple2/component"
/*
ThunderClock`, real time clock card.
See:
https://ia800706.us.archive.org/22/items/ThunderClock_Plus/ThunderClock_Plus.pdf
https://prodos8.com/docs/technote/01/
https://www.semiee.com/file/backup/NEC-D1990.pdf
uPD1990AC hookup:
bit 0 = data in
bit 1 = CLK
bit 2 = STB
bit 3 = C0
bit 4 = C1
bit 5 = C2
bit 7 = data out
*/
// CardThunderClockPlus represents a ThunderClock+ card
type CardThunderClockPlus struct {
cardBase
upd1990 component.MicroPD1990ac
}
func newCardThunderClockPlusBuilder() *cardBuilder {
return &cardBuilder{
name: "ThunderClock+ Card",
description: "Clock card",
buildFunc: func(params map[string]string) (Card, error) {
var c CardThunderClockPlus
err := c.loadRomFromResource("<internal>/ThunderclockPlusROM.bin", cardRomUpper)
if err != nil {
return nil, err
}
return &c, nil
},
}
}
func (c *CardThunderClockPlus) assign(a *Apple2, slot int) {
c.addCardSoftSwitchR(0, func() uint8 {
bit := c.upd1990.Out()
// Get the next data bit from uPD1990AC on the MSB
if bit {
return 0x80
}
return 0
}, "THUNDERCLOCKR")
c.addCardSoftSwitchW(0, func(value uint8) {
dataIn := (value & 0x01) == 1
clock := ((value >> 1) & 0x01) == 1
strobe := ((value >> 2) & 0x01) == 1
command := (value >> 3) & 0x07
/* fmt.Printf("[cardThunderClock] dataIn %v, clock %v, strobe %v, command %v.\n",
dataIn, clock, strobe, command) */
c.upd1990.In(clock, strobe, command, dataIn)
}, "THUNDERCLOCKW")
c.cardBase.assign(a, slot)
}

54
cardVidHD.go Normal file
View File

@ -0,0 +1,54 @@
package izapple2
/*
Simulates just what is needed to make Total Replay use the GS modes if the VidHD card is found
See:
https://github.com/a2-4am/4cade/blob/master/src/hw.vidhd.a
http://www.applelogic.org/files/GSHARDWAREREF.pdf, page 89
*/
// CardVidHD represents a VidHD card
type CardVidHD struct {
cardBase
}
func newCardVidHDBuilder() *cardBuilder {
return &cardBuilder{
name: "VidHD Card - limited",
description: "Firmware signature of the VidHD card to trick Total Replay to use the SHR mode",
buildFunc: func(params map[string]string) (Card, error) {
var c CardVidHD
c.loadRom(buildVidHDRom(), cardRomSimple)
return &c, nil
},
}
}
func buildVidHDRom() []uint8 {
data := make([]uint8, 256)
data[0] = 0x24
data[1] = 0xEA
data[2] = 0x4C
return data
}
const (
ioDataNewVideo uint8 = 0x29
)
func (c *CardVidHD) assign(a *Apple2, slot int) {
// The softswitches are outside the card reserved ss
a.io.addSoftSwitchR(0x22, buildNotImplementedSoftSwitchR(a.io), "VIDHD-TBCOLOR")
a.io.addSoftSwitchW(0x22, buildNotImplementedSoftSwitchW(a.io), "VIDHD-TBCOLOR")
a.io.addSoftSwitchR(0x29, getStatusSoftSwitch(a.io, ioDataNewVideo), "VIDHD-NEWVIDEO")
a.io.addSoftSwitchW(0x29, setStatusSoftSwitch(a.io, ioDataNewVideo), "VIDHD-NEWVIDEO")
a.io.addSoftSwitchR(0x34, buildNotImplementedSoftSwitchR(a.io), "VIDHD-CLOCKCTL")
a.io.addSoftSwitchW(0x34, buildNotImplementedSoftSwitchW(a.io), "VIDHD-CLOCKCTL")
a.io.addSoftSwitchR(0x35, buildNotImplementedSoftSwitchR(a.io), "VIDHD-SHADOW")
a.io.addSoftSwitchW(0x35, buildNotImplementedSoftSwitchW(a.io), "VIDHD-SHADOW")
c.cardBase.assign(a, slot)
}

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

@ -1,82 +1,107 @@
package apple2
package izapple2
import (
"errors"
"fmt"
)
/*
See:
hhttps://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Companies/Apple/Documentation/Apple%20Technical%20Information%20Library/a2til041.txt
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Companies/Apple/Documentation/Apple%20Technical%20Information%20Library/a2til041.txt
*/
// CharacterGenerator represents the ROM wth the characters bitmaps
type CharacterGenerator struct {
data []uint8
data []uint8
columnMap charColumnMap
page int
pageSize int
}
type charColumnMap func(column int) int
func charGenColumnsMap2Plus(column int) int {
return 6 - column
}
func charGenColumnsMap2e(column int) int {
return column
}
const (
rev7CharGenSize = 2048
charGenPageSize2Plus = 2048
charGenPageSize2E = 2048 * 2
)
// NewCharacterGenerator instantiates a new Character Generator with the rom on the file given
func NewCharacterGenerator(filename string) *CharacterGenerator {
func newCharacterGenerator(filename string, order charColumnMap, pageSize int) (*CharacterGenerator, error) {
var cg CharacterGenerator
cg.load(filename)
return &cg
cg.columnMap = order
cg.pageSize = pageSize
err := cg.load(filename)
if err != nil {
return nil, err
}
return &cg, nil
}
func (cg *CharacterGenerator) load(filename string) {
bytes := loadResource(filename)
func (cg *CharacterGenerator) load(filename string) error {
bytes, _, err := LoadResource(filename)
if err != nil {
return err
}
size := len(bytes)
if size != rev7CharGenSize {
panic("Character ROM size not supported")
if size < cg.pageSize {
return errors.New("character ROM size not supported")
}
cg.data = bytes
return nil
}
func (cg *CharacterGenerator) setPage(page int) {
// Some clones had a switch to change codepage with extra characters
pages := len(cg.data) / cg.pageSize
cg.page = page % pages
}
func (cg *CharacterGenerator) getPage() int {
return cg.page
}
func (cg *CharacterGenerator) nextPage() {
cg.setPage(cg.page + 1)
}
func (cg *CharacterGenerator) getPixel(char uint8, row int, column int) bool {
bits := cg.data[int(char)*8+row]
bit := bits >> (uint(6 - column)) & 1
return bit == 1
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) dumpCharFast(char uint8) {
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 := 6; j >= 0; 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() {
for i := 0; i < 256; i++ {
cg.dumpChar(uint8(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++
}
}

120
component/microPD1990ac.go Normal file
View File

@ -0,0 +1,120 @@
package component
import (
"time"
)
/*
microPD1990ac Serial I/O Calendar Clock IC
See:
https://www.semiee.com/file/backup/NEC-D1990.pdf
Used by the ThunderClock+ real time clock card.
The 40 bit register has 5 bytes (10 nibbles):
byte 4:
month, binary from 1 to 12
day of week, BCD 0 to 6
byte 3: day of month, BCD 1 to 31
byte 2: hour, BCD 0 to 23
byte 1: minute, BCD 0 to 59
byte 0: seconds, BCD 0 to 59
*/
type MicroPD1990ac struct {
clock bool // CLK state
strobe bool // STB state
command uint8 // C0, C1, C2 command. From 0 to 7
register uint64 // 40 bit shift register
}
const (
mpd1990commandRegHold = 0
mpd1990commandRegShift = 1
mpd1990commandTimeSet = 2
mpd1990commandTimeRead = 3
)
func (m *MicroPD1990ac) In(clock bool, strobe bool, command uint8, dataIn bool) {
// Detect signal raise
clockRaise := clock && !m.clock
strobeRaise := strobe && !m.strobe
// Update signal status
m.clock = clock
m.strobe = strobe
// On strobe raise, update command and execute if needed
if strobeRaise {
m.command = command
switch m.command {
case mpd1990commandRegShift:
// Nothing to do
case mpd1990commandTimeRead:
m.loadTime()
default:
// Ignore unknown commands (like set time)
//panic(fmt.Sprintf("PD1990ac command %v not implemented.", m.command))
}
}
// On clock raise, with shift enable, shift the register
if clockRaise && m.command == mpd1990commandRegShift {
// Rotate right the 40 bits of the shift register
lsb := m.register & 1
m.register >>= 1
m.register += lsb << 39
}
}
func (m *MicroPD1990ac) Out() bool {
/*
if m.command == mpd1990commandRegHold {
panic("Output on RegHold should be a 1Hz signal. Not implemented.")
}
if m.command == mpd1990commandTimeRead {
panic("Output on RegHold should be a 512Hz signal with LSB. Not implemented.")
}
*/
// Return the LSB of the register shift
return (m.register & 1) == 1
}
func (m *MicroPD1990ac) loadTime() {
now := time.Now()
var register uint64
register = uint64(now.Month())
register <<= 4
register += uint64(now.Weekday())
day := uint64(now.Day())
register <<= 4
register += day / 10
register <<= 4
register += day % 10
hour := uint64(now.Hour())
register <<= 4
register += hour / 10
register <<= 4
register += hour % 10
minute := uint64(now.Minute())
register <<= 4
register += minute / 10
register <<= 4
register += minute % 10
second := uint64(now.Second())
register <<= 4
register += second / 10
register <<= 4
register += second % 10
m.register = register
}

35
component/pins.go Normal file
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,32 +0,0 @@
package core6502
import (
"fmt"
"testing"
)
func TestFunctional(t *testing.T) {
m := new(FlatMemory)
s := NewNMOS6502(m)
// Test suite from https://github.com/Klaus2m5/6502_65C02_functional_tests
m.loadBinary("testdata/6502_functional_test.bin")
s.reg.setPC(0x0400)
for true {
testCase := s.mem.Peek(0x0200)
if testCase >= 240 {
break
}
log := testCase > 43
if log {
fmt.Printf("[ %d ] ", testCase)
}
pc := s.reg.getPC()
s.ExecuteInstruction(log)
if pc == s.reg.getPC() {
t.Errorf("Failure in test %v.", testCase)
}
}
}

View File

@ -1,103 +0,0 @@
package core6502
const (
modeImplicit = iota + 1
modeImplicitX
modeImplicitY
modeAccumulator
modeImmediate
modeZeroPage
modeZeroPageX
modeZeroPageY
modeRelative
modeAbsolute
modeAbsoluteX
modeAbsoluteY
modeIndirect
modeIndexedIndirectX
modeIndirectIndexedY
)
func getWordInLine(line []uint8) uint16 {
return uint16(line[1]) + 0x100*uint16(line[2])
}
func resolveValue(s *State, line []uint8, opcode opcode) uint8 {
getValue, _, _ := resolve(s, line, opcode)
return getValue()
}
func resolveGetSetValue(s *State, line []uint8, opcode opcode) (value uint8, setValue func(uint8)) {
getValue, setValue, _ := resolve(s, line, opcode)
value = getValue()
return
}
func resolveSetValue(s *State, line []uint8, opcode opcode) func(uint8) {
_, setValue, _ := resolve(s, line, opcode)
return setValue
}
func resolveAddress(s *State, line []uint8, opcode opcode) uint16 {
_, _, address := resolve(s, line, opcode)
return address
}
func resolve(s *State, line []uint8, opcode opcode) (getValue func() uint8, setValue func(uint8), address uint16) {
hasAddress := true
register := regNone
switch opcode.addressMode {
case modeAccumulator:
getValue = func() uint8 { return s.reg.getA() }
hasAddress = false
register = regA
case modeImplicitX:
getValue = func() uint8 { return s.reg.getX() }
hasAddress = false
register = regX
case modeImplicitY:
getValue = func() uint8 { return s.reg.getY() }
hasAddress = false
register = regY
case modeImmediate:
getValue = func() uint8 { return line[1] }
hasAddress = false
case modeZeroPage:
address = uint16(line[1])
case modeZeroPageX:
address = uint16(line[1] + s.reg.getX())
case modeZeroPageY:
address = uint16(line[1] + s.reg.getY())
case modeAbsolute:
address = getWordInLine(line)
case modeAbsoluteX:
address = getWordInLine(line) + uint16(s.reg.getX())
case modeAbsoluteY:
address = getWordInLine(line) + uint16(s.reg.getY())
case modeIndexedIndirectX:
addressAddress := uint8(line[1] + s.reg.getX())
address = getZeroPageWord(s.mem, addressAddress)
case modeIndirect:
addressAddress := getWordInLine(line)
address = getWord(s.mem, addressAddress)
case modeIndirectIndexedY:
address = getZeroPageWord(s.mem, line[1]) +
uint16(s.reg.getY())
}
if hasAddress {
getValue = func() uint8 { return s.mem.Peek(address) }
}
setValue = func(value uint8) {
if hasAddress {
s.mem.Poke(address, value)
} else if register != regNone {
s.reg.setRegister(register, value)
} else {
// Todo: assert impossible
}
}
return
}

View File

@ -1,132 +0,0 @@
package core6502
import (
"encoding/binary"
"fmt"
"io"
)
// https://www.masswerk.at/6502/6502_instruction_set.html
// http://www.emulator101.com/reference/6502-reference.html
// https://www.csh.rit.edu/~moffitt/docs/6502.html#FLAGS
// https://ia800509.us.archive.org/18/items/Programming_the_6502/Programming_the_6502.pdf
// State represents the state of the simulated device
type State struct {
reg registers
mem Memory
cycles uint64
opcodes *[256]opcode
}
const (
vectorNMI uint16 = 0xfffa
vectorReset uint16 = 0xfffc
vectorBreak uint16 = 0xfffe
)
type opcode struct {
name string
bytes uint8
cycles int
addressMode int
action opFunc
}
type opFunc func(s *State, line []uint8, opcode opcode)
func (s *State) executeLine(line []uint8) {
opcode := s.opcodes[line[0]]
if opcode.cycles == 0 {
panic(fmt.Sprintf("Unknown opcode 0x%02x\n", line[0]))
}
opcode.action(s, line, opcode)
}
// ExecuteInstruction transforms the state given after a single instruction is executed.
func (s *State) ExecuteInstruction(log bool) {
pc := s.reg.getPC()
opcodeID := s.mem.Peek(pc)
opcode := s.opcodes[opcodeID]
if opcode.cycles == 0 {
panic(fmt.Sprintf("Unknown opcode 0x%02x\n", opcodeID))
}
line := make([]uint8, opcode.bytes)
for i := uint8(0); i < opcode.bytes; i++ {
line[i] = s.mem.Peek(pc)
pc++
}
s.reg.setPC(pc)
if log {
fmt.Printf("%#04x %-12s: ", pc, lineString(line, opcode))
}
opcode.action(s, line, opcode)
s.cycles += uint64(opcode.cycles)
if log {
fmt.Printf("%v, [%02x]\n", s.reg, line)
}
}
// Reset resets the processor state. Moves the program counter to the vector in 0cfffc.
func (s *State) Reset() {
startAddress := getWord(s.mem, vectorReset)
s.cycles = 0
s.reg.setPC(startAddress)
}
// GetCycles returns the count of CPU cycles since last reset.
func (s *State) GetCycles() uint64 {
return s.cycles
}
// Save saves the CPU state (registers and cycle counter)
func (s *State) Save(w io.Writer) {
binary.Write(w, binary.BigEndian, s.cycles)
binary.Write(w, binary.BigEndian, s.reg.data)
}
// Load loads the CPU state (registers and cycle counter)
func (s *State) Load(r io.Reader) {
binary.Read(r, binary.BigEndian, &s.cycles)
binary.Read(r, binary.BigEndian, &s.reg.data)
}
func lineString(line []uint8, opcode opcode) string {
t := opcode.name
switch opcode.addressMode {
case modeImplicit:
case modeImplicitX:
case modeImplicitY:
//Nothing
case modeAccumulator:
t += fmt.Sprintf(" A")
case modeImmediate:
t += fmt.Sprintf(" #%02x", line[1])
case modeZeroPage:
t += fmt.Sprintf(" $%02x", line[1])
case modeZeroPageX:
t += fmt.Sprintf(" $%02x,X", line[1])
case modeZeroPageY:
t += fmt.Sprintf(" $%02x,Y", line[1])
case modeRelative:
t += fmt.Sprintf(" *%+x", int8(line[1]))
case modeAbsolute:
t += fmt.Sprintf(" $%04x", getWordInLine(line))
case modeAbsoluteX:
t += fmt.Sprintf(" $%04x,X", getWordInLine(line))
case modeAbsoluteY:
t += fmt.Sprintf(" $%04x,Y", getWordInLine(line))
case modeIndirect:
t += fmt.Sprintf(" ($%04x)", getWordInLine(line))
case modeIndexedIndirectX:
t += fmt.Sprintf(" ($%02x,X)", line[1])
case modeIndirectIndexedY:
t += fmt.Sprintf(" ($%02x),Y", line[1])
default:
t += "UNKNOWN MODE"
}
return t
}

View File

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

View File

@ -1,43 +0,0 @@
package core6502
import "io/ioutil"
// Memory represents the addressable space of the processor
type Memory interface {
Peek(address uint16) uint8
Poke(address uint16, value uint8)
}
func getWord(m Memory, address uint16) uint16 {
return uint16(m.Peek(address)) + 0x100*uint16(m.Peek(address+1))
}
func getZeroPageWord(m Memory, address uint8) uint16 {
return uint16(m.Peek(uint16(address))) + 0x100*uint16(m.Peek(uint16(address+1)))
}
// FlatMemory puts RAM on the 64Kb addressable by the processor
type FlatMemory struct {
data [65536]uint8
}
// Peek returns the data on the given address
func (m *FlatMemory) Peek(address uint16) uint8 {
return m.data[address]
}
// Poke sets the data at the given address
func (m *FlatMemory) Poke(address uint16, value uint8) {
m.data[address] = value
}
func (m *FlatMemory) loadBinary(filename string) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
for i, v := range bytes {
m.Poke(uint16(i), uint8(v))
}
}

View File

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

View File

@ -1,242 +0,0 @@
package core6502
func buildOpTransfer(regSrc int, regDst int) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := s.reg.getRegister(regSrc)
s.reg.setRegister(regDst, value)
if regDst != regSP {
s.reg.updateFlagZN(value)
}
}
}
func buildOpIncDec(inc bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value, setValue := resolveGetSetValue(s, line, opcode)
if inc {
value++
} else {
value--
}
s.reg.updateFlagZN(value)
setValue(value)
}
}
func buildOpShift(isLeft bool, isRotate bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value, setValue := resolveGetSetValue(s, line, opcode)
oldCarry := s.reg.getFlagBit(flagC)
var carry bool
if isLeft {
carry = (value & 0x80) != 0
value <<= 1
if isRotate {
value += oldCarry
}
} else {
carry = (value & 0x01) != 0
value >>= 1
if isRotate {
value += oldCarry << 7
}
}
s.reg.updateFlag(flagC, carry)
s.reg.updateFlagZN(value)
setValue(value)
}
}
func buildOpLoad(regDst int) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
s.reg.setRegister(regDst, value)
s.reg.updateFlagZN(value)
}
}
func buildOpStore(regSrc int) opFunc {
return func(s *State, line []uint8, opcode opcode) {
setValue := resolveSetValue(s, line, opcode)
value := s.reg.getRegister(regSrc)
setValue(value)
}
}
func buildOpUpdateFlag(flag uint8, value bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
s.reg.updateFlag(flag, value)
}
}
func buildOpBranch(flag uint8, value bool) opFunc {
return func(s *State, line []uint8, opcode opcode) {
if s.reg.getFlag(flag) == value {
// This assumes that PC is already pointing to the next instruction
pc := s.reg.getPC()
pc += uint16(int8(line[1]))
s.reg.setPC(pc)
}
}
}
func opBIT(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
acc := s.reg.getA()
// Future note: The immediate addressing mode (65C02 or 65816 only) does not affect V.
s.reg.updateFlag(flagZ, value&acc == 0)
s.reg.updateFlag(flagN, value&(1<<7) != 0)
s.reg.updateFlag(flagV, value&(1<<6) != 0)
}
func buildOpCompare(reg int) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
reference := s.reg.getRegister(reg)
s.reg.updateFlagZN(reference - value)
s.reg.updateFlag(flagC, reference >= value)
}
}
func operationAnd(a uint8, b uint8) uint8 { return a & b }
func operationOr(a uint8, b uint8) uint8 { return a | b }
func operationXor(a uint8, b uint8) uint8 { return a ^ b }
func buildOpLogic(operation func(uint8, uint8) uint8) opFunc {
return func(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
result := operation(value, s.reg.getA())
s.reg.setA(result)
s.reg.updateFlagZN(result)
}
}
func opADC(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
aValue := s.reg.getA()
carry := s.reg.getFlagBit(flagC)
total := uint16(aValue) + uint16(value) + uint16(carry)
signedTotal := int16(int8(aValue)) + int16(int8(value)) + int16(carry)
truncated := uint8(total)
if s.reg.getFlag(flagD) {
totalBcdLo := int(aValue&0x0f) + int(value&0x0f) + int(carry)
totalBcdHi := int(aValue>>4) + int(value>>4)
if totalBcdLo >= 10 {
totalBcdHi++
}
totalBcd := (totalBcdHi%10)<<4 + (totalBcdLo % 10)
s.reg.setA(uint8(totalBcd))
s.reg.updateFlag(flagC, totalBcdHi > 9)
} else {
s.reg.setA(truncated)
s.reg.updateFlag(flagC, total > 0xFF)
}
// ZNV flags behave for BCD as if the operation was binary?
s.reg.updateFlagZN(truncated)
s.reg.updateFlag(flagV, signedTotal < -128 || signedTotal > 127)
}
func opSBC(s *State, line []uint8, opcode opcode) {
value := resolveValue(s, line, opcode)
aValue := s.reg.getA()
carry := s.reg.getFlagBit(flagC)
total := 0x100 + uint16(aValue) - uint16(value) + uint16(carry) - 1
signedTotal := int16(int8(aValue)) - int16(int8(value)) + int16(carry) - 1
truncated := uint8(total)
if s.reg.getFlag(flagD) {
totalBcdLo := 10 + int(aValue&0x0f) - int(value&0x0f) + int(carry) - 1
totalBcdHi := 10 + int(aValue>>4) - int(value>>4)
if totalBcdLo < 10 {
totalBcdHi--
}
totalBcd := (totalBcdHi%10)<<4 + (totalBcdLo % 10)
s.reg.setA(uint8(totalBcd))
s.reg.updateFlag(flagC, totalBcdHi >= 10)
} else {
s.reg.setA(truncated)
s.reg.updateFlag(flagC, total > 0xFF)
}
// ZNV flags behave for SBC as if the operation was binary
s.reg.updateFlagZN(truncated)
s.reg.updateFlag(flagV, signedTotal < -128 || signedTotal > 127)
}
const stackAddress uint16 = 0x0100
func pushByte(s *State, value uint8) {
adresss := stackAddress + uint16(s.reg.getSP())
s.mem.Poke(adresss, value)
s.reg.setSP(s.reg.getSP() - 1)
}
func pullByte(s *State) uint8 {
s.reg.setSP(s.reg.getSP() + 1)
adresss := stackAddress + uint16(s.reg.getSP())
return s.mem.Peek(adresss)
}
func pushWord(s *State, value uint16) {
pushByte(s, uint8(value>>8))
pushByte(s, uint8(value))
}
func pullWord(s *State) uint16 {
return uint16(pullByte(s)) +
(uint16(pullByte(s)) << 8)
}
func opPLA(s *State, line []uint8, opcode opcode) {
value := pullByte(s)
s.reg.setA(value)
s.reg.updateFlagZN(value)
}
func opPLP(s *State, line []uint8, opcode opcode) {
value := pullByte(s)
s.reg.setP(value)
}
func opPHA(s *State, line []uint8, opcode opcode) {
pushByte(s, s.reg.getA())
}
func opPHP(s *State, line []uint8, opcode opcode) {
pushByte(s, s.reg.getP()|(flagB+flag5))
}
func opJMP(s *State, line []uint8, opcode opcode) {
address := resolveAddress(s, line, opcode)
s.reg.setPC(address)
}
func opNOP(s *State, line []uint8, opcode opcode) {}
func opJSR(s *State, line []uint8, opcode opcode) {
pushWord(s, s.reg.getPC()-1)
address := resolveAddress(s, line, opcode)
s.reg.setPC(address)
}
func opRTI(s *State, line []uint8, opcode opcode) {
s.reg.setP(pullByte(s))
s.reg.setPC(pullWord(s))
}
func opRTS(s *State, line []uint8, opcode opcode) {
s.reg.setPC(pullWord(s) + 1)
}
func opBRK(s *State, line []uint8, opcode opcode) {
pushWord(s, s.reg.getPC()+1)
pushByte(s, s.reg.getP()|(flagB+flag5))
s.reg.setFlag(flagI)
s.reg.setPC(getWord(s.mem, vectorBreak))
}

View File

@ -1,92 +0,0 @@
package core6502
import "fmt"
const (
regA = 0
regX = 1
regY = 2
regP = 3
regSP = 4
regPC = 5 // 2 bytes
regPC2 = 6
regNone = -1
)
const (
flagN uint8 = 1 << 7
flagV uint8 = 1 << 6
flag5 uint8 = 1 << 5
flagB uint8 = 1 << 4
flagD uint8 = 1 << 3
flagI uint8 = 1 << 2
flagZ uint8 = 1 << 1
flagC uint8 = 1 << 0
)
type registers struct {
data [7]uint8
}
func (r *registers) getRegister(i int) uint8 { return r.data[i] }
func (r *registers) getA() uint8 { return r.data[regA] }
func (r *registers) getX() uint8 { return r.data[regX] }
func (r *registers) getY() uint8 { return r.data[regY] }
func (r *registers) getP() uint8 { return r.data[regP] }
func (r *registers) getSP() uint8 { return r.data[regSP] }
func (r *registers) setRegister(i int, v uint8) {
r.data[i] = v
}
func (r *registers) setA(v uint8) { r.setRegister(regA, v) }
func (r *registers) setX(v uint8) { r.setRegister(regX, v) }
func (r *registers) setY(v uint8) { r.setRegister(regY, v) }
func (r *registers) setP(v uint8) { r.setRegister(regP, v) }
func (r *registers) setSP(v uint8) { r.setRegister(regSP, v) }
func (r *registers) getPC() uint16 {
return uint16(r.data[regPC])*256 + uint16(r.data[regPC2])
}
func (r *registers) setPC(v uint16) {
r.data[regPC] = uint8(v >> 8)
r.data[regPC2] = uint8(v)
}
func (r *registers) getFlagBit(i uint8) uint8 {
if r.getFlag(i) {
return 1
}
return 0
}
func (r *registers) getFlag(i uint8) bool {
return (r.data[regP] & i) != 0
}
func (r *registers) setFlag(i uint8) {
r.data[regP] |= i
}
func (r *registers) clearFlag(i uint8) {
r.data[regP] &^= i
}
func (r *registers) updateFlag(i uint8, v bool) {
if v {
r.setFlag(i)
} else {
r.clearFlag(i)
}
}
func (r *registers) updateFlagZN(t uint8) {
r.updateFlag(flagZ, t == 0)
r.updateFlag(flagN, t >= (1<<7))
}
func (r registers) String() string {
return fmt.Sprintf("A: %#02x, X: %#02x, Y: %#02x, SP: %#02x, PC: %#04x, P: %#02x, (NV-BDIZC): %08b",
r.getA(), r.getX(), r.getY(), r.getSP(), r.getPC(), r.getP(), r.getP())
}

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

View File

@ -1,174 +0,0 @@
package apple2
import (
"os"
)
/*
See:
"Beneath Apple DOS" https://fabiensanglard.net/fd_proxy/prince_of_persia/Beneath%20Apple%20DOS.pdf
https://github.com/TomHarte/CLK/wiki/Apple-GCR-disk-encoding
*/
const (
numberOfTracks = 35
numberOfSectors = 16
bytesPerSector = 256
bytesPerTrack = numberOfSectors * bytesPerSector
nibBytesPerTrack = 6656
nibImageSize = numberOfTracks * nibBytesPerTrack
dskImageSize = numberOfTracks * numberOfSectors * bytesPerSector
defaultVolumeTag = 254
)
type diskette16sector struct {
track [numberOfTracks][]byte
}
func (d *diskette16sector) read(track int, position int) (value uint8, newPosition int) {
value = d.track[track][position]
newPosition = (position + 1) % nibBytesPerTrack
return
}
func (d *diskette16sector) write(track int, position int, value uint8) int {
d.track[track][position] = value
return (position + 1) % nibBytesPerTrack
}
func loadDisquette(filename string) *diskette16sector {
var d diskette16sector
data := loadResource(filename)
size := len(data)
if size == nibImageSize {
// Load file already in nib format
for i := 0; i < numberOfTracks; i++ {
d.track[i] = data[nibBytesPerTrack*i : nibBytesPerTrack*(i+1)]
}
} else if size == dskImageSize {
// Convert to nib
for i := 0; i < numberOfTracks; i++ {
trackData := data[i*bytesPerTrack : (i+1)*bytesPerTrack]
d.track[i] = nibEncodeTrack(trackData, defaultVolumeTag, byte(i))
}
} else {
panic("Invalid disk size")
}
return &d
}
func (d *diskette16sector) saveNib(filename string) {
f, err := os.Create(filename)
if err != nil {
panic(err)
}
defer f.Close()
for _, v := range d.track {
_, err := f.Write(v)
if err != nil {
panic(err)
}
}
}
var dos33SectorsLogicOrder = [16]int{
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF,
}
var sixAndTwoTranslateTable = [0x40]byte{
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
}
const (
gap1Len = 48
gap2Len = 5
primaryBufferSize = bytesPerSector
secondaryBufferSize = bytesPerSector/3 + 1
)
func oddEvenEncodeByte(b byte) []byte {
/*
A byte is encoded in two bytes to make sure the bytes start with 1 and
does not have two consecutive zeros.
Data byte: D7-D6-D5-D4-D3-D2-D1-D0
resutl[0]: 1-D7- 1-D5- 1-D3-1 -D1
resutl[1]: 1-D6- 1-D4- 1-D2-1 -D0
*/
e := make([]byte, 2)
e[0] = ((b >> 1) & 0x55) | 0xaa
e[1] = (b & 0x55) | 0xaa
return e
}
func nibEncodeTrack(data []byte, volume byte, track byte) []byte {
b := make([]byte, 0, nibBytesPerTrack) // Buffer slice with enough capacity
// Initialize gaps to be copied for each sector
gap1 := make([]byte, gap1Len)
for i := range gap1 {
gap1[i] = 0xff
}
gap2 := make([]byte, gap2Len)
for i := range gap2 {
gap2[i] = 0xff
}
for physicalSector := byte(0); physicalSector < numberOfSectors; physicalSector++ {
/* On the DSK file the sectors are in DOS3.3 logical order
but on the physical encoded track as well as in the nib
files they are in phisical order.
*/
logicalSector := dos33SectorsLogicOrder[physicalSector]
sectorData := data[logicalSector*bytesPerSector : (logicalSector+1)*bytesPerSector]
// 6and2 prenibbilizing.
primaryBuffer := make([]byte, primaryBufferSize)
secondaryBuffer := make([]byte, secondaryBufferSize)
for i, v := range sectorData {
// Primary buffer is easy: the 6 MSB
primaryBuffer[i] = v >> 2
// Secondary buffer: the 2 LSB reversed, shifted and in their place
shift := uint((i / secondaryBufferSize) * 2)
bit0 := ((v & 0x01) << 1) << shift
bit1 := ((v & 0x02) >> 1) << shift
position := i % secondaryBufferSize
secondaryBuffer[position] |= bit0 | bit1
}
// Render sector
// Address field
b = append(b, gap1...)
b = append(b, 0xd5, 0xaa, 0x96) // Address prolog
b = append(b, oddEvenEncodeByte(volume)...) // 4-4 encoded volume
b = append(b, oddEvenEncodeByte(track)...) // 4-4 encoded track
b = append(b, oddEvenEncodeByte(physicalSector)...) // 4-4 encoded sector
b = append(b, oddEvenEncodeByte(volume^track^physicalSector)...) // Checksum
b = append(b, 0xde, 0xaa, 0xeb) // Epilog
// Data field
b = append(b, gap2...)
b = append(b, 0xd5, 0xaa, 0xad) // Data prolog
prevV := byte(0)
for _, v := range secondaryBuffer {
b = append(b, sixAndTwoTranslateTable[v^prevV])
prevV = v
}
for _, v := range primaryBuffer {
b = append(b, sixAndTwoTranslateTable[v^prevV])
prevV = v
}
b = append(b, sixAndTwoTranslateTable[prevV]) // Checksum
b = append(b, 0xde, 0xaa, 0xeb) // Data epilog
}
return b
}

BIN
doc/dos33.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
doc/karateka.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
doc/totalreplay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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

2
dockerbuild/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build

18
dockerbuild/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM golang:1.18.0
LABEL MAINTAINER="Ivan Izaguirre <ivanizag@gmail.com>"
RUN apt-get update
RUN apt-get install -y libsdl2-dev mingw-w64 unzip
RUN wget https://www.libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz
RUN tar -xzf SDL2-devel-2.0.12-mingw.tar.gz
RUN cp -r SDL2-2.0.12/x86_64-w64-mingw32 /usr
RUN wget https://www.libsdl.org/release/SDL2-2.0.12-win32-x64.zip
RUN unzip SDL2-2.0.12-win32-x64.zip -d /sdl2runtime
COPY buildindocker.sh .
RUN chmod +x buildindocker.sh
CMD ["./buildindocker.sh"]

5
dockerbuild/build.sh Executable file
View File

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

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

@ -0,0 +1,49 @@
#!/bin/bash
cd /tmp
git clone https://github.com/ivanizag/izapple2
# Build izapple2console for Linux
echo "Building Linux console frontend"
cd /tmp/izapple2/frontend/console
env CGO_ENABLED=1 go build -tags static -ldflags "-s -w" .
chown --reference /build console
cp console /build/izapple2console_linux_amd64
# Build izapple2console.exe for Windows
echo "Building Windows console frontend"
cd /tmp/izapple2/frontend/console
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -tags static -ldflags "-s -w" -o izapple2console.exe .
chown --reference /build izapple2console.exe
cp izapple2console.exe /build/izapple2console_windows_amd64.exe
# Build izapple2sdl for Linux
echo "Building Linux SDL frontend"
cd /tmp/izapple2/frontend/a2sdl
env CGO_ENABLED=1 go build -tags static -ldflags "-s -w" .
chown --reference /build a2sdl
cp a2sdl /build/izapple2sdl_linux_amd64
# Build izapple2sdl.exe for Windows
echo "Building Windows SDL frontend"
cd /tmp/izapple2/frontend/a2sdl
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib -lSDL2" CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -tags static -ldflags "-s -w" -o izapple2sdl.exe .
chown --reference /build izapple2sdl.exe
cp izapple2sdl.exe /build/izapple2sdl_windows_amd64.exe
# Build izapple2fyne for Linux
echo "Building Linux Fyne frontend"
cd /tmp/izapple2/frontend/a2fyne
env CGO_ENABLED=1 go build -tags static -ldflags "-s -w" .
chown --reference /build a2fyne
cp a2fyne /build/izapple2fyne_linux_amd64
# Build izapple2fyne.exe for Windows
echo "Building Windows Fyne frontend"
cd /tmp/izapple2/frontend/a2fyne
env CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows CGO_LDFLAGS="-L/usr/x86_64-w64-mingw32/lib " CGO_FLAGS="-I/usr/x86_64-w64-mingw32/include -D_REENTRANT" go build -tags static -ldflags "-s -w" -o izapple2fyne.exe .
chown --reference /build izapple2fyne.exe
cp izapple2fyne.exe /build/izapple2fyne_windows_amd64.exe
# Copy SDL2 Runtime
cp /sdl2runtime/* /build

64
e2e_boot_test.go Normal file
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

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