From 37174127a3325c38f0162b9dffe7729fdcfa1ea4 Mon Sep 17 00:00:00 2001 From: "Vanya A. Sergeev" Date: Thu, 2 Jan 2020 23:19:53 -0600 Subject: [PATCH] pwm: add pwm module --- Makefile | 2 +- README.md | 59 ++++++- docs/pwm.md | 321 +++++++++++++++++++++++++++++++++ src/pwm.c | 451 +++++++++++++++++++++++++++++++++++++++++++++++ src/pwm.h | 73 ++++++++ tests/test_pwm.c | 292 ++++++++++++++++++++++++++++++ 6 files changed, 1194 insertions(+), 4 deletions(-) create mode 100644 docs/pwm.md create mode 100644 src/pwm.c create mode 100644 src/pwm.h create mode 100644 tests/test_pwm.c diff --git a/Makefile b/Makefile index bc9fe7a..86af9d9 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ LIB = periphery.a -SRCS = src/gpio.c src/led.c src/spi.c src/i2c.c src/mmio.c src/serial.c src/version.c +SRCS = src/gpio.c src/led.c src/pwm.c src/spi.c src/i2c.c src/mmio.c src/serial.c src/version.c SRCDIR = src OBJDIR = obj diff --git a/README.md b/README.md index 07a5684..0c20d98 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # c-periphery [![Build Status](https://travis-ci.org/vsergeev/c-periphery.svg?branch=master)](https://travis-ci.org/vsergeev/c-periphery) [![GitHub release](https://img.shields.io/github/release/vsergeev/c-periphery.svg?maxAge=7200)](https://github.com/vsergeev/c-periphery) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vsergeev/c-periphery/blob/master/LICENSE) -## C Library for Linux Peripheral I/O (GPIO, LED, SPI, I2C, MMIO, Serial) +## C Library for Linux Peripheral I/O (GPIO, LED, PWM, SPI, I2C, MMIO, Serial) -c-periphery is a small C library for GPIO, LED, SPI, I2C, MMIO, and Serial peripheral I/O interface access in userspace Linux. c-periphery simplifies and consolidate the native Linux APIs to these interfaces. c-periphery is useful in embedded Linux environments (including Raspberry Pi, BeagleBone, etc. platforms) for interfacing with external peripherals. c-periphery is re-entrant, has no dependencies outside the standard C library and Linux, compiles into a static library for easy integration with other projects, and is MIT licensed. +c-periphery is a small C library for GPIO, LED, PWM, SPI, I2C, MMIO, and Serial peripheral I/O interface access in userspace Linux. c-periphery simplifies and consolidate the native Linux APIs to these interfaces. c-periphery is useful in embedded Linux environments (including Raspberry Pi, BeagleBone, etc. platforms) for interfacing with external peripherals. c-periphery is re-entrant, has no dependencies outside the standard C library and Linux, compiles into a static library for easy integration with other projects, and is MIT licensed. Using Python or Lua? Check out the [python-periphery](https://github.com/vsergeev/python-periphery) and [lua-periphery](https://github.com/vsergeev/lua-periphery) projects. @@ -109,6 +109,59 @@ int main(void) { [Go to LED documentation.](docs/led.md) +### PWM + +``` c +#include +#include + +#include "pwm.h" + +int main(void) { + pwm_t *pwm; + + pwm = pwm_new(); + + /* Open PWM chip 0, channel 10 */ + if (pwm_open(pwm, 0, 10) < 0) { + fprintf(stderr, "pwm_open(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Set frequency to 1 kHz */ + if (pwm_set_frequency(pwm, 1e3) < 0) { + fprintf(stderr, "pwm_set_frequency(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Set duty cycle to 75% */ + if (pwm_set_duty_cycle(pwm, 0.75) < 0) { + fprintf(stderr, "pwm_set_duty_cycle(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Enable PWM */ + if (pwm_enable(pwm) < 0) { + fprintf(stderr, "pwm_enable(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Change duty cycle to 50% */ + if (pwm_set_duty_cycle(pwm, 0.50) < 0) { + fprintf(stderr, "pwm_set_duty_cycle(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + pwm_close(pwm); + + pwm_free(pwm); + + return 0; +} +``` + +[Go to PWM documentation.](docs/pwm.md) + ### SPI ``` c @@ -376,7 +429,7 @@ $ ## Building c-periphery into another project -Include the header files in `src/` (e.g. `gpio.h`, `led.h`, `spi.h`, `i2c.h`, `mmio.h`, `serial.h`) and link in the `periphery.a` static library. +Include the header files in `src/` (e.g. `gpio.h`, `led.h`, `pwm.h`, `spi.h`, `i2c.h`, `mmio.h`, `serial.h`) and link in the `periphery.a` static library. ``` console $ gcc -I/path/to/periphery/src myprog.c /path/to/periphery/periphery.a -o myprog diff --git a/docs/pwm.md b/docs/pwm.md new file mode 100644 index 0000000..70ac9fa --- /dev/null +++ b/docs/pwm.md @@ -0,0 +1,321 @@ +### NAME + +PWM wrapper functions for Linux userspace sysfs PWMs. + +### SYNOPSIS + +``` c +#include + +/* Primary Functions */ +pwm_t *pwm_new(void); +int pwm_open(pwm_t *pwm, unsigned int chip, unsigned int channel); +int pwm_enable(pwm_t *pwm); +int pwm_disable(pwm_t *pwm); +int pwm_close(pwm_t *pwm); +void pwm_free(pwm_t *pwm); + +/* Getters */ +int pwm_get_enabled(pwm_t *pwm, bool *enabled); +int pwm_get_period_ns(pwm_t *pwm, uint64_t *period_ns); +int pwm_get_duty_cycle_ns(pwm_t *pwm, uint64_t *duty_cycle_ns); +int pwm_get_period(pwm_t *pwm, double *period); +int pwm_get_duty_cycle(pwm_t *pwm, double *duty_cycle); +int pwm_get_frequency(pwm_t *pwm, double *frequency); +int pwm_get_polarity(pwm_t *pwm, pwm_polarity_t *polarity); + +/* Setters */ +int pwm_set_enabled(pwm_t *pwm, bool enabled); +int pwm_set_period_ns(pwm_t *pwm, uint64_t period_ns); +int pwm_set_duty_cycle_ns(pwm_t *pwm, uint64_t duty_cycle_ns); +int pwm_set_period(pwm_t *pwm, double period); +int pwm_set_duty_cycle(pwm_t *pwm, double duty_cycle); +int pwm_set_frequency(pwm_t *pwm, double frequency); +int pwm_set_polarity(pwm_t *pwm, pwm_polarity_t polarity); + +/* Miscellaneous */ +unsigned int pwm_chip(pwm_t *pwm); +unsigned int pwm_channel(pwm_t *pwm); +int pwm_tostring(pwm_t *pwm, char *str, size_t len); + +/* Error Handling */ +int pwm_errno(pwm_t *pwm); +const char *pwm_errmsg(pwm_t *pwm); +``` + +### ENUMERATIONS + +* `pwm_polarity_t` + * `PWM_POLARITY_NORMAL`: Normal polarity + * `PWM_POLARITY_INVERSED`: Inversed polarity + +### DESCRIPTION + +``` c +pwm_t *pwm_new(void); +``` +Allocate a PWM handle. + +Returns a valid handle on success, or NULL on failure. + +------ + +``` c +int pwm_open(pwm_t *pwm, unsigned int chip, unsigned int channel); +``` +Open the sysfs PWM with the specified chip and channel. + +`pwm` should be a valid pointer to an allocated PWM handle structure. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_enable(pwm_t *pwm); +``` +Enable the PWM output. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_disable(pwm_t *pwm); +``` +Disable the PWM output. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_close(pwm_t *pwm); +``` +Close the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +void pwm_free(pwm_t *pwm); +``` +Free a PWM handle. + +------ + +``` c +int pwm_get_enabled(pwm_t *pwm, bool *enabled); +``` +Get the output state of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_get_period_ns(pwm_t *pwm, uint64_t *period_ns); +int pwm_get_duty_cycle_ns(pwm_t *pwm, uint64_t *duty_cycle_ns); +``` +Get the period in nanoseconds or duty cycle in nanoseconds, respectively, of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_get_period(pwm_t *pwm, double *period); +int pwm_get_duty_cycle(pwm_t *pwm, double *duty_cycle); +int pwm_get_frequency(pwm_t *pwm, double *frequency); +``` +Get the period in seconds, duty cycle as a ratio between 0.0 to 1.0, or frequency in Hz, respectively, of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_get_polarity(pwm_t *pwm, pwm_polarity_t *polarity); +``` +Get the output polarity of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_set_enabled(pwm_t *pwm, bool enabled); +``` +Set the output state of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_set_period_ns(pwm_t *pwm, uint64_t period_ns); +int pwm_set_duty_cycle_ns(pwm_t *pwm, uint64_t duty_cycle_ns); +``` +Set the period in nanoseconds or duty cycle in nanoseconds, respectively, of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_set_period(pwm_t *pwm, double period); +int pwm_set_duty_cycle(pwm_t *pwm, double duty_cycle); +int pwm_set_frequency(pwm_t *pwm, double frequency); +``` +Set the period in seconds, duty cycle as a ratio between 0.0 to 1.0, or frequency in Hz, respectively, of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +int pwm_set_polarity(pwm_t *pwm, pwm_polarity_t *polarity); +``` +Set the output polarity of the PWM. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +Returns 0 on success, or a negative [PWM error code](#return-value) on failure. + +------ + +``` c +unsigned int pwm_chip(pwm_t *pwm); +``` +Return the chip number of the PWM handle. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +This function is a simple accessor to the PWM handle structure and always succeeds. + +------ + +``` c +unsigned int pwm_channel(pwm_t *pwm); +``` +Return the channel number of the PWM handle. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +This function is a simple accessor to the PWM handle structure and always succeeds. + +------ + +``` c +int pwm_tostring(pwm_t *pwm, char *str, size_t len); +``` +Return a string representation of the PWM handle. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +This function behaves and returns like `snprintf()`. + +------ + +``` c +int pwm_errno(pwm_t *pwm); +``` +Return the libc errno of the last failure that occurred. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +------ + +``` c +const char *pwm_errmsg(pwm_t *pwm); +``` +Return a human readable error message of the last failure that occurred. + +`pwm` should be a valid pointer to a PWM handle opened with `pwm_open()`. + +### RETURN VALUE + +The periphery PWM functions return 0 on success or one of the negative error codes below on failure. + +The libc errno of the failure in an underlying libc library call can be obtained with the `pwm_errno()` helper function. A human readable error message can be obtained with the `pwm_errmsg()` helper function. + +| Error Code | Description | +|-----------------------|-----------------------------------| +| `PWM_ERROR_ARG` | Invalid arguments | +| `PWM_ERROR_OPEN` | Opening PWM | +| `PWM_ERROR_QUERY` | Querying PWM attributes | +| `PWM_ERROR_CONFIGURE` | Configuring PWM attributes | +| `PWM_ERROR_CLOSE` | Closing PWM | + +### EXAMPLE + +``` c +#include +#include + +#include "pwm.h" + +int main(void) { + pwm_t *pwm; + + pwm = pwm_new(); + + /* Open PWM chip 0, channel 10 */ + if (pwm_open(pwm, 0, 10) < 0) { + fprintf(stderr, "pwm_open(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Set frequency to 1 kHz */ + if (pwm_set_frequency(pwm, 1e3) < 0) { + fprintf(stderr, "pwm_set_frequency(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Set duty cycle to 75% */ + if (pwm_set_duty_cycle(pwm, 0.75) < 0) { + fprintf(stderr, "pwm_set_duty_cycle(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Enable PWM */ + if (pwm_enable(pwm) < 0) { + fprintf(stderr, "pwm_enable(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + /* Change duty cycle to 50% */ + if (pwm_set_duty_cycle(pwm, 0.50) < 0) { + fprintf(stderr, "pwm_set_duty_cycle(): %s\n", pwm_errmsg(pwm)); + exit(1); + } + + pwm_close(pwm); + + pwm_free(pwm); + + return 0; +} +``` + diff --git a/src/pwm.c b/src/pwm.c new file mode 100644 index 0000000..53596b6 --- /dev/null +++ b/src/pwm.c @@ -0,0 +1,451 @@ +/* + * c-periphery + * https://github.com/vsergeev/c-periphery + * License: MIT + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "pwm.h" + +#define P_PATH_MAX 256 +/* Delay between checks for successful PWM export (100ms) */ +#define PWM_EXPORT_STAT_DELAY 100000 +/* Number of retries to check for successful PWM exports */ +#define PWM_EXPORT_STAT_RETRIES 10 + +struct pwm_handle { + unsigned int chip; + unsigned int channel; + uint64_t period_ns; + + struct { + int c_errno; + char errmsg[96]; + } error; +}; + +static int _pwm_error(pwm_t *pwm, int code, int c_errno, const char *fmt, ...) { + va_list ap; + + pwm->error.c_errno = c_errno; + + va_start(ap, fmt); + vsnprintf(pwm->error.errmsg, sizeof(pwm->error.errmsg), fmt, ap); + va_end(ap); + + /* Tack on strerror() and errno */ + if (c_errno) { + char buf[64]; + strerror_r(c_errno, buf, sizeof(buf)); + snprintf(pwm->error.errmsg+strlen(pwm->error.errmsg), sizeof(pwm->error.errmsg)-strlen(pwm->error.errmsg), ": %s [errno %d]", buf, c_errno); + } + + return code; +} + +pwm_t *pwm_new(void) { + pwm_t *pwm = calloc(1, sizeof(pwm_t)); + if (pwm == NULL) + return NULL; + + pwm->chip = -1; + pwm->channel = -1; + + return pwm; +} + +int pwm_open(pwm_t *pwm, unsigned int chip, unsigned int channel) { + char channel_path[P_PATH_MAX]; + struct stat stat_buf; + int ret; + + snprintf(channel_path, sizeof(channel_path), "/sys/class/pwm/pwmchip%u/pwm%u", chip, channel); + + /* Check if PWM channel exists */ + if (stat(channel_path, &stat_buf) < 0) { + char path[P_PATH_MAX]; + char buf[16]; + int fd, len; + + /* Export the PWM channel */ + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%u/export", chip); + + len = snprintf(buf, sizeof(buf), "%u\n", channel); + + if ((fd = open(path, O_WRONLY)) < 0) + return _pwm_error(pwm, PWM_ERROR_OPEN, errno, "Opening PWM: opening 'export'"); + + if (write(fd, buf, len) < 0) { + int errsv = errno; + close(fd); + return _pwm_error(pwm, PWM_ERROR_OPEN, errsv, "Opening PWM: writing 'export'"); + } + + if (close(fd) < 0) + return _pwm_error(pwm, PWM_ERROR_OPEN, errno, "Opening PWM: closing 'export'"); + + /* Wait until PWM channel appears */ + unsigned int retry_count; + for (retry_count = 0; retry_count < PWM_EXPORT_STAT_RETRIES; retry_count++) { + if ((ret = stat(path, &stat_buf)) < 0 && errno != ENOENT) + return _pwm_error(pwm, PWM_ERROR_OPEN, errno, "Opening PWM: stat 'pwm%u/' after export", channel); + else if (ret == 0) + break; + + usleep(PWM_EXPORT_STAT_DELAY); + } + + if (retry_count == PWM_EXPORT_STAT_RETRIES) + return _pwm_error(pwm, PWM_ERROR_OPEN, 0, "Opening PWM: waiting for 'pwm%u/' timed out", channel); + + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%u/pwm%u/period", chip, channel); + + /* Loop until period is writable. This could take some time after + * export as application of udev rules after export is asynchronous. */ + for (retry_count = 0; retry_count < PWM_EXPORT_STAT_RETRIES; retry_count++) { + if ((fd = open(path, O_WRONLY)) < 0) { + if (errno != EACCES || (errno == EACCES && retry_count == PWM_EXPORT_STAT_RETRIES - 1)) + return _pwm_error(pwm, PWM_ERROR_OPEN, errno, "Opening PWM: opening 'pwm%u/period' after export", channel); + } else { + close(fd); + break; + } + + usleep(PWM_EXPORT_STAT_DELAY); + } + } + + memset(pwm, 0, sizeof(pwm_t)); + pwm->chip = chip; + pwm->channel = channel; + + ret = pwm_get_period_ns(pwm, &pwm->period_ns); + if (ret < 0) + return ret; + + return 0; +} + +int pwm_enable(pwm_t *pwm) { + return pwm_set_enabled(pwm, true); +} + +int pwm_disable(pwm_t *pwm) { + return pwm_set_enabled(pwm, false); +} + +int pwm_close(pwm_t *pwm) { + char path[P_PATH_MAX]; + char buf[16]; + int len; + int fd; + + if (pwm->channel == ((unsigned int) -1)) + return 0; + + /* Unexport the PWM */ + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%u/unexport", pwm->chip); + + len = snprintf(buf, sizeof(buf), "%u\n", pwm->channel); + + if ((fd = open(path, O_WRONLY)) < 0) + return _pwm_error(pwm, PWM_ERROR_CLOSE, errno, "Closing PWM: opening 'unexport'"); + + if (write(fd, buf, len) < 0) { + int errsv = errno; + close(fd); + return _pwm_error(pwm, PWM_ERROR_CLOSE, errsv, "Closing PWM: writing 'unexport'"); + } + + if (close(fd) < 0) + return _pwm_error(pwm, PWM_ERROR_CLOSE, errno, "Closing PWM: closing 'unexport'"); + + pwm->chip = -1; + pwm->channel = -1; + + return 0; +} + +void pwm_free(pwm_t *pwm) { + free(pwm); +} + +static int pwm_read_attribute(pwm_t *pwm, const char *name, char *buf, size_t len) { + char path[P_PATH_MAX]; + int fd, ret; + + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%u/pwm%u/%s", pwm->chip, pwm->channel, name); + + if ((fd = open(path, O_RDONLY)) < 0) + return _pwm_error(pwm, PWM_ERROR_QUERY, errno, "Opening PWM '%s'", name); + + if ((ret = read(fd, buf, len)) < 0) { + int errsv = errno; + close(fd); + return _pwm_error(pwm, PWM_ERROR_QUERY, errsv, "Reading PWM '%s'", name); + } + + if (close(fd) < 0) + return _pwm_error(pwm, PWM_ERROR_QUERY, errno, "Closing PWM '%s'", name); + + buf[ret] = '\0'; + + return 0; +} + +static int pwm_write_attribute(pwm_t *pwm, const char *name, const char *buf, size_t len) { + char path[P_PATH_MAX]; + int fd; + + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%u/pwm%u/%s", pwm->chip, pwm->channel, name); + + if ((fd = open(path, O_WRONLY)) < 0) + return _pwm_error(pwm, PWM_ERROR_CONFIGURE, errno, "Opening PWM '%s'", name); + + if (write(fd, buf, len) < 0) { + int errsv = errno; + close(fd); + return _pwm_error(pwm, PWM_ERROR_CONFIGURE, errsv, "Writing PWM '%s'", name); + } + + if (close(fd) < 0) + return _pwm_error(pwm, PWM_ERROR_CONFIGURE, errno, "Closing PWM '%s'", name); + + return 0; +} + +int pwm_get_enabled(pwm_t *pwm, bool *enabled) { + char buf[2]; + int ret; + + if ((ret = pwm_read_attribute(pwm, "enable", buf, sizeof(buf))) < 0) + return ret; + + if (buf[0] == '0') + *enabled = false; + else if (buf[0] == '1') + *enabled = true; + else + return _pwm_error(pwm, PWM_ERROR_QUERY, errno, "Unknown PWM 'enabled' value"); + + return 0; +} + +int pwm_get_period_ns(pwm_t *pwm, uint64_t *period_ns) { + char buf[32]; + int ret; + uint64_t value; + + if ((ret = pwm_read_attribute(pwm, "period", buf, sizeof(buf))) < 0) + return ret; + + errno = 0; + value = strtoul(buf, NULL, 10); + if (errno != 0) + return _pwm_error(pwm, PWM_ERROR_QUERY, errno, "Unknown PWM 'period' value"); + + /* Cache the period for fast duty cycle updates */ + pwm->period_ns = value; + + *period_ns = value; + + return 0; +} + +int pwm_get_duty_cycle_ns(pwm_t *pwm, uint64_t *duty_cycle_ns) { + char buf[32]; + int ret; + uint64_t value; + + if ((ret = pwm_read_attribute(pwm, "duty_cycle", buf, sizeof(buf))) < 0) + return ret; + + errno = 0; + value = strtoul(buf, NULL, 10); + if (errno != 0) + return _pwm_error(pwm, PWM_ERROR_QUERY, errno, "Unknown PWM 'duty_cycle' value"); + + *duty_cycle_ns = value; + + return 0; +} + +int pwm_get_period(pwm_t *pwm, double *period) { + int ret; + uint64_t period_ns; + + if ((ret = pwm_get_period_ns(pwm, &period_ns)) < 0) + return ret; + + *period = ((double) period_ns) / 1e9; + + return 0; +} + +int pwm_get_duty_cycle(pwm_t *pwm, double *duty_cycle) { + int ret; + uint64_t duty_cycle_ns; + + if ((ret = pwm_get_duty_cycle_ns(pwm, &duty_cycle_ns)) < 0) + return ret; + + *duty_cycle = ((double) duty_cycle_ns) / ((double) pwm->period_ns); + + return 0; +} + +int pwm_get_frequency(pwm_t *pwm, double *frequency) { + int ret; + uint64_t period_ns; + + if ((ret = pwm_get_period_ns(pwm, &period_ns)) < 0) + return ret; + + *frequency = 1e9 / ((double) period_ns); + + return 0; +} + +int pwm_get_polarity(pwm_t *pwm, pwm_polarity_t *polarity) { + int ret; + char buf[16]; + + if ((ret = pwm_read_attribute(pwm, "polarity", buf, sizeof(buf))) < 0) + return ret; + + if (strcmp(buf, "normal\n") == 0) + *polarity = PWM_POLARITY_NORMAL; + else if (strcmp(buf, "inversed\n") == 0) + *polarity = PWM_POLARITY_INVERSED; + else + return _pwm_error(pwm, PWM_ERROR_QUERY, errno, "Unknown PWM 'polarity' value"); + + return 0; +} + +int pwm_set_enabled(pwm_t *pwm, bool enabled) { + return pwm_write_attribute(pwm, "enable", enabled ? "1\n" : "0\n", 2); +} + +int pwm_set_period_ns(pwm_t *pwm, uint64_t period_ns) { + char buf[32]; + int len; + int ret; + + len = snprintf(buf, sizeof(buf), "%" PRId64 "\n", period_ns); + + if ((ret = pwm_write_attribute(pwm, "period", buf, len)) < 0) + return ret; + + /* Cache the period for fast duty cycle updates */ + pwm->period_ns = period_ns; + + return 0; +} + +int pwm_set_duty_cycle_ns(pwm_t *pwm, uint64_t duty_cycle_ns) { + char buf[32]; + int len; + + len = snprintf(buf, sizeof(buf), "%" PRId64 "\n", duty_cycle_ns); + + return pwm_write_attribute(pwm, "duty_cycle", buf, len); +} + +int pwm_set_period(pwm_t *pwm, double period) { + uint64_t period_ns = (uint64_t)(period * 1e9); + + return pwm_set_period_ns(pwm, period_ns); +} + +int pwm_set_duty_cycle(pwm_t *pwm, double duty_cycle) { + uint64_t duty_cycle_ns; + + if (duty_cycle < 0 || duty_cycle > 1) + return _pwm_error(pwm, PWM_ERROR_ARG, 0, "PWM duty cycle out of bounds (should be between 0.0 and 1.0)"); + + duty_cycle_ns = (uint64_t)(((double) pwm->period_ns) * duty_cycle); + + return pwm_set_duty_cycle_ns(pwm, duty_cycle_ns); +} + +int pwm_set_frequency(pwm_t *pwm, double frequency) { + uint64_t period_ns = (uint64_t)(1e9 / frequency); + + return pwm_set_period_ns(pwm, period_ns); +} + +int pwm_set_polarity(pwm_t *pwm, pwm_polarity_t polarity) { + const char *buf; + + if (polarity == PWM_POLARITY_NORMAL) + buf = "normal\n"; + else if (polarity == PWM_POLARITY_INVERSED) + buf = "inversed\n"; + else + return _pwm_error(pwm, PWM_ERROR_ARG, 0, "Invalid PWM polarity (can be normal, inversed)"); + + return pwm_write_attribute(pwm, "polarity", buf, strlen(buf)); +} + +unsigned int pwm_chip(pwm_t *pwm) { + return pwm->chip; +} + +unsigned int pwm_channel(pwm_t *pwm) { + return pwm->channel; +} + +int pwm_tostring(pwm_t *pwm, char *str, size_t len) { + double period; + char period_str[16]; + double duty_cycle; + char duty_cycle_str[16]; + pwm_polarity_t polarity; + const char *polarity_str; + bool enabled; + const char *enabled_str; + + if (pwm_get_period(pwm, &period) < 0) + strncpy(period_str, "", sizeof(period_str)); + else + snprintf(period_str, sizeof(period_str), "%f", period); + + if (pwm_get_duty_cycle(pwm, &duty_cycle) < 0) + strncpy(duty_cycle_str, "", sizeof(duty_cycle_str)); + else + snprintf(duty_cycle_str, sizeof(duty_cycle_str), "%f", duty_cycle); + + if (pwm_get_polarity(pwm, &polarity) < 0) + polarity_str = ""; + else + polarity_str = (polarity == PWM_POLARITY_NORMAL) ? "normal" : + (polarity == PWM_POLARITY_INVERSED) ? "inversed" : "unknown"; + + if (pwm_get_enabled(pwm, &enabled) < 0) + enabled_str = ""; + else + enabled_str = enabled ? "true" : "false"; + + return snprintf(str, len, "PWM %u, chip %u (period=%s sec, duty_cycle=%s%%, polarity=%s, enabled=%s)", pwm->channel, pwm->chip, period_str, duty_cycle_str, polarity_str, enabled_str); +} + +int pwm_errno(pwm_t *pwm) { + return pwm->error.c_errno; +} + +const char *pwm_errmsg(pwm_t *pwm) { + return pwm->error.errmsg; +} diff --git a/src/pwm.h b/src/pwm.h new file mode 100644 index 0000000..ebc443e --- /dev/null +++ b/src/pwm.h @@ -0,0 +1,73 @@ +/* + * c-periphery + * https://github.com/vsergeev/c-periphery + * License: MIT + */ + +#ifndef _PERIPHERY_PWM_H +#define _PERIPHERY_PWM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +enum pwm_error_code { + PWM_ERROR_ARG = -1, /* Invalid arguments */ + PWM_ERROR_OPEN = -2, /* Opening PWM */ + PWM_ERROR_QUERY = -3, /* Querying PWM attributes */ + PWM_ERROR_CONFIGURE = -4, /* Configuring PWM attributes */ + PWM_ERROR_CLOSE = -5, /* Closing PWM */ +}; + +typedef enum pwm_polarity { + PWM_POLARITY_NORMAL, /* Normal polarity */ + PWM_POLARITY_INVERSED, /* Inversed polarity */ +} pwm_polarity_t; + +typedef struct pwm_handle pwm_t; + +/* Primary Functions */ +pwm_t *pwm_new(void); +int pwm_open(pwm_t *pwm, unsigned int chip, unsigned int channel); +int pwm_enable(pwm_t *pwm); +int pwm_disable(pwm_t *pwm); +int pwm_close(pwm_t *pwm); +void pwm_free(pwm_t *pwm); + +/* Getters */ +int pwm_get_enabled(pwm_t *pwm, bool *enabled); +int pwm_get_period_ns(pwm_t *pwm, uint64_t *period_ns); +int pwm_get_duty_cycle_ns(pwm_t *pwm, uint64_t *duty_cycle_ns); +int pwm_get_period(pwm_t *pwm, double *period); +int pwm_get_duty_cycle(pwm_t *pwm, double *duty_cycle); +int pwm_get_frequency(pwm_t *pwm, double *frequency); +int pwm_get_polarity(pwm_t *pwm, pwm_polarity_t *polarity); + +/* Setters */ +int pwm_set_enabled(pwm_t *pwm, bool enabled); +int pwm_set_period_ns(pwm_t *pwm, uint64_t period_ns); +int pwm_set_duty_cycle_ns(pwm_t *pwm, uint64_t duty_cycle_ns); +int pwm_set_period(pwm_t *pwm, double period); +int pwm_set_duty_cycle(pwm_t *pwm, double duty_cycle); +int pwm_set_frequency(pwm_t *pwm, double frequency); +int pwm_set_polarity(pwm_t *pwm, pwm_polarity_t polarity); + +/* Miscellaneous */ +unsigned int pwm_chip(pwm_t *pwm); +unsigned int pwm_channel(pwm_t *pwm); +int pwm_tostring(pwm_t *pwm, char *str, size_t len); + +/* Error Handling */ +int pwm_errno(pwm_t *pwm); +const char *pwm_errmsg(pwm_t *pwm); + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/tests/test_pwm.c b/tests/test_pwm.c new file mode 100644 index 0000000..0b7c41a --- /dev/null +++ b/tests/test_pwm.c @@ -0,0 +1,292 @@ +/* + * c-periphery + * https://github.com/vsergeev/c-periphery + * License: MIT + */ + +#include "test.h" + +#include +#include +#include +#include + +#include "../src/pwm.h" + +unsigned int chip; +unsigned int channel; + +void test_arguments(void) { + ptest(); + + /* No real argument validation needed in the PWM wrapper */ +} + +static double fabs(double x) { + return (x < 0) ? -x : x; +} + +void test_open_config_close(void) { + pwm_t *pwm; + uint64_t period_ns; + uint64_t duty_cycle_ns; + double period; + double frequency; + double duty_cycle; + pwm_polarity_t polarity; + bool enabled; + + ptest(); + + /* Allocate PWM */ + pwm = pwm_new(); + passert(pwm != NULL); + + /* Open non-existent PWM chip */ + passert(pwm_open(pwm, 9999, channel) == PWM_ERROR_OPEN); + + /* Open non-existent PWM channel */ + passert(pwm_open(pwm, chip, 9999) == PWM_ERROR_OPEN); + + /* Open legitimate PWM chip/channel */ + passert(pwm_open(pwm, chip, channel) == 0); + + /* Check properties */ + passert(pwm_chip(pwm) == chip); + passert(pwm_channel(pwm) == channel); + + /* Initialize period and duty cycle */ + passert(pwm_set_period(pwm, 5e-3) == 0); + passert(pwm_set_duty_cycle(pwm, 0) == 0); + + /* Set period, check period, check period_ns, check frequency */ + passert(pwm_set_period(pwm, 1e-3) == 0); + passert(pwm_get_period(pwm, &period) == 0); + passert(fabs(period - 1e-3) < 1e-4); + passert(pwm_get_period_ns(pwm, &period_ns) == 0); + passert(fabs(period_ns - 1000000) < 1e5); + passert(pwm_get_frequency(pwm, &frequency) == 0); + passert(fabs(frequency - 1000) < 100); + + passert(pwm_set_period(pwm, 5e-4) == 0); + passert(pwm_get_period(pwm, &period) == 0); + passert(fabs(period - 5e-4) < 1e-5); + passert(pwm_get_period_ns(pwm, &period_ns) == 0); + passert(fabs(period_ns - 500000) < 1e4); + passert(pwm_get_frequency(pwm, &frequency) == 0); + passert(fabs(frequency - 2000) < 100); + + /* Set frequency, check frequency, check period, check period_ns */ + passert(pwm_set_frequency(pwm, 1000) == 0); + passert(pwm_get_frequency(pwm, &frequency) == 0); + passert(fabs(frequency - 1000) < 100); + passert(pwm_get_period(pwm, &period) == 0); + passert(fabs(period - 1e-3) < 1e-4); + passert(pwm_get_period_ns(pwm, &period_ns) == 0); + passert(fabs(period_ns - 1000000) < 1e5); + + passert(pwm_set_frequency(pwm, 2000) == 0); + passert(pwm_get_frequency(pwm, &frequency) == 0); + passert(fabs(frequency - 2000) < 100); + passert(pwm_get_period(pwm, &period) == 0); + passert(fabs(period - 5e-4) < 1e-5); + passert(pwm_get_period_ns(pwm, &period_ns) == 0); + passert(fabs(period_ns - 500000) < 1e4); + + /* Set period_ns, check period_ns, check period, check frequency */ + passert(pwm_set_period_ns(pwm, 1000000) == 0); + passert(pwm_get_period_ns(pwm, &period_ns) == 0); + passert(fabs(period_ns - 1000000) < 1e5); + passert(pwm_get_period(pwm, &period) == 0); + passert(fabs(period - 1e-3) < 1e-4); + passert(pwm_get_frequency(pwm, &frequency) == 0); + passert(fabs(frequency - 1000) < 100); + + passert(pwm_set_period_ns(pwm, 500000) == 0); + passert(pwm_get_period_ns(pwm, &period_ns) == 0); + passert(fabs(period_ns - 500000) < 1e4); + passert(pwm_get_period(pwm, &period) == 0); + passert(fabs(period - 5e-4) < 1e-5); + passert(pwm_get_frequency(pwm, &frequency) == 0); + passert(fabs(frequency - 2000) < 100); + + passert(pwm_set_period_ns(pwm, 1000000) == 0); + + /* Set duty cycle, check duty cycle, check duty_cycle_ns */ + passert(pwm_set_duty_cycle(pwm, 0.25) == 0); + passert(pwm_get_duty_cycle(pwm, &duty_cycle) == 0); + passert(fabs(duty_cycle - 0.25) < 1e-3); + passert(pwm_get_duty_cycle_ns(pwm, &duty_cycle_ns) == 0); + passert(fabs(duty_cycle_ns - 250000) < 1e4); + + passert(pwm_set_duty_cycle(pwm, 0.50) == 0); + passert(pwm_get_duty_cycle(pwm, &duty_cycle) == 0); + passert(fabs(duty_cycle - 0.50) < 1e-3); + passert(pwm_get_duty_cycle_ns(pwm, &duty_cycle_ns) == 0); + passert(fabs(duty_cycle_ns - 500000) < 1e4); + + passert(pwm_set_duty_cycle(pwm, 0.75) == 0); + passert(pwm_get_duty_cycle(pwm, &duty_cycle) == 0); + passert(fabs(duty_cycle - 0.75) < 1e-3); + passert(pwm_get_duty_cycle_ns(pwm, &duty_cycle_ns) == 0); + passert(fabs(duty_cycle_ns - 750000) < 1e4); + + /* Set duty_cycle_ns, check duty_cycle_ns, check duty_cycle */ + passert(pwm_set_duty_cycle_ns(pwm, 250000) == 0); + passert(pwm_get_duty_cycle_ns(pwm, &duty_cycle_ns) == 0); + passert(fabs(duty_cycle_ns - 250000) < 1e4); + passert(pwm_get_duty_cycle(pwm, &duty_cycle) == 0); + passert(fabs(duty_cycle - 0.25) < 1e-3); + + passert(pwm_set_duty_cycle_ns(pwm, 500000) == 0); + passert(pwm_get_duty_cycle_ns(pwm, &duty_cycle_ns) == 0); + passert(fabs(duty_cycle_ns - 500000) < 1e4); + passert(pwm_get_duty_cycle(pwm, &duty_cycle) == 0); + passert(fabs(duty_cycle - 0.50) < 1e-3); + + passert(pwm_set_duty_cycle_ns(pwm, 750000) == 0); + passert(pwm_get_duty_cycle_ns(pwm, &duty_cycle_ns) == 0); + passert(fabs(duty_cycle_ns - 750000) < 1e4); + passert(pwm_get_duty_cycle(pwm, &duty_cycle) == 0); + passert(fabs(duty_cycle - 0.75) < 1e-3); + + /* Set polarity, check polarity */ + passert(pwm_set_polarity(pwm, PWM_POLARITY_NORMAL) == 0); + passert(pwm_get_polarity(pwm, &polarity) == 0); + passert(polarity == PWM_POLARITY_NORMAL); + + passert(pwm_set_polarity(pwm, PWM_POLARITY_INVERSED) == 0); + passert(pwm_get_polarity(pwm, &polarity) == 0); + passert(polarity == PWM_POLARITY_INVERSED); + + /* Set enabled, check enabled */ + passert(pwm_set_enabled(pwm, true) == 0); + passert(pwm_get_enabled(pwm, &enabled) == 0); + passert(enabled == true); + + passert(pwm_set_enabled(pwm, false) == 0); + passert(pwm_get_enabled(pwm, &enabled) == 0); + passert(enabled == false); + + /* Use pwm_enable()/pwm_disable(), check enabled */ + passert(pwm_enable(pwm) == 0); + passert(pwm_get_enabled(pwm, &enabled) == 0); + passert(enabled == true); + + passert(pwm_disable(pwm) == 0); + passert(pwm_get_enabled(pwm, &enabled) == 0); + passert(enabled == false); + + /* Set invalid polarity */ + passert(pwm_set_polarity(pwm, 123) == PWM_ERROR_ARG); + + passert(pwm_close(pwm) == 0); + + /* Free PWM */ + pwm_free(pwm); +} + +void test_loopback(void) { + ptest(); +} + +bool getc_yes(void) { + char buf[4]; + fgets(buf, sizeof(buf), stdin); + return (buf[0] == 'y' || buf[0] == 'Y'); +} + +void test_interactive(void) { + char str[256]; + pwm_t *pwm; + + ptest(); + + /* Allocate PWM */ + pwm = pwm_new(); + passert(pwm != NULL); + + passert(pwm_open(pwm, chip, channel) == 0); + + printf("Starting interactive test. Get out your oscilloscope, buddy!\n"); + printf("Press enter to continue...\n"); + getc(stdin); + + /* Set initial parameters and enable PWM */ + passert(pwm_set_duty_cycle(pwm, 0.0) == 0); + passert(pwm_set_frequency(pwm, 1e3) == 0); + passert(pwm_set_polarity(pwm, PWM_POLARITY_NORMAL) == 0); + passert(pwm_enable(pwm) == 0); + + /* Check tostring */ + passert(pwm_tostring(pwm, str, sizeof(str)) > 0); + printf("PWM description: %s\n", str); + printf("PWM description looks OK? y/n\n"); + passert(getc_yes()); + + /* Set 1 kHz frequency, 0.25 duty cycle */ + passert(pwm_set_frequency(pwm, 1e3) == 0); + passert(pwm_set_duty_cycle(pwm, 0.25) == 0); + printf("Frequency is 1 kHz, duty cycle is 25%%? y/n\n"); + passert(getc_yes()); + + /* Set 1 kHz frequency, 0.50 duty cycle */ + passert(pwm_set_frequency(pwm, 1e3) == 0); + passert(pwm_set_duty_cycle(pwm, 0.50) == 0); + printf("Frequency is 1 kHz, duty cycle is 50%%? y/n\n"); + passert(getc_yes()); + + /* Set 2 kHz frequency, 0.25 duty cycle */ + passert(pwm_set_frequency(pwm, 2e3) == 0); + passert(pwm_set_duty_cycle(pwm, 0.25) == 0); + printf("Frequency is 2 kHz, duty cycle is 25%%? y/n\n"); + passert(getc_yes()); + + /* Set 2 kHz frequency, 0.50 duty cycle */ + passert(pwm_set_frequency(pwm, 2e3) == 0); + passert(pwm_set_duty_cycle(pwm, 0.50) == 0); + printf("Frequency is 2 kHz, duty cycle is 50%%? y/n\n"); + passert(getc_yes()); + + passert(pwm_set_duty_cycle(pwm, 0.0) == 0); + passert(pwm_disable(pwm) == 0); + + passert(pwm_close(pwm) == 0); + + /* Free PWM */ + pwm_free(pwm); +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n\n", argv[0]); + fprintf(stderr, "[1/4] Arguments test: No requirements.\n"); + fprintf(stderr, "[2/4] Open/close test: PWM channel should be real.\n"); + fprintf(stderr, "[3/4] Loopback test: No test.\n"); + fprintf(stderr, "[4/4] Interactive test: PWM channel should be observed with an oscilloscope or logic analyzer.\n\n"); + fprintf(stderr, "Hint: for Raspberry Pi 3, enable PWM0 and PWM1 with:\n"); + fprintf(stderr, " $ echo \"dtoverlay=pwm-2chan,pin=18,func=2,pin2=13,func2=4\" | sudo tee -a /boot/config.txt\n"); + fprintf(stderr, " $ sudo reboot\n"); + fprintf(stderr, "Monitor GPIO 18 (header pin 12), and run this test with:\n"); + fprintf(stderr, " %s 0 0\n", argv[0]); + fprintf(stderr, "or, monitor GPIO 13 (header pin 33), and run this test with:\n"); + fprintf(stderr, " %s 0 1\n\n", argv[0]); + exit(1); + } + + chip = strtoul(argv[1], NULL, 10); + channel = strtoul(argv[2], NULL, 10); + + test_arguments(); + printf(" " STR_OK " Arguments test passed.\n\n"); + test_open_config_close(); + printf(" " STR_OK " Open/close test passed.\n\n"); + test_loopback(); + printf(" " STR_OK " Loopback test passed.\n\n"); + test_interactive(); + printf(" " STR_OK " Interactive test passed.\n\n"); + + printf("All tests passed!\n"); + return 0; +} +