Skip to content

Commit 1f1571f

Browse files
authored
✨ ESP32 - Hardware PWM for fan, cutter, servos (MarlinFirmware#23802)
1 parent 9b2c060 commit 1f1571f

File tree

6 files changed

+116
-41
lines changed

6 files changed

+116
-41
lines changed

Marlin/Configuration_adv.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -3464,7 +3464,7 @@
34643464
#define SPINDLE_LASER_USE_PWM // Enable if your controller supports setting the speed/power
34653465
#if ENABLED(SPINDLE_LASER_USE_PWM)
34663466
#define SPINDLE_LASER_PWM_INVERT false // Set to "true" if the speed/power goes up when you want it to go slower
3467-
#define SPINDLE_LASER_FREQUENCY 2500 // (Hz) Spindle/laser frequency (only on supported HALs: AVR and LPC)
3467+
#define SPINDLE_LASER_FREQUENCY 2500 // (Hz) Spindle/laser frequency (only on supported HALs: AVR, ESP32 and LPC)
34683468
#endif
34693469

34703470
//#define AIR_EVACUATION // Cutter Vacuum / Laser Blower motor control with G-codes M10-M11

Marlin/src/HAL/ESP32/HAL.cpp

+82-19
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,16 @@ uint16_t MarlinHAL::adc_result;
7373
esp_adc_cal_characteristics_t characteristics[ADC_ATTEN_MAX];
7474
adc_atten_t attenuations[ADC1_CHANNEL_MAX] = {};
7575
uint32_t thresholds[ADC_ATTEN_MAX];
76-
volatile int numPWMUsed = 0,
77-
pwmPins[MAX_PWM_PINS],
78-
pwmValues[MAX_PWM_PINS];
76+
77+
volatile int numPWMUsed = 0;
78+
volatile struct { pin_t pin; int value; } pwmState[MAX_PWM_PINS];
79+
80+
pin_t chan_pin[CHANNEL_MAX_NUM + 1] = { 0 }; // PWM capable IOpins - not 0 or >33 on ESP32
81+
82+
struct {
83+
uint32_t freq; // ledcReadFreq doesn't work if a duty hasn't been set yet!
84+
uint16_t res;
85+
} pwmInfo[(CHANNEL_MAX_NUM + 1) / 2];
7986

8087
// ------------------------
8188
// Public functions
@@ -254,25 +261,81 @@ void MarlinHAL::adc_start(const pin_t pin) {
254261
adc1_set_attenuation(chan, atten);
255262
}
256263

257-
void analogWrite(pin_t pin, int value) {
258-
// Use ledc hardware for internal pins
259-
if (pin < 34) {
260-
static int cnt_channel = 1, pin_to_channel[40] = { 0 };
261-
if (pin_to_channel[pin] == 0) {
262-
ledcAttachPin(pin, cnt_channel);
263-
ledcSetup(cnt_channel, 490, 8);
264-
ledcWrite(cnt_channel, value);
265-
pin_to_channel[pin] = cnt_channel++;
264+
// ------------------------
265+
// PWM
266+
// ------------------------
267+
268+
int8_t channel_for_pin(const uint8_t pin) {
269+
for (int i = 0; i <= CHANNEL_MAX_NUM; i++)
270+
if (chan_pin[i] == pin) return i;
271+
return -1;
272+
}
273+
274+
// get PWM channel for pin - if none then attach a new one
275+
// return -1 if fail or invalid pin#, channel # (0-15) if success
276+
int8_t get_pwm_channel(const pin_t pin, const uint32_t freq, const uint16_t res) {
277+
if (!WITHIN(pin, 1, MAX_PWM_IOPIN)) return -1; // Not a hardware PWM pin!
278+
int8_t cid = channel_for_pin(pin);
279+
if (cid >= 0) return cid;
280+
281+
// Find an empty adjacent channel (same timer & freq/res)
282+
for (int i = 0; i <= CHANNEL_MAX_NUM; i++) {
283+
if (chan_pin[i] == 0) {
284+
if (chan_pin[i ^ 0x1] != 0) {
285+
if (pwmInfo[i / 2].freq == freq && pwmInfo[i / 2].res == res) {
286+
chan_pin[i] = pin; // Allocate PWM to this channel
287+
ledcAttachPin(pin, i);
288+
return i;
289+
}
290+
}
291+
else if (cid == -1) // Pair of empty channels?
292+
cid = i & 0xFE; // Save lower channel number
266293
}
267-
ledcWrite(pin_to_channel[pin], value);
294+
}
295+
// not attached, is an empty timer slot avail?
296+
if (cid >= 0) {
297+
chan_pin[cid] = pin;
298+
pwmInfo[cid / 2].freq = freq;
299+
pwmInfo[cid / 2].res = res;
300+
ledcSetup(cid, freq, res);
301+
ledcAttachPin(pin, cid);
302+
}
303+
return cid; // -1 if no channel avail
304+
}
305+
306+
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*/) {
307+
const int8_t cid = get_pwm_channel(pin, PWM_FREQUENCY, PWM_RESOLUTION);
308+
if (cid >= 0) {
309+
uint32_t duty = map(invert ? v_size - v : v, 0, v_size, 0, _BV(PWM_RESOLUTION)-1);
310+
ledcWrite(cid, duty);
311+
}
312+
}
313+
314+
int8_t MarlinHAL::set_pwm_frequency(const pin_t pin, const uint32_t f_desired) {
315+
const int8_t cid = channel_for_pin(pin);
316+
if (cid >= 0) {
317+
if (f_desired == ledcReadFreq(cid)) return cid; // no freq change
318+
ledcDetachPin(chan_pin[cid]);
319+
chan_pin[cid] = 0; // remove old freq channel
320+
}
321+
return get_pwm_channel(pin, f_desired, PWM_RESOLUTION); // try for new one
322+
}
323+
324+
// use hardware PWM if avail, if not then ISR
325+
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!
326+
// Use ledc hardware for internal pins
327+
const int8_t cid = get_pwm_channel(pin, freq, res);
328+
if (cid >= 0) {
329+
ledcWrite(cid, value); // set duty value
268330
return;
269331
}
270332

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

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

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

282345
// Take new slot for pin
283346
idx = numPWMUsed;
284-
pwmPins[idx] = pin;
347+
pwmState[idx].pin = pin;
285348
// Start timer on first use
286349
if (idx == 0) HAL_timer_start(MF_TIMER_PWM, PWM_TIMER_FREQUENCY);
287350

288351
++numPWMUsed;
289352
}
290353

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

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

301364
for (int i = 0; i < numPWMUsed; ++i) {
302365
if (count == 0) // Start of interval
303-
WRITE(pwmPins[i], pwmValues[i] ? HIGH : LOW);
304-
else if (pwmValues[i] == count) // End of duration
305-
WRITE(pwmPins[i], LOW);
366+
digitalWrite(pwmState[i].pin, pwmState[i].value ? HIGH : LOW);
367+
else if (pwmState[i].value == count) // End of duration
368+
digitalWrite(pwmState[i].pin, LOW);
306369
}
307370

308371
// 128 for 7 Bit resolution

Marlin/src/HAL/ESP32/HAL.h

+18-7
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464
#define CRITICAL_SECTION_START() portENTER_CRITICAL(&spinlock)
6565
#define CRITICAL_SECTION_END() portEXIT_CRITICAL(&spinlock)
6666

67+
#define HAL_CAN_SET_PWM_FREQ // This HAL supports PWM Frequency adjustment
68+
#define PWM_FREQUENCY 1000u // Default PWM frequency when set_pwm_duty() is called without set_pwm_frequency()
69+
#define PWM_RESOLUTION 10u // Default PWM bit resolution
70+
#define CHANNEL_MAX_NUM 15u // max PWM channel # to allocate (7 to only use low speed, 15 to use low & high)
71+
#define MAX_PWM_IOPIN 33u // hardware pwm pins < 34
72+
6773
// ------------------------
6874
// Types
6975
// ------------------------
@@ -83,7 +89,7 @@ typedef Servo hal_servo_t;
8389
void tone(const pin_t _pin, const unsigned int frequency, const unsigned long duration=0);
8490
void noTone(const pin_t _pin);
8591

86-
void analogWrite(pin_t pin, int value);
92+
void analogWrite(const pin_t pin, const uint16_t value, const uint32_t freq=PWM_FREQUENCY, const uint16_t res=8);
8793

8894
//
8995
// Pin Mapping for M42, M43, M226
@@ -209,12 +215,17 @@ class MarlinHAL {
209215
static uint16_t adc_value() { return adc_result; }
210216

211217
/**
212-
* Set the PWM duty cycle for the pin to the given value.
213-
* No inverting the duty cycle in this HAL.
214-
* No changing the maximum size of the provided value to enable finer PWM duty control in this HAL.
218+
* If not already allocated, allocate a hardware PWM channel
219+
* to the pin and set the duty cycle..
220+
* Optionally invert the duty cycle [default = false]
221+
* Optionally change the scale of the provided value to enable finer PWM duty control [default = 255]
215222
*/
216-
static void set_pwm_duty(const pin_t pin, const uint16_t v, const uint16_t=255, const bool=false) {
217-
analogWrite(pin, v);
218-
}
223+
static void set_pwm_duty(const pin_t pin, const uint16_t v, const uint16_t v_size=255, const bool invert=false);
224+
225+
/**
226+
* Allocate and set the frequency of a hardware PWM pin
227+
* Returns -1 if no pin available.
228+
*/
229+
static int8_t set_pwm_frequency(const pin_t pin, const uint32_t f_desired);
219230

220231
};

Marlin/src/HAL/ESP32/Servo.cpp

+8-10
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,26 @@
3131
// so we only allocate servo channels up high to avoid side effects with regards to analogWrite (fans, leds, laser pwm etc.)
3232
int Servo::channel_next_free = 12;
3333

34-
Servo::Servo() {
35-
channel = channel_next_free++;
36-
}
34+
Servo::Servo() {}
3735

3836
int8_t Servo::attach(const int inPin) {
39-
if (channel >= CHANNEL_MAX_NUM) return -1;
4037
if (inPin > 0) pin = inPin;
41-
42-
ledcSetup(channel, 50, 16); // channel X, 50 Hz, 16-bit depth
43-
ledcAttachPin(pin, channel);
44-
return true;
38+
channel = get_pwm_channel(pin, 50u, 16u);
39+
return channel; // -1 if no PWM avail.
4540
}
4641

47-
void Servo::detach() { ledcDetachPin(pin); }
42+
// leave channel connected to servo - set duty to zero
43+
void Servo::detach() {
44+
if (channel >= 0) ledcWrite(channel, 0);
45+
}
4846

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

5149
void Servo::write(int inDegrees) {
5250
degrees = constrain(inDegrees, MIN_ANGLE, MAX_ANGLE);
5351
int us = map(degrees, MIN_ANGLE, MAX_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
5452
int duty = map(us, 0, TAU_USEC, 0, MAX_COMPARE);
55-
ledcWrite(channel, duty);
53+
if (channel >= 0) ledcWrite(channel, duty); // don't save duty for servos!
5654
}
5755

5856
void Servo::move(const int value) {

Marlin/src/HAL/ESP32/Servo.h

+1-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ class Servo {
3030
MAX_PULSE_WIDTH = 2400, // Longest pulse sent to a servo
3131
TAU_MSEC = 20,
3232
TAU_USEC = (TAU_MSEC * 1000),
33-
MAX_COMPARE = _BV(16) - 1, // 65535
34-
CHANNEL_MAX_NUM = 16;
33+
MAX_COMPARE = _BV(16) - 1; // 65535
3534

3635
public:
3736
Servo();

Marlin/src/HAL/ESP32/inc/SanityCheck.h

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
#error "EMERGENCY_PARSER is not yet implemented for ESP32. Disable EMERGENCY_PARSER to continue."
2626
#endif
2727

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

3232
#if HAS_TMC_SW_SERIAL
@@ -40,3 +40,7 @@
4040
#if ENABLED(POSTMORTEM_DEBUGGING)
4141
#error "POSTMORTEM_DEBUGGING is not yet supported on ESP32."
4242
#endif
43+
44+
#if MB(MKS_TINYBEE) && ENABLED(FAST_PWM_FAN)
45+
#error "FAST_PWM_FAN is not available on TinyBee."
46+
#endif

0 commit comments

Comments
 (0)