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