【嵌入式学习笔记】OLED超容易小作业:简单多级菜单交互
本文介绍了基于嵌入式系统的OLED显示与按键交互实现方案。通过全局变量管理模式状态(增加/减少)和数值(0-10),采用刷新标志位机制优化显示性能。具体实现包括:1)使用KEY6切换工作模式;2)通过KEY5在不同模式下增减数值;3)OLED实时显示当前模式和数值。系统采用事件驱动设计,仅在数据变化时刷新屏幕,有效避免了I2C总线资源浪费。文章详细解析了状态管理、按键控制和显示刷新的协作机制,并强
前言
注意:本博客是基于前面所有博客的知识的小作业,不可单独食用。
实验任务
在提供的工程模板基础上,修改 oled_app 和 btn_app 相关文件,实现以下交互功能:
模式切换逻辑(Mode Control)
-
操作按键:KEY6(对应
USER_BUTTON_5)。 -
功能要求:每按下一次 KEY6,系统工作模式在 “模式 I (Increment)” 与 “模式 D (Decrement)” 之间循环切换。
-
显示要求:OLED 第一行实时显示当前模式,格式为
Mode: I或Mode: D。
数值调整逻辑(Value Adjustment)
-
操作按键:KEY5(对应
USER_BUTTON_4)。 -
功能要求:
-
当处于 模式 I 时:按下 KEY5,数值(Number)增加 1。
-
当处于 模式 D 时:按下 KEY5,数值(Number)减少 1。
-
-
约束条件:数值限制在 [0, 10] 区间内。即:数值达到 10 后不再增加,降低至 0 后不再减少。
-
显示要求:OLED 第二行实时显示当前数值,格式为
Number: [数值]。
系统性能要求
-
刷新机制:要求引入刷新标志位机制。仅在按键触发导致数据变化时才执行 OLED 写操作,避免在任务调度中频繁占用 I2C 总线。
-
交互响应:按键应使用单击事件触发,响应需灵敏且无误触。
项目实现
实现逻辑:
-
状态管理:在
oled_app.c中添加了全局变量,用于管理模式(增加/减少)、数值(0-10)以及一个刷新标志(RefreshFlag),确保仅在数据发生变化时才更新屏幕。 -
显示控制:修改了
oled_task任务,使其通过检查标志位来打印 "Mode: I/D" 和 "Number: X"。 -
按键控制:修改了
btn_app.c中的事件回调,以便在按下 KEY5 或 KEY6 时更新这些全局变量并触发刷新。
1. oled_app.h
添加了模式枚举并导出了全局变量,以便 btn_app.c 可以访问并修改它们。
/* oled_app.h */
#ifndef __OLED_APP_H__
#define __OLED_APP_H__
#include "mydefine.h"
#include "stdarg.h"
#include "stdio.h"
#include "oled.h"
// --- 定义工作模式枚举 ---
// 使用枚举 (enum) 可以让代码更易读,比直接用 0 和 1 更清楚
typedef enum {
MODE_INCREMENT = 0, // 代表 'I' 模式 (增加)
MODE_DECREMENT = 1 // 代表 'D' 模式 (减少)
} WorkMode_t;
// --- 导出全局变量 (Extern) ---
// 关键字 'extern' 告诉编译器:这些变量在别的地方(oled_app.c)定义了,
// 这里只是声明一下,为了让 btn_app.c 等其他文件也能访问它们。
extern WorkMode_t g_current_mode; // 当前模式 (I 或 D)
extern int8_t g_current_number; // 当前数值 (0-10)
extern uint8_t g_oled_refresh_flag; // 屏幕刷新标志位 (1=需要刷新, 0=不需要)
// --- 函数声明 ---
int Oled_Printf(uint8_t x, uint8_t y, const char *format, ...);
void oled_task(void);
#endif
2. oled_app.c
实现了变量定义和显示逻辑。为了防止 I2C 总线过度拥堵,oled_task 现在仅在 g_oled_refresh_flag 为 1 时才执行刷新操作。
#include "oled_app.h"
// --- 全局变量定义与初始化 ---
// 这些变量真正存储数据的地方
WorkMode_t g_current_mode = MODE_INCREMENT; // 默认为 I 模式
int8_t g_current_number = 0; // 默认数字为 0
uint8_t g_oled_refresh_flag = 1; // 初始化为 1,确保上电开机时能显示一次初始画面
/**
* @brief 类似于C语言printf的显示函数
* @param x 像素起始X坐标 (0-127)
* @param y 行起始Y坐标 (页地址 0-3)
* @param format, ... 格式化字符串 (例如 "Num: %d", num)
**/
int Oled_Printf(uint8_t x, uint8_t y, const char *format, ...)
{
char buffer[128]; // 定义一个字符数组作为缓冲区
va_list arg;
int len;
// 下面这几行是处理可变参数的标准写法,将 %d, %s 等替换为实际数值
va_start(arg, format);
len = vsnprintf(buffer, sizeof(buffer), format, arg);
va_end(arg);
// 调用底层的驱动函数显示处理好的字符串,字体高度为8
OLED_ShowStr(x, y, (char*)buffer, 8);
return len;
}
/* * Oled 显示任务
* 这个函数会被调度器(Scheduler)定期调用 (例如每1ms或5ms)
*/
void oled_task(void)
{
// --- 核心优化逻辑 ---
// 只有当 g_oled_refresh_flag 被置为 1 时,才执行刷屏操作。
// 如果没有这个判断,OLED会不停地刷新,占用大量 I2C 总线资源,导致系统卡顿。
if (g_oled_refresh_flag == 1)
{
// --- 第一行:显示模式 ---
if (g_current_mode == MODE_INCREMENT)
{
// 显示 Mode: I,后面加空格是为了覆盖掉可能存在的旧字符
Oled_Printf(0, 0, "Mode: I ");
}
else
{
Oled_Printf(0, 0, "Mode: D ");
}
// --- 第二行:显示数字 ---
// %d 显示整数变量 g_current_number
// 后面加两个空格 " " 是为了防止数字位数变少时(如10变成9),屏幕上还残留着'0'
Oled_Printf(0, 2, "Number: %d ", g_current_number);
// --- 清除标志位 ---
// 任务完成,将标志位置 0。
// 直到下一次按键按下将它置为 1 之前,不会再执行上面的显示代码。
g_oled_refresh_flag = 0;
}
}
3. btn_app.c
添加了 oled_app.h 的包含。在 prv_btn_event 中实现了 KEY5(数值修改)和 KEY6(模式切换)的具体逻辑。
#include "btn_app.h"
#include "ebtn.h"
#include "gpio.h"
#include <string.h>
#include "mydefine.h"
// 必须包含 oled_app.h,否则无法操作 g_current_mode 等变量
#include "oled_app.h"
extern uint8_t ucLed[6];
static uint8_t led_clipboard[6] = {0};
// 定义按键 ID
typedef enum
{
USER_BUTTON_0 = 0,
USER_BUTTON_1,
USER_BUTTON_2,
USER_BUTTON_3,
USER_BUTTON_4, // 对应板子上的 KEY5
USER_BUTTON_5, // 对应板子上的 KEY6
USER_BUTTON_MAX,
// ... (组合键定义略)
} user_button_t;
/* User defined settings ... (参数配置代码略) ... */
// ... (按键数组定义略) ...
// ... (GPIO 读取函数 prv_btn_get_state 略) ...
/**
* @brief 按键事件回调函数
* @param btn 触发事件的按键对象
* @param evt 触发的事件类型 (按下、松开、单击、双击等)
*/
void prv_btn_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
// 我们只关心“单击”事件 (EBTN_EVT_ONCLICK)
if (evt == EBTN_EVT_ONCLICK)
{
uint16_t click_cnt = ebtn_click_get_count(btn); // 获取点击次数(1=单击)
switch (btn->key_id)
{
// ... (KEY 0-3 的代码略) ...
// --------------------------------------------------------
// KEY5 (USER_BUTTON_4) 逻辑:修改数字
// --------------------------------------------------------
case USER_BUTTON_4:
if (click_cnt == 1) // 确认是单击
{
// 判断当前处于什么模式
if (g_current_mode == MODE_INCREMENT)
{
// [加模式]
// 只有当数字小于 10 时才允许自增,防止超过 10
if(g_current_number < 10) {
g_current_number++;
}
}
else
{
// [减模式]
// 只有当数字大于 0 时才允许自减,防止出现负数
if(g_current_number > 0) {
g_current_number--;
}
}
// --- 关键步骤:请求刷新 ---
// 修改完数据后,必须把刷新标志位置 1
// 这样 oled_task 才会知道数据变了,需要更新屏幕
g_oled_refresh_flag = 1;
// 串口打印调试信息,方便在电脑上观察状态
my_printf(&huart1, "KEY5 Pressed: Num -> %d\r\n", g_current_number);
}
break;
// --------------------------------------------------------
// KEY6 (USER_BUTTON_5) 逻辑:切换模式
// --------------------------------------------------------
case USER_BUTTON_5:
if (click_cnt == 1) // 确认是单击
{
// 在 I 和 D 模式之间来回切换
if (g_current_mode == MODE_INCREMENT) {
g_current_mode = MODE_DECREMENT; // 如果是 I,就切成 D
} else {
g_current_mode = MODE_INCREMENT; // 否则切回 I
}
// --- 关键步骤:请求刷新 ---
// 模式变了,屏幕文字需要改变,置位刷新标志
g_oled_refresh_flag = 1;
my_printf(&huart1, "KEY6 Pressed: Mode Toggle\r\n");
}
break;
// ... (组合键代码略) ...
default:
break;
}
}
}
void app_ebtn_init(void)
{
int init_ok = ebtn_init(btns, EBTN_ARRAY_SIZE(btns), btns_combo, EBTN_ARRAY_SIZE(btns_combo), prv_btn_get_state, prv_btn_event);
if (!init_ok)
{
return;
}
int btn0_idx = ebtn_get_btn_index_by_key_id(USER_BUTTON_0);
int btn1_idx = ebtn_get_btn_index_by_key_id(USER_BUTTON_1);
int btn2_idx = ebtn_get_btn_index_by_key_id(USER_BUTTON_2);
int btn3_idx = ebtn_get_btn_index_by_key_id(USER_BUTTON_3);
// Setup Combos
if (btn0_idx >= 0 && btn1_idx >= 0)
{
ebtn_combo_btn_add_btn_by_idx(&btns_combo[0], btn0_idx);
ebtn_combo_btn_add_btn_by_idx(&btns_combo[0], btn1_idx);
}
if (btn0_idx >= 0 && btn2_idx >= 0)
{
ebtn_combo_btn_add_btn_by_idx(&btns_combo[1], btn0_idx);
ebtn_combo_btn_add_btn_by_idx(&btns_combo[1], btn2_idx);
}
if (btn0_idx >= 0 && btn3_idx >= 0)
{
ebtn_combo_btn_add_btn_by_idx(&btns_combo[2], btn0_idx);
ebtn_combo_btn_add_btn_by_idx(&btns_combo[2], btn3_idx);
}
}
void btn_task(void)
{
ebtn_process(uwTick);
}
项目解读
这个功能的实现其实是一个典型的 “输入(按键) -> 处理(逻辑) -> 输出(显示)” 的模型。
为了让你更容易理解,我将这三个文件的逻辑拆解为三个部分来讲解:
1. 核心思想:数据的“共享” (oled_app.h / oled_app.c)
在嵌入式开发中,如果一个文件(按键)想要修改另一个文件(屏幕)显示的内容,通常需要通过全局变量来实现。
-
变量定义 (
oled_app.c): 我们在oled_app.c里定义了三个变量,它们就像是写在“公共黑板”上的数据,谁都可以看,谁都可以改。-
g_current_mode: 记录当前是 I (加模式) 还是 D (减模式)。 -
g_current_number: 记录当前的数字 (0 到 10)。 -
g_oled_refresh_flag: 这是一个非常关键的变量(刷新标志位)。它的作用是告诉屏幕:“嘿,数据变了,你该擦黑板重写了!”
-
-
变量声明 (
oled_app.h): 在.c文件里定义变量,别的.c文件是看不见的。所以我们在.h文件里加上extern关键字。-
extern的意思就是告诉编译器:“这个变量在别的地方定义过了,你让我用就行。” -
这样,
btn_app.c只要引用了oled_app.h,就能修改上面那三个变量了。
-
2. 输入端:按键控制逻辑 (btn_app.c)
btn_app.c 负责监听你的手指动作,并根据动作修改“公共黑板”上的数据。
我们主要修改了 prv_btn_event 函数,这个函数只有在按键发生事件(比如点击)时才会执行。
-
KEY6 (USER_BUTTON_5) - 切换模式:
-
逻辑: 当检测到单击时,它查看
g_current_mode。如果是I就改成D,如果是D就改成I。 -
关键动作: 修改完模式后,它把
g_oled_refresh_flag设为1。这意味着:“屏幕,我改模式了,你刷新一下!”
-
-
KEY5 (USER_BUTTON_4) - 修改数字:
-
逻辑: 当检测到单击时,它先看当前是什么模式。
-
如果是 I 模式: 检查数字是不是小于 10。如果小于 10,就
+1。 -
如果是 D 模式: 检查数字是不是大于 0。如果大于 0,就
-1。
-
-
边界保护: 代码里的
if(g_current_number < 10)和if(g_current_number > 0)就是为了防止数字变成 11 或者 -1。 -
关键动作: 同样,修改完数字后,把
g_oled_refresh_flag设为1。
-
3. 输出端:屏幕刷新逻辑 (oled_app.c)
oled_app.c 里的 oled_task 是由调度器(Scheduler)定期调用的(比如每 1 毫秒或者是空闲时一直调用,取决于你的调度器配置)。
-
为什么需要标志位? OLED 的通信(I2C协议)相对较慢。如果我们每次循环都发指令给屏幕说“显示Mode... 显示Number...”,单片机就会把大量时间浪费在刷屏上,导致按键反应变慢或者系统卡顿。
-
oled_task 的逻辑:
-
检查: 进门先看
if (g_oled_refresh_flag == 1)。 -
如果为 0 (没变化): 直接退出,什么都不做。这样极大地节省了 CPU 资源。
-
如果为 1 (有变化):
-
判断
g_current_mode,在第一行显示 "Mode: I" 或 "Mode: D"。 -
读取
g_current_number,在第二行显示 "Number: 5" (举例)。 -
最重要的一步:
g_oled_refresh_flag = 0;。把标志位清零。这表示:“我已经刷新过了,下次循环别再叫我了,除非按键再次把标志位置 1。”
-
-
微信视频2026-01-15_174017_182
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)