# HG changeset patch # User Paul Boddie # Date 1329166809 -3600 # Node ID 99b0ecdba781b21bdf32c25df7f1a77c33b26b96 # Parent 9adfa08bfa74973defd0345bc5deff637c9665e5 Introduced RAM access simulation, attempting to support the mechanism through which RAM addresses are made and sent to the RAM circuits, with the results returned 4 bits at a time. Added more information about video timing and its presumed effect on the ULA, attempting to support the appropriate timing periods for pixel generation, horizontal and vertical synchronisation, and the necessary waiting periods before and after utilised scanlines and scanline regions. diff -r 9adfa08bfa74 -r 99b0ecdba781 ULA.txt --- a/ULA.txt Fri Feb 10 01:07:20 2012 +0100 +++ b/ULA.txt Mon Feb 13 22:00:09 2012 +0100 @@ -1,16 +1,13 @@ Timing ------ -According to the above (15.3.2 in the AUG), there are 312 scanlines, 256 of -which are used to generate pixel data. At 50Hz, this means that 128 cycles are -spent on each scanline (2000000 cycles / 50 = 40000 cycles; 40000 cycles / 312 -~= 128 cycles). This is consistent with the observation that each scanline +According to 15.3.2 in the Advanced User Guide, there are 312 scanlines, 256 +of which are used to generate pixel data. At 50Hz, this means that 128 cycles +are spent on each scanline (2000000 cycles / 50 = 40000 cycles; 40000 cycles / +312 ~= 128 cycles). This is consistent with the observation that each scanline requires at most 80 bytes of data, and that the ULA is apparently busy for 40 out of 64 microseconds in each scanline. -See: Acorn Electron Advanced User Guide -See: http://mdfs.net/Docs/Comp/Electron/Techinfo.htm - Access to RAM involves accessing four 64Kb dynamic RAM devices (IC4 to IC7, each providing two bits of each byte) using two cycles within the 500ns period of the 2MHz clock to complete each access operation. Since the CPU and ULA @@ -19,9 +16,6 @@ accessing RAM). The CPU is driven by an external clock (IC8) whose 16MHz frequency is divided by the ULA (IC1) depending on the screen mode in use. -See: Acorn Electron Service Manual - http://acorn.chriswhy.co.uk/docs/Acorn/Manuals/Acorn_ElectronSM.pdf - Each 16MHz cycle is approximately 62.5ns. To access the memory, the following patterns corresponding to 16MHz cycles are required: @@ -47,11 +41,84 @@ communicate the latter behaviour. In the TM4164EC4 datasheet, it appears that "page mode" provides the appropriate behaviour for that particular product. -See: http://www.datasheetarchive.com/dl/Datasheets-112/DSAP0051030.pdf +Video Timing +------------ + +According to 8.7 in the Service Manual, and the PAL Wikipedia page, +approximately 4.7µs is used for the sync pulse, 5.7µs for the "back porch" +(including the "colour burst"), and 1.65µs for the "front porch", totalling +12.05µs and thus leaving 51.95µs for the active video signal for each +scanline. As the Service Manual suggests in the oscilloscope traces, the +display information is transmitted more or less centred within the active +video period since the ULA will only be providing pixel data for 40µs in each +scanline. Each 62.5ns cycle happens to correspond to 64µs divided by 1024, meaning that each scanline can be divided into 1024 cycles, although only 640 at most are -actively used to provide pixel data. +actively used to provide pixel data. Pixel data production should only occur +within a certain period on each scanline, approximately 262 cycles after the +start of hsync: + + active video period = 51.95µs + pixel data period = 40µs + total silent period = 51.95µs - 40µs = 11.95µs + silent periods (before and after) = 11.95µs / 2 = 5.975µs + hsync and back porch period = 4.7µs + 5.7µs = 10.4µs + time before pixel data period = 10.4µs + 5.975µs = 16.375µs + pixel data period start cycle = 16.375µs / 62.5ns = 262 + +By choosing a number divisible by 8, the RAM access mechanism can be +synchronised with the pixel production. Thus, 264 is a more appropriate start +cycle. + +The "vertical blanking period", meaning the period before picture information +in each field is 25 lines out of 312 (strictly 312.5) and thus lasts for +1.6ms. Of this, 2.5 lines occur before the vsync (field sync) which also lasts +for 2.5 lines. Thus, the first visible scanline on the first field of a frame +occurs half way through the 23rd scanline period measured from the start of +vsync: + + 10 20 23 + Line in frame: 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 + Line from 1: 0 22 3 + Line on screen: .:::::VVVVV::::: 12233445566 + |_________________________________________________| + 25 line vertical blanking period + +In the second field of a frame, the first visible scanline coincides with the +24th scanline period measured from the start of line 313 in the frame: + + 310 336 + Line in frame: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 + Line from 313: 0 23 + Line on screen: 88:::::VVVVV:::: 11223344 + 288 | | + |_________________________________________________| + 25 line vertical blanking period + +In order to consider only full lines, we might consider the start of each +frame to occur 23 lines after the start of vsync. + +Again, it is likely that pixel data production should only occur on scanlines +within a certain period on each frame. The "625/50" document indicates that +only a certain region is "safe" to use, suggesting a vertically centred region +with approximately 15 blank lines above and below the picture. Thus, the start +of the picture could be chosen as 38 lines after the start of vsync. + +See: Acorn Electron Advanced User Guide +See: http://mdfs.net/Docs/Comp/Electron/Techinfo.htm +See: http://en.wikipedia.org/wiki/PAL +See: http://en.wikipedia.org/wiki/Analog_television#Structure_of_a_video_signal +See: The 625/50 PAL Video Signal and TV Compatible Graphics Modes + http://lipas.uwasa.fi/~f76998/video/modes/ +See: PAL TV timing and voltages + http://www.retroleum.co.uk/electronics-articles/pal-tv-timing-and-voltages/ +See: Line Standards + http://www.pembers.freeserve.co.uk/World-TV-Standards/Line-Standards.html +See: TM4164EC4 65,536 by 4-Bit Dynamic RAM Module + http://www.datasheetarchive.com/dl/Datasheets-112/DSAP0051030.pdf +See: Acorn Electron Service Manual + http://acorn.chriswhy.co.uk/docs/Acorn/Manuals/Acorn_ElectronSM.pdf Shadow/Expanded Memory ---------------------- diff -r 9adfa08bfa74 -r 99b0ecdba781 main.py --- a/main.py Fri Feb 10 01:07:20 2012 +0100 +++ b/main.py Mon Feb 13 22:00:09 2012 +0100 @@ -52,30 +52,34 @@ ula.set_mode(2); ula.reset() - ula.fill(0x3000, 0x5800 - 320, encode((1, 6), 4)) - ula.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) + ula.ram.fill(0x3000, 0x5800 - 320, encode((1, 6), 4)) + ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) ula_screen = update(ula) update_screen(screen, ula_screen) + print "Screen updated." mainloop() ula.screen_start = 0x3000 + 2 ula_screen = update(ula) update_screen(screen, ula_screen) + print "Screen updated." mainloop() # Test MODE 6. ula.set_mode(6); ula.reset() - ula.fill(0x6000, 0x6f00 + 160, encode((1, 0, 1, 1, 0, 0, 1, 1), 1)) - ula.fill(0x6f00 + 160, 0x7f40, encode((1, 0, 1, 0, 1, 0, 1, 0), 1)) + ula.ram.fill(0x6000, 0x6f00 + 160, encode((1, 0, 1, 1, 0, 0, 1, 1), 1)) + ula.ram.fill(0x6f00 + 160, 0x7f40, encode((1, 0, 1, 0, 1, 0, 1, 0), 1)) ula_screen = update(ula) update_screen(screen, ula_screen) + print "Screen updated." mainloop() ula.screen_start = 0x6f00 + 160 ula_screen = update(ula) update_screen(screen, ula_screen) + print "Screen updated." mainloop() # vim: tabstop=4 expandtab shiftwidth=4 diff -r 9adfa08bfa74 -r 99b0ecdba781 ula.py --- a/ula.py Fri Feb 10 01:07:20 2012 +0100 +++ b/ula.py Mon Feb 13 22:00:09 2012 +0100 @@ -9,11 +9,23 @@ LINES_PER_ROW = 8 # the number of pixel lines per character row MAX_HEIGHT = 256 # the height of the screen in pixels +MAX_WIDTH = 640 # the width of the screen in pixels + +MAX_CSYNC = 2 # the scanline during which vsync ends +MIN_PIXELLINE = 38 # the first scanline involving pixel generation MAX_SCANLINE = 312 # the number of scanlines in each frame -MAX_WIDTH = 640 # the width of the screen in pixels -MAX_SCANPOS = 1024 # the number of positions in each scanline + +MAX_PIXELLINE = MIN_PIXELLINE + MAX_HEIGHT + +MAX_HSYNC = 75 # the number of cycles in each hsync period +MIN_PIXELPOS = 264 # the first cycle involving pixel generation +MAX_SCANPOS = 1024 # the number of cycles in each scanline + +MAX_PIXELPOS = MIN_PIXELPOS + MAX_WIDTH + SCREEN_LIMIT = 0x8000 # the first address after the screen memory MAX_MEMORY = 0x10000 # the number of addressable memory locations +MAX_RAM = 0x10000 # the number of addressable RAM locations (64Kb in each IC) BLANK = (0, 0, 0) def update(ula): @@ -30,6 +42,7 @@ ula.update() video.update() i += 1 + return video.screen class Video: @@ -43,19 +56,70 @@ self.colour = BLANK self.csync = 1 self.hs = 1 - self.reset() + self.x = 0 + self.y = 0 - def reset(self): - self.pos = 0 + def set_csync(self, value): + if self.csync and not value: + self.y = 0 + self.pos = 0 + self.csync = value + + def set_hs(self, value): + if self.hs and not value: + self.x = 0 + self.y += 1 + self.hs = value def update(self): - if self.csync: - if self.hs: + if MIN_PIXELLINE <= self.y < MAX_PIXELLINE: + if MIN_PIXELPOS <= self.x < MAX_PIXELPOS: self.screen[self.pos] = self.colour[0]; self.pos += 1 self.screen[self.pos] = self.colour[1]; self.pos += 1 self.screen[self.pos] = self.colour[2]; self.pos += 1 - else: - self.pos = 0 + self.x += 1 + +class RAM: + + """ + A class representing the RAM circuits (IC4 to IC7). Each circuit + traditionally holds 64 kilobits, with two accesses required to read 2 bits + from each in order to obtain a whole byte. Here, we model the circuits with + a list of 65536 half-bytes with each bit representing a bit stored on a + separate IC. + """ + + def __init__(self): + + "Initialise the RAM circuits." + + self.memory = [0] * MAX_RAM + self.row_address = 0 + self.column_address = 0 + self.data = 0 + + def row_select(self, address): + self.row_address = address + + def row_deselect(self): + pass + + def column_select(self, address): + self.column_address = address + + # Read the data. + + self.data = self.memory[self.row_address << 8 | self.column_address] + + def column_deselect(self): + pass + + # Convenience methods. + + def fill(self, start, end, value): + for i in xrange(start, end): + self.memory[i << 1] = value >> 4 + self.memory[i << 1 | 0x1] = value & 0xf class ULA: @@ -74,25 +138,29 @@ palette = range(0, 8) * 2 - def __init__(self, memory, video): + def __init__(self, ram, video): - "Initialise the ULA with the given 'memory' and 'video'." + "Initialise the ULA with the given 'ram' and 'video' instances." - self.memory = memory + self.ram = ram self.video = video self.set_mode(6) - # Internal state. - - self.buffer = [0] * 8 - self.reset() def reset(self): "Reset the ULA." - self.vsync() + # Internal state. + + self.cycle = 0 # counter within each 2MHz period + self.access = 0 # counter used to determine whether a byte needs reading + self.ram_address = 0 # address given to the RAM + self.data = 0 # data read from the RAM + self.buffer = [0] * 8 # pixel buffer for decoded RAM data + + self.reset_vertical() def set_mode(self, mode): @@ -116,6 +184,7 @@ self.width, self.depth, rows = self.modes[mode] columns = (self.width * self.depth) / 8 # bits read -> bytes read + self.access_frequency = 80 / columns # cycle frequency for reading bytes row_size = columns * LINES_PER_ROW # Memory access configuration. @@ -139,7 +208,21 @@ self.buffer_limit = 8 / self.depth - def vsync(self): + def vsync(self, value=0): + + "Signal the start of a frame." + + self.csync = value + self.video.set_csync(value) + + def hsync(self, value=0): + + "Signal the end of a scanline." + + self.hs = value + self.video.set_hs(value) + + def reset_vertical(self): "Signal the start of a frame." @@ -147,18 +230,17 @@ self.line = self.line_start % LINES_PER_ROW self.ssub = 0 self.y = 0 - self.reset_horizontal() - - # Signal the video circuit. + self.x = 0 - self.csync = self.video.csync = 1 + def reset_horizontal(self): - def hsync(self): - - "Signal the end of a scanline." + "Reset horizontal state within the active region of the frame." self.y += 1 - self.reset_horizontal() + self.x = 0 + + if not self.inside_frame(): + return # Support spacing between character rows. @@ -193,70 +275,176 @@ self.line_start = self.address - def reset_horizontal(self): - - "Reset horizontal state." - - self.x = 0 - self.buffer_index = self.buffer_limit # need refill - - # Signal the video circuit. - - self.hs = self.video.hs = 1 + def in_frame(self): return MIN_PIXELLINE <= self.y < MAX_PIXELLINE + def inside_frame(self): return MIN_PIXELLINE < self.y < MAX_PIXELLINE + def read_pixels(self): return MIN_PIXELPOS - 8 <= self.x < MAX_PIXELPOS - 8 and self.in_frame() + def make_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() def update(self): """ - Update the pixel colour by reading from the pixel buffer. + Update the state of the ULA for each clock cycle. This involves updating + the pixel colour by reading from the pixel buffer. """ - # Detect the end of the line. + # Detect the end of the scanline. + + if self.x == MAX_SCANPOS: + self.reset_horizontal() + + # Detect the end of the frame. + + if self.y == MAX_SCANLINE: + self.reset_vertical() + + + + # Clock management. + + access_ram = self.access == 0 and self.read_pixels() and not self.ssub + + # Set row address (for ULA access only). + + if self.cycle == 0: + + # NOTE: Propagate CPU address here. + + if access_ram: + self.ram_address = (self.address & 0xff80) >> 7 + + # Latch row address, set column address (for ULA access only). + + elif self.cycle == 1: + + # NOTE: Permit CPU access here. - if self.x >= MAX_WIDTH: - if self.x == MAX_WIDTH: - self.hs = self.video.hs = 0 + if access_ram: + self.ram.row_select(self.ram_address) + + # NOTE: Propagate CPU address here. + + if access_ram: + self.ram_address = (self.address & 0x7f) << 1 + + # Latch column address. + + elif self.cycle == 2: + + # NOTE: Permit CPU access here. - # Detect the end of the scanline. + if access_ram: + self.ram.column_select(self.ram_address) + + # Read 4 bits (for ULA access only). + # NOTE: Perhaps map alternate bits, not half-bytes. + + elif self.cycle == 3: + + # NOTE: Propagate CPU data here. + + if access_ram: + self.data = self.ram.data << 4 + + # Set column address (for ULA access only). + + elif self.cycle == 4: + self.ram.column_deselect() - elif self.x == MAX_SCANPOS: - self.hsync() + # NOTE: Propagate CPU address here. + + if access_ram: + self.ram_address = (self.address & 0x7f) << 1 | 0x1 + + # Latch column address. + + elif self.cycle == 5: + + # NOTE: Permit CPU access here. + + if access_ram: + self.ram.column_select(self.ram_address) - # Detect the end of the frame. + # Read 4 bits (for ULA access only). + # NOTE: Perhaps map alternate bits, not half-bytes. + + elif self.cycle == 6: + + # NOTE: Propagate CPU data here. + + if access_ram: + self.data = self.data | self.ram.data + + # Advance to the next column. + + self.address += LINES_PER_ROW + self.wrap_address() + + # Reset addresses. - if self.y == MAX_SCANLINE: - self.vsync() + elif self.cycle == 7: + self.ram.column_deselect() + self.ram.row_deselect() + + # Update the RAM access controller. + + self.access = (self.access + 1) % self.access_frequency + + self.cycle = (self.cycle + 1) % 8 + + + + # Video signalling. + + # Detect any sync conditions. - # Detect the end of the screen. + if self.x == 0: + self.hsync() + if self.y == 0: + self.vsync() + + # Detect the end of hsync. - elif self.y == MAX_HEIGHT: - self.csync = self.video.csync = 0 + elif self.x == MAX_HSYNC: + self.hsync(1) + + # Detect the end of vsync. + + elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: + self.vsync(1) + + + + # Pixel production. # Detect spacing between character rows. - if self.ssub: + if not self.make_pixels() or self.ssub: self.video.colour = BLANK - # Detect horizontal and vertical sync conditions. - - elif not self.hs or not self.csync: - pass - # For pixels within the frame, obtain and output the value. else: + # Detect the start of the pixel generation. + + if self.x == MIN_PIXELPOS: + self.xcounter = self.xscale + self.buffer_index = 0 + self.fill_pixel_buffer() # Scale pixels horizontally, only accessing the next pixel value # after the required number of scan positions. - if self.x % self.xscale == 0: + elif self.xcounter == 0: + self.xcounter = self.xscale self.buffer_index += 1 - # Fill the buffer once all values have been read. + # Fill the pixel buffer, assuming that data is available. - if self.buffer_index >= self.buffer_limit: - self.buffer_index = 0 - self.fill_pixel_buffer() + if self.buffer_index >= self.buffer_limit: + self.buffer_index = 0 + self.fill_pixel_buffer() + self.xcounter -= 1 self.video.colour = self.buffer[self.buffer_index] self.x += 1 @@ -268,27 +456,17 @@ mode. """ - byte_value = self.memory[self.address] + byte_value = self.data # which should have been read automatically i = 0 for colour in decode(byte_value, self.depth): self.buffer[i] = get_physical_colour(self.palette[colour]) i += 1 - # Advance to the next column. - - self.address += LINES_PER_ROW - self.wrap_address() - def wrap_address(self): if self.address >= SCREEN_LIMIT: self.address -= self.screen_size - # Convenience methods. - - def fill(self, start, end, value): - fill(self.memory, start, end, value) - def get_physical_colour(value): """ @@ -345,7 +523,7 @@ "Return a ULA initialised with a memory array and video." - return ULA(get_memory(), get_video()) + return ULA(get_ram(), get_video()) def get_video(): @@ -353,22 +531,19 @@ return Video() -def get_memory(): - - "Return an array representing the computer's memory." +def get_ram(): - return [0] * MAX_MEMORY + "Return an instance representing the computer's RAM hardware." -def fill(memory, start, end, value): - for i in xrange(start, end): - memory[i] = value + return RAM() # Test program providing coverage (necessary for compilers like Shedskin). if __name__ == "__main__": ula = get_ula() ula.set_mode(2) - ula.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) + ula.reset() + ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) # Make a simple two-dimensional array of tuples (three-dimensional in pygame # terminology).