Describing my struggles with a menu for my home small project – an audio amplifier. The main challenge is that we have just 3 buttons (the encoder), and two of them are actually not buttons but rotation directions.
First of all, I drafted a block diagram with the logic of the operation (right-left is a rotation direction, down – the button is pressed).
The volume change here is mentioned twice since it is a very basic mode, i.e. if there was no entering menu event, then the encoder rotation leads to just volume level change. So, how did I build the menu:
- the main control element – the encoder, grabbing the procedures from our experiment.
- There is a button on the encoder, which is also the control element. To process it reliably, I connected it to RB0 and started the interrupt handler INTIF.
- Created two variables for the encoder rotation – up and down.
- Created a status variable for entering the menu
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; }
Then, as I mentioned, the button is pressed event should lead us to the menu:
if (!volume_flag) { //Если нажали кнопку и volume_flag = 0 volume_flag = 1; //то присваиваем ей 1 menu(0); //и вызываем меню на экран }
Not a nicer way to call the function inside of the interrupt handler, but it worked for me so I decided to let it be. Then we need to scroll menu items, we will do that with the same variables up and down (though, calling them will happen inside of the main function):
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; }
There is one more variable added – exit_status, which is responsible for the exit from the menu, if needed.
The entering menu realized, the scrolling also, then step by step:
- The first item is the Mute operation, adding the new variable and additional piece of the code:
if (itemp == 1) mute_status = 1; else mute_status = 0;
But so far, rotation and setting the position to mute will not do anything, we still need to press the button:
if (mute_status) { itemp = 0; mute_status = 0; //сбрасываем переменную, дабы не было проблем volume_flag = 0; //если еще раз кнопка будет нажата, попадем в меню mute(); //функция mute() }
Thus, if the button has been pressed the device will move to the mute condition, and to move it out of it we need to press the button one more time.
2. Back to the volume adjustment, as I said earlier, we have the volume_flag variable, but I did not see an easy way to use it just by itself, adding one more variable for this purpose:
if (itemp == 2) volume_status = 1; else volume_status = 0;
and the code for the button handler:
if (volume_status) { itemp = 0; //можно и без этого volume_flag = 0; //включаем дефолтный режим изменения громкости volume_status = 0; //обнуляемся }
As soon as the button has been pressed, return back to the default mode.
3. Bass change
if (itemp == 3) bass_status = 1; else bass_status = 0;
button handler
if (bass_status) { itemp = 0; volume_flag = 0; //чтобы не попасть в чужую функцию bass_status = 0; //обнуляемся bass_flag = 1; //переменная для главного цикла }
bass_flag is here for correct rotation processing within this menu item:
if ((down)&&(bass_flag)) { //все прозрачно, ничего необычного bass--; bass_setup(bass); down = 0; } if ((up)&&(bass_flag)) { bass++; bass_setup(bass); up = 0; }
4. A treble control. The same as #3, no reason to describe
5. The time setup. Special case, special functions:
if ((time_flag)&&(!time_status)) { GIE = 0; //Выключаем прерывания, работаем только в нашей функции time_flag=0; setup_time(); //функция установки времени }
Two variables here are needed for the further correct setup of the microcontroller:
if (time_status) { itemp = 0; time_status = 0; time_flag = 1; }
6. Stand By – the same as Mute
7. Exit already discussed, going now to the button handler:
if (exit_status) { exit_flag = 1; exit_status = 0; volume_flag = 0; //выброс из меню в установку громкости }
adding strings to the main cycle:
if (exit_flag) { display_tt(); //дежурная функция отображения времени и температуры внутри корпуса exit_flag = 0; }
Reminding, that two variables are needed for calling functions from the main cycle, not right from the interrupt handler. Let me add some touch-ups to the bd.
It was more convenient to use this bd for a debug, we could fire up the uart and use it. The function display_tt() – a refresh function for showing time/temperature. It is worth noticing that I tried to spare memory by using a bit type.
So, the menu structure is prepared, the scroll is ready, and now need to fill it up with something meaningful.
- The main function – the volume adjustment:
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"); }
This menu shows digital value in the top row and some graphical representation in the bottom row. In the future, I will add here a function sending i2c command for the audio processor.
- menu display procedure
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; } }
Basically, just a set of static frames, which changes in dependence on the rotation direction.
- A function of Mute. Since the volume should not be changed during the mute, so we need to block it:
void mute() { TRISB2 = 0; //делаем так, чтобы вращение энкодера ни к чему не приводило TRISB1 = 0; lcd_clear(); //очищаем экран и ждем еще одного нажатия на кнопку для выхода lcd_goto(0x06); lcd_puts("MUTE"); }
The time setup:
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; //Включаем все прерывания }
Enough of this.
test comment
replying on the test comment
reply on the reply on the test comment
Pingback: A TDA7294 audio amplifier with the PIC16F877a brain (abandoned, not finished) | diymicro.org