【CSDN 编者按】在大模型、AI 芯片和多核服务器不断“卷”硬件性能的时代,是否还有人愿意回到极简主义的硬件设计,去探索最小系统的可能性?本篇文章讲述了一位开发者如何用仅仅三个 8 引脚芯片,打造出一台可运行 Linux 的迷你电脑,他不仅开源了所有硬件原理图和固件代码,还详细介绍了从烧录、启动到扩展使用的全过程。 作者 | Dmitry.GR 翻译 | 郑丽媛 出品 | CSDN(ID:CSDNnews) 长期以来,我一直在尝试用最少的组件构建一台能运行 Linux 的微型计算机。我也尝试过极限简化和各种有趣的形态设计,而这次我想做一次新的尝试:用最简单的方式,即只用三个 8 引脚芯片,来组装出一台可运行 Linux 的迷你电脑套件。 
设计一台“最小化”的极简电脑 (1)初步构思 曾几何时,人们可以买到 DIY 套件,然后自己在家中亲手组装出一台能与市售计算机媲美的设备——可如今,这样的时代早已一去不返。 现在的计算机由成百上千颗复杂的大型芯片构成,这些芯片不仅没有公开的数据手册,还通过复杂的电源传输拓扑结构供给数百瓦的电力。而现代操作系统对硬件的要求也愈发苛刻:GB 级别的 RAM、TB 级别的存储空间、始终在线的网络连接……简直像是为了“更好地监控你”而生。 那么问题来了:如果想在家就能动手组装一台现代计算机,可能吗?我认为,只要能运行 Debian Linux、能用 vi 编辑器、能用 gcc 编译器并能执行 make 命令,就已经够“现代”了——于是这成为了我的目标。 基于我之前的探索,我知道这其实并不需要太高的配置:8MB 内存 + 1 MIPS(百万条指令每秒)的处理能力就足够了。存储方面更简单,SD 卡早就能轻松满足容量需求。虽然现在的电脑大多没有串口了,但对于嵌入式系统来说,串口仍是最简单的接口方式,用 USB 转串口即可代替传统串口。 因此,最终我设定的目标如下:至少 8MB 的 RAM、至少 1 MIPS 的处理能力、SD 卡存储、USB 接口(用于串口通信)。 在硬件设计方面,我希望能设计出一种在家也能轻松焊接组装的计算机,让焊接经验几乎为零、仅拥有一把 RadioShack 45W 电烙铁的人也能做到。整机要小巧、可爱且低成本。为了降低焊接难度,我决定只使用 8 引脚芯片,这本身也是一次趣味挑战。由于每颗芯片至少要保留电源和地线引脚,剩下最多只能用 6 个引脚来实现功能。这一限制对整个电路设计影响非常大,也造成了很多局限。 在外形上,我决定将其设计为一个小巧的圆形电路板,在顶部边缘设置一个 USB-C 接口,如下图所示。这就是最终能正常工作的版本,也确实是我亲手用 RadioShack 45W 电烙铁焊接完成的! 
(2)零件选择 事实上,能支持 USB 通信的 8 引脚芯片几乎没有,不过勉强说的话,应该有“一种半”。 第一种是真正可用的解决方案:PL2303GL。这是一款非常小巧、实用的 USB 转串口桥接芯片,无需任何外部元器件,还能额外输出 100mA 的 3.3V 稳压电压,非常方便!它的表现完全符合预期,我个人非常喜欢。Prolific 官方也为几乎所有主流和小众操作系统都提供了驱动支持。唯一稍微麻烦一点的是,在 macOS 上,这些驱动程序需要从 App Store 安装,但整个过程也还算简单。需要一提的是,它的前代型号 PL2303SA 其实也可以用,但由于它已经停止生产(EOL),因此我并不推荐。 那所谓的“半个芯片”又是什么意思呢?这就要提到 V-USB 项目了——它让 ATTINYx5 系列芯片也可以实现 USB 通信功能。虽然只能支持低速(Low-Speed) USB ,并且会占用大量 CPU 资源,但它的确可以工作。问题在于:现有的 USB 转串口协议基本都要用 BULK 传输端点,而 USB 规范中明令禁止在低速设备中使用 BULK 端点。如果我们要完全遵守 USB 标准,那就得用中断端点(Interrupt Endpoint)自己设计一个通信协议,并为所有主流操作系统编写驱动程序——这个工作量实在太大,我也没什么兴趣。所幸,所有主流操作系统实际上并未严格执行这一规则,即便是低速 USB 设备用上了 BULK 端点,也依然能正常识别和通信。所以,我们可以直接用 V-USB 模拟串口设备(ACM 接口),基本上都能用。 至于 RAM 的选择,其实没什么好犹豫的,SOIC-8 封装的 PSRAM 就是最合适的解决方案。目前常见的供应商包括 ISSI、APMEMORY 和 Vilsion,他们已经吹了一年多说自己要出 16MB 的 PSRAM 芯片了,但到现在都没实现承诺,所以很可能是在画饼。好在 8MB 的芯片货源充足、价格便宜,在各大电商平台花几美元就能买到。因此,我最终决定直接用 8MB 的 RAM 来构建这台 Linux 小电脑。 最后一个问题是:该选用哪款微控制器?通过参数筛选功能,我最终把目光放到了 STM32G0 系列。按理说,STM 芯片应该作为最后的选择,因为它们从不认真发布完整准确的勘误手册。STM32G030 第一个被淘汰,因为其中一个引脚被硬设为 RESET,只剩 5 个 I/O 太局限了。STM32G031J4M6 看起来还行,这是一款比较新的芯片,也许 STM 这次有点靠谱,把以往的各种坑都填了?再说了,这个项目计划中也不会用到太多片上外设,也许会没问题?32KB 闪存、8KB RAM——这两个数字秒杀所有其他选项!Cortex-M0+ 核心也让它成为性能方面最强的候选之一。官方数据显示,这款芯片主频为 64MHz,稍加努力能达到 80MHz,再下点狠手,甚至能跑到 150MHz。假设我能避开勘误表中各种已知/未知的 bug,这毫无疑问是最强选择——虽然我不太喜欢 STM,但也只能这样了。 
硬件设计 (1)控制台 UART 的引脚几乎无法与其他功能复用。试图将 UART 的 RX 引脚复用会带来数据丢失的风险——当“其他功能”正在运行时,如果此时正好有串口数据到来,就会被错过。而将 UART 的 TX 引脚复用也同样不靠谱:无论“其他功能”切换得有多快,只要产生低电平脉冲,PC 就可能误判这是一位串口字符。如果是短暂的低电平,PC 通常会将其识别为 0xFF。 理论上来说,可以通过启用奇偶校验来掩盖这些干扰,但这并不是可靠方案。更何况,2025 年了谁还会用奇偶校验呢?说到底,UART 引脚由于缺乏更高层的协议、片选信号或独立时钟,基本不适合与其他功能复用。 所以,这 6 个引脚中,已经被占用掉 2 个了。我也只能无奈接受这个现实…… (2)RAM 所有的 SPI PSRAM 芯片都支持 QSPI 模式,以提高传输速度。但遗憾的是,QSPI 需要 6 个引脚,而现在只剩下 4 个了。好在多数 PSRAM 也支持双线 SPI 模式,这种模式下 MOSI 和 MISO 会同时传输,一次时钟周期内传送 2 位数据,是普通 SPI 速率的两倍。更棒的是,它不需要比普通 SPI 多占任何引脚,并且可以与其他设备共享 SPI 总线——因为在未被选中(deselected)时,这些设备不会驱动 MISO,也不会尝试读取 MOSI。 不过,STM32G031 并不原生支持 dual-SPI。如果想用,只能通过软件模拟实现。但问题来了:软件实现 dual-SPI 的速度是否能赶上硬件 SPI 模块?硬件 SPI 模块可以以 CPU 时钟一半的速率运行,并能连接 DMA 单元实现持续的数据传输。 要想用纯 CPU 模拟实现同样的吞吐,需要保证每次传输周期只消耗 4 个指令周期,这几乎是 CPU 的极限操作,再快就不可能了。既然最快也只是“勉强追平”硬件 SPI,那又何必折腾?结论:RAM 还是用普通 SPI 访问就好。不过,这下就直接用掉了最后剩下的 4 个引脚了。唉…… (3)SD 卡 所以,情况变得棘手了:没有引脚剩余,但还要连一个 SD 卡。 SD 卡可以使用 SPI 通信,只需再提供一个引脚用于片选信号即可,但已经没有多余的引脚了。我考虑了几种方案,最简单的做法是,在 RAM 的 nCS 上接一个反相器(inverter),并将其输出作为 SD 卡的 nCS 信号。我把这个思路被做了原型测试,发现效果还不错。但有两个问题:首先,有些 SD 卡不能接受那种“被选中但没有数据发送”的情况。如果两次 RAM 访问之间没有写入数据,对 SD 卡来说就像是这种异常操作,存在兼容性隐患。其次是反相器需要一个额外的 IC 或至少一个三极管,这会增加 BOM 复杂度。对于想 DIY 这块板子的初学者来说,板子上的组件越多,组装就越困难。因此这个方案被贴上了“最坏方案”标签,暂时搁置,还得继续寻找更好的解决方法。 由于这个设备的数据产出速率不高,UART 的波特率其实可以设得很低。于是可以考虑:是否能把 UART 的 TX 引脚加个低通滤波器,然后同时用作 SD 卡的 nCS?只要 SD 命令足够短、时钟频率足够高,选通信号的时间窗口就可以被滤波器“放过去”。这个方法在理论上可行,但实际上非常脆弱。我进一步计算发现,如果想符合 SD 协议初始化要求,UART 的波特率必须低至 300bps 或更低,而即便如此,一旦 SD 卡响应速度稍慢,系统就容易崩溃,因为 SD 协议中明确禁止在读取响应过程中取消片选信号。所以,这个方案甚至比第一个还差。 正当我准备回到最初那个“最坏方案”时,又突然冒出一个更疯狂的想法:RAM 是否介意被选中后又立即取消,而不执行任何命令或数据传输?实验表明:不介意,这项测试在所有 SPI RAM 芯片上都通过了! 为啥?因为 SD 卡除了支持 SPI,还支持 SDIO 协议。SDIO 不用 4 根单向线,而是用 1 根时钟线(CLK)和 2 根双向线(CMD 和 DAT)。如果是 4-bit 模式,会额外加 3 根 DAT,但本项目中只用 1-bit 模式即可。尽管 SDIO 协议在公开 SD 规范中并没有详细说明,但可以通过观察推理出来——这个方案虽然在引脚上节省不了多少,但带来了新的可能性组合。 于是问题变成:是否能把 SDIO 的 3 根信号线与 RAM 的引脚复用?经过反复推演,终于找到“可行映射”:RAM 的 nCS → SD 的 CLK;RAM 的 CLK → SD 的 CMD;RAM 的 MOSI → SD 的 DAT。分析各自访问行为后发现:当访问 RAM 时,SD 卡看到的是 CLK 拉低,而当 RAM 被取消片选,CLK 就拉高。RAM 的 SPI 设置为 Mode 3,CLK 空闲态为高电平,所以每次访问 RAM,对 SD 来说都像是 CMD 线上发送了一个“1”位。这正好对应 SDIO 协议中命令间隙的空闲状态,是安全的。 同样,SD 卡在命令之间不会读或写 DAT 线,所以 RAM 的 MOSI 信号不会被误判。反过来看,当访问 SD 卡时,需要切换 CLK 和写入 CMD、DAT,对 RAM 来说就是快速选中再取消——RAM 也能接受这个行为。完美! 需要注意的是,这个方案的前提是 SD 卡事务必须“一次性完成”,过程中不能访问 RAM。这就意味着不能用多块读写(multi-block),考虑到目前这种引脚紧张的现状,这是可以接受的。 好了,这就是一个潜在可行的解决方案!接下来是实验验证,结果是——成功!当然了,由于 STM32G031 没有对应的硬件模块,因此 SDIO 的访问是完全手工 bit-bang 实现的。最终我写的汇编代码达到了每位 14 个 CPU 周期的传输效率,总的来说表现还不错。 (4)再回到控制台 现在所有的 I/O 接口理论上都能塞进 6 个引脚里了,是时候正式分配每个引脚的功能了。 部分引脚的功能已经确定:RAM 要用标准 SPI,对应的那几根线就直接保留给它。SD 卡与 RAM 共享这些线,也无需额外分配。还剩下引脚 7 和引脚 8,这两个引脚正好是 SWD 调试接口,在开发早期用于调试非常方便。另外通过排除法,它们也也需要作为串行端口。引脚 8 作为 A14 可以充当 USART2.TX,通过启用 USART 的“引脚交换”功能,可以将其转换为 USART2.RX。因为如果没有硬件协助的话,UART 接收是很麻烦的,所以引脚 7 就留给 TX。这个引脚并不支持 USART 的任何替代功能,但没关系,我们可以用 bit-bang 的方式手工实现 UART 发送。 有趣的是,之前在考虑共享引脚方案时,我还想着让 UART 尽可能慢;而现在,为了手动实现 UART,波特率又得尽可能快——因为发送期间 CPU 必须专注“盯着发”,不能被打断。每发送一个字符(以 115200bps 计算)大约只需 87 微秒。理论上我们也可以通过定时中断逐位发送每个字符,但中断带来的时间抖动可能造成串口误码。幸好,大多数情况下设备并不会频繁输出,所以现在这个方案已经很好了。UART 发送 bit-bang 实现效果良好,引脚分配也已完成,接下来就可以进入软件开发阶段了。 但你可能会问,初次烧录怎么办?在这种“非常规引脚布局”下,STM32 官方的引导加载程序(bootloader)怎么可能支持?确实不支持,所以我在板子上设计了 4 个可焊接跳线桥(solder bridge),通过跳线配置可以切换串口的连接方式:在“开发模式”下,bootloader 可用,但 RAM 和 SD 卡都无法工作;在“正式模式”下,ROM bootloader 失效,但项目可以正常启动。幸好,本项目带有自定义 bootloader,所以在初次烧录之后,以后就无需依赖 ROM bootloader 了。 

关于软件的故事 (1)模拟器 早在项目开始前,我就已经写好了一个可以启动 Linux 的 MIPS 模拟器,整个代码用 ARMv6M 汇编语言编写,要在新项目中复用这部分代码并不难。 为了进一步提升性能,我还写了一个 MIPS 到 ARMv6M 的 JIT(即时编译器),运行效果也不错。但不幸的是,这个 JIT 编译器的体积太大,编译后的代码有 46KB,而我在这个项目中可用的翻译缓存只有 6KB,因此性能提升效果并不明显。最终,我选择将这个 JIT 暂时搁置,留待日后再用。 在这次项目中,STM32G031 芯片的 32KB 闪存被划分为两个区域:8KB 分配给引导程序(bootloader),24KB 用于主程序(main application)。除了一些必要的优化和适配,主模拟器代码基本保留了原样。 (2)引导加载程序(bootloader) 那么,为什么需要一个引导程序呢?原因其实很简单:板子上已经没有多余的引脚用于调试;项目还处于开发阶段,需要一种方式来升级固件、修复 bug 或添加新功能。最直接有效的方案就是设计一个支持 SD 卡、能够识别 FAT 文件系统、并在检测到新版本时自动升级固件的 bootloader。 之所以 bootloader 的大小会达到 8KB(实际上是 6.5KB,但由于闪存块大小为 2KB,所以向上取整到了 8KB),是因为它必须包含完整的 SDIO 驱动、FAT 文件系统驱动、闪存写入代码以及大量的日志记录以排查更新时的各种问题。当然,它还内嵌了一个基于 bit-bang 实现的 UART 发送模块。bootloader 会检查主程序镜像中偏移地址 16 处的数值,这是主程序的版本号。只有当更新文件中的版本号高于当前主程序,并且通过了一些基本校验,才会执行更新。至于 bootloader 自身的版本号,则被记录在偏移地址 8 处,仅用于显示启动信息。符合条件的固件更新文件名为 FIRMWARE.BIN,一旦通过校验,就会被应用。 bootloader 本身在芯片复位后运行,其默认频率为 16MHz。而主程序运行时的频率是可调的,方便用户尝试超频。但频繁地手动修改代码、重新编译并烧录固件,太费劲了。这个问题可以通过一个小技巧轻松解决:既然 bootloader 已经挂载了 FAT 文件系统用于检查更新,那么顺便也可以扫描是否存在以 CLOCK 开头命名的文件或文件夹。如果有,它后面的数字就会被解析为主程序的运行频率(单位 MHz)。如果这个值不在合理范围(32–200MHz)内,或者找不到相关文件,则默认设为 132MHz。 (3)SD 卡分区与启动过程 和我之前几个基于 MIPS 模拟器的项目一样,本项目的启动流程也借鉴了 PC 启动过程的设计。系统首先读取 SD 卡的第一个扇区(sector),将其加载到内存前几个字节中,然后跳转执行;这个一级引导代码会继续寻找一个分区类型为 0xBB 的分区,并将其完整加载至内存地址 0x80001000,然后再次跳转;此时,二级引导程序开始运行,它具备日志和串口输出功能;它会扫描所有分区,找到被标记为“active”的那个,尝试将其挂载为 FAT16 文件系统;如果该分区中存在名为 VMLINUX 的文件,它会将其作为 ELF 文件加载,并跳转至其入口地址;如果该文件是一个有效的 Linux 内核,就会进入 Linux 系统启动流程。 传递给内核的启动参数(command line)被硬编码在 bootloader 中,不支持动态修改。它指定内核将 /dev/pvd3 作为根文件系统,并以 /sbin/uMIPSinit 作为 init 程序,同时它还会尝试将 /dev/pvd1 挂载为 /boot 目录。 仔细阅读上文可以发现,虽然系统要求根文件系统必须在第 3 个分区(pvd3),但其它分区顺序其实并没有强制要求——这是刻意为之的设计。对于这个项目,FAT 分区是第一个,bootloader 分区是第二个,Linux 根文件系统是第三个。为什么呢?当插入具有多个分区的 SD 卡时,Windows 和 macOS 会挂载第一个分区, 而Linux 会挂载所有分区,这意味着:
bootloader 在开始执行任何操作之前,首先会静静地等待 6 秒钟。这段延迟是在重新配置引脚前进行的,目的是为用户留出一段时间连接调试器(SWD),因为开发板上预留了一个 4 针的调试接口。超过 6 秒后,bootloader 会重新配置引脚,此时 SWD 功能不再可用,调试器无法再连接。此外,作为一种后备机制,bootloader 还会通过设置 option bytes 选项字节,实现如下行为:如果将 BOOT0 引脚(第 8 引脚)拉高,芯片将从 ROM 启动; 禁用芯片的 RESET 引脚(在本项目中作为普通 GPIO 使用)和 BOR(低电压复位,在本项目中没有用)。一切就绪后,bootloader 会尝试初始化与 SD 卡的通信,检查更新文件,最后正式启动系统。 
性能如何? STM32G031 官方标称的最高运行频率是 64MHz,那为什么这里还要讨论让它跑到 150MHz 呢?原因是:只要适当应用一些“黑科技”,STM32G031 实际上超频能力非常强。STM32G031 的 CPU 核心电压由内部稳压器供电,而这个电压可以通过 PWR->CR1 寄存器进行调整。ST 官方文档中提到了两种电压设置:VOS2(对应 1.0V 的 Vcore),在这种设置下芯片只能跑到 16MHz;VOS1(对应于 1.2V 的 Vcore),这种情况下芯片只能跑到 64MHz。 实际测试表明,在 VOS1 模式下,STM32G031 可以稳定运行在 75MHz 左右,这已经算是一次不错的超频了,但还不算惊艳。然而,早期文档(以及同系列芯片的资料)中还提到了 VOS0 模式,对应 1.35V 的 Vcore。如果我们强行尝试开启这个电压模式,会怎样?结果让人惊喜——真的能用,而且超频潜力大幅提高:大多数芯片在 136MHz 时仍能稳定运行,个别体质优良的芯片甚至能冲上 180MHz!当然,Flash 的访问速度并不会随着频率提升而同步加快,因此必须正确设置 Flash 的等待周期(wait state),虽然这会影响一些速度提升,但综合来看还是值得的。 在主频达到 148MHz 时,这颗 STM32 模拟运行的 MIPS CPU,大致等效于 1.65MHz 的 MIPS R3000(禁用 FPU)。它算不上性能怪兽,但能在大约一分钟内启动,并且 vi、make、objdump 和 gcc 等工具都能正常工作——这是一个完整的 Debian 系统,你甚至可以通过 /boot 目录导入 .deb 包并安装,一切都能跑起来。 
最后组装! (1)获取零件 你可以选择自己购买零件,并把 PCB 板送到自己喜欢的打板厂打样焊接。另外,我们也正在寻找合作厂商来打包成套件发售,如果你有相关线索可以联系我——这可能是极佳的 DIY 礼物! 
(2)初步焊接 接下来是你们最关心的部分:如何自己动手焊出这块板子。你手上这块板子,其实我已经尽量把它设计成容易组装的。我们推荐的焊接顺序如下:
(3)二次焊接 这个阶段要给 STM32 刷入 bootloader。为此,可以下载 STM 提供的烧录工具(Windows 用户可使用官方 flasher,其他系统可以用开源的 stm32flash 工具)。准备一根 USB-C 线,用两根细导线桥接电路板上标记为 R101 和 R201 的位置,此时确保 SD 卡未插入且尚未焊接 RAM 芯片。然后,将板子通过 USB-C 接入电脑,系统应识别到一个虚拟串口,使用烧录工具写入 BOOTLOADER.BIN 文件(路径和串口名称视系统而定)。 烧录完成后,移除用于桥接 R101 和 R201 的焊接导线,改为连接 R102 和 R202,这也是串口引脚的最终正确配置。最后,焊接 RAM 芯片(APS6408 或 VTI7064,位置为 U2),此芯片的引脚 1 也有一个小凹坑标识,将其对齐 PCB 上的小圆点并焊接即可。至此,硬件部分组装完成! 
烧录主固件并完成首次启动 我们需要使用磁盘镜像写入工具,将提供的系统镜像写入一张容量不小于 1GB 的 SD 卡中,具体如下: (1)Windows:使用 [Win32 Disk Imager]; (2)macOS:使用系统自带的“磁盘工具”(Disk Utility); (3)Linux:使用 dd 命令。 这个系统镜像里已经包含了完整的启动流程:包括第一阶段的 MIPS 启动程序、第二阶段的 MIPS 启动程序、包含 Linux 内核与固件的分区,以及一个 Debian 根文件系统(rootfs)。 镜像写入完成后,弹出并重新插入 SD 卡,电脑会识别并挂载 FAT 文件系统分区。此时,请将下载包中的主固件 FIRMWARE.BIN 拷贝进 SD 卡的 FAT 分区中。这个步骤的作用是让启动引导程序在第一次启动时自动识别并烧录该固件。如果你没有重新编译固件,其实这个步骤可以跳过,因为镜像本身已经包含了这个文件。不过即便重复操作也不会有任何副作用,可以放心执行——如此,一切便准备就绪! 插入 SD 卡,再次将 USB-C 数据线连接至电脑,打开你喜欢的串口终端软件,并将其配置为 115,200 bps,8N1 格式。几秒钟后,你将看到串口终端开始打印启动信息,这是多个启动阶段依次执行的过程。第一次启动时,STM32 的熔丝(fuse)将被写入配置,此时可能需要你在串口信息停止时重新插拔一次 USB-C 接口。由于熔丝是非易失性的,因此只需执行一次即可。大约 20 秒后,你将看到 Linux 内核启动的打印信息,整个启动过程大约需要 1 分钟,最终你将看到一 shell 提示符。考虑到系统仅有 8MB 的内存,因此建议你在登录后先执行 swapon /swapfile 命令启用交换空间,启用过程大约需要几十秒,完成后你就可以运行更多命令和程序了!
|