CGA上的60fps视频?– GlyphBlaster
摘要
GlyphBlaster是一款基于Raspberry Pi Pico的设备,它替换了CGA显卡上的字模ROM,通过将字模ROM读取作为像素可寻址帧缓冲区,在文本模式下实现了60fps的视频播放。
暂无内容
查看缓存全文
缓存时间: 2026/05/14 15:24
# 在CGA上实现60fps视频?—— GlyphBlaster
源: https://martypc.blogspot.com/2026/05/60fps-video-on-cga-glyphblaster.html
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinVLKlP-V6Kxpgx4BS_B32v-6MmkQjKPRPHNwR2hZWF1A0kFAR0XhoNmxBn4qXs9WWqlqbVB5JMq2-rqUAXa9Y6iIoT3JId0yQX8rREm-Kr6BE9RQZLkuv2Im1qgvbRGbdOyUa-wbHCWUEcbXiH4f65IGMZPe6ZoVjLk6KCRzPb-Ds6mvGHY66YlA2fSQv/s948/cat_still.jpg)GlyphBlaster 正在工作——那是文本模式下的猫吗??
有一种复古硬件改造的套路,大家都知道技术上算作弊,但那种大胆创新的精神却值得尊重。我希望这个项目也能归到这一类!
几年前,我被 TheRasteri 的 PiPU 项目 (https://www.youtube.com/watch?v=FzVN9kIUNxw) 深深吸引——简单来说,这是一个用树莓派驱动的 NES 映射器。当然,它可以玩 DOOM:
树莓派 Pico (https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html) 及其 RP2040 微控制器极大地复兴了复古计算爱好,催生了诸如 PicoGUS (https://picogus.com/)、PicoMEM (https://github.com/FreddyVRetro/ISA-PicoMEM)、PicoIDE (https://picoide.com/) 以及最近的 OneROM (https://onerom.org/) 等设备。正是后者让我开始思考。
我最近用 OneROM 替换了 CGA 卡上的字库 ROM,从而得以用自定义字体替换 IBM 经典的 8x8 字体。下图是 Polyducks 制作的 Frogblock (https://polyducks.itch.io/frogblock):
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQeVJ_Y5S6MdnkxQcfqDe9FUqqaGjubYJ1GRwRkJEIqMtF7IYMjkzb1Fl2AvHmrZ4P2cFiCEkph9WquDSG4NA7GssFb9Yc9os9rxc7LMapnIsgMg8luXcHHK48txYCL6bYjsIWbAX9WqceiuzofxFMrdHQQV5GZOOsl4mK31AsVWNMaLY-Vw1_YlksBRR2/s1655/frogblock.jpg)加载到 OneROM 中替换 CGA 字库 ROM 的 "Frogblock" 字体
OneROM 的可编程性引起了我的兴趣——尤其是它的插件系统。但我想更深入地了解 Pico,并学习如何编程其强大的 PIO 模式 (https://blues.com/blog/raspberry-pi-pico-pio/)。既然 Pico 2 (https://www.raspberrypi.com/products/raspberry-pi-pico-2/) 已经发布,我主要对它感兴趣,因为它更快、功能更强。在成功完成一个基于 Pico 2 的项目——我的 IBM PCjr 无线键盘转 USB 适配器 (https://github.com/dbalsom/pcjr_kbd/tree/main/pico_jr_kbd) 之后,我决定第二个项目将是 CGA 字库 ROM 替换。
简而言之,我的想法是利用字库 ROM 的读取作为 1bpp、像素可寻址的帧缓冲区。
## 滥用文本模式
在文本模式下,IBM CGA 卡会在每个字符时钟周期读取字库 ROM,即使在非活动显示区域也是如此。这也许是 IBM 的偷懒之举,但对我们来说却很便利。每次读取字库 ROM 时,它会返回一个字节,代表一个 8 像素宽的字符字形 (https://en.wikipedia.org/wiki/Glyph) 的某一行。在非活动显示区域,返回的值会被忽略——那时它很可能只是无意义的数据。
对字库 ROM 的持续读取给我们提供了一个规则的字符时钟,基于 ROM 插座 /CE 引脚的下降沿,对应于整个显示区域的每 8 个像素跨度。我们只需要一个外部同步信号——VSYNC——来与显示器上输出的视频信号保持同步。
本项目我给自己设定的挑战之一是保持 CGA 卡其他部分不变,所以我使用测试夹连接到 CGA 的 DE9 接口背部露出的 VSYNC 引脚。
基本思路如下:
- 我们在 VSYNC 脉冲结束时开始新的一帧,用一个 PIO 程序等待 /CE 引脚。
- 在另一个 PIO 程序中,我们通过 DMA 从帧缓冲区读取 FIFO 中的数据字节。
- 我们可以用静态图像甚至视频填充这个缓冲区——实际上,由于 Pico 2 W 支持 Wi-Fi,我的目标之一就是通过无线方式向 CGA 流式传输 1bpp 视频。
不过我给它取了个好名字。最初我称之为 PicoCGA,但这暗示它是一块完整的显卡而不是附加组件。我得感谢 Retro Pico Hardware discord 上的 wbcbz7 提出 "GlyphBlaster" 这个名字。
## 制作原型
首先,我需要某种适配器将 Pico 插入 ROM 插座。我从前一个总线嗅探项目 (https://martypc.blogspot.com/2023/10/bus-sniffing-ibm-5150.html) 中剩下了一些 PCB,它们原本设计用于 8087 插座。幸运的是,DIP-40 和这个 ROM 的 DIP-24 插座宽度相同,所以这块 PCB 兼容。
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAeHf7ZqgtSsg7YvrUSDtzImoH2eLGSAU_YQXSZCuPpFD05Mf5qp6i_DkZmxubyPyjYseYNgemGVY-_f7BnMuuArcW576nDQ8JJKknIGHhNFFMaStQ4PbheWY_X_r6AMkEJuD_G7-WheTBghi9xayIGdpfnUpKuhnPMr5OKCahSAl1CmrV88gwz4mQo053/s2060/cga_card_01.jpg)安装在 CGA 卡上的 GlyphBlaster 初始原型初始原型完全忽略了地址线,只连接了 8 条数据线和 /CE 芯片使能线。绿色导线是我们的 VSYNC。
你可能熟悉 CGA 通常所说的 640x200 分辨率;这对应 80 列 x 25 行的 8x8 字体。但考虑到整个 NTSC 场大小,包括前肩、后肩和消隐期,我们得到的有效分辨率为 912x262。
为简单起见,我将图像预格式化为 912x262 的位图。这是经典的曼德勃罗特集合的灯泡:
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTmbYiqqqUagcXk6CwfM1JfuXWUf4kxL4MmyUR4pIOySm4ihdJQVUFcfl4K2AxVEQw4SvemIwXKM2j_m2U97MRuA0ffbxh_Mak7IVlMnJkPC5dLpWUgiDkBbJCrIOJLPxUEzrCkuJKYm3Wr8gTzMcEinDyo_qdspeJ_zI8YzR2rPJb1pOEr16fD7MnWwKs/s912/mandeblrot.png)格式化为 912x262 的曼德勃罗特位图经过反复试验,我们终于在屏幕上看到了它:
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEo0YGrCE_OgReZDycYxB1Weq5eFJoZ-BH01_nROjAg6rYkM2su7zbQbO0hH2IAL82CU_vnGgywNgh_vf5xk_30nNaN3JGfGluZDCq0eXNv3WDvHKnOzACv0V3o1qKsyZWleRxn1RvEZLJzm2fVqu_W9fZYAEjFBi63rmWK4UV_mOxWce8wgX0spLM-9bH/s1759/20260502_151931.jpg)通过 CGA 字库 ROM 插座在显示器上绘制的曼德勃罗特位图!彩色条是启动 GLaBIOS (https://github.com/640-KB/GLaBIOS) 后残留的前景属性。请原谅映在屏幕上的凌乱工作台!
## 流式传输视频
基本原型工作正常后,我准备尝试向其流式传输 Bad Apple (https://www.youtube.com/watch?v=UkgK8eUdpAo),当然,这是为了致敬 8088 Domination (https://trixter.oldskool.org/2014/06/19/8088-domination-post-mortem-part-1/)。初始原型是用 Rust 编写的,使用 rp-hal (https://github.com/rp-rs/rp-hal)。为了更方便地利用 Pico 2 W 的 Wi-Fi 模块,我决定切换到 Embassy (https://embassy.dev/),它有一个很好的基于任务的异步执行器。我发现 Embassy 非常容易上手——最初只需在 Cargo.toml 中修改几行,并在 main 函数中添加一个宏声明即可。
我有扎实的网络背景,但从未设计过视频流协议。我决定从最简单的开始——UDP 数据包,仅包含一个短头部和之后的 1bpp 帧数据。我们编码数据包所属的帧号、数据包代表的起始和结束扫描线、一个字节的压缩方法(初始为 0 表示未压缩),最后是扫描线数据。
由于我们发送的是完整的 912 水平点显示区域,每条扫描线为 114 字节。我需要保持在 1500 字节的 MTU 以下以避免分片,所以我简单地将 10 条扫描线打包到一个数据包中。
在高楼公寓等密集 RF 环境中,UDP over Wi-Fi 本身就不太可靠,而 Pico 没有外部天线支持更是雪上加霜。最后我买了一个小型旅行路由器,放在我的 IBM 5150 旁边,以获得可以流式传输视频的可靠信号。
## 转换视频
尽管 Bad Apple 以几乎在所有平台上都能以 1bpp 演示而闻名,但它实际上并非 1bpp 视频——字符轮廓有抗锯齿,而且有几个场景使用了灰度阴影。
此外,CGA 卡的显示分辨率为 640x200,横向压缩严重,因此我们需要将其压缩。使用 40 列文本模式可以得到更接近方形的 320x200,但我认为在 80 列模式下播放视频更有趣。
第一步是从源视频中提取所有帧到目录:
ffmpeg \-i touhou bad apple smile\.mp4 \./output\_frames/frame\_%06d\.png
然后,使用 ImageMagick (https://imagemagick.org/),我们将帧调整为 640x200,并使用有序抖动 (https://en.wikipedia.org/wiki/Ordered_dithering) 转换为 1bpp:
magick mogrify \-path \./resized\_frames \-resize 640x200\! \-colorspace Gray \-ordered\-dither o8x8 \-type bilevel \-depth 1 \./output\_frames/\*
尽管我的流式传输应用之后可以无需重新编码处理,但我还是将每张图像填充到 912x262:
magick mogrify \-path \./final\_frames \-format png \-background black \-gravity center \-extent 912x262 \./resized\_frames/\*
这样生成了一帧符合 CGA 的 912x262 原生显示场时序的帧:
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlNOYa3w5eZc-i-cuBmcs8mTQJmcLYRPMdB3e6FCtvEeMytlMxsczWYzwAByeqRTFD4dq9pri28lbZDpi71V2k2i46qJUs0NLmuCznz2BBJnRbDTXjPJjybjG0ViM26w7zPRtuwL6RxhfKDepy8XGUs0C7_BWePHdGJx61BxeoYz2F1B63CG-MgRQEaPCk/s912/frame_000326.png)最终的 912x262 Bad Apple 帧
## 传输视频
在 912x262x1bpp 下,每帧未压缩视频约为 30KB。以 Bad Apple 的 25fps 计算,这相当于 5.97Mbps,对于微控制器来说是不小的数据量。因此,我实现了一个非常简单的 RLE 方案,在我们的 10 扫描线数据包上平均压缩率达到 75%。
为了避免将整个帧的所有 UDP 数据包一次性发送给 Pico,客户端流式传输应用程序会粗略估算解码一帧所需的时间,并对 UDP 数据包进行定速发送。由于每个数据包都标记了起始和结束扫描线,它们可以乱序接收,直到接收到属于新帧的数据包并且 Pico 进行页面翻转。Pico 每帧都以最后一帧的副本开始,因此任何丢失的数据包最多只会造成轻微的视频闪烁。
效果如何?相当不错!
目前的主要问题是安装了 GlyphBlaster 后系统无法正常使用,因为屏幕上除了 GlyphBlaster 绘制的图形外,我们看不到任何文本模式的内容。我们需要一种方法让 GlyphBlaster 模拟字库 ROM,或者采取偷懒的方式,将这些职责传递给原有的字库 ROM。
## ROM 直通
为了实现 ROM 直通,我焊接了一个悬空的 ROM 插座。即使作为原型,这也相当粗糙。
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzErDCImj2HSY6qIln1d_oAyF36Jv9RnYBz6PNakWGQ_MGrjAzgNT5M1QMDYZrG6x9vqggbnzPdiEMjG2iuasO_iaZgkOkTL5-FrWZRu8T2fbMaBUYNtpF3ySk0qCAOr1-L8KM9p2-IbFcGPvdOZ1C_NnPpJLkiQjKtG7jv1BY7ARln8oXytMS3jGV0FCy/s2021/rom_socket_bodge-a-rama.jpg)用大量飞线添加的悬空 ROM 插座。发现有一个引脚没连接!这个东西一直拒绝工作,直到我意识到一个重要的事情——一定要读数据手册——这个系列的 Mostek ROM 有一个内部地址锁存器,由 /CE 的下降沿触发。你不能简单地将 /CE 拉低。调整 PIO 代码以触发 ROM 的 /CE 引脚后,我们能够读取现有 ROM 的内容,地址线直接传递给 ROM。
但有一个问题——我们希望将 Pico 的帧缓冲区与 ROM 数据结合起来,使图形以叠加方式显示在屏幕上。不幸的是,PIO 虽然强大,但缺乏任何 ALU 或按位布尔运算,因此你不能说对 ROM 内容与帧缓冲区数据进行 XOR 操作。不过,你可以通过将反转的 ROM 数据作为写入 Pico 的 PIO pindirs 寄存器的值来实现 OR 操作。这意味着 ROM 返回的 1 位会导致 Pico 的输出引脚浮动——而 Pico 的内部上拉足以将这些值拉高到 1。
现在系统可用了,我们可以选择在文本模式上显示图形,这在 CGA 上原本是不可能的:
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhcM12qhCPHaCcTWfRDi98OJ_W3Dc9YYzXLS2_vuo_g0TX1Hu_BTWdG2G6luheYymYcj5MSZOuH_eONSW0kqPB2IrW51jqz8sAQvpIHx1Xug4z4m2IIYs24OPP_dhBYrWv6OTZVyi95yD9pMZBu5hZCOYJLw0ygc2p-TeaynzzrBAEO9agNi4ny09vZTRod/s3325/screen_overlay_01.jpg)通过滥用 Pico 的内部上拉,实现与文本模式 OR 叠印的图形显示。
我实现了一些不需要 Wi-Fi 连接的小玩具。首先,我觉得复制经典的弹跳 DVD 标志会很有趣。
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEigsJNOcyWsotwhjA7QUnUjLGMEryH35GOVNyutbkquILUpfo51IZG6aR5MWz7PiYVxqoaUxbqfdco-JLm-pZY7eWtj5D5o2uaAl3phwfn1WD4pfWp7T6HXB6YfZ1mXaVTblAabs3afuG2K81r_gFygThLq07waNNszfgz6lml13tGEEefR9bghZtKfb7-Z/s1190/dvd_bounce_01.JPG)DVD 标志在 CGA 文本模式屏幕上以 60fps 弹跳然后我忍不住想拿 Amiga 开个玩笑:
如果能以某种方式与显示交互,那就更酷了。目前主机无法与 GlyphBlaster 通信,反之亦然。不过,有一些有用的信号可以接入,可能会很有趣。一个候选是 Motorola MC6845 的 CURSOR 引脚。当光标应在特定字符单元格中输出时,该引脚变为高电平。
通过在 PIO 程序中捕获 CURSOR 信号的上升沿,保存当前的 DMA 地址,然后推算出屏幕上的位置,这相当简单。
许多人怀念一个叫做 Neko (https://en.wikipedia.org/wiki/Neko_(software)) 的桌面小玩具,内容是一只猫跟随鼠标光标移动等把戏。我决定制作一个 PC 版本,以向著名的 PC 启动游戏 AlleyCat (https://www.mobygames.com/game/190/alley-cat/) 致敬。
我使用了我的模拟器 MartyPC,利用内存可视化工具从游戏中提取了各种精灵图。
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN97KTOKTcbcqY6egaLplwNqIkAh8zC1FxGtkIIfmEKMdS8tAdTzkVFMRdfCNXuR-NPJwZxvtkKy2vZ5X4CjohDhiB4v1EhT4ox3ExNpsN8yDrpMEHPjHmUmKbz5LjZSSXG2kU1qe9mPnEIYTE57abKnggQccx08Q1vS27FwYO6agGk1-aBm9EYem7GNE7/s226/alleycat_sprites.png)
这只猫会跟随文本模式的光标移动,坐下并进入一个空闲动画,期间它会转头、眨眼、摇尾巴。当光标移动到新位置并静止片刻时,猫就会前往新光标位置,如果在追赶过程中光标移动了,它还会追上去。
以下是视频演示:
GlyphBlaster 可以使用多种同步源。另一个有前景的是直接来自光笔的光笔选通信号——这允许猫在屏幕上追逐光笔:
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJWU6GzSGLs6fdKUkFyr2lx8k6mePgl6o6tEqh52WA_jbu8didQyR4IP69Zmm-sh9_hyK39sW6zDBjIiqGVKSrpTVSbzwzsh4rN4a6rSqOj3wrVTdjRF2635d_pxsBvGz7IM2grXPjcYWIvHvHTNP4bqfp58AlGPjuOmtwlZBJIf-1LWNSx2QcdwGxhyXB/s1073/pen_demo_01.JPG)猫在屏幕上追逐光笔
除了作为复古电脑的有趣玩具,还有一些引人入胜的可能性。如果我们将地址线连接到 Pico,Pico 可以读取文本模式屏幕的内容——这可以使其成为一个非常有用的、独立于驱动和操作系统的屏幕读取设备。
我已经设计了一些肯定会让
相似文章
用RP2350监视Z80
一篇博客文章,探讨如何使用带PIO的Raspberry Pi Pico RP2350来监视Z80微处理器的地址和数据总线,包括时序考虑和时钟速度限制。
将我的3D点云渲染器移植到ZX Spectrum 48K上
一位开发者将3D点云渲染器移植到ZX Spectrum 48K上,通过Z80汇编优化达到每秒14帧,并创建了一个预计算版本,运行速度为每秒40帧。
Loading Sega Games Off a Vinyl Record [video]
作者成功将世嘉Genesis游戏数据编码为音频,通过黑胶唱片播放,经Raspberry Pi Pico解码后加载到主机运行,完成概念验证。
How I made a 60fps Eink monitor, the Modos Flow
After four years of development, the author created the Modos Flow, a 13.3-inch Eink monitor capable of 60fps by using pixel-level independent updates, and open-sourced all designs.
我打造了一个袖珍Macintosh
使用运行Pico Micro Mac固件的Raspberry Pi Pico打造了一个袖珍Macintosh,具备VGA输出和USB键盘/鼠标。