mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
475 Commits
Z80Tests
...
2024-05-05
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6724a701a | ||
|
|
d90eedfc8c | ||
|
|
63009d00b4 | ||
|
|
6a2261d217 | ||
|
|
c3ad2154b5 | ||
|
|
3d61861737 | ||
|
|
7545786436 | ||
|
|
a997b6c677 | ||
|
|
72d4f638aa | ||
|
|
b15ff6d442 | ||
|
|
cb70967971 | ||
|
|
42aea2663c | ||
|
|
a882faa7f6 | ||
|
|
5da01e4fd8 | ||
|
|
71c5a1d419 | ||
|
|
03c3da7338 | ||
|
|
47b276ca0b | ||
|
|
c7747ec5a0 | ||
|
|
5a84e98256 | ||
|
|
b66d69b60c | ||
|
|
dfaea5a922 | ||
|
|
f4da417c3a | ||
|
|
fa7fff86eb | ||
|
|
d480f9eae2 | ||
|
|
4f1aef90b8 | ||
|
|
24f4538eb7 | ||
|
|
38d096cad6 | ||
|
|
b82af9c471 | ||
|
|
0bff2089c4 | ||
|
|
36d9c40d7b | ||
|
|
becb6ce2e0 | ||
|
|
56b65780d2 | ||
|
|
265d151879 | ||
|
|
c485097eed | ||
|
|
f86e9fe086 | ||
|
|
c91ce4cfea | ||
|
|
8e64a854fc | ||
|
|
7c9383cd6b | ||
|
|
82d03e3980 | ||
|
|
0775e3ad58 | ||
|
|
ea3eef3817 | ||
|
|
83eac172c9 | ||
|
|
5b13d3e893 | ||
|
|
807835b9fe | ||
|
|
4bf02122ee | ||
|
|
e6c4454059 | ||
|
|
d464ce831a | ||
|
|
018f0e097f | ||
|
|
2acb853021 | ||
|
|
acd477df39 | ||
|
|
da520de9ef | ||
|
|
e680a973b0 | ||
|
|
07984a2f8b | ||
|
|
16532136e9 | ||
|
|
30c2c65b77 | ||
|
|
b63178132d | ||
|
|
6d66c90aad | ||
|
|
c807c75412 | ||
|
|
f6feaddfe6 | ||
|
|
87d1a476a4 | ||
|
|
f7337f2400 | ||
|
|
fac94a5d36 | ||
|
|
140228a936 | ||
|
|
06fd91f002 | ||
|
|
c3d4d0ee38 | ||
|
|
30cca54e6c | ||
|
|
6ac6e48b95 | ||
|
|
779794632e | ||
|
|
88bb16f261 | ||
|
|
c134c7bdc2 | ||
|
|
6c6cda3db5 | ||
|
|
a29f246536 | ||
|
|
d9d675a74f | ||
|
|
d62ea95889 | ||
|
|
e2e951ad0b | ||
|
|
a5a653d684 | ||
|
|
6123350895 | ||
|
|
ec73c00c3b | ||
|
|
dd24f5f4f3 | ||
|
|
82a2e802ea | ||
|
|
3b75eeb70a | ||
|
|
ee4575b70f | ||
|
|
46a4ec1cb1 | ||
|
|
8ab77a3260 | ||
|
|
b875e349c1 | ||
|
|
169298af42 | ||
|
|
5e502df48b | ||
|
|
4f58664f97 | ||
|
|
ffd298218c | ||
|
|
d2b077c573 | ||
|
|
547dc29a60 | ||
|
|
69aeca5c0e | ||
|
|
ed7cd4b277 | ||
|
|
7bf831e1a6 | ||
|
|
0092cb8c36 | ||
|
|
543b1c644a | ||
|
|
cfaea7a90c | ||
|
|
b821645644 | ||
|
|
2865190499 | ||
|
|
3f40e409c5 | ||
|
|
002e235d90 | ||
|
|
7d8a364658 | ||
|
|
41c471ca52 | ||
|
|
dd127f64fe | ||
|
|
b19dcfd6dc | ||
|
|
55369464ad | ||
|
|
609c117267 | ||
|
|
3b62a2fe7a | ||
|
|
7c9715f00c | ||
|
|
7de92a9457 | ||
|
|
0866caf934 | ||
|
|
914b88d115 | ||
|
|
cc122a7a68 | ||
|
|
31979649c6 | ||
|
|
335d13d06d | ||
|
|
ec785f3a8a | ||
|
|
1f83a5425e | ||
|
|
4882d6d0f2 | ||
|
|
722743659b | ||
|
|
6e64a79b52 | ||
|
|
8a6bf84cff | ||
|
|
a0fdd8f4eb | ||
|
|
bda1783624 | ||
|
|
2a14557478 | ||
|
|
0ddbc67b1f | ||
|
|
ffb5149890 | ||
|
|
bb339d619f | ||
|
|
2ed11877e8 | ||
|
|
ea6b83815b | ||
|
|
740b0e35d5 | ||
|
|
2e7c1acb88 | ||
|
|
4fcb85d132 | ||
|
|
f175dcea58 | ||
|
|
c04c708a9d | ||
|
|
f4cf1e5313 | ||
|
|
0e17f382a1 | ||
|
|
f38bca37a2 | ||
|
|
166793ebe6 | ||
|
|
8b04d0e3ef | ||
|
|
a3931674dc | ||
|
|
bd4ef5ec57 | ||
|
|
3ba12630ab | ||
|
|
342d90c929 | ||
|
|
9078fc994b | ||
|
|
f46af4b702 | ||
|
|
b112987556 | ||
|
|
fc880ac130 | ||
|
|
a2d95cb982 | ||
|
|
d2776071e4 | ||
|
|
72a645ec1e | ||
|
|
1154ffd072 | ||
|
|
8ba9708942 | ||
|
|
521fca6089 | ||
|
|
ae684edbe1 | ||
|
|
fa0a9aa611 | ||
|
|
5da9e0486a | ||
|
|
6980fd760c | ||
|
|
3549488b7a | ||
|
|
c1602cc8fe | ||
|
|
189dd176de | ||
|
|
3cf262d1f7 | ||
|
|
ccfc389274 | ||
|
|
0e07f802ac | ||
|
|
55f92e2411 | ||
|
|
c720f3910a | ||
|
|
4215edd11b | ||
|
|
09a61cf1a7 | ||
|
|
5967ad0865 | ||
|
|
eb34c38332 | ||
|
|
5ccb18225a | ||
|
|
58bbce1a15 | ||
|
|
9ea3e547ee | ||
|
|
fb5fdc9f10 | ||
|
|
de7b7818f4 | ||
|
|
c4e6b18294 | ||
|
|
ae6cf69449 | ||
|
|
4a2dcff028 | ||
|
|
aa6acec8fa | ||
|
|
4ac4da908c | ||
|
|
66e62857c4 | ||
|
|
bbc0d8b050 | ||
|
|
0f8bc416d1 | ||
|
|
2ec235170e | ||
|
|
2de1a2dd0d | ||
|
|
1f49c3b113 | ||
|
|
5c645fb3c2 | ||
|
|
40b5227f0b | ||
|
|
847dba8f07 | ||
|
|
417c6c4629 | ||
|
|
2d6a4d490e | ||
|
|
a6ec870872 | ||
|
|
389541be6d | ||
|
|
208f3e24de | ||
|
|
f7e36a1e03 | ||
|
|
1341816791 | ||
|
|
b986add74a | ||
|
|
08673ff021 | ||
|
|
3a2d9c6082 | ||
|
|
43a3959b8f | ||
|
|
85a738acff | ||
|
|
17dbdce230 | ||
|
|
9d084782ae | ||
|
|
106937b679 | ||
|
|
623eda7162 | ||
|
|
2ad6bb099b | ||
|
|
9d858bc61b | ||
|
|
612c9ce49a | ||
|
|
64e025484a | ||
|
|
7b1f800387 | ||
|
|
2712d50e05 | ||
|
|
47e9279bd4 | ||
|
|
635efd0212 | ||
|
|
1c1d2891c7 | ||
|
|
1979d2e5ba | ||
|
|
c25d0e8843 | ||
|
|
3a899ea4be | ||
|
|
9d08282e28 | ||
|
|
18154278d1 | ||
|
|
9063852857 | ||
|
|
bc27e3998d | ||
|
|
19fa0b8945 | ||
|
|
4987bdfec9 | ||
|
|
0e4615564d | ||
|
|
7aeea535a1 | ||
|
|
6b18d775ab | ||
|
|
2ed031e440 | ||
|
|
5d6bb11eb7 | ||
|
|
c6b91559e1 | ||
|
|
6efc41ded7 | ||
|
|
e9c5582fe1 | ||
|
|
8b3c0abe93 | ||
|
|
a5ebac1b29 | ||
|
|
1ccfae885c | ||
|
|
971bfb2ecb | ||
|
|
e7457461ba | ||
|
|
e8c1e8fd3f | ||
|
|
ca779bc841 | ||
|
|
a28c97c0de | ||
|
|
db49146efe | ||
|
|
830d70d3aa | ||
|
|
336292bc49 | ||
|
|
bd62228cc6 | ||
|
|
ccdd340c9a | ||
|
|
0b42f5fb30 | ||
|
|
e9e1db7a05 | ||
|
|
21278d028c | ||
|
|
fbc273f114 | ||
|
|
06a5df029d | ||
|
|
e17700b495 | ||
|
|
655b1e516c | ||
|
|
4e7a63f792 | ||
|
|
a2896b9bd0 | ||
|
|
a4cf86268e | ||
|
|
d059e7c5d8 | ||
|
|
d6f882a8bb | ||
|
|
08f50f3eff | ||
|
|
47f7340dfc | ||
|
|
fdef8901ab | ||
|
|
ca1c3dc005 | ||
|
|
9406a97141 | ||
|
|
a46ec4cffb | ||
|
|
9bb5dc3c2b | ||
|
|
f6ea442606 | ||
|
|
fa8fcd2218 | ||
|
|
2a36d0fcbc | ||
|
|
0e92885ed5 | ||
|
|
f5225b69e5 | ||
|
|
15ee84b2eb | ||
|
|
d380cecdb7 | ||
|
|
ae3cd924e8 | ||
|
|
a0f0f73bde | ||
|
|
7cdceb7b4f | ||
|
|
38b5624639 | ||
|
|
3405b3b287 | ||
|
|
173fc9329a | ||
|
|
691a42d81e | ||
|
|
4059905f85 | ||
|
|
bbb520fd12 | ||
|
|
108a056f1c | ||
|
|
ed92e98ca2 | ||
|
|
0d666f9935 | ||
|
|
fe467be124 | ||
|
|
ba5f142515 | ||
|
|
ed586e80bc | ||
|
|
871c5467d7 | ||
|
|
387791635e | ||
|
|
b7a1363add | ||
|
|
341b705bef | ||
|
|
0b65aa39cd | ||
|
|
1b7c3644f4 | ||
|
|
0cdca12e06 | ||
|
|
61d4c69e45 | ||
|
|
79865e295b | ||
|
|
1f43047de8 | ||
|
|
6f0ad0ab71 | ||
|
|
447734b1e9 | ||
|
|
3e80651a0e | ||
|
|
692a9da2e4 | ||
|
|
e27312c980 | ||
|
|
eae92a0cdb | ||
|
|
95cc34ba23 | ||
|
|
7532b461cd | ||
|
|
230e9c6327 | ||
|
|
11c4d2f09e | ||
|
|
f2db1b4aae | ||
|
|
b42a6e447d | ||
|
|
8a83d71560 | ||
|
|
9fd7d5c10f | ||
|
|
7a5ed6c427 | ||
|
|
4e7963ee81 | ||
|
|
945b7e90da | ||
|
|
99f0233b76 | ||
|
|
62da0dee7f | ||
|
|
1663d3d9d1 | ||
|
|
37499d493a | ||
|
|
c0dd96eb7c | ||
|
|
2abae4c8bf | ||
|
|
c865da67e0 | ||
|
|
e6f77a9b80 | ||
|
|
7b28b3d634 | ||
|
|
42ba6d1281 | ||
|
|
85b7afd530 | ||
|
|
f2f59a4de5 | ||
|
|
5759798ad7 | ||
|
|
ab1dd7f57e | ||
|
|
53a2ea3a57 | ||
|
|
1f1e7236be | ||
|
|
fd2c5b6679 | ||
|
|
0b287c55d5 | ||
|
|
0de8240238 | ||
|
|
1449b2a2a6 | ||
|
|
0f691766ee | ||
|
|
3ce05e9de1 | ||
|
|
98f5d0cdb7 | ||
|
|
93b4008f81 | ||
|
|
904462b881 | ||
|
|
3b320bcdef | ||
|
|
3368bdb99f | ||
|
|
4d400c3cb7 | ||
|
|
474f9da3c2 | ||
|
|
c49b26701f | ||
|
|
9b42d35d56 | ||
|
|
645152a1fd | ||
|
|
487ade56ed | ||
|
|
60d1b36e9a | ||
|
|
5a48c15e46 | ||
|
|
d6bf1808f9 | ||
|
|
b676153d21 | ||
|
|
a3339cf882 | ||
|
|
b4e0b46bac | ||
|
|
09c1b2d7db | ||
|
|
4255283e33 | ||
|
|
16e827bb2c | ||
|
|
def69ce6d5 | ||
|
|
054a799699 | ||
|
|
580f402bb6 | ||
|
|
030dda34f0 | ||
|
|
cd21b39f44 | ||
|
|
481b6d0e69 | ||
|
|
a88d41bf00 | ||
|
|
0ee3b628e8 | ||
|
|
45628ba9df | ||
|
|
c843c395ea | ||
|
|
9bdaf31d04 | ||
|
|
4ac2baeb9d | ||
|
|
c56e82207a | ||
|
|
82abebec6e | ||
|
|
60286c3a15 | ||
|
|
8460fe2118 | ||
|
|
4b5456c9ba | ||
|
|
ddf136556d | ||
|
|
4c45f4468e | ||
|
|
73d2acca12 | ||
|
|
56a5df3783 | ||
|
|
d205e538e1 | ||
|
|
f9cbec668b | ||
|
|
6577f68efc | ||
|
|
e986ae2878 | ||
|
|
b2696450d5 | ||
|
|
2bbaf73aa2 | ||
|
|
0fe2c1406b | ||
|
|
954d920b9e | ||
|
|
57b45076c5 | ||
|
|
d639dc8bcb | ||
|
|
9a74ab6a8e | ||
|
|
4c53414cc3 | ||
|
|
c36288dd6b | ||
|
|
bc5727af14 | ||
|
|
bd0a15c054 | ||
|
|
a758112084 | ||
|
|
3981f3a874 | ||
|
|
e8036127fe | ||
|
|
17abd87791 | ||
|
|
35545451fe | ||
|
|
64bec0cc3d | ||
|
|
fadd3bc6fc | ||
|
|
d9ec11c62e | ||
|
|
093a029b8c | ||
|
|
b4a3b23571 | ||
|
|
be99183f1d | ||
|
|
dda5f41487 | ||
|
|
a09457dab5 | ||
|
|
ac171d166e | ||
|
|
51de1892c0 | ||
|
|
e1fdda928a | ||
|
|
1c8261dc09 | ||
|
|
cb22278c7f | ||
|
|
809bc9d6a8 | ||
|
|
be11f31d5d | ||
|
|
0103761b7b | ||
|
|
3ac5fdafab | ||
|
|
1e877c7563 | ||
|
|
07c11e8268 | ||
|
|
0dcceff410 | ||
|
|
2a684ab302 | ||
|
|
27059233b3 | ||
|
|
7f84d5ac6f | ||
|
|
d43f050922 | ||
|
|
3ba2618547 | ||
|
|
a3e104f8e2 | ||
|
|
1bb82189e9 | ||
|
|
e06a66644c | ||
|
|
6dcc13921f | ||
|
|
fd45745600 | ||
|
|
507c3da927 | ||
|
|
f14e45f93e | ||
|
|
3d2d9ac45e | ||
|
|
1895b4ee5d | ||
|
|
d49c07687c | ||
|
|
3a208460e2 | ||
|
|
472297e411 | ||
|
|
25085cb5af | ||
|
|
9909146c59 | ||
|
|
609d81d75d | ||
|
|
c105acf1c7 | ||
|
|
f3d0827d14 | ||
|
|
a4a983eb81 | ||
|
|
48be7c677e | ||
|
|
147d817977 | ||
|
|
d481f335b8 | ||
|
|
228012cd0c | ||
|
|
f4d8c04f3c | ||
|
|
c6c9be0b08 | ||
|
|
3827929a15 | ||
|
|
ca7e4b3a0e | ||
|
|
fd73c24fc3 | ||
|
|
ce0d53b277 | ||
|
|
7dc3b5ba06 | ||
|
|
17cad73177 | ||
|
|
b28e3eb419 | ||
|
|
d811501421 | ||
|
|
c68dd50fde | ||
|
|
ad8abf2e05 | ||
|
|
01d9455897 | ||
|
|
cbf8849004 | ||
|
|
f3a7d82dc1 | ||
|
|
017674de35 | ||
|
|
4f211334b5 | ||
|
|
31e261f7e5 | ||
|
|
15b5a62e01 | ||
|
|
f5800aa004 | ||
|
|
584aa78695 | ||
|
|
030f49db83 | ||
|
|
cc165b65be | ||
|
|
cb125e6336 | ||
|
|
a3337ea90f | ||
|
|
ae31f85f0c | ||
|
|
5e9f484662 | ||
|
|
f2e29357bf | ||
|
|
8a1a14ba4c | ||
|
|
31cbcb206f | ||
|
|
bf9e913a7b | ||
|
|
d3cea4a10f | ||
|
|
a6df20ff84 | ||
|
|
7122a9ee16 |
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: Build
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
build-mac:
|
||||
name: Mac UI on ${{ matrix.os }}
|
||||
build-mac-xcodebuild:
|
||||
name: Mac UI / xcodebuild / ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [macos-11, macos-12, macos-13, macos-14]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -13,13 +13,49 @@ jobs:
|
||||
- name: Make
|
||||
working-directory: OSBindings/Mac
|
||||
run: xcodebuild CODE_SIGN_IDENTITY=-
|
||||
build-sdl:
|
||||
name: SDL UI on ${{ matrix.os }}
|
||||
build-sdl-cmake:
|
||||
name: SDL UI / cmake / ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
case $RUNNER_OS in
|
||||
Linux)
|
||||
sudo apt-get --allow-releaseinfo-change update
|
||||
sudo apt-get --fix-missing install cmake gcc-10 libsdl2-dev
|
||||
;;
|
||||
macOS)
|
||||
brew install cmake sdl2
|
||||
;;
|
||||
esac
|
||||
- name: Make
|
||||
shell: bash
|
||||
run: |
|
||||
case $RUNNER_OS in
|
||||
Linux)
|
||||
jobs=$(nproc --all)
|
||||
;;
|
||||
macOS)
|
||||
jobs=$(sysctl -n hw.activecpu)
|
||||
;;
|
||||
*)
|
||||
jobs=1
|
||||
esac
|
||||
cmake -S. -Bbuild -DCLK_UI=SDL -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -v -j"$jobs"
|
||||
build-sdl-scons:
|
||||
name: SDL UI / scons / ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-14, ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Activity {
|
||||
*/
|
||||
class Observer {
|
||||
public:
|
||||
virtual ~Observer() {}
|
||||
virtual ~Observer() = default;
|
||||
|
||||
/// Provides hints as to the sort of information presented on an LED.
|
||||
enum LEDPresentation: uint8_t {
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class Machine {
|
||||
Atari2600,
|
||||
AtariST,
|
||||
Amiga,
|
||||
Archimedes,
|
||||
ColecoVision,
|
||||
Electron,
|
||||
Enterprise,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "../../../Numeric/CRC.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
using namespace Analyser::Static::Acorn;
|
||||
|
||||
@@ -86,34 +87,56 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::
|
||||
auto catalogue = std::make_unique<Catalogue>();
|
||||
Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk);
|
||||
|
||||
// Grab the second half of the free-space map because it has the boot option in it.
|
||||
const Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.sector(0, 0, 1);
|
||||
if(!free_space_map_second_half) return nullptr;
|
||||
catalogue->has_large_sectors = free_space_map_second_half->samples[0].size() == 1024;
|
||||
|
||||
// Possibility: this is a large-sector disk with an old-style free space map. In which
|
||||
// case the above just read the start of the root directory.
|
||||
uint8_t first_directory_sector = 2;
|
||||
if(catalogue->has_large_sectors && !memcmp(&free_space_map_second_half->samples[0][1], "Hugo", 4)) {
|
||||
free_space_map_second_half = parser.sector(0, 0, 0);
|
||||
if(!free_space_map_second_half) return nullptr;
|
||||
first_directory_sector = 1;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> root_directory;
|
||||
root_directory.reserve(5 * 256);
|
||||
for(uint8_t c = 2; c < 7; c++) {
|
||||
root_directory.reserve(catalogue->has_large_sectors ? 2*1024 : 5*256);
|
||||
|
||||
for(uint8_t c = first_directory_sector; c < first_directory_sector + (catalogue->has_large_sectors ? 2 : 5); c++) {
|
||||
const Storage::Encodings::MFM::Sector *const sector = parser.sector(0, 0, c);
|
||||
if(!sector) return nullptr;
|
||||
root_directory.insert(root_directory.end(), sector->samples[0].begin(), sector->samples[0].end());
|
||||
}
|
||||
|
||||
// Quick sanity checks.
|
||||
if(root_directory[0x4cb]) return nullptr;
|
||||
if(root_directory[1] != 'H' || root_directory[2] != 'u' || root_directory[3] != 'g' || root_directory[4] != 'o') return nullptr;
|
||||
if(root_directory[0x4FB] != 'H' || root_directory[0x4FC] != 'u' || root_directory[0x4FD] != 'g' || root_directory[0x4FE] != 'o') return nullptr;
|
||||
// Check for end of directory marker.
|
||||
if(root_directory[catalogue->has_large_sectors ? 0x7d7 : 0x4cb]) return nullptr;
|
||||
|
||||
switch(free_space_map_second_half->samples[0][0xfd]) {
|
||||
default: catalogue->bootOption = Catalogue::BootOption::None; break;
|
||||
case 1: catalogue->bootOption = Catalogue::BootOption::LoadBOOT; break;
|
||||
case 2: catalogue->bootOption = Catalogue::BootOption::RunBOOT; break;
|
||||
case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT; break;
|
||||
// Check for both directory identifiers.
|
||||
const uint8_t *const start_id = &root_directory[1];
|
||||
const uint8_t *const end_id = &root_directory[root_directory.size() - 5];
|
||||
catalogue->is_hugo = !memcmp(start_id, "Hugo", 4) && !memcmp(end_id, "Hugo", 4);
|
||||
const bool is_nick = !memcmp(start_id, "Nick", 4) && !memcmp(end_id, "Nick", 4);
|
||||
if(!catalogue->is_hugo && !is_nick) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(!catalogue->has_large_sectors) {
|
||||
// TODO: I don't know where the boot option rests with large sectors.
|
||||
switch(free_space_map_second_half->samples[0][0xfd]) {
|
||||
default: catalogue->bootOption = Catalogue::BootOption::None; break;
|
||||
case 1: catalogue->bootOption = Catalogue::BootOption::LoadBOOT; break;
|
||||
case 2: catalogue->bootOption = Catalogue::BootOption::RunBOOT; break;
|
||||
case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the root directory, at least.
|
||||
for(std::size_t file_offset = 0x005; file_offset < 0x4cb; file_offset += 0x1a) {
|
||||
for(std::size_t file_offset = 0x005; file_offset < (catalogue->has_large_sectors ? 0x7d7 : 0x4cb); file_offset += 0x1a) {
|
||||
// Obtain the name, which will be at most ten characters long, and will
|
||||
// be terminated by either a NULL character or a \r.
|
||||
char name[11];
|
||||
char name[11]{};
|
||||
std::size_t c = 0;
|
||||
for(; c < 10; c++) {
|
||||
const char next = root_directory[file_offset + c] & 0x7f;
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Analyser::Static::Acorn {
|
||||
|
||||
/// Describes a DFS- or ADFS-format catalogue(/directory): the list of files available and the catalogue's boot option.
|
||||
struct Catalogue {
|
||||
bool is_hugo = false;
|
||||
bool has_large_sectors = false;
|
||||
std::string name;
|
||||
std::vector<File> files;
|
||||
enum class BootOption {
|
||||
|
||||
@@ -60,12 +60,13 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
}
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
|
||||
auto target = std::make_unique<Target>();
|
||||
auto target8bit = std::make_unique<Target>();
|
||||
auto targetArchimedes = std::make_unique<Analyser::Static::Target>(Machine::Archimedes);
|
||||
|
||||
// strip out inappropriate cartridges
|
||||
target->media.cartridges = AcornCartridgesFrom(media.cartridges);
|
||||
// Copy appropriate cartridges to the 8-bit target.
|
||||
target8bit->media.cartridges = AcornCartridgesFrom(media.cartridges);
|
||||
|
||||
// if there are any tapes, attempt to get data from the first
|
||||
// If there are any tapes, attempt to get data from the first.
|
||||
if(!media.tapes.empty()) {
|
||||
std::shared_ptr<Storage::Tape::Tape> tape = media.tapes.front();
|
||||
std::vector<File> files = GetFiles(tape);
|
||||
@@ -94,30 +95,34 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
|
||||
|
||||
// Inspect first file. If it's protected or doesn't look like BASIC
|
||||
// then the loading command is *RUN. Otherwise it's CHAIN"".
|
||||
target->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n";
|
||||
target8bit->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n";
|
||||
|
||||
target->media.tapes = media.tapes;
|
||||
target8bit->media.tapes = media.tapes;
|
||||
}
|
||||
}
|
||||
|
||||
if(!media.disks.empty()) {
|
||||
// TODO: below requires an [8-bit compatible] 'Hugo' ADFS catalogue, disallowing
|
||||
// [Archimedes-exclusive] 'Nick' catalogues.
|
||||
//
|
||||
// Would be better to form the appropriate target in the latter case.
|
||||
std::shared_ptr<Storage::Disk::Disk> disk = media.disks.front();
|
||||
std::unique_ptr<Catalogue> dfs_catalogue, adfs_catalogue;
|
||||
dfs_catalogue = GetDFSCatalogue(disk);
|
||||
if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk);
|
||||
if(dfs_catalogue || adfs_catalogue) {
|
||||
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.
|
||||
target->media.disks = media.disks;
|
||||
target->has_dfs = bool(dfs_catalogue);
|
||||
target->has_pres_adfs = bool(adfs_catalogue);
|
||||
target8bit->media.disks = media.disks;
|
||||
target8bit->has_dfs = bool(dfs_catalogue);
|
||||
target8bit->has_pres_adfs = bool(adfs_catalogue);
|
||||
|
||||
// Check whether a simple shift+break will do for loading this disk.
|
||||
Catalogue::BootOption bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption;
|
||||
if(bootOption != Catalogue::BootOption::None) {
|
||||
target->should_shift_restart = true;
|
||||
target8bit->should_shift_restart = true;
|
||||
} else {
|
||||
target->loading_command = "*CAT\n";
|
||||
target8bit->loading_command = "*CAT\n";
|
||||
}
|
||||
|
||||
// Check whether adding the AP6 ROM is justified.
|
||||
@@ -133,39 +138,44 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
|
||||
"VERIFY", "ZERO"
|
||||
}) {
|
||||
if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) {
|
||||
target->has_ap6_rom = true;
|
||||
target->has_sideways_ram = true;
|
||||
target8bit->has_ap6_rom = true;
|
||||
target8bit->has_sideways_ram = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(adfs_catalogue) {
|
||||
targetArchimedes->media.disks = media.disks;
|
||||
}
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
target->has_pres_adfs = false; // To override a floppy selection, if one was made.
|
||||
target->has_acorn_adfs = true;
|
||||
target8bit->has_pres_adfs = false; // To override a floppy selection, if one was made.
|
||||
target8bit->has_acorn_adfs = true;
|
||||
|
||||
// Assume some sort of later-era Acorn work is likely to happen;
|
||||
// so ensure *TYPE, etc are present.
|
||||
target->has_ap6_rom = true;
|
||||
target->has_sideways_ram = true;
|
||||
target8bit->has_ap6_rom = true;
|
||||
target8bit->has_sideways_ram = true;
|
||||
|
||||
target->media.mass_storage_devices = media.mass_storage_devices;
|
||||
target8bit->media.mass_storage_devices = media.mass_storage_devices;
|
||||
|
||||
// Check for a boot option.
|
||||
const auto sector = target->media.mass_storage_devices.front()->get_block(1);
|
||||
const auto sector = target8bit->media.mass_storage_devices.front()->get_block(1);
|
||||
if(sector[0xfd]) {
|
||||
target->should_shift_restart = true;
|
||||
target8bit->should_shift_restart = true;
|
||||
} else {
|
||||
target->loading_command = "*CAT\n";
|
||||
target8bit->loading_command = "*CAT\n";
|
||||
}
|
||||
}
|
||||
|
||||
TargetList targets;
|
||||
if(!target->media.empty()) {
|
||||
targets.push_back(std::move(target));
|
||||
if(!target8bit->media.empty()) {
|
||||
targets.push_back(std::move(target8bit));
|
||||
}
|
||||
if(!targetArchimedes->media.empty()) {
|
||||
targets.push_back(std::move(targetArchimedes));
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::
|
||||
auto new_chunk = std::make_unique<File::Chunk>();
|
||||
int shift_register = 0;
|
||||
|
||||
// TODO: move this into the parser
|
||||
#define shift() shift_register = (shift_register >> 1) | (parser.get_next_bit(tape) << 9)
|
||||
// TODO: move this into the parser
|
||||
const auto shift = [&] {
|
||||
shift_register = (shift_register >> 1) | (parser.get_next_bit(tape) << 9);
|
||||
};
|
||||
|
||||
// find next area of high tone
|
||||
while(!tape->is_at_end() && (shift_register != 0x3ff)) {
|
||||
@@ -32,8 +34,6 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::
|
||||
shift();
|
||||
}
|
||||
|
||||
#undef shift
|
||||
|
||||
parser.reset_crc();
|
||||
parser.reset_error_flag();
|
||||
|
||||
|
||||
@@ -34,12 +34,14 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
|
||||
Model model = Model::IIe;
|
||||
DiskController disk_controller = DiskController::None;
|
||||
SCSIController scsi_controller = SCSIController::None;
|
||||
bool has_mockingboard = true;
|
||||
|
||||
Target() : Analyser::Static::Target(Machine::AppleII) {
|
||||
if(needs_declare()) {
|
||||
DeclareField(model);
|
||||
DeclareField(disk_controller);
|
||||
DeclareField(scsi_controller);
|
||||
DeclareField(has_mockingboard);
|
||||
|
||||
AnnounceEnum(Model);
|
||||
AnnounceEnum(DiskController);
|
||||
@@ -48,4 +50,8 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
|
||||
}
|
||||
};
|
||||
|
||||
constexpr bool is_iie(Target::Model model) {
|
||||
return model == Target::Model::IIe || model == Target::Model::EnhancedIIe;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,12 +38,15 @@ struct Media {
|
||||
}
|
||||
|
||||
Media &operator +=(const Media &rhs) {
|
||||
#define append(name) name.insert(name.end(), rhs.name.begin(), rhs.name.end());
|
||||
append(disks);
|
||||
append(tapes);
|
||||
append(cartridges);
|
||||
append(mass_storage_devices);
|
||||
#undef append
|
||||
const auto append = [&](auto &destination, auto &source) {
|
||||
destination.insert(destination.end(), source.begin(), source.end());
|
||||
};
|
||||
|
||||
append(disks, rhs.disks);
|
||||
append(tapes, rhs.tapes);
|
||||
append(cartridges, rhs.cartridges);
|
||||
append(mass_storage_devices, rhs.mass_storage_devices);
|
||||
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
@@ -54,7 +57,7 @@ struct Media {
|
||||
*/
|
||||
struct Target {
|
||||
Target(Machine machine) : machine(machine) {}
|
||||
virtual ~Target() {}
|
||||
virtual ~Target() = default;
|
||||
|
||||
// This field is entirely optional.
|
||||
std::unique_ptr<Reflection::Struct> state;
|
||||
|
||||
99
BUILD.md
Normal file
99
BUILD.md
Normal file
@@ -0,0 +1,99 @@
|
||||

|
||||
# Building Clock Signal
|
||||
|
||||
Clock Signal is available as [a macOS native application using
|
||||
Metal](#macos-app) or as [a cross-platform command-line-driven SDL executable
|
||||
using OpenGL](#sdl-app).
|
||||
|
||||
## macOS app
|
||||
|
||||
The macOS native application requires a Metal-capable Mac running macOS 10.13 or
|
||||
later and has no prerequisites beyond the normal system libraries. It can be
|
||||
built [using Xcode](#building-the-macos-app-using-xcode) or on the command line
|
||||
[using `xcodebuild`](#building-the-macos-app-using-xcodebuild).
|
||||
|
||||
Machine ROMs are intended to be built into the application bundle; populate the
|
||||
dummy folders below ROMImages before building.
|
||||
|
||||
The Xcode project is configured to sign the application using the developer's
|
||||
certificate, but if you are not the developer then you will get a "No signing
|
||||
certificate" error. To avoid this, you'll specify that you want to sign the
|
||||
application to run locally.
|
||||
|
||||
### Building the macOS app using Xcode
|
||||
|
||||
Open the Clock Signal Xcode project in OSBindings/Mac.
|
||||
|
||||
To avoid signing errors, edit the project, select the Signing & Capabilities
|
||||
tab, and change the Signing Certificate drop-down menu from "Development" to
|
||||
"Sign to Run Locally".
|
||||
|
||||
To avoid crashes when running Clock Signal via Xcode on older Macs due to
|
||||
"unrecognized selector sent to instance" errors, edit the scheme, and in the Run
|
||||
section, scroll down to the Metal heading and uncheck the "API Validation"
|
||||
checkbox.
|
||||
|
||||
To build, choose "Build" from Xcode's Product menu or press
|
||||
<kbd>Command</kbd> + <kbd>B</kbd>.
|
||||
|
||||
To build and run, choose "Run" from the Product menu or press
|
||||
<kbd>Command</kbd> + <kbd>R</kbd>.
|
||||
|
||||
To see the folder where the Clock Signal application was built, choose "Show
|
||||
Build Folder in Finder" from the Product menu. Look in the "Products" folder for
|
||||
a folder named after the configuration (e.g. "Debug" or "Release").
|
||||
|
||||
### Building the macOS app using `xcodebuild`
|
||||
|
||||
To build, change to the OSBindings/Mac directory in the Terminal, then run
|
||||
`xcodebuild`, specifying `-` as the code sign identity to sign the application
|
||||
to run locally to avoid signing errors:
|
||||
|
||||
cd OSBindings/Mac
|
||||
xcodebuild CODE_SIGN_IDENTITY=-
|
||||
|
||||
`xcodebuild` will create a "build" folder in this directory which is where you
|
||||
can find the Clock Signal application after it's compiled, in a directory named
|
||||
after the configuration (e.g. "Debug" or "Release").
|
||||
|
||||
## SDL app
|
||||
|
||||
The SDL app can be built on Linux, BSD, macOS, and other Unix-like operating
|
||||
systems. Prerequisites are SDL 2, ZLib and OpenGL (or Mesa). OpenGL 3.2 or
|
||||
better is required at runtime. It can be built [using
|
||||
SCons](#building-the-sdl-app-using-scons).
|
||||
|
||||
### Building the SDL app using SCons
|
||||
|
||||
To build, change to the OSBindings/SDL directory and run `scons`. You can add a
|
||||
`-j` flag to build in parallel. For example, if you have 8 processor cores:
|
||||
|
||||
cd OSBindings/SDL
|
||||
scons -j8
|
||||
|
||||
The `clksignal` executable will be created in this directory. You can run it
|
||||
from here or install it by copying it where you want it, for example:
|
||||
|
||||
cp clksignal /usr/local/bin
|
||||
|
||||
To start an emulator with a particular disk image `file`, if you've installed
|
||||
`clksignal` to a directory in your `PATH`, run:
|
||||
|
||||
clksignal file
|
||||
|
||||
Or if you're running it from the current directory:
|
||||
|
||||
./clksignal file
|
||||
|
||||
Other options are availble. Run `clksignal` or `./clksignal` with no arguments
|
||||
to learn more.
|
||||
|
||||
Setting up `clksignal` as the associated program for supported file types in
|
||||
your favoured filesystem browser is recommended; it has no file navigation
|
||||
abilities of its own.
|
||||
|
||||
Some emulated systems require the provision of original machine ROMs. These are
|
||||
not included and may be located in either /usr/local/share/CLK/ or
|
||||
/usr/share/CLK/. You will be prompted for them if they are found to be missing.
|
||||
The structure should mirror that under OSBindings in the source archive; see the
|
||||
readme.txt in each folder to determine the proper files and names ahead of time.
|
||||
30
BUILD.txt
30
BUILD.txt
@@ -1,30 +0,0 @@
|
||||
Linux, BSD
|
||||
==========
|
||||
|
||||
Prerequisites are SDL 2, ZLib and OpenGL (or Mesa), and SCons for the provided build script. OpenGL 3.2 or better is required at runtime.
|
||||
|
||||
Build:
|
||||
|
||||
cd OSBindings/SDL
|
||||
scons
|
||||
|
||||
Optionally:
|
||||
|
||||
cp clksignal /usr/bin
|
||||
|
||||
To launch:
|
||||
|
||||
clksignal file
|
||||
|
||||
Setting up clksignal as the associated program for supported file types in your favoured filesystem browser is recommended; it has no file navigation abilities of its own.
|
||||
|
||||
Some emulated systems require the provision of original machine ROMs. These are not included and may be located in either /usr/local/share/CLK/ or /usr/share/CLK/. You will be prompted for them if they are found to be missing. The structure should mirror that under OSBindings in the source archive; see the readme.txt in each folder to determine the proper files and names ahead of time.
|
||||
|
||||
macOS
|
||||
=====
|
||||
|
||||
There are no prerequisites beyond the normal system libraries; the macOS build is a native Cocoa application.
|
||||
|
||||
Build: open the Xcode project in OSBindings/Mac and press command+b.
|
||||
|
||||
Machine ROMs are intended to be built into the application bundle; populate the dummy folders below ROMImages before building.
|
||||
70
CMakeLists.txt
Normal file
70
CMakeLists.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
cmake_minimum_required(VERSION 3.19 FATAL_ERROR)
|
||||
|
||||
project(CLK
|
||||
LANGUAGES CXX
|
||||
VERSION 24.01.22
|
||||
)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(CLK_UIS "SDL")
|
||||
#list(PREPEND CLK_UIS "QT")
|
||||
#if(APPLE)
|
||||
# list(PREPEND CLK_UIS "MAC")
|
||||
# set(CLK_DEFAULT_UI "MAC")
|
||||
#else()
|
||||
set(CLK_DEFAULT_UI "SDL")
|
||||
#endif()
|
||||
|
||||
set(CLK_UI ${CLK_DEFAULT_UI} CACHE STRING "User interface")
|
||||
set_property(CACHE CLK_UI PROPERTY STRINGS ${CLK_UIS})
|
||||
|
||||
if(NOT CLK_UI IN_LIST CLK_UIS)
|
||||
list(JOIN CLK_UIS ", " CLK_UIS_PRETTY)
|
||||
message(FATAL_ERROR "Invalid value for 'CLK_UI'; must be one of ${CLK_UIS_PRETTY}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Configuring for ${CLK_UI} UI")
|
||||
|
||||
list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
include("CLK_SOURCES")
|
||||
|
||||
add_executable(clksignal ${CLK_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(clksignal PRIVATE /W4)
|
||||
else()
|
||||
# TODO: Add -Wpedandic.
|
||||
target_compile_options(clksignal PRIVATE -Wall -Wextra)
|
||||
endif()
|
||||
|
||||
find_package(ZLIB REQUIRED)
|
||||
target_link_libraries(clksignal PRIVATE ZLIB::ZLIB)
|
||||
|
||||
if(CLK_UI STREQUAL "MAC")
|
||||
enable_language(OBJC OBJCXX SWIFT)
|
||||
# TODO: Build the Mac version.
|
||||
else()
|
||||
find_package(OpenGL REQUIRED)
|
||||
target_link_libraries(clksignal PRIVATE OpenGL::GL)
|
||||
if(APPLE)
|
||||
target_compile_definitions(clksignal PRIVATE "GL_SILENCE_DEPRECATION" "IGNORE_APPLE")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(CLK_UI STREQUAL "QT")
|
||||
# TODO: Build the Qt version.
|
||||
elseif(APPLE)
|
||||
set(BLA_VENDOR Apple)
|
||||
find_package(BLAS REQUIRED)
|
||||
target_link_libraries(clksignal PRIVATE BLAS::BLAS)
|
||||
endif()
|
||||
|
||||
if(CLK_UI STREQUAL "SDL")
|
||||
find_package(SDL2 REQUIRED CONFIG REQUIRED COMPONENTS SDL2)
|
||||
target_link_libraries(clksignal PRIVATE SDL2::SDL2)
|
||||
endif()
|
||||
|
||||
# TODO: Investigate building on Windows.
|
||||
@@ -73,6 +73,11 @@ template <typename TimeUnit> class DeferredQueue {
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns @c true if no actions are enqueued; @c false otherwise.
|
||||
bool empty() const {
|
||||
return pending_actions_.empty();
|
||||
}
|
||||
|
||||
private:
|
||||
// The list of deferred actions.
|
||||
struct DeferredAction {
|
||||
|
||||
@@ -30,7 +30,7 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
@param p The type of controller to emulate.
|
||||
*/
|
||||
WD1770(Personality p);
|
||||
virtual ~WD1770() {}
|
||||
virtual ~WD1770() = default;
|
||||
|
||||
/// Sets the value of the double-density input; when @c is_double_density is @c true, reads and writes double-density format data.
|
||||
using Storage::Disk::MFMController::set_is_double_density;
|
||||
|
||||
@@ -68,7 +68,7 @@ class IRQDelegatePortHandler: public PortHandler {
|
||||
/// Sets the delegate that will receive notification of changes in the interrupt line.
|
||||
void set_interrupt_delegate(Delegate *delegate);
|
||||
|
||||
/// Overrides PortHandler::set_interrupt_status, notifying the delegate if one is set.
|
||||
/// Overrides @c PortHandler::set_interrupt_status, notifying the delegate if one is set.
|
||||
void set_interrupt_status(bool new_status);
|
||||
|
||||
private:
|
||||
|
||||
@@ -82,6 +82,8 @@ template <typename PortHandlerT, Personality personality> class MOS6526:
|
||||
void advance_counters(int);
|
||||
|
||||
bool serial_line_did_produce_bit(Serial::Line<true> *line, int bit) final;
|
||||
|
||||
Log::Logger<Log::Source::MOS6526> log;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
namespace MOS::MOS6526 {
|
||||
|
||||
enum Interrupts: uint8_t {
|
||||
@@ -132,13 +129,11 @@ void MOS6526<BusHandlerT, personality>::write(int address, uint8_t value) {
|
||||
|
||||
// Shift control.
|
||||
case 12:
|
||||
printf("TODO: write to shift register\n");
|
||||
log.error().append("TODO: write to shift register");
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("Unhandled 6526 write: %02x to %d\n", value, address);
|
||||
assert(false);
|
||||
break;
|
||||
// Logically unreachable.
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +174,8 @@ uint8_t MOS6526<BusHandlerT, personality>::read(int address) {
|
||||
// Shift register.
|
||||
case 12: return shift_data_;
|
||||
|
||||
default:
|
||||
printf("Unhandled 6526 read from %d\n", address);
|
||||
assert(false);
|
||||
break;
|
||||
// Logically unreachable.
|
||||
default: break;
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <array>
|
||||
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
namespace MOS::MOS6526 {
|
||||
|
||||
@@ -220,7 +221,8 @@ struct MOS6526Storage {
|
||||
control = v;
|
||||
|
||||
if(v&2) {
|
||||
printf("UNIMPLEMENTED: PB strobe\n");
|
||||
Log::Logger<Log::Source::MOS6526> log;
|
||||
log.error().append("UNIMPLEMENTED: PB strobe");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue)
|
||||
void AudioGenerator::set_volume(uint8_t volume) {
|
||||
audio_queue_.enqueue([this, volume]() {
|
||||
volume_ = int16_t(volume) * range_multiplier_;
|
||||
dc_offset_ = volume_ >> 4;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,7 +106,8 @@ static uint8_t noise_pattern[] = {
|
||||
// testing against 0x80. The effect should be the same: loading with 0x7f means an output update every cycle, loading with 0x7e
|
||||
// means every second cycle, etc.
|
||||
|
||||
void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
template <Outputs::Speaker::Action action>
|
||||
void AudioGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
for(unsigned int c = 0; c < number_of_samples; ++c) {
|
||||
update(0, 2, shift);
|
||||
update(1, 1, shift);
|
||||
@@ -114,23 +116,22 @@ void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target)
|
||||
|
||||
// this sums the output of all three sounds channels plus a DC offset for volume;
|
||||
// TODO: what's the real ratio of this stuff?
|
||||
target[c] = int16_t(
|
||||
const int16_t sample =
|
||||
(shift_registers_[0]&1) +
|
||||
(shift_registers_[1]&1) +
|
||||
(shift_registers_[2]&1) +
|
||||
((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1)
|
||||
) * volume_ + (volume_ >> 4);
|
||||
}
|
||||
}
|
||||
((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1);
|
||||
|
||||
void AudioGenerator::skip_samples(std::size_t number_of_samples) {
|
||||
for(unsigned int c = 0; c < number_of_samples; ++c) {
|
||||
update(0, 2, shift);
|
||||
update(1, 1, shift);
|
||||
update(2, 0, shift);
|
||||
update(3, 1, increment);
|
||||
Outputs::Speaker::apply<action>(
|
||||
target[c],
|
||||
Outputs::Speaker::MonoSample(
|
||||
sample * volume_ + dc_offset_
|
||||
));
|
||||
}
|
||||
}
|
||||
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void AudioGenerator::set_sample_volume_range(std::int16_t range) {
|
||||
range_multiplier_ = int16_t(range / 64);
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
|
||||
namespace MOS::MOS6560 {
|
||||
|
||||
// audio state
|
||||
class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
class AudioGenerator: public Outputs::Speaker::BufferSource<AudioGenerator, false> {
|
||||
public:
|
||||
AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
|
||||
@@ -25,10 +25,9 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
void set_control(int channel, uint8_t value);
|
||||
|
||||
// For ::SampleSource.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void skip_samples(std::size_t number_of_samples);
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
@@ -37,6 +36,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
unsigned int shift_registers_[4] = {0, 0, 0, 0};
|
||||
uint8_t control_registers_[4] = {0, 0, 0, 0};
|
||||
int16_t volume_ = 0;
|
||||
int16_t dc_offset_ = 0;
|
||||
int16_t range_multiplier_ = 1;
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Intel::i8272 {
|
||||
|
||||
class BusHandler {
|
||||
public:
|
||||
virtual ~BusHandler() {}
|
||||
virtual ~BusHandler() = default;
|
||||
virtual void set_dma_data_request([[maybe_unused]] bool drq) {}
|
||||
virtual void set_interrupt([[maybe_unused]] bool irq) {}
|
||||
};
|
||||
|
||||
@@ -163,7 +163,7 @@ void Base<personality>::posit_sprite(int sprite_number, int sprite_position, uin
|
||||
}
|
||||
|
||||
const auto sprite_row = uint8_t(screen_row - sprite_position);
|
||||
if(sprite_row < 0 || sprite_row >= sprite_height_) return;
|
||||
if(sprite_row >= sprite_height_) return; // The less-than-zero case is dealt with by the cast to unsigned.
|
||||
|
||||
if(fetch_sprite_buffer_->active_sprite_slot == mode_timing_.maximum_visible_sprites) {
|
||||
status_ |= StatusSpriteOverflow;
|
||||
|
||||
@@ -38,6 +38,7 @@ constexpr size_t memory_size(Personality p) {
|
||||
case TI::TMS::V9938: return 128 * 1024;
|
||||
case TI::TMS::V9958: return 192 * 1024;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr size_t memory_mask(Personality p) {
|
||||
|
||||
@@ -59,7 +59,7 @@ struct YamahaFetcher {
|
||||
type(type),
|
||||
id(id) {}
|
||||
|
||||
constexpr Event() noexcept {}
|
||||
constexpr Event() noexcept = default;
|
||||
};
|
||||
|
||||
// State that tracks fetching position within a line.
|
||||
|
||||
@@ -132,7 +132,7 @@ struct Command {
|
||||
CommandContext &context;
|
||||
ModeDescription &mode_description;
|
||||
Command(CommandContext &context, ModeDescription &mode_description) : context(context), mode_description(mode_description) {}
|
||||
virtual ~Command() {}
|
||||
virtual ~Command() = default;
|
||||
|
||||
/// @returns @c true if all output from this command is done; @c false otherwise.
|
||||
virtual bool done() = 0;
|
||||
|
||||
@@ -10,17 +10,24 @@
|
||||
|
||||
#include "AY38910.hpp"
|
||||
|
||||
//namespace GI {
|
||||
//namespace AY38910 {
|
||||
|
||||
using namespace GI::AY38910;
|
||||
|
||||
// Note on dividers: the real AY has a built-in divider of 8
|
||||
// prior to applying its tone and noise dividers. But the YM fills the
|
||||
// same total periods for noise and tone with double-precision envelopes.
|
||||
// Therefore this class implements a divider of 4 and doubles the tone
|
||||
// and noise periods. The envelope ticks along at the divide-by-four rate,
|
||||
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
|
||||
// matching the YM datasheet's depiction of envelope level 31 as equal to
|
||||
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
|
||||
|
||||
template <bool is_stereo>
|
||||
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {
|
||||
AY38910SampleSource<is_stereo>::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {
|
||||
// Don't use the low bit of the envelope position if this is an AY.
|
||||
envelope_position_mask_ |= personality == Personality::AY38910;
|
||||
|
||||
// Set up envelope lookup tables.
|
||||
// Set up envelope lookup tables; these are based on 32 volume levels as used by the YM2149F.
|
||||
// The AY38910 will just use only even table entries, and therefore only even volumes.
|
||||
for(int c = 0; c < 16; c++) {
|
||||
for(int p = 0; p < 64; p++) {
|
||||
switch(c) {
|
||||
@@ -74,7 +81,8 @@ AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue
|
||||
set_sample_volume_range(0);
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_sample_volume_range(std::int16_t range) {
|
||||
// Set up volume lookup table; the function below is based on a combination of the graph
|
||||
// from the YM's datasheet, showing a clear power curve, and fitting that to observed
|
||||
// values reported elsewhere.
|
||||
@@ -92,7 +100,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
|
||||
a_left_ = uint8_t(a_left * 255.0f);
|
||||
b_left_ = uint8_t(b_left * 255.0f);
|
||||
c_left_ = uint8_t(c_left * 255.0f);
|
||||
@@ -101,78 +110,50 @@ template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_lef
|
||||
c_right_ = uint8_t(c_right * 255.0f);
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
// Note on structure below: the real AY has a built-in divider of 8
|
||||
// prior to applying its tone and noise dividers. But the YM fills the
|
||||
// same total periods for noise and tone with double-precision envelopes.
|
||||
// Therefore this class implements a divider of 4 and doubles the tone
|
||||
// and noise periods. The envelope ticks along at the divide-by-four rate,
|
||||
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
|
||||
// matching the YM datasheet's depiction of envelope level 31 as equal to
|
||||
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
|
||||
|
||||
std::size_t c = 0;
|
||||
while((master_divider_&3) && c < number_of_samples) {
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
master_divider_++;
|
||||
c++;
|
||||
}
|
||||
|
||||
while(c < number_of_samples) {
|
||||
#define step_channel(c) \
|
||||
if(tone_counters_[c]) tone_counters_[c]--;\
|
||||
else {\
|
||||
tone_outputs_[c] ^= 1;\
|
||||
tone_counters_[c] = tone_periods_[c] << 1;\
|
||||
}
|
||||
|
||||
// Update the tone channels.
|
||||
step_channel(0);
|
||||
step_channel(1);
|
||||
step_channel(2);
|
||||
|
||||
#undef step_channel
|
||||
|
||||
// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
|
||||
// it into the official 17 upon divider underflow.
|
||||
if(noise_counter_) noise_counter_--;
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::advance() {
|
||||
const auto step_channel = [&](int c) {
|
||||
if(tone_counters_[c]) --tone_counters_[c];
|
||||
else {
|
||||
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
|
||||
noise_output_ ^= noise_shift_register_&1;
|
||||
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
|
||||
noise_shift_register_ >>= 1;
|
||||
tone_outputs_[c] ^= 1;
|
||||
tone_counters_[c] = tone_periods_[c] << 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
|
||||
// implementing non-repeating patterns by locking them to the final table position.
|
||||
if(envelope_divider_) envelope_divider_--;
|
||||
else {
|
||||
envelope_divider_ = envelope_period_;
|
||||
envelope_position_ ++;
|
||||
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
|
||||
}
|
||||
// Update the tone channels.
|
||||
step_channel(0);
|
||||
step_channel(1);
|
||||
step_channel(2);
|
||||
|
||||
evaluate_output_volume();
|
||||
|
||||
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
c++;
|
||||
master_divider_++;
|
||||
}
|
||||
// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
|
||||
// it into the official 17 upon divider underflow.
|
||||
if(noise_counter_) --noise_counter_;
|
||||
else {
|
||||
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
|
||||
noise_output_ ^= noise_shift_register_&1;
|
||||
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
|
||||
noise_shift_register_ >>= 1;
|
||||
}
|
||||
|
||||
master_divider_ &= 3;
|
||||
// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
|
||||
// implementing non-repeating patterns by locking them to the final table position.
|
||||
if(envelope_divider_) --envelope_divider_;
|
||||
else {
|
||||
envelope_divider_ = envelope_period_ << 1;
|
||||
++envelope_position_;
|
||||
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
|
||||
}
|
||||
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
|
||||
template <bool is_stereo>
|
||||
typename Outputs::Speaker::SampleT<is_stereo>::type AY38910SampleSource<is_stereo>::level() const {
|
||||
return output_volume_;
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::evaluate_output_volume() {
|
||||
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
|
||||
|
||||
// The output level for a channel is:
|
||||
@@ -214,19 +195,18 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
|
||||
|
||||
// Mix additively, weighting if in stereo.
|
||||
if constexpr (is_stereo) {
|
||||
int16_t *const output_volumes = reinterpret_cast<int16_t *>(&output_volume_);
|
||||
output_volumes[0] = int16_t((
|
||||
output_volume_.left = int16_t((
|
||||
volumes_[volumes[0]] * channel_levels[0] * a_left_ +
|
||||
volumes_[volumes[1]] * channel_levels[1] * b_left_ +
|
||||
volumes_[volumes[2]] * channel_levels[2] * c_left_
|
||||
) >> 8);
|
||||
output_volumes[1] = int16_t((
|
||||
output_volume_.right = int16_t((
|
||||
volumes_[volumes[0]] * channel_levels[0] * a_right_ +
|
||||
volumes_[volumes[1]] * channel_levels[1] * b_right_ +
|
||||
volumes_[volumes[2]] * channel_levels[2] * c_right_
|
||||
) >> 8);
|
||||
} else {
|
||||
output_volume_ = uint32_t(
|
||||
output_volume_ = int16_t(
|
||||
volumes_[volumes[0]] * channel_levels[0] +
|
||||
volumes_[volumes[1]] * channel_levels[1] +
|
||||
volumes_[volumes[2]] * channel_levels[2]
|
||||
@@ -234,18 +214,21 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
|
||||
}
|
||||
}
|
||||
|
||||
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() const {
|
||||
template <bool is_stereo>
|
||||
bool AY38910SampleSource<is_stereo>::is_zero_level() const {
|
||||
// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero.
|
||||
return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
|
||||
}
|
||||
|
||||
// MARK: - Register manipulation
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::select_register(uint8_t r) {
|
||||
selected_register_ = r;
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_register_value(uint8_t value) {
|
||||
// There are only 16 registers.
|
||||
if(selected_register_ > 15) return;
|
||||
|
||||
@@ -314,7 +297,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t va
|
||||
if(update_port_a) set_port_output(false);
|
||||
}
|
||||
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
|
||||
template <bool is_stereo>
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_register_value() {
|
||||
// This table ensures that bits that aren't defined within the AY are returned as 0s
|
||||
// when read, conforming to CPC-sourced unit tests.
|
||||
const uint8_t register_masks[16] = {
|
||||
@@ -328,24 +312,28 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
|
||||
|
||||
// MARK: - Port querying
|
||||
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) {
|
||||
template <bool is_stereo>
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_port_output(bool port_b) {
|
||||
return registers_[port_b ? 15 : 14];
|
||||
}
|
||||
|
||||
// MARK: - Bus handling
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_port_handler(PortHandler *handler) {
|
||||
port_handler_ = handler;
|
||||
set_port_output(true);
|
||||
set_port_output(false);
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_data_input(uint8_t r) {
|
||||
data_input_ = r;
|
||||
update_bus();
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_port_output(bool port_b) {
|
||||
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
|
||||
// so that when in the "input" mode, all pins will read normally high". Therefore,
|
||||
// report programmer selection of input mode as creating an output of 0xff.
|
||||
@@ -355,7 +343,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b)
|
||||
}
|
||||
}
|
||||
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
|
||||
template <bool is_stereo>
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_data_output() {
|
||||
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
|
||||
// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
|
||||
// value returned to the CPU when reading it is the and of the output value and any input.
|
||||
@@ -371,7 +360,8 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
|
||||
return data_output_;
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_control_lines(ControlLines control_lines) {
|
||||
switch(int(control_lines)) {
|
||||
default: control_state_ = Inactive; break;
|
||||
|
||||
@@ -386,7 +376,32 @@ template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLine
|
||||
update_bus();
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_reset(bool active) {
|
||||
if(active == reset_) return;
|
||||
reset_ = active;
|
||||
|
||||
// Reset upon the leading edge; TODO: is this right?
|
||||
if(reset_) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::reset() {
|
||||
// TODO: the below is a guess. Look up real answers.
|
||||
|
||||
selected_register_ = 0;
|
||||
std::fill(registers_, registers_ + 16, 0);
|
||||
|
||||
task_queue_.enqueue([&] {
|
||||
std::fill(output_registers_, output_registers_ + 16, 0);
|
||||
evaluate_output_volume();
|
||||
});
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::update_bus() {
|
||||
// Assume no output, unless this turns out to be a read.
|
||||
data_output_ = 0xff;
|
||||
switch(control_state_) {
|
||||
@@ -398,5 +413,10 @@ template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
|
||||
}
|
||||
|
||||
// Ensure both mono and stereo versions of the AY are built.
|
||||
template class GI::AY38910::AY38910<true>;
|
||||
template class GI::AY38910::AY38910<false>;
|
||||
template class GI::AY38910::AY38910SampleSource<true>;
|
||||
template class GI::AY38910::AY38910SampleSource<false>;
|
||||
|
||||
// Perform an explicit instantiation of the BufferSource to hope for
|
||||
// appropriate inlining of advance() and level().
|
||||
template struct GI::AY38910::AY38910<true>;
|
||||
template struct GI::AY38910::AY38910<false>;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
#include "../../Reflection/Struct.hpp"
|
||||
@@ -66,10 +66,11 @@ enum class Personality {
|
||||
|
||||
This AY has an attached mono or stereo mixer.
|
||||
*/
|
||||
template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
template <bool stereo> class AY38910SampleSource {
|
||||
public:
|
||||
/// Creates a new AY38910.
|
||||
AY38910(Personality, Concurrency::AsyncTaskQueue<false> &);
|
||||
AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue<false> &);
|
||||
AY38910SampleSource(const AY38910SampleSource &) = delete;
|
||||
|
||||
/// Sets the value the AY would read from its data lines if it were not outputting.
|
||||
void set_data_input(uint8_t r);
|
||||
@@ -80,6 +81,12 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
/// Sets the current control line state, as a bit field.
|
||||
void set_control_lines(ControlLines control_lines);
|
||||
|
||||
/// Strobes the reset line.
|
||||
void reset();
|
||||
|
||||
/// Sets the current value of the reset line.
|
||||
void set_reset(bool reset);
|
||||
|
||||
/*!
|
||||
Gets the value that would appear on the requested interface port if it were in output mode.
|
||||
@parameter port_b @c true to get the value for Port B, @c false to get the value for Port A.
|
||||
@@ -105,24 +112,24 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
*/
|
||||
void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
|
||||
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
// Sample generation.
|
||||
typename Outputs::Speaker::SampleT<stereo>::type level() const;
|
||||
void advance();
|
||||
bool is_zero_level() const;
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return is_stereo; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
|
||||
bool reset_ = false;
|
||||
|
||||
int selected_register_ = 0;
|
||||
uint8_t registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
uint8_t output_registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
uint8_t registers_[16]{};
|
||||
uint8_t output_registers_[16]{};
|
||||
|
||||
int master_divider_ = 0;
|
||||
|
||||
int tone_periods_[3] = {0, 0, 0};
|
||||
int tone_counters_[3] = {0, 0, 0};
|
||||
int tone_outputs_[3] = {0, 0, 0};
|
||||
int tone_periods_[3]{};
|
||||
int tone_counters_[3]{};
|
||||
int tone_outputs_[3]{};
|
||||
|
||||
int noise_period_ = 0;
|
||||
int noise_counter_ = 0;
|
||||
@@ -150,7 +157,7 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
|
||||
uint8_t data_input_, data_output_;
|
||||
|
||||
uint32_t output_volume_;
|
||||
typename Outputs::Speaker::SampleT<stereo>::type output_volume_;
|
||||
|
||||
void update_bus();
|
||||
PortHandler *port_handler_ = nullptr;
|
||||
@@ -166,6 +173,20 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
friend struct State;
|
||||
};
|
||||
|
||||
/// Defines a default AY to be the sample source with a master divider of 4;
|
||||
/// real AYs have a divide-by-8 step built in but YMs apply only a divide by 4.
|
||||
///
|
||||
/// The implementation of AY38910SampleSource combines those two worlds
|
||||
/// by always applying a divide by four and scaling other things as appropriate.
|
||||
template <bool stereo> struct AY38910:
|
||||
public AY38910SampleSource<stereo>,
|
||||
public Outputs::Speaker::SampleSource<AY38910<stereo>, stereo, 4> {
|
||||
|
||||
// Use the same constructor as `AY38910SampleSource` (along with inheriting
|
||||
// the rest of its interface).
|
||||
using AY38910SampleSource<stereo>::AY38910SampleSource;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides helper code, to provide something closer to the interface exposed by many
|
||||
AY-deploying machines of the era.
|
||||
|
||||
@@ -21,8 +21,6 @@ namespace Apple::Clock {
|
||||
*/
|
||||
class ClockStorage {
|
||||
public:
|
||||
ClockStorage() {}
|
||||
|
||||
/*!
|
||||
Advances the clock by 1 second.
|
||||
|
||||
|
||||
@@ -8,27 +8,23 @@
|
||||
|
||||
#include "AudioToggle.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace Audio;
|
||||
|
||||
Audio::Toggle::Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void Toggle::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
for(std::size_t sample = 0; sample < number_of_samples; ++sample) {
|
||||
target[sample] = level_;
|
||||
}
|
||||
}
|
||||
|
||||
void Toggle::set_sample_volume_range(std::int16_t range) {
|
||||
volume_ = range;
|
||||
level_ = level_active_ ? volume_ : 0;
|
||||
}
|
||||
|
||||
void Toggle::skip_samples(std::size_t) {}
|
||||
|
||||
void Toggle::set_output(bool enabled) {
|
||||
if(is_enabled_ == enabled) return;
|
||||
is_enabled_ = enabled;
|
||||
audio_queue_.enqueue([this, enabled] {
|
||||
level_active_ = enabled;
|
||||
level_ = enabled ? volume_ : 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
namespace Audio {
|
||||
@@ -16,13 +16,18 @@ namespace Audio {
|
||||
/*!
|
||||
Provides a sample source that can programmatically be set to one of two values.
|
||||
*/
|
||||
class Toggle: public Outputs::Speaker::SampleSource {
|
||||
class Toggle: public Outputs::Speaker::BufferSource<Toggle, false> {
|
||||
public:
|
||||
Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
Outputs::Speaker::fill<action>(target, target + number_of_samples, level_);
|
||||
}
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
void skip_samples(const std::size_t number_of_samples);
|
||||
bool is_zero_level() const {
|
||||
return !level_;
|
||||
}
|
||||
|
||||
void set_output(bool enabled);
|
||||
bool get_output() const;
|
||||
@@ -34,6 +39,7 @@ class Toggle: public Outputs::Speaker::SampleSource {
|
||||
|
||||
// Accessed on the audio thread.
|
||||
int16_t level_ = 0, volume_ = 0;
|
||||
bool level_active_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -55,10 +55,6 @@ uint8_t IWM::read(int address) {
|
||||
logger.info().append("Invalid read\n");
|
||||
return 0xff;
|
||||
|
||||
// "Read all 1s".
|
||||
// printf("Reading all 1s\n");
|
||||
// return 0xff;
|
||||
|
||||
case 0:
|
||||
case ENABLE: { /* Read data register. Zeroing afterwards is a guess. */
|
||||
const auto result = data_register_;
|
||||
|
||||
@@ -30,7 +30,6 @@ DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) :
|
||||
// MARK: - Speed Selection
|
||||
|
||||
void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) {
|
||||
// printf("At track %d\n", to_position.as_int());
|
||||
// The 800kb drive automatically selects rotation speed as a function of
|
||||
// head position; the 400kb drive doesn't do so.
|
||||
if(is_800k_) {
|
||||
|
||||
226
Components/I2C/I2C.cpp
Normal file
226
Components/I2C/I2C.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// I2C.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "I2C.hpp"
|
||||
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace I2C;
|
||||
|
||||
namespace {
|
||||
|
||||
Log::Logger<Log::Source::I2C> logger;
|
||||
|
||||
}
|
||||
|
||||
void Bus::set_data(bool pulled) {
|
||||
set_clock_data(clock_, pulled);
|
||||
}
|
||||
bool Bus::data() {
|
||||
bool result = data_;
|
||||
if(peripheral_bits_) {
|
||||
result |= !(peripheral_response_ & 0x80);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Bus::set_clock(bool pulled) {
|
||||
set_clock_data(pulled, data_);
|
||||
}
|
||||
bool Bus::clock() {
|
||||
return clock_;
|
||||
}
|
||||
|
||||
void Bus::set_clock_data(bool clock_pulled, bool data_pulled) {
|
||||
// Proceed only if changes are evidenced.
|
||||
if(clock_pulled == clock_ && data_pulled == data_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool prior_clock = clock_;
|
||||
const bool prior_data = data_;
|
||||
clock_ = clock_pulled;
|
||||
data_ = data_pulled;
|
||||
|
||||
// If currently serialising from a peripheral then shift onwards on
|
||||
// every clock trailing edge.
|
||||
if(peripheral_bits_) {
|
||||
// Trailing edge of clock => bit has been consumed.
|
||||
if(!prior_clock && clock_) {
|
||||
logger.info().append("<< %d", (peripheral_response_ >> 7) & 1);
|
||||
--peripheral_bits_;
|
||||
peripheral_response_ <<= 1;
|
||||
|
||||
if(!peripheral_bits_) {
|
||||
signal(Event::FinishedOutput);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Not currently serialising implies listening.
|
||||
if(!clock_ && prior_data != data_) {
|
||||
// A data transition outside of a clock cycle implies a start or stop.
|
||||
in_bit_ = false;
|
||||
if(data_) {
|
||||
logger.info().append("S");
|
||||
signal(Event::Start);
|
||||
} else {
|
||||
logger.info().append("W");
|
||||
signal(Event::Stop);
|
||||
}
|
||||
} else if(clock_ != prior_clock) {
|
||||
// Bits: wait until the falling edge of the cycle.
|
||||
if(!clock_) {
|
||||
// Rising edge: clock period begins.
|
||||
in_bit_ = true;
|
||||
} else if(in_bit_) {
|
||||
// Falling edge: clock period ends (assuming it began; otherwise this is a preparatory
|
||||
// clock transition only, immediately after a start bit).
|
||||
in_bit_ = false;
|
||||
|
||||
if(data_) {
|
||||
logger.info().append("0");
|
||||
signal(Event::Zero);
|
||||
} else {
|
||||
logger.info().append("1");
|
||||
signal(Event::One);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Bus::signal(Event event) {
|
||||
const auto capture_bit = [&]() {
|
||||
input_ = uint16_t((input_ << 1) | (event == Event::Zero ? 0 : 1));
|
||||
++input_count_;
|
||||
};
|
||||
|
||||
const auto acknowledge = [&]() {
|
||||
// Post an acknowledgement bit.
|
||||
peripheral_response_ = 0;
|
||||
peripheral_bits_ = 1;
|
||||
};
|
||||
|
||||
const auto set_state = [&](State state) {
|
||||
state_ = state;
|
||||
input_count_ = 0;
|
||||
input_ = 0;
|
||||
};
|
||||
|
||||
const auto enqueue = [&](std::optional<uint8_t> next) {
|
||||
if(next) {
|
||||
peripheral_response_ = static_cast<uint16_t>(*next);
|
||||
peripheral_bits_ = 8;
|
||||
set_state(State::AwaitingByteAcknowledge);
|
||||
} else {
|
||||
set_state(State::AwaitingAddress);
|
||||
}
|
||||
};
|
||||
|
||||
const auto stop = [&]() {
|
||||
set_state(State::AwaitingAddress);
|
||||
active_peripheral_ = nullptr;
|
||||
};
|
||||
|
||||
// Allow start and stop conditions at any time.
|
||||
if(event == Event::Start) {
|
||||
set_state(State::CollectingAddress);
|
||||
active_peripheral_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
if(event == Event::Stop) {
|
||||
if(active_peripheral_) {
|
||||
active_peripheral_->stop();
|
||||
}
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
switch(state_) {
|
||||
// While waiting for an address, don't respond to anything other than a
|
||||
// start bit, which is actually dealt with above.
|
||||
case State::AwaitingAddress: break;
|
||||
|
||||
// To collect an address: shift in eight bits, and if there's a device
|
||||
// at that address then acknowledge the address and segue into a read
|
||||
// or write loop.
|
||||
case State::CollectingAddress:
|
||||
capture_bit();
|
||||
if(input_count_ == 8) {
|
||||
auto pair = peripherals_.find(uint8_t(input_) & 0xfe);
|
||||
if(pair != peripherals_.end()) {
|
||||
active_peripheral_ = pair->second;
|
||||
active_peripheral_->start(input_ & 1);
|
||||
|
||||
if(input_&1) {
|
||||
acknowledge();
|
||||
set_state(State::CompletingReadAcknowledge);
|
||||
} else {
|
||||
acknowledge();
|
||||
set_state(State::ReceivingByte);
|
||||
}
|
||||
} else {
|
||||
state_ = State::AwaitingAddress;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Receiving byte: wait until a scheduled acknowledgment has
|
||||
// happened, then collect eight bits, then see whether the
|
||||
// active peripheral will accept them. If so, acknowledge and repeat.
|
||||
// Otherwise fall silent.
|
||||
case State::ReceivingByte:
|
||||
if(event == Event::FinishedOutput) {
|
||||
return;
|
||||
}
|
||||
capture_bit();
|
||||
if(input_count_ == 8) {
|
||||
if(active_peripheral_->write(static_cast<uint8_t>(input_))) {
|
||||
acknowledge();
|
||||
set_state(State::ReceivingByte);
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// The initial state immediately after a peripheral has been started
|
||||
// in read mode and the address-select acknowledgement is still
|
||||
// being serialised.
|
||||
//
|
||||
// Once that is completed, enqueues the first byte from the peripheral.
|
||||
case State::CompletingReadAcknowledge:
|
||||
if(event != Event::FinishedOutput) {
|
||||
break;
|
||||
}
|
||||
enqueue(active_peripheral_->read());
|
||||
break;
|
||||
|
||||
// Repeating state during reading; waits until the previous byte has
|
||||
// been fully serialised, and if the host acknowledged it then posts
|
||||
// the next. If the host didn't acknowledge, stops the connection.
|
||||
case State::AwaitingByteAcknowledge:
|
||||
if(event == Event::FinishedOutput) {
|
||||
break;
|
||||
}
|
||||
if(event != Event::Zero) {
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
|
||||
// Add a new byte if there is one.
|
||||
enqueue(active_peripheral_->read());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Bus::add_peripheral(Peripheral *peripheral, int address) {
|
||||
peripherals_[address] = peripheral;
|
||||
}
|
||||
82
Components/I2C/I2C.hpp
Normal file
82
Components/I2C/I2C.hpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// I2C.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace I2C {
|
||||
|
||||
/// Provides the virtual interface for an I2C peripheral; attaching this to a bus
|
||||
/// provides automatic protocol handling.
|
||||
struct Peripheral {
|
||||
/// Indicates that the host signalled the start condition and addressed this
|
||||
/// peripheral, along with whether it indicated a read or write.
|
||||
virtual void start([[maybe_unused]] bool is_read) {}
|
||||
|
||||
/// Indicates that the host signalled a stop.
|
||||
virtual void stop() {}
|
||||
|
||||
/// Requests the next byte to serialise onto the I2C bus after this peripheral has
|
||||
/// been started in read mode.
|
||||
///
|
||||
/// @returns A byte to serialise or std::nullopt if the peripheral declines to
|
||||
/// continue to communicate.
|
||||
virtual std::optional<uint8_t> read() { return std::nullopt; }
|
||||
|
||||
/// Provides a byte received from the bus after this peripheral has been started
|
||||
/// in write mode.
|
||||
///
|
||||
/// @returns @c true if the write should be acknowledged; @c false otherwise.
|
||||
virtual bool write(uint8_t) { return false; }
|
||||
};
|
||||
|
||||
class Bus {
|
||||
public:
|
||||
void set_data(bool pulled);
|
||||
bool data();
|
||||
|
||||
void set_clock(bool pulled);
|
||||
bool clock();
|
||||
|
||||
void set_clock_data(bool clock_pulled, bool data_pulled);
|
||||
|
||||
void add_peripheral(Peripheral *, int address);
|
||||
|
||||
private:
|
||||
bool data_ = false;
|
||||
bool clock_ = false;
|
||||
bool in_bit_ = false;
|
||||
std::unordered_map<int, Peripheral *> peripherals_;
|
||||
|
||||
uint16_t input_ = 0xffff;
|
||||
int input_count_ = -1;
|
||||
|
||||
Peripheral *active_peripheral_ = nullptr;
|
||||
uint16_t peripheral_response_ = 0xffff;
|
||||
int peripheral_bits_ = 0;
|
||||
|
||||
enum class Event {
|
||||
Zero, One, Start, Stop, FinishedOutput,
|
||||
};
|
||||
void signal(Event);
|
||||
|
||||
enum class State {
|
||||
AwaitingAddress,
|
||||
CollectingAddress,
|
||||
|
||||
CompletingReadAcknowledge,
|
||||
AwaitingByteAcknowledge,
|
||||
|
||||
ReceivingByte,
|
||||
} state_ = State::AwaitingAddress;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -19,15 +19,16 @@ bool SCC::is_zero_level() const {
|
||||
return !(channel_enable_ & 0x1f);
|
||||
}
|
||||
|
||||
void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
template <Outputs::Speaker::Action action>
|
||||
void SCC::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
if(is_zero_level()) {
|
||||
std::memset(target, 0, sizeof(std::int16_t) * number_of_samples);
|
||||
Outputs::Speaker::fill<action>(target, target + number_of_samples, Outputs::Speaker::MonoSample());
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t c = 0;
|
||||
while((master_divider_&7) && c < number_of_samples) {
|
||||
target[c] = transient_output_level_;
|
||||
Outputs::Speaker::apply<action>(target[c], transient_output_level_);
|
||||
master_divider_++;
|
||||
c++;
|
||||
}
|
||||
@@ -44,12 +45,15 @@ void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
evaluate_output_volume();
|
||||
|
||||
for(int ic = 0; ic < 8 && c < number_of_samples; ++ic) {
|
||||
target[c] = transient_output_level_;
|
||||
Outputs::Speaker::apply<action>(target[c], transient_output_level_);
|
||||
c++;
|
||||
master_divider_++;
|
||||
}
|
||||
}
|
||||
}
|
||||
template void SCC::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SCC::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SCC::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void SCC::write(uint16_t address, uint8_t value) {
|
||||
address &= 0xff;
|
||||
@@ -111,5 +115,3 @@ uint8_t SCC::read(uint16_t address) {
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
namespace Konami {
|
||||
@@ -20,7 +20,7 @@ namespace Konami {
|
||||
and five channels of output. The original SCC uses the same wave for channels
|
||||
four and five, the SCC+ supports different waves for the two channels.
|
||||
*/
|
||||
class SCC: public ::Outputs::Speaker::SampleSource {
|
||||
class SCC: public ::Outputs::Speaker::BufferSource<SCC, false> {
|
||||
public:
|
||||
/// Creates a new SCC.
|
||||
SCC(Concurrency::AsyncTaskQueue<false> &task_queue);
|
||||
@@ -29,9 +29,9 @@ class SCC: public ::Outputs::Speaker::SampleSource {
|
||||
bool is_zero_level() const;
|
||||
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
/// Writes to the SCC.
|
||||
void write(uint16_t address, uint8_t value);
|
||||
@@ -45,7 +45,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
|
||||
// State from here on down is accessed ony from the audio thread.
|
||||
int master_divider_ = 0;
|
||||
std::int16_t master_volume_ = 0;
|
||||
int16_t transient_output_level_ = 0;
|
||||
Outputs::Speaker::MonoSample transient_output_level_ = 0;
|
||||
|
||||
struct Channel {
|
||||
int period = 0;
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "../../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
namespace Yamaha::OPL {
|
||||
|
||||
template <typename Child> class OPLBase: public ::Outputs::Speaker::SampleSource {
|
||||
template <typename Child, bool stereo> class OPLBase: public ::Outputs::Speaker::BufferSource<Child, stereo> {
|
||||
public:
|
||||
void write(uint16_t address, uint8_t value) {
|
||||
if(address & 1) {
|
||||
|
||||
@@ -278,7 +278,8 @@ void OPLL::set_sample_volume_range(std::int16_t range) {
|
||||
total_volume_ = range;
|
||||
}
|
||||
|
||||
void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
template <Outputs::Speaker::Action action>
|
||||
void OPLL::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
// Both the OPLL and the OPL2 divide the input clock by 72 to get the base tick frequency;
|
||||
// unlike the OPL2 the OPLL time-divides the output for 'mixing'.
|
||||
|
||||
@@ -289,12 +290,16 @@ void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
while(number_of_samples--) {
|
||||
if(!audio_offset_) update_all_channels();
|
||||
|
||||
*target = output_levels_[audio_offset_ / channel_output_period];
|
||||
Outputs::Speaker::apply<action>(*target, output_levels_[audio_offset_ / channel_output_period]);
|
||||
++target;
|
||||
audio_offset_ = (audio_offset_ + 1) % update_period;
|
||||
}
|
||||
}
|
||||
|
||||
template void OPLL::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void OPLL::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void OPLL::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void OPLL::update_all_channels() {
|
||||
oscillator_.update();
|
||||
|
||||
|
||||
@@ -19,24 +19,26 @@
|
||||
|
||||
namespace Yamaha::OPL {
|
||||
|
||||
class OPLL: public OPLBase<OPLL> {
|
||||
class OPLL: public OPLBase<OPLL, false> {
|
||||
public:
|
||||
/// Creates a new OPLL or VRC7.
|
||||
OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider = 1, bool is_vrc7 = false);
|
||||
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
bool is_zero_level() const { return false; } // TODO.
|
||||
|
||||
// The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in
|
||||
// rhythm mode, but it's correct for melodic output.
|
||||
double get_average_output_peak() const { return 0.5; }
|
||||
double average_output_peak() const { return 0.5; }
|
||||
|
||||
/// Reads from the OPL.
|
||||
uint8_t read(uint16_t address);
|
||||
|
||||
private:
|
||||
friend OPLBase<OPLL>;
|
||||
friend OPLBase<OPLL, false>;
|
||||
void write_register(uint8_t address, uint8_t value);
|
||||
|
||||
int audio_divider_ = 0;
|
||||
|
||||
@@ -99,10 +99,11 @@ void SN76489::evaluate_output_volume() {
|
||||
);
|
||||
}
|
||||
|
||||
void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
template <Outputs::Speaker::Action action>
|
||||
void SN76489::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
std::size_t c = 0;
|
||||
while((master_divider_& (master_divider_period_ - 1)) && c < number_of_samples) {
|
||||
target[c] = output_volume_;
|
||||
Outputs::Speaker::apply<action>(target[c], output_volume_);
|
||||
master_divider_++;
|
||||
c++;
|
||||
}
|
||||
@@ -151,7 +152,7 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
evaluate_output_volume();
|
||||
|
||||
for(int ic = 0; ic < master_divider_period_ && c < number_of_samples; ++ic) {
|
||||
target[c] = output_volume_;
|
||||
Outputs::Speaker::apply<action>(target[c], output_volume_);
|
||||
c++;
|
||||
master_divider_++;
|
||||
}
|
||||
@@ -159,3 +160,6 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
|
||||
master_divider_ &= (master_divider_period_ - 1);
|
||||
}
|
||||
template void SN76489::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SN76489::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SN76489::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
namespace TI {
|
||||
|
||||
class SN76489: public Outputs::Speaker::SampleSource {
|
||||
class SN76489: public Outputs::Speaker::BufferSource<SN76489, false> {
|
||||
public:
|
||||
enum class Personality {
|
||||
SN76489,
|
||||
@@ -28,10 +28,10 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
||||
void write(uint8_t value);
|
||||
|
||||
// As per SampleSource.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
bool is_zero_level() const;
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
int master_divider_ = 0;
|
||||
|
||||
@@ -161,9 +161,7 @@ void Line<include_clock>::update_delegate(bool level) {
|
||||
// Forward as many bits as occur.
|
||||
Storage::Time time_left(cycles_to_forward, int(clock_rate_.as_integral()));
|
||||
const int bit = level ? 1 : 0;
|
||||
int bits = 0;
|
||||
while(time_left >= time_left_in_bit_) {
|
||||
++bits;
|
||||
if(!read_delegate_->serial_line_did_produce_bit(this, bit)) {
|
||||
read_delegate_phase_ = ReadDelegatePhase::WaitingForZero;
|
||||
if(bit) return;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Inputs {
|
||||
*/
|
||||
class Joystick {
|
||||
public:
|
||||
virtual ~Joystick() {}
|
||||
virtual ~Joystick() = default;
|
||||
|
||||
/*!
|
||||
Defines a single input, any individually-measured thing — a fire button or
|
||||
|
||||
@@ -45,7 +45,7 @@ class Keyboard {
|
||||
/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys.
|
||||
Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers);
|
||||
|
||||
virtual ~Keyboard() {}
|
||||
virtual ~Keyboard() = default;
|
||||
|
||||
// Host interface.
|
||||
|
||||
|
||||
133
InstructionSets/ARM/BarrelShifter.hpp
Normal file
133
InstructionSets/ARM/BarrelShifter.hpp
Normal file
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// BarrelShifter.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 28/02/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace InstructionSet::ARM {
|
||||
|
||||
enum class ShiftType {
|
||||
LogicalLeft = 0b00,
|
||||
LogicalRight = 0b01,
|
||||
ArithmeticRight = 0b10,
|
||||
RotateRight = 0b11,
|
||||
};
|
||||
|
||||
template <bool writeable> struct Carry;
|
||||
template <> struct Carry<true> {
|
||||
using type = uint32_t &;
|
||||
};
|
||||
template <> struct Carry<false> {
|
||||
using type = const uint32_t;
|
||||
};
|
||||
|
||||
/// Apply a rotation of @c type to @c source of @c amount; @c carry should be either @c 1 or @c 0
|
||||
/// at call to represent the current value of the carry flag. If @c set_carry is @c true then @c carry will
|
||||
/// receive the new value of the carry flag following the rotation — @c 0 for no carry, @c non-0 for carry.
|
||||
///
|
||||
/// Shift amounts of 0 are given the meaning attributed to them for immediate shift counts.
|
||||
template <ShiftType type, bool set_carry, bool is_immediate_shift>
|
||||
void shift(uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) {
|
||||
switch(type) {
|
||||
case ShiftType::LogicalLeft:
|
||||
if(amount > 32) {
|
||||
if constexpr (set_carry) carry = 0;
|
||||
source = 0;
|
||||
} else if(amount == 32) {
|
||||
if constexpr (set_carry) carry = source & 1;
|
||||
source = 0;
|
||||
} else if(amount > 0) {
|
||||
if constexpr (set_carry) carry = source & (0x8000'0000 >> (amount - 1));
|
||||
source <<= amount;
|
||||
}
|
||||
break;
|
||||
|
||||
case ShiftType::LogicalRight:
|
||||
if(!amount && is_immediate_shift) {
|
||||
// An immediate logical shift right by '0' is treated as a shift by 32;
|
||||
// assemblers are supposed to map LSR #0 to LSL #0.
|
||||
amount = 32;
|
||||
}
|
||||
|
||||
if(amount > 32) {
|
||||
if constexpr (set_carry) carry = 0;
|
||||
source = 0;
|
||||
} else if(amount == 32) {
|
||||
if constexpr (set_carry) carry = source & 0x8000'0000;
|
||||
source = 0;
|
||||
} else if(amount > 0) {
|
||||
if constexpr (set_carry) carry = source & (1 << (amount - 1));
|
||||
source >>= amount;
|
||||
}
|
||||
break;
|
||||
|
||||
case ShiftType::ArithmeticRight: {
|
||||
if(!amount && is_immediate_shift) {
|
||||
// An immediate arithmetic shift of '0' is treated as a shift by 32.
|
||||
amount = 32;
|
||||
}
|
||||
|
||||
const uint32_t sign = (source & 0x8000'0000) ? 0xffff'ffff : 0x0000'0000;
|
||||
|
||||
if(amount >= 32) {
|
||||
if constexpr (set_carry) carry = source & 0x8000'0000;
|
||||
source = sign;
|
||||
} else if(amount > 0) {
|
||||
if constexpr (set_carry) carry = source & (1 << (amount - 1));
|
||||
source = (source >> amount) | (sign << (32 - amount));
|
||||
}
|
||||
} break;
|
||||
|
||||
case ShiftType::RotateRight: {
|
||||
if(!amount) {
|
||||
if(is_immediate_shift) {
|
||||
// Immediate rotate right by 0 is treated as a rotate right by 1 through carry.
|
||||
const uint32_t high = carry << 31;
|
||||
if constexpr (set_carry) carry = source & 1;
|
||||
source = (source >> 1) | high;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// "ROR by 32 has result equal to Rm, carry out equal to bit 31 ...
|
||||
// [for] ROR by n where n is greater than 32 ... repeatedly subtract 32 from n
|
||||
// until the amount is in the range 1 to 32"
|
||||
amount &= 31;
|
||||
if(amount) {
|
||||
if constexpr (set_carry) carry = source & (1 << (amount - 1));
|
||||
source = (source >> amount) | (source << (32 - amount));
|
||||
} else {
|
||||
if constexpr (set_carry) carry = source & 0x8000'0000;
|
||||
}
|
||||
} break;
|
||||
|
||||
// TODO: upon adoption of C++20, use std::rotr.
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Acts as per @c shift above, but applies runtime shift-type selection.
|
||||
template <bool set_carry, bool is_immediate_shift>
|
||||
void shift(ShiftType type, uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) {
|
||||
switch(type) {
|
||||
case ShiftType::LogicalLeft:
|
||||
shift<ShiftType::LogicalLeft, set_carry, is_immediate_shift>(source, amount, carry);
|
||||
break;
|
||||
case ShiftType::LogicalRight:
|
||||
shift<ShiftType::LogicalRight, set_carry, is_immediate_shift>(source, amount, carry);
|
||||
break;
|
||||
case ShiftType::ArithmeticRight:
|
||||
shift<ShiftType::ArithmeticRight, set_carry, is_immediate_shift>(source, amount, carry);
|
||||
break;
|
||||
case ShiftType::RotateRight:
|
||||
shift<ShiftType::RotateRight, set_carry, is_immediate_shift>(source, amount, carry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
270
InstructionSets/ARM/Disassembler.hpp
Normal file
270
InstructionSets/ARM/Disassembler.hpp
Normal file
@@ -0,0 +1,270 @@
|
||||
//
|
||||
// Disassembler.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 19/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "OperationMapper.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
|
||||
namespace InstructionSet::ARM {
|
||||
|
||||
/// Holds a single ARM operand, whether a source/destination or immediate value, potentially including a shift.
|
||||
struct Operand {
|
||||
enum class Type {
|
||||
Immediate, Register, RegisterList, None
|
||||
} type = Type::None;
|
||||
uint32_t value = 0;
|
||||
|
||||
// TODO: encode shifting
|
||||
|
||||
operator std::string() const {
|
||||
switch(type) {
|
||||
default: return "";
|
||||
case Type::Register: return std::string("r") + std::to_string(value);
|
||||
case Type::RegisterList: {
|
||||
std::stringstream stream;
|
||||
stream << '[';
|
||||
bool first = true;
|
||||
for(int c = 0; c < 16; c++) {
|
||||
if(value & (1 << c)) {
|
||||
if(!first) stream << ", ";
|
||||
first = false;
|
||||
|
||||
stream << 'r' << c;
|
||||
}
|
||||
}
|
||||
stream << ']';
|
||||
|
||||
return stream.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Describes a single ARM instruction, suboptimally but such that all relevant detail has been extracted
|
||||
/// by the OperationMapper and is now easy to inspect or to turn into a string.
|
||||
struct Instruction {
|
||||
Condition condition = Condition::AL;
|
||||
enum class Operation {
|
||||
AND, EOR, SUB, RSB,
|
||||
ADD, ADC, SBC, RSC,
|
||||
TST, TEQ, CMP, CMN,
|
||||
ORR, MOV, BIC, MVN,
|
||||
|
||||
LDR, STR,
|
||||
LDM, STM,
|
||||
|
||||
B, BL,
|
||||
|
||||
SWI,
|
||||
|
||||
MRC, MCR,
|
||||
|
||||
Undefined,
|
||||
} operation = Operation::Undefined;
|
||||
|
||||
Operand destination, operand1, operand2;
|
||||
bool sets_flags = false;
|
||||
bool is_byte = false;
|
||||
|
||||
std::string to_string(uint32_t address) const {
|
||||
std::ostringstream result;
|
||||
|
||||
// Treat all nevers as nops.
|
||||
if(condition == Condition::NV) {
|
||||
return "nop";
|
||||
}
|
||||
|
||||
// Print operation.
|
||||
switch(operation) {
|
||||
case Operation::Undefined: return "undefined";
|
||||
case Operation::SWI: return "swi";
|
||||
|
||||
case Operation::B: result << "b"; break;
|
||||
case Operation::BL: result << "bl"; break;
|
||||
|
||||
case Operation::AND: result << "and"; break;
|
||||
case Operation::EOR: result << "eor"; break;
|
||||
case Operation::SUB: result << "sub"; break;
|
||||
case Operation::RSB: result << "rsb"; break;
|
||||
case Operation::ADD: result << "add"; break;
|
||||
case Operation::ADC: result << "adc"; break;
|
||||
case Operation::SBC: result << "sbc"; break;
|
||||
case Operation::RSC: result << "rsc"; break;
|
||||
case Operation::TST: result << "tst"; break;
|
||||
case Operation::TEQ: result << "teq"; break;
|
||||
case Operation::CMP: result << "cmp"; break;
|
||||
case Operation::CMN: result << "cmn"; break;
|
||||
case Operation::ORR: result << "orr"; break;
|
||||
case Operation::MOV: result << "mov"; break;
|
||||
case Operation::BIC: result << "bic"; break;
|
||||
case Operation::MVN: result << "mvn"; break;
|
||||
|
||||
case Operation::LDR: result << "ldr"; break;
|
||||
case Operation::STR: result << "str"; break;
|
||||
case Operation::LDM: result << "ldm"; break;
|
||||
case Operation::STM: result << "stm"; break;
|
||||
|
||||
case Operation::MRC: result << "mrc"; break;
|
||||
case Operation::MCR: result << "mcr"; break;
|
||||
}
|
||||
|
||||
// Append the sets-flags modifier if applicable.
|
||||
if(sets_flags) result << 's';
|
||||
|
||||
// Possibly a condition code.
|
||||
switch(condition) {
|
||||
case Condition::EQ: result << "eq"; break;
|
||||
case Condition::NE: result << "ne"; break;
|
||||
case Condition::CS: result << "cs"; break;
|
||||
case Condition::CC: result << "cc"; break;
|
||||
case Condition::MI: result << "mi"; break;
|
||||
case Condition::PL: result << "pl"; break;
|
||||
case Condition::VS: result << "vs"; break;
|
||||
case Condition::VC: result << "vc"; break;
|
||||
case Condition::HI: result << "hi"; break;
|
||||
case Condition::LS: result << "ls"; break;
|
||||
case Condition::GE: result << "ge"; break;
|
||||
case Condition::LT: result << "lt"; break;
|
||||
case Condition::GT: result << "gt"; break;
|
||||
case Condition::LE: result << "le"; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// If this is a branch, append the target.
|
||||
if(operation == Operation::B || operation == Operation::BL) {
|
||||
result << " 0x" << std::hex << ((address + 8 + operand1.value) & 0x3fffffc);
|
||||
}
|
||||
|
||||
if(
|
||||
operation == Operation::LDR || operation == Operation::STR ||
|
||||
operation == Operation::LDM || operation == Operation::STM
|
||||
) {
|
||||
if(is_byte) result << 'b';
|
||||
result << ' ' << static_cast<std::string>(destination);
|
||||
result << ", [" << static_cast<std::string>(operand1) << "]";
|
||||
// TODO: learn how ARM shifts/etc are normally presented.
|
||||
}
|
||||
|
||||
return result.str();
|
||||
}
|
||||
};
|
||||
|
||||
/// A target for @c dispatch that merely captures a description of the decoded instruction, being
|
||||
/// able to vend it later via @c last().
|
||||
template <Model model>
|
||||
struct Disassembler {
|
||||
Instruction last() {
|
||||
return instruction_;
|
||||
}
|
||||
|
||||
bool should_schedule(Condition condition) {
|
||||
instruction_ = Instruction();
|
||||
instruction_.condition = condition;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <Flags f> void perform(DataProcessing fields) {
|
||||
constexpr DataProcessingFlags flags(f);
|
||||
|
||||
instruction_.operand1.type = Operand::Type::Register;
|
||||
instruction_.operand1.value = fields.operand1();
|
||||
|
||||
instruction_.destination.type = Operand::Type::Register;
|
||||
instruction_.destination.value = fields.destination();
|
||||
|
||||
if(flags.operand2_is_immediate()) {
|
||||
instruction_.operand2.type = Operand::Type::Immediate;
|
||||
// instruction_.operand2.value = fields.immediate(), fields.rotate();
|
||||
// TODO: decode immediate.
|
||||
|
||||
} else {
|
||||
instruction_.operand2.type = Operand::Type::Register;
|
||||
instruction_.operand2.value = fields.operand2();
|
||||
// TODO: capture shift_type(), etc.
|
||||
}
|
||||
|
||||
instruction_.sets_flags = flags.set_condition_codes();
|
||||
|
||||
switch(flags.operation()) {
|
||||
case DataProcessingOperation::AND: instruction_.operation = Instruction::Operation::AND; break;
|
||||
case DataProcessingOperation::EOR: instruction_.operation = Instruction::Operation::EOR; break;
|
||||
case DataProcessingOperation::ORR: instruction_.operation = Instruction::Operation::ORR; break;
|
||||
case DataProcessingOperation::BIC: instruction_.operation = Instruction::Operation::BIC; break;
|
||||
case DataProcessingOperation::MOV: instruction_.operation = Instruction::Operation::MOV; break;
|
||||
case DataProcessingOperation::MVN: instruction_.operation = Instruction::Operation::MVN; break;
|
||||
case DataProcessingOperation::TST: instruction_.operation = Instruction::Operation::TST; break;
|
||||
case DataProcessingOperation::TEQ: instruction_.operation = Instruction::Operation::TEQ; break;
|
||||
case DataProcessingOperation::ADD: instruction_.operation = Instruction::Operation::ADD; break;
|
||||
case DataProcessingOperation::ADC: instruction_.operation = Instruction::Operation::ADC; break;
|
||||
case DataProcessingOperation::CMN: instruction_.operation = Instruction::Operation::CMN; break;
|
||||
case DataProcessingOperation::SUB: instruction_.operation = Instruction::Operation::SUB; break;
|
||||
case DataProcessingOperation::SBC: instruction_.operation = Instruction::Operation::SBC; break;
|
||||
case DataProcessingOperation::CMP: instruction_.operation = Instruction::Operation::CMP; break;
|
||||
case DataProcessingOperation::RSB: instruction_.operation = Instruction::Operation::RSB; break;
|
||||
case DataProcessingOperation::RSC: instruction_.operation = Instruction::Operation::RSC; break;
|
||||
}
|
||||
}
|
||||
|
||||
template <Flags> void perform(Multiply) {}
|
||||
template <Flags f> void perform(SingleDataTransfer fields) {
|
||||
constexpr SingleDataTransferFlags flags(f);
|
||||
instruction_.operation =
|
||||
(flags.operation() == SingleDataTransferFlags::Operation::STR) ?
|
||||
Instruction::Operation::STR : Instruction::Operation::LDR;
|
||||
|
||||
instruction_.destination.type = Operand::Type::Register;
|
||||
instruction_.destination.value = fields.destination();
|
||||
|
||||
instruction_.operand1.type = Operand::Type::Register;
|
||||
instruction_.operand1.value = fields.base();
|
||||
}
|
||||
template <Flags f> void perform(BlockDataTransfer fields) {
|
||||
constexpr BlockDataTransferFlags flags(f);
|
||||
instruction_.operation =
|
||||
(flags.operation() == BlockDataTransferFlags::Operation::STM) ?
|
||||
Instruction::Operation::STM : Instruction::Operation::LDM;
|
||||
|
||||
instruction_.destination.type = Operand::Type::Register;
|
||||
instruction_.destination.value = fields.base();
|
||||
|
||||
instruction_.operand1.type = Operand::Type::RegisterList;
|
||||
instruction_.operand1.value = fields.register_list();
|
||||
}
|
||||
template <Flags f> void perform(Branch fields) {
|
||||
constexpr BranchFlags flags(f);
|
||||
instruction_.operation =
|
||||
(flags.operation() == BranchFlags::Operation::BL) ?
|
||||
Instruction::Operation::BL : Instruction::Operation::B;
|
||||
instruction_.operand1.type = Operand::Type::Immediate;
|
||||
instruction_.operand1.value = fields.offset();
|
||||
}
|
||||
template <Flags f> void perform(CoprocessorRegisterTransfer) {
|
||||
constexpr CoprocessorRegisterTransferFlags flags(f);
|
||||
instruction_.operation =
|
||||
(flags.operation() == CoprocessorRegisterTransferFlags::Operation::MRC) ?
|
||||
Instruction::Operation::MRC : Instruction::Operation::MCR;
|
||||
}
|
||||
template <Flags> void perform(CoprocessorDataOperation) {}
|
||||
template <Flags> void perform(CoprocessorDataTransfer) {}
|
||||
|
||||
void software_interrupt(SoftwareInterrupt) {
|
||||
instruction_.operation = Instruction::Operation::SWI;
|
||||
}
|
||||
void unknown() {
|
||||
instruction_.operation = Instruction::Operation::Undefined;
|
||||
}
|
||||
|
||||
private:
|
||||
Instruction instruction_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
669
InstructionSets/ARM/Executor.hpp
Normal file
669
InstructionSets/ARM/Executor.hpp
Normal file
@@ -0,0 +1,669 @@
|
||||
//
|
||||
// Executor.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BarrelShifter.hpp"
|
||||
#include "OperationMapper.hpp"
|
||||
#include "Registers.hpp"
|
||||
#include "../../Numeric/Carry.hpp"
|
||||
|
||||
namespace InstructionSet::ARM {
|
||||
|
||||
/// Maps from a semantic ARM read of type @c SourceT to either the 8- or 32-bit value observed
|
||||
/// by watching the low 8 bits or all 32 bits of the data bus.
|
||||
template <typename DestinationT, typename SourceT>
|
||||
DestinationT read_bus(SourceT value) {
|
||||
if constexpr (std::is_same_v<DestinationT, SourceT>) {
|
||||
return value;
|
||||
}
|
||||
if constexpr (std::is_same_v<DestinationT, uint8_t>) {
|
||||
return uint8_t(value);
|
||||
} else {
|
||||
return value | (value << 8) | (value << 16) | (value << 24);
|
||||
}
|
||||
}
|
||||
|
||||
struct NullControlFlowHandler {
|
||||
/// Indicates that a potential pipeline-affecting status flag change occurred,
|
||||
/// i.e. a change to processor mode or interrupt flags.
|
||||
void did_set_status() {}
|
||||
|
||||
/// Indicates that the PC was altered by the instruction.
|
||||
void did_set_pc() {}
|
||||
|
||||
/// Provides notification that an SWI is about to happen along with the option of skipping it; this gives handlers the
|
||||
/// chance to substitute a high-level reimplementation of the service call.
|
||||
bool should_swi([[maybe_unused]] uint32_t comment) { return true; }
|
||||
};
|
||||
|
||||
/// A class compatible with the @c OperationMapper definition of a scheduler which applies all actions
|
||||
/// immediately, updating either a set of @c Registers or using the templated @c MemoryT to access
|
||||
/// memory. No hooks are currently provided for applying realistic timing.
|
||||
///
|
||||
/// If a ControlFlowHandlerT is specified, it'll receive calls as defined in the NullControlFlowHandler above.
|
||||
template <Model model, typename MemoryT, typename ControlFlowHandlerT = NullControlFlowHandler>
|
||||
struct Executor {
|
||||
template <typename... Args>
|
||||
Executor(ControlFlowHandlerT &handler, Args &&...args) : bus(std::forward<Args>(args)...), control_flow_handler_(handler) {}
|
||||
|
||||
template <typename... Args>
|
||||
Executor(Args &&...args) : bus(std::forward<Args>(args)...) {}
|
||||
|
||||
/// @returns @c true if @c condition implies an appropriate perform call should be made for this instruction,
|
||||
/// @c false otherwise.
|
||||
bool should_schedule(Condition condition) {
|
||||
// This short-circuit of registers_.test provides the necessary compiler clue that
|
||||
// Condition::AL is not only [[likely]] but [[exceedingly likely]].
|
||||
return condition == Condition::AL ? true : registers_.test(condition);
|
||||
}
|
||||
|
||||
template <bool allow_register, bool set_carry, typename FieldsT>
|
||||
uint32_t decode_shift(FieldsT fields, uint32_t &rotate_carry, uint32_t pc_offset) {
|
||||
// "When R15 appears in the Rm position it will give the value of the PC together
|
||||
// with the PSR flags to the barrel shifter. ...
|
||||
//
|
||||
// If the shift amount is specified in the instruction, the PC will be 8 bytes ahead.
|
||||
// If a register is used to specify the shift amount, the PC will be ... 12 bytes ahead
|
||||
// when used as Rn or Rm."
|
||||
uint32_t operand2;
|
||||
if(fields.operand2() == 15) {
|
||||
operand2 = registers_.pc_status(pc_offset);
|
||||
} else {
|
||||
operand2 = registers_[fields.operand2()];
|
||||
}
|
||||
|
||||
// TODO: in C++20, a quick `if constexpr (requires` can eliminate the `allow_register` parameter.
|
||||
if constexpr (allow_register) {
|
||||
if(fields.shift_count_is_register()) {
|
||||
uint32_t shift_amount;
|
||||
|
||||
// "When R15 appears in either of the Rn or Rs positions it will give the value
|
||||
// of the PC alone, with the PSR bits replaced by zeroes. ...
|
||||
//
|
||||
// If a register is used to specify the shift amount, the
|
||||
// PC will be 8 bytes ahead when used as Rs."
|
||||
shift_amount =
|
||||
fields.shift_register() == 15 ?
|
||||
registers_.pc(4) :
|
||||
registers_[fields.shift_register()];
|
||||
|
||||
// "The amount by which the register should be shifted may be contained in
|
||||
// ... **the bottom byte** of another register".
|
||||
shift_amount &= 0xff;
|
||||
shift<set_carry, false>(fields.shift_type(), operand2, shift_amount, rotate_carry);
|
||||
return operand2;
|
||||
}
|
||||
}
|
||||
|
||||
shift<set_carry, true>(fields.shift_type(), operand2, fields.shift_amount(), rotate_carry);
|
||||
return operand2;
|
||||
}
|
||||
|
||||
template <Flags f> void perform(DataProcessing fields) {
|
||||
constexpr DataProcessingFlags flags(f);
|
||||
const bool shift_by_register = !flags.operand2_is_immediate() && fields.shift_count_is_register();
|
||||
|
||||
// Write a raw result into the PC proxy if the target is R15; it'll be stored properly later.
|
||||
uint32_t pc_proxy = 0;
|
||||
auto &destination = fields.destination() == 15 ? pc_proxy : registers_[fields.destination()];
|
||||
|
||||
// "When R15 appears in either of the Rn or Rs positions it will give the value
|
||||
// of the PC alone, with the PSR bits replaced by zeroes. ...
|
||||
//
|
||||
// If the shift amount is specified in the instruction, the PC will be 8 bytes ahead.
|
||||
// If a register is used to specify the shift amount, the PC will be ... 12 bytes ahead
|
||||
// when used as Rn or Rm."
|
||||
const uint32_t operand1 =
|
||||
(fields.operand1() == 15) ?
|
||||
registers_.pc(shift_by_register ? 8 : 4) :
|
||||
registers_[fields.operand1()];
|
||||
|
||||
uint32_t operand2;
|
||||
uint32_t rotate_carry = registers_.c();
|
||||
|
||||
// Populate carry from the shift only if it'll be used.
|
||||
constexpr bool shift_sets_carry = is_logical(flags.operation()) && flags.set_condition_codes();
|
||||
|
||||
// Get operand 2.
|
||||
if constexpr (flags.operand2_is_immediate()) {
|
||||
operand2 = fields.immediate();
|
||||
shift<ShiftType::RotateRight, shift_sets_carry, false>(operand2, fields.rotate(), rotate_carry);
|
||||
} else {
|
||||
operand2 = decode_shift<true, shift_sets_carry>(fields, rotate_carry, shift_by_register ? 8 : 4);
|
||||
}
|
||||
|
||||
uint32_t conditions = 0;
|
||||
const auto sub = [&](uint32_t lhs, uint32_t rhs) {
|
||||
conditions = lhs - rhs;
|
||||
|
||||
if constexpr (flags.operation() == DataProcessingOperation::SBC || flags.operation() == DataProcessingOperation::RSC) {
|
||||
conditions += registers_.c() - 1;
|
||||
}
|
||||
|
||||
if constexpr (flags.set_condition_codes()) {
|
||||
// "For a subtraction, including the comparison instruction CMP, C is set to 0 if
|
||||
// the subtraction produced a borrow (that is, an unsigned underflow), and to 1 otherwise."
|
||||
registers_.set_c(!Numeric::carried_out<false, 31>(lhs, rhs, conditions));
|
||||
registers_.set_v(Numeric::overflow<false>(lhs, rhs, conditions));
|
||||
}
|
||||
|
||||
if constexpr (!is_comparison(flags.operation())) {
|
||||
destination = conditions;
|
||||
}
|
||||
};
|
||||
|
||||
// Perform the data processing operation.
|
||||
switch(flags.operation()) {
|
||||
// Logical operations.
|
||||
case DataProcessingOperation::AND: conditions = destination = operand1 & operand2; break;
|
||||
case DataProcessingOperation::EOR: conditions = destination = operand1 ^ operand2; break;
|
||||
case DataProcessingOperation::ORR: conditions = destination = operand1 | operand2; break;
|
||||
case DataProcessingOperation::BIC: conditions = destination = operand1 & ~operand2; break;
|
||||
|
||||
case DataProcessingOperation::MOV: conditions = destination = operand2; break;
|
||||
case DataProcessingOperation::MVN: conditions = destination = ~operand2; break;
|
||||
|
||||
case DataProcessingOperation::TST: conditions = operand1 & operand2; break;
|
||||
case DataProcessingOperation::TEQ: conditions = operand1 ^ operand2; break;
|
||||
|
||||
case DataProcessingOperation::ADD:
|
||||
case DataProcessingOperation::ADC:
|
||||
case DataProcessingOperation::CMN:
|
||||
conditions = operand1 + operand2;
|
||||
|
||||
if constexpr (flags.operation() == DataProcessingOperation::ADC) {
|
||||
conditions += registers_.c();
|
||||
}
|
||||
|
||||
if constexpr (flags.set_condition_codes()) {
|
||||
registers_.set_c(Numeric::carried_out<true, 31>(operand1, operand2, conditions));
|
||||
registers_.set_v(Numeric::overflow<true>(operand1, operand2, conditions));
|
||||
}
|
||||
|
||||
if constexpr (!is_comparison(flags.operation())) {
|
||||
destination = conditions;
|
||||
}
|
||||
break;
|
||||
|
||||
case DataProcessingOperation::SUB:
|
||||
case DataProcessingOperation::SBC:
|
||||
case DataProcessingOperation::CMP:
|
||||
sub(operand1, operand2);
|
||||
break;
|
||||
|
||||
case DataProcessingOperation::RSB:
|
||||
case DataProcessingOperation::RSC:
|
||||
sub(operand2, operand1);
|
||||
break;
|
||||
}
|
||||
|
||||
if(!is_comparison(flags.operation()) && fields.destination() == 15) {
|
||||
set_pc<true>(pc_proxy);
|
||||
}
|
||||
if constexpr (flags.set_condition_codes()) {
|
||||
// "When Rd is R15 and the S flag in the instruction is set, the PSR is overwritten by the
|
||||
// corresponding bits in the ALU result... [even] if the instruction is of a type that does not
|
||||
// normally produce a result (CMP, CMN, TST, TEQ) ... the result will be used to update those
|
||||
// PSR flags which are not protected by virtue of the processor mode"
|
||||
if(fields.destination() == 15) {
|
||||
set_status(conditions);
|
||||
} else {
|
||||
// Set N and Z in a unified way.
|
||||
registers_.set_nz(conditions);
|
||||
|
||||
// Set C from the barrel shifter if applicable.
|
||||
if constexpr (shift_sets_carry) {
|
||||
registers_.set_c(rotate_carry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <Flags f> void perform(Multiply fields) {
|
||||
constexpr MultiplyFlags flags(f);
|
||||
|
||||
// R15 rules:
|
||||
//
|
||||
// * Rs: no PSR, 8 bytes ahead;
|
||||
// * Rn: with PSR, 8 bytes ahead;
|
||||
// * Rm: with PSR, 12 bytes ahead.
|
||||
|
||||
const uint32_t multiplicand = fields.multiplicand() == 15 ? registers_.pc(4) : registers_[fields.multiplicand()];
|
||||
const uint32_t multiplier = fields.multiplier() == 15 ? registers_.pc_status(4) : registers_[fields.multiplier()];
|
||||
const uint32_t accumulator =
|
||||
flags.operation() == MultiplyFlags::Operation::MUL ? 0 :
|
||||
(fields.multiplicand() == 15 ? registers_.pc_status(8) : registers_[fields.accumulator()]);
|
||||
|
||||
const uint32_t result = multiplicand * multiplier + accumulator;
|
||||
|
||||
if constexpr (flags.set_condition_codes()) {
|
||||
registers_.set_nz(result);
|
||||
// V is unaffected; C is undefined.
|
||||
}
|
||||
|
||||
if(fields.destination() != 15) {
|
||||
registers_[fields.destination()] = result;
|
||||
}
|
||||
}
|
||||
|
||||
template <Flags f> void perform(Branch branch) {
|
||||
constexpr BranchFlags flags(f);
|
||||
|
||||
if constexpr (flags.operation() == BranchFlags::Operation::BL) {
|
||||
registers_[14] = registers_.pc_status(0);
|
||||
}
|
||||
set_pc<true>(registers_.pc(4) + branch.offset());
|
||||
}
|
||||
|
||||
template <Flags f> void perform(SingleDataTransfer transfer) {
|
||||
constexpr SingleDataTransferFlags flags(f);
|
||||
|
||||
// Calculate offset.
|
||||
uint32_t offset;
|
||||
if constexpr (flags.offset_is_register()) {
|
||||
// The 8 shift control bits are described in 6.2.3, but
|
||||
// the register specified shift amounts are not available
|
||||
// in this instruction class.
|
||||
uint32_t carry = registers_.c();
|
||||
offset = decode_shift<false, false>(transfer, carry, 4);
|
||||
} else {
|
||||
offset = transfer.immediate();
|
||||
}
|
||||
|
||||
// Obtain base address.
|
||||
uint32_t address =
|
||||
transfer.base() == 15 ?
|
||||
registers_.pc(4) :
|
||||
registers_[transfer.base()];
|
||||
|
||||
// Determine what the address will be after offsetting.
|
||||
uint32_t offsetted_address = address;
|
||||
if constexpr (flags.add_offset()) {
|
||||
offsetted_address += offset;
|
||||
} else {
|
||||
offsetted_address -= offset;
|
||||
}
|
||||
|
||||
// If preindexing, apply now.
|
||||
if constexpr (flags.pre_index()) {
|
||||
address = offsetted_address;
|
||||
}
|
||||
|
||||
// Check for an address exception.
|
||||
if(is_invalid_address(address)) {
|
||||
exception<Registers::Exception::Address>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Decide whether to write back — when either postindexing or else write back is requested.
|
||||
//
|
||||
// Note to future self on write-back:
|
||||
//
|
||||
// It's currently unclear what to do in the case of e.g. `str r13, [r13, #0x10]!`. Is the value
|
||||
// written r13 as modified or the original r13? If it's as modified, does that imply that
|
||||
// write back has occurred regardless of a data abort?
|
||||
//
|
||||
// TODO: resolve uncertainty.
|
||||
constexpr bool should_write_back = !flags.pre_index() || flags.write_back_address();
|
||||
|
||||
// "... post-indexed data transfers always write back the modified base. The only use of the [write-back address]
|
||||
// bit in a post-indexed data transfer is in non-user mode code, where setting the W bit forces the /TRANS pin
|
||||
// to go LOW for the transfer"
|
||||
const bool trans = (registers_.mode() == Mode::User) || (!flags.pre_index() && flags.write_back_address());
|
||||
if constexpr (flags.operation() == SingleDataTransferFlags::Operation::STR) {
|
||||
const uint32_t source =
|
||||
transfer.source() == 15 ?
|
||||
registers_.pc_status(8) :
|
||||
registers_[transfer.source()];
|
||||
|
||||
bool did_write;
|
||||
if constexpr (flags.transfer_byte()) {
|
||||
did_write = bus.template write<uint8_t>(address, uint8_t(source), registers_.mode(), trans);
|
||||
} else {
|
||||
// "The data presented to the data bus are not affected if the address is not word aligned".
|
||||
did_write = bus.template write<uint32_t>(address, source, registers_.mode(), trans);
|
||||
}
|
||||
|
||||
if(!did_write) {
|
||||
exception<Registers::Exception::DataAbort>();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
bool did_read;
|
||||
uint32_t value = 0;
|
||||
|
||||
if constexpr (flags.transfer_byte()) {
|
||||
uint8_t target = 0; // Value should never be used; this avoids a spurious GCC warning.
|
||||
did_read = bus.template read<uint8_t>(address, target, registers_.mode(), trans);
|
||||
if(did_read) {
|
||||
value = target;
|
||||
}
|
||||
} else {
|
||||
did_read = bus.template read<uint32_t>(address, value, registers_.mode(), trans);
|
||||
|
||||
if constexpr (model != Model::ARMv2with32bitAddressing) {
|
||||
// "An address offset from a word boundary will cause the data to be rotated into the
|
||||
// register so that the addressed byte occuplies bits 0 to 7."
|
||||
//
|
||||
// (though the test set that inspired 'ARMv2with32bitAddressing' appears not to honour this;
|
||||
// test below assumes it went away by the version of ARM that set supports)
|
||||
switch(address & 3) {
|
||||
case 0: break;
|
||||
case 1: value = (value >> 8) | (value << 24); break;
|
||||
case 2: value = (value >> 16) | (value << 16); break;
|
||||
case 3: value = (value >> 24) | (value << 8); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!did_read) {
|
||||
exception<Registers::Exception::DataAbort>();
|
||||
return;
|
||||
}
|
||||
|
||||
if(transfer.destination() == 15) {
|
||||
set_pc<true>(value);
|
||||
} else {
|
||||
registers_[transfer.destination()] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (should_write_back) {
|
||||
// Empirically: I think order of operations for a load is: (i) write back; (ii) store value from bus.
|
||||
// So if this is a load, don't allow write back to overwrite what was loaded.
|
||||
if(flags.operation() == SingleDataTransferFlags::Operation::STR || transfer.base() != transfer.destination()) {
|
||||
if(transfer.base() == 15) {
|
||||
set_pc<true>(offsetted_address);
|
||||
} else {
|
||||
registers_[transfer.base()] = offsetted_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template <Flags f> void perform(BlockDataTransfer transfer) {
|
||||
constexpr BlockDataTransferFlags flags(f);
|
||||
constexpr bool is_ldm = flags.operation() == BlockDataTransferFlags::Operation::LDM;
|
||||
|
||||
// Ensure that *base points to the base register if it can be written back;
|
||||
// also set address to the base.
|
||||
uint32_t *base = nullptr;
|
||||
uint32_t address;
|
||||
if(transfer.base() == 15) {
|
||||
address = registers_.pc_status(4);
|
||||
} else {
|
||||
base = ®isters_[transfer.base()];
|
||||
address = *base;
|
||||
}
|
||||
|
||||
// For an LDM pc_proxy will receive any read R15 value;
|
||||
// for an STM it'll hold the value to be written.
|
||||
uint32_t pc_proxy = 0;
|
||||
|
||||
// Read the base address and take a copy in case a data abort means that
|
||||
// it has to be restored later.
|
||||
uint32_t initial_address = address;
|
||||
|
||||
// Grab the register list and decide whether user registers are being used.
|
||||
const uint16_t list = transfer.register_list();
|
||||
const bool adopt_user_mode = flags.load_psr() && (!is_ldm || !(list & (1 << 15)));
|
||||
|
||||
// Write back will prima facie occur if:
|
||||
// (i) the instruction asks for it; and
|
||||
// (ii) the write-back register isn't R15.
|
||||
bool write_back = base && flags.write_back_address();
|
||||
|
||||
// Collate a transfer list; this is a very long-winded way of implementing STM
|
||||
// and LDM but right now the objective is merely correctness.
|
||||
//
|
||||
// If this is LDM and it turns out that base is also in the transfer list,
|
||||
// disable write back.
|
||||
uint32_t *transfer_sources[16];
|
||||
uint32_t total = 0;
|
||||
for(uint32_t c = 0; c < 15; c++) {
|
||||
if(list & (1 << c)) {
|
||||
uint32_t *const next = ®isters_.reg(adopt_user_mode, c);
|
||||
if(is_ldm && next == base) write_back = false;
|
||||
transfer_sources[total++] = next;
|
||||
}
|
||||
}
|
||||
|
||||
// If the last thing in the list is R15, redirect it to the PC proxy,
|
||||
// possibly populating with a meaningful value.
|
||||
if(list & (1 << 15)) {
|
||||
if(!is_ldm) {
|
||||
pc_proxy = registers_.pc_status(8);
|
||||
}
|
||||
transfer_sources[total++] = &pc_proxy;
|
||||
}
|
||||
|
||||
// If this is STM and the first thing in the list is the same as base,
|
||||
// point it at initial_address instead.
|
||||
if(!is_ldm && total && transfer_sources[0] == base) {
|
||||
transfer_sources[0] = &initial_address;
|
||||
}
|
||||
|
||||
// Calculate final_address, which is what will be written back if required;
|
||||
// update address to point to the low end of the transfer block.
|
||||
//
|
||||
// Writes are always ordered from lowest address to highest; adjust the
|
||||
// start address if this write is supposed to fill memory downward from
|
||||
// the base.
|
||||
uint32_t final_address;
|
||||
if constexpr (!flags.add_offset()) {
|
||||
// Decrementing mode; final_address is the value the base register should
|
||||
// have after this operation if writeback is enabled, so it's below
|
||||
// the original address. But also writes always occur from lowest address
|
||||
// to highest, so push the current address to the bottom.
|
||||
final_address = address - total * 4;
|
||||
address = final_address;
|
||||
} else {
|
||||
final_address = address + total * 4;
|
||||
}
|
||||
|
||||
// Write back if enabled.
|
||||
if(write_back) {
|
||||
*base = final_address;
|
||||
}
|
||||
|
||||
// Update address in advance for:
|
||||
// * pre-indexed upward stores; and
|
||||
// * post-indxed downward stores.
|
||||
if constexpr (flags.pre_index() == flags.add_offset()) {
|
||||
address += 4;
|
||||
}
|
||||
|
||||
// Perform all memory accesses, tracking whether either kind of abort will be
|
||||
// required.
|
||||
const bool trans = registers_.mode() == Mode::User;
|
||||
const bool address_error = is_invalid_address(address);
|
||||
bool accesses_succeeded = true;
|
||||
|
||||
if constexpr (is_ldm) {
|
||||
// Keep a record of the value replaced by the last load and
|
||||
// where it came from. A data abort cancels both the current load and
|
||||
// the one before it, so this might be used by this implementation to
|
||||
// undo the previous load.
|
||||
struct {
|
||||
uint32_t *target = nullptr;
|
||||
uint32_t value = 0;
|
||||
} last_replacement;
|
||||
|
||||
for(uint32_t c = 0; c < total; c++) {
|
||||
uint32_t &value = *transfer_sources[c];
|
||||
|
||||
// When ARM detects a data abort during a load multiple instruction, it modifies the operation of
|
||||
// the instruction to ensure that recovery is possible.
|
||||
//
|
||||
// * Overwriting of registers stops when the abort happens. The aborting load will not
|
||||
// take place, nor will the preceding one ...
|
||||
// * The base register is restored, to its modified value if write-back was requested.
|
||||
if(accesses_succeeded) {
|
||||
const uint32_t replaced = value;
|
||||
accesses_succeeded &= bus.template read<uint32_t>(address, value, registers_.mode(), trans);
|
||||
|
||||
// Update the last-modified slot if the access succeeded; otherwise
|
||||
// undo the last modification if there was one, and undo the base
|
||||
// address change.
|
||||
if(accesses_succeeded) {
|
||||
last_replacement.value = replaced;
|
||||
last_replacement.target = transfer_sources[c];
|
||||
} else {
|
||||
if(last_replacement.target) {
|
||||
*last_replacement.target = last_replacement.value;
|
||||
}
|
||||
|
||||
// Also restore the base register, including to its original value
|
||||
// if write back was disabled.
|
||||
if(base) {
|
||||
if(write_back) {
|
||||
*base = final_address;
|
||||
} else {
|
||||
*base = initial_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Implicitly: do the access anyway, but don't store the value. I think.
|
||||
uint32_t throwaway;
|
||||
bus.template read<uint32_t>(address, throwaway, registers_.mode(), trans);
|
||||
}
|
||||
|
||||
// Advance.
|
||||
address += 4;
|
||||
}
|
||||
} else {
|
||||
for(uint32_t c = 0; c < total; c++) {
|
||||
uint32_t &value = *transfer_sources[c];
|
||||
|
||||
if(!address_error) {
|
||||
// "If the abort occurs during a store multiple instruction, ARM takes little action until
|
||||
// the instruction completes, whereupon it enters the data abort trap. The memory manager is
|
||||
// responsible for preventing erroneous writes to the memory."
|
||||
accesses_succeeded &= bus.template write<uint32_t>(address, value, registers_.mode(), trans);
|
||||
} else {
|
||||
// Do a throwaway read.
|
||||
uint32_t throwaway;
|
||||
bus.template read<uint32_t>(address, throwaway, registers_.mode(), trans);
|
||||
}
|
||||
|
||||
// Advance.
|
||||
address += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally throw an exception if necessary.
|
||||
if(address_error) {
|
||||
exception<Registers::Exception::Address>();
|
||||
} else if(!accesses_succeeded) {
|
||||
exception<Registers::Exception::DataAbort>();
|
||||
} else {
|
||||
// If this was an LDM to R15 then apply it appropriately.
|
||||
if(is_ldm && list & (1 << 15)) {
|
||||
set_pc<true>(pc_proxy);
|
||||
if constexpr (flags.load_psr()) {
|
||||
set_status(pc_proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void software_interrupt(SoftwareInterrupt swi) {
|
||||
if(control_flow_handler_.should_swi(swi.comment())) {
|
||||
exception<Registers::Exception::SoftwareInterrupt>();
|
||||
}
|
||||
}
|
||||
void unknown() {
|
||||
exception<Registers::Exception::UndefinedInstruction>();
|
||||
}
|
||||
|
||||
// Act as if no coprocessors present.
|
||||
template <Flags> void perform(CoprocessorRegisterTransfer) {
|
||||
exception<Registers::Exception::UndefinedInstruction>();
|
||||
}
|
||||
template <Flags> void perform(CoprocessorDataOperation) {
|
||||
exception<Registers::Exception::UndefinedInstruction>();
|
||||
}
|
||||
template <Flags> void perform(CoprocessorDataTransfer) {
|
||||
exception<Registers::Exception::UndefinedInstruction>();
|
||||
}
|
||||
|
||||
/// @returns The current registers state.
|
||||
const Registers ®isters() const {
|
||||
return registers_;
|
||||
}
|
||||
|
||||
// Included primarily for testing; my full opinion on this is still
|
||||
// incompletely-formed.
|
||||
Registers ®isters() {
|
||||
return registers_;
|
||||
}
|
||||
|
||||
/// Indicates a prefetch abort exception.
|
||||
void prefetch_abort() {
|
||||
exception<Registers::Exception::PrefetchAbort>();
|
||||
}
|
||||
|
||||
/// Sets the expected address of the instruction after whichever is about to be executed.
|
||||
/// So it's PC+4 compared to most other systems.
|
||||
///
|
||||
/// By default this is not forwarded to the control-flow handler.
|
||||
template <bool notify = false>
|
||||
void set_pc(uint32_t pc) {
|
||||
registers_.set_pc(pc);
|
||||
if constexpr (notify) {
|
||||
control_flow_handler_.did_set_pc();
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns The address of the instruction that should be fetched next. So as execution of each instruction
|
||||
/// begins, this will be +4 from the instruction being executed; at the end of the instruction it'll either still be +4
|
||||
/// or else be some other address if a branch or exception has occurred.
|
||||
uint32_t pc() const {
|
||||
return registers_.pc(0);
|
||||
}
|
||||
|
||||
MemoryT bus;
|
||||
|
||||
private:
|
||||
template <Registers::Exception type>
|
||||
void exception() {
|
||||
registers_.exception<type>();
|
||||
control_flow_handler_.did_set_pc();
|
||||
}
|
||||
|
||||
void set_status(uint32_t status) {
|
||||
registers_.set_status(status);
|
||||
control_flow_handler_.did_set_status();
|
||||
}
|
||||
|
||||
using ControlFlowHandlerTStorage =
|
||||
typename std::conditional<
|
||||
std::is_same_v<ControlFlowHandlerT, NullControlFlowHandler>,
|
||||
ControlFlowHandlerT,
|
||||
ControlFlowHandlerT &>::type;
|
||||
ControlFlowHandlerTStorage control_flow_handler_;
|
||||
Registers registers_;
|
||||
|
||||
static bool is_invalid_address(uint32_t address) {
|
||||
if constexpr (model == Model::ARMv2with32bitAddressing) {
|
||||
return false;
|
||||
}
|
||||
return address >= 1 << 26;
|
||||
}
|
||||
};
|
||||
|
||||
/// Executes the instruction @c instruction which should have been fetched from @c executor.pc(),
|
||||
/// modifying @c executor.
|
||||
template <Model model, typename MemoryT, typename StatusObserverT>
|
||||
void execute(uint32_t instruction, Executor<model, MemoryT, StatusObserverT> &executor) {
|
||||
executor.set_pc(executor.pc() + 4);
|
||||
dispatch<model>(instruction, executor);
|
||||
}
|
||||
|
||||
}
|
||||
553
InstructionSets/ARM/OperationMapper.hpp
Normal file
553
InstructionSets/ARM/OperationMapper.hpp
Normal file
@@ -0,0 +1,553 @@
|
||||
//
|
||||
// OperationMapper.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/02/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Reflection/Dispatcher.hpp"
|
||||
#include "BarrelShifter.hpp"
|
||||
|
||||
namespace InstructionSet::ARM {
|
||||
|
||||
enum class Model {
|
||||
ARMv2,
|
||||
|
||||
/// Like an ARMv2 but all non-PC addressing is 64-bit. Primarily useful for a particular set of test
|
||||
/// cases that I want to apply retroactively; not a real iteration.
|
||||
ARMv2with32bitAddressing,
|
||||
};
|
||||
|
||||
enum class Condition {
|
||||
EQ, NE, CS, CC,
|
||||
MI, PL, VS, VC,
|
||||
HI, LS, GE, LT,
|
||||
GT, LE, AL, NV,
|
||||
};
|
||||
|
||||
//
|
||||
// Implementation details.
|
||||
//
|
||||
static constexpr int FlagsStartBit = 20;
|
||||
using Flags = uint8_t;
|
||||
|
||||
template <int position>
|
||||
constexpr bool flag_bit(uint8_t flags) {
|
||||
static_assert(position >= 20 && position < 28);
|
||||
return flags & (1 << (position - FlagsStartBit));
|
||||
}
|
||||
|
||||
//
|
||||
// Methods common to data processing and data transfer.
|
||||
//
|
||||
struct WithShiftControlBits {
|
||||
constexpr WithShiftControlBits(uint32_t opcode) noexcept : opcode_(opcode) {}
|
||||
|
||||
/// The operand 2 register index if @c operand2_is_immediate() is @c false; meaningless otherwise.
|
||||
uint32_t operand2() const { return opcode_ & 0xf; }
|
||||
/// The type of shift to apply to operand 2 if @c operand2_is_immediate() is @c false; meaningless otherwise.
|
||||
ShiftType shift_type() const { return ShiftType((opcode_ >> 5) & 3); }
|
||||
/// @returns @c true if the amount to shift by should be taken from a register; @c false if it is an immediate value.
|
||||
bool shift_count_is_register() const { return opcode_ & (1 << 4); }
|
||||
/// The shift amount register index if @c shift_count_is_register() is @c true; meaningless otherwise.
|
||||
uint32_t shift_register() const { return (opcode_ >> 8) & 0xf; }
|
||||
/// The amount to shift by if @c shift_count_is_register() is @c false; meaningless otherwise.
|
||||
uint32_t shift_amount() const { return (opcode_ >> 7) & 0x1f; }
|
||||
|
||||
protected:
|
||||
uint32_t opcode_;
|
||||
};
|
||||
|
||||
//
|
||||
// Branch (i.e. B and BL).
|
||||
//
|
||||
struct BranchFlags {
|
||||
constexpr BranchFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
enum class Operation {
|
||||
B, /// Add offset to PC; programmer allows for PC being two words ahead.
|
||||
BL, /// Copy PC and PSR to R14, then branch. Copied PC points to next instruction.
|
||||
};
|
||||
|
||||
/// @returns The operation to apply.
|
||||
constexpr Operation operation() const {
|
||||
return flag_bit<24>(flags_) ? Operation::BL : Operation::B;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
struct Branch {
|
||||
constexpr Branch(uint32_t opcode) noexcept : opcode_(opcode) {}
|
||||
|
||||
/// The 26-bit offset to add to the PC.
|
||||
uint32_t offset() const { return (opcode_ & 0xff'ffff) << 2; }
|
||||
|
||||
private:
|
||||
uint32_t opcode_;
|
||||
};
|
||||
|
||||
//
|
||||
// Data processing (i.e. AND to MVN).
|
||||
//
|
||||
enum class DataProcessingOperation {
|
||||
AND, /// Rd = Op1 AND Op2.
|
||||
EOR, /// Rd = Op1 EOR Op2.
|
||||
SUB, /// Rd = Op1 - Op2.
|
||||
RSB, /// Rd = Op2 - Op1.
|
||||
ADD, /// Rd = Op1 + Op2.
|
||||
ADC, /// Rd = Op1 + Ord2 + C.
|
||||
SBC, /// Rd = Op1 - Op2 + C.
|
||||
RSC, /// Rd = Op2 - Op1 + C.
|
||||
TST, /// Set condition codes on Op1 AND Op2.
|
||||
TEQ, /// Set condition codes on Op1 EOR Op2.
|
||||
CMP, /// Set condition codes on Op1 - Op2.
|
||||
CMN, /// Set condition codes on Op1 + Op2.
|
||||
ORR, /// Rd = Op1 OR Op2.
|
||||
MOV, /// Rd = Op2
|
||||
BIC, /// Rd = Op1 AND NOT Op2.
|
||||
MVN, /// Rd = NOT Op2.
|
||||
};
|
||||
|
||||
constexpr bool is_logical(DataProcessingOperation operation) {
|
||||
switch(operation) {
|
||||
case DataProcessingOperation::AND:
|
||||
case DataProcessingOperation::EOR:
|
||||
case DataProcessingOperation::TST:
|
||||
case DataProcessingOperation::TEQ:
|
||||
case DataProcessingOperation::ORR:
|
||||
case DataProcessingOperation::MOV:
|
||||
case DataProcessingOperation::BIC:
|
||||
case DataProcessingOperation::MVN:
|
||||
return true;
|
||||
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr bool is_comparison(DataProcessingOperation operation) {
|
||||
switch(operation) {
|
||||
case DataProcessingOperation::TST:
|
||||
case DataProcessingOperation::TEQ:
|
||||
case DataProcessingOperation::CMP:
|
||||
case DataProcessingOperation::CMN:
|
||||
return true;
|
||||
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct DataProcessingFlags {
|
||||
constexpr DataProcessingFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
/// @returns The operation to apply.
|
||||
constexpr DataProcessingOperation operation() const {
|
||||
return DataProcessingOperation((flags_ >> (21 - FlagsStartBit)) & 0xf);
|
||||
}
|
||||
|
||||
/// @returns @c true if operand 2 is defined by the @c rotate() and @c immediate() fields;
|
||||
/// @c false if it is defined by the @c shift_*() and @c operand2() fields.
|
||||
constexpr bool operand2_is_immediate() const { return flag_bit<25>(flags_); }
|
||||
|
||||
/// @c true if the status register should be updated; @c false otherwise.
|
||||
constexpr bool set_condition_codes() const { return flag_bit<20>(flags_); }
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
struct DataProcessing: public WithShiftControlBits {
|
||||
using WithShiftControlBits::WithShiftControlBits;
|
||||
|
||||
/// The destination register index. i.e. Rd.
|
||||
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
|
||||
|
||||
/// The operand 1 register index. i.e. Rn.
|
||||
uint32_t operand1() const { return (opcode_ >> 16) & 0xf; }
|
||||
|
||||
//
|
||||
// Immediate values for operand 2.
|
||||
//
|
||||
|
||||
/// An 8-bit value to rotate right @c rotate() places if @c operand2_is_immediate() is @c true; meaningless otherwise.
|
||||
uint32_t immediate() const { return opcode_ & 0xff; }
|
||||
/// The number of bits to rotate @c immediate() by to the right if @c operand2_is_immediate() is @c true; meaningless otherwise.
|
||||
uint32_t rotate() const { return (opcode_ >> 7) & 0x1e; }
|
||||
};
|
||||
|
||||
//
|
||||
// MUL and MLA.
|
||||
//
|
||||
struct MultiplyFlags {
|
||||
constexpr MultiplyFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
/// @c true if the status register should be updated; @c false otherwise.
|
||||
constexpr bool set_condition_codes() const { return flag_bit<20>(flags_); }
|
||||
|
||||
enum class Operation {
|
||||
MUL, /// Rd = Rm * Rs
|
||||
MLA, /// Rd = Rm * Rs + Rn
|
||||
};
|
||||
|
||||
/// @returns The operation to apply.
|
||||
constexpr Operation operation() const {
|
||||
return flag_bit<21>(flags_) ? Operation::MLA : Operation::MUL;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
struct Multiply {
|
||||
constexpr Multiply(uint32_t opcode) noexcept : opcode_(opcode) {}
|
||||
|
||||
/// The destination register index. i.e. 'Rd'.
|
||||
uint32_t destination() const { return (opcode_ >> 16) & 0xf; }
|
||||
|
||||
/// The accumulator register index for multiply-add. i.e. 'Rn'.
|
||||
uint32_t accumulator() const { return (opcode_ >> 12) & 0xf; }
|
||||
|
||||
/// The multiplicand register index. i.e. 'Rs'.
|
||||
uint32_t multiplicand() const { return (opcode_ >> 8) & 0xf; }
|
||||
|
||||
/// The multiplier register index. i.e. 'Rm'.
|
||||
uint32_t multiplier() const { return opcode_ & 0xf; }
|
||||
|
||||
private:
|
||||
uint32_t opcode_;
|
||||
};
|
||||
|
||||
//
|
||||
// Single data transfer (LDR, STR).
|
||||
//
|
||||
struct SingleDataTransferFlags {
|
||||
constexpr SingleDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
enum class Operation {
|
||||
LDR, /// Read single byte or word from [base + offset], possibly mutating the base.
|
||||
STR, /// Write a single byte or word to [base + offset], possibly mutating the base.
|
||||
};
|
||||
|
||||
constexpr Operation operation() const {
|
||||
return flag_bit<20>(flags_) ? Operation::LDR : Operation::STR;
|
||||
}
|
||||
|
||||
constexpr bool offset_is_register() const { return flag_bit<25>(flags_); }
|
||||
constexpr bool pre_index() const { return flag_bit<24>(flags_); }
|
||||
constexpr bool add_offset() const { return flag_bit<23>(flags_); }
|
||||
constexpr bool transfer_byte() const { return flag_bit<22>(flags_); }
|
||||
constexpr bool write_back_address() const { return flag_bit<21>(flags_); }
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
struct SingleDataTransfer: public WithShiftControlBits {
|
||||
using WithShiftControlBits::WithShiftControlBits;
|
||||
|
||||
/// The destination register index. i.e. 'Rd' for LDR.
|
||||
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
|
||||
|
||||
/// The destination register index. i.e. 'Rd' for STR.
|
||||
uint32_t source() const { return (opcode_ >> 12) & 0xf; }
|
||||
|
||||
/// The base register index. i.e. 'Rn'.
|
||||
uint32_t base() const { return (opcode_ >> 16) & 0xf; }
|
||||
|
||||
/// The immediate offset, if @c offset_is_register() was @c false; meaningless otherwise.
|
||||
uint32_t immediate() const { return opcode_ & 0xfff; }
|
||||
};
|
||||
|
||||
//
|
||||
// Block data transfer (LDR, STR).
|
||||
//
|
||||
struct BlockDataTransferFlags {
|
||||
constexpr BlockDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
enum class Operation {
|
||||
LDM, /// Read 1–16 words from [base], possibly mutating it.
|
||||
STM, /// Write 1-16 words to [base], possibly mutating it.
|
||||
};
|
||||
|
||||
constexpr Operation operation() const {
|
||||
return flag_bit<20>(flags_) ? Operation::LDM : Operation::STM;
|
||||
}
|
||||
|
||||
constexpr bool pre_index() const { return flag_bit<24>(flags_); }
|
||||
constexpr bool add_offset() const { return flag_bit<23>(flags_); }
|
||||
constexpr bool load_psr() const { return flag_bit<22>(flags_); }
|
||||
constexpr bool write_back_address() const { return flag_bit<21>(flags_); }
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
struct BlockDataTransfer: public WithShiftControlBits {
|
||||
using WithShiftControlBits::WithShiftControlBits;
|
||||
|
||||
/// The base register index. i.e. 'Rn'.
|
||||
uint32_t base() const { return (opcode_ >> 16) & 0xf; }
|
||||
|
||||
/// A bitfield indicating which registers to load or store.
|
||||
uint16_t register_list() const { return static_cast<uint16_t>(opcode_); }
|
||||
uint32_t popcount() const {
|
||||
const uint16_t list = register_list();
|
||||
|
||||
// TODO: just use std::popcount when adopting C++20.
|
||||
uint32_t total = ((list & 0xaaaa) >> 1) + (list & 0x5555);
|
||||
total = ((total & 0xcccc) >> 2) + (total & 0x3333);
|
||||
total = ((total & 0xf0f0) >> 4) + (total & 0x0f0f);
|
||||
total = ((total & 0xff00) >> 8) + (total & 0x00ff);
|
||||
|
||||
return total;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Coprocessor data operation.
|
||||
//
|
||||
struct CoprocessorDataOperationFlags {
|
||||
constexpr CoprocessorDataOperationFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
constexpr int coprocessor_operation() const { return (flags_ >> (FlagsStartBit - 20)) & 0xf; }
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
struct CoprocessorDataOperation {
|
||||
constexpr CoprocessorDataOperation(uint32_t opcode) noexcept : opcode_(opcode) {}
|
||||
|
||||
uint32_t operand1() const { return (opcode_ >> 16) & 0xf; }
|
||||
uint32_t operand2() const { return opcode_ & 0xf; }
|
||||
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
|
||||
uint32_t coprocessor() const { return (opcode_ >> 8) & 0xf; }
|
||||
uint32_t information() const { return (opcode_ >> 5) & 0x7; }
|
||||
|
||||
private:
|
||||
uint32_t opcode_;
|
||||
};
|
||||
|
||||
//
|
||||
// Coprocessor register transfer.
|
||||
//
|
||||
struct CoprocessorRegisterTransferFlags {
|
||||
constexpr CoprocessorRegisterTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
enum class Operation {
|
||||
MRC, /// Move from coprocessor register to ARM register.
|
||||
MCR, /// Move from ARM register to coprocessor register.
|
||||
};
|
||||
|
||||
constexpr Operation operation() const {
|
||||
return flag_bit<20>(flags_) ? Operation::MRC : Operation::MCR;
|
||||
}
|
||||
constexpr int coprocessor_operation() const { return (flags_ >> (FlagsStartBit - 20)) & 0x7; }
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
struct CoprocessorRegisterTransfer {
|
||||
constexpr CoprocessorRegisterTransfer(uint32_t opcode) noexcept : opcode_(opcode) {}
|
||||
|
||||
uint32_t operand1() const { return (opcode_ >> 16) & 0xf; }
|
||||
uint32_t operand2() const { return opcode_ & 0xf; }
|
||||
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
|
||||
uint32_t coprocessor() const { return (opcode_ >> 8) & 0xf; }
|
||||
uint32_t information() const { return (opcode_ >> 5) & 0x7; }
|
||||
|
||||
private:
|
||||
uint32_t opcode_;
|
||||
};
|
||||
|
||||
//
|
||||
// Coprocessor data transfer.
|
||||
//
|
||||
struct CoprocessorDataTransferFlags {
|
||||
constexpr CoprocessorDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
|
||||
|
||||
enum class Operation {
|
||||
LDC, /// Coprocessor data transfer load.
|
||||
STC, /// Coprocessor data transfer store.
|
||||
};
|
||||
|
||||
constexpr Operation operation() const {
|
||||
return flag_bit<20>(flags_) ? Operation::LDC : Operation::STC;
|
||||
}
|
||||
constexpr bool pre_index() const { return flag_bit<24>(flags_); }
|
||||
constexpr bool add_offset() const { return flag_bit<23>(flags_); }
|
||||
constexpr bool transfer_length() const { return flag_bit<22>(flags_); }
|
||||
constexpr bool write_back_address() const { return flag_bit<21>(flags_); }
|
||||
|
||||
private:
|
||||
uint8_t flags_;
|
||||
};
|
||||
|
||||
//
|
||||
// Software interrupt.
|
||||
//
|
||||
struct SoftwareInterrupt {
|
||||
constexpr SoftwareInterrupt(uint32_t opcode) noexcept : opcode_(opcode) {}
|
||||
|
||||
/// The 24-bit comment field, often decoded by the receiver of an SWI.
|
||||
uint32_t comment() const { return opcode_ & 0xff'ffff; }
|
||||
|
||||
private:
|
||||
uint32_t opcode_;
|
||||
};
|
||||
|
||||
struct CoprocessorDataTransfer {
|
||||
constexpr CoprocessorDataTransfer(uint32_t opcode) noexcept : opcode_(opcode) {}
|
||||
|
||||
int base() const { return (opcode_ >> 16) & 0xf; }
|
||||
|
||||
int source() const { return (opcode_ >> 12) & 0xf; }
|
||||
int destination() const { return (opcode_ >> 12) & 0xf; }
|
||||
|
||||
int coprocessor() const { return (opcode_ >> 8) & 0xf; }
|
||||
int offset() const { return opcode_ & 0xff; }
|
||||
|
||||
private:
|
||||
uint32_t opcode_;
|
||||
};
|
||||
|
||||
/// Operation mapper; use the free function @c dispatch as defined below.
|
||||
template <Model>
|
||||
struct OperationMapper {
|
||||
static Condition condition(uint32_t instruction) {
|
||||
return Condition(instruction >> 28);
|
||||
}
|
||||
|
||||
template <int i, typename SchedulerT>
|
||||
static void dispatch(uint32_t instruction, SchedulerT &scheduler) {
|
||||
// Put the 8-bit segment of instruction back into its proper place;
|
||||
// this allows all the tests below to be written so as to coordinate
|
||||
// properly with the data sheet, and since it's all compile-time work
|
||||
// it doesn't cost anything.
|
||||
constexpr auto partial = uint32_t(i << 20);
|
||||
|
||||
// Cf. the ARM2 datasheet, p.45. Tests below match its ordering
|
||||
// other than that 'undefined' is the fallthrough case. More specific
|
||||
// page references are provided were more detailed versions of the
|
||||
// decoding are depicted.
|
||||
|
||||
// Multiply and multiply-accumulate (MUL, MLA); cf. p.23.
|
||||
//
|
||||
// This usurps a potential data processing decoding, so needs priority.
|
||||
if constexpr (((partial >> 22) & 0b111'111) == 0b000'000) {
|
||||
// This implementation provides only eight bits baked into the template parameters so
|
||||
// an additional dynamic test is required to check whether this is really, really MUL or MLA.
|
||||
if((instruction & 0b1111'0000) == 0b1001'0000) {
|
||||
scheduler.template perform<i>(Multiply(instruction));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Data processing; cf. p.17.
|
||||
if constexpr (((partial >> 26) & 0b11) == 0b00) {
|
||||
scheduler.template perform<i>(DataProcessing(instruction));
|
||||
return;
|
||||
}
|
||||
|
||||
// Single data transfer (LDR, STR); cf. p.25.
|
||||
if constexpr (((partial >> 26) & 0b11) == 0b01) {
|
||||
scheduler.template perform<i>(SingleDataTransfer(instruction));
|
||||
return;
|
||||
}
|
||||
|
||||
// Block data transfer (LDM, STM); cf. p.29.
|
||||
if constexpr (((partial >> 25) & 0b111) == 0b100) {
|
||||
scheduler.template perform<i>(BlockDataTransfer(instruction));
|
||||
return;
|
||||
}
|
||||
|
||||
// Branch and branch with link (B, BL); cf. p.15.
|
||||
if constexpr (((partial >> 25) & 0b111) == 0b101) {
|
||||
scheduler.template perform<i>(Branch(instruction));
|
||||
return;
|
||||
}
|
||||
|
||||
// Software interreupt; cf. p.35.
|
||||
if constexpr (((partial >> 24) & 0b1111) == 0b1111) {
|
||||
scheduler.software_interrupt(SoftwareInterrupt(instruction));
|
||||
return;
|
||||
}
|
||||
|
||||
// Both:
|
||||
// Coprocessor data operation; cf. p. 37; and
|
||||
// Coprocessor register transfers; cf. p. 42.
|
||||
if constexpr (((partial >> 24) & 0b1111) == 0b1110) {
|
||||
if(instruction & (1 << 4)) {
|
||||
// Register transfer.
|
||||
scheduler.template perform<i>(CoprocessorRegisterTransfer(instruction));
|
||||
} else {
|
||||
// Data operation.
|
||||
scheduler.template perform<i>(CoprocessorDataOperation(instruction));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Coprocessor data transfers; cf. p.39.
|
||||
if constexpr (((partial >> 25) & 0b111) == 0b110) {
|
||||
scheduler.template perform<i>(CoprocessorDataTransfer(instruction));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback position.
|
||||
scheduler.unknown();
|
||||
}
|
||||
};
|
||||
|
||||
/// A brief documentation of the interface expected by @c dispatch below; will be a concept if/when this project adopts C++20.
|
||||
struct SampleScheduler {
|
||||
/// @returns @c true if the rest of the instruction should be decoded and supplied
|
||||
/// to the scheduler as defined below; @c false otherwise.
|
||||
bool should_schedule(Condition condition);
|
||||
|
||||
// Template argument:
|
||||
//
|
||||
// Flags, an opaque type which can be converted into a DataProcessingFlags, MultiplyFlags, etc,
|
||||
// by simple construction, to provide all flags that can be baked into the template parameters.
|
||||
//
|
||||
// Function argument:
|
||||
//
|
||||
// An operation-specific encapsulation of the operation code for decoding of fields that didn't
|
||||
// fit into the template parameters.
|
||||
//
|
||||
// Either or both may be omitted if unnecessary.
|
||||
template <Flags> void perform(DataProcessing);
|
||||
template <Flags> void perform(Multiply);
|
||||
template <Flags> void perform(SingleDataTransfer);
|
||||
template <Flags> void perform(BlockDataTransfer);
|
||||
template <Flags> void perform(Branch);
|
||||
template <Flags> void perform(CoprocessorRegisterTransfer);
|
||||
template <Flags> void perform(CoprocessorDataOperation);
|
||||
template <Flags> void perform(CoprocessorDataTransfer);
|
||||
|
||||
// Irregular operations.
|
||||
void software_interrupt(SoftwareInterrupt);
|
||||
void unknown();
|
||||
};
|
||||
|
||||
/// Decodes @c instruction, making an appropriate call into @c scheduler.
|
||||
///
|
||||
/// In lieu of C++20, see the sample definition of SampleScheduler above for the expected interface.
|
||||
template <Model model, typename SchedulerT> void dispatch(uint32_t instruction, SchedulerT &scheduler) {
|
||||
OperationMapper<model> mapper;
|
||||
|
||||
// Test condition.
|
||||
const auto condition = mapper.condition(instruction);
|
||||
if(!scheduler.should_schedule(condition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch body.
|
||||
Reflection::dispatch(mapper, (instruction >> FlagsStartBit) & 0xff, instruction, scheduler);
|
||||
}
|
||||
|
||||
}
|
||||
388
InstructionSets/ARM/Registers.hpp
Normal file
388
InstructionSets/ARM/Registers.hpp
Normal file
@@ -0,0 +1,388 @@
|
||||
//
|
||||
// Status.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 25/02/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "OperationMapper.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace InstructionSet::ARM {
|
||||
|
||||
namespace ConditionCode {
|
||||
|
||||
static constexpr uint32_t Negative = 1u << 31;
|
||||
static constexpr uint32_t Zero = 1u << 30;
|
||||
static constexpr uint32_t Carry = 1u << 29;
|
||||
static constexpr uint32_t Overflow = 1u << 28;
|
||||
static constexpr uint32_t IRQDisable = 1u << 27;
|
||||
static constexpr uint32_t FIQDisable = 1u << 26;
|
||||
static constexpr uint32_t Mode = (1u << 1) | (1u << 0);
|
||||
|
||||
static constexpr uint32_t Address = FIQDisable - Mode - 1;
|
||||
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
User = 0b00,
|
||||
FIQ = 0b01,
|
||||
IRQ = 0b10,
|
||||
Supervisor = 0b11,
|
||||
};
|
||||
|
||||
/// Combines the ARM registers and status flags into a single whole, given that the architecture
|
||||
/// doesn't have the same degree of separation as others.
|
||||
///
|
||||
/// The PC contained here is always taken to be **the address of the current instruction + 4**,
|
||||
/// i.e. whatever should be executed next, disregarding pipeline differences.
|
||||
///
|
||||
/// Appropriate prefetch offsets are left to other code to handle.
|
||||
/// This is to try to keep this structure independent of a specific ARM implementation.
|
||||
struct Registers {
|
||||
public:
|
||||
// Don't allow copying.
|
||||
Registers(const Registers &) = delete;
|
||||
Registers &operator =(const Registers &) = delete;
|
||||
Registers() = default;
|
||||
|
||||
/// Sets the N and Z flags according to the value of @c result.
|
||||
void set_nz(uint32_t value) {
|
||||
zero_result_ = negative_flag_ = value;
|
||||
}
|
||||
|
||||
/// Sets C if @c value is non-zero; resets it otherwise.
|
||||
void set_c(uint32_t value) {
|
||||
carry_flag_ = value;
|
||||
}
|
||||
|
||||
/// @returns @c 1 if carry is set; @c 0 otherwise.
|
||||
uint32_t c() const {
|
||||
return carry_flag_ ? 1 : 0;
|
||||
}
|
||||
|
||||
/// Sets V if the highest bit of @c value is set; resets it otherwise.
|
||||
void set_v(uint32_t value) {
|
||||
overflow_flag_ = value;
|
||||
}
|
||||
|
||||
/// @returns The current status bits, separate from the PC — mode, NVCZ and the two interrupt flags.
|
||||
uint32_t status() const {
|
||||
return
|
||||
uint32_t(mode_) |
|
||||
(negative_flag_ & ConditionCode::Negative) |
|
||||
(zero_result_ ? 0 : ConditionCode::Zero) |
|
||||
(carry_flag_ ? ConditionCode::Carry : 0) |
|
||||
((overflow_flag_ >> 3) & ConditionCode::Overflow) |
|
||||
interrupt_flags_;
|
||||
}
|
||||
|
||||
/// @returns The full PC + status bits.
|
||||
uint32_t pc_status(uint32_t offset) const {
|
||||
return
|
||||
((active_[15] + offset) & ConditionCode::Address) |
|
||||
status();
|
||||
}
|
||||
|
||||
/// Sets status bits only, subject to mode.
|
||||
void set_status(uint32_t status) {
|
||||
// ... in user mode the other flags (I, F, M1, M0) are protected from direct change
|
||||
// but in non-user modes these will also be affected, accepting copies of bits 27, 26,
|
||||
// 1 and 0 of the result respectively.
|
||||
|
||||
negative_flag_ = status;
|
||||
overflow_flag_ = status << 3;
|
||||
carry_flag_ = status & ConditionCode::Carry;
|
||||
zero_result_ = ~status & ConditionCode::Zero;
|
||||
|
||||
if(mode_ != Mode::User) {
|
||||
set_mode(Mode(status & 3));
|
||||
interrupt_flags_ = status & (ConditionCode::IRQDisable | ConditionCode::FIQDisable);
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns The current mode.
|
||||
Mode mode() const {
|
||||
return mode_;
|
||||
}
|
||||
|
||||
/// Sets a new PC.
|
||||
void set_pc(uint32_t value) {
|
||||
active_[15] = value & ConditionCode::Address;
|
||||
}
|
||||
|
||||
/// @returns The stored PC plus @c offset limited to 26 bits.
|
||||
uint32_t pc(uint32_t offset) const {
|
||||
return (active_[15] + offset) & ConditionCode::Address;
|
||||
}
|
||||
|
||||
// MARK: - Exceptions.
|
||||
|
||||
enum class Exception {
|
||||
/// Reset line went from high to low.
|
||||
Reset = 0x00,
|
||||
/// Either an undefined instruction or a coprocessor instruction for which no coprocessor was found.
|
||||
UndefinedInstruction = 0x04,
|
||||
/// Code executed a software interrupt.
|
||||
SoftwareInterrupt = 0x08,
|
||||
/// The memory subsystem indicated an abort during prefetch and that instruction has now come to the head of the queue.
|
||||
PrefetchAbort = 0x0c,
|
||||
/// The memory subsystem indicated an abort during an instruction; if it is an LDR or STR then this should be signalled
|
||||
/// before any instruction execution. If it was an LDM then loading stops upon a data abort but both an LDM and STM
|
||||
/// otherwise complete, including pointer writeback.
|
||||
DataAbort = 0x10,
|
||||
/// The first data transfer attempted within an instruction was above address 0x3ff'ffff.
|
||||
Address = 0x14,
|
||||
/// The IRQ line was low at the end of an instruction and ConditionCode::IRQDisable was not set.
|
||||
IRQ = 0x18,
|
||||
/// The FIQ went low at least one cycle ago and ConditionCode::FIQDisable was not set.
|
||||
FIQ = 0x1c,
|
||||
};
|
||||
static constexpr uint32_t pc_offset_during(Exception exception) {
|
||||
// The below is somewhat convoluted by the assumed execution model:
|
||||
//
|
||||
// * exceptions occuring during execution of an instruction are taken
|
||||
// to occur after R15 has already been incremented by 4; but
|
||||
// * exceptions occurring instead of execution of an instruction are
|
||||
// taken to occur with R15 pointing to an instruction that hasn't begun.
|
||||
//
|
||||
// i.e. in net R15 always refers to the next instruction
|
||||
// that has not yet started.
|
||||
switch(exception) {
|
||||
// "To return normally from FIQ use SUBS PC, R14_fiq, #4".
|
||||
case Exception::FIQ: return 4;
|
||||
|
||||
// "To return normally from IRQ use SUBS PC, R14_irq, #4".
|
||||
case Exception::IRQ: return 4;
|
||||
|
||||
// "If a return is required from [address exception traps], use
|
||||
// SUBS PC, R14_svc, #4. This will return to the instruction after
|
||||
// the one causing the trap".
|
||||
case Exception::Address: return 4;
|
||||
|
||||
// "A Data Abort requires [work before a return], the return being
|
||||
// done by SUBS PC, R14_svc, #8" (and returning to the instruction
|
||||
// that aborted).
|
||||
case Exception::DataAbort: return 4;
|
||||
|
||||
// "To continue after a Prefetch Abort use SUBS PC, R14_svc #4.
|
||||
// This will attempt to re-execute the aborting instruction."
|
||||
case Exception::PrefetchAbort: return 4;
|
||||
|
||||
// "To return from a SWI, use MOVS PC, R14_svc. This returns to the instruction
|
||||
// following the SWI".
|
||||
case Exception::SoftwareInterrupt: return 0;
|
||||
|
||||
// "To return from [an undefined instruction trap] use MOVS PC, R14_svc.
|
||||
// This returns to the instruction following the undefined instruction".
|
||||
case Exception::UndefinedInstruction: return 0;
|
||||
|
||||
// Unspecified; a guess.
|
||||
case Exception::Reset: return 0;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
/// Updates the program counter, interupt flags and link register if applicable to begin @c exception.
|
||||
template <Exception type>
|
||||
void exception() {
|
||||
const auto r14 = pc_status(pc_offset_during(type));
|
||||
switch(type) {
|
||||
case Exception::IRQ: set_mode(Mode::IRQ); break;
|
||||
case Exception::FIQ: set_mode(Mode::FIQ); break;
|
||||
default: set_mode(Mode::Supervisor); break;
|
||||
}
|
||||
active_[14] = r14;
|
||||
|
||||
interrupt_flags_ |= ConditionCode::IRQDisable;
|
||||
if constexpr (type == Exception::Reset || type == Exception::FIQ) {
|
||||
interrupt_flags_ |= ConditionCode::FIQDisable;
|
||||
}
|
||||
set_pc(uint32_t(type));
|
||||
}
|
||||
|
||||
/// Returns @c true if: (i) the exception type is IRQ or FIQ; and (ii) the processor is currently accepting such interrupts.
|
||||
/// Otherwise returns @c false.
|
||||
template <Exception type>
|
||||
bool would_interrupt() {
|
||||
switch(type) {
|
||||
case Exception::IRQ:
|
||||
if(interrupt_flags_ & ConditionCode::IRQDisable) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Exception::FIQ:
|
||||
if(interrupt_flags_ & ConditionCode::FIQDisable) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - Condition tests.
|
||||
|
||||
/// @returns @c true if @c condition tests as true; @c false otherwise.
|
||||
bool test(Condition condition) const {
|
||||
const auto ne = [&]() -> bool {
|
||||
return zero_result_;
|
||||
};
|
||||
const auto cs = [&]() -> bool {
|
||||
return carry_flag_;
|
||||
};
|
||||
const auto mi = [&]() -> bool {
|
||||
return negative_flag_ & ConditionCode::Negative;
|
||||
};
|
||||
const auto vs = [&]() -> bool {
|
||||
return overflow_flag_ & ConditionCode::Negative;
|
||||
};
|
||||
const auto hi = [&]() -> bool {
|
||||
return carry_flag_ && zero_result_;
|
||||
};
|
||||
const auto lt = [&]() -> bool {
|
||||
return (negative_flag_ ^ overflow_flag_) & ConditionCode::Negative;
|
||||
};
|
||||
const auto le = [&]() -> bool {
|
||||
return !zero_result_ || lt();
|
||||
};
|
||||
|
||||
switch(condition) {
|
||||
case Condition::EQ: return !ne();
|
||||
case Condition::NE: return ne();
|
||||
case Condition::CS: return cs();
|
||||
case Condition::CC: return !cs();
|
||||
case Condition::MI: return mi();
|
||||
case Condition::PL: return !mi();
|
||||
case Condition::VS: return vs();
|
||||
case Condition::VC: return !vs();
|
||||
|
||||
case Condition::HI: return hi();
|
||||
case Condition::LS: return !hi();
|
||||
case Condition::GE: return !lt();
|
||||
case Condition::LT: return lt();
|
||||
case Condition::GT: return !le();
|
||||
case Condition::LE: return le();
|
||||
|
||||
case Condition::AL: return true;
|
||||
case Condition::NV: return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Sets current execution mode.
|
||||
void set_mode(Mode target_mode) {
|
||||
if(mode_ == target_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For outgoing modes other than FIQ, only save the final two registers for now;
|
||||
// if the incoming mode is FIQ then the other five will be saved in the next switch.
|
||||
// For FIQ, save all seven up front.
|
||||
switch(mode_) {
|
||||
// FIQ outgoing: save R8 to R14.
|
||||
case Mode::FIQ:
|
||||
std::copy(active_.begin() + 8, active_.begin() + 15, fiq_registers_.begin());
|
||||
break;
|
||||
|
||||
// Non-FIQ outgoing: save R13 and R14. If saving to the user registers,
|
||||
// use only the final two slots.
|
||||
case Mode::User:
|
||||
std::copy(active_.begin() + 13, active_.begin() + 15, user_registers_.begin() + 5);
|
||||
break;
|
||||
case Mode::Supervisor:
|
||||
std::copy(active_.begin() + 13, active_.begin() + 15, supervisor_registers_.begin());
|
||||
break;
|
||||
case Mode::IRQ:
|
||||
std::copy(active_.begin() + 13, active_.begin() + 15, irq_registers_.begin());
|
||||
break;
|
||||
}
|
||||
|
||||
// For all modes except FIQ: restore the final two registers to their appropriate values.
|
||||
// For FIQ: save an additional five, then overwrite seven.
|
||||
switch(target_mode) {
|
||||
case Mode::FIQ:
|
||||
// FIQ is incoming, so save registers 8 to 12 to the first five slots of the user registers.
|
||||
std::copy(active_.begin() + 8, active_.begin() + 13, user_registers_.begin());
|
||||
|
||||
// Replace R8 to R14.
|
||||
std::copy(fiq_registers_.begin(), fiq_registers_.end(), active_.begin() + 8);
|
||||
break;
|
||||
case Mode::User:
|
||||
std::copy(user_registers_.begin() + 5, user_registers_.end(), active_.begin() + 13);
|
||||
break;
|
||||
case Mode::Supervisor:
|
||||
std::copy(supervisor_registers_.begin(), supervisor_registers_.end(), active_.begin() + 13);
|
||||
break;
|
||||
case Mode::IRQ:
|
||||
std::copy(irq_registers_.begin(), irq_registers_.end(), active_.begin() + 13);
|
||||
break;
|
||||
}
|
||||
|
||||
// If FIQ is outgoing then there's another five registers to restore.
|
||||
if(mode_ == Mode::FIQ) {
|
||||
std::copy(user_registers_.begin(), user_registers_.begin() + 5, active_.begin() + 8);
|
||||
}
|
||||
|
||||
mode_ = target_mode;
|
||||
}
|
||||
|
||||
uint32_t &operator[](uint32_t offset) {
|
||||
return active_[static_cast<size_t>(offset)];
|
||||
}
|
||||
|
||||
uint32_t operator[](uint32_t offset) const {
|
||||
return active_[static_cast<size_t>(offset)];
|
||||
}
|
||||
|
||||
/// @returns A reference to the register at @c offset. If @c force_user_mode is true,
|
||||
/// this will the the user-mode register. Otherwise it'll be that for the current mode. These references
|
||||
/// are guaranteed to remain valid only until the next mode change.
|
||||
uint32_t ®(bool force_user_mode, uint32_t offset) {
|
||||
switch(mode_) {
|
||||
default:
|
||||
case Mode::User: return active_[offset];
|
||||
|
||||
case Mode::Supervisor:
|
||||
case Mode::IRQ:
|
||||
if(force_user_mode && (offset == 13 || offset == 14)) {
|
||||
return user_registers_[offset - 8];
|
||||
}
|
||||
return active_[offset];
|
||||
|
||||
case Mode::FIQ:
|
||||
if(force_user_mode && (offset >= 8 && offset < 15)) {
|
||||
return user_registers_[offset - 8];
|
||||
}
|
||||
return active_[offset];
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Mode mode_ = Mode::Supervisor;
|
||||
|
||||
uint32_t zero_result_ = 1;
|
||||
uint32_t negative_flag_ = 0;
|
||||
uint32_t interrupt_flags_ = ConditionCode::IRQDisable | ConditionCode::FIQDisable;
|
||||
uint32_t carry_flag_ = 0;
|
||||
uint32_t overflow_flag_ = 0;
|
||||
|
||||
// Various shadow registers.
|
||||
std::array<uint32_t, 7> user_registers_{};
|
||||
std::array<uint32_t, 7> fiq_registers_{};
|
||||
std::array<uint32_t, 2> irq_registers_{};
|
||||
std::array<uint32_t, 2> supervisor_registers_{};
|
||||
|
||||
// The active register set.
|
||||
std::array<uint32_t, 16> active_{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -222,7 +222,7 @@ struct Instruction {
|
||||
|
||||
Instruction(Operation operation, AddressingMode addressing_mode, uint8_t opcode) : operation(operation), addressing_mode(addressing_mode), opcode(opcode) {}
|
||||
Instruction(uint8_t opcode) : opcode(opcode) {}
|
||||
Instruction() {}
|
||||
Instruction() = default;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
||||
@@ -478,7 +478,7 @@ class Preinstruction {
|
||||
static constexpr int SizeShift = 0;
|
||||
};
|
||||
|
||||
Preinstruction() {}
|
||||
Preinstruction() = default;
|
||||
|
||||
/// Produces a string description of this instruction; if @c opcode
|
||||
/// is supplied then any quick fields in this instruction will be decoded;
|
||||
|
||||
@@ -1362,9 +1362,9 @@ struct Instruction {
|
||||
bool is_supervisor = false;
|
||||
uint32_t opcode = 0;
|
||||
|
||||
Instruction() noexcept {}
|
||||
Instruction(uint32_t opcode) noexcept : opcode(opcode) {}
|
||||
Instruction(Operation operation, uint32_t opcode, bool is_supervisor = false) noexcept : operation(operation), is_supervisor(is_supervisor), opcode(opcode) {}
|
||||
constexpr Instruction() noexcept = default;
|
||||
constexpr Instruction(uint32_t opcode) noexcept : opcode(opcode) {}
|
||||
constexpr Instruction(Operation operation, uint32_t opcode, bool is_supervisor = false) noexcept : operation(operation), is_supervisor(is_supervisor), opcode(opcode) {}
|
||||
|
||||
// Instruction fields are decoded below; naming is a compromise between
|
||||
// Motorola's documentation and IBM's.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Instruction Sets
|
||||
# Instruction Sets
|
||||
|
||||
Code in here provides the means to disassemble, and to execute code for certain instruction sets.
|
||||
|
||||
It **does not seek to emulate specific processors** other than in terms of implementing their instruction sets. So:
|
||||
|
||||
* it doesn't involve itself in the actual bus signalling of real processors; and
|
||||
* instruction-level timing (e.g. total cycle counts) may be unimplemented, and is likely to be incomplete.
|
||||
|
||||
@@ -13,6 +14,7 @@ This part of CLK is intended primarily to provide disassembly services for stati
|
||||
A decoder extracts fully-decoded instructions from a data stream for its associated architecture.
|
||||
|
||||
The meaning of 'fully-decoded' is flexible but it means that a caller can easily discern at least:
|
||||
|
||||
* the operation in use;
|
||||
* its addressing mode; and
|
||||
* relevant registers.
|
||||
@@ -20,6 +22,7 @@ The meaning of 'fully-decoded' is flexible but it means that a caller can easily
|
||||
It may be assumed that callers will have access to the original data stream for immediate values, if it is sensible to do so.
|
||||
|
||||
In deciding what to expose, what to store ahead of time and what to obtain just-in-time a decoder should have an eye on two principal consumers:
|
||||
|
||||
1. disassemblers; and
|
||||
2. instruction executors.
|
||||
|
||||
@@ -50,6 +53,7 @@ A sample interface:
|
||||
std::pair<int, Instruction> decode(word_type *stream, size_t length) { ... }
|
||||
|
||||
In this sample the returned pair provides an `int` size that is one of:
|
||||
|
||||
* a positive number, indicating a completed decoding that consumed that many `word_type`s; or
|
||||
* a negative number, indicating the [negatived] minimum number of `word_type`s that the caller should try to get hold of before calling `decode` again.
|
||||
|
||||
@@ -58,6 +62,7 @@ A caller is permitted to react in any way it prefers to negative numbers; they'r
|
||||
## Parsers
|
||||
|
||||
A parser sits one level above a decoder; it is handed:
|
||||
|
||||
* a start address;
|
||||
* a closing bound; and
|
||||
* a target.
|
||||
@@ -65,6 +70,7 @@ A parser sits one level above a decoder; it is handed:
|
||||
It is responsible for parsing the instruction stream from the start address up to and not beyond the closing bound, and no further than any unconditional branches.
|
||||
|
||||
It should post to the target:
|
||||
|
||||
* any instructions fully decoded;
|
||||
* any conditional branch destinations encountered;
|
||||
* any immediately-knowable accessed addresses; and
|
||||
@@ -75,6 +81,7 @@ So a parser has the same two primary potential recipients as a decoder: diassemb
|
||||
## Executors
|
||||
|
||||
An executor is responsible for only one thing:
|
||||
|
||||
* mapping from decoded instructions to objects that can perform those instructions.
|
||||
|
||||
An executor is assumed to bundle all the things that go into instruction set execution: processor state and memory, alongside a parser.
|
||||
|
||||
@@ -564,7 +564,7 @@ constexpr Operation rep_operation(Operation operation, Repetition repetition) {
|
||||
/// It cannot natively describe a base of ::None.
|
||||
class ScaleIndexBase {
|
||||
public:
|
||||
constexpr ScaleIndexBase() noexcept {}
|
||||
constexpr ScaleIndexBase() noexcept = default;
|
||||
constexpr ScaleIndexBase(uint8_t sib) noexcept : sib_(sib) {}
|
||||
constexpr ScaleIndexBase(int scale, Source index, Source base) noexcept :
|
||||
sib_(uint8_t(
|
||||
@@ -703,7 +703,7 @@ template<bool is_32bit> class Instruction {
|
||||
using ImmediateT = typename std::conditional<is_32bit, uint32_t, uint16_t>::type;
|
||||
using AddressT = ImmediateT;
|
||||
|
||||
constexpr Instruction() noexcept {}
|
||||
constexpr Instruction() noexcept = default;
|
||||
constexpr Instruction(Operation operation) noexcept :
|
||||
Instruction(operation, Source::None, Source::None, ScaleIndexBase(), false, AddressSize::b16, Source::None, DataSize::None, 0, 0) {}
|
||||
constexpr Instruction(
|
||||
|
||||
636
Machines/Acorn/Archimedes/Archimedes.cpp
Normal file
636
Machines/Acorn/Archimedes/Archimedes.cpp
Normal file
@@ -0,0 +1,636 @@
|
||||
//
|
||||
// Archimedes.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Archimedes.hpp"
|
||||
|
||||
#include "HalfDuplexSerial.hpp"
|
||||
#include "InputOutputController.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "KeyboardMapper.hpp"
|
||||
#include "MemoryController.hpp"
|
||||
#include "Sound.hpp"
|
||||
|
||||
#include "../../AudioProducer.hpp"
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
#include "../../MediaTarget.hpp"
|
||||
#include "../../MouseMachine.hpp"
|
||||
#include "../../ScanProducer.hpp"
|
||||
#include "../../TimedMachine.hpp"
|
||||
|
||||
#include "../../../Activity/Source.hpp"
|
||||
|
||||
#include "../../../InstructionSets/ARM/Disassembler.hpp"
|
||||
#include "../../../InstructionSets/ARM/Executor.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
#include "../../../Components/I2C/I2C.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
#ifndef NDEBUG
|
||||
namespace {
|
||||
Log::Logger<Log::Source::Archimedes> logger;
|
||||
}
|
||||
|
||||
template <InstructionSet::ARM::Model model, typename Executor>
|
||||
struct HackyDebugger {
|
||||
void notify(uint32_t address, uint32_t instruction, Executor &executor) {
|
||||
pc_history[pc_history_ptr] = address;
|
||||
pc_history_ptr = (pc_history_ptr + 1) % pc_history.size();
|
||||
|
||||
// if(
|
||||
// executor_.pc() > 0x038021d0 &&
|
||||
// last_r1 != executor_.registers()[1]
|
||||
// ||
|
||||
// (
|
||||
// last_link != executor_.registers()[14] ||
|
||||
// last_r0 != executor_.registers()[0] ||
|
||||
// last_r10 != executor_.registers()[10] ||
|
||||
// last_r1 != executor_.registers()[1]
|
||||
// )
|
||||
// ) {
|
||||
// logger.info().append("%08x modified R14 to %08x; R0 to %08x; R10 to %08x; R1 to %08x",
|
||||
// last_pc,
|
||||
// executor_.registers()[14],
|
||||
// executor_.registers()[0],
|
||||
// executor_.registers()[10],
|
||||
// executor_.registers()[1]
|
||||
// );
|
||||
// logger.info().append("%08x modified R1 to %08x",
|
||||
// last_pc,
|
||||
// executor_.registers()[1]
|
||||
// );
|
||||
// last_link = executor_.registers()[14];
|
||||
// last_r0 = executor_.registers()[0];
|
||||
// last_r10 = executor_.registers()[10];
|
||||
// last_r1 = executor_.registers()[1];
|
||||
// }
|
||||
|
||||
// if(instruction == 0xe8fd7fff) {
|
||||
// printf("At %08x [%d]; after last PC %08x and %zu ago was %08x\n",
|
||||
// address,
|
||||
// instr_count,
|
||||
// pc_history[(pc_history_ptr - 2 + pc_history.size()) % pc_history.size()],
|
||||
// pc_history.size(),
|
||||
// pc_history[pc_history_ptr]);
|
||||
// }
|
||||
// last_r9 = executor_.registers()[9];
|
||||
|
||||
// log |= address == 0x038031c4;
|
||||
// log |= instr_count == 53552731 - 30;
|
||||
// log &= executor_.pc() != 0x000000a0;
|
||||
|
||||
// log = (executor_.pc() == 0x038162afc) || (executor_.pc() == 0x03824b00);
|
||||
// log |= instruction & ;
|
||||
|
||||
// The following has the effect of logging all taken SWIs and their return codes.
|
||||
/* if(
|
||||
(instruction & 0x0f00'0000) == 0x0f00'0000 &&
|
||||
executor.registers().test(InstructionSet::ARM::Condition(instruction >> 28))
|
||||
) {
|
||||
if(instruction & 0x2'0000) {
|
||||
swis.emplace_back();
|
||||
swis.back().count = swi_count++;
|
||||
swis.back().opcode = instruction;
|
||||
swis.back().address = executor.pc();
|
||||
swis.back().return_address = executor.registers().pc(4);
|
||||
for(int c = 0; c < 10; c++) swis.back().regs[c] = executor.registers()[uint32_t(c)];
|
||||
|
||||
// Possibly capture more detail.
|
||||
//
|
||||
// Cf. http://productsdb.riscos.com/support/developers/prm_index/numswilist.html
|
||||
uint32_t pointer = 0;
|
||||
switch(instruction & 0xfd'ffff) {
|
||||
case 0x41501:
|
||||
swis.back().swi_name = "MessageTrans_OpenFile";
|
||||
|
||||
// R0: pointer to file descriptor; R1: pointer to filename; R2: pointer to hold file data.
|
||||
// (R0 and R1 are in the RMA if R2 = 0)
|
||||
pointer = executor.registers()[1];
|
||||
break;
|
||||
case 0x41502:
|
||||
swis.back().swi_name = "MessageTrans_Lookup";
|
||||
break;
|
||||
case 0x41506:
|
||||
swis.back().swi_name = "MessageTrans_ErrorLookup";
|
||||
break;
|
||||
|
||||
case 0x4028a:
|
||||
swis.back().swi_name = "Podule_EnumerateChunksWithInfo";
|
||||
break;
|
||||
|
||||
case 0x4000a:
|
||||
swis.back().swi_name = "Econet_ReadLocalStationAndNet";
|
||||
break;
|
||||
case 0x4000e:
|
||||
swis.back().swi_name = "Econet_SetProtection";
|
||||
break;
|
||||
case 0x40015:
|
||||
swis.back().swi_name = "Econet_ClaimPort";
|
||||
break;
|
||||
|
||||
case 0x40541:
|
||||
swis.back().swi_name = "FileCore_Create";
|
||||
break;
|
||||
|
||||
case 0x80156:
|
||||
case 0x8015b:
|
||||
swis.back().swi_name = "PDriver_MiscOpForDriver";
|
||||
break;
|
||||
|
||||
case 0x05:
|
||||
swis.back().swi_name = "OS_CLI";
|
||||
pointer = executor.registers()[0];
|
||||
break;
|
||||
case 0x0d:
|
||||
swis.back().swi_name = "OS_Find";
|
||||
if(executor.registers()[0] >= 0x40) {
|
||||
pointer = executor.registers()[1];
|
||||
}
|
||||
break;
|
||||
case 0x1d:
|
||||
swis.back().swi_name = "OS_Heap";
|
||||
break;
|
||||
case 0x1e:
|
||||
swis.back().swi_name = "OS_Module";
|
||||
break;
|
||||
|
||||
case 0x20:
|
||||
swis.back().swi_name = "OS_Release";
|
||||
break;
|
||||
case 0x21:
|
||||
swis.back().swi_name = "OS_ReadUnsigned";
|
||||
break;
|
||||
case 0x23:
|
||||
swis.back().swi_name = "OS_ReadVarVal";
|
||||
|
||||
// R0: pointer to variable name.
|
||||
pointer = executor.registers()[0];
|
||||
break;
|
||||
case 0x24:
|
||||
swis.back().swi_name = "OS_SetVarVal";
|
||||
|
||||
// R0: pointer to variable name.
|
||||
pointer = executor.registers()[0];
|
||||
break;
|
||||
case 0x26:
|
||||
swis.back().swi_name = "OS_GSRead";
|
||||
break;
|
||||
case 0x27:
|
||||
swis.back().swi_name = "OS_GSTrans";
|
||||
pointer = executor.registers()[0];
|
||||
break;
|
||||
case 0x29:
|
||||
swis.back().swi_name = "OS_FSControl";
|
||||
break;
|
||||
case 0x2a:
|
||||
swis.back().swi_name = "OS_ChangeDynamicArea";
|
||||
break;
|
||||
|
||||
case 0x4c:
|
||||
swis.back().swi_name = "OS_ReleaseDeviceVector";
|
||||
break;
|
||||
|
||||
case 0x43057:
|
||||
swis.back().swi_name = "Territory_LowerCaseTable";
|
||||
break;
|
||||
case 0x43058:
|
||||
swis.back().swi_name = "Territory_UpperCaseTable";
|
||||
break;
|
||||
|
||||
case 0x42fc0:
|
||||
swis.back().swi_name = "Portable_Speed";
|
||||
break;
|
||||
case 0x42fc1:
|
||||
swis.back().swi_name = "Portable_Control";
|
||||
break;
|
||||
}
|
||||
|
||||
if(pointer) {
|
||||
while(true) {
|
||||
uint8_t next;
|
||||
executor.bus.template read<uint8_t>(pointer, next, InstructionSet::ARM::Mode::Supervisor, false);
|
||||
++pointer;
|
||||
|
||||
if(next < 32) break;
|
||||
swis.back().value_name.push_back(static_cast<char>(next));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(executor.registers().pc_status(0) & InstructionSet::ARM::ConditionCode::Overflow) {
|
||||
logger.error().append("SWI called with V set");
|
||||
}
|
||||
}
|
||||
if(!swis.empty() && executor.pc() == swis.back().return_address) {
|
||||
// Overflow set => SWI failure.
|
||||
auto &back = swis.back();
|
||||
if(executor.registers().pc_status(0) & InstructionSet::ARM::ConditionCode::Overflow) {
|
||||
auto info = logger.info();
|
||||
|
||||
info.append("[%d] Failed swi ", back.count);
|
||||
if(back.swi_name.empty()) {
|
||||
info.append("&%x", back.opcode & 0xfd'ffff);
|
||||
} else {
|
||||
info.append("%s", back.swi_name.c_str());
|
||||
}
|
||||
|
||||
if(!back.value_name.empty()) {
|
||||
info.append(" %s", back.value_name.c_str());
|
||||
}
|
||||
|
||||
info.append(" @ %08x ", back.address);
|
||||
for(uint32_t c = 0; c < 10; c++) {
|
||||
info.append("r%d:%08x ", c, back.regs[c]);
|
||||
}
|
||||
}
|
||||
|
||||
swis.pop_back();
|
||||
}*/
|
||||
|
||||
if(log) {
|
||||
InstructionSet::ARM::Disassembler<model> disassembler;
|
||||
InstructionSet::ARM::dispatch<model>(instruction, disassembler);
|
||||
|
||||
auto info = logger.info();
|
||||
info.append("[%d] %08x: %08x\t\t%s\t prior:[",
|
||||
instr_count,
|
||||
executor.pc(),
|
||||
instruction,
|
||||
disassembler.last().to_string(executor.pc()).c_str());
|
||||
for(uint32_t c = 0; c < 15; c++) {
|
||||
info.append("r%d:%08x ", c, executor.registers()[c]);
|
||||
}
|
||||
info.append("]");
|
||||
}
|
||||
// opcodes.insert(instruction);
|
||||
// if(accumulate) {
|
||||
// int c = 0;
|
||||
// for(auto instr : opcodes) {
|
||||
// printf("0x%08x, ", instr);
|
||||
// ++c;
|
||||
// if(!(c&15)) printf("\n");
|
||||
// }
|
||||
// accumulate = false;
|
||||
// }
|
||||
|
||||
++instr_count;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<uint32_t, 75> pc_history;
|
||||
std::size_t pc_history_ptr = 0;
|
||||
uint32_t instr_count = 0;
|
||||
uint32_t swi_count = 0;
|
||||
|
||||
struct SWICall {
|
||||
uint32_t count;
|
||||
uint32_t opcode;
|
||||
uint32_t address;
|
||||
uint32_t regs[10];
|
||||
uint32_t return_address;
|
||||
std::string value_name;
|
||||
std::string swi_name;
|
||||
};
|
||||
std::vector<SWICall> swis;
|
||||
uint32_t last_pc = 0;
|
||||
// uint32_t last_r9 = 0;
|
||||
bool log = false;
|
||||
bool accumulate = true;
|
||||
|
||||
std::set<uint32_t> opcodes;
|
||||
};
|
||||
#else
|
||||
template <InstructionSet::ARM::Model model, typename Executor>
|
||||
struct HackyDebugger {
|
||||
void notify(uint32_t, uint32_t, Executor &) {}
|
||||
};
|
||||
#endif
|
||||
|
||||
class ConcreteMachine:
|
||||
public Machine,
|
||||
public MachineTypes::AudioProducer,
|
||||
public MachineTypes::MappedKeyboardMachine,
|
||||
public MachineTypes::MediaTarget,
|
||||
public MachineTypes::MouseMachine,
|
||||
public MachineTypes::TimedMachine,
|
||||
public MachineTypes::ScanProducer,
|
||||
public Activity::Source
|
||||
{
|
||||
private:
|
||||
// TODO: pick a sensible clock rate; this is just code for '24 MIPS, please'.
|
||||
static constexpr int ClockRate = 24'000'000;
|
||||
|
||||
// Runs for 24 cycles, distributing calls to the various ticking subsystems
|
||||
// 'correctly' (i.e. correctly for the approximation in use).
|
||||
//
|
||||
// The implementation of this is coupled to the ClockRate above, hence its
|
||||
// appearance here.
|
||||
template <int video_divider, bool original_speed>
|
||||
void macro_tick() {
|
||||
macro_counter_ -= 24;
|
||||
|
||||
// This is a 24-cycle window, so at 24Mhz macro_tick() is called at 1Mhz.
|
||||
// Hence, required ticks are:
|
||||
//
|
||||
// * CPU: 24;
|
||||
// * video: 24 / video_divider;
|
||||
// * floppy: 8;
|
||||
// * timers: 2;
|
||||
// * sound: 1.
|
||||
|
||||
tick_cpu_video<0, video_divider, original_speed>(); tick_cpu_video<1, video_divider, original_speed>();
|
||||
tick_cpu_video<2, video_divider, original_speed>(); tick_floppy();
|
||||
tick_cpu_video<3, video_divider, original_speed>(); tick_cpu_video<4, video_divider, original_speed>();
|
||||
tick_cpu_video<5, video_divider, original_speed>(); tick_floppy();
|
||||
tick_cpu_video<6, video_divider, original_speed>(); tick_cpu_video<7, video_divider, original_speed>();
|
||||
tick_cpu_video<8, video_divider, original_speed>(); tick_floppy();
|
||||
tick_cpu_video<9, video_divider, original_speed>(); tick_cpu_video<10, video_divider, original_speed>();
|
||||
tick_cpu_video<11, video_divider, original_speed>(); tick_floppy();
|
||||
tick_timers();
|
||||
|
||||
tick_cpu_video<12, video_divider, original_speed>(); tick_cpu_video<13, video_divider, original_speed>();
|
||||
tick_cpu_video<14, video_divider, original_speed>(); tick_floppy();
|
||||
tick_cpu_video<15, video_divider, original_speed>(); tick_cpu_video<16, video_divider, original_speed>();
|
||||
tick_cpu_video<17, video_divider, original_speed>(); tick_floppy();
|
||||
tick_cpu_video<18, video_divider, original_speed>(); tick_cpu_video<19, video_divider, original_speed>();
|
||||
tick_cpu_video<20, video_divider, original_speed>(); tick_floppy();
|
||||
tick_cpu_video<21, video_divider, original_speed>(); tick_cpu_video<22, video_divider, original_speed>();
|
||||
tick_cpu_video<23, video_divider, original_speed>(); tick_floppy();
|
||||
tick_timers();
|
||||
tick_sound();
|
||||
}
|
||||
int macro_counter_ = 0;
|
||||
|
||||
template <int offset, int video_divider, bool original_speed>
|
||||
void tick_cpu_video() {
|
||||
if constexpr (!(offset % video_divider)) {
|
||||
tick_video();
|
||||
}
|
||||
|
||||
// Debug mode: run CPU a lot slower. Actually at close to original advertised MIPS speed.
|
||||
if constexpr (original_speed && (offset & 7)) return;
|
||||
if constexpr (offset & 1) return;
|
||||
tick_cpu();
|
||||
}
|
||||
|
||||
public:
|
||||
ConcreteMachine(
|
||||
const Analyser::Static::Target &target,
|
||||
const ROMMachine::ROMFetcher &rom_fetcher
|
||||
) : executor_(*this, *this, *this) {
|
||||
set_clock_rate(ClockRate);
|
||||
|
||||
constexpr ROM::Name risc_os = ROM::Name::AcornRISCOS311;
|
||||
ROM::Request request(risc_os);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
executor_.bus.set_rom(roms.find(risc_os)->second);
|
||||
insert_media(target.media);
|
||||
|
||||
fill_pipeline(0);
|
||||
}
|
||||
|
||||
void update_interrupts() {
|
||||
using Exception = InstructionSet::ARM::Registers::Exception;
|
||||
|
||||
const int requests = executor_.bus.interrupt_mask();
|
||||
if((requests & InterruptRequests::FIQ) && executor_.registers().would_interrupt<Exception::FIQ>()) {
|
||||
pipeline_.reschedule(Pipeline::SWISubversion::FIQ);
|
||||
return;
|
||||
}
|
||||
if((requests & InterruptRequests::IRQ) && executor_.registers().would_interrupt<Exception::IRQ>()) {
|
||||
pipeline_.reschedule(Pipeline::SWISubversion::IRQ);
|
||||
}
|
||||
}
|
||||
|
||||
void did_set_status() {
|
||||
// This might have been a change of mode, so...
|
||||
trans_ = executor_.registers().mode() == InstructionSet::ARM::Mode::User;
|
||||
fill_pipeline(executor_.pc());
|
||||
update_interrupts();
|
||||
}
|
||||
|
||||
void did_set_pc() {
|
||||
fill_pipeline(executor_.pc());
|
||||
}
|
||||
|
||||
bool should_swi(uint32_t) {
|
||||
using Exception = InstructionSet::ARM::Registers::Exception;
|
||||
using SWISubversion = Pipeline::SWISubversion;
|
||||
|
||||
switch(pipeline_.swi_subversion()) {
|
||||
case Pipeline::SWISubversion::None:
|
||||
return true;
|
||||
|
||||
case SWISubversion::DataAbort:
|
||||
// executor_.set_pc(executor_.pc() - 4);
|
||||
executor_.registers().exception<Exception::DataAbort>();
|
||||
break;
|
||||
|
||||
// FIQ and IRQ decrement the PC because their apperance in the pipeline causes
|
||||
// it to look as though they were fetched, but they weren't.
|
||||
case SWISubversion::FIQ:
|
||||
executor_.set_pc(executor_.pc() - 4);
|
||||
executor_.registers().exception<Exception::FIQ>();
|
||||
break;
|
||||
case SWISubversion::IRQ:
|
||||
executor_.set_pc(executor_.pc() - 4);
|
||||
executor_.registers().exception<Exception::IRQ>();
|
||||
break;
|
||||
}
|
||||
|
||||
did_set_pc();
|
||||
return false;
|
||||
}
|
||||
|
||||
void update_clock_rates() {
|
||||
video_divider_ = executor_.bus.video().clock_divider();
|
||||
}
|
||||
|
||||
private:
|
||||
// MARK: - ScanProducer.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
executor_.bus.video().crt().set_scan_target(scan_target);
|
||||
}
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const override {
|
||||
return executor_.bus.video().crt().get_scaled_scan_status() * video_divider_;
|
||||
}
|
||||
|
||||
// MARK: - TimedMachine.
|
||||
int video_divider_ = 1;
|
||||
void run_for(Cycles cycles) override {
|
||||
#ifndef NDEBUG
|
||||
// Debug mode: always run 'slowly' because that's less of a burden, and
|
||||
// because it allows me to peer at problems with greater leisure.
|
||||
const bool use_original_speed = true;
|
||||
#else
|
||||
// As a first, blunt implementation: try to model something close
|
||||
// to original speed if there have been 10 frame rate overages in total.
|
||||
const bool use_original_speed = executor_.bus.video().frame_rate_overages() > 10;
|
||||
#endif
|
||||
|
||||
if(use_original_speed) run_for<true>(cycles);
|
||||
else run_for<false>(cycles);
|
||||
}
|
||||
|
||||
template <bool original_speed>
|
||||
void run_for(Cycles cycles) {
|
||||
macro_counter_ += cycles.as<int>();
|
||||
|
||||
while(macro_counter_ > 0) {
|
||||
switch(video_divider_) {
|
||||
default: macro_tick<2, original_speed>(); break;
|
||||
case 3: macro_tick<3, original_speed>(); break;
|
||||
case 4: macro_tick<4, original_speed>(); break;
|
||||
case 6: macro_tick<6, original_speed>(); break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void tick_cpu() {
|
||||
const uint32_t instruction = advance_pipeline(executor_.pc() + 8);
|
||||
debugger_.notify(executor_.pc(), instruction, executor_);
|
||||
InstructionSet::ARM::execute(instruction, executor_);
|
||||
}
|
||||
|
||||
void tick_timers() { executor_.bus.tick_timers(); }
|
||||
void tick_sound() { executor_.bus.sound().tick(); }
|
||||
void tick_video() { executor_.bus.video().tick(); }
|
||||
void tick_floppy() { executor_.bus.tick_floppy(); }
|
||||
|
||||
// MARK: - MediaTarget
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
size_t c = 0;
|
||||
for(auto &disk : media.disks) {
|
||||
executor_.bus.set_disk(disk, c);
|
||||
c++;
|
||||
if(c == 4) break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - AudioProducer
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return executor_.bus.speaker();
|
||||
}
|
||||
|
||||
// MARK: - Activity::Source.
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
executor_.bus.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
// MARK: - MappedKeyboardMachine.
|
||||
MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
Archimedes::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) override {
|
||||
const int row = Archimedes::KeyboardMapper::row(key);
|
||||
const int column = Archimedes::KeyboardMapper::column(key);
|
||||
executor_.bus.keyboard().set_key_state(row, column, is_pressed);
|
||||
}
|
||||
|
||||
// MARK: - MouseMachine.
|
||||
Inputs::Mouse &get_mouse() override {
|
||||
return executor_.bus.keyboard().mouse();
|
||||
}
|
||||
|
||||
// MARK: - ARM execution.
|
||||
static constexpr auto arm_model = InstructionSet::ARM::Model::ARMv2;
|
||||
using Executor = InstructionSet::ARM::Executor<arm_model, MemoryController<ConcreteMachine, ConcreteMachine>, ConcreteMachine>;
|
||||
Executor executor_;
|
||||
bool trans_ = false;
|
||||
|
||||
void fill_pipeline(uint32_t pc) {
|
||||
if(pipeline_.interrupt_next()) return;
|
||||
advance_pipeline(pc);
|
||||
advance_pipeline(pc + 4);
|
||||
}
|
||||
|
||||
uint32_t advance_pipeline(uint32_t pc) {
|
||||
uint32_t instruction = 0; // Value should never be used; this avoids a spurious GCC warning.
|
||||
const bool did_read = executor_.bus.read(pc, instruction, trans_);
|
||||
return pipeline_.exchange(
|
||||
did_read ? instruction : Pipeline::SWI,
|
||||
did_read ? Pipeline::SWISubversion::None : Pipeline::SWISubversion::DataAbort);
|
||||
}
|
||||
|
||||
struct Pipeline {
|
||||
enum SWISubversion: uint8_t {
|
||||
None,
|
||||
DataAbort,
|
||||
IRQ,
|
||||
FIQ,
|
||||
};
|
||||
|
||||
static constexpr uint32_t SWI = 0xef'000000;
|
||||
|
||||
uint32_t exchange(uint32_t next, SWISubversion subversion) {
|
||||
const uint32_t result = upcoming_[active_].opcode;
|
||||
latched_subversion_ = upcoming_[active_].subversion;
|
||||
|
||||
upcoming_[active_].opcode = next;
|
||||
upcoming_[active_].subversion = subversion;
|
||||
active_ ^= 1;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SWISubversion swi_subversion() const {
|
||||
return latched_subversion_;
|
||||
}
|
||||
|
||||
// TODO: one day, possibly: schedule the subversion one slot further into the future
|
||||
// (i.e. active_ ^ 1) to allow one further instruction to occur as usual before the
|
||||
// action paplies. That is, if interrupts take effect one instruction later after a flags
|
||||
// change, which I don't yet know.
|
||||
//
|
||||
// In practice I got into a bit of a race condition between interrupt scheduling and
|
||||
// flags changes, so have backed off for now.
|
||||
void reschedule(SWISubversion subversion) {
|
||||
upcoming_[active_].opcode = SWI;
|
||||
upcoming_[active_].subversion = subversion;
|
||||
}
|
||||
|
||||
bool interrupt_next() const {
|
||||
return upcoming_[active_].subversion == SWISubversion::IRQ || upcoming_[active_].subversion == SWISubversion::FIQ;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Stage {
|
||||
uint32_t opcode;
|
||||
SWISubversion subversion = SWISubversion::None;
|
||||
};
|
||||
Stage upcoming_[2];
|
||||
int active_ = 0;
|
||||
|
||||
SWISubversion latched_subversion_;
|
||||
} pipeline_;
|
||||
|
||||
// MARK: - Yucky, temporary junk.
|
||||
HackyDebugger<arm_model, Executor> debugger_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
using namespace Archimedes;
|
||||
|
||||
std::unique_ptr<Machine> Machine::Archimedes(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
return std::make_unique<ConcreteMachine>(*target, rom_fetcher);
|
||||
}
|
||||
27
Machines/Acorn/Archimedes/Archimedes.hpp
Normal file
27
Machines/Acorn/Archimedes/Archimedes.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Archimedes.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine() = default;
|
||||
static std::unique_ptr<Machine> Archimedes(
|
||||
const Analyser::Static::Target *target,
|
||||
const ROMMachine::ROMFetcher &rom_fetcher
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
85
Machines/Acorn/Archimedes/CMOSRAM.hpp
Normal file
85
Machines/Acorn/Archimedes/CMOSRAM.hpp
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// CMOSRAM.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../../Components/I2C/I2C.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
struct CMOSRAM: public I2C::Peripheral {
|
||||
CMOSRAM() {
|
||||
ram_ = default_ram;
|
||||
}
|
||||
|
||||
void start(bool is_read) override {
|
||||
expecting_address_ = !is_read;
|
||||
}
|
||||
|
||||
// TODO: first 16 addresses are registers, not RAM.
|
||||
|
||||
std::optional<uint8_t> read() override {
|
||||
if(address_ < 16) {
|
||||
logger.error().append("TODO: read at %d", address_);
|
||||
}
|
||||
|
||||
const uint8_t result = ram_[address_];
|
||||
++address_;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool write(uint8_t value) override {
|
||||
if(expecting_address_) {
|
||||
address_ = value;
|
||||
expecting_address_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(address_ < 16) {
|
||||
logger.error().append("TODO: write at %d", address_);
|
||||
return true;
|
||||
}
|
||||
|
||||
ram_[address_] = value;
|
||||
++address_;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool expecting_address_ = false;
|
||||
uint8_t address_;
|
||||
|
||||
std::array<uint8_t, 256> ram_{};
|
||||
|
||||
// This is the default contents of RAM as written by RISC OS 3.11.
|
||||
static constexpr std::array<uint8_t, 256> default_ram = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x03, 0x14, 0x00, 0x6f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d,
|
||||
0x00, 0xfe, 0x00, 0xeb, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x10, 0x50, 0x20, 0x08, 0x0a, 0x2c,
|
||||
0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x5c, 0x13, 0x00, 0x00, 0x04, 0xfd, 0x08, 0x01, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
Log::Logger<Log::Source::CMOSRTC> logger;
|
||||
};
|
||||
|
||||
}
|
||||
51
Machines/Acorn/Archimedes/FloppyDisc.hpp
Normal file
51
Machines/Acorn/Archimedes/FloppyDisc.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// FloppyDisc.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/04/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../../Components/1770/1770.hpp"
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
template <typename InterruptObserverT>
|
||||
class FloppyDisc: public WD::WD1770, public WD::WD1770::Delegate {
|
||||
public:
|
||||
FloppyDisc(InterruptObserverT &observer) : WD::WD1770(P1772), observer_(observer) {
|
||||
emplace_drives(1, 8000000, 300, 2, Storage::Disk::Drive::ReadyType::ShugartModifiedRDY); // A guess at RDY type.
|
||||
set_delegate(this);
|
||||
}
|
||||
|
||||
void wd1770_did_change_output(WD::WD1770 *) override {
|
||||
observer_.update_interrupts();
|
||||
}
|
||||
|
||||
void set_control(uint8_t value) {
|
||||
// b0, b1, b2, b3 = drive selects;
|
||||
// b4 = side select;
|
||||
// b5 = motor on/off
|
||||
// b6 = floppy in use (i.e. LED?);
|
||||
// b7 = disc eject/change reset.
|
||||
set_drive((value & 0x1) ^ 0x1);
|
||||
get_drive().set_head(1 ^ ((value >> 4) & 1));
|
||||
get_drive().set_motor_on(!(value & 0x20));
|
||||
}
|
||||
void reset() {}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
get_drive(drive).set_disk(disk);
|
||||
}
|
||||
|
||||
bool ready() const {
|
||||
return get_drive().get_is_ready();
|
||||
}
|
||||
|
||||
private:
|
||||
InterruptObserverT &observer_;
|
||||
};
|
||||
|
||||
}
|
||||
88
Machines/Acorn/Archimedes/HalfDuplexSerial.hpp
Normal file
88
Machines/Acorn/Archimedes/HalfDuplexSerial.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// HalfDuplexSerial.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
/// Models a half-duplex serial link between two parties, framing bytes with one start bit and two stop bits.
|
||||
struct HalfDuplexSerial {
|
||||
static constexpr uint16_t ShiftMask = 0b1111'1110'0000'0000;
|
||||
|
||||
/// Enqueues @c value for output.
|
||||
void output(int party, uint8_t value) {
|
||||
parties_[party].output_count = 11;
|
||||
parties_[party].input = 0x7ff;
|
||||
parties_[party].output = uint16_t((value << 1) | ShiftMask);
|
||||
}
|
||||
|
||||
/// @returns The last observed input.
|
||||
uint8_t input(int party) const {
|
||||
return uint8_t(parties_[party].input >> 1);
|
||||
}
|
||||
|
||||
static constexpr uint8_t Receive = 1 << 0;
|
||||
static constexpr uint8_t Transmit = 1 << 1;
|
||||
|
||||
/// @returns A bitmask of events that occurred during the last shift.
|
||||
uint8_t events(int party) {
|
||||
const auto result = parties_[party].events;
|
||||
parties_[party].events = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool is_outputting(int party) const {
|
||||
return parties_[party].output_count != 11;
|
||||
}
|
||||
|
||||
/// Updates the shifters on both sides of the serial link.
|
||||
void shift() {
|
||||
const uint16_t next = parties_[0].output & parties_[1].output & 1;
|
||||
|
||||
for(int c = 0; c < 2; c++) {
|
||||
if(parties_[c].output_count) {
|
||||
--parties_[c].output_count;
|
||||
if(!parties_[c].output_count) {
|
||||
parties_[c].events |= Transmit;
|
||||
parties_[c].input_count = -1;
|
||||
}
|
||||
parties_[c].output = (parties_[c].output >> 1) | ShiftMask;
|
||||
} else {
|
||||
// Check for a start bit.
|
||||
if(parties_[c].input_count == -1 && !next) {
|
||||
parties_[c].input_count = 0;
|
||||
}
|
||||
|
||||
// Shift in if currently observing.
|
||||
if(parties_[c].input_count >= 0 && parties_[c].input_count < 11) {
|
||||
parties_[c].input = uint16_t((parties_[c].input >> 1) | (next << 10));
|
||||
|
||||
++parties_[c].input_count;
|
||||
if(parties_[c].input_count == 11) {
|
||||
parties_[c].events |= Receive;
|
||||
parties_[c].input_count = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Party {
|
||||
int output_count = 0;
|
||||
int input_count = -1;
|
||||
uint16_t output = 0xffff;
|
||||
uint16_t input = 0;
|
||||
uint8_t events = 0;
|
||||
} parties_[2];
|
||||
};
|
||||
|
||||
static constexpr int IOCParty = 0;
|
||||
static constexpr int KeyboardParty = 1;
|
||||
|
||||
}
|
||||
565
Machines/Acorn/Archimedes/InputOutputController.hpp
Normal file
565
Machines/Acorn/Archimedes/InputOutputController.hpp
Normal file
@@ -0,0 +1,565 @@
|
||||
//
|
||||
// InputOutputController.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CMOSRAM.hpp"
|
||||
#include "FloppyDisc.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "Sound.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
#include "../../../Activity/Observer.hpp"
|
||||
#include "../../../ClockReceiver/ClockingHintSource.hpp"
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
// IRQ A flags
|
||||
namespace IRQA {
|
||||
static constexpr uint8_t PrinterBusy = 0x01;
|
||||
static constexpr uint8_t SerialRinging = 0x02;
|
||||
static constexpr uint8_t PrinterAcknowledge = 0x04;
|
||||
static constexpr uint8_t VerticalFlyback = 0x08;
|
||||
static constexpr uint8_t PowerOnReset = 0x10;
|
||||
static constexpr uint8_t Timer0 = 0x20;
|
||||
static constexpr uint8_t Timer1 = 0x40;
|
||||
static constexpr uint8_t Force = 0x80;
|
||||
}
|
||||
|
||||
// IRQ B flags
|
||||
namespace IRQB {
|
||||
static constexpr uint8_t PoduleFIQRequest = 0x01;
|
||||
static constexpr uint8_t SoundBufferPointerUsed = 0x02;
|
||||
static constexpr uint8_t SerialLine = 0x04;
|
||||
static constexpr uint8_t IDE = 0x08;
|
||||
static constexpr uint8_t FloppyDiscChanged = 0x10;
|
||||
static constexpr uint8_t PoduleIRQRequest = 0x20;
|
||||
static constexpr uint8_t KeyboardTransmitEmpty = 0x40;
|
||||
static constexpr uint8_t KeyboardReceiveFull = 0x80;
|
||||
}
|
||||
|
||||
// FIQ flags
|
||||
namespace FIQ {
|
||||
static constexpr uint8_t FloppyDiscData = 0x01;
|
||||
static constexpr uint8_t FloppyDiscInterrupt = 0x02;
|
||||
static constexpr uint8_t Econet = 0x04;
|
||||
static constexpr uint8_t PoduleFIQRequest = 0x40;
|
||||
static constexpr uint8_t Force = 0x80;
|
||||
}
|
||||
|
||||
namespace InterruptRequests {
|
||||
static constexpr int IRQ = 0x01;
|
||||
static constexpr int FIQ = 0x02;
|
||||
};
|
||||
|
||||
template <typename InterruptObserverT, typename ClockRateObserverT>
|
||||
struct InputOutputController: public ClockingHint::Observer {
|
||||
InputOutputController(InterruptObserverT &observer, ClockRateObserverT &clock_observer, const uint8_t *ram) :
|
||||
observer_(observer),
|
||||
keyboard_(serial_),
|
||||
floppy_(*this),
|
||||
sound_(*this, ram),
|
||||
video_(*this, clock_observer, sound_, ram)
|
||||
{
|
||||
irq_a_.status = IRQA::Force | IRQA::PowerOnReset;
|
||||
irq_b_.status = 0x00;
|
||||
fiq_.status = FIQ::Force;
|
||||
|
||||
floppy_.set_clocking_hint_observer(this);
|
||||
|
||||
i2c_.add_peripheral(&cmos_, 0xa0);
|
||||
update_interrupts();
|
||||
}
|
||||
|
||||
int interrupt_mask() const {
|
||||
return
|
||||
((irq_a_.request() | irq_b_.request()) ? InterruptRequests::IRQ : 0) |
|
||||
(fiq_.request() ? InterruptRequests::FIQ : 0);
|
||||
}
|
||||
|
||||
template <int c>
|
||||
bool tick_timer() {
|
||||
if(!counters_[c].value && !counters_[c].reload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
--counters_[c].value;
|
||||
if(!counters_[c].value) {
|
||||
counters_[c].value = counters_[c].reload;
|
||||
|
||||
switch(c) {
|
||||
case 0: return irq_a_.set(IRQA::Timer0);
|
||||
case 1: return irq_a_.set(IRQA::Timer1);
|
||||
case 3: {
|
||||
serial_.shift();
|
||||
keyboard_.update();
|
||||
|
||||
const uint8_t events = serial_.events(IOCParty);
|
||||
bool did_interrupt = false;
|
||||
if(events & HalfDuplexSerial::Receive) {
|
||||
did_interrupt |= irq_b_.set(IRQB::KeyboardReceiveFull);
|
||||
}
|
||||
if(events & HalfDuplexSerial::Transmit) {
|
||||
did_interrupt |= irq_b_.set(IRQB::KeyboardTransmitEmpty);
|
||||
}
|
||||
|
||||
return did_interrupt;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
// TODO: events for timers 2 (baud).
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void tick_timers() {
|
||||
bool did_change_interrupts = false;
|
||||
did_change_interrupts |= tick_timer<0>();
|
||||
did_change_interrupts |= tick_timer<1>();
|
||||
did_change_interrupts |= tick_timer<2>();
|
||||
did_change_interrupts |= tick_timer<3>();
|
||||
if(did_change_interrupts) {
|
||||
observer_.update_interrupts();
|
||||
}
|
||||
}
|
||||
|
||||
void tick_floppy() {
|
||||
if(floppy_clocking_ != ClockingHint::Preference::None) {
|
||||
floppy_.run_for(Cycles(1));
|
||||
}
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
floppy_.set_disk(disk, drive);
|
||||
}
|
||||
|
||||
/// Decomposes an Archimedes bus address into bank, offset and type.
|
||||
struct Address {
|
||||
constexpr Address(uint32_t bus_address) noexcept {
|
||||
bank = (bus_address >> 16) & 0b111;
|
||||
type = Type((bus_address >> 19) & 0b11);
|
||||
offset = bus_address & 0b1111100;
|
||||
}
|
||||
|
||||
/// A value from 0 to 7 indicating the device being addressed.
|
||||
uint32_t bank;
|
||||
/// A seven-bit value which is a multiple of 4, indicating the address within the bank.
|
||||
uint32_t offset;
|
||||
/// Access type.
|
||||
enum class Type {
|
||||
Sync = 0b00,
|
||||
Fast = 0b10,
|
||||
Medium = 0b01,
|
||||
Slow = 0b11
|
||||
} type;
|
||||
};
|
||||
|
||||
// Peripheral addresses on the A500:
|
||||
//
|
||||
// fast/1 = FDC
|
||||
// sync/2 = econet
|
||||
// sync/3 = serial line
|
||||
//
|
||||
// bank 4 = podules
|
||||
//
|
||||
// fast/5
|
||||
|
||||
template <typename IntT>
|
||||
bool read(uint32_t address, IntT &destination) {
|
||||
const Address target(address);
|
||||
|
||||
const auto set_byte = [&](uint8_t value) {
|
||||
if constexpr (std::is_same_v<IntT, uint32_t>) {
|
||||
destination = static_cast<uint32_t>(value << 16) | 0xff'00'ff'ff;
|
||||
} else {
|
||||
destination = value;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: flatten the switch below, and the equivalent in `write`.
|
||||
|
||||
switch(target.bank) {
|
||||
default:
|
||||
logger.error().append("Unrecognised IOC read from %08x i.e. bank %d / type %d", address, target.bank, target.type);
|
||||
destination = IntT(~0);
|
||||
break;
|
||||
|
||||
// Bank 0: internal registers.
|
||||
case 0:
|
||||
switch(target.offset) {
|
||||
default:
|
||||
logger.error().append("Unrecognised IOC bank 0 read; offset %02x", target.offset);
|
||||
break;
|
||||
|
||||
case 0x00: {
|
||||
uint8_t value = control_ | 0xc0;
|
||||
value &= ~(i2c_.data() ? 0x01 : 0x00);
|
||||
value &= ~(i2c_.clock() ? 0x02 : 0x00);
|
||||
value &= ~(floppy_.ready() ? 0x00 : 0x04);
|
||||
value &= ~(video_.flyback_active() ? 0x00 : 0x80); // i.e. high during flyback.
|
||||
set_byte(value);
|
||||
// logger.error().append("IOC control read: C:%d D:%d", !(value & 2), !(value & 1));
|
||||
} break;
|
||||
|
||||
case 0x04:
|
||||
set_byte(serial_.input(IOCParty));
|
||||
irq_b_.clear(IRQB::KeyboardReceiveFull);
|
||||
observer_.update_interrupts();
|
||||
// logger.error().append("IOC keyboard receive: %02x", value);
|
||||
break;
|
||||
|
||||
// IRQ A.
|
||||
case 0x10:
|
||||
set_byte(irq_a_.status);
|
||||
// logger.error().append("IRQ A status is %02x", value);
|
||||
break;
|
||||
case 0x14:
|
||||
set_byte(irq_a_.request());
|
||||
// logger.error().append("IRQ A request is %02x", value);
|
||||
break;
|
||||
case 0x18:
|
||||
set_byte(irq_a_.mask);
|
||||
// logger.error().append("IRQ A mask is %02x", value);
|
||||
break;
|
||||
|
||||
// IRQ B.
|
||||
case 0x20:
|
||||
set_byte(irq_b_.status);
|
||||
// logger.error().append("IRQ B status is %02x", value);
|
||||
break;
|
||||
case 0x24:
|
||||
set_byte(irq_b_.request());
|
||||
// logger.error().append("IRQ B request is %02x", value);
|
||||
break;
|
||||
case 0x28:
|
||||
set_byte(irq_b_.mask);
|
||||
// logger.error().append("IRQ B mask is %02x", value);
|
||||
break;
|
||||
|
||||
// FIQ.
|
||||
case 0x30:
|
||||
set_byte(fiq_.status);
|
||||
// logger.error().append("FIQ status is %02x", fiq_.status);
|
||||
break;
|
||||
case 0x34:
|
||||
set_byte(fiq_.request());
|
||||
// logger.error().append("FIQ request is %02x", fiq_.request());
|
||||
break;
|
||||
case 0x38:
|
||||
set_byte(fiq_.mask);
|
||||
// logger.error().append("FIQ mask is %02x", fiq_.mask);
|
||||
break;
|
||||
|
||||
// Counters.
|
||||
case 0x40: case 0x50: case 0x60: case 0x70:
|
||||
set_byte(counters_[(target.offset >> 4) - 0x4].output & 0xff);
|
||||
// logger.error().append("%02x: Counter %d low is %02x", target, (target >> 4) - 0x4, value);
|
||||
break;
|
||||
|
||||
case 0x44: case 0x54: case 0x64: case 0x74:
|
||||
set_byte(counters_[(target.offset >> 4) - 0x4].output >> 8);
|
||||
// logger.error().append("%02x: Counter %d high is %02x", target, (target >> 4) - 0x4, value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
// Bank 1: the floppy disc controller.
|
||||
case 1:
|
||||
set_byte(floppy_.read(target.offset >> 2));
|
||||
// logger.error().append("Floppy read; offset %02x", target.offset);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
bool write(uint32_t address, IntT bus_value) {
|
||||
const Address target(address);
|
||||
|
||||
// Empirically, RISC OS 3.19:
|
||||
// * at 03801e88 and 03801e8c loads R8 and R9 with 0xbe0000 and 0xff0000 respectively; and
|
||||
// * subsequently uses 32-bit strs (e.g. at 03801eac) to write those values to latch A.
|
||||
//
|
||||
// Given that 8-bit ARM writes duplicate the 8-bit value four times across the data bus,
|
||||
// my conclusion is that the IOC is probably connected to data lines 15–23.
|
||||
//
|
||||
// Hence: use @c byte to get a current 8-bit value.
|
||||
const auto byte = [](IntT original) -> uint8_t {
|
||||
if constexpr (std::is_same_v<IntT, uint32_t>) {
|
||||
return static_cast<uint8_t>(original >> 16);
|
||||
} else {
|
||||
return original;
|
||||
}
|
||||
};
|
||||
|
||||
switch(target.bank) {
|
||||
default:
|
||||
logger.error().append("Unrecognised IOC write of %02x to %08x i.e. bank %d / type %d", bus_value, address, target.bank, target.type);
|
||||
break;
|
||||
|
||||
// Bank 0: internal registers.
|
||||
case 0:
|
||||
switch(target.offset) {
|
||||
default:
|
||||
logger.error().append("Unrecognised IOC bank 0 write; %02x to offset %02x", bus_value, target.offset);
|
||||
break;
|
||||
|
||||
case 0x00:
|
||||
control_ = byte(bus_value);
|
||||
i2c_.set_clock_data(!(bus_value & 2), !(bus_value & 1));
|
||||
|
||||
// Per the A500 documentation:
|
||||
// b7: vertical sync/test input bit, so should be programmed high;
|
||||
// b6: input for printer acknowledgement, so should be programmed high;
|
||||
// b5: speaker mute; 1 = muted;
|
||||
// b4: "Available on the auxiliary I/O connector"
|
||||
// b3: "Programmed HIGH, unless Reset Mask is required."
|
||||
// b2: Used as the floppy disk (READY) input and must be programmed high;
|
||||
// b1 and b0: I2C connections as above.
|
||||
break;
|
||||
|
||||
case 0x04:
|
||||
serial_.output(IOCParty, byte(bus_value));
|
||||
irq_b_.clear(IRQB::KeyboardTransmitEmpty);
|
||||
observer_.update_interrupts();
|
||||
break;
|
||||
|
||||
case 0x14:
|
||||
// b2: clear IF.
|
||||
// b3: clear IR.
|
||||
// b4: clear POR.
|
||||
// b5: clear TM[0].
|
||||
// b6: clear TM[1].
|
||||
irq_a_.clear(byte(bus_value) & 0x7c);
|
||||
observer_.update_interrupts();
|
||||
break;
|
||||
|
||||
// Interrupts.
|
||||
case 0x18:
|
||||
irq_a_.mask = byte(bus_value);
|
||||
// logger.error().append("IRQ A mask set to %02x", byte(bus_value));
|
||||
break;
|
||||
case 0x28:
|
||||
irq_b_.mask = byte(bus_value);
|
||||
// logger.error().append("IRQ B mask set to %02x", byte(bus_value));
|
||||
break;
|
||||
case 0x38:
|
||||
fiq_.mask = byte(bus_value);
|
||||
// logger.error().append("FIQ mask set to %02x", byte(bus_value));
|
||||
break;
|
||||
|
||||
// Counters.
|
||||
case 0x40: case 0x50: case 0x60: case 0x70:
|
||||
counters_[(target.offset >> 4) - 0x4].reload = uint16_t(
|
||||
(counters_[(target.offset >> 4) - 0x4].reload & 0xff00) | byte(bus_value)
|
||||
);
|
||||
break;
|
||||
|
||||
case 0x44: case 0x54: case 0x64: case 0x74:
|
||||
counters_[(target.offset >> 4) - 0x4].reload = uint16_t(
|
||||
(counters_[(target.offset >> 4) - 0x4].reload & 0x00ff) | (byte(bus_value) << 8)
|
||||
);
|
||||
break;
|
||||
|
||||
case 0x48: case 0x58: case 0x68: case 0x78:
|
||||
counters_[(target.offset >> 4) - 0x4].value = counters_[(target.offset >> 4) - 0x4].reload;
|
||||
break;
|
||||
|
||||
case 0x4c: case 0x5c: case 0x6c: case 0x7c:
|
||||
counters_[(target.offset >> 4) - 0x4].output = counters_[(target.offset >> 4) - 0x4].value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
// Bank 1: the floppy disc controller.
|
||||
case 1:
|
||||
// logger.error().append("Floppy write; %02x to offset %02x", bus_value, target.offset);
|
||||
floppy_.write(target.offset >> 2, byte(bus_value));
|
||||
// set_byte(floppy_.read(target.offset >> 2));
|
||||
break;
|
||||
|
||||
// Bank 5: both the hard disk and the latches, depending on type.
|
||||
case 5:
|
||||
switch(target.type) {
|
||||
default:
|
||||
logger.error().append("Unrecognised IOC bank 5 type %d write; %02x to offset %02x", target.type, bus_value, target.offset);
|
||||
break;
|
||||
|
||||
case Address::Type::Fast:
|
||||
switch(target.offset) {
|
||||
default:
|
||||
logger.error().append("Unrecognised IOC fast bank 5 write; %02x to offset %02x", bus_value, target.offset);
|
||||
break;
|
||||
|
||||
case 0x00:
|
||||
logger.error().append("TODO: printer data write; %02x", byte(bus_value));
|
||||
break;
|
||||
|
||||
case 0x18: {
|
||||
// TODO, per the A500 documentation:
|
||||
//
|
||||
// Latch B:
|
||||
// b0: ?
|
||||
// b1: double/single density; 0 = double.
|
||||
// b2: ?
|
||||
// b3: floppy drive reset; 0 = reset.
|
||||
// b4: printer strobe
|
||||
// b5: ?
|
||||
// b6: ?
|
||||
// b7: Head select 3?
|
||||
const uint8_t value = byte(bus_value);
|
||||
|
||||
floppy_.set_is_double_density(!(value & 0x2));
|
||||
if(value & 0x08) floppy_.reset();
|
||||
// logger.error().append("TODO: latch B write; %02x", byte(bus_value));
|
||||
} break;
|
||||
|
||||
case 0x40: {
|
||||
const uint8_t value = byte(bus_value);
|
||||
floppy_.set_control(value);
|
||||
|
||||
// Set the floppy indicator on if any drive is selected,
|
||||
// because this emulator is compressing them all into a
|
||||
// single LED, and the machine has indicated 'in use'.
|
||||
if(activity_observer_) {
|
||||
activity_observer_->set_led_status(FloppyActivityLED,
|
||||
!(value & 0x40) && ((value & 0xf) != 0xf)
|
||||
);
|
||||
}
|
||||
} break;
|
||||
|
||||
case 0x48:
|
||||
// TODO, per the A500 documentation:
|
||||
//
|
||||
// Latch C:
|
||||
// (probably not present on earlier machines?)
|
||||
// b2/b3: sync polarity [b3 = V polarity, b2 = H?]
|
||||
// b0/b1: VIDC master clock; 00 = 24Mhz, 01 = 25.175Mhz; 10 = 36Mhz; 11 = reserved.
|
||||
|
||||
logger.error().append("TODO: latch C write; %02x", byte(bus_value));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// case 0x327'0000 & AddressMask: // Bank 7
|
||||
// logger.error().append("TODO: exteded external podule space");
|
||||
// return true;
|
||||
//
|
||||
// case 0x336'0000 & AddressMask:
|
||||
// logger.error().append("TODO: podule interrupt request");
|
||||
// return true;
|
||||
//
|
||||
// case 0x336'0004 & AddressMask:
|
||||
// logger.error().append("TODO: podule interrupt mask");
|
||||
// return true;
|
||||
//
|
||||
// case 0x33a'0000 & AddressMask:
|
||||
// logger.error().append("TODO: 6854 / econet write");
|
||||
// return true;
|
||||
//
|
||||
// case 0x33b'0000 & AddressMask:
|
||||
// logger.error().append("TODO: 6551 / serial line write");
|
||||
// return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto &sound() { return sound_; }
|
||||
const auto &sound() const { return sound_; }
|
||||
auto &video() { return video_; }
|
||||
const auto &video() const { return video_; }
|
||||
auto &keyboard() { return keyboard_; }
|
||||
const auto &keyboard() const { return keyboard_; }
|
||||
|
||||
void update_interrupts() {
|
||||
const auto set = [&](Interrupt &target, uint8_t flag, bool set) {
|
||||
if(set) {
|
||||
target.set(flag);
|
||||
} else {
|
||||
target.clear(flag);
|
||||
}
|
||||
};
|
||||
|
||||
set(irq_b_, IRQB::SoundBufferPointerUsed, sound_.interrupt());
|
||||
set(fiq_, FIQ::FloppyDiscInterrupt, floppy_.get_interrupt_request_line());
|
||||
set(fiq_, FIQ::FloppyDiscData, floppy_.get_data_request_line());
|
||||
|
||||
if(video_.interrupt()) {
|
||||
irq_a_.set(IRQA::VerticalFlyback);
|
||||
}
|
||||
|
||||
observer_.update_interrupts();
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
activity_observer_ = observer;
|
||||
if(activity_observer_) {
|
||||
activity_observer_->register_led(FloppyActivityLED);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Log::Logger<Log::Source::ARMIOC> logger;
|
||||
InterruptObserverT &observer_;
|
||||
Activity::Observer *activity_observer_ = nullptr;
|
||||
static inline const std::string FloppyActivityLED = "Drive";
|
||||
|
||||
// IRQA, IRQB and FIQ states.
|
||||
struct Interrupt {
|
||||
uint8_t status = 0x00, mask = 0x00;
|
||||
uint8_t request() const {
|
||||
return status & mask;
|
||||
}
|
||||
bool set(uint8_t value) {
|
||||
status |= value;
|
||||
return status & mask;
|
||||
}
|
||||
void clear(uint8_t bits) {
|
||||
status &= ~bits;
|
||||
}
|
||||
};
|
||||
Interrupt irq_a_, irq_b_, fiq_;
|
||||
|
||||
// The IOCs four counters.
|
||||
struct Counter {
|
||||
uint16_t value = 0;
|
||||
uint16_t reload = 0;
|
||||
uint16_t output = 0;
|
||||
};
|
||||
Counter counters_[4];
|
||||
|
||||
// The KART and keyboard beyond it.
|
||||
HalfDuplexSerial serial_;
|
||||
Keyboard keyboard_;
|
||||
|
||||
// The control register.
|
||||
uint8_t control_ = 0xff;
|
||||
|
||||
// The floppy disc interface.
|
||||
FloppyDisc<InputOutputController> floppy_;
|
||||
ClockingHint::Preference floppy_clocking_ = ClockingHint::Preference::None;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) override {
|
||||
floppy_clocking_ = clocking;
|
||||
}
|
||||
|
||||
// The I2C bus.
|
||||
I2C::Bus i2c_;
|
||||
CMOSRAM cmos_;
|
||||
|
||||
// Audio and video.
|
||||
Sound<InputOutputController> sound_;
|
||||
Video<InputOutputController, ClockRateObserverT, Sound<InputOutputController>> video_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
240
Machines/Acorn/Archimedes/Keyboard.hpp
Normal file
240
Machines/Acorn/Archimedes/Keyboard.hpp
Normal file
@@ -0,0 +1,240 @@
|
||||
//
|
||||
// Keyboard.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "HalfDuplexSerial.hpp"
|
||||
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
#include "../../../Inputs/Mouse.hpp"
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
// Resource for the keyboard protocol: https://github.com/tmk/tmk_keyboard/wiki/ACORN-ARCHIMEDES-Keyboard
|
||||
struct Keyboard {
|
||||
Keyboard(HalfDuplexSerial &serial) : serial_(serial), mouse_(*this) {}
|
||||
|
||||
void set_key_state(int row, int column, bool is_pressed) {
|
||||
if(!scan_keyboard_) {
|
||||
logger_.info().append("Ignored key event as key scanning disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't waste bandwidth on repeating facts.
|
||||
if(states_[row][column] == is_pressed) return;
|
||||
states_[row][column] = is_pressed;
|
||||
|
||||
// Post new key event.
|
||||
logger_.info().append("Posting row %d, column %d is now %s", row, column, is_pressed ? "pressed" : "released");
|
||||
const uint8_t prefix = is_pressed ? 0b1100'0000 : 0b1101'0000;
|
||||
enqueue(static_cast<uint8_t>(prefix | row), static_cast<uint8_t>(prefix | column));
|
||||
consider_dequeue();
|
||||
}
|
||||
|
||||
void update() {
|
||||
if(serial_.events(KeyboardParty) & HalfDuplexSerial::Receive) {
|
||||
const auto reset = [&]() {
|
||||
serial_.output(KeyboardParty, HRST);
|
||||
state_ = State::Idle;
|
||||
};
|
||||
|
||||
const uint8_t input = serial_.input(KeyboardParty);
|
||||
|
||||
// A reset command is always accepted, usurping any other state.
|
||||
if(input == HRST) {
|
||||
logger_.info().append("HRST; resetting");
|
||||
state_ = State::ExpectingRAK1;
|
||||
event_queue_.clear();
|
||||
serial_.output(KeyboardParty, HRST);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(state_) {
|
||||
case State::ExpectingACK:
|
||||
if(input != NACK && input != SMAK && input != MACK && input != SACK) {
|
||||
logger_.error().append("No ack; requesting reset");
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
state_ = State::Idle;
|
||||
[[fallthrough]];
|
||||
|
||||
case State::Idle:
|
||||
switch(input) {
|
||||
case RQID: // Post keyboard ID.
|
||||
serial_.output(KeyboardParty, 0x81); // Declare this to be a UK keyboard.
|
||||
logger_.info().append("RQID; responded with 0x81");
|
||||
break;
|
||||
|
||||
case PRST: // "1-byte command, does nothing."
|
||||
logger_.info().append("PRST; ignored");
|
||||
break;
|
||||
|
||||
case RQMP:
|
||||
logger_.error().append("RQMP; TODO: respond something other than 0, 0");
|
||||
enqueue(0, 0);
|
||||
break;
|
||||
|
||||
case NACK: case SMAK: case MACK: case SACK:
|
||||
scan_keyboard_ = input & 1;
|
||||
scan_mouse_ = input & 2;
|
||||
logger_.info().append("ACK; keyboard:%d mouse:%d", scan_keyboard_, scan_mouse_);
|
||||
break;
|
||||
|
||||
default:
|
||||
if((input & 0b1111'0000) == 0b0100'0000) {
|
||||
// RQPD; request to echo the low nibble.
|
||||
serial_.output(KeyboardParty, 0b1110'0000 | (input & 0b1111));
|
||||
logger_.info().append("RQPD; echoing %x", input & 0b1111);
|
||||
} else if(!(input & 0b1111'1000)) {
|
||||
// LEDS: should set LED outputs.
|
||||
logger_.error().append("TODO: set LEDs %d%d%d", static_cast<bool>(input&4), static_cast<bool>(input&2), static_cast<bool>(input&1));
|
||||
} else {
|
||||
logger_.info().append("Ignoring unrecognised command %02x received in idle state", input);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::ExpectingRAK1:
|
||||
if(input != RAK1) {
|
||||
logger_.info().append("Didn't get RAK1; resetting");
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
logger_.info().append("Got RAK1; echoing");
|
||||
serial_.output(KeyboardParty, input);
|
||||
state_ = State::ExpectingRAK2;
|
||||
break;
|
||||
|
||||
case State::ExpectingRAK2:
|
||||
if(input != RAK2) {
|
||||
logger_.info().append("Didn't get RAK2; resetting");
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
logger_.info().append("Got RAK2; echoing");
|
||||
serial_.output(KeyboardParty, input);
|
||||
state_ = State::ExpectingACK;
|
||||
break;
|
||||
|
||||
case State::ExpectingBACK:
|
||||
if(input != BACK) {
|
||||
logger_.info().append("Didn't get BACK; resetting");
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
logger_.info().append("Got BACK; posting next byte");
|
||||
dequeue_next();
|
||||
state_ = State::ExpectingACK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
consider_dequeue();
|
||||
}
|
||||
|
||||
void consider_dequeue() {
|
||||
if(state_ == State::Idle) {
|
||||
// If the key event queue is empty, grab as much mouse motion
|
||||
// as available.
|
||||
if(event_queue_.empty()) {
|
||||
const int x = std::clamp(mouse_x_, -0x3f, 0x3f);
|
||||
const int y = std::clamp(mouse_y_, -0x3f, 0x3f);
|
||||
mouse_x_ -= x;
|
||||
mouse_y_ -= y;
|
||||
|
||||
if(x || y) {
|
||||
enqueue(static_cast<uint8_t>(x) & 0x7f, static_cast<uint8_t>(-y) & 0x7f);
|
||||
}
|
||||
}
|
||||
|
||||
if(dequeue_next()) {
|
||||
state_ = State::ExpectingBACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inputs::Mouse &mouse() {
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
private:
|
||||
HalfDuplexSerial &serial_;
|
||||
Log::Logger<Log::Source::Keyboard> logger_;
|
||||
|
||||
bool states_[16][16]{};
|
||||
|
||||
bool scan_keyboard_ = false;
|
||||
bool scan_mouse_ = false;
|
||||
enum class State {
|
||||
ExpectingRAK1, // Post a RAK1 and proceed to ExpectingRAK2 if RAK1 is received; otherwise request a reset.
|
||||
ExpectingRAK2, // Post a RAK2 and proceed to ExpectingACK if RAK2 is received; otherwise request a reset.
|
||||
ExpectingACK, // Process NACK, SACK, MACK or SMAK if received; otherwise request a reset.
|
||||
|
||||
Idle, // Process any of: NACK, SACK, MACK, SMAK, RQID, RQMP, RQPD or LEDS if received; also
|
||||
// unilaterally begin post a byte pair enqueued but not yet sent if any are waiting.
|
||||
|
||||
ExpectingBACK, // Dequeue and post one further byte if BACK is received; otherwise request a reset.
|
||||
} state_ = State::Idle;
|
||||
|
||||
std::vector<uint8_t> event_queue_;
|
||||
void enqueue(uint8_t first, uint8_t second) {
|
||||
event_queue_.push_back(first);
|
||||
event_queue_.push_back(second);
|
||||
}
|
||||
bool dequeue_next() {
|
||||
// To consider: a cheaper approach to the queue than this; in practice events
|
||||
// are 'rare' so it's not high priority.
|
||||
if(event_queue_.empty()) return false;
|
||||
serial_.output(KeyboardParty, event_queue_[0]);
|
||||
event_queue_.erase(event_queue_.begin());
|
||||
return true;
|
||||
}
|
||||
|
||||
static constexpr uint8_t HRST = 0b1111'1111; // Keyboard reset.
|
||||
static constexpr uint8_t RAK1 = 0b1111'1110; // Reset response #1.
|
||||
static constexpr uint8_t RAK2 = 0b1111'1101; // Reset response #2.
|
||||
|
||||
static constexpr uint8_t RQID = 0b0010'0000; // Request for keyboard ID.
|
||||
static constexpr uint8_t RQMP = 0b0010'0010; // Request for mouse data.
|
||||
|
||||
static constexpr uint8_t BACK = 0b0011'1111; // Acknowledge for first keyboard data byte pair.
|
||||
static constexpr uint8_t NACK = 0b0011'0000; // Acknowledge for last keyboard data byte pair, disables both scanning and mouse.
|
||||
static constexpr uint8_t SACK = 0b0011'0001; // Last data byte acknowledge, enabling scanning but disabling mouse.
|
||||
static constexpr uint8_t MACK = 0b0011'0010; // Last data byte acknowledge, disabling scanning but enabling mouse.
|
||||
static constexpr uint8_t SMAK = 0b0011'0011; // Last data byte acknowledge, enabling scanning and mouse.
|
||||
static constexpr uint8_t PRST = 0b0010'0001; // Does nothing.
|
||||
|
||||
|
||||
struct Mouse: public Inputs::Mouse {
|
||||
Mouse(Keyboard &keyboard): keyboard_(keyboard) {}
|
||||
|
||||
void move(int x, int y) override {
|
||||
keyboard_.mouse_x_ += x;
|
||||
keyboard_.mouse_y_ += y;
|
||||
}
|
||||
|
||||
int get_number_of_buttons() override {
|
||||
return 3;
|
||||
}
|
||||
|
||||
virtual void set_button_pressed(int index, bool is_pressed) override {
|
||||
keyboard_.set_key_state(7, index, is_pressed);
|
||||
}
|
||||
|
||||
private:
|
||||
Keyboard &keyboard_;
|
||||
};
|
||||
Mouse mouse_;
|
||||
|
||||
int mouse_x_ = 0;
|
||||
int mouse_y_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
148
Machines/Acorn/Archimedes/KeyboardMapper.hpp
Normal file
148
Machines/Acorn/Archimedes/KeyboardMapper.hpp
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// KeyboardMapper.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 23/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
public:
|
||||
static constexpr uint16_t map(int row, int column) {
|
||||
return static_cast<uint16_t>((row << 4) | column);
|
||||
}
|
||||
|
||||
static constexpr int row(uint16_t key) {
|
||||
return key >> 4;
|
||||
}
|
||||
|
||||
static constexpr int column(uint16_t key) {
|
||||
return key & 0xf;
|
||||
}
|
||||
|
||||
// Adapted from the A500 Series Technical Reference Manual.
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override {
|
||||
using k = Inputs::Keyboard::Key;
|
||||
switch(key) {
|
||||
case k::Escape: return map(0, 0);
|
||||
case k::F1: return map(0, 1);
|
||||
case k::F2: return map(0, 2);
|
||||
case k::F3: return map(0, 3);
|
||||
case k::F4: return map(0, 4);
|
||||
case k::F5: return map(0, 5);
|
||||
case k::F6: return map(0, 6);
|
||||
case k::F7: return map(0, 7);
|
||||
case k::F8: return map(0, 8);
|
||||
case k::F9: return map(0, 9);
|
||||
case k::F10: return map(0, 10);
|
||||
case k::F11: return map(0, 11);
|
||||
case k::F12: return map(0, 12);
|
||||
case k::PrintScreen: return map(0, 13);
|
||||
case k::ScrollLock: return map(0, 14);
|
||||
case k::Pause: return map(0, 15);
|
||||
|
||||
case k::BackTick: return map(1, 0);
|
||||
case k::k1: return map(1, 1);
|
||||
case k::k2: return map(1, 2);
|
||||
case k::k3: return map(1, 3);
|
||||
case k::k4: return map(1, 4);
|
||||
case k::k5: return map(1, 5);
|
||||
case k::k6: return map(1, 6);
|
||||
case k::k7: return map(1, 7);
|
||||
case k::k8: return map(1, 8);
|
||||
case k::k9: return map(1, 9);
|
||||
case k::k0: return map(1, 10);
|
||||
case k::Hyphen: return map(1, 11);
|
||||
case k::Equals: return map(1, 12);
|
||||
// TODO: pound key.
|
||||
case k::Backspace: return map(1, 14);
|
||||
case k::Insert: return map(1, 15);
|
||||
|
||||
case k::Home: return map(2, 0);
|
||||
case k::PageUp: return map(2, 1);
|
||||
case k::NumLock: return map(2, 2);
|
||||
case k::KeypadSlash: return map(2, 3);
|
||||
case k::KeypadAsterisk: return map(2, 4);
|
||||
// TODO: keypad hash key
|
||||
case k::Tab: return map(2, 6);
|
||||
case k::Q: return map(2, 7);
|
||||
case k::W: return map(2, 8);
|
||||
case k::E: return map(2, 9);
|
||||
case k::R: return map(2, 10);
|
||||
case k::T: return map(2, 11);
|
||||
case k::Y: return map(2, 12);
|
||||
case k::U: return map(2, 13);
|
||||
case k::I: return map(2, 14);
|
||||
case k::O: return map(2, 15);
|
||||
|
||||
case k::P: return map(3, 0);
|
||||
case k::OpenSquareBracket: return map(3, 1);
|
||||
case k::CloseSquareBracket: return map(3, 2);
|
||||
case k::Backslash: return map(3, 3);
|
||||
case k::Delete: return map(3, 4);
|
||||
case k::End: return map(3, 5);
|
||||
case k::PageDown: return map(3, 6);
|
||||
case k::Keypad7: return map(3, 7);
|
||||
case k::Keypad8: return map(3, 8);
|
||||
case k::Keypad9: return map(3, 9);
|
||||
case k::KeypadMinus: return map(3, 10);
|
||||
case k::LeftControl: return map(3, 11);
|
||||
case k::A: return map(3, 12);
|
||||
case k::S: return map(3, 13);
|
||||
case k::D: return map(3, 14);
|
||||
case k::F: return map(3, 15);
|
||||
|
||||
case k::G: return map(4, 0);
|
||||
case k::H: return map(4, 1);
|
||||
case k::J: return map(4, 2);
|
||||
case k::K: return map(4, 3);
|
||||
case k::L: return map(4, 4);
|
||||
case k::Semicolon: return map(4, 5);
|
||||
case k::Quote: return map(4, 6);
|
||||
case k::Enter: return map(4, 7);
|
||||
case k::Keypad4: return map(4, 8);
|
||||
case k::Keypad5: return map(4, 9);
|
||||
case k::Keypad6: return map(4, 10);
|
||||
case k::KeypadPlus: return map(4, 11);
|
||||
case k::LeftShift: return map(4, 12);
|
||||
case k::Z: return map(4, 14);
|
||||
case k::X: return map(4, 15);
|
||||
|
||||
case k::C: return map(5, 0);
|
||||
case k::V: return map(5, 1);
|
||||
case k::B: return map(5, 2);
|
||||
case k::N: return map(5, 3);
|
||||
case k::M: return map(5, 4);
|
||||
case k::Comma: return map(5, 5);
|
||||
case k::FullStop: return map(5, 6);
|
||||
case k::ForwardSlash: return map(5, 7);
|
||||
case k::RightShift: return map(5, 8);
|
||||
case k::Up: return map(5, 9);
|
||||
case k::Keypad1: return map(5, 10);
|
||||
case k::Keypad2: return map(5, 11);
|
||||
case k::Keypad3: return map(5, 12);
|
||||
case k::CapsLock: return map(5, 13);
|
||||
case k::LeftOption: return map(5, 14);
|
||||
case k::Space: return map(5, 15);
|
||||
|
||||
case k::RightOption: return map(6, 0);
|
||||
case k::RightControl: return map(6, 1);
|
||||
case k::Left: return map(6, 2);
|
||||
case k::Down: return map(6, 3);
|
||||
case k::Right: return map(6, 4);
|
||||
case k::Keypad0: return map(6, 5);
|
||||
case k::KeypadDecimalPoint: return map(6, 6);
|
||||
case k::KeypadEnter: return map(6, 7);
|
||||
|
||||
default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
507
Machines/Acorn/Archimedes/MemoryController.hpp
Normal file
507
Machines/Acorn/Archimedes/MemoryController.hpp
Normal file
@@ -0,0 +1,507 @@
|
||||
//
|
||||
// MemoryController.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "InputOutputController.hpp"
|
||||
#include "Video.hpp"
|
||||
#include "Sound.hpp"
|
||||
|
||||
#include "../../../InstructionSets/ARM/Registers.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
#include "../../../Activity/Observer.hpp"
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
/// Provides the mask with all bits set in the range [start, end], where start must be >= end.
|
||||
template <int start, int end> struct BitMask {
|
||||
static_assert(start >= end);
|
||||
static constexpr uint32_t value = ((1 << (start + 1)) - 1) - ((1 << end) - 1);
|
||||
};
|
||||
static_assert(BitMask<0, 0>::value == 1);
|
||||
static_assert(BitMask<1, 1>::value == 2);
|
||||
static_assert(BitMask<15, 15>::value == 32768);
|
||||
static_assert(BitMask<15, 0>::value == 0xffff);
|
||||
static_assert(BitMask<15, 14>::value == 49152);
|
||||
|
||||
|
||||
/// Models the MEMC, making this the Archimedes bus. Owns various other chips on the bus as a result.
|
||||
template <typename InterruptObserverT, typename ClockRateObserverT>
|
||||
struct MemoryController {
|
||||
MemoryController(InterruptObserverT &observer, ClockRateObserverT &clock_rate_observer) :
|
||||
ioc_(observer, clock_rate_observer, ram_.data()) {
|
||||
read_zones_[0] = ReadZone::HighROM; // Temporarily put high ROM at address 0.
|
||||
// TODO: could I just copy it in? Or, at least,
|
||||
// could I detect at ROM loading time whether I can?
|
||||
}
|
||||
|
||||
int interrupt_mask() const {
|
||||
return ioc_.interrupt_mask();
|
||||
}
|
||||
|
||||
void set_rom(const std::vector<uint8_t> &rom) {
|
||||
if(rom_.size() % rom.size() || rom.size() > rom_.size()) {
|
||||
// TODO: throw.
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy in as many times as it'll fit.
|
||||
std::size_t base = 0;
|
||||
while(base < rom_.size()) {
|
||||
std::copy(
|
||||
rom.begin(),
|
||||
rom.end(),
|
||||
rom_.begin() + base);
|
||||
base += rom.size();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
uint32_t aligned(uint32_t address) {
|
||||
if constexpr (std::is_same_v<IntT, uint32_t>) {
|
||||
return address & static_cast<uint32_t>(~3);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
bool write(uint32_t address, IntT source, InstructionSet::ARM::Mode, bool trans) {
|
||||
switch(write_zones_[(address >> 21) & 31]) {
|
||||
case WriteZone::LogicallyMappedRAM: {
|
||||
const auto item = logical_ram<IntT, false>(address, trans);
|
||||
if(item < reinterpret_cast<IntT *>(ram_.data())) {
|
||||
return false;
|
||||
}
|
||||
*item = source;
|
||||
} break;
|
||||
|
||||
case WriteZone::PhysicallyMappedRAM:
|
||||
if(trans) return false;
|
||||
physical_ram<IntT>(address) = source;
|
||||
break;
|
||||
|
||||
case WriteZone::DMAAndMEMC: {
|
||||
if(trans) return false;
|
||||
|
||||
const auto buffer_address = [](uint32_t source) -> uint32_t {
|
||||
return (source & 0x1'fffc) << 2;
|
||||
};
|
||||
|
||||
// The MEMC itself isn't on the data bus; all values below should be taken from `address`.
|
||||
switch((address >> 17) & 0b111) {
|
||||
case 0b000: ioc_.video().set_frame_start(buffer_address(address)); break;
|
||||
case 0b001: ioc_.video().set_buffer_start(buffer_address(address)); break;
|
||||
case 0b010: ioc_.video().set_buffer_end(buffer_address(address)); break;
|
||||
case 0b011: ioc_.video().set_cursor_start(buffer_address(address)); break;
|
||||
|
||||
case 0b100: ioc_.sound().set_next_start(buffer_address(address)); break;
|
||||
case 0b101: ioc_.sound().set_next_end(buffer_address(address)); break;
|
||||
case 0b110: ioc_.sound().swap(); break;
|
||||
|
||||
case 0b111:
|
||||
os_mode_ = address & (1 << 12);
|
||||
sound_dma_enable_ = address & (1 << 11);
|
||||
ioc_.sound().set_dma_enabled(sound_dma_enable_);
|
||||
video_dma_enable_ = address & (1 << 10);
|
||||
switch((address >> 8) & 3) {
|
||||
default:
|
||||
dynamic_ram_refresh_ = DynamicRAMRefresh::None;
|
||||
break;
|
||||
case 0b01:
|
||||
case 0b11:
|
||||
dynamic_ram_refresh_ = DynamicRAMRefresh((address >> 8) & 3);
|
||||
break;
|
||||
}
|
||||
high_rom_access_time_ = ROMAccessTime((address >> 6) & 3);
|
||||
low_rom_access_time_ = ROMAccessTime((address >> 4) & 3);
|
||||
page_size_ = PageSize((address >> 2) & 3);
|
||||
switch(page_size_) {
|
||||
default:
|
||||
case PageSize::kb4:
|
||||
page_address_shift_ = 12;
|
||||
page_adddress_mask_ = 0x0fff;
|
||||
break;
|
||||
case PageSize::kb8:
|
||||
page_address_shift_ = 13;
|
||||
page_adddress_mask_ = 0x1fff;
|
||||
break;
|
||||
case PageSize::kb16:
|
||||
page_address_shift_ = 14;
|
||||
page_adddress_mask_ = 0x3fff;
|
||||
break;
|
||||
case PageSize::kb32:
|
||||
page_address_shift_ = 15;
|
||||
page_adddress_mask_ = 0x7fff;
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info().append("MEMC Control: %08x -> OS:%d sound:%d video:%d refresh:%d high:%d low:%d size:%d", address, os_mode_, sound_dma_enable_, video_dma_enable_, dynamic_ram_refresh_, high_rom_access_time_, low_rom_access_time_, page_size_);
|
||||
map_dirty_ = true;
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
|
||||
case WriteZone::IOControllers:
|
||||
if(trans) return false;
|
||||
ioc_.template write<IntT>(address, source);
|
||||
break;
|
||||
|
||||
case WriteZone::VideoController:
|
||||
if(trans) return false;
|
||||
// TODO: handle byte writes correctly.
|
||||
ioc_.video().write(source);
|
||||
break;
|
||||
|
||||
case WriteZone::AddressTranslator:
|
||||
if(trans) return false;
|
||||
// printf("Translator write at %08x; replaces %08x\n", address, pages_[address & 0x7f]);
|
||||
pages_[address & 0x7f] = address;
|
||||
map_dirty_ = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
bool read(uint32_t address, IntT &source, bool trans) {
|
||||
switch(read_zones_[(address >> 21) & 31]) {
|
||||
case ReadZone::LogicallyMappedRAM: {
|
||||
const auto item = logical_ram<IntT, true>(address, trans);
|
||||
if(item < reinterpret_cast<IntT *>(ram_.data())) {
|
||||
return false;
|
||||
}
|
||||
source = *item;
|
||||
} break;
|
||||
|
||||
case ReadZone::HighROM:
|
||||
// Real test is: require A24=A25=0, then A25=1.
|
||||
read_zones_[0] = ReadZone::LogicallyMappedRAM;
|
||||
source = high_rom<IntT>(address);
|
||||
break;
|
||||
|
||||
case ReadZone::PhysicallyMappedRAM:
|
||||
if(trans) return false;
|
||||
source = physical_ram<IntT>(address);
|
||||
break;
|
||||
|
||||
case ReadZone::LowROM:
|
||||
// logger.error().append("TODO: Low ROM read from %08x", address);
|
||||
source = IntT(~0);
|
||||
break;
|
||||
|
||||
case ReadZone::IOControllers:
|
||||
if(trans) return false;
|
||||
ioc_.template read<IntT>(address, source);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
bool read(uint32_t address, IntT &source, InstructionSet::ARM::Mode, bool trans) {
|
||||
return read(address, source, trans);
|
||||
}
|
||||
|
||||
//
|
||||
// Expose various IOC-owned things.
|
||||
//
|
||||
void tick_timers() { ioc_.tick_timers(); }
|
||||
void tick_floppy() { ioc_.tick_floppy(); }
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
ioc_.set_disk(disk, drive);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *speaker() {
|
||||
return ioc_.sound().speaker();
|
||||
}
|
||||
|
||||
auto &sound() { return ioc_.sound(); }
|
||||
const auto &sound() const { return ioc_.sound(); }
|
||||
auto &video() { return ioc_.video(); }
|
||||
const auto &video() const { return ioc_.video(); }
|
||||
auto &keyboard() { return ioc_.keyboard(); }
|
||||
const auto &keyboard() const { return ioc_.keyboard(); }
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
ioc_.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
private:
|
||||
Log::Logger<Log::Source::ARMIOC> logger;
|
||||
|
||||
enum class ReadZone {
|
||||
LogicallyMappedRAM,
|
||||
PhysicallyMappedRAM,
|
||||
IOControllers,
|
||||
LowROM,
|
||||
HighROM,
|
||||
};
|
||||
enum class WriteZone {
|
||||
LogicallyMappedRAM,
|
||||
PhysicallyMappedRAM,
|
||||
IOControllers,
|
||||
VideoController,
|
||||
DMAAndMEMC,
|
||||
AddressTranslator,
|
||||
};
|
||||
template <bool is_read>
|
||||
using Zone = std::conditional_t<is_read, ReadZone, WriteZone>;
|
||||
|
||||
template <bool is_read>
|
||||
static std::array<Zone<is_read>, 0x20> zones() {
|
||||
std::array<Zone<is_read>, 0x20> zones{};
|
||||
for(size_t c = 0; c < zones.size(); c++) {
|
||||
const auto address = c << 21;
|
||||
if(address < 0x200'0000) {
|
||||
zones[c] = Zone<is_read>::LogicallyMappedRAM;
|
||||
} else if(address < 0x300'0000) {
|
||||
zones[c] = Zone<is_read>::PhysicallyMappedRAM;
|
||||
} else if(address < 0x340'0000) {
|
||||
zones[c] = Zone<is_read>::IOControllers;
|
||||
} else if(address < 0x360'0000) {
|
||||
if constexpr (is_read) {
|
||||
zones[c] = Zone<is_read>::LowROM;
|
||||
} else {
|
||||
zones[c] = Zone<is_read>::VideoController;
|
||||
}
|
||||
} else if(address < 0x380'0000) {
|
||||
if constexpr (is_read) {
|
||||
zones[c] = Zone<is_read>::LowROM;
|
||||
} else {
|
||||
zones[c] = Zone<is_read>::DMAAndMEMC;
|
||||
}
|
||||
} else {
|
||||
if constexpr (is_read) {
|
||||
zones[c] = Zone<is_read>::HighROM;
|
||||
} else {
|
||||
zones[c] = Zone<is_read>::AddressTranslator;
|
||||
}
|
||||
}
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
bool has_moved_rom_ = false;
|
||||
std::array<uint8_t, 2*1024*1024> rom_;
|
||||
std::array<uint8_t, 4*1024*1024> ram_{};
|
||||
InputOutputController<InterruptObserverT, ClockRateObserverT> ioc_;
|
||||
|
||||
template <typename IntT>
|
||||
IntT &physical_ram(uint32_t address) {
|
||||
address = aligned<IntT>(address);
|
||||
address &= (ram_.size() - 1);
|
||||
return *reinterpret_cast<IntT *>(&ram_[address]);
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
IntT &high_rom(uint32_t address) {
|
||||
address = aligned<IntT>(address);
|
||||
return *reinterpret_cast<IntT *>(&rom_[address & (rom_.size() - 1)]);
|
||||
}
|
||||
|
||||
std::array<ReadZone, 0x20> read_zones_ = zones<true>();
|
||||
const std::array<WriteZone, 0x20> write_zones_ = zones<false>();
|
||||
|
||||
// Control register values.
|
||||
bool os_mode_ = false;
|
||||
bool sound_dma_enable_ = false;
|
||||
bool video_dma_enable_ = false; // "Unaffected" by reset, so here picked arbitrarily.
|
||||
|
||||
enum class DynamicRAMRefresh {
|
||||
None = 0b00,
|
||||
DuringFlyback = 0b01,
|
||||
Continuous = 0b11,
|
||||
} dynamic_ram_refresh_ = DynamicRAMRefresh::None; // State at reset is undefined; constrain to a valid enum value.
|
||||
|
||||
enum class ROMAccessTime {
|
||||
ns450 = 0b00,
|
||||
ns325 = 0b01,
|
||||
ns200 = 0b10,
|
||||
ns200with60nsNibble = 0b11,
|
||||
} high_rom_access_time_ = ROMAccessTime::ns450, low_rom_access_time_ = ROMAccessTime::ns450;
|
||||
|
||||
enum class PageSize {
|
||||
kb4 = 0b00,
|
||||
kb8 = 0b01,
|
||||
kb16 = 0b10,
|
||||
kb32 = 0b11,
|
||||
} page_size_ = PageSize::kb4;
|
||||
int page_address_shift_ = 12;
|
||||
uint32_t page_adddress_mask_ = 0xffff;
|
||||
|
||||
// Address translator.
|
||||
//
|
||||
// MEMC contains one entry per a physical page number, indicating where it goes logically.
|
||||
// Any logical access is tested against all 128 mappings. So that's backwards compared to
|
||||
// the ideal for an emulator, which would map from logical to physical, even if a lot more
|
||||
// compact — there are always 128 physical pages; there are up to 8192 logical pages.
|
||||
//
|
||||
// So captured here are both the physical -> logical map as representative of the real
|
||||
// hardware, and the reverse logical -> physical map, which is built (and rebuilt, and rebuilt)
|
||||
// from the other.
|
||||
|
||||
// Physical to logical mapping.
|
||||
std::array<uint32_t, 128> pages_{};
|
||||
|
||||
// Logical to physical mapping; this is divided by 'access mode'
|
||||
// (i.e. the combination of read/write, trans and OS mode flags,
|
||||
// as multipliexed by the @c mapping() function) because mapping
|
||||
// varies by mode — not just in terms of restricting access, but
|
||||
// actually presenting different memory.
|
||||
using MapTarget = std::array<uint8_t *, 8192>;
|
||||
std::array<MapTarget, 6> mapping_;
|
||||
|
||||
template <bool is_read>
|
||||
MapTarget &mapping(bool trans, bool os_mode) {
|
||||
const size_t index = (is_read ? 1 : 0) | (os_mode ? 2 : 0) | ((trans && !os_mode) ? 4 : 0);
|
||||
return mapping_[index];
|
||||
}
|
||||
|
||||
bool map_dirty_ = true;
|
||||
|
||||
/// @returns A pointer to somewhere in @c ram_ if RAM is mapped to this area, or a pointer to somewhere lower than @c ram_.data() otherwise.
|
||||
template <typename IntT, bool is_read>
|
||||
IntT *logical_ram(uint32_t address, bool trans) {
|
||||
// Possibly TODO: this recompute-if-dirty flag is supposed to ameliorate for an expensive
|
||||
// mapping process. It can be eliminated when the process is improved.
|
||||
if(map_dirty_) {
|
||||
update_mapping();
|
||||
map_dirty_ = false;
|
||||
}
|
||||
address = aligned<IntT>(address);
|
||||
address &= 0x1ff'ffff;
|
||||
const size_t page = address >> page_address_shift_;
|
||||
|
||||
const auto &map = mapping<is_read>(trans, os_mode_);
|
||||
address &= page_adddress_mask_;
|
||||
return reinterpret_cast<IntT *>(&map[page][address]);
|
||||
}
|
||||
|
||||
void update_mapping() {
|
||||
// For each physical page, project it into logical space.
|
||||
switch(page_size_) {
|
||||
default:
|
||||
case PageSize::kb4: update_mapping<PageSize::kb4>(); break;
|
||||
case PageSize::kb8: update_mapping<PageSize::kb8>(); break;
|
||||
case PageSize::kb16: update_mapping<PageSize::kb16>(); break;
|
||||
case PageSize::kb32: update_mapping<PageSize::kb32>(); break;
|
||||
}
|
||||
}
|
||||
|
||||
template <PageSize size>
|
||||
void update_mapping() {
|
||||
// Clear all logical mappings.
|
||||
for(auto &map: mapping_) {
|
||||
// Seed all pointers to an address sufficiently far lower than the beginning of RAM as to mark
|
||||
// the entire page as unmapped no matter what offset is added.
|
||||
std::fill(map.begin(), map.end(), ram_.data() - 32768);
|
||||
}
|
||||
|
||||
// For each physical page, project it into logical space
|
||||
// and store it.
|
||||
for(const auto page: pages_) {
|
||||
uint32_t physical, logical;
|
||||
|
||||
switch(size) {
|
||||
case PageSize::kb4:
|
||||
// 4kb:
|
||||
// A[6:0] -> PPN[6:0]
|
||||
// A[11:10] -> LPN[12:11]; A[22:12] -> LPN[10:0] i.e. 8192 logical pages
|
||||
physical = page & BitMask<6, 0>::value;
|
||||
|
||||
physical <<= 12;
|
||||
|
||||
logical = (page & BitMask<11, 10>::value) << 1;
|
||||
logical |= (page & BitMask<22, 12>::value) >> 12;
|
||||
break;
|
||||
|
||||
case PageSize::kb8:
|
||||
// 8kb:
|
||||
// A[0] -> PPN[6]; A[6:1] -> PPN[5:0]
|
||||
// A[11:10] -> LPN[11:10]; A[22:13] -> LPN[9:0] i.e. 4096 logical pages
|
||||
physical = (page & BitMask<0, 0>::value) << 6;
|
||||
physical |= (page & BitMask<6, 1>::value) >> 1;
|
||||
|
||||
physical <<= 13;
|
||||
|
||||
logical = page & BitMask<11, 10>::value;
|
||||
logical |= (page & BitMask<22, 13>::value) >> 13;
|
||||
break;
|
||||
|
||||
case PageSize::kb16:
|
||||
// 16kb:
|
||||
// A[1:0] -> PPN[6:5]; A[6:2] -> PPN[4:0]
|
||||
// A[11:10] -> LPN[10:9]; A[22:14] -> LPN[8:0] i.e. 2048 logical pages
|
||||
physical = (page & BitMask<1, 0>::value) << 5;
|
||||
physical |= (page & BitMask<6, 2>::value) >> 2;
|
||||
|
||||
physical <<= 14;
|
||||
|
||||
logical = (page & BitMask<11, 10>::value) >> 1;
|
||||
logical |= (page & BitMask<22, 14>::value) >> 14;
|
||||
break;
|
||||
|
||||
case PageSize::kb32:
|
||||
// 32kb:
|
||||
// A[1] -> PPN[6]; A[2] -> PPN[5]; A[0] -> PPN[4]; A[6:3] -> PPN[3:0]
|
||||
// A[11:10] -> LPN[9:8]; A[22:15] -> LPN[7:0] i.e. 1024 logical pages
|
||||
physical = (page & BitMask<1, 1>::value) << 5;
|
||||
physical |= (page & BitMask<2, 2>::value) << 3;
|
||||
physical |= (page & BitMask<0, 0>::value) << 4;
|
||||
physical |= (page & BitMask<6, 3>::value) >> 3;
|
||||
|
||||
physical <<= 15;
|
||||
|
||||
logical = (page & BitMask<11, 10>::value) >> 2;
|
||||
logical |= (page & BitMask<22, 15>::value) >> 15;
|
||||
break;
|
||||
}
|
||||
|
||||
// printf("%08x => physical %d -> logical %d\n", page, (physical >> 15), logical);
|
||||
|
||||
// TODO: consider clashes.
|
||||
// TODO: what if there's less than 4mb present?
|
||||
const auto target = &ram_[physical];
|
||||
|
||||
const auto set_supervisor = [&](bool read, bool write) {
|
||||
if(read) mapping<true>(false, false)[logical] = target;
|
||||
if(write) mapping<false>(false, false)[logical] = target;
|
||||
};
|
||||
|
||||
const auto set_os = [&](bool read, bool write) {
|
||||
if(read) mapping<true>(true, true)[logical] = target;
|
||||
if(write) mapping<false>(true, true)[logical] = target;
|
||||
};
|
||||
|
||||
const auto set_user = [&](bool read, bool write) {
|
||||
if(read) mapping<true>(true, false)[logical] = target;
|
||||
if(write) mapping<false>(true, false)[logical] = target;
|
||||
};
|
||||
|
||||
set_supervisor(true, true);
|
||||
switch((page >> 8) & 3) {
|
||||
case 0b00:
|
||||
set_os(true, true);
|
||||
set_user(true, true);
|
||||
break;
|
||||
case 0b01:
|
||||
set_os(true, true);
|
||||
set_user(true, false);
|
||||
break;
|
||||
default:
|
||||
set_os(true, false);
|
||||
set_user(false, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
211
Machines/Acorn/Archimedes/Sound.hpp
Normal file
211
Machines/Acorn/Archimedes/Sound.hpp
Normal file
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// Audio.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
// Generate lookup table for sound output levels, and hold it only once regardless
|
||||
// of how many template instantiations there are of @c Sound.
|
||||
static constexpr std::array<int16_t, 256> generate_levels() {
|
||||
std::array<int16_t, 256> result{};
|
||||
|
||||
// There are 8 segments of 16 steps; each segment is a linear
|
||||
// interpolation from its start level to its end level and
|
||||
// each level is double the previous.
|
||||
//
|
||||
// Bit 7 provides a sign.
|
||||
|
||||
for(size_t c = 0; c < 256; c++) {
|
||||
// This is the VIDC1 rule.
|
||||
// const bool is_negative = c & 128;
|
||||
// const auto point = static_cast<int>(c & 0xf);
|
||||
// const auto chord = static_cast<int>((c >> 4) & 7);
|
||||
|
||||
// VIDC2 rule, which seems to be effective. I've yet to spot the rule by which
|
||||
// VIDC1/2 is detected.
|
||||
const bool is_negative = c & 1;
|
||||
const auto point = static_cast<int>((c >> 1) & 0xf);
|
||||
const auto chord = static_cast<int>((c >> 5) & 7);
|
||||
|
||||
const int start = (1 << chord) - 1;
|
||||
const int end = (chord == 7) ? 247 : ((start << 1) + 1);
|
||||
|
||||
const int level = start * (16 - point) + end * point;
|
||||
result[c] = static_cast<int16_t>((level * 32767) / 3832);
|
||||
if(is_negative) result[c] = -result[c];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
struct SoundLevels {
|
||||
static constexpr auto levels = generate_levels();
|
||||
};
|
||||
|
||||
/// Models the Archimedes sound output; in a real machine this is a joint efort between the VIDC and the MEMC.
|
||||
template <typename InterruptObserverT>
|
||||
struct Sound: private SoundLevels {
|
||||
Sound(InterruptObserverT &observer, const uint8_t *ram) : ram_(ram), observer_(observer) {
|
||||
speaker_.set_input_rate(1'000'000);
|
||||
speaker_.set_high_frequency_cutoff(2'200.0f);
|
||||
}
|
||||
|
||||
void set_next_end(uint32_t value) {
|
||||
next_.end = value;
|
||||
}
|
||||
|
||||
void set_next_start(uint32_t value) {
|
||||
next_.start = value;
|
||||
set_buffer_valid(true); // My guess: this is triggered on next buffer start write.
|
||||
|
||||
// Definitely wrong; testing.
|
||||
// set_halted(false);
|
||||
}
|
||||
|
||||
bool interrupt() const {
|
||||
return !next_buffer_valid_;
|
||||
}
|
||||
|
||||
void swap() {
|
||||
current_.start = next_.start;
|
||||
std::swap(current_.end, next_.end);
|
||||
set_buffer_valid(false);
|
||||
set_halted(false);
|
||||
}
|
||||
|
||||
void set_frequency(uint8_t frequency) {
|
||||
divider_ = reload_ = frequency;
|
||||
}
|
||||
|
||||
void set_stereo_image(uint8_t channel, uint8_t value) {
|
||||
if(!value) {
|
||||
positions_[channel].left =
|
||||
positions_[channel].right = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
positions_[channel].right = value - 1;
|
||||
positions_[channel].left = 6 - positions_[channel].right;
|
||||
}
|
||||
|
||||
void set_dma_enabled(bool enabled) {
|
||||
dma_enabled_ = enabled;
|
||||
}
|
||||
|
||||
void tick() {
|
||||
// Write silence if not currently outputting.
|
||||
if(halted_ || !dma_enabled_) {
|
||||
post_sample(Outputs::Speaker::StereoSample());
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply user-programmed clock divider.
|
||||
--divider_;
|
||||
if(!divider_) {
|
||||
divider_ = reload_ + 2;
|
||||
|
||||
// Grab a single byte from the FIFO.
|
||||
const uint8_t raw = ram_[static_cast<std::size_t>(current_.start) + static_cast<std::size_t>(byte_)];
|
||||
sample_ = Outputs::Speaker::StereoSample( // TODO: pan, volume.
|
||||
static_cast<int16_t>((levels[raw] * positions_[byte_ & 7].left) / 6),
|
||||
static_cast<int16_t>((levels[raw] * positions_[byte_ & 7].right) / 6)
|
||||
);
|
||||
++byte_;
|
||||
|
||||
// If the FIFO is exhausted, consider triggering a DMA request.
|
||||
if(byte_ == 16) {
|
||||
byte_ = 0;
|
||||
|
||||
current_.start += 16;
|
||||
if(current_.start == current_.end) {
|
||||
if(next_buffer_valid_) {
|
||||
swap();
|
||||
} else {
|
||||
set_halted(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post_sample(sample_);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *speaker() {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
~Sound() {
|
||||
while(is_posting_.test_and_set());
|
||||
}
|
||||
|
||||
private:
|
||||
const uint8_t *ram_ = nullptr;
|
||||
|
||||
uint8_t divider_ = 0, reload_ = 0;
|
||||
int byte_ = 0;
|
||||
|
||||
void set_buffer_valid(bool valid) {
|
||||
next_buffer_valid_ = valid;
|
||||
observer_.update_interrupts();
|
||||
}
|
||||
|
||||
void set_halted(bool halted) {
|
||||
if(halted_ != halted && !halted) {
|
||||
byte_ = 0;
|
||||
divider_ = reload_;
|
||||
}
|
||||
halted_ = halted;
|
||||
}
|
||||
|
||||
bool next_buffer_valid_ = false;
|
||||
bool halted_ = true; // This is a bit of a guess.
|
||||
bool dma_enabled_ = false;
|
||||
|
||||
struct Buffer {
|
||||
uint32_t start = 0, end = 0;
|
||||
};
|
||||
Buffer current_, next_;
|
||||
|
||||
struct StereoPosition {
|
||||
// These are maintained as sixths, i.e. a value of 6 means 100%.
|
||||
int left, right;
|
||||
} positions_[8];
|
||||
|
||||
InterruptObserverT &observer_;
|
||||
Outputs::Speaker::PushLowpass<true> speaker_;
|
||||
Concurrency::AsyncTaskQueue<true> queue_;
|
||||
|
||||
void post_sample(Outputs::Speaker::StereoSample sample) {
|
||||
samples_[sample_target_][sample_pointer_++] = sample;
|
||||
if(sample_pointer_ == samples_[0].size()) {
|
||||
while(is_posting_.test_and_set());
|
||||
|
||||
const auto post_source = sample_target_;
|
||||
sample_target_ ^= 1;
|
||||
sample_pointer_ = 0;
|
||||
queue_.enqueue([this, post_source]() {
|
||||
speaker_.push(reinterpret_cast<int16_t *>(samples_[post_source].data()), samples_[post_source].size());
|
||||
is_posting_.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
std::size_t sample_pointer_ = 0;
|
||||
std::size_t sample_target_ = 0;
|
||||
Outputs::Speaker::StereoSample sample_;
|
||||
|
||||
using SampleBuffer = std::array<Outputs::Speaker::StereoSample, 4096>;
|
||||
std::array<SampleBuffer, 2> samples_;
|
||||
std::atomic_flag is_posting_ = ATOMIC_FLAG_INIT;
|
||||
};
|
||||
|
||||
}
|
||||
625
Machines/Acorn/Archimedes/Video.hpp
Normal file
625
Machines/Acorn/Archimedes/Video.hpp
Normal file
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// Video.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/03/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Archimedes {
|
||||
|
||||
template <typename InterruptObserverT, typename ClockRateObserverT, typename SoundT>
|
||||
struct Video {
|
||||
Video(InterruptObserverT &interrupt_observer, ClockRateObserverT &clock_rate_observer, SoundT &sound, const uint8_t *ram) :
|
||||
interrupt_observer_(interrupt_observer),
|
||||
clock_rate_observer_(clock_rate_observer),
|
||||
sound_(sound),
|
||||
ram_(ram),
|
||||
crt_(Outputs::Display::InputDataType::Red4Green4Blue4) {
|
||||
set_clock_divider(3);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f));
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
}
|
||||
|
||||
static constexpr uint16_t colour(uint32_t value) {
|
||||
uint8_t packed[2]{};
|
||||
packed[0] = value & 0xf;
|
||||
packed[1] = (value & 0xf0) | ((value & 0xf00) >> 8);
|
||||
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
return static_cast<uint16_t>(packed[1] | (packed[0] << 8));
|
||||
#else
|
||||
return static_cast<uint16_t>(packed[0] | (packed[1] << 8));
|
||||
#endif
|
||||
};
|
||||
static constexpr uint16_t high_spread[] = {
|
||||
colour(0b0000'0000'0000), colour(0b0000'0000'1000), colour(0b0000'0100'0000), colour(0b0000'0100'1000),
|
||||
colour(0b0000'1000'0000), colour(0b0000'1000'1000), colour(0b0000'1100'0000), colour(0b0000'1100'1000),
|
||||
colour(0b1000'0000'0000), colour(0b1000'0000'1000), colour(0b1000'0100'0000), colour(0b1000'0100'1000),
|
||||
colour(0b1000'1000'0000), colour(0b1000'1000'1000), colour(0b1000'1100'0000), colour(0b1000'1100'1000),
|
||||
};
|
||||
|
||||
void write(uint32_t value) {
|
||||
const auto target = (value >> 24) & 0xfc;
|
||||
const auto timing_value = [](uint32_t value) -> uint32_t {
|
||||
return (value >> 14) & 0x3ff;
|
||||
};
|
||||
|
||||
switch(target) {
|
||||
case 0x00: case 0x04: case 0x08: case 0x0c:
|
||||
case 0x10: case 0x14: case 0x18: case 0x1c:
|
||||
case 0x20: case 0x24: case 0x28: case 0x2c:
|
||||
case 0x30: case 0x34: case 0x38: case 0x3c:
|
||||
colours_[target >> 2] = colour(value);
|
||||
break;
|
||||
|
||||
case 0x40: border_colour_ = colour(value); break;
|
||||
|
||||
case 0x44: case 0x48: case 0x4c:
|
||||
cursor_colours_[(target - 0x40) >> 2] = colour(value);
|
||||
break;
|
||||
|
||||
case 0x80: horizontal_timing_.period = timing_value(value); break;
|
||||
case 0x84: horizontal_timing_.sync_width = timing_value(value); break;
|
||||
case 0x88: horizontal_timing_.border_start = timing_value(value); break;
|
||||
case 0x8c: horizontal_timing_.display_start = timing_value(value); break;
|
||||
case 0x90: horizontal_timing_.display_end = timing_value(value); break;
|
||||
case 0x94: horizontal_timing_.border_end = timing_value(value); break;
|
||||
case 0x98:
|
||||
horizontal_timing_.cursor_start = (value >> 13) & 0x7ff;
|
||||
cursor_shift_ = (value >> 11) & 3;
|
||||
break;
|
||||
case 0x9c: horizontal_timing_.interlace_sync_position = timing_value(value); break;
|
||||
|
||||
case 0xa0: vertical_timing_.period = timing_value(value); break;
|
||||
case 0xa4: vertical_timing_.sync_width = timing_value(value); break;
|
||||
case 0xa8: vertical_timing_.border_start = timing_value(value); break;
|
||||
case 0xac: vertical_timing_.display_start = timing_value(value); break;
|
||||
case 0xb0: vertical_timing_.display_end = timing_value(value); break;
|
||||
case 0xb4: vertical_timing_.border_end = timing_value(value); break;
|
||||
case 0xb8: vertical_timing_.cursor_start = timing_value(value); break;
|
||||
case 0xbc: vertical_timing_.cursor_end = timing_value(value); break;
|
||||
|
||||
case 0xe0:
|
||||
// Set pixel rate. This is the value that a 24Mhz clock should be divided
|
||||
// by to get half the pixel rate.
|
||||
switch(value & 0b11) {
|
||||
case 0b00: set_clock_divider(6); break; // i.e. pixel clock = 8Mhz.
|
||||
case 0b01: set_clock_divider(4); break; // 12Mhz.
|
||||
case 0b10: set_clock_divider(3); break; // 16Mhz.
|
||||
case 0b11: set_clock_divider(2); break; // 24Mhz.
|
||||
}
|
||||
|
||||
// Set colour depth.
|
||||
colour_depth_ = Depth((value >> 2) & 0b11);
|
||||
|
||||
// Crib interlace-enable.
|
||||
vertical_timing_.is_interlaced = value & (1 << 6);
|
||||
break;
|
||||
|
||||
//
|
||||
// Sound parameters.
|
||||
//
|
||||
case 0x60: case 0x64: case 0x68: case 0x6c:
|
||||
case 0x70: case 0x74: case 0x78: case 0x7c: {
|
||||
const uint8_t channel = ((value >> 26) + 7) & 7;
|
||||
sound_.set_stereo_image(channel, value & 7);
|
||||
} break;
|
||||
|
||||
case 0xc0:
|
||||
sound_.set_frequency(value & 0xff);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.error().append("TODO: unrecognised VIDC write of %08x", value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void tick() {
|
||||
// Pick new horizontal state, possibly rolling over into the vertical.
|
||||
horizontal_state_.increment_position(horizontal_timing_);
|
||||
|
||||
if(horizontal_state_.did_restart()) {
|
||||
end_horizontal();
|
||||
|
||||
const auto old_phase = vertical_state_.phase();
|
||||
vertical_state_.increment_position(vertical_timing_);
|
||||
|
||||
const auto phase = vertical_state_.phase();
|
||||
if(phase != old_phase) {
|
||||
// Copy frame and cursor start addresses into counters at the
|
||||
// start of the first vertical display line.
|
||||
if(phase == Phase::Display) {
|
||||
address_ = frame_start_;
|
||||
cursor_address_ = cursor_start_;
|
||||
|
||||
// Accumulate a count of how many times the processor has tried
|
||||
// to update the visible buffer more than once in a frame; this
|
||||
// will usually indicate that the software being run isn't properly
|
||||
// synchronised to actual machine speed.
|
||||
++frame_starts_;
|
||||
if(frame_start_sets_ > 10) {
|
||||
overages_ += frame_start_sets_ > frame_starts_;
|
||||
frame_start_sets_ = frame_starts_ = 0;
|
||||
}
|
||||
}
|
||||
if(old_phase == Phase::Display) {
|
||||
entered_flyback_ = true;
|
||||
interrupt_observer_.update_interrupts();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which next 8 bytes will be the cursor image for this line.
|
||||
// Pragmatically, updating cursor_address_ once per line avoids probable
|
||||
// errors in getting it to appear appropriately over both pixels and border.
|
||||
if(vertical_state_.cursor_active) {
|
||||
uint8_t *cursor_pixel = cursor_image_.data();
|
||||
for(int byte = 0; byte < 8; byte ++) {
|
||||
cursor_pixel[0] = (ram_[cursor_address_] >> 0) & 3;
|
||||
cursor_pixel[1] = (ram_[cursor_address_] >> 2) & 3;
|
||||
cursor_pixel[2] = (ram_[cursor_address_] >> 4) & 3;
|
||||
cursor_pixel[3] = (ram_[cursor_address_] >> 6) & 3;
|
||||
cursor_pixel += 4;
|
||||
++cursor_address_;
|
||||
}
|
||||
}
|
||||
cursor_pixel_ = 32;
|
||||
}
|
||||
|
||||
// Fetch if relevant display signals are active.
|
||||
if(vertical_state_.display_active() && horizontal_state_.display_active()) {
|
||||
const auto next_byte = [&]() {
|
||||
const auto next = ram_[address_];
|
||||
++address_;
|
||||
|
||||
// `buffer_end_` is the final address that a 16-byte block will be fetched from;
|
||||
// the +16 here papers over the fact that I'm not accurately implementing DMA.
|
||||
if(address_ == buffer_end_ + 16) {
|
||||
address_ = buffer_start_;
|
||||
}
|
||||
bitmap_queue_[bitmap_queue_pointer_ & 7] = next;
|
||||
++bitmap_queue_pointer_;
|
||||
};
|
||||
|
||||
switch(colour_depth_) {
|
||||
case Depth::EightBPP: next_byte(); next_byte(); break;
|
||||
case Depth::FourBPP: next_byte(); break;
|
||||
case Depth::TwoBPP: if(!(pixel_count_&3)) next_byte(); break;
|
||||
case Depth::OneBPP: if(!(pixel_count_&7)) next_byte(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Move along line.
|
||||
switch(vertical_state_.phase()) {
|
||||
case Phase::Sync: tick_horizontal<Phase::Sync>(); break;
|
||||
case Phase::Blank: tick_horizontal<Phase::Blank>(); break;
|
||||
case Phase::Border: tick_horizontal<Phase::Border>(); break;
|
||||
case Phase::Display: tick_horizontal<Phase::Display>(); break;
|
||||
case Phase::StartInterlacedSync: tick_horizontal<Phase::StartInterlacedSync>(); break;
|
||||
case Phase::EndInterlacedSync: tick_horizontal<Phase::EndInterlacedSync>(); break;
|
||||
}
|
||||
++time_in_phase_;
|
||||
}
|
||||
|
||||
/// @returns @c true if a vertical retrace interrupt has been signalled since the last call to @c interrupt(); @c false otherwise.
|
||||
bool interrupt() {
|
||||
// Guess: edge triggered?
|
||||
const bool interrupt = entered_flyback_;
|
||||
entered_flyback_ = false;
|
||||
return interrupt;
|
||||
}
|
||||
|
||||
bool flyback_active() const {
|
||||
return vertical_state_.phase() != Phase::Display;
|
||||
}
|
||||
|
||||
void set_frame_start(uint32_t address) {
|
||||
frame_start_ = address;
|
||||
++frame_start_sets_;
|
||||
}
|
||||
void set_buffer_start(uint32_t address) { buffer_start_ = address; }
|
||||
void set_buffer_end(uint32_t address) { buffer_end_ = address; }
|
||||
void set_cursor_start(uint32_t address) { cursor_start_ = address; }
|
||||
|
||||
Outputs::CRT::CRT &crt() { return crt_; }
|
||||
const Outputs::CRT::CRT &crt() const { return crt_; }
|
||||
|
||||
int clock_divider() const {
|
||||
return static_cast<int>(clock_divider_);
|
||||
}
|
||||
|
||||
int frame_rate_overages() const {
|
||||
return overages_;
|
||||
}
|
||||
|
||||
private:
|
||||
Log::Logger<Log::Source::ARMIOC> logger;
|
||||
InterruptObserverT &interrupt_observer_;
|
||||
ClockRateObserverT &clock_rate_observer_;
|
||||
SoundT &sound_;
|
||||
|
||||
// In the current version of this code, video DMA occurrs costlessly,
|
||||
// being deferred to the component itself.
|
||||
const uint8_t *ram_ = nullptr;
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
// Horizontal and vertical timing.
|
||||
struct Timing {
|
||||
uint32_t period = 0;
|
||||
uint32_t sync_width = 0;
|
||||
uint32_t border_start = 0;
|
||||
uint32_t border_end = 0;
|
||||
uint32_t display_start = 0;
|
||||
uint32_t display_end = 0;
|
||||
uint32_t cursor_start = 0;
|
||||
uint32_t cursor_end = 0;
|
||||
|
||||
uint32_t interlace_sync_position = 0;
|
||||
bool is_interlaced = false;
|
||||
};
|
||||
uint32_t cursor_shift_ = 0;
|
||||
Timing horizontal_timing_, vertical_timing_;
|
||||
|
||||
enum class Depth {
|
||||
OneBPP = 0b00,
|
||||
TwoBPP = 0b01,
|
||||
FourBPP = 0b10,
|
||||
EightBPP = 0b11,
|
||||
};
|
||||
|
||||
// Current video state.
|
||||
enum class Phase {
|
||||
Sync, Blank, Border, Display, StartInterlacedSync, EndInterlacedSync,
|
||||
};
|
||||
template <bool is_vertical>
|
||||
struct State {
|
||||
uint32_t position = 0;
|
||||
uint32_t display_start = 0;
|
||||
uint32_t display_end = 0;
|
||||
|
||||
bool is_odd_iteration_ = false;
|
||||
|
||||
void increment_position(const Timing &timing) {
|
||||
const auto previous_override = interlace_override_;
|
||||
if constexpr (is_vertical) {
|
||||
interlace_override_ = Phase::Sync; // i.e. no override.
|
||||
}
|
||||
if(position == timing.sync_width) {
|
||||
state |= SyncEnded;
|
||||
if(is_vertical && timing.is_interlaced && is_odd_iteration_ && previous_override == Phase::Sync) {
|
||||
--position;
|
||||
interlace_override_ = Phase::EndInterlacedSync;
|
||||
}
|
||||
}
|
||||
if(position == timing.display_start) { state |= DisplayStarted; display_start = position; }
|
||||
if(position == timing.display_end) { state |= DisplayEnded; display_end = position; }
|
||||
if(position == timing.border_start) state |= BorderStarted;
|
||||
if(position == timing.border_end) state |= BorderEnded;
|
||||
|
||||
cursor_active |= position == timing.cursor_start;
|
||||
cursor_active &= position != timing.cursor_end;
|
||||
|
||||
if(position == timing.period) {
|
||||
state = DidRestart;
|
||||
position = 0;
|
||||
is_odd_iteration_ ^= true;
|
||||
|
||||
// Both display start and end need to be seeded as bigger than can be reached,
|
||||
// while having some overhead for addition.
|
||||
display_end = display_start = std::numeric_limits<uint32_t>::max() >> 1;
|
||||
|
||||
// Possibly label the next as a start-of-interlaced-sync.
|
||||
if(is_vertical && timing.is_interlaced && is_odd_iteration_) {
|
||||
interlace_override_ = Phase::StartInterlacedSync;
|
||||
}
|
||||
} else {
|
||||
++position;
|
||||
if(position == 1024) position = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_outputting(Depth depth) const {
|
||||
return position >= display_start + output_latencies[static_cast<uint32_t>(depth)] && position < display_end + output_latencies[static_cast<uint32_t>(depth)];
|
||||
}
|
||||
|
||||
uint32_t output_cycle(Depth depth) const {
|
||||
return position - display_start - output_latencies[static_cast<uint32_t>(depth)];
|
||||
}
|
||||
|
||||
static constexpr uint32_t output_latencies[] = {
|
||||
19 >> 1, // 1 bpp.
|
||||
11 >> 1, // 2 bpp.
|
||||
7 >> 1, // 4 bpp.
|
||||
5 >> 1 // 8 bpp.
|
||||
};
|
||||
|
||||
static constexpr uint8_t SyncEnded = 0x1;
|
||||
static constexpr uint8_t BorderStarted = 0x2;
|
||||
static constexpr uint8_t BorderEnded = 0x4;
|
||||
static constexpr uint8_t DisplayStarted = 0x8;
|
||||
static constexpr uint8_t DisplayEnded = 0x10;
|
||||
static constexpr uint8_t DidRestart = 0x20;
|
||||
uint8_t state = 0;
|
||||
Phase interlace_override_ = Phase::Sync;
|
||||
|
||||
bool cursor_active = false;
|
||||
|
||||
bool did_restart() {
|
||||
const bool result = state & DidRestart;
|
||||
state &= ~DidRestart;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool display_active() const {
|
||||
return (state & DisplayStarted) && !(state & DisplayEnded);
|
||||
}
|
||||
|
||||
Phase phase(Phase horizontal_fallback = Phase::Border) const {
|
||||
if(is_vertical && interlace_override_ != Phase::Sync) {
|
||||
return interlace_override_;
|
||||
}
|
||||
// TODO: turn the following logic into a lookup table.
|
||||
if(!(state & SyncEnded)) {
|
||||
return Phase::Sync;
|
||||
}
|
||||
if(!(state & BorderStarted) || (state & BorderEnded)) {
|
||||
return Phase::Blank;
|
||||
}
|
||||
if constexpr (!is_vertical) {
|
||||
return horizontal_fallback;
|
||||
}
|
||||
|
||||
if(!(state & DisplayStarted) || (state & DisplayEnded)) {
|
||||
return Phase::Border;
|
||||
}
|
||||
return Phase::Display;
|
||||
}
|
||||
};
|
||||
State<false> horizontal_state_;
|
||||
State<true> vertical_state_;
|
||||
|
||||
int time_in_phase_ = 0;
|
||||
Phase phase_;
|
||||
uint16_t phased_border_colour_;
|
||||
|
||||
int pixel_count_ = 0;
|
||||
int display_area_start_ = 0;
|
||||
uint16_t *pixels_ = nullptr;
|
||||
|
||||
// It is elsewhere assumed that this size is a multiple of 8.
|
||||
static constexpr size_t PixelBufferSize = 256;
|
||||
|
||||
// Programmer-set addresses.
|
||||
uint32_t buffer_start_ = 0;
|
||||
uint32_t buffer_end_ = 0;
|
||||
uint32_t frame_start_ = 0;
|
||||
uint32_t cursor_start_ = 0;
|
||||
|
||||
int frame_start_sets_ = 0;
|
||||
int frame_starts_ = 0;
|
||||
int overages_ = 0;
|
||||
|
||||
// Ephemeral address state.
|
||||
uint32_t address_ = 0;
|
||||
|
||||
// Horizontal cursor output state.
|
||||
uint32_t cursor_address_ = 0;
|
||||
int cursor_pixel_ = 0;
|
||||
std::array<uint8_t, 32> cursor_image_;
|
||||
|
||||
// Colour palette, converted to internal format.
|
||||
uint16_t border_colour_;
|
||||
std::array<uint16_t, 16> colours_{};
|
||||
std::array<uint16_t, 4> cursor_colours_{};
|
||||
|
||||
// An interrupt flag; more closely related to the interface by which
|
||||
// my implementation of the IOC picks up an interrupt request than
|
||||
// to hardware.
|
||||
bool entered_flyback_ = false;
|
||||
|
||||
// The divider that would need to be applied to a 24Mhz clock to
|
||||
// get half the current pixel clock; counting is in units of half
|
||||
// the pixel clock because that's the fidelity at which the programmer
|
||||
// places horizontal events — display start, end, sync period, etc.
|
||||
uint32_t clock_divider_ = 0;
|
||||
Depth colour_depth_;
|
||||
|
||||
// A temporary buffer that holds video contents during the latency
|
||||
// period between their generation and their output.
|
||||
uint8_t bitmap_queue_[8];
|
||||
int bitmap_queue_pointer_ = 0;
|
||||
|
||||
void set_clock_divider(uint32_t divider) {
|
||||
if(divider == clock_divider_) {
|
||||
return;
|
||||
}
|
||||
|
||||
clock_divider_ = divider;
|
||||
const auto cycles_per_line = static_cast<int>(24'000'000 / (divider * 312 * 50));
|
||||
crt_.set_new_timing(
|
||||
cycles_per_line,
|
||||
312, /* Height of display. */
|
||||
Outputs::CRT::PAL::ColourSpace,
|
||||
Outputs::CRT::PAL::ColourCycleNumerator,
|
||||
Outputs::CRT::PAL::ColourCycleDenominator,
|
||||
Outputs::CRT::PAL::VerticalSyncLength,
|
||||
Outputs::CRT::PAL::AlternatesPhase);
|
||||
clock_rate_observer_.update_clock_rates();
|
||||
}
|
||||
|
||||
void flush_pixels() {
|
||||
crt_.output_data(time_in_phase_, static_cast<size_t>(pixel_count_));
|
||||
time_in_phase_ = 0;
|
||||
pixel_count_ = 0;
|
||||
pixels_ = nullptr;
|
||||
}
|
||||
|
||||
void set_phase(Phase phase) {
|
||||
if(time_in_phase_) {
|
||||
switch(phase_) {
|
||||
default: crt_.output_sync(time_in_phase_); break;
|
||||
case Phase::Blank: crt_.output_blank(time_in_phase_); break;
|
||||
case Phase::Border: crt_.output_level<uint16_t>(time_in_phase_, phased_border_colour_); break;
|
||||
case Phase::Display: flush_pixels(); break;
|
||||
}
|
||||
}
|
||||
|
||||
phase_ = phase;
|
||||
time_in_phase_ = 0;
|
||||
phased_border_colour_ = border_colour_;
|
||||
pixel_count_ = 0;
|
||||
}
|
||||
|
||||
void end_horizontal() {
|
||||
set_phase(Phase::Sync);
|
||||
display_area_start_ = -1;
|
||||
bitmap_queue_pointer_ = 0;
|
||||
}
|
||||
|
||||
template <Phase vertical_phase> void tick_horizontal() {
|
||||
// Sync lines: obey nothing. All sync, all the time.
|
||||
if constexpr (vertical_phase == Phase::Sync) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start interlaced sync lines: do blank from horizontal sync up to the programmed
|
||||
// cutoff, then do sync.
|
||||
if constexpr (vertical_phase == Phase::StartInterlacedSync) {
|
||||
if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) {
|
||||
set_phase(Phase::Blank);
|
||||
}
|
||||
if(phase_ == Phase::Blank && horizontal_state_.position == horizontal_timing_.interlace_sync_position) {
|
||||
set_phase(Phase::Sync);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// End interlaced sync lines: do sync up to the programmed cutoff, then do blank.
|
||||
if constexpr (vertical_phase == Phase::EndInterlacedSync) {
|
||||
if(phase_ == Phase::Sync && horizontal_state_.position == horizontal_timing_.interlace_sync_position) {
|
||||
set_phase(Phase::Blank);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Blank lines: obey only the transition from sync to non-sync.
|
||||
if constexpr (vertical_phase == Phase::Blank) {
|
||||
if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) {
|
||||
set_phase(Phase::Blank);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Border lines: ignore display phases; also reset the border phase if the colour changes.
|
||||
if constexpr (vertical_phase == Phase::Border) {
|
||||
const auto phase = horizontal_state_.phase(Phase::Border);
|
||||
if(phase != phase_ || (phase_ == Phase::Border && border_colour_ != phased_border_colour_)) {
|
||||
set_phase(phase);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (vertical_phase != Phase::Display) {
|
||||
// Should be impossible.
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// Some timing facts, to explain what would otherwise be magic constants.
|
||||
static constexpr int CursorDelay = 5; // The cursor will appear six pixels after its programmed trigger point.
|
||||
// ... BUT! Border and display are currently a pixel early. So move the
|
||||
// cursor for alignment.
|
||||
|
||||
// Deal with sync and blank via set_phase(); collapse display and border into Phase::Display.
|
||||
const auto phase = horizontal_state_.phase(Phase::Display);
|
||||
if(phase != phase_) set_phase(phase);
|
||||
|
||||
// Update cursor pixel counter if applicable; this might mean triggering it
|
||||
// and it might just mean advancing it if it has already been triggered.
|
||||
cursor_pixel_ += 2;
|
||||
if(vertical_state_.cursor_active) {
|
||||
const auto pixel_position = horizontal_state_.position << 1;
|
||||
if(pixel_position <= horizontal_timing_.cursor_start && (pixel_position + 2) > horizontal_timing_.cursor_start) {
|
||||
cursor_pixel_ = int(horizontal_timing_.cursor_start) - int(pixel_position) - CursorDelay;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is not [collapsed] Phase::Display, just stop here.
|
||||
if(phase_ != Phase::Display) return;
|
||||
|
||||
// Display phase: maintain an output buffer (if available).
|
||||
if(pixel_count_ == PixelBufferSize) flush_pixels();
|
||||
if(!pixel_count_) pixels_ = reinterpret_cast<uint16_t *>(crt_.begin_data(PixelBufferSize));
|
||||
|
||||
// Output.
|
||||
if(pixels_) {
|
||||
// Paint the border colour for potential painting over.
|
||||
|
||||
if(horizontal_state_.is_outputting(colour_depth_)) {
|
||||
const auto source = horizontal_state_.output_cycle(colour_depth_);
|
||||
|
||||
// TODO: all below should be delayed an extra pixel. As should the border, actually. Fix up externally?
|
||||
switch(colour_depth_) {
|
||||
case Depth::EightBPP: {
|
||||
const uint8_t *bitmap = &bitmap_queue_[(source << 1) & 7];
|
||||
pixels_[0] = (colours_[bitmap[0] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[0] >> 4];
|
||||
pixels_[1] = (colours_[bitmap[1] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[1] >> 4];
|
||||
} break;
|
||||
|
||||
case Depth::FourBPP:
|
||||
pixels_[0] = colours_[bitmap_queue_[source & 7] & 0xf];
|
||||
pixels_[1] = colours_[bitmap_queue_[source & 7] >> 4];
|
||||
break;
|
||||
|
||||
case Depth::TwoBPP: {
|
||||
uint8_t &bitmap = bitmap_queue_[(source >> 1) & 7];
|
||||
pixels_[0] = colours_[bitmap & 3];
|
||||
pixels_[1] = colours_[(bitmap >> 2) & 3];
|
||||
bitmap >>= 4;
|
||||
} break;
|
||||
|
||||
case Depth::OneBPP: {
|
||||
uint8_t &bitmap = bitmap_queue_[(source >> 2) & 7];
|
||||
pixels_[0] = colours_[bitmap & 1];
|
||||
pixels_[1] = colours_[(bitmap >> 1) & 1];
|
||||
bitmap >>= 2;
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
pixels_[0] = pixels_[1] = border_colour_;
|
||||
}
|
||||
|
||||
// Overlay cursor if applicable.
|
||||
if(cursor_pixel_ < 32) {
|
||||
if(cursor_pixel_ >= 0) {
|
||||
const auto pixel = cursor_image_[static_cast<size_t>(cursor_pixel_)];
|
||||
if(pixel) {
|
||||
pixels_[0] = cursor_colours_[pixel];
|
||||
}
|
||||
}
|
||||
if(cursor_pixel_ >= -1 && cursor_pixel_ < 31) {
|
||||
const auto pixel = cursor_image_[static_cast<size_t>(cursor_pixel_ + 1)];
|
||||
if(pixel) {
|
||||
pixels_[1] = cursor_colours_[pixel];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pixels_ += 2;
|
||||
}
|
||||
|
||||
pixel_count_ += 2;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -8,24 +8,24 @@
|
||||
|
||||
#include "Electron.hpp"
|
||||
|
||||
#include "../../Activity/Source.hpp"
|
||||
#include "../MachineTypes.hpp"
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../../Activity/Source.hpp"
|
||||
#include "../../MachineTypes.hpp"
|
||||
#include "../../../Configurable/Configurable.hpp"
|
||||
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../Configurable/StandardOptions.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../Processors/6502/6502.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../../Configurable/StandardOptions.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../../Processors/6502/6502.hpp"
|
||||
|
||||
#include "../../Storage/MassStorage/SCSI/SCSI.hpp"
|
||||
#include "../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
|
||||
#include "../../Storage/Tape/Tape.hpp"
|
||||
#include "../../../Storage/MassStorage/SCSI/SCSI.hpp"
|
||||
#include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
|
||||
#include "../../../Storage/Tape/Tape.hpp"
|
||||
|
||||
#include "../Utility/Typer.hpp"
|
||||
#include "../../Analyser/Static/Acorn/Target.hpp"
|
||||
#include "../../Utility/Typer.hpp"
|
||||
#include "../../../Analyser/Static/Acorn/Target.hpp"
|
||||
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
#include "../../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "Interrupts.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
@@ -796,5 +796,3 @@ std::unique_ptr<Machine> Machine::Electron(const Analyser::Static::Target *targe
|
||||
return std::make_unique<Electron::ConcreteMachine<true>>(*acorn_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../Configurable/StandardOptions.hpp"
|
||||
#include "../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../ROMMachine.hpp"
|
||||
#include "../../../Configurable/Configurable.hpp"
|
||||
#include "../../../Configurable/StandardOptions.hpp"
|
||||
#include "../../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Electron {
|
||||
*/
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
virtual ~Machine() = default;
|
||||
|
||||
/// Creates and returns an Electron.
|
||||
static std::unique_ptr<Machine> Electron(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../KeyboardMachine.hpp"
|
||||
#include "../Utility/Typer.hpp"
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
#include "../../Utility/Typer.hpp"
|
||||
|
||||
namespace Electron {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Components/1770/1770.hpp"
|
||||
#include "../../Activity/Observer.hpp"
|
||||
#include "../../../Components/1770/1770.hpp"
|
||||
#include "../../../Activity/Observer.hpp"
|
||||
|
||||
namespace Electron {
|
||||
|
||||
54
Machines/Acorn/Electron/SoundGenerator.cpp
Normal file
54
Machines/Acorn/Electron/SoundGenerator.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Speaker.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/12/2016.
|
||||
// Copyright 2016 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "SoundGenerator.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
using namespace Electron;
|
||||
|
||||
SoundGenerator::SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void SoundGenerator::set_sample_volume_range(std::int16_t range) {
|
||||
volume_ = unsigned(range / 2);
|
||||
}
|
||||
|
||||
template <Outputs::Speaker::Action action>
|
||||
void SoundGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
if constexpr (action == Outputs::Speaker::Action::Ignore) {
|
||||
counter_ = (counter_ + number_of_samples) % ((divider_+1) * 2);
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_enabled_) {
|
||||
while(number_of_samples--) {
|
||||
Outputs::Speaker::apply<action>(*target, Outputs::Speaker::MonoSample((counter_ / (divider_+1)) * volume_));
|
||||
target++;
|
||||
counter_ = (counter_ + 1) % ((divider_+1) * 2);
|
||||
}
|
||||
} else {
|
||||
Outputs::Speaker::fill<action>(target, target + number_of_samples, Outputs::Speaker::MonoSample(0));
|
||||
}
|
||||
}
|
||||
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void SoundGenerator::set_divider(uint8_t divider) {
|
||||
audio_queue_.enqueue([this, divider]() {
|
||||
divider_ = divider * 32 / clock_rate_divider;
|
||||
});
|
||||
}
|
||||
|
||||
void SoundGenerator::set_is_enabled(bool is_enabled) {
|
||||
audio_queue_.enqueue([this, is_enabled]() {
|
||||
is_enabled_ = is_enabled;
|
||||
counter_ = 0;
|
||||
});
|
||||
}
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "../../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
namespace Electron {
|
||||
|
||||
class SoundGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
class SoundGenerator: public ::Outputs::Speaker::BufferSource<SoundGenerator, false> {
|
||||
public:
|
||||
SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
|
||||
@@ -23,11 +23,10 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
|
||||
static constexpr unsigned int clock_rate_divider = 8;
|
||||
|
||||
// To satisfy ::SampleSource.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void skip_samples(std::size_t number_of_samples);
|
||||
// For BufferSource.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../Storage/Tape/Tape.hpp"
|
||||
#include "../../Storage/Tape/Parsers/Acorn.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../Storage/Tape/Tape.hpp"
|
||||
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
|
||||
#include "Interrupts.hpp"
|
||||
|
||||
namespace Electron {
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "Interrupts.hpp"
|
||||
|
||||
#include <vector>
|
||||
@@ -221,7 +221,7 @@ class ConcreteMachine:
|
||||
// MARK: - MachineTypes::MouseMachine.
|
||||
|
||||
Inputs::Mouse &get_mouse() final {
|
||||
return chipset_.get_mouse();;
|
||||
return chipset_.get_mouse();
|
||||
}
|
||||
|
||||
// MARK: - MachineTypes::JoystickMachine.
|
||||
@@ -256,5 +256,3 @@ std::unique_ptr<Machine> Machine::Amiga(const Analyser::Static::Target *target,
|
||||
const Target *const amiga_target = dynamic_cast<const Target *>(target);
|
||||
return std::make_unique<Amiga::ConcreteMachine>(*amiga_target, rom_fetcher);
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Amiga {
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
virtual ~Machine() = default;
|
||||
|
||||
/// Creates and returns an Amiga.
|
||||
static std::unique_ptr<Machine> Amiga(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
|
||||
@@ -28,9 +28,6 @@ template <bool record_bus = false> class Blitter: public DMADevice<4, 4> {
|
||||
using DMADevice::DMADevice;
|
||||
|
||||
template <int id, int shift> void set_pointer(uint16_t value) {
|
||||
if(get_status() & 0x4000) {
|
||||
printf(">>>");
|
||||
}
|
||||
DMADevice<4, 4>::set_pointer<id, shift>(value);
|
||||
}
|
||||
|
||||
@@ -65,7 +62,7 @@ template <bool record_bus = false> class Blitter: public DMADevice<4, 4> {
|
||||
uint32_t address = 0;
|
||||
uint16_t value = 0;
|
||||
|
||||
Transaction() {}
|
||||
Transaction() = default;
|
||||
Transaction(Type type) : type(type) {}
|
||||
Transaction(Type type, uint32_t address, uint16_t value) : type(type), address(address), value(value) {}
|
||||
|
||||
|
||||
@@ -1293,5 +1293,3 @@ std::unique_ptr<Machine> Machine::AmstradCPC(const Analyser::Static::Target *tar
|
||||
case Target::Model::CPC464: return std::make_unique<AmstradCPC::ConcreteMachine<false>>(*cpc_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace AmstradCPC {
|
||||
*/
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
virtual ~Machine() = default;
|
||||
|
||||
/// Creates and returns an Amstrad CPC.
|
||||
static std::unique_ptr<Machine> AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
|
||||
@@ -34,7 +34,7 @@ struct Command {
|
||||
uint8_t device = AllDevices;
|
||||
uint8_t reg = NoRegister;
|
||||
|
||||
Command() {}
|
||||
Command() = default;
|
||||
Command(Type type) : type(type) {}
|
||||
Command(Type type, uint8_t device) : type(type), device(device) {}
|
||||
Command(Type type, uint8_t device, uint8_t reg) : type(type), device(device), reg(reg) {}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
using namespace Apple::ADB;
|
||||
|
||||
namespace {
|
||||
Log::Logger<Log::Source::ADBDevice> logger;
|
||||
[[maybe_unused]] Log::Logger<Log::Source::ADBDevice> logger;
|
||||
}
|
||||
|
||||
ReactiveDevice::ReactiveDevice(Apple::ADB::Bus &bus, uint8_t adb_device_id) :
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
|
||||
#include "../../../Processors/6502/6502.hpp"
|
||||
#include "../../../Components/AudioToggle/AudioToggle.hpp"
|
||||
#include "../../../Components/AY38910/AY38910.hpp"
|
||||
|
||||
#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
@@ -24,6 +26,7 @@
|
||||
#include "DiskIICard.hpp"
|
||||
#include "Joystick.hpp"
|
||||
#include "LanguageCardSwitches.hpp"
|
||||
#include "Mockingboard.hpp"
|
||||
#include "SCSICard.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
@@ -41,17 +44,71 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int DiskIISlot = 6; // Apple recommended slot 6 for the (first) Disk II.
|
||||
constexpr int SCSISlot = 7; // Install the SCSI card in slot 7, to one-up any connected Disk II.
|
||||
constexpr int DiskIISlot = 6; // Apple recommended slot 6 for the (first) Disk II.
|
||||
constexpr int SCSISlot = 7; // Install the SCSI card in slot 7, to one-up any connected Disk II.
|
||||
constexpr int MockingboardSlot = 4; // Conventional Mockingboard slot.
|
||||
|
||||
|
||||
// The system's master clock rate.
|
||||
//
|
||||
// Quick note on this:
|
||||
//
|
||||
// * 64 out of 65 CPU cycles last for 14 cycles of the master clock;
|
||||
// * every 65th cycle lasts for 16 cycles of the master clock;
|
||||
// * that keeps CPU cycles in-phase with the colour subcarrier: each line of output is 64*14 + 16 = 912 master cycles long;
|
||||
// * ... and since hsync is placed to make each line 228 colour clocks long that means 4 master clocks per colour clock;
|
||||
// * ... which is why all Apple II video modes are expressible as four binary outputs per colour clock;
|
||||
// * ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc.
|
||||
constexpr float master_clock = 14318180.0;
|
||||
|
||||
/// Provides an AY that runs at the CPU rate divided by 4 given an input of the master clock divided by 2,
|
||||
/// allowing for stretched CPU clock cycles.
|
||||
struct StretchedAYPair:
|
||||
Apple::II::AYPair,
|
||||
public Outputs::Speaker::BufferSource<StretchedAYPair, true> {
|
||||
|
||||
using AYPair::AYPair;
|
||||
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target) {
|
||||
|
||||
// (1) take 64 windows of 7 input cycles followed by one window of 8 input cycles;
|
||||
// (2) after each four windows, advance the underlying AY.
|
||||
//
|
||||
// i.e. advance after:
|
||||
//
|
||||
// * 28 cycles, {16 times, then 15 times, then 15 times, then 15 times};
|
||||
// * 29 cycles, once.
|
||||
//
|
||||
// so:
|
||||
// 16, 1; 15, 1; 15, 1; 15, 1
|
||||
//
|
||||
// i.e. add an extra one on the 17th, 33rd, 49th and 65th ticks in a 65-tick loop.
|
||||
for(std::size_t c = 0; c < number_of_samples; c++) {
|
||||
++subdivider_;
|
||||
if(subdivider_ == 28) {
|
||||
++phase_;
|
||||
subdivider_ = (phase_ & 15) ? 0 : -1;
|
||||
if(phase_ == 65) phase_ = 0;
|
||||
|
||||
advance();
|
||||
}
|
||||
|
||||
target[c] = level();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int phase_ = 0;
|
||||
int subdivider_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
#define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe))
|
||||
|
||||
template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
template <Analyser::Static::AppleII::Target::Model model, bool has_mockingboard> class ConcreteMachine:
|
||||
public Apple::II::Machine,
|
||||
public MachineTypes::TimedMachine,
|
||||
public MachineTypes::ScanProducer,
|
||||
@@ -83,14 +140,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
false>;
|
||||
Processor m6502_;
|
||||
VideoBusHandler video_bus_handler_;
|
||||
Apple::II::Video::Video<VideoBusHandler, is_iie()> video_;
|
||||
Apple::II::Video::Video<VideoBusHandler, is_iie(model)> video_;
|
||||
int cycles_into_current_line_ = 0;
|
||||
Cycles cycles_since_video_update_;
|
||||
|
||||
void update_video() {
|
||||
video_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
static constexpr int audio_divider = 8;
|
||||
static constexpr int audio_divider = has_mockingboard ? 1 : 8;
|
||||
void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
|
||||
}
|
||||
@@ -109,9 +166,23 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Outputs::Speaker::PullLowpass<Audio::Toggle> speaker_;
|
||||
StretchedAYPair ays_;
|
||||
using SourceT =
|
||||
std::conditional_t<has_mockingboard, Outputs::Speaker::CompoundSource<StretchedAYPair, Audio::Toggle>, Audio::Toggle>;
|
||||
using LowpassT = Outputs::Speaker::PullLowpass<SourceT>;
|
||||
|
||||
Outputs::Speaker::CompoundSource<StretchedAYPair, Audio::Toggle> mixer_;
|
||||
Outputs::Speaker::PullLowpass<SourceT> speaker_;
|
||||
Cycles cycles_since_audio_update_;
|
||||
|
||||
constexpr SourceT &lowpass_source() {
|
||||
if constexpr (has_mockingboard) {
|
||||
return mixer_;
|
||||
} else {
|
||||
return audio_toggle_;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cards
|
||||
static constexpr size_t NoActiveCard = 7; // There is no 'card 0' in internal numbering.
|
||||
size_t active_card_ = NoActiveCard;
|
||||
@@ -155,6 +226,24 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
void card_did_change_interrupt_flags(Apple::II::Card *) final {
|
||||
bool nmi = false;
|
||||
bool irq = false;
|
||||
|
||||
for(const auto &card: cards_) {
|
||||
if(card) {
|
||||
nmi |= card->nmi();
|
||||
irq |= card->irq();
|
||||
}
|
||||
}
|
||||
m6502_.set_nmi_line(nmi);
|
||||
m6502_.set_irq_line(irq);
|
||||
}
|
||||
|
||||
Apple::II::Mockingboard *mockingboard() {
|
||||
return dynamic_cast<Apple::II::Mockingboard *>(cards_[MockingboardSlot - 1].get());
|
||||
}
|
||||
|
||||
Apple::II::DiskIICard *diskii_card() {
|
||||
return dynamic_cast<Apple::II::DiskIICard *>(cards_[DiskIISlot - 1].get());
|
||||
}
|
||||
@@ -225,7 +314,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
const auto zero_state = auxiliary_switches_.zero_state();
|
||||
|
||||
uint8_t *const ram = zero_state ? aux_ram_ : ram_;
|
||||
uint8_t *const rom = is_iie() ? &rom_[3840] : rom_.data();
|
||||
uint8_t *const rom = is_iie(model) ? &rom_[3840] : rom_.data();
|
||||
|
||||
// Which way the region here is mapped to be banks 1 and 2 is
|
||||
// arbitrary.
|
||||
@@ -273,7 +362,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
// MARK: - Keyboard and typing.
|
||||
|
||||
struct Keyboard: public Inputs::Keyboard {
|
||||
Keyboard(Processor *m6502) : m6502_(m6502) {}
|
||||
Keyboard(Processor &m6502, AuxiliaryMemorySwitches<ConcreteMachine> &switches) : m6502_(m6502), auxiliary_switches_(switches) {}
|
||||
|
||||
void reset_all_keys() final {
|
||||
open_apple_is_pressed =
|
||||
@@ -286,7 +375,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
bool set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat) final {
|
||||
if constexpr (!is_iie()) {
|
||||
if constexpr (!is_iie(model)) {
|
||||
if(is_repeat && !repeat_is_pressed_) return true;
|
||||
}
|
||||
|
||||
@@ -297,7 +386,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace:
|
||||
if(is_iie()) {
|
||||
if(is_iie(model)) {
|
||||
value = 0x7f;
|
||||
break;
|
||||
} else {
|
||||
@@ -305,7 +394,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
case Key::Enter: value = 0x0d; break;
|
||||
case Key::Tab:
|
||||
if (is_iie()) {
|
||||
if (is_iie(model)) {
|
||||
value = '\t';
|
||||
break;
|
||||
} else {
|
||||
@@ -316,7 +405,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightMeta:
|
||||
if (is_iie()) {
|
||||
if (is_iie(model)) {
|
||||
open_apple_is_pressed = is_pressed;
|
||||
return true;
|
||||
} else {
|
||||
@@ -325,7 +414,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
case Key::RightOption:
|
||||
case Key::LeftMeta:
|
||||
if (is_iie()) {
|
||||
if (is_iie(model)) {
|
||||
closed_apple_is_pressed = is_pressed;
|
||||
return true;
|
||||
} else {
|
||||
@@ -346,7 +435,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case Key::F9: case Key::F10: case Key::F11:
|
||||
repeat_is_pressed_ = is_pressed;
|
||||
|
||||
if constexpr (!is_iie()) {
|
||||
if constexpr (!is_iie(model)) {
|
||||
if(is_pressed && (!is_repeat || character_is_pressed_)) {
|
||||
keyboard_input_ = uint8_t(last_pressed_character_ | 0x80);
|
||||
}
|
||||
@@ -365,7 +454,10 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
// Accept a bunch non-symbolic other keys, as
|
||||
// reset, in the hope that the user can find
|
||||
// at least one usable key.
|
||||
m6502_->set_reset_line(is_pressed);
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
if(!is_pressed) {
|
||||
auxiliary_switches_.reset();
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
@@ -374,12 +466,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = char(toupper(value));
|
||||
if(!is_iie(model)) value = char(toupper(value));
|
||||
|
||||
if(control_is_pressed_ && isalpha(value)) value &= 0xbf;
|
||||
|
||||
// TODO: properly map IIe keys
|
||||
if(!is_iie() && shift_is_pressed_) {
|
||||
if(!is_iie(model) && shift_is_pressed_) {
|
||||
switch(value) {
|
||||
case 0x27: value = 0x22; break; // ' -> "
|
||||
case 0x2c: value = 0x3c; break; // , -> <
|
||||
@@ -467,7 +559,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
|
||||
|
||||
// 6502 connection, for applying the reset button.
|
||||
Processor *const m6502_;
|
||||
Processor &m6502_;
|
||||
AuxiliaryMemorySwitches<ConcreteMachine> &auxiliary_switches_;
|
||||
};
|
||||
Keyboard keyboard_;
|
||||
|
||||
@@ -480,12 +573,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
video_bus_handler_(ram_, aux_ram_),
|
||||
video_(video_bus_handler_),
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_),
|
||||
ays_(audio_queue_),
|
||||
mixer_(ays_, audio_toggle_),
|
||||
speaker_(lowpass_source()),
|
||||
language_card_(*this),
|
||||
auxiliary_switches_(*this),
|
||||
keyboard_(&m6502_) {
|
||||
// The system's master clock rate.
|
||||
constexpr float master_clock = 14318180.0;
|
||||
keyboard_(m6502_, auxiliary_switches_) {
|
||||
|
||||
// This is where things get slightly convoluted: establish the machine as having a clock rate
|
||||
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
|
||||
@@ -559,6 +652,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
install_card(SCSISlot, new Apple::II::SCSICard(roms, int(master_clock / 14.0f)));
|
||||
}
|
||||
|
||||
if(target.has_mockingboard) {
|
||||
// The Mockingboard has a parasitic relationship with this class due to the way
|
||||
// that audio outputs are implemented in this emulator.
|
||||
install_card(MockingboardSlot, new Apple::II::Mockingboard(ays_));
|
||||
}
|
||||
|
||||
rom_ = std::move(roms.find(system)->second);
|
||||
// The IIe and Enhanced IIe ROMs often distributed are oversized; trim if necessary.
|
||||
if(system == ROM::Name::AppleIIe || system == ROM::Name::AppleIIEnhancedE) {
|
||||
@@ -629,7 +728,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
|
||||
}
|
||||
|
||||
if(is_iie()) {
|
||||
if(is_iie(model)) {
|
||||
auxiliary_switches_.access(address, isReadOperation(operation));
|
||||
}
|
||||
} else {
|
||||
@@ -647,6 +746,11 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
// actor, but this will actually be the result most of the time so it's not
|
||||
// too terrible.
|
||||
if(isReadOperation(operation) && address != 0xc000) {
|
||||
// Ensure any enqueued video changes are applied before grabbing the
|
||||
// vapour value.
|
||||
if(video_.has_deferred_actions()) {
|
||||
update_video();
|
||||
}
|
||||
*value = video_.get_last_read_value(cycles_since_video_update_);
|
||||
}
|
||||
|
||||
@@ -669,7 +773,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
joysticks_.button(0) ||
|
||||
(is_iie() && keyboard_.open_apple_is_pressed)
|
||||
(is_iie(model) && keyboard_.open_apple_is_pressed)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
@@ -677,7 +781,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
joysticks_.button(1) ||
|
||||
(is_iie() && keyboard_.closed_apple_is_pressed)
|
||||
(is_iie(model) && keyboard_.closed_apple_is_pressed)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
@@ -699,7 +803,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
} break;
|
||||
|
||||
// The IIe-only state reads follow...
|
||||
#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
|
||||
#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie(model)) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
|
||||
case 0xc011: IIeSwitchRead(language_card_.state().bank2); break;
|
||||
case 0xc012: IIeSwitchRead(language_card_.state().read); break;
|
||||
case 0xc013: IIeSwitchRead(auxiliary_switches_.switches().read_auxiliary_memory); break;
|
||||
@@ -718,12 +822,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
#undef IIeSwitchRead
|
||||
|
||||
case 0xc07f:
|
||||
if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00);
|
||||
if(is_iie(model)) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Write-only switches. All IIe as currently implemented.
|
||||
if(is_iie()) {
|
||||
if(is_iie(model)) {
|
||||
auxiliary_switches_.access(address, false);
|
||||
switch(address) {
|
||||
default: break;
|
||||
@@ -731,19 +835,19 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc000:
|
||||
case 0xc001:
|
||||
update_video();
|
||||
video_.set_80_store(!!(address&1));
|
||||
video_.set_80_store(address&1);
|
||||
break;
|
||||
|
||||
case 0xc00c:
|
||||
case 0xc00d:
|
||||
update_video();
|
||||
video_.set_80_columns(!!(address&1));
|
||||
video_.set_80_columns(address&1);
|
||||
break;
|
||||
|
||||
case 0xc00e:
|
||||
case 0xc00f:
|
||||
update_video();
|
||||
video_.set_alternative_character_set(!!(address&1));
|
||||
video_.set_alternative_character_set(address&1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -756,7 +860,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc050:
|
||||
case 0xc051:
|
||||
update_video();
|
||||
video_.set_text(!!(address&1));
|
||||
video_.set_text(address&1);
|
||||
break;
|
||||
case 0xc052: update_video(); video_.set_mixed(false); break;
|
||||
case 0xc053: update_video(); video_.set_mixed(true); break;
|
||||
@@ -775,7 +879,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
case 0xc05e:
|
||||
case 0xc05f:
|
||||
if(is_iie()) {
|
||||
if(is_iie(model)) {
|
||||
update_video();
|
||||
video_.set_annunciator_3(!(address&1));
|
||||
}
|
||||
@@ -785,7 +889,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
keyboard_.clear_keyboard_input();
|
||||
|
||||
// On the IIe, reading C010 returns additional key info.
|
||||
if(is_iie() && isReadOperation(operation)) {
|
||||
if(is_iie(model) && isReadOperation(operation)) {
|
||||
*value = (keyboard_.get_key_is_down() ? 0x80 : 0x00) | (keyboard_.get_keyboard_input() & 0x7f);
|
||||
}
|
||||
break;
|
||||
@@ -834,14 +938,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
This also sets the active card for the C8 region.
|
||||
*/
|
||||
active_card_ = card_number = (address - 0xc100) >> 8;
|
||||
select = Apple::II::Card::Device;
|
||||
select = Apple::II::Card::IO;
|
||||
} else {
|
||||
/*
|
||||
Decode the area conventionally used by cards for registers:
|
||||
C0n0 to C0nF: card n - 8.
|
||||
*/
|
||||
card_number = (address - 0xc090) >> 4;
|
||||
select = Apple::II::Card::IO;
|
||||
select = Apple::II::Card::Device;
|
||||
}
|
||||
|
||||
// If the selected card is a just-in-time card, update the just-in-time cards,
|
||||
@@ -922,7 +1026,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
bool prefers_logical_input() final {
|
||||
return is_iie();
|
||||
return is_iie(model);
|
||||
}
|
||||
|
||||
Inputs::Keyboard &get_keyboard() final {
|
||||
@@ -988,13 +1092,22 @@ using namespace Apple::II;
|
||||
std::unique_ptr<Machine> Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const Target *const appleii_target = dynamic_cast<const Target *>(target);
|
||||
switch(appleii_target->model) {
|
||||
default: return nullptr;
|
||||
case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe>>(*appleii_target, rom_fetcher);
|
||||
|
||||
if(appleii_target->has_mockingboard) {
|
||||
switch(appleii_target->model) {
|
||||
default: return nullptr;
|
||||
case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II, true>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus, true>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe, true>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe, true>>(*appleii_target, rom_fetcher);
|
||||
}
|
||||
} else {
|
||||
switch(appleii_target->model) {
|
||||
default: return nullptr;
|
||||
case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II, false>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus, false>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe, false>>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe, false>>(*appleii_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Apple::II {
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
virtual ~Machine() = default;
|
||||
|
||||
/// Creates and returns an AppleII.
|
||||
static std::unique_ptr<Machine> AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
|
||||
@@ -90,6 +90,10 @@ template <typename Machine> class AuxiliaryMemorySwitches {
|
||||
bool alternative_zero_page = false;
|
||||
bool video_page_2 = false;
|
||||
bool high_resolution = false;
|
||||
|
||||
void reset() {
|
||||
*this = SwitchState();
|
||||
}
|
||||
};
|
||||
|
||||
AuxiliaryMemorySwitches(Machine &machine) : machine_(machine) {}
|
||||
@@ -208,6 +212,14 @@ template <typename Machine> class AuxiliaryMemorySwitches {
|
||||
return switches_;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
switches_.reset();
|
||||
|
||||
set_main_paging();
|
||||
set_zero_page_paging();
|
||||
set_card_paging();
|
||||
}
|
||||
|
||||
private:
|
||||
Machine &machine_;
|
||||
SwitchState switches_;
|
||||
|
||||
@@ -41,12 +41,12 @@ class Card {
|
||||
virtual ~Card() {}
|
||||
enum Select: int {
|
||||
None = 0, // No select line is active.
|
||||
IO = 1 << 0, // IO select is active; i.e. access is in range $C0x0 to $C0xf.
|
||||
Device = 1 << 1, // Device select is active; i.e. access is in range $Cx00 to $Cxff.
|
||||
Device = 1 << 0, // Device select ('DEVSEL') is active; i.e. access is in range $C0x0 to $C0xf.
|
||||
IO = 1 << 1, // IO select ('IOSEL') is active; i.e. access is in range $Cx00 to $Cxff.
|
||||
|
||||
C8Region = 1 << 2, // Access is to the region $c800 to $cfff, was preceded by at least
|
||||
// one Device access to this card, and has not yet been followed up
|
||||
// by an access to $cfff.
|
||||
// by an access to $cfff. IOSTRB on original hardware.
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -54,7 +54,7 @@ class Card {
|
||||
|
||||
This is posted only to cards that announced a select constraint. Cards with
|
||||
no constraints, that want to be informed of every machine cycle, will receive
|
||||
a call to perform_bus_operation every cycle and should use that for time keeping.
|
||||
a call to @c perform_bus_operation every cycle and should use that for time keeping.
|
||||
*/
|
||||
virtual void run_for([[maybe_unused]] Cycles cycles, [[maybe_unused]] int stretches) {}
|
||||
|
||||
@@ -93,15 +93,22 @@ class Card {
|
||||
/*! Cards may supply a target for activity observation if desired. */
|
||||
virtual void set_activity_observer([[maybe_unused]] Activity::Observer *observer) {}
|
||||
|
||||
/// @returns The current semantic NMI line output of this card.
|
||||
virtual bool nmi() { return false; }
|
||||
|
||||
/// @returns The current semantic IRQ line output of this card.
|
||||
virtual bool irq() { return false; }
|
||||
|
||||
struct Delegate {
|
||||
virtual void card_did_change_select_constraints(Card *card) = 0;
|
||||
virtual void card_did_change_interrupt_flags(Card *card) = 0;
|
||||
};
|
||||
void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
protected:
|
||||
int select_constraints_ = IO | Device;
|
||||
int select_constraints_ = Device | IO;
|
||||
Delegate *delegate_ = nullptr;
|
||||
void set_select_constraints(int constraints) {
|
||||
if(constraints == select_constraints_) return;
|
||||
|
||||
@@ -46,14 +46,14 @@ void DiskIICard::perform_bus_operation(Select select, bool is_read, uint16_t add
|
||||
diskii_.set_data_input(*value);
|
||||
switch(select) {
|
||||
default: break;
|
||||
case IO: {
|
||||
case Device: {
|
||||
const int disk_value = diskii_.read_address(address);
|
||||
if(is_read) {
|
||||
if(disk_value != diskii_.DidNotLoad)
|
||||
*value = uint8_t(disk_value);
|
||||
}
|
||||
} break;
|
||||
case Device:
|
||||
case IO:
|
||||
if(is_read) *value = boot_[address & 0xff];
|
||||
break;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ void DiskIICard::set_activity_observer(Activity::Observer *observer) {
|
||||
|
||||
void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference preference) {
|
||||
diskii_clocking_preference_ = preference;
|
||||
set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : None);
|
||||
set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (Device | IO) : None);
|
||||
}
|
||||
|
||||
Storage::Disk::Drive &DiskIICard::get_drive(int drive) {
|
||||
|
||||
135
Machines/Apple/AppleII/Mockingboard.hpp
Normal file
135
Machines/Apple/AppleII/Mockingboard.hpp
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// Mockingboard.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/02/2024.
|
||||
// Copyright © 2024 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Card.hpp"
|
||||
|
||||
#include "../../../Components/6522/6522.hpp"
|
||||
|
||||
namespace Apple::II {
|
||||
|
||||
class AYPair {
|
||||
public:
|
||||
AYPair(Concurrency::AsyncTaskQueue<false> &queue) :
|
||||
ays_{
|
||||
{GI::AY38910::Personality::AY38910, queue},
|
||||
{GI::AY38910::Personality::AY38910, queue},
|
||||
} {}
|
||||
|
||||
void advance() {
|
||||
ays_[0].advance();
|
||||
ays_[1].advance();
|
||||
}
|
||||
|
||||
void set_sample_volume_range(std::int16_t range) {
|
||||
ays_[0].set_sample_volume_range(range);
|
||||
ays_[1].set_sample_volume_range(range);
|
||||
}
|
||||
|
||||
bool is_zero_level() const {
|
||||
return ays_[0].is_zero_level() && ays_[1].is_zero_level();
|
||||
}
|
||||
|
||||
Outputs::Speaker::StereoSample level() const {
|
||||
Outputs::Speaker::StereoSample level;
|
||||
level.left = ays_[0].level();
|
||||
level.right = ays_[1].level();
|
||||
return level;
|
||||
}
|
||||
|
||||
GI::AY38910::AY38910SampleSource<false> &get(int index) {
|
||||
return ays_[index];
|
||||
}
|
||||
|
||||
private:
|
||||
GI::AY38910::AY38910SampleSource<false> ays_[2];
|
||||
};
|
||||
|
||||
class Mockingboard: public Card {
|
||||
public:
|
||||
Mockingboard(AYPair &ays) :
|
||||
vias_{ {handlers_[0]}, {handlers_[1]} },
|
||||
handlers_{ {*this, ays.get(0)}, {*this, ays.get(1)}} {
|
||||
set_select_constraints(0);
|
||||
}
|
||||
|
||||
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final {
|
||||
if(!(select & IO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int index = (address >> 7) & 1;
|
||||
if(is_read) {
|
||||
*value = vias_[index].read(address);
|
||||
} else {
|
||||
vias_[index].write(address, *value);
|
||||
}
|
||||
}
|
||||
|
||||
void run_for(Cycles cycles, int) final {
|
||||
vias_[0].run_for(cycles);
|
||||
vias_[1].run_for(cycles);
|
||||
}
|
||||
|
||||
bool nmi() final {
|
||||
return handlers_[1].interrupt;
|
||||
}
|
||||
|
||||
bool irq() final {
|
||||
return handlers_[0].interrupt;
|
||||
}
|
||||
|
||||
void did_change_interrupt_flags() {
|
||||
delegate_->card_did_change_interrupt_flags(this);
|
||||
}
|
||||
|
||||
private:
|
||||
struct AYVIA: public MOS::MOS6522::PortHandler {
|
||||
AYVIA(Mockingboard &card, GI::AY38910::AY38910SampleSource<false> &ay) :
|
||||
card(card), ay(ay) {}
|
||||
|
||||
void set_interrupt_status(bool status) {
|
||||
interrupt = status;
|
||||
card.did_change_interrupt_flags();
|
||||
}
|
||||
|
||||
void set_port_output(MOS::MOS6522::Port port, uint8_t value, uint8_t) {
|
||||
if(port) {
|
||||
using ControlLines = GI::AY38910::ControlLines;
|
||||
ay.set_control_lines(
|
||||
ControlLines(
|
||||
((value & 1) ? ControlLines::BC1 : 0) |
|
||||
((value & 2) ? ControlLines::BDIR : 0) |
|
||||
ControlLines::BC2
|
||||
)
|
||||
);
|
||||
|
||||
ay.set_reset(!(value & 4));
|
||||
} else {
|
||||
ay.set_data_input(value);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t get_port_input(MOS::MOS6522::Port port) {
|
||||
if(!port) {
|
||||
return ay.get_data_output();
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
bool interrupt;
|
||||
Mockingboard &card;
|
||||
GI::AY38910::AY38910SampleSource<false> &ay;
|
||||
};
|
||||
|
||||
MOS::MOS6522::MOS6522<AYVIA> vias_[2];
|
||||
AYVIA handlers_[2];
|
||||
};
|
||||
|
||||
}
|
||||
@@ -79,13 +79,13 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre
|
||||
switch(select) {
|
||||
default: break;
|
||||
|
||||
case Select::Device:
|
||||
case Select::IO:
|
||||
if(is_read) {
|
||||
*value = rom_[address & 255];
|
||||
}
|
||||
break;
|
||||
|
||||
case Select::IO:
|
||||
case Select::Device:
|
||||
address &= 0xf;
|
||||
switch(address) {
|
||||
case 0x0: case 0x1: case 0x2: case 0x3:
|
||||
@@ -125,13 +125,13 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre
|
||||
|
||||
case 0xb:
|
||||
if(!is_read) {
|
||||
printf("TODO: NCR reset\n");
|
||||
logger_.error().append("TODO: NCR reset");
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xd:
|
||||
if(!is_read) {
|
||||
printf("TODO: Enable PDMA\n");
|
||||
logger_.error().append("TODO: Enable PDMA");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -143,7 +143,7 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("Unhandled: %04x %c %02x\n", address, is_read ? 'r' : 'w', *value);
|
||||
logger_.error().append("Unhandled: %04x %c %02x\n", address, is_read ? 'r' : 'w', *value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
|
||||
#include "../../../Storage/MassStorage/MassStorageDevice.hpp"
|
||||
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
@@ -47,6 +49,7 @@ class SCSICard: public Card {
|
||||
SCSI::Bus scsi_bus_;
|
||||
NCR::NCR5380::NCR5380 ncr5380_;
|
||||
SCSI::Target::Target<SCSI::DirectAccessDevice> storage_;
|
||||
Log::Logger<Log::Source::AppleIISCSICard> logger_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
|
||||
#include "VideoSwitches.hpp"
|
||||
|
||||
@@ -124,7 +123,8 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
bus_handler_(bus_handler) {}
|
||||
|
||||
/*!
|
||||
Obtains the last value the video read prior to time now+offset.
|
||||
Obtains the last value the video read prior to time now+offset, according to the *current*
|
||||
video mode, i.e. not allowing for any changes still enqueued.
|
||||
*/
|
||||
uint8_t get_last_read_value(Cycles offset) {
|
||||
// Rules of generation:
|
||||
|
||||
@@ -247,6 +247,10 @@ template <typename TimeUnit> class VideoSwitches {
|
||||
}
|
||||
}
|
||||
|
||||
bool has_deferred_actions() const {
|
||||
return !deferrer_.empty();
|
||||
}
|
||||
|
||||
protected:
|
||||
GraphicsMode graphics_mode(int row) const {
|
||||
if(
|
||||
@@ -306,7 +310,14 @@ template <typename TimeUnit> class VideoSwitches {
|
||||
bool annunciator_3 = false;
|
||||
} external_, internal_;
|
||||
|
||||
int flash_length = 8406;
|
||||
// 8406 lines covers a complete on-off cycle, i.e. because the Apple II has a line
|
||||
// rate of around 15734.26 lines/second, flashing has a frequency of roughly
|
||||
// 15734.26 / 8406, or roughly 1.83Hz.
|
||||
//
|
||||
// Internally that's modelled by a counter that goes to **twice** the value of the
|
||||
// constant specified below. Hence the divide by two.
|
||||
static constexpr int flash_length = 8406 / 2;
|
||||
|
||||
int flash_ = 0;
|
||||
uint8_t flash_mask() const {
|
||||
return uint8_t((flash_ / flash_length) * 0xff);
|
||||
|
||||
@@ -8,16 +8,18 @@
|
||||
|
||||
#include "AppleIIgs.hpp"
|
||||
|
||||
#include "../../../Activity/Source.hpp"
|
||||
#include "../../MachineTypes.hpp"
|
||||
|
||||
#include "../../../Analyser/Static/AppleIIgs/Target.hpp"
|
||||
|
||||
#include "ADB.hpp"
|
||||
#include "MemoryMap.hpp"
|
||||
#include "Video.hpp"
|
||||
#include "Sound.hpp"
|
||||
|
||||
#include "../AppleII/Joystick.hpp"
|
||||
|
||||
#include "../../../Activity/Source.hpp"
|
||||
#include "../../MachineTypes.hpp"
|
||||
|
||||
#include "../../../Analyser/Static/AppleIIgs/Target.hpp"
|
||||
|
||||
#include "../../../Processors/65816/65816.hpp"
|
||||
#include "../../../Components/8530/z8530.hpp"
|
||||
#include "../../../Components/AppleClock/AppleClock.hpp"
|
||||
@@ -26,8 +28,6 @@
|
||||
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
|
||||
#include "../../../Components/DiskII/DiskIIDrive.hpp"
|
||||
|
||||
#include "../AppleII/Joystick.hpp"
|
||||
|
||||
#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
|
||||
@@ -1013,6 +1013,3 @@ using namespace Apple::IIgs;
|
||||
std::unique_ptr<Machine> Machine::AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
return std::make_unique<ConcreteMachine>(*dynamic_cast<const Analyser::Static::AppleIIgs::Target *>(target), rom_fetcher);
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Apple::IIgs {
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
virtual ~Machine() = default;
|
||||
|
||||
/// Creates and returns an AppleIIgs.
|
||||
static std::unique_ptr<Machine> AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
|
||||
@@ -208,7 +208,7 @@ void MemoryMap::access(uint16_t address, bool is_read) {
|
||||
if((address & 0xfff0) == 0xc080) language_card_.access(address, is_read);
|
||||
}
|
||||
|
||||
void MemoryMap::assert_is_region(uint8_t start, uint8_t end) {
|
||||
void MemoryMap::assert_is_region([[maybe_unused]] uint8_t start, [[maybe_unused]] uint8_t end) {
|
||||
assert(region_map_[start] == region_map_[start-1]+1);
|
||||
assert(region_map_[end-1] == region_map_[start]);
|
||||
assert(region_map_[end] == region_map_[end-1]+1);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "../AppleII/LanguageCardSwitches.hpp"
|
||||
@@ -76,8 +77,8 @@ class MemoryMap {
|
||||
}
|
||||
|
||||
const auto physical = physical_address(region, address);
|
||||
assert(physical >= 0 && physical <= 0xff'ffff);
|
||||
return shadow_pages_[(physical >> 10) & 127] & shadow_banks_[physical >> 17];
|
||||
assert(physical <= 0xff'ffff);
|
||||
return shadow_pages_[(physical >> 10) & 127] && shadow_banks_[physical >> 17];
|
||||
}
|
||||
void write(const Region ®ion, uint32_t address, uint8_t value) {
|
||||
if(!region.write) {
|
||||
|
||||
@@ -108,7 +108,7 @@ uint8_t GLU::get_data() {
|
||||
|
||||
switch(address & 0xe0) {
|
||||
case 0x00: return local_.oscillators[address & 0x1f].velocity & 0xff;
|
||||
case 0x20: return local_.oscillators[address & 0x1f].velocity >> 8;;
|
||||
case 0x20: return local_.oscillators[address & 0x1f].velocity >> 8;
|
||||
case 0x40: return local_.oscillators[address & 0x1f].volume;
|
||||
case 0x60: return local_.oscillators[address & 0x1f].sample(local_.ram_); // i.e. look up what the sample was on demand.
|
||||
case 0x80: return local_.oscillators[address & 0x1f].address;
|
||||
@@ -159,29 +159,34 @@ void GLU::run_for(Cycles cycles) {
|
||||
pending_store_write_time_ += cycles.as<uint32_t>();
|
||||
}
|
||||
|
||||
void GLU::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
template <Outputs::Speaker::Action action>
|
||||
void GLU::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
// Update remote state, generating audio.
|
||||
generate_audio(number_of_samples, target);
|
||||
generate_audio<action>(number_of_samples, target);
|
||||
}
|
||||
template void GLU::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void GLU::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void GLU::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void GLU::skip_samples(const std::size_t number_of_samples) {
|
||||
// Update remote state, without generating audio.
|
||||
skip_audio(remote_, number_of_samples);
|
||||
|
||||
// Apply any pending stores.
|
||||
std::atomic_thread_fence(std::memory_order::memory_order_acquire);
|
||||
const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples);
|
||||
while(true) {
|
||||
auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire);
|
||||
if(!next_store.enabled) break;
|
||||
if(next_store.time >= final_time) break;
|
||||
remote_.ram_[next_store.address] = next_store.value;
|
||||
next_store.enabled = false;
|
||||
pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed);
|
||||
|
||||
pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1);
|
||||
}
|
||||
}
|
||||
//void GLU::skip_samples(const std::size_t number_of_samples) {
|
||||
// // Update remote state, without generating audio.
|
||||
// skip_audio(remote_, number_of_samples);
|
||||
//
|
||||
// // Apply any pending stores.
|
||||
// std::atomic_thread_fence(std::memory_order::memory_order_acquire);
|
||||
// const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples);
|
||||
// while(true) {
|
||||
// auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire);
|
||||
// if(!next_store.enabled) break;
|
||||
// if(next_store.time >= final_time) break;
|
||||
// remote_.ram_[next_store.address] = next_store.value;
|
||||
// next_store.enabled = false;
|
||||
// pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed);
|
||||
//
|
||||
// pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1);
|
||||
// }
|
||||
//}
|
||||
|
||||
void GLU::set_sample_volume_range(std::int16_t range) {
|
||||
output_range_ = range;
|
||||
@@ -256,7 +261,8 @@ void GLU::skip_audio(EnsoniqState &state, size_t number_of_samples) {
|
||||
}
|
||||
}
|
||||
|
||||
void GLU::generate_audio(size_t number_of_samples, std::int16_t *target) {
|
||||
template <Outputs::Speaker::Action action>
|
||||
void GLU::generate_audio(size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire);
|
||||
uint8_t next_amplitude = 255;
|
||||
for(size_t sample = 0; sample < number_of_samples; sample++) {
|
||||
@@ -325,7 +331,12 @@ void GLU::generate_audio(size_t number_of_samples, std::int16_t *target) {
|
||||
|
||||
// Maximum total output was 32 channels times a 16-bit range. Map that down.
|
||||
// TODO: dynamic total volume?
|
||||
target[sample] = (output * output_range_) >> 20;
|
||||
Outputs::Speaker::apply<action>(
|
||||
target[sample],
|
||||
Outputs::Speaker::MonoSample(
|
||||
(output * output_range_) >> 20
|
||||
)
|
||||
);
|
||||
|
||||
// Apply any RAM writes that interleave here.
|
||||
++pending_store_read_time_;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user