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