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