引言
OLED顾名思义就是一个屏幕, 我们让一个屏幕在特定的时间, 显示特定的画面, 就是我们所需要的, 因为这里是涉及到环境预警,所以需要加入一个应急接管页面的选项, 所以我们要把按键直接操作画面, 变成按键操作完, 我们根据优先级判断之后, 才能确定要显示的是哪个画面.
比如我们用户现在想听歌, 如果此时环境异常, 用户要操作的指令已经下发,但是在控制OLED界面的时候, 通过系统判断, 有更紧急的事情, 所以就要先处理预警系统的界面, 当处理完后, 再处理用户的普通指令, 所以, 我们控制OLED显示的画面的变量, 可以被用户的按键交互控制, 也可以被预警系统所控制.
本博客, 修改完的工程代码:
点击下载
https://wwyz.lanzoul.com/iJ6fR28p2bpe
OLED预警系统与按键交互架构图
菜单页面控制
OLED我们有能力显示字符了, 我们就要在特定时间选择特定的页面显示, 对于预警系统 和 用户交互功能, 我们进行分开判断,进行显示
OLED页面选择与底层刷新逻辑
回去(ctrl 加鼠标左键,快速跳转)
当我们知道要显示哪个界面, 剩下的就好办了, 我们只需要把特定的页面, 分成函数, 提前写好, 我们对应进行刷新就可以了, 但是单纯对于OLED界面刷新也是有技巧的, 因为我们刷新环境信息的时候, 如果页面包含文字和数据, 文字是不变的, 数据是实时刷新的, 我们不必重复刷新文字, 只是当页面第一次显示或者切换新页面的时候, 整体刷新一次, 后面就只刷新页面更新的数据就行了.(跳转下面链接,有详细演化过程)
OLED整体刷新到结合switch刷新方式演变
预警系统接管OLED菜单控制
回去2(ctrl 加鼠标左键,快速跳转)
预警系统界面是在环境异常的时候, 接管OLED界面的, 所以我们在控制OLED显示界面的时候, 要判断一下是否报警, 如果报警, 就不刷新用户正常情况下的界面, 而是和预警系统进行交互, 显示预警系统所显示的画面(跳转下面链接, 观看详细演示步骤)
预警系统OLED挑选模式
用户按键交互控制界面
我们完全可以把想显示的界面, 提前的纸上画出来, 但是我们用户想要显示特定的界面, 不像预警系统那样, 我们可以直接让预警系统直接接管, 因为用户的个性化需求, 比如在不同的时间段去听歌和观看家居信息 , 可能需要复杂的交互需求, 所以通过按键交互, 来赋予对应的界面交互状态, 从而实现用户所需的菜单交互控制。
按键收集
按键类型判断
回去(ctrl 加鼠标左键,快速跳转)
既然我们想要利用按键来收集用户的指令, 所以我们就要判断按键类型, 是单击,双击还是长按, 这样就可以减少按键的使用数量, 提升用户体验
按键,单击双击,长按区分(跳转观察详细博客)
利用队列收集和处理按键
回去(ctrl 加鼠标左键,快速跳转)
我们判断完按键类型, 如果直接进行处理, 如果当前有更重要的事情来做, 比如应急处理, 相对于用户的操作, 则是可以延后的, 因为这关乎了整个系统的安全性, 所以我们利用队列先及时的收集到按键信息, 等到系统正常运行时, 我们就可以进行处理用户按键交互, 从而显示界面了.
利用队列收集单双击和长按按键(跳转观察详细博客)
利用已有队列按键, 构造菜单交互
利用已有队列按键, 构造菜单交互(跳转观察详细博客)
当我们有了收集到的单双击和长按按键, 我们就可以根据这些按键类型, 来赋予对应的功能, 然后来控制菜单页面了, 比如我假设长按是, 切换智能页面和音乐页面, 单击在智能模式下,是切换环境信息界面和阈值界面。
双击, 在音乐界面, 是可以选中歌曲播放, 单击可以选择歌曲, 在播放的时候,单击又可以播放和暂停歌曲, 在不同的界面,都有不同的功能。
在了解了上述的功能后, 我们要清楚的在图纸上,画出我们要显示的所有界面, 并且在特定界面, 按下什么按键, 我们应该做什么, 跳转到哪个界面, 都是我们所需要构建的, 当我们知道了, 在当前界面, 按下了什么按键, 应该修改当前界面为哪个界面的时候, 我们的按键交互就构建好了。
代码实操
有了上面, 我们构建的理论, 我们现在就开始进行代码实操, 当用到哪些方面, 我会进行引导跳转到对应部分, 大家仔细阅读即可. 我也会对对应的代码进行解释. 我会站在开发者的角度上来 进行开发, 中间也会涉及到调试, 因为我们不可能一下子就开发处理出一个菜单按键, 并且每个步骤, 比如按键收集方面, 都需要进行调试, 所以我们先按照框架图, 构建底层.
先构建所有画面
我们首先从头开始构建OLED工程, 我们把所有的界面, 先定义成函数, 定义好, 这样, 我们就可以随意的显示了, 至于什么时机显示哪些页面, 是后面状态机菜单和预警系统的事情了, 我们首先完善底层
1.构建最小工程
跳转构建最小工程
其中第10步名字:
2.引入江协科技OLED的显示文件(其中图像和文字取模,我已经加入,所以复制我的即可)
最小例程上加OLED显示
3.然后在main.c函数内引入oled头文件, 然后初始化头文件, 我们显示一下页面
#include "stm32f10x.h" // Device header
//oled模块
#include "OLED.h"
int main(void)
{
/*OLED初始化*/
OLED_Init();
//开机界面
OLED_ShowImage(0,0,128,64, welcome);
OLED_Update();
while(1)
{
}
}
4.设置一下编译器为 version5
5.烧录入stlink
stm32最小系统版烧录方法
6.到此步骤, 本工程链接备份
点击下载
8.我们后面就是先画出所有的菜单页面, 然后进行取模了7.可以看到我们已经可以构建出``开机界面了
我们先看下我们要构建的页面
9.大概可以分成
页面刷新的原理和数据更新逻辑
页面刷新的原理和数据更新逻辑(ctrl 加鼠标左键,快速跳转)
开机界面
void oled_start(void)
{
if(oled.button_down == 1)
{
oled.button_down = 0;
OLED_ShowImage(0,0,128,64, welcome);
OLED_Update();
}
}
温湿度环境信息界面
//模式一1①主页面
void oled_show1(void)
{
if(oled.button_down == 1)
{
oled.button_down = 0;
OLED_Clear();
OLED_ShowChinese(0, 0, "温度");
OLED_ShowChinese(0, 24, "湿度:");
OLED_ShowChinese(0, 48, "烟雾浓度:");
OLED_Printf(48,0,OLED_8X16,"%2d",temp);
OLED_Printf(48,24,OLED_8X16,"%2d",humi);
OLED_Printf(80,48,OLED_8X16,"%2.1f",ad_value);
OLED_ShowChinese(80,0 , "℃");
OLED_ShowChinese(80,24 , "%");
OLED_ShowChinese(111,48 , "%");
OLED_Update();
}
else
if(oled.button_down == 0)
{
OLED_Printf(48,0,OLED_8X16,"%2d",temp);
OLED_Printf(48,24,OLED_8X16,"%2d",humi);
OLED_Printf(80,48,OLED_8X16,"%2.1f",ad_value);
OLED_Update();
}
}
阈值信息界面
//模式1② 温湿度烟雾阈值显示
void oled_show2(void)
{
if(oled.button_down == 1)
{
oled.button_down = 0;
OLED_Clear();
OLED_ShowChinese(0, 0, "温度阈值:");
OLED_ShowChinese(0, 24, "湿度阈值:");
OLED_ShowChinese(0, 48, "浓度阈值:");
OLED_Printf(80,0,OLED_8X16,"%3d",temp_th);
OLED_Printf(80,24,OLED_8X16,"%3d",humi_th);
OLED_Printf(80,48,OLED_8X16,"%3d",mq2_th);
OLED_ShowChinese(111,0 , "℃");
OLED_ShowChinese(111,24 , "%");
OLED_ShowChinese(111,48 , "%");
OLED_Update();
}
else
if(oled.button_down == 0)
{
OLED_Printf(80,0,OLED_8X16,"%3d",temp_th);
OLED_Printf(80,24,OLED_8X16,"%3d",humi_th);
OLED_Printf(80,48,OLED_8X16,"%3d",mq2_th);
OLED_Update();
}
}
温度报警界面
//模式1 ③ 应急模式
void oled_alarm1(void)
{
if(oled.button_down == 1)
{
oled.button_down = 0;
OLED_Clear();
OLED_ShowChinese(0, 0, "温度:");
OLED_ShowChinese(0, 24, "温度阈值:");
OLED_Printf(48,0,OLED_8X16,"%2d",temp);
OLED_Printf(80,24,OLED_8X16,"%3d",temp_th);
OLED_ShowChinese(80,0 , "℃");
OLED_ShowChinese(111,24 , "℃");
OLED_ShowChinese(0, 48, "红灯亮,风扇转");
OLED_Update();
}
else
if(oled.button_down == 0)
{
OLED_Printf(48,0,OLED_8X16,"%2d",temp);
OLED_Printf(80,24,OLED_8X16,"%3d",temp_th);
OLED_Update();
}
}
湿度报警界面
void oled_alarm2(void)
{
if(oled.button_down == 1)
{
oled.button_down = 0;
OLED_Clear();
OLED_ShowChinese(0, 0, "湿度:");
OLED_ShowChinese(0, 24, "湿度阈值:");
OLED_Printf(48,0,OLED_8X16,"%2d",humi);
OLED_Printf(80,24,OLED_8X16,"%3d",humi_th);
OLED_ShowChinese(80,0 , "%");
OLED_ShowChinese(111,24 , "%");
OLED_ShowChinese(0, 48, "蓝灯亮,风扇转");
OLED_Update();
}
else
if(oled.button_down == 0)
{
OLED_Printf(48,0,OLED_8X16,"%2d",humi);
OLED_Printf(80,24,OLED_8X16,"%3d",humi_th);
OLED_ShowChinese(80,0 , "%");
OLED_ShowChinese(111,24 , "%");
OLED_Update();
}
}
歌单
对于选中的歌曲反选, 需要通过判断我们选中的是哪首歌曲, 然后再进行反选的
void oled_musicmenu(void)
{
if(oled.button_down == 1)
{
oled.button_down = 0;
OLED_Clear();
OLED_ShowChinese(16, 0, "最强王者参见");
OLED_ShowChinese(16, 16, "我和我的祖国");
OLED_ShowChinese(16, 32, "热爱");
OLED_Printf(48,32,OLED_8X16,"%3d",105);
OLED_ShowChinese(72, 32, "℃的你");
OLED_ShowChinese(16, 48, "刚好遇见你");
if(oled.music_choice == 0)
{
OLED_ReverseArea(0, 0, 128, 16);
}
else
if(oled.music_choice == 1)
{
OLED_ReverseArea(0, 16, 128, 16);
}
else
if(oled.music_choice == 2)
{
OLED_ReverseArea(0, 32, 128, 16);
}
else
if(oled.music_choice == 3)
{
OLED_ReverseArea(0, 48, 128, 16);
}
OLED_Update();
}
}
歌曲播放
歌曲播放也是同理, 我们歌曲播放, 只是一个界面, 我们通过判断我们选择的哪首歌曲, 然后进行播放, 对于开始和暂停按钮, 也需要加入判断
void oled_music(void)
{
if(oled.button_down == 1)
{
oled.button_down = 0;
OLED_Clear();
if(oled.music_choice == 0)
{
OLED_ShowChinese(16, 8, "最强王者参见");
}
else
if(oled.music_choice == 1)
{
OLED_ShowChinese(16, 8, "我和我的祖国");
}
else
if(oled.music_choice == 2)
{
OLED_ShowChinese(16, 8, "热爱");
OLED_Printf(48,8,OLED_8X16,"%3d",105);
OLED_ShowChinese(72, 8, "℃的你");
}
else
if(oled.music_choice == 3)
{
OLED_ShowChinese(16, 8, "刚好遇见你");
}
//播放,暂停
if(oled.music_button == 0)
{
OLED_ShowImage(49,33,30,30,stop);
}
else
if(oled.music_button == 1)
{
OLED_ShowImage(49,33,30,30,start);
}
}
OLED_Update();
}
10.我们把这些页面集中起来, 然后进行选择播放
switch(oled.now_menu)
{
case 0: oled_start();break; //开机界面
case 1: oled_show1();break; //温湿度
case 2: oled_show2();break; //阈值
case 3: oled_alarm1();break; //温度报警
case 4: oled_alarm2();break; //湿度报警
// case 5: oled_alarm3();break; //烟雾浓度报警
case 6: oled_musicmenu();break; //歌单
case 7: oled_music();break; //歌曲播放
default: break;
}
预警系统接手界面
有了这个switch选择模式,其实我们就可以手动传入页面编号, 人脑控制交互来进行显示了, 但是毕竟人脑不能一直盯着系统来看, 所以我们就要把这些交给单片机了。特别是环境信息预警,一旦检测到环境异常,我们就要把界面转换到预警界面。
预警系统接管OLED菜单控制(ctrl 加鼠标左键,快速跳转)
1.在这里我们要定义一个预警模式变量
_Bool alarming = 0; //警报模式是否开启
2.在预警系统, 检测到环境异常时, 修改这个变量, 我们在赋值控制对应界面的时候, 去判断一下alarming是否报警, 如果环境正常, 就按照用户交互的模式进行显示, 如果异常, 那随之绑定的就是预警系统所指定的界面.(详细介绍, 请看上面跳转的博客, 对于完整的代码, 后续我会贴出,这里可能会用到结构体, 这是后续优化后的结果, 但是变量名字都是一致的)
用户按键交互与菜单控制
我们用户既然要进行控制, 那么首当其冲的就是按键识别, 收集到了足够多类型的按键, 我们根据按键再构建菜单, 那么, 我们就可以基于提前画好的界面,以及规定好的按键类型, 去进行跳转页面显示.
这样就构成了我们用户按键交互与菜单控制模型
按键识别
按键识别(流程思路)(ctrl 加鼠标左键,快速跳转)
1.我们现在来看下代码流程,
我们设置了PB1下降沿触发中断
2.当触发中断的时候, 我们就把按键标志位按键置成 1
3.然后在定时器里面启动计时, 还有对应的按键判断, 到达按键识别周期后, 我们就可以识别出按键了, 后面我们把按键存储到队列里面
void Key_type_judgment(void)
{
if(collectkey.button_atom == 0)//在处理函数外面,进行捕捉信息计时(默认处理函数时间很短,自动跳过处理函数)
{
//当 button_atom的时候,才能进行读取按键信息
if(collectkey.key_down == 1)
{
collectkey.key_down = 0;
collectkey.button_count++;
collectkey.count = 1; //开始计时
}
if(collectkey.count > 0)
{
collectkey.count++;
if(collectkey.count > 500)
{
collectkey.count = 0;
if(collectkey.button_count == 1)
{
collectkey.button_count = 0;
if(key_scan() == 0) //短按
{
collectkey.button_mode = 1;
//采集按键信息
collect_button();
}
else if(key_scan() == 1) //长按
{
collectkey.button_mode = 3;
//采集按键信息
collect_button();
}
}
else
if(collectkey.button_count == 2)
{
collectkey.button_count = 0;
collectkey.button_mode = 2;
//采集按键信息
collect_button();
}
else
{
collectkey.button_count = 0;//判断完,到时间照样得button_count 清零,为后面做准备
}
}
}
}
}
按键存储到队列
程序体量较少的时候, 我们本可以直接进行根据当前按下的按键进行处理的, 但是为了后续程序体量变大, 当有更紧急的事情时, 我们只能先记录下按键, 延后处理用户按键
利用队列收集和处理按键(思路)(ctrl 加鼠标左键,快速跳转)
void collect_button(void)
{
//采集按键信息
key.button_order[key.button_send] = collectkey.button_mode;
if(key.button_send >= 5) // 说明5的位置已经被填了,下个该0了(++后,所指位置为空)
{
key.button_send = 0;
}
else
{
key.button_send++;
}
}
用户根据按键信息构造交互界面
我们已经收集了足够多的按键, 我们现在就充当上帝, 来构造这些菜单之间的交互.
利用已有队列按键, 构造菜单交互(ctrl 加鼠标左键,快速跳转)
构造菜单:
我们把握一个点,就是我们当前处于什么模式, 在此页面下, 对于不同的按键,单击, 双击和长按, 我希望, 把此时的模式, 跳转到另外的哪个界面。我们提前画好
当我们构造完菜单后, 我们就知道, 当我们处于在哪个页面的时候, 按下哪个按键, 应该跳转到哪个状态.
1.直接上代码, 当我们队列收集到按键的时候, 就意味着队首和队尾不重合, 那么就代表有指令了
2.我们取出队首的按键, 先区分是什么类型
利用switch来区分, 我们对按键进行了编号(短按 1, 双按 2, 长按 3)
① 短按 1
在不同的模式下, 短按是不同的功能, 所以我们要先判断当前是处于哪个模式
处于智能模式 Now_mode = 1 , 说明是环境界面和阈值界面之间切换
处于音乐菜单模式 Now_mode = 2 ,说明我们要切换选择的歌曲,
处于音乐播放界面 Now_mode = 3 , 说明我们要暂停和开始播放歌曲
case 1:
switch(oled.Now_mode)
{
case 1:
oled.Now_environment ^= 1;
break;
case 2:
if(oled.music_choice == 3)
{
oled.music_choice = 0;
}
else
{
oled.music_choice++;
}
break;
case 3:
oled.music_button ^= 1;
break;
}
break;
② 双击
我们设置的功能是, 双击播放选择到的音乐,
在音乐菜单和播放界面, 进行切换, 所以我们就把模式在两个界面之间切换
case 2:
switch(oled.Now_mode)
{
case 1:break; //没想好干啥
case 2:
oled.Now_mode = 3;//进入音乐播放界面
oled.music_button = 1;//播放按钮打开
break;
case 3:
oled.Now_mode = 2;//返回音乐菜单
oled.music_button = 0;//播放按钮关闭
break;
default:break;
}
break;
③ 长按
长按就是我们在智能模式和音乐模式之间切换, 如果我们在播放模式 Now_mode = 3, 我们也是切换到智能模式 Now_mode = 1
case 3:
if(oled.Now_mode == 1)
{
oled.Now_mode = 2;//切换为音乐模式
oled.music_choice = 0;//音乐界面默认第一个
oled.music_button = 0;//播放按钮也关闭
}
else
{
oled.Now_mode = 1;//切换为智能模式
oled.Now_environment = 0;//界面默认第一个(环境信息)
oled.music_button = 0;//播放按钮也关闭
}
break;
4.当处理完一个按键后, 我们按键队列的队首就要向后移动一位
//然后跳到下一位,接着判断是否重合
if(key.button_deal >= 5) //队尾始终为空,向目标前进
{
key.button_deal = 0;
}
else
{
key.button_deal++;
}
根据处理后界面状态, 选择对应的界面
我们是如何知道我们用户按下了按键呢? 是因为我们检测到了 有新的按键, 就是队首和队尾不重合, 我们同时也定义了一个变量 button_down 代表按下了按键
当我们判断出按键按下,说明我们切换了界面 Now_mode, 我们就根据当前处于的模式,去选择对应的界面
我们先判断 Now_mode 是 1, 2,还是3, 从而选择提前准备好的界面
当 Now_mode = 1,
当处于智能模式下,我们再判断 Now_environment 是 0 还是 1
0: 处于环境信息界面 (oled.now_moding = 1;)
1: 阈值信息界面 (oled.now_moding = 2;)
当 Now_mode = 2,
当前处于音乐菜单模式, 我让当前用户界面oled.now_moding = 6;(本页面, 我们是歌单,因为单击也会反选歌曲,我们不可能为每个歌曲都添加一个页面, 而是通过判断选择的是哪首歌曲music_choice, 而得出是否反选对应的区域)
当 Now_mode = 3,
当前处于音乐播放模式, 并且根据我们画的页面菜单得出, 我们只能从歌单模式转换过来,所以我们已经有了对应选择的歌曲, 存放在music_choice里面(因为有四首歌曲, 我们不能为每首歌曲都加一个页面, 所以根据music_choice的数值, 通过if判断要显示哪些歌名就可以了)
界面已经确定, 开始刷新显示
经过上面 按键类型判断-> 队列收集按键-> 构造菜单页面 ->根据队列按键,挑选对应状态 -> 根据对应模式, 选择对应界面
我们已经能够确定, 我们要刷新是我们定义好的哪个界面, 我们 根据对应标号, 刷新底层就可以了
在最终接管控制的并不是 now_moding, 而是 now_menu, 这是因为, 我们这是预警系统, 如果有警报, 我们要以应急页面为主, 所以加了预警标志, 当正常情况下,预警没触发, 我们就按照用户的now_moding , 当预警触发, 我们就按照预警系统, 警报解除后, 通过判断, 仍会按照用户界面刷新
工程下载与演示
由于涉及到的步骤较多, 并且变量容易眼花缭乱, 所以我把按键收集构成了结构体, 还有oled处理也变成了结构体, 这样就方便进行归纳和移植. 下面我把相关的变量给大家整理一下, 并且带领大家熟悉一下, 上面的步骤, 走个流程, 为后续移植做出保障.
按键类型判断变量
功能代码 按键识别(ctrl 加鼠标左键,快速跳转)
//按键收集结构体重构
struct CollectKey
{
_Bool key_down;//按键是否按下
uint8_t button_count; //按键在一个判断周期内的计数次数
uint16_t count; //定时器计数毫秒数(按键按下时长)
uint8_t button_mode; //按键模式:无-0 , 单击- 1, 双击 - 2, 长按 - 3
_Bool button_atom; //当前正在处理,不可捕捉新按键信息
};
extern struct CollectKey collectkey; // 声明 collectkey 为外部变量
按键收集队列
功能代码:按键收集队列(ctrl 加鼠标左键,快速跳转)
struct Key_storage
{
//接受按键信息的容器
u8 button_order[6];
//控制输入按键信息的变量
u8 button_send;
//控制处理按键信息的变量
u8 button_deal;
};
extern struct Key_storage key;
我们加入了结构体, 就代表着这些变量就是为这一个功能区域服务的, 我们就不用担心后面变量乱飞了.
OLED菜单交互
//按键收集结构体重构
struct OLED_Choose
{
//控制按键是否按下(按键模式下, 按下按键则代表着要刷新页面)
uint8_t button_down; //按下 1 , 没按下 0
//oled界面
//********************重构变量********************
//****************模式切换************
uint8_t Now_mode; //模式选择:1-智能模式, 2-音乐模式 , 3-播放模式
uint8_t Now_environment; // 0- 环境信息界面, 1 阈值信息界面
uint8_t music_choice; // 0-最强王者参见,1-我和我的祖国,2-热爱105℃的你, 3刚好遇见你
uint8_t music_button; // 0-暂停, 1-播放
//*****************菜单控制******
u8 now_moding; //处理得出的结果的moing(没有警报的情况下, 是直接赋值给now_menu)
u8 now_menu; //当前的oled菜单
};
extern struct OLED_Choose oled;
因为用户不是一直在按按键, 所以我们也只是在用户按下按键的时候, 刷新选择界面, 所以我们要定义一个变量 button_down, 在队列中有按键的时候, 才刷新界面, 令 button_down = 1
当我们按下按键的时候, 我们button_down = 1的时候, 我们就根据构造的菜单和界面, 通过判断当前按下的是什么按键, 在当前界面是什么功能, 从而跳转到哪个模式, 进行修改界面状态
根据菜单,刷新界面
我们在上一步已经根据按键类型和当前处于的界面,进行了页面跳转, 那么我们现在就需要根据当前的菜单编号状态, 来确定 接下来用户要刷新的是哪个界面了
因为中间涉及到一个预警界面, 所以我们用户定义的界面, 不能直接传入刷新函数进行刷新, 就多定义了一个变量 now_moding , (方便预警系统插手)
控制最终刷新的界面的变量是 now_menu