bench51fa3bbf2
Merge pull request #1 from krolyxon/8051-rewrite
8051 rewrite
+7539 −910
43 files changed
- M .gitignore +2 −0
- M Cargo.lock +1695 −40
- M Cargo.toml +4 −1
- M README.md +127 −98
- A docs/screenshots/debug_breakpoint.png
- A docs/screenshots/debug_serial.png
- A docs/screenshots/errors.png
- A docs/screenshots/hex.png
- A docs/screenshots/listing.png
- D examples/arithmetic.asm +0 −9
- A examples/bcd_add.asm +21 −0
- A examples/blink.asm +23 −0
- A examples/button_int0.asm +20 −0
- D examples/call.asm +0 −7
- D examples/cmp.asm +0 −4
- D examples/div.asm +0 −4
- A examples/lookup_movc.asm +20 −0
- D examples/loops.asm +0 −13
- D examples/mul.asm +0 −4
- A examples/serial_hello.asm +43 −0
- D examples/syscall.asm +0 −7
- D examples/table.asm +0 −9
- A examples/timer_irq.asm +28 −0
- A src/asm/expr.rs +251 −0
- A src/asm/hex.rs +117 −0
- A src/asm/lexer.rs +175 −0
- A src/asm/listing.rs +53 −0
- A src/asm/mod.rs +940 −0
- D src/assembler.rs +0 −201
- D src/cpu.rs +0 −424
- D src/instructions.rs +0 −43
- A src/isa.rs +347 −0
- M src/main.rs +81 −29
- D src/memory.rs +0 −17
- A src/sfr.rs +146 −0
- A src/sim/cpu.rs +528 −0
- A src/sim/disasm.rs +91 −0
- A src/sim/mod.rs +449 −0
- A src/sim/periph.rs +403 −0
- A src/ui/app.rs +903 −0
- A src/ui/draw.rs +882 −0
- A src/ui/mod.rs +68 −0
- A src/ui/syntax.rs +122 −0
| @@ -1 +1,3 @@ | |||
| 1 | 1 | /target | |
| 2 | + | examples/*.hex | |
| 3 | + | examples/*.lst | |
| @@ -2,6 +2,21 @@ | |||
| 2 | 2 | # It is not intended for manual editing. | |
| 3 | 3 | version = 4 | |
| 4 | 4 | ||
| 5 | + | [[package]] | |
| 6 | + | name = "aho-corasick" | |
| 7 | + | version = "1.1.5" | |
| 8 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9 | + | checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" | |
| 10 | + | dependencies = [ | |
| 11 | + | "memchr", | |
| 12 | + | ] | |
| 13 | + | ||
| 14 | + | [[package]] | |
| 15 | + | name = "allocator-api2" | |
| 16 | + | version = "0.2.21" | |
| 17 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 18 | + | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | |
| 19 | + | ||
| 5 | 20 | [[package]] | |
| 6 | 21 | name = "anstream" | |
| 7 | 22 | version = "0.6.21" | |
| @@ -52,6 +67,136 @@ dependencies = [ | |||
| 52 | 67 | "windows-sys", | |
| 53 | 68 | ] | |
| 54 | 69 | ||
| 70 | + | [[package]] | |
| 71 | + | name = "anyhow" | |
| 72 | + | version = "1.0.104" | |
| 73 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 74 | + | checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" | |
| 75 | + | ||
| 76 | + | [[package]] | |
| 77 | + | name = "approx" | |
| 78 | + | version = "0.5.1" | |
| 79 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 80 | + | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" | |
| 81 | + | dependencies = [ | |
| 82 | + | "num-traits", | |
| 83 | + | ] | |
| 84 | + | ||
| 85 | + | [[package]] | |
| 86 | + | name = "atomic" | |
| 87 | + | version = "0.6.1" | |
| 88 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 89 | + | checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" | |
| 90 | + | dependencies = [ | |
| 91 | + | "bytemuck", | |
| 92 | + | ] | |
| 93 | + | ||
| 94 | + | [[package]] | |
| 95 | + | name = "autocfg" | |
| 96 | + | version = "1.5.1" | |
| 97 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 98 | + | checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" | |
| 99 | + | ||
| 100 | + | [[package]] | |
| 101 | + | name = "base64" | |
| 102 | + | version = "0.22.1" | |
| 103 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 104 | + | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" | |
| 105 | + | ||
| 106 | + | [[package]] | |
| 107 | + | name = "bench51" | |
| 108 | + | version = "0.1.0" | |
| 109 | + | dependencies = [ | |
| 110 | + | "clap", | |
| 111 | + | "ratatui", | |
| 112 | + | "ratatui-textarea", | |
| 113 | + | ] | |
| 114 | + | ||
| 115 | + | [[package]] | |
| 116 | + | name = "bit-set" | |
| 117 | + | version = "0.5.3" | |
| 118 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 119 | + | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" | |
| 120 | + | dependencies = [ | |
| 121 | + | "bit-vec", | |
| 122 | + | ] | |
| 123 | + | ||
| 124 | + | [[package]] | |
| 125 | + | name = "bit-vec" | |
| 126 | + | version = "0.6.3" | |
| 127 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 128 | + | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" | |
| 129 | + | ||
| 130 | + | [[package]] | |
| 131 | + | name = "bitflags" | |
| 132 | + | version = "1.3.2" | |
| 133 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 134 | + | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | |
| 135 | + | ||
| 136 | + | [[package]] | |
| 137 | + | name = "bitflags" | |
| 138 | + | version = "2.13.2" | |
| 139 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 140 | + | checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" | |
| 141 | + | ||
| 142 | + | [[package]] | |
| 143 | + | name = "block-buffer" | |
| 144 | + | version = "0.10.4" | |
| 145 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 146 | + | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | |
| 147 | + | dependencies = [ | |
| 148 | + | "generic-array", | |
| 149 | + | ] | |
| 150 | + | ||
| 151 | + | [[package]] | |
| 152 | + | name = "bumpalo" | |
| 153 | + | version = "3.20.3" | |
| 154 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 155 | + | checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" | |
| 156 | + | ||
| 157 | + | [[package]] | |
| 158 | + | name = "by_address" | |
| 159 | + | version = "1.2.1" | |
| 160 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 161 | + | checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" | |
| 162 | + | ||
| 163 | + | [[package]] | |
| 164 | + | name = "bytemuck" | |
| 165 | + | version = "1.25.2" | |
| 166 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 167 | + | checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" | |
| 168 | + | ||
| 169 | + | [[package]] | |
| 170 | + | name = "castaway" | |
| 171 | + | version = "0.2.4" | |
| 172 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 173 | + | checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" | |
| 174 | + | dependencies = [ | |
| 175 | + | "rustversion", | |
| 176 | + | ] | |
| 177 | + | ||
| 178 | + | [[package]] | |
| 179 | + | name = "cc" | |
| 180 | + | version = "1.9.0" | |
| 181 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 182 | + | checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" | |
| 183 | + | dependencies = [ | |
| 184 | + | "find-msvc-tools", | |
| 185 | + | "shlex", | |
| 186 | + | ] | |
| 187 | + | ||
| 188 | + | [[package]] | |
| 189 | + | name = "cfg-if" | |
| 190 | + | version = "1.0.5" | |
| 191 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 192 | + | checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" | |
| 193 | + | ||
| 194 | + | [[package]] | |
| 195 | + | name = "cfg_aliases" | |
| 196 | + | version = "0.2.2" | |
| 197 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 198 | + | checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" | |
| 199 | + | ||
| 55 | 200 | [[package]] | |
| 56 | 201 | name = "clap" | |
| 57 | 202 | version = "4.5.54" | |
| @@ -83,7 +228,7 @@ dependencies = [ | |||
| 83 | 228 | "heck", | |
| 84 | 229 | "proc-macro2", | |
| 85 | 230 | "quote", | |
| 86 | − | "syn", | |
| 231 | + | "syn 2.0.113", | |
| 87 | 232 | ] | |
| 88 | 233 | ||
| 89 | 234 | [[package]] | |
| @@ -99,88 +244,1598 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 99 | 244 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" | |
| 100 | 245 | ||
| 101 | 246 | [[package]] | |
| 102 | − | name = "cpu-emulator" | |
| 103 | − | version = "0.1.0" | |
| 247 | + | name = "compact_str" | |
| 248 | + | version = "0.9.1" | |
| 249 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 250 | + | checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" | |
| 104 | 251 | dependencies = [ | |
| 105 | − | "clap", | |
| 252 | + | "castaway", | |
| 253 | + | "cfg-if", | |
| 254 | + | "itoa", | |
| 255 | + | "rustversion", | |
| 256 | + | "ryu", | |
| 257 | + | "static_assertions", | |
| 106 | 258 | ] | |
| 107 | 259 | ||
| 108 | 260 | [[package]] | |
| 109 | − | name = "heck" | |
| 110 | − | version = "0.5.0" | |
| 261 | + | name = "convert_case" | |
| 262 | + | version = "0.10.0" | |
| 111 | 263 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 112 | − | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 264 | + | checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" | |
| 265 | + | dependencies = [ | |
| 266 | + | "unicode-segmentation", | |
| 267 | + | ] | |
| 113 | 268 | ||
| 114 | 269 | [[package]] | |
| 115 | − | name = "is_terminal_polyfill" | |
| 116 | − | version = "1.70.2" | |
| 270 | + | name = "cpufeatures" | |
| 271 | + | version = "0.2.17" | |
| 117 | 272 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 118 | − | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" | |
| 273 | + | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | |
| 274 | + | dependencies = [ | |
| 275 | + | "libc", | |
| 276 | + | ] | |
| 119 | 277 | ||
| 120 | 278 | [[package]] | |
| 121 | − | name = "once_cell_polyfill" | |
| 122 | − | version = "1.70.2" | |
| 279 | + | name = "critical-section" | |
| 280 | + | version = "1.2.0" | |
| 123 | 281 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 124 | − | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | |
| 282 | + | checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" | |
| 125 | 283 | ||
| 126 | 284 | [[package]] | |
| 127 | − | name = "proc-macro2" | |
| 128 | − | version = "1.0.104" | |
| 285 | + | name = "crossterm" | |
| 286 | + | version = "0.29.0" | |
| 129 | 287 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 130 | − | checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" | |
| 288 | + | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" | |
| 131 | 289 | dependencies = [ | |
| 132 | − | "unicode-ident", | |
| 290 | + | "bitflags 2.13.2", | |
| 291 | + | "crossterm_winapi", | |
| 292 | + | "derive_more", | |
| 293 | + | "document-features", | |
| 294 | + | "mio", | |
| 295 | + | "parking_lot", | |
| 296 | + | "rustix", | |
| 297 | + | "signal-hook", | |
| 298 | + | "signal-hook-mio", | |
| 299 | + | "winapi", | |
| 133 | 300 | ] | |
| 134 | 301 | ||
| 135 | 302 | [[package]] | |
| 136 | − | name = "quote" | |
| 137 | − | version = "1.0.42" | |
| 303 | + | name = "crossterm_winapi" | |
| 304 | + | version = "0.9.1" | |
| 138 | 305 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 139 | − | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" | |
| 306 | + | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" | |
| 307 | + | dependencies = [ | |
| 308 | + | "winapi", | |
| 309 | + | ] | |
| 310 | + | ||
| 311 | + | [[package]] | |
| 312 | + | name = "crypto-common" | |
| 313 | + | version = "0.1.7" | |
| 314 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 315 | + | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | |
| 316 | + | dependencies = [ | |
| 317 | + | "generic-array", | |
| 318 | + | "typenum", | |
| 319 | + | ] | |
| 320 | + | ||
| 321 | + | [[package]] | |
| 322 | + | name = "csscolorparser" | |
| 323 | + | version = "0.6.2" | |
| 324 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 325 | + | checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" | |
| 326 | + | dependencies = [ | |
| 327 | + | "lab", | |
| 328 | + | "phf", | |
| 329 | + | ] | |
| 330 | + | ||
| 331 | + | [[package]] | |
| 332 | + | name = "darling" | |
| 333 | + | version = "0.24.1" | |
| 334 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 335 | + | checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" | |
| 336 | + | dependencies = [ | |
| 337 | + | "darling_core", | |
| 338 | + | "darling_macro", | |
| 339 | + | ] | |
| 340 | + | ||
| 341 | + | [[package]] | |
| 342 | + | name = "darling_core" | |
| 343 | + | version = "0.24.1" | |
| 344 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 345 | + | checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" | |
| 140 | 346 | dependencies = [ | |
| 347 | + | "ident_case", | |
| 141 | 348 | "proc-macro2", | |
| 349 | + | "quote", | |
| 350 | + | "strsim", | |
| 351 | + | "syn 3.0.9", | |
| 142 | 352 | ] | |
| 143 | 353 | ||
| 144 | 354 | [[package]] | |
| 145 | − | name = "strsim" | |
| 146 | − | version = "0.11.1" | |
| 355 | + | name = "darling_macro" | |
| 356 | + | version = "0.24.1" | |
| 147 | 357 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 148 | − | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | |
| 358 | + | checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" | |
| 359 | + | dependencies = [ | |
| 360 | + | "darling_core", | |
| 361 | + | "quote", | |
| 362 | + | "syn 3.0.9", | |
| 363 | + | ] | |
| 149 | 364 | ||
| 150 | 365 | [[package]] | |
| 151 | − | name = "syn" | |
| 152 | − | version = "2.0.113" | |
| 366 | + | name = "deltae" | |
| 367 | + | version = "0.3.2" | |
| 153 | 368 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 154 | − | checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" | |
| 369 | + | checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" | |
| 370 | + | ||
| 371 | + | [[package]] | |
| 372 | + | name = "deranged" | |
| 373 | + | version = "0.5.9" | |
| 374 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 375 | + | checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" | |
| 376 | + | ||
| 377 | + | [[package]] | |
| 378 | + | name = "derive_more" | |
| 379 | + | version = "2.1.1" | |
| 380 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 381 | + | checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" | |
| 382 | + | dependencies = [ | |
| 383 | + | "derive_more-impl", | |
| 384 | + | ] | |
| 385 | + | ||
| 386 | + | [[package]] | |
| 387 | + | name = "derive_more-impl" | |
| 388 | + | version = "2.1.1" | |
| 389 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 390 | + | checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" | |
| 155 | 391 | dependencies = [ | |
| 392 | + | "convert_case", | |
| 156 | 393 | "proc-macro2", | |
| 157 | 394 | "quote", | |
| 158 | − | "unicode-ident", | |
| 395 | + | "rustc_version", | |
| 396 | + | "syn 2.0.113", | |
| 159 | 397 | ] | |
| 160 | 398 | ||
| 161 | 399 | [[package]] | |
| 162 | − | name = "unicode-ident" | |
| 163 | − | version = "1.0.22" | |
| 400 | + | name = "digest" | |
| 401 | + | version = "0.10.7" | |
| 164 | 402 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 165 | − | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" | |
| 403 | + | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | |
| 404 | + | dependencies = [ | |
| 405 | + | "block-buffer", | |
| 406 | + | "crypto-common", | |
| 407 | + | ] | |
| 166 | 408 | ||
| 167 | 409 | [[package]] | |
| 168 | − | name = "utf8parse" | |
| 169 | − | version = "0.2.2" | |
| 410 | + | name = "document-features" | |
| 411 | + | version = "0.2.12" | |
| 170 | 412 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 171 | − | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | |
| 413 | + | checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" | |
| 414 | + | dependencies = [ | |
| 415 | + | "litrs", | |
| 416 | + | ] | |
| 172 | 417 | ||
| 173 | 418 | [[package]] | |
| 174 | − | name = "windows-link" | |
| 175 | − | version = "0.2.1" | |
| 419 | + | name = "either" | |
| 420 | + | version = "1.19.0" | |
| 176 | 421 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 177 | − | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | |
| 422 | + | checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" | |
| 178 | 423 | ||
| 179 | 424 | [[package]] | |
| 180 | − | name = "windows-sys" | |
| 181 | − | version = "0.61.2" | |
| 425 | + | name = "equivalent" | |
| 426 | + | version = "1.0.3" | |
| 182 | 427 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 183 | − | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
| 428 | + | checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" | |
| 429 | + | ||
| 430 | + | [[package]] | |
| 431 | + | name = "errno" | |
| 432 | + | version = "0.3.14" | |
| 433 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 434 | + | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 184 | 435 | dependencies = [ | |
| 185 | − | "windows-link", | |
| 436 | + | "libc", | |
| 437 | + | "windows-sys", | |
| 438 | + | ] | |
| 439 | + | ||
| 440 | + | [[package]] | |
| 441 | + | name = "euclid" | |
| 442 | + | version = "0.22.14" | |
| 443 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 444 | + | checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" | |
| 445 | + | dependencies = [ | |
| 446 | + | "num-traits", | |
| 447 | + | ] | |
| 448 | + | ||
| 449 | + | [[package]] | |
| 450 | + | name = "fancy-regex" | |
| 451 | + | version = "0.11.0" | |
| 452 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 453 | + | checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" | |
| 454 | + | dependencies = [ | |
| 455 | + | "bit-set", | |
| 456 | + | "regex", | |
| 457 | + | ] | |
| 458 | + | ||
| 459 | + | [[package]] | |
| 460 | + | name = "filedescriptor" | |
| 461 | + | version = "0.8.3" | |
| 462 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 463 | + | checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" | |
| 464 | + | dependencies = [ | |
| 465 | + | "libc", | |
| 466 | + | "thiserror 1.0.69", | |
| 467 | + | "winapi", | |
| 468 | + | ] | |
| 469 | + | ||
| 470 | + | [[package]] | |
| 471 | + | name = "find-msvc-tools" | |
| 472 | + | version = "0.1.15" | |
| 473 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 474 | + | checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" | |
| 475 | + | ||
| 476 | + | [[package]] | |
| 477 | + | name = "finl_unicode" | |
| 478 | + | version = "1.5.0" | |
| 479 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 480 | + | checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2" | |
| 481 | + | ||
| 482 | + | [[package]] | |
| 483 | + | name = "fixedbitset" | |
| 484 | + | version = "0.4.2" | |
| 485 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 486 | + | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" | |
| 487 | + | ||
| 488 | + | [[package]] | |
| 489 | + | name = "fnv" | |
| 490 | + | version = "1.0.7" | |
| 491 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 492 | + | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | |
| 493 | + | ||
| 494 | + | [[package]] | |
| 495 | + | name = "foldhash" | |
| 496 | + | version = "0.2.0" | |
| 497 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 498 | + | checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" | |
| 499 | + | ||
| 500 | + | [[package]] | |
| 501 | + | name = "futures-core" | |
| 502 | + | version = "0.3.34" | |
| 503 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 504 | + | checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" | |
| 505 | + | ||
| 506 | + | [[package]] | |
| 507 | + | name = "futures-task" | |
| 508 | + | version = "0.3.34" | |
| 509 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 510 | + | checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" | |
| 511 | + | ||
| 512 | + | [[package]] | |
| 513 | + | name = "futures-util" | |
| 514 | + | version = "0.3.34" | |
| 515 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 516 | + | checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" | |
| 517 | + | dependencies = [ | |
| 518 | + | "futures-core", | |
| 519 | + | "futures-task", | |
| 520 | + | "pin-project-lite", | |
| 521 | + | "slab", | |
| 522 | + | ] | |
| 523 | + | ||
| 524 | + | [[package]] | |
| 525 | + | name = "generic-array" | |
| 526 | + | version = "0.14.7" | |
| 527 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 528 | + | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | |
| 529 | + | dependencies = [ | |
| 530 | + | "typenum", | |
| 531 | + | "version_check", | |
| 532 | + | ] | |
| 533 | + | ||
| 534 | + | [[package]] | |
| 535 | + | name = "getrandom" | |
| 536 | + | version = "0.3.4" | |
| 537 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 538 | + | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | |
| 539 | + | dependencies = [ | |
| 540 | + | "cfg-if", | |
| 541 | + | "libc", | |
| 542 | + | "r-efi 5.3.0", | |
| 543 | + | "wasip2", | |
| 544 | + | ] | |
| 545 | + | ||
| 546 | + | [[package]] | |
| 547 | + | name = "getrandom" | |
| 548 | + | version = "0.4.3" | |
| 549 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 550 | + | checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" | |
| 551 | + | dependencies = [ | |
| 552 | + | "cfg-if", | |
| 553 | + | "libc", | |
| 554 | + | "r-efi 6.0.0", | |
| 555 | + | ] | |
| 556 | + | ||
| 557 | + | [[package]] | |
| 558 | + | name = "hashbrown" | |
| 559 | + | version = "0.17.1" | |
| 560 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 561 | + | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | |
| 562 | + | dependencies = [ | |
| 563 | + | "allocator-api2", | |
| 564 | + | "equivalent", | |
| 565 | + | "foldhash", | |
| 566 | + | ] | |
| 567 | + | ||
| 568 | + | [[package]] | |
| 569 | + | name = "heck" | |
| 570 | + | version = "0.5.0" | |
| 571 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 572 | + | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 573 | + | ||
| 574 | + | [[package]] | |
| 575 | + | name = "hex" | |
| 576 | + | version = "0.4.3" | |
| 577 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 578 | + | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" | |
| 579 | + | ||
| 580 | + | [[package]] | |
| 581 | + | name = "ident_case" | |
| 582 | + | version = "1.0.1" | |
| 583 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 584 | + | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" | |
| 585 | + | ||
| 586 | + | [[package]] | |
| 587 | + | name = "indoc" | |
| 588 | + | version = "2.0.8" | |
| 589 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 590 | + | checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" | |
| 591 | + | dependencies = [ | |
| 592 | + | "rustversion", | |
| 593 | + | ] | |
| 594 | + | ||
| 595 | + | [[package]] | |
| 596 | + | name = "instability" | |
| 597 | + | version = "0.3.14" | |
| 598 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 599 | + | checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20" | |
| 600 | + | dependencies = [ | |
| 601 | + | "darling", | |
| 602 | + | "indoc", | |
| 603 | + | "proc-macro2", | |
| 604 | + | "quote", | |
| 605 | + | "syn 3.0.9", | |
| 186 | 606 | ] | |
| 607 | + | ||
| 608 | + | [[package]] | |
| 609 | + | name = "is_terminal_polyfill" | |
| 610 | + | version = "1.70.2" | |
| 611 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 612 | + | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" | |
| 613 | + | ||
| 614 | + | [[package]] | |
| 615 | + | name = "itertools" | |
| 616 | + | version = "0.14.0" | |
| 617 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 618 | + | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" | |
| 619 | + | dependencies = [ | |
| 620 | + | "either", | |
| 621 | + | ] | |
| 622 | + | ||
| 623 | + | [[package]] | |
| 624 | + | name = "itoa" | |
| 625 | + | version = "1.0.18" | |
| 626 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 627 | + | checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" | |
| 628 | + | ||
| 629 | + | [[package]] | |
| 630 | + | name = "js-sys" | |
| 631 | + | version = "0.3.106" | |
| 632 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 633 | + | checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" | |
| 634 | + | dependencies = [ | |
| 635 | + | "cfg-if", | |
| 636 | + | "futures-util", | |
| 637 | + | "wasm-bindgen", | |
| 638 | + | ] | |
| 639 | + | ||
| 640 | + | [[package]] | |
| 641 | + | name = "kasuari" | |
| 642 | + | version = "0.4.13" | |
| 643 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 644 | + | checksum = "72f14c90e40a6133f4805b83542a7ed7364f56070b447f56e7f4e90297122209" | |
| 645 | + | dependencies = [ | |
| 646 | + | "hashbrown", | |
| 647 | + | "portable-atomic", | |
| 648 | + | "thiserror 2.0.21", | |
| 649 | + | ] | |
| 650 | + | ||
| 651 | + | [[package]] | |
| 652 | + | name = "lab" | |
| 653 | + | version = "0.11.0" | |
| 654 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 655 | + | checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" | |
| 656 | + | ||
| 657 | + | [[package]] | |
| 658 | + | name = "lazy_static" | |
| 659 | + | version = "1.5.1" | |
| 660 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 661 | + | checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" | |
| 662 | + | ||
| 663 | + | [[package]] | |
| 664 | + | name = "libc" | |
| 665 | + | version = "0.2.191" | |
| 666 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 667 | + | checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" | |
| 668 | + | ||
| 669 | + | [[package]] | |
| 670 | + | name = "libm" | |
| 671 | + | version = "0.2.16" | |
| 672 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 673 | + | checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" | |
| 674 | + | ||
| 675 | + | [[package]] | |
| 676 | + | name = "line-clipping" | |
| 677 | + | version = "0.3.8" | |
| 678 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 679 | + | checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" | |
| 680 | + | dependencies = [ | |
| 681 | + | "bitflags 2.13.2", | |
| 682 | + | ] | |
| 683 | + | ||
| 684 | + | [[package]] | |
| 685 | + | name = "linux-raw-sys" | |
| 686 | + | version = "0.12.1" | |
| 687 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 688 | + | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | |
| 689 | + | ||
| 690 | + | [[package]] | |
| 691 | + | name = "litrs" | |
| 692 | + | version = "1.0.1" | |
| 693 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 694 | + | checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" | |
| 695 | + | ||
| 696 | + | [[package]] | |
| 697 | + | name = "lock_api" | |
| 698 | + | version = "0.4.14" | |
| 699 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 700 | + | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" | |
| 701 | + | dependencies = [ | |
| 702 | + | "scopeguard", | |
| 703 | + | ] | |
| 704 | + | ||
| 705 | + | [[package]] | |
| 706 | + | name = "log" | |
| 707 | + | version = "0.4.34" | |
| 708 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 709 | + | checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" | |
| 710 | + | ||
| 711 | + | [[package]] | |
| 712 | + | name = "lru" | |
| 713 | + | version = "0.18.5" | |
| 714 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 715 | + | checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" | |
| 716 | + | dependencies = [ | |
| 717 | + | "hashbrown", | |
| 718 | + | ] | |
| 719 | + | ||
| 720 | + | [[package]] | |
| 721 | + | name = "mac_address" | |
| 722 | + | version = "1.2.1" | |
| 723 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 724 | + | checksum = "f3d0581a75c45969c63afd2bb87b1d8b25cfcc556c7a918cf9bb13b0fe8d2381" | |
| 725 | + | dependencies = [ | |
| 726 | + | "nix 0.31.3", | |
| 727 | + | "windows-sys", | |
| 728 | + | ] | |
| 729 | + | ||
| 730 | + | [[package]] | |
| 731 | + | name = "memchr" | |
| 732 | + | version = "2.8.3" | |
| 733 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 734 | + | checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" | |
| 735 | + | ||
| 736 | + | [[package]] | |
| 737 | + | name = "memmem" | |
| 738 | + | version = "0.1.1" | |
| 739 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 740 | + | checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" | |
| 741 | + | ||
| 742 | + | [[package]] | |
| 743 | + | name = "memoffset" | |
| 744 | + | version = "0.9.1" | |
| 745 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 746 | + | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" | |
| 747 | + | dependencies = [ | |
| 748 | + | "autocfg", | |
| 749 | + | ] | |
| 750 | + | ||
| 751 | + | [[package]] | |
| 752 | + | name = "minimal-lexical" | |
| 753 | + | version = "0.2.1" | |
| 754 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 755 | + | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" | |
| 756 | + | ||
| 757 | + | [[package]] | |
| 758 | + | name = "mio" | |
| 759 | + | version = "1.2.4" | |
| 760 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 761 | + | checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" | |
| 762 | + | dependencies = [ | |
| 763 | + | "libc", | |
| 764 | + | "log", | |
| 765 | + | "wasi", | |
| 766 | + | "windows-sys", | |
| 767 | + | ] | |
| 768 | + | ||
| 769 | + | [[package]] | |
| 770 | + | name = "nix" | |
| 771 | + | version = "0.29.0" | |
| 772 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 773 | + | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" | |
| 774 | + | dependencies = [ | |
| 775 | + | "bitflags 2.13.2", | |
| 776 | + | "cfg-if", | |
| 777 | + | "cfg_aliases", | |
| 778 | + | "libc", | |
| 779 | + | ] | |
| 780 | + | ||
| 781 | + | [[package]] | |
| 782 | + | name = "nix" | |
| 783 | + | version = "0.31.3" | |
| 784 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 785 | + | checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" | |
| 786 | + | dependencies = [ | |
| 787 | + | "bitflags 2.13.2", | |
| 788 | + | "cfg-if", | |
| 789 | + | "cfg_aliases", | |
| 790 | + | "libc", | |
| 791 | + | "memoffset", | |
| 792 | + | ] | |
| 793 | + | ||
| 794 | + | [[package]] | |
| 795 | + | name = "nom" | |
| 796 | + | version = "7.1.3" | |
| 797 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 798 | + | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" | |
| 799 | + | dependencies = [ | |
| 800 | + | "memchr", | |
| 801 | + | "minimal-lexical", | |
| 802 | + | ] | |
| 803 | + | ||
| 804 | + | [[package]] | |
| 805 | + | name = "num-conv" | |
| 806 | + | version = "0.2.2" | |
| 807 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 808 | + | checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" | |
| 809 | + | ||
| 810 | + | [[package]] | |
| 811 | + | name = "num-derive" | |
| 812 | + | version = "0.4.2" | |
| 813 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 814 | + | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" | |
| 815 | + | dependencies = [ | |
| 816 | + | "proc-macro2", | |
| 817 | + | "quote", | |
| 818 | + | "syn 2.0.113", | |
| 819 | + | ] | |
| 820 | + | ||
| 821 | + | [[package]] | |
| 822 | + | name = "num-traits" | |
| 823 | + | version = "0.2.19" | |
| 824 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 825 | + | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | |
| 826 | + | dependencies = [ | |
| 827 | + | "autocfg", | |
| 828 | + | ] | |
| 829 | + | ||
| 830 | + | [[package]] | |
| 831 | + | name = "num_threads" | |
| 832 | + | version = "0.1.8" | |
| 833 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 834 | + | checksum = "cabcc654086861f9e1bc5115258de62ef92c79b20f4615a1b0cc57c4e82027d4" | |
| 835 | + | dependencies = [ | |
| 836 | + | "libc", | |
| 837 | + | ] | |
| 838 | + | ||
| 839 | + | [[package]] | |
| 840 | + | name = "once_cell" | |
| 841 | + | version = "1.21.4" | |
| 842 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 843 | + | checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" | |
| 844 | + | ||
| 845 | + | [[package]] | |
| 846 | + | name = "once_cell_polyfill" | |
| 847 | + | version = "1.70.2" | |
| 848 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 849 | + | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | |
| 850 | + | ||
| 851 | + | [[package]] | |
| 852 | + | name = "ordered-float" | |
| 853 | + | version = "4.6.0" | |
| 854 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 855 | + | checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" | |
| 856 | + | dependencies = [ | |
| 857 | + | "num-traits", | |
| 858 | + | ] | |
| 859 | + | ||
| 860 | + | [[package]] | |
| 861 | + | name = "palette" | |
| 862 | + | version = "0.7.7" | |
| 863 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 864 | + | checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" | |
| 865 | + | dependencies = [ | |
| 866 | + | "approx", | |
| 867 | + | "libm", | |
| 868 | + | "palette_derive", | |
| 869 | + | "palette_math", | |
| 870 | + | ] | |
| 871 | + | ||
| 872 | + | [[package]] | |
| 873 | + | name = "palette_derive" | |
| 874 | + | version = "0.7.7" | |
| 875 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 876 | + | checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" | |
| 877 | + | dependencies = [ | |
| 878 | + | "by_address", | |
| 879 | + | "proc-macro2", | |
| 880 | + | "quote", | |
| 881 | + | "syn 2.0.113", | |
| 882 | + | ] | |
| 883 | + | ||
| 884 | + | [[package]] | |
| 885 | + | name = "palette_math" | |
| 886 | + | version = "0.7.7" | |
| 887 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 888 | + | checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" | |
| 889 | + | dependencies = [ | |
| 890 | + | "libm", | |
| 891 | + | ] | |
| 892 | + | ||
| 893 | + | [[package]] | |
| 894 | + | name = "parking_lot" | |
| 895 | + | version = "0.12.5" | |
| 896 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 897 | + | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" | |
| 898 | + | dependencies = [ | |
| 899 | + | "lock_api", | |
| 900 | + | "parking_lot_core", | |
| 901 | + | ] | |
| 902 | + | ||
| 903 | + | [[package]] | |
| 904 | + | name = "parking_lot_core" | |
| 905 | + | version = "0.9.12" | |
| 906 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 907 | + | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" | |
| 908 | + | dependencies = [ | |
| 909 | + | "cfg-if", | |
| 910 | + | "libc", | |
| 911 | + | "redox_syscall", | |
| 912 | + | "smallvec", | |
| 913 | + | "windows-link", | |
| 914 | + | ] | |
| 915 | + | ||
| 916 | + | [[package]] | |
| 917 | + | name = "pest" | |
| 918 | + | version = "2.9.4" | |
| 919 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 920 | + | checksum = "fd47a460a9cbc3b3429c7e22bf0d76653e779319bae45a499ca010c9a990bb23" | |
| 921 | + | dependencies = [ | |
| 922 | + | "memchr", | |
| 923 | + | "psm", | |
| 924 | + | "stacker", | |
| 925 | + | "ucd-trie", | |
| 926 | + | ] | |
| 927 | + | ||
| 928 | + | [[package]] | |
| 929 | + | name = "pest_derive" | |
| 930 | + | version = "2.9.4" | |
| 931 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 932 | + | checksum = "203e46697708f7519ab1608393cf89239cafb46b8e6d08a8ffe359359b11ef44" | |
| 933 | + | dependencies = [ | |
| 934 | + | "pest", | |
| 935 | + | "pest_generator", | |
| 936 | + | ] | |
| 937 | + | ||
| 938 | + | [[package]] | |
| 939 | + | name = "pest_generator" | |
| 940 | + | version = "2.9.4" | |
| 941 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 942 | + | checksum = "e84ae6340a458d3d6cfc73cf328cf3cfe90b2484cd0de25eb66aa954615ed4d5" | |
| 943 | + | dependencies = [ | |
| 944 | + | "pest", | |
| 945 | + | "pest_meta", | |
| 946 | + | "proc-macro2", | |
| 947 | + | "quote", | |
| 948 | + | "syn 3.0.9", | |
| 949 | + | ] | |
| 950 | + | ||
| 951 | + | [[package]] | |
| 952 | + | name = "pest_meta" | |
| 953 | + | version = "2.9.4" | |
| 954 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 955 | + | checksum = "b57f92f32b3dbce6598fd8f5cdf1f06a6072570b8103fa21728fa9036923e710" | |
| 956 | + | dependencies = [ | |
| 957 | + | "pest", | |
| 958 | + | ] | |
| 959 | + | ||
| 960 | + | [[package]] | |
| 961 | + | name = "phf" | |
| 962 | + | version = "0.11.3" | |
| 963 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 964 | + | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" | |
| 965 | + | dependencies = [ | |
| 966 | + | "phf_macros", | |
| 967 | + | "phf_shared", | |
| 968 | + | ] | |
| 969 | + | ||
| 970 | + | [[package]] | |
| 971 | + | name = "phf_codegen" | |
| 972 | + | version = "0.11.3" | |
| 973 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 974 | + | checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" | |
| 975 | + | dependencies = [ | |
| 976 | + | "phf_generator", | |
| 977 | + | "phf_shared", | |
| 978 | + | ] | |
| 979 | + | ||
| 980 | + | [[package]] | |
| 981 | + | name = "phf_generator" | |
| 982 | + | version = "0.11.3" | |
| 983 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 984 | + | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" | |
| 985 | + | dependencies = [ | |
| 986 | + | "phf_shared", | |
| 987 | + | "rand", | |
| 988 | + | ] | |
| 989 | + | ||
| 990 | + | [[package]] | |
| 991 | + | name = "phf_macros" | |
| 992 | + | version = "0.11.3" | |
| 993 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 994 | + | checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" | |
| 995 | + | dependencies = [ | |
| 996 | + | "phf_generator", | |
| 997 | + | "phf_shared", | |
| 998 | + | "proc-macro2", | |
| 999 | + | "quote", | |
| 1000 | + | "syn 2.0.113", | |
| 1001 | + | ] | |
| 1002 | + | ||
| 1003 | + | [[package]] | |
| 1004 | + | name = "phf_shared" | |
| 1005 | + | version = "0.11.3" | |
| 1006 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1007 | + | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" | |
| 1008 | + | dependencies = [ | |
| 1009 | + | "siphasher", | |
| 1010 | + | ] | |
| 1011 | + | ||
| 1012 | + | [[package]] | |
| 1013 | + | name = "pin-project-lite" | |
| 1014 | + | version = "0.2.17" | |
| 1015 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1016 | + | checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" | |
| 1017 | + | ||
| 1018 | + | [[package]] | |
| 1019 | + | name = "portable-atomic" | |
| 1020 | + | version = "1.15.0" | |
| 1021 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1022 | + | checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" | |
| 1023 | + | ||
| 1024 | + | [[package]] | |
| 1025 | + | name = "powerfmt" | |
| 1026 | + | version = "0.2.1" | |
| 1027 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1028 | + | checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" | |
| 1029 | + | ||
| 1030 | + | [[package]] | |
| 1031 | + | name = "proc-macro2" | |
| 1032 | + | version = "1.0.104" | |
| 1033 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1034 | + | checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" | |
| 1035 | + | dependencies = [ | |
| 1036 | + | "unicode-ident", | |
| 1037 | + | ] | |
| 1038 | + | ||
| 1039 | + | [[package]] | |
| 1040 | + | name = "psm" | |
| 1041 | + | version = "0.1.24" | |
| 1042 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1043 | + | checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" | |
| 1044 | + | dependencies = [ | |
| 1045 | + | "cc", | |
| 1046 | + | ] | |
| 1047 | + | ||
| 1048 | + | [[package]] | |
| 1049 | + | name = "quote" | |
| 1050 | + | version = "1.0.42" | |
| 1051 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1052 | + | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" | |
| 1053 | + | dependencies = [ | |
| 1054 | + | "proc-macro2", | |
| 1055 | + | ] | |
| 1056 | + | ||
| 1057 | + | [[package]] | |
| 1058 | + | name = "r-efi" | |
| 1059 | + | version = "5.3.0" | |
| 1060 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1061 | + | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | |
| 1062 | + | ||
| 1063 | + | [[package]] | |
| 1064 | + | name = "r-efi" | |
| 1065 | + | version = "6.0.0" | |
| 1066 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1067 | + | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | |
| 1068 | + | ||
| 1069 | + | [[package]] | |
| 1070 | + | name = "rand" | |
| 1071 | + | version = "0.8.8" | |
| 1072 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1073 | + | checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" | |
| 1074 | + | dependencies = [ | |
| 1075 | + | "rand_core", | |
| 1076 | + | ] | |
| 1077 | + | ||
| 1078 | + | [[package]] | |
| 1079 | + | name = "rand_core" | |
| 1080 | + | version = "0.6.4" | |
| 1081 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1082 | + | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | |
| 1083 | + | ||
| 1084 | + | [[package]] | |
| 1085 | + | name = "ratatui" | |
| 1086 | + | version = "0.30.2" | |
| 1087 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1088 | + | checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" | |
| 1089 | + | dependencies = [ | |
| 1090 | + | "instability", | |
| 1091 | + | "ratatui-core", | |
| 1092 | + | "ratatui-crossterm", | |
| 1093 | + | "ratatui-macros", | |
| 1094 | + | "ratatui-termina", | |
| 1095 | + | "ratatui-termwiz", | |
| 1096 | + | "ratatui-widgets", | |
| 1097 | + | "serde", | |
| 1098 | + | ] | |
| 1099 | + | ||
| 1100 | + | [[package]] | |
| 1101 | + | name = "ratatui-core" | |
| 1102 | + | version = "0.1.2" | |
| 1103 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1104 | + | checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" | |
| 1105 | + | dependencies = [ | |
| 1106 | + | "bitflags 2.13.2", | |
| 1107 | + | "compact_str", | |
| 1108 | + | "critical-section", | |
| 1109 | + | "hashbrown", | |
| 1110 | + | "itertools", | |
| 1111 | + | "kasuari", | |
| 1112 | + | "lru", | |
| 1113 | + | "palette", | |
| 1114 | + | "serde", | |
| 1115 | + | "strum", | |
| 1116 | + | "thiserror 2.0.21", | |
| 1117 | + | "unicode-segmentation", | |
| 1118 | + | "unicode-truncate", | |
| 1119 | + | "unicode-width", | |
| 1120 | + | ] | |
| 1121 | + | ||
| 1122 | + | [[package]] | |
| 1123 | + | name = "ratatui-crossterm" | |
| 1124 | + | version = "0.1.2" | |
| 1125 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1126 | + | checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" | |
| 1127 | + | dependencies = [ | |
| 1128 | + | "cfg-if", | |
| 1129 | + | "crossterm", | |
| 1130 | + | "instability", | |
| 1131 | + | "ratatui-core", | |
| 1132 | + | ] | |
| 1133 | + | ||
| 1134 | + | [[package]] | |
| 1135 | + | name = "ratatui-macros" | |
| 1136 | + | version = "0.7.2" | |
| 1137 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1138 | + | checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" | |
| 1139 | + | dependencies = [ | |
| 1140 | + | "ratatui-core", | |
| 1141 | + | "ratatui-widgets", | |
| 1142 | + | ] | |
| 1143 | + | ||
| 1144 | + | [[package]] | |
| 1145 | + | name = "ratatui-termina" | |
| 1146 | + | version = "0.1.0" | |
| 1147 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1148 | + | checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" | |
| 1149 | + | dependencies = [ | |
| 1150 | + | "instability", | |
| 1151 | + | "ratatui-core", | |
| 1152 | + | "termina", | |
| 1153 | + | ] | |
| 1154 | + | ||
| 1155 | + | [[package]] | |
| 1156 | + | name = "ratatui-termwiz" | |
| 1157 | + | version = "0.1.2" | |
| 1158 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1159 | + | checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" | |
| 1160 | + | dependencies = [ | |
| 1161 | + | "ratatui-core", | |
| 1162 | + | "termwiz", | |
| 1163 | + | ] | |
| 1164 | + | ||
| 1165 | + | [[package]] | |
| 1166 | + | name = "ratatui-textarea" | |
| 1167 | + | version = "0.9.3" | |
| 1168 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1169 | + | checksum = "78cdce79220909bec1bb8307c29fad7d0df7434280da6ecaf44cb208393bed00" | |
| 1170 | + | dependencies = [ | |
| 1171 | + | "ratatui-core", | |
| 1172 | + | "ratatui-crossterm", | |
| 1173 | + | "ratatui-widgets", | |
| 1174 | + | "unicode-segmentation", | |
| 1175 | + | "unicode-width", | |
| 1176 | + | ] | |
| 1177 | + | ||
| 1178 | + | [[package]] | |
| 1179 | + | name = "ratatui-widgets" | |
| 1180 | + | version = "0.3.2" | |
| 1181 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1182 | + | checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" | |
| 1183 | + | dependencies = [ | |
| 1184 | + | "bitflags 2.13.2", | |
| 1185 | + | "hashbrown", | |
| 1186 | + | "indoc", | |
| 1187 | + | "instability", | |
| 1188 | + | "itertools", | |
| 1189 | + | "line-clipping", | |
| 1190 | + | "ratatui-core", | |
| 1191 | + | "serde", | |
| 1192 | + | "strum", | |
| 1193 | + | "time", | |
| 1194 | + | "unicode-segmentation", | |
| 1195 | + | "unicode-width", | |
| 1196 | + | ] | |
| 1197 | + | ||
| 1198 | + | [[package]] | |
| 1199 | + | name = "redox_syscall" | |
| 1200 | + | version = "0.5.18" | |
| 1201 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1202 | + | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" | |
| 1203 | + | dependencies = [ | |
| 1204 | + | "bitflags 2.13.2", | |
| 1205 | + | ] | |
| 1206 | + | ||
| 1207 | + | [[package]] | |
| 1208 | + | name = "regex" | |
| 1209 | + | version = "1.13.1" | |
| 1210 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1211 | + | checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" | |
| 1212 | + | dependencies = [ | |
| 1213 | + | "aho-corasick", | |
| 1214 | + | "memchr", | |
| 1215 | + | "regex-automata", | |
| 1216 | + | "regex-syntax", | |
| 1217 | + | ] | |
| 1218 | + | ||
| 1219 | + | [[package]] | |
| 1220 | + | name = "regex-automata" | |
| 1221 | + | version = "0.4.18" | |
| 1222 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1223 | + | checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" | |
| 1224 | + | dependencies = [ | |
| 1225 | + | "aho-corasick", | |
| 1226 | + | "memchr", | |
| 1227 | + | "regex-syntax", | |
| 1228 | + | ] | |
| 1229 | + | ||
| 1230 | + | [[package]] | |
| 1231 | + | name = "regex-syntax" | |
| 1232 | + | version = "0.8.11" | |
| 1233 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1234 | + | checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" | |
| 1235 | + | ||
| 1236 | + | [[package]] | |
| 1237 | + | name = "rustc_version" | |
| 1238 | + | version = "0.4.1" | |
| 1239 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1240 | + | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" | |
| 1241 | + | dependencies = [ | |
| 1242 | + | "semver", | |
| 1243 | + | ] | |
| 1244 | + | ||
| 1245 | + | [[package]] | |
| 1246 | + | name = "rustix" | |
| 1247 | + | version = "1.1.5" | |
| 1248 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1249 | + | checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" | |
| 1250 | + | dependencies = [ | |
| 1251 | + | "bitflags 2.13.2", | |
| 1252 | + | "errno", | |
| 1253 | + | "libc", | |
| 1254 | + | "linux-raw-sys", | |
| 1255 | + | "windows-sys", | |
| 1256 | + | ] | |
| 1257 | + | ||
| 1258 | + | [[package]] | |
| 1259 | + | name = "rustversion" | |
| 1260 | + | version = "1.0.23" | |
| 1261 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1262 | + | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" | |
| 1263 | + | ||
| 1264 | + | [[package]] | |
| 1265 | + | name = "ryu" | |
| 1266 | + | version = "1.0.23" | |
| 1267 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1268 | + | checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" | |
| 1269 | + | ||
| 1270 | + | [[package]] | |
| 1271 | + | name = "scopeguard" | |
| 1272 | + | version = "1.2.0" | |
| 1273 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1274 | + | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | |
| 1275 | + | ||
| 1276 | + | [[package]] | |
| 1277 | + | name = "semver" | |
| 1278 | + | version = "1.0.28" | |
| 1279 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1280 | + | checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" | |
| 1281 | + | ||
| 1282 | + | [[package]] | |
| 1283 | + | name = "serde" | |
| 1284 | + | version = "1.0.229" | |
| 1285 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1286 | + | checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" | |
| 1287 | + | dependencies = [ | |
| 1288 | + | "serde_core", | |
| 1289 | + | "serde_derive", | |
| 1290 | + | ] | |
| 1291 | + | ||
| 1292 | + | [[package]] | |
| 1293 | + | name = "serde_core" | |
| 1294 | + | version = "1.0.229" | |
| 1295 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1296 | + | checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" | |
| 1297 | + | dependencies = [ | |
| 1298 | + | "serde_derive", | |
| 1299 | + | ] | |
| 1300 | + | ||
| 1301 | + | [[package]] | |
| 1302 | + | name = "serde_derive" | |
| 1303 | + | version = "1.0.229" | |
| 1304 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1305 | + | checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" | |
| 1306 | + | dependencies = [ | |
| 1307 | + | "proc-macro2", | |
| 1308 | + | "quote", | |
| 1309 | + | "syn 3.0.9", | |
| 1310 | + | ] | |
| 1311 | + | ||
| 1312 | + | [[package]] | |
| 1313 | + | name = "sha2" | |
| 1314 | + | version = "0.10.9" | |
| 1315 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1316 | + | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" | |
| 1317 | + | dependencies = [ | |
| 1318 | + | "cfg-if", | |
| 1319 | + | "cpufeatures", | |
| 1320 | + | "digest", | |
| 1321 | + | ] | |
| 1322 | + | ||
| 1323 | + | [[package]] | |
| 1324 | + | name = "shlex" | |
| 1325 | + | version = "2.0.1" | |
| 1326 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1327 | + | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | |
| 1328 | + | ||
| 1329 | + | [[package]] | |
| 1330 | + | name = "signal-hook" | |
| 1331 | + | version = "0.3.18" | |
| 1332 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1333 | + | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" | |
| 1334 | + | dependencies = [ | |
| 1335 | + | "libc", | |
| 1336 | + | "signal-hook-registry", | |
| 1337 | + | ] | |
| 1338 | + | ||
| 1339 | + | [[package]] | |
| 1340 | + | name = "signal-hook-mio" | |
| 1341 | + | version = "0.2.5" | |
| 1342 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1343 | + | checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" | |
| 1344 | + | dependencies = [ | |
| 1345 | + | "libc", | |
| 1346 | + | "mio", | |
| 1347 | + | "signal-hook", | |
| 1348 | + | ] | |
| 1349 | + | ||
| 1350 | + | [[package]] | |
| 1351 | + | name = "signal-hook-registry" | |
| 1352 | + | version = "1.4.8" | |
| 1353 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1354 | + | checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | |
| 1355 | + | dependencies = [ | |
| 1356 | + | "errno", | |
| 1357 | + | "libc", | |
| 1358 | + | ] | |
| 1359 | + | ||
| 1360 | + | [[package]] | |
| 1361 | + | name = "siphasher" | |
| 1362 | + | version = "1.0.4" | |
| 1363 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1364 | + | checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" | |
| 1365 | + | ||
| 1366 | + | [[package]] | |
| 1367 | + | name = "slab" | |
| 1368 | + | version = "0.4.12" | |
| 1369 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1370 | + | checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" | |
| 1371 | + | ||
| 1372 | + | [[package]] | |
| 1373 | + | name = "smallvec" | |
| 1374 | + | version = "1.16.3" | |
| 1375 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1376 | + | checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" | |
| 1377 | + | ||
| 1378 | + | [[package]] | |
| 1379 | + | name = "stacker" | |
| 1380 | + | version = "0.1.25" | |
| 1381 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1382 | + | checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" | |
| 1383 | + | dependencies = [ | |
| 1384 | + | "cc", | |
| 1385 | + | "cfg-if", | |
| 1386 | + | "libc", | |
| 1387 | + | "psm", | |
| 1388 | + | "windows-sys", | |
| 1389 | + | ] | |
| 1390 | + | ||
| 1391 | + | [[package]] | |
| 1392 | + | name = "static_assertions" | |
| 1393 | + | version = "1.1.0" | |
| 1394 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1395 | + | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" | |
| 1396 | + | ||
| 1397 | + | [[package]] | |
| 1398 | + | name = "strsim" | |
| 1399 | + | version = "0.11.1" | |
| 1400 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1401 | + | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | |
| 1402 | + | ||
| 1403 | + | [[package]] | |
| 1404 | + | name = "strum" | |
| 1405 | + | version = "0.28.0" | |
| 1406 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1407 | + | checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" | |
| 1408 | + | dependencies = [ | |
| 1409 | + | "strum_macros", | |
| 1410 | + | ] | |
| 1411 | + | ||
| 1412 | + | [[package]] | |
| 1413 | + | name = "strum_macros" | |
| 1414 | + | version = "0.28.0" | |
| 1415 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1416 | + | checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" | |
| 1417 | + | dependencies = [ | |
| 1418 | + | "heck", | |
| 1419 | + | "proc-macro2", | |
| 1420 | + | "quote", | |
| 1421 | + | "syn 2.0.113", | |
| 1422 | + | ] | |
| 1423 | + | ||
| 1424 | + | [[package]] | |
| 1425 | + | name = "syn" | |
| 1426 | + | version = "1.0.109" | |
| 1427 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1428 | + | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" | |
| 1429 | + | dependencies = [ | |
| 1430 | + | "proc-macro2", | |
| 1431 | + | "quote", | |
| 1432 | + | "unicode-ident", | |
| 1433 | + | ] | |
| 1434 | + | ||
| 1435 | + | [[package]] | |
| 1436 | + | name = "syn" | |
| 1437 | + | version = "2.0.113" | |
| 1438 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1439 | + | checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" | |
| 1440 | + | dependencies = [ | |
| 1441 | + | "proc-macro2", | |
| 1442 | + | "quote", | |
| 1443 | + | "unicode-ident", | |
| 1444 | + | ] | |
| 1445 | + | ||
| 1446 | + | [[package]] | |
| 1447 | + | name = "syn" | |
| 1448 | + | version = "3.0.9" | |
| 1449 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1450 | + | checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" | |
| 1451 | + | dependencies = [ | |
| 1452 | + | "proc-macro2", | |
| 1453 | + | "quote", | |
| 1454 | + | "unicode-ident", | |
| 1455 | + | ] | |
| 1456 | + | ||
| 1457 | + | [[package]] | |
| 1458 | + | name = "termina" | |
| 1459 | + | version = "0.3.3" | |
| 1460 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1461 | + | checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" | |
| 1462 | + | dependencies = [ | |
| 1463 | + | "bitflags 2.13.2", | |
| 1464 | + | "parking_lot", | |
| 1465 | + | "rustix", | |
| 1466 | + | "signal-hook", | |
| 1467 | + | "windows-sys", | |
| 1468 | + | ] | |
| 1469 | + | ||
| 1470 | + | [[package]] | |
| 1471 | + | name = "terminfo" | |
| 1472 | + | version = "0.9.0" | |
| 1473 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1474 | + | checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" | |
| 1475 | + | dependencies = [ | |
| 1476 | + | "fnv", | |
| 1477 | + | "nom", | |
| 1478 | + | "phf", | |
| 1479 | + | "phf_codegen", | |
| 1480 | + | ] | |
| 1481 | + | ||
| 1482 | + | [[package]] | |
| 1483 | + | name = "termios" | |
| 1484 | + | version = "0.3.3" | |
| 1485 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1486 | + | checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" | |
| 1487 | + | dependencies = [ | |
| 1488 | + | "libc", | |
| 1489 | + | ] | |
| 1490 | + | ||
| 1491 | + | [[package]] | |
| 1492 | + | name = "termwiz" | |
| 1493 | + | version = "0.23.3" | |
| 1494 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1495 | + | checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" | |
| 1496 | + | dependencies = [ | |
| 1497 | + | "anyhow", | |
| 1498 | + | "base64", | |
| 1499 | + | "bitflags 2.13.2", | |
| 1500 | + | "fancy-regex", | |
| 1501 | + | "filedescriptor", | |
| 1502 | + | "finl_unicode", | |
| 1503 | + | "fixedbitset", | |
| 1504 | + | "hex", | |
| 1505 | + | "lazy_static", | |
| 1506 | + | "libc", | |
| 1507 | + | "log", | |
| 1508 | + | "memmem", | |
| 1509 | + | "nix 0.29.0", | |
| 1510 | + | "num-derive", | |
| 1511 | + | "num-traits", | |
| 1512 | + | "ordered-float", | |
| 1513 | + | "pest", | |
| 1514 | + | "pest_derive", | |
| 1515 | + | "phf", | |
| 1516 | + | "sha2", | |
| 1517 | + | "signal-hook", | |
| 1518 | + | "siphasher", | |
| 1519 | + | "terminfo", | |
| 1520 | + | "termios", | |
| 1521 | + | "thiserror 1.0.69", | |
| 1522 | + | "ucd-trie", | |
| 1523 | + | "unicode-segmentation", | |
| 1524 | + | "vtparse", | |
| 1525 | + | "wezterm-bidi", | |
| 1526 | + | "wezterm-blob-leases", | |
| 1527 | + | "wezterm-color-types", | |
| 1528 | + | "wezterm-dynamic", | |
| 1529 | + | "wezterm-input-types", | |
| 1530 | + | "winapi", | |
| 1531 | + | ] | |
| 1532 | + | ||
| 1533 | + | [[package]] | |
| 1534 | + | name = "thiserror" | |
| 1535 | + | version = "1.0.69" | |
| 1536 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1537 | + | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" | |
| 1538 | + | dependencies = [ | |
| 1539 | + | "thiserror-impl 1.0.69", | |
| 1540 | + | ] | |
| 1541 | + | ||
| 1542 | + | [[package]] | |
| 1543 | + | name = "thiserror" | |
| 1544 | + | version = "2.0.21" | |
| 1545 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1546 | + | checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" | |
| 1547 | + | dependencies = [ | |
| 1548 | + | "thiserror-impl 2.0.21", | |
| 1549 | + | ] | |
| 1550 | + | ||
| 1551 | + | [[package]] | |
| 1552 | + | name = "thiserror-impl" | |
| 1553 | + | version = "1.0.69" | |
| 1554 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1555 | + | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | |
| 1556 | + | dependencies = [ | |
| 1557 | + | "proc-macro2", | |
| 1558 | + | "quote", | |
| 1559 | + | "syn 2.0.113", | |
| 1560 | + | ] | |
| 1561 | + | ||
| 1562 | + | [[package]] | |
| 1563 | + | name = "thiserror-impl" | |
| 1564 | + | version = "2.0.21" | |
| 1565 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1566 | + | checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" | |
| 1567 | + | dependencies = [ | |
| 1568 | + | "proc-macro2", | |
| 1569 | + | "quote", | |
| 1570 | + | "syn 3.0.9", | |
| 1571 | + | ] | |
| 1572 | + | ||
| 1573 | + | [[package]] | |
| 1574 | + | name = "time" | |
| 1575 | + | version = "0.3.55" | |
| 1576 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1577 | + | checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" | |
| 1578 | + | dependencies = [ | |
| 1579 | + | "deranged", | |
| 1580 | + | "libc", | |
| 1581 | + | "num-conv", | |
| 1582 | + | "num_threads", | |
| 1583 | + | "powerfmt", | |
| 1584 | + | "serde_core", | |
| 1585 | + | "time-core", | |
| 1586 | + | ] | |
| 1587 | + | ||
| 1588 | + | [[package]] | |
| 1589 | + | name = "time-core" | |
| 1590 | + | version = "0.1.9" | |
| 1591 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1592 | + | checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" | |
| 1593 | + | ||
| 1594 | + | [[package]] | |
| 1595 | + | name = "typenum" | |
| 1596 | + | version = "1.20.1" | |
| 1597 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1598 | + | checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" | |
| 1599 | + | ||
| 1600 | + | [[package]] | |
| 1601 | + | name = "ucd-trie" | |
| 1602 | + | version = "0.1.7" | |
| 1603 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1604 | + | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" | |
| 1605 | + | ||
| 1606 | + | [[package]] | |
| 1607 | + | name = "unicode-ident" | |
| 1608 | + | version = "1.0.22" | |
| 1609 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1610 | + | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" | |
| 1611 | + | ||
| 1612 | + | [[package]] | |
| 1613 | + | name = "unicode-segmentation" | |
| 1614 | + | version = "1.13.3" | |
| 1615 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1616 | + | checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" | |
| 1617 | + | ||
| 1618 | + | [[package]] | |
| 1619 | + | name = "unicode-truncate" | |
| 1620 | + | version = "2.0.1" | |
| 1621 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1622 | + | checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" | |
| 1623 | + | dependencies = [ | |
| 1624 | + | "itertools", | |
| 1625 | + | "unicode-segmentation", | |
| 1626 | + | "unicode-width", | |
| 1627 | + | ] | |
| 1628 | + | ||
| 1629 | + | [[package]] | |
| 1630 | + | name = "unicode-width" | |
| 1631 | + | version = "0.2.2" | |
| 1632 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1633 | + | checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" | |
| 1634 | + | ||
| 1635 | + | [[package]] | |
| 1636 | + | name = "utf8parse" | |
| 1637 | + | version = "0.2.2" | |
| 1638 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1639 | + | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | |
| 1640 | + | ||
| 1641 | + | [[package]] | |
| 1642 | + | name = "uuid" | |
| 1643 | + | version = "1.28.0" | |
| 1644 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1645 | + | checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" | |
| 1646 | + | dependencies = [ | |
| 1647 | + | "atomic", | |
| 1648 | + | "getrandom 0.4.3", | |
| 1649 | + | "js-sys", | |
| 1650 | + | "wasm-bindgen", | |
| 1651 | + | ] | |
| 1652 | + | ||
| 1653 | + | [[package]] | |
| 1654 | + | name = "version_check" | |
| 1655 | + | version = "0.9.5" | |
| 1656 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1657 | + | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | |
| 1658 | + | ||
| 1659 | + | [[package]] | |
| 1660 | + | name = "vtparse" | |
| 1661 | + | version = "0.6.2" | |
| 1662 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1663 | + | checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" | |
| 1664 | + | dependencies = [ | |
| 1665 | + | "utf8parse", | |
| 1666 | + | ] | |
| 1667 | + | ||
| 1668 | + | [[package]] | |
| 1669 | + | name = "wasi" | |
| 1670 | + | version = "0.11.1+wasi-snapshot-preview1" | |
| 1671 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1672 | + | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | |
| 1673 | + | ||
| 1674 | + | [[package]] | |
| 1675 | + | name = "wasip2" | |
| 1676 | + | version = "1.0.4+wasi-0.2.12" | |
| 1677 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1678 | + | checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" | |
| 1679 | + | dependencies = [ | |
| 1680 | + | "wit-bindgen", | |
| 1681 | + | ] | |
| 1682 | + | ||
| 1683 | + | [[package]] | |
| 1684 | + | name = "wasm-bindgen" | |
| 1685 | + | version = "0.2.129" | |
| 1686 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1687 | + | checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" | |
| 1688 | + | dependencies = [ | |
| 1689 | + | "cfg-if", | |
| 1690 | + | "once_cell", | |
| 1691 | + | "rustversion", | |
| 1692 | + | "wasm-bindgen-macro", | |
| 1693 | + | "wasm-bindgen-shared", | |
| 1694 | + | ] | |
| 1695 | + | ||
| 1696 | + | [[package]] | |
| 1697 | + | name = "wasm-bindgen-macro" | |
| 1698 | + | version = "0.2.129" | |
| 1699 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1700 | + | checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" | |
| 1701 | + | dependencies = [ | |
| 1702 | + | "quote", | |
| 1703 | + | "wasm-bindgen-macro-support", | |
| 1704 | + | ] | |
| 1705 | + | ||
| 1706 | + | [[package]] | |
| 1707 | + | name = "wasm-bindgen-macro-support" | |
| 1708 | + | version = "0.2.129" | |
| 1709 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1710 | + | checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" | |
| 1711 | + | dependencies = [ | |
| 1712 | + | "bumpalo", | |
| 1713 | + | "proc-macro2", | |
| 1714 | + | "quote", | |
| 1715 | + | "syn 3.0.9", | |
| 1716 | + | "wasm-bindgen-shared", | |
| 1717 | + | ] | |
| 1718 | + | ||
| 1719 | + | [[package]] | |
| 1720 | + | name = "wasm-bindgen-shared" | |
| 1721 | + | version = "0.2.129" | |
| 1722 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1723 | + | checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" | |
| 1724 | + | dependencies = [ | |
| 1725 | + | "unicode-ident", | |
| 1726 | + | ] | |
| 1727 | + | ||
| 1728 | + | [[package]] | |
| 1729 | + | name = "wezterm-bidi" | |
| 1730 | + | version = "0.2.3" | |
| 1731 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1732 | + | checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" | |
| 1733 | + | dependencies = [ | |
| 1734 | + | "log", | |
| 1735 | + | "wezterm-dynamic", | |
| 1736 | + | ] | |
| 1737 | + | ||
| 1738 | + | [[package]] | |
| 1739 | + | name = "wezterm-blob-leases" | |
| 1740 | + | version = "0.1.1" | |
| 1741 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1742 | + | checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" | |
| 1743 | + | dependencies = [ | |
| 1744 | + | "getrandom 0.3.4", | |
| 1745 | + | "mac_address", | |
| 1746 | + | "sha2", | |
| 1747 | + | "thiserror 1.0.69", | |
| 1748 | + | "uuid", | |
| 1749 | + | ] | |
| 1750 | + | ||
| 1751 | + | [[package]] | |
| 1752 | + | name = "wezterm-color-types" | |
| 1753 | + | version = "0.3.0" | |
| 1754 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1755 | + | checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" | |
| 1756 | + | dependencies = [ | |
| 1757 | + | "csscolorparser", | |
| 1758 | + | "deltae", | |
| 1759 | + | "lazy_static", | |
| 1760 | + | "wezterm-dynamic", | |
| 1761 | + | ] | |
| 1762 | + | ||
| 1763 | + | [[package]] | |
| 1764 | + | name = "wezterm-dynamic" | |
| 1765 | + | version = "0.2.1" | |
| 1766 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1767 | + | checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" | |
| 1768 | + | dependencies = [ | |
| 1769 | + | "log", | |
| 1770 | + | "ordered-float", | |
| 1771 | + | "strsim", | |
| 1772 | + | "thiserror 1.0.69", | |
| 1773 | + | "wezterm-dynamic-derive", | |
| 1774 | + | ] | |
| 1775 | + | ||
| 1776 | + | [[package]] | |
| 1777 | + | name = "wezterm-dynamic-derive" | |
| 1778 | + | version = "0.1.1" | |
| 1779 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1780 | + | checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" | |
| 1781 | + | dependencies = [ | |
| 1782 | + | "proc-macro2", | |
| 1783 | + | "quote", | |
| 1784 | + | "syn 1.0.109", | |
| 1785 | + | ] | |
| 1786 | + | ||
| 1787 | + | [[package]] | |
| 1788 | + | name = "wezterm-input-types" | |
| 1789 | + | version = "0.1.0" | |
| 1790 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1791 | + | checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" | |
| 1792 | + | dependencies = [ | |
| 1793 | + | "bitflags 1.3.2", | |
| 1794 | + | "euclid", | |
| 1795 | + | "lazy_static", | |
| 1796 | + | "serde", | |
| 1797 | + | "wezterm-dynamic", | |
| 1798 | + | ] | |
| 1799 | + | ||
| 1800 | + | [[package]] | |
| 1801 | + | name = "winapi" | |
| 1802 | + | version = "0.3.9" | |
| 1803 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1804 | + | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" | |
| 1805 | + | dependencies = [ | |
| 1806 | + | "winapi-i686-pc-windows-gnu", | |
| 1807 | + | "winapi-x86_64-pc-windows-gnu", | |
| 1808 | + | ] | |
| 1809 | + | ||
| 1810 | + | [[package]] | |
| 1811 | + | name = "winapi-i686-pc-windows-gnu" | |
| 1812 | + | version = "0.4.0" | |
| 1813 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1814 | + | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" | |
| 1815 | + | ||
| 1816 | + | [[package]] | |
| 1817 | + | name = "winapi-x86_64-pc-windows-gnu" | |
| 1818 | + | version = "0.4.0" | |
| 1819 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1820 | + | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | |
| 1821 | + | ||
| 1822 | + | [[package]] | |
| 1823 | + | name = "windows-link" | |
| 1824 | + | version = "0.2.1" | |
| 1825 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1826 | + | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | |
| 1827 | + | ||
| 1828 | + | [[package]] | |
| 1829 | + | name = "windows-sys" | |
| 1830 | + | version = "0.61.2" | |
| 1831 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1832 | + | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
| 1833 | + | dependencies = [ | |
| 1834 | + | "windows-link", | |
| 1835 | + | ] | |
| 1836 | + | ||
| 1837 | + | [[package]] | |
| 1838 | + | name = "wit-bindgen" | |
| 1839 | + | version = "0.57.1" | |
| 1840 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1841 | + | checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" | |
| @@ -1,7 +1,10 @@ | |||
| 1 | 1 | [package] | |
| 2 | − | name = "cpu-emulator" | |
| 2 | + | name = "bench51" | |
| 3 | 3 | version = "0.1.0" | |
| 4 | 4 | edition = "2024" | |
| 5 | + | description = "A TUI Intel 8051 assembler, simulator and debugger" | |
| 5 | 6 | ||
| 6 | 7 | [dependencies] | |
| 7 | 8 | clap = { version = "4.5.54", features = ["derive"] } | |
| 9 | + | ratatui = "0.30" | |
| 10 | + | ratatui-textarea = "0.9" | |
| @@ -1,113 +1,142 @@ | |||
| 1 | − | # 8-Bit CPU Emulator | |
| 2 | − | ||
| 3 | − | This project is an educational 8-bit CPU emulator written in Rust, it's build to understand core computer architecture concepts such as instruction sets, flags, | |
| 4 | − | control flow, stacks and system calls. | |
| 5 | − | ||
| 6 | − | This project is **not** a production CPU and **not** accurate. It is designed purely for learning and experimentation. | |
| 1 | + | # bench51 — a TUI 8051 assembler, simulator & debugger | |
| 2 | + | ||
| 3 | + | A small, Keil µVision-style IDE for the Intel 8051 that runs in your terminal. | |
| 4 | + | Write assembly in the built-in editor, assemble it, inspect the listing and | |
| 5 | + | Intel HEX output, then run, pause and single-step the program while watching | |
| 6 | + | registers, SFRs, memory, ports, timers, interrupts and the serial port. | |
| 7 | + | ||
| 8 | + | Built in Rust with [ratatui](https://ratatui.rs). It is a learning tool, not a | |
| 9 | + | cycle-exact replacement for a vendor simulator. | |
| 10 | + | ||
| 11 | + |  | |
| 12 | + | ||
| 13 | + | <table> | |
| 14 | + | <tr> | |
| 15 | + | <td width="50%"><img src="docs/screenshots/listing.png" alt="Editor with the assembled Keil-style listing"></td> | |
| 16 | + | <td width="50%"><img src="docs/screenshots/debug_breakpoint.png" alt="Stopped at a breakpoint inside the timer 0 ISR, SFR view"></td> | |
| 17 | + | </tr> | |
| 18 | + | <tr> | |
| 19 | + | <td align="center"><sub>Editor + assembled listing</sub></td> | |
| 20 | + | <td align="center"><sub>Breakpoint inside a timer ISR, SFR view</sub></td> | |
| 21 | + | </tr> | |
| 22 | + | <tr> | |
| 23 | + | <td width="50%"><img src="docs/screenshots/errors.png" alt="Assembler errors marked in the gutter and listed in the Errors tab"></td> | |
| 24 | + | <td width="50%"><img src="docs/screenshots/hex.png" alt="Intel HEX output tab"></td> | |
| 25 | + | </tr> | |
| 26 | + | <tr> | |
| 27 | + | <td align="center"><sub>Assembler errors in the gutter and Errors tab</sub></td> | |
| 28 | + | <td align="center"><sub>Intel HEX output</sub></td> | |
| 29 | + | </tr> | |
| 30 | + | </table> | |
| 31 | + | ||
| 32 | + | ## Why this exists | |
| 33 | + | ||
| 34 | + | I had microcontroller practicals to get through, and the course expects you to | |
| 35 | + | use Keil µVision. Keil µVision doesn't run on Linux. Rather than fight with | |
| 36 | + | Wine or a Windows VM every lab session, I wrote my own: bench51 does the parts | |
| 37 | + | of µVision those practicals actually need (write the code, assemble it, look | |
| 38 | + | at the listing and HEX, step through it and watch the registers) right in the | |
| 39 | + | terminal. | |
| 7 | 40 | ||
| 8 | 41 | ----- | |
| 9 | 42 | ||
| 10 | 43 | ## Features | |
| 11 | 44 | ||
| 12 | − | - Custom 8-bit ISA (control-flow and arithmetic operations) | |
| 13 | − | - 4 general-purpose registers | |
| 14 | − | - 64 KB (0x0000-0xFFFF) byte-addressable memory | |
| 15 | − | - Two-pass assembler with label support | |
| 16 | − | - Stack-based function calls | |
| 17 | − | - Stack is software-managed and grows downward | |
| 18 | − | ||
| 19 | − | --- | |
| 20 | − | ||
| 21 | − | ||
| 22 | − | ||
| 23 | − | ## Registers | |
| 24 | − | | Register | Size | Description | | |
| 25 | − | | -------- | ------ | ------------------------------ | | |
| 26 | − | | A | 8-bit | General-purpose register | | |
| 27 | − | | B | 8-bit | General-purpose register | | |
| 28 | − | | C | 8-bit | General-purpose register | | |
| 29 | − | | D | 8-bit | General-purpose register | | |
| 30 | − | | PC | 16-bit | Program Counter | | |
| 31 | − | | SP | 16-bit | Stack pointer | | |
| 32 | − | ||
| 33 | − | ||
| 34 | − | ## Flags Register | |
| 35 | − | ||
| 36 | − | The CPU maintains a small flags register. | |
| 37 | − | ||
| 38 | − | | Flag | Description | | |
| 39 | − | | ----- | ------------ | | |
| 40 | − | | Z | Zero flag - set if last result was `0` | | |
| 41 | − | | C | Carry/Borrow flag | | |
| 45 | + | - **Editor** with undo/redo, breakpoint and error markers in the gutter — | |
| 46 | + | or press Ctrl-G to edit in your own `$EDITOR` (vim, nvim, nano, …). | |
| 47 | + | - **Two-pass assembler** for the full 8051 instruction set (all 255 opcodes): | |
| 48 | + | - labels, expressions (`+ - * / MOD AND OR XOR NOT SHL SHR HIGH LOW`, `$`) | |
| 49 | + | - number formats `42`, `2Ah`, `0x2A`, `101010b`, `52o`, `'*'` | |
| 50 | + | - bit addressing (`P1.3`, `ACC.7`, `20h.0`) and all predefined SFR/bit names | |
| 51 | + | - directives `ORG DB DW DS END EQU SET BIT DATA IDATA XDATA CODE CSEG AT` | |
| 52 | + | - generic `JMP`/`CALL` (assembled as `LJMP`/`LCALL`) | |
| 53 | + | - Keil-style `.lst` listing with symbol table, Intel HEX output | |
| 54 | + | - **Simulator** | |
| 55 | + | - all instructions with correct flags (CY, AC, OV, P) and machine-cycle counts | |
| 56 | + | - 4 register banks, 256 bytes IRAM, SFRs, 64K code, 64K XRAM | |
| 57 | + | - ports P0–P3 with latch vs. pin semantics (read-modify-write reads the latch) | |
| 58 | + | - timers 0/1 in modes 0–3, gate and counter inputs | |
| 59 | + | - interrupts: INT0, T0, INT1, T1, serial; two priority levels; edge/level triggers | |
| 60 | + | - UART in modes 0–3 with baud rate timing from timer 1 | |
| 61 | + | - idle and power-down modes (PCON) | |
| 62 | + | - **Debugger**: run / pause, step into / over / out, run to cursor, | |
| 63 | + | breakpoints, changed-value highlighting, memory poke, adjustable speed. | |
| 64 | + | - **Open `.hex` files**: they're disassembled into editable source that | |
| 65 | + | re-assembles to the identical image. | |
| 42 | 66 | ||
| 43 | − | - Control-flow instructions do **not** modify flags. | |
| 44 | − | ||
| 45 | − | ## Supported Instructions | |
| 46 | − | ||
| 47 | − | | Instruction | Syntax | | |
| 48 | − | | ----------- | ---------------- | | |
| 49 | − | | MOV | mov dest, src OR mov reg, imm | | |
| 50 | − | | ADD | add r1, r2 OR add reg, imm | | |
| 51 | − | | SUB | sub r1, r2 OR sub reg, imm | | |
| 52 | − | | JMP | jmp addr OR jmp addr | | |
| 53 | − | | JZ | jz addr OR jz addr | | |
| 54 | − | | JNZ | jnz addr OR jnz addr | | |
| 55 | − | | CMP | cmp r1, r2 OR cmp reg, imp | | |
| 56 | − | | MUL | mul r1, r2 | | |
| 57 | − | | DIV | div r1, r2 | | |
| 58 | − | | CALL | call \<label\> | | |
| 59 | − | | SYS | sys \<syscall_no\> | | |
| 60 | − | | HLT (Halt) | hlt | | |
| 61 | − | ||
| 62 | − | ## Syscalls | |
| 63 | − | ||
| 64 | − | | Imm | Meaning | | |
| 65 | − | | --- | ------- | | |
| 66 | − | | 0 | Exit Program | | |
| 67 | − | | 1 | Print register A as integer | | |
| 68 | − | | 2 | Print register A as ASCII char | | |
| 69 | − | ||
| 70 | − | - Syscalls also do not modify flags | |
| 71 | − | ||
| 72 | − | ## Example assembly program | |
| 67 | + | ## Usage | |
| 73 | 68 | ||
| 74 | − | ```assembly | |
| 75 | − | mov b, 3 | |
| 76 | − | mov a, 1 | |
| 69 | + | ```bash | |
| 70 | + | cargo install --path . # installs the `bench51` binary | |
| 77 | 71 | ||
| 78 | − | loop: | |
| 79 | − | sub b, a | |
| 80 | − | jnz loop | |
| 72 | + | cargo run --release -- examples/serial_hello.asm # open in the TUI | |
| 73 | + | cargo run --release # start with a template | |
| 74 | + | cargo run --release -- firmware.hex # disassemble & debug a HEX file | |
| 81 | 75 | ||
| 82 | − | sys 1 ; print register A | |
| 83 | − | sys 0 ; exit | |
| 76 | + | # Headless assembly: writes prog.hex and prog.lst | |
| 77 | + | cargo run --release -- --assemble prog.asm | |
| 78 | + | cargo run --release -- --assemble prog.asm -o out.hex --lst out.lst | |
| 84 | 79 | ``` | |
| 85 | 80 | ||
| 86 | − | ## Usage | |
| 81 | + | `--clock <MHz>` sets the oscillator frequency used for the elapsed-time display | |
| 82 | + | (default 11.0592). | |
| 83 | + | ||
| 84 | + | ## Keys | |
| 85 | + | ||
| 86 | + | | Key | Action | | |
| 87 | + | | --- | --- | | |
| 88 | + | | F1 | help | | |
| 89 | + | | F2 / Ctrl-S | save (assembling a saved file also writes `.lst` and `.hex` next to it) | | |
| 90 | + | | F3 / Ctrl-O | open `.asm` or `.hex` | | |
| 91 | + | | Ctrl-G | edit in `$EDITOR`; on save & exit the buffer reloads and reassembles | | |
| 92 | + | | F4 | switch Edit ⇄ Debug | | |
| 93 | + | | F7 | assemble, reset and load | | |
| 94 | + | | F5 | run / pause (assembles first if the source changed) | | |
| 95 | + | | F11 / F10 / F12 | step into / step over / step out | | |
| 96 | + | | F8 | run to cursor | | |
| 97 | + | | F9 | toggle breakpoint on the cursor line | | |
| 98 | + | | F6 | reset | | |
| 99 | + | | Ctrl-Q | quit | | |
| 100 | + | ||
| 101 | + | Edit mode: `Alt-1/2/3` switch between Listing, HEX and Errors; `Alt-↑/↓`, | |
| 102 | + | `Alt-PgUp/PgDn` scroll them. The editor uses Emacs-style keys | |
| 103 | + | (`Ctrl-U` undo, `Ctrl-R` redo, `Ctrl-Y` paste). | |
| 104 | + | ||
| 105 | + | Debug mode: `Tab` cycles focus between Source, Memory, Ports and Serial. | |
| 106 | + | Letter shortcuts work too, for terminals that swallow function keys: | |
| 107 | + | `r` run/pause, `s` step, `n` step over, `o` step out, `g` run to cursor, | |
| 108 | + | `b` breakpoint, `x` reset, `i`/`f`/`d`/`c` show IRAM/SFR/XRAM/CODE, | |
| 109 | + | `p` poke memory, `+`/`-` speed, `.` jump to PC, `Esc` pause / back to editor. | |
| 110 | + | ||
| 111 | + | - **Ports**: select a pin and press Space to pull it low from outside (a | |
| 112 | + | button press); the program sees the pin as `latch AND external level`. | |
| 113 | + | - **Serial**: type a line and press Enter to send it (followed by CR) to the | |
| 114 | + | 8051's receiver; everything the 8051 transmits appears above. | |
| 115 | + | ||
| 116 | + | ## Examples | |
| 117 | + | ||
| 118 | + | | File | Shows | | |
| 119 | + | | --- | --- | | |
| 120 | + | | `examples/blink.asm` | port toggling, delay loops, `ACALL`/`RET` | | |
| 121 | + | | `examples/timer_irq.asm` | timer 0 mode 1 interrupt | | |
| 122 | + | | `examples/serial_hello.asm` | UART setup, string output with `MOVC`, echo | | |
| 123 | + | | `examples/button_int0.asm` | edge-triggered INT0, idle mode | | |
| 124 | + | | `examples/bcd_add.asm` | `ADDC` and `DA A` with packed BCD | | |
| 125 | + | | `examples/lookup_movc.asm` | 7-segment table lookup | | |
| 126 | + | ||
| 127 | + | ## Notes and limitations | |
| 128 | + | ||
| 129 | + | - Indirect addressing reaches all 256 bytes of IRAM (8052-style upper 128). | |
| 130 | + | - Serial input has no 9th data bit; in modes 2/3 `RB8` reads 0. | |
| 131 | + | - No 8052 timer 2, external code memory or on-chip peripherals of derivatives. | |
| 132 | + | ||
| 133 | + | ## Development | |
| 87 | 134 | ||
| 88 | 135 | ```bash | |
| 89 | − | cargo run -- --f <examples/filename.asc> | |
| 136 | + | cargo test # assembler, CPU, peripheral and example-program tests | |
| 137 | + | cargo clippy | |
| 90 | 138 | ``` | |
| 91 | 139 | ||
| 92 | − | ## Non-goals | |
| 93 | − | ||
| 94 | − | 1. No pipelining | |
| 95 | − | 2. No interrupts | |
| 96 | − | 3. No virtual memory | |
| 97 | − | 4. No privilege levels | |
| 98 | − | 5. No hardware I/O | |
| 99 | − | ||
| 100 | − | ## Goals | |
| 101 | − | ||
| 102 | − | - [x] Assembler | |
| 103 | − | - [x] Lexer/Tokenizer | |
| 104 | − | - [x] Add label support (supporting JMP/JZ/JNZ) | |
| 105 | − | - [x] Add instructions | |
| 106 | − | - [x] CMP | |
| 107 | − | - [x] MUL | |
| 108 | − | - [x] DIV | |
| 109 | − | - [x] CALL | |
| 110 | − | - [x] RET | |
| 111 | − | - [x] SYS | |
| 112 | − | - [ ] Better error-handling | |
| 113 | − | - [ ] TUI debugger | |
| 140 | + | Code layout: `src/isa.rs` (opcode table shared by everything), `src/asm/` | |
| 141 | + | (assembler, listing, Intel HEX), `src/sim/` (CPU, peripherals, disassembler), | |
| 142 | + | `src/ui/` (ratatui front end). | |
[Binary file]
[Binary file]
[Binary file]
[Binary file]
[Binary file]
| @@ -1,9 +0,0 @@ | |||
| 1 | − | ; move imm 10 to register A | |
| 2 | − | mov a, 10 | |
| 3 | − | ; move imm 5 to register B | |
| 4 | − | mov b, 5 | |
| 5 | − | ; Add a & B | |
| 6 | − | add a, 2 | |
| 7 | − | sub a, 2 | |
| 8 | − | ; Halt | |
| 9 | − | hlt | |
| @@ -0,0 +1,21 @@ | |||
| 1 | + | ; Packed BCD addition with DA A: 4-digit 1234 + 5678 = 6912. | |
| 2 | + | ; Watch A, CY and AC in the Registers panel while stepping. | |
| 3 | + | ||
| 4 | + | ORG 0000h | |
| 5 | + | MOV R0,#34h ; low byte of first number | |
| 6 | + | MOV R1,#12h ; high byte | |
| 7 | + | MOV R2,#78h ; low byte of second number | |
| 8 | + | MOV R3,#56h ; high byte | |
| 9 | + | ||
| 10 | + | MOV A,R0 | |
| 11 | + | ADD A,R2 ; 34h + 78h = ACh (binary) | |
| 12 | + | DA A ; -> 12h, CY = 1 | |
| 13 | + | MOV 30h,A | |
| 14 | + | MOV A,R1 | |
| 15 | + | ADDC A,R3 ; 12h + 56h + carry | |
| 16 | + | DA A ; -> 69h | |
| 17 | + | MOV 31h,A ; result 6912 in 31h:30h | |
| 18 | + | ||
| 19 | + | SJMP $ | |
| 20 | + | ||
| 21 | + | END | |
| @@ -0,0 +1,23 @@ | |||
| 1 | + | ; Blink: toggle all of P1 with a software delay loop. | |
| 2 | + | ; Step with F11, or run with F5 and watch the Ports panel. | |
| 3 | + | ||
| 4 | + | ORG 0000h | |
| 5 | + | LJMP main | |
| 6 | + | ||
| 7 | + | ORG 0030h | |
| 8 | + | main: MOV P1,#0FFh | |
| 9 | + | loop: CPL P1.0 ; toggle the LED on P1.0 | |
| 10 | + | MOV A,P1 | |
| 11 | + | CPL A | |
| 12 | + | MOV P2,A ; P2 mirrors P1 inverted | |
| 13 | + | ACALL delay | |
| 14 | + | SJMP loop | |
| 15 | + | ||
| 16 | + | ; About 2 * 50 * 250 machine cycles. | |
| 17 | + | delay: MOV R6,#50 | |
| 18 | + | d1: MOV R7,#250 | |
| 19 | + | d2: DJNZ R7,d2 | |
| 20 | + | DJNZ R6,d1 | |
| 21 | + | RET | |
| 22 | + | ||
| 23 | + | END | |
| @@ -0,0 +1,20 @@ | |||
| 1 | + | ; External interrupt 0 (falling edge on P3.2) counts button presses into P1. | |
| 2 | + | ; In debug mode, Tab to the Ports panel, select P3 bit 2 and press Space | |
| 3 | + | ; twice (press + release) to generate one falling edge. | |
| 4 | + | ||
| 5 | + | ORG 0000h | |
| 6 | + | LJMP main | |
| 7 | + | ||
| 8 | + | ORG 0003h ; INT0 vector | |
| 9 | + | INC P1 | |
| 10 | + | RETI | |
| 11 | + | ||
| 12 | + | ORG 0030h | |
| 13 | + | main: MOV P1,#00h | |
| 14 | + | SETB IT0 ; edge-triggered | |
| 15 | + | SETB EX0 | |
| 16 | + | SETB EA | |
| 17 | + | sleep: ORL PCON,#01h ; idle until an interrupt arrives | |
| 18 | + | SJMP sleep ; ...and go back to idle afterwards | |
| 19 | + | ||
| 20 | + | END | |
| @@ -1,7 +0,0 @@ | |||
| 1 | − | mov a, 2 | |
| 2 | − | call square | |
| 3 | − | hlt | |
| 4 | − | ||
| 5 | − | square: | |
| 6 | − | mul a, a | |
| 7 | − | ret | |
| @@ -1,4 +0,0 @@ | |||
| 1 | − | mov a, 10 | |
| 2 | − | mov b, 10 | |
| 3 | − | cmp a, b | |
| 4 | − | hlt | |
| @@ -1,4 +0,0 @@ | |||
| 1 | − | mov a, 10 | |
| 2 | − | mov b, 10 | |
| 3 | − | div a, b | |
| 4 | − | hlt | |
| @@ -0,0 +1,20 @@ | |||
| 1 | + | ; Count 0..9 on a common-cathode 7-segment display connected to P2, | |
| 2 | + | ; using a MOVC lookup table. The digit is also kept in R2. | |
| 3 | + | ||
| 4 | + | ORG 0000h | |
| 5 | + | LJMP main | |
| 6 | + | ||
| 7 | + | ORG 0030h | |
| 8 | + | main: MOV DPTR,#segs | |
| 9 | + | again: MOV R2,#0 | |
| 10 | + | next: MOV A,R2 | |
| 11 | + | MOVC A,@A+DPTR ; table lookup | |
| 12 | + | MOV P2,A | |
| 13 | + | INC R2 | |
| 14 | + | CJNE R2,#10,next | |
| 15 | + | SJMP again | |
| 16 | + | ||
| 17 | + | ; 0 1 2 3 4 5 6 7 8 9 | |
| 18 | + | segs: DB 3Fh, 06h, 5Bh, 4Fh, 66h, 6Dh, 7Dh, 07h, 7Fh, 6Fh | |
| 19 | + | ||
| 20 | + | END | |
| @@ -1,13 +0,0 @@ | |||
| 1 | − | ; move imm 3 to register b | |
| 2 | − | mov b, 3 | |
| 3 | − | ; move imm 1 to register a | |
| 4 | − | mov a, 1 | |
| 5 | − | ||
| 6 | − | ; delcare a label | |
| 7 | − | loop: | |
| 8 | − | ; subtract a from b | |
| 9 | − | sub b, a | |
| 10 | − | ; jump to label "loop" until the zero flag is not set | |
| 11 | − | jnz loop | |
| 12 | − | ||
| 13 | − | hlt | |
| @@ -1,4 +0,0 @@ | |||
| 1 | − | mov a, 99 | |
| 2 | − | mov b, 99 | |
| 3 | − | mul a, b | |
| 4 | − | hlt | |
| @@ -0,0 +1,43 @@ | |||
| 1 | + | ; UART: 9600 baud at 11.0592 MHz (timer 1, mode 2, TH1 = FDh). | |
| 2 | + | ; Prints a greeting, then echoes everything typed in the Serial panel | |
| 3 | + | ; (Tab to the Serial panel in debug mode, type, press Enter). | |
| 4 | + | ||
| 5 | + | ORG 0000h | |
| 6 | + | LJMP main | |
| 7 | + | ||
| 8 | + | ORG 0030h | |
| 9 | + | main: MOV TMOD,#20h ; timer 1: mode 2 (8-bit auto-reload) | |
| 10 | + | MOV TH1,#0FDh | |
| 11 | + | SETB TR1 | |
| 12 | + | MOV SCON,#50h ; serial mode 1, receiver enabled | |
| 13 | + | MOV DPTR,#msg | |
| 14 | + | ACALL puts | |
| 15 | + | echo: ACALL getc | |
| 16 | + | ACALL putc | |
| 17 | + | CJNE A,#0Dh,echo ; after CR, also send LF | |
| 18 | + | MOV A,#0Ah | |
| 19 | + | ACALL putc | |
| 20 | + | SJMP echo | |
| 21 | + | ||
| 22 | + | ; Send the zero-terminated string at DPTR. | |
| 23 | + | puts: CLR A | |
| 24 | + | MOVC A,@A+DPTR | |
| 25 | + | JZ done | |
| 26 | + | ACALL putc | |
| 27 | + | INC DPTR | |
| 28 | + | SJMP puts | |
| 29 | + | done: RET | |
| 30 | + | ||
| 31 | + | putc: MOV SBUF,A | |
| 32 | + | wait: JNB TI,wait | |
| 33 | + | CLR TI | |
| 34 | + | RET | |
| 35 | + | ||
| 36 | + | getc: JNB RI,getc | |
| 37 | + | MOV A,SBUF | |
| 38 | + | CLR RI | |
| 39 | + | RET | |
| 40 | + | ||
| 41 | + | msg: DB "Hello from the 8051!", 0Dh, 0Ah, "Type something: ", 0 | |
| 42 | + | ||
| 43 | + | END | |
| @@ -1,7 +0,0 @@ | |||
| 1 | − | mov a, 72 ; 'H' | |
| 2 | − | sys 2 | |
| 3 | − | mov a, 105 ; 'i' | |
| 4 | − | sys 2 | |
| 5 | − | mov a, 10 ; '\n' | |
| 6 | − | sys 2 | |
| 7 | − | sys 0 | |
| @@ -1,9 +0,0 @@ | |||
| 1 | − | ; Table of 3 | |
| 2 | − | mov b, 3 | |
| 3 | − | mov c, 10 ; Counter | |
| 4 | − | ||
| 5 | − | loop: | |
| 6 | − | add a, b | |
| 7 | − | sub c, 1 | |
| 8 | − | jnz loop | |
| 9 | − | hlt | |
| @@ -0,0 +1,28 @@ | |||
| 1 | + | ; Timer 0 interrupt: mode 1 (16-bit) overflow every 1000 cycles toggles P1.0 | |
| 2 | + | ; and counts ticks in R7. The main loop just idles. | |
| 3 | + | ||
| 4 | + | RELOAD EQU 65536 - 1000 | |
| 5 | + | ||
| 6 | + | ORG 0000h | |
| 7 | + | LJMP main | |
| 8 | + | ||
| 9 | + | ORG 000Bh ; timer 0 overflow vector | |
| 10 | + | LJMP t0_isr | |
| 11 | + | ||
| 12 | + | ORG 0030h | |
| 13 | + | main: MOV SP,#5Fh | |
| 14 | + | MOV TMOD,#01h ; T0: mode 1, timer | |
| 15 | + | MOV TH0,#HIGH RELOAD | |
| 16 | + | MOV TL0,#LOW RELOAD | |
| 17 | + | SETB ET0 ; enable timer 0 interrupt | |
| 18 | + | SETB EA ; global interrupt enable | |
| 19 | + | SETB TR0 ; start timer 0 | |
| 20 | + | idle: SJMP idle | |
| 21 | + | ||
| 22 | + | t0_isr: MOV TH0,#HIGH RELOAD | |
| 23 | + | MOV TL0,#LOW RELOAD | |
| 24 | + | CPL P1.0 | |
| 25 | + | INC R7 | |
| 26 | + | RETI | |
| 27 | + | ||
| 28 | + | END | |
| @@ -0,0 +1,251 @@ | |||
| 1 | + | use super::lexer::{Tok, Token}; | |
| 2 | + | ||
| 3 | + | #[derive(Debug, Clone)] | |
| 4 | + | pub enum Expr { | |
| 5 | + | Num(i64), | |
| 6 | + | Sym(String, usize), | |
| 7 | + | /// `$`, the current location counter. | |
| 8 | + | Here, | |
| 9 | + | Neg(Box<Expr>), | |
| 10 | + | Not(Box<Expr>), | |
| 11 | + | High(Box<Expr>), | |
| 12 | + | Low(Box<Expr>), | |
| 13 | + | Bin(BinOp, Box<Expr>, Box<Expr>), | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | #[derive(Debug, Clone, Copy)] | |
| 17 | + | pub enum BinOp { | |
| 18 | + | Add, | |
| 19 | + | Sub, | |
| 20 | + | Mul, | |
| 21 | + | Div, | |
| 22 | + | Mod, | |
| 23 | + | And, | |
| 24 | + | Or, | |
| 25 | + | Xor, | |
| 26 | + | Shl, | |
| 27 | + | Shr, | |
| 28 | + | /// `byte.bit` bit addressing, e.g. `P1.3` or `20h.0`. | |
| 29 | + | Dot, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | pub enum EvalError { | |
| 33 | + | Undefined(String, usize), | |
| 34 | + | Other(String), | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | pub trait Env { | |
| 38 | + | fn lookup(&self, name: &str) -> Option<i64>; | |
| 39 | + | fn here(&self) -> i64; | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | impl Expr { | |
| 43 | + | pub fn eval(&self, env: &dyn Env) -> Result<i64, EvalError> { | |
| 44 | + | Ok(match self { | |
| 45 | + | Expr::Num(n) => *n, | |
| 46 | + | Expr::Here => env.here(), | |
| 47 | + | Expr::Sym(s, col) => env | |
| 48 | + | .lookup(s) | |
| 49 | + | .ok_or_else(|| EvalError::Undefined(s.clone(), *col))?, | |
| 50 | + | Expr::Neg(e) => e.eval(env)?.wrapping_neg(), | |
| 51 | + | Expr::Not(e) => !e.eval(env)? & 0xFFFF, | |
| 52 | + | Expr::High(e) => (e.eval(env)? >> 8) & 0xFF, | |
| 53 | + | Expr::Low(e) => e.eval(env)? & 0xFF, | |
| 54 | + | Expr::Bin(op, l, r) => { | |
| 55 | + | let (l, r) = (l.eval(env)?, r.eval(env)?); | |
| 56 | + | match op { | |
| 57 | + | BinOp::Add => l.wrapping_add(r), | |
| 58 | + | BinOp::Sub => l.wrapping_sub(r), | |
| 59 | + | BinOp::Mul => l.wrapping_mul(r), | |
| 60 | + | BinOp::Div | BinOp::Mod if r == 0 => { | |
| 61 | + | return Err(EvalError::Other("division by zero".into())); | |
| 62 | + | } | |
| 63 | + | BinOp::Div => l / r, | |
| 64 | + | BinOp::Mod => l % r, | |
| 65 | + | BinOp::And => l & r, | |
| 66 | + | BinOp::Or => l | r, | |
| 67 | + | BinOp::Xor => l ^ r, | |
| 68 | + | BinOp::Shl => l.wrapping_shl(r as u32), | |
| 69 | + | BinOp::Shr => l.wrapping_shr(r as u32), | |
| 70 | + | BinOp::Dot => bit_address(l, r)?, | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | }) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | fn bit_address(byte: i64, bit: i64) -> Result<i64, EvalError> { | |
| 78 | + | if !(0..8).contains(&bit) { | |
| 79 | + | return Err(EvalError::Other(format!( | |
| 80 | + | "bit index {bit} out of range 0..7" | |
| 81 | + | ))); | |
| 82 | + | } | |
| 83 | + | match byte { | |
| 84 | + | 0x20..=0x2F => Ok((byte - 0x20) * 8 + bit), | |
| 85 | + | 0x80..=0xFF if byte % 8 == 0 => Ok(byte + bit), | |
| 86 | + | _ => Err(EvalError::Other(format!( | |
| 87 | + | "address {byte:02X}h is not bit-addressable" | |
| 88 | + | ))), | |
| 89 | + | } | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | /// Recursive-descent parser over a token slice. | |
| 93 | + | pub struct Parser<'a> { | |
| 94 | + | toks: &'a [Token], | |
| 95 | + | pos: usize, | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | type PResult<T> = Result<T, (usize, String)>; | |
| 99 | + | ||
| 100 | + | impl<'a> Parser<'a> { | |
| 101 | + | pub fn new(toks: &'a [Token]) -> Self { | |
| 102 | + | Self { toks, pos: 0 } | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | /// Parses the whole slice as one expression. | |
| 106 | + | pub fn parse_all(toks: &'a [Token]) -> PResult<Expr> { | |
| 107 | + | let mut p = Self::new(toks); | |
| 108 | + | let e = p.expr()?; | |
| 109 | + | if let Some(t) = p.toks.get(p.pos) { | |
| 110 | + | return Err((t.col, "unexpected token in expression".into())); | |
| 111 | + | } | |
| 112 | + | Ok(e) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | fn peek(&self) -> Option<&Tok> { | |
| 116 | + | self.toks.get(self.pos).map(|t| &t.tok) | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | fn peek_word(&self, w: &str) -> bool { | |
| 120 | + | matches!(self.peek(), Some(Tok::Ident(s)) if s == w) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | fn col(&self) -> usize { | |
| 124 | + | self.toks | |
| 125 | + | .get(self.pos) | |
| 126 | + | .or(self.toks.last()) | |
| 127 | + | .map_or(0, |t| t.col) | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | fn expr(&mut self) -> PResult<Expr> { | |
| 131 | + | let mut l = self.xor()?; | |
| 132 | + | while self.peek_word("OR") || self.peek() == Some(&Tok::Punct('|')) { | |
| 133 | + | self.pos += 1; | |
| 134 | + | l = Expr::Bin(BinOp::Or, Box::new(l), Box::new(self.xor()?)); | |
| 135 | + | } | |
| 136 | + | Ok(l) | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | fn xor(&mut self) -> PResult<Expr> { | |
| 140 | + | let mut l = self.and()?; | |
| 141 | + | while self.peek_word("XOR") || self.peek() == Some(&Tok::Punct('^')) { | |
| 142 | + | self.pos += 1; | |
| 143 | + | l = Expr::Bin(BinOp::Xor, Box::new(l), Box::new(self.and()?)); | |
| 144 | + | } | |
| 145 | + | Ok(l) | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | fn and(&mut self) -> PResult<Expr> { | |
| 149 | + | let mut l = self.shift()?; | |
| 150 | + | while self.peek_word("AND") || self.peek() == Some(&Tok::Punct('&')) { | |
| 151 | + | self.pos += 1; | |
| 152 | + | l = Expr::Bin(BinOp::And, Box::new(l), Box::new(self.shift()?)); | |
| 153 | + | } | |
| 154 | + | Ok(l) | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | fn shift(&mut self) -> PResult<Expr> { | |
| 158 | + | let mut l = self.add()?; | |
| 159 | + | loop { | |
| 160 | + | let op = match self.peek() { | |
| 161 | + | Some(Tok::Shl) => BinOp::Shl, | |
| 162 | + | Some(Tok::Shr) => BinOp::Shr, | |
| 163 | + | Some(Tok::Ident(s)) if s == "SHL" => BinOp::Shl, | |
| 164 | + | Some(Tok::Ident(s)) if s == "SHR" => BinOp::Shr, | |
| 165 | + | _ => return Ok(l), | |
| 166 | + | }; | |
| 167 | + | self.pos += 1; | |
| 168 | + | l = Expr::Bin(op, Box::new(l), Box::new(self.add()?)); | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | fn add(&mut self) -> PResult<Expr> { | |
| 173 | + | let mut l = self.mul()?; | |
| 174 | + | loop { | |
| 175 | + | let op = match self.peek() { | |
| 176 | + | Some(Tok::Punct('+')) => BinOp::Add, | |
| 177 | + | Some(Tok::Punct('-')) => BinOp::Sub, | |
| 178 | + | _ => return Ok(l), | |
| 179 | + | }; | |
| 180 | + | self.pos += 1; | |
| 181 | + | l = Expr::Bin(op, Box::new(l), Box::new(self.mul()?)); | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | fn mul(&mut self) -> PResult<Expr> { | |
| 186 | + | let mut l = self.unary()?; | |
| 187 | + | loop { | |
| 188 | + | let op = match self.peek() { | |
| 189 | + | Some(Tok::Punct('*')) => BinOp::Mul, | |
| 190 | + | Some(Tok::Punct('/')) => BinOp::Div, | |
| 191 | + | Some(Tok::Punct('%')) => BinOp::Mod, | |
| 192 | + | Some(Tok::Ident(s)) if s == "MOD" => BinOp::Mod, | |
| 193 | + | _ => return Ok(l), | |
| 194 | + | }; | |
| 195 | + | self.pos += 1; | |
| 196 | + | l = Expr::Bin(op, Box::new(l), Box::new(self.unary()?)); | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | fn unary(&mut self) -> PResult<Expr> { | |
| 201 | + | let wrap: fn(Box<Expr>) -> Expr = match self.peek() { | |
| 202 | + | Some(Tok::Punct('-')) => Expr::Neg, | |
| 203 | + | Some(Tok::Punct('+')) => { | |
| 204 | + | self.pos += 1; | |
| 205 | + | return self.unary(); | |
| 206 | + | } | |
| 207 | + | Some(Tok::Punct('~')) => Expr::Not, | |
| 208 | + | Some(Tok::Ident(s)) if s == "NOT" => Expr::Not, | |
| 209 | + | Some(Tok::Ident(s)) if s == "HIGH" => Expr::High, | |
| 210 | + | Some(Tok::Ident(s)) if s == "LOW" => Expr::Low, | |
| 211 | + | _ => return self.dot(), | |
| 212 | + | }; | |
| 213 | + | self.pos += 1; | |
| 214 | + | Ok(wrap(Box::new(self.unary()?))) | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | fn dot(&mut self) -> PResult<Expr> { | |
| 218 | + | let l = self.primary()?; | |
| 219 | + | if self.peek() == Some(&Tok::Punct('.')) { | |
| 220 | + | self.pos += 1; | |
| 221 | + | let r = self.primary()?; | |
| 222 | + | return Ok(Expr::Bin(BinOp::Dot, Box::new(l), Box::new(r))); | |
| 223 | + | } | |
| 224 | + | Ok(l) | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | fn primary(&mut self) -> PResult<Expr> { | |
| 228 | + | let col = self.col(); | |
| 229 | + | let Some(tok) = self.peek().cloned() else { | |
| 230 | + | return Err((col, "expected expression".into())); | |
| 231 | + | }; | |
| 232 | + | self.pos += 1; | |
| 233 | + | match tok { | |
| 234 | + | Tok::Num(n) => Ok(Expr::Num(n)), | |
| 235 | + | Tok::Punct('$') => Ok(Expr::Here), | |
| 236 | + | Tok::Ident(s) => Ok(Expr::Sym(s, col)), | |
| 237 | + | Tok::Str(b) if b.len() == 1 => Ok(Expr::Num(b[0] as i64)), | |
| 238 | + | Tok::Str(b) if b.len() == 2 => Ok(Expr::Num(((b[0] as i64) << 8) | b[1] as i64)), | |
| 239 | + | Tok::Str(_) => Err((col, "string too long for an expression".into())), | |
| 240 | + | Tok::Punct('(') => { | |
| 241 | + | let e = self.expr()?; | |
| 242 | + | if self.peek() != Some(&Tok::Punct(')')) { | |
| 243 | + | return Err((self.col(), "expected ')'".into())); | |
| 244 | + | } | |
| 245 | + | self.pos += 1; | |
| 246 | + | Ok(e) | |
| 247 | + | } | |
| 248 | + | _ => Err((col, "expected expression".into())), | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | } | |
| @@ -0,0 +1,117 @@ | |||
| 1 | + | //! Intel HEX reading and writing. | |
| 2 | + | ||
| 3 | + | use std::collections::BTreeMap; | |
| 4 | + | use std::fmt::Write; | |
| 5 | + | ||
| 6 | + | const RECORD_LEN: usize = 16; | |
| 7 | + | ||
| 8 | + | /// Writes data records for contiguous runs of bytes, then the EOF record. | |
| 9 | + | pub fn write(bytes: impl Iterator<Item = (u16, u8)>) -> String { | |
| 10 | + | let image: BTreeMap<u16, u8> = bytes.collect(); | |
| 11 | + | let mut out = String::new(); | |
| 12 | + | let mut run: Vec<u8> = Vec::new(); | |
| 13 | + | let mut run_start = 0u16; | |
| 14 | + | let mut flush = |start: u16, data: &mut Vec<u8>| { | |
| 15 | + | for (i, chunk) in data.chunks(RECORD_LEN).enumerate() { | |
| 16 | + | record( | |
| 17 | + | &mut out, | |
| 18 | + | start.wrapping_add((i * RECORD_LEN) as u16), | |
| 19 | + | 0x00, | |
| 20 | + | chunk, | |
| 21 | + | ); | |
| 22 | + | } | |
| 23 | + | data.clear(); | |
| 24 | + | }; | |
| 25 | + | for (&addr, &b) in &image { | |
| 26 | + | if !run.is_empty() && run_start.wrapping_add(run.len() as u16) != addr { | |
| 27 | + | flush(run_start, &mut run); | |
| 28 | + | } | |
| 29 | + | if run.is_empty() { | |
| 30 | + | run_start = addr; | |
| 31 | + | } | |
| 32 | + | run.push(b); | |
| 33 | + | } | |
| 34 | + | flush(run_start, &mut run); | |
| 35 | + | record(&mut out, 0, 0x01, &[]); | |
| 36 | + | out | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | fn record(out: &mut String, addr: u16, kind: u8, data: &[u8]) { | |
| 40 | + | let mut sum = data.len() as u8; | |
| 41 | + | sum = sum | |
| 42 | + | .wrapping_add((addr >> 8) as u8) | |
| 43 | + | .wrapping_add(addr as u8) | |
| 44 | + | .wrapping_add(kind); | |
| 45 | + | let _ = write!(out, ":{:02X}{addr:04X}{kind:02X}", data.len()); | |
| 46 | + | for &b in data { | |
| 47 | + | sum = sum.wrapping_add(b); | |
| 48 | + | let _ = write!(out, "{b:02X}"); | |
| 49 | + | } | |
| 50 | + | let _ = writeln!(out, "{:02X}", sum.wrapping_neg()); | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | /// Parses Intel HEX into (address, byte) pairs. | |
| 54 | + | pub fn read(text: &str) -> Result<Vec<(u16, u8)>, String> { | |
| 55 | + | let mut out = Vec::new(); | |
| 56 | + | for (n, line) in text.lines().enumerate() { | |
| 57 | + | let line = line.trim(); | |
| 58 | + | if line.is_empty() { | |
| 59 | + | continue; | |
| 60 | + | } | |
| 61 | + | let fail = |m: &str| format!("line {}: {m}", n + 1); | |
| 62 | + | let hex = line | |
| 63 | + | .strip_prefix(':') | |
| 64 | + | .ok_or_else(|| fail("record must start with ':'"))?; | |
| 65 | + | if hex.len() % 2 != 0 || hex.len() < 10 { | |
| 66 | + | return Err(fail("malformed record")); | |
| 67 | + | } | |
| 68 | + | let raw = (0..hex.len()) | |
| 69 | + | .step_by(2) | |
| 70 | + | .map(|i| u8::from_str_radix(&hex[i..i + 2], 16)) | |
| 71 | + | .collect::<Result<Vec<u8>, _>>() | |
| 72 | + | .map_err(|_| fail("invalid hex digit"))?; | |
| 73 | + | let len = raw[0] as usize; | |
| 74 | + | if raw.len() != len + 5 { | |
| 75 | + | return Err(fail("record length mismatch")); | |
| 76 | + | } | |
| 77 | + | if raw.iter().fold(0u8, |a, &b| a.wrapping_add(b)) != 0 { | |
| 78 | + | return Err(fail("checksum mismatch")); | |
| 79 | + | } | |
| 80 | + | let addr = u16::from_be_bytes([raw[1], raw[2]]); | |
| 81 | + | match raw[3] { | |
| 82 | + | 0x00 => out.extend( | |
| 83 | + | raw[4..4 + len] | |
| 84 | + | .iter() | |
| 85 | + | .enumerate() | |
| 86 | + | .map(|(i, &b)| (addr.wrapping_add(i as u16), b)), | |
| 87 | + | ), | |
| 88 | + | 0x01 => break, | |
| 89 | + | // Extended address / start address records don't apply to a 64K 8051. | |
| 90 | + | _ => {} | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | Ok(out) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | #[cfg(test)] | |
| 97 | + | mod tests { | |
| 98 | + | use super::*; | |
| 99 | + | ||
| 100 | + | #[test] | |
| 101 | + | fn round_trip() { | |
| 102 | + | let bytes: Vec<(u16, u8)> = (0..20u16) | |
| 103 | + | .map(|i| (i, i as u8)) | |
| 104 | + | .chain([(0x100, 0xAA)]) | |
| 105 | + | .collect(); | |
| 106 | + | let text = write(bytes.iter().copied()); | |
| 107 | + | assert!(text.ends_with(":00000001FF\n")); | |
| 108 | + | assert_eq!(read(&text).unwrap(), bytes); | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | #[test] | |
| 112 | + | fn known_record() { | |
| 113 | + | // MOV A,#55h ; SJMP $ | |
| 114 | + | let text = write([(0, 0x74), (1, 0x55), (2, 0x80), (3, 0xFE)].into_iter()); | |
| 115 | + | assert_eq!(text.lines().next().unwrap(), ":04000000745580FEB5"); | |
| 116 | + | } | |
| 117 | + | } | |
| @@ -0,0 +1,175 @@ | |||
| 1 | + | #[derive(Debug, Clone, PartialEq)] | |
| 2 | + | pub enum Tok { | |
| 3 | + | Ident(String), | |
| 4 | + | Num(i64), | |
| 5 | + | Str(Vec<u8>), | |
| 6 | + | Punct(char), | |
| 7 | + | /// `<<` and `>>` | |
| 8 | + | Shl, | |
| 9 | + | Shr, | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | #[derive(Debug, Clone)] | |
| 13 | + | pub struct Token { | |
| 14 | + | pub tok: Tok, | |
| 15 | + | pub col: usize, | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | impl Token { | |
| 19 | + | pub fn ident(&self) -> Option<&str> { | |
| 20 | + | match &self.tok { | |
| 21 | + | Tok::Ident(s) => Some(s), | |
| 22 | + | _ => None, | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | pub fn is_punct(&self, c: char) -> bool { | |
| 27 | + | self.tok == Tok::Punct(c) | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | /// Splits one source line into tokens, stopping at a `;` comment. | |
| 32 | + | /// Identifiers are upper-cased (the assembler is case-insensitive). | |
| 33 | + | pub fn lex(line: &str) -> Result<Vec<Token>, (usize, String)> { | |
| 34 | + | let chars: Vec<char> = line.chars().collect(); | |
| 35 | + | let mut out = Vec::new(); | |
| 36 | + | let mut i = 0; | |
| 37 | + | while i < chars.len() { | |
| 38 | + | let c = chars[i]; | |
| 39 | + | let col = i; | |
| 40 | + | if c.is_whitespace() { | |
| 41 | + | i += 1; | |
| 42 | + | } else if c == ';' { | |
| 43 | + | break; | |
| 44 | + | } else if c.is_ascii_alphabetic() || c == '_' || c == '?' { | |
| 45 | + | let start = i; | |
| 46 | + | while i < chars.len() | |
| 47 | + | && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '?') | |
| 48 | + | { | |
| 49 | + | i += 1; | |
| 50 | + | } | |
| 51 | + | let s: String = chars[start..i].iter().collect(); | |
| 52 | + | out.push(Token { | |
| 53 | + | tok: Tok::Ident(s.to_ascii_uppercase()), | |
| 54 | + | col, | |
| 55 | + | }); | |
| 56 | + | } else if c.is_ascii_digit() { | |
| 57 | + | let start = i; | |
| 58 | + | while i < chars.len() && chars[i].is_ascii_alphanumeric() { | |
| 59 | + | i += 1; | |
| 60 | + | } | |
| 61 | + | let s: String = chars[start..i].iter().collect(); | |
| 62 | + | let n = parse_number(&s).ok_or((col, format!("invalid number '{s}'")))?; | |
| 63 | + | out.push(Token { | |
| 64 | + | tok: Tok::Num(n), | |
| 65 | + | col, | |
| 66 | + | }); | |
| 67 | + | } else if c == '\'' || c == '"' { | |
| 68 | + | i += 1; | |
| 69 | + | let mut bytes = Vec::new(); | |
| 70 | + | loop { | |
| 71 | + | match chars.get(i) { | |
| 72 | + | None => return Err((col, "unterminated string".into())), | |
| 73 | + | // A doubled quote inside a string stands for the quote itself. | |
| 74 | + | Some(&q) if q == c && chars.get(i + 1) == Some(&c) => { | |
| 75 | + | bytes.push(c as u8); | |
| 76 | + | i += 2; | |
| 77 | + | } | |
| 78 | + | Some(&q) if q == c => { | |
| 79 | + | i += 1; | |
| 80 | + | break; | |
| 81 | + | } | |
| 82 | + | Some(&ch) => { | |
| 83 | + | let mut buf = [0u8; 4]; | |
| 84 | + | bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes()); | |
| 85 | + | i += 1; | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | out.push(Token { | |
| 90 | + | tok: Tok::Str(bytes), | |
| 91 | + | col, | |
| 92 | + | }); | |
| 93 | + | } else if c == '<' && chars.get(i + 1) == Some(&'<') { | |
| 94 | + | out.push(Token { tok: Tok::Shl, col }); | |
| 95 | + | i += 2; | |
| 96 | + | } else if c == '>' && chars.get(i + 1) == Some(&'>') { | |
| 97 | + | out.push(Token { tok: Tok::Shr, col }); | |
| 98 | + | i += 2; | |
| 99 | + | } else if "+-*/%()#@.,:$~&|^".contains(c) { | |
| 100 | + | out.push(Token { | |
| 101 | + | tok: Tok::Punct(c), | |
| 102 | + | col, | |
| 103 | + | }); | |
| 104 | + | i += 1; | |
| 105 | + | } else { | |
| 106 | + | return Err((col, format!("unexpected character '{c}'"))); | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | Ok(out) | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | /// Number literals: `123`, `123d`, `0x1F`, `1Fh`, `0b101`, `101b`, `17o`, `17q`. | |
| 113 | + | pub fn parse_number(s: &str) -> Option<i64> { | |
| 114 | + | let l = s.to_ascii_lowercase(); | |
| 115 | + | let (digits, radix) = if let Some(r) = l.strip_prefix("0x") { | |
| 116 | + | (r, 16) | |
| 117 | + | } else if let Some(r) = l.strip_suffix('h') { | |
| 118 | + | (r, 16) | |
| 119 | + | } else if let Some(r) = l | |
| 120 | + | .strip_prefix("0b") | |
| 121 | + | .filter(|r| !r.is_empty() && r.chars().all(|c| c == '0' || c == '1')) | |
| 122 | + | { | |
| 123 | + | (r, 2) | |
| 124 | + | } else if let Some(r) = l | |
| 125 | + | .strip_suffix('b') | |
| 126 | + | .filter(|r| r.chars().all(|c| c == '0' || c == '1')) | |
| 127 | + | { | |
| 128 | + | (r, 2) | |
| 129 | + | } else if let Some(r) = l.strip_suffix('o').or_else(|| l.strip_suffix('q')) { | |
| 130 | + | (r, 8) | |
| 131 | + | } else if let Some(r) = l.strip_suffix('d') { | |
| 132 | + | (r, 10) | |
| 133 | + | } else { | |
| 134 | + | (l.as_str(), 10) | |
| 135 | + | }; | |
| 136 | + | if digits.is_empty() { | |
| 137 | + | return None; | |
| 138 | + | } | |
| 139 | + | i64::from_str_radix(digits, radix).ok() | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | #[cfg(test)] | |
| 143 | + | mod tests { | |
| 144 | + | use super::*; | |
| 145 | + | ||
| 146 | + | #[test] | |
| 147 | + | fn numbers() { | |
| 148 | + | assert_eq!(parse_number("0FFh"), Some(255)); | |
| 149 | + | assert_eq!(parse_number("0bh"), Some(11)); | |
| 150 | + | assert_eq!(parse_number("0b101"), Some(5)); | |
| 151 | + | assert_eq!(parse_number("1010b"), Some(10)); | |
| 152 | + | assert_eq!(parse_number("0x1f"), Some(31)); | |
| 153 | + | assert_eq!(parse_number("17o"), Some(15)); | |
| 154 | + | assert_eq!(parse_number("42"), Some(42)); | |
| 155 | + | assert_eq!(parse_number("12x"), None); | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | #[test] | |
| 159 | + | fn lexes_line() { | |
| 160 | + | let t = lex("loop: mov a, #'x' ; comment").unwrap(); | |
| 161 | + | let toks: Vec<_> = t.into_iter().map(|t| t.tok).collect(); | |
| 162 | + | assert_eq!( | |
| 163 | + | toks, | |
| 164 | + | vec![ | |
| 165 | + | Tok::Ident("LOOP".into()), | |
| 166 | + | Tok::Punct(':'), | |
| 167 | + | Tok::Ident("MOV".into()), | |
| 168 | + | Tok::Ident("A".into()), | |
| 169 | + | Tok::Punct(','), | |
| 170 | + | Tok::Punct('#'), | |
| 171 | + | Tok::Str(vec![b'x']), | |
| 172 | + | ] | |
| 173 | + | ); | |
| 174 | + | } | |
| 175 | + | } | |
| @@ -0,0 +1,53 @@ | |||
| 1 | + | use std::fmt::Write; | |
| 2 | + | ||
| 3 | + | use super::AsmOutput; | |
| 4 | + | ||
| 5 | + | const BYTES_PER_ROW: usize = 4; | |
| 6 | + | ||
| 7 | + | /// Keil A51-style listing: location, object bytes, line number, source, | |
| 8 | + | /// with errors inline and a symbol table at the end. | |
| 9 | + | pub fn render(out: &AsmOutput, source: &str) -> String { | |
| 10 | + | let mut s = String::new(); | |
| 11 | + | let _ = writeln!(s, "LOC OBJ LINE SOURCE"); | |
| 12 | + | let _ = writeln!(s); | |
| 13 | + | let mut diags = out.diagnostics.iter().peekable(); | |
| 14 | + | for (i, text) in source.lines().enumerate() { | |
| 15 | + | let line = out.lines.get(i).cloned().unwrap_or_default(); | |
| 16 | + | let loc = line.addr.map_or(" ".to_string(), |a| format!("{a:04X}")); | |
| 17 | + | let mut chunks = line.bytes.chunks(BYTES_PER_ROW); | |
| 18 | + | let obj = chunks.next().map(hex_bytes).unwrap_or_default(); | |
| 19 | + | let _ = writeln!(s, "{loc} {obj:<12} {:>5} {}", i + 1, text.trim_end()); | |
| 20 | + | let mut addr = line.addr.unwrap_or(0) as usize; | |
| 21 | + | for c in chunks { | |
| 22 | + | addr += BYTES_PER_ROW; | |
| 23 | + | let _ = writeln!(s, "{:04X} {}", addr & 0xFFFF, hex_bytes(c)); | |
| 24 | + | } | |
| 25 | + | while let Some(d) = diags.next_if(|d| d.line == i) { | |
| 26 | + | let _ = writeln!(s, "*** ERROR at column {}: {}", d.col + 1, d.msg); | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | let _ = writeln!(s); | |
| 31 | + | let _ = writeln!(s, "SYMBOL TABLE"); | |
| 32 | + | let _ = writeln!(s, "NAME TYPE VALUE"); | |
| 33 | + | for (name, sym) in &out.symbols { | |
| 34 | + | let _ = writeln!( | |
| 35 | + | s, | |
| 36 | + | "{name:<19} {:<7} {:04X}h", | |
| 37 | + | sym.kind.name(), | |
| 38 | + | sym.value & 0xFFFF | |
| 39 | + | ); | |
| 40 | + | } | |
| 41 | + | let _ = writeln!(s); | |
| 42 | + | let _ = writeln!( | |
| 43 | + | s, | |
| 44 | + | "ASSEMBLY COMPLETE: {} ERROR(S), {} BYTE(S) OF CODE", | |
| 45 | + | out.diagnostics.len(), | |
| 46 | + | out.code_size() | |
| 47 | + | ); | |
| 48 | + | s | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | fn hex_bytes(b: &[u8]) -> String { | |
| 52 | + | b.iter().map(|x| format!("{x:02X}")).collect() | |
| 53 | + | } | |
| @@ -0,0 +1,940 @@ | |||
| 1 | + | //! Two-pass Intel 8051 assembler. | |
| 2 | + | //! | |
| 3 | + | //! Pass one parses every line, picks the opcode for each instruction (which | |
| 4 | + | //! fixes its size) and assigns addresses to labels. Pass two evaluates operand | |
| 5 | + | //! expressions with the complete symbol table and emits bytes. | |
| 6 | + | ||
| 7 | + | mod expr; | |
| 8 | + | pub mod hex; | |
| 9 | + | mod lexer; | |
| 10 | + | mod listing; | |
| 11 | + | ||
| 12 | + | use std::collections::{BTreeMap, HashMap}; | |
| 13 | + | ||
| 14 | + | use crate::isa::{self, OpInfo, Operand}; | |
| 15 | + | use crate::sfr; | |
| 16 | + | use expr::{Env, EvalError, Expr, Parser}; | |
| 17 | + | use lexer::{Tok, Token, lex}; | |
| 18 | + | ||
| 19 | + | #[derive(Debug, Clone)] | |
| 20 | + | pub struct Diagnostic { | |
| 21 | + | /// 0-based source line. | |
| 22 | + | pub line: usize, | |
| 23 | + | /// 0-based column. | |
| 24 | + | pub col: usize, | |
| 25 | + | pub msg: String, | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 29 | + | pub enum SymKind { | |
| 30 | + | Code, | |
| 31 | + | Number, | |
| 32 | + | Bit, | |
| 33 | + | Data, | |
| 34 | + | Idata, | |
| 35 | + | Xdata, | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | impl SymKind { | |
| 39 | + | pub fn name(self) -> &'static str { | |
| 40 | + | match self { | |
| 41 | + | SymKind::Code => "CODE", | |
| 42 | + | SymKind::Number => "NUMBER", | |
| 43 | + | SymKind::Bit => "BIT", | |
| 44 | + | SymKind::Data => "DATA", | |
| 45 | + | SymKind::Idata => "IDATA", | |
| 46 | + | SymKind::Xdata => "XDATA", | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | #[derive(Debug, Clone)] | |
| 52 | + | pub struct Symbol { | |
| 53 | + | pub value: i64, | |
| 54 | + | pub kind: SymKind, | |
| 55 | + | pub line: usize, | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | /// What one source line produced. | |
| 59 | + | #[derive(Debug, Clone, Default)] | |
| 60 | + | pub struct LineOut { | |
| 61 | + | /// Location counter at the start of the line, if the line occupies or | |
| 62 | + | /// labels an address. | |
| 63 | + | pub addr: Option<u16>, | |
| 64 | + | pub bytes: Vec<u8>, | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | #[derive(Debug, Clone, Default)] | |
| 68 | + | pub struct AsmOutput { | |
| 69 | + | pub lines: Vec<LineOut>, | |
| 70 | + | pub symbols: BTreeMap<String, Symbol>, | |
| 71 | + | pub diagnostics: Vec<Diagnostic>, | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | impl AsmOutput { | |
| 75 | + | pub fn ok(&self) -> bool { | |
| 76 | + | self.diagnostics.is_empty() | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | /// All emitted code bytes as (address, byte), in source order. | |
| 80 | + | pub fn bytes(&self) -> impl Iterator<Item = (u16, u8)> + '_ { | |
| 81 | + | self.lines.iter().flat_map(|l| { | |
| 82 | + | let base = l.addr.unwrap_or(0); | |
| 83 | + | l.bytes | |
| 84 | + | .iter() | |
| 85 | + | .enumerate() | |
| 86 | + | .map(move |(i, &b)| (base.wrapping_add(i as u16), b)) | |
| 87 | + | }) | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | /// Source line holding the instruction that starts at each address. | |
| 91 | + | pub fn addr_to_line(&self) -> HashMap<u16, usize> { | |
| 92 | + | let mut m = HashMap::new(); | |
| 93 | + | for (i, l) in self.lines.iter().enumerate() { | |
| 94 | + | if let (Some(a), false) = (l.addr, l.bytes.is_empty()) { | |
| 95 | + | m.entry(a).or_insert(i); | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | m | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | pub fn code_size(&self) -> usize { | |
| 102 | + | self.lines.iter().map(|l| l.bytes.len()).sum() | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | pub fn listing(&self, source: &str) -> String { | |
| 106 | + | listing::render(self, source) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | pub fn intel_hex(&self) -> String { | |
| 110 | + | hex::write(self.bytes()) | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | /// Operand as written in the source, before matching it against the ISA. | |
| 115 | + | #[derive(Debug, Clone)] | |
| 116 | + | enum Syn { | |
| 117 | + | A, | |
| 118 | + | AB, | |
| 119 | + | C, | |
| 120 | + | Dptr, | |
| 121 | + | Reg(u8), | |
| 122 | + | AtReg(u8), | |
| 123 | + | AtDptr, | |
| 124 | + | AtAPlusDptr, | |
| 125 | + | AtAPlusPc, | |
| 126 | + | Imm(Expr), | |
| 127 | + | NotBit(Expr), | |
| 128 | + | Expr(Expr), | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | #[derive(Debug, Clone)] | |
| 132 | + | enum DbItem { | |
| 133 | + | Bytes(Vec<u8>), | |
| 134 | + | Expr(Expr, usize), | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | #[derive(Debug, Clone)] | |
| 138 | + | enum Stmt { | |
| 139 | + | Empty, | |
| 140 | + | Instr { | |
| 141 | + | info: &'static OpInfo, | |
| 142 | + | args: Vec<(Expr, usize)>, | |
| 143 | + | }, | |
| 144 | + | Org(Expr, usize), | |
| 145 | + | Db(Vec<DbItem>), | |
| 146 | + | Dw(Vec<(Expr, usize)>), | |
| 147 | + | Ds(Expr, usize), | |
| 148 | + | Equ { | |
| 149 | + | name: String, | |
| 150 | + | kind: SymKind, | |
| 151 | + | expr: Expr, | |
| 152 | + | col: usize, | |
| 153 | + | redefinable: bool, | |
| 154 | + | }, | |
| 155 | + | End, | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | impl Stmt { | |
| 159 | + | fn size(&self) -> u16 { | |
| 160 | + | match self { | |
| 161 | + | Stmt::Instr { info, .. } => info.size as u16, | |
| 162 | + | Stmt::Db(items) => items | |
| 163 | + | .iter() | |
| 164 | + | .map(|i| match i { | |
| 165 | + | DbItem::Bytes(b) => b.len() as u16, | |
| 166 | + | DbItem::Expr(..) => 1, | |
| 167 | + | }) | |
| 168 | + | .sum(), | |
| 169 | + | Stmt::Dw(items) => 2 * items.len() as u16, | |
| 170 | + | _ => 0, | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | struct ParsedLine { | |
| 176 | + | label: Option<(String, usize)>, | |
| 177 | + | stmt: Stmt, | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | type PResult<T> = Result<T, (usize, String)>; | |
| 181 | + | ||
| 182 | + | fn split_commas(toks: &[Token]) -> Vec<&[Token]> { | |
| 183 | + | if toks.is_empty() { | |
| 184 | + | return Vec::new(); | |
| 185 | + | } | |
| 186 | + | let mut parts = Vec::new(); | |
| 187 | + | let mut depth = 0; | |
| 188 | + | let mut start = 0; | |
| 189 | + | for (i, t) in toks.iter().enumerate() { | |
| 190 | + | match t.tok { | |
| 191 | + | Tok::Punct('(') => depth += 1, | |
| 192 | + | Tok::Punct(')') => depth -= 1, | |
| 193 | + | Tok::Punct(',') if depth == 0 => { | |
| 194 | + | parts.push(&toks[start..i]); | |
| 195 | + | start = i + 1; | |
| 196 | + | } | |
| 197 | + | _ => {} | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | parts.push(&toks[start..]); | |
| 201 | + | parts | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | fn parse_expr(toks: &[Token], fallback_col: usize) -> PResult<Expr> { | |
| 205 | + | if toks.is_empty() { | |
| 206 | + | return Err((fallback_col, "missing operand".into())); | |
| 207 | + | } | |
| 208 | + | Parser::parse_all(toks) | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | fn reg_num(s: &str) -> Option<u8> { | |
| 212 | + | let n = s.strip_prefix('R')?; | |
| 213 | + | if n.len() == 1 { | |
| 214 | + | n.parse().ok().filter(|&n: &u8| n < 8) | |
| 215 | + | } else { | |
| 216 | + | None | |
| 217 | + | } | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | fn parse_operand(toks: &[Token], col: usize) -> PResult<Syn> { | |
| 221 | + | let Some(first) = toks.first() else { | |
| 222 | + | return Err((col, "missing operand".into())); | |
| 223 | + | }; | |
| 224 | + | let words: Vec<Option<&str>> = toks.iter().map(|t| t.ident()).collect(); | |
| 225 | + | if toks.len() == 1 | |
| 226 | + | && let Some(w) = first.ident() | |
| 227 | + | { | |
| 228 | + | match w { | |
| 229 | + | "A" => return Ok(Syn::A), | |
| 230 | + | "AB" => return Ok(Syn::AB), | |
| 231 | + | "C" => return Ok(Syn::C), | |
| 232 | + | "DPTR" => return Ok(Syn::Dptr), | |
| 233 | + | _ => {} | |
| 234 | + | } | |
| 235 | + | if let Some(n) = reg_num(w) { | |
| 236 | + | return Ok(Syn::Reg(n)); | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | match &first.tok { | |
| 240 | + | Tok::Punct('@') => match (&words[1..], toks.get(2)) { | |
| 241 | + | ([Some("R0")], _) => Ok(Syn::AtReg(0)), | |
| 242 | + | ([Some("R1")], _) => Ok(Syn::AtReg(1)), | |
| 243 | + | ([Some("DPTR")], _) => Ok(Syn::AtDptr), | |
| 244 | + | ([Some("A"), None, Some("DPTR")], Some(p)) if p.is_punct('+') => Ok(Syn::AtAPlusDptr), | |
| 245 | + | ([Some("A"), None, Some("PC")], Some(p)) if p.is_punct('+') => Ok(Syn::AtAPlusPc), | |
| 246 | + | _ => Err(( | |
| 247 | + | first.col, | |
| 248 | + | "invalid indirect operand (expected @R0, @R1, @DPTR, @A+DPTR or @A+PC)".into(), | |
| 249 | + | )), | |
| 250 | + | }, | |
| 251 | + | Tok::Punct('#') => Ok(Syn::Imm(parse_expr(&toks[1..], first.col + 1)?)), | |
| 252 | + | Tok::Punct('/') => Ok(Syn::NotBit(parse_expr(&toks[1..], first.col + 1)?)), | |
| 253 | + | _ => Ok(Syn::Expr(parse_expr(toks, col)?)), | |
| 254 | + | } | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | fn operand_matches(op: Operand, syn: &Syn) -> bool { | |
| 258 | + | match (op, syn) { | |
| 259 | + | (Operand::A, Syn::A) | |
| 260 | + | | (Operand::AB, Syn::AB) | |
| 261 | + | | (Operand::C, Syn::C) | |
| 262 | + | | (Operand::Dptr, Syn::Dptr) | |
| 263 | + | | (Operand::AtDptr, Syn::AtDptr) | |
| 264 | + | | (Operand::AtAPlusDptr, Syn::AtAPlusDptr) | |
| 265 | + | | (Operand::AtAPlusPc, Syn::AtAPlusPc) => true, | |
| 266 | + | (Operand::Reg(a), Syn::Reg(b)) | (Operand::AtReg(a), Syn::AtReg(b)) => a == *b, | |
| 267 | + | (Operand::Imm8 | Operand::Imm16, Syn::Imm(_)) => true, | |
| 268 | + | (Operand::NotBit, Syn::NotBit(_)) => true, | |
| 269 | + | ( | |
| 270 | + | Operand::Direct | Operand::Bit | Operand::Addr11 | Operand::Addr16 | Operand::Rel, | |
| 271 | + | Syn::Expr(_), | |
| 272 | + | ) => true, | |
| 273 | + | _ => false, | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | fn parse_instruction(mnemonic: &str, col: usize, rest: &[Token]) -> PResult<Stmt> { | |
| 278 | + | let mut syns = Vec::new(); | |
| 279 | + | let mut cols = Vec::new(); | |
| 280 | + | for part in split_commas(rest) { | |
| 281 | + | let c = part.first().map_or(col, |t| t.col); | |
| 282 | + | syns.push(parse_operand(part, c)?); | |
| 283 | + | cols.push(c); | |
| 284 | + | } | |
| 285 | + | // Generic JMP/CALL always assemble to the long forms, so sizes never | |
| 286 | + | // depend on label values. | |
| 287 | + | let lookup = match (mnemonic, syns.as_slice()) { | |
| 288 | + | ("JMP", [Syn::Expr(_)]) => "LJMP", | |
| 289 | + | ("CALL", _) => "LCALL", | |
| 290 | + | (m, _) => m, | |
| 291 | + | }; | |
| 292 | + | let info = isa::by_mnemonic(lookup) | |
| 293 | + | .find(|i| { | |
| 294 | + | i.operands.len() == syns.len() | |
| 295 | + | && i.operands | |
| 296 | + | .iter() | |
| 297 | + | .zip(&syns) | |
| 298 | + | .all(|(&o, s)| operand_matches(o, s)) | |
| 299 | + | }) | |
| 300 | + | .ok_or_else(|| (col, format!("invalid operands for {mnemonic}")))?; | |
| 301 | + | let args = syns | |
| 302 | + | .into_iter() | |
| 303 | + | .zip(cols) | |
| 304 | + | .filter_map(|(s, c)| match s { | |
| 305 | + | Syn::Imm(e) | Syn::NotBit(e) | Syn::Expr(e) => Some((e, c)), | |
| 306 | + | _ => None, | |
| 307 | + | }) | |
| 308 | + | .collect(); | |
| 309 | + | Ok(Stmt::Instr { info, args }) | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | fn parse_line(toks: &[Token]) -> PResult<ParsedLine> { | |
| 313 | + | let mut toks = toks; | |
| 314 | + | let mut label = None; | |
| 315 | + | ||
| 316 | + | // `name EQU expr` and friends. | |
| 317 | + | if let (Some(name), Some(kw)) = ( | |
| 318 | + | toks.first().and_then(Token::ident), | |
| 319 | + | toks.get(1).and_then(Token::ident), | |
| 320 | + | ) { | |
| 321 | + | let kind = match kw { | |
| 322 | + | "EQU" | "SET" => Some(SymKind::Number), | |
| 323 | + | "BIT" => Some(SymKind::Bit), | |
| 324 | + | "DATA" => Some(SymKind::Data), | |
| 325 | + | "IDATA" => Some(SymKind::Idata), | |
| 326 | + | "XDATA" => Some(SymKind::Xdata), | |
| 327 | + | "CODE" => Some(SymKind::Code), | |
| 328 | + | _ => None, | |
| 329 | + | }; | |
| 330 | + | if let Some(kind) = kind { | |
| 331 | + | let expr = parse_expr(&toks[2..], toks[1].col + kw.len())?; | |
| 332 | + | return Ok(ParsedLine { | |
| 333 | + | label: None, | |
| 334 | + | stmt: Stmt::Equ { | |
| 335 | + | name: name.to_string(), | |
| 336 | + | kind, | |
| 337 | + | expr, | |
| 338 | + | col: toks[0].col, | |
| 339 | + | redefinable: kw == "SET", | |
| 340 | + | }, | |
| 341 | + | }); | |
| 342 | + | } | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | if let (Some(name), Some(colon)) = (toks.first().and_then(Token::ident), toks.get(1)) | |
| 346 | + | && colon.is_punct(':') | |
| 347 | + | { | |
| 348 | + | label = Some((name.to_string(), toks[0].col)); | |
| 349 | + | toks = &toks[2..]; | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | // Allow `.org`, `.db`, ... spellings. | |
| 353 | + | if toks.first().is_some_and(|t| t.is_punct('.')) { | |
| 354 | + | toks = &toks[1..]; | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | let Some(first) = toks.first() else { | |
| 358 | + | return Ok(ParsedLine { | |
| 359 | + | label, | |
| 360 | + | stmt: Stmt::Empty, | |
| 361 | + | }); | |
| 362 | + | }; | |
| 363 | + | let Some(word) = first.ident() else { | |
| 364 | + | return Err((first.col, "expected instruction or directive".into())); | |
| 365 | + | }; | |
| 366 | + | let rest = &toks[1..]; | |
| 367 | + | let end_col = first.col + word.len(); | |
| 368 | + | ||
| 369 | + | let stmt = match word { | |
| 370 | + | "ORG" => Stmt::Org(parse_expr(rest, end_col)?, first.col), | |
| 371 | + | "CSEG" | "XSEG" | "DSEG" if rest.first().and_then(Token::ident) == Some("AT") => { | |
| 372 | + | Stmt::Org(parse_expr(&rest[1..], end_col)?, first.col) | |
| 373 | + | } | |
| 374 | + | "DB" | "BYTE" => { | |
| 375 | + | let mut items = Vec::new(); | |
| 376 | + | for part in split_commas(rest) { | |
| 377 | + | match part { | |
| 378 | + | [ | |
| 379 | + | Token { | |
| 380 | + | tok: Tok::Str(b), .. | |
| 381 | + | }, | |
| 382 | + | ] if b.len() != 1 => items.push(DbItem::Bytes(b.clone())), | |
| 383 | + | _ => { | |
| 384 | + | let c = part.first().map_or(end_col, |t| t.col); | |
| 385 | + | items.push(DbItem::Expr(parse_expr(part, c)?, c)); | |
| 386 | + | } | |
| 387 | + | } | |
| 388 | + | } | |
| 389 | + | if items.is_empty() { | |
| 390 | + | return Err((end_col, "DB needs at least one value".into())); | |
| 391 | + | } | |
| 392 | + | Stmt::Db(items) | |
| 393 | + | } | |
| 394 | + | "DW" | "WORD" => { | |
| 395 | + | let mut items = Vec::new(); | |
| 396 | + | for part in split_commas(rest) { | |
| 397 | + | let c = part.first().map_or(end_col, |t| t.col); | |
| 398 | + | items.push((parse_expr(part, c)?, c)); | |
| 399 | + | } | |
| 400 | + | if items.is_empty() { | |
| 401 | + | return Err((end_col, "DW needs at least one value".into())); | |
| 402 | + | } | |
| 403 | + | Stmt::Dw(items) | |
| 404 | + | } | |
| 405 | + | "DS" => Stmt::Ds(parse_expr(rest, end_col)?, first.col), | |
| 406 | + | "END" => Stmt::End, | |
| 407 | + | // Accepted for compatibility, no effect in a single-module assembler. | |
| 408 | + | "CSEG" | "USING" | "NAME" => Stmt::Empty, | |
| 409 | + | m if isa::is_mnemonic(m) => parse_instruction(m, first.col, rest)?, | |
| 410 | + | _ if rest.first().is_some_and(|t| t.is_punct(':')) => { | |
| 411 | + | return Err(( | |
| 412 | + | first.col, | |
| 413 | + | "a label must be the first thing on a line".into(), | |
| 414 | + | )); | |
| 415 | + | } | |
| 416 | + | _ => { | |
| 417 | + | return Err(( | |
| 418 | + | first.col, | |
| 419 | + | format!("unknown instruction or directive '{word}'"), | |
| 420 | + | )); | |
| 421 | + | } | |
| 422 | + | }; | |
| 423 | + | Ok(ParsedLine { label, stmt }) | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | struct Symbols { | |
| 427 | + | user: HashMap<String, Symbol>, | |
| 428 | + | here: i64, | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | impl Env for Symbols { | |
| 432 | + | fn lookup(&self, name: &str) -> Option<i64> { | |
| 433 | + | if let Some(s) = self.user.get(name) { | |
| 434 | + | return Some(s.value); | |
| 435 | + | } | |
| 436 | + | sfr::SFRS | |
| 437 | + | .iter() | |
| 438 | + | .chain(sfr::BITS) | |
| 439 | + | .find(|(n, _)| *n == name) | |
| 440 | + | .map(|&(_, v)| v as i64) | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | fn here(&self) -> i64 { | |
| 444 | + | self.here | |
| 445 | + | } | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | fn is_predefined(name: &str) -> bool { | |
| 449 | + | sfr::SFRS.iter().chain(sfr::BITS).any(|(n, _)| *n == name) | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | fn is_reserved(name: &str) -> bool { | |
| 453 | + | matches!(name, "A" | "AB" | "C" | "DPTR" | "PC") | |
| 454 | + | || reg_num(name).is_some() | |
| 455 | + | || isa::is_mnemonic(name) | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | pub fn assemble(source: &str) -> AsmOutput { | |
| 459 | + | let mut diags: Vec<Diagnostic> = Vec::new(); | |
| 460 | + | let mut syms = Symbols { | |
| 461 | + | user: HashMap::new(), | |
| 462 | + | here: 0, | |
| 463 | + | }; | |
| 464 | + | let mut parsed: Vec<Option<ParsedLine>> = Vec::new(); | |
| 465 | + | let mut addrs: Vec<i64> = Vec::new(); | |
| 466 | + | let mut pending_equ: Vec<usize> = Vec::new(); | |
| 467 | + | ||
| 468 | + | let err = |diags: &mut Vec<Diagnostic>, line: usize, col: usize, msg: String| { | |
| 469 | + | diags.push(Diagnostic { line, col, msg }); | |
| 470 | + | }; | |
| 471 | + | ||
| 472 | + | // ---- Pass 1: parse, size, assign label addresses. ---- | |
| 473 | + | let mut loc: i64 = 0; | |
| 474 | + | let mut ended = false; | |
| 475 | + | for (ln, text) in source.lines().enumerate() { | |
| 476 | + | addrs.push(loc); | |
| 477 | + | if ended { | |
| 478 | + | parsed.push(None); | |
| 479 | + | continue; | |
| 480 | + | } | |
| 481 | + | let line = match lex(text).and_then(|t| parse_line(&t)) { | |
| 482 | + | Ok(l) => l, | |
| 483 | + | Err((col, msg)) => { | |
| 484 | + | err(&mut diags, ln, col, msg); | |
| 485 | + | parsed.push(None); | |
| 486 | + | continue; | |
| 487 | + | } | |
| 488 | + | }; | |
| 489 | + | ||
| 490 | + | if let Some((name, col)) = &line.label { | |
| 491 | + | define( | |
| 492 | + | &mut syms, | |
| 493 | + | &mut diags, | |
| 494 | + | name, | |
| 495 | + | *col, | |
| 496 | + | ln, | |
| 497 | + | loc, | |
| 498 | + | SymKind::Code, | |
| 499 | + | false, | |
| 500 | + | ); | |
| 501 | + | } | |
| 502 | + | ||
| 503 | + | syms.here = loc; | |
| 504 | + | match &line.stmt { | |
| 505 | + | Stmt::Org(e, col) | Stmt::Ds(e, col) => match e.eval(&syms) { | |
| 506 | + | Ok(v) if matches!(line.stmt, Stmt::Org(..)) => { | |
| 507 | + | if !(0..=0xFFFF).contains(&v) { | |
| 508 | + | err( | |
| 509 | + | &mut diags, | |
| 510 | + | ln, | |
| 511 | + | *col, | |
| 512 | + | format!("ORG address {v} out of range"), | |
| 513 | + | ); | |
| 514 | + | } else { | |
| 515 | + | loc = v; | |
| 516 | + | addrs[ln] = loc; | |
| 517 | + | } | |
| 518 | + | } | |
| 519 | + | Ok(v) => { | |
| 520 | + | if v < 0 { | |
| 521 | + | err(&mut diags, ln, *col, "DS size must not be negative".into()); | |
| 522 | + | } else { | |
| 523 | + | loc += v; | |
| 524 | + | } | |
| 525 | + | } | |
| 526 | + | Err(EvalError::Undefined(s, c)) => err( | |
| 527 | + | &mut diags, | |
| 528 | + | ln, | |
| 529 | + | c, | |
| 530 | + | format!("'{s}' must be defined before it is used here"), | |
| 531 | + | ), | |
| 532 | + | Err(EvalError::Other(m)) => err(&mut diags, ln, *col, m), | |
| 533 | + | }, | |
| 534 | + | Stmt::Equ { | |
| 535 | + | name, | |
| 536 | + | kind, | |
| 537 | + | expr, | |
| 538 | + | col, | |
| 539 | + | redefinable, | |
| 540 | + | } => match expr.eval(&syms) { | |
| 541 | + | Ok(v) => define( | |
| 542 | + | &mut syms, | |
| 543 | + | &mut diags, | |
| 544 | + | name, | |
| 545 | + | *col, | |
| 546 | + | ln, | |
| 547 | + | v, | |
| 548 | + | *kind, | |
| 549 | + | *redefinable, | |
| 550 | + | ), | |
| 551 | + | Err(EvalError::Undefined(..)) => pending_equ.push(ln), | |
| 552 | + | Err(EvalError::Other(m)) => err(&mut diags, ln, *col, m), | |
| 553 | + | }, | |
| 554 | + | Stmt::End => ended = true, | |
| 555 | + | s => loc += s.size() as i64, | |
| 556 | + | } | |
| 557 | + | if loc > 0x10000 { | |
| 558 | + | err(&mut diags, ln, 0, "code exceeds 64K address space".into()); | |
| 559 | + | loc &= 0xFFFF; | |
| 560 | + | } | |
| 561 | + | parsed.push(Some(line)); | |
| 562 | + | } | |
| 563 | + | ||
| 564 | + | // Resolve EQUs that referred to symbols defined later. | |
| 565 | + | loop { | |
| 566 | + | let before = pending_equ.len(); | |
| 567 | + | pending_equ.retain(|&ln| { | |
| 568 | + | let Some(ParsedLine { | |
| 569 | + | stmt: | |
| 570 | + | Stmt::Equ { | |
| 571 | + | name, | |
| 572 | + | kind, | |
| 573 | + | expr, | |
| 574 | + | col, | |
| 575 | + | redefinable, | |
| 576 | + | }, | |
| 577 | + | .. | |
| 578 | + | }) = &parsed[ln] | |
| 579 | + | else { | |
| 580 | + | return false; | |
| 581 | + | }; | |
| 582 | + | syms.here = addrs[ln]; | |
| 583 | + | match expr.eval(&syms) { | |
| 584 | + | Ok(v) => { | |
| 585 | + | define( | |
| 586 | + | &mut syms, | |
| 587 | + | &mut diags, | |
| 588 | + | name, | |
| 589 | + | *col, | |
| 590 | + | ln, | |
| 591 | + | v, | |
| 592 | + | *kind, | |
| 593 | + | *redefinable, | |
| 594 | + | ); | |
| 595 | + | false | |
| 596 | + | } | |
| 597 | + | Err(_) => true, | |
| 598 | + | } | |
| 599 | + | }); | |
| 600 | + | if pending_equ.len() == before { | |
| 601 | + | break; | |
| 602 | + | } | |
| 603 | + | } | |
| 604 | + | for ln in pending_equ { | |
| 605 | + | if let Some(ParsedLine { | |
| 606 | + | stmt: Stmt::Equ { expr, col, .. }, | |
| 607 | + | .. | |
| 608 | + | }) = &parsed[ln] | |
| 609 | + | { | |
| 610 | + | syms.here = addrs[ln]; | |
| 611 | + | match expr.eval(&syms) { | |
| 612 | + | Err(EvalError::Undefined(s, c)) => { | |
| 613 | + | err(&mut diags, ln, c, format!("undefined symbol '{s}'")) | |
| 614 | + | } | |
| 615 | + | Err(EvalError::Other(m)) => err(&mut diags, ln, *col, m), | |
| 616 | + | Ok(_) => {} | |
| 617 | + | } | |
| 618 | + | } | |
| 619 | + | } | |
| 620 | + | ||
| 621 | + | // ---- Pass 2: emit bytes. ---- | |
| 622 | + | let mut lines = Vec::with_capacity(parsed.len()); | |
| 623 | + | let mut used = vec![false; 0x10000]; | |
| 624 | + | for (ln, p) in parsed.iter().enumerate() { | |
| 625 | + | let mut out = LineOut::default(); | |
| 626 | + | let Some(p) = p else { | |
| 627 | + | lines.push(out); | |
| 628 | + | continue; | |
| 629 | + | }; | |
| 630 | + | let pc = addrs[ln]; | |
| 631 | + | syms.here = pc; | |
| 632 | + | let has_addr = p.label.is_some() | |
| 633 | + | || matches!( | |
| 634 | + | p.stmt, | |
| 635 | + | Stmt::Instr { .. } | Stmt::Db(_) | Stmt::Dw(_) | Stmt::Ds(..) | Stmt::Org(..) | |
| 636 | + | ); | |
| 637 | + | if has_addr { | |
| 638 | + | out.addr = Some(pc as u16); | |
| 639 | + | } | |
| 640 | + | ||
| 641 | + | let eval = |e: &Expr, col: usize, diags: &mut Vec<Diagnostic>| -> Option<i64> { | |
| 642 | + | match e.eval(&syms) { | |
| 643 | + | Ok(v) => Some(v), | |
| 644 | + | Err(EvalError::Undefined(s, c)) => { | |
| 645 | + | err(diags, ln, c, format!("undefined symbol '{s}'")); | |
| 646 | + | None | |
| 647 | + | } | |
| 648 | + | Err(EvalError::Other(m)) => { | |
| 649 | + | err(diags, ln, col, m); | |
| 650 | + | None | |
| 651 | + | } | |
| 652 | + | } | |
| 653 | + | }; | |
| 654 | + | ||
| 655 | + | match &p.stmt { | |
| 656 | + | Stmt::Instr { info, args } => { | |
| 657 | + | let mut bytes = vec![info.opcode]; | |
| 658 | + | let kinds = info.operands.iter().filter(|o| o.len() > 0); | |
| 659 | + | let mut failed = false; | |
| 660 | + | for (&kind, (e, col)) in kinds.zip(args) { | |
| 661 | + | let Some(v) = eval(e, *col, &mut diags) else { | |
| 662 | + | failed = true; | |
| 663 | + | continue; | |
| 664 | + | }; | |
| 665 | + | if let Err(m) = encode_operand(kind, v, pc, info.size, &mut bytes) { | |
| 666 | + | err(&mut diags, ln, *col, m); | |
| 667 | + | failed = true; | |
| 668 | + | } | |
| 669 | + | } | |
| 670 | + | if !failed { | |
| 671 | + | if info.opcode == 0x85 { | |
| 672 | + | // MOV dest,src is encoded with the source byte first. | |
| 673 | + | bytes.swap(1, 2); | |
| 674 | + | } | |
| 675 | + | out.bytes = bytes; | |
| 676 | + | } | |
| 677 | + | } | |
| 678 | + | Stmt::Db(items) => { | |
| 679 | + | for item in items { | |
| 680 | + | match item { | |
| 681 | + | DbItem::Bytes(b) => out.bytes.extend_from_slice(b), | |
| 682 | + | DbItem::Expr(e, col) => { | |
| 683 | + | let v = eval(e, *col, &mut diags).unwrap_or(0); | |
| 684 | + | if !(-128..=255).contains(&v) { | |
| 685 | + | err( | |
| 686 | + | &mut diags, | |
| 687 | + | ln, | |
| 688 | + | *col, | |
| 689 | + | format!("value {v} does not fit in a byte"), | |
| 690 | + | ); | |
| 691 | + | } | |
| 692 | + | out.bytes.push(v as u8); | |
| 693 | + | } | |
| 694 | + | } | |
| 695 | + | } | |
| 696 | + | } | |
| 697 | + | Stmt::Dw(items) => { | |
| 698 | + | for (e, col) in items { | |
| 699 | + | let v = eval(e, *col, &mut diags).unwrap_or(0); | |
| 700 | + | if !(-32768..=0xFFFF).contains(&v) { | |
| 701 | + | err( | |
| 702 | + | &mut diags, | |
| 703 | + | ln, | |
| 704 | + | *col, | |
| 705 | + | format!("value {v} does not fit in a word"), | |
| 706 | + | ); | |
| 707 | + | } | |
| 708 | + | out.bytes.push((v >> 8) as u8); | |
| 709 | + | out.bytes.push(v as u8); | |
| 710 | + | } | |
| 711 | + | } | |
| 712 | + | _ => {} | |
| 713 | + | } | |
| 714 | + | ||
| 715 | + | for i in 0..out.bytes.len() { | |
| 716 | + | let a = (pc as usize + i) & 0xFFFF; | |
| 717 | + | if used[a] { | |
| 718 | + | err( | |
| 719 | + | &mut diags, | |
| 720 | + | ln, | |
| 721 | + | 0, | |
| 722 | + | format!("code overlaps previously emitted code at {a:04X}h"), | |
| 723 | + | ); | |
| 724 | + | break; | |
| 725 | + | } | |
| 726 | + | used[a] = true; | |
| 727 | + | } | |
| 728 | + | lines.push(out); | |
| 729 | + | } | |
| 730 | + | ||
| 731 | + | diags.sort_by_key(|d| (d.line, d.col)); | |
| 732 | + | let symbols = syms.user.into_iter().collect(); | |
| 733 | + | AsmOutput { | |
| 734 | + | lines, | |
| 735 | + | symbols, | |
| 736 | + | diagnostics: diags, | |
| 737 | + | } | |
| 738 | + | } | |
| 739 | + | ||
| 740 | + | #[allow(clippy::too_many_arguments)] | |
| 741 | + | fn define( | |
| 742 | + | syms: &mut Symbols, | |
| 743 | + | diags: &mut Vec<Diagnostic>, | |
| 744 | + | name: &str, | |
| 745 | + | col: usize, | |
| 746 | + | line: usize, | |
| 747 | + | value: i64, | |
| 748 | + | kind: SymKind, | |
| 749 | + | redefinable: bool, | |
| 750 | + | ) { | |
| 751 | + | let msg = if is_reserved(name) { | |
| 752 | + | Some(format!("'{name}' is a reserved word")) | |
| 753 | + | } else if is_predefined(name) { | |
| 754 | + | Some(format!("'{name}' is a predefined SFR or bit name")) | |
| 755 | + | } else { | |
| 756 | + | syms.user | |
| 757 | + | .get(name) | |
| 758 | + | .filter(|_| !redefinable) | |
| 759 | + | .map(|prev| format!("'{name}' already defined on line {}", prev.line + 1)) | |
| 760 | + | }; | |
| 761 | + | match msg { | |
| 762 | + | Some(msg) => diags.push(Diagnostic { line, col, msg }), | |
| 763 | + | None => { | |
| 764 | + | syms.user | |
| 765 | + | .insert(name.to_string(), Symbol { value, kind, line }); | |
| 766 | + | } | |
| 767 | + | } | |
| 768 | + | } | |
| 769 | + | ||
| 770 | + | fn encode_operand( | |
| 771 | + | kind: Operand, | |
| 772 | + | v: i64, | |
| 773 | + | pc: i64, | |
| 774 | + | size: u8, | |
| 775 | + | bytes: &mut Vec<u8>, | |
| 776 | + | ) -> Result<(), String> { | |
| 777 | + | let range = |lo: i64, hi: i64, what: &str| { | |
| 778 | + | if (lo..=hi).contains(&v) { | |
| 779 | + | Ok(()) | |
| 780 | + | } else { | |
| 781 | + | Err(format!("{what} {v} out of range")) | |
| 782 | + | } | |
| 783 | + | }; | |
| 784 | + | match kind { | |
| 785 | + | Operand::Imm8 => { | |
| 786 | + | range(-128, 255, "immediate value")?; | |
| 787 | + | bytes.push(v as u8); | |
| 788 | + | } | |
| 789 | + | Operand::Imm16 => { | |
| 790 | + | range(-32768, 0xFFFF, "immediate value")?; | |
| 791 | + | bytes.extend_from_slice(&(v as u16).to_be_bytes()); | |
| 792 | + | } | |
| 793 | + | Operand::Direct => { | |
| 794 | + | range(0, 255, "direct address")?; | |
| 795 | + | bytes.push(v as u8); | |
| 796 | + | } | |
| 797 | + | Operand::Bit | Operand::NotBit => { | |
| 798 | + | range(0, 255, "bit address")?; | |
| 799 | + | bytes.push(v as u8); | |
| 800 | + | } | |
| 801 | + | Operand::Addr16 => { | |
| 802 | + | range(0, 0xFFFF, "address")?; | |
| 803 | + | bytes.extend_from_slice(&(v as u16).to_be_bytes()); | |
| 804 | + | } | |
| 805 | + | Operand::Addr11 => { | |
| 806 | + | range(0, 0xFFFF, "address")?; | |
| 807 | + | let next = pc + size as i64; | |
| 808 | + | if (next & 0xF800) != (v & 0xF800) { | |
| 809 | + | return Err(format!("target {v:04X}h is outside the current 2K page")); | |
| 810 | + | } | |
| 811 | + | bytes[0] |= (((v >> 8) & 7) as u8) << 5; | |
| 812 | + | bytes.push(v as u8); | |
| 813 | + | } | |
| 814 | + | Operand::Rel => { | |
| 815 | + | let off = v - (pc + size as i64); | |
| 816 | + | if !(-128..=127).contains(&off) { | |
| 817 | + | return Err(format!( | |
| 818 | + | "jump target out of range (offset {off}, must be -128..127)" | |
| 819 | + | )); | |
| 820 | + | } | |
| 821 | + | bytes.push(off as u8); | |
| 822 | + | } | |
| 823 | + | _ => unreachable!("operand {kind:?} carries no value"), | |
| 824 | + | } | |
| 825 | + | Ok(()) | |
| 826 | + | } | |
| 827 | + | ||
| 828 | + | #[cfg(test)] | |
| 829 | + | mod tests { | |
| 830 | + | use super::*; | |
| 831 | + | ||
| 832 | + | fn bytes(src: &str) -> Vec<u8> { | |
| 833 | + | let out = assemble(src); | |
| 834 | + | assert!(out.ok(), "{:?}", out.diagnostics); | |
| 835 | + | out.bytes().map(|(_, b)| b).collect() | |
| 836 | + | } | |
| 837 | + | ||
| 838 | + | fn one(line: &str) -> Vec<u8> { | |
| 839 | + | bytes(line) | |
| 840 | + | } | |
| 841 | + | ||
| 842 | + | #[test] | |
| 843 | + | fn encodes_operand_forms() { | |
| 844 | + | assert_eq!(one("MOV A,#55h"), [0x74, 0x55]); | |
| 845 | + | assert_eq!(one("mov a, r3"), [0xEB]); | |
| 846 | + | assert_eq!(one("MOV A,@R1"), [0xE7]); | |
| 847 | + | assert_eq!(one("MOV A,30h"), [0xE5, 0x30]); | |
| 848 | + | assert_eq!(one("MOV A,B"), [0xE5, 0xF0]); | |
| 849 | + | assert_eq!(one("MOV 30h,#1"), [0x75, 0x30, 0x01]); | |
| 850 | + | assert_eq!(one("MOV 30h,40h"), [0x85, 0x40, 0x30]); | |
| 851 | + | assert_eq!(one("MOV R7,#0"), [0x7F, 0x00]); | |
| 852 | + | assert_eq!(one("MOV @R0,A"), [0xF6]); | |
| 853 | + | assert_eq!(one("MOV DPTR,#1234h"), [0x90, 0x12, 0x34]); | |
| 854 | + | assert_eq!(one("MOV C,P1.3"), [0xA2, 0x93]); | |
| 855 | + | assert_eq!(one("MOV P1.3,C"), [0x92, 0x93]); | |
| 856 | + | assert_eq!(one("SETB TR0"), [0xD2, 0x8C]); | |
| 857 | + | assert_eq!(one("CLR C"), [0xC3]); | |
| 858 | + | assert_eq!(one("ANL C,/ACC.7"), [0xB0, 0xE7]); | |
| 859 | + | assert_eq!(one("CPL 20h.0"), [0xB2, 0x00]); | |
| 860 | + | assert_eq!(one("MOVC A,@A+DPTR"), [0x93]); | |
| 861 | + | assert_eq!(one("MOVX @DPTR,A"), [0xF0]); | |
| 862 | + | assert_eq!(one("JMP @A+DPTR"), [0x73]); | |
| 863 | + | assert_eq!(one("MUL AB"), [0xA4]); | |
| 864 | + | assert_eq!(one("PUSH ACC"), [0xC0, 0xE0]); | |
| 865 | + | assert_eq!(one("ORL 20h,#0Fh"), [0x43, 0x20, 0x0F]); | |
| 866 | + | assert_eq!(one("XCHD A,@R0"), [0xD6]); | |
| 867 | + | assert_eq!(one("MOV A,#-1"), [0x74, 0xFF]); | |
| 868 | + | assert_eq!(one("MOV A,#HIGH(1234h)"), [0x74, 0x12]); | |
| 869 | + | } | |
| 870 | + | ||
| 871 | + | #[test] | |
| 872 | + | fn jumps_and_labels() { | |
| 873 | + | let src = " | |
| 874 | + | start: SJMP start | |
| 875 | + | DJNZ R2, start | |
| 876 | + | CJNE A,#3,fwd | |
| 877 | + | LJMP fwd | |
| 878 | + | CALL fwd | |
| 879 | + | fwd: AJMP start | |
| 880 | + | SJMP $ | |
| 881 | + | "; | |
| 882 | + | assert_eq!( | |
| 883 | + | bytes(src), | |
| 884 | + | [ | |
| 885 | + | 0x80, 0xFE, // SJMP start | |
| 886 | + | 0xDA, 0xFC, // DJNZ R2,start | |
| 887 | + | 0xB4, 0x03, 0x06, // CJNE -> fwd at 0x0D | |
| 888 | + | 0x02, 0x00, 0x0D, // LJMP | |
| 889 | + | 0x12, 0x00, 0x0D, // LCALL | |
| 890 | + | 0x01, 0x00, // AJMP 0 | |
| 891 | + | 0x80, 0xFE, | |
| 892 | + | ] | |
| 893 | + | ); | |
| 894 | + | } | |
| 895 | + | ||
| 896 | + | #[test] | |
| 897 | + | fn directives() { | |
| 898 | + | let src = " | |
| 899 | + | COUNT EQU 3 | |
| 900 | + | LED BIT P1.0 | |
| 901 | + | ORG 100h | |
| 902 | + | MOV R0,#COUNT | |
| 903 | + | SETB LED | |
| 904 | + | tbl: DB 1, 'AB', COUNT*2 | |
| 905 | + | DW tbl | |
| 906 | + | DS 2 | |
| 907 | + | after: NOP | |
| 908 | + | END | |
| 909 | + | garbage that is ignored | |
| 910 | + | "; | |
| 911 | + | let out = assemble(src); | |
| 912 | + | assert!(out.ok(), "{:?}", out.diagnostics); | |
| 913 | + | let v: Vec<_> = out.bytes().collect(); | |
| 914 | + | assert_eq!(v[0], (0x100, 0x78)); | |
| 915 | + | let b: Vec<u8> = v.iter().map(|x| x.1).collect(); | |
| 916 | + | assert_eq!(b, [0x78, 3, 0xD2, 0x90, 1, b'A', b'B', 6, 0x01, 0x04, 0x00]); | |
| 917 | + | assert_eq!(out.symbols["AFTER"].value, 0x10C); | |
| 918 | + | } | |
| 919 | + | ||
| 920 | + | #[test] | |
| 921 | + | fn forward_equ() { | |
| 922 | + | let out = assemble("X EQU Y+1\nY EQU 2\nMOV A,#X"); | |
| 923 | + | assert!(out.ok(), "{:?}", out.diagnostics); | |
| 924 | + | assert_eq!(out.bytes().map(|b| b.1).collect::<Vec<_>>(), [0x74, 3]); | |
| 925 | + | } | |
| 926 | + | ||
| 927 | + | #[test] | |
| 928 | + | fn reports_errors_with_lines() { | |
| 929 | + | let out = assemble("NOP\nMOV A,@R2\nSJMP nowhere\nFOO\nMOV A,#300\n"); | |
| 930 | + | let lines: Vec<_> = out.diagnostics.iter().map(|d| d.line).collect(); | |
| 931 | + | assert_eq!(lines, [1, 2, 3, 4]); | |
| 932 | + | } | |
| 933 | + | ||
| 934 | + | #[test] | |
| 935 | + | fn relative_range_checked() { | |
| 936 | + | let out = assemble("SJMP far\nORG 200h\nfar: NOP"); | |
| 937 | + | assert_eq!(out.diagnostics.len(), 1); | |
| 938 | + | assert!(out.diagnostics[0].msg.contains("out of range")); | |
| 939 | + | } | |
| 940 | + | } | |
| @@ -1,201 +0,0 @@ | |||
| 1 | − | use crate::instructions::Instruction; | |
| 2 | − | use std::collections::HashMap; | |
| 3 | − | ||
| 4 | − | fn tokenize(line: &str) -> Vec<String> { | |
| 5 | − | line.split([' ', ',', '\t']) | |
| 6 | − | .filter(|s| !s.is_empty()) | |
| 7 | − | .map(|s| s.to_lowercase()) | |
| 8 | − | .collect() | |
| 9 | − | } | |
| 10 | − | ||
| 11 | − | // Returns corrensponding u8 value of the register | |
| 12 | − | fn parse_reg(s: &str) -> u8 { | |
| 13 | − | match s { | |
| 14 | − | "a" => 0, | |
| 15 | − | "b" => 1, | |
| 16 | − | "c" => 2, | |
| 17 | − | "d" => 3, | |
| 18 | − | _ => panic!("Unknown register {}", s), | |
| 19 | − | } | |
| 20 | − | } | |
| 21 | − | ||
| 22 | − | fn is_reg(s: &str) -> bool { | |
| 23 | − | matches!(s, "a" | "b" | "c" | "d") | |
| 24 | − | } | |
| 25 | − | ||
| 26 | − | fn instr_size(tokens: &[String]) -> u16 { | |
| 27 | − | match tokens[0].as_str() { | |
| 28 | − | "mov" | "add" | "sub" | "jmp" | "jz" | "jnz" | "cmp" | "mul" | "div" | "call" => 3, | |
| 29 | − | "sys" => 2, | |
| 30 | − | "hlt" | "ret" => 1, | |
| 31 | − | _ => panic!("Unknown instruction {}", tokens[0]), | |
| 32 | − | } | |
| 33 | − | } | |
| 34 | − | ||
| 35 | − | fn first_pass(source: &str) -> HashMap<String, u16> { | |
| 36 | − | let mut symbols = HashMap::new(); | |
| 37 | − | let mut addr: u16 = 0; | |
| 38 | − | ||
| 39 | − | for line in source.lines() { | |
| 40 | − | let line = line.trim(); | |
| 41 | − | ||
| 42 | − | // Ignoring comments and empty lines | |
| 43 | − | if line.is_empty() || line.starts_with(";") { | |
| 44 | − | continue; | |
| 45 | − | } | |
| 46 | − | ||
| 47 | − | // Labels (ends with ":") | |
| 48 | − | if line.ends_with(":") { | |
| 49 | − | let label = line.trim_end_matches(":").to_string(); | |
| 50 | − | symbols.insert(label, addr); | |
| 51 | − | continue; | |
| 52 | − | } | |
| 53 | − | ||
| 54 | − | let tokens = tokenize(line); | |
| 55 | − | addr += instr_size(&tokens); | |
| 56 | − | } | |
| 57 | − | ||
| 58 | − | symbols | |
| 59 | − | } | |
| 60 | − | ||
| 61 | − | pub fn assembler(source: &str) -> Vec<u8> { | |
| 62 | − | let symbols = first_pass(source); | |
| 63 | − | let mut bytes = Vec::new(); | |
| 64 | − | ||
| 65 | − | for (line_no, line) in source.lines().enumerate() { | |
| 66 | − | let line = line.trim(); | |
| 67 | − | ||
| 68 | − | // Comments in assembly start with ";" | |
| 69 | − | if line.is_empty() || line.starts_with(';') || line.ends_with(":") { | |
| 70 | − | continue; | |
| 71 | − | } | |
| 72 | − | ||
| 73 | − | let tokens = tokenize(line); | |
| 74 | − | ||
| 75 | − | match tokens[0].as_str() { | |
| 76 | − | "mov" => { | |
| 77 | − | // mov reg, imm | |
| 78 | − | let r1 = parse_reg(&tokens[1]); | |
| 79 | − | if is_reg(&tokens[2]) { | |
| 80 | − | let r2 = parse_reg(&tokens[2]); | |
| 81 | − | bytes.push(Instruction::MOV_RR as u8); | |
| 82 | − | bytes.push(r1); | |
| 83 | − | bytes.push(r2); | |
| 84 | − | } else { | |
| 85 | − | let imm: u8 = tokens[2].parse().unwrap(); | |
| 86 | − | bytes.push(Instruction::MOV_RI as u8); | |
| 87 | − | bytes.push(r1); | |
| 88 | − | bytes.push(imm); | |
| 89 | − | } | |
| 90 | − | } | |
| 91 | − | ||
| 92 | − | "add" => { | |
| 93 | − | // add a, b | |
| 94 | − | let r1 = parse_reg(&tokens[1]); | |
| 95 | − | ||
| 96 | − | if is_reg(&tokens[2]) { | |
| 97 | − | let r2 = parse_reg(&tokens[2]); | |
| 98 | − | bytes.push(Instruction::ADD_RR as u8); | |
| 99 | − | bytes.push(r1); | |
| 100 | − | bytes.push(r2); | |
| 101 | − | } else { | |
| 102 | − | let imm: u8 = tokens[2].parse().unwrap(); | |
| 103 | − | bytes.push(Instruction::ADD_RI as u8); | |
| 104 | − | bytes.push(r1); | |
| 105 | − | bytes.push(imm); | |
| 106 | − | } | |
| 107 | − | } | |
| 108 | − | ||
| 109 | − | "sub" => { | |
| 110 | − | // sub a, b | |
| 111 | − | let r1 = parse_reg(&tokens[1]); | |
| 112 | − | if is_reg(&tokens[2]) { | |
| 113 | − | let r2 = parse_reg(&tokens[2]); | |
| 114 | − | bytes.push(Instruction::SUB_RR as u8); | |
| 115 | − | bytes.push(r1); | |
| 116 | − | bytes.push(r2); | |
| 117 | − | } else { | |
| 118 | − | let imm: u8 = tokens[2].parse().unwrap(); | |
| 119 | − | bytes.push(Instruction::SUB_RI as u8); | |
| 120 | − | bytes.push(r1); | |
| 121 | − | bytes.push(imm); | |
| 122 | − | } | |
| 123 | − | } | |
| 124 | − | ||
| 125 | − | "jmp" | "jz" | "jnz" => { | |
| 126 | − | let opcode = match tokens[0].as_str() { | |
| 127 | − | "jmp" => Instruction::JMP, | |
| 128 | − | "jz" => Instruction::JZ, | |
| 129 | − | "jnz" => Instruction::JNZ, | |
| 130 | − | _ => unreachable!(), | |
| 131 | − | }; | |
| 132 | − | ||
| 133 | − | let label = &tokens[1]; | |
| 134 | − | let addr = *symbols.get(label).expect("Uknown label"); | |
| 135 | − | ||
| 136 | − | bytes.push(opcode as u8); | |
| 137 | − | bytes.push((addr & 0xFF) as u8); // low | |
| 138 | − | bytes.push((addr >> 8) as u8); // high | |
| 139 | − | } | |
| 140 | − | ||
| 141 | − | "cmp" => { | |
| 142 | − | let r1 = parse_reg(&tokens[1]); | |
| 143 | − | if tokens[2].chars().all(|c| c.is_ascii_digit()) { | |
| 144 | − | let imm: u8 = tokens[2].parse().unwrap(); | |
| 145 | − | bytes.push(Instruction::CMP_RI as u8); | |
| 146 | − | bytes.push(r1); | |
| 147 | − | bytes.push(imm); | |
| 148 | − | } else { | |
| 149 | − | let r2 = parse_reg(&tokens[2]); | |
| 150 | − | bytes.push(Instruction::CMP_RR as u8); | |
| 151 | − | bytes.push(r1); | |
| 152 | − | bytes.push(r2); | |
| 153 | − | } | |
| 154 | − | } | |
| 155 | − | ||
| 156 | − | "mul" => { | |
| 157 | − | let r1 = parse_reg(&tokens[1]); | |
| 158 | − | let r2 = parse_reg(&tokens[2]); | |
| 159 | − | ||
| 160 | − | bytes.push(Instruction::MUL as u8); | |
| 161 | − | bytes.push(r1); | |
| 162 | − | bytes.push(r2); | |
| 163 | − | } | |
| 164 | − | ||
| 165 | − | "div" => { | |
| 166 | − | let r1 = parse_reg(&tokens[1]); | |
| 167 | − | let r2 = parse_reg(&tokens[2]); | |
| 168 | − | ||
| 169 | − | bytes.push(Instruction::DIV as u8); | |
| 170 | − | bytes.push(r1); | |
| 171 | − | bytes.push(r2); | |
| 172 | − | } | |
| 173 | − | ||
| 174 | − | "call" => { | |
| 175 | − | let addr = *symbols.get(&tokens[1]).expect("Unknown label"); | |
| 176 | − | ||
| 177 | − | bytes.push(Instruction::CALL as u8); | |
| 178 | − | bytes.push((addr & 0xFF) as u8); // low | |
| 179 | − | bytes.push((addr >> 8) as u8); // high | |
| 180 | − | } | |
| 181 | − | ||
| 182 | − | "ret" => { | |
| 183 | − | bytes.push(Instruction::RET as u8); | |
| 184 | − | } | |
| 185 | − | ||
| 186 | − | "hlt" => { | |
| 187 | − | bytes.push(Instruction::HLT as u8); | |
| 188 | − | } | |
| 189 | − | ||
| 190 | − | "sys" => { | |
| 191 | − | let imm = tokens[1].parse().unwrap(); | |
| 192 | − | bytes.push(Instruction::SYS as u8); | |
| 193 | − | bytes.push(imm); | |
| 194 | − | } | |
| 195 | − | ||
| 196 | − | _ => panic!("Line {}: unknown instruction", line_no + 1), | |
| 197 | − | } | |
| 198 | − | } | |
| 199 | − | ||
| 200 | − | bytes | |
| 201 | − | } | |
| @@ -1,424 +0,0 @@ | |||
| 1 | − | use crate::instructions::Instruction; | |
| 2 | − | use crate::memory::Memory; | |
| 3 | − | ||
| 4 | − | #[derive(Default, Debug)] | |
| 5 | − | #[allow(dead_code)] | |
| 6 | − | pub struct CPU { | |
| 7 | − | pub a: u8, | |
| 8 | − | pub b: u8, | |
| 9 | − | pub c: u8, | |
| 10 | − | pub d: u8, | |
| 11 | − | ||
| 12 | − | pub pc: u16, | |
| 13 | − | pub sp: u16, | |
| 14 | − | ||
| 15 | − | pub zero: bool, | |
| 16 | − | pub carry: bool, | |
| 17 | − | ||
| 18 | − | pub halted: bool, | |
| 19 | − | } | |
| 20 | − | ||
| 21 | − | impl CPU { | |
| 22 | − | fn push16(&mut self, mem: &mut Memory, val: u16) { | |
| 23 | − | self.dec_sp(); | |
| 24 | − | mem.write(self.sp, (val >> 8) as u8); // high | |
| 25 | − | self.dec_sp(); | |
| 26 | − | mem.write(self.sp, (val & 0xFF) as u8); // low | |
| 27 | − | } | |
| 28 | − | ||
| 29 | − | fn pop16(&mut self, mem: &mut Memory) -> u16 { | |
| 30 | − | let low = mem.read(self.sp) as u16; | |
| 31 | − | self.inc_sp(); | |
| 32 | − | let high = mem.read(self.sp) as u16; | |
| 33 | − | self.inc_sp(); | |
| 34 | − | ||
| 35 | − | (high << 8) | low | |
| 36 | − | } | |
| 37 | − | ||
| 38 | − | pub fn debug_instr(&self, mem: &Memory) { | |
| 39 | − | let opcode = mem.read(self.pc); | |
| 40 | − | ||
| 41 | − | println!( | |
| 42 | − | "PC={:04X} {:<3} | A={:02X} B={:02X} C={:02X} D={:02X} | Z={} C={}", | |
| 43 | − | self.pc, | |
| 44 | − | Instruction::opcode_name(opcode), | |
| 45 | − | self.a, | |
| 46 | − | self.b, | |
| 47 | − | self.c, | |
| 48 | − | self.d, | |
| 49 | − | self.zero as u8, | |
| 50 | − | self.carry as u8 | |
| 51 | − | ); | |
| 52 | − | } | |
| 53 | − | ||
| 54 | − | pub fn step(&mut self, mem: &mut Memory) { | |
| 55 | − | let opcode = mem.read(self.pc); | |
| 56 | − | self.inc_pc(); | |
| 57 | − | ||
| 58 | − | match opcode { | |
| 59 | − | x if x == Instruction::MOV_RI as u8 => self.mov_ri(mem), | |
| 60 | − | x if x == Instruction::MOV_RR as u8 => self.mov_rr(mem), | |
| 61 | − | x if x == Instruction::ADD_RR as u8 => self.add_rr(mem), | |
| 62 | − | x if x == Instruction::ADD_RI as u8 => self.add_ri(mem), | |
| 63 | − | x if x == Instruction::SUB_RR as u8 => self.sub_rr(mem), | |
| 64 | − | x if x == Instruction::SUB_RI as u8 => self.sub_ri(mem), | |
| 65 | − | x if x == Instruction::JMP as u8 => self.jmp(mem), | |
| 66 | − | x if x == Instruction::JZ as u8 => self.jz(mem), | |
| 67 | − | x if x == Instruction::JNZ as u8 => self.jnz(mem), | |
| 68 | − | x if x == Instruction::CMP_RI as u8 => self.cmp_ri(mem), | |
| 69 | − | x if x == Instruction::CMP_RR as u8 => self.cmp_rr(mem), | |
| 70 | − | x if x == Instruction::MUL as u8 => self.mul(mem), | |
| 71 | − | x if x == Instruction::DIV as u8 => self.div(mem), | |
| 72 | − | x if x == Instruction::CALL as u8 => self.call(mem), | |
| 73 | − | x if x == Instruction::RET as u8 => self.ret(mem), | |
| 74 | − | x if x == Instruction::SYS as u8 => self.syscall(mem), | |
| 75 | − | x if x == Instruction::HLT as u8 => self.halt(), | |
| 76 | − | _ => panic!("Unknown opcode {:02X}", opcode), | |
| 77 | − | } | |
| 78 | − | } | |
| 79 | − | ||
| 80 | − | pub fn inc_pc(&mut self) { | |
| 81 | − | self.pc += 1; | |
| 82 | − | } | |
| 83 | − | ||
| 84 | − | pub fn inc_sp(&mut self) { | |
| 85 | − | self.sp = self.sp.wrapping_add(1); | |
| 86 | − | } | |
| 87 | − | ||
| 88 | − | pub fn dec_sp(&mut self) { | |
| 89 | − | self.sp = self.sp.wrapping_sub(1); | |
| 90 | − | } | |
| 91 | − | ||
| 92 | − | pub fn halt(&mut self) { | |
| 93 | − | self.halted = true; | |
| 94 | − | } | |
| 95 | − | ||
| 96 | − | pub fn mov_ri(&mut self, mem: &Memory) { | |
| 97 | − | let reg = mem.read(self.pc); | |
| 98 | − | self.inc_pc(); | |
| 99 | − | let val = mem.read(self.pc); | |
| 100 | − | self.inc_pc(); | |
| 101 | − | ||
| 102 | − | match reg { | |
| 103 | − | 0 => self.a = val, | |
| 104 | − | 1 => self.b = val, | |
| 105 | − | 2 => self.c = val, | |
| 106 | − | 3 => self.d = val, | |
| 107 | − | _ => {} | |
| 108 | − | } | |
| 109 | − | ||
| 110 | − | self.zero = val == 0; | |
| 111 | − | } | |
| 112 | − | ||
| 113 | − | pub fn mov_rr(&mut self, mem: &Memory) { | |
| 114 | − | let dest = mem.read(self.pc); | |
| 115 | − | self.inc_pc(); | |
| 116 | − | let src = mem.read(self.pc); | |
| 117 | − | self.inc_pc(); | |
| 118 | − | ||
| 119 | − | let val = self.get_reg(src); | |
| 120 | − | self.set_reg(dest, val); | |
| 121 | − | ||
| 122 | − | self.zero = val == 0; | |
| 123 | − | } | |
| 124 | − | ||
| 125 | − | pub fn add_rr(&mut self, mem: &Memory) { | |
| 126 | − | let dest = mem.read(self.pc); | |
| 127 | − | self.pc += 1; | |
| 128 | − | let src = mem.read(self.pc); | |
| 129 | − | self.pc += 1; | |
| 130 | − | ||
| 131 | − | let (result, carry) = match (dest, src) { | |
| 132 | − | (0, 0) => self.a.overflowing_add(self.a), | |
| 133 | − | (0, 1) => self.a.overflowing_add(self.b), | |
| 134 | − | (0, 2) => self.a.overflowing_add(self.c), | |
| 135 | − | (0, 3) => self.a.overflowing_add(self.d), | |
| 136 | − | ||
| 137 | − | (1, 0) => self.b.overflowing_add(self.a), | |
| 138 | − | (1, 1) => self.b.overflowing_add(self.b), | |
| 139 | − | (1, 2) => self.b.overflowing_add(self.c), | |
| 140 | − | (1, 3) => self.b.overflowing_add(self.d), | |
| 141 | − | ||
| 142 | − | (2, 0) => self.c.overflowing_add(self.a), | |
| 143 | − | (2, 1) => self.c.overflowing_add(self.b), | |
| 144 | − | (2, 2) => self.c.overflowing_add(self.c), | |
| 145 | − | (2, 3) => self.c.overflowing_add(self.d), | |
| 146 | − | ||
| 147 | − | (3, 0) => self.d.overflowing_add(self.a), | |
| 148 | − | (3, 1) => self.d.overflowing_add(self.b), | |
| 149 | − | (3, 2) => self.d.overflowing_add(self.c), | |
| 150 | − | (3, 3) => self.d.overflowing_add(self.d), | |
| 151 | − | ||
| 152 | − | _ => (0, false), | |
| 153 | − | }; | |
| 154 | − | ||
| 155 | − | match dest { | |
| 156 | − | 0 => self.a = result, | |
| 157 | − | 1 => self.b = result, | |
| 158 | − | 2 => self.c = result, | |
| 159 | − | 3 => self.d = result, | |
| 160 | − | _ => {} | |
| 161 | − | } | |
| 162 | − | ||
| 163 | − | self.zero = result == 0; | |
| 164 | − | self.carry = carry; | |
| 165 | − | } | |
| 166 | − | pub fn add_ri(&mut self, mem: &Memory) { | |
| 167 | − | let dest = mem.read(self.pc); | |
| 168 | − | self.pc += 1; | |
| 169 | − | let imm = mem.read(self.pc); | |
| 170 | − | self.pc += 1; | |
| 171 | − | ||
| 172 | − | let (result, carry) = match dest { | |
| 173 | − | 0 => self.a.overflowing_add(imm), | |
| 174 | − | 1 => self.b.overflowing_add(imm), | |
| 175 | − | 2 => self.c.overflowing_add(imm), | |
| 176 | − | 3 => self.d.overflowing_add(imm), | |
| 177 | − | _ => (0, false), | |
| 178 | − | }; | |
| 179 | − | ||
| 180 | − | match dest { | |
| 181 | − | 0 => self.a = result, | |
| 182 | − | 1 => self.b = result, | |
| 183 | − | 2 => self.c = result, | |
| 184 | − | 3 => self.d = result, | |
| 185 | − | _ => {} | |
| 186 | − | } | |
| 187 | − | ||
| 188 | − | self.zero = result == 0; | |
| 189 | − | self.carry = carry; | |
| 190 | − | } | |
| 191 | − | ||
| 192 | − | pub fn sub_rr(&mut self, mem: &Memory) { | |
| 193 | − | let dest = mem.read(self.pc); | |
| 194 | − | self.pc += 1; | |
| 195 | − | let src = mem.read(self.pc); | |
| 196 | − | self.pc += 1; | |
| 197 | − | ||
| 198 | − | let (result, borrow) = match (dest, src) { | |
| 199 | − | (0, 0) => self.a.overflowing_sub(self.a), | |
| 200 | − | (0, 1) => self.a.overflowing_sub(self.b), | |
| 201 | − | (0, 2) => self.a.overflowing_sub(self.c), | |
| 202 | − | (0, 3) => self.a.overflowing_sub(self.d), | |
| 203 | − | ||
| 204 | − | (1, 0) => self.b.overflowing_sub(self.a), | |
| 205 | − | (1, 1) => self.b.overflowing_sub(self.b), | |
| 206 | − | (1, 2) => self.b.overflowing_sub(self.c), | |
| 207 | − | (1, 3) => self.b.overflowing_sub(self.d), | |
| 208 | − | ||
| 209 | − | (2, 0) => self.c.overflowing_sub(self.a), | |
| 210 | − | (2, 1) => self.c.overflowing_sub(self.b), | |
| 211 | − | (2, 2) => self.c.overflowing_sub(self.c), | |
| 212 | − | (2, 3) => self.c.overflowing_sub(self.d), | |
| 213 | − | ||
| 214 | − | (3, 0) => self.d.overflowing_sub(self.a), | |
| 215 | − | (3, 1) => self.d.overflowing_sub(self.b), | |
| 216 | − | (3, 2) => self.d.overflowing_sub(self.c), | |
| 217 | − | (3, 3) => self.d.overflowing_sub(self.d), | |
| 218 | − | ||
| 219 | − | _ => (0, false), | |
| 220 | − | }; | |
| 221 | − | ||
| 222 | − | match dest { | |
| 223 | − | 0 => self.a = result, | |
| 224 | − | 1 => self.b = result, | |
| 225 | − | 2 => self.c = result, | |
| 226 | − | 3 => self.d = result, | |
| 227 | − | _ => {} | |
| 228 | − | } | |
| 229 | − | ||
| 230 | − | self.zero = result == 0; | |
| 231 | − | self.carry = borrow; | |
| 232 | − | } | |
| 233 | − | ||
| 234 | − | pub fn sub_ri(&mut self, mem: &Memory) { | |
| 235 | − | let dest = mem.read(self.pc); | |
| 236 | − | self.pc += 1; | |
| 237 | − | let imm = mem.read(self.pc); | |
| 238 | − | self.pc += 1; | |
| 239 | − | ||
| 240 | − | let (result, borrow) = match dest { | |
| 241 | − | 0 => self.a.overflowing_sub(imm), | |
| 242 | − | 1 => self.b.overflowing_sub(imm), | |
| 243 | − | 2 => self.c.overflowing_sub(imm), | |
| 244 | − | 3 => self.d.overflowing_sub(imm), | |
| 245 | − | _ => (0, false), | |
| 246 | − | }; | |
| 247 | − | ||
| 248 | − | match dest { | |
| 249 | − | 0 => self.a = result, | |
| 250 | − | 1 => self.b = result, | |
| 251 | − | 2 => self.c = result, | |
| 252 | − | 3 => self.d = result, | |
| 253 | − | _ => {} | |
| 254 | − | } | |
| 255 | − | ||
| 256 | − | self.zero = result == 0; | |
| 257 | − | self.carry = borrow; | |
| 258 | − | } | |
| 259 | − | ||
| 260 | − | pub fn jmp(&mut self, mem: &Memory) { | |
| 261 | − | let low = mem.read(self.pc) as u16; | |
| 262 | − | self.inc_pc(); | |
| 263 | − | let high = mem.read(self.pc) as u16; | |
| 264 | − | self.inc_pc(); | |
| 265 | − | ||
| 266 | − | let addrs = (high << 8) | low; | |
| 267 | − | ||
| 268 | − | self.pc = addrs; | |
| 269 | − | } | |
| 270 | − | ||
| 271 | − | pub fn jz(&mut self, mem: &Memory) { | |
| 272 | − | let low = mem.read(self.pc) as u16; | |
| 273 | − | self.inc_pc(); | |
| 274 | − | let high = mem.read(self.pc) as u16; | |
| 275 | − | self.inc_pc(); | |
| 276 | − | ||
| 277 | − | let addrs = (high << 8) | low; | |
| 278 | − | ||
| 279 | − | if self.zero { | |
| 280 | − | self.pc = addrs; | |
| 281 | − | } | |
| 282 | − | } | |
| 283 | − | ||
| 284 | − | pub fn jnz(&mut self, mem: &Memory) { | |
| 285 | − | let low = mem.read(self.pc) as u16; | |
| 286 | − | self.inc_pc(); | |
| 287 | − | let high = mem.read(self.pc) as u16; | |
| 288 | − | self.inc_pc(); | |
| 289 | − | ||
| 290 | − | let addrs = (high << 8) | low; | |
| 291 | − | ||
| 292 | − | if !self.zero { | |
| 293 | − | self.pc = addrs; | |
| 294 | − | } | |
| 295 | − | } | |
| 296 | − | ||
| 297 | − | pub fn cmp_rr(&mut self, mem: &Memory) { | |
| 298 | − | let r1 = mem.read(self.pc); | |
| 299 | − | self.inc_pc(); | |
| 300 | − | let r2 = mem.read(self.pc); | |
| 301 | − | self.inc_pc(); | |
| 302 | − | ||
| 303 | − | let v1 = self.get_reg(r1); | |
| 304 | − | let v2 = self.get_reg(r2); | |
| 305 | − | ||
| 306 | − | let (result, borrow) = v1.overflowing_sub(v2); | |
| 307 | − | ||
| 308 | − | self.zero = result == 0; | |
| 309 | − | self.carry = borrow; | |
| 310 | − | } | |
| 311 | − | ||
| 312 | − | pub fn cmp_ri(&mut self, mem: &Memory) { | |
| 313 | − | let r1 = mem.read(self.pc); | |
| 314 | − | self.inc_pc(); | |
| 315 | − | let r2 = mem.read(self.pc); | |
| 316 | − | self.inc_pc(); | |
| 317 | − | ||
| 318 | − | let v1 = self.get_reg(r1); | |
| 319 | − | ||
| 320 | − | let (result, borrow) = v1.overflowing_sub(r2); | |
| 321 | − | ||
| 322 | − | self.zero = result == 0; | |
| 323 | − | self.carry = borrow; | |
| 324 | − | } | |
| 325 | − | ||
| 326 | − | pub fn mul(&mut self, mem: &Memory) { | |
| 327 | − | let dest = mem.read(self.pc); | |
| 328 | − | self.inc_pc(); | |
| 329 | − | let src = mem.read(self.pc); | |
| 330 | − | self.inc_pc(); | |
| 331 | − | ||
| 332 | − | let lhs = self.get_reg(dest); | |
| 333 | − | let rhs = self.get_reg(src); | |
| 334 | − | ||
| 335 | − | let result16 = (lhs as u16) * (rhs as u16); | |
| 336 | − | let result8 = (result16 & 0xFF) as u8; | |
| 337 | − | ||
| 338 | − | self.set_reg(dest, result8); | |
| 339 | − | ||
| 340 | − | self.zero = result8 == 0; | |
| 341 | − | self.carry = result16 > 0xFF; | |
| 342 | − | } | |
| 343 | − | ||
| 344 | − | pub fn div(&mut self, mem: &Memory) { | |
| 345 | − | let dest = mem.read(self.pc); | |
| 346 | − | self.inc_pc(); | |
| 347 | − | let src = mem.read(self.pc); | |
| 348 | − | self.inc_pc(); | |
| 349 | − | ||
| 350 | − | let lhs = self.get_reg(dest); | |
| 351 | − | let rhs = self.get_reg(src); | |
| 352 | − | ||
| 353 | − | if rhs == 0 { | |
| 354 | − | panic!("Division by zero"); | |
| 355 | − | // OR: set halted = true | |
| 356 | − | // OR: define a syscall/exception later | |
| 357 | − | } | |
| 358 | − | ||
| 359 | − | let result = lhs / rhs; | |
| 360 | − | ||
| 361 | − | self.set_reg(dest, result); | |
| 362 | − | ||
| 363 | − | self.zero = result == 0; | |
| 364 | − | // carry unchanged | |
| 365 | − | } | |
| 366 | − | ||
| 367 | − | pub fn call(&mut self, mem: &mut Memory) { | |
| 368 | − | let low = mem.read(self.pc) as u16; | |
| 369 | − | self.inc_pc(); | |
| 370 | − | let high = mem.read(self.pc) as u16; | |
| 371 | − | self.inc_pc(); | |
| 372 | − | let addr = (high << 8) | low; | |
| 373 | − | ||
| 374 | − | let return_addr = self.pc; | |
| 375 | − | ||
| 376 | − | self.push16(mem, return_addr); | |
| 377 | − | self.pc = addr; | |
| 378 | − | } | |
| 379 | − | ||
| 380 | − | pub fn ret(&mut self, mem: &mut Memory) { | |
| 381 | − | let addr = self.pop16(mem); | |
| 382 | − | self.pc = addr; | |
| 383 | − | } | |
| 384 | − | ||
| 385 | − | pub fn syscall(&mut self, mem: &Memory) { | |
| 386 | − | let num = mem.read(self.pc); | |
| 387 | − | self.pc = self.pc.wrapping_add(1); | |
| 388 | − | match num { | |
| 389 | − | 0 => { | |
| 390 | − | // exit | |
| 391 | − | self.halted = true; | |
| 392 | − | } | |
| 393 | − | 1 => { | |
| 394 | − | // print A as number | |
| 395 | − | println!("{}", self.a); | |
| 396 | − | } | |
| 397 | − | 2 => { | |
| 398 | − | // print A as ASCII char | |
| 399 | − | print!("{}", self.a as char); | |
| 400 | − | } | |
| 401 | − | _ => panic!("Unknown syscall {}", num), | |
| 402 | − | } | |
| 403 | − | } | |
| 404 | − | ||
| 405 | − | fn get_reg(&self, r: u8) -> u8 { | |
| 406 | − | match r { | |
| 407 | − | 0 => self.a, | |
| 408 | − | 1 => self.b, | |
| 409 | − | 2 => self.c, | |
| 410 | − | 3 => self.d, | |
| 411 | − | _ => panic!("Invalid Register"), | |
| 412 | − | } | |
| 413 | − | } | |
| 414 | − | ||
| 415 | − | fn set_reg(&mut self, dest: u8, val: u8) { | |
| 416 | − | match dest { | |
| 417 | − | 0 => self.a = val, | |
| 418 | − | 1 => self.b = val, | |
| 419 | − | 2 => self.c = val, | |
| 420 | − | 3 => self.d = val, | |
| 421 | − | _ => panic!("Invalid register"), | |
| 422 | − | } | |
| 423 | − | } | |
| 424 | − | } | |
| @@ -1,43 +0,0 @@ | |||
| 1 | − | #[repr(u8)] | |
| 2 | − | #[allow(non_camel_case_types)] | |
| 3 | − | pub enum Instruction { | |
| 4 | − | MOV_RI = 0x01, | |
| 5 | − | MOV_RR = 0x08, | |
| 6 | − | ADD_RR = 0x02, | |
| 7 | − | ADD_RI = 0x0A, | |
| 8 | − | SUB_RR = 0x03, | |
| 9 | − | SUB_RI = 0x0B, | |
| 10 | − | JMP = 0x04, | |
| 11 | − | JZ = 0x05, | |
| 12 | − | JNZ = 0x06, | |
| 13 | − | CMP_RI = 0x07, | |
| 14 | − | CMP_RR = 0x09, | |
| 15 | − | MUL = 0x0C, | |
| 16 | − | DIV = 0x0D, | |
| 17 | − | CALL = 0x0E, | |
| 18 | − | RET = 0x0F, | |
| 19 | − | SYS = 0x10, | |
| 20 | − | HLT = 0xFF, | |
| 21 | − | } | |
| 22 | − | ||
| 23 | − | impl Instruction { | |
| 24 | − | pub fn opcode_name(op: u8) -> &'static str { | |
| 25 | − | match op { | |
| 26 | − | 0x01 | 0x08 => "MOV", | |
| 27 | − | 0x02 | 0x0A => "ADD", | |
| 28 | − | 0x03 | 0x0B => "SUB", | |
| 29 | − | 0x04 => "JMP", | |
| 30 | − | 0x05 => "JZ", | |
| 31 | − | 0x06 => "JNZ", | |
| 32 | − | 0x07 | 0x09 => "CMP", | |
| 33 | − | 0x0C => "MUL", | |
| 34 | − | 0x0D => "DIV", | |
| 35 | − | 0x0E => "CALL", | |
| 36 | − | 0x0F => "RET", | |
| 37 | − | 0x10 => "SYS", | |
| 38 | − | 0xFF => "HLT", | |
| 39 | − | ||
| 40 | − | _ => "???", | |
| 41 | − | } | |
| 42 | − | } | |
| 43 | − | } | |
| @@ -0,0 +1,347 @@ | |||
| 1 | + | //! The 8051 instruction set as a single opcode table, shared by the | |
| 2 | + | //! assembler (pattern -> opcode), the disassembler and the simulator | |
| 3 | + | //! (sizes and machine-cycle counts). | |
| 4 | + | ||
| 5 | + | use std::sync::OnceLock; | |
| 6 | + | ||
| 7 | + | /// Operand forms as they appear in an instruction encoding. | |
| 8 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 9 | + | pub enum Operand { | |
| 10 | + | A, | |
| 11 | + | AB, | |
| 12 | + | C, | |
| 13 | + | Dptr, | |
| 14 | + | Reg(u8), | |
| 15 | + | AtReg(u8), | |
| 16 | + | AtDptr, | |
| 17 | + | AtAPlusDptr, | |
| 18 | + | AtAPlusPc, | |
| 19 | + | Imm8, | |
| 20 | + | Imm16, | |
| 21 | + | Direct, | |
| 22 | + | Bit, | |
| 23 | + | NotBit, | |
| 24 | + | Addr11, | |
| 25 | + | Addr16, | |
| 26 | + | Rel, | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | impl Operand { | |
| 30 | + | /// Bytes this operand occupies after the opcode byte. | |
| 31 | + | pub fn len(self) -> u8 { | |
| 32 | + | match self { | |
| 33 | + | Operand::Imm8 | Operand::Direct | Operand::Bit | Operand::NotBit => 1, | |
| 34 | + | Operand::Addr11 | Operand::Rel => 1, | |
| 35 | + | Operand::Imm16 | Operand::Addr16 => 2, | |
| 36 | + | _ => 0, | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | #[derive(Debug, Clone, Copy)] | |
| 42 | + | pub struct OpInfo { | |
| 43 | + | pub opcode: u8, | |
| 44 | + | pub mnemonic: &'static str, | |
| 45 | + | pub operands: &'static [Operand], | |
| 46 | + | pub size: u8, | |
| 47 | + | pub cycles: u8, | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | use Operand::*; | |
| 51 | + | ||
| 52 | + | const RN: [[Operand; 1]; 8] = [ | |
| 53 | + | [Reg(0)], | |
| 54 | + | [Reg(1)], | |
| 55 | + | [Reg(2)], | |
| 56 | + | [Reg(3)], | |
| 57 | + | [Reg(4)], | |
| 58 | + | [Reg(5)], | |
| 59 | + | [Reg(6)], | |
| 60 | + | [Reg(7)], | |
| 61 | + | ]; | |
| 62 | + | const A_RN: [[Operand; 2]; 8] = [ | |
| 63 | + | [A, Reg(0)], | |
| 64 | + | [A, Reg(1)], | |
| 65 | + | [A, Reg(2)], | |
| 66 | + | [A, Reg(3)], | |
| 67 | + | [A, Reg(4)], | |
| 68 | + | [A, Reg(5)], | |
| 69 | + | [A, Reg(6)], | |
| 70 | + | [A, Reg(7)], | |
| 71 | + | ]; | |
| 72 | + | const RN_A: [[Operand; 2]; 8] = [ | |
| 73 | + | [Reg(0), A], | |
| 74 | + | [Reg(1), A], | |
| 75 | + | [Reg(2), A], | |
| 76 | + | [Reg(3), A], | |
| 77 | + | [Reg(4), A], | |
| 78 | + | [Reg(5), A], | |
| 79 | + | [Reg(6), A], | |
| 80 | + | [Reg(7), A], | |
| 81 | + | ]; | |
| 82 | + | const RN_IMM: [[Operand; 2]; 8] = [ | |
| 83 | + | [Reg(0), Imm8], | |
| 84 | + | [Reg(1), Imm8], | |
| 85 | + | [Reg(2), Imm8], | |
| 86 | + | [Reg(3), Imm8], | |
| 87 | + | [Reg(4), Imm8], | |
| 88 | + | [Reg(5), Imm8], | |
| 89 | + | [Reg(6), Imm8], | |
| 90 | + | [Reg(7), Imm8], | |
| 91 | + | ]; | |
| 92 | + | const DIR_RN: [[Operand; 2]; 8] = [ | |
| 93 | + | [Direct, Reg(0)], | |
| 94 | + | [Direct, Reg(1)], | |
| 95 | + | [Direct, Reg(2)], | |
| 96 | + | [Direct, Reg(3)], | |
| 97 | + | [Direct, Reg(4)], | |
| 98 | + | [Direct, Reg(5)], | |
| 99 | + | [Direct, Reg(6)], | |
| 100 | + | [Direct, Reg(7)], | |
| 101 | + | ]; | |
| 102 | + | const RN_DIR: [[Operand; 2]; 8] = [ | |
| 103 | + | [Reg(0), Direct], | |
| 104 | + | [Reg(1), Direct], | |
| 105 | + | [Reg(2), Direct], | |
| 106 | + | [Reg(3), Direct], | |
| 107 | + | [Reg(4), Direct], | |
| 108 | + | [Reg(5), Direct], | |
| 109 | + | [Reg(6), Direct], | |
| 110 | + | [Reg(7), Direct], | |
| 111 | + | ]; | |
| 112 | + | const RN_REL: [[Operand; 2]; 8] = [ | |
| 113 | + | [Reg(0), Rel], | |
| 114 | + | [Reg(1), Rel], | |
| 115 | + | [Reg(2), Rel], | |
| 116 | + | [Reg(3), Rel], | |
| 117 | + | [Reg(4), Rel], | |
| 118 | + | [Reg(5), Rel], | |
| 119 | + | [Reg(6), Rel], | |
| 120 | + | [Reg(7), Rel], | |
| 121 | + | ]; | |
| 122 | + | const RN_IMM_REL: [[Operand; 3]; 8] = [ | |
| 123 | + | [Reg(0), Imm8, Rel], | |
| 124 | + | [Reg(1), Imm8, Rel], | |
| 125 | + | [Reg(2), Imm8, Rel], | |
| 126 | + | [Reg(3), Imm8, Rel], | |
| 127 | + | [Reg(4), Imm8, Rel], | |
| 128 | + | [Reg(5), Imm8, Rel], | |
| 129 | + | [Reg(6), Imm8, Rel], | |
| 130 | + | [Reg(7), Imm8, Rel], | |
| 131 | + | ]; | |
| 132 | + | const RI: [[Operand; 1]; 2] = [[AtReg(0)], [AtReg(1)]]; | |
| 133 | + | const A_RI: [[Operand; 2]; 2] = [[A, AtReg(0)], [A, AtReg(1)]]; | |
| 134 | + | const RI_A: [[Operand; 2]; 2] = [[AtReg(0), A], [AtReg(1), A]]; | |
| 135 | + | const RI_IMM: [[Operand; 2]; 2] = [[AtReg(0), Imm8], [AtReg(1), Imm8]]; | |
| 136 | + | const DIR_RI: [[Operand; 2]; 2] = [[Direct, AtReg(0)], [Direct, AtReg(1)]]; | |
| 137 | + | const RI_DIR: [[Operand; 2]; 2] = [[AtReg(0), Direct], [AtReg(1), Direct]]; | |
| 138 | + | const RI_IMM_REL: [[Operand; 3]; 2] = [[AtReg(0), Imm8, Rel], [AtReg(1), Imm8, Rel]]; | |
| 139 | + | ||
| 140 | + | /// Fixed (non register-indexed) opcodes. | |
| 141 | + | const FIXED: &[(u8, &str, &[Operand], u8)] = &[ | |
| 142 | + | (0x00, "NOP", &[], 1), | |
| 143 | + | (0x02, "LJMP", &[Addr16], 2), | |
| 144 | + | (0x03, "RR", &[A], 1), | |
| 145 | + | (0x04, "INC", &[A], 1), | |
| 146 | + | (0x05, "INC", &[Direct], 1), | |
| 147 | + | (0x10, "JBC", &[Bit, Rel], 2), | |
| 148 | + | (0x12, "LCALL", &[Addr16], 2), | |
| 149 | + | (0x13, "RRC", &[A], 1), | |
| 150 | + | (0x14, "DEC", &[A], 1), | |
| 151 | + | (0x15, "DEC", &[Direct], 1), | |
| 152 | + | (0x20, "JB", &[Bit, Rel], 2), | |
| 153 | + | (0x22, "RET", &[], 2), | |
| 154 | + | (0x23, "RL", &[A], 1), | |
| 155 | + | (0x24, "ADD", &[A, Imm8], 1), | |
| 156 | + | (0x25, "ADD", &[A, Direct], 1), | |
| 157 | + | (0x30, "JNB", &[Bit, Rel], 2), | |
| 158 | + | (0x32, "RETI", &[], 2), | |
| 159 | + | (0x33, "RLC", &[A], 1), | |
| 160 | + | (0x34, "ADDC", &[A, Imm8], 1), | |
| 161 | + | (0x35, "ADDC", &[A, Direct], 1), | |
| 162 | + | (0x40, "JC", &[Rel], 2), | |
| 163 | + | (0x42, "ORL", &[Direct, A], 1), | |
| 164 | + | (0x43, "ORL", &[Direct, Imm8], 2), | |
| 165 | + | (0x44, "ORL", &[A, Imm8], 1), | |
| 166 | + | (0x45, "ORL", &[A, Direct], 1), | |
| 167 | + | (0x50, "JNC", &[Rel], 2), | |
| 168 | + | (0x52, "ANL", &[Direct, A], 1), | |
| 169 | + | (0x53, "ANL", &[Direct, Imm8], 2), | |
| 170 | + | (0x54, "ANL", &[A, Imm8], 1), | |
| 171 | + | (0x55, "ANL", &[A, Direct], 1), | |
| 172 | + | (0x60, "JZ", &[Rel], 2), | |
| 173 | + | (0x62, "XRL", &[Direct, A], 1), | |
| 174 | + | (0x63, "XRL", &[Direct, Imm8], 2), | |
| 175 | + | (0x64, "XRL", &[A, Imm8], 1), | |
| 176 | + | (0x65, "XRL", &[A, Direct], 1), | |
| 177 | + | (0x70, "JNZ", &[Rel], 2), | |
| 178 | + | (0x72, "ORL", &[C, Bit], 2), | |
| 179 | + | (0x73, "JMP", &[AtAPlusDptr], 2), | |
| 180 | + | (0x74, "MOV", &[A, Imm8], 1), | |
| 181 | + | (0x75, "MOV", &[Direct, Imm8], 2), | |
| 182 | + | (0x80, "SJMP", &[Rel], 2), | |
| 183 | + | (0x82, "ANL", &[C, Bit], 2), | |
| 184 | + | (0x83, "MOVC", &[A, AtAPlusPc], 2), | |
| 185 | + | (0x84, "DIV", &[AB], 4), | |
| 186 | + | // Encoded as: opcode, src, dest (handled specially by the encoder). | |
| 187 | + | (0x85, "MOV", &[Direct, Direct], 2), | |
| 188 | + | (0x90, "MOV", &[Dptr, Imm16], 2), | |
| 189 | + | (0x92, "MOV", &[Bit, C], 2), | |
| 190 | + | (0x93, "MOVC", &[A, AtAPlusDptr], 2), | |
| 191 | + | (0x94, "SUBB", &[A, Imm8], 1), | |
| 192 | + | (0x95, "SUBB", &[A, Direct], 1), | |
| 193 | + | (0xA0, "ORL", &[C, NotBit], 2), | |
| 194 | + | (0xA2, "MOV", &[C, Bit], 1), | |
| 195 | + | (0xA3, "INC", &[Dptr], 2), | |
| 196 | + | (0xA4, "MUL", &[AB], 4), | |
| 197 | + | (0xB0, "ANL", &[C, NotBit], 2), | |
| 198 | + | (0xB2, "CPL", &[Bit], 1), | |
| 199 | + | (0xB3, "CPL", &[C], 1), | |
| 200 | + | (0xB4, "CJNE", &[A, Imm8, Rel], 2), | |
| 201 | + | (0xB5, "CJNE", &[A, Direct, Rel], 2), | |
| 202 | + | (0xC0, "PUSH", &[Direct], 2), | |
| 203 | + | (0xC2, "CLR", &[Bit], 1), | |
| 204 | + | (0xC3, "CLR", &[C], 1), | |
| 205 | + | (0xC4, "SWAP", &[A], 1), | |
| 206 | + | (0xC5, "XCH", &[A, Direct], 1), | |
| 207 | + | (0xD0, "POP", &[Direct], 2), | |
| 208 | + | (0xD2, "SETB", &[Bit], 1), | |
| 209 | + | (0xD3, "SETB", &[C], 1), | |
| 210 | + | (0xD4, "DA", &[A], 1), | |
| 211 | + | (0xD5, "DJNZ", &[Direct, Rel], 2), | |
| 212 | + | (0xE0, "MOVX", &[A, AtDptr], 2), | |
| 213 | + | (0xE4, "CLR", &[A], 1), | |
| 214 | + | (0xE5, "MOV", &[A, Direct], 1), | |
| 215 | + | (0xF0, "MOVX", &[AtDptr, A], 2), | |
| 216 | + | (0xF4, "CPL", &[A], 1), | |
| 217 | + | (0xF5, "MOV", &[Direct, A], 1), | |
| 218 | + | ]; | |
| 219 | + | ||
| 220 | + | /// Register-indexed groups: (base opcode, mnemonic, cycles, @Ri forms, Rn forms). | |
| 221 | + | type Group = ( | |
| 222 | + | u8, | |
| 223 | + | &'static str, | |
| 224 | + | u8, | |
| 225 | + | Option<&'static [[Operand; 2]; 2]>, | |
| 226 | + | Option<&'static [[Operand; 2]; 8]>, | |
| 227 | + | ); | |
| 228 | + | const GROUPS2: &[Group] = &[ | |
| 229 | + | (0x20, "ADD", 1, Some(&A_RI), Some(&A_RN)), | |
| 230 | + | (0x30, "ADDC", 1, Some(&A_RI), Some(&A_RN)), | |
| 231 | + | (0x40, "ORL", 1, Some(&A_RI), Some(&A_RN)), | |
| 232 | + | (0x50, "ANL", 1, Some(&A_RI), Some(&A_RN)), | |
| 233 | + | (0x60, "XRL", 1, Some(&A_RI), Some(&A_RN)), | |
| 234 | + | (0x70, "MOV", 1, Some(&RI_IMM), Some(&RN_IMM)), | |
| 235 | + | (0x80, "MOV", 2, Some(&DIR_RI), Some(&DIR_RN)), | |
| 236 | + | (0x90, "SUBB", 1, Some(&A_RI), Some(&A_RN)), | |
| 237 | + | (0xA0, "MOV", 2, Some(&RI_DIR), Some(&RN_DIR)), | |
| 238 | + | (0xC0, "XCH", 1, Some(&A_RI), Some(&A_RN)), | |
| 239 | + | (0xD0, "XCHD", 1, Some(&A_RI), None), | |
| 240 | + | (0xD0, "DJNZ", 2, None, Some(&RN_REL)), | |
| 241 | + | (0xE0, "MOV", 1, Some(&A_RI), Some(&A_RN)), | |
| 242 | + | (0xF0, "MOV", 1, Some(&RI_A), Some(&RN_A)), | |
| 243 | + | ]; | |
| 244 | + | ||
| 245 | + | fn size_of(ops: &[Operand]) -> u8 { | |
| 246 | + | 1 + ops.iter().map(|o| o.len()).sum::<u8>() | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | fn build() -> Vec<Option<OpInfo>> { | |
| 250 | + | let mut t: Vec<Option<OpInfo>> = vec![None; 256]; | |
| 251 | + | let mut put = |opcode: u8, mnemonic: &'static str, operands: &'static [Operand], cycles: u8| { | |
| 252 | + | assert!( | |
| 253 | + | t[opcode as usize].is_none(), | |
| 254 | + | "duplicate opcode {opcode:02X}" | |
| 255 | + | ); | |
| 256 | + | t[opcode as usize] = Some(OpInfo { | |
| 257 | + | opcode, | |
| 258 | + | mnemonic, | |
| 259 | + | operands, | |
| 260 | + | size: size_of(operands), | |
| 261 | + | cycles, | |
| 262 | + | }); | |
| 263 | + | }; | |
| 264 | + | ||
| 265 | + | for &(op, m, ops, cyc) in FIXED { | |
| 266 | + | put(op, m, ops, cyc); | |
| 267 | + | } | |
| 268 | + | // AJMP / ACALL: addr11 high bits live in the opcode's top three bits. | |
| 269 | + | for page in 0..8u8 { | |
| 270 | + | put((page << 5) | 0x01, "AJMP", &[Addr11], 2); | |
| 271 | + | put((page << 5) | 0x11, "ACALL", &[Addr11], 2); | |
| 272 | + | } | |
| 273 | + | for i in 0..2 { | |
| 274 | + | put(0x06 + i as u8, "INC", &RI[i], 1); | |
| 275 | + | put(0x16 + i as u8, "DEC", &RI[i], 1); | |
| 276 | + | put(0xB6 + i as u8, "CJNE", &RI_IMM_REL[i], 2); | |
| 277 | + | put(0xE2 + i as u8, "MOVX", &A_RI[i], 2); | |
| 278 | + | put(0xF2 + i as u8, "MOVX", &RI_A[i], 2); | |
| 279 | + | } | |
| 280 | + | for n in 0..8 { | |
| 281 | + | put(0x08 + n as u8, "INC", &RN[n], 1); | |
| 282 | + | put(0x18 + n as u8, "DEC", &RN[n], 1); | |
| 283 | + | put(0xB8 + n as u8, "CJNE", &RN_IMM_REL[n], 2); | |
| 284 | + | } | |
| 285 | + | for &(base, m, cyc, ri, rn) in GROUPS2 { | |
| 286 | + | if let Some(ri) = ri { | |
| 287 | + | for (i, ops) in ri.iter().enumerate() { | |
| 288 | + | put(base + 6 + i as u8, m, ops, cyc); | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | if let Some(rn) = rn { | |
| 292 | + | for (n, ops) in rn.iter().enumerate() { | |
| 293 | + | put(base + 8 + n as u8, m, ops, cyc); | |
| 294 | + | } | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | t | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | fn table() -> &'static [Option<OpInfo>] { | |
| 301 | + | static TABLE: OnceLock<Vec<Option<OpInfo>>> = OnceLock::new(); | |
| 302 | + | TABLE.get_or_init(build) | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | /// Info for an opcode; `None` only for the reserved opcode 0xA5. | |
| 306 | + | pub fn info(opcode: u8) -> Option<&'static OpInfo> { | |
| 307 | + | table()[opcode as usize].as_ref() | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | /// All defined opcodes with the given mnemonic (upper case). | |
| 311 | + | pub fn by_mnemonic(mnemonic: &str) -> impl Iterator<Item = &'static OpInfo> + '_ { | |
| 312 | + | table() | |
| 313 | + | .iter() | |
| 314 | + | .flatten() | |
| 315 | + | .filter(move |i| i.mnemonic == mnemonic) | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | pub fn is_mnemonic(s: &str) -> bool { | |
| 319 | + | matches!(s, "JMP" | "CALL") || table().iter().flatten().any(|i| i.mnemonic == s) | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | #[cfg(test)] | |
| 323 | + | mod tests { | |
| 324 | + | use super::*; | |
| 325 | + | ||
| 326 | + | #[test] | |
| 327 | + | fn only_a5_is_undefined() { | |
| 328 | + | let undefined: Vec<_> = (0..=255u8).filter(|&op| info(op).is_none()).collect(); | |
| 329 | + | assert_eq!(undefined, vec![0xA5]); | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | #[test] | |
| 333 | + | fn spot_check_sizes_and_cycles() { | |
| 334 | + | let i = info(0x85).unwrap(); | |
| 335 | + | assert_eq!((i.mnemonic, i.size, i.cycles), ("MOV", 3, 2)); | |
| 336 | + | let i = info(0xB8).unwrap(); | |
| 337 | + | assert_eq!((i.mnemonic, i.size, i.cycles), ("CJNE", 3, 2)); | |
| 338 | + | let i = info(0xD6).unwrap(); | |
| 339 | + | assert_eq!((i.mnemonic, i.size), ("XCHD", 1)); | |
| 340 | + | let i = info(0xA4).unwrap(); | |
| 341 | + | assert_eq!((i.mnemonic, i.cycles), ("MUL", 4)); | |
| 342 | + | let i = info(0xF1).unwrap(); | |
| 343 | + | assert_eq!((i.mnemonic, i.size), ("ACALL", 2)); | |
| 344 | + | let i = info(0x90).unwrap(); | |
| 345 | + | assert_eq!((i.mnemonic, i.size), ("MOV", 3)); | |
| 346 | + | } | |
| 347 | + | } | |
| @@ -1,43 +1,95 @@ | |||
| 1 | − | mod assembler; | |
| 2 | − | mod cpu; | |
| 3 | − | mod instructions; | |
| 4 | − | mod memory; | |
| 1 | + | mod asm; | |
| 2 | + | mod isa; | |
| 3 | + | mod sfr; | |
| 4 | + | mod sim; | |
| 5 | + | mod ui; | |
| 5 | 6 | ||
| 6 | − | use std::io; | |
| 7 | + | use std::path::PathBuf; | |
| 8 | + | use std::process::ExitCode; | |
| 7 | 9 | ||
| 8 | − | use crate::assembler::assembler; | |
| 9 | 10 | use clap::Parser; | |
| 10 | − | use cpu::CPU; | |
| 11 | − | use memory::Memory; | |
| 12 | 11 | ||
| 12 | + | /// Intel 8051 assembler, simulator and TUI debugger. | |
| 13 | 13 | #[derive(Parser, Debug)] | |
| 14 | − | #[command(version, about, long_about = None)] | |
| 14 | + | #[command(version, about)] | |
| 15 | 15 | struct Args { | |
| 16 | − | #[arg(short, long)] | |
| 17 | − | filename: String, | |
| 18 | − | } | |
| 16 | + | /// Assembly source (.asm) or Intel HEX (.hex) file to open. | |
| 17 | + | file: Option<PathBuf>, | |
| 19 | 18 | ||
| 20 | − | fn main() { | |
| 21 | − | let args = Args::parse(); | |
| 19 | + | /// Assemble FILE without starting the TUI, writing the Intel HEX file. | |
| 20 | + | #[arg(short, long, requires = "file")] | |
| 21 | + | assemble: bool, | |
| 22 | 22 | ||
| 23 | − | let mut cpu = CPU { | |
| 24 | − | sp: 0xFFFE, | |
| 25 | − | ..Default::default() | |
| 26 | − | }; | |
| 27 | − | let mut mem = Memory::new(); | |
| 23 | + | /// Output HEX path (default: FILE with a .hex extension). | |
| 24 | + | #[arg(short, long, requires = "assemble")] | |
| 25 | + | output: Option<PathBuf>, | |
| 28 | 26 | ||
| 29 | − | let asm = std::fs::read_to_string(args.filename).unwrap(); | |
| 30 | − | let program = assembler(&asm); | |
| 27 | + | /// Also write a listing to this path (default: FILE with a .lst extension). | |
| 28 | + | #[arg(long, requires = "assemble")] | |
| 29 | + | lst: Option<PathBuf>, | |
| 31 | 30 | ||
| 32 | − | for (i, byte) in program.iter().enumerate() { | |
| 33 | − | mem.write(i as u16, *byte); | |
| 34 | − | } | |
| 31 | + | /// Oscillator frequency in MHz, used to show elapsed time. | |
| 32 | + | #[arg(long, default_value_t = 11.0592)] | |
| 33 | + | clock: f64, | |
| 34 | + | } | |
| 35 | 35 | ||
| 36 | − | while !cpu.halted { | |
| 37 | − | cpu.debug_instr(&mem); | |
| 38 | − | cpu.step(&mut mem); | |
| 36 | + | fn assemble_file(args: &Args) -> ExitCode { | |
| 37 | + | let path = args.file.as_ref().unwrap(); | |
| 38 | + | let source = match std::fs::read_to_string(path) { | |
| 39 | + | Ok(s) => s, | |
| 40 | + | Err(e) => { | |
| 41 | + | eprintln!("error: cannot read {}: {e}", path.display()); | |
| 42 | + | return ExitCode::FAILURE; | |
| 43 | + | } | |
| 44 | + | }; | |
| 45 | + | let out = asm::assemble(&source); | |
| 46 | + | let lst = args | |
| 47 | + | .lst | |
| 48 | + | .clone() | |
| 49 | + | .unwrap_or_else(|| path.with_extension("lst")); | |
| 50 | + | if let Err(e) = std::fs::write(&lst, out.listing(&source)) { | |
| 51 | + | eprintln!("error: cannot write {}: {e}", lst.display()); | |
| 52 | + | } | |
| 53 | + | if !out.ok() { | |
| 54 | + | for d in &out.diagnostics { | |
| 55 | + | eprintln!( | |
| 56 | + | "{}:{}:{}: error: {}", | |
| 57 | + | path.display(), | |
| 58 | + | d.line + 1, | |
| 59 | + | d.col + 1, | |
| 60 | + | d.msg | |
| 61 | + | ); | |
| 62 | + | } | |
| 63 | + | eprintln!("{} error(s); see {}", out.diagnostics.len(), lst.display()); | |
| 64 | + | return ExitCode::FAILURE; | |
| 65 | + | } | |
| 66 | + | let hex = args | |
| 67 | + | .output | |
| 68 | + | .clone() | |
| 69 | + | .unwrap_or_else(|| path.with_extension("hex")); | |
| 70 | + | if let Err(e) = std::fs::write(&hex, out.intel_hex()) { | |
| 71 | + | eprintln!("error: cannot write {}: {e}", hex.display()); | |
| 72 | + | return ExitCode::FAILURE; | |
| 73 | + | } | |
| 74 | + | println!( | |
| 75 | + | "{} bytes -> {}, listing -> {}", | |
| 76 | + | out.code_size(), | |
| 77 | + | hex.display(), | |
| 78 | + | lst.display() | |
| 79 | + | ); | |
| 80 | + | ExitCode::SUCCESS | |
| 81 | + | } | |
| 39 | 82 | ||
| 40 | − | let mut buf = String::new(); | |
| 41 | − | io::stdin().read_line(&mut buf).unwrap(); | |
| 83 | + | fn main() -> ExitCode { | |
| 84 | + | let args = Args::parse(); | |
| 85 | + | if args.assemble { | |
| 86 | + | return assemble_file(&args); | |
| 87 | + | } | |
| 88 | + | match ui::run(args.file, args.clock) { | |
| 89 | + | Ok(()) => ExitCode::SUCCESS, | |
| 90 | + | Err(e) => { | |
| 91 | + | eprintln!("error: {e}"); | |
| 92 | + | ExitCode::FAILURE | |
| 93 | + | } | |
| 42 | 94 | } | |
| 43 | 95 | } | |
| @@ -1,17 +0,0 @@ | |||
| 1 | − | pub struct Memory { | |
| 2 | − | pub data: [u8; 65536], | |
| 3 | − | } | |
| 4 | − | ||
| 5 | − | impl Memory { | |
| 6 | − | pub fn new() -> Self { | |
| 7 | − | Self { data: [0; 65536] } | |
| 8 | − | } | |
| 9 | − | ||
| 10 | − | pub fn read(&self, addr: u16) -> u8 { | |
| 11 | − | self.data[addr as usize] | |
| 12 | − | } | |
| 13 | − | ||
| 14 | − | pub fn write(&mut self, addr: u16, value: u8) { | |
| 15 | − | self.data[addr as usize] = value; | |
| 16 | − | } | |
| 17 | − | } | |
| @@ -0,0 +1,146 @@ | |||
| 1 | + | //! Special function register and bit addresses of the standard 8051. | |
| 2 | + | ||
| 3 | + | pub const P0: u8 = 0x80; | |
| 4 | + | pub const SP: u8 = 0x81; | |
| 5 | + | pub const DPL: u8 = 0x82; | |
| 6 | + | pub const DPH: u8 = 0x83; | |
| 7 | + | pub const PCON: u8 = 0x87; | |
| 8 | + | pub const TCON: u8 = 0x88; | |
| 9 | + | pub const TMOD: u8 = 0x89; | |
| 10 | + | pub const TL0: u8 = 0x8A; | |
| 11 | + | pub const TL1: u8 = 0x8B; | |
| 12 | + | pub const TH0: u8 = 0x8C; | |
| 13 | + | pub const TH1: u8 = 0x8D; | |
| 14 | + | pub const P1: u8 = 0x90; | |
| 15 | + | pub const SCON: u8 = 0x98; | |
| 16 | + | pub const SBUF: u8 = 0x99; | |
| 17 | + | pub const P2: u8 = 0xA0; | |
| 18 | + | pub const IE: u8 = 0xA8; | |
| 19 | + | pub const P3: u8 = 0xB0; | |
| 20 | + | pub const IP: u8 = 0xB8; | |
| 21 | + | pub const PSW: u8 = 0xD0; | |
| 22 | + | pub const ACC: u8 = 0xE0; | |
| 23 | + | pub const B: u8 = 0xF0; | |
| 24 | + | ||
| 25 | + | pub const SFRS: &[(&str, u8)] = &[ | |
| 26 | + | ("P0", P0), | |
| 27 | + | ("SP", SP), | |
| 28 | + | ("DPL", DPL), | |
| 29 | + | ("DPH", DPH), | |
| 30 | + | ("PCON", PCON), | |
| 31 | + | ("TCON", TCON), | |
| 32 | + | ("TMOD", TMOD), | |
| 33 | + | ("TL0", TL0), | |
| 34 | + | ("TL1", TL1), | |
| 35 | + | ("TH0", TH0), | |
| 36 | + | ("TH1", TH1), | |
| 37 | + | ("P1", P1), | |
| 38 | + | ("SCON", SCON), | |
| 39 | + | ("SBUF", SBUF), | |
| 40 | + | ("P2", P2), | |
| 41 | + | ("IE", IE), | |
| 42 | + | ("P3", P3), | |
| 43 | + | ("IP", IP), | |
| 44 | + | ("PSW", PSW), | |
| 45 | + | ("ACC", ACC), | |
| 46 | + | ("B", B), | |
| 47 | + | ]; | |
| 48 | + | ||
| 49 | + | pub const BITS: &[(&str, u8)] = &[ | |
| 50 | + | ("IT0", 0x88), | |
| 51 | + | ("IE0", 0x89), | |
| 52 | + | ("IT1", 0x8A), | |
| 53 | + | ("IE1", 0x8B), | |
| 54 | + | ("TR0", 0x8C), | |
| 55 | + | ("TF0", 0x8D), | |
| 56 | + | ("TR1", 0x8E), | |
| 57 | + | ("TF1", 0x8F), | |
| 58 | + | ("RI", 0x98), | |
| 59 | + | ("TI", 0x99), | |
| 60 | + | ("RB8", 0x9A), | |
| 61 | + | ("TB8", 0x9B), | |
| 62 | + | ("REN", 0x9C), | |
| 63 | + | ("SM2", 0x9D), | |
| 64 | + | ("SM1", 0x9E), | |
| 65 | + | ("SM0", 0x9F), | |
| 66 | + | ("EX0", 0xA8), | |
| 67 | + | ("ET0", 0xA9), | |
| 68 | + | ("EX1", 0xAA), | |
| 69 | + | ("ET1", 0xAB), | |
| 70 | + | ("ES", 0xAC), | |
| 71 | + | ("EA", 0xAF), | |
| 72 | + | ("RXD", 0xB0), | |
| 73 | + | ("TXD", 0xB1), | |
| 74 | + | ("INT0", 0xB2), | |
| 75 | + | ("INT1", 0xB3), | |
| 76 | + | ("T0", 0xB4), | |
| 77 | + | ("T1", 0xB5), | |
| 78 | + | ("WR", 0xB6), | |
| 79 | + | ("RD", 0xB7), | |
| 80 | + | ("PX0", 0xB8), | |
| 81 | + | ("PT0", 0xB9), | |
| 82 | + | ("PX1", 0xBA), | |
| 83 | + | ("PT1", 0xBB), | |
| 84 | + | ("PS", 0xBC), | |
| 85 | + | ("P", 0xD0), | |
| 86 | + | ("OV", 0xD2), | |
| 87 | + | ("RS0", 0xD3), | |
| 88 | + | ("RS1", 0xD4), | |
| 89 | + | ("F0", 0xD5), | |
| 90 | + | ("AC", 0xD6), | |
| 91 | + | ("CY", 0xD7), | |
| 92 | + | ]; | |
| 93 | + | ||
| 94 | + | pub fn sfr_name(addr: u8) -> Option<&'static str> { | |
| 95 | + | SFRS.iter().find(|&&(_, a)| a == addr).map(|&(n, _)| n) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | pub fn bit_name(addr: u8) -> Option<&'static str> { | |
| 99 | + | BITS.iter().find(|&&(_, a)| a == addr).map(|&(n, _)| n) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | /// Byte address and bit index of a bit address. | |
| 103 | + | pub fn bit_location(bit: u8) -> (u8, u8) { | |
| 104 | + | if bit < 0x80 { | |
| 105 | + | (0x20 + bit / 8, bit % 8) | |
| 106 | + | } else { | |
| 107 | + | (bit & 0xF8, bit & 7) | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | /// Human-readable bit operand, e.g. `TR0`, `P1.3` or `20h.5`. | |
| 112 | + | pub fn format_bit(bit: u8) -> String { | |
| 113 | + | if let Some(n) = bit_name(bit) { | |
| 114 | + | return n.to_string(); | |
| 115 | + | } | |
| 116 | + | let (byte, idx) = bit_location(bit); | |
| 117 | + | match sfr_name(byte) { | |
| 118 | + | Some(n) => format!("{n}.{idx}"), | |
| 119 | + | None => format!("{}.{idx}", hex8(byte)), | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | /// Human-readable direct address: SFR name or hex. | |
| 124 | + | pub fn format_direct(addr: u8) -> String { | |
| 125 | + | match sfr_name(addr) { | |
| 126 | + | Some(n) => n.to_string(), | |
| 127 | + | None => hex8(addr), | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | /// Assembler-style hex literal, with a leading zero when needed (`0A0h`). | |
| 132 | + | pub fn hex8(v: u8) -> String { | |
| 133 | + | if v >= 0xA0 { | |
| 134 | + | format!("0{v:02X}h") | |
| 135 | + | } else { | |
| 136 | + | format!("{v:02X}h") | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | pub fn hex16(v: u16) -> String { | |
| 141 | + | if v >= 0xA000 { | |
| 142 | + | format!("0{v:04X}h") | |
| 143 | + | } else { | |
| 144 | + | format!("{v:04X}h") | |
| 145 | + | } | |
| 146 | + | } | |
| @@ -0,0 +1,528 @@ | |||
| 1 | + | //! Instruction execution. | |
| 2 | + | ||
| 3 | + | use super::{AC, CY, OV, Sim}; | |
| 4 | + | use crate::sfr::{B, P2}; | |
| 5 | + | ||
| 6 | + | /// Operand location for the register-indexed columns (opcode low nibble 5..F). | |
| 7 | + | #[derive(Clone, Copy)] | |
| 8 | + | enum Loc { | |
| 9 | + | Direct(u8), | |
| 10 | + | Iram(u8), | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | impl Sim { | |
| 14 | + | fn fetch(&mut self) -> u8 { | |
| 15 | + | let b = self.code[self.pc as usize]; | |
| 16 | + | self.pc = self.pc.wrapping_add(1); | |
| 17 | + | b | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn fetch16(&mut self) -> u16 { | |
| 21 | + | let h = self.fetch(); | |
| 22 | + | let l = self.fetch(); | |
| 23 | + | u16::from_be_bytes([h, l]) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | fn rel_jump(&mut self, off: u8) { | |
| 27 | + | self.pc = self.pc.wrapping_add(off as i8 as u16); | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | /// Decodes the operand selected by the low nibble: 5 = direct, | |
| 31 | + | /// 6/7 = @R0/@R1, 8..F = R0..R7. | |
| 32 | + | fn loc(&mut self, op: u8) -> Loc { | |
| 33 | + | match op & 0x0F { | |
| 34 | + | 0x05 => Loc::Direct(self.fetch()), | |
| 35 | + | 0x06 | 0x07 => Loc::Iram(self.reg(op & 1)), | |
| 36 | + | n => Loc::Iram(self.reg_addr(n & 7)), | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | fn read_loc(&self, l: Loc, rmw: bool) -> u8 { | |
| 41 | + | match l { | |
| 42 | + | Loc::Direct(a) => self.read_direct(a, rmw), | |
| 43 | + | Loc::Iram(a) => self.iram[a as usize], | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | fn write_loc(&mut self, l: Loc, v: u8) { | |
| 48 | + | match l { | |
| 49 | + | Loc::Direct(a) => self.write_direct(a, v), | |
| 50 | + | Loc::Iram(a) => self.iram[a as usize] = v, | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | /// Source operand for the arithmetic/logic rows: 4 = #imm, otherwise `loc`. | |
| 55 | + | fn src(&mut self, op: u8) -> u8 { | |
| 56 | + | if op & 0x0F == 0x04 { | |
| 57 | + | self.fetch() | |
| 58 | + | } else { | |
| 59 | + | let l = self.loc(op); | |
| 60 | + | self.read_loc(l, false) | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | fn add(&mut self, v: u8, carry: bool) { | |
| 65 | + | let a = self.acc(); | |
| 66 | + | let c = carry as u16; | |
| 67 | + | let sum = a as u16 + v as u16 + c; | |
| 68 | + | let r = sum as u8; | |
| 69 | + | self.set_flag(CY, sum > 0xFF); | |
| 70 | + | self.set_flag(AC, (a & 0x0F) as u16 + (v & 0x0F) as u16 + c > 0x0F); | |
| 71 | + | self.set_flag(OV, (a ^ r) & (v ^ r) & 0x80 != 0); | |
| 72 | + | self.set_acc(r); | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | fn subb(&mut self, v: u8) { | |
| 76 | + | let a = self.acc(); | |
| 77 | + | let c = self.flag(CY) as i16; | |
| 78 | + | let diff = a as i16 - v as i16 - c; | |
| 79 | + | let r = diff as u8; | |
| 80 | + | self.set_flag(CY, diff < 0); | |
| 81 | + | self.set_flag(AC, ((a & 0x0F) as i16) < (v & 0x0F) as i16 + c); | |
| 82 | + | self.set_flag(OV, (a ^ v) & (a ^ r) & 0x80 != 0); | |
| 83 | + | self.set_acc(r); | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | fn cjne(&mut self, a: u8, b: u8) { | |
| 87 | + | let off = self.fetch(); | |
| 88 | + | self.set_flag(CY, a < b); | |
| 89 | + | if a != b { | |
| 90 | + | self.rel_jump(off); | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | fn movx_addr_ri(&self, op: u8) -> u16 { | |
| 95 | + | // The high address byte comes from the P2 latch. | |
| 96 | + | u16::from_be_bytes([self.sfr(P2), self.reg(op & 1)]) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | pub(super) fn execute(&mut self, op: u8) { | |
| 100 | + | match op { | |
| 101 | + | 0x00 => {} | |
| 102 | + | ||
| 103 | + | // AJMP / ACALL addr11 | |
| 104 | + | _ if op & 0x1F == 0x01 || op & 0x1F == 0x11 => { | |
| 105 | + | let lo = self.fetch(); | |
| 106 | + | let target = (self.pc & 0xF800) | ((op as u16 >> 5) << 8) | lo as u16; | |
| 107 | + | if op & 0x10 != 0 { | |
| 108 | + | self.push_pc(); | |
| 109 | + | } | |
| 110 | + | self.pc = target; | |
| 111 | + | } | |
| 112 | + | 0x02 => self.pc = self.fetch16(), | |
| 113 | + | 0x12 => { | |
| 114 | + | let target = self.fetch16(); | |
| 115 | + | self.push_pc(); | |
| 116 | + | self.pc = target; | |
| 117 | + | } | |
| 118 | + | 0x22 => self.pop_pc(), | |
| 119 | + | 0x32 => { | |
| 120 | + | self.pop_pc(); | |
| 121 | + | self.in_service.pop(); | |
| 122 | + | self.irq_hold = true; | |
| 123 | + | } | |
| 124 | + | 0x73 => self.pc = self.dptr().wrapping_add(self.acc() as u16), | |
| 125 | + | 0x80 => { | |
| 126 | + | let off = self.fetch(); | |
| 127 | + | self.rel_jump(off); | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | // Rotates | |
| 131 | + | 0x03 => self.set_acc(self.acc().rotate_right(1)), | |
| 132 | + | 0x23 => self.set_acc(self.acc().rotate_left(1)), | |
| 133 | + | 0x13 => { | |
| 134 | + | let a = self.acc(); | |
| 135 | + | self.set_acc((a >> 1) | ((self.flag(CY) as u8) << 7)); | |
| 136 | + | self.set_flag(CY, a & 1 != 0); | |
| 137 | + | } | |
| 138 | + | 0x33 => { | |
| 139 | + | let a = self.acc(); | |
| 140 | + | self.set_acc((a << 1) | self.flag(CY) as u8); | |
| 141 | + | self.set_flag(CY, a & 0x80 != 0); | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // INC / DEC | |
| 145 | + | 0x04 => self.set_acc(self.acc().wrapping_add(1)), | |
| 146 | + | 0x14 => self.set_acc(self.acc().wrapping_sub(1)), | |
| 147 | + | 0x05..=0x0F | 0x15..=0x1F => { | |
| 148 | + | let l = self.loc(op); | |
| 149 | + | let v = self.read_loc(l, true); | |
| 150 | + | self.write_loc( | |
| 151 | + | l, | |
| 152 | + | if op < 0x10 { | |
| 153 | + | v.wrapping_add(1) | |
| 154 | + | } else { | |
| 155 | + | v.wrapping_sub(1) | |
| 156 | + | }, | |
| 157 | + | ); | |
| 158 | + | } | |
| 159 | + | 0xA3 => self.set_dptr(self.dptr().wrapping_add(1)), | |
| 160 | + | ||
| 161 | + | // Conditional jumps on bits | |
| 162 | + | 0x10 | 0x20 | 0x30 => { | |
| 163 | + | let bit = self.fetch(); | |
| 164 | + | let off = self.fetch(); | |
| 165 | + | let set = self.read_bit(bit, op == 0x10); | |
| 166 | + | if op == 0x10 && set { | |
| 167 | + | self.write_bit(bit, false); | |
| 168 | + | } | |
| 169 | + | if set == (op != 0x30) { | |
| 170 | + | self.rel_jump(off); | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | 0x40 | 0x50 | 0x60 | 0x70 => { | |
| 174 | + | let off = self.fetch(); | |
| 175 | + | let take = match op { | |
| 176 | + | 0x40 => self.flag(CY), | |
| 177 | + | 0x50 => !self.flag(CY), | |
| 178 | + | 0x60 => self.acc() == 0, | |
| 179 | + | _ => self.acc() != 0, | |
| 180 | + | }; | |
| 181 | + | if take { | |
| 182 | + | self.rel_jump(off); | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // Arithmetic | |
| 187 | + | 0x24..=0x2F => { | |
| 188 | + | let v = self.src(op); | |
| 189 | + | self.add(v, false); | |
| 190 | + | } | |
| 191 | + | 0x34..=0x3F => { | |
| 192 | + | let v = self.src(op); | |
| 193 | + | self.add(v, self.flag(CY)); | |
| 194 | + | } | |
| 195 | + | 0x94..=0x9F => { | |
| 196 | + | let v = self.src(op); | |
| 197 | + | self.subb(v); | |
| 198 | + | } | |
| 199 | + | 0x84 => { | |
| 200 | + | let (a, b) = (self.acc(), self.b()); | |
| 201 | + | self.set_flag(CY, false); | |
| 202 | + | // Division by zero sets OV and leaves A and B unchanged. | |
| 203 | + | match a.checked_div(b) { | |
| 204 | + | Some(q) => { | |
| 205 | + | self.set_acc(q); | |
| 206 | + | self.set_sfr(B, a % b); | |
| 207 | + | self.set_flag(OV, false); | |
| 208 | + | } | |
| 209 | + | None => self.set_flag(OV, true), | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | 0xA4 => { | |
| 213 | + | let r = self.acc() as u16 * self.b() as u16; | |
| 214 | + | self.set_acc(r as u8); | |
| 215 | + | self.set_sfr(B, (r >> 8) as u8); | |
| 216 | + | self.set_flag(CY, false); | |
| 217 | + | self.set_flag(OV, r > 0xFF); | |
| 218 | + | } | |
| 219 | + | 0xD4 => { | |
| 220 | + | let mut a = self.acc() as u16; | |
| 221 | + | if a & 0x0F > 9 || self.flag(AC) { | |
| 222 | + | a += 0x06; | |
| 223 | + | } | |
| 224 | + | if a > 0xFF { | |
| 225 | + | self.set_flag(CY, true); | |
| 226 | + | } | |
| 227 | + | if (a >> 4) & 0x1F > 9 || self.flag(CY) { | |
| 228 | + | a += 0x60; | |
| 229 | + | } | |
| 230 | + | if a > 0xFF { | |
| 231 | + | self.set_flag(CY, true); | |
| 232 | + | } | |
| 233 | + | self.set_acc(a as u8); | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // Logic: ORL/ANL/XRL direct,A / direct,#imm / A,src | |
| 237 | + | 0x42 | 0x43 | 0x52 | 0x53 | 0x62 | 0x63 => { | |
| 238 | + | let addr = self.fetch(); | |
| 239 | + | let v = if op & 1 == 1 { | |
| 240 | + | self.fetch() | |
| 241 | + | } else { | |
| 242 | + | self.acc() | |
| 243 | + | }; | |
| 244 | + | let d = self.read_direct(addr, true); | |
| 245 | + | self.write_direct(addr, logic(op, d, v)); | |
| 246 | + | } | |
| 247 | + | 0x44..=0x4F | 0x54..=0x5F | 0x64..=0x6F => { | |
| 248 | + | let v = self.src(op); | |
| 249 | + | self.set_acc(logic(op, self.acc(), v)); | |
| 250 | + | } | |
| 251 | + | 0xC4 => self.set_acc(self.acc().rotate_left(4)), | |
| 252 | + | 0xE4 => self.set_acc(0), | |
| 253 | + | 0xF4 => self.set_acc(!self.acc()), | |
| 254 | + | ||
| 255 | + | // Boolean (carry) operations | |
| 256 | + | 0x72 | 0x82 | 0xA0 | 0xB0 => { | |
| 257 | + | let bit = self.fetch(); | |
| 258 | + | let v = self.read_bit(bit, false) != (op >= 0xA0); | |
| 259 | + | let c = self.flag(CY); | |
| 260 | + | self.set_flag(CY, if op & 0x10 == 0 { c || v } else { c && v }); | |
| 261 | + | } | |
| 262 | + | 0x92 => { | |
| 263 | + | let bit = self.fetch(); | |
| 264 | + | self.write_bit(bit, self.flag(CY)); | |
| 265 | + | } | |
| 266 | + | 0xA2 => { | |
| 267 | + | let bit = self.fetch(); | |
| 268 | + | self.set_flag(CY, self.read_bit(bit, false)); | |
| 269 | + | } | |
| 270 | + | 0xB2 | 0xC2 | 0xD2 => { | |
| 271 | + | let bit = self.fetch(); | |
| 272 | + | let v = match op { | |
| 273 | + | 0xB2 => !self.read_bit(bit, true), | |
| 274 | + | 0xC2 => false, | |
| 275 | + | _ => true, | |
| 276 | + | }; | |
| 277 | + | self.write_bit(bit, v); | |
| 278 | + | } | |
| 279 | + | 0xB3 => self.set_flag(CY, !self.flag(CY)), | |
| 280 | + | 0xC3 => self.set_flag(CY, false), | |
| 281 | + | 0xD3 => self.set_flag(CY, true), | |
| 282 | + | ||
| 283 | + | // Data transfer | |
| 284 | + | 0x74 => { | |
| 285 | + | let v = self.fetch(); | |
| 286 | + | self.set_acc(v); | |
| 287 | + | } | |
| 288 | + | 0x75 => { | |
| 289 | + | let addr = self.fetch(); | |
| 290 | + | let v = self.fetch(); | |
| 291 | + | self.write_direct(addr, v); | |
| 292 | + | } | |
| 293 | + | 0x76..=0x7F => { | |
| 294 | + | let l = self.loc(op); | |
| 295 | + | let v = self.fetch(); | |
| 296 | + | self.write_loc(l, v); | |
| 297 | + | } | |
| 298 | + | 0x85 => { | |
| 299 | + | let src = self.fetch(); | |
| 300 | + | let dst = self.fetch(); | |
| 301 | + | let v = self.read_direct(src, false); | |
| 302 | + | self.write_direct(dst, v); | |
| 303 | + | } | |
| 304 | + | 0x86..=0x8F => { | |
| 305 | + | let l = self.loc(op); | |
| 306 | + | let addr = self.fetch(); | |
| 307 | + | let v = self.read_loc(l, false); | |
| 308 | + | self.write_direct(addr, v); | |
| 309 | + | } | |
| 310 | + | 0xA6..=0xAF => { | |
| 311 | + | let l = self.loc(op); | |
| 312 | + | let addr = self.fetch(); | |
| 313 | + | let v = self.read_direct(addr, false); | |
| 314 | + | self.write_loc(l, v); | |
| 315 | + | } | |
| 316 | + | 0xE5..=0xEF => { | |
| 317 | + | let v = self.src(op); | |
| 318 | + | self.set_acc(v); | |
| 319 | + | } | |
| 320 | + | 0xF5..=0xFF => { | |
| 321 | + | let l = self.loc(op); | |
| 322 | + | self.write_loc(l, self.acc()); | |
| 323 | + | } | |
| 324 | + | 0x90 => { | |
| 325 | + | let v = self.fetch16(); | |
| 326 | + | self.set_dptr(v); | |
| 327 | + | } | |
| 328 | + | 0x83 => self.set_acc(self.code[self.pc.wrapping_add(self.acc() as u16) as usize]), | |
| 329 | + | 0x93 => self.set_acc(self.code[self.dptr().wrapping_add(self.acc() as u16) as usize]), | |
| 330 | + | 0xE0 => self.set_acc(self.xram[self.dptr() as usize]), | |
| 331 | + | 0xF0 => self.xram[self.dptr() as usize] = self.acc(), | |
| 332 | + | 0xE2 | 0xE3 => self.set_acc(self.xram[self.movx_addr_ri(op) as usize]), | |
| 333 | + | 0xF2 | 0xF3 => self.xram[self.movx_addr_ri(op) as usize] = self.acc(), | |
| 334 | + | 0xC0 => { | |
| 335 | + | let addr = self.fetch(); | |
| 336 | + | let v = self.read_direct(addr, false); | |
| 337 | + | self.push(v); | |
| 338 | + | } | |
| 339 | + | 0xD0 => { | |
| 340 | + | let addr = self.fetch(); | |
| 341 | + | let v = self.pop(); | |
| 342 | + | self.write_direct(addr, v); | |
| 343 | + | } | |
| 344 | + | 0xC5..=0xCF => { | |
| 345 | + | let l = self.loc(op); | |
| 346 | + | let v = self.read_loc(l, true); | |
| 347 | + | self.write_loc(l, self.acc()); | |
| 348 | + | self.set_acc(v); | |
| 349 | + | } | |
| 350 | + | 0xD6 | 0xD7 => { | |
| 351 | + | let addr = self.reg(op & 1) as usize; | |
| 352 | + | let (a, m) = (self.acc(), self.iram[addr]); | |
| 353 | + | self.set_acc((a & 0xF0) | (m & 0x0F)); | |
| 354 | + | self.iram[addr] = (m & 0xF0) | (a & 0x0F); | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | // Compare and loop | |
| 358 | + | 0xB4 => { | |
| 359 | + | let v = self.fetch(); | |
| 360 | + | self.cjne(self.acc(), v); | |
| 361 | + | } | |
| 362 | + | 0xB5 => { | |
| 363 | + | let addr = self.fetch(); | |
| 364 | + | let v = self.read_direct(addr, false); | |
| 365 | + | self.cjne(self.acc(), v); | |
| 366 | + | } | |
| 367 | + | 0xB6..=0xBF => { | |
| 368 | + | let l = self.loc(op); | |
| 369 | + | let a = self.read_loc(l, false); | |
| 370 | + | let v = self.fetch(); | |
| 371 | + | self.cjne(a, v); | |
| 372 | + | } | |
| 373 | + | 0xD5 | 0xD8..=0xDF => { | |
| 374 | + | let l = self.loc(op); | |
| 375 | + | let v = self.read_loc(l, true).wrapping_sub(1); | |
| 376 | + | self.write_loc(l, v); | |
| 377 | + | let off = self.fetch(); | |
| 378 | + | if v != 0 { | |
| 379 | + | self.rel_jump(off); | |
| 380 | + | } | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | // 0xA5 is reserved and rejected before `execute` is called. | |
| 384 | + | _ => unreachable!("unhandled opcode {op:02X}"), | |
| 385 | + | } | |
| 386 | + | } | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | fn logic(op: u8, a: u8, b: u8) -> u8 { | |
| 390 | + | match op & 0xF0 { | |
| 391 | + | 0x40 => a | b, | |
| 392 | + | 0x50 => a & b, | |
| 393 | + | _ => a ^ b, | |
| 394 | + | } | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | #[cfg(test)] | |
| 398 | + | mod tests { | |
| 399 | + | use crate::asm::assemble; | |
| 400 | + | use crate::sim::{AC, CY, OV, PARITY, Sim}; | |
| 401 | + | use std::collections::HashSet; | |
| 402 | + | ||
| 403 | + | /// Assembles `src`, then steps until the PC reaches the `done` label. | |
| 404 | + | fn run(src: &str) -> Sim { | |
| 405 | + | let src = format!("{src}\ndone: SJMP done\n"); | |
| 406 | + | let out = assemble(&src); | |
| 407 | + | assert!(out.ok(), "{:?}", out.diagnostics); | |
| 408 | + | let done = out.symbols["DONE"].value as u16; | |
| 409 | + | let mut s = Sim::new(); | |
| 410 | + | s.load_code(out.bytes()); | |
| 411 | + | for _ in 0..100_000 { | |
| 412 | + | if s.pc == done { | |
| 413 | + | return s; | |
| 414 | + | } | |
| 415 | + | s.step().unwrap(); | |
| 416 | + | } | |
| 417 | + | panic!("program did not reach 'done'"); | |
| 418 | + | } | |
| 419 | + | ||
| 420 | + | #[test] | |
| 421 | + | fn add_flags() { | |
| 422 | + | let s = run("MOV A,#0x7F\nADD A,#1"); | |
| 423 | + | assert_eq!(s.acc(), 0x80); | |
| 424 | + | assert!(s.flag(OV) && s.flag(AC) && !s.flag(CY)); | |
| 425 | + | assert!(s.flag(PARITY)); | |
| 426 | + | let s = run("MOV A,#0xFF\nSETB C\nADDC A,#0"); | |
| 427 | + | assert_eq!(s.acc(), 0); | |
| 428 | + | assert!(s.flag(CY) && s.flag(AC) && !s.flag(OV)); | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | #[test] | |
| 432 | + | fn subb_flags() { | |
| 433 | + | let s = run("CLR C\nMOV A,#0x80\nSUBB A,#1"); | |
| 434 | + | assert_eq!(s.acc(), 0x7F); | |
| 435 | + | assert!(s.flag(OV) && s.flag(AC) && !s.flag(CY)); | |
| 436 | + | let s = run("SETB C\nMOV A,#5\nSUBB A,#5"); | |
| 437 | + | assert_eq!(s.acc(), 0xFF); | |
| 438 | + | assert!(s.flag(CY)); | |
| 439 | + | } | |
| 440 | + | ||
| 441 | + | #[test] | |
| 442 | + | fn decimal_adjust() { | |
| 443 | + | let s = run("MOV A,#0x56\nADD A,#0x67\nDA A"); | |
| 444 | + | assert_eq!(s.acc(), 0x23); | |
| 445 | + | assert!(s.flag(CY)); | |
| 446 | + | let s = run("MOV A,#0x19\nADD A,#0x28\nDA A"); | |
| 447 | + | assert_eq!(s.acc(), 0x47); | |
| 448 | + | assert!(!s.flag(CY)); | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | #[test] | |
| 452 | + | fn mul_div() { | |
| 453 | + | let s = run("MOV A,#80\nMOV B,#160\nMUL AB"); | |
| 454 | + | assert_eq!((s.acc(), s.b()), (0x00, 0x32)); | |
| 455 | + | assert!(s.flag(OV)); | |
| 456 | + | let s = run("MOV A,#251\nMOV B,#18\nDIV AB"); | |
| 457 | + | assert_eq!((s.acc(), s.b()), (13, 17)); | |
| 458 | + | let s = run("MOV B,#0\nDIV AB"); | |
| 459 | + | assert!(s.flag(OV)); | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | #[test] | |
| 463 | + | fn banks_and_indirect() { | |
| 464 | + | let s = run("MOV R0,#1\nSETB RS0\nMOV R0,#2\nMOV R1,#40h\nMOV @R1,#99\nMOV A,40h"); | |
| 465 | + | assert_eq!(s.iram[0], 1); | |
| 466 | + | assert_eq!(s.iram[8], 2); | |
| 467 | + | assert_eq!(s.acc(), 99); | |
| 468 | + | assert_eq!(s.bank(), 1); | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | #[test] | |
| 472 | + | fn stack_and_calls() { | |
| 473 | + | let s = run("MOV A,#5\nPUSH ACC\nMOV A,#0\nPOP B\nCALL sub\nSJMP done\nsub: INC A\nRET"); | |
| 474 | + | assert_eq!(s.b(), 5); | |
| 475 | + | assert_eq!(s.acc(), 1); | |
| 476 | + | assert_eq!(s.sp(), 7); | |
| 477 | + | } | |
| 478 | + | ||
| 479 | + | #[test] | |
| 480 | + | fn loops_and_compares() { | |
| 481 | + | let s = run("MOV R2,#10\nCLR A\nl: ADD A,#2\nDJNZ R2,l"); | |
| 482 | + | assert_eq!(s.acc(), 20); | |
| 483 | + | let s = run("MOV A,#3\nCJNE A,#5,ne\nne: NOP"); | |
| 484 | + | assert!(s.flag(CY)); | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | #[test] | |
| 488 | + | fn bits() { | |
| 489 | + | let s = run("SETB 20h.3\nMOV C,20h.3\nCPL ACC.0\nANL C,/ACC.0\nMOV 21h.0,C"); | |
| 490 | + | assert_eq!(s.iram[0x20], 0x08); | |
| 491 | + | assert_eq!(s.acc(), 1); | |
| 492 | + | assert_eq!(s.iram[0x21], 0); | |
| 493 | + | let s = run("SETB 20h.1\nJBC 20h.1,x\nx: NOP"); | |
| 494 | + | assert_eq!(s.iram[0x20], 0); | |
| 495 | + | } | |
| 496 | + | ||
| 497 | + | #[test] | |
| 498 | + | fn movc_movx_xchd() { | |
| 499 | + | let s = run( | |
| 500 | + | "MOV DPTR,#tbl\nMOV A,#2\nMOVC A,@A+DPTR\nSJMP go\ntbl: DB 10,20,30\ngo: MOV DPTR,#1234h\nMOVX @DPTR,A\nMOV R0,#30h\nMOV @R0,#0x5A\nMOV A,#0xC3\nXCHD A,@R0", | |
| 501 | + | ); | |
| 502 | + | assert_eq!(s.xram[0x1234], 30); | |
| 503 | + | assert_eq!(s.acc(), 0xCA); | |
| 504 | + | assert_eq!(s.iram[0x30], 0x53); | |
| 505 | + | } | |
| 506 | + | ||
| 507 | + | #[test] | |
| 508 | + | fn ports_read_pins_but_rmw_reads_latch() { | |
| 509 | + | let mut s = run("MOV P1,#0FFh"); | |
| 510 | + | s.pins[1] = 0xF0; | |
| 511 | + | assert_eq!(s.read_direct(0x90, false), 0xF0); | |
| 512 | + | assert_eq!(s.read_direct(0x90, true), 0xFF); | |
| 513 | + | } | |
| 514 | + | ||
| 515 | + | #[test] | |
| 516 | + | fn step_over_and_out() { | |
| 517 | + | let out = assemble("CALL f\nNOP\nSJMP $\nf: MOV A,#1\nMOV A,#2\nRET"); | |
| 518 | + | let mut s = Sim::new(); | |
| 519 | + | s.load_code(out.bytes()); | |
| 520 | + | let none = HashSet::new(); | |
| 521 | + | s.step_over(1000, &none); | |
| 522 | + | assert_eq!((s.pc, s.acc()), (3, 2)); | |
| 523 | + | s.reset(); | |
| 524 | + | s.step().unwrap(); // into f | |
| 525 | + | s.step_out(1000, &none); | |
| 526 | + | assert_eq!(s.pc, 3); | |
| 527 | + | } | |
| 528 | + | } | |
| @@ -0,0 +1,91 @@ | |||
| 1 | + | use crate::isa::{self, Operand}; | |
| 2 | + | use crate::sfr::{format_bit, format_direct, hex8, hex16}; | |
| 3 | + | ||
| 4 | + | pub struct Disasm { | |
| 5 | + | pub text: String, | |
| 6 | + | pub size: u8, | |
| 7 | + | } | |
| 8 | + | ||
| 9 | + | /// Disassembles the instruction at `addr`. | |
| 10 | + | pub fn disassemble(code: &[u8; 0x10000], addr: u16) -> Disasm { | |
| 11 | + | let byte = |i: u16| code[addr.wrapping_add(i) as usize]; | |
| 12 | + | let op = byte(0); | |
| 13 | + | let Some(info) = isa::info(op) else { | |
| 14 | + | return Disasm { | |
| 15 | + | text: format!("DB {}", hex8(op)), | |
| 16 | + | size: 1, | |
| 17 | + | }; | |
| 18 | + | }; | |
| 19 | + | let next = addr.wrapping_add(info.size as u16); | |
| 20 | + | let mut pos = 1u16; | |
| 21 | + | let mut parts: Vec<String> = Vec::new(); | |
| 22 | + | for &o in info.operands { | |
| 23 | + | let s = match o { | |
| 24 | + | Operand::A => "A".into(), | |
| 25 | + | Operand::AB => "AB".into(), | |
| 26 | + | Operand::C => "C".into(), | |
| 27 | + | Operand::Dptr => "DPTR".into(), | |
| 28 | + | Operand::Reg(n) => format!("R{n}"), | |
| 29 | + | Operand::AtReg(n) => format!("@R{n}"), | |
| 30 | + | Operand::AtDptr => "@DPTR".into(), | |
| 31 | + | Operand::AtAPlusDptr => "@A+DPTR".into(), | |
| 32 | + | Operand::AtAPlusPc => "@A+PC".into(), | |
| 33 | + | Operand::Imm8 => format!("#{}", hex8(byte(pos))), | |
| 34 | + | Operand::Imm16 => format!("#{}", hex16(u16::from_be_bytes([byte(pos), byte(pos + 1)]))), | |
| 35 | + | Operand::Direct => format_direct(byte(pos)), | |
| 36 | + | Operand::Bit => format_bit(byte(pos)), | |
| 37 | + | Operand::NotBit => format!("/{}", format_bit(byte(pos))), | |
| 38 | + | Operand::Addr11 => { | |
| 39 | + | hex16((next & 0xF800) | (((op >> 5) as u16) << 8) | byte(pos) as u16) | |
| 40 | + | } | |
| 41 | + | Operand::Addr16 => hex16(u16::from_be_bytes([byte(pos), byte(pos + 1)])), | |
| 42 | + | Operand::Rel => hex16(next.wrapping_add(byte(pos) as i8 as u16)), | |
| 43 | + | }; | |
| 44 | + | pos += o.len() as u16; | |
| 45 | + | parts.push(s); | |
| 46 | + | } | |
| 47 | + | if op == 0x85 { | |
| 48 | + | // Encoded as source, destination. | |
| 49 | + | parts.swap(0, 1); | |
| 50 | + | } | |
| 51 | + | let text = if parts.is_empty() { | |
| 52 | + | info.mnemonic.to_string() | |
| 53 | + | } else { | |
| 54 | + | format!("{:<5} {}", info.mnemonic, parts.join(",")) | |
| 55 | + | }; | |
| 56 | + | Disasm { | |
| 57 | + | text, | |
| 58 | + | size: info.size, | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | #[cfg(test)] | |
| 63 | + | mod tests { | |
| 64 | + | use super::*; | |
| 65 | + | use crate::asm::assemble; | |
| 66 | + | ||
| 67 | + | /// Disassembling and re-assembling every opcode gives the same bytes. | |
| 68 | + | #[test] | |
| 69 | + | fn round_trips_every_opcode() { | |
| 70 | + | for op in 0..=255u8 { | |
| 71 | + | if op == 0xA5 { | |
| 72 | + | continue; | |
| 73 | + | } | |
| 74 | + | let mut code = Box::new([0u8; 0x10000]); | |
| 75 | + | code[0x100] = op; | |
| 76 | + | code[0x101] = 0x30; | |
| 77 | + | code[0x102] = 0x40; | |
| 78 | + | let d = disassemble(&code, 0x100); | |
| 79 | + | let src = format!("ORG 100h\n{}", d.text); | |
| 80 | + | let out = assemble(&src); | |
| 81 | + | assert!(out.ok(), "{op:02X} '{}': {:?}", d.text, out.diagnostics); | |
| 82 | + | let bytes: Vec<u8> = out.bytes().map(|b| b.1).collect(); | |
| 83 | + | assert_eq!( | |
| 84 | + | bytes, | |
| 85 | + | code[0x100..0x100 + d.size as usize], | |
| 86 | + | "opcode {op:02X} '{}'", | |
| 87 | + | d.text | |
| 88 | + | ); | |
| 89 | + | } | |
| 90 | + | } | |
| 91 | + | } | |
| @@ -0,0 +1,449 @@ | |||
| 1 | + | //! Intel 8051 simulator: CPU core, memories and on-chip peripherals. | |
| 2 | + | ||
| 3 | + | mod cpu; | |
| 4 | + | pub mod disasm; | |
| 5 | + | mod periph; | |
| 6 | + | ||
| 7 | + | use std::collections::{HashSet, VecDeque}; | |
| 8 | + | ||
| 9 | + | use crate::isa; | |
| 10 | + | use crate::sfr::{self, ACC, B, DPH, DPL, IE, IP, P0, P1, P2, P3, PCON, PSW, SBUF, SP}; | |
| 11 | + | pub use periph::Uart; | |
| 12 | + | ||
| 13 | + | pub const CY: u8 = 0x80; | |
| 14 | + | pub const AC: u8 = 0x40; | |
| 15 | + | pub const OV: u8 = 0x04; | |
| 16 | + | pub const PARITY: u8 = 0x01; | |
| 17 | + | ||
| 18 | + | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 19 | + | pub enum Stop { | |
| 20 | + | /// Ran the requested number of instructions / cycle budget. | |
| 21 | + | Budget, | |
| 22 | + | Breakpoint(u16), | |
| 23 | + | /// A step-over / step-out target was reached. | |
| 24 | + | Target, | |
| 25 | + | ReservedOpcode { | |
| 26 | + | pc: u16, | |
| 27 | + | opcode: u8, | |
| 28 | + | }, | |
| 29 | + | PowerDown, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | pub struct Sim { | |
| 33 | + | pub code: Box<[u8; 0x10000]>, | |
| 34 | + | /// Internal RAM. Indirect addressing reaches all 256 bytes (8052-style | |
| 35 | + | /// upper 128); direct addresses 80h-FFh go to the SFRs instead. | |
| 36 | + | pub iram: [u8; 256], | |
| 37 | + | /// SFR space 80h-FFh, indexed by `addr - 0x80`. | |
| 38 | + | pub sfr: [u8; 128], | |
| 39 | + | pub xram: Box<[u8; 0x10000]>, | |
| 40 | + | pub pc: u16, | |
| 41 | + | /// Machine cycles since reset. | |
| 42 | + | pub cycles: u64, | |
| 43 | + | /// Levels driven onto the port pins from outside (1 = released/high). | |
| 44 | + | /// A pin reads as `latch & external`. | |
| 45 | + | pub pins: [u8; 4], | |
| 46 | + | pub uart: Uart, | |
| 47 | + | pub idle: bool, | |
| 48 | + | pub power_down: bool, | |
| 49 | + | /// Priority levels of interrupts currently in service (innermost last). | |
| 50 | + | pub in_service: Vec<u8>, | |
| 51 | + | /// Vector of the last interrupt taken, for the UI log. | |
| 52 | + | pub last_irq: Option<u16>, | |
| 53 | + | /// RETI or a write to IE/IP delays interrupt acceptance by one instruction. | |
| 54 | + | irq_hold: bool, | |
| 55 | + | /// Pin levels of P3 in the previous cycle, for edge detection. | |
| 56 | + | prev_p3: u8, | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | impl Default for Sim { | |
| 60 | + | fn default() -> Self { | |
| 61 | + | Self::new() | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | impl Sim { | |
| 66 | + | pub fn new() -> Self { | |
| 67 | + | let mut s = Self { | |
| 68 | + | code: Box::new([0; 0x10000]), | |
| 69 | + | iram: [0; 256], | |
| 70 | + | sfr: [0; 128], | |
| 71 | + | xram: Box::new([0; 0x10000]), | |
| 72 | + | pc: 0, | |
| 73 | + | cycles: 0, | |
| 74 | + | pins: [0xFF; 4], | |
| 75 | + | uart: Uart::default(), | |
| 76 | + | idle: false, | |
| 77 | + | power_down: false, | |
| 78 | + | in_service: Vec::new(), | |
| 79 | + | last_irq: None, | |
| 80 | + | irq_hold: false, | |
| 81 | + | prev_p3: 0xFF, | |
| 82 | + | }; | |
| 83 | + | s.reset(); | |
| 84 | + | s | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | /// Hardware reset: SFRs to their reset values, PC to 0. Code and XRAM | |
| 88 | + | /// are kept; IRAM is cleared so runs are reproducible. | |
| 89 | + | pub fn reset(&mut self) { | |
| 90 | + | self.iram = [0; 256]; | |
| 91 | + | self.sfr = [0; 128]; | |
| 92 | + | for p in [P0, P1, P2, P3] { | |
| 93 | + | self.set_sfr(p, 0xFF); | |
| 94 | + | } | |
| 95 | + | self.set_sfr(SP, 0x07); | |
| 96 | + | self.pc = 0; | |
| 97 | + | self.cycles = 0; | |
| 98 | + | self.uart.reset(); | |
| 99 | + | self.idle = false; | |
| 100 | + | self.power_down = false; | |
| 101 | + | self.in_service.clear(); | |
| 102 | + | self.last_irq = None; | |
| 103 | + | self.irq_hold = false; | |
| 104 | + | self.prev_p3 = self.port_pins(3); | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | pub fn load_code(&mut self, bytes: impl Iterator<Item = (u16, u8)>) { | |
| 108 | + | self.code.fill(0); | |
| 109 | + | for (a, b) in bytes { | |
| 110 | + | self.code[a as usize] = b; | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // ---- Raw register access ---- | |
| 115 | + | ||
| 116 | + | pub fn sfr(&self, addr: u8) -> u8 { | |
| 117 | + | self.sfr[(addr & 0x7F) as usize] | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | pub fn set_sfr(&mut self, addr: u8, v: u8) { | |
| 121 | + | self.sfr[(addr & 0x7F) as usize] = v; | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | pub fn acc(&self) -> u8 { | |
| 125 | + | self.sfr(ACC) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | pub fn set_acc(&mut self, v: u8) { | |
| 129 | + | self.set_sfr(ACC, v); | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | pub fn b(&self) -> u8 { | |
| 133 | + | self.sfr(B) | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | pub fn psw(&self) -> u8 { | |
| 137 | + | self.sfr(PSW) | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | pub fn sp(&self) -> u8 { | |
| 141 | + | self.sfr(SP) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | pub fn dptr(&self) -> u16 { | |
| 145 | + | u16::from_be_bytes([self.sfr(DPH), self.sfr(DPL)]) | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | pub fn set_dptr(&mut self, v: u16) { | |
| 149 | + | let [h, l] = v.to_be_bytes(); | |
| 150 | + | self.set_sfr(DPH, h); | |
| 151 | + | self.set_sfr(DPL, l); | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | pub fn flag(&self, mask: u8) -> bool { | |
| 155 | + | self.psw() & mask != 0 | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | pub fn set_flag(&mut self, mask: u8, on: bool) { | |
| 159 | + | let p = self.psw(); | |
| 160 | + | self.set_sfr(PSW, if on { p | mask } else { p & !mask }); | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | pub fn bank(&self) -> u8 { | |
| 164 | + | (self.psw() >> 3) & 3 | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | /// IRAM address of register Rn in the active bank. | |
| 168 | + | pub fn reg_addr(&self, n: u8) -> u8 { | |
| 169 | + | self.bank() * 8 + n | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | pub fn reg(&self, n: u8) -> u8 { | |
| 173 | + | self.iram[self.reg_addr(n) as usize] | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | /// Pin levels of port `n`: latch AND externally driven level. | |
| 177 | + | pub fn port_pins(&self, n: usize) -> u8 { | |
| 178 | + | self.sfr(P0 + 0x10 * n as u8) & self.pins[n] | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // ---- Addressing modes ---- | |
| 182 | + | ||
| 183 | + | /// Reads a direct address. Ports read their pins, except for | |
| 184 | + | /// read-modify-write instructions (`rmw`) which read the latch. | |
| 185 | + | pub fn read_direct(&self, addr: u8, rmw: bool) -> u8 { | |
| 186 | + | match addr { | |
| 187 | + | 0x00..=0x7F => self.iram[addr as usize], | |
| 188 | + | P0 | P1 | P2 | P3 if !rmw => self.port_pins(((addr - P0) >> 4) as usize), | |
| 189 | + | SBUF => self.uart.sbuf_rx, | |
| 190 | + | _ => self.sfr(addr), | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | pub fn write_direct(&mut self, addr: u8, v: u8) { | |
| 195 | + | match addr { | |
| 196 | + | 0x00..=0x7F => self.iram[addr as usize] = v, | |
| 197 | + | SBUF => self.uart_transmit(v), | |
| 198 | + | IE | IP => { | |
| 199 | + | self.set_sfr(addr, v); | |
| 200 | + | self.irq_hold = true; | |
| 201 | + | } | |
| 202 | + | PCON => { | |
| 203 | + | // IDL (bit 0) and PD (bit 1) take effect immediately. | |
| 204 | + | self.set_sfr(addr, v); | |
| 205 | + | self.power_down |= v & 0x02 != 0; | |
| 206 | + | self.idle |= v & 0x01 != 0; | |
| 207 | + | } | |
| 208 | + | _ => self.set_sfr(addr, v), | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | pub fn read_bit(&self, bit: u8, rmw: bool) -> bool { | |
| 213 | + | let (byte, idx) = sfr::bit_location(bit); | |
| 214 | + | self.read_direct(byte, rmw) & (1 << idx) != 0 | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | pub fn write_bit(&mut self, bit: u8, on: bool) { | |
| 218 | + | let (byte, idx) = sfr::bit_location(bit); | |
| 219 | + | let v = self.read_direct(byte, true); | |
| 220 | + | self.write_direct(byte, if on { v | (1 << idx) } else { v & !(1 << idx) }); | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | pub fn push(&mut self, v: u8) { | |
| 224 | + | let sp = self.sp().wrapping_add(1); | |
| 225 | + | self.set_sfr(SP, sp); | |
| 226 | + | self.iram[sp as usize] = v; | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | pub fn pop(&mut self) -> u8 { | |
| 230 | + | let sp = self.sp(); | |
| 231 | + | self.set_sfr(SP, sp.wrapping_sub(1)); | |
| 232 | + | self.iram[sp as usize] | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | pub fn push_pc(&mut self) { | |
| 236 | + | let [h, l] = self.pc.to_be_bytes(); | |
| 237 | + | self.push(l); | |
| 238 | + | self.push(h); | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | pub fn pop_pc(&mut self) { | |
| 242 | + | let h = self.pop(); | |
| 243 | + | let l = self.pop(); | |
| 244 | + | self.pc = u16::from_be_bytes([h, l]); | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // ---- Execution control ---- | |
| 248 | + | ||
| 249 | + | /// Executes one instruction (or one idle cycle), then services interrupts. | |
| 250 | + | pub fn step(&mut self) -> Result<(), Stop> { | |
| 251 | + | if self.power_down { | |
| 252 | + | return Err(Stop::PowerDown); | |
| 253 | + | } | |
| 254 | + | if self.idle { | |
| 255 | + | self.tick(1); | |
| 256 | + | } else { | |
| 257 | + | let pc = self.pc; | |
| 258 | + | let opcode = self.code[pc as usize]; | |
| 259 | + | let Some(info) = isa::info(opcode) else { | |
| 260 | + | return Err(Stop::ReservedOpcode { pc, opcode }); | |
| 261 | + | }; | |
| 262 | + | self.pc = pc.wrapping_add(1); | |
| 263 | + | self.execute(opcode); | |
| 264 | + | let p = self.acc().count_ones() % 2 == 1; | |
| 265 | + | self.set_flag(PARITY, p); | |
| 266 | + | self.tick(info.cycles as u32); | |
| 267 | + | } | |
| 268 | + | self.service_interrupts(); | |
| 269 | + | Ok(()) | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | fn current_call_len(&self) -> Option<u16> { | |
| 273 | + | let op = self.code[self.pc as usize]; | |
| 274 | + | match op { | |
| 275 | + | 0x12 => Some(3), | |
| 276 | + | _ if op & 0x1F == 0x11 => Some(2), | |
| 277 | + | _ => None, | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | /// Runs up to `max_steps` instructions, stopping at breakpoints. | |
| 282 | + | /// `resume` ignores a breakpoint at the starting PC, so execution can | |
| 283 | + | /// continue from the breakpoint it last stopped at. | |
| 284 | + | pub fn run(&mut self, max_steps: u32, breakpoints: &HashSet<u16>, resume: bool) -> Stop { | |
| 285 | + | for i in 0..max_steps { | |
| 286 | + | if (i > 0 || !resume) && !self.idle && breakpoints.contains(&self.pc) { | |
| 287 | + | return Stop::Breakpoint(self.pc); | |
| 288 | + | } | |
| 289 | + | if let Err(s) = self.step() { | |
| 290 | + | return s; | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | Stop::Budget | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | /// Like `step`, but executes a whole ACALL/LCALL subroutine. | |
| 297 | + | pub fn step_over(&mut self, max_steps: u32, breakpoints: &HashSet<u16>) -> Stop { | |
| 298 | + | match self.current_call_len() { | |
| 299 | + | Some(len) => { | |
| 300 | + | let target = self.pc.wrapping_add(len); | |
| 301 | + | let sp = self.sp(); | |
| 302 | + | self.run_until(max_steps, breakpoints, |s| s.pc == target && s.sp() == sp) | |
| 303 | + | } | |
| 304 | + | None => match self.step() { | |
| 305 | + | Ok(()) => Stop::Target, | |
| 306 | + | Err(s) => s, | |
| 307 | + | }, | |
| 308 | + | } | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | /// Runs until the current subroutine (or interrupt handler) returns. | |
| 312 | + | pub fn step_out(&mut self, max_steps: u32, breakpoints: &HashSet<u16>) -> Stop { | |
| 313 | + | let sp = self.sp(); | |
| 314 | + | let mut returning = false; | |
| 315 | + | self.run_until_with(max_steps, breakpoints, |s, before| { | |
| 316 | + | if before { | |
| 317 | + | let op = s.code[s.pc as usize]; | |
| 318 | + | returning = (op == 0x22 || op == 0x32) && s.sp() <= sp && !s.idle; | |
| 319 | + | false | |
| 320 | + | } else { | |
| 321 | + | returning | |
| 322 | + | } | |
| 323 | + | }) | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | fn run_until( | |
| 327 | + | &mut self, | |
| 328 | + | max_steps: u32, | |
| 329 | + | bps: &HashSet<u16>, | |
| 330 | + | mut done: impl FnMut(&Sim) -> bool, | |
| 331 | + | ) -> Stop { | |
| 332 | + | self.run_until_with(max_steps, bps, |s, before| !before && done(s)) | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | /// `done(sim, before)` is called before (`true`) and after (`false`) each step. | |
| 336 | + | fn run_until_with( | |
| 337 | + | &mut self, | |
| 338 | + | max_steps: u32, | |
| 339 | + | bps: &HashSet<u16>, | |
| 340 | + | mut done: impl FnMut(&Sim, bool) -> bool, | |
| 341 | + | ) -> Stop { | |
| 342 | + | for i in 0..max_steps { | |
| 343 | + | if i > 0 && !self.idle && bps.contains(&self.pc) { | |
| 344 | + | return Stop::Breakpoint(self.pc); | |
| 345 | + | } | |
| 346 | + | done(self, true); | |
| 347 | + | if let Err(s) = self.step() { | |
| 348 | + | return s; | |
| 349 | + | } | |
| 350 | + | if done(self, false) { | |
| 351 | + | return Stop::Target; | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | Stop::Budget | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | /// Queue bytes for the UART receiver, as if typed on a serial terminal. | |
| 358 | + | pub fn uart_send(&mut self, bytes: &[u8]) { | |
| 359 | + | self.uart.rx_queue.extend(bytes.iter().copied()); | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | pub fn uart_rx_pending(&self) -> &VecDeque<u8> { | |
| 363 | + | &self.uart.rx_queue | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | #[cfg(test)] | |
| 368 | + | mod example_tests { | |
| 369 | + | use super::Sim; | |
| 370 | + | use crate::asm::assemble; | |
| 371 | + | ||
| 372 | + | fn load(src: &str) -> Sim { | |
| 373 | + | let out = assemble(src); | |
| 374 | + | assert!(out.ok(), "{:?}", out.diagnostics); | |
| 375 | + | let mut s = Sim::new(); | |
| 376 | + | s.load_code(out.bytes()); | |
| 377 | + | s | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | fn steps(s: &mut Sim, n: usize) { | |
| 381 | + | for _ in 0..n { | |
| 382 | + | s.step().unwrap(); | |
| 383 | + | } | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | #[test] | |
| 387 | + | fn serial_hello_greets_and_echoes() { | |
| 388 | + | let mut s = load(include_str!("../../examples/serial_hello.asm")); | |
| 389 | + | steps(&mut s, 60_000); | |
| 390 | + | assert_eq!( | |
| 391 | + | String::from_utf8_lossy(&s.uart.tx_log), | |
| 392 | + | "Hello from the 8051!\r\nType something: " | |
| 393 | + | ); | |
| 394 | + | s.uart_send(b"hi\r"); | |
| 395 | + | steps(&mut s, 20_000); | |
| 396 | + | assert!(String::from_utf8_lossy(&s.uart.tx_log).ends_with("hi\r\n")); | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | #[test] | |
| 400 | + | fn timer_irq_ticks() { | |
| 401 | + | let mut s = load(include_str!("../../examples/timer_irq.asm")); | |
| 402 | + | steps(&mut s, 5_000); | |
| 403 | + | // ~10000 cycles at one interrupt per 1000 cycles. | |
| 404 | + | assert!((3..=10).contains(&s.reg(7)), "R7 = {}", s.reg(7)); | |
| 405 | + | } | |
| 406 | + | ||
| 407 | + | #[test] | |
| 408 | + | fn button_counts_edges_while_idle() { | |
| 409 | + | let mut s = load(include_str!("../../examples/button_int0.asm")); | |
| 410 | + | steps(&mut s, 20); | |
| 411 | + | assert!(s.idle); | |
| 412 | + | for _ in 0..3 { | |
| 413 | + | s.pins[3] &= !0x04; | |
| 414 | + | steps(&mut s, 10); | |
| 415 | + | s.pins[3] |= 0x04; | |
| 416 | + | steps(&mut s, 10); | |
| 417 | + | } | |
| 418 | + | assert_eq!(s.sfr(crate::sfr::P1), 3); | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | #[test] | |
| 422 | + | fn bcd_and_lookup() { | |
| 423 | + | let mut s = load(include_str!("../../examples/bcd_add.asm")); | |
| 424 | + | steps(&mut s, 20); | |
| 425 | + | assert_eq!((s.iram[0x31], s.iram[0x30]), (0x69, 0x12)); | |
| 426 | + | let mut s = load(include_str!("../../examples/lookup_movc.asm")); | |
| 427 | + | steps(&mut s, 3 + 4 * 5 + 3); // setup, digits 0-3, then up to MOV P2,A | |
| 428 | + | assert_eq!(s.sfr(crate::sfr::P2), 0x66); // digit 4 | |
| 429 | + | } | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | #[cfg(test)] | |
| 433 | + | mod run_tests { | |
| 434 | + | use super::{Sim, Stop}; | |
| 435 | + | use crate::asm::assemble; | |
| 436 | + | use std::collections::HashSet; | |
| 437 | + | ||
| 438 | + | #[test] | |
| 439 | + | fn breakpoint_at_chunk_boundary_is_hit() { | |
| 440 | + | let out = assemble("l: NOP\nNOP\nSJMP l"); | |
| 441 | + | let mut s = Sim::new(); | |
| 442 | + | s.load_code(out.bytes()); | |
| 443 | + | let bps = HashSet::from([1u16]); | |
| 444 | + | assert_eq!(s.run(1, &bps, true), Stop::Budget); // stops at PC 1 by budget | |
| 445 | + | assert_eq!(s.run(10, &bps, false), Stop::Breakpoint(1)); // next chunk must not skip it | |
| 446 | + | assert_eq!(s.run(10, &bps, true), Stop::Breakpoint(1)); // resuming loops back to it | |
| 447 | + | assert_eq!(s.pc, 1); | |
| 448 | + | } | |
| 449 | + | } | |
| @@ -0,0 +1,403 @@ | |||
| 1 | + | //! Timers 0/1, the UART and interrupt dispatch, advanced per machine cycle. | |
| 2 | + | ||
| 3 | + | use std::collections::VecDeque; | |
| 4 | + | ||
| 5 | + | use super::Sim; | |
| 6 | + | use crate::sfr::{IE, IP, PCON, SCON, TCON, TH0, TH1, TL0, TL1, TMOD}; | |
| 7 | + | ||
| 8 | + | // TCON bits | |
| 9 | + | const TF1: u8 = 0x80; | |
| 10 | + | const TR1: u8 = 0x40; | |
| 11 | + | const TF0: u8 = 0x20; | |
| 12 | + | const TR0: u8 = 0x10; | |
| 13 | + | const IE1: u8 = 0x08; | |
| 14 | + | const IT1: u8 = 0x04; | |
| 15 | + | const IE0: u8 = 0x02; | |
| 16 | + | const IT0: u8 = 0x01; | |
| 17 | + | // SCON bits | |
| 18 | + | const SM2: u8 = 0x20; | |
| 19 | + | const REN: u8 = 0x10; | |
| 20 | + | const RB8: u8 = 0x04; | |
| 21 | + | const TI: u8 = 0x02; | |
| 22 | + | const RI: u8 = 0x01; | |
| 23 | + | // P3 pins | |
| 24 | + | const INT0_PIN: u8 = 0x04; | |
| 25 | + | const INT1_PIN: u8 = 0x08; | |
| 26 | + | const T0_PIN: u8 = 0x10; | |
| 27 | + | const T1_PIN: u8 = 0x20; | |
| 28 | + | ||
| 29 | + | #[derive(Debug, Clone, Copy)] | |
| 30 | + | struct Shift { | |
| 31 | + | byte: u8, | |
| 32 | + | /// Remaining time in the mode's unit (see `Sim::uart_units`). | |
| 33 | + | remaining: u32, | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | #[derive(Debug, Default)] | |
| 37 | + | pub struct Uart { | |
| 38 | + | /// Everything the 8051 has transmitted. | |
| 39 | + | pub tx_log: Vec<u8>, | |
| 40 | + | /// Bytes waiting to be received by the 8051. | |
| 41 | + | pub rx_queue: VecDeque<u8>, | |
| 42 | + | /// The receive half of SBUF. | |
| 43 | + | pub sbuf_rx: u8, | |
| 44 | + | tx: Option<Shift>, | |
| 45 | + | rx: Option<Shift>, | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | impl Uart { | |
| 49 | + | /// Clears the shift registers and the TX log; queued input is kept. | |
| 50 | + | pub fn reset(&mut self) { | |
| 51 | + | self.tx_log.clear(); | |
| 52 | + | self.sbuf_rx = 0; | |
| 53 | + | self.tx = None; | |
| 54 | + | self.rx = None; | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | pub fn transmitting(&self) -> bool { | |
| 58 | + | self.tx.is_some() | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | /// Interrupt sources in hardware polling order: (flag test, IE/IP bit, vector). | |
| 63 | + | const SOURCES: [(u8, u16); 5] = [ | |
| 64 | + | (0x01, 0x03), | |
| 65 | + | (0x02, 0x0B), | |
| 66 | + | (0x04, 0x13), | |
| 67 | + | (0x08, 0x1B), | |
| 68 | + | (0x10, 0x23), | |
| 69 | + | ]; | |
| 70 | + | ||
| 71 | + | impl Sim { | |
| 72 | + | pub(super) fn tick(&mut self, cycles: u32) { | |
| 73 | + | for _ in 0..cycles { | |
| 74 | + | self.cycles += 1; | |
| 75 | + | let p3 = self.port_pins(3); | |
| 76 | + | let fell = self.prev_p3 & !p3; | |
| 77 | + | self.prev_p3 = p3; | |
| 78 | + | self.external_interrupts(p3, fell); | |
| 79 | + | let t1_overflows = self.timers(p3, fell); | |
| 80 | + | self.uart_tick(t1_overflows); | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | fn external_interrupts(&mut self, p3: u8, fell: u8) { | |
| 85 | + | let mut tcon = self.sfr(TCON); | |
| 86 | + | for (pin, it, ie) in [(INT0_PIN, IT0, IE0), (INT1_PIN, IT1, IE1)] { | |
| 87 | + | if tcon & it != 0 { | |
| 88 | + | if fell & pin != 0 { | |
| 89 | + | tcon |= ie; | |
| 90 | + | } | |
| 91 | + | } else if p3 & pin == 0 { | |
| 92 | + | tcon |= ie; | |
| 93 | + | } else { | |
| 94 | + | tcon &= !ie; | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | self.set_sfr(TCON, tcon); | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | /// Advances both timers by one machine cycle. Returns the number of | |
| 101 | + | /// timer 1 overflows (the UART baud clock in modes 1 and 3). | |
| 102 | + | fn timers(&mut self, p3: u8, fell: u8) -> u32 { | |
| 103 | + | let tmod = self.sfr(TMOD); | |
| 104 | + | let mut tcon = self.sfr(TCON); | |
| 105 | + | let t0_mode = tmod & 0x03; | |
| 106 | + | let mut t1_overflows = 0; | |
| 107 | + | ||
| 108 | + | for t in 0..2u8 { | |
| 109 | + | let ctl = (tmod >> (4 * t)) & 0x0F; | |
| 110 | + | let mode = ctl & 0x03; | |
| 111 | + | let (tr, int_pin, count_pin) = if t == 0 { | |
| 112 | + | (TR0, INT0_PIN, T0_PIN) | |
| 113 | + | } else { | |
| 114 | + | (TR1, INT1_PIN, T1_PIN) | |
| 115 | + | }; | |
| 116 | + | let gate_ok = ctl & 0x08 == 0 || p3 & int_pin != 0; | |
| 117 | + | let running = tcon & tr != 0 && gate_ok; | |
| 118 | + | let event = if ctl & 0x04 != 0 { | |
| 119 | + | fell & count_pin != 0 | |
| 120 | + | } else { | |
| 121 | + | true | |
| 122 | + | }; | |
| 123 | + | ||
| 124 | + | // Timer 1 is stopped in mode 3; timer 0 mode 3 also clocks TH0 from TR1. | |
| 125 | + | if t == 1 && mode == 3 { | |
| 126 | + | continue; | |
| 127 | + | } | |
| 128 | + | let (tl_addr, th_addr) = if t == 0 { (TL0, TH0) } else { (TL1, TH1) }; | |
| 129 | + | if t == 0 && mode == 3 && tcon & TR1 != 0 { | |
| 130 | + | let th = self.sfr(TH0).wrapping_add(1); | |
| 131 | + | self.set_sfr(TH0, th); | |
| 132 | + | if th == 0 { | |
| 133 | + | tcon |= TF1; | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | if !(running && event) { | |
| 137 | + | continue; | |
| 138 | + | } | |
| 139 | + | let (tl, th) = (self.sfr(tl_addr), self.sfr(th_addr)); | |
| 140 | + | let (tl, th, overflow) = match mode { | |
| 141 | + | 0 => { | |
| 142 | + | let low = (tl.wrapping_add(1)) & 0x1F; | |
| 143 | + | let th2 = if low == 0 { th.wrapping_add(1) } else { th }; | |
| 144 | + | ((tl & 0xE0) | low, th2, low == 0 && th2 == 0) | |
| 145 | + | } | |
| 146 | + | 1 => { | |
| 147 | + | let v = u16::from_be_bytes([th, tl]).wrapping_add(1); | |
| 148 | + | let [h, l] = v.to_be_bytes(); | |
| 149 | + | (l, h, v == 0) | |
| 150 | + | } | |
| 151 | + | 2 => { | |
| 152 | + | let l = tl.wrapping_add(1); | |
| 153 | + | if l == 0 { | |
| 154 | + | (th, th, true) | |
| 155 | + | } else { | |
| 156 | + | (l, th, false) | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | _ => { | |
| 160 | + | // Timer 0 mode 3: TL0 is an 8-bit timer. | |
| 161 | + | let l = tl.wrapping_add(1); | |
| 162 | + | (l, th, l == 0) | |
| 163 | + | } | |
| 164 | + | }; | |
| 165 | + | self.set_sfr(tl_addr, tl); | |
| 166 | + | self.set_sfr(th_addr, th); | |
| 167 | + | if overflow { | |
| 168 | + | if t == 1 { | |
| 169 | + | t1_overflows += 1; | |
| 170 | + | // With timer 0 in mode 3, TF1 belongs to TH0. | |
| 171 | + | if t0_mode != 3 { | |
| 172 | + | tcon |= TF1; | |
| 173 | + | } | |
| 174 | + | } else { | |
| 175 | + | tcon |= TF0; | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | self.set_sfr(TCON, tcon); | |
| 180 | + | t1_overflows | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | /// Frame length of the current serial mode, in the units that | |
| 184 | + | /// `uart_tick` counts: machine cycles (mode 0), oscillator periods | |
| 185 | + | /// (mode 2) or timer 1 overflows (modes 1 and 3). | |
| 186 | + | fn uart_frame(&self) -> u32 { | |
| 187 | + | let smod = self.sfr(PCON) & 0x80 != 0; | |
| 188 | + | let bit = |base: u32| if smod { base / 2 } else { base }; | |
| 189 | + | match self.sfr(SCON) >> 6 { | |
| 190 | + | 0 => 8, | |
| 191 | + | 1 => 10 * bit(32), | |
| 192 | + | 2 => 11 * bit(64), | |
| 193 | + | _ => 11 * bit(32), | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | pub(super) fn uart_transmit(&mut self, byte: u8) { | |
| 198 | + | let remaining = self.uart_frame(); | |
| 199 | + | self.uart.tx = Some(Shift { byte, remaining }); | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | fn uart_tick(&mut self, t1_overflows: u32) { | |
| 203 | + | let mode = self.sfr(SCON) >> 6; | |
| 204 | + | let units = match mode { | |
| 205 | + | 0 => 1, | |
| 206 | + | 2 => 12, | |
| 207 | + | _ => t1_overflows, | |
| 208 | + | }; | |
| 209 | + | if units == 0 { | |
| 210 | + | return; | |
| 211 | + | } | |
| 212 | + | if let Some(tx) = &mut self.uart.tx { | |
| 213 | + | tx.remaining = tx.remaining.saturating_sub(units); | |
| 214 | + | if tx.remaining == 0 { | |
| 215 | + | self.uart.tx_log.push(tx.byte); | |
| 216 | + | self.uart.tx = None; | |
| 217 | + | self.set_sfr(SCON, self.sfr(SCON) | TI); | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | let scon = self.sfr(SCON); | |
| 222 | + | if self.uart.rx.is_none() | |
| 223 | + | && scon & REN != 0 | |
| 224 | + | && scon & RI == 0 | |
| 225 | + | && let Some(byte) = self.uart.rx_queue.pop_front() | |
| 226 | + | { | |
| 227 | + | self.uart.rx = Some(Shift { | |
| 228 | + | byte, | |
| 229 | + | remaining: self.uart_frame(), | |
| 230 | + | }); | |
| 231 | + | } | |
| 232 | + | if let Some(rx) = &mut self.uart.rx { | |
| 233 | + | rx.remaining = rx.remaining.saturating_sub(units); | |
| 234 | + | if rx.remaining == 0 { | |
| 235 | + | let byte = rx.byte; | |
| 236 | + | self.uart.rx = None; | |
| 237 | + | // Terminal input has no 9th data bit; in modes 1-3 RB8 gets | |
| 238 | + | // the stop bit (1). With SM2 set in modes 2/3 that 9th bit | |
| 239 | + | // would be 0, so the byte is discarded. | |
| 240 | + | let mut scon = self.sfr(SCON); | |
| 241 | + | if !(mode >= 2 && scon & SM2 != 0) { | |
| 242 | + | self.uart.sbuf_rx = byte; | |
| 243 | + | scon |= RI; | |
| 244 | + | if mode == 1 { | |
| 245 | + | scon |= RB8; | |
| 246 | + | } else if mode >= 2 { | |
| 247 | + | scon &= !RB8; | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | self.set_sfr(SCON, scon); | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | pub(super) fn service_interrupts(&mut self) { | |
| 256 | + | if self.irq_hold { | |
| 257 | + | self.irq_hold = false; | |
| 258 | + | return; | |
| 259 | + | } | |
| 260 | + | let ie = self.sfr(IE); | |
| 261 | + | if ie & 0x80 == 0 { | |
| 262 | + | return; | |
| 263 | + | } | |
| 264 | + | let tcon = self.sfr(TCON); | |
| 265 | + | let scon = self.sfr(SCON); | |
| 266 | + | let pending = |mask: u8| match mask { | |
| 267 | + | 0x01 => tcon & IE0 != 0, | |
| 268 | + | 0x02 => tcon & TF0 != 0, | |
| 269 | + | 0x04 => tcon & IE1 != 0, | |
| 270 | + | 0x08 => tcon & TF1 != 0, | |
| 271 | + | _ => scon & (RI | TI) != 0, | |
| 272 | + | }; | |
| 273 | + | let ip = self.sfr(IP); | |
| 274 | + | let current = self.in_service.last().copied(); | |
| 275 | + | for level in [1u8, 0] { | |
| 276 | + | if current.is_some_and(|c| c >= level) { | |
| 277 | + | continue; | |
| 278 | + | } | |
| 279 | + | let found = SOURCES.iter().find(|&&(mask, _)| { | |
| 280 | + | ie & mask != 0 && (ip & mask != 0) == (level == 1) && pending(mask) | |
| 281 | + | }); | |
| 282 | + | if let Some(&(mask, vector)) = found { | |
| 283 | + | // Hardware clears timer flags and edge-triggered external flags. | |
| 284 | + | let mut tcon = self.sfr(TCON); | |
| 285 | + | match mask { | |
| 286 | + | 0x01 if tcon & IT0 != 0 => tcon &= !IE0, | |
| 287 | + | 0x02 => tcon &= !TF0, | |
| 288 | + | 0x04 if tcon & IT1 != 0 => tcon &= !IE1, | |
| 289 | + | 0x08 => tcon &= !TF1, | |
| 290 | + | _ => {} | |
| 291 | + | } | |
| 292 | + | self.set_sfr(TCON, tcon); | |
| 293 | + | self.idle = false; | |
| 294 | + | self.set_sfr(PCON, self.sfr(PCON) & !0x01); | |
| 295 | + | self.push_pc(); | |
| 296 | + | self.pc = vector; | |
| 297 | + | self.in_service.push(level); | |
| 298 | + | self.last_irq = Some(vector); | |
| 299 | + | self.tick(2); | |
| 300 | + | return; | |
| 301 | + | } | |
| 302 | + | } | |
| 303 | + | } | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | #[cfg(test)] | |
| 307 | + | mod tests { | |
| 308 | + | use crate::asm::assemble; | |
| 309 | + | use crate::sim::Sim; | |
| 310 | + | ||
| 311 | + | fn sim(src: &str) -> Sim { | |
| 312 | + | let out = assemble(src); | |
| 313 | + | assert!(out.ok(), "{:?}", out.diagnostics); | |
| 314 | + | let mut s = Sim::new(); | |
| 315 | + | s.load_code(out.bytes()); | |
| 316 | + | s | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | #[test] | |
| 320 | + | fn timer0_mode1_interrupt() { | |
| 321 | + | let mut s = sim(" | |
| 322 | + | ORG 0 | |
| 323 | + | LJMP main | |
| 324 | + | ORG 0Bh | |
| 325 | + | INC R7 | |
| 326 | + | RETI | |
| 327 | + | main: MOV TMOD,#01h | |
| 328 | + | MOV TH0,#0FFh | |
| 329 | + | MOV TL0,#0F0h | |
| 330 | + | SETB ET0 | |
| 331 | + | SETB EA | |
| 332 | + | SETB TR0 | |
| 333 | + | SJMP $ | |
| 334 | + | "); | |
| 335 | + | let mut saw_vector = false; | |
| 336 | + | for _ in 0..200 { | |
| 337 | + | s.step().unwrap(); | |
| 338 | + | saw_vector |= s.pc == 0x0B; | |
| 339 | + | } | |
| 340 | + | assert!(saw_vector); | |
| 341 | + | assert!(s.reg(7) >= 1); | |
| 342 | + | // TF0 is cleared by hardware on vectoring. | |
| 343 | + | assert_eq!(s.sfr(crate::sfr::TCON) & 0x20, 0); | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | #[test] | |
| 347 | + | fn timer_mode2_auto_reload() { | |
| 348 | + | let mut s = sim("MOV TMOD,#02h\nMOV TH0,#0F0h\nMOV TL0,#0FEh\nSETB TR0\nSJMP $"); | |
| 349 | + | for _ in 0..8 { | |
| 350 | + | s.step().unwrap(); | |
| 351 | + | } | |
| 352 | + | assert!(s.sfr(crate::sfr::TCON) & 0x20 != 0, "TF0 set"); | |
| 353 | + | assert!(s.sfr(crate::sfr::TL0) >= 0xF0); | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | #[test] | |
| 357 | + | fn uart_tx_and_rx() { | |
| 358 | + | let mut s = sim(" | |
| 359 | + | MOV TMOD,#20h ; timer 1 mode 2 | |
| 360 | + | MOV TH1,#0FDh ; 9600 baud @ 11.0592 MHz | |
| 361 | + | SETB TR1 | |
| 362 | + | MOV SCON,#50h ; mode 1, REN | |
| 363 | + | MOV SBUF,#'H' | |
| 364 | + | w: JNB TI,w | |
| 365 | + | CLR TI | |
| 366 | + | r: JNB RI,r | |
| 367 | + | MOV A,SBUF | |
| 368 | + | SJMP $ | |
| 369 | + | "); | |
| 370 | + | s.uart_send(b"x"); | |
| 371 | + | for _ in 0..10_000 { | |
| 372 | + | s.step().unwrap(); | |
| 373 | + | } | |
| 374 | + | assert_eq!(s.uart.tx_log, b"H"); | |
| 375 | + | assert_eq!(s.acc(), b'x'); | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | #[test] | |
| 379 | + | fn external_interrupt_edge() { | |
| 380 | + | let mut s = sim(" | |
| 381 | + | LJMP main | |
| 382 | + | ORG 03h | |
| 383 | + | INC R0 | |
| 384 | + | RETI | |
| 385 | + | main: SETB IT0 | |
| 386 | + | SETB EX0 | |
| 387 | + | SETB EA | |
| 388 | + | SJMP $ | |
| 389 | + | "); | |
| 390 | + | for _ in 0..10 { | |
| 391 | + | s.step().unwrap(); | |
| 392 | + | } | |
| 393 | + | s.pins[3] &= !0x04; // pull INT0 low | |
| 394 | + | for _ in 0..10 { | |
| 395 | + | s.step().unwrap(); | |
| 396 | + | } | |
| 397 | + | s.pins[3] |= 0x04; | |
| 398 | + | for _ in 0..10 { | |
| 399 | + | s.step().unwrap(); | |
| 400 | + | } | |
| 401 | + | assert_eq!(s.reg(0), 1, "one falling edge, one interrupt"); | |
| 402 | + | } | |
| 403 | + | } | |
| @@ -0,0 +1,903 @@ | |||
| 1 | + | use std::collections::{HashMap, HashSet}; | |
| 2 | + | use std::path::{Path, PathBuf}; | |
| 3 | + | use std::process::Command; | |
| 4 | + | ||
| 5 | + | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; | |
| 6 | + | use ratatui_textarea::{CursorMove, TextArea}; | |
| 7 | + | ||
| 8 | + | use crate::asm::{self, AsmOutput}; | |
| 9 | + | use crate::sim::{Sim, Stop, disasm}; | |
| 10 | + | ||
| 11 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 12 | + | pub enum Mode { | |
| 13 | + | Edit, | |
| 14 | + | Debug, | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | /// Right-hand pane in edit mode. | |
| 18 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 19 | + | pub enum OutTab { | |
| 20 | + | Listing, | |
| 21 | + | Hex, | |
| 22 | + | Errors, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | /// Focusable panels in debug mode. | |
| 26 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 27 | + | pub enum Focus { | |
| 28 | + | Source, | |
| 29 | + | Memory, | |
| 30 | + | Ports, | |
| 31 | + | Serial, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 35 | + | pub enum MemTab { | |
| 36 | + | Iram, | |
| 37 | + | Sfr, | |
| 38 | + | Xram, | |
| 39 | + | Code, | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 43 | + | pub enum PromptKind { | |
| 44 | + | Open, | |
| 45 | + | SaveAs, | |
| 46 | + | /// `addr value` poke into the memory shown in the memory panel. | |
| 47 | + | Poke, | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | pub struct Prompt { | |
| 51 | + | pub kind: PromptKind, | |
| 52 | + | pub input: String, | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | /// Result of the last successful assembly, which the debugger runs. | |
| 56 | + | pub struct Build { | |
| 57 | + | pub source: Vec<String>, | |
| 58 | + | pub out: AsmOutput, | |
| 59 | + | pub addr_to_line: HashMap<u16, usize>, | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | impl Build { | |
| 63 | + | /// First address emitted by `line`, if any. | |
| 64 | + | pub fn line_addr(&self, line: usize) -> Option<u16> { | |
| 65 | + | self.out | |
| 66 | + | .lines | |
| 67 | + | .get(line) | |
| 68 | + | .filter(|l| !l.bytes.is_empty()) | |
| 69 | + | .and_then(|l| l.addr) | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | /// Instructions executed per frame (~30 fps) for each speed setting. | |
| 74 | + | pub const SPEEDS: [u32; 8] = [1, 5, 25, 100, 500, 2_000, 10_000, 40_000]; | |
| 75 | + | ||
| 76 | + | pub struct App { | |
| 77 | + | pub path: Option<PathBuf>, | |
| 78 | + | pub editor: TextArea<'static>, | |
| 79 | + | saved_text: String, | |
| 80 | + | pub mode: Mode, | |
| 81 | + | pub out_tab: OutTab, | |
| 82 | + | pub focus: Focus, | |
| 83 | + | pub mem_tab: MemTab, | |
| 84 | + | pub mem_scroll: usize, | |
| 85 | + | /// Last assembler result, even if it had errors (for diagnostics). | |
| 86 | + | pub last_asm: Option<(String, AsmOutput)>, | |
| 87 | + | /// Last error-free build, loaded into the simulator. | |
| 88 | + | pub build: Option<Build>, | |
| 89 | + | pub sim: Sim, | |
| 90 | + | /// Breakpoints by source line. | |
| 91 | + | pub breakpoints: HashSet<usize>, | |
| 92 | + | pub running: bool, | |
| 93 | + | /// The next run chunk starts a fresh run and may leave a breakpoint. | |
| 94 | + | resume: bool, | |
| 95 | + | pub speed: usize, | |
| 96 | + | pub clock_mhz: f64, | |
| 97 | + | pub status: String, | |
| 98 | + | /// Selected line in the debug source view. | |
| 99 | + | pub src_cursor: usize, | |
| 100 | + | pub src_scroll: usize, | |
| 101 | + | pub out_scroll: usize, | |
| 102 | + | pub port_sel: (usize, u8), | |
| 103 | + | pub serial_input: String, | |
| 104 | + | pub prev_iram: [u8; 256], | |
| 105 | + | pub prev_sfr: [u8; 128], | |
| 106 | + | pub show_help: bool, | |
| 107 | + | pub prompt: Option<Prompt>, | |
| 108 | + | pub quit: bool, | |
| 109 | + | quit_armed: bool, | |
| 110 | + | /// Set by Ctrl-G; the event loop suspends the TUI and calls | |
| 111 | + | /// `edit_externally`. | |
| 112 | + | pub external_edit: bool, | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | impl App { | |
| 116 | + | pub fn new(path: Option<PathBuf>, clock_mhz: f64) -> Self { | |
| 117 | + | let mut app = Self { | |
| 118 | + | path: None, | |
| 119 | + | editor: TextArea::default(), | |
| 120 | + | saved_text: String::new(), | |
| 121 | + | mode: Mode::Edit, | |
| 122 | + | out_tab: OutTab::Listing, | |
| 123 | + | focus: Focus::Source, | |
| 124 | + | mem_tab: MemTab::Iram, | |
| 125 | + | mem_scroll: 0, | |
| 126 | + | last_asm: None, | |
| 127 | + | build: None, | |
| 128 | + | sim: Sim::new(), | |
| 129 | + | breakpoints: HashSet::new(), | |
| 130 | + | running: false, | |
| 131 | + | resume: false, | |
| 132 | + | speed: 5, | |
| 133 | + | clock_mhz, | |
| 134 | + | status: "F1 help · F7 assemble · F5 run · F11 step".into(), | |
| 135 | + | src_cursor: 0, | |
| 136 | + | src_scroll: 0, | |
| 137 | + | out_scroll: 0, | |
| 138 | + | port_sel: (1, 0), | |
| 139 | + | serial_input: String::new(), | |
| 140 | + | prev_iram: [0; 256], | |
| 141 | + | prev_sfr: [0; 128], | |
| 142 | + | show_help: false, | |
| 143 | + | prompt: None, | |
| 144 | + | quit: false, | |
| 145 | + | quit_armed: false, | |
| 146 | + | external_edit: false, | |
| 147 | + | }; | |
| 148 | + | match path { | |
| 149 | + | Some(p) => app.open(&p), | |
| 150 | + | None => app.set_text(TEMPLATE), | |
| 151 | + | } | |
| 152 | + | app | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | fn set_text(&mut self, text: &str) { | |
| 156 | + | let lines: Vec<String> = text.lines().map(String::from).collect(); | |
| 157 | + | self.editor = TextArea::new(if lines.is_empty() { | |
| 158 | + | vec![String::new()] | |
| 159 | + | } else { | |
| 160 | + | lines | |
| 161 | + | }); | |
| 162 | + | crate::ui::draw::style_editor(&mut self.editor); | |
| 163 | + | self.saved_text = self.text(); | |
| 164 | + | self.breakpoints.clear(); | |
| 165 | + | self.build = None; | |
| 166 | + | self.last_asm = None; | |
| 167 | + | self.focus = Focus::Source; | |
| 168 | + | self.serial_input.clear(); | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | pub fn text(&self) -> String { | |
| 172 | + | self.editor.lines().join("\n") | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | pub fn dirty(&self) -> bool { | |
| 176 | + | self.text() != self.saved_text | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | /// Source differs from what the simulator is running. | |
| 180 | + | pub fn stale(&self) -> bool { | |
| 181 | + | self.build | |
| 182 | + | .as_ref() | |
| 183 | + | .is_none_or(|b| b.source != self.editor.lines()) | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | pub fn title(&self) -> String { | |
| 187 | + | let name = self | |
| 188 | + | .path | |
| 189 | + | .as_ref() | |
| 190 | + | .map_or("untitled.asm".to_string(), |p| p.display().to_string()); | |
| 191 | + | format!("{name}{}", if self.dirty() { " [+]" } else { "" }) | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // ---- Files ---- | |
| 195 | + | ||
| 196 | + | fn open(&mut self, path: &Path) { | |
| 197 | + | let text = match std::fs::read_to_string(path) { | |
| 198 | + | Ok(t) => t, | |
| 199 | + | Err(e) => { | |
| 200 | + | self.status = format!("cannot open {}: {e}", path.display()); | |
| 201 | + | return; | |
| 202 | + | } | |
| 203 | + | }; | |
| 204 | + | if path | |
| 205 | + | .extension() | |
| 206 | + | .is_some_and(|e| e.eq_ignore_ascii_case("hex") || e.eq_ignore_ascii_case("ihx")) | |
| 207 | + | { | |
| 208 | + | match asm::hex::read(&text) { | |
| 209 | + | Ok(bytes) => { | |
| 210 | + | let src = disassemble_image(&bytes, path); | |
| 211 | + | self.set_text(&src); | |
| 212 | + | self.saved_text.clear(); | |
| 213 | + | self.path = None; | |
| 214 | + | self.assemble(); | |
| 215 | + | self.status = format!( | |
| 216 | + | "loaded {} ({} bytes) and disassembled it", | |
| 217 | + | path.display(), | |
| 218 | + | bytes.len() | |
| 219 | + | ); | |
| 220 | + | } | |
| 221 | + | Err(e) => self.status = format!("invalid HEX file: {e}"), | |
| 222 | + | } | |
| 223 | + | } else { | |
| 224 | + | self.set_text(&text); | |
| 225 | + | self.path = Some(path.to_path_buf()); | |
| 226 | + | self.mode = Mode::Edit; | |
| 227 | + | self.status = format!("opened {}", path.display()); | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | fn save(&mut self, path: Option<PathBuf>) { | |
| 232 | + | let Some(path) = path.or_else(|| self.path.clone()) else { | |
| 233 | + | self.prompt = Some(Prompt { | |
| 234 | + | kind: PromptKind::SaveAs, | |
| 235 | + | input: String::new(), | |
| 236 | + | }); | |
| 237 | + | return; | |
| 238 | + | }; | |
| 239 | + | let mut text = self.text(); | |
| 240 | + | text.push('\n'); | |
| 241 | + | match std::fs::write(&path, text) { | |
| 242 | + | Ok(()) => { | |
| 243 | + | self.saved_text = self.text(); | |
| 244 | + | self.status = format!("saved {}", path.display()); | |
| 245 | + | self.path = Some(path); | |
| 246 | + | } | |
| 247 | + | Err(e) => self.status = format!("cannot save {}: {e}", path.display()), | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | /// Opens the source in `$EDITOR` and loads the result back. The | |
| 252 | + | /// caller must have suspended the TUI. A saved file is edited in place | |
| 253 | + | /// (unsaved changes are saved first); an untitled buffer goes through | |
| 254 | + | /// a temporary file. | |
| 255 | + | pub fn edit_externally(&mut self) { | |
| 256 | + | let (path, temp) = match self.path.clone() { | |
| 257 | + | Some(p) => { | |
| 258 | + | if self.dirty() { | |
| 259 | + | self.save(None); | |
| 260 | + | if self.dirty() { | |
| 261 | + | return; // save failed; `save` set the status | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | (p, false) | |
| 265 | + | } | |
| 266 | + | None => { | |
| 267 | + | let p = std::env::temp_dir().join(format!("bench51-{}.asm", std::process::id())); | |
| 268 | + | if let Err(e) = std::fs::write(&p, self.text() + "\n") { | |
| 269 | + | self.status = format!("cannot write {}: {e}", p.display()); | |
| 270 | + | return; | |
| 271 | + | } | |
| 272 | + | (p, true) | |
| 273 | + | } | |
| 274 | + | }; | |
| 275 | + | ||
| 276 | + | let line = self.editor.cursor().0 + 1; | |
| 277 | + | let result = editor_command(&path, line).status(); | |
| 278 | + | let text = std::fs::read_to_string(&path); | |
| 279 | + | if temp { | |
| 280 | + | let _ = std::fs::remove_file(&path); | |
| 281 | + | } | |
| 282 | + | match (result, text) { | |
| 283 | + | (Err(e), _) => self.status = format!("cannot start editor: {e}"), | |
| 284 | + | (Ok(s), _) if !s.success() => { | |
| 285 | + | self.status = format!("editor exited with {s}; changes not loaded") | |
| 286 | + | } | |
| 287 | + | (Ok(_), Err(e)) => self.status = format!("cannot read {}: {e}", path.display()), | |
| 288 | + | (Ok(_), Ok(text)) => self.reload(&text, !temp), | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | /// Replaces the buffer with externally edited text, keeping the cursor | |
| 293 | + | /// and breakpoints, and reassembles if anything changed. | |
| 294 | + | fn reload(&mut self, text: &str, from_saved_file: bool) { | |
| 295 | + | let lines: Vec<String> = text.lines().map(String::from).collect(); | |
| 296 | + | let lines = if lines.is_empty() { | |
| 297 | + | vec![String::new()] | |
| 298 | + | } else { | |
| 299 | + | lines | |
| 300 | + | }; | |
| 301 | + | if lines == self.editor.lines() { | |
| 302 | + | self.status = "no changes from external editor".into(); | |
| 303 | + | return; | |
| 304 | + | } | |
| 305 | + | let cursor = self.editor.cursor(); | |
| 306 | + | let (row, col) = (cursor.0, cursor.1); | |
| 307 | + | self.editor = TextArea::new(lines); | |
| 308 | + | crate::ui::draw::style_editor(&mut self.editor); | |
| 309 | + | self.editor | |
| 310 | + | .move_cursor(CursorMove::Jump(row as u16, col as u16)); | |
| 311 | + | if from_saved_file { | |
| 312 | + | self.saved_text = self.text(); | |
| 313 | + | } | |
| 314 | + | let n = self.editor.lines().len(); | |
| 315 | + | self.breakpoints.retain(|&l| l < n); | |
| 316 | + | if self.assemble() { | |
| 317 | + | self.status = format!("reloaded from external editor; {}", self.status); | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | // ---- Build & run ---- | |
| 322 | + | ||
| 323 | + | /// Assembles the editor contents. On success the program is loaded | |
| 324 | + | /// into a freshly reset simulator. | |
| 325 | + | pub fn assemble(&mut self) -> bool { | |
| 326 | + | let source = self.text(); | |
| 327 | + | let out = asm::assemble(&source); | |
| 328 | + | let ok = out.ok(); | |
| 329 | + | if ok { | |
| 330 | + | self.sim.load_code(out.bytes()); | |
| 331 | + | self.sim.reset(); | |
| 332 | + | self.running = false; | |
| 333 | + | self.status = format!("assembled: {} bytes, 0 errors", out.code_size()); | |
| 334 | + | self.write_outputs(&out, &source); | |
| 335 | + | self.build = Some(Build { | |
| 336 | + | source: self.editor.lines().to_vec(), | |
| 337 | + | addr_to_line: out.addr_to_line(), | |
| 338 | + | out: out.clone(), | |
| 339 | + | }); | |
| 340 | + | self.snapshot(); | |
| 341 | + | self.follow_pc(); | |
| 342 | + | } else { | |
| 343 | + | let first = &out.diagnostics[0]; | |
| 344 | + | self.status = format!( | |
| 345 | + | "{} error(s); line {}: {}", | |
| 346 | + | out.diagnostics.len(), | |
| 347 | + | first.line + 1, | |
| 348 | + | first.msg | |
| 349 | + | ); | |
| 350 | + | self.editor | |
| 351 | + | .move_cursor(CursorMove::Jump(first.line as u16, first.col as u16)); | |
| 352 | + | self.mode = Mode::Edit; | |
| 353 | + | self.out_tab = OutTab::Errors; | |
| 354 | + | } | |
| 355 | + | self.out_scroll = 0; | |
| 356 | + | self.last_asm = Some((source, out)); | |
| 357 | + | ok | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | /// Writes `.lst` and `.hex` next to a saved source file. | |
| 361 | + | fn write_outputs(&mut self, out: &AsmOutput, source: &str) { | |
| 362 | + | let Some(path) = &self.path else { return }; | |
| 363 | + | let res = std::fs::write(path.with_extension("lst"), out.listing(source)) | |
| 364 | + | .and_then(|_| std::fs::write(path.with_extension("hex"), out.intel_hex())); | |
| 365 | + | if let Err(e) = res { | |
| 366 | + | self.status = format!("assembled, but writing .lst/.hex failed: {e}"); | |
| 367 | + | } | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | /// Makes sure an up-to-date build is loaded; returns false on errors. | |
| 371 | + | fn ensure_built(&mut self) -> bool { | |
| 372 | + | if self.stale() && !self.assemble() { | |
| 373 | + | return false; | |
| 374 | + | } | |
| 375 | + | if self.mode == Mode::Edit { | |
| 376 | + | self.mode = Mode::Debug; | |
| 377 | + | self.focus = Focus::Source; | |
| 378 | + | } | |
| 379 | + | true | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | pub fn bp_addrs(&self) -> HashSet<u16> { | |
| 383 | + | let Some(b) = &self.build else { | |
| 384 | + | return HashSet::new(); | |
| 385 | + | }; | |
| 386 | + | self.breakpoints | |
| 387 | + | .iter() | |
| 388 | + | .filter_map(|&l| b.line_addr(l)) | |
| 389 | + | .collect() | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | pub fn snapshot(&mut self) { | |
| 393 | + | self.prev_iram = self.sim.iram; | |
| 394 | + | self.prev_sfr = self.sim.sfr; | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | pub fn pc_line(&self) -> Option<usize> { | |
| 398 | + | self.build.as_ref()?.addr_to_line.get(&self.sim.pc).copied() | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | /// Scrolls the source view so the PC line is visible. | |
| 402 | + | pub fn follow_pc(&mut self) { | |
| 403 | + | if let Some(l) = self.pc_line() { | |
| 404 | + | self.src_cursor = l; | |
| 405 | + | } | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | fn report(&mut self, stop: Stop) { | |
| 409 | + | self.running = false; | |
| 410 | + | self.status = match stop { | |
| 411 | + | Stop::Breakpoint(pc) => format!("breakpoint at {pc:04X}h"), | |
| 412 | + | Stop::ReservedOpcode { pc, opcode } => { | |
| 413 | + | format!("reserved opcode {opcode:02X}h at {pc:04X}h — stopped") | |
| 414 | + | } | |
| 415 | + | Stop::PowerDown => "CPU in power-down mode (PCON.PD) — reset to continue".into(), | |
| 416 | + | Stop::Target | Stop::Budget => format!("stopped at {:04X}h", self.sim.pc), | |
| 417 | + | }; | |
| 418 | + | self.follow_pc(); | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | fn step_cmd(&mut self, f: impl FnOnce(&mut Sim, &HashSet<u16>) -> Stop) { | |
| 422 | + | if !self.ensure_built() { | |
| 423 | + | return; | |
| 424 | + | } | |
| 425 | + | self.running = false; | |
| 426 | + | self.snapshot(); | |
| 427 | + | let bps = self.bp_addrs(); | |
| 428 | + | let stop = f(&mut self.sim, &bps); | |
| 429 | + | match stop { | |
| 430 | + | Stop::Target => { | |
| 431 | + | self.status = format!("PC = {:04X}h", self.sim.pc); | |
| 432 | + | self.follow_pc(); | |
| 433 | + | } | |
| 434 | + | Stop::Budget => self.report(Stop::Budget), | |
| 435 | + | s => self.report(s), | |
| 436 | + | } | |
| 437 | + | } | |
| 438 | + | ||
| 439 | + | fn toggle_run(&mut self) { | |
| 440 | + | if self.running { | |
| 441 | + | self.running = false; | |
| 442 | + | self.status = format!("paused at {:04X}h", self.sim.pc); | |
| 443 | + | self.follow_pc(); | |
| 444 | + | } else if self.ensure_built() { | |
| 445 | + | self.running = true; | |
| 446 | + | self.resume = true; | |
| 447 | + | self.status = "running…".into(); | |
| 448 | + | } | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | fn run_to_cursor(&mut self) { | |
| 452 | + | if !self.ensure_built() { | |
| 453 | + | return; | |
| 454 | + | } | |
| 455 | + | let line = if self.mode == Mode::Debug { | |
| 456 | + | self.src_cursor | |
| 457 | + | } else { | |
| 458 | + | self.editor.cursor().0 | |
| 459 | + | }; | |
| 460 | + | let Some(target) = self.build.as_ref().and_then(|b| b.line_addr(line)) else { | |
| 461 | + | self.status = "no code on that line".into(); | |
| 462 | + | return; | |
| 463 | + | }; | |
| 464 | + | self.step_cmd(|s, bps| { | |
| 465 | + | let mut bps = bps.clone(); | |
| 466 | + | bps.insert(target); | |
| 467 | + | match s.run(5_000_000, &bps, true) { | |
| 468 | + | Stop::Breakpoint(pc) if pc == target => Stop::Target, | |
| 469 | + | other => other, | |
| 470 | + | } | |
| 471 | + | }); | |
| 472 | + | } | |
| 473 | + | ||
| 474 | + | fn reset(&mut self) { | |
| 475 | + | self.sim.reset(); | |
| 476 | + | self.running = false; | |
| 477 | + | self.snapshot(); | |
| 478 | + | self.follow_pc(); | |
| 479 | + | self.status = "reset".into(); | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | fn toggle_breakpoint(&mut self) { | |
| 483 | + | let line = if self.mode == Mode::Debug { | |
| 484 | + | self.src_cursor | |
| 485 | + | } else { | |
| 486 | + | self.editor.cursor().0 | |
| 487 | + | }; | |
| 488 | + | if !self.breakpoints.remove(&line) { | |
| 489 | + | self.breakpoints.insert(line); | |
| 490 | + | } | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | /// Called once per frame. | |
| 494 | + | pub fn tick(&mut self) { | |
| 495 | + | if !self.running { | |
| 496 | + | return; | |
| 497 | + | } | |
| 498 | + | self.snapshot(); | |
| 499 | + | let bps = self.bp_addrs(); | |
| 500 | + | let resume = std::mem::take(&mut self.resume); | |
| 501 | + | match self.sim.run(SPEEDS[self.speed], &bps, resume) { | |
| 502 | + | Stop::Budget => { | |
| 503 | + | if let Some(l) = self.pc_line() { | |
| 504 | + | self.src_cursor = l; | |
| 505 | + | } | |
| 506 | + | } | |
| 507 | + | s => self.report(s), | |
| 508 | + | } | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | // ---- Input ---- | |
| 512 | + | ||
| 513 | + | pub fn on_mouse(&mut self, m: MouseEvent) { | |
| 514 | + | let delta: isize = match m.kind { | |
| 515 | + | MouseEventKind::ScrollDown => 3, | |
| 516 | + | MouseEventKind::ScrollUp => -3, | |
| 517 | + | _ => return, | |
| 518 | + | }; | |
| 519 | + | match self.mode { | |
| 520 | + | Mode::Edit => self.out_scroll = self.out_scroll.saturating_add_signed(delta), | |
| 521 | + | Mode::Debug => self.src_cursor = self.src_cursor.saturating_add_signed(delta), | |
| 522 | + | } | |
| 523 | + | } | |
| 524 | + | ||
| 525 | + | pub fn on_key(&mut self, key: KeyEvent) { | |
| 526 | + | let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); | |
| 527 | + | if !(ctrl && key.code == KeyCode::Char('q')) { | |
| 528 | + | self.quit_armed = false; | |
| 529 | + | } | |
| 530 | + | if self.prompt.is_some() { | |
| 531 | + | self.on_prompt_key(key); | |
| 532 | + | return; | |
| 533 | + | } | |
| 534 | + | if self.show_help { | |
| 535 | + | self.show_help = false; | |
| 536 | + | return; | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | // Global keys. | |
| 540 | + | match (key.code, ctrl) { | |
| 541 | + | (KeyCode::Char('q'), true) => { | |
| 542 | + | if self.dirty() && !self.quit_armed { | |
| 543 | + | self.quit_armed = true; | |
| 544 | + | self.status = "unsaved changes — press Ctrl-Q again to quit".into(); | |
| 545 | + | } else { | |
| 546 | + | self.quit = true; | |
| 547 | + | } | |
| 548 | + | } | |
| 549 | + | (KeyCode::F(1), _) => self.show_help = true, | |
| 550 | + | (KeyCode::F(2), _) | (KeyCode::Char('s'), true) => self.save(None), | |
| 551 | + | (KeyCode::Char('g'), true) => self.external_edit = true, | |
| 552 | + | (KeyCode::F(3), _) | (KeyCode::Char('o'), true) => { | |
| 553 | + | self.prompt = Some(Prompt { | |
| 554 | + | kind: PromptKind::Open, | |
| 555 | + | input: String::new(), | |
| 556 | + | }) | |
| 557 | + | } | |
| 558 | + | (KeyCode::F(4), _) => match self.mode { | |
| 559 | + | // ensure_built switches to debug mode on success. | |
| 560 | + | Mode::Edit => { | |
| 561 | + | self.ensure_built(); | |
| 562 | + | } | |
| 563 | + | Mode::Debug => self.mode = Mode::Edit, | |
| 564 | + | }, | |
| 565 | + | (KeyCode::F(5), _) => self.toggle_run(), | |
| 566 | + | (KeyCode::F(6), _) => self.reset(), | |
| 567 | + | (KeyCode::F(7), _) => { | |
| 568 | + | if self.assemble() && self.mode == Mode::Edit { | |
| 569 | + | self.out_tab = OutTab::Listing; | |
| 570 | + | } | |
| 571 | + | } | |
| 572 | + | (KeyCode::F(8), _) => self.run_to_cursor(), | |
| 573 | + | (KeyCode::F(9), _) => self.toggle_breakpoint(), | |
| 574 | + | (KeyCode::F(10), _) => self.step_cmd(|s, b| s.step_over(5_000_000, b)), | |
| 575 | + | (KeyCode::F(11), _) => self.step_cmd(|s, _| match s.step() { | |
| 576 | + | Ok(()) => Stop::Target, | |
| 577 | + | Err(e) => e, | |
| 578 | + | }), | |
| 579 | + | (KeyCode::F(12), _) => self.step_cmd(|s, b| s.step_out(5_000_000, b)), | |
| 580 | + | _ => match self.mode { | |
| 581 | + | Mode::Edit => self.on_edit_key(key), | |
| 582 | + | Mode::Debug => self.on_debug_key(key), | |
| 583 | + | }, | |
| 584 | + | } | |
| 585 | + | } | |
| 586 | + | ||
| 587 | + | fn on_edit_key(&mut self, key: KeyEvent) { | |
| 588 | + | let alt = key.modifiers.contains(KeyModifiers::ALT); | |
| 589 | + | match key.code { | |
| 590 | + | // Alt-1/2/3 switch the output tab; Alt-Up/Down scroll it. | |
| 591 | + | KeyCode::Char('1') if alt => self.out_tab = OutTab::Listing, | |
| 592 | + | KeyCode::Char('2') if alt => self.out_tab = OutTab::Hex, | |
| 593 | + | KeyCode::Char('3') if alt => self.out_tab = OutTab::Errors, | |
| 594 | + | KeyCode::Up if alt => self.out_scroll = self.out_scroll.saturating_sub(1), | |
| 595 | + | KeyCode::Down if alt => self.out_scroll += 1, | |
| 596 | + | KeyCode::PageUp if alt => self.out_scroll = self.out_scroll.saturating_sub(20), | |
| 597 | + | KeyCode::PageDown if alt => self.out_scroll += 20, | |
| 598 | + | KeyCode::Esc if self.running => self.toggle_run(), | |
| 599 | + | _ => { | |
| 600 | + | self.editor.input(key); | |
| 601 | + | } | |
| 602 | + | } | |
| 603 | + | } | |
| 604 | + | ||
| 605 | + | fn on_debug_key(&mut self, key: KeyEvent) { | |
| 606 | + | if self.focus == Focus::Serial { | |
| 607 | + | match key.code { | |
| 608 | + | KeyCode::Enter => { | |
| 609 | + | let mut bytes = std::mem::take(&mut self.serial_input).into_bytes(); | |
| 610 | + | bytes.push(b'\r'); | |
| 611 | + | self.sim.uart_send(&bytes); | |
| 612 | + | return; | |
| 613 | + | } | |
| 614 | + | KeyCode::Backspace => { | |
| 615 | + | self.serial_input.pop(); | |
| 616 | + | return; | |
| 617 | + | } | |
| 618 | + | KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => { | |
| 619 | + | self.serial_input.push(c); | |
| 620 | + | return; | |
| 621 | + | } | |
| 622 | + | _ => {} | |
| 623 | + | } | |
| 624 | + | } | |
| 625 | + | ||
| 626 | + | let page = 16usize; | |
| 627 | + | match key.code { | |
| 628 | + | KeyCode::Tab => self.cycle_focus(true), | |
| 629 | + | KeyCode::BackTab => self.cycle_focus(false), | |
| 630 | + | KeyCode::Esc => { | |
| 631 | + | if self.running { | |
| 632 | + | self.toggle_run(); | |
| 633 | + | } else if self.focus == Focus::Serial { | |
| 634 | + | self.focus = Focus::Source; | |
| 635 | + | } else { | |
| 636 | + | self.mode = Mode::Edit; | |
| 637 | + | } | |
| 638 | + | } | |
| 639 | + | KeyCode::Char('+') | KeyCode::Char('=') => { | |
| 640 | + | self.speed = (self.speed + 1).min(SPEEDS.len() - 1) | |
| 641 | + | } | |
| 642 | + | KeyCode::Char('-') => self.speed = self.speed.saturating_sub(1), | |
| 643 | + | KeyCode::Char('?') => self.show_help = true, | |
| 644 | + | // Letter aliases for terminals that swallow function keys. | |
| 645 | + | KeyCode::Char('r') => self.toggle_run(), | |
| 646 | + | KeyCode::Char('s') => self.on_key(KeyEvent::from(KeyCode::F(11))), | |
| 647 | + | KeyCode::Char('n') => self.on_key(KeyEvent::from(KeyCode::F(10))), | |
| 648 | + | KeyCode::Char('o') => self.on_key(KeyEvent::from(KeyCode::F(12))), | |
| 649 | + | KeyCode::Char('g') => self.run_to_cursor(), | |
| 650 | + | KeyCode::Char('b') => self.toggle_breakpoint(), | |
| 651 | + | KeyCode::Char('x') => self.reset(), | |
| 652 | + | KeyCode::Char('e') => self.mode = Mode::Edit, | |
| 653 | + | KeyCode::Char('i') => self.set_mem_tab(MemTab::Iram), | |
| 654 | + | KeyCode::Char('f') => self.set_mem_tab(MemTab::Sfr), | |
| 655 | + | KeyCode::Char('d') => self.set_mem_tab(MemTab::Xram), | |
| 656 | + | KeyCode::Char('c') => self.set_mem_tab(MemTab::Code), | |
| 657 | + | KeyCode::Char('p') => { | |
| 658 | + | self.prompt = Some(Prompt { | |
| 659 | + | kind: PromptKind::Poke, | |
| 660 | + | input: String::new(), | |
| 661 | + | }); | |
| 662 | + | } | |
| 663 | + | _ => match self.focus { | |
| 664 | + | Focus::Source => self.move_src(key.code, page), | |
| 665 | + | Focus::Memory => self.move_mem(key.code, page), | |
| 666 | + | Focus::Ports => self.move_ports(key.code), | |
| 667 | + | Focus::Serial => {} | |
| 668 | + | }, | |
| 669 | + | } | |
| 670 | + | } | |
| 671 | + | ||
| 672 | + | fn set_mem_tab(&mut self, tab: MemTab) { | |
| 673 | + | self.mem_tab = tab; | |
| 674 | + | self.mem_scroll = 0; | |
| 675 | + | self.focus = Focus::Memory; | |
| 676 | + | } | |
| 677 | + | ||
| 678 | + | fn cycle_focus(&mut self, fwd: bool) { | |
| 679 | + | const ORDER: [Focus; 4] = [Focus::Source, Focus::Memory, Focus::Ports, Focus::Serial]; | |
| 680 | + | let i = ORDER.iter().position(|&f| f == self.focus).unwrap_or(0); | |
| 681 | + | let n = ORDER.len(); | |
| 682 | + | self.focus = ORDER[if fwd { (i + 1) % n } else { (i + n - 1) % n }]; | |
| 683 | + | } | |
| 684 | + | ||
| 685 | + | fn move_src(&mut self, code: KeyCode, page: usize) { | |
| 686 | + | let max = self | |
| 687 | + | .build | |
| 688 | + | .as_ref() | |
| 689 | + | .map_or(0, |b| b.source.len().saturating_sub(1)); | |
| 690 | + | self.src_cursor = match code { | |
| 691 | + | KeyCode::Up => self.src_cursor.saturating_sub(1), | |
| 692 | + | KeyCode::Down => self.src_cursor + 1, | |
| 693 | + | KeyCode::PageUp => self.src_cursor.saturating_sub(page), | |
| 694 | + | KeyCode::PageDown => self.src_cursor + page, | |
| 695 | + | KeyCode::Home => 0, | |
| 696 | + | KeyCode::End => max, | |
| 697 | + | KeyCode::Char('.') => self.pc_line().unwrap_or(self.src_cursor), | |
| 698 | + | _ => self.src_cursor, | |
| 699 | + | } | |
| 700 | + | .min(max); | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | fn move_mem(&mut self, code: KeyCode, page: usize) { | |
| 704 | + | let rows = match self.mem_tab { | |
| 705 | + | MemTab::Iram => 16, | |
| 706 | + | MemTab::Sfr => 8, | |
| 707 | + | MemTab::Xram | MemTab::Code => 4096, | |
| 708 | + | }; | |
| 709 | + | self.mem_scroll = match code { | |
| 710 | + | KeyCode::Up => self.mem_scroll.saturating_sub(1), | |
| 711 | + | KeyCode::Down => self.mem_scroll + 1, | |
| 712 | + | KeyCode::PageUp => self.mem_scroll.saturating_sub(page), | |
| 713 | + | KeyCode::PageDown => self.mem_scroll + page, | |
| 714 | + | KeyCode::Home => 0, | |
| 715 | + | KeyCode::End => rows, | |
| 716 | + | KeyCode::Left => { | |
| 717 | + | self.mem_tab = match self.mem_tab { | |
| 718 | + | MemTab::Iram => MemTab::Code, | |
| 719 | + | MemTab::Sfr => MemTab::Iram, | |
| 720 | + | MemTab::Xram => MemTab::Sfr, | |
| 721 | + | MemTab::Code => MemTab::Xram, | |
| 722 | + | }; | |
| 723 | + | 0 | |
| 724 | + | } | |
| 725 | + | KeyCode::Right => { | |
| 726 | + | self.mem_tab = match self.mem_tab { | |
| 727 | + | MemTab::Iram => MemTab::Sfr, | |
| 728 | + | MemTab::Sfr => MemTab::Xram, | |
| 729 | + | MemTab::Xram => MemTab::Code, | |
| 730 | + | MemTab::Code => MemTab::Iram, | |
| 731 | + | }; | |
| 732 | + | 0 | |
| 733 | + | } | |
| 734 | + | _ => self.mem_scroll, | |
| 735 | + | } | |
| 736 | + | .min(rows.saturating_sub(1)); | |
| 737 | + | } | |
| 738 | + | ||
| 739 | + | fn move_ports(&mut self, code: KeyCode) { | |
| 740 | + | let (port, bit) = &mut self.port_sel; | |
| 741 | + | match code { | |
| 742 | + | KeyCode::Up => *port = port.saturating_sub(1), | |
| 743 | + | KeyCode::Down => *port = (*port + 1).min(3), | |
| 744 | + | // Bits are drawn MSB first, so Left moves towards bit 7. | |
| 745 | + | KeyCode::Left => *bit = (*bit + 1).min(7), | |
| 746 | + | KeyCode::Right => *bit = bit.saturating_sub(1), | |
| 747 | + | KeyCode::Char(' ') | KeyCode::Enter => { | |
| 748 | + | self.sim.pins[*port] ^= 1 << *bit; | |
| 749 | + | } | |
| 750 | + | _ => {} | |
| 751 | + | } | |
| 752 | + | } | |
| 753 | + | ||
| 754 | + | fn on_prompt_key(&mut self, key: KeyEvent) { | |
| 755 | + | let Some(p) = &mut self.prompt else { return }; | |
| 756 | + | match key.code { | |
| 757 | + | KeyCode::Esc => self.prompt = None, | |
| 758 | + | KeyCode::Backspace => { | |
| 759 | + | p.input.pop(); | |
| 760 | + | } | |
| 761 | + | KeyCode::Char(c) => p.input.push(c), | |
| 762 | + | KeyCode::Enter => { | |
| 763 | + | let p = self.prompt.take().unwrap(); | |
| 764 | + | let input = p.input.trim().to_string(); | |
| 765 | + | if input.is_empty() { | |
| 766 | + | return; | |
| 767 | + | } | |
| 768 | + | match p.kind { | |
| 769 | + | PromptKind::Open => self.open(Path::new(&input)), | |
| 770 | + | PromptKind::SaveAs => self.save(Some(PathBuf::from(input))), | |
| 771 | + | PromptKind::Poke => self.poke(&input), | |
| 772 | + | } | |
| 773 | + | } | |
| 774 | + | _ => {} | |
| 775 | + | } | |
| 776 | + | } | |
| 777 | + | ||
| 778 | + | /// `addr value` → write `value` at `addr` in the current memory tab. | |
| 779 | + | fn poke(&mut self, input: &str) { | |
| 780 | + | let parse = |s: &str| { | |
| 781 | + | let s = s.trim_start_matches("0x").trim_end_matches(['h', 'H']); | |
| 782 | + | u32::from_str_radix(s, 16).ok() | |
| 783 | + | }; | |
| 784 | + | let parts: Vec<&str> = input.split_whitespace().collect(); | |
| 785 | + | let (Some(addr), Some(val)) = ( | |
| 786 | + | parts.first().and_then(|s| parse(s)), | |
| 787 | + | parts.get(1).and_then(|s| parse(s)), | |
| 788 | + | ) else { | |
| 789 | + | self.status = "usage: <addr> <value> (hex), e.g. 30 FF".into(); | |
| 790 | + | return; | |
| 791 | + | }; | |
| 792 | + | if val > 0xFF { | |
| 793 | + | self.status = "value must be a byte".into(); | |
| 794 | + | return; | |
| 795 | + | } | |
| 796 | + | let v = val as u8; | |
| 797 | + | let ok = match self.mem_tab { | |
| 798 | + | MemTab::Iram if addr <= 0xFF => { | |
| 799 | + | self.sim.iram[addr as usize] = v; | |
| 800 | + | true | |
| 801 | + | } | |
| 802 | + | MemTab::Sfr if (0x80..=0xFF).contains(&addr) => { | |
| 803 | + | self.sim.set_sfr(addr as u8, v); | |
| 804 | + | true | |
| 805 | + | } | |
| 806 | + | MemTab::Xram if addr <= 0xFFFF => { | |
| 807 | + | self.sim.xram[addr as usize] = v; | |
| 808 | + | true | |
| 809 | + | } | |
| 810 | + | MemTab::Code if addr <= 0xFFFF => { | |
| 811 | + | self.sim.code[addr as usize] = v; | |
| 812 | + | true | |
| 813 | + | } | |
| 814 | + | _ => false, | |
| 815 | + | }; | |
| 816 | + | self.status = if ok { | |
| 817 | + | format!("wrote {v:02X}h at {addr:X}h") | |
| 818 | + | } else { | |
| 819 | + | "address out of range".into() | |
| 820 | + | }; | |
| 821 | + | } | |
| 822 | + | } | |
| 823 | + | ||
| 824 | + | /// Command that opens `path` at `line` in the user's editor: `$EDITOR`, | |
| 825 | + | /// then `$VISUAL`, then `vi`. Run through `sh` so values such as | |
| 826 | + | /// `code --wait` work as they do for git. | |
| 827 | + | fn editor_command(path: &Path, line: usize) -> Command { | |
| 828 | + | let editor = ["EDITOR", "VISUAL"] | |
| 829 | + | .iter() | |
| 830 | + | .filter_map(|v| std::env::var(v).ok()) | |
| 831 | + | .find(|v| !v.trim().is_empty()) | |
| 832 | + | .unwrap_or_else(|| "vi".into()); | |
| 833 | + | // Editors known to accept `+LINE` before the file name. | |
| 834 | + | let program = editor.split_whitespace().next().unwrap_or_default(); | |
| 835 | + | let name = Path::new(program) | |
| 836 | + | .file_name() | |
| 837 | + | .and_then(|n| n.to_str()) | |
| 838 | + | .unwrap_or_default(); | |
| 839 | + | let goto = matches!( | |
| 840 | + | name, | |
| 841 | + | "vi" | "vim" | "nvim" | "nano" | "emacs" | "micro" | "kak" | "ne" | |
| 842 | + | ) | |
| 843 | + | .then(|| format!("+{line}")); | |
| 844 | + | ||
| 845 | + | let mut cmd = Command::new("sh"); | |
| 846 | + | cmd.arg("-c").arg(format!("{editor} \"$@\"")).arg("sh"); | |
| 847 | + | cmd.args(goto).arg(path); | |
| 848 | + | cmd | |
| 849 | + | } | |
| 850 | + | ||
| 851 | + | /// Builds an assembler source from a HEX image by disassembling each | |
| 852 | + | /// contiguous run of bytes. Reassembling it reproduces the image exactly. | |
| 853 | + | fn disassemble_image(bytes: &[(u16, u8)], path: &Path) -> String { | |
| 854 | + | let mut code = Box::new([0u8; 0x10000]); | |
| 855 | + | let mut used = vec![false; 0x10000]; | |
| 856 | + | for &(a, b) in bytes { | |
| 857 | + | code[a as usize] = b; | |
| 858 | + | used[a as usize] = true; | |
| 859 | + | } | |
| 860 | + | let mut src = format!("; disassembled from {}\n", path.display()); | |
| 861 | + | let mut addr = 0usize; | |
| 862 | + | while addr < 0x10000 { | |
| 863 | + | if !used[addr] { | |
| 864 | + | addr += 1; | |
| 865 | + | continue; | |
| 866 | + | } | |
| 867 | + | src.push_str(&format!( | |
| 868 | + | "\n ORG {}\n", | |
| 869 | + | crate::sfr::hex16(addr as u16) | |
| 870 | + | )); | |
| 871 | + | while addr < 0x10000 && used[addr] { | |
| 872 | + | let d = disasm::disassemble(&code, addr as u16); | |
| 873 | + | let size = d.size as usize; | |
| 874 | + | if (addr..addr + size).all(|a| a < 0x10000 && used[a]) { | |
| 875 | + | src.push_str(&format!(" {:<24}; {:04X}\n", d.text, addr)); | |
| 876 | + | addr += size; | |
| 877 | + | } else { | |
| 878 | + | src.push_str(&format!( | |
| 879 | + | " DB {:<21}; {:04X}\n", | |
| 880 | + | crate::sfr::hex8(code[addr]), | |
| 881 | + | addr | |
| 882 | + | )); | |
| 883 | + | addr += 1; | |
| 884 | + | } | |
| 885 | + | } | |
| 886 | + | } | |
| 887 | + | src.push_str(" END\n"); | |
| 888 | + | src | |
| 889 | + | } | |
| 890 | + | ||
| 891 | + | const TEMPLATE: &str = "\ | |
| 892 | + | ; New 8051 program. F7 assembles, F5 runs, F11 steps, F1 shows all keys. | |
| 893 | + | ORG 0000h | |
| 894 | + | LJMP main | |
| 895 | + | ||
| 896 | + | ORG 0030h | |
| 897 | + | main: MOV A,#01h ; walking LED pattern on P1 | |
| 898 | + | loop: MOV P1,A | |
| 899 | + | RL A | |
| 900 | + | SJMP loop | |
| 901 | + | ||
| 902 | + | END | |
| 903 | + | "; | |
| @@ -0,0 +1,882 @@ | |||
| 1 | + | use ratatui::Frame; | |
| 2 | + | use ratatui::layout::{Constraint, Layout, Rect}; | |
| 3 | + | use ratatui::style::{Color, Modifier, Style}; | |
| 4 | + | use ratatui::text::{Line, Span}; | |
| 5 | + | use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph, Tabs, Wrap}; | |
| 6 | + | use ratatui_textarea::TextArea; | |
| 7 | + | ||
| 8 | + | use super::app::{App, Focus, MemTab, Mode, OutTab, PromptKind, SPEEDS}; | |
| 9 | + | use super::syntax::highlight; | |
| 10 | + | use crate::sfr::{self, IE, IP, PCON, PSW, SCON, TCON, TH0, TH1, TL0, TL1, TMOD}; | |
| 11 | + | ||
| 12 | + | const ACCENT: Color = Color::Cyan; | |
| 13 | + | const CHANGED: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD); | |
| 14 | + | const DIM: Style = Style::new().fg(Color::DarkGray); | |
| 15 | + | const LABEL: Style = Style::new().fg(Color::Gray); | |
| 16 | + | const ON: Style = Style::new() | |
| 17 | + | .fg(Color::LightGreen) | |
| 18 | + | .add_modifier(Modifier::BOLD); | |
| 19 | + | const PC_BG: Color = Color::Indexed(58); | |
| 20 | + | const CURSOR_BG: Color = Color::Indexed(236); | |
| 21 | + | ||
| 22 | + | pub fn style_editor(ed: &mut TextArea<'static>) { | |
| 23 | + | ed.set_line_number_style(DIM); | |
| 24 | + | ed.set_cursor_line_style(Style::new().bg(CURSOR_BG)); | |
| 25 | + | ed.set_tab_length(8); | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | fn panel(title: &str, focused: bool) -> Block<'_> { | |
| 29 | + | Block::default() | |
| 30 | + | .borders(Borders::ALL) | |
| 31 | + | .border_type(BorderType::Rounded) | |
| 32 | + | .border_style(if focused { | |
| 33 | + | Style::new().fg(ACCENT) | |
| 34 | + | } else { | |
| 35 | + | DIM | |
| 36 | + | }) | |
| 37 | + | .title(Span::styled( | |
| 38 | + | format!(" {title} "), | |
| 39 | + | if focused { | |
| 40 | + | Style::new().fg(ACCENT).bold() | |
| 41 | + | } else { | |
| 42 | + | LABEL | |
| 43 | + | }, | |
| 44 | + | )) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | pub fn draw(f: &mut Frame, app: &mut App) { | |
| 48 | + | let [header, body, footer] = Layout::vertical([ | |
| 49 | + | Constraint::Length(1), | |
| 50 | + | Constraint::Min(5), | |
| 51 | + | Constraint::Length(1), | |
| 52 | + | ]) | |
| 53 | + | .areas(f.area()); | |
| 54 | + | draw_header(f, app, header); | |
| 55 | + | match app.mode { | |
| 56 | + | Mode::Edit => draw_edit(f, app, body), | |
| 57 | + | Mode::Debug => draw_debug(f, app, body), | |
| 58 | + | } | |
| 59 | + | draw_footer(f, app, footer); | |
| 60 | + | if app.show_help { | |
| 61 | + | draw_help(f); | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | fn draw_header(f: &mut Frame, app: &App, area: Rect) { | |
| 66 | + | let (mode, mode_style) = match app.mode { | |
| 67 | + | Mode::Edit => ( | |
| 68 | + | " EDIT ", | |
| 69 | + | Style::new().bg(Color::Blue).fg(Color::White).bold(), | |
| 70 | + | ), | |
| 71 | + | Mode::Debug => ( | |
| 72 | + | " DEBUG ", | |
| 73 | + | Style::new().bg(Color::Magenta).fg(Color::White).bold(), | |
| 74 | + | ), | |
| 75 | + | }; | |
| 76 | + | let (state, state_style) = if app.running { | |
| 77 | + | ( | |
| 78 | + | " RUNNING ", | |
| 79 | + | Style::new().bg(Color::Green).fg(Color::Black).bold(), | |
| 80 | + | ) | |
| 81 | + | } else if app.build.is_none() { | |
| 82 | + | ( | |
| 83 | + | " NOT BUILT ", | |
| 84 | + | Style::new().bg(Color::DarkGray).fg(Color::White), | |
| 85 | + | ) | |
| 86 | + | } else { | |
| 87 | + | ( | |
| 88 | + | " STOPPED ", | |
| 89 | + | Style::new().bg(Color::Yellow).fg(Color::Black).bold(), | |
| 90 | + | ) | |
| 91 | + | }; | |
| 92 | + | let mut spans = vec![ | |
| 93 | + | Span::styled(" bench51 ", Style::new().bg(ACCENT).fg(Color::Black).bold()), | |
| 94 | + | Span::raw(" "), | |
| 95 | + | Span::styled(mode, mode_style), | |
| 96 | + | Span::styled(state, state_style), | |
| 97 | + | Span::raw(format!(" {}", app.title())), | |
| 98 | + | ]; | |
| 99 | + | if app.build.is_some() && app.stale() { | |
| 100 | + | spans.push(Span::styled( | |
| 101 | + | " (source changed — F7 to rebuild)", | |
| 102 | + | Style::new().fg(Color::Yellow), | |
| 103 | + | )); | |
| 104 | + | } | |
| 105 | + | spans.push(Span::styled( | |
| 106 | + | format!(" speed {}/frame (+/-)", SPEEDS[app.speed]), | |
| 107 | + | DIM, | |
| 108 | + | )); | |
| 109 | + | f.render_widget(Line::from(spans), area); | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | fn draw_footer(f: &mut Frame, app: &App, area: Rect) { | |
| 113 | + | if let Some(p) = &app.prompt { | |
| 114 | + | let label = match p.kind { | |
| 115 | + | PromptKind::Open => "Open file (.asm or .hex): ", | |
| 116 | + | PromptKind::SaveAs => "Save as: ", | |
| 117 | + | PromptKind::Poke => match app.mem_tab { | |
| 118 | + | MemTab::Iram => "Write IRAM — <addr> <value> (hex): ", | |
| 119 | + | MemTab::Sfr => "Write SFR — <addr> <value> (hex): ", | |
| 120 | + | MemTab::Xram => "Write XRAM — <addr> <value> (hex): ", | |
| 121 | + | MemTab::Code => "Write CODE — <addr> <value> (hex): ", | |
| 122 | + | }, | |
| 123 | + | }; | |
| 124 | + | let line = Line::from(vec![ | |
| 125 | + | Span::styled(label, Style::new().fg(ACCENT).bold()), | |
| 126 | + | Span::raw(p.input.clone()), | |
| 127 | + | Span::styled("█", Style::new().fg(ACCENT)), | |
| 128 | + | Span::styled(" Enter confirm · Esc cancel", DIM), | |
| 129 | + | ]); | |
| 130 | + | f.render_widget(line, area); | |
| 131 | + | return; | |
| 132 | + | } | |
| 133 | + | let keys = match app.mode { | |
| 134 | + | Mode::Edit => "F7 build F5 run F11 step F9 bp F4 debug ^G $EDITOR F1 help", | |
| 135 | + | Mode::Debug => { | |
| 136 | + | "F5 run/pause F11 step F10 over F12 out F8 to cursor F9 bp F6 reset Tab focus F1 help" | |
| 137 | + | } | |
| 138 | + | }; | |
| 139 | + | let width = area.width as usize; | |
| 140 | + | let status = format!(" {}", app.status); | |
| 141 | + | let pad = width.saturating_sub(status.chars().count() + keys.len() + 1); | |
| 142 | + | let line = Line::from(vec![ | |
| 143 | + | Span::styled(status, Style::new().fg(Color::White)), | |
| 144 | + | Span::raw(" ".repeat(pad)), | |
| 145 | + | Span::styled(keys, DIM), | |
| 146 | + | ]); | |
| 147 | + | f.render_widget(line, area); | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // ---------------------------------------------------------------- Edit mode | |
| 151 | + | ||
| 152 | + | fn draw_edit(f: &mut Frame, app: &mut App, area: Rect) { | |
| 153 | + | let [left, right] = | |
| 154 | + | Layout::horizontal([Constraint::Percentage(58), Constraint::Percentage(42)]).areas(area); | |
| 155 | + | ||
| 156 | + | let block = panel("Editor", true); | |
| 157 | + | let inner = block.inner(left); | |
| 158 | + | f.render_widget(block, left); | |
| 159 | + | let [gutter, text] = | |
| 160 | + | Layout::horizontal([Constraint::Length(2), Constraint::Min(1)]).areas(inner); | |
| 161 | + | f.render_widget(&app.editor, text); | |
| 162 | + | ||
| 163 | + | // Breakpoint / error markers aligned with the editor's scroll position. | |
| 164 | + | let top = app.editor.scroll_offset().0 as usize; | |
| 165 | + | let errors: Vec<usize> = app | |
| 166 | + | .last_asm | |
| 167 | + | .as_ref() | |
| 168 | + | .filter(|(src, _)| *src == app.text()) | |
| 169 | + | .map(|(_, o)| o.diagnostics.iter().map(|d| d.line).collect()) | |
| 170 | + | .unwrap_or_default(); | |
| 171 | + | let marks: Vec<Line> = (top..top + gutter.height as usize) | |
| 172 | + | .map(|l| { | |
| 173 | + | if errors.contains(&l) { | |
| 174 | + | Line::styled("✗", Style::new().fg(Color::Red).bold()) | |
| 175 | + | } else if app.breakpoints.contains(&l) { | |
| 176 | + | Line::styled("●", Style::new().fg(Color::Red)) | |
| 177 | + | } else { | |
| 178 | + | Line::raw("") | |
| 179 | + | } | |
| 180 | + | }) | |
| 181 | + | .collect(); | |
| 182 | + | f.render_widget(Paragraph::new(marks), gutter); | |
| 183 | + | ||
| 184 | + | draw_output(f, app, right); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | fn draw_output(f: &mut Frame, app: &mut App, area: Rect) { | |
| 188 | + | let n_err = app | |
| 189 | + | .last_asm | |
| 190 | + | .as_ref() | |
| 191 | + | .map_or(0, |(_, o)| o.diagnostics.len()); | |
| 192 | + | let block = panel("Output", false); | |
| 193 | + | let inner = block.inner(area); | |
| 194 | + | f.render_widget(block, area); | |
| 195 | + | let [tabs_area, content] = | |
| 196 | + | Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(inner); | |
| 197 | + | let err_title = if n_err > 0 { | |
| 198 | + | format!("Errors ({n_err})") | |
| 199 | + | } else { | |
| 200 | + | "Errors".to_string() | |
| 201 | + | }; | |
| 202 | + | let selected = match app.out_tab { | |
| 203 | + | OutTab::Listing => 0, | |
| 204 | + | OutTab::Hex => 1, | |
| 205 | + | OutTab::Errors => 2, | |
| 206 | + | }; | |
| 207 | + | let tabs = Tabs::new(vec![ | |
| 208 | + | "Listing [Alt-1]".to_string(), | |
| 209 | + | "HEX [Alt-2]".to_string(), | |
| 210 | + | format!("{err_title} [Alt-3]"), | |
| 211 | + | ]) | |
| 212 | + | .select(selected) | |
| 213 | + | .style(DIM) | |
| 214 | + | .highlight_style(Style::new().fg(ACCENT).bold().underlined()); | |
| 215 | + | f.render_widget(tabs, tabs_area); | |
| 216 | + | ||
| 217 | + | let lines: Vec<Line> = match (app.out_tab, &app.last_asm) { | |
| 218 | + | (_, None) => vec![Line::styled("Press F7 to assemble.", DIM)], | |
| 219 | + | (OutTab::Listing, Some((src, out))) => out | |
| 220 | + | .listing(src) | |
| 221 | + | .lines() | |
| 222 | + | .map(|l| { | |
| 223 | + | if l.starts_with("***") { | |
| 224 | + | Line::styled(l.to_string(), Style::new().fg(Color::Red).bold()) | |
| 225 | + | } else if l.len() > 25 && l.as_bytes()[0].is_ascii_hexdigit() { | |
| 226 | + | // LOC OBJ LINE | source | |
| 227 | + | let (head, src) = l.split_at(25); | |
| 228 | + | let mut spans = | |
| 229 | + | vec![Span::styled(head.to_string(), Style::new().fg(Color::Gray))]; | |
| 230 | + | spans.extend(highlight(src, Style::new())); | |
| 231 | + | Line::from(spans) | |
| 232 | + | } else if l.len() > 25 && l.starts_with(' ') { | |
| 233 | + | let (head, src) = l.split_at(25); | |
| 234 | + | let mut spans = vec![Span::styled(head.to_string(), DIM)]; | |
| 235 | + | spans.extend(highlight(src, Style::new())); | |
| 236 | + | Line::from(spans) | |
| 237 | + | } else { | |
| 238 | + | Line::styled(l.to_string(), LABEL) | |
| 239 | + | } | |
| 240 | + | }) | |
| 241 | + | .collect(), | |
| 242 | + | (OutTab::Hex, Some((_, out))) if out.ok() => { | |
| 243 | + | out.intel_hex().lines().map(hex_line).collect() | |
| 244 | + | } | |
| 245 | + | (OutTab::Hex, Some(_)) => vec![Line::styled("Fix the errors to generate HEX output.", DIM)], | |
| 246 | + | (OutTab::Errors, Some((_, out))) if out.ok() => { | |
| 247 | + | vec![Line::styled("No errors.", Style::new().fg(Color::Green))] | |
| 248 | + | } | |
| 249 | + | (OutTab::Errors, Some((_, out))) => out | |
| 250 | + | .diagnostics | |
| 251 | + | .iter() | |
| 252 | + | .map(|d| { | |
| 253 | + | Line::from(vec![ | |
| 254 | + | Span::styled( | |
| 255 | + | format!("line {:>4}:{:<3} ", d.line + 1, d.col + 1), | |
| 256 | + | Style::new().fg(Color::Red), | |
| 257 | + | ), | |
| 258 | + | Span::raw(d.msg.clone()), | |
| 259 | + | ]) | |
| 260 | + | }) | |
| 261 | + | .collect(), | |
| 262 | + | }; | |
| 263 | + | app.out_scroll = app.out_scroll.min(lines.len().saturating_sub(1)); | |
| 264 | + | f.render_widget( | |
| 265 | + | Paragraph::new(lines).scroll((app.out_scroll as u16, 0)), | |
| 266 | + | content, | |
| 267 | + | ); | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | /// Colors the fields of an Intel HEX record. | |
| 271 | + | fn hex_line(l: &str) -> Line<'static> { | |
| 272 | + | if l.len() < 11 { | |
| 273 | + | return Line::raw(l.to_string()); | |
| 274 | + | } | |
| 275 | + | let n = l.len(); | |
| 276 | + | Line::from(vec![ | |
| 277 | + | Span::styled(":", DIM), | |
| 278 | + | Span::styled(l[1..3].to_string(), Style::new().fg(Color::Magenta)), | |
| 279 | + | Span::styled(l[3..7].to_string(), Style::new().fg(Color::Yellow)), | |
| 280 | + | Span::styled(l[7..9].to_string(), Style::new().fg(Color::Blue)), | |
| 281 | + | Span::raw(l[9..n - 2].to_string()), | |
| 282 | + | Span::styled(l[n - 2..].to_string(), DIM), | |
| 283 | + | ]) | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // --------------------------------------------------------------- Debug mode | |
| 287 | + | ||
| 288 | + | fn draw_debug(f: &mut Frame, app: &mut App, area: Rect) { | |
| 289 | + | let [left, right] = | |
| 290 | + | Layout::horizontal([Constraint::Percentage(45), Constraint::Percentage(55)]).areas(area); | |
| 291 | + | draw_source(f, app, left); | |
| 292 | + | ||
| 293 | + | let [top, mid, bottom] = Layout::vertical([ | |
| 294 | + | Constraint::Length(10), | |
| 295 | + | Constraint::Min(6), | |
| 296 | + | Constraint::Length(8), | |
| 297 | + | ]) | |
| 298 | + | .areas(right); | |
| 299 | + | let [regs, periph] = | |
| 300 | + | Layout::horizontal([Constraint::Length(30), Constraint::Min(20)]).areas(top); | |
| 301 | + | let [ports, serial] = | |
| 302 | + | Layout::horizontal([Constraint::Length(26), Constraint::Min(20)]).areas(bottom); | |
| 303 | + | draw_registers(f, app, regs); | |
| 304 | + | draw_peripherals(f, app, periph); | |
| 305 | + | draw_memory(f, app, mid); | |
| 306 | + | draw_ports(f, app, ports); | |
| 307 | + | draw_serial(f, app, serial); | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | fn draw_source(f: &mut Frame, app: &mut App, area: Rect) { | |
| 311 | + | let focused = app.focus == Focus::Source; | |
| 312 | + | let block = panel("Source", focused); | |
| 313 | + | let inner = block.inner(area); | |
| 314 | + | f.render_widget(block, area); | |
| 315 | + | let Some(build) = &app.build else { | |
| 316 | + | f.render_widget( | |
| 317 | + | Paragraph::new("Nothing assembled yet — press F7.").style(DIM), | |
| 318 | + | inner, | |
| 319 | + | ); | |
| 320 | + | return; | |
| 321 | + | }; | |
| 322 | + | ||
| 323 | + | let h = inner.height as usize; | |
| 324 | + | if app.src_cursor < app.src_scroll { | |
| 325 | + | app.src_scroll = app.src_cursor; | |
| 326 | + | } else if app.src_cursor >= app.src_scroll + h { | |
| 327 | + | app.src_scroll = app.src_cursor + 1 - h; | |
| 328 | + | } | |
| 329 | + | let pc_line = build.addr_to_line.get(&app.sim.pc).copied(); | |
| 330 | + | let lines: Vec<Line> = build | |
| 331 | + | .source | |
| 332 | + | .iter() | |
| 333 | + | .enumerate() | |
| 334 | + | .skip(app.src_scroll) | |
| 335 | + | .take(h) | |
| 336 | + | .map(|(i, text)| { | |
| 337 | + | let out = &build.out.lines[i]; | |
| 338 | + | let is_pc = pc_line == Some(i); | |
| 339 | + | let bg = if is_pc { | |
| 340 | + | Style::new().bg(PC_BG) | |
| 341 | + | } else if i == app.src_cursor && focused { | |
| 342 | + | Style::new().bg(CURSOR_BG) | |
| 343 | + | } else { | |
| 344 | + | Style::new() | |
| 345 | + | }; | |
| 346 | + | let bp = if app.breakpoints.contains(&i) { | |
| 347 | + | Span::styled("●", bg.fg(Color::Red)) | |
| 348 | + | } else { | |
| 349 | + | Span::styled(" ", bg) | |
| 350 | + | }; | |
| 351 | + | let arrow = if is_pc { | |
| 352 | + | Span::styled("▶", bg.fg(Color::Yellow).bold()) | |
| 353 | + | } else { | |
| 354 | + | Span::styled(" ", bg) | |
| 355 | + | }; | |
| 356 | + | let addr = match (out.addr, out.bytes.is_empty()) { | |
| 357 | + | (Some(a), false) => Span::styled(format!(" {a:04X} "), bg.fg(Color::Gray)), | |
| 358 | + | _ => Span::styled(" ", bg), | |
| 359 | + | }; | |
| 360 | + | let mut spans = vec![bp, arrow, addr]; | |
| 361 | + | spans.extend(highlight(text, bg)); | |
| 362 | + | let used: usize = spans.iter().map(|s| s.content.chars().count()).sum(); | |
| 363 | + | spans.push(Span::styled( | |
| 364 | + | " ".repeat((inner.width as usize).saturating_sub(used)), | |
| 365 | + | bg, | |
| 366 | + | )); | |
| 367 | + | Line::from(spans) | |
| 368 | + | }) | |
| 369 | + | .collect(); | |
| 370 | + | f.render_widget(Paragraph::new(lines), inner); | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | fn hex2(v: u8, changed: bool) -> Span<'static> { | |
| 374 | + | Span::styled( | |
| 375 | + | format!("{v:02X}"), | |
| 376 | + | if changed { | |
| 377 | + | CHANGED | |
| 378 | + | } else { | |
| 379 | + | Style::new().fg(Color::White) | |
| 380 | + | }, | |
| 381 | + | ) | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | fn flags(byte: u8, names: [&'static str; 8]) -> Vec<Span<'static>> { | |
| 385 | + | names | |
| 386 | + | .iter() | |
| 387 | + | .enumerate() | |
| 388 | + | .filter(|(_, n)| !n.is_empty()) | |
| 389 | + | .flat_map(|(i, &n)| { | |
| 390 | + | let on = byte & (0x80 >> i) != 0; | |
| 391 | + | [Span::styled(n, if on { ON } else { DIM }), Span::raw(" ")] | |
| 392 | + | }) | |
| 393 | + | .collect() | |
| 394 | + | } | |
| 395 | + | ||
| 396 | + | fn draw_registers(f: &mut Frame, app: &App, area: Rect) { | |
| 397 | + | let s = &app.sim; | |
| 398 | + | let block = panel("Registers", false); | |
| 399 | + | let inner = block.inner(area); | |
| 400 | + | f.render_widget(block, area); | |
| 401 | + | let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize]; | |
| 402 | + | let reg = |n: u8| { | |
| 403 | + | let a = s.reg_addr(n) as usize; | |
| 404 | + | vec![ | |
| 405 | + | Span::styled(format!("R{n} "), LABEL), | |
| 406 | + | hex2(s.iram[a], s.iram[a] != app.prev_iram[a]), | |
| 407 | + | Span::raw(" "), | |
| 408 | + | ] | |
| 409 | + | }; | |
| 410 | + | let micros = s.cycles as f64 * 12.0 / app.clock_mhz; | |
| 411 | + | let time = if micros >= 1_000_000.0 { | |
| 412 | + | format!("{:.3} s", micros / 1e6) | |
| 413 | + | } else if micros >= 1000.0 { | |
| 414 | + | format!("{:.3} ms", micros / 1e3) | |
| 415 | + | } else { | |
| 416 | + | format!("{micros:.2} µs") | |
| 417 | + | }; | |
| 418 | + | let lines = vec![ | |
| 419 | + | Line::from(vec![ | |
| 420 | + | Span::styled("PC ", LABEL), | |
| 421 | + | Span::styled( | |
| 422 | + | format!("{:04X}", s.pc), | |
| 423 | + | Style::new().fg(Color::Yellow).bold(), | |
| 424 | + | ), | |
| 425 | + | Span::styled(" DPTR ", LABEL), | |
| 426 | + | Span::styled( | |
| 427 | + | format!("{:04X}", s.dptr()), | |
| 428 | + | if ch(sfr::DPL) || ch(sfr::DPH) { | |
| 429 | + | CHANGED | |
| 430 | + | } else { | |
| 431 | + | Style::new() | |
| 432 | + | }, | |
| 433 | + | ), | |
| 434 | + | ]), | |
| 435 | + | Line::from(vec![ | |
| 436 | + | Span::styled("A ", LABEL), | |
| 437 | + | hex2(s.acc(), ch(sfr::ACC)), | |
| 438 | + | Span::styled(" B ", LABEL), | |
| 439 | + | hex2(s.b(), ch(sfr::B)), | |
| 440 | + | Span::styled(" SP ", LABEL), | |
| 441 | + | hex2(s.sp(), ch(sfr::SP)), | |
| 442 | + | ]), | |
| 443 | + | Line::from([0, 1, 2, 3].into_iter().flat_map(reg).collect::<Vec<_>>()), | |
| 444 | + | Line::from([4, 5, 6, 7].into_iter().flat_map(reg).collect::<Vec<_>>()), | |
| 445 | + | Line::from( | |
| 446 | + | [ | |
| 447 | + | Span::styled("PSW ", LABEL), | |
| 448 | + | hex2(s.psw(), ch(PSW)), | |
| 449 | + | Span::raw(" "), | |
| 450 | + | ] | |
| 451 | + | .into_iter() | |
| 452 | + | .chain(flags(s.psw(), ["C", "AC", "F0", "", "", "OV", "", "P"])) | |
| 453 | + | .collect::<Vec<_>>(), | |
| 454 | + | ), | |
| 455 | + | Line::from(vec![ | |
| 456 | + | Span::styled("Bank ", LABEL), | |
| 457 | + | Span::raw(format!("{}", s.bank())), | |
| 458 | + | Span::styled(" Cycles ", LABEL), | |
| 459 | + | Span::raw(format!("{}", s.cycles)), | |
| 460 | + | ]), | |
| 461 | + | Line::from(vec![ | |
| 462 | + | Span::styled("Time ", LABEL), | |
| 463 | + | Span::raw(time), | |
| 464 | + | Span::styled(format!(" @{}MHz", app.clock_mhz), DIM), | |
| 465 | + | ]), | |
| 466 | + | ]; | |
| 467 | + | f.render_widget(Paragraph::new(lines), inner); | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | fn draw_peripherals(f: &mut Frame, app: &App, area: Rect) { | |
| 471 | + | let s = &app.sim; | |
| 472 | + | let block = panel("Peripherals", false); | |
| 473 | + | let inner = block.inner(area); | |
| 474 | + | f.render_widget(block, area); | |
| 475 | + | let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize]; | |
| 476 | + | let sfr_val = |name: &'static str, addr: u8| { | |
| 477 | + | vec![ | |
| 478 | + | Span::styled(name, LABEL), | |
| 479 | + | hex2(s.sfr(addr), ch(addr)), | |
| 480 | + | Span::raw(" "), | |
| 481 | + | ] | |
| 482 | + | }; | |
| 483 | + | ||
| 484 | + | let tmod = s.sfr(TMOD); | |
| 485 | + | let tcon = s.sfr(TCON); | |
| 486 | + | let timer = |n: u8| { | |
| 487 | + | let ctl = (tmod >> (4 * n)) & 0x0F; | |
| 488 | + | let (th, tl, tr) = if n == 0 { | |
| 489 | + | (TH0, TL0, 0x10) | |
| 490 | + | } else { | |
| 491 | + | (TH1, TL1, 0x40) | |
| 492 | + | }; | |
| 493 | + | let kind = if ctl & 4 != 0 { "ctr" } else { "tmr" }; | |
| 494 | + | let gate = if ctl & 8 != 0 { " gate" } else { "" }; | |
| 495 | + | vec![ | |
| 496 | + | Span::styled(format!("T{n} "), LABEL), | |
| 497 | + | Span::styled( | |
| 498 | + | format!("{:02X}{:02X}", s.sfr(th), s.sfr(tl)), | |
| 499 | + | if ch(th) || ch(tl) { | |
| 500 | + | CHANGED | |
| 501 | + | } else { | |
| 502 | + | Style::new() | |
| 503 | + | }, | |
| 504 | + | ), | |
| 505 | + | Span::styled(format!(" m{} {kind}{gate} ", ctl & 3), DIM), | |
| 506 | + | if tcon & tr != 0 { | |
| 507 | + | Span::styled("run", ON) | |
| 508 | + | } else { | |
| 509 | + | Span::styled("stop", DIM) | |
| 510 | + | }, | |
| 511 | + | Span::raw(" "), | |
| 512 | + | ] | |
| 513 | + | }; | |
| 514 | + | ||
| 515 | + | let scon = s.sfr(SCON); | |
| 516 | + | let irq = if s.in_service.is_empty() { | |
| 517 | + | Span::styled("none", DIM) | |
| 518 | + | } else { | |
| 519 | + | Span::styled( | |
| 520 | + | s.in_service | |
| 521 | + | .iter() | |
| 522 | + | .map(|l| format!("L{l}")) | |
| 523 | + | .collect::<Vec<_>>() | |
| 524 | + | .join(">"), | |
| 525 | + | Style::new().fg(Color::Yellow), | |
| 526 | + | ) | |
| 527 | + | }; | |
| 528 | + | let last = s.last_irq.map_or("-".to_string(), |v| { | |
| 529 | + | format!("{v:04X}h ({})", vector_name(v)) | |
| 530 | + | }); | |
| 531 | + | let mut cpu_state = vec![]; | |
| 532 | + | if s.idle { | |
| 533 | + | cpu_state.push(Span::styled(" IDLE", Style::new().fg(Color::Yellow).bold())); | |
| 534 | + | } | |
| 535 | + | if s.power_down { | |
| 536 | + | cpu_state.push(Span::styled( | |
| 537 | + | " POWER-DOWN", | |
| 538 | + | Style::new().fg(Color::Red).bold(), | |
| 539 | + | )); | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | let lines = vec![ | |
| 543 | + | Line::from([sfr_val("TMOD ", TMOD), timer(0), timer(1)].concat()), | |
| 544 | + | Line::from( | |
| 545 | + | [ | |
| 546 | + | sfr_val("TCON ", TCON), | |
| 547 | + | flags( | |
| 548 | + | tcon, | |
| 549 | + | ["TF1", "TR1", "TF0", "TR0", "IE1", "IT1", "IE0", "IT0"], | |
| 550 | + | ), | |
| 551 | + | ] | |
| 552 | + | .concat(), | |
| 553 | + | ), | |
| 554 | + | Line::from( | |
| 555 | + | [ | |
| 556 | + | sfr_val("SCON ", SCON), | |
| 557 | + | vec![Span::styled(format!("mode{} ", scon >> 6), DIM)], | |
| 558 | + | flags(scon, ["", "", "SM2", "REN", "TB8", "RB8", "TI", "RI"]), | |
| 559 | + | ] | |
| 560 | + | .concat(), | |
| 561 | + | ), | |
| 562 | + | Line::from(vec![ | |
| 563 | + | Span::styled("SBUF ", LABEL), | |
| 564 | + | Span::styled("rx ", DIM), | |
| 565 | + | hex2(s.uart.sbuf_rx, false), | |
| 566 | + | Span::styled( | |
| 567 | + | if s.uart.transmitting() { | |
| 568 | + | " tx busy" | |
| 569 | + | } else { | |
| 570 | + | " tx idle" | |
| 571 | + | }, | |
| 572 | + | DIM, | |
| 573 | + | ), | |
| 574 | + | Span::styled( | |
| 575 | + | format!(" {} byte(s) queued", s.uart_rx_pending().len()), | |
| 576 | + | DIM, | |
| 577 | + | ), | |
| 578 | + | ]), | |
| 579 | + | Line::from( | |
| 580 | + | [ | |
| 581 | + | sfr_val("IE ", IE), | |
| 582 | + | flags(s.sfr(IE), ["EA", "", "", "ES", "ET1", "EX1", "ET0", "EX0"]), | |
| 583 | + | ] | |
| 584 | + | .concat(), | |
| 585 | + | ), | |
| 586 | + | Line::from( | |
| 587 | + | [ | |
| 588 | + | sfr_val("IP ", IP), | |
| 589 | + | flags(s.sfr(IP), ["", "", "", "PS", "PT1", "PX1", "PT0", "PX0"]), | |
| 590 | + | sfr_val(" PCON ", PCON), | |
| 591 | + | flags(s.sfr(PCON), ["SMOD", "", "", "", "", "", "PD", "IDL"]), | |
| 592 | + | ] | |
| 593 | + | .concat(), | |
| 594 | + | ), | |
| 595 | + | Line::from( | |
| 596 | + | [ | |
| 597 | + | vec![ | |
| 598 | + | Span::styled("IRQ ", LABEL), | |
| 599 | + | Span::styled("active ", DIM), | |
| 600 | + | irq, | |
| 601 | + | Span::styled(" last ", DIM), | |
| 602 | + | ], | |
| 603 | + | vec![Span::raw(last)], | |
| 604 | + | cpu_state, | |
| 605 | + | ] | |
| 606 | + | .concat(), | |
| 607 | + | ), | |
| 608 | + | ]; | |
| 609 | + | f.render_widget(Paragraph::new(lines), inner); | |
| 610 | + | } | |
| 611 | + | ||
| 612 | + | fn vector_name(v: u16) -> &'static str { | |
| 613 | + | match v { | |
| 614 | + | 0x03 => "INT0", | |
| 615 | + | 0x0B => "T0", | |
| 616 | + | 0x13 => "INT1", | |
| 617 | + | 0x1B => "T1", | |
| 618 | + | 0x23 => "UART", | |
| 619 | + | _ => "?", | |
| 620 | + | } | |
| 621 | + | } | |
| 622 | + | ||
| 623 | + | fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) { | |
| 624 | + | let focused = app.focus == Focus::Memory; | |
| 625 | + | let block = panel("Memory", focused); | |
| 626 | + | let inner = block.inner(area); | |
| 627 | + | f.render_widget(block, area); | |
| 628 | + | let [tabs_area, content] = | |
| 629 | + | Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(inner); | |
| 630 | + | let selected = match app.mem_tab { | |
| 631 | + | MemTab::Iram => 0, | |
| 632 | + | MemTab::Sfr => 1, | |
| 633 | + | MemTab::Xram => 2, | |
| 634 | + | MemTab::Code => 3, | |
| 635 | + | }; | |
| 636 | + | f.render_widget( | |
| 637 | + | Tabs::new(vec!["IRAM [i]", "SFR [f]", "XRAM [d]", "CODE [c]"]) | |
| 638 | + | .select(selected) | |
| 639 | + | .style(DIM) | |
| 640 | + | .highlight_style(Style::new().fg(ACCENT).bold().underlined()), | |
| 641 | + | tabs_area, | |
| 642 | + | ); | |
| 643 | + | ||
| 644 | + | if app.mem_tab == MemTab::Sfr { | |
| 645 | + | draw_sfr_table(f, app, content); | |
| 646 | + | return; | |
| 647 | + | } | |
| 648 | + | ||
| 649 | + | let s = &app.sim; | |
| 650 | + | let per_row: usize = if content.width >= 74 { 16 } else { 8 }; | |
| 651 | + | let total = match app.mem_tab { | |
| 652 | + | MemTab::Iram => 256, | |
| 653 | + | _ => 0x10000, | |
| 654 | + | }; | |
| 655 | + | let rows = total / per_row; | |
| 656 | + | let h = content.height as usize; | |
| 657 | + | app.mem_scroll = app.mem_scroll.min(rows.saturating_sub(h)); | |
| 658 | + | let bps = app.bp_addrs(); | |
| 659 | + | let pc = s.pc as usize; | |
| 660 | + | let pc_len = crate::isa::info(s.code[pc]).map_or(1, |i| i.size as usize); | |
| 661 | + | let bank = s.reg_addr(0) as usize; | |
| 662 | + | ||
| 663 | + | let lines: Vec<Line> = (app.mem_scroll..rows.min(app.mem_scroll + h)) | |
| 664 | + | .map(|row| { | |
| 665 | + | let base = row * per_row; | |
| 666 | + | let mut spans = vec![Span::styled( | |
| 667 | + | if total > 256 { | |
| 668 | + | format!("{base:04X} ") | |
| 669 | + | } else { | |
| 670 | + | format!("{base:02X} ") | |
| 671 | + | }, | |
| 672 | + | LABEL, | |
| 673 | + | )]; | |
| 674 | + | let mut ascii = String::new(); | |
| 675 | + | for i in 0..per_row { | |
| 676 | + | let a = base + i; | |
| 677 | + | let (v, style) = match app.mem_tab { | |
| 678 | + | MemTab::Iram => { | |
| 679 | + | let v = s.iram[a]; | |
| 680 | + | let st = if a == s.sp() as usize { | |
| 681 | + | Style::new().fg(Color::Black).bg(Color::LightMagenta) | |
| 682 | + | } else if v != app.prev_iram[a] { | |
| 683 | + | CHANGED | |
| 684 | + | } else if (bank..bank + 8).contains(&a) { | |
| 685 | + | Style::new().fg(Color::LightBlue) | |
| 686 | + | } else if v == 0 { | |
| 687 | + | DIM | |
| 688 | + | } else { | |
| 689 | + | Style::new() | |
| 690 | + | }; | |
| 691 | + | (v, st) | |
| 692 | + | } | |
| 693 | + | MemTab::Xram => { | |
| 694 | + | let v = s.xram[a]; | |
| 695 | + | (v, if v == 0 { DIM } else { Style::new() }) | |
| 696 | + | } | |
| 697 | + | _ => { | |
| 698 | + | let v = s.code[a]; | |
| 699 | + | let st = if (pc..pc + pc_len).contains(&a) { | |
| 700 | + | Style::new().fg(Color::Black).bg(Color::Yellow) | |
| 701 | + | } else if bps.contains(&(a as u16)) { | |
| 702 | + | Style::new().fg(Color::White).bg(Color::Red) | |
| 703 | + | } else if v == 0 { | |
| 704 | + | DIM | |
| 705 | + | } else { | |
| 706 | + | Style::new() | |
| 707 | + | }; | |
| 708 | + | (v, st) | |
| 709 | + | } | |
| 710 | + | }; | |
| 711 | + | spans.push(Span::styled(format!("{v:02X}"), style)); | |
| 712 | + | spans.push(Span::raw(if i == 7 && per_row == 16 { " " } else { " " })); | |
| 713 | + | ascii.push(if v.is_ascii_graphic() || v == b' ' { | |
| 714 | + | v as char | |
| 715 | + | } else { | |
| 716 | + | '·' | |
| 717 | + | }); | |
| 718 | + | } | |
| 719 | + | spans.push(Span::styled(ascii, DIM)); | |
| 720 | + | Line::from(spans) | |
| 721 | + | }) | |
| 722 | + | .collect(); | |
| 723 | + | f.render_widget(Paragraph::new(lines), content); | |
| 724 | + | } | |
| 725 | + | ||
| 726 | + | fn draw_sfr_table(f: &mut Frame, app: &App, area: Rect) { | |
| 727 | + | let s = &app.sim; | |
| 728 | + | let cols = if area.width >= 64 { 3 } else { 2 }; | |
| 729 | + | let per_col = sfr::SFRS.len().div_ceil(cols); | |
| 730 | + | let lines: Vec<Line> = (0..per_col) | |
| 731 | + | .map(|r| { | |
| 732 | + | let mut spans = Vec::new(); | |
| 733 | + | for c in 0..cols { | |
| 734 | + | if let Some(&(name, addr)) = sfr::SFRS.get(c * per_col + r) { | |
| 735 | + | let v = s.read_direct(addr, true); | |
| 736 | + | let changed = s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize]; | |
| 737 | + | spans.push(Span::styled(format!("{name:<5}"), LABEL)); | |
| 738 | + | spans.push(Span::styled(format!("{addr:02X} "), DIM)); | |
| 739 | + | spans.push(hex2(v, changed)); | |
| 740 | + | spans.push(Span::styled( | |
| 741 | + | format!(" {v:08b} "), | |
| 742 | + | if changed { CHANGED } else { DIM }, | |
| 743 | + | )); | |
| 744 | + | } | |
| 745 | + | } | |
| 746 | + | Line::from(spans) | |
| 747 | + | }) | |
| 748 | + | .collect(); | |
| 749 | + | f.render_widget(Paragraph::new(lines), area); | |
| 750 | + | } | |
| 751 | + | ||
| 752 | + | fn draw_ports(f: &mut Frame, app: &App, area: Rect) { | |
| 753 | + | let focused = app.focus == Focus::Ports; | |
| 754 | + | let block = panel("Ports", focused); | |
| 755 | + | let inner = block.inner(area); | |
| 756 | + | f.render_widget(block, area); | |
| 757 | + | let s = &app.sim; | |
| 758 | + | let mut lines: Vec<Line> = (0..4) | |
| 759 | + | .map(|p| { | |
| 760 | + | let latch = s.sfr(sfr::P0 + 0x10 * p as u8); | |
| 761 | + | let pins = s.port_pins(p); | |
| 762 | + | let mut spans = vec![Span::styled(format!("P{p} "), LABEL)]; | |
| 763 | + | for bit in (0..8u8).rev() { | |
| 764 | + | let m = 1 << bit; | |
| 765 | + | let (sym, mut st) = if pins & m != 0 { | |
| 766 | + | ("●", Style::new().fg(Color::LightGreen)) | |
| 767 | + | } else if s.pins[p] & m == 0 { | |
| 768 | + | // Pulled low from outside (e.g. a pressed button). | |
| 769 | + | ("○", Style::new().fg(Color::LightRed)) | |
| 770 | + | } else { | |
| 771 | + | ("○", Style::new().fg(Color::DarkGray)) | |
| 772 | + | }; | |
| 773 | + | if focused && app.port_sel == (p, bit) { | |
| 774 | + | st = st.add_modifier(Modifier::REVERSED); | |
| 775 | + | } | |
| 776 | + | spans.push(Span::styled(sym, st)); | |
| 777 | + | spans.push(Span::raw(if bit == 4 { " " } else { " " })); | |
| 778 | + | } | |
| 779 | + | spans.push(Span::styled(format!("{latch:02X}"), DIM)); | |
| 780 | + | Line::from(spans) | |
| 781 | + | }) | |
| 782 | + | .collect(); | |
| 783 | + | if focused { | |
| 784 | + | lines.push(Line::styled("←→↑↓ select · space: pull pin low", DIM)); | |
| 785 | + | } else { | |
| 786 | + | lines.push(Line::styled("●=1 ○=0 red=pulled low", DIM)); | |
| 787 | + | } | |
| 788 | + | f.render_widget(Paragraph::new(lines), inner); | |
| 789 | + | } | |
| 790 | + | ||
| 791 | + | fn draw_serial(f: &mut Frame, app: &App, area: Rect) { | |
| 792 | + | let focused = app.focus == Focus::Serial; | |
| 793 | + | let block = panel("Serial (UART)", focused); | |
| 794 | + | let inner = block.inner(area); | |
| 795 | + | f.render_widget(block, area); | |
| 796 | + | let [out_area, in_area] = | |
| 797 | + | Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(inner); | |
| 798 | + | ||
| 799 | + | let mut text = String::new(); | |
| 800 | + | for &b in &app.sim.uart.tx_log { | |
| 801 | + | match b { | |
| 802 | + | b'\n' => text.push('\n'), | |
| 803 | + | b'\r' => {} | |
| 804 | + | 0x20..=0x7E => text.push(b as char), | |
| 805 | + | _ => text.push('·'), | |
| 806 | + | } | |
| 807 | + | } | |
| 808 | + | let lines: Vec<&str> = text.split('\n').collect(); | |
| 809 | + | let h = out_area.height as usize; | |
| 810 | + | let shown: Vec<Line> = lines[lines.len().saturating_sub(h)..] | |
| 811 | + | .iter() | |
| 812 | + | .map(|l| Line::raw(l.to_string())) | |
| 813 | + | .collect(); | |
| 814 | + | f.render_widget(Paragraph::new(shown).wrap(Wrap { trim: false }), out_area); | |
| 815 | + | ||
| 816 | + | let input = if focused { | |
| 817 | + | Line::from(vec![ | |
| 818 | + | Span::styled("> ", Style::new().fg(ACCENT)), | |
| 819 | + | Span::raw(app.serial_input.clone()), | |
| 820 | + | Span::styled("█", Style::new().fg(ACCENT)), | |
| 821 | + | ]) | |
| 822 | + | } else { | |
| 823 | + | Line::styled("Tab here to type; Enter sends text + CR", DIM) | |
| 824 | + | }; | |
| 825 | + | f.render_widget(input, in_area); | |
| 826 | + | } | |
| 827 | + | ||
| 828 | + | fn draw_help(f: &mut Frame) { | |
| 829 | + | let area = f.area(); | |
| 830 | + | let w = 72.min(area.width); | |
| 831 | + | let h = 31.min(area.height); | |
| 832 | + | let rect = Rect::new((area.width - w) / 2, (area.height - h) / 2, w, h); | |
| 833 | + | f.render_widget(Clear, rect); | |
| 834 | + | let k = |key: &str, desc: &str| { | |
| 835 | + | Line::from(vec![ | |
| 836 | + | Span::styled(format!(" {key:<16}"), Style::new().fg(ACCENT).bold()), | |
| 837 | + | Span::raw(desc.to_string()), | |
| 838 | + | ]) | |
| 839 | + | }; | |
| 840 | + | let head = |t: &str| Line::styled(t.to_string(), Style::new().fg(Color::Yellow).bold()); | |
| 841 | + | let lines = vec![ | |
| 842 | + | head("Global"), | |
| 843 | + | k("F1", "this help"), | |
| 844 | + | k( | |
| 845 | + | "F2 / Ctrl-S", | |
| 846 | + | "save (writes .asm; F7 also writes .lst + .hex)", | |
| 847 | + | ), | |
| 848 | + | k("F3 / Ctrl-O", "open .asm, or .hex (disassembled)"), | |
| 849 | + | k("Ctrl-G", "edit in $EDITOR; reloads + reassembles on exit"), | |
| 850 | + | k("F4", "switch Edit ⇄ Debug"), | |
| 851 | + | k("F7", "assemble (reset + load)"), | |
| 852 | + | k("F5", "run / pause (assembles if needed)"), | |
| 853 | + | k("F11 F10 F12", "step into · step over · step out"), | |
| 854 | + | k("F8", "run to cursor line"), | |
| 855 | + | k("F9", "toggle breakpoint on cursor line"), | |
| 856 | + | k("F6", "reset CPU"), | |
| 857 | + | k("Ctrl-Q", "quit"), | |
| 858 | + | head("Edit mode"), | |
| 859 | + | k("Alt-1/2/3", "Listing / HEX / Errors tab"), | |
| 860 | + | k("Alt-↑↓ PgUp/Dn", "scroll output"), | |
| 861 | + | k("Ctrl-U / Ctrl-R", "undo / redo (Emacs-style editing keys)"), | |
| 862 | + | head("Debug mode"), | |
| 863 | + | k("Tab / Shift-Tab", "focus Source → Memory → Ports → Serial"), | |
| 864 | + | k( | |
| 865 | + | "r s n o g b x", | |
| 866 | + | "run · step · next · out · to cursor · bp · reset", | |
| 867 | + | ), | |
| 868 | + | k("i f d c", "memory: IRAM · SFR · XRAM · CODE"), | |
| 869 | + | k("p", "poke a byte into the shown memory"), | |
| 870 | + | k("+ / -", "simulation speed"), | |
| 871 | + | k(".", "jump source cursor to PC"), | |
| 872 | + | k("Esc", "pause / leave serial / back to editor"), | |
| 873 | + | Line::raw(""), | |
| 874 | + | Line::styled(" press any key to close", DIM), | |
| 875 | + | ]; | |
| 876 | + | f.render_widget( | |
| 877 | + | Paragraph::new(lines) | |
| 878 | + | .block(panel("Help", true)) | |
| 879 | + | .style(Style::new().bg(Color::Black)), | |
| 880 | + | rect, | |
| 881 | + | ); | |
| 882 | + | } | |
| @@ -0,0 +1,68 @@ | |||
| 1 | + | mod app; | |
| 2 | + | pub mod draw; | |
| 3 | + | mod syntax; | |
| 4 | + | ||
| 5 | + | use std::io; | |
| 6 | + | use std::path::PathBuf; | |
| 7 | + | use std::time::{Duration, Instant}; | |
| 8 | + | ||
| 9 | + | use ratatui::crossterm::event::{ | |
| 10 | + | self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind, | |
| 11 | + | }; | |
| 12 | + | use ratatui::crossterm::execute; | |
| 13 | + | use ratatui::crossterm::terminal::{ | |
| 14 | + | EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, | |
| 15 | + | }; | |
| 16 | + | ||
| 17 | + | use app::App; | |
| 18 | + | ||
| 19 | + | const FRAME: Duration = Duration::from_millis(33); | |
| 20 | + | ||
| 21 | + | pub fn run(path: Option<PathBuf>, clock_mhz: f64) -> io::Result<()> { | |
| 22 | + | let mut terminal = ratatui::init(); | |
| 23 | + | execute!(io::stdout(), EnableMouseCapture)?; | |
| 24 | + | let mut app = App::new(path, clock_mhz); | |
| 25 | + | let result = event_loop(&mut terminal, &mut app); | |
| 26 | + | let _ = execute!(io::stdout(), DisableMouseCapture); | |
| 27 | + | ratatui::restore(); | |
| 28 | + | result | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | fn event_loop(terminal: &mut ratatui::DefaultTerminal, app: &mut App) -> io::Result<()> { | |
| 32 | + | let mut next_frame = Instant::now(); | |
| 33 | + | while !app.quit { | |
| 34 | + | terminal.draw(|f| draw::draw(f, app))?; | |
| 35 | + | // Wait for input until the next frame is due; while running, the | |
| 36 | + | // simulation advances once per frame. | |
| 37 | + | let timeout = next_frame.saturating_duration_since(Instant::now()); | |
| 38 | + | if event::poll(timeout)? { | |
| 39 | + | match event::read()? { | |
| 40 | + | Event::Key(k) if k.kind != KeyEventKind::Release => app.on_key(k), | |
| 41 | + | Event::Mouse(m) => app.on_mouse(m), | |
| 42 | + | _ => {} | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | if std::mem::take(&mut app.external_edit) { | |
| 46 | + | suspend()?; | |
| 47 | + | app.edit_externally(); | |
| 48 | + | resume(terminal)?; | |
| 49 | + | } | |
| 50 | + | if Instant::now() >= next_frame { | |
| 51 | + | app.tick(); | |
| 52 | + | next_frame = Instant::now() + FRAME; | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | Ok(()) | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | /// Hands the terminal back to the shell (for `$EDITOR`). | |
| 59 | + | fn suspend() -> io::Result<()> { | |
| 60 | + | execute!(io::stdout(), DisableMouseCapture, LeaveAlternateScreen)?; | |
| 61 | + | disable_raw_mode() | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | fn resume(terminal: &mut ratatui::DefaultTerminal) -> io::Result<()> { | |
| 65 | + | enable_raw_mode()?; | |
| 66 | + | execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; | |
| 67 | + | terminal.clear() | |
| 68 | + | } | |
| @@ -0,0 +1,122 @@ | |||
| 1 | + | //! Minimal 8051 assembly syntax highlighting. | |
| 2 | + | ||
| 3 | + | use ratatui::style::{Color, Modifier, Style}; | |
| 4 | + | use ratatui::text::Span; | |
| 5 | + | ||
| 6 | + | use crate::{isa, sfr}; | |
| 7 | + | ||
| 8 | + | const COMMENT: Style = Style::new().fg(Color::DarkGray); | |
| 9 | + | const LABEL: Style = Style::new().fg(Color::Yellow); | |
| 10 | + | const MNEMONIC: Style = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD); | |
| 11 | + | const DIRECTIVE: Style = Style::new().fg(Color::Magenta); | |
| 12 | + | const REGISTER: Style = Style::new().fg(Color::LightBlue); | |
| 13 | + | const NUMBER: Style = Style::new().fg(Color::LightGreen); | |
| 14 | + | const STRING: Style = Style::new().fg(Color::Green); | |
| 15 | + | const SFR: Style = Style::new().fg(Color::LightMagenta); | |
| 16 | + | ||
| 17 | + | fn is_directive(w: &str) -> bool { | |
| 18 | + | matches!( | |
| 19 | + | w, | |
| 20 | + | "ORG" | |
| 21 | + | | "DB" | |
| 22 | + | | "DW" | |
| 23 | + | | "DS" | |
| 24 | + | | "END" | |
| 25 | + | | "EQU" | |
| 26 | + | | "SET" | |
| 27 | + | | "BIT" | |
| 28 | + | | "DATA" | |
| 29 | + | | "IDATA" | |
| 30 | + | | "XDATA" | |
| 31 | + | | "CODE" | |
| 32 | + | | "CSEG" | |
| 33 | + | | "XSEG" | |
| 34 | + | | "DSEG" | |
| 35 | + | | "AT" | |
| 36 | + | | "USING" | |
| 37 | + | | "NAME" | |
| 38 | + | | "BYTE" | |
| 39 | + | | "WORD" | |
| 40 | + | ) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | fn word_style(w: &str, first_word: bool) -> Style { | |
| 44 | + | let u = w.to_ascii_uppercase(); | |
| 45 | + | let reg = matches!(u.as_str(), "A" | "AB" | "C" | "DPTR" | "PC") | |
| 46 | + | || (u.len() == 2 | |
| 47 | + | && u.starts_with('R') | |
| 48 | + | && u.as_bytes()[1].is_ascii_digit() | |
| 49 | + | && u.as_bytes()[1] < b'8'); | |
| 50 | + | if reg { | |
| 51 | + | REGISTER | |
| 52 | + | } else if is_directive(&u) { | |
| 53 | + | DIRECTIVE | |
| 54 | + | } else if first_word && isa::is_mnemonic(&u) { | |
| 55 | + | MNEMONIC | |
| 56 | + | } else if sfr::SFRS.iter().chain(sfr::BITS).any(|(n, _)| *n == u) { | |
| 57 | + | SFR | |
| 58 | + | } else if matches!( | |
| 59 | + | u.as_str(), | |
| 60 | + | "HIGH" | "LOW" | "MOD" | "AND" | "OR" | "XOR" | "NOT" | "SHL" | "SHR" | |
| 61 | + | ) { | |
| 62 | + | DIRECTIVE | |
| 63 | + | } else { | |
| 64 | + | Style::new() | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | /// Splits a source line into styled spans, applying `base` underneath. | |
| 69 | + | pub fn highlight(line: &str, base: Style) -> Vec<Span<'static>> { | |
| 70 | + | let chars: Vec<char> = line.chars().collect(); | |
| 71 | + | let mut spans = Vec::new(); | |
| 72 | + | let mut i = 0; | |
| 73 | + | let mut seen_word = false; | |
| 74 | + | let push = |spans: &mut Vec<Span<'static>>, s: String, st: Style| { | |
| 75 | + | spans.push(Span::styled(s, base.patch(st))) | |
| 76 | + | }; | |
| 77 | + | while i < chars.len() { | |
| 78 | + | let c = chars[i]; | |
| 79 | + | let start = i; | |
| 80 | + | if c == ';' { | |
| 81 | + | push(&mut spans, chars[i..].iter().collect(), COMMENT); | |
| 82 | + | break; | |
| 83 | + | } else if c == '\'' || c == '"' { | |
| 84 | + | i += 1; | |
| 85 | + | while i < chars.len() && chars[i] != c { | |
| 86 | + | i += 1; | |
| 87 | + | } | |
| 88 | + | i = (i + 1).min(chars.len()); | |
| 89 | + | push(&mut spans, chars[start..i].iter().collect(), STRING); | |
| 90 | + | } else if c.is_ascii_alphabetic() || c == '_' || c == '?' { | |
| 91 | + | while i < chars.len() | |
| 92 | + | && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '?') | |
| 93 | + | { | |
| 94 | + | i += 1; | |
| 95 | + | } | |
| 96 | + | let w: String = chars[start..i].iter().collect(); | |
| 97 | + | let is_label = !seen_word && chars.get(i) == Some(&':'); | |
| 98 | + | let style = if is_label { | |
| 99 | + | LABEL | |
| 100 | + | } else { | |
| 101 | + | let st = word_style(&w, !seen_word); | |
| 102 | + | seen_word = true; | |
| 103 | + | st | |
| 104 | + | }; | |
| 105 | + | push(&mut spans, w, style); | |
| 106 | + | } else if c.is_ascii_digit() || c == '#' || c == '$' { | |
| 107 | + | i += 1; | |
| 108 | + | while i < chars.len() && chars[i].is_ascii_alphanumeric() { | |
| 109 | + | i += 1; | |
| 110 | + | } | |
| 111 | + | push(&mut spans, chars[start..i].iter().collect(), NUMBER); | |
| 112 | + | } else { | |
| 113 | + | while i < chars.len() | |
| 114 | + | && !(chars[i].is_ascii_alphanumeric() || "_?;'\"#$".contains(chars[i])) | |
| 115 | + | { | |
| 116 | + | i += 1; | |
| 117 | + | } | |
| 118 | + | push(&mut spans, chars[start..i].iter().collect(), Style::new()); | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | spans | |
| 122 | + | } | |