# HG changeset patch # User Paul Boddie # Date 1324338993 -3600 # Node ID 579ebc9db48b41705f59e547a8fa081dcd124430 # Parent 2d6bb0876770d810e7b1898f1d524b4240753ff7 Introduced a separate video abstraction, making the ULA responsible for performing the appropriate signalling for each clock cycle. Added some details of the different ULA pins, introducing some of this detail into the software for authenticity. diff -r 2d6bb0876770 -r 579ebc9db48b ULA.txt --- a/ULA.txt Sun Dec 18 19:29:20 2011 +0100 +++ b/ULA.txt Tue Dec 20 00:56:33 2011 +0100 @@ -8,7 +8,7 @@ of data, and that the ULA is apparently busy for 40 out of 64 microseconds in each scanline. -See: The Advanced User Guide for the Acorn Electron +See: Acorn Electron Advanced User Guide See: http://mdfs.net/Docs/Comp/Electron/Techinfo.htm Hardware Scrolling @@ -338,3 +338,75 @@ control over the palette (using address &FE21, compared to &FE07-F on the Electron) and other system-specific functions. Since the location usage is generally incompatible, this region could be reused for other purposes. + +ULA Pin Functions +----------------- + +The functions of the ULA pins are described in the Electron Service Manual. Of +interest to video processing are the following: + + CSYNC (low during horizontal or vertical synchronisation periods, high + otherwise) + + HS (low during horizontal synchronisation periods, high otherwise) + + RED, GREEN, BLUE (pixel colour outputs) + + CLOCK IN (a 16MHz clock input, 4V peak to peak) + + PHI OUT (a 1MHz, 2MHz and stopped clock signal for the CPU) + +More general memory access pins: + + RAM0...RAM3 (data lines to/from the RAM) + + RA0...RA7 (address lines for sending both row and column addresses to the RAM) + + RAS (row address strobe setting the row address on a negative edge) + + CAS (column address strobe setting the column address on a negative edge) + + WE (sets write enable with logic 0, read with logic 1) + + ROM (select data access from ROM) + +CPU-oriented memory access pins: + + A0...A15 (CPU address lines) + + PD0...PD7 (CPU data lines) + + R/W (indicates CPU write with logic 0, CPU read with logic 1) + +Interrupt-related pins: + + NMI (CPU request for uninterrupted 1MHz access to memory) + + IRQ (signal event to CPU) + + POR (power-on reset, resetting the ULA on a positive edge and asserting the + CPU's RST pin) + + RST (master reset for the CPU signalled on power-up and by the Break key) + +Keyboard-related pins: + + KBD0...KBD3 (keyboard inputs) + + CAPS LOCK (control status LED) + +Sound-related pins: + + SOUND O/P (sound output using internal oscillator) + +Cassette-related pins: + + CAS IN (cassette circuit input, between 0.5V to 2V peak to peak) + + CAS OUT (pseudo-sinusoidal output, 1.8V peak to peak) + + CAS RC (detect high tone) + + CAS MO (motor relay output) + + ÷13 IN (~1200 baud clock input) diff -r 2d6bb0876770 -r 579ebc9db48b main.py --- a/main.py Sun Dec 18 19:29:20 2011 +0100 +++ b/main.py Tue Dec 20 00:56:33 2011 +0100 @@ -50,7 +50,7 @@ # Test MODE 2. - ula.set_mode(2) + ula.set_mode(2); ula.reset() ula.fill(0x3000, 0x5800 - 320, encode((1, 6), 4)) ula.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) @@ -65,7 +65,7 @@ # Test MODE 6. - ula.set_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)) diff -r 2d6bb0876770 -r 579ebc9db48b ula.py --- a/ula.py Sun Dec 18 19:29:20 2011 +0100 +++ b/ula.py Tue Dec 20 00:56:33 2011 +0100 @@ -16,36 +16,55 @@ MAX_MEMORY = 0x10000 # the number of addressable memory locations BLANK = (0, 0, 0) -screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT)) - def update(ula): """ - Return a screen array by reading from the 'ula'. This function effectively - has the role of the video circuit, but also provides the clock signal to the - ULA. + Update the 'ula' for one frame. Return the resulting screen. + """ + + video = ula.video + + i = 0 + limit = MAX_SCANLINE * MAX_SCANPOS + while i < limit: + ula.update() + video.update() + i += 1 + return video.screen + +class Video: + + """ + A class representing the video circuitry. """ - ula.vsync() - pos = 0 - y = 0 - while y < MAX_SCANLINE: - x = 0 - while x < MAX_SCANPOS: - colour = ula.get_pixel_colour() - if x < MAX_WIDTH and y < MAX_HEIGHT: - screen[pos] = colour[0]; pos += 1 - screen[pos] = colour[1]; pos += 1 - screen[pos] = colour[2]; pos += 1 - x += 1 - ula.hsync() - y += 1 + def __init__(self): + self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT)) + self.colour = BLANK + self.csync = 1 + self.hs = 1 + self.reset() - return screen + def reset(self): + self.pos = 0 + + def update(self): + if self.csync: + if self.hs: + 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 class ULA: - "The ULA functionality." + """ + A class providing the ULA functionality. Instances of this class refer to + the system memory, maintain internal state (such as information about the + current screen mode), and provide outputs (such as the current pixel + colour). + """ modes = [ (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) @@ -55,17 +74,26 @@ palette = range(0, 8) * 2 - def __init__(self, memory): + def __init__(self, memory, video): - "Initialise the ULA with the given 'memory'." + "Initialise the ULA with the given 'memory' and 'video'." self.memory = memory + self.video = video self.set_mode(6) # Internal state. self.buffer = [0] * 8 + self.reset() + + def reset(self): + + "Reset the ULA." + + self.vsync() + def set_mode(self, mode): """ @@ -80,12 +108,15 @@ * horizontal pixel scaling factor * line spacing in pixels * number of entries in the pixel buffer + + The ULA should be reset after a mode switch in order to cleanly display + a full screen. """ self.width, self.depth, rows = self.modes[mode] - self.columns = (self.width * self.depth) / 8 # bits read -> bytes read - row_size = self.columns * LINES_PER_ROW + columns = (self.width * self.depth) / 8 # bits read -> bytes read + row_size = columns * LINES_PER_ROW # Memory access configuration. # Note the limitation on positioning the screen start. @@ -115,19 +146,19 @@ self.line_start = self.address = self.screen_start self.line = self.line_start % LINES_PER_ROW self.ssub = 0 + self.y = 0 self.reset_horizontal() - def reset_horizontal(self): - - "Reset horizontal state." + # Signal the video circuit. - self.xsub = 0 - self.column = 0 - self.buffer_index = self.buffer_limit # need refill + self.csync = self.video.csync = 1 def hsync(self): - "Signal the end of a line." + "Signal the end of a scanline." + + self.y += 1 + self.reset_horizontal() # Support spacing between character rows. @@ -135,7 +166,6 @@ self.ssub -= 1 return - self.reset_horizontal() self.line += 1 # If not on a row boundary, move to the next line. @@ -163,36 +193,73 @@ self.line_start = self.address - def get_pixel_colour(self): + 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 update(self): """ - Return a pixel colour by reading from the pixel buffer. + Update the pixel colour by reading from the pixel buffer. """ + # Detect the end of the line. + + if self.x >= MAX_WIDTH: + if self.x == MAX_WIDTH: + self.hs = self.video.hs = 0 + + # Detect the end of the scanline. + + elif self.x == MAX_SCANPOS: + self.hsync() + + # Detect the end of the frame. + + if self.y == MAX_SCANLINE: + self.vsync() + + # Detect the end of the screen. + + elif self.y == MAX_HEIGHT: + self.csync = self.video.csync = 0 + # Detect spacing between character rows. if self.ssub: - return BLANK + self.video.colour = BLANK - # Scale pixels horizontally. + # Detect horizontal and vertical sync conditions. - if self.xsub == self.xscale: - self.xsub = 0 - self.buffer_index += 1 + elif not self.hs or not self.csync: + pass + + # For pixels within the frame, obtain and output the value. + + else: - if self.buffer_index == self.buffer_limit: - self.buffer_index = 0 - self.column += 1 + # Scale pixels horizontally, only accessing the next pixel value + # after the required number of scan positions. - # Detect the end of the scanline. + if self.x % self.xscale == 0: + self.buffer_index += 1 + + # Fill the buffer once all values have been read. - if self.column > self.columns: - return BLANK + if self.buffer_index >= self.buffer_limit: + self.buffer_index = 0 + self.fill_pixel_buffer() - self.fill_pixel_buffer() + self.video.colour = self.buffer[self.buffer_index] - self.xsub += 1 - return self.buffer[self.buffer_index] + self.x += 1 def fill_pixel_buffer(self): @@ -276,9 +343,15 @@ def get_ula(): - "Return a ULA initialised with a memory array." + "Return a ULA initialised with a memory array and video." + + return ULA(get_memory(), get_video()) - return ULA(get_memory()) +def get_video(): + + "Return a video circuit." + + return Video() def get_memory():