Народ, я тут как-то недавно писал про программный драйвер для LCD от мультиметра. Но там-то всё было просто - там у LCD был один бэкплейн и задача решалась в лоб.
Гораздо интереснее, когда бэкплейнов несколько. Оно, конечно, всегда можно взять специализированную микросхему - драйвер или же просто микроконтроллер у которого такой драйвер есть на борту (например, ATmega329P), но хотелось попробовать сделать программно.
Ну, взял (из той же коробки-некрополя для безвременно почивших девайсов) сломанную 3D ручку и выпаял оттуда крохотный дисплейчик всего на три цифры, но аж с четырьмя бэкплейнами (фото этого чуда будет ниже). Такие же стоят обычно в паяльниках и вообще в устройствах типа “ручки”.
У дисплея 10 настоящих ножек (а не резинка), правда шаг меньше, чем 2,54, пришлось переходник делать для макетки. Вызвонил распиновку. Получилось вот так:
Ну, что - мултиплексируем! Берём временной фрейм не более, чем 20мс (чтобы частота обновления экрана была не меньше, чем 50Гц), делим его на восемь (количество бэкплейнов, умноженное на два) равных частей (“фаз”). В первой фазе выставляем первый бэклейн в HIGH, остальные
оставляем посередине между HIGH и LOW
для этого к ножке подводим два резистора по 10к каждый. Один резистор соединяем с питанием, а второй с землёй, а соответствующий пин переводим в режим INPUT)
Во второй фазе - второй бэкплейн в HIGH, остальные посередине, в третьей - третий и т.д.
В фазах с 5-ой по восьмую, делаем всё также, только вместо HIGH выставляем соответствующий бэкплейн в LOW.
Получается, что бэкплейны ведут себя вот так:
Первая фаза - первый в HIGH, остальные посерёдке;
Вторая фаза - второй в HIGH, остальные посерёдке;
…
Пятая фаза - первый в LOW, остальные посерёдке;
Шестая фаза - второй в LOW, остальные посерёдке;
…
Бакплейн, который на данной фазе выставлен в HIGH или в LOW будем называть “активным”.
На каждой фазе выставляем пины сегментов, которые не должны светиться в тот же уровень, что и у активного бэкплейна, а пины тех сегментов, которые должны светиться - в уровень противоположный уровню активного бэкплейна.
Собственно, практически всё.
На самом деле, если глянуть на распиновку, то видно, что на одном из бэкплейнов пины 6, 8 и 10 не задействованы. На соответствующих фазах эти пины тоже будем оставлять “посерёдке”, для этого к ним тоже делаем делители напряжения, как и к пинам бэкпелйнов.
По сути получилось “динамическая индикация” или “мультиплексирование бэкплейнов”.
Все нормально работает, грех жаловаться, посмотрите на фото:
но есть нюанс. Изображение немного менее контрастно, чем если бы не было никакого мультиплексирования, а был бы один бэкплейн. Хотя, как видно на фото, контрастность вполне приемлемая, но … осадочек остался.
И тут мне попалась замечательная аппнота от компании Renesas, которая так и называется “Software LCD Driver”
Там рассказывают, как это делается для одного бэкплейна (также, как мы и делали), как это делается для нескольких бэкплейнов (тоже, также, как мы и делали), а потом объясняется физика нашего “осадочка” и предлагается для, улучшения контрастности, каждую из наших фаз поделить ещё на две части. Первая часть - “активная фаза” обрабатывается также как у нас, а вторая - “мёртвая фаза” отрабатывает таким образом: в первой половине фрейма ВСЕ ПИНЫ (и бэкплейны, и сегменты) выставляются в HIGH), а во второй половине фрейма во время мёртвых фаз все пины выставляются в LOW. Это должно понизить RMS при включённом и выключенном сегментах (одинаково) и, за счёт нелинейности уровня контрастности, напряжения уйдут на более крутую часть кривой и контрастность улучшится. Чтобы мне не пересказывать все подробности, посмотрите рисунок 8 в аппноте и текст к нему.
Они говорят, что соотношение времени между “живой” и “мёртвой” фазами нуждается в подборе для каждого типа дисплея и каждого напряжения питания.
Я не стал искать их софт (он, наверняка, для их контроллеров и его пришлось бы переделывать чуть менее, чем полностью). Вместо этого я по быстрому, на коленке, написал драйвер для своего дисплейчика на Ардуино Нано по их идее. Переключение между “живой” и “мёртвой” фазами я сделал через ШИМ, а уровнем ШИМа управляю через потенциометр, который посадил на пин A7.
В шестой строке определяется константа NO_DEAD_PHASE. Если её определить как false, то будет алгоритм с коррекцией контрастности (с мёртвыми фазами). А если её определить как true, то будет, как я описывал выше, без мёртвых фаз. Я там использую свои “библиотеки”, которые мы тут уже обсуждали. Они, по-прежнему, живут вот здесь.
Код просто показывает числа от 0 до 999 и так по кругу, сменяя их раз в секунду. Если покрутить потенциометр, меняется контрастность (если NO_DEAD_PHASE == false, а если true, то потенциометр ни на что не влияет).
Текст, уж простите, не причёсывал, как сделал на коленке, так и выкладываю, извините.
Файл RenesasDriver.ino
#include <ConstTimers.h>
#include <Printing.h>
#include <PinOps.h>
#include <bitmask.h>
#define NO_DEAD_PHASE false
#include "PinsAndSegements.h"
constexpr uint8_t totalBackPlanes = 4;
constexpr uint8_t totalAlivePhases = totalBackPlanes * 2;
constexpr uint8_t totalDeadPhases = totalAlivePhases;
constexpr uint8_t totalPhases = totalAlivePhases + totalDeadPhases;
//
// Один период ШИМ состоит из пары - "живая и мёртвая фазы".
// Значит, для того, чтобы экран обновлялся с частотой не менее 50Гц,
// период ШИМ должен быть не более, чем (20ms / totalAlivePhases)
// или частота ШИМ должна быть не меньше, чем (50 * totalAlivePhases)
//
constexpr uint16_t minFreq = 50*totalAlivePhases;
//
// Например, для 16МГц выбираем битрейт 15 (частота 488,3 Гц)
//
static constexpr uint8_t getBitRate(const uint8_t n) {
return (n<=2) ? n : (static_cast<float>(F_CPU) / (1ul << n) >= minFreq ? n : getBitRate(n - 1));
}
static constexpr uint16_t maxPWMTick = static_cast<uint16_t>((1ul << getBitRate(16)) - 1);
//
// Положим, 1000 тактов хватит на обработку прерываний с огромным запасом.
// Пересчитаем в проценты
//
static constexpr uint8_t percMin = (1000UL * 100) / maxPWMTick + 1;
static constexpr uint8_t percMax = 100 - percMin;
static volatile TScreen screen = number2Screen(0);
static inline void setActivePhase(uint8_t percentage) {
OCR1A = static_cast<uint16_t>(static_cast<uint32_t>(maxPWMTick) * percentage / 100);
}
static inline uint8_t getContrast(void) {
const uint8_t ca = (100ul * analogRead(contrastAdjust) + 512) / 1024;
return ca < percMin ? percMin : ca > percMax ? percMax : ca;
}
void setup(void) {
Serial.begin(115200);
allPinsOutput();
noInterrupts();
TCCR1B = bitMask(WGM13, WGM12);
TCCR1A = bitMask(WGM11);
TCCR1C = TCNT1 = OCR1A = OCR1B = 0;
ICR1 = maxPWMTick;
TIMSK1 = bitMask(OCIE1A, TOIE1);
setActivePhase(75);
TIFR1 = bitMask(OCIE1A, TOIE1);
TCCR1B |= bitMask(CS10);
interrupts();
}
void loop(void) {
static uint8_t contrast = 50;
const uint8_t newContrast = getContrast();
if (newContrast != contrast) {
setActivePhase(contrast = newContrast);
printVarLn(contrast);
}
static uint16_t counter = 0;
static uint32_t oldMillis = 0;
const uint32_t curMillis = millis();
if (curMillis - oldMillis >= 1000) {
oldMillis = curMillis;
counter = (counter + 1) % 1000;
cli();
screen = number2Screen(counter);
sei();
}
}
//
// Прерывание - начало "живой" фазы
//
ISR(TIMER1_OVF_vect) {
static uint8_t phase = 0;
//
// phase меняется от 0 до (totalAlivePhases - 1)
// при этом от 0 до (totalBackPlanes - 1) перебираются все бэкплейны и они ставтся в HIGH
// затем от totalBackPlanes до (totalAlivePhases - 1) - те же бэкплейны ставятся в LOW
// Выделяем отдельно bpIsHigh и "рабочую фазу" (от 0 до (totalBackPlanes - 1)) которая
// определит номер бэкплейна.
//
const bool bpIsHigh = phase < totalBackPlanes;
const uint8_t wPhase = (bpIsHigh) ? phase : (phase - totalBackPlanes);
const bool p1 = pinValue(wPhase, pin1, screen) ? !bpIsHigh : bpIsHigh;
const bool p7 = pinValue(wPhase, pin7, screen) ? !bpIsHigh : bpIsHigh;
const bool p9 = pinValue(wPhase, pin9, screen) ? !bpIsHigh : bpIsHigh;
//
const bool p6 = pinValue(wPhase, pin6, screen) ? !bpIsHigh : bpIsHigh;
const bool p8 = pinValue(wPhase, pin8, screen) ? !bpIsHigh : bpIsHigh;
const bool p10 = pinValue(wPhase, pin10, screen) ? !bpIsHigh : bpIsHigh;
switch(wPhase) {
case 0: PinOps::digitalWrite(bp0, bpIsHigh); break;
case 1: PinOps::digitalWrite(bp1, bpIsHigh); break;
case 2: PinOps::digitalWrite(bp2, bpIsHigh); break;
case 3: PinOps::digitalWrite(bp3, bpIsHigh); break;
}
pinDirections(wPhase);
PinOps::digitalWrite(pin1, p1);
PinOps::digitalWrite(pin7, p7);
PinOps::digitalWrite(pin9, p9);
if (wPhase < 3) {
PinOps::digitalWrite(pin6, p6);
PinOps::digitalWrite(pin8, p8);
PinOps::digitalWrite(pin10, p10);
}
phase = (phase + 1) % totalAlivePhases;
}
//
// Прерывание - начало "мёртвой" фазы
// В appnote от Renesas "Software LCD Driver" сказано, что
// в мёртвых фазах все пины должны быть HIGH в первой половине
// фрейма и LOW - во второй.
//
#if NO_DEAD_PHASE
EMPTY_INTERRUPT(TIMER1_COMPA_vect)
#else
ISR(TIMER1_COMPA_vect) {
static uint8_t phase = 0;
const bool mustBeHIGH = phase < totalBackPlanes;
if (mustBeHIGH) allPinsHIGH(); else allPinsLOW();
allPinsOutput();
phase = (phase + 1) % totalDeadPhases;
}
#endif
Файл PinsAndSegements.h
#ifndef PINSANDSEGMENTS_H
#define PINSANDSEGMENTS_H
///////////////////////////////////////////////
//
// тип данных для экрана
//
typedef __uint24 TScreen;
//////////////////////////////////////////////
//
// Прототипы функций
//
static void allPinsOutput(void) __attribute__((always_inline));
static void allPinsHIGH(void) __attribute__((always_inline));
static void allPinsLOW(void) __attribute__((always_inline));
static uint8_t & digit(const uint8_t n, TScreen scr) __attribute__((always_inline));
static TScreen number2Screen(const uint16_t num, const bool leadingZeroes = false) __attribute__((always_inline));
static bool pinValue(const uint8_t bp, const uint8_t pin, const TScreen scr) __attribute__((always_inline));
static void pinDirections(const uint8_t phase) __attribute__((always_inline));
static constexpr uint16_t contrastAdjust = A7;
static constexpr uint8_t pin1 = 4;
static constexpr uint8_t pin2 = 5;
static constexpr uint8_t pin3 = 6;
static constexpr uint8_t pin4 = 7;
static constexpr uint8_t pin5 = 8;
static constexpr uint8_t pin6 = A1;
static constexpr uint8_t pin7 = A2;
static constexpr uint8_t pin8 = A3;
static constexpr uint8_t pin9 = A4;
static constexpr uint8_t pin10 = A5;
static constexpr uint8_t bp0 = pin2;
static constexpr uint8_t bp1 = pin3;
static constexpr uint8_t bp2 = pin4;
static constexpr uint8_t bp3 = pin5;
static inline void allPinsOutput(void) {
PinOps::pinMode(pin1, OUTPUT);
PinOps::pinMode(pin2, OUTPUT);
PinOps::pinMode(pin3, OUTPUT);
PinOps::pinMode(pin4, OUTPUT);
PinOps::pinMode(pin5, OUTPUT);
PinOps::pinMode(pin6, OUTPUT);
PinOps::pinMode(pin7, OUTPUT);
PinOps::pinMode(pin8, OUTPUT);
PinOps::pinMode(pin9, OUTPUT);
PinOps::pinMode(pin10, OUTPUT);
}
#define ALL_TOGETHER(level) \
static inline void allPins##level(void) { \
PinOps::digitalWrite(pin1, level); \
PinOps::digitalWrite(pin2, level); \
PinOps::digitalWrite(pin3, level); \
PinOps::digitalWrite(pin4, level); \
PinOps::digitalWrite(pin5, level); \
PinOps::digitalWrite(pin6, level); \
PinOps::digitalWrite(pin7, level); \
PinOps::digitalWrite(pin8, level); \
PinOps::digitalWrite(pin9, level); \
PinOps::digitalWrite(pin10, level); \
}
ALL_TOGETHER(HIGH)
ALL_TOGETHER(LOW)
static constexpr uint8_t allPins[] = { pin1, pin2, pin3, pin4, pin5, pin6 };
//////////////////////
//
// Сегменты
//
static constexpr uint8_t SEG_A = (1 << 0);
static constexpr uint8_t SEG_B = (1 << 1);
static constexpr uint8_t SEG_C = (1 << 2);
static constexpr uint8_t SEG_D = (1 << 3);
static constexpr uint8_t SEG_E = (1 << 4);
static constexpr uint8_t SEG_F = (1 << 5);
static constexpr uint8_t SEG_G = (1 << 6);
//////////////////////////////////////////////////////////////////////////////////
//
// Определение цифр и букв (какие есть)
//
static constexpr uint8_t SYMBOL_0 = (SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F);
static constexpr uint8_t SYMBOL_1 = (SEG_B | SEG_C);
static constexpr uint8_t SYMBOL_2 = (SEG_A | SEG_B | SEG_D | SEG_E | SEG_G);
static constexpr uint8_t SYMBOL_3 = (SEG_A | SEG_B | SEG_C | SEG_D | SEG_G);
static constexpr uint8_t SYMBOL_4 = (SEG_B | SEG_C | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_5 = (SEG_A | SEG_C | SEG_D | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_6 = (SEG_A | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_7 = (SEG_A | SEG_B | SEG_C);
static constexpr uint8_t SYMBOL_8 = (SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_9 = (SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G);
;
static constexpr uint8_t SYMBOL_MINUS = (SEG_G);
static constexpr uint8_t SYMBOL_A = (SEG_A | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_C = (SEG_A | SEG_E | SEG_F | SEG_D);
static constexpr uint8_t SYMBOL_E = (SEG_A | SEG_D | SEG_E | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_H = (SEG_B | SEG_C | SEG_E | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_P = (SEG_A | SEG_B | SEG_E | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_G = (SEG_A | SEG_E | SEG_F);
static constexpr uint8_t SYMBOL_L = (SEG_D | SEG_E | SEG_F);
static constexpr uint8_t SYMBOL_F = (SEG_A | SEG_E | SEG_F | SEG_G);
static constexpr uint8_t SYMBOL_d = (SEG_B | SEG_C | SEG_D | SEG_E | SEG_G);
static constexpr uint8_t SYMBOL_b = (SEG_C | SEG_D | SEG_E | SEG_F | SEG_G);
///////////////////////////////////////////////
//
// Массив всех цифр
//
static constexpr uint8_t allDigits[] = {
SYMBOL_0, SYMBOL_1, SYMBOL_2, SYMBOL_3, SYMBOL_4,
SYMBOL_5, SYMBOL_6, SYMBOL_7, SYMBOL_8, SYMBOL_9
};
static inline uint8_t & digit(const uint8_t n, TScreen scr) {
uint8_t *p = reinterpret_cast<uint8_t *>(& scr);
return p[n];
}
static inline TScreen number2Screen(const uint16_t num, const bool leadingZeroes) {
const uint8_t ones = allDigits[num % 10];
uint8_t tens = allDigits[num / 10 % 10];
uint8_t hundreds = allDigits[num / 100 % 10];
if (! leadingZeroes) {
if (hundreds == SYMBOL_0) hundreds = 0;
if (! hundreds && tens == SYMBOL_0) tens = 0;
}
return TScreen(
static_cast<TScreen>(ones) +
(static_cast<TScreen>(tens) << 8) +
(static_cast<TScreen>(hundreds) << 16)
);
}
static inline bool pinValue(const uint8_t bp, const uint8_t pin, const TScreen scr) {
static constexpr uint8_t pins2sements [4][2] = {
{ SEG_D, SEG_C },
{ SEG_E, SEG_G },
{ SEG_F, SEG_B },
{ 0, SEG_A }
};
uint8_t dig = 2;
if (pin == pin9 || pin == pin8) dig = 1;
else if (pin == pin1 || pin == pin10) dig = 0;
const uint8_t curDigit = digit(dig, scr);
const uint8_t pinN = pin == pin1 || pin == pin7 || pin == pin9;
const uint8_t mask = pins2sements[bp][pinN];
return curDigit & mask;
}
static inline void pinDirections(const uint8_t phase) {
switch(phase) {
case 0:
PinOps::pinMode(bp0, OUTPUT);
PinOps::pinMode(bp1, INPUT);
PinOps::pinMode(bp2, INPUT);
PinOps::pinMode(bp3, INPUT);
#if NO_DEAD_PHASE
PinOps::pinMode(pin6, OUTPUT);
PinOps::pinMode(pin8, OUTPUT);
PinOps::pinMode(pin10, OUTPUT);
#endif
break;
case 1:
PinOps::pinMode(bp0, INPUT);
PinOps::pinMode(bp1, OUTPUT);
PinOps::pinMode(bp2, INPUT);
PinOps::pinMode(bp3, INPUT);
break;
case 2:
PinOps::pinMode(bp0, INPUT);
PinOps::pinMode(bp1, INPUT);
PinOps::pinMode(bp2, OUTPUT);
PinOps::pinMode(bp3, INPUT);
break;
case 3:
PinOps::pinMode(bp0, INPUT);
PinOps::pinMode(bp1, INPUT);
PinOps::pinMode(bp2, INPUT);
PinOps::pinMode(bp3, OUTPUT);
//
// Пины 6, 8 и 10 на фазе 3 не нужны
// Они снова станут OUTPUT в ближайшую мёртвую фазу
//
PinOps::pinMode(pin6, INPUT);
PinOps::pinMode(pin8, INPUT);
PinOps::pinMode(pin10, INPUT);
break;
}
}
#endif // PINSANDSEGMENTS_H
К сожалению, мне это не помогло. Наилучшая контрастность получается с минимальной мёртвой фазой и на глаз не отличается от той, что получается без мёртвых фаз вовсе.
Думаю, что всё же совет там правильный, просто на таком крохотном дисплейчике с таким маленьким количеством сегментов выгода от такой техники просто пока не видна. Но саму предложенную ими технику, мне кажется, стоит запомнить.
Впрочем, Вы видели фото, проблемы-то и нет - грех жаловаться.