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 PASSED

The log confirms correct behavior across four distinct stored keys (indices 0, 2, 5 and the priority case) plus two absent keys. Note that key 0x3C is written at both index 1 and index 6; the search returns index 1, proving the lowest-index priority select works. The two absent keys return match=0, so a stuck-at or mis-compared bit would surface as a FAIL line.

This variant extends the CAM to return the stored data word on a hit and to report a hit_count of 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
endmodule
PASS 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 PASSED

The log proves single-match reads return the right index and data (key 0x10→data 0xAA, key 0x30→data 0xCC), and that the triple-programmed key 0x20 (indices 1, 4, 7) returns the lowest index (1), its data (0xBB), and hit_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 on posedge 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_W equal to ceil(log2(ENTRIES)). The initial block clears the valid bits for simulation; on an ASIC you need an explicit reset to clear valid, since initial is not guaranteed in silicon. On most FPGAs the initial values 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_count so 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_W cannot encode ENTRIES-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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD