公開日:2021/03/27
title.png

15th Season


#15-7 I/Oユニットを揃える

 今回一気に、よく使いそうなI/Oユニットを揃えてしまって、「最小限のマイコン」として動作可能な形を整えたいと思います。

システムタイマユニットSYSTIM

 Arduinoのプログラミングで言う所のmicros()関数やmillis()関数は、実時間や経過時間などでタイミングを取って制御する時に必須の機能だと認識しています。
 よって、これらの機能をハードウェアでカウントするタイマを作っておきます。名付けてシステムタイマ(SYSTIM)です(以下、ブロック図)。

a157_sytm.png

※黄色に着色した箱は、CPUからアクセス可能なI/Oレジスタです。


 以下表に、I/Oレジスタマッピングを示します。

Addr Name R/W Bit:15〜0 Memo
0xf010 sytmctl RW SI
MU
TC
OE
MC
OE
ML
OE
RS
T
system timer control
0xf012 reserved
0xf014 sytmtckh R TICK[31:16] tick counter high word
0xf016 sytmtckl R TICK[15:0] tick counter low word
0xf018 sytmmcrh R MICRO[31:16] micro second counter high word
0xf01a sytmmcrl R MICRO[15:0] micro second counter low word
0xf01c sytmmilh R MILLI[31:16] milli second counter high word
0xf01e sytmmill R MILLI[15:0] milli second counter low word

 BUSC出力のセレクト信号はbcs_sytm_nです。上表の範囲でアサートされるので、SYSTIMユニット内では、アドレスbadr[3:0]を復号してレジスタを特定しています。このI/Oユニットはバイトアクセス禁止です。必ずワードアクセスしてください。

 タイマカウンタは3つで、全て32ビット幅です。

 16ビットCPUのMoscoviumでは、32ビットレジスタを一度に読み出すことが出来ません。ldw命令を使って、16ビットレジスタ×2回に分けて読み出すことになります。
 しかし、カウンタは止まることなく動き続けているため、最初に読み出した16ビットと、後から読み出した16ビットは異なる時間のカウンタ値となり、32ビットカウント値として使用することが出来ません。
 そこで、ATmega328などでも実装されている、一時レジスタによるアクセスを採用しています(↓)。

a157_reg32.png

 タイマ割り込みを使い、ソフトウェアでカウントしているmillis()関数やmicros()関数と異なり、カッチリとハードウェアでカウントされているタイマなので、精度良い時間制御が可能となります。

汎用I/OポートユニットPORT8I8O

 スイッチの状態をセンスしたり、LEDを光らせたり💡、色々と使い道のある汎用I/Oポート(GPIOポート)を作っておこうと思います。
 入力信号8本、出力信号8本とします。ブロック図は以下となります。

a157_port.png

 基本的に、外部入出力ポートをI/Oレジスタの各ビットに割り付けておいて、CPUからのI/Oレジスタアクセスをこなせば良いだけなので、とてもシンプルです。

 以下表にI/Oレジスタ一覧を示します。

Addr Name RW Bit:15〜0 Memo
0xf020 porin R PINP[7:0] input data
0xf022 porind R PIND[7:0] direct input data
0xf024 porsel RW PSEL[7:0] input selection
0xf026 pordir RW PDIR[7:0] i/o direction
0xf028 porout RW POUT[7:0] output data
0xf02a porset W PSET[7:0] set porout bits
0xf02c porclr W PCLR[7:0] clear porout bits
0xf02e porode RW PODE[7:0] open drain enable

 BUSC出力のセレクト信号はbcs_port_nです。上表の範囲でアサートされるので、PORT8I8Oユニット内では、アドレスbadr[3:0]を復号してレジスタを特定しています。8入力8出力なので、レジスタの有効ビットは下位8ビットです。
 ただし、このI/Oユニットはバイトアクセス禁止です。必ずワードアクセスしてください。

 I/Oポートの入出力バッファ回路を以下図に示します。

a179_inout.png

 出力ポートを入出力ポートとして使用できるように拡張(「#17-9 双方向I/Oポート」)しました。詳しくは、上のI/Oレジスタporsel,pordir,porodeの説明をご覧ください。


 入力ポートについては次節で説明します。

 出力ポートについては、poroutレジスタに書き込んだ値が、FPGA端子に出力されます。
 Moscoviumは、AVRの様なビット操作命令を持っていません。その事情はARMなどと同じなのですが、この場合、特定のビットだけを1にセットしたい場合に、以下の様な手順が必要となって、面倒です。

  ldwi r7,porout
  ldw r0,[r7]
  ori r0,0x08  // PORT[3]を1にセットしたい場合
  stw [r7],r0


 そこで、ARM等のマイコンと同様、ビットセット用レジスタ(porset)とビットクリア用レジスタ(porclr)も備えています。上の処理をporsetレジスタを使うと、以下の様になります。

  ldwi r7,porset
  ldbiu r0,0x08  // PORT[3]を1にセットしたい場合
  stw [r7],r0


 元々(ARM等)は、リード(ldw)→モディファイ(ori)→ライト(stw)の間で割り込みがかかって、割り込みルーチン内でporoutレジスタが変更されてしまった場合、stw命令で古い値を書き戻してしまうのを防ぐための機構です。
 現在のMoscoviumでは「割り込み」に非対応なので必須の機構ではありませんが、将来の割り込み拡張に備えておくことは良きことでしょう。

※実際、「#15-9 割り込み機能を追加する」にて、Moscoviumに割り込み機能を追加しました。

I/Oポートからの入力信号

 一般に、クロック立ち上がりエッジclk↑でデータを取り込むFFを使った同期回路に於いて、clkと非同期に変化するデータをFFに取り込む場合には、注意が必要です。
 丁度、clk↑の瞬間に入力データが変化してしまうと、FFの出力が0にも1にも定まらない不確定な状態が数〜数10[ns]程度続いてしまう場合があります。この状態をメタステーブル状態と呼びます。

 信号が0または1に確定していることを期待して、さらに、FFの出力がclk↑のタイミングでしか変化しないことが前提となっているデジタル回路の中でメタステーブル状態が発生した場合、高確率で誤動作を引き起こします。
 実際、メタステーブル状態が原因で誤動作した例が多数あって、昔から呪文のように唱えられてきた対策が有ります。

  「非同期信号はFF二段で受けてから使え

a157_synch.png

 メタステーブル状態の持続時間は数〜数10[ns]程度らしいので、clk↑で2回たたけば、1個目のFFがメタステーブル状態となったとしても、2個目のFFがデータをラッチするタイミングまでには収まっているだろう、と言うのが対策の根拠です。
 ATmega328とかSAM3X8E等のデータシートで、I/Oポート章の回路図をご確認いただくと、シンクロナイザ(FF×2個)を通った後で、ポートレジスタとして読み出しされていることが解るはずです。

 通常は、入力ポートの状態をFF二段で受けた後、レジスタから読み出します。これがporinレジスタの動作となります。

 一方、何らかのストローブ信号を使って、外部回路とハンドシェイクしてデータを受け取る場合、ハンドシェイクによって、レジスタリードするタイミングでは信号が変化しないことが保証されているのであれば、シンクロナイザを通す必要はありません。
 このケースでは逆に、シンクロナイザ(FF二段、同期化回路)は邪魔なだけです。だって、2クロックも昔の、過去の値が読めるなんて……。
 そこで、シンクロナイザを通らない読み出しレジスタとして、porindレジスタも備えることにしました。

※特に、porindレジスタのに関しては、本節の議論を十分理解した上で使用してください。本節の議論が理解できない場合は、porindレジスタは使用せず、porinレジスタを使っておくのが安全です。

非同期シリアルユニットUART8N1

 今後、ソフトウェアのデバッグを始めると、何かしら実行状態を外部から確認できるようにしておいた方がデバッグがし易いです。一番お手軽で、一番使い勝手が良いのはシリアルモニタですよね?
 シリアルモニタは、PCとマイコンが非同期シリアル通信(UART)で通信して、データをやり取りしています。即ち、ハードウェアでUARTユニットを作っておくと、PCとのシリアルモニタ通信が可能となります。

 ひとまず、シリアルモニタにつながることが目標なので、フォーマットは8N1(8ビットデータ、LSBファースト、パリティなし、1ストップビット)に固定してしまいます。
 その他のフォーマットも、ちょっとした追加論理で対応することは可能ですが、その為にフォーマットを指定する制御レジスタのビットを設けたり、ステートマシンに状態遷移を追加したりする必要が生じます。追加するだけならまだしも、追加した以上、動作検証する必要があります。
 シリアルモニタを前提とする限り、使う予定の無いフォーマットの為にわざわざそんな手間をかけるのはバカバカしいので、最小限目的を果たせる回路とします。

 以下、ブロック図となります。

a157_uart8n1.png

※緑色の箱は、TD IDE(Anlogic EG4S20BG256チップ)のマクロを使うブロックです。

I/Oレジスタ

 I/Oレジスタ表を以下に示します。

Addr Name R/W Bit:15〜0 Memo
0xf030 uartctl RW RA
VL
RF
UL
BR
DF
TF
UL
FR
ST
RX
E
TX
E
uart control
0xf032 uartbaud RW BAUD[15:0] baud rate control
0xf034 uarttdat W TDAT[7:0] transfer data
0xf036 uartrdat R RDAT[7:0] receive data
0xf038 reserved
0xf03a reserved
0xf03c reserved
0xf03e uartbres R BRES[15:0] bit rate estimator

 BUSC出力のセレクト信号はbcs_uart_nです。上表の範囲でアサートされるので、UART8N1ユニット内では、アドレスbadr[3:0]を復号してレジスタを特定しています。このI/Oユニットはバイトアクセス禁止です。必ずワードアクセスしてください。

通信速度

 通信速度は自由に設定可能とします。9,600bausとか、115,200baudとか言った一般的に使われる通信速度は、確実に抑えておく必要があります。
 例えば、通信速度9,600baudの場合、1ビットの送受信時間Tbit=1/9,600baud≒104.167μsです。この時間を24MHzクロックで動作するタイマでカウントすると、Tbit×24MHz=2,500サイクルです。
 24MHzでカウントアップするタイマを基準に、送信する場合は2,500サイクル毎に送信データのビットに応じてTXD=0/1出力を切り替えれば良いし、受信する場合は2,500サイクルのちょうど中央で受信データRXDを取り込んで行けば良いわけです。
 この1ビット当たりの通信時間をuartbaudレジスタに指定することで、自由に通信速度を調整できるようにしておきます。uartbaudレジスタに設定すべき値は、必要なサイクル数−1です。目標通信速度BAUDからuartbaudレジスタ値を計算する方法は以下の通りです。

  uartbaudレジスタ設定値=24MHz/BAUD−1

 通信速度115,200baudの場合は、uartbaudレジスタ=24MHz/115,200baud−1≒207.3に設定します。uartbaudレジスタは整数値となりますので、四捨五入して207を設定した場合、実際の通信速度は115,384baudとなり、誤差およそ0.16%です。
 1フレームはスタート1ビット+データ8ビット+ストップ1ビットの計10ビットなので、

  115,200baudで10ビット送受信する時間=10/115,200baud≒86.806μs
  115,384baudで10ビット送受信する時間=10/115,384baud≒86.667μs

 1フレーム送受信すると、次の送受信はスタートビットの立ち下がりエッジでリタイミングされるので、上の計算値86.806−86.667≒0.139μsが最大誤差です。115,200baudの1ビット送受信時間Tbit≒8.681μsに対して十分小さい誤差(約1.6%)なので、問題なく通信可能でしょう。

 uartbaudレジスタは16ビット幅ですので、設定可能な最大値uartbaud=65,535では、通信速度366baudが限界(下限)です。Arduino IDEのシリアルモニタでは、300baudの通信速度を設定できますが、この通信速度では通信できません(仕様制限)。
 速い方の通信速度(上限)は、恐らく非同期シリアル通信の限界を軽く突破できるので、気にしなくて良いと思います。

受信データバッファ(FIFO)

 ATmega328等では受信データバッファは2段しか無くて、受信したデータを読み出す前に次のデータを受信してしまうと、受信データを消失してしまいます。そうならない様にATmega328の場合には、データ受信フラグで割り込みをかけ、割り込みサービスルーチン内で受信データを読み出すように制御しています。

 現時点でMoscoviumコアは割り込みを実装していないため、ATmega328の様な回避手段が取れません。

 そこで、受信データを記憶するための十分大きなバッファ(FIFO)を接続して使用することを前提としています。
 FIFOバッファは、TD IDEのFIFOマクロを使いますので、「Toolsメニュー ⇒ IP Generator」で、以下の様にして生成してください。

a157_fifoip.png

 FIFOの段数は上図の通り、512段(Write Depth)になっています。さすがに、UARTの受信バッファに512段は「多過ぎ」の感があり、32〜128段くらいで十分だと思ったのですが、TD IDEのFIFOマクロでは512段が最小段数でした。なので仕方なく……。

 製品として量産する場合は512段のFIFOに対して選別テストを実施しなければならないし、シミュレーションなどでバッファフルの動作を検証する場合も、データを512バイトも受信させないと検証できません。必要以上に大きなバッファは面倒なだけなのですが……。
 FFを使ってRTLでFIFOを作ってしまうと、今度はFFのお化けになってしまうので、FIFOマクロを使っておくのが吉でしょう。もろもろ妥協の結果、512段のFIFOマクロとなりました。

※FIFOマクロは9KビットRAMマクロを使って実装されているので、8ビット幅のFIFOであれば、1個の9KビットRAMマクロで1,024段のFIFOを構成することが出来ます。1,024段なんて更に必要ないので、512段で妥協です。

 UART8N1モジュールと、上で作成したFIFOモジュールを以下の様にインスタンスして使用します。

  wire [7:0] urxf_dati;
  wire [7:0] urxf_dato;
  uart8n1 uart (
    .clk(clk),
    .rst_n(rst_n),
    .uart_rxd(uart_rxd),
    .bcs_uart_n(bcs_uart_n),
    .brdy(brdy),
    .bcmdr(bcmd[0]),
    .bcmdw(bcmd[1]),
    .badr(badr[3:0]),
    .bdatw(bdatw[15:0]),
    .uart_txd(uart_txd),
    .bdatr(bdatr_uart[15:0]),
    // fifo i/f
    .urxf_empty(urxf_empty),
    .urxf_full(urxf_full),
    .urxf_dato(urxf_dato[7:0]),
    .urxf_read(urxf_read),
    .urxf_frst(urxf_frst),
    .urxf_write(urxf_write),
    .urxf_dati(urxf_dati[7:0])
  );

  uart_rx_fifo fifo (
    .clk(clk),
    .di(urxf_dati[7:0]),
    .re(urxf_read),
    .rst(urxf_frst),
    .we(urxf_write),
    .do(urxf_dato[7:0]),
    .empty_flag(urxf_empty),
    .full_flag(urxf_full)
  );


 以下のデザインデータダウンロードページに置いてあるのは、UART8N1モジュールだけなので、使う方はFIFOマクロをご自身で生成する必要があります。

  btn_moscovium.png

状態遷移図

 最後に、受信側の制御を司るステートマシンの状態遷移図を以下に示して説明します。

a157_uart_fsm.png

 筆者がステートマシンを設計する時、普段は上のような状態遷移図は描きません(1^×^;)。設計仕様書とか、他人に説明する時に作るくらいです。確かに、状態遷移図を描いて頭を整理してからステートマシンをコーディングした方がスムースかも知れないのですが、無ければ無いで特に不便を感じたことは無い感じです。

※そもそも、ステート数が増えて、遷移条件が複雑になって来ると、上図の様な状態遷移図を描くのは困難な作業となります。検証とか、他の工数に充てた方がずっと効率的です。

 と言う事で、上図の様に受信側は作成してみましたが、送信側の状態遷移図は存在していません、あしからず。まあ、送信側も基本的に受信側と大きくは変わらないので、想像してください。

 以下、各ステートでの動作を説明しておきます。

 urx_ovfl,urx_halfは、通信速度を計測するタイマ(ビットカウンタ)の、オーバフロウ(uartbaudレジスタ値に到達した時)と、その1/2周期(uartbaudレジスタ値/2に到達した時)にアサートされる信号です。

動作クロック

 通常、I/OユニットはCPUと同じ高速クロックで動作する必要は無い為、CPUクロックから分周した低速クロックで動作させることが多いのですが、Moscovium用のI/Oユニットは、CPUと同じクロックで動作します。24MHzです。
 定格20MHz動作のATmega328も、同様にCPUと同じクロックでI/Oユニットを駆動しているので、このクラス(24MHz程度)では、それほど変な仕様では無いと思われます。

 お陰で、タイミング制約やクロックツリー等はとてもシンプルだし、信号の受け渡しタイミングなどもほとんど気にする必要がありませんでした。こういうの好き。


 以下ページでデザインデータをダウンロードすることが出来ます。

  btn_moscovium.png


 漸く、マイコンとして最小限必要な回路が揃いました(CPU、周辺回路(ROM,RAM,BUSC)、I/Oユニット(SYSTIM,PORT,UART))。いよいよ、次回、全体を組み上げてFPGAボードに書き込んでみたいと思います。おっと、実は筆者自身、ここまでずっとシミュレーションだけで動作確認していて、一度もFPGAボード(Sipeed Lichee Tang Primer)に書き込んでおりませぬ……(1^ー^;)。まあ、論理回路(LSI)の設計なんて、そんなものですわね。


btn_arduino.png btn_moscovium.png

btn_prev.png btn_next.png

home