88 键钢琴掉线后:让音符有序收尾

2026-10-062

按住一个音,扫描板随后失去响应。手已经离开琴键,软件却还记着“按下”。这时继续播放,还是立刻静音?如果同一个音还被屏幕触摸或自动播放持有,处理就更有意思了。

这篇把问题放到一套 88 键电子钢琴里,重新设计并运行一个独立 C 扫描模块。目标是让单板异常有明确的收尾规则,同时保住健康板和其他输入来源的状态。文中的运行结果来自主机模拟总线,尚未接入实体钢琴。

七块扫描板组成的完整键盘

图 1|原项目的装配状态。七块板合计 88 键,用来说明分板结构;照片不证明本文新策略已经烧录或完成实体演奏测试。

一块板读错,为什么全键盘会等它?

先沿着源码找调用关系。每块 PCA9555 提供两组 8 位输入,原程序分别读取 P0、P1,再由批量函数汇总结果。对于已经初始化的设备,只要有一块读取失败,批量函数就返回错误。外层只有在整批返回 RT_EOK 时,才更新物理键状态。

因此,可能发生的是:某板持续读取失败,其他板的物理按键也暂停更新,原先按下的状态得不到释放。原程序虽然在失败处填入 0xFF,外层会跳过这一批处理,不能把这些填充值直接说成“触发了所有键”。未初始化板也被外层跳过,但循环中没有运行时重新初始化路径。

还有一个细节:初始化检查了输入方向配置,却没有检查两次极性写入的返回值,也没有回读极性。软件用“1 表示按下”解释数据,必须先确认这个约定在设备中成立。

单个触摸输入的高清原理图

图 2|PAD12 V1.0 的 U9/PT2041AT6 触摸通道,KEY1 接到输入映射。这里是电容触摸结构,本文处理的是通信失效,不能把机械触点去抖方案直接套进来。

把 88 个键的边界先数清楚

本次沿用源码实际使用的七个地址 0x20~0x26。首板 15 键、中间五板各 12 键、末板 13 键,恰好是 15 + 5×12 + 13 = 88。原程序还扫描 0x27,但物理键映射只遍历七板;独立示例按这七板管理。

键编号从 0 开始。每板 P0 bit0~7 对应 board×12+3+bit;P1 bit4~7 对应 board×12+14−(bit−4)。首板 P1 bit1~3 补上编号 2、1、0,末板 P1 bit3 对应 87。测试逐位遍历,确认 0~87 各出现一次,没有重号或漏号。

每块板保留自己的最近有效时间、按下位图和重试状态。健康板拿到完整数据就推进,异常板只影响自己的状态。这个隔离成立的前提是底层传输会在有限时间内返回;一块设备把整条总线拉死时,软件无法凭空读到其他板。

U6 扩展器供电与输入接口

图 3|从同版登录 EDA 原理图重新导出的 U6 局部。两个端口需要独立映射;VCC5V 供电还带来后文的输入门限核验问题。

数据完整了,再交给状态机

读取函数用临时数组接收结果,仅在两条消息都完成后发布两个字节。下面是附件中实际编译的函数,read_pair 的返回值是完成消息数,适配 RT-Thread 时不能把 RT_EOK 当成 2。

bool piano_read_pair(const piano_bus *bus, uint8_t addr, uint8_t reg,
                     uint8_t out[2])
{
    uint8_t candidate[2] = {0u, 0u};
    if (!bus || !bus->read_pair || !out) return false;
    if (bus->read_pair(bus->ctx, addr, reg, candidate) != 2) return false;
    out[0] = candidate[0];
    out[1] = candidate[1];
    return true;
}

底层应执行“写寄存器指针 + 连续读两字节”。PCA9555 在同一寄存器对内交替访问两个端口,因此从 0x00 开始读两个字节可得到 P0、P1。这减少了软件只提交一个端口的窗口,但两个端口仍在总线上先后采样,不能称作同步快照。

初始化把方向 0x06/0x07 和极性 0x04/0x05 全部写为 0xFF,检查每次写入并回读。每轮接受输入前,再确认这两组模式寄存器仍符合约定。输入有效位中 1 表示按下,其他未用位通过掩码排除。

这项检查也有成本:健康板每轮需要三次两字节组合读,七板共 21 次。只计地址、数据和应答时钟,一次约 45 个 SCL 时钟,合计 945 个;在 400 kHz 下约 2.36 ms,在 100 kHz 下约 9.45 ms,还没有计入起停、调度和驱动开销。原循环中的 5 ms 延时也不等于完整扫描周期。实际接入要测完整周期,再定陈旧阈值。

PAD12 顶底层连接区域

图 4|同版 EDA 原生 PCB 顶、底层导出的连接区域。底层已经水平镜像,按底面观察方向标注。接入前应沿连接器核对 SDA、SCL、地、电源和地址配置;这张图不是新电路,也不代表已经通过 DRC 或波形验证。

掉线后,旧状态保留多久?

示例把陈旧阈值设为 100 ms、配置重试间隔设为 20 ms。短暂失败期间保留最后一次有效状态;距离最后完整样本达到阈值后,释放该板已经按下的物理键。下面这一段放在消费恢复数据之前,即使线程停顿了一段时间,也先处理已经过期的状态。

if (b->fresh && !b->expired &&
    (uint32_t)(now_ms - b->last_good) >= s->stale_ms) {
    emit_physical(s, i, b->down, 0u, true);
    b->down = 0u;
    b->inhibit = used;
    b->expired = true;
}

inhibit 是恢复保护位图。过期后,该板每个有效键都要先观察到一次松开,才允许再次按下。如果线恢复时手仍按着,立即接受这个高电平会重新触发刚刚结束的音符;要求“松开再按”能给恢复一个清楚的边界。代价也明确:恢复瞬间新按下、却未观察到松开的键同样会被暂时抑制。

100 ms 是测试中的逻辑阈值,不是测得的静音延迟。状态机靠轮询执行,底层阻塞、回调耗时或线程失去调度都会推迟动作。附件的时间回绕检查覆盖 32 位计时跨零,使用时仍须保证单调计时和足够频繁的调度。

物理键释放,别把触摸和自动播放一起关了

源码的屏幕触摸和自动播放也调用同一个音符入口。为每个键保留三种来源:物理键占位 1、屏幕触摸占位 2、自动播放占位 4。任意来源还持有这个键,聚合状态就保持按下。

bool piano_set_owner(piano_scan *s, unsigned key, uint8_t owner, bool down)
{
    if (!s || key >= PIANO_KEYS ||
        (owner != OWNER_PHYSICAL && owner != OWNER_TOUCH && owner != OWNER_PLAYER))
        return false;
    uint8_t before = s->owners[key];
    uint8_t after = down ? (uint8_t)(before | owner)
                         : (uint8_t)(before & (uint8_t)~owner);
    s->owners[key] = after;
    if ((before != 0u) != (after != 0u) && s->note_event)
        s->note_event(s->event_ctx, key, after != 0u);
    return true;
}

例如,一个键的来源值是 1|2|4=7。物理板掉线,只清掉 1,结果仍是 6,所以不会发送音符关闭。随后触摸松开,值变为 4;最后自动播放释放,值才归零,发送一次关闭。

接回原系统时,扫描、触摸和自动播放都应进入一个串行调度器,只有聚合事件再调用实际音符入口。继续保留触摸或播放对 set_key_state 的直接调用,会绕过仲裁。示例本身没有线程安全或可重入承诺,也未实现 RT-Thread 队列适配。一个来源位只代表一个逻辑持有者;同来源多触点或重叠音符还需要先计数。

跑过的故障,和还没测过的电路

主机工程用 MSVC C11、/W4 /WX 编译,通过了九组检查:短传输不修改公开字节、88 键映射、极性配置失败、单板隔离、超时只释放一次、恢复重按、多来源持有、计时回绕与调度间隙,以及重试和未用位处理。

主机故障注入的实际输出

图 5|演示程序的真实输出,时间是注入输入。5 ms 时键 3 按下;10 ms 起首板读取失败,健康板的键 27 仍可按下、松开;105 ms 时键 3 执行故障释放。125 ms 恢复但仍按住时不重触发,130 ms 观察松开,135 ms 再按才产生新事件。

还有一项不能靠软件略过。图 3 的 U6 电源是 VCC5V,触摸通道和总线拉电阻却使用 VCC3.3V。

I2C 拉电阻的实际供电网

图 6|同版原理图中的 R2、R3 为 4.7 kΩ,拉到 VCC3.3V。实物上的并联数量和开关状态还会改变总线等效拉电阻,需要另行确认。

若实物采用 TI PCA9555,SCPS131J 的输入高门限最低为 0.7×VCC;5 V 供电时是 3.5 V,3.3 V 信号不满足保证条件。当前没有确认 U6 的实际厂家型号,也没有完成电平和波形测量,因此本文不提供整板直接复刻结论。调整扩展器供电或加入合适的电平转换,需要重新核对全部输入、地址脚和电源时序,不能凭一处改线宣称解决。

模式回读与输入读取也不是原子操作。本次故障模型没有覆盖模式检查后、输入采样前芯片复位的情况;电容触摸阈值、总线拉死、芯片掉电和音频性能都没有实体验证。完成的是一个能重复运行、明确说明边界的扫描故障策略,后续可按同样方法做目标平台接入与实测。

源码与创作说明

完整最小工程:piano-scan-v1.0-source.zip。包内包含四个 C 源文件、构建与接入说明、实际测试日志、演示日志、NOTICE 和完整 AGPL-3.0 许可证,不包含音频素材或上游整套 SDK。

创作来源:立创开源广场《专业级全键盘电子钢琴》,原作者 lovelessing。

项目完整网址:https://oshwhub.com/lovelessing/professional-grade-full-keyboard

独立撰写的文字以 CC BY 许可发布;图片和代码按各自许可复用,未变更第三方许可。

项目及 EDA 标示 GPL-3.0。本文复用允许使用的装配照,并从登录的嘉立创 EDA 工程 f442835ac92e4dd49dd50e5edbddb6c4、PAD12 V1.0 重新导出原理图和 PCB 顶底层 PNG;进行了局部裁剪、保真缩放、底层观察方向镜像,封面仅轻微调整曝光,没有生成缺失细节。软件参考仓库根许可为 AGPL-3.0,新示例保留 AGPL-3.0-only。

软件参考:https://gitee.com/lovelessing/luban-lite-v120.git ,固定提交 760044183467c19b73f716e9ef67da3fe0c3523f,目录 packages/artinchip/lvgl-ui/aic_demo/piano_demo。

参考资料:TI PCA9555 数据手册 SCPS131J,输入门限、配置/极性寄存器与成对读取说明:https://www.ti.com/lit/ds/symlink/pca9555.pdf

本文独立分析并完成 C 状态机、主机故障注入与来源仲裁测试;没有修改原硬件、编译完整目标固件、烧录、执行 ERC/DRC 或进行实体演奏、电平及延迟测量。

技术交流,欢迎关注微信公众号:美男子玩编程。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -