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 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 width, self.depth, rows = self.modes[mode] 237 238 columns = (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 / 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 access_cycle(self): return (self.x / 8) % self.access_frequency == 0 325 def would_access_ram(self): return self.access_cycle() and self.read_pixels() and self.in_line() 326 def access_ram(self): return not self.nmi and self.would_access_ram() 327 def in_line(self): return self.line < LINES_PER_ROW 328 def in_frame(self): return MIN_PIXELLINE <= self.y < (MIN_PIXELLINE + self.display_height) 329 def inside_frame(self): return MIN_PIXELLINE < self.y < (MIN_PIXELLINE + self.display_height) 330 def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() 331 def write_pixels(self): return self.pcycle != 0 332 def next_pixel(self): return self.xscale == 1 or (self.xscale == 2 and self.cycle & 0b01010101) or (self.xscale == 4 and self.cycle & 0b00010001) 333 334 def posedge(self): 335 336 "Update the state of the ULA for each clock cycle." 337 338 self.posedge_ram() 339 340 def posedge_ram(self): 341 342 """ 343 RAM signalling. 344 345 States handled: * _ * * _ * * _ 346 """ 347 348 # Clock management. 349 350 # Read 4 bits (for RAM access only). 351 352 if self.cycle == 1: 353 354 # Reset addresses. 355 356 self.ram.column_deselect() 357 self.ram.row_deselect() 358 359 # Either read from a required address or transfer CPU data. 360 361 if self.have_pixels: 362 self.data = (self.data & 0xf0) | self.ram.data 363 else: 364 self.cpu_update_clock() 365 self.cpu_transfer_low() 366 367 # Latch row address. 368 369 elif self.cycle == 4: 370 371 # Select an address needed by the ULA or CPU. 372 373 self.ram.row_select(self.ram_address) 374 375 # Latch column address. 376 377 elif self.cycle == 8: 378 379 # Select an address needed by the ULA or CPU. 380 381 self.ram.column_select(self.ram_address) 382 383 # Assert the RAM write enable if appropriate. 384 385 if self.access_ram(): 386 self.ram.read_select() 387 else: 388 self.cpu_transfer_select() 389 390 # Read 4 bits (for RAM access only). 391 392 elif self.cycle == 32: 393 394 # Prepare to latch column address. 395 396 self.ram.column_deselect() 397 398 # Either read from a required address or transfer CPU data. 399 400 if self.access_ram(): 401 self.data = self.ram.data << 4 402 else: 403 self.cpu_transfer_high() 404 405 # Latch column address. 406 407 elif self.cycle == 64: 408 409 # Select an address needed by the ULA or CPU. 410 411 self.ram.column_select(self.ram_address) 412 413 def negedge(self): 414 415 """ 416 Update the state of the device on the negative edge of each clock cycle. 417 """ 418 419 self.negedge_video() 420 self.negedge_ram() 421 self.negedge_state() 422 self.negedge_pixel() 423 424 def negedge_video(self): 425 426 "Video signalling." 427 428 # Detect the end of the scanline. 429 430 if self.x == MAX_SCANPOS: 431 self.next_vertical() 432 433 # Detect the end of the frame. 434 435 if self.y == MAX_SCANLINE: 436 self.next_frame() 437 438 # Detect any sync conditions. 439 440 if self.x == 0: 441 self.hsync() 442 if self.y == 0: 443 self.vsync() 444 self.irq_vsync = 0 445 elif self.y == MAX_PIXELLINE: 446 self.irq_vsync = 1 447 448 # Detect the end of hsync. 449 450 elif self.x == MAX_HSYNC: 451 self.hsync(1) 452 453 # Detect the end of vsync. 454 455 elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: 456 self.vsync(1) 457 458 def negedge_ram(self): 459 460 """ 461 Prepare addresses and pixel data. 462 463 States handled: * * * _ _ * _ * 464 """ 465 466 # Clock management. 467 468 # Initialise the pixel buffer if appropriate. Output starts after 469 # this cycle. 470 471 if self.cycle == 1 and self.have_pixels: 472 self.pdata = decode(self.data, self.depth) 473 self.pcycle = 1 474 self.have_pixels = 0 475 476 # Set row address (for RAM access only). 477 478 elif self.cycle == 2: 479 480 # Either assert a required address or propagate the CPU address. 481 482 if self.access_ram(): 483 self.init_row_address(self.pixel_address) 484 else: 485 self.init_row_address(self.cpu_address) 486 487 # Latch row address, set column address (for RAM access only). 488 489 elif self.cycle == 4: 490 491 # Either assert a required address or propagate the CPU address. 492 493 if self.access_ram(): 494 self.init_column_address(self.pixel_address, 0) 495 else: 496 self.init_column_address(self.cpu_address, 0) 497 498 # Set column address (for RAM access only). 499 500 elif self.cycle == 32: 501 502 # Either assert a required address or propagate the CPU address. 503 504 if self.access_ram(): 505 self.init_column_address(self.pixel_address, 1) 506 else: 507 self.init_column_address(self.cpu_address, 1) 508 509 # Update addresses. 510 511 elif self.cycle == 128: 512 513 # Advance to the next column even if an NMI is asserted. 514 515 if self.would_access_ram(): 516 self.next_horizontal() 517 518 # If the ULA accessed RAM, indicate that a read needs completing. 519 520 if self.access_ram(): 521 self.have_pixels = 1 522 523 def negedge_state(self): 524 525 "Start a new cycle." 526 527 self.cycle = rotate(self.cycle, 1) 528 self.x += 1 529 530 def negedge_pixel(self): 531 532 "Pixel production." 533 534 # For pixels within the frame, obtain and output the value. 535 536 if self.write_pixels(): 537 self.output_colour_value() 538 539 # Scale pixels horizontally, only accessing the next pixel value 540 # after the required number of scan positions. 541 542 if self.next_pixel(): 543 self.next_pixel_value() 544 545 # Detect spacing between character rows. 546 547 else: 548 self.video.colour = BLANK 549 550 def output_colour_value(self): 551 552 """ 553 Output the colour value for the current pixel by translating memory 554 content for the current mode. 555 """ 556 557 value = value_of_bits(self.pdata, self.depth) 558 self.video.colour = self.palette[value] 559 560 def next_pixel_value(self): 561 self.pdata = rotate(self.pdata, self.depth) 562 self.pcycle = rotate(self.pcycle, self.depth, zero=True) 563 564 def wrap_address(self): 565 if self.pixel_address >= SCREEN_LIMIT: 566 self.pixel_address -= self.screen_size 567 568 def init_row_address(self, address): 569 self.ram_address = (address & 0xff80) >> 7 570 571 def init_column_address(self, address, offset): 572 self.ram_address = (address & 0x7f) << 1 | offset 573 574 def cpu_transfer_high(self): 575 if self.cpu_read: 576 self.cpu_data = self.ram.data << 4 577 else: 578 self.ram.data = self.cpu_data >> 4 579 580 def cpu_transfer_low(self): 581 if self.cpu_read: 582 self.cpu_data = self.data | self.ram.data 583 else: 584 self.ram.data = self.cpu_data & 0b00001111 585 586 def cpu_read_address(self): 587 self.cpu_address = self.cpu.address 588 589 def cpu_transfer_data(self): 590 if self.cpu_read: 591 self.cpu.data = self.cpu_data 592 else: 593 self.cpu_data = self.cpu.data 594 595 def cpu_update_clock(self): 596 self.cpu_clock = not self.cpu_clock 597 if self.cpu_clock: 598 self.cpu_transfer_data() 599 else: 600 self.cpu_read_address() 601 602 def cpu_transfer_select(self): 603 self.cpu_read = self.cpu.read_not_write 604 if self.cpu_read: 605 self.ram.read_select() 606 else: 607 self.ram.write_select() 608 609 def rotate(value, depth, width=8, zero=False): 610 611 """ 612 Return 'value' rotated left by the number of bits given by 'depth', doing so 613 within a value 'width' given in bits. If 'zero' is true, rotate zero bits 614 into the lower bits when rotating. 615 """ 616 617 field = width - depth 618 top = value >> field 619 mask = 2 ** (width - depth) - 1 620 rest = value & mask 621 return (rest << depth) | (not zero and top or 0) 622 623 def value_of_bits(value, depth): 624 625 """ 626 Convert the upper bits of 'value' to a result, using 'depth' to indicate the 627 number of bits involved. 628 """ 629 630 return value >> (8 - depth) 631 632 def get_physical_colour(value): 633 634 """ 635 Return the physical colour as an RGB triple for the given 'value'. 636 """ 637 638 return value & 1, value >> 1 & 1, value >> 2 & 1 639 640 def decode(value, depth): 641 642 """ 643 Decode the given byte 'value' according to the 'depth' in bits per pixel, 644 returning a sequence of pixel values. 645 """ 646 647 if depth == 1: 648 return value 649 elif depth == 2: 650 return ((value & 128) | ((value & 8) << 3) | ((value & 64) >> 1) | ((value & 4) << 2) | 651 ((value & 32) >> 2) | ((value & 2) << 1) | ((value & 16) >> 3) | (value & 1)) 652 elif depth == 4: 653 return ((value & 128) | ((value & 32) << 1) | ((value & 8) << 2) | ((value & 2) << 3) | 654 ((value & 64) >> 3) | ((value & 16) >> 2) | ((value & 4) >> 1) | (value & 1)) 655 else: 656 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 657 658 # Convenience functions. 659 660 def encode(values, depth): 661 662 """ 663 Encode the given 'values' according to the 'depth' in bits per pixel, 664 returning a byte value for the pixels. 665 """ 666 667 result = 0 668 669 if depth == 1: 670 for value in values: 671 result = result << 1 | (value & 1) 672 elif depth == 2: 673 for value in values: 674 result = result << 1 | (value & 2) << 3 | (value & 1) 675 elif depth == 4: 676 for value in values: 677 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 678 else: 679 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 680 681 return result 682 683 def get_ula(): 684 685 "Return a ULA initialised with a memory array and video." 686 687 return ULA(get_cpu(), get_ram(), get_video()) 688 689 def get_video(): 690 691 "Return a video circuit." 692 693 return Video() 694 695 def get_ram(): 696 697 "Return an instance representing the computer's RAM hardware." 698 699 return RAM() 700 701 def get_cpu(): 702 703 "Return an instance representing the CPU." 704 705 return CPU() 706 707 # Test program providing coverage (necessary for compilers like Shedskin). 708 709 if __name__ == "__main__": 710 ula = get_ula() 711 ula.set_mode(2) 712 ula.reset() 713 ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) 714 715 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 716 # terminology). 717 718 a = update(ula) 719 720 # vim: tabstop=4 expandtab shiftwidth=4