Back
Featured image of post 在 PC 上使用“任天堂牌”的鼠标

在 PC 上使用“任天堂牌”的鼠标

《Mario Paint》

乍一听起来可能很奇怪,任天堂咋还出过鼠标呢?这款鼠标其实是针对 SFC 游戏《Mario Paint》的外设,咱就先来说说《Mario Paint》这款神奇的游戏。

Mario Paint 卡带
Mario Paint 卡带

《Mario Paint》是任天堂在 1992 年发售的一款创作型游戏,你可以在其中绘图、制作简单的动画,甚至还能创作音乐。这款游戏最初研发的目的是为了改善 “游戏机在父母眼中的形象”,所以总体方向上偏向于激发儿童创造力,最终销量也表明它确实得到了父母们的认可,而且绝对的领先于它的后续模仿者们。虽然《Mario Paint》没有再出过续作,但是它的很多元素都在任天堂的另一款创作向游戏上得到了延续。没错,就是《超级马力欧创作家》系列,比如:

  • “马造”中的重来狗、声响青蛙、关卡机器人等角色都是来自《Mario Paint》的;
  • “马造”中不同物体撞击音符砖块可以发出不同音色的声音,其中很多音色的具体设定与《Mario Paint》音乐模式中保持对应;
  • 初代“马造”种的加载动画来自于《Mario Paint》;
  • “马造”中比较模型的“猪叫”和“娃娃脸”音效都是来自《Mario Paint》的音乐模式。

绘图模式
绘图模式
音乐模式
音乐模式

SFC/SNES 作为家用主机,没法触屏操作,只靠手柄操作这类游戏显然是十分困难的。所以,任天堂推出了一款专门针对 SFC/SNES 的鼠标外设,最初作为《Mario Paint》套装内的物品出售,后来伴随着支持的游戏越来越多,也会推出单独出售的版本。

在 PC 上使用

我在闲鱼上捡到了垃圾成色的《Mario Paint》套装内容,包含游戏本体、鼠标外设、鼠标垫。拿到后用游戏本提测试了下,鼠标工作正常。但是作为一个“鼠标”,却只能在 SFC/SNES 上使用,这也太缺少鼠标的灵魂了。所以咱今天就尝试把这款鼠标转接到现代 PC 上。

通信协议

开始之前,我们要先搞明白鼠标本体跟 SFC/SNES 方便起见,下文统一使用 SFC 主机通信的过程。这款鼠标的接口和主机手柄的接口一样,是一个 7 Pin 接口,从左到右编号:

 

对应的功能如下:

Pin Description Wire Color
1 +5v 棕色
2 Data Clock 红色
3 Data Latch 橙色
4 Serial Data 黄色
5 N/C -
6 N/C -
7 Ground 绿色

在之际运行时,SFC 主机每 16.67ms (大概 60Hz)会向手柄/鼠标请求一次状态信息,具体流程如下:

  1. SFC 主机通过 Pin 3 发送 12us 的高电平;
  2. Pin 3 高电平恢复 6us 后, SFC 主机向 Pin 2 输入震荡时钟信号,周期为 12us。普通手柄持续 16 周期,鼠标持续 32 周期;
  3. SFC 主机发送时钟信号的同时从 Pin 4 读取手柄/鼠标发来的对应的数据位。

通过规定数据位与时钟周期的对应关系,SFC 主机就可以读到手柄不同按键的状态,或者是鼠标的位置信息。对于鼠标来说,32 位通过 Pin 4 读取到的信息含义如下表:

0 1 2 3 4 5 6 7 8 9 A B C D E F
00 B Y Select Start Up Down Left Right A X L R 1 1 1 0
10 Dir Y Y6 Y5 Y4 Y3 Y2 Y1 Y0 Dir X X6 X5 X4 X3 X2 X1 X0

其中前 15 位与普通手柄的语义一致,但是第 16 位为0,以此告知 SFC 主机插入的是鼠标,而不是普通手柄。第17 位开始位鼠标的位置变化信息,鼠标的两个按键则被映射到了 A 和 X 键。

转接到 PC

弄清楚通信协议后,接下来的思路就清晰了:我们需要一个转接的硬件,模拟 SFC 主机读取鼠标数据,同时模拟 PC 的鼠标执行鼠标动作。

硬件上,我选择的是 Arduino Leonardo,与 UNO 最大的区别是它可以很方便的模拟 USB 设备,正好符合我们的场景。接下来需要把鼠标连接到 Leonardo 的引脚上,比较推荐的做法是再买一个 SFC手柄延长线,这样我们只需要把延长线抛开接上引脚,然后鼠标接上延长线就行,不会破坏鼠标原来的接口。

代码上思路就比较简单了,循环读取鼠标信息,并向 USB 发送模拟的指令,借助于 Leonardo 封装的 Mouse 库,实现起来很简单:

#include "Mouse.h"

int DATA_CLOCK    = 6;
int DATA_LATCH    = 7;
int DATA_SERIAL  = 12;

int range = 5;    // output range of X or Y movement; affects movement speed

int buttons[32];
int x = 0;
int y = 0;

void setup(){
  setupPins();
  Mouse.begin();
}

void loop(){
  getControllerData();
  updateMouse();
  delay(1);
}

void setupPins(void){
  // Set DATA_CLOCK normally HIGH
  pinMode(DATA_CLOCK, OUTPUT);
  digitalWrite(DATA_CLOCK, HIGH);
  
  // Set DATA_LATCH normally LOW
  pinMode(DATA_LATCH, OUTPUT);
  digitalWrite(DATA_LATCH, LOW);

  // Set DATA_SERIAL normally HIGH
  pinMode(DATA_SERIAL, OUTPUT);
  digitalWrite(DATA_SERIAL, HIGH);
  pinMode(DATA_SERIAL, INPUT);  
}

void getControllerData(void){
    // Latch for 12us
    digitalWrite(DATA_LATCH, HIGH);
    delayMicroseconds(12);
    digitalWrite(DATA_LATCH, LOW);
    delayMicroseconds(6);
    
    for(int i = 0; i < 32; i++){
        digitalWrite(DATA_CLOCK, LOW);
        delayMicroseconds(6);
        buttons[i] = 1 - digitalRead(DATA_SERIAL);
        digitalWrite(DATA_CLOCK, HIGH);
        delayMicroseconds(6);
    }
    bool output = false;

}

void updateMouse(){
  if (buttons[9] == 1) {
    // if the mouse is not pressed, press it:
    if (!Mouse.isPressed(MOUSE_LEFT)) {
      Mouse.press(MOUSE_LEFT);
    }
  }
  // else the mouse button is not pressed:
  else {
    // if the mouse is pressed, release it:
    if (Mouse.isPressed(MOUSE_LEFT)) {
      Mouse.release(MOUSE_LEFT);
    }
  }

  if (buttons[8] == 1) {
    // if the mouse is not pressed, press it:
    if (!Mouse.isPressed(MOUSE_RIGHT)) {
      Mouse.press(MOUSE_RIGHT);
    }
  }
  // else the mouse button is not pressed:
  else {
    // if the mouse is pressed, release it:
    if (Mouse.isPressed(MOUSE_RIGHT)) {
      Mouse.release(MOUSE_RIGHT);
    }
  }

  //get x\y offset
  x = 0;
  y = 0;
  for (int i = 0; i < 7; i++)
  {
    x *= 2;
    y *= 2;
    x += buttons[17+i];
    y += buttons[25+i];
  }
  
  if (buttons[16]==1){
    x = x *-1;
  }
  if (buttons[24]==1){
    y = y *-1;
  }
  
  if ((x != 0) || (y != 0)) {
    Mouse.move(y*range, x*range, 0);
  }

}

实际体验

先说结论:在现代的高分辨率的 PC 上基本不能用,因为 DPI 实在是太低太低了,稍微小点的区域根本移动不进去。不过老硬件就要配上老电脑嘛,在 Windows 3.1 上用起来还是不错的,视频效果如下(y2b须扶墙):

参考链接

Super Nintendo Entertainment System: pinouts & protocol

Super Mario Maker 2 - Super Mario Wiki, the Mario encyclopedia