たまりば

パソコン・インターネット パソコン・インターネット三鷹市 三鷹市

I2C液晶が動かない理由が分かった
2017年01月31日 01:54

以前、秋月で超小型のI2Cキャラクタ液晶(I2C接続小型キャラクタLCDモジュール 8x2行)を買い、PIC10F200で色々と表示していた。
ある時思いついたネタを実装しようとしたところ10F200ではROMが足りなかったので、石をPIC12F510に変えることにした。
移植作業はわりと楽であった。ADコンバータ関連を無効化すればあとはだいたい似たようなものである。
しかし動かない。
しかも不思議な事に信号線にLEDやテスターをつなぐと動く。つまり原因は単純なプログラムのミスではなくアナログ的な微妙な差異によるもののようだ。
色々考えたが分からないのでその時はあきらめた。一昨年の5月ころだ。

(昨年の4月に)その原因が判明したので顛末をここに書き留める。何かの参考になれば幸いだ。

まず最初に言っておくと、このプログラムで出していた信号は正常なI2Cではない。
第一に、本来I2CはLowとプルアップで駆動するものをHighとLowで駆動している。プログラムの簡略化と高速化の為だ。
第二に、クロック立ち下がりからデータ変化の時間、tHD:DATはデータシートによれば最小0・最大0.9μs(後述)のところ、最小ギリギリの0にしている。10F200で可能な間隔が1μsの為だ。またデータシートによればこれでも300nsの余裕があるように読める(後述)。

もっとまともな信号にすれば動くかもしれなかったが、それは解決にならない。
問題は動かない事ではなく、動かない理由が分からない事である。
規格外や規格ギリギリではあるものの、これでも動くはずだと考えてやっているのだし、実際10F200ではほぼ確実に動くのである。
この状態で適当に変更して動くようになったとしても本当にその変更が正しかったのか分からない。何か重大な思い違いがあったら危険だ。
またなにより、何が問題なのか分からないままというのは気持ち悪い。

移植して動かなかった時から(覚えている限り)時系列に沿って状況を書く。

MPLABのシミュレータで見た波形は全く同じ。
怪しいのは実際に信号が出ているかだ。TRISの設定を間違ったり、間違って別のピンにつないでいたりすることはしょっちゅうだ。
なので信号が出ているかどうかを確認するため、信号線に青LEDをつないだ。(LEDをつなぐと点滅具合で色々分かる。青なのは電源が電池2本の時抵抗を入れなくても大丈夫で便利だからだ)
すると…不思議なことにほぼ確実に動作するようになった。
最初はデータとクロック両方に付けていたが、データ線だけでも動作する。
赤LED(+抵抗)だと駄目。電圧の問題かと思い赤LED+ダイオード1、2本で試すも駄目。また、データ線にテスターを当てるとほぼ確実に動作する。

この時点で問題はアナログ的なものと分かった。

アナログ的な仕様に何か違いがあるのではないかとデータシートを見る。ほとんど同じだったが、1つだけ、デバイスリセットタイマ(DRT)の時間が異なっていた。早速その分の待ちを入れてみる。
…が、変わらず。

もはやこれはオシロスコープを買うしかない。
2万円の機種と迷ったが3万円のものを購入。オシロスコープで12F510で動く時(青LED付き)と動かない時を確認すると信号に違いが!
…と思ったがよく見るとそれはACKが返っていないだけであった。
分かったことは、最初の1バイト(アドレス)から既にACKが返っていないということだけ。
あとはノイズだが、電源投入からI2C送信までの間に目立ったノイズは無く、電源投入時のノイズはわりと酷いがどちらも同様に酷い。

そして10F200と12F510を比較してみると、I2C信号を送信するところは一切違いが見当たらないが、リセット直後の挙動が違っていた。
調べてみると、デバイスリセットタイマ相当の待ちを入れた時、待ちの後にHigh-ZからHighにするつもりのところを間違って待ちの前にHighにしてしまっていた。
…しかしこれを直しても変わらず。
もしかしてデバイスリセットタイマ中とプログラムが走っている時では何か違いがあるのだろうか。そうなると12F510で動かすのは不可能ということに。

この辺でできることも無くなり諦めてしばらく経った。

ある日、なんとなくI2Cについて考えていて思った。なぜtHD:DATに上限があるのだろう。
液晶コントローラのデータシートに上限があるように書かれていたので不審に思いながらも従っていたのだが、やはりおかしい。
I2C液晶データシート
I2Cのデータシートを見ると、違う書かれ方をしている。(なお後で知ったが古いデータシートでは上限0.9μsになっていた)
I2Cデータシート
上限はなし、その上で注釈として、
The maximum tHD:DAT could be 3.45 μs and 0.9 μs for Standard-mode and Fast-mode, but must be less than the maximum of tVD:DAT or tVD:ACK by a transition time.
最大のtHD:DATはスタンダードモードで3.45μs、ファースト(fast)モードで0.9μ秒になりうるが、tVD:DATまたはtVD:ACKの最大値よりトランジション時間だけ短くなくてはならない。
This maximum must only be met if the device does not stretch the LOW period (tLOW) of the SCL signal.
この最大値はデバイスがSCL信号のLOW期間(tLOW)をストレッチしないときのみ満たす必要がある。
If the clock stretches the SCL, the data must be valid by the set-up time before it releases the clock.
もしクロックがSCLをストレッチするなら(訳注: 意味が分からない。SCLはクロックだ)、データはそれ(訳注: どれ? クロック?)がクロックを解放する前にセットアップ時間だけ有効でなければならない。
CBUSとの兼ね合いかとも思って調べようともしていたが、どうも死んだ規格らしく情報がない。その辺で当時は諦めていた気がする。
しかしさらに考えてやっと分かった。この0.9μsという上限の値は、スレーブに対する制限なのだ。
「マスターがクロックを生成しスレーブがデータを送信するとき、マスターがクロックをLowにしている間にスレーブはデータを変化させなければならない」という当たり前の制限について言っているだけだ。

クロックがLowの間にデータを変化させるには、データのホールド時間(tHD:DAT)・データの立ち上がり時間(tr)または立ち下がり時間(tf)・データのセットアップ時間(tSU:DAT)の和がクロックのLow期間(tLOW)より短くなければならない。
I2Cタイミング関係
ファーストモードではクロックのLow期間の最小値が1.3μs、立ち上がり時間の最大値が300ns、データセットアップ時間の最小値が100nsなので、
1.3-0.3-0.1=0.9
となり注記の値に一致する。

スタンダードモードでも同様に
4.7-1-0.25=3.45
となる。

今回データ送信はマスター側からのみ行うため、スレーブの制限は無関係だ。よって最大0.9μsは無視してもっと長い時間とれば余裕を持った通信ができる。

解決方法は見えた。あとは原因の方だ。10F200と12F510、LEDやテスタリードを付けた12F510で何が違うのか。

もう一度オシロで波形を見てみよう。
ここまでI2Cの通信内容が分かる程度の縮尺で見ていたのだが、限界までサンプリング速度を上げて拡大して見てみる。すると波形が明らかに違っていた!
I2C_10F200/12F510比較
10F200はシンクが強いのか立ち下がりが速い。
これによって、SCK・SDAを同時に操作してもSCKの立ち下がりの後にSDAの立ち上がりが来てくれていたようなのだ。それが12F510では逆になっている。
この違いで動作が分かれていたのだろう。
なおこの3万円のオシロのサンプリング速度は250Msps(2ch時)、帯域は60MHz。迷っていた2万円の方の機種は100Msps・25MHzである。
1/(60MHz)=17ns、1/(25MHz)=40nsであり、25MHzの帯域で今回の波形の違いを捉えられたかはかなり怪しい。
高い方を選んで良かった。

さて最後に、冒頭で言った300nsの余裕の件だ。

tHD:DATの下限について、以下のように注釈がある。バージョンによって言い回しが少々変わっているが内容は同じようだ。
・A device must internally provide a hold time of at least 300 ns for the SDA signal (referred to the VIHmin of the SCL signal) to bridge the undefined region of the falling edge of SCL.
・A device must internally provide a hold time of at least 300 ns for the SDA signal (with respect to the VIH(min) of the SCL signal) to bridge the undefined region of the falling edge of SCL.
この文章も微妙に意味が取れないのだが、精一杯の訳が以下だ。
「SCLの立ち下がりの不定区間を橋渡しするため、デバイスはSDA信号に対して内部的に最低300nsのホールド時間を(SCL信号のVIH(min)を基準に)用意しなければならない。」
internally・reffered・with respect to・bridgeあたりの意味がよく分からない。
なお誤訳の可能性を考えてオランダ語の原文があるかと探したのだがどうやら英語が原文のようである。

これを自分はこう理解した。

受信側はSDA線の立ち下がりから300ns間内部的にHighと扱う。
よってSCKよりSDAが(300ns-立ち下がり時間)だけ早くてもよい。
立ち下がり時間の最大が300nsなので、都合余裕は0。

これは12F510で動作しない現状と食い違うが、上記の文章の解釈は他に思いつかない。
(「送信側がSDA線を300ns遅らせて駆動しなければならない」という説も見かけたが、それではinternallyでないのと、それはtHD:DATの下限が300nsと書けばよいことなので、別に注釈として書かれている説明がつかない。)
自分の解釈が間違っているのか、このデバイスが仕様に従っていないのか分からない。
まあどちらにせよ、諸悪の根源はI2Cの仕様書が分かりづらいせいだ。マスターの条件とスレーブの条件が混ざって書かれているし、0.9μsだの300nsだのと注釈で変な条件を追加するし、その注釈は文章の意味が取れない。

とそんなわけで不可解な点は残ったものの、まとめると、

・最大値が0.9μsだと思っていたが制限は無かった。
・最小値が0だが300nsの余裕があると思っていたが、文の解釈はよく分からず、現に余裕は無かった。
・石を変えて動かなくなったのは立ち下がりの速さの差のせい。

tHD:DATを長くしたところ、安定して動くようになった。
12F510でI2C液晶が動いた
(なお表示は温度計を作ろうとしていた時のダミー表示)  

  • ゲームボーイの吸い出し機を作った (後編)
    2017年01月16日 22:44

    前編の続き。プログラム側について。

    まずは単純に読むことを試みる。
    手持ちの中でバンク切り替えなし(32kB)のソフトとしてDr.マリオを選択。

    バンク切り替えが無ければアドレスを出してデータを読むだけ。
    …とはいえCLKとかCSとかRDとかどう制御すればいいのか。
    GBのカートリッジの仕様くらいいくらでも見つかると思っていたのだが、ROMの読み出し方法は常識なのか、細かい解説が意外と見つからない。
    結局、分かってみると単純で、
    /RDがLにアサートされている間、アドレスピンで指定されたアドレスのデータが、データピンに出る
    というだけのことだった。
    つまり読み出すには、/RDをLにアサートしたままアドレスを順次変え、データピンを読むだけでよい。
    これが分かるまでに/RDをH/L切り替えてしていた。
    あと/CSはSRAMを読む時アサートするものだった。

    プログラミング自体もなんだかんだで苦労した。
    やはり合っているか分からない操作を正しく組めているか分からない機械にプログラムするのは疑心暗鬼に陥ってだいぶ精神を消耗する。
    まずシリアル通信するところからしてうまくいかない。
    シリアルポートが全く反応しなくて焦った挙句Windows10へアップグレードした時にケーブル類全部抜いてたのを戻してなかっただけだったりもした。
    何か出るようになったと思えば文字化けしている。これはどうもTeraTermのバグかWindows10との相性のようで、最新版を使ったら正常であった。
    新しいPICを使う際には毎回のようにGPIO以外の機能を切っていなくて問題が起こるのだが、今回もまんまとその罠にはまった。
    ADコンバータとコンパレータを切ったまでは良かったのだが、それで読もうとしても何も読めない(FFが読める)。
    ポートのレジスタを直接インクリメントしてたのがまずかったかと思い、別の変数をインクリメントしてそれを出力するようにしたところ何かは読めるようになったが、全体にわたりほぼ確実に8バイトづつ同じ値が取れる。
    つまりアドレスの下3bitが何かおかしい。そのピンを調べると、LCDドライバの電圧生成機能がデフォルトでONであった。
    ということはポートのレジスタを直接インクリメントすること自体は問題なく、最下位bitの書き込みが無視されていたせいでインクリメントできていなかったんだな。

    というわけでついにDr.マリオが読めた。
    Dr.マリオ

    次はバンク切り替え…の前に色々なソフトをバンク0だけ読んでみることにした。
    するとポケモンYellowやポケモンカードなど読めるものもあるが、ポケモンSilverが読めない。
    読めないというのは、ほぼ全てFFが返ってくる。ごく稀にFF以外のものが返ってくることもあるのがまた不可解である。
    FFでない箇所のパターンは規則的で、なにかありそうである。2進数で「xxxx xxx1 0000 000x」と「xx00 0110 0000 0001」、つまり0100, 0101, 0300, 0301,…と0601, 4601がFFでない。
    不要なはずのクロックだがMBCを積んでいることもあり何か変わるのではないかと入れてみる。当然変わらず。
    …散々悩んだ挙句、電圧不足であった。
    いつもPICを動かす時はEneloop2本(2.6V程度)を使っていて、使いやすい5V電源を持っていないこともあり、とりあえずそれでやっていたのだが、GBの電圧は5Vなので動かなくてなんの不思議も無いのであった。
    Dr.マリオを始めいくつかのソフトで(バンク0は)読めていたので発覚が遅れた。

    改めてバンク切り換えだ。
    バンクの少ない(最少の4バンク)ものとしてQIXを選択。
    バンク切り換えの手順はMBCによって少々違うが、基本的に特定のアドレスにデータを書き込むだけである。
    書き込むべきデータを入れていなかったり、PICのIOを入力のまま出力したつもりになっていたり、書き込むアドレスを間違ったりして手間取ったが、まあまあすんなりと読めた。
    QIX

    さて続いて本命のポケモンSilverだ。
    128バンクあるが、バンク切り換えのやり方は同じなので、単純に数が多いだけ。難しいことは何もない。
    しかしなぜか途中でデータが飛ぶ。読み取ったデータを見るとファイルサイズが想定より小さい。
    今までこのような大量のデータをシリアルで受信したことは無かった。シリアル通信の信頼性はこの程度なのだろうか。
    だがそれは想定の内。1バンクごとに目印を入れてあるのでどこで抜けたかは分かる。何度か読んで正常な部分を切り貼りすればよいだろう。
    …と思っていたのだが、不思議な事に常にエラーが出ている場所がある。
    バッファ切れを疑ってバッファ量を変えたりウェイトを入れたりしてもなんとなく変化はあるものの直らず。
    データの問題かと思いXOR 0x55したデータを送ってみると抜けの量はほぼ変わらず、抜けの位置が変わった。特定のデータが来ると問題が起こるのだろうか。
    速度を落としてみるとだいぶ改善した。抜けが6バンクまで減ったので試しに起動してみると、一応起動はした。
    ポケモンSilver_データ抜け1ポケモンSilver_データ抜け2
    このような分かりやすいバグった表示になるものなんだなあ。
    なお部屋に出口がないので進めなかった。その後もう1バンク正常に取れたのでそれと合わせると部屋から出られたがBGMが異常になったりする。
    しかしここで何度とってもほぼ同じ場所でエラーを起こす。
    やはり速度を落とすだけでは解決しない。特定のデータが問題という線で考えてみよう。
    改行の処理に時間が掛かっている可能性を考えてCRの後にウェイトを入れてみる。むしろ悪化。
    あとは…エスケープシーケンス。何らかのエスケープシーケンスが来るとそれの処理に時間がかかってデータを取りこぼすのではないか。
    ここでTeraTermを調べて、受信した文字をすべて表示するデバッグモードがあることを知る。
    デバッグモードで受信するようにしたところ、一切取りこぼさなくなった!
    ポケモンSilver
    後で調べたところ、制御シーケンスに「1B 63: 端末リセット」というものがあるらしい。
    つまりこのバイト列が来るとTeraTermがリセット動作を起こし、その間に来たデータを取りこぼしていたようだ。
    調べてみると「1B 63」は最後まで読めなかった5バンク中の3ヶ所にあった。残り2ヶ所やそれ以前のエラー箇所は分からないままだがたぶん他のエスケープシーケンスだろう。
    デバッグモード以外にエスケープシーケンスを無視する方法がないか調べたのだが、見つからなかった。
    人が読む文字列を出す時は改行は使いたいのだが、致し方ない。

    次に困ったのがX(エックス)だ。バンク切り替えができない。
    調べてみるとこれに使われているMBC2はバンク切り替え時に書き込むアドレスに制限があり、今まで使っていた0x2000では駄目だった。
    と0x2100に変えてみたが、やはりバンクは切り替わらない。
    そこで読み取れたバンク0のコードを見てみることにした。この中にバンク切り替えのコードがあるはずである。
    するとやはり0x2100に書き込んでいる。
    合っているのにおかしいなと思い更に調べると、バンク1に変更する時は0x2100だったのだが、バンク2では0x2101、バンク3では0x2102…と、バンクNに変更する時0x2100+(N-1)に書き込むようになっていた。
    1少ないのは書き込み後のインクリメントの関係だろう。ということでバンクNに変更する時は0x2100+Nに書き込むようにコードを書き換えてみると、見事読み取りに成功した。
    なんだろう。バスコンフリクトだろうか。ファミコンのMMCでバスコンフリクトを起こすものがあるという情報はあるが、GBで起こるというのは見たことがなかった。
    X(エックス)

    さて次はニンテンドウパワーのGBメモリの読み取りを試みている。
    これは複数のMBCの動作を再現する特殊なコントローラを積んでおり、普通のバンク切り換えとは異なるコマンドを入れる必要があるらしい。
    色々試しているのだがまだ一切反応がない。一番つらい時期だ。
    読み取りができたら、どうも書き込みも出来るらしいのでやりたい。自作ソフトを実機で動かすのは夢である。

    ただその前に、どうも読み取りが安定しないのでどうにかしたい。
    今まで読めていたソフトでも読めなくなったりしている。
    断線しかかっているとかだろうか…。  

  • ファミコンで全画面に任意の画像(ただしモノクロ)を表示
    2017年01月14日 00:00

    ファミコンは普通には全画面に自由に画像を表示できないことはよく知られている。
    でもモノクロ2値ならできることに気づいたのでやってみた。
    ファミコン全画面_猫耳とりあえず猫耳。
    ファミコン全画面_羽根っ娘酉年らしく羽根っ娘。
    ファミコン全画面_漢字全画面任意なのが分かりやすい図柄も。

    まず前提として、ファミコンのBG(背景)面は32×30=960個のキャラクタで構成されるが、BGの(スプライトも)キャラクタパターンの指定は1バイトなので、一度に256種類からしか選べない。
    よって普通にはBGだけでは256キャラ、画面の1/4強しか埋められず、スプライトを8×16ドットモードで最大の64個使って128キャラ用意してやっと384/960=40%にしかならない。
    何らかのマッパーを使えば描画中にバンク切り替えで全画面を別のキャラクタで埋められるが、マッパーを使ってできるのは面白くないので今回考えないことにする。

    そこで今回の手法だが、パレットの色分けとキャラクタテーブルの描画中切り替えで実質キャラ数を4倍に増やしている。
    まずパレット。
    ファミコンのキャラごとの色数は4色だが、これを2色しか使わなければ例えば「白黒白黒」と「白白黒黒」の2種類のパレットを左右で使い分けることで1つのキャラで2つの絵を表示することができる。
    00011011

    0111111111111100
    1111111111111111
    1111000000001111
    1111010101011110
    1111111111111111
    1111101010101111
    1111010101011111
    1111010101011110
    00011011

    0111111111111100
    1111111111111111
    1111000000001111
    1111010101011110
    1111111111111111
    1111101010101111
    1111010101011111
    1111010101011110
    00011011

    0111111111111100
    1111111111111111
    1111000000001111
    1111010101011110
    1111111111111111
    1111101010101111
    1111010101011111
    1111010101011110
    比較のために単一のパレットで表示したものがこちらだ。
    単一パレット表示

    次にキャラクタテーブル。
    「一度に256種類からしか選べない」と書いたが、この「一度に」の部分がポイントだ。
    仕組みを詳しく言うと、定義できるキャラは256個の領域が2つあり、BGと8×8のスプライトはそれぞれその2つのどちらの領域からキャラを選ぶかを設定できる。同じ領域をBGとスプライトの両方で使うこともできる。
    (なお今回関係ないが8×16のスプライトは一度に2キャラを使うので、どちらかの領域から選ぶのではなく、2つの領域を合わせた512キャラを2キャラづつの256組と見たものから選択する)
    ここでこの「どちらの領域から選ぶか」の設定は画面の描画中にも切り替えることができる。これにより上半分と下半分で別のテーブルからキャラを選ぶことが可能なのだ。
    比較のためにこの切り換えを行わなかったものがこちら。
    切り換えを行わない表示

    基本的なアルゴリズムは以上だ。
    実際のコードは下に置いたが、少々細かい解説を加えておく。

    今回CHR-ROMでなくCHR-RAMを使っている。
    そのためまず最初にPRG-ROMに持っている画像データを元にCHR-RAM上にパターンテーブルのデータを生成している。
    これは事前に適切な並びにしたデータをCHR-ROMに持っておけば必要ない操作だ。
    ただ、この並べ替えのプログラムは、ファミコン自体で行うか外部で事前に行うかの違いでどうせ書かなければならない。
    ならば1つにまとめた方が扱いが楽なのでこうした。
    下に書いたコードと、任意のモノクロビットマップを用意するだけで、NESASMでビルドできる。

    このパターンテーブル書き込みのコードは少々複雑だが、やっていることは単純に、256×240ドットの1bitビットマップを適切な順に並び替えているだけだ。
    パターンテーブル前半0x0000~0x0FFFが画像の上128ドット、後半のうち0x1000~0x0DFFが画像の残り。0x1F00~0x1FFFには後述の通り0x0F00~0x0FFFと同じものを重複して配置している。

    「どちらの領域から選ぶか」の設定、つまりパターンテーブルのベースの切り換えだが、このような描画中のタイミング合わせはファミコンではふつう0番スプライトヒットフラグをポーリングすることで行う。
    だが今回別の方法をとった。
    0番スプライトヒットはBGとスプライトが共に不透明な点でしか起こらない。つまり画像の当該箇所が透明なとき使えない。
    今回任意の画像を表示できるようにしようとしているのでこれは困る。
    代わりにあまり知られていないスプライトオーバーフローフラグを使った。
    これはライン上にスプライトが9個以上並んで横並び数制限を越えたときに立つはずだったフラグだ。
    このフラグはスプライトが透明でも変わらず立つので、BGの内容に関係なく使えて便利だ。
    ただ重大な欠点があって、このフラグ、アドレスの桁上がりの誤りで別のデータを参照するせいでまともに動かないという、致命的かつしょうもないバグが存在する。
    使い物にならないと思うかもしれないが、バグの条件は厳密に判明している。
    詳しくはNesDevの「PPU sprite evaluation」のページを読むとよいが、
    スプライト番号順にライン内に存在するかをチェックし、8つめが見つかった次のチェックまでは正しく、その次から誤ったアドレスを参照しだす。
    ここから、
    ・スプライト数が7つ以下なら、必ずフラグは立たない
    ・スプライト数が9つ以上の時、8つめと9つめのスプライト番号が隣り合っていれば、必ずフラグが立つ
    ということが言える。
    なので今回はスプライト番号順に連続した9つを目的の場所に置くことで確実にスプライトオーバーフローフラグを立てることにした。
    というか残りも面倒なので同じ位置に置いた。

    切り換えタイミングだが、画像を単純に上下に分割した場合、きっちりHBlank中に切り換えを起こすようにしないとこのように画面が乱れてしまう。
    ファミコン全画面_画面乱れ
    そのレベルで合わせるには描画中のスプライトの判定処理の流れをきちんと理解してコードを書かねばならないが、今回面倒なのでそこまではしなかった。
    その代わり、1列分(8ライン)のキャラをパターンテーブル前半と後半で重複させておいた。これでこの8ラインの中で切り替えれば画面を乱さないようにできる。

    画像データのインクルード部分は「画像データ(生)」と「画像データ(BMP)」の2種類を用意した。(生)は256×240bitの生データを使う時用で、(BMP)はWindowsのMSペイントで出力したBMPファイルをそのまま使えるようにヘッダ分だけずらした位置に配置するものだ。
    これを使う時は、まずペイントで適当に描いた絵を上下反転
    ペイントで描いて上下反転
    モノクロビットマップで保存して
    保存
    できたファイル名をソースに書き込んでビルドするとできあがり。
    ビルドした表示
    ぜひ試してみてほしい。

    ; 全画面に任意のモノクロ2値のビットマップを表示する
    ; NESASM v3.1でビルド、NNNesterJとNintendulatorで動作確認している。

        ; INESヘッダー
        .inesprg 1 ; PRG-ROM容量(0x4000=16KB単位)
        .ineschr 0 ; CHR-ROM容量、CHR-RAMでは0
        .inesmir 0 ; Don'tCare(水平ミラーリング)
        .inesmap 0 ; マッパーなし

        .bank 0

    ; レジスタ名定義(してたりしてなかったり、してるのに使ってなかったり)
    PPUMASK = $2001
    PPUADDR = $2006
    PPUDATA = $2007

    ;(グローバル)変数定義
    ;RAMの連続したアドレスにラベルを付けたいだけなのだがどうもROMに値をとっている。
    ;どうせ上書きされる場所で実害ないからいいけど、もっとまともな方法があるのだろうか…。
    ;まあとりあえず今回はこのままでいいや。
        .org $0000
    chrptr .dw 0 ;CHR-RAM書き込み時のポインタ
    cnt1 .db 0 ;カウンタ変数
    sprpos .db 0 ;スプライト書き込み時のポインタ
    posx2 .db 0 ;何だっけ

    ;割り込みベクタ
        .bank 1     ; 0x2000(8KB)単位。ファイルが全16KBなのでbank3でなく1。
        .org $FFFA  ; BFFAだけど。

        .dw intvb   ; VBlank割り込み
        .dw init    ; リセット割り込み。起動時とリセット時
        .dw 0       ; ハードウェア割り込みとソフトウェア割り込み

        .bank 0
        .org $8000

    init:
    ; PPUレジスタに書き込みできるようになるまで時間がかかるらしいので待つ
        ldy #25
    .loop:
        dex
        bne .loop ;5*256=1280 ;30000 30000/1280=23.4375
        dey
        bne .loop

    .loop2:
        lda $2002  ; VBlank待ち
        bpl .loop2

        ; VBlankのNMIを無効
        lda #%00110000
        ;     ^||||||| VBlank開始時にNMI発生
        ;      ^|||||| PPUマスター/スレーブ
        ;       ^||||| スプライトサイズ 0:8x8 1:8x16
        ;        ^|||| BGパターンテーブル
        ;         ^||| スプライトパターンテーブルアドレス
        ;          ^|| VRAMアドレス増分 0:1 1:32
        ;           ^^ ベースネームテーブルアドレス 0:2000 1:2400 2:2800 3:2C00
        sta $2000
        lda #%00000110 ; 初期化中はスプライトとBGを表示OFFにする
        sta $2001

        ; パレットRAM=$3F00
        lda #$3F
        sta $2006
        lda #$00
        sta $2006

        ldx #$00
    loadPal:
        lda pallet, x
        sta $2007
        inx
        cpx #32
        bne loadPal

        lda #50
        sta posx2

    setBg: ;BGのネームテーブルを適宜セット
    ; 00,01,...0F,00,01,...0F
    ; 10,11,...1F,10,11,...1F
    ; ...
    ; F0,
    ; 00,
    ; ...
    ; D0,
        ; $2000: BG1 name table
        lda #$20
        sta $2006 ;PPU_ADDR
        lda #$00
        sta $2006
        ldx $00
        lda #30
        sta cnt1
        ldy #0
    bgLoop0:
        ldx #16
    bgLoop1:
        sty PPUDATA
        iny
        dex
        bne bgLoop1
        tya
        sec
        sbc #$10
        tay
        ldx #16
    bgLoop2:
        sty PPUDATA
        iny
        dex
        bne bgLoop2
        dec cnt1
        bne bgLoop0

    ;CHR-RAM書き込み
    setChrRam:
        lda #0
        sta chrptr
        lda #high(chr)
        sta chrptr+1
        ldy #$00
        sty PPUADDR
        sty PPUADDR
        ldx #30

    .loop
        lda [chrptr],Y
        sta PPUDATA

    ; 8: タイル縦px数 000yxxxx ~ 111yxxxx
    ;Y+=20
        tya
        clc
        adc #$20
        tay
        bcc .loop ; if no carry loop
    ; 2: 色bit 0000xxxx, 0001xxxx
    ;Y+=10
        tya
        eor #$10
        tay
        and #$10
        bne .loop ;if !y&10 loop
    ; 16: 横タイル数 00000000 ~ 00001111
        iny
        tya
        and #$0F
        bne .loop; if neq loop
    ; 30: 縦タイル数
        ldy #$00
        inc (chrptr+1)
        dex
        bne .loop

    clearChrRam: ;背景に使わない16キャラ(=256バイト)をゼロクリア (2個スプライトに使う)
    ;    PPUADDR = #$1E00
        lda #0
    .loop:
        sta PPUDATA
        dey
        bne .loop

    setChrRamOverlap: ;BGのベースを変える境目でオーバーラップさせておく
    ;    PPUADDR = $1F00
        lda #high(chr)+$0F
        sta chrptr+1
        ldy #0
    .loop
        lda [chrptr],Y
        sta PPUDATA

    ; 8:タイル縦 000yxxxx ~ 111yxxxx
    ;Y+=20
        tya
        clc
        adc #$20
        tay
        bcc .loop ; if no carry loop
    ; 2:パレットbit 0000xxxx, 0001xxxx
    ;Y+=10
        tya
        eor #$10
        tay
        and #$10
        bne .loop ;if !y&10 loop
    ; 16: 横タイル数 00000000 ~ 00001111
        iny
        tya
        and #$0F
        bne .loop; if neq loop


    ;属性テーブル (BGパレット)
        ldy #$23
        sty PPUADDR
        ldy #$C0
        sty PPUADDR
        ldx #8
    attrloop:
        lda #$00 ; 左半分 00 00 00 00
        sta PPUDATA
        sta PPUDATA
        sta PPUDATA
        sta PPUDATA
        lda #$55 ; 右半分 01 01 01 01
        sta PPUDATA
        sta PPUDATA
        sta PPUDATA
        sta PPUDATA
        dex
        bne attrloop

    ;scroll
        lda #0
        sta $2005
        sta $2005

        ; スプライト配置
        lda #$00
        sta $2003 ; OAMADDR

    ; Byte 0-3: Y,TileNo,Attr,X
    ; Attr:
    ;76543210
    ;||||||||
    ;||||||++- Palette (4 to 7) of sprite
    ;|||+++--- Unimplemented
    ;||+------ Priority (0: in front of background; 1: behind background)
    ;|+------- Flip sprite horizontally
    ;+-------- Flip sprite vertically

        ldx #64
    sprLoop:
        lda #$7B ; BGのベースの切り替わりあたり
        sta $2004
        lda #$E1 ; 1:E0 空スプライト
        sta $2004
        lda #$00
        sta $2004
        lda #$B0 ; 特に意味はない
        sta $2004

        dex
        bne sprLoop

        ; 表示開始
        lda #%00011110    ; スプライトとBGの表示をONにする
        sta $2001

        lda #%11110000 ;VBlank割り込みをON
        sta $2000

    waitInt:
        jmp waitInt


    intvb:
        lda #%11100000 ;BGパターンテーブルベース=0
        sta $2000

    waitVBlankEnd: ;VBlank終了時点でスプライトオーバーフローフラグが消えるのを待つ
        lda $2002
        and #$20
        bne waitVBlankEnd
       
    waitSprOvr: ;スプライトオーバーフローフラグが立つのを待ってBGパターンを切り替える
        lda $2002
        and #$20
        beq waitSprOvr

        lda #%11110000 ;BGパターンテーブルベース=1
        sta $2000

        rti

    ;色0・色1
    C0 = $0F
    C1 = $30

    pallet:
        .db C0, C1, C0, C1, C0, C0, C1, C1, C0, C0, C0, C0, C0, C0, C0, C0
        .db C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0
    ;    .db 0x03, 0x11, 0x15, 0x30, 0x03, 0x11, 0x15, 0x30, 0x03, 0x11, 0x15, 0x30, 0x03, 0x11, 0x15, 0x30
    ;    .db 0x03, 0x11, 0x15, 0x30, 0x03, 0x11, 0x15, 0x30, 0x03, 0x11, 0x15, 0x30, 0x03, 0x11, 0x15, 0x30

    ; 画像データ(生)
    ;    .bank 1
    ;    .org $A000
    ;chr:
    ;    .incbin "image.bin" ; 画像。256×240/8=0x1E00バイト

    ; 画像データ(BMP)
        .bank 1
        .org $A0C2
        .incbin "image.bmp"
        .org $A100
    chr:


    (2019/07/17 追記)
    CHR-RAMの初期化忘れで空白のつもりのスプライトがノイズとなって見えていたのを修正しました。
    やはり実機で動作させないと初期化忘れは見逃しがちである。
    最初に気づいたのは、これをベースに作った9×9ドットフォントの方で、こっちをまず修正した。
    しかし全画面表示の方はVRAMのほぼ全域に書き込んでいたので同じ問題は起こらないと思いこんでしまった。
    実のところ、スプライト用のメモリは初期化していなかったのだ。

    こちらが最初にわざわざ乱数(Xorshift)で初期化するコードを入れて再現したもの。
    初期化忘れ

    再現したところで修正して、乱数初期化を消して出来上がり。(エミュレータでの見た目は前と変わりない)
    初期化あり

    修正箇所はここを
    setChrRomOverlap: ;BGのベースを変える境目でオーバーラップさせておく
        ldy #$1F
        sty PPUADDR
        ldy #$00
        sty PPUADDR
    こう。
    clearChrRam: ;16キャラをゼロクリア
    ;    PPUADDR = #$1E00
        lda #0
    .loop:
        sta PPUDATA
        dey
        bne .loop

    setChrRamOverlap: ;BGのベースを変える境目でオーバーラップさせておく
    ;    PPUADDR = $1F00 ←アドレスが直前からの続きになったのでセット不要に
        lda #high(chr)+$0F
        sta chrptr+1
        ldy #0
    .loop
        lda [chrptr],Y
        sta PPUDATA

    それで直して確認していたところスタックがオーバーフローしていることに気づき、調べてみると
    ・初期化部分のコードの終わりからVBlank割り込みルーチンに流れ込む
    ・VBlank割り込みルーチンは最後にリターンせずにVBlank待ちに入る
    というめちゃくちゃなコードであった。他にスタックを使う関数呼び出しなどしていなかったので動いてしまっていた。
    これも直し、あとついでにCHR-RAMに書き込むとこのラベルがsetChrRom:だったのも修正した。

    上のコードは修正済み。
    なお、基本的に修正前の文章は残すように心がけているのだが、処理が面倒で適当に編集していたので申し訳ないが今回旧コードは残っていない。  

  • 最近のWindowsのビットマップフォントの太字
    2017年01月09日 18:49

    MSゴシックのようなウェイト(太さ)が1種類しかないビットマップフォントをテキストエディタやHTMLなどで太字指定すると、自動生成された太字が表示される。
    単純な太字化アルゴリズムとして古くから用いられているのが横方向に1px太らせる手法で、WindowsでもXPまで長らく使われていた。
    単純な太字
    この手法の問題点は、1pxの隙間を開けて縦線が並んでいる箇所で隙間がつぶれてしまうことだ。
    「棚」「鵬」などの縦線の多い字を小サイズで表示すると黒い塊になってしまう。
    隙間が潰れる

    このつぶれを避ける改良版のアルゴリズムとして、「横に1px太らせてつぶれるなら、太らせない」というものがある。
    スーパーの太字
    なお、右に太らすか左に太らすかで2種類存在する。
    スーパーの商品の値札の文字や、Javaで作られたGUIの文字表示に使われているのをよく見かける。
    最初にこれに気づいたのがスーパーの値札だったので、自分の中でこれは「スーパーの太字化アルゴリズム」と認識されている。
    これを最初に見かけた時は画期的だと感心したものだが、これにも欠点はあって、元々連続的な線であったところが太くなるところとならないところが混在すると形状が乱れて文字が読みにくくなってしまう。
    スーパーの太字_乱れ
    図の赤で示したあたりが不自然な形状になっている。

    そして本題の最近のWindowsの太字だ。これが更に画期的なのである。
    最近のWindowsの太字
    これは最初に気づいたのがWindows7のインストール画面で、Windows7で始めて実装されたものと思っていたのだが、確認してみるとVistaでも使われていた。
    Vistaにも最初からあったのに気づいていなかったのか、それともアップデートで実装されたのか分からない。ともかく、XPまでは無く、7からはある。
    この手法の特徴は見て分かるように白黒2値でなく中間調の灰色を使っている点だ。
    単純な太字化で問題ないところでは黒で横に1px太らせ、単純にはつぶれてしまうところでは灰色で太らせる。
    ここでスーパーのアルゴリズムと異なり、縦に連続する黒ピクセルは常に同じ色で太らせる。これにより形状が乱れることを防げている。

    しかしこのアルゴリズムは思った以上に複雑で色々とわからない点がある。
    Windows太字の不明点
    ・黒にするか灰色にするかの判断
    単純に太らせると別の点に接触する場合は灰色なのかと思いきや、そうでない場合がある。例えば「棚」の赤で示した木偏の縦線は下2pxが隣の月に接触しているが構わず黒で太らせている。

    ・灰色の濃さ
    接触する点が多いほど薄くなっている傾向は分かるのだが、細かく見るとそう単純ではない。
    「鵬」の月の中と、月と月の間は両方とも100%接触なのだが色が違う。率だけでなく、ドット数や分断の有無で変わるのだろうか。
    「繭」の一番左の灰色は、5ドット中4ドット接触だが、100%接触の鵬のドットより色が薄い。

    ・ひとつづきに扱う範囲
    「繭」の赤で示したドットは、真ん中と下は横線で隔てられているが同じ色で太っている。また上は白ドットで隔てられていて違う色で太っている。
    白ドットで隔てられた時は別扱いかと思いきや、青で示したドットは同じ色で太っている。(ともに100%接触だが、ドット数が違えば色は変わるものと思われる)

    そんなわけで詳しいアルゴリズムがずっと気になっているのだが、不思議な事にこのアルゴリズムについてネット上で情報を見かけない。Windows7当時から探しているのだが、本当に1つも見つからない。
    何か情報があればぜひ教えて欲しい。