# HG changeset patch # User Paul Boddie # Date 1324339288 -3600 # Node ID da771131998bd703e09ab64c2d18c8757bd510dc # Parent 1d0cbf9dd9032affe2b8bf904a9fe969374b4f5f# Parent 579ebc9db48b41705f59e547a8fa081dcd124430 Merged general architecture-related changes. diff -r 1d0cbf9dd903 -r da771131998b ULA.txt --- a/ULA.txt Sun Dec 18 19:31:33 2011 +0100 +++ b/ULA.txt Tue Dec 20 01:01:28 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 1d0cbf9dd903 -r da771131998b main.py --- a/main.py Sun Dec 18 19:31:33 2011 +0100 +++ b/main.py Tue Dec 20 01:01:28 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 1d0cbf9dd903 -r da771131998b ula.py --- a/ula.py Sun Dec 18 19:31:33 2011 +0100 +++ b/ula.py Tue Dec 20 01:01:28 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.tolist() + +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.tolist() + 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, 0, 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 = ULA.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():