で、思いついてしまったのだが、ROM/RAM を共有する DUAL_CORE が簡単にできる。- ROM/RAM を取り外して、外部で 構成するだけだ。単なる DUAL_CORE ではなく、SMP (Symmetric Multiple Processor) だ 。
これは、是非ともトライしたい。SMP を自分でデザインすることになるとは、予想もしなかった要素だ。ますます、面白くなってきてしまった。ちょっと検討してみよう。
( 『bitstreamベースのAVRコア(案)』なんて記事を書いたがどうでも良くなった。準備だけは出来たから、いずれやることにして、凍結しよう。)
SMP でも いわゆる Processor 固有領域というものが必要だ。スタックがあるから 下位 256B は違う領域を指さないと困る。
そうなると下位 128B までにある グローバル変数も プロセッサが違うと 違うデータになる。
共有するのは上位アドレスの領域。
SRAM は 2KB もあるから、両者でマッピングを変える。
マッピングについては、後で詳細を詰めるとして ... RAM をもっと付けたくなるかも知れない。
いままで、RAM には、2KB の空間しか渡していなかったが、8KB の空間を渡すように変更しよう。(ただし、8KB のとき 使えるのは、8KB - 64 バイト)。
サイズの設定は、ROM と同じように RAM_SIZE=2048 といったパラメータにする。
ROM には、プログラムから読み込む機能があるが、2つのポートを それぞれの AVR に割り振ると それができなくなる。プログラムメモリを節約できるかわりに、初期値付きデータが利用できなくなったりして 使い勝手が低下する。それに ISP でも 2 つ目のポートを使っている。
やはり、gcc の普通のプログラムが動かないのは論外だし、ISP も使いたい。
プログラム と データを同時にアクセスしないよう AVR のパイプライン制御に手を加えると、CPU あたり 1 port で済ませることが可能になる。(従来のAVR は LPM命令でそうしていた)
... このコードを実装してみた。変更する所を最小にするために、見かけは Dual Port だが、競合しないことが保証されているので、1 port しか使わないようにしたわけだ。
define は、ROM_PSEUDO_DP 。
これをベースに、擬似 4 port の ROM module を作ってやれば良い。 ISP から見るとインターフェイスが 2 つになる。が、実体は 1 つなので、どちらを使っても良い。
あと、プロセッサ間の通信。メモリを共有しているから、基本的にデータの転送は不要だ。ロックの命令は作るつもりはない。同期制御には、イベントの通知だけを使う。-- これは USART を相互に接続することで、対応しておこう。(... 最初の考え)
一旦シリアルにして、またパラレルに戻すのは無駄が多い。インターフェイスは、USART だが実際はパラレル ... FT232R と FT245R の関係 のようなモジュールを作っておきたい。
それに、FIFO 化もしたい。USART は 既に 1 つの FIFO に対応しているから、少し仕様を追加するだけで、使い勝手良く実装できそうだ。
... これについては、『AVR互換コア(FIFOモジュール)』の記事参照。
IOR は独立に持つ。SP/SREG が含まれているし、通信するにもデバイスを独立に制御できなくてはならない。
- プログラム空間が 1 つであれば、プロセッサ ID が必要だ。attiny40 ならば、そういうデータを置く領域が 64bit の空間にある。-- rtavr はどうしよう。
現状 NVMCSR を使って ROM のプロテクトをいれている。DUAL CORE なら 相手の RAM のプロテクトを入れたい。どうせ仕様変更が必要なら、そこに プロセッサ ID を割りこませよう。 - IOR のデバイスの構成は、ifdef になっている。現状だと CPU0 にだけ 特定のデバイスを付ける... といったことができない。同じデバイスを両方とも持ち、空間や割り込み番号、ポートへのマッピングは同一になってしまう。
これは... 一体どうしたら良いのだろう?
rtavr は、IOR を外すことさえできる。SMP 専用の IOR を作り、外部で接続することは可能だ。だが、デバイスの実体が 1 つだとすれば、調停機構が必要になる。IOR 側だけでなく、コア側にも リクエストと ACK/NAK の概念を入れないといけない。あと、割り込みもどういう風に制御するのか設計が必要。
ROM の調停機構が入ったから、これをヒントに軽く検討してみよう。
まず、IOR へのアクセスには、IN/OUT CBI/SBI と 条件スキップの SBIC/SBIS 命令、さらに X/Y/Z レジスタ間接の LD/ST (+LDD/STD) 命令がある。
レジスタ間接は、アドレスが確定するのが遅いから、調停したくない。-- この条件から IOR のDual Port 化は必要ということになる。ー アクセスはできるが、競合した結果は保証しないという風にまず持っていくわけだ。その後で実際には競合しないように制御を追加すれば良い。
さて、IN/OUT CBI/SBI と SBIC/SBIS は、アドレスが 6bit しかなく、INST にアドレスが入っているから、S1 で速やかに判断できる。S2 で 実際に使う 1 クロック前に 判断できるので、競合したときは、パイプラインストールさせれば良い。
ここでひねりを入れる。SBIC/SBIS の後続命令が CBI/SBI , IN/OUT なら 続いて IOR を専有させる.. と見事にロックができる。( ただし、2 つの命令の間で割り込みが起きないようにしておかないといけない。)
ロックする対象のレジスタは、なんでも良いが ソフトで使える GPIOR (0/1/2) というのも用意する。( 普通の AVR にはあるが、モデルとなった attiny40 にはないので追加する )
(追記) ロックする対象のレジスタは、0 - 0x1f の範囲。 CBI/SBI と SBIC/SBIS はその範囲しかアクセスできない。
... そういえば RAMWIN の RAMDR が 0x1f , RAMAR が 0x20 でなぜ半端なとこに置くのか不思議だったが、こういう理由があったのか。
... どうも作ることは、可能そうだ。ならば是非トライしたい。
RAM空間の定義
まず、いままで RAM をどう使っているか整理しておく。
RAM物理アドレス 論理空間
(RAM PHYS) (VIRTUAL ADDR)
+----------+
| |
0x0040-0x0800 | A | 0x0040-0x07ff
| | (0x0840 - 0x0fff)
+----------+
0x0000-0x003f | B | 0x0800-0x083f
+----------+
アドレスに IOR 空間の分を加算せず サイクリックに使っている。B の部分は、0x800-0x840 で見えるわけだ。ちなみに、デコードをさぼっているので、0x0840 - 0x0fff も A の部分が アクセスできてしまう。
考慮すべき要素:
- LDS/STS 命令は、RAM の下位 128B しかアクセスできない。空間で言うと 0x0040 - 0x00bf 。
- AVR_Toolchain の設定は、attiny40 と同じにしているので、RAM は 256B しかない扱い。初期化も 256B までしかしないし、スタックポインタも 0x013F に設定される。
- スタックのアドレスは、再定義することが可能だ。また、malloc のプールの範囲も再定義が可能。
これを DUAL_CORE 化でどうするか ...
RAMの下位アドレスは、プロセッサ固有空間にしなくてはならない。malloc のプールなどがあるから、256B と限らず もっと大きくしたい。かといって 共有空間では同じアドレスでアクセスしたい。
ロックがないから、共有のプール領域の管理が難しい。それぞれのプロセッサが管理する領域を持つとすれば ... 相手が割り付けた領域を 使うことはできるが、相手しか free できない。
まぁ、AVR で malloc を使ったことすらないのだが... 。
(追記) ロックは可能になったから、共有のプール領域の管理が普通の SMP マシンのように出来る。プロセッサ固有空間のサイズを RAM 容量に合わせて変更できるようにしてみたが、使わなさそうだ。
ちなみに、ロックは割り込み処理との排他にも使える atomic 操作。シングルプロセッサでも有効に使える要素がある。
RAM マッピング
- CPU 1 では、RAM の 下位 1KB に対して アドレス の bit 9 を反転する..というのはどうだろう?
CPU0 CPU1 WP(self)
+----------+ +----------+ WP(other)
0x0800-0x083f | PSB(1) | | PSB(0) | x o
+----------+ +----------+
0x0400-0x07ff | COMMON | | COMMON | x x
+----------+ +----------+
0x0240-0x03ff | PSA(1) | | PSA(0) | o x
+----------+ +----------+
0x0200-0x023f | PSB(0) | | PSB(1) | o x
+----------+ +----------+
0x0040-0x0200 | PSA(0) | | PSA(1) | x o
+----------+ +----------+
PSA(0) プロセッサ固有A CPU 0
PSB(0) プロセッサ固有B CPU 0
PSA(1) プロセッサ固有A CPU 1
PSB(1) プロセッサ固有B CPU 1
共有領域は、RAM_SIZE - 1KB 。
プロセッサ固有A アドレスに + 0x200 すると 相手の データにアクセスできる。だが、プロセッサ固有B は、一種の HIGHMEM でややこしい。
自分用のメモリだが、RAM のライトプロテクトをかけると、相手ではなく自分が書けなくなる。相手からは自由に書けるのだが、8KB のときだけ相手は見えない。
この制御は、外部でも出来るが、CPUID を rtavr の中に持たせるので、rtavr の中でも出来る。-- rtavr で制御することにしよう。
ちなみに CPUID は、rtavr のパラメータにする。
ロックの実装
まず、-- 条件スキップの SBIC/SBIS 命令に続く IN/OUT , CBI/SBI の間では割り込みを入れないというところ -- こんなので良いのか検討してみよう。
// 0x81: CPU0, 0x82 : CPU1
if (bit_is_clear(GPIOR0, 7)) GPIOR0 = 0x81;
if (bit_is_set(GPIOR0, 0)) { // ロック成功
:
GPIOR0 = 0x0; // アンロック
}
こういう使い方はできる。だが、ロック変数を最大で 3 つまでしか持てない。
そうなると、二重ロックを使わざるを得ない。
#define bit_set(port,bit) { port |= (1<<(bit));}
#define bit_clear(port,bit) { port &= ~(1<<(bit));}
if (bit_is_clear(GPIOR1, 0)) {
// 0x81: CPU0, 0x82 : CPU1
if (bit_is_clear(GPIOR0, 7)) GPIOR0 = 0x81;
if (bit_is_set(GPIOR0, 0)) { // LVL1 ロック成功
if (bit_is_clear(GPIOR1, 0)) { // LVL2 ロック成功
bit_set(GPIOR1, 0);
GPIOR0 = 0x0; // アンロック LVL1
:
:
bit_clear(GPIOR1, 0); // アンロック LVL2
}
}
}
LVL1 ロックは、LVL2 ロック変数を変更する権利だと考えれば、別に GPIOR1 にロック変数を置く必要はないのかも知れない。
まぁとにかく、複数命令間でアトミックでないと困るのは、 GPIOR0 = 0x81;
割り込みを入れないとか、2 連続でアクセス権利を獲得できるのは、
条件スキップの SBIC/SBIS 命令に続く IN/OUT , CBI/SBI で 同じアドレス。
という条件が良いかも知れない。
(追記) RAMWIN のことを忘れていた。
RAMWIN は、IOR 経由で RAM にアクセスできる機能。最初の実装は、RAM の dual port を使っていたので、SMP では使えないものと思い込んでいた。
だが、これを使えば、RAM の任意のバイトに対し LVL1 ロックが可能ではないか。
unsigned char *lock;
unsigned char lock_val = (NVMCSR & 1) ? 0x82 : 0x81;
// 0x81: CPU0, 0x82 : CPU1
cli();
RAMARH = (uint16_t)lock >> 8;
RAMAR = (uint16_t)lock & 0xff;
if (bit_is_clear(RAMDR, 7)) RAMDR = lock_val;
if (bit_is_set(RAMDR, 0)) { // ロック成功
sei();
:
cli();
RAMARH = (uint16_t)lock >> 8;
RAMAR = (uint16_t)lock & 0xff;
RAMDR = 0x0; // アンロック
sei();
} else { // ロック失敗
sei();
}
こんな使い方ができる。lock 変数は、共有領域でないといけない。
こういうことならば、RAMWIN の機能をちゃんと実装しよう。
対応状況
- ROM_PSEUDO_DP
これは、ROM 領域を LD/ST(+LDD/STD) でアクセスする場合に、命令の読み込みと競合しないようにする。ROM とのインターフェイスは、相変わらず DUAL PORTのままだが、ROM 内で SINGLE PORT 化可能になった。
- SUPPORT_LOCKING
『条件スキップの SBIC/SBIS 命令 の後続命令 が、同じアドレスに対する IN/OUT , CBI/SBI ならば、その間に割り込みを入れない』
.. いまのところこれだけの機能。これは、比較的簡単に入った。
だが、妙なことに規模があまり増えない。... 調べてみると SBIC/SBIS 命令の S0 でのプリデコード PD_SBIX が規模を減らす効果があった。さらに調べると、NEXTPC_TUNE でオプション化されていた PD_EXT (命令群のプリデコード)も、同じく規模が減る。この 2 つを 標準に変更。
さて、今の SUPPORT_LOCKING は、まだまだ布石。IOR の調停機構にお伺いを立てて ACK を受け取らない限り パイプラインをストールさせる機能も入れる。... だが、それをする前に、IOR の Dual Port 化を完了させなければならない。 - IOR_HAVE_GPIOR
0 - 0xf の 16 個のアドレス範囲に対して GPIOR をサポート。
.. FF で作るより、分散メモリで作った方が 規模が減る。なにしろ 1:16 だ。1 つの GPIOR を作るコストで 16 個分用意できる。また、この変更に伴い、CPI0/CPI1 のアドレスを移動。
実装は、レジスタの下に置くという風にした。読み出しで、同じアドレスに 他のレジスタがなければ GPIOR を読み込む。書き込みの方は、レジスタがあっても書きこんで構わない。
ただ、デコードに対するオプションが、LARGE_MAPPER , MUX_TRI , MUX_OR , +デフォルト とあって実装の組み合わせが増える。MUX_TRI , MUX_ORは、『レジスタが上にない』という意味の 信号を定義しないといけなくて特に面倒。なので、MUX_TRI , MUX_ORを切り捨ててコード削除。今後 MUX_TRI は使えなくなる。 MUX_ORは、使えるが IOR のサブモジュールに対する指定に限定。 - IOR_HAVE_RAMWIN, IOR_HAVE_RAMWIN_HA
これを Single Port でちゃんと実装することにした。 IOR_HAVE_RAMWIN_HA は、RAM の上位アドレス(HA) を指定する機能で、アクセスしやすいように 仕様変更した。
実装する際にアドレス空間について見直した。もともとは、単に RAM の物理アドレスを差すだけだったのだが、CPU1 からアクセスする場合、アドレス変換が入らないと使いにくい。また、RAM のライトプロテクト機能も効かせたい。
... というわけで、アドレス変換は少々面倒なことになった。ちなみに対応コードは rtavr.v 。
だいぶ進んだが、IOR Dual Port 化への道は遠い。
メモリなどと違って、リソースの種類が沢山あるから、恐ろしく面倒なのだ。
まずやらなければならないことは、CPU 固有のリソースと装置全体のリソースに分けること。 - rtavr_ior_ps.v
CPU 固有のリソースは、SREG/SP , INT0(+INT1) , NVM , RAMWIN 。それに加えて TMR0 / CPI0 ということにした。CPI0 は、Dual Port 化 IOR 内で クロス接続の予定。
まずは、rtavr_ior.v を rtavr_ior_ps.v にコピーして、上記のものだけにする作業をした。その際に デコードに対するオプションを、LARGE_MAPPER 相当のみに機能削減。
あと、INT0(+INT1)の入力信号の定義と、『アドレスに対応するレジスタが存在しない』という意味の 信号を定義。
この作業は、削除が主なので比較的楽なほう。 - rtavr_ior_ms.v
次は、rtavr_ior.v を rtavr_ior_ms.v にコピーして、CPU 固有のリソースを削る。残ったものは、PORTA/B/C , USART, SPI , TWI(未実装), CPI1 。
CPI1 は、外部との接続用で CPI0 と違う扱い。
こちらもアドレスデコードは、LARGE_MAPPER 相当のみに機能削減。... これは、やりすぎかも。デフォルトだったデコード方式は、サブモジュールで 1 つに一旦まとめるというもの。Dual Port 化したとき 2つのデコード方式で意味が変わる。
LARGE_MAPPER 方式は、READ に対しては、レジスタ1つ1つを独立にアクセスできる。コードの修正も楽な方。そのかわり、実際の配線量が多い。
WRITE に関しても似た様なもの.. のはず。ただ、すべての レジスタに対して 入力データのセレクタが付くことになる。
この配線量が非常に多そうな気がする。SUPPORT_LOCKING では、アクセスしようとするアドレスも仕様にいれているので、IOR 調停で プロセッサ固有のレジスタ以外は、同時には 更新できないという風にすると セレクタを 1 つにできて 配線量を減らせるはず。
これは間違いだった。LARGE_MAPPER であっても、WRITE の場合はサブモジュール単位で 1 データ。レジスタ単位で WRITE データを別にはできない。1 サブモジュールに対して同時に WRITE すると、2 つのレジスタに同じデータ(CPU0側)が書きこまれる。... どうも凝ったことしないでも良さそうだ。
GPIOR も事情は同じ。WRITE の場合 1 つのデータしか書けない。で、アドレスデコードを適当にすると、ひどいことになる。
サブモジュール方式だと、同一モジュールに対して 同時にはアクセスできない。アドレスが 1 つしかないから、どちらかのアクセスを選ばないといけない。同時にアクセスされた場合の動作は未定義。-- 実装上は CPU0 優先にする。
... こんな風になる。サブモジュール方式の方が良さそうなのだが、どうせだいぶ作り直さないといけないので、一旦削除で良いだろう。 - 割り込みのルーティング(ToDo)
装置で共有するデバイスの割り込みを どちらの CPU に回すかの 機構がいる。
両方ともに同じ信号を送り、ACK を 単に OR することで、どちらかが ACK を返せば割り込みがクリアされるようにした。基本どちらかだけが、割り込みを処理するようにしないといけない。両方のCPU で割り込みを許可すると、同時に両方で割り込みが入ったり、片側だけに割り込みが入ったりする。好ましくない挙動だが、とりあえずはこれでも良いだろう。
本来なら、割り込みを許可している CPU を 1 つ選ぶ 。両方許可していたら SLEEP している方を優先。
.. といった制御が良さそうだ。-- これはいずれ。 - IOR アクセス調停機構(Todo)
Dual Port 化の実装方法の違いで、調停のしかたも随分違うようだ。 - まとめ
こんな風に IOR Dual Port 化では、コードが大きく変わる。一旦 IOR の ソースを分岐させるが、最終的には、Dual Port 化 IOR を Single Port でも使うようにするつもり。
Version 0.9 として リリースする予定だったが、大更新中なので、なかなか難しい。コードが落ち着かないので スナップショットも作る気になれない。ソースコードの提示は形が定まった後にする。
SMP化の効果
- 規模
CPI 自体の規模は非常に小さい。CPI だけを使うと ほぼ 2 倍の規模になると見積もれる。
一方 SMP 化で共用するのは、ROM/RAM/装置IOR(PORT/SPI/USART)
ROM/RAM はブロックRAM で単に使用個数の問題。SMP だと いわゆる分割損の分節約できるかも知れない。ただ、すなおに Dual Port の機能を使っていたのが、(CPU から見て)Single Port 化 することになり規模的にはセレクタの分が少々増える。
IOR には、プロセッサ固有のものがある (SREG/SP/TMR0/CPI0)。これらは、単純に 2 倍になるから CPI だけの場合と比べて違いはない。
装置IOR(PORT/SPI/USART)だけは、1 つしか持たない。これらは SMP の方が有利そうに思えるが、調停機構や、それぞれの CPU からのアクセスができるようにする配線で相殺されそうなかんじ。
まぁ SMP だからといって有利にも不利にもならないように思える。 - 扱いやすさ
rtavr 同士で通信するなら、整合性がとれたプログラムが 2 つ必要だ。SMP なら 1 つなのだ。プログラミングには、少々コツがあるが、SMP の方が扱いやすい。
構成についても同じ。SMP でなければ、2 種類を構成し、プログラムも別々に定義しないといけない。ー 実をいうと 今の rtavr は、2 種類を Implement することを想定しておらず、それ自体を行うためにいろいろ面倒なことをしないといけない。 - 性能
そもそも 2 個付けるのは、性能のためである。で、SMP はどう有利なのだろう?
SMP では、CPI を相互に接続するから、不利な点はない。初期化以降 CPI だけを使って通信するなら、CPI だけのときと等価の動作も可能だ。
ある装置から収集したデータを加工して もうひとつの CPU に送り込み、そこでさらに加工して出力する場合を考えてみる。
こういう単純な通信しかしないのであれば、CPI を使うだけで事足りる。FIFO に送り込んだりするなら、ポインタの管理が不要な分高速にすらできる。
性能的に有利になるとすれば、バッファが 16段の FIFO で足りない場合か、マルチストリームが必要な場合。
... といっても 細工すれば、バッファは増やせる。64段程度ぐらいまでなら対応できそうだ。マルチストリームも CPI を 増設する方法がある。2 個 3 個ぐらいならなんとかなる。規模には限界があるが、そもそもプログラムの規模が増えると rtavr に入らない。
ランダムアクセスが必要な場合は、SMP が圧倒的に楽だ。CPI だとプロトコルを組んでいろいろ検討しないといけない。... といっても ランダムアクセスといえるだけのデータ量はそもそも扱えない。
やはり、SMP だからといって性能は特別有利とは思えない。-- SMPの特徴は柔軟性だと言えそうだ。プログラムを変えることでいろんなやり方ができる。
正直なところ、SMP化してどれだけの効果があるものなのかよく分からない。
『AVR互換コア(FIFOモジュール)』の記事で書いた CPI を使って rtavr 同士をつなげば十分のような気がする。
一方SMP化自体には意味がある。第一に rtavr を特徴づけることができる。SMP の 8bit 組み込み向け CPU なぞ世の中にほとんどないはずだ。作る立場として、意義を見いだせることは重要なのだ。
SMP としての特徴は盛り込むつもりだから、教育用としても意味があるはずだ、SMP 制御の基礎を簡単なコードで動かしてみることができる。
CPI だけを使った場合と比べて、どういう効果があるかちょっと考えてみる。
対応状況(その2)
// Target Device: xc3s200a-4ft256 , top-level rtavr
// 従来 ior 新 ior
// Number of Slice Flip Flops: 390 390
// Number of 4 input LUTs: 1176 1081
// Number of occupied Slices: 652 654
// Number used as a route-thru: 33 33
// Number used for Dual Port RAMs: 16 16
// Number used as Shift registers: - -
// Number of bonded IOBs: 12 12
// IOB Flip Flops 1 1
// Number of BUFGMUXs 3 3
// Number of RAMB16BWEs - -
// Number of DCMs - -
// Number of BSCANs - -
// Post-PAR Static Timing Report
// Maximum frequency(MHz): 43.894 44.715
とりあえずは、rtavr_ior.v のかわりに rtavr_ior_ms.v/rtavr_ior_ps.v を使って Implement できるようになった。50A 用の Config ではわずかに +2 スライスの増加で済んでいる。
// SHRINKED SMALL-SP SMALL-SMP
// Number of Slice Flip Flops: 390 421 732 (+ 342)
// Number of 4 input LUTs: 1081 1198 2268 (+1070)
// Number of occupied Slices: 654 727 1341 (+ 614)
// Number used as a route-thru: 33 33 61
// Number used for Dual Port RAMs: 16 16 32
// Number used as 16x1 RAM: - 8 8
// Number used as Shift registers: - - 16
// Number of bonded IOBs: 12 12 12
// IOB Flip Flops - - -
// Number of BUFGMUXs 1 1 1
// Number of RAMB16BWEs 3 3 3
// Number of DCMs - - -
// Number of BSCANs - - -
// Post-PAR Static Timing Report
// Maximum frequency(MHz): 44.715 45.319 46.134
//
// SHRINKED : soc/configs/50A/rtavr_defs.v
// SMALL-SP : smp/configs/small-sp/rtavr_defs.v
// SHRINKED + ROM_PSEUDO_DP + SUPPORT_LOCKING + IOR_HAVE_GPIOR
// + IOR_HAVE_RAMWIN + IOR_HAVE_RAMWIN_HA + NVM_RAM_PROTECT
// SMALL-SMP : smp/configs/small-sp/rtavr_defs.v
SMP が Implement できるようになった。SHRINKED は、上記 新IOR に ROM を共通化した rtavr_rom_p4.v を使ったもの。これに、SMP で使う機能を加えたのが SMALL-SP 。SMALL-SMP はそれを SMP 化したもので、top-level として rtavr_smp.v を追加(他のソースコードは共通)。CPU 間通信用の CPI0 も入っている。
現状、多数の Warning が出ている。直していくと多少規模が増えるかも。
これに、IOR の調停機構を入れて、ひとまず完成のつもり。
ところで、SMP のテストは、悩ましい。タイミングで動作が変わるからシミュレータとのトレース比較ができないのだ。ただ、動作が変わらないコードを書いて、それぞれの CPU の トレースを比較することはできそう。
愕然となった事実
いろいろ見直してたら、いろんな所で RESET == 0 のとき、リセット動作をしているのに気がついた。... ずっと RESET は正論理 -- 1 でリセットのつもりだったのだ。で、現状は、正論理と負論理が混在。
良く動いていたものだ。もともと正論理のつもりなので、それに合わせて修正。テストベンチも変更。
IOR の調停(の準備)
GPIOR は、SMP で CBI/SBI が自由にできるように導入したのだが、分散メモリだと 調停するのが面倒なことになる。結局、構成を自由に変えられることにして FF で作り直した。これで、アドレス単位で調停すれば良いことになる。
GPIOR0 - GPIOR2 の 3 つを構成すると 654 スライスが 681 スライスになった。SP(Single Processor)でこれだから、SMP だと もっと増えそうだ。
あと、アドレス単位での調停をしたいのは PORT と RAMWIN の RAMDR 。
PORT には、複数の装置が割り当てられているので、調停なしだと、ソフトで排他制御してアクセスすることになる。ビット出力するにもロックしてからとなると、非効率どころではない。
RAMDR は、排他制御をできるようにするためのカナメだから、調停は必須。
PORT も インターフェイスを変更して DDR と PORT を同時に書けるようにした。
(調停機能の定義)以上のレジスタは、アドレスが同じなら 同時にアクセスできないようにする。
RAMDR を除く CPU 固有のレジスタは、調停が必要ない。それ以外のレジスタも調停しないことにする。 ひとつのサブモジュールに対して同時に更新すると誤動作するので、ソフトでの排他制御が必要になる。サプモジュール毎にアクセスする CPU を決める使い方ならソフトでの排他制御は必要ない。
愕然となった事実(2)
RAMWIN は、0x1f の RAMDR に対して SBI/CBI することができる。要するに RAM に read-modfy-write の機能が必要だということだ。
実際にテストしてみて初めて気がついた。幸い ROM/RAM を 1 クロックの 前半 READ / 後半 WRITE と 2 回アクセスできる仕組みを以前使っていたので対応できた。ただ、IOR は非同期 READ を前提にしていてラッチしている。RAM の READ は間に合わないので、RAMWINからの READ は、ラッチを通さずセレクタで切り替えることにした。
だが、ちゃんと対応しても動かない。変だと思って調べたら 直前の SBIS/SBIC 命令でおかしなことをしていた。... 原因はデコードのバグ。
... RESET もそうだが、今に至ってこのレベルのバグがみつかるとは!
ちょっと不安になってきた。
rtavr-0.9.0
- rtavr-0.9.0.tar.gz
なんだかんだで、まだまだという気はするが、ついに 0.9.0 にした。
ファイルを整理して、中途半端ながら ドキュメントも付けた。
avr8l_trace の機能を追加。(ドキュメントはできていない)
GPIOR0/GPIOR1 を付け、 RAMWIN の仕様を現状に合わせた。
さらに、SBI/CBI 命令の仕様を変更。
-exbi を付けると、CBI/SBI で変更するビットの変更前の値を T レジスタに入れる。
... こうすると TEST & SET の機能になる。GPIOR0/GPIOR1 などを 1bit 単位で ロック変数にできるのだ。
もちろん rtavr での機能も実装。(SUPPORT_EXBI) 。tests/tbi_001 でコードを入れて確認済み。
000004c2 <t_test_and_set>:
261: 2788 eor r24, r24
262: 9a1f sbi 0x03, 7 ; 3
263: f980 bld r24, 0
264: fb81 bst r24, 1
265: 9508 ret
テストコードは、こんな風にした。sbi で GPIOR bit7 に 1 を入れる。old-value が T に入るから 戻り値の bit0 に移す。バグったときトレースの差分が沢山出ないように T を bst でクリア。
で、結果が 0 なら old-value が 0 だったということだからロック成功。1 なら失敗。
あと RAMWIN のテストコードも紹介しておくと..
000004ae <t_lock>:
257: e744 ldi r20, 0x74 ; 116
258: e050 ldi r21, 0x00 ; 0
259: bb5e out 0x1e, r21 ; 30
25a: bd40 out 0x20, r20 ; 32
25b: 9bff sbis 0x1f, 7 ; 31
25c: bb8f out 0x1f, r24 ; 31
25d: b38f in r24, 0x1f ; 31
25e: 9508 ret
まず、アドレスを 0x0074(char lock) に設定。lock の MSB が 0 なら 第一引数(val: 0x80 とか)をout 。
改めて in で READ して同じ値なら ロック成功。
CBI/SBI の仕様変更が嫌ならこのやりかたをする。
bst/bld なんて滅多に使わないからチェックは楽なんだが、互換性がないと困る場合もあるかも知れない。命令を新設するのはやりたくないので、互換性を維持してロックを作れる この方法もサポートする。
これらは、0.9.0 の tbi_001 に入れてある。
SMP を『Icarus Verilog』で 確認
- rtavr-0.9.1.tar.gz
... ここまで書いてコードも提示しないのは、どうかとも思うので 0.9.1 にした。.. これから 0.9.x の x をどんどん上げていく。
0.9.1 では、rtavr_rom_4p.v は、セレクタ方式だが、次から レベルセンシティブ・ラッチに変更する。(セレクタ方式もとりあえずは残す) 。ちなみに 両方とも ROM_PSEUDO_DPにしたときの規模の増加は同じで、+ 26 スライス。
0.9.1 での重要な修正はまだある。CBI/SBI の仕様を変更したものを default にしたこと。これで、互換コアと言いづらくなった。(互換モードは残しておくので、互換だと言い張るつもりだが)

ちょっと修正が必要だったのだが、『Icarus Verilog』で 確認できるようになった。
基本的に rtavr と rtavr_smp の インターフェイスは同じにしている。わずかな修正で 従来のテストベンチを使えるようにできた。
動かしてみると ... 当然と言えば当然なのだが、IORの調停が必要にならない限り CPU0 と CPU1 は 全く同じタイミングで動作する。そして、CPU_ID が入っている NVMCSR 以外のデータは全く同一で、同一データに対する結果も同じ。
tb_shuffle や tb_queen は、コアテスト用なので IOR にアクセスしていない。当然ながら CPU0 と CPU1 は、最後まで同じ動作をする。
ちなみに、トレースを取るのは、CPU0 だけだが、define PRC_WATCH_P1 とすると CPU1 だけを取るように変わる(未確認)。avr8l_trace も対応していて、-1 オプションを付けると、CPU_ID の値を 1 にする。(ただし、CPU0 と CPU1 で協調動作した場合のトレースは無理。)
tbi_001 はロック命令のテストコードがあるので、CPU0 と CPU1 とで違う動作になった。
BB5E は、0x1e (RAMARH) に対する OUT 命令で、BD40 は、0x20(RAMAR) に対する OUT 命令。0x1e に対しては調停の要求が出て 0x20 に対しては 要求が出ない。-- これは S0 のPRE命令デコードで決めている。
この競合で勝つのは、lk_last でない方。-- lk_last の初期値を 1 にして CPU0 を勝たせている 。
負けた CPU1 は 1 CLK ずれる。
9BFF は、SBIS 命令で、次が同じアドレスに対する OUT 命令なので、2 クロック分を要求する。1 CLK 遅れた CPU1 はまた負けるのだ。
結構良いところまで行っているのだが、バグっている。-- 負けた側が 2 回目の REQ を出していない。
よくよく見れば、負けた側が 次の命令にいってしまっている。S1 での判定にしたが、引き伸ばせるのは、S0 だけで S1 に入ってしまったら、キャンセルすることしかできない。S0 で調停して負けたら そこで引き延ばさないとだめだ。-- 全然ダメのように見えるかも知れないが、コード量は少ないし、確認できるわけだから実は大した事はない。

直してみた。それ自体は簡単だったのだが、ROM_PSEUDO_DP のコードを入れたときに ROM データを CLK_180 でラッチしていたのだった。そのため、判定で使える期間が 1/4 CLK になってしまっていた。これでは ボトルネックになりそうなので、ROM のタイミングを変更。
ROM は CLK_0 で アドレスを出しておいて CLk_90 で 読み込み。データが安定するのはそれ以降だから CLK_180 でラッチしていた。なぜそれが必要だったかというと、ROM を書き換える場合 CLK_180 で 再度アクセスして、そのときに 出力データが変わってしまうため。命令データは、CLK_0 まで変化してはいけないのだ。
で、正しい解決方法は良くわからない。とりあえず、CLk_180 で読み出しデータと ラッチデータをセレクタで 切り替えるようにしてみたが、正しい解決方法のような気がしない。まだレベルセンシティブ・ラッチの方がましかも知れない。
Ver. 0.9.2
- rtavr-0.9.2.tar.gz
0.9.2 を置いた。
変更点は、rtavr_rom_4p.v を レベルセンシティブ・ラッチに変更したぐらい。
あとは、rtavr_defs.v の見直し。RTAVR_SMP を導入し、SMP の設定を楽にするのが目的。この変更ではじめて `undef を使った。
ついでに、RTAVR_TARGET_MachXO2 を導入。いくつかの項目をチェックして自動で define する。Lattice MachXO2 に対応できているわけではなく、本体でも参照しない rtavr_defs.v の中だけの定義。ーまだまだ準備といったところ。
それより、scripts/unifdef.awk の追加と doc/TOOLS-j.txt の追加がメイン。 unifdef.awk は、ifdef を展開して、ひとつの Verilog ファイルを作るのが目的。 どのコードが実際有効なのか、よくわからなくなった時に使うのが目的だが、一応ちゃんと Implement できるから、これを使って Implement しても OK 。複数のファイルを remove → add するより便利かも。
あとソースの規模の見積もり。全体で 8118 行もあるが、50A 用の config なら、実際に使われるのは、3902 行というのが分かる。ほとんどの機能を ON したはずの large でも、4606 行。large-smp でようやく 5497 行。
追記) scripts/unifdef.awk はバグっていたので修正: あと、debug 用出力も可能にしておいた。次で修正版にする。
そろそろ
実際に動かすことをメインにしていこうと思う。
ツールつくるの面倒なので、SMP とか 論理設計に走ってしまったが、もう十分堪能した。
発注しなおした基板も到着したし、いいかげん作りだそう。
Ver. 0.9.3
- rtavr-0.9.3.tar.gz
0.9.3 を置いた。これで当面凍結。
変更点は、rtavr_defs.v から rtavr_common.v を分離して、config/ に置いたことと、トップディレクトリに Makefile を置いて unifdef.awk でひとつにまとめた ソースを生成できるようにしたこと。
バグ修正は、unifdef.awk と ROM_PSEUDO_DP のラッチ版。一応 tests での make で結果が一致する状態になっている。
まだあった。MachXO2 版の準備。xo2_sample で JTAG と接続するようにしておいた。
(次はツール編)