Software PWM on OpenWrt

Hi all,
I am trying to find a way to get PWM on spare GPIO pins using sysfs controls. I could not find any up to date documentation or repositories that work with the current 24.10 compilation from source.

I have found the only official documentation is this PWM emulation page from 2018, but I can't figure out the vague and probably outdated instructions. (as far as I know the way the kernel handles GPIO has changed a few years ago so it is guaranteed not to work)

I have tried this soft_pwm repo that seems to be more recent by adding it and enabling in menuconfig only to get a bunch of errors

/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:52:44: error: format '%s' expects argument of type 'char *', but argument 4 has type 'unsigned int' [-Werror=format=]
   52 |         return scnprintf(buf, PAGE_SIZE, "%s\n", desc->duty_cycle);
      |                                           ~^     ~~~~~~~~~~~~~~~~
      |                                            |         |
      |                                            char *    unsigned int
      |                                           %d
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c: In function 'show_enable':
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:73:44: error: format '%s' expects argument of type 'char *', but argument 4 has type 'unsigned int' [-Werror=format=]
   73 |         return scnprintf(buf, PAGE_SIZE, "%s\n", desc->enable);
      |                                           ~^     ~~~~~~~~~~~~
      |                                            |         |
      |                                            char *    unsigned int
      |                                           %d
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c: In function 'store_enable':
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:92:11: error: implicit declaration of function '__gpio_set_value'; did you mean 'gpio_set_value'? [-Werror=implicit-function-declaration]
   92 |           __gpio_set_value(desc->gpio,0);
      |           ^~~~~~~~~~~~~~~~
      |           gpio_set_value
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c: In function 'show_period':
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:103:44: error: format '%s' expects argument of type 'char *', but argument 4 has type 'unsigned int' [-Werror=format=]
  103 |         return scnprintf(buf, PAGE_SIZE, "%s\n", desc->enable);
      |                                           ~^     ~~~~~~~~~~~~
      |                                            |         |
      |                                            char *    unsigned int
      |                                           %d
In file included from ./include/linux/kobject.h:20,
                 from ./include/linux/module.h:21,
                 from /home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:13:
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c: At top level:
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:205:22: error: initialization of 'ssize_t (*)(const struct class *, const struct class_attribute *, const char *, size_t)' {aka 'int (*)(const struct class *, const struct class_attribute *, const char *, unsigned int)'} from incompatible pointer type 'ssize_t (*)(struct class *, struct class_attribute *, const char *, size_t)' {aka 'int (*)(struct class *, struct class_attribute *, const char *, unsigned int)'} [-Werror=incompatible-pointer-types]
  205 | static CLASS_ATTR_WO(export);
      |                      ^~~~~~
./include/linux/sysfs.h:135:19: note: in definition of macro '__ATTR_WO'
  135 |         .store  = _name##_store,                                        \
      |                   ^~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:205:8: note: in expansion of macro 'CLASS_ATTR_WO'
  205 | static CLASS_ATTR_WO(export);
      |        ^~~~~~~~~~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:205:22: note: (near initialization for 'class_attr_export.store')
  205 | static CLASS_ATTR_WO(export);
      |                      ^~~~~~
./include/linux/sysfs.h:135:19: note: in definition of macro '__ATTR_WO'
  135 |         .store  = _name##_store,                                        \
      |                   ^~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:205:8: note: in expansion of macro 'CLASS_ATTR_WO'
  205 | static CLASS_ATTR_WO(export);
      |        ^~~~~~~~~~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:206:22: error: initialization of 'ssize_t (*)(const struct class *, const struct class_attribute *, const char *, size_t)' {aka 'int (*)(const struct class *, const struct class_attribute *, const char *, unsigned int)'} from incompatible pointer type 'ssize_t (*)(struct class *, struct class_attribute *, const char *, size_t)' {aka 'int (*)(struct class *, struct class_attribute *, const char *, unsigned int)'} [-Werror=incompatible-pointer-types]
  206 | static CLASS_ATTR_WO(unexport);
      |                      ^~~~~~~~
./include/linux/sysfs.h:135:19: note: in definition of macro '__ATTR_WO'
  135 |         .store  = _name##_store,                                        \
      |                   ^~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:206:8: note: in expansion of macro 'CLASS_ATTR_WO'
  206 | static CLASS_ATTR_WO(unexport);
      |        ^~~~~~~~~~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:206:22: note: (near initialization for 'class_attr_unexport.store')
  206 | static CLASS_ATTR_WO(unexport);
      |                      ^~~~~~~~
./include/linux/sysfs.h:135:19: note: in definition of macro '__ATTR_WO'
  135 |         .store  = _name##_store,                                        \
      |                   ^~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:206:8: note: in expansion of macro 'CLASS_ATTR_WO'
  206 | static CLASS_ATTR_WO(unexport);
      |        ^~~~~~~~~~~~~
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:220:4: error: 'struct class' has no member named 'owner'
  220 |   .owner =       THIS_MODULE,
      |    ^~~~~
In file included from ./include/linux/linkage.h:7,
                 from ./include/linux/kernel.h:18,
                 from /home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:12:
./include/linux/export.h:29:21: error: initialization of 'const struct attribute_group **' from incompatible pointer type 'struct module *' [-Werror=incompatible-pointer-types]
   29 | #define THIS_MODULE (&__this_module)
      |                     ^
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:220:18: note: in expansion of macro 'THIS_MODULE'
  220 |   .owner =       THIS_MODULE,
      |                  ^~~~~~~~~~~
./include/linux/export.h:29:21: note: (near initialization for 'soft_pwm_class.class_groups')
   29 | #define THIS_MODULE (&__this_module)
      |                     ^
/home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.c:220:18: note: in expansion of macro 'THIS_MODULE'
  220 |   .owner =       THIS_MODULE,
      |                  ^~~~~~~~~~~
cc1: all warnings being treated as errors
make[6]: *** [scripts/Makefile.build:243: /home/gerge/Downloads/wrt_hack/openwrt/build_dir/target-mips_24kc_musl/linux-ath79_generic/soft_pwm/soft_pwm.o] Error 1

No idea if I am even doing it right as again zero instructions provided and I don't even know if it is compatible with 24.10

I am not a kernel dev, all I know is from following the helloworld guide. I have tried searching the forum for PWM related content, but nothing came up other than a few compilation errors of the above mentioned PWM emulation.

Please help

A general search for "linux gpio pwm" seems to turn up a wealth of information, especially this driver patch which was accepted into the 6.11 kernel. The 25.12 branch, for which the first release candidate has just dropped, includes the 6.12 kernel and the driver has been packaged as kmod-pwm-gpio though a custom build from source may be required for your target to get the necessary GPIO and PWM support in the kernel (the package won't be visible in make menuconfig if these kernel options haven't been selected). A patch backporting this driver to the 6.6 kernel exists for the RaspberryPi targets for OpenWrt 24.10.

Thanks for pointing that out
I have compiled OpenWrt 25.12 after finding the pulse width modulation menu in make kernel_menuconfig enabling GPIO PWM and flashed the sysupgrade image onto a test router. Now I see pwm in sysfs but there is nothing in there...

root@OpenWrt:/sys/class# ls
bdi         firmware    ieee80211   mem         net         ppp         spi_master  watchdog
block       gpio        leds        misc        pci_bus     pwm         tty
devlink     i2c-dev     mdio_bus    mtd         phy         regulator   usbmisc

I am guessing I need to compile with kmod-pwm-gpio but I can't see that option in menuconfig even after enabling pwm in the kernel_menuconfig

What am I missing? Do I need to edit devicetree in order to get PWM on certain pins or can I just decide that in sysfs for any GPIO?

Sorry it's kmod-gpio-pwm not kmod-pwm-gpio :(, and yes I believe you will need it. You can search in make menuconfig using the "/" key and the dependency analysis will be shown so you should be able to figure out if anything else needs to be enabled for the package to become visible.

I can see it in search, but does not appear in the menu

────────────────────────────────────────────────────────────────────────────────────────────────────── Search Results ──────────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Symbol: DEFAULT_kmod-gpio-pwm [=DEFAULT_kmod-gpio-pwm]                                                                                                                                                                     │  
  │ Type  : unknown                                                                                                                                                                                                            │  
  │                                                                                                                                                                                                                            │  
  │                                                                                                                                                                                                                            │  
  │ Symbol: PACKAGE_kmod-gpio-pwm [=n]                                                                                                                                                                                         │  
  │ Type  : tristate                                                                                                                                                                                                           │  
  │ Defined at tmp/.config-package.in:17139                                                                                                                                                                                    │  
  │   Prompt: kmod-gpio-pwm........................................... PWM GPIO support                                                                                                                                        │  
  │   Depends on: GPIO_SUPPORT [=y] && PWM_SUPPORT [=n]                                                                                                                                                                        │  
  │   Location:                                                                                                                                                                                                                │  
  │     -> Kernel modules                                                                                                                                                                                                      │  
  │ (1)   -> GPIO support                                                                                                                                                                                                      │  
  │         -> kmod-gpio-pwm........................................... PWM GPIO support (PACKAGE_kmod-gpio-pwm [=n])

I can see CONFIG_GPIO_SUPPORT=y but there is not such parameter as PWM_SUPPORT in my .config. I would assume if it is not there that means it is =n by default, I don't understand what is wrong

If PWM_SUPPORT is not defined somehow then it will be treated as =n.

Please remember that the OpenWrt build system is not a generic firmware creation tool, but is focused on creating firmware for routers. It therefore relies on properties of devices being defined so that appropriate packages are available for any given device for OpenWrt's purposes. What you are trying to do by adding a PWM capability to a device that isn't nominally intended to have such a thing (as defined in OpenWrt currently) is outside the normal scope. In order to proceed you need to become more familiar with how OpenWrt's build system hangs together in order to coerce it into doing what you want. This also matters going forward as when it comes time to update to a later version you need to know how to apply your changes to the new version.

I've had a quick look and suggest you look into adding pwm to the FEATURES variable in the target.mk of whichever subtarget you're trying to build this for. You may need to force a rebuild of the config data cache if PWM_SUPPORT doesn't start showing as 'y' when searched in make menuconfig (easiest way I know is to delete the tmp subdirectory that make menuconfig creates in the build root directory before re-running make menuconfig). This is one way of changing a device's capability definition - the least difficult to maintain that occurs to me.