diff options
| -rw-r--r-- | ports/samd/Makefile | 1 | ||||
| -rw-r--r-- | ports/samd/machine_pwm.c | 362 | ||||
| -rw-r--r-- | ports/samd/main.c | 2 | ||||
| -rw-r--r-- | ports/samd/modmachine.c | 1 | ||||
| -rw-r--r-- | ports/samd/modmachine.h | 1 | ||||
| -rw-r--r-- | ports/samd/mpconfigport.h | 4 | ||||
| -rw-r--r-- | ports/samd/mphalport.c | 4 | ||||
| -rw-r--r-- | ports/samd/mphalport.h | 1 |
8 files changed, 376 insertions, 0 deletions
diff --git a/ports/samd/Makefile b/ports/samd/Makefile index 910ecdac3..514c92ba1 100644 --- a/ports/samd/Makefile +++ b/ports/samd/Makefile @@ -142,6 +142,7 @@ SRC_QSTR += \ machine_adc.c \ machine_led.c \ machine_pin.c \ + machine_pwm.c \ modutime.c \ modmachine.c \ modsamd.c \ diff --git a/ports/samd/machine_pwm.c b/ports/samd/machine_pwm.c new file mode 100644 index 000000000..6f9ca42b1 --- /dev/null +++ b/ports/samd/machine_pwm.c @@ -0,0 +1,362 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2020-2021 Damien P. George + * Copyright (c) 2022 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 "sam.h" +#include "pin_af.h" + +/******************************************************************************/ +// MicroPython bindings for machine.PWM + +typedef struct _machine_pwm_obj_t { + mp_obj_base_t base; + Tcc *instance; + uint8_t pin_id; + uint8_t alt_fct; + uint8_t device; + uint8_t channel; + uint8_t output; + uint16_t prescaler; + uint32_t period; // full period count ticks +} machine_pwm_obj_t; + +#define PWM_NOT_INIT (0) +#define PWM_CLK_READY (1) +#define PWM_TCC_ENABLED (2) +#define PWM_MASTER_CLK (48000000) +#define PWM_FULL_SCALE (65536) + +static Tcc *tcc_instance[] = TCC_INSTS; + +#if defined(MCU_SAMD21) + +static const int tcc_gclk_id[] = { + GCLK_CLKCTRL_ID_TCC0_TCC1, GCLK_CLKCTRL_ID_TCC0_TCC1, GCLK_CLKCTRL_ID_TCC2_TC3 +}; +const uint8_t tcc_channel_count[] = {4, 2, 2}; +const static uint8_t tcc_channel_offset[] = {0, 4, 6}; +static uint32_t pwm_duty_values[8]; + +#define PERBUF PERB +#define CCBUF CCB + +#elif defined(MCU_SAMD51) + +static const int tcc_gclk_id[] = { + TCC0_GCLK_ID, TCC1_GCLK_ID, TCC2_GCLK_ID, + #if TCC_INST_NUM > 3 + TCC3_GCLK_ID, TCC4_GCLK_ID + #endif +}; + +#if TCC_INST_NUM > 3 +const uint8_t tcc_channel_count[] = {6, 4, 3, 2, 2}; +const static uint8_t tcc_channel_offset[] = {0, 6, 10, 13, 15}; +static uint32_t pwm_duty_values[17]; +#else +const uint8_t tcc_channel_count[] = {6, 4, 3}; +const static uint8_t tcc_channel_offset[] = {0, 6, 10}; +static uint32_t pwm_duty_values[13]; +#endif // TCC_INST_NUM > 3 + +#endif // defined(MCU_SAMD51) + +#define put_duty_value(device, channel, duty) \ + pwm_duty_values[tcc_channel_offset[device] + channel] = duty; + +#define get_duty_value(device, channel) \ + pwm_duty_values[tcc_channel_offset[device] + channel] + +static uint8_t duty_type_flags[TCC_INST_NUM]; +static uint8_t device_status[TCC_INST_NUM]; +static uint8_t output_active[TCC_INST_NUM]; +const uint16_t prescaler_table[] = {1, 2, 4, 8, 16, 64, 256, 1024}; + +STATIC void pwm_stop_device(int device); +STATIC void mp_machine_pwm_freq_set(machine_pwm_obj_t *self, mp_int_t freq); +STATIC void mp_machine_pwm_duty_set_u16(machine_pwm_obj_t *self, mp_int_t duty_u16); +STATIC void mp_machine_pwm_duty_set_ns(machine_pwm_obj_t *self, mp_int_t duty_ns); + +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); + mp_printf(print, "PWM P%c%02u device=%u channel=%u output=%u", + "ABCD"[self->pin_id / 32], self->pin_id % 32, self->device, self->channel, self->output); +} + +// PWM(pin) +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 *all_args) { + enum { ARG_pin, ARG_freq, ARG_duty_u16, ARG_duty_ns, ARG_invert, ARG_device }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_pin, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_freq, MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_duty_u16, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_duty_ns, 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_device, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + }; + + // Parse the arguments. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Get GPIO and optional device to connect to PWM. + uint32_t pin_id = mp_hal_get_pin_obj(args[ARG_pin].u_obj); + int32_t wanted_dev = args[ARG_device].u_int; // -1 = any + + // Get the peripheral object and populate it + + pwm_config_t config = get_pwm_config(pin_id, wanted_dev, device_status); + uint8_t device = config.device_channel >> 4; + if (device >= TCC_INST_NUM) { + mp_raise_ValueError(MP_ERROR_TEXT("wrong device")); + } + + machine_pwm_obj_t *self = mp_obj_malloc(machine_pwm_obj_t, &machine_pwm_type); + self->instance = tcc_instance[device]; + self->device = device; + self->pin_id = pin_id; + self->alt_fct = config.alt_fct; + self->channel = (config.device_channel & 0x0f) % tcc_channel_count[device]; + self->output = config.device_channel & 0x0f; + self->prescaler = 1; + self->period = 1; // Use an invalid but safe value + put_duty_value(self->device, self->channel, 0); + + Tcc *tcc = self->instance; + + if (device_status[device] == PWM_NOT_INIT) { + // Enable the device clock at first use. + #if defined(MCU_SAMD21) + // Enable synchronous clock. The bits are nicely arranged + PM->APBCMASK.reg |= PM_APBCMASK_TCC0 << device; + // Select multiplexer generic clock source and enable. + GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK2 | tcc_gclk_id[device]; + // Wait while it updates synchronously. + while (GCLK->STATUS.bit.SYNCBUSY) { + } + #elif defined(MCU_SAMD51) + // GenClk2 to the tcc + GCLK->PCHCTRL[tcc_gclk_id[device]].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(2); + while (GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL_GCLK2) { + } + // Enable MCLK + switch (device) { + case 0: + MCLK->APBBMASK.reg |= MCLK_APBBMASK_TCC0; + break; + case 1: + MCLK->APBBMASK.reg |= MCLK_APBBMASK_TCC1; + break; + case 2: + MCLK->APBCMASK.reg |= MCLK_APBCMASK_TCC2; + break; + #if TCC_INST_NUM > 3 + case 3: + MCLK->APBCMASK.reg |= MCLK_APBCMASK_TCC3; + break; + case 4: + MCLK->APBDMASK.reg |= MCLK_APBDMASK_TCC4; + break; + #endif + } + #endif + // Reset the device + tcc->CTRLA.reg = TCC_CTRLA_SWRST; + while (tcc->SYNCBUSY.reg & TCC_SYNCBUSY_SWRST) { + } + tcc->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV1; + tcc->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM; + // Flag the clock as initialized, but not the device as enabled. + device_status[device] = PWM_CLK_READY; + } + + if (args[ARG_invert].u_int != -1) { + bool invert = !!args[ARG_invert].u_int; + if (device_status[device] != PWM_CLK_READY) { + pwm_stop_device(device); + } + uint32_t mask = 1 << (self->output + TCC_DRVCTRL_INVEN0_Pos); + if (invert) { + tcc->DRVCTRL.reg |= mask; + } else { + tcc->DRVCTRL.reg &= ~(mask); + } + } + if (args[ARG_duty_u16].u_int != -1) { + mp_machine_pwm_duty_set_u16(self, args[ARG_duty_u16].u_int); + } + if (args[ARG_duty_ns].u_int != -1) { + mp_machine_pwm_duty_set_ns(self, args[ARG_duty_ns].u_int); + } + if (args[ARG_freq].u_int != -1) { + mp_machine_pwm_freq_set(self, args[ARG_freq].u_int); + } + return MP_OBJ_FROM_PTR(self); +} + +STATIC void pwm_stop_device(int device) { + Tcc *tcc = tcc_instance[device]; + tcc->CTRLA.bit.ENABLE = 0; + while (tcc->SYNCBUSY.reg & TCC_SYNCBUSY_ENABLE) { + } + device_status[device] = PWM_CLK_READY; +} + +// Stop all TTC devices +void pwm_deinit_all(void) { + for (int i = 0; i < TCC_INST_NUM; i++) { + Tcc *tcc = tcc_instance[i]; + tcc->CTRLA.reg = TCC_CTRLA_SWRST; + while (tcc->SYNCBUSY.reg & TCC_SYNCBUSY_SWRST) { + } + device_status[i] = PWM_NOT_INIT; + duty_type_flags[i] = 0; + output_active[i] = 0; + } +} + +// Switch off an output. If all outputs of a device are off, +// switch off that device. +// This stops all channels, but keeps the configuration +// Calling pwm.freq(n) will start an instance again. +STATIC void mp_machine_pwm_deinit(machine_pwm_obj_t *self) { + mp_hal_clr_pin_mux(self->pin_id); // Switch the output off + output_active[self->device] &= ~(1 << self->output); // clear output flasg + // Stop the device, if no output is active. + if (output_active[self->device] == 0) { + pwm_stop_device(self->device); + } +} + +STATIC mp_obj_t mp_machine_pwm_freq_get(machine_pwm_obj_t *self) { + return MP_OBJ_NEW_SMALL_INT(PWM_MASTER_CLK / self->prescaler / self->period); +} + +STATIC void mp_machine_pwm_freq_set(machine_pwm_obj_t *self, mp_int_t freq) { + // Set the frequency. The period counter is 24 bit or 16 bit with a pre-scaling + // of up to 1024, allowing a range from 24 MHz down to 1 Hz. + static const uint32_t max_period[5] = {1 << 24, 1 << 24, 1 << 16, 1 << 16, 1 << 16}; + + Tcc *tcc = self->instance; + if (freq < 1) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid freq")); + } + + // Get the actual settings of prescaler & period from the unit + // To be able for cope for changes. + uint32_t prev_period = tcc->PER.reg + 1; + + // Check for the right prescaler + uint8_t index; + for (index = 0; index < 8; index++) { + uint32_t temp = PWM_MASTER_CLK / prescaler_table[index] / freq; + if (temp < max_period[self->device]) { + break; + } + } + self->prescaler = prescaler_table[index]; + + uint32_t period = PWM_MASTER_CLK / self->prescaler / freq; + if (period < 2) { + mp_raise_ValueError(MP_ERROR_TEXT("freq too large")); + } + // Check, if the prescaler has to be changed and stop the device if so. + if (index != tcc->CTRLA.bit.PRESCALER) { + // stop the device + pwm_stop_device(self->device); + // update the prescaler + tcc->CTRLA.bit.PRESCALER = index; + } + // Lock the update to get a glitch-free change of period and duty cycle + tcc->CTRLBSET.reg = TCC_CTRLBSET_LUPD; + tcc->PERBUF.reg = period - 1; + self->period = period; + + // Check if the Duty rate has to be aligned again when freq or prescaler were changed. + // This condition is as well true on first call after instantiation. So (re-)configure + // all channels with a duty_u16 setting. + if (period != prev_period) { + for (uint16_t ch = 0; ch < tcc_channel_count[self->device]; ch++) { + if ((duty_type_flags[self->device] & (1 << ch)) != 0) { // duty_u16 type? + tcc->CCBUF[ch].reg = (uint64_t)get_duty_value(self->device, ch) * period / + PWM_FULL_SCALE; + } + } + } + // If the prescaler was changed, the device is disabled. So this condition is true + // after the instantiation and after a prescaler change. + // (re-)configure all channels with a duty_ns setting. + if (!(tcc->CTRLA.reg & TCC_CTRLA_ENABLE)) { + for (uint16_t ch = 0; ch < tcc_channel_count[self->device]; ch++) { + if ((duty_type_flags[self->device] & (1 << ch)) == 0) { // duty_ns type? + tcc->CCBUF[ch].reg = (uint64_t)get_duty_value(self->device, ch) * PWM_MASTER_CLK / + self->prescaler / 1000000000ULL; + } + } + } + // Remember the output as active. + output_active[self->device] |= 1 << self->output; // set output flag + // (Re-)Select PWM function for given GPIO. + mp_hal_set_pin_mux(self->pin_id, self->alt_fct); + // Enable the device, if required. + if ((device_status[self->device] & PWM_TCC_ENABLED) == 0) { + tcc->CTRLBSET.reg = TCC_CTRLBSET_CMD_UPDATE; + tcc->CTRLA.reg |= TCC_CTRLA_ENABLE; + while (tcc->SYNCBUSY.reg & TCC_SYNCBUSY_ENABLE) { + } + device_status[self->device] |= PWM_TCC_ENABLED; + } + // Unlock the register update, now that the settings are complete + tcc->CTRLBCLR.reg = TCC_CTRLBCLR_LUPD; +} + +STATIC mp_obj_t mp_machine_pwm_duty_get_u16(machine_pwm_obj_t *self) { + return MP_OBJ_NEW_SMALL_INT(self->instance->CC[self->channel].reg * PWM_FULL_SCALE / (self->instance->PER.reg + 1)); +} + +STATIC void mp_machine_pwm_duty_set_u16(machine_pwm_obj_t *self, mp_int_t duty_u16) { + put_duty_value(self->device, self->channel, duty_u16); + // If the device is enabled, than the period is set and we get a reasonable value for + // the duty cycle, set to the CCBUF register. Otherwise, PWM does not start. + if (self->instance->CTRLA.reg & TCC_CTRLA_ENABLE) { + self->instance->CCBUF[self->channel].reg = (uint64_t)duty_u16 * (self->instance->PER.reg + 1) / PWM_FULL_SCALE; + } + duty_type_flags[self->device] |= 1 << self->channel; +} + +STATIC mp_obj_t mp_machine_pwm_duty_get_ns(machine_pwm_obj_t *self) { + return MP_OBJ_NEW_SMALL_INT(1000000000ULL * self->instance->CC[self->channel].reg * self->prescaler / PWM_MASTER_CLK); +} + +STATIC void mp_machine_pwm_duty_set_ns(machine_pwm_obj_t *self, mp_int_t duty_ns) { + put_duty_value(self->device, self->channel, duty_ns); + self->instance->CCBUF[self->channel].reg = (uint64_t)duty_ns * PWM_MASTER_CLK / self->prescaler / 1000000000ULL; + duty_type_flags[self->device] &= ~(1 << self->channel); +} diff --git a/ports/samd/main.c b/ports/samd/main.c index ece86f588..4af4b2b4f 100644 --- a/ports/samd/main.c +++ b/ports/samd/main.c @@ -34,6 +34,7 @@ extern uint8_t _sstack, _estack, _sheap, _eheap; extern void adc_deinit_all(void); +extern void pwm_deinit_all(void); void samd_main(void) { mp_stack_set_top(&_estack); @@ -64,6 +65,7 @@ void samd_main(void) { mp_printf(MP_PYTHON_PRINTER, "MPY: soft reboot\n"); adc_deinit_all(); + pwm_deinit_all(); gc_sweep_all(); mp_deinit(); } diff --git a/ports/samd/modmachine.c b/ports/samd/modmachine.c index 287641cc3..e0a037846 100644 --- a/ports/samd/modmachine.c +++ b/ports/samd/modmachine.c @@ -133,6 +133,7 @@ STATIC const mp_rom_map_elem_t machine_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_ADC), MP_ROM_PTR(&machine_adc_type) }, { MP_ROM_QSTR(MP_QSTR_LED), MP_ROM_PTR(&machine_led_type) }, { MP_ROM_QSTR(MP_QSTR_Pin), MP_ROM_PTR(&machine_pin_type) }, + { MP_ROM_QSTR(MP_QSTR_PWM), MP_ROM_PTR(&machine_pwm_type) }, { MP_ROM_QSTR(MP_QSTR_SoftI2C), MP_ROM_PTR(&mp_machine_soft_i2c_type) }, { MP_ROM_QSTR(MP_QSTR_SoftSPI), MP_ROM_PTR(&mp_machine_soft_spi_type) }, }; diff --git a/ports/samd/modmachine.h b/ports/samd/modmachine.h index 53914a10c..00e72ede1 100644 --- a/ports/samd/modmachine.h +++ b/ports/samd/modmachine.h @@ -31,5 +31,6 @@ extern const mp_obj_type_t machine_adc_type; extern const mp_obj_type_t machine_led_type; extern const mp_obj_type_t machine_pin_type; +extern const mp_obj_type_t machine_pwm_type; #endif // MICROPY_INCLUDED_SAMD_MODMACHINE_H diff --git a/ports/samd/mpconfigport.h b/ports/samd/mpconfigport.h index 6441818ba..70bd202a3 100644 --- a/ports/samd/mpconfigport.h +++ b/ports/samd/mpconfigport.h @@ -99,6 +99,10 @@ #define MICROPY_PY_UASYNCIO (1) #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_SOFTSPI (1) +#define MICROPY_PY_MACHINE_PWM (1) +#define MICROPY_PY_MACHINE_PWM_INIT (0) +#define MICROPY_PY_MACHINE_PWM_DUTY_U16_NS (1) +#define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/samd/machine_pwm.c" #define MP_STATE_PORT MP_STATE_VM diff --git a/ports/samd/mphalport.c b/ports/samd/mphalport.c index e51f13ab7..7a3786be8 100644 --- a/ports/samd/mphalport.c +++ b/ports/samd/mphalport.c @@ -61,6 +61,10 @@ void mp_hal_set_pin_mux(mp_hal_pin_obj_t pin, uint8_t mux) { } } +void mp_hal_clr_pin_mux(mp_hal_pin_obj_t pin) { + int pin_grp = pin / 32; + PORT->Group[pin_grp].PINCFG[pin % 32].bit.PMUXEN = 0; // Disable Mux +} void mp_hal_delay_ms(mp_uint_t ms) { if (ms > 10) { diff --git a/ports/samd/mphalport.h b/ports/samd/mphalport.h index 0a1db7df0..6161d9821 100644 --- a/ports/samd/mphalport.h +++ b/ports/samd/mphalport.h @@ -84,6 +84,7 @@ extern uint32_t machine_pin_open_drain_mask[]; mp_hal_pin_obj_t mp_hal_get_pin_obj(mp_obj_t pin_in); void mp_hal_set_pin_mux(mp_hal_pin_obj_t pin, uint8_t mux); +void mp_hal_clr_pin_mux(mp_hal_pin_obj_t pin); static inline unsigned int mp_hal_pin_name(mp_hal_pin_obj_t pin) { return pin; |
