这是本次测评的最后一篇。前两篇分别做了开箱环境搭建、以及硬件资源梳理和代码框架,这篇把采集端真正跑起来,并记录调试过程中遇到的几个问题。

一、系统构成
1.1 传感器选型
| BH1750 | 光照度 | I2C | 0x23 |
| BMP280 | 气压 / 温度 / 海拔 | I2C | 0x76 |
| DHT22 | 温度 / 湿度 | 单总线 | GPIO4 |
| MQ-2 | 可燃气体 | ADC | GPIO42 |
| ST7789 屏 | 本地显示 | SPI | GPIO35~40 |
选型时特意让四路覆盖三种接口类型:I2C 是数字传感器最常见的接法,可以一条总线挂多个;单总线只有一根数据线,时序全靠软件配合;MQ-2 输出模拟量,走 ADC,能顺带验证一下 S31 的模拟前端。
1.2 接线
J2 排针上 3V3 只有两个、5V 只有两个,DHT22 和屏就把 3V3 用完了。所以把面包板当配电盘用:
板子 3V3 ──→ 上轨 +
板子 G ──→ 上轨 −
(跳线把上下两轨连起来)
面包板的上下两条电源轨是独立的,不加这两根跳线的话下半边没电,这是接的时候容易忽略的地方。
信号线没走面包板,直接用杜邦线连板子。少两个接触点,出问题好查。
需要注意的是线材数量。板子到面包板一共要连 7 条(3V3、G、5V、47、48、42、4),公对公的跳线很快就不够用了。后来的做法是:模块尽量直接插在面包板上,模块侧的连接就全是面包板内部的事;另外把 MQ-2 的 VCC 和 GND 改成从板子直连,腾出两根公对母给 BH1750 用。
1.3 MQ-2 的分压
MQ-2 加热丝要求 5V,AO 输出最高接近供电电压,直接进 ADC 会超量程,所以用两个电阻分压:
MQ-2 AO ──[R1]──┬── GPIO42
│
[R2]
│
GND
R1/R2 取 10k 和 20k,分压比 2/3,5V 进来是 3.33V。

面包板上插这两个电阻有个讲究:电阻要横跨不同的列,但要共用中间那一列。因为面包板的孔是按列连通的(一列五个孔上下相通,列与列之间不通),R1 的右脚和 R2 的左脚落在同一列就自动接上了,不需要额外跳线。
两个容易出错的地方:电阻不能两只脚插同一列,那样会被列内连通短路掉;也不能跨中间那条凹槽,凹槽把面包板分成上下两组独立的孔,跨过去是不通的。
二、软件
代码按模块拆开:
| board_config.h | 所有引脚定义 |
| i2c_bus.c/h | I2C 初始化与地址扫描 |
| bh1750 / bmp280 / dht22 / mq2 | 各传感器驱动 |
| st7789_disp.c/h | 显示屏,含 5x7 点阵字模 |
| main.c | 采集调度、显示刷新 |
引脚集中在 board_config.h 里,改接线的时候只动这一个文件。
主循环里每个传感器独立采集、独立失败:
if (s_status.dht22_ok) {
/* 读失败就打警告,继续跑 */
} else {
ESP_LOGI(TAG, "[DHT22 ] n/a");
}
没接的模块打 n/a 而不是反复重试,启动时探测一次就记住状态。这样调试时能接一个测一个,出问题范围小。
三、调试记录
四路跑通一共踩了五个问题,按遇到的顺序记下来。
3.1 BMP280 气压正常,温度不对
气压读出来 1014.60 hPa,和当地天气对得上,但温度显示 0.27℃,而同一房间的 DHT22 是 26 度。
气压对、温度错,这个组合本身就说明了问题在哪。BMP280 的温度补偿和气压补偿是两个函数,气压补偿用的是温度补偿算出来的 t_fine,不经过温度的最终换算。所以错误只能在温度那一步:
T = (*t_fine_out) / 5120.0; /* T 是摄氏度,比如 26.53 */
return (int32_t)T; /* 这里截断成了 26 */
而调用方按 0.01℃ 解释返回值:
out->temperature_c = (float)temp_centi / 100.0f; /* 26 / 100 = 0.26 */
返回值和调用方的单位约定不一致,改成 return (int32_t)(T * 100.0) 就对了。
改之前串口打印 T=0.27 C,改之后是 T=28.03 C,和 DHT22 的 27 度能对上。
3.2 MQ-2 通道配不起来
屏上 MQ-2 那行一直显示 N/A,但模块的电源灯是亮的,说明供电没问题。看启动日志:
E (802) adc_oneshot: adc_oneshot_config_channel(199): invalid attenuation
E (808) mq2: config channel failed: ESP_ERR_INVALID_ARG
W (813) gateway: MQ-2 not available
查 adc_oneshot_config_channel 的实现,第 199 行有这么个检查:
ESP_RETURN_ON_FALSE(config->atten < SOC_ADC_ATTEN_NUM, ...);
S31 的 SOC_ADC_ATTEN_NUM 是 1,也就是 atten 只能传 0。而 adc_atten_t 枚举里 DB_0=0、DB_2_5=1、DB_6=2、DB_12=3,我填的是 DB_12,直接返回错误。
改成 ADC_ATTEN_DB_0 就好了。
这里我自己走了个弯路。之前查代码时看到 adc_oneshot_ll_set_atten() 的函数体是 (void)atten,参数被硬件忽略,就以为填什么值都无所谓。实际上运行时确实忽略,但配置阶段会做范围校验,填错值连通道都配不上。查了一半就下结论,代价是多花了一轮排查。
3.3 改了代码没反应
修完温度问题重新编译,设备跑起来温度还是 0.27。当时以为是修复没生效,看启动日志才发现:
I (206) boot: compile time Sep 24 2026 12:50:24
I (373) app_init: Compile time: Sep 24 2026 12:47:34
编译时间是 9 月 24 号,而当时是 10 月 5 号。烧进去的是十天前的固件。
原因是习惯在 VSCode 里点烧录按钮,IDE 扩展用的构建目录和命令行不是同一个。后来改用命令行烧录就正常了。
日志里的编译时间戳可以直接用来确认烧的是不是最新那版,比怀疑代码快。
3.4 显示屏
驱动用的是 esp_lcd 框架,ST7789 的驱动在 IDF 本体里就有,不需要额外拉组件。
遇到的第一个问题是字符横向拉伸,看着像乱码。原因是我画字符时用了二维数组当缓冲区:
uint16_t buf[FONT_H * 4][FONT_W * 4]; /* [28][20],每行 20 个元素 */ esp_lcd_panel_draw_bitmap(..., (uint16_t *) buf); /* 按 cw=10 的宽度去读 */
draw_bitmap 要的是连续的 cw × chh 个像素,二维数组每行有 20 个,多出来的 10 个被当成同一行的像素读了,整幅字就错位。
改成按实际宽度排布的一维数组:
uint16_t buf[FONT_H * 4 * FONT_W * 4]; buf[(r * scale + sy) * cw + (c * scale + sx)] = color; /* stride 用 cw */
这个错误编译期查不出来,两边类型都是 uint16_t*,只能看实际效果。
第二个问题是画面转了 90 度。ST7789 芯片原生的帧缓冲是 240×320,而 3.2 寸模块的物理长边是 320,需要用 swap_xy 把坐标轴换过来。换完之后还有镜像,再调 mirror 的两个参数。
调这个的时候我把三个开关做成了宏,写在 board_config.h 里:
#define BOARD_LCD_SWAP_XY 1 #define BOARD_LCD_MIRROR_X 0 #define BOARD_LCD_MIRROR_Y 1
对着现象改比较快:整体转 90 度就改 swap,左右反了改 mx,上下反了改 my,这三个是独立的。

3.5 BH1750 损伤
焊接排针时不小心焊掉了一颗电容,旁边那颗电容的两个焊盘还被锡连在了一起。
先量 VCC 和 GND,蜂鸣档不响,说明不是直接短路。然后串了一个 100Ω 电阻限流再上电——BH1750 工作电流只有 0.12mA,串 100Ω 压降 0.012V 不影响工作,但万一内部短路电流也只有 33mA,烧不了板子。
结果扫描正常,读到的数据也对。看来掉一颗去耦电容不影响使用。

四、实测数据

[DHT22 ] T=27.2 C RH=59.7 % [BH1750] lux=15.8 [BMP280] T=30.03 C P=1015.45 hPa alt=-18.30 m [MQ-2 ] raw=2080 ratio=0.0159
BMP280 的温度比 DHT22 高大约 2 度。刚上电时差 3.1 度,过十几分钟热平衡之后收敛到 2 度左右,是芯片自发热和位置差异造成的。
MQ-2 在预热期间读数会持续往上爬,涨到 2084 左右才平稳。加热丝温度升高,SnO2 敏感层内阻下降,分压点电压跟着上升,读数变大。

4.1 响应测试
光照:拿手机手电筒照 BH1750,读数从 15.8 变化到675.8。遮住之后又降回去。数字和模拟两路都能跟着环境变,这一路是通的。
MQ-2:手边没有标准气体源,对着传感器吹了口气,读数从 2084 降到 2042 左右,幅度几十。
这个方向有点反直觉——如果有可燃气体,应该是 Rs 下降、分压点电压上升、读数变大才对。查了 MQ-2 的资料,它的敏感层工作温度在 300℃ 左右,吹气会带走热量,短时间内降温效应盖过了化学反应,所以看到的是下降。
这个现象也说明器件对气流很敏感,实际部署时要考虑加防风罩。没有标准气体源没法做定量测试,只能记下现象。
关于浓度换算:常规做法是算 Rs/R0,需要准确的 AO 电压和洁净空气里的 R0 标定。S31 的 ADC 目前只有一档衰减,而且曲线拟合校准还没实现(adc_cali_schemes.h 里是注释掉的状态,标着 TODO),电压换算不可靠。所以代码里只输出原始值和归一化比值,用来做相对比较,不换算成物理量。
五、以太网与网页服务
5.1 链路
板载的是 YT8531,RGMII 接口,控制引脚 MDC/MDIO/RST 分别是 GPIO5/6/7。实测结果:
PHY : YT8531 (RGMII)
Speed : 1000 Mbps, full duplex
IP / 网关 : 192.168.31.28 / 192.168.31.1
上电→Link Up : 约 2.8 秒
Link Up→IP : 约 4.8 秒
协商到了千兆全双工,说明 RGMII 的四对数据线时序是对的。同价位的板子不少还是百兆,这一条算是这块板子比较突出的地方。
5.2 PHY 的两个非标准行为
日志里有两行值得记下来:
auto-negotiation re-enabled
RGMII delay set: RX ~2ns (coarse), TX ~2ns (13 x 150ps)
自行关闭自动协商——这是它没写在标准流程里的行为,不重新开启的话链路根本起不来。
第二行:RGMII 的收发时钟需要配置内部延迟,不配的话链路能建立但收发的数据是错的。日志里 TX 配的是 13 × 150ps ≈ 1.95ns。
这两点都不是标准 RGMII 流程里会遇到的东西,属于这颗 PHY 的脾气。好在 IDF 的以太网例程里已经封好了。
5.3 网页服务
既然板子有千兆网口,就把数据送到浏览器上看,比只盯着串口直观。
在 gateway 里加了两个模块:net_eth(从之前的链路验证工程移植过来)和 web_server(HTTP 服务端)。后者提供两个路由:
/ 返回一个单页
/api/data 返回当前读数的 JSON
页面每 2 秒拉一次数据,四路读数加上底部的网络状态一起刷新:

前端是自包含的——CSS 和 JS 全部内联,不引用任何 CDN。这样网关在没外网的局域网里也能正常打开,不会因为拉不到外部资源而白屏。
采集任务和 HTTP 任务之间用一份快照结构体交换数据,没有加锁。偶尔可能读到半新半旧的一组值,对网页显示来说无所谓,不值得为它引入互斥锁。
加了这两块之后固件从 292KB 涨到 532KB,app 分区还剩 49%,空间还够。

六、总结
整个测评做下来,S31 这块板子和预览版的 IDF 用起来有几点感受。
硬件方面,J2 排针引出得比较全,常用的 GPIO 都能接到。受限的地方是两个电源引脚数量少,接多路外设的时候必须做电源分配。片上的 RGB 灯在预览版上驱动不完整(RMT 相关文件缺失),只能当电源指示用。
软件方面,预览版 IDF 的组件完整度参差不齐。以太网、SPI、I2C 和 HTTP 服务端都实测跑通了;WiFi 6 的驱动编译通过,但没有做实际的连接测试;RMT 在 soc 层缺文件,DHT22 这类靠时序的器件得用 GPIO 位操作自己实现;ADC 只有一档衰减且参数被忽略,校准也没有,只能读相对值。这些在正式版应该会补齐。
回头看这篇记录里的五个问题,有三个是真正影响功能的:温度单位、ADC 衰减、固件没更新。它们的共同点是编译都能过,跑起来才暴露。剩下两个是显示和硬件本身的问题。写下来给后来用这块板子的人参考。
我要赚赏金
