
Основы ESP32: Генерация ШИМ-сигнала на ESP32
Если вы раньше работали с Arduino, вам знакомо, как просто сгенерировать ШИМ-сигнал с помощью функции analogWrite() — достаточно указать пин и коэффициент заполнения, и всё готово.
Но с ESP32 всё как в игре на чуть более сложном уровне. Мы получаем больше элементов управления (ура!), но нам также нужно грамотно ими управлять (что немного сложнее). ESP32 требует от нас точного указания нескольких дополнительных параметров, таких как частота ШИМ, разрешение ШИМ, используемый канал и, конечно же, коэффициент заполнения и номер пина. Ого, звучит как много, но не переживайте!
Это руководство научит вас всему, что нужно знать о ШИМ на ESP32 — от основных концепций до практических примеров.
Выводы ШИМ ESP32
На ESP32 ШИМ-выход возможен на всех GPIO-выводах, за исключением четырёх выводов, работающих только на вход. Выделенные ниже GPIO поддерживают ШИМ.
ШИМ на ESP32
ESP32 имеет два периферийных устройства ШИМ: периферийное устройство управления светодиодами (LEDC) и периферийное устройство ШИМ для управления двигателями (MCPWM).
Периферийное устройство MCPWM предназначено для управления двигателями и включает дополнительные функции, такие как мёртвая зона и автоматическое торможение. С другой стороны, периферийное устройство LEDC специально разработано для управления светодиодами и включает такие функции, как автоматическое затухание, а также более продвинутые возможности. Тем не менее, его можно использовать для генерации ШИМ-сигналов для множества других целей.
В этом руководстве мы сосредоточимся в основном на периферийном устройстве LEDC.
Частота ШИМ
Периферийное устройство LEDC, как и большинство ШИМ-контроллеров, использует таймер для генерации ШИМ-сигналов.
Представьте таймер как «тикающий» механизм, считающий вверх до максимального числа, после чего он сбрасывается в ноль и начинается следующий цикл счёта. Время между этими сбросами (то есть сколько времени требуется для достижения максимального значения) представляет собой частоту ШИМ и измеряется в герцах (Гц). Например, если мы зададим частоту 1 Гц, таймер потратит 1 секунду на счёт от 0 до максимального значения перед началом следующего цикла. Если мы зададим частоту 1000 Гц, таймеру потребуется всего 1 миллисекунда для счёта от 0 до максимального значения.
ESP32 может генерировать ШИМ-сигнал с частотой до 40 МГц.
Разрешение ШИМ
Итак, что же представляет собой это «максимальное» значение? «Максимальное» значение определяется разрешением ШИМ. Если разрешение ШИМ составляет «n» бит, таймер считает от 0 до 2n-1 перед сбросом. Например, если мы настроим таймер с частотой 1 Гц и разрешением 8 бит, таймер потратит 1 секунду на счёт от 0 до 255 (28). В случае частоты 1 Гц и разрешения 16 бит таймер всё ещё потратит 1 секунду, но будет считать от 0 до 65 535 (216).
Важно понимать, что при более высоком разрешении у нас по сути больше «тактов таймера» в пределах одного и того же заданного периода времени. Таким образом, мы получаем большую гранулярность в таймингах.
Разрешение ШИМ ESP32 может регулироваться от 1 до 16 бит. Это означает, что коэффициент заполнения может быть задан на уровне до 65 536 (216) различных уровней. Это даёт вам тонкий контроль над такими вещами, как светодиоды, позволяя им светиться с тонкими вариациями яркости, или двигателями, позволяя им работать на очень точных скоростях.
Коэффициент заполнения
Далее мы определяем коэффициент заполнения ШИМ-выхода. Коэффициент заполнения указывает, сколько тактов таймера ШИМ-выход будет оставаться в высоком состоянии перед переходом в низкое. Это значение хранится в регистре захвата/сравнения (CCR) таймера.
Когда таймер сбрасывается, ШИМ-выход переходит в высокое состояние. Когда таймер достигает значения, хранящегося в регистре захвата/сравнения, ШИМ-выход переходит в низкое состояние. Однако таймер продолжает считать. Как только таймер достигает максимального значения, ШИМ-выход снова переходит в высокое состояние, и таймер сбрасывается для начала счёта следующего периода.
Например, представим, что мы хотим сгенерировать ШИМ-сигнал с частотой 1000 Гц, 8-битным разрешением и коэффициентом заполнения 75%. При 8-битном разрешении максимальное значение таймера составит 255 (28-1). С частотой 1000 Гц таймеру потребуется 1 мс (0,001 с) для счёта от 0 до 255. Коэффициент заполнения ШИМ установлен на 75%, что означает, что значение 256 * 75% = 192 будет записано в регистр захвата/сравнения. В этом случае, когда таймер сбрасывается, ШИМ-выход будет установлен в высокое состояние. ШИМ-выход останется высоким, пока счётчик не достигнет 192, после чего он переключится в низкое состояние. Как только таймер достигнет 255, ШИМ-выход переключится обратно в высокое состояние, и таймер сбросится для начала счёта следующего периода.
Каналы ШИМ
Теперь обратим внимание на концепцию канала. Канал представляет собой уникальный выходной ШИМ-сигнал.
ESP32 имеет 16 каналов, что означает, что он может генерировать 16 уникальных ШИМ-сигналов. Эти каналы разделены на две группы, каждая из которых содержит 8 каналов: 8 высокоскоростных каналов и 8 низкоскоростных каналов.
Высокоскоростные каналы реализованы аппаратно и поэтому способны обеспечивать автоматические и безглючные изменения коэффициента заполнения ШИМ. Низкоскоростные каналы, напротив, лишены этих возможностей и полагаются на программное обеспечение для изменения коэффициента заполнения.
Внутри каждой группы 4 таймера разделены между 8 каналами, что означает, что каждые два канала используют один и тот же таймер. Поскольку таймер определяет частоту, важно понимать, что мы не можем настраивать частоту каждого канала в паре независимо. Однако мы можем управлять коэффициентом заполнения ШИМ каждого канала независимо.
Подводя итог, ESP32 имеет 16 каналов ШИМ, которые могут работать на восьми различных частотах, и каждый из этих каналов может работать с разным коэффициентом заполнения.
Для генерации ШИМ-сигнала на определённом пине вы «привязываете» этот пин к каналу. Эта связь указывает ESP32 выводить ШИМ-сигнал, генерируемый каналом, на указанный пин. К одному каналу можно привязать несколько пинов, что означает, что все они могут выводить один и тот же ШИМ-сигнал. Несмотря на то, что все GPIO-выводы поддерживают ШИМ-выход, ESP32 имеет только 16 каналов, поэтому одновременно может быть создано только 16 различных ШИМ-сигналов. Это не ограничивает количество пинов, которые могут выводить ШИМ-сигналы, но ограничивает разнообразие сигналов, которые могут выводиться одновременно.
С практической точки зрения, если у вас есть набор светодиодов, которые вы хотите мигать в идеальной синхронности, вы можете настроить один канал с определённой частотой и коэффициентом заполнения, а затем привязать все соответствующие пины (которые подключены к светодиодам) к этому каналу. Однако при работе с сервоприводами, особенно в таких ситуациях, как роботизированная рука, где каждый сустав (серво) должен управляться независимо, становится выгодным назначать разные пины разным каналам.
Выбор частоты и разрешения ШИМ
ESP32 может генерировать ШИМ-сигнал с частотой до 40 МГц, а разрешение ШИМ может регулироваться от 1 до 16 бит. Но это не означает, что вы можете установить частоту 40 МГц и разрешение 16 бит одновременно. Это связано с тем, что максимальная частота ШИМ и разрешение ограничены источником тактового сигнала.
Чтобы проиллюстрировать это, рассмотрим тактовый генератор (будь то тактовый сигнал процессора или таймера — не имеет значения), работающий на частоте 40 МГц. В этом случае максимально достижимая частота ШИМ также составляет 40 МГц. Мы не можем генерировать ШИМ-сигнал быстрее, чем позволяет наш тактовый генератор.
А что насчёт разрешения? Разрешение — это, по сути, то, насколько тонко мы можем разделить один период ШИМ-сигнала на различные коэффициенты заполнения. И вот ключевое понимание: разделение ШИМ-сигнала требует тактового сигнала процессора, работающего на частоте PWM_freq * 2PWM_resolution. Почему? Потому что для создания этих коэффициентов заполнения нужно иметь возможность создавать эти временные интервалы.
Из этого становятся ясны два важных момента:
PWM_freq * 2PWM_resolution не может превышать тактовую частоту.
Частота ШИМ и разрешение взаимозависимы. Чем выше частота ШИМ, тем ниже разрешение коэффициента заполнения (и наоборот).
Согласно документации Espressif, источник тактового сигнала низкоскоростного таймера LEDC — это тактовый генератор APB на 80 МГц. В качестве общего ориентира следует стремиться к тому, чтобы PWM_freq * 2PWM_resolution было меньше 80 МГц.
Кроме того, документация Espressif включает примеры, подтверждающие это:
Частота ШИМ 5 кГц может иметь максимальное разрешение коэффициента заполнения 13 бит, что даёт разрешение ~0,012%, или 213=8192 дискретных уровня.
Частота ШИМ 20 МГц может иметь максимальное разрешение коэффициента заполнения 2 бита, что даёт разрешение 25%, или 22=4 дискретных уровня.
Частота ШИМ 40 МГц может иметь разрешение коэффициента заполнения всего 1 бит, что означает, что коэффициент заполнения остаётся фиксированным на 50% и не может быть изменён.
Если всё вышесказанное вам непонятно, учтите следующее: Arduino Uno обеспечивает ШИМ-сигнал ~490 Гц при 8 битах. Этого более чем достаточно для плавного затухания светодиода. Так что вы всегда можете начать с этого (частота 500 Гц, 8-битное разрешение), а затем экспериментировать.
Генерация ШИМ-сигнала с помощью библиотеки LEDC
Перейдём к делу! Ядро ESP32 Arduino включает библиотеку LEDC, которая упрощает управление широтно-импульсной модуляцией (ШИМ) на ESP32. Хотя библиотека LEDC была разработана для управления светодиодами, она также может использоваться для других приложений, где полезны ШИМ-сигналы, таких как воспроизведение «музыки» через пьезоизлучатели и управление двигателями.
Приведённые ниже шаги показывают, как использовать библиотеку LEDC для генерации ШИМ-сигнала на ESP32 с помощью Arduino IDE.
Выберите канал ШИМ: Доступны 16 каналов на выбор, пронумерованных от 0 до 15.
Определите частоту ШИМ: Она может достигать 40 МГц, но для нашего примера затухания светодиода частоты 500 Гц будет достаточно.
Определите разрешение ШИМ: Оно варьируется от 1 до 16 бит. Количество дискретных уровней коэффициента заполнения определяется как 2resolution. Например, установка разрешения на 8 бит даёт 256 дискретных уровней коэффициента заполнения [0–255]. С другой стороны, разрешение 16 бит обеспечивает 65 536 дискретных уровней коэффициента заполнения [0–65535].
Выберите GPIO-выводы: Выберите один или несколько GPIO-выводов на ESP32 для вывода ШИМ-сигнала.
Привяжите вывод(ы) к каналу ШИМ: Привяжите выбранный вывод(ы) к выбранному каналу с заданной частотой и разрешением с помощью функции ledcAttachChannel(pin, freq, resolution, channel).
Установите коэффициент заполнения: Наконец, установите фактическое значение коэффициента заполнения для данного канала с помощью функции ledcWriteChannel(channel, dutycycle).
Пример 1 — Затухание светодиода
Вот быстрый пример скетча, показывающий, как плавно изменять яркость светодиода — отличный способ продемонстрировать генерацию ШИМ на ESP32.
Подключение
Подключение довольно простое. Возьмите светодиод и токоограничивающий резистор на 330 Ом и установите их на макетную плату, как показано на рисунке ниже. Подключите более длинную ножку светодиода — анод — к пину GP18 через резистор 330 Ом, а более короткую ножку — к выводу GND на вашем ESP32.
Код
Скопируйте приведённый ниже код в вашу Arduino IDE.
const int PWM_CHANNEL = 0; // ESP32 has 16 channels which can generate 16 independent waveforms
const int PWM_FREQ = 500; // Recall that Arduino Uno is ~490 Hz. Official ESP32 example uses 5,000Hz
const int PWM_RESOLUTION = 8; // We'll use same resolution as Uno (8 bits, 0-255) but ESP32 can go up to 16 bits
// The max duty cycle value based on PWM resolution (will be 255 if resolution is 8 bits)
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
const int LED_OUTPUT_PIN = 18;
const int DELAY_MS = 4; // delay between fade increments
void setup() {
// Attach the GPIO to the Channel
ledcAttachChannel(LED_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
}
void loop() {
// fade up PWM on given channel
for (int dutyCycle = 0; dutyCycle <= MAX_DUTY_CYCLE; dutyCycle++) {
ledcWriteChannel(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
// fade down PWM on given channel
for (int dutyCycle = MAX_DUTY_CYCLE; dutyCycle >= 0; dutyCycle--) {
ledcWriteChannel(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
}
Проверка примера
Теперь загрузите код на ваш ESP32. Вы увидите, как яркость светодиода плавно изменяется от полностью выключенного до максимальной яркости и обратно.
Объяснение кода
В начале скетча определены несколько констант для настройки параметров ШИМ. Сначала определена константа PWM_CHANNEL, установленная в 0. ESP32 имеет 16 каналов (от 0 до 15), и каждый может генерировать независимые сигналы.
Затем определена PWM_FREQ, установленная в 500. Это частота нашего ШИМ-сигнала. Напомним, что Arduino Uno использует ~490 Гц. Этого достаточно для плавного затухания светодиода.
Далее PWM_RESOLUTION установлено в 8. Это разрешение (в битах) ШИМ-сигнала. Хотя мы используем 8 бит (как Arduino Uno), ESP32 может поддерживать до 16 бит.
const int PWM_CHANNEL = 0;
const int PWM_FREQ = 500;
const int PWM_RESOLUTION = 8;
После этого MAX_DUTY_CYCLE вычисляется по формуле 2PWM_RESOLUTION−1. Это значение определяет максимально достижимый коэффициент заполнения на основе выбранного разрешения.
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
Далее LED_OUTPUT_PIN установлен в 18. Это GPIO-вывод ESP32, к которому подключён светодиод.
И наконец, DELAY_MS определён и установлен в 4. Это задержка (в миллисекундах) между приращениями для управления скоростью затухания светодиода.
const int LED_OUTPUT_PIN = 18;
const int DELAY_MS = 4;
В функции setup вызывается функция ledcAttachChannel() для привязки GPIO-вывода к каналу ШИМ, ответственному за генерацию ШИМ-сигнала с выбранной частотой и разрешением.
ledcAttachChannel(LED_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
В цикле loop первый цикл for последовательно увеличивает коэффициент заполнения от 0 до его максимально возможного значения (MAX_DUTY_CYCLE). Это постепенно увеличивает яркость светодиода.
for(int dutyCycle = 0; dutyCycle <= MAX_DUTY_CYCLE; dutyCycle++){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
Второй цикл for уменьшает коэффициент заполнения от MAX_DUTY_CYCLE до 0: это постепенно уменьшает яркость светодиода.
for(int dutyCycle = MAX_DUTY_CYCLE; dutyCycle >= 0; dutyCycle--){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
В обоих циклах for функция ledcWriteChannel() используется для установки яркости светодиода. Эта функция принимает в качестве аргументов канал, генерирующий сигнал, и коэффициент заполнения.
ledcWriteChannel(ledChannel, dutyCycle);
Пример 2 — Один ШИМ-сигнал на нескольких GPIO
Вы можете получить один и тот же ШИМ-сигнал на нескольких GPIO одновременно. Для этого достаточно привязать эти GPIO к одному и тому же каналу.
Подключение
Добавьте ещё два светодиода в вашу схему так же, как и первый. Подключите их к GPIO 19 и 21.
На изображении ниже показано, как всё подключить.
Код
Теперь давайте модифицируем предыдущий пример для затухания трёх светодиодов с использованием одного и того же ШИМ-сигнала от одного канала.
const int PWM_CHANNEL = 0; // ESP32 has 16 channels which can generate 16 independent waveforms
const int PWM_FREQ = 500; // Recall that Arduino Uno is ~490 Hz. Official ESP32 example uses 5,000Hz
const int PWM_RESOLUTION = 8; // We'll use same resolution as Uno (8 bits, 0-255) but ESP32 can go up to 16 bits
// The max duty cycle value based on PWM resolution (will be 255 if resolution is 8 bits)
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
const int LED_1_OUTPUT_PIN = 18;
const int LED_2_OUTPUT_PIN = 19;
const int LED_3_OUTPUT_PIN = 21;
const int DELAY_MS = 4; // delay between fade increments
void setup() {
// Attach the GPIOs to the same Channel
ledcAttachChannel(LED_1_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
ledcAttachChannel(LED_2_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
ledcAttachChannel(LED_3_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
}
void loop() {
// fade up PWM on given channel
for (int dutyCycle = 0; dutyCycle <= MAX_DUTY_CYCLE; dutyCycle++) {
ledcWriteChannel(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
// fade down PWM on given channel
for (int dutyCycle = MAX_DUTY_CYCLE; dutyCycle >= 0; dutyCycle--) {
ledcWriteChannel(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
}
Проверка примера
Теперь загрузите код на ваш ESP32. Вы увидите, как все три светодиода затухают одновременно, потому что все GPIO выводят один и тот же ШИМ-сигнал.
Объяснение кода
Если вы сравните этот скетч с первым, вы заметите, что они очень похожи, с лишь несколькими отличиями. Давайте рассмотрим эти отличия.
В глобальной области определены три дополнительные константы с именами LED_1_OUTPUT_PIN, LED_2_OUTPUT_PIN и LED_3_OUTPUT_PIN, установленные в 18, 19 и 21 соответственно. Это указывает на то, что мы работаем с тремя отдельными светодиодами, каждый из которых подключён к своему GPIO-выводу на ESP32.
const int LED_1_OUTPUT_PIN = 18;
const int LED_2_OUTPUT_PIN = 19;
const int LED_3_OUTPUT_PIN = 21;
Затем в функции setup функция ledcAttachChannel() вызывается три раза вместо одного, как в предыдущем коде. Каждый вызов функции привязывает другой GPIO-вывод (LED_1_OUTPUT_PIN, LED_2_OUTPUT_PIN, LED_3_OUTPUT_PIN) к одному и тому же каналу ШИМ (PWM_CHANNEL), что означает, что один и тот же ШИМ-сигнал будет выводиться на все три светодиода.
ledcAttachChannel(LED_1_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
ledcAttachChannel(LED_2_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
ledcAttachChannel(LED_3_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
Обратите внимание, что несмотря на добавление новых светодиодов, функция loop() не изменилась. Это связано с тем, что все светодиоды управляются одним и тем же каналом ШИМ.
Пример 3 — Затухание светодиода с помощью потенциометра
Этот пример скетча показывает, как управлять яркостью светодиода с помощью потенциометра.
Подключение
Уберите два дополнительных светодиода из вашей схемы и добавьте потенциометр. Подключите один крайний вывод потенциометра к 3.3V, противоположный крайний вывод к GND, а его средний вывод (движок) к GPIO 34.
На изображении ниже показано, как всё подключить.
Код
const int PWM_CHANNEL = 0; // ESP32 has 16 channels which can generate 16 independent waveforms
const int PWM_FREQ = 500; // Recall that Arduino Uno is ~490 Hz. Official ESP32 example uses 5,000Hz
const int PWM_RESOLUTION = 8; // We'll use same resolution as Uno (8 bits, 0-255) but ESP32 can go up to 16 bits
// The max duty cycle value based on PWM resolution (will be 255 if resolution is 8 bits)
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
const int LED_OUTPUT_PIN = 18;
const int POT_PIN = 34;
const int DELAY_MS = 100; // delay between fade increments
void setup() {
// Attach the GPIO to the Channel
ledcAttachChannel(LED_OUTPUT_PIN, PWM_FREQ, PWM_RESOLUTION, PWM_CHANNEL);
}
void loop() {
int dutyCycle = map(analogRead(POT_PIN), 0, 4095, 0, MAX_DUTY_CYCLE);
ledcWriteChannel(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
Проверка примера
Теперь попробуйте повернуть потенциометр до упора в одну сторону, а затем до упора в другую. Наблюдайте за светодиодом; на этот раз вы увидите, как яркость светодиода плавно изменяется от полностью выключенного на одном конце диапазона ручки потенциометра до максимальной яркости на другом.
Объяснение кода
Снова! Между этим скетчем и первым есть лишь несколько отличий. Давайте рассмотрим эти отличия.
В глобальной области определена дополнительная константа с именем POT_PIN. Она установлена в 34, что указывает на то, что потенциометр подключён к GPIO 34 на ESP32 и будет использоваться для динамического определения коэффициента заполнения и, следовательно, яркости светодиода.
const int POT_PIN = 34;
Затем в цикле loop, вместо использования циклов for для постепенного увеличения и уменьшения яркости светодиода, вызывается функция analogRead(POT_PIN), которая считывает сырое значение с потенциометра.
analogRead(POT_PIN);
Показание потенциометра, которое находится в диапазоне от 0 до 4095, затем преобразуется в новый диапазон от 0 до MAX_DUTY_CYCLE с помощью функции map(). Это преобразование согласовывает значения потенциометра с допустимыми значениями коэффициента заполнения ШИМ-сигнала. Это гарантирует, что яркость светодиода может изменяться по всему диапазону.
int dutyCycle = map(analogRead(POT_PIN), 0, 4095, 0, MAX_DUTY_CYCLE);
Наконец, функция ledcWriteChannel() принимает это преобразованное значение и напрямую применяет его к ШИМ-сигналу, регулируя яркость светодиода в реальном времени в зависимости от положения потенциометра.
ledcWriteChannel(PWM_CHANNEL, dutyCycle);