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