Быстрый ответ Алисы AI
Проектирование в стеснённых условиях — это действительно особый вызов. Я бы сказала так: это не просто «меньше места», а необходимость искать нестандартные, но надёжные решения, чтобы совместить задачу с жёсткими ограничениями.
В моей практике пришлось разрабатывать программную часть измерительных приборов, источников тока, испытательных установок автономных, не связанных с компьютером. Меню как помощник и путеводитель пользователя было необходимо. Хочу поделиться опытом разработки своей библиотеки.
Во всех устройствах был обычный джентльменский набор: дисплей, 4 строки по 20 символов и клавиатура, 16 клавиш. Схематически это выглядело так:
Клавиатура
Базовые функции работы с клавиатурой.
unsigned char mgetc(void) {…} // чтение кода нажатой клавиши
Фунцция mgetc() возвращает код нажатой клавиши или 0xFF, если ничего не нажато. Её реализация зависит от схемотехнического решения подключения к прибору.
unsigned char mgetcnw(void) { // чтение кода нажатой клавиши без ожидания
unsigned char ch;
if ((ch = mgetc())== 0xFF) // ничего не нажато
return ch;
while (mgetc() != 0xFF); // клавиша нажата, ожидание отпускания
return ch;
}
Функция mgetcnw() базируется на mgetc(), не ожидает нажатия, и возвращает либо код нажатой клавиши, либо 0xFF. Но есть одна особенность, если клавиша нажата, она ждет отпускания. Это позволяет исключить многократную реакцию на одно нажатие.
unsigned char mgetcw(void) { // чтение кода нажатой клавиши с ожиданием
unsigned char ch;
while ((ch = mgetcnw()) == 0xFF); // ожидание нажатия
return ch;
}
Функция mgetcw() базируется на mgetcnw(), ожидает нажатия, и возвращает код нажатой клавиши. Вот собственно и все о клавиатуре.
Дисплей
С дисплеем все довольно просто. Содержание базовых функций работы с дисплеем зависит от конкретного типа дисплея.
code struct {
int ns; // число строк
int ls; // длина строки (на будущее)
} Disply = {4,20};
Disply, это структура, описывающая дисплей.
Два макроса управления отображением курсора.
#define KURSOR_ON InitCommIND(0x0E) // включить отображение курсора
#define KURSOR_OFF InitCommIND(0x0C) // выключить отображение курсора
int PutcIND(char ch, unsigned char y, unsigned char x)
Функция PutcIND() выводит символ.
int PutsIND(char *str, unsigned char y, unsigned char x)
Функция PutsIND() выводит массив символов.
void ClrIND(void)
Функция ClrIND() чистит экран.
void MoveCur(unsigned char y, unsigned char x)
Функция MoveCur() перемещает курсор.
Параметры y, x– это номерстрокии номер позиции в строке. Нумерация строк и символов в строке начинается с 0.
Меню
Идею меню можно сформулировать так: это массив строк, каждая из которых имеет ссылку либо на другое меню, либо на функцию. Ниже я привожу макросы и структуры, реализующие эту идею.
// Макроязык описания меню
#define MENU_DEF code struct Menu // меню
#define MENU_POINT xdata struct Menu* // указатель на меню
#define MENU_COUNT xdata struct MenuI // отображение меню на дисплее
#define STRINGS code char // строки меню в памяти code
#define STRINGSx xdata char // строки меню в памяти xdata
#define MENU_PAR(M) &M // указатель на меню родитель
#define MENU_SONS code struct Menu // меню, потомки
#define FUNCTIONS code void // функции пользователя
#define MENU_INI(M) M.ni=M.nb=0 // меню на дисплее, инициализация
#define MENU_INI_NI(M,NI) M.ni=NI;M.nb=0 // инициализация, текущая строка NI
#define SIZE(N) (sizeof(N)/sizeof(char *)) // размер массива строк меню
Примечание: code – постоянная память команд и констант, xdata – оперативная память данных. Структура MENU_DEF располагается в памяти code, структура MENU_COUNT в памяти xdata.
// Описание меню
MENU_DEF { // объединяющая структура
int ns; // число строк меню
char **St; // массив строк меню
struct Menu *Par; // ссылка на меню предка
struct Menu **Sons; // ссылка на список меню потомков
void(**Fun)(void); // функции пользователя
struct MenuI *Mi; // ссылка на MENU_COUNT
};
Размерность массивов Sons и Fun равна ns – количеству строк меню. Если строка ссылается на другое меню, соответствующий элемент массива Fun равен 0 и, наоборот, если строка ссылается на функцию, соответствующий элемент массива Sons равен 0.
MENU_COUNT { // текущее отображение меню на дисплее
int nb; // номер первой строки на экране
int ni; // номер текущей строки
};
Я использую понятие текущее меню — это меню, отображаемое на дисплее в настоящий момент.
MENU_POINT MenuTek; // Cсылка на текущее меню
На базе структур MENU_DEF и MENU_COUNT получаются следующие функции.
// Функции работы с меню
// Функция MenuDisp() отображает на экране текущее состояние меню MenuTek.
void MenuDisp(void) { // отобразить меню MenuTek
int n, ns; // определение отображаемой части меню
ns = (MenuTek->ns <= Disply.ns) ? MenuTek->ns : Disply.ns;
ClrIND();
for (n = 0; n < ns; ++n){ // перерисовка экрана
PutsIND(» «, n, 0);
PutsIND(MenuTek->St[MenuTek->Mi->nb + n], n, 1);
}
PutsIND(«>», (MenuTek->Mi->ni — MenuTek->Mi->nb), 0); // «>»-маркер текущей строки
}
// Функция MenuNext() отрабатывает переход к следующей строке меню.
void MenuNext(void) { // на строку вниз
if (MenuTek->Mi->ni < MenuTek->ns-1){
MenuTek->Mi->ni++; // переход на следующую строку
if ((MenuTek->Mi->ni — MenuTek->Mi->nb) >= Disply.ns)
MenuTek->Mi->nb++; // скроллинг вверх
MenuDisp();
}
}
// Функция MenuPrev() отрабатывает переход к предыдущей строке меню.
void MenuPrev(void) { // на строку вверх
if (MenuTek->Mi->ni > 0){
MenuTek->Mi->ni—; // переход на предыдущую строку
if (MenuTek->Mi->ni < MenuTek->Mi->nb)
MenuTek->Mi->nb—; // скроллинг вниз
MenuDisp();
}
}
// Функция MenuEnter() отрабатывает выбор текущей строки меню.
void MenuEnter(void) { // выбор подменю или функция
if(*(MenuTek->Fun[MenuTek->Mi->ni]) != 0) // функция
(*(MenuTek->Fun[MenuTek->Mi->ni]))();
else
if(MenuTek->Sons[MenuTek->Mi->ni] != 0){ // подменю
MenuTek = MenuTek->Sons[MenuTek->Mi->ni];
MenuTek->Mi->ni=MenuTek->Mi->nb=0;
}
MenuDisp(); // отображение текущего меню
}
// Функция MenuBack() отрабатывает выход из текущего меню.
void MenuBack(void) { // к родителю
if (MenuTek->Par == 0) // родитель отсутствует
return;
ClrIND();
MenuTek = MenuTek->Par; // переход к родителю
MenuDisp();
}
// Функция UserFun() — пример пользовательской функции.
void UserFun(void) { // пользовательская функция
ClrIND();
PutsIND(«Пользовательская», 0,0);
PutsIND(«функция …», 1,0);
while(mgetcw() == 0xFF);
}
Как отрабатывать нажатие клавиш и работу с меню показываю ниже.
// Клавиши работы с меню
#define KEY_PREV kA // вверх/влево
#define KEY_NEXT kB // вниз/вправо
#define KEY_ENTER kD // выбор
#define KEY_ESC kC // выход
void Menu(unsigned char KeyIn) { // работа с меню по нажатию клавиш
switch(KeyIn){
case KEY_PREV: // вверх
MenuPrev();
break;
case KEY_NEXT: // вниз
MenuNext();
break;
case KEY_ENTER: // выбор
MenuEnter();
break;
case KEY_ESC: // выход
MenuBack();
break;
}
}
Вот, собственно и все. Примеры ниже.
Меню. Пример №1 (фрагменты)
// Главное меню
Корневое меню MenuMain состоит из трех пунктов. Выбор первых двух приводит к вызову функций FunON() и FunOFF() соответственно. При выборе третьего, текущим становится меню MenuTok.
|
> Включение ламп Выключение ламп Задать ток |
STRINGS *StMain[] = {«Включение ламп», «Выключение ламп», «Задать ток»};
FUNCTIONS (*FunMain[])() = {FunON, FunOFF, MenuTokIni};
MENU_SONS *SonsMain[] = {0,0,0};
MENU_COUNT MiMain = {0,0};
MENU_DEF MenuMain = {SIZE(StMain), StMain, MENU_PAR(0), SonsMain, FunMain, &MiMain};
// Меню «Задать ток»
В меню MenuTok содержание строк меняется функцией MenuTokIni(). Массивы L1[],L2[],L3[] располагаются в оперативной памяти xdata.
|
> Лампа №1 4.00 A Лампа №2 5.00 A Лампа №3 6.00 A |
STRINGSx L1[21], L2[21], L3[21];
STRINGS *StTok[] = {L1, L2, L3};
FUNCTIONS (*FunTok[])() = {LampTok, LampTok, LampTok};
MENU_SONS *SonsTok[] = {0,0,0};
MENU_COUNT MiTok = {0,0};
MENU_DEF MenuTok = {SIZE(StTok), StTok, MENU_PAR(MenuNast), SonsTok, FunTok, &MiTok};
// Иницилизация меню «Задать ток»
xdata float Tok[N_LAMP]; // массив токов ламп
void MenuTokIni(void){
int i;
for(i=0; i<N_LAMP; ++i)
sprintf(StTok[i], » Лампа №%d: %.2fA», i+1, Tok[i]);
MenuTek = &MenuTok; // затем MenuDisp() перерисует экран
}
// Функция «Задать ток»
void LampTok(void){
int nLamp, yn;
nLamp = MenuTok.Mi->ni; // номер лампы
yn = MenuTok.Mi->ni — MenuTok.Mi->nb; // положение строки на экране
getfloatN(yn, 12, &Tok[nLamp], StTok[nLamp], 4);
}
Меню. Пример №2 (фрагменты)
Этим примером я покажу дополнительные приемы работы с меню. Итак, меню список ламп, назовем его MenuEnter.
|
>Лампа №1 Выбрать Лампа №2 Выбрать Лампа №3 Выбрать |
Когда лампа выбрана, активизируется меню MenuSet. В MenuSet задаются и измеряются токи. Когда выбрана строка Рассчитать, вычисляются поправочные коэффициенты для ЦАП и АЦП, и активизируется меню MenuSave.
|
>4A 4.00 4.00 Задать 5A 5.00 5.00 Задать 6A 6.00 6.00 Задать Рассчитать <D> |
Меню MenuSave отображает результаты рассчета и предлагает сохранить значения KEY_ENTER или выйти без сохранения KEY_ESC.
|
ЦАП: А=1.00 В=0.00 АЦП: А=1.00 В=0.00 >Записать <D> |
Примечание: ЦАП, АЦП — цифро-аналоговый и аналого-цифровой преобразователи. О методе корректировки значений, задаваемых в ЦАП или читаемых из АЦП, можно прочесть в моей статье «Метод наименьших квадратов Гаусса с весовыми коэффициентами отклонений»
// Меню ВЫБРАТЬ
STRINGS *StEnter[] = {”Лампа №1 Выбрать”, ”Лампа №2 Выбрать”, ”Лампа №3 Выбрать”}; // список ламп
FUNCTIONS (*FunEnter[])() = {MenuSetIni, MenuSetIni, MenuSetIni};
MENU_SONS *SonsEnter[] = {0,0,0};
MENU_COUNT MiEnter = {0,0};
MENU_DEF MenuEnter = {SIZE(StEnter), StEnter, MENU_PAR(0), SonsEnter, FunEnter, &MiEnter};
Функция MenuSetIni() каждый раз инициализирует меню MenuSet для выбранной лампы.
// Меню ЗАДАТЬ
STRINGSx Set1[21]; // 4A x.xx x.xx Задать
STRINGSx Set2[21]; // 5A x.xx x.xx Задать
STRINGSx Set3[21]; // 6A x.xx x.xx Задать
STRINGSx Set4[21]; //>РАССЧИТАТЬ
STRINGS *StSet[] = {Set1, Set2, Set3, Set4}; // строки меню
FUNCTIONS (*FunSet[])() = {FSet,FSet,FSet,FRass};
MENU_SONS *SonsSet[] = {0,0,0,0};
MENU_COUNT MiSet = {0,0};
MENU_DEF MenuSet = {SIZE(StSet), StSet, MENU_PAR(MenuEnter), SonsSet, FunSet, &MiSet};
Функция FSet() задает и измеряет ток.
Функция Frass() рассчитывает поправочные коэффициенты и делает текущим меню MenuSave.
// Меню РАССЧИТАТЬ: >Записать
STRINGSx Save1[21]; // ЦАП
STRINGSx Save2[21]; // АЦП
STRINGSx Save3[21]; // пустая строка
STRINGSx Save4[21]; // ЗАПИСАТЬ
STRINGS *StSave[] = {Save1, Save2, Save3, Save4}; // строки меню
FUNCTIONS (*FunSave[])() = {0,0,0,FSaveAB};
MENU_SONS *SonsSave[] = {0,0,0,0};
MENU_COUNT MiSave = {0,0};
MENU_DEF MenuSave = {SIZE(StSave), StSave, MENU_PAR(MenuEnter), SonsSave, FunSave, &MiSave};
Функция FSaveAB() сохраняет результаты, затем по клавише KEY_ESC переходит в меню MenuEnter.
// Рабочие массивы
code float TokXt[]={4,5,6}; // ток задаваемый в ЦАП, технический массив
xdata float TokX[N_IZM]; // токи измеренные АЦП
xdata float TokY[N_IZM]; // токи измеренные прибором, вводятся с клавиатуры
// Иницилизация меню Задать
void MenuSetIni(void){
int nIzm;
MENU_INI(MiSet);
for(nIzm=0; nIzm<N_IZM; ++nIzm)
sprintf(StSet[nIzm], «%.0fA %.2f %.2f Задать», TokXt[nIzm], 0, 0);
strcpy(Set4,»РАССЧИТАТЬ»);
MenuTek = &MenuSet;
}
// Задание и измерение тока лампы
void FSet(void){
int nLamp, nIzm; unsigned int TokIzmKod;
nLamp = MenuEnter.Mi->ni; // номер лампы из меню MenuEnter
nIzm = MenuSet.Mi->ni; // номер измерения из меню MenuSet
// задание тока, ЦАП
START_PROCESS(TokOnMetr); // запуск процесса TokOnMetr, плавного наращивания тока лампы
while(!END_PROCESS(TokOnMetr)); // ожидание завершения TokOnMetr
// измерение тока, АЦП
TokIzmKod = ConvADC(AdrADC[nLamp], N_IZM_ADC); // усредненное значение кода тока
TokX[nIzm] = TokIzmKod*ADCv_MAX/ADCc_MAX; // пересчет в амперы
// измерение прибором, ввод значения с клавиатуры
sprintf(StSet[nIzm], «>%.0fA %.2f %.2f Задать», TokXt[nIzm], TokX[nIzm], 0);
getfloatN(&TokY[nIzm], StSet[nIzm], nIzm, 9, 4); // ввод значения, измеренного прибором
// перерисовка текущей строки меню MenuSet
sprintf(StSet[nIzm], «%.0fA %.2f %.2f Задать», TokXt[nIzm], TokX[nIzm], TokY[nIzm]);
}
Примечание: о процессах можно прочесть в моей статье «Интерпретация параллельных процессов в среде языка «С» микроконтроллера ADuC и ему подобных».
// Рассчет поправочных коэффициентов (фрагмент)
void FRass(void){
// рассчет, затем:
MenuSaveIni(); // Иницилизация меню MenuSave
MenuTek = &MenuSave; // меню MenuSave становится текущим
}
// Иницилизация меню Записать
void MenuSaveIni(void){
int nLamp;
nLamp = MenuEnter.Mi->ni; // из меню MenuEnter получаем номер лампы
MENU_INI_NI(MiSave,3); // становимся на строку «ЗАПИСАТЬ»
// Поправочные коэффициенты для ЦАП и АЦП
sprintf(Save1, «ЦАП: A=%.2f B=%.2f», KA_DAC[nLamp], KB_DAC[nLamp]);
sprintf(Save2, «АЦП: A=%.2f B=%.2f», KA_ADC[nLamp], KB_ADC[nLamp]);
strcpy(Save3,» «);
strcpy(Save4,»ЗАПИСАТЬ»);
}
Клавиатура. Пример №3
Здесь я приведу несколько функций из библиотеки «Клавиатура».
// Выбор из списка
// Параметры: ym, xm – положение на экране; st – массив строк; sti – начальная строка; stn – количество строк. Возвращает номер выбранной строки.
unsigned char getlist(unsigned char ym, unsigned char xm, char **st, unsigned char sti, unsigned char stn){
unsigned char ch, stii;
ClrIND(); stii = sti; // запоминаем номер начальной строки
PutsIND(st[stii], ym, xm);
while(1){
ch=mgetcw();
if(ch == KEY_PREV){ // вверх по списку
if(stii > 0)—stii;
}
else if(ch == KEY_NEXT){ // вниз по списку
if(stii < stn-1)++stii;
}
else if(ch == KEY_ENTER)return stii; // выбор
else if(ch == KEY_ESC)return sti; // отказ
PutsIND(st[stii], ym, xm); // вывод текущей строки
}
}
code char chk[11]=»0123456789«; // служебный массив
// Ввод цифровых символов
// Параметры: ym, xm – положение на экране; chs – редактируемый массив; lm – количество символов.
unsigned char getchs(unsigned char ym, unsigned char xm, char *chs, unsigned char lm){
unsigned char ch, xi, xb, xe;
xi = xb = xm; // первый и текущий символ
xe = xb+lm-1; // последний символ
MoveCur(ym, xi);
while(1){
ch=mgetcw();
if(ch >= k0 && ch <= k9){ // цифра
chs[xi] = chk[ch];
PutcIND(chk[ch],ym,xi);
if(xi < xe)++xi;
}
else if(ch == kS){ // пробел
chs[xi] = ‘ ‘;
PutcIND(‘ ‘,ym,xi);
Xi = (xi < 19)?xi+1:xi;
}
else if(ch == kZ){ // точка
chs[xi] = ‘.’;
PutcIND(‘.’,ym,xi);
Xi = (xi < 19)?xi+1:xi;
}
else if(ch == KEY_PREV){ // влево курсор
if(xi > xb)—xi;
}
else if(ch == KEY_NEXT){ // вправо курсор
if(xi < xe)++xi;
}
else if(ch == KEY_ENTER){ // ввод
chs[xe+1] = ‘\0’;
return RET_OK;
}
else if(ch == KEY_ESC){ // отказ
chs[xe+1] = ‘\0’;
return RET_ERR;
}
MoveCur(ym, xi)
}
}
// Ввод целого
// Параметры: ym, xm – положение на экране и в буфере; указатель на результат, st – буфер размером 21; lm – размер поля ввода.
unsigned char getint(unsigned char ym, unsigned char xm, int mem, char st, unsigned char lm){
KURSOR_ON;
PutsIND(st, ym, 0); // вывод st на экран
if (getchs(ym, xm, st, lm) == RET_OK){ // KEY_ENTER
sscanf(&st[xm], «%d», mem);
KURSOR_OFF;
return RET_OK;
}
KURSOR_OFF;
return RET_ERR; // KEY_ESC
}
// Ввод плавающего с точкой
// Параметры: ym, xm – положение на экране и в буфере; указатель на результат, st – буфер размером 21; lm – размер поля ввода.
unsigned char getfloatN(unsigned char ym, unsigned char xm, float mem, char st, unsigned char lm){
KURSOR_ON;
PutsIND(st, ym, 0); // вывод st на экран
strcpy(chs, st);
if (getchs(ym, xm, lm)==RET_OK){ // KEY_ENTER
sscanf(&chs[xm], «%f», mem);
KURSOR_OFF;
return RET_OK;
}
KURSOR_OFF;
return RET_ERR; // KEY_ESC
}
ссылка на оригинал статьи https://habr.com/ru/articles/1063030/