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