Jump to content
IGNORED

VHDL for 99xx chips


pnr

Recommended Posts

The other day I came across one of Grant Searle's projects that I had not noticed before: his "multicomp" project. It is a simple, low cost FPGA project, where he has more or less created a "software breadboard" for prototyping various retro computers. Googling for it shows various successful builds, including those with ready made PCB's, such as:

https://www.retrobrewcomputers.org/forum/index.php?t=msg&th=111&start=0&

(but there are several more such efforts).

 

Maybe it is a nice idea to extend this idea to the 99xx world and create a few components to make it easy to prototype various small 99xx systems. The main thing missing in Grant's setup is the CRU bus and VHDL for CRU based chips. Probably the 9900 CPU core that 'speccery' did for last year's retro challenge can be reworked into a component for such a project without too much effort. Other chips to be added would be the 9902 and the 9901.

 

As I have a need for a 9902 in VHDL for another project, I thought I might as well attempt to write one. Looking at he datasheet (figure 2 on page 3) it is not all that complex. There seem to be:

- 6 plain registers

- 2 shift registers

- 3 counters

- 3 controllers / FSM's

The logic for the controllers is documented in flow charts on page 16, 18 and 19. I'm guessing it will be about 1,000 lines of VHDL?

 

I'll post my public domain source as I go along. Peer review certainly welcome.

 

PS : one guy did an interesting VHDL project around recreating the 9902.

 

 

Edited by pnr
  • Like 6
Link to comment
Share on other sites

That's a good idea for several reasons--original 9902 chips aren't too common anymore. They aren't unobtainium yet, but that day will come. Another possibility is to build an extended VHDL design that allows for all of the existing speed range of a standard 9902 and adds something at the top end to make it top out at speeds comparable to the PC serial chips. That way it retains compatibility with all of the older software but can still be used in high-speed modes. The rest of the system will have to be able to keep up, but if it can, this may improve things for connections to local PCs (HDX or other serial applications) or UDS10 (or other) network connections.

  • Like 2
Link to comment
Share on other sites

Another possibility is to build an extended VHDL design that allows for all of the existing speed range of a standard 9902 and adds something at the top end to make it top out at speeds comparable to the PC serial chips. That way it retains compatibility with all of the older software but can still be used in high-speed modes. The rest of the system will have to be able to keep up, but if it can, this may improve things for connections to local PCs (HDX or other serial applications) or UDS10 (or other) network connections.

 

That by itself would not be too hard I think. A real 9902 (typically) runs on an internal clock of 1MHz (3Mhz clock internally divided by 3). An FPGA version should be able to go much faster than that (say >50x faster). One could use one of the spare bits in the control register to enable such a turbo mode.

 

Keeping up would be hard: even in a tightly coded unrolled loop I'd be surprised if a 9900 could move more than 100 kbps into or out of the 9902, say 50 kpbs for full duplex and that then consumes the CPU for 100%...

 

For going much faster you will need an FPGA-based turbo CPU as well.

  • Like 1
Link to comment
Share on other sites

I'm working on a Verilog 9902. The idea is to provide CRU bus compatibility with existing TI-99/4A software that writes directly to the 9902.

It's easy to add a large ring buffer behind it.

 

Once it's virtualized, there's many uses for it. You would use any TI-99/4A terminal program. One port can access an internal device console. Another port can be routed to the outside world through a USB adaptor. An actual RS232 DB25 connector is kind of the last thing on my mind.

  • Like 2
Link to comment
Share on other sites

I'm working on a Verilog 9902. The idea is to provide CRU bus compatibility with existing TI-99/4A software that writes directly to the 9902.

It's easy to add a large ring buffer behind it.

 

 

Hi FarmerPotato, that is very interesting! I'm not too familiar with Verilog, but my understanding is that -- when working with code intended for synthesis -- the difference between VHDL and and Verilog is not all that much. Once I get my initial design and code done, I'd love to compare notes. And of course I would appreciate any comments that you may have on the code I will post.

 

Once it's virtualized, there's many uses for it. You would use any TI-99/4A terminal program. One port can access an internal device console. Another port can be routed to the outside world through a USB adaptor. An actual RS232 DB25 connector is kind of the last thing on my mind.

 

That's a very cool idea: the 9902 as a standard API for pumping bits down a channel, whatever that channel may be.

:thumbsup:

Link to comment
Share on other sites

Well, I got underway with this project.

 

I'm kinda new to working with VHDL and mostly following the advice given in the Freerange VHDL book. I'm trying to follow the coding style recommended by Gaisler and the best practices from P. Chu's lecture slides.

 

So far I have done the code for the 6 simple registers and for the CRU interface. Some highlights below.

 

I start out with some boilerplate and with defining the pins of the 9902:

library ieee;
use ieee.std_logic_1164.all;

entity tms9902 is
port (
	PHI	: in  std_logic; -- input CLK
	nRTS	: out std_logic;
	nDSR	: in  std_logic;
	nCTS	: in  std_logic;
	nINT	: out std_logic;
	nCE	: in  std_logic;
	CRUOUT	: in  std_logic;
	CRUIN	: out std_logic;
	CRUCLK	: in  std_logic;
	XOUT	: out std_logic;
	RIN	: in  std_logic;
	S	: in  std_logic_vector(4 downto 0)
	);
end;

Next, I'm declaring the registers. In most cases this is simply a bag of bits, like so:

	-- interval register
	signal tmr_q, tmr_d : std_logic_vector(7 downto 0);

The signals ending in "_q" are the D flip-flop outputs, the signals ending in "_d" are the inputs. The entire circuit will be synchronous on a single clock (signal "clk").

 

In some cases (the flag register and the control register) I find it easier to define the bits by a mnemonic name, so I'm using a record to do that like so:

	-- control register
	type ctl_type is record
		sbs		: std_logic_vector(2 downto 1);
		penb		: std_logic;
		podd		: std_logic;
		clk4m		: std_logic;
		rcl		: std_logic_vector(1 downto 0);
	end record;
	signal ctl_q, ctl_d : ctl_type;

Each register is built up out of two statements: the first defines the D flip-flops, the second is pure combinational and defines the "_d" inputs. Most of the time "_d" equals "_q" and the flip-flops remain in the same state. The exception is when there is a CRU write operation.

 

Defining the register flip-flops for the timer interval register:

        tmr_reg : process(clk)
	begin
		if rising_edge(clk) then tmr_q <= tmr_d; end if;
	end process;

And defining its inputs:

	tmr_cmb : process(tmr_q, nCE, CRUCLK, flg_q, CRUOUT, S)
	variable v : std_logic_vector(7 downto 0);
	begin
		v := tmr_q;
		if nCE='0' and CRUCLK='1' and flg_q.ldctl='0' and flg_q.ldir='1' then
			case S is
				when "00111" => v(7) := CRUOUT;
				when "00110" => v(6) := CRUOUT;
				when "00101" => v(5) := CRUOUT;
				when "00100" => v(4) := CRUOUT;
				when "00011" => v(3) := CRUOUT;
				when "00010" => v(2) := CRUOUT;
				when "00001" => v(1) := CRUOUT;
				when "00000" => v(0) := CRUOUT;
				when others => null;
			end case;
		end if;
		tmr_d <= v;
	end process;

It uses an intermediary variable for simulation efficiency. The variable is initialized with the "_q" outputs, and at the end the "_d" inputs are set from the variable: most of the time nothing happens. Only if the chip is selected, there is an active CRUCLK input, the LDCTL bit is reset and the LDIR bit is set (see datasheet for the latter two), one of the bits is changed. When S4..S0 is all zeroes, the lsb is modified, etc.

 

Most of the registers work the same simple way. Only the register with the flag bits is a bit more complex, as there is some nifty logic associated with that. Again see the datasheet for details, there is an almost 1:1 relationship between the specs and the VHDL code.

 

Reading is a bit simpler. Basically I hook CRUIN up to the tri-state output of a 32->1 multiplexer. I don't have all the required signals yet, but the structure is there:

        CRUIN <=                'Z'             when nCE='1' else
                                intr            when S="11111" else     -- 31, any interrupt pending
                                flag            when S="11110" else     -- 30, 'flag' field
                                dsch            when S="11101" else     -- 29, device status change
                                not nCTS        when S="11100" else     -- 28, inverse of nCTS input
                                not nDSR        when S="11011" else     -- 27, inverse of nDSR input
                                flg_q.rtson     when S="11010" else     -- 26, inverse of nRTS output
                                timelp          when S="11001" else     -- 25, timer elapsed
                                '0'             when S="11000" else     -- 24, 'timerr', todo
                                '0'             when S="10111" else     -- 23, 'xsre', todo
                                xbre            when S="10110" else     -- 22, transmit buffer register empty
                                rbrl            when S="10101" else     -- 21, receive buffer register full
                                dscint          when S="10100" else     -- 20, device status change interrupt pending
                                timint          when S="10011" else     -- 19, timer interrupt pending
                                '0'             when S="10010" else     -- 18, not used (always 0)
                                xint            when S="10001" else     -- 17, transmit interrupt pending
                                rint            when S="10000" else     -- 16, receive interrupt pending
                                RIN             when S="01111" else     -- 15, direct copy of RIN
                                '0'             when S="01110" else     -- 14, 'rsbd', todo
                                '0'             when S="01101" else     -- 13, 'rfbd', todo
                                '0'             when S="01100" else     -- 12, 'rfer', todo
                                '0'             when S="01011" else     -- 11, 'rover', todo
                                '0'             when S="01010" else     -- 10, 'rper', todo
                                '0'             when S="01001" else     --  9, 'rcverr', todo
                                '0'             when S="01000" else     --  8, not used (always 0)
                                rbr_q(7)        when S="00111" else     --  7, receive buffer register, bit 7
                                rbr_q(6)        when S="00110" else
                                rbr_q(5)        when S="00101" else
                                rbr_q(4)        when S="00100" else
                                rbr_q(3)        when S="00011" else
                                rbr_q(2)        when S="00010" else
                                rbr_q(1)        when S="00001" else
                                rbr_q(0)        when S="00000" else     --  0, receive buffer register, bit 0
                                '0';

Well that's the main points of what I have now. I've attached the full file for review (.txt added so the forum will accept it). So far it seems to compile and synthesize.

 

A good 300 lines so far. Next up I'll look at the timer controller and timer counter.

 

tms9902_v1.vhd.txt

  • Like 4
Link to comment
Share on other sites

Seems like a good start ! I have also been planning to implement 9902, but maybe I don’t have to since you’re already working on it.

 

Referring back to your earlier post, I have already partially done the 9901 in VHDL. I put together a quick and dirty version for my TI-99/4A clone. If I remember correctly that part is not implemented as a separate module, but rather baked in to the toplevel VHDL code to implement the keyboard and joystick interfaces in a way compatible with the 99/4A. I could move that code over to a separate module, might be useful for reference.

Link to comment
Share on other sites

A few comments, take them as you like:

 

* Add a suffix to the port entries, _i for input _o for output. For example: nRTS_o, nDSR_i, etc. Personally I typically keep is all lower-case and add more underscores, i.e. n_rts_o, n_dsr_i, etc. But I'm always undecided if I really like that style... ;-)

 

* Always do synchronous design, as well as synchronous reset.

 

* Avoid variables and the blocking operator := if you plan to actually synthesize the design for use in an FPGA. There are cases where it will synthesize, but it is just easier to not have to deal with it, IMO.

 

* If you interface with real external signals and bring them into your synchronous system, you will have to synchronize them.

 

* If you don't register your inputs and outputs, keep in mind that you may see some very interesting signal relationships if your design fails any timing constraints. You will look at the error and say "how the heck does *that* signal affect this one??" Propagation delays are calculated between registers, no matter where those registers are.

 

Pong Chu's books are worth the money! Also, using _d as a suffix is interesting, I don't know why I did not think of that. I typically use _x (short-hand for _next) that I picked up from Chu's code. I think I'll have to switch to _d. ;-) I'll have to check out Gaisler's style guide, I have never heard of that before.

 

It looks good so far.

Link to comment
Share on other sites

That's a good idea for several reasons--original 9902 chips aren't too common anymore. They aren't unobtainium yet, but that day will come. Another possibility is to build an extended VHDL design that allows for all of the existing speed range of a standard 9902 and adds something at the top end to make it top out at speeds comparable to the PC serial chips. That way it retains compatibility with all of the older software but can still be used in high-speed modes. The rest of the system will have to be able to keep up, but if it can, this may improve things for connections to local PCs (HDX or other serial applications) or UDS10 (or other) network connections.

Exactly.

 

If we can have a 9902 in a FPGA, we can easily interface it to a number of current off the shelf serial chips. Legacy software wouldn't notice the difference. Maybe have a mode on/off to where the 9902 is "bypassed" when you have an app that can talk to the new serial chips directly, like Fred's HDX software (he has it talking to the NanoPEB and UberGROM chips directly.)

 

This project is a most excellent idea. I really want to use Telco again. :)

 

Sent from my moto x4 using Tapatalk

Link to comment
Share on other sites

The other day I came across one of Grant Searle's projects that I had not noticed before: his "multicomp" project. It is a simple, low cost FPGA project, where he has more or less created a "software breadboard" for prototyping various retro computers. Googling for it shows various successful builds, including those with ready made PCB's, such as:

https://www.retrobrewcomputers.org/forum/index.php?t=msg&th=111&start=0&

(but there are several more such efforts).

 

Maybe it is a nice idea to extend this idea to the 99xx world and create a few components to make it easy to prototype various small 99xx systems. The main thing missing in Grant's setup is the CRU bus and VHDL for CRU based chips. Probably the 9900 CPU core that 'speccery' did for last year's retro challenge can be reworked into a component for such a project without too much effort. Other chips to be added would be the 9902 and the 9901.

 

 

 

Grant Searle's project was actually the thing that got me restarted with FPGA chips. I had some FPGA boards already in the past, but it was his multicomp that re-ignated my interest. I actually have built one of these (just with jumper wires - no PCB),I think it currently has the Z80 version loaded. Would indeed by nice to extend this concept to 99xx systems.

Link to comment
Share on other sites

That 9902 page does not give very much detail about the project, i.e. what FPGAs were used, etc. But it does mention doing a "few thousand" units, which made the development costs reasonable. However, it does also mention that special PCBs and PCB-pins were used due the the problem of keeping the top of the PCB clear for the BGA devices. *That* is the problem I had with the F18A and is not a cheap solution. There is also mention of having to use three BGA devices due to the narrow size of the PCB in staying within the footprint of the original IC. More parts means more expense. Last, most FPGAs do *NOT* have 5V tolerant I/O and you need level shifters. Since there is no mention about which FPGAs were used, or details about other components, it is hard to say if the design is risky or actually has the proper level shifting.

 

All-in-all, making a replacement 9902 that fits within the original 9902 IC footprint is not a trivial project. The first step might be to just contact the FullCircuit person and see if they have any for sale.

Edited by matthew180
Link to comment
Share on other sites

A few comments, take them as you like:

[...]

 

 

Thanks Matthew, those are useful comments!

 

I've already found that I need to work on grouping and signal naming to keep the code readable. Rather than refactor now, I think I will first complete the design in its current setup.

 

I haven't thought much about constraints files, timing and simulation yet. I guess a "test bench" might be as much work as the 9902 itself.

Link to comment
Share on other sites

I've now added the timer circuitry.

 

First I had a design with a finite state machine (FSM) with states like 'reset', 'wait', 'load', etc. When thinking that through I came to the conclusion that the timer controller really only has one state (apart from the 'timelp' and 'timerr' status bits). My current implementation is:

   -- define timer counter register
   --
   timctr_reg : process(clk)
   begin
      if rising_edge(clk) then timctr_q <= timctr_d; end if;
   end process;

   timctr_cmb : process(timctr_q, tmr_q, sig_ldir_reset)
   variable v : std_logic_vector(13 downto 0);
   variable z : std_logic;
   begin
      v := timctr_q;
      if v="00000000000000" then z := '1'; else z := '0'; end if;

      if sig_ldir_reset='1' or z='1' then
         v := tmr_q&"000000";
      else
         v := v - 1;
      end if;

      timctr_d <= v;
      sig_timctr_iszero <= z;
   end process;

   -- define timer controller register
   --
   timFSM_reg : process(clk)
   begin
      if rising_edge(clk) then timFSM_q <= timFSM_d; end if;
   end process;

   timFSM_cmb : process(timFSM_q, sig_reset, sig_timenb, sig_timctr_iszero)
   variable v  : timFSM_type;
   begin
      v := timFSM_q;
      if sig_reset='1' or sig_timenb='1'then
         v.timelp := '0';
         v.timerr := '0';
      elsif sig_timctr_iszero='1' then
         if v.timelp='1' then v.timerr := '1'; end if;
         v.timelp := '1';
      end if;
      timFSM_d <= v;
   end process;

The timer counter has 6 extra bits at the bottom versus the timer interval register (14 instead of 8 total): these are for the initial divide by 64.

 

I've added four signals ('wires') for communication between the elements: sig_reset, sig_timenb, sig_ldir_reset, sig_timctr_iszero. Each is asserted for 1 clock to signal certain conditions. The first three emirate from the CRU interface, the last from the timer counter.

 

The timer turned out a lot shorter and simpler than I thought. Stuff still compiles and synthesises, now at 438 lines. Full file attached for review.

 

I've got the design for the transmitter mostly done, coding that up is next.

 

tms9902_v2.vhd.txt

Edited by pnr
  • Like 2
Link to comment
Share on other sites

I've now added the timer circuitry.

 

...

 

The timer turned out a lot shorter and simpler than I thought. Stuff still compiles and synthesises, now at 438 lines. Full file attached for review.

 

I've got the design for the transmitter mostly done, coding that up is next.

 

 

 

Good progress! When you've got the transmitter done, I'd be happy to integrate it with my FPGA TI-99/4A to test how that works.

Link to comment
Share on other sites

 

Good progress! When you've got the transmitter done, I'd be happy to integrate it with my FPGA TI-99/4A to test how that works.

 

 

That would be cool. Note however that I have not simulated it with a testbench, and the old adage is: "if it hasn't been tested it doesn't work" ;)

 

I can imagine things like bits shifting out the wrong direction, counters being of by one, parity in reverse, etc. Perhaps also fundamental design errors.

 

I'll post the transmitter shortly.

  • Like 1
Link to comment
Share on other sites

Got the transmitter blocks coded up. Now at 650 lines, of which 220 for the transmitter. If the receiver is ~250 lines the total would be 900, close to the original estimate of one thousand.

 

The transmitter is driven of a clock signal running at twice the bitrate, just as in a real 9902. This allows the transmitter to send 1.5 stop bits (and makes the design of transmitter and receiver similar, as the receiver also needs half-bit times).

xhbctr_reg : process(clk)
begin
if rising_edge(clk) then xhbctr_q <= xhbctr_d; end if;
end process;

xhbctr_cmb : process(xhbctr_q, xdr_q, sig_xhb_reset)
variable v : std_logic_vector(13 downto 0);
variable z : std_logic;
begin
v := xhbctr_q;
if v="000000000000000" then z := '1'; else z := '0'; end if;

if sig_xhb_reset='1' or z='1' then
v := xdr_q(10)&"000"&xdr_q(9 downto 0);
else
v := v - 1;
end if;
xhbctr_d <= v;
sig_xhbctr_iszero <= z;
end process;

The half-bit timer is a free running 14 bit counter that counts down from the value in the data rate register to zero. It can be reset by the transmit controller at the start of a new byte being sent. When the counter crosses zero it emits a one-clock signal to the controller.

 

Then there is the transmit shift register. It has two control signals, "load from the transmit buffer register" and "shift", both provided by the controller:

   xsr_reg : process(clk)
   begin
      if rising_edge(clk) then xsr_q <= xsr_d; end if;
   end process;

   xsr_cmb : process(xsr_q, xbr_q, sig_xsr_load, sig_xsr_shift)
   variable v    : std_logic_vector(7 downto 0);
   begin
      v := xsr_q;
      if sig_xsr_load='1' then
         v := xbr_q;
      elsif sig_xsr_shift='1' then
         v := '0'&xsr_q(7 downto 1);
      end if;
      xsr_d <= v;
   end process; 

(Perhaps I should do the shift with the VHDL 'ssr' operator, not sure if that is better).

 

The controller is the most complex, about 100 lines. It has the following register bits:

type xmtstat is (IDLE, BREAK, START, BITS, PARITY, STOP);

type xmtFSM_type is record
xbre : std_logic;
xsre : std_logic;
xout : std_logic;
rts : std_logic;
par : std_logic;
bitctr : std_logic_vector(4 downto 0);
state : xmtstat;
end record;

The elements 'xbre' to 'rts' are as per the datasheet and the bit 'par' holds the parity bit. The 'bitctr' element counts out delay times, running from N to 1. The 'state' field holds the current state of the controller (intended to be binary encoded).

 

The controller starts out with figuring out the waiting times for the databits (depending on how many there are) and for the stop bits. It also makes the bit counter count down at twice the bit rate:

   xmtFSM_cmb : process(xmtFSM_q, ctl_q, flg_q, xsr_q, nCTS, sig_reset, sig_xbr7, sig_xhbctr_iszero)
   variable v   : xmtFSM_type;
   variable par : std_logic;
   variable xsr_load, xsr_shift, xhb_reset : std_logic;
   variable xbits : std_logic_vector(4 downto 0);
   variable sbits : std_logic_vector(4 downto 0);
   begin
      v := xmtFSM_q; xsr_load := '0'; xsr_shift := '0'; xhb_reset := '0';

      -- prepare half-bit times for data word and stop bits
      case ctl_q.rcl is
         when "11"   => xbits := "10000";
         when "10"   => xbits := "01110";
         when "01"   => xbits := "01100";
         when "00"   => xbits := "01010";
      end case;
      case ctl_q.sbs is
         when "00"   => sbits := "00011";
         when "01"   => sbits := "00100";
         when others => sbits := "00010";
      end case;

      if sig_xhbctr_iszero='1' then
         v.bitctr := v.bitctr - 1;
      end if;

Next is the handling of reset (write to CRU bit 31) and the IDLE and BREAK states. These are independent of the bit rate clock:

      if sig_reset='1' then
         v.xout := '1';
         v.rts  := '0';
         v.xsre := '1';
         v.xbre := '1';

      elsif sig_xbr7='1'then
         v.xbre := '0';

      elsif v.state=BREAK then
         v.xout := '0';
         if flg_q.brkon='0' then v.state := IDLE; end if;

      elsif v.state=IDLE then
         if flg_q.rtson='1' then v.rts := '1'; else v.rts := '0'; end if;
         if nCTS='0' then
            if v.xbre='1' then
               if flg_q.brkon='1' then v.state := BREAK; end if;
            else
               v.state   := START;
               v.bitctr  := "00010";
               xhb_reset := '1';
            end if;
         end if;

This is all intended to be in line with the top of the flow diagram in the datasheet. When a new character transmission starts, the half-bit counter is reset, the wait time is set to two half-bit times, and the next state is START. This state (and all remaining ones) are synchronous with the half-bit timer.

      elsif sig_xhbctr_iszero='1' then
         case v.state is

            when START =>
               v.xsre := '0';
               v.xbre := '1';
               v.xout := '0';
               if v.bitctr=0 then
                  xsr_load := '1';
                  v.state  := BITS;
                  v.bitctr := xbits;
                  v.par  := '0';
               end if;

The START state waits for two half-bit times, keeping 'xout' zero. At the end, it also loads the shift register, sets up a new waiting time and inits the parity calculation bit.

 

The next state is BITS. This is where the controller waits for the data bits to shift out and calculates even parity as it goes along.

            when BITS =>
               if v.bitctr(0)='1' then
                  v.par := v.par xor xsr_q(0);
                  xsr_shift := '1';
               end if;
               if v.bitctr=0 then
                  if ctl_q.penb='1' then
                     v.state  := PARITY;
                     v.bitctr := "00010";
                  else
                     v.state := STOP;
                     v.bitctr := sbits;
                  end if;
               end if;

When all the bits have been shifted out, it either moves to the PARITY state or to the STOP state.

 

The PARITY state is responsible for sending out the parity bit.

            when PARITY =>
               if ctl_q.podd='1' then v.xout := not v.par; else v.xout := v.par; end if;
               if v.bitctr=0 then
                  v.state := STOP;
                  v.bitctr := sbits;
               end if;

In this state 'xout' equals the even/odd parity bit. At the end it sets up the STOP state, including its precalculated waiting time.

 

The STOP state simply sets 'xout' to one and waits. It then moves back to the IDLE state. The rest of the controller code is boilerplate:

            when STOP =>
               v.xout := '1';
               if v.bitctr=0 then
                  v.state := IDLE;
               end if;

            when others => v.state := IDLE;

         end case;
      end if;
      xmtFSM_d <= v;
      sig_xhb_reset <= xhb_reset;
      sig_xsr_load  <= xsr_load;
      sig_xsr_shift <= xsr_shift;
   end process;

So far, it still compiles and synthesises, but nothing at allhas been tested yet.

 

I suspect that the code for the receiver will be very similar to that of the transmitter and perhaps some 250 lines.

 

The full code of where I am at has been attached.

 

 

tms9902_v3.vhd.txt

  • Like 2
Link to comment
Share on other sites

That would be cool. Note however that I have not simulated it with a testbench, and the old adage is: "if it hasn't been tested it doesn't work" ;)

...

 

 

Somewhat off-topic:

My process with learning to do things in VHDL has been very unusual for me. With programming languages, I've always picked up a book first, read it, and then as I am finishing with the book I've started to test things out.

 

With VHDL things went very differently - I took a very unorganised way and started by modifying designs done by others (read: I ported some designs to FPGA boards I had acquired - among them the all important arcade game Scramble). I had some FPGA boards laying around as my phase of "intending to start with FPGA boards" took several years. I then took Grant Searle's Multicomp, the Z80 version, and turned it into a Sinclair Spectrum. There was not much Multicomp left at the end. The most interesting and Spectrum specific piece was the VGA controller. As usual, there was also a software development effort involved, as I built a custom ROM by modifying the original ROM of the Spectrum so that it could load a memory snapshot (I think they are .z80 files from emulators) from a paged ROM memory area and then start from it correctly. And it worked! In this process I actually first ported the Multicomp from Altera first to XC3S1200E FPGA and then to the XC6SLX9 that I have mostly used.

 

In these projects I did not use any simulation, except for debugging the reset logic for the Spectrum. With the TMS9900 CPU I did use a testbench pretty extensively, as this was a much more complex project and very hard to debug without a test bench. It actually was hard to debug even with the test bench, as the CPU gained functionality the problems with TI-99/4A software started to appear only after a million clock cycles or so, and my test bench was only for the CPU, so I ended up building debug features into the CPU core itself.

Link to comment
Share on other sites

I will try to find some time this evening to integrate the TMS9902 transmitter into my TI-99/4A clone. A stupid question: what sound the clock frequency be for the TMS9902? For my TMS9995 breadboard project (a beefed up version of Stuart's excellent project) I guess the CLKIN to the TMS9902 was CLKOUT of the TMS9995, so I suppose 3MHz. I have some test code for the TMS9902 somewhere, I can probably adapt that. Or just use Easybug to initialise the TMS9902. The master clock of the FPGA in my system is 100MHz, so I need to divide that into something more reasonable for the serial transmission.

Link to comment
Share on other sites

One quick comment - perhaps more of a note to myself - the code has blocks like the one below. For as long as CRUCLK is high the register xbr_q gets updated on every clock cycle of CLK. I guess that's okay. For my TMS9900 CPU the CRUCLK is high for a few 100MHz clock cycles, and during that time the processor is basically idle, holding its state and waiting for external logic (such as this TMS9902) to sample the "external" signals. That does mean that the CLK has to be essentially the same clock that is driving the CPU - this of course has been the idea all along with the original TMS9902. In practice this probably means that CLK for me will be 100MHz and there needs to be a divider somewhere dividing the 100MHz to 3MHz (or something like that) to drive the baud rate generator of the TMS9902, otherwise the divider probably would need many more bits to generate something like 19200 Hz for the 9600bps bit rate. It would be good to keep the TMS9902 initialisation sequence the same as with software of the day, so the FPGA TMS9902 will probably indeed an additional divider for 1) software compatibility and 2) independency from the TMS9900 clock (which I personally want to be very high since I think that is cool).


-- define transmit buffer register
--
xbr_reg : process(clk)
begin
if rising_edge(clk) then xbr_q <= xbr_d; end if;
end process;
xbr_cmb : process(xbr_q, nCE, CRUCLK, flg_q, CRUOUT, S)
variable v : std_logic_vector(7 downto 0);
variable xbr7 : std_logic;
begin
v := xbr_q; xbr7 := '0';
if nCE='0' and CRUCLK='1' and flg_q.ldctl='0' and flg_q.ldir='0'
and flg_q.lrdr='0' and flg_q.lxdr='0' then
case S is
when "00111" => v(7) := CRUOUT;
when "00110" => v(6) := CRUOUT;
when "00101" => v(5) := CRUOUT;
when "00100" => v(4) := CRUOUT;
when "00011" => v(3) := CRUOUT;
when "00010" => v(2) := CRUOUT;
when "00001" => v(1) := CRUOUT;
when "00000" => v(0) := CRUOUT;
when others => null;
end case;
-- writing to bit 7 resets the XBRE flag in the controller.
if S="00111" then xbr7 := '1'; end if;
end if;
xbr_d <= v;
sig_xbr7 <= xbr7;
end process;
Link to comment
Share on other sites

 

[...], so the FPGA TMS9902 will probably indeed an additional divider for 1) software compatibility and 2) independency from the TMS9900 clock (which I personally want to be very high since I think that is cool).

 

 

Yeah... I've now designed it for PHI to be either 3 or 4 MHz, just like a real part: I'm thinking this design will maybe fit in a Altera EPM7160 CPLD, which may be nice for 5V-based experiments. The v3 source has the divide-by-3-or-4 clock divider circuit implemented.

 

For most of the registers it does not matter how fast CLK is, 100MHz would be acceptable. The only issue to solve would be the bit rate counters, which have to count at about 1Mhz. What you could do when interfacing with a 100MHz design is modifying the clock circuit thus:

   PHI : the 100Mhz system clock

---
   signal clk      : std_logic;
   signal bitclk   : std_logic;
   signal clkctr_q : std_logic_vector(6 downto 0);

----

   clk <= PHI; -- run 9902 logic at 100MHz

   clkdiv: process(PHI, clkctr_q)
   variable v : std_logic_vector(1 downto 0);
   begin
      v := clkctr_q;
      if rising_edge(phi) then
         v := v + 1;
         if v="1100100" then v:="0000000"; end if;
         clkctr_q <= v;
      end if;
   end process;
   bitclk <= '1' when clkctr_q="0000000" else '0'; -- 1 Mhz clock

.

The bit rate clock would then be modified like this:

   xhbctr_cmb : process(xhbctr_q, xdr_q, bitclk, sig_xhb_reset)
   variable v : std_logic_vector(13 downto 0);
   variable z : std_logic;
   begin
      v := xhbctr_q;
      if v="000000000000000" then z := '1'; else z := '0'; end if;

      if sig_xhb_reset='1' or z='1' then
         v := xdr_q(10)&"000"&xdr_q(9 downto 0);
      elsif bitclk='1' then
         v := v - 1;
      end if;
      xhbctr_d <= v;
      sig_xhbctr_iszero <= z;
   end process;

.

This of course all assumes that the internal clock rate of the 9902 is exactly 1 Mhz, like it is in Stuart's breadboard/PCB designs. If using software designed to work on hardware with another clock rate the clock divider would need a different constant to match the proper internal 9902 frequency.

Link to comment
Share on other sites

I did most of the initial F18A VHDL without any test benches... I also spent a lot of time generating non-working bit-streams. Not doing a test bench is almost like writing your code on punch cards and submitting it for execution, with the only results being that it worked or it did not. HDL is hard enough, so don't avoid test benches and simulation just because you don't yet understand it. Once I took a small amount of time to try using a test bench, I was very angry that I did not use them sooner. If you are using one of the Xilinx tools ISE or Vivado, then creating all the boiler-plate for a test bench can be done by the tool (add new source, test bench, select UUT (unit under test)). Add some basic stimulus and start the simulation. It is too easy *not* to do.

 

@pnr: Your use of the "variable" type is... interesting. After reading the Gaisler paper I see where you picked that up, and the author makes some interesting points that I will be adopting for sure (using records). However, you will find that most VHDL that is written for synthesis to an actual FPGA (as opposed to just simulation) will stick to a very small set of types (usually just signal, constants, and occasionally a variable). Admittedly it looks a little strange to me, and the difference between signal and variable is hard to understand, IMO. I'm still trying to make sure I realize the subtle differences. Nothing in an FPGA is "instant" so I shied away from variable types. HDL is about describing hardware, and the variable type does not have a hardware equivalent (signals represent wires). This is all just my opinion and such, so don't take this wrong, I'm just contrasting and comparing, and trying to decide if using the variable type is something I might start trying to use more of. Your HDL is the most use of the variable type that I have ever seen, and I wonder if it will make it equally as strange to you when you see HDL that does not use variables at all.

Link to comment
Share on other sites

All: when it comes to running VHDL simulations I'm starting from absolute zero. :-o I'd be happy to hear of tips, suggestions and perhaps pointers to tutorials for running simulations / test benches on the Altera Quartus II software. My confusion already starts at the constraints file.

 

 

I then took Grant Searle's Multicomp, the Z80 version, and turned it into a Sinclair Spectrum. There was not much Multicomp left at the end. The most interesting and Spectrum specific piece was the VGA controller. As usual, there was also a software development effort involved, as I built a custom ROM by modifying the original ROM of the Spectrum so that it could load a memory snapshot (I think they are .z80 files from emulators) from a paged ROM memory area and then start from it correctly. And it worked! In this process I actually first ported the Multicomp from Altera first to XC3S1200E FPGA and then to the XC6SLX9 that I have mostly used.

 

It sounds like I'm on a similar journey, only 4 years later. I'm hoping to get Stuarts 9995 breadboard running multicomp style, and hook up a real 99105 to an otherwise emulated system next.

Link to comment
Share on other sites

@pnr: Your use of the "variable" type is... interesting. After reading the Gaisler paper I see where you picked that up, and the author makes some interesting points that I will be adopting for sure (using records). Admittedly it looks a little strange to me, and the difference between signal and variable is hard to understand, IMO. I'm still trying to make sure I realize the subtle differences.

 

Well, I'm just a beginner here and maybe later on in this project I will realise that I approached it all wrong. No better school than the school of hard knocks :^)

 

My coding style is also influenced by this project: https://github.com/wfjm/w11

 

My understanding of variables and signals in VHDL is as follows. When used for simulation, the VHDL runtime keeps an event queue with the next, current and previous state of all signals. Assigning a signal reads from the current state and writes to the next state. What is 'current' shifts continuously forward as simulated time progresses. Variables only exist inside a process block and are like regular variables. It would seem that variables keep their value from event to event (i.e. process blocks in simulation are "closures") and if one relies on this the block becomes non-synthesizable. In my "*_cmb" process block code all variables are re-initialised for each event (i.e. the process blocks are pure combinational) and this can be synthesised, or so it seems.

 

Assigning a signal in simulation is expensive (it requires updating the event queue) and according to Gaisler some 100x slower than simply assigning a variable. Hence I keep all my intermediaries in variables, even though my designs are so small that I probably won't notice the difference.

 

My understanding is that synthesising was something that was later overlaid on the VHDL language (and is arguably a language in itself). What constructs are being recognised as what circuit seems to me to be a bit of a black art, with differences between tools and changing as technology progresses. In effect, my *_cmb process blocks are truth tables written up like code. It would seem that toolchains have learned to recognise this (and even the 15 year old synthesiser shipped by Atmel/Microchip seems to handle it well).

 

Now that I have written some code I think that I did not quite understand Gaisler: I'm still writing code with each register in a separate process statement. This is not necessary: all the registers for the timer could be in a big single record, as could all the registers for the transmitter. Doing so keeps all related code together and this is perhaps easier to read and maintain. Once I have working code I will try that refactoring and see if makes sense.

Edited by pnr
Link to comment
Share on other sites

For most of the registers it does not matter how fast CLK is, 100MHz would be acceptable. The only issue to solve would be the bit rate counters, which have to count at about 1Mhz.

You want to use one clock and generate "enable" signals. Do not gate clocks in FPGAs (in case you have not read that yet). FPGAs have dedicated fabric for clock routing, and all flip-flops have enable inputs. You really want to keep your design synchronous to a single clock so all transitions happen at the same time.

 

If you have a design where you need a fast clock, like 100MHz (for example, to drive a modern video output), yet slow components (1MHz, 3.5MHz, etc.) because you are doing legacy or SoC work (like we all are in this case), then make a module that creates all the enables you will need, then distribute the enable with the main 100MHz clock. Then in the register-transfer (or where ever you use the clock), you add the enable:

if rising_edge(clk_100m0_i) and clk_1m0_en_i = '1' then

The enables are literally just counters, driven by the main clock and provide a single-tick at the main clock rate. An example for a 1MHz enable might be:

    clk_100m0_i  :  in std_logic;  -- 100MHz system clock
    clk_1m0_en_o : out std_logic;  -- 1MHz enable output


    -- Initial values are for simulation and bit-stream.
    signal tick_1m0_q, tick_1m0_d   : std_logic := '0';
    signal cnt_1m0_q, cnt_1m0_d     : unsigned(6 downto 0) := "0000000";


    -- Standard register-transfer
    clk_1m0_en_rt : process(clk_100m0_i)
    begin
        if rising_edge(clk_100m0_i) then
            tick_1m0_q <= tick_1m0_d;
            cnt_1m0_q  <= cnt_1m0_d;
        end if;
    end process;

    -- Next state logic
    process ( tick_1m0_q, cnt_1m0_q )
    begin
        if cnt_1m0_q = 99 then
            tick_1m0_d <= '1';
            cnt_1m0_d <= (others => '0');
        else
            tick_1m0_d <= '0';
            cnt_1m0_d <= cnt_1m0_q + 1;
        end if;
    end process;

    -- Ouputs
    clk_1m0_en_o <= tick_1m0_q;



Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...