Программный драйвер LCD (аппнота от Renesas)

Народ, я тут как-то недавно писал про программный драйвер для LCD от мультиметра. Но там-то всё было просто - там у LCD был один бэкплейн и задача решалась в лоб.

Гораздо интереснее, когда бэкплейнов несколько. Оно, конечно, всегда можно взять специализированную микросхему - драйвер или же просто микроконтроллер у которого такой драйвер есть на борту (например, ATmega329P), но хотелось попробовать сделать программно.

Ну, взял (из той же коробки-некрополя для безвременно почивших девайсов) сломанную 3D ручку и выпаял оттуда крохотный дисплейчик всего на три цифры, но аж с четырьмя бэкплейнами (фото этого чуда будет ниже). Такие же стоят обычно в паяльниках и вообще в устройствах типа “ручки”.

У дисплея 10 настоящих ножек (а не резинка), правда шаг меньше, чем 2,54, пришлось переходник делать для макетки. Вызвонил распиновку. Получилось вот так:

image

Ну, что - мултиплексируем! Берём временной фрейм не более, чем 20мс (чтобы частота обновления экрана была не меньше, чем 50Гц), делим его на восемь (количество бэкплейнов, умноженное на два) равных частей (“фаз”). В первой фазе выставляем первый бэклейн в HIGH, остальные

оставляем посередине между HIGH и LOW

для этого к ножке подводим два резистора по 10к каждый. Один резистор соединяем с питанием, а второй с землёй, а соответствующий пин переводим в режим INPUT)

Во второй фазе - второй бэкплейн в HIGH, остальные посередине, в третьей - третий и т.д.

В фазах с 5-ой по восьмую, делаем всё также, только вместо HIGH выставляем соответствующий бэкплейн в LOW.

Получается, что бэкплейны ведут себя вот так:

image

Первая фаза - первый в HIGH, остальные посерёдке;
Вторая фаза - второй в HIGH, остальные посерёдке;

Пятая фаза - первый в LOW, остальные посерёдке;
Шестая фаза - второй в LOW, остальные посерёдке;

Бакплейн, который на данной фазе выставлен в HIGH или в LOW будем называть “активным”.

На каждой фазе выставляем пины сегментов, которые не должны светиться в тот же уровень, что и у активного бэкплейна, а пины тех сегментов, которые должны светиться - в уровень противоположный уровню активного бэкплейна.

Собственно, практически всё.

На самом деле, если глянуть на распиновку, то видно, что на одном из бэкплейнов пины 6, 8 и 10 не задействованы. На соответствующих фазах эти пины тоже будем оставлять “посерёдке”, для этого к ним тоже делаем делители напряжения, как и к пинам бэкпелйнов.

По сути получилось “динамическая индикация” или “мультиплексирование бэкплейнов”.

Все нормально работает, грех жаловаться, посмотрите на фото:

ecran

но есть нюанс. Изображение немного менее контрастно, чем если бы не было никакого мультиплексирования, а был бы один бэкплейн. Хотя, как видно на фото, контрастность вполне приемлемая, но … осадочек остался.

И тут мне попалась замечательная аппнота от компании 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

К сожалению, мне это не помогло. Наилучшая контрастность получается с минимальной мёртвой фазой и на глаз не отличается от той, что получается без мёртвых фаз вовсе.

Думаю, что всё же совет там правильный, просто на таком крохотном дисплейчике с таким маленьким количеством сегментов выгода от такой техники просто пока не видна. Но саму предложенную ими технику, мне кажется, стоит запомнить.

Впрочем, Вы видели фото, проблемы-то и нет - грех жаловаться.

2 лайка

по моим , вполне возможно устаревшим сведениям, гоняться за частотой жки бессмыслено, из за большого времени ответа сегментов, а так очень интересное сообщение, спасибо

Кстати, здесь, наверное, более правильно было бы делать не напрямую к пинам ардуины, а через сдвиговый регистр (два) с защёлкой, только такой, что 3-state понимал, разумеется. Это позволило бы переключать пины все одновременно (по защёлке).

Попался другой экран с четырьмя подложками, на этот раз от музыкального центра, вот такой:

И вот с ним-то подход из аппноты с “мёртвыми зонами” пригодился во весь рост. Без мёртвых зон контрастность была такой, что казалось, что я вывел все сегменты одновременно и лишь под некоторыми углами удавалось разглядеть что же выведено. Включил мёртвые зоны - контрастность хорошо изменяется. Например, на фото ниже - 40% живая зона, остальные 60% - мёртвая.

Т.е. методика, описанная в аппноте отлично работает для этого экрана.