Как я протезировал индикатор UPS
В конце 90-х появился у меня UPS. Красивый, с LED индикатором и кучей кнопок, имел внутри два аккумулятора и мог поддерживать жизнь моего тогдашнего компьютера (вместе с монитором!) аж целых 15 минут. Времена на Камчатке тогда были тяжелые, свет выключали регулярно, поэтому данный прибор был весьма кстати. Я прошел вместе с ним весь энергетический кризис, и не раз он спасал мои курсовые от внезапного пропадания электричества. А еще, к нему можно было подключить магнитофон и при свете свечи слушать радио или любимые кассеты, готовя себе ужин на портативной газовой плитке…
Естественно, UPS ломался. В первый раз у него сгорел трансформатор. Не тот, который большой и стоит в инверторе, а какой-то маленький, наверное, для измерения напряжения в сети. Не найдя такой же заводской, я поставил самопальный, и девайс еще какое-то время поработал. Затем перестал. Долго, очень долго, я не мог найти причину. Приходилось выпаивать разные детали, проверять их на работоспособность и впаивать обратно. Проблема не находилась. Распотрошенный прибор пару лет провалялся под кроватью, пока, в один прекрасный день, мне не пришла в голову идея подать 5 вольт непосредственно на контроллер. И о чудо: раздался писк встроенного динамика и на LED индикаторе появились цифры. Он был жив! Дальше — дело техники: прошелся по цепи питания вольтметром и выяснил, что на плате был распаян плавкий предохранитель, наглым образом выглядевший как резистор! Предохранитель (естественно, сгоревший), был заменен и UPS ожил.
К сожалению, мой ремонт и два года под кроватью не прошли для прибора даром. Каким-то непостижимым образом в контроллере сгорел порт, который отвечал за свечение зеленого светодиода «On-Line» и самого нижнего сегмента всех цифровых сегментных индикаторов. Тут уж ничего не поделаешь — пришлось смириться. Через некоторое время я уехал с Камчатки и наши пути разошлись.
Прошли годы, и приехав в гости к родителям в дальнем углу я обнаружил свой любимый бесперебойник: заброшенный, грязный, без аккумуляторов и резиновых ножек. К тому времени я уже обзавелся собственным жильем в другом городе, поэтому было принято решение забрать приборище к себе, восстановить его работоспособность и использовать по прямому назначению.
Задача
В первую очередь, UPS«ник был вымыт и высушен. Затем, в неком магазине радиодеталей были куплены подходящие резиновые ножки и новые аккумуляторы. К большому своему удивлению, в этом же магазине был найден подходящий трансформатор, взамен моей самоделки. Пару дней работы, и отмытый, обновленный бесперебойник радостно пискнул и начал заряжать свои новые аккумуляторы. Все было хорошо, но индикатор по прежнему оставался не рабочим.
Идея починить его приходила мне и раньше. Нарисовав на тетрадном листке все цифры (и кое-какие буквы) семи сегментного индикатора, я понял, что определить состояние самого нижнего сегмента возможно по состоянием остальных. Зеленый же светодиод можно зажигать тогда, когда не горят другие светодиоды. Мыслей, как это можно было бы сделать имелось много: от простой микросхемы ПЗУ до простенькой FPGA. Но, поскольку я был студентом и жил на Камчатке, то не имел возможности приобрести что-нибудь сложнее мелкой логики. Починка индикатора откладывалась.
В этот раз я решил заняться проблемой всерьез. Порывшись в закромах, я снова не нашел у себя ни ПЗУ, ни FPGA и какого-нибудь CPLD. Зато, под руки попала Adrduino Nano, вернее, его дешевый китайский клон с Ali Express. Ардуину я купил для того, чтобы сделать мини-компьютер на базе WiFi SD карточки от Transcend. К сожалению, карточка померла во время экспериментов, и плата с микроконтроллером осталась лежать без дела. Ничего, мы нашли ей новую задачу!
Работа
В дисплейном модуле реализована динамическая индикация: сигналы сегментов общие для всех четырех индикаторов так, что в один момент времени светится только один из них. Более того: как-бы пятым индикатором подключены еще и три светодиода. Пять сигналов выбора позволяют указать, какой именно индикатор (или линейка светодиодов) используется сейчас. Эти сигналы выбора последовательно сканируются с довольно высокой скоростью и, благодаря инертности зрения, кажется, что горят все индикаторы одновременно.
Вначале я хотел обойтись самым простым решением: обыкновенный цикл, который проверяет сигналы c шести рабочих сегментов, и включает или выключает не рабочий седьмой. Фактически это просто эмуляция ПЗУ, про которую я думал в самом начале.
Для этого, мне пришлось подключить шесть рабочих сегментом ко входу микроконтроллера, а нерабочий сегмент — к выходу.
Набросав небольшой массивчик, который сопоставлял различные состояния входов с выходом и цикл, который обходит это массив, я загрузил все в контроллер и сразу же получил проблему: нижний сегмент светился всегда. Первая мысль: косяк в программе. Однако, сколько я ни смотрел на код, никаких ошибок не находилось. В конце концов пришло понимание, что мой цикл никак не синхронизирован с переключением индикаторов. Если мы прочитаем состояние сегментов в конце цикла выбора одного индикатора, то велика вероятность, что нижний сегмент мы зажгем или погасим уже у следующего. Непорядок.
Недолго думая я припаял пять сигналов выбора индикатора к оставшимся свободным входам Arduino, настроил их на генерацию прерывания и стал использовать обработчик прерывания вместо цикла. Лучше стало, но проблему не решило. В нужных местах сегмент горел как надо, но в тех местах, где он должен был погашен, оставалось не яркое остаточное свечение.
Подумав еще какое-то время, я решил, что данный эффект может появляться, если цикл поиска в массиве нужного состояния для сегментов занимает больше времени, чем время горения индикатора. В этом случае мы тоже вылезаем из своей фазы и управляем сегментом уже следующего индикатора. Необходимо чтобы между моментом получения прерывания от сигнала выбора до команды управления сегментом прошло как можно меньше времени. Сделать это можно было только единственным образом: вынести код, принимающий решение о состоянии сегмента из обработчика прерывания, запустить его в основном цикле с минимальным приоритетом, а результат сохранять в неком глобальном буфере. Обработчику прерывания останется только прочитать значение их этого глобального буфера и погасить или зажечь сегмент, в зависимости от его содержимого. В самом плохом случае, мы можем только опоздать с изменением состояния сегмента в определенном индикаторе, но уже не залезем на соседний.
Это было правильным решением. Но окончательно все заработало только после того, как я синхронизировал цикл принятия решения с прерыванием с помощью spin-блокировки и запретил обработку прерывания во время работы этого цикла. И оно не просто заработало, а заработало так как надо!
С индикаторами оставалась еще одна проблема: большую часть времени они показывали только цифры. Однако, после включения UPSзапускался процесс тестирования, во время которого, помимо цифр, появлялись еще и два слова: TEST и PASS. И если, буквы T, E и P можно было просто добавить в массив допустимых символов, а S — была аналогична 5-ки, то буква A ничем, с точки зрения моей программы, не отличалась от восьмерки. Цикл принятия решений просто находил соответствующий шаблон в массиве и дорисовывал нижний сегмент. Нужно было что-то придумать, чтобы этого избежать.
И я придумал. Во время прихода сигнала о смене индикатора, нужно определять, к какому индикатору он относится и сохранять состояния его сегментов в специально отведенную для него переменную. Теперь, в любой момент времени я достаточно точно могу определить текущее содержимое сразу всех четырех индикаторов. И если на первом, третьем и четвертом отображаются символы P, 5 и 5 соответственно, то вторым символом однозначно является A, и нижний сегмент зажигать не нужно. На всякий случай, я еще добавил обработку слова FAIL, которое я ни разу не видел, но возможность появления которого предполагал.
Все, с цифровым индикатором покончено. Осталось только починить зеленый светодиод «On-Line». Но тут меня ожидал сюрприз… Идея была такая: зеленый светодиод (On-Line) всегда горит один. Если горят желтый (Battery) или красный (Low Battery) светодиоды, то зеленый гореть не должен. Таким образом, подпаиваем к микроконтроллеру провода от этих светодиодов, ставим простой if () с логическим «OR» и все должно заработать. Но выяснилось, что, когда горит желтый светодиод, он, на самом деле, не горит постоянно, а быстро мигает. Быстро, но недостаточно, чтобы if () это пропустил и не зажег зеленый светодиод. Получалось, что при работе от сети, зеленый светодиод горит в полую яркость, но при переключении на батарею, он горел в половину яркости, но все-равно горел. Ну, не проблема, подумал я, поставлю простенький низкочастотный фильтр: буду отсекать все быстрые мигания, но оставлю только медленные, которые соответствуют переходу на батарею и обратно. Анализ времени мигания желтого светодиода принес следующую неожиданность: период импульсов, которые на него подаются, весьма нестабилен и может достигать доволно больших величин. Получалось, что фильтр должен пропускать сигналы никак не выше 0.5–1 гц. Это не есть очень хорошо, поскольку получаем довольно большую задержку в изменении сигнала On-Line, но вполне терпимо.
Фильтр я решил сделать очень простой. Мы 50 раз, в равные промежутки времени, отслеживаем состояние желтого и красного светодиодов. Если один из них горит, то увеличиваем на единицу специальный счетчик. Затем, проверяем значение счетчика, и если, контрольные светодиоды горят в течении 50% проверяемого времени, то считаем, что он включен, а если меньше, то — выключен. В процессе отладки, пришлось уменьшить эту цифру до 10%. Почему — так и не разобрался.
И все заработало! Оставалось только красиво смонтировать плату Arduion Nano в корпусе UPS с помощью двухстороннего скотча и клеящего пистолета.
Для любопытных:
#include
#include
#include
#define AVG_TIME 50
#define THS_TIME 45
#define SD_SEG_A _BV(0)
#define SD_SEG_B _BV(1)
#define SD_SEG_C _BV(2)
#define SD_SEG_D _BV(3)
#define SD_SEG_E _BV(4)
#define SD_SEG_F _BV(5)
#define SD_SEG_G _BV(6)
#define SD_LED_RED SD_SEG_A
#define SD_LED_YLW SD_SEG_C
#define LD_SEL_LED _BV(0)
#define LD_SEL_1 _BV(1)
#define LD_SEL_2 _BV(2)
#define LD_SEL_3 _BV(3)
#define LD_SEL_4 _BV(4)
#define GET_SEL (PINC & 0x1f)
#define SD_SYM_NONE (0)
#define SD_SYM_0 (SD_SEG_A | SD_SEG_B | SD_SEG_C | SD_SEG_D | SD_SEG_E | SD_SEG_F)
#define SD_SYM_1 (SD_SEG_B | SD_SEG_C)
#define SD_SYM_2 (SD_SEG_A | SD_SEG_B | SD_SEG_D | SD_SEG_E | SD_SEG_G)
#define SD_SYM_3 (SD_SEG_A | SD_SEG_B | SD_SEG_C | SD_SEG_D | SD_SEG_G)
#define SD_SYM_4 (SD_SEG_B | SD_SEG_C | SD_SEG_F | SD_SEG_G)
#define SD_SYM_5 (SD_SEG_A | SD_SEG_C | SD_SEG_D | SD_SEG_F | SD_SEG_G)
#define SD_SYM_6 (SD_SEG_A | SD_SEG_C | SD_SEG_D | SD_SEG_E | SD_SEG_F | SD_SEG_G)
#define SD_SYM_7 (SD_SEG_A | SD_SEG_B | SD_SEG_C)
#define SD_SYM_8 (SD_SEG_A | SD_SEG_B | SD_SEG_C | SD_SEG_D | SD_SEG_E | SD_SEG_F | SD_SEG_G)
#define SD_SYM_9 (SD_SEG_A | SD_SEG_B | SD_SEG_C | SD_SEG_D | SD_SEG_F | SD_SEG_G)
#define SD_SYM_E (SD_SEG_A | SD_SEG_D | SD_SEG_E | SD_SEG_F | SD_SEG_G)
#define SD_SYM_P (SD_SEG_A | SD_SEG_B | SD_SEG_E | SD_SEG_F | SD_SEG_G)
#define SD_SYM_T (SD_SEG_D | SD_SEG_E | SD_SEG_F | SD_SEG_G)
#define GET_SYM (~PIND & 0x7f)
#define BROKEN_SEG (SD_SEG_D)
static uint8_t sd_symbols[] = { // list of known symbols
SD_SYM_NONE,
SD_SYM_0, SD_SYM_1, SD_SYM_2, SD_SYM_3, SD_SYM_4,
SD_SYM_5, SD_SYM_6, SD_SYM_7, SD_SYM_8, SD_SYM_9,
SD_SYM_E, SD_SYM_P, SD_SYM_T
};
volatile static uint8_t sel, symbol; // current input signals
volatile static short fb0, fb1, fb2, fb3, fb4; // display frame buffer
// display routine
ISR(PCINT1_vect) {
sel = GET_SEL;
symbol = GET_SYM;
if (((sel & LD_SEL_LED) && fb0) ||
((sel & LD_SEL_1) && fb1) ||
((sel & LD_SEL_2) && fb2) ||
((sel & LD_SEL_3) && fb3) ||
((sel & LD_SEL_4) && fb4)){
PORTD &= ~BROKEN_SEG;
}
else {
PORTD |= BROKEN_SEG;
}
}
//
// entry point
//
int main(void)
{
int cur_time;
int led_on_time;
uint8_t last_symbol_1, last_symbol_2, last_symbol_3, last_symbol_4;
int i;
// setup GPIO ports
DDRC = 0;
DDRD = BROKEN_SEG;
// setup pin change interrupt
PCICR |= _BV(PCIE1);
PCMSK1 |= _BV(PCINT8) | _BV(PCINT9) | _BV(PCINT10) | _BV(PCINT11) | _BV(PCINT12);
cur_time = 0;
led_on_time = 0;
last_symbol_1 = last_symbol_2 = last_symbol_3 = last_symbol_4 = 0;
fb0 = fb1 = fb2 = fb3 = fb4 = 0;
while(1) {
// sync with display strobe
sei();
while (sel == 0) {}
cli();
// if select one of segment indicator
if (sel & (LD_SEL_1 | LD_SEL_2 | LD_SEL_3 | LD_SEL_4)) {
// looking for displayed symbol
for (i = 0; i < 14; i++) {
uint8_t sd_symbol = sd_symbols[i];
if ((symbol & ~BROKEN_SEG) == (sd_symbol & ~BROKEN_SEG)) {
short val;
if (sd_symbol & BROKEN_SEG) {
val = 1;
} else {
val = 0;
}
if (sel & LD_SEL_1) {
last_symbol_1 = sd_symbol;
fb1 = val;
} else if (sel & LD_SEL_2) {
last_symbol_2 = sd_symbol;
fb2 = val;
} else if (sel & LD_SEL_3) {
last_symbol_3 = sd_symbol;
fb3 = val;
} else if (sel & LD_SEL_4) {
last_symbol_4 = sd_symbol;
fb4 = val;
}
// PASS workaround
if ((last_symbol_1 == SD_SYM_P) &&(last_symbol_2 == SD_SYM_8) &&
(last_symbol_3 == SD_SYM_5) && (last_symbol_4 == SD_SYM_5)) {
fb2 = 0;
}
// FAIL workaround
else if ((last_symbol_1 == SD_SYM_E) &&(last_symbol_2 == SD_SYM_8) &&
(last_symbol_3 == SD_SYM_1) && (last_symbol_4 == SD_SYM_1)) {
fb1 = 0;
fb2 = 0;
fb4 = 1;
}
break;
}
}
}
// if select LED line
else if (sel & LD_SEL_LED) {
if (cur_time++ > AVG_TIME) {
if (led_on_time < THS_TIME) {
fb0 = 0;
} else {
fb0 = 1;
}
cur_time = 0;
led_on_time = 0;
} else {
if ((symbol & (SD_LED_RED | SD_LED_YLW)) == 0) {
led_on_time++;
}
}
}
// reset sync flag
sel = 0;
}
return 0;
}