--[[
  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 not static[cpu] then
    static[cpu] = {
      numInstances = 0,
      instances = {},
      preFinalized = false,
      finalized = false,
      ps_protection_block = nil,
      updateMEPAdded = false,
      enableCodeGenerated = nil
    }
  end
  Epwm['instance'] = static[cpu].numInstances
  static[cpu].numInstances = static[cpu].numInstances + 1

  function Epwm:createImplicitEpwm(epwm, params, req)
    self.epwm = epwm
    
    U.enforceParamContract(
      params,
      {
        fsw = U.isReal,
        cbx_carrierType = U.isComboBox,
        opt_highResDutyModeActive = U.isBoolean,
        opt_highResPrdModeActive = U.isBoolean,
        polarityIsActiveHigh = U.isBoolean,
        outMode = {U.isFixedLengthArrayOf, 2, U.isString},
        opt_highResDeadTimeModeActive = U.isBoolean,
        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,
        
        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
    if self.params.opt_highResPrdModeActive then
      -- prd is integer part and HighRes ticks hold the fractional part
      self.prd = math.floor(timing.desiredPeriodAsFloat)
      self.prdHighRes = math.floor(((timing.desiredPeriodAsFloat - self.prd) * 256) + 0.5)
    else
      -- Non-HR uses the rounded period
      self.prd = timing.period
      self.prdHighRes = 0
    end
    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
      if self.params.opt_highResDeadTimeModeActive then
        self.rising_delay_counts = dr * globals.target.getDeadTimeClock()
        self.falling_delay_counts = df * globals.target.getDeadTimeClock()
      else
        self.rising_delay_counts = math.floor(dr * globals.target.getDeadTimeClock() + 0.5)
        self.falling_delay_counts = math.floor(df * globals.target.getDeadTimeClock() + 0.5)
      end
      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
      -- High resolution not available on this target
      if (self.params.opt_highResDutyModeActive or self.params.opt_highResPrdModeActive or self.params.opt_highResDeadTimeModeActive) then
        U.error('Target does not support high resolution modes.')
      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 num_hrpwms = globals.target.getTargetParameters()['hrpwms']['num_units']
      if (self.params.opt_highResDutyModeActive or self.params.opt_highResPrdModeActive or self.params.opt_highResDeadTimeModeActive)
      and (num_hrpwms == nil or num_hrpwms < epwm or epwm < 1) then
        U.error('PWM generator %d may not be configured for high resolution' % {epwm})
      end

      local pinconf = {}
      local gpios = {}
      local specifyGpioEnValue = Target.Variables['Pwm%iGpioEn' % {self.epwm}]
      for channel = 0, 1 do
        local channelChar = U.zeroIndexedIntToChar(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, channelChar}]
            if type(altgpio) ~= 'number' then
              U.error([[
                Invalid GPIO configuration for PWM%(epwm)i%(channelChar)s.
                Correct the setting under Coder Options -> Target -> PWM.
              ]] % {epwm = self.epwm, channelChar = channelChar})
            end
            gpio = altgpio
          end

          local gpioConfig
          if self:targetMatches({'2837x', '2838x'}) then
            gpioConfig = 'GPIO_%(gpio)i_EPWM%(epwm)i%(channelChar)s'
          elseif self:targetMatches({'2806x', '2833x', '28003x', '28004x', '280013x', '28P55x', '28P65x', '29H85x'}) then
            gpioConfig = 'GPIO_%(gpio)i_EPWM%(epwm)i_%(channelChar)s'
          else
            U.throwUnhandledTargetError()
          end
          local conf = gpioConfig % {gpio = gpio, epwm = self.epwm, channelChar = channelChar}
          if not globals.target.validateAlternateFunction(conf) then
            U.error([[
              PWM%(epwm)i%(channelChar)s cannot be connected to GPIO%(gpio)i.
              Correct the setting under Coder Options -> Target -> PWM.
            ]] % {gpio = gpio, epwm = self.epwm, channelChar = channelChar})
          end
          globals.target.allocateGpio(gpio, {}, req)
          table.insert(pinconf, conf)
          table.insert(gpios, gpio)
        end
      end
      globals.syscfg:addPeripheralEntry('epwm', {
        unitAsInt = epwm,
        pinconf = pinconf,
        gpios = gpios,
        core = globals.target.getTiCpuNumber(self.cpu),
      })
    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:generateTaskTriggerIsrCode(f)
    f.Declarations:append(globals.target.getEpwmIsrCode(self.epwm))
    f.Declarations:append(globals.target.getEpwmCheckOverrunCode(self.epwm))
    f.InterruptEnableCode:append(globals.target.getEpwmIsrEnableCode(self.epwm))
    f.PreInitCode:append(self:getIsrConfigCode())
  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

    local hrPrdCode = ''
    if self.params.opt_highResPrdModeActive then
      hrPrdCode =
         'params.reg.TBPRDHR = (uint16_t)%(prdHr)i << 8; // only higher 8 bits are used' %
         {
           prdHr = self.prdHighRes
         }
    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;
        %(hrPrdCode)s
      ]] % {
      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),
      hrPrdCode = hrPrdCode,
    })
    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

        -- High resolution period mode enforces shadowing
        if self.params.opt_highResPrdModeActive then
          if self.params.cbx_carrierType.equals('triangle') then
            self.params.opt_cmp_shadow_load = 'zp'
          else
            self.params.opt_cmp_shadow_load = 'p'
          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
        local cbx_tzMode
        if  static[self.cpu].ps_protection_block
        and not self.params.disableControlByProtectionBlock then
          cbx_tzMode = static[self.cpu].ps_protection_block:getTripZoneModeCombo(z)
        else 
          cbx_tzMode = U.newComboBox('ignore', {'ignore', 'cbc', 'osht'})
        end

        c.PreInitCode:append([[
          params.reg.TZSEL.bit.CBC%(z)d = %(cb_cbc)d;
          params.reg.TZSEL.bit.OSHT%(z)d = %(cb_osht)d;
          ]] % {
          z = z,
          cb_cbc = cbx_tzMode.equals('cbc') and 1 or 0,
          cb_osht = cbx_tzMode.equals('osht') and 1 or 0,
        })
      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:forceSafeEnabled() 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

    -- HRPWM params
    local highResParams = [[
      params.highResDutyEnabled = %s;
      params.highResPeriodEnabled = %s;
      params.highResDeadTimeEnabled = %s;
    ]] % {
      self.params.opt_highResDutyModeActive and self.params.opt_highResDutyModeActive or 0,
      self.params.opt_highResPrdModeActive and self.params.opt_highResPrdModeActive or 0,
      self.params.opt_highResDeadTimeModeActive and self.params.opt_highResDeadTimeModeActive or 0,
    }
    local highResDbConfig = ''
    if self.params.opt_highResDeadTimeModeActive then
      local redShadowHrMode
      local fedShadowHrMode
      -- Match shadowing to non-HR settings
      if self.params.opt_fed_shadow_load == 'z' then
        fedShadowHrMode = '0'
      elseif self.params.opt_fed_shadow_load == 'p' then
        fedShadowHrMode = '1'
      else
        fedShadowHrMode = '2'
      end

      if self.params.opt_red_shadow_load == 'z' then
        redShadowHrMode = '0'
      elseif self.params.opt_red_shadow_load == 'p' then
        redShadowHrMode = '1'
      else
        redShadowHrMode = '2'
      end
      highResDbConfig = [[
        params.reg.DBCTL.bit.HALFCYCLE = 1;
        params.reg.HRCNFG2.bit.CTLMODEDBFED = %(fedShadowHrMode)s;
        params.reg.HRCNFG2.bit.CTLMODEDBRED = %(redShadowHrMode)s;
        params.reg.HRCNFG2.bit.EDGMODEDB = 3;
      ]] % {
        fedShadowHrMode = fedShadowHrMode,
        redShadowHrMode = redShadowHrMode,
      }
    end
    if self.params.opt_highResPrdModeActive or
    (self.params.opt_highResDutyModeActive and self.params.cbx_carrierType.equals('triangle')) then
      if (not self.params.opt_sequence0) or self.params.opt_sequence0.defined_in_aq then
        U.error('Variable sequence and custom AQ sequence not supported with HRPWM.')
      end

      -- Negative sequence with sawtooth not supported for HR period
      if (self.params.cbx_carrierType.equals('sawtooth') and self.params.opt_sequence0.value == 0) then
        U.error('Cannot use high resolution period with negative or variable sequence.')
      end

      c.PreInitCode:append([[
        params.reg.HRCNFG.bit.EDGMODE = HR_BEP;
        params.reg.HRCNFG.bit.EDGMODEB = HR_BEP;
        params.reg.HRCNFG.bit.CTLMODE = 0;
        params.reg.HRCNFG.bit.CTLMODEB = 0;
        params.reg.HRCNFG.bit.HRLOAD = HR_CTR_ZERO_PRD;
        params.reg.HRCNFG.bit.HRLOADB = HR_CTR_ZERO_PRD;
        params.reg.HRCNFG.bit.AUTOCONV = 1;
        params.reg.HRPCTL.bit.HRPE = 1;
        %(highResDbConfig)s
        %(highResParams)s
      ]] % {
        highResDbConfig = highResDbConfig,
        highResParams = highResParams,
      })

    elseif self.params.opt_highResDutyModeActive then
      local chAUseFEP = true
      local chBUseFEP = false

      -- Flip if polarity is active low
      if not self.params.polarityIsActiveHigh then
        chAUseFEP = not chAUseFEP
      end

      -- Variable sequence not supported
      if (not self.params.opt_sequence0) or self.params.opt_sequence0.defined_in_aq then
        U.error('Variable sequence and custom AQ sequence not supported with HRPWM.')
      end

      -- Flip again if sequence is negative
      if self.params.opt_sequence0.value == 0 then
        chAUseFEP = not chAUseFEP
      end

      -- CHB opposite if complimentary, same if dual
      if self.params.outMode[2] == '!A' then
        chBUseFEP = not chAUseFEP
      else
        chBUseFEP = chAUseFEP
      end

      local chAEdgeMode = chAUseFEP and 'HR_FEP' or 'HR_REP'
      local chBEdgeMode = chBUseFEP and 'HR_FEP' or 'HR_REP'
    
      -- If output not enabled, don't set anything
      if self.params.outMode[1] == '' then
        chAEdgeMode = '0'
      end
      if self.params.outMode[2] == '' then
        chBEdgeMode = '0'
      end

      c.PreInitCode:append([[
        params.reg.HRCNFG.bit.EDGMODE = %(chAEdgeMode)s;
        params.reg.HRCNFG.bit.EDGMODEB = %(chBEdgeMode)s;
        params.reg.HRCNFG.bit.CTLMODE = 0;
        params.reg.HRCNFG.bit.CTLMODEB = 0;
        params.reg.HRCNFG.bit.HRLOAD = HR_CTR_ZERO;
        params.reg.HRCNFG.bit.HRLOADB = HR_CTR_ZERO;
        params.reg.HRCNFG.bit.AUTOCONV = 1;
        params.reg.HRPCTL.bit.HRPE = 0;
        %(highResDbConfig)s
        %(highResParams)s
      ]] %
        {
          chAEdgeMode = chAEdgeMode,
          chBEdgeMode = chBEdgeMode,
          highResDbConfig = highResDbConfig,
          highResParams = highResParams,
        })
    elseif self.params.opt_highResDeadTimeModeActive then
      c.PreInitCode:append([[
        params.reg.HRCNFG.bit.AUTOCONV = 1;
        %(highResDbConfig)s
        %(highResParams)s
      ]] % {
        highResDbConfig = highResDbConfig,
        highResParams = highResParams,
      })
    end

    -- Add SFO background call if HRPWM is used
    if not static[self.cpu].updateMEPAdded and
    (self.params.opt_highResDutyModeActive or self.params.opt_highResPrdModeActive) then
      c.BackgroundTaskCodeBlocks:append('PLXHAL_PWM_updateMEP();')
      static[self.cpu].updateMEPAdded = true
    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)f, %(fallCounts)f);
            ]] % {
          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 cbx_tripSignalGroups = static[self.cpu].ps_protection_block:getTripSignalGroupModeComboMap()
        local trip = globals.target.getTargetParameters().trip_groups
        local trip_combination_string = ''
        for group, cbx_tripSignal in pairs(cbx_tripSignalGroups) do
          if not cbx_tripSignal.equals('ignore') then
            if trip_combination_string == '' then
              trip_combination_string = 'EPWM_DC_COMBINATIONAL_TRIPIN%i' %
                 {trip[group]}
            else
              trip_combination_string = trip_combination_string..
                 '| EPWM_DC_COMBINATIONAL_TRIPIN%i' % {trip[group]}
            end
          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

      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
