Датчик воды в баке на дальнем крае участка: просыпается раз в минуту, делает сотню analogRead, усредняет, отправляет три байта по радио — и снова спит. Вся работа занимает десятки миллисекунд, а тактирую его, как ни в чём не бывало, на шестнадцать мегагерц. Ток чипа в активном режиме растёт почти линейно с частотой: при 5 В это порядка пятнадцати миллиампер на 16 МГц, около восьми на 8 МГц и единицы миллиампер на 1 МГц. Четыре пятых оборотов тому узлу не нужны — я буквально сжигал батарейку в пустоту.
Обзорная фраза «сон экономит энергию» была в уроке про таймеры, watchdog и сон, а в уроке про режимы сна я разложил сам сон. Здесь — третий слой экономии, который работает и без сна: понизить тактовую частоту ядра и выключить периферию, которой в проекте всё равно не пользуется. Механики две: регистр предделителя такта CLKPR и регистр снижения мощности PRR с битами на каждую периферию.
Плата за скорость конкретная, и я её пережил на себе: millis() и delay() начинают течь в число раз медленнее, Serial уезжает в бодах, ADC читает втрое-ввосемь раз дольше. Разберём обе механики и каждую цену — по цифрам.
CLKPR: регулятор оборотов ядра
У ATmega328P есть регистр CLKPR, который делит такт кварца на 1, 2, 4, 8, ..., 256 для всего чипа: ядра и периферии одновременно. Кварц продолжает честно тикать свои шестнадцать мегагерц — экономия возникает не в кварце, а в том, что каждый такт ядра и каждый шаг периферии теперь стоят больше наносекунд и меньше тока. Запись защищена: сначала в CLKPR пишут бит разрешения CLKPCE, и в течение четырёх следующих тактов — новое значение делителя. В Arduino-обвязке есть готовая обёртка clock_prescale_set(clock_div_2) из avr/power.h, которая делает ту же песню одной строкой; менять делитель на лету можно хоть в loop, на практике удобнее один раз в setup.
Номера получаются такие: при 5 В голый чип на 16 МГц берёт около пятнадцати миллиампер, на 8 МГц — около восьми, на 2 МГц — два с половиной, на 1 МГц — около полутора. Практическое правило для прикидок: примерно миллиампер на мегагерц при 5 В. Понижение питания с 5 до 3,3 В снимает ещё треть — но и пороги логики уезжают, это уже тема отдельной сборки платы.
И сразу три ловушки, о которых даташит говорит скучно, а гугл — цветисто. Первая: millis(), micros() и delay() считают время от тактов Timer0 — при делителе 8 секунда растягивается в восемь раз, и весь millis-планировщик из урока про millis вместо delay поедет синхронно. Вторая: скорость UART считается от константы F_CPU, а не от живого делителя — порт уедет в бодах ровно во столько же раз. Третья: analogRead при 104 микросекундах на 16 МГц на делителе 8 займёт восемьсот тридцать микросекунд — для редких чтений неважно, для быстрых фильтров смертельно.
#include <avr/power.h> const int PIN_POT = A0; unsigned long zammerAdc(byte raz) { // время 100 чтений ADC, мкс unsigned long t0 = micros(); unsigned long summa = 0; for (byte i = 0; i < 100; i++) { summa += analogRead(PIN_POT); } return (micros() - t0) / 1; // summa печатаем, чтобы цикл жил} void setup() { Serial.begin(9600); Serial.print("16 MHz, 100 x analogRead: "); Serial.println(zammerAdc(100)); clock_prescale_set(clock_div_8); // 16 -> 2 MHz // UART считает бод от F_CPU-константы: после делителя порт уехал бы // в 8 раз медленнее. Перезапускаем с бодом x8 - на линии снова 9600. Serial.begin(9600 * 8); Serial.print("2 MHz, 100 x analogRead: "); Serial.println(zammerAdc(100)); Serial.println("millis teper techet v 8 raz medlennee - uchityvaj");} void loop() { delay(1000); // фактически 8 секунд}Разбор по строкам. Замер zammerAdc() до и после смены делителя показывает цену честно: сотня чтений ADC на 16 МГц занимает около десяти миллисекунд, на 2 МГц — восемьдесят. Приём с Serial.begin(9600 * 8) — главный сюрприз урока: библиотека считает делитель бода от константы F_CPU и ничего не знает о живом CLKPR, поэтому бод при пониженном такте умножают вручную на делитель. Аналогично, если после понижения нужен честный millis(), умножайте свои интервалы на делитель или возвращайте clock_div_1 на время замеров — методика точных замеров на micros() разобрана в уроке про micros и точные замеры.
Что при этом не меняется: точность кварца, частота ШИМ в тактах таймера, логика скетча. Что меняется: всё, что отмеряет время. Поэтому узлы, где такт понижен постоянно, обычно сразу пишут код в «медленных» единицах и проверяют Serial через умноженный бод — иначе каждый эксперимент превращается в раскопки.
| Делитель CLKPR | Такт ядра | Ток голого чипа при 5 В | 100 x analogRead |
|---|---|---|---|
| clock_div_1 | 16 МГц | около 15 мА | около 10 мс |
| clock_div_2 | 8 МГц | около 8 мА | около 20 мс |
| clock_div_8 | 2 МГц | около 2,5 мА | около 80 мс |
| clock_div_16 | 1 МГц | около 1,5 мА | около 160 мс |
PRR и BOD: выключаем то, чем не пользуемся
Вторая механика — регистр снижения мощности PRR, по биту на каждую периферию: PRADC, PRUSART0, PRTIM0, PRTIM1, PRTIM2, PRSPI, PRTWI. Бит, поставленный в единицу, отрезает такт от блока — блок перестаёт есть, даже когда вы думали, что он «простаивает». Ядро Arduino включает ADC сразу после старта, и простой ADC на хорошей сборке стоит около четверти миллиампера — для сети это пустяк, для батарейного узла заметная строка. Удобнее битов — готовые функции из avr/power.h: power_adc_disable() и парная power_adc_enable(), когда analogRead снова понадобится.
Практическая граница двух приёмов: пока узел бодрствует дольше, чем спит, делитель такта даёт больше, чем сон; когда узел спит девяносто процентов времени, приёмы работают в пару, и выигрыш складывается.
Правила безопасности простые. Отрезали PRTIM0 — встал Timer0, и с ним millis(); отрезали PRUSART0 — замолчал Serial до power_usart0_enable(). Практика, которая мне нравится: в узле, который просыпается раз в минуту, сразу после пробуждения включается всё нужное, делается работа, а перед сном всё лишнее отрезается — сон при этом становится глубже, ведь некоторые блоки в power-down всё же подхлёбывают. Связка с режимами сна ложится внахлёст: там мы выключали сон, здесь — факторы, которые мешают сну быть глухим.
Отдельная строка — детектор провалов питания BOD. Он сравнивает напряжение с порогом непрерывно и стоит порядка двадцати микроампер. Функция sleep_bod_disable() из avr/sleep.h выключает его только на ближайший сон — вызывать нужно непосредственно перед засыпанием. На power-down со сторожем это выглядит как минус двадцать мкА к пяти-семи: экономия в разы на той строке, которая остаётся главной. Цена честно оговорена: во время сна чип не заметит просадку питания — на стабильном источнике это не страшно, на севшей батарее чип может заснуть так глубоко, что не проснётся до подзарядки.
Что докрутить
Задания. Первое: соберите таблицу ток/частота у себя — мультиметр в разрыв питания, clock_prescale_set по очереди на 1, 2, 8, 16, запишите цифры и сравните с правилом «миллиампер на мегагерц». Второе: добавьте в узел из урока про сон понижение такта до 1 МГц на время деловой фазы и посчитайте, сколько миллиампер-часов экономится в сутки. Третье: включите и выключите ADC вокруг analogRead в цикле из десяти тысяч чтений и замерьте цену самих пар enable/disable — так вы узнаете, где пары функций окупаются, а где стоят дороже экономии.
И четвёртое, теоретическое: разберитесь по даташиту, какие блоки в power-down всё же подхлёбывают ток при включённых битах PRR, и напишите себе шпаргалку «что отрезать перед глухим сном». Механику таймеров, от которых зависит millis(), я раскладывал в уроке-обзоре таймеров ATmega328 — после неё биты PRTIM перестанут быть абракадаброй.
- 1. Как растёт ток активного чипа при снижении тактовой частоты?
- 2. Что происходит с millis() при clock_prescale_set(clock_div_8)?
- 3. Как удержать Serial на 9600 бод после установки делителя 8?
- 4. Что делает бит PRADC в регистре PRR?
- 5. Сколько экономит sleep_bod_disable() и на что это распространяется?
- Ток активного чипа при 5 В падает почти линейно с частотой: ~15 мА на 16 МГц до ~1,5 мА на 1 МГц — прикидка «1 мА на МГц»
- CLKPR делит такт ядра и периферии разом; запись защищена окном CLKPCE, в Arduino — clock_prescale_set() из avr/power.h
- Цена делителя: millis(), micros() и delay() замедляются в число раз, UART уезжает в бодах — Serial.begin(9600 * делитель) возвращает порт на место
- PRR режет такт по блокам: ADC стоит около 0,25 мА, пары power_adc_disable/enable стоят десятки тактов и ставятся вокруг каждого цикла работы
- sleep_bod_disable() срезает ~20 мкА, но только на ближайший сон — и чип перестаёт замечать просадку питания, пока спит
Частые вопросы по теме урока
- Как растёт ток активного чипа при снижении тактовой частоты?
- При 5 В замеры дают порядка 15 мА на 16 МГц, 8 мА на 8 МГц, около 1,5 мА на 1 МГц. Для прикидок годится правило «примерно 1 мА на 1 МГц». падает вдесятеро при любом снижении
- Что происходит с millis() при clock_prescale_set(clock_div_8)?
- Timer0 тактируется от общего делителя, поэтому millis() и delay() замедляются синхронно с ядром. Интервалы нужно умножать на делитель или менять такт на время измерений. замедляется в 8 раз: секунда наступает за 8 настоящих секунд
- Как удержать Serial на 9600 бод после установки делителя 8?
- Библиотека считает делитель UART от константы F_CPU и не знает о живом CLKPR. Умножив задаваемый бод на делитель, возвращаем фактическую скорость на линии к 9600. никак, порт молчит до возврата clock_div_1
- Что делает бит PRADC в регистре PRR?
- PRR отключает тактирование периферийного блока. ADC ядро Arduino включает сразу после старта, и простой преобразователь тратит заметную для батарейного узла долю тока. сбрасывает результат АЦП
- Сколько экономит sleep_bod_disable() и на что это распространяется?
- Детектор провалов питания стоит порядка 20 мкА непрерывно. sleep_bod_disable() выключает его лишь на следующий уход в сон, поэтому вызывать нужно непосредственно перед засыпанием. около 20 мА, на весь дальнейший скетч