ファミコンのスクロールテクニックを確認した

前置き(長い)

最近息抜きに Castlevania 3 をやっている. そこで気づいたのは縦スクロールのための空白(空黒?)領域が広いということ.

この領域をなくすか考えてある程度プログラムを作った. この方針でも解決できるとは思うがプログラムが複雑になりすぎた*1こと、不具合の原因調査の過程で nesdev wiki に Split X/Y scroll というテクニックが載っていたので、これのほうが簡単に実装できると判断したので、思いついたアイディアはボツにした.

画面描画中に scroll register を更新する場合、特に Y 座標は CPU address $2006 経由で更新すると 8 pixel 単位の制限がつく. なのでスコアのようなスクロールをしない部分があるゲームで縦スクロールするゲームではスコアは下にあることが大半. スコアが上で縦スクロールするゲームで違和感なく実装できた市販品は悪魔城伝説忍者龍剣伝3の2本だけだと思う. だめな例はデビルマンの洞窟のシーンをみるとわかる. ゼルダの伝説に似たレイアウトは 8 pixel 単位で動いていることが大半.

Split X/Y scroll の自分の解釈

https://www.nesdev.org/wiki/PPU_scrolling
記事のほうが正確なので、自分の解釈はおかしいと思ったら元の記事を確認すること.

レジスタを4度書くだけでそれらの制限から開放される. 各種レジスタ t,v,x,y の動作原理は後述する. 用意することは下記.

  • x,y で 9 bit のスクロール値を作っておく.
    • x は通常の 9 bit の値
    • y は bit 7:0 は 0 から 239 の値を取り 240 から 255 の区間は計算して carray を bit8 に設ける.
  • 下記のプログラムで 4 byte の値を作っておき、スクロールを切り替えるタイミングでレジスタへ書き込む.

自分が作った算出プログラムは下記.

;注意: y bit7:0 の範囲は 0 から 239 (0x00 から 0xef) とする
scroll_bit_concat:
;3 = y8
;2 = x8
	lda	scroll_x_val+1
	lsr	a
	lda	scroll_y_val+1
 rept 3
	rol	a
 endm
	sta	w_2006_0
;7:6 = y7:6
;2:0 = y2:0
	lda	scroll_y_val
	sta	w_2005_1
;2:0 = x2:0
	ldy	scroll_x_val
	sty	w_2005_0
;7:5 = y5:3
;4:0 = x7:3
;y >>= 3
;3.times{
;  y >>= 1
;  x = (x >> 1) | (c << 7)
;}
;	ldy	scroll_x_val
	sty	w_2006_1
;	lda	scroll_y_val
 rept 3
	lsr	a
 endm
 rept 3
	lsr	a
	ror	w_2006_1
 endm
	rts

nametable やパレットはデバッガで出したデータを自分のプログラムに組み込んだ結果は下記のようになる.


サンプルプログラム制作所感

  • スクロールレジスタを書き換える部分はタイミングがかなり重要でできれば HBlank である cycle 256 から 320 の間に4度目のレジスタを書くとちらつかない. mesen ではこれを簡単に確認できるが、それ以前はかなり大変な確認である.
  • タイミング調整のためには IRQ のハンドラ内部では予め計算した結果をレジスタに書き換えるだけにしてループを含む分岐を一切なくす. jsr-rts もできればやらずインライン展開すべきである.
  • 制作したプログラムはスクロールレジスタの更新の後バンクレジスタを書き換える処理をいれたのでちらつきが発生している. ちらつきを避けるには割り込み期間は 2 line の時間を確保しておき、スクロールレジスタを書き換えた後に市販品のような空白領域を設ける.
  • 空白領域でバンクレジスタを書き換えた後、HBlank で空白領域を解除する.

市販品の解析

mesen の event viewer でみた所感.

  • 悪魔城伝説: 先頭の端数分で何も見えないバンクを用意して違和感がない. かなり試行錯誤をしていて無駄な処理が多いように見える.
  • 忍者龍剣伝3: 先頭の端数分で横scrollをすることで空白のnametableを割り当てて端数部を処理している. 処理は簡潔でタイミング調整がきれい.
  • デビルマン: 基本的なプログラムの質が低い. なぜスコア部を下に配置しなかったのか謎.

スクロールレジスタの解説

描画途中に CPU address $2006 を書くときに data で nametable address (例:$20a0)を書くと次の描画で 2 line ずれる. この port を書く用途は CPU address $2007 を介して、CPU から PPU 領域への読み書きに使うのは正しいが、スクロールを更新する場合は name table 参照元として $2xxx を書いてしまいがちで市販品もそれがかなり多いし自分も勘違いした.

CPU address $2006 の1度目の書き込みの data bit 5:4 へ書くと内部で保持されるレジスタは nesdev wiki では t の fine Y scroll と呼ばれるもの. t の fine Y scroll の bit 2 は 0 が代入されて、同 bit1:0 は CPU data bit 5:4 が代入される.

  • CPU address $2007 を介す場合は v の fine Y scroll の bit 1:0 は PPU A13:12 の出力として利用する.
  • PPU が描画として name table を参照する場合は v の fine Y scroll の bit 2:0 は PPU A2:0 の出力として利用し、 PPU A13:12 は 2'b10 が出力される.

この2つは理解しづらく、描画で 2 line ずれる原因となる. CPU address $2006 の1度目の書き込みで fine Y scroll の bit2 は 0 が代入されるのが制限の原因となる. これを回避できるのが Split X/Y scroll の手法である.

*1:8pixelで縦に調整した置いた絵を用意して IRQ を2回かけて空白をなくす

開発日記

database を改める

ROM の hash や構成は MAME の hash/nes.xml を正規化して SQLite にいれて、再集計している. ただ nes.xml の中身は信憑性が低いものが散見されるのでそれの集計や傾向を追っていると時間を浪費してしまう.

信憑性の低さを上げるには図書館や博物館の記録としての知識がいると思う. 集計だけしていても楽しくない.

そんなことを気にしながら本来の目的である、nametable control register がないソフトに対してどのドライバを使うのかという検索のためのデータを出力したり API を作った. 中断期間をいれて1週間かかった.

API を改める

それ以外にも新しい GUI に対しての API の更新が必要でそれの修正. 7月に設定ファイルの管理はGUI レイヤで GUI 担当者に作ってもらったらうまくいなかった. これは私の設計が悪いので、 GUI/CUI 共通部分は mruby レイヤでやれるように9月上旬に修正した. 修正したものの GUI の更新を放置してしまっており CUI だけで使えるようになっていたので、今回ちゃんと統合した.

この処理の都合でファイルの管理がかなり必要になった. 具体的には一時的なファイルの削除と作成、ディレクトリの作成がほしい. cruby ではここらへんは File とか Dir のメソッドが豊富なのに対して mruby はかなり貧弱になっている.

そこで mruby から C レイヤで msys がサポートしている mkdir() とか unlink() をその場しのぎにいれていたが、GUI の統合のために直した. 20年以上前から変わっていない(変えられない)話で Windows のファイル名は非 ASCII であると面倒である. 先述の mkdir() や unlink() のファイル名は最終的には Windows API にリンクするはずだが(未確認)、それらの関数が期待するファイル名の文字コードは default code page というやつで、日本語設定になっている Windows では Windows-31J という文字コードをいれればちゃんと動くが、他の言語を使っている場合は deafault code page は latin とか big-5 なのでだめになる.

これの対処は char * の Windows-31J なり UTF-8 を wchar_t * の Unicode に変換してから Windows API の名前の末尾が W の関数を使うのが適切である(たぶん).

この対応も中断期間をいれて1週間かかってしまった.

ファームウェアの修正にようやく入れる.

完成が見えない. つらい.

68000 アセンブラテクニックその2

move.[bw] #0,dn

clr.[bw] dn

clr.l では moveq に置換するの対して move.[bw] では clr.[bw] を使う. moveq に変えてしまうと使用容量と実行速度が同じ上に bit 31:16 が 0 になってしまうのでよいことはない.

話がそれるが clr 命令はレジスタのみに使う. clr 命令をメモリに使うのはよくない. マイクロコードの都合なのか不要な memory read が発生して遅い. 68010 以降はこの問題がない. bclr 命令はメモリを読んでフラグを更新してからメモリに書くので memory read の必要性はあるんだが、clr 命令はそれがない.

lea (an,dn.[wl]),an

src と dest の an が同じ場合に限る.

adda.[wl] dn,an

data pointer table 共通事項

jump table 共通事項の jmp/jsr がない. 基本方針は jump table と同じで dc.l を dc.w か dc.b の配列に変える.

このため index が pointer の数を超えている動作の場合は同等にならない. jump table では高確率でおかしくなるので元々のプログラム開発時に気づいて直していると思うが, data table は気づかずに動いてしまっていることが十分に考えられる.

pointer の中身が1種類

jump table の場合
	jmp pointer
data table の場合
	lea poiter,an

意外なことにこれが結構あった.

pointer が等間隔

pointer0
	dc.w	0,1,2,3,4,5
pointer1:
	dc.w	6,7,8,9,10,11

基本は data pointer table だけに適用する. jump table の場合は定数設定だけで分岐している場合だったので適切なコードを作った方がいい.

;間隔12の場合は index * 4 + index * 8 -> (index << 2) + (index << 3)
;として乗算をシフトと加算に変える. 
	lsl.w	#2,dn
 if near
	lea	(pc,pointer0,dn.w),an
 else
	lea	pointer0,an
	adda.w	dn,an
 endif
	lsl.w	#1,dn
	adda.w	dn,an

2の累乗ではない値の乗算回避のテーブルと思いきや間隔が 2 や 8 もあった.
加算回数が多すぎる場合は自動生成をやめて元通りテーブルを使うのも悪くはない. その場合は dc.l (→ lea; movea.l) から dc.w (→ lea; adda.w) に変えるほうが容量を削減できる.

pointer の中身が -0x80 から 0x7f で収まる場合

ほかの条件は jump table とやることは大体同じなので省略.

 if near
	move.b	(pc,table,dn.w),dn
	ext.w	dn
	lea	(pc,table,dn.w),an
 else
	lea	table,an
	move.b	(an,dn.w),dn
	ext.w	dn
	adda.w	dn,an
 endif
	...
	...
table:
	dc.b	pointer0 - table
	dc.b	pointer1 - table
	align	2

その他

pointer table の index を 8 bit でうち bit7 を別の意味を持たせている場合.

元のコード
	lea	table,an
	tst.w	dn
	bmi	xxx
	andi.w	#$007f,d7
	lsl.w	#2,d7
	movea.l	(an,d7.w),an
	...
	...
	...
xxx:
	andi.w	#$007f,d7
	lsl.w	#2,d7
	movea.l	(an,d7.w),an
	...
	...
	...
修正後
	lea	table,an
	andi.w	#$00ff,d7 ;bit15:8 を 0 にする
	lsl.b	#1,d7 ;.w から .b に変えて bit7 の有無をフラグにする
	movea.w	(an,d7.w),an ;movea はフラグが変わらない
	bcs	xxx
	...
	...
	...
xxx:
	...
	...
	...

src の bit0 を dest の bit7 へ代入

static uint8_t hoge(uint8_t src, uint8_t dest)
{
	dest &= 0x7f;
	if(src & 1){
		dest |= 0x80;
	}
	return dest;
}
;d0 = src, d1 = dest
	lsl.b	#1,d1 ;d1 <<= 1
	lsr.b	#1,d0 ;x=d0[0]
	roxr.b	#1,d1 ;d1[7:0]= {x,d1[7:1]}

68000 アセンブラテクニックその1

データ分離して逆アセンブルした状態で、プログラムから置換をするという試行. プログラム全体の流れをみることはないので正常動作としては100%同じ動作になることを優先.

clr.l dn

moveq #0,dn

move.l #imm,dn

imm は -0x80 から 0x7f に限る.

moveq #imm,dn

(add|sub)[ai]?.[bwl] #imm,ea

imm は 1 から 8 に限る. moveq と違って ea の制限が少ない.

(add|sub)q.[bwl] #imm,ea

(jsr|bsr) ea; rts

削除する rts にラベルが登録してない場合に限る.

(jmp|bra) ea

(jsr|jmp|lea) abs(,an)?

jsr,jmp,lea で例が多いので ea 次第では他の命令で利用可能.

ea が -0x8000から0x7fffの場合
(jsr|jmp|lea) abs.w(,an)?
ea が pc+2 との距離が -0x8000から0x7fffの場合
(jsr|jmp|lea) (pc,ea),an

bCC ea

bsr ではなく, ea と pc+2 の距離が 0 の場合
(削除)
ea と pc+2 の距離が -0x80 から 0x7e の場合
bCC.s ea
  • 元のプログラム作成側では省略すると .w になっていたような挙動なので削ることが結構できる.
  • 私が利用しているアセンブラの機能として .s か .w を書かないと最短にしてくれるがアセンブルに時間が異様にかかる.

jump table 共通次項

下記の命令を想定.

	lea	table.l,an
	move.w	ram,dn
	lsl.w	#2,dn
	movea.l	(an,dn),an
	jmp	(an)
table:
	dc.l	pointer0,pointer1,...

jump table が近い場合

  • jmp/jsr の pc+2 との距離が table が -0x80 から 0x7e の場合. jsr は近くにないことがある.
  • addressing (offset,pc,ix) を使うため offset が狭い.
pointer と bra.s の PC + 2 との距離が -0x80 から 0x7e で収まる場合
	move.w	ram,dn
	lsl.w	#1,dn
	jmp	(table,pc,dn.w)
table:
	bra.s	pointer0
	bra.s	pointer1

命令数は減るが実行数は変わらないと思う.

pointer の値が -0x8000 から 0x7ffe で収まる場合
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(table,pc,dn.w),an
	jmp	(an)
table:
	dc.w	pointer0
	dc.w	pointer1
pointer と table の距離が -0x8000 から 0x7ffe で収まる場合
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(table,pc,dn.w),an
	jmp	(table,pc,an.l)
table:
	dc.w	pointer0 - table
	dc.w	pointer1 - table

jump table が遠い場合

pointer と bra.s の PC + 2 との距離が -0x80 から 0x7e で収まる場合
	lea	table,an ;先述の table.w, pc(table) も併用する
	move.w	ram,dn
	lsl.w	#1,dn
	jmp	(an,dn.w)
	...
	...
	...
table:
	bra.s	pointer0
	bra.s	pointer1
pointer の値が -0x8000 から 0x7ffe で収まる場合
	lea	table,an ;同上
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(an,dn.w),an
	jmp	(an)
	...
	...
	...
table:
	dc.w	pointer0
	dc.w	pointer1

pc に関わらず利用できる.

pointer と table の距離が -0x8000 から 0x7ffe で収まる場合
	lea	table,an ;同上
	move.w	ram,dn
	lsl.w	#1,dn
	adda.w	(an,dn.w),an
	jmp	(an)
	...
	...
	...
table:
	dc.w	pointer0 - table
	dc.w	pointer1 - table

これはあまりいいものが思いつかなかった.

pointer が上記の方法で16bit以下に収まらない場合

pointer の最大値と最小値の差が 0x10000 未満の場合
center	set	pointer.min + (pointer.max - pointer.min) / 2
	lea	table,an ;同上
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(an,dn.w),an
 if center < 0x8000
	jmp	(an,center)
 else
	adda.l	#center,an
	jmp	(an)
 endif
table:
	dc.w	pointer0 - center
	dc.w	pointer1 - center

一応動作確認したが pointer の最大値と最小値を算出するのはビルドの過程で効率が悪すぎる. table の中身が奇数になることもありちょっと変. また adda.l が増えるので遅いので実用に向いていない.

憂慮

長年 web 検索に google を使っていたがおせっかいな機能(映画のネタバレのような知らないふりをして楽しむことに事実を目立つ位置に堂々と書いてきた)とUIがおかしくなってきたことが原因で別の検索エンジンを検討している.

google が直接悪くはないが、プログラミングや数学について調べごとをすると、参考にできないような試行錯誤の報告がでてくるならまだいいものの、現役xxxが解説!!という google が間接的に悪くしているものがあまりにも多くなりすぎてる. 特定のドメインを消すプラグインがいまはたくさんあるのでいれてみたが、そういうツールはいつの間にか使えなくなることが何度もあったので気休めかもしれない.

プログラミングや数学については規格書原文、開発ツール提供団体、大学、試行錯誤した個人の報告順に優先順位をつける. 個人の報告は利用した手段を提供団体や規格書まで遡って信憑性や妥当性を確認する.

ruby ができて mruby にできないことは提供団体が提供する C レベルの実装に対する文書があまりにも少なく10年前の個人の報告をもとにヘッダファイルを読むことがある(例えば Array.size に相当する C レベルの呼び出し). 運が悪いとソースコードを読むことになり、本来の目的から離れていく.

その点では Microsoft ドメインが提供する WindowsAPI の解説は優秀である. 検索でもノイズをかき分けながら頭の方にでてきてくれる.

開発日記

DMA 転送途中への USB での送信

8月中に実装した機能で録音と disksystem は相手のデバイスを動かしながらデータを取得し、バッファが半分溜まったら USB で送信するというリングバッファ状態を実装していた. ここまで下地を作ってしまったので ROM dump でも実装をしてみた. 結果はシミュレータの実装にかなり手間取ったがちゃんと動いた.

転送速度

1 byte あたりの転送速度は下記となる.

  • voice: 48 kHz (1 sample 12 bit で、サンプル頻度 24 kHz)
  • disk: 12 kHz (かなりブレるので早めの参考値)
  • ROM: 747 kHz (これもブレるので早めの参考値, 740 kHz の場合もある)

voice は DMA を利用しているが, disk では evsys の制約から DMA を利用できないので (MCUの中の) CPU でバッファへ転送している. このレベルであれば ROM は相手が動くのを待つことを無視できるので MCU への実装次第でもっと早くなるが、安いこの MCU の機能を利用して安定させたらこの速度が限界と思われる.

ROM 向けの DMA の実装

DMA channel を 3 つ用意し AH (出力, A15:8), AL (出力, A7:0), DR (入力, D7:0) に割り振る. AL は 0x100 byte 周期で descriptor 1個無限ループ. ここまではいままでの実装.

DR を descriptor を 2つ用意し、buffer を半分に割り振り、descriptor 2つで無限ループ. descriptor 1 つが終わったときに evsys 経由で割り込みをかける. 録音で実装済みで使い回す.

AH は buffer は buffer size を 0x100 bytes 単位で割り振り, 最大 0x100 bytes を 1 descriptor とする. 例えば buffer が 0x550 bytes, 開始アドレスが 0x0080 なら下記となる.

 # data  count link
 0 0x00  0x80  1
 1 0x01  0x100 2
 2 0x02  0x100 3
 3 0x03  0x100 4
 4 0x04  0x100 5
 5 0x05  0x0d0 end

これがいままでの実装でこれを下記のように AH の DMA を #1 から #5 で無限ループにする.

 # data  count link
 0 0x00  0x80  1
 1 0x01  0x100 2
 2 0x02  0x100 3
 3 0x03  0x100 4
 4 0x04  0x100 5
 5 0x05  0x100 1

DR descriptor の1つが終わると 0x2a8 bytes 読んだので address 0x0080 + 0x2a8 = 0x0328 まで読み終わったので descriptor の参照先の data の 0, 1, 2 を更新する. (ただし data 0 は無限ループ外なので書き換えても意味がない)

 # data  count link
 0 0x00  0x80  1
 1 0x06  0x100 2
 2 0x07  0x100 3
 3 0x03  0x100 4
 4 0x04  0x100 5
 5 0x05  0x100 1

DR descriptor が終わるたびに更新を続ける. address 0x1f7f まで読み込むとすると AH の参照先 data 0x1f で count と link を書き換えてしまう.

 # data  count link
 0 0x00  0x80  1
 1 0x1f  0x80  end
 2 0x1b  0x100 3
 3 0x1c  0x100 4
 4 0x1d  0x100 5
 5 0x1e  0x100 1

感想

参照先を書き換えるのは問題ないのは確認しているが descrptor を書き換えてしまうとちゃんと動くのか少し不安だったが動いてくれた.
今回の実装での手間はこの descriptor と参照先の書き換えをシミュレータで実現すること. C だけで作っていればポインタで更新されるが、 mruby の変数にしていたのでこれを更新する仕組みを作るのがかなりの手間.

一旦実装は出来たものの末尾の更新がうまくいかずにかなりの時間を要してしまった. 上記のリストであれば #0 から更新していたので #1 の link が end になっていると #2 以降が古いままになっている. これに気づくのに2日間、一番古いデータから(上記では#2 から)更新する仕組みを思いつくまで2日間かかってしまった.

今後は flash programming 以外の基本機能はようやくこれで実装したので後回しにしていたUI周りをちゃんとつくる.

開発日記

Disksystem の対応

この手の ROM dumper で diskimage を dump できるのかをようやく挑戦した. カードエッジの CPU(2A03) IRQ# を MCU の外部割り込み端子とをこの目的のために接続してありようやく動作確認した. 結果は数日で diskimage を dump できるまでになった. いまは解析資料が豊富にあるからとても助かる. nesdev wiki のひと、そこに貼ってあったリンクの方々、ありがとうございます.

GAP 区間と RAM アダプタ

RAM adapter からデータを取る場合に特有な問題は GAP 区間に read/write にするレジスタを有効化し、正味のデータの区間が終わった後はそのレジスタを無効化する必要がある. これをやらないと次の正味のデータ区間の中身がデタラメになってしまう. この制約をクリアするにはハードにより近い MCU で効率的に行う必要があり、PC からスクリプト経由で随時命令を送ることが困難である.

この制限のせいか、エミュレータFDSファイルは正味のデータのみで、正味のデータでも末尾のCRCは捨てている.

実装途中はこの仕様を理解していなかったのでデタラメにレジスタを有効にしていたもので、1度のシーケンシャルアクセスで正味のデータが1区間だけ見えるということになった. ただ GAP とか ブロック開始符号が見えたので、試せば正味以外のデータ区間も見ることができるのではないだろうか.

でもディスクドライブのコネクタに直接つなぐような機材は実在して安く手に入るし、そっちでは完璧なデータが効率よく見えるのはわかりきっている.

ディスク末尾のデータ

FDS ファイルは正味のデータだけを1面 65500 bytes と定義されているがファイル(ブロック)を増やせば GAP 区間は増えて正味のデータ区間は減る. この矛盾を理解できていないので、ディスク末尾はどこまで読めばいいのかよくわからない.

そのせいなのか、私の作った dumper では末尾に配置されるセーブデータが dump できていないとか起動しないというソフトが1つずつ見つかった. 逆に言うとそれ以外はほとんど問題がないらしい.

MCU の話

外部割り込み自体はあっさり認識したものの、 DMA の CTRLB と EVSYS のあたりがまたうまく行かずに RAM アダプタからの IRQ をトリガに動かすということに苦労した.

GAP 区間では待ち時間の対応を入れるためにタイマのソースコードを書き直した. DMAのつなぎ合わせで待ち時間 2 clock だけのタイマを大量に利用しているとかでタイマが足りなくなっているため、必要応じて切り替えを多用することにした. これは実はかなりキツイので実装の見直しを考えた方がいい.

例えばアクセスランプの点滅にタイマを確保していたのだが、これもやめたほうがいい. 10から60Hzの点滅にタイマを使うぐらいなら割り込みのたびにソフトでIOを ON.OFF したほうがいいと思う.

ソフトウェア面では先述のディスク制御特有の処理のために C で実装したので ROM の空きが 10% を切ってしまった. ほかは RAM 管理を静的に確保するのに限界をとても感じているので動的に確保するようにしたい. 今回のせいで C++ ではないのにヘッダファイルが200行にもなってしまった巨大な struct を分離する必要が出てきた.

開発日記

複数の似た処理の統合

かなり手間取ったが統合できた. 入力命令の解釈が不適切で DMA 2ch で動かせる内容を DMA 4ch で動いていて結果としてはあっているという問題が直せた.
data を increment-pattern でいれると MCU が止まってしまうバグがあったが、統合時に increment-pattern と無限ループに対する定義が仕様書になかったので明示して、それに準拠した.

DMAC が RAM に結果を書くタイプの設定不具合

ソースがきれいになったのでいままでやれなかった電源投入後のDMA開始の調整と DMA descriptor と EVSYS を再設定無しに再実行を実装.

ADC が動かない. これまで3度の改訂で処理を少しずつ変えているのだが毎回理由不明で苦しめられる. 今回に関しては再実行で read した data (DR) の先頭にゴミデータが入るという不具合も発生.

今回の傾向から判断するに...

  • DR と ADC は EVSYS 経由ではなく CHCTRLB.TRIGSRC から取って来るものである.
  • CHCTRLA.ENABLE = 0, CHCTRL.SWRST = 1 にしても改善しない.
  • DMA 完了割り込みのときのみに CHCTRL.SWRST = 1 するとなぜか改善する.

以上から CHCTRLA.ENABLE = 0 であっても TRIGSRC を書き換えていないと、DMA ch は TRIGGER を認識しているが ENABLE = 0 なので pending になってしまって DMA がつまるのだろうという仮説ができた. 今回の実装では DMA 完了割り込みのあとに SPI を動かしたりすること、 TIMER が止まってないと ADC が再度動いてしまうことが予想できたからだ.

ということで CHCTRLB = 0 (全ビット)としたら改善した. DR と ADC の channel の TRIGSRC のみ = 0 としたがこれは改善せず、全ての channel の CHCTRLB = 0 としたら期待通りになった. 他の要因は EVSYS につなぐことなんだろうけどここらへんは直接レジスタを見づらく対処に困る.

TRIG がつまってしまった場合にそれを明確に止める方法がないのが対策に時間がかかった.

開発日記

前振り(ながい)

昨年8月まで設計は 74595 を 3 つにして、 CPU PHI2, CPU ROMSEL といった制御線は GPIO で制御していたが、 flash programming の性能がよくない(具体的にはソースが複雑になりすぎる上に遅い)ので制御線も 74595 を 1つ追加して DMA で動かすことにした.

ROM dump では address が increment, data 出力は不要. この場合は動かす address 16 bit で 74595 は 2 個ですむため追加した制御線の DMA を利用するかは条件を設定して切り替えるようにした. 一方 flash programming は制御線を DMA で激しく替える. ROM dump で制御線もいれて動かすこともできるが処理速度は半分になる.

複数の似た処理の統合

このため制御線の DMA を利用するか否かはソースでは処理の途中で新旧の実装でかなり分岐していて、最後にハードウェアを動かすところで1つになる. 途中の分岐は無駄が多いのでできるだけ減らすのが今回の目的.

ハードウェアに近いレイヤの統合は予想外に早く終わったのでその上位のレイヤの統合をし始めた. こちらは難易度が高く、統合ができていない. 統合の処理は使い回す関数をコピーして flash programming のコードは一旦削除して、簡単に構造に戻している.

転送の限界調査

ハードウェアに近いレイヤの統合の時点で ADC の Sampling Freq を可変値にすることができた. 音質は重要ではなく sampling freq を上げていって、どの速さまで限界を知りたい. できれば ROM dump の DMA が動いているときに USB の転送も ROM dump より早く動いてくれているとわかればもっと早くできるし、DMA を頻繁に止める処理もいらなくなる.

sampling freq は前回は定数 16 kHz だったので、20 kHz, 30 kHz と上げていったが 21 kHz で止まった. 一番長い音声が約3秒で、21 kHz の時点でデータ量が約 65,000 bytes になっていた. 止まった理由は一度に転送できるデータを 16 bit に指定していてそれを超える量を想定した設計になってない. つまりプロトコルの再設計が必要.

Cにおけるラベル直後の変数宣言とswitch

void hoge0(int v)
{
	//{} がない switch, 分岐が1つだけで使い物にならない
	switch(v)
	label: break;
	
	//{} がないので label は有効
	goto label;
}

void hoge1(int v)
{
	//{} がある switch 
	switch(v){
	case 1: break;
	label: break;
	}
	
	//label: は switch{} のそとでも使える (なんで?)
	goto label;
}

int hoge2(int v)
{
	switch(v){
	case 1: //error, case x:, label: の直後に変数宣言はできない
		int i = 0;
		break;
	label: break;
	}
	return i;
}

int hoge3(int v)
{
	switch(v){
	case 1: ; //ダミーの ; をいれれば変数宣言は通る
		int i = 0;
		break;
	label: 
		i = 2; //i は有効
		break;
	}
	//error, i の有効範囲は switch の {} に制限されている
	return i;
}

int hoge4(int v)
{
	switch(v){
	case 1: { //ダミーの {} をいれれば変数宣言は通るが
		int i = 0;
		}break;
	label: 
		i = 2; //error, 上の {} の外では i は 無効
		break;
	}
	//error, この i も無効
	return i;
}
int hoge5(int v)
{
	switch(v){
	int i = 0; //ラベルをいれなかったらこの変数宣言は有効
	case 1:
		i = 2;
		break;
	label:
		i += 3; //この場合, i の宣言だけが有効で代入(i=0)は無効
		//case の前か後かは関係ないようだ
		//コンパイラが未初期化の値を利用していると警告した
		break;
	}
	if(i < 4){ //error, i の有効期間は {} の中まで
		goto label;
	}
	return i;
}