这是一篇超级详细的徽章的入门指南,带你一步步使用指令,让你的徽章动起来,带你再次了解汇编的底层知识和逻辑!
文章来自:使用说明 |2022 Hackaday Supercon 6 徽章指南 |Hackaday.io
Github链接:GitHub - Hack-a-Day/2022-Supercon6-徽章工具
现代计算机提供千兆字节的 RAM 和比我们手指打字更多的处理器内核。但众所周知,黑客的定义不在于他们拥有的工具,而在于他们如何有技巧和想象力地使用这些工具。
因此,今年我们采取了略有不同的方法。我们没有试图像2019年那样在徽章上塞进更多的最先进硬件,而是决定回朔。2022年Supercon徽章是一个关于真正控制一件硬件的课程,了解每一个内存位是做什么的,以及为什么。
毫无疑问,这将是一个挑战。事实上,我们敢打赌,在11月4日拿到徽章的人中,大多数人以前从未做过任何类似的事情。人们将被迫离开他们的舒适区,但这正是我们的本意。
工具,包括汇编程序、仿真器和一堆示例代码,可以在 Supercon6 Badge Tools Github 存储库中找到。
如果您想了解有关徽章的所有信息,您可以在 Voja 的大量文档 PDF 中找到它,可在此处找到: 官方徽章页面 - 技术深入探究
徽章基础知识
想象一下,现在是1975年的夏天。随着一阵温暖的微风吹过窗户,你细心地把盖子滑到刚组装好的Altair 8800上。你凝视着一排排的LED和开关,并自豪地意识到,你很可能是镇上唯一一个拥有自己电脑的人。你真的像柯克船长一样生活在未来。
现在只有一个问题...你究竟要用它来做什么呢?
就像那些勇敢的人们,在看到《大众电子》杂志上的广告后邮购了自己的Altair套件一样,你可能也在想,你现在怎样才能好好利用你这个新奇的高科技玩具。
不用担心,感到有点困惑是很自然的。毕竟,它看起来和我们一生中使用过的大部分计算机都不太一样。但毫无疑问,它是一台计算机,一旦你学会了如何使用它,你会惊讶于它的强大功能。
讲语言
虽然我们历尽千辛万苦开发了汇编器和模拟器,使你能够使用你的现代计算机为Supercon.6徽章编写和测试代码,但我们相信,直接使用徽章前面板上的按钮输入一些程序有很大的好处。
首先,通过按下背面的电源按钮来打开徽章。然后按下左下角的模式按钮,直到PGM旁边的LED灯亮起。
这会将徽章置于编程模式,在这种模式下,你将直接使用标有OPCODE、OPERAND X和OPERAND Y的三组四个按钮输入指令。最右边你会看到一个标有“Data In”的按钮,如果还没有亮起,你得按它直到BIN旁边的LED亮起。这将允许你使用按钮直接切换二进制位,亮起的LED代表1,熄灭的LED代表0。
例如,要输入二进制序列0010 0100 1100,LED应该如下所示:
要实际输入12位指令到徽章中,你需要按标有DEP+的按钮。这将清除其余按钮上方的LED,为下一个指令重置。 - ADDR + 按钮可以用来逐行遍历代码,允许你验证和更正之前的输入。
额外的模式
就本文档而言,我们主要关注PGM(编程)模式,但在我们进一步探讨之前,还应该了解一下徽章更高级的功能。
DIR(直接模式)
在直接模式中,你可以从徽章的内部闪存以及通过串行连接的其他设备保存和加载程序。它还可以让你实验CPU的不同部分,例如累加器、X和Y寄存器、加法器/减法器、逻辑组和标志位。在这个模式下,可以使用时钟按钮执行指令。
有关DIR模式的更多信息,请参阅手册的18 - 22页。
SS(单步模式)
在单步模式下,你可以一次执行一个输入的程序指令。这对于调试非常宝贵,因为徽章上的LED会让你确切地看到CPU内部和RAM中发生的事情。如果你的程序没有如你所预期的工作,通过单步模式进行调试很可能会帮助你找出原因。
有关SS模式的更多信息,请参阅手册的23 - 25页。
一小步
现在我们已经把基础知识说清楚了,让我们现在输入一个完整的指令到徽章并执行它。
将徽章置于PGM模式,我们首先通过按住ALT并同时按下两个ADDR按钮来清除任何现有指令。现在,确保徽章底部右侧的“Data In”选择器设为BIN,按照以下方式配置LED:
这对应于以下指令,它会将数字7放入寄存器R9中:
在确认正确的LED灯已经点亮之后,按下DEP+按钮来存入这条指令。注意,按钮上方的LED灯应该熄灭,以准备下一条指令。然后按MODE按钮,直到旁边的RUN LED灯亮起,再按标有RUN的按钮。如果你的操作都正确,徽章上的LED灯应该会像右边的图像一样显示。
确实,这些LED灯中包含了大量的信息,但目前重要的是要注意,在LED矩阵的右侧,紧挨着数字9的位置,有三个红色LED灯亮起。这表明,根据我们输入的指令,数字7(0111)已经被存储到了寄存器9中。
恭喜你,你已经正式在4-bit Supercon.6徽章上运行了你的第一个指令!现在可以休息一下,也许还可以向别人炫耀一下你的成就。当你准备好了,继续进行下一课,我们将看看能否做一些更有趣的事情。
数学运算
如果你已经做到了这一步,我们就假设你已经掌握了在Supercon.6徽章上输入二进制指令和执行它们的基本知识。现在,我们要通过将多条指令串联起来编写实际的程序,从而加大难度。
同时,我们也将开始引入一些新的指令来帮助你在学习过程中前进。总共有31个操作码(opcode),虽然这个基础介绍不会涵盖它们所有,但到我们结束时,你应该已经掌握了足够的知识,能够进行一些更加复杂的操作。
现在,让我们从一些基本的数学运算开始。
寄存器
在我们开始进行数位间的计算之前,我们需要讨论一个非常重要的概念:寄存器。
在现代术语中,你可能会把这些看作是变量。但更准确地说,它们是内存中的位置,CPU可以快速地在这些位置存储和回忆数据。Supercon.6中的4位CPU拥有10个通用寄存器(R0至R9),以及许多我们现在不会深入讨论的特殊寄存器。(别担心,有一整本手册专门介绍它们。)
大多数指令至少需要一个寄存器作为参数,有些指令实际上是硬编码的,只能与R0一起工作。这意味着,要完成任何任务,你需要习惯于在寄存器之间交换数据。
既然解释了这些,让我们看看如何实际上使用寄存器来在徽章上完成任务。
加法
超越文字上的简单描述,以下程序是可能最简单的程序之一:我们将把两个数字相加,并查看结果。该程序的代码如下所示:
1001 0000 0010 mov r0, 2 ; Put 2 into R0
1001 0001 0010 mov r1, 2 ; Put 2 into R1
0001 0000 0001 add r0, r1 ; Add the two registers
在左侧,我们有你可以直接输入到你徽章中的二进制序列(去试试看吧),中间是等价的汇编代码,最后在右边我们有一些描述性的评论。
运行这个程序后,徽章的矩阵应该看起来像右边的图像。每行的四个LED对应于每个寄存器中持有的4位,所以第0行(0100)的二进制序列意味着数字4当前在R0中。第二行的LED(0010)告诉我们2在R1中。
逻辑上,你可能期望我们小小的加法程序的答案会出现在第三行,但是加法指令只接受两个寄存器,所以这就是我们目前能使用的全部。一般来说,任何操作的结果都会保存在这两个寄存器中的第一个,所以在这个例子中,我们得到的是R0中的4和R1中仍然存在的2。
现在,细心的读者可能已经注意到,在这个程序中(很像在上一课的单个指令演示中),我们没有给出任何实际将结果输出到LED矩阵的命令。这是因为,这个矩阵实际上并不是一个传统意义上的显示器——它是徽章CPU工作内存的一个窗口。
通过仔细操作其视野内的内存,可以将矩阵用作显示器,这是我们稍后将会介绍的内容。但现在,我们将直接查看寄存器的内容。
减法
正如你可能预料的那样,减法与加法非常相似:
1001 0000 1100 mov r0, 12 ; Put 12 into R0
1001 0001 0111 mov r1, 7 ; Put 7 into R1
0011 0000 0001 sub r0, r1 ; Subtract R1 from R0
在徽章上运行程序,我们看到R1(0111)旁边的LED再次显示它保留了原来的值7,而R0(0101)的LED显示12减去7的结果是5。
增加/减少
当你开始编写更复杂的程序时,你经常需要对寄存器加1或减1——比如当你想要跟踪循环进行了多少次时。事实上,这是一个非常常见的任务,以至于CPU有专门的inc(增加)和dec(减少)指令来执行它。
考虑以下程序:
1001 0000 0010 mov r0, 2 ; Put 1 into R0
1000 0001 0000 mov r1, r0 ; Copy R0 to R1
0000 0010 0000 inc r0 ; Add 1 to R0
0000 0010 0000 inc r0 ; Add 1 to R0
0000 0011 0001 dec r1 ; Sub 1 from R1
首先,我们把2放入R0,然后在下一行将其复制到R1,使它们相等。然后我们对R0执行两次inc(增加)操作,对R1执行一次dec(减少)操作。
当你运行程序并观察LED灯时,你会看到R0现在包含了2 + 1 + 1的结果,即4(0100)。另一方面,R1降到了1(0001),因为我们从原来的2中减去了1。
从技术上讲,你可以通过add和sub指令完成同样的事情,但在这些情况下,你需要提供一个包含1的第二个寄存器。在这么受限的系统上,这是一个很大的要求,这就是为什么inc和dec指令如此有价值的原因。
乘法
在另一端,乘法是你相对不经常做的事情。实际上,甚至没有乘法指令。当然,这并不意味着我们不能做乘法——我们只需要稍微跳出常规思维。
例如,你可能没有一个命令可以让你执行5 x 3,但你可以反复给自己加5:
1001 0000 0101 mov r0, 5 ; Put 5 into R0
1000 0001 0000 mov r1, r0 ; Copy R0 to R1
0001 0000 0001 add r0, r1 ; Add (5 + 5)
0001 0000 0001 add r0, r1 ; Add again (5 + 5 + 5)
结果(15)会点亮R0上的所有四个LED,这恰巧是我们能够使用这些4位指令实际处理的最大数字。技术上有些指令是采用8位数的,我们不久将介绍到。
除法
除法可以用非常相似的方式完成,你只需不断减去这个数字直到你得到零。当然,这里的技巧是跟踪你减去了这个数字多少次,并检查结果是否为零。
我们还没有涉及这些概念,这使得这是一个介绍新主题的合理地方:程序流。
程序流
到目前为止,我们的所有程序都只有几行,直接一路运行下去。但是当你开发更复杂的代码时,你最终会想要进行循环,或者根据存储在特定寄存器中的值改变程序的行为。
虽然这些是核心的编程概念,但在如Supercon.6徽章这样受限的系统上实现的方式可能不如你习惯的那样直观。例如,在CPU的操作码中你不会发现传统的IF...THEN语句,但有一些等效的指令在你继续进行时你会经常使用。
比较和跳过
cp(比较)指令的作用正如其名:比较两个值。但这里有一个小技巧,因为这是只能与寄存器0一起使用的指令之一。所以,任何时候你想要比较两个值,你总得先把它放进R0。
使用cp指令,你将能够确定给定值是与R0中存储的值相同、少于还是多于。这是通过在比较后检查C和Z标志的状态来完成的。
你可以用这些标志做几件事情,但也许你最常用的与它们结合的指令是skip,正如你可能猜到的,它根据标志的状态有条件地跳过程序中的行。
让我们用一个小程序来演示:
1001 0000 1111 mov r0, 15 ; Put 15 into R0
0000 0000 0101 cp r0, 5 ; Compare R0 to 5
0000 1111 0001 skip c, 1 ; Skip next line if R0 < 5
1001 0000 0010 mov r0, 2 ; Put 2 into R0
0000 0000 1111 cp r0, 15 ; Compare R0 to 15
0000 1111 1010 skip z, 2 ; Skip next two lines if R0 = 15
1001 0000 0000 mov r0, 0 ; Put 0 into R0
1001 0001 1111 mov r1, 15 ; Put 15 into R1
运行这个程序后,你应该在第0行的右侧看到四个LED亮起,这表明R0仍然包含原始的15这个值。那是因为会改变它的两行代码被跳过了,首先是因为R0中的值大于5,又因为它等于15。
此外,你应该会看到第一行没有LED亮起。那是因为第二个跳过指令实际上跳过了两行,而不是一行。跳过多个指令的能力绝对是有帮助的,但请记住,你最多只能跳过4行。
对于现代程序员来说,在比较后跳过一行可能看起来有些违背常理——通常情况下,跟在IF语句后面的行是你期望执行的。但正如你将看到的,跳过指令可以和执行它们一样有用。
相对跳转(循环)
虽然没有确切的循环指令,但4位CPU可以根据需要在程序中向前和向后跳转,如果小心使用,可以让你重复执行代码的特定部分(或者完全避免)。
下面结合jr(跳转相对)指令、inc(递增)、cp(比较)和skip(跳过)来演示如何控制程序的流程:
1001 0000 0001 mov r0, 1 ; Put 1 into R0
0000 0010 0000 inc r0 ; Increment R0
0000 0000 0101 cp r0, 5 ; Compare R0 to 5
0000 1111 1001 skip z, 1 ; Skip next line if R0 = 5
1111 1111 1100 jr -4 ; Jump back 4 lines
1001 0001 1111 mov r1, 15 ; Put 15 into R1
好的,我们一步一步来详细讲解这里的过程。
首先我们在R0中放入一个1,然后对其进行递增。此时,R0等于2。我们将其与5进行比较,发现不相等,所以我们不会跳过jr -4指令。这使得程序回到前面4行(jr指令本身算作一行)到inc r0的位置。我们现在有一个循环,将一直持续到R0等于5为止。
一旦发生这种情况,循环退出,我们的最后一条指令就可以执行。结果应该是LED显示出右侧图片的样子——R0中为5,R1中为15。
递减和跳过
如之前提到的,递增和递减指令非常有用,尤其是当它们与jr和skip组合起来使用时,实际上有一个现成的复合指令你可以使用,这使事情变得更简单了:dsz
这条指令将递减给定的寄存器,直到它等于零,一旦发生这种情况,它就会跳过下一条指令。下面是一个简要的示例:
1001 0000 0101 mov r0, 5 ; Put 5 into R0
1001 0001 1010 mov r1, 10 ; Put 10 into R1
0000 0010 0001 inc r1 ; Increment R1
0000 0100 0000 dsz r0 ; Decrement R0 until 0
1111 1111 1101 jr -3 ; Jump back 3 lines
1001 0010 1111 mov r2, 15 ; Put 15 into R2
在这个示例中,我们在递减R0的同时递增R1,使用一个jr指令继续循环,直到dsz察觉到R0等于0并跳过它。最终结果,如右侧所示,第0行没有LED亮起,第1行和第2行各有四个LED亮起。
如你所见,当你想重复执行特定次数的操作时,这个指令非常完美。与手动执行相比,它可以节省几行代码,另外由于它是硬编码寻找等于条件的,你不必记住哪些标志代表什么。
除法(重访)
既然我们已经覆盖了循环和条件控制程序流的内容,让我们来处理上一章提到的除法程序:
1001 0000 1111 mov r0, 15 ; Put 15 into R0
1001 0001 0011 mov r1, 3 ; Put 3 into R1
1001 0010 0001 mov r2, 1 ; Start result counter at 1
0011 0000 0001 sub r0, r1 ; Subtract R1 from R0
0000 1111 1010 skip z, 2 ; Skip next two lines if result is zero
0000 0010 0010 inc r2 ; Increment counter
1111 1111 1100 jr -4 ; Jump back to division
首先,被除数(15)放入R0中,除数(3)放在R1中。然后程序通过循环不断地从R0中减去R1,直到结果为0。这里我们不需要使用cp指令,因为sub指令会方便地为我们触发Z标志。一旦R0为空,接下来的两行将被跳过,以便循环能够退出。
最终结果应该看起来像下边的图片——第0行不会有LED亮起,第1行会显示0011(3),第2行会显示我们除法问题的答案:5(0101)。
但等等......如果有余数怎么办?或者你想要将一个较小的数除以一个较大的数呢?嗯,这是个好问题,你应该深入研究一下,找出答案。毕竟,我们不可能在这里显示一切。
这就结束了程序流程章节。接下来是硬件I/O。
硬件I/O
正如你所意识到的,与现代计算机相比,Supercon.6徽章只提供了最基本的必需功能。但它确实包括了一些有趣的板载功能,并且有许多系统参数可以通过软件控制。还有一个物理扩展口,包括四个输入和四个输出引脚,可以用来将徽章连接到其他设备。
要真正了解徽章的功能,你应该查阅完整的文档。特别是有关特殊功能寄存器(SFRs)的部分,这些寄存器允许你修改徽章的配置——这个主题实际上有自己的专门手册。
但为了给你一个可能性的概念,让我们来看一些例子。
设置CPU速度
如果你已经看过徽章的示例程序,你会知道许多程序都以设置仿真的4位CPU速度的指令开始。虽然宿主PIC24FJ256始终以16 MHz运行,但仿真CPU有16个速度等级供你选择。
没有特殊指令来设置CPU速度——相反,你只需要使用mov指令,将所需设置写入特殊功能寄存器0xF0,这与你将数据写入通用寄存器R0至R9的方式非常相似。其他的SFRs通常也是这样,一旦你查阅了文档并且知道了可接受的参数,配置徽章的硬件就变得非常直接了当。
以下示例展示了如何即时改变CPU的频率,以及它如何影响程序执行的速度:
1001 0000 1000 mov r0, 8 ; Put 8 into R0
1100 1111 0001 mov [0xF1], r0 ; Set CPU speed to 8 (100 Hz)
1001 0001 1111 mov r1, 15 ; Put 15 into R1
0000 0100 0001 dsz r1 ; Decrement R1 until 0
1111 1111 1110 jr -2 ; Jump back 2 lines
1001 0000 1100 mov r0, 12 ; Put 10 into R0
1100 1111 0001 mov [0xF1], r0 ; Set CPU speed to 12 (5 Hz)
1001 0001 1111 mov r1, 15 ; Repeat same loop as before
0000 0100 0001 dsz r1
1111 1111 1110 jr -2
在这个示例中,我们使用dsz指令从15倒数到零,首先CPU频率设置为100 Hz,然后再次以5 Hz运行。运行这个程序时,你应该会看到第一行的LED在第一次快速闪烁接近一秒钟。当第一行的LED回来并开始再次倒数时,它将需要大约8秒钟来完成循环。
生成随机数
考虑到Supercon.6徽章包含的奢侈品有多么少,你可能会惊讶地发现它具有一个板载系统用于生成高质量的(相对而言)随机数。
更准确地说,徽章有的是所谓的伪随机数生成器(PRNG),因为尽管Voja实现了一些巧妙的技巧(你可以在特殊功能寄存器手册的第26页阅读具体技术),在没有更专业硬件的情况下获得真正的随机数据是困难的。不过,这个功能是一个令人惊喜的特性,对许多程序肯定是非常实用的。
要获取四位的新鲜随机性,你只需使用mov指令从特殊功能寄存器0xFF读取:
1001 0000 1100 mov r0, 12 ; Set CPU speed to 12 (5 Hz)
1100 1111 0001 mov [0xF1], r0 ;
1001 0001 1111 mov r1, 15 ; Put 15 into R1
1101 1111 1111 mov r0, [0xFF] ; Read from PRNG, put into R0
0000 0100 0001 dsz r1 ; Decrement R1 until 0
1111 1111 1101 jr -3 ; Jump back 3 lines
运行这个程序,你应该会看到第0行的LED在几秒钟内跳跃到随机值,同时第1行的LED从15倒数。请注意,我们仅降低CPU速度是为了放慢过程并使其更易于可视化——调整CPU频率并不是读取PRNG所必需的。
获取按钮状态
到目前为止,我们所有的程序都是自动运行的,无需用户输入。但如果你确实想不时从用户那里获得输入,有一个特殊功能寄存器你可以读取,以查看徽章正面的哪个按钮被按下。
找出最后一个被按下的按钮就像执行单个mov指令到SFR 0xFD一样简单:
1101 1111 1101 mov r0, [0xFD] ; Read last button pressed into R0
运行这个单行程序,你会注意到每次你按下徽章正面的按钮时,第0行的LED都会变化。你可以通过实验来找出每个按钮的值,但幸运的是手册中已经为我们提供了这些数据:
如果你只想知道按钮何时被按下,但不一定需要知道具体是哪一个,也有一个特殊功能寄存器(SFR)可以做到:0xFC。
这里的技巧是,位于0xFC中的4位值实际上表示四个独立的信息,所以你需要隔离每一位,而不是仅比较整个值。为了做到这一点,我们可以使用恰如其分的bit指令:
1001 0001 0000 mov r1, 0 ; Zero out R1
1101 1111 1100 mov R0, [0xFC] ; Read key status into R0
0000 1001 0010 bit R0, 2 ; If second bit of R0 is 0, set Z flag
0000 1111 1001 skip z, 1 ; Skip next line if Z is set
1001 0001 1111 mov r1, 15 ; Put 15 into R1
这个程序检查了特殊功能寄存器0xFC的第二位,这对应于AnyPress标志。简而言之,如果那一位是1,那么徽章上的一个按钮正在被按下。bit命令让我们能够检查那个特定的位,并且如果读取为0,skip指令会被用来跳过将15加载到R1的那一行。
最终结果是,按下任何按钮(或至少是大多数按钮)应该会导致第1行的所有四个LED亮起。
使用扩展接口
Supercon.6徽章具有一个12针的扩展连接器,这不仅用于最初编程徽章,还提供了四个输入引脚和四个输出引脚,这些引脚可以通过软件轻松控制。
事实上,获取输入引脚的状态如此简单,以至于你在技术上不需要做任何事情就能看到它在行动。你有没有注意到,当程序在徽章上运行时,矩阵的B行上总是似乎有四个LED亮着?那实际上是四个输入引脚的状态——如果你将这些引脚中的任何一个短接到地,它对应的灯就会熄灭。
使用针对特殊功能寄存器0x0B的bit指令,将允许你将这些信息拉入你的程序:
1001 0001 0000 mov r1, 0 ; Zero out R1
1101 0000 1011 mov r0, [0x0B] ; Read input pin status into R0
0000 1001 0001 bit r0, 1 ; If first bit of R0 is 0, set Z flag
0000 1111 1101 skip nz, 1 ; Skip next line if Z is NOT set
1001 0001 1111 mov r1, 15 ; Put 15 into R1
运行这个程序后,将扩展连接器上的输入引脚1短接到地应该会导致第1行的4个LED点亮。在0x0B中的每一位对应它们自己的引脚,因此在你的徽章上接线四个额外的按钮是相当简单的:
使用扩展接口的输出部分也非常相似,除了从特殊功能寄存器0x0B的各个位读取数据外,你将用bset指令直接设置0x0A的位,或者用btg指令来反转它们当前的状态。
通过UART进行通信
询问任何硬件黑客,他们会告诉你相同的一点——如果没有串行端口,它就不是一台合格的计算机,Supercon.6徽章也不例外。你不仅会用徽章的UART能力从其他徽章或你的“真实”计算机上传下载程序,它也可以用于与各种有趣的小装置进行任意通信。
与执行许多其他功能相比,通过UART传输和接收数据的过程实际上比你可能预期的要简单一些。如果有个技巧的话,那就是对于你通过线缆发送的每个字节,你需要做两次单独的写入——前4位写入到特殊功能寄存器0xF7,另外4位写入到0xF6。一旦字节的后半部分被写入到0xF6,它就会自动发送。
同样值得一提的是,默认情况下,徽章使用SAO头进行UART通信,而不是扩展头上的引脚。这种行为可以在配置中改变,但为了简单起见,我们在这个例子中保留默认设置。
注意:在撰写本文时(11月4日),徽章固件中的一个BUG阻止了扩展头中的UART引脚正常工作。这将在更新中得到解决,但与此同时,SAO引脚确实如预期工作。
作为一个基础示例,我们来看看下面的程序,它将不断地通过UART发送ASCII字符"A":
1001 0010 0100 mov r2, 0b0100 ; High nibble of ASCII "A"
1001 0011 0001 mov r3, 0b0001 ; Low nibble of ASCII "A"
1000 0000 0010 mov r0, r2 ; Write high nibble to UART
1100 1111 0111 mov [0xF7], r0 ;
1000 0000 0011 mov r0, r3 ; Write low nibble to UART
1100 1111 0110 mov [0xF6], r0 ; Transmit
原理上操作很简单……但实践中你可以看到,仅使用通用寄存器发送任何严肃的消息将会有多么困难。要真正利用UART,你需要一种比我们迄今为止探讨的更高效的数据处理方式。
听起来是转向下一章节的完美时机:操纵内存。
操纵内存
到目前为止,我们主要关注的是通用寄存器R0到R9,以及一些特殊功能寄存器(SFRs),它们允许我们触摸和操纵徽章的内部机制。对于许多任务来说,这已经足够了。
要充分利用徽章,你需要更多地了解其内存的定位和处理方式。如果你有大量(相对来说)数据想要保存并在以后恢复,通用寄存器显然无法满足需求。幸运的是,我们能够利用的,有一大块RAM被夹在通用和特殊寄存器之间。
如你在右侧的图片中看到的,Supercon.6徽章的内存可以被视为一种电子表格。到目前为止,我们已经用了R0和R1这样的友好名称来指代这张表格中的单元格,CPU的指令在很大程度上围绕着它们设计,以便于使用。但是,有些指令的变体允许以不同的方式定位那些位置。
在以下例子中,我们将探索在Supercon.6徽章上利用内存的一些更高级的方法,以及它能解锁的能力。
直接内存寻址
虽然确实有一些指令设置成只能与特定的寄存器(通常是R0)一起使用,但理解在本文档中我们已经习惯使用的寄存器并没有固有的独特之处是很重要的。当你在程序中看到R0或R1时,你本质上看到的是一个书签,它指向徽章内存中的特定位置。这对我们人类来说是一件方便的事情,但就CPU而言,内存就是内存。
因此,存在像mov这样的特殊变体指令,可以指向任意内存位置。虽然使用这些指令对程序员来说努力要多一点,因为你需要跟踪你存放数据的位置,但这对代码本身的影响并不大:
1001 0000 1100 mov r0, 12 ; First put value in R0
1100 0001 0011 mov [19], r0 ; Move into memory location
1001 0000 1111 mov r0, 15 ; Put new value into R0
1100 0001 0100 mov [20], r0 ; Copy to sequential locations
1100 0001 0101 mov [21], r0 ; Note different mov opcode
1100 0001 0110 mov [22], r0 ;
1101 0001 0101 mov r0, [21] ; Read value from memory into R0
确实,当存储或回忆数据时,你仍然需要暂时将值移动到R0,但对于你不需要频繁访问的数据,这是一种很好的方式,可以释放你的命名寄存器,以便进行更高优先级的任务。
间接内存寻址
直接内存寻址很方便,但它有其局限性。具体来说,你仍然必须硬编码你想要寻址的值。在许多情况下这不是一个巨大的问题,但如果能在读取或写入操作的循环中简单地迭代内存地址就会更高效。
恰好有一个mov的版本可以让我们做到这一点。这个指令将两个寄存器作为其输入,代表所需地址的高位半字节和低位半字节。这不仅能够表示8位内存地址(0到255),还允许你使用我们已经学过的指令操纵所需的地址。
在这种情况下,把寄存器想象成二维数组中的索引是有帮助的:第一个寄存器代表所需的列,第二个寄存器代表行。LED矩阵的排列顺序也是如此......这是一个非常方便的关系,但我们稍后再讲。
让我们看看这个示例,它可以快速地用所需的值填充一块内存:
1001 0000 1111 mov r0, 15 ; First put value in R0
1001 0001 0001 mov r1, 1 ; Page 1 of memory
1001 0010 1110 mov r2, 14 ; Start at row 15 so we can use dsz
1010 0001 0010 mov [r1:r2], r0 ; Provide dimensions to mov
0000 0100 0010 dsz r2 ; Loop to fill 14 addresses
1111 1111 1101 jr -3 ;
1001 0000 0000 mov r0, 0 ; Direct addressing
1100 0001 1011 mov [1:11], r0 ; in two dimensions
在运行这个程序之后,你应该会看到LED矩阵左侧大约3/4的区域被完全填满,代表了我们在10个内存位置中存放了值15的结果。对于三行代码来说,并不差。其中的一行也会是空的,这是最后一行代码的结果,它展示了如何在二维上直接寻址一个内存位置。
当然,你也应该注意到的是,如果不是因为矩阵右侧的一些点亮的LED让事情变得复杂,显示出来的将是一个完美的感叹号...
这意味着我们准备结合到目前为止我们所学的一切,并且应对我们旅程的最终挑战:图形处理。
图形处理
如本文档前面所述,就像你此时亲自见到的那样,Supercon.6徽章上的LED矩阵并不是传统意义上的显示器。
它更像是计算机内存的实时窗口,通过四个LED来显示每个半字节内存中的二进制值。结合徽章的单步模式,这在你使用程序时提供了非凡的透明度,因为它让你能实时看到每一个比特何时被改变。
虽然没有现成的方法可以在LED矩阵上显示图像或文本,但不难想象如何将它作为通用输出设备使用。毕竟,我们现在已经看到了如何直接操纵内存中的值,并且我们知道LED会如何响应这些值。
如果你将这与我们早些时候学习的程序流程控制方法,如循环、跳过和跳转相结合,就有可能使用16x8的LED阵列“绘制”基本图像。
清除屏幕(设置内存页面)
然而,这个想法有一个明显的问题,因为我们所有的通用寄存器,甚至一些特殊功能寄存器(SFRs)已经在矩阵上可见,并占用了我们在处理极低分辨率时无法负担损失的“屏幕”空间。
幸运的是,这个问题有一个简单的解决方案:SFR 0xF0允许我们改变矩阵检查的内存区域。因此,我们只需一个指令就可以将矩阵指向一个新的“页面”,在这个页面上只显示我们有意放置的数据:
1001 0000 0010 mov r0, 2 ; Move matrix over to page 2
1100 1111 0000 mov [0xF0], r0 ;
在运行这个程序之后,你可能会觉得什么都没发生。但是仔细观察矩阵会发现,每一个LED都被关闭了;在程序运行时,到这个时刻为止,你本不会看到这种情况。这并不是因为我们禁用了矩阵,我们只是将它指向了一块未使用的RAM,所以没有什么可以显示的。
这意味着接下来要做的唯一事情就是用一些有趣的东西来填充那块RAM。
感叹号(重新审视)
掌握了这些新知识,你可以回到之前的例子,并在一个干净的背景上画出那个感叹号。但是它仍然会是偏离中心的,因为矩阵的每一侧只显示一页RAM。
为了解决这个问题,我们需要将我们的精灵图形的每一半存储在不同的寄存器中,然后利用一个循环和间接内存寻址来画出来:
1001 0000 0010 mov r0, 2 ; Move to page 2
1100 1111 0000 mov [0xF0], r0
1001 0101 0011 mov r5, 0b0011 ; Left side of icon
1001 0110 1100 mov r6, 0b1100 ; Right side of icon
1001 0010 0010 mov r2, 2 ; Register for page 2
1001 0011 0011 mov r3, 3 ; Register for page 3
1001 0100 1110 mov r4, 14 ; Start at row 14
1000 0000 0101 mov r0, r5 ; Get left side data into R0
1010 0011 0100 mov [r3:r4], r0 ; Draw left side
1000 0000 0110 mov r0, r6 ; Get right side data into R0
1010 0010 0100 mov [r2:r4], r0 ; Draw right side
0000 0100 0100 dsz r4 ; Decrement row
1111 1111 1010 jr -6 ; Draw 14 rows
1001 0000 0000 mov r0, 0 ; Clear line at bottom
1100 0011 1011 mov [3:11], r0 ;
1100 0010 1011 mov [2:11], r0 ;
如果程序正确运行,你应该会在矩阵的正中心看到一个大的感叹号。
凭借一点想象力,以及准确统计内存中所有位的位置,这种基础技术可以被用来书写文本或展示简单的图像。通过读取徽章按钮的状态,甚至可以使它们移动。
我们迫不及待想看到谁会是第一个为他们的徽章创建一个贪吃蛇或俄罗斯方块克隆版。
闪烁的灯光
如果你跟着整个指南做到了这里,恭喜你。在相对短的时间内,你已经从幼儿园级的算术进步到通过直接在内存中操作位来绘制图像;而你做到这一切只使用了一些触感按钮和几百个LED灯。
为了纪念这一成就,最后的程序结合了前面课程中涉及的所有内容,为你的徽章产生了一个视觉上令人印象深刻的“屏幕保护程序”。我们很乐见你在会场上持续运行它——随身证明你不需要千兆字节的RAM和16核心(或键盘与监视器)就能硬件黑客。
你可能是走进2022年Hackaday Supercon时没有任何裸机编程的亲身经验,但你离开的时候,情况肯定已经不一样了。
1001 0000 0011 mov r0, 3 ; Set CPU speed
1100 1111 0001 mov [0xF1], r0 ;
1001 0000 0010 mov r0, 2 ; Set matrix page
1100 1111 0000 mov [0xF0], r0 ;
1001 0010 0010 mov r2, 2 ; Set memory row and columns
1001 0011 0011 mov r3, 3 ;
1001 0100 0000 mov r4, 0 ;
1101 1111 1111 mov r0, [0xFF] ; Read random number
1010 0011 0100 mov [r3:r4], r0 ; Move into position on matrix
1101 1111 1111 mov r0, [0xFF] ;
1010 0010 0100 mov [r2:r4], r0 ; Repeat for other side
1000 0000 0100 mov r0, r4 ;
0000 0000 1111 cp r0, 15 ; Check row count
0000 0010 0100 inc r4 ; Move to next row
0000 1111 1001 skip z, 1 ; Skip after row 15
1111 1111 0111 jr -9 ; Loop forever
抱歉,这次没有剧透图片。如果你想要看到这个特定程序的输出效果,你得让你的拇指动起来。
结局只是新的开始
不要以为仅仅因为你完成了这个指南,你就已经掌握了Supercon.6徽章。远非如此——我们只是刚刚触及到这台机器所能做到的表面。有许多重要的概念由于简洁起见,在这个教程中简单地没有被包含。接下来由你这位读者来决定,在你新获得的对底层编程的尊重下,这个徽章将带你走向哪里。
如果你想继续这段旅程,看看我们准备的强大汇编器,它将允许你使用你最喜欢的文本编辑器为徽章编写程序,并通过常见的USB-to-serial适配器发送它们。如果你没有带来一个适配器,也不用担心——我们应该有很多可以使用。尽管有个借口拍拍邻居的肩膀,问他们能不能借用适配器,也不失为在Supercon上搭讪的一个好方式。
汇编器不仅可以让你省去直接用拇指输入所有程序的麻烦,它还解锁了强大的伪指令,如goto和gosub,结合能够标记代码段的能力,这是一种改变游戏规则的推动。甚至还为存储和回调图形数据提供了一些便利,对于任何希望为徽章带来一些游戏的人来说,这是一个受欢迎的能力。
但无论你是选择转向使用汇编器,享受稍微现代一些的软件开发范式,还是坚持使用你可靠的拇指和不会说谎的LED,重要的是你要继续黑客活动。