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