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