This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git

commit c9c6faa23d88713dba16168a97a278752ebe4712
Author: Ulaş Sertan Kemeç <[email protected]>
AuthorDate: Thu Sep 10 09:16:34 2026 +0300

    arch/arm/am67: Add EPWM0/EPWM1 PWM lower-half driver.
    
    Adds a PWM lower-half for EPWM0 and EPWM1, covering both output channels of
    each.  The CTRL_MMR EPWM clock enables are unlocked once during board 
bring-up.
    
    t3-gem-o1 registers /dev/pwm0 and /dev/pwm1 with PWM_NCHANNELS=2.
    
    Verified on t3-gem-o1: all four outputs (EPWM0 A+B, EPWM1 A+B) drive 
physical
    pins, jumpered into a Linux GPIO input -- 50% and 20% duty read back at the
    expected sample ratios, and gpiomon timed a 50 Hz half-period at 
9.998-10.002
    ms.  examples/pwm starts and stops a 1 kHz train cleanly.
    
    Assisted-by: Claude Code:claude-fable-5
    Signed-off-by: Ulaş Sertan Kemeç <[email protected]>
---
 arch/arm/src/am67/Kconfig                          |   21 +
 arch/arm/src/am67/Make.defs                        |    3 +
 arch/arm/src/am67/am67_pinmux.h                    |    3 +-
 arch/arm/src/am67/am67_pwm.c                       | 1187 ++++++++++++++++++++
 arch/arm/src/am67/am67_pwm.h                       |  105 ++
 arch/arm/src/am67/am67_pwm_hw.h                    |  146 +++
 boards/arm/am67/t3-gem-o1/configs/nsh/defconfig    |    5 +
 boards/arm/am67/t3-gem-o1/src/Makefile             |    5 +
 boards/arm/am67/t3-gem-o1/src/am67_bringup.c       |   58 +
 .../t3-gem-o1/src/{am67_bringup.c => am67_pwm.c}   |   55 +-
 10 files changed, 1538 insertions(+), 50 deletions(-)

diff --git a/arch/arm/src/am67/Kconfig b/arch/arm/src/am67/Kconfig
index 352105c0bf0..6fed471a433 100644
--- a/arch/arm/src/am67/Kconfig
+++ b/arch/arm/src/am67/Kconfig
@@ -13,6 +13,27 @@ config AM67_GPIO
                example applications (LED, IMU_EN, SPI CS padconfig).
 
 
+config AM67_EPWM0
+       bool "AM67 EPWM0 driver"
+       default n
+       depends on PWM
+       select ARCH_HAVE_PWM_MULTICHAN
+       ---help---
+               Enable EPWM driver on EPWM0 (outputs A and B; requires
+               PWM_MULTICHAN with PWM_NCHANNELS >= 2)
+
+config AM67_EPWM1
+       bool "AM67 EPWM1 driver"
+       default n
+       depends on PWM
+       select ARCH_HAVE_PWM_MULTICHAN
+       ---help---
+               Enable EPWM driver on EPWM1 (outputs A and B on HAT GPIO-6
+               and GPIO-13; requires PWM_MULTICHAN with PWM_NCHANNELS >= 2).
+               EPWM2 is the board cooling fan and is deliberately not
+               supported.
+
+
 config AM67_MCSPI0
        bool "AM67 MCU_MCSPI0 master driver"
        default n
diff --git a/arch/arm/src/am67/Make.defs b/arch/arm/src/am67/Make.defs
index ba6ca865ea0..b7049b1baf9 100644
--- a/arch/arm/src/am67/Make.defs
+++ b/arch/arm/src/am67/Make.defs
@@ -33,6 +33,9 @@ CHIP_CSRCS += am67_timer.c
 ifeq ($(CONFIG_AM67_GPIO),y)
 CHIP_CSRCS += am67_gpio.c
 endif
+ifeq ($(CONFIG_AM67_EPWM0),y)
+CHIP_CSRCS += am67_pwm.c
+endif
 ifeq ($(CONFIG_AM67_MCSPI0),y)
 CHIP_CSRCS += am67_mcspi.c
 endif
diff --git a/arch/arm/src/am67/am67_pinmux.h b/arch/arm/src/am67/am67_pinmux.h
index cc336a89ccd..23ce9724bb6 100644
--- a/arch/arm/src/am67/am67_pinmux.h
+++ b/arch/arm/src/am67/am67_pinmux.h
@@ -100,7 +100,7 @@ enum pinmux_main_offsets_e
   PIN_GPMC0_AD14     = 0x0074,
   PIN_GPMC0_AD15     = 0x0078,
   PIN_GPMC0_WAIT1    = 0x009c,
-  PIN_SPI0_CS1       = 0x01bb,
+  PIN_SPI0_CS1       = 0x01b8,
   PIN_UART0_TXD      = 0x01cc,
   PIN_UART0_RXD      = 0x01c8,
   PIN_SPI0_CS0       = 0x01b4,
@@ -315,5 +315,6 @@ void am67_mcu_pinmux_config(const struct pinmux_conf_s 
*pinmux_conf);
 void am67_pinmux_init(void);
 void am67_spi_pinmux_init(void);
 void am67_i2c_pinmux_init(void);
+void am67_epwm_pinmux_init(int epwm);
 
 #endif /* __ARCH_ARM_SRC_AM67_AM67_PINMUX_H */
diff --git a/arch/arm/src/am67/am67_pwm.c b/arch/arm/src/am67/am67_pwm.c
new file mode 100644
index 00000000000..8c89cd15ac3
--- /dev/null
+++ b/arch/arm/src/am67/am67_pwm.c
@@ -0,0 +1,1187 @@
+/****************************************************************************
+ * arch/arm/src/am67/am67_pwm.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <assert.h>
+#include <debug.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <nuttx/timers/pwm.h>
+
+#include "arm_internal.h"
+#include "am67_pinmux.h"
+#include "am67_pwm.h"
+#include "am67_pwm_hw.h"
+
+#if CONFIG_PWM_NCHANNELS < 2
+#  error "AM67 EPWM requires CONFIG_PWM_NCHANNELS >= 2"
+#endif
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+/* MAIN_CTRL_MMR partition 1 lock (kick) registers */
+
+#define AM67_MAIN_CTRL_MMR_BASE           0x00100000
+#define AM67_CTRL_MMR_LOCK1_KICK0         0x5008      /* Offset from base */
+#define AM67_CTRL_MMR_LOCK1_KICK1         0x500c
+#define AM67_CTRL_MMR_KICK0_UNLOCK_KEY    0x68ef3490
+#define AM67_CTRL_MMR_KICK0_UNLOCKED      (1u << 0)   /* 1 = unlocked */
+#define AM67_CTRL_MMR_KICK1_UNLOCK_KEY    0xd172bc5a
+
+/* EPWM time-base clock gate (CTRL_MMR partition 1) */
+
+#define AM67_CTRL_MMR_EPWM_TB_CLKEN       0x4130      /* Offset from base */
+#define AM67_EPWM_TB_CLKEN_EPWM0_EN       (1u << 0)
+#define AM67_EPWM_TB_CLKEN_EPWM1_EN       (1u << 1)
+#define AM67_EPWM_TB_CLKEN_EPWM2_EN       (1u << 2)
+
+/* EPWM functional clock (FICLK).  Verified on copper 2026-07-16: with
+ * HSPCLKDIV=1, CLKDIV=4, TBPRD=62499 the pin measured 1.000 kHz, which
+ * pins FICLK to 250 MHz within 0.1%.
+ */
+
+#define AM67_EPWM_FICLK_HZ                250000000u
+
+/* The time-base counter is 16-bit: one period is TBPRD+1 ticks, at most
+ * 65536.  The 2-tick floor (TBPRD=1) is a deliberate hardware-limit
+ * policy: the driver does not cap the user's frequency below what the
+ * silicon can express, but duty resolution degrades as ticks shrink
+ * (at the 125 MHz ceiling only 0/50/100% duty exist).
+ */
+
+#define AM67_EPWM_MAX_TICKS               65536u
+#define AM67_EPWM_MIN_TICKS               2u
+
+/* Channel bookkeeping masks: channel number (1 = A, 2 = B) to its bit
+ * in the active/requested/joiner sets.  CH_BIT(1) == CH_A_ACTIVE and
+ * CH_BIT(2) == CH_B_ACTIVE by construction.
+ */
+
+#define CH_A_ACTIVE     (1u << 0)
+#define CH_B_ACTIVE     (1u << 1)
+#define CH_BIT(ch)      (1u << ((ch) - 1))
+
+/****************************************************************************
+ * Private Types
+ ****************************************************************************/
+
+/* PWM lower-half state.  Must begin with the ops pointer so that this
+ * structure can be cast to and from struct pwm_lowerhalf_s.  base,
+ * clken_mask and pinmux_id identify the instance; frequency and tbprd
+ * cache the running wave (frequency == 0 means no wave is running;
+ * see am67_epwm_stop()).
+ */
+
+struct am67_epwm_s
+{
+  const struct pwm_ops_s *ops;
+  uint32_t base;
+  uint32_t clken_mask;      /* This instance's TB_CLKEN gate bit */
+  uint32_t frequency;
+  uint16_t tbprd;
+  uint8_t active_channels;
+  uint8_t pinmux_id;        /* Instance number for pad muxing */
+};
+
+/****************************************************************************
+ * Private Function Prototypes
+ ****************************************************************************/
+
+/* PWM driver methods */
+
+static int am67_epwm_setup(struct pwm_lowerhalf_s *dev);
+static int am67_epwm_shutdown(struct pwm_lowerhalf_s *dev);
+static int am67_epwm_start(struct pwm_lowerhalf_s *dev,
+                           const struct pwm_info_s *info);
+static int am67_epwm_stop(struct pwm_lowerhalf_s *dev);
+static int am67_epwm_ioctl(struct pwm_lowerhalf_s *dev,
+                           int cmd, unsigned long arg);
+
+/****************************************************************************
+ * Private Data
+ ****************************************************************************/
+
+static const struct pwm_ops_s g_am67_epwmops =
+{
+  .setup       = am67_epwm_setup,
+  .shutdown    = am67_epwm_shutdown,
+  .start       = am67_epwm_start,
+  .stop        = am67_epwm_stop,
+  .ioctl       = am67_epwm_ioctl,
+};
+
+#ifdef CONFIG_AM67_EPWM0
+static struct am67_epwm_s g_am67_epwm0 =
+{
+  .ops        = &g_am67_epwmops,
+  .base       = AM67_EPWM0_BASE,
+  .clken_mask = AM67_EPWM_TB_CLKEN_EPWM0_EN,
+  .pinmux_id  = 0,
+};
+#endif
+
+#ifdef CONFIG_AM67_EPWM1
+static struct am67_epwm_s g_am67_epwm1 =
+{
+  .ops        = &g_am67_epwmops,
+  .base       = AM67_EPWM1_BASE,
+  .clken_mask = AM67_EPWM_TB_CLKEN_EPWM1_EN,
+  .pinmux_id  = 1,
+};
+#endif
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: am67_epwm_getreg
+ *
+ * Description:
+ *   Get a 32-bit register value by offset.
+ *
+ ****************************************************************************/
+
+static inline uint32_t am67_epwm_getreg(uint32_t base, uint32_t offset)
+{
+  return getreg32(base + offset);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_putreg
+ *
+ * Description:
+ *   Put a 32-bit register value by offset.
+ *
+ ****************************************************************************/
+
+static inline void am67_epwm_putreg(uint32_t base, uint32_t offset,
+                                    uint32_t value)
+{
+  putreg32(value, base + offset);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_getreg16
+ *
+ * Description:
+ *   Get a 16-bit register value by offset.  All EPWM core registers
+ *   except PID are 16-bit; 32-bit access is unaligned for half of them
+ *   and clobbers the neighboring register for the rest.
+ *
+ ****************************************************************************/
+
+static inline uint16_t am67_epwm_getreg16(uint32_t base, uint32_t offset)
+{
+  return getreg16(base + offset);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_putreg16
+ *
+ * Description:
+ *   Put a 16-bit register value by offset.
+ *
+ ****************************************************************************/
+
+static inline void am67_epwm_putreg16(uint32_t base, uint32_t offset,
+                                      uint16_t value)
+{
+  putreg16(value, base + offset);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_enable_register_write
+ *
+ * Description:
+ *   Unlock partition 1 of the MAIN_CTRL_MMR (kick lock) so that the EPWM
+ *   time-base clock gate register can be written.  The KICK0 status bit
+ *   is read back to confirm that the partition is unlocked.
+ *
+ * Returned Value:
+ *   Zero (OK) on success; -EIO if the partition is still locked after
+ *   writing the unlock keys.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_enable_register_write(void)
+{
+  uint32_t regval = am67_epwm_getreg(AM67_MAIN_CTRL_MMR_BASE,
+                                     AM67_CTRL_MMR_LOCK1_KICK0);
+
+  if ((regval & AM67_CTRL_MMR_KICK0_UNLOCKED) == 0u)
+    {
+      am67_epwm_putreg(AM67_MAIN_CTRL_MMR_BASE, AM67_CTRL_MMR_LOCK1_KICK0,
+                       AM67_CTRL_MMR_KICK0_UNLOCK_KEY);
+      am67_epwm_putreg(AM67_MAIN_CTRL_MMR_BASE, AM67_CTRL_MMR_LOCK1_KICK1,
+                       AM67_CTRL_MMR_KICK1_UNLOCK_KEY);
+    }
+
+  regval = am67_epwm_getreg(AM67_MAIN_CTRL_MMR_BASE,
+                            AM67_CTRL_MMR_LOCK1_KICK0);
+
+  if ((regval & AM67_CTRL_MMR_KICK0_UNLOCKED) == 0u)
+    {
+      pwmerr("ERROR: Could not unlock CTRL_MMR partition 1\n");
+      return -EIO;
+    }
+
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_enable_clock
+ *
+ * Description:
+ *   Enable one instance's time-base clock in the shared EPWM_TB_CLKEN
+ *   register.  Read-modify-write to preserve the gates of the other
+ *   EPWM instances (EPWM2 drives the board cooling fan).  Read back to
+ *   verify that the gate bit stuck.
+ *
+ * Returned Value:
+ *   Zero (OK) on success; -EIO if the clock gate did not enable.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_enable_clock(uint32_t clken_mask)
+{
+  uint32_t regval = am67_epwm_getreg(AM67_MAIN_CTRL_MMR_BASE,
+                                     AM67_CTRL_MMR_EPWM_TB_CLKEN);
+
+  regval |= clken_mask;
+
+  am67_epwm_putreg(AM67_MAIN_CTRL_MMR_BASE, AM67_CTRL_MMR_EPWM_TB_CLKEN,
+                   regval);
+
+  regval = am67_epwm_getreg(AM67_MAIN_CTRL_MMR_BASE,
+                            AM67_CTRL_MMR_EPWM_TB_CLKEN);
+
+  if ((regval & clken_mask) == 0u)
+    {
+      pwmerr("ERROR: Could not enable EPWM clock: TB_CLKEN: 0x%08" PRIx32
+             "\n", regval);
+      return -EIO;
+    }
+
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_disable_clock
+ *
+ * Description:
+ *   Gate one instance's time-base clock off.  Read-modify-write: bit 2
+ *   of the shared TB_CLKEN register is the cooling fan (EPWM2) and must
+ *   not be disturbed.  Read back to verify.
+ *
+ * Returned Value:
+ *   Zero (OK) on success; -EIO if the clock gate did not disable.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_disable_clock(uint32_t clken_mask)
+{
+  uint32_t regval = am67_epwm_getreg(AM67_MAIN_CTRL_MMR_BASE,
+                                     AM67_CTRL_MMR_EPWM_TB_CLKEN);
+
+  regval &= ~clken_mask;
+
+  am67_epwm_putreg(AM67_MAIN_CTRL_MMR_BASE, AM67_CTRL_MMR_EPWM_TB_CLKEN,
+                   regval);
+
+  regval = am67_epwm_getreg(AM67_MAIN_CTRL_MMR_BASE,
+                            AM67_CTRL_MMR_EPWM_TB_CLKEN);
+
+  if ((regval & clken_mask) != 0u)
+    {
+      pwmerr("ERROR: Could not disable EPWM clock: TB_CLKEN: 0x%08" PRIx32
+             "\n", regval);
+      return -EIO;
+    }
+
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_check_pid
+ *
+ * Description:
+ *   Read the EPWM peripheral ID register and compare it against the
+ *   expected value.  Verifies that the module is powered and that the
+ *   base address is correct.  An unpowered domain reads zeros without
+ *   any bus fault, so PID == 0 means "not powered".
+ *
+ * Returned Value:
+ *   Zero (OK) on success; -EIO if the PID does not match.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_check_pid(uint32_t base)
+{
+  uint32_t regval = am67_epwm_getreg(base, AM67_EPWM_PID_OFFSET);
+
+  if (regval != AM67_EPWM_PID_EXPECTED)
+    {
+      pwmerr("ERROR: Unexpected EPWM PID: 0x%08" PRIx32
+             " (expected 0x%08" PRIx32 ")\n",
+             regval, (uint32_t)AM67_EPWM_PID_EXPECTED);
+      return -EIO;
+    }
+
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_config_aqctl
+ *
+ * Description:
+ *   Program the action qualifier for one output: SET at counter zero,
+ *   CLEAR at that output's own compare event on the way up (CAU/CMPA
+ *   for channel 1, CBU/CMPB for channel 2).  Up-count asymmetric
+ *   recipe: duty is proportional to the compare value.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_config_aqctl(uint32_t base, int8_t channel)
+{
+  uint32_t offset = (channel == 1) ? AM67_EPWM_AQCTLA_OFFSET
+                                   : AM67_EPWM_AQCTLB_OFFSET;
+  uint16_t regval = am67_epwm_getreg16(base, offset);
+
+  if (channel == 1)
+    {
+      regval |= (AM67_EPWM_AQ_SET << AM67_EPWM_AQCTLA_ZRO_SHIFT);
+      regval |= (AM67_EPWM_AQ_CLEAR << AM67_EPWM_AQCTLA_CAU_SHIFT);
+    }
+  else
+    {
+      regval |= (AM67_EPWM_AQ_SET << AM67_EPWM_AQCTLB_ZRO_SHIFT);
+      regval |= (AM67_EPWM_AQ_CLEAR << AM67_EPWM_AQCTLB_CBU_SHIFT);
+    }
+
+  am67_epwm_putreg16(base, offset, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_clear_aqctl
+ *
+ * Description:
+ *   Return one output's action qualifier to its reset state (no actions
+ *   on any event).  Part of the shutdown teardown.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_clear_aqctl(uint32_t base, int8_t channel)
+{
+  uint32_t offset = (channel == 1) ? AM67_EPWM_AQCTLA_OFFSET
+                                   : AM67_EPWM_AQCTLB_OFFSET;
+  uint16_t regval = am67_epwm_getreg16(base, offset);
+
+  if (channel == 1)
+    {
+      regval &= ~AM67_EPWM_AQCTLA_ZRO_MASK;
+      regval &= ~AM67_EPWM_AQCTLA_CAU_MASK;
+    }
+  else
+    {
+      regval &= ~AM67_EPWM_AQCTLB_ZRO_MASK;
+      regval &= ~AM67_EPWM_AQCTLB_CBU_MASK;
+    }
+
+  am67_epwm_putreg16(base, offset, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_rldcsf_immediate
+ *
+ * Description:
+ *   Set the AQCSFRC load mode to immediate.  Must precede any force
+ *   write while the counter may be frozen: otherwise the AQCSFRC shadow
+ *   never loads and the force silently never takes effect.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_rldcsf_immediate(uint32_t base)
+{
+  uint16_t regval = am67_epwm_getreg16(base, AM67_EPWM_AQSFRC_OFFSET);
+
+  regval &= ~AM67_EPWM_AQSFRC_RLDCSF_MASK;
+  regval |= (AM67_EPWM_AQSFRC_RLDCSF_IMMEDIATE <<
+             AM67_EPWM_AQSFRC_RLDCSF_SHIFT);
+
+  am67_epwm_putreg16(base, AM67_EPWM_AQSFRC_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_park_outputs
+ *
+ * Description:
+ *   Park BOTH pins low via continuous software force.  One composed
+ *   write: the stop path owns every field of AQCSFRC (both targets are
+ *   known), so both pins park in the same instruction with no window
+ *   where one waves on alone.  The force is a mask in front of the AQ
+ *   output latch, not a write to it; releasing it exposes whatever the
+ *   latch last held.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_park_outputs(uint32_t base)
+{
+  uint16_t regval;
+
+  am67_epwm_rldcsf_immediate(base);
+
+  regval  = (AM67_EPWM_CSFA_FORCE_LOW << AM67_EPWM_AQCSFRC_CSFA_SHIFT);
+  regval |= (AM67_EPWM_CSFA_FORCE_LOW << AM67_EPWM_AQCSFRC_CSFB_SHIFT);
+
+  am67_epwm_putreg16(base, AM67_EPWM_AQCSFRC_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_force_release
+ *
+ * Description:
+ *   Release ONE channel's continuous software force so that pin follows
+ *   the AQ output again.  Read-modify-write of that channel's 2-bit
+ *   field only: CSFA and CSFB share this register, and a full write
+ *   here would release the neighbor's park as a side effect.  Called
+ *   only after that channel is wired and the counter runs: the AQ latch
+ *   is then live wave state, so the release exposes the true waveform
+ *   and not a stale level.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_force_release(uint32_t base, int8_t channel)
+{
+  uint16_t regval;
+
+  am67_epwm_rldcsf_immediate(base);
+
+  regval = am67_epwm_getreg16(base, AM67_EPWM_AQCSFRC_OFFSET);
+
+  if (channel == 1)
+    {
+      regval &= ~AM67_EPWM_AQCSFRC_CSFA_MASK;
+      regval |= (AM67_EPWM_CSFA_FORCE_DISABLE <<
+                 AM67_EPWM_AQCSFRC_CSFA_SHIFT);
+    }
+  else
+    {
+      regval &= ~AM67_EPWM_AQCSFRC_CSFB_MASK;
+      regval |= (AM67_EPWM_CSFA_FORCE_DISABLE <<
+                 AM67_EPWM_AQCSFRC_CSFB_SHIFT);
+    }
+
+  am67_epwm_putreg16(base, AM67_EPWM_AQCSFRC_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_set_tbctl
+ *
+ * Description:
+ *   Compose the static TBCTL policy with the counter frozen: CTRMODE =
+ *   stop-freeze (ignition is start()'s separate final step), PRDLD
+ *   immediate (a shadowed TBPRD would wait for a zero event that a
+ *   frozen counter never generates), SYNCO off.  Full compose, not RMW:
+ *   this function owns every policy field.  The frequency-dependent
+ *   fields (HSPCLKDIV/CLKDIV) belong to am67_epwm_set_clock_values().
+ *
+ ****************************************************************************/
+
+static void am67_epwm_set_tbctl(uint32_t base)
+{
+  uint16_t regval = (AM67_EPWM_TBCTL_CTRMODE_STOP_FREEZE <<
+                     AM67_EPWM_TBCTL_CTRMODE_SHIFT);
+
+  regval |= AM67_EPWM_TBCTL_PRDLD_IMMEDIATE;
+  regval |= (3u << AM67_EPWM_TBCTL_SYNCOSEL_SHIFT);  /* 3 = SYNCO off */
+
+  am67_epwm_putreg16(base, AM67_EPWM_TBCTL_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_set_cmpctl
+ *
+ * Description:
+ *   Configure counter-compare for both channels: CMPA and CMPB shadowed
+ *   (SHDWxMODE=0), shadows loaded into the active registers on the PRD
+ *   event (LOADxMODE=1) so a duty update never races the SET action at
+ *   counter zero.  Per-module policy: both channels' compares behave
+ *   identically whether or not channel B is in use.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_set_cmpctl(uint32_t base)
+{
+  uint16_t regval = 0u;
+
+  regval |= (1u << AM67_EPWM_CMPCTL_LOADAMODE_SHIFT);
+  regval |= (1u << AM67_EPWM_CMPCTL_LOADBMODE_SHIFT);
+
+  am67_epwm_putreg16(base, AM67_EPWM_CMPCTL_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_clear_cmpctl
+ *
+ * Description:
+ *   Return CMPCTL to its reset state (all fields zero).
+ *
+ ****************************************************************************/
+
+static void am67_epwm_clear_cmpctl(uint32_t base)
+{
+  am67_epwm_putreg16(base, AM67_EPWM_CMPCTL_OFFSET, 0u);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_tbctl_ctrmode_freeze
+ *
+ * Description:
+ *   Freeze the time-base counter (CTRMODE = stop-freeze).  Freeze holds
+ *   the current count and pin level; it does not clear them.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_tbctl_ctrmode_freeze(uint32_t base)
+{
+  uint16_t regval = am67_epwm_getreg16(base, AM67_EPWM_TBCTL_OFFSET);
+
+  regval &= ~AM67_EPWM_TBCTL_CTRMODE_MASK;
+  regval |= (AM67_EPWM_TBCTL_CTRMODE_STOP_FREEZE <<
+             AM67_EPWM_TBCTL_CTRMODE_SHIFT);
+
+  am67_epwm_putreg16(base, AM67_EPWM_TBCTL_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_tbctl_ctrmode_up
+ *
+ * Description:
+ *   Ignition: flip CTRMODE from freeze to up-count with everything else
+ *   already configured.  Clear-then-set on the live register.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_tbctl_ctrmode_up(uint32_t base)
+{
+  uint16_t regval = am67_epwm_getreg16(base, AM67_EPWM_TBCTL_OFFSET);
+
+  regval &= ~AM67_EPWM_TBCTL_CTRMODE_MASK;
+  regval |= (AM67_EPWM_TBCTL_CTRMODE_UP << AM67_EPWM_TBCTL_CTRMODE_SHIFT);
+
+  am67_epwm_putreg16(base, AM67_EPWM_TBCTL_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_reset_tbcnt
+ *
+ * Description:
+ *   Zero the time-base counter (safe while frozen; clears stale count).
+ *
+ ****************************************************************************/
+
+static void am67_epwm_reset_tbcnt(uint32_t base)
+{
+  am67_epwm_putreg16(base, AM67_EPWM_TBCNT_OFFSET, 0u);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_calculate_clock_values
+ *
+ * Description:
+ *   Pure divider solver (no register access, no driver state): map a
+ *   requested pin frequency onto the EPWM clock tree,
+ *
+ *     f_pin = FICLK / (hsp * clk * (tbprd + 1))
+ *
+ *   hsp and clk are returned as divider values (1..14 / 1..128); the
+ *   register field encodings (2n vs 2^n) are deliberately kept out of
+ *   the math.  ticks = FICLK / frequency truncates: worst-case error is
+ *   under one tick, ppm-level at any frequency this driver serves.
+ *
+ *   Selection policy: must fit in 16-bit TBPRD; prefer exact division
+ *   (zero frequency error); among equals prefer the smallest total
+ *   divider (largest TBPRD+1 = finest duty resolution).
+ *
+ * Returned Value:
+ *   OK on success; -ERANGE if the frequency is 0, above the 125 MHz
+ *   2-tick ceiling, or below the ~2.13 Hz divide-by-1792 floor.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_calculate_clock_values(uint32_t frequency,
+                                            uint16_t *hsp, uint16_t *clk,
+                                            uint16_t *tbprd)
+{
+  /* The silicon's divider menus: HSPCLKDIV is even steps, CLKDIV is
+   * powers of two.  Only pairs from these menus are representable.
+   */
+
+  static const uint16_t hsp_menu[] =
+  {
+    1, 2, 4, 6, 8, 10, 12, 14
+  };
+
+  static const uint16_t clk_menu[] =
+  {
+    1, 2, 4, 8, 16, 32, 64, 128
+  };
+
+  uint32_t ticks;
+  uint32_t best_div = 0;
+  bool best_exact = false;
+  int i;
+  int j;
+
+  if (frequency == 0u)
+    {
+      return -ERANGE;
+    }
+
+  ticks = AM67_EPWM_FICLK_HZ / frequency;
+
+  if (ticks < AM67_EPWM_MIN_TICKS)
+    {
+      return -ERANGE;
+    }
+
+  if (ticks <= AM67_EPWM_MAX_TICKS)
+    {
+      *hsp   = 1;
+      *clk   = 1;
+      *tbprd = (uint16_t)(ticks - 1u);
+      return OK;
+    }
+
+  /* Too many ticks for a 16-bit period: brute-force all 64 menu pairs.
+   * 64 iterations of integer math beats clever factoring for both
+   * correctness and readability.
+   */
+
+  for (i = 0; i < 8; i++)
+    {
+      for (j = 0; j < 8; j++)
+        {
+          uint32_t div = (uint32_t)hsp_menu[i] * clk_menu[j];
+          bool exact;
+
+          if (ticks > div * AM67_EPWM_MAX_TICKS)
+            {
+              continue; /* Does not fit in TBPRD with this divider */
+            }
+
+          exact = ((ticks % div) == 0u);
+
+          if (best_div == 0u ||
+              (exact && !best_exact) ||
+              (exact == best_exact && div < best_div))
+            {
+              best_div   = div;
+              best_exact = exact;
+              *hsp       = hsp_menu[i];
+              *clk       = clk_menu[j];
+            }
+        }
+    }
+
+  if (best_div == 0u)
+    {
+      return -ERANGE;
+    }
+
+  *tbprd = (uint16_t)(ticks / best_div - 1u);
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_loop_log2
+ *
+ * Description:
+ *   Integer log2 of a power of two (CLKDIV divider value to register
+ *   field code: 1->0, 2->1, ... 128->7).
+ *
+ ****************************************************************************/
+
+static uint16_t am67_epwm_loop_log2(uint16_t value)
+{
+  uint16_t result = 0;
+
+  while (value >>= 1)
+    {
+      result++;
+    }
+
+  return result;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_set_clock_values
+ *
+ * Description:
+ *   Write the solver's results to the hardware: TBPRD, then HSPCLKDIV
+ *   and CLKDIV (clear-then-set: the register is live and the fields may
+ *   need to shrink, which OR alone cannot do).  This is the only place
+ *   that knows the divider field encodings: HSPCLKDIV is div/2 (with
+ *   1 -> 0 falling out of integer division), CLKDIV is log2.
+ *
+ ****************************************************************************/
+
+static void am67_epwm_set_clock_values(uint32_t base, uint16_t hsp,
+                                       uint16_t clk, uint16_t tbprd)
+{
+  uint16_t regval;
+
+  am67_epwm_putreg16(base, AM67_EPWM_TBPRD_OFFSET, tbprd);
+
+  regval = am67_epwm_getreg16(base, AM67_EPWM_TBCTL_OFFSET);
+
+  regval &= ~AM67_EPWM_TBCTL_HSPCLKDIV_MASK;
+  regval |= (hsp / 2) << AM67_EPWM_TBCTL_HSPCLKDIV_SHIFT;
+
+  regval &= ~AM67_EPWM_TBCTL_CLKDIV_MASK;
+  regval |= am67_epwm_loop_log2(clk) << AM67_EPWM_TBCTL_CLKDIV_SHIFT;
+
+  am67_epwm_putreg16(base, AM67_EPWM_TBCTL_OFFSET, regval);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_set_duty
+ *
+ * Description:
+ *   Convert the ub16 duty fraction to compare ticks and write it to the
+ *   given channel's compare register (CMPA for channel 1, CMPB for
+ *   channel 2).  The write lands in the shadow register and loads at
+ *   the next PRD event (glitch-free live update).  Worst case product
+ *   is 65535 * 65536 < 2^32: no overflow in 32-bit math.  duty = 0
+ *   gives exact 0% (compare outranks zero in AQ priority); exact 100%
+ *   is unreachable by the ub16 format itself (max 65535/65536).
+ *
+ ****************************************************************************/
+
+static void am67_epwm_set_duty(struct pwm_lowerhalf_s *dev, int8_t channel,
+                               ub16_t duty)
+{
+  struct am67_epwm_s *priv = (struct am67_epwm_s *)dev;
+  uint32_t offset = (channel == 1) ? AM67_EPWM_CMPA_OFFSET
+                                   : AM67_EPWM_CMPB_OFFSET;
+  uint16_t cmp = (uint16_t)((duty * (priv->tbprd + 1u)) >> 16);
+
+  am67_epwm_putreg16(priv->base, offset, cmp);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_setup
+ *
+ * Description:
+ *   Called by the upper half on the first open of /dev/pwm0.  Bring the
+ *   module to a configured-but-silent state: clock on, PID sanity
+ *   check, pinmux, and the parameter-independent policies (CMPCTL,
+ *   AQCTLA, frozen TBCTL).  No pulses until start().
+ *
+ * Returned Value:
+ *   Zero (OK) on success; a negated errno from the first failing step.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_setup(struct pwm_lowerhalf_s *dev)
+{
+  struct am67_epwm_s *priv = (struct am67_epwm_s *)dev;
+  int ret;
+
+  ret = am67_epwm_enable_clock(priv->clken_mask);
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  ret = am67_epwm_check_pid(priv->base);
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  am67_epwm_pinmux_init(priv->pinmux_id);
+  am67_epwm_set_cmpctl(priv->base);
+  am67_epwm_set_tbctl(priv->base);
+
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_shutdown
+ *
+ * Description:
+ *   Called by the upper half on the last close.  Teardown in reverse
+ *   order of setup: park the pin (stop handles a still-running wave for
+ *   free), clear the policy registers, then gate the clock off last.
+ *
+ * Returned Value:
+ *   Zero (OK) on success; a negated errno value on failure.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_shutdown(struct pwm_lowerhalf_s *dev)
+{
+  struct am67_epwm_s *priv = (struct am67_epwm_s *)dev;
+
+  am67_epwm_stop(dev);
+  am67_epwm_clear_aqctl(priv->base, 1);
+  am67_epwm_clear_aqctl(priv->base, 2);
+  am67_epwm_clear_cmpctl(priv->base);
+
+  return am67_epwm_disable_clock(priv->clken_mask);
+}
+
+/****************************************************************************
+ * Name: am67_epwm_start
+ *
+ * Description:
+ *   Start (or update) the pulsed output(s).  One unified path; the
+ *   module-level work is conditional on a frequency change:
+ *
+ *   VALIDATE: scan the channel array (channel 1 = output A, 2 = B;
+ *     0 skips a slot, -1 ends the array; anything else, a duplicate
+ *     channel, or an active duty above ub16 is rejected).  Any failure
+ *     returns here with neither hardware nor cached state touched, so
+ *     a running wave survives a bad request unchanged.  A request with
+ *     no active channels is a legal no-op.
+ *   ACT (module, only if the frequency changed): solve dividers, stop
+ *     first (parks both pins and clears the active set, so every
+ *     requested channel re-wires as a joiner below), program the
+ *     timebase.
+ *   ACT (per channel, always): write the compare; a channel not in the
+ *     active set is a joiner and gets its AQ wiring here.  While
+ *     AQCTLx is still reset (all zero) the compare write is inert, so
+ *     configuring a joiner cannot glitch a live neighbor.
+ *   IGNITE: only a frequency change needs it; on a pure join the
+ *     counter is already running.  Joiners' forces are released after
+ *     ignition (the AQ latch is then live wave state), one field-RMW
+ *     each so an already-running neighbor's force bits are untouched.
+ *   COMMIT: frequency/tbprd land just before the module writes because
+ *     set_duty derives the compare from priv->tbprd; the active set is
+ *     updated last, after the writes that cannot fail.
+ *
+ *   The first period after (re)ignition runs with the previous active
+ *   compare (0 on a fresh start = one silent period): accepted,
+ *   safe-low.
+ *
+ * Returned Value:
+ *   Zero (OK) on success; -EINVAL for a bad channel array or duty;
+ *   -ERANGE if the frequency cannot be produced.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_start(struct pwm_lowerhalf_s *dev,
+                           const struct pwm_info_s *info)
+{
+  struct am67_epwm_s *priv = (struct am67_epwm_s *)dev;
+  uint8_t requested = 0;
+  uint8_t joiners = 0;
+  uint16_t hsp = 0;
+  uint16_t clk = 0;
+  uint16_t tbprd = 0;
+  bool freq_changed;
+  int8_t ch;
+  int ret;
+  int i;
+
+  /* VALIDATE: one pass over the array, no writes.  The requested mask
+   * doubles as the duplicate detector: a channel's bit already set
+   * means the caller named it twice with (potentially) two different
+   * duties, and guessing which one was meant is not this driver's job.
+   */
+
+  for (i = 0; i < CONFIG_PWM_NCHANNELS; i++)
+    {
+      ch = info->channels[i].channel;
+
+      if (ch == -1)
+        {
+          break;
+        }
+
+      if (ch == 0)
+        {
+          continue;
+        }
+
+      if (ch != 1 && ch != 2)
+        {
+          pwmerr("ERROR: No such channel: %d\n", ch);
+          return -EINVAL;
+        }
+
+      if ((requested & CH_BIT(ch)) != 0u)
+        {
+          pwmerr("ERROR: Channel %d requested twice\n", ch);
+          return -EINVAL;
+        }
+
+      if (info->channels[i].duty > 0xffff)
+        {
+          pwmerr("ERROR: Duty out of range on channel %d\n", ch);
+          return -EINVAL;
+        }
+
+      requested |= CH_BIT(ch);
+    }
+
+  if (requested == 0u)
+    {
+      return OK;
+    }
+
+  freq_changed = (priv->frequency != info->frequency);
+
+  if (freq_changed)
+    {
+      ret = am67_epwm_calculate_clock_values(info->frequency, &hsp, &clk,
+                                             &tbprd);
+      if (ret < 0)
+        {
+          pwmerr("ERROR: Cannot produce %" PRIu32 " Hz, keeping %" PRIu32
+                 " Hz\n", info->frequency, priv->frequency);
+          return ret;
+        }
+
+      /* Commit: nothing below can fail (void register writes only),
+       * and set_duty derives the compare from priv->tbprd, so priv
+       * must be current before the writes begin.
+       */
+
+      priv->frequency = info->frequency;
+      priv->tbprd     = tbprd;
+
+      /* Stop-first: parks both pins and empties the active set, which
+       * makes every requested channel a joiner below - the frequency
+       * change re-ignites through the same join mechanism as a fresh
+       * start.
+       */
+
+      am67_epwm_stop(dev);
+      am67_epwm_set_clock_values(priv->base, hsp, clk, tbprd);
+      am67_epwm_reset_tbcnt(priv->base);
+    }
+
+  for (i = 0; i < CONFIG_PWM_NCHANNELS; i++)
+    {
+      ch = info->channels[i].channel;
+
+      if (ch == -1)
+        {
+          break;
+        }
+
+      if (ch == 0)
+        {
+          continue;
+        }
+
+      am67_epwm_set_duty(dev, ch, info->channels[i].duty);
+
+      /* A channel not yet in the active set is a joiner: wire its AQ
+       * actions here; its force is released after ignition below.
+       */
+
+      if ((priv->active_channels & CH_BIT(ch)) == 0u)
+        {
+          am67_epwm_config_aqctl(priv->base, ch);
+          joiners |= CH_BIT(ch);
+        }
+    }
+
+  if (freq_changed)
+    {
+      am67_epwm_tbctl_ctrmode_up(priv->base);
+    }
+
+  /* Force release LAST: post-ignition the AQ latch carries live wave
+   * state, not a fossil.  Field-RMW per joiner - a channel that was
+   * already waving keeps its (released) force bits untouched, and a
+   * channel that stays parked keeps its park.
+   */
+
+  if ((joiners & CH_A_ACTIVE) != 0u)
+    {
+      am67_epwm_force_release(priv->base, 1);
+    }
+
+  if ((joiners & CH_B_ACTIVE) != 0u)
+    {
+      am67_epwm_force_release(priv->base, 2);
+    }
+
+  priv->active_channels |= requested;
+
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_stop
+ *
+ * Description:
+ *   Park both outputs: force the pins low (RLDCSF first, one composed
+ *   write for both channels - no window where one waves on alone),
+ *   then freeze the counter.  The order is load-bearing: the force
+ *   masks the pins before the freeze fossilizes the AQ latches at
+ *   arbitrary levels.  Both caches are invalidated: frequency = 0 (no
+ *   wave) so a same-frequency restart cannot skip the module block on
+ *   a frozen timebase, and the active set is emptied so every channel
+ *   of the next start re-wires and re-releases as a joiner.  The books
+ *   go dark here because this is where reality does.
+ *
+ * Returned Value:
+ *   Zero (OK) always.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_stop(struct pwm_lowerhalf_s *dev)
+{
+  struct am67_epwm_s *priv = (struct am67_epwm_s *)dev;
+
+  am67_epwm_park_outputs(priv->base);
+  am67_epwm_tbctl_ctrmode_freeze(priv->base);
+
+  priv->frequency       = 0;
+  priv->active_channels = 0;
+
+  return OK;
+}
+
+/****************************************************************************
+ * Name: am67_epwm_ioctl
+ *
+ * Description:
+ *   No platform-specific ioctl commands are supported.
+ *
+ * Returned Value:
+ *   -ENOTTY always.
+ *
+ ****************************************************************************/
+
+static int am67_epwm_ioctl(struct pwm_lowerhalf_s *dev,
+                           int cmd, unsigned long arg)
+{
+  return -ENOTTY;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: am67_epwminitialize
+ *
+ * Description:
+ *   Return the EPWM lower-half instance for the given PWM number so the
+ *   board bringup can bind it to the upper half with pwm_register().
+ *   No hardware is touched here.
+ *
+ * Input Parameters:
+ *   pwm - PWM instance number: 0 (EPWM0) or 1 (EPWM1).  EPWM2 is the
+ *   board cooling fan and is deliberately not supported.
+ *
+ * Returned Value:
+ *   Pointer to the lower-half driver on success; NULL on an unsupported
+ *   or unconfigured instance number.
+ *
+ ****************************************************************************/
+
+struct pwm_lowerhalf_s *am67_epwminitialize(int pwm)
+{
+  switch (pwm)
+    {
+#ifdef CONFIG_AM67_EPWM0
+      case 0:
+        pwminfo("Initialize EPWM%d\n", pwm);
+        return (struct pwm_lowerhalf_s *)&g_am67_epwm0;
+#endif
+
+#ifdef CONFIG_AM67_EPWM1
+      case 1:
+        pwminfo("Initialize EPWM%d\n", pwm);
+        return (struct pwm_lowerhalf_s *)&g_am67_epwm1;
+#endif
+
+      default:
+        pwmerr("ERROR: No such PWM instance: %d\n", pwm);
+        return NULL;
+    }
+}
+
+/****************************************************************************
+ * Name: am67_epwm_init
+ *
+ * Description:
+ *   Boot-time EPWM preparation: unlock CTRL_MMR partition 1 (kick lock)
+ *   so that the clock-gate and pad writes issued later by the PWM
+ *   lower-half setup() can land.  Everything else (clock enable, PID
+ *   check, pinmux, waveform) is done by the lower-half ops, called by
+ *   the upper half on open/ioctl.  Must run before pwm_register().
+ *
+ * Assumptions:
+ *   The EPWM0 power domain must be on (it is managed by the DMSC/TISCI
+ *   firmware, not by this driver).  On the current board it has been
+ *   observed to stay on without any intervention (mechanism unconfirmed;
+ *   possibly shared with the EPWM2 cooling fan or an unreaped boot
+ *   default) - do NOT rely on this.  The only explicit guarantee today
+ *   is forcing the device active from Linux before the R5F starts:
+ *
+ *     echo on > /sys/devices/platform/bus@f0000/23000000.pwm/power/control
+ *
+ *   ("on" disables Linux runtime power management for that device, which
+ *   keeps the domain powered until reboot.)  If the domain is off, EPWM
+ *   registers read as zeros - no bus fault - so a PID mismatch of
+ *   0x00000000 means "not powered", not "wrong address".  Sub-word
+ *   (8-bit) accesses also read as zeros; use 16/32-bit accesses only.
+ *
+ * WARNING: This power domain is currently held up by an unidentified
+ *   party on the Linux side.  A Linux reboot or deliberate PM action
+ *   (pwmchip unexport, driver unbind, 'echo auto') WILL release it, and
+ *   if the holder is the EPWM2 fan's thermal policy, it could drop
+ *   spontaneously (e.g. fan off when cool) - unverified but not
+ *   excluded.  Either way the R5F gets no notification; outputs die
+ *   silently.  Do not drive safety-critical loads (motors, ESCs) until
+ *   a NuttX-side TISCI client owns this domain.
+ *
+ * Returned Value:
+ *   Zero (OK) on success; a negated errno value from the first failing
+ *   step otherwise.
+ *
+ ****************************************************************************/
+
+int am67_epwm_init(void)
+{
+  return am67_epwm_enable_register_write();
+}
+
diff --git a/arch/arm/src/am67/am67_pwm.h b/arch/arm/src/am67/am67_pwm.h
new file mode 100644
index 00000000000..daa13297916
--- /dev/null
+++ b/arch/arm/src/am67/am67_pwm.h
@@ -0,0 +1,105 @@
+/****************************************************************************
+ * arch/arm/src/am67/am67_pwm.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+#ifndef __ARCH_ARM_SRC_AM67_AM67_PWM_H
+#define __ARCH_ARM_SRC_AM67_AM67_PWM_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+#include <nuttx/timers/pwm.h>
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Public Function Prototypes
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: am67_epwm_init
+ *
+ * Description:
+ *   Boot-time EPWM preparation: unlock CTRL_MMR partition 1 (kick lock)
+ *   so that the clock-gate and pad writes issued later by the PWM
+ *   lower-half setup() can land.  Everything else (clock enable, PID
+ *   check, pinmux, waveform) is done by the lower-half ops, called by
+ *   the upper half on open/ioctl.  Must run before pwm_register().
+ *
+ * Assumptions:
+ *   The EPWM0 power domain must be on (it is managed by the DMSC/TISCI
+ *   firmware, not by this driver).  On the current board it has been
+ *   observed to stay on without any intervention (mechanism unconfirmed;
+ *   possibly shared with the EPWM2 cooling fan or an unreaped boot
+ *   default) - do NOT rely on this.  The only explicit guarantee today
+ *   is forcing the device active from Linux before the R5F starts:
+ *
+ *     echo on > /sys/devices/platform/bus@f0000/23000000.pwm/power/control
+ *
+ *   ("on" disables Linux runtime power management for that device, which
+ *   keeps the domain powered until reboot.)  If the domain is off, EPWM
+ *   registers read as zeros - no bus fault - so a PID mismatch of
+ *   0x00000000 means "not powered", not "wrong address".  Sub-word
+ *   (8-bit) accesses also read as zeros; use 16/32-bit accesses only.
+ *
+ * WARNING: This power domain is currently held up by an unidentified
+ *   party on the Linux side.  A Linux reboot or deliberate PM action
+ *   (pwmchip unexport, driver unbind, 'echo auto') WILL release it, and
+ *   if the holder is the EPWM2 fan's thermal policy, it could drop
+ *   spontaneously (e.g. fan off when cool) - unverified but not
+ *   excluded.  Either way the R5F gets no notification; outputs die
+ *   silently.  Do not drive safety-critical loads (motors, ESCs) until
+ *   a NuttX-side TISCI client owns this domain.
+ *
+ * Returned Value:
+ *   Zero (OK) on success; a negated errno value from the first failing
+ *   step otherwise.
+ *
+ ****************************************************************************/
+
+int am67_epwm_init(void);
+
+/****************************************************************************
+ * Name: am67_epwminitialize
+ *
+ * Description:
+ *   Return the EPWM lower-half instance for the given PWM number so the
+ *   board bringup can bind it to the upper half with pwm_register().
+ *   No hardware is touched here; the upper half drives the hardware
+ *   through the ops (setup on first open, start/stop via ioctl).
+ *
+ * Input Parameters:
+ *   pwm - PWM instance number: 0 (EPWM0) or 1 (EPWM1).  EPWM2 is the
+ *   board cooling fan and is deliberately not supported.
+ *
+ * Returned Value:
+ *   Pointer to the lower-half driver on success; NULL on an unsupported
+ *   or unconfigured instance number.
+ *
+ ****************************************************************************/
+
+struct pwm_lowerhalf_s *am67_epwminitialize(int pwm);
+
+#endif /* __ARCH_ARM_SRC_AM67_AM67_PWM_H */
diff --git a/arch/arm/src/am67/am67_pwm_hw.h b/arch/arm/src/am67/am67_pwm_hw.h
new file mode 100644
index 00000000000..4e0580b355a
--- /dev/null
+++ b/arch/arm/src/am67/am67_pwm_hw.h
@@ -0,0 +1,146 @@
+/****************************************************************************
+ * arch/arm/src/am67/am67_pwm_hw.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+#ifndef __ARCH_ARM_SRC_AM67_AM67_PWM_HW_H
+#define __ARCH_ARM_SRC_AM67_AM67_PWM_HW_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+/* Base addresses */
+
+#define AM67_EPWM0_BASE                 0x23000000
+#define AM67_EPWM1_BASE                 0x23010000
+
+/* Expected values */
+
+#define AM67_EPWM_PID_EXPECTED          0x44d10903u
+
+/* AQ Module Actions */
+#define AM67_EPWM_AQ_NOP             0
+#define AM67_EPWM_AQ_CLEAR           1
+#define AM67_EPWM_AQ_SET             2
+#define AM67_EPWM_AQ_TOGGLE          3
+
+/* CSFA Actions */
+#define AM67_EPWM_CSFA_DISABLE                0
+#define AM67_EPWM_CSFA_FORCE_LOW              1
+#define AM67_EPWM_CSFA_FORCE_HIGH             2
+#define AM67_EPWM_CSFA_FORCE_DISABLE          3
+
+/* CTR Modes */
+#define AM67_EPWM_TBCTL_CTRMODE_UP                 0
+#define AM67_EPWM_TBCTL_CTRMODE_DOWN               1
+#define AM67_EPWM_TBCTL_CTRMODE_UP_DOWN            2
+#define AM67_EPWM_TBCTL_CTRMODE_STOP_FREEZE        3
+
+/* Register offsets *********************************************************/
+
+#define AM67_EPWM_PID_OFFSET            0x05c
+
+/* TB module */
+
+#define AM67_EPWM_TBCTL_OFFSET          0x000
+#define AM67_EPWM_TBSTS_OFFSET          0x002
+#define AM67_EPWM_TBPHS_OFFSET          0x006
+#define AM67_EPWM_TBCNT_OFFSET          0x008
+#define AM67_EPWM_TBPRD_OFFSET          0x00a
+
+/* CC module */
+
+#define AM67_EPWM_CMPCTL_OFFSET         0x00e
+#define AM67_EPWM_CMPA_OFFSET           0x012
+#define AM67_EPWM_CMPB_OFFSET           0x014
+
+/* AQ module */
+
+#define AM67_EPWM_AQCTLA_OFFSET         0x016
+#define AM67_EPWM_AQCTLB_OFFSET         0x018
+#define AM67_EPWM_AQSFRC_OFFSET         0x01a
+#define AM67_EPWM_AQCSFRC_OFFSET        0x01c
+
+/* Register bit field definitions *******************************************/
+
+/* Time-Base Control Register (TBCTL) */
+
+#define AM67_EPWM_TBCTL_CTRMODE_SHIFT                   (0)
+#define AM67_EPWM_TBCTL_CTRMODE_MASK                    (3u << 0)
+#define AM67_EPWM_TBCTL_PHSEN_SHIFT                     (2)
+#define AM67_EPWM_TBCTL_PHSEN_MASK                      (1u << 2)
+#define AM67_EPWM_TBCTL_PRDLD_IMMEDIATE                 (1u << 3)
+#define AM67_EPWM_TBCTL_SYNCOSEL_SHIFT                  (4)
+#define AM67_EPWM_TBCTL_SYNCOSEL_MASK                   (3u << 4)
+#define AM67_EPWM_TBCTL_SWFSYNC_SHIFT                   (6)
+#define AM67_EPWM_TBCTL_SWFSYNC_MASK                    (1u << 6)
+#define AM67_EPWM_TBCTL_HSPCLKDIV_SHIFT                 (7)
+#define AM67_EPWM_TBCTL_HSPCLKDIV_MASK                  (7u << 7)
+#define AM67_EPWM_TBCTL_CLKDIV_SHIFT                    (10)
+#define AM67_EPWM_TBCTL_CLKDIV_MASK                     (7u << 10)
+#define AM67_EPWM_TBCTL_PHSDIR_SHIFT                    (13)
+#define AM67_EPWM_TBCTL_PHSDIR_MASK                     (1u << 13)
+#define AM67_EPWM_TBCTL_FREE_SOFT_SHIFT                 (14)
+#define AM67_EPWM_TBCTL_FREE_SOFT_MASK                  (3u << 14)
+
+/* Counter-Compare Control Register (CMPCTL) */
+
+#define AM67_EPWM_CMPCTL_LOADAMODE_SHIFT                (0)
+#define AM67_EPWM_CMPCTL_LOADBMODE_SHIFT                (2)
+#define AM67_EPWM_CMPCTL_LOADAMODE_MASK                 (3u << 0)
+#define AM67_EPWM_CMPCTL_LOADBMODE_MASK                 (3u << 2)
+#define AM67_EPWM_CMPCTL_SHDWAMODE_IMMEDIATE_SHIFT      (4)
+#define AM67_EPWM_CMPCTL_SHDWBMODE_IMMEDIATE_SHIFT      (6)
+#define AM67_EPWM_CMPCTL_SHDWAMODE_IMMEDIATE            (1u << 4)
+#define AM67_EPWM_CMPCTL_SHDWBMODE_IMMEDIATE            (1u << 6)
+
+/* AQ Registers */
+
+#define AM67_EPWM_AQCTLA_ZRO_SHIFT                      (0)
+#define AM67_EPWM_AQCTLA_ZRO_MASK                       (3u << 0)
+#define AM67_EPWM_AQCTLA_CAU_SHIFT                      (4)
+#define AM67_EPWM_AQCTLA_CAU_MASK                       (3u << 4)
+
+/* AQCTLB has the same event field layout as AQCTLA; output B's compare
+ * event is CBU (CMPB, up-count), bits 9:8.
+ */
+
+#define AM67_EPWM_AQCTLB_ZRO_SHIFT                      (0)
+#define AM67_EPWM_AQCTLB_ZRO_MASK                       (3u << 0)
+#define AM67_EPWM_AQCTLB_CBU_SHIFT                      (8)
+#define AM67_EPWM_AQCTLB_CBU_MASK                       (3u << 8)
+
+/* AQCSFRC: CSFA and CSFB share this register; per-channel writes must be
+ * read-modify-write of the 2-bit field only.  The CSFA_* action codes
+ * above apply to both fields.
+ */
+
+#define AM67_EPWM_AQCSFRC_CSFA_SHIFT                    (0)
+#define AM67_EPWM_AQCSFRC_CSFA_MASK                     (3u << 0)
+#define AM67_EPWM_AQCSFRC_CSFB_SHIFT                    (2)
+#define AM67_EPWM_AQCSFRC_CSFB_MASK                     (3u << 2)
+#define AM67_EPWM_AQSFRC_RLDCSF_SHIFT                   (6)
+#define AM67_EPWM_AQSFRC_RLDCSF_MASK                    (3u << 6)
+#define AM67_EPWM_AQSFRC_RLDCSF_IMMEDIATE               (3)
+
+#endif /* __ARCH_ARM_SRC_AM67_AM67_PWM_HW_H */
diff --git a/boards/arm/am67/t3-gem-o1/configs/nsh/defconfig 
b/boards/arm/am67/t3-gem-o1/configs/nsh/defconfig
index 6504186a010..b2ad3f09949 100644
--- a/boards/arm/am67/t3-gem-o1/configs/nsh/defconfig
+++ b/boards/arm/am67/t3-gem-o1/configs/nsh/defconfig
@@ -14,6 +14,8 @@ CONFIG_16550_UART0_CLOCK=48000000
 CONFIG_16550_UART0_IRQ=211
 CONFIG_16550_UART0_SERIAL_CONSOLE=y
 CONFIG_16550_UART=y
+CONFIG_AM67_EPWM0=y
+CONFIG_AM67_EPWM1=y
 CONFIG_AM67_I2C=y
 CONFIG_AM67_MCSPI0=y
 CONFIG_AM67_WKUP_I2C0=y
@@ -29,6 +31,7 @@ CONFIG_ARM_MPU=y
 CONFIG_BOARD_LOOPSPERMSEC=2813
 CONFIG_BOOT_RUNFROMISRAM=y
 CONFIG_BUILTIN=y
+CONFIG_EXAMPLES_PWM=y
 CONFIG_FS_LINKS=y
 CONFIG_FS_PROCFS=y
 CONFIG_HAVE_CXX=y
@@ -41,6 +44,8 @@ CONFIG_NSH_MAXARGUMENTS=16
 CONFIG_NSH_READLINE=y
 CONFIG_PSEUDOFS_ATTRIBUTES=y
 CONFIG_PSEUDOFS_FILE=y
+CONFIG_PWM=y
+CONFIG_PWM_NCHANNELS=2
 CONFIG_RAM_SIZE=14680064
 CONFIG_RAM_START=0xA2200000
 CONFIG_READLINE_CMD_HISTORY=y
diff --git a/boards/arm/am67/t3-gem-o1/src/Makefile 
b/boards/arm/am67/t3-gem-o1/src/Makefile
index 2adc9793435..a26bb94c513 100644
--- a/boards/arm/am67/t3-gem-o1/src/Makefile
+++ b/boards/arm/am67/t3-gem-o1/src/Makefile
@@ -24,6 +24,11 @@ include $(TOPDIR)/Make.defs
 
 CSRCS = am67_bringup.c am67_appinit.c
 
+ifeq ($(CONFIG_AM67_EPWM0),y)
+CSRCS += am67_pwm.c
+CFLAGS += ${INCDIR_PREFIX}$(TOPDIR)/arch/arm/src/am67
+endif
+
 ifeq ($(CONFIG_AM67_MCSPI0),y)
 CSRCS += am67_spi.c
 CFLAGS += ${INCDIR_PREFIX}$(TOPDIR)/arch/arm/src/am67
diff --git a/boards/arm/am67/t3-gem-o1/src/am67_bringup.c 
b/boards/arm/am67/t3-gem-o1/src/am67_bringup.c
index 461956e98fb..3ba12324005 100644
--- a/boards/arm/am67/t3-gem-o1/src/am67_bringup.c
+++ b/boards/arm/am67/t3-gem-o1/src/am67_bringup.c
@@ -39,6 +39,10 @@
 #include "am67_i2c.h"
 #endif
 
+#if defined(CONFIG_AM67_EPWM0) || defined(CONFIG_AM67_EPWM1)
+#include "am67_pwm.h"
+#endif
+
 /****************************************************************************
  * Public Functions
  ****************************************************************************/
@@ -71,6 +75,60 @@ int am67_bringup(void)
   am67_i2cdev_initialize();
 #endif
 
+#if defined(CONFIG_AM67_EPWM0) || defined(CONFIG_AM67_EPWM1)
+  ret = am67_epwm_init();
+  if (ret < 0)
+    {
+      syslog(LOG_ERR, "ERROR: Failed to initialize EPWM: %d\n", ret);
+    }
+  else
+    {
+      struct pwm_lowerhalf_s *lower;
+
+      syslog(LOG_INFO, "EPWM: CTRL_MMR unlocked\n");
+
+#ifdef CONFIG_AM67_EPWM0
+      lower = am67_epwminitialize(0);
+      if (lower == NULL)
+        {
+          syslog(LOG_ERR, "ERROR: Failed to get EPWM0 lower half\n");
+        }
+      else
+        {
+          ret = pwm_register("/dev/pwm0", lower);
+          if (ret < 0)
+            {
+              syslog(LOG_ERR, "ERROR: pwm_register failed: %d\n", ret);
+            }
+          else
+            {
+              syslog(LOG_INFO, "EPWM0: registered /dev/pwm0\n");
+            }
+        }
+#endif
+
+#ifdef CONFIG_AM67_EPWM1
+      lower = am67_epwminitialize(1);
+      if (lower == NULL)
+        {
+          syslog(LOG_ERR, "ERROR: Failed to get EPWM1 lower half\n");
+        }
+      else
+        {
+          ret = pwm_register("/dev/pwm1", lower);
+          if (ret < 0)
+            {
+              syslog(LOG_ERR, "ERROR: pwm_register failed: %d\n", ret);
+            }
+          else
+            {
+              syslog(LOG_INFO, "EPWM1: registered /dev/pwm1\n");
+            }
+        }
+#endif
+    }
+#endif /* CONFIG_AM67_EPWM0 || CONFIG_AM67_EPWM1 */
+
 #ifdef CONFIG_FS_PROCFS
   /* Mount the procfs file system */
 
diff --git a/boards/arm/am67/t3-gem-o1/src/am67_bringup.c 
b/boards/arm/am67/t3-gem-o1/src/am67_pwm.c
similarity index 59%
copy from boards/arm/am67/t3-gem-o1/src/am67_bringup.c
copy to boards/arm/am67/t3-gem-o1/src/am67_pwm.c
index 461956e98fb..c1cbab17db7 100644
--- a/boards/arm/am67/t3-gem-o1/src/am67_bringup.c
+++ b/boards/arm/am67/t3-gem-o1/src/am67_pwm.c
@@ -1,5 +1,5 @@
 /****************************************************************************
- * boards/arm/am67/t3-gem-o1/src/am67_bringup.c
+ * boards/arm/am67/t3-gem-o1/src/am67_pwm.c
  *
  * SPDX-License-Identifier: Apache-2.0
  *
@@ -25,61 +25,18 @@
  ****************************************************************************/
 
 #include <nuttx/config.h>
-#include <nuttx/fs/fs.h>
-#include <debug.h>
 
-#include "t3-gem-o1.h"
+#include <debug.h>
 
-#ifdef CONFIG_AM67_MCSPI0
-#include "am67_mcspi.h"
-#include "am67_gpio.h"
-#endif
+#include <nuttx/timers/pwm.h>
 
-#if defined(CONFIG_AM67_I2C0) || defined(CONFIG_AM67_WKUP_I2C0)
-#include "am67_i2c.h"
-#endif
+#include "am67_pwm.h"
 
 /****************************************************************************
- * Public Functions
+ * Pre-processor Definitions
  ****************************************************************************/
 
 /****************************************************************************
- * Name: am67_bringup
- *
- * Description:
- *   Perform architecture-specific initialization.
- *
- *   CONFIG_BOARD_LATE_INITIALIZE=y :
- *     Called from board_late_initialize().
- *
- *   CONFIG_BOARD_LATE_INITIALIZE=n && CONFIG_BOARDCTL=y :
- *     Called from the NSH library.
- *
+ * Public Functions
  ****************************************************************************/
 
-int am67_bringup(void)
-{
-  int ret = OK;
-
-#ifdef CONFIG_AM67_MCSPI0
-  am67_sensors_power_enable(true);
-  am67_spiinitialize();
-  am67_spidev_initialize();
-#endif
-
-#if defined(CONFIG_AM67_I2C0) || defined(CONFIG_AM67_WKUP_I2C0)
-  am67_i2cdev_initialize();
-#endif
-
-#ifdef CONFIG_FS_PROCFS
-  /* Mount the procfs file system */
-
-  ret = nx_mount(NULL, "/proc", "procfs", 0, NULL);
-  if (ret < 0)
-    {
-      syslog(LOG_ERR, "ERROR: Failed to mount procfs at /proc: %d\n", ret);
-    }
-#endif
-
-  return ret;
-}

Reply via email to