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