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 * line spacing in pixels 245 * number of entries in the pixel buffer 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.spacing = MAX_HEIGHT / rows - LINES_PER_ROW # pixels between rows 268 269 # Start of unused region. 270 271 self.footer = rows * LINES_PER_ROW 272 self.margin = MAX_SCANLINE - rows * (LINES_PER_ROW + self.spacing) + self.spacing 273 274 def vsync(self, value=0): 275 276 "Signal the start of a frame." 277 278 self.csync = value 279 self.video.set_csync(value) 280 281 def hsync(self, value=0): 282 283 "Signal the end of a scanline." 284 285 self.hs = value 286 self.video.set_hs(value) 287 288 def next_frame(self): 289 290 "Signal the start of a frame." 291 292 self.line_start = self.address = self.screen_start 293 self.line = self.line_start % LINES_PER_ROW 294 self.ssub = 0 295 self.y = 0 296 self.x = 0 297 298 def next_horizontal(self): 299 300 "Visit the next horizontal position." 301 302 self.address += LINES_PER_ROW 303 self.wrap_address() 304 305 def next_vertical(self): 306 307 "Reset horizontal state within the active region of the frame." 308 309 self.y += 1 310 self.x = 0 311 312 if not self.inside_frame(): 313 return 314 315 # Support spacing between character rows. 316 317 if self.ssub: 318 self.ssub -= 1 319 return 320 321 self.line += 1 322 323 # If not on a row boundary, move to the next line. 324 325 if self.line % LINES_PER_ROW: 326 self.address = self.line_start + 1 327 self.wrap_address() 328 329 # After the end of the last line in a row, the address should already 330 # have been positioned on the last line of the next column. 331 332 else: 333 self.address -= LINES_PER_ROW - 1 334 self.wrap_address() 335 336 # Test for the footer region. 337 338 if self.spacing and self.line == self.footer: 339 self.ssub = self.margin 340 return 341 342 # Support spacing between character rows. 343 344 self.ssub = self.spacing 345 346 self.line_start = self.address 347 348 def in_frame(self): return MIN_PIXELLINE <= self.y < MAX_PIXELLINE 349 def inside_frame(self): return MIN_PIXELLINE < self.y < MAX_PIXELLINE 350 def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() 351 352 def update(self): 353 354 """ 355 Update the state of the ULA for each clock cycle. This involves updating 356 the pixel colour by reading from the pixel buffer. 357 """ 358 359 # Detect the end of the scanline. 360 361 if self.x == MAX_SCANPOS: 362 self.next_vertical() 363 364 # Detect the end of the frame. 365 366 if self.y == MAX_SCANLINE: 367 self.next_frame() 368 369 370 371 # Clock management. 372 373 would_access_ram = self.access == 0 and self.read_pixels() and not self.ssub 374 access_ram = not self.nmi and would_access_ram 375 376 # Set row address (for ULA access only). 377 378 if self.cycle[0]: 379 380 # Either assert a required address or propagate the CPU address. 381 382 if access_ram: 383 self.init_row_address(self.address) 384 else: 385 self.init_row_address(self.cpu_address) 386 387 # Initialise the pixel buffer if appropriate. 388 389 if not self.writing_pixels and self.have_pixels: 390 self.buffer_index = 0 391 self.fill_pixel_buffer() 392 self.writing_pixels = 1 393 394 # Latch row address, set column address (for ULA access only). 395 396 elif self.cycle[1]: 397 398 # Select an address needed by the ULA or CPU. 399 400 self.ram.row_select(self.ram_address) 401 402 # Either assert a required address or propagate the CPU address. 403 404 if access_ram: 405 self.init_column_address(self.address, 0) 406 else: 407 self.init_column_address(self.cpu_address, 0) 408 409 # Latch column address. 410 411 elif self.cycle[2]: 412 413 # Select an address needed by the ULA or CPU. 414 415 self.ram.column_select(self.ram_address) 416 417 # Read 4 bits (for ULA access only). 418 # NOTE: Perhaps map alternate bits, not half-bytes. 419 420 elif self.cycle[3]: 421 422 # Either read from a required address or transfer CPU data. 423 424 if access_ram: 425 self.data = self.ram.data << 4 426 else: 427 self.cpu_transfer_high() 428 429 # Set column address (for ULA access only). 430 431 elif self.cycle[4]: 432 self.ram.column_deselect() 433 434 # Either assert a required address or propagate the CPU address. 435 436 if access_ram: 437 self.init_column_address(self.address, 1) 438 else: 439 self.init_column_address(self.cpu_address, 1) 440 441 # Latch column address. 442 443 elif self.cycle[5]: 444 445 # Select an address needed by the ULA or CPU. 446 447 self.ram.column_select(self.ram_address) 448 449 # Read 4 bits (for ULA access only). 450 # NOTE: Perhaps map alternate bits, not half-bytes. 451 452 elif self.cycle[6]: 453 454 # Either read from a required address or transfer CPU data. 455 456 if access_ram: 457 self.data = self.data | self.ram.data 458 self.have_pixels = 1 459 else: 460 self.cpu_transfer_low() 461 462 # Advance to the next column even if an NMI is asserted. 463 464 if would_access_ram: 465 self.next_horizontal() 466 467 # Reset addresses. 468 469 elif self.cycle[7]: 470 self.ram.column_deselect() 471 self.ram.row_deselect() 472 473 # Update the RAM access controller. 474 475 self.access = (self.access + 1) % self.access_frequency 476 477 # Update the state of the device. 478 479 self.cycle.set_input(self.cycle[7]) 480 self.cycle.shift() 481 482 483 484 # Video signalling. 485 486 # Detect any sync conditions. 487 488 if self.x == 0: 489 self.hsync() 490 if self.y == 0: 491 self.vsync() 492 self.irq_vsync = 0 493 elif self.y == MAX_PIXELLINE: 494 self.irq_vsync = 1 495 496 # Detect the end of hsync. 497 498 elif self.x == MAX_HSYNC: 499 self.hsync(1) 500 501 # Detect the end of vsync. 502 503 elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: 504 self.vsync(1) 505 506 507 self.x += 1 508 509 510 # Pixel production. 511 512 # Detect spacing between character rows. 513 514 if not self.writing_pixels or self.ssub: 515 self.video.colour = BLANK 516 517 # For pixels within the frame, obtain and output the value. 518 519 else: 520 self.video.colour = self.buffer[self.buffer_index] 521 522 # Scale pixels horizontally, only accessing the next pixel value 523 # after the required number of scan positions. 524 525 if self.x % self.xscale == 0: 526 self.buffer_index += 1 527 528 # Finish writing pixels. 529 530 if self.x % PIXEL_POSITIONS == 0: 531 self.writing_pixels = 0 532 533 def fill_pixel_buffer(self): 534 535 """ 536 Fill the pixel buffer by translating memory content for the current 537 mode. 538 """ 539 540 byte_value = self.data # which should have been read automatically 541 542 i = 0 543 for colour in decode(byte_value, self.depth): 544 self.buffer[i] = get_physical_colour(self.palette[colour]) 545 i += 1 546 547 def wrap_address(self): 548 if self.address >= SCREEN_LIMIT: 549 self.address -= self.screen_size 550 551 def init_row_address(self, address): 552 self.ram_address = (address & 0xff80) >> 7 553 554 def init_column_address(self, address, offset): 555 self.ram_address = (address & 0x7f) << 1 | offset 556 557 def cpu_transfer_high(self): 558 if self.cpu_read: 559 self.cpu_data = self.ram.data << 4 560 561 def cpu_transfer_low(self): 562 if self.cpu_read: 563 self.cpu_data = self.data | self.ram.data 564 565 def get_physical_colour(value): 566 567 """ 568 Return the physical colour as an RGB triple for the given 'value'. 569 """ 570 571 return value & 1, value >> 1 & 1, value >> 2 & 1 572 573 def decode(value, depth): 574 575 """ 576 Decode the given byte 'value' according to the 'depth' in bits per pixel, 577 returning a sequence of pixel values. 578 """ 579 580 if depth == 1: 581 return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, 582 value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) 583 elif depth == 2: 584 return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, 585 value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) 586 elif depth == 4: 587 return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, 588 value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) 589 else: 590 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 591 592 # Convenience functions. 593 594 def encode(values, depth): 595 596 """ 597 Encode the given 'values' according to the 'depth' in bits per pixel, 598 returning a byte value for the pixels. 599 """ 600 601 result = 0 602 603 if depth == 1: 604 for value in values: 605 result = result << 1 | (value & 1) 606 elif depth == 2: 607 for value in values: 608 result = result << 1 | (value & 2) << 3 | (value & 1) 609 elif depth == 4: 610 for value in values: 611 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 612 else: 613 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 614 615 return result 616 617 def get_ula(): 618 619 "Return a ULA initialised with a memory array and video." 620 621 return ULA(get_ram(), get_video()) 622 623 def get_video(): 624 625 "Return a video circuit." 626 627 return Video() 628 629 def get_ram(): 630 631 "Return an instance representing the computer's RAM hardware." 632 633 return RAM() 634 635 # Test program providing coverage (necessary for compilers like Shedskin). 636 637 if __name__ == "__main__": 638 ula = get_ula() 639 ula.set_mode(2) 640 ula.reset() 641 ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) 642 643 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 644 # terminology). 645 646 a = update(ula) 647 648 # vim: tabstop=4 expandtab shiftwidth=4