我一直在关注本·埃特(Ben Eater)关于在面包板上建造一台6502计算机的系列文章。当我通过使用更大的4线x 20字符LCD显示器获得65C22后,我想知道是否可以直接将计算机与外部键盘接口。IBM Personal System/2(PS/2)于1987年发布,它有一个非常常用的键盘和鼠标连接器,它使用一些方便的电信号来与更古老的技术接口,比如这台板式计算机所基于的WDC 65C02和65C22。
电学上,PS/2非常简单:电源、地、时钟和数据。时钟由键盘内部的微控制器驱动,速率约为10-16.7 kHz。数据线在时钟较低时有效,数据和时钟都是开路收集器,可以通过上拉电阻直接与计算机相连。
我买了一个PS/2连接器,并将四根电线焊接到正确的引脚上。电源+5V,接地0V,键盘的三个状态灯闪烁,所以我知道我没有短路或向后安装引脚。那很好。
我把PS/2时钟线连接到NmiB(DIP-40 W65C02S上的第6针),并用1kΩ上拉电阻把它绑在5V导轨上,因为我无论如何都是在做硬件更改,所以我也给IRQB(W65C02S的第4针)增加了一个按钮,以便更容易地触发第二类中断。(它在调试时很有帮助-在interruptservice处理程序中,我可以打印任何我想要的信息。)。
同样,数据线也通过1kΩ上拉电阻连接到PA0(40针W65C22的引脚2)。
6502上的两个中断引脚的使用略有不同,不可屏蔽中断(NMIB)是边缘触发的,这通常意味着只有一个中断源可以使用它。但这对PS/2来说很好,因为我们只想在每个时钟信号上中断一次。
中断请求(IRQB)是电平触发的:只要中断被启用并且输入较低,处理器就会生成中断。这意味着服务多个设备要容易得多,但是必须有某种方法来清除另一端的中断-而且我们在PS/2总线上实际上没有这种能力,所以这不是一个好的选择,除非我们想要添加一些粘合逻辑。听起来很贵,而且我的面包板上也没有多少空位了。
WDC65C02S支持一些额外的操作码,所以我将使用-wdc02标志让vasm理解这些操作码。我还添加了-chklabels、-wail和-x。它们分别在标签与助记符/指令匹配时发出警告,在警告时返回错误代码,在引用未定义符号时显示错误。
为它们中的每一个设置矢量-这将替换Ben Eater视频中的.org$fffc:
现在,是时候声明和初始化一些内存了。首先,在零页中命名四个字节:
实际值并不重要-它们是指向RAM的前256B中字节的指针。
前两个KEY_BUF_X和KEY_READ_X是256字节循环数组的偏移量。让我们将该数组命名为:
后两个变量用于解码PS/2字节。现在,让我们在重置处理程序中将它们全部初始化为零。
所有这些都不起作用,但是我们已经在RAM中保留了解码原始PS/2字节流所需的所有空间。
如前所述,PS/2时钟速率在10-16.7 kHz之间,数据线仅在时钟周期的下半部分有效。
我的计算机有1 MHz时钟,这意味着我们有1M/16.7k=59.88个周期来完全处理NMI。而且数据只在上半年有效。
WDC65C02需要6(或7?)。周期来处理中断,并且只有在前一条指令完成后才能处理中断,所以当我们开始中断处理程序的第一条指令时,我们可能已经进入了12或13个周期!
我们将从65C22 VIA读取端口,并且需要使用一些位掩码来获取第一位。如果设置高位而不是低位会非常方便,但是我已经在使用PA7作为LCD上的E引脚了,我不想仅仅为了方便而破坏与Ben代码的兼容性。
因此,在中断例程期间,我会将PA0旋转到PA7位置,然后将其屏蔽。
NMI:PHA;保留A寄存器LDA端口;数据位在A错误的低位;数据位在进位标志错误;数据位在A的高位和#KEY_DATA;所有其他位已被清除;将A存储在KEY_BUF[KEY_BUF_X]phx LDX KEY_BUF_X STA KEY_BUF,x INC KEY_BUF_X;按正确的顺序PLX PLA恢复X,A;完成中断RTBUF[KEY_BUF_X]phx LDX KEY_BUF_X sta KEY_BUF_X。
解码PS/2比特流所需的周期比我们在不可屏蔽中断处理程序中所能承受的多几个周期。因此,处理程序是浪费的,并且将位样本作为字节写入256字节的循环缓冲区中。
第一位(状态0)是起始位,其后是8个数据位(状态1-8),最低有效优先,然后是奇偶校验位(状态9),然后是停止位(状态10)。
LOOP:PS2_CHECK_BIT:LDA KEY_READ_X CMP KEY_BUF_X beq LOOP PROCESS_ONE_PS2_BIT:LDA PS2_BIT_NUMBER CMP#0 beq PS2_START_BIT CMP#9 beq PS2_Parity_Bit CMP#10 beq PS2_STOP_BIT;否则,转到PS2_DATA_BIT PS2_DATA_BIT:;将$80/$00移入PS2_NEXT_BYTE LDX KEY_READ_X LDA PS2_NEXT_BUF和#$7F ora KEY_BUF,x sta PS2_NEXT_BYTE NEXT_PS2_BIT:;推进状态机INC PS2_BIT_NUMBER;推进读取位指针INC KEY_READ_X;检查是否有更多位要解码JMP PS2_CHECK_BIT PS2_START_BIT:;TODO:我们可以。TODO:我们可以验证奇偶校验位是否正确JMP NEXT_PS2_BIT PS2_STOP_BIT:LDA PS2_NEXT_BYTE;TODO:我们可以验证停止位是否正确;TODO:我们已成功将PS/2字节解码到A寄存器。STZ PS2_BIT_NUMBER INC KEY_READ_X;给主循环(当前为空)一个运行JMP循环的机会。
PS/2不发送ASCII,它发送扫描码。大多数PS/2键盘使用“SET 2”代码。我们关注的字符在按下键时发送一个单字节的“make”代码(按住键时每秒发送几次),在释放键时发送一个两字节的“Break”代码。
一个很好的抽象方法是将这些扫描码解码到一个缓冲区中,这样我们就可以进行行编辑等操作,但我只会将我识别的所有字符直接打印到LCD上。
首先,我们需要创建PS/2扫描码的映射。你可以看看键盘上的按键矩阵是如何连线的。
。对齐8 PS2_SCAN_CODE:;0123456789ABCDEF。ASC";?`?";;0。ASC";?Q1?ZSAW2?";;1.。ASC";?CXDE43??VFTR5?";;2.。ASC";?NBHGY6?MJU78?";;3.。ASC";?,KIO09??./L;P-?";;4.。ASC";??';??[=?]?\??";;5。
PRINT_PS2_KEY:;如果我们之前收到#$F0,则忽略此字节位PS2_IGNORE_NEXT_CODE BMI CODE_IGNORED;A为扫描码。;查看是否有中断代码CMP#$F0 beq IGNORE_NEXT;边界检查PS2_SCAN_CODES CMP#$5F BPL TOO_HIGH;索引到PS2_SCAN_CODES Tax LDA PS2_SCAN_CODES,x JSR PRINT_CHAR RTS TOH_HIGH:RTS IGNORE_NEXT:LDA#$FF ps2_IGNORE_NEXT_CODE RTS CODE_IGNORED:STZ PS2_IGNORE_NEXT_CODE RTS。
还有更多的工作要做,比如Shift、Backspace、箭头键,甚至将行输出到屏幕上的正确位置。就像我上面提到的,将这些ASCII码存储在行缓冲区或任何适合您的应用程序用途的地方可能更有意义,您甚至可以使用另一个循环缓冲区来跟踪指针。
但是,嘿,至少我们可以说你好,世界,这才是最重要的。