Routing the MAXI000 board has been a slog. A mostly enjoyable slog, but a slog all the same.
Before actually getting to the PCB design, the first task was to associate footprints with each schematic symbol.
I always dread this task; you are presented with an intimidating list of symbols from the schematic and must pick the correct footprint for the component using some pretty basic search functions. It was a particularly big chore for the MAXI000 because it is my first proper SMT board. And due to the sheer number of different size parts that are available, choosing the right one is tricky.
With through-hole resistors, for example, there are really just two choices: the resistor can be mounted flat against the board or vertically. Whilst there are larger resistors available, for use in power supplies and other high current applications, they are infrequently used. Likewise through-hole ICs are generally DIP and a 16 pin IC has really only one footprint choice.
SMT footprints are not nearly so simple.
Firstly, there are many resistor sizes in use, from 1206 (which is what MAXI000 uses) to the relatively tiny 0403 and even smaller. This is not really a problem as it is clear what size a part is since it can be measured if documentation isn’t available.
ICs are a different matter. The 74HC245 (PDF) octal bus transceiver from Texas Instruments is available in 4 different “DIP style” SMT package types (this does not include other variant factors, like supported temperature range). This includes two width variants with 50mil spaced pins, which is the spacing the MAXI000 generally uses so as to make things manageable and consistent.
Ordering the right size parts is tricky. I have so far ordered the wrong size parts three times. Hopefully I will learn from my mistakes.
As usual this is a 4 layer board. I’m quite sure it would be possible to route the power connections on this kind of board with only two layers, but the skill level required is too great for me, at least at the moment.
After associating the correct footprint to each symbol, the next task was to lay out sections of related parts, for example the connections specific to the SC16C654 (PDF) quad UART.
The general idea when laying out closely related parts was to keep the traces on the component side of the board, with the back side used only when essential to do so. At the same time, all “DIP style” ICs are orientated left to right, with the component side favoring traces perpendicular to the rows of IC pins, ie. traces on the front side should tend to run up and down the board
One particularly interesting area of the board is the video memory IC buses and how they join to the Beta FPGA. I used manual back annotations to map the address and data buses (and control signals) to convenient pins on the FPGA, as this picture shows:
Pretty much the entire set of traces between the FPGA and the memory ICs is on the component side, which would have been impossible to achieve without remapping the FPGA connections to match up with the memory ICs leads.
Also, to save some board space, the passive parts, including decoupling capacitors and the resistors that make up the SVGA DAC, are on the back side of the board.
At this stage the “islands” of parts remained isolated. After creating these islands, and it’s what dictated the size of the board, the task of figuring out how to place the connectors. The board has a quite lot of them:
- Barrel Jack for power
- DB15 SVGA
- 3.5mm stereo jack
- DB9 Atari joystick
- DB15 PC Game port joystick
- Dual RJ45 RS-232
- RJ10 keyboard connector
- PS/2 connector
Although these connectors do not have to be at the edge of the board.
I like to have the bottom edge of the board, which is nearest the keyboard, clear of external connectors so placed the SIMM slot there. The expansion connector was placed at the right edge, allowing a hypothetical daughter board to rest over most of the board. The remaining connectors were placed along the top and left edges.
After this, the mammoth job of trying to link everything together.
My initial attempt at routing the board made heavy use of KiCAD‘s push routing feature. This is a fantastic feature, but I eventually found that it was not very useful when routing parallel buses and ended up doing most of the routing with a 20mil grid. But because of the “tighter” areas, especially around the PLCC flash sockets, 6mil traces were used with some areas making use of 6mil clearances, which is the tightest clearances allpcb.com provides with its standard service.
The board needs a final check, but here’s a picture of the board, without the internal layers, as it currently stands:
I am pretty pleased with the board. There’s obviously many, many vias. I’m sure a professional PCB router would be appaled, but I’ve learned a lot through this proces. SMT boards are a different animal to through-hole boards, and not only because of the more finely pitched parts: the sides of the board are no longer equal. The technique of putting traces in difference orientations on different sides works, but only up to a point. The routing has to favor the front side, since that is where the componets are.
The obligatory 3D render:
For completeness, here’s a render of the back side of the board:
After satisfying myself that the board was basically complete I thought it would be sensible to verify that the footprints chosen matched the sizes of the parts I had ordered. I did this using the low tech approach of printing out the design on paper and placing some of the key parts on it:
At the point I took the above picture the layout had one footprint problem: a resistor array, of which the board has 6 parts, was the wrong size. This was quickly solved by switching footprint in KiCAD and reworking the connections, and actually the board is better for it as the correct footprint is a good deal larger, meaning the traces aren’t so bunched together.
There’s a few differences in the schematic used to layout the board, relative to the schematic published in the previous post. As I worked on the PCB layout, I tweaked the schematic in small ways. Hopefully by the time of the next post I’ll have collated the schematics togehter, with the PCB design files, for anyone interested.
The first change is the aforementioned re-pinning of the video memory connections to the Beta FPGA.
There are some other minor tweaks to the FPGA pins, including routing the /VPA 68000 pin to Beta instead of Alpha, as it’s main use is for indicating an autovectored interrupt, and Beta is the device responsible for signalling the processor of an interrupt. Also, another connection has been made from Beta to Alpha so it can indicate that Alpha must assert /DTACK. This will be needed, for example, if an SPI access is requested but the SPI bus is busy, at that moment, with a PCM sound SPI write to the MCP4902 (PDF), which may occur asynchronously to the 68000 if Beta is writing values to it from its attached memory. There may be other unforeseen reasons why the 68000 should be stalled via accesses initiated to Beta, and to not account for this does not seem wise. This means there are now three links between Alpha and Beta FPGAs: a Chip Select to Beta, an interrupt request to Beta, and a /DTACK request to Alpha. Of course these are only the anticipated uses of those connections.
To better balance the unused FPGA pins, which are exposed at the expansion connector, the user-addressable LED and buzzer are now attached to Alpha instead of Beta. Three pins on each FPGA are then available to a potential expansion daughter board.
I have removed the write protect jumper for the two 512K x 8 flashes, SST39SF040 (PDF), since they are not needed due to inadvertent writes being essentially impossible due to the way that write requests are setup in the flash.
The board includes a few test points for attaching the logic analyzer. These might be useful for the bring up to diagnose any errors etc. The expansion connector is usually sufficient for this purpose but a way to view more “burred” signals could be useful, especially around the SIMM slot.
Because of wanting to free up some pins on the Beta FPGA, the 4 bit RGB DAC has been degraded to a 3 bit one. This will limit the number of available colours to 512 instead of 4096 as I’d hoped. For the same reason the SPI PCM DACs no longer have dedicated pins on Beta. This is slightly irritating but in practice it shouldn’t really matter as the three remaining SPI ICs; the real-time clock and the two analogue joystick ADCs, should not need to be accessed any more then once per video frame. It does mean implementing some kind of arbitration mechanism for the SPI bus though, as well as the aforementioned possibility that the 68000 will need to be held via /DTACK, as sound playback will be contended with the MPUs own SPI requests.
The reason for wanting to free up some pins on Beta is to add a PS/2 port to the system.
And the reason for wanting a PS/2 port is that it would be fantastic to add a mouse port to the system. It will allow someone else building their own MAXI000 board to operate the computer without building the Amiga 600 keyboard or, perhaps, building their own translator board to convert some other keyboard to the asynchronous serial protocol used by my Amiga 600 keyboard control board.
PS/2 ports use a synchronous (clocked) serial protocol that operates on units of bytes. Unlike SPI the protocol it is bi-directional on a common data wire, so it is a little like I2C except without any addressing, which would be redundant anyway since there is only one host and one device. Two wires and two pins are used: clock and data. The device (a mouse or a keyboard) drives the clock signal for both directions.
For keyboards, key events obviously originate at the keyboard end. Communications from the host end is used to do various things including:
- Reset the keyboard
- Turn LEDs (Caps Lock, Num Lock and Scroll Lock) on and off
- Set key repeat rates
Without communication from the host, the keyboard will still generate scancodes after powering on. As an aside, it was interesting to see that key up events use an escape prefix (0xf0) instead of setting the top bit, like mine and other keyboards. Obviously there must be keyboards around with more the 128 keys.
Mice are similar, but the message formatting is more involved. A 3 byte sequence is used to send the state of the buttons and X and Y offsets. And just getting the mouse to start sending movement information requires sending a command byte. Therefore my PS/2 implementation must include communication from the host to the device.
Whilst there are ready to integrate ICs that implement a PS/2 host controller, including the VT82C42 (PDF), after reading about the protocol I thought it looked like an interesting challenge to implement my own controller. One of the things which makes an it interesting challenge is that the protocol uses one wire for two way communications, something I have not attempted before.
The hardware for the prototype setup is extremely simple:
This is the same FPGA dev-board add-on board I used when bringing up my SVGA implementation; the little board includes a 3 bit RGB DAC with attached DB15 connector, along with a 6 pin mini-DIN as used by PS/2. With the multimeter I found out that the clock and data pins both have 2.2K pull-ups, which are needed because the communication channel is bi-directional. The clock and data pins were attached to free pins on the MINI000 EPM7128S (PDF) CPLD. I managed to squeeze in two logic analyzer probes for producing traces of the clock and data signals as well.
The best description of the PS/2 protocol I found is probably this one. The most useful part of this description is perhaps a diagram showing how host to device communications operate:
Whilst I did find some existing VHDL implementations, they were generally too complex and did too much. I did use one for ideas though, and borrowed the technique of sampling an external clock by shifting it into a shift register clocked by the system clock.
Before even writing a line of VHDL I I attached a USB keyboard to my logic analyser and benchtop power supply. Older keyboards (and mice), support the PS/2 wire protocol via a simple adapter – the purple device in the above picture. The data and clock pins were directly attached to the logic analyzer to capture some waveforms. This also confirmed that this particular USB keyboard supported the PS/2 protocol.
Here’s a capture obtained after typing a simple message:
I was surprised to see that this software had analysers for PS/2 ports. It even decodes the scancode into a key cap, though I think it assumes a US keyboard is always used! Interestingly the data rate, set by the keyboard, is quite low: only 12KHz.
The next job was to figure out how to read the 8 bit quantities into a register in the CPLD, which would be presented to the MPU.
First up, the mechanism to detect falling edges on the PS/2 clock pin. This is used for both transmission and reception of PS/2 data bytes. The entity (interface) is completely trivial:
entity ps2edgefinder is port ( MPU_CLOCK : in STD_LOGIC; EDGE_FOUND : out STD_LOGIC; -- '1' when a falling edge is found CLOCK : in STD_LOGIC); -- The input clock to look at end entity;
The implementation is pretty simple as well:
architecture behavioral of ps2edgefinder is signal EDGE_FINDER : STD_LOGIC_VECTOR (3 downto 0) := x"0"; begin process (MPU_CLOCK) begin if (MPU_CLOCK'Event and MPU_CLOCK = '1') then -- Shift the incoming clock signal in EDGE_FINDER <= EDGE_FINDER (2 downto 0) & CLOCK; -- Check for a match against a falling edge if (EDGE_FINDER = "1100") then EDGE_FOUND <= '1'; else EDGE_FOUND <= '0'; end if; end if; end process; end architecture;
PS/2 clock samples are taken on each rising edge of the MPU clock and fed into the right hand end of a 4 bit shift register, which shifts left. If the shift register contains “1100” then a falling edge has been found.
Next, the interface for the receive section:
entity ps2rxshifter is port ( MPU_CLOCK : in STD_LOGIC; EDGE_FOUND : in STD_LOGIC; -- Has a clock edge been found? RX_SCANCODE : out STD_LOGIC_VECTOR (7 downto 0);-- What have we shifted in SCANCODE_READY_SET : out STD_LOGIC; -- A new scancode is available PARITY_ERROR : out STD_LOGIC; -- A parity error occured CLOCK : in STD_LOGIC; -- PS2 clock pin DATA : in STD_LOGIC); -- PS2 data pin end entity;
This is pretty straight-forward. EDGE_FOUND is determined using the above. SCANCODE_READY_SET is an enable signal for another signal which is set by the act of receiving a byte (scancode) on the PS/2 port, and cleared when the MPU reads the scancode data register and obtains the actual scancode.
PS/2 has a simple parity check (the protocol uses odd parity) and the PARITY_ERROR output is used to indicate a parity error, indicating that the host end should disregard this byte.
CLOCK and DATA are the signals from the PS/2 port itself.
architecture behavioral of ps2rxshifter is signal BYTE_BUFFER : STD_LOGIC_VECTOR (7 downto 0) := x"00";-- Currently shifting byte signal BYTE_COUNTER : integer range 0 to 7; -- Byte shifting counter (0..7) signal PARITY_CHECK : STD_LOGIC := '0'; -- Accumulated parity -- Used to rsset the shifter if no new bit by the time it overflows signal SCANCODE_RX_COUNTER : STD_LOGIC_VECTOR (11 downto 0) := (others => '0'); type T_STATE is (RX_START, RX_BYTE, RX_ODD_PARITY, RX_STOP);-- State stuff signal STATE : T_STATE := RX_START;
Next, the local signals.
SCANCODE_RX_COUNTER is possibly the most interesting element. It is incremented by the MPU_CLOCK signal and if it ever reaches it’s maximum value, the state machine is reset to the RX_START state. It is cleared at the start of each received bit, so this counter is used as a “timeout” to keep the system in-sync with the transmitting side. This is a 12 bit counter, so at 16 MHz the keyboard (or mouse) has approximately 256uS to generate each clock pulse. Looking at the logic analyser capture above, you can see that the period of that particular keyboard’s clock pulse is 81.6uS, ie. less then the 256uS cut-off.
process (MPU_CLOCK) begin if (MPU_CLOCK'Event and MPU_CLOCK = '1') then SCANCODE_READY_SET <= '0'; -- If we have had no clock edges for 2^12 MPU clocks, reset the state if (SCANCODE_RX_COUNTER = x"fff") then STATE <= RX_START; end if; -- Assuming we are not in the start state, move the "timeout" counter along if (STATE /= RX_START) then SCANCODE_RX_COUNTER <= SCANCODE_RX_COUNTER + '1'; end if; if (EDGE_FOUND = '1') then SCANCODE_RX_COUNTER <= (others => '0'); case STATE is when RX_START => BYTE_COUNTER <= 0; BYTE_BUFFER <= x"00"; PARITY_CHECK <= '0'; STATE <= RX_BYTE; when RX_BYTE => -- Update the parity state with the latest PS/2 bit PARITY_CHECK <= PARITY_CHECK xor DATA; -- Store it in the output buffer BYTE_BUFFER (BYTE_COUNTER) <= DATA; -- See if we just stored the last bit? if (BYTE_COUNTER = 7) then STATE <= RX_ODD_PARITY; end if; BYTE_COUNTER <= BYTE_COUNTER + 1; when RX_ODD_PARITY => -- Check for an even number of ones: good! if (PARITY_CHECK = not DATA) then -- No error PARITY_ERROR <= '0'; else PARITY_ERROR <= '1'; end if; STATE <= RX_STOP; when RX_STOP => -- Copy byte/scancode out and mark that we have a scancode RX_SCANCODE <= BYTE_BUFFER; SCANCODE_READY_SET <= '1'; STATE <= RX_START; end case; end if; end if; end process; end architecture;
Finally the “meat” of the receive action.
On each MPU clock, we assume that no complete scancode (byte) has been received. We then check for SCANCODE_RX_COUNTER overflow, and if it has happened then we reset the state and will later discard anything so far received. If we are not in the initial state (RX_START) then we increment this counter.
The rest of the logic deals with each PS/2 clock’s falling edge. Each edge is the reception of a bit:
- Start bit
- 8 data bits
- Parity bit
- Stop bit
The parity bit is possibly the most interesting aspect. The parity bit is calculated by accumulating XOR operations through the received data bit and what has been so far calculated. Since the XOR binary operation flips the output when the two inputs differ, this can be used for parity calculation, essentially counting the number of bits that are set, but keeping only the lowest bit of the sum.
Once a stop bit has been received, the received scancode (byte) is copied to the output and SCANCODE_READY_SET is set to 1, which is used to signal the processor, via a second addressable register, that a scancode is available for reading.
Needless to say, PS/2 transmission is a little more complicated. First up, the interface:
entity ps2txshifter is port ( MPU_CLOCK : in STD_LOGIC; nRESET : in STD_LOGIC; -- Global reset EDGE_FOUND : in STD_LOGIC; -- PS/2 falling clock edge? TX_COMMAND_BYTE : in STD_LOGIC_VECTOR (7 downto 0); -- What we are shifting out TX_COMMAND_TRIGGER : in STD_LOGIC; -- Go signal TX_CLOCK_DRIVEN : out STD_LOGIC; -- Drive the clock pin? TX_DATA_DRIVEN : out STD_LOGIC; -- Drive the data pin? CLOCK : out STD_LOGIC; -- PS/2 clock OUT DATA : out STD_LOGIC); -- PS2 data pin OUT end entity;
Unlike the transmission side, here the main nRESET signal is used to clear the state. Actually this kind of tidy up is missing at various places in the code, but it happens to be needed here.
TX_CLOCK_DRIVEN and TX_COMMAND_DRIVEN are used by the “outside world” to determine whether, at this point in time, the two pins are to be driven to a value or used as inputs. This is needed as the host drives the PS/2 clock pin low briefly to indicate to the remote end that it wants to send a byte. Through the EDGE_FOUND signal, the transmission of PS/2 data from host end both reads and drives the clock pin.
architecture behavioral of ps2txshifter is signal MPU_CLOCK_COUNTER : STD_LOGIC_VECTOR (9 downto 0) := (others => '0'); signal BYTE_COUNTER : integer range 0 to 7 := 0;-- Byte shifting counter (0..7) signal PARITY_CHECK : STD_LOGIC := '0'; -- Accumulated parity signal REQUEST_TO_SEND1 : STD_LOGIC := '0'; -- Sending clock pulse? signal REQUEST_TO_SEND2 : STD_LOGIC := '0'; -- Sending data pulse? signal TX_BUSY : STD_LOGIC := '0'; -- Transmittion is busy type T_STATE is (TX_BYTE, TX_ODD_PARITY, TX_STOP, TX_END); signal STATE : T_STATE := TX_BYTE;
Since the initial request to send a byte uses an unclocked (by the PS/2 device) pulse, a counter, clocked by the MPU clock is used to time its duration. This is the MPU_CLOCK_COUNTER signal, and it is used twice, once for a low PS/2 clock pulse, and once for a low data pulse, which acts as a start bit, as can be seen in the timing diagram above. REQUEST_TO_SEND1 and 2 are used to control this action, with a simple state machine, moved through its states via the EDGE_FOUND input, used to handle the rest of the processing.
begin process (MPU_CLOCK, nRESET) begin if (nRESET = '0') then TX_CLOCK_DRIVEN <= '0'; TX_DATA_DRIVEN <= '0'; elsif (MPU_CLOCK'Event and MPU_CLOCK = '1') then if (TX_COMMAND_TRIGGER = '1') then -- Pull clock low to tell the device we want to tx MPU_CLOCK_COUNTER <= (others => '0'); PARIT Y_CHECK <= '0'; REQUEST_TO_SEND1 <= '1'; TX_CLOCK_DRIVEN <= '1'; CLOCK <= '0'; end if; -- Send a "reqest to send" pulse on data if (REQUEST_TO_SEND1 = '1') then if (MPU_CLOCK_COUNTER = "1111111111") then REQUEST_TO_SEND1 <= '0'; TX_DATA_DRIVEN <= '1'; DATA <= '0'; MPU_CLOCK_COUNTER <= (others => '0'); REQUEST_TO_SEND2 <= '1'; end if; MPU_CLOCK_COUNTER <= MPU_CLOCK_COUNTER + '1'; end if; -- Send start bit, but at this point the device is not driving the clock if (REQUEST_TO_SEND2 = '1') then if (MPU_CLOCK_COUNTER = "1111111111") then REQUEST_TO_SEND2 <= '0'; TX_CLOCK_DRIVEN <= '0'; TX_BUSY <= '1'; BYTE_COUNTER <= 0; STATE <= TX_BYTE; end if; MPU_CLOCK_COUNTER <= MPU_CLOCK_COUNTER + '1'; end if; if (TX_BUSY = '1' and EDGE_FOUND = '1') then case STATE is when TX_BYTE => -- Calculate the parity and send the bit PARITY_CHECK <= PARITY_CHECK xor TX_COMMAND_BYTE (BYTE_COUNTER); DATA <= TX_COMMAND_BYTE (BYTE_COUNTER); if (BYTE_COUNTER = 7) then STATE <= TX_ODD_PARITY; end if; BYTE_COUNTER <= BYTE_COUNTER + 1; when TX_ODD_PARITY => DATA <= not PARITY_CHECK; STATE <= TX_STOP; when TX_STOP => DATA <= '1'; STATE <= TX_END; when TX_END => TX_BUSY <= '0'; TX_DATA_DRIVEN <= '0'; end case; end if; end if; end process; end architecture;
I’m pretty sure this logic could be simplfied. It’s especially frustrating having to time the REQUEST_TO_SEND2 block, instead of moving into a TX_START state clocked by the remote device. However, it seems that at this point the device has not yet determined that it needs to send clock pulses.
After sending the “request to send” sequence, a state machine is entered which is responsible for shifting out the data bytes, parity, and stop bit, as clocked by the EDGE_FOUND signal.
Once again the XOR function is used to build up the parity bit, this time to append it to the data sent.
Another reason to look at simplifiying this code is to save resources in the CPLD/FPGA. The complete implementation (including the MPU register read and write actions) needs around 80 macrocells in the EPM7128S (PDF) CPLD present on the MINI000 board, which is more then half of the total available. In fact the current design entirly fills the CPLD, and I had to remove the logic for the onboard buzzer. I have yet to try building the implemenation into the barely-started design for the Beta FPGA; hopefully the Flex 10K (PDF) FPGA cells are more efficent then the MAX7000 CPLD cells.
One other task was to tie the PS/2 signals together at the CPLDs pins:
USER (0) <= PS2_TX_CLOCK when (PS2_TX_CLOCK_DRIVEN = '1') else 'Z'; USER (1) <= PS2_TX_DATA when (PS2_TX_DATA_DRIVEN = '1') else 'Z';
Thus USER (0 and 1) is put into tri-state (and used for reading) when the _DRIVEN signals are low, otherwise the requisit signal is put on as an output.
The summary of all this is the PS/2 controller works rather well. I am able to read scancodes using the monitor program, polling for a new scancode on the SCANCODE_READY status bit. I am also able to send commands to a keyboard, and mouse and read from both. Here is a capture of the 68000 being used to turn on the keyboard LEDs, using a PS/2 controller implemented inside MINI000’s CPLD:
From the left:
- The host (CPLD) is pulling the clock low.
- The host then pulls the data pin low, completeing the “request to send” sequence
- The host pushes out data bits, under control of the clock signal originating at the keyboard
- After 8 bits are sent, the calculated parity bit is sent
- The stop bit is then sent (marked by the analyser’s decoder with a red dot)
- The keyboard stops sending clock pulses
- The keyboard then sends an acknowledgement byte (0xfa)
- The same sequence occurs for the second transmitted byte
In the example above the host is sending 0xed followed by 0x07 to turn on all 3 LEDs.
The implementation is not entirly complete. One thing missing, but it’s easy to add, is exposure to the TX_BUSY bit within the published status register. The 68000 code which sends two bytes, used to send the LED sequence above, is timed with a delay loop instead of polling on a “byte sending” status bit.
The MAXI000 board will include two PS/2 interfaces, making use of four pins on the Beta FPGA, but on a single port. A fairly common configuration on contemporary laptops was to include only one PS/2 port with a splitter cable. If no splitter is used, the laptop would assume that a keyboard was used. With a splitter cable, both a keyboard and mouse could be connected. Of course this is just a software convention as the PS/2 protocol itself is agnostic to the type of device on a particular port.
Thus my plan for my MAXI000 computer will be to attach my trusty Amiga 600 keyboard to the RJ10 port which is hooked up to the 16C654 (PDF) Quad UART, with a PS/2 mouse attached to the PS/2 port. But if anyone else wants to build the board they may instead choose to use a PS/2 keyboard and PS/2 mouse.
I now have two outstanding jobs:
- Check, check and check some more the MAXI000 schematic and PCB design
- I also have to order the outstanding parts (including those FPGAs) from utsource.net.
I’ve been pretty organised with this board; I have a nice project box containing most of the parts I need all ready to go. But on the subject of parts storage, one problem I do currently have is storing SMT parts, as the familular methods – parts drawers, anti-static foam, etc – do not really work. Some research will be required…