ULA

Annotated ula.py

66:6363b9a42865
2014-02-01 Paul Boddie Introduced a shift register abstraction for the ULA's internal state.
paul@1 1
#!/usr/bin/env python
paul@1 2
paul@1 3
"""
paul@1 4
Acorn Electron ULA simulation.
paul@1 5
"""
paul@1 6
paul@29 7
from array import array
paul@29 8
from itertools import repeat
paul@29 9
paul@22 10
LINES_PER_ROW = 8       # the number of pixel lines per character row
paul@22 11
MAX_HEIGHT = 256        # the height of the screen in pixels
paul@40 12
MAX_WIDTH = 640         # the width of the screen in pixels
paul@40 13
paul@40 14
MAX_CSYNC = 2           # the scanline during which vsync ends
paul@40 15
MIN_PIXELLINE = 38      # the first scanline involving pixel generation
paul@22 16
MAX_SCANLINE = 312      # the number of scanlines in each frame
paul@40 17
paul@40 18
MAX_PIXELLINE = MIN_PIXELLINE + MAX_HEIGHT
paul@40 19
paul@40 20
MAX_HSYNC = 75          # the number of cycles in each hsync period
paul@42 21
MIN_PIXELPOS = 256      # the first cycle involving pixel generation
paul@40 22
MAX_SCANPOS = 1024      # the number of cycles in each scanline
paul@40 23
paul@40 24
MAX_PIXELPOS = MIN_PIXELPOS + MAX_WIDTH
paul@40 25
paul@22 26
SCREEN_LIMIT = 0x8000   # the first address after the screen memory
paul@22 27
MAX_MEMORY = 0x10000    # the number of addressable memory locations
paul@40 28
MAX_RAM = 0x10000       # the number of addressable RAM locations (64Kb in each IC)
paul@3 29
BLANK = (0, 0, 0)
paul@1 30
paul@29 31
def update(ula):
paul@1 32
paul@1 33
    """
paul@31 34
    Update the 'ula' for one frame. Return the resulting screen.
paul@31 35
    """
paul@31 36
paul@31 37
    video = ula.video
paul@31 38
paul@31 39
    i = 0
paul@31 40
    limit = MAX_SCANLINE * MAX_SCANPOS
paul@31 41
    while i < limit:
paul@31 42
        ula.update()
paul@31 43
        video.update()
paul@31 44
        i += 1
paul@40 45
paul@31 46
    return video.screen
paul@31 47
paul@31 48
class Video:
paul@31 49
paul@31 50
    """
paul@31 51
    A class representing the video circuitry.
paul@1 52
    """
paul@1 53
paul@31 54
    def __init__(self):
paul@31 55
        self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT))
paul@31 56
        self.colour = BLANK
paul@31 57
        self.csync = 1
paul@31 58
        self.hs = 1
paul@40 59
        self.x = 0
paul@40 60
        self.y = 0
paul@1 61
paul@40 62
    def set_csync(self, value):
paul@40 63
        if self.csync and not value:
paul@40 64
            self.y = 0
paul@40 65
            self.pos = 0
paul@40 66
        self.csync = value
paul@40 67
paul@40 68
    def set_hs(self, value):
paul@40 69
        if self.hs and not value:
paul@40 70
            self.x = 0
paul@40 71
            self.y += 1
paul@40 72
        self.hs = value
paul@31 73
paul@31 74
    def update(self):
paul@40 75
        if MIN_PIXELLINE <= self.y < MAX_PIXELLINE:
paul@42 76
            if MIN_PIXELPOS + 8 <= self.x < MAX_PIXELPOS + 8:
paul@31 77
                self.screen[self.pos] = self.colour[0]; self.pos += 1
paul@31 78
                self.screen[self.pos] = self.colour[1]; self.pos += 1
paul@31 79
                self.screen[self.pos] = self.colour[2]; self.pos += 1
paul@40 80
            self.x += 1
paul@40 81
paul@40 82
class RAM:
paul@40 83
paul@40 84
    """
paul@40 85
    A class representing the RAM circuits (IC4 to IC7). Each circuit
paul@48 86
    traditionally holds 64 kilobits, with each access obtaining 1 bit from each
paul@48 87
    IC, and thus two accesses being required to obtain a whole byte. Here, we
paul@48 88
    model the circuits with a list of 65536 half-bytes with each bit in a
paul@48 89
    half-byte representing a bit stored on a separate IC.
paul@40 90
    """
paul@40 91
paul@40 92
    def __init__(self):
paul@40 93
paul@40 94
        "Initialise the RAM circuits."
paul@40 95
paul@40 96
        self.memory = [0] * MAX_RAM
paul@40 97
        self.row_address = 0
paul@40 98
        self.column_address = 0
paul@40 99
        self.data = 0
paul@40 100
paul@40 101
    def row_select(self, address):
paul@59 102
paul@59 103
        "The operation of asserting a row 'address' via RA0...RA7."
paul@59 104
paul@40 105
        self.row_address = address
paul@40 106
paul@40 107
    def row_deselect(self):
paul@40 108
        pass
paul@40 109
paul@40 110
    def column_select(self, address):
paul@59 111
paul@59 112
        "The operation of asserting a column 'address' via RA0...RA7."
paul@59 113
paul@40 114
        self.column_address = address
paul@40 115
paul@40 116
        # Read the data.
paul@40 117
paul@40 118
        self.data = self.memory[self.row_address << 8 | self.column_address]
paul@40 119
paul@40 120
    def column_deselect(self):
paul@40 121
        pass
paul@40 122
paul@40 123
    # Convenience methods.
paul@40 124
paul@40 125
    def fill(self, start, end, value):
paul@40 126
        for i in xrange(start, end):
paul@40 127
            self.memory[i << 1] = value >> 4
paul@40 128
            self.memory[i << 1 | 0x1] = value & 0xf
paul@29 129
paul@66 130
class ShiftRegister:
paul@66 131
paul@66 132
    """
paul@66 133
    A class representing a shift register, used for the internal state of the
paul@66 134
    ULA within each 2MHz period.
paul@66 135
    """
paul@66 136
paul@66 137
    def __init__(self):
paul@66 138
        self.state = [0] * 8
paul@66 139
        self.input = 0
paul@66 140
paul@66 141
    def set_input(self, input):
paul@66 142
        self.input = input
paul@66 143
paul@66 144
    def shift(self):
paul@66 145
paul@66 146
        # NOTE: This is not meant to be "nice" Python, but instead models the
paul@66 147
        # NOTE: propagation of state through the latches.
paul@66 148
paul@66 149
        self.state[0], self.state[1], self.state[2], self.state[3], \
paul@66 150
        self.state[4], self.state[5], self.state[6], self.state[7] = \
paul@66 151
        self.input,    self.state[0], self.state[1], self.state[2], \
paul@66 152
        self.state[3], self.state[4], self.state[5], self.state[6]
paul@66 153
paul@66 154
    def __getitem__(self, i):
paul@66 155
        return self.state[i]
paul@66 156
paul@2 157
class ULA:
paul@2 158
paul@31 159
    """
paul@31 160
    A class providing the ULA functionality. Instances of this class refer to
paul@31 161
    the system memory, maintain internal state (such as information about the
paul@31 162
    current screen mode), and provide outputs (such as the current pixel
paul@31 163
    colour).
paul@31 164
    """
paul@1 165
paul@2 166
    modes = [
paul@2 167
        (640, 1, 32), (320, 2, 32), (160, 4, 32),   # (width, depth, rows)
paul@3 168
        (640, 1, 25), (320, 1, 32), (160, 2, 32),
paul@3 169
        (320, 1, 25)
paul@2 170
        ]
paul@2 171
paul@2 172
    palette = range(0, 8) * 2
paul@2 173
paul@40 174
    def __init__(self, ram, video):
paul@1 175
paul@40 176
        "Initialise the ULA with the given 'ram' and 'video' instances."
paul@2 177
paul@40 178
        self.ram = ram
paul@31 179
        self.video = video
paul@2 180
        self.set_mode(6)
paul@1 181
paul@31 182
        self.reset()
paul@31 183
paul@31 184
    def reset(self):
paul@31 185
paul@31 186
        "Reset the ULA."
paul@31 187
paul@43 188
        # General state.
paul@43 189
paul@43 190
        self.nmi = 0            # no NMI asserted initially
paul@43 191
        self.irq_vsync = 0      # no IRQ asserted initially
paul@43 192
paul@59 193
        # Communication.
paul@59 194
paul@59 195
        self.ram_address = 0    # address given to the RAM via RA0...RA7
paul@59 196
        self.data = 0           # data read from the RAM via RAM0...RAM3
paul@59 197
        self.cpu_address = 0    # address selected by the CPU via A0...A15
paul@59 198
        self.cpu_read = 0       # data read/write by the CPU selected using R/W
paul@59 199
paul@40 200
        # Internal state.
paul@40 201
paul@40 202
        self.access = 0         # counter used to determine whether a byte needs reading
paul@42 203
        self.have_pixels = 0    # whether pixel data has been read
paul@42 204
        self.writing_pixels = 0 # whether pixel data can be written
paul@45 205
        self.buffer = [BLANK]*8 # pixel buffer for decoded RAM data
paul@40 206
paul@66 207
        self.cycle = ShiftRegister()    # 8-state counter within each 2MHz period
paul@66 208
paul@66 209
        self.cycle.set_input(1) # assert the input to set the first state output
paul@66 210
        self.cycle.shift()
paul@66 211
        self.cycle.set_input(0) # reset the input since only one state output will be active
paul@50 212
paul@40 213
        self.reset_vertical()
paul@31 214
paul@2 215
    def set_mode(self, mode):
paul@1 216
paul@2 217
        """
paul@2 218
        For the given 'mode', initialise the...
paul@1 219
paul@2 220
          * width in pixels
paul@2 221
          * colour depth in bits per pixel
paul@2 222
          * number of character rows
paul@2 223
          * character row size in bytes
paul@2 224
          * screen size in bytes
paul@2 225
          * default screen start address
paul@2 226
          * horizontal pixel scaling factor
paul@2 227
          * line spacing in pixels
paul@2 228
          * number of entries in the pixel buffer
paul@31 229
paul@31 230
        The ULA should be reset after a mode switch in order to cleanly display
paul@31 231
        a full screen.
paul@2 232
        """
paul@1 233
paul@3 234
        self.width, self.depth, rows = self.modes[mode]
paul@3 235
paul@31 236
        columns = (self.width * self.depth) / 8                             # bits read -> bytes read
paul@40 237
        self.access_frequency = 80 / columns                                # cycle frequency for reading bytes
paul@31 238
        row_size = columns * LINES_PER_ROW
paul@2 239
paul@3 240
        # Memory access configuration.
paul@4 241
        # Note the limitation on positioning the screen start.
paul@3 242
paul@4 243
        screen_size = row_size * rows
paul@4 244
        self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00
paul@4 245
        self.screen_size = SCREEN_LIMIT - self.screen_start
paul@3 246
paul@3 247
        # Scanline configuration.
paul@1 248
paul@22 249
        self.xscale = MAX_WIDTH / self.width                                # pixel width in display pixels
paul@3 250
        self.spacing = MAX_HEIGHT / rows - LINES_PER_ROW                    # pixels between rows
paul@3 251
paul@3 252
        # Start of unused region.
paul@3 253
paul@3 254
        self.footer = rows * LINES_PER_ROW
paul@22 255
        self.margin = MAX_SCANLINE - rows * (LINES_PER_ROW + self.spacing) + self.spacing
paul@3 256
paul@3 257
        # Internal pixel buffer size.
paul@3 258
paul@2 259
        self.buffer_limit = 8 / self.depth
paul@1 260
paul@40 261
    def vsync(self, value=0):
paul@40 262
paul@40 263
        "Signal the start of a frame."
paul@40 264
paul@40 265
        self.csync = value
paul@40 266
        self.video.set_csync(value)
paul@40 267
paul@40 268
    def hsync(self, value=0):
paul@40 269
paul@40 270
        "Signal the end of a scanline."
paul@40 271
paul@40 272
        self.hs = value
paul@40 273
        self.video.set_hs(value)
paul@40 274
paul@40 275
    def reset_vertical(self):
paul@2 276
paul@2 277
        "Signal the start of a frame."
paul@1 278
paul@2 279
        self.line_start = self.address = self.screen_start
paul@5 280
        self.line = self.line_start % LINES_PER_ROW
paul@3 281
        self.ssub = 0
paul@31 282
        self.y = 0
paul@40 283
        self.x = 0
paul@2 284
paul@40 285
    def reset_horizontal(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@40 292
        if not self.inside_frame():
paul@40 293
            return
paul@2 294
paul@3 295
        # Support spacing between character rows.
paul@3 296
paul@3 297
        if self.ssub:
paul@3 298
            self.ssub -= 1
paul@3 299
            return
paul@3 300
paul@2 301
        self.line += 1
paul@2 302
paul@3 303
        # If not on a row boundary, move to the next line.
paul@3 304
paul@3 305
        if self.line % LINES_PER_ROW:
paul@2 306
            self.address = self.line_start + 1
paul@2 307
            self.wrap_address()
paul@2 308
paul@2 309
        # After the end of the last line in a row, the address should already
paul@2 310
        # have been positioned on the last line of the next column.
paul@1 311
paul@2 312
        else:
paul@2 313
            self.address -= LINES_PER_ROW - 1
paul@2 314
            self.wrap_address()
paul@1 315
paul@3 316
            # Test for the footer region.
paul@3 317
paul@3 318
            if self.spacing and self.line == self.footer:
paul@22 319
                self.ssub = self.margin
paul@3 320
                return
paul@1 321
paul@3 322
            # Support spacing between character rows.
paul@2 323
paul@22 324
            self.ssub = self.spacing
paul@3 325
paul@3 326
        self.line_start = self.address
paul@1 327
paul@40 328
    def in_frame(self): return MIN_PIXELLINE <= self.y < MAX_PIXELLINE
paul@40 329
    def inside_frame(self): return MIN_PIXELLINE < self.y < MAX_PIXELLINE
paul@42 330
    def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame()
paul@31 331
paul@31 332
    def update(self):
paul@1 333
paul@2 334
        """
paul@40 335
        Update the state of the ULA for each clock cycle. This involves updating
paul@40 336
        the pixel colour by reading from the pixel buffer.
paul@2 337
        """
paul@2 338
paul@40 339
        # Detect the end of the scanline.
paul@40 340
paul@40 341
        if self.x == MAX_SCANPOS:
paul@40 342
            self.reset_horizontal()
paul@40 343
paul@40 344
        # Detect the end of the frame.
paul@40 345
paul@40 346
        if self.y == MAX_SCANLINE:
paul@40 347
            self.reset_vertical()
paul@40 348
paul@40 349
paul@40 350
paul@40 351
        # Clock management.
paul@40 352
paul@43 353
        access_ram = not self.nmi and self.access == 0 and self.read_pixels() and not self.ssub
paul@40 354
paul@40 355
        # Set row address (for ULA access only).
paul@40 356
paul@50 357
        if self.cycle[0]:
paul@40 358
paul@59 359
            # Either assert a required address or propagate the CPU address.
paul@40 360
paul@40 361
            if access_ram:
paul@59 362
                self.init_row_address(self.address)
paul@59 363
            else:
paul@59 364
                self.init_row_address(self.cpu_address)
paul@40 365
paul@42 366
            # Initialise the pixel buffer if appropriate.
paul@42 367
paul@42 368
            if not self.writing_pixels and self.have_pixels:
paul@42 369
                self.xcounter = self.xscale
paul@42 370
                self.buffer_index = 0
paul@42 371
                self.fill_pixel_buffer()
paul@42 372
                self.writing_pixels = 1
paul@42 373
paul@40 374
        # Latch row address, set column address (for ULA access only).
paul@40 375
paul@50 376
        elif self.cycle[1]:
paul@40 377
paul@59 378
            # Select an address needed by the ULA or CPU.
paul@59 379
paul@59 380
            self.ram.row_select(self.ram_address)
paul@59 381
paul@59 382
            # Either assert a required address or propagate the CPU address.
paul@31 383
paul@40 384
            if access_ram:
paul@59 385
                self.init_column_address(self.address, 0)
paul@59 386
            else:
paul@59 387
                self.init_column_address(self.cpu_address, 0)
paul@40 388
paul@40 389
        # Latch column address.
paul@40 390
paul@50 391
        elif self.cycle[2]:
paul@40 392
paul@59 393
            # Select an address needed by the ULA or CPU.
paul@31 394
paul@59 395
            self.ram.column_select(self.ram_address)
paul@40 396
paul@40 397
        # Read 4 bits (for ULA access only).
paul@40 398
        # NOTE: Perhaps map alternate bits, not half-bytes.
paul@40 399
paul@50 400
        elif self.cycle[3]:
paul@40 401
paul@59 402
            # Either read from a required address or transfer CPU data.
paul@40 403
paul@40 404
            if access_ram:
paul@40 405
                self.data = self.ram.data << 4
paul@59 406
            else:
paul@59 407
                self.cpu_transfer_high()
paul@40 408
paul@40 409
        # Set column address (for ULA access only).
paul@40 410
paul@50 411
        elif self.cycle[4]:
paul@40 412
            self.ram.column_deselect()
paul@31 413
paul@59 414
            # Either assert a required address or propagate the CPU address.
paul@40 415
paul@40 416
            if access_ram:
paul@59 417
                self.init_column_address(self.address, 1)
paul@59 418
            else:
paul@59 419
                self.init_column_address(self.cpu_address, 1)
paul@40 420
paul@40 421
        # Latch column address.
paul@40 422
paul@50 423
        elif self.cycle[5]:
paul@40 424
paul@59 425
            # Select an address needed by the ULA or CPU.
paul@40 426
paul@59 427
            self.ram.column_select(self.ram_address)
paul@31 428
paul@40 429
        # Read 4 bits (for ULA access only).
paul@40 430
        # NOTE: Perhaps map alternate bits, not half-bytes.
paul@40 431
paul@50 432
        elif self.cycle[6]:
paul@40 433
paul@59 434
            # Either read from a required address or transfer CPU data.
paul@40 435
paul@40 436
            if access_ram:
paul@40 437
                self.data = self.data | self.ram.data
paul@42 438
                self.have_pixels = 1
paul@40 439
paul@40 440
                # Advance to the next column.
paul@40 441
paul@40 442
                self.address += LINES_PER_ROW
paul@40 443
                self.wrap_address()
paul@59 444
            else:
paul@59 445
                self.cpu_transfer_low()
paul@40 446
paul@40 447
        # Reset addresses.
paul@31 448
paul@50 449
        elif self.cycle[7]:
paul@40 450
            self.ram.column_deselect()
paul@40 451
            self.ram.row_deselect()
paul@40 452
paul@40 453
            # Update the RAM access controller.
paul@40 454
paul@40 455
            self.access = (self.access + 1) % self.access_frequency
paul@40 456
paul@66 457
        # Update the state of the device.
paul@66 458
paul@66 459
        self.cycle.set_input(self.cycle[7])
paul@66 460
        self.cycle.shift()
paul@66 461
paul@40 462
paul@40 463
paul@40 464
        # Video signalling.
paul@40 465
paul@40 466
        # Detect any sync conditions.
paul@31 467
paul@40 468
        if self.x == 0:
paul@40 469
            self.hsync()
paul@40 470
            if self.y == 0:
paul@40 471
                self.vsync()
paul@43 472
                self.irq_vsync = 0
paul@43 473
            elif self.y == MAX_PIXELLINE:
paul@43 474
                self.irq_vsync = 1
paul@40 475
paul@40 476
        # Detect the end of hsync.
paul@31 477
paul@40 478
        elif self.x == MAX_HSYNC:
paul@40 479
            self.hsync(1)
paul@40 480
paul@40 481
        # Detect the end of vsync.
paul@40 482
paul@40 483
        elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2:
paul@40 484
            self.vsync(1)
paul@40 485
paul@40 486
paul@40 487
paul@40 488
        # Pixel production.
paul@31 489
paul@3 490
        # Detect spacing between character rows.
paul@3 491
paul@42 492
        if not self.writing_pixels or self.ssub:
paul@31 493
            self.video.colour = BLANK
paul@3 494
paul@31 495
        # For pixels within the frame, obtain and output the value.
paul@31 496
paul@31 497
        else:
paul@40 498
paul@42 499
            self.xcounter -= 1
paul@42 500
            self.video.colour = self.buffer[self.buffer_index]
paul@1 501
paul@31 502
            # Scale pixels horizontally, only accessing the next pixel value
paul@31 503
            # after the required number of scan positions.
paul@22 504
paul@42 505
            if self.xcounter == 0:
paul@40 506
                self.xcounter = self.xscale
paul@31 507
                self.buffer_index += 1
paul@31 508
paul@42 509
                # Handle the buffer empty condition.
paul@22 510
paul@40 511
                if self.buffer_index >= self.buffer_limit:
paul@42 512
                    self.writing_pixels = 0
paul@2 513
paul@31 514
        self.x += 1
paul@2 515
paul@2 516
    def fill_pixel_buffer(self):
paul@1 517
paul@2 518
        """
paul@2 519
        Fill the pixel buffer by translating memory content for the current
paul@2 520
        mode.
paul@2 521
        """
paul@1 522
paul@40 523
        byte_value = self.data # which should have been read automatically
paul@1 524
paul@2 525
        i = 0
paul@2 526
        for colour in decode(byte_value, self.depth):
paul@2 527
            self.buffer[i] = get_physical_colour(self.palette[colour])
paul@2 528
            i += 1
paul@2 529
paul@2 530
    def wrap_address(self):
paul@2 531
        if self.address >= SCREEN_LIMIT:
paul@2 532
            self.address -= self.screen_size
paul@1 533
paul@59 534
    def init_row_address(self, address):
paul@59 535
        self.ram_address = (address & 0xff80) >> 7
paul@59 536
paul@59 537
    def init_column_address(self, address, offset):
paul@59 538
        self.ram_address = (address & 0x7f) << 1 | offset
paul@59 539
paul@59 540
    def cpu_transfer_high(self):
paul@59 541
        if self.cpu_read:
paul@59 542
            self.cpu_data = self.ram.data << 4
paul@59 543
paul@59 544
    def cpu_transfer_low(self):
paul@59 545
        if self.cpu_read:
paul@59 546
            self.cpu_data = self.data | self.ram.data
paul@59 547
paul@1 548
def get_physical_colour(value):
paul@1 549
paul@1 550
    """
paul@1 551
    Return the physical colour as an RGB triple for the given 'value'.
paul@1 552
    """
paul@1 553
paul@1 554
    return value & 1, value >> 1 & 1, value >> 2 & 1
paul@1 555
paul@1 556
def decode(value, depth):
paul@1 557
paul@1 558
    """
paul@1 559
    Decode the given byte 'value' according to the 'depth' in bits per pixel,
paul@1 560
    returning a sequence of pixel values.
paul@1 561
    """
paul@1 562
paul@1 563
    if depth == 1:
paul@1 564
        return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1,
paul@1 565
                value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1)
paul@1 566
    elif depth == 2:
paul@1 567
        return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1,
paul@1 568
                value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1)
paul@1 569
    elif depth == 4:
paul@1 570
        return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1,
paul@1 571
                value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1)
paul@1 572
    else:
paul@1 573
        raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth
paul@1 574
paul@1 575
# Convenience functions.
paul@1 576
paul@1 577
def encode(values, depth):
paul@1 578
paul@1 579
    """
paul@1 580
    Encode the given 'values' according to the 'depth' in bits per pixel,
paul@1 581
    returning a byte value for the pixels.
paul@1 582
    """
paul@1 583
paul@1 584
    result = 0
paul@1 585
paul@1 586
    if depth == 1:
paul@1 587
        for value in values:
paul@1 588
            result = result << 1 | (value & 1)
paul@1 589
    elif depth == 2:
paul@1 590
        for value in values:
paul@1 591
            result = result << 1 | (value & 2) << 3 | (value & 1)
paul@1 592
    elif depth == 4:
paul@1 593
        for value in values:
paul@1 594
            result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1)
paul@1 595
    else:
paul@1 596
        raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth
paul@1 597
paul@1 598
    return result
paul@1 599
paul@11 600
def get_ula():
paul@11 601
paul@31 602
    "Return a ULA initialised with a memory array and video."
paul@31 603
paul@40 604
    return ULA(get_ram(), get_video())
paul@11 605
paul@31 606
def get_video():
paul@31 607
paul@31 608
    "Return a video circuit."
paul@31 609
paul@31 610
    return Video()
paul@11 611
paul@40 612
def get_ram():
paul@10 613
paul@40 614
    "Return an instance representing the computer's RAM hardware."
paul@7 615
paul@40 616
    return RAM()
paul@1 617
paul@7 618
# Test program providing coverage (necessary for compilers like Shedskin).
paul@7 619
paul@7 620
if __name__ == "__main__":
paul@11 621
    ula = get_ula()
paul@7 622
    ula.set_mode(2)
paul@40 623
    ula.reset()
paul@40 624
    ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4))
paul@7 625
paul@7 626
    # Make a simple two-dimensional array of tuples (three-dimensional in pygame
paul@7 627
    # terminology).
paul@7 628
paul@29 629
    a = update(ula)
paul@7 630
paul@1 631
# vim: tabstop=4 expandtab shiftwidth=4