初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,176 @@
# M5Stack Unit ADC v1.1 / ADS1110 开发约定
本文供后续 Codex 或维护者读取。动手写驱动前,先确认实物是 `M5Stack Unit ADC v1.1`、SKU `U013-V11`、ADC 芯片 `ADS1110`。不要把旧版 Unit ADC、ADS1100 库或其他 ADS111x 芯片的寄存器接口混进来。
## 1. 当前项目边界
- 主控:`WalnutPi ZeroW`(H618)
- 目的:测量点阵屏输入端 5V 电压,作为供电压降的数字观测值
- I²C 总线:`/dev/i2c-1`
- 7 位地址:`0x48`
- GPIO:物理脚 3 / `PI8/SDA1`,物理脚 5 / `PI7/SCL1`
- 供电假设:系统已经提供可靠、共地的 `5V/GND`
- 当前交付:本地资料、独立脚本,以及主服务中的电压状态、校准和可选低电压亮度保护
- 明确不做:电流、功率、实际发光亮度识别、BMS、物理断电、充电管理或核桃派关机
测量端是并联采样。屏幕负载电流不得经过模块。v1.1 原理图中 P1 测量负端经 `0Ω` 电阻接模块 GND,因此不能把它当成与核桃派隔离的差分表使用。
## 2. 资料优先级和已知冲突
遇到资料冲突时采用以下优先级:
1. 实物型号、v1.1 原理图和 TI ADS1110 数据手册
2. M5Stack Unit ADC v1.1 产品描述及管脚表
3. M5Stack 产品页中的规格汇总表
4. M5Stack 旧 Arduino 示例和 `M5-ADS1100` 仓库
当前 M5Stack 产品页存在混杂信息:正文把 v1.1 描述为 ADS1110,并给出 `15/30/60/240 SPS`;但部分规格/比较表仍显示 ADS1100 或 `8/16/32/128 SPS`,链接的旧 Arduino 例程也使用 `M5_ADS1100.h`。对 `U013-V11` 必须使用 ADS1110 的 `15/30/60/240 SPS` 位定义,不得照抄旧库常量。
本地官方文件、原始 URL、下载日期和 SHA-256 见 `官方资料/来源索引.md`。
## 3. 板级模拟换算
v1.1 原理图在测量输入端使用 `510kΩ` 与 `100kΩ` 分压,并由 LM358 缓冲后送入 ADS1110。标称分压倍率为:
```text
(510k + 100k) / 100k = 6.1
```
ADS1110 在 PGA=1 时的差分满量程为 `±2.048V`。选择 15 SPS 时输出为 16 位二进制补码,因此:
```text
ADC差分电压 = signed_raw × 2.048 / 32768
屏幕输入电压 = ADC差分电压 × 6.1 × calibration_factor
```
默认 `calibration_factor = 1.0`。屏幕端每 LSB 的标称分辨率为:
```text
2.048 / 32768 × 6.1 = 0.00038125V
```
快速合理性检查:
| 屏幕输入端标称电压 | 预期 raw(约) |
|---:|---:|
| `0V` | `0` |
| `4.5V` | `11803` |
| `5.0V` | `13115` |
| `5.3V` | `13902` |
| `12.0V` | `31475` |
`raw=32767` 对应标称约 `12.4924V`,但这只是数学满量程;模块官方输入范围仍按 `0V ~ 12V`,不能把多出的计算余量当成允许输入范围。
## 4. ADS1110 配置字节
ADS1110 配置寄存器只有 1 字节:
| 位 | 名称 | 本项目值 | 含义 |
|---:|---|---:|---|
| 7 | `ST/DRDY` | 写入时 `0` | 连续模式下写入值被忽略;读取时 `0` 表示本次结果是新数据 |
| 6:5 | 保留 | `00` | 必须为 0 |
| 4 | `SC` | `0` | 连续转换模式 |
| 3:2 | `DR1:DR0` | `11` | 15 SPS、16 位 |
| 1:0 | `PGA1:PGA0` | `00` | PGA=1 |
因此本项目写入:
```text
0b00001100 = 0x0C
```
ADS1110 上电默认配置读值通常为 `0x8C`;位 7 是读取状态,校验配置时只比较低 7 位是否等于 `0x0C`。不要把 ADS1100 的采样率名称套在相同位值上。
## 5. I²C 读写顺序
ADS1110 没有常见的“寄存器地址指针”步骤:
1. 以 7 位地址 `0x48` 打开 I²C 从设备。
2. 写入单个配置字节 `0x0C`。
3. 至少等待一个 15 SPS 转换周期,约 `66.7ms`;实现可留少量余量。
4. 一次读取 3 字节:
- 字节 0:输出寄存器高字节
- 字节 1:输出寄存器低字节
- 字节 2:配置/状态字节
5. 将前两个字节按大端、有符号 16 位二进制补码解析。
6. 检查配置低 7 位仍为 `0x0C`;位 7 为 `0` 时,本次结果为新数据。
不要使用会自动在事务前发送“寄存器编号”的 SMBus 便捷函数,除非确认它能发出 ADS1110 所需的原始事务。当前独立脚本直接操作 `/dev/i2c-1`,避免引入依赖或错误的寄存器前缀。
## 6. 独立脚本接口
文件:`示例程序/read_unit_adc_v1_1.py`
默认行为:
- 总线 `/dev/i2c-1`
- 地址 `0x48`
- 配置 `0x0C`
- 每次取 5 个新样本的中位数
- 输出屏幕输入端电压、raw 中位数和配置字节
常用命令:
```bash
# 本机纯计算/协议自测,不访问 I²C,Windows 也可以运行
python "硬件相关资料和硬件的连接/M5Stack Unit ADC v1.1相关内容/示例程序/read_unit_adc_v1_1.py" --self-test
# 核桃派单次读数
python3 "硬件相关资料和硬件的连接/M5Stack Unit ADC v1.1相关内容/示例程序/read_unit_adc_v1_1.py"
# 连续读数
python3 "硬件相关资料和硬件的连接/M5Stack Unit ADC v1.1相关内容/示例程序/read_unit_adc_v1_1.py" --watch
# 使用一次比例校正
python3 "硬件相关资料和硬件的连接/M5Stack Unit ADC v1.1相关内容/示例程序/read_unit_adc_v1_1.py" --calibration-factor 1.00604 --watch
```
额外参数通过 `--help` 查看。独立工具必须保持 Python 标准库实现并与主服务监测器分开;不能把独立无限循环直接放进 FastAPI 进程,也不能与主服务同时长期读取同一地址。
## 7. 错误与排障语义
后续修改脚本时必须保留可区分的错误:
- `/dev/i2c-1` 不存在:I²C 未启用或设备节点不同。
- 权限不足:用户没有访问 I²C 设备的权限。
- 地址无响应:`0x48` 没有 ACK,优先查接线、供电和型号。
- 地址被占用:内核驱动已经 claim 该地址。
- 短读取:没有取得完整 3 字节,结果不可用。
- 配置低 7 位不是 `0x0C`:设备/协议不符或配置被改变,不能继续按 15 SPS/PGA=1 换算。
- 负 raw:按二进制补码保留并报告,不要在协议层静默截成 0;对本项目的正电压输入,持续负值应视为接线或偏置异常。
- 读数超出 `0V ~ 12V`:报告异常并检查硬件,不能用软件裁剪后假装正常。
`i2cdetect` 只用于首次诊断,不要在长期读取过程中反复扫描总线。
## 8. 主服务集成约束
主服务已经按 `WEB-SCREEN-VOLTAGE` / `CONFIG-SCREEN-VOLTAGE` 接入网页电压显示和每台设备独立的软件校准,并按 `WEB-LOW-VOLTAGE-PROTECTION`、`CONFIG-LOW-VOLTAGE-PROTECTION`、`DISPLAY-LOW-VOLTAGE-PROTECTION` 接入可选保护。实现必须继续满足:
- ADC 缺失或读取失败不能拖垮显示服务。
- 只有一个组件负责持续读取设备,其他组件读取缓存值,避免并发争用 I²C。
- 原始电压、校正系数、采样时间和错误状态要可区分,不能把读取失败伪装成 `0V`。
- 低电压保护阈值来自本项目真实屏幕和供电试验:校准后、未舍入的组中位数严格大于 `4.8V` 不限制,`4.5..4.8V` 使用 `floor(50 + 50 × (V-4.5)/0.3)` 限制实际亮度,严格低于 `4.5V` 进入 35% 低电图标;不能从 ADC 量程另行推导或改写边界。35% 来自覆盖保护膜后的红色可视性复测。
- 电压正常不等于屏幕状态正常;界面文案必须称为“屏幕输入电压”,不能称为“屏幕亮度传感器”。
- 正常轮询与校准专用采样都必须经过同一个监测组件,不能让 API 请求和后台线程并发打开设备。
- 校准使用同一测量点的可靠万用表参考值,只做单点比例修正;更换 ADC、接线或测量点后恢复 `1.0` 并重新校准。
- 每组继续取 `5` 个 15 SPS 新样本中位数。保护关闭或稳定高于 `4.8V` 时组起始间隔为 `5s`;高于阈值但持续下降时按预计到达阈值缩短为 `1..5s`;限亮为 `1s`;首次临界为 `0.5s`,之后按 `clamp(0.5, 2.0, 0.02/max(|dV/dt|, 0.01))` 自适应,放慢每轮最多增加 `0.5s`、加速立即生效;错误按 `1/2/5s` 退避。
- 每次 I²C 尝试使用单调序号并拒绝晚到旧结果。更严格保护一组立即发布,任何放宽需连续两组成功样本;失败取消待放宽,首次尚无样本不锁屏,已有成功样本后保持最后保护并标记 stale。
- 保护回调必须在监测锁和 I²C 锁外执行;回调错误与 ADC 状态分开,不能污染样本或解除保护,并要在后续周期重试。完整网页、配置和显示契约以 `WEB-LOW-VOLTAGE-PROTECTION`、`CONFIG-LOW-VOLTAGE-PROTECTION`、`DISPLAY-LOW-VOLTAGE-PROTECTION` 为准。
## 9. 浏览器救援条件
以下情况才进入浏览器:
- 实物不是 `U013-V11` 或 PCB 修订发生变化。
- 地址不是 `0x48`。
- 本地 PDF 损坏或与实物丝印冲突。
- 需要确认 ADS1110 电气极限、时序或未在本文覆盖的模式。
优先访问官方来源:
- M5Stack:<https://docs.m5stack.com/zh_CN/unit/Unit-ADC_V1.1>
- v1.1 原理图:<https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/576/Sch_UNIT_ADC_v1.1.pdf>
- TI ADS1110:<https://www.ti.com/lit/ds/symlink/ads1110.pdf>
- 核桃派项目约定:使用系统已有的 `/dev/i2c-1`,不运行其他开发板专用配置工具
浏览时使用 `U013-V11 ADS1110` 作为组合关键词,并记录所用来源和日期。论坛、商家详情和旧 `M5-ADS1100` 代码只能作为线索,不能覆盖原理图与 TI 数据手册。
@@ -0,0 +1,11 @@
# M5Stack Unit ADC v1.1 资料入口
本目录只适用于 `M5Stack Unit ADC v1.1`、SKU `U013-V11`、ADC 芯片 `ADS1110`。不要使用旧版 Unit ADC、ADS1100 示例或其他 ADS111x 寄存器协议替代。
- 人工接线与首次上电:[核桃派ZeroW_Unit_ADC接线与首次上电检查.md](核桃派ZeroW_Unit_ADC接线与首次上电检查.md)
- 协议、换算、错误与主服务边界:[02_ADS1110使用与开发约定_给Codex.md](02_ADS1110使用与开发约定_给Codex.md)
- 标准库独立脚本:[示例程序/read_unit_adc_v1_1.py](示例程序/read_unit_adc_v1_1.py)
- 官方资料与 SHA-256:[官方资料/来源索引.md](官方资料/来源索引.md)
固定事实:总线 `/dev/i2c-1`,7 位地址 `0x48`,配置低 7 位 `0x0C`,15 SPS、PGA=1。模块并联测量屏幕输入端电压,不承载屏幕电流。
@@ -0,0 +1,59 @@
# M5Stack Unit ADC v1.1 官方资料索引
下载日期:`2026-07-20`(Asia/Shanghai)
本目录用于离线接线和开发。PDF 均直接从 M5Stack 或 Texas Instruments 官方地址取得,未修改内容。SHA-256 用于以后判断文件是否被误改或重新下载后是否发生变化。
## 本地文件
| 本地文件 | 官方来源 | 页数 | 字节数 | SHA-256 |
|---|---|---:|---:|---|
| [`M5Stack_Unit_ADC_v1.1_产品资料.pdf`](M5Stack_Unit_ADC_v1.1_产品资料.pdf) | [M5Stack 产品 PDF](https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/static/pdf/static/zh_CN/unit/Unit-ADC_V1.1.pdf) | 5 | 2,244,061 | `46B43649CB12D297E0F4D35A7575B76E6D83643838876892B84A8D14CF34E49A` |
| [`M5Stack_Unit_ADC_v1.1_原理图.pdf`](M5Stack_Unit_ADC_v1.1_原理图.pdf) | [M5Stack v1.1 原理图](https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/576/Sch_UNIT_ADC_v1.1.pdf) | 1 | 95,462 | `01BA1309BB599D97125128CD398F2A364CFEBCEFA9BE1B0258BAD2FB0CB89F79` |
| [`TI_ADS1110_数据手册.pdf`](TI_ADS1110_数据手册.pdf) | [TI ADS1110 数据手册](https://www.ti.com/lit/ds/symlink/ads1110.pdf) | 26 | 858,090 | `1E15543D347DBF9E119A8DEA59D5154ED0A76DCFEFDAC9032693808C05BCE09D` |
重新核验哈希时,在 PowerShell 中运行:
```powershell
Get-ChildItem -LiteralPath ".\硬件相关资料和硬件的连接\M5Stack Unit ADC v1.1相关内容\官方资料" -Filter *.pdf |
Get-FileHash -Algorithm SHA256
```
## 资料职责和优先级
1. **v1.1 原理图**:确认板级接线、`510kΩ/100kΩ` 分压、LM358 缓冲、I²C 上拉及测量负端与 GND 的关系。
2. **TI ADS1110 数据手册**:确认寄存器位、15/30/60/240 SPS、PGA、二进制补码、三字节读取和电气极限。
3. **M5Stack 产品资料**:确认产品 SKU、包装、线色、输入范围、地址和机械尺寸。
4. **网页或示例代码**:只作补充,不能覆盖原理图和 TI 数据手册。
## M5Stack 页面中的已知冲突
截至下载日期,M5Stack Unit ADC v1.1 页面正文写明 `ADS1110` 和 `15/30/60/240 SPS`,但同一页面的部分规格/比较表仍出现 `ADS1100` 或 `8/16/32/128 SPS`,其 Arduino 链接也指向旧 `M5-ADS1100` 示例。
本项目固定使用:
- 芯片:`ADS1110`
- 配置:连续转换、15 SPS、PGA=1
- 配置字节:`0x0C`
- 读取:输出高字节、输出低字节、配置字节
不要把旧示例中的 `RATE_8`、`RATE_16`、`RATE_32`、`RATE_128` 名称或 `M5_ADS1100.h` 当成 v1.1 的权威定义。
## 浏览器后路
平时先使用本地 PDF。仅在下列情况进入浏览器:
- 实物不是 `U013-V11`,或 PCB 丝印/端子与本地原理图不同。
- `i2cdetect -y 1` 发现的地址不是 `0x48`。
- 本地 PDF 无法打开或哈希不符。
- 官方修订了硬件、原理图或 ADS1110 数据手册。
- 需要查询本文档未覆盖的核桃派 I²C 系统配置。
优先网址:
- M5Stack 在线产品页:<https://docs.m5stack.com/zh_CN/unit/Unit-ADC_V1.1>
- M5Stack 官方硬件资料入口:<https://docs.m5stack.com/zh_CN/unit/Unit-ADC_V1.1>
- TI ADS1110 产品资料:<https://www.ti.com/lit/ds/symlink/ads1110.pdf>
- 核桃派 I²C 以本项目已经核验的 `/dev/i2c-1`、物理脚 3/5 和设备树为准;不得套用其他开发板的配置命令。
浏览器检索时组合使用 `M5Stack Unit ADC v1.1 U013-V11 ADS1110`。只搜索 `M5Stack ADC` 很容易进入旧 ADS1100 页面。若官方资料发生变化,记录访问日期、URL 和实物修订后再更新本地文件及哈希,不静默覆盖现有资料。
@@ -0,0 +1,66 @@
# 核桃派 ZeroW + Unit ADC v1.1 接线与首次上电检查
适用实物:核桃派 ZeroW、已经接好并通过基础画面测试的 HUB75 屏、`M5Stack Unit ADC v1.1 / U013-V11 / ADS1110`。本模块只并联测量屏幕输入端 5V,不测电流。
## 接线前必须断电
> 拔掉核桃派 USB-C,并关闭或拔掉屏幕独立 5V 电源。屏幕、核桃派和 Unit ADC 全部无电后才能插拔或移动任何线。
- 不拆改已经验证通过的 HUB75 信号线。
- 不把屏幕 5V 接到核桃派物理脚 2 或 4,不通过核桃派排针给屏幕供电。
- 不让屏幕的大电流经过 Unit ADC、Grove 线或 VIN 测量线。
- 不使用物理脚 32;它仍然保留。
- ADC 输入不得超过模块官方 `0..12V` 范围,当前只测约 5V。
## 六个连接点
Unit ADC 有两组不同职责的连接:Grove 四线负责模块供电与 I²C;VIN 两线只负责并联测量。两组线都接到同一套屏幕输入 5V/GND,但不能省略或混淆。
| 模块端 | 常见线色 | 连接位置 | 用途 |
|---|---|---|---|
| Grove `SDA` | 黄 | 核桃派物理脚 `3` / `PI8` / `SDA1` | I²C 数据 |
| Grove `SCL` | 白 | 核桃派物理脚 `5` / `PI7` / `SCL1` | I²C 时钟 |
| Grove `5V` | 红 | 屏幕输入端所用的同一稳定系统 `5V` | 只给 Unit ADC 供电 |
| Grove `GND` | 黑 | 屏幕输入端所用的同一系统 `GND` | 模块供电地与 I²C 共地 |
| 测量 `VIN+` | 按端子丝印 | 并联到屏幕电源输入端的 `5V` 测试点 | 被测正端 |
| 测量 `VIN-/GND` | 按端子丝印 | 并联到同一屏幕电源输入端的 `GND` | 被测负端 |
不要只根据跳线颜色猜测 VIN 正负;VIN 端必须以模块外壳、PCB 或端子丝印为准。Grove 线色若与表格不同,也必须以模块和线束标注的 `SDA/SCL/5V/GND` 为准。
## 并联关系
```text
屏幕电源 5V ────────┬────────> 屏幕 5V 输入(原大电流路径不变)
├────────> Unit ADC Grove 红线 5V
└────────> Unit ADC VIN+
屏幕电源 GND ───────┬────────> 屏幕 GND 输入(原大电流路径不变)
├────────> Unit ADC Grove 黑线 GND
└────────> Unit ADC VIN-/GND
核桃派物理脚 3 ──────────────> Unit ADC 黄线 SDA
核桃派物理脚 5 ──────────────> Unit ADC 白线 SCL
```
## 断电状态下逐项检查
- [ ] 模块丝印确认为 `U013-V11`,芯片方案为 `ADS1110`。
- [ ] 核桃派 USB-C 已拔,屏幕独立 5V 已关闭或拔掉。
- [ ] 黄线 `SDA → 物理脚 3`,白线 `SCL → 物理脚 5`,没有对调。
- [ ] 红线只接系统 5V,黑线只接系统 GND,没有反接。
- [ ] `VIN+` 接屏幕输入端 5V,`VIN-/GND` 接同一点的 GND。
- [ ] 屏幕原有粗电源线保持直接供电,任何负载电流都不经过 ADC。
- [ ] HUB75 信号接线保持原样,物理脚 32 仍空置。
- [ ] 所有裸露导体固定,无短路、松脱或碰到金属背板的风险。
完成以上检查后仍然保持断电,并回复:`ADC 接线完成,可上电检查`。得到下一步指令前不要自行上电。
## 上电后由 Codex 逐项检查
1. 先确认核桃派和服务恢复在线,不立即校准。
2. 确认 `/dev/i2c-1` 仍存在,地址 `0x48` 有响应。
3. 检查配置低 7 位为 `0x0C`,取得 5 个新样本中位数。
4. 确认未校准电压合理且稳定,网页故障状态自动恢复。
5. 再单独暂停,请用户把万用表并联到同一个屏幕输入 5V/GND 点并只提供一个稳定读数。
任一步出现无响应、持续负 raw、短读、配置不符、异常发热或显示故障,都先断电并只检查对应线组,不跨过失败项。
@@ -0,0 +1,357 @@
#!/usr/bin/env python3
"""读取 M5Stack Unit ADC v1.1 (U013-V11 / ADS1110) 的屏幕输入电压。
真实 I2C 读取仅支持 Linux;--self-test 不访问硬件,可在 Windows 上运行。
本脚本只使用 Python 标准库,不接入项目主服务。
"""
from __future__ import annotations
import argparse
import errno
import math
import os
import statistics
import sys
import time
from dataclasses import dataclass
from typing import Sequence
DEFAULT_BUS = "/dev/i2c-1"
DEFAULT_ADDRESS = 0x48
I2C_SLAVE = 0x0703
CONFIG_CONTINUOUS_15SPS_PGA1 = 0x0C
CONFIG_VALUE_MASK = 0x7F
DATA_READY_MASK = 0x80
REFERENCE_VOLTS = 2.048
ADC_CODE_SCALE = 32768
BOARD_DIVIDER_RATIO = 6.1
CONVERSION_WAIT_SECONDS = (1.0 / 15.0) + 0.010
FRESH_DATA_TIMEOUT_SECONDS = 0.75
class AdcError(RuntimeError):
"""用户可处理的 ADC 访问错误。"""
class AdcProtocolError(AdcError):
"""ADS1110 返回的数据不符合本项目固定协议。"""
@dataclass(frozen=True)
class Reading:
raw_median: float
volts: float
config: int
def parse_address(value: str) -> int:
try:
address = int(value, 0)
except ValueError as exc:
raise argparse.ArgumentTypeError("I2C 地址应为 0x48 这类整数") from exc
if not 0x08 <= address <= 0x77:
raise argparse.ArgumentTypeError("7 位 I2C 地址必须在 0x08 到 0x77 之间")
return address
def positive_int(value: str) -> int:
try:
result = int(value)
except ValueError as exc:
raise argparse.ArgumentTypeError("应输入正整数") from exc
if result <= 0:
raise argparse.ArgumentTypeError("应输入大于 0 的整数")
return result
def positive_float(value: str) -> float:
try:
result = float(value)
except ValueError as exc:
raise argparse.ArgumentTypeError("应输入正数") from exc
if not math.isfinite(result) or result <= 0:
raise argparse.ArgumentTypeError("应输入大于 0 的有限数值")
return result
def voltage_from_raw(raw: int | float, calibration_factor: float = 1.0) -> float:
if not -32768 <= raw <= 32767:
raise ValueError("ADS1110 raw 必须在 -32768 到 32767 之间")
if not math.isfinite(calibration_factor) or calibration_factor <= 0:
raise ValueError("calibration_factor 必须是大于 0 的有限数值")
adc_volts = float(raw) * REFERENCE_VOLTS / ADC_CODE_SCALE
return adc_volts * BOARD_DIVIDER_RATIO * calibration_factor
def validate_config(config: int) -> None:
if not 0 <= config <= 0xFF:
raise AdcProtocolError(f"配置字节超出范围: {config!r}")
actual = config & CONFIG_VALUE_MASK
if actual != CONFIG_CONTINUOUS_15SPS_PGA1:
raise AdcProtocolError(
"ADS1110 配置不符合连续转换/15SPS/PGA=1: "
f"收到 0x{config:02X},低 7 位应为 0x{CONFIG_CONTINUOUS_15SPS_PGA1:02X}"
)
def decode_conversion(data: bytes) -> tuple[int, int]:
if len(data) != 3:
raise AdcProtocolError(f"ADS1110 应返回 3 字节,实际收到 {len(data)} 字节")
raw = int.from_bytes(data[:2], byteorder="big", signed=True)
config = data[2]
validate_config(config)
return raw, config
def describe_os_error(action: str, exc: OSError, address: int) -> AdcError:
remote_io = getattr(errno, "EREMOTEIO", 121)
if exc.errno in (remote_io, errno.ENXIO):
return AdcError(
f"{action}失败:I2C 地址 0x{address:02X} 没有响应;"
"请检查 v1.1 型号、5V/GND、SDA/SCL,并先运行 i2cdetect -y 1"
)
if exc.errno == errno.EBUSY:
return AdcError(
f"{action}失败:I2C 地址 0x{address:02X} 已被内核驱动占用;"
"如果 i2cdetect 显示 UU,请先查明占用原因"
)
if exc.errno in (errno.EACCES, errno.EPERM):
return AdcError(
f"{action}失败:没有访问 I2C 设备的权限;请检查 /dev/i2c-1 权限和当前用户的 i2c 组"
)
return AdcError(f"{action}失败:{exc.strerror or exc} (errno={exc.errno})")
class Ads1110Device:
def __init__(self, bus_path: str, address: int) -> None:
self.bus_path = bus_path
self.address = address
self._fd: int | None = None
def __enter__(self) -> "Ads1110Device":
if os.name != "posix":
raise AdcError("真实 I2C 读取只支持 Linux;在 Windows 上请使用 --self-test")
try:
import fcntl
except ImportError as exc:
raise AdcError("当前 Python 缺少 Linux fcntl,无法访问 /dev/i2c-* 设备") from exc
try:
self._fd = os.open(self.bus_path, os.O_RDWR)
except FileNotFoundError as exc:
raise AdcError(
f"没有找到 {self.bus_path};请确认核桃派 I2C1 设备节点和总线编号"
) from exc
except PermissionError as exc:
raise AdcError(
f"没有权限打开 {self.bus_path};请检查设备权限和当前用户的 i2c 组"
) from exc
except OSError as exc:
raise describe_os_error(f"打开 {self.bus_path}", exc, self.address) from exc
try:
fcntl.ioctl(self._fd, I2C_SLAVE, self.address)
except OSError as exc:
self.close()
raise describe_os_error("选择 I2C 从设备", exc, self.address) from exc
return self
def __exit__(self, exc_type: object, exc: object, traceback: object) -> None:
self.close()
def close(self) -> None:
if self._fd is not None:
os.close(self._fd)
self._fd = None
def _require_fd(self) -> int:
if self._fd is None:
raise AdcError("I2C 设备尚未打开")
return self._fd
def configure(self) -> None:
fd = self._require_fd()
try:
written = os.write(fd, bytes([CONFIG_CONTINUOUS_15SPS_PGA1]))
except OSError as exc:
raise describe_os_error("写入 ADS1110 配置", exc, self.address) from exc
if written != 1:
raise AdcProtocolError(f"ADS1110 配置应写入 1 字节,实际写入 {written} 字节")
def read_fresh(self) -> tuple[int, int]:
fd = self._require_fd()
deadline = time.monotonic() + FRESH_DATA_TIMEOUT_SECONDS
while True:
time.sleep(CONVERSION_WAIT_SECONDS)
try:
data = os.read(fd, 3)
except OSError as exc:
raise describe_os_error("读取 ADS1110", exc, self.address) from exc
raw, config = decode_conversion(data)
if (config & DATA_READY_MASK) == 0:
return raw, config
if time.monotonic() >= deadline:
raise AdcProtocolError("等待 ADS1110 新转换数据超时,ST/DRDY 始终为 1")
def take_reading(
device: Ads1110Device,
sample_count: int,
calibration_factor: float,
) -> Reading:
raw_values: list[int] = []
last_config = CONFIG_CONTINUOUS_15SPS_PGA1
for _ in range(sample_count):
raw, last_config = device.read_fresh()
raw_values.append(raw)
raw_median = float(statistics.median(raw_values))
return Reading(
raw_median=raw_median,
volts=voltage_from_raw(raw_median, calibration_factor),
config=last_config,
)
def format_raw(raw: float) -> str:
if raw.is_integer():
return str(int(raw))
return f"{raw:.1f}"
def print_reading(reading: Reading) -> None:
print(
f"屏幕输入端电压: {reading.volts:.3f} V "
f"raw中位数: {format_raw(reading.raw_median)} "
f"配置: 0x{reading.config:02X}",
flush=True,
)
if reading.volts < 0 or reading.volts > 12:
print(
"警告:读数超出模块规定的 0V~12V 输入范围,请停止并检查型号、接线和测量点。",
file=sys.stderr,
flush=True,
)
def require_self_test(condition: bool, message: str) -> None:
if not condition:
raise AssertionError(message)
def run_self_test() -> None:
require_self_test(CONFIG_CONTINUOUS_15SPS_PGA1 == 0x0C, "固定配置字节不是 0x0C")
require_self_test(voltage_from_raw(0) == 0.0, "raw=0 的换算结果不是 0V")
raw_5v, config = decode_conversion(bytes([0x33, 0x3B, 0x0C]))
require_self_test(raw_5v == 13115, "5V 样例 raw 解析错误")
require_self_test(config == 0x0C, "5V 样例配置解析错误")
require_self_test(
abs(voltage_from_raw(raw_5v) - 5.0) < 0.001,
"raw=13115 没有换算为约 5.000V",
)
negative_raw, config_with_old_data = decode_conversion(bytes([0xFF, 0xFF, 0x8C]))
require_self_test(negative_raw == -1, "16 位负数符号解析错误")
require_self_test(config_with_old_data == 0x8C, "ST/DRDY 状态位解析错误")
require_self_test(
abs(voltage_from_raw(32767) - 12.49241875) < 1e-9,
"正满量程换算错误",
)
try:
decode_conversion(bytes([0x00, 0x00]))
except AdcProtocolError:
pass
else:
raise AssertionError("短读取必须触发 AdcProtocolError")
try:
validate_config(0x08)
except AdcProtocolError:
pass
else:
raise AssertionError("错误采样配置必须触发 AdcProtocolError")
print("自测通过:配置 0x0C = 连续转换 / 15SPS / PGA=1")
print(f"自测通过:raw=13115 -> {voltage_from_raw(13115):.6f}V(约 5.000V)")
print("自测通过:16 位负数符号、满量程、短读取和错误配置检查正常")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="读取 M5Stack Unit ADC v1.1 (ADS1110) 的屏幕输入端电压"
)
parser.add_argument("--bus", default=DEFAULT_BUS, help=f"I2C 设备路径,默认 {DEFAULT_BUS}")
parser.add_argument(
"--address",
type=parse_address,
default=DEFAULT_ADDRESS,
help="7 位 I2C 地址,默认 0x48",
)
parser.add_argument(
"--samples",
type=positive_int,
default=5,
help="每次输出使用的新样本数量,取中位数,默认 5",
)
parser.add_argument(
"--calibration-factor",
type=positive_float,
default=1.0,
help="可选比例校正系数,默认 1.0",
)
parser.add_argument("--watch", action="store_true", help="连续读取,按 Ctrl+C 停止")
parser.add_argument(
"--interval",
type=positive_float,
default=1.0,
help="--watch 两次输出之间的最短秒数,默认 1.0",
)
parser.add_argument(
"--self-test",
action="store_true",
help="只测试协议常量和换算,不访问 I2C,可在 Windows 运行",
)
return parser
def run_hardware(args: argparse.Namespace) -> None:
with Ads1110Device(args.bus, args.address) as device:
device.configure()
while True:
started = time.monotonic()
print_reading(take_reading(device, args.samples, args.calibration_factor))
if not args.watch:
return
elapsed = time.monotonic() - started
time.sleep(max(0.0, args.interval - elapsed))
def main(argv: Sequence[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.self_test:
run_self_test()
return 0
try:
run_hardware(args)
except KeyboardInterrupt:
print("\n已停止。")
return 0
except (AdcError, ValueError) as exc:
print(f"错误:{exc}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,2 @@
屏幕型号:RGB-Matrix-P3-64x64-F
参考网站:https://docs.waveshare.net/RGB-Matrix-Px-64x64?variant=P3-64x64-F&spec=P3-64x64-F
@@ -0,0 +1,88 @@
# 核桃派 ZeroW + 64×64 HUB75 屏接线与首次上电检查
适用范围:一块核桃派 ZeroW、一块 `RGB-Matrix-P3-64x64-F`(64×64、1/32 扫描、ABCDE)、一套独立 5V 屏幕电源。本阶段**不连接 Unit ADC**。
## 先看安全边界
> **接线前必须让核桃派和屏幕全部断电。** 拔掉核桃派 USB-C 供电,关闭或拔掉屏幕 5V 电源。不得带电插拔 HUB75、不得把屏幕 5V 接到核桃派 40-pin 的 5V 脚、不得把 5V 接到任何 PI 信号脚。
- 核桃派继续用自己的 USB-C 供电。
- 屏幕用独立、极性正确的 5V 电源直接接屏幕电源端;参考额定能力 5V/4A。
- 两者只通过 HUB75 GND 和电源负极形成共地;屏幕的大电流不得流经核桃派排针。
- 只接屏幕标有 `INPUT` / `IN` 的 HUB75 口;`OUTPUT` / `OUT` 本项目不用。
- 物理脚 3、5 留给后续 ADC,物理脚 32(PI16)保留,本阶段都不接。
## HUB75 INPUT 的编号
以下编号采用本项目已经核验的 Waveshare HUB75 INPUT 定义,不根据线材颜色猜测:
```text
HUB75 INPUT(按信号定义核对)
pin 16 R1 pin 15 G1 pin 14 B1 pin 13 GND
pin 12 R2 pin 11 G2 pin 10 B2 pin 9 E
pin 8 A pin 7 B pin 6 C pin 5 D
pin 4 CLK pin 3 LAT pin 2 OE pin 1 GND
```
若插座缺口、丝印或观看方向与上面排布不一致,以屏幕 PCB 的 `R1/G1/.../GND` 信号丝印和 `INPUT` 标记为准,不要强插排线。
## 逐根信号接线表
| HUB75 pin | 信号 | 核桃派物理脚 | H618 管脚 | 检查用途 |
|---:|---|---:|---|---|
| 16 | `R1` | 29 | `PI0` | 上半屏红 |
| 15 | `G1` | 31 | `PI1` | 上半屏绿 |
| 14 | `B1` | 33 | `PI2` | 上半屏蓝 |
| 13 | `GND` | 34 | `GND` | 第二根信号地 |
| 12 | `R2` | 35 | `PI3` | 下半屏红 |
| 11 | `G2` | 37 | `PI4` | 下半屏绿 |
| 10 | `B2` | 8 | `PI5` | 下半屏蓝 |
| 9 | `E` | 16 | `PI12` | 行地址 bit 4,不能省 |
| 8 | `A` | 10 | `PI6` | 行地址 bit 0 |
| 7 | `B` | 28 | `PI9` | 行地址 bit 1 |
| 6 | `C` | 27 | `PI10` | 行地址 bit 2 |
| 5 | `D` | 15 | `PI11` | 行地址 bit 3 |
| 4 | `CLK` | 38 | `PI13` | 移位时钟 |
| 3 | `LAT` | 36 | `PI15/GPIO` | GPIO 锁存 |
| 2 | `OE` | 40 | `PI14/PWM4` | 硬件定时输出使能,低有效 |
| 1 | `GND` | 30 | `GND` | 第一根信号地 |
## 屏幕独立供电
1. 屏幕电源正极只接屏幕电源端 `5V` / `VCC`。
2. 屏幕电源负极只接屏幕电源端 `GND`。
3. 核桃派物理脚 30、34 分别接 HUB75 INPUT pin 1、13 的 GND,形成信号共地。
4. 不把屏幕 5V 接到核桃派物理脚 2 或 4;不从核桃派给屏幕供电。
5. 电源关闭时用目视或万用表通断档确认没有把屏幕 5V 和 GND 反接。
## 断电完成后的逐项检查
- [ ] 核桃派 USB-C 已拔,屏幕 5V 电源已关闭或拔掉。
- [ ] 使用的是屏幕 `INPUT`,不是 `OUTPUT`。
- [ ] 16 根 HUB75 连接逐行与表格一致,没有按线色猜信号。
- [ ] `A/B/C/D/E` 五根都已接,尤其 `E → 物理脚 16`。
- [ ] `CLK/LAT/OE → 物理脚 38/36/40`;特别确认 LAT/OE 不是旧版的相反接法。
- [ ] 两组 RGB 没有上下半屏或红绿蓝互换。
- [ ] HUB75 pin 1、13 分别到核桃派 GND 物理脚 30、34。
- [ ] 屏幕独立 5V 极性正确,且没有接到核桃派 5V 排针。
- [ ] 物理脚 3、5、32 都保持空置;Unit ADC 完全未接。
- [ ] 裸露导体没有互碰,线材不会被风扇、屏幕背板或金属外壳夹住。
## 首次上电顺序(必须等 Codex 指令)
本表只配套硬件映射 `walnutpi-pi-bank-pwm-oe-v2`。接线完成后先不要自行长时间点亮。回复“屏幕接线完成,LAT 已接物理脚 36,OE 已接物理脚 40,已按要求检查,可上电测试”。随后按指令:先启动低亮度全黑安全画面,再依次测试全红、全绿、全蓝、全白、四角、关键行、方向、文字、图片和动画。任何一步异常都停在该步,只检查对应线组。
## 快速故障对应关系
| 现象 | 只优先检查 |
|---|---|
| 上半屏缺某颜色 | `R1/G1/B1` 对应单线 |
| 下半屏缺某颜色 | `R2/G2/B2` 对应单线 |
| 红绿蓝互换 | 六根 RGB 的信号名,不查行地址 |
| 半屏、重复行、行位置错误 | `A/B/C/D/E`,特别是 `E` |
| 画面横向错位或随机数据 | `CLK` 与六根 RGB |
| 整屏不亮或闪一下 | `OE`、`LAT`、共地和屏幕 5V 极性 |
| 随机亮点或抖动 | 共地、线长、接触和 5V 稳定性 |
失败时整机断电后再动线;不得带电逐根试插。