
最近把 Analog Devices 的 ADT7410 温度传感器和 Adafruit PyPortal 开发板凑在了一起用 CircuitPython 写了一套 IoT 数据记录器。做出来的效果是桌面放一个 3.2 英寸小屏幕实时显示当前室温同时数据通过 Adafruit IO 上传到云端网页端自动生成温度曲线手机也能看历史趋势。如果你正在计划做类似的环境监测小项目或者想把一个 I2C 传感器接入 Adafruit IO这套思路可以直接套用。这篇文章把我从硬件选型、电路接线、平台配置到代码实现整个过程捋了一遍也把我在实际操作中踩过的几个坑单独列了一节。内容比较适合已经跑过一点 CircuitPython 基础例程、但对完整数据链路还不太熟的朋友如果你只是对 ADT7410 本身感兴趣也能从硬件章节获得不少细节。1. 项目概述与整体设计思路1.1 这个项目到底做了什么项目核心是完成一条完整的数据链路ADT7410 温度传感器通过 I2C 总线把温度数据交给 PyPortal 开发板PyPortal 上的 CircuitPython 程序读取温度后一边在屏幕上渲染大字温度数值一边通过板载 ESP32 协处理器连接 WiFi把数据发布到 Adafruit IO 的指定 Feed。Adafruit IO 侧创建了 Dashboard 和 Line Chart Block温度值上传后会实时画出一条曲线。放在桌面场景里它就是一个能看“现在温度”和“过去一段趋势”的小型环境气象站。因为 Adafruit IO 免费版已经支持数据保存和图表渲染所以我不需要自己搭 Web 服务器也不需要数据库从硬件到云端可视化的成本被压得非常低。整个项目从零件到能跑差不多一个下午能完成。1.2 为什么选 ADT7410 而不是 DHT22、BME280很多朋友看到“温度数据记录器”第一反应是手边那颗 DHT11/DHT22或者 BME280。DHT22 的精度标称 ±0.5°C日常看室温完全够用但它走的是类单总线协议时序要求偏严格在 CircuitPython 这种解释型环境里虽然也能用可程序设计自由度不高也不能共享 I2C 总线。BME280 确实更全面温度、湿度、气压都有但单纯从“温度记录”这个目标看它的温度精度通常是 ±1.0°C比 ADT7410 的 ±0.5°C 规格差一个档次。ADT7410 最大的优势是 16 位数字输出没有模拟前端校准的问题数据直接从寄存器读出来就是摄氏度。I2C 接口对 CircuitPython 也特别友好库很成熟初始化几行代码就能跑。再加上它可以配置出四个不同 I2C 地址一条总线上并几颗传感器监控多点温度也方便。这个设计用在这里是典型的“项目需求决定选型”不是单纯追求指标。1.3 整体架构传感器 → PyPortal → Adafruit IO画一下数据流向ADT7410 通过 STEMMA QT 线接到 PyPortal 的 I2C 排座供电 3.3VSDA 和 SCL 各自独立。PyPortal 运行 CircuitPython主控是 ATSAMD51WiFi 功能由板载 ESP32 协处理器完成两者之间走 SPI。CircuitPython 程序读取 ADT7410 的温度值处理后推送到 io.adafruit.com 的 MQTT Broker。Adafruit IO 平台保存数据点Dashboard 里的 Line Chart 自动绘图网页上能看到实时数值和历史曲线。同一份温度数据还会更新 PyPortal 屏幕上的 Label 文本本地显示和云端同步进行。这套架构最舒服的地方在于所有环节都有 Adafruit 官方或社区现成方案省去了很多底层协议的折腾。整条链路里最容易出问题的是 WiFi 连接和 MQTT 断线重连我在后面的章节会专门说。2. 硬件选型与 ADT7410 核心知识2.1 物料清单这个项目用到的硬件不多核心三样硬件型号/规格作用主控开发板Adafruit PyPortal运行 CircuitPython负责屏幕显示和网络上传温度传感器Adafruit ADT7410 Breakout采集温度I2C 输出连接线STEMMA QT 转 STEMMA QT 或杜邦线连接 PyPortal 与传感器PyPortal 自带 3.2 英寸 TFT 屏幕、扬声器、Neopixel 指示灯和 STEMMA QT 接口基本就是为桌面 IoT 小工具设计的。ADT7410 Breakout 板上已经集成了上拉电阻和去耦电容不需要额外外围电路用 STEMMA QT 线一插就完成接线。电源用一个普通的手机充电头和 Micro USB 线就能搞定PyPortal 整板功耗不高。2.2 ADT7410 关键参数解读这颗芯片来自 Analog Devices数字温度传感器里的老牌型号。几个关键参数值得拿出来解释分辨率16 位模式下的温度分辨率约 0.0078°C这个数值不是“绝对精度”只是机器能分辨的最小温度步进。实际测量精度受芯片规格限制典型 ±0.5°C。量程-40°C 到 105°C覆盖常见环境监测场景没有压力。接口标准 I2C支持 SMBus 兼容模式传输速率 100kHz 和 400kHz 都行CircuitPython 默认初始化通常没问题。输出格式温度数据存在 16 位寄存器里实际有效的温度数据是 13 位低 3 位为 0。换算时把整个 16 位值看作有符号数除以 128 就得到摄氏度。很多第一次接触 ADT7410 的人会困惑温度值到底怎么算。我们看一个真实样例寄存器读回 0x0C80十六进制转十进制是 32003200 / 128 25.0°C。如果是负数环境温度读数会以二进制补码形式出现比如 -10°C 对应 -1280 / 128 -10.0°CCircuitPython 的库已经处理了符号扩展直接读adt.temperature就行。2.3 I2C 地址与接线方式ADT7410 的 I2C 地址由芯片的 SDO/AD0 引脚决定。默认情况下 SDO 接地地址是 0x48。Adafruit Breakout 板上通常有地址选择焊盘可以把地址改成其他值SDO/AD0 状态I2C 地址接地默认0x48接高电平0x49接 SDA0x4A接 SCL0x4B接线时只需要四根线VIN 接 3.3V、GND 接 GND、SDA 接 PyPortal 的 SDA、SCL 接 PyPortal 的 SCL。PyPortal 板载 STEMMA QT 接口本身就是标准 I2C 排针用成品线直接插省心不少。我实际测试时发现如果使用杜邦线手工连接尽量让 SDA 和 SCL 两根线短一些避免在数据线上引入干扰导致 I2C 偶发失败。2.4 为什么不用分析传感器内部结构对于这种数字芯片你不需要知道内部 ADC 怎么做滤波、基准怎么产生只需要把它当做一个“能通过 I2C 读温度的器件”来用。不过了解配置寄存器会有帮助。ADT7410 的寄存器 0x03 是配置寄存器可以设置运行模式连续转换模式芯片一直测量数据实时更新适合我们这个场景。1 次/秒模式降低功耗但读数更新慢。单次转换模式读取一次触发一次测量适合低功耗应用。Adafruit 的 CircuitPython 库里封装了模式常量代码里直接设置adt.mode adafruit_adt7410.ADT7410_MODE_CONTINUOUS就能让传感器连续工作。我喜欢把电路连接顺序固定成“先接电源再接 I2C 信号”的习惯其实对这套系统影响不大但能防止带电插拔时烧坏 SDA/SCL 引脚。3. Adafruit IO 平台配置Feed、Dashboard 与 Key3.1 注册账号与创建 FeedAdafruit IO 是 Adafruit 官方的 IoT 数据平台免费版适合小数据量项目。登录 io.adafruit.com 后右上角 Actions 里选择 Create New Feed。Feed 本质是一个数据通道相当于一个带名字的变量。我创建了一个名为temperature的 FeedUpload/Download 都是它。创建完之后Feed 右侧能看到它的 Key通常是一串带下划线的字符串比如temperature。这个 Key 会出现在 MQTT Topic 里也会在 HTTP API URL 中出现。如果之后要自己在代码里拼接 REST 接口记住 Feed Key 不能乱改。3.2 创建 Dashboard 与 Line Chart BlockFeed 只负责保存数据要看曲线还要建 Dashboard。操作路径在 Adafruit IO 页面顶部选 Dashboards点击 Create New Dashboard起个名字。建完后进到这个 Dashboard点 Add Block选择 Line Chart然后把temperatureFeed 关联进去。图表上还能配置最小刻度、最大刻度、刷新间隔一般保持默认就够。一个很实用的细节是 Line Chart Block 支持多条数据叠加这意味着以后如果加了湿度 Feed就可以在同一张图上对比温湿度曲线不需要重新建 Dashboard。Dashboard 本身只要把 URL 分享出去不用登录也能看用来做公共展示非常方便。3.3 获取 AIO Key 以及安全存放代码里要使用用户名和“AIO Key”进行身份认证。AIO Key 获取方式在 io.adafruit.com 页面右侧点击 View AIO Key弹出的对话框里有一行 Active Key复制这一长串。这套认证信息不能硬编码在代码中。CircuitPython 9.x 以后推荐使用板载settings.toml文件保存配置。我的settings.toml长这样CIRCUITPY_WIFI_SSID 你的WiFi名称 CIRCUITPY_WIFI_PASSWORD 你的WiFi密码 AIO_USERNAME 你的Adafruit IO用户名 AIO_KEY 你的Adafruit IO激活Keysettings.toml放在 CIRCUITPY 盘根目录代码里通过os.getenv读取。好处是换 WiFi 或换账号时不用动代码只改配置文件。把项目传到 Git 前记得忽略settings.toml毕竟里面是明文密码。3.4 免费版限制与上传频率选择Adafruit IO 免费版对数据速率限制比较严格公开的说明是每分钟最多 30 条数据数据保留期一般为 30 天。虽然是“每分钟 30 条”但为了保险以及避免 Dashboard 曲线密集得看不清楚我建议把上传间隔设成 30 秒或更长。30 秒一条一天 2880 条数据免费账户毫无压力曲线也足够顺滑。如果做高温室或者需要秒级变化监测的场景再考虑缩短到 15 秒左右。4. CircuitPython 代码实现传感器读取、数据上传与屏幕显示4.1 固件与依赖库准备PyPortal 出厂通常预装 CircuitPython不过版本可能比较旧。建议去 CircuitPython 官网下载 PyPortal 对应版本的最新固件拖到 BOOTLOADER 模式下的 CIRCUITPY 盘即可完成升级。光有固件还不够还要把用到的第三方库放到 LIB 目录。最省心的方式是从 Adafruit CircuitPython Bundle 下载最新 bundle然后抽出以下内容复制到板子adafruit_adt7410.mpyadafruit_minimqtt整个文件夹adafruit_io整个文件夹adafruit_esp32spi整个文件夹adafruit_requests.mpyadafruit_display_text整个文件夹如果你需要用 HTTP 方式代替 MQTTadafruit_requests是必备的。如果只跑 MQTT也建议装上 requests调试和测试 REST 接口时会比较方便。4.2 完整的核心代码下面是我实测跑通的完整代码。你可以直接保存成code.py放在 CIRCUITPY 盘根目录。import os import time import board import busio import digitalio import displayio import terminalio from adafruit_display_text import label import adafruit_adt7410 import adafruit_minimqtt.adafruit_minimqtt as MQTT from adafruit_io.adafruit_io import IO_MQTT import adafruit_esp32spi.adafruit_esp32spi as esp32spi import adafruit_esp32spi.adafruit_esp32spi_wifimanager as wifimanager # ---------- 读取配置 ---------- ssid os.getenv(CIRCUITPY_WIFI_SSID) password os.getenv(CIRCUITPY_WIFI_PASSWORD) aio_username os.getenv(AIO_USERNAME) aio_key os.getenv(AIO_KEY) FEED temperature PUBLISH_INTERVAL 30 # ---------- 初始化 ESP32 WiFi ---------- esp32_cs digitalio.DigitalInOut(board.ESP_CS) esp32_ready digitalio.DigitalInOut(board.ESP_BUSY) esp32_reset digitalio.DigitalInOut(board.ESP_RESET) spi busio.SPI(board.SCK, board.MOSI, board.MISO) esp esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) wifi wifimanager.ESPSPI_WiFiManager(esp, {ssid: ssid, password: password}) # ---------- 初始化 ADT7410 ---------- i2c busio.I2C(board.SCL, board.SDA) adt adafruit_adt7410.ADT7410(i2c, address0x48) adt.mode adafruit_adt7410.ADT7410_MODE_CONTINUOUS # ---------- 初始化屏幕 ---------- display board.DISPLAY splash displayio.Group() display.show(splash) bg_bitmap displayio.Bitmap(320, 240, 1) bg_palette displayio.Palette(1) bg_palette[0] 0x101010 splash.append(displayio.TileGrid(bg_bitmap, pixel_shaderbg_palette)) title_label label.Label(terminalio.FONT, textRoom Temperature, color0xCCCCCC, x20, y28) value_label label.Label(terminalio.FONT, text--.- C, color0xFFD600, x20, y110, scale3) state_label label.Label(terminalio.FONT, textbooting, color0x888888, x20, y210) splash.append(title_label) splash.append(value_label) splash.append(state_label) # ---------- Adafruit IO MQTT 回调 ---------- def connected(client): print(Connected to Adafruit IO) state_label.text connected def disconnected(client): print(Disconnected from Adafruit IO) state_label.text disconnected mqtt_client MQTT.MQTT( brokerio.adafruit.com, port1883, usernameaio_username, passwordaio_key, ) io_client IO_MQTT(mqtt_client, aio_username, aio_key) io_client.on_connect connected io_client.on_disconnect disconnected def moving_average(samples): samples sorted(samples) samples samples[1:-1] return sum(samples) / len(samples) # ---------- 主循环 ---------- print(Connecting to WiFi...) state_label.text wifi wifi.connect() print(Connecting to Adafruit IO...) state_label.text mqtt try: io_client.connect() except Exception as e: print(MQTT connect error:, e) state_label.text net error last_publish 0 while True: io_client.loop(timeout1) raw_samples [] for _ in range(7): raw_samples.append(adt.temperature) time.sleep(0.1) temp_c moving_average(raw_samples) value_label.text f{temp_c:.1f} C now time.monotonic() if now - last_publish PUBLISH_INTERVAL: try: io_client.publish(FEED, f{temp_c:.2f}) last_publish now state_label.text published except Exception as e: print(Publish error:, e) state_label.text pub failed这段代码同时处理了本地显示、数据上报、WiFi 连接。运行之后你会看到屏幕背景变成深灰顶部标题显示 Room Temperature中间的大号字体是当前温度底部状态栏会在 wifi、mqtt、connected、published 之间切换。4.3 关键逻辑解释传感器读取为什么加滤波adt.temperature能直接读到摄氏度但在 16 位模式下最后一两位数据会有些跳变。10 次独立读数可能相差 0.008°C 到 0.02°C看起来有点“抖动”。从参数表看这个抖动完全正常但显示在小屏幕上会让人以为传感器不稳定。我的处理方式是连续采集 7 次去掉最大值和最小值再对中间 5 个数求平均也就是一维排序滤波。这样既滤掉了偶发毛刺又不会像滑动平均那样把真实温度变化拖得太平。反正 ADT7410 读取一次非常快7 次采样加 0.1 秒间隔也就 0.6 秒不影响上传节奏。4.4 MQTT 上传为什么用 1883 端口Adafruit IO 的 MQTT Broker 支持两种连接方式端口 1883 是明文 MQTT端口 8883 是带 TLS 加密的 MQTT。CircuitPython 也支持 TLS但需要额外的 SSL Context 配置且在低配置环境里更容易出证书问题。家里或实验室这种使用场景数据内容只是温度值无敏感信息用 1883 端口最省事。如果做商业产品或者跨公网传输敏感数据建议升级到 8883 端口并配置 TLS。Adafruit 官方文档里有对应的 CircuitPython 示例照着改MQTT.MQTT构造参数和ssl_context相关代码就行。4.5 HTTP REST 备选方案如果你不想引入 MQTT也可以用 HTTP 方式直接把温度 POST 到 Adafruit IO REST API。这种方式思路更直白适合只做定时上报、不需要长期保持连接的场景。核心调用长这样import adafruit_requests requests adafruit_requests.Session(wifi, debugFalse) url fhttps://io.adafruit.com/api/v2/{aio_username}/feeds/{FEED}/data headers {X-AIO-KEY: aio_key, Content-Type: application/json} payload f{{value: {temp_c:.2f}}} resp requests.post(url, headersheaders, datapayload) resp.close()HTTP 方式的好处是逻辑简单每次请求独立不怕断线重连。缺点是每次握手有一定能耗且稍微慢一点。对于锂电池供电的设备HTTP 更合适因为可以发完立刻休眠对于常供电的桌面设备MQTT 更方便因为它可以保持长连接适合未来增加下行控制消息。5. 踩坑实录I2C、MQTT、显示与数据噪声问题排查5.1 I2C 扫描不到 ADT7410我在第一次接好线后code.py直接报I2C bus error。排查步骤如下用i2c.try_lock()后调用i2c.scan()扫描总线上的设备地址看能不能看到0x48。检查是否真的把 SDA 接到了 SDASCL 接到了 SCL。STEMMA 线颜色是固定规范黑色 GND、红色 3V、蓝色 SDA、黄色 SCL但市面上第三方线材可能存在颜色差异不能依赖颜色。检查是否先上电再接传感器。总线上只有一个设备时如果供电没接好扫描不到地址是很正常的。ADT7410 的地址如果不是默认的 0x48比如之前焊过地址跳线那就得改成 0x49 试试。扫描代码可以直接用 REPL 跑import board, busio i2c busio.I2C(board.SCL, board.SDA) i2c.try_lock() print([hex(x) for x in i2c.scan()]) i2c.unlock()5.2 MQTT 连接成功但发布没有反应一个典型症状是屏幕显示 connected但 Adafruit IO 的 Feed 里看不到新数据。最常见的原因是主循环里没有及时调用io_client.loop(timeout1)或者循环被time.sleep()阻塞太长时间。CircuitPython 的 MQTT 客户端需要自己轮询后台消息尤其要处理 PING 维持心跳。我的主循环每次迭代都先调io_client.loop(timeout1)它最多阻塞 1 秒正好当作整个循环的节拍。另一个隐藏点io_client.publish()之后如果立即再发布第二次需要留出网络处理时间。所以我用PUBLISH_INTERVAL 30做时间间隔确保每次发布之间有充足时间。5.3 WiFi 长时间运行后掉线PyPortal 的 ESP32 协处理器使用 SPI 与主控通信长时间运行偶尔会出现 WiFi 状态异常。现象是屏幕还亮着但上传中断控制台打印Connection refused或者RuntimeError: Failed to connect。我的处理策略是给 MQTT 断线回调加自动重连逻辑。在disconnected(client)回调里直接刷新状态显示主循环每次检测到io_client.loop()返回非正常状态时尝试重新调用io_client.connect()。如果 WiFi 链路本身断了需要先重新wifi.connect()再重连 MQTT。在代码里增加一个简单的异常处理就能让设备自动恢复。5.4 温度值看起来偏高或偏低如果读到的温度总是明显偏高先检查传感器是不是紧挨着 PyPortal 主板或电源适配器。PyPortal 本身的主芯片和背光屏会发热板子附近的温度可能比环境高 23°C。ADT7410 芯片是贴片封装如果紧贴开发板会受到板载热源影响。我的做法是用 STEMMA 延长线把传感器放到离主板十几厘米的桌面上读数才更接近真实室温。如果读到的温度始终低可能是在空调出风口附近或贴近冷源。这类问题不是代码 bug是实验环境导致部署时要注意传感器位置不能靠近明显热源。5.5 屏幕上字体太小或者背景刺眼PyPortal 屏幕是 320x240完全够用。想要大号温度数字把 Label 的scale参数设为 3 或 4 即可