--[[
  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 EpwmVar = require('blocks.epwm_common').getBlock(globals, cpu)
  if not static[cpu] then
    static[cpu] = {
      numInstances = 0,
      numChannels = 0,
      instances = {},
      finalized = false,
    }
  end
  EpwmVar['instance'] = static[cpu].numInstances
  static[cpu].numInstances = static[cpu].numInstances + 1

  function EpwmVar:checkMaskParameters()
    -- this list corresponds to the PWM (Variable) block interface
    self.inIdx = U.enum({'m', 'ph', 'SyncIn', 'f', 'en', 'd_rf', 'seq',})
    self.outIdx = U.enum({'task', 'ADC', 'SyncOut', 'P'})

    -- Now you can access with named indexes and abstract this block interface.
    -- Block.InputSignal[1] --> Block.InputSignal[self.inIdx.m]

  end

  function EpwmVar:p_getDirectFeedthroughCode()
    local Require = ResourceList:new()
    local InitCode = U.CodeLines:new()
    local OutputSignal = StringList:new()
    local OutputCode = U.CodeLines:new()

    table.insert(static[self.cpu].instances, self.bid)

    local pwm_selected = {}

    if Block.Mask.SyncChainConfig == 2 then
      -- 'custom' selection
      pwm_selected = Block.Mask.PwmUnit
    else
      -- legacy fixed selection
      if Block.Mask.NumPwmsInSyncGroup == 1 then
        local pwm1 = Block.Mask.PwmSyncGroup1
        if pwm1 == 1 then
          pwm_selected = {1}
        elseif pwm1 == 2 then
          pwm_selected = {4}
        elseif pwm1 == 3 then
          pwm_selected = {7}
        elseif pwm1 == 4 then
          pwm_selected = {10}
        elseif pwm1 == 5 then
          pwm_selected = {13}
        elseif pwm1 == 6 then
          pwm_selected = {16}
        else
          error('EXCEPTION: Invalid selection.')
        end
      elseif Block.Mask.NumPwmsInSyncGroup == 2 then
        local pwm2 = Block.Mask.PwmSyncGroup2
        if pwm2 == 1 then
          pwm_selected = {1, 2}
        elseif pwm2 == 2 then
          pwm_selected = {4, 5}
        elseif pwm2 == 3 then
          pwm_selected = {7, 8}
        elseif pwm2 == 4 then
          pwm_selected = {10, 11}
        elseif pwm2 == 5 then
          pwm_selected = {13, 14}
        elseif pwm2 == 6 then
          pwm_selected = {16, 17}
        else
          error('EXCEPTION: Invalid selection.')
        end
      elseif Block.Mask.NumPwmsInSyncGroup == 3 then
        local pwm3 = Block.Mask.PwmSyncGroup3
        if pwm3 == 1 then
          pwm_selected = {1, 2, 3}
        elseif pwm3 == 2 then
          pwm_selected = {4, 5, 6}
        elseif pwm3 == 3 then
          pwm_selected = {7, 8, 9}
        elseif pwm3 == 4 then
          pwm_selected = {10, 11, 12}
        elseif pwm3 == 5 then
          pwm_selected = {13, 14, 15}
        elseif pwm3 == 6 then
          pwm_selected = {16, 17, 18}
        else
          error('EXCEPTION: Invalid selection.')
        end
      else
        error('EXCEPTION: Invalid selection.')
      end
    end

    local num_pwm = #pwm_selected
    if num_pwm == 0 then
      U.error('At least one PWM generator must be configured.')
    end

    self.first_unit = pwm_selected[1]
    self.last_unit = pwm_selected[num_pwm]
    self.num_units = num_pwm
    self.first_channel = static[self.cpu].numChannels
    local task_name = Block.Task['Name']

    local max_event_period = globals.target.getTargetParameters()['epwms']['max_event_period']

    -- modulation (duty cycle)
    if #Block.InputSignal[1] ~= 1 and #Block.InputSignal[1] ~= num_pwm then
      U.error("The 'm' input must be scalar or a vector with %i elements." % {num_pwm})
    end


    -- switching frequency
    self.fsw = Block.Mask.CarrierFreq
    self.variable_frequency = false
    if Block.Mask.VariableFreqMode == 2 then
      -- frequency scaling port enabled
      local fStr = Block.InputSignal[self.inIdx.f][1]
      if string.find(fStr, 'UNCONNECTED') then
        -- keeping port disconnected disables frequency scaling
      else
        self.variable_frequency = true
        if Block.InputType[self.inIdx.f][1] == 'float' then
          -- see if in fact scaling is constant
          local constant_scaling = tonumber(string.sub(fStr, 1, -2))
          if constant_scaling ~= nil then
            self.variable_frequency = false
            self.fsw = self.fsw * constant_scaling
          end
        end
      end
    end

    self.cbx_carrierType = U.enforceMask_newComboBox('CarrierType', {'sawtooth', 'triangle'})

    -- High Resolution settings
    self.highResDutyModeActive = U.comboEnabled(Block.Mask.HighResDutyMode)
    self.highResPrdModeActive = U.comboEnabled(Block.Mask.HighResPrdMode)
    self.highResDeadTimeModeActive = U.comboEnabled(Block.Mask.HighResDeadTimeMode)

    if self.cbx_carrierType.equals('triangle') and self.highResPrdModeActive then
      U.error('%(highResPrdLink)s cannot be enabled with %(carrierTypeLink)s set to symmetrical.' %
        {
          highResPrdLink = U.paramLink('HighResPrdMode', false),
          carrierTypeLink = U.paramLink('CarrierType', true),
        })
    end
       
    local timing = globals.target.getPwmFrequencySettings(self.fsw, self.cbx_carrierType)
    if self.cbx_carrierType.equals('sawtooth') then
      self.prd_in_ticks = timing.period
    else
      self.prd_in_ticks = 2 * timing.period
    end

    if self.highResPrdModeActive then
      self.fsw_actual = timing.freqHR
    else
      self.fsw_actual = timing.freq
    end

    -- accuracy of frequency settings
    if Block.Mask.CarrierFreqTol == 1 then  -- enforce exact value (no rounding)
      U.enforceExactFrequencyTolerance({
        freqDesired = self.fsw, 
        freqAchievable = self.fsw_actual,
        descriptor = 'PWM frequency',
      })
    end

    -- deadtime
    self.dead_time_is_variable = not (Block.Mask.VariableDeadTime == 1)
    if not self.dead_time_is_variable then
      self.dead_time = Block.Mask.FixedDeadTime
    else
      self.dead_time = Block.Mask.MinDeadTime  -- default and minimal value
      if #Block.InputSignal[self.inIdx.d_rf] ~= 2 and #Block.InputSignal[6] ~= (2 * num_pwm) then
        U.error([[
          The 'd[r,f]' input must be a vector with 2 elements or one element for each rising edge and each falling edge (%d elements). Inputs are in seconds.
        ]] % {2 * num_pwm})
      end
      for i = 1, #Block.InputSignal[6] do
        if Block.InputType[6][i] ~= 'float' then
          U.error("The 'd[r,f]' input must be of float type.")
        end
      end
    end

    -- show enable
    self.showEnablePort = (Block.Mask.ShowEnable == 2)
    if self.showEnablePort then
      -- Enable port may not be used on non-driverlib targets
      if self:targetUsesPinsets() then
        U.error('The PWM enable port is not supported on this target.')
      end
      if #Block.InputSignal[5] ~= 1 and #Block.InputSignal[5] ~= num_pwm then
        U.error("The 'en' input must be scalar or a vector with %i elements." % {num_pwm})
      end
    end

    -- show output swap
    self.show_swap = (Block.Mask.ShowSwap == 2)
    if self.show_swap then
      if #Block.InputSignal[10] ~= 1 and #Block.InputSignal[10] ~= num_pwm then
        U.error("The 'swap' input must be scalar or a vector with %i elements." % {num_pwm})
      end
    end

    -- sequence
    self.const_sequence = {}
    self.sequence_defined_in_aq = false
    if Block.Mask.Sequence == 1 then
      for i = 1, num_pwm do
        self.const_sequence[i] = 1 -- fixed positive sequence
      end
    elseif Block.Mask.Sequence == 2 then
      for i = 1, num_pwm do
        self.const_sequence[i] = 0 -- fixed negative sequence
      end
    elseif Block.Mask.Sequence == 4 then
      self.sequence_defined_in_aq = true
      for i = 1, num_pwm do
        self.const_sequence[i] = Block.Mask.SequenceAq[i] -- fixed AQ-based sequence
      end
    else
      -- variable sequence options    
      local sequencePortIndex
      local sequencePortLabel
      if Block.Mask.Sequence == 3 then
        sequencePortIndex = 7
        sequencePortLabel = 'seq'
      elseif Block.Mask.Sequence == 5 then
        sequencePortIndex = 9
        sequencePortLabel = 'AQ'
        self.sequence_defined_in_aq = true
      else
        assert('Unexpected value for Block.Mask.Sequence.')
      end 

      if #Block.InputSignal[sequencePortIndex] ~= 1 and #Block.InputSignal[sequencePortIndex] ~= num_pwm then
        U.error("The '%s' input must be scalar or a vector with %i elements." % {sequencePortLabel, num_pwm})
      end

      for i = 1, num_pwm do
        self.const_sequence[i] = self:getConstantSignal(sequencePortIndex, math.min(i, #Block.InputSignal[sequencePortIndex]), 
                                                        "'%s'" % {sequencePortLabel})
        if  (self.const_sequence[i] == nil)
        and (globals.target.getTargetParameters().epwms.type < 4) then
          U.error("Variable sequence not supported by this target (%s)." % {Target.Name})
        end
      end
    end

    -- shadowing
    function getShadowModeString(comboIndex)
      if comboIndex == 1 then
        return 'z'
      elseif comboIndex == 2 then
        return 'p'
      else
        return 'zp'
      end
    end

    self.cmp_shadow_load = getShadowModeString(Block.Mask.CmpShadowMode)

    -- If high resolution period enabled, enforce shadowing settings
    if (self.highResPrdModeActive) then
      if (self.cbx_carrierType.equals('triangle')) then
        self.cmp_shadow_load = 'zp'
      else
        self.cmp_shadow_load = 'p'
      end
    end

    -- protection
    self.disable_protection = (Block.Mask.AllowProtection == 2)

    -- task trigger event
    self.int_loc = ''
    if Block.Mask.TaskTriggerMode == 2 then
      self.int_loc = 'z'
    elseif Block.Mask.TaskTriggerMode == 3 then
      self.int_loc = 'p'
    elseif Block.Mask.TaskTriggerMode == 4 then
      if self.cbx_carrierType.equals('sawtooth') then
        U.error('Invalid task trigger event for sawtooth carrier')
      end
      if globals.target.getTargetParameters().epwms.type == 0 then
        U.error('Dual task trigger events not supported by this chip.')
      end
      self.int_loc = 'zp'
    end

    self.int_prd = U.enforceMask(
      U.isIntScalarInClosedInterval, 'TaskTriggerDivider', 1, max_event_period)


    -- ADC trigger event
    self.soc_loc = ''
    if Block.Mask.AdcTriggerMode == 2 then
      self.soc_loc = 'z'
    elseif Block.Mask.AdcTriggerMode == 3 then
      self.soc_loc = 'p'
    elseif Block.Mask.AdcTriggerMode == 4 then
      if self.cbx_carrierType.equals('sawtooth') then
        U.error('Invalid ADC trigger event for sawtooth carrier')
      end
      if globals.target.getTargetParameters().epwms.type == 0 then
        U.error('Dual ADC trigger events not supported by this chip.')
      end
      self.soc_loc = 'zp'
    end

    self.soc_prd = U.enforceMask(
      U.isIntScalarInClosedInterval, 'AdcTriggerDivider', 1, max_event_period)

    -- trip zone (trip generated by digital input) -- deprecated
    self.trip_zone_settings = {}
    for z = 1, 3 do
      if Block.Mask['tz%imode' % {z}] == 2 then
        self.trip_zone_settings[z] = 'cbc'
      elseif Block.Mask['tz%imode' % {z}] == 3 then
        self.trip_zone_settings[z] = 'osht'
      end
    end

    -- synchronization and phase
    self.const_phase = {}
    for i = 1, num_pwm do
      local phStr = Block.InputSignal[2][i]
      if string.find(phStr, 'UNCONNECTED') then
        self.const_phase[i] = 0
      elseif Block.InputType[2][i] == 'float' then
        self.const_phase[i] = tonumber(string.sub(phStr, 1, -2))
      end
    end

    self.is_self_synchronized = (Block.Mask.SyncSrc == 1)
    self.is_synchronized = false
    if not self.is_self_synchronized or Block.Mask.SyncOut == 2 then
      self.is_synchronized = true
    else
      for i = 1, num_pwm do
        if self.const_phase[i] == nil or self.const_phase[i] ~= 0 then
          self.is_synchronized = true
          break
        end
      end
    end

    local cbx_peripheralSyncMode = U.enforceMask_newComboBox('PeripheralSyncMode',
      {'disabled', 'underflow', 'overflow'}
    )
    local persyncSrc
    if cbx_peripheralSyncMode.equals('underflow') then
      persyncSrc = 'z'
    elseif cbx_peripheralSyncMode.equals('overflow') then
      persyncSrc = 'p'
    end

    if self.is_self_synchronized and self.const_phase[1] ~= nil then
      if self.const_phase[1] ~= 0 then
        U.warning('For a self-synchronized PWM block the first channel phase is ignored (assumed to be zero).')
      end
    end

    -- check to deal with limitations of older MCUs
    if globals.target.getTargetParameters().epwms.type < 4 then
      if self.variable_frequency then
        for i = 1, num_pwm do
          local phStr = Block.InputSignal[2][i]
          if not string.find(phStr, 'UNCONNECTED') then
            if string.sub(phStr, -1, -1) == 'f' then
              phStr = string.sub(phStr, 1, -2)
            end
            local ph = tonumber(phStr)
            if (ph == nil) or (ph ~= 0) then
              U.error([[
                This MCU does not support phase shifting combined with variable frequency operation.
                Please disable variable frequency or leave the ph' port disconnected.]])
            end
          end
        end
      end

      if self.showEnablePort then
        U.error("Enable port 'en' not supported by this target (%s)." % {Target.Name})
      end
      if self.show_swap then
        U.error("Output swap port 'swap' not supported by this target (%s)." % {Target.Name})
      end
    end

    -- now create all implicit ePWM instances
    self.channels = {}
    for pwmChannel = 1, num_pwm do
      local pwm = pwm_selected[pwmChannel]
      if pwm == 1 then
        self:registerSelfAsEpwm1()  -- implemented in epwm_common, and used for sync chain verifications
      end

      local epwm = self:makeBlock('epwm', self.cpu)

      local phase0
      if self.is_synchronized then
        phase0 = self.const_phase[pwmChannel]
      end

      -- output settings
      self.polarityIsActiveHigh = U.enforceMask_newComboBox('Polarity', {'positive', 'negative'}).equals('positive')

      local sequence0
      if self.const_sequence[pwmChannel] then
        sequence0 = {
          defined_in_aq = self.sequence_defined_in_aq,
          value = self.const_sequence[pwmChannel],
        }
      end
      self.outmode = {'A', '!A'}

      -- deadtime settings
      local const_rising_delay, const_falling_delay, deadtime_input_offset, dead_time_shadow_mode
      if self.dead_time_is_variable then
        deadtime_input_offset = 2 * (pwmChannel - 1) + 1
        if #Block.InputSignal[6] == 2 then
          deadtime_input_offset = 1;
        end
        -- drop 'f' and try to convert (will be nil if conversion fails, which means input is not constant)
        const_rising_delay = tonumber(string.sub(Block.InputSignal[6][deadtime_input_offset], 1, -2))
        const_falling_delay = tonumber(string.sub(Block.InputSignal[6][deadtime_input_offset + 1], 1, -2))
        -- if constant, enforce minimal deadtime at this point
        if const_rising_delay ~= nil and const_rising_delay < self.dead_time then
          const_rising_delay = self.dead_time
        end
        if const_falling_delay ~= nil and const_falling_delay < self.dead_time then
          const_falling_delay = self.dead_time
        end

        -- enable deadtime shadowing on chips that support it
        if globals.target.getTargetParameters().epwms.type >= 4 then
          if const_falling_delay == nil or const_rising_delay == nil then
            dead_time_shadow_mode = 'zp'
            self.red_shadow_load = getShadowModeString(Block.Mask.RedShadowMode)
            self.fed_shadow_load = getShadowModeString(Block.Mask.FedShadowMode)
          end
        end
      end

      epwm:createImplicit(
        pwm, {
          fsw = self.fsw,
          cbx_carrierType = self.cbx_carrierType,
          opt_highResDutyModeActive = self.highResDutyModeActive,
          opt_highResPrdModeActive = self.highResPrdModeActive,
          polarityIsActiveHigh = self.polarityIsActiveHigh,
          outMode = self.outmode,
          opt_highResDeadTimeModeActive = self.highResDeadTimeModeActive,
          opt_sequence0 = sequence0,
          opt_dead_time = self.dead_time,
          trip_zone_settings = self.trip_zone_settings,
          disableControlByProtectionBlock = self.disable_protection,
          showEnablePort = self.showEnablePort,
          opt_dead_time_shadow_mode = dead_time_shadow_mode,
          opt_rising_delay = const_rising_delay,
          opt_falling_delay = const_falling_delay,
          opt_phase0 = phase0,
          opt_red_shadow_load = self.red_shadow_load,
          opt_fed_shadow_load = self.fed_shadow_load,
          opt_cmp_shadow_load = self.cmp_shadow_load,
          opt_peripheralSyncSrc = persyncSrc,
        }, Require)

      self:logLine('EPWM implicitly created for channel %i, pwm %i.' %
        {static[self.cpu].numChannels, pwm})

      local epwmInstance = epwm:getObjIndex()

      self.channels[static[self.cpu].numChannels] = epwm

      if self.outmode[1] ~= '' or self.outmode[2] ~= '' then
        if self.dead_time_is_variable then
          if const_rising_delay == nil then
            OutputCode:append('PLXHAL_PWM_setRisingDelay(%i, fmaxf(%s, %s));' % {
              epwmInstance,
              Block.InputSignal[6][deadtime_input_offset],
              Block.Mask.MinDeadTime
            })
          end
          if const_falling_delay == nil then
            OutputCode:append('PLXHAL_PWM_setFallingDelay(%i, fmaxf(%s, %s));' % {
              epwmInstance,
              Block.InputSignal[6][deadtime_input_offset + 1],
              Block.Mask.MinDeadTime
            })
          end
        end

        local setSequenceCode = ''
        if self.const_sequence[pwmChannel] == nil then
          local codeList = U.CodeLines:new()
          if self.sequence_defined_in_aq then
            codeList:append('PLXHAL_PWM_setSequenceAq(%i, %s);' % {
              epwmInstance,
              Block.InputSignal[9][math.min(pwmChannel, #Block.InputSignal[9])]
            })
          else
            codeList:append('PLXHAL_PWM_setSequence(%i, %s);' % {
              epwmInstance,
              Block.InputSignal[7][math.min(pwmChannel, #Block.InputSignal[7])]
            })
          end
          if self.showEnablePort then
            codeList:append(
              'PLXHAL_PWM_prepareSetOutToXTransition(%i);' % {epwmInstance})
          end
          setSequenceCode = table.concat(codeList, '\n')
        end

        local setDutyCode = ''
        do
          local codeList = U.CodeLines:new()
          local dutyInputSignal = Block.InputSignal[1][math.min(pwmChannel, #Block.InputSignal[1])]

          if not self.variable_frequency then
            if self.const_phase[pwmChannel] ~= nil then
              codeList:append(
                'PLXHAL_PWM_setDuty(%i, %s);'
                % {
                  epwmInstance,
                  dutyInputSignal
                })
            else
              codeList:append(
                'PLXHAL_PWM_setDutyPhase(%i, %s, %s);'
                % {
                  epwmInstance,
                  dutyInputSignal,
                  Block.InputSignal[2][pwmChannel]
                })
            end
          else
            -- TPBHS must be updated when frequency (TBPRD) is variable, unless phase is constant and zero
            if (self.const_phase[pwmChannel] ~= nil) and (self.const_phase[pwmChannel] == 0) then
              codeList:append(
                'PLXHAL_PWM_setDutyFreq(%i, %s, %s);'
                % {
                  epwmInstance,
                  dutyInputSignal,
                  Block.InputSignal[4][1]
                })
            else
              codeList:append(
                'PLXHAL_PWM_setDutyFreqPhase(%i, %s, %s, %s);'
                % {
                  epwmInstance,
                  dutyInputSignal,
                  Block.InputSignal[4][1],
                  Block.InputSignal[2][pwmChannel]
                })
            end
          end
          setDutyCode = table.concat(codeList, '\n')
        end

        if self.showEnablePort then
          OutputCode:append([[
            if((%(en)s) == 0){
              PLXHAL_PWM_setToPassive(%(instance)i);
            } else {
              %(setSequenceCode)s
              %(setDutyCode)s
              PLXHAL_PWM_setToOperational(%(instance)i);
            }
          ]] % {
            en = Block.InputSignal[5][math.min(pwmChannel, #Block.InputSignal[5])],
            instance = epwmInstance,
            setSequenceCode = setSequenceCode,
            setDutyCode = setDutyCode,
          })
        else
          OutputCode:append([[
            %(setSequenceCode)s
            %(setDutyCode)s
          ]]% {
            setSequenceCode = setSequenceCode,
            setDutyCode = setDutyCode,
          })
        end
        
        if self.show_swap then
          OutputCode:append([[
            PLXHAL_PWM_enableOutputSwap(%(instance)i, ((%(swap)s) == 1));
          ]] % {
            swap = Block.InputSignal[10][math.min(pwmChannel, #Block.InputSignal[10])],
            instance = epwmInstance
          })
        end
      end

      static[self.cpu].numChannels = static[self.cpu].numChannels + 1
    end

    if (self.int_loc == '') then
      OutputSignal:append('{}')
    else
      self.modTrigTs = 1 / (self.fsw_actual * #self.int_loc)
      if self.int_prd ~= nil then
        self.modTrigTs = self.modTrigTs * self.int_prd;
      end
      OutputSignal:append('{modtrig = {bid = %i}}' % {self:getId()})
    end

    if (self.soc_loc == '') then
      OutputSignal:append('{}')
    else
      self.adcTrigTs = 1 / (self.fsw_actual * #self.soc_loc)
      if self.soc_prd ~= nil then
        self.adcTrigTs = self.adcTrigTs * self.soc_prd;
      end
      OutputSignal:append('{adctrig = {bid = %i}}' % {self:getId()})
    end

    -- synchronization
    OutputSignal:append('{synco = {bid = %i}}' % {EpwmVar:getId()})
    OutputSignal:append('{syncper = {bid = %i}}' % {self:getId()})

    return {
      InitCode = InitCode,
      OutputSignal = OutputSignal,
      OutputCode = OutputCode,
      Require = Require,
      UserData = {bid = self:getId()}
    }
  end

  function EpwmVar:p_getNonDirectFeedthroughCode()
    local Require = ResourceList:new()
    local UpdateCode = U.CodeLines:new()

    local enableCode = self.channels[self.first_channel]:getEnableCode()
    if enableCode ~= nil then
      UpdateCode:append(enableCode)
    end

    local powerstage_obj = self:getBlockInstance('powerstage')

    for zone, _ in pairs(self.trip_zone_settings) do
      if powerstage_obj == nil then
        U.error(
          'TZ%i protection requires the use of a Powerstage Protection block.' %
          {zone})
      end
      if not powerstage_obj:isTripZoneConfigured(zone) then
        if powerstage_obj:isDeprecated() then
          U.error('Please replace deprecated powerstage protection block.')
        else
          U.error('Please enable TZ%i under Coder Options -> Target -> Protections.' % {zone})
        end
      end
    end

    -- establish synchronization source of first channel
    if self.is_self_synchronized then
      self.sync_src_bid = self:getId()
    else
      local synci_top = Block.InputSignal[3][1]
      synci_top = synci_top:gsub('%s+', '')  -- remove whitespace

      local trig

      if synci_top:sub(1, #'{synco') == '{synco' then
        trig = eval(synci_top)['synco']
      elseif synci_top:sub(1, #'{tevent') == '{tevent' then
        trig = eval(synci_top)['tevent']
      else
        U.error([['sycni' port must be connected to one of the following:
          - the sync port of another PWM block,
          - the output of an External Sync block,
          - the the output of a CMPSS block.]])
      end
      if trig['bid'] == nil then
        error('Malformed trigger expression: %s' % {synci_top})
      end
      self.sync_src_bid = trig['bid']
    end

    if self.is_synchronized  then
      self:validateSyncChain()
    end

    -- establish T event (e.g. for peak-current control)
    if Block.Mask.EnableTripAction == 2 then
      if self:targetMatches({'2806x', '2833x', '2837x'}) then
        -- note: 2837x is not supported due to lack of EPWM_AQ_TRIGGER_EVENT_TRIG_DC_EVTFILT
        U.error("Trip 'T' input not supported by this target (%(target)s)." % {
          target = Target.Name
        })
      elseif not self:targetMatches({'2838x', '28003x', '28004x', '280013x', '28P55x', '28P65x', '29H85x'}) then
        U.throwUnhandledTargetError()
      end

      local t_event = Block.InputSignal[8][1]
      if not string.find(t_event, 'UNCONNECTED') then
        t_event = t_event:gsub('%s+', '')  -- remove whitespace
        if t_event:sub(1, #'{tevent') ~= '{tevent' then
          U.error("'T' port must be connected to the output of a CMPSS block.")
        end
        local event = eval(t_event)['tevent']
        if event['bid'] == nil then
          error('Malformed T event expression: %s' % {t_event})
        end
        self.cmp_bid = event['bid']
        self.cmp_obj = globals.instances[self.cmp_bid]

        local cbc_blanking_time_pu = 0
        local cbc_blanking_delay_pu = 0
        local cbc_blanking_start_event

        -- TODO: also handle blanking location Z,P,ZP
        if Block.Mask.TripBlankingMode ~= 1 then
          if Block.Mask.TripBlankingMode == 2 then

          elseif Block.Mask.TripBlankingMode == 3 then
            cbc_blanking_start_event = 'p'
          else
            cbc_blanking_start_event = 'zp'
          end

          cbc_blanking_time_pu = Block.Mask.TripBlankingTime * self.fsw
          if cbc_blanking_time_pu < 0 then
            cbc_blanking_time_pu = 0
          elseif cbc_blanking_time_pu >= 1 then
            U.error('Trip blanking time cannot exceed carrier period.')
          end
          local blanking_time_ticks = math.floor(self.prd_in_ticks * cbc_blanking_time_pu + 0.5)
          if blanking_time_ticks < 1 then
            -- this is needed in case of the compare event still beeing active at
            -- the start of the period
            -- cbc_blanking_time_pu = 1/self.prd
          end

          cbc_blanking_delay_pu = Block.Mask.TripBlankingOffset * self.fsw
          local blanking_delay_ticks = math.floor(self.prd_in_ticks * cbc_blanking_delay_pu + 0.5)

          if blanking_delay_ticks > -4 then
            -- to ensure minimal required tz pulse length, the cmpss filter is set to 3 TBCLK (see comp.lua)
            -- we therefore need to start blanking TZ accordingly before the PWM sync pulse
            -- cbc_blanking_delay_pu = -4/self.prd
          end
        end

        local cbc_trip = {
          input = self.cmp_obj:getParameter('tripInputSignal'),
          blanking_start_event = cbc_blanking_start_event,
          blanking_time_pu = cbc_blanking_time_pu,
          blanking_delay_pu = cbc_blanking_delay_pu
        }

        -- apply to all channels
        for c, p in pairs(self.channels) do
          self.channels[c]:configureCycleByCycleTrip(cbc_trip)
        end
      end
    end

    return {Require = Require, UpdateCode = UpdateCode}
  end

  function EpwmVar:finalizeThis(c)
    if self['modtrig'] ~= nil then
      for _, b in ipairs(self['modtrig']) do
        if b:blockMatches('tasktrigger') then
          local modTriggerCpu = self.cpu
          if b:getCpu() ~= self.cpu then
            modTriggerCpu = b:getCpu()
          end
          c.FinalizeNeedsRerun = true
          local isrObj = self:makeBlock('interrupt', modTriggerCpu)
          isrObj:createImplicit({
            isrCode = globals.target.getEpwmIsrCode(self.first_unit),
            isrEnableCode = globals.target.getEpwmIsrEnableCode(self.first_unit),
            bid = self.channels[self.first_channel]:getId()
          })
        end
      end
    end
  end

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

    local EpwmCommonStatic = self:getStaticMethods()

    c.Include:append('plx_pwm.h')
    c.Include:append('math.h')

    c.Declarations:append('extern PLX_PWM_Handle_t EpwmHandles[];')

    local code = [==[
      #pragma CODE_SECTION(PLXHAL_PWM_setDutyFreqPhase, "ramfuncs")
      void PLXHAL_PWM_setDutyFreqPhase(uint16_t aChannel, float aDuty, float aFreqScaling, float aPhase) {
        PLX_PWM_Handle_t handle = EpwmHandles[aChannel];
        if (aFreqScaling > (1.0 / 65536.0)) {
          PLX_PWM_scalePeriod(handle, 1.0 / aFreqScaling);
        } else {
          PLX_PWM_scalePeriod(handle, 65536.0);
        }
        PLX_PWM_setPhase(handle, aPhase);
        PLX_PWM_setPwmDuty(handle, aDuty);
      }

      #pragma CODE_SECTION(PLXHAL_PWM_setDutyFreq, "ramfuncs")
      void PLXHAL_PWM_setDutyFreq(uint16_t aChannel, float aDuty, float aFreqScaling) {
        PLX_PWM_Handle_t handle = EpwmHandles[aChannel];
        if (aFreqScaling > (1.0 / 65536.0)) {
          PLX_PWM_scalePeriod(handle, 1.0 / aFreqScaling);
        } else {
          PLX_PWM_scalePeriod(handle, 65536.0);
        }
        PLX_PWM_setPwmDuty(handle, aDuty);
      }

      #pragma CODE_SECTION(PLXHAL_PWM_setDutyPhase, "ramfuncs")
      void PLXHAL_PWM_setDutyPhase(uint16_t aChannel, float aDuty, float aPhase) {
        PLX_PWM_Handle_t handle = EpwmHandles[aChannel];
        PLX_PWM_setPhase(handle, aPhase);
        PLX_PWM_setPwmDuty(handle, aDuty);
      }
    ]==]
    c.Declarations:append(code)

    -- generate HAL code shared with epwm_basic.lua
    EpwmCommonStatic.generatePlxHalCode(c)

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

    static[self.cpu].finalized = true
  end

  return EpwmVar
end

return Module
