Saturation Counter

A regular VHDL unsigned VHDL counter will wrap around to zero when counting up from the maximum value it can represent. For example, a 3-bit counter, counting up, will have values 0, 1, 2, …, 14, 15, 0, 1, … Note that after reaching the maximum unsigned value it can represent (in this case, 15), the next value will be zero.

For most of the VHDL counters this behavior is accepted and useful. But there are some cases where this behavior is not wanted nor useful.

Suppose you included a counter to log exceptions or error conditions on your system. It may well be that you cannot know how many exceptions there will be. You put your system to work and after some time you read your counter and see the value ‘3’. Can you be sure that there were only three exceptions?

Not at all. If your counter is a 3-bit counter, it may have wrapped around. So if you read 3, it could be 3. But it could also be 19. Or 35. Or actually, any value of the form n x 16 + 3.

You could say that you may use a bigger counter, and in some applications that would be OK. But again, you could never be sure.

One way to be sure is to use a saturation counter. If you used a saturation counter, and you read 3, you can be sure that the quantity of exceptions was exactly 3. If the quantity was any number higher than 15, the reading would be 15.

Here is the code for the saturation counter:

library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

entity sat_cnt is
   generic (
      DATA_W:     natural := 32;
      CNT_MAX:    natural := 20
   );
   port (
      clk:        in std_logic;
      rst:        in std_logic;
      
      -- inputs
      sclr:       in std_logic;
      en:         in std_logic;
      
      data_out:   out std_logic_vector(DATA_W-1 downto 0)
  );
end sat_cnt;

architecture rtl of sat_cnt is
   signal   cnt : unsigned(DATA_W-1 downto 0);
   signal   max : std_logic;

begin 
   max      <= '1' when cnt = to_unsigned(CNT_MAX-1, cnt'length) else '0';
   data_out <= std_logic_vector(cnt);

   counter_pr: process (clk, rst) 
   begin 
      if (rst = '1') then 
         cnt <= (others => '0');
      elsif (rising_edge(clk)) then       
         if (sclr = '1') then                  -- sync clear
            cnt <= (others => '0');
         elsif (en = '1' and max = '0') then   -- enabled and maximum not reached?
            cnt <= cnt + 1;                    -- increment counter
         end if;  
      end if;
   end process;

end rtl;

The entity definition starts at line 5. The counter width DATA_W and the saturation value CNT_MAX are generic parameters (lines 7-8).

The sclr signal synchronously resets the counter to zero. This signal could be used to restart the counting at any time on run-time. The counter counts up only while the enable signal en is asserted.

The max signal is used as a flag in main the logic of the counter (lines 34-40). It sclr is asserted, the counter is cleared. If it is not, and enable is asserted (but the saturation value is not reached), the counter value increases at each rising edge of the clock.

When the counter reaches its saturation value, any further clock pulses have no effect on the value of the counter.

Let’ see some simulation waveforms. The simulation was done for MAX_CNT = 17:

sat_cnt1

On Cursor 1, rst is released. On Cursor 2, enable signal en is asserted so the counter starts counting up until en is de-asserted on Cursor 3.

sat_cnt2

On Cursor 4, the counter finally reaches MAX_CNT-1, so it remains at this value until sclr is asserted at Cursor 5. At Cursor 6 the counter reaches saturation again.

The sources, testbench files, waveform, simulation project files, etc., are released under Github.

Link for this project on Github

Link for all releases of this project under Github


Proposed exercise:

Change the code so the saturation value will be an input to the block, instead of a generic parameter