Организация древовидного меню

Почти для всех проектов на микроконтроллере с экранчиком требуется система меню. Для каких-то проектов одноуровневое, для других — многоуровневое древовидное. Памяти, как обычно, мало, поэтому хочется запихнуть все во флэш.

Попутно, из проекта в проект, развивалась своя псевдоОС — таймеры, события, диспетчеры.
Я ее полностью писал на си, оптимизацией и вылизыванием еще не занимался.

Перебирая разные системы, наткнулся на MicroMenu:

Попробуем разобрать ее на части и прикрутить к системе.

Структура данных:
Меню организовано в виде четырехсвязного списка. Каждый элемент меню (пункт меню) ссылается на предыдущего и последующего элемента, также ссылается на своего родителя (пункт меню предыдущего уровня) и потомка (пункт подменю). Если это первый пункт, то предыдущего элемента у него нет, соответствующая ссылка пустая.

Изобразим это на рисунке:

Для чего такая избыточность? По сути, с текущим пунктом меню можно сделать четыре вещи:

  • Перейти вверх или вниз (предыдущий или следующий пункт)
  • Вернуться в родительское меню (если есть)
  • Выбрать текущий пункт. При этом мы или переходим в подменю (ели оно есть), или выполняется команда, прикрепленная к этому пункту меню.

При наличии джойстика (или четырех кнопок «крестом») эти действия как раз вешаются на свою кнопку.

Соответственно, все эти действия отражают четыре указателя. В оригинальной системе указатель на потомка обозван SIBLING, но я считаю это идеологически неверным. Sibling – это родственник того же уровня. Брат или сестра. Но никак не потомок. Поэтому мы будем использовать идеологически выверенное CHILD.

Итак, описание структуры пункта меню:

1
2
3
4
5
6
7
8
typedef struct PROGMEM{
	void       *Next;
	void       *Previous;
	void       *Parent;
	void       *Child;
	uint8_t     Select;
	const char  Text[];
} menuItem;

Добавлен байт Select – это код команды, привязанный к текущему пункту. Если у данного пункта есть подменю, код нулевой. Также есть поле Text. Капитан Очевидность подсказывает, что это, собственно, текст пункта меню. По расходам памяти — на каждый пункт меню расходуется 9 байт плюс длина текстовой части. И это все — кладется во флеш.

Самое полезное, почерпнутое у MicroMenu – набор дефайнов для быстрого и удобного определения меню.

1
2
3
4
5
6
#define MAKE_MENU(Name, Next, Previous, Parent, Child, Select, Text) \
	extern menuItem Next;     \
	extern menuItem Previous; \
	extern menuItem Parent;   \
	extern menuItem Child;  \
	menuItem Name = {(void*)&Next, (void*)&Previous, (void*)&Parent, (void*)&Child, (uint8_t)Select, { Text }}

В чем пафос такой конструкции? Для того, чтобы определить текущий элемент, нам надо указать ссылку на следующий, еще не известный компилятору. Поэтому этот дефайн создает заведомо избыточное количество описаний extern. Это означает, что такой идентификатор будет где-то описан, не обязательно в этом же файле. В качестве бонуса это позволит растащить меню по нескольким файлам, если вдруг возникнет такое неудовлетворенное желание.

Теперь самое интересное: описание структуры меню, как на рисунке.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// для начала — пустой элемент. Который NULL на рисунке
#define NULL_ENTRY Null_Menu
menuItem	Null_Menu = {(void*)0, (void*)0, (void*)0, (void*)0, 0, {0x00}};
 
enum {
    MENU_CANCEL=1,
    MENU_RESET,
    MENU_MODE1,
    MENU_MODE2,
    MENU_MODE3,
    MENU_SENS1,
    MENU_SENS2,
};
 
//                 NEXT,      PREVIOUS     PARENT,     CHILD
MAKE_MENU(m_s1i1,  m_s1i2,    NULL_ENTRY,  NULL_ENTRY, m_s2i1,       0, "Запуск");
MAKE_MENU(m_s1i2,  m_s1i3,    m_s1i1,      NULL_ENTRY, m_s3i1,       0, "Настройка");
MAKE_MENU(m_s1i3,  NULL_ENTRY,m_s1i2,      NULL_ENTRY, NULL_ENTRY,   MENU_RESET, "Сброс");
 
// подменю Запуск
MAKE_MENU(m_s2i1,  m_s2i2,    NULL_ENTRY,  m_s1i1,     NULL_ENTRY,   MENU_MODE1, "Режим 1");
MAKE_MENU(m_s2i2,  m_s2i3,    m_s2i1,      m_s1i1,     NULL_ENTRY,   MENU_MODE2, "Режим 2");
MAKE_MENU(m_s2i3,  NULL_ENTRY,m_s2i2,      m_s1i1,     NULL_ENTRY,   MENU_MODE3, "Режим 3");
 
// подменю Настройка
MAKE_MENU(m_s3i1,  m_s3i2,    NULL_ENTRY,  m_s1i2,     m_s4i1,       0, "Давление");
MAKE_MENU(m_s3i2,  NULL_ENTRY,m_s3i1,      m_s1i2,     m_s5i1,       0, "Время");
 
// подменю Давление
MAKE_MENU(m_s4i1,  m_s4i2,    NULL_ENTRY,  m_s3i1,     NULL_ENTRY,   MENU_SENS1, "Датчик 1");
MAKE_MENU(m_s4i2,  NULL_ENTRY,m_s4i1,      m_s3i1,     NULL_ENTRY,   MENU_SENS2, "Датчик 2");
 
// подменю Время
MAKE_MENU(m_s5i1,  m_s5i2,    NULL_ENTRY,  m_s3i2,     NULL_ENTRY,   MENU_WARM, "Разогрев");
MAKE_MENU(m_s5i2,  NULL_ENTRY,m_s5i1,      m_s3i2,     NULL_ENTRY,   MENU_PROCESS, "Процесс");

Готово!

Естественно, пункты меню можно описывать и вперемешку, в порядке обхода дерева. Типа такого:

1
2
3
4
5
6
MAKE_MENU(m_s1i1,  m_s1i2,    NULL_ENTRY,  NULL_ENTRY, m_s2i1,       0, "Запуск");
    // подменю Запуск
    MAKE_MENU(m_s2i1,  m_s2i2,    NULL_ENTRY,  m_s1i1,     NULL_ENTRY,   MENU_MODE1, "Режим 1");
    MAKE_MENU(m_s2i2,  m_s2i3,    m_s2i1,      m_s1i1,     NULL_ENTRY,   MENU_MODE2, "Режим 2");
    MAKE_MENU(m_s2i3,  NULL_ENTRY,m_s2i2,      m_s1i1,     NULL_ENTRY,   MENU_MODE3, "Режим 3");
MAKE_MENU(m_s1i2,  m_s1i3,    m_s1i1,      NULL_ENTRY, m_s3i1,       0, "Настройка");

Можно даже пойти дальше — строить меню в какой-нибудь визуальной среде, а потом автоматически генерировать такой список. Но это на потом.

Плюсы и минусы такой организации. Минус — явная избыточность. Плюс — возможность быстро редактировать меню — вставить новый пункт, поменять местами, удалить. Изменяются только соседние элементы меню, без тотальной перенумерации. Мне этот плюс перевесил все остальные минусы.

Опять же бонус — можно организовать несколько не связанных друг с другом деревьев меню. Главное не потерять точку входа.

Дальше. Как ходить по меню. Автор предлагает несколько дефайнов. Я их сохранил, хотя можно и без них обойтись.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define PREVIOUS   ((menuItem*)pgm_read_word(&selectedMenuItem->Previous))
#define NEXT       ((menuItem*)pgm_read_word(&selectedMenuItem->Next))
#define PARENT     ((menuItem*)pgm_read_word(&selectedMenuItem->Parent))
#define CHILD      ((menuItem*)pgm_read_word(&selectedMenuItem->Child))
#define SELECT	    (pgm_read_byte(&selectedMenuItem->Select))
 
menuItem* selectedMenuItem; // текущий пункт меню
 
void menuChange(menuItem* NewMenu)
{
	if ((void*)NewMenu == (void*)&NULL_ENTRY)
	  return;
 
	selectedMenuItem = NewMenu;
}

Вроде должно быть понятно. Выполняется проверка, если есть куда переходить, то переходим. Иначе — не переходим. Вызывается эта процедура таким образом:

1
menuChange(PREVIOUS);

Далее, процедура реакции на нажатие клавиш (в качестве параметра передается код нажатой клавиши):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
uint8_t keyMenu(msg_par par) {
	switch (par) {
	case 0: {
		return 1;
	}
	case KEY_UP: {
		menuChange(PREVIOUS);
		break;
	}
	case KEY_DOWN: {
		menuChange(NEXT);
		break;
	}
	case KEY_RIGHT:
		;
	case KEY_OK:
		{ // выбор пункта
			uint8_t sel;
			sel = SELECT;
			if (sel != 0) {
				sendMessage(MSG_MENU_SELECT, sel);
 
				killHandler(MSG_KEY_PRESS, &keyMenu);
				killHandler(MSG_DISP_REFRESH, &dispMenu);
 
				return (1);
			} else {
				menuChange(CHILD);
			}
			break;
		}
	case KEY_LEFT: { // отмена выбора (возврат)
		menuChange(PARENT);
	}
	}
	dispMenu(0);
	return (1);
}

Процедура отрисовки меню. Зависит от выбранного экранчика, а также от используемой анимации при выборе. Например у меня экранчик 128х64 точки, текущий пункт меню всегда по середине экрана, сверху и снизу выводятся два предыдущих и два последующих элемента (если есть). Отрисовка вызывается после каждого нажатися на кнопку и по таймеру, два раза в секунду. Мало ли, может изменится что.
В шапке можно выводить текст родителя, чтобы знать, где находимся. Можно двигать курсор по пунктам, а не пункты прокручивать. На вкус и цвет все фломастеры разные.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
char* menuText(int8_t menuShift)
{
	int8_t i;
	menuItem* tempMenu;
 
	if ((void*)selectedMenuItem == (void*)&NULL_ENTRY)
	  return strNULL;
 
	i = menuShift;
	tempMenu = selectedMenuItem;
	if (i>0) {
		while( i!=0 ) {
			if ((void*)tempMenu != (void*)&NULL_ENTRY) {
				tempMenu = (menuItem*)pgm_read_word(&tempMenu->Next);
			}
			i--;
		}
	} else {
		while( i!=0 ) {
			if ((void*)tempMenu != (void*)&NULL_ENTRY) {
				tempMenu = (menuItem*)pgm_read_word(&tempMenu->Previous);
			}
			i++;
		}
	}
 
	if ((void*)tempMenu == (void*)&NULL_ENTRY) {
		return strNULL;
	} else {
		return ((char *)tempMenu->Text);
	}
}
 
unsigned char dispMenu(msg_par par) {
	char buf[25];
 
	LCD_clear();
 
	sprintf_P(buf, PSTR(">%-20S"), menuText(0));
	LCD_putsXY(2, 28, buf);
 
	sprintf_P(buf, PSTR("%-20S"), menuText(-2));
	LCD_putsXY(2, 8, buf);
 
	sprintf_P(buf, PSTR("%-20S"), menuText(-1));
	LCD_putsXY(5, 17, buf);
 
 
	sprintf_P(buf, PSTR("%-20S"), menuText(1));
	LCD_putsXY(5, 39, buf);
 
	sprintf_P(buf, PSTR("%-20S"), menuText(2));
	LCD_putsXY(2, 48, buf);
 
	LCD_drawLine(0, 26, 127, 26, 1);
	LCD_drawLine(0, 34, 127, 34, 1);
 
	return (1);
}

И последний штрих — инициализация меню:

1
2
3
4
5
6
7
void startMenu(void) {
	selectedMenuItem = (menuItem*)&m_l1i1;
 
	dispMenu(0);
	setHandler(MSG_KEY_PRESS, &keyMenu);
	setHandler(MSG_DISP_REFRESH, &dispMenu);
}

Для начала хватит. В продолжении — сделать несколько меню, сделать процедуру работы с меню реентерабельной, забабахать модель в протеусе.

Краткое описание того, что делает процедура setHandler — она привязывает обработчик к событию. В данном случае, при возникновении события MSG_KEY_PRESS вызовется функция keyMenu для обработки этого события.

Для демонстрации системы меню, описанной в предыдущем посте, собрал модель в протеусе. На базе двухстрочного LCD-индикатора, контроллераatmega32 и пяти кнопок (влево-вправо-вверх-вниз и выбор). В своих схемах использую джойстики от мобилок, они тоже пятипозиционные. Также воткнул три светодиода, чтобы хоть как-то реагировать на выбор пунктов меню.

Поскольку на экране у нас всего две строчки, решил отображение меню сделать горизонтальным. В верхней строчке отображается родительский пункт меню (или просто «Меню:», если мы на верхнем уровне), во второй строчке — текущий пункт меню. Клавишами влево-вправо выбираем предыдущий-следующий. Клавишей вверх — возвращаемся в родительское меню, клавиша вниз (или ОК) — заходим в выбранный пункт.

Обработка меню:

  • при выборе пунктов Start/Mode 1, Start/Mode 2, Start/Mode 3 загорается соответствующий светодиод
  • при выборе пункта Reset — все светодиоды гаснут
  • после выбора любого конечного пункта, возвращаемся обратно в корень меню.

Некоторые модификации, связанные с моделированием:

  • заменил весь текст на английский, потому что в оригинале модель экранчика не поддерживает русский язык. Да, я знаю, про замену dll, но не у всех она есть, а для просмотра — пойдет.
  • моя боевая библиотека работы с LCD почему-то отказалась работать с моделью. Поэтому взял какую-то первую попавшуюся из древнего проекта.
  • отключил автоповтор кнопок (на модели неудобно работать), но он есть

Ну и, надеюсь, мне простят подключение светодиодов без балластного резистора? ;)

Файлы к статье

112 thoughts on “Организация древовидного меню”

    1. А в чем проблема то? Поменяй тип отображения только и всего. А в поле текст загони какой нибудь 7сег код спецсимвола, который у тебя будет отвечать за имя пункта меню.

        1. Берем любой код, работающий с динамической индикацией, например вот этот:
          http://radiokot.ru/start/mcu_fpga/avr/15/

          В конец знакогенератора (здесь это DcMatrix) дописываем новых символов (например А Б В Г Е и т.п.). Дальше просто выводим символы как и обычные цифры, только что код >9.

          P.S. Надеюсь не сильно туплю, время позднее …

    2. На холодильных установках стоят веселые менюшки :) Как они бедные извращались, чтобы названия параметров вместить в 3 семисегментника …

      Так что сделать реально, правда русские буквы плохо выходят из палок, да и английские далеко не все.

    3. А я вообще сюда попал, набросал в C# такой код, и он работает,как ни странно.
      using System;
      using System.Collections.Generic;
      using System.ComponentModel;
      using System.Data;
      using System.Drawing;
      using System.Linq;
      using System.Text;
      using System.Windows.Forms;
      using AvrUsbDevice;
      using System.Timers;

      namespace LedTest
      {
      public partial class Form1 : Form
      {
      bool ledOn = false; // Флаг включен (true) или нет (false) светодиодик на плате
      ushort vid = 0x16C0, pid = 0x05DC; // Тут комментарии излишни — это VID и PID
      ATMega8 dev; // Объявляем объект типа ATMega16(так в оригинале я заменил на М8)

      public Form1()
      {
      InitializeComponent();
      }

      private void Form1_Load(object sender, EventArgs e)
      {
      dev = new ATMega8(vid, pid); // Создаем объект dev класса ATMega16.
      if (!dev.IsOpen()) // Если есть проблемы с USB — сообщим и выйдем
      {
      MessageBox.Show(String.Format(«Невозможно найти устройство vid = 0x{0:X}, pid = 0x{1:X}», vid, pid),
      «Ошибка USB», MessageBoxButtons.OK, MessageBoxIcon.Error);
      {

      }
      Close();
      }
      else // Если все хорошо, настроим микроконтроллер по USB
      {
      dev.DDRB |= 0x3f; // Пины bx00111111 порта B — на вывод
      dev.PORTB &= 0x00; // Выключим светодиодик на плате
      dev.DDRC |= 0x3f; // Пины bx00111111 порта C — на вывод
      dev.PORTC &= 0x00; // Выключим светодиодик на плате
      dev.DDRD |= 0xeb; // Пины bx11101011 порта D — на вывод
      dev.PORTD &= 0x00; // Выключим светодиодик на плате
      //dev.DDRA |= 0x55; // для меги 16-32
      //dev.PORTA &= 0x00; // Выключим светодиодик на плате
      }
      }

      // работа с портом B ++++++++++++++++++++++++++++++++++++++++++++++++++++++
      private void button1_Click(object sender, EventArgs e)
      {

      ledOn = !ledOn;
      if (ledOn)

      {
      dev.PORTB |= 0x01; // Включим светодиодик на плате
      //System.Threading.Thread.Sleep(5000); // это таймер

      }

      else
      {

      dev.PORTB &= 0x00; // Выключим светодиодик на плате
      dev.PORTC &= 0x00; // Гасим все в порту С это на пробу!
      dev.PORTD &= 0x00; // Гасим все в порту D это на пробу!
      panel1.BackColor = Color.LightYellow;
      panel2.BackColor = Color.LightYellow;
      panel3.BackColor = Color.LightYellow;
      panel4.BackColor = Color.LightYellow;
      panel5.BackColor = Color.LightYellow;
      panel6.BackColor = Color.LightYellow;
      panel7.BackColor = Color.LightYellow;
      panel8.BackColor = Color.LightYellow;
      panel9.BackColor = Color.LightYellow;
      panel10.BackColor = Color.LightYellow;
      panel11.BackColor = Color.LightYellow;
      panel12.BackColor = Color.LightYellow;
      panel13.BackColor = Color.LightYellow;
      panel14.BackColor = Color.LightYellow;
      panel15.BackColor = Color.LightYellow;
      panel16.BackColor = Color.LightYellow;
      panel17.BackColor = Color.LightYellow;
      panel18.BackColor = Color.LightYellow;

      }

      } это часть кода
      Смысл в том что я удаленно перегружаю зависшие устройства, там реле стоят. Только надо б чтоб если комп завис а реле само вернулось в исходное состояние-5 сек.
      Памагите!

    1. Компактно произвольное дерево в многомерный массив не упихать. Будут дырки, причем не маленькие. Также все равно нужны связи «назад», чтобы вернуться, например, в меню верхнего уровня. Либо запоминать всю иерархию.

      По поводу колбэк функции. Да, в оригинале было сделано именно так. В моей парадигме — возникает событие
      sendMessage(MSG_MENU_SELECT, sel);
      а кто его будет обрабатывать — это не головная боль меню.

        1. Да? А второй размер массива какой предложите? Я даже не говорю о скорости работы с таким вот «деревом».

          Почитайте книжку Кормена «Алгоритмы: построение и анализ», там описаны «стандартные» и очень эффективные реализации большинства часто встречающихся структур данных.

          1. Здесь речь не идет о супер-скоростной обработке информации, типа сотни тысяч раз в секунду. Даже если работа с этим деревом будет затянется на десятки миллисекунд (что очень и очень врядли), то все равно это будет быстрее, чем скорость обновления картинки на недорогом ЖК индикаторе. Более приоритетные прерывания во время выполнения этой задачи — тоже не проблема, пусть себе выполняются.

            Здесь мне кажется удобство разработки и объем занимаемой памяти заметно более важно, чем быстродействие.

            А вообще, наваять свою удобную и себе понятную архитектуру поддержки меню является весьма хорошим упражнением по программизму.

      1. Да, зато нету необходимости ручной работы. Забудешь ссылку поменять (изменилось имя переменной) и кушай гемморой с битой ссылкой. А тут можно компактно обработать рекурсивной функцией. Да займет больше ресурсов, зато красивое и масштабируемое решение. Конечно если пишешь для Тини прийдётся немного попрыгать на углях :3

      2. А я делал подобную систему на нескольких массивах структур — получилось довольно компактно (почти без дыр) и удобно. А главное — почти всё в статике, кроме полей ввода/вывода. Правда доделать до конца пока ещё не успел (появился срочный проект), как руки дойдут, может опишу поподробнее.

    2. Еще минус по поводу колбэк-функции: данную систему я использую не только, как Главное Меню Приложения, а также как дополнительное («выпадающее» или «контекстное» в терминах большого интерфейса). Например сохранить-отменить, просмотреть-удалить-редактировать-записать и т.д. Естественно, вызываться оно может из разных мест и выполнять при выборе должно совершенно разные вещи.
      В случае фиксированных колбэк-функций надо будет городить несколько идентичных меню, различающихся только колбэками. Или сами функции делать навороченными.

    1. А он, собственно, и висит на кнопке вправо. Просто, поскольку джойстик пятипозиционный, то нажатие на него такжа приравнивается к движению вправо.

  1. режет глаз использование указателей вида «void *Next;» Скажем так, небезопасно, тем более, что тут одна структура определяет элемент меню.

    может я чего не догоняю, но не лучше ли использовать «menuItem* Next;» сразу становится понятнее, что находится по указателю.

    1. Проходил через такое описание, когда текст отдельно, а пункты — отдельно.

      // массив меню изменения режима меню
      PROGMEM t_menu m_mode[] = {
      item_sim(mmm1,menu_one),
      item_sim(mmm2,menu_multi)
      };

      // массив меню редактирования
      PROGMEM t_menu m_edit[] = {
      item_opt(ed_uint, &number1, 0),
      item_opt(ed_int, &number2, 0),
      item_opt(ed_scal1, &number3, 0),
      item_opt(ed_scal2, &number4, 0)
      };

      При любых изменениях — править в трех местах. Очень неудобно. MicroMenu подкупило компактным описанием пункта — все в одной строке.

  2. А названия в принципе-то и не важны. Если принять второй формат описания, когда подпункты меню идут сразу вслед за родительским пунктом, то структура меню будет видна и так, невооруженным глазом.

  3. У меня тоже как-то встала проблема постоения меню. Честно говоря смотреть что-то готовое даже в голову не приходило, да и тогда наверное этого не так много было. Писал я на Си, знаю я правда его достаточно посредственно( всякие тайпдефы, указатели и прочее я практически не знаю и очень редко применяю, из-за чего иногда получается громоздко).

    Задача была аналогичная — сделать меню, на экранчике 128х64, древовидное. Причем напротив некоторых пунктов нужно было сделать чекбокс, а в некоторых возможность ввода чисел, не выходя из интерфейса меню. Впрочем это больше проблемы отображения чем построения самого меню.
    Структуру меню я решил хранить ввиде строки для минимального использования памяти

    flash char menu[]=»^void|void\
    ^Main\
    |Transmitter\
    |Reciever\
    |Settings\
    ^Transmitter\
    |CGS database\
    |Start CGS\
    |View atabase\
    ^Reciever\
    |View recieved data\
    |Copy recieved data\
    |Clear memory\
    |Set ID\
    |Write sample data\
    ^Settings\
    |Backlight: $%\
    |Time to glow: $s\
    |Clear CDS db\
    |Clear code db\
    |Date and time\
    ^CGS: &\
    |ID: $\
    |Note: $\
    |Start: $\
    |Stop: $\
    |Repeat: $\
    |Interval: $x0.05\
    |Run!\
    ^меню\
    |пункт\
    ^»;

    ^ — означает начало новой страницы меню,
    | — разделяет пункты меню. причем первая строчка после ^ является заголовком страницы, а не пунктом
    $ — означает то место куда должно вписываться число, которое можно редактировать с клавиатуры.

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

    Кроме всего этого необходимы таблицы переходов, которые выглядели так(специально укоротил чтобы суть ясна была):

    ТАБЛИЦА ПЕРЕХОДОВ:

    //указатель меню
    flash signed char menu_page[]= { -2, 1, 1, 1, 2, 4, 5, 3}; // номер страницы меню
    flash unsigned char menu_line[]= { 0, 0, 1, 2, 1, 4, 6, 0}; // номер пункта меню
    flash signed char menu_to_page[]= { 1, 2, 3, 4, 5, -5, -4, -6}; // куда посылает данный пункт меню

    Тут следует заметить, что при нажатии на пункт меню может происходить две разные в корне вещи
    вариант 1 — мы переходим на следущий уровень меню
    вариант 2 — выполняется какое либо нестандартное действие — открытие специального окна, или какие-то действия и возврат в меню.
    В первом случае в третьем массиве указывается номер страницы меню. Во втором — пишется отрицательное число. его модуль означает номер специального действия, которые все описаны в программе.

    ТАБЛИЦА СО ЗНАЧЕНИЯМИ

    // Всякие значения в менюшках
    // ПЕРВУЮ КОЛОНКУ ОСТАВИТЬ ПУСТОЙ
    flash unsigned char page[]= { 0, 0, 5, 5, 5, 5, 5, 5}; // страница меню
    flash unsigned char line[]= { 0, 5, 0, 1, 2, 3, 4, 5}; // пункт меню
    unsigned int value[]= { 0, 100, 0, 0, 0,59048, 1, 0}; // значение (хранится в оперативной памяти)
    flash unsigned int max_value[]= { 0, 1000, 1000, 255,59048,59048, 255, 255}; // максимально допустимое число
    flash unsigned char digits[]= { 0, 4, 4, 3, 5, 5, 3, 3}; // сколько позиций отводить на написание числа(минимум)

    ТАБЛИЦА С ЧЕКБОКСАМИ
    тут все аналогично. в моей программе они пока не использовались, поэтому и пусто везде.

    // чекбоксы в меню
    // ПЕРВУЮ КОЛОНКУ ОСТАВИТЬ ПУСТОЙ
    flash unsigned char box_page[]= { 0, 0, 0, 0, 0, 0, 0, 0};
    flash unsigned char box_line[]= { 0, 0, 0, 0, 0, 0, 0, 0};
    unsigned char box_value[]= { 2, 2, 2, 2, 2, 2, 2, 2};

    Прочитав как другие люди по-нормальному делают меню, понимаю что можно было наверное намного ровнее сделать :) Особенно всякие таблицы переходов можно было сделать на тайпдефах. Главный недостаток этой схемы — нельзя добавить пункт меню между другими без перенумерации в пределах одной страницы. Впрочем, если часто все это использовать — можно написать редактор, который будет генерить весь код, и тогда все проблемы такого плана отпадут.

    Конечно к этому всему нужны функции вывода, но они весьма специфичны ибо писались под один конкретный проект и выкладывать их нет смысла.

    Визуально получилось вот так http://tmp.avr.net.ru/menu.jpg
    Сверху заголовок. Цифры справа сверху в заголовке — отладочные, в будущем их не будет. Выделяются пункты меню достаточно умно. Когда выбран первый, то выделение стоит на нем и он находится вверху экрана. Затем, пока курсор не дойдет до середины экрана, меню не двигается а двигается курсор. Как только курсор оказался в центре экрана, двигается само меню, а курсор стоит на месте. В конце разумеется меню останавливается, так тчобы последний пункт был в самом низу экрана, и курсор начинает двигаться вниз. Если все пункты меню умещаются на экран(количество<=5), то соответственно само меню никуда не двигается а двигается только курсор. Еще справа есть аналог скролл бара, который показывает на каком месте меню мы сейчас находимся. Причем высота «ползунка» зависит от количества пунктов в меню(скажем если их 10, то ползунок будет высотой 50% от всей полосы прокрутки, т.к. на экран влезает только половина пунктов).

    1. >Главный недостаток этой схемы — нельзя добавить пункт меню между другими без перенумерации в пределах одной страницы.

      Вот это меня всегда убивало и сподвигло на поиск новой системы.

  4. я не писал для электроники, но когда-то кодил для телефонов j2me игрушки. меню делал на автоматах. описывал автомат отдельно от кода игры: структуру меню, действия, доп действия и т.д. и по этому описанию генерил java код, который представлял из себя конечный автомат. в итоге описание легко править, ошибок практически не было, и работал очень быстро. не было динамических структур, все данные есть до компиляции. получался минимальный размер кода и т.д.

    1. я сколько ни пробовал прикрутить конечные автоматы, они какие-то нежизнеспособные. Либо количество состояний зашкаливает, либо параллельность надо.

  5. извините за оффтоп, вопрос по асму для к580)))
    msk equ 25h
    mvi h, 14h
    mvi l, 0h
    lxi b ff11h
    ldax b
    xri msk
    jnz m1
    m1: inr l
    dcx b
    dcr h
    jnz
    ассемблер выдает «неопределенный символ» на строке 0004
    в чем дело????

    1. запятую пропустил. Да и дальше — строки 7 и 8 вместе не имеют смысла. Строка 11 — нет метки куда переходить.

      Эх, давно я не видел этого асма. Где ж такое до сих пор преподают? И главное, зачем? Преподавали бы что-нибудь, что хоть как-то можно было бы использовать на практике. Если так хочется 8-битный то уже AVR, или PIC или 8051 на худой конец.

    1. У си главные грабли — нехватка стека. Если в обработчике прерывания вызывается функция, особенно библиотечная, то он любит посохранять в стеке все регистры, на всякий случай. Плюс на стек кладутся локальные переменные (объявленные внутри функции).

      Конечно, можно писать на си в стиле асма — все переменные глобальные/статические, функций-процедур минимум, все развернуто в main. Тогда оверхед небольшой — порядка 20-30 байт программной памяти на стартап.
      Например для тиньки вполне себе можно писать и на си.

      1. Я успешно программирую Тини13А на Си и очень доволен.
        Что для этого нужно?
        1.Нужно уметь программировать на Си.
        2.Нужно понимать особенности используемого компилятора и архитектуры.Обязательно скурить это AVR035: Efficient C Coding for AVR http://www.atmel.com/dyn/resources/prod_documents/doc1497.pdf актуально не только для IAR.
        3.Найти компромисс между «писать на си в стиле асма» и «писать на си в стиле с++»Т.е. найти компромисс между уровнем абстракции и эффективностью.
        4.Обязательно поглядывать в генерируемый асм код и делать выводы об эффективности тех или иных конструкций языка.
        5.Не надо думать, что компилятор — волшебник. Ему надо очень активно помогать. :)

        >У си главные грабли — нехватка стека.
        Это у программиста ИМХО грабли со стеком.
        Вызывать что-то в прерывании какбэ вообще минздрав не рекомендует.
        Не надо забывать об inline.
        А ещё очень важен дизайн и если он кривой — то нехватка стека это самая малая из пролем.
        >Плюс на стек кладутся локальные переменные (объявленные внутри функции)только когда уже нехватает регистров. И тут опять нужно помнить о том, что переменные надо объявлят «как можно локальнее» а не наоборот как вы советуете. Правило простое: переменная должна по возможности скрываться из области видимости как только она перестаёт быть нужна.

        >то он любит посохранять в стеке все регистры, на всякий случай
        все только перед входом в обработчик прерывания. Call сопровождается сохранением вполне определенного набора регистров.

        В общем главное не останавливаться на достигнутом и побольше читать и практиковаться. Уверяю Вас: грамотное сочетание Си+асм позволяет и экономить время и решать задачу.

        1. такая муть, сплошные подводные камни…
          >Вызывать что-то в прерывании какбэ вообще минздрав не рекомендует.
          ну а это вообще жесть. почему я должен плодить бессмысленные сущности, раздувая прошивку, если могу просто вызывать процедуру, например, запихивания байта в стек FIFO UART’а или опроса клавы? спору нет, лишние push, pop и jmp, но процедура-то может быть немаленькой. «не успеет обработаться, навалится 100500 прерываний»? ну так я обычно кварцы на 16 МГц ставлю, изредка — на 8 (когда, на самом деле, можно и встроенным RC-шником обойтись).
          короче, родилась уверенность, что до какой-нибудь 128-й меги проще будет писать на асме и не выёживаться))

          1. Хозяин — барин ))

            Я лишь хотел подчеркнуть, что на Си таки писать можно и достаточно эффективно(по критерию время разработки программы / оверхед).

            Конечно вы можете в прерывании вызвать небольшую функцию — нет проблем.
            Просто архитектура АВР таковая, что чем меньше времени проц проводит в обработчике прерывания — тем лучше.

            Опять таки, почитайте AVR035. Это всё не я сам выдумал :)

            1. неприятности больше в том, что надо контролировать выхлопной код и сложнее отлавливать баги. яркий пример: если что-то не работает в асмовом сурсе, то исправить это можно даже без дебаггера — тупо отсмотрев сырец и возомнив себя процессором )) на сях же, дополнительно к архитектуре, надо ещё постоянно держать в голове особенности работы компилятора. и здесь уже, мне кажется, дело вкуса — или таки разбираться с компилятором, или с простынями асмовых сырцов ))

              логика минимального времени нахождения в прерывании тоже вполне понятна — если sei не сделать, то до конца текущего прерывания ничё больше «прерваться» не сможет. но, опять же, — решает задача и стиль её решения. если в main один smth:rjmp smth, то прерывания могут выполняться и подольше, но конечный результат, естественно, зависит именно от временнЫх расчётов и поставленной задачи.

              1. >неприятности больше в том, что надо контролировать выхлопной код и сложнее отлавливать баги. яркий пример: если что-то не работает в асмовом сурсе, то исправить это можно даже без дебаггера — тупо отсмотрев сырец и возомнив себя процессором ))

                Оо, как Вы ошибаетесь! И, кстати, DI HALT писал в одной из своих статей.
                код Си будет работать именно так, как написано. Контролировать «выхлопной» код приходится лишь для того, чтобы понять на сколько оптимальный код сгенерирован и как бы компилятору возможно что-то подсказать, чтобы он был оптимальнее. Но вот с работоспособностью кода(т.е. с багами) там ВСЕГДА всё в порядке. И прикидываться процессором во время отладки легче — в асме приходится все регистры в голове эмулировать, а в Си программе — только переменные и выраженные почти человеческими словами управляющие конструкции. И приславутый «пристальный взгляд» как раз таки больше подходит к Си, нежели к ассемблеру.
                Просто, видимо, Вы привыкли к ассемблеру и не до конца поняли Си и прелести высокоуровневости.

                P.S. кстати, кажется в одной из статей, DI HALT писал об отладке.
                И там он тоже подчеркнул, что «тупо посмотрел сырец» намного лучше работает с Си, чем с асмом :)

                P.S. так уж устроен человек — всегда своё болото хвалит ))))

                Кстати, подумайте, сколько Вам придется пыхтеть на асме над такой штукой как стек TCP/IP? И готов поспорить: такого рода код, полный ветвлений условий — компилятор «уложит» не хуже вас, а вот писать и отлаживать это всё будет в разы удобнее.

                Ну, ежели мы говорим про мигалку светодиодами — тогда и спорить нечего. Там и на паскале и на бейсике для AVR пишут(чего я, честно говоря, не очень то понимаю)

                1. «код Си будет работать именно так, как написано.»

                  Один маленький нюанс, чтобы получить на Си именно то что хочешь нужно в совершенстве знать диалект си под конкретный контроллер и четко понимать что ты пишешь. Чтобы прочухать где надо заатомировать, а где стоит volatile поставить и так далее. Где можно напрямую указателями работать, а где костыли вроде pgm_read применять. И места эти неочевидные и гуглить их струдно, т.к. обычно это хочу что то, но толком не знаю что. Вот и выходит что язык предназначенный для упрощения только все больше запутывает.

                  1. Хальт, я думаю, ты не будешь спорить с тем, что для абстрактных вещей, типа деревьев, списков и прочей лабуды если оно надо, C ощутимо удобнее? Обходить дерево, причем итеративно(стек экономим) — это на Си-то не очень очевидный алгоритм, а на асме, так вообще лютая жопа.

                    С другой стороны распихивать биты по регистрам на асме удобнее, но асмовые вставки в гцц(например) несколько неочевидная вещь и граблей там навалом.

                    1. Абсолютно согласен. Под каждый масштаб свой инструмент.

                  2. >Один маленький нюанс, чтобы получить на Си именно то что хочешь >нужно в совершенстве знать диалект си под конкретный контроллер и >четко понимать что ты пишешь. Чтобы прочухать где надо >заатомировать, а где стоит volatile поставить и так далее

                    Бред. Надо знать Си. И volatile, register и т.д. — к диалекту не относится, это стандартный Си. Да, всегда есть пара макросов, прагм(#pragma) или атрибутов(в gcc)специфичных для этой платформы.
                    Но поверьте мне, все ваши трудности с языком Си связаны лишь с тем, что вы его плохо знаете и мало на нем программировали.

                    1. oops. Это был DI HALT.
                      Пардон :)

                      Ну, в общем то если бы это был не DI HALT — я бы сказал тоже самое, только из уважения переделал немного последнюю фразу :)

                    2. Вот именно что! Надо знать! Иметь опыт! Поэтому меня люто доставляют заявы дескать Си прост и для новичков самое то — начинайте с него, а ассемблер оставьте для профессионалов. Гы гы гы :)

                    3. В деле микроконтроллеров(и embedded вообще) без ассемблера никуда — это факт неоспоримый.
                      Однако и быдлокодинг и «знание Си»(да да, именно в кавычках) тут на много более заметны, чем на PC.
                      Честно говоря я бы не сказал, что Си(сам по себе) прямо так уж и прост — нет. Однако _реализовывать алгоритм на Си_ — несомненно проще.

                      Я тут немного ударился в Холли Вар, но на самом деле могу точно сказать, что практически в любом коде можно обойтись максимум 30 — 40% асма там где это дейстивтельно необходимо.
                      Ведь на самом деле языки программирования, компиляторы — это инструменты(и не более того).
                      Человек, который бъет себя в грудь кулаком и говорит, что будет писать всё на асме до меги128 — подобен лесорубу, который говорит, что будет пилить ручной ножовкой, в то время как есть бензопила, а ручной ножовкой только мелкие веточки хорошо обпиливать.))) Как-то так)))

                      Что действительно нужно — так это ВНИМАТЕЛЬНО прочитать о синтаксисе, а потом что-ли понять его суть(то, как оно работает в железе после компиляции и знание асма тут только в помощь). Тогда не будет никаких проблем ни с указателями ни с энумами и битфилдами(и с юнионами там тоже всё просто).
                      Вопрос только в том, хочется ли действительно научиться программировать на Си или оставаться на обочине прогресса со своим асмом форэва ;)

                      Самое важное в нашей професси — это всё таки время.
                      А ещё нужно помнить, что 30% усилий дают 70% результата — это подходит и к асм оптимизации кода :)

  6. Обычно несколько удобнее вместо указателей на prev и next использовать одномерный массив.

    Я делал так:
    struct MenuItem setupMenu2[] = {
    {MIT_LABEL, » Тонкая настройка (Менять опасно!) «, 0, «»},
    {MIT_LABEL, «», 0, «» },
    {MIT_SELECT,»Полярность: $+ $- «,&smPol,»»},
    {MIT_EDIT, «Программное усиление: #9 «,&smG, «!0:99!»},
    {MIT_SELECT,»USB порт: $Включён $Выключен «,&smUSB, «» },
    {MIT_BUTTON,» [ Принять ] «,(long*)1000, «»},
    MENU_END
    };
    Здесь пункты меню можно не только выбрать, но и к примеру отредактировать значение, макросами MIT_* задаётся тип меню:
    LABEL — невыбираемый пункт (курсор просто проскакивает его),
    BUTTON — нажимаемая кнопка (при выборе происходит выход с выдачей кода, указанного в виде (long*)nnnn),
    SELECT — кнопками вправо и влево можно выбирать опции (они обозначены знаками $ в строке, при нажатии между ними перемещается маркер),
    EDIT — можно изменить число (позиция вывода числа задаётся символами #9, это такой кастомный принтф).
    После текста меню идут два поля, значение которых зависит от типа пункта меню.
    Переходов выше-ниже по иерархии я не делал (их мало и это обрабатывалось вручную в основном коде), но добавить никакой сложности нет.

    1. >BUTTON — нажимаемая кнопка (при выборе происходит выход с выдачей кода, указанного в виде (long*)nnnn)
      >(long*)1000 ?

      Куда-то «в небо» будет указывать такой указатель о__О
      Или я не понял чего-то?

    2. >Переходов выше-ниже по иерархии я не делал (их мало и это обрабатывалось вручную в основном коде), но добавить никакой сложности нет.

      Есть ньюанс — в таких плоских системах при переходе на подуровни надо как минимум запоминать всю цепочку от корня меню, чтобы можно было вернуться. Обычно флеша много, а оперативки мало. Поэтому в моем случае каждый пункт меню хранит в себе точное положение в иерархии, и достаточно хранить только указатель на текущий.

      Такой трейдофф меня устроил вполне.

  7. При разработке древовидного меню у меня была поставлена задача сделать максимальную универсальность. рассмотрел этот вариант, но он мне не понравился. В этом виде меню кажется весьма нагроможденным (особенно описание его структуры). В моем варианте описание структуры меню представляет собой одномерный числовой массив с количеством ячеек 4*число файлов. Посмотреть можно тут:
    http://rln.nnov.ru/index.php?ind=reviews&op=entry_view&iden=209
    Кнопок всего 4, комбинаций нажатия соответственно — до 16 (в текущем проекте задействованы только 4 — одиночные нажатия отдельных клавиш).
    Единственный недостаток моей организации — просто так взять и добавить новую строчку не получится. придется переписывать весь массив. Для этого надо прогрммку написать будет.

    1. Если бы в avr-gcc можно было бы попроще определять строки, которые должны лечь во флеш, то многие проблемы отпали бы сами собой. Организовать дерево — достаточно простая задача.

      И озвученный минус — то что нельзя просто добавить пункт — пречеркивает все плюсы. Если использовать дополнительную прогу для предкомпиляции меню, то можно и сильнее ужать, имхо.

      Я по свободе попробую переписать дефайны, чтобы описание стало более читабельным.

      1. Доработал свою версию меню. теперь у меня тратится даже 3 байта заместо 4-х. И количество пунктов меню максимально 2^32 (куда столько?). Это стало возвожно, благодаря реализации динамически генерируемых папок — т.е. папка, внутри которой лежит много копий одного и того же файла. причем файл может быть любым — хоть также динамической папкой, хоть обычной, хоть программой.
        Видео: http://www.youtube.com/watch?v=4oDKsgSoGAA
        теперь осталось это красиво оформить и открыть код :)
        Простое добавление нового пункта меню к этому времени как раз реализую.
        Но теперь имеется привязка к количеству строк дисплея — оно должно быть кратно 2. В моем случае их 4. Все из-за побитовой маски. С ее помощью удалось существенно сократить код.

  8. Я пишу на асме. Из этой статьи только общий смысл уловил. Спросить вот что хотел. Получается здесь нет состояний? Навигация по ссылкам осуществляется?

  9. Нужно сказать, что в оригинальном наборе исходников все гораздо проще и понятнее сделано. Именно макросы позволяют разделить программу на сущности (отделить алгоритм работы программы, отображение меню и управление кнопками).
    Если, используя исходник с avrfreaks, прикрутить это к «ОС на событийном принципе», то получится весьма элегантное решение.
    Мне кажется, что зря Вы выбросили функции, вызываемые при выборе пункта и отображении пункта меню. Это делает очень гибким это меню.

    1. Мне эти функции не пригодились. Тем более в моей парадигме правильно кинуть событие, а не делать коллбэки. В принципе и получилось отдельно — отдельно функция dispMenu, которая занимается только выводом на экран, отдельно функция keyMenu, которая только обрабатывает клавиши. Все остальное соединяет ОС.

    1. Можно. Опять же говорю про себя — у меня были отдельные блоки параметров (отдельно температуры, отдельно давления, отдельно временные интервалы) каждый со своим названием-меткой, поэтому просто при выборе пункта меню, например, «установка температур» открывался список со всеми температурами. Поскольку значения однородные (например температуры вводились в градусах с двумя десятичным знаками, время вводилась в минутах:секундах, давление — в килопаскалях), то и редакторы значений были отдельными сущностями. Построенными примерно на такой же схеме, только без древовидности.

  10. Здравствуйте. С интересом прочитал статью. Возникли вопросы.
    1. Логичней будет к джойстику добавить ещё две кнопки — Ok и Esc.
    2. Допустим, потребуется вместе с текстом выводить данные. Причём неизвестно
    какого типа (например (знаковую) температуру и (беззнаковую) дату время).
    Придется каждый раз как-то переписывать функцию dispMenu
    3. В структуру menuItem требуется добавить указатель на связанную переменную, опять
    же неизвестно какого типа.
    4. Допустим, потребуется редактировать переменную (например, минимальной температуры)
    с клавиатуры. Напрашивается такой сценарий – доходим до пункта меню (“Установка
    температуры”) и клавишами вверх, вниз редактируем, затем клавишей Ok,Esc
    принимаем или отклоняем. Потребуется ещё два указателя на две функции отклика на
    клавиши вверх, вниз (прибавить, убавить) и так для каждой редактируемой
    переменной.
    Сдаётся, огород нехилый наберётся.

    1. Давайте по порядку :)
      >1. Логичней будет к джойстику добавить ещё две кнопки — Ok и Esc.
      Джойстик, который использую я, пятипозиционный, то есть у него уже есть кнопка OK. Отмена обычно происходит ко кнопке «влево» — выход в предыдущее подменю, например.
      Естественно, каждый проектирует свой интерфейс исходя из потребностей.
      >2. Допустим, потребуется вместе с текстом выводить данные.
      Первое, что приходит в голову — делать шаблон, наподобие printf. Это если в каждой строке надо что-то свое городить. Если задача просто в уголке отображать, например, состояние батареи, то проблем не вижу.
      >4. Допустим, потребуется редактировать переменную
      Я делаю так — при выборе пункта меню вызываю функцию editTemp(blah, blah), которая уже редактирует величину (одну или целый список).

      Просто меню — это меню. Его задача — дать пользователю выбрать вариант из развесистого дерева.

      1. Спасибо за коммент.
        Если можно поподробнее про функцию editTemp , где её вызывать? В обработчике клавиатуры menuKey(msg_par par)? Тогда в описании пункта меню требуется:

        Например пункт меню
        // NEXT, PREVIOUS PARENT, CHILD
        MAKE_MENU( m_s3i1, m_s3i2, NULL_ENTRY, m_s1i2, m_s4i1, 0, «Pressure»);
        Вместо полей next (m_s3i2) b и previous(null_entry) подставить указатели на вызов функции editTemp. Так что ли?

        1. не. Есть поле Select, которое как раз и обозначает пункт меню. При выборе такого пункта появляется событие:

          sendMessage(MSG_MENU_SELECT, sel);

          На этом работа меню заканчивается. Кто уже будет обрабатывать событие, модулю меню безразлично. Типа «я прокукарекал, а там хоть не рассветай».

          Вот в обработчике события MSG_MENU_SELECT и обрабатывается редактирование переменной (в том числе).

  11. Привет всем! очень полезно. познавательно. но я пытался портировать этот код на CVAVR и у меня не вышло… Может мне кто то помочь. или подсказать простенькое меню организованное таким же способом

  12. Кто- нибудь делал на асме меню? Сейчас потихоньку ковыряю проект с меню (для себя), дисплей 4х20, навигация по меню экранами, то есть пролистываем не по пунктам, а экран (3 пункта, верхняя строчка- заголовок) целиком. Соответственно, кнопки- вверх, вниз, назад, 3 кнопки выбрать (для 1,2,3 строки, они же- f1,f2,f3 в приложении. будут.).

    Организация экрана- 5 строк .db, по порядку:
    1- заголовок, 20 символов в 1 строчку, всё
    2- адреса, тут адрес экрана вниз, вверх, родитель (или нули, если куда- то некуда)
    3-5- пункты 1-3, 20 символов имя пункта, его тип (ниже), данные в зависимости от типа

    типы пунктов:
    1- пункт с подпунктами- ссылка на подменю, данные- адрес экрана, куда он ссылается, там в качестве родителя указываем этот экран. Впрочем, ничего не мешает указать другой :-)
    2- пункт вызова приложения- данными идет адрес что вызываем.

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

  13. Здравствуйте! Очень интересная реализация меню. Взяла её за основу своей менюшки. Но возникла одна проблемка (не очень-то я дружу с указателями в Си). Может, подскажите, как решить… Есть необходимость в разных режимах выводить разные меню, точнее, не совсем разные, а укороченные версии основного меню (например, в одном режиме выводить всё полностью, в другом пункты 1,2 и 8, а в третьем 3,6,9). В моём представлении для реализации этих менюшек достаточно добавить к описанию струткуры MenuItem элемент, содержащий список режимов, в которых конкретный пункт меню будет отображаться. И динамически менять в структере selectedMenuItem значения полей Next и Prev. Всё бы ничего, но я никак не могу эти значения поменять в силу того, что программист я только начинающий и где-то путаюсь.
    Например, в функции menuChange такая запись:
    seletedMenuItem->Next=NewItem; — не имеет никакого эффекта.
    Поясните, пожалуйста, что я не так делаю и почему. Чую, что надо как-то с другого бока подойти к полю Next, но до конца разобраться так и не смогла.

    1. seletedMenuItem->Next=NewItem;
      Вот такая конструкция не имеет смысла вообще. Потому что данные пункта меню хранятся во флеше (неизменяемой памяти). Этот метод специально так и сделан, чтобы не тратить драгоценную оперативную память.

      Поэтому структура меню — жестко задана.

      Как вариант решения твоей задачи — можно для каждого пункта меню дополнительно завести маску — в каких подменю оно должно быть. Например, если у тебя не более восьми режимов, то можно использовать uint8_t
      Выставляешь соответствующие битики видимости для каждого режима

      Как с ним работать: когда тебе нужно получить следующий/предыдущий пункт меню, то просто пропускаешь невидимые в данном режиме пункты меню (анализируешь по маске).

      Аккуратно надо будет отрабатывать ситуацию, когда невидим «хвост» меню — последние пункты. Чтобы не потерять текущий элемент.

  14. На Си не пишу. Есть несколько вопросов. В меню\подменю может быть разное количество пунктов. Где информация о количестве пунктов? Также как различать, выполнять функцию или вывести подменю?

  15. Здравствуйте. Уважаемый DI HALT, а можно статейку про так начинающим делать простенькие менюшки, и вообще про общий принцип построения меню,как их привязывать к коду и какие бывают типы: на таблицах на структурах и т.д.
    Например на МК сделаны таймеры, привязанные ко времени секунды, минуты, каждый таймер управляет ножкой порта МК включает нагрузку от чего либо, и выключает через установленный промежуток времени.
    Время устанавливается соответственно через меню пользователя.
    Заранее спасибо.

  16. Добрый День. Понимамю что старый пост, но может кто подскажет как в процедуре отрисовки меню сделать прокручивание курсора, а не пунктов…. Что-то не могу въехать….. Спасибо. DI_HALT и тебе тоже спасибо за сайт….

    1. Тебе нужно где-то хранить позицию курсора на экране. Ну и количество пунктов на экране.
      При этом при нажатии вверх-вниз кроме стандартного смещения по дереву нужно увеличивать или уменьшать позицию курсора, проверяя что делать при достижении края — прокручивать список оставляя его в последней строке или менять страницу и ставить курсор на первую строчку.

      Отображение будет практически таким же за исключением того, что текущую запись мы выводим в строку, где стоит курсор (с соответствующим цветом/выделением). Выше него (до края экрана) мы выводим предыдущие записи. А можем и не выводить, если курсор в первой строке. Ниже — последующие. Опять же, если курсор в последней строке — ничего выводить не надо. Выводится выше и ниже будет не фиксированное число записей (как у меня в примере), а зависящее от позиции курсора.

      1. Steel.ne спасибо. Т.е. мне в функцие keyMenu к кнопкам вверх, вниз прикрутить еще функцию (через диспетчер) для курсора, в ней сделать счетчик положений и потом в функцие dispMenu проверять счетчик….. Я просто применительно к твоему примеру….

  17. Вот наконец добил я свою менюшку кому мож пригодиться на lcd 4*20
    Низкий поклон DI HALT за разжеванный материал.
    Небольшой апгрейд меню на 4 строки с прорисовкой конца подпунктов.
    Меню русифицированное…..
    Вставлю свой код тут http://easyelectronics.ru/repository.php?act=view&id=97

  18. В статье «AVR. Учебный Курс. Архитектура Программ» DI HALT выделил такие типы программ:
    Суперцикл
    Суперцикл+прерывания
    Флаговый автомат
    Диспетчер
    Приоритетный диспетчер
    Кооперативная RTOS
    Вытесняющая RTOS
    Скажите пожалуйста к какому типу программы можно определить исходник выложенный в конце статьи?

    1. Разобрался сам. Программа далась эта очень тяжело. Разбирал ее с самодельным JTAG_ICE в реальном железе. Очень сложно было понять как крутится выполнение задач через выставления сообщений с параметрами.

  19. Смотри по ветке выше. Люди делали на разных компиляторах…
    Мой кусок с реально работающего ус-ва http://easyelectronics.ru/repository.php?act=view&id=97
    там много лишнего (для тебя),но меню рабочее подгони для себя.
    компилятор CVAVR с небольшим натягом можно подогнать под IAR.
    В принципе если знать где ковырять под любой подогнать можно.
    Главное разберись с организацией структуры описание пунктов

    typedef struct _selection
    {
    flash unsigned char *mas;
    unsigned char len ; // Указатель на длину Punktа
    void (*function)(void); //Указатель на функцию выполняющуюся по нажатии на enter/escape
    unsigned ent_f; //Флаг входа 4 бита — обычно ID меню в которое надо войти
    unsigned esc_f; //Флаг выхода 4 бита — обычно ID меню в которое надо вернуться
    }SELECTION;

    и отрисовка меню опираясь на структуру
    void print_menu()
    {
    if (menu[current_menu].num_selections «);
    lcd_gotoxy(1,1);
    dd_lcd(menu[current_menu].m[current_poz].mas,menu[current_menu].m[current_poz].len);
    lcd_gotoxy(1,2);
    dd_lcd(menu[current_menu].m[current_poz+1].mas,menu[current_menu].m[current_poz+1].len);
    }
    ……

    Для новичков проще начать CVAVR (не надо объявлять мне holywar про другие компиляторы),
    со временем сам остановишся на удобном тебе.

  20. Мне вот одно интересно. Зачем было убирать главное — функции из списка? Я когда начал си изучать, пытался разбираться с этим проектом. Мозги просто вскипели, но функций в упор не нашел. Решил отложить до тех пор, пока более-менее си не освою. Недавно опять взялся разбираться с вашим проектом. И увидел ясно, указателей на функции действительно нет. И хоть вы написали, что прикрутили micro menu для облегчения написания программ, но вы только усложнили сами себе задачу. Посмотрел источник, там все нормально. Есть указатели на списки, есть указатели на функции.
    Вот вместо того, чтобы изучить взаимодействие модулей, архитектуру программ вы впихиваете где ни попадя эти ваши самодельные «RTOS», диспетчеры… И других учите этой пагубной манере…

    1. 1. Эта статья — как организовать древовидный двусвязный список. Все. Ни на откровения по взаимодействию модулей ни на архитектуру программ я не претендую. Для этого есть очень полезные темы на форуме http://forum.easyelectronics.ru/viewtopic.php?f=4&t=18441 и http://forum.easyelectronics.ru/viewtopic.php?f=4&t=18159

      2. Написано по-русски: «Добавлен байт Select – это код команды, привязанный к текущему пункту.» Искать в пяти полях указатель на функцию — это уже за гранью добра и зла. Хочешь — ставь указатель. Код открыт.

      3. Критикуя — предлагай. Ни одной статьи от тебя я не увидел. Расскажи о взаимодействии модулей и архитектуре программ.

      1. Не нужно ничего искать, в вашем проекте структура. Я еще не полностью разобрался с micro menu (источником). Плюс у меня свои требования, хотелки. Напиши мне в личку свой емэйл. Когда что-то будет готово, отправлю.
        Проба пера была здесь Как статью написать, я не знаю. Так и не разобрался.
        По поводу взаимодействия модулей и архитектуры у меня пока у самого с этим слабовато. Но кое-какие принципы я уже описывал. Модули, активное использование КА, никаких долгих циклов. Функции разбиваются на логические блоки (КА, очень редко флаги). Быстрое выполнение кода — выход. Проверка условий — выход. Проверка условий, выполнение кода выход. Ну примерно как-то так. И мне до сих пор пока не требовались всякие RTOS, диспетчеры. Как я уже писал, на си недавно переполз. Как дело дойдет до графических дисплеев, вот тогда посмотрю в сторону чего-нибудь вытесняющего. Диспетчер или RTOS какая…

  21. Я правильно понял, что в этой реализации меню можно настраивать только один параметр на каждом конкретном экране? И отображать можно только текст?
    Я тоже делал что-то подобное (тоже на основе микро-меню), но с возможностью выводить цифры, мигающие курсоры, полоски-индикаторы, стрелочки и прочее подобное. Вместо const char Text[]; в структуре menuItem впилил указатель на массив структур, описывающую один элемент меню, а в самой структуре указываю координаты на дисплее, тип контента (текст, число, бегунок), его свойства (если число, то с точкой или нет, сколько знакомест занимает), привязана ли к нему какая-либо функция, и т.д.

  22. а если вместо параметра Seleckt в дефайне сделать указатель на функцию и запускать ее сразу в обработчике кнопок по нажатию? В чем минус такого подхода?

  23. Я за основу взял оригинальный проект, но имена оставил из этой статьи. Можно переделать проект под свои нужды. Но нужно садиться и прикидывать, рисовать на бумажке, что вы хотите получить.

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