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