用RP2350监视Z80

Hacker News Top 新闻

摘要

一篇博客文章,探讨如何使用带PIO的Raspberry Pi Pico RP2350来监视Z80微处理器的地址和数据总线,包括时序考虑和时钟速度限制。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/05 08:08

# 从 RP2350 观察 Z80 总线 来源:https://emalliab.wordpress.com/2026/05/26/watching-a-z80-from-an-rp2350/ 我正拿树莓派 Pico RP2350 和 PIO 折腾着玩,想着什么时候能把它连到 Z80 上。作为起步,我一直在试验能否让 RP2350 以某种方式捕获 Z80 的地址和数据总线。我用的是自己设计的定制转接 PCB(https://diyelectromusic.com/2026/05/04/pga2350-breakout-pcb-build-guide/)上的 Pimoroni PGA2350(https://shop.pimoroni.com/products/pga2350)。 ### Z80 总线 关于 Z80 和 Z80 总线的详细资料已经很多了,这里不再赘述,但列出一些关键参考: - http://www.z80.info/ —— 一切 Z80 相关的大本营! - RC2014 总线规范(https://smallcomputercentral.com/rc2014-bus/specification-rc2014-bus/)及说明(https://rc2014.co.uk/full-kits/rc2014-classic-ii/)。 - Electronic Circuits – interfacing with a Z80 microprocessor v3(数据和地址总线)http://sebastianmihai.com/z80-v3.html。 - Z80 用户手册(http://www.z80.info/zip/z80cpu_um.pdf)中的“时序”章节。 - 《Build Your Own Z80 Computer》—— Steve Ciarcia(https://en.wikipedia.org/wiki/Build_Your_Own_Z80_Computer)。 与本文相关的关键特性: - 16 位地址总线(A0-A15)。 - 8 位数据总线(D0-D7)。 - 若干输出控制线。 - 若干输入控制线(包括 RESET 和 CLOCK)。 最初的 Z80 是 NMOS 器件,运行在 2.5MHz(Z80),但很快出现了 4MHz(Z80A)和 6MHz(Z80B)版本。随后 CMOS 版本问世,时钟速度范围更广,典型的有 8MHz(Z84C0008)和 10MHz(Z84C0010),甚至高达 20MHz(Z84C0020)。这些芯片由多家制造商生产——Zilog、SGS、ST、NEC、Mostek 等(更多信息见 https://en.wikipedia.org/wiki/Zilog_Z80),但似乎并非所有厂商都采用我能查到的相同编号方案。该芯片于 2024 年停产,尽管一些海外市场向我保证,他们能以不到几英镑的价格提供全新的 20MHz Zilog Z84C0020 😉。 一个 Z80 CPU 测试器(https://emalliab.wordpress.com/2026/05/10/z80-cpu-tester-fail/)可以让你了解任何“野生” Z80 的型号和速度(我自己就愉快地测试过一批所谓的“20MHz”器件)。 ### Z80 时钟 我特别关心时钟速度,尤其是最低可能的时钟频率。有些(似乎是现代)版本的 Z80 是完全静态的——也就是说,它们会在时钟脉冲之间(包括完全停止时)保持状态。但我不清楚大多数是否如此。不过,既然有人成功制造出了手摇式 Z80 计算机(https://hackaday.com/2014/12/01/a-z80-computer-with-switches-and-blinkenlights/),我怀疑极慢的时钟也不成问题。 这里有注释(http://sebastianmihai.com/z80-v1.html)提到,在不被循环驱动时,最好将时钟保持在高电平。回到最初的 Z80A 数据手册,我们发现: [](https://emalliab.wordpress.com/wp-content/uploads/2026/05/image-21.png) 最大时钟周期在注释 [12] 中定义为: [](https://emalliab.wordpress.com/wp-content/uploads/2026/05/image-22.png) 所以基本上是把这些值加起来。低电平的最大脉宽有给定值 2000ns,上升和下降时间固定为 30ns,因此决定最长可接受周期的关键是高电平的最大脉宽。该值在注释 [E] 中给出: [](https://emalliab.wordpress.com/wp-content/uploads/2026/05/image-23.png) 这意味着,时钟低电平不应超过 2µs,而时钟高电平原则上可以是任意长度,保证功能正常的上限是 200µs。这样总时钟周期至少约为 203µs,对应的时钟频率略低于 5KHz。这就解释了之前关于将时钟保持在高电平的注释。 一个问题可能是某些信号需要几个时钟周期。特别是 RESET,它需要保持 3 个时钟周期才能确保正确复位,因此较长的时钟脉冲下,RESET 时间也自然得更长。 此时值得注意:Z80 有两种周期概念:机器(M)周期和时钟(T)周期(有时也称为状态)。机器周期是时钟周期的倍数(通常每个 M 周期包含 3 到 6 个 T 周期)。许多 Z80 指令参考会列出执行一条指令所需的周期数(M)和状态数(T)。 此外,外部外设(例如内存)可以使 Z80 CPU 等待。这通过断言 /WAIT 信号直到外设准备就绪来实现。这允许 CPU 与较慢内存之间的同步。 这一切带来的一个后果是,单时钟步进选项并不是特别有用,因为单步时钟需要多个步骤才能执行一条 Z80 指令。有意思的是,《Build Your Own Z80 Computer》一书中(图 4.5)有一个指令感知的单步电路,它可以监视 /M1 并使用 /WAIT 暂停 CPU,直到下一步。 ### RP2350 GPIO 我使用的是来自 https://github.com/earlephilhower/arduino-pico 的 Arduino Pico 核心,这使得构建和下载代码到 RP2350 变得容易,但我尽量坚持使用 Pico C/C++ SDK 函数,而不是 Arduino 环境覆盖层。 在 RP2040/RP2350 上初始化 GPIO 需要以下代码: ```c gpio_init(pin); gpio_set_dir(pin, dir); gpio_put(pin, value); value = gpio_get(pin); ``` 但这对于总线读取来说太慢了。不过,有一种选项可以同时操作多个 GPIO 引脚,例如: ```c gpio_init_mask(gpiobitmap); gpio_set_dir_in_masked(gpiobitmap); gpio_clr_mask(gpio_bitmap); gpio_set_mask(gpio_bitmap); uint32_t value = gpio_get_all(); ``` 但这些 API 是为 RP2040 设计的,因此只需要处理最多 32 个 GPIO 引脚。我使用的 RP2350B 有 48 个 GPIO,因此 SDK 引入了 GPIO 基址的概念,可以是 0(对应 0-31 号引脚)或 1(对应 32-63 号引脚)。不幸的是,这在 API 中的体现有些不一致。有些情况下有 \_n 版本的 API,它接受基址(0 或 1)作为参数。有些情况下(例如单个 GPIO 引脚),任何不超过最大支持的数值都是可以的。PIO 又有所不同,需要使用 pio\_set\_gpio\_base 预先设置基址。但对于 PIO,基址的处理方式不同:基址 0 对应 GPIO 0-31,基址 1 对应 GPIO 16-47。我猜这是为了在两种情况下都能完全并行使用 32 个 GPIO 引脚,但与非 PIO 函数不同,这令人困惑。 一些 API 函数没有对应的版本(至少我没找到),例如我找不到 gpio\_init\_mask 的“基址 1”版本。要初始化我需要的所有 GPIO,使用以下代码: ```c gpio_init_mask(GP_BASE0_MASK); gpio_set_dir_in_masked(GP_BASE0_MASK); for (int i=0; i<32; i++) { gpio_set_dir(i, GPIO_IN); } ``` 但这仅适用于前 32 个 GPIO。要处理 32 号以上的引脚,必须单独处理。 既然我已经有了 48 个引脚的接口,我可以将 Z80 地址总线(16 条线)和数据总线(8 条线)以及一些控制线(例如 /MREQ、/IORQ、/RD、/WR、/M1,或许还有 /WAIT 和 RESET)连接到我的 RP2350。我计划将这些信号映射到尽可能方便的 GPIO 组中,以允许同时进行总线读取。 我用了一块 RC2014 主板作为 Z80 系统。我的 RP2350 连接到 40 引脚总线扩展接口,如下所示: - A0-A15 地址 → RP2350 GPIO 0-15 - D0-D7 数据 → RP2350 GPIO 16-23 - /MREQ → RP2350 GPIO 24 - /IORQ → RP2350 GPIO 25 - /RD → RP2350 GPIO 26 - /WR → RP2350 GPIO 27 - /M1 → RP2350 GPIO 28 - CLOCK → RP2350 GPIO 29 - /WAIT → RP2350 GPIO 30 - RESET → RP2350 GPIO 31 - 一些 LED(通过 GPIO 32 及以上) 有关我的 RP2350 PCB 的更多详细信息,请参见上述链接,但基本思路是使用 GPIO 的 16/32 位并发读取和写入功能来捕获或设置总线信号。 ### PIO 总线监控 我不想仅仅为了观察总线而一直占用一个完整的 RP2350 核心。由于 RP2350 有 PIO(可编程 I/O)子系统,我可以使用它来监控总线,而主处理器可以处理其他任务。这是 PIO 的一个完美用例。 由于我有 48 个 GPIO,并且 PIO 有 32 位输入和输出端口,我可以将地址和数据总线映射到 PIO 输入端口,以便在单条 PIO 指令中捕获所有 24 位(16 位地址 + 8 位数据)。 设计一个完整的 PIO 状态机用于总线监控超出了本文范围,但基本思路是:使用 PIO 等待总线周期的开始(例如检测 /MREQ 或 /IORQ 下降沿),然后捕获地址和数据,并可选地等待 /RD 或 /WR 以获取读或写数据。然后可以将这些信息存储在 PIO 的 FIFO 中,供主处理器使用。 ### 测试驱动 为了验证基本概念,我编写了一个简单的测试程序,它不断读取 GPIO 值并查找某个特定模式。例如,我可以查找 /IORQ 和 /WR 信号,然后从数据总线读取数据并点亮一些 LED。这为我提供了 RC2014 总线的一个原始但功能的“逻辑分析仪”。 基本代码如下所示: ```c void setup() { // 初始化所有 GPIO gpio_init_mask(GP_BASE0_MASK); gpio_set_dir_in_masked(GP_BASE0_MASK); for (int i=0; i<32; i++) { gpio_set_dir(i, GPIO_IN); } // 初始化 LED GPIO(假设为 GPIO 32+) // 此处省略具体 LED 初始化代码 } void loop() { uint32_t gpio_all = gpio_get_all(); // 检查 /IORQ 和 /WR 为低,且地址低 8 位为 0x00 if ((gpio_all & 0xF0000000) == 0x00000000) { // 简化条件 uint8_t data = gpio_all >> 16; // 数据在 GPIO 16-23 // 将数据输出到 LED gpio_put_mask(0xFFFF0000, data << 16); // 假设 LED 在 GPIO 32-39 } } ``` 当然这只是一个非常简化的示例,但它展示了基本思路。 ### 更进一步:一个简单的 8 位 I/O 模块 既然能读取总线了,我决定构建一个简单的 I/O 模块,用于在 RC2014 上写入和读取数据。我编写了以下代码,它监视总线上的 I/O 写入,并将数据总线上的值显示在 LED 上。 ```c #define GP_BASE0_MASK 0xFFFFFFFF // 前 32 个 GPIO 全设为输入 #define GP_CTRL_MASK (1<<25) | (1<<27) // /IORQ 和 /WR #define GP_DATA_START 16 #define GP_LED_MASK (0xFF<<32) // 假设 LED 在 GPIO 32-39 void setup() { gpio_init_mask(GP_BASE0_MASK); gpio_set_dir_in_masked(GP_BASE0_MASK); // 单独初始化 LED 对应的 GPIO 32-39 为输出 for (int i=32; i<40; i++) { gpio_init(i); gpio_set_dir(i, GPIO_OUT); } } void loop() { uint32_t gpio32 = gpio_get_all(); // 查找 /IORQ 和 /WR 为低,地址低 8 位为 0x00 if ((gpio32 & 0x03000000) == 0x00000000) { // 简化检查 uint8_t data8 = (gpio32 >> 16) & 0xFF; // 将数据写入 LED gpio_clr_mask_n(1, GP_LED_MASK); // 清除 LED gpio_set_mask_n(1, (data8 << 32)); // 设置 LED } } ``` 这基本上就是使用一个 150MHz 的双核 ARM Cortex M0+ 来模拟 RC2014 数字 I/O 模块(https://rc2014.co.uk/modules/digital-io/)中的几个 TTL 逻辑芯片,从而从一个 8 位、10MHz 的处理器上闪烁一些 LED!这是 RC2014 上运行的 BASIC 代码: ```basic 10 FOR F=1 TO 6 20 OUT 0,2^F 30 GOSUB 100 40 NEXT F 50 FOR F=7 TO 0 STEP -1 60 OUT 0,2^F 70 GOSUB 100 80 NEXT F 90 GOTO 10 100 FOR Z=1 TO 200 110 NEXT Z 120 RETURN ``` 你可以在本文开头的视频中看到它的运行效果。 ### 结论 当然,这是一件相当疯狂的事情,但它展示了基本思路。目前,它只是以全 CPU 速度运行,消耗 RP2350 的一个核心来监视总线上的活动。现在有很多有趣的方向可以探索。我正在思考的一些方向包括: - 支持读写操作,即输入和输出 I/O 访问。 - 将 I/O 处理放到 RP2350 的一个核心上,同时另一个核心利用这些信息做其他事情。 - 制作一个内存映射设备,监视内存读写。 - 从 RP2350 控制 Z80 时钟,以实现一些有趣的时钟控制可能性——可变速度、指令感知步进等。 - 回到我在树莓派 Pico 上关于 PIO 的实验(https://emalliab.wordpress.com/2026/04/02/pio-on-the-raspberry-pi-pico/),看看能否更自主地实现。 - 模拟一些现有的 Z80 外设,类似于 Pico 在 RP6502 The Picocomputer(https://emalliab.wordpress.com/2026/01/02/rp6502-the-picocomputer/)中的使用方式。 虽然值得注意的是,这基本上就是 Z80-MBC(https://github.com/eprive/Z80-MBC3)正在做的事情……我怀疑这些想法大多会停留在思考阶段,但现在基本机制似乎已经可行,选择还是很多的。 Kevin

相似文章

将1980年代的内存映射与Raspi Pico进行比较

Hacker News Top

比较1989年摩托罗拉68000内存映射设计与现代树莓派Pico的设计,突出整数地址分配的奢侈性以及内存规模相似性(192KB对比260KB),同时讨论了架构差异和可行性。

Zilog Z80 迎来50周年

Lobsters Hottest

Zilog Z80 微处理器庆祝其50周年,回顾其对早期个人电脑、家用电脑和工业应用的深远影响,以及个人轶事和历史背景。

CGA上的60fps视频?– GlyphBlaster

Hacker News Top

GlyphBlaster是一款基于Raspberry Pi Pico的设备,它替换了CGA显卡上的字模ROM,通过将字模ROM读取作为像素可寻址帧缓冲区,在文本模式下实现了60fps的视频播放。

Show HN:Pokémon Emerald 移植到 Raspberry Pi Pico 2

Hacker News Top

一位开发者将 Pokémon Emerald 移植到 Raspberry Pi Pico 2 (RP2350) 微控制器上原生运行,通过将 GBA 游戏重新编译为 Cortex-M33,并在第二个核心上重新实现 PPU,实现了 60 fps 的 HDMI 输出。

我打造了一个袖珍Macintosh

Jeff Geerling

使用运行Pico Micro Mac固件的Raspberry Pi Pico打造了一个袖珍Macintosh,具备VGA输出和USB键盘/鼠标。