把算盘改造成USB键盘:基于磁传感与I2C的嵌入式输入终端

📅 2026/8/27 5:22:22
把算盘改造成USB键盘:基于磁传感与I2C的嵌入式输入终端
前阵子接了个挺冷门的需求把一把老算盘改造成一个数据录入终端Abacus Data Entry Terminal。客户做库存盘点每天要把几百条数字手工敲进系统小键盘按到手指发僵问我有没办法换个更不容易错、能一次录一整行数字的输入方式。折腾了几版之后我给出的答案是直接把算盘本身当成输入设备拨珠即录入。简单说这个项目就是把算盘每根柱子上的每一粒珠子都变成可检测的开关用单片机实时读出所有珠子的位置再换算成数字最后以USB键盘的形态把数字敲到电脑里。它解决的核心痛点是批量录入纯数字时的错误率和手部疲劳。算盘有个键盘比不了的特性——当前数值是物理可见的拨到什么位置就代表什么数改起来也是拨一个珠子的事。这个项目适合谁参考一类是嵌入式入门、想完整体验传感器、I2C扩展、HID协议这条技术链路的创客另一类是做数据录入相关工具的人想看看传统物件能不能变成更顺手的输入设备。不需要太深的单片机基础只要会焊线、能跟着跑Arduino例程就能把它复现出来。1. 项目整体设计与思路拆解1.1 算盘本身就是一台“可视化数字状态机”算盘这东西本质上是一组二值开关阵列。以最常用的13柱日式算盘为例每一根柱子是一个数位每柱有一颗上珠和四颗下珠。上珠拨到靠梁位置代表5下珠每有一颗拨到靠梁就代表1。一列珠子停在一个状态下这列显示的数字就是固定的。换算规则很直白某一列的数值等于上珠状态乘以5再加上靠梁下珠的数量。比如上珠靠梁、一颗下珠靠梁这一列就是6上珠离梁、三颗下珠靠梁这一列就是3。换句话说整把算盘当前呈现的是从高位到低位的一串数字只不过需要有人去“读”它。这个特性在交互设计里非常值钱。键盘输入数字时按下去之后屏幕上显示的数值和手指动作没有物理上的对应关系按错了往往要等扫一眼屏幕才能发现。算盘不一样拨完珠子数值就明明白白摆在面前操作者用眼睛余光就能校验。所以把算盘数字化核心不是让它算得快而是把“物理可见的数”变成“电子可读的数”这正好符合数据录入终端对可靠性的要求。1.2 技术路线选型传感器阵列方案为什么最稳要做数字化摆在面前的无非三条路。第一条是给每颗珠子塞磁铁在珠子靠梁的判定点放磁簧开关或霍尔传感器读二值状态。第二条是用光电对管在梁的位置布置光路珠子靠梁时挡住光路来触发。第三条相对取巧用摄像头做视觉识别通过图像算法解析珠子位置。视觉方案看上去很高级实际做起来最麻烦。算盘珠子的颜色、环境光、摄像头角度、反光都会影响识别率而且为了实时录入还要保证较高的帧率树莓派加摄像头加算法成本一下子拉高了。光电方案的问题在于算盘梁的位置本来就有结构遮挡光路不好布置珠子是木头或塑料的遮挡效果也不稳定。对比下来磁传感阵列是最稳的磁铁不怕灰尘、不挑珠子材质、传感器体积小磁簧开关几毛钱一颗霍尔传感器也就一两块钱成本完全可控。方案定下来之后我对整个系统的分层就很清楚了。最底层是传感器阵列和I2C引脚扩展中间层是单片机固件里的去抖、状态快照和解码算法最上层是USB HID键盘输出。每一层都相对独立后续想换成蓝牙或者串口输出都不用动底层。2. 算盘硬件改造传感器选型与安装全流程2.1 检测方案横向对比磁簧开关、霍尔传感器、光电对管磁簧开关是玻璃管里封装了两片簧片磁铁靠近时簧片吸合远离时断开本质是一个干接点。霍尔传感器靠磁场强度输出电压变化分开关型和线性型开关型输出直接就是高低电平。光电对管则是发射红外光、接收端检测光路是否被遮挡。直接说结论我这次用的是磁簧开关。为什么第一磁簧开关不需要供电直接把两端接到I2C扩展器的引脚和地线之间就行少了一大堆电源线第二它的吸合和断开基本没有方向性只要磁铁靠近就动作安装容错率高第三机械簧片的通断虽然理论上有寿命限制但算盘拨珠的频率远达不到它的寿命上限用几年没问题。当然磁簧开关的缺点也很明显簧片吸合瞬间会有机械抖动必须做软件去抖。另外它的灵敏度受到磁铁距离影响很大磁铁离远了可能吸不上离太近又可能一直吸住不释放。霍尔传感器没有这些问题直接输出稳定的电平信号如果你对稳定性要求高、预算也够可以考虑霍尔。光电对管在这类场景里最不推荐算盘珠子在轨道上滑动时会遮挡光路但木珠对红外线的遮挡不完全而且环境光干扰很难排除。2.2 磁铁、传感器与固定方式的实操细节改造的第一步是给珠子加磁铁。我用的是一把全新的13柱算盘珠子是原木色木珠中间孔洞直径约5mm。买了一批直径3mm、厚度1mm的钕磁铁用2mm钻头在珠子朝外的一侧打个浅孔深度刚好把磁铁埋进去再点一滴快干胶固定。这里有个特别容易踩的坑所有磁铁的极性必须统一。磁簧开关虽然没有极性之分但后续如果你想换霍尔传感器霍尔是有极性的到时候发现一半珠子永远触发不了排查起来想哭。传感器布置是整台设备安装精度要求最高的环节。以日式算盘为例上珠停靠在横梁上方下珠停靠在横梁下方所以上下珠的判定点不在同一个高度。我的做法是在横梁上下两侧各钻一排小孔下珠的判定点设在横梁下缘往下4mm的位置上珠的判定点设在横梁上缘往上4mm的位置。每颗珠子对应一个传感器槽位用热熔胶把磁簧开关固定进去线圈引脚朝外。安装的时候要先做一块测试板验证位置。把磁簧开关临时贴在算盘框架背面拨动对应珠子看开关能否可靠通断。正常情况是珠子拨到靠梁位置时开关吸合拨回离梁位置时开关断开。如果发现珠子在中间位置也触发说明传感器位置太高或太低需要微调如果珠子靠梁了却不触发多半是磁铁极性反了或者距离太远可以把磁铁孔打深一点让磁铁更靠近传感器。2.3 I2C扩展用MCP23017解决引脚荒算盘有13根柱子每柱5颗珠子一共要检测65路开关。再算上提交按钮、状态LED总共有接近70路信号。普通Arduino开发板的数字引脚只有十几个压根不够用所以必须做引脚扩展。这里我选了MCP23017它是一颗16路I2C GPIO扩展芯片一颗芯片可以当16个数字引脚用I2C地址通过A0、A1、A2三个引脚配置最多支持8颗芯片级联。65路信号用5颗MCP23017正好5乘16等于80留出15路余量给按钮、LED和后续扩展。I2C地址这样分配第一颗芯片A0A1A2全接低地址0x20第二颗A0接高地址0x21依次类推到0x24。接线方式很简单所有芯片的SDA并联到单片机的SDA引脚SCL并联到SCL引脚电源统一接5V或3.3V然后I2C总线两端各接一个4.7k上拉电阻到VCC。这里提醒一句I2C总线对线长很敏感。算盘宽度大概25cm传感器分布在两侧如果用很长的飞线把5颗芯片串起来通信很容易出错。我实际布线时把MCP23017分散安装在算盘框架内每颗芯片尽量靠近它负责的那3列传感器然后用短粗线连接。I2C通信频率也降到了100kHz实测跑一晚都没有数据错误。3. 固件核心逻辑把珠位变成真正能用的键盘输入3.1 去抖读取与状态快照先扫全盘再解码固件开发环境是Arduino IDE主控板选了Arduino Pro Micro理由是它用的ATmega32U4芯片带原生USB可以直接模拟键盘不需要额外接USB转串口芯片。整体固件逻辑分三块传感器扫描、算盘解码、HID键盘输出。传感器扫描首先要解决抖动问题。磁簧开关是机械触点珠子拨到位的瞬间簧片会来回弹跳直接读取的话同一个珠子可能在几十毫秒内出现好几种状态。我写了一个带软件去抖的读取函数连续读两次相隔5ms如果两次状态一致就确认否则再读一次。机械抖动在几毫秒内就会结束这个时间去抖足够。还有一点是状态快照。算盘拨珠不是一次性动作当你从123拨到456中间会经过若干中间状态。如果一边拨一边逐列解码发送很有可能会把中间状态当作有效数据发出去。我的做法是在按下“提交”按钮之后先把所有65路传感器状态一次性读入内存数组等全部读完了再统一解码。这样能保证发送出去的永远是珠子的最终状态而不是拨动过程中的某个瞬态。// 带5ms去抖的读状态函数 bool readBeadDebounce(int col, int bead) { bool s1 readBead(col, bead); delay(5); bool s2 readBead(col, bead); if (s1 s2) return s1; return readBead(col, bead); } // 整体快照把所有列的状态读进内存 void takeSnapshot(int snapshot[]) { for (int col 0; col NUM_COLUMNS; col) { snapshot[col] decodeColumn(col); } }主循环里只做一件事检测提交按钮是否被按下。按下之后先做20ms延时等按钮彻底稳定然后拍快照、发送数据、等待按钮释放。这样整个数据流是单向的不会出现按一次发两条数据的情况。3.2 算盘解码算法上珠乘五下珠累加算盘解码是整个固件的核心算法逻辑上其实就两句话下珠靠梁的个数就是下珠数值上珠靠梁则加上5。一列珠子的数值等于下珠数量加上上珠状态对应的5。这里有个细节解码过程中上珠和下珠要分开读因为它们在传感器布局时是不同的判定点。还要注意算盘列的顺序。标准的13柱算盘最左边是最高位最右边是最低位。录入数据时一般从高位到低位读取所以遍历列的时候从第0列遍历到第12列依次把每列数据拼接成一个字符串再通过键盘输出。输出完成后加一个回车模拟扫码枪录入完一行的行为正好对应用户在Excel或ERP系统里按回车跳到下一个单元格。// 解码一列珠子返回0-9之间的数值 int decodeColumn(int col) { int lower 0; for (int bead 0; bead 4; bead) { if (readBeadDebounce(col, bead)) lower; } bool upper readBeadDebounce(col, 4); // 第5颗是上珠 return lower (upper ? 5 : 0); } // 按下提交按钮后拼接整串数字并发送 void sendAllColumns() { int snapshot[NUM_COLUMNS]; takeSnapshot(snapshot); for (int col 0; col NUM_COLUMNS; col) { Keyboard.print(snapshot[col]); } Keyboard.write(\n); }有人可能会问算盘珠子的状态本身已经能保证编码是合法的吗比如下珠三颗靠梁、上珠同时靠梁那这一列就是8物理上完全是有效编码。但如果有人乱拨同时出现两个下珠中间隔着一颗离梁的这种情况在真实算盘上不会出现因为珠子是串在柱上的且只能上下滑动所以可以放心按数量累加。3.3 原生USB HID让终端插上电脑就能打字Pro Micro最方便的地方就是原生USB HID。在Arduino代码里引入Keyboard.h调用Keyboard.begin()之后单片机就变成了一个标准的USB键盘设备插到任何电脑上都会被识别为键盘。不需要装驱动Windows、macOS、Linux通用这是数据录入终端最关键的需求之一——即插即用不能让人现场装驱动。实际使用中我设计了两种发送模式。第一种是手动模式用户把当前这一行数字全部拨好按一下侧面的实体按钮终端就把整串数字连同回车一次性敲进电脑。第二种是自动模式固件每隔200ms扫描一次算盘状态如果连续三次扫描结果一致且与上一次发送的结果不同就自动发送。自动模式适合连续录入场景操作者双手不用离开算盘拨完一串数字稍作停顿数据就自动上屏了。为了给操作者反馈我在终端上加了一个绿色LED。数据发送成功时LED亮300ms让用户知道这一条已经录进去了。如果算盘状态非法或者I2C读取失败LED会快速闪烁三次。这个反馈看起来简单实际用起来非常重要没有它操作者会经常不确定数据到底有没有发出去。4. 实测踩坑数据录入终端的常见问题与排查4.1 误触发与灵敏度漂移磁铁位置是最大变量第一次整机组装完成后测试就出现了问题第4列的上珠明明没有拨到靠梁位置解码结果却是5。排查下来发现是相邻柱子的磁铁距离太近磁簧开关的灵敏度又偏高两颗珠子同时处于中间位置时距离较远的磁铁产生的磁场也足以让簧片吸合。这个问题在传感器间隔小的算盘上尤其明显。解决思路有两个方向。一个是加大相邻传感器之间的物理隔离我用一小片1mm厚的硅钢片夹在相邻磁簧开关之间相当于做磁屏蔽效果立竿见影。另一个是调整磁铁选型把直径3mm的磁铁换成直径2mm的并适当加深孔位让磁铁离传感器远一点降低触发灵敏度。实测下来把磁铁中心到磁簧开关的距离控制在4到6mm范围内是最舒服的太近会误触发太远又不可靠。还有一个漂移问题是热熔胶固定磁簧开关导致的。刚固定好时位置是准的用几天后热熔胶软化开关稍微位移就变得时灵时不灵。后来我改用环氧树脂固定问题就消失了。如果不想打胶也可以用3D打印一个小卡扣把磁簧开关压在算盘框架上效果更规整。4.2 多列同时拨动时的数据打架问题算盘录入数字有个特点进位的时候会同时拨动好几列珠子比如从199拨到200需要同时操作一列下珠、两列上珠。这时候如果固件处理不好快照里就可能出现一个荒诞的画面高位已经变成了2低位还没拨完中间列还是9。实际发送出去的数据就成了299、200和201混在一起的乱七八糟结果。我把快照逻辑改成“提交时统一读取”之后这个问题解决了一大半。但手动模式下还有一个小坑用户按提交按钮的瞬间手指可能还没离开珠子某些列的珠子状态还在变化中。所以我又加了一个稳定性检测按下按钮后先等50ms再连续读取两次完整状态完全一致才解码发送否则再等50ms重试。最多重试5次如果一直不稳定就点亮错误LED提示用户检查算盘。这样处理下来多列同时拨动时的数据一致性基本没问题了。关键思路说白了就是不要在数据还没稳定的时候去读宁可多等几十毫秒也要拿到确定的状态。4.3 常见问题速查表现象可能原因处理方法某几列永远读取为0对应MCP23017的I2C地址冲突检查A0A1A2电平确保5颗芯片地址互不重复某列数值每次都多个5上珠传感器被相邻柱磁铁触发加磁屏蔽、换小磁铁、拉开间距按下提交按钮没反应按钮引脚没有上拉或接线虚接固件启用内部上拉重新焊接数据时对时错间隔性乱码I2C总线过长或上拉电阻缺失缩短线缆、加4.7k上拉电阻、降频到100kHz首列数据丢失或滞后快照时机太早珠位未稳定增加50ms延时并做连续两次一致性校验插入电脑识别不到键盘Pro Micro的USB线是纯充电线换带数据线的USB线4.3 关于供电和地线的注意事项传感器阵列加扩展芯片加主控板整体电流不大但接线质量很关键。因为算盘是木结构我用导线把所有MCP23017的地线单独汇流之后统一接到主控板地没有让地电流经过算盘框架或机箱金属件避免形成环路。电源用的是一根USB口的5V供电最大输出2A实际整机功耗不到300mA留了足够余量。I2C从设备分布在算盘各处我特意给每颗MCP23017的电源引脚旁边加了100nF陶瓷电容用来滤掉电源线上的高频噪声。调试的时候发现加了电容之后I2C通信的错误率明显下降。这个细节在很多教程里不会提但做这种多点分布的硬件最好养成习惯。5. 场景扩展与后续改造方向5.1 盘点录入台、教学演示与防错录入项目交付之后客户用得很顺手但我发现它的价值不止于此。数据录入终端最核心的竞争力是“物理可见、状态保持”这些特性在不少场景里都能用上。比如仓库盘点两个人配合一个人拨算盘一个人报账录入员只需要确认算盘数字和单据一致就按一下提交比之前逐个敲键盘快很多而且错误发生的位置能精确到拨珠的瞬间方便追溯。教学场景是个意外发现。有个做小学数学创客教育的朋友看了之后立刻想把它改成课堂演示教具算盘背后接一个大屏幕老师拨珠屏幕上同步显示数字演变过程学生能直观看到“516”在算盘上是怎么拨出来的。这个改动非常简单只需要把HID输出改成串口输出再写一个简单的可视化界面就行。对于任何需要批量录入数字的岗位这个终端都能当防错工具用。传统键盘输入你只能事后看屏幕核对算盘终端是事前就把数字摆在眼前录入动作的多余步骤反而成了纠错的机会。工业上很多“防呆”设计就是这个思路用物理方式减少出错的概率。5.2 无线化、LED反馈与更大列数扩展如果要继续往产品级方向做第一个改造点就是无线化。把主控板换成ESP32-S2或ESP32-S3它们有原生USB HID和BLE蓝牙两种模式可以做到无线蓝牙键盘形态在平板上也能用。我已经在测试板上跑通了ESP32-S3的方案固件逻辑基本不用改把Keyboard库换成ESP32的BLEKeyboard库就行。第二个改造点是每列加一个LED数字反馈。现在没有屏幕用户只能靠看算盘本身确认数值但在光线暗的环境里不太方便。可以在每根柱子旁边贴一个小LED数码管或直接用WS2812灯条做氛围提示数据发送后按列点亮对应数字。这个功能对教学场景尤其有用。第三个方向是支持更多列数。中国传统的上位算盘有时是17柱甚至21柱MCP23017的级联数量足够支持固件里NUM_COLUMNS改一下就好。如果担心扫描延迟I2C在400kHz频率下读65路状态大概只要几毫秒远不到人可感知的范围。想让数据录入终端变得更顺手最大的空间其实在交互层比如加一个语音播报确认、加一个脚踏开关提交、做成左右手分离的双算盘录入台都是很有意思的玩法。最后说点个人体会。这个项目最让我意外的不是硬件或代码的难度而是传统物件在现代交互里依然有生命力。算盘是一个简洁的输入装置珠子就是状态位置就是数据不需要任何软件层面的缓存就能保持住当前值这在电子设备全部“掉电即丢”的时代几乎是一股清流。改造过程中我一直在想数据录入终端不一定非得是冷冰冰的键盘加屏幕只要能减少错误、提高效率老物件反而有独特优势。后面如果再迭代我大概率会保留算盘这个形态只往里面加更顺手的反馈和连接方式让老工具继续干新活。