Content-Addressable Memory (CAM) in Verilog with Code
A content-addressable memory (CAM), also called associative memory, is a special storage array that is searched by content rather than by address. You present a search key and, in a single cycle, every stored entry is compared in parallel; the CAM returns a match flag plus the index (and often the data) of the entry that matched. This page gives you working, simulator-verified content addressable memory Verilog code you can drop into a testbench and run.
Below are two complete modules: a parameterized CAM that returns a match flag and the matching index using a combinational parallel compare with lowest-index priority, and a variant that also returns the stored data on a hit and counts how many entries matched. Both are written in portable Verilog-2005 and ship with self-checking testbenches.
This parameterized CAM stores a key at each index through a synchronous write port. On a search, a combinational always @(*) block compares the search key against every valid entry at once and drives a match flag and the matching index, with the lowest index winning when more than one entry holds the key.
module cam #(
parameter ENTRIES = 8,
parameter KEY_W = 8,
parameter IDX_W = 3
)(
input clk,
input we,
input [IDX_W-1:0] waddr,
input [KEY_W-1:0] wkey,
input [KEY_W-1:0] skey,
output match,
output [IDX_W-1:0] match_index
);
reg [KEY_W-1:0] mem [0:ENTRIES-1];
reg valid [0:ENTRIES-1];
integer i;
initial begin
for (i=0;i<ENTRIES;i=i+1) begin mem[i]=0; valid[i]=1'b0; end
end
always @(posedge clk) begin
if (we) begin mem[waddr] <= wkey; valid[waddr] <= 1'b1; end
end
reg match_r;
reg [IDX_W-1:0] idx_r;
integer j;
always @(*) begin
match_r = 1'b0;
idx_r = {IDX_W{1'b0}};
for (j=ENTRIES-1;j>=0;j=j-1) begin
if (valid[j] && (mem[j]==skey)) begin match_r = 1'b1; idx_r = j[IDX_W-1:0]; end
end
end
assign match = match_r;
assign match_index = idx_r;
endmodule
module tb;
localparam ENTRIES=8, KEY_W=8, IDX_W=3;
reg clk, we;
reg [IDX_W-1:0] waddr;
reg [KEY_W-1:0] wkey, skey;
wire match;
wire [IDX_W-1:0] match_index;
cam #(.ENTRIES(ENTRIES), .KEY_W(KEY_W), .IDX_W(IDX_W)) dut (
.clk(clk), .we(we), .waddr(waddr), .wkey(wkey),
.skey(skey), .match(match), .match_index(match_index));
initial clk=0;
always #5 clk=~clk;
integer errors;
task do_write(input [IDX_W-1:0] a, input [KEY_W-1:0] k);
begin
@(negedge clk); we=1; waddr=a; wkey=k;
@(negedge clk); we=0;
end
endtask
task do_search(input [KEY_W-1:0] k, input exp_m, input [IDX_W-1:0] exp_i);
begin
skey=k; #1;
if (match!==exp_m || (exp_m && match_index!==exp_i)) begin
errors=errors+1;
$display("FAIL search key=%0d got match=%b idx=%0d exp match=%b idx=%0d",
k, match, match_index, exp_m, exp_i);
end else begin
$display("PASS search key=%0d -> match=%b idx=%0d", k, match, match_index);
end
end
endtask
initial begin
we=0; waddr=0; wkey=0; skey=0; errors=0;
@(negedge clk);
do_write(3'd0, 8'hA5);
do_write(3'd1, 8'h3C);
do_write(3'd2, 8'h77);
do_write(3'd5, 8'hFF);
// duplicate key at a higher index to show lowest-index priority
do_write(3'd6, 8'h3C);
do_search(8'hA5, 1'b1, 3'd0);
do_search(8'h77, 1'b1, 3'd2);
do_search(8'hFF, 1'b1, 3'd5);
do_search(8'h3C, 1'b1, 3'd1); // lowest index wins over idx 6
do_search(8'h00, 1'b0, 3'd0); // absent
do_search(8'h42, 1'b0, 3'd0); // absent
if (errors==0) $display("ALL TESTS PASSED");
else $display("TESTS FAILED: %0d", errors);
$finish;
end
endmodule
PASS search key=165 -> match=1 idx=0 PASS search key=119 -> match=1 idx=2 PASS search key=255 -> match=1 idx=5 PASS search key=60 -> match=1 idx=1 PASS search key=0 -> match=0 idx=0 PASS search key=66 -> match=0 idx=0 ALL TESTS PASSEDThe log confirms correct behavior across four distinct stored keys (indices 0, 2, 5 and the priority case) plus two absent keys. Note that key
0x3Cis written at both index 1 and index 6; the search returns index 1, proving the lowest-index priority select works. The two absent keys returnmatch=0, so a stuck-at or mis-compared bit would surface as a FAIL line.This variant extends the CAM to return the stored
dataword on a hit and to report ahit_countof how many entries matched the key. The priority-select still returns the lowest matching index and its data, while the counter accumulates every parallel match.module cam_data #( parameter ENTRIES = 8, parameter KEY_W = 8, parameter DATA_W = 8, parameter IDX_W = 3 )( input clk, input we, input [IDX_W-1:0] waddr, input [KEY_W-1:0] wkey, input [DATA_W-1:0] wdata, input [KEY_W-1:0] skey, output match, output [IDX_W-1:0] match_index, output [DATA_W-1:0] match_data, output [IDX_W:0] hit_count ); reg [KEY_W-1:0] keymem [0:ENTRIES-1]; reg [DATA_W-1:0] datamem [0:ENTRIES-1]; reg valid [0:ENTRIES-1]; integer i; initial begin for (i=0;i<ENTRIES;i=i+1) begin keymem[i]=0; datamem[i]=0; valid[i]=1'b0; end end always @(posedge clk) begin if (we) begin keymem[waddr]<=wkey; datamem[waddr]<=wdata; valid[waddr]<=1'b1; end end reg match_r; reg [IDX_W-1:0] idx_r; reg [DATA_W-1:0] data_r; reg [IDX_W:0] cnt_r; integer j; always @(*) begin match_r = 1'b0; idx_r = {IDX_W{1'b0}}; data_r = {DATA_W{1'b0}}; cnt_r = {(IDX_W+1){1'b0}}; for (j=ENTRIES-1;j>=0;j=j-1) begin if (valid[j] && (keymem[j]==skey)) begin match_r = 1'b1; idx_r = j[IDX_W-1:0]; // lowest matching index wins data_r = datamem[j]; cnt_r = cnt_r + 1'b1; // count all matches end end end assign match = match_r; assign match_index = idx_r; assign match_data = data_r; assign hit_count = cnt_r; endmodule module tb; localparam ENTRIES=8, KEY_W=8, DATA_W=8, IDX_W=3; reg clk, we; reg [IDX_W-1:0] waddr; reg [KEY_W-1:0] wkey, skey; reg [DATA_W-1:0] wdata; wire match; wire [IDX_W-1:0] match_index; wire [DATA_W-1:0] match_data; wire [IDX_W:0] hit_count; cam_data #(.ENTRIES(ENTRIES),.KEY_W(KEY_W),.DATA_W(DATA_W),.IDX_W(IDX_W)) dut ( .clk(clk),.we(we),.waddr(waddr),.wkey(wkey),.wdata(wdata), .skey(skey),.match(match),.match_index(match_index), .match_data(match_data),.hit_count(hit_count)); initial clk=0; always #5 clk=~clk; integer errors; task do_write(input [IDX_W-1:0] a, input [KEY_W-1:0] k, input [DATA_W-1:0] d); begin @(negedge clk); we=1; waddr=a; wkey=k; wdata=d; @(negedge clk); we=0; end endtask task chk(input [KEY_W-1:0] k, input em, input [IDX_W-1:0] ei, input [DATA_W-1:0] ed, input [IDX_W:0] ec); begin skey=k; #1; if (match!==em || hit_count!==ec || (em && (match_index!==ei || match_data!==ed))) begin errors=errors+1; $display("FAIL key=%0d got m=%b idx=%0d data=%0d cnt=%0d exp m=%b idx=%0d data=%0d cnt=%0d", k, match, match_index, match_data, hit_count, em, ei, ed, ec); end else begin $display("PASS key=%0d -> m=%b idx=%0d data=%0d cnt=%0d", k, match, match_index, match_data, hit_count); end end endtask initial begin we=0; waddr=0; wkey=0; wdata=0; skey=0; errors=0; @(negedge clk); do_write(3'd0, 8'h10, 8'hAA); do_write(3'd1, 8'h20, 8'hBB); do_write(3'd2, 8'h30, 8'hCC); do_write(3'd4, 8'h20, 8'hDD); // duplicate key 0x20 -> multi-match do_write(3'd7, 8'h20, 8'hEE); // triple match on key 0x20 chk(8'h10, 1'b1, 3'd0, 8'hAA, 4'd1); chk(8'h30, 1'b1, 3'd2, 8'hCC, 4'd1); chk(8'h20, 1'b1, 3'd1, 8'hBB, 4'd3); // lowest index idx1 wins, 3 hits chk(8'h99, 1'b0, 3'd0, 8'h00, 4'd0); // absent if (errors==0) $display("ALL TESTS PASSED"); else $display("TESTS FAILED: %0d", errors); $finish; end endmodulePASS key=16 -> m=1 idx=0 data=170 cnt=1
PASS key=48 -> m=1 idx=2 data=204 cnt=1
PASS key=32 -> m=1 idx=1 data=187 cnt=3
PASS key=153 -> m=0 idx=0 data=0 cnt=0
ALL TESTS PASSEDThe log proves single-match reads return the right index and data (key
0x10→data0xAA, key0x30→data0xCC), and that the triple-programmed key0x20(indices 1, 4, 7) returns the lowest index (1), its data (0xBB), andhit_count=3. The absent key returns match 0 with a zero count, so any miscount or wrong-priority bug would print FAIL.How a CAM works: search by content, not by address
A conventional RAM is addressed: you supply an address and it returns the data stored there. A CAM inverts this relationship. You supply data (the search key) and the CAM returns where that data lives, as a match line and an encoded index. Internally, each entry holds a stored key and a valid bit. On every search the key is broadcast to all entries, and each entry runs its own comparator; the match lines are combined by a priority encoder to produce the final index.
In the Verilog above, the parallel compare is the loop inside
always @(*). Every iteration is a separate comparator in hardware — the loop is unrolled by synthesis, not executed sequentially. Iterating from the highest index down to zero and overwriting the result on each hit makes the lowest matching index win, which is the usual priority convention. The write port is a normal synchronous memory write onposedge clk, so a CAM is essentially a RAM array bolted to a comparator-per-row plus a priority encoder.CAM vs RAM: the key difference
- RAM: address in → data out. One decoder drives one word line. Cheap and dense.
- CAM: data in → address (match) out. Every row compares simultaneously. Fast lookups, but far more logic.
Because a CAM compares all entries in a single cycle, lookup time is independent of the number of entries — it is O(1) in the array depth, whereas a software hash or linear scan is not. That constant-time search is the whole reason CAMs exist in hardware.
Where CAMs are used
CAMs show up wherever a single-cycle "does this value exist, and where?" lookup is on the critical path:
- TLBs and cache tags: a processor's translation lookaside buffer uses a CAM to match a virtual page number against all cached translations at once. Fully-associative caches match tags the same way.
- Network routing and switching: routers use CAMs (and ternary CAMs, TCAMs) for longest-prefix-match forwarding tables and MAC address lookup. A TCAM stores don't-care bits so a single entry can match a whole subnet.
- Lookup and classification tables: packet classifiers, ACLs, and any associative lookup where the search set is small but must be probed every cycle.
The parallel-compare area cost
The speed of a CAM is not free. A RAM needs one comparator-free bit cell per bit; a CAM needs a comparator at every bit of every row, plus match-line logic and a priority encoder. For an N-entry, K-bit CAM that is N×K comparators switching on every search, which dominates both area and dynamic power. This is why real CAMs/TCAMs are small and why TCAM-heavy routers are power-hungry.
In an FPGA, the behavioral loop in these examples synthesizes to LUT-based comparators and a wide OR/priority tree; it does not map to block RAM, so depth is limited by fabric. For large associative stores, designers often replace a true CAM with a hashed RAM lookup to trade exactness-of-timing for density.
Synthesis notes
The code is synthesizable: the write port is a clean synchronous write, and the search path is purely combinational. Keep
IDX_Wequal toceil(log2(ENTRIES)). Theinitialblock clears the valid bits for simulation; on an ASIC you need an explicit reset to clearvalid, sinceinitialis not guaranteed in silicon. On most FPGAs theinitialvalues are honored at configuration.Multi-match and priority
Real CAMs can have several entries matching one key. You must decide what a match means: return the first (priority CAM), return all (a one-hot match vector), or flag the conflict. The first example resolves ties by lowest index; the second additionally exposes
hit_countso firmware can detect duplicates. For a strict priority encoder, the lowest-index-wins loop is the idiomatic pattern. If you need the full set of matches, output the raw per-entry match vector instead of encoding it.Common mistakes
- Forgetting the valid bit: an uninitialized entry whose key happens to equal the search key will false-match. Always gate the compare with a valid bit.
- Wrong priority direction: iterating low-to-high and overwriting gives highest-index priority; iterate high-to-low (as here) for lowest-index priority.
- Latches on the search path: make sure every output is assigned on every path of the combinational block (the defaults at the top of
always @(*)prevent inferred latches).- Index width too small: if
IDX_Wcannot encodeENTRIES-1, the returned index silently wraps.FAQ
Is this content addressable memory Verilog code synthesizable?
Yes. Both modules use a synchronous write port and a combinational search, which map to registers/LUTs on an FPGA and standard cells on an ASIC. Add an explicit reset for the valid bits if you cannot rely on
initial.How is a CAM different from a hash table?
A hardware CAM gives an exact, constant-latency single-cycle lookup regardless of occupancy, at high area/power cost. A hash table trades a small, variable number of probes for much denser storage.
What is a TCAM?
A ternary CAM stores 0, 1, or don't-care per bit, so one entry can match a range of keys — the basis of longest-prefix-match routing. The examples here are binary CAMs; extend each entry with a mask to build a TCAM.
Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres