文章目录

前言

一、UI界面介绍

 二、具体实现

1、平台

2、实现

(1)先测试OLED屏,学习板能否使用

(2)在主界面显示一级菜单

(3)多级菜单

三、总结


前言

对于智能车比赛而言,在决赛时,不允许下载程序,那么一个好的UI必不可少,同时好的UI也会让调参变得轻松。

一、UI界面介绍

由于程序里面设计到很多参数,这少不了需要设计多级菜单,那么主菜单(即一级菜单)用于存放参数类别,然后在每一个参数类别下面再设计一个界面显示具体的参数,除了显示具体参数之外,还要支持参数可修改。如下图所示:

 二、具体实现

在这一节,将一行一行代码带着大家将上图中的两级菜单实现,并且支持参数修改。

1、平台

逐飞STC32学习板,一块OLED屏

2、实现

(1)先测试OLED屏,学习板能否使用

#include "headfile.h"

void main()
{
	board_init();			// 初始化寄存器,勿删除此句代码。
	oled_init_spi();        //使用SPI通信
	// 此处编写用户代码(例如:外设初始化代码等)
	
    while(1)
	{
		oled_p6x8str_spi(0,0,"hello world!");// 显示hello world!
    }
}

正常显示,可以进行接下来的操作。

(2)在主界面显示一级菜单

写一个函数DisplayMain()用来显示一级菜单中的参数

//显示一级菜单
void DisplayMain(void)
{
	oled_p6x8str_spi(7,0,"1.LeftMotorPara");//左电机参数     		
    oled_p6x8str_spi(7,1,"2.RightMotorPara");//右电机参数       	
    oled_p6x8str_spi(7,2,"3.ServoPara");//舵机参数         	 
	oled_p6x8str_spi(7,3,"4.IMUPara");//IMU参数         	 
	oled_p6x8str_spi(7,4,"5.ElementPara");//元素参数         	
	oled_p6x8str_spi(7,5,"6.Start");//启动 
	oled_p6x8str_spi(7,6,"7.Stop");//停止
}

main()函数调用结果如图:

显示完成之后,还需要通过按键去选择,然后有一个光标显示选中的是哪个参数,这里先写一个按键的程序。

//key.h文件
//定义按键引脚
#define Up    	P70//上
#define Down    P71//下
#define Enter   P72//确认
#define Back    P73//返回
#define Key5    P45

//开关标志位
typedef enum
{
	key1_flag = 1,
	key2_flag = 2,
	key3_flag = 3,
	key4_flag = 4,
    key4_flag = 5
};



uint8 Get_Key_flag();
//key.c文件
#include "Key.h"

//开关状态变量
uint8 key1_status = 1;
uint8 key2_status = 1;
uint8 key3_status = 1;
uint8 key4_status = 1;
uint8 key5_status = 1;


//上一次开关状态变量
uint8 key1_last_status;
uint8 key2_last_status;
uint8 key3_last_status;
uint8 key4_last_status;
uint8 key5_last_status;



uint8 Get_Key_flag()//获取按键状态
{
	//保存按键状态
    key1_last_status = key1_status;
    key2_last_status = key2_status;
    key3_last_status = key3_status;
    key4_last_status = key4_status;
	key5_last_status = key5_status;

    //读取当前按键状态
    key1_status = Up;
	key2_status = Down;
	key3_status = Enter;
	key4_status = Back;
	key5_status = Key5;
    
	//检测到按键按下之后  并放开置位标志位
	if(key1_status && !key1_last_status)    return key1_flag;
	if(key2_status && !key2_last_status)    return key2_flag;
	if(key3_status && !key3_last_status)    return key3_flag;
	if(key4_status && !key4_last_status)    return key4_flag;
	if(key5_status && !key5_last_status)    return key5_flag;
	return 0;
}

通过Get_Key_flag()函数就可以判断哪个按键按下。接着写一个光标显示函数DisplayCursor():显示光标函数其实很好实现:只需要知道当前在哪一行,那么哪一行就显示">"。

int8 arrow = 0;//定义光标所在的行,初始化在第一行
void DisplayCursor()
{
	oled_p6x8str_spi(0,arrow,">");//在参数前面显示>
}

接下来写一个函数用来接收按键值,然后根据按键值来更新光标。

//在main()函数中只需要调用该函数即可
void UI()
{
	DisplayMain();
	DisplayCursor();
	UI_ContentKey();
}


//接收按键值,并更新arrow
void UI_ContentKey()
{
	uint8 key = Get_Key_flag();//获取按键值
    if(key == key1_flag){//向上 up
        oled_p6x8str_spi(0,arrow," ");  //需要将当前行的光标隐藏,不然当arrow更新之后,出现两行显示光标
		arrow--;//向上移动,arrow减小
	}
    if(key == key2_flag){//向下 down
        oled_p6x8str_spi(0,arrow," ");  arrow++;}//和up同理
    if(key == key3_flag){//进入子页面  enter
        oled_fill_spi(0x00);//需要将当前显示清屏,用于显示子界面    
		arrow = 0;
	}
    if(key == key4_flag){//返回上一个页面 back
        oled_fill_spi(0x00);    
		arrow = 0;
	}
	
	//对arrow限幅,OLED屏只能显示8行,所以需要限制 0 <= arrow <8
	if(arrow < 0)
		arrow = 7;//意思是:当按下up键,光标一直向上移动,当运动到第一行即arrow=0,
				  //再按下up时,光标直接跳到最后一行,arrow = 7
	else if(arrow >7)
		arrow = 0;

}

结果如图:

实现效果:

光标上下移动

主菜单显示完成,并且可以通过按键选择相应的参数,接下来就是根据选择的参数跳转到相应的子程序。

(3)多级菜单

当通过按键选择好主菜单参数后,按下enter键,则需要当前界面显示子界面(即二级菜单),最简单的方法就是人为给每一个界面设置一个ID,比如主界面ID为0,然后根据ID值去判断需要显示哪一个参数。

如图:

 在程序中,使用pagenum来表示ID。

和主菜单一样,先将每一个ID下的内容编写好:

void DisplayLeftMotorPara(void)//显示左电机参数
{
	oled_p6x8str_spi(7,0,"LeftMotorkp");       	oled_printf_float_spi(80,0,Pid_Left_Motor.kp,3,1);//3位整数,1位小数
	oled_p6x8str_spi(7,1,"LeftMotorki");      	oled_printf_float_spi(80,1,Pid_Left_Motor.ki,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,2,"LeftMotorkd");      	oled_printf_float_spi(80,2,Pid_Left_Motor.kd,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,3,"CurrentSpeed");       oled_printf_float_spi(80,3,LeftCurrentSpeed,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,4,"TargetSpeed");        oled_printf_float_spi(80,4,Pid_Left_Motor.TargetSpeed,3,1);//3位整数,1位小数
	oled_p6x8str_spi(7,5,"Out");         		oled_printf_float_spi(80,5,Pid_Left_Motor.out,3,1);//3位整数,1位小数
}
void DisplayRightMotorPara(void)//显示右电机参数
{
	oled_p6x8str_spi(7,0,"RightMotorkp");      	oled_printf_float_spi(80,0,Pid_Right_Motor.kp,3,1);//3位整数,1位小数
	oled_p6x8str_spi(7,1,"RightMotorki");      	oled_printf_float_spi(80,1,Pid_Right_Motor.ki,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,2,"RightMotorkd");      	oled_printf_float_spi(80,2,Pid_Right_Motor.kd,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,3,"CurrentSpeed");       oled_printf_float_spi(80,3,RightCurrentSpeed,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,4,"TargetSpeed");        oled_printf_float_spi(80,4,Pid_Right_Motor.TargetSpeed,3,1);//3位整数,1位小数
	oled_p6x8str_spi(7,5,"Out");         		oled_printf_float_spi(80,5,Pid_Left_Motor.out,3,1);//3位整数,1位小数
}
void DisplayServoPara(void)//显示舵机参数
{
	oled_p6x8str_spi(7,0,"Servokp");       		oled_printf_float_spi(80,0,Pid_Servo.kp,3,1);//3位整数,1位小数
	oled_p6x8str_spi(7,1,"Servoki");      		oled_printf_float_spi(80,1,Pid_Servo.ki,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,2,"Servokd");      		oled_printf_float_spi(80,2,Pid_Servo.kd,3,1);//3位整数,1位小数
	oled_p6x8str_spi(7,3,"Out");         		oled_printf_float_spi(80,3,Pid_Servo.out,3,1);//3位整数,1位小数
}
void DisplayIMUPara(void)//显示IMU参数
{
	oled_p6x8str_spi(7,0,"pitch_angle");       	oled_printf_float_spi(80,0,pitch_angle,3,1);//3位整数,1位小数
	oled_p6x8str_spi(7,1,"yaw_angle");      	oled_printf_float_spi(80,1,yaw_angle,3,1);//3位整数,1位小数
    oled_p6x8str_spi(7,2,"roll_angle");      	oled_printf_float_spi(80,2,roll_angle,3,1);//3位整数,1位小数
}
void DisplayElementPara(void)//显示元素参数
{
	oled_p6x8str_spi(7,0,"Ring");//圆环
	oled_p6x8str_spi(7,1,"Barrier");//障碍物
	oled_p6x8str_spi(7,2,"podao");//坡道
}
void DisplayRingPara(void)//显示圆环参数
{
	oled_p6x8str_spi(7,0,"RingFlag");			oled_int16_spi(80,0,RingFlag);
	oled_p6x8str_spi(7,1,"AngleTh");			oled_printf_float_spi(80,1,AngleTh,2,1);
	//其他变量根据需要增加
	//详情见:十八届智能车负压电磁组(四):赛道特殊元素处理篇
	// https://blog.csdn.net/weixin_51303932/article/details/134793651?spm=1001.2014.3001.5502
}
void DisplayBarrierPara(void)//显示障碍物参数
{
	oled_p6x8str_spi(7,0,"BarrierFlag");		oled_int16_spi(80,0,BarrierFlag);
	//其他变量根据需要增加
	//详情见:十八届智能车负压电磁组(四):赛道特殊元素处理篇
	// https://blog.csdn.net/weixin_51303932/article/details/134793651?spm=1001.2014.3001.5502
}
void DisplayPodaoPara(void)//显示坡道参数
{
	oled_p6x8str_spi(7,0,"PodaoFlag");			oled_int16_spi(80,0,PodaoFlag);
	//其他变量根据需要增加
	//详情见:十八届智能车负压电磁组(四):赛道特殊元素处理篇
	// https://blog.csdn.net/weixin_51303932/article/details/134793651?spm=1001.2014.3001.5502
}

每一个ID下的内容编写好之后,需要根据pagenum的数值来显示相应的内容,

void UI_Content(void)
{
	oled_fill_spi(0x00);//在显示新一级菜单时,需要清屏
	// 根据pagenum去显示菜单
	switch(pagenum)
    {
        case 0://显示主菜单
		{
			DisplayMain();
		}break;
		case 1://显示左电机参数
		{
			DisplayLeftMotorPara();
		}break;
		case 2://显示右电机参数
		{
			DisplayRightMotorPara();
		}break;
		case 3://显示舵机参数
		{
			DisplayServoPara();
		}break;
		case 4://显示IMU参数
		{
			DisplayIMUPara();
		}break;
		case 5://显示元素参数
		{
			DisplayElementPara();
		}break;
		case 6://显示圆环参数
		{
			DisplayRingPara();
		}break;
		case 7://显示障碍物参数
		{
			DisplayBarrierPara();
		}break;
		case 8://显示坡道参数
		{
			DisplayPodaoPara();
		}break;
		default://其他非法情况
		{
			DisplayMain();	
		}break;
	}
}

对于pagenum的更新,则通过按键来更改,即,当按下enter键时,根据光标当前所在的行来进入相应的子菜单;按下back键时,则需要返回上一级菜单。实现如下:

//返回上一级菜单
uint8 pagenumup(void)
{
	switch(pagenum)
	{
		case 0://在一级菜单
			return 0;
		case 1://在一级菜单
			return 0;
		case 2://在一级菜单
			return 0;
		case 3://在一级菜单
			return 0;
		case 4://在一级菜单
			return 0;
		case 5://在一级菜单
			return 0;
		//这里解释一下,当在二级菜单ID=6时,返回的上一级是ID=5
		//详情参考:十八届智能车负压电磁组(五):UI设计
		//
		case 6://在二级菜单
			return 5;
		case 7://在二级菜单
			return 5;
		case 8://在二级菜单
			return 5;
		
		default:
			return 0;
	}
}

//进入下一级菜单
//需要判断当前ID:知道在那一页,有些什么参数
//还需要判断光标所在行:知道要进入哪一个变量里面
uint8 pagenumdown(void)
{
	switch(pagenum)
	{
		case 0://在主界面
		{
			switch(arrow)//判断在第几行
			{
				case 0://在第0行---->对应着参数LeftMotorPara
				{
					return 1;//返回LeftMotorPara的ID
				}
				case 1: return 2;//对应RightMotorPara
				case 2: return 3;//对应ServoPara
				case 3: return 4;//对应IMUPara
				case 4: return 5;//对应ElemnetPara
			}
		}break;
		case 1://在LeftMotorPara界面
		{
			return 1;//返回当前界面
			//在该界面下没有下一级,就直接break;
		}break;
		case 2: return 2;break;//在RightMotorPara界面
		case 3: return 3;break;//ServoPara界面
		case 4: return 4;break;//IMUPara界面
		case 5://ElemnetPara界面
		{
			switch(arrow)
			{
				case 0: return 6;//对应Ring
				case 1: return 7;//对应Barrier
				case 2: return 8;//对应podao
				default : return 5;//返回当前界面
			}
		}break;
		
		default:break;
	}
}

//接收按键值,并更新arrow
void UI_ContentKey()
{
	uint8 key = Get_Key_flag();//获取按键值
    if(key == key1_flag){//向上 up
        oled_p6x8str_spi(0,arrow," ");  //需要将当前行的光标隐藏,不然当arrow更新之后,出现两行显示光标
		arrow--;//向上移动,arrow减小
	}
    if(key == key2_flag){//向下 down
        oled_p6x8str_spi(0,arrow," ");  arrow++;}//和up同理

/**********上面的key1,key2按键是用来上下移动光标的***********/
/**********下面的key3,key4按键是用来进入相应的父/子菜单的***********/

    if(key == key3_flag){//进入子页面  enter
        oled_fill_spi(0x00);//需要将当前显示清屏,用于显示子界面    
		pagenum = pagenumdown();//进入子界面 
		arrow = 0;//从第0行开始
	}
    if(key == key4_flag){//返回上一个页面 back
        oled_fill_spi(0x00);    
		pagenum = pagenumup();
		arrow = 0;
	}
	if(key == key5_flag)//更改参数
	{
		oled_fill_spi(0x00);    
		datapage = 1;    
		mul = 1;
	}

	//对arrow限幅,OLED屏只能显示8行,所以需要限制 0 <= arrow <8
	if(arrow < 0)
		arrow = 7;//意思是:当按下up键,光标一直向上移动,当运动到第一行即arrow=0,
				  //再按下up时,光标直接跳到最后一行,arrow = 7
	else if(arrow >7)
		arrow = 0;

}


UI()函数则需要更改为

void UI()
{
	UI_Content();//显示ID对应的函数
	UI_ContentKey();//更新ID
	
	DisplayCursor();//光标显示
	
}

通过main()函数调用后,可以达到进入子菜单和返回上一级菜单的效果。

实现效果:

UI多级菜单实现

接下来,讲解一下如何更改对应的参数,例如调电机PID时,需要更改kp,ki,kd三个参数,如果每调整一个参数都要下载依次程序,会很麻烦,所以UI必须支持更改参数。

思路:先确定当前在哪一页,哪一行,然后在新的界面显示出该变量。

float mul = 1;//倍率

//显示具体的参数
void UI_Datapage()
{
	uint8 x = 20,y = 40;
	oled_fill_spi(0x00);//清屏,新开一个界面用于显示需要修改的参数
	switch(pagenum)
	{
		case 0:break;//在ID为0的界面没有需要修改的参数
		case 1://对应LeftMotorPara
		{
			switch(arrow)
			{
				case 0://kp
				{
					oled_p6x8str_spi(x,0,"LeftMotorkp"); 
					oled_printf_float_spi(y,3,Pid_Left_Motor.kp,3,1);
				}break;
				case 1://ki
				{
					oled_p6x8str_spi(x,0,"LeftMotorki"); 
					oled_printf_float_spi(y,3,Pid_Left_Motor.ki,3,1);
				}break;
				case 2://kd
				{
					oled_p6x8str_spi(x,0,"LeftMotorkd"); 
					oled_printf_float_spi(y,3,Pid_Left_Motor.kd,3,1);
				}break;
				case 3: break;//CurrentSpeed---->不支持修改
				case 4://TargetSpeed
				{
					oled_p6x8str_spi(x,0,"TargetSpeed"); 
					oled_printf_float_spi(y,3,Pid_Left_Motor.TargetSpeed,3,1);
				}break;
				default:break;
			}
			oled_printf_float_spi(40,7,mul,4,3);//显示倍率
		}break;
		
		case 2://对应RightMotorPara
		{
			switch(arrow)
			{
				case 0://kp
				{
					oled_p6x8str_spi(x,0,"RightMotorkp"); 
					oled_printf_float_spi(y,3,Pid_Right_Motor.kp,3,1);
				}break;
				case 1://ki
				{
					oled_p6x8str_spi(x,0,"RightMotorki"); 
					oled_printf_float_spi(y,3,Pid_Right_Motor.ki,3,1);
				}break;
				case 2://kd
				{
					oled_p6x8str_spi(x,0,"RightMotorkd"); 
					oled_printf_float_spi(y,3,Pid_Right_Motor.kd,3,1);
				}break;
				case 3: break;//CurrentSpeed---->不支持修改
				case 4://TargetSpeed
				{
					oled_p6x8str_spi(x,0,"TargetSpeed"); 
					oled_printf_float_spi(y,3,Pid_Right_Motor.TargetSpeed,3,1);
				}break;
				default:break;
			}
			oled_printf_float_spi(40,7,mul,4,3);//显示倍率
		}break;
		case 3://对应ServoPara
		{
			switch(arrow)
			{
				case 0://kp
				{
					oled_p6x8str_spi(x,0,"Servokp"); 
					oled_printf_float_spi(y,3,Pid_Servo.kp,3,1);
				}break;
				case 1://ki
				{
					oled_p6x8str_spi(x,0,"Servoki"); 
					oled_printf_float_spi(y,3,Pid_Servo.ki,3,1);
				}break;
				case 2://kd
				{
					oled_p6x8str_spi(x,0,"Servokd"); 
					oled_printf_float_spi(y,3,Pid_Servo.kd,3,1);
				}break;
				default:break;
			}
			oled_printf_float_spi(40,7,mul,4,3);//显示倍率
		}break;			
		case 4:break;//对应IMUPara,不支持修改
		case 5:break;//对应ElementPara,不支持修改
		case 6://对应RingPara
		{
			switch(arrow)
			{
				case 0:break;//对应RingFlag,不支持修改
				case 1://对应AngleTh
				{
					oled_p6x8str_spi(x,0,"AngleTh"); 
					oled_printf_float_spi(y,3,AngleTh,3,1);
				}break;
				default:break;
			}
			oled_printf_float_spi(40,7,mul,4,3);//显示倍率
		}break;
		case 7://对应BarrierPara
		{
			//自行添加变量
			
		}break;
		case 8://对应podaoPara
		{
			//自行添加变量
		}break;
		
		default:break;
	}
}

通过按键去更改选中的参数

uint8 datapage = 0;//0:显示ID对应的函数,1:修改arrow对应的参数

//更改参数
void UI_DatapageKey()
{
	uint8 key = Get_Key_flag();
	if(key == key1_flag)//   +
	{
		switch(pagenum)//判断在哪一页
		{
			case 1://LeftMotorPara
			{
				switch(arrow)//哪一行
				{
					case 0:Pid_Left_Motor.kp += mul;break;
					//其他自行添加,这里做一个示范
					default:break;
				}
			}break;
			case 2://RightMotorPara
			{
				switch(arrow)
				{
					case 0:Pid_Right_Motor.kp += mul;break;
					//其他自行添加,这里做一个示范
					default:break;
				}
			}break;
			case 3://ServoPara
			{
				switch(arrow)
				{
					case 0:Pid_Servo.kp += mul;break;
					//其他自行添加,这里做一个示范
					default:break;
				}
			}break;
			// .....后面的就自己根据情况添加
			default:break;
		}
	}
	if(key == key2_flag)// -
	 {
		switch(pagenum)//判断在哪一页
		{
			case 1://LeftMotorPara
			{
				switch(arrow)//哪一行
				{
					case 0:Pid_Left_Motor.kp -= mul;break;
					//其他自行添加,这里做一个示范
					default:break;
				}
			}break;
			case 2://RightMotorPara
			{
				switch(arrow)
				{
					case 0:Pid_Right_Motor.kp -= mul;break;
					//其他自行添加,这里做一个示范
					default:break;
				}
			}break;
			case 3://ServoPara
			{
				switch(arrow)
				{
					case 0:Pid_Servo.kp -= mul;break;
					//其他自行添加,这里做一个示范
					default:break;
				}
			}break;
			// .....后面的就自己根据情况添加
			default:break;
		}
	}
	if(key == key3_flag)//更改倍率
    {
        mul = mul / 10;
    }
    if(key == key4_flag)// 确认更改参数
    {
        mul = mul * 10;
    }
	if(key == key5_flag)
	{
		datapage = 0;
	}
	
}

UI()函数更改为

void UI()
{
	if(!datapage)
	{
		UI_Content();//显示ID对应的函数
		UI_ContentKey();//更新ID
	}
	else
	{
		UI_Datapage();  //数据页
        UI_DatapageKey();   //数据按键处理
	}
	DisplayCursor();//光标显示
}

实现效果:

UI支持修改参数

至此,一个支持修改参数的UI完成。 

三、总结

在整个实现过程中,用到了5个按键(其实也可以用会更少的按键,那样代码会比现在的复杂一点,大家可以根据需要进行更改),而板子上只有四个按键,剩下的一个是复位键 RST,所以我用杜邦线一端接地,另外一端去接触芯片引脚。

至此,关于十八届智能车比赛源码的一些主要部分已经讲解完成,后面将会不定时更新一些其他有用的东西。

源码:UI设计icon-default.png?t=N7T8https://gitee.com/nameiscs/a-sleeping-bug.git

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐