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 PIXEL_POSITIONS = 8 # the number of pixel positions per byte 41 # (doubled or quadrupled in lower resolutions) 42 43 SCREEN_LIMIT = 0x8000 # the first address after the screen memory 44 MAX_MEMORY = 0x10000 # the number of addressable memory locations 45 MAX_RAM = 0x10000 # the number of addressable RAM locations (64Kb in each IC) 46 BLANK = (0, 0, 0) 47 48 def update(ula): 49 50 """ 51 Update the 'ula' for one frame. Return the resulting screen. 52 """ 53 54 video = ula.video 55 56 i = 0 57 limit = MAX_SCANLINE * MAX_SCANPOS 58 while i < limit: 59 ula.update() 60 video.update() 61 i += 1 62 63 return video.screen 64 65 class Video: 66 67 """ 68 A class representing the video circuitry. 69 """ 70 71 def __init__(self): 72 self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT)) 73 self.colour = BLANK 74 self.csync = 1 75 self.hs = 1 76 self.x = 0 77 self.y = 0 78 79 def set_csync(self, value): 80 if self.csync and not value: 81 self.y = 0 82 self.pos = 0 83 self.csync = value 84 85 def set_hs(self, value): 86 if self.hs and not value: 87 self.x = 0 88 self.y += 1 89 self.hs = value 90 91 def update(self): 92 if MIN_PIXELLINE <= self.y < MAX_PIXELLINE: 93 if MIN_PIXELPOS + 8 <= self.x < MAX_PIXELPOS + 8: 94 self.screen[self.pos] = self.colour[0]; self.pos += 1 95 self.screen[self.pos] = self.colour[1]; self.pos += 1 96 self.screen[self.pos] = self.colour[2]; self.pos += 1 97 self.x += 1 98 99 class RAM: 100 101 """ 102 A class representing the RAM circuits (IC4 to IC7). Each circuit 103 traditionally holds 64 kilobits, with each access obtaining 1 bit from each 104 IC, and thus two accesses being required to obtain a whole byte. Here, we 105 model the circuits with a list of 65536 half-bytes with each bit in a 106 half-byte representing a bit stored on a separate IC. 107 """ 108 109 def __init__(self): 110 111 "Initialise the RAM circuits." 112 113 self.memory = [0] * MAX_RAM 114 self.row_address = 0 115 self.column_address = 0 116 self.data = 0 117 118 def row_select(self, address): 119 120 "The operation of asserting a row 'address' via RA0...RA7." 121 122 self.row_address = address 123 124 def row_deselect(self): 125 pass 126 127 def column_select(self, address): 128 129 "The operation of asserting a column 'address' via RA0...RA7." 130 131 self.column_address = address 132 133 # Read the data. 134 135 self.data = self.memory[self.row_address << 8 | self.column_address] 136 137 def column_deselect(self): 138 pass 139 140 # Convenience methods. 141 142 def fill(self, start, end, value): 143 for i in xrange(start, end): 144 self.memory[i << 1] = value >> 4 145 self.memory[i << 1 | 0x1] = value & 0xf 146 147 class ShiftRegister: 148 149 """ 150 A class representing a shift register, used for the internal state of the 151 ULA within each 2MHz period. 152 """ 153 154 def __init__(self): 155 self.state = [0] * 8 156 self.input = 0 157 158 def set_input(self, input): 159 self.input = input 160 161 def shift(self): 162 163 # NOTE: This is not meant to be "nice" Python, but instead models the 164 # NOTE: propagation of state through the latches. 165 166 self.state[0], self.state[1], self.state[2], self.state[3], \ 167 self.state[4], self.state[5], self.state[6], self.state[7] = \ 168 self.input, self.state[0], self.state[1], self.state[2], \ 169 self.state[3], self.state[4], self.state[5], self.state[6] 170 171 def __getitem__(self, i): 172 return self.state[i] 173 174 class ULA: 175 176 """ 177 A class providing the ULA functionality. Instances of this class refer to 178 the system memory, maintain internal state (such as information about the 179 current screen mode), and provide outputs (such as the current pixel 180 colour). 181 """ 182 183 modes = [ 184 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 185 (640, 1, 25), (320, 1, 32), (160, 2, 32), 186 (320, 1, 25) 187 ] 188 189 palette = range(0, 8) * 2 190 191 def __init__(self, ram, video): 192 193 "Initialise the ULA with the given 'ram' and 'video' instances." 194 195 self.ram = ram 196 self.video = video 197 self.set_mode(6) 198 199 self.reset() 200 201 def reset(self): 202 203 "Reset the ULA." 204 205 # General state. 206 207 self.nmi = 0 # no NMI asserted initially 208 self.irq_vsync = 0 # no IRQ asserted initially 209 210 # Communication. 211 212 self.ram_address = 0 # address given to the RAM via RA0...RA7 213 self.data = 0 # data read from the RAM via RAM0...RAM3 214 self.cpu_address = 0 # address selected by the CPU via A0...A15 215 self.cpu_read = 0 # data read/write by the CPU selected using R/W 216 217 # Internal state. 218 219 self.access = 0 # counter used to determine whether a byte needs reading 220 self.have_pixels = 0 # whether pixel data has been read 221 self.writing_pixels = 0 # whether pixel data can be written 222 self.buffer = [BLANK]*8 # pixel buffer for decoded RAM data 223 224 self.cycle = ShiftRegister() # 8-state counter within each 2MHz period 225 226 self.cycle.set_input(1) # assert the input to set the first state output 227 self.cycle.shift() 228 self.cycle.set_input(0) # reset the input since only one state output will be active 229 230 self.next_frame() 231 232 def set_mode(self, mode): 233 234 """ 235 For the given 'mode', initialise the... 236 237 * width in pixels 238 * colour depth in bits per pixel 239 * number of character rows 240 * character row size in bytes 241 * screen size in bytes 242 * default screen start address 243 * horizontal pixel scaling factor 244 * row height in pixels 245 * display height in pixels 246 247 The ULA should be reset after a mode switch in order to cleanly display 248 a full screen. 249 """ 250 251 self.width, self.depth, rows = self.modes[mode] 252 253 columns = (self.width * self.depth) / 8 # bits read -> bytes read 254 self.access_frequency = 80 / columns # cycle frequency for reading bytes 255 row_size = columns * LINES_PER_ROW 256 257 # Memory access configuration. 258 # Note the limitation on positioning the screen start. 259 260 screen_size = row_size * rows 261 self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 262 self.screen_size = SCREEN_LIMIT - self.screen_start 263 264 # Scanline configuration. 265 266 self.xscale = MAX_WIDTH / self.width # pixel width in display pixels 267 self.row_height = MAX_HEIGHT / rows # row height in display pixels 268 self.display_height = rows * self.row_height # display height in pixels 269 270 def vsync(self, value=0): 271 272 "Signal the start of a frame." 273 274 self.csync = value 275 self.video.set_csync(value) 276 277 def hsync(self, value=0): 278 279 "Signal the end of a scanline." 280 281 self.hs = value 282 self.video.set_hs(value) 283 284 def next_frame(self): 285 286 "Signal the start of a frame." 287 288 self.line_start = self.address = self.screen_start 289 self.line = self.line_start % LINES_PER_ROW 290 self.y = 0 291 self.x = 0 292 293 def next_horizontal(self): 294 295 "Visit the next horizontal position." 296 297 self.address += LINES_PER_ROW 298 self.wrap_address() 299 300 def next_vertical(self): 301 302 "Reset horizontal state within the active region of the frame." 303 304 self.y += 1 305 self.x = 0 306 307 if not self.inside_frame(): 308 return 309 310 self.line += 1 311 312 # After the end of the last line in a row, the address should already 313 # have been positioned on the last line of the next column. 314 315 if self.line == self.row_height: 316 self.address -= LINES_PER_ROW - 1 317 self.wrap_address() 318 self.line = 0 319 320 # Support spacing between character rows. 321 322 elif not self.in_line(): 323 return 324 325 # If not on a row boundary, move to the next line. Here, the address 326 # needs bringing back to the previous character row. 327 328 else: 329 self.address = self.line_start + 1 330 self.wrap_address() 331 332 # Record the position of the start of the pixel row. 333 334 self.line_start = self.address 335 336 def in_line(self): return self.line < LINES_PER_ROW 337 def in_frame(self): return MIN_PIXELLINE <= self.y < (MIN_PIXELLINE + self.display_height) 338 def inside_frame(self): return MIN_PIXELLINE < self.y < (MIN_PIXELLINE + self.display_height) 339 def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() 340 341 def update(self): 342 343 """ 344 Update the state of the ULA for each clock cycle. This involves updating 345 the pixel colour by reading from the pixel buffer. 346 """ 347 348 # Detect the end of the scanline. 349 350 if self.x == MAX_SCANPOS: 351 self.next_vertical() 352 353 # Detect the end of the frame. 354 355 if self.y == MAX_SCANLINE: 356 self.next_frame() 357 358 359 360 # Clock management. 361 362 would_access_ram = self.access == 0 and self.read_pixels() and self.in_line() 363 access_ram = not self.nmi and would_access_ram 364 365 # Set row address (for ULA access only). 366 367 if self.cycle[0]: 368 369 # Either assert a required address or propagate the CPU address. 370 371 if access_ram: 372 self.init_row_address(self.address) 373 else: 374 self.init_row_address(self.cpu_address) 375 376 # Initialise the pixel buffer if appropriate. 377 378 if not self.writing_pixels and self.have_pixels: 379 self.buffer_index = 0 380 self.fill_pixel_buffer() 381 self.writing_pixels = 1 382 383 # Latch row address, set column address (for ULA access only). 384 385 elif self.cycle[1]: 386 387 # Select an address needed by the ULA or CPU. 388 389 self.ram.row_select(self.ram_address) 390 391 # Either assert a required address or propagate the CPU address. 392 393 if access_ram: 394 self.init_column_address(self.address, 0) 395 else: 396 self.init_column_address(self.cpu_address, 0) 397 398 # Latch column address. 399 400 elif self.cycle[2]: 401 402 # Select an address needed by the ULA or CPU. 403 404 self.ram.column_select(self.ram_address) 405 406 # Read 4 bits (for ULA access only). 407 # NOTE: Perhaps map alternate bits, not half-bytes. 408 409 elif self.cycle[3]: 410 411 # Either read from a required address or transfer CPU data. 412 413 if access_ram: 414 self.data = self.ram.data << 4 415 else: 416 self.cpu_transfer_high() 417 418 # Set column address (for ULA access only). 419 420 elif self.cycle[4]: 421 self.ram.column_deselect() 422 423 # Either assert a required address or propagate the CPU address. 424 425 if access_ram: 426 self.init_column_address(self.address, 1) 427 else: 428 self.init_column_address(self.cpu_address, 1) 429 430 # Latch column address. 431 432 elif self.cycle[5]: 433 434 # Select an address needed by the ULA or CPU. 435 436 self.ram.column_select(self.ram_address) 437 438 # Read 4 bits (for ULA access only). 439 # NOTE: Perhaps map alternate bits, not half-bytes. 440 441 elif self.cycle[6]: 442 443 # Either read from a required address or transfer CPU data. 444 445 if access_ram: 446 self.data = self.data | self.ram.data 447 self.have_pixels = 1 448 else: 449 self.cpu_transfer_low() 450 451 # Advance to the next column even if an NMI is asserted. 452 453 if would_access_ram: 454 self.next_horizontal() 455 456 # Reset addresses. 457 458 elif self.cycle[7]: 459 self.ram.column_deselect() 460 self.ram.row_deselect() 461 462 # Update the RAM access controller. 463 464 self.access = (self.access + 1) % self.access_frequency 465 466 # Update the state of the device. 467 468 self.cycle.set_input(self.cycle[7]) 469 self.cycle.shift() 470 471 472 473 # Video signalling. 474 475 # Detect any sync conditions. 476 477 if self.x == 0: 478 self.hsync() 479 if self.y == 0: 480 self.vsync() 481 self.irq_vsync = 0 482 elif self.y == MAX_PIXELLINE: 483 self.irq_vsync = 1 484 485 # Detect the end of hsync. 486 487 elif self.x == MAX_HSYNC: 488 self.hsync(1) 489 490 # Detect the end of vsync. 491 492 elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: 493 self.vsync(1) 494 495 496 self.x += 1 497 498 499 # Pixel production. 500 501 # Detect spacing between character rows. 502 503 if not self.writing_pixels or not self.in_line(): 504 self.video.colour = BLANK 505 506 # For pixels within the frame, obtain and output the value. 507 508 else: 509 self.video.colour = self.buffer[self.buffer_index] 510 511 # Scale pixels horizontally, only accessing the next pixel value 512 # after the required number of scan positions. 513 514 if self.x % self.xscale == 0: 515 self.buffer_index += 1 516 517 # Finish writing pixels. 518 519 if self.x % PIXEL_POSITIONS == 0: 520 self.writing_pixels = 0 521 522 def fill_pixel_buffer(self): 523 524 """ 525 Fill the pixel buffer by translating memory content for the current 526 mode. 527 """ 528 529 # Rearrange the byte value. 530 531 byte_value = decode(self.data, self.depth) 532 533 i = 0 534 o = 0 535 while i < 8: 536 colour = value_of_bits(byte_value[i:i+self.depth]) 537 self.buffer[o] = get_physical_colour(self.palette[colour]) 538 i += self.depth 539 o += 1 540 541 def wrap_address(self): 542 if self.address >= SCREEN_LIMIT: 543 self.address -= self.screen_size 544 545 def init_row_address(self, address): 546 self.ram_address = (address & 0xff80) >> 7 547 548 def init_column_address(self, address, offset): 549 self.ram_address = (address & 0x7f) << 1 | offset 550 551 def cpu_transfer_high(self): 552 if self.cpu_read: 553 self.cpu_data = self.ram.data << 4 554 555 def cpu_transfer_low(self): 556 if self.cpu_read: 557 self.cpu_data = self.data | self.ram.data 558 559 def value_of_bits(bits): 560 561 "Convert the sequence of 'bits' into a value." 562 563 value = 0 564 for bit in bits: 565 value *= 2 566 value += bit and 1 or 0 567 return value 568 569 def get_physical_colour(value): 570 571 """ 572 Return the physical colour as an RGB triple for the given 'value'. 573 """ 574 575 return value & 1, value >> 1 & 1, value >> 2 & 1 576 577 def decode(value, depth): 578 579 """ 580 Decode the given byte 'value' according to the 'depth' in bits per pixel, 581 returning a sequence of pixel values. 582 """ 583 584 if depth == 1: 585 return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, 586 value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) 587 elif depth == 2: 588 return (value >> 7, value >> 3 & 1, value >> 6 & 1, value >> 2 & 1, 589 value >> 5 & 1, value >> 1 & 1, value >> 4 & 1, value & 1) 590 elif depth == 4: 591 return (value >> 7, value >> 5 & 1, value >> 3 & 1, value >> 1 & 1, 592 value >> 6 & 1, value >> 4 & 1, value >> 2 & 1, value & 1) 593 else: 594 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 595 596 # Convenience functions. 597 598 def encode(values, depth): 599 600 """ 601 Encode the given 'values' according to the 'depth' in bits per pixel, 602 returning a byte value for the pixels. 603 """ 604 605 result = 0 606 607 if depth == 1: 608 for value in values: 609 result = result << 1 | (value & 1) 610 elif depth == 2: 611 for value in values: 612 result = result << 1 | (value & 2) << 3 | (value & 1) 613 elif depth == 4: 614 for value in values: 615 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 616 else: 617 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 618 619 return result 620 621 def get_ula(): 622 623 "Return a ULA initialised with a memory array and video." 624 625 return ULA(get_ram(), get_video()) 626 627 def get_video(): 628 629 "Return a video circuit." 630 631 return Video() 632 633 def get_ram(): 634 635 "Return an instance representing the computer's RAM hardware." 636 637 return RAM() 638 639 # Test program providing coverage (necessary for compilers like Shedskin). 640 641 if __name__ == "__main__": 642 ula = get_ula() 643 ula.set_mode(2) 644 ula.reset() 645 ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) 646 647 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 648 # terminology). 649 650 a = update(ula) 651 652 # vim: tabstop=4 expandtab shiftwidth=4