CS 202 - Project 01: Associative Memory
Due: 03/29/2023
Goals
- Combine what you have learned about combinational and sequential logic to make a more complex circuit
- Demonstrate that you know how to take a complex problem, break it down into smaller pieces, implement solutions to those and then assemble them into a final solution
Objective
In class, we discussed traditional memory, in which data is stored and retrieved according to its location (ie, its address). For this assignment, you will implement an associative memory instead, in which data is stored and retrieved by an associated key (hence the name).
Just like the 4-by-3 memory shown in lecture 08, your associative memory will store a bunch of values (in this case, eight). Also like that memory, yours will have inputs to select which value to read and which value to write but, unlike that example, your memory for this assignment won't select values by location. Recall that the 4-by-3 memory allowed you to pick which value you read and, separately, which value you wrote, by providing an address. In your associative memory, every value will be associated with a key—you will insert them as a pair, and you will need to provide a key to select a value to output.
The desired behavior is similar to dictionaries in Python, which allow you to store a value by key and later retrieve it by key. In the following Python example, the dictionary my_dict stores two values: the key "hello" is associated with value 12 and the key 19 is associated with value True. The value 12 is retrieved by providing its key.
>>> my_dict = {}
>>> my_dict["hello"] = 12
>>> my_dict[19] = True
>>> print(my_dict["hello"])
12
Many programming languages include data structures that behave similarly; the generic term is associative array (because it behaves like an array, except that arbitrary keys are associated with each value, rather than numeric array indexes). You're going to implement an associative memory, which is a memory that identifies stored data according to a key rather than its location. Therefore, instead of the implicit addresses we saw in the first lecture on memory, you're going to have explicit keys.
There are two primary differences between the dictionary you've used in Python and the associative memory you will build. Both are due to the (seemingly) infinite facilities provided you by Python which are not available to us lowly hardware engineers who have to deal with the pesky limitations of "reality". First, the keys in your associative memory will be 6-bit values. Second, your associative memory will only store up to 8 different key/value pairs.
This second limitation raises an interesting question: what should your associative memory do when a ninth item is written to it? The answer is that it should cause the least recently written key/value pair to disappear before storing this new item. (The method of deciding which item has to go when a memory is full is called an eviction policy; we'll see more on this topic near the end of the semester.)
Requirements
You must implement an associative memory that
- uses 6-bit keys
- stores 8-bit values
- implements a least-recently written eviction policy
Your memory should have the following interface
name | width | direction |
---|---|---|
write_key | 6 bits | in |
write_value | 8 bits | in |
clock | 1 bit | in |
read_key | 6 bits | in |
read_value | 8 bits | out |
On the rising edge of "clock", the memory should store "write_value" associated with the key "write_key". If the memory is full, it should evict the least recently written key/value pair to make room for this one.
At all times, the memory should show on its "read_value" output the value associated with the key on its "read_key" input. If there is no value stored with that key, the memory output should be floating (ie, Us in Logisim).
You are limited to the following Logisim components:
- wiring
- gates
- plexers
- arithmetic
- memory
You may add as many subcircuits as you deem appropriate.
Complication: resetting the circuit sets all register contents to zero
So if you're storing the keys in registers, how do you tell the difference between a register storing the value 000000 and a register that has just been reset? I suggest you actually use a larger register to store keys and use an additional bit within each to indicate whether or not the contents are valid.
Complication: getting Us on the output
The last time we were "producing" U output, we were using transistors. While we could build a custom circuit with transistors that allowed us to selectively turn off the output, there is an easier solution in the form of the Controlled Buffer, which you will find in the Gate library.
Advice
Getting from the components we've seen to a fully-functional associative memory is a big leap. If you recall how we developed the idea of the register file and ALU (lecture 11), we started with a sequence of examples; I suggest you do the same here. Write down a sequence of inputs to the associative memory and figure out what the output should be at each step. Then, figure out what hardware is necessary to achieve this behavior.
What follows is a more detailed description of this process, specifically tailored to this assignment. For each step, I've also noted how the advice applies to computer sciencey problem-solving in general.
-
Start by figuring out what data your memory needs to store to do its job (which is, ultimately, making sure the output is correct). Setting aside the eviction policy for a moment, what information do you need to remember to make sure you can later retrieve the value associated with a particular key? What storage elements can you use to remember these data? (General lesson: understand what data you need to solve the problem at hand.)
-
In the "normal" memory I showed in class, we used the notion of addresses to identify a given value; these addresses were assigned implicitly based on the physical location of the value within the memory. The associative memory has no notion of addresses, so you can decide how to physically organize it to your advantage. I suggest you organize it such that the items are ordered according to when they were written. (General lesson: be aware of what is and, possibly more importantly, what is not required to solve your problem and jettison the unnecessary parts if it serves your purposes.)
-
On a low-tech piece of paper, sketch out the storage elements you decided on in step 1 and run through scenarios in which you insert values with various keys, maintaining the property that all items are ordered according to the order in which they were written. Under most circumstances, items are going to move around your memory in a regular, predictable fashion. There are, however, a couple situations where this does not happen. Identify them. (General lesson: figure out the desired behavior of your project both under common inputs as well as uncommon, potentially tricky inputs—the latter are called corner cases.)
-
Given the behavior you just sketched out, how can you test to make sure your circuit does the right thing under all circumstances? What exact inputs can you give it and what outputs do you expect? Write this down. This is your test vector. You might have some test(s) that check your circuit's behavior under normal circumstances and you might have other test(s) that verify the corner cases; that's okay. (General lesson: figure out how you're going to know you won before you start building so you can verify your solution as you build it.)
-
As a corollary to this, in the practical realm of being a student, you're anticipating how we're going to grade your assignment. What aspects of its behavior are we going to test? How do you think we're going to test them? (General lesson: learn to read your graders' minds.)
-
Consider the simple behavior you identified in step 3 (i.e., ignore the corner cases for now). Figure out how you can connect together circuit elements to achieve this behavior. Start small: maybe only support storing 2 values instead of 8 at the beginning. Use the applicable tests from your test vector to make sure it works. Add the rest of the circuitry to support 8 values. Run the tests again. (General lessons: solve the easy part first. Test early and test often.)
-
Now consider the corner cases above. Under what circumstances does your circuit behave differently than the common case? Can you come up with logic that describes these circumstances? Can you implement that logic as gates and integrate it into your existing circuit? If you can, great. If you can't, consider how you can modify your circuit to accommodate such a solution. (General lesson: try to fit your corner-case solutions into your solution to the easy part, but don't be afraid to reconsider your solution to the latter.)
-
Build it. Test it. (General lesson: Test. Test. Test. Test.)
-
If any of these things are unclear, ask. If you want to talk through your test vector, ask. If you want to talk through your ideas for implementing the logic you're considering, ask. If you're stuck and you don't even know why, ask. If you have no clue how to proceed, ask. Often, when we keep our uncertainties bottled up in our head, we're not even sure what the uncertainties are! (General lesson: the process of putting our question into words is often enlightening all by itself. See rubber duck debugging.)
Submission
Upload your .circ file to Canvas.
Last updated 03/29/2023