Tihle:一个独特的TI计算器仿真器

2020-07-30 07:37:11

今天我要发布Tihle,这是一个新的仿真器,面向TI图形计算器(目前只有83+,但可能会更晚)。关于它有很多话要说,但在这里,我将讨论开发新仿真器的动机和最新技术,然后是关于设计和初始开发过程的技术说明。

请继续阅读该讨论,或者直接跳转到GitLab上的项目主页,GitLab上有一个在您的Web浏览器和其他资源(包括源代码)中运行的livedemo。

我还录制了这篇文章的音频版本,嵌入在这里,你可能更喜欢听而不是读这7000个单词。1它与书面版本基本一致,但省略了一些难以合理叙述的脚注和代码。运行时间约为35分钟。

加载音频失败。

长期以来,我一直参与为TI-83+系列图形计算器编程的社区;我最初就是在这个平台上开始编程的。这些天来,我在编写程序中扮演的更多的是咨询而不是积极的角色,我在幕后运行Cemetech的大部分内容,并偶尔向其他人提供输入。我认为计算器是向人们介绍编程的一种很好的方式,因为它们是现成的设备,不会太复杂而无法入门,同时仍然有足够的能力支持有经验的开发人员做有趣的事情(因为它们是容易访问的嵌入式系统)。我完全归功于开始编程计算器,因为它最终以专业的嵌入式系统编程而告终。

不幸的是,TI计算器的编程前景似乎岌岌可危。大约一个月前,有消息称,用于TI-84+CE(83+的最新版本,具有彩色屏幕和改进的eZ80处理器)的新版操作系统将取消对运行本机代码的支持。

虽然这并不一定影响旧的计算器或程序,但从长远来看,它似乎意味着计算器的厄运,特别是作为开发嵌入式软件的一条道路。用BASIC计算器方言编写的程序仍然可以访问,新的Python实现在一定程度上填补了这一空白,但它们缺乏深度-用户可能会花费时间和精力开发仅受以前运行的硬件限制的本地程序,未来用户将仅限于TI软件提供的那些功能。

就像我认为Scratch是一个不错的编程入门,但完全隐藏了有趣的细节,并且没有任何人认真地使用它,而是将其用作学习工具一样,我也相信取消对运行本机代码的支持最终将意味着人们将不再拥有计算器(通常是必需的学习设备!)。作为严肃编程的有用入口点。

除了失去了向人们介绍编程的方法之外,取消对本机代码的支持还有效地丢弃了一个可以追溯到20多年前的大型现有程序库(1996年首次上线的ticalc.org很好地体现了这一点)。虽然相对于TI-83+系列整体而言,84+CE是一个年轻的平台(并且与早期软件不兼容),但它已经拥有由用户创建的丰富程序库,当用户不能再在其设计的硬件上运行时,这些程序库实际上将丢失。

有效地破坏社区成员的工作的先例是困难的,我有动力去寻找保护它的方法。今天,像Apple II和Commodore64这样长期过时的家用计算机周围仍然存在着蓬勃发展的社区,我认为值得尝试提供类似的机会,通过努力让系统和软件保持持续的生命,使其进入一个充满敌意的未来,使其超出创建者的预期(或者更有意义的是,超出他们认为可以赚钱的范围)。

与那些仍有活跃社区的早期家用计算机相比,作为一个较新的平台,这些计算器可能更容易获得信息和资源,因为许多信息都是数字生成的,并且一直都是在线的。然而,这对保存也是危险的,因为如果物品在网上随时可以买到,如果原始物品消失了,可能就没有任何替代品了。例如,TI过去曾免费为83+提供SDK,但最近使其更难访问。也许更令人担忧的是,TI的网站似乎不再提供关于TI-84+CSE的任何信息,似乎否认它曾经存在过(尽管手册和软件仍然存在,如果你知道去哪里找的话)。

认识到对初级程序员失去有价值的资源和教育-工业复合体的磨练失去了有趣的历史的担忧,2我们该怎么办?

有许多网站记录计算器,并提供与之相关的资源-我已经参与其中,这些资源很有价值,通常保存完好,保存在互联网档案馆。但是,如果现有的资源在当前的硬件上是无用的,就像当大类程序不能运行时它们在很大程度上变得无用一样,那么就需要新的工具:我相信这种情况需要对计算器进行仿真,以使每个人都可以访问这个平台。

虽然它们中的大多数都足够精确,可以运行大多数程序,但这并不是全部:所有这些都需要ROM映像才能工作。ROM包括计算器的操作系统和引导代码,它们通过实现浮点算法和TI-BASIC解释器实现了键盘和显示处理的大量功能。

TI小心翼翼地保护他们的操作系统和引导代码,因为从他们的业务角度来看,计算器的全部价值在于它的软件。如果仿真器可以在更通用的机器上运行相同的软件,那么用户为什么要从TI购买100美元的设备呢?3但是如果我们想保留社区软件用于仿真,我们实际上并不关心TI感兴趣保护的相同功能。

虽然真正准确的仿真仍然要求所有操作系统功能都以相同的方式可用,但许多应用程序只使用其中的一小部分。那么,有哪些选项可用于模拟系统软件呢?

TI的状态方程(TI-OS)随计算器一起提供,可以从TI下载以更新计算器。然而,他们在2013年向操作系统添加了一项点击包装协议,试图禁止使用该操作系统进行仿真。虽然这一条款的可执行性有问题,但没有人(包括我自己)对挑战它非常感兴趣-主要是因为TI在历史上倾向于对ROM的分发睁一只眼闭一只眼。在一个更大、更公开的论坛中进行任何这样的分发都会增加请律师的风险,因此,基于上面讨论的这些和原因,我认为使用TI的操作系统进行普遍可用的仿真是站不住脚的。

Brandon Wilson的OS2似乎最相关,因为它想要重新实现TI-OS“但更好”。然而,在这一点上,它已经11年没有更新了,似乎已经死了。它还依赖于Zilog提供的汇编器和工具,它们只能在Windows上运行,并且是封闭源代码的。

Knight OS类似于UNIX,似乎有一系列有用的软件可供选择,但除了针对它的一小部分端口库之外,它与任何现有软件都不兼容。

其他许多人(包括我自己)都曾尝试编写操作系统,但在此不值得一提,因为它们的实际用处更小。

虽然计算器操作系统实现了大部分功能,但引导代码也是相关的-TI的操作系统使用引导代码中的一些支持代码,当然,引导代码负责处理系统加电顺序,并允许从无操作系统的情况下恢复。

这些计算器上的引导代码应该是只读的,4并且在出厂时已编程到设备中。因此,除了从物理计算器的内存中读取引导代码之外,没有正式的引导代码源代码,社区的共识是共享引导代码的副本将侵犯TI的版权。

本·“Floppus Maximus”穆迪曾经发布了名为“BootFree”的计算器引导代码的重新实现,除了与WabbitEmu集成的版本外,它似乎已经从互联网上消失了。虽然BootFree似乎消失的原因与TI提供的EOS下载中添加了点击包裹协议有关,5 6我认为同样的原因并不适用于非EOS软件的仿真-因此,BootFree似乎是追求使仿真对所有人都可用的一个很好的资源。

如果目标是自由地模拟计算器程序,我们认识到虽然引导代码的情况似乎与精确的模拟兼容,但是当前可用的libreOS是不够的。虽然模拟TI的EOS(模拟当前软件库的唯一合理选择)在技术上是可行的,但法律力量使其无法作为公开可用的模拟的基础,目的是允许任何人为该平台运行程序。

同样值得注意的是,在这些计算器上运行的程序有两个主要部分:它们可以用提供的BASIC方言(TI-BASIC)实现,或者作为计算器的机器代码分发。TI-OS实现了TI-BASIC的解释器,因此这两种语言都可以在现有的仿真器上运行,但是因为使用TI-OS对于这个项目来说不是一个选项,所以我们需要选择如何处理事情。仿真项目的可能性空间分叉:

虽然我过去曾探索过从头开始创建TI-BASIC解释器的可行性,但它从未取得太大进展,我陷入了对计算器相当独特的十进制浮点格式的精确仿真。7相反,我选择在不需要TI-OS的全机模拟器上工作。

将计算器程序的仿真与游戏系统的仿真进行比较是很有趣的(这也是常见的仿真目标)。在游戏系统中,对仿真的兴趣几乎完全集中在独立于系统的程序(游戏)上。同样,我目前对提供碰巧在计算器上运行的第三方程序的仿真很感兴趣。

游戏系统倾向于有少量游戏软件可能使用的平台代码,仿真器往往会隐藏这些平台代码的存在:极少数仿真器用户将有能力从他们拥有的游戏系统中提取固件,虽然大多数用户可能会为了获得游戏软件的副本而侵犯版权,以便在仿真器上运行,但仿真器的存在归功于它们本身不包含任何侵犯版权的代码。

在计算器上,用现有的仿真器模拟有趣的程序需要系统软件的副本,理论上需要访问物理计算器才能获得。这意味着模拟器可以被可靠地认为不会违反任何法律,尽管进入的门槛提高了。然而,这些程序本身几乎完全是由其作者免费提供的-在编写计算器软件的业余爱好者中,该平台在很大程度上是开放的。如果仿真器不需要计算器的ROM映像,那么向所有参与者免费提供现有的软件库是可行的。

游戏系统仿真器倾向于通过高级仿真来解决它们无法分发系统固件的问题:可以识别软件可能执行的各种操作,并且仿真器可以自己实现这些操作。这种方法既避免了对系统固件实现的任何依赖,而且通常可以获得更好的性能,因为仿真器不需要忠实地仿真底层进程-只有它的副作用在范围内。虽然高级仿真往往会以可能导致仿真软件不正确行为的方式牺牲准确性,但这通常是因为仿真器必须采取捷径才能获得可接受的性能。当用运行在几兆赫兹的已有40年历史的CPU架构模拟acalculator时,在没有明显捷径的情况下实现可接受的性能应该容易得多。8个。

有了现有仿真器的先例,这些仿真器非常忠于已知的硬件行为,并决定没有任何现有的计算器操作系统适合于应用程序,TIHLE就诞生了。这个名称表明了该特性对其存在的重要性:它是TI计算器的高级仿真器。9个。

心中有了计划,我不得不开始建造一些东西。我本可以从现有仿真器的核心开始,但有几个理由(主要)从头开始:

代码质量是高度可变的,我不熟悉他们的代码,我不知道他们中可能存在什么样的错误和缺陷。

可移植性是有问题的,因为它们大多是在各种风格的C或C++中实现的,并且通常将Windows作为主要目标系统。

我是Rust的忠实粉丝,我知道Rust程序的可移植性非常好,因为语言实现的标准库在抽象平台细节方面做得相当好。此外,我知道编译器对WebAssembly作为平台有很好的支持,这应该会使在Web浏览器上运行的端口变得很容易。在Web浏览器中运行是可取的,因为这是允许人们模拟类别的最容易访问的方式-例如,参见Emulality,它支持Internet Archive上数以千计的程序的模拟。

虽然我选择从头开始使用Rust编写的仿真器,但我仍然不想花费精力从头开始实现CPU仿真。由于Z80是一个如此老的CPU,并且使用如此广泛,所以有许多现有的仿真器可以让我使用它的内核。Rust很容易链接到C代码,所以有很多选择需要考虑。其中最引人注目的是:

FUSE也非常成熟,模仿ZX Spectrum。它还享有高准确度的声誉,这在一定程度上要归功于其全面的测试套件。

我最终选择了一个不属于任何主要仿真器的核心:Manuel Sainz de Baranda y Goñi的“redcode”核心。它是用非常便携的C编写的,被设计成一个库,所以只需很少的设置或修改就可以在我的应用程序中使用。它确实依赖于提供的大型外部标头库,但我可以通过一些工作将其调整为更加独立。

我倾向于在我的软件上使用许可许可,但是redcode Z80COREIS GPL许可的。因为将它链接到我的二进制文件中将需要我的代码也是GPL,所以我更愿意使用具有更多许可的核心,但是接受GPL来换取不需要自己实现核心似乎是一种公平和权宜之计的交易。将来,我可能会选择实现我自己的内核,并更改tihle的许可证,但目前它将是版权所有。

构建CPU核心并使其运行一些代码并不是非常困难,这就留下了如何实现高级仿真的核心问题。仿真器需要能够识别CPU何时需要“捕获”仿真器提供的操作。

我最初将陷阱定义为简单地对指令提取采取一些操作,当CPU尝试读取选定地址的内存时,仿真器将根据正在读取的内存地址执行陷阱处理程序,然后返回与适合陷阱的指令等价的数据-通常只返回ret指令的值C9。

当我测试仿真器时,这种方法很快被证明是有问题的。我选择Phoenix作为初始开发的目标程序,因为它是一种经典的计算器游戏,作为额外的好处,它不应该太严格地依赖于仿真的准确性-Phoenix的设计是可移植到许多计算器上的,所以希望它不包含关于系统的太多假设。

不幸的是,我很快就在Phoenix生成随机数的方式上遇到了问题,它的fast_Random例程使用一些自修改代码从半随机内存地址读取值,通过几次移位和XOR混合读取的值。它读取的初始地址是CPU重置向量0x0000,我还将其定义为终止仿真的陷阱。为了解决这个问题,我修改了重置检测:不是简单地捕获来自该地址的读取,而是使“Terminate”标志由对端口255的任何写入控制,该端口在计算器上未使用。通过将代码放在设置向量中写入端口255,我们可以避免虚假终止。

很快就会发现,将终止与其他陷阱分开处理效果不佳。如果程序决定从0x0028 for instance读取一个值(其中控制流跳转以执行系统例程),它将导致采取不正确的操作,这几乎肯定会导致程序开始不正确地运行。所需要的是一种确保陷阱只在执行目标代码时被捕获的方法,而不是在系统碰巧将给定的内存地址读取为数据的情况下捕获。这不是一个迫在眉睫的问题,但这一点稍后会得到解决。

内存子系统的初始实现只模拟RAM,而不是保存操作系统的计算器中的闪存。常规程序只能从RAM中执行,因此很容易捕获任何对Flash的访问。一旦对Phoenix的模拟达到一定程度,这就成了一个问题,因为事实证明,我选择了使用菲尼克斯的MirageOS版本,这意味着对Flash内容的另一个依赖是我没有预料到的。10 MirageOS11是一个实现为Flash应用程序的shell;它直接从Flash运行,并为旨在利用它的程序提供有用的支持例程。

这个版本的菲尼克斯使用幻影提供的setupint例程来实现控制游戏速度的计时器中断。由于幻影不是开源的,我要么对其工作细节进行反向工程,要么简单地将幻影映射到内存中。我选择了后者,因为幻影是可以自由再分发的,在仿真器陷阱方面,模仿它总是比忠实地实现它的功能要容易得多。

在将幻影添加到系统中后,事情仍然不起作用。我花了一些时间进行反向工程设置,并将其与仿真器中的执行跟踪进行了艰苦的比较,例程采用的流程似乎是合理的。它看起来像是在设置后的第一个中断服务时跳到了不正确的地方!检查指定内核中断向量表位置的I寄存器的值,似乎不正确-这意味着内核有问题。让我们开始冒险吧(如果您不想了解我的调试过程,也可以跳到下一节):

MIRAGE将其中断向量表安装到位于0x8B00的内存块,并用值0x8A填充。由于83+上的自定义中断必须始终使用Z80的中断模式2,但计算器未设计为使用此模式,因此这可确保所有中断都将指向0x8A8A。在加载I寄存器的0x71D2处中断执行:

ROM:71CE ld hl,8B00h;中断向量在8B00ROM:71D1 ld a,hROM:71D2 ld i,arom:71D4 dec a;A=8AROM:71D5 ld BC,101hROM:71D8 rst 28h;在8B00ROM:71D8填充256个字节的8A;----ROM:71D9 dw 4C33h;记忆集。

正如预期的那样,HL是0x8B00,A应该是8B,但是取而代之的是它的值0x84,并且在ld I之后,指令i的值没有改变!

深入研究后,我必须调试仿真器,而不仅仅是检查仿真的CPU状态。这实际上是莫。

.