使用 Linux 控制弹球机
老式电子弹球机非常吸引人,因为它体现了刚好在多面手黑客掌握之内的复杂性。您可以通过访问美国专利和商标局的开源存储库来了解其工作原理。Bally 制造公司使用围绕其 AS2518 微处理器单元 (MPU) 构建的系统,该系统在 1977 年至 1985 年间生产了超过 35 万台,美国专利号为 4,198,051。您可能还记得玩过 Evel Knievel、KISS、Mata Hari 或 Space Invaders 吗?
目前,您可以以不到 250 美元的价格购买到大多数无法正常工作的游戏机。许多游戏机都附带原始文档,其中包括电路原理图。结合您可以从专利和其他出版物中学到的知识,以及您对 PC 硬件和免费开源软件的了解,您可以拼凑出一些独特的东西:一台可以正常工作、支持 Web 的经典弹球机,它按照您的规则运行,运行您的程序。您可以合法地做到这一点,而且成本低于更换 MPU 板的成本,只需一台旧 PC 和像 Fedora 这样的标准 Linux 发行版即可。
逆向工程 AS2518 MPU 是我工业技术硕士论文的主题。无法正常工作的游戏机通常会遭受我们在旧电脑主板上看到的相同悲惨的设计缺陷。图 1 显示了焊接在 MPU 上的泄漏的镍镉电池造成的损坏。它不仅破坏了 IC 插座中的电气连接,还腐蚀了将 MPU 连接到系统其余部分的线束。
其他电路板通常仍然完好无损。当您开始处理您的游戏机时,请检查测试点的电压以确保这一点。我选择完全废弃不可靠的 +5 VDC 电路,并使用 PC 的电源。移除 MPU 后,您会剩下四个线束,总共有 66 根电线。要将您的 PC 连接到弹球机,您需要构建一个带有匹配接头引脚的接口板。设计目标是在所有电线上产生与原始 MPU 相同的输入和输出。这似乎是一项艰巨的任务,但请记住,这是 20 世纪 80 年代的技术。我使用迭代、分解、设计、构建和测试的方法,一次逆向工程一个子系统。
这个项目与典型的模拟器的区别在于,它没有参考 MPU 固件上编码的原始程序。相反,我采用了一种黑盒或净室方法,基于研究它们的功能而不是它们的内部结构。对我来说,将这 66 个电气连接解释为它们在闭环过程控制模型中的用途是有意义的。也就是说,每个连接要么是输入、输出、反馈电路的一部分,要么是电源的一部分。弹球机控制系统的四个主要部分是螺线管、开关矩阵、功能灯和数字显示器。我故意在第一个原型中忽略了数字显示器,这就是为什么该装置使用计算机显示器来显示分数。分析得出了图 2 所示的过程模型。
面对总共 11 个输入和 20 个输出,并希望有扩展空间,我决定构建一个 48 端口数字 I/O 板。通过一些网络搜索可以找到设计,并且可以从 Jameco 订购组件。英特尔 8255 并行外围接口 (PPI) 集成电路提供两个 8 位端口和两个 4 位端口,每个端口都可以配置为输入或输出。在我的板上,我将其中两个 IC 硬连线到地址 0x280-0x283 和 0x2A0-0x2A3。每个地址的前三个字节都内存映射到上述端口。第四个字节用于控制端口设置。我使用了一段十英尺长的 25 对双绞线电缆,通过螺钉端子将其连接到接口板。如图 3 所示,这绝对是一种 hack。您可能想使用 50 芯 SCSI 电缆和接头引脚。
AS2518 MPU 基于摩托罗拉 6800 微处理器。它使用两个 6820 外围接口适配器 (PIA) 为系统的其余部分提供 I/O。英特尔 8255 在功能上与之类似。必须在接口板上复制的是 PIA I/O 线和接头引脚之间的电路元件。这些是通过直接检查和研究专利和操作手册随附的电路原理图确定的,主要由电阻器和电容器组成。图 4 显示了我创建的板的照片。标签机非常适合标记电线和连接器。
首先,我尝试使控制系统作为普通用户空间程序工作。使用分而治之的方法,弹球机最容易 hack 的子系统是连续螺线管。它们要么长时间开启,要么长时间关闭。在我的游戏机上,我只实现了挡板继电器,它在正常游戏过程中开启,在游戏结束或倾斜时关闭,这样挡板按钮就不会起作用。这个操作很容易通过我编写的 C 程序的变体来完成,该程序用于测试 I/O 板。根据原理图,挡板继电器通过使其输出低电平而不是高电平来启用。这被称为负逻辑。我很快了解了 PC 架构的一些知识:即使使用上拉电阻,端口从计算机通电的那一刻起就处于低电平状态。这产生了在控制程序甚至启动之前就打开挡板的意外结果。为了解决这个问题,我在接口板上添加了一个 7404 反相器。现在,当输出设置为高电平时,挡板被启用。
接下来,按复杂程度排序,是瞬时螺线管的控制。这些东西包括弹簧保险杠、钟声、弹弓、飞碟和出球孔踢球器,它们在整个游戏中短暂爆发。Bally 文档指出,大多数通电时间为 26 毫秒;有些,例如落靶复位,通电时间是其两倍。为了触发 16 个可能的螺线管之一,使用了五条输出线来驱动螺线管驱动器板上的 74LS154 解码器。四条线提供所需螺线管的二进制表示,一条线启用或禁用解码器输出。每个输出依次驱动 16 个瞬时螺线管之一。
与连续螺线管一样,74LS154 使能使用负逻辑。编程这个动作似乎很简单。从使能高电平开始。输出四位螺线管编号,将使能设置为低电平持续所需时间,然后再次将其设置为高电平。实际上,这会产生一个问题,即普通 Linux 用户进程在实时行为方面的能力受到挑战。您不能依赖 usleep(26000) 来精确产生 26 毫秒的延迟;它可能并且经常会产生更长的延迟,正如手册页警告的那样。如果螺线管使能时间超过 100 毫秒,可能会损坏它并熔断保险丝。《端口编程 HOWTO》中讨论的一种选择是使用多个 outb() 调用,因为每个调用大约需要一微秒才能执行。然而,这相当于在忙循环中浪费大量 CPU 时间。
当我开始实现开关矩阵时,用户空间控制进程的前景更加黯淡。Bally 文档解释说,每 8.3 毫秒创建一个开关矩阵的快照,然后分析更改,例如当弹球撞击游戏场上的众多开关之一时。它是一个矩阵,因为 40 个单独的开关被连接到五行八列。行是输出,列是输入。逻辑高电平输出到第一行,也称为选通行。在短暂延迟以允许在电路的另一端检测到电压后,输入操作将八个单位列读取为一个字节的数据。然后对下一行重复该过程,依此类推。
这里,实时要求对于正确的游戏操作变得至关重要。如果在行选通和列输入之间没有创建足够的延迟,您将得到垃圾数据;游戏的闭环反馈系统将失效。如果在每个样本之间经过的时间太长,例如当进程被调度程序换出时,可能会错过开关关闭。确保控制进程以高频率(120 赫兹)执行的挑战使我从用户空间转向内核。
我编写的模块基于优秀教程 Linux 内核模块编程指南 中给出的示例。每个内核模块都需要一个初始化函数,该函数在通过 insmod 安装模块时调用。在这里,我将控制字写入两个 8255 PPI,定义哪些端口用于输入,哪些端口用于输出。这也是注册字符设备文件的好地方,字符设备文件是内核空间和用户空间之间进行通信的简单方法。我创建了一个名为 /dev/pmrek 的设备文件。
为了将此模块转换为定期进程,我为其声明了一个工作队列。工作队列是 2.6 内核的新功能。我想使用工作队列调用的设备驱动程序中的函数是 pmrek_process_io()。工作队列在模块代码的全局级别定义,语句如下
static struct workqueue_struct * pmrek_workqueue; static struct work_struct pmrek_task; static DECLARE_WORK(pmrek_task, pmrek_process_io, NULL);
然后,在模块初始化函数 pmrek_init() 中,使用以下代码创建工作队列
pmrek_workqueue = create_workqueue(pmrek_WORKQUEUE);
这实际上并没有调度工作队列。这发生在监管程序激活它时。图 5 是 pmrek_process_io() 执行的底层硬件 I/O 操作的流程图。
它做的第一件事是使用 inb() 读取开关列。如果检测到任何有效的开关,它们将被写入日志缓冲区。此日志缓冲区由监管进程使用,游戏玩法会根据检测到的开关而推进。开关检测会通过内联汇编命令获取 CPU 实时时间戳计数器 (RTSC) 来标记其发生的准确时间
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (cpu_time));
这将 cpu_time 设置为自启动以来发生的 CPU 机器周期数。它对于精确的计时测量非常方便。某些开关,例如弹簧保险杠和弹弓,需要立即的螺线管响应。
接下来,任何排队的命令都通过调用函数 pmrek_process_commands() 按顺序执行。命令可以从监管程序通过写入 /dev/pmrek 发送,也可以源自模块本身。如果要触发瞬时螺线管,则使用 outb() 输出四位螺线管编号。然后将使能输出设置为高电平以打开 74LS154 解码器输出。使能持续时间由一个计数器保持,该计数器由工作队列进程延迟(三毫秒)递减。因此,26 毫秒的螺线管脉冲将需要八个工作队列周期,然后使能位才会再次设置为低电平以将其关闭。
接下来,控制进程服务于功能灯。AS2518 架构包括一个灯驱动器板,该板上装有 60 个硅控整流器 (SCR),用于选择性地打开或关闭游戏场和背板上的各个灯泡。与瞬时螺线管一样,这些 SCR 由解码器驱动,解码器接受四位输入并打开 16 个输出之一。为了处理所有 60 个功能灯,有四个解码器。控制程序遍历 16 个位置,并选择性地打开与其关联的四个灯中的任何一个。所有这些都必须在 120 赫兹整流直流电源波形的每个周期开始时完成。在 AS2518 上,这是通过电源过零检测器触发的中断来实现的。我决定不使用中断。相反,我采用了一种“霰弹枪”方法,以两倍或更快的速度执行控制进程,确保 SCR 在每个周期都被触发。
工作队列进程执行的最后一个 I/O 操作是为下一次读取开关矩阵输出下一个行选通。然后,该进程通过发出命令来重新调度自身
queue_delayed_work(pmrek_workqueue, &pmrek_task, pmrek_i.workqueue_delay);
数据结构 pmrek_i 包含有关弹球控制系统的各种信息,包括其工作队列延迟,其值为 3。内核定时器以 1,000Hz 运行,是内核的心跳。工作队列延迟是延迟工作执行之前的节拍数。使用这种机制,可以实现比在内核外部为普通用户进程调度的频率高得多的频率,并且就每次执行时使用的资源而言,它们效率更高。
并非弹球机控制系统中的所有内容都必须像底层硬件 I/O 操作那样频繁地执行。游戏玩法本身——机器如何响应开关检测、点亮不同的灯和增加玩家分数——作为普通用户进程运行良好。从某种意义上说,它实际上是底层 I/O 处理的监管控制器。
内核模块应该适用于每个基于 AS2518 MPU 的游戏机。您可以从 SourceForge.net 上的弹球机逆向工程工具包项目下载源代码,并为其内核编译它。然后,您需要编写监管控制软件来玩您正在 hack 的特定游戏。表 1 列出了此软件包中的其他源代码文件。
表 1. 弹球机逆向工程工具包的源代码
源代码文件 | 用途 |
---|---|
analyze_testbed_output.php | 使用 user_pmrek.exe 的解析文本文件输出和保存的系统活动记录来分析游戏。 |
common_functions.php | PHP 程序共享的函数。 |
Makefile_pmrek | 用于编译内核模块和可执行文件的 GNU Make 命令文件。 |
pmrek_bash_profile | 附加到自动登录用户的 bash 配置文件;调用 start_testbed。 |
pmrek.c | 用于硬件控制进程的 Linux 2.6 内核模块。 |
pmrek.h | 包含定义和数据结构的头文件。 |
pmrek.sql | 用于创建数据库、表和访问权限的 MySQL 脚本。 |
start_testbed | 用于运行独立测试台系统的 Shell 脚本;运行 testbed.exe,如果因升级而终止,则重新启动。 |
testbed.c | 用于控制内核模块、玩 Evel Knievel、记录和分析进程数据的监管进程;编译为可执行文件 testbed.exe。 |
testbed_performance.php | 创建所有分析游戏的摘要统计信息。 |
user_pmrek.c | 用于解析 testbed.exe 输出、显示数据结构大小和模拟内核模块操作的实用程序;编译为可执行文件 user_pmrek.exe。 |
您可以随意修改我为 Evel Knievel 编写的 C 程序 testbed.c。它使用 ncurses 屏幕处理包来提供控制台彩色显示和用户输入。诊断显示器显示开关矩阵、灯和最近触发的螺线管的配置。它还显示玩家分数,以及运行时统计信息,例如内核工作队列进程的平均周期频率和执行时间。可以输入键盘命令来打开或关闭连续螺线管、触发瞬时螺线管、打开或关闭功能灯以及调整工作队列延迟。图 6 显示了正在进行的游戏。请注意关闭的开关;这些是被击中的落靶。
监管程序通过读取 /dev/pmrek 接收从内核模块传递的事件,它使用系统调用 open() 打开 /dev/pmrek,就像任何其他文件一样。然后,通过写入 /dev/pmrek 将命令发送回模块。我尝试使主要功能对应于我对弹球游戏中关键事件的印象。它们列在表 2 中。
表 2. 监管控制程序功能
函数名称 | 用途 |
---|---|
game_add_player() | 当按下投币按钮(并且有币)以开始新游戏或添加更多玩家时调用。 |
game_ball_end() | 当在游戏中检测到出球孔开关时调用,以启动奖励倒计时,前进到下一个球、下一个玩家或结束游戏。 |
game_collect_bonus() | 在球结束后调用,以倒计时当前玩家的奖励。 |
game_segment_display() | 在计算机屏幕上模拟七段数字显示,用于显示玩家分数、比赛计数、投币数和游戏中球数。 |
game_lamp_update() | 在处理开关检测后调用,以一次更新所有功能灯的配置。 |
game_play_tune() | 通过以预定义的顺序触发钟声瞬时螺线管来播放各种曲调。 |
game_switch_response() | 为从内核模块检索的每个有效开关检测调用;启动与正常游戏操作相关的所有其他事件。 |
game_watchdog() | 每秒调用一次以检测游戏故障,包括错过的开关检测,并重新处理开关响应或终止程序。 |
process_output_file() | 由 fork 的子进程在游戏完成后调用,以分析游戏过程中记录的日志文件。 |
termination_handler() | 用于干净地结束程序的信号处理程序;关闭数据日志文件并将内核模块置于空闲状态。 |
main() | 主程序初始化内核模块数据结构、计算机屏幕并循环直到捕获到终止信号;主循环处理用户键盘输入、从内核模块读取事件、调用游戏进程函数、将日志文件写入磁盘并更新计算机屏幕显示。 |
您应该能够通过调整函数 game_switch_response() 和 game_lamp_update() 来使此代码适应您的特定游戏。如何在不偷看原始制造商源代码的情况下编写程序?游戏场本身上有很多线索,告诉您每个开关的得分等等。当然,您也可以创建自己的规则,或许可以改进原始设计中的弱点。
诊断显示器非常适合测试,但玩家分数太小。默认情况下,控制台模拟原始背板上的大型数字显示器,如图 7 所示。您可以通过按下弹球机投币门内的自检开关来进入诊断显示器。
2005 年 4 月,我们将游戏机带到了密歇根州卡拉马祖的 Pinball at the Zoo。数百人玩了这款游戏,收集了统计数据,我将这些数据用于我的硕士论文。每次游戏完成后,PHP 程序都会读取游戏程序创建的日志文件。它生成一个 HTML 文档,总结游戏的事件历史和有关其实时性能的统计信息。然后将这些结果存储在 MySQL 数据库中,以方便分析整体性能。图 8 是设置的框图。图 9 显示了游戏机的运行情况。
这个项目是 Linux 2.6 内核的成功案例。它证明了可以使用内核工作队列而不是复杂的硬件中断或额外的实时软件包(如 RTLinux)来创建复杂的实时过程控制应用程序。此外,通过选择弹球机,多面手黑客可以创造出真正有用且有趣的东西。
本文资源: /article/8529。
John R. Bork 是位于俄亥俄州芬德利的 Marathon Petroleum Company 的 IT 系统集成商。自 1999 年以来,他一直在 hack Linux 和弹球机。