1.1 --- a/ula.py Fri Feb 10 01:07:20 2012 +0100
1.2 +++ b/ula.py Mon Feb 13 22:00:09 2012 +0100
1.3 @@ -9,11 +9,23 @@
1.4
1.5 LINES_PER_ROW = 8 # the number of pixel lines per character row
1.6 MAX_HEIGHT = 256 # the height of the screen in pixels
1.7 +MAX_WIDTH = 640 # the width of the screen in pixels
1.8 +
1.9 +MAX_CSYNC = 2 # the scanline during which vsync ends
1.10 +MIN_PIXELLINE = 38 # the first scanline involving pixel generation
1.11 MAX_SCANLINE = 312 # the number of scanlines in each frame
1.12 -MAX_WIDTH = 640 # the width of the screen in pixels
1.13 -MAX_SCANPOS = 1024 # the number of positions in each scanline
1.14 +
1.15 +MAX_PIXELLINE = MIN_PIXELLINE + MAX_HEIGHT
1.16 +
1.17 +MAX_HSYNC = 75 # the number of cycles in each hsync period
1.18 +MIN_PIXELPOS = 264 # the first cycle involving pixel generation
1.19 +MAX_SCANPOS = 1024 # the number of cycles in each scanline
1.20 +
1.21 +MAX_PIXELPOS = MIN_PIXELPOS + MAX_WIDTH
1.22 +
1.23 SCREEN_LIMIT = 0x8000 # the first address after the screen memory
1.24 MAX_MEMORY = 0x10000 # the number of addressable memory locations
1.25 +MAX_RAM = 0x10000 # the number of addressable RAM locations (64Kb in each IC)
1.26 BLANK = (0, 0, 0)
1.27
1.28 def update(ula):
1.29 @@ -30,6 +42,7 @@
1.30 ula.update()
1.31 video.update()
1.32 i += 1
1.33 +
1.34 return video.screen
1.35
1.36 class Video:
1.37 @@ -43,19 +56,70 @@
1.38 self.colour = BLANK
1.39 self.csync = 1
1.40 self.hs = 1
1.41 - self.reset()
1.42 + self.x = 0
1.43 + self.y = 0
1.44
1.45 - def reset(self):
1.46 - self.pos = 0
1.47 + def set_csync(self, value):
1.48 + if self.csync and not value:
1.49 + self.y = 0
1.50 + self.pos = 0
1.51 + self.csync = value
1.52 +
1.53 + def set_hs(self, value):
1.54 + if self.hs and not value:
1.55 + self.x = 0
1.56 + self.y += 1
1.57 + self.hs = value
1.58
1.59 def update(self):
1.60 - if self.csync:
1.61 - if self.hs:
1.62 + if MIN_PIXELLINE <= self.y < MAX_PIXELLINE:
1.63 + if MIN_PIXELPOS <= self.x < MAX_PIXELPOS:
1.64 self.screen[self.pos] = self.colour[0]; self.pos += 1
1.65 self.screen[self.pos] = self.colour[1]; self.pos += 1
1.66 self.screen[self.pos] = self.colour[2]; self.pos += 1
1.67 - else:
1.68 - self.pos = 0
1.69 + self.x += 1
1.70 +
1.71 +class RAM:
1.72 +
1.73 + """
1.74 + A class representing the RAM circuits (IC4 to IC7). Each circuit
1.75 + traditionally holds 64 kilobits, with two accesses required to read 2 bits
1.76 + from each in order to obtain a whole byte. Here, we model the circuits with
1.77 + a list of 65536 half-bytes with each bit representing a bit stored on a
1.78 + separate IC.
1.79 + """
1.80 +
1.81 + def __init__(self):
1.82 +
1.83 + "Initialise the RAM circuits."
1.84 +
1.85 + self.memory = [0] * MAX_RAM
1.86 + self.row_address = 0
1.87 + self.column_address = 0
1.88 + self.data = 0
1.89 +
1.90 + def row_select(self, address):
1.91 + self.row_address = address
1.92 +
1.93 + def row_deselect(self):
1.94 + pass
1.95 +
1.96 + def column_select(self, address):
1.97 + self.column_address = address
1.98 +
1.99 + # Read the data.
1.100 +
1.101 + self.data = self.memory[self.row_address << 8 | self.column_address]
1.102 +
1.103 + def column_deselect(self):
1.104 + pass
1.105 +
1.106 + # Convenience methods.
1.107 +
1.108 + def fill(self, start, end, value):
1.109 + for i in xrange(start, end):
1.110 + self.memory[i << 1] = value >> 4
1.111 + self.memory[i << 1 | 0x1] = value & 0xf
1.112
1.113 class ULA:
1.114
1.115 @@ -74,25 +138,29 @@
1.116
1.117 palette = range(0, 8) * 2
1.118
1.119 - def __init__(self, memory, video):
1.120 + def __init__(self, ram, video):
1.121
1.122 - "Initialise the ULA with the given 'memory' and 'video'."
1.123 + "Initialise the ULA with the given 'ram' and 'video' instances."
1.124
1.125 - self.memory = memory
1.126 + self.ram = ram
1.127 self.video = video
1.128 self.set_mode(6)
1.129
1.130 - # Internal state.
1.131 -
1.132 - self.buffer = [0] * 8
1.133 -
1.134 self.reset()
1.135
1.136 def reset(self):
1.137
1.138 "Reset the ULA."
1.139
1.140 - self.vsync()
1.141 + # Internal state.
1.142 +
1.143 + self.cycle = 0 # counter within each 2MHz period
1.144 + self.access = 0 # counter used to determine whether a byte needs reading
1.145 + self.ram_address = 0 # address given to the RAM
1.146 + self.data = 0 # data read from the RAM
1.147 + self.buffer = [0] * 8 # pixel buffer for decoded RAM data
1.148 +
1.149 + self.reset_vertical()
1.150
1.151 def set_mode(self, mode):
1.152
1.153 @@ -116,6 +184,7 @@
1.154 self.width, self.depth, rows = self.modes[mode]
1.155
1.156 columns = (self.width * self.depth) / 8 # bits read -> bytes read
1.157 + self.access_frequency = 80 / columns # cycle frequency for reading bytes
1.158 row_size = columns * LINES_PER_ROW
1.159
1.160 # Memory access configuration.
1.161 @@ -139,7 +208,21 @@
1.162
1.163 self.buffer_limit = 8 / self.depth
1.164
1.165 - def vsync(self):
1.166 + def vsync(self, value=0):
1.167 +
1.168 + "Signal the start of a frame."
1.169 +
1.170 + self.csync = value
1.171 + self.video.set_csync(value)
1.172 +
1.173 + def hsync(self, value=0):
1.174 +
1.175 + "Signal the end of a scanline."
1.176 +
1.177 + self.hs = value
1.178 + self.video.set_hs(value)
1.179 +
1.180 + def reset_vertical(self):
1.181
1.182 "Signal the start of a frame."
1.183
1.184 @@ -147,18 +230,17 @@
1.185 self.line = self.line_start % LINES_PER_ROW
1.186 self.ssub = 0
1.187 self.y = 0
1.188 - self.reset_horizontal()
1.189 -
1.190 - # Signal the video circuit.
1.191 + self.x = 0
1.192
1.193 - self.csync = self.video.csync = 1
1.194 + def reset_horizontal(self):
1.195
1.196 - def hsync(self):
1.197 -
1.198 - "Signal the end of a scanline."
1.199 + "Reset horizontal state within the active region of the frame."
1.200
1.201 self.y += 1
1.202 - self.reset_horizontal()
1.203 + self.x = 0
1.204 +
1.205 + if not self.inside_frame():
1.206 + return
1.207
1.208 # Support spacing between character rows.
1.209
1.210 @@ -193,70 +275,176 @@
1.211
1.212 self.line_start = self.address
1.213
1.214 - def reset_horizontal(self):
1.215 -
1.216 - "Reset horizontal state."
1.217 -
1.218 - self.x = 0
1.219 - self.buffer_index = self.buffer_limit # need refill
1.220 -
1.221 - # Signal the video circuit.
1.222 -
1.223 - self.hs = self.video.hs = 1
1.224 + def in_frame(self): return MIN_PIXELLINE <= self.y < MAX_PIXELLINE
1.225 + def inside_frame(self): return MIN_PIXELLINE < self.y < MAX_PIXELLINE
1.226 + def read_pixels(self): return MIN_PIXELPOS - 8 <= self.x < MAX_PIXELPOS - 8 and self.in_frame()
1.227 + def make_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame()
1.228
1.229 def update(self):
1.230
1.231 """
1.232 - Update the pixel colour by reading from the pixel buffer.
1.233 + Update the state of the ULA for each clock cycle. This involves updating
1.234 + the pixel colour by reading from the pixel buffer.
1.235 """
1.236
1.237 - # Detect the end of the line.
1.238 + # Detect the end of the scanline.
1.239 +
1.240 + if self.x == MAX_SCANPOS:
1.241 + self.reset_horizontal()
1.242 +
1.243 + # Detect the end of the frame.
1.244 +
1.245 + if self.y == MAX_SCANLINE:
1.246 + self.reset_vertical()
1.247 +
1.248 +
1.249 +
1.250 + # Clock management.
1.251 +
1.252 + access_ram = self.access == 0 and self.read_pixels() and not self.ssub
1.253 +
1.254 + # Set row address (for ULA access only).
1.255 +
1.256 + if self.cycle == 0:
1.257 +
1.258 + # NOTE: Propagate CPU address here.
1.259 +
1.260 + if access_ram:
1.261 + self.ram_address = (self.address & 0xff80) >> 7
1.262 +
1.263 + # Latch row address, set column address (for ULA access only).
1.264 +
1.265 + elif self.cycle == 1:
1.266 +
1.267 + # NOTE: Permit CPU access here.
1.268
1.269 - if self.x >= MAX_WIDTH:
1.270 - if self.x == MAX_WIDTH:
1.271 - self.hs = self.video.hs = 0
1.272 + if access_ram:
1.273 + self.ram.row_select(self.ram_address)
1.274 +
1.275 + # NOTE: Propagate CPU address here.
1.276 +
1.277 + if access_ram:
1.278 + self.ram_address = (self.address & 0x7f) << 1
1.279 +
1.280 + # Latch column address.
1.281 +
1.282 + elif self.cycle == 2:
1.283 +
1.284 + # NOTE: Permit CPU access here.
1.285
1.286 - # Detect the end of the scanline.
1.287 + if access_ram:
1.288 + self.ram.column_select(self.ram_address)
1.289 +
1.290 + # Read 4 bits (for ULA access only).
1.291 + # NOTE: Perhaps map alternate bits, not half-bytes.
1.292 +
1.293 + elif self.cycle == 3:
1.294 +
1.295 + # NOTE: Propagate CPU data here.
1.296 +
1.297 + if access_ram:
1.298 + self.data = self.ram.data << 4
1.299 +
1.300 + # Set column address (for ULA access only).
1.301 +
1.302 + elif self.cycle == 4:
1.303 + self.ram.column_deselect()
1.304
1.305 - elif self.x == MAX_SCANPOS:
1.306 - self.hsync()
1.307 + # NOTE: Propagate CPU address here.
1.308 +
1.309 + if access_ram:
1.310 + self.ram_address = (self.address & 0x7f) << 1 | 0x1
1.311 +
1.312 + # Latch column address.
1.313 +
1.314 + elif self.cycle == 5:
1.315 +
1.316 + # NOTE: Permit CPU access here.
1.317 +
1.318 + if access_ram:
1.319 + self.ram.column_select(self.ram_address)
1.320
1.321 - # Detect the end of the frame.
1.322 + # Read 4 bits (for ULA access only).
1.323 + # NOTE: Perhaps map alternate bits, not half-bytes.
1.324 +
1.325 + elif self.cycle == 6:
1.326 +
1.327 + # NOTE: Propagate CPU data here.
1.328 +
1.329 + if access_ram:
1.330 + self.data = self.data | self.ram.data
1.331 +
1.332 + # Advance to the next column.
1.333 +
1.334 + self.address += LINES_PER_ROW
1.335 + self.wrap_address()
1.336 +
1.337 + # Reset addresses.
1.338
1.339 - if self.y == MAX_SCANLINE:
1.340 - self.vsync()
1.341 + elif self.cycle == 7:
1.342 + self.ram.column_deselect()
1.343 + self.ram.row_deselect()
1.344 +
1.345 + # Update the RAM access controller.
1.346 +
1.347 + self.access = (self.access + 1) % self.access_frequency
1.348 +
1.349 + self.cycle = (self.cycle + 1) % 8
1.350 +
1.351 +
1.352 +
1.353 + # Video signalling.
1.354 +
1.355 + # Detect any sync conditions.
1.356
1.357 - # Detect the end of the screen.
1.358 + if self.x == 0:
1.359 + self.hsync()
1.360 + if self.y == 0:
1.361 + self.vsync()
1.362 +
1.363 + # Detect the end of hsync.
1.364
1.365 - elif self.y == MAX_HEIGHT:
1.366 - self.csync = self.video.csync = 0
1.367 + elif self.x == MAX_HSYNC:
1.368 + self.hsync(1)
1.369 +
1.370 + # Detect the end of vsync.
1.371 +
1.372 + elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2:
1.373 + self.vsync(1)
1.374 +
1.375 +
1.376 +
1.377 + # Pixel production.
1.378
1.379 # Detect spacing between character rows.
1.380
1.381 - if self.ssub:
1.382 + if not self.make_pixels() or self.ssub:
1.383 self.video.colour = BLANK
1.384
1.385 - # Detect horizontal and vertical sync conditions.
1.386 -
1.387 - elif not self.hs or not self.csync:
1.388 - pass
1.389 -
1.390 # For pixels within the frame, obtain and output the value.
1.391
1.392 else:
1.393 + # Detect the start of the pixel generation.
1.394 +
1.395 + if self.x == MIN_PIXELPOS:
1.396 + self.xcounter = self.xscale
1.397 + self.buffer_index = 0
1.398 + self.fill_pixel_buffer()
1.399
1.400 # Scale pixels horizontally, only accessing the next pixel value
1.401 # after the required number of scan positions.
1.402
1.403 - if self.x % self.xscale == 0:
1.404 + elif self.xcounter == 0:
1.405 + self.xcounter = self.xscale
1.406 self.buffer_index += 1
1.407
1.408 - # Fill the buffer once all values have been read.
1.409 + # Fill the pixel buffer, assuming that data is available.
1.410
1.411 - if self.buffer_index >= self.buffer_limit:
1.412 - self.buffer_index = 0
1.413 - self.fill_pixel_buffer()
1.414 + if self.buffer_index >= self.buffer_limit:
1.415 + self.buffer_index = 0
1.416 + self.fill_pixel_buffer()
1.417
1.418 + self.xcounter -= 1
1.419 self.video.colour = self.buffer[self.buffer_index]
1.420
1.421 self.x += 1
1.422 @@ -268,27 +456,17 @@
1.423 mode.
1.424 """
1.425
1.426 - byte_value = self.memory[self.address]
1.427 + byte_value = self.data # which should have been read automatically
1.428
1.429 i = 0
1.430 for colour in decode(byte_value, self.depth):
1.431 self.buffer[i] = get_physical_colour(self.palette[colour])
1.432 i += 1
1.433
1.434 - # Advance to the next column.
1.435 -
1.436 - self.address += LINES_PER_ROW
1.437 - self.wrap_address()
1.438 -
1.439 def wrap_address(self):
1.440 if self.address >= SCREEN_LIMIT:
1.441 self.address -= self.screen_size
1.442
1.443 - # Convenience methods.
1.444 -
1.445 - def fill(self, start, end, value):
1.446 - fill(self.memory, start, end, value)
1.447 -
1.448 def get_physical_colour(value):
1.449
1.450 """
1.451 @@ -345,7 +523,7 @@
1.452
1.453 "Return a ULA initialised with a memory array and video."
1.454
1.455 - return ULA(get_memory(), get_video())
1.456 + return ULA(get_ram(), get_video())
1.457
1.458 def get_video():
1.459
1.460 @@ -353,22 +531,19 @@
1.461
1.462 return Video()
1.463
1.464 -def get_memory():
1.465 -
1.466 - "Return an array representing the computer's memory."
1.467 +def get_ram():
1.468
1.469 - return [0] * MAX_MEMORY
1.470 + "Return an instance representing the computer's RAM hardware."
1.471
1.472 -def fill(memory, start, end, value):
1.473 - for i in xrange(start, end):
1.474 - memory[i] = value
1.475 + return RAM()
1.476
1.477 # Test program providing coverage (necessary for compilers like Shedskin).
1.478
1.479 if __name__ == "__main__":
1.480 ula = get_ula()
1.481 ula.set_mode(2)
1.482 - ula.fill(0x5800 - 320, 0x8000, encode((2, 7), 4))
1.483 + ula.reset()
1.484 + ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4))
1.485
1.486 # Make a simple two-dimensional array of tuples (three-dimensional in pygame
1.487 # terminology).