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.

I think I managed to compile it. In order to get the kmod-gpio-pwm in menuconfig I edited target/linux/ath79/config-6.12 to include

CONFIG_PWM=y
CONFIG_PWM_GPIO=m

In kernel_menuconfig I enabled Device_Drivers>Pulse-Width_Modulation_PWM_support>GPIO_PWM_support as module <M>

Then I deleted my tmp and it appeared in menuconfig

At the moment I am on holiday and don't have a router to test, but the compile worked and I can see
bin/targets/ath79/generic/packages/kmod-gpio-pwm-6.12.62-r1.apk

I am still looking into how the devicetree definition works for loading it for specific GPIO pins. The example here does not show how to configure two or more pwm pins

    pwm {
        #pwm-cells = <3>;
        compatible = "pwm-gpio";
        gpios = <&gpio 1 GPIO_ACTIVE_HIGH>;
    };

I was able to finally test the pwm package I compiled.

I flashed the sysupgrade image that I built from source, this made /sys/class/pwm appear, but empty

lsmod does not list gpio_pwm

I then though there can be two issues:

  1. I built kmod-gpio-pwm as a module and it is not loaded or included in the image, but I have the package file as stated in previous comment
  2. The pwm sysfs location is empty because I did not define any pwm pins in the devicetree

So to save having to rebuild everything I modified the image I compiled by adding to the devicetree as I usually do for my 16M mod

# #PWM
fdtput --auto-path -t s devtree.dtb /pwm-gpio compatible pwm-gpio
fdtput --auto-path -t x devtree.dtb /pwm-gpio gpios 0f 15 0
fdtput --auto-path -t x devtree.dtb /pwm-gpio "#pwm-cells" 3
fdtput -r devtree.dtb /gpio-export/gpio_usb1_power
fdtput -r devtree.dtb /gpio-export/gpio_usb2_power

0x0f is the &gpio phandle, 0x15 is GPIO21, 0x0 is GPIO_ACTIVE_HIGH
(because this is a compiled devicetree mod)

I will make all documentation/scripts available after everything is figured out and this forum question is solved

after this I do another sysupgrade so that the devicetree has the pwm pin defined, the sysfs pwm folder is still empty, but after scp-ing over the kmod-gpio-pwm package and installing

root@OpenWrt:/tmp# apk add --allow-untrusted kmod-gpio-pwm-6.12.62-r1.apk 
(1/2) Installing dtc (1.7.2-r1)
  Executing dtc-1.7.2-r1.post-install
(2/2) Installing kmod-gpio-pwm (6.12.62-r1)
  Executing kmod-gpio-pwm-6.12.62-r1.post-install
OK: 12.8 MiB in 119 packages

I get pwm_gpio in lsmod and I can see /sys/class/pwm/pwmchip0

I can then export and control my pwm pin from sysfs (same way as gpiochip works)

unfortunately it looks like gpio pwm only supports a single pin

  gpios:
    description:
      GPIO to be modulated
    maxItems: 1

so I can't use pwm for two motors to make a differential drive robot

I was able to add multiple PWM pins by simply defining multiple instances of the pwm entry in the devicetree

# #PWM
fdtput --auto-path -t s devtree.dtb /pwm-gpio compatible pwm-gpio
fdtput --auto-path -t x devtree.dtb /pwm-gpio gpios 0f 15 0
fdtput --auto-path -t x devtree.dtb /pwm-gpio "#pwm-cells" 3
fdtput --auto-path -t s devtree.dtb /pwm-gpio1 compatible pwm-gpio
fdtput --auto-path -t x devtree.dtb /pwm-gpio1 gpios 0f 16 0
fdtput --auto-path -t x devtree.dtb /pwm-gpio1 "#pwm-cells" 3
fdtput -r devtree.dtb /gpio-export/gpio_usb1_power
fdtput -r devtree.dtb /gpio-export/gpio_usb2_power

so even though I can only have one PWM pin, I can have multiple pwmchips

root@OpenWrt:/sys/class/pwm# ls
pwmchip0  pwmchip1

I have tested with two motors plugged into the USB ports and can drive them independently.

Now I will go an build a fun little differential drive RC car out of this router. I will post my detailed documentation for the project here when ready, but will mark the issue solved now.

Thanks @pythonic, it was a pleasant learning experience.

1 Like

Here is the repo for anybody interested