| /* SPDX-License-Identifier: LGPL-2.1-only */ |
| /* SPDX-FileCopyrightText: 2023-2025 Uwe Kleine-König <u.kleine-koenig@baylibre.com> */ |
| |
| #include "config.h" |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <sys/ioctl.h> |
| #include <unistd.h> |
| |
| #include <pwm.h> |
| |
| #ifdef HAVE_USABLE_LINUX_PWM_H |
| #include <linux/pwm.h> |
| #else |
| #include "uapi-pwm.h" |
| #endif |
| |
| #include "pwm-internal.h" |
| |
| struct pwm_cdev { |
| struct pwm pwm; |
| unsigned int hwpwm; |
| }; |
| |
| struct pwm_chip_cdev { |
| struct pwm_chip chip; |
| int fd; |
| struct pwm_cdev *pwms[]; |
| }; |
| |
| static void pwm_chip_cdev_close(struct pwm_chip *chip) |
| { |
| struct pwm_chip_cdev *chip_cdev = container_of(chip, struct pwm_chip_cdev, chip); |
| size_t i; |
| |
| for (i = 0; i < chip->npwm; ++i) |
| if (chip_cdev->pwms[i]) { |
| ioctl(chip_cdev->fd, PWM_IOCTL_FREE, i); |
| free(chip_cdev->pwms[i]); |
| } |
| |
| close(chip_cdev->fd); |
| free(chip_cdev); |
| } |
| |
| static struct pwm *pwm_chip_cdev_get_pwm(struct pwm_chip *chip, |
| unsigned int offset) |
| { |
| struct pwm_chip_cdev *chip_cdev = container_of(chip, struct pwm_chip_cdev, chip); |
| struct pwm_cdev *pwm_cdev; |
| struct pwm *pwm; |
| int ret; |
| |
| if (chip_cdev->pwms[offset]) |
| return &chip_cdev->pwms[offset]->pwm; |
| |
| pwm_cdev = calloc(1, sizeof(*pwm_cdev)); |
| if (!pwm_cdev) |
| return NULL; |
| pwm = &pwm_cdev->pwm; |
| pwm->chip = chip; |
| pwm_cdev->hwpwm = offset; |
| |
| ret = ioctl(chip_cdev->fd, PWM_IOCTL_REQUEST, offset); |
| if (ret) { |
| free(pwm_cdev); |
| return NULL; |
| } |
| |
| chip_cdev->pwms[offset] = pwm_cdev; |
| |
| return pwm; |
| } |
| |
| static int pwm_chip_cdev_round_waveform(struct pwm *pwm, |
| struct pwm_waveform *wf) |
| { |
| struct pwm_cdev *pwm_cdev = container_of(pwm, struct pwm_cdev, pwm); |
| struct pwm_chip_cdev *chip_cdev = container_of(pwm->chip, struct pwm_chip_cdev, chip); |
| struct pwmchip_waveform cwf = { |
| .hwpwm = pwm_cdev->hwpwm, |
| .period_length_ns = wf->period_length_ns, |
| .duty_length_ns = wf->duty_length_ns, |
| .duty_offset_ns = wf->duty_offset_ns, |
| }; |
| int ret; |
| |
| ret = ioctl(chip_cdev->fd, PWM_IOCTL_ROUNDWF, &cwf); |
| if (ret) |
| return ret; |
| |
| *wf = (struct pwm_waveform){ |
| .period_length_ns = cwf.period_length_ns, |
| .duty_length_ns = cwf.duty_length_ns, |
| .duty_offset_ns = cwf.duty_offset_ns, |
| }; |
| |
| return 0; |
| } |
| |
| static int pwm_chip_cdev_set_waveform(struct pwm *pwm, |
| const struct pwm_waveform *wf) |
| { |
| struct pwm_cdev *pwm_cdev = container_of(pwm, struct pwm_cdev, pwm); |
| struct pwm_chip_cdev *chip_cdev = container_of(pwm->chip, struct pwm_chip_cdev, chip); |
| const struct pwmchip_waveform cwf = { |
| .hwpwm = pwm_cdev->hwpwm, |
| .period_length_ns = wf->period_length_ns, |
| .duty_length_ns = wf->duty_length_ns, |
| .duty_offset_ns = wf->duty_offset_ns, |
| }; |
| |
| return ioctl(chip_cdev->fd, PWM_IOCTL_SETROUNDEDWF, &cwf); |
| } |
| |
| static int pwm_chip_cdev_set_waveform_exact(struct pwm *pwm, |
| const struct pwm_waveform *wf) |
| { |
| struct pwm_cdev *pwm_cdev = container_of(pwm, struct pwm_cdev, pwm); |
| struct pwm_chip_cdev *chip_cdev = container_of(pwm->chip, struct pwm_chip_cdev, chip); |
| const struct pwmchip_waveform cwf = { |
| .hwpwm = pwm_cdev->hwpwm, |
| .period_length_ns = wf->period_length_ns, |
| .duty_length_ns = wf->duty_length_ns, |
| .duty_offset_ns = wf->duty_offset_ns, |
| }; |
| |
| return ioctl(chip_cdev->fd, PWM_IOCTL_SETEXACTWF, &cwf); |
| } |
| |
| static int pwm_chip_cdev_get_waveform(struct pwm *pwm, struct pwm_waveform *wf) |
| { |
| struct pwm_cdev *pwm_cdev = container_of(pwm, struct pwm_cdev, pwm); |
| struct pwm_chip_cdev *chip_cdev = container_of(pwm->chip, struct pwm_chip_cdev, chip); |
| const struct pwmchip_waveform cwf = { |
| .hwpwm = pwm_cdev->hwpwm, |
| }; |
| int ret; |
| |
| ret = ioctl(chip_cdev->fd, PWM_IOCTL_GETWF, &cwf); |
| if (ret) |
| return ret; |
| |
| wf->period_length_ns = cwf.period_length_ns; |
| wf->duty_length_ns = cwf.duty_length_ns; |
| wf->duty_offset_ns = cwf.duty_offset_ns; |
| |
| return 0; |
| } |
| |
| struct pwm_chip *pwm_chip_cdev_open_by_number(unsigned int num) |
| { |
| struct pwm_chip_cdev *chip_cdev; |
| struct pwm_chip *chip; |
| int fd, fdnpwm; |
| ssize_t ret; |
| unsigned int npwm; |
| char buf[128]; |
| |
| ret = snprintf(buf, sizeof(buf), "/dev/pwmchip%d", num); |
| if (ret < 0) |
| return NULL; |
| if (ret >= sizeof(buf)) { |
| /* huh */ |
| errno = EINVAL; |
| return NULL; |
| } |
| |
| fd = open(buf, O_RDWR | O_CLOEXEC); |
| if (fd < 0) |
| return NULL; |
| |
| ret = snprintf(buf, sizeof(buf), "/sys/class/pwm/pwmchip%d/npwm", num); |
| if (ret < 0) { |
| close(fd); |
| return NULL; |
| } |
| if (ret >= sizeof(buf)) { |
| /* huh */ |
| close(fd); |
| errno = EINVAL; |
| return NULL; |
| } |
| |
| fdnpwm = open(buf, O_RDONLY | O_CLOEXEC); |
| if (fdnpwm < 0) { |
| close(fd); |
| return NULL; |
| } |
| |
| ret = read(fdnpwm, buf, sizeof(buf) - 1); |
| close(fdnpwm); |
| if (ret < 0) { |
| close(fd); |
| return NULL; |
| } |
| buf[ret] = '\0'; |
| npwm = atoi(buf); |
| |
| if (npwm > 128) { |
| close(fd); |
| errno = EINVAL; |
| return NULL; |
| } |
| |
| chip_cdev = calloc(1, sizeof(*chip_cdev) + npwm * sizeof(chip_cdev->pwms[0])); |
| if (!chip_cdev) { |
| close(fd); |
| return NULL; |
| } |
| |
| chip = &chip_cdev->chip; |
| chip->close = pwm_chip_cdev_close; |
| chip->get_pwm = pwm_chip_cdev_get_pwm; |
| chip->round_waveform = pwm_chip_cdev_round_waveform; |
| chip->set_waveform = pwm_chip_cdev_set_waveform; |
| chip->set_waveform_exact = pwm_chip_cdev_set_waveform_exact; |
| chip->get_waveform = pwm_chip_cdev_get_waveform; |
| chip->npwm = npwm; |
| |
| chip_cdev->fd = fd; |
| |
| return chip; |
| } |