Сейчас, когда у меня скопилось несколько микроконтроллеров различного типа, настало время разобраться с их энергопотреблением, чтобы не полагаясь на рекламные лозунги различных компаний, понять, что есть что в мире автономного питания.
Первым делом мой взгляд упал на ATtiny13A. Небольшая микросхема, если логически подумать, должна потреблять минимум энергии. На практике однако, это зависит от используемого техпроцесса при изготовлении этой микросхемы.
Справедливости ради стоит заметить, что ATtiny13 с пониженным энергопотреблением имеют индекс V. А чипы с технологией picoPower, снабжаются индексом P. Что же до ATtiny13A, согласно документации, его потребление в обычном режиме, при питании 1.8V и частоте 1МГц - 160 микро ампер. В Idle режиме при тех же условиях - 24 микро ампер. Питание 1.8В я не собираюсь использовать, меня больше интересует потребление при 3.3В.
Мой мультиметр фирмы Mastech, модель: M830BZ. Это не весть что, но если измерять потребление различных чипов одним прибором, выводы сделать можно будет.
Для работы с режимами энергосбережения в avr-gcc есть библиотека avr/sleep.h. Переводной мануал по данной библиотеке можно почитать здесь. Я же пойду по традиционному пути ковыряния регистров.
В качестве исходного примера возьмем blink из старого поста:
// blink.c for AVR ATtiny 13/25/45/85 #include <avr/io.h> #define F_CPU 1200000UL // частота резонатора 1МГц #include <util/delay.h> int main(void) { // макрос _BV(число) заменяет конструкцию (1 << число) DDRB |= _BV(PB0); // аналог pinMode(PB0,OUTPUT); в Wiring for (;;) { PORTB ^= _BV(PB0); // инвертируем состояние порта PB0 _delay_ms(1000); // ждем 1 секунду } return 0; }
Фьюзы пусть будут установлены по умолчанию: hfuse = 0xFF; lfuse = 0x6A; Батарея, CR2032, у меня не первой свежести выдает 3.05В без нагрузки. Чип тактируется от внутреннего генератора частотой 1MHz
Итого потребление в активном режиме: 525 мкА
Заглянув на 30-ю страницу официального руководства, раздел семь: "Power Management and Sleep Modes" можно найти описание регистра: " Power Reduction Register" который позволяет отключать часть периферии:
т.е.
#define F_CPU 1200000UL #include <avr/io.h> #include <util/delay.h> int main(void) { PRR=(1<<PRTIM0)|(1<<PRADC); DDRB |= (1<<PB0); for (;;) { PORTB ^= (1<<PB0); _delay_ms(3000); } return 0; }
Ok энергопотребление снизилось до 512 мА. Хм, как-то не впечатляет.
Теперь попробуем отпустить микроконтроллер в Idle режим, когда CPU и флеш память засыпают, а прерывания продолжают работать.
Режимы энергосбережения управляются через регистр MCUCR:
Порядок работы такой: сначала задается режим энергосбережения, и только потом он включается установкой флага SE. Включение энергосбережения происходит ассемблерной командой sleep. С учетом всего вышесказанного, перепишем blink на таймере:
#define F_CPU 1200000UL #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h> volatile uint8_t i; ISR(TIM0_OVF_vect) { if (!(++i%16)) //period 4s PORTB ^= (1<<PB0); } int main(void) { // PRR = (1<<PRTIM0) | (1<<PRADC); // shut down Timer0 & ADC PRR = (1<<PRADC); // shut down ADC TIMSK0 = (1<<TOIE0); // timer0 overflow interrupt enable TCCR0B = (1<<CS02) | (1<<CS00); // prescaler 1/1024 DDRB |= (1<<PB0); i=0; // set_sleep_mode (SLEEP_MODE_IDLE); // sleep_enable(); MCUCR &= ~(1<<SM1); // idle mode MCUCR &= ~(1<<SM0); // idle mode MCUCR |= (1<<SE); sei(); while(1) { // sleep_cpu(); asm("sleep"); } return 0; }
Энергопотребление упало до 180 мкА, недурно! А теперь попробуем взглянуть на максимум того, на что можно расчитывать, и пошлем микроконтроллер в power-down режим:
#define F_CPU 1200000UL #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h> volatile uint8_t i; ISR(TIM0_OVF_vect) { if (!(++i%16)) //period 4s PORTB ^= (1<<PB0); } int main(void) { // PRR = (1<<PRTIM0) | (1<<PRADC); // shut down Timer0 & ADC PRR = (1<<PRADC); // shut down ADC TIMSK0 = (1<<TOIE0); // timer0 overflow interrupt enable TCCR0B = (1<<CS02) | (1<<CS00); // prescaler 1/1024 DDRB |= (1<<PB0); i=0; // set_sleep_mode (SLEEP_MODE_PWR_DOWN); // sleep_enable(); MCUCR |= (1<<SM1); // power-down mode MCUCR &= ~(1<<SM0); // power-down mode MCUCR |= (1<<SE); sei(); while(1) { // sleep_cpu(); asm("sleep"); } return 0; }
Иииииии.... питание упало до запредельных 0.1 мкА или ~100 нА. Здорово! Но и наш светодиод перестал мигать. Так зачем же такой режим если из него нельзя ничего делать?
На самом деле, делать из него можно много чего. Но сперва призываю обратить внимание на фьюзы. Они описаны в разделе 17.2 официального руководства.
Здесь ноль означает, что соответствующий бит установлен, единица - сброшен. т.е. если старший байт, hfuse, имеет значение 0хFF - это означает, что все его флаги сброшены.
Вообще, я изначально думал, что через фьюз биты устанавливается частота тактирования от внутреннего генератора. Отчасти это так, но не все так просто. Через фьюзы можно сменить частоту внутреннего генератора:
Однако делитель для этого генератора задается в регистре CLKPR:
Интересен этот параграф:
"The CKDIV8 Fuse determines the initial value of the CLKPS bits. If CKDIV8 is unprogrammed, the CLKPS bits will be reset to “0000”. If CKDIV8 is programmed, CLKPS bits are reset to “0011”, giving a division factor of eight at start up."
Т.е. если фьюз CKDIV8 установлен, тогда в регистру CLKPR при старте присваивается значение 0011 т.е. делитель восемь.
Теперь смотрим дефолтное значение младшего фьюз байта:
0х6А = 0110 1010
Установлены биты: SPIEN(мой любимый способ залочивания чипов), CLDIV8, SUT0, CKSEL0
Из этого видим, что внутренний генератор работает на частоте 9.6МГц с делителем равным восьми, т.е. примерно 1МГц.
Интересный способ залочивания чипов заключается в изменении фьюз битов CKSEL1/CKSEL0:
Можно тактировать микроконтроллер от низкочастотного таймера watchdog'а что должно дать на первый взгляд(время выполнения кода увеличится пропорционально) существенную экономию знергопотребления. Однако после этого, перед тем как прошить микроконтроллер еще раз, придется пройти квест: "найди на программаторе джампер slowSCL".
Ok, с тактированием разобрались, возвращаемся к энергопотреблению. Для батарейного устройства вроде бы неплохо будет монитор питания BOD. Он настраивается через фьюз биты BODLEVEL:
Для трехвольтового источника питания соответствует вторая строчка, т.е. hfuse будет равен: 11111011 = 0xFB
$ avrdude -p t13 -c usbasp -U hfuse:w:0xfb:m avrdude: warning: cannot set sck period. please check for usbasp firmware update. avrdude: AVR device initialized and ready to accept instructions Reading | ################################################## | 100% 0.00s avrdude: Device signature = 0x1e9007 avrdude: reading input file "0xfb" avrdude: writing hfuse (1 bytes): Writing | ################################################## | 100% 0.01s avrdude: 1 bytes of hfuse written avrdude: verifying hfuse memory against 0xfb: avrdude: load data hfuse data from input file 0xfb: avrdude: input file 0xfb contains 1 bytes avrdude: reading on-chip hfuse data: Reading | ################################################## | 100% 0.00s avrdude: verifying ... avrdude: 1 bytes of hfuse verified avrdude: safemode: Fuses OK (E:FF, H:FB, L:6A) avrdude done. Thank you.
Но если сейчас замерить энергопотребление обнаружится неприятное: потребление в power-down режиме выросло с 0.1 мкА до 104мкА.
Программно отключить BOD можно через BODCR регистр:
Я же лучше сброшу фьюз на исходное значение.
В заключение хочу уберечь от попыток изменить RSTDISBL и SPIEN фьюзы. Они отключат возможность работы программатора.
После того как замерили потребление BOD, заодно разобравшись с фьюзами, осталось разобраться с тем как заставить микроконтроллер работать в power-down режиме. Ответ кроется в использовании watchdog'а.
Про эту штуку часто можно услышать из сообщений NASA когда бортовой компьютер КА в очередной раз сломали инопланетяне завис и он был перезапущен watchdog'ом. На ихнем сайте по ключевому слову watchdog timer, вообще много чего интересного выпадает.
Суть в том, что watchdog это отдельный энергоэффективный таймер, и пока основная программа работает, она этот таймер периодически сбрасывает. Если же главный рабочий цикл подвис, то watchdog дождавшись переполнения счетчика просто сделает то для чего предназначен - софтовый reset.
Причем в AVR есть регистр MCUSR по флагу WDRF которого можно определить, что микроконтроллер был сброшен именно watchdog'ом а не BOD - монитором наример:
Хороший пример программы на прерывании WDT можно найти здесь: AVR - Power management или как правильно спать Пример настолько замечательный что его следует сразу скомпилировать и прошить в микроконтроллер:
#include <avr/io.h> #include <avr/wdt.h> // здесь организована работа с ватчдогом #include <avr/sleep.h> // здесь описаны режимы сна #include <avr/interrupt.h> // работа с прерываниями volatile uint8_t i; ISR (WDT_vect) { if ((++i%4) == 0) PORTB |= (1<<PB0); // включаем светодиод else PORTB &= ~(1<<PB0); // выключаем светодиод WDTCR |= (1<<WDTIE); // разрешаем прерывания по ватчдогу. Иначе будет резет. } int main() { DDRB = (1<<PB0); // на этом пине висит светодиод i=0; //инициализация ватчдога wdt_reset(); // сбрасываем wdt_enable(WDTO_1S); // разрешаем ватчдог 1 сек WDTCR |= (1<<WDTIE); // разрешаем прерывания по ватчдогу. Иначе будет резет. sei(); // разрешаем прерывания set_sleep_mode(SLEEP_MODE_PWR_DOWN); // если спать - то на полную while(1) { sleep_enable(); // разрешаем сон sleep_cpu(); // спать! } }
Т.о. теперь, потребление тока составляет- 4.7мкА. Т.е. заряда обычной батареи типа "таблетка" на 200мА хватит на 42553 часов, или 5 лет.
Нужно заметить, что значение таймера watchdog не абсолютное, а зависит от напряжения батареи.
Касаемо ATiny13, хотелось бы затронуть еще такой вопрос как программный UART. Трудно писать программу вслепую, и если, например, делаешь температурный регулятор оборотов вентилятора, то хочется знать что термодатчик показывает и скорость с какой какой крутиться вентилятор.
По правде говоря программный UART для ATtiny уже давно написан и описан здесь:
Программа изложенная там хорошая, она работает. Правда там ничего не сказано про частоту чипа, на которой он работал у автора, но методом перебора, мне удалось установить свазь для 1МГц чипа на 1200 бод.
Кроме того упомянутая программа не оптимизирована по размеру. Попытка оптимизации была предпринята здесь: UART в ATtiny13 или Как вывести данные из МК за 52р
однако автору не хватило смелости на более кардинальную оптимизацию. Покрутив какое-то время программу в отладчике, я составил свою версию:
#define F_CPU 1200000UL #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> volatile uint8_t c; ISR(TIM0_COMPA_vect) { c=1; TCNT0=0; } static int UART_trans(uint8_t data) { uint8_t f,data2; data=~data; TCCR0B=(1<<CS01); OCR0A=115; TIMSK0=(1<<OCIE0A); for(f=0;f<10;f++) { while(c==0); c=0; if (f>8) { PORTB|=(1<<PB4); } else if (f==0) { PORTB&=~(1<<PB4); } else { data2 = data & (1<<(f-1)); if (data2) PORTB&=~(1<<PB4); else PORTB|=(1<<PB4); } } c=0; TIMSK0=0; return 0; } int main(void) { DDRB |= (1<<PB0) | (1<<PB4); sei(); for(;;) { PORTB ^= (1<<PB0); _delay_ms(1000); // Пауза для наглядности UART_trans('L'); // Отправляем обратно } return 0; }
После компиляции прошивка весит 202 байта, работает только на передачу, частота чипа 1МГц, битрейт 1200 бод. Для увеличения битрейта используйте пропорциональный делитель для регистра OCR0A. Для подключения USB-TTL преобразователя к работающему чипу, нужно соединить "землю" преобразователя и чипа, а также подключить RX линию преобразователя к TX линии чипа. Питание преобразователя подключать не надо!
Результат работы: