前言

注意:本博客是基于前面所有博客的知识的小作业,不可单独食用。

实验任务

在提供的工程模板基础上,修改 oled_appbtn_app 相关文件,实现以下交互功能:

模式切换逻辑(Mode Control)

  • 操作按键:KEY6(对应 USER_BUTTON_5)。

  • 功能要求:每按下一次 KEY6,系统工作模式在 “模式 I (Increment)”“模式 D (Decrement)” 之间循环切换。

  • 显示要求:OLED 第一行实时显示当前模式,格式为 Mode: IMode: 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 里定义了三个变量,它们就像是写在“公共黑板”上的数据,谁都可以看,谁都可以改。

    1. g_current_mode: 记录当前是 I (加模式) 还是 D (减模式)

    2. g_current_number: 记录当前的数字 (0 到 10)。

    3. 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 的逻辑:

    1. 检查: 进门先看 if (g_oled_refresh_flag == 1)

    2. 如果为 0 (没变化): 直接退出,什么都不做。这样极大地节省了 CPU 资源。

    3. 如果为 1 (有变化):

      • 判断 g_current_mode,在第一行显示 "Mode: I" 或 "Mode: D"。

      • 读取 g_current_number,在第二行显示 "Number: 5" (举例)。

      • 最重要的一步: g_oled_refresh_flag = 0;。把标志位清零。这表示:“我已经刷新过了,下次循环别再叫我了,除非按键再次把标志位置 1。”

微信视频2026-01-15_174017_182

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐