看到一篇有趣且硬核的文案,边翻译边学习。
作者自制了一个 CPU ,而后用 Rust 实现了软件部分,包含有些简单的程序:绘图器、BASIC/Scheme 语言解释器、Web 服务器、终端模拟器和MIDI 音乐播放器等。本文将触及许多主题内容,喝一杯,慢慢看。 “
原文:https://zdimension.fr/crabs-all-the-way-down/
尽管自90年代败兴,各样各样的 CPU 架构数量已然逐步减少,但此刻仍然有许多区别的、不兼容的CPU架构在运用。大都数计算机运用x86_64,几乎所有的移动设备和近期的 Mac 都运用某种基于ARM64的ISA(指令集架构)。
不外在特定行业,还有更奇特的:大都数路由器仍然运用MIPS(历史原由),一部分研发者运用RISC-V,PS3 运用PowerPC,20年前的有些服务器运用Itanium,当然,IBM 仍然在营销她们基于S/390的大型机(此刻改名为z/Architecture)。嵌入式世界的制品就更加多了。AVR(用于Arduino)、SuperH(土星、Dreamcast、卡西欧9860计算器),以及可敬的8051,一个1980年的英特尔芯片,迄今仍在生产、营销,乃至被第三方扩展。
所有这些架构在其定义特征上都有所区别,重点的区别是: 字的体积(word size)。8、16、31、32、64位,有时更加多。设计风格(design style)。RISC(指令少,操作简单),CISC(指令多,执行繁杂的操作,VLIW(指令长,同期并行做非常多事情)。存储架构(memory architecture)。哈佛(Harvard,独立的代码存储器和数据存储器),冯-诺伊曼(von Neumann,共享)。许可成本。RISC-V是开放的,可以避免费运用,而X86和ARM等则需要许可费。特性集(features set):有些特性在特定架构平台有特定的支持。例如,浮点数(x87)、加密(AES-NI)、支持本地高级字节码执行(Jazelle、AVR32B)、矢量计算(SSE、AVX、AltiVec)。“
这还不算 DSP 架构,轻描淡写地说,它是 ISA 版本的“阴阳魔界(Twilight Zone)”(支持各样奥妙的算术运算,特殊的数据体积等)。
译注: Twilight,本意是晨光或暮光,特指黎明或傍晚时分的太阳尚位置于地平线以下,微弱的阳光已起始照耀大地,使世界陷入一种为朦胧的状态。傍晚时分一般被视为通往幻想世界的入口。
非常多人构建了她们自制的CPU,要么在实质的面包板上,要么在软件中,用于模拟器或电路合成 。她们都是非常有趣的项目,即使对初学者来讲亦是如此,由于它确实有助于把握代码怎样转化为电信号,为咱们运用的每一个设备供给动力,以及怎样真正在简单操作的基本上实现繁杂的语言功能。
制作一个 CPU
有些场景促进我在数字电路模拟器中设计一个简单的 类 ARM(ARM-ish)的 CPU。我最初运用的是logisim-evolution (后来我作为其研发团队的成员),近期因为性能原由,我将电路迁移到了Digital(Logisim没法在超过50或60Hz的频率下模拟我的电路,而 Digital 则达到了20kHz)。
选取 ARM 是由于它支持ARM Thumb指令集的一个子集,它本身便是ARM CPU所支持的多个指令集之一。它运用32位字,但指令的宽度为16位。
但又类 ARM (-ish)是由于,它只支持其中的一个子集(很大,但远不完整),并且在某些方面被故意限制。不支持哪些奇怪的指令,如PUSH / POP / LDM / STM系列(RISC ARM ISA中的一个巨大的来自CISC的污点),并且被汇编器实现为手动 load/store。同期亦不支持中断(Interrupt)。
【此段重复,可忽略】从本质上讲,我设计的不仅是一个CPU,而是一个能够叫作为计算机的东西;它有一个ROM,一个RAM,以及做为 "前面板 "的各样设备。
从本质上讲,我设计的不仅是一个 CPU,而是一个能够叫作为计算机的东西:它有一个ROM,一个RAM,以及做为 "前面板(front panel)"的各样设备。
设备
一台真正有用的计算机,不仅需要一个 CPU 和一个内存芯片。它还会有外围设备和其他设备:键盘、屏幕、磁盘驱动器、扬声器、网卡等,几乎所有你能(或不可)想象的东西都已然被做成为了计算机设备。
归根结底,你独一需要的是能够从设备中传输数据。有两种相反的方式来做到这一点:要么设备是尤其的,要么就不是。
基本上,有些架构(X86,说的便是你)除了内存之外,还有一个特殊的、独立的位置空间用于I/O,有其特殊的、区别的指令:在8086上,你会用MOV来读写主内存,用IN/OUT来读写设备。有些设备(最重要的设备:PS/2掌控器、软盘、串口......)有一个固定的端口号,其他设备在起步时由BIOS分配一个端口号。在过去,一般的做法是需求设置环境变量或编写配置文件来通告软件那些设备被插进(例如著名的BLASTER配置行)。这被叫作为PMIO(端口映射的输入/输出)。
另一个选取,亦是几乎所有其他人运用的选取,包含现代X86计算机,是持有一个统一的虚拟(virtual)位置空间。 “
我在这儿运用虚拟这个词是为了将这个统一的位置空间与真正的理学内存位置空间区掰开来(理学内存位置空间本身仅在拥有单一内存单元的设备上有真正的道理)。还有一个叫做虚拟内存(virtual memory)的概念,它指的是一个完全不关联的东西(虽然在方式上很类似):经过运用交换(swap)等策略,为程序供给一个比计算机RAM更大的位置空间,准许将RAM页面移动到磁盘存储中,以释放工作内存的空间。
想象一下,IP位置应该是映射全部互联网的,但在现实中,一个位置不必定要精确地映射到某个地区的一台设备。例如,127.0.0.1(IPv6中的::1)是本地回环位置,映射到你正在运用的设备。这不需要经过网络通信的软件晓得,由于映射是由于操作系统的网络栈完成的。
这儿亦是同样:(虚拟)位置空间的区域被映射到理学组件上。为了给你一个现实世界的例子,下图是NES 的位置空间。
img从0到800(十六进制)的位置被映射到WRAM(工作RAM),从2000到2008映射到PPU(图形卡)的掌控寄存器,从4000到4018映射到APU(声卡)。这被叫作为MMIO(内存映射的输入/输出)。
对我而言,这种办法的最大优点是它的简单性。从CPU的方向来讲:它只是内存罢了!我认为这是最重要的。取设备区域的位置、读、写,真的很简单。它亦使软件层面更易:你不必写内联汇编来调用特殊指令,只要你能从指针上读写,就能够了。
除此之外,内存映射还能够用来供给对区别内存芯片(如ROM和RAM)的拜访。下面是我的电路图示意。
circuit重视组件和映射器之间的边缘的箭头;它们暗示组件是只读、读/写或只写的。
CPU
与真正的 CPU 相比,咱们要做的这个 CPU 非常简陋。
它和拇指同样大,有 16 个 32 位的寄存器,编号为r0到r15。最后三个有专用别名:r13是sp(Stack Pointer,栈指针),r14是lr(Link register,链接寄存器),r15是pc(Program Counter,程序计数器)。
栈指针(Stack Pointer)
内存是很难的,暂且不表,后面会讲。
16个是非常多的。因此在现实中,它们被分为低(r0-r7)和高(r8-r15)寄存器。高位寄存器只能用特定的指令进行操作,因此低位寄存器是平常要用到的。
指令被分为几类,每一类都包括有一个相同头部(common header)的指令。我不在这儿一一列举,但最常用的是ALU(算术和规律)运算、load/store 指令(相针对pc、sp或通常寄存器)、栈操作指令和分支指令(有要求和无要求)。
指令组由独立的子电路处理,它们都写入共享总线中。
总线(Bus)“
Bus 是一个令人惊讶的多义词。它在非常多行业都有定义,乃至在电子和硬件行业,它亦被用来做各样事情。所有这些定义之间的一起原因是 "连接其他事物的东西"。与前几部分同样,无论怎样我都会简化解释,由于电子学是一个庞大的科研行业。
在电路设计术语中,总线是一组连接在一块的导线。详细来讲,在这种状况下,它是一组电线,在某一瞬间仅有一根电线发出信号。在电气行业,这是经过运用所说的三态规律来实现的:一个信号要么是0,要么是1,要么是Z(发音为 "高阻抗")。Z是 “较弱”的信号,即,倘若你将Z信号与0或1信号相连,将输出后者。这特别有用:你能够有非常多独立的元件,每一个都有一个 "enable"输入,仅有在它们被使能时才输出信号,否则就输出Z。它是一个简单的规律门,倘若被启用,则输出其输入信号而不改变,否则输出Z。
而后,所有这些组件都能够插在一块,你能够使能其中一个,并容易得到其输出。
组件实例:带寄存器偏移的 load/store
这是一个处理形如 {direction}R{sign}{mode} {destination}, [{base}, {offset} 指令的组件,其中: {direction}:不是 LD(load),便是 ST(store){sign}:要么不做(不扩展),要么便是 S(符号扩展值,以填充32位){mode}:要么是 nothing(全字,32位)和 H(半字,16位),要么是 B(字节,8位){destination}:目的寄存器,要读出/写入{base}, {offset}:内存中的位置(将是两者的值之和)例如,ldrh r1, [r2, r3] 大致相当于C代码中的r1 = *(short*)(r2 + r3)。
这组指令能够按如下表所示编码: 15141312111098765432100101opcodeRoRbRdopcode 是一个 3位的值,用于同期对{direction}, {sign} 和 {mode}进行编码。 “
{direction} 有两个可能的值(load, store)。{sign} 有两个(raw, sign-extended),{mode}有三个 (word, halfword, byte)。这就寓意着有 2⋅2⋅3=12 种可能多组合,起码超过了opcode 的 2^3=8 种可能值,因此有些组合是不可能的。
这是由于仅有对不完整值(halfword, byte)的 load操作能够进行符号扩展,因此无效的组合(strsh, strsb, strs, ldrs)无得到编码。
这儿是电路图:
circuit2这儿运用到有些区别的规律单元: 小三角形是隧道(Tunnel):命名的导线能够在电路中的任何地区接入。大梯形是复用器(Multiplexers):它们输出第n个输入,其中n亦是一个输入。三条线的三角形是缓冲器(Buffers):倘若边线是高电平,它们就输出其输入,否则就输出Z(高阻抗)。黄色方框将一个3位的低位寄存器号码转换成4位的寄存器号码(在其前面加一个0)。旁边有整数范围的大矩形是分割器(splitters):它们将一个多比特的值分割成多个较小的值,以拜访单个比特或比特范围。从上到下依次为: 三个寄存器(Rd, Rb, Ro)在各自的位置(0-2, 3-5, 6-8)从指令中被读取,并被送到相应的全局通道(RW, RA, RB)。opcode被解码,以检测它是一个store(000,001,010)还是一个load(剩余值)。opcode被解码以找到模式的值。0表率字,1表率半字,2表率字节。opcode再次被解码以找到符号的值(仅对操作码011和111来说是真的,因此咱们能够检测最后两个比特是高位)。Instr0708隧道是这个组件的激活引脚;倘若当前指令属于这个指令组,它便是高电平。
几乎所有的其他元件都和这个元件同样,当你把它们都插在一块时,你就得到了一个能够执行指令的电路。
内存很难
操作数据并将其存储在某处以便你以后能够取回,这个看似简单的问题实际上......并不简单。让你的CPU拜访一个大的线性内存单元阵列是不足的,你必须决定你要用它来做什么。瞧瞧这个Python程序。 print("Hello, World!"
)
字符串应该存放在哪里?它必定是在某个地区。那print呢?它不是一条指令,它只是一个全局变量,恰好被设置为一个内置函数或办法类型的对象,你能够用()操作符调用。它亦必须被储存在某个地区。记住,你日前真正持有的东西是一个大的数字数组。除此之外,你真正持有的独一一组操作是{"按位置加载(load)数值","按位置存储(store)数值"}。不是吗?
CPU的语言是汇编指令。这些指令有一个固定的、定义好的编码,在ARM Thumb指令集上,它们总是(亦便是几乎总是)有相同的体积:16位。忽略指令头(告诉你这是哪条指令),这将占用几个比特,咱们火速就会发掘,倘若咱们把位置做为立即值(指令中为常量值),咱们不可寻址到超过216字节的内存。
因此呢:寻址模式和内存对齐。
倘若咱们看一下平常的程序,能够观察到内存有两个重点的运用状况:存储局部变量(函数中的变量,或参数),和存储全局变量(全局配置,将在程序之间共享的内存)。 用例分配体积最大生命周期分配时间释放时间本地(Local)一般为小分配在当前函数调用内当进入当前函数时当离开当前函数时全局(Global)任意静态生命周期任意时刻任意时刻有一个显著的区别:一方面,"本地内存",用于小的、确定的分配,而 "全局内存",用于任何事情,在任何时间,很少有限制。
这怎样映射到咱们的 "大块内存单元"?咱们将从 "全局 "内存起始。咱们对它的运用方式一无所知,因此咱们不可做太多的假设。你能够在任何时候需求任何数量的字节,并在任何时候把它还给操作系统,况且你一般期盼 "还给 "的空间能够被后续分配运用。这很难。现实世界中的对应物是一大堆东西,躺在地上,等着某个程序把它捡起来,运用它,或把它扔进垃圾桶。因为这个原由,它被叫作为 “堆(heap)”。
接下来,咱们能够看到 "本地内存 "以一种特定的方式演变:当咱们进入一个函数时,它就会增长,当咱们退出时,它就会缩小,况且函数调用遵循类似栈(stack)的模式(当你进入一个函数时,你能够做任何你想做的事,但你最后总是在某个点退出它)。事实上,它确实是一个栈(在算法数据结构的道理上),它有两个操作:push (增长)和pop(缩小)。这个 "本地内存 "被叫作为栈。
因为它是以这种方式增长和收缩的,因此咱们并不需要对分配的内存块做任何登记,例如它们在哪里,用什么策略来选取分配新块的位置,等等。咱们独一需要的数据是 “深度”(即咱们在该堆栈中的深度,或换句话说,栈的长度)。一般的做法是,咱们将内存中的某个地区设置为栈的起点,并在某个地区(例如,在一个寄存器中)保存一个全局变量,该变量包括栈最顶层的项(topmost item)在内存中的位置:栈指针(在ARM上为sp,或其全名为r13)。
还有一点我无说明:栈的增长方向。有些架构使它向上生长(push等于增多栈指针,pop等于减少),但大都数架构则相反,使它向下生长。向下增长寓意着你能够很容易地使堆(heap)从位置0起始,并使栈(stack)从任何最大的位置起始,况且你能够保证它们不会出现碰撞,直到堆向上增长得太多或堆向下增长得太多。 “
在内存术语中,向上寓意着增多,向下寓意着减少。"栈向下增长 "寓意着栈的增长会减少栈指针,反之也然。网上的许多图都是以位置0为顶点来讲明内存,暗示向下寓意着增多,但这是一种误导。
此刻咱们晓得了内存的工作原理,那样咱们该怎样拜访它呢?咱们已经看到,因为指令太小,咱们没法所有寻址,那样咱们怎样应对这种状况呢?
答案是运用区别的寻址模式。例如,倘若你想拜访栈上的内存,你一般会拜访栈顶部的东西(例如你的局部变量),因此你不必给出完整的内存位置(大),而只需要给出相针对栈指针的数据距离(小)。这便是sp-relative寻址模式,看起来像ldr r1, [sp, #8]。
另外,咱们能够假设你大多会存储4字节或更大的东西,因此咱们会说栈是字对齐(word-aligned)的:所有东西都会被移动,以便位置是4的倍数。
有时,你想存储只对单一功能有用的数据。例如, switch / match指令一般运用跳(jump)表来实现:在程序中存储一个偏移量列表,而后加载正确的偏移量并转过去。因为这将被存储在函数本身的代码中,因此呢相针对代码中的当前位置进行内存操作就变得特别有用,这便是怎样得到pc-relative寻址的:ldr r2, [pc, #16]。与sp同样,内存是字对齐的,因此偏移量必须是4的倍数。
函数调用
在汇编中,调用函数的最简单办法是经过运用jump。你把一个标签放在某个地区,而后跳到那里。但有一个问题:你怎么回去呢?一个函数能够从多个地区被调用,因此你需要能够 “记住 ”这个函数是从哪里被调用的,况且你需要能够跳到一个位置,而不是跳到一个已知的标签。
简单的办法,就像之前的栈指针同样,是运用一个全局变量(即寄存器)来存储调用者的位置,并有一个特殊的转指令,将寄存器设置到当前位置(链接),这般咱们就能够在以后回到它(分支)。在ARM上,这便是bl(branch-link)系列指令,该寄存器被叫作为链接寄存器(缩写为lr,昵叫作为r14)。
然则还有一个问题:它对嵌套调用不起功效! 倘若你从另一个被调用的函数里面调用一个函数,链接寄存器的值会被覆盖。
这并不是一个新问题:当你调用一个函数时,其他的寄存器亦会被覆盖,你不可指望程序员去阅读她们所调用的每一个函数的代码,瞧瞧那些寄存器是安全的,那些不是。这儿触及到调用惯例:在每一个架构(X86、ARM......)上都有一套规则(ABI),告诉你一切是怎样工作的,一个函数被准许做什么,尤其是那些寄存器应该被被调用者保存下来。一个保存的寄存器不是只读的:被调用者能够对它做任何事情,只要当掌控权被交还给调用者时,旧的值就回来了。
处理这个问题的办法是经过寄存器保留。当进入一个函数时,在栈中为局部变量分配空间,但亦为必须保存的寄存器分配空间,当退出时,原始值从栈中放回到寄存器中。
在ARM上的这些寄存器中,链接寄存器亦被保留。ARM特殊寄存器能够做为通用寄存器运用的一个很酷的方面是,你不必运用分支指令来转到某个地区:你能够直接写到PC中去。
ARM汇编中的函数的一般模式是这般的:
my_function: push {r4, r5, lr} ; save r4, r5 and
lr movs r4,#123 ; do stuff movs r5, #42 pop {r4, r5, pc} ; restore the values to r4, r5 and
*pc*!
设备
要使一个电路看起来像一台计算机,需要的东西并不多。针对初学者来讲,你可能想从以下方面入手。 一个键盘(读取原始字符输入)。终端表示器(表示字符,如终端模拟器)。一个视频表示器(表示原始像素数据)。一个随机数出现器。一个十进制的7段表示器。一个网卡(能够经过TCP接收和传输数据)。所有这些都被CPU和在其上运行的程序视为内存中的位置。例如,向位置0xFFFFFF00写一个字节将在终端表示器上表示一个字符。从位置0xFFFFFF18中读取一个字节,就能够晓得键盘缓冲区是不是为空。运行代码
在这个东西上运行代码的最简单办法是简单地编写设备代码并将其加载到ROM中。
这儿有一个简单的程序。
movs r0, #255 ; r0 = 255 (0x000000FF)
mvns r0, r0 ; r0 = ~r0 (0xFFFFFF00, address of terminal)
movs r1, #65 ; r1 = 65 (ASCII code of A) str r1, [r0] ; *r0 = r1它将被汇编为 20ff 43c0 2141 6001(8 字节),当被加载运行时,它会在 4 个周期后输出 A:
当然,用汇编编写程序并不完全实用。咱们在很久以前就为此发明了宏汇编程序和高级(与汇编相比)编程语言,因此在这儿就这般做吧。我最初用的是C语言,但火速就换成为了Rust,由于它的易用性和强大的宏支持(针对像这般的受限环境特别有用)。
Rust(技术上来讲,参考编译器rustc)运用LLVM做为编译的后端,因此任何LLVM支持的目的,Rust都在必定程度上支持。在这儿,我运用内置目的thumbv6m-none-eabi(ARM v6-M Thumb,无供应商或操作系统,嵌入式ABI),但有一个很大的限制:我的CPU不是一个完整的ARM CPU。
因为不是所有的指令都被支持(有些指令是由于我自制的汇编器模拟的),我不可只是创立ARM二进制文件并加载它们。我需要运用我自己的汇编器,因此我直接调用编译器,告诉它发出原始汇编代码,而后将其发送到我的汇编器,最后生成可加载的二进制文件。
另外,因为我运行的代码无操作系统,无任何外边代码,因此我不可运用Rust的标准库。这是一个完全支持的用例(叫作为no_std),并不寓意着我完全不可运用任何东西:它只是寓意着我不运用std crate(一般的标准库),而是运用 core crate,它只包括最基本的必需品,尤其是不依赖于运行在下面的操作系统。然而,核心库不包含任何依赖堆分配的东西(如String或Vec),这些都是在alloc库中找到的,因为与我的构建系统相关的有些繁杂原由,我亦不运用这个核心库。
基本上,我写了我自己的标准库。我此刻能够写有些程序,例如: fn main
() { println!("Hello, world!"
); screen::circle(10, 10, 20
, ColorSimple::Red); let x = fp32::from(0.75
); let mut video = screen::tty::blank().offset(50, 50
); println!("sin(", Blue.fg(), x, Black.fg(), ") = ", Green.fg(), x.sin(), => &mut
video);
}
输出:
陷阱
运用rustc的原始汇编输出寓意着我不可依靠我正在构建的 crate 以外的其他 crate 的代码(这需要运用链接器,我在这儿无运用)。我乃至不可运用编译器的内置函数:像memcpy或memclr这般的函数经常被用来执行块拷贝,但它们并不存在于生成的汇编中,因此我不得不自己实现它们(我从Redox这儿借用了有些代码)。
另一个问题是,因为我正在模拟有些指令(经过将它们翻译成其他支持的指令序列),分支偏移量可能比编译器预期的要大。问题是:Thumb上的要求性分支需要一个8位有符号的立即值,因此倘若你试图跳到超过128条指令的前面或后面,你就不可对该指令进行编码。
在实践中,这寓意着我经常要从函数中提取代码块以使其更小,况且全部代码库都运用了#[inline(never)],以迫使编译器将这些代码块放在单独的函数中。
实现一个可用的标准库并不是最简单的任务。保证全部库符合人机工效学原理,运用起来很顺手则更难。我不得不运用许多不稳定的(nightly-only)功能,如GATs、相关类型默认值和特化等等。
选取 Rust 完成整个项目的舒适度,让我有兴趣在将来的底层/嵌入式研发项目中选取运用 Rust。倘若用C语言来做这个项目的百分之一,难度会大得多,况且代码的可读性亦不会像这个项目同样。
成品展示
绘图器(Plotter)
展示视频:https://zdimension.fr/content/media/2022/08/javaw_8FU7Np02mK.mp4
这个绘图器运用fixed-point(16.16)数字库,三角函数是用泰勒级数实现的。
BASIC 解释器
这是一个简单的BASIC解释器REPL,类似于80年代的家用电脑(如C64)上的东西。你能够逐行输入程序,表示它们,并运行它们。支持的指令有PRINT、INPUT、CLS、GOTO和LET。提示支持LIST、RUN、LOAD、ASM和ASMRUN。
basic程序亦能够经过网络加载(类似于C64上的磁带加载),用一个程序,如: cat $1 <(echo) | # read file, add newlinedos2unix | # convert lineends to Unix (LF)nc -N ::1 4567 # send to port
在这儿,LOAD起始监听,收到的行会以#为前缀表示,并像用户输入的那样进行阅读。
basic2程序亦能够被编译成 Thumb 汇编以得到更高的性能。关联视频:https://zdimension.fr/content/media/2022/08/javaw_ABdTnpRbRl.mp4
这部分工作机制: 每一个BASIC指令和表达式都被转换(编译)为一连串的汇编指令,例如CLS只是将12(\f)存入终端输出的位置,而表达式只是设置了一个小的栈机,对公式进行计算并将结果存入r1,LET指令计算其表达式并将r1存入变量的存储单元中。一旦程序被编译,它就像一个函数同样被执行,像这般。let
ptr = instructions.as_ptr(); let as_fn: extern "C" fn() -> () = unsafe
{ core::mem::transmute(ptr) };
as_fn(); bx lr指令被附加在指令列表的最后,因此当程序结束时,它将掌控权交还给解释器。Web 服务器
web
Scheme 语言 REPL
终端模拟器
这是一个简单的终端模拟器,支持ANSI(VT)转义代码的一个子集(足以表示美丽的颜色和移动光标)。
它运用从这儿借来的5x7字体,ANSI解码规律是手工编写的(外面有有些工具箱便是这般做的,但它们支持所有的ANSI代码,而我只需要这个程序的一个非常小的子集)。
像这般的一个脚本能够用来起步一个shell,并把它传送到电路中。 exec 3<>/dev/tcp/127.0.0.1/4567
cd ~ unbuffer -p sh -c stty echo -onlcr cols 80 rows 30erase ^H;sh <&3 1>&3 2>&3
shellMIDI 音乐播放器
Digital供给了一个MIDI输出组件,它支持按下或释放某个乐器的键,因此我写了一个简单的程序,用midly来解析经过网络发送的MIDI文件,而后解码有用的信息来播放歌曲。
关联视频:https://zdimension.fr/content/media/2022/08/javaw_vJd5RnPa6r-1.mp4
小结
总而言之,这特别有趣。ARM/Thumb是一个很好的架构,能够做为一个辅助项目来实施,由于它得到了编译器的良好支持,况且它的一个足够小的子集就足以运行有趣的代码。Logisim和Digital都是用于数字电路仿真的优秀工具。Rust是办事情和制造东西的好工具。
最后,本文项目源码:parm_extended:https://github.com/zdimension/parm_extended
|