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