В данной статье представлены муки по созданию меню для дисплея усилителя. Главная проблема здесь – то что мы имеем всего три кнопки (энкодер), причем две из которых и не кнопки вовсе по сути.
Для начала нарисуем как должно выглядеть наше меню (влево-вправо это вращение энкодера, вниз – нажатие на кнопку):
Изменение громкости здесь присутствует два раза потому что это базовый режим, то есть если не было входа в меню, то вращение энкодера приводит просто к изменению уровня громкости. Итак, как я строил меню:
- Основной элемент управления – энкодер, утаскиваем процедуры его обработки из соответствующей статьи.
- На энкодере есть кнопка, которая так же является нашим элементом управления, а чтобы ее обрабатывать четко и моментально я ее завел на пин RB0 и завел соответствующий обработчик прерываний INTIF.
- Завел две переменные индикатора вращения энкодера – up и down.
- Завел переменную статус для входа в меню регулировки уровня громкости volume_flag изначально равную нулю. Вот что примерно получилось (код находится в главной функции в бесконечном цикле):
if ((up)&&(!volume_flag)) //если крутили вправо { if (volume<100) volume++; //до 100 процентов увеличиваем setup_volume(volume); //процедура установки громкости up = 0; //обнуляем нашу переменную } if ((down)&&(!volume_flag)) //аналогично предыдущей функции, но в другом направлении { if (volume>0) volume--; setup_volume(volume); down = 0; }
- Дальше по нажатию кнопки мы должны попадать в меню, как я уже говорил кнопка обрабатывается в прерывании INTIF:
if (!volume_flag) { //Если нажали кнопку и volume_flag = 0 volume_flag = 1; //то присваиваем ей 1 menu(0); //и вызываем меню на экран }
Некрасиво конечно пихать вызов функции в обработчик прерываний, но у меня этот вариант оказался самым работоспосбным и я решил оставить его именно таким. Дальше нам нужно скроллить пункты меню, делать будем с помощью тех же переменных up и down (опять же в главной функции в бесконечном цикле):
if ((up)&&(volume_flag)) { if (itemp < 7) { //новая переменная для скролла пунктов меню if (itemp<6) { itemp++; exit_status = 0; //переменная инициализирующая выход из меню } else { itemp = 7; exit_status = 1; //если мы в седьмом пункте то если что выходим :) } } else { //если пытаемся скролить еще выше, то перескакиваем в начало itemp = 1; exit_status = 0; } menu(itemp); //отображаем соответствующий пункт up = 0; } if ((down)&&(volume_flag)) { //аналогично предыдущему, только в другую сторону if (itemp > 1 ) { itemp--; exit_status = 0; } else { itemp = 7; exit_status = 1; } menu(itemp); down = 0; }
В коде попутно введена еще одна статусная переменная – exit_status, которая отвечает за выход из меню если потребуется.
Вход в меню выполнен, скролл организован, идем дальше по пунктам:
- Первым у нас стоит операция Mute, добавляем новую переменную, ответственную за этот пункт и вставим кусок кода в главный цикл:
if (itemp == 1) mute_status = 1; else mute_status = 0;
Есть еще мысль вставить присваивание статусным переменным в прошлый код и организовать все с помощью оператора swtich, попозже попробую провернуть это, а то код монструозный получается и так.
Но крутанув энкодер и установив флажок на этот пункт пока ничего не произойдет, нужно нажать на кнопку, добавляем в обработчик код:
if (mute_status) { itemp = 0; mute_status = 0; //сбрасываем переменную, дабы не было проблем volume_flag = 0; //если еще раз кнопка будет нажата, попадем в меню mute(); //функция mute() }
Таким образом, если будет нажата кнопка то девайс перейдет в состояния mute, и для того, чтобы его из него вывести нужно еще раз будет нажать на эту же кнопку.
2. Снова регулировка громкости, как уже упоминалось ранее за нее отвечает переменная volume_flag, но здесь ее так просто заюзать не получится, добавляем еще одну статусную для регулировки громкости из меню:
if (itemp == 2) volume_status = 1; else volume_status = 0;
Ну и как обычно код в обработчике нажатия кнопки:
if (volume_status) { itemp = 0; //можно и без этого volume_flag = 0; //включаем дефолтный режим изменения громкости volume_status = 0; //обнуляемся }
Как только кнопка будет нажата, управление будет передано дефолтному режиму изменения уровня громкости в главном цикле. А про него уже написано сверху.
3. Изменение басов. Идем по уже накатанной дорожке:
if (itemp == 3) bass_status = 1; else bass_status = 0;
Обработчик прерывания по кнопке:
if (bass_status) { itemp = 0; volume_flag = 0; //чтобы не попасть в чужую функцию bass_status = 0; //обнуляемся bass_flag = 1; //переменная для главного цикла }
bass_flag заведена для того, чтобы в главном цикле корректно обрабатывалось вращение энкодера в этом пункте, вот собственно что в этой обработке у меня есть:
if ((down)&&(bass_flag)) { //все прозрачно, ничего необычного bass--; bass_setup(bass); down = 0; } if ((up)&&(bass_flag)) { bass++; bass_setup(bass); up = 0; }
4. Контроль тембра. Здесь совершенно идиентично пункту 3, поэтому описывать все не вижу смысла, в архиве есть весь код и можно посмотреть на общую картину.
5. Настройки времени. Здесь особый случай, дело в том, что для настройки времени используется своя специфическая функция, поэтому я использовал примерно такую реализацию:
if ((time_flag)&&(!time_status)) { GIE = 0; //Выключаем прерывания, работаем только в нашей функции time_flag=0; setup_time(); //функция установки времени }
Две переменные здесь нужны для того, чтобы корректно настроить микроконтроллер на будущую работу. Ну и как обычно одна из переменных присвается в обработчике:
if (time_status) { itemp = 0; time_status = 0; time_flag = 1; }
Саму процедуру настройки рассмотрим немного позже, а пока продолжим по меню.
6. Stand By – аналогично Mute
7. Exit , установка статусной переменной для выхода уже рассматривалась, так что перейдем сразу к обработчику нажатия RB0:
if (exit_status) { exit_flag = 1; exit_status = 0; volume_flag = 0; //выброс из меню в установку громкости }
а в главном цикле добавим следующие строки:
if (exit_flag) { display_tt(); //дежурная функция отображения времени и температуры внутри корпуса exit_flag = 0; }
Еще раз напоминаю, что использование в некоторых случаях двух переменных обусловлено необходимостью вызова функции из главного цикла, а не прямо из прерывания. Вот в принципе и все по самой организации меню, теперь когда мы знаем назначение всех статусных переменных, можем добавить в нашу визуализацию алгоритма еще кое-какие штрихи:
По этой диаграмке удобно отлаживать программу, можно диагностировать состояние переменных через уарт и сравнивать с ней. Функция display_tt() – уже упоминаемая дежурная функция вывода времени и температуры. Еще следует сделать акцент на то, что переменных здесь как грязи, а память не резиновая, поэтому все статусные переменные имеют тип bit.
Ну что, структура меню готова, скролл организован, осталось наполнить его чем-то осмысленным. Привожу реализацию вывода на дисплей и не только соответствующих функций.
- Главная функция – регулировка громкости:
setup_volume(unsigned char value) { lcd_clear(); LCD_RS = 0; lcd_write(0b00001100); //выключаем курсор __delay_us(100); lcd_goto(0x00); lcd_puts("Volume"); if (value<100) display_digit(value, 0x0D); //если значение меньше 100, то все стандартно else { lcd_goto(0x0C); //иначе отображаем цифру 100 lcd_putch(0b00110001); lcd_putch(0b00110000); lcd_putch(0b00110000); } lcd_goto(0x0F); //создаем красивое заполнение нижней строки lcd_putch(0b00100101); lcd_goto(0x40); lcd_putch(0b01111110); lcd_goto(0x46); if (value == 0) lcd_puts("----------"); if ((value > 0) && (value <= 10)) lcd_puts("O---------"); if ((value > 10) && (value <= 20)) lcd_puts("OO--------"); if ((value > 20) && (value <= 30)) lcd_puts("OOO-------"); if ((value > 30) && (value <= 40)) lcd_puts("OOOO------"); if ((value > 40) && (value <= 50)) lcd_puts("OOOOO-----"); if ((value > 50) && (value <= 60)) lcd_puts("OOOOOO----"); if ((value > 60) && (value <= 70)) lcd_puts("OOOOOOO---"); if ((value > 70) && (value <= 80)) lcd_puts("OOOOOOOO--"); if ((value > 80) && (value < 100)) lcd_puts("OOOOOOOOO-"); if (value == 100) lcd_puts("OOOOOOOOOO"); }
Данное меню отображает цифровое значение в верхней строке экрана и организовывает красивое заполнение нижнией строки.
В дальнейшем в эту функцию добавится процедура установки громкости по шине I2C для аудиопроцессора.
- Отображение самого меню:
void menu(unsigned char i) { lcd_clear(); LCD_RS = 0; lcd_write(0b00001100); //Выключаем курсор __delay_us(100); switch (i) { case 0 : lcd_goto(0x00); //организовываем статические картинки для скроллинга lcd_puts("Menu:"); lcd_goto(0x08); lcd_puts("1.Mute"); lcd_goto(0x48); lcd_puts("2.Volume"); break; case 1 : lcd_goto(0x00); lcd_puts("Menu:"); lcd_goto(0x07); lcd_putch(0b00111110); //указатель пункта меню lcd_goto(0x08); lcd_puts("1.Mute"); lcd_goto(0x48); lcd_puts("2.Volume"); break; case 2 : lcd_goto(0x00); lcd_puts("Menu:"); lcd_goto(0x08); lcd_puts("1.Mute"); lcd_goto(0x47); lcd_putch(0b00111110); lcd_goto(0x48); lcd_puts("2.Volume"); break; case 3 : lcd_goto(0x00); lcd_puts("Menu:"); lcd_goto(0x07); lcd_putch(0b00111110); lcd_goto(0x08); lcd_puts("3.Bass"); lcd_goto(0x48); lcd_puts("4.Treble"); break; case 4 : lcd_goto(0x00); lcd_puts("Menu:"); lcd_goto(0x08); lcd_puts("3.Bass"); lcd_goto(0x47); lcd_putch(0b00111110); lcd_goto(0x48); lcd_puts("4.Treble"); break; case 5 : lcd_goto(0x00); lcd_puts("Menu:"); lcd_goto(0x07); lcd_putch(0b00111110); lcd_goto(0x08); lcd_puts("5.Time"); lcd_goto(0x48); lcd_puts("6.StBy"); break; case 6 : lcd_goto(0x00); lcd_puts("Menu:"); lcd_goto(0x08); lcd_puts("5.Time"); lcd_goto(0x47); lcd_putch(0b00111110); lcd_goto(0x48); lcd_puts("6.StBy"); break; case 7 : lcd_goto(0x00); lcd_puts("Menu:"); lcd_goto(0x07); lcd_putch(0b00111110); lcd_goto(0x08); lcd_puts("7.Exit"); break; } }
Функция отображения меню, по сути набор статичных кадров, которые меняются в зависимости от направления вращения энкодера.
- Функция Mute. Так как во время этой функции громкости изменяться не должна, то надо вырубить вращение энкодера:
void mute() { TRISB2 = 0; //делаем так, чтобы вращение энкодера ни к чему не приводило TRISB1 = 0; lcd_clear(); //очищаем экран и ждем еще одного нажатия на кнопку для выхода lcd_goto(0x06); lcd_puts("MUTE"); }
Басы и тембр я пока оставил в зачаточном состоянии, т.к. аудиопроца на руках еще не имею, поэтому пока пропустим их.
- Установка времени:
void setup_time() { unsigned char temp = 0; char i; static bit exit = 0; static bit shr = 0; static bit smin = 0; static bit ssec = 0; unsigned char OldEncData = 3; static bit smenu = 0; i = 0; exit = 0; smenu = 0; temp = ReadHour(); //Выводим значение часов lcd_clear(); lcd_goto(0x01); lcd_puts("Hr:"); lcd_goto(0x00); lcd_putch(0b00111110); display_digit(temp, 0x05); //Функция отображения цифр на LCD temp = ReadMin(); //Выводим значения минут lcd_goto(0x41); lcd_puts("Min:"); display_digit(temp, 0x45); temp = ReadSeconds(); //Выводим значения секунд lcd_goto(0x0A); lcd_puts("Sec:"); display_digit(temp, 0x0E); lcd_goto(0x4A); lcd_puts("Exit?"); while(!exit) { /*Процедура обработки вращения энкодера*/ __delay_ms(1); EncData = PORTB & 0b00000110; EncData >>= 1; if (OldEncData != EncData) { switch (OldEncData) { case 0 : if (EncData == 1) {upcount++; downcount=0; } if (EncData == 2) {downcount++; upcount = 0; } break; case 1 : if (EncData == 3) {upcount++; downcount=0; } if (EncData == 0) {downcount++; upcount = 0; } break; case 2 : if (EncData == 0) {upcount++; downcount=0; } if (EncData == 3) {downcount++; upcount = 0; } break; case 3 : if (EncData == 2) {upcount++; downcount=0; } if (EncData == 1) {downcount++; upcount = 0; } break; } OldEncData = EncData; } /* Конец процедуры обработки вращения энкодера */ if (!smenu) { //Скроллим пункты меню if (upcount >= 4) { if (i<3) i++; else i = 0; upcount = 0; } if (downcount >= 4 ) { if (i>0) i--; else i = 4; downcount = 0; } pointer_time(i); //Функция отображения указателя } else { //Если не меню, то увеличиваем/уменьшаем временную переменную if (upcount >= 4) { temp++; upcount = 0; } if (downcount >= 4 ) { temp--; downcount = 0; } if (shr) { //если shr = 1, то отображаем/устанавливаем часы if (temp >= 24) temp = 0; display_digit(temp, 0x05); i = 5; } if (smin) { if (temp >= 60) temp = 0; //если smin = 1, то отображаем/устанавливаем минуты display_digit(temp, 0x45); i = 6; } if (ssec) { //если ssec = 1, то отображаем/устанавливаем секунды if (temp >= 60) temp = 0; display_digit(temp, 0x0E); i = 7; } } if (!ebutton) { //если нажали на кнопку __delay_ms(200); //антидребезг smenu = !smenu; //переключаем статусную переменную switch (i) { case 0 : shr = 1; smin = 0; ssec = 0; temp = ReadHour(); lcd_goto(0x00); lcd_putch(0b00101010); break; case 1 : smin = 1; shr = 0; ssec = 0; temp = ReadMin(); lcd_goto(0x40); lcd_putch(0b00101010); break; case 2 : ssec = 1; shr = 0; smin = 0; temp = ReadSeconds(); lcd_goto(0x09); lcd_putch(0b00101010); break; case 3 : exit = 1; break; case 5 : SetHour(temp); shr = 0; i = 0; pointer_time(i); break; case 6 : SetMin(temp); smin = 0; i = 1; pointer_time(i); break; case 7 : SetSeconds(temp); ssec = 0; i = 2; pointer_time(i); break; } } } GIE = 1; //Включаем все прерывания }
Ну вот пожалуй и хватит, напоследок выкладываю видео с работающим девайсом (качество только вот его подкачало):
Можете исходные файлы проекта выложить, хочется посмотреть на весь код в целом 🙂
Версия парумесячной давности здесь. У меня проект на данный момент очень вялотекущий 🙂