专栏名称: iOS开发
分享iOS相关技术文章、学习资料、视频教程、热点资讯、工具资源、课程书籍等。每天推送,欢迎投稿!
目录
相关文章推荐
中国证券报  ·  降费 ·  21 小时前  
证券时报  ·  11连阳后,基金公司发“预警”! ·  昨天  
上海证券报  ·  AI最强黑马!6个交易日涨超260% ·  昨天  
51好读  ›  专栏  ›  iOS开发

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

iOS开发  · 公众号  ·  · 2024-07-08 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。







请到「今天看啥」查看全文