1. 硬件准备与基础接线
第一次玩STM32配OLED的朋友可能会被那些密密麻麻的引脚吓到,其实接线比想象中简单多了。我用的是一块STM32F103C8T6核心板,搭配0.96寸的SSD1306 OLED屏,这种组合在淘宝上三十块钱就能搞定。具体接线时记住两个要点:I2C接口只需要四根线,按键最好用下拉电阻防干扰。
OLED屏的接线其实就四根线:
- SCL接PB8(I2C时钟线)
- SDA接PB9(I2C数据线)
- VCC接3.3V(千万别接5V会烧屏!)
- GND接地
按键部分我用的是三个轻触开关,配置成下拉输入模式:
- KEY1接PB12(菜单选择)
- KEY2接PB13(确认)
- KEY3接PB14(返回) 这里有个坑要注意:STM32的IO口内部下拉电阻较弱,建议外接10KΩ下拉电阻,否则容易误触发。
// 按键初始化代码示例 void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); // KEY1-PB12, KEY2-PB13, KEY3-PB14 GPIO_InitStruct.Pin = GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); }2. 菜单状态机设计精髓
多级菜单本质上就是个状态机,我见过有人用switch-case硬编码,后期维护简直噩梦。推荐用结构体+函数指针的方式,扩展性会好很多。先定义一个菜单项结构体:
typedef struct { const char *name; // 菜单显示文本 MenuItem *parent; // 父级菜单指针 MenuItem *children[5]; // 子菜单数组 uint8_t childCount; // 子菜单数量 void (*action)(void); // 确认键回调函数 } MenuItem;实际项目中我会用二维数组预定义所有菜单项,比如主菜单有四个子项:
MenuItem mainMenu = { "主菜单", NULL, {&settingMenu, &sensorMenu, &aboutMenu}, 3, NULL };按键处理的核心逻辑是状态转移。当按下确认键时,检查当前菜单项是否有子菜单,有则进入子菜单,没有就执行回调函数。返回键更简单,直接跳转到parent指针指向的菜单:
void handleOKKey() { if(currentMenu->childCount > 0) { currentMenu = currentMenu->children[selectedIndex]; selectedIndex = 0; // 重置光标位置 } else if(currentMenu->action) { currentMenu->action(); // 执行功能函数 } }3. OLED显示优化技巧
SSD1306这块屏刷新率不高,全屏刷新会有明显闪烁。经过实测,局部刷新能提升至少3倍流畅度。我的做法是只重绘变化部分,比如光标移动时:
void updateCursor() { // 清除旧光标(全黑填充矩形) OLED_Fill(0, oldPosY, 5, oldPosY+8, BLACK); // 绘制新光标(白色三角符号) OLED_DrawChar(0, newPosY, '>', WHITE); // 只刷新受影响的两行 OLED_RefreshArea(0, 0, 128, 16); }中文显示是个痛点,直接加载完整字库会占用几十KB Flash。我的解决方案是只提取需要的汉字,比如温湿度项目只需要"温度"、"湿度"等十几个字,用PCtoLCD2003生成字模数据,体积能控制在1KB以内。
4. 传感器数据融合实战
光敏和温湿度传感器(我用的是DHT11)的数据要实时显示在菜单里。这里有个架构设计技巧:把传感器读数放在独立任务中,通过全局变量共享数据:
// 在RTOS任务中周期性读取传感器 void sensorTask(void *arg) { while(1) { temperature = DHT11_ReadTemp(); humidity = DHT11_ReadHumidity(); light = GetLightSensorValue(); osDelay(1000); // 1秒更新一次 } }菜单显示时直接读取这些全局变量,但要注意数据竞争问题。简单场景下可以关闭中断再读取:
void showSensorData() { __disable_irq(); float temp = temperature; float humi = humidity; __enable_irq(); OLED_ShowString(0,0,"温度:",16); OLED_ShowFloat(40,0,temp,2,16); //...其他显示代码 }5. 蓝牙控制进阶玩法
加上HC-05蓝牙模块后,可以通过手机APP控制设备。串口中断接收处理是关键:
void USART_IRQHandler(void) { if(USART1->SR & USART_SR_RXNE) { char cmd = USART1->DR; if(cmd == '1') menuHandler(KEY_OK); else if(cmd == '0') menuHandler(KEY_BACK); //...其他命令解析 } }在安卓端用MIT App Inventor做个简易控制器,发送'1'模拟确认键,'0'模拟返回键。更复杂的协议可以定义JSON格式,比如控制舵机角度:{"servo":90}
6. 避坑指南
调试时踩过几个深坑值得分享:
- I2C通信失败:SCL/SDA线过长会导致波形畸变,超过20cm建议加1KΩ上拉电阻
- 按键抖动:软件消抖至少延时20ms,我用的是状态机方式检测稳定电平
- 内存不足:STM32F103只有20KB RAM,避免动态内存分配,所有数组静态定义
- OLED残影:长时间显示静态内容要定期刷新,否则会烧屏
项目完整代码我放在了Gitee上,包含以上所有功能的实现。移植时只需修改硬件抽象层(HAL)的驱动部分,菜单逻辑完全可复用。