Parallel to serial converter

This VHDL module receives parallel data as input at it outputs the data in serial format.

This VHDL module receives parallel data from the data_in bus when load is asserted. One clock after load is de-asserted, the data is serially transmitted out on the data_out line, MSB first, and valid is also asserted. The frame signal is asserted together with the end of the transmission (i.e. when the LSB is transmitted).

busy signal informs the host on the parallel side that a transmission is ongoing.

I posted a previous version of this block on Reddit and received extensive remarks and help from user @asp_digital. The resulting code, generic and with lots of comments, is presented below:

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

entity par2ser is
  port (
    clk      : in  std_logic;

    -- inputs
    data_in  : in  std_logic_vector;
    load     : in  std_logic;

    -- outputs
    data_out : out std_logic;
    busy     : out std_logic := '0';
    frame    : out std_logic := '0'
  );
end par2ser;
architecture rtl of par2ser is
  signal cnt : natural range 0 to data_in'left;
  signal reg : std_logic_vector (data_in'left downto 0) := (others => '0');
  -- normalize the unconstrained input
  alias data_in_norm : std_logic_vector(data_in'length - 1 downto 0) is data_in; -- normalized unconstrained input

begin

  par2ser_pr : process (clk)
  begin
    if (rising_edge(clk)) then
      -- the bit counter, for framing, always counts down to zero and then stops.
      -- The counter is preset when the parallel register is loaded.
      BitCounter : if cnt > 0 then
        cnt <= cnt - 1;
      end if BitCounter;

      -- busy flag should be cleared when not shifting.
      -- It gets set immediately on load, overriding this.
      -- Busy can be cleared on the last clock of the current data transmision
      EndBusy : if cnt = 1 then
        busy <= '0';
      end if EndBusy;

      ParallelReg : if (load = '1') then
        reg <= data_in_norm; -- load parallel register
        cnt <= data_in_norm'left; -- size of the parallel register
        busy <= '1'; -- ensure immediate busy flag
      end if ParallelReg;

      -- frame strobe goes true when the count reaches zero, and is cleared right away.
      -- Let's take advantage of pipelining.
      AssertFrameStrobe : if cnt = 1 then
        frame <= '1';
      else
        frame <= '0';
      end if AssertFrameStrobe;

    end if;
  end process par2ser_pr;

  data_out <= reg(cnt);

end rtl;

The par2ser_pr process handles the load signal and updates the internal counter cnt to check when a transaction is complete. The process frame_pr is used to generate a single clock pulse when cnt achieves the value zero, indicating the end of the transaction.

In the waveforms below, several packets are sent for two instantiations of the parallel to serial converter (each one with different width):

In the first instantiation of the module, the input and output signals are shown separately. The second instantiation signals summary is shown after the last divider.

This implementation of the block generates a mux on line 57. As proposed by the same user, @asp_digital, I attach below an implementation that uses a shift register to convert the data from parallel to serial format:

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

entity par2ser_sr is
  port (
    clk      : in  std_logic;

    -- inputs
    data_in  : in  std_logic_vector;
    load     : in  std_logic;

    -- outputs
    data_out : out std_logic;
    busy     : out std_logic := '0';
    frame    : out std_logic := '0'
  );
end par2ser_sr;
architecture rtl of par2ser_sr is
  signal cnt : natural range 0 to data_in'left;
  signal reg : std_logic_vector (data_in'left downto 0) := (others => '0');

begin

  par2ser_pr : process (clk)
  begin
    if (rising_edge(clk)) then
      -- the bit counter, for framing, always counts down to zero and then stops.
      -- The counter is preset when the parallel register is loaded.
      BitCounter : if cnt > 0 then
        cnt <= cnt - 1;
        reg <= reg(data_in'left-1 downto 0) & '0';
      end if BitCounter;

      -- busy flag should be cleared when not shifting.
      -- It gets set immediately on load, overriding this.
      EndBusy : if cnt = 0 then
        busy <= '0';
      end if EndBusy;

      ParallelReg : if (load = '1') then
        reg <= data_in; -- load parallel register
        cnt <= data_in'left; -- size of the parallel register
        busy <= '1'; -- ensure immediate busy flag
      end if ParallelReg;

      -- frame strobe goes true when the count reaches zero, and is cleared right away.
      -- Let's take advantage of pipelining.
      AssertFrameStrobe : if cnt = 1 then
        frame <= '1';
      else
        frame <= '0';
      end if AssertFrameStrobe;

    end if;
  end process par2ser_pr;

  data_out <= reg(data_in'left);

end rtl;

As proposed exercises:

  • Write a testbench for the par2ser_sr module
  • Synthesize several sizes of the two blocks and compare which implementation takes fewer resources and can be implemented using a faster clock, the mux-based implementation, or the shit-register one.

The source codes (for mux base and for shift_register based implementations), testbench, and Vivado waveform file for simulation are available on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *