Having reached the point where the Acorn Electron core can now boot from ROM to the Basic prompt in mode 6. I had a choice between adding keyboard support or further graphics modes. I decided keyboard support should take priority as it will make testing different graphics/text modes simpler.
The way the Acorn Keyboard works is the CPU selects a pseudo keyboard ROM which occupies ROM slot 8 and 9. Once the keyboard “ROM” is selected the keyboard matrix can be queried via address lines 0..13 (active low).
One at a time the 14 address lines will be taken low by the CPU and the state of the four keys tied to that line will become available to the ULA on the KBD0..3 pins which the ULA will output to the data bus (if the keyboard ROM is selected)
For example, when the CPU outputs 0 on addr(6), the state of keys 7, U, J and M will be placed on the data bus lines 0..3
This is a quite simple scheme and there wouldn’t be much to implement if the Replay board had a real Electron keyboard hooked up, however, the Replay uses the PS2 keyboard format and that means a little more work.
... subtype t_key_row_state is word(3 downto 0); type t_key_state is array (13 downto 0) of t_key_row_state; signal key_state : t_key_state := (others => (others => '1')); signal key_release : bit1; signal key_extended : bit1; ...
A small translation component is needed to track the current PS2 based key states in a two dimensional “key_state” array. The 14x4 2D array simulates the layout of the Electron’s keyboard matrix.
The p_kbd_scan process handles translating a PS2 key press/release to the corresponding key_state matrix entry. Whilst the p_addr_scan process handles outputting the matrix row (four key states) any time an address line goes low.
p_kbd_scan : process(i_clk_sys, i_rst_sys, i_kb_inhibit) begin ... -- keys in scancode_ps2 are extended bit & scancode case key_extended & i_kb_ps2_data is when c_PS2_ESC => key_state(13)(0) <= key_release; when c_PS2_CAPS_LOCK => key_state(13)(1) <= key_release; when c_PS2_LEFT_CTRL | c_PS2_RIGHT_CTRL => key_state(13)(2) <= key_release; when c_PS2_LEFT_SHIFT | c_PS2_RIGHT_SHIFT => key_state(13)(3) <= key_release; when c_PS2_1 => key_state(12)(0) <= key_release; when c_PS2_Q => key_state(12)(1) <= key_release; when c_PS2_A => key_state(12)(2) <= key_release; when c_PS2_Z => key_state(12)(3) <= key_release; ... end; p_addr_scan : process(i_addr, key_state) variable result : t_key_row_state := (others => '1'); begin result := (others => '1'); -- Addr and key state are active low. for I in i_addr'range loop for J in result'range loop result(J) := result(J) and (i_addr(I) or key_state(I)(J)); end loop; end loop; o_data <= result; end process;
Due to the Replay Framework handling the low level details of the PS2 interface this turned out to be quite a straightforward implementation.
Most non-shifted keys on the Electron map to the same non-shifted key on a regular keyboard, however where that is not the case, I’ve favoured the Electron layout i.e the shifted and ctrl states will be the Electron symbol not the symbol shown on a regular keyboard.
Exceptions to this are:
- : and the shifted * are moved to the equals key “=” and “shift+=”
- COPY key is bound to “[”
For example, the “shift+8” on a normal keyboard outputs “*” whilst on the Electron you get a “(”. The “*” symbol on the Electron keyboard is shown instead on the “shift+:” key. However, as noted above, the core uses the “shift+=” key for a “*” due to the “:” key conflicting with the “;” key and its shifted key “+”.
As with the original electron, caps-lock is toggled via shift+capslock, whilst holding capslock will result in the function key, i.e capslock+e will output “ELSE”
With keyboard support working, I realised why the memory clear discussed in an earlier post skipped some regions of RAM on reset.
There’s a distinction made between POR1 occurring and the Reset/break key being pressed. The POR flag in the ULA register should only be set on initial power on and not during a BREAK key induced reset.
If that distinction is not made, the following is not possible:
> print Z% 0 > Z% = 4 > print Z% 4 (hit break/reset key) > print Z% 4
The above sets a variable to the value 4 then the Electron is reset by means of the BREAK key (not a full POR). The variable Z% retains the value 4 despite the reset.
CPU Clock Transition
Variable CPU clocking was the next task to tackle and this proved to be an “Interesting” problem, in a pull your hair out and bang your head against the desk, way. Note: this section may not make a whole lot of sense to anyone not familiar with the Electron’s hardware.
Up until now the Core assumed the CPU was always clocked @1MHz, this allowed the CPU and ULA to time-share RAM access. The CPU accessing RAM during clock phases 0..7 and the ULA in phases 8..15. However, as noted in previous posts, the ULA in some modes can take over both slots and STOP the CPU from running at all. In addition, when the CPU reads from ROM there is no contention with the ULA so the Electron’s CPU is allowed to run at 2MHz.
When the CPU is running at 1MHz and the ULA is in mode 4..6 this time sharing approach to RAM access works out perfectly.
When the CPU accesses ROM the 2MHz clocks occur on phase 0 and 8 rather than just phase 0 (as with 1MHz ticks). This leads to a problem. The CPU can attempt to access RAM on phase 8.
Phase 8 to 15 is the ULA’s RAM time, so the CPUs read/write request will be ignored, then on phase 0 when the CPU could get access, a 1MHz clock tick occurs, this might set up a new RAM access and oops the previous RAM access that has yet to be served is lost forever.
2MHz to 1MHz Transition
So how does the Electron handle this case? According to the Advanced User Guide and various Application Notes, by keeping the clock line high for 750ns or 1250ns depending on the 2MHz clock phase (ie whether the 2MHz tick that tries to access RAM is on phase 0 or 8).
That extra high time ensures the next 1MHz clock tick doesn’t occur, the same data/addr will be present on the bus for the following phase 0 through phase 7 ula clocks and the ULA will now perform the RAM read/write.
As the Electron core is using clock enables which have to be 1 sys clock pulse wide and cannot be kept high any longer, that can’t be done directly. However, the same can be accomplished by simply skipping the 1MHz enable directly following a transition.
As a consequence of this, the next 2MHz clock is also skipped as the address is unchanged and still signaling a RAM access, this will not change until the following 1MHz clock.
This appears to match the findings of others in that a 2MHz clock is lost during a 2MHz to 1MHz transition.
To do this, clocking has been moved over to a small state machine described by the image below.
In the 1Mhz or 2MHz states phi_out will be asserted anytime the ULA clock phase is 0000 or in the case of 2MHz, 0000 or 1000. If the cpu is clocked at 2MHz due to accessing ROM for example but then needs to access RAM which has to occur at 1MHz, there are two possibilities.
The 2MHz clock occurs on clock phase 0000 which is the same phase as 1MHz clocks occur. A state change to 1MHz can be made with no additional fuss.
The 2MHz clock occurs on clock phase 1000. If a transition to 1MHz occurred directly, the RAM access would be lost. Instead a transition is made to a delay state which will eat the next 1MHz clock rather than ticking (ram access will occur at this time and not be lost). It will transition back to the 1MHz state by also eating the next 2MHz clock.
The actual implementation deviated from this FSM slightly. Generation of clock pulses was split out from state changes to allow the generation to occur on cph(2) which ensures the enable is asserted edge aligned with cph(3).
p_clk_gen : process(i_clk_sys, rst) begin if (rst = '1') then vid_rst <= '1'; clk_phase <= "0000"; cpu_clk_state <= CLK_1MHz; elsif rising_edge(i_clk_sys) then phi_out <= '0'; -- Bring video out of reset to align hpix 0 with phase 0 if (i_cph_sys(0) = '1' and vid_rst = '1' and clk_phase = "1111") then vid_rst <= '0'; end if; if i_cph_sys(1) = '1' or i_cph_sys(3) = '1' then clk_phase <= clk_phase + 1; end if; -- CPU_STOPPED will be asserted after cph(2) has created a pulse, but before cph(3) has changed -- states. Likewise /CPU_STOPPED will occur on cph(3) allowing state change to occur without -- generating an extra clock pulse that would otherwise cause the delayed RAM access to be lost. -- NOTE: This relies on ram_contention changing only on phase 0000. if (cpu_target_clk /= CPU_STOPPED) then -- Clock gen on cph(2) to edge align with cph(3) if (i_cph_sys(2) = '1') then if (cpu_clk_state = CLK_1MHz and cpu_target_clk = CPU_1MHz and clk_phase = "0000") then -- 1MHz pulse phi_out <= '1'; elsif ( (cpu_clk_state = CLK_2MHz or cpu_target_clk = CPU_2MHz) and clk_phase(2 downto 0) = "000") then -- 2MHz pulse or transition to 2MHz pulsing phi_out <= '1'; end if; end if; -- State transitions checked on cph(3) if (i_cph_sys(3) = '1') then case cpu_clk_state is when CLK_1MHz => if (cpu_target_clk = CPU_2MHz) then -- 2MHz transition safe to occur at any time cpu_clk_state <= CLK_2MHz; end if; when CLK_2MHz => if (cpu_target_clk = CPU_1MHz) and (clk_phase(3) = '1') then -- ram access attempt during 2MHz only phase, transition to 1MHz cpu_clk_state <= CLK_TRANSITION; elsif (cpu_target_clk = CPU_1MHz) and (clk_phase(3) = '0') then -- ram access during 1MHz aligned phase, no transition required cpu_clk_state <= CLK_1MHz; end if; when CLK_TRANSITION => -- 2MHz -> 1MHz -- Transition on 1000 to ensure a full 1MHz RAM cycle has -- occured whilst waiting in this state if (clk_phase = "1000") then cpu_clk_state <= CLK_1MHz; end if; end case; end if; end if; end if; end process;
The target clock speed is determined as follows:
-- CPU clocking based on access type cpu_target_clk <= CPU_1MHz when nmi = '1' else CPU_1MHz when i_addr(15 downto 9) = "1111110" else -- ROM Fred/Jim CPU_2MHz when i_addr(15) = '1' else -- Any other ROM access CPU_STOPPED when misc_control(MISC_DISPLAY_MODE'LEFT) = '0' and display_period else -- RAM access mode 0..3 CPU_1MHz; -- Ram access
This results in the following simulation traces for a RAM write.
The yellow line marks the 2MHz CPU clock on phase 8 when the CPU tries to write to RAM. The clock must be transitioned to 1MHz so clk_state moves to CLK_TRANSITION where it remains from phase 8 through to the following phase 8. This ensures the 1MHz clock on phase 0 does not occur and a full RAM write cycle can occur unhindered during phase 0 to 7.
By the following phase 0, the clock is now in the CLK_1MHz state and clocking at 1MHz.
This appears to work great, although it has to be said my first implementation was quite a spectacular failure.
One yet to be tested part of the FSM is what happens in video modes 0 to 3 when the ULA needs to take over the CPU’s RAM access slot to keep up with video generation. It does this by stopping the CPU clock during active video rendering. The FSM will in turn need to stop any transitions during this time.
If contention can be asserted on one clock phase and go low on a different phase, with the current FSM implementation, there’s potential for a CPU RAM access to be lost.
For this reason, the contention flag will only be set or cleared on clk_phase 0000.
I’m quite curious as to how the real ULA in the Electron implemented the contention and 2MHz to 1MHz transition logic. Hopefully if/when reverse engineering of the ULA is done, I’ll be able to answer that and perhaps improve my implementation.
POR - Power On Reset ↩︎