Memory Control Logic

Introduction

This is going to be the next-to-last chapter about the memory subsystem. We have all the difficult, analog components in place to build a core memory: the current source, the line drivers, the inhibit drivers and the sense amplifiers. What we need is the logic to glue them all together into a functional subsystem. This chiefly includes the address decoders and the control/timing state-machine. There is technically also a bus interface section that attaches the memory to the CPU, but I think I won’t break it out as a separate block. Instead, I will describe the bus interface signals and their purpose, but will merge the implementation with other parts.

With that, let’s jump in!

Bus interface

I have not gone into a lot of details as to what the bus interface of the CPU will look like, when I described its micro-architecture. That’s because I didn’t know how I wanted it to look like back then. Now, I have a much better idea, so let’s get down to the details!

The bus interface will have the following signals:

b_a[15…0]: The address bus. This is driven by the CPU at all times. The top 2 bits will be used to decode various memory regions: 00 – RAM, 01: unused, 10: ROM, 11: peripherals

b_d_w[15…0]: The data write bus. This is driven by the CPU and contains data during write-cycles. During reads, its content should be ignored.

b_d_r[15…0]: The data read bus. This is driven by the addressed target (ROM, RAM, peripheral). The bus itself will be driven by everyone in an open-collector fashion and each line will have one common pull-up resistor. Unaddressed targets should simply drive ‘1’-s to their outputs

b_wr_n: Active low write enable signal, driven by the CPU. This signal is low for one clock cycle during writes. The address bus is valid whenever wr_n is low. The d_w bus is valid on the rising edge of wr_n.

b_rd_n: Active low read enable signal, driven by the CPU. This signal is low for one clock cycle during reads. The address bus is valid whenever rd_n is low. The d_r bus is captured by the processor on the rising edge of rd_n.

b_inst: Active high signal driven by CPU. It is driven high for one cycle when an instruction code is read by the CPU.

b_cs_n: Active low enable signal, driven by the top level address decoder. This signal must be high for the core memory to do anything.

I’m not completely sure about the separation of the d_r and d_w buses. Maybe I’ll merge them into a more traditional data-bus, though – honestly – the extra 16 wires is not that big a deal.

Address decode

The address bus presents 16 bits. The top 2 decodes the subsystem to be addressed, (00 for core memory) the rest is used as follows:

Here we take advantage of the matrix drive-scheme of the X and Y lines. Even though we have 128 X and Y lines each in the memory, we don’t have to do a 7-bit decoder. Instead, we can decode the top 3 and the bottom 4 bits of the address to generate the matrix selection lines. This is a much much easier thing to do.

To do the decoding, I will – essentially – use 74LS138 decoders:

Except of course, I’m going to build this logic out of discrete transistors. These can do the job right away for the top 3 bits of both X and Y. A pair of them will generate the bottom 4-bit decode. Let’s take a look at the X drive:

The first decoder will be hooked up as follows:

A0 – b_a[0]
A1 – b_a[1]
A2 – b_a[2]
G0 – b_a[3]
G1 – b_rd_n & b_wr_n & b_cs_n
G2 – VCC

The outputs will drive the first 8 X column lines (x_row_n[7…0]). The second decoder will be hooked up slightly differently:

A0 – b_a[0]
A1 – b_a[1]
A2 – b_a[2]
G0 – GND
G1 – b_rd_n & b_wr_n & b_cs_n
G2 – b_a[3]

The outputs will drive the second 8 X column lines (x_row_n[15..8]). You can see, how – using the inverting and non-inverting select inputs – we can incorporate the fourth address bit into the decoder logic.

The third decoder, for the X row lines is straightforward:

A0 – b_a[4]
A1 – b_a[5]
A2 – b_a[6]
G0 – GND
G1 – b_rd_n & b_wr_n & b_cs_n
G2 – VCC

The outputs drive the X row selectors (x_row_n[7…0]).

A similar triplet exists for the Y lines with the obvious changes to the address bits used.

Let’s recall that the line drivers are essentially H-bridges:

Except now, I need to add indices to indicate which of the many bridges I’m talking about. So, let’s start naming things, assuming that the control signals are active-low:

x_tl_n[7…0]: The 8 high-side row drivers for the X lines
x_bl_n[7…0]: The 8 low-side row drivers for the X lines
x_tr_n[15…0]: The 16 high-side column drivers for the X lines
x_br_n[15…0]: The 16 low-side column drivers for the X lines

A similar set exists for the Y lines of course. Now, we can start writing the logic equations for the driver drive (heh!) signals:

x_tl_n[i] = x_row_n[i] | x_rd_en_n
x_bl_n[i] = x_row_n[i] | x_wr_en_n
x_tr_n[j] = x_col_n[j] | x_wr_en_n
x_br_n[j] = x_col_n[j] | x_rd_en_n

In other words, we simply have to condition the decoder signals with the appropriate read- or write-enable signals. These will be generated by the control state-machine that we will get to in a moment.

Again, obviously a similar logic exists on the Y line side of things as well.

Inhibit driver control

During writes, we can only write ‘1’-s into the memory. A previous read ensured that the location in memory is reset to 0. If we intend to write a bit to 0, we have to energize the corresponding inhibit line to prevent the write from taking effect. We have 16 active-low inhibit enable control signals, one for each bit-plane:

inh_en_n[15…0]

These need to be set to low, only during writes, when the corresponding data bit in the data bus is set to zero. So:

inh_en_n[i] = b_d_w[i] | inh_en_n

Here inh_en_n is another control signal generated by the state-machine. We will have to be careful about the generation of this signal though: it needs to be asserted only when the content of b_d_w is valid. This means that I will have to look back at the setup delays of the CPU and adjust the timing such that this condition holds.

Sense logic

Let’s look back at this picture:

What we see here is this: during the current ramp (X or Y, doesn’t matter), there is coupling from the X and Y lines into the sense wire. That’s the first hump on the bottom line. Some time after the read current ramped to its maximum value (both X and Y currents that is), a magnetic flip in the core will result in a back-EMF voltage spike on the sense wire. Of course, if there is no flip, there won’t be a second hump.

The sense amplifier is always active and its output would look something like this:

The sense amplifiers are always active, but, during reads, we actually care about what their output is. To be more specific, we are about what their output is at a precise point in time during the read cycle. To be even more specific, we care about the output of the sense amplifiers during a time-window; after the X and Y currents ramped and the end of the (read) current pulse. This is what I denoted by the red lines above.

Within that window, there should only be a ‘1’ output on the sense amplifier, if there was a magnetic flip in the core. The problem is that we don’t precisely know when within that window that pulse may come. That depends on the ferrite material properties as well as the precise read current values, even the ramp rate to a certain degree. The way DEC dealt with this problem in this particular memory system is the following:

They had an RS flip-flop for every bit-plane. This flip-flop is kept in reset all the way up until the first red line. During this same time they mask the sense amplifier output, ensuring it’s 0. When time passes the first red line, they remove the reset drive from the RS flop and unmask the sense amplifier output. From that point on, if the sense amplifier output goes high at any time, the RS flop will be set. When we reach the second red line, we mask out the output of the sense amplifiers again, making sure that whatever noise we get on their outputs during the turn-off transient is not affecting the flip-flop state. This is how the timing diagram would look like:

In order to get the timing right, we only need to know when the current ramp ends (the position of the first red line). This is something we can get from simulation and it is on the order of 70ns for my circuit.

Driving the data-bus

Finally, we have to address the question of generating b_d_r. Not that it’s difficult, but still:

b_d_r[i] = data_out[i] | b_cs_n

In other words, the sense amps directly drive the bus, but only when the memory is addressed. When the memory is not addressed, all outputs are high, which – using the wired-and logic on the bus – enables other targets to drive their data to the same wires.

The control state-machine

Let’s take stock of the signals that we need to generate:

x_rd_en_n: needs to be low during read operations, when we want to drive the X lines
x_wr_en_n: needs to be low during write operations, when we want to drive the X lines
y_rd_en_n: needs to be low during read operations, when we want to drive the Y lines
y_wr_en_n: needs to be low during write operations, when we want to drive the Y lines
inh_en_n: needs to be low during write operations, when want to drive the inhibit lines
sense_clr: needs to be high during the current ramp
sense_en: needs to be high from the end of the current ramp till the end of the read current pulse

Read timing

DEC obviously had to solve the same problem, albeit their signal naming is quite a bit different. Here is their diagram:

Not all that useful without explanation to be honest. To boil it down to it’s essence (provided I understood the description they provided correctly), MATC_READ_EARLY_L controls the Y lines, MATC_READ_LATE_L controls the X lines. When they are low, the corresponding currents are flowing through the select lines. This would make the Y lines being on for 300ns and the X ones for 275ns.

In my case, here’s what I intend to do, for reads at least:

I will have a 8MHz clock (clk_8m), which I will divide down by 4 to generate the 2MHz system clock (sys_clk). These, along with the bus control signals are the inputs to the state machine. Each half-cycle of clk_8m is 62.5ns. We need to give some time for the CPU and the system address decoder logic to settle their outputs. So, we will react to them only one half clock cycle after the rising edge of sys_clk. That’s when we enable the Y currents and a half-cycle later add the X currents to the mix as well. We allow them one half-cycle to ramp, at which point we arm the sense amplifiers. They will remain active till the end of the cycle, when we start ramping the currents down.

The sense amp has about 70ns delay from input to output. Similarly the X and Y drivers have a roughly 70ns propagation delay. This is close enough to the 62.5ns half clock cycle, that I will simply assume that these circuits delay by that much.

There are a few variations here I want to entertain:

  • I might want to delay the transition between sense_clr and sense_en by half a cycle to ensure that we surely passed the current ramp phase
  • I might want to extend the stable current phase all the way to the end of the cycle (beginning of phase 7). That still allows sufficient time for the current to ramp back to zero as well as the RS-flops and the CPU to capture the results.

I will make sure I have the option to select either of these options in the state machine.

Write timing

The timing diagram for writes is very similar to reads with some obvious changes:

The sense amps are never enabled, nor cleared for a write and we need to active (some of the) inhibit wires by asserting inh_en_n.

Here too there is the potential to enable the inhibit wires half a cycle earlier or keep both X, Y and inhibit lines enabled for half a cycle longer. These options will be available in my design for sure.

Circuit design

I’m not going to bore you with all the details, but I’ll show you how the state machine will (I think) eventually work. In the following eye-chart you see how the system clock is generated:

Sorry, not terribly readable. The basic idea is a two-bit shift-register, built out of 4 latches. This is fed back to itself and clocked by the 8MHz clock. The clock is inverted between any adjacent stage and the feedback is inverted as well. So, if the power-on state of the register is all zeros, each half clock step will shift in a ‘1’ from the left, until all latches contain a ‘1’. After that ‘0’-s start shifting in, again, until the whole shift register is cleared. This takes 8 half clock cycles (or 4 whole clock cycles) to complete, so the output of the shift register will oscillate at 8/4=2MHz. Of course, every intermediate node in the register also switch at the same rate, but with different phases relative to the output. All in all, we have 8 phases of our 2MHz clock. Here is the simulation output:

One thing I have to mention still: the shift register only works properly, if it starts up with all 0-s (or all 1-s) after power-up. The power-on-reset circuit on the left of the schematic ensures that. This is also the reason why oscillation doesn’t start immediately: the circuit is held in reset for a short while. In the real system the reset time constant will have to significantly longer, maybe even generated in a more reliable manner, but nevertheless, it works for simulation purposes.

From the 8 clock phases, the various control signals are rather simple to assemble, especially if I show them on the timing diagram:

For instance:

x_wr_en_n = ~(sys_clk2 | sys_clk3) | b_wr_n | b_cs_n

Outro

Let me stop here. From these pieces, you can probably figure out how to build the required circuitry. I will have to do that too, but it doesn’t seem terribly difficult. One thing I’ll have to be careful about is to ensure that there are no glitches on the control signals: they directly drive analog circuitry and those won’t appreciate that. That was the reason to go with the many-phased clock approach: it allows me direct access to highly overlapping pulses, simplifying the generation of the various signals. I’ll come back to that at a later point though, for now, let me call it a day and see you next time.