Разгон подсветки монитора +57


Оверклокинг процессора или памяти — это понятно, но зачем разгонять подсветку монитора?



Речь пойдёт о стареньком 23-дюймовом Samsung SyncMaster BX2340 (выпущен в январе 2011) со светодиодной подсветкой. Со временем стал замечать, что работать за ним утомительно, а сосредоточиться всё сложнее. И даже не только работать, просто читать, например. Сам монитор остался тот же, но мне стало труднее. А за другими экранами работалось вполне нормально.

Как-то в интернетах читал про субъективные ощущения пользователей телефонов с OLED дисплеями с частотой обновления 240 Гц. Жаловались на утомляемость и головную боль. И были упоминания (без пруфов) исследований по влиянию частоты диммирования подсветки на организм: хотя глаз не видит мерцания в 240 Гц, мозг на него реагирует. А постоянное свечение или с частотой более 3 кГц не нагружает мозг таким образом.

Затем на ютубе попался ролик про переделку подсветки монитора на постоянный ток. Вмешательство в схему было кардинальным. Под роликом были комментарии о смещении цветов при низких токах на сведиодах. А у меня подсветка работает на значениях 10-25%, т. к. помещение довольно тёмное. UPD: В ролике у автора была только одна гирлянда светодиодов, а у меня — 4.

Было решено оставить управление яркостью с помощью ШИМ, но увеличить частоту. Я даже не стал мерять мерцание неинвазивным методом с помощью фоторезистора или фотодиода, сразу разобрал монитор.



Контроллер подсветки — OZ9993CN. Нормального даташита не оказалось, только групповой драйверов подсветки производства O2Micro. Выяснилось, что драйвер занимается также и повышением напряжения (согласно измерениям с 14,4 В до 54,6 В) с использованием мощного внешнего полевого транзистора и индуктивности.

Одна из схем похожего по смыслу драйвера, номера выводов не совпадают:



На плате дорожка сигнала ШИМ на драйвер подписана как B-Dim (Backlight dimming?), искать не пришлось. Далее в дело вступил клон цифрового USB-осцилографа USBee AX в сочетании с sigrok на стороне ПК. Замер показал, что частота подсветки 180 Гц (маловато будет!). Высокий уровень сигнала — 5 В.



Теперь нужно как-то поднять частоту ШИМ до килогерцовых значений, раз в 16. Первое, что пришло в голову — влепить в разрыв дорожки ШИМ микроконтроллер для приёма сигнала и воспроизведения его в 16 раз ускоренном варианте. Нужны 2 таймера, один будет измерять длительность низкого и высокого уровней, другой — выдавать сигнал ШИМ. Подобрав коэффициенты предделителя, обойдёмся вообще без арифметики, просто копированием. Нет, Ардуино не будет. Ассемблера тоже не будет, будет GCС. Мелким МК с минимум двумя таймерами (из имеющихся в запасе) оказался ATtiny15. Но WinAVR не хочет с ним работать, поэтому пришлось взять более старшую версию — ATtiny45 (ATtiny25/85 так же подойдут).

Схема:

             100n
      --------++-------¬
      ¦ -------------¬ ¦
      ¦ ¦ 1        8 +-+- VCC
      ¦ ¦ 2        7 +- PB2 (INT0)  INPUT     
      ¦ ¦ 3        6 +- PB1 (OC1A)  OUTPUT
 GND -+-+ 4        5 ¦
        L-------------
           ATtiny45

Подбираем множители предделителей таймеров. Частоту CPU возьмём примерно 8 МГц, от встроенного RC-генератора.

  • Измерительный таймер. Сколько тактов в периоде диммирования? $8000000 / 180 \approx 44444$. Чтобы это влезло в восьмибитный регистр таймера с минимальной потерей точности, предделитель возьмём 256, максимальное значение счётчика будет $8000000 / 180 / 256 \approx 173,6$.
  • Таймер ШИМ. Частоту сделаем в 16 раз больше: $180 \cdot 16 = 2880 Гц$, тогда предделитель во столько же раз меньше: $256 / 16 = 16$.

Входной сигнал заведён на ножку внешнего прерывания. Обработчик оного:

/* External Interrupt 0 */
ISR(INT0_vect, ISR_NAKED) {
    uint8_t timer = TCNT0; // Значение таймера измерения интервалов
    if (PINB & 1<<PB2) { // Входящий сигнал. Высокий уровень - окончание цикла измерения
        OCR1C = timer; // Период ШИМ
        TCNT0 = 0; // Обнуление таймера измерения интервалов
    }
    else { // Низкий уровень - скважность
        OCR1A = timer; // Скважность ШИМ
    }
    reti();
}

Что за ISR_NAKED?
«ISR_NAKED» означает, что выкинуто сохранение/восстановление регистров и флагов процессора, это сделано для ускорения. Это можно сделать, удостоверившись, что в главном цикле они не затрагиваются (у нас там просто бесконечный цикл while(1) {}), и что не будет вызовов из подпрограмм. Ну и в конце прописывам возврат из функции с взведением флага разрешения прерываний reti().

Спаял, прошил — и оно заработало!



Но дроссель стал пищать. Смотрим, что там на затворе полевика, управляющим током через силовой дроссель:



С дросселем всё в порядке, он продолжает работать на частоте 320 кГц, но если раньше частота ШИМ была 180 Гц и почти не слышна (только если поднести ухо), то 2,9 кГц очень хорошо слышно. И комфорта явно не прибавилось. А что если вывести частоту за верхнюю границу слышимости? Например, $180 Гц \cdot 128 = 23040 Гц$? Меняем множитель предделителя таймера ШИМ с 16 на 2, прошиваем. Оказалось, что всё в порядке. Почти.

Восьмибитных таймеров в данном случае недостаточно, нужно больше минералов. Проявляется это в виде низкочастотных флуктуаций яркости, с плавным нарастанием и исчезновением периодичностью в несколько секунд. Чтобы справиться с этой напастью, можно взять кристалл пожирнее, но это не наш путь. Будем наращивать разрядность измеряющего таймера программным путём и введём порог (гистерезис) для надёжного обнаружения переключения яркости пользователем (0–100 с дискретностью 1). Точность измерительного таймера поднимем в 256 раз, и множитель предделителя становится равным 1.

Обработчик переполнения измерительного таймера с вариантом «что-то пошло не так и длительность уровня затянулась»:

/* Timer/Counter0 Overflow */
ISR(TIM0_OVF_vect, ISR_NAKED) {

    #define TIME_H_LIM (UCHAR_MAX-1)

    if (time_h < TIME_H_LIM) { // Normal way
        time_h += 1;
    }
    else { // High part overflowed
        if (PINB & 1<<PB2) {
            OCR1A = TIME_H_LIM; // Always on
        }
        else {
            OCR1A = 0; // Always off
        }
        OCR1C = TIME_H_LIM;
        time_h = 0;
        time_cycle = 0;
        time_on = 0;
    }
    reti(); // Because ISR_NAKED
}

Внешнее прерывание теперь обрабатывается тоже несколько сложнее:

/* External Interrupt 0 */
ISR(INT0_vect, ISR_NAKED) {

    // F_CPU / Timer1 prescaler / F_PWM_IN / grades / 4
    #define THRESHOLD (F_CPU / 1 / F_PWM_IN / 100 / 4)

    uint16_t time;
    uint8_t time_l = TCNT0;

    if ((TIFR & 1<<TOV0) && (time_l <= UCHAR_MAX/2)) { // Overflow occured right now
        time_l = UCHAR_MAX; // 0xff
    }
    time = (time_h << 8) + time_l;

    if (PINB & 1<<PB2) { // Risen
        if (abs(time - time_cycle) > THRESHOLD) {
            time_cycle = time;
            OCR1C = time_h;
        }
        TCNT0 = 0;
        time_h = 0;
        if (TIFR & 1<<TOV0) {
            TIFR = 1<<TOV0; // Clear Timer0 overflow flag
        }
    }
    else { // Falled
        if (abs(time - time_on) > THRESHOLD) {
            time_on = time;
            OCR1A = time_h;
        }
    }
    reti(); // Because ISR_NAKED
}

Появились глобальные переменные, которые я загнал в регистры, у нас же оверклокинг как-никак. SRAM используется только для сохранения адреса возврата при входе в обработчики прерываний. Старшая часть счётчика измерения интервалов находится в переменной time_h, а величины измеренной длины цикла ШИМ и скважности — в time_cycle и time_on соответственно. THRESHOLD — порог детекции изменения яркости.

Вот теперь всё заработало, как и задумывалось.

Полный код
/*
 PWM frequency multiplier x128

             100n
      --------++-------¬
      ¦ -------------¬ ¦
      ¦ ¦ 1        8 +-+- VCC
      ¦ ¦ 2        7 +- PB2 (INT0)  INPUT     
      ¦ ¦ 3        6 +- PB1 (OC1A)  OUTPUT
 GND -+-+ 4        5 ¦
        L-------------
           ATtiny45

fuses: lfuse=0xe2 hfuse=0xdf
*/
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <stdlib.h>
#include <limits.h>

#define F_CPU 8000000UL
#define F_PWM_IN 180U

register uint8_t time_h asm("r4"); // High part of time counter
register uint16_t time_cycle asm("r12"); // Period 
register uint16_t time_on asm("r14"); // H level duration

__attribute__((naked)) int main(void) {

    time_h = 0;
    time_cycle = 0;
    time_on = 0;

    ACSR |= 1<<ACD; // Comparator disable

    // Timer0
    TCCR0A = 0;
    // CK/1
    TCCR0B = 1<<CS00;

    // Timer1
    DDRB |= 1<<PB1; // PWM output
    // CK/2, Clear the OC1A output line
    TCCR1 = 1<<CTC1|1<<PWM1A|2<<COM1A0|2<<CS10;

    TIMSK |= 1<<TOIE0; // Timer0 overflow

    // Ext int 0
    MCUCR |= 1<<ISC00; // Any logical change on INT0 generates an interrupt request
    GIMSK |= 1<<INT0; // External Interrupt Request 0 Enable
    PORTB |= 1<<PB2; // Input

    wdt_enable(WDTO_120MS); // Watchdog on
    sei(); // Interrupts enable

    while (1) { // Do not use flags or registers
        wdt_reset(); // Watchdog reset
    }
}


/* External Interrupt 0 */
ISR(INT0_vect, ISR_NAKED) {

    // F_CPU / Timer1 prescaler / F_PWM_IN / grades / 4
    #define THRESHOLD (F_CPU / 1 / F_PWM_IN / 100 / 4)

    uint16_t time;
    uint8_t time_l = TCNT0;

    if ((TIFR & 1<<TOV0) && (time_l <= UCHAR_MAX/2)) { // Overflow occured right now
        time_l = UCHAR_MAX; // 0xff
    }
    time = (time_h << 8) + time_l;

    if (PINB & 1<<PB2) { // Risen
        if (abs(time - time_cycle) > THRESHOLD) {
            time_cycle = time;
            OCR1C = time_h;
        }
        TCNT0 = 0;
        time_h = 0;
        if (TIFR & 1<<TOV0) {
            TIFR = 1<<TOV0; // Clear Timer0 overflow flag
        }
    }
    else { // Falled
        if (abs(time - time_on) > THRESHOLD) {
            time_on = time;
            OCR1A = time_h;
        }
    }
    reti(); // Because ISR_NAKED

}

/* Timer/Counter0 Overflow */
ISR(TIM0_OVF_vect, ISR_NAKED) {

    #define TIME_H_LIM (UCHAR_MAX-1)

    if (time_h < TIME_H_LIM) { // Normal way
        time_h += 1;
    }
    else { // High part overflowed
        if (PINB & 1<<PB2) {
            OCR1A = TIME_H_LIM; // Always on
        }
        else {
            OCR1A = 0; // Always off
        }
        OCR1C = TIME_H_LIM;
        time_h = 0;
        time_cycle = 0;
        time_on = 0;
    }
    reti(); // Because ISR_NAKED
}


Можете называть это самовнушением, но результат такой: жить стало лучше, жить стало веселей! Даже сдвинулись давно зависшие проекты.

Если в вашем случае частота подсветки никак не влияет на самочувствие и продуктивность — считайте, что вам повезло. Наверное. Как и людям, уверяющим, что им абсолютно комфортно при содержании CO2 в помещении более 0,2% (2000 м. д.).

А как же ссылка на гитхаб?




К сожалению, не доступен сервер mySQL