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