Skip to content

Commit

Permalink
✨ ESP32 - Hardware PWM for fan, cutter, servos (#23802)
Browse files Browse the repository at this point in the history
  • Loading branch information
HoverClub authored and thinkyhead committed May 7, 2022
1 parent c7a2ecc commit cc8e485
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 41 deletions.
2 changes: 1 addition & 1 deletion Marlin/Configuration_adv.h
Original file line number Diff line number Diff line change
Expand Up @@ -3464,7 +3464,7 @@
#define SPINDLE_LASER_USE_PWM // Enable if your controller supports setting the speed/power
#if ENABLED(SPINDLE_LASER_USE_PWM)
#define SPINDLE_LASER_PWM_INVERT false // Set to "true" if the speed/power goes up when you want it to go slower
#define SPINDLE_LASER_FREQUENCY 2500 // (Hz) Spindle/laser frequency (only on supported HALs: AVR and LPC)
#define SPINDLE_LASER_FREQUENCY 2500 // (Hz) Spindle/laser frequency (only on supported HALs: AVR, ESP32 and LPC)
#endif

//#define AIR_EVACUATION // Cutter Vacuum / Laser Blower motor control with G-codes M10-M11
Expand Down
101 changes: 82 additions & 19 deletions Marlin/src/HAL/ESP32/HAL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,16 @@ uint16_t MarlinHAL::adc_result;
esp_adc_cal_characteristics_t characteristics[ADC_ATTEN_MAX];
adc_atten_t attenuations[ADC1_CHANNEL_MAX] = {};
uint32_t thresholds[ADC_ATTEN_MAX];
volatile int numPWMUsed = 0,
pwmPins[MAX_PWM_PINS],
pwmValues[MAX_PWM_PINS];

volatile int numPWMUsed = 0;
volatile struct { pin_t pin; int value; } pwmState[MAX_PWM_PINS];

pin_t chan_pin[CHANNEL_MAX_NUM + 1] = { 0 }; // PWM capable IOpins - not 0 or >33 on ESP32

struct {
uint32_t freq; // ledcReadFreq doesn't work if a duty hasn't been set yet!
uint16_t res;
} pwmInfo[(CHANNEL_MAX_NUM + 1) / 2];

// ------------------------
// Public functions
Expand Down Expand Up @@ -254,25 +261,81 @@ void MarlinHAL::adc_start(const pin_t pin) {
adc1_set_attenuation(chan, atten);
}

void analogWrite(pin_t pin, int value) {
// Use ledc hardware for internal pins
if (pin < 34) {
static int cnt_channel = 1, pin_to_channel[40] = { 0 };
if (pin_to_channel[pin] == 0) {
ledcAttachPin(pin, cnt_channel);
ledcSetup(cnt_channel, 490, 8);
ledcWrite(cnt_channel, value);
pin_to_channel[pin] = cnt_channel++;
// ------------------------
// PWM
// ------------------------

int8_t channel_for_pin(const uint8_t pin) {
for (int i = 0; i <= CHANNEL_MAX_NUM; i++)
if (chan_pin[i] == pin) return i;
return -1;
}

// get PWM channel for pin - if none then attach a new one
// return -1 if fail or invalid pin#, channel # (0-15) if success
int8_t get_pwm_channel(const pin_t pin, const uint32_t freq, const uint16_t res) {
if (!WITHIN(pin, 1, MAX_PWM_IOPIN)) return -1; // Not a hardware PWM pin!
int8_t cid = channel_for_pin(pin);
if (cid >= 0) return cid;

// Find an empty adjacent channel (same timer & freq/res)
for (int i = 0; i <= CHANNEL_MAX_NUM; i++) {
if (chan_pin[i] == 0) {
if (chan_pin[i ^ 0x1] != 0) {
if (pwmInfo[i / 2].freq == freq && pwmInfo[i / 2].res == res) {
chan_pin[i] = pin; // Allocate PWM to this channel
ledcAttachPin(pin, i);
return i;
}
}
else if (cid == -1) // Pair of empty channels?
cid = i & 0xFE; // Save lower channel number
}
ledcWrite(pin_to_channel[pin], value);
}
// not attached, is an empty timer slot avail?
if (cid >= 0) {
chan_pin[cid] = pin;
pwmInfo[cid / 2].freq = freq;
pwmInfo[cid / 2].res = res;
ledcSetup(cid, freq, res);
ledcAttachPin(pin, cid);
}
return cid; // -1 if no channel avail
}

void MarlinHAL::set_pwm_duty(const pin_t pin, const uint16_t v, const uint16_t v_size/*=_BV(PWM_RESOLUTION)-1*/, const bool invert/*=false*/) {
const int8_t cid = get_pwm_channel(pin, PWM_FREQUENCY, PWM_RESOLUTION);
if (cid >= 0) {
uint32_t duty = map(invert ? v_size - v : v, 0, v_size, 0, _BV(PWM_RESOLUTION)-1);
ledcWrite(cid, duty);
}
}

int8_t MarlinHAL::set_pwm_frequency(const pin_t pin, const uint32_t f_desired) {
const int8_t cid = channel_for_pin(pin);
if (cid >= 0) {
if (f_desired == ledcReadFreq(cid)) return cid; // no freq change
ledcDetachPin(chan_pin[cid]);
chan_pin[cid] = 0; // remove old freq channel
}
return get_pwm_channel(pin, f_desired, PWM_RESOLUTION); // try for new one
}

// use hardware PWM if avail, if not then ISR
void analogWrite(const pin_t pin, const uint16_t value, const uint32_t freq/*=PWM_FREQUENCY*/, const uint16_t res/*=8*/) { // always 8 bit resolution!
// Use ledc hardware for internal pins
const int8_t cid = get_pwm_channel(pin, freq, res);
if (cid >= 0) {
ledcWrite(cid, value); // set duty value
return;
}

// not a hardware PWM pin OR no PWM channels available
int idx = -1;

// Search Pin
for (int i = 0; i < numPWMUsed; ++i)
if (pwmPins[i] == pin) { idx = i; break; }
if (pwmState[i].pin == pin) { idx = i; break; }

// not found ?
if (idx < 0) {
Expand All @@ -281,15 +344,15 @@ void analogWrite(pin_t pin, int value) {

// Take new slot for pin
idx = numPWMUsed;
pwmPins[idx] = pin;
pwmState[idx].pin = pin;
// Start timer on first use
if (idx == 0) HAL_timer_start(MF_TIMER_PWM, PWM_TIMER_FREQUENCY);

++numPWMUsed;
}

// Use 7bit internal value - add 1 to have 100% high at 255
pwmValues[idx] = (value + 1) / 2;
pwmState[idx].value = (value + 1) / 2;
}

// Handle PWM timer interrupt
Expand All @@ -300,9 +363,9 @@ HAL_PWM_TIMER_ISR() {

for (int i = 0; i < numPWMUsed; ++i) {
if (count == 0) // Start of interval
WRITE(pwmPins[i], pwmValues[i] ? HIGH : LOW);
else if (pwmValues[i] == count) // End of duration
WRITE(pwmPins[i], LOW);
digitalWrite(pwmState[i].pin, pwmState[i].value ? HIGH : LOW);
else if (pwmState[i].value == count) // End of duration
digitalWrite(pwmState[i].pin, LOW);
}

// 128 for 7 Bit resolution
Expand Down
25 changes: 18 additions & 7 deletions Marlin/src/HAL/ESP32/HAL.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
#define CRITICAL_SECTION_START() portENTER_CRITICAL(&spinlock)
#define CRITICAL_SECTION_END() portEXIT_CRITICAL(&spinlock)

#define HAL_CAN_SET_PWM_FREQ // This HAL supports PWM Frequency adjustment
#define PWM_FREQUENCY 1000u // Default PWM frequency when set_pwm_duty() is called without set_pwm_frequency()
#define PWM_RESOLUTION 10u // Default PWM bit resolution
#define CHANNEL_MAX_NUM 15u // max PWM channel # to allocate (7 to only use low speed, 15 to use low & high)
#define MAX_PWM_IOPIN 33u // hardware pwm pins < 34

// ------------------------
// Types
// ------------------------
Expand All @@ -83,7 +89,7 @@ typedef Servo hal_servo_t;
void tone(const pin_t _pin, const unsigned int frequency, const unsigned long duration=0);
void noTone(const pin_t _pin);

void analogWrite(pin_t pin, int value);
void analogWrite(const pin_t pin, const uint16_t value, const uint32_t freq=PWM_FREQUENCY, const uint16_t res=8);

//
// Pin Mapping for M42, M43, M226
Expand Down Expand Up @@ -209,12 +215,17 @@ class MarlinHAL {
static uint16_t adc_value() { return adc_result; }

/**
* Set the PWM duty cycle for the pin to the given value.
* No inverting the duty cycle in this HAL.
* No changing the maximum size of the provided value to enable finer PWM duty control in this HAL.
* If not already allocated, allocate a hardware PWM channel
* to the pin and set the duty cycle..
* Optionally invert the duty cycle [default = false]
* Optionally change the scale of the provided value to enable finer PWM duty control [default = 255]
*/
static void set_pwm_duty(const pin_t pin, const uint16_t v, const uint16_t=255, const bool=false) {
analogWrite(pin, v);
}
static void set_pwm_duty(const pin_t pin, const uint16_t v, const uint16_t v_size=255, const bool invert=false);

/**
* Allocate and set the frequency of a hardware PWM pin
* Returns -1 if no pin available.
*/
static int8_t set_pwm_frequency(const pin_t pin, const uint32_t f_desired);

};
18 changes: 8 additions & 10 deletions Marlin/src/HAL/ESP32/Servo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,26 @@
// so we only allocate servo channels up high to avoid side effects with regards to analogWrite (fans, leds, laser pwm etc.)
int Servo::channel_next_free = 12;

Servo::Servo() {
channel = channel_next_free++;
}
Servo::Servo() {}

int8_t Servo::attach(const int inPin) {
if (channel >= CHANNEL_MAX_NUM) return -1;
if (inPin > 0) pin = inPin;

ledcSetup(channel, 50, 16); // channel X, 50 Hz, 16-bit depth
ledcAttachPin(pin, channel);
return true;
channel = get_pwm_channel(pin, 50u, 16u);
return channel; // -1 if no PWM avail.
}

void Servo::detach() { ledcDetachPin(pin); }
// leave channel connected to servo - set duty to zero
void Servo::detach() {
if (channel >= 0) ledcWrite(channel, 0);
}

int Servo::read() { return degrees; }

void Servo::write(int inDegrees) {
degrees = constrain(inDegrees, MIN_ANGLE, MAX_ANGLE);
int us = map(degrees, MIN_ANGLE, MAX_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
int duty = map(us, 0, TAU_USEC, 0, MAX_COMPARE);
ledcWrite(channel, duty);
if (channel >= 0) ledcWrite(channel, duty); // don't save duty for servos!
}

void Servo::move(const int value) {
Expand Down
3 changes: 1 addition & 2 deletions Marlin/src/HAL/ESP32/Servo.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ class Servo {
MAX_PULSE_WIDTH = 2400, // Longest pulse sent to a servo
TAU_MSEC = 20,
TAU_USEC = (TAU_MSEC * 1000),
MAX_COMPARE = _BV(16) - 1, // 65535
CHANNEL_MAX_NUM = 16;
MAX_COMPARE = _BV(16) - 1; // 65535

public:
Servo();
Expand Down
8 changes: 6 additions & 2 deletions Marlin/src/HAL/ESP32/inc/SanityCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
#error "EMERGENCY_PARSER is not yet implemented for ESP32. Disable EMERGENCY_PARSER to continue."
#endif

#if ENABLED(FAST_PWM_FAN) || SPINDLE_LASER_FREQUENCY
#error "Features requiring Hardware PWM (FAST_PWM_FAN, SPINDLE_LASER_FREQUENCY) are not yet supported on ESP32."
#if (ENABLED(SPINDLE_LASER_USE_PWM) && SPINDLE_LASER_FREQUENCY > 78125) || (ENABLED(FAST_PWM_FAN_FREQUENCY) && FAST_PWM_FAN_FREQUENCY > 78125)
#error "SPINDLE_LASER_FREQUENCY and FAST_PWM_FREQUENCY maximum value is 78125Hz for ESP32."
#endif

#if HAS_TMC_SW_SERIAL
Expand All @@ -40,3 +40,7 @@
#if ENABLED(POSTMORTEM_DEBUGGING)
#error "POSTMORTEM_DEBUGGING is not yet supported on ESP32."
#endif

#if MB(MKS_TINYBEE) && ENABLED(FAST_PWM_FAN)
#error "FAST_PWM_FAN is not available on TinyBee."
#endif

0 comments on commit cc8e485

Please sign in to comment.