Top page
        CPUの働き


AVRやPICを始め、x86プロセッサなどの中心部はCPU (Central Processing Unit)と呼ばれ、コンピュータの中でも最も重要な働きをしています。CPUは、プログラム・メモリーに書かれた命令を順次読み込んで、これを実行することで、データを外部から取り込んだり、計算を実行し、結果を判断し、これを表示したりできるのです。
ここでは、CPUの構造や、その働きを見てみることにします。


CPUのアーキテクチャー
CPUを大きく分けると2タイプあって、一つはハーバード・アーキテクチャー、もう一つはノイマン型アーキテクチャーと呼ばれるタイプです。両者は、メモリのアドレス配置が異なっていて、

 ノイマン型アーキテクチャーのコンピュータのメモリ (
   ROM:    0000〜7FFF番地  プログラム・メモリ領域
   RAM:    F780〜FF7F     書換え可メモリ領域
   REG:    FF80〜FFFF     内部レジスタ

 ハーバード・アーキテクチャーのコンピュータのメモリ (
   REG:    00〜1F        汎用レジスタ
   I/O:     20〜5F         I/Oレジスタ
   SRAM    60〜DF        書換え可メモリ領域
   ---------------------------------
   FROM:    000〜3FF番地   プログラム・メモリ領域

のようなメモリ領域配置が取られます。

ノイマン型のメモリ領域は連続しているため、シンプルで分かり易く、ソフトとの相性もいいのですが、どのアドレス領域をアクセスする場合もフル・アドレス桁を使わなければならないため、CPU内部の回路が複雑になり、アドレスを含む命令長も長くなるため、実行速度を上げることが困難となります。

この点で、ハーバード型は、書換えが必要なメモリ領域と、書換えが不要なプログラム・データ領域とを分けてしまっているため、少ないアドレス桁でメモリをアクセスでき、CPUの内部構造がシンプルで高速動作が可能になります。小規模なCPUに適しています。AVRは、このハーバード・アーキテクチャーのCPUです。

なお、PCは起動用の小さなROMを除いて、総てのプログラムやデータはハードディスクからRAMに読み込まれて動くノイマン型です。

注)上に説明したCPUの分類方法は、CPUの内部の仕組み(アーキテクチャ)の分類方法です。
一方、コンピューター自体(全体)の分類方法に「ノイマン型」と「非ノイマン型」という分類のしかたがあるため、混乱を招く場合があります。この場合の「ノイマン型」は「プログラムにより動作する装置」の意味で、「非ノイマン型」は「プログラムによらない動作をする装置」ということになります。現在のコンピューターはハードディスクを含む全メモリをクリヤした状態では何も動作できませんが、自らの判断結果が引き起こす外部の刺激に応じて次の自分の判断結果を変更してゆくようなコンピューターがあるとしたら、それは非ノイマン型コンピューターとい呼ばれるはずです。
一切、プログラムメモリを持たず、ステップ動作に拠らないコンピューターの研究もされているようですが、ハードウェア自体は現在のままのノイマン型を使って、ソフトウェア上で非ノイマン型空間を構築する方法もあるかもしれません。現在進められている各種AIもハードウェア―は普通のノイマン型コンピューターを使っていますが、解を探す手法としては予め定められた方法に拠らない、いわば非ノイマン的な手法使う場合もあるようです。


◆CPUのブロック図
下の図1は、アトメル社のAVR CPU (同社ではマイクロ・コントローラーと呼んでいます)のブロック図です。

図1 AVR のCPU部



CPU各部の働き
プログラム・カウンタ(PC)は実行するプログラムのアドレスを管理するカウンタです。プログラム・メモリにアドレスを出力して、そこに書かれているデータをインストラクション・レジスタに読み出す働きをします。
命令が実行されるたびに、プログラム・カウンタ値は、取り出した命令の長さ分だけ増加して、次の命令のアドレスを指示するのですが、分岐命令など、連続しないアドレスの命令が実行する時は、PC値は不連続に変化します。
条件分岐命令だった場合は、その時のステータス・レジスタ(SR)の内容によって次のカウンタ値に行くか、全く異なるカウンタ値へジャンプするか、分かれることになります。

プログラム・メモリ(PM)は実行するプログラムを書き込んでおくメモリです。メモリは、アドレス線に番地のデータを入力すると、内部に書き込まれたデータがデータ線に出力される素子です。
AVRでは書換え可能なフラッシュ・メモリが使われています。 書換えができないタイプのROMや、紫外線消去により再書き込みができるEPROMが使われる場合もあります。パーソナルコンピュータ(PC)ではプログラムがハードディスクからDRAMに読み込まれます。DRAMは電源が入っている間、メモリのデータが消えないよう頻繁にリフレッシュすることで内容保持できる大容量メモリです。どんなタイプのメモリが使われていても、基本的な仕組みは同じで、プログラム・メモリにプログラムがロードされた状態でプログラムが実行されます。

インストラクション・レジスタ(IR) はプログラム・メモリから読み出された命令を解析するためにレジスタです。ここには命令を解析して実行するための複雑な論理回路が接続されています。

スタック・ポインタ(SP)は、スタックと呼ばれる機能を使うためのポインタ・アドレスが入れられるレジスタです。スタックは「積み重ねる」を意味する言葉ですが、メモリ(SRAM)の上位アドレスを積み重ね用の記憶領域として使い、スタックされたもっとも上のアドレス(少ない番地)を示す値がスタック・ポインタで管理されます。

メモリ(SRAM)は、正式にはスタティック・ランダム・アクセス・メモリ(順不同に読み出せる静的記憶素子)ですが、一般的には単にメモリと呼ばれています。電源が供給されている間、記憶が保持でき、高速でデータを読み書きできるメモリです。
CPUのSRAMの下位アドレスのメモリは、各種変数やフラグ類などを、索引(インデックス)を付けて使う整理されたファイルのように扱います。
また、上位アドレスのメモリは、スタック・メモリとして使われます。スタックメモリは机の上に積み重ねた書類の一時置き場に似て、レジスタに入りきれない一時データを積み上げ(PUSH)たり、サブルーチンを呼ぶ際の戻り番地をPUSHしたりします。スタックされたデータは、上から順に取り出していきます(POP)。この順番をカウントするのがスタック・ポインタです。

汎用レジスタは、CPUが最も頻繁に使うレジスタ群です。AVRでは、8ビットのレジスタが R0〜R31 まであります。データの転送や演算は汎用レジスタを介して行われます。また、特定のペアの汎用レジスタは16ビットレジスタとしてX,Y,Zなどのレジスタとして使うことができます。
アセンブリ言語では、プログラマーが汎用レジスタを直接操作できますが、C言語や高級言語ではコンパイラが使うレジスタを自動的に割り当てますので、操作することはできません。

演算ユニット(ALU)は 汎用レジスタのデータを使って演算をします。演算は、AND、OR、NOT、EXOR、加算、減算、インクリメント、デクリメント、ビット演算などです。高級なCPUは乗除算をする機能などを持っています。

ステータス・レジスタ(SR) 演算ユニットが実行した演算の結果、桁上げなどが発生すると、その結果はステータス・レジスタの各ビットに反映されます。代表的なステータス・ビットは
 Z:ゼロ・フラグ(演算結果がゼロになった)
 C:キャリー・フラグ(桁上げ、桁下げが発生した)
 H:ハーフキャリー・フラグ(下位4ビットからの桁上げ、桁下げフラグ)

ポート・レジスタ(PR)  CPUの入出力は、ポート・レジスタを介して行われます。
例えば
 OUT PORTB,R0
という命令が実行されると、汎用レジスタ R0 のデータがポートBに出力されます。

AVRでは、入出力の方向を決めるレジスタは DDR と呼ばれ、対応ビットに1を書くと出力ポートになり、0を書くと入力ポートになります。例えば Aポートの上4ビットを入力ポートにして、下4ビットを出力ポートに設定する場合は、DDRA レジスタに 0x0f を書き込みます。 出力ポートAは、PORTA で、ここから 0を出力する時は、PORTA に 0 を出力します。なお、AVRの入力レジスタは出力レジスタと違うアドレスが当てられており、例えば、 D ポートの入力は PIND というポートから入力します。



◆サブルーチンの呼び出し
プログラムを作る場合、繰り返して何度も使われるような手続きは、サブルーチンと呼ばれる、まとまったプログラムにしておくと便利です。例えば、

MAIN:            ; @ プログラムの開始
 CALL ON_LED     ; ON_LEDを呼び出し
 CALL ON_LED     ; ON_LEDを呼び出し
 (プログラム停止)

;***** LEDの点滅サブルーチン******
ON_LED:          ; A
    OUT  PB,1    ; ポートBに1を出力してLEDを点灯
    (1秒タイマー)
    OUT  PB,0   , ポートBのLEDを消灯
    (0.5秒タイマー)
    RET        ; サブルーチン復帰

というプログラムの MAIN @ を実行すると、LEDを2回点滅させることができるのです。 ON_LED A 以降は、サブルーチンと呼ばれ、MAINの中の CALL ON_LED という命令によって、2回呼び出されています。
なお、サブルーチンの最後の RET は呼んだところへ復帰せよ、という命令です。


以下は、CPUの中でサブルーチンが呼び出される様子の説明です。(ただし、アドレスの値などは簡略化しています)
 PCはプログラムカウンタ、SPはスタックポインタ、SRAMはメモリ、ROMはプログラムメモリです。



図2はROMの200番地からスタートしたプログラムが202番地の命令を実行している時点の様子です。PCは202を示しています。
なお、この時点では、プログラムの1行目と2行目が既に実行されています。
1行目(200番地)は、レジスタR16に値100をセットする命令、2行目(201番地)は、スタックポインタにR16の値を出力する命令です。この結果、SPには値100がセットされた状態になっています。このことは、SRAMの100番地にSPの矢印が表示されていることで表現してあります。

さて、これ以後プログラムが進んで、500番地が実行されると、
 CALL 1000
という命令が実行されます(図3)。



すると、PCの値は1000に変化し。プログラムはサブルーチンの1000番地に飛ぶことになります。
同時に、次に実行すべきだったアドレス 501 が、SPが示していたSRAMの100番地に書き込まれます。
そして、SPの値は -1 され、99を指示すようになります。



図4はサブルーチンが1003番地に書かれたRET命令を実行した直後の状態です。
サブルーチンのリターン命令 RET が実行されると、SPの値が + されて戻り、その値が示すSRAMの番地(ここでは501番地)へプログラムが復帰します。

つまり、サブルーチンが呼ばれる際は、スタックに戻り番地が積まれ、サブルーチンが終わると、積まれていた戻り番地に戻るという訳です。

以上の説明は、SUB1というサブルーチンが1回呼び出される例でしたが、もし、SUB1の中で更に別のサブルーチンが呼ばれたらどうなるでしょうか? この場合は、99番地に戻り番地が積まれ、SPは98へと変わり、別なルーチンに実行が移されることになります。これを2重のネスティングといいます。無論そのサブルーチンが終わると元のSUB1にRETし、更に1003で元のメインルーチンの501へと戻るのです。

ネスティングが深くなる度にSPは上昇しますが、変数やフラグに使っている領域を侵食しない限り大丈夫です。ただ、C言語などでは、関数の中で宣言した一時的な変数は、全てスタック領域を使いますので、大きな2次元変数などを使うと、小さなCPUではスタックが溢れてしまう危険性もありますので注意が必要です。余談ですが、時折パソコンが固まってしまうのは、スタック領域が溢るなどで、スタックポインタが戻れなくなることにも一因があるようです。

ところで、サブルーチンの呼び出し方は、割込み(インターラプト)動作の際も同じことが行われます。ただ、サブルーチンはCALL 命令のような呼び出し命令が実行されることで発生し、RET 命令で元のプログラムに復帰するのですが、割込みの場合は、割込み信号線の状態が変化することで、あるいは、クロック割込みタイマーが働くことで発生し、復帰は割込み復帰命令 (RETI)によって、呼ばれる前のプログラムに戻る という点が異なっています。スタックの使われ方は同じです。
詳しくは→割込みの項を参照してください。



◆アセンブラ・インストラクション(命令)
CPUは、プログラムの命令によって動作するのですが、その命令は

 11000100
 01001111
 00100011
 ,,,,

のような データが、プログラム・メモリに書かれていて、それをCPUが順次取り込んで、命令に従って動くのですが、プログラムを作るプログラマーは、直接こういったマシン語でプログラムを作る訳ではありません(最初の頃のプログラマーはそうしていたのですが!)

マシン語より、少し人間の言語に近いコンピュータ言語はアセンブリ言語と呼ばれる言語です。そして、アセンブリ言語で書かれた命令をマシン語に変換(翻訳)するソフトがアセンブラーです。
アセンブリ言語で書かれた命令は、CPUの中のあらゆるレジスターなどを、自在にマニュアル操作することができます。

アセンブリ言語の、1行の命令は、
 どうせよ   [何を] [,どのように]
のような文法で、CPUを命令通りに動かします。例えば、

 MOV      R8,R0
 データを転送せよ  R8レジスタへ、R0レジスタから

 PUSH     R0
 スタックへプッシュせよ R0レジスタを

のような言語です。メーカーやCPUによって若干異なった表記の命令語が使われているのですが、基本的な違いはありません。


◆AVRのインストラクション
以下はAVRのアセンブリ言語で使われる命令表(インストラクション・コード)です。
大きく分けると (演算命令、分岐命令、ビット操作命令、転送命令、CPU制御命令)に分かれます。
 K,K6: 定数(8,6ビット定数)
 P: I/Oレジスタ
 Rd: 操作対象の汎用レジスタ (R0〜R31)
 Rr: 参照される汎用レジスタ (R0〜R31)
 X: XH(R27)  XL(R26)
 Y: YH(R29)  YL(R28)
 Z: ZH(R31)  ZL(R30)
 b: ビット(0〜7)
 k: アドレス定数(7,12,16ビット)
 q: 符号なし6ビット定数
 s:ステータスレジスタ(C,Z,N,V,X,H,T,I)

 Flags:実行の結果が反映されるステータスフラグ
 Clock:実行に要するクロック数

演算命令


汎用レジスタを使って演算をする命令です。例えば
 ADD  R0,R3   ; R0 ← R0 + R3
 INC  R4     ; R4 ← R4 + 1
 DEC  R0     ; R0 ← R0 - 1
 CLR  R3     ; R3 ← 0
ANDI のように I が含まれる命令は、イミーディエイト(即値)命令と呼ばれ、レジスタと定数を直接演算をする命令です。


分岐命令


分岐(Branch)命令は、プログラムの流れを変える命令です。

 RJMP  100     ;現在のプログラム・カウンタ値から、100番地 離れた番地へジャンプする(相対ジャンプ)
 IJMP         ; Zレジスタが示すアドレスにジャンプする(間接ジャンプ)
 RET         ; サブルーチンから復帰
 RETI         ; 割込みからの復帰
 SBRC R0,2     ; Skip if Bit in Register Cleared / R0のビット2が0なら、次の命令はスキップ(とばす)。


ビット操作命令



ビット操作命令は、ビット単位で操作する命令です。

 SBI  PORTB,3   ; ポートBのbit 3を 1 にセットする。
 CBI  PORTB,3   ; ポートBのbit 3を 0 にクリアする。
 LSL  R2       ; R2を左へ1bit論理シフト
 ROL  R2       ; R2をキャリーを通して左へシフト
 SEC           ; キャリーフラグをセット


転送命令


転送命令は、データを転送する命令です。元データはそのまま残ります。

 MOV   R0,R1   ; R0 ← R1
 LDI    R2,0x1f  ; R2 ← 0x1f
 LD     R0,X    ; R0 ← Xレジスタで示すアドレスの中身(間接指定)
 LD     R0,X+   ;R0 ← Xレジスタで示すアドレスの中身の転送後Xレジスターをインクリメント
 ST    X,R0    ; Xレジスタで示すアドレスへ ← R0
 IN    R3,PORTB ; R3へポートBから入力
 OUT  PORTA,R2 ; R2のデータをポートAへ出力
 PUSH R2      ; R2をスタックへプッシュ
 POP  R4      ;スタックからR4へポップ


CPU制御命令


 NOP    何もしない(1クロックの短いタイマー)



割込み
割込みは、仕事中にかかってくる電話に似ています。現在している作業を一旦中止して、電話を応答し、要件が終わったらまた、中断していた作業に戻るような仕組みがCPUで実行されます。

割込みが掛かると、実行していた命令の次の命令アドレス(復帰アドレス)はスタックに退避され、
割込み要因により、ベクタテーブル上の対応番地へジャンプします。この番地は、0番地から始まるベクタテーブルと呼ばれています。
例えば、タイマー0のカウンタに桁上げが発生した場合は、006番地に書かれたアドレス(TIM0_OVF)へプログラムが飛ぶことになります。無論、割込みが許可状態になっている必要があります。 そして、TIM0_OVF: に書かれた割込みプログラムが実行されると、最後に書かれた RETI命令で、中断されていたプログラムに戻ることになります。
下記は、ベクタテーブルの一部と、TIM0_OVF: の部分の部分です。

;****ベクタテーブル*****
000     RJMP RESET        ;リセット
001     RJMP EXT_INT0      ;外部割込み0
002     RJMP EXT_INT1      ;外部割込み1
003     RJMP TIM1_CAPT     ;タイマー1キャプチャ
004     RJMP TIM1_COMPA   ;タイマー1比較一致
005     RJMP TIM1_OVF      ;タイマー1オーバーフロー
006     RJMP TIM0_OVF      ;タイマー0オーバーフロー
 、、、
;****リセットプログラム*****
      RESET:            ; 電源ONで開始するプログラムの開始
          、、、、、、

;****タイマ0オーバーフロー割り込み*****
      TIM0_OVF:          ;タイマー0オーバーフロー割込みで実行されるプログラム
       PUSH R0         ; 割込みの中で使用するレジスタの退避
          、、、         ; 必要な作業を実行
          、、、
       POP R0          ; レジスタの回復
       RETI            ;割込みの終了

なお、電源ONの時は、000番地に書かれたジャンプ先(RESET)にプログラムが飛び、ラベル RESET: 以降にI/Oの初期化から始まるプログラムを書いておけば、CPUが立ち上がります。

ところで、AVRの割込みを許可するためには、ステータスレジスタ(SR)の 割込み許可ビット I を1にする必要があります。Iを0にすることで全割込み禁止することができます。
各個別の割込みにも、割込みモードの設定用のレジスタやビットがあります。

一つの割込みが受け付けられている間は、他の割込みは禁止されるのですが、割込みの中で、割込み許可ビット I を1にして、他の割込みを許可する方法もありますが、それなりの注意が必要です。



◆AVR Studio を使ったアセンブラ・プログラミング
AVR studioを使うとフリーでAVRのアセンブラ開発をすることができます。AVR studioとWinAVRを参照してください。AVR studioとWinAVRを導入しておくと、アセンブラだけでなくC言語でも開発をすることができます。

ここではアセンブラの使い方を説明します。

WinAVR4を実行し、 New Project をクリックします。
Atmel AVR Assembler を選び、Location のボタンを押し、フォルダ(例 C:\TEMP)を指定します。
Project name を入力します (例 TEST) 
Finish します。

次にCPU_TEST.asmをコピーして編集画面にペーストすると下のような画面になります。



Build → Build をすると、アセンブラがスタートし、エラーがなければ Assembry complate が表示されます。



注)サンプルプログラムはgeocities.jp/kuman2600/o06assembly.html様を使わせていただきました。


2回目からは、aps ファイルをクリックするだけでファイルをオープンすることができます。
できたHEXファイルをAVRに焼く方法は前記ページを参照してください。
また、AVRアセンブラの詳しい使い方は各種ホームページを参照してください。



◆プログラミング言語と実行速度
アセンブラで少し複雑なプログラムを作り、実際にCPUで走らせてみると分かるのですが、他の言語で記述したプログラムとは比較にならない速度が得られます。無論、プログラムの作り方や、最適化の条件にもよりますが、すこし習熟したプログラマーは短いコードでかつ高速動作するプログラムを書くことができるのです。

しかし、アセンブリ言語で作ったプログラムの最大の弱点は、異なったCPUへの移植ができない点です。また例え、同じCPUを使っていても、一部のモジュールをそのままコピーして使うことは、大きな危険が伴います。

それはアセンブラが、各レジスタを直接操作する言語だからです。レジスタ構成が異なるCPUに使えないのは当然ですが、同じCPUのシステムに、一部のモジュールを移植しようとしても、各レジスタの使われ方が異なっているシステムへ割り込んで行くことは危険極まりない暴挙でもあるのです。

Cは、この点でも優れた言語と言えます。y = f(x1,x2, ,) にみられるように、複数の引数(アドレスも可)を渡し、一つの返り値を使うという自己規制の概念と、括弧の中の関数内でのみ有効な変数を、スタックを使って生成するという技は、Cを世界に広めた原動力かも知れません。

無論Cで書かれたプログラムにも弱点はあって、基本的な約束事を守るためにコンパイル後のコードには、PUSH,POP の山が出来上がってしまいます。汎用レジスタの数が少ないCPUの場合は特にその傾向が強いため、実行速度が思ったほど上がらないのです。まあ、それを割り引いても、Cには大きな魅力があることは確かですが、


CPUは自作できるか?
この問いに答えたのが、「CPUの創り方」渡波郁著 毎日モミュニケーションズ刊 です。実際、この中に発表されている4ビットCPUが動作し、LEDが点滅している様子を動画サイトで見た人も多いはずです。
ただこの本は、著者が冒頭に述べているように、作って楽しむ電子工作本ではなく、CPUの理論を学ぶ工学書でもありません。回路の簡素化のために熟考が重ねられたとは言え、実際に作ろうとなると、配線には相当な苦労が必要で、その結果、完成されるであろう10数ステップのCPU動作に、過大な期待をすると、がっかりするゲーム好きもいる筈です。

 TD4


TD4の構成は
 4ビットレジスタ×2
 4ビット出力ポート(LED)×1
 ALU: 4ビット加算
 クロック: 1Hz / ステップ動作
 4ビット入力ポート(スイッチ)×1
 8ビットROM(スイッチ)×16アドレス(最大)
 命令種:13

というシンプルなCPUで、RAMは無く、当然ながら、サブルーチンCALLも、割込み機能もなく、加算はできますが、減算も論理演算もありません。サンプルプログラムとしての「3分ラーメンタイマー」は16ステップを使い切っています。

しかしながら、このシンプルさ故に、CPU全体を見渡せることができます。特に重要なのはCPUの心臓部であるCommand decoder (図面ほぼ中央) の接続元と接続先が見えています。ROMから命令のコマンドデータが読み込まれると、このデコーダー回路により、目的LOAD先のモジュールがアクセスされることがわかります。

CPU設計の労力のかなりの部分はこのデコーダーを如何に合理的に構築するかでもあります。それは当然、与えられたコマンドを単にデコードできる回路に落とすということではなく、コマンドが持つビットグループ単位のハード動作との関連を深く理解しておかなければなりません。

さて、このTD4ですが、CPUとしての実用性なら、内部バス構成を取った方がいいに決まっているのですが、それでは、初心者は、ほぼ100%自作できません。このCPUは、ALUを演算に使うだけでなく、素通りさせることで、バスとしても使うなど、やや変則技も繰り出していますが、こうした点はCPU設計の通常手法では無いにせよ、あらゆる技を駆使することの重要性を、お手本として示しているようです。(ちょっと褒め過ぎかも知れませんが)

ま、私がどうのこうの言うより、実際この本を手に取って頂いた方がいいことは確かです。ブックカバー無しで持って歩くことが躊躇される装丁ではありますが、著者に敬意を払い、敢えてそうしたいものです!(?)
























                    Top page

inserted by FC2 system