blob: 8f922af764f9f98d87f3c5f654a9da58f0be1da1 [file] [log] [blame]
/*
* 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.
*/
#include <assert.h>
#include <errno.h>
#include <string.h>
#include "os/mynewt.h"
#include <pwm/pwm.h>
#include <hal/hal_bsp.h>
#include <hal/hal_gpio.h>
#define BASE_FREQ MYNEWT_VAL(OS_CPUTIME_FREQ)
#define MAX_FREQ BASE_FREQ / 2
#define DEV_COUNT MYNEWT_VAL(SOFT_PWM_DEVS)
#define CHAN_COUNT MYNEWT_VAL(SOFT_PWM_CHANS)
#define NO_PIN 0xff
struct soft_pwm_channel {
uint8_t pin;
bool inverted;
uint16_t fraction;
bool running;
struct hal_timer toggle_timer;
};
struct soft_pwm_dev {
bool playing;
uint32_t frequency;
uint16_t top_value;
uint32_t n_cycles;
uint32_t cycle_cnt;
user_handler_t cycle_handler;
user_handler_t seq_end_handler;
void* cycle_data;
void* seq_end_data;
struct hal_timer cycle_timer;
struct soft_pwm_channel chans[CHAN_COUNT];
};
static struct soft_pwm_dev instances[DEV_COUNT];
/**
* Cycle start callback
*
* Initializes every channel's output to high (or low accrding to its polarity).
* Schedules toggle_cb for every channel.
*/
static void cycle_cb(void* arg)
{
int cnum;
bool inverted;
struct soft_pwm_dev* instance = (struct soft_pwm_dev *) arg;
struct soft_pwm_channel* chans = instance->chans;
uint32_t now = os_cputime_get32();
if (instance->n_cycles) {
instance->cycle_cnt++;
instance->playing = (instance->cycle_cnt < instance->n_cycles);
}
if (instance->playing) {
for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
if (chans[cnum].running) {
inverted = chans[cnum].inverted;
hal_gpio_write(chans[cnum].pin, (inverted) ? 0 : 1);
os_cputime_timer_start(&chans[cnum].toggle_timer,
now + chans[cnum].fraction);
}
}
os_cputime_timer_start(&instance->cycle_timer,
now + instance->top_value);
if (instance->cycle_handler) {
instance->cycle_handler(instance->cycle_data);
}
} else if (instance->seq_end_handler) {
instance->seq_end_handler(instance->seq_end_data);
}
}
/**
* Channel output toggle callback
*
* Toggles a channel's output.
*/
static void toggle_cb(void* arg)
{
uint32_t *pin = (uint32_t *) arg;
hal_gpio_toggle(*pin);
}
/**
* Open the Soft PWM device
*
* This function locks the device for access from other tasks.
*
* @param odev The OS device to open
* @param wait The time in MS to wait. If 0 specified, returns immediately
* if resource unavailable. If OS_WAIT_FOREVER specified, blocks
* until resource is available.
* @param arg
* @return 0 on success, non-zero on failure.
*/
static int
soft_pwm_open(struct os_dev *odev, uint32_t wait, void *arg)
{
uint32_t cnum;
uint32_t stat = 0;
struct pwm_dev *dev = (struct pwm_dev *) odev;
struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];
if (os_started()) {
stat = os_mutex_pend(&dev->pwm_lock, wait);
if (stat != OS_OK) {
return (stat);
}
}
if (odev->od_flags & OS_DEV_F_STATUS_OPEN) {
os_mutex_release(&dev->pwm_lock);
stat = OS_EBUSY;
return (stat);
}
instance->frequency = 100;
instance->top_value = BASE_FREQ / 100;
os_cputime_timer_init(&instance->cycle_timer,
cycle_cb,
&instances[dev->pwm_instance_id]);
instance->playing = false;
for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
instance->chans[cnum].pin = NO_PIN;
instance->chans[cnum].fraction = 0;
instance->chans[cnum].inverted = false;
instance->chans[cnum].running = false;
os_cputime_timer_init(&instance->chans[cnum].toggle_timer,
toggle_cb,
&instance->chans[cnum].pin);
}
return (0);
}
/**
* Close the Soft PWM device.
*
* This function unlocks the device.
*
* @param odev The device to close.
*/
static int
soft_pwm_close(struct os_dev *odev)
{
uint8_t cnum;
struct pwm_dev *dev = (struct pwm_dev *) odev;
struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];
os_cputime_timer_stop(&instance->cycle_timer);
for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
os_cputime_timer_stop(&instance->chans[cnum].toggle_timer);
}
if (os_started()) {
os_mutex_release(&dev->pwm_lock);
}
return (0);
}
/**
* Configure a PWM device.
*
* @param dev The device to configure.
* @param cfg Configuration data for this device. If NULL the device will be
* given default configuration values.
*
* @return 0 on success, non-zero error code on failure.
*/
int
soft_pwm_configure_device(struct pwm_dev *dev, struct pwm_dev_cfg *cfg)
{
struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];
if (!cfg) {
instance->cycle_handler = NULL;
instance->seq_end_handler = NULL;
instance->cycle_data = NULL;
instance->seq_end_data = NULL;
cfg->n_cycles = 0;
return (0);
}
instance->n_cycles = (cfg->n_cycles) ? cfg->n_cycles : 0;
/* Configure Interrupts */
if (cfg->cycle_handler || cfg->seq_end_handler) {
instance->cycle_handler = (user_handler_t) cfg->cycle_handler;
instance->seq_end_handler = (user_handler_t) cfg->seq_end_handler;
instance->cycle_data = cfg->cycle_data;
instance->seq_end_data = cfg->seq_end_data;
}
return (0);
}
/**
* Configure a channel on the PWM device.
*
* @param dev The device to configure.
* @param cnum The channel number to configure.
* @param cfg Configuration data for this channel. If NULL the channel will be
* disabled or given default configuration values.
*
* @return 0 on success, non-zero error code on failure.
*/
static int
soft_pwm_configure_channel(struct pwm_dev *dev,
uint8_t cnum,
struct pwm_chan_cfg *cfg)
{
uint32_t last_pin;
struct soft_pwm_dev *instance = &instances[dev->pwm_instance_id];
struct soft_pwm_channel *chan = &instance->chans[cnum];
last_pin = (chan->pin == NO_PIN) ?
cfg->pin :
chan->pin;
/* Set the previously used pin to low */
if (cfg->pin != last_pin) {
if (chan->running) {
chan->running = false;
os_cputime_timer_stop(&chan->toggle_timer);
}
hal_gpio_write(last_pin, 0);
}
if (cfg) {
chan->pin = cfg->pin;
chan->inverted = cfg->inverted;
hal_gpio_init_out(cfg->pin, (cfg->inverted) ? 1 : 0);
} else { /* unconfigure the channel */
chan->pin = NO_PIN;
chan->inverted = false;
chan->fraction = 0;
chan->running = false;
}
return (0);
}
/**
* Enable the PWM with specified duty cycle.
*
* This duty cycle is a fractional duty cycle where 0 == off, clk_freq/pwm_freq=on,
* and any value in between is on for fraction clocks and off
* for 65535-fraction clocks.
*
* @param dev The device to configure.
* @param cnum The channel number. The channel should be in use.
* @param fraction The fraction value.
*
* @return 0 on success, negative on error.
*/
static int
soft_pwm_enable(struct pwm_dev *dev)
{
uint8_t cnum;
bool level;
struct soft_pwm_dev* instance = &instances[dev->pwm_instance_id];
struct soft_pwm_channel* chans = instance->chans;
/* Handling 0% and 100% duty cycle channels. */
for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
if ((chans[cnum].pin != NO_PIN) && (!chans[cnum].running)) {
level = (chans[cnum].inverted && chans[cnum].fraction == 0) ||
(!chans[cnum].inverted && chans[cnum].fraction != 0);
hal_gpio_write(chans[cnum].pin, (level) ? 1 : 0);
}
}
if (instance->n_cycles) {
instance->cycle_cnt = 0;
}
instance->playing = true;
cycle_cb(instance);
return (0);
}
/**
* Enable the PWM with specified duty cycle.
*
* This duty cycle is a fractional duty cycle where 0 == off, clk_freq/pwm_freq=on,
* and any value in between is on for fraction clocks and off
* for 65535-fraction clocks.
*
* @param dev The device to configure.
* @param cnum The channel number. The channel should be in use.
* @param fraction The fraction value.
*
* @return 0 on success, negative on error.
*/
static int
soft_pwm_set_duty_cycle(struct pwm_dev *dev, uint8_t cnum, uint16_t fraction)
{
bool level;
struct soft_pwm_dev* instance = &instances[dev->pwm_instance_id];
struct soft_pwm_channel* chan = &instance->chans[cnum];
assert (chan->pin != NO_PIN);
chan->fraction = (fraction <= instance->top_value) ?
fraction :
instance->top_value;
/* Handling 0% and 100% duty cycles. */
if ((fraction < instance->top_value) && (fraction > 0)) {
chan->running = true;
} else {
chan->running = false;
os_cputime_timer_stop(&chan->toggle_timer);
if (instance->playing) {
level = (chan->inverted && fraction == 0) ||
(!chan->inverted && fraction != 0);
hal_gpio_write(chan->pin, (level) ? 1 : 0);
}
}
return (0);
}
/**
* Check whether a PWM channel is enabled on a given device.
*
* @param dev The device which the channel belongs to.
* @param cnum The channel being queried.
*
* @return true if enabled, false if not.
*/
static bool
soft_pwm_is_enabled(struct pwm_dev *dev)
{
return instances[dev->pwm_instance_id].playing;
}
/**
* Disable the PWM device, the device will stop playing while
* remaining configured.
*
* @param dev The device to disable.
*
* @return 0 on success, negative on error.
*/
static int
soft_pwm_disable(struct pwm_dev *dev)
{
uint8_t cnum;
struct soft_pwm_dev* instance = &instances[dev->pwm_instance_id];
instance->playing = false;
os_cputime_timer_stop(&instance->cycle_timer);
for (cnum = 0; cnum < CHAN_COUNT; cnum++) {
if (instance->chans[cnum].pin != NO_PIN) {
os_cputime_timer_stop(&instance->chans[cnum].toggle_timer);
hal_gpio_write(instance->chans[cnum].pin, 0);
}
}
return (0);
}
/**
* This frequency must be between 1/2 the clock frequency and
* the clock divided by the resolution. NOTE: This may affect
* other PWM channels.
*
* @param dev The device to configure.
* @param freq_hz The frequency value in Hz.
*
* @return A value is in Hz on success, negative on error.
*/
static int
soft_pwm_set_frequency(struct pwm_dev *dev, uint32_t freq_hz)
{
freq_hz = (freq_hz > MAX_FREQ) ? MAX_FREQ : freq_hz;
freq_hz = (freq_hz < 2) ? 2 : freq_hz;
instances[dev->pwm_instance_id].top_value = BASE_FREQ / freq_hz;
return BASE_FREQ;
}
/**
* Get the underlying clock driving the PWM device.
*
* @param dev
*
* @return value is in Hz on success, negative on error.
*/
static int
soft_pwm_get_clock_freq(struct pwm_dev *dev)
{
return BASE_FREQ;
}
/**
* Get the top value for the cycle counter, i.e. the value which sets
* the duty cycle to 100%.
*
* @param dev
*
* @return value in cycles on success, negative on error.
*/
int
soft_pwm_get_top_value(struct pwm_dev *dev)
{
return (instances[dev->pwm_instance_id].top_value);
}
/**
* Get the resolution of the PWM in bits.
*
* @param dev The device to query.
*
* @return The value in bits on success, negative on error.
*/
static int
soft_pwm_get_resolution_bits(struct pwm_dev *dev)
{
int shamt;
uint16_t mask = 0x8000;
uint16_t top_val = instances[dev->pwm_instance_id].top_value;
for (shamt = 0; shamt < 15; shamt++) {
if (top_val & mask) {
break;
}
mask >>= 1;
}
return (16 - shamt - 1);
}
/**
* Callback to initialize an adc_dev structure from the os device
* initialization callback. This sets up a soft_pwm_device(), so
* that subsequent lookups to this device allow us to manipulate it.
*/
int
soft_pwm_dev_init(struct os_dev *odev, void *arg)
{
struct pwm_dev *dev;
struct pwm_driver_funcs *pwm_funcs;
assert(odev);
dev = (struct pwm_dev *) odev;
/*
* Originally arg was expected to be a pointer to int with pwm_instance_id,
* however this wastes sizeof(int) of memory only to keep a simple number
* which is used only once. Instead, instance_id can be passed as int casted
* to pointer using helper macro so let's handle both versions here:
* - if number is valid instance_id, let's use it directly
* - otherwise assume it's a valid pointer
*/
if (POINTER_TO_UINT(arg) < DEV_COUNT) {
dev->pwm_instance_id = POINTER_TO_UINT(arg);
} else {
dev->pwm_instance_id = *((int*) arg);
assert(dev->pwm_instance_id < DEV_COUNT);
}
dev->pwm_chan_count = CHAN_COUNT;
os_mutex_init(&dev->pwm_lock);
OS_DEV_SETHANDLERS(odev, soft_pwm_open, soft_pwm_close);
pwm_funcs = &dev->pwm_funcs;
pwm_funcs->pwm_configure_device = soft_pwm_configure_device;
pwm_funcs->pwm_configure_channel = soft_pwm_configure_channel;
pwm_funcs->pwm_set_duty_cycle = soft_pwm_set_duty_cycle;
pwm_funcs->pwm_enable = soft_pwm_enable;
pwm_funcs->pwm_is_enabled = soft_pwm_is_enabled;
pwm_funcs->pwm_set_frequency = soft_pwm_set_frequency;
pwm_funcs->pwm_get_clock_freq = soft_pwm_get_clock_freq;
pwm_funcs->pwm_get_top_value = soft_pwm_get_top_value;
pwm_funcs->pwm_get_resolution_bits = soft_pwm_get_resolution_bits;
pwm_funcs->pwm_disable = soft_pwm_disable;
return (0);
}