You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

617 lines
21 KiB

/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2020-2021 Damien P. George
* Copyright (c) 2021 Robert Hammelrath
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "py/runtime.h"
#include "py/mphal.h"
#include "modmachine.h"
#include "pin.h"
#include "fsl_clock.h"
#include "fsl_iomuxc.h"
#include "hal/pwm_backport.h"
#define PWM_MIDDLE (0)
#define PWM_BEGIN (1)
#define PWM_END (2)
#define PWM_CHANNEL1 (1)
#define PWM_CHANNEL2 (2)
typedef struct _machine_pwm_obj_t {
mp_obj_base_t base;
PWM_Type *instance;
bool is_flexpwm;
uint8_t complementary;
uint8_t module;
uint8_t submodule;
uint8_t channel1;
uint8_t channel2;
uint8_t invert;
bool sync;
uint32_t freq;
int16_t prescale;
uint16_t duty_u16;
uint32_t duty_ns;
uint16_t center;
uint32_t deadtime;
bool output_enable_1;
bool output_enable_2;
uint8_t xor;
bool is_init;
} machine_pwm_obj_t;
static char channel_char[] = {'B', 'A', 'X' };
static char *ERRMSG_FREQ = "PWM frequency too low";
static char *ERRMSG_INIT = "PWM set-up failed";
static char *ERRMSG_VALUE = "value larger than period";
STATIC void machine_pwm_start(machine_pwm_obj_t *self);
STATIC void mp_machine_pwm_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
machine_pwm_obj_t *self = MP_OBJ_TO_PTR(self_in);
if (self->is_flexpwm) {
mp_printf(print, "<FLEXPWM module=%u submodule=%u ", self->module, self->submodule);
if (self->complementary) {
mp_printf(print, "channel=%c/%c", channel_char[self->channel1], channel_char[self->channel2]);
} else {
mp_printf(print, "channel=%c", channel_char[self->channel1]);
}
if (self->duty_ns != 0) {
mp_printf(print, " duty_ns=%u", self->duty_ns);
} else {
mp_printf(print, " duty_u16=%u", self->duty_u16);
}
mp_printf(print, " freq=%u center=%u, deadtime=%u, sync=%u>",
self->freq, self->center, self->deadtime, self->sync);
#ifdef FSL_FEATURE_SOC_TMR_COUNT
} else {
mp_printf(print, "<QTMR_PWM module=%u channel=%u freq1=%u ",
self->module, self->channel1, self->freq);
if (self->duty_ns != 0) {
mp_printf(print, "duty_ns=%u>", self->duty_ns);
} else {
mp_printf(print, "duty_u16=%u>", self->duty_u16);
}
#endif
}
}
// Utility functions for decoding and convertings
//
STATIC uint32_t duty_ns_to_duty_u16(uint32_t freq, uint32_t duty_ns) {
uint64_t duty = (uint64_t)duty_ns * freq * PWM_FULL_SCALE / 1000000000ULL;
if (duty >= PWM_FULL_SCALE) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_VALUE));
}
return (uint32_t)duty;
}
STATIC uint8_t module_decode(char channel) {
switch (channel) {
case '0':
return kPWM_Module_0;
case '1':
return kPWM_Module_1;
case '2':
return kPWM_Module_2;
case '3':
return kPWM_Module_3;
default:
return kPWM_Module_1;
}
}
STATIC uint8_t channel_decode(char channel) {
switch (channel) {
case 'A':
return kPWM_PwmA;
case 'B':
return kPWM_PwmB;
case 'X':
return kPWM_PwmX;
default:
return kPWM_PwmA;
}
}
// decode the AF objects module and Port numer. Returns NULL if it is not a FLEXPWM object
STATIC const machine_pin_af_obj_t *af_name_decode_flexpwm(const machine_pin_af_obj_t *af_obj,
uint8_t *module, uint8_t *submodule, uint8_t *channel) {
const char *str;
size_t len;
str = (char *)qstr_data(af_obj->name, &len);
// test for the name starting with FLEXPWM
if (len < 15 || strncmp(str, "FLEXPWM", 7) != 0) {
return NULL;
}
// Get module, submodule and channel from the name, e.g. FLEXPWM1_PWM0_A
*module = str[7] - '0';
*submodule = module_decode(str[12]);
*channel = channel_decode(str[14]);
return af_obj;
}
#ifdef FSL_FEATURE_SOC_TMR_COUNT
STATIC uint8_t qtmr_decode(char channel) {
switch (channel) {
case '0':
return kQTMR_Channel_0;
case '1':
return kQTMR_Channel_1;
case '2':
return kQTMR_Channel_2;
case '3':
return kQTMR_Channel_3;
default:
return kPWM_Module_1;
}
}
// decode the AF objects module and Port numer. Returns NULL if it is not a QTMR object
STATIC const machine_pin_af_obj_t *af_name_decode_qtmr(const machine_pin_af_obj_t *af_obj, uint8_t *module, uint8_t *channel) {
const char *str;
size_t len;
str = (char *)qstr_data(af_obj->name, &len);
// test for the name starting with TMR
if (len < 11 || strncmp(str, "TMR", 3) != 0) {
return NULL;
}
// Get module, submodule and channel from the name, e.g. FLEXPWM1_PWM0_A
*module = str[3] - '0';
*channel = qtmr_decode(str[10]);
return af_obj;
}
#endif
STATIC bool is_board_pin(const machine_pin_obj_t *pin) {
for (int i = 0; i < num_board_pins; i++) {
if (pin == machine_pin_board_pins[i]) {
return true;
}
}
return false;
}
// Functions for configuring the PWM Device
//
STATIC int calc_prescaler(uint32_t clock, uint32_t freq) {
float temp = (float)clock / (float)PWM_FULL_SCALE / (float)freq;
for (int prescale = 0; prescale < 8; prescale++, temp /= 2) {
if (temp <= 1) {
return prescale;
}
}
// Frequency too low, cannot scale down.
return -1;
}
STATIC void configure_flexpwm(machine_pwm_obj_t *self) {
pwm_signal_param_u16_t pwmSignal;
// Initialize PWM module.
uint32_t pwmSourceClockInHz = CLOCK_GetFreq(kCLOCK_IpgClk);
int prescale = calc_prescaler(pwmSourceClockInHz, self->freq);
if (prescale < 0) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_FREQ));
}
if (self->prescale != prescale || self->is_init == false) {
pwm_config_t pwmConfig;
PWM_GetDefaultConfig(&pwmConfig);
self->prescale = prescale;
pwmConfig.prescale = prescale;
pwmConfig.reloadLogic = kPWM_ReloadPwmFullCycle;
if (self->complementary) {
pwmConfig.pairOperation = self->channel1 == kPWM_PwmA ? kPWM_ComplementaryPwmA : kPWM_ComplementaryPwmB;
} else {
pwmConfig.pairOperation = kPWM_Independent;
}
pwmConfig.clockSource = kPWM_BusClock;
pwmConfig.enableWait = false;
pwmConfig.initializationControl = self->sync ? kPWM_Initialize_MasterSync : kPWM_Initialize_LocalSync;
if (PWM_Init(self->instance, self->submodule, &pwmConfig) == kStatus_Fail) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_INIT));
}
}
// Disable the fault detect function to avoid using the xbara
PWM_SetupFaultDisableMap(self->instance, self->submodule, self->channel1, kPWM_faultchannel_0, 0);
PWM_SetupFaultDisableMap(self->instance, self->submodule, self->channel1, kPWM_faultchannel_1, 0);
if (self->complementary) {
PWM_SetupFaultDisableMap(self->instance, self->submodule, self->channel2, kPWM_faultchannel_0, 0);
PWM_SetupFaultDisableMap(self->instance, self->submodule, self->channel2, kPWM_faultchannel_1, 0);
}
if (self->channel1 != kPWM_PwmX) { // Only for A/B channels
// Initialize the channel parameters
pwmSignal.pwmChannel = self->channel1;
pwmSignal.level = (self->invert & PWM_CHANNEL1) ? kPWM_LowTrue : kPWM_HighTrue;
pwmSignal.dutyCycle_u16 = self->duty_u16;
pwmSignal.Center_u16 = self->center;
pwmSignal.deadtimeValue = ((uint64_t)pwmSourceClockInHz * self->deadtime) / 1000000000ULL;
PWM_SetupPwm_u16(self->instance, self->submodule, &pwmSignal, self->freq,
pwmSourceClockInHz, self->output_enable_1);
if (self->complementary) {
// Initialize the second channel of the pair.
pwmSignal.pwmChannel = self->channel2;
pwmSignal.level = (self->invert & PWM_CHANNEL2) ? kPWM_LowTrue : kPWM_HighTrue;
PWM_SetupPwm_u16(self->instance, self->submodule, &pwmSignal, self->freq,
pwmSourceClockInHz, self->output_enable_2);
}
if (self->xor == 1) {
// Set the DBLEN bit for A, B = A ^ B
self->instance->SM[self->submodule].CTRL &= ~PWM_CTRL_SPLIT_MASK;
self->instance->SM[self->submodule].CTRL |= PWM_CTRL_DBLEN_MASK;
} else if (self->xor == 2) {
// Set the DBLEN and SPLIT bits for A, B = A ^ B
self->instance->SM[self->submodule].CTRL |= PWM_CTRL_DBLEN_MASK | PWM_CTRL_SPLIT_MASK;
} else {
self->instance->SM[self->submodule].CTRL &= ~(PWM_CTRL_DBLEN_MASK | PWM_CTRL_SPLIT_MASK);
}
} else {
PWM_SetupPwmx_u16(self->instance, self->submodule, self->freq, self->duty_u16,
self->invert, pwmSourceClockInHz);
if (self->xor) {
// Set the DBLX bit for X = A ^ B
self->instance->SM[self->submodule].CTRL |= PWM_CTRL_DBLX_MASK;
} else {
self->instance->SM[self->submodule].CTRL &= ~PWM_CTRL_DBLX_MASK;
}
}
// Set the load okay bit for the submodules
PWM_SetPwmLdok(self->instance, 1 << self->submodule, true);
// Start the PWM generation from the Submodules
PWM_StartTimer(self->instance, 1 << self->submodule);
}
#ifdef FSL_FEATURE_SOC_TMR_COUNT
STATIC void configure_qtmr(machine_pwm_obj_t *self) {
qtmr_config_t qtmrConfig;
int prescale;
TMR_Type *instance = (TMR_Type *)self->instance;
prescale = calc_prescaler(CLOCK_GetFreq(kCLOCK_IpgClk), self->freq);
if (prescale < 0) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_FREQ));
}
if (prescale != self->prescale) {
QTMR_GetDefaultConfig(&qtmrConfig);
qtmrConfig.primarySource = prescale + kQTMR_ClockDivide_1;
QTMR_Init(instance, self->channel1, &qtmrConfig);
self->prescale = prescale;
}
// Set up the PWM channel
if (QTMR_SetupPwm_u16(instance, self->channel1, self->freq, self->duty_u16,
self->invert, CLOCK_GetFreq(kCLOCK_IpgClk) / (1 << prescale), self->is_init) == kStatus_Fail) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_INIT));
}
// Start the output
QTMR_StartTimer(instance, self->channel1, kQTMR_PriSrcRiseEdge);
}
#endif // FSL_FEATURE_SOC_TMR_COUNT
STATIC void configure_pwm(machine_pwm_obj_t *self) {
// Set the clock frequencies
// Freq range is 15Hz to ~ 3 MHz.
static bool set_frequency = true;
// set the frequency only once
if (set_frequency) {
CLOCK_SetDiv(kCLOCK_IpgDiv, 0x3); // Set IPG PODF to 3, divide by 4
set_frequency = false;
}
if (self->duty_ns != 0) {
self->duty_u16 = duty_ns_to_duty_u16(self->freq, self->duty_ns);
}
if (self->is_flexpwm) {
configure_flexpwm(self);
#ifdef FSL_FEATURE_SOC_TMR_COUNT
} else {
configure_qtmr(self);
#endif
}
}
// Micropython API functions
//
STATIC void mp_machine_pwm_init_helper(machine_pwm_obj_t *self,
size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_freq, ARG_duty_u16, ARG_duty_ns, ARG_center, ARG_align,
ARG_invert, ARG_sync, ARG_xor, ARG_deadtime };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_freq, MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_duty_u16, MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_duty_ns, MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_center, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} },
{ MP_QSTR_align, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1}},
{ MP_QSTR_invert, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1}},
{ MP_QSTR_sync, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1}},
{ MP_QSTR_xor, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1}},
{ MP_QSTR_deadtime, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1}},
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args,
MP_ARRAY_SIZE(allowed_args), allowed_args, args);
if ((n_args + kw_args->used) > 0 || self->is_init == false) {
// Maybe change PWM timer
if (args[ARG_freq].u_int > 0) {
self->freq = args[ARG_freq].u_int;
}
// Set duty_u16 cycle?
uint32_t duty = args[ARG_duty_u16].u_int;
if (duty != 0) {
if (duty >= PWM_FULL_SCALE) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_VALUE));
}
self->duty_u16 = duty;
self->duty_ns = 0;
}
// Set duty_ns value?
duty = args[ARG_duty_ns].u_int;
if (duty != 0) {
self->duty_ns = duty;
self->duty_u16 = duty_ns_to_duty_u16(self->freq, self->duty_ns);
}
// Set center value?
int32_t center = args[ARG_center].u_int;
if (center >= 0) {
if (center >= PWM_FULL_SCALE) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_VALUE));
}
self->center = center;
} else { // Use alignment setting shortcut
if (args[ARG_align].u_int >= 0) {
uint8_t align = args[ARG_align].u_int & 3; // limit to 0..3
if (align == PWM_BEGIN) {
self->center = self->duty_u16 / 2;
} else if (align == PWM_END) {
self->center = PWM_FULL_SCALE - self->duty_u16 / 2;
} else {
self->center = 32768; // Default value: mid.
}
}
}
if (args[ARG_invert].u_int >= 0) {
self->invert = args[ARG_invert].u_int & (PWM_CHANNEL1 | PWM_CHANNEL2);
}
if (args[ARG_sync].u_int >= 0) {
self->sync = args[ARG_sync].u_int != false && self->submodule != 0;
}
if (args[ARG_xor].u_int >= 0) {
self->xor = args[ARG_xor].u_int & 0x03;
}
if (args[ARG_deadtime].u_int >= 0) {
self->deadtime = args[ARG_deadtime].u_int;
}
configure_pwm(self);
self->is_init = true;
} else {
machine_pwm_start(self);
}
}
// PWM(pin | pin-tuple, freq, [args])
STATIC mp_obj_t mp_machine_pwm_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
// Check number of arguments
mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true);
mp_obj_t *pins;
const machine_pin_obj_t *pin1;
const machine_pin_obj_t *pin2;
// Get referred Pin object(s)
if (mp_obj_is_type(args[0], &mp_type_tuple)) {
mp_obj_get_array_fixed_n(args[0], 2, &pins);
pin1 = pin_find(pins[0]);
pin2 = pin_find(pins[1]);
} else {
pin1 = pin_find(args[0]);
pin2 = NULL;
}
// Check whether it supports PWM and decode submodule & channel
const machine_pin_af_obj_t *af_obj1 = NULL;
uint8_t submodule1;
uint8_t channel1;
const machine_pin_af_obj_t *af_obj2 = NULL;
uint8_t submodule2;
uint8_t channel2;
uint8_t module;
bool is_flexpwm = false;
for (int i = 0; i < pin1->af_list_len; ++i) {
af_obj1 = af_name_decode_flexpwm(&(pin1->af_list[i]), &module, &submodule1, &channel1);
if (af_obj1 != NULL) {
break;
}
}
if (pin2 != NULL) {
for (int i = 0; i < pin1->af_list_len; ++i) {
af_obj2 = af_name_decode_flexpwm(&(pin2->af_list[i]), &module, &submodule2, &channel2);
if (af_obj2 != NULL) {
break;
}
}
}
if (af_obj1 == NULL) {
submodule1 = 0;
#ifdef FSL_FEATURE_SOC_TMR_COUNT
// Check for QTimer support
if (is_board_pin(pin1)) {
for (int i = 0; i < pin1->af_list_len; ++i) {
af_obj1 = af_name_decode_qtmr(&(pin1->af_list[i]), &module, &channel1);
if (af_obj1 != NULL) {
break;
}
}
}
#endif
if (af_obj1 == NULL) {
mp_raise_ValueError(MP_ERROR_TEXT("the requested Pin(s) does not support PWM"));
}
} else {
// is flexpwm, check for instance match
is_flexpwm = true;
if (pin2 != NULL && af_obj1->instance != af_obj2->instance && submodule1 != submodule2) {
mp_raise_ValueError(MP_ERROR_TEXT("the pins must be a A/B pair of a submodule"));
}
}
// Create and populate the PWM object.
machine_pwm_obj_t *self = mp_obj_malloc(machine_pwm_obj_t, &machine_pwm_type);
self->is_flexpwm = is_flexpwm;
self->instance = af_obj1->instance;
self->module = module;
self->submodule = submodule1;
self->channel1 = channel1;
self->invert = 0;
self->freq = 1000;
self->prescale = -1;
self->duty_u16 = 32768;
self->duty_ns = 0;
self->center = 32768;
self->output_enable_1 = is_board_pin(pin1);
self->sync = false;
self->deadtime = 0;
self->xor = 0;
self->is_init = false;
// Initialize the Pin(s).
CLOCK_EnableClock(kCLOCK_Iomuxc); // just in case it was not set yet
IOMUXC_SetPinMux(pin1->muxRegister, af_obj1->af_mode, af_obj1->input_register, af_obj1->input_daisy,
pin1->configRegister, 0U);
IOMUXC_SetPinConfig(pin1->muxRegister, af_obj1->af_mode, af_obj1->input_register, af_obj1->input_daisy,
pin1->configRegister, pin_generate_config(PIN_PULL_DISABLED, PIN_MODE_OUT, PIN_DRIVE_5, pin1->configRegister));
// Settings for the second pin, if given.
if (pin2 != NULL && pin2 != pin1) {
self->complementary = 1;
self->channel2 = channel2;
self->output_enable_2 = is_board_pin(pin2);
// Initialize the Pin(s)
IOMUXC_SetPinMux(pin2->muxRegister, af_obj2->af_mode, af_obj2->input_register, af_obj2->input_daisy,
pin2->configRegister, 0U);
IOMUXC_SetPinConfig(pin2->muxRegister, af_obj2->af_mode, af_obj2->input_register, af_obj2->input_daisy,
pin2->configRegister, pin_generate_config(PIN_PULL_DISABLED, PIN_MODE_OUT, PIN_DRIVE_5, pin2->configRegister));
} else {
self->complementary = 0;
}
// Process the remaining parameters.
mp_map_t kw_args;
mp_map_init_fixed_table(&kw_args, n_kw, args + n_args);
mp_machine_pwm_init_helper(self, n_args - 1, args + 1, &kw_args);
return MP_OBJ_FROM_PTR(self);
}
// Disable all PWM devices. Called on soft reset
void machine_pwm_deinit_all(void) {
static PWM_Type *const pwm_bases[] = PWM_BASE_PTRS;
for (int i = 1; i < ARRAY_SIZE(pwm_bases); i++) {
PWM_StopTimer(pwm_bases[i], 0x0f); // Stop all submodules
pwm_bases[i]->OUTEN = 0; // Disable ouput on all submodules, all channels
}
#ifdef FSL_FEATURE_SOC_TMR_COUNT
static TMR_Type *const tmr_bases[] = TMR_BASE_PTRS;
for (int i = 1; i < ARRAY_SIZE(tmr_bases); i++) {
for (int j = 0; j < 4; j++) {
QTMR_StopTimer(tmr_bases[i], j); // Stop all timers
}
}
#endif
}
STATIC void machine_pwm_start(machine_pwm_obj_t *self) {
if (self->is_flexpwm) {
PWM_StartTimer(self->instance, 1 << self->submodule);
#ifdef FSL_FEATURE_SOC_TMR_COUNT
} else {
QTMR_StartTimer((TMR_Type *)self->instance, self->channel1, kQTMR_PriSrcRiseEdge);
#endif
}
}
STATIC void mp_machine_pwm_deinit(machine_pwm_obj_t *self) {
if (self->is_flexpwm) {
PWM_StopTimer(self->instance, 1 << self->submodule);
#ifdef FSL_FEATURE_SOC_TMR_COUNT
} else {
QTMR_StopTimer((TMR_Type *)self->instance, self->channel1);
#endif
}
}
mp_obj_t mp_machine_pwm_freq_get(machine_pwm_obj_t *self) {
return MP_OBJ_NEW_SMALL_INT(self->freq);
}
void mp_machine_pwm_freq_set(machine_pwm_obj_t *self, mp_int_t freq) {
self->freq = freq;
configure_pwm(self);
}
mp_obj_t mp_machine_pwm_duty_get_u16(machine_pwm_obj_t *self) {
return MP_OBJ_NEW_SMALL_INT(self->duty_u16);
}
void mp_machine_pwm_duty_set_u16(machine_pwm_obj_t *self, mp_int_t duty) {
if (duty >= 0) {
if (duty >= PWM_FULL_SCALE) {
mp_raise_ValueError(MP_ERROR_TEXT(ERRMSG_VALUE));
}
self->duty_u16 = duty;
self->duty_ns = 0;
configure_pwm(self);
}
}
mp_obj_t mp_machine_pwm_duty_get_ns(machine_pwm_obj_t *self) {
return MP_OBJ_NEW_SMALL_INT(1000000000ULL / self->freq * self->duty_u16 / PWM_FULL_SCALE);
}
void mp_machine_pwm_duty_set_ns(machine_pwm_obj_t *self, mp_int_t duty) {
if (duty >= 0) {
self->duty_ns = duty;
self->duty_u16 = duty_ns_to_duty_u16(self->freq, self->duty_ns);
configure_pwm(self);
}
}