先核 ESP32-2432S028R 和 ST7789。源码里另有 cyd_ili9341,但它是其他屏变体备用,不是本项目推荐环境。需要确认实际驱动,再选环境,不用“两个都刷一下”判断板型。
BNO086 · CYD ST7789 · V1.7
BNO086:接线、代码与LCD读屏
先认准CYD ST7789,再核六线与完整V1.7工程。按启动现象排错,读懂屏幕与真实日志里的测量值、更新状态和测试范围。
Quick Facts
- 主板:ESP32-2432S028R / ST7789
- 固件配置:0x4B · SDA27 / SCL22
- 控制:INT35 / RST19 · 3.3V
- 固件:V1.7,上电自动独立会话
- 范围:三类报告;完整 XYZ 仍待测
- 下载:源码已备,网盘待真实链接
第一次用,从这里开始 · V1.7
先配对接线与代码,再看屏幕读数
这套方案让 CYD 屏幕显示 BNO086 的加速度、转动快慢和朝向。先认准主板和屏幕,按这版代码接六根线,再打开完整工程编译。接线和代码版本要成对,不能只照一张相似板子的图。
遇到无数据,先读 按顺序排查;跑起来后读 LCD 11 区图解、动作对应的变化 和 真实日志解读。解释按 2026-10-05 核对的 V1.7 源码,没有读取此刻板上的实时状态。
第1步 · 先认板,再找接头
这页对应哪块主板、哪种屏幕?
唯一推荐组合:CYD ESP32-2432S028R + ST7789 + BNO086 + V1.7 / cyd_st7789。本项目按用户确认的这块板制作;屏幕已经有实测记录。外观相似或也叫“小黄板”的产品可能换过屏幕、接头和板修订,不能靠黄色PCB认定是同一配置。
GPIO 是ESP32引脚的电气编号,不是排针第几根。翻到板背面辨认 P3;GPIO35 用作 INT。GPIO21 是屏幕背光,不能拿它代替本程序的 RST19。
对照同版背面整板:P3 / CN1 在哪里

模块图只用于对照信号名;用户模块为其他/自制品牌,按用户说明参考SparkFun同电路,不能由图片保证品牌或逐项电气一致。按模块实际丝印找 3V3/VCC、GND、SDA、SCL、INT、RST。
Six connections
断电逐根接线:CN1四线、P3 INT和RST19
第2步:先断电,按下面的信号名逐根核。本程序固定 SDA27/SCL22/INT35/RST19,不用常见ESP32示例的默认SDA21。移除SD卡,六根线都按3.3V系统连接,先核模块电源端的实际丝印。

- 电源:BNO086 的 3V3/VCC → CYD CN1 3V3。本方案用3.3V;不能只按模块图片颜色辨电源。
- 地线:BNO086 GND → CYD CN1 GND,必须共地。
- 数据:BNO086 SDA → CYD CN1 IO27。SDA不是IO22;程序明确 Wire.begin(27,22,400000)。
- 时钟:BNO086 SCL → CYD CN1 IO22。P3也有IO22,但不需要另接第二根SCL。
- 数据提醒:BNO086 INT → CYD 背面P3 IO35,按丝印核对。GPIO35只能输入、无内部上拉;是否加偏置要按实际模块电路核,不能只因没有另加电阻就断言故障。
- 复位:BNO086 RST → SKU02753 microSD Sniffer DAT0/DO(GPIO19),该引出方案已由用户确认。GPIO21供背光,不能代替RST19;本程序不使用SD卡。
查看最新软件逻辑接线图(供核对,不是焊点指南)
来源图的六线连接已经与项目映射核对。原图中竖排文字在小屏上较小,请同时看本节接线表和下面的说明。

| 模块端 | CYD 端 | 用途 / 易错点 |
|---|---|---|
| 3V3 | 3.3V | 本方案供电与逻辑使用 3.3V;不能据此推定其他模块耐 5V |
| GND | GND | 共地 |
| SDA | GPIO27 | 数据线;不要与 SCL 交换 |
| SCL | GPIO22 | 时钟线 |
| INT | GPIO35,背面 P3 | 低有效输入;GPIO35 无内部上拉。背面 P3 有位置证据,图中前视虚拟点不是焊点。实际电平与输出结构仍需按模块电路核对 |
| RST | GPIO19;TF DAT0 / DO 的电气身份 | 低有效复位;使用用户确认的SKU02753 microSD Sniffer DAT0/DO引出;不能按逻辑图虚拟点焊接。本方案不使用 SD 卡 |
本项目按 INT 配合读取。SparkFun 库接线流程同时要求 INT / RST,Qwiic 四线接口本身只带电源、GND、SDA、SCL。官方接线指南、ESP32 GPIO 文档。
只适用BNO086 SKU01954 + CYD ESP32-2432S028R / ESP32 classic / 4MB / ST7789。浏览器会核芯片与Flash容量、下载SHA-256及写后MD5。烧录会覆盖原程序、分区及写入范围内的数据,请自行备份。串口由你主动选择。
复用已验证ST7789 V1.7应用;本次只做离线导出和浏览器检查,没有刷写实际板卡。
第3步 · 接线和这份工程一起用
打开整包 V1.7:从下载到编译、烧录
- 解压完整 ZIP。打开
BNO086_I2C_Test_V1.7目录,应同时看到platformio.ini、src和lib。不是只下载 main.cpp,也不是在 Arduino IDE 里新建一个空草图。 - 用 VS Code + PlatformIO 打开项目文件夹。安装VS Code,在扩展里安装官方 PlatformIO IDE并等待初始化完成,它自带Core。通过“文件 → 打开文件夹”选择含 platformio.ini 的目录;确认默认环境
cyd_st7789。项目使用Arduino框架,构建入口是PlatformIO;命令可在PlatformIO Core CLI终端执行。 - 保留本包依赖与配置。platformio.ini 固定
espressif32@6.13.0和 Arduino-ESP32 2.0.17,目标esp32dev。lib已带 SparkFun BNO08x 1.0.6修补副本、TFT_eSPI 2.5.43、U8g2_for_TFT_eSPI 1.7.0;首次构建需下载对应平台和工具链。不要先换成网上同名BNO库,或重配默认SDA/SCL。 - 先 Build。在PlatformIO Project Tasks展开
cyd_st7789 → General → Build,或执行下方只编译命令。成功应出现该环境SUCCESS;失败先保留完整报错,检查打开目录、完整lib和工具链下载,不先改硬件接线。 - 再由你确认目标串口并 Upload。接好已核对的六线、移除SD卡,用可传数据的USB线连接CYD。确认目标端口,关闭占用它的串口监视器/日志工具,再执行上传命令。COM9只是本项目历史端口,不保证是你电脑上的端口。
- 上传完成后按下一节核启动。V1.7上电自动开始AUTO会话,不需要先发START才有读数。打开串口时以
INFO核对固件标识、ST7789和地址0x4B;永久记录另用日志工具保存。
pio run -e cyd_st7789
pio device listpio run -e cyd_st7789 --target upload --upload-port COMx
pio device monitor --port COMx --baud 115200上传会写入目标板固件,命令中的 COMx 是必须替换的占位符。本次网页制作只核对文件,没有执行上传或串口操作。公开ZIP已核验290份文件哈希与原快照一致;原项目曾编译、刷写并实测,本次没有对公开ZIP重新做编译或硬件测试。
想看程序内容或复制整份文件,见 代码查看与复制;日志工具、校验清单和说明在 下载区。独立 main.cpp 不包含完整依赖。
安装与命令参考:PlatformIO官方VS Code说明、pio run与上传参数;项目实际设置以包内platformio.ini为准。
第4步 · 核对是否真正开始读数据
成功启动时,屏幕和日志应该出现什么?
- 屏幕亮起并显示标题。“BNO086 模块测试”说明显示程序运行到了绘制界面;仅此还没有证明传感器通信成功。
- 初始化完成。ID 出现 AUTO_…,阶段进入 WARMUP,再STILL;地址为0x4B,出现 SH2 OK。串口 INFO 对应
BNO086_MODULE_TEST_V1.7。若只显示 Waiting for START,先核版本与STATE,而不是直接认定模块坏了。 - 确认持续报告。A/G/RPY 三行有值,Valid 的三个累计数持续增长,Hz 随最近数据更新。静止时数字很稳定也可以正常工作;OLD 则表示该类数据未到或已超过500ms没有效更新。
- 先完成静置,再轻缓动作。预热5秒、静置15秒期间放稳,等 READY 再观察拿起/转动响应。G显示转得多快,RPY显示朝向;完整XYZ结果要按串口命令逐轴记录。
- 保存并阅读本轮结果。I2Cerr等异常应结合完整日志;超过60秒也不会自动FINISH。通信有数据、静置满足窗口、三轴完成是分别判断的事,不能因有SH2 OK就宣称全功能通过。
每次重启产生新的 boot / run / AUTO 标签并清零,不恢复旧模块标签或结论。未连主机工具时的串口启动输出不能事后补回;屏幕实时数据显示本身不是永久日志存储。
Troubleshooting
没有数据时,按这个顺序查
- 先记录现象。拍下LCD标签/阶段/OLD/异常;保存INFO、STATUS和启动日志。问题可能涉及接线、供电、版本、依赖或器件,不凭一句“没数据”判定原因。
- 核板型与代码是否配对。这页是CYD ST7789 + V1.7 + cyd_st7789;确认INFO固件身份、地址0x4B和SDA27/SCL22/INT35/RST19。旧V1.6可能等待START,不能按V1.7启动行为判断。
- 断电核六线和实物接头。CN1 3V3/GND/IO27/IO22,P3 IO35,已确认的GPIO19引出;检查接触、共地、模块电源/协议设置、SD卡未插、GPIO21仍用于背光。RST使用SKU02753 DAT0/DO,不试猜TF触点。
- 把地址应答与数据报告分开。I2C_PROBE wire_result=0只有地址应答;还需PRODUCT、REPORT_ENABLE成功和SAMPLE。只有地址没有SH2或Valid不增长,继续核INT/RST、实际模块协议与本包修补库。
- 按现象处理显示或刷新。屏幕不亮/颜色错/布局异常,先核ST7789环境、供电和GPIO21背光;有正常LCD但数值稳定,先看Valid是否增长、是否OLD,再判断是不是只是静止。串口与日志工具一次只让一个程序占用同一端口。
- 改动后新建独立记录。保留旧结果,断电改接后重启,记录新boot/run。阶段CHECK/!或REVIEW要看动作、环境、原始计数与窗口,不能抹掉错误后写成功。
上传成功,但屏幕不亮或显示异常?
上传成功只说明写入完成。先核cyd_st7789环境、板型、供电和GPIO21背光没有被拿去接RST;串口INFO是否显示V1.7/ST7789。触摸CS未配置对应本项目不启用触摸,不直接等于传感器通信故障。保留屏幕现象和串口输出再排查。
扫到 0x4B,为什么还不能说工作正常?
ACK 只说明地址有应答。还需确认 SH-2 产品响应、报告订阅成功、INT / RST 时序以及连续有效数据。保存本轮标签、run、异常,断电核六线,再另开轮复测,不能用扫描结果代替测试结论。
重启后如何自动显示,原来的测试记录在哪里?
本 V1.7 每次上电自动建立新的 AUTO 会话,实时显示;旧版本 V1.6 需要 START。请先 INFO 核固件。自动会话在 RAM 和串口上更新,永久文件由主机工具保存,未连接期间的启动行不能补回。新 run 不继承旧计数或标签。
没有另外加 INT 上拉,是不是接错了?
GPIO35 无内部上拉,但实际是否需要外加偏置还取决于模块输出电路。SparkFun v10 的 INT 网未画单独上拉,用户同电路说明尚未电气测量。先核电源、连线和电平,10kΩ到3.3V只作为可选排查,记录改动前后,不能单独归因。
零通信异常为什么仍然 REVIEW 或 INCOMPLETE?
通信、静置和阶段完成度分别记录。最后 AUTO 快照通信计数为0,但静置没有满足窗口,保留 REVIEW;68.390秒轮次静置满足粗测窗口但三轴未做,整体为 INCOMPLETE。原因应结合动作、环境和原始记录复核,不能直接推定模块损坏。
偏航变化或漂移,应检查什么?
先核所用报告、轴向、单位与报告 Status,再记录附近磁性物体和电流回路。Game RV 不用磁力计,偏航可能随时间漂移;Rotation Vector 使用磁北参考也受磁场环境影响。本工程没有独立磁北 / 精度验证,需用受控参考另测。
屏幕和日志里的角速度单位如何对照?
库值为 rad/s,屏幕与 SAMPLE.g_dps 乘以 180/π,单位为 °/s;静置 gyro_norm_peak_rads 和运动 axis_gyro_peak_rads 统计保留 rad/s。对照阈值或报告时按字段写单位,例如0.6 rad/s约为34.4°/s,不能把两个数字直接比较。
能用旧 BNO055 或普通扫描仪的程序吗?
BNO086 使用 SH-2 / SHTP 报告协议,BNO055 与 MPU6050 的库和寄存器不能直接替代。使用本包的修补库和默认 ST7789 环境,核对应六线;本项目不使用 SD 卡或触摸功能。
按当前程序逐项读屏
LCD 从上到下:11 区读懂每个字段
屏幕标题是“BNO086 模块测试”。真实 V1.7 是白字黑底,约每半秒重画一次读数;没有按数值自动变红、变绿的报警颜色。固件版本在串口 INFO 输出中确认,标题本身不显示版本。
main.cpp:355–394 的绘制顺序复刻;数字位置用横线或方括号留空,不是实机截图,也没有填造实测读数。蓝色编号是网页注释,真实 LCD 没有编号。打开原尺寸图ID:… run:… · 本轮是谁
ID 是 AUTO_… 自动标签或你输入的 M001 等人工标签。run 是这次上电内的会话序号。两者都不是芯片出厂序列号;完整 boot-run 在日志里,同为 run:0001 也可能来自不同上电。
阶段 · 倒计时 · 0x4B SH2 OK
WARMUP 预热 5 秒 → STILL 静置 15 秒 → READY 等你开始三轴动作;发送 X/Y/Z 后进入 MOTION,对应轴测试 8 秒。倒计时是当前阶段还剩几秒,READY 的 0s 不是故障。0x4B 是程序选择的 I²C 通信地址;SH2 OK 表示产品响应与三类报告订阅已完成,不能当作整机测试通过。
A m/s2 · X / Y / Z 加速度
三个数依次是模块坐标的 X、Y、Z,单位 m/s²,显示两位小数。包含重力,放在桌上也不应该期待三项全为 0;倾斜时重力会分配到不同轴。正负号表示沿哪个方向,不能只凭画面“往左”猜符号。
G d/s · X / Y / Z 转动快慢
三个数依次是绕 X、Y、Z 的角速度,显示一位小数。屏幕 d/s 指度/秒(°/s),不是距离,也不是转过的总角度。转动时会变化,停止转动后通常回到接近 0。库内部用 rad/s,程序显示时换算为 °/s。
RPY · Roll / Pitch / Yaw 朝向
依次是横滚、俯仰、偏航,单位度(°),屏幕取整。可以把它们理解为左右倾、前后仰、朝向转了多少,但要先对照模块轴向和安装方向。它来自 BNO086 的融合四元数,再由 ESP32 换算;不是直接照抄 G,也不是移动距离。停止转动后角度可以保持非零。复杂倾斜时可能多个角一起变化,接近角度边界的跳变不等于器件瞬间乱转。
Hz A/G/Q · 最近数据更新有多快
依次是 A 加速度、G 角速度、Q 融合姿态的最近约一秒有效报告次数,不是屏幕刷新次数。Q 行虽然叫 Q,LCD 用 RPY 显示该报告的朝向,并没有显示四元数四个数。程序每类请求 50 Hz;实测有效速率另看日志,不能强求三项始终等于 50.0。
Valid · 本轮读到了多少条有效报告
三个累计数顺序也是 A / G / Q。持续采集时应该不断增长;不是校准分数或合格率。新 AUTO 或新贴签会话从头计数,FINISH 后停止增长。三项不必相等,因为实际报告速率不同。
I2Cerr · Bad · Gap · Rst · 异常线索
I2Cerr:初始化和运行时读写出错次数;Bad:无效数值/重复报告及解码错误;Gap:A/G/Q 断流事件和 INT 等待超时,持续断流通常先计一次事件;Rst:意外器件复位和运行期主机硬复位,不含初始化的计划复位。这些数是诊断次数,不是测量值。四项为 0 仍应看完整日志:缺序号、过大包、启用失败等并没有逐项列在这行。
Still · XYZ · …s/60s · 粗测做到了哪一步
Still:WAIT 还没完成静置;OK 是静置粗测满足窗口;CHECK 是没满足,需复核动作与环境。XYZ 三个位置按 X/Y/Z 排列:- 没做,字母 O 做完且满足该轴窗口,! 做完但没满足。拿起模块不会自动把“---”变成“OOO”,需要 READY 后通过串口启动各轴阶段。…s/60s 是有效流开始后的累计秒数,60 秒为最低观察要求,不是自动结束的倒计时,超过 60 秒可继续增长。
大字状态 · TESTING / REVIEW / OBSERVED / INCOMPLETE
TESTING 表示会话还在进行,没有因通信/数据异常提前进入 REVIEW;它不是“已通过”。活动时可以同时出现 Still:CHECK 与 TESTING,因为阶段结果在 FINISH 后才汇总。REVIEW 表示有异常或结束时某项粗测没满足,需要检查;INCOMPLETE 是初始化、时长或阶段未完成;OBSERVED 是结束后满足本程序的观察窗口,仍不是计量精度或所有芯片功能的合格证明。
底部操作提示
AUTO 会话一直显示 AUTO label; START M001 to assign,意思是可以给模块贴一个人工标签;不是让你必须发送 START 才能看到数据。操作员会话 READY 时显示 Serial X/Y/Z; FINISH after 60s;其他阶段提示 Serial STATUS / FINISH。Serial 是串口命令,当前程序没有屏幕触摸按钮来执行它们。
如果看到 INIT INCOMPLETE,没有正常数据区
初始化没完成时,LCD 显示 Starting sensor…、Waiting for START 或 INIT INCOMPLETE,并显示 SDA27/SCL22/INT35/RST19、GPIO21 背光、3.3V/不使用TF卡及串口提示;不会显示正常 A/G/RPY 三行。先保存 INFO/STATUS 和启动日志,再断电核对接线。V1.7 上电 INIT 失败会保留 INCOMPLETE,不循环重试。GPIO19从SKU02753 DAT0/DO引出,不按逻辑图虚拟点焊接。
屏幕上哪个是校准等级?
这版 LCD 没有校准等级一栏。Still OK、Valid、SH2 OK 都不是校准完成。程序把报告状态低两位记为串口 SAMPLE 的 accuracy,0/1/2/3 表示报告估计的不可靠/低/中/高;它没有在 LCD 显示,也没有作为 Valid 的校准门槛。订阅“校准角速度”是输出类型的名字,本程序没有显式执行交互校准或保存校准命令。
布局与逻辑来源:公开 main.cpp 的 349–394 行;刷新 419–420 行,阶段 284–301 行;test_config.h;evidence_rules.h。数据类型定义见 SH-2 手册 §6.5.9 / §6.5.13 / §6.5.18。
Model & purpose
BNO086 做什么,本方案实际读了哪些报告
含重力的X/Y/Z三个分量;静止也有读数,单位m/s²。
绕X/Y/Z的角速度;停转后通常接近0,屏幕单位°/s。
融合朝向换算成横滚/俯仰/偏航;停止时可以保持非零,单位°。
BNO086 把三轴加速度计、陀螺仪、磁力计和运行 SH-2 的处理器集成在一个器件内。片内融合把多种传感输入整理成朝向、重力等报告,主机按需要订阅。I²C 负责传输,本项目用 CYD 屏幕显示读数并记录每轮状态。功能背景见 CEVA 官方产品说明。
| 对象 | 本页采用的范围 | 尚待确认 |
|---|---|---|
| BNO086 实物模块 | 用户说明采用与 SparkFun 同样电路;本页按型号组织 | 专用购买链接、逐项电路一致性和电气测量;模块SKU01954已确认。品牌不能写为已经保证的 SparkFun 原装实物 |
| CYD 测试方案 | ESP32-2432S028R / ST7789,PlatformIO + Arduino 框架,V1.7 | 其他屏幕和主控需要核引脚与驱动,不能直接沿用本板的端点位置 |
| 输出 | 含义 / 单位 | 本 V1.7 状态 |
|---|---|---|
| Accelerometer(A) | 包含重力的加速度,m/s²;不是原始 ADC 数值 | 已启用,SH2_ACCELEROMETER |
| Calibrated Gyroscope(G) | 角速度,库值为 rad/s;屏幕 / SAMPLE.g_dps 为 °/s;峰值统计字段 *_rads 为 rad/s | 已启用,SH2_GYROSCOPE_CALIBRATED |
| Rotation Vector(Q / RPY) | 九轴融合的四元数,源码转换为横滚、俯仰和偏航 | 已启用,SH2_ROTATION_VECTOR |
| Game Rotation Vector | 六轴姿态,不用磁力计修正航向 | 芯片支持,本工程未订阅 |
| 独立磁力计、Gravity、Linear Acceleration、步数、Interactive Calibration | 分别处理磁场、重力、运动加速度和其他应用 | 支持项;本工程没有独立启用 / 验证这些报告或流程 |
三类已启用报告均请求 20000 μs,即 50 Hz。请求速率、400 kHz 总线时钟与主机实际有效报告速率分别记录,不能混为一个“采样率”。
已有 BNO055 九轴资料 和 MPU6050 六轴 STM32 方案 可对照阅读。它们的协议、库和接线不同,不能将一个型号的代码直接套到另一个。
看动作,认数据
放稳、拿起、转动:先观察这些变化
动作前先读阶段:WARMUP / STILL 时放稳,不要为了让数字变化去晃动。等到 READY,再做轻缓动作;如果要留下三轴阶段结果,应按测试步骤通过串口开始 X、Y、Z,而不是只随手转一下。下面讲的是预期关系,不是为你的模块预填读数。
G 通常接近 0;A 仍有重力,分量取决于摆放方向,模长通常在约 9.8 m/s² 附近;RPY 可以是非零角度。看 Valid 继续增长,确认“数值稳定”与“停止更新”不是一回事。
起步、加速、减速时 A 可能变化;若朝向没改变,G/RPY 不应只因为换了位置而大幅变化。移到别处再放稳,它不会显示“走了多少厘米”。
A 的重力分量会在三个轴之间重新分配,RPY 也随朝向变化。A 某个数改变不一定意味着你在平移,可能只是板子倾斜了。
相应 G 角速度与朝向会变化;停止时 G 通常回近 0,RPY 保留新的朝向。按模块丝印与安装核轴向;有倾斜时可能多个 RPY 数一起变,不能只按录像左右方向猜正负。
LCD 约每秒更新 2 次,而传感报告远快于它,短促动作的峰值可能没恰好显示出来。需要比较数值或排查断流时,看串口原始日志;不要把某一帧的整数 RPY 当作精度证据。
| 已经在这版程序中观察什么 | 本程序没有给出什么 |
|---|---|
| A:含重力的三轴加速度;G:三轴角速度;Q换算的RPY:融合朝向;数据是否连续、异常和粗测阶段 | 空间位置、移动距离、线速度;独立磁场读数、单独去重力加速度、温度、电压;屏幕校准等级;绝对角度精度与长期可靠性证书 |
真实日志 · 原结论保留
后续成功日志:读到了什么,证实到哪一步?
这是保存于2026-10-04的V1.7真实串口记录,2026-10-05复查没有找到更晚的新轮硬件实测。每段保留自己的run,不把不同会话拼成一次通过;下方原始正文只去除了主机前缀。既有实拍视频尚未与这些run核对配对,不作为同步证据。
① 上电自动开始,模块应答并订阅成功
FW,BNO086_MODULE_TEST_V1.7,title=BNO086 module test,board=ESP32-2432S028R,display=ST7789,address=0x4B,boot=7012DC1D
EV,boot_ms=718,test_ms=25,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=INIT,event=START,fw=BNO086_MODULE_TEST_V1.7,address=0x4B,SDA=27,SCL=22,INT=35,RST=19,I2C_Hz=400000,id_source=automatic_boot_label,unique_serial=not_read
EV,boot_ms=728,test_ms=35,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=INIT,event=I2C_PROBE,address=0x4B,wire_result=0
EV,boot_ms=1307,test_ms=614,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=INIT,event=REPORT_ENABLE,acc=1,gyro=1,quat=1,request_us=20000
EV,boot_ms=1327,test_ms=634,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=WARMUP,event=STAGE_BEGIN,duration_ms=5000,instruction=place_module_stillV1.txt 原行 36、39、40、47、49 的选摘;非连续行没有拼成一条新记录。
FW确认V1.7、ST7789;START是自动上电标签。address=0x4B是本配置地址,wire_result=0表示地址探测成功。acc=1,gyro=1,quat=1说明三类报告的启用请求成功,request_us=20000是每类请求间隔20ms。还需要下一段持续SAMPLE才能证明实际有更新。
② 静止也在更新:看计数,不只看数字跳动
EV,boot_ms=21647,test_ms=20954,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=READY,event=SAMPLE,a_ms2=-8.312/-4.453/0.258,g_dps=-0.11/-0.11/-0.11,rpy_deg=-86.5/61.8/-14.9,hz=64.9/50.0/50.0,valid=1308/1012/1012,age_ms=7/3/0,sh2_estimated_us=21637239/21638639/21639639,accuracy=2/3/0,anomalies=0
EV,boot_ms=22647,test_ms=21954,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=READY,event=SAMPLE,a_ms2=-8.309/-4.473/0.277,g_dps=-0.11/0.34/0.34,rpy_deg=-86.5/61.8/-15.0,hz=64.0/50.0/50.0,valid=1372/1062/1062,age_ms=16/3/0,sh2_estimated_us=22628239/22639244/22640739,accuracy=2/3/0,anomalies=0V1.txt 原行 73、74 的选摘;非连续行没有拼成一条新记录。
这里 test_ms从20954到21954,相隔1000ms;valid从1308/1012/1012增至1372/1062/1062,分别新增64/50/50条。A/G/RPY读数变化很小,报告仍持续抵达。
| 这条真实SAMPLE字段 | 怎么读 |
|---|---|
| boot_ms / test_ms | 分别是本次启动后毫秒数、本轮会话开始后毫秒数;不是日期,也不等于有效流时长。 |
| a_ms2=-8.312/-4.453/0.258 | A的X/Y/Z,m/s²,含重力;这是当时摆放方向的真实分量。 |
| g_dps=-0.11/-0.11/-0.11 | G的X/Y/Z,°/s;接近零不是没收到数据。 |
| rpy_deg=-86.5/61.8/-14.9 | 当时的横滚/俯仰/偏航,°;LCD取整显示,串口保留一位小数。 |
| hz / valid | 最近约一秒报告率 / 本轮累计有效数,顺序A/G/Q。 |
| age_ms=7/3/0 | 距各类最后有效更新7、3、0ms;超过500ms或未收到,LCD相应行标OLD。 |
| accuracy=2/3/0 | 报告状态等级,顺序A/G/Q;不是LCD的Still或大字结果。本程序仍接收状态0的有效数值,不能据此证明姿态精度。 |
| sh2_estimated_us / anomalies | 主机接收与报告延迟推算的时间 / 程序汇总异常。推算时间诊断另记,见下文,不能把0解释成所有诊断都为0。 |
③ 68.390秒结束记录:通信观察成立,整轮未完成
EV,boot_ms=69717,test_ms=69024,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=FINISHED,event=FINISH,reason=REPLACED_BY_OPERATOR_LABEL,test_duration_ms=69024
EV,boot_ms=69738,test_ms=69024,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=FINISHED,event=SUMMARY,overall=INCOMPLETE,communication=PRODUCT_RESPONSE,stream=OBSERVED,stream_ms=68390,anomalies=0,still=COARSE_WINDOW,X=NOT_RUN,Y=NOT_RUN,Z=NOT_RUN
EV,boot_ms=69831,test_ms=69024,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=FINISHED,event=TRANSPORT,read=21044,read_error=0,write=0,write_error=0,int_wait_timeout=0,host_hw_reset=0,device_reset_event=0,decode_or_frame_error=0,oversize=0,report_enable_error=0V1.txt 原行 135、137、141 的选摘;非连续行没有拼成一条新记录。
read=21044是I²C读取次数,read_error=0是读取错误次数,两者不能混淆。该摘要的写错误、INT等待超时、解码/结构异常、过大包、启用错误、运行复位均为0;三类报告的无效、断流、缺序号、重复也均为0。
test_duration_ms=69024包含初始化;stream_ms=68390是68.390秒有效流观察。stream=OBSERVED满足该通信观察窗口,但overall=INCOMPLETE和X/Y/Z=NOT_RUN明确说明三轴没有做。FINISH原因是人工标签接管,不能写成三个模块或全部功能通过。
展开完整报告诊断:有一个诊断计数并不是零
EV,boot_ms=69759,test_ms=69024,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=FINISHED,event=REPORT_SUMMARY,type=0,valid=4406,invalid=0,timeout_episodes=0,sequence_missing=0,duplicates=0,sh2_time_nonmonotonic=49,max_gap_ms=61,mean_hz=64.42,last_accuracy=2
EV,boot_ms=69779,test_ms=69024,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=FINISHED,event=REPORT_SUMMARY,type=1,valid=3413,invalid=0,timeout_episodes=0,sequence_missing=0,duplicates=0,sh2_time_nonmonotonic=2,max_gap_ms=87,mean_hz=49.90,last_accuracy=3
EV,boot_ms=69800,test_ms=69024,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=FINISHED,event=REPORT_SUMMARY,type=2,valid=3413,invalid=0,timeout_episodes=0,sequence_missing=0,duplicates=0,sh2_time_nonmonotonic=2,max_gap_ms=87,mean_hz=49.90,last_accuracy=0
EV,boot_ms=69852,test_ms=69024,module=AUTO_7012DC1D,run=7012DC1D-0001,stage=FINISHED,event=INIT_TRANSPORT,read=10,read_error=0,write=5,write_error=0,int_wait_timeout=0,planned_host_hw_reset=1,decode_or_frame_error=0,oversize=0V1.txt 原行 138、139、140、142 的选摘;非连续行没有拼成一条新记录。
type=0/1/2依次为A/G/Q;sh2_time_nonmonotonic=49/2/2不是零。源码把SH-2时间视作主机接收时间与延迟的估计,单独记录倒退次数,没有加进 anomalyCount,所以原来的 anomalies=0可以与它同时出现。保留这项诊断,不据此断言器件时钟坏了。初始化 planned_host_hw_reset=1是计划复位,不是LCD的意外Rst错误。
④ 最新保留会话:仍有数据,但静置需要复核且未收尾
EV,boot_ms=21330,test_ms=20637,module=AUTO_4B9E7573,run=4B9E7573-0001,stage=STILL,event=STILL_RESULT,result=REVIEW,acc_n=967,gyro_n=749,g_mean_ms2=9.4523,g_std_ms2=0.3056,gyro_norm_peak_rads=3.3680
EV,boot_ms=21802,test_ms=21109,module=AUTO_4B9E7573,run=4B9E7573-0001,stage=READY,event=SUMMARY,overall=REVIEW,communication=PRODUCT_RESPONSE,stream=INCOMPLETE,stream_ms=20475,anomalies=0,still=REVIEW,X=NOT_RUN,Y=NOT_RUN,Z=NOT_RUN
EV,boot_ms=23649,test_ms=22956,module=AUTO_4B9E7573,run=4B9E7573-0001,stage=READY,event=SAMPLE,a_ms2=-8.320/-4.367/0.766,g_dps=0.00/0.00/0.00,rpy_deg=-80.1/62.0/-6.0,hz=64.9/49.9/49.9,valid=1437/1112/1112,age_ms=6/3/0,sh2_estimated_us=23638738/23639938/23640338,accuracy=2/3/0,anomalies=0V1.txt 原行 216、221、228 的选摘;非连续行没有拼成一条新记录。
最新摘要是 stream_ms=20475,即20.475秒,静置与整体都是REVIEW,XYZ未做。静置标准差0.3056m/s²与角速度模长峰值3.3680rad/s未满足粗测窗口;具体动作/环境原因未确定,不能据此直接归因模块损坏。
之后最后一条SAMPLE的 test_ms=22956表示会话运行到22.956秒,Valid增至1437/1112/1112,证明摘要后仍有更新;它不是新的stream_ms摘要或新的通过结果。采集器最后记录“Closed without FINISH”,没有完整结束记录。该轮保留REVIEW/INCOMPLETE范围。
原V1.txt第229行为采集器说明:Closed without FINISH; final new AUTO session left streaming. 关闭的是采集工具;未连接之后的数据无法事后补回。
原始V1.txt共229条,项目原文件未改;SHA-256:72a71b77bdc8615ca3114ccfe42dd2d2e2d8c2629d811be8824ebcbaf88706d8。公开附件只包含选摘正文、原行号和审核边界,不含本机用户名、目录或原Flash。
Observed scope · 2026-10-04
实机记录:通信已观察,完整阶段仍待完成
本项目实测 当前固件为 V1.7,主板是用户确认的 CYD ESP32-2432S028R / ST7789。上电自动创建新会话,屏幕实时更新;操作员开始贴签测试时,旧 AUTO 先结束,再清零建立新的 run。AUTO 和 M001 都是软件或人工标签,不是芯片唯一序列号。
| 版本 / 独立 run | 实际保存的观测 | 原结果与限制 |
|---|---|---|
| V1.7 / 79C93EE4-0001 | 直接 INFO 时已经自动采集,71.951 s 快照异常 0 | 流 OBSERVED,整体 INCOMPLETE;连接前的启动输出未补造 |
| V1.7 / 7012DC1D-0001 | 流 68.390 s;A/G/Q 有效报告 4406/3413/3413,均值 64.42/49.90/49.90 Hz;I²C 读取 21044 次,读错误、无效、断流、缺序号、重复及其他传输异常均 0 | 静置 COARSE_WINDOW;流 OBSERVED,整体 INCOMPLETE;三轴未执行。被人工标签接管时有 FINISH |
| V1.7 / 7012DC1D-0002 | HANDOFF_CHECK 接管后计数独立;4.188 s 快照,异常 0 | 随后受控重启,缺 FINISH;静置和三轴未做,未完整收尾 |
| V1.7 / 4B9E7573-0001 | 新 AUTO 的 20.475 s 摘要,指定传输 / 序列异常 0;摘要后最后 SAMPLE test_ms=22956、Valid1437/1112/1112,未出现新 SUMMARY 或 FINISH;静置加速度模长标准差 0.3056 m/s²,角速度模长峰值 3.3680 rad/s | 静置 REVIEW、整体 REVIEW;流 INCOMPLETE、三轴未做、未 FINISH。具体原因未确定 |
| V1.6 / 9B1EA1B9-0001(历史) | 流 186.755 s;A/G/Q 12040/9327/9327,64.47/49.94/49.94 Hz,通信和序列异常 0 | 静置 COARSE_WINDOW,流 OBSERVED、整体 INCOMPLETE;三轴未做,保留独立历史记录 |
| 软件 / 硬件 | 当前项目记录 |
|---|---|
| 目标显示 / 通信 | ST7789;地址 0x4B,SDA27 / SCL22 / INT35 / RST19,400 kHz |
| 构建环境 | PlatformIO espressif32 6.13.0 / Arduino-ESP32 2.0.17;ST7789 与 ILI9341 备用环境均编译成功,实际刷写 ST7789 |
| 软件验证 | 原项目主程序 62 + HAL 41,共 103 项断言 0 失败;日志归属检查 5 项通过。软件回归不替代硬件观测 |
| 已刷 V1.7 应用 SHA-256 | d0ddd940a04b922dfff1a24245cc6f5b08cebe7e3e038cc6aaf62ccaa3e4142f |
Fusion · concept only
六轴、九轴融合:姿态参考从哪里来
| 报告 | 使用传感输入 | 朝向参考和限制 |
|---|---|---|
| Game Rotation Vector · 六轴 | 加速度计 + 陀螺仪 | 横滚、俯仰参考重力;偏航没有磁北参考,时间长了可能漂移 |
| Rotation Vector · 九轴 | 加速度计 + 陀螺仪 + 磁力计 | 参考重力和磁北;受周边磁场、安装、校准条件影响,不能视作任意环境下的精确指南针 |
概念动画 · 非实测数据
- 加速度:重力方向
- 陀螺仪:转动变化
- 磁力计:磁北参考
- 融合:输出四元数
九轴报告使用磁力计修正航向;本 V1.7 订阅的是这一类。
Accelerometer 含重力;Gravity 与 Linear Acceleration 分别描述重力分量和去重力后的运动分量。必须在同一坐标系里理解它们,静止时加速度三轴不一定全为零。当前工程没有独立订阅后两项,下面动画中的运动和数值只用来讲解。
60 秒概念片为示意,未合成旁白、没有模拟实测曲线。采用项目交付的修订版,已将早期轮询说法限定为本 V1.7 的 INT 读取流程;新版轮询边界参考 CEVA v1.17。
Quaternion → RPY
四元数表示朝向,RPY 便于人读
项目写作 q=(w,x,y,z):w 为实部,x/y/z 对应虚部 i/j/k。它表示模块整体的旋转姿态,不是空间位置。RPY 是横滚 Roll、俯仰 Pitch、偏航 Yaw;源码将四元数按固定公式转换成度。实物轴向应根据板上丝印和安装方向核对,不能根据录像里“向左转”猜正负号。
交互示意 · 仅绕示意 X 轴 · 非实测数据
q=(1.000, 0.000, 0.000, 0.000)
RPY=(0°, 0°, 0°)
纯 X 轴旋转:q=(cos(θ/2), sin(θ/2), 0, 0)。二维板形只是朝向示意,没有接入传感器。
例如 θ=90° 时,q≈(0.707, 0.707, 0, 0),对应本约定下横滚约 90°。q 和 −q 表示同一个朝向;欧拉角接近特定姿态时会遇到表达歧义,因此做连续旋转计算通常保留四元数。三张录像关键帧不能补成“实测连续姿态曲线”。SH-2 参考手册。
Three different states
校准状态、静置粗测、测试完成度要分开读
| 状态 | 具体含义 | 不能推导什么 |
|---|---|---|
| 报告 Status 0 / 1 / 2 / 3 | 芯片估计当前报告的准确性:不可靠 / 低 / 中 / 高;源码读取 status 的低两位 | 等级 3 不是计量证书,也不证明所有报告或整个模块都已校准 |
| StillCHECK / COARSE_WINDOW / REVIEW | 本测试程序检查静置时加速度模长和角速度的粗测窗口 | 它不是 SH-2 的校准等级;静置满足窗口不等于全性能合格 |
| XYZ--- / NOT_RUN / TESTING | 程序阶段完成度:三轴尚未完成、会话仍测试中 | 看到实时数值变化不等于已执行三轴测试 |
动态校准用于补偿偏置等因素,动作与配置要按报告类型和安装场景执行。每只实物分别记录;“calibrated gyroscope”是报告类型的名字,不能据此宣称这只模块通过外部精度校验。本工程没有启用或完成 BNO086 的 Interactive Calibration,也没有提供保存校准数据的操作步骤。
校准动作参考 CEVA / Hillcrest 校准说明;BNO086 专有流程以 v1.17 数据手册 为准。磁性物件、电流回路和安装变化可能影响带磁北参考的姿态,应保留环境说明并重新核验。
INT / RST / SHTP
INT 提醒主机,RST 建立启动边界
INT 低有效,提醒主机处理芯片数据;RST 低有效,用于启动复位。本工程依次初始化、订阅报告,再按 INT 配合读取 SHTP 头部和载荷,通过通道、报告类型与序号解码。ACK 只说明某地址应答,不能代替有效报告和持续流测试。
流程演示 · 不按真实时序比例
- 复位 / 启动
- 配置三类报告
- INT 低:提醒处理
- 读头部 / 完整载荷
- 解码 / 显示 / 日志
INT 可在识别器件地址后撤销,不能把它当成“剩余字节数”。
依据 CEVA v1.17 §1.2.2.2 / I²C 时序、SHTP 协议,以及本工程已修补的 HAL;本页没有实测波形或响应延迟承诺。
PlatformIO · V1.7 source
查看与复制代码:完整工程按下载包构建
默认环境是 cyd_st7789,备用 cyd_ili9341 只对应其他屏幕。程序使用项目内修补库:每包全部报告都参与计数,INT 下降沿 epoch 配合有界读取,显示和串口摘要之间继续服务数据。不要用未修补的同名库替换后再套用本页测试记录。
main.cpp SHA-256:33c583346621097bf7f342dfedc59cac4b4b4c53c29aac42585ba75d96be572b
#include <Arduino.h>
#include <Wire.h>
#include <TFT_eSPI.h>
#include <U8g2_for_TFT_eSPI.h>
#include <SparkFun_BNO08x_Arduino_Library.h>
#include <esp_system.h>
#include <cstdarg>
#include "test_config.h"
#include "evidence_rules.h"
TFT_eSPI tft;
U8g2_for_TFT_eSPI chinese;
BNO08x imu;
enum class Stage { Idle, Initializing, Warmup, Still, Ready, Motion, Finished };
struct Report {
uint32_t good=0, bad=0, gaps=0, missing=0, duplicate=0, maxGap=0, timestampBack=0;
uint32_t lastMs=0, rateBase=0;
uint64_t sensorUs=0;
uint8_t seq=0, accuracy=0;
bool seen=false, timedOut=false;
float hz=0;
};
struct Transport {
uint32_t reads=0, readErrors=0, writes=0, writeErrors=0;
uint32_t intTimeouts=0, hardwareResets=0, decodeErrors=0, oversized=0;
};
Report report[3];
Transport transport, initTransport;
Stage stage=Stage::Idle;
bool active=false, initialized=false, productResponded=false, armed=false, fault=false;
bool automaticSession=false;
bool inSensorCallback=false;
bool stillDone=false, stillOK=false, motionDone[3]={}, motionOK[3]={};
uint32_t bootId=0, sessionNo=0, started=0, streamStart=0, stageStart=0;
uint32_t ended=0, unexpectedResets=0, enableErrors=0, lastTick=0, lastDraw=0;
char module[25]="UNASSIGNED", runId[32]="", lineBuffer[96];
size_t lineLength=0;
bool lineOverflow=false;
int motionAxis=-1;
float accel[3]={}, gyro[3]={}, quat[4]={}, rpy[3]={};
float stillSum=0, stillSumSq=0, stillGyroPeak=0, stillMean=0, stillStd=0;
uint32_t stillAccelN=0, stillGyroN=0;
float motionPeak=0, motionAngle=0, motionBaseQuat[4]={};
uint32_t motionGyroN=0, motionQuatN=0;
bool motionHasBase=false;
void drawScreen();
const char *stageName() {
switch(stage) {
case Stage::Idle:return "IDLE"; case Stage::Initializing:return "INIT";
case Stage::Warmup:return "WARMUP"; case Stage::Still:return "STILL";
case Stage::Ready:return "READY"; case Stage::Motion:return "MOTION";
default:return "FINISHED";
}
}
void logEvent(const char *event, const char *fmt="", ...) {
char detail[640]; va_list args; va_start(args,fmt);
vsnprintf(detail,sizeof(detail),fmt,args); va_end(args);
const uint32_t now=millis();
Serial.printf("EV,boot_ms=%lu,test_ms=%lu,module=%s,run=%s,stage=%s,event=%s,%s\n",
(unsigned long)now,(unsigned long)(active?now-started:stage==Stage::Finished?ended-started:0),
module,runId,stageName(),event,detail);
// Serial summaries may span many lines; drain data after each line, not after
// the entire summary. Never reenter SH2 from a sensor callback's diagnostic log.
static bool draining=false;
if(active&&initialized&&!draining&&!inSensorCallback) {
draining=true;
for(int n=0;n<8&&digitalRead(BNO_INT)==LOW;++n) {sh2_service();yield();}
draining=false;
}
}
// Local vendor hooks count every decoded event, including multiple events in one SHTP packet.
// They do no serial IO. This avoids silently retaining only the last event in a packet.
void bno08xDiagnosticTransport(uint8_t code) {
Transport &t=armed?transport:initTransport;
switch(code) {
case 1:++t.reads;break; case 2:++t.readErrors;break;
case 3:++t.writes;break;case 4:++t.writeErrors;break;
case 5:++t.intTimeouts;break;case 6:++t.hardwareResets;break;
case 7:++t.decodeErrors;break;case 8:++t.oversized;break;
}
if (armed && (code==2||code==4||code==5||code==6||code==7||code==8)) fault=true;
}
void bno08xDiagnosticEvent(const sh2_SensorValue_t *v) {
if(!armed||!active) return;
int k=-1; float values[4]; int count=3;
if(v->sensorId==SH2_ACCELEROMETER) {
k=0;values[0]=v->un.accelerometer.x;values[1]=v->un.accelerometer.y;values[2]=v->un.accelerometer.z;
} else if(v->sensorId==SH2_GYROSCOPE_CALIBRATED) {
k=1;values[0]=v->un.gyroscope.x;values[1]=v->un.gyroscope.y;values[2]=v->un.gyroscope.z;
} else if(v->sensorId==SH2_ROTATION_VECTOR) {
k=2;count=4;values[0]=v->un.rotationVector.real;values[1]=v->un.rotationVector.i;
values[2]=v->un.rotationVector.j;values[3]=v->un.rotationVector.k;
} else return;
Report &r=report[k]; const uint32_t now=millis();
bool valid=finiteVector(values,count,k==0?160.0f:k==1?36.0f:1.15f);
float norm=0;
if(k==2) {
for(int i=0;i<4;++i) norm+=values[i]*values[i];
norm=sqrtf(norm);valid=valid&&norm>=0.85f&&norm<=1.15f;
}
if(r.seen) {
const uint8_t step=(uint8_t)(v->sequence-r.seq);
if(step==0) {++r.duplicate;valid=false;}
else if(step!=1) {r.missing+=(uint8_t)(step-1);fault=true;}
// SH2 timestamps are estimates from host receive time and report delay, not a sensor clock.
if(v->timestamp<=r.sensorUs)++r.timestampBack;
}
if(!valid) {++r.bad;fault=true;return;}
const uint32_t gap=now-(r.seen?r.lastMs:streamStart);
if(gap>r.maxGap)r.maxGap=gap;
if(gap>GAP_MS&&!r.timedOut) {++r.gaps;fault=true;}
r.timedOut=false;r.lastMs=now;r.sensorUs=v->timestamp;r.seq=v->sequence;
r.accuracy=v->status&3;r.seen=true;++r.good;
if(k==0) {
memcpy(accel,values,sizeof(accel));
if(stage==Stage::Still) {
const float g=sqrtf(accel[0]*accel[0]+accel[1]*accel[1]+accel[2]*accel[2]);
stillSum+=g;stillSumSq+=g*g;++stillAccelN;
}
} else if(k==1) {
memcpy(gyro,values,sizeof(gyro));
if(stage==Stage::Still) {
const float w=sqrtf(gyro[0]*gyro[0]+gyro[1]*gyro[1]+gyro[2]*gyro[2]);
stillGyroPeak=fmaxf(stillGyroPeak,w);++stillGyroN;
}
if(stage==Stage::Motion) {motionPeak=fmaxf(motionPeak,fabsf(gyro[motionAxis]));++motionGyroN;}
} else {
for(int i=0;i<4;++i)quat[i]=values[i]/norm;
const float w=quat[0],x=quat[1],y=quat[2],z=quat[3];
rpy[0]=atan2f(2*(w*x+y*z),1-2*(x*x+y*y))*RAD_TO_DEG;
rpy[1]=asinf(fmaxf(-1,fminf(1,2*(w*y-z*x))))*RAD_TO_DEG;
rpy[2]=atan2f(2*(w*z+x*y),1-2*(y*y+z*z))*RAD_TO_DEG;
if(stage==Stage::Motion) {
++motionQuatN;
if(!motionHasBase) {memcpy(motionBaseQuat,quat,sizeof(quat));motionHasBase=true;}
float dot=0;for(int i=0;i<4;++i)dot+=quat[i]*motionBaseQuat[i];
motionAngle=fmaxf(motionAngle,2*acosf(fminf(1,fabsf(dot)))*RAD_TO_DEG);
}
}
}
void onSensorReport(void *,sh2_SensorEvent_t *event) {
inSensorCallback=true;
sh2_SensorValue_t value{};
if(sh2_decodeSensorEvent(&value,event)!=SH2_OK) {
bno08xDiagnosticTransport(7);
static uint8_t details=0;
if(details<8) {++details;logEvent("DECODE_ERROR","report_id=%u,len=%u",event->reportId,event->len);}
inSensorCallback=false;return;
}
bno08xDiagnosticEvent(&value);
inSensorCallback=false;
}
uint32_t elapsedStream() {return initialized?(active?millis():ended)-streamStart:0;}
uint32_t anomalyCount() {
uint32_t n=transport.readErrors+transport.writeErrors+transport.intTimeouts+
transport.hardwareResets+transport.decodeErrors+transport.oversized+unexpectedResets+enableErrors;
for(auto &r:report)n+=r.bad+r.gaps+r.missing;
return n+(fault&&n==0?1:0); // Includes initialization anomalies latched before streaming.
}
Evidence streamResult() {
uint32_t counts[3]={report[0].good,report[1].good,report[2].good};
return streamEvidence(initialized,elapsedStream(),MIN_STREAM_MS,anomalyCount(),counts,
MIN_OBSERVED_HZ,MAX_OBSERVED_HZ);
}
Evidence result() {return overallEvidence(streamResult(),stillDone,stillOK,motionDone,motionOK);}
const char *evidenceName(Evidence e) {
return e==Evidence::Observed?"OBSERVED":e==Evidence::Review?"REVIEW":"INCOMPLETE";
}
bool reportsOn() {
bool a=imu.enableReport(SH2_ACCELEROMETER,REPORT_US);
bool g=imu.enableReport(SH2_GYROSCOPE_CALIBRATED,REPORT_US);
bool q=imu.enableReport(SH2_ROTATION_VECTOR,REPORT_US);
if(!a||!g||!q){++enableErrors;fault=true;}
logEvent("REPORT_ENABLE","acc=%d,gyro=%d,quat=%d,request_us=%lu",a,g,q,(unsigned long)REPORT_US);
return a&&g&&q;
}
void checkTimeouts() {
const uint32_t now=millis();
for(int i=0;i<3;++i) {
Report &r=report[i];
const uint32_t gap=now-(r.seen?r.lastMs:streamStart);
if(gap>r.maxGap)r.maxGap=gap;
if(gap>GAP_MS&&!r.timedOut) {
r.timedOut=true;++r.gaps;fault=true;
logEvent("REPORT_TIMEOUT","type=%d,gap_ms=%lu",i,(unsigned long)gap);
}
}
}
void summary() {
logEvent("STATE","active=%d,initialized=%d,reports_armed=%d,int_level=%d,session_mode=%s",
active,initialized,armed,digitalRead(BNO_INT),!runId[0]?"none":automaticSession?"automatic_boot":"operator");
logEvent("SUMMARY","overall=%s,communication=%s,stream=%s,stream_ms=%lu,anomalies=%lu,still=%s,X=%s,Y=%s,Z=%s",
evidenceName(result()),productResponded?"PRODUCT_RESPONSE":"NOT_OBSERVED",evidenceName(streamResult()),
(unsigned long)elapsedStream(),(unsigned long)anomalyCount(),
stillDone?(stillOK?"COARSE_WINDOW":"REVIEW"):"NOT_RUN",
motionDone[0]?(motionOK[0]?"OBSERVED":"REVIEW"):"NOT_RUN",
motionDone[1]?(motionOK[1]?"OBSERVED":"REVIEW"):"NOT_RUN",
motionDone[2]?(motionOK[2]?"OBSERVED":"REVIEW"):"NOT_RUN");
for(int i=0;i<3;++i) {
const Report &r=report[i];
logEvent("REPORT_SUMMARY","type=%d,valid=%lu,invalid=%lu,timeout_episodes=%lu,sequence_missing=%lu,duplicates=%lu,sh2_time_nonmonotonic=%lu,max_gap_ms=%lu,mean_hz=%.2f,last_accuracy=%u",
i,(unsigned long)r.good,(unsigned long)r.bad,(unsigned long)r.gaps,(unsigned long)r.missing,
(unsigned long)r.duplicate,(unsigned long)r.timestampBack,(unsigned long)r.maxGap,elapsedStream()?r.good*1000.0f/elapsedStream():0,r.accuracy);
}
logEvent("TRANSPORT","read=%lu,read_error=%lu,write=%lu,write_error=%lu,int_wait_timeout=%lu,host_hw_reset=%lu,device_reset_event=%lu,decode_or_frame_error=%lu,oversize=%lu,report_enable_error=%lu",
(unsigned long)transport.reads,(unsigned long)transport.readErrors,(unsigned long)transport.writes,
(unsigned long)transport.writeErrors,(unsigned long)transport.intTimeouts,(unsigned long)transport.hardwareResets,
(unsigned long)unexpectedResets,(unsigned long)transport.decodeErrors,(unsigned long)transport.oversized,(unsigned long)enableErrors);
logEvent("INIT_TRANSPORT","read=%lu,read_error=%lu,write=%lu,write_error=%lu,int_wait_timeout=%lu,planned_host_hw_reset=%lu,decode_or_frame_error=%lu,oversize=%lu",
(unsigned long)initTransport.reads,(unsigned long)initTransport.readErrors,(unsigned long)initTransport.writes,
(unsigned long)initTransport.writeErrors,(unsigned long)initTransport.intTimeouts,(unsigned long)initTransport.hardwareResets,
(unsigned long)initTransport.decodeErrors,(unsigned long)initTransport.oversized);
}
void finishSession(const char *reason) {
if(!active)return;
if(initialized)checkTimeouts();
ended=millis();active=false;armed=false;stage=Stage::Finished;
logEvent("FINISH","reason=%s,test_duration_ms=%lu",reason,(unsigned long)(ended-started));
summary();
}
bool validId(const char *id) {
const size_t n=strlen(id);if(!n||n>24)return false;
for(size_t i=0;i<n;++i)if(!isalnum((unsigned char)id[i])&&id[i]!='_'&&id[i]!='-')return false;
return true;
}
void startSession(const char *id,bool automatic=false) {
if(!validId(id)){logEvent("COMMAND_REJECT","reason=id_1_to_24_ASCII_letters_digits_dash_underscore");return;}
if(active) {
if(automaticSession&&!automatic)finishSession("REPLACED_BY_OPERATOR_LABEL");
else {logEvent("COMMAND_REJECT","reason=finish_current_session_first");return;}
}
sh2_close();
for(auto &r:report)r=Report{};
transport=Transport{};initTransport=Transport{};unexpectedResets=enableErrors=0;
initialized=productResponded=armed=fault=stillDone=stillOK=false;motionAxis=-1;
memset(motionDone,0,sizeof(motionDone));memset(motionOK,0,sizeof(motionOK));
memset(accel,0,sizeof(accel));memset(gyro,0,sizeof(gyro));memset(quat,0,sizeof(quat));memset(rpy,0,sizeof(rpy));
stillSum=stillSumSq=stillGyroPeak=stillMean=stillStd=0;stillAccelN=stillGyroN=0;
strlcpy(module,id,sizeof(module));
automaticSession=automatic;
snprintf(runId,sizeof(runId),"%08lX-%04lu",(unsigned long)bootId,(unsigned long)++sessionNo);
active=true;started=millis();stage=Stage::Initializing;
drawScreen();
logEvent("START","fw=%s,address=0x%02X,SDA=%d,SCL=%d,INT=%d,RST=%d,I2C_Hz=%lu,id_source=%s,unique_serial=not_read",
FW_ID,BNO_ADDRESS,BNO_SDA,BNO_SCL,BNO_INT,BNO_RST,(unsigned long)I2C_HZ,
automaticSession?"automatic_boot_label":"operator_label");
// Dedicated bus: probe the selected address only; an ACK never constitutes a result.
Wire.beginTransmission(BNO_ADDRESS);uint8_t ack=Wire.endTransmission();
logEvent("I2C_PROBE","address=0x%02X,wire_result=%u",BNO_ADDRESS,ack);
if(!imu.begin(BNO_ADDRESS,Wire,BNO_INT,BNO_RST)) {
logEvent("INIT_FAILED","reason=no_valid_SH2_product_response");finishSession("INIT_FAILED");return;
}
if(!imu.prodIds.numEntries) {logEvent("INIT_FAILED","reason=empty_product_response");finishSession("INIT_FAILED");return;}
productResponded=true;
sh2_setSensorCallback(onSensorReport,nullptr);
for(unsigned i=0;i<imu.prodIds.numEntries;++i) {
const auto &p=imu.prodIds.entry[i];
logEvent("PRODUCT","entry=%u,sw_part=%lu,version=%u.%u.%u,build=%lu,reset_cause=%u,unique_serial=not_available",
i,(unsigned long)p.swPartNumber,p.swVersionMajor,p.swVersionMinor,p.swVersionPatch,(unsigned long)p.swBuildNumber,p.resetCause);
}
if(!reportsOn()){finishSession("REPORT_ENABLE_FAILED");return;}
(void)imu.wasReset();
logEvent("INIT_TRANSPORT","read_error=%lu,write_error=%lu,int_wait_timeout=%lu,host_hw_reset=%lu",
(unsigned long)initTransport.readErrors,(unsigned long)initTransport.writeErrors,
(unsigned long)initTransport.intTimeouts,(unsigned long)initTransport.hardwareResets);
// Initialization contains planned resets; errors/timeouts still require review.
if(initTransport.readErrors||initTransport.writeErrors||initTransport.intTimeouts||initTransport.decodeErrors||initTransport.oversized)fault=true;
initialized=true;armed=true;streamStart=stageStart=millis();stage=Stage::Warmup;
logEvent("STAGE_BEGIN","duration_ms=%lu,instruction=place_module_still",(unsigned long)WARMUP_MS);
}
void startAutomaticSession() {
char id[25];
snprintf(id,sizeof(id),"AUTO_%08lX",(unsigned long)bootId);
startSession(id,true);
}
void startMotion(int axis) {
if(!active||stage!=Stage::Ready){logEvent("COMMAND_REJECT","reason=wait_for_READY");return;}
// Repeating a stage never overwrites its prior outcome; use a new session for retest.
if(motionDone[axis]){logEvent("COMMAND_REJECT","reason=axis_already_recorded_start_new_session_for_retest");return;}
motionAxis=axis;motionPeak=motionAngle=0;motionGyroN=motionQuatN=0;motionHasBase=false;
stage=Stage::Motion;stageStart=millis();logEvent("STAGE_BEGIN","axis=%c,duration_ms=%lu,instruction=rotate_about_module_axis_slowly",'X'+axis,(unsigned long)MOVE_MS);
}
void updateStages() {
const uint32_t dt=millis()-stageStart;
if(stage==Stage::Warmup&&dt>=WARMUP_MS) {
stage=Stage::Still;stageStart=millis();logEvent("STAGE_BEGIN","duration_ms=%lu,instruction=do_not_move",(unsigned long)STILL_MS);
} else if(stage==Stage::Still&&dt>=STILL_MS) {
stillDone=true;stillMean=stillAccelN?stillSum/stillAccelN:0;
stillStd=stillAccelN?sqrtf(fmaxf(0,stillSumSq/stillAccelN-stillMean*stillMean)):0;
stillOK=stillAccelN>=STILL_MS*MIN_OBSERVED_HZ/1000&&stillGyroN>=STILL_MS*MIN_OBSERVED_HZ/1000&&
stillMean>=8.3f&&stillMean<=11.3f&&stillStd<=0.25f&&stillGyroPeak<=0.15f;
logEvent("STILL_RESULT","result=%s,acc_n=%lu,gyro_n=%lu,g_mean_ms2=%.4f,g_std_ms2=%.4f,gyro_norm_peak_rads=%.4f",
stillOK?"COARSE_WINDOW":"REVIEW",(unsigned long)stillAccelN,(unsigned long)stillGyroN,stillMean,stillStd,stillGyroPeak);
stage=Stage::Ready;logEvent("READY","instruction=send_X_then_Y_then_Z_each_8_seconds");
} else if(stage==Stage::Motion&&dt>=MOVE_MS) {
motionDone[motionAxis]=true;
motionOK[motionAxis]=motionGyroN>=MOVE_MS*MIN_OBSERVED_HZ/1000&&motionQuatN>=MOVE_MS*MIN_OBSERVED_HZ/1000&&motionPeak>=0.6f&&motionAngle>=20;
logEvent("MOTION_RESULT","axis=%c,result=%s,gyro_n=%lu,quat_n=%lu,axis_gyro_peak_rads=%.4f,orientation_change_deg=%.2f",
'X'+motionAxis,motionOK[motionAxis]?"OBSERVED":"REVIEW",(unsigned long)motionGyroN,(unsigned long)motionQuatN,motionPeak,motionAngle);
stage=Stage::Ready;
}
}
void info() {
Serial.printf("FW,%s,title=BNO086 module test,board=ESP32-2432S028R,display=%s,address=0x%02X,boot=%08lX\n",FW_ID,
#ifdef ST7789_DRIVER
"ST7789",
#else
"ILI9341",
#endif
BNO_ADDRESS,(unsigned long)bootId);
Serial.println("HELP,START M001 | X | Y | Z | STATUS | FINISH | INFO; disconnect power before changing modules");
logEvent("STATE","active=%d,initialized=%d,reports_armed=%d,int_level=%d,session_mode=%s,boot_action=new_automatic_session",
active,initialized,armed,digitalRead(BNO_INT),!runId[0]?"none":automaticSession?"automatic_boot":"operator");
}
void command(const char *s) {
if(!strncmp(s,"START ",6))startSession(s+6);
else if(!strcmp(s,"X"))startMotion(0);else if(!strcmp(s,"Y"))startMotion(1);else if(!strcmp(s,"Z"))startMotion(2);
else if(!strcmp(s,"FINISH"))finishSession("OPERATOR_FINISH");
else if(!strcmp(s,"STATUS")){if(initialized&&active)checkTimeouts();summary();}
else if(!strcmp(s,"INFO"))info();
else logEvent("COMMAND_REJECT","reason=unknown_command");
}
void readCommands() {
while(Serial.available()) {
const char c=Serial.read();
if(c=='\r')continue;
if(c=='\n') {
if(lineOverflow)logEvent("COMMAND_REJECT","reason=line_too_long");
else if(lineLength){lineBuffer[lineLength]='\0';command(lineBuffer);}
lineLength=0;lineOverflow=false;
} else if(lineLength<sizeof(lineBuffer)-1)lineBuffer[lineLength++]=c;
else lineOverflow=true;
}
}
void telemetry() {
if(stage==Stage::Finished){lastTick=millis();return;}
const uint32_t now=millis();const uint32_t dt=now-lastTick;
for(auto &r:report){r.hz=dt?(r.good-r.rateBase)*1000.0f/dt:0;r.rateBase=r.good;}
lastTick=now;
if(!active||!initialized)return;
logEvent("SAMPLE","a_ms2=%.3f/%.3f/%.3f,g_dps=%.2f/%.2f/%.2f,rpy_deg=%.1f/%.1f/%.1f,hz=%.1f/%.1f/%.1f,valid=%lu/%lu/%lu,age_ms=%lu/%lu/%lu,sh2_estimated_us=%llu/%llu/%llu,accuracy=%u/%u/%u,anomalies=%lu",
accel[0],accel[1],accel[2],gyro[0]*RAD_TO_DEG,gyro[1]*RAD_TO_DEG,gyro[2]*RAD_TO_DEG,rpy[0],rpy[1],rpy[2],report[0].hz,report[1].hz,report[2].hz,
(unsigned long)report[0].good,(unsigned long)report[1].good,(unsigned long)report[2].good,
(unsigned long)(now-(report[0].seen?report[0].lastMs:streamStart)),(unsigned long)(now-(report[1].seen?report[1].lastMs:streamStart)),(unsigned long)(now-(report[2].seen?report[2].lastMs:streamStart)),
(unsigned long long)report[0].sensorUs,(unsigned long long)report[1].sensorUs,(unsigned long long)report[2].sensorUs,
report[0].accuracy,report[1].accuracy,report[2].accuracy,(unsigned long)anomalyCount());
}
void text(int y,const char *s,int font=2,uint16_t color=TFT_WHITE) {
// Opaque glyph backgrounds and padding remove the previous line's trailing text.
tft.setTextColor(color,TFT_BLACK);tft.setTextPadding(304);tft.drawString(s,6,y,font);
// Drain ready data between rows so display drawing cannot delay an entire frame.
if(active&&initialized&&digitalRead(BNO_INT)==LOW) sh2_service();
}
void drawScreen() {
static bool painted=false, previousLayoutInitialized=false;
char s[112];
// Clear only when the two layouts change; regular updates never blank the full screen.
if(!painted||previousLayoutInitialized!=initialized) {
tft.fillScreen(TFT_BLACK);
chinese.setForegroundColor(TFT_WHITE);chinese.drawUTF8(6,17,"BNO086 模块测试");
painted=true;previousLayoutInitialized=initialized;
}
snprintf(s,sizeof(s),"ID:%s run:%04lu",module,(unsigned long)sessionNo);text(21,s);
uint32_t remaining=0;uint32_t dt=millis()-stageStart;
const uint32_t limit=stage==Stage::Warmup?WARMUP_MS:stage==Stage::Still?STILL_MS:stage==Stage::Motion?MOVE_MS:0;
if(limit&&dt<limit)remaining=(limit-dt+999)/1000;
snprintf(s,sizeof(s),"%s %c %lus 0x%02X %s",stageName(),stage==Stage::Motion?'X'+motionAxis:' ',(unsigned long)remaining,BNO_ADDRESS,
initialized?"SH2 OK":stage==Stage::Idle?"WAIT START":stage==Stage::Initializing?"STARTING":"NO SH2");text(39,s,2);
if(!initialized) {
text(68,stage==Stage::Finished?"INIT INCOMPLETE":stage==Stage::Initializing?"Starting sensor...":"Waiting for START",4);
text(108,"SDA27 SCL22 INT35 RST19");text(130,"GPIO21 = BACKLIGHT");
text(158,"TF card OUT / power 3.3V");text(204,"INFO / STATUS / FINISH");return;
}
const uint32_t now=active?millis():ended;
const bool freshA=report[0].seen&&now-report[0].lastMs<=GAP_MS;
const bool freshG=report[1].seen&&now-report[1].lastMs<=GAP_MS;
const bool freshQ=report[2].seen&&now-report[2].lastMs<=GAP_MS;
snprintf(s,sizeof(s),"A m/s2: %+.2f %+.2f %+.2f %s",accel[0],accel[1],accel[2],freshA?"":"OLD");text(59,s);
snprintf(s,sizeof(s),"G d/s: %+.1f %+.1f %+.1f %s",gyro[0]*RAD_TO_DEG,gyro[1]*RAD_TO_DEG,gyro[2]*RAD_TO_DEG,freshG?"":"OLD");text(77,s);
snprintf(s,sizeof(s),"RPY: %+.0f %+.0f %+.0f %s",rpy[0],rpy[1],rpy[2],freshQ?"":"OLD");text(95,s);
snprintf(s,sizeof(s),"Hz A/G/Q: %.1f / %.1f / %.1f",report[0].hz,report[1].hz,report[2].hz);text(113,s);
snprintf(s,sizeof(s),"Valid: %lu / %lu / %lu",(unsigned long)report[0].good,(unsigned long)report[1].good,(unsigned long)report[2].good);text(131,s);
snprintf(s,sizeof(s),"I2Cerr:%lu Bad:%lu Gap:%lu Rst:%lu",
(unsigned long)(transport.readErrors+transport.writeErrors+initTransport.readErrors+initTransport.writeErrors),
(unsigned long)(report[0].bad+report[1].bad+report[2].bad+transport.decodeErrors+initTransport.decodeErrors),
(unsigned long)(report[0].gaps+report[1].gaps+report[2].gaps+transport.intTimeouts+initTransport.intTimeouts),
(unsigned long)(unexpectedResets+transport.hardwareResets));text(149,s);
snprintf(s,sizeof(s),"Still:%s XYZ:%c%c%c %lus/60s",stillDone?(stillOK?"OK":"CHECK"):"WAIT",
motionDone[0]?(motionOK[0]?'O':'!'):'-',motionDone[1]?(motionOK[1]?'O':'!'):'-',motionDone[2]?(motionOK[2]?'O':'!'):'-',(unsigned long)(elapsedStream()/1000));text(167,s);
const char *status=(fault||anomalyCount())?"REVIEW":stage==Stage::Finished?evidenceName(result()):"TESTING";
text(188,status,4,TFT_WHITE);
text(219,automaticSession?"AUTO label; START M001 to assign":
stage==Stage::Ready?"Serial X/Y/Z; FINISH after 60s":"Serial STATUS / FINISH");
}
void setup() {
Serial.begin(115200);bootId=esp_random();
// Keep unused SPI peripherals deselected. GPIO19 is NOT initialized as SD MISO.
pinMode(5,OUTPUT);digitalWrite(5,HIGH);pinMode(33,OUTPUT);digitalWrite(33,HIGH);
pinMode(BNO_INT,INPUT); // GPIO35 has no internal pull-up; external bias is an optional module-specific check.
Wire.setBufferSize(128);Wire.begin(BNO_SDA,BNO_SCL,I2C_HZ);Wire.setTimeOut(50);
tft.init();tft.setRotation(1);tft.invertDisplay(false);
chinese.begin(tft);chinese.setFont(u8g2_font_wqy16_t_gb2312);chinese.setFontMode(1);
info();lastTick=millis();drawScreen();
startAutomaticSession();
}
void loop() {
readCommands();
if(active&&initialized) {
// BNO086 does not support I2C polling without INT. Drain ready packets only.
for(int n=0;n<24&&digitalRead(BNO_INT)==LOW;++n) {sh2_service();yield();}
if(imu.wasReset()) {
++unexpectedResets;fault=true;logEvent("DEVICE_RESET","instruction=review_power_bus_and_module");
for(auto &r:report){r.seen=false;r.timedOut=false;}
if(!reportsOn())finishSession("REPORT_REENABLE_FAILED");
}
if(active){checkTimeouts();updateStages();}
}
if(millis()-lastTick>=1000)telemetry();
if(millis()-lastDraw>=500){lastDraw=millis();drawScreen();}
delay(1);
}
#pragma once
// Configured for the user-confirmed ESP32-2432S028R. Check physical wiring before START.
// GPIO19 requires TF DAT0 breakout.
// No SD card, touch, speaker, RGB, WiFi or Bluetooth functions are initialized.
constexpr int BNO_SDA = 27;
constexpr int BNO_SCL = 22;
constexpr int BNO_INT = 35; // Input only, no internal pull-up; 10k to 3.3V is an optional wiring check.
constexpr int BNO_RST = 19; // TF DAT0/DO, NOT P3 GPIO21 (screen backlight).
constexpr uint8_t BNO_ADDRESS = 0x4B;
constexpr uint32_t I2C_HZ = 400000; // Within BNO086 supported range; recorded in every run.
constexpr uint32_t REPORT_US = 20000;
constexpr uint32_t WARMUP_MS = 5000;
constexpr uint32_t STILL_MS = 15000;
constexpr uint32_t MOVE_MS = 8000;
constexpr uint32_t MIN_STREAM_MS = 60000;
constexpr uint32_t GAP_MS = 500;
constexpr float MIN_OBSERVED_HZ = 35.0f; // Practical host evidence floor, not datasheet precision.
constexpr float MAX_OBSERVED_HZ = 110.0f;
constexpr char FW_ID[] = "BNO086_MODULE_TEST_V1.7";
#pragma once
#include <cmath>
#include <cstdint>
enum class Evidence { Incomplete, Review, Observed };
inline Evidence streamEvidence(bool initialized, uint32_t elapsed, uint32_t minimum,
uint32_t anomalies, const uint32_t counts[3],
float lowHz, float highHz) {
if (!initialized) return Evidence::Incomplete;
if (anomalies) return Evidence::Review;
if (elapsed < minimum) return Evidence::Incomplete;
for (int i = 0; i < 3; ++i) {
const float hz = elapsed ? counts[i] * 1000.0f / elapsed : 0;
if (hz < lowHz || hz > highHz) return Evidence::Review;
}
return Evidence::Observed;
}
inline Evidence overallEvidence(Evidence stream, bool stillDone, bool stillOK,
const bool motionDone[3], const bool motionOK[3]) {
if (stream == Evidence::Review || (stillDone && !stillOK)) return Evidence::Review;
for (int i = 0; i < 3; ++i)
if (motionDone[i] && !motionOK[i]) return Evidence::Review;
if (stream != Evidence::Observed || !stillDone) return Evidence::Incomplete;
for (int i = 0; i < 3; ++i) if (!motionDone[i]) return Evidence::Incomplete;
return Evidence::Observed;
}
inline bool finiteVector(const float *v, int n, float limit) {
for (int i = 0; i < n; ++i)
if (!std::isfinite(v[i]) || std::fabs(v[i]) > limit) return false;
return true;
}
[platformio]
default_envs = cyd_st7789
[env]
platform = espressif32@6.13.0
board = esp32dev
framework = arduino
platform_packages =
platformio/framework-arduinoespressif32 @ ~3.20017.0
board_build.partitions = huge_app.csv
monitor_speed = 115200
; Upload port deliberately unset. Verify the physical target before uploading.
lib_ldf_mode = deep+
build_flags =
-D USER_SETUP_LOADED=1
-D TFT_WIDTH=240
-D TFT_HEIGHT=320
-D TFT_MOSI=13
-D TFT_SCLK=14
-D TFT_CS=15
-D TFT_DC=2
-D TFT_RST=-1
-D TFT_MISO=-1
-D TFT_BL=21
-D TFT_BACKLIGHT_ON=HIGH
-D LOAD_GLCD=1
-D LOAD_FONT2=1
-D LOAD_FONT4=1
-D SPI_FREQUENCY=40000000
[env:cyd_st7789]
build_flags = ${env.build_flags} -D ST7789_DRIVER=1 -D TFT_INVERSION_OFF=1
[env:cyd_ili9341]
build_flags = ${env.build_flags} -D ILI9341_DRIVER=1
Repeatable steps
一轮测试怎么做,怎样保留独立记录
- 断电核线。 确认六线、ST7789、地址 0x4B、SD 卡未插。给实物贴自己的标签,再上电。V1.7 自动会话每次重启都换 boot / run,不复用旧计数。
- 连接日志工具并核固件。 安装 pyserial,先列端口,核对目标后显式指定端口。工具先用 INFO 核 V1.7,再输入 start 建操作员会话;它负责 START 时不需重复手动发送。
- 预热 5 秒,静置 15 秒。 放稳并保持不动,保存现场、计时和原始日志。加速度含重力,向量模长不该因“静止”而全为零。
- READY 后依次 X、Y、Z,各 8 秒。 按实物丝印轴缓慢往返转动,建议约 30–60°/s、姿态变化明显超过 20°。每轴结束等待 READY;不要只平移或靠屏幕方向猜轴向。
- 流观测达到至少 60 秒再 FINISH。 看完整 SUMMARY、REPORT_SUMMARY 和 TRANSPORT。初始化时间不算流时长;必须保留有效报告与异常,不能只截结果一行。
- 复测或换模块另开轮。 先保存结束记录,断电更换;写清改线、环境或动作变化。缺 FINISH 如实标“未完整收尾”,不能补写完成。
python -m pip install -r requirements.txt
python 模块日志采集.py --list-ports
python 模块日志采集.py --port COM9 --module M001 --output ./logsCOM9 是本项目历史端口示例,使用时换成自己确认的目标端口。M001 是人工贴签,不是硬件 SN。永久文件由主机工具保存;未连接期间的启动输出无法从屏幕或 RAM 补回。
| 固件命令 | 动作 |
|---|---|
| INFO / STATUS | 核固件、会话、阶段、有效报告、异常。reports_armed 表示主机统计启用,不表示结束时已关芯片订阅 |
| START M001 | 结束 AUTO 并建立贴签独立 run;活动操作员会话重复 START 拒绝 |
| X / Y / Z | READY 时启动相应轴阶段;同轮已完成轴不能重做 |
| FINISH | 保存本轮实际结束与摘要;异常不因结束而清除 |
查看本版本的粗测窗口与结果含义
这些是程序筛查窗口,不是厂家的精度允差。静置 15 s:加速度模长均值 8.3–11.3 m/s²、标准差 ≤0.25 m/s²、角速度模长峰值 ≤0.15 rad/s,A/G 各至少 525 条;各轴 8 s:对应角速度峰值 ≥0.6 rad/s、相对起点姿态变化 ≥20°、G/Q 各至少 280 条。全轮各报告平均有效更新率窗口 35–110 Hz,超过 500 ms 无有效更新记超时。
OBSERVED 表示规定条件下已观察到所需阶段和数据;REVIEW 表示已发现异常或已执行阶段没满足窗口,需要复核;INCOMPLETE 表示初始化、阶段或时长不足。stream 和 overall 分别判断,流 OBSERVED 仍可因三轴未做而整体 INCOMPLETE。已有异常时 REVIEW 优先;所有记录按实际保存。
Local files · cloud link pending
资料下载
源码包 SHA-256:91af51899e6ab08aaa51ca7e64fc85e34019e76dd5316078938023f099ae68fd。真实网盘分享链接待配置。
Real footage · one module
实拍:13.5 秒连续读数响应
来自实际原声录像的连续区间 00:04.000–00:17.500,网站副本降为 720p / 30 fps。画面为一只已接模块,标签 AUTO_4B9E7573 / run0001,可见 TESTING、StillCHECK、XYZ---;背景中的其他模块不代表它们也完成了测试。
没有建立录像与全部串口记录的完整时间同步。短片不套用另一轮的时长、速率或最终结论;图像中的零计数只描述画面所属会话当时累计的程序观测。
Reference board ≠ measured module
硬件资料:参考电路与实物核验分开
| 事项 | 官方 / 项目证据 | 适用范围 |
|---|---|---|
| PS0 / PS1 | SparkFun v10 原理图:0 / 0 对应 I²C | 实际模块需核协议选择 |
| ADR 与地址 | SparkFun v10 ADR 开路默认 0x4B,闭合 0x4A;本项目设 0x4B | 其他模块以实物与有效协议响应为准 |
| SDA / SCL 上拉 | SparkFun v10 各 2.2kΩ,经 I²C 跳线连接 | 参考板参数,未测这只模块阻值 |
| RST / INT | SparkFun v10:RST 有 10kΩ 上拉,INT 网未画单独上拉 | 不能据此保证用户模块电路或 INT 输出结构 |
| GPIO35 | Espressif:仅输入、无内部上拉 | 实际稳定电平应结合模块输出电路测量 |
| 精度 / 量程 / 温漂 | 请查 CEVA 芯片手册及对应报告定义 | 本机没有计量实测,不将典型芯片指标写成模块性能保证 |
地址对照采用 SparkFun 官方 v10 原理图 与 CEVA 手册。指南 ADR 段落的 0x48 与原理图不一致,本页按可核对的 0x4A / 0x4B 说明。
Primary sources · checked 2026-10-05
官方来源与本页信息来源
| 页面内容 | 来源 / 边界 |
|---|---|
| 六线配置、程序行为、已启用报告 | 当前 V1.7 原工程源码、最新六线软件图与来源映射;GPIO19采用用户确认的SKU02753 DAT0/DO引出,本次未新增硬件实测 |
| 表中时间、计数、阶段与结果 | 原工程当前验证状态与独立实测记录,未合并不同 run |
| 13.5 秒视频 | 真实原声录像的连续片段;未与全部串口记录同步 |
| 芯片报告、融合、INT 与校准解释 | CEVA / SH-2 / SparkFun / Espressif 官方资料,参考电路与实物参数分开 |
| LCD图解、背面接头与日志选摘 | V1.7 drawScreen与判断代码;同版双USB背面照片;V1.txt原行36–50、71–74、135–142、216、220–229,正文不改写,去除主机前缀。LCD图是字段位置复刻,照片不是用户本轮接线图 |
| 动画与交互示意 | 教育解释用示意,不含实测曲线或预测性能 |
| 待补资料 | 实际接线复测、专用购买链接、网盘真实分享地址、完整 XYZ 与计量 / 长期测试 |