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