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