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