成本不到 40 元!DIY 大神用树莓派,重现 40 年前、售价 1.8 万元的 Mac

源码共读

共 10863字,需浏览 22分钟

 ·

2024-07-28 18:00

架构师大咖
架构师大咖,打造有价值的架构师交流平台。分享架构师干货、教程、课程、资讯。架构师大咖,每日推送。
公众号

这件事的起因,源于对 RP2040 MCU(Raspberry Pi 的首款微控制器)的一次讨论。

当时大家正在探讨如何为 RP2040 MCU 构建一个简单的桌面/图形用户界面,我便随口说了句“要么,干脆运行一些旧操作系统算了”。说完之后,我突然联想到了最初的 Macintosh。

Macintosh 最早发布于 40 年前,是一款硬件非常简单却相当酷的设备。不过内存方面有些紧张:最初的 128KB 版本内存不足,仅售卖了几个月便被 512K 的 Macintosh 取代,可以看出 512K 的内存似乎更为适当。

尽管如此,128KB 版本仍能运行一些真正的应用程序。虽然当时它还没有 MultiFinder/实际多任务处理功能,我依然觉得它很有魅力。

在 1984 年,Mac 的价格大约是 VW Golf(大众高尔夫,一款小型家庭轿车)的三分之一。但如今,我想用这块售价 3.80 英镑(合人民币约 34.8 元)的 RPi Pico 微控制器板来试试:RP2040 有 264KB 内存,扣除 Mac 的 128KB 之后还剩下很多空间可以用——如果能快速搞定,然后在这上面玩 Mac,那该有多酷?

一段时间过去后,我真的做到了:

说出来你可能不信,这个高质量项目我没花多长时间就完成了。软件显然是最关键的部分,我分成了 3 个不同的项目来进行。

那么接下来,你将看到一个关于我这场“开发之旅”的故事。


什么是 pico-mac?

它是一个基于 Raspberry Pi RP2040 微控制器(安装在 Pico 板上)的系统,能够驱动单色 VGA 视频并接受 USB 键盘/鼠标输入,仿真 Macintosh 128K 计算机及其磁盘存储。RP2040 的 RAM 容量足以容纳 Mac 的内存和仿真器的内存。通过一些小技巧,它的速度能达到真实 Macintosh 的性能,还具备 USB 主机功能,并且 PIO 模块使得驱动 VGA 视频也相对简单。基本版 Pico 板的 2MB 闪存足以容纳操作系统和软件的磁盘映像。

以下是 Pico MicroMac 的实际运行情况,为未来的“无纸化办公”做好了准备:

(未来的 Pico MicroMac RISC CISC 工作站)

我以前没怎么用过 Mac 128K,只在博物馆的机器上点击过几下。但我知道它们可以运行 MacDraw、MacWrite 和 MacPaint——对于 128K 设备来说,这三个应用程序非常棒:一个基本所见即所得的文字处理器,带有多种字体,还有一个矢量绘图软件。

如果想体验早期 Macintosh 系统软件和这些出色的应用软件,有一个方法是访问 https://infinitemac.org,这个网站通过 emscript 把 Mini vMac 仿真器封装在浏览器中运行(强烈推荐,有很多有趣内容可以玩)

提前剧透一下,我开发的 MicroMac 确实可以运行 MacDraw,在“仿真硬件”上用它非常有趣:

如果你也想做一个自己的 Pico-Mac,可以参考 GitHub 链接(https://github.com/evansm7/pico-mac),里面有具体的制作说明。


我的开发之旅,开始!

细想了一下,其实我一开始并没有打算做一个 Pico 项目,只是隐约对它是否可行有点感兴趣,于是开始在我的普通电脑上捣鼓制作了一个 Mac 128K仿真器。

三条规则

对于这个项目,我最初定了几条简单的规则:

  • 必须要做有趣的事。为了让它能正常运行,黑进去做一些改动也是可以的。

  • 我喜欢写仿真程序,但我不想深入学习和了解 68K 汇编语言。我知道有很多人喜欢 68K,它也确实很好,但我不喜欢把它作为 CPU。所以,一开始我本想直接用别人做好的现成 68K 解释器。

  • 同样,我还想深入了解很多操作系统的内部结构,但早期的 Mac 系统软件并不在我的考虑之列。我只需要进入系统、模拟硬件、把操作系统作为黑盒启动,就可以了。

但在整个项目过程中,我经常打破上述规则,有时是两条,有时是全部。


Mac 128K

这类机器一般都非常简单,也符合当时的时代特征。我从原理图和《Inside Macintosh》开始学习,这些 PDF 文件涵盖了原始 Mac 硬件、内存映射、鼠标/键盘等各种细节。

Macintosh 的硬件配置:

  • 运行频率大约 8MHz 的 Motorola 68000 CPU;

  • 扁平内存结构,将内存解码为不同区域,用于内存映射 IO,连接到 6522 VIA、8530 SCC 和 IWM 软盘控制器(某些地址解码有些复杂)

  • 键盘和鼠标通过 VIA/SCC 芯片连接。

  • 没有外部中断控制器:68K 有 3 条 IRQ 线,对应 3 个 IRQ 来源(VIA、SCC、编程器开关/NMI)

  • 没有插槽或扩展卡。

  • 没有 DMA 控制器:一个简单的自主 PAL 状态机从 DRAM 中扫描视频和音频样本。视频分辨率固定为 512x342 1BPP。

  • 唯一的存储设备是内部软盘驱动器(外加一个外部驱动器),由 IWM 芯片驱动。

前三款 Mac 型号非常相似:

  • Mac 128K 和 Mac 512K 是同一款设备,只是内存不同。

  • Mac Plus 在内存映射中添加了 SCSI 接口和一个 800K 软盘驱动器,该驱动器是双面的,而原来的软驱是单面 400K。

  • Mac Plus 的 ROM 也支持 128K/512K,是 Macintosh 512Ke 的升级版。其中 ‘e’ 表示额外的 ROM 功能。

Mac Plus 的 ROM 支持 HD20 外部硬盘和 HFS 文件系统,Steve Chamberlin 对其拆解进行了注释。这就是我要使用的 ROM:我正在制作一台 Macintosh 128Ke。


Mac 仿真器:umac

经过大约 8 分钟的研究,我选择了 Musashi 68K 解释器。它是用 C 语言编写的,接口简单,并且提供了一个简单的、开箱即用的 68K 系统示例,包含 RAM、ROM 和一些 IO。Musashi 适合嵌入到更大的项目中:连接内存读/写回调、一个引发 IRQ 的函数,并在循环中调用执行,完成。

我开始围绕它构建一个仿真器,最终这个项目成为了 umac。前半部分进行得相当顺利:

1、构建一个简单的命令行应用程序,加载 ROM 镜像,分配 RAM、提供调试消息、断言和日志记录,并配置 Musashi。

2、添加地址解码:将 CPU 的读/写操作引导到 RAM 或 ROM。“overlay”寄存器使得 ROM 可以在 0x00000000 地址启动,然后在设置 CPU 异常向量后跳转到一个高地址的 ROM 镜像——这会影响地址解码。这是通过操作 VIA 寄存器完成的,所以现在只解码了该寄存器的一部分。

3、此时,ROM 开始运行并访问更多不存在的 VIA 和 SCC 寄存器。于是添加更多的地址解码和一个模拟这些设备的框架——让 MMIO 读/写操作只是被简单地标记出来。

4、有一些 ROM 访问的特殊地址会“错过”记录在案的设备:有一个制造测试选项,它会探测是否有插件,然后我们就会看到 RAM 大小的探测结果。Mac Plus ROM 正在寻找最多 4MB 的 RAM。在分配给 RAM 的大区域中,实际 RAM 的较小容量被反复镜像,因此探针会在高地址和开始环绕的点写入一个特殊值,

5、然后初始化 RAM 并填充已知模式。这是一个令人兴奋的时刻,因为我可以转储 RAM,将用于视频帧缓冲区的区域转换为图像,并看到用于 RAM 测试的“对角条纹”图案!

6、并非所有设备代码都喜欢读取全零值,所以有时需要参考反汇编并返回 0xffffffff 以推动它进一步运行。我们的目标是让它能够访问 IWM 芯片,即尝试加载操作系统。

7、在看到一些 IWM 访问并返回随机无意义的值后,第一个美妙的时刻是出现了带问号的“未知磁盘”图标——真正的图形!ROM 真的在做一些事!

8、此时我还没有实现任何 IRQ,并发现 ROM 进入了一个无限循环:它在计算几个 Vsync 以延迟闪烁的问号。于是我转向了更好的 VIA,它能为 GPIO 寄存器读/写和 IRQ 处理提供回调。这还需要连接到 Musashi 的 IRQ 函数。

以上过程很大程度上激励了我继续做下去——记住规则一:尽管这是通过手动内存转储和 ImageMagick 转换才看到的“图形”,但这依然很棒。


IWM、68K 和磁盘驱动程序

其实在上个步骤中,我就知道 IWM 是一款很“有趣”的芯片,但对具体细节不太了解,因此打算在需要时再弄清楚——幸亏我把研究 IWM 的事拖到了现在。如果我在项目开始时就读了它的“数据手册”(一份含糊不清的寄存器文档),我肯定会原地放弃。

IWM 确实很不错,但它非常底层。其他同时代机器的磁盘控制器,例如 WD1770,会抽象出磁盘的物理操作,所以在某种程度上,你只需拨动寄存器,让控制器步进到第 17 条轨道,然后抓取第 3 扇区。但 IWM 不是这样的:首先,磁盘是恒线速度的,这意味着角速度需要根据当前轨道进行调整;其次,IWM 只会给 CPU 提供从磁盘头读取的大量原始数据(几乎没有解码)

我花了很长时间阅读 ROM 中 IWM 驱动程序的反汇编代码(违反了规则 1 和规则 2):驱动程序包含某种伺服控制环路,通过调节发送到 DAC 的 PWM 值来控制磁盘马达,并与 VIA 定时器的参考值进行比较,以实现动态速率匹配,从磁盘扇区获取正确的比特率。我认为,一旦找到轨道起点,驱动程序就会将轨道数据流入内存,解码符号(更复杂的编码)并选择感兴趣的扇区。

说实话,我有点丧。我原以为像 Basilisk II 和 Mini vMac 这样的仿真器已经通过某种巧妙的方式解决了这个问题,因为它们能模拟软盘——但实际上它们并没有,而是直接避开了这个问题。

至于其他仿真器,对 ROM 进行了很多补丁处理:ROM 并不是未经修改就运行的。可能有人会说,虽然这样修改 ROM 它就不再是完美的硬件仿真了,但那又如何?嗯,我怀疑他们也遵循了规则 1,因为我也打算这样做。

我研究了一些 Mac 驱动程序接口的工作原理(唉,还是违反了规则 3),并理解了其他仿真器是如何进行补丁的。它们使用自定义的半虚拟化 68K 驱动程序,覆盖 ROM 中的 IWM 驱动程序,为来自块层的 .Sony 请求提供服务,并将其路由到更方便的主机端代码来管理这些请求。Basilisk II 使用了一些自定义的 68K 操作码和一个简单的驱动程序,而 Mini vMac 则使用了一个复杂的驱动程序,对自定义的内存区域进行“陷阱”访问。我重新使用了 Basilisk II 驱动程序,但将其转换为访问一个自定义区域(这样更容易路由:只需模拟另一个设备)。驱动程序的回调主机 / C 端执行,一些简化的 Basilisk II 代码解释请求,并将数据复制到操作系统提供的缓冲区或从中复制数据。这样一来,我只需要从一个磁盘读取块:不需要不同的格式(甚至不需要写入支持),也不需要多个驱动器,更不需要弹出/更换镜像。

从磁盘加载第一个数据块比第一部分花的总时间还长。我本来想着要不再学点 68K 汇编(又违反了规则 3……),但在这千钧一发之际,我看到了一个 Happy Mac 图标,表示系统软件开始加载。

这时,我的仿真器仍然是一个简单的 Linux 命令行应用程序,没有任何用户界面,没有键盘或鼠标,也没有视频输出。于是,我觉得是时候将它封装在一个 SDL2 前端中了,这样能实时看到屏幕重绘效果。我把 1Hz 的计时器中断添加到 VIA 中,它就成功启动了!

(第一次启动)

顺便一提,我试着为所有嵌入式项目都创建一个双目标构建,即一个用于快速原型设计/调试的本地主机构建,用 libSDL 代替 LCD,这意味着我不需要在 MCU 上编码。

接下来是鼠标支持。Macintosh 内部和原理图展示了它是如何与 VIA 和 SCC 连接的。SCC 是我在这台机器中第二个不喜欢的芯片:很复杂,数据手册似乎故意隐藏信息、惹恼读者、报复世界。但它能执行各种上世纪 80 年代的线路编码方案,减轻 CPU 的工作负担,对于支持 AppleTalk 等功能至关重要

到这一步,雏形几乎就完整了:有一个能工作的鼠标,我可以用 Mini vMac 构建一个新的磁盘镜像,其中还包含 Missile Command 这款游戏——不到 10KB,非常好玩。

总体来说:

  • 视频正常

  • 能从磁盘启动

  • 鼠标正常,Missile Command 也能运行

虽然还没有键盘,但大部分功能已经实现。是时候开始第二个子项目了。


硬件和 RP2040

与 umac 无关,我设计了一个电路和固件,目的有两个:

(1)用最少的组件将 512x342x1 的视频显示到 VGA 上。

(2)让 TinyUSB HID 示例正常工作并集成。

准确来说,这个项目只是为了将测试图像复制到帧缓冲区,并通过 printf() 输出键盘/鼠标,作为一个概念验证。视频部分的工作很有趣:虽然我之前做过一些 I2S 音频 PIO 的项目,但这次我想输出视频信号并随意控制 Vsync 和 Hsync。

为了测试,我需要一个电路。VGA 接口要求视频 R、G、B 信号最大电压为 0.7V,以及同步信号的某些电压(具体数值略)。R、G、B 信号对地电阻为 75Ω:经过计算,3.3V GPIO 通过 100Ω 电阻驱动这三个信号大致可行。

开始焊接的那天,我需要一个 VGA 接口。我手头虽说有一个 DB15 接头,但想把它用在另一个项目上,剪断 VGA 电缆也不太合适。午餐后散步时,我无意在街边发现了一些电缆,其中就有一根 VGA 电缆——虽然生锈了,但看起来有一种随性的美感。

(免费的 VGA 电缆)

VGA PIO 这部分非常有趣。最终,PIO 动态读取配置信息来控制 Hsync 宽度、显示位置等,然后用一些 DMA 技巧扫描出配置信息与帧缓冲区数据。通过正确的位移方向并使用 RP2040 DMA 上的字节交换选项,无需在 CPU 端进行拷贝或格式转换,就能直接输出大端 Mac 帧缓冲区。

不过,我总共重写了三次视频部分:

(1)第一个版本有两个 DMA 通道写入 PIO TX FIFO。第一个传输配置信息,然后触发第二个传输视频数据,接着引发 IRQ。然后,IRQ 处理程序会在短时间内选择要读取的新帧缓冲区地址,并重新对 DMA 进行编程。这种方法能正常工作,但对系统中的其他活动非常敏感。有个很明显的解决方法是,任何对延迟敏感的 IRQ 处理程序都必须具有 __not_in_flash_func() 属性,避免 RAM 耗尽。但即便如此,该设计也没有给重新配置 DMA 留出太多时间:快速移动鼠标时,会出现随机闪烁和空白。

(3)第二个版本采用了双缓冲区,目的是让 IRQ 处理程序的工作变得简单:快速插入预先准备好的 DMA 配置,然后在关键时刻计算出下次使用的缓冲区。这种方法的效果好了很多,但在高负载下仍会有一些故障。更奇怪的是,它有时会完全空白,需要重置,这让我困惑了好一阵子。最终我打印出了 PIO FIFO 的 FDEBUG 寄存器,试图在运行中发现错误。我看到 TXOVER 溢出标志被设置了,但这应该是不可能的:FIFO 根据需求从 DMA 拉取数据,带有 DMA 请求和基于信用的流量控制……哦,等等,如果信用发生混乱或重复,就会发生过多传输,导致接收端溢出。

我漏看了 RP2040 DMA 文档中的一个细节规则:“多个通道不应连接到相同的 DREQ。”

(3)因此第三个版本……并没有违反这个规则,但也变得更加复杂:

  • 一个 DMA 通道传输数据到 PIO TX FIFO

  • 另一个通道负责设置第一个通道,从配置数据缓冲区发送数据

  • 第三个通道负责设置第一个通道,从视频数据缓冲区发送数据

  • 第一个通道的设置触发相应的“next reprogram me”通道

除了不会出现锁定或视频损坏之外,还有一个好处就是在视频行扫描期间会触发 Hsync IRQ,从而大大缩短了重新配置 DMA 的时间限制。我还想进一步改进这一点(再增加一个 DMA 通道),让每行传输都不需要 IRQ,因为目前 IRQ 的开销约占 CPU 时间的 1%。

所以,现在我们有了一个可以嵌入 umac 的平台和固件框架,支持 HID 输入和视频输出。至此,硬件部分已完成,接下来交给软件团队。


回到仿真器的开发工作上

看了一眼本地 umac 二进制文件,我发现要在 Pico 上运行还需要解决一些问题:

  • Musashi 运行时在 RAM 中构建了一个巨大的操作码解码跳转表。这个表永远不会改变,也不会在运行时更改。我添加了一个 Musashi 构建时生成器,这样该表就可以设为 const(可存储在 flash 中)

  • 反汇编器占用了很大空间,而且在 Pico 上也没用,所以可以一个构建不包含反汇编器的版本。

  • Musashi 为了准确计算每条指令的执行周期,用了很多的大型查找表。虽然这对一些游戏主机来说很有用,但对 Mac 来说并不重要,因此我移除了这些查找表。

pico-mac 开始成形,ROM 和磁盘镜像存储在 flash 中,现在可以在 Pico 上构建并运行了!只要注意不要把东西塞进 RAM,RAM 的使用情况还是不错的。仿真器和 HID 代码总共使用了大约 35-40KB 的 Mac 128KB RAM 区域,还剩 95KB 以上的可用 RAM。

这正是为 umac 添加键盘支持的好时机。Mac 键盘通过 VIA 的“移位寄存器”串行接口连接,这是一个基本的同步串行接口。虽然逻辑上很简单,但在早期尝试响应 ROM 的“初始化”命令时总是被忽略。ROM 的反汇编又派上了大用场:在阅读键盘启动代码时,如果响应字节在请求发送后过早出现,就会导致中断确认的竞争条件。因此我在请求发送后插入了一个延迟,将响应延迟到稍后轮询,然后就只需要映射按键代码了。

有了键盘支持,就达到了 MacWrite 的最终关卡:

但有一个问题:它的表现完全不行,速度超级慢。我添加了一个 1Hz 的指令计数转储,发现它每秒只执行约 300 KIPS(千条指令)

68000 CPU 在 IPC 方面并不出色。虽然有些指令可以在 4 个周期内执行,但如果你想用那些复杂的寻址模式,访问内存会花费很多周期。当然我不是专家,但我觉得为大约 8MHz 的 68000 设定大约 1 MIPS(百万条指令)的目标并不过分,只需要提高 3 倍。


性能

我可没说我不会作弊:让我们把 Pico 的运行频率从 125MHz 提高到 250MHz。好是好了点,但也没好到翻倍,我记得好像只提高了大约 30%。

Musashi 有很多可配置选项。我的第一个目标是让主循环(从反汇编/编译后端来看)变小:Mac 不会报告总线错误,所以寄存器不需要副本展开。操作码总是从 16 位边界获取,因此不需要对齐检查,可以用半字加载(而不是将两个字节加载合并为一个半字)。对于 Cortex-M0+/armv6m ISA ,通过重新排列 CPU 上下文结构字段,可实现即时偏移访问和更好的代码。令人费解的是,CPU 类型是动态可变的,这导致了大量运行时的间接操作。

看起来好多了,也许有 2 倍的改进,但还不够。Missile Command 仍然很卡,鼠标也依旧不流畅!接下来,是一些较为激进的优化:删除地址对齐检查,因为在这种受限环境中不会发生未对齐的访问。

不过,真正的优化来自下面提及的另一个小技巧。


RP2040 内存访问

RP2040 拥有快速 RAM,该 RAM 采用多行设计,允许多个用户(如两个 CPU 和 DMA)进行单周期访问。默认情况下,大多数代码通过外部 QSPI 闪存的 XIP 运行,而 QSPI 通常以内核时钟(默认 125MHz)的速度运行,但随机读取一个字的延迟约为 20 个周期。为缩短这种延迟,RP2040 配备了一个 16KB 的简单缓存,但如果代码量大,就更容易在调用函数时触发 QSPI 读取。当超频到 250MHz 时,QSPI 无法达到那么快的频率,所以会保持在 125MHz。这样一来,当缓存未命中时, QSPI 的 20 个周期延迟会变成 40 个 CPU 周期。

这里的问题在于,Musashi 在构建时会生成大量代码,每 1968 个操作码都有一个函数,再加上一个 256KB 的操作码跳转表。即使内部执行循环非常高效,操作码分发和函数调用也可能在闪存缓存中未命中。如果我们想在 200 MIPS 的基础上实现 1 MIPS ,那这些延迟就会累加。

面对这种情况,可以用 __not_in_flash_func() 属性将指定函数复制到 RAM 中,以保证快速执行。最起码,主循环和内存访问函数需要这个属性,因为每条指令都需要访问一个操作码,并且很可能会读写 RAM 。

这样的优化,又提升了几个百分点的性能。

接下来,我尝试对整类操作码进行优化:移动很频繁,分支也很频繁,所以把它们放在 RAM 中。这确实能提高性能,但 RAM 很快就用完了,距离目标的 1 MIPS 也还有差距。

还记得我说 RISC 架构会改变一切吗?

我们想要加速的 1968 个 68K 操作码中,有哪些是最常用的?在 umac 中加入一个 64K 的计数器表,启动 Mac 并运行一些关键应用程序(实际上是玩了一会儿 Missile Command ),就能得到一份动态指令使用情况的统计概况。结果显示,最常用的 100 个操作码(占总数的 5%)占了 89% 的执行次数,而最常用的 200 个操作码则占了 98% 的执行次数。

根据这个统计结果,umac 在构建后对 Musashi 自动生成的代码进行处理,并将最常用的 200 个函数附上 __not_in_flash_func() 属性。这样只增加了 17KB 的 RAM 使用(剩余 95KB),而性能提升到了约 1.4 MIPS!

终于,我们可以流畅地享受 Missile Command 的黑暗主题了:


MacPaint 如何?

人人都爱 MacPaint,但你会发现我一直对它避而不谈,因为:

它无法在 Mac 128Ke 上运行,因为 Mac Plus ROM 使用的 RAM 比原来的多:我在 68kMLA 上看到过一个关于“Mac 256K”的讨论,很可能 Mac 128K 在实验室里实际上是 Mac 256K(甚至可能本来打算是 256K,但在发布前削减了成本),因为操作系统在 256KB RAM 下运行良好。

当时我在想,Mac ROM/OS 是否一定要是 2 的 n 次方才行?如果不是,那我还有 95K 空闲内存,能否制作一个“Mac 200K”,然后运行去 MacPaint?于是,我试了一个本地黑客程序,可以根据给定的内存大小修改 ROM,更新其全局 memTop 变量。结果不错,我还用 256K 、 208K 和 192K 进行了启动测试。不过,也有一些问题需要解决:如果内存大小不是 2 的 n 次方,ROM memtest 就会出错,而跳过这个测试又会导致其他问题。这些问题都可以解决,但有些启动过程会访问超出 RAM 末尾的区域。另外,2 的 n 次方以通过简单的地址掩码将 RAM 访问限制在有效缓冲区内,而 192K 无法做到这一点。

不幸的是,当我测试 MacPaint 时,它仍然无法运行,因为它需要将临时文件写入一个只读引导卷。这完全违反了规则 1,所以我们现在暂时还是保持 128KB。另外,256K MicroMac 是完全可行的,只需要一个内存容量为 300KB 的微控制器就大功告成了。

Python入门到精通
Python入门到精通:人生苦短,我用Python!Python每日推送、Python教程、Python资料、Python视频、Python项目、Python学习等。
公众号
浏览 48
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报