ロボットシステムのコンピュータには、一般的なコンピュータをそのまま適用したもの、あるいは、一般的なコンピュータを部分的に変更したものを使います。そこで、このページでは、一般的なコンピュータの構成を整理しながら、ロボットに関連することに言及していきます。
コンピュータの構成要素
コンピュータを構成する要素をまとめます。大雑把に言いますと、コンピュータは「ハードウェア」と「ソフトウェア」によって構成されます(下図)。
コンピュータの定義は「入出力を備えた計算装置(演算装置)」であり、計算を行うハードウェアが必要になります。ハードウェアの大部分は電子回路という形態をとっており、その機能は固定化されています。現代のコンピュータには、計算内容を柔軟に変更できるという要件が暗に求められており、ハードウェアでは対応できないその要件をソフトウェアが担います。ソフトウェアは、計算内容と計算に使うデータが電子データという形で用意されます。そしてハードウェアは、あらかじめ決められたルールによってソフトウェアの電子データを処理できるように作られています。このようにハードウェアとソフトウェアが一体になってはじめてコンピュータとして機能します。
コンピュータの構成をもう一歩具体化します。ハードウェアとソフトウェアの中身を具体化し、階層構造として下図に示します。
コンピュータのハードウェアの最も重要な部分は「CPU(プロセッサ)」「メインメモリ」「ストレージ」です。そして、その他のハードウェアデバイスと接続するための各種の I/F(インターフェース)が加わります。
ソフトウェアは、大きくは「OS(オペレーティングシステム)」と「アプリケーションソフトウェア」によって構成されます。アプリケーションソフトウェアが、最終的にコンピュータで実現したい機能にあたります。Windows PC であれば、Excel、Web ブラウザ、ゲームソフトなどが例として挙げられます。OS は、これらのアプリケーションソフトが動作しやすくしたり、ソフトを開発しやすくするための基盤ソフトウェアです。OS の例としては、Windows、Mac OS、Linux、Android などが挙げられます。OS は、メモリ管理やファイルアクセスといった、多くのアプリケーションソフトウェアで共通で必要な機能をまとめて提供します。ただし、コンピュータに接続する全てのハードウェアデバイスに必要な機能を全て OS に盛り込むのは困難なため、デバイス固有の機能を「デバイスドライバ」として用意します。
その他のハードウェアデバイスの例としては、マウス、キーボード、ディスプレイ、各種のセンサーやアクチュエータなどが挙げられます。これらのデバイスは、USB や HDMI といった対応のインターフェースによってコンピュータに物理的に接続されます。
なお、ソフトウェアの階層構造は、別のパターンになることもあります。代表的な3つの構造を下図に示します。
真ん中が前出の図と同じ構成です。左は OS を搭載しない構成です。アプリケーションソフトウェアの機能が限定的な場合には、OS を搭載しないこともあります。この構成を「ベアメタル」と呼びます。OS が無い場合は、アプリケーションソフトウェアと CPU・メインメモリの間に HAL(Hardware Abstraction Layer:ハードウェア抽象化レイヤー) というソフトウェアを用意します。これは、OS の核となる部分を切り出したような位置づけのソフトです。アプリケーション開発者が CPU の制御まで含めたプログラムを書くのはなかなか大変なので、HAL が CPU を覆い隠して抽象化する機能を担います。
なお、CPU やメインメモリもハードウェアデバイスの一種ですが、これらと接続するソフトウェアはデバイスドライバとは言いません。HAL は本来であれば CPU に限らずその他のハードウェアデバイス向けのデバイスドライバの中にも含まれる、定義の範囲が広い用語です。しかし、CPU 専用のデイバスドライバに相当する用語がないため、ここでは HAL と表記しました。また、OS を搭載する場合には HAL は OS の中に組み込まれる形になります。
上図の右は OS の上に「ミドルウェア」という階層を加えたパターンです。ミドルウェアは、サーバー向けのコンピュータで良く使われる言葉です。ミドルウェアソフトウェアの機能の例として、Web サーバー機能やデータベースサーバー機能が挙げられます。よく使われる機能なので多くのユーザーが簡単に使えるように共通化しておきたいけど OS に標準的に入れるほどではない、という性質の機能がミドルウェアとして整備されています。ロボット向けでは「ROS」というミドルウェアがあり、世界的に浸透しつつあります。
情報の抽象化
ソフトウェアがなぜこのような複雑な階層構造になっているかというと、情報を抽象化するためです。階層の上流ほど抽象度が高くなるという関係になっています。ハードウェアに近い層では、「抽象化」はハードウェアの個々の違いを意識せずに上流のソフトウェアを記述できるようになることを指します。抽象化しておくと、異なるハードウェアでも同一のソフトウェアコードで動作するようになります。例えば、同じ Intel の core i7 シリーズの CPU 製品であっても、型番が違うと挙動が少しずつ異なります。ソフトウェア開発者はその微妙な違いを意識してコードを微調整するのは大変なので、メーカー側が HAL やデバイスドライバを用意してソフトウェア開発者の負担を減らすという役割分担になっています。(新しく購入したハードウェアをパソコンに接続する際に、ハードウェアのメーカーの Web サイトからデバイスドライバをダウンロードするという作業が発生するのは、こういう状況によるもの。)
アプリケーションソフトウェアに近い層では、「抽象化」は少ないコードで同一の機能を記述できるようにするという意味合いも持ちます。例えば、C 言語よりも Python 言語で記述した方が少ないコードになることが挙げられます。(Python の方が C よりも抽象度が高い。)
また、抽象化の過程で階層を複数に分けるのは、様々な技術要素が絡み合うのを防ぐためです。CPU やメモリなどのハードウェアを効果的に使う技術は OS に任せ、効率的なサーバー機能を実現する技術はミドルウェアに任せ、アプリケーションソフトウェアはユーザーに快適な機能を提供することに集中する、といった感じです。こうすることで、それぞれの階層のソフトウェアの守備範囲が限定され、保守性や拡張性が向上します。
なお、OS、デバイスドライバ、ミドルウェアなどのレイヤーの分け方は、コンピュータが進化するにつれて国際的に徐々に合意形成され、今の形に落ち着きました。最近は仮想化技術の進展に伴い、ハードウェアの直上にハイパーバイザーというソフトウェアのレイヤーを配置して、その上に複数の OS を同時に載せる、という階層構造も登場するなど、変化は続いています。
構成要素の抽象度
コンピュータの構成要素の抽象度を可視化すると次のような図になります。ここでは、基本的にハードウェアの主役は CPU だと思ってください。
下側がハードウェアで上側がソフトウェアです。上に行くほど抽象度が高くなります。情報を抽象化する目的はハードウェアの機種ごとの違いを隠蔽することです。HAL やデバイスドライバといったソフトウェアが直接的にハードウェアの制御やデータの処理を担い、上流のソフトウェアが扱わなくてもすむようにします。(HAL は OS の中に含まれることもありますが、イメージを膨らませやすくするために OS から分離しました。)隠蔽によって、例えば同種のハードウェアの同種の機能であれば同じソフトウェア関数で呼び出すことができるようになったりして、上流のソフトウェアからハードウェアの違いが見えなくなります。
ところで、ソフトウェアは電子データであり、ハードウェアは実体を伴うデジタル回路で、形態が異なります。そこで、両者を結ぶためにコンピュータの開発者はルールを設けています。これを「命令セット」と呼びます。命令セットは、どのような電子データをハードウェアに入力したらどのように制御とデータ処理が実行されるかを決める対応表のようなものです。例えば、整数の足し算を「ADD」という「命令」に、データの読み込みは「LOAD」という命令に対応させる、といった感じです。「ADD」はアセンブリ言語という人間が判別できる形式の表現方法で、実際の電子データは例えば「0010」のように 0 と 1 の機械語が対応します。(アセンブリ言語と機械語は後述します。)
命令セットは CPU によって若干異なるものの、いくつかの標準的な種類のものに集約され、異なる CPU メーカーの間で共有されていたりします。例えば x86 という命令セットは Intel 社と AMD 社で共有されています。(完全に同じ命令セットに対応しているわけではありませんが。)そのおかげで、Windows など OS は CPU ごとに別々に開発する必要はなく、大部分が同じプログラムで流用できています。また、場合によっては命令セットを先に決めておいて後でそれに対応した CPU を設計・製造する、ということもあります。RISC-V という命令セットがその最たる例です。どのような命令セットを用意しておくかでコンピュータの動作の効率が左右され、それがコンピュータアーキテクチャの根幹を成します。そのため、命令セットを整備するだけでも有用であり、時には CPU 本体よりも重要とされます。
HAL やデバイスドライバは、命令セットに含まれる命令を組み合わせてハードウェアの具体的な動作を記述したソフトウェアです。例えば「設定用レジスタ X の値を A という値に書き換える」といった処理のプログラムを記述することを考えます。命令の組み合わせでは「A という値が格納されているメインメモリのアドレスを特定 → CPU 内の一時保管用のレジスタへ値をロード(コピー)→ 設定用レジスタ X のアドレスを指定→ 値を A で上書き」といったプログラムになります。HAL では、この処理を例えば「RegX()」のように 1 つの関数にまとめ、RegX(A)」という 1 行の記述で所望の処理ができるようにしたりします。HAL によって、上流のプログラム開発で記述量を減らすことができますし、レジスタ X のアドレスというデバイス固有の情報を意識せずにすむようになります。
なお、HAL やデバイスドライバ等のソフトウェアは、命令を組み合わせて構成されたプログラム(アセンブリ言語)ではなく、C 言語などの高級言語によって記述することが可能な場合もあります。これは、CPU やマイコンの開発元が、コンパイラやライブラリをどのくらい整備してくれているかによって変わってきます。
OS は下層のソフトウェアで作られた関数や機能を組み合わせて作られます。OS には様々な機能がありますが、大まかな役割は「アプリケーションソフトウェアを効率的に処理できるようにコンピュータのハードウェアリソースを管理する」ことです。コンピュータのハードウェアリソースには、プロセッサの演算速度(時間)、メインメモリの容量、ストレージの容量などがあります。つまり OS は、アプリケーションソフトウェアのためにプロセッサを割り当てたり、メモリの領域を確保したり、ストレージにファイルを書き込んだりします。アプリケーションソフトウェアに付随する「複数のタスク」の全体を見渡し、各タスクにリソースの割り当てを行います。そういう点で、OS は HAL やデバイスドライバに比べてより統合的にハードウェアを隠蔽する働きをします。(OS、HAL、デバイスドライバの間に明確な境界はありませんが。)
OS 搭載のコンピュータでは、アプリケーションソフトウェアは OS の機能を利用して動作します。OS のおかげで、アプリケーションソフトウェアのプログラムを作成する際にコンピュータのリソースをあまり意識せずにすみ、実現したい機能に集中することができます。プログラムは、OS によって用意されている関数や API を組み合わせたり、C 言語などの一般的な文法によって記述したりして作ります。OS 特有の機能は、その OS 向けに用意されたコンパイラが「いい感じに」呼び出してくれます。
ソフトウェアの情報の変換の流れ
ソフトウェア(プログラムコード)がどのように変換されてハードウェアが扱える情報になるか、変換のフローを整理します。下図に全体像を示します。
一般的にプログラム開発者が使用するプログラミング言語を「高級言語」と呼びます。C、C++、Java、Python などが代表的な例です。細かいことを言うと、C、C++、Java は「コンパイラ言語」と呼ばれ、Python は「インタプリタ言語」と呼ばれます。コンパイラ言語は、後述の通り、コンパイル&アセンブルされて機械語に変換される「普通の」言語です。一方でインタプリタ言語は、コンパイルされることなく、コードの1行1行が逐次的に処理されるという性質を持っています。実際には、Python 言語の文字列を入力として動作するように別のソフトウェアが裏で動いていて、あたかもコンパイラ言語と同じように処理の出力が得られるようになっています。これらのソフトウェアは C 言語などのコンパイラ言語で作られており、あらかじめ機械語に変換されています。以上のように一応補足しましたが、変換のフローの全体像を掴む際には、とりあえずコンパイル言語もインタプリタ言語も区別せずに同じようなものと考えて良いのではないかと思います。
変換のフローの中でまずはじめに触れておきたいのは、アプリケーションソフトウェア、OS、デバイスドライバなどのいずれのプログラムも、基本的には同じ変換の流れをたどる、ということです。(一般的には、これらのプログラムは C 言語などの高級言語で書かれています。)ソフトウェアを開発する際には抽象度別に階層に分けますが、情報の変換の仕方には大きく差はありません。いずれのソフトウェアも「コンパイラ」というツール(これもソフトウェア)によって「アセンブリ言語」というコードに変換されます。この変換工程を「コンパイル」と言います。次に、アセンブリ言語を「アセンブラ」というツールを使って「機械語」に変換します。この変換工程を「アセンブル」と言います。なお、アセンブリ言語への変換をスキップして、直接的に機械語に変換されることもあります。この機械語とアセンブリ言語がどういったものなのかを、紹介していきます。
最終的には、ソフトウェアはハードウェア(デジタル回路)で動作するようにしないといけません。そのために、全てのソフトウェアは、ハードウェアが判読できる「機械語」というコードに変換されます。機械語は、1 と 0 の羅列で構成される形式のコードで「バイナリー」とも言われます。(普通の人間にはもはや判別不可能な形式です。)1と 0 という 2 値で表現することで、ソフトウェア側との入出力を担うデジタル回路が動作可能になります。デジタル回路は、例えば 3V と 0V のように 2 値の電圧信号で動作するように作られています。あらかじめ「1」を「3V」に「0」を「0V」に対応させるといった形でルールを決めてデジタル回路を作ったり機械語を用意したりすることで、機械語のコードによってデジタル回路に所望の動作をさせることができるようになります。
機械語コードは、CPU(デジタル回路)内のデコーダ回路に電圧信号として入力されます。デコーダ回路に「0010」のように何 bit かの信号の塊が入ると「機能 A を ON して機能 B と機能 C を OFF する」というように各回路へ伝達される 1 つ 1 つの信号に分解されます。(デコーダ回路は、基本的には真理値表に基づいて動作する組み合わせ回路です。)例えば、「0010」という信号が入ったら演算回路内の加算機能を選択する、「0110」という信号が入ったら演算回路でレジスタのアドレスを1つずらす、といったようなイメージです。(例は適当です。)
また、機械語は CPU への命令の羅列で構成されます。(よほどの天才でない限り)人間が機械語を直接読み解くことは不可能ですので、一般的なプログラムコードは一旦「アセンブリ言語」という人間が読むことができる形式のコードに変換されます。このアセンブリ言語のプログラムをアセンブラによって変換(アセンブル)することで機械語のコードを生成することができます。
人間がアセンブリ言語のコードを作成してアセンブラへ通すこともできます。直接的にハードウェアを意識したプログラムを書くことができますので、処理を高速化したり、少ないメモリで動作したりするようにできます。ただし、C 言語などの一般的なプログラミング言語で開発する場合に比べて、ハードウェアの知識が必要になるという高いハードルがありますし、記述しなければいけないコードの行数が飛躍的に増えますので、この開発手法を採用することは滅多にありません。
一般的なプログラムコードをアセンブリ言語に変換してくれるツールが「コンパイラ」です。コンパイラは、プログラムコードの構文(文章の構造)を解析し、命令に置き換えるという処理をしてくれます。コンパイラは CPU ごとに用意されており、CPU の命令セットの中身を知っています。{ } や ( ) などで囲われるブロック(スコープ)、if 文や for 文などの予約語といった構文を分析し、構造の種類ごとに命令の組み合わせに置き換えてくれるのです。こうしてアセンブリ言語のコードが出来上がります。
なお、アセンブリ言語のコードが十分信頼できる場合には、毎回、人間が確認する必要はありませんので、アセンブリ言語への変換の工程は省略されることもあります。この場合は、コンパイラがアセンブラの役割も兼ねる形になります。
最後に、C 言語のコード、アセンブリ言語のコード、機械語のコードのイメージを載せておきます。C 言語のコードをコンパイル&アセンブルして生成したアセンブリ言語コードと機械語コードを一部抜粋して記載しています。
#C言語のコード
int main(void)
{
int a, b, c;
a = 2;
b = 3;
c = a + b;
return 0;
}
この C 言語コードをコンパイルすると下記のようなアセンブリ言語コードになります。(64 bit マシンでコンパイルしています。)
#アセンブリ言語のコード(一部のみ抜粋)
pushq %rbp
movq %rsp, %rbp
movl $2, -12(%rbp)
movl $3, -8(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
movl $0, %eax
popq %rbp
ret
例えば、movq はデータをレジスタへコピーするという命令で、addl は足し算の命令です。
このアセンブリ言語コードをアセンブルすると下記のような機械語コードになります。(ごくごく一部のみを抜粋しています。)また表記の都合上、途中で改行を入れていますが、実際には改行などの区切りは含みません。
011111110100010101001100010001100000001000000001
000000010000000000000000000000000000000000000000
000000000000000000000000000000000000001100000000
001111100000000000000001000000000000000000000000
010000000001000000000000000000000000000000000000
000000000000000001000000000000000000000000000000
000000000000000000000000000000000001000000111001
000000000000000000000000000000000000000000000000
000000000000000000000000000000000100000000000000
001110000000000000001101000000000100000000000000
000111010000000000011100000000000000011000000000
000000000000000000000100000000000000000000000000
010000000000000000000000000000000000000000000000
000000000000000001000000000000000000000000000000
000000000000000000000000000000000100000000000000
000000000000000000000000000000000000000000000000
110110000000001000000000000000000000000000000000
000000000000000011011000000000100000000000000000
000000000000000000000000000000000000100000000000
000000000000000000000000000000000000000000000000
000000110000000000000000000000000000010000000000
000000000000000000011000000000110000000000000000
000000000000000000000000000000000001100000000011
000000000000000000000000000000000000000000000000
000110000000001100000000000000000000000000000000
000000000000000000011100000000000000000000000000