Part 3 - Implementing the 6545 CRTC Chip and Cursor
This article continues on from Part 2 - Video Controller.
In the last article I managed to get FPGABee to show the Microworld Basic boot screen. I didn't mention though that it wasn't quite right. Although the welcome message was appearing the command prompt '>' wasn't and I didn't know why.
Debugging an Infinite Loop
My best guess was that the CPU was stuck in a loop somewhere with the Basic ROM waiting for something from the hardware that wasn't yet implemented.
To track this down I brought in a previous experimental circuit for driving the Nexys3's 4-Digit 7-Segment LED display and hooked up one of the on-board buttons to have it show the current address the Z80 was executing at. The Z80 control line
M1 goes active when the CPU is in the first phase of executing an instruction. By watching for a memory request at the same time, I could pick up the current instruction address and display it.
I then disassembled the BASIC ROM and took a look at what it was doing and sure enough it was try to read from the keyboard, waiting for a response that just never going to come. Conveniently, this directly related to what I had to work on next anyway... the cursor.
The 6545 CRTC Chip
In the previous article I also mentioned the 6545 CRTC chip which forms the final part of the Microbee's video controller. On a real Microbee it's responsible for doing much of the work already already done by FPGABee's VGA controller including generating video RAM address lines and the video vertical and horizontal sync signals.
The 6545 however also implements a number of registers that control things like the cursor position and shape, character size metrics and screen dimensions. In FPGABee the character size and screen dimensions are currently hard wired and the few programs that manipulate these settings won't work correctly.
But finally and more interestingly, the 6545 chip also has support for a light pen - but the Microbee doesn't use it for that, it uses it for the keyboard - which we'll come to later.
The interface to a 6545 chip is a bit weird - it has:
- Approximately 16 internal 8-bit registers
- A read-only status register
- A bi-directional 8-bit data bus
- A read/write control signal
- A register/address select signal
- and no address lines.
and you use it like this:
- To set a register you write the register number to the data bus with the address signal active and then you write the register value with the address signal in-active.
- To read a register you write the register number to the data bus with the address signal active and then you read the register value with the address signal in-active.
- To read the status register you read with the address signal in-active.
This is all wired up to the Z80 on two I/O ports so that
- Writing to port 0x0C writes a register number
- Reading or writing port 0x0D reads or writes the register selected on port 0x0C
- Reading from port 0x0C reads the status register
Implementing FPGABee's 6545
So far everything I'd done on FPGABee was pretty much just wiring existing components together. The 6545 was the first piece that I was going to have actually code some functionality for. To start with I created a new VHDL component:
entity Crtc6545 is port ( clock: in STD_LOGIC; reset: in STD_LOGIC; wr: in STD_LOGIC; rs: in STD_LOGIC; din: in STD_LOGIC_VECTOR(7 downto 0); dout: out STD_LOGIC_VECTOR(7 downto 0); ); end Crtc6545;
connected it the appropriate CPU's ports:
crtc_data_port <= '1' when (z80_addr(7 downto 0) = x"0d") else '0'; crtc_addr_port <= '1' when (z80_addr(7 downto 0) = x"0c") else '0'; crtc_wr <= '1' when (port_wr='1' and (crtc_addr_port='1' or crtc_data_port='1')) else '0'; crtc_rs <= '1' when (crtc_data_port='1') else '0'; z80_din <= -- (others omitted) crtc_dout when (port_rd='1' and (crtc_addr_port='1' or crtc_data_port='1')) else Crtc6545: entity work.Crtc6545 PORT MAP ( clock => z80_clock, reset => reset, wr => crtc_wr, rs => crtc_rs, din => z80_dout, dout => crtc_dout, );
and implemented the minimal set of registers:
process(clock, reset) begin if (reset='1') then -- reset all registers reg_addr <= (others=>'0'); reg_cursor_mode <= (others=>'0'); reg_cursor_startline <= (others=>'0'); reg_cursor_endline <= (others=>'0'); reg_cursor_pos <= (others=>'0'); elsif (clock'event and clock='1') then if (wr = '1') then if (rs = '1') then -- write to register case (reg_addr) is when "01010" => -- cursor start scan line R10 reg_cursor_startline <= din(4 downto 0); reg_cursor_mode <= din(6 downto 5); when "01011" => -- cursor end scan line R11 reg_cursor_endline <= din(4 downto 0); when "01110" => -- cursor pos h R14 reg_cursor_pos(13 downto 8) <= din(5 downto 0); when "01111" => -- cursor pos l R15 reg_cursor_pos(7 downto 0) <= din(7 downto 0); when others => null; end case; else -- write to address register reg_addr <= din(4 downto 0); end if; else if (rs = '1') then -- read register case (reg_addr) is when "01110" => -- cursor pos H R14 dout <= "00" & reg_cursor_pos(13 downto 8); when "01111" => -- cursor pos L R15 dout <= reg_cursor_pos(7 downto 0); when "10000" => -- light pen H R16 dout <= "00" & reg_light_pen(13 downto 8); reg_status_light_pen_ready <= '0'; when "10001" => -- light pen L R17 dout <= reg_light_pen(7 downto 0); reg_status_light_pen_ready <= '0'; when others => dout <= (others => '0'); end case; else -- read status register dout <= "10000000"; end if; end if; end if; end process;
See that last bit about reading the status register? That's what the Basic ROM was stuck waiting for so I just hard-wired it to '1' for now and voilà... the command prompt now appeared as expected.
I now had everything I needed to implement the hardware cursor, except it was all in the wrong spot: I needed access to the cursor registers in the video controller code, not here in the 6545 implementation. I decided to moved all the video controller code into the 6545 component.
The next thing I needed was some sort of signal to handle the cursor blink. The 6545 has 4 blink modes that are controlled by bits 5 and 6 in register 10. The modes are always on, always off, blink slow and blink fast. For this I use a 22-bit counter that is incremented on each z80 clock tick. I then use the highest bit for the slow blink and the second highest bit for the fast blink:
-- Workout if cursor on/off/blinking cursor_on <= '1' when (reg_cursor_mode="00") else -- cursor on '0' when (reg_cursor_mode="01") else -- cursor off reg_blink_counter(21) when (reg_cursor_mode="10") else -- slow blink reg_blink_counter(20); -- fast blink
Now for the actual pixel twiddling to make the cursor appear. First work out if the current pixel is "inside" the cursor. This simply compares the cursor position to the address in video RAM video ram address being draw by the video code and compares the current scan line to the scan line range in the cursor registers:
-- Work out if cursor is in the current character and cursor scanline range cursor_pixel <= '1' when ( (cursor_on = '1') and ("0" & char_pixel_y>=reg_cursor_startline) and ("0" & char_pixel_y<=reg_cursor_endline) and (vram_addr= reg_cursor_pos(10 downto 0)) ) else '0';
And finally, invert the current output pixel if the cursor covers it:
-- Generate the output pixel pixel <= '0' when (video_blank = '1' or pixel_in_range = '0') else not current_pixel when (cursor_pixel = '1') else current_pixel;
An Exact Microbee Boot Screen.
I now had an exact clone of the Microbee boot screen - the welcome message, the command prompt and a little flashing cursor. By now, I'm coming to realise that all I needed to really bring this thing to life is a working keyboard.