NES 支持许多设备,最常见的还是官方手柄,它有 8 个按键:
A
B
SELECT
START
UP
DOWN
LEFT
RIGHT
读取的时候比较奇怪,按道理来讲 8 个按键刚好可以用 1 个 byte 表示,读一次就可以了,但是 NES 读取的时候却是串行的,读 8 次,每次读一个按键,这样做应该是为了兼容性第三方控制器
1. 寄存器
手柄的寄存器位于 CPU 总线的 0x4016 和 0x4017,分别对应 1P 和 2P
7 bit 0
---- ----
xxxx xxxS
|
+- Controller shift register strobe
寄存器只有 bit 0 有效,做为选通标志。当写入选通为 1 时,则可以通过读取寄存器,每次返回一个按键状态,下一次读取返回下一个按钮状态。当写入 0 时,offset 被置位,再次选通读取时会重新从第一个按钮读取
需要注意的是,只能往 4016 写(写 4017 给 APU 用了),读可以往 4016 和 4017 读。写 4016 时,对两个手柄都有效,读时则 4016 为 P1,4017 为 P2
2. 按钮映射
按钮对应的比特位为:
bit
7
6
5
4
3
2
1
0
button
A
B
Select
Start
Up
Down
Left
Right
3. 实现
手柄实现非常简单,只需要读写和更新按钮状态的函数
export enum StandardControllerButton {
A = 0x80,
B = 0x40,
SELECT = 0x20,
START = 0x10,
UP = 0x08,
DOWN = 0x04,
LEFT = 0x02,
RIGHT = 0x01,
}
export class StandardController implements IStandardController {
private data: number;
private isStrobe = false;
private offset = 0;
public updateButton(button: StandardControllerButton, isPressDown: boolean) {
if (isPressDown) {
this.data |= button;
} else {
this.data &= ~button & 0xFF;
}
}
public write(data: uint8) {
if (data & 0x01) {
this.isStrobe = true;
} else {
this.offset = 0;
this.isStrobe = false;
}
}
public read(): uint8 {
const data = this.isStrobe ? this.data & StandardControllerButton.A : this.data & (0x80 >> this.offset++);
return data ? 1 : 0;
}
}