前章にてLEDをすでに点滅させているということは、ハードウェアを制御するプログラムを皆さんはすでに動かしていることになります。その仕組みを解説していきましょう。
本章以降は組み込み開発に必要な環境構築にてダウンロードしたH8/36064のデータシートとビュートローバーの回路図のドキュメントが必要となります。PDFファイルを開いておくとよいでしょう。
メモリに配置されたレジスタ
ハードウェア制御に欠かせないものがレジスタと呼ばれる特殊なメモリです。プログラムからハードウェアを制御するとはレジスタというメモリへの読み書きにて行います。
プログラムとはCPUがメモリの情報を読み書きし演算を行うことです。このメモリへの読み書きによりハードウェアを制御することができるのです。
レジスタが配置されたメモリ場所の確認
マイコンには固有のレジスタ領域がメモリ上に用意されています。H8/36064マイコンのデータシート「2.1章 アドレス空間とメモリマップ」にメモリの割り当てが記載されています。
このようなマイコンのメモリを表現した図をメモリマップと呼びます。メモリマップを見ることでプログラムメモリや静的メモリの配置場所も確認することができます。
このマイコンではメモリ番地が2Byteで表現されるのがわかる。つまり、ポインタ変数のサイズは2Byteである!
レジスタを使ったハードウェア制御
プログラムからのレジスタアクセス方法
プログラムから具体的にレジスタへどのようにアクセスするのかを見てみましょう。前章で次のプログラムを記述しましたね。
IO.PCR6 = 0x01;
IO.PDR6.BYTE = 0x01;
これこそがレジスタへの書き込み処理となります。
構造体メンバとしてアクセスしているのがわかります。レジスタは特殊なメモリではありますが、このように普通の変数かのようにアクセスすることができます。
iodefine.hによるレジスタ構造の定義
IO.PCR6といった表記によるレジスタアクセスはもちろん定義が存在します。その定義ファイルがiodefine.hになります。中を見てみましょう。
このファイルは統合開発環境HEWが自動生成したH8/36064のレジスタ情報を定義したファイルになります。このファイルを解析する力があればこのマイコンのレジスタに自由にアクセスすることができるようになります。
この定義を読み解くにはマクロ定義、構造体、共用体、ビットフィールドに関する知識が必要となります。これらの用語がピンとこない方はC言語の学習が足りていません。C言語入門編で技術を習得してから進めることをお勧めします。
iodefine.hの読み解き方
iodefine.hは大きく分けてレジスタ構造の型定義部とレジスタの位置情報のマクロ定義部の2つで構成されています。
全体の定義量は多いですがレジスタの種類により個別に定義が分かれているため、IO機能と呼ばれるレジスタ定義を抜粋して解説しましょう。
レジスタ位置のマクロ定義部の意味
iodefine.hの最下部には次のようなマクロ定義が行われています。
#define IO (*(volatile struct st_io *)0xFFD0) /* IO Address*/
IOというマクロ名を0xFFD0という数値に置換しており、構造体st_ioへのポインタへキャストしているのがわかりますね。ではこの定義の意味を解説しましょう。
①:レジスタのメモリ位置
マイコンには様々な機能が搭載されており、機能毎にレジスタがグループ分けされています。IO機能に関するレジスタは0xFFD0メモリ番地に配置してあることを示しています。レジスタはマイコンの仕様上メモリの番地が固定で決められているのです。その数値がこの値です。
H8データシートの「20章 レジスタ一覧」を見ると次のようにレジスタと番地が明確に記載されていることがわかるでしょう。メモリ位置の定義はこの仕様と整合をとっているのです。
②:構造体ポインタへのキャスト
本来①の情報はメモリの番地であるため、このメモリにアクセスすためには型情報が必要になりますね。
ポインタの理解が足りていない方は『C言語 ポインタを使いこなせ【身に付けるための9の極意】』を参照してください。
この構造体ポインタへのキャストはイメージとしては次のものになります。
後述するst_io構造体型を指定のメモリ番地へ重ねるイメージとして捉えてください。このようにすることで、任意のレジスタへのアクセスを構造体メンバを参照することで可能とするのです。
③:ポインタ参照先へのアクセス
プログラム上で皆さんがこれから行うのはレジスタというメモリ領域への読み書きが目的となります。ポインタに対して「*(間接参照演算子)」を指定するとは番地が示すメモリ場所への参照を示しているのでしたね。②で重ねた構造体型に対してメモリアクセスするために「*」を付けているのです。
レジスタ構造の型定義部の意味
それでは位置定義の中でも登場したst_io構造体のレジスタ構造の定義方法を見てみましょう。st_io構造体は定義メンバが多いため、PDR6とPCR6のレジスタ構造の型定義部を抜粋します。
st_io構造体定義の意味
struct st_io { /* struct IO */
//・・・一部省略・・・
union { /* PDR6 */
unsigned char BYTE; /* Byte Access */
struct { /* Bit Access */
unsigned char B7:1; /* Bit 7 */
unsigned char B6:1; /* Bit 6 */
unsigned char B5:1; /* Bit 5 */
unsigned char B4:1; /* Bit 4 */
unsigned char B3:1; /* Bit 3 */
unsigned char B2:1; /* Bit 2 */
unsigned char B1:1; /* Bit 1 */
unsigned char B0:1; /* Bit 0 */
} BIT; /* */
} PDR6; /* */
//・・・一部省略・・・
unsigned char PCR6; /* PCR6 */
//・・・一部省略・・・
}; /* */
レジスタにより共用体、構造体、ビットフィールドを使用するものと通常のメンバ定義で行うといった違いがあるのがわかります。これは各レジスタのアクセス制約によるものなのですが、本章のQ&Aにて解説しましょう。
st_io構造体はこのようにレジスタ情報を仕様と同順に構造体メンバとして定義することでプログラムから簡易的にアクセスできるようにするのです。
PDR6共用体定義の意味
続いてst_io構造体に含まれているPDR6共用体の定義にスポットを当てて解説します。
共用体、構造体、ビットフィールドを皆さん覚えていますか?
忘れてしまった人は次の記事を見てください。
『C言語 構造体 struct【情報のパッケージ化とそのメリット】』
『C言語 共用体 union【メモリをシェアする考え方と使い方】』
『C言語 ビットフィールドを使ったビット単位のパッケージ方法紹介』
ここで登場する共用体の使い方は私が過去の開発経験で唯一効果的と感じた定義方法です。共用体を使用することでPDR6というレジスタメモリへの参照を1Byteのアクセスと1Bit単位のアクセスを両方実現しているのです。
では具体的にどのようにPDR6レジスタにアクセスするのかを示しましょう。
この図はPDR6レジスタのメモリ内容を0x21に書き換えるプログラムです。Byte単位で書き換える方法とBit単位で書き換える方法があり、皆さんが選択できるということになります。
レジスタはビット単位で役割が異なるため、ビットフィールドによるBITアクセスは組み込み開発初級者の方には非常にわかりやすい制御方法を提供してくれます。
レジスタへのアクセス方法はiodefine.hの定義を見ることでわかる!
Q&A:レジスタに関するよくある質問
volatile(ボラタイル)はconstやstaticといった修飾子の仲間になります。volatileは一般的なC言語の書籍などでは解説されないのですが、組み込み開発においてはレジスタ定義の際に必ず出てくる修飾子です。
C言語学習編にてコンパイラについて学びました。コンパイラの一番の仕事は翻訳ですが、翻訳の際に最適化と呼ばれるプログラムコードの改善処理を行います。この最適化により皆さんが作成したプログラムサイズを小さくしたり、無駄な処理を効率化して実行速度を上げたりするのです。
例えば次のプログラムはflg変数の値が1で初期化されているためprintf文の「Hello」が出力されることは絶対にありません。コンパイラはこの状況を判断し、実行されないプログラムを削除したうえで翻訳を行うのです。
しかし、この最適化が時にはプログラムに悪い影響を与えてしまうことがあるのです。volatileはその最適化を抑制(最適化を実施させない)するための修飾子です。
変数定義にvolatile修飾子を付与することでコンパイラに対して「この変数に対する最適化処理は行わないで!」と指示を与えることが可能なのです。
レジスタ定義の際になぜvolatileを指定する必要があるかに関しては本章で語る段階ではないので別の機会としましょう。
一般的な構造体変数であればIOの部分は構造体変数ですが、本レジスタの定義においてはIOは変数ではありません。表記は変数っぽく見えるのですが変数ではないのです。
#define IO (*(volatile struct st_io *)0xFFD0) /* IO Address*/
この定義を見てわかる通り、IOの正体はマクロ定義で置換された番地情報ですね。この定数の番地情報にポインタ機能を利用してst_io構造体の型を被せることでメモリにアクセスしているのです。
このようにC言語は変数を使用せずともダイレクトに任意のメモリ場所にアクセスができるのです。マイコンにおけるメモリの固定番地に配置されたレジスタメモリに直接アクセスできる、これこそがC言語がいまだに組み込み開発の現場で使用され続けている理由なのです。
いいえ、そんなことはありません。まず、iodefine.hですがルネサスエレクトロニクス社の統合開発環境独自の仕組みになります。マイコン基礎編で使用しているH8マイコンとHEWはそれぞれルネサスエレクトロニクス社が提供しています。そのため、ソフトウェア開発者がプログラムをしやすくするため、iodefine.hという仕組みを設けたわけです。
組み込み開発では様々なマイコンを扱うことが多いです。そのため、皆さんがルネサスエレクトロニクス社以外のマイコンを制御しなければならない状況になった場合は、一般的にレジスタの位置や型情報は皆さん自身が定義しなければなりません。大変そうですね。
しかし、iodefine.hの仕組みを知っている人であれば、手作業ではありますが自分でマイコンのデータシートを見ながら位置情報と型情報を同様の仕組みで構築すればよいだけです。そのような意味でもiodefine.hをしっかりと理解することは大変意味のあることなのです。