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.posedge() 57 video.update() 58 ula.negedge() 59 i += 1 60 61 return video.screen 62 63 class Video: 64 65 """ 66 A class representing the video circuitry. 67 """ 68 69 def __init__(self): 70 self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT)) 71 self.colour = BLANK 72 self.csync = 1 73 self.hs = 1 74 self.x = 0 75 self.y = 0 76 77 def set_csync(self, value): 78 if self.csync and not value: 79 self.y = 0 80 self.pos = 0 81 self.csync = value 82 83 def set_hs(self, value): 84 if self.hs and not value: 85 self.x = 0 86 self.y += 1 87 self.hs = value 88 89 def update(self): 90 if MIN_PIXELLINE <= self.y < MAX_PIXELLINE: 91 if MIN_PIXELPOS + 8 <= self.x < MAX_PIXELPOS + 8: 92 self.screen[self.pos] = self.colour[0]; self.pos += 1 93 self.screen[self.pos] = self.colour[1]; self.pos += 1 94 self.screen[self.pos] = self.colour[2]; self.pos += 1 95 self.x += 1 96 97 class RAM: 98 99 """ 100 A class representing the RAM circuits (IC4 to IC7). Each circuit 101 traditionally holds 64 kilobits, with each access obtaining 1 bit from each 102 IC, and thus two accesses being required to obtain a whole byte. Here, we 103 model the circuits with a list of 65536 half-bytes with each bit in a 104 half-byte representing a bit stored on a separate IC. 105 """ 106 107 def __init__(self): 108 109 "Initialise the RAM circuits." 110 111 self.memory = [0] * MAX_RAM 112 self.row_address = 0 113 self.column_address = 0 114 self.data = 0 115 116 def row_select(self, address): 117 118 "The operation of asserting a row 'address' via RA0...RA7." 119 120 self.row_address = address 121 122 def row_deselect(self): 123 pass 124 125 def column_select(self, address): 126 127 "The operation of asserting a column 'address' via RA0...RA7." 128 129 self.column_address = address 130 131 # Read the data. 132 133 self.data = self.memory[self.row_address << 8 | self.column_address] 134 135 def column_deselect(self): 136 pass 137 138 # Convenience methods. 139 140 def fill(self, start, end, value): 141 for i in xrange(start, end): 142 self.memory[i << 1] = value >> 4 143 self.memory[i << 1 | 0x1] = value & 0xf 144 145 class ULA: 146 147 """ 148 A class providing the ULA functionality. Instances of this class refer to 149 the system memory, maintain internal state (such as information about the 150 current screen mode), and provide outputs (such as the current pixel 151 colour). 152 """ 153 154 modes = [ 155 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 156 (640, 1, 25), (320, 1, 32), (160, 2, 32), 157 (320, 1, 25) 158 ] 159 160 def __init__(self, ram, video): 161 162 "Initialise the ULA with the given 'ram' and 'video' instances." 163 164 self.ram = ram 165 self.video = video 166 self.set_mode(6) 167 self.palette = map(get_physical_colour, range(0, 8) * 2) 168 169 self.reset() 170 171 def reset(self): 172 173 "Reset the ULA." 174 175 # General state. 176 177 self.nmi = 0 # no NMI asserted initially 178 self.irq_vsync = 0 # no IRQ asserted initially 179 180 # Communication. 181 182 self.ram_address = 0 # address given to the RAM via RA0...RA7 183 self.data = 0 # data read from the RAM via RAM0...RAM3 184 self.cpu_address = 0 # address selected by the CPU via A0...A15 185 self.cpu_read = 0 # data read/write by the CPU selected using R/W 186 187 # Internal state. 188 189 self.access = 0 # counter used to determine whether a byte needs reading 190 self.have_pixels = 0 # whether pixel data has been read 191 self.pdata = 0 # decoded RAM data for pixel output 192 self.cycle = 1 # 8-state counter within each 2MHz period 193 self.pcycle = 0 # 8/4/2-state pixel output counter 194 195 self.next_frame() 196 197 def set_mode(self, mode): 198 199 """ 200 For the given 'mode', initialise the... 201 202 * width in pixels 203 * colour depth in bits per pixel 204 * number of character rows 205 * character row size in bytes 206 * screen size in bytes 207 * default screen start address 208 * horizontal pixel scaling factor 209 * row height in pixels 210 * display height in pixels 211 212 The ULA should be reset after a mode switch in order to cleanly display 213 a full screen. 214 """ 215 216 self.width, self.depth, rows = self.modes[mode] 217 218 columns = (self.width * self.depth) / 8 # bits read -> bytes read 219 self.access_frequency = 80 / columns # cycle frequency for reading bytes 220 row_size = columns * LINES_PER_ROW 221 222 # Memory access configuration. 223 # Note the limitation on positioning the screen start. 224 225 screen_size = row_size * rows 226 self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 227 self.screen_size = SCREEN_LIMIT - self.screen_start 228 229 # Scanline configuration. 230 231 self.xscale = MAX_WIDTH / self.width # pixel width in display pixels 232 self.row_height = MAX_HEIGHT / rows # row height in display pixels 233 self.display_height = rows * self.row_height # display height in pixels 234 235 def vsync(self, value=0): 236 237 "Signal the start of a frame." 238 239 self.csync = value 240 self.video.set_csync(value) 241 242 def hsync(self, value=0): 243 244 "Signal the end of a scanline." 245 246 self.hs = value 247 self.video.set_hs(value) 248 249 def next_frame(self): 250 251 "Signal the start of a frame." 252 253 self.line_start = self.address = self.screen_start 254 self.line = self.line_start % LINES_PER_ROW 255 self.y = 0 256 self.x = 0 257 258 def next_horizontal(self): 259 260 "Visit the next horizontal position." 261 262 self.address += LINES_PER_ROW 263 self.wrap_address() 264 265 def next_vertical(self): 266 267 "Reset horizontal state within the active region of the frame." 268 269 self.y += 1 270 self.x = 0 271 272 if self.inside_frame(): 273 self.line += 1 274 275 # At the end of a row... 276 277 if self.line == self.row_height: 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 self.address -= LINES_PER_ROW - 1 283 self.wrap_address() 284 self.line = 0 285 286 # Record the position of the start of the pixel row. 287 288 self.line_start = self.address 289 290 # Before any spacing between character rows... 291 292 elif self.in_line(): 293 294 # If not on a row boundary, move to the next line. Here, the address 295 # needs bringing back to the previous character row. 296 297 self.address = self.line_start + 1 298 self.wrap_address() 299 300 # Record the position of the start of the pixel row. 301 302 self.line_start = self.address 303 304 def in_line(self): return self.line < LINES_PER_ROW 305 def in_frame(self): return MIN_PIXELLINE <= self.y < (MIN_PIXELLINE + self.display_height) 306 def inside_frame(self): return MIN_PIXELLINE < self.y < (MIN_PIXELLINE + self.display_height) 307 def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() 308 def write_pixels(self): return self.pcycle != 0 309 def next_pixel(self): return self.xscale == 1 or (self.xscale == 2 and self.cycle & 0b10101010) or (self.xscale == 4 and self.cycle & 0b10001000) 310 311 def posedge(self): 312 313 """ 314 Update the state of the ULA for each clock cycle. This involves updating 315 the pixel colour by reading from the pixel buffer. 316 """ 317 318 # Video signalling. 319 320 # Detect the end of the scanline. 321 322 if self.x == MAX_SCANPOS: 323 self.next_vertical() 324 325 # Detect the end of the frame. 326 327 if self.y == MAX_SCANLINE: 328 self.next_frame() 329 330 331 332 # Detect any sync conditions. 333 334 if self.x == 0: 335 self.hsync() 336 if self.y == 0: 337 self.vsync() 338 self.irq_vsync = 0 339 elif self.y == MAX_PIXELLINE: 340 self.irq_vsync = 1 341 342 # Detect the end of hsync. 343 344 elif self.x == MAX_HSYNC: 345 self.hsync(1) 346 347 # Detect the end of vsync. 348 349 elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: 350 self.vsync(1) 351 352 353 354 # Clock management. 355 356 would_access_ram = self.access == 0 and self.read_pixels() and self.in_line() 357 access_ram = not self.nmi and would_access_ram 358 359 # Set row address (for ULA access only). 360 361 if self.cycle == 1: 362 363 # Either assert a required address or propagate the CPU address. 364 365 if access_ram: 366 self.init_row_address(self.address) 367 else: 368 self.init_row_address(self.cpu_address) 369 370 # Latch row address, set column address (for ULA access only). 371 372 elif self.cycle == 2: 373 374 # Select an address needed by the ULA or CPU. 375 376 self.ram.row_select(self.ram_address) 377 378 # Either assert a required address or propagate the CPU address. 379 380 if access_ram: 381 self.init_column_address(self.address, 0) 382 else: 383 self.init_column_address(self.cpu_address, 0) 384 385 # Latch column address. 386 387 elif self.cycle == 4: 388 389 # Select an address needed by the ULA or CPU. 390 391 self.ram.column_select(self.ram_address) 392 393 # Read 4 bits (for ULA access only). 394 395 elif self.cycle == 8: 396 397 # Either read from a required address or transfer CPU data. 398 399 if access_ram: 400 self.data = self.ram.data << 4 401 else: 402 self.cpu_transfer_high() 403 404 # Set column address (for ULA access only). 405 406 elif self.cycle == 16: 407 self.ram.column_deselect() 408 409 # Either assert a required address or propagate the CPU address. 410 411 if access_ram: 412 self.init_column_address(self.address, 1) 413 else: 414 self.init_column_address(self.cpu_address, 1) 415 416 # Latch column address. 417 418 elif self.cycle == 32: 419 420 # Select an address needed by the ULA or CPU. 421 422 self.ram.column_select(self.ram_address) 423 424 # Read 4 bits (for ULA access only). 425 426 elif self.cycle == 64: 427 428 # Either read from a required address or transfer CPU data. 429 430 if access_ram: 431 self.data = self.data | self.ram.data 432 self.have_pixels = 1 433 else: 434 self.cpu_transfer_low() 435 436 # Advance to the next column even if an NMI is asserted. 437 438 if would_access_ram: 439 self.next_horizontal() 440 441 # Reset addresses. 442 443 elif self.cycle == 128: 444 self.ram.column_deselect() 445 self.ram.row_deselect() 446 447 # Update the RAM access controller. 448 449 self.access = (self.access + 1) % self.access_frequency 450 451 452 453 # Pixel production. 454 455 # For pixels within the frame, obtain and output the value. 456 457 if self.write_pixels(): 458 self.output_colour_value() 459 460 # Scale pixels horizontally, only accessing the next pixel value 461 # after the required number of scan positions. 462 463 if self.next_pixel(): 464 self.next_pixel_value() 465 466 # Detect spacing between character rows. 467 468 else: 469 self.video.colour = BLANK 470 471 def negedge(self): 472 473 "Update the state of the device." 474 475 # Initialise the pixel buffer if appropriate. Output starts after 476 # this cycle. 477 478 if self.cycle == 128 and self.have_pixels: 479 self.pdata = decode(self.data, self.depth) 480 self.pcycle = 1 481 self.have_pixels = 0 482 483 # Start a new cycle. 484 485 self.cycle = rotate(self.cycle, 1) 486 self.x += 1 487 488 def output_colour_value(self): 489 490 """ 491 Output the colour value for the current pixel by translating memory 492 content for the current mode. 493 """ 494 495 value = value_of_bits(self.pdata, self.depth) 496 self.video.colour = self.palette[value] 497 498 def next_pixel_value(self): 499 self.pdata = rotate(self.pdata, self.depth) 500 self.pcycle = rotate(self.pcycle, self.depth, zero=True) 501 502 def wrap_address(self): 503 if self.address >= SCREEN_LIMIT: 504 self.address -= self.screen_size 505 506 def init_row_address(self, address): 507 self.ram_address = (address & 0xff80) >> 7 508 509 def init_column_address(self, address, offset): 510 self.ram_address = (address & 0x7f) << 1 | offset 511 512 def cpu_transfer_high(self): 513 if self.cpu_read: 514 self.cpu_data = self.ram.data << 4 515 516 def cpu_transfer_low(self): 517 if self.cpu_read: 518 self.cpu_data = self.data | self.ram.data 519 520 def rotate(value, depth, width=8, zero=False): 521 522 """ 523 Return 'value' rotated left by the number of bits given by 'depth', doing so 524 within a value 'width' given in bits. If 'zero' is true, rotate zero bits 525 into the lower bits when rotating. 526 """ 527 528 field = width - depth 529 top = value >> field 530 mask = 2 ** (width - depth) - 1 531 rest = value & mask 532 return (rest << depth) | (not zero and top or 0) 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