
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 的六轴 IMUI2C/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 或 0x6BSHT40 固定 0x44SGP40 固定 0x59SH1107 常见 0x3C 或 0x3D。实际接线前先用 I2C 扫描确认一遍能省掉大量“为什么读不到数据”的排查时间。供电方面所有器件都是 3.3V 逻辑UNO Q 的 3.3V 输出电流足够驱动它们但 OLED 刷新时电流波动较大建议在 OLED 的 VCC 和 GND 之间并一颗 100nF 加一颗 10uF 电容实测能明显减少花屏和复位现象。2. 核心细节解析与实操要点2.1 SHT40 的 I2C 地址与读取时序SHT40 的 I2C 地址是固定的0x447 位地址不可以通过引脚改变。这一点和很多传感器不同所以总线上只能挂一颗 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 的排线尽量短减少干扰。具体接线如下表器件VCCGNDSDASCL备注LSM6DSO3.3VGNDA4A5地址 0x6ASHT403.3VGNDA4A5地址 0x44SGP403.3VGNDA4A5地址 0x59SH11073.3VGNDA4A5地址 0x3C所有 SDA 接 A4SCL 接 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 读一次 SGP40OLED 每 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 指数第四行加速度值。字体用 6x10128x128 的屏能放下。如果屏是 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 是 0x44SGP40 是 0x59LSM6DSO 是 0x6ASH1107 是 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 读数不动加热器未启动重新上电或检查 I2COLED 偏移两列用了 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 的事了先把这一版玩透再说。