Open107

自作ファミコンROM作成

ピン留めされたアイテム
balmychanbalmychan

自作ファミコンROM作成に関してツラツラと書いていくスクラップです

ピン留めされたアイテム
balmychanbalmychan

参考記事

最近

ファミコン関連(超参考になる)

ファミコン関連(マッパー1)

ファミコン関連(次点)

ファミコン関連(ソフト側)

PCB関連

その他(参考になる)

FRAM

自作ROM作成まわり

WiFiまわり

その他(次点)

スーパーファミコン

書籍

balmychanbalmychan

マッパー0の基盤は3つ手に入ったので(ヤフオクでひたすら四人打ち麻雀とベースボールを買いまくった)

  • ヒートガンが届いたら基盤からPRGROMを抜いて、データを抜き出してみるというのをやる。
  • FlashROMへの書き込みを試す。抜いたデータを書き込む

というのを直近はやる予定。

balmychanbalmychan

下記記事を参考に先にFlashROM(EN29F002T)への書き込みを試そうとした。

http://blog.kshoji.jp/2013/05/famicom-flashcart.html

が、手元にあるArduino YUNではI/Oピンの数が足りず、試すのを断念...(仕方ないのでArduino Mega互換機を注文した)

アドレス用: 15本
データ用: 8本
制御用: 4本

が必要。(YUNには20本しかない)

balmychanbalmychan

最終的にファミコンのオンラインゲームを作ることが目標。
(3コンの端子にネット接続できるマイコンをつないで、自作ファミコンソフトでそこと通信すればできるんじゃないかと夢想)

ちなみにファミコンで馬券を買えるものはあった。(昔父親がこれで馬券を買っていたなぁ...)

balmychanbalmychan

ヒートガンとハンダ吸い取り機でROMを抜くことはできた。(写真1)

※1:ヒートガンで温めながら、ハンダ吸い取り機で丁寧にピンの穴からハンダを取り除いていった
※2: ベースボールは温めながらドライバーでこじりまくったら、傷ついてボロボロになってしまった...

抜き取ったROMから内部のデータを抜けないか、Arduinoとつないで試した(写真2)

だが、期待したデータがこない...(別途抜いておいたベースボールのバイナリデータによると
48 86 0D 84 0E AD 02 20 A9 02 8D 14 40 A0 00 BE となるはずだが、全然合わない...)(写真3)

そもそもやり方がおかしいのかもしれない。
一旦今日はここで断念...

balmychanbalmychan

Arduino Mega(の互換機)が届いたので、改めて抜き取ったROMをつないで読み取りを試してみた。(写真1)(ちなみに読み取るROMはベースボールではなく四人打ち麻雀に変えた)

昨日はArduino YunのI/Oピンの数が足りずアドレスピンA0 ~ A7までしか繋ぐことができなかったが、A0~A15まで全て繋いでみた。(また、基板を見ると(というか、マッパー0はそうなのか?)もともとA14とA15は+5Vをつないで常にHIGHにしているのが正しい様子だったので、そのようにした)

(一部読み取りエラーが発生して値が異なるが)ほぼ似たようなデータをArduinoが吐き出し始めた!!(写真2)
これは成功くさい。
左がArduinoでROMを読み取った出力
右が別途抜いていた四人打ち麻雀のROMデータ
(NESファイルの先頭16バイトはNES用の管理ヘッダーで、PRGROMのデータは17バイト目の0x10から)

一応ROM読み取りのためのArduinoのコードも貼っておく
(内容は、単にアドレスピンでアドレスを指定して、その後データピンからデータを読み取っている)


#define ADDRESS_0 38
#define ADDRESS_1 22
#define ADDRESS_2 23
#define ADDRESS_3 24
#define ADDRESS_4 25
#define ADDRESS_5 26
#define ADDRESS_6 27
#define ADDRESS_7 28
#define ADDRESS_8 29
#define ADDRESS_9 30
#define ADDRESS_10 31
#define ADDRESS_11 32
#define ADDRESS_12 33
#define ADDRESS_13 34

#define DATA_0 42
#define DATA_1 43
#define DATA_2 44
#define DATA_3 45
#define DATA_4 48
#define DATA_5 49
#define DATA_6 50
#define DATA_7 51

#define BIT_0 (1<<0)
#define BIT_1 (1<<1)
#define BIT_2 (1<<2)
#define BIT_3 (1<<3)
#define BIT_4 (1<<4)
#define BIT_5 (1<<5)
#define BIT_6 (1<<6)
#define BIT_7 (1<<7)
#define BIT_8 (1<<8)
#define BIT_9 (1<<9)
#define BIT_10 (1<<10)
#define BIT_11 (1<<11)
#define BIT_12 (1<<12)
#define BIT_13 (1<<13)

void setup() {
  // put your setup code here, to run once:
  pinMode(ADDRESS_0, OUTPUT);
  pinMode(ADDRESS_1, OUTPUT);
  pinMode(ADDRESS_2, OUTPUT);
  pinMode(ADDRESS_3, OUTPUT);
  pinMode(ADDRESS_4, OUTPUT);
  pinMode(ADDRESS_5, OUTPUT);
  pinMode(ADDRESS_6, OUTPUT);
  pinMode(ADDRESS_7, OUTPUT);
  pinMode(ADDRESS_8, OUTPUT);
  pinMode(ADDRESS_9, OUTPUT);
  pinMode(ADDRESS_10, OUTPUT);
  pinMode(ADDRESS_11, OUTPUT);
  pinMode(ADDRESS_12, OUTPUT);
  pinMode(ADDRESS_13, OUTPUT);
  
  pinMode(DATA_0, INPUT);
  pinMode(DATA_1, INPUT);
  pinMode(DATA_2, INPUT);
  pinMode(DATA_3, INPUT);
  pinMode(DATA_4, INPUT);
  pinMode(DATA_5, INPUT);
  pinMode(DATA_6, INPUT);
  pinMode(DATA_7, INPUT);

  Serial.begin(9600);

  scanROM();
}

void loop() {
  
}

void scanROM() {
  int data = 0x00;
  for(int current = 0; current < 16384; current += 1) {
    if ((current % 16) == 0) {
      Serial.println("");
    }
    
    // Set address
    digitalWrite(ADDRESS_0, current & BIT_0);
    digitalWrite(ADDRESS_1, current & BIT_1);
    digitalWrite(ADDRESS_2, current & BIT_2);
    digitalWrite(ADDRESS_3, current & BIT_3);
    digitalWrite(ADDRESS_4, current & BIT_4);
    digitalWrite(ADDRESS_5, current & BIT_5);
    digitalWrite(ADDRESS_6, current & BIT_6);
    digitalWrite(ADDRESS_7, current & BIT_7);
    digitalWrite(ADDRESS_8, current & BIT_8);
    digitalWrite(ADDRESS_9, current & BIT_9);
    digitalWrite(ADDRESS_10, current & BIT_10);
    digitalWrite(ADDRESS_11, current & BIT_11);
    digitalWrite(ADDRESS_12, current & BIT_12);
    digitalWrite(ADDRESS_13, current & BIT_13);

    delay(100);
    
    data = 0x00;
    data |= digitalRead(DATA_0) << 0;
    data |= digitalRead(DATA_1) << 1;
    data |= digitalRead(DATA_2) << 2;
    data |= digitalRead(DATA_3) << 3;
    data |= digitalRead(DATA_4) << 4;
    data |= digitalRead(DATA_5) << 5;
    data |= digitalRead(DATA_6) << 6;
    data |= digitalRead(DATA_7) << 7;
    
    char dataString[50] = {0};
    sprintf(dataString, "%02X", data);
    Serial.print(dataString);
    Serial.print(" ");
  }
}

ちなみにこの書籍がかなり勉強になりそうだったので買ってみた(少しずつ読んでいこう)

https://www.amazon.co.jp/dp/4798147524

ROMの読み取り方はなんとなく分かったので(アドレスピンに信号送ったら、データピンから信号が返ってくるというシンプルなもの)
次は別のFlashROMに適当なデータを書き込むのにトライする(それが成功したら、四人打ち麻雀を書き込む)

balmychanbalmychan

ブレッドボードに挿すジャンパワイヤーの数が足りない...

balmychanbalmychan

(また、基板を見ると(というか、マッパー0はそうなのか?)もともとA14とA15は+5Vをつないで常にHIGHにしているのが正しい様子だったので、そのようにした)

これについては、下記サイトに情報があった。
http://img.atwikiimg.com/www34.atwiki.jp/cc65/pub/dsoft/mmap.html#map0

マッパー0の場合はC000からデータが入っているようで、A14とA15をHIGHにすると二進数で11000000000000、つまり16進でC000となるので、確かにという感じ。
FlashROMに書き込むときは、0000から書き込むならA14とA15は基板に繋がないか、繋ぐならC000からデータを書き込むのが良さそう。

balmychanbalmychan

FlashROMへの書き込みを試している

しかし、全然期待通りの値が書き込まれない...
色々原因は考えられるので、難儀しそうな予感

  • FlashROMはPLCCのため、PLCCからDIPへの変換基板を使っているが、そのハンダ付けがうまく行っていない
  • 変換基板のピンの並びが誤っている (この記事にもそのような情報があるので)
  • そもそも書き込みプログラムに問題がある

balmychanbalmychan

変換基板のピンの並びが誤っている

↑この説が濃厚なので、確認した。

これがその変換基板。PLCCのソケットのピンからストレートに伸びている。

PLCCのソケットの裏蓋を開けて(適当にこじったら開いた)、PLCCのピン番号と変換後のピン番号がどう対応するかを調べた。調べた結果はこんな感じ。

Arduinoと繋ぐ際に、例えばPLCCの1に繋ぎたいなら、右の変換表で変換し、31に繋ぐ、という感じで使う。

続きはまた後日...

balmychanbalmychan

あと、この日は呉服町にあるマルツに行って、少し工具やら部品を買ってきた。
写真1 変換基板につけるピンソケットx8(右が変換基板にソケットをはんだ付けしたあと)

写真2 IC抜き取り工具(左はPLCC用)

balmychanbalmychan

FlashROMへの書き込みに成功した!

  • 昨日作成した変換表を元に、Arduinoと繋ぎなおし
  • https://github.com/kshoji/FlashWriter も参考にしながら、書き込みと、読み取りプログラムを書いた

最初うまくいかずにいた(読み取りはできてそうだったが書き込みが全然上手くいかなかった)が、
0x00から書き込んでいるのが良くなかった?っぽい(どう頑張っても0x00に書き込みは成功しなかった。しかも0x00に書き込んだあとは、その後の読み取りで変な値を出力してしまうようになっていた。意味不明で良くわからない...。)
どちらにせよマッパー1の場合0xC000から書き込めれば良いので、0xC000に対して書き込みを行ったら、なんか成功した。

コードも貼っておく↓

/**
 * Arduino - ROM
 * 
 * A16: 22 - PLCC 2 (変換基板 1)
 * A15: 23 - PLCC 3 (変換基板 2)
 * A12: 24 - PLCC 4 (変換基板 3)
 * A6 : 25 - PLCC 6 (変換基板 4)
 * A7 : 26 - PLCC 5 (変換基板 5)
 * A4 : 27 - PLCC 8 (変換基板 6)
 * A5 : 28 - PLCC 7 (変換基板 7)
 * A2 : 29 - PLCC 10 (変換基板 8)
 * A3 : 30 - PLCC 9 (変換基板 9)
 * A1 : 31 - PLCC 11 (変換基板 10)
 * A0 : 32 - PLCC 12 (変換基板 11)
 * A10: 33 - PLCC 23 (変換基板 23)
 * A11: 34 - PLCC 25 (変換基板 24)
 * A9 : 35 - PLCC 26 (変換基板 25)
 * A8 : 36 - PLCC 27 (変換基板 26)
 * A13 : 37 - PLCC 28 (変換基板 27)
 * A14 : 38 - PLCC 29 (変換基板 28)
 * A17 : 39 - PLCC 30 (変換基板 30)
 */
#define ADDRESS_0 32
#define ADDRESS_1 31
#define ADDRESS_2 29
#define ADDRESS_3 30
#define ADDRESS_4 27
#define ADDRESS_5 28
#define ADDRESS_6 25
#define ADDRESS_7 26
#define ADDRESS_8 36
#define ADDRESS_9 35
#define ADDRESS_10 33
#define ADDRESS_11 34
#define ADDRESS_12 24
#define ADDRESS_13 37
#define ADDRESS_14 38
#define ADDRESS_15 23
#define ADDRESS_16 22
#define ADDRESS_17 39

/**
 * Arduino - ROM
 * 
 * D0 : 40 - PLCC 13 (変換基板 12)
 * D2 : 41 - PLCC 15 (変換基板 13)
 * D1 : 42 - PLCC 14 (変換基板 14)
 * D3 : 43 - PLCC 17 (変換基板 15)
 * D4 : 44 - PLCC 18 (変換基板 17)
 * D5 : 45 - PLCC 19 (変換基板 18)
 * D6 : 46 - PLCC 20 (変換基板 19)
 * D7 : 47 - PLCC 21 (変換基板 21)
 */
#define DATA_0 40
#define DATA_1 42
#define DATA_2 41
#define DATA_3 43
#define DATA_4 44
#define DATA_5 45
#define DATA_6 46
#define DATA_7 47

/**
 * Arduino - ROM
 * 
 * CE   : 48 - PLCC 22 (変換基板 20)
 * OE   : 49 - PLCC 24 (変換基板 22)
 * WE   : 50 - PLCC 31 (変換基板 29)
 * RESET: 51 - PLCC 1  (変換基板 31)
 */
#define RESET 51
#define CE 48
#define OE 49
#define WE 50

/**
 * Arduino - ROM
 * 
 * VCC: 5V  - PLCC 32 (変換基板 32)
 * VSS: GND - PLCC 16 (変換基板 16)
 */

#define VCC 3
#define GND 4

void setup() {
  pinMode(RESET, OUTPUT);
  pinMode(CE, OUTPUT);
  pinMode(OE, OUTPUT);
  pinMode(WE, OUTPUT);

  pinMode(VCC, OUTPUT);
  pinMode(GND, OUTPUT);
  
  pinMode(ADDRESS_0, OUTPUT);
  pinMode(ADDRESS_1, OUTPUT);
  pinMode(ADDRESS_2, OUTPUT);
  pinMode(ADDRESS_3, OUTPUT);
  pinMode(ADDRESS_4, OUTPUT);
  pinMode(ADDRESS_5, OUTPUT);
  pinMode(ADDRESS_6, OUTPUT);
  pinMode(ADDRESS_7, OUTPUT);
  pinMode(ADDRESS_8, OUTPUT);
  pinMode(ADDRESS_9, OUTPUT);
  pinMode(ADDRESS_10, OUTPUT);
  pinMode(ADDRESS_11, OUTPUT);
  pinMode(ADDRESS_12, OUTPUT);
  pinMode(ADDRESS_13, OUTPUT);
  pinMode(ADDRESS_14, OUTPUT);
  pinMode(ADDRESS_15, OUTPUT);
  pinMode(ADDRESS_16, OUTPUT);
  pinMode(ADDRESS_17, OUTPUT);

  setDataPinMode(OUTPUT);

  digitalWrite(RESET, HIGH);
  digitalWrite(WE, HIGH);
  digitalWrite(OE, HIGH);
  digitalWrite(CE, LOW);

  digitalWrite(GND, LOW);
  digitalWrite(VCC, HIGH);

  Serial.begin(9600);

  char bytes[] = "Hello World!";
  writeBytes(0xC000, bytes, sizeof(bytes));

  for (unsigned long i = 0; i < 16 /* 16384 */; i++) {
    if ((i % 16) == 0) {
      Serial.println("");
    }
    
    unsigned char ch = readByte(i + 0xC000);
    char dataString[50] = {0};
    sprintf(dataString, "%02X", ch);
    Serial.print(dataString);
    Serial.print(" ");
  }
}

void loop() {
}

/**
 * バイト配列を書き込む
 * 
 * @address このアドレスから指定size分書き込む
 * @bytes 書き込むデータ
 * @size データの長さ
 */
void writeBytes(unsigned long address, char* bytes, int size) {
  for(int i = 0; i <= size; i++) {
    writeByte(address + i, bytes[i]);
  }
}

/**
 * データピンのモードをINPUT/OUTPUTに変更する
 */
void setDataPinMode(int mode) {
  if (mode == OUTPUT) {
    pinMode(DATA_0, OUTPUT);
    pinMode(DATA_1, OUTPUT);
    pinMode(DATA_2, OUTPUT);
    pinMode(DATA_3, OUTPUT);
    pinMode(DATA_4, OUTPUT);
    pinMode(DATA_5, OUTPUT);
    pinMode(DATA_6, OUTPUT);
    pinMode(DATA_7, OUTPUT);
  } else {
    pinMode(DATA_0, INPUT_PULLUP);
    pinMode(DATA_1, INPUT_PULLUP);
    pinMode(DATA_2, INPUT_PULLUP);
    pinMode(DATA_3, INPUT_PULLUP);
    pinMode(DATA_4, INPUT_PULLUP);
    pinMode(DATA_5, INPUT_PULLUP);
    pinMode(DATA_6, INPUT_PULLUP);
    pinMode(DATA_7, INPUT_PULLUP);
  }
}

/**
 * アドレスをセットする
 */
void setAddress(unsigned long address) {
  digitalWrite(ADDRESS_0,  ((address & 0x000001) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_1,  ((address & 0x000002) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_2,  ((address & 0x000004) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_3,  ((address & 0x000008) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_4,  ((address & 0x000010) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_5,  ((address & 0x000020) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_6,  ((address & 0x000040) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_7,  ((address & 0x000080) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_8,  ((address & 0x000100) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_9,  ((address & 0x000200) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_10, ((address & 0x000400) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_11, ((address & 0x000800) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_12, ((address & 0x001000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_13, ((address & 0x002000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_14, ((address & 0x004000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_15, ((address & 0x008000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_16, ((address & 0x010000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_17, ((address & 0x020000) == 0) ? LOW : HIGH);
}

void commandFlash(unsigned long address, unsigned char data) {
  digitalWrite(WE, HIGH);
  digitalWrite(OE, HIGH);
  setDataPinMode(OUTPUT);

  setAddress(address);

  digitalWrite(WE, LOW);
    
  digitalWrite(DATA_0, ((data & 0x01) == 0) ? LOW : HIGH);
  digitalWrite(DATA_1, ((data & 0x02) == 0) ? LOW : HIGH);
  digitalWrite(DATA_2, ((data & 0x04) == 0) ? LOW : HIGH);
  digitalWrite(DATA_3, ((data & 0x08) == 0) ? LOW : HIGH);
  digitalWrite(DATA_4, ((data & 0x10) == 0) ? LOW : HIGH);
  digitalWrite(DATA_5, ((data & 0x20) == 0) ? LOW : HIGH);
  digitalWrite(DATA_6, ((data & 0x40) == 0) ? LOW : HIGH);
  digitalWrite(DATA_7, ((data & 0x80) == 0) ? LOW : HIGH);

  // data latching occurs 'rising edge'
  digitalWrite(WE, HIGH);
}

/**
 * 状態をリセって
 */
void reset() {
  digitalWrite(RESET, LOW);
  delayMicroseconds(10);
  
  digitalWrite(RESET, HIGH);
  delayMicroseconds(10);

  digitalWrite(WE, HIGH);
  digitalWrite(OE, HIGH);
  digitalWrite(CE, LOW);
}

/**
 * 1バイト書き込み
 */
void writeByte(unsigned long address, unsigned char data) {
  Serial.print("Write ");
  Serial.print(data, HEX);
  Serial.print(" to ");
  Serial.println(address, HEX);
  if (data == 0xff) {
    return;
  }
  
  commandFlash(0x5555, 0xAA);
  commandFlash(0x2AAA, 0x55);
  commandFlash(0x5555, 0xA0);
  commandFlash(address, data);
  
  waitForData(data);
}

/**
 * 1バイト読み込み
 */
unsigned char readByte(unsigned long address) {
  unsigned char data = 0;
  
  setAddress(address);
  
  digitalWrite(CE, LOW);
  digitalWrite(OE, LOW);

  setDataPinMode(INPUT);
  
  data |= digitalRead(DATA_0) == HIGH ? 0x01 : 0;
  data |= digitalRead(DATA_1) == HIGH ? 0x02 : 0;
  data |= digitalRead(DATA_2) == HIGH ? 0x04 : 0;
  data |= digitalRead(DATA_3) == HIGH ? 0x08 : 0;
  data |= digitalRead(DATA_4) == HIGH ? 0x10 : 0;
  data |= digitalRead(DATA_5) == HIGH ? 0x20 : 0;
  data |= digitalRead(DATA_6) == HIGH ? 0x40 : 0;
  data |= digitalRead(DATA_7) == HIGH ? 0x80 : 0;

  digitalWrite(OE, HIGH);
  
  return data;
}

/**
 * データ書き込まれるまで待機
 */
void waitForData(unsigned char value) {
  unsigned char data = 0;
  
  setDataPinMode(INPUT);
  
  while (true) {
    digitalWrite(OE, LOW);
    data  = digitalRead(DATA_0) == HIGH ? 0x01 : 0;
    data |= digitalRead(DATA_1) == HIGH ? 0x02 : 0;
    data |= digitalRead(DATA_2) == HIGH ? 0x04 : 0;
    data |= digitalRead(DATA_3) == HIGH ? 0x08 : 0;
    data |= digitalRead(DATA_4) == HIGH ? 0x10 : 0;
    data |= digitalRead(DATA_5) == HIGH ? 0x20 : 0;
    data |= digitalRead(DATA_6) == HIGH ? 0x40 : 0;
    data |= digitalRead(DATA_7) == HIGH ? 0x80 : 0;
    digitalWrite(OE, HIGH);

    if (data == value) {
      return;
    }
    
    delay(10);
  }
}
balmychanbalmychan

次は、FlashROMに四人打ち麻雀を書き込んで、実機で起動できるかを試す。
と、その前に、カセット側を先に用意しておく。

写真1 四人打ち麻雀からPRGROMを抜いた箇所に、ピンソケットを付けて...

写真2 以前抜き取った四人打ち麻雀のROMをジャンパワイヤーでつなぐ

これで起動するか...

写真3 無事起動!!(元のROMに繋いでるから当たり前ですが。ただ、何度か間違った位置に繋いでおり、全然起動しなくて焦った...)

あとはFlashROMに書き込んで、元のROMの代わりにFlashROMを繋げば、起動するはず。

balmychanbalmychan

書き込んで見ているが

  • 書き込むサイズが16KBあり、Arduinoのスケッチファイルに埋め込むとサイズ上限に引っかかるので分割する必要あり
  • 128~255の値を書き込むと、書き込んだあとの値チェックで値が合わない(unsigned charにしているから問題ないはずだが...謎)
balmychanbalmychan

128~255の値を書き込むと、書き込んだあとの値チェックで値が合わない(unsigned charにしているから問題ないはずだが...謎)

この問題はなんだかよくわからないが直ってしまった。
あとは、300バイトずつなど結構小さい単位でしか書き込みが成功しないので、なんとか小分けに書き込む仕組みが必要。

続きはまた後日...

balmychanbalmychan

書き込むサイズが16KBあり、Arduinoのスケッチファイルに埋め込むとサイズ上限に引っかかるので分割する必要あり

上に関して隙間時間に調べた。
Arduino Megaのスペックは以下の通り

  • Flashメモリ 256KB、そのうち8KBはブートローダで使用
  • SRAM 8 KB

プログラムを配置する領域(Flashメモリ)は256KBあるので、16KBのROMファイルなら載せれるはず。ただ、単純に下記のようなコードにすると、スタティックメモリに載っかるので、8KBが限界

unsigned char ROM[] = { 0x00, 0x01, 0x02, 0x03 ... };

writeBytes(0x4000, ROM);

調べてみると、Flashメモリに載せる方法があった。↓

https://qiita.com/kemako/items/04c32d0225b91e26f8f1

PRGMEM修飾子を使えば、そのデータがArduinoのFlashメモリに載る。
(実際読み取るときは、pgm_read_byte関数を使ってバイト単位で読み取れるらしい)

balmychanbalmychan

ということで早速書き込みをした。0xC000から16KB分書き込み

写真1 こんな感じでしこしこ書き込み(前半は書き込みのログ、後半は書き込んだデータの読み取りのログ)

写真2 データを書き込んだFlashROMとカセットの基板を繋ぐ...
※一部VCCとGNDを複数取るためにブレッドボードもかましている

写真3 うおー、起動した!!
(実際は2時間くらいハマっている。ので、起動成功したときはめっちゃ興奮した...)

コツ?としては、下記が必要だった

  • FlashROM側のA17とA16(使わないアドレスピン)はGNDに繋ぐ
  • FlashROM側の/WE(Write Enable)と/RESETはVCCに繋ぐ(無効にする)
  • FlashROM側の/OE(Output Enable)と/CE(Chip Enable)はGNDに繋ぐ(有効にする)

A17とA16(ファミコン側にはないが、今回用意したFlashROMには存在する)もちゃんとGNDに繋がないといけないのがミソ(LOWにするなら繋がなくても良いものかとテキトーにやってしまっていた)
とりあえずこれで、用意したFlashROMにデータを書き込んで、カセットに繋いで起動する、ということに成功した。やったー。

ファミコンROMはPRG-ROM(プログラムが入っているROM)と、CHR-ROM(キャラクターデータが入っているROM)とで分かれており、
今回はPRGROMの差し替えに成功したまで。
自作ゲームを実機で起動するには、CHR-ROMも同じ要領で差し替えが必要。
引き続きCHR-ROMの差し替えも進める。(CHR-ROMなしのゲームなら作れるかもですが)

一応ROMを書き込んだコードも貼っておきます(ただ、四人打ち麻雀の生ROMデータが含まれているので、そこは3バイトだけ残して消しておいている。適宜書き込みたいROMを3行目にあるROM[]に代入すればOK)

#include <avr/pgmspace.h>

const unsigned char ROM[] PROGMEM = { 0xD8 ,0x78 ,0xA9  };
  
/**
   Arduino - ROM

   A16: 22 - PLCC 2 (変換基板 1)
   A15: 23 - PLCC 3 (変換基板 2)
   A12: 24 - PLCC 4 (変換基板 3)
   A6 : 25 - PLCC 6 (変換基板 4)
   A7 : 26 - PLCC 5 (変換基板 5)
   A4 : 27 - PLCC 8 (変換基板 6)
   A5 : 28 - PLCC 7 (変換基板 7)
   A2 : 29 - PLCC 10 (変換基板 8)
   A3 : 30 - PLCC 9 (変換基板 9)
   A1 : 31 - PLCC 11 (変換基板 10)
   A0 : 32 - PLCC 12 (変換基板 11)
   A10: 33 - PLCC 23 (変換基板 23)
   A11: 34 - PLCC 25 (変換基板 24)
   A9 : 35 - PLCC 26 (変換基板 25)
   A8 : 36 - PLCC 27 (変換基板 26)
   A13 : 37 - PLCC 28 (変換基板 27)
   A14 : 38 - PLCC 29 (変換基板 28)
   A17 : 39 - PLCC 30 (変換基板 30)
*/
#define ADDRESS_0 32
#define ADDRESS_1 31
#define ADDRESS_2 29
#define ADDRESS_3 30
#define ADDRESS_4 27
#define ADDRESS_5 28
#define ADDRESS_6 25
#define ADDRESS_7 26
#define ADDRESS_8 36
#define ADDRESS_9 35
#define ADDRESS_10 33
#define ADDRESS_11 34
#define ADDRESS_12 24
#define ADDRESS_13 37
#define ADDRESS_14 38
#define ADDRESS_15 23
#define ADDRESS_16 22
#define ADDRESS_17 39

/**
   Arduino - ROM

   D0 : 40 - PLCC 13 (変換基板 12)
   D2 : 41 - PLCC 15 (変換基板 13)
   D1 : 42 - PLCC 14 (変換基板 14)
   D3 : 43 - PLCC 17 (変換基板 15)
   D4 : 44 - PLCC 18 (変換基板 17)
   D5 : 45 - PLCC 19 (変換基板 18)
   D6 : 46 - PLCC 20 (変換基板 19)
   D7 : 47 - PLCC 21 (変換基板 21)
*/
#define DATA_0 40
#define DATA_1 42
#define DATA_2 41
#define DATA_3 43
#define DATA_4 44
#define DATA_5 45
#define DATA_6 46
#define DATA_7 47

/**
   Arduino - ROM

   CE   : 48 - PLCC 22 (変換基板 20)
   OE   : 49 - PLCC 24 (変換基板 22)
   WE   : 50 - PLCC 31 (変換基板 29)
   RESET: 51 - PLCC 1  (変換基板 31)
*/
#define RESET 51
#define CE 48
#define OE 49
#define WE 50

/**
   Arduino - ROM

   VCC: 5V  - PLCC 32 (変換基板 32)
   VSS: GND - PLCC 16 (変換基板 16)
*/

#define VCC 3
#define GND 4

void setup() {
  pinMode(RESET, OUTPUT);
  pinMode(CE, OUTPUT);
  pinMode(OE, OUTPUT);
  pinMode(WE, OUTPUT);

  pinMode(VCC, OUTPUT);
  pinMode(GND, OUTPUT);

  pinMode(ADDRESS_0, OUTPUT);
  pinMode(ADDRESS_1, OUTPUT);
  pinMode(ADDRESS_2, OUTPUT);
  pinMode(ADDRESS_3, OUTPUT);
  pinMode(ADDRESS_4, OUTPUT);
  pinMode(ADDRESS_5, OUTPUT);
  pinMode(ADDRESS_6, OUTPUT);
  pinMode(ADDRESS_7, OUTPUT);
  pinMode(ADDRESS_8, OUTPUT);
  pinMode(ADDRESS_9, OUTPUT);
  pinMode(ADDRESS_10, OUTPUT);
  pinMode(ADDRESS_11, OUTPUT);
  pinMode(ADDRESS_12, OUTPUT);
  pinMode(ADDRESS_13, OUTPUT);
  pinMode(ADDRESS_14, OUTPUT);
  pinMode(ADDRESS_15, OUTPUT);
  pinMode(ADDRESS_16, OUTPUT);
  pinMode(ADDRESS_17, OUTPUT);

  setDataPinMode(OUTPUT);

  digitalWrite(RESET, HIGH);
  digitalWrite(WE, HIGH);
  digitalWrite(OE, HIGH);
  digitalWrite(CE, LOW);

  digitalWrite(GND, LOW);
  digitalWrite(VCC, HIGH);

  Serial.begin(9600);

  reset();
  erase();
  reset();
  
  unsigned long baseAddress = 0xC000;  

//  Serial.println(sizeof(ROM));

  for (unsigned long i = 0; i < sizeof(ROM); i++) {
    unsigned char data = pgm_read_byte(&ROM[i]);
    writeByte(baseAddress + i, data);
  }

  for (unsigned long i = 0; i < 0xFFFF /* 16384 */; i++) {
    if ((i % 16) == 0) {
      Serial.println("");
    }

    unsigned char ch = readByte(i + baseAddress);
    char dataString[50] = {0};
    sprintf(dataString, "%02X", ch);
    Serial.print(dataString);
    Serial.print(" ");
  }
}

void loop() {
}

/**
   バイト配列を書き込む

   @address このアドレスから指定size分書き込む
   @bytes 書き込むデータ
   @size データの長さ
*/
void writeBytes(unsigned long address, unsigned char* bytes, int size) {
  Serial.print("Size: ");
  Serial.println(size);

  for (int i = 0; i <= size; i++) {
    writeByte(address + i, bytes[i]);
  }
}

/**
   データピンのモードをINPUT/OUTPUTに変更する
*/
void setDataPinMode(int mode) {
  if (mode == OUTPUT) {
    pinMode(DATA_0, OUTPUT);
    pinMode(DATA_1, OUTPUT);
    pinMode(DATA_2, OUTPUT);
    pinMode(DATA_3, OUTPUT);
    pinMode(DATA_4, OUTPUT);
    pinMode(DATA_5, OUTPUT);
    pinMode(DATA_6, OUTPUT);
    pinMode(DATA_7, OUTPUT);
  } else {
    pinMode(DATA_0, INPUT_PULLUP);
    pinMode(DATA_1, INPUT_PULLUP);
    pinMode(DATA_2, INPUT_PULLUP);
    pinMode(DATA_3, INPUT_PULLUP);
    pinMode(DATA_4, INPUT_PULLUP);
    pinMode(DATA_5, INPUT_PULLUP);
    pinMode(DATA_6, INPUT_PULLUP);
    pinMode(DATA_7, INPUT_PULLUP);
  }
}

/**
   アドレスをセットする
*/
void setAddress(unsigned long address) {
  digitalWrite(ADDRESS_0,  ((address & 0x000001) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_1,  ((address & 0x000002) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_2,  ((address & 0x000004) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_3,  ((address & 0x000008) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_4,  ((address & 0x000010) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_5,  ((address & 0x000020) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_6,  ((address & 0x000040) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_7,  ((address & 0x000080) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_8,  ((address & 0x000100) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_9,  ((address & 0x000200) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_10, ((address & 0x000400) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_11, ((address & 0x000800) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_12, ((address & 0x001000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_13, ((address & 0x002000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_14, ((address & 0x004000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_15, ((address & 0x008000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_16, ((address & 0x010000) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_17, ((address & 0x020000) == 0) ? LOW : HIGH);
}

void commandFlash(unsigned long address, unsigned char data) {
  digitalWrite(WE, HIGH);
  digitalWrite(OE, HIGH);
  setDataPinMode(OUTPUT);

  setAddress(address);

  digitalWrite(WE, LOW);

  digitalWrite(DATA_0, ((data & 0x01) == 0) ? LOW : HIGH);
  digitalWrite(DATA_1, ((data & 0x02) == 0) ? LOW : HIGH);
  digitalWrite(DATA_2, ((data & 0x04) == 0) ? LOW : HIGH);
  digitalWrite(DATA_3, ((data & 0x08) == 0) ? LOW : HIGH);
  digitalWrite(DATA_4, ((data & 0x10) == 0) ? LOW : HIGH);
  digitalWrite(DATA_5, ((data & 0x20) == 0) ? LOW : HIGH);
  digitalWrite(DATA_6, ((data & 0x40) == 0) ? LOW : HIGH);
  digitalWrite(DATA_7, ((data & 0x80) == 0) ? LOW : HIGH);

  // data latching occurs 'rising edge'
  digitalWrite(WE, HIGH);
}

/**
   状態をリセって
*/
void reset() {
  digitalWrite(RESET, LOW);
  delayMicroseconds(10);

  digitalWrite(RESET, HIGH);
  delayMicroseconds(10);

  digitalWrite(WE, HIGH);
  digitalWrite(OE, HIGH);
  digitalWrite(CE, LOW);
}

/**
   1バイト書き込み
*/
void writeByte(unsigned long address, unsigned char data) {
//  Serial.print("Write ");
//  Serial.print(data, HEX);
//  Serial.print(" to ");
//  Serial.println(address, HEX);
//  if (data == 0xff) {
//    return;
//  }

  commandFlash(0x5555, 0xAA);
  commandFlash(0x2AAA, 0x55);
  commandFlash(0x5555, 0xA0);
  commandFlash(address, data);

  waitForData(data);
}

/**
   1バイト読み込み
*/
unsigned char readByte(unsigned long address) {
  unsigned char data = 0;

  setAddress(address);

  digitalWrite(CE, LOW);
  digitalWrite(OE, LOW);

  setDataPinMode(INPUT);

  data |= digitalRead(DATA_0) == HIGH ? 0x01 : 0;
  data |= digitalRead(DATA_1) == HIGH ? 0x02 : 0;
  data |= digitalRead(DATA_2) == HIGH ? 0x04 : 0;
  data |= digitalRead(DATA_3) == HIGH ? 0x08 : 0;
  data |= digitalRead(DATA_4) == HIGH ? 0x10 : 0;
  data |= digitalRead(DATA_5) == HIGH ? 0x20 : 0;
  data |= digitalRead(DATA_6) == HIGH ? 0x40 : 0;
  data |= digitalRead(DATA_7) == HIGH ? 0x80 : 0;

  digitalWrite(OE, HIGH);

  return data;
}

/**
   データ書き込まれるまで待機
*/
void waitForData(unsigned char value) {
  unsigned char data = 0;

  setDataPinMode(INPUT);

  while (true) {
    digitalWrite(OE, LOW);
    data  = digitalRead(DATA_0) == HIGH ? 0x01 : 0;
    data |= digitalRead(DATA_1) == HIGH ? 0x02 : 0;
    data |= digitalRead(DATA_2) == HIGH ? 0x04 : 0;
    data |= digitalRead(DATA_3) == HIGH ? 0x08 : 0;
    data |= digitalRead(DATA_4) == HIGH ? 0x10 : 0;
    data |= digitalRead(DATA_5) == HIGH ? 0x20 : 0;
    data |= digitalRead(DATA_6) == HIGH ? 0x40 : 0;
    data |= digitalRead(DATA_7) == HIGH ? 0x80 : 0;
    digitalWrite(OE, HIGH);
    
    if (data == value) {
      return;
    }

    delay(10);
  }
}

/**
 * 全消去
 */
void erase() {  
  commandFlash(0x5555, 0xAA);
  commandFlash(0x2AAA, 0x55);
  commandFlash(0x5555, 0x80);
  commandFlash(0x5555, 0xAA);
  commandFlash(0x2AAA, 0x55);
  commandFlash(0x5555, 0x10);

  waitForData(0xFF);

  digitalWrite(WE, HIGH);
  digitalWrite(CE, LOW);
}
balmychanbalmychan

今日はあまり進捗なし

  • 書き込みを楽にできるように、ユニバーサル基板で簡単にArduinoに挿せるようなものを作ろうとした→が、配線用の線が足りず断念(注文した)

あとは、一応下記記事を参考に、nesファイルのビルド環境は整えておいた。(普通はアセンブラで書くのだが、とりあえず最初はCで)
http://muto.world.coocan.jp/nesapp/nesapp-build.html
(画像は、エミュレーターで実行した様子)

PRG-ROMだけだったらこれをFlashROMに書き込んでみようと思ったが、CHR-ROMも必要なようだったので、ちょっと断念。
そしてCHR-ROMもFlashROM化するには部品が足りなかったので、PLCC-DIPの変換基板をaitendoで追加注文した。

今日これだけだとあれなので、CHR-ROMを外すだけしておいた(起動できることも確認)

balmychanbalmychan

絶縁ワイヤーの被覆を剥がすストリッパーと、それとはまた別にすずめっき線が届いた。

が、すずめっき線は絶縁ではないのでやっぱりうまく干渉せずつけれる自信もなく、絶縁ワイヤーでやるのはすでに失敗してるのでそちらも自信なく。
明日くらいに本命のポリウレタン線(エナメル線)が届くので、それが届いてからにする。
特に今日はあんまりやることないので休館

balmychanbalmychan

今日はCHR-ROMをFlashROMに載せ替える作業をやっている

が、全然うまくいかない...

こんな状態↓

本当はこうなってほしい↓

接続が悪いのか、書き込みが悪いのかいまいち判然としない状態。
CHR-ROM自体は8KBなのだが、FlashROM側には一応0x00000~0x10000まで8KBずつミラーして書き込みをした。なのでFlashROMのデータは問題ないはずと思われるが...
ちなみに元々載っていたPRG-ROMとCHR-ROMは同じ製品なので、ピン配置についても差がない。PRG-ROM載せ替えのときと同じ接続にしているのに、上手く行かないのが不可解。

ちなみに、0xFFで埋めたFlashROMで起動するとこんな感じ↓

なので、先程の画面は真っ黒にはならないあたり、一応なにか読み取ってはいるのかとは思われる。

書き込んでいるデータ(NESファイルからCHR-ROMの箇所から抜き出したもの)がおかしいのかと思い、実際のCHR-ROMからデータを確認してみた。
結構データが違うので、NESフォーマットだとCHR-ROMはなにか特殊な形になっているのかもしれない...(要調査)

そして、抜き取ったデータをFlashROMに書き込んで試してみたところ...

なんかおしい!!
うーむ、CHR-ROM手強い。
(ただ、きれいに表示されている箇所はあるはあるので、書き込むデータの問題だとは思われる)

今日はこのへんで

balmychanbalmychan

諦めきれずに少し調べていたら、こんなツイートが(自分が持っているCHR-ROMもRP2368なので同じ)

https://twitter.com/GOROman/status/839139885311127552

↑の画像の通り、通常のEPROMのピン配置は左だが、今回のCHR-ROMは右のようになっている。
データピンは同じだがアドレスピンの配置が違う。
ツイートの返信でもあるとおり、簡易的なハードウェアプロテクトとではとのこと。

なので、さっき実際のCHR-ROMから抜き出したデータは誤っていたと思われる(通常のEPROMのピン配置だと思って配線してデータを抜いたので)

で、一応NESファイルのCHR-ROMを再度書き出して、カセットと繋いでみたが、うまくはいかず...

(多分だけど)配線間違えた状態でデータを抜き出して、それをFlashROMに書いて、配線間違えた状態でカセットと繋いだから、半分くらい表示がうまくいったのかもしれない...?

また後日、実際のCHR-ROMのデータを再度抜き出して、書き出してみる。

balmychanbalmychan

今日はCHR-ROMの件は置いておいて、ポリウレタン線(エナメル線)が届いたので、小休止がてらFlashROM書き換えの時にArduinoへの差し替えが便利なようにそれようの基板を作っていた。


↑こんな感じで、90度に折れ曲がったピンヘッダとPLCCソケットをユニバーサル基板に取り付けて、エナメル線で繋いだもの

↑Arduinoにこんな感じで刺せるようにしている

↑何度か衝撃で線が外れてしまい苦戦したが、無事書き込めるようになった。(初心者すぎて、線をはんだ付けするのがあまりうまくいかず、すぐ外れそう...)

小休止と言いながらも、32本も付けるのはかなり苦労した...
今日はこのへんにしておきます。

balmychanbalmychan

今日はCHR-ROMの件の続き。
配線を見直してCHR-ROMから改めてデータを抜き出して、FlashROMに書き込んでみた。
が、やっぱり画面はバグったまま。。。もう打つ手がない。

で、ちょっとこの特殊なCHR-ROMとその基板だとこれ以上進展がないと見て、別のカセットを犠牲にすることにした。

何個か買っていた4人打ち麻雀のうち、CHR-ROMが特殊なピン配置ではないのを発見!これのCHR-ROMを載せ替えることにした。(FlashROMに書き込んだデータは、NESファイルのもの。)

写真1 基板のCHR-ROMの位置にFlashROMを改めて繋いで...

写真2 うおー、成功した

まさか、ハードウェアプロテクトのかかっている基板を引いてしまっていたせいだったとは...。早めに見限って別のカセットに手を付けてみてよかった

※ちなみにファミコンカセットは、同じカセットで裏側に記載された製造番号?も同じだったとしても、DIPのROMだったりプラスチックで封がされてて全く手が出せないROMだったりするので、こういったハードウェアプロテクト(っぽいもの)がかかったものについても、そうだったりそうでなかったりするのだろう...

ハードウェアプロテクト(っぽいもの)がかかっていた基板は↓で(HVC-RROM-05と記載がある)

かかってなかったほうは↓(HVC-NROM-03と記載がある)

今日はここまで

balmychanbalmychan

昨日の続き。新しいカセット基板の方のCHR-ROMの載せ替えは終わったので、今度はPRG-ROM側を載せ替えた

写真1 こんな感じで、元のROMからFlashROMに載せ替えた

写真2 FlashROM版でも無事起動!

これで、カセット側の用意が整ったので、以前作成したテストプログラムを書き込んで、実機で起動するか確認する

balmychanbalmychan

その後、書き込みをしようかと思ったものの、cc65コンパイラが吐き出すnesファイルのPRG-ROMの中身を見てみると、32KBになっていた。これだとそのまま書き出してもうまくいかないだろうなーという感じ...
というのも今回のカセット基板の元となる4人打ち麻雀は16KBしか使っておらず、それ故A14はファミコン本体からカセットまで結線されていない。(つまり、16KBまでのアドレスしか指定できない。A0~A13の14ビットあれば16KB分のアドレス指定はできるので。)

下記のいずれかでできないかなーと模索中

  • cc65が16KB想定の吐き出しができないか
  • A14を無理やり結線できないか(基板の表面削って繋いじゃえばいけそうではある)

前者の方法について

Hello World的なプログラムの場合はほとんど容量を使っていないので、ほぼ0x00で埋まっている。が、32KB中最後の6バイトに 35 80 00 80 5C 80 という値が入っている。これだけのために32KB分必要になっている(というか、これはPRG-ROMの末尾に埋め込むなにかの命令なのかとは思うが)

後者の方法について

ファミコンとカセットを繋ぐ端子は60ある
(ファミコンが上手く起動しないときにフーフーすると思うが、そこのこと。片面30で、両面で60ある)
これがそれぞれPRG-ROMやCHR-ROMに繋がっている。以下の感じ

A14を使う(=32KB分のアドレス空間を使う)には、ファミコン側の35番とPRG-ROM側の27番を繋ぐ必要があるが、4人打ち麻雀は元々16KBしかないため、32KB分のアドレス空間は必要なく、ここが繋がるような基板になっていないということ。(実際見てみると、本当に、全く何にも繋がっていない。端子があるだけ、という感じ)

これを無理やり繋げばいけるかもしれない。という方法

balmychanbalmychan

cc65が16KB想定の吐き出しができないか

こっちの方向性にしつつ、どうせCで書いてもいつか頭打ちになりそうなので、アセンブリで書くことにした。
(nesasmというアセンブラを使うと、アセンブリファイルからnesファイルを吐き出すことができる)
といっても

ここが超参考になる&サンプルプログラム(asmファイル)を置いてくれているので、簡単にできる。

で、アセンブリを書く際に、どのアドレスからプログラムを配置するかを指定できるので、ここを$C000から配置することで、16KBのサイズのPRG-ROMのNESファイルが出来上がった。

※下の画像の青丸の部分を変えた(左上に写っているのが、エミュレーターで実行した画面。ギコ猫が表示されるだけのプログラム)

アセンブリコードも貼っておく↓(ギコ猫サイトのサンプルのgiko005そのまんまだが。↑画像の青丸のとこだけ変えている)

	; スプライト表示サンプル

	; INESヘッダー
	.inesprg 1 ;   - プログラムにいくつのバンクを使うか。今は1つ。
	.ineschr 1 ;   - グラフィックデータにいくつのバンクを使うか。今は1つ。
	.inesmir 0 ;   - 水平ミラーリング
	.inesmap 0 ;   - マッパー。0番にする。

	.bank 1      ; バンク1
	.org $FFFA   ; $FFFAから開始

	.dw 0        ; VBlank割り込み
	.dw Start    ; リセット割り込み。起動時とリセットでStartに飛ぶ
	.dw 0        ; ハードウェア割り込みとソフトウェア割り込みによって発生

	.bank 0			; バンク0
	.org $C000  ; $C000から開始

	; ここからプログラムコード開始
	
Start:  
	lda $2002  ; VBlankが発生すると、$2002の7ビット目が1になる
	bpl Start  ; bit7が0の間は、Startラベルの位置に飛んでループして待つ

	; PPUコントロールレジスタ初期化
	lda #%00001000 
	sta $2000
	lda #%00000110		; 初期化中はスプライトとBGを表示OFFにする
	sta $2001

	ldx #$00    ; Xレジスタクリア

	; VRAMアドレスレジスタの$2006に、パレットのロード先のアドレス$3F00を指定する。
	lda #$3F
	sta $2006
	lda #$00
	sta $2006

loadPal: ; ラベルは、「ラベル名+:」の形式で記述
	lda tilepal, x ; Aに(ourpal + x)番地のパレットをロードする

	sta $2007 ; $2007にパレットの値を読み込む

	inx ; Xレジスタに値を1加算している

	cpx #32 ; Xを32(10進数。BGとスプライトのパレットの総数)と比較して同じかどうか比較している	
	bne loadPal ;	上が等しくない場合は、loadpalラベルの位置にジャンプする
	; Xが32ならパレットロード終了

	; スプライト描画
	lda #$00   ; $00(スプライトRAMのアドレスは8ビット長)をAにロード
	sta $2003  ; AのスプライトRAMのアドレスをストア

	lda #50     ; 50(10進数)をAにロード
	sta $2004   ; Y座標をレジスタにストアする
	lda #00     ; 0(10進数)をAにロード
	sta $2004   ; 0をストアして0番のスプライトを指定する
	sta $2004   ; 反転や優先順位は操作しないので、再度$00をストアする
	lda #20		;	20(10進数)をAにロード
	sta $2004   ; X座標をレジスタにストアする

	; PPUコントロールレジスタ2初期化
	lda #%00011110	; スプライトとBGの表示をONにする
	sta $2001

infinityLoop:
	jmp infinityLoop	; 今回は描画して終わりなので無限ループで良い

tilepal: .incbin "giko.pal" ; パレットをincludeする

	.bank 2       ; バンク2
	.org $0000    ; $0000から開始

	.incbin "giko.bkg"  ; 背景データのバイナリィファイルをincludeする
	.incbin "giko.spr"  ; スプライトデータのバイナリィファイルをincludeする

このギコネコのサンプルgiko005のコードについては

で丁寧に解説してくれているので、すごい助かります

で、出来上がった16KBのPRG-ROMと8KBのCHR-ROMをNESファイルから抜き出して、FlashROMに書き込んでみました

すると...

成功!(なんかエミュレータで実行したときと違ってギコ猫が増殖しているけど笑)

ちょっと変な表示になってしまっているけど、このあたりはおいおいソフト側作りながら理解していこうと思います。

ということで、ここからしばらくは、アセンブリのプログラミングを粛々とやっていくことになりそう

ハードウェア側も

  • 簡単にFlashROMを載せ替えられるように少し改良する(今はジャンパワイヤーが邪魔して容易にFlashROMの入れ替えができない...。PLCCソケットとピンソケットを互いに逆向きにつければよかったと激しく後悔)
  • 自作カセットを容易に量産できるように、プリント基板を設計してみる&発注してみる

をやっていこうと思ってます

今日はここまで

balmychanbalmychan

「プリント基板を設計して発注する」を進めている。

とりあえずKiCadとAutodesk Eagleをどっちが良いか触ってみている。しかし、このあたりのツールは使ったことがなく、全然わからない。まあでもなんとか触ってみてる感じ。

だいたい大筋の流れとしては

  • 回路図の作成
  • 回路が問題なければ、それをどう基板に落とし込んでいくか、基板の設計

という感じかなー
ツールの使い方もしかり、回路自体への知識・経験不足もあり、結構習得するのに時間かかりそうな予感
YouTube動画や記事を参考に学習を進めている。

ゲーム開発の方は、ちょっとあんまり食指が伸びずしばらく放置になりそう
(ゲーム作りたいわけではなく自作ROM作りたいだけだったというのもあり...)

balmychanbalmychan

KiCadを使うことにした(OSSだし無料だし)
少し慣れてきて、前にユニバーサル基板で作ったFlashROM書き換え機用の基板を作ってみている

こんな感じ↓ (まだ穴だけで配線できてないですが...)

改めて流れは

  • 回路図の作成
  • 回路図に配置したシンボル(≒回路記号)にフットプリントを紐付ける(フットプリント≒基板にどういった形状の穴やら銅箔やらを付けるかを示すもの)
  • 基板設計に配置
  • 基板設計上で配線

という感じだと思われる。まずはこの基板を完成させて、発注してみようと思う

balmychanbalmychan

今日も引き続きKiCadで基板設計

https://qiita.com/nanbuwks/items/38e313614c537e9c04a0

この記事を参考にしつつ、PLCC32のソケットのシンボルを作ったり、回路図書いたり、基板に落とし込んだりなど。

で、いざ基板エディタで、配線をしようと思ったところ...

配線がかなり難しい!!!もはや2本で詰んでしまった。

配線はぶつかってはいけない。裏表で2層あるのだが、それでも32本をきれいに配線するのは正直無理ゲー。

どうにかならないかと検索していたら
https://garchiving.com/auto-routing-with-kicad/

の記事を発見。

https://github.com/freerouting/freerouting
↑のfreeroutingというツールを使うと、良い具合にしてくれるらしい。

Java製で、しかしなぜか自前でビルドしなければならないようなので、下記な感じでビルドした

  • Java11のJDKを入れる https://www.oracle.com/java/technologies/javase-jdk11-downloads.html ※Java14だとビルド失敗したので注意
  • ※JAVA_HOMEやPathはシステム環境変数で設定しましょう
  • githubのREADMEに従って gradlew assembleコマンドを実行してみる。するとビルドされる
  • build/libs/freerouting-executable.jarを実行する

で、こんな感じで自動配線を試してみる↓

https://www.dropbox.com/s/8w8goc3a6p6wpcs/00.gif?dl=0

やばっ...なにこれ...超便利...
(一度も配線をせず諦めた奴が言うのもなんですが)

で、出来上がったので、KiCad テキスト はじめて編(Ver.5対応) - Qiita に従って、ガーバーデータなるもの(良くわからないが、PCB=プリント基板(正確には部品がついていないものはプリント配線板、と呼ぶらしいが)を発注する際に使うフォーマットなのだろう)を吐き出します

で、どこに発注かけようかなと思ったんですが

以前 https://kohacraft.com/archives/201907280934.html の記事を見かけたことがあったので、JLCPCBというところに注文することにしました。

JLCPCBのサイトでガーバーデータを上げて、ごにょごにょしたら

無事発注できました。

お値段はこんな感じ(基板自体は5枚で$2。安い。送料は$14でした)

ということで、本当にできているのか不安になりつつも、到着を待とうと思います...

こんなのが届くはずの予定

今日はここまで

balmychanbalmychan

時間取れず進捗はなし

ただ、JLCPCBからリジェクトされていた!理由は↓

Hi Sir/ Madam, there is no board outline in your file, please kindly check it!You can click 'Replace file' button to re-upload the file in your JLCPCB account page .Thanks.

アウトラインが無いよ、とのことで、書き出すときになにか失敗している様子...
下記が参考になりそう。

balmychanbalmychan

前回のJLCPCBのリジェクトの対応をしていた。

おそらく基板の外枠の線を「Edge.Cuts」で指定しなかったのが原因と思われる。

Edge.Cutsで引き直して、再度提出した。こちらはまた判定待ち...

また、それとは別で、いままでファミコン本体は「ファミつく」という基板むき出しの互換機を使っていた
↓これ

https://www.amazon.co.jp/dp/B01B619UKM

ただ、ちょっとこれのコントローラーが壊れて?全く効かなくなっていたので、改めて買い直した。

流石にいまどきRFスイッチで繋ぐ訳にもいかないので、コンポジット端子に対応してる改造ファミコンをメルカリで購入することにした。

https://www.mercari.com/jp/items/m88148830096/

これ。5800円もしたが、ヤフオクでもだいたいコンポジット端子対応やら縦縞対策をしたファミコンはこのくらいの相場...。ちなみにUSBから電源取れるオプションも付けてもらった。

で、届いたものの、自前のFlashROMを繋いだ状態だとさせない...(当然ですが...)

カバーを外してむき出しにすればよいのだが、上側のカバーに基板はくっついているので、なんだか外すのもなぁという感じ(むき出しで使っているとふと壊れそうな感じが)

balmychanbalmychan

カセット基板にFlashROMをくっつけるための基板を設計中
(本当は先のJCLPCBに頼んでいる基板が届いてから着手したいが、他にやることもないので)

ここの赤丸のところにFlashROMをくっつけられるようにする(今はジャンパワイヤーを使って繋いでいるが、それもきついので)

で、はじめは、CHR-ROMとPRG-ROMそれぞれでくっつけるイメージで作っていたが

それだと、明らかに縦の幅が足りなかった↓(FlashROMがソケットも付けるとそこそこの縦幅になるので、収まらない)

で、先日のスーファミの参考サイトの基板を参考にして、CHR-ROMとPRG-ROMを同時に搭載可能にしつつ、既存のカセット基板に今回作るFlashROMの基板を重ねてくっつけることにした。こんなもの↓(イメージビューだとピンヘッダついてますが、ここは実際はなにもついていないピンホールです)

ファミコンに差し込んだ時のイメージとしてはこんな感じになる予定↓

FlashROM部は差し込み口の上に出てないと干渉してしまうので、これが良さげではある
ここまでやるなら、ファミコンに差し込む部分の端子まで同一基板で作っても良い気もするが、
急にステップを飛ばすと何か問題があったときに切り分けしづらいので、一旦は既存のカセット基板に、FlashROMを搭載できるような基板にしておく。
(これが成功したら、今度はファミコンに差し込む下部の方の基板を設計すれば良くなるので、段階的に作ることもできる)

ちょっと設計のサイズ感がうまくいっているか心配なので、もう少し寝かせて、後日確認する。
(ここが既存のカセット基板とぴったり合わないと意味ないので、慎重にいきたい...)
あと、正確に既存のカセット基板のサイズを測るために、デジタルノギスをポチった

今日は眠いのでここまで

balmychanbalmychan

今日も昨日の続き

CHR-ROMとPRG-ROMを同時に搭載可能にしつつ

同時搭載しなくても、単にそれぞれ上に伸ばすようにくっつければいいだけだったので、そんな感じにした。
(CHR-ROMとPRG-ROMの間隔をミスると困るので)
縦の間隔は、28ピンのICをユニバーサル基板に付けたときに間に5個ランドがあったのと、ユニバーサル基板が2.54mmピッチなので、
KiCadのグリッドを2.54mmにして、5個間隔開けて置いてみた。多分ダイジョブだろう(ホントかな...)
あと、以前ジャンパワイヤーで繋いで起動成功した時の経験を踏まえて

  • PLCC側のA16とA17はGNDと繋ぎ
  • PLCC側の/RESETと/WEはVCCに繋ぐ

とした。これで動くはず...

ということで↑でまたJLCPCBに注文してみた。
(本当は同時に色々注文したほうが送料の問題から良いんでしょうが、まだ不慣れなんでまぁ良いかなと...早く欲しいし)

balmychanbalmychan

やばい、ソフト側(ゲーム自体)を作る気が全然起きない

balmychanbalmychan

ちなみに全然関係ない話ですが

ファミコン側のコンポジット端子をHDMIに変換してディスプレイに映すには、アップスキャンコンバーターというのが必要なんですが、どれも結構値段が張る(だいたい5000円くらい。マルツの店頭で売っていたやつも7000円くらいしてやめた)

Amazonで探して見つけた、これがおすすめ(1389円と安いし、ちゃんと動いたし)

https://www.amazon.co.jp/gp/product/B07QPSQVMM

balmychanbalmychan

今日もソフトウェア側の開発を先延ばしにするべくKiCadをいじっていた(最近はずっとこればかり)
いよいよカセット基板自体を作ってみることにした。(まだ前に作ったやつ届いてないが...笑)

以前、ファミコンカセットの端子部分は、片面30、両面で60端子あることを書きました。下記の画像を見てみて下さい。


左の画像が表面で、左から1~30の端子がついており、右の画像が裏面で、右から31~60の端子がついています。うっすらと基板上に数字が書かれているのが分かるかと思います。(ちなみに実はICが付いているほうが裏面です)

で、それぞれの端子がPRG-ROMやCHR-ROMにどう繋がっているかというと

こんな感じです。

ROMは28ピンあり、PRG-ROMとCHR-ROM合わせて56ピンです。
残りの4ピンがなんなのかは今はよくわかりません。しかも上の図を見ても、よくわからん端子が4つ以上ある笑。

それについて調べるのはまた後日として、
KiCadで基板を作るときに、この60の端子をどんな感じで作ればいいのか悩ましかったので、先にそっちに取り組みました

既存のライブラリにも、インターネット上にもファミコンに刺すのに使えそうなフットプリントは落ちてなかったので、自分でフットプリントを作ってみることにしました。

このへんの記事が参考になりました↓(特に両面フットプリントについて)

フットプリントを作る場合、パッドを置いていくわけですが、普通は穴の空いたスルーホールを選択します。が、ファミコンのような端子を作る場合、パッド形状を「コネクタ」にするのがミソです。↓

そしてこんな感じでちまちまと置いておく...↓「 裏返し」を選択すると裏に配置されます。
(端子の間隔は2.54mm、端子自体のサイズは、実際のカセットの端子を測って、幅1.6mm、高さ8.4mmにしました)

https://www.dropbox.com/s/fnufkvu9sqd8g6z/00 (2).gif?dl=0

で、一旦基板にフットプリントを置いてみました。

こんな感じ↓

(表面にFlashROMがあったほうが見栄えかっこいいので、表にしようかと思っている)

今日はここまで。

それぞれの端子がどうPRG-ROMやCHR-ROMに繋がっているか

については、実際にファミコンの基板の配線を見ながら、地道に調べようと思います。
外側のプラスチックのカセットの外殻も自作したいな...(どうやればいいのかよく分かってないですが)

balmychanbalmychan

今日は、カセットの端子とPRG-ROM、CHR-ROMをどう配線するかについて、実際のカセットの基板で確認しながら調べた。

結果は下記の画像

ポイントを書くと

  • PRG-ROMとCHR-ROMのアドレスバスとデータバスは単に対応する端子に繋がっている
  • 45 SOUND IN46 SOUND OUTは直結(「ファミコン カートリッジコネクタ仕様」にもその旨が書いてある)
  • 48 VRAM ~CE49 PPU A13も直結(ここを直結する理由は良くわからないが、CHR-ROMは8KBでA13は常にLOWなので、それを使ってVRAMのCEをLOWにしているのか?)
  • 35 CPU A14は何にも繋がっていない(これは、四人打ち麻雀が16KBしか使ってないからとは思われる)
  • 14 CPU R/~W15 ~IRQ32 φ247 PPU ~WRは何にも繋がっていない
  • 56 PPU A13 はCHR-ROMの20 /CE と繋がっている(CHR-ROMは8KBしかないので、おそらく常に- A13にはLOWが流れ、/CEは有効になるのかと思われる)
  • 18 VRAM ~A10 は、54 PPU A11 と繋がっている(これについては後述)

という感じ

一番難解だったのは

18 VRAM ~A10 は、54 PPU A11 と繋がっている(これについては後述)

のところ。
これについては「ファミコンカートリッジ自作計画」に詳しい説明が載っている。
抜粋すると

VRAM-A10は画面出力時のアドレス読み替えで、ミラーリングの選択に使用。 54:CHR-A11に接続すると水平ミラーリング(垂直接続)、 53:CHR-A10に接続すると垂直ミラーリング(水平接続)になる。

とのことだった。
ファミコンの背景(BG)は「NES研究室 グラフィック」にもある通り、4画面並べることができるが、しかし、実際はVRAMの容量的には2画面分しかなく、2画面をミラーした形になる。そして、そのミラーの方向を水平にミラーするか垂直にミラーするかを選ぶことができる。
どちらで制御したいかによって、A11に繋いだりA10に繋いだりするようだ。
(ここがプログラムで制御ではなく結線で制御することに驚き)

ちなみに、なぜA11に繋いだら垂直になるかというと、多分だが
画面1は、VRAM(PPU)のアドレスが$2000~$23FFにマッピングされており、画面2は$2400~$27FFにマッピングされているとのこと。
で、垂直ミラーをする場合は画面1と画面3で同一アドレスを参照するようにする必要がある。
つまり、垂直ミラーの場合はVRAMの$2000と$2800が同じアドレスを指すようにする必要があり、
それぞれ2進数にすると10_0000_0000_000010_1000_0000_0000 で、ちょうどA11の位置が0と1とで差があるのが分かる。
ちょっと詳細は判然としないのだが、この54 PPU A11 と、18 VRAM ~A10 を繋ぐと、何らかの作用でA11がLOWとなって、同一のアドレスを指すように読み替えられるのかも?
一方、水平ミラーの場合画面1と画面2を同一アドレスを参照する必要がある。($2000と$2400を同じアドレスにする)
それぞれ2進数にすると10_0000_0000_0000 と10_0100_0000_0000 となり、ちょうどA10の位置が0と1とで差があるのが分かる。
こちらは53 PPU A10 と18 VRAM ~A10 と繋いだ場合は、A10が否定の形になって、これまた何らかの作用でA10がLOWとなって、同一のアドレスを指すように読み替えられるのかも?
自分で言ってても釈然とせずよく分からない説明なんですが...
(なんとなくアドレスの読み替えが行われているということは分かるのですが、具体的にファミコンがどう動作してこの18 VRAM ~A10のHIGHとLOWが制御されているかが調べても良くわかりませんでした)
まぁとにかくアドレスの読み替えが行われるのだろうということで。

水平/垂直ミラーリングに関する結線については、下記サイトも参考になりました。

まぁなんだか変な説明になってしまったものの、__水平/垂直ミラーリングが基板の結線によって決まる__ということは全然知らなかったので、良い収穫だった。

このことは

  • もし縦スクロール系のシューティングゲームを作りたい場合は水平ミラーリングを選択するほうがより自然な表現ができる
  • マリオのような横スクロール系のゲームを作りたい場合は垂直ミラーリングを選択するほうがより自然な表現ができる

ということを示します。

一応ミラーではあるものの4画面はあるので、上下左右にスクロールはできるが、垂直ミラーの場合に例えば下にスクロールすると、若干書き換え前の上の部分が見えてしまうということかと思う。

balmychanbalmychan

JLCPCBに最初に注文した基板が届いたので、ハンダ付けしていた。
(届いたといっても、DHLの袋がうちの近くの道路に落ちていて、超親切な人が届けてくれた...最初はDHLに捨てられたのかと思ったけど、流石に道路に落ちてたと言ってたから落としたのかな...)

届いた基板はこんな感じ

PLCCのソケットは無事くっついたものの、ピンソケットを付ける側の穴が小さく、貫通しなかった...しかたなくハンダで無理やりくっつけた。

※スルーホールの大きさについてはこれから気をつける

で、Arduinoに刺すとこんな感じ

無事、以前の書き込みプログラムで動きました

以前のユニバーサル基板で作ったものよりも、線がむき出しではないのでちょっと安心感?があります。

↓以前のはこれ
https://zenn.dev/link/comments/aa63f54ca48a6e

とりあえず初めてのPCBの発注は親切な方の存在もあって、無事成功に終わりました!
次に届く予定のカセットにくっつける用の基板が道路に落とされないことを祈ります...

balmychanbalmychan

(届いたといっても、DHLの袋がうちの近くの大きな道路に落ちていて、超親切な人が届けてくれた...最初は住所が分からなくてDHLに捨てられたのかと思ったけど、流石に道路に落ちてたと言ってたから落としたのかな...)

先程佐川がものすごい申し訳無さそうに謝りに来た
(多分どこかのタイミングで佐川に委譲しているのかと思われる)
事情を言ったら安堵の表情で去っていった。
(なんかめっちゃ探したんだろうなと逆に申し訳なくなったが、佐川に委譲していることも連絡先も分からなかったので...)

balmychanbalmychan

カセット基板の方の回路図の作成と、配線を行ってた


こんな感じで一旦完成

ということで、基板が届くまでは一旦ハード側でやりたいことはやり終わったので、そろそろソフトウェア側開発に入ろうかなと思います。

※といってもゲーム開発というよりは、BGの表示とか、スプライトの表示とか、コントローラーによる制御とか、そういったのを少しずつ試していこうと思います。

balmychanbalmychan

今日は下記について調べていた

  • パレット、BG、スプライトの表示
  • CPU、PPUのメモリ空間
  • I/Oレジスタ

それぞれについて軽く説明すると

パレット
絵の具のパレットと同じです。ファミコンは同時に発色できるのはBGで16色、スプライト側で16色となっています。その16色を束ねた色群を、パレットと呼びます

BG
背景のこと

スプライト
キャラクタを描画する仕組みのこと。だいたいゲームやゲーム専用機にはBGとは別にスプライト機能というのをハードウェア的に備えており、BGで背景を表示し、スプライトでキャラクタを表示して、それを重ねて表示します。

上記のパレット情報や、BG、スプライト、ドット作成などは、「YY-CHR」というエディタを使うのが良さそうです。
(だいたいゲームづくりって、こういうエディタづくりから始ります。僕も昔PC-9801やX68000向けにゲームっぽいものを作ってたときは、エディタから作りました。こういうエディタが落ちてるのは大変ありがたい)

以下、ずらずらと抑えるべきところを箇条書き

  • CPUと描画専用のPPU(Picture Processing Unit)がある
  • CPUはご存知の通りプログラム(命令)を実行するプロセッサで、ファミコンには6502が使われている。(AppleⅡと一緒だとか)6502向けのアセンブリコードを書いて、実行コードを生成し、それをCPUが実行することになる
  • PPUは、グラフィック関連の処理を行うプロセッサで、CPUとは別のメモリ空間(VRAM)を持ち、VRAMの状態をもとに画面の描画を行うといった感じ
  • CPUからアクセスできるメモリ空間と、PPUからアクセスするメモリ空間(VRAM)は別空間になっており、CPUからVRAMを直接書き換えることはできない。CPU側のメモリ空間にマップされているI/Oレジスタを経由してVRAMにアクセスする
  • I/Oレジスタはサウンドのユニットに音を鳴らさせたり、コントローラーから入力状態を受け取ったりなど、他のユニットとのやりとりで、いたるところで活躍する
  • CPU側のメモリ空間のマップは「Famicom開発室 2-3. メモリマップ」を参照
  • I/Oレジスタに関する説明は「Famicom開発室 6. I/Oレジスタ」を参照
  • PPU側のメモリ空間(VRAM)のマップは「Famicom開発室 3-1. メモリマップ」を参照
  • スプライト情報は専用のRAMに格納されており、書き込みについても、I/Oレジスタである$2003と
  • $2004を使う。
  • 1スプライトにつき4バイトあり、合計64個分、合計256バイトの領域となっている
  • スプライト情報の4バイトの内訳は以下
1バイト目 Y座標
2バイト目 タイルインデクス番号
3バイト目
  bit7:垂直反転(1で反転)
  bit6:水平反転(1で反転)
  bit5:BGとの優先順位(0:手前、1:奥)
  bit0-1:パレットの上位2bit
  (他のビットは0に)
4バイト目 X座標
  • $2003に更新するスプライトのアドレスを指定し、その後$2004に合計4回の書き込みを行うことで、スプライトの情報を更新する
  • スプライトの表示順は、0が最も手前で、63が最も奥

今日はこのへんで力尽きます

balmychanbalmychan

ファミコンプログラミングを超簡単に要すると

I/Oレジスタ経由でコントローラーの状態を受け取って

  • CPUで、6502の命令群(とレジスタ)を駆使して色々計算して(RAM領域やROM領域へのアクセスもしつつ)
  • PPUのVRAMにパレットの情報やBGの情報やキャラクターのスプライトの情報を書き込んで画面表示して
  • 必要ならI/Oレジスタ経由でサウンドも鳴らして
  • それをひたすら繰り返す(ファミコンは60fpsなので、1サイクル16ミリ秒)

です。

今日はこのへんで力尽きます

balmychanbalmychan

なにか適当なグラフィックでBGを埋めてみようと思ったが、苦戦中。
ここに説明を書きながら、理解を深めていこうと思います

先日話した通り、PPU側にもメモリ空間があり、一部抜粋すると

$0000 ~ $0FFF (4096 byte) : パターンテーブル0
$1000 ~ $1FFF (4096 byte) : パターンテーブル1
$2000 ~ $23BF (4096 byte) : BGネームテーブル0
$23C0 ~ $23FF (64 byte) : BG属性テーブル0
$2400 ~ $27BF (960 byte) : BGネームテーブル1
$27C0 ~ $27FF (64 byte) : BG属性テーブル1
$2800 ~ $2BFF (960 byte) : BGネームテーブル2
$2BC0 ~ $2BFF (64 byte) : BG属性テーブル2
$2C00 ~ $2FBF (960 byte) : BGネームテーブル3
$2FC0 ~ $2FFF (64 byte) : BG属性テーブル3
$3F00 ~ $3F1F (32 byte) : カラーパレット

となっている。
($0000~$1FFFまでのパターンテーブルは、CHR-ROMの8KB分がマッピングされている)

パターンテーブル には簡単に言うと8x8のキャラクタデータ(ドット絵)が、256個入っており、0~255のキャラクタデータ番号が付いている

で、1ドットには、パレットの何番の色を使うかと言った、色番号が入っている。(色自体の値ではなく、0~3のインデックス番号です)
以前パレットの説明で、BG用に16色使えると伝えましたが、実際ひとつのキャラクタにつき、同時に使える色は4色となっています。
パレットにある16色のうち

0, 1, 2, 3
4, 5, 6, 7
8, 9, 10, 11
12, 13, 14, 15

の4つずつのセットで使うことになります。この4色のセットをパレットと呼ぶようです。4パレットあるということですね。
このパレットの中の、何番の色を使うのか、というのが、1ドット分の情報と思って下さい。
そうすると1ドットが0~3の2bitとなるので、
2bit x 横8ドット x 縦8ドット x 256キャラクタ = 32768 bit = 4096 byte
となり、パターンテーブルのサイズと合うことが分かります。

パターンテーブルには0と1とで2つあるのが分かると思いますが、それぞれBG用、スプライト用に使います(0をBG用、1をスプライト用にしてもいいですし、逆の設定にもできます)
BG用とスプライト用で、それぞれ256個のキャラクタデータを、パターンテーブルに置いておくことができる、という感じですね。
(実際はCHR-ROMにこの合計8KB分のキャラクタデータを書き込んでおきます)

次に ネームテーブル です

ファミコンのBGは、先程のキャラクタデータを1画面で横32x縦30でタイル表示することができます。
ネームテーブルは、この32x30に対して、何番のキャラクタデータを表示するのか、というキャラクタデータの番号が入っています。(入っているというか、実際はプログラムから書き込みをします)
(ネームテーブルが4つあるのは、以前書いたとおり、ファミコンには画面が4つあるためです。※これも書きましたが、実際は2画面で、ミラーされます)

32x30で960のタイルとなり、指定する番号はキャラクタデータの256個分(0~255)指定できれば良いので、1タイルにつき1byteあれば足りることが分かります。そのため、1ネームテーブルにつき960 byte分、領域があります。

例えばパターンテーブル0をBG用に設定し、VRAMの$2000に20を書き込むと、パターンテーブル0のキャラクタデータ19番が、画面0の0, 0に表示される、といった具合です。

次に__属性テーブル__です。(これ結構ややこしいです)

  • パターンテーブルには0~255のキャラクタデータがあり、
  • ネームテーブルには32x30の、どのキャラクタデータを使うかの指定がされている

と伝えました。ただ、これだとまだ、どのタイルがどのパレットを使うかといった、色に関する情報がないのが分かるかと思います。
先述の通り、パターンテーブルのキャラクタデータの1ドットには、色は指定されておらず、あくまでも0~3の色番号が入っているだけです。
ネームテーブルのどの箇所に、どのパレットを使うかを指定するのが、この属性テーブルです。

ただ、サイズを見てみると、64 byteしかないのが分かるかと思います。
ネームテーブルが960タイルあり、どのパレットを選択するかでパレット番号0~3の2bit分必要なので、単純に計算すると960 x 2bit = 1920bit = 240byte かかりそうな気がします。

なぜ64byteの領域で済むのか。この謎については「Ubuntuでファミコンプログラム その3(背景描画とパレット)」がとてもわかり易い説明&図を示してくれています。

3.8x8の背景1つにつき、1つの色指定ができるというわけではなく、
8x8の背景を4ブロックまとめた塊に対して色の指定を行なっていく。

上手い説明が思いつかないのですが、背景を4x4の塊に分けて、左上を0x23c0とし、右下に向けて0x23ffまでのブロックに分けます。
各々のアドレスに対して、使用する色情報を指定していきます。

つまり、4つのタイルをまとめて、パレット番号を1つ指定する、という感じになってます。

さっきの240byteを4で割ると、60byteとなり、64byteで足りることが分かります(余った4byteは使っていないようです。32x32のタイルだったら必要だったけど、32x30であるがゆえに不要な様子)

この、1つのグループ(4つのタイル)につきパレット番号0~3の2bitでパレット番号を指定することになるので、
たとえば属性テーブルの$23C0の1byteには、4グループ分のパレット番号が入っていることになります。

上の引用した図でも分かるように、$23C0に11_10_01_00という値が入っていたとして、
上位2bitから右下、左下、右上、左上のグループのパレット番号が指定することになっているので

  • 11 = 右下のグループのパレット番号は3
  • 10 = 左下のグループのパレット番号は2
  • 01 = 右上のグループのパレット番号は1
  • 00 = 左上のグループのパレット番号は0

となります。

最後に カラーパレット ですが

これは単純に上位16byteにBG用の16色、下位16byteにスプライト用の16色が入っています。
(ファミコンに使える色は64色あり、そこから選ばれしそれぞれの16色がこのカラーパレット領域にあるという感じです)

以上です。

ということで、BGの描画について最後に要すると

  • パターンテーブルにキャラクタデータを置いておいて(これはCHR-ROMからマップされるので、プログラムで何かすることは不要)
  • カラーパレットにパレットの色情報を書き込んでおき
  • ネームテーブルを書き換えてBGのどの位置にどのキャラクターを表示するかを指定して
  • 属性テーブルでパレット番号を指定して色付けする

と、画面にBGが表示されます。

これに加えて、スプライトの描画が重なって、ゲーム画面が表示されます。

画面がどのように重なって描画されているのかは、「PPU 2C02」にわかりやすい説明&図があります。


↑これ。

今回説明したのは、BGの描画の仕組みについてですね。

スプライトについては昨日説明したとおり、また別のVRAM領域があり、そこを書き換えることによって描画します。(詳しい説明はまた機会があれば後日)

balmychanbalmychan

ということで、以上の調査を踏まえ、再度BG表示にチャレンジしてみたところ...

草を生やすことに成功!

(上の草がBGで、下のギコ猫はスプライト)

属性テーブルにパレット番号を指定していなかったのが原因だったようです。

雑なんですが、コードも貼っておきます

	; スプライト表示サンプル

	; INESヘッダー
	.inesprg 1 ;   - プログラムにいくつのバンクを使うか。今は1つ。
	.ineschr 1 ;   - グラフィックデータにいくつのバンクを使うか。今は1つ。
	.inesmir 0 ;   - 水平ミラーリング
	.inesmap 0 ;   - マッパー。0番にする。

	.bank 1      ; バンク1
	.org $FFFA   ; $FFFAから開始

	.dw 0        ; VBlank割り込み
	.dw Start    ; リセット割り込み。起動時とリセットでStartに飛ぶ
	.dw 0        ; ハードウェア割り込みとソフトウェア割り込みによって発生

	.bank 0			; バンク0
	.org $C000  ; $C000から開始

	; ここからプログラムコード開始
	
Start:  
	lda $2002  ; VBlankが発生すると、$2002の7ビット目が1になる
	bpl Start  ; bit7が0の間は、Startラベルの位置に飛んでループして待つ

	; PPUコントロールレジスタ初期化
	lda #%00001000 
	sta $2000
	lda #%00000110		; 初期化中はスプライトとBGを表示OFFにする
	sta $2001

	ldx #$00    ; Xレジスタクリア

	; VRAMアドレスレジスタの$2006に、パレットのロード先のアドレス$3F00を指定する。
	lda #$3F
	sta $2006
	lda #$00
	sta $2006

loadPal: ; ラベルは、「ラベル名+:」の形式で記述
	lda tilepal, x ; Aに(ourpal + x)番地のパレットをロードする

	sta $2007 ; $2007にパレットの値を読み込む

	inx ; Xレジスタに値を1加算している

	cpx #32 ; Xを32(10進数。BGとスプライトのパレットの総数)と比較して同じかどうか比較している	
	bne loadPal ;	上が等しくない場合は、loadpalラベルの位置にジャンプする
	; Xが32ならパレットロード終了

	; $2000(ネームテーブル)から...
	lda #$20
	sta $2006
	lda #$00
	sta $2006
	; 0番を$2000~$2003に書き込む
	lda #$00
	sta $2007
	lda #$00
	sta $2007
	lda #$00
	sta $2007
	lda #$00
	sta $2007

	; 属性を書き込み
	lda #$23
	sta $2006
	lda #$C0
	sta $2006
	lda #%00000000
	sta $2007

	; スプライト描画
	lda #$00   ; $00(スプライトRAMのアドレスは8ビット長)をAにロード
	sta $2003  ; AのスプライトRAMのアドレスをストア

	lda #120    ; 50(10進数)をAにロード
	sta $2004   ; Y座標をレジスタにストアする
	lda #00     ; 0(10進数)をAにロード
	sta $2004   ; 0をストアして0番のスプライトを指定する
	sta $2004   ; 反転や優先順位は操作しないので、再度$00をストアする
	lda #20		;	20(10進数)をAにロード
	sta $2004   ; X座標をレジスタにストアする

	; PPUコントロールレジスタ2初期化
	lda #%00011110	; スプライトとBGの表示をONにする
	sta $2001

infinityLoop:
	jmp infinityLoop	; 今回は描画して終わりなので無限ループで良い

tilepal: .incbin "giko.pal" ; パレットをincludeする

	.bank 2       ; バンク2
	.org $0000    ; $0000から開始

	.incbin "giko.bkg"  ; 背景データのバイナリファイルをincludeする
	.incbin "giko.spr"  ; スプライトデータのバイナリィファイルをincludeする
balmychanbalmychan

ちなみに余談ですが、↓のサイトのドラクエのフォントについての説明がなかなかおもしろい...

https://www.wizforest.com/OldGood/ntsc/famicom.html;p2#dqfont

文字などのいわゆるフォントデータも先程のパターンテーブルにキャラクタとして置くことになるわけですが、その節約のために、3色の色を使って、2種類のフォントデータを重ねて入れていたとのこと。

↑はサイトから引用したGIF。

  • 赤でアルファベットを書いて、緑でひらがなを書いて、重なる部分を黄色にしておいて
  • アルファベットを抽出したい場合は赤と黄色の部分を白にして、それ以外を黒にするとアルファベットが浮かび上がり
  • ひらがなを抽出したい場合は緑と黄色の部分を白にして、それ以外を黒にするとひらがなが浮かび上がる

とのこと。(なかなかすごいテクニック...)

balmychanbalmychan

以前発注していた、PLCCを既存のファミコンカセットの基板に取り付ける下駄的な基板が届いた

その時のスクラップはこちら→https://zenn.dev/link/comments/94b38c340adbf6

こんな感じ↓

ピッタリくっつくか不安だったが...
ハマった!

ハンダ付けした

ファミコン本体に差した様子はこんな感じ


とりあえず元の4人打ち麻雀をFlashROMに書き込んでみると
無事起動!!(配線ミスってなくてよかった...)

この下駄のおかげで、ジャンパワイヤーで頑張って繋ぐ生活ともおさらばです

一応昨日作ったプログラムもFlashROMに書き込んでみました

なんだか様子がおかしい...

まぁとりあえず、下駄制作は成功ということで!!

balmychanbalmychan

(子供が寝静まるのを待ってたら寝落ちしてしまいあまり進捗せず...)
今日はマルツに寄ったので、いらなそうだけどいりそうなものを買っておいた

  • いくつかピンヘッダー
  • ICピンそろった
  • ラジオペンチとニッパー(持ってるのがボロボロだったので)
  • ゼロプレッシャーICソケット

先日下駄を履かせた4人打ち麻雀のカセット基板はミラーリングが垂直ミラーリングの配線になっているので、水平ミラーリングのほうも作っておく予定

カセットに関する詳細な情報は「NES Cart DataBase」で調べると分かる

例えばお馴染み4人打ち麻雀は

4人打ち麻雀
http://bootgod.dyndns.org:7777/profile.php?id=1479

これ。ミラーリングはVerticalと垂直ミラーリングであると分かる。
あと今回の話には関係ないのですが、結構重要なのは、iNES Mapperが0となっているところです。マッパー0はPRG-ROMが16KBで、CHR-ROMが8KBの最も単純な構造になってます。なので今回の様に自作する場合は、かなりやりやすくなってます。
4人打ち麻雀、バイナリィランド、ボンバーマン、ベースボールあたりがマッパー0となっているので、過去買い漁って手元に結構あります。

で、バイナリィランドがちょうど水平ミラーリングの基板なようで、今回はこれを換装しておこうと思ってます。

バイナリィランド
http://bootgod.dyndns.org:7777/profile.php?id=1664

先述の通り寝落ちしてしまったのでこの日はここまで...

ハンダシュッ太郎ほしいけど高くてためらう

balmychanbalmychan

垂直ミラーリング用と水平ミラーリング用のカセット基板を用意していた。
昨日言った通り、バイナリィランドのカセットに犠牲になってもらった。

ROMを外した様子↓


ここで変な線があることに気づく...(赤丸のところ)

この線は、A14とGNDを繋いでおり、A14をLOWにしている。

4人打ち麻雀の方はA15とA14はVCCと繋がっており、常にHIGHとなっていたので、FlashROMにも$C000からプログラムを書き込んでいた。が、バイナリィランドはA15はHIGHでA14はLOWなので、$8000から書き込む必要がありそう。

とはいえこれを考慮するのは面倒なので、この線は外して、A15とA14をハンダで繋いだ(これでA15とA14がHIGHになる)

これで無事起動した。

これで垂直ミラーリング用のカセットと、水平ミラーリング用のカセットが用意できた。↓

その後、FlashROMへの書き込みがうまくいかず、1~2時間ロス...

結局ユニバーサル基板版書き込み機もPCB版書き込み機も壊れてしまって(うまく読み取りができなくなってしまった)
再度PCB版の基板を元に作り直した。(元々5枚あったのであまっている)
これで無事書き込みは成功。

さて、ソフト側ももう少し進めておこうと思って、コントローラーの入力を試しました。

上下左右ボタンでスプライトを動かしている様子↓

https://youtu.be/ipUHflBWNlQ

コードも貼っておきます

	; ヘッダー
	.inesprg 1 ;   - プログラムにいくつのバンクを使うか。今は1つ。
	.ineschr 1 ;   - グラフィックデータにいくつのバンクを使うか。今は1つ。
	.inesmir 0 ;   - 水平ミラーリング
	.inesmap 0 ;   - マッパー。0番にする。

	; 割り込みの設定
	.bank 1
	.org $FFFA
	.dw mainLoop	; VBlank割り込み
	.dw Start		; リセット割り込み。起動時とリセットでStartに飛ぶ
	.dw 0			; ハードウェア割り込みとソフトウェア割り込み

	.bank 0			; バンク0

	.org $0300	 ; $0300から開始、スプライトDMAデータ配置
Sprite1_Y:     .db  120   ; スプライト#1 Y座標
Sprite1_T:     .db  0   ; スプライト#1 ナンバー
Sprite1_S:     .db  0   ; スプライト#1 属性
Sprite1_X:     .db  20   ; スプライト#1 X座標

	; プログラムコード
	.org $C000;
	
Start:  
	lda $2002  ; VBlankが発生すると、$2002の7ビット目が1になる
	bpl Start  ; bit7が0の間は、Startラベルの位置に飛んでループして待つ

	; PPUコントロールレジスタ初期化
	lda #%00001000 
	sta $2000
	lda #%00000110		; 初期化中はスプライトとBGを表示OFFにする
	sta $2001

	ldx #$00    ; Xレジスタクリア

	; VRAMアドレスレジスタの$2006に、パレットのロード先のアドレス$3F00を指定する。
	lda #$3F
	sta $2006
	lda #$00
	sta $2006

loadPal: ; ラベルは、「ラベル名+:」の形式で記述
	lda tilepal, x ; Aに(ourpal + x)番地のパレットをロードする

	sta $2007 ; $2007にパレットの値を読み込む

	inx ; Xレジスタに値を1加算している

	cpx #32 ; Xを32(10進数。BGとスプライトのパレットの総数)と比較して同じかどうか比較している	
	bne loadPal ;	上が等しくない場合は、loadpalラベルの位置にジャンプする
	; Xが32ならパレットロード終了

	; $2000(ネームテーブル)から...
	lda #$20
	sta $2006
	lda #$00
	sta $2006
	; 0番を$2000~$2003に書き込む
	lda #$00
	sta $2007
	lda #$00
	sta $2007
	lda #$00
	sta $2007
	lda #$00
	sta $2007

	; 属性を書き込み
	lda #$23
	sta $2006
	lda #$C0
	sta $2006
	lda #%00000000
	sta $2007

	; スプライト位置の初期化
	lda X_Pos_Init
	sta Sprite1_X
	lda Y_Pos_Init
	sta Sprite1_Y

	; PPUコントロールレジスタ2初期化
	lda #%00011110	; スプライトとBGの表示をONにする
	sta $2001

	; PPUコントロールレジスタ1の割り込み許可フラグを立てる
	lda #%10001000
	sta $2000

infinityLoop:
	jmp infinityLoop	; 今回は描画して終わりなので無限ループで良い

mainLoop:	; メインループ
	; スプライト描画(DMAを利用)
	;lda #$3 	; スプライトデータは$0300から配置しているので、3をロード
	;sta $4014

	; スプライト描画
	lda #$00   ; $00(スプライトRAMのアドレスは8ビット長)をAにロード
	sta $2003  ; AのスプライトRAMのアドレスをストア

	; Y座標
	lda $0300
	sta $2004
	
	; 番号
	lda #00
	sta $2004

	; 属性
	sta $2004

	; X座標
	lda $0303
	sta $2004

	; パッドI/Oレジスタの初期化
	lda #$01
	sta $4016
	lda #$00
	sta $4016

	; パッド入力チェック
	lda $4016 ; Aボタン
	lda $4016 ; Bボタン
	lda $4016 ; Selectボタン
	lda $4016 ; Startボタン
	
	; ↑ボタン
	lda $4016
	and #1
	bne UPKEYdown
	
	; ↓ボタン
	lda $4016
	and #1
	bne DOWNKEYdown

	; ←ボタン
	lda $4016
	and #1
	bne LEFTKEYdown

	; →ボタン
	lda $4016
	and #1
	bne RIGHTKEYdown

	jmp NOTHINGdown 
	
UPKEYdown:
	dec Sprite1_Y
	jmp NOTHINGdown

DOWNKEYdown:
	inc Sprite1_Y
	jmp NOTHINGdown

LEFTKEYdown:
	dec Sprite1_X
	jmp NOTHINGdown

RIGHTKEYdown:
	inc Sprite1_X
	jmp NOTHINGdown

NOTHINGdown
	rti

	; 初期データ
X_Pos_Init   .db 20
Y_Pos_Init   .db 120

tilepal: .incbin "giko.pal" ; パレットをincludeする

	.bank 2       ; バンク2
	.org $0000    ; $0000から開始

	.incbin "giko.bkg"  ; 背景データのバイナリファイルをincludeする
	.incbin "giko.spr"  ; スプライトデータのバイナリィファイルをincludeする

上のコードだとRAM領域に保存した座標データをI/Oレジスタ通して設定していますが、スプライトの情報はDMAで送るのが良いようです。

DMAは、CPUを経由せずにメモリにデータを直接転送する仕組み(一般用語)です。今回だと、CPU側のメモリ空間にあるデータを、CPUを介さず(I/Oレジスタによるアクセスをせずに)にPPU側のVRAMに転送できます。(いちいちI/OレジスタでVRAMに書き込みしてられないですしね)
上のギコ猫のサンプルだと、スプライトの表示は最大64個までなので、RAM領域の$0300~$0400の$0100分をDMAで転送しています。

次回は、このDMAによるスプライト制御も試してみたいと思います。

balmychanbalmychan

RAMの話が出たのでついでに説明しておくと、ファミコンのRAMはCPU側のメモリ空間の$0000 ~ $0800にマッピングされています。つまり、ファミコンのメモリ(RAM領域)は2KBということですね(意外と多いと見るか、少ないと見るか...)

balmychanbalmychan

DMAを試した
コードはこちら

	; ヘッダー
	.inesprg 1 ;   - プログラムにいくつのバンクを使うか。今は1つ。
	.ineschr 1 ;   - グラフィックデータにいくつのバンクを使うか。今は1つ。
	.inesmir 0 ;   - 水平ミラーリング
	.inesmap 0 ;   - マッパー。0番にする。

	; 割り込みの設定
	.bank 1
	.org $FFFA
	.dw mainLoop	; VBlank割り込み
	.dw Start		; リセット割り込み。起動時とリセットでStartに飛ぶ
	.dw 0			; ハードウェア割り込みとソフトウェア割り込み

	.bank 0			; バンク0

	.org $0300	 ; $0300から開始、スプライトDMAデータ配置
Sprite1_Y:     .db  0   ; スプライト#1 Y座標
Sprite1_T:     .db  0   ; スプライト#1 ナンバー
Sprite1_S:     .db  0   ; スプライト#1 属性
Sprite1_X:     .db  0   ; スプライト#1 X座標

	; プログラムコード
	.org $C000;
	
Start:  
	lda $2002  ; VBlankが発生すると、$2002の7ビット目が1になる
	bpl Start  ; bit7が0の間は、Startラベルの位置に飛んでループして待つ

	; PPUコントロールレジスタ初期化
	lda #%00001000 
	sta $2000
	lda #%00000110		; 初期化中はスプライトとBGを表示OFFにする
	sta $2001

	ldx #$00    ; Xレジスタクリア

	; VRAMアドレスレジスタの$2006に、パレットのロード先のアドレス$3F00を指定する。
	lda #$3F
	sta $2006
	lda #$00
	sta $2006

loadPal: ; ラベルは、「ラベル名+:」の形式で記述
	lda tilepal, x ; Aに(ourpal + x)番地のパレットをロードする

	sta $2007 ; $2007にパレットの値を読み込む

	inx ; Xレジスタに値を1加算している

	cpx #32 ; Xを32(10進数。BGとスプライトのパレットの総数)と比較して同じかどうか比較している	
	bne loadPal ;	上が等しくない場合は、loadpalラベルの位置にジャンプする
	; Xが32ならパレットロード終了

	; $2000(ネームテーブル)から...
	lda #$20
	sta $2006
	lda #$00
	sta $2006
	; 0番を$2000~$2003に書き込む
	lda #$00
	sta $2007
	lda #$00
	sta $2007
	lda #$00
	sta $2007
	lda #$00
	sta $2007

	; 属性を書き込み
	lda #$23
	sta $2006
	lda #$C0
	sta $2006
	lda #%00000000
	sta $2007

	; スプライト1の初期化
	; X座標
	lda X_Pos_Init
	sta Sprite1_X
	; Y座標
	lda Y_Pos_Init
	sta Sprite1_Y
	; 属性
	lda #$0
	sta Sprite1_S
	; タイル番号
	lda #$0
	sta Sprite1_T

	; PPUコントロールレジスタ2初期化
	lda #%00011110	; スプライトとBGの表示をONにする
	sta $2001

	; PPUコントロールレジスタ1の割り込み許可フラグを立てる
	lda #%10001000
	sta $2000

infinityLoop:
	jmp infinityLoop	; 今回は描画して終わりなので無限ループで良い

mainLoop:	; メインループ
	; スプライト描画(DMAを利用)
	lda #$3 ; スプライトデータは$0300から配置しているので、3をロード
	sta $4014
	
	; パッドI/Oレジスタの初期化
	lda #$01
	sta $4016
	lda #$00
	sta $4016

	; パッド入力チェック
	lda $4016 ; Aボタン
	lda $4016 ; Bボタン
	lda $4016 ; Selectボタン
	lda $4016 ; Startボタン
	
	; ↑ボタン
	lda $4016
	and #1
	bne UPKEYdown
	
	; ↓ボタン
	lda $4016
	and #1
	bne DOWNKEYdown

	; ←ボタン
	lda $4016
	and #1
	bne LEFTKEYdown

	; →ボタン
	lda $4016
	and #1
	bne RIGHTKEYdown

	jmp NOTHINGdown 
	
UPKEYdown:
	dec Sprite1_Y
	jmp NOTHINGdown

DOWNKEYdown:
	inc Sprite1_Y
	jmp NOTHINGdown

LEFTKEYdown:
	dec Sprite1_X
	jmp NOTHINGdown

RIGHTKEYdown:
	inc Sprite1_X
	jmp NOTHINGdown

NOTHINGdown
	rti

	; 初期データ
X_Pos_Init   .db 20
Y_Pos_Init   .db 120

tilepal: .incbin "giko.pal" ; パレットをincludeする

	.bank 2       ; バンク2
	.org $0000    ; $0000から開始

	.incbin "giko.bkg"  ; 背景データのバイナリファイルをincludeする
	.incbin "giko.spr"  ; スプライトデータのバイナリィファイルをincludeする

つまづいたポイントとしては

	.org $0300	 ; $0300から開始、スプライトDMAデータ配置
Sprite1_Y:     .db  0   ; スプライト#1 Y座標
Sprite1_T:     .db  0   ; スプライト#1 ナンバー
Sprite1_S:     .db  0   ; スプライト#1 属性
Sprite1_X:     .db  0   ; スプライト#1 X座標

この時点で$0300~$0303には0が入っていて、DMA転送するだけで画面にスプライトが表示されると思っていたが、どうもそういうわけでもなく、

	; スプライト1の初期化
	; X座標
	lda X_Pos_Init
	sta Sprite1_X
	; Y座標
	lda Y_Pos_Init
	sta Sprite1_Y
	; 属性
	lda #$0
	sta Sprite1_S
	; タイル番号
	lda #$0
	sta Sprite1_T

と$0300~$0303には別途値を設定する必要がありました。
(ちょっとまだ理解が不明確なのでまた調べようと思います)

balmychanbalmychan

あまり時間が取れなかったが、ギコ猫のサイトやその他サイトで、サウンド再生についてちょっと勉強中

サウンド再生についても、PPUと同じ様にAPU(Audio Processing Unit)という専用のプロセッサが搭載されている(但しファミコンの場合はCPUにワンチップ化されて内臓されているとのこと)
そして、いつもながらI/Oレジスタを通して制御を行う

  • 短形波 ... 好きな高さの音を出せる(何秒間鳴らすとか指定可能な様子)
  • 三角波 ... ベース音に使われる
  • ノイズ ... 主に効果音に使われる
  • DPCM ... PCM音源。任意の波形を出力できる(まだ良くわかってない)

とこの4つを駆使して再生するようです。
これだけでどう良い感じに音楽が再生できるのか...(サウンドプログラマーが必要な理由も分かる)

balmychanbalmychan

カセット基盤が届いた。こんな感じ。サイズ感はちょうどよく、ちゃんとファミコンに刺さった




で、ベースボールを書き込んでみたものの、起動しない...

回路図を見返してみたところ

  • A15をHIGHに繋ぐべきところが、LOWに繋がっていた

と分かったので、FlashROMへのデータの書き込みを$C000からではなく、$4000からにした。そしたら一応起動したものの、こんな状態で、表示がバグっている↓

回路図を見返してもおかしいところはないし、かなり難儀しそうな予感...

ちなみにこの件とは関係ないですが、既存のカセットを見てて気づいたところが。
例の水平ミラーリングでA11に繋ぐか垂直ミラーリングでA10に繋ぐかは、下記の画像のように、どちらにハンダを付けるかで容易に変えれるようにしているようだった

確かにこれなら一つのカセット基板で、垂直にするか水平にするか変更可能なので、良さそう。次作るときは参考にする。
※今回作ったやつは、垂直ミラーリング固定にしてしまった

表示がバグる件は、引き続き調査中...

balmychanbalmychan

ひとつずつじっくり配線を調べた結果、回路図作成時点で2箇所ミスがあった。

ミス1

  • 誤: ROM側26 A13を端子側56 PPU A13に繋いでいた
  • 正: ROM側26 A13は端子側16 GNDに繋ぐのが正しい(CHR-ROMは8KBなのでA13は不要)

ミス2

  • 誤: ROM側20 /CEを端子側16 GNDに繋いでいた
  • 正: ROM側20 /CEを端子側56 PPU A13に繋ぐのが正しい(良くわからないがまあこうなっているので)

で、そうと分かれば早速試してみたいので、パターンカットしたり繋ぎ直したりした↓

そして無事直った↓

ちなみにベースボールは水平ミラーリングのソフトだったので、タイトル画面の先のゲーム画面は表示が変なままだった。
垂直ミラーリングのボンバーマンを書き込んでみると

ゲーム画面も問題なさそうだった↓

ということで、カセット基板の設計は

  • CHR-ROM側26 A13は端子側16 GNDに繋ぐように直す
  • CHR-ROM側20 /CEを端子側56 PPU A13に繋ぐように直す
  • PRG-ROM側01 A15はHIGHに繋ぐ
  • CHR-ROM側01 A15はHIGHに繋ぐ
  • ミラーリングを制御するための端子側18 VRAM ~A10は、CHR-ROM側の21 A10または23 A11に容易に繋ぎ変えれるように設計する

と直せば、マッパー0のカセット基板が無事完成となりそうです。

これでカセットの量産体制が整ったと言いたいところなんですが、実は今回ROMとして使っていたFlashROMのEN29F002Tが、秋月で販売が終わっているようで、入手が困難になっていた...(涙)

  • 海外から取り寄せるなり
  • 別のFlashROMを調達するなり

が必要そう。

まぁでも一旦念願の「いちからファミコンカセット(ハード)を作る」というのができ、嬉しいので良しとする
(この企画は2~3年前からやりたくて何度かチャレンジしたものの頓挫してたので...)

balmychanbalmychan

取り急ぎeBayでFM1608というこのFRAMと↓

https://www.ebay.com/itm/392368833838

共立エレショップで汎用ICをポチった
https://eleshop.jp/shop/g/gT11453/

今まで全然触ることがなかったが、74HC02の方は汎用ロジックICと言われるもので、
AND回路やらOR回路やらフリップフロップやら、いろいろな論理回路がワンチップで内蔵されているものらしい。
上記のFM1608のFRAMを通常のSRAMと同じ様に使えるようにするために、この汎用ICを組み込んで、信号を変えればいけるらしい。

balmychanbalmychan

今日は下記のようなパターンをどうやってKiCadに落とし込むか考えていた

色々ググったもの有力情報がなくどうしようかなーと思っていたものの、
これって単なるスイッチかと思い、回路図上はスイッチにして、フットプリントを上のように独自で作るような感じにしようかなと思います
(なんなら実際にトグルスイッチ付けてもいいですしね)

※社内メンバーの助言でジャンパーと呼ぶことが分かりました

こんな感じ(A10に繋ぐかA11に繋ぐか切り替え可能)

フットプリントも、単純に下記のような感じで作った(ハンダで繋いでも良いし、トグルスイッチを実装しても良いしという感じ)

で、こんな感じで置きました。


注文完了で今日は終了

今後は拡張RAM搭載のマッパー1について調べて行く予定。(拡張RAMの使いかたについて知らないと、FRAMへの載せ替えもなにもないので)
運良く数年前にたまたま買って手元にある「谷川浩司の将棋指南Ⅲ」「ドラゴンクエストⅢ」がマッパー1だったので、この基板を元に調べていこうと思います。

balmychanbalmychan

今日はマッパー1の調べ物がメイン。ググって片っ端から読みつつ、手元のマッパー1の基板を眺めつつ。

「谷川浩司の将棋指南Ⅲ」を例に取ると

  • 右下が128KBのPRG-ROM
  • 左下が8KBのCHR-RAM (ROMではなくRAMです)
  • 右上が拡張RAM
  • 左上がMMC1

となってます。マッパー1の話が出るときは必ずMMC1のことがセットで話されており、
個人的にはこのMMC1というのが拡張RAM(バッテリーバックアップ領域)と思っていましたが

実はこのMMC1は、バンク切り替えを行うためのチップとのことでした。
バンク切り替えについては「よいこのための?ファミコン講座 ファミコンカセット編」の中段にも記載がありますが、
マッパー0の場合はPRG-ROMが32KBでCHR-ROMが8KB固定のもので、とてもシンプルであるものの、しかい容量が少ないです。この容量不足を補うために、メモリ空間のある領域を切り替えられるようにして、増やせるようにするための仕組みです。(画像も上のサイトから引用)

で、先述のMMC1が、このバンク切り替えを制御するチップのようです。

もう一点。
先程CHR-ROMについて、CHR-RAMだと説明しましたが、マッパー1ではROMだったりRAMだったりするようです。(「UNROM とか SNROM ってなんですか」を読むと良いかも)
CHR-ROMの場合は単に8KBがPPUのメモリ空間にマッピングされるだけですが、RAMの場合は、PRG-ROMにあるキャラクタデータをCHR-RAMに転送するようです。
手元にある谷川将棋もドラクエ3もCHR-RAMのタイプでした。

CHR-RAMの利点については「NesDev CHR RAM」に記載があります。以下抜粋

Can switch tiles in small increments, and the granularity of switching does not depend on the mapper's complexity.
Tile data can be compressed in ROM.
Tile data can be otherwise generated in real time.
Only one chip to rewire and put on the board when replicating your work on cartridge.
All data is stored in one address space, as opposed to a small amount being inaccessible when rendering is on and unreliable when DPCM is on.

結構CHR-ROMだと無駄な重複や無駄な空きが生まれたりするというのは聞いたことがあったのと、上に記載されているように、いちいち2つのROMに書き出さないで良いのと、あと特に利点として大きそうなのがリアルタイムにキャラクタデータを生成できる点ですかね。

マッパー1の調査は今日はここまで。

別件で先程の参考サイトの「ファミコン用カートリッジ基板の自作(組み立て編)」の中段に気になる文章があった。

もう1つの原因はBGとスプライトの初期化が抜けていたことです。エミュレータだと自動的に初期化されているので、ミスに気が付きませんでした。

以前自作プログラム書き込んで実機で試したときに変な表示になっていたのはこれのせいかもしれません。PPUのスプライトやBGに関する領域の値は初期化したほうが良いのかも。

あともう1件、ファミコンカセットの外側のケースがAliExpressのFunlineStoreというショップで販売されていたので、購入してみた。(もしかしたらいつか使うかもなので)

https://ja.aliexpress.com/item/32868558621.html

今日は以上

balmychanbalmychan

昨日スイッチにした部分は、社内メンバーにジャンパーだよと教えてもらったので、ジャンパーに直した(注文はまた次回)

回路図はデュアルジャンパーというのがあったのでそれに変えて

フットプリントと、イメージはこんな感じ

あと今日はSRAMの動作を遊びながら確認しようと思い、谷川将棋からSRAMを取り外した。(ついでにMMC1も取っておいた)

谷川将棋に載っていたチップはMN4464-08LLというもので、ネットでデータシートを見つけたものの、どういうピンアサインになっているかは分からなかった。

参考記事として

にピンアサインの図が載っている。下記。

8ピンのROMも同じアサインだったので、ROMでもRAMでもDIPの28ピンだったらこの配置になるんだろうか(良く分からない)
とりあえず今回手元にあるMN4464-08LLも同じピンアサインだろうということで実験を進めようと思います。

参考記事にもある通り、下記のような手順になる(予定)

最初に

  • /CEをLOWにしてチップを有効にする

書き込み

  • アドレスピンに信号を出力
  • /WEをLOWにする(書き込みを有効にする)
  • データピンに信号を出力
  • /WEをHIGHにする(書き込みを無効にする)

読み取り

  • アドレスピンに信号を出力
  • /OEをLOWにする(これで、データが出力される)
  • データピンからデータを読み取る
  • /OEをHIGHにする

今日はここまでで、また後日実際にやってみようと思います

balmychanbalmychan

ArudinoでSRAMの読み取りと書き込み、を試していた

※昨日書いた書き込み、読み取りの手順はもう少しシンプルになったので修正しました。

昨日のピンの図通りに繋いで試したものの、読み取りも書き込みも成功せず...
やはりこのチップ(MN4464-08LL)のデータシートを見つけないとうまくいかないのかなとネットを漁っていた。

するとこんな良さげな質問記事が

SRAM Datasheet

同じ様に、MN4464-08LLのデータシートが見つからない的な悩みを投稿している。その回答が

Almost guaranteed to be a plain old 6264 (8KB static RAM) with standard pinout. Nearly(?) all NES cartridges with expansion RAM (battery or not) used 'em.

とのことなので、6264というチップのデータシートを参考にすれば良さそう。それで見つけたデータシートがこれ↓
http://users.ece.utexas.edu/~valvano/Datasheets/MCM6264.pdf

重要なところを抜粋↓

ここから下記が分かった

  • 1 A14と思っていたものはNCとなっていて、なにも繋がないのが良い
  • 20 /CSは/E1、26 A13はE2だった(これについての説明は次)
  • チップの有効無効は、20 E1と26 E2セットで制御する(20 /E1をLOWにして、26 E2をHIGHにするとチップが有効になる)

これを元に再度プログラムを修正し、試した見たところ、書き込みに成功した

上の動画の流れを説明すると

  • 最初のダンプではゴミデータが入っているのが分かる
  • Hello Worldを書き込み(後ろの00は余計に書き込みしてるだけなので気にしないでください)
  • 電源を一時的に切って全消去
  • Hello Worldが消えている

という感じ。

繋いでいる様子↓

今回のArduinoのコードはこちら↓

#define SIZE_256B 256UL
#define SIZE_1KB 1024UL
#define SIZE_8KB 8192UL
#define SIZE_16KB 16384UL

#define NC 22
#define ADDRESS_12 23
#define ADDRESS_7 24
#define ADDRESS_6 25
#define ADDRESS_5 26
#define ADDRESS_4 27
#define ADDRESS_3 28
#define ADDRESS_2 29
#define ADDRESS_1 30
#define ADDRESS_0 31
#define DATA_0 32
#define DATA_1 33
#define DATA_2 34
#define GND 35

#define DATA_3 36
#define DATA_4 37
#define DATA_5 38
#define DATA_6 39
#define DATA_7 40
#define E1 41
#define ADDRESS_10 42
#define OE 43
#define ADDRESS_11 44
#define ADDRESS_9 45
#define ADDRESS_8 46
#define E2 47
#define WE 48
#define VCC 49


void setup() {
  pinMode(E1, OUTPUT);
  pinMode(E2, OUTPUT);
  pinMode(OE, OUTPUT);
  pinMode(WE, OUTPUT);

  pinMode(VCC, OUTPUT);
  pinMode(GND, OUTPUT);

  pinMode(ADDRESS_0, OUTPUT);
  pinMode(ADDRESS_1, OUTPUT);
  pinMode(ADDRESS_2, OUTPUT);
  pinMode(ADDRESS_3, OUTPUT);
  pinMode(ADDRESS_4, OUTPUT);
  pinMode(ADDRESS_5, OUTPUT);
  pinMode(ADDRESS_6, OUTPUT);
  pinMode(ADDRESS_7, OUTPUT);
  pinMode(ADDRESS_8, OUTPUT);
  pinMode(ADDRESS_9, OUTPUT);
  pinMode(ADDRESS_10, OUTPUT);
  pinMode(ADDRESS_11, OUTPUT);
  pinMode(ADDRESS_12, OUTPUT);

  setDataPinMode(OUTPUT);

  digitalWrite(WE, HIGH);
  digitalWrite(OE, HIGH);
  digitalWrite(E1, LOW);
  digitalWrite(E2, HIGH);

  digitalWrite(GND, LOW);
  digitalWrite(VCC, HIGH);

  Serial.begin(19200);

  printCommands();
}

char buffer[256];

void loop() {
  if (Serial.readBytes(buffer, 1) == 1) {
    if (strncmp("W", buffer, 1) == 0) {
      unsigned char dataString[50] = "Hello World";
      writeBytes(0x0000UL, dataString, sizeof(dataString));
      printCommands();
    } else if (strncmp("D", buffer, 1) == 0) {
      dump(SIZE_256B);
      printCommands();
    } else if (strncmp("E", buffer, 1) == 0) {
      eraseAll();
      printCommands();
    }
  }
}

/**
 * コマンド一覧を出力
 */
void printCommands() {
  Serial.println("commands? :");
  Serial.println("W : Write Hello World");
  Serial.println("D : Dump");
  Serial.println("E : Erase");
}

/**
 * 指定サイズ分ダンプ出力
 */
void dump(unsigned long size) {
  char dataString[50] = {0};
  for (unsigned long i = 0; i < size; i++) {
    if ((i % 16) == 0) {
      sprintf(dataString, "0x%04X", i);
      Serial.print(dataString);
      Serial.print(" : ");
    }

    unsigned char ch = readByte(i);
    sprintf(dataString, "%02X", ch);
    Serial.print(dataString);
    Serial.print(" ");
    if ((i % 16) == 15) {
      Serial.println("");
    }
  }
  Serial.println("");
}

/**
 * バイト配列を書き込む
 * @address このアドレスから指定size分書き込む
 * @bytes 書き込むデータ
 * @size データの長さ
 */
void writeBytes(unsigned long address, unsigned char* bytes, int size) {
  for (int i = 0; i <= size; i++) {
    writeByte(address + i, bytes[i]);
  }
}

/**
 * データピンのモードをINPUT/OUTPUTに変更する
 */
void setDataPinMode(int mode) {
  if (mode == OUTPUT) {
    pinMode(DATA_0, OUTPUT);
    pinMode(DATA_1, OUTPUT);
    pinMode(DATA_2, OUTPUT);
    pinMode(DATA_3, OUTPUT);
    pinMode(DATA_4, OUTPUT);
    pinMode(DATA_5, OUTPUT);
    pinMode(DATA_6, OUTPUT);
    pinMode(DATA_7, OUTPUT);
  } else {
    pinMode(DATA_0, INPUT_PULLUP);
    pinMode(DATA_1, INPUT_PULLUP);
    pinMode(DATA_2, INPUT_PULLUP);
    pinMode(DATA_3, INPUT_PULLUP);
    pinMode(DATA_4, INPUT_PULLUP);
    pinMode(DATA_5, INPUT_PULLUP);
    pinMode(DATA_6, INPUT_PULLUP);
    pinMode(DATA_7, INPUT_PULLUP);
  }
}

/**
 * アドレスをセットする
 */
void setAddress(unsigned long address) {
  digitalWrite(ADDRESS_0,  ((address & 0x000001) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_1,  ((address & 0x000002) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_2,  ((address & 0x000004) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_3,  ((address & 0x000008) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_4,  ((address & 0x000010) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_5,  ((address & 0x000020) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_6,  ((address & 0x000040) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_7,  ((address & 0x000080) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_8,  ((address & 0x000100) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_9,  ((address & 0x000200) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_10, ((address & 0x000400) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_11, ((address & 0x000800) == 0) ? LOW : HIGH);
  digitalWrite(ADDRESS_12, ((address & 0x001000) == 0) ? LOW : HIGH);
}

/**
 * 1バイト書き込み
 */
void writeByte(unsigned long address, unsigned char data) {
  setDataPinMode(OUTPUT);
  setAddress(address);
  digitalWrite(WE, LOW);
  digitalWrite(DATA_0, ((data & 0x01) == 0) ? LOW : HIGH);
  digitalWrite(DATA_1, ((data & 0x02) == 0) ? LOW : HIGH);
  digitalWrite(DATA_2, ((data & 0x04) == 0) ? LOW : HIGH);
  digitalWrite(DATA_3, ((data & 0x08) == 0) ? LOW : HIGH);
  digitalWrite(DATA_4, ((data & 0x10) == 0) ? LOW : HIGH);
  digitalWrite(DATA_5, ((data & 0x20) == 0) ? LOW : HIGH);
  digitalWrite(DATA_6, ((data & 0x40) == 0) ? LOW : HIGH);
  digitalWrite(DATA_7, ((data & 0x80) == 0) ? LOW : HIGH);

  digitalWrite(WE, HIGH);
}

/**
 * 1バイト読み込み
 */
unsigned char readByte(unsigned long address) {
  unsigned char data = 0;

  setDataPinMode(INPUT);
  setAddress(address);
  digitalWrite(OE, LOW);
  data |= digitalRead(DATA_0) == HIGH ? 0x01 : 0;
  data |= digitalRead(DATA_1) == HIGH ? 0x02 : 0;
  data |= digitalRead(DATA_2) == HIGH ? 0x04 : 0;
  data |= digitalRead(DATA_3) == HIGH ? 0x08 : 0;
  data |= digitalRead(DATA_4) == HIGH ? 0x10 : 0;
  data |= digitalRead(DATA_5) == HIGH ? 0x20 : 0;
  data |= digitalRead(DATA_6) == HIGH ? 0x40 : 0;
  data |= digitalRead(DATA_7) == HIGH ? 0x80 : 0;
  digitalWrite(OE, HIGH);

  return data;
}

/**
 * 全消去
 */
void eraseAll() {
  // 電源を切って5秒待つ(5秒は適当)
  digitalWrite(VCC, LOW);
  delay(5000);
  digitalWrite(VCC, HIGH);
}

SRAMの制御の仕方はなんとなく分かったので、以上

balmychanbalmychan

さぼりすぎたので何かやらねばと思い、今日はソフト側の開発をすることにした。
以前BG表示したときは一部分しかやっていなかったので、今日は画面全体にBGを敷き詰めるところから。

しかし、8ビットコンピュータのプログラミングの壁にぶつかる。。。

下記のようなBG用のマップデータを用意して、これを単にPPUの$2000~$23C0に書き込んでいくだけなのだが

BGは横32x縦30=960あり、960回ループをする必要があるが、そもそもレジスタが8ビットの容量しか無いので、ループするにも255回までしかカウントできなかったり、相対アドレス指定するためのインデックスの値も255までしかできなかったり、もうめちゃくちゃハマった。

これを解決するには

  • 「ゼロページ」というレジスタと同じ様に扱えるRAM領域があり
  • 上記のゼロページ領域に下位1バイトと上位1バイトを入れておき、間接アドレッシングという指定方法でアドレス指定する

ということをやれば良いということが分かった。
しかし、このあたりを調べたり良い感じな書き方を模索するのにほぼ丸一日費やしてしまった...。
とりあえず8ビットCPUの場合8ビットを超えた計算やアドレスを扱うにはやや面倒なことをしなければならない(ただ、それ故にファミコンはこの時代の家庭用ゲーム機にしてはそこそこのパフォーマンスを出せたとかなんとか)

とにかくアセンブラを書くのは簡単なループだけでも一苦労...(あんまり慣れていないのもありますが)

そんなこんなあって、下記のようにBGを敷き詰めることができました。
(フォントや雲のキャラクタデータは某ゲームから拝借しました)

BGを敷き詰めたら実機でも良い感じに表示されるだろうと思い、実機でも起動してみました。↓

コードも貼っておきます(attr.binやbg.binなどの外部ファイルがないとアセンブルできませんが)

; ==============================================================================
; =====	Header =================================================================
; ==============================================================================

	; ヘッダー
	.inesprg 1 ;   - プログラムにいくつのバンクを使うか。今は1つ。
	.ineschr 1 ;   - グラフィックデータにいくつのバンクを使うか。今は1つ。
	.inesmir 1 ;   - 垂直ミラーリング
	.inesmap 0 ;   - マッパー。0番にする。

	.bank 1

; ==============================================================================
; =====	Zero-page RAM ==========================================================
; ==============================================================================

	.org $0000
MapAdr_L = $00		; 文字列下位アドレス
MapAdr_H = $01		; 文字列上位アドレス

; ==============================================================================
; =====	Zero-page RAM ==========================================================
; ==============================================================================

	; 割り込みの設定
	.org $FFFA
	.dw mainLoop	; VBlank割り込み
	.dw start		; リセット割り込み。起動時とリセットでstartに飛ぶ
	.dw 0			; ハードウェア割り込みとソフトウェア割り込み

	.bank 0			; バンク0

; ==============================================================================
; =====	General RAM ============================================================
; ==============================================================================

	.org $0300	 ; $0300から開始、スプライトDMAデータ配置
AppaLT_Y:     .db  0   ; アッパマン左上 Y座標
AppaLT_T:     .db  0   ; アッパマン左上 ナンバー
AppaLT_S:     .db  0   ; アッパマン左上 属性
AppaLT_X:     .db  0   ; アッパマン左上 X座標
AppaRT_Y:     .db  0   ; アッパマン右上 Y座標
AppaRT_T:     .db  0   ; アッパマン右上 ナンバー
AppaRT_S:     .db  0   ; アッパマン右上 属性
AppaRT_X:     .db  0   ; アッパマン右上 X座標
AppaLB_Y:     .db  0   ; アッパマン左下 Y座標
AppaLB_T:     .db  0   ; アッパマン左下 ナンバー
AppaLB_S:     .db  0   ; アッパマン左下 属性
AppaLB_X:     .db  0   ; アッパマン左下 X座標
AppaRB_Y:     .db  0   ; アッパマン右下 Y座標
AppaRB_T:     .db  0   ; アッパマン右下 ナンバー
AppaRB_S:     .db  0   ; アッパマン右下 属性
AppaRB_X:     .db  0   ; アッパマン右下 X座標

; ==============================================================================
; =====	Program ================================================================
; ==============================================================================

	.org $C000;
	
start:  
	lda $2002  ; VBlankが発生すると、$2002の7ビット目が1になる
	bpl start  ; bit7が0の間は、startラベルの位置に飛んでループして待つ

	; PPUコントロールレジスタ初期化
	lda #%00001000 
	sta $2000
	lda #%00000110		; 初期化中はスプライトとBGを表示OFFにする
	sta $2001

	; VRAMアドレスレジスタの$2006に、パレットのロード先のアドレス$3F00を指定する。
	lda #$3F
	sta $2006
	lda #$00
	sta $2006

	; パレットをロード
	jsr loadPal

	; ゼロページ初期化
	jsr initZeroPage

	; BGをクリア
	jsr clearBG0
	jsr clearBG1
	jsr clearBG2
	jsr clearBG3

	; 属性テーブルをクリア
	jsr clearAttr0
	jsr clearAttr1

	; BGを描画
	jsr renderBG

	; アッパマンの初期化
	jsr initAppa

	; PPUコントロールレジスタ2初期化
	lda #%00011110	; スプライトとBGの表示をONにする
	sta $2001

	; PPUコントロールレジスタ1の割り込み許可フラグを立てる
	lda #%10001000
	sta $2000

infinityLoop:
	jmp infinityLoop	; 今回は描画して終わりなので無限ループで良い

mainLoop:	; メインループ
	; スプライト描画(DMAを利用)
	lda #$3 ; スプライトデータは$0300から配置しているので、3をロード
	sta $4014
	
	; パッドI/Oレジスタの初期化
	lda #$01
	sta $4016
	lda #$00
	sta $4016

	; パッド入力チェック
	lda $4016 ; Aボタン
	lda $4016 ; Bボタン
	lda $4016 ; Selectボタン
	lda $4016 ; Startボタン
	
	; ↑ボタン
	lda $4016
	and #1
	bne UPKEYdown
UPKEYdownReturn:
	; ↓ボタン
	lda $4016
	and #1
	bne DOWNKEYdown

DOWNKEYdownReturn:
	; ←ボタン
	lda $4016
	and #1
	bne LEFTKEYdown

LEFTKEYdownReturn:
	; →ボタン
	lda $4016
	and #1
	bne RIGHTKEYdown

RIGHTKEYdownReturn:
	jmp NOTHINGdown 
	
UPKEYdown:
	dec AppaLT_Y
	dec AppaRT_Y
	dec AppaLB_Y
	dec AppaRB_Y
	jmp UPKEYdownReturn

DOWNKEYdown:
	inc AppaLT_Y
	inc AppaRT_Y
	inc AppaLB_Y
	inc AppaRB_Y
	jmp DOWNKEYdownReturn

LEFTKEYdown:
	dec AppaLT_X
	dec AppaRT_X
	dec AppaLB_X
	dec AppaRB_X
	jmp LEFTKEYdownReturn

RIGHTKEYdown:
	inc AppaLT_X
	inc AppaRT_X
	inc AppaLB_X
	inc AppaRB_X
	jmp RIGHTKEYdownReturn

NOTHINGdown
	rti

; ==============================================================================
; =====	サブルーチン ============================================================
; ==============================================================================

; =============================
; ネームテーブル0のクリア
; =============================
clearBG0:
	; ネームテーブル0クリア
	; ネームテーブルの$2000から
	lda #$20
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG0Sub
	sta $2007
	dex
	bne .clearBG0Sub
	ldx #240
	dey
	bne .clearBG0Sub
	rts

; =============================
; ネームテーブル1のクリア
; =============================
clearBG1:
	; ネームテーブル1クリア
	; ネームテーブルの$2400から
	lda #$24
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG1Sub
	sta $2007
	dex
	bne .clearBG1Sub
	ldx #240
	dey
	bne .clearBG1Sub
	rts

; =============================
; ネームテーブル2のクリア
; =============================
clearBG2:
	; ネームテーブル2クリア
	; ネームテーブルの$2800から
	lda #$24
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG2Sub
	sta $2007
	dex
	bne .clearBG2Sub
	ldx #240
	dey
	bne .clearBG2Sub
	rts

; =============================
; ネームテーブル3のクリア
; =============================
clearBG3:
	; ネームテーブル3クリア
	; ネームテーブルの$2800から
	lda #$2C
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG3Sub
	sta $2007
	dex
	bne .clearBG3Sub
	ldx #240
	dey
	bne .clearBG3Sub
	rts

; =============================
; 属性テーブル0のクリア
; =============================
clearAttr0
	; 属性テーブルの設定
	lda #$23
	sta $2006
	lda #$C0
	sta $2006
	ldx #0
.clearAttr0Sub:
	lda #0
	sta $2007
	inx
	cpx #64
	bne .clearAttr0Sub
	rts

; =============================
; 属性テーブル1のクリア
; =============================
clearAttr1
	; 属性テーブルの設定
	lda #$27
	sta $2006
	lda #$C0
	sta $2006
	ldx #0
.clearAttr1Sub:
	lda #0
	sta $2007
	inx
	cpx #64
	bne .clearAttr1Sub
	rts

; =============================
; BGの描画
; =============================
renderBG
	; $2000(ネームテーブル)に書き込む
	lda #$20
	sta $2006
	lda #$00
	sta $2006

	lda #low(BG_DATA)
	sta <MapAdr_L
	lda #high(BG_DATA)
	sta <MapAdr_H
	
	ldx #0
	ldy #0
.loadBG:
	lda [MapAdr_L], y
	sta $2007
	iny
	cpy #0
	bne .loadBG
.incrementHigh:
	inc <MapAdr_H
	ldy #0
	inx
	cpx #4
	bne .loadBG
;.finishBG:
;	; 属性テーブルの設定
;	lda #$23
;	sta $2006
;	lda #$C0
;	sta $2006
;	ldx #0
	
.loadAttrTable:
	lda ATTR_DATA, x
	sta $2007
	inx
	cpx #64
	bne .loadAttrTable
	rts

; =============================
; アッパマンの初期化
; =============================
initAppa:
	; X座標
	lda X_Pos_Init
	sta AppaLT_X
	sta AppaLB_X
	adc #7
	sta AppaRT_X
	sta AppaRB_X
	; Y座標
	lda Y_Pos_Init
	sta AppaLT_Y
	sta AppaRT_Y
	adc #7
	sta AppaLB_Y
	sta AppaRB_Y
	; 属性
	lda #%00000000
	sta AppaLT_S
	lda #%00000000
	sta AppaRT_S
	lda #%00000000
	sta AppaLB_S
	lda #%00000000
	sta AppaRB_S
	; タイル番号
	lda #$00
	sta AppaLT_T
	lda #$01
	sta AppaRT_T
	lda #$10
	sta AppaLB_T
	lda #$11
	sta AppaRB_T
	rts

; =============================
; パレットの読み込み
; =============================
loadPal
	ldx #0
.loadPalSub:
	lda tilepal, x
	sta $2007
	inx
	cpx #32
	bne .loadPalSub
	rts

; =============================
; ゼロページ初期化
; =============================
initZeroPage:
	ldx #$00
.initZeroPageSub:
	sta <$00, x
	inx
	bne .initZeroPageSub
	rts

; ==============================================================================
; =====	Data ===================================================================
; ==============================================================================

; =============================
; 初期データ
; =============================
X_Pos_Init	.db 20
Y_Pos_Init	.db 174

; =============================
; マップデータ
; =============================
BG_DATA: .incbin "bg.bin"

; =============================
; 属性データ
; =============================
ATTR_DATA: .incbin "attr.bin"

; =============================
; パレットデータ
; =============================
tilepal: .incbin "giko.pal" ; パレットをincludeする

; ==============================================================================
; =====	CHR-ROM Data ===========================================================
; ==============================================================================

	.bank 2
	.org $0000

	.incbin "giko.bkg"  ; 背景データのバイナリファイルをincludeする
	.incbin "giko.spr"  ; スプライトデータのバイナリィファイルをincludeする

; ==============================================================================
; =====	End ====================================================================
; ==============================================================================

このコードが先程説明したあたり

; =============================
; BGの描画
; =============================
renderBG
    ; $2000(ネームテーブル)に書き込む
    lda #$20
    sta $2006
    lda #$00
    sta $2006
    lda #low(BG_DATA)
    sta <MapAdr_L
    lda #high(BG_DATA)
    sta <MapAdr_H
    
    ldx #0
    ldy #0
.loadBG:
    lda [MapAdr_L], y
    sta $2007
    iny
    cpy #0
    bne .loadBG
.incrementHigh:
    inc <MapAdr_H
    ldy #0
    inx
    cpx #4
    bne .loadBG

MapAdr_Lにアドレスの下位バイト、MapAdr_Hにアドレスの上位バイトが入っており、
lda [MapAdr_L], yで、上位バイト+下位バイト+レジスタYの値のアドレスの値がレジスタAに入るといった感じ。

BG用とスプライト用のキャラクタデータはこんな感じ(上がBG、下がスプライト)

balmychanbalmychan

マッパー1はまだハードルが高いので、まずはバンク切り替えについて学ぶ。

マッパー1よりもう少しシンプルと思われる、PRG-ROMのバンク切り替えのあるマッパー2について調査中

マッパー2のカセット自体は、いつものNES Cart Databaseで調べて、ボンバーキングというやつにした。(このゲーム恥ずかしながら知らなかった...ボンバーマンのシリアス系なストーリーという感じ)

殻割りすると、基板はこんな感じ


右上になにかICが2つ付いている。で、マッパー2に関する情報を漁ったものの、あまりすぐに理解できず、この日は割と早めに断念

balmychanbalmychan

以前再発注したマッパー0用の基板が届いたので、その件と、引き続きマッパー2について調べていた。
マッパー0基板はこんな感じ↓


で、こんな感じで垂直ミラーリングになるよう半田付けして↓


無事起動しました。

ただ、マッパー0はマッパー0でも、32KBは使えず16KB固定の基板になってしまっているので(A15を電源に繋いでしまっているので)、ここも切り替えられるようにしておけばなーと後悔

あと、スイッチも付けてみた↓(手元にこんなスイッチしかなくて、こんな感じに...)

マッパー0基板については以上。

balmychanbalmychan

マッパー2については

先ほどのボンバーキングの基板をひたすら眺め続け、そして

この辺の記事を眺め続けて、少し分かってきたことがありました。

特に参考になったのがこの画像

まとめきれないので、とにかく箇条書きで書きます

  • マッパー2は、PRG-ROMが128KB(または256KB)で、CHR-ROMはなく8KBのCHR-RAMの構成
  • PRG-ROMは、16KBの領域でバンク切り替えを行い、合計8バンク使うことができる
  • PRG-ROMのA14, A15, A16に対して、OR回路が3つ繋がっている(HC32という汎用IC)
  • その手前にはHC161というICが繋がっていて、更にそのHC161にはD0, D1, D2が繋がっている
  • ボンバーキングの基板の一番右上にあるのがHC32という汎用ICで、OR回路が3つ入っている。その左隣がHC161
  • HC161はカウンターICと言われるもので、クロックの制御に使われるものらしい?(詳しくはまた今度調べる。「カウンタ (電子回路)」)
  • 「ファミリーコンピューター ROMカセット マッパー2、94」によるとHC161はカウンタ機能を使用せずデータセットのみになりHC161のA~Dのデータ入力がそのままQA~QDに出力されます。 なので、カウンター機能自体は使っていない様子。単に/ROMSELが0になったら、D0, D1, D2の値を保持させているだけ?の様子(カウンターの中身はフリップフロップで値を保持する仕組みになっていると思われる)
  • カセット端子には/ROMSELという端子がある。これについてはよく分かっていなかったが、$8000にアクセスを行おうとすると、LOWになるらしい。おそらくバンク切り替えのための制御用の端子なのかもしれない(もう少し分かってきたらまた書きます)
  • Memory-Map of NES マッパー#2によると バンクの切り替えは$8000-$FFFFのカートリッジ側メモリ空間にバンク番号を書き込むと $8000-$BFFFの16KBが指定したバンクに切り替わる仕組みです。 とのこと
  • つまり、$8000に書き込もうとすると、/ROMSELがLOWになり、書き込もうとした値のD0~D2(3ビットあるので、バンク番号0~7が書き込める)がHC161に保持され、それがPRG-ROMのA14~A16を変化させ、読み取るアドレスを変換するという感じなのだろう
  • で、もう一点注意点は「ファミリーコンピューター ROMカセット マッパー2、94」バンクキーデータとROMデータの2つのデータバスが衝突する様になっています。...中略...その為、バンクキーデータと書き込んだアドレスのROMデータとデータ(Bit2-0)が同じでないといけません の部分と、その後の魔界村の例。つまり単に適当に8000以降のC000などに好きなバンク番号を書き込めばいいというわけではなく、そのアドレスにバンク番号と一致するデータを入れて置かなければいけないということ(なぜデータバスが衝突したら互いの信号を一致させないといけないのか、よくまだ分かっていない)
  • HC32のOR回路の方を見ると分かるが、例えばD0~D2に001が渡された(=バンク番号が1)場合はA16, A15がLOWでA14がHIGHとなるので、$4000からデータが読み取られる。バンク番号が2の場合はA16とA14がLOWでA15がHIGHとなるので、8000からデータが読み取られる。バンク番号が3の場合はA16がLOWでA15とA14がHIGHとなるのでC000からデータが読み取られる、という感じになっている。
  • ※CPUのA14と3つのOR回路の片側が繋がっているのはちょっとまだ良くわかっていない。またA14が1($C000をアクセス)した場合はバンク#07を選択されます($C000-$FFFFはバンク#07に固定)がヒントと思うが、まだ釈然とせず

色々長々と書きましたが、要するに

「特定のアドレス領域への書き込み」という動作によってHC161の出力する値を変えて、PRG-ROMから読み取る際のアドレスの位置を変える

という感じです。

このバンク切り替えの仕組みによって、おそらくマッパー1の拡張RAM領域への書き込みも実現しているはずです。ちょっとだけ近づいたかも...?
引き続き、マッパー2について調査を続ける

balmychanbalmychan

引き続きマッパー2について

マッパー2のサンプルプログラムを作って試そうとしているが、その前にマッパー2基板側の準備をする。当然ボンバーキングに犠牲になってもらう。

いつものように、PRG-ROMを外してピンヘッダーを付ける

一応外した状態でテスト起動(当然元のROMを繋いでいるので起動します)


で、以前作った既存のファミコン基板のROM部分にFlashROMを取り付けれる基板を取り付けます。
また、マッパー0の時のROM(16KB)と、マッパー2のROM(128KB)だと、下記のようなピンアサインの差があります(/OEがA16になっている)

そのため、取り付ける基板の配線を少し変える必要があったので、パターンカットやら配線やらをしなおしました。

で、こんな感じで取り付け↓

そして、128KBのボンバーキングのROMイメージをFlashROMに書き込んでみると...
(ちなみにArduinoで書き込むのに16KBずつ配列を分けたりしなければならず地味に苦労した...)

無事起動しました

CHR-RAMになっていると、キャラクタデータもすべてPRG-ROMに書き込めばいいだけなので、楽ですね。また、既存のカセットの基板を流用するときも、PRG-ROMだけ外せばよいというのも楽ちんです。

ということで、マッパー2を実機で試す準備は整ったので、次はマッパー2を試す(≒バンク切り替えを試す)サンプルプログラムを書いていこうと思います。

balmychanbalmychan

PRG-ROMが複数バンクあるサンプルプログラムを書いている

今まであまり気にしていなかったが、

  • .inesprgや.ineschrヘッダー部
  • .bank n, .org $xxxx

について挙動を確かめていた。
nesのヘッダーに当たるinesprgは、PRG-ROMのバンク数を表す。これは16KB単位で、例えば2を指定したら16KBx2で32KB分、PRG-ROMがあることを示す
一方、データやプログラムを配置するアドレス位置を決める.bankや.orgは、ちょっと想定と違う動きだった。
.bankで指定するバンク番号は8KB単位となっているようで、例えば

.bank 0
.org $0000
.db 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255

と指定したら、バンクが0(つまり$0000)にFF FF FF ...というデータが配置される。
で、仮に

.bank 0
.org $2000
.db 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255

こんな感じで、バンク0なのに8KBを超えて(.org $2000)データを配置しても、バンクを超えているので無視された。
$2000に配置したいなら

.bank 1
.org $0000
.db 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255

こう書く。.orgの方は、そのバンク位置の中のアドレスを指定するようだ。
以下のコードは、PRG-ROMが2バンク(32KB)で、それぞれ8KBずつにデータを配置した形。

; ==============================================================================
; =====	Header =================================================================
; ==============================================================================

	; ヘッダー
	.inesprg 2 ;   - PRG-ROM バンクは4つ
	.ineschr 0 ;   - CHR-RAMなので0
	.inesmir 1 ;   - 垂直ミラーリング
	.inesmap 2 ;   - マッパー2

	; PRG-ROM 1-1
	.bank 0
	.org $0000
	.db 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255

	; PRG-ROM 1-2
	.bank 1
	.org $0000
	.db 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255

	; PRG-ROM 2-1
	.bank 2
	.org $0000
	.db 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255

	; PRG-ROM 2-3
	.bank 3
	.org $0000
	.db 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255

マッパー2の場合は最終バンク($C000 ~ $FFFF)が固定となるので、そこでメインとなるプログラムを配置して、$0000 ~ $BFFFにバンクを切り替えて使うプログラムデータやキャラクタデータなどを配置すれば、うまく動くはず...?

ちょろちょろやってみてはいるもののうまくいかない...今日は断念。

balmychanbalmychan

eBayで購入してたFRAM(FM1608)が届いた。

過去ログ→https://zenn.dev/link/comments/fd97bc2ba3103e

とりあえず書き込みと、電源切ってもデータ消えないことを確認してみた。

書き込みとダンプの様子は↓

書き込んだあとに電源抜いて、その後もう一度ダンプしてもデータ(Hello World)が残っていることを確認。

今日はちょっと仕事で帰りが遅かったので、このくらいで

balmychanbalmychan

引き続きマッパー2用のプログラムを出力するのにどう配置すればいいのか、色々記事を探しながら検討中...(あまり進捗なし)

balmychanbalmychan

相変わらず、マッパー2のプログラムを書き出してもこんな感じでエラーが出てしまう状況。

このまま情報漁っても情報がそもそも少ないので、落ち着いてじっくり考えた。

http://hp.vector.co.jp/authors/VA042397/nes/adrmap.html

上のサイトを見ると分かるように、CPUから見たプログラムROMは、大きく2つ領域がある。

  • 8000~BFFFのプログラムROM LOWという領域(ここは、バンク切り替えで切り替わる)
  • C000~FFFFのプラグラムROM HIGHという領域(ここは、常に最終バンクが読み込まれる)

なぜ、$C000以降への読み取りが、最終バンクになるかというのを改めて考えてみると、以前話した

※CPUのA14と3つのOR回路の片側が繋がっているのはちょっとまだ良くわかっていない。またA14が1($C000をアクセス)した場合はバンク#07を選択されます($C000-$FFFFはバンク#07に固定)がヒントと思うが、まだ釈然とせず

に関わってくる。改めて下の図の、赤丸のところを見ると

A14がHIGHとなるアドレス $C000(100_000_000_000_000) 以降を読み込もうとすると、上図の配線の関係で、A14~A16が常にHIGHとなって、実際はROMの $1C000(11_100_000_000_000_000) から読み取られる。つまり最終バンクの領域が読み取られることになる。

それゆえ、プログラムROM HIGHの領域は、常に最終バンク固定となるのだろう。

CPUがPRG-ROMのメモリ空間にアクセスした際、

  • $8000 ~ $BFFFは、HC161で制御された、バンク番号に沿った形でROMから読み取られ
  • $C000 ~ $FFFFは、A14~A16は常にHIGHなのでROMの$1C000から読み取られる
    といった具合だ。

この整理を元に、下表を作った。

バンク番号に関する列が2つあるのが不思議に思うかもしれないが、PRG-ROMのバンク番号(0~7)と、nesasmに用意されている.bank疑似命令はそれぞれサイズが違う。(PRG-ROMのバンク領域は16KB単位だが、.bankの指す領域は8KB単位なっている。おそらくCHR-ROMのバンク領域は8KB単位なので、同じ.bankでまかなえるようにこうしているのか?)

※蛇足ですが、疑似命令というのは、アセンブルする際にマシン語には落とされないが、擬似的に命令となっているものを指すよう。Cのプリプロセッサと同じもんかと思ったが、「擬似命令とプリプロセッサ命令の違い」を見るとわかりやすい

以上のことから、最終バンクであるバンク7($1C000)にエントリーポイントとなるプログラムを書くのが正しいと思われるので、ここに書き出してみることにした。

すると...

エラーは出なくなった!

ただ、CHR-RAMへの書き込み方がまだ分からないので、絵は出せていない。
次は、バンク0あたりにキャラクタデータを書き込んでおいて、それを実行時にCHR-RAMへ転送する(書き込む)のを試す。

balmychanbalmychan

その後、そもそもプログラムがなにも実行されていないだけなことに気づいた。

ちなみにPRG-ROMのFFFA~FFFFは特殊な領域になっており、ファミコンの各種割り込みが発生した場合に、ここに書かれたアドレスに応じて処理が呼び出されることになっている。それぞれ

  • $FFFA ... VBlank割り込み
  • $FFFC ... 電源ONやリセットボタンの割り込み
  • $FFFE ... ハードウェア割り込みとソフトウェア割り込み

という感じ。

なので今回 $FFFC にあたるのは、固定バンク(最終バンク)になるので、最終バンクから見た$FFFCにあたる、 $1FFFC に書き込んでおくのが正しいと思われるので、そうした。
が、全然割り込み処理に飛んでくれない。
吐き出したROMデータは正しい(と思っている)はずなんですが、なぜだか分からず...

balmychanbalmychan

が、全然割り込み処理に飛んでくれない。

VBlank割り込みに指定していたアドレスが間違っていただけだった。
例えば

	.bank 15

	; 割り込みの設定
	.org $FFFA
	.dw mainLoop	; VBlank割り込み
	.dw start		; リセット割り込み。起動時とリセットでstartに飛ぶ
	.dw 0			; ハードウェア割り込みとソフトウェア割り込み

	.bank 14
	.org $C000 ; ここが、$0000にしてしまっていた。

start:
	; ...

mainLoop:
	; ...

このようになっていた。ここで重要になるのが

  • .bank疑似命令
  • .org疑似命令

です。
.bankについては、例えば

.bank 1

.db 00, 01, 02

とやった場合、実際に吐き出されるバイナリには、bank 1からになるので

$2000 00 01 02 ...

という形でバイナリが生成される。これは実際にROMのどの領域に吐き出すべきかを制御するための疑似命令です。
一方.org疑似命令の方は、startなどのラベルを付けた場合に.orgで指定されたアドレスを基準としたアドレスがラベルが示すものとなる。
先程のプログラムを例に取ると、.org C000とした場合、C000が基準となるので、startラベルは$C000を示す。
仮に.orgを$0000とした場合は、$0000が基準となるので、startラベルは0000を示してしまう。なので、このラベルをFFFAや$FFFCに書き込んでも、$0000に飛んでしまって、処理が実行されていなかった。
.orgについては、単にラベルを付ける場合の基準となるアドレス、というだけかなと思われる。

balmychanbalmychan

これを踏まえた上で書き直したところ、成功しました。
以下の画像は、バンク0にあるキャラクタデータを使った画面

で、下の画像は、バンク1にあるキャラクタデータを使った画面(若干キャラクターやブロック画像が変わっているのが分かるかと思います)

とりあえずこれで、バンク切り替えが、ちょっと分かってきました。(多分これでこのプログラムをFlashROMに書き込めば、マッパー2のカセット基板で起動するはず)

いつもどおりコードも貼っておきます。

; ==============================================================================
; =====	Header =================================================================
; ==============================================================================

	; ヘッダー
	.inesprg 8 ;   - プログラムにいくつのバンクを使うか。今は1つ。
	.ineschr 0 ;   - グラフィックデータにいくつのバンクを使うか。今は1つ。
	.inesmir 1 ;   - 垂直ミラーリング
	.inesmap 2 ;   - マッパー2

; ==============================================================================
; =====	Zero-page RAM ==========================================================
; ==============================================================================

	.bank 15
MapAdr_L = $00		; 文字列下位アドレス
MapAdr_H = $01		; 文字列上位アドレス

; ==============================================================================
; =====	General RAM ============================================================
; ==============================================================================

	.org $0300	 ; $0300から開始、スプライトDMAデータ配置
AppaLT_Y:     .db  0   ; アッパマン左上 Y座標
AppaLT_T:     .db  0   ; アッパマン左上 ナンバー
AppaLT_S:     .db  0   ; アッパマン左上 属性
AppaLT_X:     .db  0   ; アッパマン左上 X座標
AppaRT_Y:     .db  0   ; アッパマン右上 Y座標
AppaRT_T:     .db  0   ; アッパマン右上 ナンバー
AppaRT_S:     .db  0   ; アッパマン右上 属性
AppaRT_X:     .db  0   ; アッパマン右上 X座標
AppaLB_Y:     .db  0   ; アッパマン左下 Y座標
AppaLB_T:     .db  0   ; アッパマン左下 ナンバー
AppaLB_S:     .db  0   ; アッパマン左下 属性
AppaLB_X:     .db  0   ; アッパマン左下 X座標
AppaRB_Y:     .db  0   ; アッパマン右下 Y座標
AppaRB_T:     .db  0   ; アッパマン右下 ナンバー
AppaRB_S:     .db  0   ; アッパマン右下 属性
AppaRB_X:     .db  0   ; アッパマン右下 X座標

; ==============================================================================
; =====	割り込み ===============================================================
; ==============================================================================

	.bank 15
	
	; バンク番号を書き込み
	.org $FFF0
	.db 00, 01, 02, 03, 04, 05, 06, 07

	; 割り込みの設定
	.org $FFFA
	.dw mainLoop	; VBlank割り込み
	.dw start		; リセット割り込み。起動時とリセットでstartに飛ぶ
	.dw 0			; ハードウェア割り込みとソフトウェア割り込み

; ==============================================================================
; =====	Program ================================================================
; ==============================================================================

	.bank 14
	.org $C000;

start:
	lda $2002  ; VBlankが発生すると、$2002の7ビット目が1になる
	bpl start  ; bit7が0の間は、startラベルの位置に飛んでループして待つ

	; PPUコントロールレジスタ初期化
	lda #%00001000 
	sta $2000
	lda #%00000110		; 初期化中はスプライトとBGを表示OFFにする
	sta $2001

	; VRAMアドレスレジスタの$2006に、パレットのロード先のアドレス$3F00を指定する。
	lda #$3F
	sta $2006
	lda #$00
	sta $2006

	; パレットをロード
	jsr loadPal

	; ゼロページ初期化
	jsr initZeroPage

	; BGをクリア
	jsr clearBG0
	jsr clearBG1
	jsr clearBG2
	jsr clearBG3

	; 属性テーブルをクリア
	jsr clearAttr0
	jsr clearAttr1

	; バンク0か1を使う
	jsr switchToBank0
	;jsr switchToBank1

	; CHR-RAMに書き込み
	jsr writeToCHRRAM

	; BGを描画
	jsr renderBG

	; アッパマンの初期化
	jsr initAppa

	; PPUコントロールレジスタ2初期化
	lda #%00011110	; スプライトとBGの表示をONにする
	sta $2001

	; PPUコントロールレジスタ1の割り込み許可フラグを立てる
	lda #%10001000
	sta $2000

infinityLoop:
	jmp infinityLoop	; 今回は描画して終わりなので無限ループで良い

mainLoop:	; メインループ
	; スプライト描画(DMAを利用)
	lda #$3 ; スプライトデータは$0300から配置しているので、3をロード
	sta $4014
	
	; パッドI/Oレジスタの初期化
	lda #$01
	sta $4016
	lda #$00
	sta $4016

	; パッド入力チェック
	lda $4016 ; Aボタン
	lda $4016 ; Bボタン
	lda $4016 ; Selectボタン
	lda $4016 ; Startボタン
	
	; ↑ボタン
	lda $4016
	and #1
	bne UPKEYdown
UPKEYdownReturn:
	; ↓ボタン
	lda $4016
	and #1
	bne DOWNKEYdown

DOWNKEYdownReturn:
	; ←ボタン
	lda $4016
	and #1
	bne LEFTKEYdown

LEFTKEYdownReturn:
	; →ボタン
	lda $4016
	and #1
	bne RIGHTKEYdown

RIGHTKEYdownReturn:
	jmp NOTHINGdown 
	
UPKEYdown:
	dec AppaLT_Y
	dec AppaRT_Y
	dec AppaLB_Y
	dec AppaRB_Y
	jmp UPKEYdownReturn

DOWNKEYdown:
	inc AppaLT_Y
	inc AppaRT_Y
	inc AppaLB_Y
	inc AppaRB_Y
	jmp DOWNKEYdownReturn

LEFTKEYdown:
	dec AppaLT_X
	dec AppaRT_X
	dec AppaLB_X
	dec AppaRB_X
	jmp LEFTKEYdownReturn

RIGHTKEYdown:
	inc AppaLT_X
	inc AppaRT_X
	inc AppaLB_X
	inc AppaRB_X
	jmp RIGHTKEYdownReturn

NOTHINGdown
	rti

; ==============================================================================
; =====	サブルーチン ============================================================
; ==============================================================================

; =============================
; ネームテーブル0のクリア
; =============================
clearBG0:
	; ネームテーブル0クリア
	; ネームテーブルの$2000から
	lda #$20
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG0Sub
	sta $2007
	dex
	bne .clearBG0Sub
	ldx #240
	dey
	bne .clearBG0Sub
	rts

; =============================
; ネームテーブル1のクリア
; =============================
clearBG1:
	; ネームテーブル1クリア
	; ネームテーブルの$2400から
	lda #$24
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG1Sub
	sta $2007
	dex
	bne .clearBG1Sub
	ldx #240
	dey
	bne .clearBG1Sub
	rts

; =============================
; ネームテーブル2のクリア
; =============================
clearBG2:
	; ネームテーブル2クリア
	; ネームテーブルの$2800から
	lda #$24
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG2Sub
	sta $2007
	dex
	bne .clearBG2Sub
	ldx #240
	dey
	bne .clearBG2Sub
	rts

; =============================
; ネームテーブル3のクリア
; =============================
clearBG3:
	; ネームテーブル3クリア
	; ネームテーブルの$2800から
	lda #$2C
	sta $2006
	lda #$00
	sta $2006
	lda #$00        ; 0番(透明)
	ldx #240		; 240回繰り返す
	ldy #4			; それを4回、計960回繰り返す
.clearBG3Sub
	sta $2007
	dex
	bne .clearBG3Sub
	ldx #240
	dey
	bne .clearBG3Sub
	rts

; =============================
; 属性テーブル0のクリア
; =============================
clearAttr0
	; 属性テーブルの設定
	lda #$23
	sta $2006
	lda #$C0
	sta $2006
	ldx #0
.clearAttr0Sub:
	lda #0
	sta $2007
	inx
	cpx #64
	bne .clearAttr0Sub
	rts

; =============================
; 属性テーブル1のクリア
; =============================
clearAttr1
	; 属性テーブルの設定
	lda #$27
	sta $2006
	lda #$C0
	sta $2006
	ldx #0
.clearAttr1Sub:
	lda #0
	sta $2007
	inx
	cpx #64
	bne .clearAttr1Sub
	rts

; =============================
; BGの描画
; =============================
renderBG
	; $2000(ネームテーブル)に書き込む
	lda #$20
	sta $2006
	lda #$00
	sta $2006

	lda #low(BG_DATA)
	sta <MapAdr_L
	lda #high(BG_DATA)
	sta <MapAdr_H
	
	ldx #0
	ldy #0
.loadBG:
	lda [MapAdr_L], y
	sta $2007
	iny
	cpy #0
	bne .loadBG
.incrementHigh:
	inc <MapAdr_H
	ldy #0
	inx
	cpx #4
	bne .loadBG
;.finishBG:
;	; 属性テーブルの設定
;	lda #$23
;	sta $2006
;	lda #$C0
;	sta $2006
;	ldx #0
	
.loadAttrTable:
	lda ATTR_DATA, x
	sta $2007
	inx
	cpx #64
	bne .loadAttrTable
	rts

; =============================
; アッパマンの初期化
; =============================
initAppa:
	; X座標
	lda X_Pos_Init
	sta AppaLT_X
	sta AppaLB_X
	adc #7
	sta AppaRT_X
	sta AppaRB_X
	; Y座標
	lda Y_Pos_Init
	sta AppaLT_Y
	sta AppaRT_Y
	adc #7
	sta AppaLB_Y
	sta AppaRB_Y
	; 属性
	lda #%00000000
	sta AppaLT_S
	lda #%00000000
	sta AppaRT_S
	lda #%00000000
	sta AppaLB_S
	lda #%00000000
	sta AppaRB_S
	; タイル番号
	lda #$00
	sta AppaLT_T
	lda #$01
	sta AppaRT_T
	lda #$10
	sta AppaLB_T
	lda #$11
	sta AppaRB_T
	rts

; =============================
; パレットの読み込み
; =============================
loadPal
	ldx #0
.loadPalSub:
	lda tilepal, x
	sta $2007
	inx
	cpx #32
	bne .loadPalSub
	rts

; =============================
; ゼロページ初期化
; =============================
initZeroPage:
	ldx #$00
.initZeroPageSub:
	sta <$00, x
	inx
	bne .initZeroPageSub
	rts

; =============================
; バンク0に切り替え
; =============================

switchToBank0:
	lda #0
	sta $C000
	rts

switchToBank1:
	lda #1
	sta $C000
	rts

; =============================
; 現在のバンクをCHR-RAMに書き込み
; =============================

writeToCHRRAM
	; バンクROMの先頭アドレスをMapAdrに入れておく
	lda #low($8000)
	sta <MapAdr_L
	lda #high($8000)
	sta <MapAdr_H

	; PPU側の$0000に書き込み準備
	lda #$00
	sta $2006
	lda #$00
	sta $2006

	ldx #0
	ldy #0
.writeToCHRRAM:
	lda [MapAdr_L], y
	sta $2007
	iny
	cpy #0
	bne .writeToCHRRAM
	; 高位アドレスをインクリメントしてまた戻る
	inc <MapAdr_H
	ldy #0
	inx
	cpx #32
	bne .writeToCHRRAM
	rts

; ==============================================================================
; =====	Data ===================================================================
; ==============================================================================

; =============================
; 初期データ
; =============================
X_Pos_Init	.db 20
Y_Pos_Init	.db 174

; =============================
; マップデータ
; =============================
BG_DATA: .incbin "bg.bin"

; =============================
; 属性データ
; =============================
ATTR_DATA: .incbin "attr.bin"

; =============================
; パレットデータ
; =============================
tilepal: .incbin "giko.pal" ; パレットをincludeする

; ==============================================================================
; =====	CHR-ROM Data ===========================================================
; ==============================================================================

	.bank 0
	.org $0000

	.incbin "giko.bkg"  ; 背景データのバイナリファイルをincludeする
	.incbin "giko.spr"  ; スプライトデータのバイナリィファイルをincludeする

	.bank 2
	.org $0000

	.incbin "giko2.bkg"  ; 背景データのバイナリファイルをincludeする
	.incbin "giko2.spr"  ; スプライトデータのバイナリィファイルをincludeする

; ==============================================================================
; =====	End ====================================================================
; ==============================================================================

ちなみに

ただ、CHR-RAMへの書き込み方がまだ分からないので、絵は出せていない。
次は、バンク0あたりにキャラクタデータを書き込んでおいて、それを実行時にCHR-RAMへ転送する(書き込む)のを試す。

については、単にPPUとやり取りするI/Oレジスタで、スプライト用キャラクタデータやBG用キャラクタデータを置く領域(PPU側の$0000~$1FFF)に、書き込みするだけで大丈夫だった。

※こんな感じ

writeToCHRRAM
	; バンクROMの先頭アドレスをMapAdrに入れておく
	lda #low($8000)
	sta <MapAdr_L
	lda #high($8000)
	sta <MapAdr_H

	; PPU側の$0000に書き込み準備
	lda #$00
	sta $2006
	lda #$00
	sta $2006

	ldx #0
	ldy #0
.writeToCHRRAM:
	lda [MapAdr_L], y
	sta $2007
	iny
	cpy #0
	bne .writeToCHRRAM
	; 上位アドレスをインクリメントしてまた戻る
	inc <MapAdr_H
	ldy #0
	inx
	cpx #32 ; 256byte x 32回 = 8KB分繰り返す
	bne .writeToCHRRAM
	rts

マッパー2はもう大体いいかなと思いますので、そろそろ問題のマッパー1(バッテリーバックアップ拡張RAM付き)に取り組んでいきたいなと思います。

balmychanbalmychan

マッパー1に関するこのへんの↓

http://www43.tok2.com/home/cmpslv/Famic/Fcmp1.htm

この画像を眺めて終わった

また、まずは理解を深める?ために、マッパー1のドラクエ3のROMを外して、FlashROMに置き換えるいつもの作業から行おうと思います。
しかし、以前のPRG-ROMとは違い、32ピン(これまでは28ピン)あるので、これまで使ってた下駄は使えないので、どうしよーかなーと悩み中。

ピンアサインは、これまでのと比べてこんな感じ↓

あと、ついにハンダシュッ太郎NEOをポチった。(ROM外すときにヒートガン使うと抵抗などの余計なものも外れて面倒なので)

balmychanbalmychan

以前eBayで購入していたカセットのケースと、はんだシュッたろうが届いた

これ↓

で、マッパー1のカセット(ドラクエ3)のPRG-ROMの載せ替えを行っていた

こんな感じ↓


256KBもあるので、Arduinoを使うと16回に分けて書き込みをしないとならず、一苦労しつつもFlashROMにドラクエ3のROMを書き込み、起動してみたものの

配線が悪いのか、うまくいかない...朝方近くまで見直していたが、まだ原因は判明せず。

下駄をつける前に、以前のマッパー0のときのように、ジャンパワイヤーで一つひとつ配線して、正しい配線を模索していこうと思います

balmychanbalmychan

FlashROM版でドラクエ3を起動するために悪戦苦闘

FlashROMへの書き込み自体一部ミスっているところがあったので、せこせこ書き直して(原因は不明...Arduinoで128KBずつ転送して書き込んでいたのだが、スケッチのサイズが大きくなるとうまく書き込めない特定のアドレス領域がある謎の問題にかなり悩まれた。(色々あって、配線も含め3~4時間費やした)
配線については、一つ一つ丁寧に繋いでいった。配線のポイントは

  • DIP側31 PGMには+5Vを繋ぐ
  • PLCC側31 /RESETと29 /WEに忘れずHIGHを繋いでおく
  • DIP側30 NCは何にも繋がない

というところ

こんな感じで配線して

無事起動!!

しかし、以前の下駄のときとなにか配線に違いがあったようには感じないので、もしかしたら単にFlashROMへの書き込みが失敗していたのが原因だったのかも。

ということで気を取り直してまた下駄版で試した

こうして

こう(起動した良かった)

ちょっと原因が100%確信持てないですが、結局FlashROMへの書き込みがだめだったのが原因かも。(最終バンクの書き込みは失敗してなかったので、オープニングくらい起動してもいいかなと思ったんですが、結局どのバンクがプログラムに使っているかは分からないので、そういうわけでもなかったのかも)

ということで、これでマッパー1用のソフトを実機で試すこともできるようになりました。(つまり、バッテリーバックアップ拡張RAMを実機で試せる)
マッパー1用の専用下駄を作ることもできそうなので、それはまた別途KiCadで作りたいと思います。

balmychanbalmychan

バッテリーバックアップRAMを実機で試してみました

プログラム的にはとても簡単で、CPUのアドレス空間の$6000~$7FFFがバックアップRAMに繋がっています。ここに書き込みをするだけです。
試しに、いつものアッパマンのプログラムに、Aボタンを押したら座標位置をバックアップRAMに保存するようにしてみました。

以下、プログラムの一部抜粋です。saveAppaではゼロページにある座標の値を$6000に書き込んでいます。loadAppaはその逆ですね(ソフト起動時にロードします)

; =============================
; バックアップRAM
; =============================

 	.org $6000	 ; バックアップRAM領域
SAVE_AppaLT_Y:     .db  0   ; アッパマン左上 Y座標
SAVE_AppaLT_X:     .db  0   ; アッパマン左上 X座標

; =============================
; General RAM
; =============================

	.org $0300	 ; $0300から開始、スプライトDMAデータ配置
AppaLT_Y:     .db  0   ; アッパマン左上 Y座標
AppaLT_T:     .db  0   ; アッパマン左上 ナンバー
AppaLT_S:     .db  0   ; アッパマン左上 属性
AppaLT_X:     .db  0   ; アッパマン左上 X座標
AppaRT_Y:     .db  0   ; アッパマン右上 Y座標
AppaRT_T:     .db  0   ; アッパマン右上 ナンバー
AppaRT_S:     .db  0   ; アッパマン右上 属性
AppaRT_X:     .db  0   ; アッパマン右上 X座標
AppaLB_Y:     .db  0   ; アッパマン左下 Y座標
AppaLB_T:     .db  0   ; アッパマン左下 ナンバー
AppaLB_S:     .db  0   ; アッパマン左下 属性
AppaLB_X:     .db  0   ; アッパマン左下 X座標
AppaRB_Y:     .db  0   ; アッパマン右下 Y座標
AppaRB_T:     .db  0   ; アッパマン右下 ナンバー
AppaRB_S:     .db  0   ; アッパマン右下 属性
AppaRB_X:     .db  0   ; アッパマン右下 X座標

; 中略

; =============================
; 座標の保存
; =============================
saveAppa:
	lda AppaLT_Y
	sta SAVE_AppaLT_Y
	lda AppaLT_X
	sta SAVE_AppaLT_X
	rts

; =============================
; 座標の保存
; =============================
loadAppa:
	; X座標
	lda SAVE_AppaLT_X
	sta AppaLT_X
	sta AppaLB_X
	adc #7
	sta AppaRT_X
	sta AppaRB_X
	; Y座標
	lda SAVE_AppaLT_Y
	sta AppaLT_Y
	sta AppaRT_Y
	adc #7
	sta AppaLB_Y
	sta AppaRB_Y
	rts

↓実機で動かした様子
https://youtu.be/TCwRh5nGSK8

Aボタンを押したら座標をバッテリーバックアップRAMに保存するようにした、Aボタン押下後はリセットボタンを押しても位置が復元されることが分かります。(もちろん電源を切っても復元されます)

balmychanbalmychan

以下3点に取り組んでいる

  1. ファミコンと、Arduinoとを同じRAMに繋いだ際(ファミコンをネット接続する方法として、RAMを経由してファミコンとArduinoとでやりとりする方法を考えているため)に、読み取り/書き込みのタイミングが衝突せずに済む配線を考えている
  2. マッパー1のバンク切り替えはまたマッパー2とは違うようで、それを理解しないといけない(シフトレジスタを使っている様子)
    参考資料
  1. 上のシフトレジスタで、シリアル信号をパラレル信号に変えることができる。これを使えばピンを少なくできるので、ピンの少ない他のArduinoやRaspberryPIなどでも書き込み機が作れるかもしれない。のでシフトレジスタについて学んでいる
    参考資料

正直拡張RAMを使うだけなら、マッパー0を拡張する形で作れそうではあるので、それでもいいかなーとも感じてます(マッパー1はなかなか複雑そうで、量産するのも大変そうなのと、バンク数はそこまで必要と思ってないため)

ただ、独自のカセットの規格にしてしまうと、エミュレーターで動かなかったり、吸い取り機で吸えないなどあるかもしれません。(そもそもArduino(などのマイコン)とつなごうとしている時点でエミュレーターで動かすなんて無理かもですが)

エミュレーターで試すことができなくなるというのも、なかなかネックになりそうなので、簡単にカセットに書き込んで試せる仕組みが構築もしたいなと思います。(FlashROMをカセットに付けたままでも書き込みできるような書き込み機を作れれば良い)

balmychanbalmychan

正直拡張RAMを使うだけなら、マッパー0を拡張する形で作れそうではあるので、それでもいいかなーとも感じてます(マッパー1はなかなか複雑そうで、量産するのも大変そうなのと、バンク数はそこまで必要と思ってないため)

構造はシンプルにしておきたいので、↑を試すことにした
マッパー0の場合CHR-RAMではなくCHR-ROMになるが、CHR-RAMは便利で使いたいので

  • バンク切り替えなしのマッパー0をベースに
  • CHR-RAMは搭載して
  • バッテリーバックアップ拡張RAMも搭載して($6000~$7FFF)
  • 前半16KB($8000 ~ $BFFF)をキャラクタデータ用に使って
  • 後半16KB($C000 ~ $FFFF)をプログラムデータを搭載

という形にすることにした。
おそらく独自の構造になるので(もしかしたらそういうマッパーも存在するのかもだが)ちょっと心配だが
一応この形でプログラムを書いてみて、エミュレーター上では動作は問題なかった(バッテリーバックアップ拡張RAMへの保存も問題ない)

以前作ったマッパー0用カセット基板からの変更点としては

  • A14は常時HIGHにせず、CPU A14と繋ぐ(これで8000とC000の切り替えが可能)※A15は今まで通り常時HIGH
  • CPU A0~A12を拡張RAMにも繋ぐ
  • CPU R/Wを拡張RAMの/WEに繋ぐ(書き込みのときにRAMの書き込みが有効になるように)
  • 拡張RAMへの書き込みの際はPRG-ROMを無効にする必要があるので(そうしないと拡張RAMへの書き込みの信号と衝突するので)PRG-ROMの/OEと/CEを、$8000未満に書き込む際は無効にする必要がある。$8000未満へのアクセス時はファミコンからの/ROMSELがHIGHになるはずなので、/ROMSELをROMの/OEと/CEに繋げばいいかも(ここはかなり怪しい)

でいけると思われる

いきなりカセット基板作って発注するのは危険なので、今あるマッパー0用基板に手を加えて、試してみる

また、今回拡張RAMを使いたいのはArduinoとのやりとりのためなので、電源ON時だけ値を保持してくれれば問題ない。なのでバッテリー自体は不要なので、電池を繋いで電源は確保しない形でやってみる。

今あるマッパー0用基板に手を加えて

いや、そもそも以前作った基板はCHR-RAM仕様になってないんだった。やっぱりCHR-ROMを使う形で、拡張RAMを追加搭載する形にする

つまりA14の件だけ除いて、こう↓

  • CPU A0~A12を拡張RAMにも繋ぐ
  • CPU R/Wを拡張RAMの/WEに繋ぐ(書き込みのときにRAMの書き込みが有効になるように)
  • 拡張RAMへの書き込みの際はPRG-ROMを無効にする必要があるので(そうしないと拡張RAMへの書き込みの信号と衝突するので)PRG-ROMの/OEと/CEを、$8000未満に書き込む際は無効にする必要がある。$8000未満へのアクセス時はファミコンからの/ROMSELがHIGHになるはずなので、/ROMSELをROMの/OEと/CEに繋げばいいかも(ここはかなり怪しい)

これが成功したら、次はA0 ~ A12をArduinoにも繋ぐ(ただ、これについてはどうすれば信号が衝突せずにファミコンとArduinoとで相互に拡張RAMの読取/書込ができるかは分かってない)

balmychanbalmychan

ただ、これについてはどうすれば信号が衝突せずにファミコンとArduinoとで相互に拡張RAMの読取/書込ができるかは分かってない

これについて考えてみた。要は

  • ArduinoがRAMを読み書きしていることがファミコン側に伝わり
  • ファミコン側がRAMを読み書きしていることがArduino側に伝わる

ということができれば良い。電気により制御できるスイッチがあればできそうではある。

電気によるスイッチといえばトランジスタが思いつくが、トランジスタの場合方向があるらしく、使えないのかな、と思った。

で、色々探してみたら、アナログスイッチという汎用ICがあった

これ↓
http://akizukidenshi.com/catalog/g/gI-05673/

これで、Arduinoからの信号により、ファミコンとRAMを遮断する制御ができそう

ということで、考えたのが下図

図に書き込んでいる文章の通り

  • 最初に、特定のアドレス(例えばCPUのアドレス空間の$7FFF。つまりRAMの$1FFF)に、FFを書き込んでおく
    で、ファミコン側は
  • $7FFFを読み取っても00しか返ってこない場合は、Arduino側が読み書きしているとみなし、なにもしない(スイッチが切られているのと、ROM側も/OEがHIGHなので、データが読み取られないので、00になる、はず)
  • $7FFFがFFの場合、読み書きを開始する
  • ※ファミコン側が読み書きしている場合は、/ROMSELがHIGHになる
    Arduino側は
  • RWはHIGHにしておく
  • /ROMSELがHIGHの場合はファミコンがRAMにアクセスしているしているので、待機
  • /ROMSELがLOWになったら、RWをLOWにして、ファミコンとRAMの接続を断つ
  • 読み書きが終わったら、RWをHIGHに戻す

という感じ

これで行けそうな気がする。まずはアナログスイッチを調達にいかねば...

balmychanbalmychan

マッパー0に拡張RAMを繋ぐにあたって、カセットの端子とRAMをどう繋ぐか、作業前に整理していた。

下表の通り。

balmychanbalmychan

今日もちょっと拡張RAMつなぎはやる気がでないので、先日注文したアナログスイッチの汎用IC(TC74HC4066AP)が届いたので、その動作確認だけした。

ピンアサインは以下の感じ

1と2が、13がHIGHかLOWかで通電するかどうかが決まる(13がLOWの場合に非通電)

こんな感じで、13にHIGHを繋いだら(実際繋がなくても通電してるので、大事なのはLOWに繋いだときに非通電になるということ)

LEDが点きますが、LOWに繋いだら、LEDが消えました。13に電気を流すかどうかによって、1と2で通電するかどうかが制御できてます。

balmychanbalmychan

マッパー0の基板に無理やり拡張RAMを付けるのをやっていた。

以前書いた通りに

https://zenn.dev/link/comments/f9b797afeafba7

こんな感じでつなげた↓(無理やりジャンパワイヤーをハンダでくっつけている)

一応起動はしたものの、拡張RAM領域への書き込みは成功しない...

おそらく、RAM側の/WE、CE2、/OE、/CEの配線が正しくないと思われる。
特に/OEを5Vにつないでいるが、なんでこうしたのか分からない笑。なんとなく、CPU R/Wを反転させたものを繋ぐのが良いのかなーと思われる。(/WEと逆になるように)
ちょうど先日NOT回路の入っている汎用ICは買っていたので、それで試してみる

で、試したものの、うまくいかない...なにか根本的に間違っているのかもしれないなーとも思われる。
(CPUから見たRAMのアドレス空間は$6000~$7FFFだが、単にA0~A12を直接繋いでいるのがこれでいいのかなーとも感じている)

balmychanbalmychan

昨日の続き

CPUから見たRAMのアドレス空間は$6000~$7FFFだが、単にA0~A12を直接繋いでいるのがこれでいいのかなーとも感じている

なぜCPUから見た$6000~$7FFFへのアクセスが、拡張RAMの$0000~$1FFFになるのかがなんとなく分かった。
以前貼り付けた

https://zenn.dev/link/comments/46f88395b398f6

を見ると分かる通り、右上の拡張RAMにはA0~A12までしか接続されていない。
で、例えばCPU側で$6000にアクセスすると、2進数だと

110_0000_0000_0000

となる。で、拡張RAMはA14とA13は繋がっていないので、上位2桁が使われず、全て0となる。
つまり、CPUからは$6000にアクセスしているものの、拡張RAM側からは、$0000にアクセスしていることになる。

balmychanbalmychan

以前書いた

https://zenn.dev/link/comments/576c0ee7b078af

の、最後のROM側の/CEと/OEの制御が不足していたので、その対応を行った

要するに

  • /ROMSELがLOWのときは$8000 ~ $FFFFのPRG-ROMの領域にアクセスしているので、PRG-ROM側の/CEや/OEをLOW(有効)にし、RAM側のCE2をHIGH(無効)にする
  • /ROMSELがHIGHのときは$0000 ~ $7FFFのRAMなどの領域にアクセスしているので、PRG-ROM側の/CEや/OEをHIGH(無効)にし、RAM側のCE2をLOW(有効)にする

といった感じで、それぞれROMやRAMにアクセスしている際は、それぞれの信号が衝突しないように、お互いを有効/無効とする必要がある。

で、これを踏まえて配線し直したものの、うまくいかない...

そもそもこの配線でうまく信号が衝突せずに制御できているのかが怪しいので、Arduino - ROM - RAMと繋いで、ファミコンのROMSELの動きを真似てみて、それぞれ

  • ROMからの読み取り
  • RAMへの書き込み
  • RAMからの読み込み

がうまくいっているのか、試してみようかと思う
うーむ、難易度が高い

balmychanbalmychan

昨日の続き

RAMのpin outについては
http://nesdev.com/NES ROM Pinouts.txt
に書いてあるのが正しそうなので、これにならった。
(でも、違いは22 /OEがGNDだったくらい)

で、いくつか配線が外れているものがあったりなど、色々見直した

が、やはりRAMの読み書きがうまくいっていない...もう配線は絶対これで合ってるはずなんだが、全然うまくいかない。

で、マッパー1の時のようにMMC1を介す場合と、今回作っているように直接繋いでいる場合に、なにか大きな違いがないか情報を漁っていたところ

https://wiki.nesdev.com/w/index.php/PRG_RAM_circuit
上のページにある、/ROMSEL delayや/ROMSEL delay issuesあたりが怪しいなーと感じている。
このあたりを元にTwitterを漁っていたら、有力な情報が

https://twitter.com/HD64180/status/1248914863293747200

↑これ

自分も、/ROMSELは、$0000~$7FFFにアクセスしたらHIGHになって、8000~FFFFにアクセスしたらLOWになるものだと思っていた。(それに合わせてROMの有効/無効や、RAMの有効/無効が切り替わるように配線していた)

うーん、でもわからん

balmychanbalmychan

昨日の続き

もしかしてマッパー1についているMMC1を繋げばうまくいくのか?と思い、繋いでみた

青丸つけたあたりがミソ(ROMSELの制御などを多分してくれる)

で、こんな感じ

(MMC1が宙に浮いている...)

しかし、ダメ!!

https://youtu.be/1RO9cs4ZYVE

↑こんな感じで、起動はするものの、リセットすると位置が戻ってしまう(つまりRAMの読み書きが成功していない。エミューレータ上だと問題ない)

もう無理だ!!マッパー0 + 拡張RAM構成はそう単純ではないことが分かった。

先日の/ROMSEL delayに関連しているかわからないが、マッパー0基盤はいくつか抵抗やコンデンサも入っていたりしていて


↑このへん

もうそう単純にいかないのかと思われる。

これ以上やってもらちが明かないので、いったん別の方向にする。

  • マッパー1の基盤を自前で作って、それに成功したら、Arduinoが繋げられるように拡張する
  • 拡張RAMが使える単純な構造のマッパーを探す

のどちらかかな。(とはいえ、この辺のROMSELの仕組みやdelayについてもっと詳しくならないと、どこかでまた詰む懸念もある...)

balmychanbalmychan

諦めきれずに調べていたら

https://wiki.nesdev.com/w/index.php/PRG_RAM_circuit

このページに、kyuusakuという人がとあるフォーラムで考案した3入力NANDを使ったRAMとROMの切り替えに関して記載されていた。(4入力NANDのパターンもある)

MMC1は使わずにこのパターンでてきないか試してみようと思う。ということで

3入力NANDの74HC10(https://eleshop.jp/shop/g/gT11462/)と、
4入力NANDの74LS20(https://eleshop.jp/shop/g/gT11687/)

を注文した。到着したら再チャレンジしてみる。

balmychanbalmychan

先述のNANDが届くまでは暇なので、マッパー1用の下駄を作っている

あと今使っているFlashROMは在庫がなくて追加購入できないので、別途在庫を確保できる別のFlashROMを注文した。

これ↓
https://jp.rs-online.com/web/p/flash-memory/1459063/

ちなみにこれ、PLCCではなくDIP版もあって、このほうが扱いやすそうだなーとも思っている↓
https://jp.rs-online.com/web/p/flash-memory/1779666/

新しいFlashROMは若干ピンが違うので、上の下駄や、書き込み基板も多少修正が必要そう

balmychanbalmychan

NAND(4入力と3入力どちらも)が届いたので、試してみる

https://wiki.nesdev.com/w/index.php/PRG_RAM_circuit

Using 7420
He also suggested a circuit based on a 74HC20 (double 4-input NAND), which appears to be the same one in Family BASIC:
Or you could just use a NAND4 to decode any active low memory, also using the /WE priority method. If this is done with a two gate 7420, the second gate could be used to invert r/w to prevent bus conflicts as in the circuit above. This is probably the *final* best way unless you happen to need the extra AND3 from the 7410 and have a positive CE.
The pinout:
A = Phi2
B = /ROMSEL
C = A14
D = A13
Y = PRG RAM /CE
PRG RAM /OE = GND
PRG RAM /WE = Vcc or R//W, depending on the Family BASIC cart's write-protect switch
Kevin Horton suggested the same circuit.
You could also use the other gate to invert R//W for /OE on the ROM to prevent bus conflicts.

とあるように、NANDのそれぞれの入力(A, B, C, D)をカセット端子のPhi2, /ROMSEL, A14, A13を繋ぎ、出力(Y)をRAMの/CEに繋ぐ。
※あと上の記事にもリンクがありますが https://forums.nesdev.com/viewtopic.php?p=76149#p76149 の回答も役立つ

こんな感じ↓(だいぶグチャグチャしてきたので、どれがカセットでどれがRAMでどれがNANDか示します

さてどうなるか...

きたー!

https://youtu.be/24H66P_v6tg

(めっちゃ分かりづらいですが、画面上部にアッパマン(キャラクター)が動いていて、リセットボタンを押しても同じ場所から再開しているのが分かります。座標がRAMに保存されています)

やっとできた...

ポイントを要約すると

  • データバス(D0~D7)とアドレスバス(A0~A12)は、ROMとRAMで共有されているので、同時にチップを有効にすると、信号が衝突する
  • $6000~$7FFFにアクセスする際はRAMを有効(/CEをLOW)にし、ROMを無効(/CEをHIGH)にする必要がある。逆に$6000~$7FFFにアクセスする際はRAMを無効(/CEをHIGH)にし、ROMを有効(/CEをLOW)にする必要がある

という感じで、同時にROMとRAMが有効にならないように制御する必要がある。
そのための制御に今回のNANDを使うという感じ。

A = Phi2
B = /ROMSEL
C = A14
D = A13
Y = PRG RAM /CE

とあったように、それぞれ

  • Phi2がHIGHのとき(これはファミコンから出るクロック信号とのこと、これを今回使う理由はまだ良くわかっていない)
  • /ROMSELがHIGHのとき(つまり$8000未満にアクセスしているということ)
  • A13, A14がHIGHのとき(つまり$6000以降にアクセスしている = RAMにアクセスしている)

となった場合に、YがLOWになり、RAMが有効になる(NANDなので、A, B, C, Dの入力全てがHIGHのときに、YがLOWになる)

同時にROMとRAMを有効にしてはいけないことはなんとなく分かっていたが、それをうまく制御する方法がわかっていなかったので、この記事には大変助けられた、良かった。

これで割とシンプルなマッパー0+RAMという構成が作れそう!!

次は、Arduinoも繋いで、RAMの読み書きができるかを試す。これも信号が衝突しないように配慮する必要がある。
(苦戦しそうだ...)

balmychanbalmychan

続き

NANDを使った形と、先日RAMとArduinoをどう繋ぐか検討したときの形を組み合わせて、下記のようにすることにした。

で、先日

https://www.marutsu.co.jp/pc/i/37603/

この4つ入りのアナログスイッチを5個買ったのだが、数が足りないことに気づいた。
A0~A12の計13と、D0~D7までの計8で合計21必要だが、5個だと20個しかないので、一個足りない。
ということで、追加注文する。
(やっぱこういう安いパーツは、たとえ使わない可能性があってもある程度量を頼んでおくと二度手間がなくて良いな...)

balmychanbalmychan

ということで、追加注文する。

やっぱ待ちきれなかったので、パーツショップに買いに行きました。(ついでにブレッドボードも欲しかったので)

で、マッパー0カセット + RAM + 4入力NAND + 6個のアナログスイッチを組み合わせてチマチマ組み立てて行きました

組み立て開始時の様子↓

そして出来上がったのがこれ(ものすごいかさばる)↓

これを見てもなんのこっちゃと思うので、どこがどういう役割なのかを書きました↓

この状態でファミコン実機で起動することは確認できました。(また、アナログスイッチを+5Vに繋いでONにするとRAMへの書き込みが成功し、GNDに繋いでOFFにするとRAMへの書き込みがされないことも確認。アナログスイッチが動作していることが確認できた)

あとはArduinoを繋いでみて、ArduinoがRAMへ書き込んでいる間はアナログスイッチでOFFにして信号が衝突しないようにすれば、うまくいく(はず)予定。

Arduinoとの接続は、また明日以降やります

balmychanbalmychan

続き

Arduino側のRAM読み書きプログラムを用意し、上のやつと繋いだ

が、ArduinoからRAMに書き込もうとArduino側のアドレス用ピンをOUTPUTモードにすると、ファミコンがバグって(ハングアップ)しまう現象が発生。

どうもアナログスイッチに繋がる信号をArduino側でLOWにしても、ROM-RAMの接続が切れておらず、信号が衝突してファミコンが死んでしまっている様子(試しにアナログスイッチに繋がる信号をGNDに繋いだらハングアップすることはなかったので、やっぱりこの辺が怪しいかなーと思われる)

今日はここまでで、また後日にします。

balmychanbalmychan

https://twitter.com/HD64180/status/1286879654293340160?s=19

このツイートがめっちゃ気になる。この人もファミコンカセットにArduino搭載しようとしてるようで、ファミコンとArduinoの通信にこの74HC245(双方向スリーステートバッファというICらしい)を使って双方向にシリアル通信しようとしてる?ような感じに見える。
これでうまく行くならRAMなんて使わなくて良いしシンプルになるので、これについても調べる。

balmychanbalmychan

今日は調べ物だけ

自分がアナログスイッチでやろうとしていた信号線の接続/切断は、どうもアナログスイッチをぐちゃぐちゃ組み合わせるのではなく、
昨日知った74HC245(双方向3ステートバッファ)でやるのが良いようだった。
というか、まさしくそういう用途に使う汎用ICだったようだ。(アナログスイッチを複数組合わるなんてアホみたいなことだったんだろう多分笑)

↑のこのへんの記事が、大変分かりやすいです。
特に今回のケースのように、D0~D7のデータ線の信号衝突を制御するのに使われることが多いようで、8回路入ったスリーステートバッファがよく使われるようです。

上の記事では、AND回路やOR回路を、信号を通すか通さないかを制御する「ゲート」とみなすと良いよ、という話がとても良い。
加えて、そこから発展してスリーステートバッファにも触れている。

スリーステート=3状態と呼ばれるのは、LOWを通す、HIGHを通す、電気を全く通さない(ハイインピーダンスな状態と呼ぶ?完全に絶縁されている状態)を制御できるからのよう。
で、今回のように信号線の衝突を防ぐには、ハイインピーダンスにする必要もあるのだと思われる。

で、更に「スリーステートを2つ」組み合わせたものが双方向と呼ばれるもので、双方向のスリーステートバッファで、双方向スリーステートバッファとか、双方向バッファと呼ぶよう。
双方向にすることで、ROM-RAMの左側も右側のどちらかがハイインピーダンスになるように制御できるのかと思う。ちょっとまだハイインピーダンスが明確に分かってないが。

ただ、先日買ったアナログスイッチも制御信号をLOWにしたら出力側はハイインピーダンスになるはずなのだが、もしかしたらこの双方向、というのが大事なのかもしれない。

ということで、74HC245をとりあえず10個ポチッた。
https://www.marutsu.co.jp/pc/i/95788/

balmychanbalmychan

DIPへの変換基板が届いたので、その作業

左が74HC245で、右がDIPへの変換基板

表面実装部品のはんだ付けはやったことなかったので、

表面実装部品(SMD)のはんだ付け【ICの実装】

このへんを参考にやった。(いずれはリフローではんだ付けしてみたい)

こんな感じで6個作った

一個のICで8ピンまで対応できるので、D0~7用で1個, A0~A12用で2個。
で、「ファミコン - RAM間」と「RAM - Arduino間」にそれぞれ配置したほうが良いのかも、と思ってきたので、x2セット作った。

今日はこれで力尽きる

balmychanbalmychan

上の双方向バッファを使った形で組み直している

が、アナログスイッチと違って方向というものがあるので(スリーステートバッファーは1方向、双方向スリーステートバッファは信号で方向を変えれる)
ここで苦戦している。データピン、アドレスピンでそれぞれ下記のように制御しようと思っている

  • アドレスピン: 常にCPUからアドレスを指定するので、方向は常にCPU → RAMとなる
  • データピン: RAM読み取り時はRAM → CPU、RAM書き込み時はCPU → RAMという方向になるようにする

R/Wを使って方向を制御するように配線したが、うまくいかない(ファミコンがバグって止まる)
RAMにつないでいる/CEの制御を、双方向バッファの有効無効(/G)につなぐとうまくいくかもなーと思いついたので、また試してみる。

balmychanbalmychan

なにもせず
そういえば以前FlashROMのEN29F002T調達が難しいので代わりに

https://jp.rs-online.com/web/p/flash-memory/8234501/

を購入した件だが、(https://zenn.dev/link/comments/89513a6e646d07 でつぶやいたとおり)

書き込みプログラムの方も作り直しかなーと思っていたが、なんとそのまま使えた。(ピンアサインは同じであることは分かっていたが、書き込みや全消去コマンドが違うと思っていたので、そのまま使えるのは意外だった。このあたりも統一されているんだろうか?)
RSコンポーネンツに結構在庫はあるみたいなので、FlashROMの在庫不足はこれで解消したので良かった。

balmychanbalmychan

いまRAM接続を試すのに使っているマッパー0基板の起動率が異常に悪くなってきて、かなり効率が悪いので、再度マッパー0基板を用意していて終わった

balmychanbalmychan

今までファミコン互換機で試していたが、マッパー0基板をRAM接続仕様(具体的に言うと/CEを常時GNDに接続するのではなく、/ROMSELで制御するようにつなぎ替える)
にすると、互換機では起動するが実機だと起動しないことに気づいた。

おそらく/CEを/ROMSELで制御するようにしたからと思うが、調べてもまだ分かっていない状況。(以前ちらっと見た記事で/ROMSELは若干の遅れがあるから云々みたいなのはあったのだが、探し直せていない)

うーん、でもマッパー2の場合は/CEと/ROMSELを直接繋いでいるんだよなぁ...(マッパー2とマッパー0は大した違いないから、問題ないはずなのだが)
http://www43.tok2.com/home/cmpslv/Famic/Fcmp2.htm

balmychanbalmychan

※ここから仕事が忙しくなったのもあって更新できていません。またいずれ再開します