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