Шесть тильд на Uno кончаются у меня за один вечер. Витрина с поворотной платформой: два серво разобрали пины 9 и 10 (и заодно лишили analogWrite прав, о чём я уже писал в разборе конфликта с Servo.h), мотор с драйвером L298N сел ШИМ-ногами на 5 и 6, зуммер взял 3, подсветка дисплея — 11. Осталась подсветка самой витрины, которую хочется плавно гасить на ночь, — и четыре пина без всякой черты: 4, 7, 8, 12. analogWrite(8, 128) на них молча превращается во включено-выключено: у пина нет таймера под ним.
Первый порог — привычный тупик: перекомпоновать схему. Иногда спасает, но тут каждый ШИМ-пин занят по делу: мотору и сервам плавность нужна честная, аппаратная. Второй порог — мигалка на digitalWrite с delay-полосками: глаз видит рябь, а loop тонет в паузах. Осталось честное решение — программный ШИМ: таймер тикает прерыванием в разы чаще периода, в обработчике живёт фаза и уставка duty, пин включается на старте периода и гасится, когда фаза догоняет duty. Тот же analogWrite по смыслу, только тянут его переменные, а не аппаратура.
Здесь собираю приём целиком: тик 31 250 Гц на Timer2 в CTC из урока про прерывание по таймеру, фаза-байт, который сам переполняется с 255 на 0, duty с потенциометра. И сразу честная цена: такой ШИМ ест процессор, и я покажу таблицей, сколько именно — а заодно где граница, за которой честнее вернуть схему к аппаратному analogWrite.
Фаза, уставка и цена тика
Арифметика частот: глаз перестаёт видеть мерцание примерно от 100 Гц, беру с запасом 122 Гц. Период — 8,2 мс, ступеней яркости хочу полный байт, 256: значит тик обязан приходить каждые 8,2 мс / 256 = 32 мкс, это 31 250 раз в секунду. На Timer2 расклад чистый: предделитель 32 (16 МГц / 32 = 500 кГц), граница OCR2A = 15 — шестнадцать тактов на тик. Ровно как в уроке про свою частоту ШИМ, только прерывание служит не звуку, а яркости.
В обработчике три строки: фаза увеличивается на единицу и переполняется сама — байт после 255 лезет в 0 без всяких if; на фазе 0 пин включается; на фазе, догнавшей duty, выключается. Полученный процент горения и есть яркость: duty 128 — половина периода HIGH. Ставить duty может loop() хоть каждый оборот: байт — атомарная величина, обработчик не поймает половину записи, тут даже замок из урока про volatile не нужен.
Теперь цена, которую все стесняются посчитать. Вход в ISR, два digitalWrite, выход — порядка 140 тактов; 31 250 визитов в секунду — это 4,4 миллиона тактов из 16 миллионов: примерно треть процессора съедает один канал такого ШИМ. Для одной подсветки рядом с двумя серво — приемлемо, серво всё равно командует отдельный контроллер. Но два-три таких канала плюс Serial — и на остальную задачу тактов не останется. Лечения два, оба честные: резать ступени (глаз между 64 и 256 градациями почти не различает) или менять digitalWrite на прямые порты из урока про PORTB — тогда тот же канал стоит порядка восьми процентов. Оба пути — в таблице.
// Программный ШИМ на пине 8 (без тильды): Timer2 тикает 31 250 Гц,// 256 шагов на период -> частота мерцания 122 Гц. duty 0..255.volatile byte faza = 0;volatile byte duty = 128; ISR(TIMER2_COMPA_vect) { faza = faza + 1; // байт переполнится с 255 на 0 сам if (faza == 0) { digitalWrite(8, HIGH); // новый период: включили } if (faza == duty) { digitalWrite(8, LOW); // фаза догнала уставку: выключили }} void setup() { pinMode(8, OUTPUT); TCCR2A = (1 << WGM21); // CTC TCCR2B = (1 << CS21) | (1 << CS20); // предделитель 32 OCR2A = 15; // (15+1) x 2 мкс = 32 мкс на тик TIMSK2 = (1 << OCIE2A); // прерывание по совпадению A} void loop() { duty = analogRead(A0) / 4; // 0..1023 -> 0..255, запись байта атомарна delay(50);}Разбор по строкам. Нулевой и стодвадцать восьмой duty обрабатываются сами: при duty = 0 оба if срабатывают в одном визите, пин получает HIGH и тут же LOW — света нет; при duty = 255 горение длится 255 из 256 тиков — почти полный накал, честных ста процентов у этой схемы нет и не надо. loop() живёт своей жизнью: читает ручку и пересчитывает duty, никакой дрожи в свете от занятого loop не будет — тик приходит из прерывания, ему безразлично, чем занята программа. Именно это отличает приём от delay-мигалки, и именно за это я его терплю несмотря на цену.
Где приём уместен: подсветки, индикаторные огни, плавные переходы «дышащих» светодиодов на обычных пинах — то, что глазу нужна плавность, а схеме не нужны килогерцы. Где не уместен: моторы и реле — им нужны килогерцы аппаратного ШИМ, программные сотни герц заставят мотор петь, а реле щёлкать. И обязательная проверка перед внедрением: что висит на Timer2 в этом скетче. Зазвучит tone() — регистры перепишутся, тик умрёт, пин замрёт в последнем состоянии, и разбор этой ловушки лежит в уроке про tone и таймеры. Зуммер в проекте — значит, тик переносят на Timer1, как в соседнем уроке про CTC на 16 битах.
| Ступеней на период | Тиков в секунду | Цена при digitalWrite | Что видит глаз |
|---|---|---|---|
| 256 (8 бит) | 31 250 | около 30% процессора | плавно, градаций с запасом |
| 64 (6 бит) | 7 813 | около 7% | плавно, разницу с 8 битами не отличить |
| 16 (4 бита) | 1 953 | около 2% | заметны ступени на низкой яркости |
| 256 + порты вместо digitalWrite | 31 250 | около 8% | плавно, код становится регистровым |
- 1. Сколько процессора съедает один канал программного ШИМ: 256 ступеней, 122 Гц, digitalWrite внутри ISR?
- 2. В проект зазвучал tone(). Что станет с программным ШИМ, тик которого живёт на Timer2?
- программный ШИМ = тик в 256 раз чаще периода: 122 Гц -> 31 250 Гц, Timer2 CTC, OCR2A = 15
- фаза-байт переполняется сама, duty-байт атомарен: loop может менять уставку без замков
- цена при digitalWrite около 30% тактов: 64 ступени или прямые порты снижают до 7-8%
- tone() убивает тик на Timer2 — проверяй карту таймеров до внедрения, зуммер живёт на другом канале
Частые вопросы по теме урока
- Сколько процессора съедает один канал программного ШИМ: 256 ступеней, 122 Гц, digitalWrite внутри ISR?
- Визит в ISR с парой digitalWrite стоит порядка 140 тактов; 31 250 визитов в секунду дают около 4,4 млн тактов из 16 млн — примерно треть процессора. Резка ступеней или порты снижают цену в разы. около трети тактов — 31 250 визитов в секунду недёшевы
- В проект зазвучал tone(). Что станет с программным ШИМ, тик которого живёт на Timer2?
- tone() занимает Timer2 целиком и молча перезаписывает TCCR2A/OCR2A/TIMSK2 — тот же конфликт, что у секундного тика из урока про CTC. Лечение: перенос тика на свободный Timer1 или отказ от tone. тик умрёт: tone() перепишет регистры Timer2, пин замрёт в последнем состоянии