paul@1 | 1 | #!/usr/bin/env python |
paul@1 | 2 | |
paul@1 | 3 | """ |
paul@1 | 4 | Acorn Electron ULA simulation. |
paul@71 | 5 | |
paul@84 | 6 | Copyright (C) 2011, 2012, 2013, 2014, 2016 Paul Boddie <paul@boddie.org.uk> |
paul@71 | 7 | |
paul@71 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@71 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@71 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@71 | 11 | version. |
paul@71 | 12 | |
paul@71 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT ANY |
paul@71 | 14 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A |
paul@71 | 15 | PARTICULAR PURPOSE. See the GNU General Public License for more details. |
paul@71 | 16 | |
paul@71 | 17 | You should have received a copy of the GNU General Public License along |
paul@71 | 18 | with this program. If not, see <http://www.gnu.org/licenses/>. |
paul@1 | 19 | """ |
paul@1 | 20 | |
paul@29 | 21 | from array import array |
paul@29 | 22 | from itertools import repeat |
paul@29 | 23 | |
paul@22 | 24 | LINES_PER_ROW = 8 # the number of pixel lines per character row |
paul@22 | 25 | MAX_HEIGHT = 256 # the height of the screen in pixels |
paul@40 | 26 | MAX_WIDTH = 640 # the width of the screen in pixels |
paul@40 | 27 | |
paul@40 | 28 | MAX_CSYNC = 2 # the scanline during which vsync ends |
paul@84 | 29 | MIN_PIXELLINE = 28 # the first scanline involving pixel generation |
paul@22 | 30 | MAX_SCANLINE = 312 # the number of scanlines in each frame |
paul@40 | 31 | |
paul@40 | 32 | MAX_PIXELLINE = MIN_PIXELLINE + MAX_HEIGHT |
paul@40 | 33 | |
paul@84 | 34 | MAX_HSYNC = 64 # the number of cycles in each hsync period |
paul@42 | 35 | MIN_PIXELPOS = 256 # the first cycle involving pixel generation |
paul@40 | 36 | MAX_SCANPOS = 1024 # the number of cycles in each scanline |
paul@40 | 37 | |
paul@40 | 38 | MAX_PIXELPOS = MIN_PIXELPOS + MAX_WIDTH |
paul@40 | 39 | |
paul@22 | 40 | SCREEN_LIMIT = 0x8000 # the first address after the screen memory |
paul@22 | 41 | MAX_MEMORY = 0x10000 # the number of addressable memory locations |
paul@40 | 42 | MAX_RAM = 0x10000 # the number of addressable RAM locations (64Kb in each IC) |
paul@3 | 43 | BLANK = (0, 0, 0) |
paul@1 | 44 | |
paul@29 | 45 | def update(ula): |
paul@1 | 46 | |
paul@1 | 47 | """ |
paul@31 | 48 | Update the 'ula' for one frame. Return the resulting screen. |
paul@31 | 49 | """ |
paul@31 | 50 | |
paul@31 | 51 | video = ula.video |
paul@31 | 52 | |
paul@31 | 53 | i = 0 |
paul@31 | 54 | limit = MAX_SCANLINE * MAX_SCANPOS |
paul@31 | 55 | while i < limit: |
paul@95 | 56 | ula.posedge() |
paul@31 | 57 | video.update() |
paul@95 | 58 | ula.negedge() |
paul@31 | 59 | i += 1 |
paul@40 | 60 | |
paul@31 | 61 | return video.screen |
paul@31 | 62 | |
paul@31 | 63 | class Video: |
paul@31 | 64 | |
paul@31 | 65 | """ |
paul@31 | 66 | A class representing the video circuitry. |
paul@1 | 67 | """ |
paul@1 | 68 | |
paul@31 | 69 | def __init__(self): |
paul@31 | 70 | self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT)) |
paul@31 | 71 | self.colour = BLANK |
paul@31 | 72 | self.csync = 1 |
paul@31 | 73 | self.hs = 1 |
paul@40 | 74 | self.x = 0 |
paul@40 | 75 | self.y = 0 |
paul@1 | 76 | |
paul@40 | 77 | def set_csync(self, value): |
paul@40 | 78 | if self.csync and not value: |
paul@40 | 79 | self.y = 0 |
paul@40 | 80 | self.pos = 0 |
paul@40 | 81 | self.csync = value |
paul@40 | 82 | |
paul@40 | 83 | def set_hs(self, value): |
paul@40 | 84 | if self.hs and not value: |
paul@40 | 85 | self.x = 0 |
paul@40 | 86 | self.y += 1 |
paul@40 | 87 | self.hs = value |
paul@31 | 88 | |
paul@31 | 89 | def update(self): |
paul@40 | 90 | if MIN_PIXELLINE <= self.y < MAX_PIXELLINE: |
paul@42 | 91 | if MIN_PIXELPOS + 8 <= self.x < MAX_PIXELPOS + 8: |
paul@31 | 92 | self.screen[self.pos] = self.colour[0]; self.pos += 1 |
paul@31 | 93 | self.screen[self.pos] = self.colour[1]; self.pos += 1 |
paul@31 | 94 | self.screen[self.pos] = self.colour[2]; self.pos += 1 |
paul@40 | 95 | self.x += 1 |
paul@40 | 96 | |
paul@40 | 97 | class RAM: |
paul@40 | 98 | |
paul@40 | 99 | """ |
paul@40 | 100 | A class representing the RAM circuits (IC4 to IC7). Each circuit |
paul@48 | 101 | traditionally holds 64 kilobits, with each access obtaining 1 bit from each |
paul@48 | 102 | IC, and thus two accesses being required to obtain a whole byte. Here, we |
paul@48 | 103 | model the circuits with a list of 65536 half-bytes with each bit in a |
paul@48 | 104 | half-byte representing a bit stored on a separate IC. |
paul@40 | 105 | """ |
paul@40 | 106 | |
paul@40 | 107 | def __init__(self): |
paul@40 | 108 | |
paul@40 | 109 | "Initialise the RAM circuits." |
paul@40 | 110 | |
paul@40 | 111 | self.memory = [0] * MAX_RAM |
paul@40 | 112 | self.row_address = 0 |
paul@40 | 113 | self.column_address = 0 |
paul@40 | 114 | self.data = 0 |
paul@97 | 115 | self.read_not_write = 1 |
paul@40 | 116 | |
paul@40 | 117 | def row_select(self, address): |
paul@59 | 118 | |
paul@59 | 119 | "The operation of asserting a row 'address' via RA0...RA7." |
paul@59 | 120 | |
paul@40 | 121 | self.row_address = address |
paul@40 | 122 | |
paul@40 | 123 | def row_deselect(self): |
paul@40 | 124 | pass |
paul@40 | 125 | |
paul@40 | 126 | def column_select(self, address): |
paul@59 | 127 | |
paul@59 | 128 | "The operation of asserting a column 'address' via RA0...RA7." |
paul@59 | 129 | |
paul@40 | 130 | self.column_address = address |
paul@40 | 131 | |
paul@40 | 132 | # Read the data. |
paul@40 | 133 | |
paul@97 | 134 | if self.read_not_write: |
paul@97 | 135 | self.data = self.memory[self.row_address << 8 | self.column_address] |
paul@97 | 136 | else: |
paul@97 | 137 | self.memory[self.row_address << 8 | self.column_address] = self.data |
paul@40 | 138 | |
paul@40 | 139 | def column_deselect(self): |
paul@40 | 140 | pass |
paul@40 | 141 | |
paul@97 | 142 | def read_select(self): |
paul@97 | 143 | self.read_not_write = 1 |
paul@97 | 144 | |
paul@97 | 145 | def write_select(self): |
paul@97 | 146 | self.read_not_write = 0 |
paul@97 | 147 | |
paul@40 | 148 | # Convenience methods. |
paul@40 | 149 | |
paul@40 | 150 | def fill(self, start, end, value): |
paul@40 | 151 | for i in xrange(start, end): |
paul@40 | 152 | self.memory[i << 1] = value >> 4 |
paul@40 | 153 | self.memory[i << 1 | 0x1] = value & 0xf |
paul@29 | 154 | |
paul@97 | 155 | class CPU: |
paul@97 | 156 | |
paul@97 | 157 | "A CPU abstraction." |
paul@97 | 158 | |
paul@97 | 159 | def __init__(self): |
paul@97 | 160 | self.address = 0x1000 |
paul@100 | 161 | self.data = 0 |
paul@97 | 162 | self.read_not_write = 1 |
paul@97 | 163 | |
paul@2 | 164 | class ULA: |
paul@2 | 165 | |
paul@31 | 166 | """ |
paul@31 | 167 | A class providing the ULA functionality. Instances of this class refer to |
paul@31 | 168 | the system memory, maintain internal state (such as information about the |
paul@31 | 169 | current screen mode), and provide outputs (such as the current pixel |
paul@31 | 170 | colour). |
paul@31 | 171 | """ |
paul@1 | 172 | |
paul@2 | 173 | modes = [ |
paul@2 | 174 | (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) |
paul@3 | 175 | (640, 1, 25), (320, 1, 32), (160, 2, 32), |
paul@3 | 176 | (320, 1, 25) |
paul@2 | 177 | ] |
paul@2 | 178 | |
paul@97 | 179 | def __init__(self, cpu, ram, video): |
paul@1 | 180 | |
paul@97 | 181 | "Initialise the ULA with the given 'cpu', 'ram' and 'video' instances." |
paul@2 | 182 | |
paul@97 | 183 | self.cpu = cpu |
paul@40 | 184 | self.ram = ram |
paul@31 | 185 | self.video = video |
paul@2 | 186 | self.set_mode(6) |
paul@91 | 187 | self.palette = map(get_physical_colour, range(0, 8) * 2) |
paul@1 | 188 | |
paul@31 | 189 | self.reset() |
paul@31 | 190 | |
paul@31 | 191 | def reset(self): |
paul@31 | 192 | |
paul@31 | 193 | "Reset the ULA." |
paul@31 | 194 | |
paul@43 | 195 | # General state. |
paul@43 | 196 | |
paul@43 | 197 | self.nmi = 0 # no NMI asserted initially |
paul@43 | 198 | self.irq_vsync = 0 # no IRQ asserted initially |
paul@97 | 199 | self.cpu_clock = 0 # drive the CPU clock low |
paul@43 | 200 | |
paul@59 | 201 | # Communication. |
paul@59 | 202 | |
paul@59 | 203 | self.ram_address = 0 # address given to the RAM via RA0...RA7 |
paul@59 | 204 | self.data = 0 # data read from the RAM via RAM0...RAM3 |
paul@59 | 205 | self.cpu_address = 0 # address selected by the CPU via A0...A15 |
paul@59 | 206 | self.cpu_read = 0 # data read/write by the CPU selected using R/W |
paul@59 | 207 | |
paul@40 | 208 | # Internal state. |
paul@40 | 209 | |
paul@42 | 210 | self.have_pixels = 0 # whether pixel data has been read |
paul@93 | 211 | self.pdata = 0 # decoded RAM data for pixel output |
paul@93 | 212 | self.cycle = 1 # 8-state counter within each 2MHz period |
paul@93 | 213 | self.pcycle = 0 # 8/4/2-state pixel output counter |
paul@50 | 214 | |
paul@87 | 215 | self.next_frame() |
paul@31 | 216 | |
paul@2 | 217 | def set_mode(self, mode): |
paul@1 | 218 | |
paul@2 | 219 | """ |
paul@2 | 220 | For the given 'mode', initialise the... |
paul@1 | 221 | |
paul@2 | 222 | * width in pixels |
paul@2 | 223 | * colour depth in bits per pixel |
paul@2 | 224 | * number of character rows |
paul@2 | 225 | * character row size in bytes |
paul@2 | 226 | * screen size in bytes |
paul@2 | 227 | * default screen start address |
paul@2 | 228 | * horizontal pixel scaling factor |
paul@88 | 229 | * row height in pixels |
paul@88 | 230 | * display height in pixels |
paul@31 | 231 | |
paul@31 | 232 | The ULA should be reset after a mode switch in order to cleanly display |
paul@31 | 233 | a full screen. |
paul@2 | 234 | """ |
paul@1 | 235 | |
paul@3 | 236 | self.width, self.depth, rows = self.modes[mode] |
paul@3 | 237 | |
paul@31 | 238 | columns = (self.width * self.depth) / 8 # bits read -> bytes read |
paul@40 | 239 | self.access_frequency = 80 / columns # cycle frequency for reading bytes |
paul@31 | 240 | row_size = columns * LINES_PER_ROW |
paul@2 | 241 | |
paul@3 | 242 | # Memory access configuration. |
paul@4 | 243 | # Note the limitation on positioning the screen start. |
paul@3 | 244 | |
paul@4 | 245 | screen_size = row_size * rows |
paul@4 | 246 | self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 |
paul@4 | 247 | self.screen_size = SCREEN_LIMIT - self.screen_start |
paul@3 | 248 | |
paul@3 | 249 | # Scanline configuration. |
paul@1 | 250 | |
paul@22 | 251 | self.xscale = MAX_WIDTH / self.width # pixel width in display pixels |
paul@88 | 252 | self.row_height = MAX_HEIGHT / rows # row height in display pixels |
paul@88 | 253 | self.display_height = rows * self.row_height # display height in pixels |
paul@3 | 254 | |
paul@40 | 255 | def vsync(self, value=0): |
paul@40 | 256 | |
paul@40 | 257 | "Signal the start of a frame." |
paul@40 | 258 | |
paul@40 | 259 | self.csync = value |
paul@40 | 260 | self.video.set_csync(value) |
paul@40 | 261 | |
paul@40 | 262 | def hsync(self, value=0): |
paul@40 | 263 | |
paul@40 | 264 | "Signal the end of a scanline." |
paul@40 | 265 | |
paul@40 | 266 | self.hs = value |
paul@40 | 267 | self.video.set_hs(value) |
paul@40 | 268 | |
paul@87 | 269 | def next_frame(self): |
paul@2 | 270 | |
paul@2 | 271 | "Signal the start of a frame." |
paul@1 | 272 | |
paul@98 | 273 | self.line_start = self.pixel_address = self.screen_start |
paul@5 | 274 | self.line = self.line_start % LINES_PER_ROW |
paul@31 | 275 | self.y = 0 |
paul@40 | 276 | self.x = 0 |
paul@2 | 277 | |
paul@87 | 278 | def next_horizontal(self): |
paul@87 | 279 | |
paul@87 | 280 | "Visit the next horizontal position." |
paul@87 | 281 | |
paul@98 | 282 | self.pixel_address += LINES_PER_ROW |
paul@87 | 283 | self.wrap_address() |
paul@87 | 284 | |
paul@87 | 285 | def next_vertical(self): |
paul@1 | 286 | |
paul@40 | 287 | "Reset horizontal state within the active region of the frame." |
paul@31 | 288 | |
paul@31 | 289 | self.y += 1 |
paul@40 | 290 | self.x = 0 |
paul@40 | 291 | |
paul@96 | 292 | if self.inside_frame(): |
paul@96 | 293 | self.line += 1 |
paul@2 | 294 | |
paul@96 | 295 | # At the end of a row... |
paul@96 | 296 | |
paul@96 | 297 | if self.line == self.row_height: |
paul@2 | 298 | |
paul@96 | 299 | # After the end of the last line in a row, the address should already |
paul@96 | 300 | # have been positioned on the last line of the next column. |
paul@1 | 301 | |
paul@98 | 302 | self.pixel_address -= LINES_PER_ROW - 1 |
paul@96 | 303 | self.wrap_address() |
paul@96 | 304 | self.line = 0 |
paul@96 | 305 | |
paul@96 | 306 | # Record the position of the start of the pixel row. |
paul@88 | 307 | |
paul@98 | 308 | self.line_start = self.pixel_address |
paul@88 | 309 | |
paul@96 | 310 | # Before any spacing between character rows... |
paul@88 | 311 | |
paul@96 | 312 | elif self.in_line(): |
paul@88 | 313 | |
paul@96 | 314 | # If not on a row boundary, move to the next line. Here, the address |
paul@96 | 315 | # needs bringing back to the previous character row. |
paul@1 | 316 | |
paul@98 | 317 | self.pixel_address = self.line_start + 1 |
paul@96 | 318 | self.wrap_address() |
paul@3 | 319 | |
paul@96 | 320 | # Record the position of the start of the pixel row. |
paul@96 | 321 | |
paul@98 | 322 | self.line_start = self.pixel_address |
paul@1 | 323 | |
paul@100 | 324 | def access_cycle(self): return (self.x / 8) % self.access_frequency == 0 |
paul@100 | 325 | def would_access_ram(self): return self.access_cycle() and self.read_pixels() and self.in_line() |
paul@100 | 326 | def access_ram(self): return not self.nmi and self.would_access_ram() |
paul@88 | 327 | def in_line(self): return self.line < LINES_PER_ROW |
paul@88 | 328 | def in_frame(self): return MIN_PIXELLINE <= self.y < (MIN_PIXELLINE + self.display_height) |
paul@88 | 329 | def inside_frame(self): return MIN_PIXELLINE < self.y < (MIN_PIXELLINE + self.display_height) |
paul@42 | 330 | def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() |
paul@94 | 331 | def write_pixels(self): return self.pcycle != 0 |
paul@100 | 332 | def next_pixel(self): return self.xscale == 1 or (self.xscale == 2 and self.cycle & 0b01010101) or (self.xscale == 4 and self.cycle & 0b00010001) |
paul@31 | 333 | |
paul@95 | 334 | def posedge(self): |
paul@1 | 335 | |
paul@100 | 336 | "Update the state of the ULA for each clock cycle." |
paul@2 | 337 | |
paul@100 | 338 | self.posedge_video() |
paul@100 | 339 | self.posedge_ram() |
paul@100 | 340 | self.posedge_pixel() |
paul@100 | 341 | |
paul@100 | 342 | def posedge_video(self): |
paul@100 | 343 | |
paul@100 | 344 | "Video signalling." |
paul@95 | 345 | |
paul@40 | 346 | # Detect the end of the scanline. |
paul@40 | 347 | |
paul@40 | 348 | if self.x == MAX_SCANPOS: |
paul@87 | 349 | self.next_vertical() |
paul@40 | 350 | |
paul@40 | 351 | # Detect the end of the frame. |
paul@40 | 352 | |
paul@40 | 353 | if self.y == MAX_SCANLINE: |
paul@87 | 354 | self.next_frame() |
paul@40 | 355 | |
paul@95 | 356 | # Detect any sync conditions. |
paul@95 | 357 | |
paul@95 | 358 | if self.x == 0: |
paul@95 | 359 | self.hsync() |
paul@95 | 360 | if self.y == 0: |
paul@95 | 361 | self.vsync() |
paul@95 | 362 | self.irq_vsync = 0 |
paul@95 | 363 | elif self.y == MAX_PIXELLINE: |
paul@95 | 364 | self.irq_vsync = 1 |
paul@95 | 365 | |
paul@95 | 366 | # Detect the end of hsync. |
paul@95 | 367 | |
paul@95 | 368 | elif self.x == MAX_HSYNC: |
paul@95 | 369 | self.hsync(1) |
paul@95 | 370 | |
paul@95 | 371 | # Detect the end of vsync. |
paul@95 | 372 | |
paul@95 | 373 | elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: |
paul@95 | 374 | self.vsync(1) |
paul@95 | 375 | |
paul@100 | 376 | def posedge_ram(self): |
paul@95 | 377 | |
paul@101 | 378 | """ |
paul@101 | 379 | RAM signalling. |
paul@101 | 380 | |
paul@101 | 381 | States handled: * _ * * _ * * _ |
paul@101 | 382 | """ |
paul@95 | 383 | |
paul@40 | 384 | # Clock management. |
paul@40 | 385 | |
paul@101 | 386 | # Read 4 bits (for RAM access only). |
paul@100 | 387 | |
paul@100 | 388 | if self.cycle == 1: |
paul@101 | 389 | |
paul@101 | 390 | # Reset addresses. |
paul@101 | 391 | |
paul@100 | 392 | self.ram.column_deselect() |
paul@100 | 393 | self.ram.row_deselect() |
paul@100 | 394 | |
paul@101 | 395 | # Either read from a required address or transfer CPU data. |
paul@40 | 396 | |
paul@101 | 397 | if self.have_pixels: |
paul@101 | 398 | self.data = (self.data & 0xf0) | self.ram.data |
paul@101 | 399 | else: |
paul@101 | 400 | self.cpu_update_clock() |
paul@101 | 401 | self.cpu_transfer_low() |
paul@40 | 402 | |
paul@101 | 403 | # Latch row address. |
paul@40 | 404 | |
paul@100 | 405 | elif self.cycle == 4: |
paul@40 | 406 | |
paul@59 | 407 | # Select an address needed by the ULA or CPU. |
paul@59 | 408 | |
paul@59 | 409 | self.ram.row_select(self.ram_address) |
paul@59 | 410 | |
paul@40 | 411 | # Latch column address. |
paul@40 | 412 | |
paul@100 | 413 | elif self.cycle == 8: |
paul@40 | 414 | |
paul@59 | 415 | # Select an address needed by the ULA or CPU. |
paul@31 | 416 | |
paul@59 | 417 | self.ram.column_select(self.ram_address) |
paul@40 | 418 | |
paul@97 | 419 | # Assert the RAM write enable if appropriate. |
paul@97 | 420 | |
paul@100 | 421 | if self.access_ram(): |
paul@97 | 422 | self.ram.read_select() |
paul@97 | 423 | else: |
paul@97 | 424 | self.cpu_transfer_select() |
paul@97 | 425 | |
paul@101 | 426 | # Read 4 bits (for RAM access only). |
paul@40 | 427 | |
paul@100 | 428 | elif self.cycle == 32: |
paul@101 | 429 | |
paul@101 | 430 | # Prepare to latch column address. |
paul@101 | 431 | |
paul@40 | 432 | self.ram.column_deselect() |
paul@31 | 433 | |
paul@101 | 434 | # Either read from a required address or transfer CPU data. |
paul@40 | 435 | |
paul@100 | 436 | if self.access_ram(): |
paul@101 | 437 | self.data = self.ram.data << 4 |
paul@59 | 438 | else: |
paul@101 | 439 | self.cpu_transfer_high() |
paul@40 | 440 | |
paul@40 | 441 | # Latch column address. |
paul@40 | 442 | |
paul@100 | 443 | elif self.cycle == 64: |
paul@40 | 444 | |
paul@59 | 445 | # Select an address needed by the ULA or CPU. |
paul@40 | 446 | |
paul@59 | 447 | self.ram.column_select(self.ram_address) |
paul@31 | 448 | |
paul@100 | 449 | def posedge_pixel(self): |
paul@40 | 450 | |
paul@100 | 451 | "Pixel production." |
paul@31 | 452 | |
paul@31 | 453 | # For pixels within the frame, obtain and output the value. |
paul@31 | 454 | |
paul@95 | 455 | if self.write_pixels(): |
paul@92 | 456 | self.output_colour_value() |
paul@1 | 457 | |
paul@31 | 458 | # Scale pixels horizontally, only accessing the next pixel value |
paul@31 | 459 | # after the required number of scan positions. |
paul@22 | 460 | |
paul@93 | 461 | if self.next_pixel(): |
paul@92 | 462 | self.next_pixel_value() |
paul@31 | 463 | |
paul@95 | 464 | # Detect spacing between character rows. |
paul@95 | 465 | |
paul@95 | 466 | else: |
paul@95 | 467 | self.video.colour = BLANK |
paul@95 | 468 | |
paul@95 | 469 | def negedge(self): |
paul@95 | 470 | |
paul@99 | 471 | """ |
paul@99 | 472 | Update the state of the device. |
paul@99 | 473 | |
paul@101 | 474 | States handled: * * * _ _ * _ * |
paul@99 | 475 | """ |
paul@99 | 476 | |
paul@99 | 477 | # Clock management. |
paul@99 | 478 | |
paul@100 | 479 | # Initialise the pixel buffer if appropriate. Output starts after |
paul@100 | 480 | # this cycle. |
paul@100 | 481 | |
paul@100 | 482 | if self.cycle == 1 and self.have_pixels: |
paul@100 | 483 | self.pdata = decode(self.data, self.depth) |
paul@100 | 484 | self.pcycle = 1 |
paul@100 | 485 | self.have_pixels = 0 |
paul@99 | 486 | |
paul@101 | 487 | # Set row address (for RAM access only). |
paul@99 | 488 | |
paul@101 | 489 | elif self.cycle == 2: |
paul@99 | 490 | |
paul@101 | 491 | # Either assert a required address or propagate the CPU address. |
paul@99 | 492 | |
paul@100 | 493 | if self.access_ram(): |
paul@101 | 494 | self.init_row_address(self.pixel_address) |
paul@101 | 495 | else: |
paul@101 | 496 | self.init_row_address(self.cpu_address) |
paul@101 | 497 | |
paul@101 | 498 | # Latch row address, set column address (for RAM access only). |
paul@101 | 499 | |
paul@101 | 500 | elif self.cycle == 4: |
paul@101 | 501 | |
paul@101 | 502 | # Either assert a required address or propagate the CPU address. |
paul@101 | 503 | |
paul@101 | 504 | if self.access_ram(): |
paul@101 | 505 | self.init_column_address(self.pixel_address, 0) |
paul@99 | 506 | else: |
paul@101 | 507 | self.init_column_address(self.cpu_address, 0) |
paul@101 | 508 | |
paul@101 | 509 | # Set column address (for RAM access only). |
paul@101 | 510 | |
paul@101 | 511 | elif self.cycle == 32: |
paul@99 | 512 | |
paul@101 | 513 | # Either assert a required address or propagate the CPU address. |
paul@101 | 514 | |
paul@101 | 515 | if self.access_ram(): |
paul@101 | 516 | self.init_column_address(self.pixel_address, 1) |
paul@101 | 517 | else: |
paul@101 | 518 | self.init_column_address(self.cpu_address, 1) |
paul@101 | 519 | |
paul@101 | 520 | # Update addresses. |
paul@99 | 521 | |
paul@100 | 522 | elif self.cycle == 128: |
paul@99 | 523 | |
paul@101 | 524 | # Advance to the next column even if an NMI is asserted. |
paul@101 | 525 | |
paul@101 | 526 | if self.would_access_ram(): |
paul@101 | 527 | self.next_horizontal() |
paul@101 | 528 | |
paul@101 | 529 | # If the ULA accessed RAM, indicate that a read needs completing. |
paul@99 | 530 | |
paul@100 | 531 | if self.access_ram(): |
paul@99 | 532 | self.have_pixels = 1 |
paul@22 | 533 | |
paul@95 | 534 | # Start a new cycle. |
paul@95 | 535 | |
paul@95 | 536 | self.cycle = rotate(self.cycle, 1) |
paul@95 | 537 | self.x += 1 |
paul@2 | 538 | |
paul@92 | 539 | def output_colour_value(self): |
paul@1 | 540 | |
paul@2 | 541 | """ |
paul@92 | 542 | Output the colour value for the current pixel by translating memory |
paul@92 | 543 | content for the current mode. |
paul@2 | 544 | """ |
paul@1 | 545 | |
paul@92 | 546 | value = value_of_bits(self.pdata, self.depth) |
paul@92 | 547 | self.video.colour = self.palette[value] |
paul@92 | 548 | |
paul@92 | 549 | def next_pixel_value(self): |
paul@92 | 550 | self.pdata = rotate(self.pdata, self.depth) |
paul@95 | 551 | self.pcycle = rotate(self.pcycle, self.depth, zero=True) |
paul@2 | 552 | |
paul@2 | 553 | def wrap_address(self): |
paul@98 | 554 | if self.pixel_address >= SCREEN_LIMIT: |
paul@98 | 555 | self.pixel_address -= self.screen_size |
paul@1 | 556 | |
paul@59 | 557 | def init_row_address(self, address): |
paul@59 | 558 | self.ram_address = (address & 0xff80) >> 7 |
paul@59 | 559 | |
paul@59 | 560 | def init_column_address(self, address, offset): |
paul@59 | 561 | self.ram_address = (address & 0x7f) << 1 | offset |
paul@59 | 562 | |
paul@59 | 563 | def cpu_transfer_high(self): |
paul@59 | 564 | if self.cpu_read: |
paul@59 | 565 | self.cpu_data = self.ram.data << 4 |
paul@97 | 566 | else: |
paul@97 | 567 | self.ram.data = self.cpu_data >> 4 |
paul@59 | 568 | |
paul@59 | 569 | def cpu_transfer_low(self): |
paul@59 | 570 | if self.cpu_read: |
paul@59 | 571 | self.cpu_data = self.data | self.ram.data |
paul@97 | 572 | else: |
paul@97 | 573 | self.ram.data = self.cpu_data & 0b00001111 |
paul@97 | 574 | |
paul@97 | 575 | def cpu_read_address(self): |
paul@97 | 576 | self.cpu_address = self.cpu.address |
paul@97 | 577 | |
paul@97 | 578 | def cpu_transfer_data(self): |
paul@97 | 579 | if self.cpu_read: |
paul@97 | 580 | self.cpu.data = self.cpu_data |
paul@97 | 581 | else: |
paul@97 | 582 | self.cpu_data = self.cpu.data |
paul@97 | 583 | |
paul@97 | 584 | def cpu_update_clock(self): |
paul@97 | 585 | self.cpu_clock = not self.cpu_clock |
paul@97 | 586 | if self.cpu_clock: |
paul@97 | 587 | self.cpu_transfer_data() |
paul@97 | 588 | else: |
paul@97 | 589 | self.cpu_read_address() |
paul@97 | 590 | |
paul@97 | 591 | def cpu_transfer_select(self): |
paul@97 | 592 | self.cpu_read = self.cpu.read_not_write |
paul@97 | 593 | if self.cpu_read: |
paul@97 | 594 | self.ram.read_select() |
paul@97 | 595 | else: |
paul@97 | 596 | self.ram.write_select() |
paul@59 | 597 | |
paul@95 | 598 | def rotate(value, depth, width=8, zero=False): |
paul@89 | 599 | |
paul@93 | 600 | """ |
paul@95 | 601 | Return 'value' rotated left by the number of bits given by 'depth', doing so |
paul@95 | 602 | within a value 'width' given in bits. If 'zero' is true, rotate zero bits |
paul@95 | 603 | into the lower bits when rotating. |
paul@93 | 604 | """ |
paul@93 | 605 | |
paul@93 | 606 | field = width - depth |
paul@92 | 607 | top = value >> field |
paul@93 | 608 | mask = 2 ** (width - depth) - 1 |
paul@92 | 609 | rest = value & mask |
paul@95 | 610 | return (rest << depth) | (not zero and top or 0) |
paul@92 | 611 | |
paul@92 | 612 | def value_of_bits(value, depth): |
paul@92 | 613 | |
paul@92 | 614 | """ |
paul@92 | 615 | Convert the upper bits of 'value' to a result, using 'depth' to indicate the |
paul@92 | 616 | number of bits involved. |
paul@92 | 617 | """ |
paul@92 | 618 | |
paul@92 | 619 | return value >> (8 - depth) |
paul@89 | 620 | |
paul@1 | 621 | def get_physical_colour(value): |
paul@1 | 622 | |
paul@1 | 623 | """ |
paul@1 | 624 | Return the physical colour as an RGB triple for the given 'value'. |
paul@1 | 625 | """ |
paul@1 | 626 | |
paul@1 | 627 | return value & 1, value >> 1 & 1, value >> 2 & 1 |
paul@1 | 628 | |
paul@1 | 629 | def decode(value, depth): |
paul@1 | 630 | |
paul@1 | 631 | """ |
paul@1 | 632 | Decode the given byte 'value' according to the 'depth' in bits per pixel, |
paul@1 | 633 | returning a sequence of pixel values. |
paul@1 | 634 | """ |
paul@1 | 635 | |
paul@1 | 636 | if depth == 1: |
paul@92 | 637 | return value |
paul@1 | 638 | elif depth == 2: |
paul@92 | 639 | return ((value & 128) | ((value & 8) << 3) | ((value & 64) >> 1) | ((value & 4) << 2) | |
paul@92 | 640 | ((value & 32) >> 2) | ((value & 2) << 1) | ((value & 16) >> 3) | (value & 1)) |
paul@1 | 641 | elif depth == 4: |
paul@92 | 642 | return ((value & 128) | ((value & 32) << 1) | ((value & 8) << 2) | ((value & 2) << 3) | |
paul@92 | 643 | ((value & 64) >> 3) | ((value & 16) >> 2) | ((value & 4) >> 1) | (value & 1)) |
paul@1 | 644 | else: |
paul@1 | 645 | raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth |
paul@1 | 646 | |
paul@1 | 647 | # Convenience functions. |
paul@1 | 648 | |
paul@1 | 649 | def encode(values, depth): |
paul@1 | 650 | |
paul@1 | 651 | """ |
paul@1 | 652 | Encode the given 'values' according to the 'depth' in bits per pixel, |
paul@1 | 653 | returning a byte value for the pixels. |
paul@1 | 654 | """ |
paul@1 | 655 | |
paul@1 | 656 | result = 0 |
paul@1 | 657 | |
paul@1 | 658 | if depth == 1: |
paul@1 | 659 | for value in values: |
paul@1 | 660 | result = result << 1 | (value & 1) |
paul@1 | 661 | elif depth == 2: |
paul@1 | 662 | for value in values: |
paul@1 | 663 | result = result << 1 | (value & 2) << 3 | (value & 1) |
paul@1 | 664 | elif depth == 4: |
paul@1 | 665 | for value in values: |
paul@1 | 666 | result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) |
paul@1 | 667 | else: |
paul@1 | 668 | raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth |
paul@1 | 669 | |
paul@1 | 670 | return result |
paul@1 | 671 | |
paul@11 | 672 | def get_ula(): |
paul@11 | 673 | |
paul@31 | 674 | "Return a ULA initialised with a memory array and video." |
paul@31 | 675 | |
paul@97 | 676 | return ULA(get_cpu(), get_ram(), get_video()) |
paul@11 | 677 | |
paul@31 | 678 | def get_video(): |
paul@31 | 679 | |
paul@31 | 680 | "Return a video circuit." |
paul@31 | 681 | |
paul@31 | 682 | return Video() |
paul@11 | 683 | |
paul@40 | 684 | def get_ram(): |
paul@10 | 685 | |
paul@40 | 686 | "Return an instance representing the computer's RAM hardware." |
paul@7 | 687 | |
paul@40 | 688 | return RAM() |
paul@1 | 689 | |
paul@97 | 690 | def get_cpu(): |
paul@97 | 691 | |
paul@97 | 692 | "Return an instance representing the CPU." |
paul@97 | 693 | |
paul@97 | 694 | return CPU() |
paul@97 | 695 | |
paul@7 | 696 | # Test program providing coverage (necessary for compilers like Shedskin). |
paul@7 | 697 | |
paul@7 | 698 | if __name__ == "__main__": |
paul@11 | 699 | ula = get_ula() |
paul@7 | 700 | ula.set_mode(2) |
paul@40 | 701 | ula.reset() |
paul@40 | 702 | ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) |
paul@7 | 703 | |
paul@7 | 704 | # Make a simple two-dimensional array of tuples (three-dimensional in pygame |
paul@7 | 705 | # terminology). |
paul@7 | 706 | |
paul@29 | 707 | a = update(ula) |
paul@7 | 708 | |
paul@1 | 709 | # vim: tabstop=4 expandtab shiftwidth=4 |