1 #!/usr/bin/env python 2 3 """ 4 Acorn Electron ULA simulation. 5 6 Copyright (C) 2011, 2012, 2013, 2014, 2016 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT ANY 14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 15 PARTICULAR PURPOSE. See the GNU General Public License for more details. 16 17 You should have received a copy of the GNU General Public License along 18 with this program. If not, see <http://www.gnu.org/licenses/>. 19 """ 20 21 from array import array 22 from itertools import repeat 23 24 LINES_PER_ROW = 8 # the number of pixel lines per character row 25 MAX_HEIGHT = 256 # the height of the screen in pixels 26 MAX_WIDTH = 640 # the width of the screen in pixels 27 28 MAX_CSYNC = 2 # the scanline during which vsync ends 29 MIN_PIXELLINE = 28 # the first scanline involving pixel generation 30 MAX_SCANLINE = 312 # the number of scanlines in each frame 31 32 MAX_PIXELLINE = MIN_PIXELLINE + MAX_HEIGHT 33 34 MAX_HSYNC = 64 # the number of cycles in each hsync period 35 MIN_PIXELPOS = 256 # the first cycle involving pixel generation 36 MAX_SCANPOS = 1024 # the number of cycles in each scanline 37 38 MAX_PIXELPOS = MIN_PIXELPOS + MAX_WIDTH 39 40 SCREEN_LIMIT = 0x8000 # the first address after the screen memory 41 MAX_MEMORY = 0x10000 # the number of addressable memory locations 42 MAX_RAM = 0x10000 # the number of addressable RAM locations (64Kb in each IC) 43 BLANK = (0, 0, 0) 44 45 def update(ula): 46 47 """ 48 Update the 'ula' for one frame. Return the resulting screen. 49 """ 50 51 video = ula.video 52 53 i = 0 54 limit = MAX_SCANLINE * MAX_SCANPOS 55 while i < limit: 56 ula.posedge() 57 video.update() 58 ula.negedge() 59 i += 1 60 61 return video.screen 62 63 class Video: 64 65 """ 66 A class representing the video circuitry. 67 """ 68 69 def __init__(self): 70 self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT)) 71 self.colour = BLANK 72 self.csync = 1 73 self.hs = 1 74 self.x = 0 75 self.y = 0 76 77 def set_csync(self, value): 78 if self.csync and not value: 79 self.y = 0 80 self.pos = 0 81 self.csync = value 82 83 def set_hs(self, value): 84 if self.hs and not value: 85 self.x = 0 86 self.y += 1 87 self.hs = value 88 89 def update(self): 90 if MIN_PIXELLINE <= self.y < MAX_PIXELLINE: 91 if MIN_PIXELPOS + 8 <= self.x < MAX_PIXELPOS + 8: 92 self.screen[self.pos] = self.colour[0]; self.pos += 1 93 self.screen[self.pos] = self.colour[1]; self.pos += 1 94 self.screen[self.pos] = self.colour[2]; self.pos += 1 95 self.x += 1 96 97 class RAM: 98 99 """ 100 A class representing the RAM circuits (IC4 to IC7). Each circuit 101 traditionally holds 64 kilobits, with each access obtaining 1 bit from each 102 IC, and thus two accesses being required to obtain a whole byte. Here, we 103 model the circuits with a list of 65536 half-bytes with each bit in a 104 half-byte representing a bit stored on a separate IC. 105 """ 106 107 def __init__(self): 108 109 "Initialise the RAM circuits." 110 111 self.memory = [0] * MAX_RAM 112 self.row_address = 0 113 self.column_address = 0 114 self.data = 0 115 self.read_not_write = 1 116 117 def row_select(self, address): 118 119 "The operation of asserting a row 'address' via RA0...RA7." 120 121 self.row_address = address 122 123 def row_deselect(self): 124 pass 125 126 def column_select(self, address): 127 128 "The operation of asserting a column 'address' via RA0...RA7." 129 130 self.column_address = address 131 132 # Read the data. 133 134 if self.read_not_write: 135 self.data = self.memory[self.row_address << 8 | self.column_address] 136 else: 137 self.memory[self.row_address << 8 | self.column_address] = self.data 138 139 def column_deselect(self): 140 pass 141 142 def read_select(self): 143 self.read_not_write = 1 144 145 def write_select(self): 146 self.read_not_write = 0 147 148 # Convenience methods. 149 150 def fill(self, start, end, value): 151 for i in xrange(start, end): 152 self.memory[i << 1] = value >> 4 153 self.memory[i << 1 | 0x1] = value & 0xf 154 155 class CPU: 156 157 "A CPU abstraction." 158 159 def __init__(self): 160 self.address = 0x1000 161 self.data = 0 162 self.read_not_write = 1 163 164 class ULA: 165 166 """ 167 A class providing the ULA functionality. Instances of this class refer to 168 the system memory, maintain internal state (such as information about the 169 current screen mode), and provide outputs (such as the current pixel 170 colour). 171 """ 172 173 modes = [ 174 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 175 (640, 1, 25), (320, 1, 32), (160, 2, 32), 176 (320, 1, 25) 177 ] 178 179 def __init__(self, cpu, ram, video): 180 181 "Initialise the ULA with the given 'cpu', 'ram' and 'video' instances." 182 183 self.cpu = cpu 184 self.ram = ram 185 self.video = video 186 self.set_mode(6) 187 self.palette = map(get_physical_colour, range(0, 8) * 2) 188 189 self.reset() 190 191 def reset(self): 192 193 "Reset the ULA." 194 195 # General state. 196 197 self.nmi = 0 # no NMI asserted initially 198 self.irq_vsync = 0 # no IRQ asserted initially 199 self.cpu_clock = 0 # drive the CPU clock low 200 201 # Communication. 202 203 self.ram_address = 0 # address given to the RAM via RA0...RA7 204 self.data = 0 # data read from the RAM via RAM0...RAM3 205 self.cpu_address = 0 # address selected by the CPU via A0...A15 206 self.cpu_read = 0 # data read/write by the CPU selected using R/W 207 208 # Internal state. 209 210 self.have_pixels = 0 # whether pixel data has been read 211 self.pdata = 0 # decoded RAM data for pixel output 212 self.cycle = 1 # 8-state counter within each 2MHz period 213 self.pcycle = 0 # 8/4/2-state pixel output counter 214 self.x = 0 215 216 self.next_frame() 217 218 def set_mode(self, mode): 219 220 """ 221 For the given 'mode', initialise the... 222 223 * width in pixels 224 * colour depth in bits per pixel 225 * number of character rows 226 * character row size in bytes 227 * screen size in bytes 228 * default screen start address 229 * horizontal pixel scaling factor 230 * row height in pixels 231 * display height in pixels 232 233 The ULA should be reset after a mode switch in order to cleanly display 234 a full screen. 235 """ 236 237 width, self.depth, rows = self.modes[mode] 238 239 columns = (width * self.depth) / 8 # bits read -> bytes read 240 self.access_frequency = 80 / columns # cycle frequency for reading bytes 241 row_size = columns * LINES_PER_ROW 242 243 # Memory access configuration. 244 # Note the limitation on positioning the screen start. 245 246 screen_size = row_size * rows 247 self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 248 self.screen_size = SCREEN_LIMIT - self.screen_start 249 250 # Scanline configuration. 251 252 self.xscale = MAX_WIDTH / width # pixel width in display pixels 253 self.row_height = MAX_HEIGHT / rows # row height in display pixels 254 self.display_height = rows * self.row_height # display height in pixels 255 256 def vsync(self, value=0): 257 258 "Signal the start of a frame." 259 260 self.csync = value 261 self.video.set_csync(value) 262 263 def hsync(self, value=0): 264 265 "Signal the end of a scanline." 266 267 self.hs = value 268 self.video.set_hs(value) 269 270 def next_frame(self): 271 272 "Signal the start of a frame." 273 274 self.line_start = self.pixel_address = self.screen_start 275 self.line = self.line_start % LINES_PER_ROW 276 self.y = 0 277 self.x_next = 0 278 279 def next_horizontal(self): 280 281 "Visit the next horizontal position." 282 283 self.pixel_address += LINES_PER_ROW 284 self.wrap_address() 285 self.x_next = self.x + 1 286 287 def next_vertical(self): 288 289 "Reset horizontal state within the active region of the frame." 290 291 self.y += 1 292 self.x_next = 0 293 294 if self.inside_frame(): 295 self.line += 1 296 297 # At the end of a row... 298 299 if self.line == self.row_height: 300 301 # After the end of the last line in a row, the address should already 302 # have been positioned on the last line of the next column. 303 304 self.pixel_address -= LINES_PER_ROW - 1 305 self.wrap_address() 306 self.line = 0 307 308 # Record the position of the start of the pixel row. 309 310 self.line_start = self.pixel_address 311 312 # Before any spacing between character rows... 313 314 elif self.in_line(): 315 316 # If not on a row boundary, move to the next line. Here, the address 317 # needs bringing back to the previous character row. 318 319 self.pixel_address = self.line_start + 1 320 self.wrap_address() 321 322 # Record the position of the start of the pixel row. 323 324 self.line_start = self.pixel_address 325 326 def access_cycle(self): return (self.x / 8) % self.access_frequency == 0 327 def would_access_ram(self): return self.access_cycle() and self.read_pixels() and self.in_line() 328 def access_ram(self): return not self.nmi and self.would_access_ram() 329 def in_line(self): return self.line < LINES_PER_ROW 330 def in_frame(self): return MIN_PIXELLINE <= self.y < (MIN_PIXELLINE + self.display_height) 331 def inside_frame(self): return MIN_PIXELLINE < self.y < (MIN_PIXELLINE + self.display_height) 332 def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() 333 def write_pixels(self): return self.pcycle != 0 334 def next_pixel(self): return self.xscale == 1 or (self.xscale == 2 and self.cycle & 0b01010101) or (self.xscale == 4 and self.cycle & 0b00010001) 335 336 def posedge(self): 337 338 "Update the state of the ULA for each clock cycle." 339 340 self.posedge_ram() 341 342 def posedge_ram(self): 343 344 """ 345 RAM signalling. 346 347 States handled: * _ * * _ * * _ 348 """ 349 350 # Clock management. 351 352 # Read 4 bits (for RAM access only). 353 354 if self.cycle == 1: 355 356 # Reset addresses. 357 358 self.ram.column_deselect() 359 self.ram.row_deselect() 360 361 # Either read from a required address or transfer CPU data. 362 363 if self.have_pixels: 364 self.data = (self.data & 0xf0) | self.ram.data 365 else: 366 self.cpu_update_clock() 367 self.cpu_transfer_low() 368 369 # Latch row address. 370 371 elif self.cycle == 4: 372 373 # Select an address needed by the ULA or CPU. 374 375 self.ram.row_select(self.ram_address) 376 377 # Latch column address. 378 379 elif self.cycle == 8: 380 381 # Select an address needed by the ULA or CPU. 382 383 self.ram.column_select(self.ram_address) 384 385 # Assert the RAM write enable if appropriate. 386 387 if self.access_ram(): 388 self.ram.read_select() 389 else: 390 self.cpu_transfer_select() 391 392 # Read 4 bits (for RAM access only). 393 394 elif self.cycle == 32: 395 396 # Prepare to latch column address. 397 398 self.ram.column_deselect() 399 400 # Either read from a required address or transfer CPU data. 401 402 if self.access_ram(): 403 self.data = self.ram.data << 4 404 else: 405 self.cpu_transfer_high() 406 407 # Latch column address. 408 409 elif self.cycle == 64: 410 411 # Select an address needed by the ULA or CPU. 412 413 self.ram.column_select(self.ram_address) 414 415 def negedge(self): 416 417 """ 418 Update the state of the device on the negative edge of each clock cycle. 419 """ 420 421 self.negedge_video() 422 self.negedge_ram() 423 self.negedge_state() 424 self.negedge_pixel() 425 self.x = self.x_next 426 427 def negedge_video(self): 428 429 "Video signalling." 430 431 # Detect any sync conditions. 432 433 if self.x == 0: 434 self.hsync() 435 if self.y == 0: 436 self.vsync() 437 self.irq_vsync = 0 438 elif self.y == MAX_PIXELLINE: 439 self.irq_vsync = 1 440 441 # Detect the end of hsync. 442 443 elif self.x == MAX_HSYNC: 444 self.hsync(1) 445 446 # Detect the end of vsync. 447 448 elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: 449 self.vsync(1) 450 451 def negedge_ram(self): 452 453 """ 454 Prepare addresses and pixel data. 455 456 States handled: * * * _ _ * _ * 457 """ 458 459 # Clock management. 460 461 # Initialise the pixel buffer if appropriate. Output starts after 462 # this cycle. 463 464 if self.cycle == 1 and self.have_pixels: 465 self.pdata = decode(self.data, self.depth) 466 self.pcycle = 1 467 self.have_pixels = 0 468 469 # Set row address (for RAM access only). 470 471 elif self.cycle == 2: 472 473 # Either assert a required address or propagate the CPU address. 474 475 if self.access_ram(): 476 self.init_row_address(self.pixel_address) 477 else: 478 self.init_row_address(self.cpu_address) 479 480 # Latch row address, set column address (for RAM access only). 481 482 elif self.cycle == 4: 483 484 # Either assert a required address or propagate the CPU address. 485 486 if self.access_ram(): 487 self.init_column_address(self.pixel_address, 0) 488 else: 489 self.init_column_address(self.cpu_address, 0) 490 491 # Set column address (for RAM access only). 492 493 elif self.cycle == 32: 494 495 # Either assert a required address or propagate the CPU address. 496 497 if self.access_ram(): 498 self.init_column_address(self.pixel_address, 1) 499 else: 500 self.init_column_address(self.cpu_address, 1) 501 502 # Update addresses. 503 504 elif self.cycle == 128: 505 506 # If the ULA accessed RAM, indicate that a read needs completing. 507 508 if self.access_ram(): 509 self.have_pixels = 1 510 511 def negedge_state(self): 512 513 "Start a new cycle and update the pixel counters." 514 515 # Detect the end of the scanline. 516 517 if self.x + 1 == MAX_SCANPOS: 518 519 # Detect the end of the frame. 520 521 if self.y + 1 == MAX_SCANLINE: 522 self.next_frame() 523 else: 524 self.next_vertical() 525 526 # Advance to the next column even if an NMI is asserted. 527 528 elif self.cycle == 128 and self.would_access_ram(): 529 self.next_horizontal() 530 531 else: 532 self.x_next = self.x + 1 533 534 self.cycle = rotate(self.cycle, 1) 535 536 def negedge_pixel(self): 537 538 "Pixel production." 539 540 # For pixels within the frame, obtain and output the value. 541 542 if self.write_pixels(): 543 self.output_colour_value() 544 545 # Scale pixels horizontally, only accessing the next pixel value 546 # after the required number of scan positions. 547 548 if self.next_pixel(): 549 self.next_pixel_value() 550 551 # Detect spacing between character rows. 552 553 else: 554 self.video.colour = BLANK 555 556 def output_colour_value(self): 557 558 """ 559 Output the colour value for the current pixel by translating memory 560 content for the current mode. 561 """ 562 563 value = value_of_bits(self.pdata, self.depth) 564 self.video.colour = self.palette[value] 565 566 def next_pixel_value(self): 567 self.pdata = rotate(self.pdata, self.depth) 568 self.pcycle = rotate(self.pcycle, self.depth, zero=True) 569 570 def wrap_address(self): 571 if self.pixel_address >= SCREEN_LIMIT: 572 self.pixel_address -= self.screen_size 573 574 def init_row_address(self, address): 575 self.ram_address = (address & 0xff80) >> 7 576 577 def init_column_address(self, address, offset): 578 self.ram_address = (address & 0x7f) << 1 | offset 579 580 def cpu_transfer_high(self): 581 if self.cpu_read: 582 self.cpu_data = self.ram.data << 4 583 else: 584 self.ram.data = self.cpu_data >> 4 585 586 def cpu_transfer_low(self): 587 if self.cpu_read: 588 self.cpu_data = self.data | self.ram.data 589 else: 590 self.ram.data = self.cpu_data & 0b00001111 591 592 def cpu_read_address(self): 593 self.cpu_address = self.cpu.address 594 595 def cpu_transfer_data(self): 596 if self.cpu_read: 597 self.cpu.data = self.cpu_data 598 else: 599 self.cpu_data = self.cpu.data 600 601 def cpu_update_clock(self): 602 self.cpu_clock = not self.cpu_clock 603 if self.cpu_clock: 604 self.cpu_transfer_data() 605 else: 606 self.cpu_read_address() 607 608 def cpu_transfer_select(self): 609 self.cpu_read = self.cpu.read_not_write 610 if self.cpu_read: 611 self.ram.read_select() 612 else: 613 self.ram.write_select() 614 615 def rotate(value, depth, width=8, zero=False): 616 617 """ 618 Return 'value' rotated left by the number of bits given by 'depth', doing so 619 within a value 'width' given in bits. If 'zero' is true, rotate zero bits 620 into the lower bits when rotating. 621 """ 622 623 field = width - depth 624 top = value >> field 625 mask = 2 ** (width - depth) - 1 626 rest = value & mask 627 return (rest << depth) | (not zero and top or 0) 628 629 def value_of_bits(value, depth): 630 631 """ 632 Convert the upper bits of 'value' to a result, using 'depth' to indicate the 633 number of bits involved. 634 """ 635 636 return value >> (8 - depth) 637 638 def get_physical_colour(value): 639 640 """ 641 Return the physical colour as an RGB triple for the given 'value'. 642 """ 643 644 return value & 1, value >> 1 & 1, value >> 2 & 1 645 646 def decode(value, depth): 647 648 """ 649 Decode the given byte 'value' according to the 'depth' in bits per pixel, 650 returning a sequence of pixel values. 651 """ 652 653 if depth == 1: 654 return value 655 elif depth == 2: 656 return ((value & 128) | ((value & 8) << 3) | ((value & 64) >> 1) | ((value & 4) << 2) | 657 ((value & 32) >> 2) | ((value & 2) << 1) | ((value & 16) >> 3) | (value & 1)) 658 elif depth == 4: 659 return ((value & 128) | ((value & 32) << 1) | ((value & 8) << 2) | ((value & 2) << 3) | 660 ((value & 64) >> 3) | ((value & 16) >> 2) | ((value & 4) >> 1) | (value & 1)) 661 else: 662 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 663 664 # Convenience functions. 665 666 def encode(values, depth): 667 668 """ 669 Encode the given 'values' according to the 'depth' in bits per pixel, 670 returning a byte value for the pixels. 671 """ 672 673 result = 0 674 675 if depth == 1: 676 for value in values: 677 result = result << 1 | (value & 1) 678 elif depth == 2: 679 for value in values: 680 result = result << 1 | (value & 2) << 3 | (value & 1) 681 elif depth == 4: 682 for value in values: 683 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 684 else: 685 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 686 687 return result 688 689 def get_ula(): 690 691 "Return a ULA initialised with a memory array and video." 692 693 return ULA(get_cpu(), get_ram(), get_video()) 694 695 def get_video(): 696 697 "Return a video circuit." 698 699 return Video() 700 701 def get_ram(): 702 703 "Return an instance representing the computer's RAM hardware." 704 705 return RAM() 706 707 def get_cpu(): 708 709 "Return an instance representing the CPU." 710 711 return CPU() 712 713 # Test program providing coverage (necessary for compilers like Shedskin). 714 715 if __name__ == "__main__": 716 ula = get_ula() 717 ula.set_mode(2) 718 ula.reset() 719 ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) 720 721 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 722 # terminology). 723 724 a = update(ula) 725 726 # vim: tabstop=4 expandtab shiftwidth=4