----------------------------------------------------------------------------------
-- Carrier-based pulse-width modulation (PWM) module
-- Documented in https://imperix.com/doc/implementation/fpga-pwm-modulator
----------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity UserCbPwm is

  Port (
    CLOCK_period : in std_logic_vector(15 downto 0);
    CLOCK_timer : in std_logic_vector(15 downto 0);
    CLOCK_prescaler : in std_logic_vector(15 downto 0);
    CLOCK_clk_en : in std_logic;

    -- must be between 0 and CLOCK_period
    -- loaded when the carrier reaches zero
    i_nextDutyCycle : in std_logic_vector(15 downto 0);
    i_enableDoubleRate : in std_logic;

    o_carrier : out std_logic_vector(15 downto 0);
    o_pwm : out std_logic;

    clk_250_mhz : in std_logic
  );
end UserCbPwm;

architecture impl of UserCbPwm is

  ATTRIBUTE X_INTERFACE_INFO : STRING;
  ATTRIBUTE X_INTERFACE_INFO of clk_250_mhz: SIGNAL is "xilinx.com:signal:clock:1.0 clk_250_mhz CLK";

  ATTRIBUTE X_INTERFACE_INFO of CLOCK_period:    SIGNAL is "imperix.ch:ix:clock_gen_rtl:1.0 CLOCK period";
  ATTRIBUTE X_INTERFACE_INFO of CLOCK_timer:     SIGNAL is "imperix.ch:ix:clock_gen_rtl:1.0 CLOCK timer";
  ATTRIBUTE X_INTERFACE_INFO of CLOCK_prescaler: SIGNAL is "imperix.ch:ix:clock_gen_rtl:1.0 CLOCK prescaler";
  ATTRIBUTE X_INTERFACE_INFO of CLOCK_clk_en:    SIGNAL is "imperix.ch:ix:clock_gen_rtl:1.0 CLOCK clk_en";

  attribute X_INTERFACE_MODE : string;
  attribute X_INTERFACE_MODE of CLOCK_timer : signal is "monitor";

  signal reg_Pwm : std_logic;
  signal reg_Carrier : unsigned(15 downto 0);

  signal reg_ClkEnable : std_logic;
  signal reg_HalfPeriodMinusOne : unsigned(15 downto 0);
  signal reg_DutyCycle : unsigned(15 downto 0);
  signal reg_DutyCyclePlusOne : unsigned(15 downto 0);

  signal reg_Timer : unsigned(15 downto 0);

  type t_CarrierStates is (COUNTING_UP, COUNTING_DOWN);
  signal reg_CarrierState: t_CarrierStates;

begin

  o_carrier <= std_logic_vector(reg_Carrier);
  o_pwm <= reg_Pwm;

  P_INPUT_SAMPLING : process(clk_250_mhz)
  begin
    if rising_edge(clk_250_mhz) then
      reg_Timer <= unsigned(CLOCK_timer);
      reg_ClkEnable <= CLOCK_clk_en;
      reg_HalfPeriodMinusOne <= shift_right(unsigned(CLOCK_period), 1) - 1;
      -- update the duty-cycle when the carrier hits zero
      if reg_Carrier = 0 or (i_enableDoubleRate = '1' and reg_Carrier = unsigned(CLOCK_period)) then
        reg_DutyCycle <= unsigned(i_nextDutyCycle);
        reg_DutyCyclePlusOne <= unsigned(i_nextDutyCycle) + 1;
      end if;
    end if;
  end process P_INPUT_SAMPLING;

  P_TRIANGLE_CARRIER: process(clk_250_mhz)
  begin
    if rising_edge(clk_250_mhz) then
      -- reg_ClkEnable serves to slow down the logic if the CLOCK_prescaler is used
      -- it is used only if the frequency is lower than 3.8 kHz
      if reg_ClkEnable = '1' then
        if reg_CarrierState = COUNTING_UP then
          reg_Carrier <= reg_Carrier + 2;
          if reg_Timer >= reg_HalfPeriodMinusOne then -- minus one because well go in counting down in next clock cycle
            reg_CarrierState <= COUNTING_DOWN;
          end if;
        else -- reg_CarrierState = COUNTING_DOWN
          if reg_Carrier >= 2 then
            reg_Carrier <= reg_Carrier - 2;
          end if;
          if reg_Timer = 0 then
            reg_CarrierState <= COUNTING_UP;
            reg_Carrier <= (others => '0');
          end if;
        end if;
      end if;
    end if;
  end process P_TRIANGLE_CARRIER;

  P_OUTPUT: process(clk_250_mhz)
  begin
    if rising_edge(clk_250_mhz) then
      if (reg_DutyCycle /= 0) AND (reg_DutyCyclePlusOne > reg_Carrier) then
        reg_Pwm <= '1';
      else
        reg_Pwm <= '0';
      end if;
    end if;
  end process P_OUTPUT;

end impl;