把按键和 LED 状态显示出来:第一次看见程序到底在干什么

经典三问

Q: 输入到反馈链路是什么?
A: 它是“按键电平被读取、变量被修改、LED 和 OLED 显示结果”的完整路径。

Q: 为什么需要观察完整链路?
A: 只看最终灯光无法判断故障发生在输入、业务逻辑还是输出显示,分段反馈能缩小排查范围。

Q: 输入到反馈链路常见使用场景?
A: 调试菜单、传感器状态、运动模式切换和任何需要现场确认程序状态的功能。


按下 A,屏幕上的 VALUE 从 0 变成 1,最右边那颗 LED 亮起。再按一次,数字到 2,亮的换成中间那颗。连按到 5,屏幕写着 101,两端的 LED 亮着。

这时按键有没有被读到,程序里的变量变没变,变量有没有送到 GPIO,显示还能不能跟上,它们全摆在桌面上。

这干的事虽小:A 键让数值加一,B 键让数值减一;0 到 7 循环;三颗外接 LED 把这个数写成三位二进制;OLED 把按键电平、事件次数和当前状态同时摊开,但是后面电机、编码器、循迹模块和 PID 都会沿用这条路,只是变量不再这么简单。

先把桌面接成同一套

这篇沿用前面 OLED 的软件 I2C 接线:PB8 接 SCL,PB9 接 SDA。新增的按键和 LED 固定为下面这一套,之后菜单、按键事件和非阻塞实验也继续使用它。

STM32 引脚 接到哪里 这根线在做什么
PB11 按键 A,按键另一端接 GND 读取“加一”操作
PB1 按键 B,按键另一端接 GND 读取“减一”操作
PC13 外接 LED 0 正极 显示二进制 bit 0
PC14 外接 LED 1 正极 显示二进制 bit 1
PC15 外接 LED 2 正极 显示二进制 bit 2
三颗 LED 负极 GND 给 LED 一个共同参考
PB8 / PB9 OLED SCL / SDA OLED 的软件 I2C

按键初始化为内部上拉输入。松开时,PB11 和 PB1 被芯片内部拉成高电平;按下后接到 GND,读到低电平。Key_A_IsPressed() 已经把这层反转包起来了,所以业务代码看见的 1 就是“正在按”,不需要每次再记住低电平才算按下。

三颗 LED 一开始按“高电平亮”连接。这个约定收在 led.c 的一行,其他代码不必到处跟着改:

#define LED_ACTIVE_LEVEL    1U

改成 0U 后,同一个二进制值仍会被送到 bit 0、bit 1、bit 2,只是 GPIO 的物理高低电平翻转,亮灭关系也会一起翻转。这个小开关很重要:应用层只关心“bit 1 要显示出来”,接线的有效电平由驱动层消化。

三颗灯怎么写一个数

一颗 LED 只能亮或灭,刚好装下一位二进制。三颗灯就有三位:

十进制值 OLED 上的 BITS PC15 PC14 PC13
0 000 灭 灭 灭
1 001 灭 灭 亮
2 010 灭 亮 灭
3 011 灭 亮 亮
4 100 亮 灭 灭
5 101 亮 灭 亮
6 110 亮 亮 灭
7 111 亮 亮 亮

表格从左向右写的是高位到低位,所以 PC13 在最右边,它装的是 bit 0。代码也按这个方向拆数:

void LED_ShowBinary(uint8_t value)
{
    LED_Write(GPIO_Pin_13, (uint8_t)((value >> 0) & 0x01U));
    LED_Write(GPIO_Pin_14, (uint8_t)((value >> 1) & 0x01U));
    LED_Write(GPIO_Pin_15, (uint8_t)((value >> 2) & 0x01U));
}

>> 1 的意思是把原数右移一格,让原来的 bit 1 走到最低位;& 0x01U 再把其余位抹掉。LED_Write() 收到的于是只会是 0 或 1。这里不需要直接去摸 GPIO 寄存器,标准库已经把“把某根引脚置高、置低”的细节装进 GPIO_SetBits() 和 GPIO_ResetBits()。

LED_Write() 本身只干一件事:按 LED_ACTIVE_LEVEL 决定这根引脚该置高还是置低。

void LED_Write(uint16_t pin, uint8_t level)
{
    if (level == LED_ACTIVE_LEVEL)
    {
        GPIO_SetBits(GPIOC, pin);
    }
    else
    {
        GPIO_ResetBits(GPIOC, pin);
    }
}

一次按下走过了哪些地方

先看整条链:

手指按下 A
    -> PB11 从高变低
    -> Key_A_DetectPressEdge() 给出一次事件
    -> value 加一,A 的事件次数加一
    -> LED_ShowBinary(value) 改三根 GPIO
    -> OLED 显示 value、BITS、按键原始状态和次数

它没有神秘的跳跃。按键驱动负责读输入;main.c 决定这一次按键是什么意思;LED 和 OLED 驱动把结果送到外设。以后把 value 换成电机目标速度、循迹状态或 PID 参数,骨架还是这副骨架。

OLED 上的 A RAW、B RAW 是当前是否正按着,按住显示 1,松开回到 0。A EVT、B EVT 是从上电开始各自收到了多少次按下沿。把这两组数字分开摆,按键有问题时就能先分清楚:是输入电平没变,还是一次动作被当成了多次事件。

完整应用代码

key.c 和 led.c 已经分别封装好了输入与输出,这里只需要把应用层写清楚:

#include "stm32f10x.h"
#include "led.h"
#include "key.h"
#include "OLED.h"

static void OLED_ShowBits(uint8_t value)
{
    OLED_ShowNum(36, 8, (uint32_t)((value >> 2) & 0x01U), 1, OLED_6X8);
    OLED_ShowNum(42, 8, (uint32_t)((value >> 1) & 0x01U), 1, OLED_6X8);
    OLED_ShowNum(48, 8, (uint32_t)(value & 0x01U), 1, OLED_6X8);
}

static void OLED_ShowPanel(uint8_t value, uint32_t key_a_count, uint32_t key_b_count)
{
    OLED_ShowString(0, 0, "VALUE:", OLED_6X8);
    OLED_ShowString(0, 8, "BITS:", OLED_6X8);
    OLED_ShowString(0, 16, "A RAW:", OLED_6X8);
    OLED_ShowString(0, 24, "B RAW:", OLED_6X8);
    OLED_ShowString(64, 16, "A EVT:", OLED_6X8);
    OLED_ShowString(64, 24, "B EVT:", OLED_6X8);

    OLED_ShowNum(42, 0, value, 1, OLED_6X8);
    OLED_ShowBits(value);
    OLED_ShowNum(42, 16, Key_A_IsPressed(), 1, OLED_6X8);
    OLED_ShowNum(42, 24, Key_B_IsPressed(), 1, OLED_6X8);
    OLED_ShowNum(106, 16, key_a_count, 3, OLED_6X8);
    OLED_ShowNum(106, 24, key_b_count, 3, OLED_6X8);
}

int main(void)
{
    uint8_t value = 0;
    uint8_t last_key_a_raw = 0xFFU;
    uint8_t last_key_b_raw = 0xFFU;
    uint32_t key_a_count = 0;
    uint32_t key_b_count = 0;

    LED_Init();
    Key_Init();
    OLED_Init();
    OLED_Clear();

    LED_ShowBinary(value);
    OLED_ShowPanel(value, key_a_count, key_b_count);
    OLED_Update();

    while (1)
    {
        if (Key_A_DetectPressEdge())
        {
            value = (uint8_t)((value + 1U) & 0x07U);
            key_a_count++;

            LED_ShowBinary(value);
            OLED_ShowNum(42, 0, value, 1, OLED_6X8);
            OLED_ShowBits(value);
            OLED_ShowNum(106, 16, key_a_count, 3, OLED_6X8);
            OLED_Update();
        }

        if (Key_B_DetectPressEdge())
        {
            value = (uint8_t)((value + 7U) & 0x07U);
            key_b_count++;

            LED_ShowBinary(value);
            OLED_ShowNum(42, 0, value, 1, OLED_6X8);
            OLED_ShowBits(value);
            OLED_ShowNum(106, 24, key_b_count, 3, OLED_6X8);
            OLED_Update();
        }

        if (Key_A_IsPressed() != last_key_a_raw)
        {
            last_key_a_raw = Key_A_IsPressed();
            OLED_ShowNum(42, 16, last_key_a_raw, 1, OLED_6X8);
            OLED_Update();
        }

        if (Key_B_IsPressed() != last_key_b_raw)
        {
            last_key_b_raw = Key_B_IsPressed();
            OLED_ShowNum(42, 24, last_key_b_raw, 1, OLED_6X8);
            OLED_Update();
        }
    }
}

& 0x07U 只保留最低三位。A 键在 7 上再加 1,得到 8,低三位又是 000,所以回到 0。B 键不能直接写 value--,因为 uint8_t 减到 0 以下会绕到 255;先加 7,再保留三位,0 + 7 正好变成 7,5 + 7 变成 12,低三位是 4。

last_key_a_raw 和 last_key_b_raw 只是 OLED 的局部刷新记忆。它们起初故意写成 0xFFU,这和真实的 0、1 都不同,程序第一次进入循环就会把两个原始状态画出来。之后电平没有变化,屏幕不会被同一个数字反复覆盖。

别把这段“只在变化时刷新”的写法误会成按键消抖。它只节省了 OLED 的重复工作。机械按键抖动时,A EVT 仍可能一次跳好几格;等系统有了稳定的时间基准,才有条件把“按下多久”“隔多久再确认”这类事认真处理。

oled上电显示的初始状态

先按这几个场景跑一遍

先连续按 A 五次。屏幕应到 VALUE: 5 和 BITS: 101,PC15、PC13 亮,PC14 灭。接着按 B 一次,状态退到 4,BITS 变 100。在 0 时按 B,观察它跳到 7;在 7 时按 A,观察它回到 0。

按住B11按钮+1
按住B1按钮-1
+7后-8直到状态到111

两个判断保留为两个独立的 if。如果同一轮扫描里 A、B 都产生了按下沿,两个动作都会执行。

故意让 OLED 停在昨天

把 A 键分支里的三行显示更新暂时注释掉,保留 LED_ShowBinary(value):

LED_ShowBinary(value);
/* OLED_ShowNum(42, 0, value, 1, OLED_6X8); */
/* OLED_ShowBits(value); */
/* OLED_ShowNum(106, 16, key_a_count, 3, OLED_6X8); */

烧录后按 A。三颗 LED 仍在按 0 到 7 变化,OLED 的 VALUE、BITS、A EVT 却停在第一次的数。此时 value 已经变了,GPIO 也已经跟着输出;漏掉的是把新状态画回屏幕的那几行。

把三行恢复,再烧录一次,面板重新跟上。这个故障很值得亲手做,因为后面会有更多“控制已经更新,显示还没更新”的场景。知道它们是不同工作,排查时就不会在 OLED 驱动里找一下午电机问题。(我之前就经常遇到有人测试陀螺仪模块的时候,要显示出角度,也就是浮点数,但是oled函数当时不支持浮点数,只支持整数,导致角度一直是显示0,以为是陀螺仪链路的问题,排查了半天......)

posted @ 2026-09-26 19:23  Zw-awa  阅读(50)  评论(0)    收藏  举报