
1. 从一块开发板说起SENSE v0.1 到底想解决什么问题1.1 这个项目是什么适合谁看SENSE v0.1 是我最近折腾的一个小型环境感知节点项目核心思路很简单用一块 Arduino UNO Q 做主控挂上 SH1107 OLED 显示屏、LSM6DSO 六轴惯性传感器、SHT40 温湿度传感器和 SGP40 空气质量传感器做成一个能实时显示环境数据的桌面小设备。名字叫 SENSE取的就是感知的意思v0.1 说明它还在很早期的阶段功能不算复杂但该有的骨架都搭起来了。这个项目适合几类人参考一是刚接触 Arduino 生态、想找一个传感器种类够多但不至于劝退的练手项目的人二是手里已经攒了几块传感器模块、想找个理由把它们串起来用的人三是对环境监测、桌面小摆件感兴趣想自己做一个能看温湿度、空气质量的小工具的人。如果你完全没碰过 Arduino也不用慌我会把每个环节的坑都讲清楚照着做基本能跑通。需要提前说明的是SENSE v0.1 不是一个追求极致精度或工业级可靠性的项目它更像是一个能跑起来、能看数据、能继续往上加功能的原型。我个人的定位是把它当成一个可扩展的感知平台后面想加什么传感器、想接什么显示逻辑都可以在这个基础上改。1.2 为什么选这几个器件选型这件事我踩过不少坑最后定下来的这套组合背后是有取舍的。主控选 Arduino UNO Q主要是看中它在经典 UNO 形态上做了升级引脚布局熟悉社区资料多同时性能比老款 UNO 强不少跑多个 I2C 传感器加一块 OLED 刷新完全够用。如果你手里是别的 UNO 兼容板大部分逻辑也能直接搬过去只要注意 I2C 引脚定义和电压电平就行。显示屏用 SH1107 OLED而不是更常见的 SSD1306原因很实际SH1107 常见于 1.12 寸到 1.3 寸的小屏分辨率有 128x128 这种方形规格显示多行数据时排版更舒服。SSD1306 大多是 128x64行数少塞温湿度加空气质量会显得挤。SH1107 的驱动库虽然没 SSD1306 那么泛滥但 Adafruit 和 U8g2 都支持实际用下来问题不大。传感器三件套的分工是这样的LSM6DSO 负责加速度和角速度能感知设备的姿态和晃动SHT40 负责温湿度精度和稳定性在同类里口碑很好SGP40 负责挥发性有机物VOC相关的空气质量指数适合做趋势判断。这三个都是 I2C 接口接线简单地址也不冲突这是能挂在一起的前提。提示选传感器时I2C 地址冲突是最容易被忽略的坑。SHT40 默认地址是 0x44SGP40 是 0x59LSM6DSO 常见是 0x6A 或 0x6BSH1107 一般是 0x3C 或 0x3D。动手前先用 I2C 扫描程序确认一遍能省掉大量为什么读不到数据的排查时间。2. 硬件连接与地址规划把线接对是成功的一半2.1 供电与电平的注意事项Arduino UNO Q 的 I2C 引脚通常工作在 3.3V 或 5V 逻辑具体要看板子版本。SHT40、SGP40、LSM6DSO 这几款传感器基本都是 3.3V 供电和 3.3V 逻辑SH1107 OLED 模块有的支持 3.3V 到 5V 宽压有的只支持 3.3V。这里有个关键点如果主控 I2C 是 5V 逻辑而传感器是 3.3V 逻辑直接连可能损坏传感器或者通信不稳定。我的做法是统一走 3.3V。UNO Q 上一般有 3.3V 输出引脚把四个模块的 VCC 都接到 3.3VGND 共地SDA 和 SCL 分别并联到主控的 I2C 引脚。这样电平一致最省心。如果某个 OLED 模块只支持 5V 供电但逻辑是 3.3V那就要单独处理供电别偷懒。实际接线时我建议用面包板先搭一遍确认能通信再考虑焊接到洞洞板或做 PCB。I2C 总线上的上拉电阻也值得留意很多模块自带上拉如果多个模块都带上拉并联后阻值会变小可能导致总线负载过重。一般 4.7k 到 10k 是常见值模块自带的话通常不用额外加但如果通信不稳可以检查一下总上拉阻值。2.2 I2C 地址扫描与冲突排查接线完成后第一件事不是写业务代码而是跑一个 I2C 扫描程序把所有在线设备的地址打印出来。这一步能直接告诉你哪些模块被识别到了、地址是多少。#include Wire.h void setup() { Wire.begin(); Serial.begin(115200); while (!Serial); Serial.println(I2C Scanner starting...); } void loop() { byte error, address; int nDevices 0; Serial.println(Scanning...); for (address 1; address 127; address) { Wire.beginTransmission(address); error Wire.endTransmission(); if (error 0) { Serial.print(Device found at 0x); if (address 16) Serial.print(0); Serial.println(address, HEX); nDevices; } } if (nDevices 0) Serial.println(No I2C devices found); delay(5000); }跑完这个程序你应该能看到类似 0x3COLED、0x44SHT40、0x59SGP40、0x6ALSM6DSO这样的地址列表。如果某个设备没出现先检查接线再检查供电最后检查模块本身是否损坏。我遇到过好几次是杜邦线接触不良换根线就好了别一上来就怀疑代码。注意SHT40 的地址是固定的 0x44不能通过引脚改。如果你同时要用两个 SHT40就需要 I2C 多路复用器比如 TCA9548A。SGP40 地址固定 0x59同理。LSM6DSO 可以通过 SA0 引脚在 0x6A 和 0x6B 之间切换这个设计在需要多传感器时很有用。2.3 各模块的初始化顺序初始化顺序在 v0.1 里我做了固定安排先初始化 Wire 库再初始化 OLED然后依次初始化三个传感器。原因是 OLED 初始化最快先点亮屏幕能给后面的调试一个视觉反馈传感器初始化如果失败可以在屏幕上直接显示错误信息比只看串口方便。每个传感器的初始化都要检查返回值。比如 SHT40 的 begin 函数如果返回 false说明通信失败这时候不要继续往下跑直接在屏幕上显示SHT40 FAIL然后进入一个安全的等待循环。这样你一眼就能看出是哪个模块出了问题而不是面对一堆乱码数据发呆。3. 传感器数据读取每个器件的脾气都不一样3.1 SHT40 温湿度读取的细节SHT40 用起来很舒服但有几个细节不注意就会读到错误数据。它支持几种测量模式区别在于精度和功耗。v0.1 里我用的是高精度模式命令是 0xFD测量时间大约 8.3 毫秒。发送命令后要等足够时间再读数据读太早会拿到旧值或者无效值。数据格式是 6 个字节温度高字节、温度低字节、温度 CRC、湿度高字节、湿度低字节、湿度 CRC。转换公式是温度 -45 175 * (raw_temp / 65535)湿度 -6 125 * (raw_hum / 65535)湿度公式里的 -6 和 125 是官方给的修正系数直接套用就行。CRC 校验建议做虽然大多数时候数据没问题但一旦总线受干扰没有校验就可能把错误数据当成真实值显示出来。#include Wire.h #include Adafruit_SHT4x.h Adafruit_SHT4x sht4 Adafruit_SHT4x(); void setupSHT40() { if (!sht4.begin()) { Serial.println(SHT40 not found); return; } sht4.setPrecision(SHT4X_HIGH_PRECISION); sht4.setHeater(SHT4X_NO_HEATER); } void readSHT40() { sensors_event_t humidity, temp; sht4.getEvent(humidity, temp); Serial.print(Temp: ); Serial.print(temp.temperature); Serial.println( C); Serial.print(Hum: ); Serial.print(humidity.relative_humidity); Serial.println( %); }加热器功能在潮湿环境下有用但会显著增加功耗和温度读数偏差v0.1 里默认关闭。如果你发现湿度长期接近 100%可以考虑短时开启加热器驱潮但别一直开着。3.2 SGP40 空气质量数据的理解SGP40 输出的是一个原始信号需要经过算法转换成 VOC 指数。它不像某些传感器直接给你一个空气质量等级而是给你一个 0 到 500 左右的指数数值越低越好。这个指数是相对值不是绝对浓度所以更适合看趋势而不是看单点数值。SGP40 的读取需要先发送湿度补偿值因为它的测量结果受环境湿度影响。SHT40 读到的温湿度正好可以用来做补偿这也是我把这两个传感器放在一起的原因之一。补偿值的转换有固定公式把温度和湿度转成对应的 ticks 再传给 SGP40。#include Wire.h #include Adafruit_SGP40.h Adafruit_SGP40 sgp; void setupSGP40() { if (!sgp.begin()) { Serial.println(SGP40 not found); return; } } void readSGP40(float temp, float hum) { uint16_t voc sgp.measureVocIndex(temp, hum); Serial.print(VOC Index: ); Serial.println(voc); }SGP40 有个预热特性刚上电时读数会偏高需要运行一段时间才稳定。我一般让它跑 5 到 10 分钟再看数据。另外它对面部酒精、香水、烹饪油烟都很敏感放在厨房附近数值会飙升这是正常现象不是故障。3.3 LSM6DSO 姿态数据的处理LSM6DSO 是六轴传感器输出三轴加速度和三轴角速度。v0.1 里我主要用它做两件事一是检测设备是否被移动或倾斜二是给后续可能的姿态计算留接口。加速度数据的单位是 g静止时某个轴应该接近 1g取决于摆放方向。角速度单位是度每秒静止时应该接近 0。如果静止时读数漂移很大可能是量程设置不合适或者传感器需要校准。#include Wire.h #include Adafruit_LSM6DSO.h Adafruit_LSM6DSO lsm; void setupLSM6DSO() { if (!lsm.begin_I2C()) { Serial.println(LSM6DSO not found); return; } lsm.setAccelRange(LSM6DS_ACCEL_RANGE_2_G); lsm.setGyroRange(LSM6DS_GYRO_RANGE_250_DPS); lsm.setAccelDataRate(LSM6DS_ACCEL_RATE_104_HZ); lsm.setGyroDataRate(LSM6DS_GYRO_RATE_104_HZ); } void readLSM6DSO() { sensors_event_t accel, gyro, temp; lsm.getEvent(accel, gyro, temp); Serial.print(Accel X: ); Serial.print(accel.acceleration.x); Serial.print( Y: ); Serial.print(accel.acceleration.y); Serial.print( Z: ); Serial.println(accel.acceleration.z); }量程选择上2G 和 250dps 适合桌面级应用灵敏度高。如果你要做剧烈运动的检测可以调到 4G 或 8G但分辨率会下降。数据速率 104Hz 对显示用途来说绰绰有余再高只会增加总线负担。4. SH1107 显示布局小屏幕上怎么塞下所有信息4.1 屏幕初始化与坐标系SH1107 的初始化比 SSD1306 稍微麻烦一点因为它的显存寻址方式和分辨率规格更多样。我用的是 U8g2 库它对 SH1107 的支持比较完善能自动处理不同分辨率的偏移问题。#include U8g2lib.h #include Wire.h U8G2_SH1107_128X128_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); void setupOLED() { u8g2.begin(); u8g2.setContrast(128); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(0, 12, SENSE v0.1); u8g2.sendBuffer(); }这里有个坑不同厂家的 SH1107 模块显存列偏移可能不一样。有的模块需要设置 0x00 偏移有的需要 0x02。如果显示内容整体偏移了两列或者右边有花屏大概率是这个问题。U8g2 的构造参数里可以指定偏移具体值要查你手上模块的规格书或者试出来。4.2 多传感器数据的排版策略128x128 的屏幕用 6x10 的字体一行能放大约 21 个字符纵向能放 12 行左右。我的排版是这样的第一行标题 SENSE v0.1第二到三行温度、湿度第四到五行VOC 指数和状态第六到七行加速度 X/Y/Z第八到九行角速度 X/Y/Z底部运行时间或状态提示这样安排的好处是信息分区清晰一眼能看出哪块数据异常。如果屏幕更小比如 64x128就要考虑分页显示用按键切换页面。v0.1 里我没做按键因为屏幕够用后面加按键的话可以扩展成多页。刷新策略上不要每次循环都全屏重绘那样会闪。我的做法是只更新数值变化的区域用u8g2.clearBuffer()配合局部绘制或者用双缓冲机制。U8g2 本身支持缓冲sendBuffer()才真正推送到屏幕所以绘制过程不会闪。4.3 数值格式化与单位显示传感器读出来的是浮点数直接显示会带一长串小数既占地方又不好看。我的处理是温度保留一位小数湿度保留整数VOC 显示整数加速度保留两位小数。用dtostrf或者snprintf格式化后再绘制。char buf[16]; snprintf(buf, sizeof(buf), T: %.1f C, temp); u8g2.drawStr(0, 24, buf); snprintf(buf, sizeof(buf), H: %.0f %%, hum); u8g2.drawStr(0, 36, buf);单位符号要注意字体支持。U8g2 的某些字体不包含摄氏度符号显示出来会是乱码。我一般直接用 C 代替 °C省事又不会出错。百分号在大多数字体里都有问题不大。5. 常见问题与排查技巧实录5.1 传感器读不到数据的排查顺序这是最高频的问题我整理了一个排查顺序按这个走基本能定位现象可能原因排查方法I2C 扫描无设备接线错误或供电不足检查 VCC/GND/SDA/SCL测电压扫描到地址但读数为 0初始化失败或模式未设置检查 begin 返回值确认测量模式数据偶尔跳变总线干扰或上拉不当缩短线长检查上拉电阻某个传感器一直失败模块损坏或地址冲突单独测试该模块换模块验证OLED 显示花屏显存偏移或分辨率不匹配调整 U8g2 构造参数我遇到最多的是供电问题。四个模块同时挂在 3.3V 上如果主控的 3.3V 输出电流不够就会出现某些模块时好时坏。这时候可以给传感器单独供电或者换一个电流能力更强的稳压模块。5.2 SGP40 数值异常的判断SGP40 的数值异常通常有三种表现一直很高、一直很低、剧烈跳动。一直很高最常见的原因是刚上电没预热或者附近有酒精、香水、清洁剂。等十分钟再看如果还是高检查一下是不是放在了密闭空间里VOC 积累会导致读数偏高。一直很低可能是传感器没真正开始测量或者湿度补偿值传错了。确认measureVocIndex的调用参数是当前温湿度而不是固定值。剧烈跳动多半是电源噪声或者 I2C 干扰。可以在 SGP40 的 VCC 和 GND 之间加一个 100nF 电容能明显改善。5.3 OLED 显示不全或偏移的处理SH1107 的显示问题我踩过两次。一次是内容整体右移两列查了半天发现是模块的显存偏移设置不对在 U8g2 初始化时指定偏移量后解决。另一次是屏幕下半部分不显示原因是分辨率参数写错了实际是 128x128我按 128x64 初始化自然只显示一半。提示拿到一块不熟悉的 OLED 模块先确认三件事驱动芯片型号SH1107 还是 SSD1306、分辨率128x64 还是 128x128、I2C 地址0x3C 还是 0x3D。这三个信息对了显示基本不会出大问题。5.4 整体运行不稳定的系统级排查如果单个模块都正常但合在一起跑就出问题那多半是系统级的原因。我总结了几条经验电源要够四个模块加主控峰值电流可能超过 200mAUSB 供电有时会不够建议用独立电源。线要短I2C 总线越长越容易受干扰尽量控制在 20cm 以内超过就要考虑降低速率或加缓冲。循环要合理不要在一个 loop 里把所有传感器都读一遍然后立刻刷新屏幕可以分时读取比如每 100ms 读一个传感器轮流来减轻总线压力。看门狗要用长时间运行的程序建议开启看门狗防止某个传感器卡死导致整个系统无响应。6. 后续可以怎么扩展SENSE v0.1 只是个起点我在做的过程中已经想好了几个扩展方向。一是加一个 SD 卡模块把数据记录下来方便回看趋势二是加一个蜂鸣器或 LED当 VOC 指数超过阈值时报警三是把数据通过串口或无线模块传到电脑上做更复杂的可视化。硬件上I2C 总线还能再挂几个传感器比如气压计、光照传感器只要地址不冲突就行。软件上可以把显示逻辑做成多页用按键切换这样即使屏幕小也能展示更多信息。我个人在实际操作中的体会是这类多传感器项目最难的不是写代码而是把硬件调稳。代码可以慢慢改但接线、供电、地址这些基础问题如果没处理好后面会一直返工。所以我的建议是每加一个模块就先单独测通再合到一起别一次性全接上然后指望它一次跑通。踩过的坑告诉我分步验证是最省时间的做法。