Когда Texas Instruments оснастил свою знаменитую низким энергопотреблением серию MSP430 ферроэлектрической памятью, работать с ней стало еще легче и приятней, ведь такая память совмещает преимущества флэша и оперативной памяти. А новые лончпады с сегментным индикатором — просто готовая платформа для какой нибудь домашней автоматизации. А что если нужен графический дисплей? Тут на помощь приходят электронные чернила, ведь они потребляют энергию только при обновлении и способны годами радовать красивой контрастной картинкой, питаясь от одного комплекта батарей. Если вам интересен опыт программирования подобного устройства — добро пожаловать под кат. Я расскажу о том, что такое ферроэлектрическая память, и зачем она нужна, как добиться максимально низкого энергопотребления и получить красивую картинку на электронной бумаге и при этом воспользоваться на техасе кодом, написанным под ардуино.
Изучение серии MSP430 я начинал с первого 'народного' лончпада MSP-EXP430G2. Поэтому, когда в продаже появились лончпады MSP-EXP430FR4133 и MSP-EXP430FR6989 с перламутровыми пуговицами ферроэлектрической памятью и сегментным LCD, причем за почти ту же цену, я сразу загорелся идеей сделать на них какой нибудь красивый гаджет.
Чем хороша ферроэлектрическая память
В основе ферроэлектрической памяти лежит специальный материал — цирконат-титанат свинца (PZT). Он является сегнетоэлектриком, то есть способен к спонтанной поляризации. В структуре этого материала есть положительно заряженные атомы, 'запертые' в ячейке отрицательно заряженной кристаллической решетки, и способные смещаться относительно центра этой ячейки. Это смещение происходит под действием электрического поля и приводит к протеканию во внешней цепи электрического заряда. После снятия электрического поля ячейка остается поляризованной, что позволяет использовать этот эффект для хранения двоичной информации. Нули и единицы кодируются поляризацией PZT в противоположных направлениях. Единственная проблема — считывание возможно только при изменении направления поляризации, поскольку только так можно получить импульс тока во внешней цепи. То есть, чтение сопряжено со стиранием ранее записанной информации, и ее необходимо сразу же восстановить. Этот недостаток компенсируется тем, что для изменения направления поляризации требуется очень небольшое время — порядка наносекунды. Так что запись происходит гораздо быстрее, чем в традиционной флэш-памяти, и не требует предварительного стирания блоков. Фактически с такой памятью можно работать точно так же, как с оперативной, и при этом она сохраняет свое содержимое после выключения питания. Представим себе, что нам нужен буфер для хранения настроек или для того, чтобы уместить массив данных, который не помещается в оперативную память. Мы можем реализовать его следующим образом:
const uint8_t buff_holder[BUFF_SZ]; // статический буфер в FRAM
uint8_t* buff = (uint8_t*)buff_holder;
// работаем с buff как если бы это была оперативная память
Гораздо удобнее, чем возиться со стиранием блоков флэша не правда ли?
Боремся с обжорством
Первое, что я сделал, взяв в руки плату MSP-EXP430FR6989, — измерил потребляемый ею ток и был слегка удивлен, поскольку при остановленном процессоре ток составил около 80 микроампер. Я написал простейшую программу из двух строчек, смысл которой сводится к остановке процессора:
void main()
{
WDTCTL = WDTPW | WDTHOLD; // останавливаем сторожевой таймер
_bis_SR_register(LPM3_bits); // останавливаем процессор
}
Конечно оставлять порты ввода-вывода неинициализированными неправильно, По умолчанию они работают на ввод, и плавающий потенциал входа приводит к протеканию сквозного тока через транзисторы входного буфера. Нужно либо запрограммировать порты на вывод, либо подключить подтягивающие резисторы. Я реализовал второй вариант, опасаясь за подключенную к процессору периферию:
void main()
{
WDTCTL = WDTPW | WDTHOLD; // останавливаем сторожевой таймер
// подтягиваем к земле все порты
P1OUT = P2OUT = P3OUT = P4OUT = P5OUT = P6OUT = P7OUT = P8OUT = P9OUT = P10OUT = PJOUT = 0;
P1REN = P2REN = P3REN = P4REN = P5REN = P6REN = P7REN = P8REN = P9REN = P10REN = PJREN = ~0;
_bis_SR_register(LPM3_bits); // останавливаем процессор
}
Потребляемый ток уменьшился до 40 микроампер, что все равно в 10 раз больше результата, который демонстрировали процессоры MSP430 предыдущего поколения. В чем же дело? Оказывается, в новых процессорах реализована продвинутая схема контроля за качеством клока, которая с особым пристрастием следит за кварцевыми генераторами. Именно внешний кварцевый генератор выбран по умолчанию в качестве источника низкочастотного клока. Если он не подключен, или вы не настроили ножки, к которым он подключен, то взводится флаг ошибки низкочастотного клока, после чего запускается резервный внутренний RC генератор частоты 5MHz, это частота делится на 128 и используется в качестве низкочастотного клока. Именно этот генератор и потребляет 40 микроампер. Парадокс этой навязчивой заботы о качестве клока в том, что низкочастотный клок в данной ситуации не используется вообще!
Есть 2 способа борьбы с этим явлением. Во-первых, вы можете использовать внутренний RC генератор низкочастотного клока. Код для этого случая показан ниже:
void main()
{
WDTCTL = WDTPW | WDTHOLD; // останавливаем сторожевой таймер
// подтягиваем к земле все порты
P1OUT = P2OUT = P3OUT = P4OUT = P5OUT = P6OUT = P7OUT = P8OUT = P9OUT = P10OUT = PJOUT = 0;
P1REN = P2REN = P3REN = P4REN = P5REN = P6REN = P7REN = P8REN = P9REN = P10REN = PJREN = ~0;
CSCTL0_H = CSKEY >> 8; // разблокируем регистры управления клоком
CSCTL2 = SELA__VLOCLK | SELS__DCOCLK | SELM__DCOCLK; // используем внутренние генераторы для всех клоков
_bis_SR_register(LPM3_bits); // останавливаем процессор
}
Если вам важна точность тактовой частоты (например для часов), то этот способ не подходит. Вам реально нужен низкочастотный кварцевый генератор. Вы можете добавить цикл, который будет ждать стабилизации клока и очищать флаги ошибок, не забыв предварительно настроить ножки, к которым подключен кварц:
void main()
{
WDTCTL = WDTPW | WDTHOLD; // останавливаем сторожевой таймер
// подтягиваем к земле все порты
P1OUT = P2OUT = P3OUT = P4OUT = P5OUT = P6OUT = P7OUT = P8OUT = P9OUT = P10OUT = PJOUT = 0;
P1REN = P2REN = P3REN = P4REN = P5REN = P6REN = P7REN = P8REN = P9REN = P10REN = PJREN = ~0;
PJSEL0 = BIT4 | BIT5; // настроим ножки кварца
CSCTL0_H = CSKEY >> 8; // разблокируем регистры управления клоком
CSCTL4 &= ~LFXTOFF; // включаем низкочастотный генератор
do {
CSCTL5 &= ~LFXTOFFG; // очищаем флаг ошибки низкочастотного резонатора
SFRIFG1 &= ~OFIFG; // очищаем флаг прерывания
} while (SFRIFG1 & OFIFG); // проверяем флаг прерывания
_bis_SR_register(LPM3_bits); // останавливаем процессор
}
Проблема этого решения в том, что если в процессе работы из-за электромагнитных помех, к примеру, снова возникает ошибка низкочастотного генератора, она уже никак не обрабатывается, вследствие чего ваши часы пойдут неправильно и будут активно кушать батарею. Альтернативный вариант, который работает всегда — обработать ошибки в прерывании:
void main()
{
WDTCTL = WDTPW | WDTHOLD; // останавливаем сторожевой таймер
// подтягиваем к земле все порты
P1OUT = P2OUT = P3OUT = P4OUT = P5OUT = P6OUT = P7OUT = P8OUT = P9OUT = P10OUT = PJOUT = 0;
P1REN = P2REN = P3REN = P4REN = P5REN = P6REN = P7REN = P8REN = P9REN = P10REN = PJREN = ~0;
PJSEL0 = BIT4 | BIT5; // настроим ножки кварца
CSCTL0_H = CSKEY >> 8; // разблокируем регистры управления клоком
CSCTL4 &= ~LFXTOFF; // включаем низкочастотный генератор
SFRIE1 = OFIE; // разрешаем прерывание по ошибке генератора
for (;;) {
_bis_SR_register(LPM3_bits + GIE); // останавливаем процессор, разрешаем прерывания
}
}
#pragma vector=UNMI_VECTOR
__interrupt void UNMI_ISR(void)
{
do {
CSCTL5 &= ~LFXTOFFG; // очищаем флаг ошибки низкочастотного резонатора
SFRIFG1 &= ~OFIFG; // очищаем флаг прерывания
} while (SFRIFG1 & OFIFG); // проверяем флаг прерывания
}
Теперь плата в режиме останова процессора потребляет 0.9 микроампера с кварцевым генератором и 0.6 без него, что почти на порядок лучше результата предыдущего поколения MSP430!
Управление имеющимся на плате сегментым LCD благодаря предоставленным TI библиотекам не представляет проблемы, но и оно преподнесло несколько сюрпризов. Во-первых, код инициализации LCD, предоставленный TI, инициализирует больше сегментов, чем их есть на самом деле. В результате на многих выводах разъема расширения присутствовало переменное напряжение со средним значением близким к половине питающего напряжения. Во-вторых, LCD потребляет неоправданно много — около 40 микроампер. Но, как известно, чтобы корова меньше ела, ее всего лишь нужно реже кормить. Уменьшение частоты тактового сигнала LCD позволило уменьшить потребляемый ток до 6.5 микроампер.
Подключаем экран
В качестве графического дисплея был выбран экран на электронных чернилах. На страничке производителя вы найдете массу библиотек, но как водится, не тех, что нужно. Библиотеки, предоставленные производителем экрана, вообще производят тяжелое впечатление, поскольку сделаны с расчетом на удобство написания и представляют собой многослойный пирог, производительность которого будет приемлема только на чем-то очень быстром, вроде RaspberryPi. Нашлась, однако, и адаптированная версия под ардуино, где весь этот слоеный пирог утрамбовали в один модуль. С него и решено было начать за неимением лучшего и нежеланием изобретать очередной велосипед. Сначала я просто реализовал функции доступа к портам вроде digitalWrite / digitalRead в самом общем виде. Когда я увидел вместо ожидаемой картинки медленно проступающие на экране мутные разводы, моим первым желанием было отправить экран обратно. Однако, по здравому разумению, я решил, что что-то здесь не так. Сначала я ускорил процесс проявления мутных разводов, слегка оптимизировав код — сделал все функции ввода-вывода инлайн. При этом я просто положился на то, что все нужные ножки у меня будут на одном порту, так что ардуинский номер ножки — это соответствующий ей бит. Работа с SPI тоже стала инлайном, результат можно увидеть тут. По ходу дела я с удивлением узнал, что в ардуине SPI порт может быть только один — отсюда последние 2 строчки в файле по ссылке выше. Оптимизированный вариант рисовал разводы значительно быстрее, причем выглядели они уже не такими грязными, как раньше. Тогда я пошел еще дальше и повысил тактовую частоту процессора до 8MHz — картинка стала практически идеальной. Видимо контроллер дисплея не имеет ни своей памяти, ни тактового генератора, а просто использует входные сигналы для генерации необходимых ему напряжений. Соответственно, если передача данных идет слишком медленно, эти питающие напряжения оказываются меньше, чем необходимо для обновления. Процесс обновления иллюстрирует следующий видео ролик.
Последнее, что необходимо сделать с экраном, если вы собираетесь питать его от батареи, — удалить лишние детали, установленные на плате контроллера дисплея на всякий случай в качестве бонуса. Датчик температуры U6 потребляет 100 микроампер. Этот ток можно уменьшить лишь послав ему команду выключения, но мы же не собираемся только ради этого подключать его к I2C порту. Гораздо проще перерезать провод питания. Флэш память U5 потребляет еще 5 микроампер, я справился с этим, отпаяв ее целиком. Теперь вся система с работающими часами реального времени потребляет 10 микроампер. Экран при обновлении потребляет 10 миллиампер в течение 5 секунд. При емкости батареи 1 А*час (типичное значение для AA формата) ее хватит на 10 лет, при обновлении экрана раз в полтора часа — на 5 лет. Осталось только нарисовать на нем что-то осмысленное, но об этом в следующий раз.
Исходный код
Лежит тут. Для компиляции использовался IAR Embedded Workbench for MSP430 версии 6.40.1. Никаких дополнительных библиотек не требуется.
Автор: oleg_v