十八届智能车负压电磁组(五):UI设计
对于智能车比赛而言,在决赛时,不允许下载程序,那么一个好的UI必不可少,同时好的UI也会让调参变得轻松。由于程序里面设计到很多参数,这少不了需要设计多级菜单,那么主菜单(即一级菜单)用于存放参数类别,然后在每一个参数类别下面再设计一个界面显示具体的参数,除了显示具体参数之外,还要支持参数可修改。
文章目录
前言
对于智能车比赛而言,在决赛时,不允许下载程序,那么一个好的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,所以我用杜邦线一端接地,另外一端去接触芯片引脚。
至此,关于十八届智能车比赛源码的一些主要部分已经讲解完成,后面将会不定时更新一些其他有用的东西。
更多推荐
https://gitee.com/nameiscs/a-sleeping-bug.git


所有评论(0)