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