多级菜单设计

UI简介

​对于做智能车、或需要大量调试的同学来讲,有一个优美实用的人机交互系统是十分关键的。本文提供一种编程思路,方便大家快速编写好属于自己的调参UI。
​当涉及到少量模式的更改,使用一个按键或者拨动开关是一个不错的选择,但做过调过车的朋友们知道,参数是满天飞,模式是一套接一套,这时没有一个便于扩展的框架也是非常挠头的。
​下图是我实际使用的一个示意图,本质就是一块OLED屏加上一个IO拓展,一是为了不同的单片机之间的迁移,二是有的单片机IO资源确实非常紧张,再者就是一个IIC解决了如此多的事,岂不快哉!
在这里插入图片描述

还是先简单介绍一个这玩意我的一个实现效果,节约一些不喜欢此类操作方式读者的时间。

首先整体界面重实用,没有骚气的画风和切换,按键对应的功能如下表。

按键功能
S1 - S9数字1-9
S11数字0
S10返回键;小数点输入;删除键
S12确认键

页面分为三种类型:菜单页面、调参页面、模式切换页面。

首先就是菜单页面,以主菜单为例,这样省滴!
在这里插入图片描述

父级页面中上方会显示当前页面标题,同时并列显示并标识所有的子级页面,每个子级标题占一行,行头标记索引号1-9,用户只需根据索引号按对应的按键S1-S9即可切换到子页面。子页面切换到它的子级同理,切回父级则按S10

其次就是调参页面,这样省滴。
在这里插入图片描述

​ 通过按索引号对应的按键便可调试修改该参数,如上图修改参数y。同时会出现一个>符号用以提示,此时依次按按键即可修改对应参数,默认为单精度浮点型输入,短按S10为小数点输入,长按S10为删除键,一次删除一位。如:输入1.2只需按S1,S10,S2,最后按S12确认即可。

最后就是模式切换界面,在上图中有体现这里就不放图了。

通过按对应索引号来切换成相反的模式或提前设置好的模式,一般是用在车体功能模块的开关、赛道元素的识别与否、车体模式的切换等等。

上边就是整个调参的一个实现效果,自我感觉还是非常方便友好的!

代码

​ 代码主要分为四部分,一是OLED驱动,二是IO扩展驱动,三是按键状态计算,四是调参UI系统。前两部分驱动库本文未提供,只提供了后两部分。

多的也不说了,直接上代码!总共有四个 Key.c, Key.h, Debug.c, Debug.h

先是按键状态计算库 Key.c, Key.h

#include "Key.h"

/*-------------------------------------------------------------------
 *@breif 初始化
 *-------------------------------------------------------------------
*/
void key_init(void)
{
    oled_init(); //屏幕初始化
    init_mcp23017(); //IO扩展板初始化
}


/*-------------------------------------------------------------------
 *@breif 按键状态更新 包括按下抬起、抬起按下、长按、按键是否按下
 *-------------------------------------------------------------------
*/
int i2c_key_update(IfKey* key)
{
    if(key->Time_Now +KEY_ELI >= HAL_GetTick())				//单位间隔时间内返回false值,即消抖,即按键有效状态保留时间
    {
        return 0;
    }
    uint16_t Vkey = 0;
    Vkey |= gpio_iic_read(portB); //读取按键状态
    Vkey <<= 8;
    Vkey |= gpio_iic_read(portA); //读取按键状态
    Vkey &= 0xFFF0;				  //清除多余位,保留前12位有效位 (因为实际使用的是12个按键)

    key.Last_State = key.Now_State;    //保存上一次按键值
    key->Now_State = Vkey ^ 0xFFF0;             按键当前状态
    key->Up_Down = key->Now_State & (key->Now_State ^ key->Last_State);	 //按键按下抬起过程
    key->Down_Up = key->Last_State & (key->Now_State ^ key->Last_State);  //按键抬起按下过程

    //下方为更新长按状态,不需要长按的可删去
    key->Time_Now = HAL_GetTick();		//更新时间线 单位:ms
    key->Continue = 0;				                      //清除原有持续按下值
    for(int i = 0; i < KEY_NUMBER; i++)
    {
        if(key->Now_State & (1 << i))			 			//检测持续按下
        {
            if(key->Time_Flag[i] == 0)key->Time_Flag[i] = key->Time_Now + KEY_DELAY;
        }
        else							   					//其他情况flag清零
        {
            key->Time_Flag[i] = 0;
        }
        if((key->Time_Now >= key->Time_Flag[i]) && key->Time_Flag[i] != 0) //时间超过或到达目标时间
        {

            key->Continue |= 1 << i;
            key->Time_Flag[i] = key->Time_Now + KEY_WAIT;          		//重置目标时间
        }
    }
    return 1;
}

/*
*读取按键抬起按下过程
*return:1 按下
*读取后会清除状态
*/
int get_key_up_down(IfKey* key, Key_t KeyValue)
{
	uint16_t up_down;
	up_down = key->Up_Down & Key;
	//读取后清除按键状态
    key->Up_Down ^= up_down;
    if (up_down)return 1;
	else return 0;	
}

/*
*检测按键按下抬起过程
*读取后会清除该状态
*/
int get_key_down_up(IfKey* key, Key_t KeyValue)
{
	uint16_t down_up;
    down_up = key->Down_Up & KeyValue;
	//读取后清除按键状态
	key->Down_Up ^= down_up; 
    if (down_up)return 1;
	else return 0;
}

/*
*读取按键当前是否正在按下
*return 1则正在按下
*/
int get_key_now_state(IfKey* key, Key_t KeyValue)
{
	uint16_t now_state;
    now_state = key->Now_State & KeyValue;
    if (now_state)return 1;
	else return 0;
}

/*
*读取按键长按间隔确认值,与上个函数的差别在于长按时每隔KEY_WAIT才返回1。
*读取后会清除状态
*/
int get_key_cont_enter(IfKey* key, Key_t KeyValue)
{
	uint16_t cont;
	cont = key->Continue & KeyValue;
	//读取后清除按键状态
	key->Continue ^= cont;
    if (cont)return 1;
	else return 0;
}


#ifndef __KEY_H__
#define __KEY_H__

#include "OLED.h"

#define KEY_ELI  10          // 消抖时间,按键有效状态保留时间
#define KEY_DELAY  800       //间隔长按开始时间	ms
#define KEY_WAIT   500      //间隔长按的间隔时间   ms
#define KEY_NUMBER 16       //按键个数

//这个枚举对应着各个按键所对应的bit位
typedef enum
{
    S1 =1<<(7+8),       //GPB7
    S4 =1<<(6+8),       //GPB6
    S7 =1<<(5+8),       //GPB5
    S10=1<<(4+8),       //GPB4		//返回键
    S2 =1<<(3+8),       //GPB3
    S5 =1<<(2+8),       //GPB2
    S8 =1<<(1+8),       //GPB1
    S11=1<<(0+8),       //GPB0
    S3 =1<<(7+0),       //GPA7
    S6 =1<<(6+0),       //GPA6
    S9 =1<<(5+0),       //GPA5
    S12=1<<(4+0),       //GPA4		//确认键
}Key_t;

typedef struct{
	uint16_t  Up_Down;					//检测抬起-按下状态
	uint16_t  Down_Up;					//检测按下-抬起状态
	uint16_t  Now_State;				//检测当前是否正在按下(按下就为1)
	uint16_t  Last_State;							
	uint16_t  Continue;				    //检测当前是否按下(间隔脉冲返回1)          
	uint32_t  Time_Flag[KEY_NUMBER];	//目标时间
	uint32_t  Time_Now;		   			//按键时间线
}IfKey;


//初始化
void key_init(void);

/* 此函数增加了消抖功能,消抖时间内返回0,按键读取成功返回1 */
extern int i2c_key_update(IfKey* key);

//读取按键抬起按下过程
extern int get_key_up_down(IfKey* key, Key_t KeyValue);

//读取按键按下抬起过程
extern int get_key_down_up(IfKey* key, Key_t KeyValue);

//读取按键当前是否正在按下
extern int get_key_now_state(IfKey* key, Key_t KeyValue);

//长按状态获取,1为执行长按,读取后按键值清除
extern int get_key_cont_enter(IfKey* key, Key_t KeyValue);

#endif 

然后就是UI库了 Debug.c, Debug.h

#include "Debug.h"

///这部分外设库自行对应
/*-------------------------------------
 *@breif OLED屏以8*16大小显示字符
 *-------------------------------------
*/
void showb(int x, int y, uint8_t* txt) {}
/*-------------------------------------
 *@breif OLED屏以6*8大小显示字符
 *-------------------------------------
*/
void showl(int x, int y, uint8_t* txt) {}
/*-------------------------------------
 *@breif OLED屏以6*8大小显示浮点型
 *-------------------------------------
*/
void showf(int x, int y, float Num, int size) {}
/*-------------------------------------
 *@breif OLED屏清除行显示
 *-------------------------------------
*/
void Oled_cls_line(int y) {}
/*-------------------------------------
 *@breif OLED屏清屏
 *-------------------------------------
*/
void Oled_cls() {}



/*-----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------*/
/*-------------------------------------
 *@breif 调参初始化
 *-------------------------------------
*/
void DebugInit(Debug_t* debug)
{
    //按键等硬件初始化
    key_init();
    //获取相关参数指针
    debug->chassis = get_chassis_point();
    debug->carmode = get_carmode_point();
}

/*-------------------------------------
 *@breif 调参主函数
 *-------------------------------------
*/
void DebugRun(Debug_t* debug)
{
    //按键状态定时读取
    if (!i2c_key_update(&debug->key)) return;

    if (get_key_cont_enter(S12)) //长按S12清屏
        oled_cls();

    /
    //调参界面
    /
    uint16_t enter = debug->key.Up_Down; //获取所有按键按下抬起状态

    if (debug->level_1 & _Para)
    {
        if (debug->level_2 & _Chassis)
        {
            //底盘页面下显示内容 以此为例
            showb(30, 0, "Chassis");
            showl(6, 2, "1.Kp"); showf(60, 2, debug->chassis.pid.Kp, 7);
            showl(6, 3, "2.Ki"); showf(60, 3, debug->chassis.pid.Ki, 7);
            showl(6, 4, "3.Kd"); showf(60, 4, debug->chassis.pid.Kd, 7);

            //这里即为浮点型参数输入,加了这个也就意味着该级页面下没有子页面了,因为子页面就是对应的调参
            if (debug->level_3)
            {
                number_in(debug, enter);
                if (enter & S12)
                {
                    debug->level_3 = 0; //清楚状态标记
                    Oled_cls();         //清屏
                    debug->point_site = debug->size = 0; //清除标记
                    debug->temp = 0;          //清除交换值
                }
            }
            if (debug->level_3 & _KP) debug->chassis.pid.Kp = debug->temp;
            else if (debug->level_3 & _KI) debug->chassis.pid.Ki = debug->temp;
            else if (debug->level_3 & _KD) debug->chassis.pid.Kd = debug->temp;
            else
            {
                if (enter & S1) debug->level_3 = _KP, Oled_cls_line(2), showl(0, 2, ">");
                else if (enter & S2) debug->level_3 = _KI, Oled_cls_line(3), showl(0, 3, ">");
                else if (enter & S3) debug->level_3 = _KD, Oled_cls_line(4), showl(0, 4, ">");
                else if (enter & S10)debug->level_2 = 0, Oled_cls(); //返回键
            }
        }
        else if (debug->level_2 & _PAGE2)
        {
            showb(44, 0, "_Page2");

            if (enter & S10)debug->level_2 = 0, Oled_cls(); //返回键
        }
        else
        {
            showb(40, 0, "Param");
            showl(0, 2, "1.Chassis");
            showl(0, 3, "2.Page2");
            if (enter)oled_cls();
            if (enter & S1)debug->level_2 = _Chassis;
            else if (enter & S2)debug->level_2 = _PAGE2;
            else if (enter & S10)debug->level_1 = 0, oled_cls(); //返回键
        }
    }
    else if (debug->level_1 & _Mode)
    {
        //模式切换页面确实就这么简单
        if (debug->level_2 & _Elements)
        {
            showb(32, 0, "Elements");
            showl(0, 2, "1.island");
            if (*(debug->carmode) & _Island)showl(100, 2, "OPEN"); else showl(100, 2, "OFF ");

            if (enter & S1); *(debug->carmode) ^= _ISLAND_L;
            else if (enter & S10)debug->level_2 = 0, Oled_cls();
        }
    }
    else
    {
        showb(36, 0, "Desktop");
        showb(0, 2, "1.");
        showl(16, 3, "Paraments");
        showb(0, 4, "2.");
        showl(44, 5, "Mode");
        if (enter)
        {
            if (enter & S1)debug->level_1 = _Para;
            if (enter & S2)debug->level_1 = _Mode;
            oled_cls();
        }
    }
}


//物理按键与数字一一映射
int number_enter(int enter)
{
    int out = 0;
    if (enter & S1)out = 1;
    else if (enter & S2)out = 2;
    else if (enter & S3)out = 3;
    else if (enter & S4)out = 4;
    else if (enter & S5)out = 5;
    else if (enter & S6)out = 6;
    else if (enter & S7)out = 7;
    else if (enter & S8)out = 8;
    else if (enter & S9)out = 9;
    else if (enter & S11)out = 0;
    else out = -1;
    return out;
}


//数字输入处理函数(不用管咋写的,调用就行)
void  number_in(Debug_t* debug, uint16_t enter)
{
    int Temp = number_enter(enter);
    if (Temp != -1)              //检测到数字输入时
    {
        debug->size += 1;
        if (debug->point_site == 0)//没有小数点时
        {
            debug->temp *= 10;
            debug->temp += Temp;
        }
        else                      //含有小数点时
        {
            for (int i = 0; i < debug->size - debug->point_site; i++)//恢复成整数
                debug->temp *= 10;
            debug->temp += Temp;
            for (int i = 0; i < debug->size - debug->point_site; i++)//变成实际小数
                debug->temp /= 10;
        }
    }
    if ((enter & S10) && !debug->point_site) //小数点未赋值
        debug->point_site = debug->size;
    if (get_key_cont_enter(&debug->key, S10)) //长按S10删除
    {
        if (debug->point_site == debug->size)debug->point_site = 0;
        else
        {
            if (debug->point_site)       //有小数点情况下
            {
                for (int i = 0; i < debug->size - debug->point_site; i++)//恢复为整数
                    debug->temp *= 10;
                int temp = (int)debug->temp;
                temp /= 10;
                debug->temp = (float)temp;
                debug->size -= 1;
                for (int i = 0; i < debug->size - debug->point_site; i++)//变为实际小数
                    debug->temp /= 10;
            }
            else                  //没有小数点情况下
            {
                int temp = (int)debug->temp;
                temp /= 10;
                debug->temp = (float)temp;
                debug->size -= 1;
            }
        }
    }
}
#ifndef DEBUG_H
#define DEBUG_H

#include "Key.h"

typedef struct Debug_t
{
	//参数指针
	chassis_move_t* chassis;
	uint32_t* carmode;

	//页面所属等级
	uint8_t level_1;
	uint8_t level_2;
	uint8_t level_3;

	//浮点输入计算需要用到的
	float temp;
	uint8_t  point_site;
	uint8_t  size;
	//按键状态信息
	IfKey key;
}Debug_t;


typedef enum
{
	/* 一级目录    */
	_Para = 1 << 0,
	_Mode = 1 << 1,
	_Run = 1 << 2,
	/* 二级目录    */
	//Para
	_Chassis = 1 << 0,
	_PAGE2 = 1 << 1,
	//_Mode
	_Elements = 1 << 0,
	//_Run
	_NORMAL = 1 << 0,
	/*	三级目录	*/
	//_Chassis
	_KP = 1 << 0,
	_KI = 1 << 1,
	_KD = 1 << 2,
	//_Elements
	_Island = 1 << 0,
}Debug_Catalog;


/*-------------------------------------
 *@breif 调参初始化
 *-------------------------------------
*/
void DebugInit(Debug_t* debug);

/*-------------------------------------
 *@breif 调参主函数
 *-------------------------------------
*/
void DebugRun(Debug_t* debug);

/*-------------------------------------
 *@breif 数字输入处理函数
 *-------------------------------------
*/
int number_enter(int enter);
void  number_in(Debug_t* debug, uint16_t enter);

#endif
	_Island = 1 << 0,
}Debug_Catalog;


/*-------------------------------------
 *@breif 调参初始化
 *-------------------------------------
*/
void DebugInit(Debug_t* debug);

/*-------------------------------------
 *@breif 调参主函数
 *-------------------------------------
*/
void DebugRun(Debug_t* debug);

/*-------------------------------------
 *@breif 数字输入处理函数
 *-------------------------------------
*/
int number_enter(int enter);
void  number_in(Debug_t* debug, uint16_t enter);

#endif
  • 2
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值