--[[
  Copyright (c) 2021-2023 by Plexim GmbH
  All rights reserved.

  A free license is granted to anyone to use this software for any legal
  non safety-critical purpose, incluEpwmg commercial applications, provided
  that:
  1) IT IS NOT USED TO DIRECTLY OR INDIRECTLY COMPETE WITH PLEXIM, and
  2) THIS COPYRIGHT NOTICE IS PRESERVED in its entirety.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
--]]

local U = require('common.utils')

local static = {}
local Module = {}

function Module.getBlock(globals, cpu)
  local Epwm = require('common.block').getBlock(globals, cpu)
  if static[cpu] == nil then
    static[cpu] = {
      numInstances = 0,
      instances = {},
      preFinalized = false,
      finalized = false,
      ps_protection_block = nil,
      enableCodeGenerated = nil
    }
  end
  Epwm['instance'] = static[cpu].numInstances
  static[cpu].numInstances = static[cpu].numInstances + 1

  function Epwm:createImplicit(epwm, params, req)
    self.epwm = epwm
    
    U.enforceParamContract(
      params,
      {
        fsw = U.isReal,
        cbx_carrierType = U.isComboBox, 
        polarityIsActiveHigh = U.isBoolean,
        outMode = {U.isFixedLengthArrayOf, 2, U.isString},
        showEnablePort = U.isBoolean,
        disableControlByProtectionBlock = U.isBoolean,
        opt_dead_time = U.isNonNegativeScalar,
        opt_dead_time_shadow_mode = U.isString,
        opt_rising_delay = U.isNonNegativeScalar,
        opt_falling_delay = U.isNonNegativeScalar,
        opt_red_shadow_load = U.isString,
        opt_fed_shadow_load = U.isString,

        trip_zone_settings = U.isTable, -- Deprecated Tab
        
        opt_peripheralSyncSrc = U.isString, -- this is a weird flag 'z' or 'p', perhaps we should change it to a cbx type?
        opt_phase0 = {U.isScalarInClosedInterval, 0, 1},
        opt_sequence0 = {
          defined_in_aq = U.isBoolean,
          value = U.isScalar,
        },
        opt_cmp_shadow_load = U.isString,

      })
    self.params = params -- Store all of the implicit params with the contract!!

    static[self.cpu].instances[self.epwm] = self.bid
    self:logLine('EPWM%i implicitly created.' % {self.epwm})

    local timing = globals.target.getPwmFrequencySettings(self.params.fsw,
                                                          self.params.cbx_carrierType)
    self.fsw_actual = timing.freq
    self.prd = timing.period
    self.periodInSysTicks = timing.periodInSysTicks

    if self.prd > 0xFFFF then
      U.error('Unable to achieve the desired PWM frequency (%f Hz is too low).' %
        {self.params.fsw})
    end

    -- deadtime config and checks
    if self.params.outMode[2] == 'B' then
      -- deadtime does not apply for dual (independent) channel mode
    else
      local dr = self.params.opt_rising_delay
      if dr == nil then
        dr = self.params.opt_dead_time
      end
      local df = self.params.opt_falling_delay
      if df == nil then
        df = self.params.opt_dead_time
      end
      if dr < 0 or df < 0 then
        U.error('Blanking time must be a positive value.')
      end
      self.rising_delay_counts = math.floor(dr * globals.target.getDeadTimeClock())
      self.falling_delay_counts = math.floor(df * globals.target.getDeadTimeClock())
      local delay_max_counts
      if self:targetMatches({'2806x', '2833x'}) then
        delay_max_counts = 0x3FF   -- 10 bit
      elseif self:targetMatches({'2837x', '2838x', '28003x', '28004x', '280013x', '28P55x', '28P65x', '29H85x'}) then
        delay_max_counts = 0x3FFF  -- 14 bit
      else
        U.throwUnhandledTargetError()
      end
      if self.rising_delay_counts > delay_max_counts or self.falling_delay_counts > delay_max_counts then
        U.error('Blanking time cannot exceed %e seconds.' % {delay_max_counts / globals.target.getDeadTimeClock()})
      end
    end

    req:add('PWM', epwm)

    if self:targetUsesPinsets() then
      local p = globals.target.getTargetParameters()['epwms']['gpio'][epwm]
      if p == nil then
        U.error('PWM generator %d is not available for this target device.' %
          {epwm})
      end
      local pins = {}
      if self.params.outMode[1] ~= '' then
        globals.target.allocateGpio(p[1], {}, req)
        pins[1] = 2 * (epwm - 1)
      end
      if self.params.outMode[2] ~= '' then
        globals.target.allocateGpio(p[2], {}, req)
        pins[2] = 2 * (epwm - 1) + 1
      end
    else  -- target uses driverlib
      local num_units = globals.target.getTargetParameters()['epwms']['num_units']
      if num_units == nil or num_units < epwm or epwm < 1 then
        U.error('PWM generator %d is not available for this target device.' %
          {epwm})
      end

      local pinconf = {}
      local gpios = {}
      local specifyGpioEnValue = Target.Variables['Pwm%iGpioEn' % {self.epwm}]
      for channel = 0, 1 do
        local channelLetter = string.char(65 + channel)
        if self.params.outMode[channel + 1] ~= '' then
          local gpio = 2 * (self.epwm - 1) + channel  -- default gpio assignment
          if specifyGpioEnValue ~= nil and specifyGpioEnValue == 1 then
            local altgpio = Target.Variables['Pwm%i%sGpio' % {self.epwm, channelLetter}]
            if type(altgpio) ~= 'number' then
              U.error([[
                Invalid GPIO configuration for PWM%(epwm)i%(channelLetter)s.
                Correct the setting under Coder Options -> Target -> PWM.
              ]] % {epwm = self.epwm, channelLetter = channelLetter})
            end
            gpio = altgpio
          end

          local gpioConfig
          if self:targetMatches({'2837x', '2838x'}) then
            gpioConfig = 'GPIO_%(gpio)i_EPWM%(epwm)i%(channelLetter)s'
          elseif self:targetMatches({'2806x', '2833x', '28003x', '28004x', '280013x', '28P55x', '28P65x', '29H85x'}) then
            gpioConfig = 'GPIO_%(gpio)i_EPWM%(epwm)i_%(channelLetter)s'
          else
            U.throwUnhandledTargetError()
          end
          local conf = gpioConfig % {gpio = gpio, epwm = self.epwm, channelLetter = channelLetter}
          if not globals.target.validateAlternateFunction(conf) then
            U.error([[
              PWM%(epwm)i%(channelLetter)s cannot be connected to GPIO%(gpio)i.
              Correct the setting under Coder Options -> Target -> PWM.
            ]] % {gpio = gpio, epwm = self.epwm, channelLetter = channelLetter})
          end
          globals.target.allocateGpio(gpio, {}, req)
          table.insert(pinconf, conf)
          table.insert(gpios, gpio)
        end
      end
      globals.syscfg:addEntry('epwm', {
        unit = epwm,
        pinconf = pinconf,
        gpios = gpios,
        core = self.cpu + 1,
      })
    end
  end

  function Epwm:configureSocEvents(params)
    if params.soc_prd >
    globals.target.getTargetParameters()['epwms']['max_event_period'] then
      self:logLine('EXCEPTION: Excessive SOC trigger divider value.')  -- FIX ME: ?? return nothing?
      return
    end
    self.soc_loc = params.soc_loc
    self.soc_prd = params.soc_prd
    self.soc_delayed = params.soc_delayed
  end

  function Epwm:configureInterruptEvents(params)
    if params.int_prd >
    globals.target.getTargetParameters()['epwms']['max_event_period'] then
      self:logLine('EXCEPTION: Excessive interrupt trigger divider value.')  -- FIX ME: ?? return nothing?
      return
    end
    self.int_loc = params.int_loc
    self.int_prd = params.int_prd
    self.isr = params.isr
  end

  function Epwm:configureSync(sync)
    self.sync = sync
  end

  -- sync could be nil.
  function Epwm:getOptSync()
    return self.sync
  end

  function Epwm:configureCycleByCycleTrip(cbc_trip)
    self.cbc_trip = cbc_trip
  end

  function Epwm:checkMaskParameters()
    U.error('Explicit use of EPWM via target block not supported.')
  end

  function Epwm:p_getDirectFeedthroughCode()
    U.error('Explicit use of EPWM via target block not supported.')
  end

  function Epwm:getEnableCode()  -- must be called from nondirect feedthrough code
    if static[self.cpu].ps_protection_block == nil then
      -- see if there is a powerstage protection block in the circuit
      static[self.cpu].ps_protection_block = self:getBlockInstance('powerstage')
    end

    if static[self.cpu].enableCodeGenerated == nil then
      static[self.cpu].enableCodeGenerated = true
      if static[self.cpu].ps_protection_block ~= nil then
        return static[self.cpu].ps_protection_block:getEnableCode()
      else
        return 'PLXHAL_PWM_enableAllOutputs();'
      end
    end
  end

  function Epwm:getIsrConfigCode()
    return globals.target.getEpwmIsrConfigCode(self.epwm, {
      isr = self.isr
    })
  end

  function Epwm:finalizeThis(c)

    self.EPWMi_BASE = 'EPWM%i_BASE' % {self.epwm}
    
    local outModeStr = 'DUAL'
    if     self.params.outMode[1] == ''
    and    self.params.outMode[2] == '' then
      outModeStr = 'DISABLED'
    elseif self.params.outMode[2] == '' then
      outModeStr = 'SINGLE'
    end

    c.PreInitCode:append([[
      // configure PWM%(pwm)i at %(fswActual).1f Hz in %(carrierTypeStr)s mode
      // %(socLocComment)s %(intLocComment)s %(fswActComment)s
      {
        PLX_PWM_Params_t params;
        PLX_PWM_setDefaultParams(&params);
        params.outMode = PLX_PWM_OUTPUT_MODE_%(outModeStr)s;
        params.reg.TBPRD = %(prd)i;
        params.reg.TBCTL.bit.CTRMODE = %(carrierTypeReg)i;
      ]] % {
      pwm = self.epwm,
      fswActual = self.fsw_actual,
      carrierTypeStr = self.params.cbx_carrierType.asString(),
      socLocComment = self.soc_loc and "soc='%s' " % {self.soc_loc} or '',
      intLocComment = self.int_loc and "int='%s' " % {self.int_loc} or '',
      fswActComment = (self.fsw_actual ~= self.params.fsw) and 'desired frequency was %1.f Hz ' % {self.params.fsw} or '',
      outModeStr = outModeStr,
      prd = self.prd,
      carrierTypeReg = (self.params.cbx_carrierType.equals('triangle') and 2 or 0),
    })
    if self.params.outMode[1] ~= '' or self.params.outMode[2] ~= '' then
      if self.params.opt_cmp_shadow_load then
        function getCcCtrString(mode)
          if mode == 'z' then
            return 'CC_CTR_ZERO'
          elseif mode == 'p' then
            return 'CC_CTR_PRD'
          else
            return 'CC_CTR_ZERO_PRD'
          end
        end
  
        c.PreInitCode:append(
          'params.reg.CMPCTL.bit.LOADAMODE = %s;' % {getCcCtrString(self.params.opt_cmp_shadow_load)})
        if self.params.outMode[2] == 'B' then
          c.PreInitCode:append(
            'params.reg.CMPCTL.bit.LOADBMODE = %s;' % {getCcCtrString(self.params.opt_cmp_shadow_load)})
        end
      end

      if self.params.polarityIsActiveHigh then
        c.PreInitCode:append('// active state is high')
        if self.params.outMode[2] ~= 'B' then
          c.PreInitCode:append('params.reg.DBCTL.bit.POLSEL = 2; // DB_ACTV_HIC')
        else
          c.PreInitCode:append('params.reg.DBCTL.bit.POLSEL = 0; // DB_ACTV_HI')
        end
      else
        c.PreInitCode:append('// active state is low')
        if self.params.outMode[2] ~= 'B' then
          c.PreInitCode:append('params.reg.DBCTL.bit.POLSEL = 1; // DB_ACTV_LOC')
        else
          c.PreInitCode:append('params.reg.DBCTL.bit.POLSEL = 3; // DB_ACTV_LO')
        end
      end
      c.PreInitCode:append('// enable deadtime insertion')
      c.PreInitCode:append('params.reg.DBCTL.bit.OUT_MODE = 3; // DB_FULL_ENABLE')
      if self.params.outMode[2] == '' or self.params.outMode[2] == '!A' then
        c.PreInitCode:append('params.reg.DBCTL.bit.IN_MODE = 0; // DBA_ALL')
      else
        c.PreInitCode:append('params.reg.DBCTL.bit.IN_MODE = 2; // DBA_RED_DBB_FED')
      end

      c.PreInitCode:append('// TZ settings')
      for z = 1, 3 do
        -- note: trip_zone_settings is deprecated (self-destructs on PWM and PWM Var blocks)
        if (self.params.trip_zone_settings[z] == nil) and static[self.cpu].ps_protection_block ~= nil and not self.params.disableControlByProtectionBlock then
          self.params.trip_zone_settings[z] = static[self.cpu].ps_protection_block:getTripZoneMode(z)
        end
        if self.params.trip_zone_settings[z] == 'cbc' then
          c.PreInitCode:append('params.reg.TZSEL.bit.CBC%i = 1;' % {z})
          c.PreInitCode:append('params.reg.TZSEL.bit.OSHT%i = 0;' % {z})
        elseif self.params.trip_zone_settings[z] == 'osht' then
          c.PreInitCode:append('params.reg.TZSEL.bit.CBC%i = 0;' % {z})
          c.PreInitCode:append('params.reg.TZSEL.bit.OSHT%i = 1;' % {z})
        else
          c.PreInitCode:append('params.reg.TZSEL.bit.CBC%i = 0;' % {z})
          c.PreInitCode:append('params.reg.TZSEL.bit.OSHT%i = 0;' % {z})
        end
      end

      local tzsafe
      -- OSHT
      if static[self.cpu].ps_protection_block ~= nil and not self.params.disableControlByProtectionBlock then
        if static[self.cpu].ps_protection_block:getParameter('force_safe') == true then
          if self.params.polarityIsActiveHigh then
            tzsafe = 2
          else
            tzsafe = 1
          end
        else
          tzsafe = 0
        end
      end

      -- CBC
      if (self.cbc_trip ~= nil) and self:targetMatches({'2837x'}) then
        -- cbc control on 2837x relies on trip zone (as no EPWM_AQ_TRIGGER_EVENT_TRIG_DC_EVTFILT)
        -- must override global setting from powerstage protection block
        if self.params.polarityIsActiveHigh then
          tzsafe = 2
        else
          tzsafe = 1
        end
      end

      if tzsafe then
        if tzsafe == 2 then
          c.PreInitCode:append('// force low when tripped')
        elseif tzsafe == 1 then
          c.PreInitCode:append('// force high when tripped')
        else
          c.PreInitCode:append('// float output when tripped')
        end
        c.PreInitCode:append([[
          params.reg.TZCTL.bit.TZA = %(tzsafe)i;
          params.reg.TZCTL.bit.TZB = %(tzsafe)i;]] % {tzsafe = tzsafe})
      end
    end

    c.PreInitCode:append('PLX_PWM_configure(EpwmHandles[%i], %i, &params);' %
      {self['instance'], self.epwm})

    if self.params.outMode[1] ~= '' or self.params.outMode[2] ~= '' then
      if static[self.cpu].ps_protection_block ~= nil and not self.params.disableControlByProtectionBlock then
        c.PostInitCode:append(
          'PLX_PWR_registerPwmChannel(EpwmHandles[%i]);' %
          {self['instance']})
      end
      if self.rising_delay_counts and self.falling_delay_counts then
        if self.params.opt_dead_time_shadow_mode or self.params.opt_red_shadow_load or self.params.opt_fed_shadow_load then
          function getOnCntrString(mode)
            if mode == 'z' then
              return 'ON_CNTR_ZERO'
            elseif mode == 'p' then
              return 'ON_CNTR_PERIOD'
            else
              return 'ON_CNTR_ZERO_PERIOD'
            end
          end

          if self.params.opt_dead_time_shadow_mode then
            c.PreInitCode:append(
              'EPWM_setDeadBandControlShadowLoadMode(%(EPWMi_BASE)s, EPWM_DB_LOAD_%(on_ctr)s);' 
              % {
                EPWMi_BASE = self.EPWMi_BASE,
                on_ctr = getOnCntrString(self.params.opt_dead_time_shadow_mode),
              })
          end
          if self.params.opt_red_shadow_load then
            c.PreInitCode:append(
              'EPWM_setRisingEdgeDelayCountShadowLoadMode(%(EPWMi_BASE)s, EPWM_RED_LOAD_%(on_ctr)s);' 
              % {
                EPWMi_BASE = self.EPWMi_BASE,
                on_ctr = getOnCntrString(self.params.opt_red_shadow_load),
              })
          end
          if self.params.opt_fed_shadow_load then
            c.PreInitCode:append(
              'EPWM_setFallingEdgeDelayCountShadowLoadMode(%(EPWMi_BASE)s, EPWM_FED_LOAD_%(on_ctr)s);' % {
                EPWMi_BASE = self.EPWMi_BASE,
                on_ctr = getOnCntrString(self.params.opt_fed_shadow_load),
              })
          end
        end
        c.PreInitCode:append([[
          // configure deadtime to %(riseSec)e/%(fallSec)e seconds (rising/falling)
          PLX_PWM_setDeadTimeCounts(EpwmHandles[%(instance)i], %(riseCounts)i, %(fallCounts)i);
          ]] % {
          riseSec = self.rising_delay_counts / globals.target.getDeadTimeClock(),
          fallSec = self.falling_delay_counts / globals.target.getDeadTimeClock(),
          instance = self.instance, 
          riseCounts = self.rising_delay_counts, 
          fallCounts = self.falling_delay_counts,
        })
      end
      if not self.params.opt_sequence0 then
        -- the sequence is variable, and therefore we initialize all events to 'no action'
        c.PreInitCode:append([[
          // initialize all AQ events to "no action"
          PLX_PWM_setSequenceAq(EpwmHandles[%(instance)i], 0);
        ]] % {
          instance = self.instance,
        })
      else
        if not self.params.opt_sequence0.defined_in_aq then
          c.PreInitCode:append([[
            // PWM sequence starting with %(seqComment)s state
            PLX_PWM_setSequence(EpwmHandles[%(instance)i], %(sequence)i);
          ]] % {
            seqComment = (self.params.opt_sequence0.value == 0) and 'passive' or 'active',
            instance = self.instance,
            sequence = self.params.opt_sequence0.value,
          })
        else

          local comment = ''
          do  -- prepare comment for generated C code
            local actions = {[0] = 'NONE', 'CLR', 'SET', 'TGL'}
            -- local events = {  
              
            --   'Z', 'P', 'CAU', 'CAD', 'CBU', 'CBD', 'RVD', 'RVD', 
            --   'T1U', 'T1D', 'T2U', 'T2D'}

            -- each event in the register can be associated with one of the above actions
            local events = {
              'RVD', 'RVD', 'RVD', 'RVD', 'T2D', 'T2U', 'T1D', 'T1U', -- AQCTLx2
              'RVD', 'RVD', 'CBD', 'CBU', 'CAD', 'CAU', 'PRD', 'ZRO', -- AQCTLx
            }
            local mask = self.params.opt_sequence0.value
            if mask < 0 then
              U.error('Mask value is negative: '..mask)
            end
            local eventIdx = 1
            local warning = ''
            while mask ~= 0 do
              local actionBits = (mask & 0xC0000000) >> 30  -- we mask from the top to generate the comment in the desired order.
              local event = events[eventIdx]

              if actionBits ~= 0 then
                local action = actions[actionBits]
                comment = comment..' %i: %s=%s' % {eventIdx, event, action}
                if  not self.params.cbx_carrierType.equals('triangle')  -- down events only supported in triangle mode.
                and (event == 'CAD'
                  or event == 'CBD'
                  or event == 'T1D'
                  or event == 'T2D') -- add note that no T2D events are suported
                or  event == 'RVD' then
                  warning = warning..' %s=%s' % {event, action}
                end
              end
              eventIdx = eventIdx + 1
              mask = mask << 2 -- actions are 2 bits wide
            end
            if warning ~= '' then
              U.warning('The following event(s) will be ignored for PWM %i: %s.' % {self.epwm, warning})
            end
          end  -- end comment generation code
          
          c.PreInitCode:append([[
            // PWM sequence with custom AQ settings: %(comment)s
            PLX_PWM_setSequenceAq(EpwmHandles[%(instance)i], %(sequence)i);
          ]] % {
            comment = comment,
            instance = self.instance,
            sequence = self.params.opt_sequence0.value,
          })
        end

        if self.params.showEnablePort then
          -- if using forcing, configure shadow behavior
          c.PreInitCode:append(
            'PLX_PWM_prepareSetOutToXTransition(EpwmHandles[%i]);' %
            {self['instance']})
        end
      end
    end

    if self.params.opt_phase0 then
      c.PreInitCode:append(
        'PLX_PWM_setPhase(EpwmHandles[%i], %f);' % {self.instance, self.params.opt_phase0})
    end

    local SOCASEL
    if self['soc_loc'] ~= nil then
      if self['soc_loc'] == 'z' then
        if not self.soc_delayed then
          SOCASEL = 1
        else
          SOCASEL = 4
        end
      elseif self['soc_loc'] == 'p' then
        if not self.soc_delayed then
          SOCASEL = 2
        else
          SOCASEL = 5
        end
      elseif self['soc_loc'] == 'zp' then
        SOCASEL = 3
      end
    end

    local INTSEL
    if self['int_loc'] ~= nil then
      if self.int_loc == 'p' then
        INTSEL = 2
      elseif self.int_loc == 'zp' then
        INTSEL = 3
      else
        INTSEL = 1  -- default value for ETSEL.bit.INTSEL is ET_CTR_ZERO
      end
    end

    c.PreInitCode:append(globals.target.getEpwmSetupCode(self.epwm, {
      soca_sel = SOCASEL,
      soca_prd = self.soc_prd,
      int_sel = INTSEL,
      int_prd = self.int_prd,
      sync = self.sync
    }))

    -- synchronization from CMPSS via Digital Compare
    if (self.sync ~= nil) and (self.sync.tripi_sel ~= nil) then
      c.PreInitCode:append([[
        EPWM_setTripZoneDigitalCompareEventCondition(%(EPWMi_BASE)s,
                                                    EPWM_TZ_DC_OUTPUT_B1,
                                                    EPWM_TZ_EVENT_DCXL_HIGH);
        EPWM_enableDigitalCompareTripCombinationInput(%(EPWMi_BASE)s,
                                                    EPWM_DC_COMBINATIONAL_TRIPIN%(trip)i,
                                                    EPWM_DC_TYPE_DCBL);
        EPWM_enableDigitalCompareSyncEvent(%(EPWMi_BASE)s, EPWM_DC_MODULE_B);
      ]] % {
        EPWMi_BASE = self.EPWMi_BASE,
        trip = self.sync.tripi_sel,
      })
    end

    if self.params.outMode[1] ~= '' or self.params.outMode[2] ~= '' then
      -- at least one output of the PWM pair is active
      if static[self.cpu].ps_protection_block ~= nil and not self.params.disableControlByProtectionBlock then
        local trip_signal_groups = static[self.cpu].ps_protection_block:getTripSignalGroupModes()
        if trip_signal_groups ~= nil then
          local trip = globals.target.getTargetParameters().trip_groups
          local trip_combination_string = ''
          for grp, _ in pairs(trip_signal_groups) do
            if trip_combination_string == '' then
              trip_combination_string = 'EPWM_DC_COMBINATIONAL_TRIPIN%i' % {trip[grp]}
            else
              trip_combination_string = trip_combination_string..'| EPWM_DC_COMBINATIONAL_TRIPIN%i' % {trip[grp]}
            end
          end

          if trip_combination_string ~= '' then
            c.PreInitCode:append([[
              EPWM_setTripZoneDigitalCompareEventCondition(%(EPWMi_BASE)s,
                                                            EPWM_TZ_DC_OUTPUT_A1,
                                                            EPWM_TZ_EVENT_DCXH_HIGH);
              EPWM_enableDigitalCompareTripCombinationInput(%(EPWMi_BASE)s,
                                                            %(trips)s,
                                                            EPWM_DC_TYPE_DCAH);

              EPWM_setDigitalCompareEventSource(%(EPWMi_BASE)s,
                                                EPWM_DC_MODULE_A,
                                                EPWM_DC_EVENT_1,
                                                EPWM_DC_EVENT_SOURCE_ORIG_SIGNAL);

              EPWM_enableTripZoneSignals(%(EPWMi_BASE)s, EPWM_TZ_SIGNAL_DCAEVT1);
            ]] % {
              EPWMi_BASE = self.EPWMi_BASE,
              trips = trip_combination_string,
            })
          end
        end
      end

      if self.cbc_trip ~= nil then
        local blanking_event_zp
        if self.cbc_trip.cbc_blanking_start_event == 'z' then
          blanking_event_zp = 'ZERO'
        elseif self.cbc_trip.cbc_blanking_start_event == 'p' then
          blanking_event_zp = 'PERIOD'
        else
          blanking_event_zp = 'ZERO_PERIOD'
        end

        local dcCode = [[
          EPWM_setTripZoneDigitalCompareEventCondition(%(EPWMi_BASE)s,
                                                      EPWM_TZ_DC_OUTPUT_B2,
                                                      EPWM_TZ_EVENT_DCXH_HIGH);
          EPWM_enableDigitalCompareTripCombinationInput(%(EPWMi_BASE)s,
                                                      |<TRIPS>|,
                                                      EPWM_DC_TYPE_DCBH);

          EPWM_setDigitalCompareFilterInput(%(EPWMi_BASE)s, EPWM_DC_WINDOW_SOURCE_DCBEVT2);
          EPWM_setDigitalCompareBlankingEvent(%(EPWMi_BASE)s, EPWM_DC_WINDOW_START_TBCTR_%(blanking_event_zp)s);
          EPWM_setDigitalCompareWindowOffset(%(EPWMi_BASE)s, |<BLANKING_OFFSET>|);
          EPWM_setDigitalCompareWindowLength(%(EPWMi_BASE)s, |<BLANKING_WINDOW>|);
          EPWM_enableDigitalCompareBlankingWindow(%(EPWMi_BASE)s);

          EPWM_setDigitalCompareEventSource(%(EPWMi_BASE)s,
                                            EPWM_DC_MODULE_B,
                                            EPWM_DC_EVENT_2,
                                            EPWM_DC_EVENT_SOURCE_FILT_SIGNAL);

          EPWM_setDigitalCompareEventSyncMode(%(EPWMi_BASE)s,
                                              EPWM_DC_MODULE_B,
                                              EPWM_DC_EVENT_2,
                                              EPWM_DC_EVENT_INPUT_SYNCED);
        ]] % {
          EPWMi_BASE = self.EPWMi_BASE,
          blanking_event_zp = blanking_event_zp
        }
        if self:targetMatches({'2837x'}) then
          -- CBC control on 2837x relies on trip zone (as no EPWM_AQ_TRIGGER_EVENT_TRIG_DC_EVTFILT)
          if self.params.cbx_carrierType.equals('triangle') then
            U.error('CBC with symmetrical carrier not supported for this device.')
          end
          dcCode = dcCode..[[
            EPWM_enableTripZoneSignals(%(EPWMi_BASE)s, EPWM_TZ_SIGNAL_DCBEVT2);
          ]] % {EPWMi_BASE = self.EPWMi_BASE,}
        elseif self:targetMatches({'2806x', '2833x', '2838x', '28003x', '28004x', '280013x', '28P55x', '28P65x', '29H85x'}) then
      
          -- realize CBC with T1 event
          dcCode = dcCode..[[
            EPWM_setActionQualifierT1TriggerSource(%(EPWMi_BASE)s,
                                                    EPWM_AQ_TRIGGER_EVENT_TRIG_DC_EVTFILT);                
          ]] % {EPWMi_BASE = self.EPWMi_BASE,}
        else
          U.throwUnhandledTargetError()
        end

        dcCode = string.gsub(
          dcCode, '|<TRIPS>|',
          'EPWM_DC_COMBINATIONAL_TRIPIN%i' % {self.cbc_trip.input})

        local blanking_time = math.floor(self.prd * self.cbc_trip.blanking_time_pu + 0.5)
        dcCode = string.gsub(dcCode, '|<BLANKING_WINDOW>|', '%i' % {blanking_time})

        local blanking_delay = math.floor(self.prd * self.cbc_trip.blanking_delay_pu + 0.5)
        if blanking_delay < 0 then
          blanking_delay = blanking_delay + self.prd
        end
        dcCode = string.gsub(dcCode, '|<BLANKING_OFFSET>|', '%i' % {blanking_delay})

        c.PreInitCode:append(dcCode)
      end
    end

    if self.params.opt_peripheralSyncSrc then
      c.PreInitCode:append([[
            HRPWM_setSyncPulseSource(%(EPWMi_BASE)s, HRPWM_PWMSYNC_SOURCE_%(source)s);
        ]] % {
        EPWMi_BASE = self.EPWMi_BASE,
        source = self.params.opt_peripheralSyncSrc == 'z' and 'ZERO' or 'PERIOD',
      })
    end

    c.PreInitCode:append('}')
  end

  function Epwm:preFinalize()
    if static[self.cpu].preFinalized then
      return {}
    end

    -- configure synchronization options
    -- this has to be done at the very end as synchronization chains can
    -- can be composed of multiple blocks and blocks of different type
    for _, b in ipairs(globals.instances) do
      if b.configurePwmSynchronizationChain then
        b:configurePwmSynchronizationChain()
      end
    end
    static[self.cpu].preFinalized = true
  end

  function Epwm:finalize(c)
    if static[self.cpu].finalized then
      return
    end

    c.Include:append('plx_pwm.h')
    if self:targetUsesDriverLib() then
      -- includes for driverlib targets
      c.Include:append('epwm.h')
      c.Include:append('hrpwm.h')
    end

    c.Declarations:append('PLX_PWM_Handle_t EpwmHandles[%i];' %
      {static[self.cpu].numInstances})
    c.Declarations:append('PLX_PWM_Obj_t EpwmObj[%i];' % {static[self.cpu].numInstances})

    -- see if the model contains a powerstage block
    local powerstage_obj = self:getBlockInstance('powerstage')

    -- see if the model contains a pil block
    local pil_obj = self:getGlobalBlockInstance('pil')

    if powerstage_obj == nil then
      if pil_obj ~= nil then
        c.Declarations:append('bool EpwmForceDisable = false;')
      end
      c.Declarations:append('void PLXHAL_PWM_enableAllOutputs(){')
      if pil_obj ~= nil then
        c.Declarations:append('  if(!EpwmForceDisable){')
      end
      for _, bid in U.pairsSorted(static[self.cpu].instances) do
        local epwm = globals.instances[bid]
        c.Declarations:append('    PLX_PWM_enableOut(EpwmHandles[%i]);' %
          epwm:getObjIndex())
      end
      if pil_obj ~= nil then
        c.Declarations:append('  }')
      end
      c.Declarations:append('}')
    end

    local code = [[
      PLX_PWM_sinit();
      for (int i = 0; i < %d; i++) {
        EpwmHandles[i] = PLX_PWM_init(&EpwmObj[i], sizeof(EpwmObj[i]));
      }
    ]]
    c.PreInitCode:append(code % {static[self.cpu].numInstances})

    for _, bid in U.pairsSorted(static[self.cpu].instances) do
      local epwm = globals.instances[bid]
      if epwm:getCpu() == self.cpu then
        epwm:finalizeThis(c)
      end
    end

    if self:targetUsesPinsets() then
      -- for driver lib targets, sync code is handled in syscfg module
      c.TimerSyncCode:append(globals.target.getEpwmTimersSyncCode())
    end

    static[self.cpu].finalized = true
  end

  return Epwm
end

return Module
