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.read_not_write = 1 162 163 class ULA: 164 165 """ 166 A class providing the ULA functionality. Instances of this class refer to 167 the system memory, maintain internal state (such as information about the 168 current screen mode), and provide outputs (such as the current pixel 169 colour). 170 """ 171 172 modes = [ 173 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 174 (640, 1, 25), (320, 1, 32), (160, 2, 32), 175 (320, 1, 25) 176 ] 177 178 def __init__(self, cpu, ram, video): 179 180 "Initialise the ULA with the given 'cpu', 'ram' and 'video' instances." 181 182 self.cpu = cpu 183 self.ram = ram 184 self.video = video 185 self.set_mode(6) 186 self.palette = map(get_physical_colour, range(0, 8) * 2) 187 188 self.reset() 189 190 def reset(self): 191 192 "Reset the ULA." 193 194 # General state. 195 196 self.nmi = 0 # no NMI asserted initially 197 self.irq_vsync = 0 # no IRQ asserted initially 198 self.cpu_clock = 0 # drive the CPU clock low 199 200 # Communication. 201 202 self.ram_address = 0 # address given to the RAM via RA0...RA7 203 self.data = 0 # data read from the RAM via RAM0...RAM3 204 self.cpu_address = 0 # address selected by the CPU via A0...A15 205 self.cpu_read = 0 # data read/write by the CPU selected using R/W 206 207 # Internal state. 208 209 self.access = 0 # counter used to determine whether a byte needs reading 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 215 self.next_frame() 216 217 def set_mode(self, mode): 218 219 """ 220 For the given 'mode', initialise the... 221 222 * width in pixels 223 * colour depth in bits per pixel 224 * number of character rows 225 * character row size in bytes 226 * screen size in bytes 227 * default screen start address 228 * horizontal pixel scaling factor 229 * row height in pixels 230 * display height in pixels 231 232 The ULA should be reset after a mode switch in order to cleanly display 233 a full screen. 234 """ 235 236 self.width, self.depth, rows = self.modes[mode] 237 238 columns = (self.width * self.depth) / 8 # bits read -> bytes read 239 self.access_frequency = 80 / columns # cycle frequency for reading bytes 240 row_size = columns * LINES_PER_ROW 241 242 # Memory access configuration. 243 # Note the limitation on positioning the screen start. 244 245 screen_size = row_size * rows 246 self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 247 self.screen_size = SCREEN_LIMIT - self.screen_start 248 249 # Scanline configuration. 250 251 self.xscale = MAX_WIDTH / self.width # pixel width in display pixels 252 self.row_height = MAX_HEIGHT / rows # row height in display pixels 253 self.display_height = rows * self.row_height # display height in pixels 254 255 def vsync(self, value=0): 256 257 "Signal the start of a frame." 258 259 self.csync = value 260 self.video.set_csync(value) 261 262 def hsync(self, value=0): 263 264 "Signal the end of a scanline." 265 266 self.hs = value 267 self.video.set_hs(value) 268 269 def next_frame(self): 270 271 "Signal the start of a frame." 272 273 self.line_start = self.pixel_address = self.screen_start 274 self.line = self.line_start % LINES_PER_ROW 275 self.y = 0 276 self.x = 0 277 278 def next_horizontal(self): 279 280 "Visit the next horizontal position." 281 282 self.pixel_address += LINES_PER_ROW 283 self.wrap_address() 284 285 def next_vertical(self): 286 287 "Reset horizontal state within the active region of the frame." 288 289 self.y += 1 290 self.x = 0 291 292 if self.inside_frame(): 293 self.line += 1 294 295 # At the end of a row... 296 297 if self.line == self.row_height: 298 299 # After the end of the last line in a row, the address should already 300 # have been positioned on the last line of the next column. 301 302 self.pixel_address -= LINES_PER_ROW - 1 303 self.wrap_address() 304 self.line = 0 305 306 # Record the position of the start of the pixel row. 307 308 self.line_start = self.pixel_address 309 310 # Before any spacing between character rows... 311 312 elif self.in_line(): 313 314 # If not on a row boundary, move to the next line. Here, the address 315 # needs bringing back to the previous character row. 316 317 self.pixel_address = self.line_start + 1 318 self.wrap_address() 319 320 # Record the position of the start of the pixel row. 321 322 self.line_start = self.pixel_address 323 324 def in_line(self): return self.line < LINES_PER_ROW 325 def in_frame(self): return MIN_PIXELLINE <= self.y < (MIN_PIXELLINE + self.display_height) 326 def inside_frame(self): return MIN_PIXELLINE < self.y < (MIN_PIXELLINE + self.display_height) 327 def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() 328 def write_pixels(self): return self.pcycle != 0 329 def next_pixel(self): return self.xscale == 1 or (self.xscale == 2 and self.cycle & 0b10101010) or (self.xscale == 4 and self.cycle & 0b10001000) 330 331 def posedge(self): 332 333 """ 334 Update the state of the ULA for each clock cycle. This involves updating 335 the pixel colour by reading from the pixel buffer. 336 """ 337 338 # Video signalling. 339 340 # Detect the end of the scanline. 341 342 if self.x == MAX_SCANPOS: 343 self.next_vertical() 344 345 # Detect the end of the frame. 346 347 if self.y == MAX_SCANLINE: 348 self.next_frame() 349 350 351 352 # Detect any sync conditions. 353 354 if self.x == 0: 355 self.hsync() 356 if self.y == 0: 357 self.vsync() 358 self.irq_vsync = 0 359 elif self.y == MAX_PIXELLINE: 360 self.irq_vsync = 1 361 362 # Detect the end of hsync. 363 364 elif self.x == MAX_HSYNC: 365 self.hsync(1) 366 367 # Detect the end of vsync. 368 369 elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: 370 self.vsync(1) 371 372 373 374 # Clock management. 375 376 would_access_ram = self.access == 0 and self.read_pixels() and self.in_line() 377 access_ram = not self.nmi and would_access_ram 378 379 # Set row address (for ULA access only). 380 381 if self.cycle == 1: 382 383 # Either assert a required address or propagate the CPU address. 384 385 if access_ram: 386 self.init_row_address(self.pixel_address) 387 else: 388 self.init_row_address(self.cpu_address) 389 390 # Latch row address, set column address (for ULA access only). 391 392 elif self.cycle == 2: 393 394 # Select an address needed by the ULA or CPU. 395 396 self.ram.row_select(self.ram_address) 397 398 # Either assert a required address or propagate the CPU address. 399 400 if access_ram: 401 self.init_column_address(self.pixel_address, 0) 402 else: 403 self.init_column_address(self.cpu_address, 0) 404 405 # Latch column address. 406 407 elif self.cycle == 4: 408 409 # Select an address needed by the ULA or CPU. 410 411 self.ram.column_select(self.ram_address) 412 413 # Assert the RAM write enable if appropriate. 414 415 if access_ram: 416 self.ram.read_select() 417 else: 418 self.cpu_transfer_select() 419 420 # Cycle handled in negedge. 421 422 # Set column address (for ULA access only). 423 424 elif self.cycle == 16: 425 self.ram.column_deselect() 426 427 # Either assert a required address or propagate the CPU address. 428 429 if access_ram: 430 self.init_column_address(self.pixel_address, 1) 431 else: 432 self.init_column_address(self.cpu_address, 1) 433 434 # Latch column address. 435 436 elif self.cycle == 32: 437 438 # Select an address needed by the ULA or CPU. 439 440 self.ram.column_select(self.ram_address) 441 442 # Read 4 bits (for ULA access only). 443 444 elif self.cycle == 64: 445 446 # Advance to the next column even if an NMI is asserted. 447 448 if would_access_ram: 449 self.next_horizontal() 450 451 # Reset addresses. 452 453 elif self.cycle == 128: 454 self.ram.column_deselect() 455 self.ram.row_deselect() 456 457 # Update the RAM access controller. 458 459 self.access = (self.access + 1) % self.access_frequency 460 461 # Read the CPU address, if appropriate. 462 463 if not access_ram: 464 self.cpu_update_clock() 465 466 467 468 # Pixel production. 469 470 # For pixels within the frame, obtain and output the value. 471 472 if self.write_pixels(): 473 self.output_colour_value() 474 475 # Scale pixels horizontally, only accessing the next pixel value 476 # after the required number of scan positions. 477 478 if self.next_pixel(): 479 self.next_pixel_value() 480 481 # Detect spacing between character rows. 482 483 else: 484 self.video.colour = BLANK 485 486 def negedge(self): 487 488 """ 489 Update the state of the device. 490 491 Cycles handled: _ _ _ * _ _ * * 492 """ 493 494 # Clock management. 495 496 would_access_ram = self.access == 0 and self.read_pixels() and self.in_line() 497 access_ram = not self.nmi and would_access_ram 498 499 # Read 4 bits (for ULA access only). 500 501 if self.cycle == 8: 502 503 # Either read from a required address or transfer CPU data. 504 505 if access_ram: 506 self.data = self.ram.data << 4 507 else: 508 self.cpu_transfer_high() 509 510 # Read 4 bits (for ULA access only). 511 512 elif self.cycle == 64: 513 514 # Either read from a required address or transfer CPU data. 515 516 if access_ram: 517 self.data = self.data | self.ram.data 518 self.have_pixels = 1 519 else: 520 self.cpu_transfer_low() 521 522 # Initialise the pixel buffer if appropriate. Output starts after 523 # this cycle. 524 525 elif self.cycle == 128 and self.have_pixels: 526 self.pdata = decode(self.data, self.depth) 527 self.pcycle = 1 528 self.have_pixels = 0 529 530 # Start a new cycle. 531 532 self.cycle = rotate(self.cycle, 1) 533 self.x += 1 534 535 def output_colour_value(self): 536 537 """ 538 Output the colour value for the current pixel by translating memory 539 content for the current mode. 540 """ 541 542 value = value_of_bits(self.pdata, self.depth) 543 self.video.colour = self.palette[value] 544 545 def next_pixel_value(self): 546 self.pdata = rotate(self.pdata, self.depth) 547 self.pcycle = rotate(self.pcycle, self.depth, zero=True) 548 549 def wrap_address(self): 550 if self.pixel_address >= SCREEN_LIMIT: 551 self.pixel_address -= self.screen_size 552 553 def init_row_address(self, address): 554 self.ram_address = (address & 0xff80) >> 7 555 556 def init_column_address(self, address, offset): 557 self.ram_address = (address & 0x7f) << 1 | offset 558 559 def cpu_transfer_high(self): 560 if self.cpu_read: 561 self.cpu_data = self.ram.data << 4 562 else: 563 self.ram.data = self.cpu_data >> 4 564 565 def cpu_transfer_low(self): 566 if self.cpu_read: 567 self.cpu_data = self.data | self.ram.data 568 else: 569 self.ram.data = self.cpu_data & 0b00001111 570 571 def cpu_read_address(self): 572 self.cpu_address = self.cpu.address 573 574 def cpu_transfer_data(self): 575 if self.cpu_read: 576 self.cpu.data = self.cpu_data 577 else: 578 self.cpu_data = self.cpu.data 579 580 def cpu_update_clock(self): 581 self.cpu_clock = not self.cpu_clock 582 if self.cpu_clock: 583 self.cpu_transfer_data() 584 else: 585 self.cpu_read_address() 586 587 def cpu_transfer_select(self): 588 self.cpu_read = self.cpu.read_not_write 589 if self.cpu_read: 590 self.ram.read_select() 591 else: 592 self.ram.write_select() 593 594 def rotate(value, depth, width=8, zero=False): 595 596 """ 597 Return 'value' rotated left by the number of bits given by 'depth', doing so 598 within a value 'width' given in bits. If 'zero' is true, rotate zero bits 599 into the lower bits when rotating. 600 """ 601 602 field = width - depth 603 top = value >> field 604 mask = 2 ** (width - depth) - 1 605 rest = value & mask 606 return (rest << depth) | (not zero and top or 0) 607 608 def value_of_bits(value, depth): 609 610 """ 611 Convert the upper bits of 'value' to a result, using 'depth' to indicate the 612 number of bits involved. 613 """ 614 615 return value >> (8 - depth) 616 617 def get_physical_colour(value): 618 619 """ 620 Return the physical colour as an RGB triple for the given 'value'. 621 """ 622 623 return value & 1, value >> 1 & 1, value >> 2 & 1 624 625 def decode(value, depth): 626 627 """ 628 Decode the given byte 'value' according to the 'depth' in bits per pixel, 629 returning a sequence of pixel values. 630 """ 631 632 if depth == 1: 633 return value 634 elif depth == 2: 635 return ((value & 128) | ((value & 8) << 3) | ((value & 64) >> 1) | ((value & 4) << 2) | 636 ((value & 32) >> 2) | ((value & 2) << 1) | ((value & 16) >> 3) | (value & 1)) 637 elif depth == 4: 638 return ((value & 128) | ((value & 32) << 1) | ((value & 8) << 2) | ((value & 2) << 3) | 639 ((value & 64) >> 3) | ((value & 16) >> 2) | ((value & 4) >> 1) | (value & 1)) 640 else: 641 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 642 643 # Convenience functions. 644 645 def encode(values, depth): 646 647 """ 648 Encode the given 'values' according to the 'depth' in bits per pixel, 649 returning a byte value for the pixels. 650 """ 651 652 result = 0 653 654 if depth == 1: 655 for value in values: 656 result = result << 1 | (value & 1) 657 elif depth == 2: 658 for value in values: 659 result = result << 1 | (value & 2) << 3 | (value & 1) 660 elif depth == 4: 661 for value in values: 662 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 663 else: 664 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 665 666 return result 667 668 def get_ula(): 669 670 "Return a ULA initialised with a memory array and video." 671 672 return ULA(get_cpu(), get_ram(), get_video()) 673 674 def get_video(): 675 676 "Return a video circuit." 677 678 return Video() 679 680 def get_ram(): 681 682 "Return an instance representing the computer's RAM hardware." 683 684 return RAM() 685 686 def get_cpu(): 687 688 "Return an instance representing the CPU." 689 690 return CPU() 691 692 # Test program providing coverage (necessary for compilers like Shedskin). 693 694 if __name__ == "__main__": 695 ula = get_ula() 696 ula.set_mode(2) 697 ula.reset() 698 ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) 699 700 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 701 # terminology). 702 703 a = update(ula) 704 705 # vim: tabstop=4 expandtab shiftwidth=4