CUSTOMELECTRONICS.RU
Информационно-учебный блог о разработке электроники
Эл. почта: info@customelectronics.ru

AVR. Сегментный индикатор

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

Подготовка к работе

Прежде чем начинать программировать, разберемся с тем, как работает сегментный индикатор. По сути, он представляет собой матрицу светодиодов. В семисегментных индикаторах эти сегменты расположены так, что включая эти светодиоды, можно вывести цифры от 0 до 9. На рисунке показано расположение светодиодов на дисплее и его электрическая схема. Индикаторы бывают с общим анодом и общим катодом. На рисунке с общим анодом.

Схема индикатора

Схема индикатора

Управлять сегментным индикатором можно при помощи логических уровней. Для этого достаточно подключить общий анод к плюсу, а остальные выводы через резисторы к выводам микроконтроллера. Если на выводах МК выводить 0 сегменты будут загораться, если 1 — гаснуть.
Напоминаем, что все примеры выполнены с использованием EduBoard и TutorShield.
На нашем шилде двухразрядный индикатор. Схема подключения индикатора на нем:
TutorShield_Schematic
Этот индикатор предназначен для динамической индикации, поэтому аноды разрядов подключены через транзисторы, для того чтобы их можно было отключить. Если подать ноль на базу такого транзистора, то он откроется и на аноды требуемого разряда будет подано напряжение питания.
Теперь установите перемычки так, как показано на рисунке, подключите шилд к EduBoard и подключите платы к USB.

Перемычки для включения индикатора

Перемычки для включения индикатора

Первый пример

Для начала сделаем программу, которая будет с периодичностью 1с на 0,5с зажигать последовательно цифры от 0 до 2.
Для того, чтобы это сделать необходимо сначала настроить режимы работы портов. Старший разряд индикатора подключен к выводу PD4, а младший к PD5. Для того, чтобы включить разряд нужно настроить индикатор выход и записать в соответствующий бит регистра "0". Для отключения нужно записать "1".
Сегменты разряда имеют следующее подключение: a — PD6, b — PD7, c — PB0, d — PB1, e — PB2, f — PB3, g — PB4. Для зажигания светодиода нужно выставить на выводе логический "0", а для отключения "1".
Если записать весь код следуя этим инструкциям, он будет иметь следующий вид:

#define F_CPU 16000000UL //16MHz
#include <avr/io.h>
#include <util/delay.h>

int main(void) {
	DDRD = 0b11110000;
	PORTD = 0b11010000;
	DDRB =0b00011111;
	PORTB =0b00011111;
	while(1) {
		PORTD &= ~0b11000000;
		PORTB &= ~0b00001111;
		_delay_ms(500);
		PORTD |= 0b11000000;
		PORTB |= 0b00011111;
		_delay_ms(500);
		PORTD &= ~0b10000000;
		PORTB &= ~0b00000001;
		_delay_ms(500);
		PORTD |= 0b11000000;
		PORTB |= 0b00011111;
		_delay_ms(500);
		PORTD &= ~0b11000000;
		PORTB &= ~0b00010110;
		_delay_ms(500);
		PORTD |= 0b11000000;
		PORTB |= 0b00011111;
		_delay_ms(500);
	}
}

Создайте новый проект, введите код, скомпилируйте его и загрузите в память МК. Если все сделано правильно, то работать это будет вот так:

Этот код работает как надо, но он очень неудобен в написании и очень сложно воспринимается человеком. При написании программ обычно стараются находить баланс между объемом текста программ и ее читаемостью. Можно повысить читаемость программы, если использовать директиву #define. Эта директива определяет идентификатор и последовательность символов, которую компилятор будет подставлять вместо идентификатора.
Рассмотрим на примере:

#define A_PD6 6

Такая строчка скажет компилятору, что каждый раз, когда он будет встречать в тексте программы идентификатор A_PD6 ему нужно будет подставить вместо нее символ 6. Хотя использование этих директив увеличивает объем кода, зато становится гораздо понятнее, что происходит в программе, так как идентификатор содержит одновременно и описание назначения вывода и к какому выводу микроконтроллера он подключен.
Приведенный ниже код работает совершенно идентично, но с точки зрения человека гораздо понятнее:

#define F_CPU 16000000UL //16MHz
#include <avr/io.h>
#include <util/delay.h>

#define DIG1_PD4 4
#define DIG2_PD5 5
#define A_PD6 6
#define B_PD7 7
#define C_PB0 0
#define D_PB1 1
#define E_PB2 2
#define F_PB3 3
#define G_PB4 4
#define DELAY_MS 500

void hardware_init(void) {
	DDRD |= ((1<<DIG1_PD4)|(1<<DIG2_PD5)|(1<<A_PD6)|(1<<B_PD7));
	PORTD |= ((1<<DIG1_PD4)|(1<<A_PD6)|(1<<B_PD7));
	PORTD &= ~(1<<DIG2_PD5);
	DDRB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
	PORTB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
}

int main(void) {
	hardware_init();
	while(1) {
		//show "0"
		PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
		PORTB &= ~((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3));
		_delay_ms(DELAY_MS);
		//turn off all
		PORTD |= ((1<<A_PD6)|(1<<B_PD7));
		PORTB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
		_delay_ms(DELAY_MS);
		//show "1"
		PORTD &= ~(1<<B_PD7);
		PORTB &= ~(1<<C_PB0);
		_delay_ms(DELAY_MS);
		//turn off all
		PORTD |= ((1<<A_PD6)|(1<<B_PD7));
		PORTB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
		_delay_ms(DELAY_MS);
		//show "2"
		PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
		PORTB &= ~((1<<D_PB1)|(1<<E_PB2)|(1<<G_PB4));
		_delay_ms(DELAY_MS);
		//turn off all
		PORTD |= ((1<<A_PD6)|(1<<B_PD7));
		PORTB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
		_delay_ms(DELAY_MS);
	}
}

Помимо прочего в нем мы сделали еще несколько улучшений.
Во-первых, мы написали функцию hardware_init(). Контроллер исполняет программу, описанную в функции main(). Когда он дойдет до строчки hardware_init() он просто "сходит" и выполнит содержимое этой функции, а затем вернется к выполнению основной программу с того места, на котором остановился. Такие конструкции позволяют упростить вид основной программы и избегать повторения больших кусков кода (функции можно вызывать несколько раз). Также это экономит размер самой программы в памяти микроконтроллера.
Во-вторых мы добавили комментарии. Записи вида //show "0" игнорируются компилятором, но дают человеку понять, что делается в этой части программы. Так, если вы увидели, что неправильно отображается, например, цифра "2" — вы быстро сможете найти в какой части программы ошибка.
В-третьих, мы объявили в заголовке задержку. Теперь, если нужно изменить длительности пауз и работы, править этот код нужно будет не везде, где эта задержка встречается, а только в строке #define DELAY_MS 500.

Второй пример

Логичным продолжением будет создание дополнительной функции управляющей индикатором, чтобы в main’е остались только команды на вывод символа.
Для этого создадим две дополнительные функции — clean() и show(). clean() будет полностью гасить весь разряд. show() — функция с параметром. Она принимает на входе значение, которое нужно вывести на индикаторе и зажигает нужные сегменты. Посмотрите, как при этом выглядит программа:

#define F_CPU 16000000UL //16MHz
#include <avr/io.h>
#include <util/delay.h>

#define DIG1_PD4 4
#define DIG2_PD5 5
#define A_PD6 6
#define B_PD7 7
#define C_PB0 0
#define D_PB1 1
#define E_PB2 2
#define F_PB3 3
#define G_PB4 4
#define DELAY_MS 1000

void clean(void) {
    PORTD |= ((1<<A_PD6)|(1<<B_PD7));
    PORTB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
}

void show(int digit) {
    switch(digit) {
        case 0: {
            PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
            PORTB &= ~((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3));
        }
        break;
        case 1: {
            PORTD &= ~(1<<B_PD7);
            PORTB &= ~(1<<C_PB0);
        }
        break;
        case 2: {
            PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
            PORTB &= ~((1<<D_PB1)|(1<<E_PB2)|(1<<G_PB4));
        }
        break;
        case 3: {
            PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
            PORTB &= ~((1<<C_PB0)|(1<<D_PB1)|(1<<G_PB4));
        }
        break;
        case 4: {
            PORTD &= ~(1<<B_PD7);
            PORTB &= ~((1<<C_PB0)|(1<<F_PB3)|(1<<G_PB4));
        }
        break;
        case 5: {
            PORTD &= ~(1<<A_PD6);
            PORTB &= ~((1<<C_PB0)|(1<<D_PB1)|(1<<F_PB3)|(1<<G_PB4));
        }
        break;
        case 6: {
            PORTD &= ~(1<<A_PD6);
            PORTB &= ~((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
        }
        break;
        case 7: {
            PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
            PORTB &= ~(1<<C_PB0);
        }
        break;
        case 8: {
            PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
            PORTB &= ~((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
        }
        break;
        case 9: {
            PORTD &= ~((1<<A_PD6)|(1<<B_PD7));
            PORTB &= ~((1<<C_PB0)|(1<<D_PB1)|(1<<F_PB3)|(1<<G_PB4));
        }
        break;
    }
}

void hardware_init(void) {
    DDRD |= ((1<<DIG1_PD4)|(1<<DIG2_PD5)|(1<<A_PD6)|(1<<B_PD7));
    PORTD |= ((1<<DIG1_PD4)|(1<<A_PD6)|(1<<B_PD7));
    PORTD &= ~(1<<DIG2_PD5);
    DDRB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
    PORTB |= ((1<<C_PB0)|(1<<D_PB1)|(1<<E_PB2)|(1<<F_PB3)|(1<<G_PB4));
}

int main(void) {
    hardware_init();
    while(1) {
        clean(); show(0); _delay_ms(DELAY_MS);
        clean(); show(1); _delay_ms(DELAY_MS);
        clean(); show(2); _delay_ms(DELAY_MS);
        clean(); show(3); _delay_ms(DELAY_MS);
        clean(); show(4); _delay_ms(DELAY_MS);
        clean(); show(5); _delay_ms(DELAY_MS);
        clean(); show(6); _delay_ms(DELAY_MS);
        clean(); show(7); _delay_ms(DELAY_MS);
        clean(); show(8); _delay_ms(DELAY_MS);
        clean(); show(9); _delay_ms(DELAY_MS);
    }
}

Для написания функции show() мы использовали оператор switch. Этот оператор позволяет осуществить выбор между несколькими фрагментами кода.
Основная программа, выполняемая в main(), стала гораздо проще. Теперь для вывода очередного символа достаточно просто очистить сегмент и вывести новое значение. Если все сделано правильно, то на индикаторе вы увидите следующее:

Индивидуальные задания

Попробуйте самостоятельно выполнить следующие задания:

  1. В последнем примере цифры в цикле loop() задаются простым перебором. Попробуйте организовать перебор значений, используя цикл for.
  2. В большинстве случаев информация на дисплее отображается непрерывно. Доработайте функцию Show() таким образом, чтобы не надо было каждый раз перед ее вызовом очищать дисплей
  3. Добавьте любой дополнительный символ, который может быть отображен на индикаторе. Например H, Г, А и так далее.

Остальные статьи цикла можно найти здесь.

Метки: , , , , Просмотров: 7304