资讯动态

STM32入门实战:按键、LED与OLED状态显示系统

发布时间:2026/10/3 12:34:16 来源:尧图企业网站定制
1. 从“点灯”到“看见状态”为什么这是嵌入式入门最关键的一步很多人第一次接触 STM32都是从“点亮一颗 LED”开始的。但真正让新手产生“我终于看懂了程序在干什么”这种顿悟感的不是灯亮了而是按键按下去LED 跟着变OLED 上还能把当前状态显示出来。这个闭环一旦跑通你对 GPIO、输入输出模式、上下拉电阻、HAL 库调用流程、I2C 通信这些概念的理解就不再是书本上干巴巴的定义而是能亲眼看到、亲手摸到的真实反馈。我见过太多人卡在“灯亮了然后呢”这个阶段。灯亮只能证明你成功配置了一个输出引脚但程序到底有没有在跑、跑到哪一步、按键有没有被识别、状态有没有变化这些信息你一概不知道。把按键和 LED 状态显示出来本质上是在给你的程序装上一双“眼睛”和一双“耳朵”——按键是耳朵负责接收外部输入LED 和 OLED 是眼睛负责把内部状态外化出来。这套东西搭好之后你后面学定时器、中断、串口、ADC都会有一个可靠的调试手段。这篇文章面向的是刚上手 STM32 但还处于“能抄代码但不太明白为什么”阶段的朋友也适合那些想重新梳理 GPIO 输入输出逻辑的进阶学习者。我会从整体设计思路讲起把按键消抖、GPIO 模式选择、OLED 驱动、状态机组织这些核心环节拆开揉碎再给出可以直接复现的实操流程和踩坑记录。你不需要有很深的 C 语言功底但至少要能看懂基本的 if-else 和函数调用。提示本文以 STM32F103C8T6 和 HAL 库为基准平台OLED 选用常见的 0.96 寸 I2C 接口模块。不同型号的引脚编号和时钟配置会有差异但核心逻辑完全通用。2. 整体设计思路输入、处理、输出三件事怎么串起来2.1 为什么选“按键 LED OLED”这个组合嵌入式系统再复杂拆到最底层都是“输入—处理—输出”这三个环节。按键是最简单、最可靠的数字输入设备你按下去它导通松开它断开没有模拟量、没有协议、没有时序要求拿来理解 GPIO 输入模式再合适不过。LED 是最简单的数字输出设备给高电平就亮给低电平就灭用来验证输出配置立竿见影。OLED 则稍微复杂一点它通过 I2C 总线通信能显示文字和图形用来把“程序内部的状态”可视化出来。这三者组合在一起覆盖了嵌入式开发中最基础的几个技能点GPIO 输入配置、GPIO 输出配置、I2C 通信、状态管理、人机交互逻辑。而且它们的成本极低一个最小系统板加上按键、LED、OLED总共花不了几十块钱但能让你把 HAL 库中最常用的几个函数都练一遍。有人可能会问为什么不用串口打印来代替 OLED串口当然也可以但你得连着电脑、开着串口助手才能看到信息调试的时候还行一旦脱离电脑就瞎了。OLED 是本地显示板子通上电就能看到状态更适合做一个小型独立设备。而且 OLED 的 I2C 驱动本身就是一个很好的学习案例理解了它你后面接其他 I2C 传感器比如温湿度、光照强度会轻松很多。2.2 状态机的引入让程序逻辑不再是一团乱麻新手写按键控制 LED最容易写成这样在主循环里不断判断按键引脚电平如果按下就翻转 LED。这种写法在只有一个按键、一个 LED 的时候勉强能用但一旦按键数量增加、功能变复杂代码就会变成一堆嵌套的 if-else改一处就牵一发而动全身。更合理的做法是引入一个简单的状态机。你可以把系统想象成有几个明确的“状态”比如“待机”“运行”“设置”“报警”等等。按键的作用是触发状态之间的切换LED 和 OLED 则负责把当前状态展示出来。这样一来你的主循环就变成了三件事读取按键事件、根据事件更新状态、根据状态刷新显示。逻辑清晰扩展也方便。具体到我们这个项目状态可以设计得很简单一个 LED 的开关状态亮或灭一个按键计数器的值按了多少次以及一个当前模式比如正常模式和闪烁模式。按键短按切换 LED 开关长按切换模式OLED 上实时显示 LED 状态、按键次数和当前模式。这样既练了基本操作又有一定的实用价值。2.3 模块划分与文件组织写代码之前先想清楚文件怎么分。我习惯把跟硬件直接打交道的部分单独放在一个文件里比如bsp_key.c、bsp_led.c、bsp_oled.c每个文件只负责自己那一块硬件的初始化和基本操作。主函数main.c只负责调用这些模块组织业务逻辑。这样做的好处是以后换一个引脚或者换一个 OLED 型号只需要改对应的 bsp 文件主逻辑不用动。HAL 库本身已经提供了HAL_GPIO_Init、HAL_GPIO_ReadPin、HAL_GPIO_WritePin这些函数我们不需要从寄存器层面重新造轮子但需要在它们之上封装一层更符合业务语义的接口。比如Key_Scan()返回按键事件类型LED_On()、LED_Off()、LED_Toggle()控制灯OLED_ShowStatus()刷新显示。这样主循环读起来就像在描述需求而不是在操作寄存器。3. 核心细节解析GPIO 模式、按键消抖与 OLED 驱动3.1 GPIO 的 8 种工作模式到底怎么选STM32 的 GPIO 有 8 种工作模式新手最容易在这里犯迷糊。我按实际使用场景给你捋一遍模式典型用途关键特点浮空输入外部已有上下拉的信号引脚悬空时电平不确定上拉输入按键一端接地内部上拉按下为低下拉输入按键一端接电源内部下拉按下为高模拟输入ADC 采集关闭数字功能开漏输出I2C 总线只能拉低需外部上拉推挽输出LED、继电器能拉高也能拉低复用开漏I2C 外设引脚由外设控制复用推挽SPI、UART 外设引脚由外设控制按键这边我一般用上拉输入。原因很简单按键一端接引脚另一端接 GND平时引脚被内部上拉电阻拉到高电平按下时被拉到低电平。这样不需要在电路板上额外焊一个上拉电阻省事。如果你用的是下拉输入按键就得接 VCC按下时读到高电平逻辑上稍微别扭一点。LED 这边用推挽输出。推挽的意思是引脚能主动输出高电平和低电平驱动能力强点亮 LED 没问题。开漏输出只能主动拉低要输出高电平得靠外部上拉电阻点灯也能用但没必要。OLED 的 I2C 引脚要注意SCL 和 SDA 必须配置成复用开漏模式并且使能 I2C 外设。如果你手动把它配成普通推挽输出然后用软件模拟 I2C 时序那也可以但 HAL 库的硬件 I2C 就用不了了。我建议新手先用硬件 I2C配置简单代码量少。注意配置 GPIO 之前一定要先使能对应的时钟。STM32 的外设时钟默认是关闭的你不开时钟写再多配置寄存器都没用。GPIOA、GPIOB、GPIOC 挂在 APB2 总线上用__HAL_RCC_GPIOA_CLK_ENABLE()这类宏来开启。3.2 按键消抖为什么你按一下程序却认为你按了十下机械按键在按下和松开的瞬间触点会产生抖动电平会在高和低之间快速跳变几次持续时间通常在 5 到 20 毫秒之间。如果你的程序在主循环里直接读引脚电平就会把一次按下识别成多次按下。这就是为什么很多人写的按键计数程序按一下数字跳好几下。解决方法有两种硬件消抖和软件消抖。硬件消抖是在按键两端并联一个 0.1uF 左右的电容利用电容的充放电特性把抖动滤掉。软件消抖则是在检测到电平变化后延时 10 到 20 毫秒再读一次如果电平仍然保持变化后的状态才确认按键真的被按下了。我一般用软件消抖因为不用改电路。但这里的延时不能用HAL_Delay()因为它是阻塞式的会让整个程序停在那里等。更好的做法是用一个定时器产生 1 毫秒的中断在中断里做按键扫描和计数主循环只管读取按键事件。不过对于刚开始学的朋友先用HAL_Delay(15)在按键扫描函数里做简单消抖也完全可以等理解了再升级到定时器方案。除了消抖还要处理长按和短按的区别。我的做法是检测到按下后开始计时如果 500 毫秒内松开算短按如果超过 500 毫秒还没松开算长按并且长按只触发一次直到松开后才允许下一次触发。这样短按用来切换 LED长按用来切换模式互不干扰。3.3 OLED 显示I2C 地址、初始化序列和汉字显示0.96 寸 OLED 模块通常使用 SSD1306 驱动芯片I2C 地址一般是 0x788 位地址或 0x3C7 位地址。这里有个坑不同厂家的模块地址可能不一样有的背面电阻跳线决定了地址是 0x78 还是 0x7A。如果你发现 OLED 不亮先用 I2C 扫描程序确认一下地址。初始化序列是一长串命令包括设置对比度、显示模式、扫描方向、时钟分频等等。这些命令你不需要逐条理解直接用厂家提供的初始化代码或者开源库里的初始化函数就行。但你要知道初始化没做对OLED 就是一片黑什么都不会显示。显示汉字需要取模。OLED 本身不带中文字库每个汉字你要用取模软件生成点阵数据然后在代码里通过数组引用。常用的取模软件有 PCtoLCD2002设置成“阴码、逐列式、顺向”这些参数生成的数组直接放到代码里就能用。如果你不想折腾汉字先用英文和数字也足够展示状态了。我建议新手先用现成的 OLED 驱动库比如网上流传很广的oled.c和oled.h把初始化和基本显示函数调通再慢慢研究里面的实现。不要一上来就自己从头写 I2C 时序和 SSD1306 命令那样容易卡住。4. 实操过程从零搭建按键、LED 与 OLED 状态显示系统4.1 硬件连接与引脚分配先确定引脚。以 STM32F103C8T6 最小系统板为例我一般这样分配LED接在 PA5串联一个 1K 电阻到 GND。PA5 输出高电平时 LED 亮。按键接在 PB0另一端接 GND。PB0 配置为上拉输入按下时读到低电平。OLEDSCL 接 PB6SDA 接 PB7。这两个引脚是 I2C1 的默认复用引脚。接线的时候注意几点LED 的限流电阻不能省否则电流过大可能烧掉 LED 或者损坏引脚。按键如果用的是四脚轻触开关注意哪两个脚是相通的别接错了。OLED 的 VCC 一般接 3.3V有些模块标称支持 5V但为了安全统一用 3.3V。提示如果你用的是 STM32F103C8T6 的蓝色药丸板板载 LED 接在 PC13低电平点亮。你可以直接用板载 LED省去外接的麻烦。但 PC13 的驱动能力较弱建议还是外接一个 LED 到 PA5 更方便观察。4.2 工程配置时钟、GPIO 和 I2C 初始化用 STM32CubeMX 生成初始化代码是最快的方式。打开 CubeMX选好芯片型号然后按下面的步骤配置系统时钟在 RCC 里把 HSE 设为 Crystal/Ceramic Resonator然后在 Clock Configuration 里把主频拉到 72MHz。GPIO 输出找到 PA5设为 GPIO_Output输出模式选 Push Pull不上拉也不下拉速度选 Low。GPIO 输入找到 PB0设为 GPIO_Input上拉模式选 Pull-up。I2C1把 PB6 和 PB7 设为 I2C1_SCL 和 I2C1_SDAI2C 速度模式选 Standard Mode 或 Fast Mode 都可以OLED 一般支持 400kHz。生成代码工具链选 MDK-ARM 或 STM32CubeIDE生成工程。生成出来的main.c里MX_GPIO_Init()和MX_I2C1_Init()已经帮你把底层配置做好了。你只需要在main()的 while 循环里写业务逻辑。如果你不用 CubeMX手动写初始化代码也行但要注意时钟使能、引脚模式、上下拉配置这些细节一个都不能少。我建议新手先用 CubeMX 把工程跑通再回头去看生成的代码理解每一行在做什么。4.3 按键扫描函数的实现与参数计算按键扫描我封装成一个函数返回一个枚举类型表示按键事件typedef enum { KEY_NONE 0, KEY_SHORT_PRESS, KEY_LONG_PRESS } KeyEvent; KeyEvent Key_Scan(void) { static uint8_t key_pressed 0; static uint32_t press_time 0; KeyEvent event KEY_NONE; if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET) { HAL_Delay(15); // 消抖 if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET) { if (!key_pressed) { key_pressed 1; press_time HAL_GetTick(); } } } else { if (key_pressed) { uint32_t duration HAL_GetTick() - press_time; if (duration 500) { event KEY_SHORT_PRESS; } else { event KEY_LONG_PRESS; } key_pressed 0; } } return event; }这里的 15 毫秒是消抖延时500 毫秒是长按阈值。这两个参数可以根据实际手感调整。消抖时间太短可能滤不干净太长会让按键反应变迟钝。长按阈值太短容易误触发太长按起来费劲。我实测下来15 毫秒和 500 毫秒是比较舒服的组合。注意HAL_GetTick()返回的是系统启动以来的毫秒数用它来计时比用HAL_Delay()累加要准确得多。HAL_Delay()本身会占用 CPU而HAL_GetTick()只是读一个变量几乎不耗时。4.4 LED 控制与状态变量设计LED 控制很简单三个函数搞定void LED_On(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); } void LED_Off(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); } void LED_Toggle(void) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); }状态变量我用一个结构体来组织typedef struct { uint8_t led_on; uint8_t mode; uint32_t key_count; } SystemState; SystemState sys_state {0, 0, 0};led_on记录 LED 当前是亮还是灭mode记录当前模式0 表示正常1 表示闪烁key_count记录按键被按了多少次。每次按键事件发生后更新这些变量然后调用显示刷新函数。4.5 OLED 显示刷新与状态可视化OLED 刷新我放在主循环里但不会每轮都刷而是用一个时间戳控制刷新频率比如每 100 毫秒刷一次。刷太快没必要人眼也看不出来还占用 I2C 带宽。void OLED_ShowStatus(SystemState *state) { OLED_Clear(); OLED_ShowString(0, 0, LED: ); OLED_ShowString(40, 0, state-led_on ? ON : OFF); OLED_ShowString(0, 2, Mode: ); OLED_ShowString(40, 2, state-mode ? BLINK : NORMAL); OLED_ShowString(0, 4, Count: ); OLED_ShowNumber(48, 4, state-key_count, 5); OLED_Refresh(); }OLED_ShowString、OLED_ShowNumber、OLED_Refresh这些函数来自你用的 OLED 驱动库。不同库的函数名可能不一样但思路是一样的先把内容写到显存缓冲区最后统一刷新到屏幕。不要每写一个字符就刷新一次那样屏幕会闪。主循环的整体结构就是while (1) { KeyEvent event Key_Scan(); if (event KEY_SHORT_PRESS) { sys_state.led_on !sys_state.led_on; sys_state.key_count; if (sys_state.led_on) LED_On(); else LED_Off(); } else if (event KEY_LONG_PRESS) { sys_state.mode !sys_state.mode; sys_state.key_count; } if (sys_state.mode 1 sys_state.led_on) { LED_Toggle(); HAL_Delay(200); } if (HAL_GetTick() - last_refresh 100) { OLED_ShowStatus(sys_state); last_refresh HAL_GetTick(); } }这段代码里短按翻转 LED 并计数长按切换模式并计数。闪烁模式下LED 每 200 毫秒翻转一次。OLED 每 100 毫秒刷新一次。逻辑不复杂但已经把输入、处理、输出三个环节都串起来了。5. 常见问题与排查技巧实录5.1 按键按下去没反应怎么一步步排查按键没反应是最常见的问题排查顺序可以按下面来先确认引脚电平用万用表或者调试器看按键按下时引脚电平有没有变化。如果没变化说明硬件连接有问题检查按键是否接对、上拉电阻是否生效。再确认 GPIO 配置在调试模式下看 GPIOB 的配置寄存器确认 PB0 确实被配成了上拉输入。如果配成了浮空输入引脚悬空时电平不确定按键按下也读不到稳定低电平。然后确认扫描逻辑在Key_Scan()里加一个断点或者翻转一个调试引脚看函数有没有被调用、有没有进入按下分支。最后确认消抖参数如果按下后偶尔有反应偶尔没有可能是消抖时间太短试着把 15 毫秒改成 20 毫秒。我遇到过一种情况按键接在 PB0但 PB0 同时被复用成了其他功能导致输入模式不生效。解决办法是在 CubeMX 里确认 PB0 没有被分配给其他外设或者手动在代码里重新配置一次。5.2 OLED 不亮的几种原因和解决办法OLED 不亮先别急着怀疑代码按这个顺序查现象可能原因解决办法完全黑屏电源没接好检查 VCC 和 GND确认 3.3V 正常完全黑屏I2C 地址不对用扫描程序确认地址改初始化代码完全黑屏初始化序列没执行检查初始化函数是否被调用有微弱亮光但无内容对比度设置太低调高对比度命令参数显示乱码取模方式不对检查取模软件的设置显示闪烁刷新太频繁降低刷新频率或改用缓冲区刷新I2C 地址这个问题特别常见。有的模块标称地址 0x78实际是 0x7A有的标称 0x3C实际是 0x3D。写一个简单的 I2C 扫描程序把总线上所有应答的地址打印出来一目了然。5.3 程序跑着跑着就卡死可能是什么原因卡死通常有几个来源一是HAL_Delay()在中断里被调用导致优先级冲突二是 I2C 通信没有超时机制从设备不响应时主设备一直等三是数组越界或者指针错误把内存写乱了。我的习惯是给所有可能阻塞的操作加超时。比如 I2C 传输用HAL_I2C_Master_Transmit()时最后一个参数是超时时间不要填HAL_MAX_DELAY填个 100 毫秒就行。这样即使 OLED 没接好程序也不会卡死而是返回错误码你可以根据错误码做处理。另外HAL_Delay()不要在中断服务函数里用。中断里需要延时的话用计数器或者标志位在主循环里处理。5.4 按键计数不准按一下跳好几个数这就是典型的消抖没做好。除了前面说的 15 毫秒延时还要注意一点消抖延时之后要再读一次引脚确认电平确实变了。只延时不再读等于没消抖。还有一种情况是按键松开时也有抖动导致一次按下被识别成“按下—松开—按下”。解决办法是在松开检测里也加消抖或者用状态机的方式只有从“未按下”到“按下”的跳变才计数从“按下”到“未按下”的跳变不计数。我自己的做法是用一个key_pressed标志位按下时置 1松开时清 0只有在标志位从 0 变 1 的时候才触发事件。这样无论抖动多少次只要标志位已经是 1就不会重复触发。6. 从这个小项目还能延伸出什么这套按键、LED、OLED 的框架搭好之后你可以很方便地往上加东西。比如加一个 DHT11 温湿度传感器把温湿度显示在 OLED 上按键用来切换显示页面。或者加一个超声波模块按键控制测距的启动和停止。再或者把 LED 换成继电器按键控制继电器的开关OLED 显示开关状态和累计操作次数。我个人的体会是嵌入式学习最怕的就是“学一个知识点丢一个知识点”。你今天学了 GPIO明天学了 I2C但如果它们之间没有联系过几天就忘了。而按键、LED、OLED 这个小项目把 GPIO 输入、GPIO 输出、I2C 通信、状态管理、人机交互都串在了一起是一个很好的“知识锚点”。以后你学定时器中断可以把按键扫描搬到中断里学低功耗模式可以让 OLED 在无操作时自动熄屏学 RTOS可以把按键扫描、显示刷新拆成独立任务。这个框架能陪你走很远。最后分享一个小技巧在 OLED 上留一行显示HAL_GetTick()的值这样你能直观看到程序运行了多久、有没有卡死。这个简单的做法在调试定时逻辑的时候特别有用比串口打印还方便。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑