#include "Pwm.h" #include "Config.h" #include "Core.h" #if CONFIG_IDF_TARGET_ESP32C3 #include #elif CONFIG_IDF_TARGET_ESP32S3 #include #endif namespace { #if CONFIG_IDF_TARGET_ESP32C3 constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE; constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0; void setDivider(uint32_t dividerRaw) { ledc_dev_t *hardware = LEDC_LL_GET_HW(); ledc_ll_timer_pause(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_set_clock_divider(hardware, PWM_SPEED_MODE, PWM_TIMER, dividerRaw); ledc_ll_timer_rst(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_ls_timer_update(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_timer_resume(hardware, PWM_SPEED_MODE, PWM_TIMER); } bool dividerIsSet(uint32_t expectedRaw) { uint32_t rawDivider = 0; ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider); return rawDivider == expectedRaw; } #elif CONFIG_IDF_TARGET_ESP32S3 mcpwm_timer_handle_t mcpwmTimer = nullptr; mcpwm_oper_handle_t mcpwmOperator = nullptr; mcpwm_cmpr_handle_t mcpwmComparator = nullptr; mcpwm_gen_handle_t mcpwmGenerator = nullptr; uint32_t mcpwmFrequencyHz = 0; void releaseMcpwm() { if (mcpwmGenerator) { mcpwm_del_generator(mcpwmGenerator); mcpwmGenerator = nullptr; } if (mcpwmComparator) { mcpwm_del_comparator(mcpwmComparator); mcpwmComparator = nullptr; } if (mcpwmOperator) { mcpwm_del_operator(mcpwmOperator); mcpwmOperator = nullptr; } if (mcpwmTimer) { mcpwm_timer_disable(mcpwmTimer); mcpwm_del_timer(mcpwmTimer); mcpwmTimer = nullptr; } } uint8_t periodResolutionBits(uint32_t periodTicks) { uint8_t bits = 0; while (periodTicks > 1U) { periodTicks >>= 1U; ++bits; } return bits ? bits : 1U; } #endif } void PwmGenerator::begin() { #if CONFIG_IDF_TARGET_ESP32C3 // Match LEDC_SOURCE_CLOCK_HZ and make the timer calculation deterministic. ledcSetClockSource(LEDC_USE_XTAL_CLK); pinMode(GPIO_PWM, OUTPUT); stop(); #elif CONFIG_IDF_TARGET_ESP32S3 mcpwm_timer_config_t timerConfig = {}; timerConfig.group_id = 0; timerConfig.clk_src = MCPWM_TIMER_CLK_SRC_PLL160M; timerConfig.resolution_hz = MCPWM_RESOLUTION_HZ; timerConfig.count_mode = MCPWM_TIMER_COUNT_MODE_UP; timerConfig.period_ticks = MCPWM_RESOLUTION_HZ / 1000U; mcpwm_operator_config_t operatorConfig = {}; operatorConfig.group_id = 0; mcpwm_comparator_config_t comparatorConfig = {}; mcpwm_generator_config_t generatorConfig = {}; generatorConfig.gen_gpio_num = GPIO_PWM; bool ok = mcpwm_new_timer(&timerConfig, &mcpwmTimer) == ESP_OK; ok = ok && mcpwm_new_operator(&operatorConfig, &mcpwmOperator) == ESP_OK; ok = ok && mcpwm_operator_connect_timer(mcpwmOperator, mcpwmTimer) == ESP_OK; ok = ok && mcpwm_new_comparator(mcpwmOperator, &comparatorConfig, &mcpwmComparator) == ESP_OK; ok = ok && mcpwm_new_generator(mcpwmOperator, &generatorConfig, &mcpwmGenerator) == ESP_OK; ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, timerConfig.period_ticks / 2U) == ESP_OK; ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator, MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, TX_LIGHT_ON_GPIO_LEVEL == HIGH ? MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK; ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator, MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, mcpwmComparator, TX_LIGHT_OFF_GPIO_LEVEL == HIGH ? MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK; ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK; if (!ok) { releaseMcpwm(); pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL); return; } mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true); #endif } bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) { const uint8_t activeLevel = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL; const uint8_t inactiveLevel = activeLevel == HIGH ? LOW : HIGH; #if CONFIG_IDF_TARGET_ESP32C3 IntegerPwmConfig config = {}; if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false; const uint8_t bits = config.bits; const uint32_t levels = 1UL << bits; const uint32_t duty = config.dutyCount; for (uint8_t attempt = 0; attempt < 2; ++attempt) { stop(); const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL); if (attached) { // Native LEDC produces a HIGH pulse. Invert the GPIO matrix output when // the configured active pulse level is LOW. if (!ledcOutputInvert(GPIO_PWM, activeLevel == LOW)) { ledcDetach(GPIO_PWM); delay(2); continue; } // Apply the selected Q10.8 divider explicitly. This lets pulse width, // rather than duty percentage, drive the hardware quantization. setDivider(config.dividerRaw); } if (attached && dividerIsSet(config.dividerRaw) && ledcWriteChannel(LEDC_CHANNEL, duty)) { // On the first configuration after power-up the duty update is latched // on a timer edge. Reading immediately can therefore return zero. uint32_t settleUs = static_cast((2000000ULL + hz - 1U) / hz); if (settleUs > 2000U) settleUs = 2000U; delayMicroseconds(settleUs); const uint32_t actualHz = ledcReadFreq(GPIO_PWM); if (actualHz == config.actualHz) { a = {hz, actualHz, pulseNs, config.actualPulseNs, 100.0f * duty / levels, bits}; running_ = true; return true; } } if (attached) ledcDetach(GPIO_PWM); delay(2); } pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL); return false; #elif CONFIG_IDF_TARGET_ESP32S3 if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || !pulseNs || MCPWM_RESOLUTION_HZ % hz) return false; const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz; if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false; uint32_t activeTicks = static_cast( (static_cast(pulseNs) * MCPWM_RESOLUTION_HZ + 500000000ULL) / 1000000000ULL); if (activeTicks == 0U) activeTicks = 1U; if (activeTicks >= periodTicks) activeTicks = periodTicks - 1U; stop(); bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK; ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, activeTicks) == ESP_OK; ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator, MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, activeLevel == HIGH ? MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK; ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator, MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, mcpwmComparator, inactiveLevel == HIGH ? MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK; // stop() applies a continuous force level (hold_on=true). Remove that same // continuous-force action; hold_on=false addresses a different, one-shot // force mechanism and would leave the safe level permanently active. ok = ok && mcpwm_generator_set_force_level(mcpwmGenerator, -1, true) == ESP_OK; ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK; if (!ok) { mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true); return false; } const uint32_t actualPulseNs = static_cast( (static_cast(activeTicks) * 1000000000ULL + MCPWM_RESOLUTION_HZ / 2U) / MCPWM_RESOLUTION_HZ); a = {hz, hz, pulseNs, actualPulseNs, 100.0f * activeTicks / periodTicks, periodResolutionBits(periodTicks)}; mcpwmFrequencyHz = hz; running_ = true; return true; #endif } void PwmGenerator::stop() { #if CONFIG_IDF_TARGET_ESP32C3 if (running_) ledcDetach(GPIO_PWM); pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL); #elif CONFIG_IDF_TARGET_ESP32S3 if (mcpwmGenerator) mcpwm_generator_set_force_level( mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true); if (running_ && mcpwmTimer) { mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY); const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U; delayMicroseconds(waitUs); } mcpwmFrequencyHz = 0; #endif running_ = false; } void PwmGenerator::active() { // First detach/stop the PWM peripheral, then apply the same active level // that denotes the pulse during a running test. stop(); #if CONFIG_IDF_TARGET_ESP32C3 digitalWrite(GPIO_PWM, activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL); #elif CONFIG_IDF_TARGET_ESP32S3 const uint8_t level = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL; if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, level, true); else { pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, level); } #endif } void PwmGenerator::lightOn() { stop(); #if CONFIG_IDF_TARGET_ESP32C3 digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL); #elif CONFIG_IDF_TARGET_ESP32S3 if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_ON_GPIO_LEVEL, true); else { pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL); } #endif }