ESP32 - Hardware PWM for fan, cutter, servos (#23802)

This commit is contained in:
John Robertson 2022-03-18 03:21:53 +00:00 committed by Scott Lahteine
parent c7a2ecc31a
commit cc8e485e1c
6 changed files with 116 additions and 41 deletions

View file

@ -3464,7 +3464,7 @@
#define SPINDLE_LASER_USE_PWM // Enable if your controller supports setting the speed/power #define SPINDLE_LASER_USE_PWM // Enable if your controller supports setting the speed/power
#if ENABLED(SPINDLE_LASER_USE_PWM) #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_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 #endif
//#define AIR_EVACUATION // Cutter Vacuum / Laser Blower motor control with G-codes M10-M11 //#define AIR_EVACUATION // Cutter Vacuum / Laser Blower motor control with G-codes M10-M11

View file

@ -73,9 +73,16 @@ uint16_t MarlinHAL::adc_result;
esp_adc_cal_characteristics_t characteristics[ADC_ATTEN_MAX]; esp_adc_cal_characteristics_t characteristics[ADC_ATTEN_MAX];
adc_atten_t attenuations[ADC1_CHANNEL_MAX] = {}; adc_atten_t attenuations[ADC1_CHANNEL_MAX] = {};
uint32_t thresholds[ADC_ATTEN_MAX]; uint32_t thresholds[ADC_ATTEN_MAX];
volatile int numPWMUsed = 0,
pwmPins[MAX_PWM_PINS], volatile int numPWMUsed = 0;
pwmValues[MAX_PWM_PINS]; 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 // Public functions
@ -254,25 +261,81 @@ void MarlinHAL::adc_start(const pin_t pin) {
adc1_set_attenuation(chan, atten); adc1_set_attenuation(chan, atten);
} }
void analogWrite(pin_t pin, int value) { // ------------------------
// Use ledc hardware for internal pins // PWM
if (pin < 34) { // ------------------------
static int cnt_channel = 1, pin_to_channel[40] = { 0 };
if (pin_to_channel[pin] == 0) { int8_t channel_for_pin(const uint8_t pin) {
ledcAttachPin(pin, cnt_channel); for (int i = 0; i <= CHANNEL_MAX_NUM; i++)
ledcSetup(cnt_channel, 490, 8); if (chan_pin[i] == pin) return i;
ledcWrite(cnt_channel, value); return -1;
pin_to_channel[pin] = cnt_channel++; }
// 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;
} }
ledcWrite(pin_to_channel[pin], value); }
else if (cid == -1) // Pair of empty channels?
cid = i & 0xFE; // Save lower channel number
}
}
// 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; return;
} }
// not a hardware PWM pin OR no PWM channels available
int idx = -1; int idx = -1;
// Search Pin // Search Pin
for (int i = 0; i < numPWMUsed; ++i) for (int i = 0; i < numPWMUsed; ++i)
if (pwmPins[i] == pin) { idx = i; break; } if (pwmState[i].pin == pin) { idx = i; break; }
// not found ? // not found ?
if (idx < 0) { if (idx < 0) {
@ -281,7 +344,7 @@ void analogWrite(pin_t pin, int value) {
// Take new slot for pin // Take new slot for pin
idx = numPWMUsed; idx = numPWMUsed;
pwmPins[idx] = pin; pwmState[idx].pin = pin;
// Start timer on first use // Start timer on first use
if (idx == 0) HAL_timer_start(MF_TIMER_PWM, PWM_TIMER_FREQUENCY); if (idx == 0) HAL_timer_start(MF_TIMER_PWM, PWM_TIMER_FREQUENCY);
@ -289,7 +352,7 @@ void analogWrite(pin_t pin, int value) {
} }
// Use 7bit internal value - add 1 to have 100% high at 255 // 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 // Handle PWM timer interrupt
@ -300,9 +363,9 @@ HAL_PWM_TIMER_ISR() {
for (int i = 0; i < numPWMUsed; ++i) { for (int i = 0; i < numPWMUsed; ++i) {
if (count == 0) // Start of interval if (count == 0) // Start of interval
WRITE(pwmPins[i], pwmValues[i] ? HIGH : LOW); digitalWrite(pwmState[i].pin, pwmState[i].value ? HIGH : LOW);
else if (pwmValues[i] == count) // End of duration else if (pwmState[i].value == count) // End of duration
WRITE(pwmPins[i], LOW); digitalWrite(pwmState[i].pin, LOW);
} }
// 128 for 7 Bit resolution // 128 for 7 Bit resolution

View file

@ -64,6 +64,12 @@
#define CRITICAL_SECTION_START() portENTER_CRITICAL(&spinlock) #define CRITICAL_SECTION_START() portENTER_CRITICAL(&spinlock)
#define CRITICAL_SECTION_END() portEXIT_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 // Types
// ------------------------ // ------------------------
@ -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 tone(const pin_t _pin, const unsigned int frequency, const unsigned long duration=0);
void noTone(const pin_t _pin); 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 // Pin Mapping for M42, M43, M226
@ -209,12 +215,17 @@ public:
static uint16_t adc_value() { return adc_result; } static uint16_t adc_value() { return adc_result; }
/** /**
* Set the PWM duty cycle for the pin to the given value. * If not already allocated, allocate a hardware PWM channel
* No inverting the duty cycle in this HAL. * to the pin and set the duty cycle..
* No changing the maximum size of the provided value to enable finer PWM duty control in this HAL. * 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) { static void set_pwm_duty(const pin_t pin, const uint16_t v, const uint16_t v_size=255, const bool invert=false);
analogWrite(pin, v);
} /**
* 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);
}; };

View file

@ -31,20 +31,18 @@
// so we only allocate servo channels up high to avoid side effects with regards to analogWrite (fans, leds, laser pwm etc.) // 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; int Servo::channel_next_free = 12;
Servo::Servo() { Servo::Servo() {}
channel = channel_next_free++;
}
int8_t Servo::attach(const int inPin) { int8_t Servo::attach(const int inPin) {
if (channel >= CHANNEL_MAX_NUM) return -1;
if (inPin > 0) pin = inPin; if (inPin > 0) pin = inPin;
channel = get_pwm_channel(pin, 50u, 16u);
ledcSetup(channel, 50, 16); // channel X, 50 Hz, 16-bit depth return channel; // -1 if no PWM avail.
ledcAttachPin(pin, channel);
return true;
} }
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; } int Servo::read() { return degrees; }
@ -52,7 +50,7 @@ void Servo::write(int inDegrees) {
degrees = constrain(inDegrees, MIN_ANGLE, MAX_ANGLE); degrees = constrain(inDegrees, MIN_ANGLE, MAX_ANGLE);
int us = map(degrees, MIN_ANGLE, MAX_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH); int us = map(degrees, MIN_ANGLE, MAX_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
int duty = map(us, 0, TAU_USEC, 0, MAX_COMPARE); 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) { void Servo::move(const int value) {

View file

@ -30,8 +30,7 @@ class Servo {
MAX_PULSE_WIDTH = 2400, // Longest pulse sent to a servo MAX_PULSE_WIDTH = 2400, // Longest pulse sent to a servo
TAU_MSEC = 20, TAU_MSEC = 20,
TAU_USEC = (TAU_MSEC * 1000), TAU_USEC = (TAU_MSEC * 1000),
MAX_COMPARE = _BV(16) - 1, // 65535 MAX_COMPARE = _BV(16) - 1; // 65535
CHANNEL_MAX_NUM = 16;
public: public:
Servo(); Servo();

View file

@ -25,8 +25,8 @@
#error "EMERGENCY_PARSER is not yet implemented for ESP32. Disable EMERGENCY_PARSER to continue." #error "EMERGENCY_PARSER is not yet implemented for ESP32. Disable EMERGENCY_PARSER to continue."
#endif #endif
#if ENABLED(FAST_PWM_FAN) || SPINDLE_LASER_FREQUENCY #if (ENABLED(SPINDLE_LASER_USE_PWM) && SPINDLE_LASER_FREQUENCY > 78125) || (ENABLED(FAST_PWM_FAN_FREQUENCY) && FAST_PWM_FAN_FREQUENCY > 78125)
#error "Features requiring Hardware PWM (FAST_PWM_FAN, SPINDLE_LASER_FREQUENCY) are not yet supported on ESP32." #error "SPINDLE_LASER_FREQUENCY and FAST_PWM_FREQUENCY maximum value is 78125Hz for ESP32."
#endif #endif
#if HAS_TMC_SW_SERIAL #if HAS_TMC_SW_SERIAL
@ -40,3 +40,7 @@
#if ENABLED(POSTMORTEM_DEBUGGING) #if ENABLED(POSTMORTEM_DEBUGGING)
#error "POSTMORTEM_DEBUGGING is not yet supported on ESP32." #error "POSTMORTEM_DEBUGGING is not yet supported on ESP32."
#endif #endif
#if MB(MKS_TINYBEE) && ENABLED(FAST_PWM_FAN)
#error "FAST_PWM_FAN is not available on TinyBee."
#endif