Code
示例代码:四个平台
四份代码功能一致:屏上显示 DHT11 温湿度,四个按键各管一件事,每按一下都有立刻能看见的反馈。
四个按键分别干什么
按键的功能是照着「按下去能不能立刻看出来」设计的——客户拿到手不用接串口、不用会写代码,四个键挨个按一遍就知道每个键是不是好的。这一点对售后很重要:客户说「按键没反应」时,可以直接让他按这四个键描述屏上的变化,一句话就能分清是按键坏了、线接错了、还是他根本没按对位置。
| 按键 | 引脚 | 功能 | 按下去屏上会怎样 |
| 左上 | PB11 | 翻页 | 温湿度页 → 按键计数页 → 最高/最低记录页 → 循环回来 |
| 右上 | PB10 | 反色显示 | 整屏黑白瞬间对调,最一眼能看出来的一个 |
| 左下 | PB1 | 亮度三档 | 暗 → 中 → 亮 循环(改 SSD1306 的对比度寄存器 0x81) |
| 右下 | PB0 | 清零 | 按键计数归零,温湿度的最高/最低记录重新开始记 |
另外四个键的按下次数一直在后台累加,翻到「按键计数页」能看到 LT / RT / LB / RB 四个数字(对应左上/右上/左下/右下)。屏幕右上角常驻两个状态标记:B1~B3 是当前亮度档,I 表示正处于反色状态。
客户自测四个按键的
您好,四个按键都有独立功能,您可以自己验一遍:左上按一下会翻页,右上按一下整个屏幕黑白反过来,左下按一下屏幕亮度变一档,右下按一下把计数清零。翻到第二页(按左上键翻)能看到四个键各自被按了多少次,LT 是左上、RT 是右上、LB 是左下、RB 是右下。哪个键按了屏上没反应、计数也不加,那就是这个键的线没接好,重点查它接 GND 的那一脚。
要改功能只要动 loop() 里那个 switch,四个 case 分别对应左上/右上/左下/右下;要换引脚只改 KEY_PINS 那一行,顺序是固定的「左上、右上、左下、右下」。
驱动认错会「屏全黑」,而且骗过所有检查 —— 本页这块屏实测是 SSD1306
2026-08-09 在实物上踩过一次,过程值得记下来:I2C 扫描正常扫到 0x3C、endTransmission() 返回 0(正常应答)、display.begin() 返回 true(成功)——所有能查的都是绿的,但屏一个亮点都没有。
原因是充电泵。SSD1306 面板的驱动高压靠控制器内部 charge pump 升出来,必须发 0x8D 0x14 这条命令才会打开;而 Adafruit SH110X 这个库从头到尾不发它(SH1106 用的是另一套 0xAD 0x8B)。结果就是:总线一切正常、数据也确实写进显存了,但面板没有电压,全黑。所有软件层面的自检都会告诉你「一切正常」。
两个方向的症状正好不一样,记住这个就能反推:
- SSD1306 的屏用 SH1106 的库 → 整屏全黑,但 I2C 完全正常(本页这次踩的就是这个)
- SH1106 的屏用 SSD1306 的库 → 屏是亮的,画面左右偏 2 个像素、边缘一条竖线或雪花(SH1106 显存 132 列,只显示中间 128 列)
两种情况屏都没坏。下面四份代码全部按 SSD1306 写好,对应本页这块实物。
客户的屏不是 SSD1306 怎么办:只要改 4 行
本页四份代码全部是按 SSD1306 驱动写的。客户手里如果是 SH1106(1.3 寸屏多为 SH1106,0.96 寸两种都有),不用重写程序,接线一根都不用动——I2C 就是 SCL/SDA 两根线,跟驱动型号无关——只要按下表把库和这几行换掉就行。
| 改哪里 | SSD1306(本页代码,本批屏用这个) | 换成 SH1106 要改成 |
| 装哪个库 | Adafruit SSD1306 | Adafruit SH110X |
| 头文件 | #include <Adafruit_SSD1306.h> | #include <Adafruit_SH110X.h> |
| 建对象 | Adafruit_SSD1306 display(128, 64, &Wire, -1); | Adafruit_SH1106G display(128, 64, &Wire, -1); |
| 初始化 | display.begin(SSD1306_SWITCHCAPVCC, 0x3C); | display.begin(0x3C, true); |
| 颜色常量 | SSD1306_WHITE | SH110X_WHITE |
| 树莓派(luma.oled) | from luma.oled.device import ssd1306
device = ssd1306(...) | from luma.oled.device import sh1106
device = sh1106(...) |
| 用 U8g2 库的话 | U8G2_SSD1306_128X64_NONAME_F_HW_I2C | U8G2_SH1106_128X64_NONAME_F_HW_I2C |
SSD1306_SWITCHCAPVCC 这个参数不是可有可无的写法习惯,它就是「叫控制器打开内部充电泵」的意思。改代码时最容易漏掉的就是它。
不知道客户的屏是哪个驱动?让他跑一遍「双驱动轮换」
商品页和丝印上经常不写驱动型号,靠猜很浪费时间。最快的办法是写一段两种驱动轮流试的程序:先用 SSD1306 的库初始化并在屏上写「SSD1306」,等 5 秒;再用 SH1106 的库初始化并写「SH1106」,等 5 秒;无限循环。客户看到屏上出现哪个字,就是哪个驱动,一次就问清了,不用来回猜。
本页这块屏就是这么定下来的——人一开始说是 SH1106,按 SH1106 写的代码屏全黑,跑完这个轮换程序,屏上清清楚楚出的是 SSD1306。
如果两种都不出字,那就不是驱动问题了,去查供电:万用表量 OLED 的 VDD 对 GND 有没有 3.3V,特别注意 MB-102 面包板的电源轨中间是断开的,左右两半不通。
验证状态:STM32 这份 ✓ 实物跑通了
① STM32F103 版本:2026-08-09 在实物上完整跑通。PlatformIO 编译无报错,ST-Link 烧录 OpenOCD 回报 Verified OK。实测结果:I2C 扫描只有 0x3C 一个设备、应答码 0;DHT11 读到 28°C / 49%RH;四个按键上拉电平全部正常;屏正常显示(驱动是 SSD1306,用 SH1106 的库会全黑,这个坑的完整经过见上面的红色警示块)。库版本:Adafruit SSD1306 2.5.x、GFX 1.12.6、DHT sensor library 1.4.7。
② UNO / ③ ESP32 / ④ 树莓派:按同一套逻辑改写,尚未在这三个平台上机跑过,标记为「供参考」。
下载 STM32 PlatformIO 工程 ZIP(实测版)
解压后用 VS Code 打开整个文件夹(不要只复制 main.cpp),装 PlatformIO 插件,接好 ST-Link 点 Upload 即可。库会自动下载。
① STM32F103C8T6(VS Code + PlatformIO + STM32duino)
[env:bluepill_f103c8]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
lib_deps =
adafruit/Adafruit SSD1306@^2.5.13
adafruit/Adafruit GFX Library@^1.11.11
adafruit/DHT sensor library@^1.4.6
adafruit/Adafruit Unified Sensor@^1.1.14
// O31 STM32F103C8T6 + 0.96 OLED(SSD1306) + DHT11 + 4 按键
// 接线:
// OLED VDD->3.3V GND->GND SCK/SCL->PB8 SDA->PB9
// 注意 PB8 只能当 SCL、PB9 只能当 SDA,芯片定死不能对调
// DHT11 VCC->3.3V GND->GND S->PA1 (模块自带上拉)
// 按键 左上->PB11 右上->PB10 左下->PB1 右下->PB0
// 另一脚全部接 GND(内部上拉,按下为低)
// 下载 ST-Link V2: 3.3V / GND / SWDIO(PA13) / SWCLK(PA14)
//
// 四个按键的功能(每个都有立刻能看见的反馈,客户可以自测):
// 左上 PB11 = 翻页 温湿度页 -> 按键计数页 -> 最值记录页 -> 循环
// 右上 PB10 = 反色 整屏黑白对调
// 左下 PB1 = 亮度 暗 -> 中 -> 亮 循环
// 右下 PB0 = 清零 按键计数清零 + 温湿度最值重新记
//
// 屏驱动:SSD1306(2026-08-09 实测确认。用 SH1106 的库会全黑,因为不发充电泵命令 0x8D 0x14)
// 环境:VS Code + PlatformIO,framework = arduino (STM32duino)
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#define OLED_SCL_PIN PB8 // I2C1_SCL 的重映射脚(默认在 PB6)
#define OLED_SDA_PIN PB9 // I2C1_SDA 的重映射脚(默认在 PB7)
#define OLED_ADDR 0x3C // ✓ 实测:I2C 扫描只有这一个设备
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define DHT_PIN PA1
#define DHT_TYPE DHT11
#define DHT_INTERVAL 2000UL // DHT11 采样慢,最少 2 秒读一次
#define KEY_COUNT 4
#define DEBOUNCE_MS 20UL // 机械开关抖动 5~20ms
#define PAGE_COUNT 3
// 顺序 = 面包板上的位置:KEY1 左上、KEY2 右上、KEY3 左下、KEY4 右下
const uint32_t KEY_PINS[KEY_COUNT] = {PB11, PB10, PB1, PB0};
const char *KEY_NAME[KEY_COUNT] = {"LT", "RT", "LB", "RB"};
// 亮度三档:SSD1306 的对比度寄存器 0x81,取值 0x00~0xFF
const uint8_t BRIGHTNESS[3] = {0x10, 0x7F, 0xCF};
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(DHT_PIN, DHT_TYPE);
bool keyStable[KEY_COUNT] = {true, true, true, true}; // true = 松开(上拉)
bool keyLastRaw[KEY_COUNT] = {true, true, true, true};
uint32_t keyLastChange[KEY_COUNT] = {0, 0, 0, 0};
uint16_t keyCount[KEY_COUNT] = {0, 0, 0, 0};
int page = 0; // 0=温湿度 1=按键计数 2=最值
bool inverted = false; // 反色
uint8_t brightIdx = 2; // 亮度档位,默认最亮
float temperature = NAN, humidity = NAN;
float tMin = NAN, tMax = NAN, hMin = NAN, hMax = NAN;
uint32_t lastDht = 0;
// 返回值:该按键这一轮是否发生「按下」事件(下降沿)
bool scanKey(int i) {
bool raw = (digitalRead(KEY_PINS[i]) == HIGH); // 上拉输入:HIGH=松开,LOW=按下
uint32_t now = millis();
if (raw != keyLastRaw[i]) { // 电平变了,重新计时
keyLastRaw[i] = raw;
keyLastChange[i] = now;
return false;
}
if (now - keyLastChange[i] < DEBOUNCE_MS) return false; // 还在抖动窗口内
if (raw != keyStable[i]) { // 稳定了且和上次确认的状态不同
keyStable[i] = raw;
if (raw == false) return true; // false = 按下
}
return false;
}
void applyBrightness() {
// SSD1306 设置对比度:命令 0x81 + 一个字节数值
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(BRIGHTNESS[brightIdx]);
}
void resetAll() {
for (int i = 0; i < KEY_COUNT; i++) keyCount[i] = 0;
tMin = tMax = hMin = hMax = NAN;
}
void drawHeader(const char *title) {
display.setTextSize(1);
display.setCursor(0, 0);
display.print(title);
// 右上角状态:亮度档 + 是否反色
display.setCursor(92, 0);
display.print(F("B"));
display.print(brightIdx + 1);
display.print(inverted ? F(" I") : F(" "));
display.drawFastHLine(0, 10, SCREEN_WIDTH, SSD1306_WHITE);
}
void drawScreen() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
if (page == 0) {
drawHeader("DHT11 T/H");
display.setTextSize(2);
display.setCursor(0, 18);
if (isnan(temperature)) display.print(F("-- C"));
else { display.print(temperature, 1); display.print(F(" C")); }
display.setCursor(0, 42);
if (isnan(humidity)) display.print(F("-- %"));
else { display.print(humidity, 1); display.print(F(" %")); }
} else if (page == 1) {
drawHeader("Key count");
display.setTextSize(1);
for (int i = 0; i < KEY_COUNT; i++) {
display.setCursor((i % 2) * 64, 16 + (i / 2) * 22);
display.print(KEY_NAME[i]);
display.print(F(": "));
display.print(keyCount[i]);
}
display.setCursor(0, 54);
display.print(F("LT page RT inv LB dim"));
} else {
drawHeader("Min / Max");
display.setTextSize(1);
display.setCursor(0, 16);
display.print(F("T min "));
if (isnan(tMin)) display.print(F("--")); else display.print(tMin, 1);
display.setCursor(64, 16);
display.print(F("max "));
if (isnan(tMax)) display.print(F("--")); else display.print(tMax, 1);
display.setCursor(0, 30);
display.print(F("H min "));
if (isnan(hMin)) display.print(F("--")); else display.print(hMin, 1);
display.setCursor(64, 30);
display.print(F("max "));
if (isnan(hMax)) display.print(F("--")); else display.print(hMax, 1);
display.setCursor(0, 50);
display.print(F("RB = clear"));
}
display.display();
}
void setup() {
for (int i = 0; i < KEY_COUNT; i++) {
pinMode(KEY_PINS[i], INPUT_PULLUP); // 内部上拉,外面不用加电阻
}
Wire.setSCL(OLED_SCL_PIN); // STM32duino 会自动处理 I2C1 重映射
Wire.setSDA(OLED_SDA_PIN);
Wire.begin();
// SSD1306_SWITCHCAPVCC 这个参数就是叫控制器打开内部充电泵(命令 0x8D 0x14),
// 面板的高压全靠它。少了这一步屏会全黑——即使 I2C 通信完全正常。
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
applyBrightness();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 16);
display.println(F("O31 + SSD1306"));
display.println(F("DHT11 + 4 keys"));
display.println();
display.println(F("LT page RT invert"));
display.println(F("LB bright RB clear"));
display.display();
dht.begin();
delay(1500); // DHT11 上电后需要稳定时间
}
void loop() {
bool needRedraw = false;
for (int i = 0; i < KEY_COUNT; i++) {
if (!scanKey(i)) continue;
keyCount[i]++;
switch (i) {
case 0: // 左上:翻页
page = (page + 1) % PAGE_COUNT;
break;
case 1: // 右上:反色
inverted = !inverted;
display.invertDisplay(inverted);
break;
case 2: // 左下:亮度三档
brightIdx = (brightIdx + 1) % 3;
applyBrightness();
break;
case 3: // 右下:清零
resetAll();
break;
}
needRedraw = true;
}
if (millis() - lastDht >= DHT_INTERVAL) {
lastDht = millis();
float h = dht.readHumidity();
float t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) { // 读失败时保留上一次的值,不要刷成 nan
humidity = h;
temperature = t;
if (isnan(tMin) || t < tMin) tMin = t;
if (isnan(tMax) || t > tMax) tMax = t;
if (isnan(hMin) || h < hMin) hMin = h;
if (isnan(hMax) || h > hMax) hMax = h;
}
needRedraw = true;
}
if (needRedraw) drawScreen();
}
② Arduino UNO R3(Arduino IDE)
库管理器里装三个:Adafruit SSD1306、Adafruit GFX Library、DHT sensor library(会带上 Adafruit Unified Sensor)。UNO 的硬件 I2C 固定在 A4/A5,不用也不能设置。四个键的功能和 STM32 版完全一致。
// Arduino UNO R3 + 0.96 OLED(SSD1306) + DHT11 + 4 按键
// 接线:
// OLED VDD->5V(或3.3V) GND->GND SCL->A5 SDA->A4 (UNO 硬件 I2C 固定脚,换不了)
// DHT11 VCC->5V GND->GND S->D2
// 按键 左上->D4 右上->D5 左下->D6 右下->D7,另一脚全部接 GND
// 四键功能:左上翻页 / 右上反色 / 左下亮度三档 / 右下清零
// 环境:Arduino IDE,开发板选 Arduino Uno。库:Adafruit SSD1306 + GFX + DHT sensor library
// 验证状态:未实测,供参考
#define DHT_PIN 2
const uint8_t KEY_PINS[4] = {4, 5, 6, 7}; // 左上 / 右上 / 左下 / 右下
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#define OLED_ADDR 0x3C
#define DHT_TYPE DHT11
#define DHT_INTERVAL 2000UL
#define KEY_COUNT 4
#define DEBOUNCE_MS 20UL
#define PAGE_COUNT 3
const char *KEY_NAME[KEY_COUNT] = {"LT", "RT", "LB", "RB"}; // 左上/右上/左下/右下
const uint8_t BRIGHTNESS[3] = {0x10, 0x7F, 0xCF}; // 亮度三档(对比度寄存器 0x81)
Adafruit_SSD1306 display(128, 64, &Wire, -1);
DHT dht(DHT_PIN, DHT_TYPE);
bool keyStable[KEY_COUNT] = {true, true, true, true};
bool keyLastRaw[KEY_COUNT] = {true, true, true, true};
unsigned long keyLastChange[KEY_COUNT] = {0, 0, 0, 0};
unsigned int keyCount[KEY_COUNT] = {0, 0, 0, 0};
int page = 0; // 0=温湿度 1=按键计数 2=最值
bool inverted = false;
uint8_t brightIdx = 2;
float temperature = NAN, humidity = NAN;
float tMin = NAN, tMax = NAN, hMin = NAN, hMax = NAN;
unsigned long lastDht = 0;
bool scanKey(int i) {
bool raw = (digitalRead(KEY_PINS[i]) == HIGH); // 上拉输入:HIGH=松开,LOW=按下
unsigned long now = millis();
if (raw != keyLastRaw[i]) { keyLastRaw[i] = raw; keyLastChange[i] = now; return false; }
if (now - keyLastChange[i] < DEBOUNCE_MS) return false;
if (raw != keyStable[i]) { keyStable[i] = raw; if (!raw) return true; }
return false;
}
void applyBrightness() {
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(BRIGHTNESS[brightIdx]);
}
void resetAll() {
for (int i = 0; i < KEY_COUNT; i++) keyCount[i] = 0;
tMin = tMax = hMin = hMax = NAN;
}
void drawHeader(const char *title) {
display.setTextSize(1);
display.setCursor(0, 0);
display.print(title);
display.setCursor(92, 0);
display.print(F("B")); display.print(brightIdx + 1);
display.print(inverted ? F(" I") : F(" "));
display.drawFastHLine(0, 10, 128, SSD1306_WHITE);
}
void drawScreen() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
if (page == 0) {
drawHeader("DHT11 T/H");
display.setTextSize(2);
display.setCursor(0, 18);
if (isnan(temperature)) display.print(F("-- C"));
else { display.print(temperature, 1); display.print(F(" C")); }
display.setCursor(0, 42);
if (isnan(humidity)) display.print(F("-- %"));
else { display.print(humidity, 1); display.print(F(" %")); }
} else if (page == 1) {
drawHeader("Key count");
display.setTextSize(1);
for (int i = 0; i < KEY_COUNT; i++) {
display.setCursor((i % 2) * 64, 16 + (i / 2) * 22);
display.print(KEY_NAME[i]); display.print(F(": ")); display.print(keyCount[i]);
}
display.setCursor(0, 54);
display.print(F("LT page RT inv LB dim"));
} else {
drawHeader("Min / Max");
display.setTextSize(1);
display.setCursor(0, 16); display.print(F("T min "));
if (isnan(tMin)) display.print(F("--")); else display.print(tMin, 1);
display.setCursor(64, 16); display.print(F("max "));
if (isnan(tMax)) display.print(F("--")); else display.print(tMax, 1);
display.setCursor(0, 30); display.print(F("H min "));
if (isnan(hMin)) display.print(F("--")); else display.print(hMin, 1);
display.setCursor(64, 30); display.print(F("max "));
if (isnan(hMax)) display.print(F("--")); else display.print(hMax, 1);
display.setCursor(0, 50); display.print(F("RB = clear"));
}
display.display();
}
void setup() {
for (int i = 0; i < KEY_COUNT; i++) pinMode(KEY_PINS[i], INPUT_PULLUP);
Wire.begin(); // UNO 固定 A4=SDA, A5=SCL
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR); // SWITCHCAPVCC = 开充电泵,少了会全黑
applyBrightness();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 16);
display.println(F("UNO + SSD1306"));
display.println(F("DHT11 + 4 keys"));
display.println();
display.println(F("LT page RT invert"));
display.println(F("LB bright RB clear"));
display.display();
dht.begin();
delay(1500);
}
void loop() {
bool needRedraw = false;
for (int i = 0; i < KEY_COUNT; i++) {
if (!scanKey(i)) continue;
keyCount[i]++;
switch (i) {
case 0: page = (page + 1) % PAGE_COUNT; break; // 左上:翻页
case 1: inverted = !inverted; display.invertDisplay(inverted); break; // 右上:反色
case 2: brightIdx = (brightIdx + 1) % 3; applyBrightness(); break; // 左下:亮度
case 3: resetAll(); break; // 右下:清零
}
needRedraw = true;
}
if (millis() - lastDht >= DHT_INTERVAL) {
lastDht = millis();
float h = dht.readHumidity(), t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) {
humidity = h; temperature = t;
if (isnan(tMin) || t < tMin) tMin = t;
if (isnan(tMax) || t > tMax) tMax = t;
if (isnan(hMin) || h < hMin) hMin = h;
if (isnan(hMax) || h > hMax) hMax = h;
}
needRedraw = true;
}
if (needRedraw) drawScreen();
}
③ ESP32 DevKit(Arduino IDE / PlatformIO)
和 UNO 那份逻辑完全一样,差别只有两处:ESP32 的 I2C 脚可以自己指定(Wire.begin(SDA, SCL)),以及按键换成了 GPIO32/33/25/26 这四个带内部上拉、又不是启动模式脚的安全脚。
// ESP32 DevKit + 0.96 OLED(SSD1306) + DHT11 + 4 按键
// 接线:
// OLED VDD->3.3V GND->GND SCL->GPIO22 SDA->GPIO21
// DHT11 VCC->3.3V GND->GND S->GPIO4
// 按键 左上->GPIO32 右上->GPIO33 左下->GPIO25 右下->GPIO26,另一脚全部接 GND
// 注意 GPIO34~39 没有内部上拉不能这样接;GPIO0/2/12/15 是启动模式脚,也别用
// 四键功能:左上翻页 / 右上反色 / 左下亮度三档 / 右下清零
// 环境:Arduino IDE,开发板选 ESP32 Dev Module
// 验证状态:未实测,供参考
#define DHT_PIN 4
#define OLED_SDA 21
#define OLED_SCL 22
const uint8_t KEY_PINS[4] = {32, 33, 25, 26}; // 左上 / 右上 / 左下 / 右下
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#define OLED_ADDR 0x3C
#define DHT_TYPE DHT11
#define DHT_INTERVAL 2000UL
#define KEY_COUNT 4
#define DEBOUNCE_MS 20UL
#define PAGE_COUNT 3
const char *KEY_NAME[KEY_COUNT] = {"LT", "RT", "LB", "RB"}; // 左上/右上/左下/右下
const uint8_t BRIGHTNESS[3] = {0x10, 0x7F, 0xCF}; // 亮度三档(对比度寄存器 0x81)
Adafruit_SSD1306 display(128, 64, &Wire, -1);
DHT dht(DHT_PIN, DHT_TYPE);
bool keyStable[KEY_COUNT] = {true, true, true, true};
bool keyLastRaw[KEY_COUNT] = {true, true, true, true};
unsigned long keyLastChange[KEY_COUNT] = {0, 0, 0, 0};
unsigned int keyCount[KEY_COUNT] = {0, 0, 0, 0};
int page = 0; // 0=温湿度 1=按键计数 2=最值
bool inverted = false;
uint8_t brightIdx = 2;
float temperature = NAN, humidity = NAN;
float tMin = NAN, tMax = NAN, hMin = NAN, hMax = NAN;
unsigned long lastDht = 0;
bool scanKey(int i) {
bool raw = (digitalRead(KEY_PINS[i]) == HIGH); // 上拉输入:HIGH=松开,LOW=按下
unsigned long now = millis();
if (raw != keyLastRaw[i]) { keyLastRaw[i] = raw; keyLastChange[i] = now; return false; }
if (now - keyLastChange[i] < DEBOUNCE_MS) return false;
if (raw != keyStable[i]) { keyStable[i] = raw; if (!raw) return true; }
return false;
}
void applyBrightness() {
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(BRIGHTNESS[brightIdx]);
}
void resetAll() {
for (int i = 0; i < KEY_COUNT; i++) keyCount[i] = 0;
tMin = tMax = hMin = hMax = NAN;
}
void drawHeader(const char *title) {
display.setTextSize(1);
display.setCursor(0, 0);
display.print(title);
display.setCursor(92, 0);
display.print(F("B")); display.print(brightIdx + 1);
display.print(inverted ? F(" I") : F(" "));
display.drawFastHLine(0, 10, 128, SSD1306_WHITE);
}
void drawScreen() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
if (page == 0) {
drawHeader("DHT11 T/H");
display.setTextSize(2);
display.setCursor(0, 18);
if (isnan(temperature)) display.print(F("-- C"));
else { display.print(temperature, 1); display.print(F(" C")); }
display.setCursor(0, 42);
if (isnan(humidity)) display.print(F("-- %"));
else { display.print(humidity, 1); display.print(F(" %")); }
} else if (page == 1) {
drawHeader("Key count");
display.setTextSize(1);
for (int i = 0; i < KEY_COUNT; i++) {
display.setCursor((i % 2) * 64, 16 + (i / 2) * 22);
display.print(KEY_NAME[i]); display.print(F(": ")); display.print(keyCount[i]);
}
display.setCursor(0, 54);
display.print(F("LT page RT inv LB dim"));
} else {
drawHeader("Min / Max");
display.setTextSize(1);
display.setCursor(0, 16); display.print(F("T min "));
if (isnan(tMin)) display.print(F("--")); else display.print(tMin, 1);
display.setCursor(64, 16); display.print(F("max "));
if (isnan(tMax)) display.print(F("--")); else display.print(tMax, 1);
display.setCursor(0, 30); display.print(F("H min "));
if (isnan(hMin)) display.print(F("--")); else display.print(hMin, 1);
display.setCursor(64, 30); display.print(F("max "));
if (isnan(hMax)) display.print(F("--")); else display.print(hMax, 1);
display.setCursor(0, 50); display.print(F("RB = clear"));
}
display.display();
}
void setup() {
for (int i = 0; i < KEY_COUNT; i++) pinMode(KEY_PINS[i], INPUT_PULLUP);
Wire.begin(OLED_SDA, OLED_SCL); // ESP32 的 I2C 脚可以自己指定
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR); // SWITCHCAPVCC = 开充电泵,少了会全黑
applyBrightness();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 16);
display.println(F("ESP32 + SSD1306"));
display.println(F("DHT11 + 4 keys"));
display.println();
display.println(F("LT page RT invert"));
display.println(F("LB bright RB clear"));
display.display();
dht.begin();
delay(1500);
}
void loop() {
bool needRedraw = false;
for (int i = 0; i < KEY_COUNT; i++) {
if (!scanKey(i)) continue;
keyCount[i]++;
switch (i) {
case 0: page = (page + 1) % PAGE_COUNT; break; // 左上:翻页
case 1: inverted = !inverted; display.invertDisplay(inverted); break; // 右上:反色
case 2: brightIdx = (brightIdx + 1) % 3; applyBrightness(); break; // 左下:亮度
case 3: resetAll(); break; // 右下:清零
}
needRedraw = true;
}
if (millis() - lastDht >= DHT_INTERVAL) {
lastDht = millis();
float h = dht.readHumidity(), t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) {
humidity = h; temperature = t;
if (isnan(tMin) || t < tMin) tMin = t;
if (isnan(tMax) || t > tMax) tMax = t;
if (isnan(hMin) || h < hMin) hMin = h;
if (isnan(hMax) || h > hMax) hMax = h;
}
needRedraw = true;
}
if (needRedraw) drawScreen();
}
④ 树莓派(Python 3)
树莓派上 OLED 用 luma.oled(ssd1306 和 sh1106 两种驱动都原生支持,换一个词就行)、DHT11 用 adafruit-circuitpython-dht、按键用 gpiozero(bounce_time 自带消抖,比 Arduino 那边省事)。四个键的功能和 STM32 版一致。
# 1) 打开 I2C:sudo raspi-config → Interface Options → I2C → Yes → 重启
# 2) 确认屏的地址(应该看到 3c)
sudo apt update
sudo apt install -y python3-pip i2c-tools libgpiod2
i2cdetect -y 1
# 3) 装库(建议放虚拟环境;系统 Python 报 externally-managed 时加 --break-system-packages)
pip3 install luma.oled adafruit-circuitpython-dht gpiozero
# 树莓派 + 0.96 OLED(SSD1306) + DHT11 + 4 按键
# 接线(BCM 编号 / 括号是物理脚号):
# OLED VDD->3.3V(1) GND->GND(6) SDA->GPIO2(3) SCL->GPIO3(5)
# DHT11 VCC->3.3V(1) GND->GND(9) S->GPIO4(7)
# 按键 左上->GPIO17(11) 右上->GPIO27(13) 左下->GPIO22(15) 右下->GPIO23(16)
# 另一脚全部接 GND
# 警告 树莓派 GPIO 只有 3.3V,绝对不能接 5V,会烧 SoC
# 四键功能:左上翻页 / 右上反色 / 左下亮度三档 / 右下清零
# 环境:Raspberry Pi OS + Python 3
# 验证状态:未实测,供参考
import time
import board
import adafruit_dht
from gpiozero import Button
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306 # 本批屏实测是 SSD1306;SH1106 的屏改成 sh1106
from luma.core.render import canvas
from PIL import ImageFont
OLED_ADDR = 0x3C
KEY_BCM = [("LT", 17), ("RT", 27), ("LB", 22), ("RB", 23)] # 左上/右上/左下/右下
DHT_INTERVAL = 2.0 # DHT11 最少 2 秒读一次
BRIGHTNESS = [0x10, 0x7F, 0xCF] # 亮度三档
device = ssd1306(i2c(port=1, address=OLED_ADDR), width=128, height=64)
font = ImageFont.load_default()
dht = adafruit_dht.DHT11(board.D4)
state = {
"page": 0, # 0=温湿度 1=按键计数 2=最值
"inverted": False,
"bright": 2,
"counts": {name: 0 for name, _ in KEY_BCM},
"t": None, "h": None,
"tmin": None, "tmax": None, "hmin": None, "hmax": None,
}
def apply_brightness():
device.contrast(BRIGHTNESS[state["bright"]])
def reset_all():
for name in state["counts"]:
state["counts"][name] = 0
state["tmin"] = state["tmax"] = state["hmin"] = state["hmax"] = None
def on_press(name):
def handler():
state["counts"][name] += 1
if name == "LT": # 左上:翻页
state["page"] = (state["page"] + 1) % 3
elif name == "RT": # 右上:反色
state["inverted"] = not state["inverted"]
elif name == "LB": # 左下:亮度三档
state["bright"] = (state["bright"] + 1) % 3
apply_brightness()
elif name == "RB": # 右下:清零
reset_all()
draw()
return handler
buttons = []
for name, pin in KEY_BCM:
b = Button(pin, pull_up=True, bounce_time=0.02) # 内部上拉 + 20ms 消抖
b.when_pressed = on_press(name)
buttons.append(b)
def fmt(v):
return "--" if v is None else "%.1f" % v
def draw():
fg, bg = (0, 255) if state["inverted"] else (255, 0) # 反色就是前景背景对调
with canvas(device) as d:
d.rectangle(device.bounding_box, outline=bg, fill=bg)
head = ["DHT11 T/H", "Key count", "Min / Max"][state["page"]]
d.text((0, 0), head, font=font, fill=fg)
d.text((92, 0), "B%d%s" % (state["bright"] + 1, " I" if state["inverted"] else ""),
font=font, fill=fg)
d.line((0, 10, 127, 10), fill=fg)
if state["page"] == 0:
d.text((0, 20), "%s C" % fmt(state["t"]), font=font, fill=fg)
d.text((0, 40), "%s %%" % fmt(state["h"]), font=font, fill=fg)
elif state["page"] == 1:
for i, (name, _) in enumerate(KEY_BCM):
d.text(((i % 2) * 64, 16 + (i // 2) * 22),
"%s: %d" % (name, state["counts"][name]), font=font, fill=fg)
d.text((0, 54), "LT page RT inv LB dim", font=font, fill=fg)
else:
d.text((0, 16), "T min %s max %s" % (fmt(state["tmin"]), fmt(state["tmax"])),
font=font, fill=fg)
d.text((0, 30), "H min %s max %s" % (fmt(state["hmin"]), fmt(state["hmax"])),
font=font, fill=fg)
d.text((0, 50), "RB = clear", font=font, fill=fg)
apply_brightness()
last_dht = 0.0
try:
draw()
while True:
now = time.monotonic()
if now - last_dht >= DHT_INTERVAL:
last_dht = now
try:
t, h = dht.temperature, dht.humidity
if t is not None and h is not None: # 读失败就保留上一次的值
state["t"], state["h"] = t, h
state["tmin"] = t if state["tmin"] is None else min(state["tmin"], t)
state["tmax"] = t if state["tmax"] is None else max(state["tmax"], t)
state["hmin"] = h if state["hmin"] is None else min(state["hmin"], h)
state["hmax"] = h if state["hmax"] is None else max(state["hmax"], h)
except RuntimeError:
pass # DHT11 在树莓派上偶尔读失败是正常的,下一轮再来
draw()
time.sleep(0.05)
except KeyboardInterrupt:
pass
finally:
dht.exit()
树莓派不是实时系统,DHT11 这种靠微秒级时序的单总线传感器偶尔读失败是正常现象,代码里已经 try/except RuntimeError 兜住了,不要当成模块坏。要稳定读温湿度,建议换 I2C 接口的 SHT30 之类。
树莓派这份的按键回调里同样可以照 四键功能表接功能;改引脚只要改 KEY_BCM 那一行。