1. 项目缘起与整体设计思路
1.1 为什么我要做一块“SENSE v0.1”环境感知板
手头攒了一堆传感器,LSM6DSO、SHT40、SGP40 这几颗芯片单独玩都跑通过,但每次想同时采集运动、温湿度、空气质量数据,就得在面包板上重新插一遍,杜邦线一多就接触不良,调试起来非常烦。于是决定做一块固定的小板子,把这几颗传感器和一块 SH1107 OLED 整合到一起,插上 Arduino UNO Q 就能直接读数,这就是 SENSE v0.1 的由来。
它的定位很明确:一块低功耗、多模态的环境感知节点。能同时输出三轴加速度/角速度、环境温湿度、以及挥发性有机物(VOC)指数,并在本地 OLED 上实时刷新。适合谁?适合刚接触嵌入式传感采集的开发者、做环境数据记录的学生、以及想快速验证多传感器融合逻辑的创客。你不需要从零画板子,照着这套选型和接线逻辑走,一两个小时就能复现。
1.2 核心器件选型背后的取舍
选型不是拍脑袋,每一颗都有明确的理由。主控用Arduino UNO Q,它同时具备经典 UNO 的引脚兼容性和更强的处理能力,跑多传感器轮询加 OLED 刷新绰绰有余,而且生态成熟,库支持好,新手不容易卡在环境配置上。
传感器方面,LSM6DSO是 ST 的六轴 IMU,I2C/SPI 双接口,内置 FIFO,功耗低,适合做姿态和振动监测。SHT40是 Sensirion 的温湿度传感器,精度高、长期漂移小,I2C 地址固定,接线简单。SGP40是 VOC 空气质量传感器,输出的是 VOC 指数而非绝对浓度,需要配合湿度补偿,这也是为什么必须和 SHT40 搭配使用。
显示用SH1107 OLED,常见于 128x128 或 128x64 分辨率的小屏,I2C 接口,驱动芯片和 SSD1306 类似但显存寻址方式不同,很多人第一次用会踩坑,后面我会详细说。
注意:SGP40 的 VOC 指数受湿度影响很大,单独用它读数会飘,必须用 SHT40 的温湿度做补偿,这是选型时就要考虑的组合逻辑,不是后期补丁。
1.3 系统架构与数据流设计
整块板子的数据流很清晰:UNO Q 作为 I2C 主机,轮询三颗传感器,读取原始数据后做单位换算和补偿计算,再把结果格式化后推送到 SH1107 显示。为什么不把补偿算法放在传感器端?因为 SGP40 只输出原始 VOC 指数,补偿必须由主控完成。
I2C 总线上挂四个设备(三传感器加一屏),地址不能冲突。LSM6DSO 默认地址 0x6A 或 0x6B,SHT40 固定 0x44,SGP40 固定 0x59,SH1107 常见 0x3C 或 0x3D。实际接线前先用 I2C 扫描确认一遍,能省掉大量“为什么读不到数据”的排查时间。
供电方面,所有器件都是 3.3V 逻辑,UNO Q 的 3.3V 输出电流足够驱动它们,但 OLED 刷新时电流波动较大,建议在 OLED 的 VCC 和 GND 之间并一颗 100nF 加一颗 10uF 电容,实测能明显减少花屏和复位现象。
2. 核心细节解析与实操要点
2.1 SHT40 的 I2C 地址与读取时序
SHT40 的 I2C 地址是固定的0x44,7 位地址,不可以通过引脚改变。这一点和很多传感器不同,所以总线上只能挂一颗 SHT40,想挂多颗就得用 I2C 多路复用器。
读取流程分两步:先发送测量命令,等待转换完成,再读取 6 个字节的数据。高精度模式下命令是 0xFD,转换时间典型值 8.3ms。数据格式是温度两个字节加 CRC,湿度两个字节加 CRC。温度换算公式是-45 + 175 * raw / 65535,湿度是-6 + 125 * raw / 65535,湿度结果要钳制在 0 到 100 之间。
Wire.beginTransmission(0x44); Wire.write(0xFD); Wire.endTransmission(); delay(10); Wire.requestFrom(0x44, 6); uint16_t tRaw = (Wire.read() << 8) | Wire.read(); Wire.read(); // CRC uint16_t hRaw = (Wire.read() << 8) | Wire.read(); Wire.read(); // CRC float temperature = -45.0 + 175.0 * tRaw / 65535.0; float humidity = -6.0 + 125.0 * hRaw / 65535.0; humidity = constrain(humidity, 0.0, 100.0);提示:CRC 校验建议保留,虽然多写几行代码,但能过滤掉 I2C 干扰导致的错误读数,尤其在电机或继电器附近工作时非常有用。
2.2 SGP40 的 VOC 指数与湿度补偿逻辑
SGP40 输出的是 VOC 指数,范围 0 到 500 左右,数值越高表示挥发性有机物浓度越高。它内部有个加热器,上电后需要一段预热时间,典型 10 秒到 1 分钟,刚上电的读数不可信。
关键点在于:SGP40 的测量命令需要传入湿度和温度的原始值做补偿。Sensirion 官方提供了转换函数,把 SHT40 读到的温湿度转成 SGP40 需要的补偿参数。如果不做这一步,VOC 指数在湿度变化时会明显漂移。
// 将温湿度转换为 SGP40 补偿参数 uint16_t humidityComp = (uint16_t)(humidity * 65535.0 / 100.0); uint16_t temperatureComp = (uint16_t)((temperature + 45.0) * 65535.0 / 175.0); // 发送测量命令 0x260F,后跟补偿参数和 CRC实测下来,补偿前后在空调房里 VOC 指数能差 20 到 30 个点,所以这一步不能省。另外 SGP40 的读数需要做滑动平均,单次读数跳动较大,我一般取 10 次平均后再显示。
2.3 LSM6DSO 的配置与数据读取
LSM6DSO 支持 I2C 和 SPI,这里用 I2C 简化接线。默认地址 0x6A,如果 SA0 引脚拉高则变成 0x6B。初始化时要配置加速度计量程和陀螺仪量程,我一般设加速度 ±2g、陀螺仪 ±250dps,兼顾精度和量程。
数据寄存器从 0x28 开始,连续 12 个字节分别是加速度 X/Y/Z 和陀螺仪 X/Y/Z,每轴两个字节小端格式。读取后要乘以灵敏度系数,±2g 对应 0.061 mg/LSB,±250dps 对应 8.75 mdps/LSB。
Wire.beginTransmission(0x6A); Wire.write(0x28); Wire.endTransmission(); Wire.requestFrom(0x6A, 12); int16_t ax = (Wire.read() | (Wire.read() << 8)); // 同理读取 ay, az, gx, gy, gz float accelX = ax * 0.061 / 1000.0; // 单位 g注意:LSM6DSO 上电后默认处于掉电模式,必须先写 CTRL1_XL 和 CTRL2_G 寄存器使能加速度计和陀螺仪,否则读出来全是零。这个坑我踩过,排查了半天以为是接线问题。
2.4 SH1107 OLED 的驱动差异与显存寻址
SH1107 和常见的 SSD1306 最大的区别在于显存寻址方式。SSD1306 是 128x64 分 8 页,每页 128 列;SH1107 常见于 128x128 或 128x64,但它的列地址是从 0x02 开始的,不是 0x00。如果你直接用 SSD1306 的库去驱动 SH1107,画面会整体偏移两列,右边缺一块。
解决办法是用支持 SH1107 的库,比如 U8g2,初始化时选择对应的构造器。U8g2 的U8G2_SH1107_128X128_HW_I2C或U8G2_SH1107_SEEED_128X128_HW_I2C都能用,具体看你的屏是哪种。硬件 I2C 比软件 I2C 刷新快很多,128x128 全屏刷新能到 30fps 以上。
#include <U8g2lib.h> U8G2_SH1107_128X128_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); void setup() { u8g2.begin(); } void loop() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(0, 12, "SENSE v0.1"); u8g2.sendBuffer(); }提示:SH1107 的 I2C 地址通常是 0x3C,如果扫描不到就试 0x3D。有些模块背面有地址选择焊盘,改起来也方便。
3. 实操过程与核心环节实现
3.1 硬件接线与 I2C 总线布局
接线本身不复杂,但布局有讲究。我建议把 SHT40 和 SGP40 放在板子一侧,远离 LSM6DSO 和 OLED,因为 SGP40 的加热器会产生微弱热量,离温湿度传感器太近会影响读数。OLED 的排线尽量短,减少干扰。
具体接线如下表:
| 器件 | VCC | GND | SDA | SCL | 备注 |
|---|---|---|---|---|---|
| LSM6DSO | 3.3V | GND | A4 | A5 | 地址 0x6A |
| SHT40 | 3.3V | GND | A4 | A5 | 地址 0x44 |
| SGP40 | 3.3V | GND | A4 | A5 | 地址 0x59 |
| SH1107 | 3.3V | GND | A4 | A5 | 地址 0x3C |
所有 SDA 接 A4,SCL 接 A5,这是 UNO Q 的硬件 I2C 引脚。上拉电阻 UNO Q 内部已有,但如果你接的线比较长,建议在 SDA 和 SCL 上各加一颗 4.7k 上拉到 3.3V,能显著提升稳定性。
3.2 初始化顺序与 I2C 扫描验证
上电后的初始化顺序很重要。先初始化 Wire,然后扫描 I2C 总线确认所有设备在线,再依次初始化各传感器,最后初始化 OLED。这样如果某个设备没响应,你能立刻定位问题。
#include <Wire.h> void scanI2C() { for (uint8_t addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.print("Found: 0x"); Serial.println(addr, HEX); } } }正常应该看到 0x3C、0x44、0x59、0x6A 四个地址。少一个就检查对应器件的接线和供电。SGP40 上电后需要预热,扫描时能读到地址但测量数据要等一会儿才有效。
3.3 数据采集与 OLED 刷新循环
主循环里我采用分时策略:每 100ms 读一次 LSM6DSO,每 500ms 读一次 SHT40,每 1s 读一次 SGP40,OLED 每 200ms 刷新一次。这样既保证实时性,又不会因为 I2C 阻塞导致显示卡顿。
unsigned long lastIMU = 0, lastTH = 0, lastVOC = 0, lastOLED = 0; void loop() { unsigned long now = millis(); if (now - lastIMU >= 100) { readIMU(); lastIMU = now; } if (now - lastTH >= 500) { readSHT40(); lastTH = now; } if (now - lastVOC >= 1000) { readSGP40(); lastVOC = now; } if (now - lastOLED >= 200) { updateOLED(); lastOLED = now; } }OLED 显示布局我分成四行:第一行标题,第二行温湿度,第三行 VOC 指数,第四行加速度值。字体用 6x10,128x128 的屏能放下。如果屏是 128x64,就精简成两行,温湿度一行、VOC 一行。
3.4 参数计算与单位换算的实操记录
单位换算看起来简单,但容易出错。LSM6DSO 的加速度原始值转 g 要除以 1000,因为灵敏度是 mg/LSB。陀螺仪转 dps 要除以 1000,因为灵敏度是 mdps/LSB。SHT40 的温湿度公式前面已经给了,注意湿度钳制。
SGP40 的 VOC 指数不需要换算,直接读出来就是指数值,但要做滑动平均。我建了一个长度为 10 的环形缓冲区,每次新数据进来替换最旧的,然后求平均。这样显示出来的数值稳定,不会一直跳。
#define VOC_WINDOW 10 uint16_t vocBuf[VOC_WINDOW]; uint8_t vocIdx = 0; uint32_t vocSum = 0; uint16_t vocAvg(uint16_t newVal) { vocSum -= vocBuf[vocIdx]; vocBuf[vocIdx] = newVal; vocSum += newVal; vocIdx = (vocIdx + 1) % VOC_WINDOW; return vocSum / VOC_WINDOW; }注意:滑动平均的窗口大小要权衡,窗口太大响应慢,太小又起不到平滑作用。10 次在 1s 采样间隔下就是 10s 的平滑窗口,实测比较合适。
4. 常见问题与排查技巧实录
4.1 I2C 设备扫描不到怎么办
这是最常见的问题。先确认供电,用万用表量器件 VCC 和 GND 之间是不是 3.3V。然后确认 SDA 和 SCL 没有接反,很多模块的丝印方向不一致。再检查地址,SHT40 是 0x44,SGP40 是 0x59,LSM6DSO 是 0x6A,SH1107 是 0x3C,如果扫描到其他地址说明器件型号不对。
如果都正常还是扫不到,试试降低 I2C 速率。UNO Q 默认 100kHz,有些模块在 400kHz 下不稳定。在Wire.begin()之后调用Wire.setClock(100000)强制降到 100kHz。
4.2 SGP40 读数一直偏高或偏低
SGP40 需要预热,刚上电的读数不可信,等 1 分钟后再看。如果预热后还是偏高,检查湿度补偿有没有做。补偿参数不对会导致 VOC 指数整体偏移。另外 SGP40 对酒精、香水、油烟非常敏感,如果你在刚喷过酒精的桌面上测试,读数会爆表,这是正常的。
如果读数一直很低不动,可能是加热器没工作。SGP40 的加热器由内部寄存器控制,正常测量命令会自动开启,但如果 I2C 通信有丢包,加热器可能没启动。重新上电再试。
4.3 SH1107 显示偏移或花屏
偏移两列是 SH1107 和 SSD1306 的显存寻址差异导致的,换用 U8g2 的 SH1107 构造器就能解决。花屏通常是供电问题,OLED 刷新时电流突增导致电压跌落,在 VCC 和 GND 之间并电容能改善。如果花屏伴随复位,检查 UNO Q 的 3.3V 输出电流是否足够,必要时用外部 3.3V 稳压供电。
4.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 扫描不到任何设备 | 供电或接线问题 | 量电压、检查 SDA/SCL |
| 只扫到部分设备 | 某器件地址冲突或损坏 | 单独测试该器件 |
| SHT40 湿度读数 100% | 传感器表面凝露 | 擦干后重新上电 |
| SGP40 读数不动 | 加热器未启动 | 重新上电或检查 I2C |
| OLED 偏移两列 | 用了 SSD1306 库 | 换 U8g2 SH1107 构造器 |
| OLED 花屏 | 供电波动 | 加 100nF 和 10uF 电容 |
| LSM6DSO 读数全零 | 未使能传感器 | 写 CTRL1_XL 和 CTRL2_G |
提示:排查 I2C 问题时,先用最小系统测试,只接一颗传感器加 OLED,跑通后再逐个加器件。这样能快速定位是哪颗器件或哪段接线引入的问题。
4.5 几个我踩过的坑和独家技巧
第一个坑是 SHT40 的 CRC 校验。一开始我图省事跳过 CRC,结果在电机旁边测试时偶尔读到离谱的温湿度值,加上 CRC 校验后就没再出现。第二个坑是 SGP40 的预热时间,官方文档写 10 秒,但实测在室温较低时 1 分钟才稳定,所以我的代码里上电后先等 60 秒再开始显示 VOC。
独家技巧方面,我建议在 OLED 上留一个状态指示,比如右上角显示一个小圆点,每次成功读取所有传感器就闪烁一下。这样一眼就能看出系统是否在正常工作,不用连串口。另外,把 I2C 扫描结果在 OLED 上显示出来,调试时非常方便,不用开电脑就能确认设备在线情况。
这个 SENSE v0.1 目前跑得很稳,连续运行 24 小时没有丢数据。后续还可以扩展,比如加 SD 卡做数据记录,或者加无线模块做远程上报。但那是 v0.2 的事了,先把这一版玩透再说。