Динамическая индикация: экономим выводы МК

Семисегментный индикатор является самым простым и популярным средством вывода информации из девайса с не очень сложной логикой работы. Взгляните на электронные весы на рынке или на те же самые часы. Семисегментные светодиодные индикаторы, или LED-индикаторы, обладают хорошим углом обзора, при наличии светофильтра хорошей контрастностью и не требуют подсветки, так как сами они светятся. Кроме вывода информации, девайс скорее всего будет требовать ее ввода, например, цену за один кг вашего любимого печенья или текущее время по Москве. И тут на сцену выходят обычные механические кнопки, как самое простое и достаточно удобное решение для определенных задач. Поэтому каждый уважающий себя электронщик обязан уметь работать с этими устройствами ввода и вывода информации.

Рассмотрим самый обычный светодиод. Чем-то он похож на обычную лампочку накаливания — если через него пропустить ток, то он начнет излучать свет. На этом сходство заканчивается. У светодиода есть анод и катод, и для того, чтобы он начал излучать свет, необходимо анод подключить к плюсу источника тока, а катод к минусу. Если перепутать полярность, то ни чего страшного не произойдет (кроме отдельных случаев, которые мы сейчас рассматривать не будем), просто светодиод не будет светиться. Это, собственно, и демонстрирует следующая картинка, не первой схеме светодиод не горит, так как неправильно установлена батарейка, на второй это поправили:

Следует обратить внимание на токоограничивающий резистор R1 (или R2). Для чего он нужен, расскажу в одной из следующих статей.

Возьмем теперь восемь светодиодов и соединим их катоды вместе, а аноды от каждого оставим свободными. Или наоборот, аноды светодиодов соединим в одной точке, а катоды трогать не будем. Каждый светодиод обзовем своей буквой, а именно A, B, C, D, E, F, G и DP. Затем, расположим их так, как показано на рисунке и засунем в корпус:

Вот у нас и получился светодиодный семисегментный индикатор с десятичной точкой, которая обозначается как DP или H, с общим катодом или общим анодом.

Рассмотрим индикатор с общим катодом (Common Cathode). Чтобы вывести, например, цифру «1», нужно зажечь сегменты B и С (см. картинку выше). Для этого общий провод (тот, к которому все светодиоды подключены одним концом) цепляем на минус источника тока, а сегменты B и C на плюс, т.е. сегменты в таком индикаторе зажигаются плюсом источника тока. При этом остальные сегменты можно ни куда не подключать, либо повесить на минус. Таким образом, включая нужные сегменты, можно отобразить любую цифру от 0 до 9, некоторые символы и буквы.

 

Для индикаторов с общим анодом (Common Anode) все аналогично, за исключением того, что общий провод надо цеплять на плюс, а зажигать сегменты минусом.

Давайте теперь посчитаем ноги. Для управления одним семисегментным индикатором нужно 8 ног микроконтроллера (7 сегментов и 1 десятичная точка). 2 индикатора — 2*8=16 ног, 4 индикатора — 4*8=32. Как-то грустно… Для того, чтобы сделать электронные часы, нужно минимум 4-ре индикатора, а у AVR-ке в большом корпусе DIP-40 как раз 32 порта ввода-вывода. Даже кнопки прицепить некуда. Но выход есть! (нет, не в окно)

Возьмем для примера четыре индикатора и соединим их так, как показано на схеме. Правда здесь я взял индикаторы без десятичной точки, но это не особо важно.

Все одноименные сегменты соединены вместе, а общие выводы оставлены как есть. А как же теперь управлять этими индикаторами? На просторах интернета нашел 2 отличные gif-ки, которые отображают суть процесса:

То есть, на общем проводе интересующей нас цифре устанавливаем активный уровень (для Common Cathode подключаем к минусу, Common Anode к плюсу) и зажигаем нужные сегменты. Такой тип индикации называется динамический. Возникает вопрос: а как сделать, чтобы горели все цифры, а не только одна, да еще чтоб каждая показывала что-то свое? Ведь одновременно мы уже не модем зажигать все цифры в таком включении. Да, одновременно не можем, но это и не надо. Если мы будем очень быстро переключать цифры, как показано на gif-ках, то для глазу человека будет казаться, что все они горят одновременно! Уже при 25 герцах можно без проблем прочитать все значение целиком. А если сделать частоту 100 Гц, то проблемы вообще не будет.

Посчитаем теперь количество выводов микроконтроллера, которое необходимо задействовать в такой схеме включения индикаторов. На 2-е цифры на мнужно 8+2=10 выводов, 8 для сегментов (7, если нам не нужна десятичная точка) и 2 общих провода от каждой цифры; для 4-х цифр 8+4=12 выводов. То есть каждая дополнительная цифра прибавляет только один дополнительный вывод, а не 8. Промышленностью выпускаются готовые индикаторы на 2, 3, 4 и так далее цифр, имеющие внутри все необходимые соединения для динамической индикации, наружу торчит только то, что надо, самому что-то соединять нет необходимости. Красота!

Правда есть один небольшой минус. Если у нас 4-ре цифры, то каждая будет гореть только 1/4 времени от полного цикла обновления (эдакий ШИМ с заполнением 25%), что негативно сказывается на яркости индикации, поэтому, если стоИт задача получить максимально яркое свечение индикатора, этот способ не прокатит. Устранить эту проблему можно уменьшением номинала токоограничительных резисторов каждой группы сегментов, но тут нельзя выходить за максимальное импульсное значение силы тока через сегмент.

Так, с принципами разобрались, перейдем теперь к схемотехнике и программированию.

  • Исходные условия: в наличии имеем семисегментный индикатор на 4-ре цифры для динамической индикации и микроконтроллер AVR ATMega16.
  • Задача: подключить этот индикатор к AVR-ке, написать управляющую программу, которая будет иметь возможность вывода произвольного числового значения на дисплей.

Поехали. Для начала в Proteus-e нарисуем вот такую схему:

Проще некуда: сегменты подключены к выводам PD0-PD7 микроконтроллера, цифры к PC0-PC3.

Перейдем теперь в Atmel Studio. Создадим новый проект, выберем микроконтроллер atmega16.

Где-нибудь вначале файла напишем define-ы, которые будут определять, куда подключен наш индикатор

//Цифры поделючены к порту C микроконтроллера
#define DIGIT_PORT		PORTC
#define DIGIT_DDR		DDRC

//Сегменты к порту D
#define SEG_PORT		PORTD
#define SEG_PIN			PIND
#define SEG_DDR			DDRD


//К каким пинам подулючены цифры индикаторов
#define LED_DIGIT_0		(1<<0)
#define LED_DIGIT_1		(1<<1)
#define LED_DIGIT_2		(1<<2)
#define LED_DIGIT_3		(1<<3)

//К каким пинам подключены сегменты индикаторов
#define LED_SEG_A		(1<<0)
#define LED_SEG_B		(1<<1)
#define LED_SEG_C		(1<<2)
#define LED_SEG_D		(1<<3)
#define LED_SEG_E		(1<<4)
#define LED_SEG_F		(1<<5)
#define LED_SEG_G		(1<<6)
#define LED_SEG_H		(1<<7)

Теперь поговорим немного об алгоритме. Я предлагаю следующий. У нас будет функция, которая будет вызываться с частотой 400 Гц. В ней мы будем выполнять следующие действия:

  1. гасим все индикаторы;
  2. выводим на порт, к которому подключены сегменты, необходимое значение, которое должна отображать цифра с номером led_index;
  3. зажигаем цифру led_index;
  4. увеличиваем led_index на единицу;
  5. если led_index стал больше количества цифр индикатора, то обнуляем его.

Таким образом, мы будем перебирать все цифры индикатора с частотой 400 Гц, и так как у нас 4-е цифры, то частота обновление всего дисплея полностью будет составлять 100 Гц.

Напишем необходимые функции

uint8_t DigitMask[] =
{
	LED_DIGIT_0,
	LED_DIGIT_1,
	LED_DIGIT_2,
	LED_DIGIT_3
};

//Функция, которая гасит все цифры индикатора
void AllDigitsOff(void)
{
	//подаем на все общие выводы цифр лог. 1
	//так как у нас инликаторы с общим катодом
	//и зажигаются они нулем на общем проводе 
	//и 1 на сегментах
	DIGIT_PORT |= (LED_DIGIT_0 | LED_DIGIT_1 | LED_DIGIT_2 | LED_DIGIT_3);
}

//Включаем определенную цифру
void DigitOn(uint8_t dgt)
{
	//подаем ноль на общий провод 
	//цифре, с номером dgt
	DIGIT_PORT &= ~(DigitMask[dgt]);
}

//Это у нас буфер, в котором хранится информация
//о том, какие сегменты надо включить, а какие нет
//эдакая видеопамять, объемом 4 байта ;)
//кто-нибудь видел видеокарту
//с объемом памяти 4 БАЙТА?))
//Вот она!!!
uint8_t led_buffer[4];


//Эту функцию надо вызывать 400 раз в секунду
void DispProcess(void)
{
	//Счетчик цифр.
	//
	//Переменная внутри функции, 
	//объявленная как static,
	//не теряет своего значение после выхода
	//из функции
	static uint8_t led_index = 0;
	
	//гасим все, что горело
	AllDigitsOff();
	
	//устанавливаем активный уровень на нужных сегментах
	SEG_PORT = led_buffer[led_index];
	
	//зажигаем цифру
	DigitOn(led_index);
	
	//увеличиваем счетчик цифр на 1
	led_index++;
	
	//проверяем, не вышли ли мы за границы
	if(led_index > sizeof(led_buffer))
		led_index = 0;
}

//Тут производим все необходимые предварительные настройки
void DispInit(void)
{
	//Настройка портов
	DIGIT_DDR |= (LED_DIGIT_0 | LED_DIGIT_1 | LED_DIGIT_2 | LED_DIGIT_3);
	AllDigitsOff();
	SEG_DDR = 0xFF;
	
	//Инициализация таймера-счетчика 0
	//Устанавливаем предделитель на 1024
	TCCR0 = 1<<WGM01 | 0<<WGM00 |
	1<<CS02 | 0<<CS01 | 1<<CS00;
	
	//Регистр сравнения.
	//Частота процессора у нас 8000000Гц
	//Количество индикаторов - 4 шт
	//Желаемая частота обновления экрана - 100Гц
	//предделитель - 1024
	//8000000/1024/4/100 = 19,53125 ~= 20
	OCR0 = 20;
	
	//Разрешаем прерывания от таймера
	TIMSK |= 1<<OCIE0;
}

//прерывание от таймера 0
//возникает 400 раз в секунду
ISR(TIMER0_COMP_vect)
{
	DispProcess();
}

Ну и функция main:

int main(void)
{
	//Выполняем инициализацию системы
	DispInit();
	
	//глобально разрешаем прерывания
	sei();
	
	//заполняем буфер дисплея нужными символами
	led_buffer[0] = 0x5B; //символ "2". горят сегменты A, B, G, E и D
	led_buffer[1] = 0x3F; //"0",  сегменты A, B, C, D, E, F
	led_buffer[2] = 0x06; //"1", сегменты B и C
	led_buffer[3] = 0x07; //"7", сегменты A, B, C
	
	while (1)
	{
	}
}

Смотрим результат в симуляторе:

Все работает!

Однако, такой способ заполнения буфера дисплея не очень удобен, надо помнить, какие сегменты куда подключены и вообще, из каких сегментов состоит тот или иной символ. Давайте решим это неудобство одной простой функцией, которая на вход будет принимать 2 параметра: номер цифры и число от 0 до 9, которое мы хотим вывести.

//таблица с масками цифр и символов
//во времена самодельных ПК, типа 
//Радио86-РК
//такую таблицу называли знакогенератор
//мы ей на вход код символа,
//на выходе получаем его изображение
uint8_t LED_CharsTable[] =
{
	/*0:  0 */		(LED_SEG_A | LED_SEG_B | LED_SEG_C | LED_SEG_D | LED_SEG_E | LED_SEG_F),
	/*1:  1 */		(LED_SEG_B | LED_SEG_C),
	/*2:  2 */		(LED_SEG_A | LED_SEG_B | LED_SEG_G | LED_SEG_E | LED_SEG_D),
	/*3:  3 */		(LED_SEG_A | LED_SEG_B | LED_SEG_C | LED_SEG_D | LED_SEG_G),
	/*4:  4 */		(LED_SEG_F | LED_SEG_G | LED_SEG_B | LED_SEG_C),
	/*5:  5 */		(LED_SEG_A | LED_SEG_F | LED_SEG_G | LED_SEG_C | LED_SEG_D),
	/*6:  6 */		(LED_SEG_A | LED_SEG_F | LED_SEG_E |LED_SEG_D | LED_SEG_C | LED_SEG_G),
	/*7:  7 */		(LED_SEG_A | LED_SEG_B | LED_SEG_C),
	/*8:  8 */		(LED_SEG_A | LED_SEG_B | LED_SEG_C | LED_SEG_D | LED_SEG_E | LED_SEG_F | LED_SEG_G),
	/*9:  9 */		(LED_SEG_A | LED_SEG_B | LED_SEG_C | LED_SEG_D | LED_SEG_F | LED_SEG_G),
	/*10: Null*/    (0), //Индикатор погашен
	/*11: Minus*/	(LED_SEG_G) //Символ "-"
};

//функция, которая предоставляет удобный интерфейс вывода
//информации на индикатор
void LedSetVal(uint8_t digit, uint8_t val)
{
	led_buffer[digit] = LED_CharsTable[val];
}

 

Теперь main выглядит так:

int main(void)
{
	//Выполняем инициализацию системы
	DispInit();
	
	//глобально разрешаем прерывания
	sei();
	
	LedSetVal(0,1); //"1"
	LedSetVal(1,2); //"2"
	LedSetVal(2,3); //"3"
	LedSetVal(3,4); //"4"
	
	while (1)
	{
	}
}

 

Ну и демонстрация работы:

Теперь можно даже так:

//добавим где-нибудь перед main-ом
#define LED_CHAR_NULL	10
#define LED_CHAR_MINUS	11

//в main-е:

LedSetVal(0,LED_CHAR_NULL);
LedSetVal(1,LED_CHAR_MINUS);
LedSetVal(2,1);
LedSetVal(3,4);

Результат:

На этом пока все! В следующей статье разберемся с кнопками и посмотрим, как там можно сэкономить.

Архив с программой и моделями Proteus: https://yadi.sk/d/s-E3BAQL3NkGnz

 

Метки: , , . Закладка Постоянная ссылка.

1 комментарий: Динамическая индикация: экономим выводы МК

Добавить комментарий