Как я протезировал индикатор 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;
}


© Geektimes