1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-10-04 23:17:01 +00:00

Compare commits

...

166 Commits

Author SHA1 Message Date
Thomas Harte
7229acb34f Merge pull request #1596 from TomHarte/FaultyLineLength
Correct collation test, to ensure no accidental buffer mixing.
2025-10-03 18:06:34 -04:00
Thomas Harte
43cb91760a Update SAA5050 row counter only in teletext mode. 2025-10-03 18:05:36 -04:00
Thomas Harte
7bb4d052d1 Correct collation test, to ensure no accidental buffer mixing. 2025-10-03 17:29:45 -04:00
Thomas Harte
5885bdf0f8 Merge pull request #1595 from TomHarte/BBCSaving
Ensure 1770 doesn't get stuck when writing.
2025-10-03 16:51:24 -04:00
Thomas Harte
05042b1859 Remove unnecessary log. 2025-10-03 16:42:04 -04:00
Thomas Harte
3ca2f72184 Merge pull request #1594 from TomHarte/CompoundingTyper
Simplify typer logic.
2025-10-03 16:41:43 -04:00
Thomas Harte
b076450b73 Ensure 1770 doesn't get stuck when writing. 2025-10-03 16:39:27 -04:00
Thomas Harte
7a90662c06 Merge branch 'master' into CompoundingTyper 2025-10-03 16:06:47 -04:00
Thomas Harte
eb31aaeb7d Merge pull request #1593 from TomHarte/FakeLightpen
Support CB2 output strobe as triggering lightpen capture.
2025-10-03 16:05:11 -04:00
Thomas Harte
ebfb215246 Support CB2 output strobe as triggering lightpen capture. 2025-10-03 15:39:16 -04:00
Thomas Harte
eb97e4e518 Reserve entire FF page; simplify logic. 2025-10-03 13:10:45 -04:00
Thomas Harte
61d3e65c05 Merge pull request #1591 from TomHarte/BetterTyper
BBC typer: properly support lowercase input.
2025-10-03 12:27:32 -04:00
Thomas Harte
0ac5681d13 Confirmed: 'capslock' has yet to become a single word. 2025-10-03 11:37:39 -04:00
Thomas Harte
1e27c5759b Add missing const. 2025-10-03 09:26:53 -04:00
Thomas Harte
5e71aedc99 Support lowercase typing into the BBC. 2025-10-03 09:25:58 -04:00
Thomas Harte
d0d8c2316b Bump version number. 2025-10-02 22:55:50 -04:00
Thomas Harte
8f0a5b2191 Merge pull request #1589 from TomHarte/6522PB7
Expose PB7 timer regardless.
2025-10-02 22:48:31 -04:00
Thomas Harte
242b180862 Expose PB7 timer regardless. 2025-10-02 22:40:36 -04:00
Thomas Harte
feb4766d7b Merge pull request #1587 from TomHarte/BetterAnalogue
Improve mapping of digital inputs to analogue joysticks.
2025-10-02 21:22:21 -04:00
Thomas Harte
a224eea077 Merge pull request #1586 from TomHarte/SAA5050Split
Make centre row the thick one.
2025-10-02 20:59:55 -04:00
Thomas Harte
ecefbc23ae Resolve ability of analogue joysticks to get stuck with digital input. 2025-10-02 20:52:16 -04:00
Thomas Harte
e5e0cbfc53 Make centre row the thick one. 2025-10-02 20:35:53 -04:00
Thomas Harte
38781c9395 Merge pull request #1585 from TomHarte/BBCJoystick
Add BBC joysticks.
2025-10-02 20:21:31 -04:00
Thomas Harte
e70d72b614 Merge pull request #1584 from TomHarte/CursorMask
Variously tweak and improve BBC graphics infrastructure.
2025-10-02 17:44:10 -04:00
Thomas Harte
fcf648bbb2 Flip axes, maximise range. 2025-10-02 17:43:33 -04:00
Thomas Harte
a8325b6bce Add BBC joysticks. 2025-10-02 17:10:27 -04:00
Thomas Harte
22554a9ba4 Incorporate a one-column delay into CPC output. 2025-10-02 15:22:01 -04:00
Thomas Harte
02a10ef651 Fall in line with nonsense. 2025-10-02 15:11:01 -04:00
Thomas Harte
e3ca44f3ca Reseparate pixels. 2025-10-02 13:24:00 -04:00
Thomas Harte
a9abc0dd5f Document a little further. 2025-10-02 09:30:33 -04:00
Thomas Harte
cbcc7c718e SAA: smooth output just in time. 2025-10-02 09:20:58 -04:00
Thomas Harte
4377c79068 Switch blink rates. 2025-10-02 09:10:12 -04:00
Thomas Harte
514993bc2e Pull out cursor mask as a separate concern. 2025-10-02 07:56:07 -04:00
Thomas Harte
c182176134 Merge pull request #1583 from TomHarte/SizedInt
Rename SizedCounter.
2025-10-01 22:23:51 -04:00
Thomas Harte
abd1f10395 Ensure <=> is implemented. 2025-10-01 22:01:35 -04:00
Thomas Harte
f279bebc1a Reduce redundant masking. 2025-10-01 21:59:40 -04:00
Thomas Harte
4e3fa5a6ff Use std::popcount. 2025-10-01 21:25:12 -04:00
Thomas Harte
01d355a247 Rename SizedCounter. 2025-10-01 20:58:34 -04:00
Thomas Harte
009f71a186 Update version number. 2025-09-30 21:52:53 -04:00
Thomas Harte
de5c311d84 Merge pull request #1582 from TomHarte/SAAFlash
Switch to asymmetric SAA flash interval.
2025-09-30 21:48:16 -04:00
Thomas Harte
eefe34f99e Merge branch 'master' into SAAFlash 2025-09-30 21:43:32 -04:00
Thomas Harte
82e3c870b3 Merge pull request #1581 from TomHarte/BetterKeyboardMap
BBC Micro: add character mapper, other improvements.
2025-09-30 21:43:15 -04:00
Thomas Harte
d44a1d9761 Give SAA flashing an asymmetric appearance. 2025-09-30 21:33:37 -04:00
Thomas Harte
e6fd54c14b Correct forward slash key. 2025-09-30 21:33:15 -04:00
Thomas Harte
ccb8e90110 Improve naming. 2025-09-30 21:27:34 -04:00
Thomas Harte
a4e66f291a Avoid ambiguity with new DelaySlot 'key'. 2025-09-30 21:23:30 -04:00
Thomas Harte
3ab3f34bef If there's only one BASIC file, CHAIN that. 2025-09-30 21:13:38 -04:00
Thomas Harte
99da8c4424 Avoid assuming 0 is not a valid key. 2025-09-30 21:07:02 -04:00
Thomas Harte
9a1bf1cf74 Reduce delay. 2025-09-30 21:06:56 -04:00
Thomas Harte
2256e99157 Attempt to add a typer. 2025-09-30 20:57:28 -04:00
Thomas Harte
3a5f8b4987 Fill in rest of character mapper. 2025-09-29 23:02:51 -04:00
Thomas Harte
6de5fcc980 Simplify test. 2025-09-29 22:52:56 -04:00
Thomas Harte
a454d0d4b7 Begin work on character mapper. 2025-09-29 22:52:48 -04:00
Thomas Harte
67339754e3 Resolve potential crash at startup. 2025-09-29 16:13:56 -04:00
Thomas Harte
7316fe00ee Support native blink speeds. 2025-09-29 16:13:39 -04:00
Thomas Harte
feb4a7021c Add enum of BBC key names. 2025-09-29 15:00:56 -04:00
Thomas Harte
5e84b671b6 Merge pull request #1580 from TomHarte/BBCReadme
Add BBC Micro to README.
2025-09-28 22:32:20 -04:00
Thomas Harte
bcefef62f9 Add BBC Micro to README. 2025-09-28 22:30:06 -04:00
Thomas Harte
cc953dda34 Merge pull request #1579 from TomHarte/EliteInterrupts
6522: avoid handshaking with register F.
2025-09-28 22:25:41 -04:00
Thomas Harte
f9e5b0f0c7 6522: avoid handshaking with register F. 2025-09-28 22:12:53 -04:00
Thomas Harte
578654411e Merge pull request #1578 from TomHarte/CapsLock
Add meta as a synonym of control, option as caps lock.
2025-09-27 22:38:43 -04:00
Thomas Harte
247e92cfa2 Correct mapping of F5. 2025-09-27 22:27:57 -04:00
Thomas Harte
66f605de0f Add meta as a synonym of control, option as caps lock. 2025-09-27 22:03:27 -04:00
Thomas Harte
709b0efc9b Merge pull request #1577 from TomHarte/BBCAnalyser
Poke around trying to find a way to discern BBC and Electron software.
2025-09-27 08:17:57 -04:00
Thomas Harte
622679f4c2 Slow flash rate (though it's probably asymmetrical?) 2025-09-27 07:58:11 -04:00
Thomas Harte
fdeb421513 Prefer the BBC for DFS media. 2025-09-27 07:49:42 -04:00
Thomas Harte
8fe25cde8d Add search for 'MODE7'. 2025-09-27 07:41:21 -04:00
Thomas Harte
fbd71451f1 Enable sideways RAM by default. 2025-09-26 22:00:40 -04:00
Thomas Harte
0d91ce8e6a Add some Electron addresses. 2025-09-26 21:29:49 -04:00
Thomas Harte
d71796c88a Support automatic disk starting. 2025-09-26 15:55:04 -04:00
Thomas Harte
277748c8f5 Install a basic search for CRTC/etc addresses. 2025-09-26 15:52:20 -04:00
Thomas Harte
8c1358ace9 Generate a BBC target, even though not yet exposed. 2025-09-26 15:37:32 -04:00
Thomas Harte
1254916058 Merge pull request #1576 from TomHarte/ReadmitBBC
Permit the BBC to appear in release builds.
2025-09-26 13:51:16 -04:00
Thomas Harte
f228bee4b8 Permit the BBC to appear in release builds. 2025-09-26 13:49:19 -04:00
Thomas Harte
8094477b09 Merge pull request #1575 from TomHarte/CropFactor
Introduce a crop, centred on the pixel area.
2025-09-26 12:42:43 -04:00
Thomas Harte
32a5bf76cd Introduce a crop, centred on the pixel area. 2025-09-26 12:06:44 -04:00
Thomas Harte
0375d000e6 Merge pull request #1574 from TomHarte/SAA5050
Add SAA5050 and hence BBC Mode 7.
2025-09-25 23:12:28 -04:00
Thomas Harte
141d43d3e5 Further express smoothing in terms of pixel patterns. 2025-09-25 23:02:33 -04:00
Thomas Harte
823f7b1d2e Attempt held graphics. 2025-09-25 22:47:13 -04:00
Thomas Harte
6579f011d0 Support flash and conceal. 2025-09-25 22:37:38 -04:00
Thomas Harte
93f768af9b Bump control codes up in the roster. 2025-09-25 22:18:52 -04:00
Thomas Harte
f8c11bf217 Rejig to ensure SAA output ends. 2025-09-25 21:31:21 -04:00
Thomas Harte
26ccd930c3 Begin tidying. 2025-09-25 17:53:54 -04:00
Thomas Harte
82211c7312 Add some 'graphics' support. 2025-09-25 17:50:26 -04:00
Thomas Harte
2015c154fe Correctly clear double-height flags. 2025-09-25 13:28:22 -04:00
Thomas Harte
ef17d116a8 Don't permit single-height text on a lower double-height row. 2025-09-25 13:22:25 -04:00
Thomas Harte
46fddc44bf Support double-height text. 2025-09-25 13:21:49 -04:00
Thomas Harte
0214a77cd7 Add TODO. 2025-09-25 13:10:52 -04:00
Thomas Harte
425ed658f1 Support colour control codes, clarify SAA5050 signalling. 2025-09-25 13:03:55 -04:00
Thomas Harte
a53adb561e Erase TODO, continue to update state without target. 2025-09-25 09:25:46 -04:00
Thomas Harte
3c3c55090a Port forward ElectrEm's font smoothing. 2025-09-25 09:22:16 -04:00
Thomas Harte
ebc04c6520 Eliminate warning. 2025-09-24 22:58:50 -04:00
Thomas Harte
8b0e8f5b13 Move all work [near] definitively into the SAA5050. 2025-09-24 22:55:49 -04:00
Thomas Harte
16132a007e Remove silly call. 2025-09-24 22:26:37 -04:00
Thomas Harte
b6e41ceea7 Hack in low-resolution Mode 7. 2025-09-24 22:25:43 -04:00
Thomas Harte
7015e46227 Put together enough of an interface to expect to see some pixels. 2025-09-24 22:08:04 -04:00
Thomas Harte
cce2607c80 Add file for SAA5050 logic. 2025-09-24 21:43:25 -04:00
Thomas Harte
9dd2ec8bda Merge pull request #1573 from TomHarte/New6845
Improve 6845.
2025-09-24 21:36:16 -04:00
Thomas Harte
068726e0ab Add TODO. 2025-09-24 21:26:04 -04:00
Thomas Harte
89e86ad9bd Delay publication of the refresh address. 2025-09-24 21:20:20 -04:00
Thomas Harte
2e49bc2044 Add teletext pixel route, albeit without proper selection. 2025-09-24 20:33:07 -04:00
Thomas Harte
174c8dafbf Resolve potential out-of-phase line counter. 2025-09-24 17:26:40 -04:00
Thomas Harte
90a96293de Implement interlace-dependent row addressing. 2025-09-24 17:20:04 -04:00
Thomas Harte
84877c4fec Reenable the cursor; good enough for now. 2025-09-24 14:37:52 -04:00
Thomas Harte
a7cceb5fa9 Avoid circular state dependency. 2025-09-24 14:30:37 -04:00
Thomas Harte
ca6359a597 Reintroduce pixels, proving myself to be off-by-one. 2025-09-24 14:29:25 -04:00
Thomas Harte
b7c3667be1 Work out inadvertent discrepancies. 2025-09-24 14:11:06 -04:00
Thomas Harte
b6dea59db3 This tests lines, not rows. 2025-09-24 13:56:16 -04:00
Thomas Harte
aa51f13743 Reorder to avoid dependencies upon values that mutate. 2025-09-24 13:54:09 -04:00
Thomas Harte
f34ec03ff0 Attempt to fix off-by-one; adopt fixed pixel pattern. 2025-09-24 13:42:17 -04:00
Thomas Harte
1363be59b7 Formalise field size. 2025-09-24 11:17:47 -04:00
Thomas Harte
622c24ef24 This indicates a line, not a row. 2025-09-23 22:36:56 -04:00
Thomas Harte
539b0e49d4 Start in mode 7, reallow interlaced modes. 2025-09-23 14:45:32 -04:00
Thomas Harte
0c42976312 Add notes to self. 2025-09-23 14:42:16 -04:00
Thomas Harte
3f6b3a4fa0 Don't allow a state to be permanently accumulated. 2025-09-23 14:41:59 -04:00
Thomas Harte
67e1773495 This flag covers rows, not lines. 2025-09-23 14:29:00 -04:00
Thomas Harte
a199b64aa0 Clarify naming, attempt better to conform to FPGA precedent. 2025-09-23 14:27:21 -04:00
Thomas Harte
ebf09aceb2 Further extend. This is becoming more of a SizedInt. 2025-09-23 14:26:58 -04:00
Thomas Harte
ca226e4295 Merge branch 'master' into New6845 2025-09-22 13:28:33 -04:00
Thomas Harte
9261939f62 Switch to working PC for testing. 2025-09-22 13:24:35 -04:00
Thomas Harte
0349931953 Shuffle declare order. 2025-09-22 13:21:48 -04:00
Thomas Harte
d612a385d2 Dig in further on types. 2025-09-22 13:20:10 -04:00
Thomas Harte
ed4f299d55 Start formalising types. 2025-09-22 13:09:30 -04:00
Thomas Harte
7cef789d41 Merge branch 'master' into New6845 2025-09-22 12:47:32 -04:00
Thomas Harte
66bfb86d42 Introduce SizedCounter as start of CRTC reworking. 2025-09-22 12:46:39 -04:00
Thomas Harte
c4a5bc12ef Merge pull request #1572 from TomHarte/BBCADFS
Support ADFS, sideways RAM.
2025-09-20 23:27:15 -04:00
Thomas Harte
557631f6ba Support ADFS, sideways RAM. 2025-09-20 22:33:08 -04:00
Thomas Harte
362ffaff7f Merge pull request #1571 from TomHarte/RandomPauses
Correct uPD7002 interrupt wiring and behaviour.
2025-09-20 22:08:13 -04:00
Thomas Harte
fb5ef200fb Correct uPD7002 interrupt wiring. 2025-09-20 21:51:19 -04:00
Thomas Harte
5e78ac3af5 Adjust keyboard map slightly. 2025-09-20 21:35:01 -04:00
Thomas Harte
719a090b34 Retain bit 2. 2025-09-20 20:06:28 -04:00
Thomas Harte
3af85da6e0 Adjust conversion bits in status. 2025-09-20 19:52:47 -04:00
Thomas Harte
8fd62aa525 Disable interrupt at start of conversion. 2025-09-20 19:49:16 -04:00
Thomas Harte
40747f51bd Disable ADC interrupt, experimentally. 2025-09-20 17:41:22 -04:00
Thomas Harte
f3cef6bd73 Merge pull request #1570 from TomHarte/BBCCursor
Add BBC cursor.
2025-09-20 08:42:53 -04:00
Thomas Harte
eef0ee8180 Support cursor to end of row. 2025-09-20 08:27:58 -04:00
Thomas Harte
503e974375 Restrict cursor to visible area, fix width. 2025-09-20 08:15:02 -04:00
Thomas Harte
c959f2fee5 Attempt to show the hardware cursor. 2025-09-20 07:54:37 -04:00
Thomas Harte
7d5e434cba Merge pull request #1569 from TomHarte/BBCActivityIndicators
Add activity indicators.
2025-09-19 23:51:18 -04:00
Thomas Harte
2720bcdf18 Retrench to static inline const. 2025-09-19 23:40:30 -04:00
Thomas Harte
c513b7262b Hit up two further strings for constexpr. 2025-09-19 23:37:11 -04:00
Thomas Harte
57a795df96 Add keyboard LEDs. 2025-09-19 23:34:51 -04:00
Thomas Harte
6bdd9e4543 Add drive activity indicators. 2025-09-19 23:26:50 -04:00
Thomas Harte
ede3def37f Merge pull request #1568 from TomHarte/BBC1770
Add 1770 DFS support.
2025-09-19 23:20:29 -04:00
Thomas Harte
87d9022280 Collapse operations. 2025-09-19 23:03:11 -04:00
Thomas Harte
ff0ba7d48b Reduce logging again. 2025-09-19 22:59:58 -04:00
Thomas Harte
b49c47425f Set I flag on NMI and reset. 2025-09-19 22:59:37 -04:00
Thomas Harte
3916ba1a42 This intermittently succeeds. Doubling down on investigation. 2025-09-19 20:33:02 -04:00
Thomas Harte
0b3d22b97c Take a swing and a miss at alternative documentation interpretations. 2025-09-19 19:59:12 -04:00
Thomas Harte
9b8b0f2023 Attempt to introduce a DFS ROM and WD1770. 2025-09-19 10:38:22 -04:00
Thomas Harte
06e0d17be0 Merge pull request #1567 from TomHarte/AllPixelModes
Perform proper pixel generation in all bitmap modes.
2025-09-18 21:54:24 -04:00
Thomas Harte
239c485f3c An underclock will do. 2025-09-18 21:35:08 -04:00
Thomas Harte
5e5fdda0ca Correct audio. 2025-09-18 21:33:25 -04:00
Thomas Harte
4b2dddf3c6 Remove stale TODO. 2025-09-18 21:21:51 -04:00
Thomas Harte
c99ec745ca Remove dead logging. 2025-09-18 21:20:27 -04:00
Thomas Harte
1ec2e455ec Support flash, mixed modes. 2025-09-18 21:19:33 -04:00
Thomas Harte
69304737c6 Switch red and blue. 2025-09-18 17:53:58 -04:00
Thomas Harte
fe91670127 Pull count outside loop, simplify state machine. 2025-09-18 17:50:46 -04:00
Thomas Harte
7a59f94f3d Install more realistic pixel pipeline. 2025-09-18 17:46:09 -04:00
Thomas Harte
4efe3a333d Merge pull request #1566 from TomHarte/BBCADC
Add the BBC's ADC.
2025-09-18 12:39:34 -04:00
Thomas Harte
421bf28582 Add comments, correct address decoding. 2025-09-18 12:27:13 -04:00
Thomas Harte
4c49ffe3d1 Attmept full ADC implementation. 2025-09-18 12:21:25 -04:00
Thomas Harte
26b1ef247b Add calls to ADB. 2025-09-17 23:11:48 -04:00
Thomas Harte
3aafba707a Use more efficient means for blank lines. 2025-09-17 22:33:59 -04:00
Thomas Harte
ae774e88fa Add header for ADC. 2025-09-17 21:42:42 -04:00
Thomas Harte
ff56dd53cf Remove dead code. 2025-09-17 21:42:33 -04:00
45 changed files with 2284 additions and 651 deletions

View File

@@ -19,6 +19,23 @@
using namespace Analyser::Static::Acorn;
namespace {
bool is_basic(const File &file) {
std::size_t pointer = 0;
const uint8_t *const data = file.data.data();
const std::size_t data_size = file.data.size();
while(true) {
if(pointer >= data_size-1 || data[pointer] != 0x0d) {
return false;
}
if((data[pointer+1]&0x7f) == 0x7f) break;
pointer += data[pointer+3];
}
return true;
}
}
static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
AcornCartridgesFrom(const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) {
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> acorn_cartridges;
@@ -68,11 +85,16 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
TargetPlatform::IntType,
bool
) {
auto target8bit = std::make_unique<ElectronTarget>();
auto targetElectron = std::make_unique<ElectronTarget>();
auto targetBBC = std::make_unique<BBCMicroTarget>();
auto targetArchimedes = std::make_unique<ArchimedesTarget>();
int bbc_hits = 0;
int electron_hits = 0;
bool format_prefers_bbc = false;
// Copy appropriate cartridges to the 8-bit target.
target8bit->media.cartridges = AcornCartridgesFrom(media.cartridges);
// Copy appropriate cartridges to the 8-bit targets.
targetElectron->media.cartridges = AcornCartridgesFrom(media.cartridges);
targetBBC->media.cartridges = AcornCartridgesFrom(media.cartridges);
// If there are tapes, attempt to get data from the first.
if(!media.tapes.empty()) {
@@ -80,35 +102,15 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
auto serialiser = tape->serialiser();
std::vector<File> files = GetFiles(*serialiser);
// continue if there are any files
// Continue only if there are any files.
if(!files.empty()) {
bool is_basic = true;
// If a file is execute-only, that means *RUN.
if(files.front().flags & File::Flags::ExecuteOnly) {
is_basic = false;
}
// Check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code,
// so that's also justification to *RUN.
if(is_basic) {
std::size_t pointer = 0;
uint8_t *const data = &files.front().data[0];
const std::size_t data_size = files.front().data.size();
while(true) {
if(pointer >= data_size-1 || data[pointer] != 0x0d) {
is_basic = false;
break;
}
if((data[pointer+1]&0x7f) == 0x7f) break;
pointer += data[pointer+3];
}
}
// Inspect first file. If it's protected or doesn't look like BASIC
// then the loading command is *RUN. Otherwise it's CHAIN"".
target8bit->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n";
target8bit->media.tapes = media.tapes;
targetElectron->loading_command =
(files.front().flags & File::Flags::ExecuteOnly) || !is_basic(files.front()) ? "*RUN\n" : "CHAIN\"\"\n";
targetElectron->media.tapes = media.tapes;
// TODO: my BBC Micro doesn't yet support tapes; evaluate here in the future.
}
}
@@ -123,25 +125,50 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
// 8-bit options: DFS and Hugo-style ADFS.
if(dfs_catalogue || (adfs_catalogue && !adfs_catalogue->has_large_sectors && adfs_catalogue->is_hugo)) {
// Accept the disk and determine whether DFS or ADFS ROMs are implied.
// Use the Pres ADFS if using an ADFS, as it leaves Page at &EOO.
target8bit->media.disks = media.disks;
target8bit->has_dfs = bool(dfs_catalogue);
target8bit->has_pres_adfs = bool(adfs_catalogue);
// Electron: use the Pres ADFS if using an ADFS, as it leaves Page at &EOO.
targetElectron->media.disks = media.disks;
targetElectron->has_dfs = bool(dfs_catalogue);
targetElectron->has_pres_adfs = bool(adfs_catalogue);
// BBC: only the 1770 DFS is currently supported, so use that.
targetBBC->media.disks = media.disks;
targetBBC->has_1770dfs = bool(dfs_catalogue);
targetBBC->has_adfs = bool(adfs_catalogue);
// Check whether a simple shift+break will do for loading this disk.
const auto bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption;
if(bootOption != Catalogue::BootOption::None) {
target8bit->should_shift_restart = true;
targetBBC->should_shift_restart = targetElectron->should_shift_restart = true;
} else {
target8bit->loading_command = "*CAT\n";
// Otherwise: if there's only one BASIC program then chain it.
// Failing that, do a *CAT to be communicative.
const File *sole_basic_file = nullptr;
for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) {
if(is_basic(file)) {
if(!sole_basic_file) {
sole_basic_file = &file;
} else {
sole_basic_file = nullptr;
break;
}
}
}
targetBBC->loading_command = targetElectron->loading_command =
sole_basic_file ? "CHAIN \"" + sole_basic_file->name + "\"\n" : "*CAT\n";
}
// Check whether adding the AP6 ROM is justified.
// For now this is an incredibly dense text search;
// if any of the commands that aren't usually present
// on a stock Electron are here, add the AP6 ROM and
// some sideways RAM such that the SR commands are useful.
// Add a slight preference for the BBC over the Electron, all else being equal, if this is a DFS floppy.
format_prefers_bbc = bool(dfs_catalogue);
for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) {
// Electron: check whether adding the AP6 ROM is justified.
// For now this is an incredibly dense text search;
// if any of the commands that aren't usually present
// on a stock Electron are here, add the AP6 ROM and
// some sideways RAM such that the SR commands are useful.
for(const auto &command: {
"AQRPAGE", "BUILD", "DUMP", "FORMAT", "INSERT", "LANG", "LIST", "LOADROM",
"LOCK", "LROMS", "RLOAD", "ROMS", "RSAVE", "SAVEROM", "SRLOAD", "SRPAGE",
@@ -149,10 +176,60 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
"VERIFY", "ZERO"
}) {
if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) {
target8bit->has_ap6_rom = true;
target8bit->has_sideways_ram = true;
targetElectron->has_ap6_rom = true;
targetElectron->has_sideways_ram = true;
}
}
// Look for any 'BBC indicators', i.e. direct access to BBC-specific hardware.
// Also currently a dense search.
const auto hits = [&](const std::initializer_list<uint16_t> collection) {
int hits = 0;
for(const auto address: collection) {
const uint8_t sta_address[3] = {
0x8d, uint8_t(address & 0xff), uint8_t(address >> 8)
};
if(std::search(
file.data.begin(), file.data.end(),
std::begin(sta_address), std::end(sta_address)
) != file.data.end()) {
++hits;
}
// I think I'll want std::ranges::contains_subrange if/when building for C++23.
}
return hits;
};
bbc_hits += hits({
// The video control registers.
0xfe20, 0xfe21,
// The system VIA.
0xfe40, 0xfe41, 0xfe42, 0xfe43, 0xfe44, 0xfe45, 0xfe46, 0xfe47,
0xfe48, 0xfe49, 0xfe4a, 0xfe4b, 0xfe4c, 0xfe4d, 0xfe4e, 0xfe4f,
// The user VIA.
0xfe60, 0xfe61, 0xfe62, 0xfe63, 0xfe64, 0xfe65, 0xfe66, 0xfe67,
0xfe68, 0xfe69, 0xfe6a, 0xfe6b, 0xfe6c, 0xfe6d, 0xfe6e, 0xfe6f,
});
// BASIC for "MODE7".
static constexpr uint8_t mode7[] = {0xeb, 0x37};
bbc_hits += std::search(
file.data.begin(), file.data.end(),
std::begin(mode7), std::end(mode7)
) != file.data.end();
electron_hits += hits({
// ULA addresses that aren't also the BBC's CRTC.
0xfe03, 0xfe04, 0xfe05,
0xfe06, 0xfe07, 0xfe08,
0xfe09, 0xfe0a, 0xfe0b,
0xfe0c, 0xfe0d, 0xfe0e,
0xfe0f,
});
}
} else if(adfs_catalogue) {
// Archimedes options, implicitly: ADFS, non-Hugo.
@@ -197,28 +274,38 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
// Enable the Acorn ADFS if a mass-storage device is attached;
// unlike the Pres ADFS it retains SCSI logic.
if(!media.mass_storage_devices.empty()) {
target8bit->has_pres_adfs = false; // To override a floppy selection, if one was made.
target8bit->has_acorn_adfs = true;
targetElectron->has_pres_adfs = false; // To override a floppy selection, if one was made.
targetElectron->has_acorn_adfs = true;
// Assume some sort of later-era Acorn work is likely to happen;
// so ensure *TYPE, etc are present.
target8bit->has_ap6_rom = true;
target8bit->has_sideways_ram = true;
targetElectron->has_ap6_rom = true;
targetElectron->has_sideways_ram = true;
target8bit->media.mass_storage_devices = media.mass_storage_devices;
targetElectron->media.mass_storage_devices = media.mass_storage_devices;
// Check for a boot option.
const auto sector = target8bit->media.mass_storage_devices.front()->get_block(1);
const auto sector = targetElectron->media.mass_storage_devices.front()->get_block(1);
if(sector[0xfd]) {
target8bit->should_shift_restart = true;
targetElectron->should_shift_restart = true;
} else {
target8bit->loading_command = "*CAT\n";
targetElectron->loading_command = "*CAT\n";
}
}
TargetList targets;
if(!target8bit->media.empty()) {
targets.push_back(std::move(target8bit));
if(!targetElectron->media.empty() && !targetBBC->media.empty()) {
if(bbc_hits > electron_hits || (bbc_hits == electron_hits && format_prefers_bbc)) {
targets.push_back(std::move(targetBBC));
} else {
targets.push_back(std::move(targetElectron));
}
} else {
if(!targetElectron->media.empty()) {
targets.push_back(std::move(targetElectron));
} else if(!targetBBC->media.empty()) {
targets.push_back(std::move(targetBBC));
}
}
if(!targetArchimedes->media.empty()) {
targets.push_back(std::move(targetArchimedes));

View File

@@ -38,12 +38,21 @@ private:
struct BBCMicroTarget: public ::Analyser::Static::Target, public Reflection::StructImpl<BBCMicroTarget> {
std::string loading_command;
bool should_shift_restart = false;
bool has_1770dfs = false;
bool has_adfs = false;
bool has_sideways_ram = true;
BBCMicroTarget() : Analyser::Static::Target(Machine::BBCMicro) {}
private:
friend Reflection::StructImpl<BBCMicroTarget>;
void declare_fields() {}
void declare_fields() {
DeclareField(has_1770dfs);
DeclareField(has_adfs);
DeclareField(has_sideways_ram);
}
};
struct ArchimedesTarget: public ::Analyser::Static::Target, public Reflection::StructImpl<ArchimedesTarget> {

View File

@@ -114,6 +114,7 @@ uint8_t WD1770::read(const int address) {
update_status([] (Status &status) {
status.data_request = false;
});
// Logger::info().append("Returned data %02x; [drq:%d]", data_, status_.data_request);
return data_;
}
}
@@ -134,12 +135,13 @@ void WD1770::run_for(const Cycles cycles) {
void WD1770::posit_event(const int new_event_type) {
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__:
#define RESUME_WAIT(mask) interesting_event_mask_ = int(mask); return;
#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer);
#define WAIT_FOR_BYTES(count) distance_into_section_ = 0; \
WAIT_FOR_EVENT(Event::Token); \
if(get_latest_token().type == Token::Byte) ++distance_into_section_; \
if(distance_into_section_ < count) { \
return; \
RESUME_WAIT(Event::Token); \
}
#define BEGIN_SECTION() switch(resume_point_) { default:
#define END_SECTION() (void)0; }

View File

@@ -23,7 +23,7 @@ template <typename T> void MOS6522<T>::access(const int address) {
}
break;
case 0xf:
// case 0xf:
case 0x1:
// In both handshake and pulse modes, CA2 goes low on any read or write of Port A.
if(handshake_modes_[0] != HandshakeMode::None) {
@@ -57,14 +57,17 @@ template <typename T> void MOS6522<T>::write(int address, const uint8_t value) {
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
bus_handler_.template set_port_output<Port::A>(value, registers_.data_direction[0]);
if(handshake_modes_[1] != HandshakeMode::None) {
set_control_line_output<Port::A, Line::Two>(LineState::Off);
}
// Avoid handshaking if this was via address 0xf.
if(address == 0x1) {
if(handshake_modes_[1] != HandshakeMode::None) {
set_control_line_output<Port::A, Line::Two>(LineState::Off);
}
registers_.interrupt_flags &= ~(
InterruptFlag::CA1ActiveEdge |
((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)
);
registers_.interrupt_flags &= ~(
InterruptFlag::CA1ActiveEdge |
((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)
);
}
reevaluate_interrupts();
break;
@@ -196,9 +199,10 @@ template <typename T> uint8_t MOS6522<T>::read(int address) {
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge);
reevaluate_interrupts();
return get_port_input<Port::B>(registers_.data_direction[1], registers_.output[1], registers_.auxiliary_control & 0x80);
case 0xf:
case 0x1: // Read Port A ('IRA').
case 0x1: // Read Port A ('IRA') [with handshaking].
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge);
[[fallthrough]];
case 0xf: // Read Port A ('IRA') [without handshaking].
reevaluate_interrupts();
return get_port_input<Port::A>(registers_.data_direction[0], registers_.output[0], 0);
@@ -246,8 +250,10 @@ uint8_t MOS6522<T>::get_port_input(
) {
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
const uint8_t input = bus_handler_.template get_port_input<port>();
output = (output & ~timer_mask) | (registers_.timer_port_b_output & timer_mask);
return (input & ~output_mask) | (output & output_mask);
// Force any timer-adjusted PB7 to be visible even if the pin is set as input.
output = (input & ~output_mask) | (output & output_mask);
return (output & ~timer_mask) | (registers_.timer_port_b_output & timer_mask);
}
template <typename T> T &MOS6522<T>::bus_handler() {

View File

@@ -9,23 +9,38 @@
#pragma once
#include "ClockReceiver/ClockReceiver.hpp"
#include "Numeric/SizedInt.hpp"
#include <cstdint>
#include <cstdio>
//
// WARNING: code is in flux. I'm attempting to use hoglet's FPGA implementation at
// https://github.com/hoglet67/BeebFpga/blob/master/src/common/mc6845.vhd as an authoritative guide to proper behaviour,
// having found his Electron ULA to be excellent. This is starting by mapping various bits of internal state here
// to hoglet's equivalents; cf. comments.
//
namespace Motorola::CRTC {
using RefreshAddress = Numeric::SizedInt<14>;
using LineAddress = Numeric::SizedInt<5>;
using SyncCounter = Numeric::SizedInt<4>;
using CharacterAddress = Numeric::SizedInt<8>;
using RowAddress = Numeric::SizedInt<7>;
struct BusState {
bool display_enable = false;
bool hsync = false;
bool vsync = false;
bool hsync = false; // hs
bool vsync = false; // vs
bool cursor = false;
uint16_t refresh_address = 0;
uint16_t row_address = 0;
RefreshAddress refresh;
LineAddress line;
// Not strictly part of the bus state; provided because the partition between 6845 and bus handler
// doesn't quite hold up in some emulated systems where the two are integrated and share more state.
int field_count = 0;
Numeric::SizedInt<5> field_count = 0; // field_counter
};
class BusHandler {
@@ -39,14 +54,8 @@ enum class Personality {
UM6845R, // Type 1 in CPC parlance. Status register, fixed-length VSYNC.
MC6845, // Type 2. No status register, fixed-length VSYNC, no zero-length HSYNC.
AMS40226, // Type 3. Status is get register, fixed-length VSYNC, no zero-length HSYNC.
EGA, // Extended EGA-style CRTC; uses 16-bit addressing throughout.
};
constexpr bool is_egavga(const Personality p) {
return p >= Personality::EGA;
}
// https://www.pcjs.org/blog/2018/03/20/ advises that "the behavior of bits 5 and 6 [of register 10, the cursor start
// register is really card specific".
//
@@ -54,10 +63,10 @@ constexpr bool is_egavga(const Personality p) {
enum class CursorType {
/// No cursor signal is generated.
None,
/// Built-in 6845 style: 00 => no blinking; 01 => no cursor; 10 => slow blink; 11 => fast blink
Native,
/// MDA style: 00 => symmetric blinking; 01 or 10 => no blinking; 11 => short on, long off.
MDA,
/// EGA style: ignore the bits completely.
EGA,
};
// TODO UM6845R and R12/R13; see http://www.cpcwiki.eu/index.php/CRTC#CRTC_Differences
@@ -90,38 +99,26 @@ public:
}
void set_register(const uint8_t value) {
static constexpr bool is_ega = is_egavga(personality);
const auto load_low = [value](uint16_t &target) {
target = (target & 0xff00) | value;
};
const auto load_high = [value](uint16_t &target) {
static constexpr uint8_t mask = RefreshMask >> 8;
target = uint16_t((target & 0x00ff) | ((value & mask) << 8));
};
switch(selected_register_) {
case 0: layout_.horizontal.total = value; break;
case 1: layout_.horizontal.displayed = value; break;
case 2: layout_.horizontal.start_sync = value; break;
case 3:
layout_.horizontal.sync_width = value & 0xf;
layout_.horizontal.sync_width = value;
layout_.vertical.sync_lines = value >> 4;
// TODO: vertical sync lines:
// "(0 means 16 on some CRTC. Not present on all CRTCs, fixed to 16 lines on these)"
break;
case 4: layout_.vertical.total = value & 0x7f; break;
case 5: layout_.vertical.adjust = value & 0x1f; break;
case 6: layout_.vertical.displayed = value & 0x7f; break;
case 7: layout_.vertical.start_sync = value & 0x7f; break;
case 4: layout_.vertical.total = value; break;
case 5: layout_.vertical.adjust = value; break;
case 6: layout_.vertical.displayed = value; break;
case 7: layout_.vertical.start_sync = value; break;
case 8:
// TODO: an error elsewhere appears to cause modes other than InterlaceMode::Off never to hit
// vertical sync.
// switch(value & 3) {
// default: layout_.interlace_mode_ = InterlaceMode::Off; break;
// case 0b01: layout_.interlace_mode_ = InterlaceMode::InterlaceSync; break;
// case 0b11: layout_.interlace_mode_ = InterlaceMode::InterlaceSyncAndVideo; break;
// }
switch(value & 3) {
default: layout_.interlace_mode_ = InterlaceMode::Off; break;
case 0b01: layout_.interlace_mode_ = InterlaceMode::Sync; break;
case 0b11: layout_.interlace_mode_ = InterlaceMode::SyncAndVideo; break;
}
// Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R.
if(personality != Personality::UM6845R && personality != Personality::MC6845) {
@@ -132,20 +129,23 @@ public:
}
}
break;
case 9: layout_.vertical.end_row = value & 0x1f; break;
case 9: layout_.vertical.end_line = value; break;
case 10:
layout_.vertical.start_cursor = value & 0x1f;
layout_.cursor_flags = (value >> 5) & 3;
layout_.vertical.start_cursor = value;
layout_.cursor_flags = value >> 5;
update_cursor_mask();
break;
case 11:
layout_.vertical.end_cursor = value & 0x1f;
layout_.vertical.end_cursor = value;
break;
case 12: load_high(layout_.start_address); break;
case 13: load_low(layout_.start_address); break;
case 14: load_high(layout_.cursor_address); break;
case 15: load_low(layout_.cursor_address); break;
case 12: layout_.start_address.template load<8>(value); break;
case 13: layout_.start_address.template load<0>(value); break;
case 14: layout_.cursor_address.template load<8>(value); break;
case 15: layout_.cursor_address.template load<0>(value); break;
}
// Take redundant copies of all registers, limited to their actual bit sizes,
// to proffer up if the registers are read.
static constexpr uint8_t masks[] = {
0xff, // Horizontal total.
0xff, // Horizontal display end.
@@ -154,11 +154,11 @@ public:
// EGA: b0b4: end of horizontal blank;
// b5b6: "Number of character clocks to delay start of display after Horizontal Total has been reached."
is_ega ? 0xff : 0x7f, // Start horizontal retrace.
0x7f, // Start horizontal retrace.
0x1f, 0x7f, 0x7f,
0xff, 0x1f, 0x7f, 0x1f,
uint8_t(RefreshMask >> 8), uint8_t(RefreshMask),
uint8_t(RefreshMask >> 8), uint8_t(RefreshMask),
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
};
if(selected_register_ < 16) {
@@ -170,8 +170,8 @@ public:
}
void trigger_light_pen() {
registers_[17] = bus_state_.refresh_address & 0xff;
registers_[16] = bus_state_.refresh_address >> 8;
registers_[17] = bus_state_.refresh.get() & 0xff;
registers_[16] = bus_state_.refresh.get() >> 8;
status_ |= 0x40;
}
@@ -182,180 +182,291 @@ public:
// ordered so that whatever assignments result don't affect any subsequent conditionals
// Do bus work.
bus_state_.cursor = is_cursor_line_ &&
bus_state_.refresh_address == layout_.cursor_address;
bus_state_.display_enable = character_is_visible_ && line_is_visible_;
bus_handler_.perform_bus_cycle(bus_state_);
//
// External bus activity.
//
bus_state_.line = line_is_interlaced_ ? (line_ & LineAddress::IntT(~1)) | (odd_field_ ? 1 : 0) : line_;
bus_state_.display_enable = character_is_visible_ && row_is_visible_;
bus_state_.cursor = (cursor_mask_ && is_cursor_line_ && bus_state_.refresh == layout_.cursor_address)
&& bus_state_.display_enable;
bus_handler_.perform_bus_cycle(bus_state_);
bus_state_.refresh = refresh_; // Deliberate: do this after bus activity.
// TODO: is this a hack?
//
// Shared, stateless signals.
// Shared signals.
//
const bool character_total_hit = character_counter_ == layout_.horizontal.total;
const uint8_t lines_per_row =
layout_.interlace_mode_ == InterlaceMode::InterlaceSyncAndVideo ?
layout_.vertical.end_row & ~1 : layout_.vertical.end_row;
const bool row_end_hit = bus_state_.row_address == lines_per_row && !is_in_adjustment_period_;
const bool was_eof = eof_latched_;
const bool character_total_hit = character_counter_ == layout_.horizontal.total; // r00_h_total_hit
const auto lines_per_row =
layout_.interlace_mode_ == InterlaceMode::SyncAndVideo ?
layout_.vertical.end_line & LineAddress::IntT(~1) : layout_.vertical.end_line; // max_scanline
const bool line_end_hit = line_ == lines_per_row && !is_in_adjustment_period_; // max_scanline_hit
const bool new_frame =
character_total_hit && was_eof &&
character_total_hit && eof_latched_ &&
(
layout_.interlace_mode_ == InterlaceMode::Off ||
!odd_field_
);
!bus_state_.field_count.bit<0>() ||
extra_line_
); // new_frame
//
// Horizontal.
//
// Update horizontal sync.
if(bus_state_.hsync) {
++hsync_counter_;
bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width;
}
if(character_counter_ == layout_.horizontal.start_sync) {
hsync_counter_ = 0;
bus_state_.hsync = true;
}
// Check for end-of-line.
character_reset_history_ <<= 1;
if(character_total_hit) {
character_counter_ = 0;
character_is_visible_ = true;
character_reset_history_ |= 1;
} else {
character_counter_++;
}
// Check for end of visible characters.
if(character_counter_ == layout_.horizontal.displayed) {
character_is_visible_ = false;
}
//
// End-of-frame.
//
if(character_total_hit) {
if(was_eof) {
eof_latched_ = eom_latched_ = is_in_adjustment_period_ = false;
adjustment_counter_ = 0;
} else if(is_in_adjustment_period_) {
adjustment_counter_ = (adjustment_counter_ + 1) & 31;
}
}
if(character_reset_history_ & 2) {
eom_latched_ |= row_end_hit && row_counter_ == layout_.vertical.total;
}
if(character_reset_history_ & 4 && eom_latched_) {
// TODO: I don't believe the "add 1 for interlaced" test here is accurate;
// others represent the extra scanline as additional state, presumably because
// adjust total might be reprogrammed at any time.
const auto adjust_length =
layout_.vertical.adjust + (layout_.interlace_mode_ != InterlaceMode::Off && odd_field_ ? 1 : 0);
is_in_adjustment_period_ |= adjustment_counter_ != adjust_length;
eof_latched_ |= adjustment_counter_ == adjust_length;
}
//
// Vertical.
//
// Sync.
const bool vsync_horizontal =
(!odd_field_ && !character_counter_) ||
(odd_field_ && character_counter_ == (layout_.horizontal.total >> 1));
if(vsync_horizontal) {
if((row_counter_ == layout_.vertical.start_sync && !bus_state_.row_address) || bus_state_.vsync) {
bus_state_.vsync = true;
vsync_counter_ = (vsync_counter_ + 1) & 0xf;
} else {
vsync_counter_ = -1;//0; // TODO: this ensures the first time the condition above is met,
// vsync_counter starts at 0. It's a hack though.
}
if(vsync_counter_ == layout_.vertical.sync_lines) {
bus_state_.vsync = false;
}
}
// Row address.
if(character_total_hit) {
if(was_eof) {
bus_state_.row_address = 0;
eof_latched_ = eom_latched_ = false;
} else if(row_end_hit) {
bus_state_.row_address = 0;
} else if(layout_.interlace_mode_ == InterlaceMode::InterlaceSyncAndVideo) {
bus_state_.row_address = (bus_state_.row_address + 2) & ~1 & 31;
} else {
bus_state_.row_address = (bus_state_.row_address + 1) & 31;
}
}
// Row counter.
row_counter_ = next_row_counter_;
if(new_frame) {
next_row_counter_ = 0;
is_first_scanline_ = true;
} else {
next_row_counter_ = row_end_hit && character_total_hit ?
(next_row_counter_ + 1) : next_row_counter_;
is_first_scanline_ &= !row_end_hit;
}
// Vertical display enable.
if(is_first_scanline_) {
line_is_visible_ = true;
odd_field_ = bus_state_.field_count & 1;
} else if(line_is_visible_ && row_counter_ == layout_.vertical.displayed) {
line_is_visible_ = false;
++bus_state_.field_count;
}
// Cursor.
if constexpr (cursor_type != CursorType::None) {
// Check for cursor enable.
is_cursor_line_ |= bus_state_.row_address == layout_.vertical.start_cursor;
is_cursor_line_ &= bus_state_.row_address != layout_.vertical.end_cursor;
switch(cursor_type) {
// MDA-style blinking.
// https://retrocomputing.stackexchange.com/questions/27803/what-are-the-blinking-rates-of-the-caret-and-of-blinking-text-on-pc-graphics-car
// gives an 8/8 pattern for regular blinking though mode 11 is then just a guess.
case CursorType::MDA:
switch(layout_.cursor_flags) {
case 0b11: is_cursor_line_ &= (bus_state_.field_count & 8) < 3; break;
case 0b00: is_cursor_line_ &= bool(bus_state_.field_count & 8); break;
case 0b01: is_cursor_line_ = false; break;
case 0b10: is_cursor_line_ = true; break;
default: break;
}
break;
}
}
//
// Addressing.
//
// Start-of-line address: seeded with the programmed display start address upon a new frame;
// otherwise copied from the refresh address at the end of each line of characters.
const auto initial_line_address = line_address_;
if(new_frame) {
bus_state_.refresh_address = layout_.start_address;
line_address_ = layout_.start_address;
} else if(character_counter_ == layout_.horizontal.displayed && line_end_hit) {
line_address_ = refresh_;
}
// Refresh address: seeded with the programmed display start address upon a new frame;
// otherwise copied from the start-of-line address is a new line is about to start;
// otherwise incremented across the line.
if(new_frame) {
refresh_ = layout_.start_address;
} else if(character_total_hit) {
bus_state_.refresh_address = line_address_;
refresh_ = initial_line_address;
} else {
bus_state_.refresh_address = (bus_state_.refresh_address + 1) & RefreshMask;
++refresh_;
}
//
// Per hoglet: b0 does not vary within a line even if you switch in/out of interlaced mode.
// He reproduces the same with extra state, which probably doesn't exist on the real device.
// This implementation follows his lead.
//
if(character_total_hit) {
line_is_interlaced_ = layout_.interlace_mode_ == InterlaceMode::SyncAndVideo;
}
//
// Sync.
//
// Vertical sync.
//
// Counter:
// Sync width of 0 => 16 lines of sync.
// Triggered by the row counter becoming equal to the sync start position, regardless of when.
// Subsequently increments at the start of each line.
const bool hit_vsync = row_counter_ == layout_.vertical.start_sync; // vs_hit
const bool is_vsync_rising_edge = hit_vsync && !hit_vsync_last_;
hit_vsync_last_ = hit_vsync;
// Select odd or even sync depending on the field.
// (Noted: the reverse-odd-test is intentional)
bus_state_.vsync = (layout_.interlace_mode_ != InterlaceMode::Off && !odd_field_) ?
vsync_odd_ : vsync_even_;
// Odd sync copies even sync, but half a line later.
if(character_counter_ == layout_.horizontal.total >> 1) {
vsync_odd_ = vsync_even_;
}
// Even sync begins on the rising edge of vsync, then continues until the counter hits its proper
// target, one cycle after reset of the horizontal counter.
if(is_vsync_rising_edge) {
vsync_even_ = true;
} else if(vsync_counter_ == layout_.vertical.sync_lines && character_reset_history_.bit<0>()) {
vsync_even_ = false;
}
// The vsync counter is zeroed by the rising edge of sync but subsequently increments immediately
// upon reset of the horizontal counter.
if(is_vsync_rising_edge) {
vsync_counter_ = 0;
} else if(character_total_hit) {
++vsync_counter_;
}
// Horizontal sync.
//
// A sync width of 0 should mean that no sync is observed.
// Hitting the start sync condition while sync is already ongoing should have no effect.
if(bus_state_.hsync) {
++hsync_counter_;
} else {
hsync_counter_ = 0;
}
if(hsync_counter_ == layout_.horizontal.sync_width) {
bus_state_.hsync = false;
} else if(character_counter_ == layout_.horizontal.start_sync) {
bus_state_.hsync = true;
}
//
// Horizontal.
//
// Check for visible characters; visibility starts in the first column and continues
if(!character_counter_) {
character_is_visible_ = true;
}
if(character_counter_ == layout_.horizontal.displayed || character_total_hit) {
character_is_visible_ = false;
}
// Check for end-of-line.
//
// character_reset_history_ is used because some events are defined to occur one or two
// cycles after end-of-line regardless of whether an additional end of line is hit in
// the interim.
if(character_total_hit) {
character_counter_ = 0;
} else {
++character_counter_;
}
//
// Vertical.
//
// Update line counter (which also counts the vertical adjust period).
//
// Counts in steps of 2 only if & 3) mode is InterlaceMode::SyncAndVideo and this is
// not the adjustment period. Otherwise counts in steps of 1.
if(new_frame) {
line_ = 0;
} else if(character_total_hit) {
line_ = next_line_;
}
if(line_end_hit) {
next_line_ = 0;
} else if(is_in_adjustment_period_ || layout_.interlace_mode_ != InterlaceMode::SyncAndVideo) {
next_line_ = line_ + 1;
} else {
next_line_ = (line_ + 2) & LineAddress::IntT(~1);
}
// Update row counter.
//
// Very straightforward: tests at end of line whether row end has also been hit. If so, increments.
row_counter_ = next_row_counter_;
if(new_frame) {
next_row_counter_ = 0;
} else if(character_total_hit && line_end_hit) {
next_row_counter_ = row_counter_ + 1;
}
// Vertical display enable.
if(is_first_scanline_) {
row_is_visible_ = true;
odd_field_ = bus_state_.field_count.bit<0>();
} else if(row_is_visible_ && row_counter_ == layout_.vertical.displayed) {
row_is_visible_ = false;
++bus_state_.field_count;
update_cursor_mask();
}
//
// End-of-frame.
//
if(new_frame) {
is_in_adjustment_period_ = false;
} else if(character_total_hit && eom_latched_ && will_adjust_) {
is_in_adjustment_period_ = true;
}
if(new_frame) {
line_address_ = layout_.start_address;
} else if(character_counter_ == layout_.horizontal.displayed && row_end_hit) {
line_address_ = bus_state_.refresh_address;
is_first_scanline_ = true;
} else if(character_total_hit) {
is_first_scanline_ = false;
}
// The extra-line flag holds true for a single line if one is needed to complete
// an odd interlaced field.
if(
character_total_hit &&
eof_latched_ &&
layout_.interlace_mode_ != InterlaceMode::Off &&
bus_state_.field_count.bit<0>() &&
!extra_line_
) {
extra_line_ = true;
} else if(character_total_hit) {
extra_line_ = false;
}
// EOF (end of field) marks the end of the regular set of scans, including the adjustment area.
// It doesn't include the extra line added during odd interlaced fields.
if(new_frame) {
eof_latched_ = false;
} else if(eom_latched_ && !will_adjust_ && character_reset_history_.bit<2>()) {
eof_latched_ = true;
}
// Will-adjust indicates whether an adjustment area is upcoming; if so then it occurs after EOM.
if(new_frame) {
will_adjust_ = false;
} else if(character_reset_history_.bit<1>() && eom_latched_) {
if(next_line_ == layout_.vertical.adjust) {
will_adjust_ = false;
} else {
will_adjust_ = true;
}
}
// EOM (end of main) marks the end of the visible set of rows, prior to any adjustment area.
// It is set one cycle after the most-recent start of line.
if(new_frame) {
eom_latched_ = false;
} else if(character_reset_history_.bit<0>() && line_end_hit && row_counter_ == layout_.vertical.total) {
eom_latched_ = true;
}
//
// Cursor
//
cursor_history_ <<= 1;
if constexpr (cursor_type != CursorType::None) {
if(character_total_hit) {
// This is clearly a nonsense test; there's absolutely no reason a real 6845 would do anything
// other than equality comparisons, to maintain internal state.
//
// ... that said, I have been unable to reconcile:
//
// 1. the PCjs results on real MC6845Ps that show wraparound cursors
// Cf. https://www.pcjs.org/blog/2018/03/20/ ; and
// 2. the expectations of the BBC Micro (which sets an out-of-range stop line for its cursor
// right at initial boot) and various pieces of its software (including but
// not limited to Arcadians, which uses in-range numbers but has start > end and expects
// the cursor correspondingly to be hidden).
//
// I also note that the two BBC FPGA implementations I glanced at, hoglet's and Mister's, use
// fictional range comparisons.
//
// But, on the other hand, Tom Seddon remarks at https://github.com/tom-seddon/6845-tests that
// "Looks like the cursor switches on when cursor is off and raster matches R10, and switches
// off when cursor is on and raster matches R11."
//
// (but also seems to use a range test in his software implementation?)
is_cursor_line_ =
line_ >= layout_.vertical.start_cursor &&
line_ <= layout_.vertical.end_cursor;
}
}
//
// Event history.
//
// Somewhat of a fiction, this keeps a track of recent character resets because
// some events are keyed on 1 cycle after last reset, 2 cycles after last reset, etc.
character_reset_history_ <<= 1;
character_reset_history_ |= character_total_hit;
}
}
@@ -364,78 +475,124 @@ public:
}
private:
static constexpr uint16_t RefreshMask = (personality >= Personality::EGA) ? 0xffff : 0x3fff;
BusHandlerT &bus_handler_;
BusState bus_state_;
enum class InterlaceMode {
/// No interlacing.
Off,
InterlaceSync,
InterlaceSyncAndVideo,
/// Provide interlaced sync, but just scan out the exact same display for each field.
Sync,
/// Provide interlaced sync and scan even/odd lines depending on field.
SyncAndVideo,
};
enum class BlinkMode {
// TODO.
};
// Comments on the right provide the corresponding signal name in hoglet's VHDL implementation.
struct {
struct {
uint8_t total;
uint8_t displayed;
uint8_t start_sync;
uint8_t sync_width;
CharacterAddress total; // r00_h_total
CharacterAddress displayed; // r01_h_displayed
CharacterAddress start_sync; // r02_h_sync_pos
SyncCounter sync_width; // r03_h_sync_width
} horizontal;
struct {
uint8_t total;
uint8_t displayed;
uint8_t start_sync;
uint8_t sync_lines;
uint8_t adjust;
RowAddress total; // r04_v_total
RowAddress displayed; // r06_v_displayed
RowAddress start_sync; // r07_v_sync_pos
SyncCounter sync_lines; // r03_v_sync_width
LineAddress adjust; // r05_v_total_adj
uint8_t end_row;
uint8_t start_cursor;
uint8_t end_cursor;
LineAddress end_line; // r09_max_scanline_addr
LineAddress start_cursor; // r10_cursor_start
LineAddress end_cursor; // r11_cursor_end
} vertical;
InterlaceMode interlace_mode_ = InterlaceMode::Off;
uint8_t end_row() const {
return interlace_mode_ == InterlaceMode::InterlaceSyncAndVideo ? vertical.end_row & ~1 : vertical.end_row;
}
InterlaceMode interlace_mode_ = InterlaceMode::Off; // r08_interlace
uint16_t start_address;
uint16_t cursor_address;
uint16_t light_pen_address;
uint8_t cursor_flags;
RefreshAddress start_address; // r12_start_addr_h + r13_start_addr_l
RefreshAddress cursor_address; // r14_cursor_h + r15_cursor_l
RefreshAddress light_pen_address; // r16_light_pen_h + r17_light_pen_l
Numeric::SizedInt<2> cursor_flags; // r10_cursor_mode
} layout_;
uint8_t registers_[18]{};
uint8_t dummy_register_ = 0;
int selected_register_ = 0;
uint8_t character_counter_ = 0;
uint32_t character_reset_history_ = 0;
uint8_t row_counter_ = 0, next_row_counter_ = 0;
CharacterAddress character_counter_; // h_counter
Numeric::SizedInt<3> character_reset_history_; // sol
RowAddress row_counter_; // row_counter
RowAddress next_row_counter_; // row_counter_next
LineAddress line_; // line_counter
LineAddress next_line_; // line_counter_next
RefreshAddress refresh_; // ma_i
uint8_t adjustment_counter_ = 0;
bool character_is_visible_ = false;
bool line_is_visible_ = false;
bool character_is_visible_ = false; // h_display
bool row_is_visible_ = false; // v_display
bool is_first_scanline_ = false;
bool is_cursor_line_ = false;
bool cursor_mask_ = false;
int hsync_counter_ = 0;
int vsync_counter_ = 0;
bool is_in_adjustment_period_ = false;
SyncCounter hsync_counter_; // h_sync_counter
SyncCounter vsync_counter_; // v_sync_counter
bool will_adjust_ = false; // in_adj
bool is_in_adjustment_period_ = false; // adj_in_progress
uint16_t line_address_ = 0;
RefreshAddress line_address_; // ma_row
uint8_t status_ = 0;
int display_skew_mask_ = 1;
unsigned int character_is_visible_shifter_ = 0;
bool eof_latched_ = false;
bool eom_latched_ = false;
uint16_t next_row_address_ = 0;
bool odd_field_ = false;
bool eof_latched_ = false; // eof_latched
bool eom_latched_ = false; // eom_latched
bool odd_field_ = false; // odd_field
bool extra_line_ = false; // extra_scanline
bool hit_vsync_last_ = false; // vs_hit_last
bool vsync_even_ = false; // vs_even
bool vsync_odd_ = false; // vs_odd
bool reset_ = false;
Numeric::SizedInt<3> cursor_history_; // cursor0, cursor1, cursor2 [TODO]
bool line_is_interlaced_ = false;
void update_cursor_mask() {
switch(cursor_type) {
case CursorType::None:
break;
// MDA-style blinking.
// https://retrocomputing.stackexchange.com/questions/27803/what-are-the-blinking-rates-of-the-caret-and-of-blinking-text-on-pc-graphics-car
// gives an 8/8 pattern for regular blinking though mode 11 is then just a guess.
case CursorType::MDA:
switch(layout_.cursor_flags.get()) {
case 0b11: cursor_mask_ = (bus_state_.field_count & 8) < 3; break;
case 0b00: cursor_mask_ = bus_state_.field_count.bit<3>(); break;
case 0b01: cursor_mask_ = false; break;
case 0b10: cursor_mask_ = true; break;
default: break;
}
break;
// Standard built-in 6845 blinking.
case CursorType::Native:
switch(layout_.cursor_flags.get()) {
case 0b00: cursor_mask_ = true; break;
case 0b01: cursor_mask_ = false; break;
case 0b10: cursor_mask_ = bus_state_.field_count.bit<3>(); break;
case 0b11: cursor_mask_= bus_state_.field_count.bit<4>(); break;
default: break;
}
break;
}
}
};
}

View File

@@ -8,6 +8,7 @@
#include "6850.hpp"
#include <bit>
#include <cassert>
using namespace Motorola::ACIA;
@@ -141,11 +142,8 @@ int ACIA::expected_bits() {
return 1 + data_bits_ + stop_bits_ + (parity_ != Parity::None);
}
uint8_t ACIA::parity(uint8_t value) {
value ^= value >> 4;
value ^= value >> 2;
value ^= value >> 1;
return value ^ (parity_ == Parity::Even);
uint8_t ACIA::parity(const uint8_t value) {
return (std::popcount(value) & 1) ^ (parity_ == Parity::Even);
}
bool ACIA::serial_line_did_produce_bit(Serial::Line<false> *, const int bit) {

View File

@@ -0,0 +1,351 @@
//
// SAA5050.cpp
// Clock Signal
//
// Created by Thomas Harte on 24/09/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "SAA5050.hpp"
#include <algorithm>
#include <cstdint>
namespace {
// SAA5050 font, padded out to one byte per row. The least-significant five bits of each byte
// are the meaningful pixels for that row, with the LSB being on the right.
constexpr uint8_t font[][10] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, // Character 32.
{0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x0a, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x06, 0x09, 0x08, 0x1c, 0x08, 0x08, 0x1f, 0x00, 0x00, },
{0x00, 0x0e, 0x15, 0x14, 0x0e, 0x05, 0x15, 0x0e, 0x00, 0x00, },
{0x00, 0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03, 0x00, 0x00, },
{0x00, 0x08, 0x14, 0x14, 0x08, 0x15, 0x12, 0x0d, 0x00, 0x00, },
{0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00, 0x00, },
{0x00, 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00, 0x00, },
{0x00, 0x04, 0x15, 0x0e, 0x04, 0x0e, 0x15, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, },
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x11, 0x0a, 0x04, 0x00, 0x00, },
{0x00, 0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x1f, 0x01, 0x02, 0x06, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02, 0x00, 0x00, },
{0x00, 0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
{0x00, 0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x02, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0e, 0x00, 0x00, },
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x13, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x10, 0x0e, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x00, 0x04, 0x08, 0x1f, 0x08, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x10, 0x10, 0x16, 0x01, 0x02, 0x04, 0x07, },
{0x00, 0x00, 0x04, 0x02, 0x1f, 0x02, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x04, 0x0e, 0x15, 0x04, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x0a, 0x0a, 0x1f, 0x0a, 0x1f, 0x0a, 0x0a, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0e, 0x01, 0x0f, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x10, 0x10, 0x0f, 0x00, 0x00, },
{0x00, 0x01, 0x01, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x1f, 0x10, 0x0e, 0x00, 0x00, },
{0x00, 0x02, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x04, 0x00, 0x0c, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, },
{0x00, 0x08, 0x08, 0x09, 0x0a, 0x0c, 0x0a, 0x09, 0x00, 0x00, },
{0x00, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1a, 0x15, 0x15, 0x15, 0x15, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x10, 0x10, },
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x01, },
{0x00, 0x00, 0x00, 0x0b, 0x0c, 0x08, 0x08, 0x08, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x0e, 0x01, 0x1e, 0x00, 0x00, },
{0x00, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0a, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
{0x00, 0x00, 0x00, 0x1f, 0x02, 0x04, 0x08, 0x1f, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x10, 0x10, 0x11, 0x03, 0x05, 0x07, 0x01, },
{0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x00, 0x00, },
{0x00, 0x18, 0x04, 0x18, 0x04, 0x19, 0x03, 0x05, 0x07, 0x01, },
{0x00, 0x00, 0x04, 0x00, 0x1f, 0x00, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x00, 0x00, },
};
enum ControlCode: uint8_t {
RedAlpha = 0x01,
GreenAlpha = 0x02,
YellowAlpha = 0x03,
BlueAlpha = 0x04,
MagentaAlpha = 0x05,
CyanAlpha = 0x06,
WhiteAlpha = 0x07,
Flash = 0x08,
Steady = 0x09,
RedGraphics = 0x11,
GreenGraphics = 0x12,
YellowGraphics = 0x13,
BlueGraphics = 0x14,
MagentaGraphics = 0x15,
CyanGraphics = 0x16,
WhiteGraphics = 0x17,
Conceal = 0x18,
ContinuousGraphics = 0x19,
SeparatedGraphics = 0x1a,
NormalHeight = 0xc,
DoubleHeight = 0xd,
BlackBackground = 0x1c,
NewBackground = 0x1d,
HoldGraphics = 0x1e,
ReleaseGraphics = 0x1f,
};}
using namespace Mullard;
void SAA5050Serialiser::begin_frame(const bool is_odd) {
line_ = -2;
row_ = 0;
odd_frame_ = is_odd;
row_has_double_height_ = false;
double_height_offset_ = 0;
++frame_counter_;
}
void SAA5050Serialiser::begin_line() {
line_ += 2;
if(line_ == 20) {
line_ = 0;
++row_;
if(row_has_double_height_) {
double_height_offset_ = (double_height_offset_ + 5) % 10;
}
row_has_double_height_ = false;
}
output_.reset();
has_output_ = false;
apply_control(ControlCode::WhiteAlpha);
apply_control(ControlCode::Steady);
apply_control(ControlCode::NormalHeight);
apply_control(ControlCode::ContinuousGraphics);
apply_control(ControlCode::BlackBackground);
apply_control(ControlCode::ReleaseGraphics);
}
bool SAA5050Serialiser::has_output() const {
return has_output_;
}
SAA5050Serialiser::Output SAA5050Serialiser::output() {
has_output_ = false;
return output_;
}
void SAA5050Serialiser::apply_control(const uint8_t value) {
const auto set_alpha = [&](const uint8_t colour) {
alpha_mode_ = true;
conceal_ = false;
output_.alpha = colour;
hold_graphics_ = false;
};
const auto set_graphics = [&](const uint8_t colour) {
alpha_mode_ = false;
conceal_ = false;
output_.alpha = colour;
hold_graphics_ = false;
};
switch(value) {
default: break;
case RedAlpha: set_alpha(0b100); break;
case GreenAlpha: set_alpha(0b010); break;
case YellowAlpha: set_alpha(0b110); break;
case BlueAlpha: set_alpha(0b001); break;
case MagentaAlpha: set_alpha(0b101); break;
case CyanAlpha: set_alpha(0b011); break;
case WhiteAlpha: set_alpha(0b111); break;
case Flash: flash_ = true; break;
case Steady: flash_ = false; break;
case RedGraphics: set_graphics(0b100); break;
case GreenGraphics: set_graphics(0b010); break;
case YellowGraphics: set_graphics(0b110); break;
case BlueGraphics: set_graphics(0b001); break;
case MagentaGraphics: set_graphics(0b101); break;
case CyanGraphics: set_graphics(0b011); break;
case WhiteGraphics: set_graphics(0b111); break;
case Conceal: conceal_ = true; break;
case ContinuousGraphics: separated_graphics_ = false; break;
case SeparatedGraphics: separated_graphics_ = true; break;
case NormalHeight: double_height_ = false; break;
case DoubleHeight: double_height_ = row_has_double_height_ = true; break;
case BlackBackground: output_.background = 0; break;
case NewBackground: output_.background = output_.alpha; break;
case HoldGraphics: hold_graphics_ = true; break;
case ReleaseGraphics: hold_graphics_ = false; last_graphic_ = 32; break;
}
}
void SAA5050Serialiser::set_reveal(const bool reveal) {
reveal_ = reveal;
}
void SAA5050Serialiser::add(const Numeric::SizedInt<7> c) {
has_output_ = true;
if(c.get() < 32) {
if(hold_graphics_) {
load_pixels(last_graphic_);
} else {
output_.reset();
}
apply_control(c.get());
return;
}
load_pixels(c.get());
}
void SAA5050Serialiser::load_pixels(const uint8_t c) {
if(flash_ && ((frame_counter_&31) > 23)) { // Complete guess on the blink period here.
output_.reset();
return;
}
if(conceal_ && !reveal_) {
output_.reset();
return;
}
// Divert into graphics only if both the mode and the character code allows it.
if(!alpha_mode_ && (c & (1 << 5))) {
last_graphic_ = c;
// Graphics layout:
//
// |----|----|
// | | |
// | b0 | b1 |
// | | |
// |----|----|
// | | |
// | b2 | b3 |
// | | |
// |----|----|
// | | |
// | b4 | b6 |
// | | |
// |----|----|
if(separated_graphics_ && (line_ == 6 || line_ == 12 || line_ == 18)) {
output_.reset();
return;
}
uint8_t pixels;
if(line_ < 6) {
pixels =
((c & 1) ? 0b111'000 : 0) |
((c & 2) ? 0b000'111 : 0);
} else if(line_ < 14) {
pixels =
((c & 4) ? 0b111'000 : 0) |
((c & 8) ? 0b000'111 : 0);
} else {
pixels =
((c & 16) ? 0b111'000 : 0) |
((c & 64) ? 0b000'111 : 0);
}
if(separated_graphics_) {
pixels &= 0b011'011;
}
output_.load(pixels);
return;
}
if(double_height_) {
const auto top_address = (line_ >> 2) + double_height_offset_;
const uint8_t top = font[c - 32][top_address];
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
if(line_ & 2) {
output_.load(bottom, top);
} else {
output_.load(top, bottom);
}
} else {
if(double_height_offset_) {
output_.reset();
} else {
const auto top_address = line_ >> 1;
const uint8_t top = font[c - 32][top_address];
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
if(odd_frame_) {
output_.load(bottom, top);
} else {
output_.load(top, bottom);
}
}
}
}

View File

@@ -0,0 +1,100 @@
//
// SAA5050.hpp
// Clock Signal
//
// Created by Thomas Harte on 24/09/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include <cstdint>
#include "Numeric/SizedInt.hpp"
namespace Mullard {
struct SAA5050Serialiser {
public:
void begin_frame(bool is_odd);
void begin_line();
void add(Numeric::SizedInt<7>);
struct Output {
void reset() {
top_ = bottom_ = 0;
}
void load(const uint8_t top, const uint8_t bottom) {
top_ = top;
bottom_ = bottom;
}
void load(const uint8_t top) {
top_ = bottom_ = top;
}
// The low twelve bits of this word provide 1bpp pixels.
uint16_t pixels() const {
// Adapted from old ElectrEm source; my original provenance for this being the correct logic is unknown.
uint16_t wide =
((top_ & 0b000001) ? 0b0000'0000'0011 : 0) |
((top_ & 0b000010) ? 0b0000'0000'1100 : 0) |
((top_ & 0b000100) ? 0b0000'0011'0000 : 0) |
((top_ & 0b001000) ? 0b0000'1100'0000 : 0) |
((top_ & 0b010000) ? 0b0011'0000'0000 : 0) |
((top_ & 0b100000) ? 0b1100'0000'0000 : 0);
if(top_ != bottom_) {
if((top_ & 0b10000) && (bottom_ & 0b11000) == 0b01000) wide |= 0b0000'1000'0000;
if((top_ & 0b01000) && (bottom_ & 0b01100) == 0b00100) wide |= 0b0000'0010'0000;
if((top_ & 0b00100) && (bottom_ & 0b00110) == 0b00010) wide |= 0b0000'0000'1000;
if((top_ & 0b00010) && (bottom_ & 0b00011) == 0b00001) wide |= 0b0000'0000'0010;
if((top_ & 0b01000) && (bottom_ & 0b11000) == 0b10000) wide |= 0b0001'0000'0000;
if((top_ & 0b00100) && (bottom_ & 0b01100) == 0b01000) wide |= 0b0000'0100'0000;
if((top_ & 0b00010) && (bottom_ & 0b00110) == 0b00100) wide |= 0b0000'0001'0000;
if((top_ & 0b00001) && (bottom_ & 0b00011) == 0b00010) wide |= 0b0000'0000'0100;
}
return wide;
}
// Colours for foreground and background pixels.
uint8_t alpha;
uint8_t background;
private:
uint8_t top_, bottom_;
};
bool has_output() const;
Output output();
void set_reveal(bool);
private:
Output output_;
bool has_output_ = false;
int row_, line_;
bool odd_frame_;
bool flash_ = false;
int frame_counter_ = 0;
bool reveal_ = false;
bool conceal_ = false;
bool alpha_mode_ = true;
bool separated_graphics_ = false;
bool double_height_ = false;
bool row_has_double_height_ = false;
int double_height_offset_ = 0;
bool hold_graphics_ = false;
uint8_t last_graphic_ = 0;
void load_pixels(const uint8_t);
void apply_control(const uint8_t);
};
}

View File

@@ -0,0 +1,85 @@
//
// uPD7002.cpp
// Clock Signal
//
// Created by Thomas Harte on 17/09/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "uPD7002.hpp"
using namespace NEC;
uPD7002::uPD7002(const HalfCycles clock_rate) {
// Per the BBC AUG: "8 bit conversions typically take 4 ms to complete whereas 10 bit
// conversions typically take 10 ms to complete".
fast_period_ = clock_rate / 250;
slow_period_ = clock_rate / 100;
}
void uPD7002::run_for(const HalfCycles count) {
if(!conversion_time_remaining_) {
return;
}
if(count >= conversion_time_remaining_) {
conversion_time_remaining_ = HalfCycles(0);
result_ = uint16_t(inputs_[channel_] * 65535.0f) & (high_precision_ ? 0xfff0 : 0xff00);
set_interrupt(true);
return;
}
conversion_time_remaining_ -= count;
}
bool uPD7002::interrupt() const {
return interrupt_;
}
void uPD7002::write(const uint16_t address, const uint8_t value) {
const auto local_address = address & 3;
if(!local_address) {
channel_ = value & 0b0000'0011;
spare_ = value & 0b0000'0100;
high_precision_ = value & 0b0000'1000;
conversion_time_remaining_ = high_precision_ ? slow_period_ : fast_period_;
set_interrupt(false);
return;
}
}
uint8_t uPD7002::read(const uint16_t address) {
switch(address & 3) {
default:
case 0: return status();
case 1:
set_interrupt(false);
return uint8_t(result_ >> 8);
case 2: return uint8_t(result_);
case 3: return 0xff;
}
}
uint8_t uPD7002::status() const {
return
channel_ |
spare_ |
(high_precision_ ? 0x08 : 0) |
((result_ >> 14) & 0x30) |
(conversion_time_remaining_ > HalfCycles(0) ? 0x00 : 0x40) |
(interrupt_ ? 0x00 : 0x80);
}
void uPD7002::set_delegate(Delegate *const delegate) {
delegate_ = delegate;
}
void uPD7002::set_interrupt(const bool value) {
if(interrupt_ == value) return;
interrupt_ = value;
if(delegate_) delegate_->did_change_interrupt_status(*this);
}
void uPD7002::set_input(const int channel, const float value) {
inputs_[channel] = value;
}

View File

@@ -0,0 +1,51 @@
//
// uPD7002.hpp
// Clock Signal
//
// Created by Thomas Harte on 17/09/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "ClockReceiver/ClockReceiver.hpp"
namespace NEC {
class uPD7002 {
public:
/// Constructs a PD7002 that will receive @c run_for updates at the specified clock rate.
uPD7002(HalfCycles clock_rate);
void run_for(HalfCycles);
/// @returns The current state of the interrupt line.
bool interrupt() const;
/// Defines a mean for an observer to receive notifications upon updates to the interrupt line.
struct Delegate {
virtual void did_change_interrupt_status(uPD7002 &) = 0;
};
void set_delegate(Delegate *);
void write(uint16_t address, uint8_t value);
uint8_t read(uint16_t address);
/// Sets the floating point value, which should be in the range [0.0, 1.0], for the signal currently
/// being supplied to @c channel.
void set_input(int channel, float value);
private:
float inputs_[4]{};
uint16_t result_ = 0;
bool interrupt_ = false;
uint8_t channel_ = 0, spare_ = 0;
bool high_precision_ = false;
HalfCycles conversion_time_remaining_{};
HalfCycles fast_period_, slow_period_;
uint8_t status() const;
void set_interrupt(bool);
Delegate *delegate_ = nullptr;
};
}

View File

@@ -165,10 +165,11 @@ public:
const bool is_analogue_axis = input.is_analogue_axis();
if(is_digital_axis || is_analogue_axis) {
const size_t required_size = size_t(input.info.control.index+1);
if(stick_types_.size() < required_size) {
stick_types_.resize(required_size);
if(sticks_.size() < required_size) {
sticks_.resize(required_size);
}
stick_types_[size_t(input.info.control.index)] = is_digital_axis ? StickType::Digital : StickType::Analogue;
sticks_[size_t(input.info.control.index)].type =
is_digital_axis ? Stick::Type::Digital : Stick::Type::Analogue;
}
}
}
@@ -179,26 +180,50 @@ public:
void set_input(const Input &input, const bool is_active) final {
// If this is a digital setting to a digital property, just pass it along.
if(input.is_button() || stick_types_[input.info.control.index] == StickType::Digital) {
if(input.is_button() || sticks_[input.info.control.index].type == Stick::Type::Digital) {
did_set_input(input, is_active);
return;
}
// Otherwise this is logically to an analogue axis; for now just use some
// convenient hard-coded values. TODO: make these a function of time.
using Type = Joystick::Input::Type;
// Otherwise this is logically to an analogue axis; map appropriately.
// TODO: make these a function of time.
auto &stick = sticks_[input.info.control.index];
stick.apply_digital(input, is_active);
const auto analogue_value = [&](const int mask) {
switch(mask) {
default: return 0.5f;
case 0b01: return digital_maximum();
case 0b10: return digital_minimum();
}
};
switch(input.type) {
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.1f : 0.5f); break;
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.9f : 0.5f); break;
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.1f : 0.5f); break;
case Type::Down: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.9f : 0.5f); break;
using enum Joystick::Input::Type;
default:
did_set_input(input, is_active ? 1.0f : 0.0f);
break;
case Left:
case Right:
did_set_input(
Input(Horizontal, input.info.control.index),
analogue_value(stick.digital_mask(Horizontal))
);
break;
case Up:
case Down:
did_set_input(
Input(Vertical, input.info.control.index),
analogue_value(stick.digital_mask(Vertical))
);
break;
}
}
void set_input(const Input &input, const float value) final {
// If this is an analogue setting to an analogue property, just pass it along.
if(!input.is_button() && stick_types_[input.info.control.index] == StickType::Analogue) {
if(!input.is_button() && sticks_[input.info.control.index].type == Stick::Type::Analogue) {
did_set_input(input, value);
return;
}
@@ -206,7 +231,9 @@ public:
// Otherwise apply a threshold test to convert to digital, with remapping from axes to digital inputs.
using Type = Joystick::Input::Type;
switch(input.type) {
default: did_set_input(input, value > 0.5f); break;
default:
did_set_input(input, value > 0.5f);
break;
case Type::Horizontal:
did_set_input(Input(Type::Left, input.info.control.index), value <= 0.25f);
did_set_input(Input(Type::Right, input.info.control.index), value >= 0.75f);
@@ -221,15 +248,44 @@ public:
protected:
virtual void did_set_input([[maybe_unused]] const Input &input, [[maybe_unused]] float value) {}
virtual void did_set_input([[maybe_unused]] const Input &input, [[maybe_unused]] bool value) {}
virtual float digital_minimum() const { return 0.1f; }
virtual float digital_maximum() const { return 0.9f; }
private:
const std::vector<Input> inputs_;
enum class StickType {
Digital,
Analogue
struct Stick {
enum class Type {
Digital,
Analogue
} type;
void apply_digital(const Input &input, const bool is_active) {
const int mask = [&] {
switch(input.type) {
default: return 0;
case Input::Type::Up: return 1 << 1;
case Input::Type::Down: return 1 << 2;
case Input::Type::Right: return 1 << 3;
case Input::Type::Left: return 1 << 4;
}
} ();
if(is_active) {
digital_inputs_ |= mask;
} else {
digital_inputs_ &= ~mask;
}
}
int digital_mask(const Input::Type axis) const {
switch(axis) {
default: return 0;
case Input::Type::Horizontal: return (digital_inputs_ >> 3) & 3;
case Input::Type::Vertical: return (digital_inputs_ >> 1) & 3;
}
}
int digital_inputs_ = 0;
};
std::vector<StickType> stick_types_;
std::vector<Stick> sticks_;
};
}

View File

@@ -7,17 +7,24 @@
//
#include "BBCMicro.hpp"
#include "Keyboard.hpp"
#include "Activity/Source.hpp"
#include "Machines/MachineTypes.hpp"
#include "Machines/Utility/MemoryFuzzer.hpp"
#include "Machines/Utility/Typer.hpp"
#include "Processors/6502/6502.hpp"
#include "Components/6522/6522.hpp"
#include "Components/6845/CRTC6845.hpp"
#include "Components/SN76489/SN76489.hpp"
#include "Components/6850/6850.hpp"
#include "Components/SAA5050/SAA5050.hpp"
#include "Components/SN76489/SN76489.hpp"
#include "Components/uPD7002/uPD7002.hpp"
// TODO: factor this more appropriately.
#include "Machines/Acorn/Electron/Plus3.hpp"
#include "Analyser/Static/Acorn/Target.hpp"
#include "Outputs/Log.hpp"
@@ -26,6 +33,8 @@
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
#include "Keyboard.hpp"
#include <algorithm>
#include <array>
#include <bitset>
@@ -37,15 +46,62 @@ namespace BBCMicro {
namespace {
using Logger = Log::Logger<Log::Source::BBCMicro>;
/*!
Provides an analogue joystick with a single fire button.
*/
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick(NEC::uPD7002 &adc, const int first_channel) :
ConcreteJoystick({
Input(Input::Horizontal),
Input(Input::Vertical),
Input(Input::Fire)
}),
adc_(adc),
first_channel_(first_channel) {}
void did_set_input(const Input &input, const float value) final {
switch(input.type) {
case Input::Horizontal:
case Input::Vertical:
adc_.set_input(first_channel_ + (input.type == Input::Vertical), 1.0f - value);
break;
default: break;
}
}
void did_set_input(const Input &input, const bool is_active) final {
if(input.type == Input::Fire) {
fire_ = is_active;
}
}
bool fire() const {
return fire_;
}
private:
float digital_minimum() const final {
return 0.0f;
}
float digital_maximum() const final {
return 1.0f;
}
NEC::uPD7002 &adc_;
const int first_channel_;
bool fire_ = false;
};
/*!
Combines an SN76489 with an appropriate asynchronous queue and filtering speaker.
*/
struct Audio {
Audio() :
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_),
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 2),
speaker_(sn76489_)
{
// I'm *VERY* unsure about this.
// Combined with the additional divider specified above, implies this chip is clocked at 4Mhz.
speaker_.set_input_rate(2'000'000.0f);
}
@@ -54,16 +110,16 @@ struct Audio {
}
TI::SN76489 *operator ->() {
flush();
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
return &sn76489_;
}
void operator +=(const HalfCycles duration) {
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
void operator +=(const Cycles duration) {
time_since_update_ += duration;
}
void flush() {
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
audio_queue_.perform();
}
@@ -75,7 +131,7 @@ private:
Concurrency::AsyncTaskQueue<false> audio_queue_;
TI::SN76489 sn76489_;
Outputs::Speaker::PullLowpass<TI::SN76489> speaker_;
HalfCycles time_since_update_;
Cycles time_since_update_;
};
/*!
@@ -109,13 +165,21 @@ struct SystemVIAPortHandler;
using SystemVIA = MOS::MOS6522::MOS6522<SystemVIAPortHandler>;
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
SystemVIAPortHandler(Audio &audio, VideoBaseAddress &video_base, SystemVIA &via) :
audio_(audio), video_base_(video_base), via_(via)
struct Delegate {
virtual void strobe_lightpen() = 0;
};
SystemVIAPortHandler(
Audio &audio,
VideoBaseAddress &video_base,
SystemVIA &via,
Delegate &delegate,
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks,
const bool run_disk
) :
audio_(audio), video_base_(video_base), via_(via), joysticks_(joysticks), delegate_(delegate)
{
// Set initial mode to mode 0.
set_key(7, true);
set_key(8, true);
set_key(9, true);
set_key_flag(6, run_disk);
}
// CA2: key pressed;
@@ -157,7 +221,18 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
// Update keyboard LEDs.
if(mask >= 0x40) {
Logger::info().append("CAPS: %d SHIFT: %d", bool(latch_ & 0x40), bool(latch_ & 0x40));
const bool new_caps = latch_ & 0x80;
const bool new_shift = latch_ & 0x40;
if(new_caps != caps_led_state_) {
caps_led_state_ = new_caps;
activity_observer_->set_led_status(caps_led, caps_led_state_);
}
if(new_shift != shift_led_state_) {
shift_led_state_ = new_shift;
activity_observer_->set_led_status(shift_led, shift_led_state_);
}
}
}
@@ -166,9 +241,12 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
if(port == MOS::MOS6522::Port::B) {
// TODO:
//
// b4/5: joystick fire buttons;
// b6/7: speech interrupt/ready inputs.
return 0x3f; // b6 = b7 = 0 => no speech hardware.
// b4/5: joystick fire buttons (0 = pressed);
// b6/7: speech interrupt/ready inputs. (0 expected if no speech hardware)
return
0xf |
(static_cast<Joystick *>(joysticks_[0].get())->fire() ? 0x00 : 0x10) |
(static_cast<Joystick *>(joysticks_[1].get())->fire() ? 0x00 : 0x20);
}
if(latch_ & LatchFlags::KeyboardIsScanning) {
@@ -180,8 +258,23 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
return key_state;
}
template<MOS::MOS6522::Port port, MOS::MOS6522::Line line>
void set_control_line_output(const bool value) {
if constexpr (port == MOS::MOS6522::Port::B && line == MOS::MOS6522::Line::Two) {
if(previous_cb2_ != value && !value) {
delegate_.strobe_lightpen();
}
previous_cb2_ = value;
}
}
void set_key(const uint8_t key, const bool pressed) {
key_column(key)[key_row(key)] = pressed;
set_key_flag(key, pressed);
update_ca2();
}
void clear_all_keys() {
key_states_ = std::array<KeyRow, 16>{};
update_ca2();
}
@@ -199,6 +292,22 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
keyboard_scan_column_ = ending_column;
}
void set_activity_observer(Activity::Observer *const observer) {
activity_observer_ = observer;
if(activity_observer_) {
activity_observer_->register_led(caps_led, Activity::Observer::LEDPresentation::Persistent);
activity_observer_->register_led(shift_led, Activity::Observer::LEDPresentation::Persistent);
activity_observer_->set_led_status(caps_led, caps_led_state_);
activity_observer_->set_led_status(shift_led, shift_led_state_);
}
}
bool caps_lock() const {
return caps_led_state_;
}
private:
uint8_t latch_ = 0;
enum LatchFlags: uint8_t {
@@ -207,6 +316,7 @@ private:
};
uint8_t port_a_output_ = 0;
bool previous_cb2_ = false;
Audio &audio_;
VideoBaseAddress &video_base_;
@@ -219,6 +329,9 @@ private:
std::array<KeyRow, 16> key_states_{};
int keyboard_scan_column_ = 0;
void set_key_flag(const uint8_t key, const bool pressed) {
key_column(key)[key_row(key)] = pressed;
}
KeyRow &key_column(const uint8_t key) {
return key_states_[key & 0xf];
}
@@ -241,6 +354,15 @@ private:
via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
}
static inline const std::string caps_led = "CAPS";
static inline const std::string shift_led = "SHIFT";
bool caps_led_state_ = false;
bool shift_led_state_ = false;
Activity::Observer *activity_observer_ = nullptr;
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks_;
Delegate &delegate_;
};
/*!
@@ -251,20 +373,29 @@ public:
CRTCBusHandler(const uint8_t *const ram, SystemVIA &system_via) :
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
ram_(ram),
system_via_(system_via) {}
system_via_(system_via)
{
crt_.set_visible_area(crt_.get_rect_for_area(30, 256, 160, 800, 4.0f / 3.0f));
}
void set_palette(const uint8_t value) {
const auto index = value >> 4;
Logger::info().append("Palette entry %d set to %x", index, value & 0xf);
palette_[index] = uint8_t(
7 ^ (
((value & 0b100) >> 2) |
((value & 0b001) << 2) |
(value & 0b010)
)
);
flash_flags_[size_t(index)] = value & 0b1000;
}
void set_control(const uint8_t value) {
Logger::info().append("Video control set to %x", value);
cycle_length_ = (value & 0x10) ? 8 : 16;
Logger::info().append("TODO: video control => flash %d", bool(value & 0x01));
Logger::info().append("TODO: video control => teletext %d", bool(value & 0x02));
Logger::info().append("TODO: video control => columns %d", (value >> 2) & 0x03);
Logger::info().append("TODO: video control => cursor segment %d%d%d", bool(value & 0x80), bool(value & 0x40), bool(value & 0x20));
active_collation_.crtc_clock_multiplier = (value & 0x10) ? 1 : 2;
active_collation_.pixels_per_clock = 1 << ((value >> 2) & 0x03);
active_collation_.is_teletext = value & 0x02;
flash_mask_ = value & 0x01 ? 7 : 0;
cursor_mask_ = value & 0b1110'0000;
}
/*!
@@ -272,60 +403,104 @@ public:
bus state and determines what output to produce based on the current palette and mode.
*/
void perform_bus_cycle(const Motorola::CRTC::BusState &state) {
static constexpr size_t PixelAllocationUnit = 480; // Is assumed to be a multiple of both 12 and 16.
// i.e. a multiple of 48.
static_assert(!(PixelAllocationUnit % 16));
static_assert(!(PixelAllocationUnit % 12));
system_via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(state.vsync);
// bool print = false;
// uint16_t start_address = 0x7c00;
// int rows = 24;
// if(print) {
// for(int y = 0; y < rows; y++) {
// for(int x = 0; x < 40; x++) {
// printf("%c", ram_[start_address + y*40 + x]);
// }
// printf("\n");
// }
// }
// Count cycles since horizontal sync to insert a colour burst.
if(state.hsync) {
++cycles_into_hsync_;
} else {
cycles_into_hsync_ = 0;
}
const bool is_colour_burst = (cycles_into_hsync_ >= 5 && cycles_into_hsync_ < 9);
// TODO: this is copy/pasted from the CPC. How does the BBC do it?
// if(state.hsync) {
// ++cycles_into_hsync_;
// } else {
// cycles_into_hsync_ = 0;
// }
// const bool is_colour_burst = cycles_into_hsync_ >= 5 && cycles_into_hsync_ < 9;
// Sync is taken to override pixels, and is combined as a simple OR.
const bool is_sync = state.hsync || state.vsync;
const bool is_blank = !is_sync && state.hsync;
OutputMode output_mode;
if(is_sync) {
output_mode = OutputMode::Sync;
} else if(is_colour_burst) {
output_mode = OutputMode::ColourBurst;
} else if(is_blank) {
output_mode = OutputMode::Blank;
} else if(state.display_enable) {
output_mode = OutputMode::Pixels;
} else {
output_mode = OutputMode::Border;
// Check for a cursor leading edge.
cursor_shifter_ >>= 4;
if(state.cursor != previous_cursor_enabled_) {
if(state.cursor) {
cursor_shifter_ =
((cursor_mask_ & 0x80) ? 0x0007 : 0) |
((cursor_mask_ & 0x40) ? 0x0070 : 0) |
((cursor_mask_ & 0x20) ? 0x7700 : 0);
}
previous_cursor_enabled_ = state.cursor;
}
// Consider some SAA5050 signalling.
if(!state.vsync && previous_vsync_) {
// Complete fiction here; the SAA5050 field flag is set by peeking inside CRTC state.
// TODO: what really sets CRS for the SAA5050? Time since hsync maybe?
saa5050_serialiser_.begin_frame(state.field_count.bit<0>());
}
previous_vsync_ = state.vsync;
if(state.display_enable && !previous_display_enabled_ && active_collation_.is_teletext) {
saa5050_serialiser_.begin_line();
}
previous_display_enabled_ = state.display_enable;
// Grab 5050 output, if any.
bool has_5050_output_ = saa5050_serialiser_.has_output();
const auto saa_50505_output_ = saa5050_serialiser_.output();
// Fetch, possibly.
const bool should_fetch = state.display_enable && (active_collation_.is_teletext || !(state.line.get() & 8));
if(should_fetch) {
const uint16_t address = [&] {
// Teletext address generation.
if(state.refresh.get() & (1 << 13)) {
return uint16_t(
0x3c00 |
((state.refresh.get() & 0x800) << 3) |
(state.refresh.get() & 0x3ff)
);
}
uint16_t address = uint16_t((state.refresh.get() << 3) | (state.line.get() & 7));
if(address & 0x8000) {
address = (address + video_base_) & 0x7fff;
}
return address;
} ();
const uint8_t fetched = ram_[address];
pixel_shifter_ = fetched;
saa5050_serialiser_.add(fetched);
}
// Pick new output mode.
const OutputMode output_mode = [&] {
if(state.hsync || state.vsync) {
return OutputMode::Sync;
}
// if(is_colour_burst) {
// return OutputMode::ColourBurst;
// }
if(
(should_fetch && !active_collation_.is_teletext) ||
(has_5050_output_ && active_collation_.is_teletext) ||
cursor_shifter_
) {
return OutputMode::Pixels;
}
return OutputMode::Blank;
} ();
// If a transition between sync/border/pixels just occurred, flush whatever was
// in progress to the CRT and reset counting.
if(output_mode != previous_output_mode_) {
// in progress to the CRT and reset counting. Also flush if this mode has just been effective
// for a really long time, so as not to buffer too much.
if(output_mode != previous_output_mode_ || cycles_ == 1024) {
if(cycles_) {
switch(previous_output_mode_) {
default:
case OutputMode::Blank: crt_.output_blank(cycles_); break;
case OutputMode::Sync: crt_.output_sync(cycles_); break;
case OutputMode::Border: crt_.output_blank(cycles_); break;
case OutputMode::ColourBurst: crt_.output_default_colour_burst(cycles_); break;
case OutputMode::Pixels:
crt_.output_data(cycles_, pixels_);
pixel_pointer_ = pixel_data_ = nullptr;
pixels_ = 0;
break;
case OutputMode::Pixels: flush_pixels(); break;
}
}
@@ -333,61 +508,50 @@ public:
previous_output_mode_ = output_mode;
}
// Increment cycles since state changed.
cycles_ += cycle_length_;
// Collect some more pixels if output is ongoing.
if(previous_output_mode_ == OutputMode::Pixels) {
if(!pixel_data_) {
pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8);
if(output_mode == OutputMode::Pixels) {
// Flush the current buffer pixel if full; the CRTC allows many different display
// widths so it's not necessarily possible to predict the correct number in advance
// and using the upper bound could lead to inefficient behaviour.
if(pixel_data_ && (pixels_collected() == PixelAllocationUnit || active_collation_ != previous_collation_)) {
flush_pixels();
cycles_ = 0;
}
if(pixel_pointer_) {
uint16_t address;
previous_collation_ = active_collation_;
if(state.refresh_address & (1 << 13)) {
// Teletext address generation mode.
address = uint16_t(
0x3c00 |
((state.refresh_address & 0x800) << 3) |
(state.refresh_address & 0x3ff)
);
// TODO: wraparound?
} else {
address = uint16_t((state.refresh_address << 3) | (state.row_address & 7));
if(address & 0x8000) {
address = (address + video_base_) & 0x7fff;
if(!pixel_data_) {
pixel_pointer_ = pixel_data_ = crt_.begin_data(PixelAllocationUnit);
}
if(pixel_data_) {
if(active_collation_.is_teletext) {
if(has_5050_output_) {
uint16_t pixels = saa_50505_output_.pixels();
for(int c = 0; c < 12; c++) {
*pixel_pointer_++ =
((pixels & 0b1000'0000'0000) ? saa_50505_output_.alpha : saa_50505_output_.background)
^ uint8_t(cursor_shifter_);
pixels <<= 1;
}
} else {
std::fill(pixel_pointer_, pixel_pointer_ + 12, 0);
pixel_pointer_ += 12;
}
}
// Hard coded from here for Mode 0!
if(state.row_address & 8) {
std::fill(pixel_pointer_, pixel_pointer_+8, 0);
} else {
const auto source = ram_[address];
pixel_pointer_[0] = (source & 0x80) ? 0xff : 0x00;
pixel_pointer_[1] = (source & 0x40) ? 0xff : 0x00;
pixel_pointer_[2] = (source & 0x20) ? 0xff : 0x00;
pixel_pointer_[3] = (source & 0x10) ? 0xff : 0x00;
pixel_pointer_[4] = (source & 0x08) ? 0xff : 0x00;
pixel_pointer_[5] = (source & 0x04) ? 0xff : 0x00;
pixel_pointer_[6] = (source & 0x02) ? 0xff : 0x00;
pixel_pointer_[7] = (source & 0x01) ? 0xff : 0x00;
}
pixel_pointer_ += 8;
pixels_ += 8;
// Flush the current buffer pixel if full; the CRTC allows many different display
// widths so it's not necessarily possible to predict the correct number in advance
// and using the upper bound could lead to inefficient behaviour.
if(pixels_ == 320) {
crt_.output_data(cycles_, pixels_);
pixel_pointer_ = pixel_data_ = nullptr;
cycles_ = 0;
pixels_ = 0;
switch(active_collation_.crtc_clock_multiplier * active_collation_.pixels_per_clock) {
case 1: shift_pixels<1>(cursor_shifter_ & 7); break;
case 2: shift_pixels<2>(cursor_shifter_ & 7); break;
case 4: shift_pixels<4>(cursor_shifter_ & 7); break;
case 8: shift_pixels<8>(cursor_shifter_ & 7); break;
case 16: shift_pixels<16>(cursor_shifter_ & 7); break;
default: break;
}
}
}
}
// Increment cycles since state changed.
cycles_ += active_collation_.crtc_clock_multiplier << 3;
}
/// Sets the destination for output.
@@ -410,40 +574,99 @@ public:
return crt_.get_display_type();
}
private:
enum class OutputMode {
Sync,
Blank,
ColourBurst,
Border,
Pixels
} previous_output_mode_ = OutputMode::Sync;
};
struct PixelCollation {
int crtc_clock_multiplier = 1;
int pixels_per_clock = 4;
bool is_teletext = false;
bool operator !=(const PixelCollation &rhs) {
// If both are teletext, just inspect the clock multiplier.
if(is_teletext && rhs.is_teletext) {
return crtc_clock_multiplier != rhs.crtc_clock_multiplier;
}
// If one is teletext but the other isn't, that's a sufficient difference.
if(is_teletext != rhs.is_teletext) return true;
// Compare pixel clock rate.
return pixels_per_clock != rhs.pixels_per_clock || crtc_clock_multiplier != rhs.crtc_clock_multiplier;
}
};
OutputMode previous_output_mode_ = OutputMode::Sync;
int cycles_ = 0;
int cycles_into_hsync_ = 0;
int cycle_length_ = 8;
Outputs::CRT::CRT crt_;
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
size_t pixels_;
size_t pixels_collected() const {
return size_t(pixel_pointer_ - pixel_data_);
}
void flush_pixels() {
crt_.output_data(cycles_, pixels_collected());
pixel_pointer_ = pixel_data_ = nullptr;
}
PixelCollation previous_collation_;
uint8_t palette_[16];
std::bitset<16> flash_flags_;
uint8_t flash_mask_ = 0;
PixelCollation active_collation_;
uint8_t pixel_shifter_ = 0;
uint8_t cursor_mask_ = 0;
uint32_t cursor_shifter_ = 0;
bool previous_cursor_enabled_ = false;
bool previous_display_enabled_ = false;
bool previous_vsync_ = false;
template <int count> void shift_pixels(const uint8_t cursor_mask) {
for(int c = 0; c < count; c++) {
const uint8_t colour =
((pixel_shifter_ & 0x80) >> 4) |
((pixel_shifter_ & 0x20) >> 3) |
((pixel_shifter_ & 0x08) >> 2) |
((pixel_shifter_ & 0x02) >> 1);
pixel_shifter_ <<= 1;
*pixel_pointer_++ = palette_[colour] ^ (flash_flags_[colour] ? flash_mask_ : 0x00) ^ cursor_mask;
}
}
const uint8_t *const ram_ = nullptr;
SystemVIA &system_via_;
Mullard::SAA5050Serialiser saa5050_serialiser_;
};
using CRTC = Motorola::CRTC::CRTC6845<
CRTCBusHandler,
Motorola::CRTC::Personality::HD6845S,
Motorola::CRTC::CursorType::None>;
Motorola::CRTC::CursorType::Native>;
}
template <bool has_1770>
class ConcreteMachine:
public Activity::Source,
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::JoystickMachine,
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::MediaTarget,
public MachineTypes::ScanProducer,
public MachineTypes::TimedMachine,
public MOS::MOS6522::IRQDelegatePortHandler::Delegate
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public NEC::uPD7002::Delegate,
public SystemVIAPortHandler::Delegate,
public Utility::TypeRecipient<CharacterMapper>,
public WD::WD1770::Delegate
{
public:
ConcreteMachine(
@@ -451,22 +674,36 @@ public:
const ROMMachine::ROMFetcher &rom_fetcher
) :
m6502_(*this),
system_via_port_handler_(audio_, crtc_bus_handler_, system_via_),
system_via_port_handler_(audio_, crtc_bus_handler_, system_via_, *this, joysticks_, target.should_shift_restart),
user_via_(user_via_port_handler_),
system_via_(system_via_port_handler_),
crtc_bus_handler_(ram_.data(), system_via_),
crtc_(crtc_bus_handler_),
acia_(HalfCycles(2'000'000)) // TODO: look up real ACIA clock rate.
acia_(HalfCycles(2'000'000)), // TODO: look up real ACIA clock rate.
adc_(HalfCycles(2'000'000))
{
set_clock_rate(2'000'000);
// Install two joysticks.
joysticks_.emplace_back(new Joystick(adc_, 0));
joysticks_.emplace_back(new Joystick(adc_, 2));
system_via_port_handler_.set_interrupt_delegate(this);
user_via_port_handler_.set_interrupt_delegate(this);
adc_.set_delegate(this);
// Grab ROMs.
using Request = ::ROM::Request;
using Name = ::ROM::Name;
const auto request = Request(Name::AcornBASICII) && Request(Name::BBCMicroMOS12);
auto request = Request(Name::AcornBASICII) && Request(Name::BBCMicroMOS12);
if(target.has_1770dfs) {
request = request && Request(Name::BBCMicroDFS226);
}
if(target.has_adfs) {
request = request && Request(Name::BBCMicroADFS130);
}
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
@@ -475,8 +712,27 @@ public:
const auto os_data = roms.find(Name::BBCMicroMOS12)->second;
std::copy(os_data.begin(), os_data.end(), os_.begin());
// Put BASIC in pole position.
install_sideways(15, roms.find(Name::AcornBASICII)->second, false);
// Install filing systems: put the DFS before the ADFS because it's more common on the BBC.
size_t fs_slot = 14;
if(target.has_1770dfs) {
install_sideways(fs_slot--, roms.find(Name::BBCMicroDFS226)->second, false);
}
if(target.has_adfs) {
install_sideways(fs_slot--, roms.find(Name::BBCMicroADFS130)->second, false);
}
// Throw sideways RAM into all unused slots.
if(target.has_sideways_ram) {
for(size_t c = 0; c < 16; c++) {
if(!rom_inserted_[c]) {
rom_inserted_[c] = rom_write_masks_[c] = true;
}
}
}
// Setup fixed parts of memory map.
page(0, &ram_[0], true);
page(1, &ram_[16384], true);
@@ -484,7 +740,14 @@ public:
page(3, os_.data(), false);
Memory::Fuzz(ram_);
(void)target;
if constexpr (has_1770) {
wd1770_.set_delegate(this);
}
insert_media(target.media);
if(!target.loading_command.empty()) {
type_string(target.loading_command);
}
}
// MARK: - 6502 bus.
@@ -516,7 +779,8 @@ public:
};
// Determine whether this access hits the 1Mhz bus; if so then apply appropriate penalty, and update phase.
const auto duration = is_1mhz(address) ? Cycles(2 + (phase_&1)) : Cycles(1);
const auto duration = Cycles(is_1mhz(address) ? 2 + (phase_&1) : 1);
if(typer_) typer_->run_for(duration);
phase_ += duration.as<int>();
@@ -524,7 +788,6 @@ public:
// 1Mhz devices.
//
const auto half_cycles = HalfCycles(duration.as_integral());
audio_ += half_cycles;
system_via_.run_for(half_cycles);
system_via_port_handler_.advance_keyboard_scan(half_cycles);
user_via_.run_for(half_cycles);
@@ -533,7 +796,7 @@ public:
//
// 2Mhz devices.
//
// TODO: if CRTC clock is 1Mhz, adapt.
audio_ += duration;
if(crtc_2mhz_) {
crtc_.run_for(duration);
} else {
@@ -541,8 +804,14 @@ public:
const auto cycles = (phase_ >> 1) - ((phase_ - duration.as<int>()) >> 1);
crtc_.run_for(Cycles(cycles));
}
adc_.run_for(duration);
if constexpr (has_1770) {
// The WD1770 is nominally clocked at 8Mhz.
wd1770_.run_for(duration * 4);
}
//
// Questionably-clocked devices.
//
@@ -552,7 +821,6 @@ public:
//
// Check for an IO access; if found then perform that and exit.
//
// static bool log = false;
if(address >= 0xfc00 && address < 0xff00) {
if(address >= 0xfe40 && address < 0xfe60) {
if(is_read(operation)) {
@@ -615,6 +883,27 @@ public:
// Logger::info().append("ACIA write: %02x", *value);
acia_.write(address, *value);
}
} else if(address >= 0xfec0 && address < 0xfee0) {
if(is_read(operation)) {
*value = adc_.read(address);
} else {
adc_.write(address, *value);
}
} else if(has_1770 && address >= 0xfe80 && address < 0xfe88) {
switch(address) {
case 0xfe80:
if(!is_read(operation)) {
wd1770_.set_control_register(*value);
}
break;
default:
if(is_read(operation)) {
*value = wd1770_.read(address);
} else {
wd1770_.write(address, *value);
}
break;
}
}
else {
Logger::error()
@@ -627,14 +916,6 @@ public:
//
// ROM or RAM access.
//
// if(operation == CPU::MOS6502Esque::BusOperation::ReadOpcode) {
// log |= address == 0xc4c0;
//
// if(log) {
// printf("%04x\n", address);
// }
// }
if(is_read(operation)) {
// TODO: probably don't do this with this condition? See how it compiles. If it's a CMOV somehow, no problem.
if((address >> 14) == 2 && !sideways_read_mask_) {
@@ -645,10 +926,6 @@ public:
} else {
if(memory_write_masks_[address >> 14]) {
memory_[address >> 14][address] = *value;
if(address >= 0x7c00 && *value) {
Logger::info().append("Output character: %c", *value);
}
}
}
@@ -656,6 +933,14 @@ public:
}
private:
// MARK: - Activity::Source.
void set_activity_observer(Activity::Observer *const observer) override {
if(has_1770) {
wd1770_.set_activity_observer(observer);
}
system_via_port_handler_.set_activity_observer(observer);
}
// MARK: - AudioProducer.
Outputs::Speaker::Speaker *get_speaker() override {
return audio_.speaker();
@@ -670,6 +955,12 @@ private:
return crtc_bus_handler_.get_scaled_scan_status();
}
// MARK: - SystemVIAPortHandler::Delegate.
void strobe_lightpen() override {
crtc_.trigger_light_pen();
}
// MARK: - KeyboardMachine.
BBCMicro::KeyboardMapper mapper_;
KeyboardMapper *get_keyboard_mapper() override {
@@ -677,12 +968,63 @@ private:
}
void set_key_state(const uint16_t key, const bool is_pressed) override {
if(key == BBCMicro::KeyboardMapper::KeyBreak) {
m6502_.set_reset_line(is_pressed);
} else {
system_via_port_handler_.set_key(uint8_t(key), is_pressed);
switch(Key(key)) {
case Key::SwitchOffCaps:
// Store current caps lock state for a potential restore; press caps lock
// now if there's a need to exit caps lock mode.
was_caps_ = system_via_port_handler_.caps_lock();
if(was_caps_) {
system_via_port_handler_.set_key(uint8_t(Key::CapsLock), true);
}
break;
case Key::RestoreCaps:
// Press caps lock again if the machine was originally in the caps lock state.
// If so then SwitchOffCaps switched it off.
if(was_caps_) {
system_via_port_handler_.set_key(uint8_t(Key::CapsLock), true);
}
break;
case Key::Break:
m6502_.set_reset_line(is_pressed);
break;
default:
system_via_port_handler_.set_key(uint8_t(key), is_pressed);
break;
}
}
bool was_caps_ = false;
void clear_all_keys() final {
m6502_.set_reset_line(false);
system_via_port_handler_.clear_all_keys();
}
HalfCycles get_typer_delay(const std::string &text) const final {
if(!m6502_.get_is_resetting()) {
return Cycles(0);
}
// Add a longer delay for a command at reset that involves pressing a modifier;
// empirically this seems to be a requirement, in order to avoid a collision with
// the system's built-in modifier-at-startup test (e.g. to perform shift+break).
CharacterMapper test_mapper;
const uint16_t *const sequence = test_mapper.sequence_for_character(text[0]);
return is_modifier(Key(sequence[0])) ? Cycles(1'000'000) : Cycles(750'000);
}
HalfCycles get_typer_frequency() const final {
return Cycles(60'000);
}
void type_string(const std::string &string) final {
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
}
bool can_type(const char c) const final {
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
}
// MARK: - TimedMachine.
void run_for(const Cycles cycles) override {
@@ -700,6 +1042,20 @@ private:
update_irq_line();
}
// MARK: - uPD7002::Delegate.
void did_change_interrupt_status(NEC::uPD7002 &) override {
system_via_.set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(adc_.interrupt());
}
// MARK: - MediaTarget.
bool insert_media(const Analyser::Static::Media &media) override {
if(!media.disks.empty() && has_1770) {
wd1770_.set_disk(media.disks.front(), 0);
}
return !media.disks.empty();
}
// MARK: - Clock phase.
int phase_ = 0;
@@ -755,6 +1111,20 @@ private:
bool crtc_2mhz_ = true;
Motorola::ACIA::ACIA acia_;
NEC::uPD7002 adc_;
// MARK: - WD1770.
Electron::Plus3 wd1770_;
void wd1770_did_change_output(WD::WD1770 &) override {
m6502_.set_nmi_line(wd1770_.get_interrupt_request_line() || wd1770_.get_data_request_line());
}
// MARK: - Joysticks
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
return joysticks_;
}
};
}
@@ -767,5 +1137,9 @@ std::unique_ptr<Machine> Machine::BBCMicro(
) {
using Target = Analyser::Static::Acorn::BBCMicroTarget;
const Target *const acorn_target = dynamic_cast<const Target *>(target);
return std::make_unique<BBCMicro::ConcreteMachine>(*acorn_target, rom_fetcher);
if(acorn_target->has_1770dfs || acorn_target->has_adfs) {
return std::make_unique<BBCMicro::ConcreteMachine<true>>(*acorn_target, rom_fetcher);
} else {
return std::make_unique<BBCMicro::ConcreteMachine<false>>(*acorn_target, rom_fetcher);
}
}

View File

@@ -9,62 +9,244 @@
#pragma once
#include "Machines/KeyboardMachine.hpp"
#include "Machines/Utility/Typer.hpp"
#include <cstdint>
#include <unordered_map>
namespace BBCMicro {
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
static constexpr uint16_t KeyBreak = 0xfffd;
enum class Key: uint16_t {
Escape = 0x70, Q = 0x10, F0 = 0x20, k1 = 0x30,
CapsLock = 0x40, ShiftLock = 0x50, Tab = 0x60, Shift = 0x00,
F1 = 0x71, k3 = 0x11, W = 0x21, k2 = 0x31,
A = 0x41, S = 0x51, Z = 0x61, Control = 0x01,
F2 = 0x72, k4 = 0x12, E = 0x22, D = 0x32,
X = 0x42, C = 0x52, Space = 0x62, Bit7 = 0x02,
F3 = 0x73, k5 = 0x13, T = 0x23, R = 0x33,
F = 0x43, G = 0x53, V = 0x63, Bit6 = 0x03,
F5 = 0x74, F4 = 0x14, k7 = 0x24, k6 = 0x34,
Y = 0x44, H = 0x54, B = 0x64, Bit5 = 0x04,
F6 = 0x75, k8 = 0x15, I = 0x25, U = 0x35,
J = 0x45, N = 0x55, M = 0x65, Bit4 = 0x05,
F8 = 0x76, F7 = 0x16, k9 = 0x26, O = 0x36,
K = 0x46, L = 0x56, Comma = 0x66, Bit3 = 0x06,
F9 = 0x77, Hyphen = 0x17, k0 = 0x27, P = 0x37,
At = 0x47, Semicolon = 0x57, FullStop = 0x67, Bit2 = 0x07,
Backslash = 0x78,
Caret = 0x18,
Underscore = 0x28,
OpenSquareBracket = 0x38,
Colon = 0x48,
CloseSquareBracket = 0x58,
ForwardSlash = 0x68,
Bit1 = 0x08,
Right = 0x79, Left = 0x19, Down = 0x29, Up = 0x39,
Return = 0x49, Delete = 0x59, Copy = 0x69, Bit0 = 0x09,
//
// Break; a key, but not on the keyboard matrix.
//
Break = 0xfe00,
//
// Fictional keys to aid key entry.
//
SwitchOffCaps = 0xfe01,
RestoreCaps = 0xfe02,
//
// Master only keys.
//
Keypad4 = 0x7a, Keypad6 = 0x1a, Keypad8 = 0x2a, KeypadPlus = 0x3a,
KeypadDivide = 0x4a, KeypadHash = 0x5a, Keypad0 = 0x6a,
Keypad5 = 0x7b, Keypad7 = 0x1b, Keypad9 = 0x2b, KeypadMinus = 0x3b,
KeypadDeleted = 0x4b, KeypadMultiply = 0x5b, Keypad1 = 0x6b,
Keypad2 = 0x7c, F11 = 0x1c, PauseBreak = 0x2c, KeypadReturn = 0x3c,
KeypadDot = 0x4c, KeypadComma = 0x5c, Keypad3 = 0x6c,
Alt = 0x02,
LeftShift = 0x03,
LeftControl = 0x04,
LeftAlt = 0x05,
RightShift = 0x06,
RightControl = 0x07,
RightAlt = 0x08,
MouseSelect = 0x09,
MouseMenu = 0x0a,
MouseAdjust = 0x0b,
};
constexpr bool is_modifier(const Key key) {
return key == Key::Shift || key == Key::Control;
}
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
uint16_t mapped_key_for_key(const Inputs::Keyboard::Key key) const override {
const auto found = key_map.find(key);
return found != key_map.end() ? found->second : MachineTypes::MappedKeyboardMachine::KeyNotMapped;
return found != key_map.end() ? uint16_t(found->second) : MachineTypes::MappedKeyboardMachine::KeyNotMapped;
}
private:
using Key = Inputs::Keyboard::Key;
static inline const std::unordered_map<Key, uint16_t> key_map{
{Key::Escape, 0x70},
{Key::F12, KeyBreak},
using CLKKey = Inputs::Keyboard::Key;
static inline const std::unordered_map<CLKKey, Key> key_map{
{CLKKey::Escape, Key::Escape},
{CLKKey::F12, Key::Break},
{Key::F10, 0x20}, {Key::F1, 0x71}, {Key::F2, 0x72}, {Key::F3, 0x73}, {Key::F4, 0x14},
{Key::F5, 0x75}, {Key::F6, 0x75}, {Key::F7, 0x16}, {Key::F8, 0x76}, {Key::F9, 0x77},
// These are all wilfully off-by-one to approximate correct layout.
{CLKKey::F1, Key::F0}, {CLKKey::F2, Key::F1}, {CLKKey::F3, Key::F2}, {CLKKey::F4, Key::F3},
{CLKKey::F5, Key::F4}, {CLKKey::F6, Key::F5}, {CLKKey::F7, Key::F6}, {CLKKey::F8, Key::F7},
{CLKKey::F9, Key::F8}, {CLKKey::F10, Key::F9},
{Key::Backslash, 0x78},
{CLKKey::Backslash, Key::Backslash},
{Key::Left, 0x19}, {Key::Right, 0x79}, {Key::Up, 0x39}, {Key::Down, 0x29},
{CLKKey::Left, Key::Left}, {CLKKey::Right, Key::Right}, {CLKKey::Up, Key::Up}, {CLKKey::Down, Key::Down},
{Key::Q, 0x10}, {Key::W, 0x21}, {Key::E, 0x22}, {Key::R, 0x33}, {Key::T, 0x23},
{Key::Y, 0x44}, {Key::U, 0x35}, {Key::I, 0x25}, {Key::O, 0x36}, {Key::P, 0x37},
{Key::A, 0x41}, {Key::S, 0x51}, {Key::D, 0x32}, {Key::F, 0x43}, {Key::G, 0x53},
{Key::H, 0x54}, {Key::J, 0x45}, {Key::K, 0x46}, {Key::L, 0x56}, {Key::Z, 0x61},
{Key::X, 0x42}, {Key::C, 0x52}, {Key::V, 0x63}, {Key::B, 0x64}, {Key::N, 0x55},
{Key::M, 0x65},
{CLKKey::Q, Key::Q}, {CLKKey::W, Key::W}, {CLKKey::E, Key::E}, {CLKKey::R, Key::R},
{CLKKey::T, Key::T}, {CLKKey::Y, Key::Y}, {CLKKey::U, Key::U}, {CLKKey::I, Key::I},
{CLKKey::O, Key::O}, {CLKKey::P, Key::P}, {CLKKey::A, Key::A}, {CLKKey::S, Key::S},
{CLKKey::D, Key::D}, {CLKKey::F, Key::F}, {CLKKey::G, Key::G}, {CLKKey::H, Key::H},
{CLKKey::J, Key::J}, {CLKKey::K, Key::K}, {CLKKey::L, Key::L}, {CLKKey::Z, Key::Z},
{CLKKey::X, Key::X}, {CLKKey::C, Key::C}, {CLKKey::V, Key::V}, {CLKKey::B, Key::B},
{CLKKey::N, Key::N}, {CLKKey::M, Key::M},
{Key::k0, 0x27}, {Key::k1, 0x30}, {Key::k2, 0x31}, {Key::k3, 0x11}, {Key::k4, 0x12},
{Key::k5, 0x13}, {Key::k6, 0x34}, {Key::k7, 0x24}, {Key::k8, 0x15}, {Key::k9, 0x26},
{CLKKey::k0, Key::k0}, {CLKKey::k1, Key::k1}, {CLKKey::k2, Key::k2}, {CLKKey::k3, Key::k3},
{CLKKey::k4, Key::k4}, {CLKKey::k5, Key::k5}, {CLKKey::k6, Key::k6}, {CLKKey::k7, Key::k7},
{CLKKey::k8, Key::k8}, {CLKKey::k9, Key::k9},
{Key::Comma, 0x66},
{Key::FullStop, 0x67},
{Key::ForwardSlash, 0x68},
{CLKKey::Comma, Key::Comma},
{CLKKey::FullStop, Key::FullStop},
{CLKKey::ForwardSlash, Key::ForwardSlash},
{Key::Hyphen, 0x17},
{Key::Equals, 0x18},
{Key::Quote, 0x69},
{CLKKey::Hyphen, Key::Hyphen},
{CLKKey::Equals, Key::Caret},
{CLKKey::BackTick, Key::Copy},
{Key::OpenSquareBracket, 0x38},
{Key::CloseSquareBracket, 0x58},
{Key::Semicolon, 0x48},
{CLKKey::OpenSquareBracket, Key::OpenSquareBracket},
{CLKKey::CloseSquareBracket, Key::CloseSquareBracket},
{Key::Enter, 0x49},
{Key::Backspace, 0x59},
{CLKKey::Semicolon, Key::Semicolon},
{CLKKey::Quote, Key::Colon},
{Key::LeftShift, 0x00}, {Key::RightShift, 0x00},
{Key::LeftControl, 0x01}, {Key::RightControl, 0x01},
{CLKKey::Enter, Key::Return},
{CLKKey::Backspace, Key::Delete},
{Key::Space, 0x62},
{CLKKey::LeftShift, Key::Shift}, {CLKKey::RightShift, Key::Shift},
{CLKKey::LeftControl, Key::Control}, {CLKKey::RightControl, Key::Control},
{CLKKey::LeftOption, Key::CapsLock}, {CLKKey::RightOption, Key::CapsLock},
{CLKKey::Space, Key::Space},
};
};
struct CharacterMapper: public ::Utility::CharacterMapper {
const uint16_t *sequence_for_character(const char character) const override {
const auto found = sequences.find(character);
return found != sequences.end() ? found->second.data() : nullptr;
}
bool needs_pause_after_reset_all_keys() const override { return false; }
bool needs_pause_after_key(const uint16_t key) const override {
return !is_modifier(Key(key));
}
private:
static constexpr size_t MaxSequenceLength = 4;
using Sequence = std::array<uint16_t, MaxSequenceLength>;
template <size_t n>
requires (n < MaxSequenceLength - 1)
static constexpr Sequence keys(const Key (&keys)[n]){
Sequence sequence;
for(size_t c = 0; c < n; c++) {
sequence[c] = uint16_t(keys[c]);
}
sequence[n] = MachineTypes::MappedKeyboardMachine::KeyEndSequence;
return sequence;
}
static inline const std::unordered_map<char, Sequence> sequences = {
{Utility::Typer::BeginString, keys({Key::SwitchOffCaps})},
{Utility::Typer::EndString, keys({Key::RestoreCaps})},
{'q', keys({Key::Q}) }, {'w', keys({Key::W}) },
{'e', keys({Key::E}) }, {'r', keys({Key::R}) },
{'t', keys({Key::T}) }, {'y', keys({Key::Y}) },
{'u', keys({Key::U}) }, {'i', keys({Key::I}) },
{'o', keys({Key::O}) }, {'p', keys({Key::P}) },
{'a', keys({Key::A}) }, {'s', keys({Key::S}) },
{'d', keys({Key::D}) }, {'f', keys({Key::F}) },
{'g', keys({Key::G}) }, {'h', keys({Key::H}) },
{'j', keys({Key::J}) }, {'k', keys({Key::K}) },
{'l', keys({Key::L}) }, {'z', keys({Key::Z}) },
{'x', keys({Key::X}) }, {'c', keys({Key::C}) },
{'v', keys({Key::V}) }, {'b', keys({Key::B}) },
{'n', keys({Key::N}) }, {'m', keys({Key::M}) },
{'Q', keys({Key::Shift, Key::Q}) }, {'W', keys({Key::Shift, Key::W}) },
{'E', keys({Key::Shift, Key::E}) }, {'R', keys({Key::Shift, Key::R}) },
{'T', keys({Key::Shift, Key::T}) }, {'Y', keys({Key::Shift, Key::Y}) },
{'U', keys({Key::Shift, Key::U}) }, {'I', keys({Key::Shift, Key::I}) },
{'O', keys({Key::Shift, Key::O}) }, {'P', keys({Key::Shift, Key::P}) },
{'A', keys({Key::Shift, Key::A}) }, {'S', keys({Key::Shift, Key::S}) },
{'D', keys({Key::Shift, Key::D}) }, {'F', keys({Key::Shift, Key::F}) },
{'G', keys({Key::Shift, Key::G}) }, {'H', keys({Key::Shift, Key::H}) },
{'J', keys({Key::Shift, Key::J}) }, {'K', keys({Key::Shift, Key::K}) },
{'L', keys({Key::Shift, Key::L}) }, {'Z', keys({Key::Shift, Key::Z}) },
{'X', keys({Key::Shift, Key::X}) }, {'C', keys({Key::Shift, Key::C}) },
{'V', keys({Key::Shift, Key::V}) }, {'B', keys({Key::Shift, Key::B}) },
{'N', keys({Key::Shift, Key::N}) }, {'M', keys({Key::Shift, Key::M}) },
{'0', keys({Key::k0}) }, {'1', keys({Key::k1}) },
{'2', keys({Key::k2}) }, {'3', keys({Key::k3}) },
{'4', keys({Key::k4}) }, {'5', keys({Key::k5}) },
{'6', keys({Key::k6}) }, {'7', keys({Key::k7}) },
{'8', keys({Key::k8}) }, {'9', keys({Key::k9}) },
{'\n', keys({Key::Return}) },
{'\r', keys({Key::Return}) },
{'\b', keys({Key::Delete}) },
{'\t', keys({Key::Tab}) },
{' ', keys({Key::Space}) },
{'!', keys({Key::Shift, Key::k1}) },
{'"', keys({Key::Shift, Key::k2}) },
{'#', keys({Key::Shift, Key::k3}) },
{'$', keys({Key::Shift, Key::k4}) },
{'%', keys({Key::Shift, Key::k5}) },
{'&', keys({Key::Shift, Key::k6}) },
{'\'', keys({Key::Shift, Key::k7}) },
{'(', keys({Key::Shift, Key::k8}) },
{')', keys({Key::Shift, Key::k9}) },
{'-', keys({Key::Hyphen}) },
{'^', keys({Key::Caret}) },
{'\\', keys({Key::Backslash}) },
{'=', keys({Key::Shift, Key::Hyphen}) },
{'~', keys({Key::Shift, Key::Caret}) },
{'|', keys({Key::Shift, Key::Backslash}) },
{'@', keys({Key::At}) },
{'[', keys({Key::OpenSquareBracket}) },
{'_', keys({Key::Underscore}) },
{'{', keys({Key::Shift, Key::OpenSquareBracket}) },
// {'£', keys({BBCKey::Shift, BBCKey::Underscore}) },
{';', keys({Key::Semicolon}) },
{':', keys({Key::Colon}) },
{']', keys({Key::CloseSquareBracket}) },
{'+', keys({Key::Shift, Key::Semicolon}) },
{'*', keys({Key::Shift, Key::Colon}) },
{'}', keys({Key::Shift, Key::CloseSquareBracket}) },
{',', keys({Key::Comma}) },
{'.', keys({Key::FullStop}) },
{'/', keys({Key::ForwardSlash}) },
{'<', keys({Key::Shift, Key::Comma}) },
{'>', keys({Key::Shift, Key::FullStop}) },
{'?', keys({Key::Shift, Key::ForwardSlash}) },
};
};

View File

@@ -778,7 +778,7 @@ private:
bool speaker_is_enabled_ = false;
// MARK: - Caps Lock status and the activity observer.
const std::string caps_led = "CAPS";
static inline const std::string caps_led = "CAPS";
bool caps_led_state_ = false;
Activity::Observer *activity_observer_ = nullptr;
};

View File

@@ -10,7 +10,7 @@
using namespace Electron;
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
uint16_t KeyboardMapper::mapped_key_for_key(const Inputs::Keyboard::Key key) const {
#define BIND(source, dest) case Inputs::Keyboard::Key::source: return Electron::Key::dest
switch(key) {
default: break;
@@ -70,7 +70,7 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
}
const uint16_t *CharacterMapper::sequence_for_character(char character) const {
const uint16_t *CharacterMapper::sequence_for_character(const char character) const {
#define KEYS(...) {__VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
#define SHIFT(...) {KeyShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
#define CTRL(...) {KeyControl, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
@@ -148,6 +148,6 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
return table_lookup_sequence_for_character(key_sequences, character);
}
bool CharacterMapper::needs_pause_after_key(uint16_t key) const {
return key != KeyControl && key != KeyShift && key != KeyFunc;
bool CharacterMapper::needs_pause_after_key(const uint16_t key) const {
return !is_modifier(Key(key));
}

View File

@@ -30,9 +30,8 @@ enum Key: uint16_t {
KeyShift = 0x00d0 | 0x08, KeyControl = 0x00d0 | 0x04, KeyFunc = 0x00d0 | 0x02, KeyEscape = 0x00d0 | 0x01,
// Virtual keys.
KeyF1 = 0xfff0, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
KeyBreak = 0xfffd,
KeyF1 = 0xfe00, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
KeyBreak,
};
constexpr bool is_modifier(const Key key) {

View File

@@ -36,6 +36,7 @@ void Plus3::set_control_register(uint8_t control) {
// bit 1 => enable or disable drive 2
// bit 2 => side select
// bit 3 => single density select
// bit 5 => reset?
uint8_t changes = control ^ last_control_;
last_control_ = control;

View File

@@ -275,7 +275,7 @@ private:
Mouse &mouse_;
Joystick *joystick_ = nullptr;
Activity::Observer *observer_ = nullptr;
inline static const std::string led_name = "Power";
static inline const std::string led_name = "Power";
} cia_a_handler_;
class CIABHandler: public MOS::MOS6526::PortHandler {

View File

@@ -187,6 +187,71 @@ public:
forceinline void perform_bus_cycle(const Motorola::CRTC::BusState &state) {
// TODO: there's a one-tick delay on pixel output; incorporate that.
// For the interrupt timer: notify the leading edge of vertical sync and the
// trailing edge of horizontal sync.
if(previous_state_.vsync != state.vsync) {
interrupt_timer_.set_vsync(state.vsync);
}
if(previous_state_.hsync && !state.hsync) {
interrupt_timer_.signal_hsync();
}
// Now, finally, process the previous state so as to incorporate a one-tick delay.
output(previous_state_);
previous_state_ = state;
}
/// Sets the destination for output.
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
crt_.set_scan_target(scan_target);
}
/// @returns The current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 4.0f;
}
/// Sets the type of display.
void set_display_type(const Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
/// Gets the type of display.
Outputs::Display::DisplayType get_display_type() const {
return crt_.get_display_type();
}
/*!
Sets the next video mode. Per the documentation, mode changes take effect only at the end of line,
not immediately. So next means "as of the end of this line".
*/
void set_next_mode(const int mode) {
next_mode_ = mode;
}
/// Palette management: selects a pen to modify.
void select_pen(const int pen) {
pen_ = pen;
}
/// Palette management: sets the colour of the selected pen.
void set_colour(const uint8_t colour) {
if(pen_ & 16) {
// If border is[/was] currently being output, flush what should have been
// drawn in the old colour.
if(previous_output_mode_ == OutputMode::Border) {
output_border(cycles_);
cycles_ = 0;
}
border_ = mapped_palette_value(colour);
} else {
palette_[pen_] = mapped_palette_value(colour);
patch_mode_table(size_t(pen_));
}
}
private:
void output(const Motorola::CRTC::BusState &state) {
// The gate array waits 2us to react to the CRTC's vsync signal, and then
// caps output at 4us. Since the clock rate is 1Mhz, that's 2 and 4 cycles,
// respectively.
@@ -251,9 +316,9 @@ public:
// ... so form the real access address.
const uint16_t address =
uint16_t(
((state.refresh_address & 0x3ff) << 1) |
((state.row_address & 0x7) << 11) |
((state.refresh_address & 0x3000) << 2)
((state.refresh.get() & 0x3ff) << 1) |
((state.line.get() & 0x7) << 11) |
((state.refresh.get() & 0x3000) << 2)
);
// Fetch two bytes and translate into pixels. Guaranteed: the mode can change only at
@@ -308,71 +373,8 @@ public:
}
build_mode_table();
}
// For the interrupt timer: notify the leading edge of vertical sync and the
// trailing edge of horizontal sync.
if(was_vsync_ != state.vsync) {
interrupt_timer_.set_vsync(state.vsync);
}
if(was_hsync_ && !state.hsync) {
interrupt_timer_.signal_hsync();
}
// Update current state for edge detection next time around.
was_vsync_ = state.vsync;
was_hsync_ = state.hsync;
}
/// Sets the destination for output.
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
crt_.set_scan_target(scan_target);
}
/// @returns The current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 4.0f;
}
/// Sets the type of display.
void set_display_type(const Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
/// Gets the type of display.
Outputs::Display::DisplayType get_display_type() const {
return crt_.get_display_type();
}
/*!
Sets the next video mode. Per the documentation, mode changes take effect only at the end of line,
not immediately. So next means "as of the end of this line".
*/
void set_next_mode(const int mode) {
next_mode_ = mode;
}
/// Palette management: selects a pen to modify.
void select_pen(const int pen) {
pen_ = pen;
}
/// Palette management: sets the colour of the selected pen.
void set_colour(const uint8_t colour) {
if(pen_ & 16) {
// If border is[/was] currently being output, flush what should have been
// drawn in the old colour.
if(previous_output_mode_ == OutputMode::Border) {
output_border(cycles_);
cycles_ = 0;
}
border_ = mapped_palette_value(colour);
} else {
palette_[pen_] = mapped_palette_value(colour);
patch_mode_table(size_t(pen_));
}
}
private:
void output_border(const int length) {
assert(length >= 0);
@@ -543,7 +545,6 @@ private:
} previous_output_mode_ = OutputMode::Sync;
int cycles_ = 0;
bool was_hsync_ = false, was_vsync_ = false;
int cycles_into_hsync_ = 0;
Outputs::CRT::CRT crt_;
@@ -568,6 +569,7 @@ private:
uint8_t border_ = 0;
InterruptTimer &interrupt_timer_;
Motorola::CRTC::BusState previous_state_;
};
using CRTC = Motorola::CRTC::CRTC6845<
CRTCBusHandler,

View File

@@ -36,15 +36,15 @@ enum Key: uint16_t {
Key9 = key(0, 0x10), KeyPlus = key(0, 0x20), KeyGBP = key(0, 0x40), KeyDelete = key(0, 0x80),
// Virtual keys.
KeyUp = 0xfff0,
KeyLeft = 0xfff1,
KeyF2 = 0xfff2,
KeyF4 = 0xfff3,
KeyF6 = 0xfff4,
KeyF8 = 0xfff5,
KeyUp = 0xfe00,
KeyLeft,
KeyF2,
KeyF4,
KeyF6,
KeyF8,
// Physical keys not within the usual matrix.
KeyRestore = 0xfffd,
KeyRestore,
};
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {

View File

@@ -137,6 +137,12 @@ public:
*/
static constexpr uint16_t KeyNotMapped = 0xfffe;
/*!
Indicates that a key is not mapped (for the keyboard mapper) or that a
character cannot be typed (for the character mapper).
*/
static constexpr uint16_t DelaySlot = 0xfffd;
/*!
Allows individual machines to provide the mapping between host keys
as per Inputs::Keyboard and their native scheme.

View File

@@ -31,8 +31,8 @@ enum Key: uint16_t {
KeyEquals = 0x0700 | 0x80, KeyReturn = 0x0700 | 0x20, KeyRightShift = 0x0700 | 0x10,
KeyForwardSlash = 0x0700 | 0x08, Key0 = 0x0700 | 0x04, KeyL = 0x0700 | 0x02, Key8 = 0x0700 | 0x01,
KeyNMI = 0xfffd,
KeyJasminReset = 0xfffc,
KeyNMI = 0xfe00,
KeyJasminReset,
};
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {

View File

@@ -258,8 +258,8 @@ private:
//
// Meanwhile, row address is used as a substitute 14th address line.
const auto base_address =
((state.refresh_address & 0xfff) << 1) +
((state.row_address & 1) << 13);
((state.refresh.get() & 0xfff) << 1) +
((state.line.get() & 1) << 13);
const uint8_t bitmap[] = {
ram[base_address],
ram[base_address + 1],
@@ -295,16 +295,16 @@ private:
}
void serialise_text(const Motorola::CRTC::BusState &state) {
const uint8_t attributes = ram[((state.refresh_address << 1) + 1) & 0x3fff];
const uint8_t glyph = ram[((state.refresh_address << 1) + 0) & 0x3fff];
const uint8_t row = font[(glyph * 8) + state.row_address];
const uint8_t attributes = ram[((state.refresh.get() << 1) + 1) & 0x3fff];
const uint8_t glyph = ram[((state.refresh.get() << 1) + 0) & 0x3fff];
const uint8_t row = font[(glyph * 8) + state.line.get()];
uint8_t colours[2] = { rgb(attributes >> 4), rgbi(attributes) };
// Apply blink or background intensity.
if(control_ & 0x20) {
// Set both colours to black if within a blink; otherwise consider a yellow-to-brown conversion.
if((attributes & 0x80) && (state.field_count & 16)) {
if((attributes & 0x80) && state.field_count.bit<4>()) {
colours[0] = colours[1] = 0;
} else {
colours[0] = yellow_to_brown(colours[0]);

View File

@@ -158,9 +158,9 @@ private:
pixel_pointer[4] = pixel_pointer[5] = pixel_pointer[6] = pixel_pointer[7] =
pixel_pointer[8] = low_intensity;
} else {
const uint8_t attributes = ram[((state.refresh_address << 1) + 1) & 0xfff];
const uint8_t glyph = ram[((state.refresh_address << 1) + 0) & 0xfff];
uint8_t row = font[(glyph * 14) + state.row_address];
const uint8_t attributes = ram[((state.refresh.get() << 1) + 1) & 0xfff];
const uint8_t glyph = ram[((state.refresh.get() << 1) + 0) & 0xfff];
uint8_t row = font[(glyph * 14) + state.line.get()];
const uint8_t intensity = (attributes & 0x08) ? high_intensity : low_intensity;
uint8_t blank = off;
@@ -178,12 +178,12 @@ private:
}
// Apply blink if enabled.
if((control_ & 0x20) && (attributes & 0x80) && (state.field_count & 16)) {
if((control_ & 0x20) && (attributes & 0x80) && state.field_count.bit<4>()) {
row ^= 0xff;
blank = (blank == off) ? intensity : off;
}
if(((attributes & 7) == 1) && state.row_address == 13) {
if(((attributes & 7) == 1) && state.line.get() == 13) {
// Draw as underline.
std::fill(pixel_pointer, pixel_pointer + 9, intensity);
} else {

View File

@@ -1130,7 +1130,7 @@ using namespace PCCompatible;
namespace {
#ifndef NDEBUG
static constexpr bool ForceAT = true;
static constexpr bool ForceAT = false;//true;
#else
static constexpr bool ForceAT = false;
#endif

View File

@@ -429,6 +429,22 @@ const std::vector<Description> &Description::all_roms() {
16_kb,
0x3c14fc70u
},
{
BBCMicroDFS226,
"BBCMicro",
"the Acorn 1770 DFS 2.26 ROM",
"dfs-2.26.rom",
16_kb,
0x5ae33e94u
},
{
BBCMicroADFS130,
"BBCMicro",
"the Acorn ADFS 1.30 ROM",
"adfs-1.30.rom",
16_kb,
0xd3855588u
},
//
// ColecoVision.

View File

@@ -82,6 +82,8 @@ enum Name {
// BBC Micro.
BBCMicroMOS12,
BBCMicroDFS226,
BBCMicroADFS130,
// ColecoVision.
ColecoVisionBIOS,

View File

@@ -10,7 +10,13 @@
using namespace Utility;
Typer::Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, CharacterMapper &character_mapper, Delegate *delegate) :
Typer::Typer(
const std::string &string,
const HalfCycles delay,
const HalfCycles frequency,
CharacterMapper &character_mapper,
Delegate *const delegate
) :
frequency_(frequency),
counter_(-delay),
delegate_(delegate),
@@ -70,7 +76,7 @@ void Typer::append(const std::string &string) {
}
}
const uint16_t *Typer::sequence_for_character(char c) const {
const uint16_t *Typer::sequence_for_character(const char c) const {
const uint16_t *const sequence = character_mapper_.sequence_for_character(c);
if(!sequence || sequence[0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) {
return nullptr;
@@ -96,20 +102,17 @@ uint16_t Typer::try_type_next_character() {
delegate_->clear_all_keys();
if(character_mapper_.needs_pause_after_reset_all_keys() ||
(string_pointer_ > 0 && string_[string_pointer_ - 1] == string_[string_pointer_])) {
return 0xffff; // Arbitrarily. Anything non-zero will do.
return MachineTypes::MappedKeyboardMachine::DelaySlot;
}
++phase_;
}
// If the sequence is over, stop.
if(sequence[phase_ - 2] == MachineTypes::MappedKeyboardMachine::KeyEndSequence) {
return 0;
// Don't forward ::KeyEndSequence.
const auto next = sequence[phase_ - 2];
if(next != MachineTypes::MappedKeyboardMachine::KeyEndSequence) {
delegate_->set_key_state(sequence[phase_ - 2], true);
}
// Otherwise, type the key.
delegate_->set_key_state(sequence[phase_ - 2], true);
return sequence[phase_ - 2];
return next;
}
bool Typer::type_next_character() {
@@ -118,13 +121,14 @@ bool Typer::type_next_character() {
while(true) {
const uint16_t key_pressed = try_type_next_character();
if(!key_pressed) {
if(key_pressed == MachineTypes::MappedKeyboardMachine::KeyEndSequence) {
phase_ = 0;
++string_pointer_;
if(string_pointer_ == string_.size()) return false;
continue;
}
if(key_pressed && character_mapper_.needs_pause_after_key(key_pressed)) {
if(character_mapper_.needs_pause_after_key(key_pressed)) {
break;
}
}

View File

@@ -82,8 +82,8 @@ public:
/// Adds the contents of @c str to the end of the current string.
void append(const std::string &str);
const char BeginString = 0x02; // i.e. ASCII start of text
const char EndString = 0x03; // i.e. ASCII end of text
static constexpr char BeginString = 0x02; // i.e. ASCII start of text.
static constexpr char EndString = 0x03; // i.e. ASCII end of text.
private:
std::string string_;

View File

@@ -23,7 +23,9 @@ template <typename Full, typename Half> union alignas(Full) alignas(Half) Regist
RegisterPair() = default;
Full full;
auto operator <=>(const RegisterPair &rhs) const = default;
auto operator <=>(const RegisterPair &rhs) const {
return full <=> rhs.full;
}
#if TARGET_RT_BIG_ENDIAN
struct {
Half high, low;

106
Numeric/SizedInt.hpp Normal file
View File

@@ -0,0 +1,106 @@
//
// SizedCounter.hpp
// Clock Signal
//
// Created by Thomas Harte on 22/09/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Sizes.hpp"
namespace Numeric {
/*!
Provides a counter that is strictly limited to the requested of bits but attempts otherwise
to act like a standard C++ numeric type.
*/
template <int bits>
struct SizedInt {
using IntT = MinIntForValue<1 << bits>::type;
inline static constexpr IntT Mask = (1 << bits) - 1;
constexpr SizedInt(const IntT start_value) noexcept : counter_(start_value & Mask) {}
SizedInt() = default;
IntT get() const {
return counter_;
}
SizedInt operator +(const SizedInt offset) const { return SizedInt<bits>(counter_ + offset.counter_); }
SizedInt operator -(const SizedInt offset) const { return SizedInt<bits>(counter_ - offset.counter_); }
SizedInt operator &(const SizedInt offset) const { return SizedInt<bits>(counter_ & offset.counter_); }
SizedInt operator |(const SizedInt offset) const { return SizedInt<bits>(counter_ | offset.counter_); }
SizedInt operator ^(const SizedInt offset) const { return SizedInt<bits>(counter_ ^ offset.counter_); }
SizedInt operator >>(const int shift) const { return SizedInt<bits>(counter_ >> shift); }
SizedInt operator <<(const int shift) const { return SizedInt<bits>(counter_ << shift); }
SizedInt &operator &=(const SizedInt offset) {
counter_ &= offset.counter_;
return *this;
}
SizedInt &operator |=(const SizedInt offset) {
counter_ |= offset.counter_;
return *this;
}
SizedInt &operator ^=(const SizedInt offset) {
counter_ ^= offset.counter_;
return *this;
}
SizedInt &operator <<=(const int shift) {
counter_ = (counter_ << shift) & Mask;
return *this;
}
SizedInt &operator >>=(const int shift) {
counter_ >>= shift;
return *this;
}
SizedInt &operator ++(int) {
++(*this);
return *this;
}
SizedInt &operator ++() {
counter_ = (counter_ + 1) & Mask;
return *this;
}
SizedInt &operator +=(const IntT rhs) {
counter_ = (counter_ + rhs) & Mask;
return *this;
}
bool operator!() const {
return !counter_;
}
auto operator <=>(const SizedInt &) const = default;
/// Replaces the bits in the range [begin, end) with those in the low-order bits of @c vlaue.
template <int begin, int end>
void load(const MinIntForValue<1 << (end - begin)>::type value) {
const auto mask = (1 << end) - (1 << begin);
counter_ &= ~mask;
counter_ |= IntT((value << begin) & mask);
}
template <int begin, typename IntT>
void load(const IntT value) {
load<begin, begin + sizeof(IntT)*8>(value);
}
template <int index>
requires (index < bits)
bool bit() const {
return counter_ & (1 << index);
}
private:
IntT counter_{};
};
}

View File

@@ -360,6 +360,9 @@
4B4518A51F75FD1C00926311 /* SSD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4518991F75FD1B00926311 /* SSD.cpp */; };
4B47770B268FBE4D005C2340 /* FAT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B477709268FBE4D005C2340 /* FAT.cpp */; };
4B47770D26900685005C2340 /* EnterpriseDaveTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B47770C26900685005C2340 /* EnterpriseDaveTests.mm */; };
4B47F3B62E7BAB94005D4DEC /* uPD7002.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */; };
4B47F3B72E7BAB94005D4DEC /* uPD7002.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */; };
4B47F3B82E7BAB94005D4DEC /* uPD7002.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */; };
4B47F6C5241C87A100ED06F7 /* Struct.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F6C4241C87A100ED06F7 /* Struct.cpp */; };
4B47F6C6241C87A100ED06F7 /* Struct.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F6C4241C87A100ED06F7 /* Struct.cpp */; };
4B49F0A923346F7A0045E6A6 /* MacintoshOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B49F0A723346F7A0045E6A6 /* MacintoshOptions.xib */; };
@@ -638,6 +641,9 @@
4B882F5B2C2F9C7700D84031 /* Shaker in Resources */ = {isa = PBXBuildFile; fileRef = 4B882F5A2C2F9C7700D84031 /* Shaker */; };
4B882F5C2C32199400D84031 /* MachineForTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */; };
4B882F5D2C3219A400D84031 /* AmstradCPC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */; };
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
4B8855A62E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
4B8855A72E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
4B89449520194CB3007DE474 /* MachineForTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */; };
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944E6201967B4007DE474 /* ConfidenceCounter.cpp */; };
4B894519201967B4007DE474 /* ConfidenceCounter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944E6201967B4007DE474 /* ConfidenceCounter.cpp */; };
@@ -1597,6 +1603,8 @@
4B477709268FBE4D005C2340 /* FAT.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = FAT.cpp; path = Parsers/FAT.cpp; sourceTree = "<group>"; };
4B47770A268FBE4D005C2340 /* FAT.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = FAT.hpp; path = Parsers/FAT.hpp; sourceTree = "<group>"; };
4B47770C26900685005C2340 /* EnterpriseDaveTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EnterpriseDaveTests.mm; sourceTree = "<group>"; };
4B47F3B32E7B9A14005D4DEC /* uPD7002.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = uPD7002.hpp; sourceTree = "<group>"; };
4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = uPD7002.cpp; sourceTree = "<group>"; };
4B47F6C4241C87A100ED06F7 /* Struct.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Struct.cpp; sourceTree = "<group>"; };
4B49F0A823346F7A0045E6A6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/MacintoshOptions.xib"; sourceTree = SOURCE_ROOT; };
4B4A762E1DB1A3FA007AAE2E /* AY38910.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AY38910.cpp; sourceTree = "<group>"; };
@@ -1789,6 +1797,9 @@
4B8805FA1DCFF807003085B1 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Oric.hpp; path = Parsers/Oric.hpp; sourceTree = "<group>"; };
4B882F582C2F9C6900D84031 /* CPCShakerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CPCShakerTests.mm; sourceTree = "<group>"; };
4B882F5A2C2F9C7700D84031 /* Shaker */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Shaker; sourceTree = "<group>"; };
4B88559C2E8185BF00E251DD /* SizedInt.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SizedInt.hpp; sourceTree = "<group>"; };
4B8855A22E84D51B00E251DD /* SAA5050.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SAA5050.hpp; sourceTree = "<group>"; };
4B8855A32E84D51B00E251DD /* SAA5050.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SAA5050.cpp; sourceTree = "<group>"; };
4B89449220194A47007DE474 /* CSStaticAnalyser+TargetVector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "CSStaticAnalyser+TargetVector.h"; path = "StaticAnalyser/CSStaticAnalyser+TargetVector.h"; sourceTree = "<group>"; };
4B8944E4201967B4007DE474 /* ConfidenceSummary.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ConfidenceSummary.hpp; sourceTree = "<group>"; };
4B8944E5201967B4007DE474 /* ConfidenceSource.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ConfidenceSource.hpp; sourceTree = "<group>"; };
@@ -3316,6 +3327,15 @@
path = Formats;
sourceTree = "<group>";
};
4B47F3B42E7B9A14005D4DEC /* uPD7002 */ = {
isa = PBXGroup;
children = (
4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */,
4B47F3B32E7B9A14005D4DEC /* uPD7002.hpp */,
);
path = uPD7002;
sourceTree = "<group>";
};
4B4A762D1DB1A35C007AAE2E /* AY38910 */ = {
isa = PBXGroup;
children = (
@@ -3727,15 +3747,16 @@
children = (
4B43984129674943006B0BFC /* BitReverse.hpp */,
4BD155312716362A00410C6E /* BitSpread.hpp */,
4B0B239F2D658C9400153879 /* BitStream.hpp */,
4281572E2AA0334300E16AA1 /* Carry.hpp */,
4B7BA03E23D55E7900B98D9E /* CRC.hpp */,
4B7BA03F23D55E7900B98D9E /* LFSR.hpp */,
4B66E1A8297719270057ED0F /* NumericCoder.hpp */,
4BB5B995281B1D3E00522DA9 /* RegisterSizes.hpp */,
4B88559C2E8185BF00E251DD /* SizedInt.hpp */,
4BFEA2F12682A90200EBF94C /* Sizes.hpp */,
4BD9713A2BFD7E7100C907AA /* StringSimilarity.hpp */,
4B0329202D0A78DC00C51EB5 /* UpperBound.hpp */,
4B0B239F2D658C9400153879 /* BitStream.hpp */,
);
name = Numeric;
path = ../../Numeric;
@@ -3831,6 +3852,15 @@
path = Data;
sourceTree = "<group>";
};
4B8855A42E84D51B00E251DD /* SAA5050 */ = {
isa = PBXGroup;
children = (
4B8855A22E84D51B00E251DD /* SAA5050.hpp */,
4B8855A32E84D51B00E251DD /* SAA5050.cpp */,
);
path = SAA5050;
sourceTree = "<group>";
};
4B8944E2201967B4007DE474 /* Analyser */ = {
isa = PBXGroup;
children = (
@@ -5073,8 +5103,10 @@
4B4B1A39200198C900A0F866 /* KonamiSCC */,
4BC23A212467600E001A6030 /* OPx */,
4BF0BC6E2973318E00CCA2B5 /* RP5C01 */,
4B8855A42E84D51B00E251DD /* SAA5050 */,
4B0ACBFF237756EC008902D0 /* Serial */,
4BB0A6582044FD3000FB3688 /* SN76489 */,
4B47F3B42E7B9A14005D4DEC /* uPD7002 */,
);
name = Components;
path = ../../Components;
@@ -6064,6 +6096,7 @@
4BB505832B962DDF0031C43C /* Keyboard.cpp in Sources */,
4BF0BC69297108D600CCA2B5 /* MemorySlotHandler.cpp in Sources */,
4B894521201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4B47F3B62E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
4B8318B522D3E548006DB630 /* Macintosh.cpp in Sources */,
4B8DF4FA254E36AE00F3433C /* Video.cpp in Sources */,
4B7BA03123C2B19C00B98D9E /* Jasmin.cpp in Sources */,
@@ -6283,6 +6316,7 @@
4B302185208A550100773308 /* DiskII.cpp in Sources */,
42EB81292B23AAC300429AF4 /* IMD.cpp in Sources */,
4B051CB1267C1CA200CA44E8 /* Keyboard.cpp in Sources */,
4B8855A72E84D51B00E251DD /* SAA5050.cpp in Sources */,
4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */,
4B055A931FAE85B50060FFFF /* BinaryDump.cpp in Sources */,
4B89452D201967B4007DE474 /* Tape.cpp in Sources */,
@@ -6379,6 +6413,7 @@
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
4BE8EB6625C750B50040BC40 /* DAT.cpp in Sources */,
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */,
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */,
4BAE49582032881E004BE78E /* CSZX8081.mm in Sources */,
4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */,
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */,
@@ -6434,6 +6469,7 @@
4B89452C201967B4007DE474 /* Tape.cpp in Sources */,
4B448E811F1C45A00009ABD6 /* TZX.cpp in Sources */,
4BBFE83D21015D9C00BF1C40 /* CSJoystickManager.m in Sources */,
4B47F3B72E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
4BEBFB512002DB30000708CC /* DiskROM.cpp in Sources */,
4B89451C201967B4007DE474 /* Disk.cpp in Sources */,
4B302184208A550100773308 /* DiskII.cpp in Sources */,
@@ -6609,6 +6645,7 @@
4B778F3123A5F0CB0000D260 /* Keyboard.cpp in Sources */,
4B778F4A23A5F1FB0000D260 /* StaticAnalyser.cpp in Sources */,
4B7752AB28217E560073E2C5 /* SZX.cpp in Sources */,
4B8855A62E84D51B00E251DD /* SAA5050.cpp in Sources */,
4BD91D772401C2B8007BDC91 /* PatrikRakTests.swift in Sources */,
4B06AAE22C645F970034D014 /* PCBooter.cpp in Sources */,
4B1082C32C1A87CA00B07C5D /* CSL.cpp in Sources */,
@@ -6792,6 +6829,7 @@
4BDA8235261E8E000021AA19 /* Z80ContentionTests.mm in Sources */,
4B882F592C2F9C6A00D84031 /* CPCShakerTests.mm in Sources */,
4B7752C328217F720073E2C5 /* Z80.cpp in Sources */,
4B47F3B82E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
4B06AACE2C645EEC0034D014 /* PCCompatible.cpp in Sources */,
4B06AB072C6461160034D014 /* StaticAnalyser.cpp in Sources */,
4B778F1A23A5ED320000D260 /* Video.cpp in Sources */,

View File

@@ -67,7 +67,6 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"
disableMainThreadChecker = "YES"
disablePerformanceAntipatternChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -75,7 +74,8 @@
migratedStopOnEveryIssue = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "NO">
allowLocationSimulation = "NO"
disablePerformanceAntipatternChecker = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference

View File

@@ -821,11 +821,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>25.09.14</string>
<string>25.10.03</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>25.09.14</string>
<string>25.10.03</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.entertainment</string>
<key>LSMinimumSystemVersion</key>

View File

@@ -164,14 +164,9 @@
self = [super init];
if(self) {
auto target = std::make_unique<Analyser::Static::Acorn::BBCMicroTarget>();
// TODO.
(void)dfs;
(void)adfs;
(void)sidewaysRAM;
// target->has_dfs = dfs;
// target->has_pres_adfs = adfs;
// target->has_ap6_rom = ap6;
// target->has_sideways_ram = sidewaysRAM;
target->has_1770dfs = dfs;
target->has_adfs = adfs;
target->has_sideways_ram = sidewaysRAM;
_targets.push_back(std::move(target));
}
return self;

View File

@@ -95,12 +95,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
func establishStoredOptions() {
let standardUserDefaults = UserDefaults.standard
// Set up data soure.
// TEMPORARY: remove the Apple IIgs and PC compatible options.
// Neither is yet a fully-working machine.
#if !DEBUG
for hidden in ["appleiigs", "bbcmicro"] {
// Remove options that are not yet fully working, except in debug builds.
for hidden in ["appleiigs"] {
let tabIndex = machineSelector.indexOfTabViewItem(withIdentifier: hidden)
machineSelector.removeTabViewItem(machineSelector.tabViewItem(at: tabIndex))
}

View File

@@ -76,8 +76,10 @@ SOURCES += \
$$SRC/Components/KonamiSCC/*.cpp \
$$SRC/Components/OPx/*.cpp \
$$SRC/Components/RP5C01/*.cpp \
$$SRC/Components/SAA5050/*.cpp \
$$SRC/Components/Serial/*.cpp \
$$SRC/Components/SN76489/*.cpp \
$$SRC/Components/uPD7002/*.cpp \
\
$$SRC/Inputs/*.cpp \
\
@@ -211,8 +213,10 @@ HEADERS += \
$$SRC/Components/OPx/*.hpp \
$$SRC/Components/OPx/Implementation/*.hpp \
$$SRC/Components/RP5C01/*.hpp \
$$SRC/Components/SAA5050/*.hpp \
$$SRC/Components/Serial/*.hpp \
$$SRC/Components/SN76489/*.hpp \
$$SRC/Components/uPD7002/*.hpp \
\
$$SRC/Concurrency/*.hpp \
\

View File

@@ -60,8 +60,10 @@ SOURCES += glob.glob('../../Components/I2C/*.cpp')
SOURCES += glob.glob('../../Components/KonamiSCC/*.cpp')
SOURCES += glob.glob('../../Components/OPx/*.cpp')
SOURCES += glob.glob('../../Components/RP5C01/*.cpp')
SOURCES += glob.glob('../../Components/SAA5050/*.cpp')
SOURCES += glob.glob('../../Components/SN76489/*.cpp')
SOURCES += glob.glob('../../Components/Serial/*.cpp')
SOURCES += glob.glob('../../Components/uPD7002/*.cpp')
SOURCES += glob.glob('../../Configurable/*.cpp')

View File

@@ -194,13 +194,10 @@ void Processor<personality, T, uses_ready_line>::run_for(const Cycles cycles) {
case OperationRSTPickVector: next_address_.full = 0xfffc; continue;
case CycleReadVectorLow: read_mem(pc_.halves.low, next_address_.full); break;
case CycleReadVectorHigh: read_mem(pc_.halves.high, next_address_.full+1); break;
case OperationSetIRQFlags:
case OperationSetInterruptFlags:
flags_.inverse_interrupt = 0;
if(is_65c02(personality)) flags_.decimal = 0;
continue;
case OperationSetNMIRSTFlags:
if(is_65c02(personality)) flags_.decimal = 0;
continue;
case CyclePullPCL: s_++; read_mem(pc_.halves.low, s_ | 0x100); break;
case CyclePullPCH: s_++; read_mem(pc_.halves.high, s_ | 0x100); break;

View File

@@ -77,7 +77,7 @@ using namespace CPU::MOS6502;
ProcessorStorage::ProcessorStorage(Personality personality) {
const InstructionList operations_6502[] = {
/* 0x00 BRK */ Program(CycleIncPCPushPCH, CyclePushPCL, OperationBRKPickVector, OperationSetOperandFromFlagsWithBRKSet, CyclePushOperand, OperationSetIRQFlags, CycleReadVectorLow, CycleReadVectorHigh),
/* 0x00 BRK */ Program(CycleIncPCPushPCH, CyclePushPCL, OperationBRKPickVector, OperationSetOperandFromFlagsWithBRKSet, CyclePushOperand, OperationSetInterruptFlags, CycleReadVectorLow, CycleReadVectorHigh),
/* 0x01 ORA x, ind */ IndexedIndirectRead(OperationORA),
/* 0x02 JAM */ JAM, /* 0x03 ASO x, ind */ IndexedIndirectReadModifyWrite(OperationASO),
/* 0x04 NOP zpg */ ZeroNop(), /* 0x05 ORA zpg */ ZeroRead(OperationORA),
@@ -228,7 +228,7 @@ ProcessorStorage::ProcessorStorage(Personality personality) {
CycleNoWritePush,
OperationRSTPickVector,
CycleNoWritePush,
OperationSetNMIRSTFlags,
OperationSetInterruptFlags,
CycleReadVectorLow,
CycleReadVectorHigh
),
@@ -242,7 +242,7 @@ ProcessorStorage::ProcessorStorage(Personality personality) {
OperationBRKPickVector,
OperationSetOperandFromFlags,
CyclePushOperand,
OperationSetIRQFlags,
OperationSetInterruptFlags,
CycleReadVectorLow,
CycleReadVectorHigh
),
@@ -256,7 +256,7 @@ ProcessorStorage::ProcessorStorage(Personality personality) {
OperationNMIPickVector,
OperationSetOperandFromFlags,
CyclePushOperand,
OperationSetNMIRSTFlags,
OperationSetInterruptFlags,
CycleReadVectorLow,
CycleReadVectorHigh
),

View File

@@ -35,8 +35,7 @@ class ProcessorStorage {
CyclePushY, // pushes Y to the stack
CyclePushOperand, // pushes operand_ to the stack
OperationSetIRQFlags, // 6502: sets I; 65C02: sets I and resets D
OperationSetNMIRSTFlags, // 6502: no-op. 65C02: resets D
OperationSetInterruptFlags, // 6502: sets I; 65C02: sets I and resets D
OperationBRKPickVector, // 65C02: sets next_address_ to the BRK vector location; 6502: as 65C02 if no NMI is pending; otherwise sets next_address_ to the NMI address and resets the internal NMI-pending flag
OperationNMIPickVector, // sets next_address_ to the NMI vector

View File

@@ -17,6 +17,7 @@ It currently contains emulations of the:
* Apple II/II+ and IIe;
* Atari 2600;
* Atari ST;
* BBC Micro;
* ColecoVision;
* Commodore Vic-20;
* Enterprise 64/128;
@@ -133,8 +134,8 @@ This emulator attempts cycle-accurate emulation of all supported machines. In so
<td width=50%><img alt="Amiga James Pond II" src="READMEImages/AmigaJamesPondII.png"></td>
<td width=50%><img alt="Atari 2600 Solaris" src="READMEImages/Atari2600Solaris.png"></td>
</tr><tr>
<td width=50%><img alt="Enterprise HERO" src="READMEImages/EnterpriseHERO.png"></td>
<td width=50%><img alt="Microsoft Flight Simulator" src="READMEImages/PCFlightSimulator.png"></td>
<td width=50%><img alt="BBC Micro Elite" src="READMEImages/BBCElite.png"></td>
</tr><tr>
<td width=50%><img alt="ColecoVision Galaxian" src="READMEImages/ColecoVisionGalaxian.png"></td>
<td width=50%><img alt="SG1000 Chack'n'Pop" src="READMEImages/SGChackNPop.png"></td>

BIN
READMEImages/BBCElite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -3,3 +3,5 @@ ROM files would ordinarily go here; the copyright status of these is uncertain s
Expected files:
os12.rom
dfs-2.26.rom
adfs-1.30.rom

View File

@@ -58,8 +58,10 @@ set(CLK_SOURCES
Components/KonamiSCC/KonamiSCC.cpp
Components/OPx/OPLL.cpp
Components/RP5C01/RP5C01.cpp
Components/SAA5050/SAA5050.cpp
Components/SN76489/SN76489.cpp
Components/Serial/Line.cpp
Components/uPD7002/uPD7002.cpp
Inputs/Keyboard.cpp