Если вы пытаетесь управлять яркостью светодиода, скоростью мотора или подать аналоговый сигнал с микроконтроллера без аппаратного PWM — вы упрётесь в одну и ту же проблему. Простой digitalWrite в цикле даёт частоту около 100–500 Гц, мотор пищит, светодиод мерцает на камере, а после RC-фильтра вы получаете не ровную линию, а зубчатую пилу. Реальная задача — получить чистый soft-PWM на 20 кГц и выше, при этом не убив процессорное время и не получив дрожащие фронты. Разберёмся, как это сделать правильно.
- Почему именно 20 кГц и что пойдёт не так
- Три подхода к реализации — и какой когда использовать
- Способ 1: Таймер с прерыванием — рабочий вариант для AVR
- Способ 2: Аппаратный PWM на высокой частоте — если ножка позволяет
- Способ 3: Прямой цикл — когда больше ничего не нужно
- RC-фильтр: почему после PWM не получается ровный сигнал
- Частые ошибки, которые убивают сигнал
- Что выбрать под вашу задачу
- Практические рекомендации
- Итог
Почему именно 20 кГц и что пойдёт не так
20 кГц — это граница слышимого диапазона. Для управления моторами и подсветкой это стандартная частота, при которой исчезает звон обмоток и мерцание на видео. Но именно здесь начинаются проблемы:
- Период сигнала — 50 мкс. У вас всего 800 тактов на частоте 16 МГц.
- Любая задержка в пару микросекунд — это уже заметное искажение скважности.
- Прерывания от других задач (Serial, millis, I2C) сбивают фронты.
- После RC-фильтра с такой частотой остаётся ощутимая пульсация, если не подобрать правильно постоянную времени.
Три главных врага чистого soft-PWM: нестабильность тайминга, влияние прерываний и неправильный фильтр на выходе. Каждый из них нужно решать отдельно.
Три подхода к реализации — и какой когда использовать
| Подход | Частота | CPU-нагрузка | Стабильность фронтов | Когда выбирать |
|---|---|---|---|---|
| Таймер + прерывание | 20–100 кГц | 2–5% | Высокая | Есть свободный таймер, нужна стабильность |
| Прямой цикл (delayMicroseconds) | До 5 кГц | 100% | Низкая | Одна-две ноги, больше ничего не делает MCU |
| DMA + GPIO (STM32, ESP32) | До МГц | ~0% | Максимальная | Много каналов, критична точность |
Способ 1: Таймер с прерыванием — рабочий вариант для AVR
Это единственный нормальный путь для Arduino Uno/Nano/Pro Mini, если вам нужен 20 кГц PWM и при этом контроллер должен делать что-то ещё. Суть: настраиваем таймер на прерывание каждые 50 мкс, в обработчике считаем тики и перекладываем ногу.
На Timer1 для ATmega328P это выглядит так:
void setupSoftPWM() {
pinMode(9, OUTPUT);
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// 20 кГц: 16 МГц / (1 * (1 + 79)) = 200 кГц — слишком быстро
// Делим дальше: CTC mode, OCR1A = 79, prescaler = 1
// 16МГц / (1 * (79+1)) = 200 кГц — прерывание каждые 5 мкс
// Считаем 10 тиков для периода 50 мкс (20 кГц)
OCR1A = 79;
TCCR1B |= (1 << WGM12); // CTC mode
TCCR1B |= (1 << CS10); // prescaler = 1
TIMSK1 |= (1 << OCIE1A); // enable compare interrupt
interrupts();
}
volatile uint8_t pwmCounter = 0;
volatile uint8_t pwmValue = 128; // скважность 0-255
ISR(TIMER1_COMPA_vect) {
pwmCounter++;
if (pwmCounter >= 10) pwmCounter = 0;
if (pwmCounter < (pwmValue >> 5)) // грубое приближение для примера
PORTB |= (1 << PB1); // pin 9 = high
else
PORTB &= ~(1 << PB1); // pin 9 = low
}
Здесь есть нюанс, который многие упускают: разрешение. При 10 тиках на период у вас всего 10 уровней яркости. Для 256 градаций нужно 256 тиков — а это значит, что прерывание должно срабатывать каждые 50/256 ≈ 0.195 мкс. На 16 МГц это 3 такта — физически невозможно, сам обработчик прерывания занимает больше.
Выход: либо понижаем разрешение до 64 уровней (прерывание каждые ~0.78 мкс — на пределе для 16 МГц), либо поднимаем частоту контроллера, либо используем аппаратный PWM с нужной частотой.
Способ 2: Аппаратный PWM на высокой частоте — если ножка позволяет
На том же ATmega328P таймеры могут генерировать PWM без CPU. Проблема в том, что стандартный analogWrite даёт 490 Гц (пины 5,6) или 980 Гц (пины 9,10). Но можно перенастроить таймер:
// Fast PWM, 8-bit, ~20 кГц на пине 9 (Timer1)
void setupHardwarePWM20kHz() {
pinMode(9, OUTPUT);
TCCR1A = (1 << COM1A1) | (1 << WGM10);
TCCR1B = (1 << WGM12) | (1 << CS10);
// 16 МГц / (1 * 256) = 62.5 кГц — многовато
// Для ровно 20 кГц используем ICR1 как TOP:
// 16 МГц / (1 * (800 + 1)) = 19.95 кГц
ICR1 = 800;
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
OCR1A = 400; // 50% скважность из 800
}
Разрешение — 800 уровней. Это уже серьёзно. Минус: жёсткая привязка к конкретным ногам (9 и 10 для Timer1) и потеря millis()/delay(), если вы трогаете Timer1 на Arduino — хотя при правильной настройке WGM это не так.
Способ 3: Прямой цикл — когда больше ничего не нужно
Если ваш контроллер только и делает, что генерирует PWM на одной-двух ногах — можно обойтись без таймеров:
void loop() {
// 20 кГц = 50 мкс период
// Скважность 50%: 25 мкс HIGH, 25 мкс LOW
PORTB |= (1 << PB0);
delayMicroseconds(25);
PORTB &= ~(1 << PB0);
delayMicroseconds(25);
}
Это работает, но delayMicroseconds на AVR имеет разрешение 4 мкс (при 16 МГц) и погрешность в пару тактов. Фронты будут плавать. Плюс — никаких других задач, никаких прерываний. На практике этот способ годится только для прототипов и отладки.
RC-фильтр: почему после PWM не получается ровный сигнал
Вы получили 20 кГц PWM. Подали на RC-фильтр. Осциллограмма показывает пилу с амплитудом 50–100 мВ. Что не так?
Частота среза фильтра должна быть в 10–20 раз ниже частоты PWM. Для 20 кГц это 1–2 кГц. Формула:
f_c = 1 / (2π × R × C)
Для f_c = 1 кГц: R = 10 кОм, C = 15 нФ. Но здесь вторая проблема — выходное сопротивление фильтра. Если следующий каскад имеет входное сопротивление 50 кОм, оно будет шунтировать резистор фильтра и менять частоту среза. Решение — буферный повторитель на операционном усилителе.
Для серьёзных применений (ЦАП из PWM) лучше использовать двухполюсный фильтр — два RC-каскада через повторитель. Это даёт затухание -40 дБ/декаду вместо -20 и пульсации упадут с 50 мВ до единиц милливольт.
Частые ошибки, которые убивают сигнал
Ошибка 1: Игнорирование прерываний. Если у вас включены Serial, millis или другие прерывания — каждый уход в обработик занимает 2–5 мкс. Для 20 кГц это 10–25% искажения скважности. Решение: отключать прерывания на время критичного кода или использовать чисто аппаратный PWM.
Ошибка 2: Неправильный выбор частоты среза фильтра. Слишком высокая — пульсации остаются. Слишком низкая — сигнал не успевает меняться при изменении скважности. Для управления яркостью светодиода это не критично, для звука или измерений — смерть.
Ошибка 3: Попытка получить 256 уровней на 20 кГц на 16 МГц. Просто не хватит тактов. Либо снижайте разрешение до 64 уровней, либо поднимайте частоту MCU до 48–72 МГц, либо используйте аппаратный PWM.
Ошибка 4: Забывать про slew rate. Если после фильтра стоит усилитель или длинный кабель, медленные фронты на выходе фильтра могут вызвать наводки и звон. Буфер с высоким slew rate решает проблему.
Что выбрать под вашу задачу
Нужно управлять яркостью светодиода на Arduino Uno: перенастройте Timer1 или Timer2 на 20+ кГц в режиме Fast PWM. Это аппаратное решение, CPU свободен, искажений нет.
Нужно 4–8 каналов soft-PWM на 20 кГц: на AVR это нереально через прерывания. Берите STM32 или ESP32 — там DMA может перекладывать десятки ног без CPU. Или используйте специализированные драйверы вроде PCA9685 (16 каналов, 12 бит, до 1.6 кГц — правда, не 20 кГц).
Нужен аналоговый сигнал из PWM (ЦАП): двухполюсный RC-фильтр через повторитель, частота PWM минимум 100 кГц, разрешение 10–12 бит. Для звука — только аппаратный PWM на высокой частоте или внешний ЦАП.
Нужно управлять мотором без писка: 20 кГц через аппаратный PWM + MOSFET с быстрым переключением. Soft-PWM здесь не нужен — аппаратный вариант работает идеально.
Практические рекомендации
- Всегда начинайте с проверки, есть ли у вашего контроллера свободный таймер с аппаратным PWM. Это самый чистый путь.
- Если нужен именно soft-PWM — считайте тики, а не используйте delayMicroseconds. Таймер с прерыванием даёт стабильность в 10–100 раз лучше.
- Для 20 кГц на AVR примите разрешение 64 уровня (6 бит). Этого достаточно для управления яркостью и скоростью мотора.
- RC-фильтр всегда делайте с буфером, если нагрузка имеет сопротивление менее 1 МОм.
- Проверяйте сигнал осциллографом. На слух или на глаз отличить 20 кГц с искажениями от чистого сигнала невозможно.
Итог
Soft-PWM на 20 кГц без искажений — это не магия, а правильный выбор инструмента. Для одного-двух каналов на Arduino — перенастройте аппаратный таймер. Для многих каналов на мощном MCU — используйте DMA. Для простых задач, где контроллер больше ничего не делает — прямой цикл с таймером. И всегда помните: после PWM нужен правильный фильтр, иначе вся работа по генерации пойдёт насмарку.
