AVR. Сегментный индикатор
Сегментный индикатор позволяет выводить различную информация в виде цифр, букв и т.д., в зависимости от конфигурации индикатора. В этой статье вы найдете описание работы с одним разрядом индикатора.
Подготовка к работе
Прежде чем начинать программировать, разберемся с тем, как работает сегментный индикатор. По сути, он представляет собой матрицу светодиодов. В семисегментных индикаторах эти сегменты расположены так, что включая эти светодиоды, можно вывести цифры от 0 до 9. На рисунке показано расположение светодиодов на дисплее и его электрическая схема. Индикаторы бывают с общим анодом и общим катодом. На рисунке с общим анодом.
Управлять сегментным индикатором можно при помощи логических уровней. Для этого достаточно подключить общий анод к плюсу, а остальные выводы через резисторы к выводам микроконтроллера. Если на выводах МК выводить 0 сегменты будут загораться, если 1 — гаснуть.
Напоминаем, что все примеры выполнены с использованием EduBoard и TutorShield.
На нашем шилде двухразрядный индикатор. Схема подключения индикатора на нем:
Этот индикатор предназначен для динамической индикации, поэтому аноды разрядов подключены через транзисторы, для того чтобы их можно было отключить. Если подать ноль на базу такого транзистора, то он откроется и на аноды требуемого разряда будет подано напряжение питания.
Теперь установите перемычки так, как показано на рисунке, подключите шилд к 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(), стала гораздо проще. Теперь для вывода очередного символа достаточно просто очистить сегмент и вывести новое значение. Если все сделано правильно, то на индикаторе вы увидите следующее:
Индивидуальные задания
Попробуйте самостоятельно выполнить следующие задания:
- В последнем примере цифры в цикле loop() задаются простым перебором. Попробуйте организовать перебор значений, используя цикл for.
- В большинстве случаев информация на дисплее отображается непрерывно. Доработайте функцию Show() таким образом, чтобы не надо было каждый раз перед ее вызовом очищать дисплей
- Добавьте любой дополнительный символ, который может быть отображен на индикаторе. Например H, Г, А и так далее.
Остальные статьи цикла можно найти здесь.
Метки: AVR, курс, программирование, сегментный индикатор, управление Просмотров: 9846